O núcleo do OpenResty: cosocket
API7.ai
October 28, 2022
Hoje vamos aprender sobre a tecnologia central do OpenResty: o cosocket.
Já mencionamos isso várias vezes nos artigos anteriores, o cosocket é a base de várias bibliotecas lua-resty-*
não bloqueantes. Sem o cosocket, os desenvolvedores não podem usar Lua para se conectar rapidamente a serviços web externos.
Nas versões anteriores do OpenResty, se você quisesse interagir com serviços como Redis e memcached, precisava usar os módulos C redis2-nginx-module
, redis-nginx-module
e memc-nginx-module
. Esses módulos ainda estão disponíveis na distribuição do OpenResty.
No entanto, com a adição do recurso cosocket, os módulos C foram substituídos por lua-resty-redis
e lua-resty-memcached
. Ninguém mais usa módulos C para se conectar a serviços externos.
O que é cosocket?
Então, o que exatamente é o cosocket? Cosocket é um termo próprio no OpenResty. O nome cosocket é composto por coroutine + socket
.
O cosocket requer suporte ao recurso de concorrência do Lua e o mecanismo de eventos fundamental no NGINX, que se combinam para permitir I/O de rede não bloqueante. O cosocket também suporta TCP, UDP e Unix Domain Socket.
A implementação interna se parece com o seguinte diagrama se chamarmos uma função relacionada ao cosocket no OpenResty.
Eu também usei este diagrama no artigo anterior sobre princípios e conceitos básicos do OpenResty. Como você pode ver no diagrama, para cada operação de rede acionada pelo script Lua do usuário, ambos terão o yield
e o resume
da corrotina.
Ao encontrar I/O de rede, ele registra o evento de rede na lista de Ouvintes do NGINX e transfere o controle (yield) para o NGINX. Quando um evento do NGINX atinge a condição de acionamento, ele acorda a corrotina para continuar o processamento (resume).
O processo acima é o plano que o OpenResty usa para encapsular as operações de conexão, envio, recebimento, etc., que compõem as APIs cosocket que vemos hoje. Vou usar a API para lidar com TCP como exemplo. A interface para controlar UDP e Unix Domain sockets é a mesma que a do TCP.
Introdução às APIs e comandos do cosocket
As APIs cosocket relacionadas ao TCP podem ser divididas nas seguintes categorias.
- Criar objetos:
ngx.socket.tcp
. - Definir tempo limite:
tcpsock:settimeout
etcpsock:settimeouts
. - Estabelecer conexão:
tcpsock:connect
. - Enviar dados:
tcpsock:send
. - Receber dados:
tcpsock:receive
,tcpsock:receiveany
etcpsock:receiveuntil
. - Pool de conexões:
tcpsock:setkeepalive
. - Fechar a conexão:
tcpsock:close
.
Também devemos prestar atenção especial aos contextos em que essas APIs podem ser usadas.
rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_
Outro ponto que também quero enfatizar é que existem muitos ambientes indisponíveis devido a várias limitações no kernel do NGINX. Por exemplo, a API cosocket não está disponível em set_by_lua*
, log_by_lua*
, header_filter_by_lua*
e body_filter_by_lua*
. Não está disponível em init_by_lua*
e init_worker_by_lua*
por enquanto, mas o kernel do NGINX não restringe essas duas fases, o suporte para as quais pode ser adicionado posteriormente.
Existem oito comandos do NGINX começando com lua_socket_
relacionados a essas APIs. Vamos dar uma olhada rapidamente.
lua_socket_connect_timeout
: tempo limite de conexão, padrão 60 segundos.lua_socket_send_timeout
: tempo limite de envio, padrão 60 segundos.lua_socket_send_lowat
: limite de envio (baixa água), padrão é 0.lua_socket_read_timeout
: tempo limite de leitura, padrão 60 segundos.lua_socket_buffer_size
: tamanho do buffer para leitura de dados, padrão 4k/8k.lua_socket_pool_size
: tamanho do pool de conexões, padrão 30.lua_socket_keepalive_timeout
: tempo ocioso do objeto cosocket do pool de conexões, padrão 60 segundos.lua_socket_log_errors
: se deve registrar erros do cosocket quando ocorrem, padrão éon
.
Aqui você também pode ver que alguns comandos têm a mesma funcionalidade que a API, como definir o tempo limite e o tamanho do pool de conexões. No entanto, se houver um conflito entre os dois, a API tem prioridade sobre os comandos e substituirá o valor definido pela ordem. Então, de modo geral, recomendamos usar as APIs para fazer as configurações, o que também é mais flexível.
A seguir, vamos ver um exemplo concreto para entender como usar essas APIs cosocket. A função do seguinte código é simples, que envia uma solicitação TCP para um site e imprime o conteúdo retornado:
$ resty -e 'local sock = ngx.socket.tcp()
sock:settimeout(1000) -- tempo limite de um segundo
local ok, err = sock:connect("api7.ai", 80)
if not ok then
ngx.say("falha ao conectar: ", 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("falha ao enviar: ", err)
return
end
local data, err, partial = sock:receive()
if err then
ngx.say("falha ao receber: ", err)
return
end
sock:close()
ngx.say("resposta é: ", data)'
Vamos analisar este código em detalhes.
- Primeiro, crie um objeto TCP cosocket com o nome
sock
usandongx.socket.tcp()
. - Em seguida, use
settimeout()
para definir o tempo limite para 1 segundo. Observe que o tempo limite aqui não diferencia entre conexão e recebimento; é uma configuração uniforme. - Em seguida, use a API
connect()
para se conectar à porta 80 do site especificado e saia se falhar. - Se a conexão for bem-sucedida, use
send()
para enviar os dados construídos e saia se falhar. - Se o envio de dados for bem-sucedido, use
receive()
para receber os dados do site. Aqui, o parâmetro padrão dereceive()
é*l
, o que significa que apenas a primeira linha de dados é retornada. Se o parâmetro for definido como*a
, ele recebe dados até que a conexão seja fechada. - Finalmente, chame
close()
para fechar a conexão do socket ativamente.
Como você pode ver, usar as APIs cosocket para fazer comunicação de rede é simples em apenas algumas etapas. Vamos fazer alguns ajustes para explorar o exemplo mais profundamente.
1. Defina o tempo limite para cada uma das três ações: conexão do socket, envio e leitura.
O settimeout()
que usamos para definir o tempo limite para um único valor. Para definir o tempo limite separadamente, você precisa usar a função settimeouts()
, como a seguir.
sock:settimeouts(1000, 2000, 3000)
Os parâmetros de settimeouts
estão em milissegundos. Esta linha de código indica um tempo limite de conexão de 1
segundo, um tempo limite de envio de 2
segundos e um tempo limite de leitura de 3
segundos.
No OpenResty e nas bibliotecas lua-resty, a maioria dos parâmetros das APIs relacionadas ao tempo estão em milissegundos. Mas há exceções que você precisa prestar atenção especial ao chamá-las.
2. Recebe o conteúdo do tamanho especificado.
Como eu acabei de dizer, a API receive()
pode receber uma linha de dados ou receber dados continuamente. No entanto, se você quiser receber apenas dados de 10K de tamanho, como deve configurar?
É aí que entra o receiveany()
. Ele foi projetado para atender a essa necessidade, então veja a seguinte linha de código.
local data, err, partial = sock:receiveany(10240)
Este código significa que apenas até 10K de dados serão recebidos.
Claro, outro requisito comum do usuário para receive()
é continuar buscando dados até encontrar a string especificada.
O receiveuntil()
foi projetado para resolver esse tipo de problema. Em vez de retornar uma string como receive()
e receiveany()
, ele retornará um iterador. Dessa forma, você pode chamá-lo em um loop para ler os dados correspondentes em segmentos e retornar nil quando a leitura estiver concluída. Aqui está um exemplo.
local reader = sock:receiveuntil("\r\n")
while true do
local data, err, partial = reader(4)
if not data then
if err then
ngx.say("falha ao ler o fluxo de dados: ", err)
break
end
ngx.say("leitura concluída")
break
end
ngx.say("leitura do bloco: [", data, "]")
end
O receiveuntil
retorna os dados antes de \r\n
e lê quatro bytes deles de cada vez através do iterador.
3. Em vez de fechar o socket diretamente, coloque-o no pool de conexões.
Como sabemos, sem o pool de conexões, uma nova conexão precisa ser criada, fazendo com que objetos cosocket sejam criados cada vez que uma solicitação chega e sejam frequentemente destruídos, resultando em perda de desempenho desnecessária.
Para evitar esse problema, após terminar de usar um cosocket, você pode chamar setkeepalive()
para colocá-lo no pool de conexões, como a seguir.
local ok, err = sock:setkeepalive(2 * 1000, 100)
if not ok then
ngx.say("falha ao definir reutilizável: ", err)
end
Este código define o tempo ocioso da conexão para 2
segundos e o tamanho do pool de conexões para 100
, de modo que quando a função connect()
for chamada, o objeto cosocket será buscado primeiro no pool de conexões.
No entanto, há duas coisas que precisamos estar cientes ao usar o pool de conexões.
- Primeiro, você não pode colocar uma conexão com erro no pool de conexões. Caso contrário, na próxima vez que você usá-la, ela falhará ao enviar e receber dados. É uma das razões pelas quais precisamos determinar se cada chamada de API foi bem-sucedida ou não.
- Segundo, precisamos descobrir o número de conexões. O pool de conexões é de nível
Worker
, e cada Worker tem seu próprio pool de conexões. Se você tiver 10Worker
s e o tamanho do pool de conexões for definido como30
, isso significa 300 conexões para o serviço de back-end.
Resumo
Para resumir, aprendemos os conceitos básicos, os comandos relacionados e as APIs do cosocket. Um exemplo prático nos familiarizou com como usar as APIs relacionadas ao TCP. O uso de UDP e Unix Domain Socket é semelhante ao do TCP. Você pode facilmente lidar com todas essas questões após entender o que aprendemos hoje.
Sabemos que o cosocket é relativamente fácil de usar, e podemos nos conectar a vários serviços externos usando-o bem.
Finalmente, podemos pensar em duas questões.
A primeira questão, no exemplo de hoje, tcpsock:send
envia uma string; e se precisarmos enviar uma tabela composta por strings?
A segunda questão, como você pode ver, o cosocket não pode ser usado em muitos estágios, então você pode pensar em algumas maneiras de contornar isso?
Sinta-se à vontade para deixar um comentário e compartilhá-lo comigo. Bem-vindo a compartilhar este artigo com seus colegas e amigos para que possamos nos comunicar e progredir juntos.