요청 하나가 아니라 전부가 느려졌다 · 무엇이 막고 무엇이 안 막는가 · 실습
스레드 풀의 칸을 세어 본다
목표
같은 계산이 어디서 도는가 에 따라 루프를 막기도 하고 안 막기도 한다는 것을
직접 재서 확인하고, libuv 스레드 풀의 칸이 몇 개인지를 숫자로 세어 봅니다.
왜 중요한가
crypto.pbkdf2 와 crypto.pbkdf2Sync 는 같은 계산을 합니다. 그런데 하나는
스레드 풀로 가고 하나는 이벤트 루프 위에서 돕니다. 이름 끝의 Sync 네 글자가
"이 요청만 느리다" 와 "전부 느리다" 를 가릅니다.
그리고 스레드 풀은 무한하지 않습니다. 기본이 네 칸이라, 무거운 비동기 작업을
다섯 개 보내면 다섯 번째는 앞의 하나가 끝날 때까지 시작조차 하지 못합니다.
파일 읽기와 DNS 조회와 압축이 같은 네 칸을 나눠 쓰기 때문에, 압축 작업이
파일 읽기를 늦추는 일이 실제로 일어납니다. 반대로 소켓 읽기와 dns.resolve 는
그 칸을 쓰지 않아서 아무 영향을 받지 않습니다. 이 지도를 외우는 것이 아니라
재서 확인하는 법을 익히는 것이 이 실습입니다.
단계
1. /root/work/blocking/probe.mjs 에 classify(name) 로 API 의 자리를 적습니다.
2. 같은 파일에 pct(samples, p) 와 withLag(fn, options) 를 만듭니다.
3. crypto.pbkdf2Sync 와 crypto.pbkdf2 를 재어/root/work/blocking/report.json 의 runs.pbkdf2Sync·runs.pbkdf2Async 에 적습니다.
4. 큰 JSON 문자열을 파싱하며 재어 runs.jsonParse 에 적습니다.
5. /root/work/blocking/wave.mjs 를 만들어 같은 일감 여러 개를 한꺼번에 보내고,runs.pool4 와 runs.pool8 에 적습니다.
6. UV_THREADPOOL_SIZE=8 로 같은 프로그램을 돌려 runs.pool8big 에 적습니다.
7. zlib.gzipSync 와 zlib.gzip 으로 같은 방법을 한 번 더 적용합니다.
참고
classify는"loop"(루프 위에서 돈다) ·"threadpool"(libuv 스레드 풀) ·withLag(fn, {durationMs, intervalMs, startAfterMs})는 재기를 먼저 시작하고,wave.mjs는process.argv[2]로 개수를 받아- 흔한 실수:
process.env.UV_THREADPOOL_SIZE = "8"을 프로그램 안에서 설정하는 것.
"kernel"(커널이 알려 줄 때까지 루프는 기다리기만 한다) 셋 중 하나를 돌려주고,
표에 없는 이름에는 "unknown" 을 돌려줍니다.
조금 뒤에 fn() 을 부르고(프로미스면 기다립니다), 그 시간을 workMs 에 담아{intervalMs, durationMs, samples, workMs} 를 돌려줍니다.
{"n": .., "threadpoolSize": .., "finishMs": [..]} 한 줄을 찍습니다.finishMs 는 끝난 시각(ms)을 오름차순으로 정렬한 것입니다.
스레드 풀은 그보다 먼저 만들어져 있어서 아무 일도 일어나지 않습니다.
단계 7개
- 먼저 지도를 그린다
- 일을 시키면서 재는 자
- 같은 계산, 다른 자리
- 옮길 곳이 없는 일
- 칸이 몇 개인지 세어 본다
- 칸을 늘리면 해결되는가
- 새 API 에 같은 방법을 적용한다