微服务架构演进历史与治理实践 | 青训营

152 阅读7分钟

微服务架构是当前大多数互联网公司的标准架构

  本节课将要来学习

  1. 系统架构的演进历史
  2. 实际中字节跳动的服务治理功能是怎么实践的

微服务架构介绍

  为什么系统架构需要演进?

  • 互联网的爆发性发展
  • 硬件设施 的快速发展
  • 需求复杂性的多样化
  • 开发人员急剧增加
  • 计算机理论及技术的发展

系统架构演变历史

  • 单体架构

    ​​image-20230820232045-ud5dn3g.png

    特点:​all in one process​ ,该架构模式没有对业务逻辑代码实现拆分,所有的代码都写入到同一个工程中里面,适合于小公司开发团队或者个人开发以及用户少量的情况。

    优势:

    1. 性能最高

    2. 项目部署在一个节点上,冗余小

    劣势:

    1. 全部项目集中,对于大项目而言 Debug 困难
    2. 模块之间耦合度高,相互影响,导致可能单个模块崩溃,整个服务下线。与其说是是 all in one​ 其实真面目是 all in boom
    3. 模块分工、开发流程
  • 垂直应用架构

    image-20230820200306-tq17pei.png

    当单体满足不了需求后,便有了垂直应用架构,通过横向的扩充

    特点:按照业务线垂直划分

    优势:

    1. 业务独立开发维护

    劣势:

    1. 不同业务存在冗余
    2. 每个业务还是单体
  • 分布式架构

    特点:抽出业务无关的公共模块

    优势:

    1. 业务无关的独立服务
    2. 服务层的业务可以复用

    劣势:

    1. 服务模块 bug 可导致全站瘫痪
    2. 调用关系复杂
  • SOA 架构

    image-20230820202313-lv4efww.png

    特点:引入面向服务

    优势

    1. 服务注册

    劣势

    1. 整个系统设计是中心化的
    2. 需要从上而下设计
    3. 重构困难
  • 微服务架构

    来到本节课的重头戏,那么微服务有什么特点:

    • 微服务是继SOA后,最流行的服务架构风格之一。

    • 按照微服务对系统进行拆分后,每个服务的业务逻辑都更加简单、清晰。服务之间是松耦合的,模块之间的边界也更加清晰。

    • 微服务有效降低了软件项目的业务复杂程度,为小团队独立开发、持续交付和部署打下了良好的基础。

    • 彻底的服务化

    优势

    1. 开发效率
    2. 业务独立设计
    3. 自下而上
    4. 故障隔离

    劣势

    1. 治理 运维难度
    2. 观测挑战
    3. 安全性
    4. 分布式系统

微服务架构核心要素

  三要素,分别是​​

  • 服务治理

    服务注册
    服务发现
    负载均衡
    扩缩容
    流量治理
    稳定性治理

  • 可观测性

    日志采集
    日志分析
    监控打点
    监控大盘
    异常报警
    链路追踪

  • 安全

    身份验证
    认证授权
    访问令牌
    审计
    传输加密
    黑产攻击

  ‍

字节跳动服务治理实践

  本部分将着眼于字节跳动关于微服务架构非常重要的一环——重试,展开来谈谈。

重试的意义

  在本地函数调用,比如下面的例子

func LocalFunc(x int) int{
	res := calculate(x * 2)
	reutrn res
}

  虽然只是一个简单的函数,但不能保证函数调用次次成功,可能遇到的异常有以下几种情况

  • 参数非法
  • OOM(Out Of Memory)
  • NPE(Null Pointer Exception)
  • 边界 case
  • 系统崩溃
  • 死循环
  • 程序异常退出
  • ........

  但是针对本地函数抛出异常的情况,其实是没有重试的必要。可能这次的错误还会继承到下次重试中。

  然而对于远程函数的调用,也有很多的情况会导致异常,比如之前 在打开抖音会发生什么 ​课程中学习到的,网络传输最后一公里出现的问题较多。

  所以这时候可能导致远程函数调用异常的情况主要围绕在网络,比如 网络抖动​ ,下游负载高导致超时​,下游机器岩机​,本地机器负载高,调度超时
