上一篇把高通跃龙IQ-9100平台部署AWS Greengrass的初始化链路走完之后,我这边的核心设备已经在 IoT Core 里显示在线,Nucleus 也稳定跑了好几天。但很多同行问我的问题其实是从这里才开始的:组件到底怎么开发?写个 Python 服务丢上去为什么一直起不来?本地调试和云端发布到底怎么衔接?这篇就把这条链路完整过一遍,从 Recipe 和 Artifact 的模型开始,到 GDK 建工程、手写生命周期、Greengrass CLI 本地联调、最后推到云端部署到设备组,再把 aarch64 Linux 上特有的一些组件坑整理出来。适合已经装好 Greengrass Core、准备在这类边缘盒子上跑自研组件的工程师直接参考。
1. 先把组件模型拆明白:Recipe 与 Artifact 到底谁管谁
1.1 Recipe 是说明书,Artifact 是交付物
Greengrass V2 里一个组件由两部分组成:Recipe 和 Artifact。很多新人上来就写代码,然后问为什么部署失败,因为根本没搞明白这两个东西的分工。
Recipe 是一份元数据文件,YAML 或 JSON 都行,它描述的是"这个组件是什么、装在哪些平台、安装时干什么、启动时干什么、依赖什么、配置项有哪些"。你可以把它理解成快递单上的配送说明:写着收货人、楼层、要不要冷藏、放门口还是打电话。Artifact 则是实际要送到设备上的"货":Python 脚本、编译好的二进制、模型文件、配置文件,都算。
| 维度 | Recipe | Artifact |
|---|---|---|
| 格式 | YAML / JSON | 文件、目录、压缩包 |
| 作用 | 描述组件元数据与生命周期 | 提供组件运行时的实际依赖 |
| 谁来消费 | Greengrass Nucleus、部署服务 | 设备上的组件进程 |
| 版本管理 | 跟随组件版本 | 跟随组件版本,放在 S3 或本地目录 |
这两者为什么要分开?最直接的原因是部署系统需要先读 Recipe 决定"要不要装、怎么装、装完跑什么",而 Artifact 往往很大或者很多,没必要跟着元数据一起下发。设备端收到部署任务后,会先解析 Recipe,再去下载 Artifact,最后执行生命周期脚本。分离之后,同一个组件逻辑可以针对不同硬件或者不同 BSP 版本写不同的 Recipe,Artifact 这一层则保持通用,这在边缘设备上非常实用。
1.2 生命周期:安装、运行、退出和恢复
Recipe 里最核心的是ComponentLifecycle,它定义了组件进程在设备上的各个阶段行为。Greengrass V2 的生命周期主要有以下步骤:
Install:安装阶段,通常用来创建目录、设置权限、安装依赖、校验设备节点。这个阶段适合做一次性准备工作,不算常驻进程的启动。Run:运行阶段,这个脚本必须前台常驻,不能启动后台进程后自己退出。这是我认为初学者最容易踩的第一个坑。Shutdown:停止阶段,做回收资源、落盘状态、通知外部系统等收尾动作。Recover:恢复阶段,Greengrass 在组件异常退出后、尝试重启之前会先执行这里的内容,适合做清理工作或者重置状态。
做个生活化类比:Install 相当于开店前打扫卫生、摆货架;Run 是开门营业,人必须在店里;Shutdown 是打烊关灯锁门;Recover 是出了事故之后,先清理现场再重新开业。
1.3 依赖与平台约束为什么要写死
Recipe 里还有两个经常被忽略的字段,ComponentDependencies和Platforms。前者声明组件依赖的 Nucleus 最低版本,或者依赖其他公共组件(比如流管理器、CLI)。后者限制组件可安装的操作系统和架构。
我这里反复强调这两个字段,是因为 IQ-9100 这类平台跑的多是 Yocto 裁剪出来的 Linux,库版本、Python 版本、甚至内核特性都可能和标准发行版不一样。如果你的组件明确知道自己需要 Nucleus 2.8 以上的特性,不写版本约束,部署到旧核心设备上就会在运行时出现诡异行为。平台字段里的架构正则建议直接写/x86_64|arm64/,或者干脆arm64,这样就不会误部署到不匹配的设备上。
2. 搭建组件开发环境:在 x86 工作站上为 IQ-9100 准备工程
2.1 先摸清设备三件事:架构、Libc、Python 版本
做边缘组件开发的第一步不是写代码,而是把目标设备的三件套搞清楚。我每次拿到新设备都会先跑这三条命令:
uname -m cat /etc/os-release python3 --version在 IQ-9100 上通常会得到类似aarch64的输出,系统是 Yocto 定制的 Linux,Python 版本可能是 3.9 到 3.11 之间。这三个信息决定了你后续所有选型:
uname -m决定交叉编译目标,以及去 PyPI 找 wheel 包时要选的平台标签。/etc/os-release能帮你确认是不是精简系统,缺了哪些运行时库,决定要不要用静态链接或者自携带依赖。python3 --version决定组件里能不能用高版本语法,以及 pip 依赖的兼容范围。
这一步看似简单,但真的有人跳过。之前帮同事排查一个组件,x86 工作站上跑得飞起,丢到 IQ-9100 上直接报告解释器路径不存在。最后发现是用本机 glibc 编的二进制,BSP 里的 glibc 版本太低,加载器直接罢工。所以先摸清楚环境,后面所有问题都好定位。
2.2 语言选型:Python 打底,C/C++ 留给性能热点
Greengrass V2 官方支持自定义组件用 Python、Node.js、Java,以及任意语言的二进制。我在这类 AI 边缘平台上的实际选型策略很简单:
| 语言 | 适合场景 | 注意事项 |
|---|---|---|
| Python | 业务逻辑、数据采集、胶水代码、快速迭代 | 依赖多时要小心 wheel 的 aarch64 支持 |
| Node.js | 已有前端/协议栈团队、事件驱动服务 | 运行时体积偏大,Yocto 系统不一定带 |
| C/C++ | NPU 推理封装、视频处理、实时控制 | 交叉编译链和 BSP 库版本必须对齐 |
| Shell | 极简启动脚本、环境准备 | 不适合常驻业务,别硬写 |
IQ-9100 这类平台真正值钱的是 NPU 和 GPU,所以我的习惯是:业务调度层用 Python 写,跑得快,改得快;真正吃性能的推理执行器用厂商原生 SDK 编成.so或者独立二进制,Python 通过子进程或者本地 IPC 调用。这样组件本身的权限边界、日志、生命周期管理都归 Greengrass 管,性能热点还是掌握在原生代码手里。
2.3 用 GDK 把工程脚手架拉起来
Greengrass Development Kit(GDK)是一个本地命令行工具,能帮你初始化组件工程、构建、发布。安装和执行初始化:
pip install greengrass-development-kit-cli gdk component init -l python以 Python 模板为例,初始化完的目录大概长这样:
. ├── gdk-config.json ├── component_recipe.yaml └── artifacts └── com.example.iq9100.infer └── 1.0.0 └── main.py几个文件的作用:
gdk-config.json:记录发布用的 AWS 账号、区域、S3 存储桶,以及组件名和版本。发布前要把这里填对,否则gdk component publish会直接报错。component_recipe.yaml:上面讲的 Recipe,GDK 模板会给你一个可运行的默认版本,后面主要改它。artifacts/<组件名>/<版本号>/:Artifact 目录,目录层级必须和组件名、版本号一一对应,这个约定在后面本地联调和云端发布都会用到。
如果现场没有外网不能装 GDK,也可以手工建这个结构,只要目录布局和 Recipe 对得上,Greengrass CLI 一样能用。GDK 只是把工程规范和发布动作自动化,并不是必需的运行时依赖。
3. 手写 Recipe 到本地联调:5 分钟跑起第一个组件
3.1 一个能用的 Python 组件 Recipe 长什么样
我直接给一个可以本地联调的 Recipe 示例,组件名用com.example.iq9100.infer,后面统一拿它举例:
--- RecipeFormatVersion: '2020-01-25' ComponentName: com.example.iq9100.infer ComponentVersion: '1.0.0' ComponentDescription: IQ-9100 NPU inference wrapper component ComponentPublisher: edge-team ComponentConfiguration: DefaultConfiguration: input_topic: edge/infer/input output_topic: edge/infer/output ComponentDependencies: aws.greengrass.Nucleus: VersionRequirement: '>=2.8.0' Platforms: - OS: linux Architecture: /x86_64|arm64/ ComponentLifecycle: Install: Script: | mkdir -p /var/run/iq9100-infer chmod 777 /var/run/iq9100-infer Run: Script: | cd {artifacts:decompressedPath} python3 -u main.py \ --input-topic {configuration:/input_topic} \ --output-topic {configuration:/output_topic}逐个解释关键点:
RecipeFormatVersion固定写'2020-01-25',这是 V2 的版本号,别乱改。- 组件名的命名空间一定要带点号,推荐
com.公司名.业务名,不要用aws.开头,那部分命名空间是 AWS 公共组件保留的。 ComponentConfiguration.DefaultConfiguration是组件运行的默认配置,生命周期脚本里可以用{configuration:/字段名}引用,部署时还能按需覆盖。ComponentDependencies写上核心依赖的版本下限,设备端 Nucleus 版本不够时,部署任务会直接失败或拒绝,而不是装上后再炸。Platforms里架构写成正则/x86_64|arm64/,方便同一份 Recipe 在开发机和目标设备上都能跑。Install阶段做目录准备,这个阶段不适合启动服务,因为它是短生命周期脚本。Run阶段通过cd {artifacts:decompressedPath}进入 artifact 目录再执行 Python 脚本,-u是为了让 print 和日志不要被缓冲,确保 Greengrass 能及时把输出收进组件日志。
3.2 main.py:组件进程的正确写法
对应上面的 Recipe,main.py 要接受两个命令行参数,然后一直在前台循环。这是我常用的骨架:
#!/usr/bin/env python3 import argparse import logging import time logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", ) logger = logging.getLogger("iq9100.infer") def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--input-topic", default="edge/infer/input") parser.add_argument("--output-topic", default="edge/infer/output") return parser.parse_args() def main(): args = parse_args() logger.info("infer component started, waiting for tasks...") while True: # 真实项目里这里做:等待输入、调用 NPU 推理、写结果 time.sleep(5) if __name__ == "__main__": main()这里最要紧的一件事:while True不能去掉。Greengrass 判断组件是否正常,就是看 Run 脚本对应的进程有没有退出。如果你在图省事,把服务用&丢到后台然后脚本结束,Greengrass 会认为组件停了,然后按策略反复重启,日志里会是一堆莫名其妙的重启记录。
另外一个细节是日志。Greengrass 会把组件进程的 stdout 和 stderr 写到/greengrass/v2/logs/<组件名>.log,所以只要用 print 或者 logging 输出到标准输出,就能被统一收集。这也是为什么我坚持在 Python 脚本里做规范的 logging,而不是把日志写到某个自定义文件,后者在排查问题时要多绕一层。
3.3 Greengrass CLI 三板斧:create、deploy、看日志
在把组件推到 AWS 云上之前,先在设备本地做一轮联调是最有效率的方式。前提是核心设备上已经装了aws.greengrass.Cli公共组件(这个是安装 Nucleus 时默认带上的)。
把工程目录传到 IQ-9100 后,执行:
# 1. 把组件注册到本地 Greengrass sudo /greengrass/v2/bin/greengrass-cli component create \ --recipe component_recipe.yaml \ --artifacts artifacts # 2. 发起一次本地部署 sudo /greengrass/v2/bin/greengrass-cli deployment create \ --merge "com.example.iq9100.infer=1.0.0" # 3. 查看部署状态 sudo /greengrass/v2/bin/greengrass-cli deployment status命令跑完后,重点看两个地方:一是部署状态是不是Success,二是组件日志。
sudo cat /greengrass/v2/logs/com.example.iq9100.infer.log我每次本地联调时,习惯先只部署、不写业务逻辑,让组件空转起来,确认生命周期链路是通的,再往里填业务。这一步能帮你把"部署机制的问题"和"业务代码的问题"在第一时间分开。如果一上来就跑完整业务,日志里既有框架报错又有业务报错,很容易被带偏。
3.4 配置与依赖绑定:别把参数写死在代码里
Greengrass 组件的一个好处是配置可以在部署时动态下发,不需要改代码。我们 Recipe 里定义了input_topic和output_topic的默认值,在云端创建部署时,可以传入一个配置覆盖值:
configurationUpdate: "com.example.iq9100.infer": input_topic: edge/camera/face_bounding_box output_topic: edge/audit/face_event生命周期脚本里的{configuration:/input_topic}会在部署时被渲染成真实值,然后作为命令行参数传给进程。组件内部不需要关心全局配置管理,只需要在启动时解析参数。
依赖版本也要在这一阶段验证。如果你的组件依赖新的 IPC 特性,而设备上 Nucleus 版本偏旧,deployment create时可能会提示依赖不满足。这时候先升级 Nucleus,再谈业务。IQ-9100 的 BSP 升级成本高,所以我的建议是尽量把核心依赖版本约束放宽,给 BSP 升级留出时间窗口。
4. 云端部署全流程:从构件上传到设备组灰度
4.1 GDK 发布:构件进 S3,元数据进 Greengrass
本地联调通过之后,就要把组件发布到云端,让 Greengrass 服务端能把它部署到任意一个设备组。GDK 两条命令搞定:
gdk component build gdk component publishbuild会把artifacts目录整理成可发布的形态;publish会把 Artifact 上传到配置好的 S3 存储桶,然后在 IoT Greengrass 服务里创建或更新组件版本。如果你没有在gdk-config.json里显式指定存储桶,GDK 会默认创建一个以greengrass-component开头的存储桶来放东西。
发布成功之后,到 IoT 控制台的 Greengrass 组件页面,就能看到com.example.iq9100.infer的1.0.0版本。这一步等于把"说明书"和"货"都上传到了云端,设备端后续部署时按图索骥地下载执行即可。
有一点要注意:组件的版本号是不可变的。同一个版本号发布后就不能再改内容,要更新代码就必须升版本号。所以组件的版本管理建议跟语义化版本走:主版本、次版本、补丁版本分开,至少保证每次发布到生产前,版本号是明确递增的。否则遇到问题想回滚,你会分不清哪个版本才是上一个稳定版。
4.2 用 IoT 设备组做分批复用:测试组与生产组
云端部署的目标不是单个设备,而是 IoT 事务组(Thing Group)。我的一般做法是给 IQ-9100 建两个组,比如iq9100-staging和iq9100-prod。先在 staging 组上做一轮灰度部署,确认业务稳定后,再往 prod 组推同一个组件版本。
控制台操作路径是:IoT Core -> Greengrass -> Deployments -> 创建部署 -> 选择目标设备组 -> 勾选组件版本 -> 按需填配置覆盖和部署策略。
如果习惯命令行,以 stage 组为例:
aws greengrassv2 create-deployment \ --target-arn "arn:aws:iot:ap-northeast-1:111111111111:thinggroup/iq9100-staging" \ --components '{"com.example.iq9100.infer":{"componentVersion":"1.0.0"}}' \ --deployment-policy '{"failureHandlingPolicy":"DO_NOTHING"}'部署策略里failureHandlingPolicy有两个常用值:DO_NOTHING表示部署失败后保持现状,不动其他组件;ROLLBACK表示自动回滚到部署前的组件版本。我的经验是灰度阶段用DO_NOTHING,因为回滚会触发额外的一轮构件下载和设备重启,反而不利于定位问题;等业务稳定后,生产环境再考虑开ROLLBACK。
4.3 验证部署状态:设备端和云端各看什么
部署任务是异步下发的,不是点了创建瞬间生效。提交后,云端状态会经历IN_PROGRESS、SUCCEEDED或FAILED,设备端则会看到组件被更新或新建。
云端验证:
aws greengrassv2 list-effective-deployments \ --core-device-thing-name iq9100-device-01设备端验证:
sudo /greengrass/v2/bin/greengrass-cli component list如果组件状态是RUNNING,说明部署链路已经通了。这时候再去翻日志确认业务逻辑正常。这里有个习惯我一直很推荐:部署完成后,先在设备上确认组件确实在跑,再去控制台刷新状态。因为控制台的状态是设备上报的,存在网络延迟和汇报周期,而设备端component list看到的是真实运行时状态。
5. 常见问题与排查实录:我在 IQ-9100 上踩过的组件坑
5.1 进程没起来 vs 组件状态 finished
这是新手问得最多的一个问题:云端部署显示成功,设备上component list里组件状态却是FINISHED或反复在重启。
原因大概率出在 Run 脚本上。最常见的两种:
- 脚本里启动后台进程后直接退出,比如
python3 main.py &。 - 脚本执行的是一个会退出的一次性命令,不是常驻服务。
排查方法很简单:看组件日志尾部有没有 "run script" 结束的记录,或者直接手动在设备上执行 Run 脚本里的命令,如果命令本身跑完就退出,那组件必然会是 FINISHED。改正思路就是让业务进程前台常驻,或者用 Shutdown/Recover 生命周期管理优雅退出和异常清理。
5.2 本地二进制与 BSP 的 glibc 不匹配
这个坑在商业 Yocto BSP 上特别常见。现象是部署成功,但组件日志里出现:
error while loading shared libraries: libxxx.so.1: cannot open shared object file或者更隐蔽的:
No such file or directory明明文件存在,却报这个错,往往是 ELF 的解释器路径不对。原因就是编译环境用的 glibc 版本比 BSP 里的高,动态加载器找不到。
我的处理策略:
- 优先在设备上直接编译,绕开交叉编译的 glibc 兼容问题。
- 必须交叉编译时,和 BSP 厂商确认对应的工具链版本和 sysroot,不要随便用 Ubuntu 自带的交叉编译器,也不要默认
GNU libc版本。 - 机器学习的 numpy、pydantic 这类带原生扩展的 Python 包,先确认 PyPI 上有没有
manylinuxaarch64 的 wheel,没有的话就只能在设备上从源码装,需要预先在 BSP 里带上编译工具链。
5.3 云上部署成功但设备上毫无动静
有时候控制台显示部署成功,设备端却迟迟没有拉取新版本。这种情况大多不是 Greengrass 的问题,而是设备与云端的连接状态或者网络出口不通。
先看 Nucleus 日志:
sudo cat /greengrass/v2/logs/greengrass.log重点确认 MQTT 连接是否正常,TLS 握手有没有报证书或时间问题。边缘设备经常犯的一个低级错误是系统时间和真实时间差太多,导致 TLS 证书校验失败。IQ-9100 如果放在内网长期不上线,没有配置 NTP 的话,很容易出现这种问题。解决方案是把 NTP 同步加进系统基础服务,确保部署前设备时间偏差在分钟级以内。
5.4 快速排查表
| 现象 | 可能原因 | 先做哪一步 |
|---|---|---|
| 组件状态 FINISHED | Run 脚本退出或后台化 | 手动执行 Run 命令验证是否常驻 |
| 日志出现 shared library error | glibc / so 版本不匹配 | ldd检查依赖,对比 BSP |
| 部署成功但进程没更新 | 设备时间偏差、连接中断 | 看 Nucleus 日志,同步 NTP |
| Install 一直失败 | 安装脚本权限不够、磁盘满 | 检查/greengrass/v2/logs和df -h |
| 组件无权限访问硬件设备 | ggc_user 不在设备节点权限组 | 按 BSP 文档调整 udev 规则或用户组 |
Greengrass 组件在 Linux 上默认以ggc_user运行。如果组件需要访问 GPIO、串口或者 NPU 设备节点,就要确认这个用户对/dev/下对应节点有权限。我一般在 BSP 定制阶段就把ggc_user加进硬件访问组,或者在 udev 规则里给目标设备节点授予组权限。不要在 Recipe 里直接把服务跑在 root 下,除非你真的清楚自己在做什么,这会破坏整个设备的权限边界。
组件日志是最直接的排查入口,先看它,再猜原因。省掉这一步,往往会在错误的方向上浪费时间。
最后分享一个对我帮助很大的习惯:每台 IQ-9100 设备出厂后,我先在它上面做一轮"空转验证"——部署一个只有 install 和 run 的空组件,跑 24 小时,确认日志、重启、配置覆盖这些机制都稳定,再往上叠加真实业务。这样后续任何一次业务组件异常,我都知道问题出在业务侧还是平台侧。Greengrass 这套组件的价值,就在于把"程序启动、停止、升级、配置下发"这些运维动作全部标准化了,剩下的就是放心把业务往里面填。