Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
65a11fd
add release note 120
zhongwen666 Feb 3, 2026
9157582
Revert "add release note 120"
zhongwen666 Feb 3, 2026
dba7590
Merge branch 'alibaba:master' into master
zhongwen666 Feb 3, 2026
b14eac6
Merge branch 'alibaba:master' into master
zhongwen666 Feb 3, 2026
d5e799c
Merge branch 'alibaba:master' into master
zhongwen666 Feb 5, 2026
b84e5a2
Merge branch 'alibaba:master' into master
zhongwen666 Feb 10, 2026
3085eed
Merge branch 'alibaba:master' into master
zhongwen666 Feb 11, 2026
6d9a4ca
Merge branch 'alibaba:master' into master
zhongwen666 Feb 12, 2026
8d6e6ca
Merge branch 'alibaba:master' into master
zhongwen666 Feb 24, 2026
acff0c0
Merge branch 'alibaba:master' into master
zhongwen666 Feb 26, 2026
f24c268
Merge branch 'alibaba:master' into master
zhongwen666 Feb 27, 2026
0a11dbf
Merge branch 'alibaba:master' into master
zhongwen666 Feb 28, 2026
5dc5453
Merge branch 'alibaba:master' into master
zhongwen666 Mar 3, 2026
cdef141
Merge branch 'alibaba:master' into master
zhongwen666 Mar 3, 2026
d6dc02e
Merge branch 'alibaba:master' into master
zhongwen666 Mar 3, 2026
6382e4e
Merge branch 'alibaba:master' into master
zhongwen666 Mar 4, 2026
a4bd676
Merge branch 'alibaba:master' into master
zhongwen666 Mar 4, 2026
f020bd7
Merge branch 'alibaba:master' into master
zhongwen666 Mar 10, 2026
9d4ceeb
Merge branch 'alibaba:master' into master
zhongwen666 Mar 12, 2026
af5f29d
Merge branch 'alibaba:master' into master
zhongwen666 Mar 12, 2026
62ccd5d
Merge branch 'alibaba:master' into master
zhongwen666 Mar 12, 2026
1d2d62c
Merge branch 'alibaba:master' into master
zhongwen666 Mar 16, 2026
c10dce0
Merge branch 'alibaba:master' into master
zhongwen666 Mar 17, 2026
8fb0f3b
Merge branch 'alibaba:master' into master
zhongwen666 Mar 23, 2026
4d70da7
Merge branch 'alibaba:master' into master
zhongwen666 Mar 24, 2026
e27cc81
Merge branch 'alibaba:master' into master
zhongwen666 Mar 27, 2026
56f6000
Merge branch 'alibaba:master' into master
zhongwen666 Mar 27, 2026
c654a33
Merge branch 'alibaba:master' into master
zhongwen666 Mar 27, 2026
c2594ea
Merge branch 'alibaba:master' into master
zhongwen666 Mar 27, 2026
46172a3
Merge branch 'alibaba:master' into master
zhongwen666 Apr 1, 2026
f345d31
Merge branch 'alibaba:master' into master
zhongwen666 Apr 17, 2026
31bd9f3
Merge branch 'alibaba:master' into master
zhongwen666 Apr 23, 2026
c3ef4ce
Merge branch 'alibaba:master' into master
zhongwen666 May 8, 2026
40f2de2
Merge branch 'alibaba:master' into master
zhongwen666 May 9, 2026
171d150
Merge branch 'alibaba:master' into master
zhongwen666 May 15, 2026
43d0aa7
Merge branch 'alibaba:master' into master
zhongwen666 May 15, 2026
ed7abca
Merge branch 'alibaba:master' into master
zhongwen666 May 18, 2026
f7987f2
Merge branch 'alibaba:master' into master
zhongwen666 May 18, 2026
023b1e4
Merge branch 'alibaba:master' into master
zhongwen666 May 21, 2026
b34a2ee
Merge branch 'alibaba:master' into master
zhongwen666 May 27, 2026
370457e
Merge branch 'alibaba:master' into master
zhongwen666 Jun 1, 2026
53dc4ad
Merge branch 'alibaba:master' into master
zhongwen666 Jun 1, 2026
27913c7
Merge branch 'alibaba:master' into master
zhongwen666 Jun 2, 2026
45317cd
Merge branch 'alibaba:master' into master
zhongwen666 Jun 9, 2026
8fceba9
Merge branch 'alibaba:master' into master
zhongwen666 Jun 11, 2026
68c779b
Merge branch 'alibaba:master' into master
zhongwen666 Jun 16, 2026
5ecae9f
Merge branch 'alibaba:master' into master
zhongwen666 Jun 22, 2026
83e1d5e
docs(spec): proxy multicore design — uvicorn workers + httpx pool reuse
zhongwen666 Jun 22, 2026
4404539
docs(plan): proxy-multicore implementation plan (TDD, 9 tasks)
zhongwen666 Jun 22, 2026
4e7cf5f
opt read cluster
zhongwen666 Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docs/_specs/proxy-multicore/01_requirement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# sandbox_proxy_router 多核化 — 需求与决策

