
Cuando implementé la interfaz de usuario del
equilibrador de carga para una
nube privada virtual, tuve que enfrentar dificultades significativas. Esto me llevó a reflexionar sobre el papel de la interfaz, que quiero compartir en primer lugar. Y luego justifique sus pensamientos, usando el ejemplo de una tarea específica.
La solución al problema resultó, en mi opinión, bastante creativa, y tuve que buscarla en un marco muy limitado, así que creo que puede ser interesante.
Rol frontend
Debo decir de inmediato que no finjo la verdad y planteo un tema controvertido. Estoy algo deprimido por la ironía del front-end y de la web en particular, como algo insignificante. Y es aún más deprimente que a veces esto ocurra razonablemente. Ahora la moda ya estaba dormida, pero hubo un momento en que todos corrían con marcos, paradigmas y otras entidades, diciendo en voz alta que todo esto es súper importante y súper necesario, y a cambio recibieron una ironía de que el front-end se ocupa de la salida de formularios y procesando clics en botones, que se pueden hacer "en la rodilla".
Ahora, al parecer, todo ha vuelto más o menos a la normalidad. Nadie realmente quiere hablar sobre cada versión menor del próximo marco. Pocas personas buscan la herramienta o el enfoque perfecto, debido a la creciente conciencia de su utilidad. Pero incluso esto, por ejemplo, no interfiere con el regaño casi irrazonable de Electron y sus aplicaciones. Creo que esto se debe a la falta de comprensión de la tarea que está resolviendo el front-end.
El frontend no es solo un medio para mostrar la información proporcionada por el backend, y no solo un medio para procesar las acciones del usuario. La interfaz es algo más, algo abstracto, y si le das una definición simple y clara, inevitablemente se perderá el significado.
La interfaz está en algún "marco". Por ejemplo, en términos técnicos, se encuentra entre la API proporcionada por el backend y la API proporcionada por las instalaciones de E / S. En términos de tareas, UX resuelve entre las tareas de la interfaz de usuario y las tareas que resuelve el backend. Por lo tanto, se obtiene una especialización frontend bastante estrecha, una especialización de la capa. Esto no significa que los proveedores front-end no puedan ejercer influencia en áreas fuera de su especialización, pero en el momento en que esta influencia es imposible, surge la verdadera tarea front-end.
Este problema puede expresarse a través de una contradicción. No se requiere la interfaz de usuario para cumplir con los modelos de datos y el comportamiento del backend. El comportamiento y los modelos de datos del backend no son necesarios para adaptarse a las tareas de la interfaz de usuario. Y luego la tarea del front-end es eliminar esta contradicción. Cuanto mayor sea la discrepancia entre las tareas del backend y la interfaz de usuario, más importante será el papel de la interfaz. Y para dejar en claro de lo que estoy hablando, daré un ejemplo donde esta discrepancia, por alguna razón, resultó ser significativa.
Declaración del problema.
OpenStack LBaaS, en mi opinión, es un complejo de herramientas de hardware y software necesarias para equilibrar la carga entre servidores. Para mí es importante que su implementación dependa de factores objetivos, de la visualización física. Debido a esto, hay algunas peculiaridades en la API y en las formas de interactuar con esta API.
Al desarrollar una interfaz de usuario, el interés principal no son las características técnicas del backend, sino sus capacidades fundamentales. La interfaz se crea para el usuario, y el usuario necesita una interfaz para administrar los parámetros de equilibrio, y el usuario no necesita sumergirse en las características internas de la implementación del backend.
El backend está desarrollado en su mayor parte por la comunidad, y es posible influir en su desarrollo en cantidades muy limitadas. Una de las características clave para mí es que los desarrolladores de back-end están listos para sacrificar la conveniencia y la simplicidad de los controles en aras del rendimiento, y esto está absolutamente justificado, ya que se trata de equilibrar la carga.
Hay un punto más sutil, y quiero resumirlo de inmediato, advirtiendo algunas preguntas. Está claro que en OpenStack y su API la luz no convergió. Siempre puede desarrollar su propio conjunto de herramientas o una "capa" que funcionará con la API de OpenStack, produciendo su propia API, conveniente para las tareas del usuario. La única pregunta es la conveniencia. Si las herramientas inicialmente disponibles le permiten implementar la interfaz de usuario como estaba previsto, ¿tiene sentido producir entidades?
La respuesta a esta pregunta es multifacética y para las empresas dependerá de los desarrolladores, su empleo, su competencia, cuestiones de responsabilidad, soporte, etc. En nuestro caso, fue más conveniente resolver algunas de las tareas en el front-end.
Características de OpenStack LBaaS
Quiero identificar solo aquellas características que han tenido una fuerte influencia en la interfaz. Las preguntas sobre por qué surgieron estas características o en qué se basan ya están fuera del alcance de este artículo.
Trabajo con documentación preparada y tengo que aceptar sus características. Aquellos que estén interesados en lo que OpenStack Octavia es desde el interior pueden familiarizarse con la
documentación oficial . Octavia es el nombre de un conjunto de herramientas diseñadas para equilibrar la carga en el ecosistema OpenStack.
La primera característica que encontré durante el desarrollo es la gran cantidad de modelos y relaciones necesarias para mostrar el estado del equilibrador. La
API de Octavia describe 12 modelos, pero solo se necesitan 7 para el lado del cliente. Estos modelos tienen conexiones, a menudo desnormalizadas, la imagen a continuación muestra un diagrama aproximado:
"Siete" no suena muy impresionante, pero en realidad, para garantizar el funcionamiento completo de la interfaz, al momento de escribir este texto, tuve que usar 16 modelos de datos y unas 30 relaciones entre ellos. Como Octavia es solo un equilibrador, requiere otros módulos OpenStack para funcionar. Y todo esto es necesario para solo dos páginas en la interfaz de usuario.
Las características segunda y tercera son Octavia
asincrónicas y transaccionales. Los modelos de datos tienen un campo de
estado que refleja el estado de las operaciones realizadas en un objeto.
La operación de lectura de un objeto se produce sincrónicamente y no tiene restricciones. Pero las operaciones de creación, actualización y eliminación pueden llevar una cantidad de tiempo indefinida. Esto se debe precisamente al hecho de que los modelos de datos tienen, en términos generales, un significado físico.
Después de enviar una solicitud de creación, podemos saber que el registro ha aparecido, podemos leerlo, pero hasta que se complete la operación de creación, no podemos realizar ninguna otra operación en este registro. Cualquier intento de este tipo dará como resultado un error. La operación de cambiar un objeto solo se puede iniciar cuando el objeto está en el estado
ACTIVO ; puede enviar un objeto para su eliminación en los estados
ACTIVO y
ERROR .
Estos estados pueden venir a través de WebSockets, lo que facilita enormemente su procesamiento, pero las transacciones son un problema mucho mayor. Al realizar cambios en cualquier objeto, todos los modelos relacionados también participarán en la transacción. Por ejemplo, al realizar cambios en
Member , se bloqueará el
Pool ,
Listener y
Loadbalancer asociados . Esto es lo que parece en términos de eventos recibidos en sockets web:
- los primeros cuatro eventos son la transferencia de objetos al estado PENDING_UPDATE : el campo de destino contiene el nombre del modelo del objeto que participa en la transacción;
- el quinto evento es solo un duplicado (no sé con qué está conectado);
- los últimos cuatro es un retorno al estado ACTIVO . En este caso, esta es una operación de cambio de peso, y lleva menos de un segundo, pero a veces lleva mucho más tiempo.
También puede ver en la captura de pantalla que el orden de los eventos no tiene que ser estricto. Por lo tanto, resulta que para iniciar cualquier operación, es necesario conocer no solo el estado del objeto en sí, sino también los estados de todas las dependencias que también participarán en la transacción.
Funciones de interfaz de usuario
Ahora imagínese en el lugar de un usuario que necesita saber en algún lugar para equilibrar entre dos servidores:
- Es necesario crear un oyente en el que se definirá el algoritmo de equilibrio.
- Crea una piscina.
- Asigne un grupo al oyente.
- Agregue enlaces a puertos balanceados al grupo.
Cada vez es necesario esperar la finalización de la operación, que depende de todos los objetos creados previamente.
Como lo demostró un estudio interno, en opinión del usuario común, solo hay una comprensión aproximada de que el equilibrador debe tener un punto de entrada, debe haber puntos de salida y los parámetros del equilibrio a realizar: algoritmo, peso y otros. El usuario no tiene que saber qué es OpenStack.
No sé cuán complicada debería ser la interfaz para la percepción, donde el usuario mismo debe seguir todas las características técnicas del backend descrito anteriormente. Para la consola, esto puede ser permisible, ya que su uso implica un alto nivel de inmersión en la tecnología, pero para la web dicha interfaz es horrible.
En la web, el usuario espera completar un formulario claro y lógico, presionar un botón, esperar y todo funcionará. Quizás esto se pueda discutir, pero propongo concentrarme en las características que afectan la implementación de la interfaz.
La interfaz fue diseñada de tal manera que involucra el uso en cascada de las operaciones: una acción en la interfaz puede involucrar varias operaciones. La interfaz no implica que el usuario pueda realizar acciones que actualmente no son posibles, pero supone que el usuario debe comprender por qué es así. La interfaz es un todo único y, por lo tanto, sus elementos individuales pueden usar información de varias entidades dependientes, incluida la metainformación.

