OpenResty의 킬러 기능: 동적
API7.ai
January 12, 2023
지금까지 OpenResty의 성능과 관련된 내용을 거의 마무리했습니다. 이러한 최적화 기술을 숙지하고 유연하게 적용하면 우리 코드의 성능을 크게 향상시킬 수 있습니다. 오늘은 성능 최적화의 마지막 부분으로, OpenResty에서 자주 간과되는 기능인 "동적(dynamic)"에 대해 알아보겠습니다.
먼저 동적이 무엇인지, 그리고 성능과 어떤 관련이 있는지 살펴보겠습니다. 여기서 동적이란 프로그램이 실행 중에 매개변수, 설정, 심지어 코드까지도 재로드 없이 수정할 수 있음을 의미합니다. 구체적으로, NGINX와 OpenResty에서는 서비스를 재시작하지 않고도 업스트림, SSL 인증서, 속도 제한 임계값 등을 변경하여 동적 기능을 구현할 수 있습니다. 동적과 성능의 관계는 명확합니다. 이러한 작업을 동적으로 수행할 수 없다면, NGINX 서비스를 자주 재로드해야 하므로 자연스럽게 성능 손실이 발생합니다.
그러나 오픈소스 버전의 NGINX는 동적 기능을 지원하지 않기 때문에, 업스트림 SSL 인증서를 변경하려면 설정 파일을 수정하고 서비스를 재시작해야 합니다. NGINX Plus(NGINX의 상용 버전)는 일부 동적 기능을 제공하며 REST API를 사용하여 업데이트할 수 있지만, 이는 근본적인 개선이라고 보기 어렵습니다.
OpenResty에서는 이러한 제약이 없으며, 동적 기능은 OpenResty의 핵심 기능입니다. NGINX를 기반으로 한 OpenResty가 왜 동적 기능을 지원할 수 있는지 궁금할 수 있습니다. 그 이유는 간단합니다. NGINX의 로직은 C 모듈을 통해 구현되지만, OpenResty는 스크립트 언어인 Lua를 통해 구현되기 때문입니다. 스크립트 언어의 장점 중 하나는 실행 중에 동적으로 변경할 수 있다는 점입니다.
동적으로 코드 로드하기
OpenResty에서 Lua 코드를 동적으로 로드하는 방법을 살펴보겠습니다.
resty -e 'local s = [[ngx.say("hello world")]]
local func, err = loadstring(s)
func()'
몇 줄의 코드만으로 문자열을 Lua 함수로 변환하고 실행할 수 있음을 확인할 수 있습니다. 이 코드를 좀 더 자세히 살펴보겠습니다:
- 먼저,
hello world
를 출력하는 Lua 코드를 포함한 문자열을 선언합니다. - 그런 다음, Lua의
loadstring
함수를 사용하여 문자열 객체를 함수 객체func
로 변환합니다. - 마지막으로, 함수 이름에 괄호를 추가하여
func
를 실행하고hello world
를 출력합니다.
물론, 이 코드를 기반으로 더 흥미롭고 실용적인 기능을 확장할 수도 있습니다. 다음으로, 이를 시도해보겠습니다.
기능 1: FaaS
첫 번째는 최근 매우 인기 있는 기술 방향인 FaaS(Function-as-a-Service)입니다. OpenResty에서 이를 어떻게 구현할 수 있는지 살펴보겠습니다. 앞서 언급한 코드에서 문자열은 Lua 코드입니다. 이를 Lua 함수로 변경할 수도 있습니다:
local s = [[
return function()
ngx.say("hello world")
end
]]
앞서 말했듯이, Lua에서 함수는 일급 시민입니다. 이 코드는 익명 함수를 반환합니다. 이 익명 함수를 실행할 때는 pcall
을 사용하여 보호 계층을 제공합니다. pcall
은 함수를 보호 모드에서 실행하고 예외를 포착합니다. 정상적으로 실행되면 true
와 실행 결과를 반환하고, 실패하면 false
와 오류 정보를 반환합니다. 이는 다음 코드와 같습니다:
local func1, err = loadstring(s)
local ret, func = pcall(func1)
당연히, 위의 두 부분을 결합하면 완전하고 실행 가능한 예제를 얻을 수 있습니다:
resty -e 'local s = [[
return function()
ngx.say("hello world")
end
]]
local func1 = loadstring(s)
local ret, func = pcall(func1)
func()'
한 걸음 더 나아가, 함수를 포함하는 문자열 s
를 사용자가 지정할 수 있는 형태로 변경하고 실행 조건을 추가할 수 있습니다. 이것이 FaaS의 프로토타입입니다. 여기서 완전한 구현을 제공합니다. FaaS에 관심이 있고 연구를 계속하고 싶다면, 링크를 통해 더 알아보세요.
기능 2: 에지 컴퓨팅
OpenResty의 동적 기능은 FaaS에 사용될 수 있으며, 스크립트 언어의 동적 기능을 함수 수준으로 정교화하여 에지 컴퓨팅에서 동적 역할을 할 수 있습니다.
이러한 장점 때문에, OpenResty의 활용 범위를 API 게이트웨이, WAF(웹 애플리케이션 방화벽), 웹 서버 등의 서버 측면에서 사용자와 가장 가까운 에지 노드(예: IoT 장치, CDN 에지 노드, 라우터 등)로 확장할 수 있습니다.
이는 단순한 상상이 아닙니다. OpenResty는 이미 위의 분야에서 널리 사용되고 있습니다. CDN 에지 노드를 예로 들면, OpenResty의 가장 큰 사용자인 Cloudflare는 오랫동안 OpenResty의 동적 기능을 활용하여 CDN 에지 노드의 동적 제어를 실현해왔습니다.
Cloudflare의 접근 방식은 위에서 설명한 동적 코드 로드 원리와 유사하며, 대략 다음과 같은 단계로 나눌 수 있습니다:
- 먼저, 키-값 데이터베이스 클러스터에서 변경된 코드 파일을 가져옵니다. 이 방법은 백그라운드 타이머 폴링 또는 "발행-구독" 모드를 통해 모니터링할 수 있습니다.
- 그런 다음, 로컬 디스크의 이전 파일을 업데이트된 코드 파일로 교체하고,
loadstring
및pcall
방법을 사용하여 메모리에 로드된 캐시를 업데이트합니다.
이렇게 하면, 다음에 처리될 클라이언트 요청은 업데이트된 코드 로직을 통해 처리됩니다. 물론, 실제 적용에서는 버전 관리 및 롤백, 예외 처리, 네트워크 중단, 에지 노드 재시작 등 더 많은 세부 사항을 고려해야 하지만, 전체 프로세스는 변함없습니다.
Cloudflare의 접근 방식을 CDN 에지 노드에서 다른 에지 시나리오로 옮기면, 에지 노드 장치에 많은 컴퓨팅 능력을 동적으로 할당할 수 있습니다. 이는 에지 노드의 컴퓨팅 능력을 충분히 활용할 뿐만 아니라, 사용자가 요청에 대해 더 빠른 응답을 얻을 수 있게 해줍니다. 에지 노드가 원본 데이터를 처리한 후 원격 서버로 요약하여 전송하므로 데이터 전송량이 크게 줄어듭니다.
그러나 FaaS와 에지 컴퓨팅에서 뛰어난 성과를 내기 위해서는 OpenResty의 동적 기능만으로는 충분하지 않습니다. 주변 생태계의 개선과 제조업체의 참여도 고려해야 하며, 이는 단순히 기술적인 범주에 속하지 않습니다.
동적 업스트림
이제 OpenResty로 돌아와 동적 업스트림을 어떻게 구현할 수 있는지 살펴보겠습니다. lua-resty-core
는 ngx.balancer
라이브러리를 제공하여 업스트림을 설정합니다. 이는 OpenResty의 balancer
단계에서 실행되어야 합니다:
balancer_by_lua_block {
local balancer = require "ngx.balancer"
local host = "127.0.0.2"
local port = 8080
local ok, err = balancer.set_current_peer(host, port)
if not ok then
ngx.log(ngx.ERR, "failed to set the current peer: ", err)
return ngx.exit(500)
end
}
set_current_peer
함수는 업스트림 IP 주소와 포트를 설정합니다. 그러나 여기서 도메인 이름은 지원되지 않습니다. 도메인 이름과 IP를 분석하기 위해 lua-resty-dns
라이브러리를 사용해야 합니다.
그러나 ngx.balancer
는 상대적으로 낮은 수준입니다. 업스트림을 설정하는 데 사용할 수 있지만, 동적 업스트림 구현은 단순하지 않습니다. 따라서 ngx.balancer
앞에 두 가지 기능이 필요합니다:
- 첫째, 업스트림 선택 알고리즘이
consistent hash
인지roundrobin
인지 결정합니다. - 둘째, 업스트림 건강 검사 메커니즘으로, 비정상적인 업스트림을 제거하고 비정상적인 업스트림이 정상으로 돌아오면 다시 추가합니다.
OpenResty의 공식 lua-resty-balancer
라이브러리는 resty.chash
와 resty.roundrobin
두 가지 알고리즘을 포함하여 첫 번째 기능을 완료하며, lua-resty-upstream-healthcheck
를 통해 두 번째 기능을 시도합니다.
그러나 여전히 두 가지 문제가 있습니다.
첫 번째는 마지막 단계의 완전한 구현이 부족하다는 점입니다. ngx.balancer
, lua-resty-balancer
, lua-resty-upstream-healthcheck
를 결합하여 동적 업스트림 기능을 구현하려면 여전히 일부 작업이 필요하며, 이는 대부분의 개발자를 막는 장벽입니다.
두 번째는 lua-resty-upstream-healthcheck
의 구현이 완전하지 않다는 점입니다. 수동 건강 검사만 있고, 능동 건강 검사는 없습니다.
여기서 수동 건강 검사는 클라이언트의 요청에 의해 트리거되며, 업스트림 반환 값을 분석하여 건강 상태를 판단합니다. 클라이언트 요청이 없으면 업스트림이 건강한지 알 수 없습니다. 능동 건강 검사는 이 결함을 보완할 수 있습니다. ngx.timer
를 사용하여 주기적으로 지정된 업스트림 인터페이스를 폴링하여 건강 상태를 감지합니다.
따라서 실제로는 lua-resty-healthcheck
를 사용하여 업스트림 건강 검사를 완료하는 것을 권장합니다. 이 라이브러리의 장점은 능동 및 수동 건강 검사를 모두 포함하며, 여러 프로젝트에서 검증되어 더 높은 신뢰성을 가지고 있습니다.
또한, 최신 마이크로서비스 API 게이트웨이인 Apache APISIX는 lua-resty-upstream-healthcheck
를 기반으로 동적 업스트림의 완전한 구현을 이루었습니다. 그 구현을 참조할 수 있습니다. 총 400줄의 코드로, 쉽게 프로젝트에 적용할 수 있습니다.
요약
OpenResty의 동적 기능을 어떤 분야와 시나리오에서 활용할 수 있는지에 대해 알아보았습니다. 이 장에서 소개한 각 부분의 내용을 확장하여 더 자세하고 깊이 있는 분석을 할 수도 있습니다.
이 글을 공유하고 더 많은 사람들과 함께 배우고 성장하길 바랍니다.