
前言:
微服务运用事件驱动 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。
exponential backoff 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 時, 我们需注意到总重送需求的数目, 并且深度的思考: 到底是哪些的业务场景, 才值得且必要的去发送重送请求。
我们必需要谨记在心的是:
任意、未经规范的重送请求, 将会对微服务整体架构的可靠性, 带来立即且致命的伤害。
参考资料:
- Microservices in Action; Mogan Bruce, Paulo A. Pereira, 2019
- tenacity; https://github.com/jd/tenacity