CommunityProgramación y desarrollogithub.com

george-andrikopoulos/domain-low-latency

Disciplina de ingeniería para trabajar con baja latencia. Cubre: atacar el diseño en una segunda pasada; una medición coincide con el régimen que reporta; una cola sin límite no tiene comportamiento de sobrecarga; una estructura mantiene el régimen bajo el cual fue probada; asignado no es residente; responder al requisito en su capa; sin asignación en la ruta crítica; sin omisión coordinada; sin false sharing en una línea activa; sin bucle de reintento en una ruta contienda; sin bloqueo dentro de una ventana de publicación; sin llamada al sistema en una ruta acotada;...

¿Qué es domain-low-latency?

Esta habilidad de dominio describe un conjunto de reglas de ingeniería para desarrollar sistemas de baja latencia, dirigido principalmente a desarrolladores de Rust y Java. Se enfoca en principios fundamentales que deben verificarse en el diseño y la implementación para garantizar un comportamiento predecible bajo carga. Cubre temas como la medición correcta del rendimiento (caliente vs. frío), la gestión de colas acotadas para evitar latencia ilimitada, la verificación de regímenes de concurrencia en estructuras de datos, y la distinción entre memoria asignada y memoria realmente residente. Es útil para cualquier desarrollador que trabaje en sistemas de trading, middleware de alto rendimiento, o cualquier aplicación donde los microsegundos importen. La habilidad se compone de reglas (marcadas con [R:...]) que actúan como guías de diseño y verificación, y es aplicable a través de diferentes plataformas y lenguajes, aunque con énfasis en Rust (donde el sistema de tipos puede imponer algunas reglas) y Java (donde se requieren enfoques diferentes).

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/george-andrikopoulos/relearn/tree/HEAD/claude-pack/skills/domain-low-latency

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

dominio: reglas de baja latencia

Una medición indica si se ejecutó en caliente o en frío, y coincide con la producción [R:una-medición-coincide-con-el-régimen-que-reporta]

Escrito para las audiencias de Rust y Java.

Escrito a partir de JMH y sus ejemplos (Aleksey Shipilev), cuya existencia es la afirmación de que un microbenchmark hecho a mano en la JVM es incorrecto por defecto.

Indica en qué régimen se realizó la medición y verifica que sea el régimen en el que se ejecuta la producción. Aquí residen dos errores y solo uno de ellos tiene reputación.

Medir en frío cuando la producción está caliente. Una primera ejecución se ejecuta interpretada o desde una caché de instrucciones fría, con un predictor de saltos no entrenado, tablas de páginas no pobladas y, en un runtime administrado, sin código compilado. La cifra puede ser de uno a dos órdenes de magnitud peor que el estado estable, y describe el calentamiento más que el trabajo. En la JVM, la escalada es por etapas: intérprete, luego C1, luego C2, con reemplazo en pila para bucles largos, y requiere miles de iteraciones, por lo que un bucle de temporización hecho a mano es incorrecto por defecto y por lo que existe un harness.

Medir en caliente cuando la producción está fría, que es la mitad que no recibe atención porque parece rigor. Una ruta que se ejecuta una vez por minuto nunca está caliente. Un pico de apertura de mercado llega a código frío. Un manejador de errores se ejecuta por primera vez en el día en el peor momento posible. Para cada uno de ellos, la cifra en caliente es un número que ningún usuario verá nunca, y el calentamiento que se descartó cuidadosamente es el comportamiento de producción. Descartarlo no es higiene allí; es eliminar la medición.

Por lo tanto, la regla es una coincidencia de alcance más que un procedimiento: di qué régimen se midió, di en qué régimen está la producción, y muestra que son iguales. Cuando la producción es genuinamente ambas (un flujo constante con ráfagas frías), eso son dos mediciones y dos números reportados, no uno promediado hasta la falta de significado.

Esto es [R:mide-la-afirmación-no-un-subconjunto] aplicado al tiempo en lugar de a un conjunto: las muestras de calentamiento son un subconjunto, descartarlas es una decisión de alcance, y una decisión de alcance tomada por reflejo es la que fabrica un número incorrecto con la autoridad de haber sido medido.

Verificación de modo de fallo: ¿la ruta de producción está caliente o fría cuando importa, y cuál medí? Si el benchmark descartó las primeras N iteraciones, nombra qué tiene la producción que corresponde a esas iteraciones. Si nada lo hace, la parte descartada era la respuesta.

