TT Lab
开始
学习 学习路径 课程

堆还有空间,服务却停了

连接池耗尽,服务就停摆

在 TT Lab 中继续学习

目标

亲手制造池耗尽,并用超时把它变成快速失败;然后把 Tomcat 的线程池和数据库连接池配置放进单独的 CATALINA_BASE 实例并真正启动。

为什么重要

服务会在最小的池处停住。比池的大小更重要的是等待上限——没有上限,一个缓慢的下游就会吞掉所有处理线程。在 Java 代码(Semaphore)和 Tomcat 配置(连接器、DataSource)两侧,亲手确认同一个原理。材料 PoolDemo.java 运行在 127.0.0.1:8086(4 个处理线程、2 个连接池连接、查询 3 秒)和管理端口 8087(/stats,单独的线程)上。Tomcat 安装在 $CATALINA_HOME。

步骤

  1. 在 /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= 开头的一行。
  2. 同时发送 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。
  3. 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。
  4. 创建 CATALINA_BASE:在 /root/jvm/pool/tc 下创建 conf(复制 $CATALINA_HOME/conf)、logs、temp、webapps、work 目录。
  5. 在 /root/jvm/pool/tc/conf/server.xml 的 8080 <Connector> 中设置 maxThreads="50"、acceptCount="20"、connectionTimeout="5000"(已有的 connectionTimeout 值要改掉)。XML 必须有效。
  6. 在 /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=""/>。
  7. 用 CATALINA_BASE=/root/jvm/pool/tc JAVA_OPTS=-Xmx256m catalina.sh start 启动实例。8080 必须返回 HTTP 状态行,并且该 JVM 的命令行中必须有 -Dcatalina.base=/root/jvm/pool/tc。
  8. 把 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。

参考

启动 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)。