Registro distribuido para juegos de ruedas: una experiencia con Hyperledger Fabric

Hola, trabajo en el equipo del proyecto DRD KP (registro de datos distribuidos para monitorear el ciclo de vida de los juegos de ruedas). Aquí quiero compartir la experiencia de nuestro equipo en el desarrollo de una blockchain empresarial para este proyecto ante las limitaciones que impone la tecnología. En su mayor parte, hablaré de Hyperledger Fabric, pero el enfoque descrito aquí se puede extrapolar a cualquier cadena de bloques autorizada. El objetivo final de nuestra investigación es preparar soluciones blockchain empresariales de tal manera que el producto final sea agradable de usar y no demasiado difícil de mantener.

No habrá descubrimientos, soluciones inesperadas y no se cubrirán aquí desarrollos únicos (porque no los tengo). Solo quiero compartir mi humilde experiencia, demostrar que "fue posible" y, tal vez, leer en los comentarios sobre la experiencia de otra persona al tomar buenas y no tan buenas decisiones.

Problema: las cadenas de bloques aún no son escalables

Hoy en día, los esfuerzos de muchos desarrolladores están dirigidos a hacer de blockchain una tecnología realmente conveniente y no una bomba de tiempo en un hermoso envoltorio. Los canales estatales, el resumen optimista, el plasma y la fragmentación pueden convertirse en algo común. Algún día. O tal vez TON vuelva a posponer el lanzamiento durante seis meses y el próximo Plasma Group dejará de existir. Podemos creer en otra hoja de ruta y leer brillantes libros blancos por la noche, pero aquí y ahora necesitamos hacer algo con lo que tenemos. Haz la mierda.

La tarea asignada a nuestro equipo en el proyecto actual es la siguiente en términos generales: hay muchos sujetos, llegando a varios miles, que no quieren construir relaciones de confianza; es necesario construir sobre DLT una solución que funcione en PC comunes sin requisitos especiales de rendimiento y proporcione una experiencia de usuario no peor que cualquier sistema de contabilidad centralizado. La tecnología detrás de la solución debería minimizar la posibilidad de manipulación maliciosa de datos, razón por la cual blockchain está aquí.

Los lemas de los libros blancos y los medios de comunicación nos prometen que el próximo desarrollo permitirá millones de transacciones por segundo. ¿Qué es realmente?

Mainnet Ethereum se está ejecutando actualmente a ~30 tps. Solo por esto, es difícil percibirlo como una cadena de bloques que sea de alguna manera adecuada para las necesidades corporativas. Entre las soluciones autorizadas, se conocen puntos de referencia que muestran 2000 tps (Quórum) o 3000 tps (Tejido Hyperledger, hay un poco menos en la publicación, pero tenga en cuenta que la prueba comparativa se realizó en el antiguo motor de consenso). Era un intento de reelaborar radicalmente Fabric, que no dio los peores resultados, 20000 tps, pero hasta ahora son solo estudios académicos que esperan su implementación estable. Es poco probable que una corporación que pueda permitirse el lujo de mantener un departamento de desarrolladores de blockchain aguante tales indicadores. Pero el problema no está sólo en el rendimiento, también está la latencia.

Estado latente

El retraso desde el momento en que se inicia una transacción hasta su aprobación final por parte del sistema depende no sólo de la velocidad del mensaje que pasa por todas las etapas de validación y pedido, sino también de los parámetros de formación del bloque. Incluso si nuestra cadena de bloques nos permite comprometernos a 1000000 de tps, pero se necesitan 10 minutos para formar un bloque de 488 MB, ¿será más fácil para nosotros?

Echemos un vistazo más de cerca al ciclo de vida de una transacción en Hyperledger Fabric para comprender qué lleva tiempo y cómo se relaciona con los parámetros de formación de bloques.

Registro distribuido para juegos de ruedas: una experiencia con Hyperledger Fabric
tomado de aquí: hyperledger-fabric.readthedocs.io/en/release-1.4/arch-deep-dive.html#swimlane

(1) El cliente forma una transacción, la envía a los pares que la respaldan, estos últimos simulan la transacción (aplican los cambios realizados por el código de cadena al estado actual, pero no se comprometen con el libro mayor) y reciben RWSet: nombres de claves, versiones y valores tomados de la colección en CouchDB, (2) los endosantes envían el RWSet firmado de regreso al cliente, (3) el cliente verifica las firmas de todos los pares requeridos (endosantes) y luego envía la transacción al servicio de pedidos , o lo envía sin verificación (la verificación aún se llevará a cabo más tarde), el servicio de pedidos forma un bloque y (4) lo envía de vuelta a todos los pares, no solo a los endosantes; los pares verifican que las versiones de las claves en el conjunto de lectura coincidan con las versiones en la base de datos, las firmas de todos los patrocinadores y finalmente confirman el bloque.

