连接池耗尽,服务就停摆
目标
亲手制造池耗尽,并用超时把它变成快速失败;然后把 Tomcat 的线程池和数据库连接池配置放进单独的 CATALINA_BASE 实例并真正启动。
为什么重要
服务会在最小的池处停住。比池的大小更重要的是等待上限——没有上限,一个缓慢的下游就会吞掉所有处理线程。在 Java 代码(Semaphore)和 Tomcat 配置(连接器、DataSource)两侧,亲手确认同一个原理。材料 PoolDemo.java 运行在 127.0.0.1:8086(4 个处理线程、2 个连接池连接、查询 3 秒)和管理端口 8087(/stats,单独的线程)上。Tomcat 安装在 $CATALINA_HOME。
步骤
- 在
/root/jvm/pool中编译 PoolDemo.java,用nohup java -cp /root/jvm/pool PoolDemo > /root/jvm/pool/server.log 2>&1 &启动,然后把 pid 写入/root/jvm/pool/server.pid。curl -s http://127.0.0.1:8086/health返回 ok,curl -s http://127.0.0.1:8087/stats返回以served=开头的一行。 - 同时发送 6 个
/query(六次curl -s -m 30 http://127.0.0.1:8086/query &),1 秒之后执行jcmd <pid> Thread.print > /root/jvm/pool/dump-pool.txt。其中必须有parking to wait for和Semaphore,并且/stats的max_waiting必须大于等于 2。 - kill 掉 server.pid 对应的进程,用
-Ddb.pool.timeout.ms=500重新启动,并更新 server.pid。再次同时发送 6 个/query,并把其中每个请求的结果用curl -s -m 10 -o /dev/null -w '%{http_code}\n' … >> /root/jvm/pool/after-timeout.txt收集起来——其中必须混有 503。 - 创建
CATALINA_BASE:在/root/jvm/pool/tc下创建conf(复制$CATALINA_HOME/conf)、logs、temp、webapps、work目录。 - 在
/root/jvm/pool/tc/conf/server.xml的 8080<Connector>中设置maxThreads="50"、acceptCount="20"、connectionTimeout="5000"(已有的 connectionTimeout 值要改掉)。XML 必须有效。 - 在
/root/jvm/pool/tc/conf/context.xml的<Context>内加入<Resource name="jdbc/app" auth="Container" type="javax.sql.DataSource" maxTotal="20" maxIdle="5" maxWaitMillis="2000" driverClassName="org.h2.Driver" url="jdbc:h2:mem:app" username="sa" password=""/>。 - 用
CATALINA_BASE=/root/jvm/pool/tc JAVA_OPTS=-Xmx256m catalina.sh start启动实例。8080 必须返回 HTTP 状态行,并且该 JVM 的命令行中必须有-Dcatalina.base=/root/jvm/pool/tc。 - 把 Tomcat JVM 的转储保存到
/root/jvm/pool/dump-tomcat.txt,从转储中读出以"http-nio-8080-exec-开头的线程数,以及 server.xml 中 8080 连接器的 maxThreads,以exec_threads=<n>和max_threads=<n>两行写入/root/jvm/pool/sizing.txt。
参考
- Tomcat 的 pid:
jcmd -l | grep Bootstrap。转储:jcmd <pid> Thread.print。 - 连接器属性文档:https://tomcat.apache.org/tomcat-10.1-doc/config/http.html ,DataSource:https://tomcat.apache.org/tomcat-10.1-doc/jndi-datasource-examples-howto.html
- 常见错误:只增加处理线程的数量;让健康检查经过数据库;不复制
conf就启动 BASE(没有配置文件会直接挂掉)。
启动 PoolDemo
在 /root/jvm/pool 中编译 PoolDemo.java,用 nohup 启动后,把 pid 写入 /root/jvm/pool/server.pid。8086 的 /health 返回 ok,8087 的 /stats 以 served= 开头。
先执行 javac -d /root/jvm/pool /opt/lab/fixtures/jvm/PoolDemo.java,再执行 nohup java -cp /root/jvm/pool PoolDemo > server.log 2>&1 & 和 echo $! > server.pid。
榨干连接池
同时发送 6 个 /query,1 秒之后执行 jcmd Thread.print > /root/jvm/pool/dump-pool.txt。其中必须有 parking to wait for 和 Semaphore,并且 /stats 的 max_waiting 必须大于等于 2。
for i in 1 2 3 4 5 6; do curl -s -m 30 http://127.0.0.1:8086/query & done; sleep 1; jcmd $(cat server.pid) Thread.print > dump-pool.txt。2 个连接对应 6 个请求,就有 4 个在等待。
为等待设置上限
kill 掉 server.pid 对应的进程,用 -Ddb.pool.timeout.ms=500 重新启动,并更新 server.pid。同时发送 6 个 /query,并把结果码收集到 /root/jvm/pool/after-timeout.txt(其中必须混有 503)。
给每个 curl 加上 -o /dev/null -w '%{http_code}\n',并用 >> after-timeout.txt 收集。500ms 内没能拿到连接的请求就是 503。等待后台 curl 时如果使用不带参数的 wait,连用 nohup 启动的 java 也会一起等,所以只对 curl 的 pid 执行 wait。
创建 CATALINA_BASE
在 /root/jvm/pool/tc 下创建 conf(复制 $CATALINA_HOME/conf)、logs、temp、webapps、work。
mkdir -p tc/logs tc/temp tc/webapps tc/work && cp -r $CATALINA_HOME/conf tc/conf。不要动安装目录(HOME)。
连接器的三个数字
在 /root/jvm/pool/tc/conf/server.xml 的 8080 Connector 中设置 maxThreads="50"、acceptCount="20"、connectionTimeout="5000"。XML 必须有效。
发行版 server.xml 的 8080 连接器中已经有 connectionTimeout="20000"——改掉这个值,再增加另外两个属性。属性放在起始标签内的任何位置都可以。
为连接池设置等待上限
在 /root/jvm/pool/tc/conf/context.xml 的 内,加入 name="jdbc/app" 的 DataSource Resource,并设置 maxTotal="20" maxIdle="5" maxWaitMillis="2000"。
在 紧前面加入一行 即可。maxWaitMillis 为 -1 就是无限等待——这正是事件的原因。
启动实例
用 CATALINA_BASE=/root/jvm/pool/tc JAVA_OPTS=-Xmx256m catalina.sh start 启动。8080 必须返回 HTTP 状态行,并且 JVM 命令行中必须有 -Dcatalina.base=/root/jvm/pool/tc。
catalina.sh 位于 $CATALINA_HOME/bin。启动需要几秒钟,请反复执行 curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/ 试一试(即使是 404,也说明状态行已经返回了)。
在转储中统计池线程
把 Tomcat JVM 的转储保存到 /root/jvm/pool/dump-tomcat.txt,并把以 "http-nio-8080-exec- 开头的线程数,以及 server.xml 中 8080 连接器的 maxThreads,以 exec_threads= 和 max_threads= 的形式写入 /root/jvm/pool/sizing.txt。
pid 用 jcmd -l | awk '/Bootstrap/{print $1}' 获取。统计方法:grep -c '^"http-nio-8080-exec-' dump-tomcat.txt。minSpareThreads 默认是 10,所以刚启动时大约有 10 个。max_threads 是 8080 连接器中的值——不要取发行版 server.xml 注释中的 Executor 示例(maxThreads=150)。