Si tenemos en cuenta que hay algunas características de la interfaz que no son exclusivas del equilibrador, como interruptores, acordeones, pestañas, un menú contextual y asumimos que sus principios operativos son claros inicialmente, entonces creo que para un usuario que sabe qué es el equilibrio de carga, no Será muy difícil leer la mayor parte de la interfaz anterior y hacer una suposición sobre cómo administrarla. Pero resaltar qué partes de la interfaz están ocultas detrás de los modelos del equilibrador, el oyente, el grupo, los miembros y otras entidades ya no es la tarea más obvia.
Resolviendo contradicciones
Espero haber podido demostrar que las características del backend no se ajustan bien a la interfaz y que el backend no siempre puede eliminar estas características. Junto con esto, las características de la interfaz no encajan bien en el backend, y tampoco siempre se pueden eliminar sin complicar la interfaz. Cada una de estas áreas resuelve sus propios problemas. La responsabilidad del front-end es resolver problemas para garantizar el nivel necesario de interacción entre la interfaz y el back-end.
En mi práctica, inmediatamente corrí a la piscina con la cabeza, sin prestar atención, o mejor dicho, ni siquiera tratando de descubrir esas características que son más altas, pero tuve suerte o la experiencia ayudó (y se eligió el vector correcto). Me he dado cuenta repetidamente de mí mismo que cuando utilizo una API o biblioteca de terceros, es muy útil familiarizarse con la documentación de antemano: cuantos más detalles, mejor. La documentación a menudo es similar entre sí, las personas aún confían en la experiencia de otras personas, pero hay una descripción de las características de cada sistema individual y está contenida en los detalles.
Si inicialmente pasé un par de horas adicionales estudiando la documentación, en lugar de extraer la información necesaria por palabras clave, habría pensado en los problemas que tendrían que enfrentar, y este conocimiento podría tener un impacto en la arquitectura del proyecto desde las primeras etapas. Volver a eliminar los errores cometidos al principio es muy desmoralizador. Y sin un contexto completo, a veces tienes que volver varias veces.
Como opción, puede doblar su línea, generando gradualmente más y más código "con un mordisco", pero cuanto más este montón de código sea, más será rastrillado al final. Al diseñar la arquitectura, por supuesto, uno no debe sumergirse demasiado profundamente, tener en cuenta todas las opciones posibles e imposibles, pasar una gran cantidad de tiempo en ella, es importante encontrar un equilibrio. Pero el conocimiento más o menos detallado de la documentación a menudo demuestra ser una inversión muy útil de no mucho tiempo.
Sin embargo, desde el principio, habiendo visto una gran cantidad de modelos involucrados, me di cuenta de que sería necesario construir un mapeo del estado del backend al cliente con todas las conexiones preservadas. Después de que logré mostrar toda la información necesaria en el cliente, con todas las conexiones, etc., fue necesario organizar una cola de tareas.
Los datos se actualizan de forma asíncrona, la disponibilidad de las operaciones está determinada por una variedad de condiciones, y cuando se requieren operaciones en cascada, no se puede prescindir de la cola en tales condiciones. Quizás, en pocas palabras, esta es toda la arquitectura de mi solución: almacenamiento con un reflejo del estado del backend y la cola de tareas.
Arquitectura de soluciones
Debido a la cantidad indefinida de modelos y relaciones, pongo escalabilidad en la estructura del repositorio al hacer esto usando una fábrica que devuelve una descripción declarativa de las colecciones del repositorio. La colección tiene un servicio, una clase de modelo simple con CRUD. Sería posible hacer una descripción de los enlaces en el modelo, como se hace, por ejemplo, en RoR o en el antiguo Backbone, pero esto requeriría una gran cantidad de código para cambiar. Por lo tanto, la descripción de las relaciones se encuentra al lado de la clase de modelo:

