NGINX의 스레드 풀을 사용하면 성능이 9배 향상됩니다

관리자
조회수 629



NGINX가 연결을 처리하는 데 비동기, 이벤트 기반 접근 방식을 사용한다는 것은 잘 알려진 사실입니다 . 즉, 각 요청에 대해 다른 전담 프로세스나 스레드를 생성하는 대신(기존 아키텍처를 사용하는 서버처럼) 하나의 작업자 프로세스에서 여러 연결과 요청을 처리합니다. 이를 달성하기 위해 NGINX는 비차단 모드에서 소켓과 함께 작동하고 epoll 및 kqueue 와 같은 효율적인 방법을 사용합니다 .

전체 가중치 프로세스의 수가 적고(일반적으로 CPU 코어당 하나만) 일정하기 때문에 메모리 소모가 훨씬 적고 CPU 사이클이 작업 전환에 낭비되지 않습니다. 이러한 접근 방식의 장점은 NGINX 자체의 예를 통해 잘 알려져 있습니다. 수백만 개의 동시 요청을 성공적으로 처리하고 매우 잘 확장됩니다.

각 프로세스는 추가 메모리를 소모하며, 프로세스 간의 각 전환은 CPU 사이클을 소모하고 L-캐시를 삭제합니다.

하지만 비동기 이벤트 기반 접근 방식에는 여전히 문제가 있습니다. 아니면, 제가 생각하기에는 "적"입니다. 그리고 적의 이름은 차단 입니다 . 안타깝게도 많은 타사 모듈이 차단 호출을 사용하고 사용자(그리고 때로는 모듈 개발자도)는 단점을 알지 못합니다. 차단 작업은 NGINX 성능을 망칠 수 있으므로 무슨 일이 있어도 피해야 합니다.

현재 공식 NGINX 코드에서도 모든 경우에 차단 작업을 피할 수는 없으며, 이 문제를 해결하기 위해 새로운 "스레드 풀" 메커니즘이 NGINX 버전 1.7.11 과 NGINX Plus 릴리스 7 에 구현되었습니다 . 이것이 무엇이고 어떻게 사용해야 하는지는 나중에 다루겠습니다. 이제 적과 마주해 봅시다.

편집자 - NGINX Plus R7 개요는 블로그에서 NGINX Plus R7 발표를 참조하세요.

NGINX Plus R7의 다른 새로운 기능에 대한 자세한 논의는 다음 관련 블로그 게시물을 참조하세요.

  • HTTP/2가 이제 NGINX Plus에서 완벽하게 지원됩니다.
  • NGINX의 소켓 샤딩
  • 릴리스 7의 새로운 NGINX Plus 대시보드<.htmla>
  • NGINX Plus R7의 TCP 부하 분산<.htmla>

 

문제

먼저, 문제를 더 잘 이해하기 위해 NGINX의 작동 방식에 대해 몇 가지 설명해 보겠습니다.

일반적으로 NGINX는 이벤트 핸들러, 즉 연결에서 발생하는 모든 이벤트에 대한 정보를 커널에서 수신한 다음 운영 체제에 수행할 작업에 대한 명령을 내리는 컨트롤러입니다. 사실 NGINX는 운영 체제를 조정하여 모든 어려운 작업을 수행하는 반면 운영 체제는 바이트를 읽고 보내는 일상적인 작업을 수행합니다. 따라서 NGINX가 빠르고 시기적절하게 응답하는 것이 매우 중요합니다.

NGINX-이벤트-루프2작업자 프로세스는 커널에서 이벤트를 수신하고 처리합니다.

이벤트는 시간 초과, 읽거나 쓸 준비가 된 소켓에 대한 알림, 발생한 오류에 대한 알림일 수 있습니다. NGINX는 여러 이벤트를 수신한 다음 하나씩 처리하여 필요한 작업을 수행합니다. 따라서 모든 처리가 한 스레드의 대기열을 통한 간단한 루프에서 수행됩니다. NGINX는 대기열에서 이벤트를 큐에서 제거한 다음 소켓을 쓰거나 읽는 등의 방식으로 이에 반응합니다. 대부분의 경우 이 작업은 매우 빠르며(아마도 메모리에 일부 데이터를 복사하는 데 몇 번의 CPU 사이클만 필요할 수 있음) NGINX는 대기열의 모든 이벤트를 순식간에 처리합니다.

