OpenResty에서 자주 사용되는 세 가지 Lua Resty 라이브러리

API7.ai

January 13, 2023

OpenResty (NGINX + Lua)

프로그래밍 언어와 플랫폼에 대해 배우는 것은 종종 구문 자체보다는 표준 및 서드파티 라이브러리를 이해하는 문제입니다. API와 성능 최적화 기술을 배운 후에는 다양한 lua-resty 라이브러리의 사용법을 배워 OpenResty의 기능을 더 많은 시나리오로 확장해야 합니다.

lua-resty 라이브러리를 어디서 찾을 수 있나요?

PHP, Python, JavaScript에 비해 현재 OpenResty의 표준 및 서드파티 라이브러리는 여전히 상대적으로 빈약하며, 적절한 lua-resty 라이브러리를 찾는 것은 쉽지 않습니다. 그러나 여전히 더 빠르게 찾을 수 있도록 도와줄 두 가지 추천 소스가 있습니다.

첫 번째 추천은 Aapo가 유지 관리하는 awesome-resty 저장소입니다. 이 저장소는 OpenResty 관련 라이브러리를 카테고리별로 정리하고 있으며, NGINX C 모듈, lua-resty 라이브러리, 웹 프레임워크, 라우팅 라이브러리, 템플릿, 테스트 프레임워크 등을 모두 포함하고 있습니다. OpenResty 리소스를 찾기 위한 첫 번째 선택지입니다.

Aapo의 저장소에서 적절한 라이브러리를 찾지 못했다면, luarocks, topm, 또는 GitHub을 살펴볼 수도 있습니다. 오픈소스로 공개된 지 얼마 되지 않아 주목을 받지 못한 라이브러리가 있을 수 있습니다.

이전 글에서 우리는 lua-resty-mlcache, lua-resty-traffic, lua-resty-shell 등과 같은 유용한 라이브러리에 대해 배웠습니다. 오늘은 OpenResty 성능 최적화 섹션의 마지막 글에서 커뮤니티 개발자들이 기여한 3개의 독특한 주변 라이브러리를 소개합니다.

ngx.var의 성능 개선

먼저, C 모듈인 lua-var-nginx-module을 살펴보겠습니다. 앞서 언급했듯이, ngx.var은 상대적으로 성능 소모가 큰 작업입니다. 따라서 실제로는 ngx.ctx를 캐시 계층으로 사용해야 합니다.

그렇다면 ngx.var의 성능 문제를 완전히 해결할 방법이 있을까요?

이 C 모듈은 이 분야에서 몇 가지 실험을 했으며, 결과는 놀라웠습니다. ngx.var보다 5배의 성능 향상을 보였습니다. 이 모듈은 FFI 방식을 사용하므로, 먼저 다음 컴파일 옵션으로 OpenResty를 컴파일해야 합니다.

./configure --prefix=/opt/openresty \
         --add-module=/path/to/lua-var-nginx-module

그런 다음 luarocks를 사용하여 lua 라이브러리를 다음과 같이 설치합니다:

luarocks install lua-resty-ngxvar

여기서 호출하는 방법도 매우 간단하며, fetch 함수 한 줄만 필요합니다. 이는 원래의 ngx.var.remote_addr과 동일하게 클라이언트의 IP 주소를 가져옵니다.

content_by_lua_block {
    local var = require("resty.ngxvar")
    ngx.say(var.fetch("remote_addr"))
}

이 기본 작업을 이해한 후, 이 모듈이 어떻게 상당한 성능 향상을 달성했는지 더 궁금해할 수 있습니다. 우리가 항상 말하듯이, "소스 코드 앞에는 비밀이 없습니다". 그래서 remote_addr 변수를 어떻게 가져오는지 알아보겠습니다.

ngx_int_t
ngx_http_lua_var_ffi_remote_addr(ngx_http_request_t *r, ngx_str_t *remote_addr)
{
    remote_addr->len = r->connection->addr_text.len;
    remote_addr->data = r->connection->addr_text.data;

    return NGX_OK;
}

이 코드를 읽어보면, 이 Lua FFI 방식은 lua-resty-core 방식과 동일합니다. FFI를 사용하여 변수를 직접 가져오는 것은 ngx.var의 원래 조회 로직을 우회하는 명백한 장점이 있습니다. 단점도 명백합니다: 각 변수를 가져오기 위해 C 함수와 FFI 호출을 추가해야 하므로 시간과 노력이 많이 듭니다.

어떤 사람들은 "왜 이렇게 시간과 노력이 많이 드는 것일까요? 위의 C 코드는 꽤 실질적으로 보이지 않나요?"라고 물을 수 있습니다. 이 코드의 출처를 살펴보겠습니다. 이 코드는 NGINX 코드의 src/http/ngx_http_variables.c에서 가져온 것입니다.

static ngx_int_t
ngx_http_variable_remote_addr(ngx_http_request_t *r,
ngx_http_variable_value_t *v, uintptr_t data)
{
    v->len = r->connection->addr_text.len;
    v->valid = 1;
    v->no_cacheable = 0;
    v->not_found = 0;
    v->data = r->connection->addr_text.data;

    return NGX_OK;
}

소스 코드를 본 후, 비밀이 밝혀졌습니다! lua-var-nginx-module은 NGINX 변수 코드의 포터이며, 외부에 FFI 래핑을 한 방식으로 성능 최적화를 달성했습니다. 이는 좋은 아이디어이며 최적화의 좋은 방향입니다.

