웹 서버를 넘어서: Privileged Process와 Timer Tasks
API7.ai
November 3, 2022
이전 글에서 우리는 OpenResty API, shared dict
, 그리고 cosocket
을 소개했습니다. 이들은 모두 NGINX와 웹 서버 영역 내에서 기능을 구현하며, 프로그래밍 가능한 웹 서버를 더 저렴하고 유지보수가 쉬운 방식으로 제공합니다.
그러나 OpenResty는 그 이상의 일도 할 수 있습니다. 오늘은 웹 서버를 넘어서는 OpenResty의 몇 가지 기능을 살펴보겠습니다. 바로 타이머 작업, 특권 프로세스, 그리고 논블로킹 ngx.pipe
입니다.
타이머 작업
OpenResty에서는 때때로 데이터 동기화, 로그 정리 등과 같은 특정 작업을 백그라운드에서 정기적으로 수행해야 할 필요가 있습니다. 이를 설계한다면 어떻게 할까요? 가장 쉬운 방법은 이러한 작업을 수행하기 위해 외부에 API 인터페이스를 제공하고, 시스템의 crontab
을 사용해 정기적으로 curl
을 호출하여 이 인터페이스에 접근하는 방식으로 간접적으로 이 요구사항을 구현하는 것입니다.
그러나 이 방법은 조각화될 뿐만 아니라 운영 및 유지보수에 더 높은 복잡성을 가져옵니다. 그래서 OpenResty는 이러한 요구사항을 해결하기 위해 ngx.timer
를 제공합니다. ngx.timer
는 OpenResty가 시뮬레이션한 클라이언트 요청으로 간주할 수 있으며, 이는 해당 콜백 함수를 트리거합니다.
OpenResty의 타이머 작업은 다음과 같은 두 가지 유형으로 나눌 수 있습니다.
ngx.timer.at
은 일회성 타이머 작업을 실행하는 데 사용됩니다.ngx.timer.every
는 고정 주기의 타이머 작업을 실행하는 데 사용됩니다.
지난 글 마지막에 남겼던 생각할 거리를 기억하시나요? init_worker_by_lua
에서 cosocket
을 사용할 수 없다는 제한을 어떻게 극복할 수 있는지에 대한 질문이었고, 그 답은 ngx.timer
입니다.
다음 코드는 0
의 지연 시간으로 타이머 작업을 시작합니다. 이는 콜백 handler
함수를 시작하고, 이 함수 내에서 cosocket
을 사용해 웹사이트에 접근합니다.
init_worker_by_lua_block {
local function handler()
local sock = ngx.socket.tcp()
local ok, err = sock:connect(“api7.ai", 80)
end
local ok, err = ngx.timer.at(0, handler)
}
이렇게 하면 이 단계에서 cosocket
을 사용할 수 없다는 제한을 우회할 수 있습니다.
이 섹션의 시작 부분에서 언급한 사용자 요구사항으로 돌아가보면, ngx.timer.at
은 주기적으로 실행해야 하는 요구사항을 해결하지 못합니다. 위의 코드 예제에서는 일회성 작업입니다.
그렇다면 주기적으로 실행하려면 어떻게 해야 할까요? ngx.timer.at
API를 기반으로 두 가지 옵션이 있는 것 같습니다.
- 콜백 함수 내에서
while true
무한 루프를 사용해 작업을 실행한 후 잠시sleep
하는 방식으로 주기적 작업을 직접 구현할 수 있습니다. - 콜백 함수의 끝에서 새로운 타이머를 생성할 수도 있습니다.
그러나 선택을 하기 전에 명확히 해야 할 점이 있습니다: 타이머는 본질적으로 요청이며, 비록 클라이언트가 시작한 요청은 아니지만, 요청은 작업을 완료한 후 종료해야 하며 항상 상주할 수 없습니다. 그렇지 않으면 다양한 자원 누출을 쉽게 초래할 수 있습니다.
따라서 while true
를 사용해 주기적 작업을 구현하는 첫 번째 해결책은 신뢰할 수 없습니다. 두 번째 해결책은 가능하지만 재귀적으로 타이머를 생성하므로 이해하기 쉽지 않습니다.
그렇다면 더 나은 해결책이 있을까요? OpenResty의 새로운 ngx.timer.every
API는 이 문제를 해결하기 위해 특별히 설계되었으며, crontab
에 더 가까운 해결책입니다.
단점은 타이머 작업을 시작한 후에는 취소할 기회가 없다는 것입니다. 결국 ngx.timer.cancel
은 아직 구현되지 않은 기능입니다.
이 시점에서 문제에 직면하게 됩니다: 타이머가 백그라운드에서 실행 중이며 취소할 수 없습니다. 타이머가 많으면 시스템 자원이 고갈되기 쉽습니다.
따라서 OpenResty는 이를 제한하기 위해 lua_max_pending_timers
와 lua_max_running_timers
두 가지 지시어를 제공합니다. 전자는 실행 대기 중인 타이머의 최대 수를 나타내고, 후자는 현재 실행 중인 타이머의 최대 수를 나타냅니다.
또한 Lua API를 사용해 현재 대기 중인 타이머 작업과 실행 중인 타이머 작업의 값을 얻을 수도 있습니다. 다음 두 예제를 참조하세요.
content_by_lua_block {
ngx.timer.at(3, function() end)
ngx.say(ngx.timer.pending_count())
}
이 코드는 1
을 출력하며, 이는 실행 대기 중인 예약 작업이 하나 있음을 나타냅니다.
content_by_lua_block {
ngx.timer.at(0.1, function() ngx.sleep(0.3) end)
ngx.sleep(0.2)
ngx.say(ngx.timer.running_count())
}
이 코드는 1
을 출력하며, 이는 실행 중인 예약 작업이 하나 있음을 나타냅니다.
특권 프로세스
다음으로 특권 프로세스를 살펴보겠습니다. 우리 모두 알고 있듯이 NGINX는 Master
프로세스와 Worker
프로세스로 나뉘며, 워커 프로세스는 사용자 요청을 처리합니다. lua-resty-core
에서 제공하는 process.type
API를 통해 프로세스의 유형을 얻을 수 있습니다. 예를 들어, resty
를 사용해 다음 함수를 실행할 수 있습니다.
$ resty -e 'local process = require "ngx.process"
ngx.say("process type:", process.type())'
이 결과는 worker
가 아닌 single
을 반환합니다. 이는 resty
가 Master
프로세스가 아닌 Worker
프로세스로 NGINX를 시작한다는 것을 의미합니다. 이는 사실입니다. resty
구현에서 다음과 같은 줄로 Master
프로세스가 꺼져 있음을 확인할 수 있습니다.
master_process off;
OpenResty는 NGINX를 확장하여 privileged agent
를 추가했습니다. 특권 프로세스는 다음과 같은 특별한 기능을 가집니다.
-
어떤 포트도 모니터링하지 않으며, 이는 외부에 서비스를 제공하지 않음을 의미합니다.
-
Master
프로세스와 동일한 권한을 가지며, 일반적으로root
사용자의 권한을 가지므로 워커 프로세스가 할 수 없는 많은 작업을 수행할 수 있습니다. -
특권 프로세스는
init_by_lua
컨텍스트에서만 열 수 있습니다. -
또한 특권 프로세스는
init_worker_by_lua
컨텍스트에서 실행될 때만 의미가 있습니다. 요청이 트리거되지 않으며content
,access
등의 컨텍스트로 이동하지 않기 때문입니다.
특권 프로세스를 열어보는 예제를 살펴보겠습니다.
init_by_lua_block {
local process = require "ngx.process"
local ok, err = process.enable_privileged_agent()
if not ok then
ngx.log(ngx.ERR, "enables privileged agent failed error:", err)
end
}
이 코드로 특권 프로세스를 열고 OpenResty 서비스를 시작하면, 이제 특권 프로세스가 NGINX 프로세스의 일부임을 확인할 수 있습니다.
nginx: master process
nginx: worker process
nginx: privileged agent process
그러나 특권 프로세스가 init_worker_by_lua
단계에서 한 번만 실행된다면 좋은 아이디어가 아닙니다. 그렇다면 특권 프로세스를 어떻게 트리거해야 할까요?
네, 방금 배운 지식에 답이 숨어 있습니다. 특권 프로세스는 포트를 리스닝하지 않으므로, 즉 터미널 요청으로 트리거할 수 없으므로, 주기적으로 트리거할 수 있는 유일한 방법은 방금 소개한 ngx.timer
를 사용하는 것입니다:
init_worker_by_lua_block {
local process = require "ngx.process"
local function reload(premature)
local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r")
if not f then
return
end
local pid = f:read()
f:close()
os.execute("kill -HUP " .. pid)
end
if process.type() == "privileged agent" then
local ok, err = ngx.timer.every(5, reload)
if not ok then
ngx.log(ngx.ERR, err)
end
end
}
위 코드는 매 5초마다 마스터 프로세스에 HUP
신호를 보내는 기능을 구현합니다. 자연스럽게, 이를 기반으로 더 흥미로운 작업을 수행할 수 있습니다. 예를 들어, 데이터베이스를 폴링하여 특권 프로세스를 위한 작업이 있는지 확인하고 실행할 수 있습니다. 특권 프로세스는 root
권한을 가지므로, 이는 분명히 "백도어" 프로그램입니다.
논블로킹 ngx.pipe
마지막으로 논블로킹 ngx.pipe
를 살펴보겠습니다. 이는 Lua의 표준 라이브러리를 사용해 외부 명령줄을 실행하며, 방금 설명한 코드 예제에서 Master
프로세스에 신호를 보냅니다.
os.execute("kill -HUP " .. pid)
자연스럽게, 이 작업은 블로킹됩니다. 그렇다면 OpenResty에서 외부 프로그램을 논블로킹 방식으로 호출할 수 있는 방법이 있을까요? 결국 OpenResty를 웹 서버가 아닌 완전한 개발 플랫폼으로 사용한다면, 이것이 필요합니다. 이를 위해 lua-resty-shell
라이브러리가 만들어졌으며, 이를 사용해 명령줄을 호출하는 것은 논블로킹입니다:
$ resty -e 'local shell = require "resty.shell"
local ok, stdout, stderr, reason, status =
shell.run([[echo "hello, world"]])
ngx.say(stdout)
이 코드는 hello world
를 출력하는 다른 방식으로, 시스템의 echo
명령을 호출해 출력을 완료합니다. 마찬가지로, Lua의 os.execute
호출 대신 resty.shell
을 사용할 수 있습니다.
우리는 lua-resty-shell
의 내부 구현이 lua-resty-core
의 ngx.pipe
API에 의존한다는 것을 알고 있습니다. 따라서 이 예제는 lua-resty-shell
을 사용해 hello world
를 출력하며, ngx.pipe
를 대신 사용하면 다음과 같습니다.
$ resty -e 'local ngx_pipe = require "ngx.pipe"
local proc = ngx_pipe.spawn({"echo", "hello world"})
local data, err = proc:stdout_read_line()
ngx.say(data)'
위는 lua-resty-shell
구현의 내부 코드입니다. ngx.pipe
문서와 테스트 케이스를 확인해 더 많은 사용 방법을 알아볼 수 있습니다. 따라서 여기서는 더 자세히 다루지 않겠습니다.
요약
오늘의 주요 내용은 여기까지입니다. 위의 기능에서 볼 수 있듯이, OpenResty는 더 나은 NGINX를 만드는 동시에 범용 플랫폼으로 나아가려고 노력하고 있으며, 개발자들이 기술 스택을 통일하고 OpenResty를 사용해 개발 요구사항을 해결할 수 있기를 바라고 있습니다. 이는 운영 및 유지보수에 매우 친화적입니다. OpenResty를 배포하기만 하면 유지보수 비용이 낮아지기 때문입니다.
마지막으로, 생각할 거리를 남기겠습니다. NGINX Worker
가 여러 개 있을 수 있으므로, timer
는 각 Worker
마다 한 번씩 실행됩니다. 이는 대부분의 시나리오에서 받아들일 수 없습니다. timer
가 한 번만 실행되도록 보장하려면 어떻게 해야 할까요?
여러분의 해결책을 댓글로 남겨주세요. 이 글을 동료와 친구들과 공유해 함께 소통하고 발전할 수 있기를 바랍니다.