物联网平台协议解析服务生产环境 OOM:MySQL 迁移 InfluxDB(2017)

背景 2017 年前后,我在大连 R 公司负责 R 平台协议解析层的开发和维护。R 平台是一套车联网平台,部署在 H 云上,用来接收和处理车载设备实时上报的数据。 协议解析层有十五个以上的服务,每个服务至少两个节点,负责解析车载设备上报的自定义 TCP 协议,网络通信与连接处理底层由 Netty 实现。平台共有 300 多台虚拟机,单个服务的规格通常不会拉很高,G4 所在的虚拟机配置是 2 CPU、4 GB 内存。 其中 G4 是协议解析层数据量最大的服务,负责解析行车数据,单条消息包含约两百个字段,涵盖车速、转速、门窗锁状态、发动机参数、各类液体温度、油量等实时车况信息。正常 QPS 约 700 条/秒,峰值超过 1000 条/秒,四个节点加起来总 QPS 在 5000 左右。 解析完成后,G4 把数据写入 Redis(实时数据)和 MySQL(行车历史数据,水平分片成 1024 个库),另外还有一些平台内部的 API 调用。MySQL 访问层是在 JDBI 基础上封装了一层。 协议解析层的数据来自上游接入层,接入层向协议解析层推送数据,同时用 RabbitMQ 做消息队列缓冲。 装车增长与早期告警 R 平台接入的设备数量持续增长,每个月有几千台新设备安装到实车上。随着在线行驶车辆增多,G4 的数据量也跟着涨。 大规模故障之前,G4 就开始在早高峰和晚高峰偶发内存告警,高峰过去后十几分钟会自行恢复。当时不清楚具体原因,尝试过把业务线程数从默认值增加到 16,改完之后等服务低峰期重启,观察下一次高峰是否有改善。效果不明确,可能有一定缓解,也可能影响有限,之后就转向其他方向排查,线程数就维持在 16 没有再改。 大规模 OOM 装车量继续增加,某天 G4 出现了大规模 OOM。现象是四个节点滚动离线:一个节点 OOM 重启,剩余节点压力上涨,接着下一个离线,重启后很快再 OOM,如此循环。 节点重启之后撑不了太久,因为上游 RabbitMQ 里已经积压了大量数据,积压时间估计有一两个小时。一个节点恢复,积压数据涌入,Netty 堆外内存里挤满了等待处理的业务数据,内存很快再次撑满。一个节点离线,剩余三个节点分摊的流量更大,内存涨得更快,形成连锁反应。节点之间的 OOM 间隔只有几分钟。 ...

Created 31316-05-31 · Updated 5056-06-05 · 2 min · 263 words · -

Committer 服务分布式锁设计(2023)

背景 Committer 服务负责消费 Kafka 消息,将网络策略通过 NSO 提交到物理网络设备。原始设计使用单线程顺序消费:每次从 Kafka 拉取一条消息后提交,等待 NSO 返回结果再处理下一条。 单线程设计的瓶颈在于 NSO 提交设备的耗时差异很大,从几秒到几分钟不等。一条消息若因设备响应慢而阻塞,整个消费循环只能等待,整体吞吐严重受限。 改造目标是将 Committer 改为多线程并发消费,让不同设备的策略可以同时提交。 设备级并发约束 多线程并发带来一个新问题:物理网络设备在同一时刻只能处理一个配置提交。若并发向同一台设备提交多条策略,NSO 会返回冲突错误,设备状态也可能不一致。 因此,并发的粒度需要控制在设备维度:不同设备的提交可以同时进行,但同一台设备上的提交必须串行。 实现方案 多线程消费 Kafka 消费线程不再直接执行提交逻辑,而是将每条消息交给 ThreadPoolExecutor(默认 10 个 worker)异步处理。消费线程本身快速返回,继续 poll 下一批消息。 设备锁设计 引入一张设备锁表,记录当前正在进行中的提交和对应的设备,以及锁变更的时间戳。 提交任务在执行前先获取设备锁: SELECT ... FROM device_lock WHERE device_id = %s FOR UPDATE NOWAIT 加锁使用 NOWAIT:竞争失败时立即抛出异常而非阻塞等待,消费线程可以快速感知锁状态,不会因为等锁而阻塞整个提交流程。 提交完成后,在 finally 块中释放锁: SELECT ... FROM device_lock WHERE device_id = %s FOR UPDATE 解锁不使用 NOWAIT:此时当前任务本身就是锁的持有者,不存在竞争,阻塞等待语义更安全,确保成功取到行后再执行删除,不会因为瞬时异常而意外跳过清理步骤。无论 NSO 超时、解析异常还是其他未知错误,设备锁都会被清理,不会出现因为异常路径遗漏而导致的死锁。 锁的粒度是设备,不是请求。同一台设备的多条策略竞争同一把锁,不同设备的提交互不影响。 锁竞争与 Kafka 重投 竞争失败的消息不会被丢弃。Kafka 本身保证至少一次投递,消息会在下一个消费周期重新投递,再次参与锁竞争。NSO 侧做了幂等处理,重复提交不会导致设备状态错误。 ...

Created 3036-06-03 · Updated 5056-06-05 · 1 min · 124 words · -

Rule Status 字段的设计取舍(2022)

