容器化自动化测试中的 Podman 锁竞争排查

我们的端到端测试是一个由 Playwright、Testcontainers 和容器运行时组成的多层工作流。 测试工作流 每个 Playwright worker 通过 Testcontainers 管理一组相互隔离的容器和 bridge network。启动前会清理残留资源,测试期间按需启动浏览器、应用和数据库,结束后再停止并删除容器和网络。 清理残留资源 -> 创建 network 和容器 -> 执行浏览器测试 -> stop/remove 容器 -> remove network 这些操作都由 Testcontainers 通过连接 podman Docker 兼容 API 完成, worker 的 teardown 会让 stop、remove、network remove 与容器状态查询交错执行,这正是本次调查关注的并发场景。 实验条件 为了避免大型业务镜像的启动成本和业务行为干扰测试,只使用了一个小镜像: 工作负载:busybox:1.36.1 Podman:quay.io/podman/stable:v5.8.2-immutable Docker:docker:dind,Docker Engine 29.7.2 每轮 24 个并发操作,共 5 轮 两个运行时均运行在 privileged 容器内,状态目录使用 4 GiB tmpfs 每个并发操作创建 bridge network,创建并启动一个 busybox 容器,然后执行 stop、remove 和 network remove。teardown 阶段同时发起 24 个 GET /containers/json?all=1 请求,并持续采样 containers-list、/info 和 /_ping。 ...

August 27, 2026

线程池饥饿排查:从 ThreadPool 指标追到 SQLite_BUSY

应用变慢时,第一反应很容易是“线程不够”或“CPU 不够”。但在高并发 Web 应用中,更常见的情况是线程池线程都被阻塞了:线程数量不断增加,工作项排队等待,新的请求迟迟得不到执行。 这就是线程池饥饿(Thread Pool Starvation)。本文用一次 SQLite 相关的现场问题说明如何观察它,以及为什么简单增加线程数往往会让问题更糟。 什么是线程池饥饿 当线程池没有可用线程处理新的工作项时,就发生了线程池饥饿。应用通常表现为响应变慢、请求超时,但 CPU 利用率未必很高。 常见诱因包括: 在异步方法上调用 .Result 或 .Wait(); 使用 Thread.Sleep; 长时间运行的定时任务或后台任务占用线程池线程; 同步阻塞 I/O; 多个线程竞争同一把锁; 数据库调用长时间等待锁释放。 尤其要注意,业务代码不需要“完全同步”才会产生饥饿。只要异步链路中夹杂了阻塞调用,线程池就可能在压力下逐渐耗尽可用线程。 用 dotnet-counters 观察现场 先对目标进程做压测,同时运行: dotnet-counters monitor -p 12256 重点观察 [System.Runtime] 下的几个指标: ThreadPool Queue Length 75 ThreadPool Thread Count 38 ThreadPool Completed Work Item Count 128 / sec 如果 ThreadPool Queue Length 持续增长,说明工作项已经开始排队;如果 ThreadPool Thread Count 长时间明显高于 CPU 核心数,同时队列仍然无法消化,这是线程池饥饿的强烈信号。现场案例中线程数长期达到 CPU 核心数约 3 倍,仍有大量工作项等待。 这个判断应该结合趋势和请求延迟一起看,不能把“线程数超过核心数”单独当成故障证明。线程池在遇到阻塞时主动注入线程本身就是一种正常的补救机制,问题在于注入的线程也被同样的阻塞点卡住了。 用 dotnet-stack 看谁在阻塞 线上通常无法附加 debugger,这时可以使用 dotnet-stack 获取托管线程的调用栈。现场看到的一类关键栈如下: ...

July 22, 2026