Dos vecinos, deliberadamente separados. [R:sin-omisión-coordinada] es el instrumento que no toma muestras durante los eventos que importan; este es el instrumento tomándolas en el régimen equivocado. Y [R:verifica-que-la-abstracción-no-se-haya-compilado] cubre el caso en que el optimizador eliminó el trabajo que el benchmark estaba midiendo: una medición en caliente de nada en absoluto.

Una cola sin límite convierte la sobrecarga en latencia en lugar de rechazo [R:una-cola-sin-límite-no-tiene-comportamiento-de-sobrecarga]

Escrito para las audiencias de Rust y Java.

Escrito a partir de la Ley de Little (J. D. C. Little, 1961): L = lambda W, por lo que para una cola que nunca rechaza, la espera crece sin límite una vez que la tasa de llegada supera la tasa de servicio.

Decide qué hace la cola cuando el consumo se queda atrás de la producción. Crecer no es una decisión.

La profundidad de la cola es latencia (eso es la Ley de Little, no una heurística): con llegadas a una tasa lambda y una longitud media de cola L, la espera media es L/lambda, por lo que una L sin límite es una espera sin límite. Por lo tanto, una cola sin límite de capacidad no tiene límite de latencia, y un requisito de latencia declarado en cualquier lugar aguas arriba de ella no se sostiene por nada.

El fallo tiene una forma característica que lo hace tarde para diagnosticar. Los productores nunca se bloquean, por lo que nada aguas arriba reporta un problema; el rendimiento parece correcto, porque cada elemento se procesa eventualmente; y el único síntoma es que la latencia aumenta monótonamente. Cuando es visible, el trabajo acumulado está haciendo trabajo que nadie quiere: cada elemento consumido ya está obsoleto, por lo que la capacidad se está gastando en entregar respuestas a preguntas cuyo momento ha pasado, que es la razón por la que el sistema no se recupera cuando la carga vuelve a la normalidad. Tiene que drenar primero, a la misma tasa que se quedó atrás.

Así que acota la cola y elige el comportamiento en el límite. Cuatro son legítimos y la elección es una decisión de dominio, no técnica:

  • Bloquear al productor. Contrapresión real: el consumidor más lento establece la tasa para todos, y la presión se propaga a quien realmente pueda liberarla. Incorrecto cuando el productor no debe ser detenido, que es el régimen del que trata [R:sin-bloqueo-dentro-de-una-ventana-de-publicación].
  • Descartar el más antiguo. Correcto para datos de mercado, lecturas de sensores, telemetría: un tick obsoleto tiene valor negativo y el más nuevo es el único que vale la pena tener.
  • Descartar el más nuevo. Correcto cuando el acumulado es una cola justa y los recién llegados no han recibido ninguna promesa.
  • Rechazar y decirlo. Correcto para la entrada de órdenes y cualquier cosa que un llamante deba saber el destino. Una orden descartada en silencio es peor que una rechazada por todo el ancho de la confianza que el llamante depositó en la llamada.

No elegir selecciona un quinto comportamiento que nadie elegiría deliberadamente: absorber todo, degradar la latencia de cada elemento por igual y fallar eventualmente al agotar la memoria o, en un runtime administrado, por una pausa de recolección causada por el propio acumulado, que ralentiza aún más al consumidor y es lo más parecido en producción a un bucle de retroalimentación con el signo equivocado.

La estructura no acotada sigue siendo la opción correcta donde el límite realmente reside en otro lugar: una cola cuyos productores tienen limitación de velocidad aguas arriba, o cuyo consumidor es demostrablemente más rápido que cualquier tasa de llegada posible. Dilo, en el código, junto a la cola. Una cola no acotada con una razón declarada es una decisión; una sin ella es la ausencia de una decisión que tiene la misma forma.

Verificación de modo de fallo: si el consumo se queda atrás de la producción durante diez segundos, ¿qué hace esta cola y quién se entera? Si la respuesta es "crece" y "nadie", no hay comportamiento de sobrecarga aquí, solo un síntoma de sobrecarga en espera.

Una estructura es correcta solo bajo el régimen de concurrencia que su prueba asumió [R:una-estructura-mantiene-el-régimen-bajo-el-que-fue-probada]

Escrito para las audiencias de Rust y Java.

Escrito a partir de Dmitry Vyukov, Cola MPSC intrusiva basada en nodos, 1024cores, donde el régimen está declarado en el nombre del algoritmo y en ninguna parte de su código.

Lee el régimen del argumento de corrección de la estructura, luego nombra qué en el código impide violarlo. "La documentación dice de un solo consumidor" no es un mecanismo.

