189 lines
5.9 KiB
Markdown
189 lines
5.9 KiB
Markdown
|
|
---
|
|||
|
|
tags:
|
|||
|
|
- MQ
|
|||
|
|
create time: 2026-05-24 19:52
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# MQ 客户端 SDK 最佳实践
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
一个好的 MQ SDK 能让开发者专注于业务逻辑,而不是陷入连接管理、重试策略、序列化等底层细节。本文从 SDK 设计原则、连接管理、优雅关闭、序列化策略、错误处理、可观测性等维度,介绍生产级 MQ SDK 的设计要点,并给出完整的 Go 封装示例。
|
|||
|
|
|
|||
|
|
## 正文
|
|||
|
|
|
|||
|
|
### SDK 设计原则
|
|||
|
|
|
|||
|
|
一个优秀的 MQ 客户端 SDK 应该遵循四个原则:
|
|||
|
|
|
|||
|
|
1. **抽象接口**:隐藏底层 MQ 实现细节,业务代码不直接依赖 Kafka/RocketMQ/RabbitMQ 的原生 API。这样切换 MQ 产品时只改 SDK 实现,不改业务代码。
|
|||
|
|
2. **可配置性**:超时、重试次数、批次大小等参数应该可配置,而不是硬编码。
|
|||
|
|
3. **重试与容错**:网络抖动、Broker 短暂不可用时自动重试,而不是让业务代码处理。
|
|||
|
|
4. **可观测性**:内置 TraceID 注入、Metrics 埋点、结构化日志,方便排查问题。
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 抽象接口设计
|
|||
|
|
type MQProducer interface {
|
|||
|
|
Send(ctx context.Context, topic string, message *Message) error
|
|||
|
|
SendBatch(ctx context.Context, topic string, messages []*Message) error
|
|||
|
|
Close() error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type MQConsumer interface {
|
|||
|
|
Subscribe(topic string, handler func(ctx context.Context, msg *Message) error) error
|
|||
|
|
Close() error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type Message struct {
|
|||
|
|
Key string
|
|||
|
|
Value []byte
|
|||
|
|
Headers map[string]string
|
|||
|
|
Timestamp time.Time
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 连接管理
|
|||
|
|
|
|||
|
|
MQ 客户端通常维护**长连接**而非短连接。建立连接的开销(TCP 三次握手 + MQ 协议握手)很大,频繁创建销毁会严重影响性能。
|
|||
|
|
|
|||
|
|
连接池设计要点:
|
|||
|
|
- 初始连接数和最大连接数可配置。
|
|||
|
|
- 连接空闲超时后自动回收。
|
|||
|
|
- 健康检查:定期发送心跳探测连接是否存活。
|
|||
|
|
- 自动重连:检测到连接断开后,后台自动重建连接。
|
|||
|
|
|
|||
|
|
### 优雅关闭(Graceful Shutdown)
|
|||
|
|
|
|||
|
|
优雅关闭是生产环境最容易被忽略、最容易出问题的环节。关闭顺序至关重要:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph LR
|
|||
|
|
A["收到 SIGTERM"] --> B["停止接受新消息"]
|
|||
|
|
B --> C["等待当前消息处理完成"]
|
|||
|
|
C --> D["提交 Offset - 消费者"]
|
|||
|
|
D --> E["刷新缓冲区 - 生产者"]
|
|||
|
|
E --> F["关闭连接"]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
核心代码:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type Client struct {
|
|||
|
|
producer MQProducer
|
|||
|
|
consumer MQConsumer
|
|||
|
|
shutdown chan struct{}
|
|||
|
|
wg sync.WaitGroup
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (c *Client) GracefulShutdown(ctx context.Context) error {
|
|||
|
|
close(c.shutdown) // 1. 通知所有 goroutine 停止接受新任务
|
|||
|
|
|
|||
|
|
// 2. 等待所有进行中的任务完成(带超时)
|
|||
|
|
done := make(chan struct{})
|
|||
|
|
go func() {
|
|||
|
|
c.wg.Wait()
|
|||
|
|
close(done)
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
select {
|
|||
|
|
case <-done:
|
|||
|
|
// 正常完成
|
|||
|
|
case <-ctx.Done():
|
|||
|
|
// 超时,强制关闭
|
|||
|
|
log.Warn("graceful shutdown timeout, forcing close")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 关闭连接
|
|||
|
|
c.producer.Close()
|
|||
|
|
c.consumer.Close()
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 序列化策略
|
|||
|
|
|
|||
|
|
消息体的序列化方式直接影响性能和可调试性:
|
|||
|
|
|
|||
|
|
| 方式 | 优点 | 缺点 | 适用场景 |
|
|||
|
|
|------|------|------|---------|
|
|||
|
|
| JSON | 人类可读、调试方便 | 体积大、序列化慢 | 业务消息、日志 |
|
|||
|
|
| Protobuf | 体积小、速度快 | 需要预定义 Schema | 高性能内部通信 |
|
|||
|
|
| Avro | Schema 演进支持好 | 生态相对小 | 数据湖、大数据场景 |
|
|||
|
|
| MessagePack | 类 JSON 但更紧凑 | 生态不如 JSON | 性能敏感 + 跨语言 |
|
|||
|
|
|
|||
|
|
### 错误处理
|
|||
|
|
|
|||
|
|
SDK 必须区分**可重试错误**和**不可重试错误**:
|
|||
|
|
|
|||
|
|
- **可重试**:网络超时、Broker 暂时不可用、限流(429)。这些错误过一会儿重试可能就成功了。
|
|||
|
|
- **不可重试**:消息格式错误、Topic 不存在、权限不足。重试多少次都不会成功。
|
|||
|
|
|
|||
|
|
不可重试的消息应该进入**死信队列(Dead Letter Queue)**,而不是无限重试或丢弃。同时触发告警通知运维人员。
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (c *Client) SendWithRetry(ctx context.Context, topic string, msg *Message) error {
|
|||
|
|
for i := 0; i <= c.maxRetries; i++ {
|
|||
|
|
err := c.producer.Send(ctx, topic, msg)
|
|||
|
|
if err == nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
if !isRetryable(err) {
|
|||
|
|
c.sendToDLQ(topic, msg, err) // 不可重试,进死信
|
|||
|
|
c.alertManager.Notify("unrecoverable mq error", err)
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
// 指数退避
|
|||
|
|
time.Sleep(c.backoff(i))
|
|||
|
|
}
|
|||
|
|
c.sendToDLQ(topic, msg, errors.New("max retries exceeded"))
|
|||
|
|
return errors.New("send failed after retries")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 可观测性集成
|
|||
|
|
|
|||
|
|
生产级 SDK 必须内置可观测性:
|
|||
|
|
|
|||
|
|
- **TraceID 注入**:在消息 Headers 中注入 TraceID,实现跨服务链路追踪。
|
|||
|
|
- **Metrics 埋点**:发送成功率、消费延迟、队列堆积量等关键指标。
|
|||
|
|
- **结构化日志**:JSON 格式日志,包含 Topic、Partition、Offset 等上下文信息。
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (c *Client) Send(ctx context.Context, topic string, msg *Message) error {
|
|||
|
|
start := time.Now()
|
|||
|
|
|
|||
|
|
// 注入 TraceID
|
|||
|
|
if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() {
|
|||
|
|
msg.Headers["trace-id"] = span.SpanContext().TraceID().String()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
err := c.producer.Send(ctx, topic, msg)
|
|||
|
|
|
|||
|
|
// Metrics 埋点
|
|||
|
|
duration := time.Since(start)
|
|||
|
|
c.metrics.Histogram("mq.send.duration").Observe(duration.Seconds())
|
|||
|
|
c.metrics.Counter("mq.send.total").Inc()
|
|||
|
|
if err != nil {
|
|||
|
|
c.metrics.Counter("mq.send.error").Inc()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 结构化日志
|
|||
|
|
c.logger.Info("message sent",
|
|||
|
|
zap.String("topic", topic),
|
|||
|
|
zap.String("key", msg.Key),
|
|||
|
|
zap.Duration("latency", duration),
|
|||
|
|
zap.Error(err),
|
|||
|
|
)
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!question]
|
|||
|
|
> SDK 应该默认开启自动重试吗?重试次数和间隔如何确定?提示:考虑幂等性和消费者的处理能力。
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[47-MQ-与微服务]]
|
|||
|
|
- [[49-MQ-客户端连接管理]]
|
|||
|
|
- [[50-MQ-设计与实现]]
|