4、系统架构设计:微服务与事件驱动架构的选择

说实话,每次聊到样本追踪的系统架构,我脑子里的第一反应不是选什么技术栈,而是先问一个问题:这个系统到底要干多少事?

样本追踪,不是简单的状态流转。它要处理数据录入、规则校验、流程调度、存储分发、审计追溯……一套系统下来,五脏俱全。选错了架构,后期改起来,成本翻十倍都不止。

我个人习惯,做架构设计时先画一张「能力地图」。看哪些模块之间耦合紧密,哪些是需要独立扩展的。然后才谈得上用微服务还是事件驱动。

这一章,我跟你聊聊我的实际选择经验。

4.1 为什么不是「二选一」

很多人以为微服务和事件驱动是互斥的。其实不是。微服务是组织架构的拆分方式,事件驱动是服务间的通信模式。

我用过一个比喻:微服务是拆墙,事件驱动是建桥。拆了墙不等于不需要桥,桥多了也不等于墙不能拆。

在样本追踪系统里,我推荐的做法是:

  • 纵向拆服务 – 按业务域拆分,比如样本接收、样本分发、检测流程、报告生成。
  • 横向解耦通信 – 服务之间通过事件总线交互,而不是直接调接口。

这样做的好处是什么?我给你列个清单:

  1. 扩展灵活 – 某个服务流量暴涨,只扩那个服务就行。
  2. 故障隔离 – 一个服务挂了,不影响其他服务。
  3. 异步可靠 – 事件驱动能扛住短时流量峰值。

你想想看,如果每个服务都互相 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 系统架构总览:我不说你看不懂这张图

讲了这么多,我觉得直接给你一张架构图更直观。这是我在一个实际项目中用过的简化版架构,覆盖了从样本接入到报告产出的全过程:

样本追踪系统:微服务 + 事件驱动架构 外部系统/设备 API Gateway 事件总线 (Kafka) 微服务层 样本录入服务 规则引擎服务 调度分发服务 报告生成服务 数据存储层 业务数据库 (PG) 审计日志库 消息归档 (ES) 对象存储 (MinIO) 说明 所有微服务之间不直接调用,通过 Kafka 事件总线异步通信。 每个微服务独立写自己的数据库/存储,保证数据自治。

4.6 选择事件驱动的「三个必须」条件

看到这里,你可能想“我把所有服务都改成事件驱动吧”。别急,事件驱动不是万能药。我自己的判断标准是这样的,供你参考:

注意事项:如果以下三个条件有一个不满足,我建议你慎重考虑事件驱动:

  1. 必须有明确的异步业务场景 – 比如样本接收后,不需要立刻返回检测结果。如果每个请求都需要同步响应,事件驱动就是给自己添堵。
  2. 必须有监控与重试机制 – 事件一旦丢失,数据就不一致了。我踩过这个坑,后来用死信队列和告警才救回来。
  3. 团队必须有运维能力 – 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:

嗯,系统架构这件事,没有银弹。微服务 + 事件驱动是一个很实用的组合拳,但前提是你真的需要它。保持简单,选择合适,才是最好的架构。