4、系统架构设计:微服务与事件驱动架构的选择
说实话,每次聊到样本追踪的系统架构,我脑子里的第一反应不是选什么技术栈,而是先问一个问题:这个系统到底要干多少事?
样本追踪,不是简单的状态流转。它要处理数据录入、规则校验、流程调度、存储分发、审计追溯……一套系统下来,五脏俱全。选错了架构,后期改起来,成本翻十倍都不止。
我个人习惯,做架构设计时先画一张「能力地图」。看哪些模块之间耦合紧密,哪些是需要独立扩展的。然后才谈得上用微服务还是事件驱动。
这一章,我跟你聊聊我的实际选择经验。
4.1 为什么不是「二选一」
很多人以为微服务和事件驱动是互斥的。其实不是。微服务是组织架构的拆分方式,事件驱动是服务间的通信模式。
我用过一个比喻:微服务是拆墙,事件驱动是建桥。拆了墙不等于不需要桥,桥多了也不等于墙不能拆。
在样本追踪系统里,我推荐的做法是:
- 纵向拆服务 – 按业务域拆分,比如样本接收、样本分发、检测流程、报告生成。
- 横向解耦通信 – 服务之间通过事件总线交互,而不是直接调接口。
这样做的好处是什么?我给你列个清单:
- 扩展灵活 – 某个服务流量暴涨,只扩那个服务就行。
- 故障隔离 – 一个服务挂了,不影响其他服务。
- 异步可靠 – 事件驱动能扛住短时流量峰值。
你想想看,如果每个服务都互相 REST 调用,那跟单体架构有什么区别?
4.2 核心决策:服务拆分粒度
细到什么程度才算「微」?我见过有人把用户登录和用户注册拆成两个服务。那完全是过犹不及。
我在项目中遇到过这样一个场景:样本追踪系统的「样本接收」和「样本入库」本该是同一个事务单元,但被拆成两个独立微服务,结果数据一致性出了大问题。最终我们又合并了回去。
经验法则:一个微服务应该自带一个完整的业务子域,并且有明确的「上下文边界」。
下面这个表格是我做过的实际项目里的服务划分参考:
| 服务名称 | 职责 | 独立扩展性 |
|---|---|---|
| 样本录入服务 | 接受外部系统或手动录入的样本数据 | 高(需要接入多种来源) |
| 规则引擎服务 | 校验样本编号、类型、时间戳等合法性 | 中(规则变更频繁) |
| 调度分发服务 | 根据样本属性决定流转到哪个检测节点 | 高(需适配不同检测设备) |
| 报告生成服务 | 汇总检测结果,生成结构化报告 | 低(通常不独立爆发流量) |
| 审计跟踪服务 | 记录所有操作日志与状态变更 | 高(需做独立写库和查询) |
提示:我建议你为每个服务定义两个指标:「变更频率」和「负载特征」。如果某个服务的变更频率和负载特征都与其他服务差异很大,那就值得拆出来。反之就别拆。
4.3 事件驱动:核心模式与实战选型
选事件驱动,最关键的问题只有一个:用消息队列,还是用事件总线?
消息队列(如 RabbitMQ)适合点对点的任务分发,一个生产者、一个消费者,消息被消费后就消失。事件总线(如 Kafka)适合发布/订阅模式,一个事件可以被多个消费者消费,数据还能持久化重放。
在样本追踪系统里,两者都用得到。
比如「样本接收完成」这个事件:
- 规则引擎需要订阅它来做校验。
- 调度服务也需要订阅它来做分发。
- 审计服务也要记录它。
这种情况下,用 Kafka 的事件总线就非常合适。
下面是一个我最常用的事件定义示例——别觉得啰嗦,少一个字段都可能出大问题:
{
"eventId": "EVT-20250404-001",
"eventType": "SampleReceived",
"sourceService": "sample-ingress",
"timestamp": "2025-04-04T10:30:00Z",
"payload": {
"sampleId": "SMP-20250404-12345",
"sampleType": "blood",
"collectorId": "USR-008",
"collectedAt": "2025-04-04T09:15:00Z",
"metadata": {
"originLab": "Lab-BJ-01",
"priority": "high"
}
},
"traceId": "trace-abc-123"
}
关键点:traceId 一定不能省。样本追踪全链路里,一次事务可能要跨 5~8 个服务,没有 traceId 你根本没法排查问题。这是我踩过最大的坑之一。
4.4 避坑指南:我曾经犯过的错误
刚才提到了 traceId 的坑。还有几个我亲历过的,写出来给你当防火墙:
- 事件顺序乱掉 – 有一次,样本状态从「已接收」变成「已检测」,但 Kafka 分区内事件顺序因为重试机制被调换了。结果是记录了「已检测」的日志,后来又写了一条「已接收」。修复办法是把
sampleId作为分区键,保证同一个样本的事件顺序不乱。 - 幂等性没做好 – 消费者重试时重复处理了同一个事件。检测结果被插入了两次。解决方案是在消费端用
eventId做去重。 - 过度设计 – 一开始把 10 个拆成 10 个服务,结果每个服务只有 2 个接口。运维成本远大于收益。后来合并到 5 个,舒服多了。
4.5 系统架构总览:我不说你看不懂这张图
讲了这么多,我觉得直接给你一张架构图更直观。这是我在一个实际项目中用过的简化版架构,覆盖了从样本接入到报告产出的全过程:
4.6 选择事件驱动的「三个必须」条件
看到这里,你可能想“我把所有服务都改成事件驱动吧”。别急,事件驱动不是万能药。我自己的判断标准是这样的,供你参考:
注意事项:如果以下三个条件有一个不满足,我建议你慎重考虑事件驱动:
- 必须有明确的异步业务场景 – 比如样本接收后,不需要立刻返回检测结果。如果每个请求都需要同步响应,事件驱动就是给自己添堵。
- 必须有监控与重试机制 – 事件一旦丢失,数据就不一致了。我踩过这个坑,后来用死信队列和告警才救回来。
- 团队必须有运维能力 – Kafka/RabbitMQ 不是装完就不管的。你想想看,如果没人懂分区再平衡、消费滞后排查,出了问题你一个人能扛吗?
4.7 一个真实的配置参考
最后,我给你贴一个我在项目中用过的 Docker Compose 的 Kafka 配置节选。别直接复制,但可以当模板参考:
version: '3.8'
services:
kafka:
image: bitnami/kafka:3.6
ports:
- "9092:9092"
environment:
- KAFKA_CFG_NODE_ID=1
- KAFKA_CFG_PROCESS_ROLES=broker,controller
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@localhost:9093
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true
volumes:
- kafka_data:/bitnami/kafka
volumes:
kafka_data:
嗯,系统架构这件事,没有银弹。微服务 + 事件驱动是一个很实用的组合拳,但前提是你真的需要它。保持简单,选择合适,才是最好的架构。