Nginx가 비동기, 이벤트 드리븐을 사용한다. 따라서 전통적인 서버 아키텍처처럼 매 요청마다 그에 상응하는 프로세스나 스레드를 생성하는 대신 여러 커넥션과 요청들을 하나의 워커 프로세스에서 처리한다. 이런 동작 과정을 위해 Nginx는 non-blocking 형태의 소켓과 epoll, kqueue와 같은 효율적인 방법을 사용한다.
Nginx는 프로세스 갯수가 적고 정적이기 떄문에 메모리 낭비가 적고 CPU 주기가 컨텍스트 스위칭으로 낭비되지 않는다. 이러한 접근법의 이점은 Nginx로 증명되었다. Nginx는 수백만개의 동시 요청과 스케일을 무리없이 감당한다.
하지만 비동기, 이벤트 드리븐 방식도 문제가 존재한다. 바로 blocking이다. 안타깝게도 많은 서드파티 모듈들은 blocking call을 사용하고 있다. 그리고 사용자들은 이런 단점들을 인식하지 못한다. Blocking 연산은 Nginx 퍼포먼스를 저하시키므로 반드시 방지해야 한다.
현재 Nginx의 코드로는 모든 blocking 연산을 비할 수 없다. 이를 해결하기 위해 새로운 "thread pool" 매커니즘이 Nginx 버전 1.7.11과 Nginx Plus Release 7 부터 적용되었다. 이 thread pool이 무엇인지, 어떠게 사용되는지는 뒤에서 다루자. 우선 현재 Nginx가 가지고 있는 문제점들을 살펴보자.
Nginx Plus R7에 대해 알고 싶다면 Announcing NGINX Plus R7을 참고하자
NGINX Plus R7의 새로운 기능을 자세히 알고 싶다면 다음 글들을 참고하자
- HTTP/2 Now Fully Supported in NGINX Plus
- Socket Sharding in NGINX
- The New NGINX Plus Dashboard in Release 7
- TCP Load Balancing in NGINX Plus R7
The Problem
Nginx가 가진 문제점에 대해 더 잘 이해하기 위해 Nginx의 동작 방식을 살펴보자.
통상적으로, Nginx는 이벤트 핸들러이다. 이 핸들러는 커널로부터 오는 커넥션에서 발생한 이벤트들과 관련된 정보들을 받는 컨트롤러다. 이런 정보들을 가지고 운영체제에게 어떤 일을 해야 하는지를 알리기 위해 운영체제에게 명령어를 준다. 사실상 Nginx가 운영체제를 조정해 모든 일을 하고, 운영체제는 그처 바이트를 읽고 보내는 일만 한다. 따라서 Nginx는 적시에 빠르게 대응해야 한다.
이 이벤트는 타임아웃, 소켓이 읽기또는 쓰기 준비 상태 중임을 알리는 알림, 에러 발생 알림일 수 있다. Nginx는 대량의 이벤트들을 받고 하나씩 처리하며 각 이벤트에 필요한 작업을 한다. 따라서 모든 작업은 단일 스레드 내의 큐상에서 간단한 루프로 완료된다. Nginx는 큐에서 이벤트를 꺼내와 소켓을 읽거나 씀음으로써 반응한다. 대개의 경우, 이 작업은 매우 빠르며 Nginx는 큐에 있는 모든 이벤트를 순식간에 처리한다.
하지만, 엄청나게 무거운 연산이 발생한다면 무슨일이 발생할까? 전체 이벤트 프로세싱 사이클이 연산 종료까지 멈춰있을 것이다.
따라서 blocking 연산은 이벤트를 관여하는 사이클 연산을 일정 시간 멈추게 하는 것이라 정의하자. 연산은 여러 이유로 blocking될 수 있다. 그리고 이련 연산들을 처리할 때는 사용가능한 시스템 리소스가 존재하고 큐에 존재하는 이벤트들이 이 리소스들을 사용할 수 있어도 워커 프로세스가 해당 이벤트에 대한 기타 연산을 할 수 없으며 다른 이벤트 역시 처리할 수 없다.
판매원 한명과 여러명의 고객이 매장에 있다고 해보자. 고객들은 줄(큐 처럼)서있다. 이 줄의 맨 앞 고객이 판매원에게 매장에 존재하지 않는 물건을 요구했다 하자. 그러면 판매원은 창고에 물건을 가지러 가는 동안 고객들은 줄을 서서 대기해야 한다. 이는 고객 경험을 크게 저하시킬 것이다. 고객들이 기다리는 시간은 물건을 창고에서 찾아 가져오는 시간 만큼 들어나는 반면 다른 고객들이 사야하는 물건은 스토어에 존재할 수도 있다.
메모리에 캐싱되지 않은(스토어에 존재하지 않는) 파일 을 읽어야 하는 요청이 왔을 때 디스크에서(창고에서) 파일을 읽어와야 하기 때문에 Nginx에서도 비슷한 상황이 발생한다. 하드드라이버는 느리다, 큐에서 대기중인 요청들은 하드드라이버를 조회하지 않을 수도 있다. 그럼에도 어쩔 수 없이 대기하고 있다. 이때문에 레이턴시는 증가하고 리소스를 전부 활용하지 못한다.
일부 운영체제의 경우 파일 읽기, 전송을 위한 비동기 인터페이스를 제공하고 Nginx는 이 인터페이스를 사용할 수 있다. FreeBSD가 이 예시에 해당한다. 하지만, 리눅스 환경도 같을 것이라는 보장을 할 수 없다. 비록 리눅스는 파일 읽기를 위한 비동기 인터페이스를 제공하지만, 중대한 문제점들을 가지고 있다. 이 문제들 중에는 파일 접근과 버퍼에 대한 정렬 요구사항이 있지만 Nginx는 이를 잘 처리하고 있다. 하지만 이 비동기 인터페이스가 0_DIRECT flag를 파일 디스크립터에 설정하는 것을 필요로 하는 것이 문제가 된다. 즉, 파일에 액세스할 때 메모리에 존재하는 캐시를 우회해 하드 디스크 로드가 증가한다. 이는 많은 경우를 최적화가 되지 않는다.
이 문제들을 해결하기 위해 thread pool이 Nginx 1.7.11과 Nginx Plus Release 7에서 소개되었다.
이제 thread pool과 동작 방식에 대해 살펴보자.
Thread Pools
다시 위에서 예시로 들었던 판매원 상황으로 돌아가 보자. 판매원이 상황을 개선하기 위해 배달 서비스를 고용했다 해보자. 이제 고객이 창고에 있는 물건을 필요로 할 때마다 판매원은 배달 서비스에게 이를 요청하면 된다. 그러면 배달 서비스가 물건을 창고로 가지러 가는 사이에 판매원은 다른 고객의 주문을 처리할 수 있다. 이제 원하는 상품이 스토어에 존재하지 않는 고객들만 배달을 기다리면 되고 다른 고객들은 원하는 물건을 즉시 받을 수 있다.
이 예시를 Nginx에 빗대어 보면, thread pool이 배달 서비스 역할을 한다고 볼 수 있다. Thread pool은 task queue와 task queue를 처리하는 여러개의 스레드로 구성되 있다. 워커 프로세스가 오랜 시간이 요구되는 연산을 해야 할 때, 직접 이 연산을 처리하는 대산 풀에 존재하는 큐에 작업을 배치해 사용 가능한 스레드에서 작업을 수행하고 처리한다.
이 방식은 큐가 하나 더 존재하는 것과 같다. 하지만 thread pool에 있는 task queue 속도는 특정 리소스에 의해 제한된다. 드라이브가 데이터를 생성하는 속도보다 더 빠르게 데이터를 읽어올 수 없다. 이제, 적어도 드라이브가 다른 이벤트들의 처리를 지연시키진 않는다. 오직 드라이브에 접근해야 하는 요청들만 대기하면 된다.
디스크에서 데이터를 읽어오는 연산은 blocking 연산의 대표적인 예시이다. 하지만, Nginx에서의 thread pool 구현은 주된 작업 처리 사이클에 적절하지 않은 작업들에 사용될 수 있다.
현재는 오직 3가지 기본 연산에만 thread pool로 연산을 넘기고 있다. 대부분의 운영체제에 존재하는 read() 시스템 호출, 리눅스에 존재하는 sendfile(), 임시 파일들(캐시 등)을 쓰기 위해 리눅스에서 사용되는 aio_write(). 앞으로도 thread pool 구현에 대한 테스트와 벤치마킹을 지속할 것이며, 명확한 이점이 존재한다면 앞으로의 버전에서 다른 연산들 역시 thread pool에 넘길 수 있게 할것이다.
aio_write() 시스템 호출의 thread pool 지원은 Nginx 1.9.13과 Nginx Plus R9에서 추가되었다.
Benchmarking
이제 이론에서 실전으로 넘어가 보자. Thread pool을 사용하는 것의 효율을 입증하기 위해 최악의 blocking 연산과 non-blocking 연산을 섞은 종합적인 벤치마크를 해보자.
이 벤치마크로 원하는 결과를 얻기 위해 메모리 만으로는 감당할 수 없는 양의 데이터 셋이 필요하다. 48GB 램을 가지고 있는 머신에서 4MB 파일로 256GB의 랜덤 데이털를 생성하고 Nginx 1.9.0이 이를 제공하게 벤치마크를 구성했다.
Nginx 환경 설정은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
worker_processes 16;
events {
accept_mutex off;
}
http {
include mime.types;
default_type application/octet-stream;
access_log off;
sendfile on;
sendfile_max_chunk 512k;
server {
listen 8000;
location / {
root /storage;
}
}
}
|
cs |
위 환경 설정에서 볼 수 있듯이, 퍼포먼스 향상을 위해 일부 설정을 적용했다. loggin과 accept_mutex를 비활성화 했으며, sendfile을 허용하고, sendfile_max_chunk를 설정했다. sendfile_max_chunk를 설정하면 Nginx가 모든 파일을 한번에 보내는 대신 512KB(위 설정에서 sendfile_max_chunk 양을 512KB로 설정했다)씩 잘라서 보내기 떄문에 blocking 연산인 sendfile()에 사용하는 최대 시간을 줄일 수 있다.
해당 Nginx가 설치된 컴퓨터는 Intel Xeon E5645(12 core, 24 HT-thread) 프로세스와 10-Gbps 네트워크 인터페이스를 가지고 있다. 디스크 서브시스템은 Western Digital WD1003FBYX 하드 드라이브로 구성되 있으며 RAID10 array로 배치되있다. 운영체제는 Ubuntu server 14.04.1 LTS를 사용하고 있다.
클라이언트는 두 개의 머신으로 이루어져 있다. 그 중 하나에서는 wrk가 Lua 스크립트를 사용해 로드를 생성한다. 이 스크립트는 200개의 동시 커넥션을 사용해 서버로부터 파일들을 무작위로 요청한다. 각 요청은 캐시 누락, disk에서의 blocking 읽기가 발생할 수 있다. 여기선 이 부하를 랜덤(random) 부하 라고 부르자.
또 다른 클라이언트 머신에서는 위에서 언급한 wrk의 복사본을 사용한다. 이 복사본은 50개의 동시 커넥션을 사용해 같은 파일을 여러번 요청한다. 이 파일은 빈번히 접근되기 때문에 항상 메모리에 존재하게 된다. 통상적인 상황에서는 Nginx가 이 요청을 빠르게 처리할 것이다. 하지만 만약 워커 프로세스가 다른 요청들에 의해 blocking 된 상황이라면 성능이 저하된다. 이 부하를 정적(constant) 부하라 하자.
퍼포먼스는 ifstat를 사용해 서버 시스템의 처리량을 측정하고 두 번쨰 클라이언트에서 wrk의 결과를 받아 측정할 것이다.
thread pool 사용 없이 실행한 첫 번째 실행은 다음과 같은 결과가 나왔다.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
% ifstat -bi eth2
eth2
Kbps in Kbps out
5531.24 1.03e+06
4855.23 812922.7
5994.66 1.07e+06
5476.27 981529.3
6353.62 1.12e+06
5166.17 892770.3
5522.81 978540.8
6208.10 985466.7
6370.79 1.12e+06
6123.33 1.07e+06
|
cs |
결과에서 확인할 수 있듯이 이 환경 설정으로는 총 1Gbps의 트래픽을 생성한다. top 명령어를 통해 모든 워커 프로세스들은 대부분의 연산 시간을 blocking I/O에 소비하는 것을 알 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
top - 10:40:47 up 11 days, 1:32, 1 user, load average: 49.61, 45.77 62.89
Tasks: 375 total, 2 running, 373 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.3 sy, 0.0 ni, 67.7 id, 31.9 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem: 49453440 total, 49149308 used, 304132 free, 98780 buffers
KiB Swap: 10474236 total, 20124 used, 10454112 free, 46903412 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4639 vbart 20 0 47180 28152 496 D 0.7 0.1 0:00.17 nginx
4632 vbart 20 0 47180 28196 536 D 0.3 0.1 0:00.11 nginx
4633 vbart 20 0 47180 28324 540 D 0.3 0.1 0:00.11 nginx
4635 vbart 20 0 47180 28136 480 D 0.3 0.1 0:00.12 nginx
4636 vbart 20 0 47180 28208 536 D 0.3 0.1 0:00.14 nginx
4637 vbart 20 0 47180 28208 536 D 0.3 0.1 0:00.10 nginx
4638 vbart 20 0 47180 28204 536 D 0.3 0.1 0:00.12 nginx
4640 vbart 20 0 47180 28324 540 D 0.3 0.1 0:00.13 nginx
4641 vbart 20 0 47180 28324 540 D 0.3 0.1 0:00.13 nginx
4642 vbart 20 0 47180 28208 536 D 0.3 0.1 0:00.11 nginx
4643 vbart 20 0 47180 28276 536 D 0.3 0.1 0:00.29 nginx
4644 vbart 20 0 47180 28204 536 D 0.3 0.1 0:00.11 nginx
4645 vbart 20 0 47180 28204 536 D 0.3 0.1 0:00.17 nginx
4646 vbart 20 0 47180 28204 536 D 0.3 0.1 0:00.12 nginx
4647 vbart 20 0 47180 28208 532 D 0.3 0.1 0:00.17 nginx
4631 vbart 20 0 47180 756 252 S 0.0 0.1 0:00.00 nginx
4634 vbart 20 0 47180 28208 536 D 0.0 0.1 0:00.11 nginx<
4648 vbart 20 0 25232 1956 1160 R 0.0 0.0 0:00.08 top
25921 vbart 20 0 121956 2232 1056 S 0.0 0.0 0:01.97 sshd
25923 vbart 20 0 40304 4160 2208 S 0.0 0.0 0:00.53 zsh
|
cs |
이 경우 CPU가 대부분의 시간 동한 유휴 상태이며 디스크 하위 시스템에 의해 트래픽이 제한되었다. wrk의 결과 역시 매우 낮게 나왔다.
1
2
3
4
5
6
7
8
|
Running 1m test @ http://192.0.2.1:8000/1/1/1
12 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 7.42s 5.31s 24.41s 74.73%
Req/Sec 0.15 0.36 1.00 84.62%
488 requests in 1.01m, 2.01GB read
Requests/sec: 8.08
Transfer/sec: 34.07MB
|
cs |
이 파일들은 메모리에서 제공되어야 했다. 위 결과에서 볼 수 있는 대량의 레이턴시들은 워커 프로세스들이 첫 번째 클라이언트에서 온 200개의 커넥션에 의해 생성된 드라이브에서 파일을 읽어오기 때문에 발생했다. 이로 인해 제때 요청들을 처리할 수 없다.
이제 thread pool을 사용해보자. 이를 위해 aio를 location 블록에 추가하자.
1
2
3
4
|
location / {
root /storage;
aio threads;
}
|
cs |
그 후, Nginx 설정을 다시 로드하자.
같은 방법으로 실험을 해보면 다음과 같은 결과를 볼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
% ifstat -bi eth2
eth2
Kbps in Kbps out
60915.19 9.51e+06
59978.89 9.51e+06
60122.38 9.51e+06
61179.06 9.51e+06
61798.40 9.51e+06
57072.97 9.50e+06
56072.61 9.51e+06
61279.63 9.51e+06
61243.54 9.51e+06
59632.50 9.50e+06
|
cs |
이제 서버는 9.5Gbps의 트래픽을 생성한다.
Nginx가 더 많은 트래픽을 생성할 수도 있지만, 네트워크 최대 용량에 도달했다. 따라서 이번 테스트에서는 Nginxrk 네트워크 인터페이의 한계로 인해 퍼포먼스가 한계에 붙이쳤다고 할 수 있다. 워커 프로세스들은 대부분의 시간을 sleeping 상태나 새로운 이벤트 대기에 사용한다(top의 S 상태에 있다).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
top - 10:43:17 up 11 days, 1:35, 1 user, load average: 172.71, 93.84, 77.90
Tasks: 376 total, 1 running, 375 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.2 us, 1.2 sy, 0.0 ni, 34.8 id, 61.5 wa, 0.0 hi, 2.3 si, 0.0 st
KiB Mem: 49453440 total, 49096836 used, 356604 free, 97236 buffers
KiB Swap: 10474236 total, 22860 used, 10451376 free, 46836580 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4654 vbart 20 0 309708 28844 596 S 9.0 0.1 0:08.65 nginx
4660 vbart 20 0 309748 28920 596 S 6.6 0.1 0:14.82 nginx
4658 vbart 20 0 309452 28424 520 S 4.3 0.1 0:01.40 nginx
4663 vbart 20 0 309452 28476 572 S 4.3 0.1 0:01.32 nginx
4667 vbart 20 0 309584 28712 588 S 3.7 0.1 0:05.19 nginx
4656 vbart 20 0 309452 28476 572 S 3.3 0.1 0:01.84 nginx
4664 vbart 20 0 309452 28428 524 S 3.3 0.1 0:01.29 nginx
4652 vbart 20 0 309452 28476 572 S 3.0 0.1 0:01.46 nginx
4662 vbart 20 0 309552 28700 596 S 2.7 0.1 0:05.92 nginx
4661 vbart 20 0 309464 28636 596 S 2.3 0.1 0:01.59 nginx
4653 vbart 20 0 309452 28476 572 S 1.7 0.1 0:01.70 nginx
4666 vbart 20 0 309452 28428 524 S 1.3 0.1 0:01.63 nginx
4657 vbart 20 0 309584 28696 592 S 1.0 0.1 0:00.64 nginx
4655 vbart 20 0 30958 28476 572 S 0.7 0.1 0:02.81 nginx
4659 vbart 20 0 309452 28468 564 S 0.3 0.1 0:01.20 nginx
4665 vbart 20 0 309452 28476 572 S 0.3 0.1 0:00.71 nginx
5180 vbart 20 0 25232 1952 1156 R 0.0 0.0 0:00.45 top
4651 vbart 20 0 20032 752 252 S 0.0 0.0 0:00.00 nginx
25921 vbart 20 0 121956 2176 1000 S 0.0 0.0 0:01.98 sshd
25923 vbart 20 0 40304 3840 2208 S 0.0 0.0 0:00.54 zsh
|
cs |
CPU 자원이 아직 많이 남아있는 것을 알 수 있다.
wrk 결과는 다음과 같다.
1
2
3
4
5
6
7
8
|
Running 1m test @ http://192.0.2.1:8000/1/1/1
12 threads and 50 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 226.32ms 392.76ms 1.72s 93.48%
Req/Sec 20.02 10.84 59.00 65.91%
15045 requests in 1.00m, 58.86GB read
Requests/sec: 250.57
Transfer/sec: 0.98GB
|
cs |
4MB 파일을 서비스하는 평균 시간이 7.42초에서 226.32 밀리 세컨드(33배 더 적다)로 줄어들었다. 초당 처리하는 요청 건수 또한 31배 늘었다(8건 -> 250건)
이 결과는 워커 프로세스가 읽기에서 blocking 되있는 동안, 요청들이 더이상 이벤트 큐에서 프로세싱을 기다리지 않고, 여유분의 thread 들에 의해 처리됨을 의미한다. 디스크 서브시스템을 사용해 첫 번째 클라이언트의 랜덤 부하를 처리할 수 있고 Nginx는 나머지 CPU 자원과 네트워크 용량으로 메모리에서 두 번째 클라이언트 요청을 처리한다.
Still Not a Silver Bullet
위 벤치마크에서 thread pool 사용이 퍼포먼스 향상을 이끈 이유는 대부분의 파일 전송과 읽기 연산이 하드드라이브에서 발생하지 않기 떄문이다. 만약 RAM에 데이터셋을 저장할 수 있는 충분한 공간이 존재한다면, 운영체제는 자주 사용하는 파일들을 캐싱할 것이다. 이를 page-cache라 부른다.
page-cache는 대부분의 일반적 상황에서 Nginx의 퍼포먼스를 좋게 만든다. page-cache에서 읽어오는 동작은 빠르며 blocking도 아니다. 즉, thread pool로 작업을 주는 것은 오버헤드가 존재한다.
따라서 만약 충분한 양의 RAM이 존재하고 데이터셋이 크지 않다면, Nginx는 thread pool 없이도 충분히 최적화된 방법을 사용하고 있다고 볼 수 있다.
Thread pool에게 읽기 연산을 전가하는 것은 특정 테스크에만 적용할 수 있는 테크닉이다. 자주 요청으로 들어오는 컨텐트들의 용량이 운영체제의 VM cache를 초과할 때 사용할 수 있다. 부하가 높은 Nginx 기반 스트리밍 서버가 이 경우에 해당한다.
읽기 연산을 thread pool로 전가하는 작업을 개선할 수 있으면 좋겠다. 이를 위해 필요한 파일들이 메모리에 존재하는지를 알아내는 효율적인 방법이며 후자의 경우(위 벤치마킹에서 정적 부하의 경우)만 다른 스레드에 테스크를 전가해야 한다.
다시 판매원 예시로 돌아가 보자. 현재 판매원은 고객이 요청한 아이템이 스토어에 존재하는지 알지 못한다. 따라서 모든 주문을 배달 서비스에 넘기거나 스스로 요청을 처리한다.
운영체제에 파일들이 메모리에 존재하는 지를 판단할 수 있는 기능이 없다는 것이 문제의 원인이다. 이 기능을 fincore() 시스템 호출로 201년에 리눅스에 추가하려 했지만 실패했다. 그 후 이 기능을 RFW_NONBLOCK 플래스와 preadv2() 시스템 호출로서 적용하려는 시도가 존재했었다(자세한 사항은 Nonblocking buffered file read operations와 Asynchronous buffered read operations를 참고하자). 이 패치들이 적용 될지는 아직 불투명하다.
한편으로는, FreeBSD 유저들은 이 문제에 대해 고민하지 않아도 된다. FreeBSD는 이미 충분히 좋은 파일 읽기 비동기 인터페이스를 가지고 있다. Free BSD 유저들은 thread pool 대신 이 인터페이스를 사용하면 된다.
Configuring Thread Pools
Thread pool을 사용해서 이익을 취할 수 있는 경우가 있다면, 다음과 같은 설정을 사용하면 된다.
이 설정은 쉽고 유연하다. 우선 Nginx 1.7.11 이상의 버전이 필요하다. configure 커멘드에 --with-threads 아규먼트를 함께 사용해 컴파일되야 한다. Nginx Plus를 사용한다면 Release 7 또는 그 이후 버전이 필요하다. 가장 간단한 경우 세팅이 매우 쉽다. 적절한 컨텍스트에 aio threads를 추가하기만 하면 된다.
1
2
|
# in the 'http', 'server', or 'location' context
aio threads;
|
cs |
이는 thread pool를 사요하기 위한 최소한의 세팅이다. 이 세팅은 다음 세팅을 함축한 것이다.
1
2
3
4
5
|
# in the 'main' context
thread_pool default threads=32 max_queue=65536;
# in the 'http', 'server', or 'location' context
aio threads=default;
|
cs |
이 설정은 default라 불리는 thread pool을 정의한다. Default thread pool은 32개의 워킹 스레드와 최대 65536개의 테스크를 담을 수 있는 테스크 큐를 가지고 있다. 만약 테스트 큐가 감당할 수 있는 테스크 양을 초과한다면 Ngixn는 요청을 reject하고 다음과 같은 로그를 내보낸다.
1
|
thread pool "NAME" queue overflow: N tasks waiting
|
cs |
이 에러는 스레드들 테스트 큐에 테스크가 추가되는 속도만큼 빠르게 테스크를 감당할 수 없을 수도 있다는 것을 의미한다. 큐 사이즈를 증가시켜서 이 문제를 해결할 수 있지만, 증가시켜도 문제가 해결되지 않는다면, 시스템이 그만큼의 요청을 감당할 수 없음을 의미한다.
위 설정에서 볼 수 있듯이 thread_pool을 사용해 스레드 갯수, 큐의 최대 길이, 특정 thread pool의 이름을 설정할 수 있다. thread_pool을 여러개 설정해서 독립된 thread pool을 여러개 성성하고 서로 다른 목적으로 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# in the 'main' context
thread_pool one threads=128 max_queue=0;
thread_pool two threads=32;
http {
server {
location /one {
aio threads=one;
}
location /two {
aio threads=two;
}
}
# ...
}
|
cs |
만약 max_queue 파라미터를 명시하지 않았다면 기본값인 65536을 사용하게 된다. max_queue의 값으로 0을 사용한다면, 설정한 스레드 갯수 만큼 테스크를 감당하게 된다. 즉, 테스크가 큐에서 대기하지 않는다.
이제 3개의 하드 드라이브를 사용하는 서버가 있다고 해보자. 이 서버를 백엔드에서 오는 모든 응답을 캐싱하는 chaching proxy로 사용한다 해보자. 예상되는 캐시 데이터의 양은 상요 가능한 램 용량보다 훨씬 크다. 이는 개인 용도의 CDN을 위한 캐싱 노드다. 이 경우 드라이브에서 최고 성능을 달성하는 것이 가장 중요하다.
이를 해결하는 한 가지 방법은 RAID 배열을 설정하는 것이다. 이 방식은 장단점이 존재한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
# We assume that each of the hard drives is mounted on one of these directories:
# /mnt/disk1, /mnt/disk2, or /mnt/disk3
# in the 'main' context
thread_pool pool_1 threads=16;
thread_pool pool_2 threads=16;
thread_pool pool_3 threads=16;
http {
proxy_cache_path /mnt/disk1 levels=1:2 keys_zone=cache_1:256m max_size=1024G
use_temp_path=off;
proxy_cache_path /mnt/disk2 levels=1:2 keys_zone=cache_2:256m max_size=1024G
use_temp_path=off;
proxy_cache_path /mnt/disk3 levels=1:2 keys_zone=cache_3:256m max_size=1024G
use_temp_path=off;
split_clients $request_uri $disk {
33.3% 1;
33.3% 2;
* 3;
}
server {
# ...
location / {
proxy_pass http://backend;
proxy_cache_key $request_uri;
proxy_cache cache_$disk;
aio threads=pool_$disk;
sendfile on;
}
}
}
|
cs |
이 설정에서 thread_pool은 각 디스크에 독립된 전용 thread pool을 정의한다. proxy_cache_path는 각 디스크에 독립된 전용 캐시를 정의한다.
split_clients 모듈은 캐시들 사이에서 로드밸런싱을 위해 사용된다.
use_temp_path=off 파라미터는 관련된 캐시 데이터가 위치한 디렉터리들에에 임시 파일들을 저장할 것을 의미한다. 이를 위해선 캐시를 업데이트 할 때 응답 데이터를 드라이브 간에서 복사하는 것을 피해야 한다.
이런 세팅은 드라이브들이 동시에 독립적으로 분리된 thread pool들과 상호작용하기 때문에 현재의 디스크 서브시스템을 최고의 퍼포먼스로 사용할 수 있다. 각 드라이브에는 16개의 독립된 스레드들과 파일 읽기, 전송을 위한 전용 테스트 큐가 제공된다.
이 예시는 Nginx가 얼마나 유연하게 하드웨어에 특화될 수 있는지를 보여준다. 이는 Nginx 사용자가 Nginx에게 머신, 데이터셋과 상호작용할 수 있는 가장 좋은 방법을 알려주는 것과 같다. 또 한, Nginx를 세부적으로 튜닝함으로써 소프트웨어, 운영체제, 하드웨어가 모든 시스템 리소스를 가능한 효율적인 방법으로 사용하게 하기 위해 최적화 시킬 수 있다.
출처 - Thread Pools in NGINX Boost Performance 9x!
'인프라 > nginx' 카테고리의 다른 글
[번역]Inside NGINX: How We Designed for Performance & Scale (1) | 2022.08.11 |
---|---|
Proxy server (0) | 2021.09.25 |
nginx (0) | 2021.09.19 |