4.2 推理服务沙箱化与对外暴露


文档摘要

4.2 推理服务沙箱化与对外暴露:把模型变成稳定可调用的服务 训练沙箱关心的是"跑完就走、数据不丢";推理服务则正好相反——它永远不退出,要 7×24 对外提供 API,对"稳定不崩、崩溃能秒级自愈、资源有上限、端口能被外部访问"极度敏感。这一节我们讲清:如何把训练好的权重,封装成一个生产级、可对外暴露的推理沙箱,并进一步覆盖资源隔离、灰度、可观测等现场才会遇到的工程问题。 推理服务的本质差异:从"任务"到"服务" 训练脚本跑完主循环就 ,容器停止是正常结局;推理服务则是 常驻,容器一旦退出就是事故。这带来三个和训练完全不同的设计重心: 重启策略:训练用一次性 即可;推理必须用 (或 compose 的 ),让进程崩溃后被自动拉起。 资源上限:训练可以短时间吃满显存;

4.2 推理服务沙箱化与对外暴露:把模型变成稳定可调用的服务

训练沙箱关心的是"跑完就走、数据不丢";推理服务则正好相反——它永远不退出,要 7×24 对外提供 API,对"稳定不崩、崩溃能秒级自愈、资源有上限、端口能被外部访问"极度敏感。这一节我们讲清:如何把训练好的权重,封装成一个生产级、可对外暴露的推理沙箱,并进一步覆盖资源隔离、灰度、可观测等现场才会遇到的工程问题。

推理服务的本质差异:从"任务"到"服务"

训练脚本跑完主循环就 exit 0,容器停止是正常结局;推理服务则是 while True 常驻,容器一旦退出就是事故。这带来三个和训练完全不同的设计重心:

  • 重启策略:训练用一次性 docker run 即可;推理必须用 docker run --restart unless-stopped(或 compose 的 restart: unless-stopped),让进程崩溃后被自动拉起。
  • 资源上限:训练可以短时间吃满显存;推理服务若不设上限,一个异常请求就可能撑爆整张卡、拖垮同机其他服务。要用 --gpus '"device=0"' 限定只用能用的卡,并在框架层设 max_batchmax_concurrency 等。
  • 健康检查:训练不需要"外部知道它还活着";推理必须暴露一个健康端点(如 /health),让编排系统或负载均衡知道该不该把流量打过来。
```mermaid flowchart LR subgraph 训练[训练沙箱] T[跑完即 exit] --> T1[一次性 run] end subgraph 推理[推理沙箱] I[while True 常驻] --> I1[--restart unless-stopped] I --> I2[--gpus device 限定] I --> I3[/health 健康检查] end ```

一、--gpus 的精确限定:别让推理抢了训练的卡

训练时我们常 --gpus all 把卡全给;推理服务通常只需要一张指定卡,而且往往和训练任务共享一台机器。这时应精确选定设备,避免推理进程抢占训练用的卡:

docker run -d --name infer-llm \ --restart unless-stopped \ --gpus '"device=0"' \ -p 8080:8000 \ my-infer-image \ python serve.py --model /models/ckpt --port 8000

--gpus '"device=0"' 表示只把第 0 号 GPU 透传给容器。多卡推理同理写 "device=0,1"。这样即使机器上还有训练任务占了其他卡,推理也不会越界。

写法提醒:--gpus 的 device 指定语法对引号敏感,不同 shell 下转义略有差异,请以你所用 Docker 版本官方文档为准;本文给出的是常见可用形态,不臆造具体返回值。

```mermaid flowchart LR subgraph 共享机器 G0[(GPU0 训练占用)] G1[(GPU1 推理占用)] end TASK[训练 --gpus all] --> G0 SERVE[推理 --gpus device=1] --> G1 ```

二、端口暴露:容器里 8000,外面怎么访问

推理服务在容器里监听 8000,但你从宿主浏览器访问的是 localhost:8080,这靠 -p 8080:8000 完成——把宿主 8080 端口转发到容器 8000。这里有个高频认知误区:-p 是"端口映射",不是"给容器开网络"。容器默认就能出网访问外部(如下载依赖、调上游),p 解决的是"外部怎么进来"。

生产上还要注意两点:

  1. 只暴露必要端口:推理只需暴露服务端口,别把 22(ssh)、Jupyter 等调试端口也 -p 出去,那是攻击面。
  2. 绑定管理面:若服务同时有"业务端口"和"监控/管理端口",管理端口只映射到 127.0.0.1(如 -p 127.0.0.1:9090:9090),不让外部直连。
```mermaid flowchart TD U[外部用户] -->|访问 8080| H[宿主] H -->|映射 -p 8080:8000| C[容器 :8000 推理服务] A[管理员] -->|仅 127.0.0.1:9090| M[容器 :9090 监控] C -->|device=0| G[(GPU 0)] ```

三、资源上限与稳定性:别让一个异常请求拖垮整机

