La Red de Conocimientos Pedagógicos - Currículum vitae - ¿Puede Rust desarrollar sistemas distribuidos?

¿Puede Rust desarrollar sistemas distribuidos?

Rust puede desarrollar sistemas distribuidos.

Introducción

Construir un sistema distribuido no es una tarea fácil. Hay muchas cuestiones a considerar. La primera es qué tipo de funciones debe proporcionar nuestro sistema, como:

Consistencia: ¿necesitamos garantizar la coherencia lineal de todo el sistema, o podemos tolerar la inconsistencia de los datos a corto plazo y solo admitir? ¿consistencia final?

Estabilidad: ¿Se puede garantizar que el sistema funcione de manera estable las 24 horas del día, los 7 días de la semana? ¿La disponibilidad del sistema es de cuatro nueves y cinco nueves? Si ocurre un desastre como un daño a la máquina, ¿puede el sistema recuperarse automáticamente?

Escalabilidad: a medida que los datos continúan creciendo, ¿se pueden reequilibrar automáticamente agregando máquinas sin afectar los servicios externos?

Transacciones distribuidas: ¿es necesario proporcionar soporte para transacciones distribuidas y en qué medida se debe admitir el nivel de aislamiento de transacciones?

Al comienzo del diseño del sistema, las cuestiones anteriores deben considerarse como los objetivos de diseño de todo el sistema. Para lograr estas características, es necesario considerar qué esquema de implementación utilizar, así como las ventajas y desventajas de cada aspecto.

Más adelante, utilizaré el TiKV de valor clave distribuido que desarrollamos como ejemplo práctico para ilustrar cómo lo elegimos e implementamos.

TiKV

TiKV es un almacén distribuido de valores clave desarrollado en Rust, que utiliza el protocolo de coherencia Raft para garantizar una sólida coherencia y estabilidad de los datos. Al mismo tiempo, la escalabilidad del sistema se logra mediante el mecanismo de cambio de configuración de Raft.

TiKV proporciona soporte básico de API de KV, a saber, Obtener, Configurar, Eliminar, Escanear y otras API comunes. TiKV también proporciona una API de transacciones que admite transacciones ACID. Podemos usar Begin para abrir una transacción, operar las claves en la transacción y finalmente usar Commit para enviar una transacción. TiKV admite niveles de aislamiento de transacciones SI y s SI para satisfacer los diferentes escenarios comerciales de los usuarios.

Rust

Después de planificar las características de TiKV, iniciamos el desarrollo de TiKV. La primera pregunta que enfrentamos en este momento es qué lenguaje utilizar para el desarrollo. En ese momento, teníamos varias opciones frente a nosotros: Go. Go es el lenguaje en el que nuestro equipo es mejor. Las rutinas, canales y otros mecanismos de Go proporcionados por Go son naturalmente adecuados para el desarrollo de sistemas distribuidos a gran escala. son flexibles y convenientes. También hay un equipaje atractivo. El primero es GC. Aunque el GC de Go se está volviendo cada vez más perfecto, siempre habrá retrasos temporales y la programación de rutinas también tendrá una sobrecarga de conmutación, lo que puede provocar mayores retrasos en las solicitudes.

Java, hay demasiados sistemas distribuidos basados ​​en Java en el mundo ahora, pero Java tiene el mismo problema general que GC. Nuestro equipo no tiene experiencia en desarrollo en Java, por lo que no lo hemos adoptado.

C++ y C++ pueden considerarse sinónimos de desarrollo de sistemas de alto rendimiento, pero no hay muchos estudiantes en nuestro equipo que dominen C++, por lo que desarrollar proyectos de C++ a gran escala no es una tarea fácil. Aunque la programación con C++ moderno reduce en gran medida riesgos como la carrera de datos y los punteros colgantes, aún podemos cometer errores.

Cuando excluimos los lenguajes principales mencionados anteriormente, descubrimos que para desarrollar TiKV, necesitamos que este lenguaje tenga las siguientes características:

Lenguaje estático, para garantizar máximo rendimiento.

Sin GC, control de memoria completamente manual.

Seguridad de la memoria, intente evitar problemas como punteros colgantes y pérdidas de memoria.

Es seguro para subprocesos y no encontrará problemas como la competencia de datos.

Con la gestión de paquetes, podemos utilizar bibliotecas de terceros de forma muy cómoda.

Enlace C eficiente, porque también podemos usar algunas bibliotecas C, por lo que no hay gastos generales al interactuar con C.

En resumen, decidimos utilizar el lenguaje de programación del sistema Rust, que proporciona las características del lenguaje que queremos anteriormente. Sin embargo, elegir Rust también tiene grandes riesgos para nosotros, principalmente en los siguientes dos aspectos:

