这是我参与「第五届青训营 」伴学笔记创作活动的第 11 天。 文章主要有核心服务治理(服务发布、流量治理、负载均衡、稳定性治理)与字节治理实践(重试策略与组件)内容。
一 核心服务治理
(一 服务发布
服务发布3大难点
- 服务不可用:服务直接挂掉不可用。用户不能使用,体验最差。
- 服务抖动:服务a到服务b的三个实例都能走流量,但是服务B其中一个实例2挂掉然后去升级,那么这时候就会导致一部分无效流量进入服务B的实力2,但是却无法使用,所以走到那里的那部分流量就会出现抖动,导致有一部分用户能使用,有一部分不能使用。
- 服务回退:如果解决了前面两个问题,那么升级完之后发现有bug。在平时开发过程中,如果发现有bug,一般都不会回退,而是直接几分钟现场修。但是在实际的开发上线过程中,是不可能花几分钟等你修的,一般都会回退到一个可以稳定使用的版本。
服务发布-蓝绿发布
蓝绿发布:这里把服务B分成两个集群,集群1和集群2。我们先断掉集群1,去升级集群1,集群2保持正常使用。然后集群1完成升级确认没有问题没有bug之后恢复使用,再去断掉集群2升级集群2。
特点:简单,稳定,但是更新时候会挂掉一半的服务,所以需要两倍资源才能承受正常的流量。所以一般在非流量高峰期进行升级比较好。
服务发布-灰度发布(金丝雀发布)
灰度发布:首先在服务B新增一个实例。然后使用一段时间发现没有问题之后撤掉一个旧实例。然后再慢慢新增一个实例,发现没有问题之后也会继续撤掉一个旧实例,直到所有实例都被替换成新实例。
特点:比较麻烦,因为要不停的切换实例,更新服务注册中心的服务表。另外最大的问题是,如果更新了很多实力之后(如90%),发现一个严重的bug,那么也要不停的一个个切换回去,耗费相当大的资源和时间。以及存在一定的难度,需要很大的平台支撑才可以做到。
(二 流量治理
治理考虑的要素:可以基于地区、集群、实例、请求等维度,对端到端流量的路由路径进行精确控制。
(三 负载均衡
负载均衡 (Load Balance) 负责分配请求在每个下游实例上的分布,是高并发、高可用系统必不可少的关键组件,目标是尽力将网络流量平均分发到多个服务器上,以提高系统整体的响应速度和可用性。
负载均衡算法:
- 轮询(Round Robin) 算法的策略是:将请求依次分发到候选服务器。如负载均衡器收到来自客户端的 6 个请求,有2个服务器,则(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。
- 源地址哈希(IP Hash) 算法 根据请求源 IP,通过哈希计算得到一个数值,用该数值在候选服务器列表的进行取模运算,得到的结果便是选中的服务器。可以保证指定IP的客户端的请求会转发到同一台服务器上。
- 随机(Random) 算法将请求随机分发到候选服务器。
- 加权随机(Weighted Rando m) 算法在随机算法的基础上,按照概率调整权重,进行负载分配。
- 最小活跃数(Least Active) 算法 将请求分发到连接数/请求数最少的候选服务器(目前处理请求最少的服务器)。
软件负载均衡:
软件负载均衡,可以在普通的服务器上运行负载均衡软件,实现负载均衡功能。目前常见的有 Nginx、HAproxy、LVS。其中的区别:
- Nginx:七层负载均衡,支持 HTTP、E-mail 协议,同时也支持 4 层负载均衡;
- HAproxy:支持七层规则的,性能也很不错。OpenStack 默认使用的负载均衡软件就是 HAproxy;
- LVS:运行在内核态,性能是软件负载均衡中最高的,严格来说工作在三层,所以更通用一些,适用各种应用服务。
(四 稳定性治理
即使平时开发过程没有bug,但是一旦部署到线上服务总是会出问题的,这与程序的正确性无关,因为总会受到各种外界因素的影响和干扰。一般来说,稳定性治理通常使用以下四个方法,保证服务之间的稳定性。
-
稳定性治理方法:
- 限流:如图所示,服务a发送5000个qps到服务B,然后服务B设置了限流限制1000个QPS,拒绝了4000个qPS,所以只有1000个qPS被服务。
- 熔断:如图所示,当服务A发送给服务B发现异常之后,服务a的熔断保护器将不会把连接发送到服务B。但是期间服务仍会不断的偶尔尝试一下连接是否成功。如果连接成功,则恢复到继续发送请求连接。
- 过载保护:如图所示,一般过载指的是CPU或者内存等硬件过载。如果我们服务B的实力机器检测到CPU已经到99%了,就会启动过载保护,从而拒绝掉大部分的连接请求,直到CPU降下来为止。
- 降级:当服务B过载之后,可以把服务币的实力机器降级,降级之后只会接收重要的服务的请求,拒绝普通服务的请求。
二 服务治理实践
请求重试的意义
本地函数调用异常
- 通常没有重试意义
远程函数调用异常
- 网络抖动、
- 下游负载高导致延时、
- 下游机器宕机、
- 本地机器负载高、
- 调度超时、
- 下游熔断/限流
- 重试是有意义的,可以避免偶发性的错误,提高 SLA
重试的意义
- 降低错误率: 假设单次请求的错误概率为 0.01,那么连续两次错误概率则为** (0.01)²,即指数级。
- 降低长尾延时: 对于偶尔出现耗时较长的情况,第二个后的请求有机会提前返回。
- 容忍暂时性错误: 重试可以尽量规避系统暂时性异常 (例如网络抖动)
- 避开下游故障实例: 一个服务中可能会有少量实例故障 (例如机器故障),重试其他实例可以成功
请求重试的难点
-
幂等性
- 是分布式环境下的一个常见问题,一般是指我们在进行多次操作时,所得到的结果是一样的,即多次运算结果是一致的。也就是说,用户对于同一操作,无论是发起一次请求还是多次请求,最终的执行结果是一致的,不会因为多次点击而产生副作用。
- 所以请求从事返回的结果是一样的,频繁请求会浪费资源。
-
重试风暴
-
随着调用链路的增加,重试次数呈指数级上升
-
如图所示,服务a调用服b调用服务器c调用服务d。然后服务D不返回响应,然后请求在每个服务都要重试三次,服务a需要重试三次,服务b也需要三次,所以3×3=9。服务c也需要重试三次,所以333等于27次,所以要频繁的请求服务D共27次,但是服务D挂掉了。所以这27次都是浪费资源。
-
-
超时设置
- 假设调用时间一共1s,经过多少时间开始重试?
- 多久不返回判断为超时合适?重试时间设定为多久合适?很难判断!
- 超时时间会因环境的各种因素的变化而变化,当前环境的时间合适,如果换到其他环境呢?。很难适配!
重试策略
-
限制重试比例
- 设定一个重试比例阈值(例如 1%),重试次数占所有请求比例不超过该阈值.
- 例如,近期一段时间,这个窗口服务的成功次数是1000次,那么我们可以设置重试的比例阈值为1%,即最大重试十次。
-
防止链路重试
- 返回特殊的 status code,表示“请求失败,但别重试”
- 防止链路风暴的解决方案
- 例如,如图。服务D异常,只需要在服务C从事三次。然后我们有一个全局的重试标记位。判断是否已经重试,C重试三次之后,他就会把这个标志全局传递给服务b和服务a告诉他们我已经重试过了,你们不要再从事了,可解决链路重试风暴
-
Hedged Requests
- 对于可能超时(或延时高)的请求,重新向另一个下游实例发送一个相同的请求,并等待先到达的响应。
- 例如我们设置超时时间1s,当达到900毫秒时,大概率这个请求会超时,所以我们就把这个请求发送给另一个最先到达的那个响应。
重试效果验证
-
字节跳动重试组件能够极大限制重试发生的链路放大效应
-
如图所示,如果只能无脑for循环重试的话,到第三层,第四层将会是指数级的从事次数。但是如果使用我们上述的重试策略以及内部的重试组件那么到第三第四层,他还是一个n加常熟级重试次数。极大的优化了性能。