이벤트 큐 처리 주기모든 처리가 하나의 스레드에 의한 간단한 루프로 수행됩니다.

하지만 길고 무거운 작업이 발생한 경우 어떻게 될까요? 이벤트 처리의 전체 사이클이 이 작업이 완료될 때까지 기다리면서 멈춥니다.

따라서 "차단 작업"이란 상당한 시간 동안 이벤트 처리 주기를 중단하는 모든 작업을 의미합니다. 작업은 다양한 이유로 차단될 수 있습니다. 예를 들어, NGINX는 길고 CPU 집약적인 처리로 바쁠 수도 있고, 리소스(예: 하드 드라이브, 동기 방식으로 데이터베이스에서 응답을 받는 뮤텍스 또는 라이브러리 함수 호출 등)에 액세스하기 위해 기다려야 할 수도 있습니다. 중요한 점은 이러한 작업을 처리하는 동안 작업자 프로세스는 다른 작업을 수행할 수 없고 다른 이벤트를 처리할 수 없다는 것입니다. 사용 가능한 시스템 리소스가 더 많고 대기열의 일부 이벤트가 해당 리소스를 활용할 수 있더라도 마찬가지입니다.

매장에 긴 줄이 서 있는 세일즈맨을 상상해 보세요. 줄을 선 첫 번째 사람이 매장에는 없지만 창고에 있는 것을 요청합니다. 세일즈맨은 창고로 가서 상품을 배달합니다. 이제 줄을 선 모든 사람이 이 배달을 위해 몇 시간을 기다려야 하며 줄을 선 모든 사람이 불만스러워합니다. 사람들의 반응을 상상해 보세요. 줄을 선 모든 사람의 대기 시간은 이 시간만큼 늘어나지만, 그들이 사려고 하는 물건은 바로 매장에 있을 수도 있습니다.

줄을 서 있는 모든 사람은 첫 번째 사람의 주문을 기다려야 합니다.

NGINX에서 메모리에 캐시되지 않았지만 디스크에서 읽어야 하는 파일을 읽으려고 할 때 거의 같은 상황이 발생합니다. 하드 드라이브는 느리고(특히 회전하는 드라이브) 큐에서 기다리는 다른 요청은 드라이브에 액세스할 필요가 없을 수 있지만 어쨌든 기다려야 합니다. 결과적으로 대기 시간이 증가하고 시스템 리소스가 완전히 활용되지 않습니다.

단 하나의 차단 작업만으로도 이후의 모든 작업이 상당한 시간 동안 지연될 수 있습니다.

일부 운영 체제는 파일을 읽고 보내기 위한 비동기 인터페이스를 제공하며 NGINX는 이 인터페이스를 사용할 수 있습니다( aio 지시어 참조). 여기서 좋은 예는 FreeBSD입니다. 안타깝게도 Linux에 대해서는 마찬가지라고 말할 수 없습니다. Linux는 파일을 읽기 위한 일종의 비동기 인터페이스를 제공하지만 몇 가지 중요한 단점이 있습니다. 그 중 하나는 파일 액세스 및 버퍼에 대한 정렬 요구 사항이지만 NGINX는 이를 잘 처리합니다. 하지만 두 번째 문제는 더 심각합니다. 비동기 인터페이스는 O_DIRECT파일 설명자에 플래그를 설정해야 하므로 파일에 대한 모든 액세스가 메모리의 캐시를 우회하고 하드 디스크의 부하를 증가시킵니다. 이는 분명히 많은 경우에 최적이 되지 않습니다.

특히 이 문제를 해결하기 위해 NGINX 1.7.11 및 NGINX Plus 릴리스 7에 스레드 풀이 도입되었습니다.

이제 스레드 풀이 무엇이고 어떻게 작동하는지 자세히 알아보겠습니다.


스레드 풀

먼 창고에서 상품을 배달하는 우리의 불쌍한 판매 보조원으로 돌아가 봅시다. 하지만 그는 더 똑똑해졌고(아니면 화난 고객 무리에게 구타당한 후에 더 똑똑해졌을까요?) 배달 서비스를 고용했습니다. 이제 누군가가 먼 창고에서 무언가를 요청하면 창고에 직접 가는 대신 배달 서비스에 주문을 내리면 그들이 주문을 처리하고 판매 보조원은 다른 고객에게 계속 서비스를 제공합니다. 따라서 상품이 매장에 없는 고객만 배송을 기다리고 다른 고객은 즉시 서비스를 받을 수 있습니다.

