
上个月我帮一家做园区安防的团队做算力下沉改造现场 20 多路摄像头、十几个传感器、三台 Jetson AGX Orin 设备之前全靠人工去每台设备上分别跑模型、分别维护、分别拉数据。改完以后设备接入、模型推理、结果回传全部收敛到一个 Kubernetes 集群里EdgeX 负责把那些不同协议的传感器变成统一格式K8s 负责把推理任务分配到合适的节点整体平台像一台边缘小数据中心一样运转。这篇文章就是这个项目的完整复盘适合正打算把 AI 推理从云端搬到边缘、又不想被设备和算力碎片化问题折磨的人。1. 为什么边缘AI平台非要碰Kubernetes和EdgeX1.1 边缘场景让人头疼的三件事第一件是设备协议太杂。一个车间里可能同时存在 Modbus RTU 电表、Modbus TCP 温控器、MQTT 上报的振动传感器、走 HTTP 的摄像头状态接口。没有中间层的时候每个推理模块都要自己对接协议、自己处理数据格式代码里到处是 if-else。第二件是算力资源分散。Jetson 设备性能不差但单机跑大模型或同时跑多路视频推理时显存和 CPU 都很紧张。缺一个统一调度层就很容易出现一台设备已经在 OOMKilled 边缘挣扎另一台却在摸鱼。第三件是运维方式跟不上。边缘端总有断电、断网、重启单机部署的模型一挂就只能现场处理。每次升级模型都得逐台登录设备改完还要祈祷进程别崩。用 Kubernetes 之后Pod 挂了自动拉起来模型更新走滚动发布这体验完全不一样。1.2 三个组件各自解决什么问题Kubernetes 解决编排问题它不生产算力但能把分散在各个边缘节点的 GPU、内存、存储当作一个资源池统一管理让推理任务按需调度到合适位置。EdgeX Foundry 解决设备接入问题它提供了一整套设备服务框架统一了设备接入、数据采集、数据分发、命令控制的标准范式。不管底层是 Modbus 还是 MQTT对上输出的是统一的数据模型。AI 推理框架解决模型运行问题把训练好的模型封装成可调用的服务。YOLOv8 跑视觉检测、llama.cpp 跑轻量大模型各司其职。这三层合起来边缘推理平台才真正具备“企业级”的形态设备可管理、算力可调度、模型可更新。1.3 什么情况下不必上这套组合如果你的场景只有一台设备、一个模型、一个人维护那 K8s 加 EdgeX 就是杀鸡用牛刀。前期的学习成本和部署成本都挺高的单机 Docker Compose、甚至直接跑脚本反而更合适。我判断是否值得上的标准很简单设备节点超过 3 台或者推理任务超过 2 种或者你希望有远程更新能力满足其中一个就值得上。如果三个都满足基本没有悬念。2. 平台架构设计与硬件选型心得2.1 整体数据流整个平台的数据流是这样一个顺序传感器和摄像头先把原始数据交给 EdgeX 设备服务层EdgeX 经过归一化和清洗之后通过消息总线推送给规则引擎或直接路由到推理服务推理服务处理完结果一部分回写 EdgeX 的 Core Data另一部分直接发给上层的业务展示系统。控制流则是反方向外部或平台下发控制指令经过 K8s API 或者 EdgeX 的 Command API最终通过设备服务把指令翻译成具体的 Modbus 寄存器写入或 MQTT 主题消息。这个设计的关键在于解耦。EdgeX 不用关心 YOLOv8 的 TensorRT 引擎怎么加载推理服务也不用关心电表数据是从串口还是网口来的。2.2 Jetson AGX Orin 作为主力节点的理由这次选型没什么悬念。Jetson AGX Orin 64GB 这个型号显存带宽和内存容量都能同时扛住一路 YOLOv8 视频推理和一个 7B 参数的量化大模型实测下来 7B 模型用 llama.cpp 做词元推理可以做到每秒 20 到 30 个词元这吞吐在边缘设备里已经够用了。另一层考虑是 JetPack 生态。Jetson 设备上的 JetPack 自带 CUDA、cuDNN、TensorRT不仅省了编译这些底层库的时间很多模型结构还能直接用 TensorRT 做定点化加速。相比之下带独立显卡的边缘服务器体积大、功耗高很多现场环境放不下。2.3 一套经过验证的软件版本组合我直接在项目里验证过的版本组合这里也列出来参考组件版本选择说明操作系统Ubuntu 22.04 on L4TJetPack 5.1.2 自带 L4T r35KubernetesK3s v1.27.7轻量、内嵌 containerd适合边缘EdgeXEdgeX Foundry Jakarta支持 Redis Streams 作为消息总线运行时containerd NVIDIA Container ToolkitJetson 跑 GPU 容器必须的运行时推理框架TensorRT 8.5、ONNX Runtime 1.16、llama.cpp按任务类型选模型YOLOv8n-seg、Qwen2-7B-Instruct Q4_K_M视觉和语言任务验证模型这个组合不是最新版本但稳。EdgeX 新版本如 Levski 也支持类似机制但不建议在关键项目里当小白鼠。3. EdgeX 设备接入层处理千奇百怪的设备协议3.1 EdgeX Core Services 在 K8s 中的部署策略EdgeX 有多个核心微服务比如 core-data、core-metadata、core-command、system-management 以及 Consul 和 Redis。每个服务在 K8s 里都是一个 Deployment建议用 Helm Chart 统一编排。部署时要注意存储依赖。Redis 负责持久化 EdgeX 的关键元数据和最近数据不用持久化历史的配置appendonly加定期 RDB 快照就够了。Consul 管配置它自己也有存储这两者的持久化卷建议放到本地存储因为边缘环境通常没有分布式存储可供依赖。启动顺序上必须保证 Consul 和 Redis 先起来然后是 core-metadata再是 core-data 和 core-command。否则设备服务注册时会找不到配置中心启动失败后反复重启而且 K8s 默认的重启策略对这种注册失败也没太多办法。3.2 device-modbus 配置实战Modbus 在企业现场太常见了。EdgeX 的device-modbus通过 Device Profile 描述数据模型通过 Device 描述具体设备的连接参数两者缺一不可。Device Profile 的一个典型逻辑是定义设备的资源类型。以读取一个温湿度传感器为例profile 需要把温度和湿度的寄存器地址、数据类型、读写权限都描述清楚。比如温度寄存器地址是 0x0001、占 16 bit、类型是 Float32并且按 0.1 倍率缩放这样外部通过Reading拿到的就是正常物理值。设备实例配置则告诉 EdgeX 怎么连设备deviceList: - name: facility-temp-humid-01 profileName: TempHumidProfile protocols: - name: modbus-tcp address: 192.168.1.20 port: 502 unitID: 1 attributes: - { name: Temperature, initialRead: true } - { name: Humidity, initialRead: true }实际踩过的一个坑是 Modbus 的字节序问题。很多国产设备的寄存器字节序和标准的大端小端不一致读取出来数值完全不对。EdgeX 的 Modbus 设备服务在数值解析上也比较粗碰到这种非典型设备往往要在 EdgeX 和推理服务之间加一层格式修正而不是直接改 EdgeX 源码。3.3 边缘端数据清洗该做什么很多人以为 EdgeX 只是个数据搬运工其实它是合适做轻量数据清洗的好地方。比如摄像头流不适合走 EdgeX 这种消息型总线实时视频推流更合适走 RTSP。需要做的清洗包括几类把非数值型设备值转换成 float把明显异常值过滤掉比如模数转换失败读到的 65535把设备按业务含义打成统一的 tag 体系比如site_id、device_type这样推理服务拿到的数据就不用再猜字段意义。先把数据形态统一了后面做推理和展示都会轻松很多。4. 基于 K3s 的 Kubernetes 编排让推理任务长在集群上4.1 为什么选 K3s 而不是标准 K8s边缘节点往往是 x86 工业计算机加 ARM 设备的混搭环境网络经常抖动带宽也有限。标准 K8s 控制面太重etcd 在弱网和高延迟下容易出各种问题。K3s 用 SQLite 替代 etcd把控制面组件打包进一个二进制内存占用小得多安装也快非常适合边缘。K3s 不是玩具Pod 调度、Deployment 滚动更新、Ingress、持久卷这些 K8s 核心能力都完整支持。在这个项目里x86 节点作为 Master 和通用计算节点三台 Jetson 作为 Agent 加入集群形成一个小的混合架构集群。4.2 边缘集群初始化要点最省心的方式是内网离线安装。提前在能联网的机器上下载 K3s 的安装脚本和二进制通过docker save或直接拷贝包的方式分发到每台节点。启动时给 Agent 节点加上--node-label标记设备类型这是后面做 GPU 调度的重要依据。# 服务端 curl -sfL https://get.k3s.io | sh -s - server \ --write-kubeconfig-mode 644 \ --disable traefik \ --disable servicelb # Agent 节点 curl -sfL https://get.k3s.io | K3S_URLhttps://server-ip:6443 \ K3S_TOKENtoken sh -s - agent \ --node-label rolejetpack \ --node-label modelagx-orin \ --node-label inferenceyes这里把traefik和servicelb禁掉是有意的。边缘平台根本不暴露公网入口控制在局域网内访问即可。还有一种情况是现场网络极差Agent 加入集群后频繁断连。K3s 会自己重连但需要控制面的 Node 状态判断要慢一点。好在 K3s 在设计上支持节点漂移后自动重新注册只要 Token 一致恢复网络后 Pod 会自动恢复调度。4.3 把 GPU 推理 Pod 调度到正确的节点边缘集群里异构节点共存是常态。x86 节点可能在跑轻量的数据转发任务Jetson 则在跑 GPU 密集型推理。如果 Pod 被调度到没有 GPU 的节点启动后就会因为找不到 CUDA 库而失败。做法分两步先给节点打标签和污点再给推理 Pod 加节点亲和和容忍。apiVersion: apps/v1 kind: Deployment metadata: name: yolo-inference spec: selector: matchLabels: app: yolo-inference template: metadata: labels: app: yolo-inference spec: nodeSelector: role: jetpack tolerations: - key: inference operator: Exists effect: NoSchedule containers: - name: yolo image: registry.local/ai/yolo-service:1.4.2 resources: limits: nvidia.com/gpu: 1Jetson 设备要用 Kubernetes 管理 GPU 资源需要安装 NVIDIA Device Plugin 的版本配合 NVIDIA Container Toolkit。装好之后K8s 才能像调度资源一样调度 GPU。这一步不做后面的资源限制就是空谈一个 Pod 可以把显存吃满影响同节点其他推理任务。5. AI 推理服务化从 YOLOv8 到 llama.cpp5.1 推理服务容器化的一般做法推理服务容器化的目标有两个运行环境一致性、快速迭代。Jetson 上跑容器最基础的工作是让 Docker 或 containerd 认到宿主机的 CUDA 库。JetPack 自带这些库容器内部只要继承自对应版本的基础镜像就能直接跑。除了库依赖另一个必须做的是把模型权重文件放到外部持久卷里而不是打进镜像。镜像打包模型会让镜像体积动不动十几个 GB更新还要重新打镜像拉镜像太繁琐。持久卷方案下模型更新就是替换文件加触发加载动作速度快得多。5.2 llama.cpp 在 Jetson 上的部署与关键参数大模型在边缘跑主流方案就是 llama.cpp。在 Jetson 上部署需要启用 CUDA 后端编译命令是关键一步官方默认的 CPU 编译跑出来的性能完全是两个量级。cmake -B build -DGGML_CUDAON -DCMAKE_CUDA_ARCHITECTURES80 cmake --build build --config Release -j 6这行命令是给 Orin 的 Ampere 架构用的-DCMAKE_CUDA_ARCHITECTURES80对应 Jetson AGX Orin 的 GPU 架构。如果选错可能导致编译能过但运行时出现 illegal instruction。启动服务的时候有几个参数必须调-ngl决定把多少层模型放入 GPU-c决定上下文长度-b是张量批大小。实测下来 7B 模型在 AGX Orin 上-ngl 999 -c 2048 -b 512的效果比较均衡模型能全部装进显存推理速度稳定在每秒 20 到 30 词元之间。如果同一个节点还要跑 YOLOv8 视觉任务就不能把显存全部留给大模型建议改成-ngl 40控制显存占用调度上通过 K8s 资源限制确保两者不互相挤爆显存。5.3 模型更新不能每次重启整个容器边缘场景更新模型的频率不低。特别是有新的业务需求时发现旧模型误检率太高就需要立刻换一个版本的权重文件。如果用老办法打新镜像再滚动更新至少需要几分钟时间且因为镜像体积大在弱网环境下极容易超时。我的做法是把模型统一放在一个 NFS 或本地持久卷目录推理服务每隔几秒检查模型文件的时间戳或校验值发现变化就自动重新加载。整个流程从替换文件到模型生效只花几秒不需要重启容器也没有观测上的中断。6. 踩坑实录六个让我折腾到半夜的问题6.1 消息总线地址解析DNS 和 Kubernetes ServiceEdgeX Jakarta 默认用 Redis Streams 作为消息总线各微服务通过地址edgex-redis:6379互相通信。在 K8s 里这个地址能不能被解析取决于 Service 名称和命名空间是否匹配。我当时犯的错是把 EdgeX 的所有组件打进了edgex命名空间但有个 device service 的配置里仍写的是localhost:6379导致在容器里一直连不上 Redis。这个问题的排查过程就是典型的网络分层问题先看 Pod 网络通不通再查 DNS 解析结果最后才发现是配置里写死了 localhost。容器里的localhost是容器自己不是宿主机。6.2 边缘断网后的集群与设备服务行为边缘环境非常考验断网恢复。K3s 在这种场景下的行为是已经调度的 Pod 继续运行但新 Pod 无法调度因为控制面联系不上 Agent。EdgeX 设备服务在断网时的表现不尽相同有的会进入无限重连有的会抛异常退出。遇到这种情况最好的处理是给设备服务 Pod 设置适当的存活探针和停止超时。我建议用tcpSocket探活探测设备地址断网时让 Pod 主动重启恢复网络时让服务重新注册到 Consul。不要依赖 EdgeX 默认的重试机制它往往不像 K8s 的探针来得可靠。6.3 LLM 服务 OOMKilled 排查链有一次 llama.cpp 服务频繁进入 CrashLoopBackOffK8s 事件显示 OOMKilled但free -h显示系统内存还有剩余。这个现象让我很困惑后来才发现问题出在Jetson 上统一内存架构让 GPU 显存和系统内存共享总量GPU 吃到上限后进程再申请 CPU 内存就会触发容器的 OOM。解决思路是三步走先确认 llama.cpp 的参数里-ngl是否过大模型层全部塞进 GPU 导致显存占满然后限制容器的系统内存资源给同节点的其他任务留出空间最后是给 llama.cpp 服务加探活确保模型加载慢的时候不会因为启动超时被误杀。6.4 其它几个值得记录的小坑第一Jetson 上跑 GPU 容器Docker 或 containerd 必须配置好 nvidia-container-runtime否则会报could not select device driver nvidia。装了 JetPack 也要手动在/etc/docker/daemon.json里加默认运行时。第二EdgeX 的 device-mqtt 对主题订阅的 QoS 支持有限某些场景下丢消息。如果要求高可靠建议让设备直接上报到 EMQX 这类独立消息中间件再由 EdgeX 通过 MQTT 订阅而不是把设备数据直接送进 EdgeX 的 MQTT 里转一圈。第三K8s 滚动更新推理服务时旧 Pod 还没退出新 Pod 已经在加载模型如果模型加载时间长可能出现两个 Pod 同时占用大块显存的情况直接把节点资源打满。给 Deployment 配上maxSurge: 0保证先停旧建新。第四离线环境里imagePullPolicy默认的IfNotPresent有时也不够用。现场设备如果更换过镜像源镜像 tag 没有变但内容不一致会一直拉到旧的。建议离线环境统一使用带 build 号或 commit 号的 tag杜绝这种隐性问题。以上就是整个平台落地的完整记录。从设备协议处理到算力编排再到模型服务化每一层单独拿出来都不算难但把它们组合在一套边缘环境里才能体会到一体化平台带来的管理和运维优势。如果你也在搞类似的项目希望这些经历可以帮你少走点弯路。