> 日期:2026-06-22 · 状态:设计已确认,待出实施计划

## 1. 背景

`sandbox_proxy_router`(`admin --role proxy`)当前通过 `rock/admin/main.py:310` 的 `uvicorn.run(app, ...)` 以**单进程、单事件循环**运行,在多核机器上只能吃满一个核心。proxy role 是数据/控制面的转发层,职责为:

- 把 HTTP / WebSocket / SSE 请求转发到 sandbox(rocklet);
- 从 Redis/DB(`meta_store`)读取 sandbox metadata;
- 生成 OSS STS token。

proxy 进程内**无跨请求会话状态**(bash session 在 rocklet;WebSocket 连接天然绑定单连接),因此适合多进程横向扩展。

## 2. 目标

1. 让 `sandbox_proxy_router` 在单 Pod 内充分利用多核心(吞吐随核数近似线性提升)。
2. 复用 httpx 连接池,消除转发热路径上的每请求建连开销。
3. 在不打爆后端(PostgreSQL)、不失控内存的前提下安全多进程化。

## 3. 关键决策(已与需求方确认)

| 决策项 | 选择 | 理由 |
|--------|------|------|
| 扩展方式 | **单 Pod 内多进程**(进程内扩展) | 直接吃满本机核心,不依赖 k8s 扩副本 |
| 进程管理 | **uvicorn 原生 `--workers`** | 改动最小,内置 master 管理子进程,原生支持 ASGI/WebSocket |
| 多进程范围 | **仅 proxy role**;admin role 恒 `workers=1` | admin 持有 scheduler 线程(`is_primary_pod()` 为 Pod 级)与 Ray/单例,多进程会重复调度 |
| 优化范围 | **httpx 连接池复用** | 修复 `http_proxy`/`host_proxy` 每请求新建 `AsyncClient` |
| 连接池/Metrics 治理 | 作为多进程化的**必要正确性项**纳入 | 多 worker 必然放大连接数、Metrics 标签冲突 |

**明确不做(本期)**:访问日志中间件瘦身、sandbox 状态查询缓存(需求方未勾选)。但留观测点——多 worker 后日志中间件的 `await request.json()+json.dumps(indent=2)` CPU 开销会在每个核各付一份,若压测发现单核仍被日志吃满需回头处理。

## 4. 约束与硬前提

### 4.1 argv 不可跨 worker(为什么必须改 env 传参)

`uvicorn.run("module:app", workers=N)` 会 fork/spawn 出 N 个 worker 子进程,**每个子进程重新 import 模块构建 app**。当前 `main.py` 顶层 `args = parser.parse_args()` 在 import 期执行,而 worker 子进程(尤其 spawn 模式)的 `sys.argv` **不是**启动命令的 argv,会退回 argparse 默认值(`role=admin`!)甚至 `SystemExit`。

→ 必须改为:**主进程 `main()` 解析 argv 并写入 env;`create_app()`/`lifespan`(每 worker 执行)只读 env**。顶层 `parse_args()` 必须删除。env 是进程继承属性,fork/spawn 都能正确继承。

### 4.2 连接池不能跨进程共享

Redis/DB 连接池 = TCP socket + 协议状态机 + 绑定的 event loop,三者都绑死单进程单 loop:同一条连接被多进程并发读写会导致协议帧错乱;asyncio 连接对象持有当前 loop 引用,跨进程 loop 无法驱动;spawn 下连 fd 都不继承。

