Mantener los servidores receptivos
En busca de mantener un rendimiento óptimo, es esencial asegurarse de que los servidores sigan respondiendo. Para lograrlo, es importante analizar los datos de sincronización y enfocarse en las operaciones con duraciones altas por llamada en lugar de duraciones totales altas.
Procesos de Trabajo + ETS
Uno de los principales desafíos que afectaban la capacidad de respuesta de los servidores eran las operaciones que requerían iterar sobre todos los miembros del gremio. Aunque estas operaciones eran poco frecuentes, su ejecución podía llevar mucho tiempo. Por ejemplo, cuando se enviaba un mensaje a todos los miembros del servidor, era necesario verificar quiénes podían ver ese mensaje, lo cual podía demorar varios segundos. Ante esta situación, era necesario encontrar una solución que permitiera ejecutar esta lógica mientras el gremio realizaba otras tareas.
Una de las herramientas disponibles en Erlang/Elixir para almacenar datos en memoria y permitir que múltiples procesos accedan a ellos de forma segura es ETSE. Esta base de datos en memoria, aunque menos eficiente que acceder a los datos en el montón de procesos, ofrece una velocidad considerable y reduce la latencia de la recolección de basura al disminuir el tamaño del montón de procesos.
Para abordar este problema, se decidió crear una estructura híbrida para mantener la lista de miembros. La lista se almacenaría en ETS, lo que permitiría que otros procesos la lean, pero también se guardaría un conjunto de cambios recientes en el montón de procesos. Dado que la mayoría de los miembros no se actualizan con frecuencia, el conjunto de cambios recientes representa solo una fracción mínima del conjunto general de miembros.
Con la implementación de ETS, se pudieron crear procesos de trabajo que recibían el identificador de la tabla ETS y se encargaban de las operaciones costosas mientras el gremio continuaba con otras tareas. Esto resultó especialmente útil en situaciones como la transferencia de un proceso de gremio de una máquina a otra, donde se podían enviar la mayoría de los miembros mientras el antiguo proceso del gremio seguía funcionando, evitando así largos períodos de inactividad.
Descarga del colector
Otra estrategia implementada para mejorar la capacidad de respuesta y superar los límites de rendimiento fue ampliar Colector, utilizando un proceso de “remitente” separado para la distribución en abanico a los nodos destinatarios en lugar de que el proceso del gremio se encargara de esta tarea. Esta modificación no solo redujo la carga de trabajo del proceso del gremio, sino que también lo protegió de la contrapresión en caso de que alguna de las conexiones de red experimentara una copia de seguridad temporal. Sin embargo, al intentar activar esta función, conocida como descarga múltiple, se descubrió que en realidad provocaba una degradación masiva del rendimiento.
Al analizar más detenidamente el problema, se observó que gran parte del trabajo adicional estaba relacionado con la recolección de basura. Mediante el uso de la función erlang.trace, se pudo obtener información sobre cada vez que el proceso del gremio realizaba una recolección de basura y determinar qué la desencadenaba. Se descubrió que la condición desencadenante para la recolección de basura principal era el montón binario virtual. Esta característica, diseñada para liberar memoria utilizada por cadenas que no están almacenadas dentro del montón del proceso, generaba una gran cantidad de trabajo innecesario en nuestro caso.
Afortunadamente, BEAM permitió ajustar este comportamiento utilizando el indicador de proceso min_bin_vheap_size. Al aumentar su valor a unos pocos megabytes, se eliminó el comportamiento patológico de la recolección de basura y se pudo activar la descarga múltiple, lo que resultó en una mejora significativa en el rendimiento.