有依据地配置线程池、堆与 GC 日志
目标
亲手配置共享 Executor、acceptCount、Keep-Alive、堆和 GC 日志, 并能够用利特尔法则计算 maxThreads,留下依据文档。
为什么重要
出现性能问题时,总会有人说“把 maxThreads 调高吧”。但如果不知道这个值
作用在哪个环节,大多数情况下要么什么都不会改变,要么变得更糟。
maxThreads 是“同时处理中的请求”,maxConnections 是“持有的套接字”,
acceptCount 是“连这个也满了之后的操作系统 backlog”,backlog 也满了,客户端收到的
不是 503,而是 Connection refused。懂得这个区别的人,能更快结束故障。
步骤
- 启动 Tomcat 后,在
server.xml的<Service>内添加共享 Executor。name="tomcatThreadPool"、namePrefix="labhub-exec-"、maxThreads="150"、minSpareThreads="20"。 - 为 8080 连接器添加
executor="tomcatThreadPool"属性,使其使用这个 Executor, 然后重启。发送几次请求后,导出线程转储并保存到/root/tc/threads.txt。转储中必须能看到labhub-exec-线程。 - 为 8080 连接器设置
acceptCount="50"、maxConnections="2000"、connectionTimeout="20000"。 - 为 8080 连接器设置
keepAliveTimeout="5000"、maxKeepAliveRequests="100", 重启后,用一次 curl 在同一个连接上发送两个请求, 并将其详细输出(-v)保存到/root/tc/keepalive.txt。 文件中必须有Re-using existing connection或Connection #0 to host这类连接复用的痕迹。 - 在
/opt/tomcat/bin/setenv.sh的CATALINA_OPTS中加入-Xms512m -Xmx512m, 然后重启。正在运行的进程参数中必须能看到这两个值。 - 在同一个
CATALINA_OPTS中追加 GC 日志选项,然后重启。 日志文件为/root/tc/gc.log,文件轮转设置为 5 个、每个 10MB。 重启后/root/tc/gc.log必须已生成且不为空。 - 创建
/root/tc/load.sh。接收两个参数(URL 동시요청수,占位符依次为 URL 与并发请求数), 同时发送请求,并在最后一行输出ok=<성공수> fail=<실패수>(占位符依次为成功数与失败数)。 HTTP 200 算成功,其他情况(包括 404)算失败。 - 编写
/root/tc/sizing.md。用下面的场景应用利特尔法则, 计算 maxThreads 的值,并留下计算过程和结果。- 目标吞吐量:300 TPS
- 平均响应时间:200ms
- 峰值余量率:30%
文档中必须恰好有一行形如
maxThreads=<계산값>(占位符为计算值)的内容, 并且TPS、ms、여유율(韩文,意为“余量率”)这三个词都要出现。
参考
- 线程转储:
jcmd <PID> Thread.print或jstack <PID> - 一个连接发送两个请求:
curl -v http://127.0.0.1:8080/ http://127.0.0.1:8080/ - GC 日志选项格式:
-Xlog:gc*:file=<경로>:time,uptime:filecount=<n>,filesize=<크기>(占位符依次为路径与文件大小) - 常见错误 1:把
Executor声明在Connector之后。必须在被引用之前先定义。 - 常见错误 2:给连接器指定了
executor,却仍保留maxThreads, 并以为那个值会生效。使用 Executor 时,连接器的 maxThreads 会被忽略。 - 常见错误 3:在利特尔法则中把响应时间直接以毫秒相乘。必须换算成秒。
定义共享 Executor
启动 Tomcat 后,在 server.xml 的 <Service> 内添加共享 Executor。
name="tomcatThreadPool"、namePrefix="labhub-exec-"、
maxThreads="150"、minSpareThreads="20"。
Executor 是 Service 的子元素,必须先于 Connector 声明。给 namePrefix 起一个有意义的名字,之后在线程转储中就能立刻区分出来。
将连接器关联到 Executor
为 8080 连接器添加 executor="tomcatThreadPool" 属性,使其使用这个 Executor,
然后重启。发送几次请求后,导出线程转储并保存到
/root/tc/threads.txt。转储中必须能看到 labhub-exec- 线程。
给连接器指定 executor 属性后,该连接器的 maxThreads 会被忽略。是否真正生效,可以在线程转储中查看线程名称来确认。请使用 jcmd 或 jstack。
连接接纳参数
为 8080 连接器设置 acceptCount="50"、maxConnections="2000"、
connectionTimeout="20000"。
acceptCount 是 maxConnections 耗尽之后的操作系统 backlog。连这里也满了,客户端收到的不是 503,而是连接被拒绝。
设置并确认 Keep-Alive
为 8080 连接器设置 keepAliveTimeout="5000"、maxKeepAliveRequests="100",
重启后,用一次 curl 在同一个连接上发送两个请求,
并将其详细输出(-v)保存到 /root/tc/keepalive.txt。
文件中必须有 Re-using existing connection 或 Connection #0 to host 这类连接复用的痕迹。
用 curl -v 可以查看响应头中的 Connection 值以及连接是否被复用。用一次 curl 发送两个请求,就能观察到复用。
固定堆大小
在 /opt/tomcat/bin/setenv.sh 的 CATALINA_OPTS 中加入 -Xms512m -Xmx512m,
然后重启。正在运行的进程参数中必须能看到这两个值。
服务器应用程序的惯例是把最小堆和最大堆设为相同。目的是消除堆反复扩大缩小所引发的 GC。
启用 GC 日志
在同一个 CATALINA_OPTS 中追加 GC 日志选项,然后重启。
日志文件为 /root/tc/gc.log,文件轮转设置为 5 个、每个 10MB。
重启后 /root/tc/gc.log 必须已生成且不为空。
JDK 9 之后使用 -Xlog 统一日志。如果不同时指定文件轮转选项(个数和大小),GC 日志会把磁盘写满。
并发负载脚本
创建 /root/tc/load.sh。接收两个参数(URL 동시요청수,占位符依次为 URL 与并发请求数),
同时发送请求,并在最后一行输出 ok=<성공수> fail=<실패수>(占位符依次为成功数与失败数)。
HTTP 200 算成功,其他情况(包括 404)算失败。
在后台启动多个 curl,再用 wait 汇总,这种方式就足够了。每个请求是否成功,请用 HTTP 状态码来判定。
编写线程池容量估算依据
编写 /root/tc/sizing.md。用下面的场景应用利特尔法则,
计算 maxThreads 的值,并留下计算过程和结果。
- 目标吞吐量:300 TPS
- 平均响应时间:200ms
- 峰值余量率:30%
文档中必须恰好有一行形如
maxThreads=<계산값>(占位符为计算值)的内容, 并且TPS、ms、여유율(韩文,意为“余量率”)这三个词都要出现。
利特尔法则:并发处理数 = 目标 TPS × 平均响应时间(秒)。乘以余量率之后向上取整。必须在文档中留下计算过程,6 个月后有人要改这个值时才有依据。