OpenResty는 동적 요청과 응답을 지원하는 강화된 NGINX입니다.
API7.ai
October 23, 2022
이전의 소개를 통해 여러분은 OpenResty의 개념과 이를 학습하는 방법에 대해 이해하셨을 것입니다. 이 글에서는 OpenResty가 클라이언트 요청과 응답을 어떻게 처리하는지 안내하겠습니다.
OpenResty는 NGINX 기반의 웹 서버이지만, NGINX와는 근본적으로 다릅니다: NGINX는 정적 구성 파일에 의해 구동되는 반면, OpenResty는 Lua API에 의해 구동되어 더 많은 유연성과 프로그래밍 가능성을 제공합니다.
Lua API의 장점을 살펴보겠습니다.
API 카테고리
먼저, OpenResty API는 다음과 같은 큰 범주로 나뉩니다.
- 요청 및 응답 처리.
- SSL 관련.
- 공유 딕셔너리.
- Cosocket.
- 4계층 트래픽 처리.
- 프로세스 및 워커.
- NGINX 변수 및 구성 접근.
- 문자열, 시간, 코덱 및 기타 일반 기능 등.
여기서 OpenResty의 Lua API 문서를 열어 API 목록과 대조해 보시길 권장합니다. 이 카테고리와 관련이 있는지 확인해 보세요.
OpenResty API는 lua-nginx-module 프로젝트뿐만 아니라 lua-resty-core 프로젝트에도 존재합니다. 예를 들어 ngx.ssl
, ngx.base64
, ngx.errlog
, ngx.process
, ngx.re.split
, ngx.resp.add_header
, ngx.balancer
, ngx.semaphore
, ngx.ocsp
등의 API가 있습니다.
lua-nginx-module 프로젝트에 없는 API는 별도로 require 해야 사용할 수 있습니다. 예를 들어, split 함수를 사용하려면 다음과 같이 호출해야 합니다.
$ resty -e 'local ngx_re = require "ngx.re"
local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5})
print(res)
'
물론, 혼란스러울 수 있습니다: lua-nginx-module
프로젝트에는 ngx.re.sub
, ngx.re.find
등으로 시작하는 여러 API가 있는데, 왜 ngx.re.split
API만 require를 먼저 해야 사용할 수 있을까요?
이전 lua-resty-core
챕터에서 언급했듯이, 새로운 OpenResty API는 lua-rety-core
저장소에서 FFI 방식으로 구현되어 있기 때문에, 어쩔 수 없이 조각난 느낌이 있습니다. 앞으로 lua-nginx-module
과 lua-resty-core
프로젝트를 병합하여 이 문제를 해결할 수 있기를 기대합니다.
요청
다음으로, OpenResty가 클라이언트 요청과 응답을 어떻게 처리하는지 살펴보겠습니다. 먼저 요청을 처리하는 API를 보겠지만, ngx.req
로 시작하는 API가 20개가 넘으니 어떻게 시작해야 할까요?
HTTP 요청 메시지는 요청 라인, 요청 헤더, 요청 본문으로 구성되므로, 이 세 부분에 따라 API를 소개하겠습니다.
요청 라인
먼저 요청 라인입니다. HTTP 요청 라인에는 요청 메서드, URI, HTTP 프로토콜 버전이 포함됩니다. NGINX에서는 내장 변수를 사용하여 이 값을 얻을 수 있지만, OpenResty에서는 ngx.var.*
API에 해당합니다. 두 가지 예를 보겠습니다.
- 내장 변수
$scheme
는 NGINX에서 프로토콜 이름을 나타내며,http
또는https
입니다; OpenResty에서는ngx.var.scheme
을 사용하여 동일한 값을 반환할 수 있습니다. $request_method
는GET
,POST
등의 요청 메서드를 나타냅니다; OpenResty에서는ngx.var.request_method
를 통해 동일한 값을 반환할 수 있습니다.
NGINX 공식 문서를 방문하여 NGINX 내장 변수의 전체 목록을 확인할 수 있습니다: http://nginx.org/en/docs/http/ngx_http_core_module.html#variables.
그렇다면 질문이 생깁니다: ngx.var.*
와 같은 변수 값을 반환하여 요청 라인의 데이터를 얻을 수 있는데, 왜 OpenResty는 요청 라인을 위한 별도의 API를 제공할까요?
이 결과에는 여러 요인이 있습니다:
- 먼저,
ngx.var
를 반복적으로 읽는 것은 성능상 비효율적이므로 권장되지 않습니다. - 둘째, 프로그램 친화적인 측면에서
ngx.var
는 문자열을 반환하며 Lua 객체가 아닙니다.args
를 얻을 때 여러 값을 반환할 수 있으므로 처리하기 어렵습니다. - 셋째, 유연성 측면에서
ngx.var
는 대부분 읽기 전용이며,$args
와limit_rate
와 같은 일부 변수만 쓰기가 가능합니다. 그러나 우리는 종종 메서드, URI, args를 수정해야 할 필요가 있습니다.
따라서 OpenResty는 요청 라인을 조작하기 위한 몇 가지 API를 제공하며, 이를 통해 요청 라인을 재작성하여 리디렉션과 같은 후속 작업을 수행할 수 있습니다.
HTTP 프로토콜 버전 번호를 API를 통해 얻는 방법을 살펴보겠습니다. OpenResty API ngx.req.http_version
은 NGINX $server_protocol
변수와 동일한 작업을 수행합니다: HTTP 프로토콜의 버전 번호를 반환합니다. 그러나 이 API의 반환 값은 문자열이 아닌 숫자 형식이며, 가능한 값은 2.0
, 1.0
, 1.1
, 0.9
입니다. 이 범위를 벗어나는 값은 Nil
이 반환됩니다.
요청 라인에서 요청 메서드를 얻는 방법을 살펴보겠습니다. 앞서 언급했듯이, ngx.req.get_method
와 NGINX의 $request_method
변수의 역할과 반환 값은 동일합니다: 문자열 형식입니다.
그러나 현재 HTTP 요청 메서드 ngx.req.set_method
의 매개변수 형식은 문자열이 아닌 내장 숫자 상수입니다. 예를 들어, 다음 코드는 요청 메서드를 POST로 재작성합니다.
ngx.req.set_method(ngx.HTTP_POST)
내장 상수 ngx.HTTP_POST
가 실제로 문자열이 아닌 숫자인지 확인하려면, 그 값을 출력하여 8이 나오는지 확인할 수 있습니다.
resty -e 'print(ngx.HTTP_POST)'
이렇게 하면 get
메서드의 반환 값은 문자열이고, set
메서드의 입력 값은 숫자입니다. set
메서드에 혼란스러운 값을 전달하면 API가 충돌하여 500
오류를 보고할 수 있습니다. 그러나 다음과 같은 판단 로직에서는:
if (ngx.req.get_method() == ngx.HTTP_POST) then
-- do something
end
이러한 코드는 오류를 보고하지 않으며, 코드 리뷰 중에도 발견하기 어렵습니다. 저도 비슷한 실수를 한 적이 있으며, 두 차례의 코드 리뷰와 불완전한 테스트 케이스를 통해 문제를 발견한 기억이 있습니다. 결국 온라인 환경에서의 이상 현상으로 문제를 추적할 수 있었습니다.
이러한 문제를 해결할 수 있는 실질적인 방법은 더 주의를 기울이거나 추가적인 캡슐화를 하는 것뿐입니다. 비즈니스 API를 설계할 때도 get
과 set
메서드의 일관된 매개변수 형식을 고려하여 일부 성능을 희생하더라도 일관성을 유지하는 것이 좋습니다.
또한, 요청 라인을 재작성하는 방법 중에는 ngx.req.set_uri
와 ngx.req.set_uri_args
라는 두 가지 API가 있으며, 이를 통해 URI와 args를 재작성할 수 있습니다. 다음 NGINX 구성을 살펴보겠습니다.
rewrite ^ /foo?a=3? break;
그렇다면, 이와 동등한 Lua API로 어떻게 해결할 수 있을까요? 답은 다음 두 줄의 코드입니다.
ngx.req.set_uri_args("a=3")
ngx.req.set_uri("/foo")
공식 문서를 읽어보셨다면, ngx.req.set_uri
에는 두 번째 매개변수인 jump
가 있으며, 기본값은 "false"입니다. 이를 "true"로 설정하면 위 예제에서 rewrite
명령의 플래그를 break
대신 last
로 설정하는 것과 같습니다.
그러나 저는 rewrite
명령의 플래그 구성이 가독성이 떨어지고 인식하기 어려우며, 코드보다 직관성과 유지보수성이 떨어진다고 생각합니다.
요청 헤더
HTTP 요청 헤더는 key : value
형식입니다. 예를 들어:
Accept: text/css,*/*;q=0.1
Accept-Encoding: gzip, deflate, br
OpenResty에서는 ngx.req.get_headers
를 사용하여 요청 헤더를 파싱하고 얻을 수 있으며, 반환 값 유형은 테이블입니다.
local h, err = ngx.req.get_headers()
if err == "truncated" then
-- one can choose to ignore or reject the current request here
end
for k, v in pairs(h) do
...
end
기본적으로 처음 100개의 헤더를 반환합니다. 100개를 초과하면 truncated
오류를 보고하며, 개발자가 이를 어떻게 처리할지 결정할 수 있습니다. 왜 이런 방식을 택했는지 궁금할 수 있는데, 이는 보안 취약점 섹션에서 언급하겠습니다.
그러나 주의할 점은 OpenResty가 특정 요청 헤더를 얻기 위한 특정 API를 제공하지 않는다는 것입니다. 즉, ngx.req.header['host']
와 같은 형식이 없습니다. 이러한 필요가 있다면 NGINX 변수 $http_xxx
에 의존해야 합니다. OpenResty에서는 ngx.var.http_xxx
를 통해 이를 얻을 수 있습니다.
이제 요청 헤더를 재작성하고 삭제하는 방법을 살펴보겠습니다. 두 작업 모두 매우 직관적인 API입니다:
ngx.req.set_header("Content-Type", "text/css")
ngx.req.clear_header("Content-Type")
물론, 공식 문서에는 요청 헤더를 제거하는 다른 방법도 언급되어 있습니다. 예를 들어, 헤더 값을 nil
로 설정하는 등입니다. 그러나 코드의 명확성을 위해 clear_header
를 사용하는 것을 권장합니다.
요청 본문
마지막으로 요청 본문을 살펴보겠습니다. 성능상의 이유로 OpenResty는 요청 본문을 적극적으로 읽지 않습니다. nginx.conf
에서 lua_need_request_body
지시문을 강제로 활성화하지 않는 한입니다. 또한, 큰 요청 본문의 경우 OpenResty는 내용을 디스크의 임시 파일에 저장하므로, 요청 본문을 읽는 전체 과정은 다음과 같습니다.
ngx.req.read_body()
local data = ngx.req.get_body_data()
if not data then
local tmp_file = ngx.req.get_body_file()
-- io.open(tmp_file)
-- ...
end
이 코드에는 디스크 파일을 읽는 IO 블로킹 작업이 포함되어 있습니다. client_body_buffer_size
(64비트 시스템에서 기본값은 16KB)의 구성을 조정하여 블로킹 작업을 최소화해야 합니다; 또한 client_body_buffer_size
와 client_max_body_size
를 동일하게 구성하여 메모리에서 완전히 처리할 수도 있습니다. 이는 메모리 크기와 동시 요청 수에 따라 달라집니다.
또한, 요청 본문을 재작성할 수 있습니다. ngx.req.set_body_data
와 ngx.req.set_body_file
두 API는 문자열과 로컬 디스크 파일을 입력 매개변수로 받아 요청 본문을 재작성합니다. 그러나 이러한 작업은 흔하지 않으며, 자세한 내용은 문서를 확인하시길 바랍니다.
응답
요청이 처리된 후, 클라이언트에게 응답을 보내야 합니다. 요청 메시지와 마찬가지로, 응답 메시지도 상태 라인, 응답 헤더, 응답 본문으로 구성됩니다. 이 세 부분에 따라 해당 API를 소개하겠습니다.
상태 라인
상태 라인에서 우리가 주로 관심을 갖는 것은 상태 코드입니다. 기본적으로 반환되는 HTTP 상태 코드는 200이며, OpenResty에 내장된 상수 ngx.HTTP_OK
입니다. 그러나 코드 세계에서는 항상 예외적인 경우를 처리하는 코드가 많습니다.
요청 메시지를 검사하여 악의적인 요청임을 발견하면 요청을 종료해야 합니다:
ngx.exit(ngx.HTTP_BAD_REQUEST)
그러나 OpenResty의 HTTP 상태 코드에는 특별한 상수가 있습니다: ngx.OK
. ngx.exit(ngx.OK)
상황에서 요청은 현재 처리 단계를 종료하고 다음 단계로 이동하며, 클라이언트에게 직접 반환되지 않습니다.
물론, 종료하지 않고 상태 코드를 재작성할 수도 있습니다. ngx.status
를 사용하여 다음과 같이 작성할 수 있습니다.
ngx.status = ngx.HTTP_FORBIDDEN
상태 코드 상수에 대해 더 알고 싶다면 문서를 참조하시길 바랍니다.
응답 헤더
응답 헤더에 대해 두 가지 방법으로 설정할 수 있습니다. 첫 번째 방법은 가장 간단합니다.
ngx.header.content_type = 'text/plain'
ngx.header["X-My-Header"] = 'blah blah'
ngx.header["X-My-Header"] = nil -- 삭제
여기서 ngx.header
는 응답 헤더 정보를 담고 있으며, 읽기, 수정, 삭제가 가능합니다.
두 번째 방법은 lua-resty-core
저장소의 ngx_resp.add_header
를 사용하여 헤더 메시지를 추가하는 것입니다. 다음과 같이 호출합니다:
local ngx_resp = require "ngx.resp"
ngx_resp.add_header("Foo", "bar")
첫 번째 방법과의 차이점은 add_header
가 동일한 이름의 기존 필드를 덮어쓰지 않는다는 것입니다.
응답 본문
마지막으로 응답 본문을 살펴보겠습니다. OpenResty에서는 ngx.say
와 ngx.print
를 사용하여 응답 본문을 출력할 수 있습니다.
ngx.say('hello, world')
두 API의 기능은 동일하며, 유일한 차이점은 ngx.say
가 끝에 줄 바꿈을 추가한다는 것입니다.
문자열 연결의 비효율성을 피하기 위해 ngx.say / ngx.print
는 문자열과 배열 형식을 매개변수로 지원합니다.
$ resty -e 'ngx.say({"hello", ", ", "world"})'
hello, world
이 방법은 Lua 수준에서의 문자열 연결을 건너뛰고 C 함수가 처리하도록 합니다.
요약
오늘의 내용을 다시 한번 살펴보겠습니다. 우리는 요청 및 응답 메시지와 관련된 OpenResty API를 소개했습니다. 보시다시피, OpenResty API는 NGINX 지시문보다 더 유연하고 강력합니다.
결론적으로, HTTP 요청을 처리할 때 OpenResty가 제공하는 Lua API가 여러분의 요구를 충족시키기에 충분한가요? 여러분의 의견을 남기고 이 글을 동료와 친구들과 공유하여 함께 소통하고 개선해 나가시길 바랍니다.