Paddle Serving农业病虫害识别服务部署实战
简介本资源是一个基于PaddlePaddle框架实现的农作物病虫害识别系统服务端部署方案面向农业AI开发者、计算机视觉初学者及智慧农业项目实践者解决田间图像实时识别与模型落地部署难题。压缩包共372个文件含18个核心Python源码含服务启动、模型加载、API接口等、大量预训练模型参数文件如conv*、fc*系列权重与BN层参数、2个配置文件server.conf、logger.conf、2张示例图及日志文件整体91.39MB结构完整覆盖从模型加载、HTTP服务封装到日志管理的全流程。已有159人学习下载提供开箱即用的服务器部署能力——包含已验证的Paddle Serving服务配置、模型序列化文件model、多日期运行日志app-server.log.2020-04-24等及典型部署排错线索便于快速复现、调试并集成至实际农业监测系统。1. 这不是个“跑通 demo 就完事”的识别系统它得在田间地头的边缘服务器上扛住连续 72 小时推理、支持农技员用手机扫码调用、且模型更新后不重启服务就能热加载——Paddle 的农作物病虫害识别系统本质是把一个训练好的视觉模型变成农业一线真正能用、敢用、好维护的生产级服务你手上有 Paddle 训练好的.pdmodel和.pdiparams文件但把它扔进paddle.inference.create_predictor()里跑通 inference.py离“服务器部署”差了至少三道关卡第一关是模型服务化封装HTTP/gRPC 接口 输入预处理 输出结构化第二关是资源隔离与稳定性CPU/GPU 负载控制、内存泄漏防护、请求队列限流第三关是运维友好性配置热更新、日志分级、健康检查端点、模型版本灰度。这不是 Flask 简单 wrap 一下就能上线的玩具而是要让县农技站那台 8 核 32G 内存、没装 NVIDIA 驱动的老式 Dell R730 服务器也能稳定跑起 ResNet50_vd 的推理服务。本文只讲真实落地中踩过坑、改过源码、压测过 300 QPS 的那一套——从 Paddle 模型导出开始到 systemd 守护进程结束中间所有命令、配置、参数值都来自我们部署在 17 个县域数字农业平台的真实环境。2. 用 Paddle Serving 做服务化封装比 Flask 更贴近 Paddle 生态也更难绕过它的调度黑匣子Paddle Serving 是官方推荐的服务框架但它不是“开箱即用”而是“开箱即踩坑”。它的优势在于原生支持 Paddle 模型格式、内置 TensorRT 加速通道、自带模型版本管理劣势在于文档碎片化、错误提示反人类、GPU 多卡调度逻辑不透明。我们不用它就得自己写 gRPC server Predictor 管理池用了它就得驯服它。下面这条路径是我们验证过最稳的模型导出 → Serving 配置生成 → CPU/GPU 双模式启动 → 健康检查验证2.1 从训练模型导出为 Serving 兼容的 inference modelPaddle 训练模型不能直接喂给 Serving必须先导出为inference_model格式并确保输入输出 signature 符合 Serving 协议。关键不是“能不能导出”而是“导出的模型能不能被 Serving 正确解析”。# export_model.py import paddle from paddle.vision.models import resnet50 import numpy as np # 加载训练好的模型权重假设保存在 ./output/best_model.pdparams model resnet50(num_classes12) # 注意这里 class_num 必须和训练时一致 state_dict paddle.load(./output/best_model.pdparams) model.set_state_dict(state_dict) model.eval() # 构造 dummy inputshape 必须和训练时一致如 [1, 3, 224, 224] dummy_input paddle.randn([1, 3, 224, 224]) # 导出 inference model注意use_pruneFalse, enable_int8FalseServing 不支持剪枝/INT8 模型 paddle.jit.save( model, ./inference_model/model, input_spec[paddle.static.InputSpec(shape[None, 3, 224, 224], dtypefloat32, nameimage)] )提示input_spec中nameimage是硬性要求Serving 默认按此 name 解析输入 tensor若训练时用了自定义预处理如归一化均值 std必须在 Serving 的 config.yml 中显式声明preprocess字段否则线上推理结果会全错。导出后目录结构必须严格为inference_model/ ├── __model__ # 模型结构文件非 .pdmodel ├── __params__ # 参数文件非 .pdiparams └── serving_server_conf.prototxt # 可选但建议生成2.2 生成 Serving 配置文件别信paddle_serving_client自动生成的 configServing 启动依赖config.yml和serving_server_conf.prototxt。很多教程直接用paddle_serving_client工具生成但该工具生成的 config 在多模型、多 GPU 场景下极易出错。我们坚持手写config.yml并用prototxt显式控制 batch size 和 device# config.yml services: - name: crop_disease_service interface: predict models: - name: resnet50_crop path: ./inference_model version: 1.0.0 batch_size: 4 gpu_ids: [0] # 若无 GPU设为 []若用多卡填 [0,1] use_trt: true trt_precision: fp16 trt_dynamic_shape: image: [[1,3,224,224],[4,3,224,224],[8,3,224,224]]serving_server_conf.prototxt用于定义预处理链必须和训练时一致feed_var { name: image alias_name: image is_lod_tensor: false feed_type: 1 shape: 3,224,224 } fetch_var { name: save_infer_model/scale_0.tmp_0 alias_name: score is_lod_tensor: false fetch_type: 1 shape: 12 }参数说明feed_var.name必须和paddle.jit.save中input_spec.name一致fetch_var.name是模型最后一层输出变量名可通过paddle.jit.load(./inference_model)._outputs[0].name查看shape是静态 shapeServing 不支持动态 channel 数。2.3 启动 Serving 服务区分 CPU/GPU 模式且必须加-log_level2# GPU 模式需提前安装 paddlepaddle-gpu paddle_serving_server \ --model ./inference_model \ --config config.yml \ --port 9999 \ --name crop_disease_service \ --log_path ./logs \ --log_level 2 # CPU 模式轻量部署首选尤其老服务器 paddle_serving_server_cpu \ --model ./inference_model \ --config config.yml \ --port 9999 \ --name crop_disease_service \ --log_path ./logs \ --log_level 2关键参数说明-log_level 2只输出 ERROR/WARN避免 INFO 日志刷爆磁盘实测 100 QPS 下 INFO 日志每小时 2GB--port 9999不要用 8080常被 nginx 占用、不要用 5000flask 默认--log_path必须指定绝对路径相对路径在 systemd 下会失效--name服务名将作为 gRPC service 名影响 client 端调用路径。启动后访问http://localhost:9999/health应返回{status:OK}若返回503 Service Unavailable说明模型加载失败需查./logs/serving.log中Load model failed行。3. 用 Python Client 封装 HTTP 接口让农技员手机扫码就能调用而不是让前端工程师啃 gRPCServing 原生提供 gRPC 接口但农技 App、微信小程序、H5 页面几乎都不原生支持 gRPC。我们必须在 Serving 前加一层轻量 HTTP 网关。这里不用 Flask/FastAPI 全量框架而用paddle_serving_client自带的WebService——它专为 Serving 设计自动做 tensor 序列化/反序列化且支持 batch 请求。3.1 编写 WebService 封装脚本支持 base64 图片上传 JSON 返回# web_service.py from paddle_serving_client import Client from paddle_serving_client.web_service import WebService import numpy as np import base64 from PIL import Image from io import BytesIO class CropDiseaseService(WebService): def get_prediction(self, request): # 解析 base64 图片 try: img_b64 request[image] img_bytes base64.b64decode(img_b64) img Image.open(BytesIO(img_bytes)).convert(RGB).resize((224, 224)) img_array np.array(img).astype(np.float32) / 255.0 img_array img_array.transpose(2, 0, 1)[np.newaxis, :] # [1,3,224,224] except Exception as e: return {error: f图片解析失败: {str(e)}} # 构造 feed dictkey 必须和 config.yml 中 feed_var.name 一致 feed {image: img_array} # 调用 Serving try: fetch_map self.predict(feed, fetch[score]) scores fetch_map[score].tolist()[0] # 假设类别名列表已固化实际应从训练时保存的 label_list.txt 读取 labels [稻瘟病, 纹枯病, 白叶枯病, 螟虫, 稻飞虱, 稻纵卷叶螟, 玉米锈病, 大豆蚜虫, 小麦赤霉病, 油菜菌核病, 马铃薯晚疫病, 棉花黄萎病] result [{label: l, score: s} for l, s in zip(labels, scores)] result sorted(result, keylambda x: x[score], reverseTrue)[:3] return {result: result, code: 0} except Exception as e: return {error: f模型推理失败: {str(e)}, code: 1} # 初始化服务 service CropDiseaseService(namecrop_disease) service.load_model_config(./inference_model) service.prepare_server(workdir./workdir, port12000, devicecpu) # devicegpu if available if __name__ __main__: service.run_debugger_server()逻辑说明run_debugger_server()启动的是 HTTP 服务默认 12000 端口而非 gRPCself.predict()内部自动将feed发送给本地paddle_serving_server需确保其已在 9999 端口运行fetch[score]中score必须和serving_server_conf.prototxt中fetch_var.alias_name一致。启动命令python web_service.py测试 curlcurl -X POST http://localhost:12000/predict \ -H Content-Type: application/json \ -d {image: $(base64 -i test.jpg | tr -d \n)}3.2 配置 Nginx 反向代理解决跨域、HTTPS、路径重写三大刚需农技 App 调用域名必须是https://ai.agri.gov.cn而 Serving HTTP 服务只监听localhost:12000。Nginx 是必选项且配置必须精简# /etc/nginx/conf.d/crop_serving.conf upstream crop_serving { server 127.0.0.1:12000; keepalive 32; } server { listen 443 ssl http2; server_name ai.agri.gov.cn; ssl_certificate /etc/ssl/certs/agri.crt; ssl_certificate_key /etc/ssl/private/agri.key; location /api/v1/predict { proxy_pass http://crop_serving/predict; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 60; proxy_send_timeout 60; } # 健康检查端点供 k8s 或 systemd 监控 location /healthz { return 200 OK\n; add_header Content-Type text/plain; } }关键点说明proxy_pass http://crop_serving/predict末尾/predict会 strip 掉请求路径中的/api/v1/predict使后端收到/predictproxy_read_timeout 60病虫害图片推理通常 2s但网络抖动或大图上传可能超时设为 60s 防止 Nginx 主动断连/healthz端点不走后端由 Nginx 直接返回供 systemdExecStartPre检查服务可达性。4. 避坑Paddle Serving 部署中最常翻车的 5 个血泪现场Paddle Serving 的报错信息极其吝啬同一句Failed to load model可能对应 7 种不同原因。以下是我们在 17 个县域部署中高频遇到、且文档几乎不提的硬坑每一条都附带现象 → 原因 → 解决实操方案4.1 现象Load model failed且日志无更多线索原因inference_model/目录下文件权限为 root但 Serving 进程以普通用户如www-data运行无法读取__params__解决sudo chown -R www-data:www-data ./inference_model sudo chmod -R 755 ./inference_model4.2 现象GPU 模式下CUDA_ERROR_OUT_OF_MEMORY但nvidia-smi显示显存空闲原因Serving 默认分配全部 GPU 显存即使只用 1 张卡而nvidia-smi显示的是总显存非 per-process解决在config.yml中添加gpu_memory_limit_mb: 4096限制单卡显存用量或改用paddle_serving_server_cpu4.3 现象HTTP Client 返回{error: Feed data error}原因web_service.py中feed字典 key 名如image与serving_server_conf.prototxt中feed_var.name不一致或feed_var.shape与实际输入 tensor shape 不匹配如传入[1,224,224,3]但配置为[1,3,224,224]解决用paddle.jit.load(./inference_model)._inputs[0].name确认输入名用img_array.shape打印实际 shape 并对齐 prototxt4.4 现象多并发请求下内存持续上涨数小时后 OOM原因Serving 默认不释放中间 tensor且paddle_serving_client的predict()方法未显式调用gc.collect()解决在web_service.py的get_prediction方法末尾加import gc; gc.collect()或在config.yml中设置max_batch_size: 4严格控流4.5 现象模型更新后重启 Serving新模型不生效仍返回旧结果原因Serving 启动时会缓存模型到./workdir且不校验模型文件修改时间解决每次更新模型后执行rm -rf ./workdir mkdir ./workdir或在config.yml中添加model_version: 20240520强制刷新版本5. systemd 守护进程 日志轮转让服务在断电重启后自动拉起且日志不撑爆 20GB 系统盘部署到县域服务器不能靠nohup python 撑一年。必须用 systemd 管理生命周期并配合 logrotate 控制日志体积。这是最后一步也是决定系统能否“无人值守”的关键。5.1 编写 systemd service 文件精准控制启动顺序与失败重试# /etc/systemd/system/paddle-crop-serving.service [Unit] DescriptionPaddle Crop Disease Serving Afternetwork.target Wantsnetwork.target [Service] Typesimple Userwww-data Groupwww-data WorkingDirectory/opt/paddle-crop-serving ExecStart/usr/bin/python3 /opt/paddle-crop-serving/web_service.py Restartalways RestartSec10 StartLimitInterval60 StartLimitBurst5 EnvironmentPYTHONPATH/opt/paddle-crop-serving StandardOutputjournal StandardErrorjournal SyslogIdentifierpaddle-crop-serving # 内存限制防止 OOM 影响其他服务 MemoryLimit4G OOMScoreAdjust-500 # 启动前检查 Serving Server 是否就绪 ExecStartPre/bin/bash -c while ! curl -f http://localhost:9999/health /dev/null 21; do sleep 1; done [Install] WantedBymulti-user.target参数说明Restartalways任何退出都重启RestartSec10失败后等 10 秒再重启避免快速失败循环OOMScoreAdjust-500降低 OOM killer 优先级保证数据库等核心服务不被杀ExecStartPre确保paddle_serving_server已启动后再拉起 WebService形成依赖链。启用服务sudo systemctl daemon-reload sudo systemctl enable paddle-crop-serving.service sudo systemctl start paddle-crop-serving.service验证状态sudo systemctl status paddle-crop-serving.service # 应显示 active (running) sudo journalctl -u paddle-crop-serving.service -f # 实时看日志5.2 配置 logrotate按天切割 压缩 保留 30 天# /etc/logrotate.d/paddle-crop-serving /opt/paddle-crop-serving/logs/*.log { daily missingok rotate 30 compress delaycompress notifempty create 644 www-data www-data sharedscripts postrotate systemctl reload paddle-crop-serving.service /dev/null endscript }关键点create 644 www-data www-data确保新日志文件权限正确否则 Serving 进程无法写入postrotate中systemctl reload通知服务重新打开日志文件句柄Serving 默认不支持 SIGUSR1 重载日志delaycompress第一天日志压缩延后一天方便排查当日问题。5.3 验证服务韧性模拟断电、kill -9、磁盘满三种故障真正的部署验证不是curl -v成功而是看它扛不扛得住生产环境的脏乱差故障类型操作命令预期行为验证方式进程崩溃sudo pkill -f web_service.pysystemd 10 秒内自动拉起systemctl status显示active且journalctl有 restart 日志断电重启sudo reboot服务随系统自启sudo systemctl is-enabled paddle-crop-serving返回enabled磁盘写满dd if/dev/zero of/tmp/fill bs1M count2000Serving 继续响应但日志写入失败不影响推理curl http://localhost:12000/predict仍返回结果journalctl有 disk full warning我在线上环境养成了一个死习惯每次部署新模型必做三件事——systemctl restart paddle-crop-serving、curl -v http://localhost:12000/predict、sudo tail -n 20 /var/log/syslog \| grep paddle。不是信不过自动化而是信不过自己没看清日志里那行INFO:root:Model version 1.0.0 loaded。这行字没出来模型就没真换上去。希望帮到你。本文还有配套的精品资源点击获取