技术探索与实践踩坑记录
技术探索与实践踩坑记录:一次服务扩容中的“血泪史”
背景介绍
去年,我所在的互联网公司正在快速扩张业务,我们负责的某个核心服务也逐渐从百万级日调用量飙升到了千万级别。原本架构是部署在Kubernetes(K8s)上的微服务架构,后端用的是Go语言写的REST API服务,数据库使用MySQL + Redis。
随着QPS持续上涨,我们发现单个Pod在高并发下开始出现响应延迟上升、CPU打满甚至偶发OOM(内存溢出)的情况。为了保证系统稳定性,我们需要对服务进行扩容和性能优化。但事情远没想象中那么简单。
这篇文章就来聊聊我们在扩容过程中踩过的那些坑,以及最终是怎么解决这些问题的。希望通过这次分享,能帮助到正在做类似工作的同学少走些弯路。
问题描述:扩了容,反而更慢了?
最初我们的想法很简单:既然QPS上去了,那就在K8s里增加副本数呗。于是我们通过Helm Chart将Deployment里的replicas参数从3调成了6,以为这就可以一劳永逸地解决问题。
结果刚上线不到一个小时,我们就收到了告警:整个服务的P99延迟反而显著上升了,有些接口甚至翻了几倍!更诡异的是,Prometheus监控显示每个Pod的负载其实并不高,但整体吞吐量却下降了……
这到底怎么回事?明明多加了实例,按理说服务能力应该增强才对啊!
解决方案:定位问题,逐层排查
我们先是怀疑是不是数据库扛不住压力了。于是先查了MySQL的慢查询日志和连接池配置。结果发现:
- MySQL连接池配置默认为
maxOpenConns=50,当服务扩容到6个Pod后,数据库的连接请求瞬间暴涨。 - 某些Redis命令用了
KEYS *这样的低效命令,导致Redis响应变慢,从而拖慢了服务的整体表现。 - Go程序中有部分函数没有做goroutine限制,在高并发下产生了大量协程竞争资源的问题。
发现问题后,我们逐步做了以下优化:
调整数据库连接池参数
将每个Pod的最大连接数适当放大,并且在K8s层面做了最大Pod数量的控制,避免连接爆炸。优化Redis访问逻辑
替换掉所有使用KEYS *的地方,改用更高效的方法(如Scan)或者引入缓存预热机制。限制Goroutine并发数
在一些异步任务处理中加入了buffered channel控制并发度,避免资源耗尽。调整HPA(Horizontal Pod Autoscaler)策略
改为基于指标自动伸缩,而不是静态固定副本数。设置合理的阈值,防止资源浪费或过度扩容。
代码实践:关键配置和优化示例
1. 数据库连接池优化:
sqlDB, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
sqlDB.SetMaxOpenConns(200) // 根据Pod数量合理调整
sqlDB.SetMaxIdleConns(50)
sqlDB.SetConnMaxLifetime(time.Minute * 5)
2. 控制Goroutine并发数:
const maxWorkers = 5
workerChan := make(chan struct{}, maxWorkers)
for _, task := range tasks {
workerChan <- struct{}{}
go func(t Task) {
defer func() { <-workerChan }()
processTask(t)
}(task)
}
3. HPA配置优化:
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: api-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 当CPU平均使用率达到70%时扩容

踩坑经验:那些意想不到的“小细节”
在整个优化过程中,我们踩了不少坑,下面是我印象最深的几点:
盲目扩容带来的连锁反应
- 扩容之后不仅没带来更好的性能,反而因为数据库连接不足导致整体性能恶化。
- 建议:扩容前要评估后端依赖服务的承载能力,不能只看当前服务的CPU/内存。
Goroutine泄露和并发控制失效
- 之前有些同事为了图方便,在并发任务中没有做任何控制,直接起一堆goroutine,结果压测一下,直接OOM。
- 建议:任何异步任务都要有并发控制机制,建议使用
sync.WaitGroup+channel或者第三方库如ants来管理goroutine池。
Redis命令选型错误
- 使用了
KEYS *这种命令去批量删除key,结果Redis单线程卡住,整个服务阻塞。 - 建议:生产环境禁用危险命令;需要批量操作时用Lua脚本或者SCAN代替KEYS。
- 使用了
HPA阈值不合理
- 刚开始的时候我们设定了太高的CPU利用率触发扩容,结果总是来不及扩容。
- 后面结合历史数据调整到70%,并增加了预判性的定时扩容策略(比如活动前手动加Pod),效果更好。
效果总结:稳定+高效=双赢
优化完成后,整个服务的表现有了明显提升:
- QPS从原来的800左右上升到了1500+;
- P99延迟从800ms降低到了200ms以内;
- MySQL连接数平稳,不再频繁报警;
- Redis响应时间恢复正常,没有出现卡顿;
- 自动扩缩容也能根据流量波动及时调整资源,节省了不少云资源成本。
最重要的是,系统比以前更稳了,运维的压力也小了很多。
经验分享:写给还在战斗的一线开发者们
如果你也在做服务扩容、性能优化之类的工作,这里有一些我觉得特别值得分享的经验:
技术选型要考虑上下游生态
- 不只是自己写得好,还要看你的服务依赖的服务是否能撑得住。比如MySQL连接池、Redis容量、消息队列堆积情况等。
不要迷信“复制粘贴”的最佳实践
- 网上有各种各样的优化文章,但实际应用还是要结合自己的业务场景来做权衡。别人家的最佳实践放到你这儿可能就是灾难。
学会看监控图胜过写十篇技术文档
- Prometheus + Grafana真的是神器,特别是在排查问题的时候,图形化展示让你一眼就能看出哪里卡住了。
自动化不是万能的,人工介入也很重要
- HPA很好用,但在大促或突发流量面前,还是得有人工干预兜底,防止自动扩容赶不上节奏。
保持警惕,永远做好预案
- 性能优化是个长期过程,没有银弹。哪怕今天搞好了,明天也可能因为新功能上线又出问题。
写在最后:技术成长,从来不是一条坦途
回过头来看,那次扩容事件虽然折腾了一周时间,但也让我们整个团队的技术视野和协作流程都上了一个台阶。特别是让我意识到,技术不只是写好代码,更重要的是如何在复杂系统中找到平衡点。
希望这篇记录能给你带来一些启发,至少让你知道:即便是在一线大厂,也一样会遇到看似“很基础”的问题,也一样会踩坑。关键是,我们怎么从中学习、成长,把一个个坑变成通往高手的阶梯。
共勉 😄
作者:@一名热爱Go的coze开发者
工作地点:某头部互联网大厂 · 北京
擅长方向:微服务架构、Kubernetes运维、高并发系统设计
GitHub:github.com/coze-developer
技术博客:blog.coze.dev

评论 0