1. 项目概述:现代消息通知系统的核心挑战与架构选型
消息通知系统作为互联网产品的标配功能,已经从简单的"小红点"演变为支撑用户粘性的关键基础设施。我在多个千万级DAU项目中负责消息系统架构时,最深刻的体会是:看似简单的消息推送背后,隐藏着高并发、低延迟、数据一致性这三座大山。
传统轮询方案在移动互联网早期尚可应付,但当用户量突破百万级时,HTTP长轮询带来的服务端压力呈指数级增长。某次大促期间,我们的系统曾因每秒百万级的轮询请求导致Nginx集群崩溃。这次事故促使我们全面转向WebSocket+消息队列的架构,核心指标提升惊人:
- 服务端连接数下降87%
- 消息延迟从秒级降至毫秒级
- 服务器成本降低65%
当前主流消息系统通常需要解决以下核心问题:
- 实时推送:确保消息在产生后立即触达用户终端
- 未读补发:处理网络闪断、客户端离线等异常场景
- 已读回写:保证已读状态在多设备间的同步一致性
- 历史消息:支持用户查看过往通知记录
- 削峰填谷:应对突发流量冲击
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈深度解析与选型依据
2.1 WebSocket:实时通信的基石
与HTTP轮询相比,WebSocket的全双工特性使其成为实时通信的不二之选。但在实际部署中,我们踩过几个关键坑:
连接保持难题
- 移动网络下的NAT超时:运营商通常设置5分钟左右的NAT表超时,需要通过心跳包维持连接。我们最终采用55秒间隔的心跳策略:
python复制# 服务端心跳检测实现
async def check_heartbeat(websocket):
while True:
try:
await asyncio.wait_for(websocket.ping(), timeout=10)
await asyncio.sleep(55) # 55秒心跳间隔
except (asyncio.TimeoutError, ConnectionResetError):
remove_connection(websocket)
break
连接数扩展方案
单机WebSocket连接数受限于内存和文件描述符。我们通过以下优化支撑百万级连接:
- 使用epoll替代select(Go的net/http已默认支持)
- 调整内核参数:
bash复制# Linux系统调优
sysctl -w fs.file-max=1000000
sysctl -w net.ipv4.tcp_max_tw_buckets=2000000
2.2 Kafka:消息流转的中枢神经
Kafka的发布-订阅模型完美适配消息系统的解耦需求。但在实际使用中,这些经验值得注意:
分区策略优化
按用户ID哈希分区会导致热点问题。我们改进为双层分区策略:
- 第一层按业务线分区(订单、社交、系统通知等)
- 第二层在消费者端二次哈希
延迟消息处理
Kafka本身不支持延迟消息,我们通过以下方案实现:
java复制// 延迟消息路由逻辑
public void sendDelayMessage(String topic, String key, String message, long delayMs) {
if (delayMs <= 0) {
kafkaTemplate.send(topic, key, message);
} else {
String delayTopic = "delay_" + (System.currentTimeMillis() + delayMs) / 60000; // 按分钟分桶
kafkaTemplate.send(delayTopic, key, new DelayMessageWrapper(topic, message));
}
}
2.3 Redis:实时状态的缓存层
Redis在系统中扮演三个关键角色:
- 在线状态管理:使用Redis Bitmap记录用户连接状态
- 未读消息暂存:Sorted Set存储待推送消息
- 分布式锁:控制已读回写的并发更新
内存优化技巧
- 使用Hash Tag确保相关数据落在同一节点:
{user123}.notifications - ZSet压缩配置:
redis复制CONFIG SET zset-max-ziplist-entries 512
CONFIG SET zset-max-ziplist-value 64
2.4 TiDB:消息数据的终极归宿
传统MySQL分库分表方案在消息系统面临两大痛点:
- 历史数据归档困难
- 跨分片查询性能差
TiDB的分布式特性完美解决这些问题:
- 自动均衡的分片策略
- 原生兼容MySQL协议
- 高效的批量插入:
sql复制-- TiDB批量插入优化
SET tidb_batch_insert = 1;
SET tidb_dml_batch_size = 1000;
INSERT INTO notifications (...) VALUES (...), (...), ...;
COMMIT;
3. 核心业务流程实现细节
3.1 实时推送流程

