← 发散思维

DIVERGE · 01

边缘设备上的服务

一张尽量不漏的地图

EDGE · SERVICE · MAP

在边缘设备上写服务,不是把云上的服务缩小一号。设备有自己的物理、自己的寿命、自己的失败方式。这篇从一个会离开你视线的常驻进程摊开,把能碰到的面都点一次名。

· 发散思维

  1. 07人与十年谁维护、谁签发更新、产品停了设备还在不在
  2. 06舰队与云一万台同时醒过来时,单台的假设会反转
  3. 05契约配置、数据、接口。三种都会比记忆活得久
  4. 04服务本体真正长期跑着的那个进程,以及它的邻居
  5. 03系统谁来启动它、限住它、在它死后把它叫回来
  6. 02硬件与总线核、盘、口、看门狗、会换批次的板子
  7. 01物理世界热、电、灰、振、掉电、摸得到的人

业务代码往往不是大头。服务要烧进板子,断网还能干自己那点活,崩溃后日志说得清原因,升级到一半拔电也不能变砖。漏掉的通常就是这几件里的某一件,然后变成凌晨的上门。

云上那套习惯直接搬下来,内存和闪存经常先不够用。

01这句话到底在说什么

边缘设备,是算力和数据离现场更近的那一端。摄像头、网关、工控机、机器人、车机、收银机、家用盒子、塞在机柜里的小主机,都算。它们不一定小。放在现场,网不一定一直在,人摸得到,而且常常要维护很多年。

服务,是这块设备上长期跑着、对内或对外提供能力的那个进程,或故意拆开的一小簇进程。不是一次性脚本,也不是只在调试时打开的网页。它要开机自起,崩溃能回来,配置能换,版本能升,日志能解释它刚才为什么死了。

这篇的中心是:一块会离开你视线的设备,上面有一个你得负责的服务。纯传感器固件、纯云端 API、只在笔记本上跑的工具,会作为邻居出现,不占中心。邻居从三个口伸进来:朝下连摄像头、传感器、开关,朝上连云和人,以及编这个服务的构建机。前两个口在业界习惯叫南向和北向。那不是房间里的东南西北,只是画图时的上下。

02服务为什么要长在设备上

留下的理由

时延等不了往返:控制、快门、对讲、刹车。原始视频、点云、高频采样传上去比本地做完结论贵。原始画面按隐私不该离开现场。电梯井、船、车间、地下室里,网络是客人。

不该留下的理由

需要全局视图的调度、模型训练、跨设备账本、改得很勤却没有可靠下发通道的策略,以及任何「这块板子丢了业务就不可恢复」的唯一副本。这些放在设备上,是把总部的职责塞进前哨。

还有一条软理由容易被说成硬理由:数据已经在本地成型,搬去云上再算是倒手。这只在你真的量过带宽、延迟和故障成本之后才成立。没量过,它只是一句偏好。

03先把约束写成数字

动手之前把约束写成数字。形容词留不住设计。

这几行如果写的是「还行」或者「以后再说」,后面的方案就是在猜。猜错的部分,最后会变成现场工单。

04硬件:进程脚下的那块板

服务不直接认识「摄像头」。它认识驱动、设备节点和总线。要过一遍的面:

硬件抽象要薄,也要显式:哪些能力是这一代都有的,哪些只在某个修订上有。探测失败就降级并说明原因,不要启动失败,也不要假装那路传感器还在。

05系统:谁来养活这个进程

常见底座是裁过的 Linux(Yocto、Buildroot,或 Debian、Ubuntu 的设备变体)、Android、RTOS(FreeRTOS、Zephyr 一类),偶尔是 Windows IoT。选底座就是在选:有没有包管理,有没有用户态隔离,实时能到哪一档,十年后还有没有人能把这套系统再编出来。

容器不是默认答案。内存和盘都紧的时候,一整套容器运行时是奢侈。设备够大、团队已经会、又需要和云上同一交付物时,再用。不够大时,一个边界清楚的二进制加上 systemd(或同等的初始化系统),往往更诚实。

根文件系统尽量只读,可变数据单独分区。临时文件放内存盘。初始化系统负责依赖顺序、重启策略、资源上限和日志去向。服务自己再写一套守护,通常是第二套会出 bug 的 init。

06一个进程,还是一群进程

云上的微服务习惯搬到设备上,经常先把内存吃掉:每个进程一份运行时、一份日志、一份健康检查,业务还没跑。默认从一个进程里的模块开始。拆开只在你能指着说的时候:

拆开之后马上要回答:谁先起,谁等谁,谁死了算整机不健康,共享状态放哪,端口和总线谁独占。没有这些答案,拆分只是文件夹美学。

