OpenResty에서 사용되는 NGINX 지식

API7.ai

September 17, 2022

OpenResty (NGINX + Lua)

이전 글을 통해 OpenResty에 대한 일반적인 지식을 얻으셨을 것입니다. 다음 몇 편의 글에서는 OpenResty의 두 기둥인 NGINX와 LuaJIT에 대해 설명하며, 이러한 기초를 마스터함으로써 OpenResty를 더 잘 이해할 수 있도록 도와드리겠습니다.

오늘은 NGINX부터 시작하겠습니다. 여기서는 OpenResty에서 사용될 수 있는 NGINX의 기본적인 내용만 소개할 것이며, 이는 NGINX의 아주 작은 부분에 불과합니다.

설정과 관련하여 OpenResty 개발에서 주의해야 할 점은 다음과 같습니다.

  • nginx.conf를 최소한으로 설정하세요.
  • if, set, rewrite 등 여러 지시어의 조합을 피하세요.
  • Lua 코드로 해결할 수 있는 경우에는 NGINX 설정, 변수, 모듈을 사용하지 마세요.

이러한 방법은 가독성, 유지보수성, 확장성을 극대화할 것입니다. 다음 NGINX 설정은 설정을 코드로 사용하는 전형적인 나쁜 예시입니다.

location ~ ^/mobile/(web/app.htm) {
    set $type $1;
    set $orig_args $args;
    if ( $http_user_Agent ~ "(iPhone|iPad|Android)" ) {
        rewrite  ^/mobile/(.*) http://touch.foo.com/mobile/$1 last;
    }
    proxy_pass http://foo.com/$type?$orig_args;
}

이것은 OpenResty로 개발할 때 피해야 할 사항입니다.

NGINX 설정

NGINX는 설정 파일을 통해 동작을 제어하며, 이는 간단한 DSL로 생각할 수 있습니다. NGINX는 프로세스가 시작될 때 설정을 읽어 메모리에 로드합니다. 설정 파일을 수정하면 NGINX를 재시작하거나 리로드해야 하며, NGINX가 설정 파일을 다시 읽을 때까지 기다려야 새로운 설정이 적용됩니다. NGINX의 상용 버전만이 런타임에 일부 동적 기능을 API 형태로 제공합니다.

다음 설정부터 시작해 보겠습니다. 이는 매우 간단합니다.

worker_processes auto;

pid logs/nginx.pid;
error_log logs/error.log notice;

worker_rlimit_nofile 65535;

events {
    worker_connections 16384;
}

http {
    server {
        listen 80;
        listen 443 ssl;

        location / {
            proxy_pass https://foo.com;
        }
    }
}

stream {
    server {
        listen 53 udp;
    }
}

그러나 이러한 간단한 설정에도 몇 가지 기본 개념이 포함되어 있습니다.

첫째, 각 지시어는 컨텍스트를 가지며, 이는 NGINX 설정 파일에서의 범위를 의미합니다.

최상위는 main이며, 여기에는 특정 비즈니스와 관련 없는 지시어가 포함됩니다. 예를 들어, worker_processes, pid, error_log는 모두 main 컨텍스트에 속합니다. 또한, 컨텍스트 간에는 계층적 관계가 있습니다. 예를 들어, location의 컨텍스트는 server, server의 컨텍스트는 http, http의 컨텍스트는 main입니다.

지시어는 잘못된 컨텍스트에서 실행될 수 없습니다. NGINX는 시작 시 nginx.conf가 합법적인지 확인합니다. 예를 들어, listen 80;을 server 컨텍스트에서 main 컨텍스트로 변경하고 NGINX 서비스를 시작하면 다음과 같은 오류가 발생합니다.

"listen" directive is not allowed here ......

둘째, NGINX는 HTTP 요청과 HTTPS 트래픽뿐만 아니라 UDP 및 TCP 트래픽도 처리할 수 있습니다. L7은 HTTP에, L4는 Stream에 있습니다. OpenResty에서 lua-nginx-modulestream-lua-nginx-module은 각각 이 두 가지에 대응합니다.

