TT Lab
开始
学习 学习路径 课程

实时通信 — WebSocket、gRPC 流式调用与 WebRTC

动手实现连接池并让它耗尽

在 TT Lab 中继续学习

目标

亲手构建一个具备上限、等待时间、废弃规则和空闲上限的客户端连接池,并用同一个测量工具,分别测量没有请求超时的慢调用把池耗干的场景,以及恢复过来的场景。

为什么重要

HTTP 客户端、DB 驱动和 gRPC 通道,内部都有连接池。连接池一旦枯竭,服务器明明很空闲,客户端的所有请求却都在排队,而只看服务器指标的人,什么也看不到。原因通常不在池的配置,而在池之外的规则——没有请求超时、把已超时的连接放回池中、重新使用对方已经关闭的连接。本实验会逐一重现并阻止这三种情形。只使用标准库。

步骤

  1. 有上限的池——在 /root/rt/pool/pool.py 中创建 PoolTimeout 异常和 Pool 类。Pool(factory, max_size, acquire_timeout, idle_timeout=None) 接收一个不带参数调用就能创建一个连接的 factory。acquire() 在有空闲连接时返回它,没有空闲连接且到目前为止创建的连接少于 max_size 时,用 factory 新建一个并返回。release(conn) 把连接放回空闲列表。如果 max_size 不是 1 以上的 int,或 acquire_timeout 不是正数,则为 ValueError,bool 不作为数字接受。
  2. 没有空位时,只等待规定的时间——修改 acquire(),使得在连接全部被占用、也无法再创建时,最多等待 acquire_timeout 秒。这期间如果有人 release,等待的一方就立即得到那个连接,时间用完则抛出 PoolTimeout。等待期间不要占着 CPU 空转。
  3. 状态不明的连接不放回——给 Pool 的 release(conn, broken=False) 加上 broken,并添加 connection() 上下文管理器。如果 broken=True,就调用连接的 close(),并把它从池中去掉,让下一次 acquire 可以新建连接。如果 with pool.connection() as conn: 块正常结束,就把连接放回,如果以异常结束,就按 broken 处理,然后原样再次抛出该异常。
  4. 用数字给出池的状态——给 Pool 添加 stats()。返回由 size(当前存活的连接数)、idle(空闲连接数)、in_use(被借走的连接数)、waiting(当前在 acquire 中等待的调用数)、timeouts(到目前为止抛出 PoolTimeout 的次数)五个键构成的 dict。
  5. 已超时的连接中还残留着上一个响应——在 /root/rt/pool/pool.py 中添加 get(pool, path, timeout)。从 pool 借出连接(套接字),设置 settimeout(timeout),用 /opt/fixtures/rt/rtnet.py 的 h1_get(sock, path) 发送 HTTP/1.1 GET,并返回(状态,正文)。包括超时在内,只要出现任何异常,就把该连接按 broken 丢弃,并重新抛出异常。评分器会用 max_size 为 1 的池让一个慢请求超时,然后立即发送下一个请求。
  6. 四个慢调用占住整个池——运行 /opt/rt-lab/bin/python /opt/fixtures/rt/pool/starve.py。用你的、大小为 4 的池,先发送四个耗时 5 秒的请求,再发送二十个快请求。会给出没有请求超时和请求超时为 0.5 秒时,各有多少个快请求成功。把输出中的 starved_ok 和 bounded_ok 两行写入 /root/rt/pool/report.txt。评分器会重新测量并核对。
  7. 不再使用服务器先关闭的连接——实现 Pool 的 idle_timeout。记录通过 release 回来的时刻,在 acquire 取出空闲连接时,对空闲时间超过 idle_timeout 秒的连接执行 close() 并丢弃,然后再看下一个。如果是 None,就不丢弃。评分器会在一个会断开安静 1 秒的连接的中继器后面放置服务器,用 idle_timeout 为 0.5 的池,依次发送请求 → 休息 1.5 秒 → 请求。

参考

有上限的池

