前言:  

微服务运用事件驱动 Sagas, 解决了在分布式架构下维持数据一致性的问题。

另一方面, 我们在设计、开发微服务的时候, 也应该同时必需要思考著:

  • 微服务在不可预期、脆弱的网络架构下, 如何还能保证微服务运维时的可靠性?

如图一所示, API Gateway, 微服务 A, B, C 的可靠性都是 0.99。所以, 产品整体的可靠性是: 0.99*0.99*0.99*0.99 = 0.96

  • 产品整体的可靠性; 0.96; 是低于个别的微服务的可靠性的; 0.99。
  • 微服务越多, 产品整体的可靠性就越低。

图一

如图二所示, 微服务间存在著依赖 (调用) 的关系:

  • 微服务 A 依赖(调用) 微服务 B、微服务 C。
  • 微服务 B 依赖(调用) 微服务 D、微服务 F。
  • 所有微服务的可靠性是: 0.99。
  • 基于某个原因微服务 D 的可靠性从 0.99 降至 0.95。
  • 微服务 D 的可靠性降至 0.95, 因而导至:
    • 微服务 B 的可靠性从 0.97; 0.99*0.99*0.99; 降至 0.93; 0.99*0.95*0.99。
    • 微服务 A 的可靠性从 0.97; 0.99*0.99*0.99; 降至 0.91; 0.99*0.93*0.99。
  • 微服务依赖 (调用) 越多其他的微服务, 就会有更大的概率, 使其自身的可靠性越来越低。
图二

从图一,  图二的例子, 我们知道:

  • 当微服务越来越多、微服务间的依赖 (调用) 越来越多时, 则将会有更大的概率, 使得微服务自身的可靠性、产品整体的可靠性, 越来越低。

所以, 在设计、开发微服务的时候, 不仅仅是需考量微服务的粒度 (边界)、如何维持微服务间的数据一致性, 也应同时需考量如何保证微服务、产品整体的可靠性。

在保证微服务、产品整体的可靠性的解决方案上, 我们有:

  • Exponential back-off retries
  • Fallbacks
  • Timeouts
  • Circuit breakers
  • Communication brokers

本文中我们将先交流: Exponential back-off retries。

本文:

如图三所示, 微服务 A, 微服务 C, 都分别部署在二个节点 (Nodes) 上:

  • API Gateway 每秒会送出 1000 次请求。
  • 经由负载均衡的机制, 每个节点上的微服务 A, 每秒会接收到 500 次请求。
  • 每个节点上的微服务 A, 每秒会送出 500 次请求。
  • 经由负载均衡的机制, 每个节点上的微服务 C, 每秒会接收到 500 次请求。
[图三]

如图四所示, 某个节点上的微服务 C 故障了; 只好由另一个节点的微服务 C 来独自接收每秒 1000 次的请求。

图四

因为, 只剩下单一节点上的微服务 C 可提供服务, 所以, 由微服务 A 送到微服务 C 的请求, 将会有部分的失败; 而使得微服务 A 需重送请求到微服务 C。如图五所示, 微服务 A 每秒将重送 100 次请求到微服务 C; 微服务 C 由原来的每秒接收 500 次的请求, 因为, 另一个节点的微服务 C 的故障, 而需每秒接收 1200 次的请求。

图五
  • 微服务 C 每秒所需接收的请求, 只会越来越多

如图六所示, 当微服务 C 每秒所接收的请求增加, 由微服务 A 送到微服务 C 的请求, 失败的次数就会越多, 因而使得微服务 A 重送到微服务 C 的请求也就越多, 也就造成了微服务 C 每秒所需接收的请求, 也就越来越多…最终, 微服务 C 也就过载了, 也就崩溃了。

图六
class ProductDataClient(object):

    base_url = 'http://product-data:8000'

    def _product_request(self, url):
        response = requests.get(f"{self.base_url}/{url}", headers={'content-type': 'application/json'})
        return response.json()

    @retry(stop=stop_after_attempt(3),
           before=before_log(logger, logging.DEBUG))
    def all_products(self):
        return self._product_request("products")

所以, 我们不能使用如上述代码一样的实现方式; 只是在获取产品信息失败后, 简单的重送三次的请求。

为了减少微服务每秒所会接收到的重送的请求数, 我们采用了exponential back-off retries 的方法:

在两个重送请求之间加入了等待的时间; 在送出重送请求后, 将等待 2^X * 1 秒后, 才会再送出下个的重送请求; 等待的时间从 4 秒开始, 最多等待 10 秒。

@retry(wait=wait_exponential(multiplier=1, min=4, max=10),
           stop=stop_after_delay(5))
    def all_products(self):
        return self._product_request("products") 

Exponential back-off retries 的作法上, 还有个问题:

  • 在某个的瞬间, 有著几十个的微服务在调用著某个微服务 X 时, 却同时的都发生失败。所以, 这几十个的微服务几乎同时将会对微服务 X 发出重送请求; 而使得微服务 X 每秒所必需接收到的请求数爆增。

要解决上述的问题也很简单:

  • 只需在两个重送请求之间的等待时间, 再加上个随机等待的时间; 加入介于  0 到  1 秒的随机等待时间。
@retry(wait=wait_exponential(multiplier=1, min=4, max=10) + wait_random(0,1),
           stop=stop_after_delay(5))
    def all_products(self):
        return self._product_request("products") 

结论:

在微服务的架构下, 当某个微服务有断断续续提供服务异常的状况下,  Exponential back-off retries 是个相当有效的解决方案。

但是, 在使用 Exponential back-off retries 我们需注意到总重送需求的数目, 并且深度的思考: 到底是哪些的业务场景, 才值得且必要的去发送重送请求。

我们必需要谨记在心的是:

任意、未经规范的重送请求, 将会对微服务整体架构的可靠性, 带来立即且致命的伤害。

参考资料:

  1. Microservices in Action; Mogan Bruce, Paulo A. Pereira, 2019
  2. tenacity; https://github.com/jd/tenacity

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据