배달 서비스에 주문을 전달하면 대기열이 해제됩니다.

NGINX의 관점에서 스레드 풀은 전달 서비스의 기능을 수행합니다. 이는 작업 큐와 큐를 처리하는 여러 스레드로 구성됩니다. 작업자 프로세스가 잠재적으로 긴 작업을 수행해야 할 때, 작업을 스스로 처리하는 대신 풀의 큐에 작업을 넣고, 여기서 모든 자유 스레드가 가져와서 처리할 수 있습니다.

스레드 풀은 느린 작업을 별도의 작업 세트에 할당하여 애플리케이션 성능을 높이는 데 도움이 됩니다.작업자 프로세스는 차단 작업을 스레드 풀로 오프로드합니다.

그러면 다른 대기열이 있는 것 같습니다. 그렇죠. 하지만 이 경우 대기열은 특정 리소스에 의해 제한됩니다. 드라이브가 데이터를 생성할 수 있는 속도보다 더 빠르게 드라이브에서 읽을 수 없습니다. 이제 적어도 드라이브는 다른 이벤트 처리를 지연시키지 않고 파일에 액세스해야 하는 요청만 대기합니다.

"디스크에서 읽기" 작업은 차단 작업의 가장 일반적인 예로 사용되지만 실제로 NGINX의 스레드 풀 구현은 주 작업 주기에서 처리하기에 적합하지 않은 모든 작업에 사용될 수 있습니다.

현재, 스레드 풀로의 오프로드는 세 가지 필수 작업에 대해서만 구현됩니다. read()대부분 운영 체제의 syscall, sendfile()Linux, 그리고 aio_write()캐시와 같은 임시 파일을 쓸 때 사용되는 Linux입니다. 우리는 구현을 계속 테스트하고 벤치마킹할 것이며, 명확한 이점이 있다면 향후 릴리스에서 다른 작업을 스레드 풀로 오프로드할 수 있습니다.

편집기 - NGINX 1.9.13 및 NGINX Plus R9aio_write() 에서 syscall에 대한 지원이 추가되었습니다 .


벤치마킹

이제 이론에서 실천으로 옮길 때입니다. 스레드 풀을 사용하는 효과를 보여주기 위해 블로킹 및 비블로킹 작업의 최악의 혼합을 시뮬레이션하는 합성 벤치마크를 수행합니다.

메모리에 맞지 않는 데이터 세트가 필요합니다. 48GB RAM이 있는 머신에서 4MB 파일에 256GB의 임의 데이터를 생성한 다음 NGINX 1.9.0을 구성하여 제공했습니다.

구성은 매우 간단합니다.

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;
        }
    }
}

보시다시피, 더 나은 성능을 얻기 위해 약간의 튜닝이 이루어졌습니다. logging및 는 accept_mutex비활성화되었고, sendfile활성화되었으며, sendfile_max_chunk설정되었습니다. 마지막 지시문은 NGINX가 전체 파일을 한 번에 보내려고 하지 않고 512KB 청크로 보내기 때문에 호출 차단에 소요되는 최대 시간을 줄일 수 있습니다 sendfile().

이 머신은 Intel Xeon E5645(12개 코어, 총 24개 HT 스레드) 프로세서 2개와 10Gbps 네트워크 인터페이스를 갖추고 있습니다. 디스크 하위 시스템은 RAID10 배열로 배열된 4개의 Western Digital WD1003FBYX 하드 드라이브로 표현됩니다. 이 모든 하드웨어는 Ubuntu Server 14.04.1 LTS로 구동됩니다.

벤치마크를 위한 부하 생성기 및 NGINX 구성

클라이언트는 동일한 사양을 가진 두 대의 머신으로 표현됩니다. 이 머신 중 하나에서 Lua 스크립트를 사용하여 로드를 생성합니다. 스크립트는 200개의 병렬 연결을 사용하여 무작위 순서로 서버에서 파일을 요청하고 각 요청은 캐시 미스와 디스크에서 차단 읽기로 이어질 가능성이 높습니다. 이 로드를 무작위wrk 로드 라고 부르겠습니다 .

