yangshan.link / 山之志 · 后端工程笔记

首页/文章/Hermes 搭建

基础设施

Hermes 搭建指南:从零到能收发第一条消息

按顺序做完就能跑起来。每一步都写清楚"做完之后应该看到什么",看不到就别往下走。

这份指南的目标很窄:在一台干净的机器上把 Hermes 跑起来,并且发出、消费掉第一条消息。不谈调优,不谈多机房,把最小可用集群立起来再说。

读之前

文中的版本号、端口、路径都按我这次实际用的写。换版本前先看官方 release note 的 breaking changes——配置项改名是这类中间件最常见的升级坑。

拓扑:先看清楚要装几个东西

很多人第一次搭失败,是因为把它当成"一个进程"。Hermes 至少是三块:元数据存储、Broker 集群、接入层,加上可选的控制台。

Figure 01 · 最小可用拓扑

Producer 业务服务 接入层 proxy :9876 Broker-0 leader Broker-1 replica Broker-2 replica 元数据 meta store Consumer 消费组 元数据只被 Broker 读写;客户端永远只连接入层

客户端不直连 Broker——这条规则决定了后面扩容能不能做到无感。

Step 1 · 依赖与主机准备

先把宿主机弄对,后面九成的"启动失败"其实都在这一步。

项要求为什么
内核Linux 4.9+低版本的 epoll 行为差异会导致连接数上不去
JDK17(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生产固定 31 副本等于没有容灾;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);
                  }
              });
    }
}
关于顺序。"全局有序"在分区模型下等价于"只用一个分区",也就等价于放弃水平扩展。绝大多数业务真正需要的是同一实体内有序——把实体 ID 当 key 就够了,这是这类中间件最重要的一个心智模型。

Step 5 · 上线前必须验证的七件事

能收发消息只说明装对了,不说明能上线。这七条我每次都跑一遍。

01

端到端连通

hermesctl produce --topic device.telemetry --msg 'hello' 后用 hermesctl consume --group analytics --from-beginning 能读到。读不到先看 proxy 日志,不要先怀疑 SDK。

02

单 Broker 宕机

docker compose stop broker-1,生产消费应在数秒内自愈。观察 hermesctl topic describe 里 ISR 列表是否正确收缩。

03

Leader 切换

停掉 leader,确认新 leader 被选出且没有消息丢失。这是 acks=all 有没有真正生效的唯一检验方式。

04

消费组重平衡

启动第二个消费实例,确认分区被重新分配、且分配期间没有重复消费到不可接受的程度。业务侧的幂等在这一步暴露。

05

堆积回放

停消费者 10 分钟制造积压,再启动,观察追平速度。这个数字决定了故障恢复窗口。

06

磁盘水位告警

确认 retention 真的在删旧数据,且磁盘告警阈值低于"写入被拒"的阈值。磁盘打满是这类系统最常见的全局故障。

07

监控接上

至少四个指标进面板:生产 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 条

← 回到全部文章