LabHub
学习 学习路径 课程

Tomcat 与 nginx 运维

maxThreads 200 实际上是什么意思

在 LabHub 中继续学习

一句话总结

acceptCount·maxConnections·maxThreads分别在不同的地点做不同的事情,如果不是慢响应的原因是线程不足,那么提高maxThreads会使情况变得更糟。

概念图: maxThreads · maxConnections · acceptCount · 白日梦饱和

为什么需要这种区分呢?

把三个值合并成一个,措施就会变得一致——增加数字。但是因为各自的阻塞点不同,所以症状也不同。如果超过acceptCount,内核就会拒绝连接本身。Connection refused如果超过了,maxConnections,虽然可以连接,但没有收到响应,如果超过了maxThreads,请求会在队列中等待顺序。只有看到症状才能选择触摸哪个数字。

三个数字做的事情各有不同。

如果出现性能问题,有人一定会这样说:“试着提高maxThreads吧。” 但是大多数情况下,这种措施什么都改变不了,反而会让情况变得更糟。 要知道为什么,就必须知道三个值分别在哪个点运行。

            [클라이언트 소켓]
                  |
                  v
   (1) acceptCount        ← OS 소켓 백로그 큐. 여기까지 넘치면 Connection refused
                  |
                  v
   (2) maxConnections     ← 톰캣이 동시에 '들고 있는' 커넥션 수
                  |
                  v
   (3) maxThreads         ← 동시에 '처리 중인' 요청 수 (워커 스레드)
                  |
                  v
              [애플리케이션]

Connection refused被拍到并不是“服务器死了” 服务器是活着的,但没有承受能力。在前端nginx日志中 connect() failed (111: Connection refused)如果看到的话,不仅后端进程会死亡 白日梦饱和也应该列入候选名单。

不能提高maxThreads的原因

如果请求处理时间的大部分是DB等待的话,增加线程是 是增加DB前面排队的人的事情。处理量不变,只有等待时间增加。 严重的话,由于连接池耗尽,整个系统会停止。

如果打开线程dump后看到这样的模式,就不是线程问题。

"http-nio-8080-exec-73" ... WAITING (parking)
  at jdk.internal.misc.Unsafe.park
  at com.zaxxer.hikari.pool.HikariPool.getConnection   ← DB 커넥션 풀 대기

这个垃圾邮件的信息很明确:问题不是WAS多线程,而是DB连接池。 将maxThreads提高到400的话,等待中的线程只会从200个增加到400个。

以有根据的方式计算——李特尔定律

不要凭感觉决定,算算吧。小李的定律是这样写的。

동시 처리 수 = 목표 처리량(TPS) × 평균 응답시간(초)

目标是每秒300件,平均响应是200ms的话,

300 × 0.2 = 60

也就是说,至少需要60个线程同时工作才能产生300 TPS。 这里乘以余裕率(与峰值相比,一般20~50%)。30%余裕的话60 × 1.3 = 78.

如果是CPU绑定工作就不一样了。比核心数更多的线程只会增加上下文切换的成本。 经验规则大致如下。

还有一定要确认——设定maxThreads后,查看与DB连接池的关系。 如果200个WAS线程都使用DB,但解法只有20个,那么180个就会等待。 相反,如果把草种成200株的话,这次是DB方面max_connections爆炸了。 实例可以面对面乘以多个。

서비스 A 12대 × 풀 20 = 240
서비스 B  8대 × 풀 15 = 120
배치 워커 4대 × 풀 5  =  20
                합계   = 380
DB max_connections     = 200   ← 롤링 배포로 인스턴스가 잠깐 두 배 되는 순간 터진다

没有做这个计算的系统在滚动部署中 FATAL: sorry, too many clients already崩溃了。更糟糕的是那个错误 甚至阻止健康检查和监测代理这一点。在障碍发生时,观测手段也会一起消失。

Keep-Alive是双刃剑

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           keepAliveTimeout="5000"
           maxKeepAliveRequests="100"
           maxThreads="150" minSpareThreads="20"
           maxConnections="2000" acceptCount="50" />

如果前端有nginx的话,nginx ↔ tomcat之间的连接是少数,重用率很高。 那时最好给keepAliveTimeout足够的时间。相反,Tomcat在网上 如果直接暴露的话,闲置连接会消耗资源,所以抓住短时间。 不是“正确答案值”,而是“根据前端构成的不同而变化的值”。

共享Executor

如果放置多个连接器,每个连接器都会有自己的线程。那么就很难控制整个线程数。 这时使用共享Executor。

<Executor name="tomcatThreadPool" namePrefix="labhub-exec-"
          maxThreads="150" minSpareThreads="20"/>
<Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" ... />

namePrefix有意义地给予是实际操作技巧。当打开线程dump时 labhub-exec-37如果像这样出现,就能马上知道这是哪个连接器的螺纹。 基本值http-nio-8080-exec-虽然也不错,但是如果连接器有很多个的话,需要区分。

在调校之前打开了Hips和GC的日志。

CATALINA_OPTS="-Xms1g -Xmx1g \
  -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/dump \
  -Xlog:gc*:file=/logs/gc.log:time,uptime:filecount=5,filesize=10m"

在打开这三行之前放入的话,稳定化期间的故障分析时间就减少了一半。

在现场相遇的样子

在性能会议上最常出现的措施是“提高maxThreads”。而且通常没有什么好处的。即使在请求等待DB响应时,线程也会被占用,所以如果DB是瓶颈,即使增加线程也会**只增加等待的线程。**相反,上下文切换和堆栈使用增加,GC停止时间变长。

所以有顺序。首先打开线程dump,看看工作者们在做什么。大部分是RUNNABLE的话,说明CPU不足,大部分是socket或锁在WAITING的话,不是线程,而是后面是瓶颈。在后者中提高maxThreads是为了增加等待室,而不是增加窗口。