第4章 中间件部署:消息队列与数据分发

各位好,欢迎来到中间件部署这一章。说实话,在轨道交通信号系统里,中间件就像人的神经系统——消息传得快不快、准不准,直接决定了整个ATS能不能正常运转。我这些年调试过的项目里,至少有一半的故障都跟中间件配置不当有关。今天咱们就把Kafka、RabbitMQ、DDS和服务注册这些硬骨头啃下来。

4.1 消息队列选型:Kafka vs RabbitMQ

先说说选型。很多新手会问:到底用Kafka还是RabbitMQ?我的回答是:看场景。

Kafka适合高吞吐、日志型、流式处理的场景。比如列车位置数据的采集、信号设备状态日志的汇聚。我在北京某线路项目中,每天要处理上亿条列车位置报文,Kafka扛得住。

RabbitMQ适合低延迟、可靠投递、复杂路由的场景。比如控制命令的下发、报警信息的实时推送。嗯,这里要注意:RabbitMQ的ACK机制很成熟,但吞吐量上限比Kafka低一个数量级。

特性 Kafka RabbitMQ
吞吐量 百万级/秒 万级/秒
消息持久化 磁盘顺序写,高可靠 内存+磁盘,可靠
路由模式 Topic分区 Exchange绑定
典型场景 日志、轨迹数据 命令、告警

我的建议:ATS系统中,建议Kafka做数据总线,RabbitMQ做控制总线。两者互补,别硬选一个。

4.2 Kafka安装与调优实战

Kafka的安装其实不复杂,但调优才是关键。我直接给出一套生产级配置。

4.2.1 基础安装

# 下载解压(以3.5.0为例)
wget https://downloads.apache.org/kafka/3.5.0/kafka_2.13-3.5.0.tgz
tar -xzf kafka_2.13-3.5.0.tgz
cd kafka_2.13-3.5.0

# 启动ZooKeeper(Kafka依赖它做协调)
bin/zookeeper-server-start.sh config/zookeeper.properties &

# 启动Kafka
bin/kafka-server-start.sh config/server.properties &

你看,启动就这么几行。但真正上线时,server.properties里的参数才是重头戏。

4.2.2 核心参数调优

我个人习惯把以下参数写在配置文件的头部,方便维护:

# 基础配置
broker.id=1
listeners=PLAINTEXT://0.0.0.0:9092
advertised.listeners=PLAINTEXT://192.168.1.100:9092

# 日志保留策略(轨道交通数据量大,别存太久)
log.retention.hours=72
log.retention.bytes=10737418240  # 10GB

# 性能调优(我踩过坑的地方)
num.network.threads=8
num.io.threads=16
socket.send.buffer.bytes=102400
socket.receive.buffer.bytes=102400
socket.request.max.bytes=104857600

# 分区数建议设置为CPU核心数的2倍
num.partitions=16

我曾经踩过的坑:有一次把log.retention.hours设成了168小时(7天),结果磁盘写满了,整个集群挂了。轨道交通的数据量比你想象的大得多,72小时足够,配合归档策略更稳妥。

4.2.3 生产级Topic创建

# 创建列车位置Topic
bin/kafka-topics.sh --create \
  --bootstrap-server localhost:9092 \
  --replication-factor 3 \
  --partitions 16 \
  --config retention.ms=259200000 \
  --topic train_position

# 创建信号设备状态Topic
bin/kafka-topics.sh --create \
  --bootstrap-server localhost:9092 \
  --replication-factor 3 \
  --partitions 8 \
  --config retention.ms=86400000 \
  --topic signal_status

复制因子设成3,这是生产环境的最低要求。你想想看,如果一台机器挂了,消息丢了,调度员那边看到的数据就是错的——这可不是闹着玩的。

4.3 RabbitMQ安装与配置

RabbitMQ在ATS里主要做命令下发。我习惯把它部署在控制中心,跟Kafka分开。

4.3.1 安装步骤

# Ubuntu/Debian
apt-get install rabbitmq-server

# 启动服务
systemctl start rabbitmq-server
systemctl enable rabbitmq-server

