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 vs Proactor 模型对比 Reactor 模型 1. 事件循环等待 IO 事件 2. 通知 Handler 事件就绪 3. Handler 自己读取数据 4. 处理业务逻辑 Proactor 模型 1. 发起异步 IO 操作 2. 操作系统读取数据 3. 通知 Handler 数据已就绪 4. 直接处理业务逻辑 Reactor:你来读 → 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 连接。
  • 如果线程数太多,连接池会被占满,其他线程就得等着。
  • 如果连接池太小,线程拿到连接后很快释放,线程池的利用率也上不去。

我一般这样配:

  1. 先确定线程池大小(CPU 核心数 × 2 + 一些余量)。
  2. 再根据线程池大小,确定连接池的最大连接数(一般是线程数的 1.5-2 倍)。
  3. 最后压测验证,调整参数。

一句话总结:连接池管资源复用,线程模型管并发处理。两者配合好了,系统才能跑得又稳又快。