性能优化:连接池调优、协程优化、数据库查询优化、响应压缩、CDN加速
性能优化这件事,说实话,我见过太多人把它当成「最后一步」来做了。项目快上线了,压测一跑,发现接口响应时间飙到三秒以上,然后才开始慌。我个人习惯是,在写第一行代码之前,脑子里就要有性能这根弦。尤其是用 Ktor 做后端,Kotlin 协程虽然快,但用不好反而会出问题。
今天咱们就聊聊五个核心方向:连接池、协程、数据库查询、响应压缩、CDN。每个点我都会结合自己踩过的坑来讲。
1. 连接池调优:别让数据库成为瓶颈
连接池这个东西,说白了就是「复用」。每次请求都新建一个数据库连接,那性能肯定崩。我在项目中遇到过,一个简单的查询接口,因为没配连接池,QPS 刚上 200 就报连接超时。
Ktor 配合 Exposed 或 HikariCP 时,连接池的配置很关键。我一般这样配:
val hikariConfig = HikariConfig().apply {
jdbcUrl = "jdbc:postgresql://localhost:5432/mydb"
driverClassName = "org.postgresql.Driver"
username = "user"
password = "pass"
maximumPoolSize = 20
minimumIdle = 5
idleTimeout = 30000
connectionTimeout = 10000
maxLifetime = 1800000
}
val dataSource = HikariDataSource(hikariConfig)
这里有几个参数我特别想强调:
- maximumPoolSize:不是越大越好。我见过有人设成 200,结果数据库 CPU 直接打满。一般建议是 CPU 核心数 × 2 + 1,比如 4 核就设 9 左右。
- connectionTimeout:等待连接的超时时间。设太短容易误报,设太长用户等不起。10 秒是个不错的起点。
- maxLifetime:连接的最大存活时间。我习惯设 30 分钟,避免数据库端主动断开后,客户端还在用旧连接。
2. 协程优化:别滥用 launch
Kotlin 协程在 Ktor 里是天然支持的,但很多人用错了。我记得有一次,一个同事在路由处理里写了十几个 launch,结果接口响应时间从 200ms 涨到了 2s。为什么?因为 launch 是 fire-and-forget,不会等待结果。
正确的做法是:
- 用
async/await做并行调用,而不是串行。 - 控制协程的并发数,别一股脑全开。
看个例子:
suspend fun fetchUserData(userId: String): UserData = coroutineScope {
val userDeferred = async { userRepository.getUser(userId) }
val ordersDeferred = async { orderRepository.getOrders(userId) }
val profileDeferred = async { profileRepository.getProfile(userId) }
UserData(
user = userDeferred.await(),
orders = ordersDeferred.await(),
profile = profileDeferred.await()
)
}
这里三个查询是并行的,总耗时取决于最慢的那个。如果串行写,那就是三个时间加起来。
Semaphore 限制并发数。比如同时只允许 5 个数据库查询在跑,避免把连接池打满。
3. 数据库查询优化:N+1 问题必须死
数据库查询优化,说白了就是「少查、快查」。我在项目中遇到最典型的问题就是 N+1 查询。比如查 100 个用户,然后每个用户再查一次订单,结果就是 1 + 100 次查询。
用 Exposed 时,我习惯用 join 或者 with 来预加载:
// 错误写法:N+1
Users.selectAll().forEach { user ->
val orders = Orders.select { Orders.userId eq user[Users.id] }
}
// 正确写法:一次 join
(Users innerJoin Orders)
.select { Users.id eq someId }
.forEach { row ->
// 一次查询拿到所有数据
}
另外,索引也很重要。我曾经在一个表上忘了加索引,结果一个简单的 WHERE 查询跑了 3 秒。加了索引后,降到 10ms。嗯,差别就这么大。
4. 响应压缩:让数据飞得更快
响应压缩,说白了就是给数据「瘦身」。Ktor 内置了压缩插件,配置起来很简单:
install(ContentNegotiation) {
gzip {
minimumSize(1024) // 小于 1KB 的不压缩
}
}
为什么设 minimumSize?因为小数据压缩后反而可能变大(压缩算法有开销)。我一般设 1KB 或 2KB 为阈值。
压缩效果有多明显?我测过一个 JSON 接口,原始响应 200KB,压缩后只有 30KB,传输时间从 500ms 降到 80ms。用户感知的提升非常明显。
5. CDN 加速:把静态资源扔到边缘
CDN 加速,说白了就是把文件放到离用户最近的地方。对于 API 来说,CDN 主要加速的是静态资源(图片、CSS、JS)和缓存友好的接口。
在 Ktor 里,我习惯把静态资源路由单独拎出来:
routing {
static("/static") {
resources("static")
default("index.html")
}
}
然后在前面的 Nginx 或 Cloudflare 里配置 CDN 缓存规则。比如图片缓存 7 天,API 响应缓存 5 分钟。
我记得有一次,一个图片接口的响应时间一直在 1 秒以上,用户反馈很多。后来上了 CDN,直接降到 50ms。说白了,用户离服务器太远,物理距离是代码解决不了的。
知识体系总览
下面这张图,是我自己总结的性能优化路线。你可以把它当成一个检查清单:
总结一下
性能优化不是一锤子买卖。我个人习惯是:先压测找到瓶颈,然后针对性地调。连接池、协程、数据库、压缩、CDN,这五个点你挨个过一遍,大部分性能问题都能解决。
你想想看,如果每个接口都快 100ms,用户感知到的就是「嗖嗖的」。嗯,这就是我们后端工程师的成就感。