라이브러리나 도구를 배울 때, 단순히 작동 수준에서 멈추지 말고 왜 그렇게 하는지 질문하고 소스 코드를 살펴보아야 합니다. 물론, 더 많은 NGINX 변수를 지원하기 위해 코드를 기여하는 것도 강력히 권장합니다.

JSON 스키마

여기서 lua-resty 라이브러리인 lua-rapidjson을 소개합니다. 이는 Tencent의 오픈소스 JSON 라이브러리인 rapidjson을 래핑한 것으로, 성능으로 유명합니다. 여기서 우리는 cjson과의 차이점인 JSON Schema 지원에 초점을 맞춥니다.

JSON Schema는 인터페이스의 매개변수 형식과 검증 방법을 정확히 설명할 수 있는 일반적인 표준입니다. 다음은 간단한 예입니다:

"stringArray": {
    "type": "array",
    "items": { "type": "string" },
    "minItems": 1,
    "uniqueItems": true
}

이 JSON은 stringArray 매개변수가 문자열 배열 타입이며, 배열이 비어 있을 수 없고 배열 요소가 중복될 수 없음을 정확히 설명합니다.

lua-rapidjson은 OpenResty에서 JSON 스키마를 사용할 수 있게 해주며, 인터페이스 검증에 큰 편의를 제공합니다. 예를 들어, 앞서 설명한 제한 카운트 인터페이스에 대해 다음 스키마를 사용하여 설명할 수 있습니다:

local schema = {
    type = "object",
    properties = {
        count = {type = "integer", minimum = 0},
        time_window = {type = "integer",  minimum = 0},
        key = {type = "string", enum = {"remote_addr", "server_addr"}},
        rejected_code = {type = "integer", minimum = 200, maximum = 600},
    },
    additionalProperties = false,
    required = {"count", "time_window", "key", "rejected_code"},
}

이렇게 하면 두 가지 매우 명백한 이점이 있습니다:

  1. 프론트엔드의 경우, 프론트엔드는 이 스키마 설명을 직접 재사용하여 프론트엔드 페이지 개발과 매개변수 검증을 할 수 있으며, 백엔드를 신경 쓸 필요가 없습니다.
  2. 백엔드의 경우, 백엔드는 lua-rapidjson의 스키마 검증 기능인 SchemaValidator를 직접 사용하여 인터페이스의 합법성을 판단할 수 있으며, 추가 코드를 작성할 필요가 없습니다.

Worker 간 통신

마지막으로, OpenResty에서 Worker 간 통신을 가능하게 하는 lua-resty 라이브러리에 대해 이야기하겠습니다. OpenResty에는 Worker 프로세스 간 직접 통신 메커니즘이 없어 많은 문제를 야기합니다. 다음 시나리오를 상상해보세요:

OpenResty 서비스에 24개의 worker 프로세스가 있고, 관리자가 REST HTTP API를 통해 시스템 구성을 업데이트하면 하나의 Worker만이 관리자의 업데이트를 받아 데이터베이스에 결과를 쓰고, 자신의 Worker 내의 shared dictlru cache를 업데이트합니다. 그렇다면 다른 23개의 worker에게 이 구성을 업데이트하도록 알리는 방법은 무엇일까요?

여러 Worker 간의 알림 메커니즘이 필요합니다. OpenResty가 이를 지원하지 않는 경우, shared dict 데이터를 통해 여러 worker 간에 데이터를 공유해야 합니다.

lua-resty-worker-events는 이 아이디어의 구체적인 구현입니다. 이는 shared dict에 버전 번호를 유지하며, 새 메시지가 게시되면 버전 번호를 하나 증가시키고 메시지 내용을 버전 번호를 key로 하여 사전에 넣습니다.

event_id, err = _dict:incr(KEY_LAST_ID, 1)
success, err = _dict:add(KEY_DATA .. tostring(event_id), json)

또한, ngx.timer를 사용하여 기본 간격이 1초인 polling 루프를 백그라운드에서 생성하여 버전 번호의 변경을 지속적으로 확인합니다:

local event_id, err = get_event_id()
if event_id == _last_event then
    return "done"
end

이렇게 하면 새로운 이벤트 알림이 처리되어야 함을 발견하면 버전 번호를 기반으로 shared dict에서 메시지 내용을 검색합니다:

while _last_event < event_id do
    count = count + 1
    _last_event = _last_event + 1
    data, err = _dict:get(KEY_DATA..tostring(_last_event))
end

전체적으로, lua-resty-worker-events는 1초의 지연이 있지만, 여전히 Worker-to-Worker 이벤트 알림 메커니즘을 구현했습니다.

그러나 실시간 시나리오, 예를 들어 메시지 푸시의 경우, OpenResty의 Worker 프로세스 간 직접 통신 부재로 인해 문제가 발생할 수 있습니다. 이에 대한 더 나은 해결책은 없지만, 좋은 아이디어가 있다면 Github에서 자유롭게 논의해주세요. OpenResty의 많은 기능은 커뮤니티에 의해 구축된 선순환 생태계에 의해 주도됩니다.

요약

오늘 소개한 세 가지 라이브러리는 독특하며 OpenResty 애플리케이션에 더 많은 가능성을 제공합니다. 마지막으로, OpenResty 주변에서 흥미로운 라이브러리를 찾았나요? 또는 오늘 언급된 라이브러리에 대해 어떤 점을 발견하거나 궁금한 점이 있나요? 이 글을 주변의 OpenResty 사용자에게 보내 함께 교류하고 발전해 나가길 바랍니다.