# 启用管理插件(方便运维)
rabbitmq-plugins enable rabbitmq_management

安装完成后,访问http://ip:15672就能看到管理界面。默认账号guest/guest,但生产环境一定要改!

4.3.2 关键配置

嗯,这里要注意:RabbitMQ的配置分两块——Erlang虚拟机参数RabbitMQ自身参数

# /etc/rabbitmq/rabbitmq.conf
# 内存阈值(别让RabbitMQ吃掉所有内存)
vm_memory_high_watermark.relative = 0.6

# 磁盘可用空间阈值
disk_free_limit.absolute = 2GB

# 心跳超时(轨道交通网络偶尔抖动,设大点)
heartbeat = 60

# 最大连接数
channel_max = 2048

我的经验:内存阈值设0.6,留40%给操作系统和Kafka。我曾经见过有人设0.9,结果OOM了,整个控制中心的消息都发不出去。

4.3.3 队列与交换机设计

ATS里我一般用Direct交换机做命令路由,用Topic交换机做告警分发。

# 创建交换机
rabbitmqadmin declare exchange name=cmd.direct type=direct
rabbitmqadmin declare exchange name=alert.topic type=topic

# 创建队列
rabbitmqadmin declare queue name=cmd.station_01 durable=true
rabbitmqadmin declare queue name=cmd.station_02 durable=true

# 绑定
rabbitmqadmin declare binding source=cmd.direct \
  destination=cmd.station_01 routing_key=station_01

说白了,就是让每个车站的ATS客户端只监听自己的队列,互不干扰。这样即使某个车站的网络断了,也不影响其他车站。

4.4 数据分发服务(DDS)配置

DDS在轨道交通里用得越来越多,尤其是实时性要求高的场景——比如列车控制命令的传输。它比Kafka/RabbitMQ更底层,走的是发布-订阅模式,没有中心节点。

4.4.1 DDS核心概念

  • DomainParticipant:参与者,每个进程一个
  • Topic:数据类型定义,比如列车位置、信号状态
  • Publisher/Subscriber:发布者和订阅者
  • QoS:服务质量策略,这是DDS的精髓

4.4.2 QoS配置实战

我以RTI Connext DDS为例,给出一个典型的ATS配置:

// 列车位置Topic的QoS配置
struct TrainPositionQos {
  // 可靠性:BEST_EFFORT(丢几个包没关系,位置更新快就行)
  reliability = BEST_EFFORT;
  
  // 历史记录:只保留最新的一条
  history = KEEP_LAST;
  history_depth = 1;
  
  // 时效性:超过100ms的数据直接丢弃
  lifespan = { sec: 0, nanosec: 100000000 };
  
  // 持久性:不持久化(位置数据实时性高)
  durability = VOLATILE;
};

// 控制命令Topic的QoS配置
struct ControlCommandQos {
  // 可靠性:RELIABLE(命令不能丢)
  reliability = RELIABLE;
  
  // 历史记录:保留所有未确认的命令
  history = KEEP_ALL;
  
  // 时效性:30秒内必须送达
  lifespan = { sec: 30, nanosec: 0 };
  
  // 持久性:持久化(防止订阅者晚到)
  durability = TRANSIENT_LOCAL;
};

关键点:位置数据用BEST_EFFORT,命令数据用RELIABLE。这个原则我在所有项目里都坚持,从来没出过问题。

4.4.3 网络分区配置

轨道交通的网络环境复杂,DDS需要配置发现协议和传输方式:

// 使用UDPv4作为传输层
dds.transport.UDPv4.builtin.parent.message_size_max = 65536

// 发现协议:使用SPDP+SEDP
dds.discovery.enabled = true
dds.discovery.initial_peers = "192.168.1.100,192.168.1.101"

// 忽略某些网段(防止跨区域干扰)
dds.discovery.ignore_loopback_interface = true
dds.discovery.ignore_network_interfaces = "docker0,veth*"

我曾经在某个项目中,DDS的发现报文跑到了Docker网桥上,导致控制中心和车站的参与者互相发现了对方,数据乱套了。加上ignore_network_interfaces就解决了。