En total, obtuve 2 tipos de conexiones: una a una, una a muchas. La retroalimentación también se puede describir. Además del tipo, se indica la colección de dependencias, el campo al que se adjunta la dependencia encontrada y el campo desde el que se lee la ID del objeto dependiente (en caso de comunicación uno a muchos, se lee la lista de ID). Si la condición de comunicación de un objeto es más complicada que los simples enlaces a objetos, entonces en la fábrica se puede describir la función de probar dos objetos, cuyos resultados determinarán la presencia de una conexión. Todo parece un poco "bicicleta", pero funciona sin dependencias innecesarias y exactamente como debería.
El repositorio tiene un módulo para esperar para agregar y eliminar un recurso, en esencia está procesando eventos únicos con verificación condicional y con una interfaz promis. Al suscribirse, se pasa el tipo de evento (agregar, eliminar), la función de prueba y el controlador. Cuando se produce un determinado evento y con un resultado de prueba positivo, se ejecuta el controlador, después de lo cual se detiene el seguimiento. Un evento puede ocurrir al suscribirse sincrónicamente.
El uso de dicho patrón permitió fijar automáticamente relaciones complejas arbitrariamente entre modelos y hacerlo en un solo lugar. Este lugar lo llamé rastreador. Al agregar un objeto al repositorio, comienza a rastrear sus relaciones. El módulo de espera le permite responder a eventos y verificar una conexión entre el objeto monitoreado y el objeto en el almacenamiento. Si el objeto ya estaba en el repositorio, el módulo de espera llama al manejador inmediatamente.
Tal dispositivo de almacenamiento le permite describir cualquier cantidad de colecciones y las relaciones entre ellas. Al agregar y eliminar objetos, la tienda coloca o restablece automáticamente las propiedades con el contenido de los objetos dependientes. Las ventajas de este enfoque son que todas las relaciones se describen explícitamente, y son monitoreadas y actualizadas por un sistema; contras - en la complejidad de la implementación y depuración.
En general, dicho repositorio es bastante trivial y lo hice yo mismo, porque sería mucho más difícil integrar una solución preparada en una base de código existente, pero sería aún más difícil adjuntar una cola de tareas a una solución preparada.
Todas las tareas, como las colecciones, tienen una descripción declarativa y son creadas por la fábrica. Las tareas pueden tener en la descripción las condiciones para comenzar y una lista de tareas que deberán agregarse a la cola una vez completada la actual.
El ejemplo anterior describe la tarea de crear un grupo. En las dependencias, el equilibrador y el oyente están indicados, de forma predeterminada, se realiza una verificación del estado
ACTIVO . El objeto del equilibrador está bloqueado, ya que las tareas de procesamiento en la cola pueden ocurrir sincrónicamente, el bloqueo le permite evitar conflictos en el momento en que se envió la solicitud de ejecución, pero el estado no ha cambiado, pero se supone que cambiará. En lugar de
PADRE , si el grupo se crea como resultado de la cascada de tareas, la
ID se sustituirá automáticamente.
Después de crear un grupo, se agregarán tareas a la cola para crear un monitor de disponibilidad y crear todos los miembros de este grupo. El resultado es una estructura que se puede convertir completamente a JSON. Esto se hace para poder restaurar la cola en caso de falla.
La cola, basada en la descripción de la tarea, monitorea independientemente todos los cambios en el repositorio y verifica las condiciones que deben cumplirse para ejecutar la tarea. Como ya dije, los estados provienen de los sockets web, y es muy simple generar los eventos necesarios para la cola, pero si es necesario, no será un problema adjuntar un mecanismo de actualización de datos del temporizador (esto se estableció originalmente en la arquitectura, ya que los sockets web eran por varias razones puede no funcionar muy estable). Una vez completada la tarea, la cola informa automáticamente al repositorio sobre la necesidad de actualizar los enlaces en los objetos especificados.
Conclusión
La necesidad de escalabilidad ha llevado a un enfoque declarativo. La necesidad de mostrar modelos y las relaciones entre ellos ha llevado a un único repositorio. La necesidad de procesar objetos dependientes ha llevado a la cola.
La combinación de estas necesidades puede no ser la tarea más fácil en términos de implementación (pero este es un tema aparte). Pero en términos de arquitectura, la solución es muy simple y le permite eliminar todas las contradicciones entre las tareas del backend y la interfaz de usuario, establecer su interacción y sentar las bases para otras características posibles de cualquiera de las partes.
Desde el lado del
panel de control de Selectel
, el proceso de equilibrio es simple y directo, lo que permite a
los clientes del
servicio no gastar recursos en la implementación independiente del equilibrador, mientras se mantiene la capacidad de administrar el tráfico de manera flexible.
Pruebe nuestro equilibrador en acción ahora y escriba su opinión en los comentarios.