Pero eso no es todo. Detrás de las palabras "el ordenante forma un bloque" se esconde no solo el orden de las transacciones, sino también 3 solicitudes de red consecutivas del líder a los seguidores y viceversa: el líder agrega un mensaje al registro, lo envía a los seguidores, estos últimos lo agregan a su registro, envía confirmación de replicación exitosa al líder, el líder confirma el mensaje, envía confirmación de confirmación a los seguidores, los seguidores confirman. Cuanto menor sea el tamaño y el tiempo del bloque, más a menudo el servicio de pedidos tendrá que establecer un consenso. Hyperledger Fabric tiene dos parámetros de formación de bloques: BatchTimeout: tiempo de formación del bloque y BatchSize: tamaño del bloque (el número de transacciones y el tamaño del bloque en bytes). Tan pronto como uno de los parámetros alcanza el límite, se genera un nuevo bloque. Cuantos más nodos ordenados, más tiempo llevará. Por lo tanto, es necesario aumentar BatchTimeout y BatchSize. Pero dado que los RWSets tienen versiones, cuanto más grande hagamos el bloque, mayor será la probabilidad de conflictos MVCC. Además, con un aumento en BatchTimeout, la UX se degrada catastróficamente. Me parece razonable y obvio el siguiente esquema para resolver estos problemas.

Cómo evitar esperar a que finalice el bloque y no perder de vista el estado de la transacción

Cuanto mayor sea el tiempo de formación y el tamaño del bloque, mayor será el rendimiento de la cadena de bloques. Uno no se sigue directamente del otro, pero debe recordarse que establecer consenso en RAFT requiere tres solicitudes de red del líder a los seguidores y viceversa. Cuantos más nodos de orden, más tiempo llevará. Cuanto menor sea el tamaño y el tiempo de formación del bloque, mayores serán estas interacciones. ¿Cómo aumentar el tiempo de formación y el tamaño del bloque sin aumentar el tiempo de respuesta del sistema para el usuario final?

Primero, necesita resolver de alguna manera los conflictos MVCC causados ​​por un tamaño de bloque grande, que puede incluir diferentes RWSets con la misma versión. Obviamente, en el lado del cliente (en relación con la red blockchain, esto bien puede ser un backend, y lo digo en serio) Manejador de conflictos MVCC, que puede ser un servicio independiente o un decorador normal sobre una llamada de inicio de transacción con lógica de reintento.

El reintento se puede implementar con una estrategia exponencial, pero luego la latencia también se degradará exponencialmente. Por lo tanto, debería utilizar un reintento aleatorio dentro de ciertos límites pequeños o uno constante. Con la vista puesta en posibles colisiones en la primera variante.

El siguiente paso es hacer que la interacción del cliente con el sistema sea asíncrona para que no espere 15, 30 o 10000000 de segundos, lo que configuraremos como BatchTimeout. Pero al mismo tiempo, es necesario mantener la capacidad de asegurarse de que los cambios iniciados por la transacción se registren o no en la cadena de bloques.
Se puede utilizar una base de datos para almacenar el estado de las transacciones. La opción más sencilla es CouchDB debido a su facilidad de uso: la base de datos tiene una interfaz de usuario lista para usar, una API REST y puede configurar fácilmente la replicación y la fragmentación. Puedes simplemente crear una colección separada en la misma instancia de CouchDB que utiliza Fabric para almacenar su estado mundial. Necesitamos almacenar documentos de este tipo.

{
 Status string // Статус транзакции: "pending", "done", "failed"
 TxID: string // ID транзакции
 Error: string // optional, сообщение об ошибке
}

Este documento se escribe en la base de datos antes de que la transacción se envíe a los pares, el ID de la entidad se devuelve al usuario (el mismo ID se usa como clave) si se trata de una operación de creación, y luego se muestran los campos Estado, TxID y Error. Se actualiza a medida que se recibe información relevante de sus pares.

Registro distribuido para juegos de ruedas: una experiencia con Hyperledger Fabric

En este esquema, el usuario no espera a que finalmente se forme el bloque, observa la rueca en la pantalla durante 10 segundos, recibe una respuesta instantánea del sistema y continúa trabajando.

