ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

打破中央计算盒子:车端原生架构的解耦、契约与自治

打破中央计算盒子:车端原生架构的解耦、契约与自治 “全车软件挤进一个盒子”这话我第一次听到是在一次电子电气架构评审会上有人拍着桌子说既然算力都堆到中央计算单元了那把所有功能全搬进去不就完事了。两年后再回头看那个盒子从人见人爱的香饽饽慢慢变成了没人愿意接手的烫手山芋。这个系列写到第五篇前面聊了怎么往里塞、塞的时候踩了哪些坑、以及为什么塞到最后塞不动这一篇专门讲打破盒子之后的事。当全车软件不再追求物理上挤进同一个铁盒为整车重新设计的那套原生架构到底长什么样它跟云原生架构之间能共享多少思路又有哪些地方必须自己长出一套规矩这就是我想在这篇里说清楚的问题。1. 把“盒子”这个包袱拆开看清楚1.1 一个盒子装下全车这个念头是怎么长出来的任何架构选择的背后都是成本账。早期整车是分布式的一个功能一个 ECU车窗一个、座椅一个、空调一个线束长得像蜘蛛网一台车几公里长的线重量和装配工时都是钱。于是行业把相关功能往一块并先并成域控制器动力域、底盘域、座舱域各一个再往后算力芯片越来越猛一颗 SoC 就能顶过去一整个机柜于是自然而然有人想那干脆一个中央计算单元把所有域的软件都收拢进来线束短了、BOM 简化了、OTA 也好做了。这个想法本身没错问题在于它把“物理集中”和“逻辑集中”当成了同一件事。物理上软件跑在同一颗芯片、同一块板子上不代表它们在逻辑上就必须共享生命周期、共享故障域、共享发布节奏。我们后来管这个叫“盒子思维”只要东西都在盒子里就当它们是一体的。真正卡住项目的从来不是盒子这个硬件形态而是盒子内部那套“一锅炖”的软件组织方式。想清楚这一点后面所有的原生架构设计才有落脚点否则你只是换了个更大的盒子继续炖。1.2 塞满之后四个硬伤一个接一个冒出来第一个是故障域耦合。一个盒子里塞几十个功能只要共享同一套内核、同一份根文件系统任何一个进程内存泄漏、死锁、或者野指针跑飞都可能把整台车拖下水。做功能安全的时候ASIL 分解极难落地因为你要证明高安全等级的功能不会被隔壁低等级功能的故障影响而它们偏偏共用一个调度器。第二个是发布节奏被绑死。座舱的应用三个月一个版本车控的底层十年不动智驾的模型两周迭代一次。这波人凑在一个镜像里谁的代码都不敢合集成变成地狱最后演变成“一个功能延期全车软件陪跑”。第三个是资源争抢。智驾推理要抢 GPU 和内存带宽车控要确定性延迟座舱要瞬时响应三拨人在同一颗芯片上抢OS 的通用调度器根本扛不住这种混合负载。第四个是生命周期错位软件更新、回滚、灰度这三件事在整车级上会互相干扰一个功能的回滚可能带上另一个功能的旧版本。这四个硬伤不是抽象的是我在项目里一个接一个撞出来的。1.3 错的不是盒子是“塞”这个动作我想强调的是打破盒子不等于把中央计算单元拆掉退回分布式。物理上集中算力是趋势省钱也是真的省该集中还得集中。真正要打破的是“把所有东西当一体来管”的思路。打破之后的形态我习惯叫它“原生架构”——这里的“原生”对应的是云原生里的那个“原生”意思是为特定运行环境从零设计而不是把别处的东西硬搬过来改吧改吧。所以原生架构的第一原则就是物理位置和逻辑边界解耦。软件可以物理上跑在同一个 SoC 上逻辑上却是独立部署、独立升级、独立故障的服务。它跑在哪、和谁共用板子对上层业务来说应该是透明的。想明白这件事剩下的就是怎么把这套“透明”落到实处这才是真正花功夫的地方。2. “原生”两个字到底原生在哪2.1 三个命题解耦、契约、自治我总结下来车端原生架构绕不开三个命题缺一个都立不住。第一个是解耦软件不能依赖它具体部署在哪个核、哪块板、哪个盒子上。这听起来简单做起来意味着资源寻址方式要彻底改从“第几颗 CPU 的第几个寄存器”变成“我要一个叫某名字的计算资源”这中间的翻译层就是架构的骨架。第二个是契约接口先行服务之间的约定必须显式写下来、版本化、可校验。云原生里这东西很成熟IDL、OpenAPI、proto 都是现成的但车端的接口契约还得多带一层东西——时序语义和失效语义。你调用一个刹车服务不只是“传参返回结果”还得约定“最坏多少毫秒内必须返回”“超时了算谁的责任”“调用失败时降级到哪个兜底”。第三个是自治每个服务能独立启停、独立升级、独立降级一个挂了不至于拖着别人一起死。这三个命题放在一起就是原生架构的“地基”后面所有机制都是在这块地基上长出来的。2.2 云原生的招哪些能抄哪些抄了会翻车云原生架构这几年太热了很多人第一反应是往车上搬。我的经验是理念能借组件得挑调度策略得重写。核心差别在于假设条件完全不同。云上假设资源近乎无限、网络基本可靠、失败可以重试、最终一致能接受车端恰好反过来资源受限、网络间歇、实时性强、安全关键、还必须断网也能活。云原生机制车端能不能用我的实际处理方式容器化OCI 镜像能用但要裁剪用轻量镜像格式去掉多余的用户态工具链启动时间压到百毫秒级微服务拆分能用但粒度要克制车端服务拆太细通信开销和部署复杂度压不住Kubernetes 编排不能直接用太重调度器要按确定性时延重写多节点一致性机制在车上用不上服务网格数据面部分能用Sidecar 内存和延迟开销太大只保留策略控制和有限遥测不可变基础设施能用换了个叫法车端叫 A/B 分区加原子切换本质上是同一套思想声明式 API强烈建议用期望状态和实际状态分离对车端配置管理和 OTA 极其友好GitOps能用但流程更严车端要有更严格的审批和签名不能一键推全车队我特别想说 Kubernetes 那一条。很多团队一开始雄心勃勃想上 k8s结果发现 Pod 调度器是为吞吐和弹性设计的不是为确定性时延设计的。车端要的是“这个服务必须在 5 毫秒内被唤醒”而 k8s 关心的是“这个 Pod 尽量均衡地铺在集群上”。所以正确做法是借它的声明式思想和健康检查机制把调度器换成实时优先级驱动的、能预留 CPU 核的定制版本。2.3 四层分法从铁到场景落地拆下来我习惯分四层。最底下是硬件和板级支持包括芯片、外设、Bootloader 和分区表。往上一层是硬件抽象层驱动、设备节点、资源抽象这一层的职责是把硬件的差异吃掉。再往上是系统服务层通信、时间同步、诊断、日志、安全、资源管理都在这一层它是所有应用共享的公共底座。最上面是应用和场景层用户能感知到的功能比如迎宾、泊车、空调联动都在这层编排。分层的价值在于“依赖单向”。应用依赖系统服务系统服务依赖硬件抽象反过来绝对不允许。你在应用层里直接读 GPIO、直接访问 CAN 控制器这种代码短期能跑长期就是架构的癌症因为它把应用和具体硬件焊死了。我在项目上吃过这个亏后来强制规定应用层不允许 include 任何驱动头文件只能通过服务接口访问硬件这条红线救了不少返工。3. 五个不能少的核心机制3.1 服务化与接口契约先把合同签了再写代码服务化最容易被误解成“把函数拆成远程调用”。真正的服务化是先定义接口契约再实现最后才谈部署。我们内部的做法是每个服务必须有一份机器可读的接口描述字段、类型、单位、取值范围、时序要求全都写死然后由工具链自动生成桩代码和文档。接口一旦发布就进入版本管理语义化版本号加兼容性规则比如新增可选字段算小版本删除或改语义算大版本并且要走评审。这里有个特别容易踩的坑参数单位不统一。温度有人用摄氏度有人用 0.1 度整数速度有人用米每秒有人用千米每小时两个团队各写各的联调的时候发现数值对不上排查半天。后来我们把“单位必须写进字段名或元数据”作为硬性规定类似的还有坐标系、时间基准。别小看这些细节架构的成熟度往往体现在这种“没人愿意写但必须写”的契约上。3.2 软硬解耦软件只认名字不认引脚软硬解耦的目标是哪天硬件换了供应商、换了芯片型号上层软件一行不用改。做法是引入一层资源映射软件申请的是逻辑资源比如“一个实时计算单元”“一段带带宽保障的通信通道”由部署描述文件在部署阶段把逻辑资源映射到物理核、物理内存、物理总线。映射表是配置不是代码。好处在换硬件时体现得淋漓尽致。我们做过一次从 A 芯片换到 B 芯片的验证因为软件只认逻辑资源名重编一下部署描述符就跑起来了真正改动的只有底层驱动。如果当初把核编号、寄存器地址写死在应用里这次移植至少要多花两三个月。代价是启动阶段要做一次映射解析多几十毫秒这个开销完全值得。要注意的是逻辑资源名要稳定、要有命名规范不然用不了多久就乱成一张大网比写死更难看懂。3.3 通信范式从拉信号线到喊服务名传统车里通信是信号级的CAN 报文的某个字节某几位代表车速接收方自己去解析。这套东西在分布式 ECU 时代挺好用但到了服务化的原生架构里就露怯了信号没有语义、没有类型检查、没有版本。改成面向服务的通信之后一个服务调用是通过方法名和结构化参数完成的底层跑的是 SOME/IP 还是 DDS 还是共享内存对上层是透明的。维度信号级通信服务级通信寻址方式报文 ID 位偏移服务名 方法名类型安全无靠约定有由 IDL 保证版本管理几乎没有语义化版本 兼容规则时序语义周期发送支持请求响应、发布订阅、超时调试难度高要对着矩阵表相对低有接口文档和追踪底层我用得比较多的是 SOME/IP 做请求响应DDS 做高频发布订阅跨分区高实时场景用共享内存直接传。要注意的是通信中间件别叠太多层我见过一个项目为了“解耦”套了三层封装结果端到端延迟从 2 毫秒涨到 18 毫秒最后不得不砍掉两层。解耦要有度每加一层都要问一句这层的延迟开销换来的灵活性值不值。3.4 资源隔离与确定性调度把“抢”变成“分”这是车端跟云端差得最远的地方。云上大家抢资源抢不到就排队问题不大车上一个实时任务晚了几毫秒可能就是安全事故。所以车端资源管理的核心不是“公平”是“确定性”。我的做法是先用 Hypervisor 做硬分区把实时域和非实时域物理隔离再在域内用容器或 cgroup 做软隔离最后用 CPU 亲和性把关键任务钉在固定的核上。举个具体的分配一颗八核 SoC我会给车控实时域预留两个核独占不允许任何其它任务进来座舱分三个核允许互相争抢但有内存上限智驾推理分三个核加 GPU 队列做带宽预留。这样即使座舱某个应用跑飞了它也超不出自己的三个核和内存配额碰不到车控的核。关键参数是“预留”和“上限”两个预留保证底线上限防止失控。要提醒的是预留核会降低整体算力利用率这是拿效率换确定性必须承认这个代价并算清楚它值不值。3.5 可观测性黑盒要能剖开看分布式时代每个 ECU 是一个黑盒出了问题只能靠诊断仪读故障码。原生架构必须做到“可解剖”日志、指标、追踪三件套一个不能少而且要能在车端本地做初步聚合不能全都往云端传。车端流量是要花钱的尤其是车队规模上量之后一根日志上传通道的流量费用能吓死人。我的策略是分层处理高频指标在车端降采样后本地留存只上传聚合结果异常关键事件全量留存本地并触发上传跨服务的调用链追踪默认采样 1%出问题时动态提高采样率。DTC 故障码和埋点要跟服务追踪关联起来这样从“哪个功能不好用”能一路追到“哪个服务的哪个方法超时了”。很多团队只做前两件套省略追踪结果排查跨服务问题时全靠猜等真正上线出现偶发问题就知道追踪有多值钱了。4. 迁移落地从盒子走到原生架构4.1 三个阶段包壳、拆骨、换脑我从不建议一步到位重构那样风险太大通常也推不动。我的路径分三步走。第一步叫“包壳”把盒子里现有的软件原封不动地封装成服务对外暴露标准化接口内部爱怎么乱怎么乱先让接口层统一起来。这一步成本最低收益来得最快因为它让上层场景开发不再被底层实现绑架。第二步叫“拆骨”把耦合在一起的功能按业务边界拆开独立部署、独立升级。这一步最难因为要动既有代码还会碰到供应商的软件包边界怎么划、谁来负责全是博弈。第三步叫“换脑”引入动态编排和声明式部署让服务能根据场景和资源情况动态加载卸载做到“按需运行”。我们实际走完三步用了一年半中间每一步都做过灰度先在非安全相关的功能上试稳定了再往关键功能迁。4.2 关键参数怎么定三个算式第一个是时延预算。端到端延迟要求先倒推比如一个迎宾场景要求用户靠近到灯光响应在 300 毫秒内那么分解下来传感器采集 20 毫秒感知服务处理 60 毫秒编排决策 80 毫秒执行指令 40 毫秒中间通信和调度预留 100 毫秒。每一段都要留冗余我一般要求各段之和不超过总预算的 70%剩下的 30% 留给抖动和异常。第二个是 CPU 预留。假设一个服务在压力测试下峰值占用 0.6 个核我会按 1.5 倍冗余预留 0.9 个核再往整数靠到 1 个核。为什么不按峰值加 10%因为车端负载会随时间漂移一年后模型变大、数据变多10% 的余量三个月就吃完了1.5 倍是留出了演进空间。第三个是内存水位。服务的内存上限我一般设成稳定运行值的 2 倍同时监控长时间运行的内存增长斜率。如果一个服务每周内存增长 5%那它一定有泄漏不能靠调大上限来掩盖。上限的作用是“失控时保护邻居”不是“掩盖问题”这两个概念千万别混。4.3 一个具体拆解把迎宾场景从盒子里挖出来拿迎宾场景举例它过去埋在盒子里跟一堆功能混在一起。拆出来之后它变成一个独立的编排服务自己不实现任何底层逻辑只负责“编排”。它订阅“用户接近”事件调用座椅服务调整位置调用空调服务设置温度调用氛围灯服务点亮灯效调用后视镜服务展开。每个被调用的服务都是独立部署的各自有自己的接口契约和降级策略。好处在哪灯具供应商换了灯效算法只更新灯光服务迎宾编排服务完全不用动。座椅服务升级的时候即使出了 bug 也只是座椅不动作灯和空调照常。这就是自治带来的韧性也是原生架构最实在的价值。要注意编排服务自己也不能变成新的“超级节点”它的逻辑要尽量薄复杂决策下沉到各子服务编排层只做顺序、条件、超时和兜底一旦它长胖你就又造了一个小盒子。5. 常见问题与排查实录5.1 问题速查表现象大概率原因排查方向某个服务偶发超时CPU 被邻居抢占看预留核是否生效查亲和性配置跨服务调用延迟忽高忽低通信中间件排队查队列深度和优先级看是否共享了低优先级通道升级后旧功能失效接口版本不兼容核对 IDL 版本和兼容性声明内存缓慢上涨服务内有泄漏看增长斜率别急着调大上限一个功能挂牵连全车故障域没隔离检查是否共享了进程或内核资源启动慢映射解析和镜像解压耗时优化部署描述符裁剪镜像5.2 踩过的坑和土办法最后分享几个我在实操里攒下的经验。第一接口契约一定要有自动化校验光靠人 review 靠不住我把契约校验塞进了 CI提交时自动比对历史版本不兼容直接打回这条规则拦下的问题比任何评审都多。第二别迷信“服务拆得越细越好”车端的通信开销和部署复杂度都是硬约束我见过一个项目把功能拆成八十多个服务结果启动时间翻了三倍最后又合回去。粒度的原则是“按变更节奏拆”变更节奏相同的放一起不同的拆开。第三降级策略必须提前设计并且真实演练不能写在文档里就算完。我们做过一次演练故意让某个服务返回超时结果发现兜底逻辑里有空指针直接崩了。这种问题不演练根本发现不了。第四迁移一定要留退路每个阶段都要能回滚到上一版我坚持每次灰度都保留完整的 A/B 切换能力哪怕多花点存储空间也值得。踩过几次坑之后我越来越确信原生架构的核心不是用了多先进的技术而是你有没有把“解耦、契约、自治”这三件事真正落到每一行代码和每一次发布里。这套东西说起来简单做起来全是细节但只要方向对了慢一点也总比在盒子里越陷越深强。
返回列表