别让一个慢目标拖垮枢纽
目标
用断路器和按目标划分的舱壁,使一个缓慢或已死的目标无法拖垮整个枢纽。
为什么重要
超时只决定一笔要等多久。如果持续调用已死的目标,等待的线程就会堆积,枢纽先窒息,连无关的交易也一起停住。必须为每个目标各自设置快速拒绝的装置,故障才不会蔓延。
步骤
- 在
/root/eaimw/breaker/breaker.py中制作CircuitBreaker(failure_threshold, reset_timeout, clock=time.monotonic)。state(CLOSED·OPEN·HALF_OPEN)、allow()、record_success()、record_failure()。连续失败达到阈值就变为 OPEN,OPEN 时allow()为 False。时刻必须通过clock()读取(评分器会传入假时钟)。 - 在 OPEN 状态下,经过
reset_timeout之后,allow()只返回一次 True,并变为 HALF_OPEN。试探调用成功则 CLOSED,失败则再次 OPEN(时间从头开始)。即使多个线程同时询问,也只允许一笔(锁)。 - 以
cp /opt/lab/fixtures/eaimw/breaker/relay_base.py /root/eaimw/breaker/relay.py开始,加入--fail-threshold(默认 5)和--reset-timeout(默认 10)参数,并用断路器包裹对目标的调用。打开时不调用,立即返回 E904。这一步中,把不是 0000 的结果算作失败。 - 设置
--max-inflight(默认 10)并发处理上限。上限已满时不等待,立即返回 E905。 - 把断路器和并发处理上限按目标(CORE、CLAIM)分别设置。账户系统的故障或饱和不能蔓延到理赔交易。
- 在
--breaker-log(默认/root/eaimw/breaker/breaker.log)中,每当状态变化,就留下一行<시각>|<대상>|<FROM>-><TO>(占位符依次为时刻、目标)。 - 把算作失败的范围缩小到系统错误(E500·E901·E902)。业务拒绝(B2xx)是目标健康地作答,所以算作成功。
参考
- 并发处理上限:
threading.BoundedSemaphore(n)的acquire(blocking=False)如果为 False,就说明上限已满。结束后必须release()(try/finally)。 - 状态转换记录:比较
allow()前后和record_*()前后的state,就能知道变化的瞬间。 - 账户系统夹具开关:
curl -XPOST localhost:9201/_ctl -d '{"mode":"fail"}'(500)、"slow"(延迟)、"normal"。统计/_stats的calls·max_inflight。 - 常见错误:整个枢纽只有一个断路器;让超出上限的请求排队等待;在 HALF_OPEN 时允许多笔;直接调用
time.time()而无视假时钟。
连续失败累积就打开回路
让 /root/eaimw/breaker/breaker.py 的 CircuitBreaker 在连续失败达到阈值时变为 OPEN,并且在 OPEN 时 allow() 为 False。
成功时把失败数归零,这样统计的才是“连续”。打开的时刻要用 self.clock() 而不是 time.time() 来记录——评分器会掌握时钟。
用一笔试探,然后关闭
在 reset_timeout 之后的 HALF_OPEN 中只允许一次试探调用,成功则 CLOSED,失败则再次 OPEN。
在 allow() 中,如果处于 OPEN 且时间已到,就变为 HALF_OPEN,并设置一个标记来表示试探调用是否已经发出。请在锁内判断,使两个线程同时询问时,只有一方得到 True。
打开时不调用
复制 relay_base.py,用断路器包裹对目标的调用,打开时立即以 E904 答复(--fail-threshold、--reset-timeout)。
handle_tx 就是要包裹的位置。如果 allow() 为 False,就不调用 call_target 而直接返回,调用之后则根据结果调用 record_success/record_failure。
超出上限就不等待
设置 --max-inflight 并发处理上限,上限已满时立即以 E905 答复。
如果 BoundedSemaphore 的 acquire(blocking=False) 为 False,就说明上限已满。调用之后,即使出现异常也要用 try/finally 确保 release。
按目标划分舱壁
把断路器和并发处理上限按 CORE、CLAIM 目标分别设置,使账户系统的故障、饱和不会蔓延到理赔。
把原来各设一个的做法,改成以目标名称为键的字典。如果只有一道舱壁,一个舱里的水就会灌满整艘船。
记录状态转换
每当状态变化,就在 --breaker-log 中留下一行“时刻|目标|FROM->TO”。
比较 allow() 前后、record 前后的 state,如果不同就写一行。多个线程会写,所以要加锁再写。
业务拒绝不是失败
把算作失败的范围缩小到 E500、E901、E902,B2xx 则算作成功。
余额不足是账户系统健康地作出的答复。如果把它算作失败,仅仅因为月底的业务拒绝,就会切断通往正常账户系统的路。