2022 年 7 月,入职某防火墙自动化团队约一个月后,接到第一个独立功能需求:在 UI 上显示每条 Rule 的当前状态。 Rule 是用户在系统里下发的防火墙策略。每条策略在系统里会经历一系列流转节点:submit、approve、dryrun、commit、rollback、archive……这些节点对应不同的业务操作,最终需要在界面上展示"这条 Rule 现在处于哪个阶段"。 这是一个看起来不大的需求,但它要求先回答一个设计问题:Rule 的状态存在哪里? 两种方案 方案一:SQL 聚合 不新增字段。系统里本来就有 rule_dryrun、rule_commit 等操作记录表,查询当前状态时通过 JOIN 这些表、判断哪些记录存在来推算出状态。 改动量最小,现有表结构不动,在查询层加逻辑就够了。 方案二:状态归属 在 rules 表增加 status 字段,每次业务状态发生变更时在对应模块里写入新状态。Rule 的状态就存在 Rule 这里,查询时直接读取。 初期需要找到所有状态变更的节点,在分散在多个模块的代码里补齐“更新 Rule 状态”的逻辑,改动范围比方案一大。 讨论 两种方案都在组内讨论过。支持方案一的理由有几个:改动文件少,风险低;项目当时还不确定做多久,不必做太完善;另外我是新入职,对 Python 代码库不熟悉,方案一可以减少上手压力。 但是…我更倾向方案二,核心判断是:Rule 的状态是 Rule 本身的属性,有独立的业务名称和语义,它应该和 Rule 在一起,而不是分散在各操作记录表里通过 JOIN 聚合出来。方案一把复杂度延后了——每增加一个新的查询场景,都要重新写一次聚合逻辑;状态维度越多,维护代价越高。 讨论中有一个具体的质疑:如果用户对一条 Rule 操作三次,rules 表就要被修改三次,会不会太频繁?我的回应是:操作了三次,状态就变了三次,写入三次和业务行为是完全对应的,代码逻辑和业务逻辑是一致的。 讨论下来,同事们接受了这个方向。其中一位熟悉代码库的同事专门指导我:Rule 的状态会在哪些业务节点发生变化,应该在哪些地方插入状态更新代码。这个指导让我能在不熟悉代码库的情况下快速定位到所有需要修改的地方。 执行 功能在一个 sprint(约一个月)内完成开发、测试、上线。改动涉及多个模块,但每个模块的改动量不大,主要工作是把状态抽象出来、并在正确的节点写入。 上线前还准备了一份 migration.sql,对存量的历史 Rule 记录做 backfill——需要根据这些 Rule 在各操作表里的记录,推算出它们当前应该处于哪个状态。这份 SQL 写起来需要复杂的跨表 JOIN,刚好印证了方案一如果长期维护会是什么样子。 回看 这个字段上线后,后续三年里所有 Rule 状态机相关的功能——新的流转流程、状态过滤、统计报表、列表查询里的复杂过滤条件——大多都直接依赖这个 status 字段,没有因为最初的设计需要绕路。后来又陆续引入了更多流程,让 Rule 在更多角色之间流转;这些变化本质上都是状态转移,没有对应的操作历史记录。如果当初用聚合方案,这些新状态根本没有操作记录可以聚合,届时无论如何都要补加 status 字段,区别只是改动量更大、时机更被动。 ...

Created 4046-06-04 · Updated 5056-06-05 · 1 min · 172 words · -

Android APK 安全评估项目回顾(2014)

2014 年,我在第一家公司接到了一个非常规的项目:对一款 Android APK 做安全评估。说"非常规",是因为那时部门的项目周期动辄两三个月到两三年,而这个只有一周。领导把任务交给我,大概是觉得我平时喜欢折腾各种新东西, 当时业余时间在研究树莓派。 背景 客户是一家制造电梯的公司,他们有一套电梯维保工单系统,安装在配发给维修工程师的 Android 平板上。工程师通过这个 APK 查看维护保养订单、处理故障报修。这个 APK 由另一家第三方公司(vendor-A)开发。 客户委托我所在的公司(vendor-H)对这个 APK 做安全评估,核心问题是:这个 APK 能不能被破解?里面内置的密钥等敏感数据能不能被提取出来? 这本质上是一次白盒逆向测试,只是用了"安全评估"这个更正式的说法。 第一次评估:一周内出报告 拿到 APK 之后,我先去查了 Android APK 逆向的相关资料。 APK 文件本质上是一个 zip 包,解压后可以看到 classes.dex——这是 Android Dalvik 虚拟机的字节码文件,而不是标准 Java 的 .class 文件。逆向的第一步是用 dex2jar 把 classes.dex 转换成普通的 jar 包,再用 jd-gui 等 Java 反编译工具将 jar 里的 .class 文件反编译成可读的 Java 源码。具体流程可以参考我当时整理的 Android APK 反编译。 第一版 APK 虽然有一些保护措施——把部分逻辑封装成单独的 jar 包作为依赖,密钥也不是直接明文写死,而是在运行时动态加载/生成——但通过反编译拿到全部源码之后,这些都可以被分析清楚。我新建了一个 Java 工程,把 APK 里的密钥加载逻辑复制进去单独执行,成功还原出了被保护的密钥。 此外,代码里也存在硬编码的敏感信息。 第一次评估报告在一周内交付给了客户,列出了几个可被利用的安全漏洞。 第二次评估:加壳技术 几个月后,客户再次找到我们,带来了 vendor-A 升级后的新版 APK。 ...

Created 5056-06-05 · Updated 5056-06-05 · 1 min · 148 words · -