Algoritmo de la rueda del tiempo de Kafka

Prefacio

En Kafka hay muchas operaciones retrasadas.

  1. Envío de un mensaje: tiempo de espera + retraso del mecanismo de reintento.
  2. Retraso del mecanismo de reconocimiento de ACKS.

Kafka no utiliza el Timer o DelayQueue que viene con JDK para implementar la función de retraso, sino que personaliza un temporizador (SystemTimer) basado en la rueda del tiempo para implementar la función de retraso.

La complejidad temporal promedio de las operaciones de inserción y eliminación de Timer y DelayQueue de JDK es O (log (n)), que no puede cumplir con los requisitos de alto rendimiento de Kafka. Según la rueda del tiempo, la complejidad temporal de las operaciones de inserción y eliminación puede ser reducido a O. (1).

La aplicación de las ruedas del tiempo no es exclusiva de Kafka, existen muchos otros escenarios de aplicación. Las ruedas del tiempo se pueden encontrar en componentes como Netty, Akka, Quartz y Zookeeper.

programación de tareas java

Supongamos que hay 1000 tareas, todas ejecutadas en diferentes momentos y que el tiempo tiene una precisión de segundos, ¿cómo se programan todas las tareas?

La primera idea es iniciar un hilo, recorrer todas las tareas cada segundo, encontrar el tiempo de ejecución que coincida con el tiempo actual y ejecutarlo. Si la cantidad de tareas es demasiado grande, recorrer y comparar todas las tareas llevará mucho tiempo.

La segunda idea es ordenar estas tareas y poner primero aquellas con el tiempo de ejecución más cercano (activadas primero). Esto implicará mucho movimiento de elementos (nuevas tareas, ejecución de tareas, eliminación de tareas, etc., todo debe reordenarse)

Temporizador

El paquete JDK viene con una clase de herramienta Temporizador (bajo el paquete java.util), que puede implementar tareas retrasadas (por ejemplo, activación después de 30 minutos) o tareas periódicas (por ejemplo, activación cada hora).

Su esencia es una cola prioritaria (TaskQueue) y un hilo (TimerThread) que ejecuta tareas.

Una cola ordinaria es una estructura de datos de primero en entrar, primero en salir, con elementos agregados al final de la cola y eliminados del encabezado. En una cola de prioridad, los elementos tienen prioridad. Cuando se accede a los elementos, primero se elimina el elemento con mayor prioridad. La cola de prioridad tiene las características de comportamiento de primero en entrar, más grande en salir. Generalmente se implementa utilizando una estructura de datos de montón.

imagen.png

imagen.png

En esta cola de prioridad, la tarea que debe ejecutarse primero ocupa el primer lugar en la cola de prioridad. Luego, TimerThread compara continuamente el tiempo de ejecución de la primera tarea con el tiempo actual. Si se acaba el tiempo, primero verifique si la tarea es una tarea ejecutada periódicamente. Si es así, modifique el tiempo de la tarea actual al siguiente tiempo de ejecución. Si no es una tarea periódica, elimine la tarea de la cola de prioridad. Finalmente realiza la tarea.

Sin embargo, Timer tiene un solo subproceso y no puede satisfacer las necesidades comerciales en muchos escenarios.

Después de JDK1.5, se introdujo una herramienta de programación de tareas que admite subprocesos múltiples, ScheduledThreadPoolExecutor, para reemplazar a TImer. Es uno de varios grupos de subprocesos de uso común y contiene una cola de retraso DelayedWorkQueue y una cola de prioridad.

imagen.png

Implementación de montón mínimo de DelayedWorkQueue

La cola de prioridad utiliza una implementación de montón mínima.

El significado de min-heap: un árbol binario completo, el valor del nodo padre es menor o igual que su nodo hijo izquierdo y su nodo hijo derecho

Por ejemplo, inserte los siguientes datos [1,2,3,7,17,19,25,36,100]

El montón mínimo se ve así.

imagen.png

La complejidad temporal de la inserción y eliminación en la cola de prioridad es O (logn). Cuando la cantidad de datos es grande, el rendimiento de la carga y descarga frecuente del montón no es muy bueno.

