通用服务可用性治理手段

通用服务可用性治理手段 幂等接口:当执行RPC请求调用下游服务接口遇到网络超时的情况时.并不知道RPC请求是否已经被下游服务成功处理.超时可能出现在请求处理的多个阶段.1.RPC请求发送超时.此时下游服务并未收到RPC请求.2.RPC请求处理超时.下游服务已经收到RPC请求.但是处理时间过长.3.RPC响应报文超时.下游服务已经处理完RPC请求,但是响应报文超时未恢复.服务无法准确判断RPC请求是否被下游服务成功处理.所以只能假定最坏的情况.下游服务已经成功处理请求.但是服务没有收到响应消息.如果服务要进行重试.下游服务必须保证再次处理同一请求的结果与用户预期相符.可以被重试调用的接口应该满足幂等性.幂等是一个数学与计算学的概念.如果一个函数f使用相同的参数重复执行并获得相同的结果.即满足公式:f(x)f(f(x)).则函数是幂等函数.编程世界里.幂等指的是对于某系统接口.无论同一请求被重复执行多少次.都应该与执行一次的结果相同.满足幂等性的接口称为幂等接口.只有幂等接口可以被安全的重试调用.所有读性质的RPC接口(即读接口)天然都是幂等接口.无论接口执行多少次都不会改变数据.而写性质的RPC接口(即写接口)会改变数据.所以需要查看多次改变数据的结果是否与一次改变数据的结果相同.以在数据库中执行各种写操作的SQL语句为例子.1.在覆盖写操作时.UPDATE table1 SET coll X WHERER col2 Y.无论成功执行多少次.coll列的值都是X.因此它是幂等的写操作.2.在更新操作时.UPDATE table1 SET coll coll 1 WHERE col2 Y.每执行一次都会使coll列的值发生变化.因此它不是幂等的写操作.3.在插入操作时.INSERT INTO table(coll,col2) VALUES(X,Y).执行多次会插入多条重复数据.因此它也不是幂等操作.涉及非幂等写操作的接口可以通过幂等性被设计成幂等接口.如果某接口涉及数据库插入操作.则可以先对数据库的相关数据表设置成唯一键.重复调用此接口时.数据库会报出键重复错误.表示此数据已经被插入.此时.若接口返回成功.此接口就可以成为幂等接口.如果某接口涉及数据库更新操作.则可以借鉴CAS的思想.为行数据引入数据版本号.重新SQL语句..UPDATE table1 SET coll coll 1 WHERE col2 Y AND version X.重复调用此接口时.由于version字段的值已经发生变化.因此最终的SQL语句未命中数据行.即它并未真正执行.接口满足幂等性.以上保证幂等性的方案要求写操作必须是数据库操作.通用性较差.更直接的做法是采用判断请求是否已处理的思路.对于每个请求.使用分布式唯一的ID作为UUID.同时在接口侧保存已处理的请求记录.在请求调用接口时.由接口侧根据请求的唯一标识查询已处理的请求记录.如果找到相应的记录.则说明它处理过此请求.直接返回成功即可.Redis分布式锁:接口使用Redis的SET命令保存已处理请求的UUID.并结合NX参数保证.当且仅当键不存在时才成功写入.否则不写入.1.接口接收到请求后.先尝试在Redis中执行SET UUID NX 命令写入请求的UUID.2.如果Redis写入成功.则证明此请求未被接口处理过.接口可以真正处理此请求.3.如果Redis写入失败.则说明Redis中已写入过此UUID.即此请求已被接口处理过.于是拦截此请求并成功返回.如果接口使用Redis永久保存已处理的请求.那么随着时间推移.Redis空间会很快被占满.更好的做法是使用SET命令并结合EX参数.为每个键设置一定长度的过期时间.比如3600s.SET UUID EX 3600 NX设置过期时间可以有效控制Redis存储空间的占用.过期时间越短.越可以节约Redis存储空间.但是当键过期后.也就无法拦截重复的请求了.因此过期时间也不能太短.过期时间代表了接口对同一个请求保证幂等性的有效期.如上例接口保证了1h内对同一个请求处理的幂等性.Redis分布式锁方案的流程如图.数据库防重表:利用数据库唯一索引的唯一性特点.可以专门创建一个表来保存接口处理过的请求记录.并以请求的UUID作为表的唯一索引.这个表称为防重表.接口在处理请求前.先将请求插入防重表中--如果发生索引冲突.则说明此请求在防重表中已经存在.于是请求被拦截.接口直接返回.数据防重交互的流程如图所示.token:token(令牌)方案也是一种通用的实现接口和幂等性方案.它要求在正式向某服务调用接口之前.先从此服务中获取一个token.然后携带token调用所有接口.1.上有服务先向目标服务申请获取token.2.目标服务生成分布式唯一ID作为token.先保存到Redis(也可以是其他存储系统中).然后返回给上游服务.3.上游服务收到响应信息后.携带token真正调用目标服务的接口.4.目标服务通过在Redis中删除token的方式.来检查请求携带的token是否有效.如果删除成功.则说明Redis中存储了此token.申请token的接口请求是首次访问.于是处理请求.如果删除token失败.则说明Redis从未存储过此token或者token已经被删除.申请token的接口请求可能是重试访问.于是直接返回响应信息.重试时机:业务逻辑错误.即下游服务认为请求不符合业务逻辑而返回的错误.比如下游服务在处理扣款请求时发现用户余额不足.或者某请求包含非法参数.这些都属于业务逻辑错误.服务质量异常错误.即反映下游服务稳定性的相关错误.比如下游服务对请求限流 下游服务拒绝提供服务.或者由于调用下游服务失败率过高请求被熔断.这些都属于服务质量异常错误.网络错误:如请求超时 数据丢包 网络抖动 连接断开等.重试的退避策略决定何时重试请求.无退避策略:请求失败立即重试.线性退避策略:每次请求失败后都等待固定的时间重试.随机退避策略:在一个时间范围内随机选取一个时间等待重试.指数退避策略:对一个请求连续重试时.每次等待的时长都是上一次的二倍.综合退避策略:可以是指数退避策略与随机策略结合的形式.各开源消息中间件在应对消息消费失败时都使用此策略.重试风险与重试风暴:重试虽然可以提高服务质量.但是也会给下游服务带来服务质量危险.假设在用户请求量较大的晚高峰时间.由于下游服务质量不足导致服务负载升高.质量下降.上游服务的请求调用就会出现网络超时或网络断开的情况.这时.如果上游服务决定重试请求.那么就会导致下游服务的负载继续升高.服务质量继续下降.最终拖垮整个服务.重试带来的请求量放大会使下游服务的负载雪上加霜.每个请求的重试次数越多.下游服务负载压力就越大.如果上游服务未限制每个请求的重试次数(无限重试).则等同于请求量被无限放大.对下游服务的影响也会无限大.上游服务应该为每个请求都设置最大重试次数.重试控制:不可重试的请求:1.非关键下游服务:如果某个下游服务不是关键下游服务.应该坦然接受失败.而不是执意重试.是否是关键的下游服务需要结合业务来综合判断.比如在查看某用户个人主页时.更在意的是用户昵称 头像 发布的内容.而用户IP地址的归属地仅仅是一个锦上添花的信息.那么个人页服务在调用地理位置服务遇到失败时可以果断的不在重试请求.而将地理位置服务的容量大方的留给那些更重要的上游服务.2.上游服务的重试请求不在重试:为了有效防止重试风暴.可以在收到上游服务的某请求后.检查这个请求是否是上游服务的重试请求.如果是.则在调用下游服务遇到失败时不在重试请求.防止请求量被级联放大.3.服务质量异常错误:如果服务遇到来自下游服务的限流错误.或者服务内部的熔断器已经将下游服务熔断.则说明下游服务的负载过高.为了不成为压垮下有服务的一根稻草.服务也不重试请求.重试控制:重试请求比通过设置最大重试次数可以有效控制单个请求的重试放大倍数.但是如果有较多的请求被重试.那么下游服务依然会收到大量的重试请求.服务负载也依然会有进一步升高的风险.所以应该对上游服务的整体重试量级进行相应的控制.Google SRE建议每个上游服务都要控制正常请求总数与重试请求总数的比例(重试请求比).如果一段时间内重试请求总数低于正常请求总数的百分之十(即重试请求比低于百分之10).那么只有当某请求失败时才允许被重试.