Nuestro equipo no tiene experiencia en desarrollo de Rust. Todos necesitan dedicar tiempo a aprender Rust, pero la curva de aprendizaje de Rust es muy pronunciada.

La falta de bibliotecas de red básicas. Aunque Rust 1.0 se lanzó en ese momento, descubrimos que muchas bibliotecas básicas no existían. Por ejemplo, solo había mio en la biblioteca de red, y no había nada bueno. El marco RPC y HTTP no estaban maduros.

Sin embargo, decidimos utilizar Rust. El primer punto es que nuestro equipo pasó casi un mes aprendiendo Rust y compitiendo con el compilador de Rust. Para el segundo punto, lo escribimos enteramente nosotros mismos.

Afortunadamente, cuando superamos el doloroso período de Rust, descubrimos que desarrollar TiKV en Rust es extremadamente eficiente. Es por eso que podemos desarrollar TiKV en poco tiempo y lanzarlo en el entorno de producción.

Protocolo de coherencia

Para sistemas distribuidos, CAP es un tema que debe considerarse, porque P, que es la tolerancia de partición, debe existir, por lo que debe considerarse como una opción C. -Consistencia o A-Disponibilidad.

Cuando diseñamos TiKV, decidimos garantizar completamente la seguridad de los datos, por lo que naturalmente elegimos C, pero de hecho no abandonamos por completo A, porque después de todo, la mayor parte del tiempo la red está desconectada y el La máquina se apaga con poca frecuencia. Solo necesitamos garantizar HA-Alta disponibilidad, que es la disponibilidad de cuatro 9 o cinco 9.

Ahora que hemos elegido C, lo siguiente que debemos considerar es qué algoritmo de consenso distribuido elegir. Paxos o Raft son los algoritmos más populares y, naturalmente, Raft es nuestra primera opción porque es simple y fácil de entender, y hay muchas bibliotecas de código abierto listas para usar como referencia.

Para la implementación de Raft, nos referimos directamente a Raft de etcd. Etcd ha sido utilizado por una gran cantidad de empresas en entornos de producción, por lo que la calidad de su biblioteca Raft está muy garantizada. Aunque etcd está implementado en Go, su biblioteca Raft es similar a C, por lo que es muy conveniente para nosotros usar Rust directamente para traducirlo. Durante el proceso de traducción, también solucionamos algunos errores y agregamos algunas funciones a Raft de etcd para hacerlo más robusto y fácil de usar.

Actualmente el código Raft todavía está en el proyecto TiKV, pero pronto lo separaremos y nos convertiremos en una biblioteca independiente para que todos puedan usar Raft en sus propios proyectos Rust.

El uso de Raft no solo puede garantizar la coherencia de los datos, sino también lograr la expansión horizontal del sistema con la ayuda del mecanismo de cambio de configuración de Raft, que explicaremos en detalle en un artículo posterior.

Motor de almacenamiento

Después de elegir el protocolo de coherencia distribuida, el siguiente paso es considerar la cuestión del almacenamiento de datos. En TiKV, almacenaremos los registros de Raft y luego también aplicaremos las solicitudes reales del cliente de los registros de Raft a la máquina de estado.

Mire primero la máquina de estado, porque almacenará los datos reales del usuario y estos datos pueden ser valores-clave aleatorios. Para manejar eficazmente la inserción de datos aleatorios, naturalmente consideramos utilizar el modelo de árbol LSM actualmente común. En este modo, RocksDB puede considerarse la mejor opción en esta etapa.

RocksDB es una tienda de valores clave de alto rendimiento desarrollada por el equipo de Facebook basada en LevelDB. Proporciona muchas opciones de configuración que se pueden ajustar según diferentes entornos de hardware. Hay una broma aquí, es que RocksDB tiene demasiadas configuraciones, e incluso los estudiantes del equipo de RocksDB no conocen el significado de todas las configuraciones.

Explicaremos en detalle cómo usarlo en TiKV, cómo optimizar RocksDB y cómo agregar funciones y corregir errores a RocksDB en artículos posteriores.

Para los registros de Raft, dado que el índice de cualquier registro aumenta de manera completamente monótona, como el Registro 1, el siguiente registro debe ser el Registro 2, por lo que la inserción de registros se puede considerar como una inserción secuencial. El método más común es escribir un archivo de segmento, pero ahora todavía usamos RocksDB, porque RocksDB también tiene un rendimiento muy alto para la escritura secuencial, que puede satisfacer nuestras necesidades. Pero no descarta utilizar un motor propio más adelante.

