vault backup: 2026-05-24 20:51:06

This commit is contained in:
hhs
2026-05-24 20:51:06 +08:00
parent 910d16682b
commit 686c0a5bd5
52 changed files with 9246 additions and 0 deletions
@@ -0,0 +1,178 @@
---
tags:
- MQ
- 监控
- Prometheus
- Grafana
create time: 2026-05-24 19:52
---
# MQ 监控指标与告警
## 概述
消息队列作为系统间的"黑盒"中间件,一旦出问题往往影响全局。本文梳理 MQ 监控的核心指标体系,涵盖 Broker、Producer、Consumer 三个维度,并介绍基于 Prometheus + Grafana 的监控告警实践。
## 正文
### 为什么 MQ 需要专门的监控
消息队列不像 Web 服务那样有直观的 HTTP 状态码,它的健康状况藏在各种内部指标里。一个 Topic 的消费延迟可能已经到了几小时,但表面上看 Producer 和 Consumer 都在正常运行——只是 Consumer 跟不上了。等到下游业务发现数据不一致时,往往已经积重难返。
MQ 监控的核心目标就三个字:**看得见**。看得见消息有没有堆积,看得见 Broker 有没有过载,看得见消费者有没有掉队。
### Broker 核心指标
| 指标 | 含义 | 关注点 |
|------|------|--------|
| 消息入队速率 (MessagesIn/s) | 每秒写入的消息数 | 突增可能表示上游流量异常 |
| 消息出队速率 (BytesOut/s) | 每秒读出的字节数 | 与入队速率对比判断消费是否跟得上 |
| 磁盘使用率 | 日志段占用的磁盘空间 | 超过 80% 就该警惕 |
| 网络 IO | 网卡吞吐量 | 高负载时容易成为瓶颈 |
| 连接数 | 当前活跃的客户端连接 | 突增可能是连接泄漏 |
| ISR 数量 | In-Sync Replicas 数量 | ISR 收缩意味着有 Follower 掉队 |
> [!question]
> ISR(In-Sync Replicas)数量减少时,消息的可靠性会受到什么影响?如果你设置了 `acks=all`,ISR 缩减到 1 会发生什么?
### Producer 核心指标
Producer 侧最需要关注的是**发送成功率**和**发送延迟**。
- **发送成功率**:失败的发送请求占比。如果持续有失败,说明 Broker 端有问题(磁盘满、网络分区)。
- **发送延迟 P99**:99 分位的发送耗时。正常情况下应该在毫秒级,如果飙升到秒级,说明 Broker 压力过大或者网络抖动。
- **重试率**:发送失败后重试的比例。高重试率意味着消息可能乱序(Kafka 中同一 Partition 内的消息顺序靠 offset 保证,但重试可能导致后发的消息先到)。
- **批大小(Batch Size)**:Producer 端的批量发送大小。批越大吞吐越高,但延迟也越大。
### Consumer 核心指标
Consumer 侧最核心的指标是 **Consumer Lag**——消费者当前的消费位置与最新消息之间的差距。
Lag 本质上衡量的是"消费者落后了多远"。如果 Lag 持续增长,说明消费者处理不过来,消息正在积压。
- **消费速率**:每秒处理的消息数,应与 Producer 的入队速率基本持平。
- **Rebalance 次数**:Consumer Group 发生 Rebalance 的频率。频繁 Rebalance 会导致消费暂停,通常是 Consumer 不稳定(频繁重启、处理超时)引起的。
- **处理耗时**:单条消息从业务处理的平均耗时。如果处理耗时接近 `max.poll.interval.ms`,就有触发 Rebalance 的风险。
### Prometheus + Grafana 监控搭建
监控架构分四层:
```mermaid
graph LR
MQ["MQ Cluster"] -->|"指标暴露"| Exporter["MQ Exporter"]
Exporter -->|"pull /metrics"| Prometheus["Prometheus"]
Prometheus -->|"查询"| Grafana["Grafana"]
Prometheus -->|"告警规则"| AlertManager["AlertManager"]
AlertManager -->|"通知"| Notify["DingTalk / PagerDuty"]
```
**Exporter 配置**:Kafka 可以用 `kafka_exporter` 或 JMX Exporter 暴露指标。`kafka_exporter` 轻量级,适合快速接入;JMX Exporter 功能更全,但配置更复杂。
```yaml
# Prometheus 配置示例
scrape_configs:
- job_name: "kafka"
static_configs:
- targets: ["kafka-exporter:9308"]
scrape_interval: 15s
```
**核心 Dashboard 设计**:一个实用的 MQ Dashboard 通常包含以下几个面板:
1. **总览面板**:集群消息入队/出队速率、总 Lag 数、Broker 存活数。
2. **Broker 面板**:每个 Broker 的磁盘使用率、网络 IO、ISR 数量、请求队列深度。
3. **Topic 面板**:每个 Topic 的消息速率、Partition 分布、Lag 趋势。
4. **Consumer Group 面板**:每个 Group 的 Lag、消费速率、Rebalance 次数。
### 告警策略
好的告警策略不是"什么都报",而是"报了就要行动"。三种常用的告警模型:
**基于阈值**:最直观,比如 `Consumer Lag > 10000` 触发告警。适合有明确 SLO 的场景。
**基于趋势**:Lag 的绝对值不重要,重要的是它是否在持续增长。比如"Lag 连续 10 分钟单调递增"就该告警,即使当前 Lag 只有 100。
**基于异常检测**:用统计方法检测指标是否偏离正常范围。比如消费速率突然降到 0,但 Producer 侧没有任何变化,这很可能是 Consumer 挂了。
> [!question]
> 你能设计一个既不会频繁误报、又不会漏报关键问题的 Consumer Lag 告警规则吗?阈值设多少合适?
### Go 代码示例:自定义 Consumer Lag 监控
```go
package main
import (
"context"
"log"
"net/http"
"time"
"github.com/IBM/sarama"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
// consumerLag 每个 Partition 的消费延迟
consumerLag = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "myapp_consumer_lag",
Help: "Consumer lag per partition",
},
[]string{"topic", "partition", "group"},
)
)
func init() {
prometheus.MustRegister(consumerLag)
}
// collectLag 定期采集 Consumer Lag 并暴露给 Prometheus
func collectLag(brokers []string, group, topic string) {
config := sarama.NewConfig()
client, err := sarama.NewClient(brokers, config)
if err != nil {
log.Fatalf("create client: %v", err)
}
defer client.Close()
ticker := time.NewTicker(10 * time.Second)
for range ticker.C {
partitions, _ := client.Partitions(topic)
for _, p := range partitions {
// 获取最新 offset
newest, _ := client.GetOffset(topic, p, sarama.OffsetNewest)
// 获取消费者组当前 offset(实际需通过 Admin API)
// 这里简化为从外部获取
groupOffset := getGroupOffset(group, topic, p)
lag := float64(newest - groupOffset)
consumerLag.WithLabelValues(topic,
string(rune(p+'0')), group).Set(lag)
}
}
}
func getGroupOffset(group, topic string, partition int32) int64 {
// 实际实现中通过 Kafka Admin API 获取
return 0
}
func main() {
go collectLag([]string{"localhost:9092"}, "my-group", "orders")
http.Handle("/metrics", promhttp.Handler())
log.Println("metrics server on :2112")
log.Fatal(http.ListenAndServe(":2112", nil))
}
```
这段代码通过 Sarama 客户端定期查询每个 Partition 的最新 offset 和消费者组当前 offset,计算差值作为 Lag,然后通过 Prometheus Gauge 暴露。Grafana 中可以直接用 `myapp_consumer_lag` 指标配置 Dashboard 和告警规则。
## 关联笔记
- [[30-MQ-核心概念与选型]]
- [[33-MQ-Kafka-架构与核心机制]]
- [[37-MQ-消费积压治理]]
- [[35-MQ-与-CDC]]
@@ -0,0 +1,192 @@
---
tags:
- MQ
- 消费积压
- 性能优化
- Kafka
create time: 2026-05-24 19:52
---
# MQ 消费积压治理
## 概述
消息积压是 MQ 使用中最常见的"事故"之一:Producer 疯狂写入,Consumer 跟不上节奏,Lag 越堆越高,下游业务开始告警。本文从积压的成因、紧急处理到根本解决方案,系统性地梳理治理思路。
## 正文
### 消息积压是怎么产生的
消息积压的本质很简单:**生产速度 > 消费速度**。但具体原因千差万别:
1. **Consumer 消费速度跟不上**:业务逻辑变重了(比如新增了一次远程调用),单条消息处理耗时从 10ms 涨到 500ms,消费速率直接腰斩。
2. **Consumer 故障**:Consumer 实例挂了、OOM 了、被 K8s 驱逐了,但你不知道——因为消息队列不会主动告诉你"你的消费者已经死了"。
3. **Rebalance 风暴**:Consumer Group 频繁 Rebalance,每次 Rebalance 期间所有 Consumer 都暂停消费。如果 Consumer 处理超时触发 Rebalance,然后 Rebalance 又导致更多超时,就会进入恶性循环。
4. **流量突增**:大促、秒杀、爬虫攻击,Producer 端流量瞬间翻几倍,但 Consumer 还是原来那点资源。
### 积压的影响
积压不是"慢一点"那么简单,它会引发连锁反应:
- **消息延迟增大**:用户下单后要等几分钟才能收到确认通知,体验极差。
- **触发过期删除**:Kafka 的日志保留策略(比如 7 天),如果积压的消息超过了保留时间,还没被消费就被删了,数据丢失。
- **下游业务受影响**:如果下游依赖 CDC 事件更新缓存或索引,积压会导致缓存长时间不更新,数据不一致。
### 紧急处理方案
发现积压后,第一步不是优化代码,而是先止血。
**方案一:快速扩容 Consumer 实例**
这是最直接的办法。Consumer Lag 了 10 万条?加 Consumer 实例就行。但有个前提:**Partition 数量要够**。
Kafka 中,一个 Partition 最多只能被同一个 Consumer Group 中的一个 Consumer 消费。如果你的 Topic 只有 4 个 Partition,那最多只能有 4 个 Consumer 同时消费,加再多实例也没用。
> [!question]
> 你的 Topic 有 8 个 Partition,当前跑了 3 个 Consumer 实例,Lag 在持续增长。这时候你决定加到 10 个 Consumer,会发生什么?多出来的 2 个在干嘛?
**方案二:临时 Topic 转发**
如果 Consumer 的消费逻辑很重(比如涉及数据库写入、远程调用),而且短时间内改不了,可以这样做:
1. 写一个"轻量消费者",只做一件事:从积压的 Topic 读消息,批量转发到一个临时 Topic。
2. 启动一批新的 Consumer 消费临时 Topic,这些 Consumer 的消费逻辑可以是更轻量的版本(比如只写入一个临时表,不做完整业务处理)。
3. 积压消化完后,再慢慢回补业务逻辑。
这个方案的核心思想是:**先快速消费完,再慢慢处理**。
**方案三:降级消费**
如果积压的消息中有关键消息和非关键消息(比如订单消息是关键的,日志消息是非关键的),可以临时修改 Consumer 逻辑,跳过非关键消息,优先处理核心业务。
```go
// 降级消费:跳过非关键消息
func handleMsg(msg *kafka.Message) {
priority := msg.Headers.Get("priority")
if priority == "low" && isBacklogHigh() {
// 积压严重时,低优先级消息直接丢弃
log.Printf("skip low priority msg: offset=%d", msg.Offset)
return
}
// 正常处理关键消息
processBusinessLogic(msg)
}
```
> [!question]
> 降级消费意味着丢弃部分消息。在什么业务场景下这是可以接受的?你能想到哪些场景绝对不能降级?
### 根本解决:优化消费逻辑
紧急处理只是止血,要根治得从消费逻辑入手。
**减少 IO 操作**:消费逻辑中的数据库写入、远程调用是最常见的瓶颈。检查一下是不是每条消息都触发一次 DB 写入?能不能改成批量写入?
**异步处理**:消费逻辑中如果有非关键步骤(比如发通知、写日志),可以异步化。消费者只做最关键的操作(比如写主库),其余的丢到 goroutine 或者另一个队列。
**批量消费**:攒一批消息一起处理,减少网络往返和事务开销。
```go
package main
import (
"context"
"log"
"time"
"github.com/segmentio/kafka-go"
)
func main() {
reader := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{"localhost:9092"},
Topic: "orders",
GroupID: "order-processor",
MinBytes: 1, // 最少 1 字节就返回
MaxBytes: 10e6, // 最多 10MB
CommitInterval: 0, // 手动提交
})
defer reader.Close()
batch := make([]kafka.Message, 0, 100)
ticker := time.NewTicker(500 * time.Millisecond) // 每 500ms 刷一次
defer ticker.Stop()
for {
select {
case <-ticker.C:
if len(batch) == 0 {
continue
}
// 批量写入数据库(一个事务搞定)
if err := batchInsert(batch); err != nil {
log.Printf("batch insert error: %v", err)
continue
}
// 手动提交 offset
if err := reader.CommitMessages(context.Background(), batch[len(batch)-1]); err != nil {
log.Printf("commit error: %v", err)
}
log.Printf("processed batch of %d messages", len(batch))
batch = batch[:0] // 清空批次
default:
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
msg, err := reader.ReadMessage(ctx)
cancel()
if err != nil {
continue // 超时,继续攒消息
}
batch = append(batch, msg)
if len(batch) >= 100 {
// 批次满了,立即处理
ticker.Reset(0) // 触发立即刷入
}
}
}
}
func batchInsert(msgs []kafka.Message) error {
// 批量插入数据库的逻辑
return nil
}
```
这段代码展示了批量消费的核心思路:攒够 100 条或者等 500ms,然后一次性写入数据库。相比逐条消费逐条写入,批量处理可以将数据库写入次数降低一到两个数量级。
**增加 Partition**:如果优化消费逻辑后还是不够,那就增加 Partition 数量,从根本上提升消费并行度。但这需要新建 Topic、迁移数据、修改 Producer 配置,成本不低,应该提前做好容量规划。
### 预防措施
与其事后救火,不如事前防火:
- **容量规划**:根据历史流量峰值,预留 2-3 倍的消费能力。
- **压力测试**:上线前用压测工具(如 Kafka 的 `kafka-producer-perf-test`)验证消费能力。
- **告警阈值**:设置 Lag 告警,比如 Lag > 5000 触发 P2 告警,Lag > 50000 触发 P1 告警,Lag 连续 5 分钟单调递增触发紧急告警。
### 积压处理决策流程
```mermaid
flowchart TD
A["发现消费积压"] --> B{"积压原因?"}
B -->|"Consumer 实例挂了"| C["重启/扩容 Consumer"]
B -->|"消费逻辑太慢"| D{"能否快速优化?"}
B -->|"流量突增"| E{"Partition 数量够?"}
D -->|"能"| F["异步化/批量消费"]
D -->|"不能"| G["临时 Topic 转发"]
E -->|"够"| C
E -->|"不够"| H["降级消费 + 后续扩容 Partition"]
C --> I["观察 Lag 趋势"]
F --> I
G --> I
H --> I
I -->|"Lag 恢复"| J["复盘 + 优化预案"]
I -->|"Lag 未恢复"| B
```
## 关联笔记
- [[30-MQ-核心概念与选型]]
- [[33-MQ-Kafka-架构与核心机制]]
- [[36-MQ-监控指标与告警]]
- [[35-MQ-与-CDC]]
@@ -0,0 +1,188 @@
---
tags:
- MQ
- 链路追踪
- 可观测性
- 分布式系统
create time: 2026-05-24 19:52
---
# MQ 消息轨迹与链路追踪
## 概述
"消息发了,但对方说没收到。"这句话是分布式系统调试中最让人头疼的开场白。消息轨迹与链路追踪就是为了解决这个问题——让你清楚地知道一条消息从生产到消费的完整生命周期。
## 正文
### 为什么需要消息轨迹
在微服务架构中,一条消息从 Producer 发出,经过 Broker 存储,最终被 Consumer 消费处理。这个链路上任何一个环节出问题,排查起来都非常困难:
- 消息到底发出去了吗?Broker 收到了吗?
- 消息被哪个 Consumer Group 消费了?
- 消费成功了还是失败了?失败的原因是什么?
- 一条业务消息从 A 服务到 B 服务到 C 服务,整条链路耗时多少?
传统的日志排查方式效率极低——你需要登录多台机器,grep 关键字,然后手动拼凑时间线。消息轨迹就是把这些信息结构化记录下来,让排查变成"查一下就知道"。
### 消息唯一标识设计
要追踪一条消息,首先得能唯一标识它。这里有三个关键标识:
**MessageID**:消息在 Broker 端的唯一标识。Kafka 用 `Topic + Partition + Offset` 三元组定位一条消息,RocketMQ 有独立的 MessageID。这是技术层面的唯一标识。
**业务 Key**:业务层面的唯一标识,比如订单号 `order_id`。一条消息可能因为重试产生多个 MessageID,但业务 Key 是不变的。排查问题时,通常从业务 Key 入手。
**TraceID**:链路追踪层面的标识,贯穿整个调用链。一个 TraceID 可以关联"HTTP 请求 → 消息发送 → 消息消费 → 数据库写入"这条完整链路。
三者的关系:MessageID 定位消息,业务 Key 定位业务实体,TraceID 串联链路。排查问题时,通常先用业务 Key 找到消息,再用 TraceID 看完整链路。
> [!question]
> 如果一条消息因为消费失败被重试了 3 次,最终成功。从消息轨迹的角度看,这 3 次重试应该怎么记录?是 3 条独立轨迹还是一条轨迹的 3 个消费记录?
### 消息轨迹的三个阶段
一条消息的完整生命周期可以分为三个阶段,每个阶段都需要记录关键信息:
**生产轨迹**:Producer 发送消息时记录。包括发送时间、发送结果(成功/失败)、目标 Topic、消息 Key、消息大小、耗时。
**存储轨迹**:Broker 接收消息时记录。包括入库时间、分配的 Partition、分配的 Offset、Broker 节点地址。
**消费轨迹**:Consumer 消费消息时记录。包括消费时间、消费结果(成功/失败/重试)、消费耗时、Consumer 实例标识、处理异常信息。
```mermaid
graph LR
Producer["Producer"] -->|"1. 发送消息"| Broker["Broker"]
Broker -->|"2. 存储消息"| Storage["Storage"]
Broker -->|"3. 投递消息"| Consumer["Consumer"]
Producer -.->|"生产轨迹"| TraceDB["Trace Storage"]
Broker -.->|"存储轨迹"| TraceDB
Consumer -.->|"消费轨迹"| TraceDB
TraceDB -->|"查询"| UI["Trace UI"]
```
### RocketMQ 消息轨迹原生支持
RocketMQ 内置了消息轨迹功能,这是它相比 Kafka 的一个差异化特性。启用方式非常简单:
```go
// Producer 端启用消息轨迹
producer, _ := rocketmq.NewProducer(
producer.WithNameServer([]string{"127.0.0.1:9876"}),
producer.WithTrace(&primitive.TraceConfig{
TraceTopic: "RMQ_SYS_TRACE_TOPIC", // 轨迹数据写入的 Topic
Access: primitive.Local,
}),
)
```
RocketMQ 的轨迹数据结构大致如下:
```go
type TraceTransferBean struct {
TimeStamp int64 // 时间戳
TraceType string // 生产/消费
MsgId string // 消息 ID
Topic string // Topic
MsgType string // 消息类型
GroupName string // 消费组
Keys string // 业务 Key
CostTime int64 // 处理耗时
Success bool // 是否成功
TraceBeans []TraceBean // 轨迹详情
}
```
轨迹数据本身也被当作一条消息,写入一个专门的 Trace Topic。这样做的好处是轨迹记录不会影响主业务流程,轨迹数据也可以像普通消息一样被消费和分析。
### 链路集成:与分布式追踪系统对接
消息轨迹解决的是 MQ 维度的问题,但在微服务架构中,一条业务链路往往跨越 HTTP 调用、消息队列、数据库操作等多个维度。这时候需要将 MQ 轨迹集成到分布式追踪系统中。
主流的分布式追踪系统(Jaeger、Zipkin、SkyWalking)都支持通过 TraceID 串联跨服务的调用链。集成的关键是:**在消息 Header 中传播 TraceID**。
Producer 发送消息时,将当前 Span 的 TraceID 写入消息 Header:
```go
package main
import (
"context"
"log"
"github.com/segmentio/kafka-go"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
func sendMessage(ctx context.Context, topic string, value []byte) {
// 从当前 Context 提取 TraceID,注入到消息 Header
propagator := otel.GetTextMapPropagator()
headers := make(map[string]string)
propagator.Inject(ctx, propagation.MapCarrier(headers))
// 将 TraceID 作为消息 Header 发送
kafkaHeaders := make([]kafka.Header, 0, len(headers))
for k, v := range headers {
kafkaHeaders = append(kafkaHeaders, kafka.Header{Key: k, Value: []byte(v)})
}
writer := &kafka.Writer{
Addr: kafka.TCP("localhost:9092"),
Topic: topic,
}
defer writer.Close()
err := writer.WriteMessages(ctx, kafka.Message{
Value: value,
Headers: kafkaHeaders,
})
if err != nil {
log.Printf("send error: %v", err)
}
}
func consumeMessage(ctx context.Context, msg kafka.Message) {
// 从消息 Header 中提取 TraceID,恢复 Trace 上下文
propagator := otel.GetTextMapPropagator()
headers := make(map[string]string)
for _, h := range msg.Headers {
headers[h.Key] = string(h.Value)
}
ctx = propagator.Extract(ctx, propagation.MapCarrier(headers))
// 在这条 Trace 下记录消费 Span
tracer := otel.Tracer("mq-consumer")
_, span := tracer.Start(ctx, "consume-message")
defer span.End()
// 业务处理...
}
```
这段代码展示了 TraceID 的传播机制:Producer 端通过 OpenTelemetry 的 Propagator 将 TraceID 注入消息 Header,Consumer 端从 Header 中提取 TraceID 并恢复 Trace 上下文。这样,一次 HTTP 请求触发的消息发送和消费,就能在 Jaeger 中看到完整的链路视图。
### 消息审计日志
除了技术排查,消息轨迹还有一个重要用途:**审计**。
在金融、电商等场景中,你需要知道"谁在什么时候发了什么消息"、"谁在什么时候消费了什么消息"。这些信息不仅是排查工具,也是合规要求。
审计日志通常记录以下信息:
- 操作人/操作服务的标识
- 操作时间
- 操作类型(生产/消费/删除)
- Topic 和消息 Key
- 消息摘要(不是完整内容,通常是前 256 字节 + Hash)
> [!question]
> 消息轨迹会增加额外的存储和性能开销。如果你的系统每天产生 1 亿条消息,每条消息记录 3 条轨迹(生产/存储/消费),每天就有 3 亿条轨迹数据。生产环境应该如何平衡追踪粒度和性能?采样是一个好策略吗?
## 关联笔记
- [[30-MQ-核心概念与选型]]
- [[33-MQ-Kafka-架构与核心机制]]
- [[35-MQ-与-CDC]]
- [[36-MQ-监控指标与告警]]
@@ -0,0 +1,200 @@
---
tags:
- MQ
- Kubernetes
- 容器化
- 运维
create time: 2026-05-24 19:52
---
# MQ 容器化与 K8s 部署
## 概述
将消息队列部署到 Kubernetes 上,是有状态服务容器化的核心挑战之一。本文从持久存储、网络标识、有序部署三个维度分析难点,介绍 StatefulSet、Operator 模式、Helm Chart 等主流方案,并以 Strimzi Kafka Operator 为例展示生产级部署实践。
## 正文
### 为什么 MQ 上 K8s 是个难题
Kubernetes 天生为无状态服务设计——Pod 可以随时被杀掉重建、IP 随时变化、多个副本之间没有区别。但 MQ 作为有状态服务,恰恰对这三点都很敏感:
- **持久存储**:消息必须落盘,Pod 重建后数据不能丢
- **网络标识**:Broker 之间需要通过固定地址互相发现(如 Kafka 的 `broker.id` 对应固定端点)
- **有序部署**:集群启动/扩容时需要按顺序进行,不能所有 Pod 同时拉起
> [!question]
> 想象一下 Kafka 的 3 个 Broker 同时启动并互相注册,会发生什么?为什么有序启动很重要?
### StatefulSet:有状态应用的基石
StatefulSet 是 K8s 专门为有状态服务设计的控制器,解决了 Deployment 的两个核心问题:
1. **稳定的网络标识**:每个 Pod 拥有固定名称(`kafka-0`、`kafka-1`、`kafka-2`),配合 Headless Service 可以通过 DNS 直接访问
2. **稳定的持久存储**:每个 Pod 绑定独立的 PVC(PersistentVolumeClaim),Pod 重建后自动挂载原来的卷
```yaml
# StatefulSet 核心配置片段
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kafka
spec:
serviceName: kafka-headless # 关联 Headless Service
replicas: 3
template:
spec:
containers:
- name: kafka
volumeMounts:
- name: data
mountPath: /var/lib/kafka/data
volumeClaimTemplates: # 每个 Pod 自动创建独立 PVC
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
```
但 StatefulSet 只解决了基础设施层面的问题。**什么时候该扩容、扩容后 Partition 怎么重分配、配置变更怎么滚动生效**——这些业务运维知识它一概不管。于是 Operator 登场了。
### Operator 模式:把运维知识编码
Operator = Custom Resource Definition(CRD)+ Custom Controller。核心思想是:**将人类运维专家的知识编码为程序**,让 K8s 自动执行运维操作。
对于 MQ 来说,Operator 能做到:
- 一键部署完整集群(含 ZooKeeper/KRaft)
- 滚动升级 Broker 版本(保证零停机)
- 自动处理 Partition 重分配
- 证书自动签发和轮转
主流 MQ Operator:
| MQ | Operator | 维护方 |
|----|----------|--------|
| Kafka | Strimzi | CNCF |
| RocketMQ | rocketmq-operator | Apache |
| RabbitMQ | cluster-operator | VMware |
| Pulsar | pulsar-helm-chart | Apache |
### Strimzi Kafka Operator 实战
Strimzi 是 CNCF 孵化项目,通过 CRD 将 Kafka 集群声明式管理。核心 CRD 有三个:
- **Kafka**:定义 Kafka 集群拓扑(Broker 数量、存储、配置)
- **KafkaTopic**:声明式管理 Topic
- **KafkaUser**:声明式管理用户和 ACL 权限
```yaml
# Strimzi Kafka CR 示例:3 节点集群
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
name: my-cluster
spec:
kafka:
version: 3.7.0
replicas: 3
listeners:
- name: plain
port: 9092
type: internal
tls: false
- name: tls
port: 9093
type: internal
tls: true
config:
offsets.topic.replication.factor: 3
transaction.state.log.replication.factor: 3
transaction.state.log.min.isr: 2
default.replication.factor: 3
min.insync.replicas: 2
storage:
type: jbod
volumes:
- id: 0
type: persistent-claim
size: 100Gi
class: fast-ssd
zookeeper:
replicas: 3
storage:
type: persistent-claim
size: 20Gi
```
Kafka 集群的完整部署架构如下:
```mermaid
graph TB
subgraph K8s["Kubernetes Cluster"]
subgraph NS["namespace: kafka"]
Operator["Strimzi Operator"]
CR["Kafka CR"]
Operator -->|"watch & reconcile"| CR
CR -->|"create"| STS["StatefulSet"]
STS -->|"manage"| Pod0["kafka-0"]
STS -->|"manage"| Pod1["kafka-1"]
STS -->|"manage"| Pod2["kafka-2"]
Pod0 ---|"PVC"| PV0["PV-0 100Gi SSD"]
Pod1 ---|"PVC"| PV1["PV-1 100Gi SSD"]
Pod2 ---|"PVC"| PV2["PV-2 100Gi SSD"]
HS["Headless Service"] -->|"DNS: kafka-0.kafka-headless"| Pod0
HS -->|"DNS: kafka-1.kafka-headless"| Pod1
HS -->|"DNS: kafka-2.kafka-headless"| Pod2
LB["LoadBalancer Service"] -->|"external access"| Pod0
LB --> Pod1
LB --> Pod2
end
end
Client["Client App"] -->|"produce/consume"| LB
```
### Helm Chart 部署
对于不想引入 Operator 的轻量场景,Helm Chart 是更简单的选择。Bitnami 提供了完善的 Kafka Helm Chart,核心优势是参数化配置和环境隔离:
```bash
# 安装 Kafka 集群
helm install my-kafka bitnami/kafka \
--namespace kafka --create-namespace \
--set replicaCount=3 \
--set persistence.size=100Gi \
--set persistence.storageClass=fast-ssd \
--set resources.requests.memory=4Gi \
--set resources.requests.cpu=2 \
--set resources.limits.memory=6Gi \
--set resources.limits.cpu=4
```
Helm 适合快速搭建和开发/测试环境,但在生产环境中,Operator 的自动化运维能力(升级、扩容、故障恢复)是 Helm 无法替代的。
### 资源规划建议
MQ 在 K8s 上的资源规划需要特别注意:
- **CPU/Memory**:Kafka Broker 建议至少 2C4G,生产环境推荐 4C8G。请求值和限制值都要设置,避免被驱逐或抢占资源
- **存储**:SSD 用于高吞吐场景(低延迟随机读写),HDD 用于大容量归档。通过 StorageClass 区分
- **JVM 堆**:Kafka Broker 堆不宜过大(4-6GB 足够),留更多内存给页缓存。推荐 G1GC 或 ZGC
- **磁盘调度**:容器内使用 `none`(noop)调度算法,避免与宿主机调度冲突
### 网络方案
| 方式 | 适用场景 | 特点 |
|------|----------|------|
| Headless Service | Broker 间内部通信 | Pod 直连,无负载均衡 |
| ClusterIP | 集群内客户端访问 | 默认方式 |
| NodePort | 开发测试外部访问 | 端口范围 30000-32767 |
| LoadBalancer | 生产外部访问 | 云厂商 LB,费用较高 |
| Ingress | HTTP 协议暴露 | MQ 一般不走 HTTP,适用 REST Proxy |
## 关联笔记
- [[40-MQ-性能调优]]
- [[42-MQ-认证与授权]]
- [[43-MQ-加密与审计]]
@@ -0,0 +1,180 @@
---
tags:
- MQ
- 性能调优
- Kafka
- JVM
create time: 2026-05-24 19:52
---
# MQ 性能调优
## 概述
MQ 的性能调优是一个系统工程,涉及 Producer、Broker、Consumer、JVM、操作系统多个层面。本文以 Kafka 为主线,从性能指标出发,逐层拆解调优策略,帮助你建立完整的调优方法论。
## 正文
### 性能指标三角
调优之前先明确目标。MQ 的核心性能指标有三个:
- **吞吐量(Throughput)**:每秒处理的消息数(msgs/sec)或字节数(MB/sec)
- **延迟(Latency)**:消息从发送到被确认/消费的时间,关注 P50、P99、P999
- **可用性(Availability)**:系统可正常服务的时间比例
> [!question]
> 吞吐量和延迟往往是矛盾的——批处理提升吞吐但增加延迟。你的业务场景更看重哪个?为什么?
### Producer 调优
Producer 是性能瓶颈的第一个关口。核心参数和调优思路:
**batch.size**(默认 16KB):批量发送的消息大小上限。增大可以提升吞吐(减少网络往返),但增加内存占用和首次延迟。生产环境建议设为 64KB-1MB。
**linger.ms**(默认 0):等待凑满 batch 的时间。默认 0 表示来一条发一条,设为 5-100ms 可以显著提升吞吐。和 batch.size 配合使用——达到任一条件即发送。
**compression.type**(默认 none):压缩算法。`lz4` 性价比最高(压缩率和速度均衡),`zstd` 压缩率更好但 CPU 开销略高。压缩能显著减少网络带宽和磁盘占用。
**acks**:可靠性级别。
- `acks=0`:不等确认,最快但可能丢消息
- `acks=1`:Leader 确认,平衡选择
- `acks=all`:ISR 全部确认,最安全但最慢
**buffer.memory**(默认 32MB):Producer 端发送缓冲区大小。高吞吐场景下如果 Broker 响应慢,缓冲区可能打满导致阻塞,可适当增大到 64-128MB。
### Broker 调优
Broker 是整个集群的性能中枢。
**刷盘策略**:Kafka 依赖页缓存和顺序写,刷盘策略直接影响性能。
- `flush.messages` 和 `flush.ms` 控制刷盘频率
- 生产环境建议**关闭主动刷盘**(设为超大值),依赖操作系统的页缓存和后台刷盘,配合多副本保证数据安全
**页缓存**:Kafka 的性能秘诀在于大量利用 OS 页缓存。消息写入先进页缓存,异步刷盘。消费者如果跟得上生产者,直接从页缓存读取,接近内存速度。
**网络线程和 IO 线程**:
- `num.network.threads`:处理网络请求的线程数,建议设为 CPU 核数
- `num.io.threads`:处理磁盘 IO 的线程数,建议设为 CPU 核数的 2 倍
- `num.replica.fetchers`:副本同步线程数,高吞吐场景可增大到 2-4
### Consumer 调优
Consumer 的调优重点在于批量拉取和并发处理。
**fetch.min.bytes**(默认 1):一次 fetch 请求的最小数据量。增大可以减少网络往返次数,但增加延迟。建议设为 1KB-64KB。
**fetch.max.wait.ms**(默认 500ms):等待凑满 fetch.min.bytes 的最大时间。配合 fetch.min.bytes 使用,在吞吐和延迟之间找平衡。
**max.poll.records**(默认 500):单次 poll 返回的最大消息数。如果消费逻辑较重(如写数据库),适当减小避免处理超时。
**并发消费**:单个 Consumer 的吞吐有限,通常通过增加 Consumer 数量(不超过 Partition 数量)来提升并行度。
> [!question]
> Consumer 数量超过 Partition 数量会发生什么?如何在不增加 Partition 的情况下提升消费能力?
### JVM 调优
Kafka Broker 运行在 JVM 上,JVM 调优至关重要。
**堆大小**:不宜过大!Kafka Broker 推荐 4-6GB。堆越大 GC 停顿越长,而且 Kafka 的性能很大程度依赖页缓存——堆占了太多内存,页缓存就被挤压了。
**GC 选择**:
- **G1GC**:成熟稳定,适合 4-8GB 堆。关键参数 `-XX:MaxGCPauseMillis=20`
- **ZGC**:超低停顿(<10ms),适合大堆场景,JDK 15+ 生产可用
**直接内存**:Kafka 的网络传输使用堆外内存(DirectBuffer),需要通过 `-XX:MaxDirectMemorySize` 预留足够空间。
### 操作系统调优
操作系统层面的优化往往被忽视,但影响显著:
- **文件描述符**:Kafka 每个 Partition 对应多个日志段文件,需要大量 fd。建议设置 `ulimit -n 100000` 以上
- **TCP 参数**:增大缓冲区 `net.core.rmem_max=16777216`、`net.core.wmem_max=16777216`,启用 TCP 快速打开
- **vm.swappiness**:设为 1(非 0),避免系统过度 swap 导致 Broker 性能急剧下降
- **磁盘调度算法**:SSD 使用 `none`(noop)或 `mq-deadline`,避免 `cfq` 的高开销
### 基准测试工具
调优前后都要用数据说话。常用工具:
- **kafka-producer-perf-test**:Producer 吞吐和延迟测试
- **kafka-consumer-perf-test**:Consumer 吞吐测试
- **JMeter**:支持自定义场景的压测框架
### 性能瓶颈定位决策树
```mermaid
graph TD
Start["性能不达标"] --> CheckWhere{"瓶颈在哪?"}
CheckWhere -->|"Producer 端"| P["Producer 调优"]
CheckWhere -->|"Broker 端"| B["Broker 调优"]
CheckWhere -->|"Consumer 端"| C["Consumer 调优"]
P --> P1["增大 batch.size"]
P --> P2["增大 linger.ms"]
P --> P3["开启压缩"]
P --> P4["acks=1"]
B --> B1["关闭主动刷盘"]
B --> B2["增大网络/IO线程"]
B --> B3["JVM 调优"]
B --> B4["OS 参数调优"]
C --> C1["增大 fetch.min.bytes"]
C --> C2["增加并发消费者"]
C --> C3["增大 max.poll.records"]
```
### Go 性能测试示例
```go
package main
import (
"fmt"
"sync/atomic"
"time"
"github.com/IBM/sarama"
)
func main() {
config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForLocal
config.Producer.Compression = sarama.CompressionLZ4 // 开启压缩
config.Producer.Flush.Bytes = 64 * 1024 // batch.size 64KB
config.Producer.Flush.Frequency = 10 * time.Millisecond // linger.ms 10
producer, _ := sarama.NewAsyncProducer([]string{"localhost:9092"}, config)
defer producer.Close()
var count int64
msg := &sarama.ProducerMessage{
Topic: "benchmark",
Value: sarama.StringEncoder("test message payload"),
}
// 10 个 goroutine 并发发送
for i := 0; i < 10; i++ {
go func() {
for {
producer.Input() <- msg
atomic.AddInt64(&count, 1)
}
}()
}
// 每秒输出吞吐量
ticker := time.NewTicker(time.Second)
for range ticker.C {
n := atomic.SwapInt64(&count, 0)
fmt.Printf("Throughput: %d msgs/sec\n", n)
}
}
```
这段代码展示了 Sarama 异步 Producer 的基准测试:开启 LZ4 压缩、64KB 批量、10ms 等待时间,10 个 goroutine 并发发送,每秒统计吞吐量。通过调整参数组合对比不同配置下的性能表现。
## 关联笔记
- [[39-MQ-容器化与-K8s-部署]]
- [[41-MQ-测试策略]]
- [[42-MQ-认证与授权]]
@@ -0,0 +1,189 @@
---
tags:
- MQ
- 测试
- 集成测试
- 契约测试
create time: 2026-05-24 19:52
---
# MQ 测试策略
## 概述
MQ 的测试面临异步性、消息顺序、环境依赖等独特挑战。本文从单元测试到混沌工程,逐层介绍 MQ 测试策略,重点讲解 Testcontainers 集成测试、Pact 契约测试和影子流量等实践方法。
## 正文
### MQ 测试的挑战
和同步的 HTTP API 相比,MQ 测试难在哪里?
1. **异步性**:消息发送后不立即得到结果,如何验证消息被正确处理?
2. **消息顺序**:测试时如何保证消息的顺序和幂等性?
3. **环境依赖**:没有真实的 Broker,很多行为无法验证(如 Partition 路由、Consumer Group Rebalance)
4. **分布式一致性**:跨多个服务的事件流转,如何端到端验证?
> [!question]
> 传统 API 测试可以用 `assert response.status == 200`。但 MQ 中,消息发出去后"成功"意味着什么?是 Broker 收到?还是被消费者处理完?
### 测试金字塔在 MQ 场景的应用
```mermaid
graph TD
subgraph Pyramid["MQ 测试金字塔"]
E2E["E2E 测试 - 完整集群 + 多服务"]
Integration["集成测试 - Testcontainers 真实 Broker"]
Contract["契约测试 - Pact 消息契约"]
Unit["单元测试 - Mock Producer/Consumer"]
end
Unit -->|"快速, 大量"| Contract
Contract -->|"契约保证兼容"| Integration
Integration -->|"真实环境验证"| E2E
```
### 单元测试:Mock 接口,验证逻辑
单元测试的核心是将消息处理逻辑与 Broker 解耦。在 Go 中,通过接口抽象 Producer 和 Consumer:
```go
// 定义接口,方便 Mock
type MessageProducer interface {
Send(ctx context.Context, topic string, key, value []byte) error
}
type OrderHandler struct {
producer MessageProducer
}
// 处理订单创建事件的业务逻辑
func (h *OrderHandler) HandleOrderCreated(ctx context.Context, event OrderEvent) error {
if event.Amount <= 0 {
return fmt.Errorf("invalid amount: %d", event.Amount)
}
// 发送支付请求事件
payment := PaymentEvent{OrderID: event.ID, Amount: event.Amount}
data, _ := json.Marshal(payment)
return h.producer.Send(ctx, "payment-requests", []byte(event.ID), data)
}
// 单元测试:Mock Producer 验证业务逻辑
func TestOrderHandler_HandleOrderCreated(t *testing.T) {
mock := &MockProducer{} // 实现 MessageProducer 接口
handler := &OrderHandler{producer: mock}
event := OrderEvent{ID: "order-1", Amount: 100}
err := handler.HandleOrderCreated(context.Background(), event)
assert.NoError(t, err)
assert.Equal(t, "payment-requests", mock.LastTopic())
assert.Equal(t, "order-1", mock.LastKey())
}
```
单元测试快速且无外部依赖,但只能验证业务逻辑,无法保证消息格式与下游兼容。
### 集成测试:Testcontainers 启动真实 Broker
Mock 测试不到的问题——Partition 路由、序列化、Consumer Group 行为——需要真实 Broker。Testcontainers 在测试中启动真实的 Docker 容器:
```go
package main
import (
"context"
"testing"
"time"
"github.com/IBM/sarama"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/kafka"
)
func TestKafkaIntegration(t *testing.T) {
ctx := context.Background()
// 启动真实的 Kafka 容器
kafkaContainer, err := kafka.Run(ctx,
"confluentinc/confluent-local:7.5.0",
)
if err != nil {
t.Fatal(err)
}
defer kafkaContainer.Terminate(ctx)
// 获取 Broker 地址
brokers, _ := kafkaContainer.Brokers(ctx)
// 创建 Producer 发送消息
config := sarama.NewConfig()
config.Producer.Return.Successes = true
producer, _ := sarama.NewSyncProducer(brokers, config)
defer producer.Close()
msg := &sarama.ProducerMessage{
Topic: "test-topic",
Value: sarama.StringEncoder("hello kafka"),
}
partition, offset, err := producer.SendMessage(msg)
if err != nil {
t.Fatalf("send failed: %v", err)
}
// 创建 Consumer 消费并验证
config2 := sarama.NewConfig()
config2.Consumer.Offsets.Initial = sarama.OffsetOldest
consumer, _ := sarama.NewConsumer(brokers, config2)
defer consumer.Close()
pc, _ := consumer.ConsumePartition("test-topic", partition, offset)
select {
case msg := <-pc.Messages():
if string(msg.Value) != "hello kafka" {
t.Errorf("unexpected message: %s", msg.Value)
}
case <-time.After(10 * time.Second):
t.Fatal("timeout waiting for message")
}
}
```
这段测试用 Testcontainers 自动拉起 Kafka 容器,发送一条消息后立即消费验证。整个过程在 CI 中无需预置环境,测试结束自动清理容器。
### 消息契约测试
在事件驱动架构中,Producer 和 Consumer 通过消息格式(Schema)解耦。但如果 Producer 改了字段名,Consumer 就会崩溃。**契约测试**解决这个问题。
Pact 框架的工作流程:
1. **Consumer 端**定义期望的消息格式(契约)
2. **Producer 端**验证自己产生的消息满足所有 Consumer 的契约
3. 契约存储在 Pact Broker 中,CI 中自动验证
> [!question]
> 消息契约测试和传统的 API 测试有什么本质区别?为什么在事件驱动架构中更重要?
关键区别在于:API 测试验证的是"请求-响应"的一对一关系,而消息契约验证的是"事件-消费者"的一对多关系。一个事件可能被 5 个服务消费,任何格式变更都必须向后兼容。
### 影子流量(Shadow Testing)
将生产流量复制到测试环境,验证新版本的消息处理逻辑是否正确,而不影响真实业务。
实现方式:
- Producer 端使用拦截器,将消息副本发送到 Shadow Topic
- Shadow Consumer 消费并处理,对比结果
- 关键:Shadow 消费者的处理结果**不会写入生产数据库**
### 故障注入
Chaos Engineering 在 MQ 场景的应用:
- **Kill Broker**:随机杀掉一个 Broker,验证 Producer/Consumer 的自动故障转移
- **网络分区**:模拟 Broker 之间网络隔离,验证脑裂防护
- **消息延迟注入**:人为增加 Broker 响应延迟,验证超时和重试机制
- **磁盘满**:模拟磁盘空间不足,验证告警和降级策略
## 关联笔记
- [[40-MQ-性能调优]]
- [[42-MQ-认证与授权]]
- [[39-MQ-容器化与-K8s-部署]]