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:settimeoutetcpsock:settimeouts. - Estabelecer conexão:
tcpsock:connect. - Enviar dados:
tcpsock:send. - Receber dados:
tcpsock:receive,tcpsock:receiveanyetcpsock: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
sockusandongx.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 10Workers 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.