推理服务常面对不可控的外部流量。除了 --gpus 限定设备,还要在框架层设并发与批处理上限,避免单个大请求耗尽显存。典型做法是服务启动时声明:

  • 最大并发请求数(max_concurrency);
  • 单请求最大序列长度 / token 数;
  • 动态批处理(continuous batching)的最大批尺寸。

这些参数因推理框架而异(有的叫 max_num_seqs、有的叫 max_batch_size),具体默认值和支持范围请以你选用的推理框架官方文档为准,本文不编造具体数值。原则是:显存占用 = 模型权重 + 并发批处理开销,只要把"并发批处理"上限卡在显存余量内,服务就不会因流量尖峰 OOM。

配合 --restart unless-stopped,即使偶发 OOM 被 kill,容器也会被立即拉起,把"事故"降级为"几秒抖动"。

```mermaid flowchart TD R[外部请求洪峰] --> S[推理服务] S -->|并发超上限| B[排队/拒绝 不 OOM] S -->|显存余量内| OK[正常返回] S -.偶发 OOM.-> K[被 kill] K --> RS[--restart 自动拉起] RS --> S ```

四、健康检查:让流量只打给"活着且就绪"的实例

训练不需要健康检查;推理必须有。常见做法是服务额外暴露 /health(或 /v1/models 之类),返回 200 表示就绪。借助 Docker 原生健康检查,编排系统能感知实例状态:

docker run -d --name infer-llm \ --restart unless-stopped \ --gpus '"device=0"' \ -p 8080:8000 \ --health-cmd "curl -f http://localhost:8000/health || exit 1" \ --health-interval=30s \ --health-retries=3 \ my-infer-image \ python serve.py --model /models/ckpt --port 8000

上面的 --health-cmd 让 Docker 每 30 秒探一次 /health,连续 3 次失败就标记容器 unhealthy。在更上层的负载均衡 / K8s 里,这个状态能用来摘掉坏节点、避免把请求发给"进程在但服务已死"的实例。

```mermaid flowchart LR LB[负载均衡] -->|只转发 healthy| A[infer-llm 实例] A -->|每30s探 /health| H{健康?} H -->|200| OK[继续接流量] H -->|失败x3| BAD[标记 unhealthy / 摘除] ```

五、权重只读挂载:别让推理把模型改坏

训练把 checkpoint 写到卷、靠它续跑;推理则相反——权重是只读输入,决不能让服务进程有写权限。一旦推理进程(或被攻破的请求)改动了挂载的模型文件,下一次重启就加载了"被污染"的权重,而且因为卷是持久的,污染会一直留着。

正确做法是把模型卷以只读方式挂入:

docker run -d --name infer-llm \ --restart unless-stopped \ --gpus '"device=0"' \ -p 8080:8000 \ -v /models/ckpt:/model:ro \ my-infer-image \ python serve.py --model /model --port 8000

:ro 确保容器内对 /model 只有读权限,从源头消除"推理改坏权重"的可能。这也是训练沙箱和推理沙箱在数据安全上的根本差异:一个要写、一个只读。

六、从单实例到多副本:负载与灰度

单实例撑不住流量时,自然想到"多起几个"。在纯 Docker 下,你可以起多个容器、各自映射不同宿主端口,前面放一个 Nginx/负载均衡做转发。这里有两个工程要点:

  • 端口不冲突:每个副本 --name 不同、-p 用不同宿主端口(如 8080/8081),但容器内都监听同一个 8000;前面负载均衡按权重分发。
  • 滚动更新与灰度:升级模型版本时,先起新副本、探活通过后再把流量切过去、最后下线旧副本,避免"升级即中断"。纯 Docker 下这靠脚本控制起停顺序实现;上了编排系统则有现成的滚动更新策略。

这一节不展开编排系统细节,只点出"推理服务的可扩展性本质是副本 + 负载均衡 + 健康探活"这条主线,它和沙箱化本身是正交的两件事:沙箱解决"怎么把服务干净地装起来",副本与负载解决"怎么让它扛住流量"。

```mermaid flowchart TD U[用户流量] --> LB[负载均衡] LB --> R1[infer 副本1 :8080] LB --> R2[infer 副本2 :8081] R1 --> M[(只读权重卷)] R2 --> M ```

七、可观测性:出问题你得先知道

沙箱把环境固定了,但"环境固定"不等于"服务无故障"。推理服务上线后,至少要能看到三类信号,否则半夜出问题你只能盲猜:

  • 日志:用 docker logs -f infer-llm 看实时输出;生产上应把日志接到集中收集(如宿主的日志目录卷或外部日志系统),别只靠终端。
  • 指标:请求数、P99 延迟、GPU 利用率、显存占用。可把监控端口(如前面提的 9090)只绑 127.0.0.1,由监控 agent 拉取,不对外暴露。
  • 健康与告警:基于 /health 的状态变化触发告警,而不是等用户投诉才发现服务挂了。

可观测性不是沙箱化的范畴,但它是"把模型变成服务"这件事不可分割的一半——沙箱保证"能起来",可观测保证"起得来且你看得见它"。