4.5 服务注册与发现

ATS系统里服务多得很——数据采集服务、逻辑处理服务、人机界面服务……手动配置IP地址?不现实。服务注册与发现就是干这个的。

4.5.1 选型:Consul vs ZooKeeper vs etcd

组件 优势 劣势 我的推荐
Consul 自带健康检查、KV存储、DNS接口 部署稍重 ATS首选
ZooKeeper 成熟稳定、Kafka依赖它 API较底层 配合Kafka使用
etcd 轻量、Kubernetes原生 功能单一 云原生场景

我个人习惯用Consul,因为它自带的健康检查太方便了。你想想看,一个服务挂了,Consul能自动摘掉它,客户端就不会再往这个节点发请求了。

4.5.2 Consul部署

# 下载Consul
wget https://releases.hashicorp.com/consul/1.16.0/consul_1.16.0_linux_amd64.zip
unzip consul_1.16.0_linux_amd64.zip
mv consul /usr/local/bin/

# 启动Server节点(控制中心)
consul agent -server -bootstrap-expect=3 \
  -data-dir=/var/consul \
  -node=ats-consul-1 \
  -bind=192.168.1.100 \
  -client=0.0.0.0 \
  -ui

# 启动Client节点(车站)
consul agent -data-dir=/var/consul \
  -node=station-consul-01 \
  -bind=192.168.2.10 \
  -join=192.168.1.100

4.5.3 服务注册示例

以Python为例,注册一个列车数据处理服务:

import consul

c = consul.Consul(host='192.168.1.100', port=8500)

# 注册服务
c.agent.service.register(
    name='train-data-processor',
    service_id='train-data-processor-01',
    address='192.168.1.50',
    port=8080,
    tags=['v1', 'production'],
    check=consul.Check().tcp('192.168.1.50', 8080, interval='10s')
)

# 健康检查失败时自动注销
# Consul会每10秒检查一次TCP连接

注意:服务ID必须唯一。我见过有人忘了设service_id,结果两个实例互相覆盖,调度界面显示的服务状态时好时坏,排查了半天。

4.5.4 服务发现与负载均衡

客户端发现服务时,Consul会返回所有健康的实例:

# 查询服务
curl http://192.168.1.100:8500/v1/health/service/train-data-processor?passing=true

# 返回示例
[
  {
    "Node": "station-consul-01",
    "Service": {
      "ID": "train-data-processor-01",
      "Service": "train-data-processor",
      "Address": "192.168.1.50",
      "Port": 8080
    }
  },
  {
    "Node": "station-consul-02",
    "Service": {
      "ID": "train-data-processor-02",
      "Service": "train-data-processor",
      "Address": "192.168.1.51",
      "Port": 8080
    }
  }
]

客户端拿到列表后,自己做负载均衡。我一般用轮询或者最少连接数算法。嗯,这里要注意:如果某个实例挂了,Consul会在10秒内把它从列表里移除,客户端最多只会有10秒的“脏数据”。这个时间窗口在轨道交通里是可以接受的。

4.6 整体架构总结

好了,咱们把这一章的内容串起来。一个典型的ATS中间件架构是这样的:

  • Kafka:负责海量数据(列车位置、设备日志)的采集和分发
  • RabbitMQ:负责控制命令和告警的可靠投递
  • DDS:负责实时性要求极高的数据(比如列车控制命令)
  • Consul:负责所有服务的注册、发现和健康检查

说白了,就是让每种中间件干自己最擅长的事。别想着用一个东西解决所有问题——我见过有人只用Kafka做一切,结果命令延迟高得离谱,调度员都投诉了。

最后一句经验之谈:中间件部署完成后,一定要做压力测试和故障演练。我曾经在凌晨3点被电话叫醒,就是因为没做演练,Kafka集群挂了之后RabbitMQ也跟着崩溃了。从那以后,每个项目我都强制要求做混沌工程实验。

下一章咱们聊聊数据库的部署与调优,那是另一个有意思的话题。