maxThreads 200 实际上是什么意思
一句话总结
acceptCount·maxConnections·maxThreads分别在不同的地点做不同的事情,如果不是慢响应的原因是线程不足,那么提高maxThreads会使情况变得更糟。
为什么需要这种区分呢?
把三个值合并成一个,措施就会变得一致——增加数字。但是因为各自的阻塞点不同,所以症状也不同。如果超过acceptCount,内核就会拒绝连接本身。Connection refused如果超过了,maxConnections,虽然可以连接,但没有收到响应,如果超过了maxThreads,请求会在队列中等待顺序。只有看到症状才能选择触摸哪个数字。
三个数字做的事情各有不同。
如果出现性能问题,有人一定会这样说:“试着提高maxThreads吧。” 但是大多数情况下,这种措施什么都改变不了,反而会让情况变得更糟。 要知道为什么,就必须知道三个值分别在哪个点运行。
[클라이언트 소켓]
|
v
(1) acceptCount ← OS 소켓 백로그 큐. 여기까지 넘치면 Connection refused
|
v
(2) maxConnections ← 톰캣이 동시에 '들고 있는' 커넥션 수
|
v
(3) maxThreads ← 동시에 '처리 중인' 요청 수 (워커 스레드)
|
v
[애플리케이션]
- maxThreads(默认200)—同时处理请求的进程数。 一个请求占用一个线程。在等待DB的同时也占用。
- maxConnections (NIO基本10000) — Tomcat维护的套接字数。 因为只有Keep-Alive打开,没有发送请求的连接不会占用线程。 这个值也可以比maxThreads大得多。这就是使用NIO的原因。
- acceptCount (默认100) — 即使maxConnections都满了时,在OS套接字队列中
等待的连接数。**如果超过这里,客户端就不会是503。
Connection refused接受。**这种差异在障碍分析中是决定性的。
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绑定工作就不一样了。比核心数更多的线程只会增加上下文切换的成本。 经验规则大致如下。
- I/O绑定(大部分业务Web应用程序):
코어 수 × (1 + 대기시간 / 처리시간) - CPU绑定(加密、图像处理):
코어 수 + 1
还有一定要确认——设定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" />
connectionTimeout— 接受连接后,在请求电话到达之前等待的时间。 这不是请求处理时间限制。误以为是这个,就会出现“延长了超时时间,但还是很慢”的提示。keepAliveTimeout— 等待下一次请求并保持连接的时间。 如果路长的话,通过重新使用连接可以减少延迟,但闲置连接会吞噬maxConnections。maxKeepAliveRequests— 在一个连接中处理的最大请求数。-1银无限制。
如果前端有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"
- **
-Xms哇-Xmx**一样抓住是服务器应用程序的惯例。 消除随着臀部变长变短而产生的GC。 - **
-XX:+HeapDumpOnOutOfMemoryError**不是选项,而是必须的。 OOM很难重现。如果在爆炸的瞬间不能收到dump,原因分析就只能是猜测。 但是,Dump文件的大小取决于硬盘的大小,所以需要确认磁盘的余地。 - GC日志在出现性能问题后打开的话太晚了。从一开始就打开,让文件旋转。
在打开这三行之前放入的话,稳定化期间的故障分析时间就减少了一半。
在现场相遇的样子
在性能会议上最常出现的措施是“提高maxThreads”。而且通常没有什么好处的。即使在请求等待DB响应时,线程也会被占用,所以如果DB是瓶颈,即使增加线程也会**只增加等待的线程。**相反,上下文切换和堆栈使用增加,GC停止时间变长。
所以有顺序。首先打开线程dump,看看工作者们在做什么。大部分是RUNNABLE的话,说明CPU不足,大部分是socket或锁在WAITING的话,不是线程,而是后面是瓶颈。在后者中提高maxThreads是为了增加等待室,而不是增加窗口。