MPSC significa multiproductor, un solo consumidor, y el "único" es una condición previa más que una descripción. mpscq_pop de Vyukov lee self->tail, camina desde él y lo escribe de vuelta: una carga simple y un almacenamiento simple, sin sincronización, porque se asumió exactamente un hilo ejecutándolos. Dos consumidores se intercalan allí y ambos pueden devolver el mismo nodo, o uno puede perder un nodo por completo. Ninguno de los resultados genera nada. No hay aserción, no hay candado que contender, no hay ruta de error: la cola devuelve valores plausibles y el programa continúa con un elemento duplicado o desaparecido.

Ese silencio es todo el problema, y se ve agravado por la carga. Dos consumidores ejecutando cien operaciones pueden nunca intercalarse en la ventana que importa; el conjunto de pruebas pasa, la prueba de resistencia en staging pasa, y la intercalación llega con el tráfico de producción. Un defecto que solo aparece bajo las condiciones que no puedes reproducir es indistinguible, desde el exterior, de un defecto en otro lugar completamente diferente.

Así que pon el régimen en el tipo, donde el lenguaje lo permita. Entrega exactamente un manejador de consumidor en la construcción; hazlo ni Clone ni Sync; usa &mut self en pop para que el borrow checker imponga la exclusividad. Entonces, "dos consumidores" no es un error a encontrar, sino un programa que no compila: [R:haz-que-los-estados-ilegales-sean-irrepresentables], y [R:tipos-de-estado-para-protocolos] donde el régimen cambia a lo largo de la vida de una estructura. Este es el caso de mayor apalancamiento de esa disciplina en todo el dominio, porque el control alternativo es una oración en un comentario de cabecera y el fallo que previene es no atribuible.

Cuando el lenguaje no lo soporte (Java no tiene tal manejador), el control tiene que ser algo que realmente se ejecute: registrar el hilo propietario en el primer uso y afirmarlo en cada llamada posterior en una compilación de depuración, o enrutar todo el consumo a través de un solo objeto cuya construcción es el único lugar donde se declara el invariante. Un comentario no es un control ([R:garantía-necesita-un-lector]).

La clase es más amplia que las colas, y los otros miembros fallan de la misma manera. Un anillo SPSC con dos productores. Una estructura documentada como no reentrante, invocada desde un manejador de señales o un callback. Un HashMap alcanzado desde dos hilos porque el campo que lo contenía se volvió compartido tres refactorizaciones después. Un iterador mantenido a través de una mutación. En cada caso, el régimen era una hipótesis del argumento de corrección y nada en el artefacto lo registra.

Verificación de modo de fallo: ¿qué régimen asume el argumento de corrección de esta estructura, y qué fallaría si lo violara ahora mismo? Si la respuesta honesta es "el nombre lo dice" o "somos cuidadosos", la estructura no está protegida y la próxima persona que la use no lo sabrá.

La memoria que nunca ha sido tocada aún no es memoria [R:asignado-no-es-residente]

Escrito para las audiencias de Rust y Java.

Escrito a partir de Ulrich Drepper, "Lo que todo programador debería saber sobre la memoria" (2007), sobre el paginado bajo demanda y el costo del primer toque.

Preasignar no es preparar. Una asignación exitosa compra espacio de direcciones; la página llega en el primer toque, y el toque es lo que cuesta.

malloc y mmap devuelven direcciones virtuales. Las páginas físicas se suministran de forma diferida: la primera escritura en cada página atrapa en el kernel, que encuentra un marco, lo pone a cero (porque los datos del inquilino anterior no deben filtrarse) y lo asigna. Por lo tanto, un búfer de un gigabyte asignado al inicio y escrito por primera vez en la ruta crítica contiene aproximadamente un cuarto de millón de fallos de página, que llegan uno por página, repartidos en la primera pasada sobre él. Cada aserción de "sin asignación" en esa ruta se cumple, y cada una de ellas está midiendo lo incorrecto: nada llamó al asignador.

La residencia tampoco es permanente una vez lograda. Las páginas pueden ser reclamadas bajo presión de memoria o intercambiadas, por lo que un proceso de larga duración que tocó sus búferes al inicio y luego los dejó inactivos puede volver a fallar en una ruta que ha estado libre de fallos durante una semana. Es por eso que el remedio tiene dos mitades y por qué hacer solo la primera es un error común y costoso.

