微服务架构让边界设计良好的服务的失效互不影响成为可能。
和所有的分布式系统一样存在网络、硬件、应用级别的问题。
微服务架构的风险
用网络通信代替内存中的方法调用,延迟高、复杂性高。
微服务的好处是团队可以单独设计、开发、部署服务。相应的,所依赖的服务也不受团队成员控制。
需要牢记在心的是:微服务可能会临时失效(不管是配置出错,还是别的什么)
优雅地退化服务
一般情况下,一个服务失效并不会影响其他服务。比如图片上传服务失效,但是浏览图片的服务未失效。
但是互相依赖的服务,如果没有适当的故障转移(failover)逻辑,就可能全部失效。
管理“变更”
Google的站点可靠性团队发现大约70%的服务中断是由“变更”引起的。
实现一个管理“变更”的策略和自动部署系统。
举例:新代码在实例的子集中逐步的实施,并监控它们。如果在关键指标上有副作用,自动回滚到原来的版本。
举例:运行两个生产环境,新代码上线时先部署在一个里,只有在确信新版本运行正常后才把负载均衡指向新版本。这叫“蓝绿部署”或“红黑部署”。
回滚代码不是坏事。不要把问题代码留在生产环境然后思考到底哪里出了问题。只要有回滚的必要,那就回滚,越快越好。
健康检查和负载均衡
负载均衡要自动地跳过不健康的实例。检查健康度可以通过轮询/health
端点,或者实例自己上报状态。
现在的服务发现方案已经可以不断地收集健康状况并把负载均衡的流量分配到健康的组件。
自我治愈
“自我治愈”就是一个应用通过必要的步骤,从失效状态中恢复。通常是一个外部的监视系统,发现实例长时间处于失效状态时,重启它。
不过有时也会陷入不断重启服务的问题。比如数据库链接失败,这时会比较复杂,需要很多额外的逻辑来捕获边界情况,让外部监视系统知道此时不必立即重启实例。
故障转移缓存
有时网络故障或系统变更会引起服务中断,好在有了负载均衡和自我治愈,这些中断都是短暂的。
故障转移缓存通常使用两个不同的到期时间,短的那个告诉你通常情况下你可以使用多久的缓存;长的那个告诉你在失效时你可以使用多久的缓存。
要注意的是只有在“过期的数据比没有数据好”这种情况下才能使用故障转移缓存。
使用标准的HTTP头就能设置缓存和故障转移缓存。max-age
指定缓存的最大有效时间。stale-if-error
指定在失效状态时缓存的服务时间。
大多数CDN和负载均衡都能提供这样的功能。
重试的逻辑
要小心的使用重试,有时候大量重试不但不能恢复服务,还会使状况更糟。
在分布式系统中,重试会出发很多其他的请求,并带来级联效应。要最小化重试带来的影响,要限制重试的次数,并用指数退避算法来加大重试的间隔,直到达到最大重试次数。
当重试是由客户端(浏览器、其他微服务等)发起的,并且客户端在处理请求的前后并不知道操作失败了,这时要让应用幂等地处理重试。所谓幂等就是多次操作和一次操作的结果是一样的。比如:用户重试一个购买操作,应用不能对这个顾客收费两次。对每一个事务使用幂等令牌能帮你处理这种问题。
速率限制器和负载卸载器
速率限制就是按时间段对用户所能发起和处理的请求数目做限制的技术。举个例子:有了速率限制,就能找到哪些用户和微服务达到了流量峰值;或者你可以确定你的应用没有过载。还可以对低优先级的流量进行控制,使重要事务得到足够的资源。
还有一种并发请求限制器。
一个fleet usage load shedder能够保证重要的事务总是有足够的资源可以使用。它保留一些资源给高优先级的请求,并且不允许低优先级的事务使用全部的这些资源。负载卸载器不单单靠单一用户的请求数量的大小做判断,而是基于整个系统的状态。负载卸载器能帮你恢复系统,因为在有冲突时,它能保持核心功能正常工作。
快速而独立地失效
使用舱壁模式(bulkhead pattern)将问题隔离在服务的级别。
组件要快速失效,而不是等到组件超时并破坏了应用实例后才失效。没有什么比迟迟收不到回应和一个无响应的UI更让人失望了。
为每一个服务设置一个超时时间,不是一个好方法。因为你基本上不能给出“好的”超时时间。这是个反模式,要避免。
使用依赖于对操作成功/失败的统计的断路器模式(circuit-breaker pattern)来代替使用超时时间。
舰璧
舰璧就是用来把船体分成一个一个部分,当其中一个破裂,就可以把这个破裂的部分封闭起来而不影响别的部分。
舰璧的概念可以应用到软件开发中来隔离资源。这样就能保护有限的资源不被耗尽。比如:我们有两种操作要连接一个限制了连接数的数据库,使用两个连接池要好过共用一个。当一个操作超时或被滥用时,不会造成两个操作都不可用。
断路器
可以用超时机制限制操作的间隔。当我们在一个高度动态的环境时,这是个反模式,因为你几乎不可能为每一种情况都给出恰当的超时时间。
断路器模式的名字来源于电路系统中真实组件,它们有相同的行为。当一个特定类型的错误出现多次时,断路器就开启了。开启断路器会防止更多这样的请求被生成(就像真实的断路器阻断电流那样)。一般在一定时间后断路器会自动关闭,给底层服务足够的时间去恢复。
记住:不是所有的错误都必须触发断路器。例如:你想给客户端发送一个4xx
的响应码,但是附带一个5xx
的服务器端错误。
有些断路器还可以有一个半开启的状态。这时服务先发送一个请求去检查系统是否是可用的,如果这个请求成功了,断路器就关闭使流量流通,否则断路器保持开启。
失效测试
你要不断地对系统常见的问题进行测试,以使你的服务能够在多种失效情况下继续存活。要让团队成员时刻准备好面对突发事件。
怎么测试呢?可以使用一个外部的服务按组标记服务实例,随机地中断分组中的一个实例。你可以准备一个单点失效,也可以停止一个区域的实例来模拟云供应商运行中断。
Netflix的ChaosMonkey是个不错的测试方案。
结论
实现并运行一个可靠的服务并不容易。这需要花费很多精力和金钱。
“可靠”也有很多级别和方面。找到适用于团队的最佳方案,分配足够的预算和时间。
关键点
- 动态的环境和分布式系统会有很高的几率失效,比如微服务。
- 服务的失效应该是独立的,能够优雅的“退化”,以提高用户体验。
- 70%的中断是由变更引起的,回滚代码不是坏事。
- 让失效快速且独立。团队成员对依赖的服务没有控制权。
- 类似缓存、舰璧、断路器、速率限制器等架构模式和技术能帮你构建可靠的微服务。