Configuring the Thread Pool, Heap and GC Logs With Reasons
한국어 원문으로 표시합니다.
목표
공유 Executor·acceptCount·Keep-Alive·힙·GC 로그를 직접 설정하고, 리틀의 법칙으로 maxThreads 를 계산해 근거 문서를 남길 수 있게 됩니다.
왜 중요한가
성능 이슈가 생기면 늘 "maxThreads 를 올리자"는 말이 나옵니다. 그런데 그 값이
어느 지점에서 동작하는지 모르면 대부분 아무것도 바뀌지 않거나 더 나빠집니다.
maxThreads 는 '동시에 처리 중인 요청', maxConnections 는 '들고 있는 소켓',
acceptCount 는 '그마저 넘쳤을 때의 OS 백로그'이고, 백로그까지 넘치면 클라이언트는
503 이 아니라 Connection refused 를 받습니다. 이 차이를 아는 사람이 장애를 빨리 끝냅니다.
단계
- Tomcat 을 기동한 뒤
server.xml의<Service>안에 공유 Executor 를 추가합니다.name="tomcatThreadPool",namePrefix="labhub-exec-",maxThreads="150",minSpareThreads="20". - 8080 커넥터가 이 Executor 를 쓰도록
executor="tomcatThreadPool"속성을 주고 재기동합니다. 요청을 몇 번 보낸 뒤 스레드 덤프를 떠서/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 동시요청수)를 받아 동시에 요청을 보내고 마지막 줄에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 를 쓰도록 executor="tomcatThreadPool" 속성을 주고
재기동합니다. 요청을 몇 번 보낸 뒤 스레드 덤프를 떠서
/root/tc/threads.txt 에 저장하세요. 덤프에 labhub-exec- 스레드가 보여야 합니다.
커넥터에 executor 속성을 주면 그 커넥터의 maxThreads 는 무시됩니다. 실제로 적용됐는지는 스레드 덤프에서 스레드 이름을 보면 확인됩니다. jcmd 나 jstack 을 쓰세요.
커넥션 수용 파라미터
8080 커넥터에 acceptCount="50", maxConnections="2000",
connectionTimeout="20000" 을 설정합니다.
acceptCount 는 maxConnections 가 소진된 뒤의 OS 백로그입니다. 여기까지 넘치면 클라이언트는 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 동시요청수)를 받아
동시에 요청을 보내고 마지막 줄에 ok=<성공수> fail=<실패수> 를 출력합니다.
HTTP 200 이면 성공, 그 외(404 포함)는 실패로 셉니다.
백그라운드로 여러 curl 을 띄우고 wait 로 모으는 방식이면 충분합니다. 각 요청의 성공 여부는 HTTP 상태코드로 판정하세요.
스레드풀 산정 근거 작성
/root/tc/sizing.md 를 작성합니다. 아래 시나리오로 리틀의 법칙을 적용해
maxThreads 값을 계산하고, 계산 과정과 결과를 남깁니다.
- 목표 처리량: 300 TPS
- 평균 응답시간: 200ms
- 피크 여유율: 30%
문서에는
maxThreads=<계산값>형태의 줄이 정확히 있어야 하고,TPS,ms,여유율세 단어가 모두 등장해야 합니다.
리틀의 법칙: 동시 처리 수 = 목표 TPS × 평균 응답시간(초). 여유율을 곱한 뒤 올림합니다. 계산 과정을 문서에 남겨야 6개월 뒤 누군가 이 값을 바꿀 때 근거가 됩니다.