→ **每个 worker 必须各自创建 Redis/DB/httpx 池**。共享的是后端服务实例本身(同一个 Redis / 同一个 PG),不是连接对象。能做的是**调小单池 + 文档写明总量预算**。

### 4.3 当前 DB 池对多 worker 是危险默认

`rock/admin/core/db_provider.py:43-45`(仅 PostgreSQL 生效,硬编码、不可配):

```python
pool_size = 100 # 每进程最多 100 条常驻连接
max_overflow = 0 # 100 为硬上限
pool_timeout = 120
```

乘法风险:`8 worker × 100 = 800`、`3 pod × 8 worker × 100 = 2400`,远超 PG 默认 `max_connections=100`。**第一个倒下的不是 CPU,而是 PostgreSQL**。

→ 多 worker 上线的**前置硬条件**:`pool_size` 改为可配,并按 `pool_size × workers × pods ≤ PG_max_connections` 反推。proxy 对 DB 基本只读 metadata、热路径走 Redis,可设很小。

## 5. 成功标准

- proxy role 以 `workers=N` 启动,N 个进程均分负载(SO_REUSEPORT/uvicorn master 分发),压测吞吐随 worker 数近似线性提升至核数上限。
- worker 子进程构建出**正确的 role 路由集合**(不会因 argv 丢失退回 admin)。
- admin role 仍为单进程,scheduler 不重复启动。
- `http_proxy`/`host_proxy` 复用共享 httpx client,不再每请求建连;共享 client 生命周期=进程,流式响应只关 response 不关 client。
- 单 Pod PG 连接总量 ≤ 现状(`pool_size = max(2, base // workers)` 保证不退化)。
163 changes: 163 additions & 0 deletions docs/_specs/proxy-multicore/02_design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# sandbox_proxy_router 多核化 — 设计

> 配套 `01_requirement.md`。本文给出四部分设计 + 容量预算 + 测试策略。

## 第 1 部分:多 worker 启动(app-factory + env 传参)

改造 `rock/admin/main.py`:

```python
# 顶层不再有 args = parse_args()(消除 import 期对 argv 的依赖)

def create_app() -> FastAPI:
"""工厂:每个 worker 子进程都会调用,只读 env,不碰 argv。"""
role = env_vars.ROCK_ADMIN_ROLE # 由主进程写入 env
env = env_vars.ROCK_ADMIN_ENV
app = FastAPI(lifespan=lifespan)
# CORS / 异常处理器 / 访问日志中间件 / include_router(按 role) 全部在此
if role == "admin":
app.include_router(sandbox_router, prefix="/apis/envs/sandbox/v1", tags=["sandbox"])
app.include_router(admin_ops_router, prefix="/apis/envs/sandbox/v1/ops", tags=["admin-ops"])
else:
app.include_router(sandbox_proxy_router, prefix="/apis/envs/sandbox/v1", tags=["sandbox"])
app.include_router(warmup_router, ...)
app.include_router(gem_router, ...)
return app

def main():
args = _parse_args() # 仅主进程
os.environ["ROCK_ADMIN_ROLE"] = args.role
os.environ["ROCK_ADMIN_ENV"] = args.env
workers = resolve_workers(args.role, args.workers, int(os.getenv("ROCK_PROXY_WORKERS", "0")))
uvicorn.run(
"rock.admin.main:create_app", factory=True,
host="0.0.0.0", port=args.port, workers=workers,
ws_ping_interval=None, ws_ping_timeout=None, timeout_keep_alive=30,
)

# rock/utils/worker.py(纯函数 util,无 I/O)
SINGLE_WORKER_ENVS = frozenset({"local", "test", "dev"})

def resolve_workers(role, override, env_workers, env=None) -> int:
if role != "proxy":
return 1 # admin 恒单进程(scheduler/Ray 单例)
if env in SINGLE_WORKER_ENVS:
return 1 # local/test/dev 强制单进程(fakeredis/in-mem 状态进程私有,多 worker 会不共享);优先级高于 override
if override and override > 0:
return override
if env_workers and env_workers > 0:
return env_workers
return 1 # 必须显式设置;不做 cpu_count 自动探测
```

