En los últimos años, con la llegada de conceptos como la computación en la nube y el big data, los sistemas distribuidos han ganado popularidad y relevancia.
Uno de estos tipos de sistemas, las cachés distribuidas que alimentan muchos sitios web dinámicos y aplicaciones web de alto tráfico, suelen consistir en un caso particular de hashing distribuido. Estos aprovechan un algoritmo conocido como hashing consistente.
¿Qué es el hashing consistente? ¿Cuál es la motivación detrás de él, y por qué debería importarte?
En este artículo, primero repasaré el concepto general de hashing y su propósito, seguido de una descripción del hashing distribuido y los problemas que conlleva. A su vez, eso nos llevará a nuestro tema del título.
¿Qué es el hashing?
¿De qué trata el «hashing»? Merriam-Webster define el sustantivo hash como «carne picada mezclada con patatas y dorada», y el verbo como «picar (como carne y patatas) en trozos pequeños». Así que, dejando a un lado los detalles culinarios, hash significa, a grandes rasgos, «picar y mezclar», y es precisamente de ahí de donde procede el término técnico.
Una función hash es una función que mapea una pieza de datos -que suele describir algún tipo de objeto, a menudo de tamaño arbitrario- a otra pieza de datos, normalmente un número entero, conocida como código hash, o simplemente hash.
Por ejemplo, una función de hash diseñada para hashear cadenas, con un rango de salida de 0 .. 100
, puede asignar la cadena Hello
a, digamos, el número 57
, Hasta la vista, baby
al número 33
, y cualquier otra cadena posible a algún número dentro de ese rango. Dado que hay muchas más entradas que salidas posibles, cualquier número tendrá muchas cadenas diferentes asignadas a él, un fenómeno conocido como colisión. Las buenas funciones hash deberían de alguna manera «cortar y mezclar» (de ahí el término) los datos de entrada de tal manera que las salidas para los diferentes valores de entrada se distribuyan lo más uniformemente posible en el rango de salida.
Las funciones hash tienen muchos usos y para cada uno de ellos, se pueden desear diferentes propiedades. Hay un tipo de funciones hash conocidas como funciones hash criptográficas, que deben cumplir con un conjunto restrictivo de propiedades y se utilizan para fines de seguridad-incluyendo aplicaciones como la protección de contraseñas, la comprobación de la integridad y la huella digital de los mensajes, y la detección de la corrupción de datos, entre otros, pero esos están fuera del alcance de este artículo.
Las funciones hash no criptográficas también tienen varios usos, siendo el más común su uso en tablas hash, que es el que nos concierne y que exploraremos con más detalle.
Introducción a las Tablas Hash (Mapas Hash)
Imagina que necesitamos mantener una lista de todos los miembros de algún club a la vez que podemos buscar a cualquier miembro específico. Podríamos manejarlo manteniendo la lista en un array (o lista enlazada) y, para realizar una búsqueda, iterar los elementos hasta encontrar el deseado (podríamos estar buscando en base a su nombre, por ejemplo). En el peor de los casos, eso significaría comprobar todos los miembros (si el que buscamos es el último, o no está presente), o la mitad de ellos en promedio. En términos de teoría de la complejidad, la búsqueda tendría entonces una complejidad O(n)
, y sería razonablemente rápida para una lista pequeña, pero se volvería cada vez más lenta en proporción directa al número de miembros.
¿Cómo se podría mejorar eso? Supongamos que todos estos socios del club tuvieran un miembro ID
, que resulta ser un número secuencial que refleja el orden en el que se unieron al club.
Suponiendo que la búsqueda por ID
fuera aceptable, podríamos colocar todos los socios en una matriz, con sus índices coincidiendo con sus ID
(por ejemplo, un socio con ID=10
estaría en el índice 10
de la matriz). Esto nos permitiría acceder a cada miembro directamente, sin necesidad de buscar. Eso sería muy eficiente, de hecho, tan eficiente como puede ser, lo que corresponde a la menor complejidad posible, O(1)
, también conocido como tiempo constante.
Pero, hay que admitir que nuestro escenario del miembro del club ID
es algo artificioso. ¿Qué pasaría si los ID
fueran números grandes, no secuenciales o aleatorios? ¿O si la búsqueda por ID
no fuera aceptable y tuviéramos que buscar por nombre (o algún otro campo)? Sin duda, sería útil mantener nuestro acceso directo rápido (o algo parecido) y, al mismo tiempo, poder manejar conjuntos de datos arbitrarios y criterios de búsqueda menos restrictivos.
Aquí es donde las funciones hash vienen al rescate. Una función hash adecuada puede utilizarse para asignar un dato arbitrario a un entero, que desempeñará un papel similar al de nuestro miembro del club ID
, aunque con algunas diferencias importantes.
En primer lugar, una buena función hash suele tener un amplio rango de salida (normalmente, todo el rango de un entero de 32 o 64 bits), por lo que construir una matriz para todos los índices posibles sería poco práctico o sencillamente imposible, y un derroche colosal de memoria. Para superar esto, podemos tener una matriz de tamaño razonable (digamos, sólo el doble del número de elementos que esperamos almacenar) y realizar una operación de módulo en el hash para obtener el índice de la matriz. Así, el índice sería index = hash(object) mod N
, donde N
es el tamaño del array.
En segundo lugar, los hashes de los objetos no serán únicos (a menos que trabajemos con un conjunto de datos fijo y una función hash perfecta hecha a medida, pero no hablaremos de eso aquí). Habrá colisiones (incrementadas por la operación modulo), y por lo tanto un simple acceso directo al índice no funcionará. Hay varias maneras de manejar esto, pero una típica es adjuntar una lista, comúnmente conocida como cubo, a cada índice del array para mantener todos los objetos que comparten un índice dado.
Así, tenemos un array de tamaño N
, con cada entrada apuntando a un cubo de objetos. Para añadir un nuevo objeto, tenemos que calcular su hash modulo N
, y comprobar el cubo en el índice resultante, añadiendo el objeto si no está ya allí. Para buscar un objeto, hacemos lo mismo, simplemente buscando en el cubo para comprobar si el objeto está allí. Una estructura de este tipo se denomina tabla hash, y aunque las búsquedas dentro de los cubos son lineales, una tabla hash de tamaño adecuado debería tener un número razonablemente pequeño de objetos por cubo, lo que resulta en un tiempo de acceso casi constante (una complejidad media de O(N/k)
, donde k
es el número de cubos).
Con los objetos complejos, la función hash normalmente no se aplica a todo el objeto, sino a una clave en su lugar. En nuestro ejemplo del socio del club, cada objeto podría contener varios campos (como nombre, edad, dirección, correo electrónico, teléfono), pero podríamos elegir, por ejemplo, el correo electrónico para que actúe como clave, de modo que la función hash se aplicaría sólo al correo electrónico. De hecho, no es necesario que la clave forme parte del objeto; es habitual almacenar pares clave/valor, en los que la clave suele ser una cadena relativamente corta y el valor puede ser un dato arbitrario. En estos casos, la tabla de hash o el mapa de hash se utiliza como un diccionario, y esa es la forma en que algunos lenguajes de alto nivel implementan los objetos o las matrices asociativas.
Escalado: Hashing distribuido
Ahora que hemos hablado del hashing, estamos listos para ver el hashing distribuido.
En algunas situaciones, puede ser necesario o deseable dividir una tabla hash en varias partes, alojadas en diferentes servidores. Una de las principales motivaciones para esto es eludir las limitaciones de memoria del uso de un solo ordenador, permitiendo la construcción de tablas hash arbitrariamente grandes (dados suficientes servidores).
En tal escenario, los objetos (y sus claves) se distribuyen entre varios servidores, de ahí el nombre.
Un caso de uso típico para esto es la implementación de cachés en memoria, como Memcached.
Estas configuraciones consisten en un grupo de servidores de caché que alojan muchos pares clave/valor y se utilizan para proporcionar un acceso rápido a los datos originalmente almacenados (o calculados) en otro lugar. Por ejemplo, para reducir la carga de un servidor de base de datos y, al mismo tiempo, mejorar el rendimiento, se puede diseñar una aplicación para que, en primer lugar, obtenga los datos de los servidores de caché y, sólo si no están presentes allí -situación conocida como cache miss-, recurra a la base de datos, ejecutando la consulta pertinente y almacenando los resultados en la caché con una clave adecuada, de modo que se pueda encontrar la próxima vez que se necesite.
Ahora bien, ¿cómo se realiza la distribución? ¿Qué criterios se utilizan para determinar qué claves se alojan en qué servidores?
La forma más sencilla es tomar el módulo del hash del número de servidores. Es decir, server = hash(key) mod N
, donde N
es el tamaño del pool. Para almacenar o recuperar una clave, el cliente primero calcula el hash, aplica una operación modulo N
y utiliza el índice resultante para contactar con el servidor apropiado (probablemente utilizando una tabla de búsqueda de direcciones IP). Tenga en cuenta que la función hash utilizada para la distribución de la clave debe ser la misma en todos los clientes, pero no tiene por qué ser la misma utilizada internamente por los servidores de caché.
Veamos un ejemplo. Digamos que tenemos tres servidores, A
, B
y C
, y tenemos unas claves de cadena con sus hashes:
KEY | HASH | HASH mod 3 |
---|---|---|
«john» | 1633428562 | 2 |
«bill» | 7594634739 | 0 |
«jane» | 5000799124 | 1 |
«steve» | 9787173343 | 0 |
«kate» | 3421657995 | 2 |
Un cliente quiere recuperar el valor de la clave john
. Su hash modulo 3
es 2
, por lo que debe contactar con el servidor C
. La clave no se encuentra allí, por lo que el cliente obtiene los datos de la fuente y los añade. El pool queda así:
A | B | C |
---|---|---|
«john» |
A continuación otro cliente (o el mismo) quiere recuperar el valor de la clave bill
. Su hash modulo 3
es 0
, por lo que debe contactar con el servidor A
. La clave no se encuentra allí, así que el cliente recupera los datos de la fuente y los añade. El pool se ve así ahora:
A | B | C |
---|---|---|
«bill» | «john» |
Después de añadir el resto de claves, el pool se ve así:
A | B | C |
---|---|---|
«bill» | «jane» | «john» |
«steve» | «kate» |
El Problema del Rehacer
Este esquema de distribución es simple, intuitivo, y funciona bien. Eso es, hasta que el número de servidores cambia. ¿Qué ocurre si uno de los servidores se bloquea o deja de estar disponible? Hay que redistribuir las claves para tener en cuenta el servidor que falta, por supuesto. Lo mismo ocurre si se añaden uno o varios servidores nuevos al pool; hay que redistribuir las claves para incluir los nuevos servidores. Esto es cierto para cualquier esquema de distribución, pero el problema con nuestra simple distribución en módulo es que cuando el número de servidores cambia, la mayoría de los hashes modulo N
cambiarán, por lo que la mayoría de las claves tendrán que ser trasladadas a un servidor diferente. Así que, aunque se elimine o se añada un solo servidor, es probable que todas las claves tengan que volver a ser trasladadas a un servidor diferente.
De nuestro ejemplo anterior, si eliminamos el servidor C
, tendríamos que rehacer todas las claves usando hash modulo 2
en lugar de hash modulo 3
, y las nuevas ubicaciones para las claves serían:
KEY | HASH | HASH mod 2 |
---|---|---|
«john» | 1633428562 | 0 |
«bill» | 7594634739 | 1 |
«jane» | 5000799124 | 0 |
«steve» | 9787173343 | 1 |
«kate» | 3421657995 | 1 |
A | B |
---|---|
«john» | «bill» |
«jane» | «steve» |
«kate» |
Nota que todas las ubicaciones de las teclas cambiaron, no sólo las del servidor C
.
En el típico caso de uso que mencionamos antes (almacenamiento en caché), esto significaría que, de repente, las claves no se encontrarán porque todavía no estarán presentes en su nueva ubicación.
Así, la mayoría de las consultas resultarán en fallos, y los datos originales probablemente necesitarán ser recuperados de nuevo desde la fuente para ser rehechos, poniendo así una pesada carga en el servidor(es) de origen (normalmente una base de datos). Esto puede muy bien degradar el rendimiento severamente y posiblemente colapsar los servidores de origen.
La Solución: Hashing consistente
Entonces, ¿cómo se puede resolver este problema? Necesitamos un esquema de distribución que no dependa directamente del número de servidores, de forma que, al añadir o eliminar servidores, se minimice el número de claves que hay que reubicar. Uno de estos esquemas, inteligente pero sorprendentemente sencillo, se llama hashing consistente, y fue descrito por primera vez por Karger et al. en el MIT en un artículo académico de 1997 (según Wikipedia).
El hashing consistente es un esquema de hashing distribuido que funciona independientemente del número de servidores u objetos en una tabla de hash distribuida, asignándoles una posición en un círculo abstracto, o anillo de hash. Esto permite a los servidores y objetos escalar sin afectar al sistema en general.
Imagina que mapeamos el rango de salida del hash en el borde de un círculo. Eso significa que el valor mínimo posible de hash, cero, correspondería a un ángulo de cero, el valor máximo posible (algún gran entero que llamaremos INT_MAX
) correspondería a un ángulo de 2𝝅 radianes (o 360 grados), y todos los demás valores de hash encajarían linealmente en algún punto intermedio. Así, podríamos tomar una clave, calcular su hash y averiguar dónde se encuentra en el borde del círculo. Asumiendo un INT_MAX
de 1010 (por ejemplo), las claves de nuestro ejemplo anterior se verían así:
CLAVE | HASH | ANGULO (DEG) |
---|---|---|
«juan» | 1633428562 | 58.8 |
«bill» | 7594634739 | 273.4 |
«jane» | 5000799124 | 180 |
«steve» | 9787173343 | 352.3 |
«kate» | 3421657995 | 123.2 |
Ahora imagina que también colocamos los servidores en el borde del círculo, asignándoles ángulos de forma pseudo-aleatoria también. Esto debe hacerse de forma repetible (o al menos de forma que todos los clientes estén de acuerdo con los ángulos de los servidores). Una manera conveniente de hacer esto es haciendo un hash del nombre del servidor (o dirección IP, o algún ID) -como haríamos con cualquier otra clave- para obtener su ángulo.
En nuestro ejemplo, las cosas podrían verse así:
KEY | HASH | ANGLE (DEG) |
---|---|---|
«john» | 1633428562 | 58.8 |
«bill» | 7594634739 | 273.4 |
«jane» | 5000799124 | 180 |
«steve» | 9787173343 | 352.3 |
«kate» | 3421657995 | 123.2 |
«A» | 5572014558 | 200.6 |
«B» | 8077113362 | 290,8 |
«C» | 2269549488 | 81.7 |
Como tenemos las claves de los objetos y de los servidores en el mismo círculo, podemos definir una regla sencilla para asociar los primeros con los segundos: Cada clave de objeto pertenecerá al servidor cuya clave esté más cerca, en sentido contrario a las agujas del reloj (o a las agujas del reloj, según las convenciones utilizadas). Es decir, para saber a qué servidor pedir una clave determinada, debemos localizar la clave en el círculo y movernos en la dirección del ángulo ascendente hasta encontrar un servidor.
En nuestro ejemplo:
KEY | HASH | ANGLE (DEG) |
---|---|---|
«john» | 1633428562 | 58.7 |
«C» | 2269549488 | 81.7 |
«kate» | 3421657995 | 123.1 |
«jane» | 5000799124 | 180 |
«A» | 5572014557 | 200.5 |
«factura» | 7594634739 | 273.4 |
«B» | 8077113361 | 290,7 |
«steve» | 787173343 | 352.3 |
KEY | HASH | ANGLE (DEG) | LABEL | SERVER |
---|---|---|---|---|
«john» | 1632929716 | 58.7 | «C» | C |
«kate» | 3421831276 | 123.1 | «A» | A |
«jane» | 5000648311 | 180 | «A» | A |
«bill» | 7594873884 | 273.4 | «B» | B |
«steve» | 9786437450 | 352.3 | «C» | C |
Desde el punto de vista de la programación, lo que haríamos es mantener una lista ordenada de los valores de los servidores (que podrían ser ángulos o números en cualquier intervalo real), y recorrer esta lista (o utilizar una búsqueda binaria) para encontrar el primer servidor con un valor mayor o igual al de la clave deseada. Si no se encuentra tal valor, tenemos que dar la vuelta, tomando el primero de la lista.
Para asegurar que las claves de los objetos se distribuyen uniformemente entre los servidores, tenemos que aplicar un sencillo truco: Asignar no una, sino muchas etiquetas (ángulos) a cada servidor. Así, en lugar de tener las etiquetas A
, B
y C
, podríamos tener, por ejemplo, A0 .. A9
, B0 .. B9
y C0 .. C9
, todas intercaladas a lo largo del círculo. El factor por el que aumentar el número de etiquetas (claves del servidor), conocido como peso, depende de la situación (e incluso puede ser diferente para cada servidor) para ajustar la probabilidad de que las claves acaben en cada uno. Por ejemplo, si el servidor B
fuera el doble de potente que el resto, se le podría asignar el doble de etiquetas y, como resultado, acabaría teniendo el doble de objetos (de media).
Para nuestro ejemplo supondremos que los tres servidores tienen un peso igual de 10 (esto funciona bien para tres servidores, para 10 a 50 servidores, un peso en el rango de 100 a 500 funcionaría mejor, y los pools más grandes pueden necesitar pesos aún mayores):
KEY | HASH | ANGLE (DEG) |
---|---|---|
«C6» | 408965526 | 14.7 |
«A1» | 473914830 | 17 |
«A2» | 548798874 | 19.7 |
«A3» | 1466730567 | 52.8 |
«C4» | 1493080938 | 53,7 |
«john» | 1633428562 | 58.7 |
«B2» | 1808009038 | 65 |
«C0» | 1982701318 | 71.3 |
«B3» | 2058758486 | 74,1 |
«A7» | 2162578920 | 77.8 |
«B4» | 2660265921 | 95,7 |
«C9» | 3359725419 | 120.9 |
«kate» | 3421657995 | 123.1 |
«A5» | 34972143 | 123.6 |
«C1» | 3672205973 | 132.1 |
«C8» | 3750588567 | 135 |
«B0» | 4049028775 | 145.7 |
«B8» | 4755525684 | 171.1 |
«A9» | 4769549830 | 171.7 |
«jane» | 5000799124 | 180 |
«C7» | 5014097839 | 180.5 |
«B1» | 5444659173 | 196 |
«A6» | 6210502707 | 223.5 |
«A0» | 6511384141 | 234.4 |
«B9» | 7292819872 | 262.5 |
«C3» | 7330467663 | 263.8 |
«C5» | 7502566333 | 270 |
«bill» | 7594634739 | 273.4 |
«A4» | 8047401090 | 289.7 |
«C2» | 8605012288 | 309.7 |
«A8» | 8997397092 | 323.9 |
«B7» | 9038880553 | 325.3 |
«B5» | 93682254 | 337.2 |
«B6» | 9379713761 | 337.6 |
«steve» | 9787173343 | 352.3 |
KEY | HASH | ANGLE (DEG) | LABEL | SERVER |
---|---|---|---|---|
«john» | 1632929716 | 58.7 | «B2» | B |
«kate» | 3421831276 | 123.1 | «A5» | A |
«jane» | 5000648311 | 180 | «C7» | C |
«bill» | 7594873884 | 273.4 | «A4» | A |
«steve» | 9786437450 | 352.3 | «C6» | C |
Entonces, ¿cuál es el beneficio de toda esta aproximación al círculo? Imagina que el servidor C
es eliminado. Para tener en cuenta esto, debemos eliminar las etiquetas C0 .. C9
del círculo. Esto hace que las claves de objeto antes adyacentes a las etiquetas eliminadas se etiqueten ahora aleatoriamente como Ax
y Bx
, reasignándolas a los servidores A
y B
.
¿Pero qué pasa con las otras claves de objeto, las que originalmente pertenecían a A
y B
? Nada. Esa es la gracia del asunto: La ausencia de etiquetas Cx
no afecta a esas claves de ninguna manera. Así que, al eliminar un servidor, sus claves de objeto se reasignan aleatoriamente al resto de los servidores, dejando todas las demás claves intactas:
KEY | HASH | ANGLE (DEG) |
---|---|---|
«A1» | 473914830 | 17 |
«A2» | 548798874 | 19.7 |
«A3» | 1466730567 | 52,8 |
«john» | 1633428562 | 58.7 |
«B2» | 1808009038 | 65 |
«B3» | 2058758486 | 74.1 |
«A7» | 2162578920 | 77,8 |
«B4» | 2660265921 | 95.7 |
«kate» | 3421657995 | 123.1 |
«A5» | 34972143 | 123.6 |
«B0» | 4049028775 | 145.7 |
«B8» | 4755525684 | 171.1 |
«A9» | 4769549830 | 171.7 |
«jane» | 5000799124 | 180 |
«B1» | 5444659173 | 196 |
«A6» | 6210502707 | 223.5 |
«A0» | 6511384141 | 234.4 |
«B9» | 7292819872 | 262.5 |
«bill» | 7594634739 | 273.4 |
«A4» | 8047401090 | 289.7 |
«A8» | 8997397092 | 323.9 |
«B7» | 9038880553 | 325.3 |
«B5» | 9368225254 | 337.2 |
«B6» | 9379713761 | 337.6 |
«steve» | 9787173343 | 352.3 |
KEY | HASH | ANGLE (DEG) | LABEL | SERVER |
---|---|---|---|---|
«john» | 1632929716 | 58.7 | «B2» | B |
«kate» | 3421831276 | 123.1 | «A5» | A |
«jane» | 5000648311 | 180 | «B1» | B |
«bill» | 7594873884 | 273.4 | «A4» | A |
«steve» | 9786437450 | 352.3 | «A1» | A |
Algo similar ocurre si, en lugar de eliminar un servidor, añadimos uno. Si quisiéramos añadir el servidor D
a nuestro ejemplo (digamos, como sustituto de C
), tendríamos que añadir las etiquetas D0 .. D9
. El resultado sería que aproximadamente un tercio de las claves existentes (todas pertenecientes a A
o B
) se reasignarían a D
, y, de nuevo, el resto se mantendría igual:
CLAVE | HASH | ANGULO (DEG) |
---|---|---|
«D2» | 439890723 | 15.8 |
«A1» | 473914830 | 17 |
«A2» | 548798874 | 19.7 |
«D8» | 796709216 | 28,6 |
«D1» | 1008580939 | 36.3 |
«A3» | 1466730567 | 52,8 |
«D5» | 1587548309 | 57.1 |
«john» | 1633428562 | 58.7 |
«B2» | 1808009038 | 65 |
«B3» | 2058758486 | 74.1 |
«A7» | 2162578920 | 77,8 |
«B4» | 2660265921 | 95.7 |
«D4» | 2909395217 | 104.7 |
«kate» | 3421657995 | 123.1 |
«A5» | 3434972143 | 123.6 |
«D7» | 3567129743 | 128.4 |
«B0» | 4049028775 | 145.7 |
«B8» | 4755525684 | 171.1 |
«A9» | 4769549830 | 171.7 |
«jane» | 5000799124 | 180 |
«B1» | 5444659173 | 196 |
«D6» | 5703092354 | 205.3 |
«A6» | 6210502707 | 223,5 |
«A0» | 6511384141 | 234.4 |
«B9» | 7292819872 | 262.5 |
«bill» | 7594634739 | 273.4 |
«A4» | 8047401090 | 289.7 |
«D0» | 8272587142 | 297.8 |
«A8» | 8997397092 | 323.9 |
«B7» | 9038880553 | 325.3 |
«D3» | 9048608874 | 325.7 |
«D9» | 9314459653 | 335.3 |
«B5» | 9368225254 | 337.2 |
«B6» | 9379713761 | 337.6 |
«steve» | 9787173343 | 352.3 |
KEY | HASH | ANGLE (DEG) | LABEL | SERVER |
---|---|---|---|---|
«john» | 1632929716 | 58.7 | «B2» | B |
«kate» | 3421831276 | 123.1 | «A5» | A |
«jane» | 5000648311 | 180 | «B1» | B |
«bill» | 7594873884 | 273.4 | «A4» | A |
«steve» | 9786437450 | 352.3 | «D2» | D |
Así es como el hashing consistente resuelve el problema del refrito.
En general, sólo k/N
claves necesitan ser remapeadas cuando k
es el número de claves y N
es el número de servidores (más específicamente, el máximo del número inicial y final de servidores).
Observamos que cuando se utiliza la caché distribuida para optimizar el rendimiento, puede ocurrir que el número de servidores de caché cambie (las razones de esto pueden ser la caída de un servidor, o la necesidad de añadir o eliminar un servidor para aumentar o disminuir la capacidad global). Al utilizar el hashing consistente para distribuir las claves entre los servidores, podemos estar seguros de que si eso sucede, el número de claves que se rehacen -y por lo tanto, el impacto en los servidores de origen- se reducirá al mínimo, evitando posibles tiempos de inactividad o problemas de rendimiento.
Hay clientes para varios sistemas, como Memcached y Redis, que incluyen soporte para el hashing consistente fuera de la caja.
Alternativamente, puede implementar el algoritmo usted mismo, en su lenguaje de elección, y que debe ser relativamente fácil una vez que se entiende el concepto.
Si la ciencia de datos le interesa, Toptal tiene algunos de los mejores artículos sobre el tema en el blog