Por ejemplo, para insertar 0, el proceso es el siguiente:

  1. Insertar último elemento
    imagen.png

  2. 0 es menor que 19, así que muévalo hacia arriba e intercámbielo.
    imagen.png

  3. 0 es menor que 2, así que muévalo hacia arriba e intercámbielo.
    imagen.png

4. 0 es menor que 2, por lo que es necesario moverlo hacia arriba e intercambiarlo.

imagen.png

Complejidad algorítmica

Un montón mínimo de datos N tiene un total de niveles logN. En el peor de los casos, es necesario moverlo logN veces.

rueda del tiempo

La rueda del tiempo primero considera agrupar todas las tareas y juntar las tareas con el mismo tiempo de ejecución. Por ejemplo, en la imagen siguiente, un subíndice en la matriz representa 1 segundo. Se convertirá en una estructura de datos de una matriz más una lista vinculada. Después de la agrupación, se reducirá el tiempo de recorrido y comparación.

imagen.png

Pero todavía hay un problema: si la cantidad de tareas es muy grande y los tiempos son diferentes, o hay tareas cuyo tiempo de ejecución está muy lejos, ¿es necesario que la longitud de la matriz sea muy larga? Por ejemplo, hay una tarea que se ejecutará en 2 meses, calculada a partir de ahora su subíndice es 5253120.

Entonces la longitud definitivamente no puede ser infinita, solo puede ser una longitud fija. Por ejemplo, la longitud fija es 8, una cuadrícula representa 1 segundo (ahora llamada ranura de cubo) y un círculo puede representar 8 segundos. El hilo transversal solo necesita obtener la tarea una cuadrícula a la vez y ejecutarla, y estará bien.

¿Cómo se puede utilizar una matriz de longitud fija para representar el tiempo más allá de la longitud máxima? Puede utilizar una matriz de bucle.

Por ejemplo, una matriz de bucle con una longitud de 8 puede representar 8 segundos. ¿Cómo poner las tareas ejecutadas después de 8 segundos? Simplemente divide entre 8, usa el resto y colócalo en la cuadrícula correspondiente. Por ejemplo, 10%8=2, se coloca en la segunda cuadrícula. Aquí viene el concepto de rondas, y la tarea en el décimo segundo sólo se ejecuta en la segunda ronda.

imagen.png

En este momento ha surgido el concepto de rueda del tiempo.

Si hay demasiadas tareas y se ejecutan muchas tareas al mismo tiempo, la lista vinculada será muy larga. Aquí podemos transformar aún más esta rueda del tiempo y hacer una rueda del tiempo de múltiples capas.

Por ejemplo: hay 8 cuadrículas en la capa más interna y cada cuadrícula dura 1 segundo; hay 8 cuadrículas en la capa externa y cada cuadrícula dura 8*8 = 64 segundos; la capa más interna se mueve una vez y la capa externa se mueve una vez. En este momento, la rueda del tiempo se parece más a un reloj. A medida que pasa el tiempo, las tareas se degradarán y las tareas de la capa exterior se trasladarán lentamente a la capa interior.

imagen.png

La complejidad temporal de la inserción y eliminación de tareas de la rueda del tiempo es O (1), tiene una gama muy amplia de aplicaciones y es más adecuada para escenarios de retraso con una gran cantidad de tareas. Está implementado en Dubbo, Netty y Kafka.

Implementación de la rueda del tiempo en Kafka

Estructura de datos TimingWheel en Kafka

imagen.png

Kafka iniciará un hilo para empujar el puntero de la rueda del tiempo para que gire. El principio de implementación es en realidad sacar el TimerTaskList colocado en la ranura frontal a través de queue.poll()

imagen.png

imagen.png

Agregar nueva tarea diferida

imagen.png

Añade nuevas tareas a la rueda del tiempo

imagen.png

El avance de la rueda del tiempo

imagen.png

El código para crear la segunda capa de la rueda del tiempo es el siguiente

imagen.png

Supongo que te gusta

Origin blog.csdn.net/qq_28314431/article/details/133075920
Recomendado
Clasificación