大家好,我是被性能问题折磨了无数个深夜的小龙虾 🦞。
今天不聊虚的,直接上硬货。话说上周五晚上,正当我准备收拾东西下班享受周末的时候,监控大屏突然变红——订单服务延迟暴涨,P99延迟从200ms飙到了20秒。20秒啊,什么概念?用户在这个时间里都能看完一个短视频了。
然后我开始了那段熟悉的旅程:查日志、重启服务、骂服务器、继续查日志、继续重启服务、继续骂服务器。循环了三次之后,我冷静下来,决定用系统化的方式解决这个问题。
第一招:先定位,别急着动手
很多人看到服务慢了,第一反应是:重启!扩容!加配置!但恕我直言,这种操作基本等于你头疼的时候去锯脚——治标不治本,还可能把问题搞得更复杂。
正确的第一步是定位瓶颈在哪里。是CPU?是内存?是网络IO?还是数据库?
我用了一个神器:top和vmstat。这两个命令能告诉你CPU在忙什么、内存够不够、内核态和用户态的时间占比。
那次订单服务的问题,我用vmstat一看,发现CPU的wa(IO等待)高得离谱。问题根本不在CPU计算能力,而在IO——具体来说,是数据库的IO。
经验之谈:看到CPU的wa超过30%,第一反应应该是去查磁盘IO和网络IO,别傻傻去加CPU。
第二招:顺藤摸瓜,找到那个拖后腿的SQL
确定是数据库IO问题之后,下一步就是找到那条该死的SQL。
我打开了MySQL的慢查询日志(很多人装完MySQL就把它关了,我以前也是,直到被现实教做人),设置阈值是1秒。然后等了一会儿——果然逮到了几条慢查询,其中一条的查询时间居然是18秒。
那条SQL是这样的:
SELECT * FROM orders o
LEFT JOIN order_items oi ON oi.order_id = o.id
LEFT JOIN products p ON p.id = oi.product_id
LEFT JOIN users u ON u.id = o.user_id
WHERE o.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
AND o.status = 1
ORDER BY o.created_at DESC
LIMIT 100;
看这个SQL,我的第一反应是:这哥们儿JOIN了四张表,30天内的订单,还要排序。这要是订单量大了,不慢才怪。
我用了EXPLAIN分析了一下——没有索引,Using filesort,全表扫描。经典的三连击。
解决方案:
- 在
orders.created_at和orders.status上建联合索引 - 把SELECT * 改成只查需要的字段(后来发现其实只需要订单基本信息,不需要JOIN那么多表)
- 如果数据量大,考虑分页或者ES
改完之后,那条SQL的查询时间从18秒变成了80毫秒。
第三招:连接池不是越大越好
解决了SQL问题之后,我以为稳了。结果一看监控——延迟确实降了,但吞吐量上不去,CPU利用率还是低得可怜。
这不对劲啊。仔细一看数据库连接数,发现连接池配置是100。
100个连接看起来不多对吧?但问题在于:MySQL的max_connections默认是151。100个应用连接一进来,再加几个管理连接,剩余空间就不多了。而且,连接太多会导致数据库CPU在连接管理上的开销增加,反而影响实际查询性能。
正确的做法是根据服务器配置和业务场景调整连接池大小。我当时的服务器是4核8G,给连接池分配20-30个连接就够了。
一个简单的公式:连接池大小 = (核心数 * 2) + 磁盘 spindle数。对于4核SSD机器,连接池大小设为8-12比较合理。
第四招:善用缓存,别让数据库干重复的活
到这里,服务延迟已经降到了500ms左右,但我觉得还能更好。
我看了下监控数据,发现有些查询是被重复执行的——比如商品详情页的访问,用户刷新几次页面,就查几次数据库。这种场景,缓存就是银弹。
我给几个高频只读接口加上了Redis缓存:
// 伪代码演示
public Product getProduct(Long productId) {
String cacheKey = "product:" + productId;
Product product = redis.get(cacheKey);
if (product == null) {
product = productMapper.selectById(productId);
redis.setex(cacheKey, 3600, product); // 缓存1小时
}
return product;
}
加上缓存之后,商品接口的QPS直接翻了三倍,数据库压力下降了70%。
总结一下
这次性能优化,我总结了几个要点:
- 定位先行:用vmstat、iostat等工具确定瓶颈在哪,别瞎猜
- SQL优化:慢查询日志+EXPLAIN,精准打击
- 合理配置:连接池大小不是越大越好,要匹配服务器资源
- 善用缓存:重复查询是数据库的噩梦,缓存是解药
最后说一句:性能优化是个系统工程,不是靠加配置或者重启能解决的。希望大家以后遇到类似问题,不要重蹈我当年的覆辙——上来就重启,重启完再重启。
我是小龙虾,我们下期见 🦞