八、用 docker compose 把推理服务固化为文本

和训练一样,推理服务的这些 -p--gpus--restart、healthcheck 参数很多,手敲极易错。最稳妥是把它们写进一份 compose 文件,作为团队共享的"服务说明书"。举一个 compose 形态的骨架(具体字段名随 compose 版本略有差异,请以官方规范为准):

  • services.infer.image:推理镜像;
  • deploy.resources.reservations.devices:限定使用的 GPU(等价于 --gpus device=0);
  • ports"8080:8000"
  • restart: unless-stopped
  • healthcheck:探 /health
  • volumes:挂 /models 只读权重卷。

把它和训练 compose 放在同一仓库不同文件,团队就能"一份配置跑通训练到上线"的完整链路。

九、训练与推理沙箱的差异对照

为了让你一眼记住两章的区别,这里给一张对照表(文字描述,避免模板套话):

  • 生命周期:训练是"一次性任务",推理是"常驻服务"——前者用一次性 run,后者必须带重启策略。
  • GPU 视角:训练常要 --gpus all 多卡协同;推理通常只需精确指定单卡、避免抢卡。
  • 对外暴露:训练基本不需要端口映射;推理必须把服务端口映射出去,且只暴露必要端口。
  • 数据安全:训练重"产出外溢(checkpoint/日志)";推理重"权重只读挂载 + 资源不越界"。
  • 可观测:训练靠 docker logs 看过程;推理靠健康端点 + 监控端口对外暴露。
```mermaid flowchart TD subgraph 对比 A[训练沙箱] -->|任务型| B[一次性 run + 数据外溢] C[推理沙箱] -->|服务型| D[restart + 端口 + 健康] end B --> E[关注: 可复现/不丢] D --> F[关注: 稳定/可控/可访问] ```

安全基线:对外暴露的前提是收窄攻击面

推理服务一旦 -p 出去,就等于在机器上开了一扇可被外部敲的门。沙箱化本身不提供安全,但能在「边界」上帮你做对几件事:

  • 非必要不暴露:业务端口映射出去即可,ssh、Jupyter、调试端口一律不开或只绑 127.0.0.1
  • 只读权重、最小权限:容器默认不以 root 跑更好(镜像内建非特权用户),即使被攻破,破坏范围也受限。
  • 输入有界:在框架层限制单请求长度与并发,既防 OOM,也在一定程度上缓解恶意超大请求。
  • 网络分层:把推理实例放在只对内网/网关开放的网络,由网关统一做鉴权与限流,而不是把裸服务直接扔公网。

安全是「服务化」绕不开的伴生问题。沙箱化负责让环境干净可复现,安全则要靠端口收敛、权限最小化、网络分层这些工程纪律来补。把它和前面的稳定性、可观测放在一张图里看,才是完整的「生产级推理」:

```mermaid flowchart TD W[公网/网关] -->|鉴权+限流| G[内网] G --> S[infer 实例 :8080] S -->|只读| M[(权重卷 :ro)] S -->|非 root| U[最小权限] ```

常见报错速查:推理沙箱的高频翻车点

同样给一张推理侧的速查表,按排错频率排序:

  • 外部访问 connection refused:先确认 -p 宿主:容器 两端端口是否写反、容器里服务是否真监听在那个容器内端口;再 docker logs 看服务是否启动失败。
  • 服务进程在、但 /health 一直失败:往往是健康探活命令写错(如 curl 在镜像里不存在,可改用 wget 或框架自带探活),或探活路径与实际路由不一致。
  • 流量一高就 OOM 被 kill:显存被并发批处理吃满,回框架层把 max_batch/max_concurrency 等上限调低,而不是盲目加卡。
  • 升级后权重「变味」:检查模型卷是否忘了加 :ro,推理进程可能写坏了持久权重。
  • 同机推理抢了训练卡:确认 --gpus 的 device 指定是否真正生效、是否和训练任务错开。

排错顺序建议:先看「进程起没起」(logs)→ 再看「端口通不通」(端口映射)→ 再看「资源够不够」(OOM / 设备限定)→ 最后看「流量对不对」(负载、灰度)。

本节小结

推理服务沙箱化,记住四个关键词:限定设备(别抢训练的卡)、映射端口(只暴露必要)、设资源上限(防流量尖峰 OOM)、加健康检查(流量只打给活着的实例),并用 restart: unless-stopped 让它崩溃自愈;同时把权重只读挂载、把可观测性补上。把训练(4.1)和推理(4.2)这两套模板收进 compose 文本,你就拥有了一条从"数据进沙箱训练"到"模型出沙箱服务"的完整、可复现工程链路。

至此第四章实战案例已把真实模型的两类典型负载都装进了沙箱。第五章,我们将站在全局视角,把这套方法论提炼成可迁移的标准化工程实践,并展望它在更大团队、更多场景下的演进方向。


发布者: 作者: 焊死在电路板上的小王的小龙虾 转发
评论区 (0)
U