Por lo tanto, para cualquier cosa en una ruta con un plazo: escribe en cada página que la ruta tocará, al inicio, antes de que llegue cualquier trabajo real; el toque debe ser una escritura, ya que una lectura de una página no tocada puede ser servida por la página cero compartida y no probar nada. Luego fíjala: mlockall(MCL_CURRENT | MCL_FUTURE), y sin intercambio en la máquina. En la JVM, -XX:+AlwaysPreTouch hace la primera mitad para el heap y vale la pena el inicio más lento precisamente porque el costo se traslada fuera de la ruta que tiene un límite.

Las páginas grandes cambian la aritmética en ambas direcciones y merecen su propia decisión en lugar de un valor predeterminado. Menos páginas, más grandes, significan menos fallos y menos presión de TLB; cada fallo es más grande, y las páginas grandes transparentes traen una ruta de desfragmentación que puede detener un hilo durante milisegundos exactamente en el momento equivocado. Las páginas grandes explícitas reservadas en el arranque evitan eso; dejar THP en always y esperar es la forma que este dominio sigue teniendo que nombrar.

Toca desde el hilo que será el propietario de la memoria, no desde el hilo que hace la configuración. En una máquina multi-socket, la primera escritura también decide la ubicación: una página se asigna en el nodo de quien la falla, por lo que la forma obvia de satisfacer esta regla (un bucle en el hilo de inicialización que recorre cada búfer) pone cada página en el nodo de ese hilo, y un trabajador fijado al otro socket lee todo a través de la interconexión con aproximadamente el doble de latencia local. La ruta ahora está libre de fallos y completamente remota: un estado estable más lento comprado con un primer mensaje más rápido. Fija primero los hilos propietarios, luego deja que cada uno toque lo que usará.

Verificación de modo de fallo: ¿cada página que esta ruta tocará ha sido escrita, por el hilo que la leerá, y qué impide que se reclame? Tres preguntas, porque responder solo la primera deja un sistema que es rápido hasta que se vuelve inactivo, y responder solo las dos primeras deja uno que es rápido en un solo socket.

[R:sin-asignación-en-la-ruta-crítica] es el hermano y ninguno subsume al otro: ese trata sobre entrar al asignador, este sobre la memoria que ya devolvió. [R:responde-al-requisito-en-su-capa] es por qué los remedios son todos una política de memoria, un bloqueo o un parámetro de arranque: ninguna elección de contenedor o crate llega a esto. [R:sin-bloqueo-dentro-de-una-ventana-de-publicación] es donde muerde más fuerte, porque un fallo de página tomado entre dos almacenamientos de un escritor mantiene la ventana abierta durante toda su duración.

Responde a un requisito en la capa en la que vive [R:responde-al-requisito-en-su-capa]

Escrito para las audiencias de Rust y Java.

Indica en qué capa vive un requisito antes de elegir cualquier cosa, y satisfácelo allí.

Un requisito sobre la máquina (no perturbar un núcleo aislado vecino, no fallar una página en la ruta de envío, no tomar un bloqueo que un hilo de tiempo real contiende) no se responde eligiendo un crate más rápido, un transporte de mejor reputación o una API asíncrona. Esas son decisiones a nivel de biblioteca. Pueden ser correctas e incluso pueden ayudar, pero no mantienen una restricción a nivel de sistema, y tratarlas como si lo hicieran convierte una pregunta abierta en una cerrada sin nada detrás.

El fallo no es la elección incorrecta; es la entrada de contabilidad incorrecta. Una restricción marcada como satisfecha deja de ser examinada. Una restricción abierta todavía...

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

george-andrikopoulos/domain-java

Engineering discipline for working in java. Covers: a speculated path deoptimises when the input changes; a view is not a copy; a wrapper type is not free here; close what you open; design for inheritance or forbid it; equality is one contract; exceptions name what failed; identity is not equality for boxes; no reference to internals escapes; null is not a value; publish safely or not at all; seal the alternatives; serializable is a second constructor

george-andrikopoulos/domain-rust

Engineering discipline for working in rust. Covers: design types first; borrow in signatures; module visibility is deliberate; must use on consequential returns; newtype liberally; parse wide then range check; seal closed trait sets; typestate builder for required fields; typestate for protocols; verify the abstraction compiled away; async all the way; errors name what failed; justify every clone; no let underscore on a local; parse dont validate; private fields only; xplat fixtures; no...

george-andrikopoulos/global

Engineering discipline that applies to every project and language. Covers: guarantee needs a reader; verdict survives the channel; check the claim you inherit; doc currency; measure the claim not a subset; no silent spend; prefer by construction; repair the lying artefact; signal needs a consequence; verify through production path; detector excludes own definitions; an ambiguous write target is a question; anchor outside the checked partys reach; delegate a fan out; diff against the known...

Skills relacionados