Preguntas frecuentes de OpenResty | Carga dinámica, NYI y almacenamiento en caché de Shared Dict
API7.ai
January 19, 2023
La serie de artículos sobre OpenResty ha sido actualizada hasta ahora, y la parte sobre optimización de rendimiento es todo lo que hemos aprendido. Felicitaciones a usted por no quedarse atrás, seguir aprendiendo y practicando activamente, y dejar sus pensamientos con entusiasmo.
Hemos recopilado muchas preguntas más típicas e interesantes, y aquí hay un vistazo a cinco de ellas.
Pregunta 1: ¿Cómo puedo lograr la carga dinámica de módulos Lua?
Descripción: Tengo una pregunta sobre la carga dinámica implementada en OpenResty. ¿Cómo puedo usar la función
loadstring
para terminar de cargar un nuevo archivo después de que haya sido reemplazado? Entiendo queloadstring
solo puede cargar cadenas, así que si quiero recargar un archivo/módulo Lua, ¿cómo puedo hacerlo en OpenResty?
Como sabemos, loadstring
se usa para cargar una cadena, mientras que loadfile
puede cargar un archivo específico, por ejemplo: loadfile("foo.lua")
. Estos dos comandos logran el mismo resultado. En cuanto a cómo cargar módulos Lua, aquí hay un ejemplo:
resty -e 'local s = [[
local ngx = ngx
local _M = {}
function _M.f()
ngx.say("hello world")
end
return _M
]]
local lua = loadstring(s)
local ret, func = pcall(lua)
func.f()'
El contenido de la cadena s
es un módulo Lua completo. Entonces, cuando encuentre un cambio en el código de este módulo, puede reiniciar la carga con loadstring
o loadfile
. De esta manera, las funciones y variables en él se actualizarán con él.
Para llevarlo un paso más allá, también puede envolver la obtención de cambios y la recarga con una capa llamada la función code_loader
.
local func = code_loader(name)
Esto hace que las actualizaciones de código sean mucho más concisas. Al mismo tiempo, code_loader
generalmente usa lru cache
para almacenar en caché s
y evitar llamar a loadstring
cada vez.
Pregunta 2: ¿Por qué OpenResty no prohíbe las operaciones de bloqueo?
Descripción: A lo largo de los años, siempre me he preguntado, ya que estas llamadas de bloqueo están oficialmente desaconsejadas, ¿por qué no simplemente deshabilitarlas? O agregar una bandera para que el usuario elija deshabilitarlas.
Aquí está mi opinión personal. Primero, porque el ecosistema alrededor de OpenResty no es perfecto, a veces tenemos que llamar a bibliotecas de bloqueo para implementar algunas funciones. Por ejemplo, antes de la versión 1.15.8, tenías que usar la biblioteca Lua os.execute
en lugar de lua-resty-shell
para llamar a comandos externos. Por ejemplo, en OpenResty, la lectura y escritura de archivos todavía solo es posible con la biblioteca de E/S de Lua, y no hay una alternativa no bloqueante.
En segundo lugar, OpenResty es muy cauteloso con este tipo de optimizaciones. Por ejemplo, lua-resty-core
se ha desarrollado durante mucho tiempo, pero nunca se ha habilitado por defecto, requiriendo que llames manualmente a require 'resty.core'
. Se habilitó hasta el último lanzamiento de la versión 1.15.8.
Finalmente, los mantenedores de OpenResty prefieren estandarizar las llamadas de bloqueo generando automáticamente código Lua altamente optimizado a través del compilador y DSL. Por lo tanto, no hay esfuerzo por hacer algo como opciones de bandera en la plataforma OpenResty en sí. Por supuesto, no estoy seguro de si esta dirección puede resolver el problema.
Desde el punto de vista de un desarrollador externo, el problema más práctico es cómo evitar tal bloqueo. Podemos extender las herramientas de detección de código Lua, como luacheck
, para encontrar y alertar sobre operaciones de bloqueo comunes, o podemos deshabilitar o reescribir intrusivamente ciertas funciones directamente reescribiendo _G
, por ejemplo:
resty -e '_G.ngx.print = function()
ngx.say("hello")
end
ngx.print()'
# hello
Con este código de ejemplo, puedes reescribir directamente la función ngx.print
.
Pregunta 3: ¿La operación de NYI de LuaJIT tiene un impacto significativo en el rendimiento?
Descripción:
loadstring
muestranever
en la lista NYI de LuaJIT. ¿Tendrá un gran impacto en el rendimiento?
En cuanto a NYI de LuaJIT, no necesitamos ser demasiado estrictos. Para operaciones que pueden ser JIT, el enfoque JIT es naturalmente el mejor; pero para operaciones que aún no pueden ser JIT, podemos seguir usándolas.
Para la optimización del rendimiento, necesitamos tomar un enfoque científico basado en estadísticas, que es de lo que se trata el muestreo de gráficos de llamas. La optimización prematura es la raíz de todo mal. Solo necesitamos optimizar el código caliente que hace muchas llamadas y consume mucha CPU.
Volviendo a loadstring
, solo lo llamaremos para recargar cuando el código cambie, no cuando se solicite, por lo que no es una operación frecuente. En este punto, no tenemos que preocuparnos por su impacto en el rendimiento general del sistema.
En conjunto con el segundo problema de bloqueo, en OpenResty, a veces también invocamos operaciones de E/S de archivos de bloqueo durante las fases init
y init worker
. Esta operación es más comprometedora en términos de rendimiento que NYI, pero como se realiza solo una vez cuando se inicia el servicio, es aceptable.
Como siempre, la optimización del rendimiento debe verse desde una perspectiva macro, un punto al que debes prestar especial atención. De lo contrario, al obsesionarse con un detalle en particular, es probable que optimices durante mucho tiempo pero no tengas un buen efecto.
Pregunta 4: ¿Puedo implementar un upstream dinámico por mi cuenta?
Descripción: Para el upstream dinámico, mi enfoque es configurar 2 upstreams para un servicio, seleccionar diferentes upstreams según las condiciones de enrutamiento, y modificar directamente la IP en el upstream cuando cambia la IP de la máquina. ¿Hay alguna desventaja o trampa en este enfoque en comparación con usar
balancer_by_lua
directamente?
La ventaja de balancer_by_lua
es que permite al usuario elegir el algoritmo de balanceo de carga, por ejemplo, si usar roundrobin
o chash
, o cualquier otro algoritmo que el usuario implemente, lo cual es flexible y de alto rendimiento.
Si lo haces de la manera de las reglas de enrutamiento, es lo mismo en términos del resultado. Pero la verificación de salud del upstream necesita ser implementada por ti, agregando mucho trabajo adicional.
También podemos ampliar esta pregunta preguntando cómo deberíamos implementar este escenario para abtest
, que requiere un upstream diferente.
Puedes decidir qué upstream usar en la fase balancer_by_lua
basado en uri
, host
, parameters
, etc. También puedes usar puertas de enlace API para convertir estos juicios en reglas de enrutamiento, decidiendo qué ruta usar en la fase inicial access
, y luego encontrar el upstream especificado a través de la relación de vinculación entre la ruta y el upstream. Este es un enfoque común de las puertas de enlace API, y hablaremos más específicamente sobre esto más adelante en la sección práctica.
Pregunta 5: ¿Es obligatorio el almacenamiento en caché de shared dict
?
Descripción:
En aplicaciones de producción reales, creo que la capa de caché de
shared dict
es imprescindible. Parece que todos solo recuerdan la bondad delru cache
, sin restricciones en el formato de datos, sin necesidad de deserializar, sin necesidad de calcular el espacio de memoria basado en el volumen de k/v, sin contención entre workers, sin bloqueos de lectura/escritura y alto rendimiento.Sin embargo, no ignores que una de sus debilidades más fatales es que el ciclo de vida de
lru cache
sigue alWorker
. Cada vez que NGINX se recarga, esta parte de la caché se perderá por completo, y en este punto, si no hayshared dict
, la fuente de datosL3
colapsará en minutos.Por supuesto, este es el caso de una mayor concurrencia, pero dado que se usa caché, el volumen de negocio ciertamente no es pequeño, lo que significa que el análisis mencionado aún se aplica. ¿Estoy en lo correcto en esta visión?
En algunos casos, es cierto que, como dijiste, el shared dict
no se pierde durante la recarga, por lo que es necesario. Pero hay un caso particular donde solo el lru cache
es aceptable si todos los datos están disponibles activamente desde L3
, la fuente de datos, en la fase init
o init_worker
.
Por ejemplo, si la puerta de enlace API de código abierto APISIX tiene su fuente de datos en etcd
, solo obtiene datos de etcd
. Los almacena en caché en lru cache
durante la fase init_worker
, y las actualizaciones posteriores de la caché se obtienen activamente a través del mecanismo watch
de etcd
. De esta manera, incluso si NGINX se recarga, no habrá estampida de caché.
Por lo tanto, podemos tener preferencias al elegir tecnología, pero no generalizar absolutamente porque no hay una bala de plata que se ajuste a todos los escenarios de almacenamiento en caché. Es una excelente manera de construir una solución mínima viable según las necesidades del escenario real y luego aumentarla gradualmente.