- En sistemas modernos donde varios núcleos físicos leen el reloj al mismo tiempo, incluso las marcas de tiempo de nanosegundos se superponen fácilmente, y en una medición simultánea con 4 núcleos físicos alrededor del 5% de todas las muestras colisionó
- Es riesgoso diseñar sistemas que usen marcas de tiempo sin procesar como si fueran identificadores únicos, y la frecuencia de colisión varía según el sistema operativo y la forma de ejecución
time.Now()de Go registra tanto el tiempo absoluto como el tiempo relativo basado en un reloj monótono, lo que permite verificar por separado la diferencia entre llamadas consecutivas y la duplicación de marcas de tiempo absolutas- En Linux de un solo hilo, el tiempo siempre aumentó y el incremento mínimo fue de 32 ns, pero cuando los hilos se separan se observa el mismo tiempo absoluto
- En Mac OS X, el tiempo absoluto tiene resolución de microsegundos, así que hay muchas más colisiones, y aun en un solo hilo se observa con frecuencia que el reloj monótono no aumenta
Frecuencia de colisiones revelada en lecturas concurrentes
- La pregunta clave es con qué frecuencia ocurren en la práctica las colisiones de marcas de tiempo de nanosegundos en sistemas modernos
- Si se lee el reloj al mismo tiempo en 4 núcleos físicos, alrededor del 5% de todas las muestras colisiona
- Incluso usando solo 2 hilos en un sistema de 4 núcleos, cerca del 2% de las marcas de tiempo se superpone
- Por lo tanto, no es seguro asumir que se puede crear un ID único solo con una marca de tiempo de nanosegundos sin procesar
Método de prueba y diferencias por sistema operativo
- El programa de prueba está escrito en Go
time.Now()de Go registra en cada llamada el tiempo absoluto y el tiempo relativo basado en un reloj monótono- La prueba compara la diferencia relativa entre marcas de tiempo consecutivas
- También verifica la duplicación de la marca de tiempo absoluta en sí
-
Linux
- En un solo hilo, el tiempo absoluto y el tiempo monótono siempre aumentan
- El incremento mínimo del sistema medido fue de 32 ns
- Entre hilos, alrededor del 5% de las veces el tiempo absoluto coincide exactamente con el de otro hilo
-
Mac OS X
- Como el tiempo absoluto tiene resolución de microsegundos, en la misma prueba ocurren muchísimas colisiones
- Incluso en un solo hilo, se observó con frecuencia que el reloj monótono no aumentaba
1 comentarios
Comentarios de Hacker News
Usar IDs que incluyan un componente de tiempo y un número de secuencia es una forma de evitar este problema
Por ejemplo, UUIDv7 tiene un componente temporal con resolución de milisegundos, un campo que incrementa para cada evento dentro del mismo milisegundo y suficientes bits aleatorios como para que la probabilidad de colisión entre IDs generados en distintas máquinas sea astronómicamente baja
Claro, la cantidad de bits es finita, así que si hay demasiados eventos en el mismo intervalo de tiempo el contador puede desbordarse, las colisiones entre máquinas sí pueden ocurrir en la práctica y la operación de incremento puede requerir sincronización de CPU, limitando la velocidad de generación de eventos
Aun así, a escala real de trabajo, UUIDv7 funciona muy bien
No encuentro bien en internet desde cuándo existe UUIDv7
Solo consume bits dentro del UUID y no aporta mucho a la entropía
Todavía no está integrado en el núcleo, pero además de usarlo directamente a nivel de aplicación, hay varias excelentes extensiones de pg que ofrecen uuidv7
Más allá de ser difíciles de entender, incluso distinguirlos visualmente entre sí es muy complicado
Por eso, en algunos casos, un identificador que no incluya absolutamente nada de información o ruido aparte del mínimo necesario puede ser útil
Un contador incremental con desbordamiento, en cualquier forma, y unos pocos bits aleatorios suele ser suficiente, y si se diseña bien, ambos pueden hacerse sin ramas
Relacionado con eso, hace tiempo fui program manager a cargo del registro de eventos de seguridad de Windows
En sistemas multinúcleo, cuando las cosas ocurren al mismo tiempo o con muy poca diferencia temporal, la planificación de hilos puede influir mucho en lo que se observa
Por ejemplo, el quantum de un hilo puede terminar antes de llegar a la llamada al sistema que obtiene la marca de tiempo, o antes de entregar el búfer que pondrá el evento en cola para marcarlo después
De hecho, en los sistemas multiprocesador de Windows de los 2000 era muy común que las entradas del registro de eventos parecieran fuera de orden, y tampoco se podía confiar demasiado en la precisión de las marcas de tiempo del log
En la práctica, el límite inferior seguro era de 1 segundo, y recuerdo que algunos componentes truncaban o redondeaban la marca de tiempo
Si necesitas un identificador único, usa un UUID versión 4, o sea, un UUID aleatorio
La probabilidad de colisión es parecida a la de que, por fluctuaciones cuánticas, aparezca de repente un dinosaurio adulto en tu dormitorio
Hablando más en serio, si se puede usar, probablemente lo mejor sea un valor incremental tradicional
Es rápido y barato, especialmente en bases de datos, pero tiene problemas de privacidad y seguridad porque permite inferir información a partir del valor del ID
En esos casos, o cuando trabajas con sistemas distribuidos, UUID es mejor
Aunque la resolución sea de nanosegundos, me pregunto cuál será realmente la precisión del reloj de una computadora
Me cuesta imaginar que de verdad sea a nivel de nanosegundos, y me recuerda a cuando en clases de laboratorio de física les insistía a los estudiantes que el dígito más pequeño que muestra un instrumento de medición no es lo mismo que su exactitud
Pero eso no significa que sea exacto a ese nivel, y en sistemas multinúcleo puede que los relojes entre núcleos ni siquiera estén sincronizados con esa precisión
ARMv8 garantiza que el reloj avance al menos a 1GHz, pero Intel y ARM anteriores son más complicados
La BEAM VM de Erlang/Elixir hace esta diferencia muy clara. Es la distinción entre monotónicamente creciente y estrictamente monotónicamente creciente
https://www.erlang.org/doc/apps/erts/time_correction.html#mo...
“En una secuencia de valores monotónicamente crecientes, cada valor que tiene un valor anterior es mayor o igual que ese valor anterior”
Esto se puede usar con la función https://www.erlang.org/doc/man/erlang.html#monotonic_time-0
https://www.erlang.org/doc/apps/erts/time_correction.html#st...
“En una secuencia de valores estrictamente monotónicamente crecientes, cada valor que tiene un valor anterior es mayor que ese valor anterior”
Los valores estrictamente monotónicos implican algún tipo de sincronización o coordinación, y eso conlleva un costo de rendimiento cuando hay muchos procesos concurrentes
Esta función se ofrece mediante https://www.erlang.org/doc/man/erlang.html#unique_integer-1, y la documentación también advierte que los valores estrictamente monotónicamente crecientes son intrínsecamente costosos de generar y no escalan bien, así que solo se debe pasar el modificador
monotoniccuando realmente se necesiteEn otras palabras, se parecen a UUIDv1 o a https://en.wikipedia.org/wiki/Snowflake_ID
Solo se necesita realmente un identificador global estrictamente monotónico cuando se requiere un ganador inmediato y consistente de primera/última escritura
En cambio, si se puede usar un ganador de primera/última escritura eventualmente consistente, por ejemplo si los eventos de escritura entran en un event store o una cola donde se linealizan por ID y, entre escrituras “concurrentes”, se conserva solo la de mayor prioridad por ID mientras las demás se descartan durante el procesamiento o en la lectura, yo consideraría primero un par comprimido
(nodeID, seq)Si hace falta ordenar globalmente los eventos, vale especialmente la pena considerar un formato de Snowflake ID como
(timestampMajor, nodeID, timestampMinor, seq)En FreeBSD no existe
CLOCK_MONOTONIC_RAW, así que al comentarlo parece que quedó bienYo entendía que, si había colisiones, algunos timestamps deberían repetirse, pero no logro provocarlas
clock_getres(CLOCK_REALTIME, ...)=1 ns,clock_getres(CLOCK_MONOTONIC, ...)=1 ns, y en 30 muestras también fue aumentando de forma continua con diferencias de aproximadamente 29~71nsAl final, da la impresión de que en algún punto esto baja al nivel de la arquitectura del conjunto de instrucciones
Un CPU que corre a 3GHz obtiene 3 ciclos de reloj por nanosegundo
Con las optimizaciones del compilador, parece bastante posible que las llamadas en ensamblador para leer el registro del reloj queden pegadas una tras otra
Si llamadas consecutivas a
time.Now()ocurren dentro de 3 ciclos de reloj, no sé si sea justo esperar una precisión de nanosegundos realmente únicaAunque las colisiones sean algo raras, que pasen varias veces al día es mucho peor que “casi nunca pasa”
Esto me recordó una leyenda sobre Lotus Notes
Antes supuestamente usaban timestamps con resolución de 1 segundo como IDs únicos
Si había una colisión, simplemente le sumaban 1 segundo, y al final hubo tantas colisiones que los elementos terminaron con marcas de tiempo del futuro
El tiempo absolutamente preciso es un problema de seguridad
Los diseñadores de CPU han estado introduciendo jitter en los relojes de forma intencional desde hace muchísimo tiempo, desde la época del Alpha de DEC, para evitar una predictibilidad total
Incluso en x86, si se ejecuta 3 o 4 veces, se guarda el valor en un registro y luego se revisa al final, parecería que se puede ver que las diferencias de tiempo no son exactamente iguales
No logro encontrar mucho buscando, y si eso incluye incluso los primeros x86, me sorprende que el problema de seguridad de los relojes precisos se hubiera reconocido tan temprano
Personalmente creo que hasta antes de este milenio yo no conocía ese problema, y habría supuesto que el jitter observable del reloj se explicaba por cosas como interrupciones
No digo que esté mal, solo me gustaría saber más
He visto a demasiada gente sorprenderse por colisiones de timestamps de milisegundos o microsegundos
El tipo que más recuerdo y más detesté es el de construir un timestamp con dos llamadas al sistema
Una llamada para los dígitos altos y otra para los bajos, pero por la expulsión del proceso, si después de leer la parte alta la parte baja pasa de 99x a 00x, se puede terminar creando un timestamp anterior al momento que realmente causó la creación de cierta entidad
Entonces parte del código se rompe de maneras muy espectaculares, y al menos dos veces vi bucles infinitos
Si no te grabas esto como algo que siempre hay que evitar, las pruebas pasan el 99.5% de las veces y hace falta alguien con muy buen instinto para detectar patrones y notar que “la misma prueba se puso roja una vez por semana durante mes y medio”
Es demasiado tiempo para dejar viva una bomba lógica dentro del código de CI/CD antes de corregirla