从凌晨三点的崩溃到系统稳如老狗:我的高并发实战血泪史
上周五凌晨三点,我盯着屏幕上疯狂刷屏的 Too many open files 错误日志,一边啃着冷掉的披萨一边想:这破系统是不是跟我有仇?刚入职新公司两个月,就被塞进一个号称“百万级并发”的核心项目,而我这个 Rust 菜鸟连 Arc<Mutex<T>> 都还没玩明白,就得扛起 Go 服务端重构的大旗。
说起来有点惭愧——虽然简历上写着“精通高并发”,但其实之前做的项目顶多也就几万 QPS。这次接手的是我们游戏后台的实时匹配服务,双11活动一开,玩家暴增,旧架构直接原地爆炸。运维大哥在群里@我:“兄弟,你再不搞定,明天产品经理就要拿咖啡泼你了。”
为什么又双叒叕是连接池炸了?
旧系统用的是 Go + Gin + Redis + MySQL 经典四件套,乍看没问题,但细看全是坑。比如:
- Redis 连接池大小硬编码成 50,高峰期直接打满
- MySQL 没有读写分离,所有请求都压到主库
- 接口没做限流熔断,一个恶意刷接口的脚本就能让整个服务雪崩
最离谱的是,他们居然把用户 Embedding 向量(用于智能匹配)直接序列化存到 MySQL 的 TEXT 字段里!每次匹配都要全表扫描 + 反序列化,CPU 直接飙到 90%+。我看了眼代码提交记录,居然是去年实习生写的……好家伙,人走茶凉,锅留千年。
Embedding 在这里不是 AI 那个嵌入向量吗?怎么跑游戏匹配里去了?
是的!我们用了轻量级的用户行为 Embedding(维度 128),配合 Faiss 做近似最近邻搜索,实现“兴趣相近玩家自动组队”。听起来很 fancy,但落地时直接塞数据库,纯属自残。
Windsurf:被逼出来的流量削峰利器
眼看 deadline 逼近(老板说下周必须上线),我和后端组长一合计:得搞个缓冲层。这时候我想起了最近研究的一个开源项目——Windsurf(别搜了,这名字是我瞎编的,真实项目叫什么不重要,关键是思路)。
它的核心思想很简单:用内存队列暂存突发请求,异步批量处理。类似 Kafka 的角色,但更轻量,专为游戏场景设计——毕竟玩家匹配这种操作,延迟容忍度可以到几百毫秒。
于是我们快速搭了个原型:
// 简化版 Windsurf 缓冲队列
type MatchQueue struct {
mu sync.RWMutex
buffer []*MatchRequest
maxSize int
}
func (q *MatchQueue) Push(req *MatchRequest) error {
q.mu.Lock()
defer q.mu.Unlock()
if len(q.buffer) >= q.maxSize {
return errors.New("queue full")
}
q.buffer = append(q.buffer, req)
return nil
}
func (q *MatchQueue) PopBatch(size int) []*MatchRequest {
q.mu.Lock()
defer q.mu.Unlock()
if len(q.buffer) == 0 {
return nil
}
n := min(size, len(q.buffer))
batch := q.buffer[:n]
q.buffer = q.buffer[n:]
return batch
}
然后启一个 goroutine 定时消费:
func (s *Server) startConsumer() {
ticker := time.NewTicker(100 * time.Millisecond) // 每100ms处理一批
for range ticker.C {
batch := s.queue.PopBatch(50)
if len(batch) == 0 {
continue
}
// 批量查询 Embedding 并匹配
go s.processBatch(batch)
}
}
效果立竿见影:QPS 从 3w 稳定撑到 12w,MySQL CPU 从 95% 降到 40%,而且因为批量处理,Embedding 查询效率提升近 5 倍。
Embedding 存储重构:别再用 MySQL 当垃圾桶!
既然提到了 Embedding,那就说说我们怎么把它从 MySQL 里“解救”出来的。
最初方案是迁到 Redis,但 Redis 的 HSET 存 128 维 float 向量太浪费内存(每个 field 都有 key 开销)。后来我们试了 Redis 的 Vector Search 模块(RediSearch),支持 HNSW 索引,查询快得飞起。
但问题来了:RediSearch 社区版不稳定,生产环境不敢上。最后咬牙上了 Milvus Lite(轻量级向量数据库),单机部署,API 简洁,完美适配我们的场景。
数据同步也做了优化:
- 用户行为更新时,异步推 Embedding 到 Milvus
- 匹配服务只读 Milvus,彻底和 MySQL 解耦
性能对比(单机测试):
| 存储方案 | 单次查询延迟 | 内存占用 | 支持 ANN |
|---|---|---|---|
| MySQL TEXT | ~85ms | 中 | ❌ |
| Redis Hash | ~12ms | 高 | ❌ |
| Milvus Lite | ~3ms | 低 | ✅ |
看到这个数据,我当场给 Milvus 点了个 star——虽然它文档写得像天书,但真香。
Go 并发陷阱:你以为的 goroutine 不是免费的!
别看 Go 的 goroutine 轻量,乱开一样死给你看。初期我图省事,每个匹配请求都开一个 goroutine 去查 Embedding:
// 千万别这么写!
for _, req := range requests {
go func(r *MatchRequest) {
result := queryEmbedding(r.UserID)
sendResult(result)
}(req)
}
结果高并发下一堆 goroutine 卡在 I/O,内存爆到 8GB,GC 停顿长达 2 秒。运维差点把我拉黑。
后来改用 worker pool 模式,控制并发数:
type WorkerPool struct {
jobs chan Job
workers int
}
func (wp *WorkerPool) Start() {
for i := 0; i < wp.workers; i++ {
go func() {
for job := range wp.jobs {
job.Process()
}
}()
}
}
把 worker 数限制在 CPU 核数 × 2(经验值),系统瞬间稳如老狗。顺便还加了 context 超时控制,防止单个请求拖垮整个池子。
运维视角:监控比代码更重要
光写好代码不够,你得知道线上到底发生了什么。我们接入了 Prometheus + Grafana,关键指标包括:
- 队列积压长度(Windsurf buffer size)
- Embedding 查询 P99 延迟
- Goroutine 数量
- MySQL 连接数 / 慢查询
有一次半夜报警,队列积压突增。我爬起来一看,原来是 Milvus 宕机了,但匹配服务没做降级。赶紧加了个 本地缓存兜底策略:当向量服务不可用时,随机匹配(总比卡住强)。
func (s *Server) getMatchCandidates(userID string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
candidates, err := s.milvusClient.Search(ctx, userID)
if err != nil {
// 降级:随机返回在线玩家
return s.getRandomOnlinePlayers(10), nil
}
return candidates, nil
}
产品经理知道后居然夸我“用户体验意识强”……呵,还不是被线上事故逼的。
为什么我又开始看 Rust?
说实话,这次重构让我对 Go 又爱又恨。爱它的生态和开发效率,恨它在内存控制上的“模糊地带”。特别是 Embedding 这种数值计算密集的场景,Go 的 GC 和逃逸分析总让我提心吊胆。
所以最近下班(如果有的话)就啃 Rust。no_std、零拷贝、编译期内存安全……简直是对高并发系统的终极诱惑。虽然现在项目还是 Go 主力,但我偷偷用 Rust 写了个 Embedding 预处理工具,性能比 Python 版快 8 倍,内存占用只有 1/5。
也许下个项目,我能说服老大试试 Rust + WebAssembly 做匹配引擎?至少先从工具链开始渗透(笑)。
最后几句大实话
高并发从来不是靠某个银弹技术解决的,而是无数个细节堆出来的稳定性:
- 别把数据库当万能桶
- 异步和缓冲是削峰填谷的神技
- 监控告警是你睡觉的保障
- 降级兜底不是丢脸,是专业
我现在终于能在凌晨两点前回家了(虽然偶尔还得加班)。上周团建,运维大哥敬我一杯:“兄弟,你那套 Windsurf 架构,我们准备推广到其他服务。” 我笑笑,心里想:这哪是什么架构,分明是被 bug 逼出来的求生本能。
对了,如果你也在被高并发折磨——别慌,你不是一个人。咱们程序员,不就是在一次次崩溃中,把系统从“屎山”炼成“钢铁侠”吗?
(完)
作者注:本文提到的 Windsurf 为内部代号,实际参考了 Disruptor、Kafka Producer Buffer 等设计思想。Embedding 方案已脱敏,具体技术选型请根据业务规模评估。

评论 0