主题
第四章:多 Worker 与连接池
本章导读
本章围绕“多 Worker 与连接池”展开,先交代适用场景,再说明核心原理,随后给出典型示例,最后补充小结与延伸阅读。
阅读顺序: 结论 → 原理 → 场景 → 代码 → 小结
Q12:3 个 Worker 进程启动后,各自有独立的 redis_client 吗?Redis 服务端看到的是 3 个连接还是 1 个?
答:
3 个独立的 redis_client,Redis 端看到 3 个 TCP 连接。
这里的“3 个 Worker 进程”指的是用 3 个独立 OS 进程启动 3 个 Worker。每个 python -m app.worker 是独立进程,各自 import 模块时创建 redis_client 单例。每个进程启动时连接池都是空的(按需建连),brpop 第一次执行时才建立第 1 条 TCP:
进程 A (PID 1001) 进程 B (PID 1002) 进程 C (PID 1003)
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ redis_client │ │ redis_client │ │ redis_client │
│ └─ Pool (max=10)│ │ └─ Pool (max=10)│ │ └─ Pool (max=10)│
│ ┌─────┬─────┐ │ │ ┌─────┬─────┐ │ │ ┌─────┬─────┐ │
│ │C-1 │ │ │ │ │C-1 │ │ │ │ │C-1 │ │ │
│ │BRPOP│空闲 │ │ │ │BRPOP│空闲 │ │ │ │BRPOP│空闲 │ │
│ └─────┴─────┘ │ │ └─────┴─────┘ │ │ └─────┴─────┘ │
│ _in_use: {C-1} │ │ _in_use: {C-1} │ │ _in_use: {C-1} │
│ _available: [] │ │ _available: [] │ │ _available: [] │
│ 实际连接: 1 个 │ │ 实际连接: 1 个 │ │ 实际连接: 1 个 │
└───────┬──────────┘ └───────┬──────────┘ └───────┬──────────┘
│ TCP:54321 │ TCP:54322 │ TCP:54323
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Redis 服务端(端口 6380) │
│ client(fd=10, addr=:54321, cmd=brpop) ← 等待链表节点 1 ← 进程 A
│ client(fd=11, addr=:54322, cmd=brpop) ← 等待链表节点 2 ← 进程 B
│ client(fd=12, addr=:54323, cmd=brpop) ← 等待链表节点 3 ← 进程 C
│ │
│ task:queue → [client(fd=10), client(fd=11), client(fd=12)] │
│ ↑ 第一个被 LPUSH 唤醒 │
└──────────────────────────────────────────────────────────────────────────┘关键点:
- 每个进程的连接池
max=10,但实际只创建 1 个连接(brpop 独占 Conn-1) - 每条 TCP 连接源端口不同(54321/54322/54323),目的端口都是 6380
- Redis 端
CLIENT LIST看到 3 行,无法判断它们属于几个进程
用 CLIENT LIST 验证:
id=1 addr=127.0.0.1:54321 fd=10 cmd=brpop age=5 idle=5
id=2 addr=127.0.0.1:54322 fd=11 cmd=brpop age=5 idle=5
id=3 addr=127.0.0.1:54323 fd=12 cmd=brpop age=5 idle=53 个 Worker = 3 行 = 3 个 TCP 连接 = 3 个等待链表节点。
Q13:Redis 等待链表里存的是什么?是"Worker 进程"吗?
答:
不是 Worker 进程或 Worker 线程这种应用侧执行单元的抽象概念,而是具体的 redisClient 结构体指针,每个对应一个 TCP 连接的文件描述符(fd)。
c
// Redis 源码 server.h
typedef struct client {
int fd; // ← TCP 连接的文件描述符
connection *conn; // ← 连接对象
// ... 其他字段
} client;
// 阻塞 key 的等待客户端链表
dict *db->blocking_keys;
// key → list of client*唤醒时 Redis 通过 fd 把数据写回给客户端。一个 client = 一个 fd = 一个 TCP 连接。
这里要特别区分两个概念:redis_client 是「应用侧」创建的 Redis 客户端对象,而等待链表中的 client 是「Redis 服务端」维护的、正在阻塞等待某个 key 的 TCP 连接。两者不是同一个层面的概念,但它们通常通过同一条连接建立对应关系。
即使一个进程内有 3 个线程同时 BRPOP,redis-py 连接池也会借出 3 个不同的连接,Redis 端看到 3 个 client,
⚠️ 关键澄清:等待链表节点数 ≠ 连接总数
一个 Python 进程借出 3 个 TCP 连接 → Redis 端有 3 个 client 结构体。但等待链表里存几个,取决于有几个连接正在 BRPOP 等待。
核心规则:
等待链表节点数 = 当前正在执行 BRPOP 且未返回的 TCP 连接数
≠ 客户端进程数
≠ redis_client 对象数
≠ 连接池里的连接总数
≠ Redis 端的 client 结构体总数场景对比:一个进程 + 3 个连接,等待链表节点数不同
场景 A:3 个连接都在 BRPOP(等待链表 3 个节点)
python
# 同一个进程,3 个线程同时 BRPOP 同一个 key
import threading
def worker():
# 每个线程独立调用一次 brpop,借出各自的连接
redis_client.brpop("task:queue", timeout=5)
# ↑ 线程1 借 Conn-1,线程2 借 Conn-2,线程3 借 Conn-3
# ↑ 3 个线程并发执行,3 个连接同时阻塞在 read_response()
# 启动 3 个线程
threads = [threading.Thread(target=worker) for _ in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
# 3 个线程 → 连接池借出 3 个 TCP 连接 → Redis 端 3 个 client → 都在等待3 个线程并发执行 brpop 时的连接池状态:
_in_use: {Conn-1, Conn-2, Conn-3} ← 3 个连接都被借出
_available: [] ← 池空(还有 7 个余量未创建)
Redis 端:
client(fd=10, cmd=brpop) ← Conn-1(线程1)
client(fd=11, cmd=brpop) ← Conn-2(线程2)
client(fd=12, cmd=brpop) ← Conn-3(线程3)
等待链表:
task:queue → [client(fd=10), client(fd=11), client(fd=12)]
↑ 3 个节点,虽然来自同一个进程LPUSH 时只唤醒 fd=10(FIFO 第一个),fd=11、fd=12 继续等。
场景 B:1 个 BRPOP + 2 个 hset(等待链表 1 个节点)
python
# 同一个进程,1 个线程 BRPOP 阻塞,2 个线程并行写进度
import threading
# 线程 1:BRPOP 阻塞等待任务
def consumer():
result = redis_client.brpop("task:queue", timeout=5)
# ↑ 借出 Conn-1,阻塞在 read_response()
# ↑ 加入等待链表,未归还 Conn-1
# 线程 2:写任务进度(立即归还)
def writer_progress_image():
redis_client.hset("task:6:progress", "stage", "IMAGE")
# ↑ 借出 Conn-2 → 发送 HSET → 收到回复 → 立即归还 Conn-2
# 线程 3:写任务结果(立即归还)
def writer_result_done():
redis_client.hset("task:6:progress", "stage", "DONE")
# ↑ 借出 Conn-3 → 发送 HSET → 收到回复 → 立即归还 Conn-3
# 启动 3 个线程(consumer 先启动确保先进入 BRPOP)
t1 = threading.Thread(target=consumer)
t2 = threading.Thread(target=writer_progress_image)
t3 = threading.Thread(target=writer_result_done)
t1.start()
import time; time.sleep(0.1) # 确保 consumer 先进入 BRPOP
t2.start()
t3.start()
t1.join(); t2.join(); t3.join()线程并发期间连接池状态:
_in_use: {Conn-1} ← 只有 Conn-1 还在阻塞(BRPOP 没返回)
_available: [Conn-2, Conn-3] ← Conn-2、Conn-3 已归还
↑ 注意:Conn-2、Conn-3 已归还但连接对象仍在池中(TCP 没断)
Redis 端:
client(fd=10, cmd=brpop) ← Conn-1 在等待链表里
client(fd=11, cmd=hset) ← Conn-2 已归还,但 client 结构体还在(TCP 仍存活)
client(fd=12, cmd=hset) ← Conn-3 已归还,但 client 结构体还在(TCP 仍存活)
等待链表:
task:queue → [client(fd=10)]
↑ 只有 1 个节点(只有 Conn-1 在 BRPOP)3 个 client 都存在(TCP 连接还活着),但只有 1 个在等待链表里。
关键澄清:
- 归还 ≠ 关闭 TCP:连接归还到池中只是从
_in_use移到_available,TCP 连接保持打开状态,Redis 端client结构体仍然存在 - client 结构体存活到 TCP 关闭:只有显式
disconnect()或连接池销毁时,TCP 才关闭,Redis 端 client 才会从CLIENT LIST消失
场景 C:本项目实际(等待链表 1 个节点)
python
# 主循环(单线程)
def process_queue():
while True:
result = redis_client.brpop("task:queue", timeout=5)
# ↑ 主线程借 Conn-1 BRPOP 阻塞(等待链表 1 个节点)
if result:
task_id = int(result[1])
# ↑ brpop 已返回,Conn-1 已归还到池中
run_task(task_id)
# ↑ 此时主线程在执行 run_task,没有线程在 BRPOP
# run_task 内部启动 3 个分镜线程(ThreadPoolExecutor)
def run_task(task_id):
redis_client.hset(f"task:{task_id}:progress", "stage", "SCRIPT")
# ↑ 主线程借 Conn-1(复用)→ 立即归还
# 3 线程并行调 Seedance API + 写进度
from concurrent.futures import ThreadPoolExecutor
def generate_frame_video(frame):
# 调 Seedance API(不在 Redis 上阻塞)
# ...
# 写进度(借连接)
redis_client.hset(f"task:{task_id}:progress",
f"frame_{frame.seq}", "done")
# ↑ 各线程借 Conn-2/3/4 → 立即归还
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(generate_frame_video, f) for f in frames]
for future in futures:
future.result()阶段 1:BRPOP 阻塞期间(主线程在 brpop 中)
连接池: _in_use: {Conn-1} _available: []
Redis 端: client(fd=10, cmd=brpop)
等待链表: task:queue → [client(fd=10)] ← 1 个节点
阶段 2:BRPOP 返回后,run_task 执行中(分镜并行)
连接池: _in_use: {} _available: [Conn-1, Conn-2, Conn-3, Conn-4]
↑ 主线程 hset 借 Conn-1 → 归还
↑ 3 个分镜线程 hset 借 Conn-2/3/4 → 归还
Redis 端: client(fd=10,11,12,13, cmd=hset)
等待链表: task:queue → [] ← 空!没有线程在 BRPOP
阶段 3:run_task 返回,主线程回到 while 循环顶部再次 brpop
连接池: _in_use: {Conn-1} _available: [Conn-2, Conn-3, Conn-4]
Redis 端: client(fd=10, cmd=brpop)
等待链表: task:queue → [client(fd=10)] ← 1 个节点关键:等待链表节点数随主线程状态变化——brpop 阻塞时 1 个节点,run_task 执行时 0 个节点(brpop 已返回,没有人在 BRPOP)。
速查表
| 概念 | 场景 A(3 线程 BRPOP) | 场景 B(1 BRPOP + 2 hset) | 场景 C(本项目) |
|---|---|---|---|
| Python 进程数 | 1 | 1 | 1 |
| 同时工作的线程数 | 3 | 3 | 1(主循环)/ 4(分镜并行时) |
连接池 _in_use 大小 | 3 | 1(另外 2 个已归还) | 1(brpop 期间)/ 0(run_task 期间) |
| 连接池 TCP 连接总数 | 3 | 3(一旦建立不关闭) | 1-4(按需创建) |
| Redis 端 client 结构体数 | 3 | 3(TCP 仍存活) | 1-4(动态) |
| 等待链表节点数 | 3 | 1 | 1(brpop 期间)/ 0(run_task 期间) |
核心规律:
| 关系 | 说明 |
|---|---|
| 等待链表节点数 = ? | 当前正在执行 BRPOP 且未返回的 TCP 连接数 |
| 已归还连接 → Redis 端 client 是否消失? | ❌ 不消失,TCP 仍存活,client 结构体保留 |
| 已归还连接 → 等待链表是否还有? | ❌ 没有,已归还说明 BRPOP 已返回 |
| 连接池 TCP 连接总数 = ? | 历史最大借出数(一旦建立不主动关闭) |
用 CLIENT LIST 验证
场景 A(3 个线程都在 BRPOP):
bash
redis-cli CLIENT LISTid=1 addr=127.0.0.1:54321 fd=10 cmd=brpop age=5 idle=5 ← 在等待链表
id=2 addr=127.0.0.1:54322 fd=11 cmd=brpop age=5 idle=5 ← 在等待链表
id=3 addr=127.0.0.1:54323 fd=12 cmd=brpop age=5 idle=5 ← 在等待链表3 行 = 3 个 client = 3 个 TCP 连接 = 等待链表 3 个节点(3 个都在等)。
场景 B(1 个 BRPOP + 2 个 hset):
id=1 addr=127.0.0.1:54321 fd=10 cmd=brpop age=5 idle=5 ← 在等待链表
id=2 addr=127.0.0.1:54322 fd=11 cmd=hset age=5 idle=0 ← 不在等待链表
id=3 addr=127.0.0.1:54323 fd=12 cmd=hset age=5 idle=0 ← 不在等待链表3 行 = 3 个 client = 3 个 TCP 连接,但等待链表只有 1 个节点(只有 cmd=brpop 且 idle 持续增长的在等)。
本章小结
等待链表里存的是"正在 BRPOP 等待的 TCP 连接",不是"客户端进程"。一个进程借出 3 个连接,如果 3 个都在 BRPOP,等待链表就有 3 个节点;如果只有 1 个在 BRPOP,等待链表就只有 1 个节点。Redis 只看 TCP 连接,不看连接属于哪个进程。
补充:每条 TCP 连接都有独立的源端口
关键概念:一条 TCP 连接由四元组唯一标识:
TCP 连接 = (源 IP, 源端口, 目的 IP, 目的端口)本项目场景:
目的 IP:目的端口 = 127.0.0.1:6380 ← Redis 服务端固定
源 IP = 127.0.0.1 ← 客户端机器固定
源端口 = ??? ← 每条连接不同!源端口是操作系统分配的,每建立一条 TCP 连接,内核就分配一个空闲的源端口:
Conn-1: 127.0.0.1:54321 ←► 127.0.0.1:6380
Conn-2: 127.0.0.1:54322 ←► 127.0.0.1:6380
Conn-3: 127.0.0.1:54323 ←► 127.0.0.1:6380
↑ 源端口不同 ↑ 目的端口相同(都是 Redis)同一进程的 3 个连接也是不同端口(代码示例见上文 Q13 场景 A):
python
# 简化示例:一个进程,主线程 + 2 个子线程,共 3 个 TCP 连接
import threading, redis
redis_client = redis.Redis(host="localhost", port=6380, max_connections=10)
# 主线程借 Conn-1
redis_client.hset("k", "main", "v1")
# 子线程各自借 Conn-2、Conn-3
def worker(tag):
redis_client.hset("k", tag, f"v-{tag}")
t1 = threading.Thread(target=worker, args=("t1",))
t2 = threading.Thread(target=worker, args=("t2",))
t1.start(); t2.start(); t1.join(); t2.join()此时 CLIENT LIST 输出(3 条 TCP 连接,3 个源端口):
id=1 addr=127.0.0.1:54321 fd=10 cmd=hset age=1 idle=0 ← Conn-1(主线程)
id=2 addr=127.0.0.1:54322 fd=11 cmd=hset age=1 idle=0 ← Conn-2(线程 t1)
id=3 addr=127.0.0.1:54323 fd=12 cmd=hset age=1 idle=0 ← Conn-3(线程 t2)
↑ ↑
3 个源端口不同 3 个 fd 不同
但是同一个进程!Redis 端无法从 CLIENT LIST 判断这 3 个连接属于同一个进程还是不同进程——它只看到 3 条独立的 TCP 连接。
为什么每条 TCP 连接需要独立端口
TCP 协议规定,一个端口同一时刻只能被一条 TCP 连接的某一端使用:
端口 54321 → 只能被 Conn-1 的客户端侧占用
端口 54322 → 只能被 Conn-2 的客户端侧占用
端口 54323 → 只能被 Conn-3 的客户端侧占用
端口 6380 → Redis 服务端共用一个端口(但能区分连接)Redis 服务端怎么用同一个端口 6380 服务多条连接:靠 fd(文件描述符)区分。每个 TCP 连接到达服务端后,Redis 通过 accept() 创建一个新的 fd,然后把它和 client 结构体绑定。
c
// Redis 服务端源码(简化)
while (1) {
int client_fd = accept(server_fd, ...); // ← 每个连接 accept 一次
// ↑ server_fd 绑定 6380 端口,但 client_fd 是新的
client *c = createClient(client_fd);
// ↑ 每个 client_fd 对应一个 client 结构体
}| 层面 | 端口 | fd |
|---|---|---|
| 客户端侧 | 每条 TCP 连接独立源端口 | 每条连接独立 fd |
| 服务端侧 | 共用 6380 监听端口 | 每条连接独立 fd |
用 lsof / netstat 验证
bash
# 查看 Redis 服务端(6380)的连接
lsof -i :6380COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
redis-ser 1234 user 6u IPv4 0x1234 0t0 TCP *:6380 (LISTEN) ← 监听 socket
redis-ser 1234 user 10u IPv4 0x5678 0t0 TCP localhost:6380→localhost:54321
redis-ser 1234 user 11u IPv4 0x5679 0t0 TCP localhost:6380→localhost:54322
redis-ser 1234 user 12u IPv4 0x567a 0t0 TCP localhost:6380→localhost:54323
↑ ↑
服务端 fd(每条不同) 客户端源端口(每条不同)3 条连接 = 3 个服务端 fd(10/11/12)= 3 个客户端源端口(54321/54322/54323)。
端口范围
操作系统给客户端分配源端口时,从一个固定范围内选:
bash
# Linux 查看端口范围
$ cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999
# macOS 查看
$ sysctl net.inet.ip.portrange.first net.inet.ip.portrange.last
net.inet.ip.portrange.first: 1024
net.inet.ip.portrange.last: 65535理论上一个客户端机器最多能连 28000 多条 TCP 到同一个 Redis 端口(Linux),但实际 Worker 进程不会这么多。
端口总结表
| 问题 | 答案 |
|---|---|
| 同一客户端的 3 个连接,CLIENT LIST 显示 3 行吗? | ✅ 是 |
| 这 3 行的端口相同吗? | ❌ 不同,每条 TCP 连接独立源端口 |
| 每条 TCP 连接都有独立端口吗? | ✅ 是(客户端侧源端口不同) |
| Redis 服务端端口相同吗? | ✅ 相同(都是 6380,靠 fd 区分) |
| Redis 能从 CLIENT LIST 判断连接属于哪个进程吗? | ❌ 不能,只看 TCP 连接 |
核心:一条 TCP 连接 = (源 IP, 源端口, 目的 IP, 目的端口) 四元组。同一个进程的 3 条连接,源端口必然不同,所以 CLIENT LIST 看到 3 行端口不同的记录。
端口与连接速查表
| 维度 | 客户端侧 | 服务端(Redis)侧 |
|---|---|---|
| IP | 127.0.0.1(客户端机器) | 127.0.0.1(Redis 机器) |
| 端口 | 每条 TCP 连接独立源端口(54321/54322/54323) | 共用 6380 监听端口 |
| fd | 每条连接独立 fd | 每条连接独立 fd(10/11/12) |
| 区分连接靠什么 | 源端口 | fd(accept 返回的新 fd) |
| 一个端口能被多少连接使用 | 同一时刻只能被一条 TCP 连接的客户端侧占用 | 同一端口可服务多条连接(靠 fd 区分) |
| 场景 | Python 进程数 | TCP 连接数 | 源端口数 | Redis 端 fd 数 | CLIENT LIST 行数 |
|---|---|---|---|---|---|
| 3 进程 × 1 连接 | 3 | 3 | 3(每进程 1 个) | 3 | 3 |
| 1 进程 × 3 连接(3 线程 BRPOP) | 1 | 3 | 3(同一进程) | 3 | 3 |
| 1 进程 × 1 连接(本项目主循环) | 1 | 1 | 1 | 1 | 1 |
| 1 进程 × 4 连接(分镜并行) | 1 | 4 | 4 | 4 | 4 |
| 问题 | 答案 |
|---|---|
| 同一客户端的 3 个连接,CLIENT LIST 显示 3 行吗? | ✅ 是 |
| 这 3 行的端口相同吗? | ❌ 不同,每条 TCP 连接独立源端口 |
| 每条 TCP 连接都有独立端口吗? | ✅ 是(客户端侧源端口不同) |
| Redis 服务端端口相同吗? | ✅ 相同(都是 6380,靠 fd 区分) |
| Redis 能从 CLIENT LIST 判断连接属于哪个进程吗? | ❌ 不能,只看 TCP 连接 |
| 同一进程的 3 条连接源端口能相同吗? | ❌ 不能,TCP 协议规定一个端口同一时刻只能被一条连接的某一端使用 |
| Redis 服务端怎么用 6380 服务多条连接? | 监听 socket 绑定 6380,accept 时为每条连接创建独立 fd |
| Linux 客户端源端口范围 | 32768-60999(约 28000 个) |
| macOS 客户端源端口范围 | 1024-65535 |
| 验证命令 | 作用 |
|---|---|
redis-cli CLIENT LIST | 查看 Redis 端所有 client(含 addr、fd、cmd、idle) |
lsof -i :6380 | 查看 6380 端口的所有 TCP 连接(含客户端源端口) |
netstat -an | grep 6380 | 查看与 6380 相关的连接状态 |
cat /proc/sys/net/ipv4/ip_local_port_range | Linux 查看客户端源端口范围 |
sysctl net.inet.ip.portrange.first net.inet.ip.portrange.last | macOS 查看客户端源端口范围 |
Q14:如果用 multiprocessing.Process 的 fork 模式启动 Worker,Redis 连接会出什么问题?
答:
fork 会复制父进程的内存,包括 redis_client 和已建立的 TCP 连接的 fd。两个进程向同一个 socket 写数据会导致 Redis 协议错乱。
python
# ❌ fork 模式的问题
import multiprocessing
from app.redis_client import redis_client # ← 父进程已创建
def worker():
# ← fork 后子进程继承了父进程的 redis_client
# ← 连接池中的 TCP fd 被复制
# ← 两个进程操作同一个 socket → 数据错乱!
redis_client.brpop("task:queue", timeout=5)
p = multiprocessing.Process(target=worker)
p.start() # fork!正确做法:子进程中重新创建 redis_client:
python
def worker():
import redis
# 子进程中重新创建,不复用父进程的连接池
client = redis.Redis(
host="localhost",
port=6380,
decode_responses=True,
max_connections=10,
)
client.brpop("task:queue", timeout=5)本项目不受影响:python -m app.worker 每次都是全新进程,不走 fork,不存在继承问题。
Q15:一个 Worker 进程实际同时占用的连接数是多少?连接池 max_connections=10 是不是浪费?
答:
实际同时占用的连接数取决于是否启用 Seedance 分镜并行:
| 模式 | 占用连接数 | 说明 |
|---|---|---|
| 单线程顺序执行(无分镜并行) | 1 个 | brpop 返回后才执行 hset,全程只用 1 个连接 |
| 3 线程分镜并行(本项目实际启用) | 3-4 个 | 3 个分镜线程 + 主线程 BRPOP |
1. 单线程顺序执行模式(1 个连接)
| 阶段 | 占用连接 | 说明 |
|---|---|---|
process_queue 中 BRPOP 阻塞 | 1 个 | 只有 BRPOP 在执行,线程被挂起 |
run_task 中写进度 | 1 个 | BRPOP 已归还,HSET 复用 Conn-1 |
run_task 中推审核队列 | 1 个 | 同上 |
| 峰值 | 1 个 | brpop 和 hset 顺序执行,不会同时 |
2. 3 线程分镜并行模式(3-4 个连接,本项目实际配置)
python
# orchestrator.py 实际代码
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(generate_frame_video, frame) for frame in frames]
# ↑ 3 个线程并行调用 Redis 写进度
# ↑ 同时主线程可能还在 brpop 阻塞(Conn-1 被独占)
# 连接池状态:
# _in_use: {Conn-1(BRPOP), Conn-2, Conn-3, Conn-4}
# _available: [Conn-5, ..., Conn-10]
# ↑ max_connections=10 留 6 个余量,不浪费| 阶段 | 占用连接 | 说明 |
|---|---|---|
| 主线程 BRPOP 阻塞 | Conn-1 | 独占 |
| 分镜线程 1 写进度 | Conn-2 | 并行借出 |
| 分镜线程 2 写进度 | Conn-3 | 并行借出 |
| 分镜线程 3 写进度 | Conn-4 | 并行借出 |
| 峰值 | 4 个 | 1 BRPOP + 3 分镜并行 |
3. 为什么 max_connections=10 不浪费
- 连接是惰性创建的:
max_connections=10只是上限,实际用几个建几个,不用的不建 - 分镜并行需要 3-4 个:3 线程并行 + 主线程 BRPOP,至少需要 4 个连接
- 留 6 个余量:未来扩展到 5-6 线程并行也不用改配置
4. 多 Worker 进程的总连接数
| Worker 进程数 | 单进程连接峰值 | 总 TCP 连接 | Redis max_clients |
|---|---|---|---|
| 1 | 4 | 4 | 默认 10000 |
| 3 | 4 | 12 | 足够 |
| 10 | 4 | 40 | 足够 |
| 50 | 4 | 200 | 足够 |
| 200 | 4 | 800 | 足够 |
详见 Redis连接池详解.md 1.3 节 — 单线程单连接 vs 多线程多连接的完整对比。