要点:
- `resolve_workers` / `compute_pool_size` 放在 `rock/utils/worker.py`(纯函数 util,可单测)。
- `lifespan` 内所有 `args.env/args.role` 改读 `env_vars.ROCK_ADMIN_ENV/ROCK_ADMIN_ROLE`。
- 新增 CLI `--workers`(可选,覆盖 env)。
- `env_vars.py` 新增懒加载默认:`ROCK_PROXY_WORKERS`(默认 `0`)。**worker 数必须显式设置**(`--workers` 或 `ROCK_PROXY_WORKERS`);两者都未设则单 worker(=1),不按 cpu_count 自动探测。
- 运维侧建议 worker 数 `≤ min(物理核数, 可用内存/单进程RSS)`(见容量预算),由运维显式决定而非进程自选。

## 第 2 部分:连接池 / Metrics 治理(必要正确性)

每个 worker 各跑一遍 `lifespan` → 各自独立的 Redis 池、DB 池、httpx 池、MetricsMonitor。收口:

### 2.1 DB 池可配 + 按 worker 缩小

- `DatabaseConfig` 新增 `pool_size`(env 可覆盖),`db_provider.init()` 不再硬编码 100。
- proxy role 实际生效值:`pool_size = max(2, base // workers)`,`base` 默认 100。
- 整除兜底,给下限 2,避免 worker 很大时算出 0/1。
- 按 Pod 维度,`workers × (base//workers) ≈ base`,**对 PG 的压力与现状单进程一致,不退化**。
- proxy 几乎只读 metadata,生产可在此基础上进一步下调。
- admin role 保持单 worker → `pool_size = base`(=100,不变)。

### 2.2 Redis 池

同理可配、按需调小;Redis 连接成本低(每条几 KB),优先级低于 DB。

### 2.3 Metrics 多进程打标

多 worker 用相同 `user_defined_tags` 上报会互相覆盖/串味。`create_app()`/`lifespan` 构建 `MetricsMonitor` 时注入 `worker_pid`(`os.getpid()`)标签,使各 worker 指标可区分(或交由后端按 tag 求和聚合)。

### 2.4 日志文件并发(部署清空 + 各 worker append)

现状 `init_file_handler` 用 `mode="w+"`:多 worker 下每个进程启动都 **truncate 同一文件**、各持独立 offset 互相覆盖 → 日志错乱。修法是把"清空"从 FileHandler(每进程各清、会打架)挪到**部署时只清一次**,之后所有 worker 各自 append:

- `init_file_handler` 模式由新 env `ROCK_LOGGING_APPEND` 决定:置位 → `"a"`(append);**默认 `"w+"` 不变**,故 rocklet / cli 等单进程服务行为不受影响。
- admin/proxy `main()`(master,spawn worker 之前):先 `reset_log_file()` 清空一次,再 `os.environ["ROCK_LOGGING_APPEND"]="true"`;worker 继承 env → 全部 append。
- 安全性:master 在 import 期不写日志,清空发生在写入之前,不会在已有 writer offset>0 时 truncate 产生空洞。
- 残留边界(本地盘 + 正常行可忽略):单条 > BufferedWriter 缓冲(~8KB)的日志(如访问日志 dump 大 body)在多进程 append 下可能交错;NFS 不保证 O_APPEND 原子。需彻底免疫则改 per-pid 文件名(本期不做,需求方仅要"清空一次 + 各 pid append")。

## 第 3 部分:httpx 连接池复用(选定优化)

### 现状

- `_send_request`(控制面 JSON RPC)已用池化 `self._httpx_client` ✅。
- `http_proxy`(`sandbox_proxy_service.py:951`)、`host_proxy`(`:873`)**每请求新建 `AsyncClient`** ❌ → 反复 TCP+TLS 握手、无 keepalive 复用。

### 改造:拆两个共享 client

避免数据面长流(SSE/大响应)阻塞控制面短 RPC:

| client | 用途 | 超时 | 池 |
|--------|------|------|-----|
| `_rpc_client` | `_send_request` 控制面 | 短(`proxy_config.timeout`) | 小 |
| `_proxy_client` | `http_proxy` / `host_proxy` 数据面(含 SSE 流式) | 读超时放宽/无总超时 | 大 |

两者均在 `__init__` 创建、随进程存活,在服务关闭时统一 `aclose()`。

