El núcleo de OpenResty: cosocket

API7.ai

October 28, 2022

OpenResty (NGINX + Lua)

Hoy aprenderemos sobre la tecnología central en OpenResty: cosocket.

Lo hemos mencionado muchas veces en los artículos anteriores, cosocket es la base de varias bibliotecas no bloqueantes lua-resty-*. Sin cosocket, los desarrolladores no pueden usar Lua para conectarse rápidamente a servicios web externos.

En versiones anteriores de OpenResty, si querías interactuar con servicios como Redis y memcached, necesitabas usar los módulos C redis2-nginx-module, redis-nginx-module y memc-nginx-module. Estos módulos todavía están disponibles en la distribución de OpenResty.

Sin embargo, con la adición de la función cosocket, los módulos C han sido reemplazados por lua-resty-redis y lua-resty-memcached. Ya nadie usa módulos C para conectarse a servicios externos.

¿Qué es cosocket?

Entonces, ¿qué es exactamente cosocket? cosocket es un término propio en OpenResty. El nombre cosocket está compuesto por coroutine + socket.

cosocket requiere soporte para la función de concurrencia de Lua y el mecanismo de eventos fundamental en NGINX, que combinados permiten E/S de red no bloqueante. cosocket también soporta TCP, UDP y Unix Domain Socket.

La implementación interna se ve como el siguiente diagrama si llamamos a una función relacionada con cosocket en OpenResty.

Llamar a una función relacionada con cosocket

También usé este diagrama en el artículo anterior sobre Principios y conceptos básicos de OpenResty. Como puedes ver en el diagrama, para cada operación de red desencadenada por el script Lua del usuario, ambos tendrán el yield y resume de la corrutina.

Cuando encuentra E/S de red, registra el evento de red en la lista de escucha de NGINX y transfiere el control (yield) a NGINX. Cuando un evento de NGINX alcanza la condición de activación, despierta la corrutina para continuar el procesamiento (resume).

El proceso anterior es el plano que OpenResty usa para encapsular las operaciones de conexión, envío, recepción, etc., que componen las API de cosocket que vemos hoy. Usaré la API para manejar TCP como ejemplo. La interfaz para controlar UDP y Unix Domain sockets es la misma que la de TCP.

Introducción a las API y comandos de cosocket

Las API de cosocket relacionadas con TCP se pueden dividir en las siguientes categorías.

  • Crear objetos: ngx.socket.tcp.
  • Establecer tiempo de espera: tcpsock:settimeout y tcpsock:settimeouts.
  • Establecer conexión: tcpsock:connect.
  • Enviar datos: tcpsock:send.
  • Recibir datos: tcpsock:receive, tcpsock:receiveany y tcpsock:receiveuntil.
  • Agrupación de conexiones: tcpsock:setkeepalive.
  • Cerrar la conexión: tcpsock:close.

También debemos prestar especial atención a los contextos en los que se pueden usar estas API.

rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_

Otro punto que también quiero enfatizar es que existen muchos entornos no disponibles debido a varias limitaciones en el núcleo de NGINX. Por ejemplo, la API de cosocket no está disponible en set_by_lua*, log_by_lua*, header_filter_by_lua* y body_filter_by_lua*. No está disponible en init_by_lua* e init_worker_by_lua* por ahora, pero el núcleo de NGINX no restringe estas dos fases, el soporte para las cuales se puede agregar más tarde.

Hay ocho comandos de NGINX que comienzan con lua_socket_ relacionados con estas API. Echemos un vistazo brevemente.

  • lua_socket_connect_timeout: tiempo de espera de conexión, predeterminado 60 segundos.
  • lua_socket_send_timeout: tiempo de espera de envío, predeterminado 60 segundos.
  • lua_socket_send_lowat: umbral de envío (bajo nivel de agua), predeterminado es 0.
  • lua_socket_read_timeout: tiempo de espera de lectura, predeterminado 60 segundos.
  • lua_socket_buffer_size: tamaño del búfer para leer datos, predeterminado 4k/8k.
  • lua_socket_pool_size: tamaño del grupo de conexiones, predeterminado 30.
  • lua_socket_keepalive_timeout: tiempo de inactividad del objeto cosocket del grupo de conexiones, predeterminado 60 segundos.
  • lua_socket_log_errors: si registrar errores de cosocket cuando ocurren, predeterminado es on.

Aquí también puedes ver que algunos comandos tienen la misma funcionalidad que la API, como establecer el tiempo de espera y el tamaño del grupo de conexiones. Sin embargo, si hay un conflicto entre los dos, la API tiene mayor prioridad que los comandos y anulará el valor establecido por el comando. Por lo tanto, en general, recomendamos usar las API para hacer las configuraciones, lo que también es más flexible.

A continuación, veamos un ejemplo concreto para entender cómo usar estas API de cosocket. La función del siguiente código es simple, que envía una solicitud TCP a un sitio web e imprime el contenido devuelto:

$ resty -e 'local sock = ngx.socket.tcp()
sock:settimeout(1000) -- un segundo de tiempo de espera
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)'

