这份指南的目标很窄:在一台干净的机器上把 Hermes 跑起来,并且发出、消费掉第一条消息。不谈调优,不谈多机房,把最小可用集群立起来再说。
读之前
文中的版本号、端口、路径都按我这次实际用的写。换版本前先看官方 release note 的 breaking changes——配置项改名是这类中间件最常见的升级坑。
拓扑:先看清楚要装几个东西
很多人第一次搭失败,是因为把它当成"一个进程"。Hermes 至少是三块:元数据存储、Broker 集群、接入层,加上可选的控制台。
Figure 01 · 最小可用拓扑
客户端不直连 Broker——这条规则决定了后面扩容能不能做到无感。
Step 1 · 依赖与主机准备
先把宿主机弄对,后面九成的"启动失败"其实都在这一步。
| 项 | 要求 | 为什么 |
|---|---|---|
| 内核 | Linux 4.9+ | 低版本的 epoll 行为差异会导致连接数上不去 |
| JDK | 17(LTS) | 官方发行版编译目标;用 11 能起但不受支持 |
| 文件句柄 | nofile ≥ 65536 | 每个分区一组文件,默认 1024 撑不过几十个 topic |
| 磁盘 | SSD,独立挂载点 | 和系统盘共用时,日志刷盘会拖垮整机 |
| 时钟 | NTP 已同步 | 集群选主依赖时间;偏差过大会反复切主 |
# 句柄数:临时生效 + 永久写入 ulimit -n 65536 cat <<'EOF' | sudo tee -a /etc/security/limits.conf * soft nofile 65536 * hard nofile 65536 EOF # 内核参数:连接积压与端口回收 cat <<'EOF' | sudo tee /etc/sysctl.d/99-hermes.conf net.core.somaxconn = 32768 net.ipv4.tcp_max_syn_backlog = 16384 vm.max_map_count = 262144 vm.swappiness = 1 EOF sudo sysctl --system # 数据目录(独立盘挂在这里) sudo mkdir -p /data/hermes/{store,meta,logs} sudo chown -R 1000:1000 /data/hermes
vm.swappiness = 1 不是笔误。这类服务把大量数据放在 page cache 里,一旦发生 swap,延迟会从毫秒级跳到秒级,而且监控上看不出任何异常——CPU、内存、磁盘 IO 都"正常"。设 0 在部分内核上会触发 OOM killer,所以留 1。
Step 2 · 容器编排
单机验证用 compose 就够。三个 Broker 跑在同一台机器上只是为了走通流程,生产必须分散到不同物理机,否则副本没有意义。
docker-compose.yml
version: "3.8" x-broker-common: &broker-common image: hermes/broker:1.8.2 restart: unless-stopped environment: HERMES_META_ADDR: "meta:2379" HERMES_HEAP_OPTS: "-Xms4g -Xmx4g -XX:+UseG1GC" HERMES_STORE_PATH: "/data/store" ulimits: nofile: { soft: 65536, hard: 65536 } depends_on: [meta] services: meta: image: hermes/meta:1.8.2 environment: META_DATA_DIR: "/data/meta" META_INITIAL_CLUSTER: "meta=http://meta:2380" volumes: - /data/hermes/meta:/data/meta ports: ["2379:2379"] broker-0: <<: *broker-common environment: HERMES_BROKER_ID: "0" HERMES_BROKER_ROLE: "leader" volumes: ["/data/hermes/store/0:/data/store"] broker-1: <<: *broker-common environment: { HERMES_BROKER_ID: "1" } volumes: ["/data/hermes/store/1:/data/store"] broker-2: <<: *broker-common environment: { HERMES_BROKER_ID: "2" } volumes: ["/data/hermes/store/2:/data/store"] proxy: image: hermes/proxy:1.8.2 environment: HERMES_META_ADDR: "meta:2379" ports: ["9876:9876", "8080:8080"] depends_on: [broker-0, broker-1, broker-2]
docker compose up -d docker compose ps # 五个容器都应该是 running docker compose logs -f proxy # 等到出现 "proxy started, listening on 9876"
这一步应该看到什么
docker compose ps 里五个服务全 running,并且 broker-* 的日志里能看到"registered to meta"。如果 Broker 反复重启,九成是 /data/hermes/store/* 的属主不对——回到 Step 1 检查 chown。
Step 3 · 元数据初始化
集群起来之后是空的,需要先建租户和 topic。这一步很多文档一笔带过,但分区数一旦定下来,后面改的代价很大。
# 建租户(命名空间隔离,多业务共用集群时必须做) hermesctl tenant create --name iot --quota-msg-per-sec 20000 # 建 topic:分区数 = 期望的最大并行消费度 hermesctl topic create \ --tenant iot \ --name device.telemetry \ --partitions 12 \ --replicas 3 \ --retention 72h # 建消费组 hermesctl group create --tenant iot --topic device.telemetry --name analytics # 确认 hermesctl topic describe --tenant iot --name device.telemetry
| 参数 | 怎么定 | 定错的后果 |
|---|---|---|
--partitions | 预估峰值 QPS ÷ 单分区处理能力,再向上取到 Broker 数的整数倍 | 偏小 → 消费者加了也没用;偏大 → 每个分区都是一组文件,元数据和句柄开销线性增长 |
--replicas | 生产固定 3 | 1 副本等于没有容灾;2 副本在脑裂时无法多数派仲裁 |
--retention | 按"最长一次故障恢复时间 × 3"估 | 太短 → 消费端修好了但数据已经过期;太长 → 磁盘打满,整个集群不可写 |
Step 4 · 接入方配置
Spring Boot 服务侧的最小配置。注意几个默认值不太友好的地方,我在注释里标出来了。
application.yml
hermes:
endpoint: hermes-proxy.internal:9876
tenant: iot
producer:
# 默认 async。要求不丢就必须显式改 sync + acks=all
ack-mode: sync
acks: all
# 默认 0,即失败直接抛。业务侧几乎总要改
retries: 3
retry-backoff: 200ms
# 攒批:吞吐和延迟的唯一旋钮
batch-size: 256
linger: 20ms
consumer:
group: analytics
# 默认 latest —— 新消费组会跳过历史消息,首次接入常踩
offset-reset: earliest
max-poll-records: 200
# 必须大于业务处理 max-poll-records 条的最坏耗时,否则会被判死反复重平衡
max-poll-interval: 5m
Producer.java
@Service public class TelemetryPublisher { private final HermesTemplate hermes; public void publish(String deviceSn, Telemetry t) { // key 决定分区。用 deviceSn 保证同一设备的消息严格有序 hermes.send("device.telemetry", deviceSn, t) .whenComplete((r, e) -> { if (e != null) { log.error("publish failed sn={}", deviceSn, e); // 落本地表,由补偿任务重发。不要在这里同步重试 outbox.save(deviceSn, t); } }); } }
Step 5 · 上线前必须验证的七件事
能收发消息只说明装对了,不说明能上线。这七条我每次都跑一遍。
端到端连通
hermesctl produce --topic device.telemetry --msg 'hello' 后用 hermesctl consume --group analytics --from-beginning 能读到。读不到先看 proxy 日志,不要先怀疑 SDK。
单 Broker 宕机
docker compose stop broker-1,生产消费应在数秒内自愈。观察 hermesctl topic describe 里 ISR 列表是否正确收缩。
Leader 切换
停掉 leader,确认新 leader 被选出且没有消息丢失。这是 acks=all 有没有真正生效的唯一检验方式。
消费组重平衡
启动第二个消费实例,确认分区被重新分配、且分配期间没有重复消费到不可接受的程度。业务侧的幂等在这一步暴露。
堆积回放
停消费者 10 分钟制造积压,再启动,观察追平速度。这个数字决定了故障恢复窗口。
磁盘水位告警
确认 retention 真的在删旧数据,且磁盘告警阈值低于"写入被拒"的阈值。磁盘打满是这类系统最常见的全局故障。
监控接上
至少四个指标进面板:生产 TPS、消费 lag、ISR 数量、磁盘使用率。lag 是唯一能提前发现问题的指标,其他三个响的时候通常已经出事了。
常见故障与对应现象
| 现象 | 大概率原因 | 确认方式 |
|---|---|---|
| Broker 启动后几秒退出,日志无明显异常 | 数据目录属主不对或磁盘只读 | docker compose logs broker-0 | tail -50 找 Permission denied |
| 生产端超时,proxy 正常 | topic 未创建,或租户配额已满 | hermesctl topic describe · hermesctl tenant describe |
| 消费组反复重平衡 | 业务处理耗时超过 max-poll-interval | 日志里搜 rebalance,看两次间隔是否接近该配置值 |
| 新消费组读不到历史消息 | offset-reset 仍是默认 latest | 看启动日志打印的实际配置,不要看配置文件 |
| lag 只涨不降 | 消费并行度 ≤ 分区数没吃满,或下游卡住 | 先看消费者线程栈——十有八九卡在下游调用上 |
搭完之后最该记住的一句
装起来只要一小时,把七条验证跑完要一整天——但省掉的那一天会在凌晨还给你
尤其是第 03 条和第 07 条