여기서 주의할 점은 OpenResty가 NGINX의 모든 기능을 지원하지 않는다는 것입니다. OpenResty의 버전은 NGINX와 일치하므로 쉽게 식별할 수 있습니다.

위의 nginx.conf에 포함된 설정 지시어는 NGINX 코어 모듈 ngx_core_module, ngx_http_core_module, ngx_stream_core_module에 있으며, 클릭하여 구체적인 문서를 확인할 수 있습니다.

MASTER-WORKER 모드

설정 파일을 이해한 후, NGINX의 다중 프로세스 모드를 살펴보겠습니다(아래 그림 참조). 보시다시피, NGINX가 시작되면 Master 프로세스와 여러 Worker 프로세스(또는 하나의 Worker 프로세스, 설정에 따라 다름)가 생성됩니다.

NGINX Worker Mode

먼저, Master 프로세스는 이름에서 알 수 있듯이 "관리자" 역할을 하며, 클라이언트의 요청을 처리하지 않습니다. 이는 Worker 프로세스를 관리하며, 관리자의 신호를 받고 Worker의 상태를 모니터링합니다. Worker 프로세스가 비정상적으로 종료되면 Master 프로세스는 새로운 Worker 프로세스를 재시작합니다.

Worker 프로세스는 클라이언트의 요청을 처리하는 "실제 작업자"입니다. 이들은 Master 프로세스에서 포크되어 서로 독립적입니다. 이 다중 프로세스 모델은 Apache의 다중 스레드 모델보다 훨씬 더 발전되어 있으며, 스레드 간 잠금이 없고 디버깅이 쉽습니다. 프로세스가 충돌하여 종료되더라도 일반적으로 다른 Worker 프로세스의 작업에 영향을 미치지 않습니다.

OpenResty는 NGINX의 Master-Worker 모델에 독특한 특권 프로세스를 추가합니다. 이 프로세스는 어떤 포트도 리스닝하지 않으며, NGINX Master 프로세스와 동일한 권한을 가지고 있어 로컬 디스크 파일에 대한 쓰기 작업과 같은 높은 권한이 필요한 작업을 수행할 수 있습니다.

특권 프로세스가 NGINX 바이너리 핫 업그레이드 메커니즘과 함께 작동하면 OpenResty는 외부 프로그램에 의존하지 않고도 전체 바이너리를 실시간으로 자체 업그레이드할 수 있습니다.

외부 프로그램에 대한 의존성을 줄이고 OpenResty 프로세스 내에서 문제를 해결하려는 노력은 배포를 용이하게 하고, 운영 및 유지보수 비용을 줄이며, 프로그램 오류의 확률을 낮춥니다. OpenResty의 특권 프로세스와 ngx.pipe는 모두 이를 위한 것입니다.

실행 단계

실행 단계는 NGINX의 필수 기능 중 하나이며, OpenResty의 구체적인 구현과 밀접한 관련이 있습니다. NGINX에는 11개의 실행 단계가 있으며, 이는 ngx_http_core_module.h의 소스 코드에서 확인할 수 있습니다.

typedef enum {
    NGX_HTTP_POST_READ_PHASE = 0,

    NGX_HTTP_SERVER_REWRITE_PHASE,

    NGX_HTTP_FIND_CONFIG_PHASE,
    NGX_HTTP_REWRITE_PHASE,
    NGX_HTTP_POST_REWRITE_PHASE,

    NGX_HTTP_PREACCESS_PHASE,

    NGX_HTTP_ACCESS_PHASE,
    NGX_HTTP_POST_ACCESS_PHASE,

    NGX_HTTP_PRECONTENT_PHASE,

    NGX_HTTP_CONTENT_PHASE,

    NGX_HTTP_LOG_PHASE
} ngx_http_phases;

이 11단계의 역할에 대해 더 알고 싶다면 NGINX 문서를 읽어보시기 바랍니다. 여기서는 더 이상 설명하지 않겠습니다.

우연히도, OpenResty도 NGINX 단계와 관련된 11개의 *_by_lua 지시어를 가지고 있으며, 아래 그림과 같습니다(lua-nginx-module 문서에서 가져옴).

