第一章:Go语言自动发消息的典型应用场景与技术全景
在现代软件架构中,自动消息推送已成为连接系统组件、触达终端用户和驱动业务流程的关键能力。Go语言凭借其高并发、低内存占用、跨平台编译及原生HTTP/GRPC支持等特性,成为构建可靠消息自动化服务的理想选择。
典型应用场景
- 运维告警通知:监控系统(如Prometheus + Alertmanager)通过Webhook调用Go编写的轻量级服务,将告警摘要实时推送到企业微信、钉钉或飞书群;
- 订单状态同步:电商后端在订单状态变更(如“已发货”)时,调用Go微服务向用户发送结构化短信或APP推送;
- 定时任务提醒:结合
github.com/robfig/cron/v3库实现毫秒级精度的定时触发,例如每日9点向团队发送站会待办汇总邮件; - IoT设备联动:边缘网关使用Go解析MQTT消息,当温湿度超阈值时自动向指定手机号发送短信(通过运营商API或Twilio)。
技术生态全景
| Go生态提供了分层清晰的消息集成能力: | 层级 | 代表工具/协议 | 典型用途 |
|---|---|---|---|
| 协议层 | net/smtp, net/http |
直连邮件服务器、调用RESTful通知API | |
| SDK封装层 | github.com/go-resty/resty/v2 |
简化HTTP请求,内置JSON序列化与重试 | |
| 消息中间件 | github.com/segmentio/kafka-go |
高吞吐异步解耦,保障消息不丢失 | |
| 云服务适配层 | cloud.google.com/go/pubsub |
无缝对接GCP Pub/Sub、AWS SNS等托管服务 |
以下为调用钉钉机器人发送文本消息的最小可行代码示例:
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
)
type DingTalkMsg struct {
MsgType string `json:"msgtype"`
Text struct {
Content string `json:"content"`
} `json:"text"`
}
func sendDingTalk(webhookURL, content string) error {
msg := DingTalkMsg{
MsgType: "text",
Text: struct{ Content string }{Content: content},
}
data, _ := json.Marshal(msg)
resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(data))
if err != nil {
return err
}
defer resp.Body.Close()
_, _ = io.ReadAll(resp.Body) // 忽略响应体,仅校验HTTP状态码
return nil
}
该函数可嵌入任意业务逻辑中,配合错误重试与日志记录即可投入生产环境。
第二章:网络通信层的五大隐形陷阱
2.1 HTTP客户端超时配置不当导致消息堆积与goroutine泄漏
问题根源:缺失超时控制的阻塞调用
当 http.DefaultClient 被直接使用且未设置超时,底层 net.Dialer 和 http.Transport 均采用无限等待策略,导致请求在 DNS 解析失败、TCP 连接挂起或服务端无响应时持续阻塞。
典型错误配置示例
// ❌ 危险:无超时,goroutine 永久阻塞
client := &http.Client{} // 等价于 http.DefaultClient
resp, err := client.Get("https://api.example.com/v1/data")
分析:
client.Get()内部调用transport.RoundTrip(),若 TCP 握手卡住(如防火墙丢包),该 goroutine 将无法被调度回收,持续占用栈内存与 OS 文件描述符。
推荐安全配置
| 超时类型 | 推荐值 | 作用说明 |
|---|---|---|
Timeout |
10s | 整个请求生命周期上限(含连接+读写) |
DialTimeout |
3s | DNS + TCP 连接建立最大耗时 |
KeepAlive |
30s | 空闲连接保活探测间隔 |
// ✅ 正确:显式控制各阶段超时
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 3 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
},
}
分析:
DialContext替代旧版Dial,支持上下文取消;Timeout作为兜底,防止Read/Write阶段长尾阻塞。
goroutine 泄漏链路
graph TD
A[发起HTTP请求] --> B{是否配置超时?}
B -- 否 --> C[阻塞于net.Conn.Read]
C --> D[goroutine无法退出]
D --> E[fd泄漏 + 内存累积]
B -- 是 --> F[超时触发context.Cancel]
F --> G[底层连接自动关闭]
2.2 TLS证书验证缺失引发中间人攻击与连接静默失败
当客户端跳过 TLS 证书验证(如 verify=False),攻击者可在网络路径中伪装成服务端,劫持加密通道。
常见危险配置示例
import requests
# ⚠️ 危险:禁用证书验证
response = requests.get("https://api.example.com", verify=False)
verify=False 绕过 CA 签名检查与域名匹配(Subject Alternative Name),使自签名或伪造证书被无条件接受,导致 MITM 可解密、篡改流量。
静默失败的典型表现
- 连接成功但返回伪造响应(如篡改的 JSON 数据)
- 无异常抛出,日志中仅显示
200 OK - 客户端逻辑误判为“服务正常”,加剧风险隐蔽性
| 风险维度 | 启用验证 | 禁用验证(verify=False) |
|---|---|---|
| 服务器身份确认 | ✅ CA + 域名校验 | ❌ 完全跳过 |
| 中间人防护 | ✅ 有效阻断 | ❌ 完全失效 |
| 错误反馈 | ❌ SSLCertVerificationError | ✅ 无异常,静默接受 |
graph TD
A[客户端发起HTTPS请求] --> B{是否验证证书?}
B -- 是 --> C[校验CA链+域名+有效期]
B -- 否 --> D[直接建立TLS会话]
C -->|失败| E[抛出SSL异常]
D --> F[与攻击者完成密钥交换]
F --> G[后续通信被解密/注入]
2.3 请求头与Content-Type未标准化造成第三方API拒绝服务
当客户端未显式设置 Content-Type 或使用非标准值(如 application/json; charset=utf8),部分严格校验的API会直接返回 415 Unsupported Media Type。
常见错误 Content-Type 示例
- ❌
application/json; charset=utf8(utf8缺少横线) - ❌
text/json(非 IANA 注册类型) - ✅
application/json; charset=utf-8
正确请求示例
POST /api/v1/users HTTP/1.1
Host: api.example.com
Content-Type: application/json; charset=utf-8
Authorization: Bearer xyz
{"name":"Alice","email":"a@example.com"}
逻辑分析:
charset=utf-8中的连字符是 RFC 8259 强制要求;缺失或错写将导致中间网关(如 AWS API Gateway)或后端框架(如 Spring Boot 的@RequestBody解析器)拒绝解析。
| 客户端行为 | 服务端响应 | 原因 |
|---|---|---|
| 未设 Content-Type | 400 | Spring 默认拒绝空类型 |
text/plain |
415 | MediaType 不匹配白名单 |
application/json |
200 | 标准注册类型,解析成功 |
graph TD
A[客户端发起请求] --> B{Content-Type 是否符合 RFC 8259?}
B -->|否| C[API 网关拦截 → 415]
B -->|是| D[反序列化 → 业务逻辑]
2.4 连接池复用不足与MaxIdleConns设置失当引发连接耗尽
当 MaxIdleConns 设为过小值(如 5),而并发请求峰值达 200,空闲连接迅速耗尽,新请求被迫新建连接——若未设 MaxOpenConns 上限,将突破数据库连接数限制。
连接池典型错误配置
db.SetMaxIdleConns(5) // ❌ 空闲连接池过小,无法缓冲突发流量
db.SetMaxOpenConns(0) // ❌ 0 表示无上限,直连 DB 最大连接数
db.SetConnMaxLifetime(1h) // ✅ 合理,避免长连接老化
SetMaxIdleConns(5) 导致高并发下频繁创建/关闭连接;SetMaxOpenConns(0) 放任连接数野蛮增长,最终触发 MySQL 的 Too many connections 错误。
关键参数对照表
| 参数 | 推荐值 | 作用 |
|---|---|---|
MaxIdleConns |
Min(50, MaxOpenConns) |
缓存可复用的空闲连接 |
MaxOpenConns |
依据 DB 实例规格设定(如 RDS 2C4G → 100) | 全局连接数硬上限 |
连接耗尽路径
graph TD
A[HTTP 请求涌入] --> B{空闲连接 ≥1?}
B -- 否 --> C[新建连接]
B -- 是 --> D[复用 idle 连接]
C --> E[检查 MaxOpenConns 是否超限?]
E -- 是 --> F[阻塞或报错]
E -- 否 --> G[成功获取连接]
2.5 异步发送中Context取消未传播导致僵尸请求与资源滞留
根本成因
当 context.WithTimeout 创建的父 Context 被取消,但异步 goroutine 中未显式监听 ctx.Done(),HTTP 客户端底层连接、缓冲区及 goroutine 将持续驻留。
典型错误模式
func sendAsync(ctx context.Context, url string) {
go func() {
// ❌ 未将 ctx 传入 http.Do,且未 select ctx.Done()
resp, _ := http.DefaultClient.Get(url) // 阻塞直至响应或超时(非 ctx 控制)
defer resp.Body.Close()
}()
}
逻辑分析:
http.Get使用默认http.DefaultClient,其超时由客户端自身Timeout字段控制,完全忽略传入的ctx;goroutine 无法感知父 Context 取消,形成僵尸协程。参数ctx形同虚设。
正确传播路径
| 组件 | 是否响应 ctx.Done() |
说明 |
|---|---|---|
http.Client |
否(默认) | 需显式构造并传入 ctx |
http.NewRequestWithContext |
是 | 唯一能绑定 Context 的入口 |
client.Do(req) |
是(仅当 req.Context() 有效) | 依赖 req 的 Context |
修复后流程
graph TD
A[父Context Cancel] --> B{select ctx.Done()}
B -->|触发| C[调用 req.Cancel()]
C --> D[底层 TCP 连接中断]
D --> E[goroutine 安全退出]
第三章:消息可靠性保障的关键实践
3.1 幂等性设计:基于消息ID与服务端去重机制的协同实现
在分布式消息场景中,网络重试或生产者重复投递易引发重复消费。核心解法是客户端生成唯一 message_id(如 UUID + 时间戳哈希),服务端结合 Redis 去重表实现原子校验。
数据同步机制
服务端接收请求后执行:
def process_message(msg):
msg_id = msg["id"]
# 使用 SETNX 实现原子插入,过期时间防内存泄漏
if redis.set(msg_id, "processed", ex=3600, nx=True):
execute_business_logic(msg) # 仅首次执行
else:
log.info(f"Duplicate message ignored: {msg_id}")
ex=3600 确保幂等窗口覆盖业务最长处理周期;nx=True 保障写入原子性。
协同保障层级
- 客户端:强制携带不可变
message_id,禁止业务层生成 - 服务端:Redis 去重 + DB 写入时
INSERT ... ON CONFLICT DO NOTHING(PostgreSQL) - 监控:对
duplicate_rate > 0.1%触发告警
| 组件 | 职责 | 失效影响 |
|---|---|---|
| 消息ID生成 | 提供全局唯一标识 | 去重完全失效 |
| Redis去重表 | 秒级去重判据 | 短时重复上升 |
| 数据库约束 | 持久化最终一致性 | 重复数据入库 |
graph TD
A[Producer] -->|send with message_id| B[Broker]
B --> C[Consumer]
C --> D{Check in Redis<br/>SETNX message_id?}
D -->|Yes| E[Execute & Persist]
D -->|No| F[Skip]
3.2 本地持久化队列选型对比(BoltDB vs Badger vs SQLite)与落地代码
核心特性对比
| 特性 | BoltDB | Badger | SQLite |
|---|---|---|---|
| 数据模型 | KV(单文件、嵌套Bucket) | KV(LSM-tree、多文件) | 关系型(SQL、ACID事务) |
| 并发写入支持 | ❌(仅单goroutine写) | ✅(高并发写优化) | ✅(WAL模式下支持) |
| 原生队列语义 | 需手动实现索引/游标 | 提供Iterator+Set原子操作 |
支持INSERT ... RETURNING+ORDER BY rowid |
典型写入代码(Badger)
// 使用Badger实现FIFO队列的原子入队
func (q *BadgerQueue) Enqueue(ctx context.Context, payload []byte) error {
return q.db.Update(func(txn *badger.Txn) error {
key := fmt.Sprintf("job:%d", time.Now().UnixNano()) // 简单时间戳键
return txn.SetEntry(badger.NewEntry([]byte(key), payload).WithTTL(24*time.Hour))
})
}
逻辑分析:Update确保写入原子性;WithTTL自动清理过期任务;键设计规避BoltDB的写锁瓶颈,利用Badger的并发写吞吐优势。
数据同步机制
- BoltDB:依赖外部序列化+内存重放,易丢数据
- Badger:
ValueLogGC()配合定期快照保障一致性 - SQLite:
PRAGMA synchronous = NORMAL+ WAL日志双写平衡性能与可靠性
graph TD
A[生产者写入] --> B{选型决策}
B -->|低延迟+强一致| C[Badger]
B -->|兼容SQL生态| D[SQLite]
B -->|极简嵌入+读多写少| E[BoltDB]
3.3 重试策略建模:指数退避+抖动+最大尝试次数的Go原生实现
在分布式系统中,瞬时故障(如网络抖动、服务限流)要求客户端具备智能重试能力。朴素的固定间隔重试易引发雪崩,而指数退避(Exponential Backoff)可有效缓解重试风暴。
核心设计要素
- 指数增长:每次等待时间 =
base * 2^attempt - 抖动(Jitter):引入随机因子避免同步重试
- 硬性上限:防止无限循环,保障响应确定性
Go 原生实现示例
func WithExponentialBackoff(base time.Duration, maxAttempts int) func(int) time.Duration {
return func(attempt int) time.Duration {
if attempt >= maxAttempts {
return 0 // 不再重试
}
// 指数退避 + 均匀抖动 [0, 1)
backoff := float64(base) * math.Pow(2, float64(attempt))
jitter := rand.Float64() * backoff
return time.Duration(backoff+jitter) * time.Millisecond
}
}
逻辑说明:
base单位为毫秒(如100表示 100ms),maxAttempts控制总尝试次数(含首次)。math.Pow实现指数增长;rand.Float64()提供[0,1)随机因子,使各实例退避时间错开。注意需在调用前rand.Seed(time.Now().UnixNano())。
| 参数 | 类型 | 推荐值 | 说明 |
|---|---|---|---|
base |
time.Duration |
100ms |
初始延迟,不宜过短 |
maxAttempts |
int |
5 |
含第1次请求,共最多5次 |
graph TD
A[发起请求] --> B{成功?}
B -- 否 --> C[计算退避时间]
C --> D[休眠]
D --> A
B -- 是 --> E[返回结果]
第四章:生产级集成中的高频崩塌点
4.1 微服务间调用链路中断时的消息状态同步与Saga补偿逻辑
当订单服务调用库存服务超时,分布式事务需保障最终一致性。核心依赖消息状态双写 + Saga 长事务补偿。
数据同步机制
采用「本地消息表 + 定时扫描」模式确保消息不丢失:
-- 本地消息表(与业务操作同库事务)
CREATE TABLE local_message (
id BIGINT PRIMARY KEY,
payload JSON NOT NULL, -- 补偿指令:{ "type": "rollback_inventory", "orderId": "xxx" }
status VARCHAR(20) DEFAULT 'pending', -- pending/sent/confirmed/failed
created_at TIMESTAMP DEFAULT NOW(),
next_retry_at TIMESTAMP, -- 指数退避重试时间点
retry_count TINYINT DEFAULT 0
);
逻辑分析:
status字段驱动状态机流转;next_retry_at避免密集轮询;payload封装幂等ID与补偿参数,供下游服务校验重放。
Saga 补偿流程
graph TD
A[订单创建成功] –> B[发「扣减库存」消息]
B –> C{库存服务响应?}
C — 成功 –> D[更新订单状态为“已支付”]
C — 失败/超时 –> E[触发补偿:发「恢复库存」消息]
关键状态映射表
| 订单状态 | 库存预留状态 | 是否可补偿 |
|---|---|---|
| CREATED | PENDING | 是 |
| PAID | RESERVED | 否(已终态) |
| CANCELLED | RELEASED | 已执行 |
4.2 日志可观测性缺失:结构化日志+OpenTelemetry trace注入实战
当微服务间调用链路断裂,仅靠 console.log("user loaded") 无法定位延迟根因——日志缺乏 trace_id 关联,形成可观测性黑洞。
结构化日志统一输出
// 使用 pino + otel SDK 注入 trace context
const logger = pino({
transport: { target: 'pino-pretty' },
base: { pid: process.pid },
formatters: {
bindings: (bindings) => ({ pid: bindings.pid }),
log: (obj) => ({
...obj,
traceId: obj?.traceId || currentSpan?.spanContext()?.traceId || '',
spanId: obj?.spanId || currentSpan?.spanContext()?.spanId || '',
service: 'auth-service',
timestamp: new Date().toISOString()
})
}
});
逻辑分析:formatters.log 动态注入 OpenTelemetry 当前 Span 上下文;traceId/spanId 来自 currentSpan.spanContext(),确保日志与分布式追踪对齐;service 字段为后续 Loki 查询提供标签维度。
trace 注入关键路径
- HTTP 入口中间件自动创建 Span
- 异步任务(如 Redis 消费)手动
startSpan并传递 Context - 日志调用时隐式绑定当前 Span(通过
context.with)
| 字段 | 来源 | 用途 |
|---|---|---|
traceId |
SpanContext.traceId |
关联全链路所有日志与指标 |
spanId |
SpanContext.spanId |
定位当前操作在调用树中的位置 |
service |
静态配置 | Prometheus/Loki 多租户隔离 |
graph TD
A[HTTP Request] --> B[Start Root Span]
B --> C[Log with traceId/spanId]
C --> D[Call User Service]
D --> E[Propagate Context via HTTP Headers]
4.3 配置热加载失效:Viper监听变更后连接池重建与消息队列刷新
当 Viper 监听配置文件变更时,若仅重载参数而未协调底层资源生命周期,将导致热加载“假成功”——配置已更新,但 *sql.DB 连接池仍持旧 maxOpen/maxIdle,RabbitMQ channel 亦复用旧 prefetchCount 与 durable 设置。
数据同步机制
需在 viper.WatchConfig() 回调中触发资源重建:
viper.OnConfigChange(func(e fsnotify.Event) {
db.Close() // 先安全关闭旧连接池
db = NewDBFromViper(viper.Sub("database"))
mq.Reconnect(viper.Sub("rabbitmq")) // 刷新channel与queue声明
})
逻辑说明:
db.Close()阻塞至所有活跃连接归还,避免连接泄漏;NewDBFromViper读取新database.max_open_conns等键重建池;Reconnect重建 AMQP connection 并重新声明 queue(含durable、auto_delete等元数据)。
关键重建依赖项
| 组件 | 依赖配置键 | 是否强制重建 |
|---|---|---|
| SQL 连接池 | database.max_open_conns |
是 |
| RabbitMQ channel | rabbitmq.prefetch_count |
是 |
| 日志级别 | logging.level |
否(运行时动态生效) |
graph TD
A[配置变更事件] --> B{是否影响连接池或MQ?}
B -->|是| C[关闭旧资源]
B -->|否| D[仅更新内存配置]
C --> E[按新配置重建资源]
E --> F[验证健康状态]
4.4 多通道统一抽象:微信/钉钉/邮件/SMS接口的Adapter模式封装与错误归一化
为屏蔽各消息通道协议差异,采用 Adapter 模式构建 MessageChannel 统一接口:
class MessageChannel:
def send(self, payload: dict) -> Result:
raise NotImplementedError
class WeComAdapter(MessageChannel):
def send(self, payload: dict) -> Result:
# payload 包含: "to_user", "msg_type", "content"
# 调用企业微信 API,捕获 40041(access_token 过期)等特有错误
return self._normalize_error(response)
逻辑分析:send() 接收标准化 payload,子类负责协议转换与异常捕获;_normalize_error() 将微信 40013、钉钉 50005、SMTP 554 等映射为统一错误码(如 ERR_CHANNEL_UNAUTHORIZED, ERR_CONTENT_REJECTED)。
错误归一化映射表
| 原始错误源 | 原始码/描述 | 归一化错误码 |
|---|---|---|
| 企业微信 | 40013(corpid错误) | ERR_CHANNEL_INVALID_CONFIG |
| 钉钉 | 50005(token失效) | ERR_CHANNEL_UNAUTHORIZED |
| SMTP | 554(被拒信) | ERR_CONTENT_REJECTED |
核心优势
- 上层业务仅依赖
Result.success或Result.error_code - 新增通道(如飞书)只需实现
MessageChannel子类,零侵入主流程
第五章:完整可运行的自动消息系统参考实现(含压测与监控指标)
系统架构概览
本实现采用轻量级微服务架构:前端通过 WebSocket 接入,后端由 Go 编写的 msg-gateway 服务统一接收请求,经 Kafka 消息队列解耦,由 Python 编写的 msg-worker 消费并执行模板渲染、渠道分发(短信/邮件/企业微信)、失败重试等逻辑。所有服务容器化部署于 Kubernetes v1.28 集群,使用 StatefulSet 管理 Kafka,Deployment 管理网关与工作节点。
核心代码片段(Go 网关主逻辑)
func handleSend(w http.ResponseWriter, r *http.Request) {
var req struct {
To string `json:"to"`
TemplateID string `json:"template_id"`
Context map[string]string `json:"context"`
}
json.NewDecoder(r.Body).Decode(&req)
msg := &kafka.Message{
Value: []byte(fmt.Sprintf(`{"to":"%s","template_id":"%s","context":%s,"ts":%d}`,
req.To, req.TemplateID, toJSON(req.Context), time.Now().UnixMilli())),
TopicPartition: kafka.TopicPartition{Topic: &topicName, Partition: kafka.PartitionAny},
}
producer.Produce(msg, nil)
w.WriteHeader(http.StatusAccepted)
}
压测配置与结果(wrk + 自定义 Lua 脚本)
使用 wrk -t4 -c500 -d60s --script=send.lua http://msg-gw.example.com/v1/send 模拟高并发下发请求。测试环境为 3 节点 Kafka(3 分区 × 2 副本)、2 实例 msg-gateway(8C16G)、4 实例 msg-worker(4C8G)。实测稳定吞吐达 12,840 msg/s,P99 延迟 217ms,Kafka 端积压峰值
关键监控指标看板(Prometheus + Grafana)
| 指标名称 | 数据源 | 报警阈值 | 采集频率 |
|---|---|---|---|
| gateway_http_request_total | Prometheus HTTP 指标 | 5xx 错误率 > 0.5% | 15s |
| kafka_topic_partition_lag | Kafka Exporter | lag > 10,000 | 30s |
| worker_task_duration_seconds | 自定义 worker metrics | P95 > 3s | 20s |
| redis_queue_length | Redis Exporter | > 50,000 | 10s |
失败消息闭环处理机制
当 msg-worker 渲染模板失败或渠道 API 返回非 2xx 响应时,消息被写入 Redis Sorted Set,按 retry_at 时间戳排序;独立的 retry-scheduler 服务每 5 秒扫描到期条目,重新投递至 Kafka 的 retry-topic(单独 3 分区),最多重试 5 次,第 5 次失败后转入 dlq-topic 并触发企业微信告警。
容器健康探针配置示例
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["sh", "-c", "curl -sf http://localhost:8080/readyz && kafka-topics.sh --bootstrap-server kafka:9092 --list | grep -q msg"]
initialDelaySeconds: 20
periodSeconds: 5
Mermaid 流程图:消息全链路追踪
flowchart LR
A[HTTP POST /v1/send] --> B[msg-gateway]
B --> C[Kafka topic: main]
C --> D[msg-worker-1]
C --> E[msg-worker-2]
C --> F[msg-worker-3]
D --> G{渲染成功?}
E --> G
F --> G
G -->|是| H[调用短信/邮件API]
G -->|否| I[写入 retry-set]
H --> J{渠道返回2xx?}
J -->|是| K[记录 success_log]
J -->|否| I
I --> L[retry-scheduler 扫描]
L --> C
日志结构化规范(JSON 格式输出)
所有服务强制输出结构化日志,包含 trace_id(OpenTelemetry 生成)、service_name、event_type(如 “template_render_start”)、duration_ms、status_code、error_message(若存在),经 Filebeat 收集至 Loki,支持按 trace_id 全链路关联查询。
配置热加载能力
msg-gateway 和 msg-worker 均集成 Viper 库,监听 Consul KV 变更事件;模板内容、渠道密钥、重试策略等参数修改后 2 秒内生效,无需重启服务。Consul 中路径 /config/msg-system/worker/smtp_timeout 更新为 15 后,worker 自动将 SMTP 连接超时从 10s 切换至 15s。
生产就绪检查清单
- [x] Kafka ACL 权限隔离(gateway 只有 produce 权限,worker 仅有 consume 权限)
- [x] 所有外部依赖(Redis、SMTP、短信网关)配置熔断器(Hystrix-go)
- [x] 每个 msg-worker 实例限制最大并发任务数为 20(避免线程耗尽)
- [x] Kafka consumer group 设置
session.timeout.ms=45000,heartbeat.interval.ms=15000 - [x] Prometheus metrics 端点暴露
/metrics,含自定义指标msg_worker_processed_total和msg_gateway_rejected_total
