性能优化:连接池调优、协程优化、数据库查询优化、响应压缩、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 分钟,避免数据库端主动断开后,客户端还在用旧连接。
注意:连接池大小不是拍脑袋定的。我曾经在一个项目里,把连接池从 10 调到 30,结果性能反而下降了。原因是数据库连接数太多,上下文切换开销剧增。建议用压测工具(比如 k6)跑一下,找到最佳值。

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。嗯,差别就这么大。

核心原则:能用一次查询解决的,绝不用两次。能用索引的,一定加索引。能用批量操作的,别一条一条 insert。

4. 响应压缩:让数据飞得更快

响应压缩,说白了就是给数据「瘦身」。Ktor 内置了压缩插件,配置起来很简单:

install(ContentNegotiation) {
    gzip {
        minimumSize(1024) // 小于 1KB 的不压缩
    }
}

为什么设 minimumSize?因为小数据压缩后反而可能变大(压缩算法有开销)。我一般设 1KB 或 2KB 为阈值。

压缩效果有多明显?我测过一个 JSON 接口,原始响应 200KB,压缩后只有 30KB,传输时间从 500ms 降到 80ms。用户感知的提升非常明显。

注意:压缩会增加 CPU 开销。如果服务器 CPU 已经很高了,可以考虑用 Brotli 算法(压缩率更高但更吃 CPU),或者干脆只对部分接口启用压缩。

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。说白了,用户离服务器太远,物理距离是代码解决不了的。

知识体系总览

下面这张图,是我自己总结的性能优化路线。你可以把它当成一个检查清单:

Ktor 性能优化 连接池调优 协程优化 数据库查询优化 响应压缩 CDN 加速 HikariCP 配置 连接池大小 async/await 并发控制 N+1 问题 索引优化 Gzip / Brotli 最小大小阈值 静态资源缓存 边缘节点 核心思路:减少等待、减少传输、减少计算 压测验证 + 逐步调优

总结一下

性能优化不是一锤子买卖。我个人习惯是:先压测找到瓶颈,然后针对性地调。连接池、协程、数据库、压缩、CDN,这五个点你挨个过一遍,大部分性能问题都能解决。

你想想看,如果每个接口都快 100ms,用户感知到的就是「嗖嗖的」。嗯,这就是我们后端工程师的成就感。

公众号:蓝海资料掘金营,微信deep3321