在 /root/rt/pool/pool.py 中创建 PoolTimeout 异常和 Pool 类。Pool(factory, max_size, acquire_timeout, idle_timeout=None) 接收一个不带参数调用就能创建一个连接的 factory。acquire() 在有空闲连接时返回它,没有空闲连接且到目前为止创建的连接少于 max_size 时,用 factory 新建一个并返回。release(conn) 把连接放回空闲列表。如果 max_size 不是 1 以上的 int,或 acquire_timeout 不是正数,则为 ValueError,bool 不作为数字接受。

优先使用空闲连接,正是池存在的理由。目的是不必为每个请求都付出创建连接的成本(TCP 握手、TLS)。评分器会统计 factory 被调用了几次。

没有空位时,只等待规定的时间

修改 acquire(),使得在连接全部被占用、也无法再创建时,最多等待 acquire_timeout 秒。这期间如果有人 release,等待的一方就立即得到那个连接,时间用完则抛出 PoolTimeout。等待期间不要占着 CPU 空转。

threading.Condition 的 wait(timeout) 不会告诉你是因为什么而醒来的。每次醒来都要重新检查条件,并重新计算剩余时间。如果先用 time.monotonic() 确定一次截止时刻,计算就会变简单。

状态不明的连接不放回

给 Pool 的 release(conn, broken=False) 加上 broken,并添加 connection() 上下文管理器。如果 broken=True,就调用连接的 close(),并把它从池中去掉,让下一次 acquire 可以新建连接。如果 with pool.connection() as conn: 块正常结束,就把连接放回,如果以异常结束,就按 broken 处理,然后原样再次抛出该异常。

出现异常的那一刻,并不知道该连接上还残留着什么。响应的后半部分可能还在套接字里。不知道的就丢弃,是池的基本规则。使用 contextlib.contextmanager 可以写得很简短。

用数字给出池的状态

给 Pool 添加 stats()。返回由 size(当前存活的连接数)、idle(空闲连接数)、in_use(被借走的连接数)、waiting(当前在 acquire 中等待的调用数)、timeouts(到目前为止抛出 PoolTimeout 的次数)五个键构成的 dict。

池的枯竭在服务器指标中看不到。因为服务器很空闲,只有客户端在排队。waiting 和 timeouts 是唯一能显示这条队伍的数字。开始等待时加一,无论因为什么原因等待结束时都要减一。

已超时的连接中还残留着上一个响应

在 /root/rt/pool/pool.py 中添加 get(pool, path, timeout)。从 pool 借出连接(套接字),设置 settimeout(timeout),用 /opt/fixtures/rt/rtnet.py 的 h1_get(sock, path) 发送 HTTP/1.1 GET,并返回(状态,正文)。包括超时在内,只要出现任何异常,就把该连接按 broken 丢弃,并重新抛出异常。评分器会用 max_size 为 1 的池让一个慢请求超时,然后立即发送下一个请求。

超时只是不再等待,并不是服务器不发送响应。那个响应即使迟到,也会通过同一个套接字到达。如果把这个套接字放回池中,下一个请求就会读到别人的响应。把 /opt/fixtures/rt 加入 sys.path,就可以导入使用 rtnet。

四个慢调用占住整个池

运行 /opt/rt-lab/bin/python /opt/fixtures/rt/pool/starve.py。用你的、大小为 4 的池,先发送四个耗时 5 秒的请求,再发送二十个快请求。会给出没有请求超时和请求超时为 0.5 秒时,各有多少个快请求成功。把输出中的 starved_ok 和 bounded_ok 两行写入 /root/rt/pool/report.txt。评分器会重新测量并核对。

没有超时的调用会无限期地占着池的位置。acquire_timeout 只能解救等待的一方,却无法让占着位置的一方结束。你必须能说明这两个值为什么会是那样。

不再使用服务器先关闭的连接

实现 Pool 的 idle_timeout。记录通过 release 回来的时刻,在 acquire 取出空闲连接时,对空闲时间超过 idle_timeout 秒的连接执行 close() 并丢弃,然后再看下一个。如果是 None,就不丢弃。评分器会在一个会断开安静 1 秒的连接的中继器后面放置服务器,用 idle_timeout 为 0.5 的池,依次发送请求 → 休息 1.5 秒 → 请求。

负载均衡器和服务器会先断开安静的连接。客户端连接池只有在下次向那个连接发送数据时才会知道。如果把池的空闲上限设得比对方的空闲上限更短,就能避开这场竞争。