### 复用正确性要点

- 流式:`resp = await self._proxy_client.send(req, stream=True)`;生成器 `finally` **只 `await resp.aclose()`,绝不关闭共享 client**。
- 非流式:`aread()` 后 `resp.aclose()`;同样不关 client。
- **每请求超时**经 `build_request(timeout=...)` 覆盖,保留现有语义:SSE 无总超时 / 普通 120s / host_proxy 90s。
- 池上限 `max_connections`/`max_keepalive` 经 `ProxyServiceConfig` 可配;长连接占池槽会形成期望中的背压,需按并发量设值。
- WebSocket 走 `websockets.connect`,不受影响。

## 第 4 部分:容量预算(capacity budget)

上线前按本节填数,定 worker 数与各池大小。

### 4.1 连接数(可精确控制)

```
PG 总连接 = pool_size × workers × pod_数
约束: ≤ PG_max_connections(留余量给 admin/其他)
默认: pool_size = max(2, 100 // workers) → 每 Pod ≈ 100,与现状一致
```

例:PG `max_connections=500`,留一半给其他,proxy 预算 250;`250 / (8 worker × 3 pod) ≈ 10`,设 proxy `pool_size=10`。

### 4.2 内存(需实测校准)

- **worker 进程内存**:每个 worker 是完整 Python 进程(spawn 下几乎不共享),粗估单进程 RSS 100–300MB。`N worker ≈ N × 单进程RSS`,通常是多 worker 的**主要内存成本**,决定 worker 上限:运维显式设 `workers ≤ min(物理核数, 可用内存 / 单进程RSS)`(代码不自动按 cpu_count 选)。
- **PG 每连接内存**:常被引用为 5–10MB/连接,但**此为经验估计,非本系统实测**;`top`/`ps` 的 RSS 因含共享内存(shared_buffers)会高估。上线前以私有内存实测校准:

```bash
# 每个 PG backend 的私有内存(PSS,比 RSS 准)
for pid in $(pgrep -f "postgres:.*"); do
awk '/^Pss:/{s+=$2} END{printf "%.1f MB\n", s/1024}' /proc/$pid/smaps_rollup 2>/dev/null
done
# 连接现状
# SELECT count(*) FROM pg_stat_activity; SHOW max_connections;
```

> 原则:连接数用硬公式精确控制;内存数字标注为经验估计、以实测为准,不拿估计值当拍 worker 数的依据。

## 第 5 部分:测试策略(TDD)

进程 spawn 本身难单测,把可测逻辑抽出:

1. **`create_app()` 按 role 返回正确路由集**:proxy 含 `sandbox_proxy_router`、不含 `sandbox_router`;admin 反之。
2. **`resolve_workers(role, override, env_workers, env)`**(`rock/utils/worker.py`):admin 恒 1;**local/test/dev 恒 1(优先级高于 override,避免 fakeredis/in-mem 状态跨 worker 不共享)**;proxy 其余走 override>env>1;无 cpu_count 自动探测。
3. **DB 池公式**:`max(2, base // workers)` 的边界(workers 极大 → 2;admin → base)。
4. **httpx 复用**:调用 `http_proxy`/`host_proxy` 后断言**共享 client 未关闭、仍可用**;数据面/控制面使用各自共享实例(可注入 mock client)。
5. **SSE 流式**:断言生成器结束只 `resp.aclose()`,不 close client。

CI 标记:大多为快测;涉及真实转发的归 `integration`。

---
## 实现状态(2026-06-22)

已实现并通过 627 项 admin+sandbox 单测:env `ROCK_PROXY_WORKERS`;`DatabaseConfig.pool_size` 可配;`resolve_workers`/`compute_pool_size` 纯函数;`main.py` app-factory + 多 worker(仅 proxy role);lifespan 按 worker 算池 + proxy 退出 `aclose`;`SandboxProxyService` 拆 `_rpc_client`/`_proxy_client` + `worker_pid` Metrics 标 + `http_proxy`/`host_proxy` 复用共享 client(不关闭)。

实现期连带修复:`tests/unit/sandbox/test_proxy_enhancements.py` 原先 patch `httpx.AsyncClient` 构造器,因 `http_proxy` 不再每请求建连而改为注入 `service._proxy_client`。
Loading
Loading