亿级流量系统架构设计与实战(六)
微服务架构与网络调用
当某个业务从单体服务架构转变为微服务架构后,多个服务之间会通过网络调用形式形成错综复杂的依赖关系。
在微服务架构中 , 一个微服务正常工作依赖它与其他微服务之间的多级网络调用。
网络是脆弱的 , RPC 请求有较大的概率会遇到超时 、 抖动 、 断开连接等各种异常情况 , 这些都会直接影响微服务的可用性。
上游下游
当微服务 A 是微服务 B 的调用方时 , 我们称 B 是 A 的下游服务 , 而 A 是 B 的上游服务 。
- 如上图,内容列表服务是个人页服务的下游服务,内容列表服务、内容服务、关系服务是计数服务的上游服务。
解决影响服务可用性的问题,从以下两方面考虑
- 容错性设计:
- 要接受网络脆弱与下游服务质量不可靠的事实 , 并在进行服务设计时充分考虑相应故障产生时的容错方案 。
- 流量控制:
- 既要对上游服务调用采取预防性策略,防止打垮我们的服务;
- 也要对下游服务有感知与保护意识 , 当感知到下游服务的质量下滑甚至服务不可用时 ,及时通过自身保护下游服务。
重试与降级是容错性设计的具体表现形式。
熔断、隔离与限流则是流量控制的常见实现方式。
重试
对于服务间 RPC 请求遇到网络抖动的情况,最简单的解决办法就是重试。
幂等接口
超时可能出现在请求处理的多个阶段:
- RPC 请求发送超时,此时下游服务并未收到 RPC 请求 。
- RPC 请求处理超时,下游服务已经收到 RPC 请求,但是处理时间过长 。
- RPC 响应报文超时,下游服务已经处理完 RPC 请求,但是响应报文超时未回复。
服务器是无法判断 RPC 请求是否被下游服务成功处理,只能假定最坏的情况:下游服务已经成功处理请求 ,但是我们的服务没有收到响应信息。
如果我们的服务要进行重试,那么下游服务必须保证再次处理同一请求的结果与用户预期相符。
怎样才算与用户预期相符呢 ?
举一个电商产品下单服务的例子。 用户选择购买价格为100 元的产品时 , 下单服务会调用用户账户服务的扣款接口 , 从用户的余额中扣除 100 元 。
假如用户账户服务已经成功从用户的余额中扣除 100 元,但是对下单服务请求的响应超时 ,这时下单服务将重试调用扣款接口 。 如果用户的余额再次被扣除 100 元 , 即用户实际支付200 元才下单了这个价格为 100 元的产品 , 那么这就不是与用户预期相符的情况 。
理论上 ,无论重试调用扣款接口多少次 , 用户的余额最终仅应该被扣除 100 元 。
幂等
幂等:幂等指的是对于某系统接口,无论同一请求被重复执行多少次,都应该与执行一次的结果相同。
可以被重试调用的接口应该满足幂等性,只有幂等接口可以被安全地重试调用。
- 读性质的 RPC 接口 ( 即读接口 ) 天然都是幂等接口, 因为无论读接口执行多少次都不会改变数据 ;
- 写性质的 RPC 接口 ( 即写接口 ) 会改变数据 , 所以需要查看多次改变数据的结果是否与一次改变数据的结果相同 。
- 以 SQL 语句为例:
- 覆盖操作:
UPDATE table1 SET col1 = X WHERE col2 = Y
,执行多次语句 col1 仍为 X,是幂等操作。 - 更新操作:
UPDATE table1 SET col1=col1 + 1 WHERE col2 = Y
,每执行一次 col1 都会发生变化,不幂等。 - 插入操作:
INSERT INTO table1 (col1,col2) VALUES(X,Y)
,执行多次会插入多条数据,不幂等。
涉及非幂等写操作的接口可以通过幂等性被设计成幂等接口。
数据库操作设计幂等
插入操作:对数据库的相关数据表设置唯一键。重复调用此接口时,数据库会报出 “ 键重复 ” 错误 , 表示此数据已经被插入。
更新操作:借鉴 CAS ( Compare And Swap , 比较与替换)的思想 , 为行数据引入数据版本号 , 重写 SQL 语句如下 :UPDATE tablel SET col1 = col1 + l WHERE version = X AND col2 = Y
通用幂等设计
对于每个请求,使用分布式唯一 ID 作为 UUID,同时在接口侧保存已处理的请求记录。
请求调用接口时,由接口侧根据请求的唯一标识查询已处理的请求记录,若找到,说明已经处理过,直接返回即可。
Redis 分布式锁
接口使用 Redis 的 SET 命令保存已处理请求的 UUID,并结合 NX 参数保证:当且仅当键不存在时才成功写入 , 否则不写入。
- 接口接收请求后 , 先尝试在 Redis 中执行 SET UUID NX 命令写入请求的 UUID 。
- 如果 Redis 写入成功 , 则说明此请求未被接口处理过 , 接口可以真正处理此请求