下游服务变慢或挂掉时,上游继续硬调,只会让自己的线程一个个被耗尽,最后连本来健康的功能也一起挂——这就是级联故障。熔断器的思路是:失败太多就暂时「断开」,直接拒绝请求,等一会儿再放一个探针看看好了没有。这篇自己实现一个,让它在真实线程池和真实计时下工作,不光看好处,也把它的代价量出来。
熔断器(核心) class Breaker: def call(self, f): with self.lock: if self.state == "open": if now - self.opened_at >= self.reset_timeout and not self.probing: self._set("half_open"); self.probing = True # 冷却到了: 放一个探针 else: raise Open() # 否则直接拒绝 elif self.state == "half_open" and self.probing: raise Open() try: r = f() except Exception: # 失败: half_open → open;closed 累计到阈值 → open ... # 成功: half_open → closed;closed 里失败计数清零
参数:连续失败 5 次就打开、冷却 0.5 s。下游模型:0.5–1.5 s 是故障期,每次调用要等 0.15 s 才抛出超时错误;其余时间 5 ms 内成功。请求由 8 个工作线程的线程池处理。
状态转换日志(相对开始的秒数):
0.743 s closed → open
1.381 s open → half_open
1.531 s half_open → open
2.041 s open → half_open
2.047 s half_open → closed完整的循环都出现了:closed → open(故障开始约 0.2 s 后累计 5 次失败)→ half_open(冷却期满,放行探针)→ 因为下游还没恢复,探针失败又回到 open → 再冷却 → half_open → 探针成功 → closed。注意时间点:下游 1.5 s 就恢复了,但第一次探针发生在 1.38 s(还在故障期,失败),第二次要到 2.04 s——熔断器不会立刻发现恢复。
无熔断器: 真正打到下游 150 次 | 成功 100 | 下游报错 50 | 快速失败 0 | 故障期内请求的延迟 中位 155.0 ms 最大 155.1 ms | 故障恢复后 1 秒内请求的延迟 中位 6.3 ms 最大 6.5 ms | 全部处理完 2.99 s 有熔断器: 真正打到下游 86 次 | 成功 73 | 下游报错 13 | 快速失败 64 | 故障期内请求的延迟 中位 0.1 ms 最大 203.8 ms | 故障恢复后 1 秒内请求的延迟 中位 0.2 ms 最大 6.3 ms | 全部处理完 2.99 s
故障期间,无熔断器时每个请求都老老实实等 0.15 s 下游超时才失败(中位 155 ms),150 次调用全部打到下游;有熔断器时 64 个请求被快速失败(中位仅 0.1 ms),下游只收到 86 次。但要看代价:在这个速率下线程池(8 个)并没有被耗尽——50 请求/秒 × 0.15 s ≈ 7.5 个并发,刚好够——所以无熔断器组并没有出现级联,只是每个失败请求慢了一些。熔断器反而在下游已经恢复后的那 0.5 秒里,让本可以成功的 25 个请求快速失败(见下面的窗口表)。
0.0–0.5 s 有熔断器 {'ok': 25} 无熔断器 {'ok': 25}
0.5–1.0 s 有熔断器 {'fail': 12, 'fast_fail': 13} 无熔断器 {'fail': 25}
1.0–1.5 s 有熔断器 {'fast_fail': 24, 'fail': 1} 无熔断器 {'fail': 25}
1.5–2.0 s 有熔断器 {'fast_fail': 25} 无熔断器 {'ok': 25}
2.0–2.5 s 有熔断器 {'fast_fail': 2, 'ok': 23} 无熔断器 {'ok': 25}
2.5–3.0 s 有熔断器 {'ok': 25} 无熔断器 {'ok': 25}「有熔断器」那一列在 1.5–2.0 s 全部是 fast_fail,而「无熔断器」已经在 1.5 s 恢复了全部成功。这是熔断器固有的取舍:用一段「本可以成功的失败」换取故障期间对下游和自己资源的保护。冷却时间(reset_timeout)越短,这个损失越小,探针失败的次数越多。
无熔断器: 打到下游 300 次 | 成功 244 | 下游报错 56 | 快速失败 0 | 故障期请求延迟 中位 373.2 ms 最大 601.5 ms | 恢复后 1 秒内延迟 中位 6.3 ms 最大 149.1 ms | 全部处理完 3.00 s
有熔断器: 打到下游 163 次 | 成功 150 | 下游报错 13 | 快速失败 137 | 故障期请求延迟 中位 0.1 ms 最大 228.7 ms | 恢复后 1 秒内延迟 中位 2.7 ms 最大 6.4 ms | 全部处理完 3.00 s
故障结束(1.5 s)之后, 「无熔断器」那一组里还在排队的请求从什么时候才被处理完:
无熔断器: 故障期间提交的最后一个请求 1.65 s 才处理完; 其中 1.5 s 之后才处理完的请求 202 个
有熔断器: 故障期间提交的最后一个请求 1.49 s 才处理完; 其中 1.5 s 之后才处理完的请求 150 个把速率提高到 100 请求/秒(故障期每次调用占用线程 0.15 s,需要约 15 个并发,而线程池只有 8 个)。无熔断器:请求在上游排队,故障期请求的延迟中位 373 ms、最大 601 ms;故障结束之后队列里积压的请求还要被逐个处理,恢复后的第一秒里最大延迟仍有 149 ms。有熔断器:失败请求直接被快速拒绝,中位延迟 0.1 ms,打到下游的调用只有 163 次(无熔断器 300 次),恢复后最大延迟 6.4 ms——上游的资源没有被故障的下游拖住。熔断器的价值在这里才真正体现:保护调用方自己。
concurrent.futures.ThreadPoolExecutor(8 个线程);时间取自 time.perf_counter,线程调度有毫秒级抖动,数字每次运行会略有不同