Debido a que RocksDB proporciona una API C, se puede usar directamente en Rust, o puede usar RocksDB en su propio proyecto Rust a través de Rust-rocksdb.

Transacciones distribuidas

Para soportar transacciones distribuidas, lo primero que hay que resolver es el problema del tiempo del sistema distribuido, es decir, qué utilizamos para identificar el orden de las diferentes transacciones. Generalmente existen varios métodos:

TrueTime es el método utilizado por Google Spanner, pero requiere el soporte de hardware GPS + reloj atómico. Spanner no explicó en detalle cómo está configurado el entorno de hardware. el papel, por lo que es difícil de implementar externamente por sí solo.

HLC HLC es un reloj lógico híbrido que utiliza tiempo físico y relojes lógicos para determinar la secuencia de eventos. HLC ya se utiliza en algunas aplicaciones, pero depende de NTP. Si el error de precisión de NTP es grande, es probable que afecte el tiempo de espera del envío.

TSO, TSO es un temporizador global que utiliza directamente un servicio de punto único para asignar el tiempo. El método TSO es simple, pero habrá un único punto de falla y también pueden ocurrir problemas de rendimiento en un solo punto.

TiKV utiliza TSO para la sincronización global, principalmente por simplicidad. Para puntos únicos de falla, implementamos el procesamiento automático de conmutación por error a través de Raft. En cuanto a los problemas de rendimiento de un solo punto, TiKV está dirigido principalmente a grupos pequeños y medianos de PB e inferiores, por lo que en términos de rendimiento, solo necesita garantizar una asignación de tiempo de millones por segundo. En términos de latencia de red, TiKV tiene. no hay requisitos globales para cross-IDC. En el caso de un solo IDC o de un IDC en la misma ciudad, la velocidad de la red es muy rápida. Incluso si se trata de un IDC ubicado en otra ubicación, no habrá mucho retraso debido a la línea dedicada.

Después de resolver el problema del tiempo, la siguiente pregunta es qué tipo de algoritmo de transacción distribuida usamos. El más común es usar 2 PC, pero el algoritmo habitual de 2 PC tendrá problemas en algunos casos extremos. Por lo tanto, la industria debe adoptar Paxos o utilizar algoritmos como 3 PC. Aquí, TiKV se refiere a Percolator y utiliza otra versión mejorada del algoritmo 2 PC.

La siguiente es una breve introducción al algoritmo de transacciones distribuidas de Percolator. Percolator utiliza un bloqueo optimista, lo que significa que los datos que se modificarán en la transacción se almacenarán en caché primero y luego, cuando se realice la confirmación, los datos que se cambiarán se bloquearán y luego se actualizarán. La ventaja de utilizar el bloqueo optimista es que puede mejorar la capacidad de procesamiento concurrente de todo el sistema en muchos escenarios, pero no es tan eficiente como el bloqueo pesimista en el caso de conflictos graves.

Para modificar una fila de datos, Percolator tendrá tres campos correspondientes, bloqueo, escritura y datos:

El bloqueo es el bloqueo real para modificar los datos. En una transacción de Percolator, hay una clave primaria y otras claves secundarias. Solo después de bloquear con éxito la clave principal, intentará bloquear las claves secundarias posteriores.

Escribir, que almacena la marca de tiempo de envío de los datos realmente enviados para escritura. Cuando una transacción se confirma con éxito, escribiremos la marca de tiempo de confirmación de la fila modificada correspondiente.

Datos, guarda los datos reales de la fila.

Cuando se inicia una transacción, primero obtendremos una marca de tiempo de inicio y luego obtendremos los datos de la fila que se va a modificar. Al obtenerlo, si ya hay un bloqueo en la fila de datos, puede finalizar la transacción actual o intentar borrar el bloqueo.

Cuando queremos confirmar una transacción, primero obtenemos la marca de tiempo de confirmación, que tiene dos etapas:

Preescritura: primero intenta bloquear la clave principal y luego intenta bloquear la clave secundaria. Si ya existe un bloqueo en la clave correspondiente, o hay una nueva operación de escritura después de la marca de tiempo de inicio, la escritura anticipada fallará y finalizaremos la transacción. Al bloquear, también escribiremos datos en los datos.

Enviar: cuando todos los datos involucrados se hayan bloqueado correctamente, podremos enviar la clave principal. En este momento, primero determinaremos si el candado agregado anteriormente todavía está ahí. Si todavía existe, elimine el bloqueo y escriba la marca de tiempo de confirmación. Cuando la clave principal se envía correctamente, podemos enviar la clave secundaria de forma asincrónica. No nos importa si la clave principal se puede enviar correctamente. Incluso si falla, existe un mecanismo para garantizar que los datos se envíen normalmente.