语言跟着约束走。靠近硬件和实时的部分,C、C++ 或 Rust 更常见。服务本体用 Go 或 Rust,启动和依赖都好收。Python 适合胶水、产测和尚在变形的逻辑;内存紧张的常驻核心要先量过它的常驻和启动,再决定它能不能留下。垃圾回收的停顿放进时延预算里看,不要放进信仰里看。

07设备上有三种时间

设备上至少有三种时间。混用就会写出「事件发生在开机之前」这种故事。

要单独处理的情况:开机后还是 1970 年;校时瞬间时间往回跳;用墙钟切分的日志和录像被切出一段洞;多台设备谁先谁后。没有可靠的公共时钟时,用设备内单调序号表达因果,墙钟只作注释,并在上报里标明时间是否可信。

校时源可以是 NTP、要更准的工业网里的 PTP、GNSS,或由网关代授。源失败时,服务继续用单调钟工作,不要因为对时失败就拒绝完成本地职责。

08朝现场:南向

边缘设备服务的南北向架构 云和人在上方,经北向与设备上的服务交换汇报和命令。摄像头、传感器和开关在下方,经南向交换采样和动作。服务画在边缘设备的边界里面。 北向 · 朝上 云 · 网关 · 看屏幕的人 汇报、事件 配置、命令 边缘设备 设备上的服务 开机自起的常驻进程 读数、动作 采样、状态 南向 · 朝下 摄像头 · 传感器 · 开关 · 产线
云在上,现场在下,服务待在设备边界里。朝上的箭头是北向,朝下的箭头是南向。

南向就是朝下这一侧:传感器、执行器、摄像机、产线总线。协议只是表面,语义才是坑。串口私有协议、Modbus、CAN、OPC UA、ONVIF、GB28181、BLE、Zigbee、专有 USB,每一种都要单独问:

读进来的值要有「卡住了怎么发现」:范围、变化率、超时未更新。写出去的动作要有「失去心跳之后怎么办」。这一侧的超时和重连是主路径。振动和接触不良会让它们在现场变成日常,而不是异常分支。

09朝云端:北向

北向就是朝上这一侧:云、区域网关,或局域网里看页面的人。设备在下面,对方在上面。多数设备没有公网入站,所以通常是设备自己往上开口,而不是等云来敲门。

无论哪一种,都要有认证、心跳、退避、离线缓存上限和优先级。命令和遥测分开,避免一次大遥测堵住「立刻停」。入站如果真的需要,做成默认关闭、有期限、有记录的隧道。不要做成永远开着的远程登录口。

10进程之间那一截

本机协作比上网简单,也更容易写出隐式耦合。还没拆进程时就是函数调用。拆开之后是 Unix 套接字、D-Bus、共享内存、管道、本地队列或文件落地。

能用明确消息,就不要约定「我知道你把结果写在那个目录」。共享内存要有主人:谁创建,谁销毁,崩溃后谁清理。本机接口也要版本,因为邻居进程明年会单独升级。不要把跟云说话的那套协议再绕回本机调用一圈,除非你是有意让外部和内部走同一套契约。

11数据留在哪

每一种数据先问四句:谁生产,谁消费,丢了会怎样,留多久。

很多设备用 SQLite 就够。要控制写放大:事务别太碎,WAL 要有检查点,别让日志和数据库抢同一块已经很累的闪存。环形缓冲适合「只关心最近」。大块媒体用文件,但要有上限和清理,不要指望有人会来删。

结构会变。旧库在现场,迁不了就启动失败,等于变砖。迁移要能从空库、从上一版、从「写到一半掉电」三种状态起来。

12离线之后,同步怎么不说谎

网络恢复不是把文件夹拷上去。

「恰好一次」在这一侧基本是空话。把系统设计成「至少一次,加上幂等」,然后把幂等键写进契约。先降采样、抽关键帧、传差值,再考虑原始文件。墙钟不可信时,序号表达因果,时间戳只是注释。

{
  "device_id": "edge-7f3a",
  "seq": 184422,
  "idempotency_key": "7f3a-184422",
  "wall_time": "2026-09-28T14:06:11+08:00",
  "wall_trusted": false,
  "kind": "event",
  "priority": "alarm"
}

13来不及的时候,丢掉谁

摄像头可以每秒 30 帧,模型只能跑 8 帧,往云上送的那条通道只容得下每秒一条事件。中间一定有人被丢掉。丢弃策略是产品决策,不是实现细节:

队列没有上限,就不是队列,是泄漏。每一级都要有长度、超时和「满了之后的语义」。背压要传到源头:推理忙时,采集侧降帧,而不是在内存里堆帧直到被杀死。

14当服务要看、要听、要算

感知管道通常是采集、预处理、推理、后处理,然后是动作或事件。每一段都可以单独失败。

