咦?panic啦
如果你是学院派,玩微服务必须dubbo,zookeeper不离手,业务保证100%可用,那本文就不是为你写的啦。
由于微服务间采用网络通讯,“错误”是难免的。在传统的进程中,除了太阳黑子爆发导致硬件寄存器出错(参见一个经典案例),调用函数几乎是不存在失败的。但微服务不同,产生“错误”主要有以下几种情况:
- 网络间发生抖动
- 某个服务发生异常,导致依赖它的服务异常
- 某个服务正在重启
重试一下
上文说到,所有的微服务都必须保证:内部一致,外部幂等。这是重试失败请求的保证。
在实践中遇到网络错误,如果是GET请求就可以毫不犹豫的重试,前提是:GET方法不能对持久数据有任何影响。
for i in range(5):
# 重试5次,看你能不能成功
try:
ret = await fetch_json(url)
...
except:
await sleep(1)
continue
像这样丑陋的代码,甚至可以直接写到fetch_json这样的底层工具上。大胆放心的用吧。
如果是post的请求失败,那情况就会复杂很多。此时,需要在服务设计之时就保证内部一致性和幂等性,也就是:
- 如果B服务接收到A服务的请求,那B服务就一定能执行完毕。
- 在一个session中,无论A调用多少次B服务的接口,B服务都只响应一次
保证一致性需要从业务具体分析,但保证幂等非常简单。引入flow_no流水号即可。
- a服务调用b服务,生成唯一流水号并传给b, flow_no=123
- b服务得到响应,将123设置为finished
- a服务网络错误,重试,使用相同的流水号 , flow_no=123
- b 响应前先查询流水号,发现123已被使用,返回“已完成”错误码(看需求,也可以返回之前计算结果)。
graceful killer
当服务A重启的时候,假如A还有任务尚未完成,那SIGTERM信号可能会直接打断这次任务。这会造成难以挽回的数据不一致。
大部分网络框架都提供了graceful killer的功能,让进程安全的退出。但必须警惕自己实现的“生产”“消费”模式。确保每个进程都能优雅的退出。类似这篇文章《python:优雅的退出程序或重启服务》
使用rabbitmq或redis等高速持久化队列,而不是将任务信息保存在内存队列中是一个比较好的做法。
recover与中间状态
微服务数据一致性的问题,总的来说是一个可信发布的问题。当上面简单的策略都无法完成业务的需求,那就只能启动容错。
比如完成操作A实质上需要进行B,C,D三个操作:
-> B
A -> C
-> D
此时需要有一种机制,当B,C,D中任何一个操作没有完成,则将A操作标记为失败。同时启动容错机制。
比如以下提现业务:
- 用户账号 Coin 扣除 500金币
- 调用第三方接口,给该用户打钱0.5元
- 插入一条提现日志到withdrawlog
此业务需要调用3个服务:coin,wetchat,withdraw。更何况支付服务是第三方的,根本无法控制。
所以,在进入提现流程前,需要对这个任务进行跟踪,并记录中间状态。具体上:
- 进入提现 创建 monitor_log , status = withdraw_pre_doing
- 锁定该用户的金币,此时用户不能再进行金币相关操作,确保第三方账户里有足够的钱。
- 用户账号 Coin 扣除 500金币 成功: status = withdraw_already_pay_coin
- 调用第三方接口,给该用户打钱0.5元 成功: status = withdraw_already_pay_wx
- 插入一条提现日志到withdrawlog 成功: status = withdraw_done
这种设计,实质上是基于消息的分布式事务,它可以保证最终一致性。实作中会每隔一段时间启动monitor的回滚,选取所有中间状态不为done的monitor。然后调用各服务对应的容错操作,在容错操作中,会读取monitor记录的信息,使用合适的方式使数据一致。并且,所有被标记为数据损坏的用户,在数据一致前都不能有进一步的操作。艰苦的过程就要来临。
注:要实现monitor,redis的天然原子操作和高性能绝对是不二之选。
注:由于保证服务内部的ACID,并且锁定了资源(coin和第三方账户)。容错操作很可能只是给失败的服务重新发送了一次请求。