두 번째 클라이언트 머신에서 50개의 병렬 연결을 사용하여 동일한 파일을 여러 번 요청하는 또 다른 사본을 실행합니다 . 이 파일은 자주 액세스되므로 항상 메모리에 남아 있습니다. 일반적인 상황에서 NGINX는 이러한 요청을 매우 빠르게 처리하지만 작업자 프로세스가 다른 요청으로 차단되면 성능이 떨어집니다. 이 부하를 상수wrk 부하라고 부르겠습니다 .

성능은 두 번째 클라이언트를 사용하여 서버 머신의 처리량을 모니터링 하고 두 번째 클라이언트에서 결과를 ifstat얻어 측정됩니다.wrk

이제 스레드 풀 없이 처음 실행하면 그다지 흥미로운 결과가 나오지 않습니다.

% ifstat -bi eth2eth2
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

보시다시피, 이 구성을 사용하면 서버는 총 약 1Gbps의 트래픽을 생성할 수 있습니다. 의 출력에서 top모든 작업자 프로세스가 대부분의 시간을 I/O 차단에 소비하는 것을 볼 수 있습니다(상태에 있음 D):

top - 10:40:47 up 11 days,  1:32,  1 user,  load average: 49.61, 45.77 62.89Tasks: 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

이 경우 처리량은 디스크 서브시스템에 의해 제한되는 반면 CPU는 대부분 유휴 상태입니다. 결과 wrk도 매우 낮습니다.

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

그리고 기억하세요, 이것은 메모리에서 제공되어야 하는 파일에 대한 것입니다! 지나치게 큰 대기 시간은 모든 작업자 프로세스가 첫 번째 클라이언트의 200개 연결로 생성된 무작위 부하를 제공하기 위해 드라이브에서 파일을 읽는 데 바쁘고, 적절한 시간에 요청을 처리할 수 없기 때문입니다.

이제 스레드 풀을 작동시킬 시간입니다. 이를 위해 블록 aio threads에 지시문을 추가하기만 하면 됩니다 location.

location / {    root /storage;
    aio threads;
}

NGINX에 구성을 다시 로드하도록 요청합니다.

그 후에 우리는 테스트를 반복합니다:

% ifstat -bi eth2eth2
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

이제 우리 서버는 스레드 풀 없이 약 1Gbps를 생성하는 데 비해 9.5Gbps를 생성합니다!

아마도 더 많이 생산할 수 있겠지만, 이미 실제 최대 네트워크 용량에 도달했기 때문에 이 테스트에서 NGINX는 네트워크 인터페이스에 의해 제한됩니다. 작업자 프로세스는 대부분의 시간을 잠자고 새 이벤트를 기다리는 데 보냅니다(상태 S 는 입니다 top).

top - 10:43:17 up 11 days,  1:35,  1 user,  load average: 172.71, 93.84, 77.90Tasks: 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

여전히 CPU 리소스가 충분합니다.

결과 wrk:

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

4MB 파일을 제공하는 데 걸리는 평균 시간이 7.42초에서 226.32밀리초(33배 감소)로 단축되었고, 초당 요청 수는 31배(250개 대 8개) 증가했습니다!

설명은 워커 프로세스가 읽기에서 차단되는 동안 더 이상 이벤트 큐에서 처리를 기다리지 않고, 자유 스레드에서 처리된다는 것입니다. 디스크 서브시스템이 첫 번째 클라이언트 머신에서 랜덤 로드를 처리할 수 있는 최선을 다해 작업을 수행하는 한, NGINX는 나머지 CPU 리소스와 네트워크 용량을 사용하여 메모리에서 두 번째 클라이언트의 요청을 처리합니다.


아직도 준비된 것은 아니다

모든 차단 작업에 대한 두려움과 몇 가지 흥미로운 결과를 겪은 후, 여러분 대부분은 이미 서버에서 스레드 풀을 구성할 것입니다. 서두르지 마세요.

사실, 다행히도 대부분의 읽기 및 보내기 파일 작업은 느린 하드 드라이브를 다루지 않습니다. 데이터 세트를 저장할 충분한 RAM이 있다면 운영 체제는 자주 사용되는 파일을 소위 "페이지 캐시"에 캐시할 만큼 똑똑할 것입니다.

페이지 캐시는 꽤 잘 작동하며 NGINX가 거의 모든 일반적인 사용 사례에서 뛰어난 성능을 보여줄 수 있게 해줍니다. 페이지 캐시에서 읽는 것은 매우 빠르며 아무도 그러한 작업을 "차단"이라고 부를 수 없습니다. 반면에 스레드 풀로 오프로드하는 데는 약간의 오버헤드가 있습니다.