Analicemos este código en detalle.

  • Primero, crea un objeto TCP cosocket con el nombre sock usando ngx.socket.tcp().
  • Luego, usa settimeout() para establecer el tiempo de espera en 1 segundo. Ten en cuenta que el tiempo de espera aquí no diferencia entre conectar y recibir; es una configuración uniforme.
  • A continuación, usa la API connect() para conectarse al puerto 80 del sitio web especificado y sale si falla.
  • Si la conexión es exitosa, usa send() para enviar los datos construidos y sale si falla.
  • Si el envío de datos es exitoso, usa receive() para recibir los datos del sitio web. Aquí, el parámetro predeterminado de receive() es *l, lo que significa que solo devuelve la primera línea de datos. Si el parámetro se establece en *a, recibe datos hasta que se cierra la conexión.
  • Finalmente, llama a close() para cerrar activamente la conexión del socket.

Como puedes ver, usar las API de cosocket para hacer comunicación de red es simple en solo unos pocos pasos. Hagamos algunos ajustes para explorar el ejemplo en profundidad.

1. Establecer el tiempo de espera para cada una de las tres acciones: conexión de socket, envío y lectura.

El settimeout() que usamos para establecer el tiempo de espera en un solo valor. Para establecer el tiempo de espera por separado, necesitas usar la función settimeouts(), como la siguiente.

sock:settimeouts(1000, 2000, 3000)

Los parámetros de settimeouts están en milisegundos. Esta línea de código indica un tiempo de espera de conexión de 1 segundo, un tiempo de espera de envío de 2 segundos y un tiempo de espera de lectura de 3 segundos.

En OpenResty y las bibliotecas lua-resty, la mayoría de los parámetros de las API relacionadas con el tiempo están en milisegundos. Pero hay excepciones a las que debes prestar especial atención al llamarlas.

2. Recibir el contenido del tamaño especificado.

Como acabo de decir, la API receive() puede recibir una línea de datos o recibir datos continuamente. Sin embargo, si solo quieres recibir datos de 10K de tamaño, ¿cómo deberías configurarlo?

Ahí es donde entra receiveany(). Está diseñado para satisfacer esta necesidad, así que mira la siguiente línea de código.

local data, err, partial = sock:receiveany(10240)

Este código significa que solo se recibirán hasta 10K de datos.

Por supuesto, otro requisito común para receive() es seguir obteniendo datos hasta que encuentre la cadena especificada.

El receiveuntil() está diseñado para resolver este tipo de problema. En lugar de devolver una cadena como receive() y receiveany(), devolverá un iterador. De esta manera, puedes llamarlo en un bucle para leer los datos coincidentes en segmentos y devolver nil cuando la lectura esté completa. Aquí tienes un ejemplo.

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

El receiveuntil devuelve los datos antes de \r\n y lee cuatro bytes de ellos a la vez a través del iterador.

3. En lugar de cerrar el socket directamente, ponerlo en el grupo de conexiones.

Como sabemos, sin agrupación de conexiones, se debe crear una nueva conexión, lo que hace que los objetos cosocket se creen cada vez que llega una solicitud y se destruyan con frecuencia, lo que resulta en una pérdida de rendimiento innecesaria.

Para evitar este problema, después de terminar de usar un cosocket, puedes llamar a setkeepalive() para ponerlo en el grupo de conexiones, como el siguiente.

local ok, err = sock:setkeepalive(2 * 1000, 100)
if not ok then
    ngx.say("failed to set reusable: ", err)
end

Este código establece el tiempo de inactividad de la conexión en 2 segundos y el tamaño del grupo de conexiones en 100, de modo que cuando se llama a la función connect(), el objeto cosocket se obtendrá primero del grupo de conexiones.

Sin embargo, hay dos cosas que debemos tener en cuenta al usar la agrupación de conexiones.

  • Primero, no puedes poner una conexión errónea en el grupo de conexiones. De lo contrario, la próxima vez que la uses, fallará al enviar y recibir datos. Es una de las razones por las que necesitamos determinar si cada llamada a la API es exitosa o no.
  • Segundo, necesitamos averiguar el número de conexiones. La agrupación de conexiones es a nivel de Worker, y cada Worker tiene su propio grupo de conexiones. Si tienes 10 Workers y el tamaño del grupo de conexiones se establece en 30, eso son 300 conexiones para el servicio backend.

Resumen

Para resumir, aprendimos los conceptos básicos, los comandos relacionados y las API de cosocket. Un ejemplo práctico nos familiarizó con cómo usar las API relacionadas con TCP. El uso de UDP y Unix Domain Socket es similar al de TCP. Puedes manejar fácilmente todas estas preguntas después de entender lo que aprendimos hoy.

Sabemos que cosocket es relativamente fácil de usar, y podemos conectarnos a varios servicios externos usándolo bien.

Finalmente, podemos pensar en dos preguntas.

La primera pregunta, en el ejemplo de hoy, tcpsock:send envía una cadena; ¿qué pasa si necesitamos enviar una tabla compuesta de cadenas?

La segunda pregunta, como puedes ver, cosocket no se puede usar en muchas etapas, ¿puedes pensar en algunas formas de evitarlo?

No dudes en dejar un comentario y compartirlo conmigo. Bienvenido a compartir este artículo con tus colegas y amigos para que podamos comunicarnos y progresar juntos.