容器化部署的 Python 服务,跑在 K8s 上,平时没问题,流量一大就开始报 ConnectionPool is full。Pod 自动扩容后好了,缩容又报。这篇文章记录一下容器环境下的排查过程。

问题现象

服务部署在 K8s 上,HPA(Horizontal Pod Autoscaler)根据 CPU 使用率自动扩缩容。高峰期 Pod 数量从 3 扩到 10,这时候开始报错:

requests.exceptions.ConnectionError: ConnectionPool(pool_size=10, connections=10, maxsize=10, blocking=False)

坑在于:Pod 扩容后,每个 Pod 都有 20 个 worker 线程,但连接池只有 10 个位置。10 个 Pod 同时调用后端,连接池瞬间满了。

排查过程

第一步:查看 Pod 日志

# 查看 Pod 日志
kubectl logs -f deployment/backend-client --tail=100

# 看到大量 ConnectionPool is full 错误

第二步:确认后端状态

# 检查后端服务是否正常
kubectl exec -it deployment/backend-client -- curl -s http://backend-service:8080/health
# 返回 200,后端没问题

第三步:检查连接池配置

import requests

# 查看当前 Session 的连接池配置
s = requests.Session()
for host, adapter in s.adapters.items():
    print(f"Host: {host}, Pool: {adapter._pool_connections}, Max: {adapter._pool_maxsize}")

发现问题:每个 Pod 的连接池大小是 10,但每个 Pod 有 20 个 worker。10 个 Pod 同时调用,连接池瞬间满了。

问题原因

说白了,就是容器扩缩容导致并发数突增,连接池容量不够。

K8s HPA 扩容时,新 Pod 启动后立即开始处理请求。如果每个 Pod 有 20 个 worker,10 个 Pod 就是 200 个并发。但连接池只有 10 个位置,根本不够用。

坑在于:连接池大小是静态配置的,不会随着 Pod 数量动态调整。

解决方案

方案一:根据 Pod 数量动态调整连接池

import os
import requests
from requests.adapters import HTTPAdapter

# 从环境变量获取 Pod 数量(或 worker 数量)
pod_count = int(os.environ.get("POD_COUNT", 1))
workers_per_pod = int(os.environ.get("WORKERS_PER_POD", 20))

# 动态计算连接池大小
pool_size = pod_count * workers_per_pod

session = requests.Session()
adapter = HTTPAdapter(pool_connections=pool_size, pool_maxsize=pool_size)
session.mount("https://", adapter)
session.mount("http://", adapter)

方案二:使用共享连接池(推荐)

如果后端服务支持 HTTP/2,可以使用共享连接池:

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()

# 配置重试策略
retry = Retry(
    total=3,
    backoff_factor=0.1,
    status_forcelist=[500, 502, 503, 504],
)

adapter = HTTPAdapter(
    pool_connections=20,
    pool_maxsize=20,
    max_retries=retry,
)
session.mount("https://", adapter)
session.mount("http://", adapter)

方案三:限制并发数

import asyncio
import aiohttp

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    # 限制并发数为 10
    semaphore = asyncio.Semaphore(10)

    async def bounded_fetch(session, url):
        async with semaphore:
            return await fetch(session, url)

    async with aiohttp.ClientSession() as session:
        tasks = [bounded_fetch(session, url) for url in urls]
        results = await asyncio.gather(*tasks)

asyncio.run(main())

生产环境最佳实践

  1. 连接池大小 >= 总并发数:总并发数 = Pod 数量 × 每个 Pod 的 worker 数
  2. 使用 HTTP/2:HTTP/2 支持多路复用,一个连接可以并发多个请求
  3. 监控连接池状态:在 Prometheus 里监控连接池使用率
  4. 设置合理的超时timeout=(3, 10) 连接超时 3 秒,读取超时 10 秒
# 生产环境推荐配置
import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# 动态计算连接池大小
pod_count = int(os.environ.get("POD_COUNT", 1))
workers_per_pod = int(os.environ.get("WORKERS_PER_POD", 20))
pool_size = pod_count * workers_per_pod

session = requests.Session()

retry = Retry(total=3, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504])
adapter = HTTPAdapter(pool_connections=pool_size, pool_maxsize=pool_size, max_retries=retry)
session.mount("https://", adapter)
session.mount("http://", adapter)

踩坑总结

  1. 容器扩缩容会导致并发数突增,连接池容量不够
  2. 连接池大小是静态配置的,不会随 Pod 数量动态调整
  3. 总并发数 = Pod 数量 × 每个 Pod 的 worker 数
  4. 使用 HTTP/2 可以减少连接数
  5. 生产环境要监控连接池状态

这个问题的坑在于:本地测试没问题,上了 K8s 就报错。关键是理解容器环境下的并发模型,根据实际 Pod 数量和 worker 数量配置连接池。

标签: Python, requests

添加新评论