背景痛点:HTTP 轮询为何撑不住 Chatbot 免费客户端
做一款“chatbot免费客户端”最怕什么?不是功能少,而是用户一多就卡成 PPT。传统 HTTP 短轮询方案在浏览器/小程序里随处可见:前端每 500 ms 发一次GET /poll,带着 userId 和 timestamp,后端把 200 或 304 还回去。看起来简单,实际埋了四颗雷:
连接数爆炸
浏览器默认 Keep-Alive,但每轮询一次仍占一个文件描述符。1 w 在线用户 ≈ 1 w 连接,Linux 默认 1024 单进程上限瞬间打满。消息延迟高
轮询间隔 500 ms,平均延迟 250 ms;网络抖动时再重试,秒级延迟是常态。空转浪费
90% 响应是 304 无消息,但 TLS 握手、TCP 慢启动一样不少。凌晨三点,服务器 CPU 30% 在“空转”加密空气。幂等噩梦
轮询带 lastMsgId,客户端超时重试,服务端若没做好幂等,同一条消息重复推送,用户侧“鬼打墙”式刷屏。
一句话:HTTP 轮询是“伪实时”,撑不起免费客户端“零成本、高并发”的野心。
下面给出一条可落地的低成本改造路径——WebSocket + 消息队列异步架构,单机 4C8G 压到 20 w 在线不撇叉。
架构设计:WebSocket 为什么赢
先把三条主流方案拉表格对比,实测环境:阿里云 ecs.c7 4C8G,CentOS 3.10,内网延迟 < 0.2 ms,Payload 统一 256 B 文本。
| 方案 | 单核 QPS | 内存/连接 | 延迟 P99 | 断线感知 | 备注 |
|---|---|---|---|---|---|
| HTTP 短轮询 | 1.2 k | 8 KB | 500 ms | 无 | 实测 1 w 并发时 CPU 65% 空转 |
| 长轮询 | 4 k | 12 KB | 250 ms | 30 s 超时 | Nginx 需要 proxy_timeout |
| gRPC 双向流 | 18 k | 15 KB | 20 ms | 应用层心跳 | 需要 HTTP/2 网关,小程序里用不了 |
| WebSocket | 22 k | 10 KB | 18 ms | TCP 层 45 s 踢掉 | 浏览器、小程序原生支持 |
选型结论:
- 免费客户端要“打开浏览器就能聊”,WebSocket 天生跨端,赢。
- 延迟与 gRPC 同档,但免去 ALPN 协商,TLS 握手少一次 RTT。
- 内存占用最低,单机 20 w 连接 ≈ 2 GB,留给业务代码足够。
整体架构图(文字版):
+------------+ WebSocket +------------+ | Browser |<------------------->| Gateway | +------------+ +------------+ | | NATS/JetStream v +------------------+ | Chatbot Worker | +------------------+Gateway 只做 I/O 转发,无状态;Worker 订阅 subject=chat.{userId},真正调用 LLM。两者通过消息队列解耦,扩容互不影响。
核心实现:连接池 + 心跳 + 压缩
1. 连接池管理(Go 版)
用 epoll ET 模式把 20 w 连接塞进一个 goroutine,核心代码:
// pool.go package main import ( "log" "net" "sync" "time" "github.com/xtaci/websocket" ) type Pool struct mu sync.RWMutex conns map[string]*Conn // key=uid } type Conn struct { ws *websocket.Conn lastPong time.Time } func (p *Pool) Add(uid string, ws *websocket.Conn) { p.mu.Lock() p.conns[uid] = &Conn{ws: ws, lastPong: time.Now()} p.mu.Unlock() go p.heartbeat(uid) } func (p *Pool) heartbeat(uid string) { tick := time.NewTicker(30 * time.Second) defer tick.Stop() for range tick.C { p.mu.RLock() c, ok := p.conns[uid] p.mu.RUnlock() if !ok { return } if err := c.ws.WriteMessage(websocket.Ping, nil); err != nil { p.Del(uid) return } } }- 边缘触发 + 非阻塞写,失败立即踢掉,防止半开连接占 fd。
- 心跳包用 Ping/Pong,TCP 层 45 s 防火墙回收,应用层 30 s 自检,双保险。
2. 消息压缩(Python 版)
浏览器到网关走 JSON 虽然调试爽,但 256 B 文本能压到 60 B。用 Protocol Buffers 定义:
syntax = "proto3"; message ChatMsg { string uid = 1; string text = 2; int64 ts = 3; }Python 端快速打包/解包:
# compress.py import chat_pb2, gzip, time def pack(uid, text): msg = chat_pb2.ChatMsg(uid=uid, text=text, ts=int(time.time()*1000)) return gzip.compress(msg.SerializeToString()) def unpack(data): msg = chat_pb2.ChatMsg() msg.ParseFromString(gzip.decompress(data)) return msg实测 1 w 条消息:
JSON 平均 267 B → PB+gzip 62 B,带宽直接省 77%,延迟抖动下降 40%。
3. 错误处理与日志
Go 侧用zerolog写本地文件,同时采样 1% 到 SLS;Python 侧structlog+concurrent-log-handler按 100 MB 轮转。
所有WriteMessage失败都带uid、error、goroutine id,方便秒级定位“谁掉线”。
性能优化:压测、内存、断线重连
1. 压测方法论
wrk 不是只能测 HTTP,配合websocket-bench插件即可:
wrk -t4 -c4000 -d30s -s ws.lua ws://gateway:8080/wsLua 脚本里完成握手后,每 2 s 发一条 PB 消息,记录 P99 延迟。
调优后发现:
- 打开
TCP_NODELAY可把 40 ms 延迟降到 18 ms; - 单进程
ulimit -n调到 1 024 000,再用SO_REUSEPORT8 监听,CPU 打满 4 核,QPS 22 k。
2. 内存泄漏检测
Go 内置 pprof,Gateway 加一行:
import _ "net/http/pprof" go func() { log.Println(http.ListenAndServe("0.0.0.0:6060", nil)) }()压测 12 h,发现bufio.Writer持续增长,定位到忘记Release的gzip.Writer池,修复后 RSS 稳定在 2.1 GB。
3. 断线重连幂等
客户端重连带lastMsgId,Gateway 把离线 5 min 内的消息用 JetStream 持久化,重放时按msgId去重。
幂等键 =userId:msgId,写入 Redis Set,TTL 10 min,内存占用 < 200 MB。
避坑指南:生产环境 3 大坑
Nginx 反向代理
默认proxy_read_timeout 60 s,WebSocket 心跳 30 s 仍会被断。
解决:proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 180 s;TLS 握手优化
免费证书链 4 KB,每次握手 2 RTT。开启 TLS 1.3 + 0-RTT,可把首包延迟再降 30 ms;但注意 0-RTT 有重放风险,Chatbot 只读接口可开,写接口关闭。消息顺序
NATS 单 subject 保证分区顺序,但多 Gateway 实例下,客户端重连可能换实例。
解决:用subject=chat.{userId}.{shard},shard=uid 末位,保证同一用户永远落同一队列,顺序不乱。
开放讨论
跨机房部署时,JetStream 的 RAFT 复制写放大 3 倍,延迟从 18 ms 涨到 120 ms。
如果让你设计“跨机房消息同步”,你会选:
- 强一致:同步双写,延迟高;
- 最终一致:机房内写完即回,异步复制,可能丢消息;
- 混合方案:重要消息同步,普通消息异步?
欢迎留言聊聊你的做法。
全文代码与压测脚本已打包,想直接跑通的小伙伴可戳这里动手:从0打造个人豆包实时通话AI
我按实验步骤 30 分钟就把 WebSocket 网关跑起来,小白也能顺利体验。祝你早日上线自己的高性能 Chatbot 免费客户端!