Order of Lua NGINX Module Directives

init_by_luaMaster 프로세스가 생성될 때만 실행되며, init_worker_by_lua는 각 Worker 프로세스가 생성될 때만 실행됩니다. 다른 *_by_lua 명령어는 클라이언트 요청에 의해 트리거되어 반복적으로 실행됩니다.

따라서 init_by_lua 단계에서 Lua 모듈과 공개 읽기 전용 데이터를 미리 로드하여 OS의 COW(copy on write) 기능을 활용하여 메모리를 절약할 수 있습니다.

대부분의 작업은 content_by_lua 내에서 수행할 수 있지만, 다음과 같이 기능에 따라 분할하는 것을 권장합니다.

  • set_by_lua: 변수 설정.
  • rewrite_by_lua: 전달, 리디렉션 등.
  • access_by_lua: 접근, 권한 등.
  • content_by_lua: 반환 내용 생성.
  • header_filter_by_lua: 응답 헤더 필터링 처리.
  • body_filter_by_lua: 응답 본문 필터링 처리.
  • log_by_lua: 로깅.

이렇게 분할하는 이점을 보여주기 위해 예를 들어보겠습니다. 외부에 많은 평문 API가 제공되고 있다고 가정하고, 이제 사용자 정의 암호화 및 복호화 로직을 추가해야 한다고 가정해 보겠습니다. 그렇다면 모든 API의 코드를 변경해야 할까요?

location /mixed {
    content_by_lua '...';
}

물론 아닙니다. 단계 기능을 사용하면 access 단계에서 복호화하고 body filter 단계에서 암호화할 수 있으며, 원래 content 단계의 코드를 변경할 필요가 없습니다.

location /mixed {
    access_by_lua '...';
    content_by_lua '...';
    body_filter_by_lua '...';
}

NGINX 바이너리 실시간 업그레이드

마지막으로, NGINX 바이너리 실시간 업그레이드에 대해 간단히 설명하겠습니다. NGINX 설정 파일을 수정한 후에는 리로드해야 적용된다는 것을 알고 있습니다. 그러나 NGINX가 자체적으로 업그레이드할 때는 실시간으로 할 수 있습니다. 이는 전통적인 정적 로드 밸런싱, 리버스 프록시, 파일 캐싱으로 시작한 NGINX의 특성을 고려하면 이해할 수 있습니다.

핫 업그레이드는 이전 Master 프로세스에 USR2WINCH 신호를 보내어 수행됩니다. 이 두 단계에서 전자는 새로운 Master 프로세스를 시작하고, 후자는 Worker 프로세스를 점차적으로 종료합니다.

이 두 단계 후, 새로운 Master와 새로운 Worker가 시작됩니다. 이 시점에서 이전 Master는 종료되지 않습니다. 종료되지 않는 이유는 간단합니다: 롤백이 필요한 경우 이전 MasterHUP 신호를 보낼 수 있기 때문입니다. 물론, 롤백이 필요하지 않다고 판단되면 이전 MasterKILL 신호를 보내 종료할 수 있습니다.

이렇게 하면 NGINX 바이너리 실시간 업그레이드가 완료됩니다.

이에 대한 더 자세한 정보를 알고 싶다면 공식 문서를 확인하여 계속 학습하시기 바랍니다.

요약

일반적으로 OpenResty에서 사용하는 것은 NGINX의 기초이며, 주로 설정, 마스터-슬레이브 프로세스, 실행 단계 등과 관련이 있습니다. Lua 코드로 해결할 수 있는 다른 것들은 가능한 한 코드로 해결하며, NGINX 모듈과 설정을 사용하지 않는 것이 OpenResty를 배울 때의 사고방식 변화입니다.

마지막으로, Nginx 공식적으로 NJS를 지원한다는 점을 알려드립니다. 이는 OpenResty와 유사하게 NGINX 로직의 일부를 JS로 작성할 수 있다는 것을 의미합니다. 이에 대해 어떻게 생각하시나요? 이 글을 공유해 주시기 바랍니다.