本节摘要:裸进程跑通之后是交付问题:换一台机器还能跑吗、崩了能自动拉起吗、内存会不会吃穿宿主机。本节用一份 Dockerfile 解决三件事——环境钉死(Python 版本与 laya 版本写进镜像)、健康检查(容器编排据它判断存活并自动重启)、资源限额(CPU 部署的内存与算力天花板,防单容器吃穿宿主机);再单独处理 laya 特有的问题:检查点要么构建期预下载进镜像(启动即用、镜像变大),要么挂载缓存卷(镜像精简、依赖运行时下载),两种策略按交付场景选。文末给一份部署自检清单。所有配置为写法示意,参数以 Docker 与 laya 官方文档为准。
裸进程部署 laya-serve 的三个痛点:环境漂移(宿主机 Python 版本、依赖版本随时间分叉,某天升级踩坑);故障恢复靠人(进程崩了要人肉拉起,夜里没人盯);资源无界(一次异常流量把内存吃穿,殃及同机其他服务)。容器化分别回应:镜像把环境钉死成不可变工件;健康检查加重启策略让恢复自动化;资源限额给容器画硬顶。laya 的 BERT 级检查点 CPU 可跑(官方 README 口径),这让「一台普通 CPU 机器跑一个决策服务」成为现实部署形态,也让限额配置变得必要——CPU 上的算力与内存比 GPU 金贵。
本章的容器写法面向单机 docker。若你的团队已有 Kubernetes 或其他编排平台,Dockerfile 原样可用,把第三节的 run 参数翻译成对应的资源字段即可——镜像层的工作是通用的,编排层各有各的语言。
# Dockerfile —— laya-serve 容器镜像(写法示意,以官方仓库 README 与 Docker 文档为准) FROM python:3.12-slim # 只装服务必需组件;slim 底座减小攻击面与体积 RUN python -m pip install --no-cache-dir "laya[serve]" # 非 root 运行:容器内服务进程权限最小化 RUN useradd --create-home --uid 1000 laya USER 1000:1000 # 构建期预下载检查点到镜像内缓存(二选一策略,见第四节) # RUN python -c "from laya import Router; Router(preload='laya-multilingual')" EXPOSE 8000 # 容器级健康检查:探测 4.1 节的健康端点(路径以实际服务为准) HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')" || exit 1 # 启动服务;监听 0.0.0.0 才能被容器外访问 CMD ["laya-serve", "--host", "0.0.0.0", "--port", "8000"]
四个设计意图。slim 底座:laya-serve 是 CPU 推理服务,不需要完整 Debian 工具链。非 root:决策服务不需要特权,权限越小越安全(这一条继承自通用容器实践)。HEALTHCHECK 指向 4.1 节的健康端点:编排器(或 docker 本身)据此判断容器是否该被重启。CMD 监听 0.0.0.0:容器内监听 127.0.0.1 会导致端口映射后外部不可达,是容器化最常见的第一个坑。
构建这条镜像的预期时间感(量级示意):基础依赖安装几分钟、检查点预下载(若启用)取决于网络、其余层秒级。第一次构建最慢,之后每次只重建改动层——把这一点与第 4.1 节的冒烟脚本串起来,就得到了最短发布回路:改配置、重建尾部层、起新容器、跑冒烟、切流量。
# bash —— 带限额运行(写法示意) docker build -t laya-serve:cpu . docker run -d --name laya-serve \ --memory 3g --memory-swap 3g \ --cpus 2.0 \ -p 8000:8000 \ --restart unless-stopped \ laya-serve:cpu # 验收:健康状态应为 healthy,随后发一条 4.1 节的冒烟请求 docker inspect --format "{{.State.Health.Status}}" laya-serve curl -s -X POST http://127.0.0.1:8000/v1/systemone -H "Content-Type: application/json" -d '{...}'
限额怎么定(示意起点,按实测调):内存要覆盖「检查点常驻加推理峰值加 Python 运行时」三块,两个 BERT 级检查点常驻已要数 GB 量级(示意值),从 3 GB 起步、压测后上调是稳妥路线;CPU 配额决定吞吐上限,BERT 级前向在 CPU 上吃满核是常态,配 2 核起(示意值),并发需求高时优先横向加容器而不是纵向加核。--restart unless-stopped 配合 HEALTHCHECK 完成「崩了自动拉起、坏了自动摘除」的闭环。
两个限额细节值得单独一说。其一,--memory-swap 与内存设成同值等于禁用交换分区:推理服务内存超限就该被杀掉重启,而不是慢吞吞地换页——决策服务慢到超时与死了没有区别,还更难被发现。其二,限额要在压测时验证「真的顶得住」:拿第 3.2 节的基准流量打到配额边缘,观察是被平稳限住(预期)还是雪崩式排队(说明配额过低或并发策略要调)。
第 3.1 节说过检查点首调自动下载,容器化时这道题变成二选一:
| 策略 | 做法 | 得到 | 失去 |
|---|---|---|---|
| 构建期预下载 | Dockerfile 里加预下载层(上文注释行) | 启动即用、离线环境可用、行为可复现 | 镜像体积增大数 GB 量级(示意值) |
| 运行时挂载 | 把宿主机缓存目录挂进容器 | 镜像精简、检查点可独立更新 | 首次启动慢、依赖网络或预先准备缓存卷 |
判据是交付形态:给客户离线交付、或要求「镜像即完整工件」的可复现部署,选预下载;自己机房联网部署、检查点更新频繁,选挂载。预下载层放在 Dockerfile 尾部(依赖安装之后),检查点变更时只重建那一层,前面的层还能吃缓存。
顺着分层说两句构建效率。Docker 的层缓存规则是「某层变了,它之后全部重build」,所以顺序就是策略:最不常变的放前面(系统底座、pip 依赖),最常变的放后面(检查点、配置)。再配一个 .dockerignore 把本地缓存、练习文件挡在构建上下文之外,镜像不会意外混入无关内容。每次改 Dockerfile 只动尾部,构建时间就从分钟级回到秒级(量级示意)。
单容器吞吐不够时,扩展方向是横向复制:同一镜像起多个容器,前置一层负载均衡把流量摊开。
┌─ laya-serve 容器 1 调用方 ──▶ 负载均衡 ├─ laya-serve 容器 2 └─ laya-serve 容器 3 每个容器独立限额、独立健康检查;均衡器按健康状态摘流
要点三个。其一,多容器无共享状态:每个容器各自持有检查点(内存各占各的),不存在「主从同步」这类问题——这是决策服务好扩展的原因。其二,健康检查是摘流依据:某个容器 OOM 重启期间,均衡器自动把流量给健康的实例。其三,容量核算按「容器数乘单容器吞吐」做,单容器吞吐用第 3.2 节的基准方法测——没有实测数字的容量规划是猜谜。均衡器的选型与配置超出本书范围,以基础设施团队的既有标准为准。
横向扩展的启动顺序也有讲究:新容器加入前先单独跑一次冒烟(4.1 节的四步),确认预热完成、辖区正确,再挂到均衡器后面——没预热的容器第一波请求会慢得像故障,引来不必要的告警。缩容同理:先摘流、再停容器,让在途请求自然排空。
| 参数(示意) | 作用 | 起点建议(示意值) |
|---|---|---|
| --memory | 容器内存硬顶 | 3g 起步,压测后调 |
| --cpus | CPU 配额 | 2.0 起步,看单容器吞吐需求 |
| --restart | 崩溃自拉起 | unless-stopped |
| HEALTHCHECK 间隔 | 存活探测频率 | 30s |
| -p 端口映射 | 对外暴露 | 与服务监听端口一致 |
| 镜像 tag | 版本钉死 | 具体版本号,禁用 latest |
| 检查项 | 命令(示意) | 通过标准 |
|---|---|---|
| 容器健康 | docker inspect 的 Health.Status | healthy |
| 冒烟请求 | curl POST /v1/systemone | 返回结构完整、四字段齐全 |
| 路由正确 | 返回的 checkpoint 字段 | 中文样本落在多语言检查点 |
| 限额生效 | docker stats | 内存与 CPU 不超配额 |
| 重启自愈 | docker restart 后再冒烟 | 恢复服务且延迟正常 |
| 版本钉死 | 镜像 tag 与依赖清单 | laya 版本明确、可追溯 |
清单六项跑一遍通常不到五分钟,建议固化成发布单的一部分——「跑过了」与「跑通过并留下记录」在事后复盘时是两种完全不同的底气。
容器化解决了「交付与运行」的问题。还有最后一类场景:目标运行时装不动整套训练栈、或者要把推理嵌进别的进程——这时候把检查点导出成 ONNX 工件,用轻量推理运行时扛起来。下一节处理这步减法。