OpenResty에서 일반적으로 사용되는 API 소개
API7.ai
November 4, 2022
이전 글들에서 여러분은 OpenResty의 중요한 Lua API들에 대해 익숙해졌을 것입니다. 오늘은 정규 표현식, 시간, 프로세스 등과 관련된 몇 가지 일반적인 API들에 대해 배워보겠습니다.
정규 표현식 관련 API
가장 흔히 사용되고 중요한 정규 표현식부터 살펴보겠습니다. OpenResty에서는 Lua 패턴 매칭 대신 ngx.re.*
에서 제공하는 API 세트를 사용하여 정규 표현식 관련 로직을 처리해야 합니다. 이는 성능상의 이유뿐만 아니라 Lua 정규 표현식이 자체적으로 포함되어 있고 PCRE
사양이 아니기 때문에 대부분의 개발자에게는 번거로울 수 있기 때문입니다.
이전 글들에서 이미 ngx.re.*
API 중 일부를 접해보았고, 그 문서는 매우 상세합니다. 따라서 여기서는 두 가지 API를 별도로 소개하겠습니다.
ngx.re.split
첫 번째는 ngx.re.split
입니다. 문자열 자르기는 매우 일반적인 기능이며, OpenResty도 이에 해당하는 API를 제공하지만 많은 개발자들이 이를 찾지 못하고 직접 구현하는 경우가 많습니다.
왜냐하면 ngx.re.split
API는 lua-nginx-module
에 있지 않고 lua-resty-core
에 있으며, lua-resty-core
홈페이지 문서가 아닌 lua-resty-core/lib/ngx/re.md
세 번째 디렉토리의 문서에 있기 때문입니다. 결과적으로 많은 개발자들이 이 API의 존재를 전혀 모르고 있습니다.
마찬가지로 발견하기 어려운 API로는 ngx_resp.add_header
, enable_privileged_agent
등이 있으며, 이전에 언급한 바 있습니다. 그렇다면 이 문제를 어떻게 빠르게 해결할 수 있을까요? lua-resty-core
홈페이지 문서를 읽는 것 외에도 lua-resty-core/lib/ngx/
디렉토리의 *.md
문서를 꼼꼼히 읽어보는 것이 필요합니다.
lua_regex_match_limit
두 번째로 소개할 것은 lua_regex_match_limit
입니다. 이전에는 OpenResty가 제공하는 NGINX 명령어에 대해 다루지 않았는데, 대부분의 경우 기본값으로 충분하며 런타임에 이를 수정할 필요가 없기 때문입니다. 예외는 정규 표현식과 관련된 lua_regex_match_limit
명령어입니다.
백트래킹 NFA 기반으로 구현된 정규 엔진을 사용하면 Catastrophic Backtracking의 위험이 있습니다. 이는 정규 표현식이 매칭 시 너무 많은 백트래킹을 하여 CPU가 100%가 되고 서비스가 차단되는 상황을 말합니다.
일단 Catastrophic Backtracking이 발생하면 gdb
를 사용하여 덤프를 분석하거나 systemtap
을 사용하여 온라인 환경을 분석하여 위치를 파악해야 합니다. 불행히도 이를 사전에 감지하는 것은 쉽지 않습니다. 특수한 요청만이 이를 유발하기 때문입니다. 이는 공격자가 이를 악용할 수 있게 하며, ReDoS
(RegEx Denial of Service)는 이러한 유형의 공격을 말합니다.
여기서는 OpenResty에서 다음과 같은 코드를 사용하여 위의 문제를 간단하고 효과적으로 피하는 방법을 소개하겠습니다:
lua_regex_match_limit
은 PCRE
정규 엔진의 백트래킹 횟수를 제한하는 데 사용됩니다. 이렇게 하면 Catastrophic Backtracking이 발생하더라도 그 결과가 CPU를 가득 차게 하지 않는 범위로 제한됩니다.
lua_regex_match_limit 100000;
시간 관련 API
가장 흔히 사용되는 시간 API는 ngx.now
로, 현재 타임스탬프를 출력합니다. 예를 들어 다음과 같은 코드가 있습니다:
resty -e 'ngx.say(ngx.now())'
출력 결과에서 볼 수 있듯이, ngx.now
는 소수 부분을 포함하므로 더 정확합니다. 관련된 ngx.time
API는 값의 정수 부분만 반환합니다. 다른 API들인 ngx.localtime
, ngx.utctime
, ngx.cookie_time
, ngx.http_time
은 주로 다양한 형식의 시간을 반환하고 처리하는 데 사용됩니다. 이를 사용하려면 문서를 확인하면 되며, 이해하기 어렵지 않으므로 여기서는 다루지 않겠습니다.
그러나 주목할 점은 현재 시간을 반환하는 이러한 API들은 비차단 네트워크 IO 작업에 의해 트리거되지 않으면 항상 캐시된 값을 반환하며, 우리가 원하는 현재 실시간 시간을 반환하지 않는다는 것입니다. 다음 샘플 코드를 살펴보세요:
$ resty -e 'ngx.say(ngx.now())
os.execute("sleep 1")
ngx.say(ngx.now())'
ngx.now
를 두 번 호출하는 사이에 Lua의 차단 함수를 사용하여 1
초 동안 잠을 잤지만, 두 경우 모두 동일한 타임스탬프가 반환됩니다.
그렇다면 이를 비차단 sleep 함수로 대체하면 어떻게 될까요? 예를 들어 다음과 같은 새로운 코드입니다:
$ resty -e 'ngx.say(ngx.now())
ngx.sleep(1)
ngx.say(ngx.now())'
이 코드는 서로 다른 타임스탬프를 출력합니다. 이는 ngx.sleep
이라는 비차단 sleep 함수를 사용하기 때문입니다. 이 함수는 지정된 시간 동안 잠을 자는 것 외에도 다른 특별한 용도가 있습니다.
예를 들어, 집중적인 계산을 수행하는 코드가 있어 많은 시간이 걸리는 경우, 이 코드에 해당하는 요청은 이 시간 동안 worker와 CPU 리소스를 계속 점유하게 되어 다른 요청들이 대기열에 쌓이고 제때 응답을 받지 못하게 됩니다. 이때 ngx.sleep(0)
을 삽입하여 이 코드가 제어권을 포기하도록 하여 다른 요청들도 처리될 수 있게 할 수 있습니다.
Worker 및 프로세스 API
OpenResty는 ngx.worker.*
와 ngx.process.*
API를 제공하여 worker와 프로세스에 대한 정보를 얻을 수 있습니다. 전자는 Nginx worker 프로세스와 관련이 있으며, 후자는 일반적으로 모든 Nginx 프로세스를 말하며, worker 프로세스뿐만 아니라 master 프로세스, privileged 프로세스 등을 포함합니다.
true
와 null
값 문제
마지막으로 true
와 null
값 문제를 살펴보겠습니다. OpenResty에서 true
값과 null
값의 판단은 매우 번거롭고 혼란스러운 점이었습니다.
Lua에서 true
값의 정의를 살펴보겠습니다: nil
과 false
를 제외한 모든 값이 true
값입니다.
따라서 true
값에는 0
, 빈 string
, 빈 table
등도 포함됩니다.
Lua에서 nil
은 undefined
를 의미합니다. 예를 들어, 변수를 선언했지만 초기화하지 않았다면 그 값은 nil
입니다.
$ resty -e 'local a
ngx.say(type(a))'
그리고 nil
은 Lua에서 데이터 타입이기도 합니다. 이 두 가지를 이해했다면, 이제 이 두 정의에서 파생된 다른 문제들을 살펴보겠습니다.
ngx.null
첫 번째 문제는 ngx.null
입니다. Lua의 nil
은 table
의 값으로 사용할 수 없기 때문에, OpenResty는 ngx.null
을 도입하여 테이블의 null
값으로 사용합니다.
$ resty -e 'print(ngx.null)'
null
$ resty -e 'print(type(ngx.null))'
userdata
위의 두 코드에서 볼 수 있듯이, ngx.null
은 null
로 출력되며, 그 타입은 userdata
입니다. 그렇다면 이를 false
값으로 간주할 수 있을까요? 물론 아닙니다. ngx.null
의 불리언 값은 true
입니다.
$ resty -e 'if ngx.null then
ngx.say("true")
end'
따라서, nil
과 false
만이 false
값이라는 점을 기억하세요. 이 점을 놓치면 쉽게 함정에 빠질 수 있습니다. 예를 들어, lua-resty-redis
를 사용할 때 다음과 같은 판단을 할 때:
local res, err = red:get("dog")
if not res then
res = res + "test"
end
반환 값 res
가 nil
이면 함수 호출이 실패한 것이고, res
가 ngx.null
이면 redis에 dog
키가 존재하지 않는 것입니다. 이 경우 dog
키가 존재하지 않으면 코드가 충돌합니다.
cdata:NULL
두 번째 문제는 cdata:NULL
입니다. LuaJIT FFI 인터페이스를 통해 C 함수를 호출할 때, 함수가 NULL
포인터를 반환하면 cdata:NULL
이라는 또 다른 종류의 null
값을 만나게 됩니다.
$ resty -e 'local ffi = require "ffi"
local cdata_null = ffi.new("void*", nil)
if cdata_null then
ngx.say("true")
end'
ngx.null
과 마찬가지로, cdata:NULL
도 true
입니다. 그러나 더 혼란스러운 점은 다음 코드가 true
를 출력한다는 것입니다. 이는 cdata:NULL
이 nil
과 동등하다는 것을 의미합니다.
$ resty -e 'local ffi = require "ffi"
local cdata_null = ffi.new("void*", nil)
ngx.say(cdata_null == nil)'
그렇다면 ngx.null
과 cdata:NULL
을 어떻게 처리해야 할까요? 애플리케이션 레이어에서 이러한 문제를 신경 쓰는 것은 좋은 해결책이 아닙니다. 차라리 이차 래퍼를 만들어 호출자가 이러한 세부 사항을 알지 못하게 하는 것이 좋습니다.
cjson.null
마지막으로 cjson
에서 나타나는 null
값을 살펴보겠습니다. cjson
라이브러리는 json의 NULL
을 Lua lightuserdata
로 디코딩하며, cjson.null
로 표현합니다.
$ resty -e 'local cjson = require "cjson"
local data = cjson.encode(nil)
local decode_null = cjson.decode(data)
ngx.say(decode_null == cjson.null)'
Lua의 nil
은 JSON으로 인코딩되고 디코딩된 후 cjson.null
이 됩니다. 이는 ngx.null
과 같은 이유로 도입된 것으로, nil
이 table
의 값으로 사용될 수 없기 때문입니다.
지금까지 OpenResty에서 이렇게 많은 종류의 null
값들에 대해 혼란스러웠나요? 걱정하지 마세요. 이 부분을 몇 번 더 읽고 스스로 정리해보면 혼란스럽지 않을 것입니다. 물론, 앞으로 if not foo then
과 같은 코드를 작성할 때 작동하는지 더 생각해봐야 합니다.
요약
오늘의 글에서는 OpenResty에서 흔히 사용되는 Lua API들을 소개했습니다.
마지막으로, ngx.now
예제에서 yield
작업이 없을 때 ngx.now
의 값이 수정되지 않는 이유는 무엇인지 질문을 남기겠습니다. 여러분의 의견을 댓글로 공유해주시고, 이 글을 공유하여 함께 소통하고 발전할 수 있기를 바랍니다.