下游熔断、限流​ 等异常情况。

  这时候就有重试的意义。网络问题进行重试可以避免掉偶发的错误,提高 SLA [服务等级协议 Service-Level Agreement,缩写SLA]

  当然除了提高SLA外,重试还有以下作用

  • 降低错误率

    假设单次请求的错误概率为0.01,美那么连续两次错误概率则为0.0001。

  • 降低长尾延时

    对于偶尔耗时较长的请求,重试请求有机会提前返回。

    长尾延时,举几个直观的栗子:

    1. 迅雷的吸血下载,最后可能卡在 1% ,多线程下载总有请求耗时长,这时候你会选择继续等待还是停止后继续下载?

      事实证明,后者可能更有效(重试的好处不就出来了),而如果你选择前者,你等待的这段时间就被称为长尾延时

    2. 你发出了 100个 Redis 请求,99个 1s内响应,唯有一个时间 100s。虽然平均时间很低,但仍有 1% 几率的用户拥有糟糕的用户体验。这 1%就是长尾延时。

  • 容忍暂时性错误

    某些时候系统会有暂时性异常(例如网络抖动),重试可以尽量规避。

  • 避开下游故障实例

    一个服务中可能会有少量实例故障(例如机器故障)重试其他实例可以成功。

  看起来,重试的好处很多,那么是否重试

  • PS: 补充概念 P95 P99 是啥?

    互联网公司在收到用户关于访问慢的投诉之后,一般会进行优化。但也需要测定实际的响应时间,衡量用户响应时间的长短如果单靠 平均值 ,容易受到众数影响

    导致异常的响应时间,在平均值的角度衡量标准下,会被掩盖掉。因此引入 P95 P99 来衡量。

    P95:将所有的响应耗时从小到大排列,处于第 95% 位置的值即为 P95 值。(P99 P99.9同理)

    通过测量 95% 用户的响应时间,并且优化这个值,可以保证大部分用户的使用体验。

重试的难点

  那么重试为什么在实际的运用过程中,却很少见到?这里需要提及三点,着重谈及 重试风暴

  • 幂等性

  • 重试风暴

    image-20230820223657-ir6swhb.png

    A 服务失败假设重试 3次,而 A 每一次服务请求在 B 上也会重试3次,再回报给 A,相当于 B重试了 3 x 3=9 次

    此时 B 下还有服务 C,D....等等,他们各自遵守自己服务的重试次数 For 循环​ 同时他们还会收到独立的可能来自上级服务的第 N 次重试请求

    假设问题可能来自于 D,而我们的重试本意是让 C 服务重试三次,可是为避免抖动每个服务都设置了重试次数,导致重试次数被放大。

    这样服务 D 不仅不能请求成功,还可能导致负载继续升高,甚至直接打挂。

    如果在业务环境中如此,便会引起重试风暴,可能导致原本一些正常的服务因重复发生雪崩,集体服务下线。

  • 超时设置

    该设置多大的值(超时时间)?

重试策略

  得知了以上的重试难点,我们有以下办法来防止以上情况的发生。

  1. 限制重试比例

    设置一个重试比例阈值(eg: 1%),重试次数占所有请求比例不超过该阈值。

  2. 防止链路重试

    image-20230820230007-q721c6p.png

    这种方式是为了防止 重试风暴 情况的发生。

    链路层面的防重试风暴的核心是限制每层都发生重试,理想情况下只有最下一层发生重试,可以返回特殊的 status​ 表明“请求失败,但别重试”。

  3. Hedged requests

    主要针对于可能超时(或延时高)的请求,重新向另一个下游实例发送一个相同的请求,并等待先到达的响应。

    image-20230820230125-yf3cr65.png

重试效果验证

  实际验证经过实践上面的重试策略后,在链路上发生的重试放大效应。

  不难看出采用上述的应对策略 与 For 循环语句对比,尤其超时错误 等待的时间 最高能达到 3.7*n

  image-20230820230353-pkizsus.png