3、连接池与线程模型:数据库连接池、HTTP连接池、Reactor与Proactor模型,Netty与Tomcat调优
连接池和线程模型,说白了就是管理资源的两种手段。一个管连接,一个管线程。这两样东西要是没用好,机器配置再高也白搭。我见过太多项目,CPU 跑不满,但请求就是卡着不动——查到最后,往往是连接池或者线程池配置出了问题。
3.1 数据库连接池:别让连接成为瓶颈
数据库连接池这东西,几乎每个后端项目都在用。但很多人只是配个最大连接数就完事了。嗯,这里要注意——连接池的核心不是「能连多少」,而是「怎么复用」。
我个人习惯,先看三个关键参数:
- 最小空闲连接数:保持多少连接在池子里等着。太低的话,突发流量一来,连接不够用,请求就得排队等连接创建。
- 最大连接数:池子最多能有多少连接。这个不是越大越好——数据库那边也有上限,而且连接多了上下文切换也费劲。
- 连接超时时间:等多久拿不到连接就放弃。我建议设成 30 秒以内,别让用户一直转圈。
我在项目中遇到过一个问题:某个服务平时 QPS 不高,但每天早高峰会突然飙一下。结果连接池最大连接数设得太小,请求全堵在池子外面,超时率直接飙到 30%。后来我把最小空闲从 5 调到 20,最大从 50 调到 200,问题就解决了。
核心原则:连接池的大小 = (核心线程数 × (1 + 等待时间 / 处理时间))。这个公式虽然粗糙,但比拍脑袋强。
常用的连接池实现,我列个对比表:
| 连接池 | 特点 | 适用场景 |
|---|---|---|
| HikariCP | 轻量、性能极高、无锁设计 | Spring Boot 默认,通用场景 |
| Druid | 功能丰富、监控完善、SQL 拦截 | 需要监控和慢 SQL 分析 |
| Tomcat JDBC Pool | 与 Tomcat 集成好、异步支持 | Tomcat 容器内使用 |
避坑指南:我曾经把最大连接数设成 1000,结果数据库直接挂了。数据库的 max_connections 也要同步调整,别光改应用端。
3.2 HTTP 连接池:微服务间的生命线
微服务架构下,服务之间调用全靠 HTTP 或 RPC。如果每次请求都新建连接,那光 TCP 三次握手就能把时间吃掉一大半。HTTP 连接池就是干这个的——复用连接,减少握手开销。
我常用的 HTTP 客户端是 OkHttp 和 Apache HttpClient。它们的连接池配置大同小异:
- 最大空闲连接数:保持多少连接在池子里待命。我一般设成 50-100。
- 存活时间:空闲连接多久没被用就关掉。默认 5 分钟,我觉得太长,一般改成 60 秒。
- 每个路由的最大连接数:同一个目标地址最多用多少连接。这个要小心,别把下游服务打爆了。
举个例子,OkHttp 的配置:
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(50, 60, TimeUnit.SECONDS))
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build();
注意:HTTP 连接池不是越大越好。连接太多会导致内存占用飙升,而且 TCP 连接本身也是资源。我曾经见过一个服务,连接池配了 500,结果内存直接飙到 2GB——全是 TIME_WAIT 状态的连接。
3.3 Reactor 与 Proactor:两种线程模型
说到线程模型,就绕不开 Reactor 和 Proactor。这两个东西,说白了就是「怎么处理 IO 事件」的两种思路。
Reactor 模型:事件来了,我通知你,你自己去读数据。这是同步非阻塞的典型代表。Netty 用的就是 Reactor。
Proactor 模型:事件来了,我帮你把数据读好,然后通知你。这是异步非阻塞。Windows 的 IOCP 就是 Proactor。
我画个图,帮你理解它们的区别:
你想想看,Reactor 模型下,Handler 还得自己调用 read() 去读数据。而 Proactor 模型下,操作系统已经把数据放到缓冲区了,Handler 直接拿来用就行。说白了,Proactor 更省事,但对操作系统的异步 IO 支持要求更高。
在 Linux 下,Reactor 是主流,因为 epoll 本质上是同步非阻塞的。Windows 下 Proactor 更常见,因为 IOCP 是真正的异步 IO。
3.4 Netty 与 Tomcat 调优
Netty 和 Tomcat 是 Java 生态里最常用的两个网络框架。一个偏底层,一个偏应用。调优思路也不太一样。
Netty 调优
Netty 的核心是 Reactor 模型,它的调优主要围绕线程模型和缓冲区:
- Boss 线程数:负责接受连接。一般设成 1 就够了,除非你有大量短连接。
- Worker 线程数:负责处理 IO 事件。我建议设成 CPU 核心数的 2 倍。
- 缓冲区大小:默认 8KB,如果传输大对象可以调大。我一般设成 64KB。
- 高低水位:控制写缓冲区的流量。高水位到了就暂停写,低水位到了再恢复。
举个例子:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(32 * 1024, 64 * 1024))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new MyHandler());
}
});
个人经验:Netty 的 ByteBuf 尽量用池化的。我见过有人每次都 new 一个 ByteBuf,结果 GC 压力巨大。用 PooledByteBufAllocator 能省不少事。
Tomcat 调优
Tomcat 的线程模型经历了从 BIO 到 NIO 再到 NIO2 的演进。现在主流是 NIO 模式。
关键参数:
| 参数 | 说明 | 建议值 |
|---|---|---|
| maxThreads | 最大工作线程数 | 200-500(视硬件而定) |
| minSpareThreads | 最小空闲线程数 | 25-50 |
| acceptCount | 等待队列长度 | 100-200 |
| connectionTimeout | 连接超时时间 | 5000-10000ms |
| maxConnections | 最大连接数 | 10000(NIO 模式下) |
配置示例:
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="400"
minSpareThreads="50"
acceptCount="150"
connectionTimeout="5000"
maxConnections="10000"
maxKeepAliveRequests="100"/>
我曾经踩过的坑:把 maxThreads 设成 1000,结果线程太多,上下文切换把 CPU 吃满了。后来压测发现,400 个线程就能跑满 CPU,再多反而性能下降。线程数不是越多越好,要压测找到拐点。
3.5 连接池与线程模型的协同
连接池和线程模型不是孤立的。它们之间有个微妙的关系:
- 线程池里的每个线程,都可能持有数据库连接或 HTTP 连接。
- 如果线程数太多,连接池会被占满,其他线程就得等着。
- 如果连接池太小,线程拿到连接后很快释放,线程池的利用率也上不去。
我一般这样配:
- 先确定线程池大小(CPU 核心数 × 2 + 一些余量)。
- 再根据线程池大小,确定连接池的最大连接数(一般是线程数的 1.5-2 倍)。
- 最后压测验证,调整参数。
一句话总结:连接池管资源复用,线程模型管并发处理。两者配合好了,系统才能跑得又稳又快。