OCR服务从实验室走向生产环境时,最先遇到的问题往往不是识别率,而是部署。模型动辄几百兆,依赖库版本冲突,CPU推理慢、GPU环境难配,这些坑让不少团队在上线阶段反复折腾。把OCR服务容器化,是当前比较主流的解法:镜像一次构建,到处运行,配合编排工具还能实现水平扩容。这篇文章就围绕容器化OCR服务的部署,从镜像构建、服务封装到负载均衡完整走一遍流程。

一、选择OCR引擎与基础镜像
目前开源OCR引擎里,PaddleOCR和Tesseract是最常被拿来对比的两个。PaddleOCR在中文场景下识别精度明显占优,检测、方向分类、识别三段式流水线完整,缺点是依赖较重,镜像体积容易失控。Tesseract轻量稳定,适合英文票据、简单印刷体场景,资源占用小,冷启动快。如果业务以中文证件、表格为主,建议优先PaddleOCR。
基础镜像的选择直接决定镜像体积和构建速度。对于PaddleOCR这类依赖paddlepaddle框架的服务,推荐使用官方的python:3.9-slim作为底座,再手动安装推理库,比直接用完整版python镜像能省下几百兆空间。如果需要GPU加速,则改用nvidia/cuda系列基础镜像,例如11.7-cudnn8-runtime版本,注意一定要选runtime而不是devel,后者带有完整的编译工具链,体积几乎大一倍,生产环境完全用不上。
一个实用技巧是把模型文件与代码分离。PaddleOCR的预训练模型加起来有上百兆,如果直接COPY进镜像,每次模型更新都要重建镜像层。更好的做法是把模型放到独立的目录,通过卷挂载进容器,镜像只保留框架代码和依赖,这样模型迭代和镜像构建互不干扰。
二、编写Dockerfile与服务封装
Dockerfile的核心思路是利用分层缓存:把变动频率低的依赖安装放在前面,变动频繁的业务代码放在后面。下面是一个可用的PaddleOCR服务镜像示例:
FROM python:3.9-slim
# 先装系统依赖,中文场景需要字体支持
RUN apt-get update && apt-get install -y \
libgomp1 libglib2.0-0 libsm6 libxext6 \
fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# 单独拷贝依赖清单,利用缓存层
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \
-i https://pypi.tuna.tsinghua.edu.cn/simple
COPY . /app
# 模型目录通过卷挂载,不打入镜像
VOLUME ["/app/models"]
EXPOSE 8866
CMD ["python", "server.py", "--port", "8866"]服务封装层建议用FastAPI或Flask暴露HTTP接口,比paddlehub自带的预测服务更灵活,方便加上鉴权、限流和批量接口。请求处理上要注意一点:图片不要以base64塞在JSON里传输大图,超过5MB的扫描件建议改用multipart文件上传,能省掉一次编码解码的CPU开销。
另外别忘了在容器内设置工作进程数。OCR推理是CPU密集型任务,单进程跑不满多核机器,用gunicorn或uvicorn开多个worker,进程数设为CPU核数的70%左右比较合适,留一点余量给系统和其他进程。
三、Compose编排与健康检查
单机多实例可以用Docker Compose拉起,配合Nginx做负载均衡。编排文件里重点配置三块:资源限制、健康检查、重启策略。OCR服务加载模型时内存峰值明显,不设memory限制容易被系统OOM杀掉,设得太小又会在模型加载阶段直接失败,建议按实测峰值乘以1.5倍预留。
version: "3.8"
services:
ocr-worker:
build: .
volumes:
- ./models:/app/models
deploy:
resources:
limits:
memory: 4g
cpus: "2"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8866/health"]
interval: 30s
timeout: 5s
retries: 3
restart: on-failure
deploy:
replicas: 3
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- ocr-worker健康检查接口不要偷懒只返回200,最好真正跑一次小图推理,这样能探测到模型文件缺失、显存异常这类半死不活的状态。Nginx侧配置proxy_next_upstream,某个worker超时或报错时自动切换到下一个实例,配合upstream的max_fails参数,可以把故障节点快速摘除。
日志方面,容器内服务日志直接打到stdout和stderr,交给Docker的json-file驱动统一收集,注意在daemon.json里配置log-opts的max-size,否则高并发下日志文件会无限膨胀把磁盘打满。识别结果中包含敏感信息的场景,记得在日志层做脱敏,不要把整张图片的base64原样打出来。
四、常见坑与排查思路
中文识别乱码是最常见的报障原因之一,多数情况是镜像里没装中文字体,检测模型把整段文字框出来但识别阶段输出空串或方块。前面Dockerfile里安装fonts-noto-cjk就是为解决这个问题。如果识别结果里英文正常、中文全是问号,优先排查字体;如果整图都识别不出来,则要检查模型文件路径挂载是否正确。
内存溢出问题通常出在批量接口上。一次传入几十张大图,每个worker同时在内存里解码多张图片,峰值内存轻松翻几倍。解决办法是在应用层加并发控制,比如用信号量限制同时处理的图片数,队列积压时直接返回429,让客户端降速重试,而不是硬扛到OOM。
GPU场景还有个高频问题:容器里nvidia-smi正常,但推理时报CUDA初始化失败。这通常是基础镜像的CUDA版本与宿主机驱动不匹配导致的,宿主机驱动版本决定了支持的CUDA上限,选镜像前先用nvidia-smi确认驱动支持的版本号,再对齐选择,能少走很多弯路。部署时记得加--gpus all参数或使用nvidia-container-runtime,否则容器根本看不到GPU。
容器化本身不复杂,难的是把识别引擎的特性摸透。把镜像瘦身、模型分离、健康检查、弹性伸缩这几件事做扎实,一套稳定可扩展的OCR服务基本就成了。后续如果规模上来,再平滑迁移到Kubernetes,加HPA按CPU负载自动扩缩容,整体架构不需要推倒重来。