OpenResty의 핵심: cosocket
API7.ai
October 28, 2022
오늘은 OpenResty의 핵심 기술인 cosocket에 대해 배워보겠습니다.
이전 글에서 여러 번 언급했듯이, cosocket은 다양한 lua-resty-*
비차단 라이브러리의 기반입니다. cosocket이 없다면 개발자들은 Lua를 사용하여 외부 웹 서비스에 빠르게 연결할 수 없습니다.
OpenResty의 초기 버전에서는 Redis나 memcached와 같은 서비스와 상호작용하기 위해 redis2-nginx-module
, redis-nginx-module
, memc-nginx-module
과 같은 C 모듈을 사용해야 했습니다. 이러한 모듈들은 여전히 OpenResty 배포판에 포함되어 있습니다.
그러나 cosocket 기능이 추가되면서 C 모듈은 lua-resty-redis
와 lua-resty-memcached
로 대체되었습니다. 이제는 더 이상 C 모듈을 사용하여 외부 서비스에 연결하지 않습니다.
cosocket이란 무엇인가?
그렇다면 cosocket이 정확히 무엇일까요? cosocket은 OpenResty에서 사용되는 고유 명사입니다. cosocket이라는 이름은 coroutine + socket
의 조합으로 이루어져 있습니다.
cosocket은 Lua의 동시성 기능과 NGINX의 기본 이벤트 메커니즘을 결합하여 비차단 네트워크 I/O를 가능하게 합니다. 또한 cosocket은 TCP, UDP, Unix Domain Socket을 지원합니다.
OpenResty에서 cosocket 관련 함수를 호출하면 내부적으로 다음과 같은 다이어그램과 같이 동작합니다.
이전 글인 OpenResty 원리와 기본 개념에서도 이 다이어그램을 사용했습니다. 다이어그램에서 볼 수 있듯이, 사용자의 Lua 스크립트에 의해 트리거된 모든 네트워크 작업은 코루틴의 yield
와 resume
을 갖게 됩니다.
네트워크 I/O를 만나면 네트워크 이벤트를 NGINX 리스너 목록에 등록하고 제어권(yield)을 NGINX로 넘깁니다. NGINX 이벤트가 트리거 조건에 도달하면 코루틴을 깨워 계속 처리(resume)합니다.
위 과정은 OpenResty가 현재 우리가 보는 cosocket API를 구성하는 connect, send, receive 등의 작업을 캡슐화하는 청사진입니다. 여기서는 TCP를 처리하는 API를 예로 들겠습니다. UDP와 Unix Domain 소켓을 제어하는 인터페이스도 TCP와 동일합니다.
cosocket API 및 명령어 소개
TCP 관련 cosocket API는 다음과 같은 범주로 나눌 수 있습니다.
- 객체 생성:
ngx.socket.tcp
. - 타임아웃 설정:
tcpsock:settimeout
및tcpsock:settimeouts
. - 연결 수립:
tcpsock:connect
. - 데이터 전송:
tcpsock:send
. - 데이터 수신:
tcpsock:receive
,tcpsock:receiveany
,tcpsock:receiveuntil
. - 연결 풀링:
tcpsock:setkeepalive
. - 연결 종료:
tcpsock:close
.
또한 이러한 API를 사용할 수 있는 컨텍스트에 특히 주의해야 합니다.
rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_
또한 강조하고 싶은 점은 NGINX 커널의 다양한 제한으로 인해 사용할 수 없는 환경이 많다는 것입니다. 예를 들어, cosocket API는 set_by_lua*
, log_by_lua*
, header_filter_by_lua*
, body_filter_by_lua*
에서 사용할 수 없습니다. 현재는 init_by_lua*
와 init_worker_by_lua*
에서도 사용할 수 없지만, NGINX 커널이 이 두 단계를 제한하지 않기 때문에 나중에 지원이 추가될 수 있습니다.
이러한 API와 관련된 lua_socket_
으로 시작하는 8개의 NGINX 명령어가 있습니다. 간단히 살펴보겠습니다.
lua_socket_connect_timeout
: 연결 타임아웃, 기본값 60초.lua_socket_send_timeout
: 전송 타임아웃, 기본값 60초.lua_socket_send_lowat
: 전송 임계값(low water), 기본값 0.lua_socket_read_timeout
: 읽기 타임아웃, 기본값 60초.lua_socket_buffer_size
: 데이터 읽기 버퍼 크기, 기본값 4k/8k.lua_socket_pool_size
: 연결 풀 크기, 기본값 30.lua_socket_keepalive_timeout
: 연결 풀 cosocket 객체의 유휴 시간, 기본값 60초.lua_socket_log_errors
: cosocket 오류 발생 시 로깅 여부, 기본값on
.
여기서도 볼 수 있듯이, 일부 명령어는 API와 동일한 기능을 가지고 있습니다. 예를 들어 타임아웃 설정과 연결 풀 크기 설정 등이 있습니다. 그러나 두 가지가 충돌할 경우 API가 명령어보다 우선순위가 높으며, 명령어로 설정된 값을 덮어씁니다. 따라서 일반적으로 API를 사용하여 설정하는 것을 권장하며, 이는 더 유연합니다.
다음으로, 이러한 cosocket API를 사용하는 방법을 이해하기 위해 구체적인 예제를 살펴보겠습니다. 다음 코드의 기능은 간단합니다. 웹사이트에 TCP 요청을 보내고 반환된 내용을 출력합니다:
$ resty -e 'local sock = ngx.socket.tcp()
sock:settimeout(1000) -- 1초 타임아웃
local ok, err = sock:connect("api7.ai", 80)
if not ok then
ngx.say("failed to connect: ", err)
return
end
local req_data = "GET / HTTP/1.1\r\nHost: api7.ai\r\n\r\n"
local bytes, err = sock:send(req_data)
if err then
ngx.say("failed to send: ", err)
return
end
local data, err, partial = sock:receive()
if err then
ngx.say("failed to receive: ", err)
return
end
sock:close()
ngx.say("response is: ", data)'
이 코드를 자세히 분석해 보겠습니다.
- 먼저,
ngx.socket.tcp()
를 사용하여sock
이라는 이름의 TCP cosocket 객체를 생성합니다. - 그런 다음,
settimeout()
을 사용하여 타임아웃을 1초로 설정합니다. 여기서 타임아웃은 연결과 수신을 구분하지 않고 통일된 설정입니다. - 다음으로,
connect()
API를 사용하여 지정된 웹사이트의 80번 포트에 연결하고, 실패하면 종료합니다. - 연결이 성공하면,
send()
를 사용하여 구성된 데이터를 전송하고, 실패하면 종료합니다. - 데이터 전송이 성공하면,
receive()
를 사용하여 웹사이트로부터 데이터를 수신합니다. 여기서receive()
의 기본 매개변수는*l
이며, 이는 첫 번째 줄의 데이터만 반환합니다. 매개변수를*a
로 설정하면 연결이 닫힐 때까지 데이터를 수신합니다. - 마지막으로,
close()
를 호출하여 소켓 연결을 명시적으로 닫습니다.
보시다시피, cosocket API를 사용하여 네트워크 통신을 하는 것은 몇 단계만으로 간단합니다. 이제 이 예제를 더 깊이 탐구하기 위해 몇 가지 조정을 해보겠습니다.
1. 소켓 연결, 전송 및 읽기 세 가지 작업에 대해 각각 타임아웃 시간을 설정합니다.
우리가 사용한 settimeout()
은 타임아웃 시간을 단일 값으로 설정합니다. 각각의 타임아웃 시간을 별도로 설정하려면 settimeouts()
함수를 사용해야 합니다. 예를 들어 다음과 같습니다.
sock:settimeouts(1000, 2000, 3000)
settimeouts
의 매개변수는 밀리초 단위입니다. 이 코드는 연결 타임아웃을 1
초, 전송 타임아웃을 2
초, 읽기 타임아웃을 3
초로 설정합니다.
OpenResty와 lua-resty 라이브러리에서 시간 관련 API의 대부분의 매개변수는 밀리초 단위입니다. 하지만 호출할 때 특별히 주의해야 할 예외도 있습니다.
2. 지정된 크기의 내용을 수신합니다.
방금 말했듯이, receive()
API는 한 줄의 데이터를 수신하거나 지속적으로 데이터를 수신할 수 있습니다. 그러나 10K 크기의 데이터만 수신하려면 어떻게 설정해야 할까요?
이때 receiveany()
가 필요합니다. 이는 이러한 요구를 충족시키기 위해 설계되었으므로 다음 코드를 살펴보세요.
local data, err, partial = sock:receiveany(10240)
이 코드는 최대 10K의 데이터만 수신한다는 의미입니다.
물론, receive()
에 대한 또 다른 일반적인 사용자 요구 사항은 지정된 문자열을 만날 때까지 데이터를 계속 가져오는 것입니다.
receiveuntil()
은 이러한 종류의 문제를 해결하기 위해 설계되었습니다. receive()
와 receiveany()
처럼 문자열을 반환하는 대신, 이터레이터를 반환합니다. 이렇게 하면 루프에서 호출하여 일치하는 데이터를 세그먼트로 읽고, 읽기가 완료되면 nil을 반환할 수 있습니다. 다음은 예제입니다.
local reader = sock:receiveuntil("\r\n")
while true do
local data, err, partial = reader(4)
if not data then
if err then
ngx.say("failed to read the data stream: ", err)
break
end
ngx.say("read done")
break
end
ngx.say("read chunk: [", data, "]")
end
receiveuntil
은 \r\n
이전의 데이터를 반환하고, 이터레이터를 통해 한 번에 4바이트씩 읽습니다.
3. 소켓을 직접 닫는 대신 연결 풀에 넣습니다.
연결 풀이 없으면 새로운 연결을 생성해야 하며, 요청이 올 때마다 cosocket 객체가 생성되고 자주 파괴되어 불필요한 성능 손실이 발생합니다.
이 문제를 피하기 위해, cosocket 사용을 마친 후 setkeepalive()
를 호출하여 연결 풀에 넣을 수 있습니다. 예를 들어 다음과 같습니다.
local ok, err = sock:setkeepalive(2 * 1000, 100)
if not ok then
ngx.say("failed to set reusable: ", err)
end
이 코드는 연결 유휴 시간을 2
초로, 연결 풀 크기를 100
으로 설정합니다. 이렇게 하면 connect()
함수가 호출될 때 cosocket 객체를 연결 풀에서 먼저 가져옵니다.
그러나 연결 풀을 사용할 때 주의해야 할 두 가지가 있습니다.
- 첫째, 오류가 있는 연결을 연결 풀에 넣을 수 없습니다. 그렇지 않으면 다음에 사용할 때 데이터 전송 및 수신이 실패할 수 있습니다. 이것이 각 API 호출이 성공했는지 확인해야 하는 이유 중 하나입니다.
- 둘째, 연결 수를 파악해야 합니다. 연결 풀은
Worker
수준이며, 각 Worker는 자신의 연결 풀을 가지고 있습니다. 만약 10개의Worker
가 있고 연결 풀 크기를30
으로 설정했다면, 백엔드 서비스에 대해 300개의 연결이 생성됩니다.
요약
요약하자면, 우리는 cosocket의 기본 개념, 관련 명령어 및 API를 배웠습니다. 실용적인 예제를 통해 TCP 관련 API를 사용하는 방법에 익숙해졌습니다. UDP와 Unix Domain Socket의 사용도 TCP와 유사합니다. 오늘 배운 내용을 이해하면 이러한 모든 질문을 쉽게 처리할 수 있습니다.
cosocket은 상대적으로 사용하기 쉽고, 이를 잘 사용하면 다양한 외부 서비스에 연결할 수 있습니다.
마지막으로, 두 가지 질문을 생각해 볼 수 있습니다.
첫 번째 질문, 오늘의 예제에서 tcpsock:send
는 문자열을 전송합니다. 만약 문자열로 구성된 테이블을 전송해야 한다면 어떻게 해야 할까요?
두 번째 질문, 보시다시피 cosocket은 많은 단계에서 사용할 수 없습니다. 그러면 이를 우회할 수 있는 방법을 생각해 볼 수 있을까요?
자유롭게 댓글을 남기고 공유해 주세요. 이 글을 동료와 친구들과 공유하여 함께 소통하고 발전할 수 있기를 바랍니다.