(注:此处应为专业流程图,描述从消息产生到客户端接收的完整路径)
关键优化点:
- 消息压缩:对JSON payload进行zstd压缩,带宽减少60%
- 优先级队列:系统通知优先于营销消息
- 推送确认机制:
go复制type PushAck struct {
MessageID string `json:"mid"`
Timestamp int64 `json:"ts"`
Status string `json:"status"` // "delivered" or "failed"
}
3.2 未读消息补发策略
网络不稳定时的补发逻辑是系统可靠性的关键。我们的分级补发策略包括:
- 即时重试(3秒内):TCP层重连时触发
- 短时补发(3分钟内):通过Redis Sorted Set存储待补发消息
- 长时补发(3天):依赖Kafka消费者offset回溯
补发去重采用BloomFilter优化:
java复制public boolean shouldResend(String messageId) {
String filterKey = "resend_filter:" + LocalDate.now();
if (!redisTemplate.opsForValue().getBit(filterKey, hash(messageId))) {
redisTemplate.opsForValue().setBit(filterKey, hash(messageId), true);
return true;
}
return false;
}
3.3 已读回写一致性保障
多设备已读同步是个典型的分布式一致性问题。我们采用"写主库+广播事件"方案:
- 客户端发送已读请求到API网关
- 网关通过Redis锁保证串行化
- 更新TiDB主记录
- 通过Kafka广播已读事件
- 各连接节点更新本地状态
python复制def mark_as_read(user_id, message_ids):
with redis.lock(f"read_lock:{user_id}", timeout=5):
# 更新主库
db.execute("UPDATE notifications SET status='read' WHERE id IN %s", (message_ids,))
# 发布已读事件
kafka.publish("user_read_events", {
"user_id": user_id,
"message_ids": message_ids,
"timestamp": time.time()
})
4. 性能优化实战记录
4.1 WebSocket集群水平扩展
我们采用Sharding+无状态设计:
- 连接绑定到特定节点通过Cookie实现
- 节点间通信通过Kafka的
websocket_eventstopic - 使用Consul进行服务发现
yaml复制# WebSocket节点配置示例
cluster:
discovery: consul://127.0.0.1:8500
sharding:
enabled: true
strategy: cookie
cookie_name: ws_node
4.2 Kafka消费者组调优
针对消息积压场景的关键参数:
properties复制# 消费者配置优化
fetch.min.bytes=65536
fetch.max.wait.ms=500
max.poll.records=500
session.timeout.ms=30000
heartbeat.interval.ms=10000
4.3 TiDB热点问题处理
消息系统常见的热点问题包括:
- 新消息集中写入
- 未读消息批量查询
我们采用的解决方案:
sql复制-- 表设计添加SHARD_ROW_ID_BITS
CREATE TABLE notifications (
id BIGINT PRIMARY KEY AUTO_RANDOM,
user_id BIGINT,
...
) SHARD_ROW_ID_BITS=4;
5. 生产环境踩坑实录
5.1 消息乱序问题
现象:用户收到消息顺序与发送顺序不一致
根因:Kafka不同分区消费速度差异
解决方案:
- 单分区+批量发送
- 客户端增加序列号校验
java复制// 客户端顺序校验
public void handleMessage(Message msg) {
long expectedSeq = lastReceivedSeq + 1;
if (msg.getSeq() < expectedSeq) {
requestResend(expectedSeq);
} else if (msg.getSeq() > expectedSeq) {
bufferOutOfOrderMsg(msg);
} else {
processMessage(msg);
lastReceivedSeq++;
}
}
5.2 僵尸连接问题
现象:服务端连接数持续增长但实际活跃用户未增加
根因:客户端异常退出未发送关闭帧
解决方案:
- TCP keepalive检测
- 应用层心跳超时
- 主动清理脚本:
bash复制# 检测僵尸连接脚本
netstat -anp | grep ESTABLISHED | grep websocket | awk '{print $7}' | cut -d/ -f1 | xargs -I{} ps -p {} -o etimes= | awk '$1 > 3600 {print $1}' | wc -l
5.3 分布式锁失效
现象:已读状态出现覆盖
根因:Redis锁过期时间设置不合理
解决方案:
- 动态锁续期
- 令牌版本控制
go复制func extendLock(lockKey string, token string, duration time.Duration) bool {
script := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("pexpire", KEYS[1], ARGV[2])
else
return 0
end`
result, _ := redis.Eval(script, []string{lockKey}, token, duration.Milliseconds()).Int()
return result == 1
}
6. 监控与告警体系建设
6.1 核心指标监控
我们使用Prometheus采集的关键指标:
yaml复制# Prometheus监控配置示例
- job_name: 'websocket'
metrics_path: '/metrics'
static_configs:
- targets: ['ws1:9090', 'ws2:9090']
relabel_configs:
- source_labels: [__address__]
target_label: instance
6.2 日志分析架构
采用ELK+Filebeat方案:
code复制Filebeat -> Kafka -> Logstash -> Elasticsearch -> Kibana
关键日志字段:
json复制{
"timestamp": "ISO8601",
"trace_id": "string",
"user_id": "number",
"message_id": "string",
"event_type": "connect|disconnect|push|ack",
"latency_ms": "number",
"error_code": "string"
}
6.3 智能告警规则
分级告警策略示例:
-
P0级(电话告警):
- WebSocket连接成功率 < 99%
- 消息端到端延迟 > 5s
-
P1级(企业微信告警):
- Kafka消费延迟 > 1m
- TiDB查询耗时 > 500ms
-
P2级(邮件告警):
- Redis内存使用 > 80%
- 节点CPU使用率 > 70%
7. 扩展思考与未来方向
在现有架构基础上,我们正在探索几个优化方向:
边缘计算推送
- 将WebSocket网关下沉到CDN边缘节点
- 使用Cloudflare Workers或EdgeRoutine实现
消息协议优化
- 评估改用QUIC协议的可能性
- 测试Avro二进制协议替代JSON
AI驱动的推送策略
- 基于用户行为预测的消息优先级调整
- 智能合并相似通知
python复制# 简单的消息合并算法示例
def merge_notifications(notifications):
merged = {}
for note in notifications:
key = (note['type'], note['sender'])
if key in merged:
merged[key]['count'] += 1
merged[key]['latest_time'] = max(
merged[key]['latest_time'],
note['time']
)
else:
merged[key] = note.copy()
merged[key]['count'] = 1
return list(merged.values())
这套系统经过三年演进,目前支撑着日均30亿条消息的稳定推送。最大的经验就是:消息系统没有银弹,必须根据业务特点持续调优。每次架构升级前,我们都会用真实流量在预发环境进行全链路压测,这也是保证系统稳定性的关键所在。
