🔥 为什么你的API总是那么慢?后端性能优化避坑指南
事情是这样的。上线前压测一切完美,一到生产环境就开始抽风。我见过太多程序员(包括我自己)在这个坑里反复横跳。今天就把后端性能优化那些事儿说个明白,不整虚的。
一、先别急着写代码,把N+1query干掉再说
这大概是后端开发者最容易踩的坑,没有之一。什么意思呢?就是你循环查数据库,看起来每条单独查都挺快,但1000条数据就是1000次数据库往返,延迟直接爆炸。
一个用户列表接口,需要展示每个用户的部门名称。初级写法:SELECT * FROM users然后循环里查部门。优雅写法:JOIN一下,一次查询搞定。性能差距10倍起步。
// 烂代码示范(别学)
users.forEach(user => {
const dept = await db.query(`SELECT name FROM departments WHERE id = ${user.dept_id}`);
user.deptName = dept.name;
});
// 正确姿势
const users = await db.query(`
SELECT u.*, d.name as dept_name
FROM users u
LEFT JOIN departments d ON u.dept_id = d.id
`);
二、缓存用不对,性能白费劲
很多人听到"加缓存",上来就Redis一顿怼。结果缓存穿透、缓存雪崩、缓存不一致,一个接一个来。缓存是好东西,但要用对场景。
缓存穿透:数据不存在也查,每次都打到DB。解决方案:布隆过滤器或者缓存空值。
缓存雪崩:大量缓存同时过期,请求全打到DB。解决方案:过期时间加随机值,或者用永不过期的版本+主动刷新。
缓存不一致:这个最恶心。解决方案就一句话:数据库是主数据源,缓存只是副本。更新时先删缓存还是先更新数据库?都行,但要保证最终一致性。
三、连接池配不好,请求排队等到老
数据库连接池的大小是个技术活儿。设小了并发上不去,设大了资源浪费还可能OOM。公式来了:
连接池大小 = (核心数 * 2) + 有效磁盘 spindles
这个公式不是绝对的,但起点基本对了。更精确的需要结合压测来调。
另外,连接池的maxLifetime要设置合理。太短频繁建立断开,太长可能碰到MySQL的wait_timeout被服务端踢掉。一般30分钟是个不错的起点。
四、同步阻塞是性能杀手
Node.js里一个async/await看着挺美好,但如果你在循环里串行等待,那就是在慢性自杀:
// 串行等待 - 慢
for (const url of urls) {
const data = await fetch(url);
results.push(data);
}
// 并行Promise - 快
const results = await Promise.all(urls.map(url => fetch(url)));
这两种写法结果一样,性能差距可能是10倍。串行等待的情况下,100个URL每个1秒,就是100秒。并行情况下,1秒搞定。
五、日志打太多,性能悄悄溜
console.log在生产环境是个隐形的性能杀手。同步写文件就不说了,就说异步的:大量日志写入也会抢占IO资源。
建议:生产环境日志级别调到WARN以上,使用日志框架的缓冲写入机制。另外,别在日志里打印大对象,对象序列化很贵的。
六、压缩和CDN,别小看这两个
response压缩(gzip/brotli)能省30%-70%的流量,传输时间直接降下来。CDN不只是加速静态资源,API响应如果可缓存(GET请求、接口结果不频繁变化),一样可以上CDN。
一个小细节:启用压缩后,CPU会略高,但换来的是显著降低的网络延迟和带宽费用。对于IO密集型服务,这笔账怎么算都划算。
七、分页不分,迟早出事
这种代码我见过不止一次:
// 一次性把十万数据全查出来
const allUsers = await db.query('SELECT * FROM users'); // 别学!
数据量小的时候没事,生产环境数据量上来,内存爆给你看。正确姿势:游标分页或者Offset分页加上max_id限制,避免深度分页带来的性能问题。
写在最后
性能优化是个系统活儿,不是堆配置、也不是疯狂加机器。先测量、再定位瓶颈、针对性优化。上来就加机器的是土豪做法,上来就加缓存的是愣头青。
记住:代码写得好,硬件来得早。祝大家的API都快如闪电,线程不卡,GC不烦。🚀