夜雨聆风学习资料网

ARTICLE · 1011008

这样的公司,你去吗? 只要离开工位就要说明理由,包括上厕所,吃饭

这样的公司,你去吗? 只要离开工位就要说明理由,包括上厕所,吃饭

某网友吐槽,公司要求员工只要离开工位就说明理由,上厕所、吃饭都不例外。这样的管理方式到底有没有必要,我不讨论。但这个场景让我想到一个线上问题:有些系统监控也喜欢盯着“进程还在不在”,就像盯着员工有没有坐在工位上一样。

进程活着,不代表服务真的能干活。

之前维护过一个 Python 服务,监控显示进程一直在线,CPU 不高,内存也正常,但业务请求已经大量超时。最麻烦的是,第一轮排查我们还判断错了方向。

这个服务负责调用下游接口,然后做一层数据聚合。早期业务量不大,为了避免每个请求都创建连接,代码里用了 ThreadPoolExecutor:

from concurrent.futures import ThreadPoolExecutorexecutor = ThreadPoolExecutor(max_workers=20)defquery_detail(order_id):return remote_client.query(order_id)defbatch_query(order_ids):    futures = [        executor.submit(query_detail, order_id)for order_id in order_ids    ]return [f.result() for f in futures]

这段代码运行了很长时间。20 个线程,对当时每秒几十个请求的流量完全够用,所以一直没人觉得有问题。

直到某次下游服务抖动,接口 RT 从正常的 100ms 左右突然涨到十几秒。应用日志开始出现:

10:21:03 INFO  request batch_query size=3010:21:08 WARN  request cost=5012ms10:21:13 WARN  request cost=10017ms10:21:18 ERROR upstream timeout10:21:18 INFO  process alive pid=18432

我们第一反应是 CPU 或数据库顶不住了,但监控里 CPU 只有 30% 左右,数据库连接数和慢 SQL 也没有异常。继续看 Python 进程,内存甚至比平时还稳定。

这时候问题就比较反常:机器没满,进程没挂,为什么请求处理能力几乎没了?

看到 Python 多线程卡住,很容易先想到 GIL。但这里主要是 HTTP I/O,线程等待网络响应时并不是持续占着 GIL 做 CPU 计算,所以 GIL 解释不了十几秒的大面积排队。

真正的问题藏在这一句:

result = future.result()

result() 默认没有超时时间。只要下游迟迟不返回,当前请求就一直等。而线程池只有 20 个 worker,一旦这 20 个线程全部卡在下游 I/O 上,新任务只能进入线程池内部队列等待。

调用链实际上变成了:

HTTP Request    |    vbatch_query()    |    vThreadPoolExecutor(20)    |    +--> worker-1  --> remote API --> blocked    +--> worker-2  --> remote API --> blocked    +--> ...    +--> worker-20 --> remote API --> blocked    |    vpending tasks keep growing

所以进程状态完全正常,真正耗尽的是服务的“有效执行能力”。

我们在线上临时打印线程池状态,现象就很明显了:

logger.info("pool threads=%s pending=%s",    len(executor._threads),    executor._work_queue.qsize())

日志很快变成:

pool threads=20 pending=43pool threads=20 pending=186pool threads=20 pending=517pool threads=20 pending=1249

_threads 和 _work_queue 都属于内部实现,不适合拿来作为长期业务监控,但临时诊断已经足够:20 个线程一个没少,待执行任务却不断堆积。

最开始我们做了一个看起来很合理的修改:

from concurrent.futures import TimeoutErrortry:    result = future.result(timeout=2)except TimeoutError:returnNone

上线前复盘时发现这里还有坑。future.result(timeout=2) 只表示调用方最多等待这个 Future 两秒,并不会自动终止已经在线程中执行的 HTTP 请求。

如果底层 HTTP 客户端本身没有 timeout,worker 还是可能继续卡着。

所以真正应该限制的是网络调用:

import requestssession = requests.Session()defquery_detail(order_id):    response = session.get(f"http://order-service/orders/{order_id}",        timeout=(0.5, 1.5)    )    response.raise_for_status()return response.json()

这里分别限制连接超时和读取超时。下游异常时,请求可以及时释放线程,而不是无限占用 worker。

但这仍然没有解决另一个问题:突发流量可以持续向线程池提交任务,而 ThreadPoolExecutor 默认使用的工作队列并没有给业务提供一个可配置的容量上限。下游已经处理不过来了,上游继续提交,只会把故障从“下游变慢”扩大成“本服务大量排队”。

没有必要为了这个问题重写线程池,我们在提交任务之前增加了 Semaphore,限制同时进入下游调用链的任务数量:

from threading import BoundedSemaphorefrom concurrent.futures import ThreadPoolExecutorexecutor = ThreadPoolExecutor(max_workers=20)slots = BoundedSemaphore(40)defsubmit_query(order_id):    acquired = slots.acquire(timeout=0.05)ifnot acquired:raise RuntimeError("downstream overloaded")try:        future = executor.submit(query_detail, order_id)except Exception:        slots.release()raise    future.add_done_callback(lambda _: slots.release())return future

20 个 worker 加上有限等待名额之后,系统行为发生了变化:以前是任务无限排队,调用方越来越慢;现在超过系统承载能力后,会尽快失败,由上层决定降级还是重试。

对应的业务层不能无脑重试,否则限流又会被重试流量打穿:

defget_order(order_id):try:        future = submit_query(order_id)return future.result(timeout=2)except Exception as e:        logger.warning("query degraded order_id=%s error=%s",            order_id, type(e).__name__        )return {"order_id": order_id, "detail": None}

这里的降级能不能接受,要看业务。如果是支付确认当然不能随便返回空数据;如果只是订单列表里的非核心展示字段,允许缺失往往比把整个接口拖死更合理。

事故还有一个放大因素:当时健康检查只返回固定字符串。

@app.get("/health")defhealth():return {"status": "ok"}

只要 Web 框架还能接收到这个请求,监控就认为实例正常。但业务线程池已经严重排队,这个 ok 没有多少意义。

后来健康指标里增加了真正影响服务能力的数据,例如下游超时率、接口 P99、正在执行的任务数量和拒绝次数:

python_pool_active       20python_pool_rejected     138upstream_timeout_total   427api_latency_p99_ms       4860

健康检查不一定要因为一次下游超时就直接摘掉实例,否则可能造成剩余实例雪崩,但告警必须能看到这些指标。监控的目标不是证明“进程还坐在工位上”,而是判断它现在还有没有处理业务的能力。

回到开头那个话题,上厕所要不要登记我没什么技术结论。但如果一个系统也只会检查“在不在线”,那确实很危险:PID 还在、端口还通,服务可能早就已经干不了活了。

相关学习资料