따라서 적당한 양의 RAM이 있고 작업 데이터 세트가 크지 않다면 NGINX는 스레드 풀을 사용하지 않고도 가장 최적의 방식으로 작동합니다.

읽기 작업을 스레드 풀로 오프로드하는 것은 매우 구체적인 작업에 적용할 수 있는 기술입니다. 자주 요청되는 콘텐츠의 양이 운영 체제의 VM 캐시에 맞지 않는 경우에 가장 유용합니다. 예를 들어, 부하가 많이 걸리는 NGINX 기반 스트리밍 미디어 서버의 경우가 그렇습니다. 이것이 벤치마크에서 시뮬레이션한 상황입니다.

읽기 작업을 스레드 풀로 오프로드하는 것을 개선할 수 있다면 좋을 텐데요. 필요한 파일 데이터가 메모리에 있는지 없는지 알 수 있는 효율적인 방법만 있으면 되고, 후자의 경우에만 읽기 작업을 별도의 스레드로 오프로드해야 합니다.

판매 비유로 돌아가면, 현재 판매원은 요청한 품목이 매장에 있는지 알 수 없고 항상 모든 주문을 배달 서비스에 전달하거나 항상 직접 처리해야 합니다.

원인은 운영 체제에 이 기능이 없다는 것입니다. Linux에 syscall로 추가하려는 첫 시도는 fincore()2010년이었지만 그렇게 되지 않았습니다. 나중에 플래그를 preadv2()사용하여 새로운 syscall 로 구현하려는 시도가 여러 번 있었습니다 RWF_NONBLOCK( 자세한 내용은 LWN.net에서 Nonblocking buffered file read operations 및 Asynchronous buffered read operations 참조 ). 이러한 모든 패치의 운명은 여전히 불분명합니다. 여기서 슬픈 점은 이러한 패치가 아직 커널에 수용되지 않은 주된 이유가 지속적인 bikeshedding 인 것 같다는 것입니다 .

반면에 FreeBSD 사용자는 전혀 걱정할 필요가 없습니다. FreeBSD는 이미 파일을 읽기 위한 충분히 좋은 비동기 인터페이스를 가지고 있는데, 스레드 풀 대신 사용해야 합니다.


스레드 풀 구성

따라서 사용 사례에서 스레드 풀을 사용하면 어느 정도 이점을 얻을 수 있다고 확신한다면 이제 구성에 대해 자세히 알아볼 차례입니다.

구성은 매우 쉽고 유연합니다. 가장 먼저 해야 할 일은 명령 --with-threads에 대한 인수 로 컴파일된 NGINX 버전 1.7.11 이상입니다 configure. NGINX Plus 사용자는 릴리스 7 이상이 필요합니다. 가장 간단한 경우 구성은 매우 단순해 보입니다. 필요한 것은 적절한 컨텍스트에 지시문을 포함하는 것뿐입니다 .aio threads

# in the 'http', 'server', or 'location' contextaio threads;

이것은 스레드 풀의 최소 가능한 구성입니다. 사실, 그것은 다음 구성의 짧은 버전입니다.

# in the 'main' contextthread_pool default threads=32 max_queue=65536;
 
# in the 'http', 'server', or 'location' context
aio threads=default;

32개의 작업 스레드와 65536개의 작업 대기열에 대한 최대 길이를 갖는 default 라는 스레드 풀을 정의합니다 . 작업 대기열이 과부하되면 NGINX는 요청을 거부하고 이 오류를 기록합니다.

thread pool "NAME" queue overflow: N tasks waiting

이 오류는 스레드가 큐에 추가되는 것만큼 빠르게 작업을 처리할 수 없다는 것을 의미합니다. 최대 큐 크기를 늘려 볼 수 있지만, 그래도 도움이 되지 않는다면 시스템이 그렇게 많은 요청을 처리할 수 없다는 것을 나타냅니다.

이미 알아차리셨듯이, thread_pool지시문을 사용하면 스레드 수, 대기열의 최대 길이, 특정 스레드 풀의 이름을 구성할 수 있습니다. 마지막은 여러 개의 독립적인 스레드 풀을 구성하여 구성 파일의 다른 위치에서 사용하여 다양한 용도로 사용할 수 있음을 의미합니다.

# 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;
        }

    }
    # ...
}