Elegimos BoltDB para almacenar los estados de las transacciones porque necesitamos ahorrar memoria y no queremos perder tiempo en la interacción de la red con un servidor de base de datos independiente, especialmente cuando esta interacción se realiza mediante el protocolo de texto sin formato. Por cierto, ya sea que use CouchDB para implementar el esquema descrito anteriormente o simplemente para almacenar el estado mundial, en cualquier caso, tiene sentido optimizar la forma en que se almacenan los datos en CouchDB. De forma predeterminada, en CouchDB, el tamaño de los nodos del árbol b es 1279 bytes, que es mucho menor que el tamaño del sector en el disco, lo que significa que tanto la lectura como el reequilibrio del árbol requerirán más accesos físicos al disco. El tamaño óptimo cumple con el estándar. Formato avanzado y es de 4 kilobytes. Para la optimización, necesitamos establecer el parámetro. btree_chunk_size igual a 4096 en el archivo de configuración de CouchDB. Para BoltDB tal intervención manual No requiere.

Contrapresión: estrategia de amortiguación

Pero puede haber muchos mensajes. Más de lo que el sistema puede manejar, compartiendo recursos con una docena de otros servicios además de los que se muestran en el diagrama, y ​​todo esto debería funcionar perfectamente incluso en máquinas en las que ejecutar Intellij Idea sería extremadamente tedioso.

El problema del diferente rendimiento de los sistemas de comunicación, productor y consumidor, se resuelve de diferentes maneras. Veamos qué podríamos hacer.

Dejar caer: podemos afirmar que podemos procesar como máximo X transacciones en T segundos. Todas las solicitudes que superen este límite se descartan. Es bastante simple, pero luego puedes olvidarte de UX.

Controlador: el consumidor debe tener alguna interfaz a través de la cual, dependiendo de la carga, pueda controlar los tps del productor. No está mal, pero impone a los desarrolladores del cliente de carga la obligación de implementar esta interfaz. Para nosotros esto es inaceptable, ya que en el futuro la cadena de bloques se integrará en una gran cantidad de sistemas existentes desde hace mucho tiempo.

Buffering: en lugar de intentar resistir el flujo de datos de entrada, podemos almacenar este flujo en un buffer y procesarlo a la velocidad requerida. Evidentemente, esta es la mejor solución si queremos ofrecer una buena experiencia de usuario. Implementamos el búfer usando una cola en RabbitMQ.

Registro distribuido para juegos de ruedas: una experiencia con Hyperledger Fabric

Se han agregado dos nuevas acciones al esquema: (1) después de recibir una solicitud de API, se pone en cola un mensaje con los parámetros necesarios para llamar a la transacción y el cliente recibe un mensaje de que el sistema ha aceptado la transacción ( 2) el backend lee datos de la cola a una velocidad especificada en la configuración; inicia una transacción y actualiza los datos en el almacén de estado.
Ahora puede aumentar el tiempo de construcción y bloquear la capacidad tanto como desee, ocultando los retrasos al usuario.

Otras herramientas

Aquí no se dijo nada sobre el código de cadena, porque generalmente no hay nada que optimizar en él. El código de cadena debe ser lo más simple y seguro posible; eso es todo lo que se requiere. El marco nos ayuda mucho a escribir código de cadena de forma sencilla y segura. CSKit de S7 Techlab y analizador estático revivir^CC.

Además, nuestro equipo está desarrollando un conjunto de utilidades para que trabajar con Fabric sea sencillo y agradable: explorador de cadena de bloques, utilidad para reconfiguración automática de la red (agregar/eliminar organizaciones, nodos RAFT), utilidad para revocación de certificado y eliminación de identidad. Si quieres contribuir, bienvenido.

Conclusión

Este enfoque facilita la sustitución de Hyperledger Fabric por Quorum, otras redes privadas de Ethereum (PoA o incluso PoW), reduce significativamente el rendimiento real, pero al mismo tiempo mantiene la UX normal (tanto para los usuarios en el navegador como desde el lado de los sistemas integrados). ). Al reemplazar Fabric con Ethereum en el esquema, solo será necesario cambiar la lógica del servicio/decorador de reintento de manejar conflictos MVCC a un incremento atómico nonce y reenvío. El almacenamiento en búfer y el estado permitieron desacoplar el tiempo de respuesta del tiempo de formación del bloque. Ahora puede agregar miles de nodos de pedidos y no tener miedo de que los bloques se formen con demasiada frecuencia y carguen el servicio de pedidos.

En general, esto es todo lo que quería compartir. Me alegraré si ayuda a alguien en su trabajo.

Fuente: habr.com

Añadir un comentario