能在设备上变成「有人进入」就不要上传脸。保留期限写进清理任务,而不是写进愿望。音视频还要面对硬件编解码、时间戳、音画对齐、切片和断流重连。用现成的管道框架,还是自己拉,取决于现场有没有人读得懂那张图。

15资源配额:别让旁路饿死主路

控制、采集、推理、上传、日志、现场网页会互相抢。没有配额时,表现是「偶尔卡一下」,原因可能是日志压缩刚好和推理叠在一起。

用 cgroup 或等价物写下限:内存上限到了就杀死该杀的那个,而不是随机杀;CPU 和 IO 给控制回路更高的权重,日志和上传做背景。磁盘也要配额,一份跑飞的转储不应占满系统分区。内存泄漏在有交换的服务器上是慢性病,在没有交换的设备上是猝死。常驻曲线要比功能测试更早设门槛。

16闪存会老,电会突然没

闪存有擦写寿命。日志每秒刷、数据库每条事务都同步、把调试信息写上盘,都是在花寿命。你以为写了 1KB,控制器可能擦了一整个块。

掉电会发生在任何一次写的中途。所以系统分区只读;数据分区上先写临时文件,同步,再改名;数据库用它承诺过掉电安全的模式,不要为了快关掉同步。升级在任意时刻断电,起来的仍应是完整的旧版或完整的新版。只读压缩根加单独的数据分区,是设备上很常见的组合。掉电后的修复要自动做完,不能停在一个需要人接显示器的检查程序上。

17启动、崩溃、看门狗

上电路径要短,也要可预测:内核、只读根、数据分区、时间、网络、服务。某一步失败时,设备进入一个能被看懂的状态,而不是黑屏重试到发烫。

启动依赖不要绕成环。网络不该是启动的前提,除非这个服务离开网络就没有任何本地职责。

18配置的三种真相

出厂默认,是刷机时就有的,重置能回到这里。期望配置,是云或本地管理端希望它成为的样子。实际生效,是设备此刻真的在用的,可能因为离线、校验失败或硬件不支持,和期望不同。

三种都要能被看见。只显示期望,现场会说「我明明改了」。只显示本地文件,云会说「我已经下发了」。校验失败就拒绝,并保持旧的。密钥不进普通配置,不进镜像,不进日志。本地应急修改和云端下发谁优先,改完是热生效还是要重启,都写下来。

19身份、密钥与攻击面

每台设备要有自己的身份,而不是全型号共用一个口令。产线注入密钥或证书,私钥放进安全元件,或至少放进权限收紧的存储。它要能轮换,能作废,并且生成之后不能再被导出。

通信做双向认证:云认得这台设备,设备也认得自己在跟谁说话,避免被同一网段里的假服务器领走。

攻击面按「摸得到」来想,因为边缘设备就是摸得到的。调试串口、远程登录、ADB、JTAG、默认口令、开着的管理页、未签名的 U 盘升级、能从拆下来的存储里直接读出的密钥、诊断包里的令牌、说不清来源的依赖。安全启动和签名校验解决「跑起来的是不是我们发的」。它不消灭业务逻辑里的洞,但没有它,后面的更新没有根。

物理接触下的防护有上限。写清楚哪些东西拆机就能拿走,哪些就算拆机也拿不走。拿不走的放进安全元件。拿走也无妨的,不要假装它是秘密。

20出厂之前,服务已经开始

生命周期从产线开始,不是从用户开机开始。

重置和返修要有路径:清掉用户数据,保留或重新注入身份。返修机不该带着上一个客户的数据回到仓库。

21更新弄不好,一批设备一起变砖

OTA 比加一个功能更容易把机器刷死。签名、槽位、回滚要单独留时间,别写在发布说明的最后一行。

还有一件容易漏:更新程序自己怎么更新。它坏了,后面所有修复都到不了。它要小,要稳,要少改。

22磁盘很小,日志不能照搬云上的做法

日志、指标、追踪按云上的密度打开,eMMC 会先被写满。设备上少留一点,但崩溃前的最后几行一定要在。

时间不准时,日志里同时留下单调钟和墙钟,不然一次重启前后的故事对不齐。

23现场要能修,门不要常开

现场的人常常不是写这个服务的人。给他的应该是一眼能懂的状态:在跑、在升级、在等网、在安全模式。灯的含义写在机身上或手册里。再加一键诊断包:版本、硬件修订、磁盘、最近日志、配置是否生效、网络能不能解析。里面没有密钥,也没有用户的原始视频。

远程支持可以有,但是经同意、有期限、有记录,到期自己关。不要留下全型号共用的口令、贴纸上的密码、永远开着的调试口,或一个「临时」但会进出货镜像的账号。临时入口一旦能把工作做完,就会活过产品本身。

24到了一万台,有些算法会变