매개변수가 지정 되지 max_queue않으면 기본적으로 값 65536이 사용됩니다. 표시된 대로 max_queue0으로 설정할 수 있습니다. 이 경우 스레드 풀은 구성된 스레드 수만큼만 작업을 처리할 수 있습니다. 큐에서 대기하는 작업은 없습니다.

이제 하드 드라이브 3개가 있는 서버가 있다고 가정해 보겠습니다. 이 서버를 백엔드의 모든 응답을 캐시하는 "캐싱 프록시"로 작동시키고 싶습니다. 캐시된 데이터의 예상 양은 사용 가능한 RAM을 훨씬 초과합니다. 실제로는 개인 CDN의 캐싱 노드입니다. 물론 이 경우 가장 중요한 것은 드라이브에서 최대 성능을 달성하는 것입니다.

여러분의 옵션 중 하나는 RAID 어레이를 구성하는 것입니다. 이 접근 방식에는 장단점이 있습니다. 이제 NGINX를 사용하면 다른 하나를 선택할 수 있습니다.

# 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;
        }
    }
}

이 구성에서 thread_pool지침은 각 디스크에 대한 전용 독립 스레드 풀을 정의하고, proxy_cache_path지침은 각 디스크에 대한 전용 독립 캐시를 정의합니다.

해당 split_clients모듈은 캐시 간(그리고 결과적으로 디스크 간) 부하 분산에 사용되며, 이 작업에 완벽하게 적합합니다.

use_temp_path=off지시문 에 대한 매개변수는 NGINX proxy_cache_path가 해당 캐시 데이터가 있는 동일한 디렉토리에 임시 파일을 저장하도록 지시합니다. 캐시를 업데이트할 때 하드 드라이브 간에 응답 데이터를 복사하지 않도록 하는 데 필요합니다.

이 모든 것을 합치면 NGINX가 별도의 스레드 풀을 통해 드라이브와 병렬로 독립적으로 상호 작용하기 때문에 현재 디스크 서브시스템에서 최대 성능을 얻을 수 있습니다. 각 드라이브는 파일을 읽고 보내기 위한 전용 작업 대기열이 있는 16개의 독립 스레드에서 제공됩니다.

저는 귀하의 고객이 이러한 맞춤형 접근 방식을 좋아할 것이라고 확신합니다. 귀하의 하드 드라이브도 이를 좋아하도록 하십시오.

이 예는 NGINX가 하드웨어에 맞게 얼마나 유연하게 조정될 수 있는지 잘 보여줍니다. 마치 NGINX에 머신과 데이터 세트와 상호 작용하는 가장 좋은 방법에 대한 지침을 제공하는 것과 같습니다. 그리고 사용자 공간에서 NGINX를 미세 조정하면 소프트웨어, 운영 체제, 하드웨어가 가장 최적의 모드로 함께 작동하여 모든 시스템 리소스를 가능한 한 효과적으로 활용할 수 있습니다.


결론

요약하자면, 스레드 풀은 NGINX의 잘 알려지고 오랜 적대 세력인 블로킹을 제거함으로써 NGINX의 성능을 새로운 수준으로 끌어올리는 훌륭한 기능입니다. 특히, 정말 큰 볼륨의 콘텐츠를 다룰 때 더욱 그렇습니다.

그리고 더 많은 것이 나올 것입니다. 이전에 언급했듯이, 이 완전히 새로운 인터페이스는 성능 저하 없이 길고 차단된 모든 작업을 오프로드할 수 있는 잠재력을 가지고 있습니다. NGINX는 대량의 새로운 모듈과 기능을 보유한다는 측면에서 새로운 지평을 엽니다. 많은 인기 있는 라이브러리는 여전히 비동기 비차단 인터페이스를 제공하지 않아 이전에는 NGINX와 호환되지 않았습니다. 우리는 일부 라이브러리의 자체 비차단 프로토타입을 개발하는 데 많은 시간과 리소스를 투자할 수 있지만, 항상 노력할 가치가 있을까요? 이제 스레드 풀이 탑재되어 이러한 라이브러리를 비교적 쉽게 사용할 수 있으며, 성능에 영향을 미치지 않고 이러한 모듈을 만들 수 있습니다.



위 내용과 같이 NGINX Plus 를 활용하여 Demo 가 필요하시면 하단의 전문가에게 상담받기 버튼을 클릭해주세요



전문가에게 상담받기