一台设备上试通的事,铺到一片现场之后,行为会变。

25已经卖出去的接口,还得继续认

接口一旦装进已出货的设备,就要服务到那一批退役。朝云的接口、朝现场的接口,还有配置,都要有版本。只加字段,不改旧字段的意思。设备忽略自己不认识的字段,云也忽略自己不认识的旧字段。新配置项在旧版本上要有明确的默认行为。

不要用「强制全员升级」代替兼容,除非你真的控制每一台,并且失败了有人能上门。在仓库里留一张表:仍在现场的最老版本能做什么。人会换,表还在。

26构建:在我机器上是好的不算数

设备上的二进制往往不是在设备上编的。交叉编译、sysroot、依赖的目标架构,任何一处偏了,就会出现编译机能跑、拷过去就段错误。

同一提交应产出可对应的制品。制品带版本、源代码修订、构建时间和目标硬件,并且签名。依赖固定。设备启动时不去公网拉一段脚本。发布物要能回答:这是哪一版,给哪块板,和上一版差在哪,回滚包在哪。答不上来的,不要进舰队。

调试符号、多余服务、宽松权限用构建开关隔开,默认不进出货配置。发布前用一份清单核对,而不是靠记得。

27测试:没有板子,和只有一块板子

每一层都要承认自己覆盖不到的东西。

开发机上的测试覆盖不了驱动和掉电。只拿一块开发板测,也覆盖不了量产时换过的那颗料。

28合规会写进代码

按这台设备实际会碰到的来写。名词堆全,代码里却没有删除和保留期限,等于没做。

合规决定数据能不能采集、远程入口能不能留、日志里能不能出现某些字段。它不是发布前一周贴上去的章。

29热、电、灰、振

实验室是二十五度、电源稳定、网线插好。现场不是。

对电池设备,功耗就是产品:谁能唤醒,醒来做什么,做完能否回去。一个「顺便」的轮询可以把续航从月变成天。

30变体、SKU 与十年

同一份服务会遇见不同传感器、有无 NPU、不同大小的盘、不同客户的云地址、不同地区的射频限制。用硬件修订和运行时探测,不要复制出四份仓库。没有那颗设备、那颗 NPU、那个分区,就降级并报告原因。

十年要单独想一次。证书有效期不要短过你不愿上门更换的周期,或者把续期做进跟云通信的那条通道里。上游会消失,所以制品和构建环境要能存档。写文档的人会离开,所以「为什么根分区只读、为什么这条队列只留最新」要写在仓库里。停产之后现场的设备还在,服务端不能在某一天突然不认识它们。

31失败会以哪些样子到来

下面这些现场都会遇到。每条最好说得清三件事:平时怎么避免,出了从哪看出来,设备自己怎么回到能用的状态。

32谁在三年后还能修它

软件会比写它的人长寿。要留下不用读代码也能看见的期望态和实际态,留下版本、硬件修订和最近一次升级结果,留下一份不依赖作者的诊断路径。也留下当时的决定:为什么不拆成一群服务,为什么根分区只读,为什么这条队列只留最新。

备件和返修流程里写明软件该擦掉什么、该保留什么。现场事故、舰队、更新包的签发,最好能看成三件事。即使今天是同一个人,也写成三个职责。请假之后,钥匙不该只存在于记忆里。

33一张放哪的决策卡

这件事更适合留在设备更适合放在云或上层
毫秒到百毫秒内必须完成的动作留下,并定义失败时的安全输出只做建议,不把动作的唯一开关放在往返之外
原始音视频、点云、高频采样本地处理,上传结论或抽样只在确有必要、并且带宽和权限都允许时保存原始
全局调度、跨设备账本不要把唯一副本放在某一块板子上权威放在上层,设备只保留执行所需的缓存
训练和大模型的重计算跑已经装得下的推理训练和装不下的计算放上层
断网也必须完成的职责这是它留在设备上的理由恢复之后再对齐,不作为当时的前提
要经常改、设备又很多的规则执行和兜底留在设备编辑、审计和分发放上层
身份设备自己的密钥留在设备里账户、权限和组织的权威留在上层

靠近物理和离线的,留下。靠近全局和权威的,上去。两边都想要的,把结论和事件送上去,把原始和动作留下。

34发散之后,收回来

上面铺得比较开,动手时不用一起做完。下面六件如果还空着,上线后通常会用最麻烦的办法补上。

  1. 这台设备离线时,必须还能完成的一件事是什么。
  2. 内存、盘和每天允许写入的数字是多少。
  3. 身份在产线如何注入,私钥还能不能导出。
  4. 升级到一半断电,起来的是哪一版。
  5. 队列满了,丢掉谁。
  6. 三年后,不是作者的人怎样知道它现在到底在跑什么。
地图