Tuesday, 7 November 2017

Ring Buffer Lmax Forex


Estoy tratando de entender el patrón disruptor. He visto el video InfoQ y he intentado leer su artículo. Entiendo que hay un búfer de anillo involucrado, que se inicializa como una matriz muy grande para aprovechar la localidad de caché, eliminar la asignación de la nueva memoria. Parece que hay uno o más enteros atómicos que mantienen un registro de las posiciones. Cada evento parece tener una identificación única y su posición en el anillo se encuentra encontrando su módulo con respecto al tamaño del anillo, etc., etc. Por desgracia, no tengo un sentido intuitivo de cómo funciona. He hecho muchas aplicaciones comerciales y estudiado el modelo de actor. Miró a SEDA, etc. En su presentación mencionaron que este patrón es básicamente cómo funcionan los enrutadores sin embargo no he encontrado buenas descripciones de cómo funcionan los enrutadores. ¿Hay algunos buenos indicadores para una mejor explicación? El proyecto de código de Google hace referencia a un documento técnico sobre la aplicación de la memoria intermedia de anillos, sin embargo, es un poco seco, académico y duro para alguien que quiere aprender cómo funciona. Sin embargo, hay algunas entradas de blog que han comenzado a explicar los internos de una manera más legible. Hay una explicación del buffer de anillo que es el núcleo del patrón disruptor, una descripción de las barreras del consumidor (la parte relacionada con la lectura del disruptor) y alguna información sobre el manejo de múltiples productores disponibles. La descripción más simple del Disruptor es: Es una forma de enviar mensajes entre hilos de la manera más eficiente posible. Se puede utilizar como una alternativa a una cola, pero también comparte una serie de características con SEDA y actores. El disruptor proporciona la capacidad de transmitir un mensaje a otros subprocesos, despertándolo si es necesario (similar a un bloqueo de preguntas). Sin embargo, hay 3 diferencias distintas. El usuario del disruptor define cómo se almacenan los mensajes extendiendo la clase Entry y proporcionando una fábrica para realizar la preasignación. Esto permite la reutilización de la memoria (copia) o la entrada podría contener una referencia a otro objeto. Poner mensajes en el disruptor es un proceso de dos fases, primero se reivindica una ranura en el búfer de anillo, que proporciona al usuario la entrada que se puede llenar con los datos apropiados. Entonces la entrada debe ser comprometida, este enfoque de 2 fases es necesario para permitir el uso flexible de la memoria mencionada anteriormente. Es el commit que hace que el mensaje sea visible para los subprocesos del consumidor. Es responsabilidad del consumidor realizar un seguimiento de los mensajes que se han consumido de la memoria intermedia de anillo. Mover esta responsabilidad fuera del búfer de anillo en sí ayudó a reducir la cantidad de contención de escritura como cada hilo mantiene su propio contador. Comparado con actores El modelo Actor es más cercano al disruptor que la mayoría de los otros modelos de programación, especialmente si utiliza las clases BatchConsumer / BatchHandler que se proporcionan. Estas clases ocultan todas las complejidades de mantener los números de secuencia consumidos y proporcionan un conjunto de devoluciones simples cuando ocurren eventos importantes. Sin embargo, hay un par de diferencias sutiles. El disruptor utiliza un modelo de consumidor de 1 hilo 1, donde los actores utilizan un modelo N: M, es decir, puede tener tantos actores como desee y se distribuirán a través de un número fijo de hilos (generalmente 1 por núcleo). La interfaz BatchHandler proporciona una devolución de llamada adicional (y muy importante) onEndOfBatch (). Esto permite consumidores lentos, p. Aquellos que realizan E / S para enlazar eventos juntos para mejorar el rendimiento. Es posible realizar lotes en otros marcos de actor, sin embargo, como casi todos los demás marcos no proporcionan una devolución de llamada al final del lote, es necesario utilizar un tiempo de espera para determinar el final del lote, resultando en una latencia deficiente. LMAX construyó el patrón Disruptor para reemplazar un enfoque basado en SEDA. La principal mejora que proporcionó sobre SEDA fue la capacidad de trabajar en paralelo. Para ello, el Disruptor admite múltiples transmisiones de los mismos mensajes (en el mismo orden) a varios consumidores. Esto evita la necesidad de etapas de horquilla en la tubería. También permitimos a los consumidores esperar los resultados de otros consumidores sin tener que poner otra etapa de cola entre ellos. Un consumidor puede simplemente mirar el número de secuencia de un consumidor de que es dependiente. Esto evita la necesidad de unir etapas en la tubería. Comparado con las barreras de la memoria Otra forma de pensarlo es como una barrera de memoria estructurada y ordenada. Cuando la barrera del productor forma la barrera de escritura y la barrera del consumidor es la barrera de lectura. No debemos esperar la primera oración del último punto (número 2) en comparación con SEDA en lugar de leer "También podemos permitir que los consumidores esperen los resultados de otros consumidores con tener que poner otra etapa de cola entre ellos". Resultados de otros consumidores sin tener que poner otra etapa de cola entre ellos (es decir, quotwithot debe ser reemplazado por quotwithoutquot) ndash runeks Apr 10 13 at 17:46 Primero me gustaría entender el modelo de programación que ofrece. Hay uno o más escritores. Hay uno o más lectores. Hay una línea de entradas, totalmente ordenada de antiguo a nuevo (foto de izquierda a derecha). Los escritores pueden agregar nuevas entradas en el extremo derecho. Cada lector lee las entradas secuencialmente de izquierda a derecha. Los lectores no pueden leer escritores pasados, obviamente. No hay concepto de borrado de entrada. Yo uso el lector en lugar del consumidor para evitar la imagen de las entradas que se consumen. Sin embargo, entendemos que las entradas a la izquierda del último lector se vuelven inútiles. Generalmente los lectores pueden leer simultáneamente e independientemente. Sin embargo, podemos declarar dependencias entre los lectores. Las dependencias del lector pueden ser gráficas acíclicas arbitrarias. Si el lector B depende del lector A, el lector B no puede leer el lector A. La dependencia del lector surge porque el lector A puede anotar una entrada, y el lector B depende de esa anotación. Por ejemplo, A realiza algún cálculo en una entrada y almacena el resultado en el campo a en la entrada. A, a continuación, seguir adelante, y ahora B puede leer la entrada, y el valor de un A almacenado. Si el lector C no depende de A, C no debe intentar leer a. Este es de hecho un interesante modelo de programación. Independientemente del rendimiento, el modelo solo puede beneficiar a muchas aplicaciones. Por supuesto, el principal objetivo de LMAX es el rendimiento. Utiliza un anillo de entradas preasignado. El anillo es lo suficientemente grande, pero está limitado para que el sistema no se cargue más allá de la capacidad de diseño. Si el anillo está lleno, el escritor (s) esperará hasta que los lectores más lentos avancen y hagan espacio. Los objetos de entrada están preasignados y viven para siempre, para reducir el costo de recolección de basura. No insertamos objetos de entrada nuevos ni eliminamos objetos de entrada antiguos; en su lugar, un escritor solicita una entrada preexistente, rellena sus campos y notifica a los lectores. Esta aparente acción de 2 fases es realmente simplemente una acción atómica. La preasignación de entradas también significa que las entradas adyacentes (muy probablemente) se ubican en celdas de memoria adyacentes y porque los lectores leen las entradas secuencialmente, esto es importante para utilizar cachés de CPU. Y un montón de esfuerzos para evitar bloqueo, CAS, incluso la barrera de la memoria (por ejemplo, utilizar una variable de secuencia no volátil si theres sólo un escritor) Para los desarrolladores de los lectores: Los diferentes lectores de anotación debe escribir en diferentes campos, para evitar la contención de escritura. (De hecho, deben escribir en diferentes líneas de caché.) Un lector de anotación no debe tocar nada que otros lectores no dependientes puedan leer. Es por eso que digo que estos lectores anota entradas, en lugar de modificar entradas. De hecho, tomé el tiempo para estudiar la fuente real, por pura curiosidad, y la idea detrás de ella es bastante simple. La versión más reciente en el momento de escribir esta publicación es 3.2.1. Hay un almacenador intermediario que almacena los acontecimientos pre-asignados que sostendrán los datos para que los consumidores lean. El búfer está respaldado por una matriz de banderas (matriz de números enteros) de su longitud que describe la disponibilidad de las ranuras de búfer (ver más detalles). La matriz se accede como un javaAtomicIntegerArray, por lo que para el propósito de esta explenation puede asumir que sea uno. Puede haber cualquier número de productores. Cuando el productor quiere escribir en el búfer, se genera un número largo (como en llamar AtomicLonggetAndIncrement, el Disruptor realmente usa su propia implementación, pero funciona de la misma manera). Llamamos a esto generado largo un productorCallId. De manera similar, se genera un consumerCallId cuando un consumidor ENDS lee una ranura de un búfer. Se accede al consumerCallId más reciente. (Si hay muchos consumidores, se selecciona la llamada con el id más bajo.) Estos ids son comparados y si la diferencia entre los dos es menor que el lado del búfer, se permite que el productor escriba. (Si el productorCallId es mayor que el reciente buffer de consumo consumerCallId, significa que el búfer está lleno y el productor se ve forzado a esperar hasta que un punto esté disponible). A continuación, se asigna al productor la ranura en el búfer basándose en su función callId (Que es prducerCallId modulo bufferSize, pero dado que el bufferSize es siempre una potencia de 2 (límite aplicado en la creación de búfer), la operación actuall utilizada es producerCallId amp (bufferSize - 1)). Es entonces libre de modificar el evento en esa ranura. (El algoritmo actual es un poco más complicado, implicando el almacenamiento en caché de consumerId reciente en una referencia atómica separada, con fines de optimización). Cuando se modificó el evento, se publica el cambio. Cuando se publica la ranura respectiva en la matriz de indicadores se llena con el indicador actualizado. El valor de la bandera es el número del bucle (producerCallId dividido por bufferSize (de nuevo, puesto que bufferSize es la potencia de 2, la operación real es un turno a la derecha).De una manera similar, puede haber cualquier número de consumidores. Acceder al búfer, se genera un consumerCallId (dependiendo de cómo se hayan añadido los consumidores al disruptor, el átomo utilizado en la generación de id puede ser compartido o separado para cada uno de ellos). Este consumerCallId se compara entonces con el producentCallId más reciente y si Es menor de los dos, se permite al lector progresar (similarmente si el productorCallId es incluso para el consumerCallId, significa que el buffer es empety y el consumidor se ve obligado a esperar. La forma de esperar se define por un WaitStrategy durante el disruptor Creación). Para los consumidores individuales (los que tienen su propio generador de id), lo siguiente que se comprueba es la capacidad de consumir por lotes. Las ranuras en el búfer se examinan en orden desde el respectivo a consumerCallId (el índice se determina en el De la misma manera que para los productores), al respectivo productor reciente. Se examinan en un bucle comparando el valor de indicador escrito en la matriz de indicadores con un valor de indicador generado para consumerCallId. Si las banderas coinciden significa que los productores que llenan las ranuras han confirmado sus cambios. Si no, el bucle se rompe, y se devuelve el cambio más alto cambiado. Las ranuras de ConsumerCallId a recibido en changeId se pueden consumir en lote. Si un grupo de consumidores lee juntos (los que tienen el generador de id compartido), cada uno solo toma un único identificador de llamada, y sólo se comprueba y devuelve la ranura para ese identificador de llamada único. El patrón disruptor es una cola de dosificación respaldada por una matriz circular (es decir, la memoria intermedia de anillo) llena de objetos de transferencia preasignados que utiliza barreras de memoria para sincronizar productores y consumidores a través de secuencias. Las barreras de memoria son un poco difíciles de explicar y el blog de Trishas ha hecho el mejor intento en mi opinión con este post: mechanitis. blogspot / 2011/08 / disecting-disruptor-por-su-tan-rápido. html Pero si no quieres Para sumergirse en los detalles de bajo nivel que sólo puede saber que las barreras de memoria en Java se implementan a través de la palabra clave volátil o a través de la java. util. concurrent. AtomicLong. Las secuencias del patrón disruptor son AtomicLong s y se comunican de un lado a otro entre productores y consumidores a través de barreras de memoria en lugar de bloqueos. Me resulta más fácil entender un concepto a través de código, por lo que el código a continuación es un simple helloworld de CoralQueue. Que es una implementación de patrón disruptor hecha por CoralBlocks con el que estoy afiliado. En el código de abajo se puede ver cómo el patrón disruptor implementa batching y cómo el buffer de anillo (es decir, matriz circular) permite la comunicación libre de basura entre dos hilos: Respondido Jun 16 14 at 22:38 The LMAX Arquitectura Contenido En los últimos años Seguimos escuchando que el almuerzo gratis es over1 - no podemos esperar incrementos en la velocidad individual de la CPU. Así que para escribir código rápido necesitamos utilizar explícitamente varios procesadores con software concurrente. Esto no es una buena noticia - escribir código concurrente es muy difícil. Las cerraduras y los semáforos son difíciles de razonar y difíciles de probar - lo que significa que estamos pasando más tiempo preocupándonos por satisfacer la computadora de lo que estamos resolviendo el problema de dominio. Varios modelos de concurrencia, como Actores y Software Transactional Memory, pretenden hacer esto más fácil - pero todavía hay una carga que introduce errores y complejidad. Así que me fascinó escuchar una charla en QCon London en marzo del año pasado de LMAX. LMAX es una nueva plataforma comercial minorista. Su innovación empresarial es que es una plataforma minorista - que permite a cualquiera comerciar con una gama de productos financieros derivados2. Una plataforma de negociación como esta necesita latencia muy baja - los oficios tienen que ser procesados ​​rápidamente porque el mercado se está moviendo rápidamente. Una plataforma de venta al por menor agrega complejidad porque tiene que hacer esto para mucha gente. Por lo tanto, el resultado es más usuarios, con muchos oficios, todos los cuales necesitan ser procesados ​​rápidamente.3 Dado el cambio al pensamiento multi-core, este tipo de rendimiento exigente sugeriría naturalmente un modelo de programación explícitamente concurrente y de hecho éste era su punto de partida. Pero lo que llamó la atención de la gente en QCon fue que esto no era donde terminaron. De hecho terminaron haciendo toda la lógica de negocio para su plataforma: todos los oficios, de todos los clientes, en todos los mercados - en un solo hilo. Un hilo que procesará 6 millones de pedidos por segundo usando el hardware de la materia.4 Procesando lotes de transacciones con baja latencia y ninguna de las complejidades del código concurrente - cómo puedo resistirme a cavar en eso Afortunadamente otra diferencia que LMAX tiene con otras compañías financieras es que Están muy contentos de hablar sobre sus decisiones tecnológicas. Así que ahora LMAX ha estado en producción durante un tiempo su tiempo para explorar su fascinante diseño. Estructura general Figura 1: Arquitectura de LMAXs en tres blobs En un nivel superior, la arquitectura tiene tres partes de procesador de lógica de negocio5 interruptores de salida de disruptores de entrada Como su nombre lo indica, el procesador de lógica de negocio maneja toda la lógica de negocio en la aplicación. Como he indicado anteriormente, lo hace como un programa java de un solo hilo que reacciona a llamadas de método y produce eventos de salida. Por lo tanto, su un simple programa java que no requiere ningún marco de plataforma para ejecutar otros que la propia JVM, lo que le permite ser ejecutado fácilmente en entornos de prueba. Aunque el procesador de lógica de negocios puede ejecutarse en un entorno simple para realizar pruebas, existe una coreografía más complicada para que funcione en un entorno de producción. Los mensajes de entrada deben ser retirados de una pasarela de red y no ser compartidos, replicados y registrados en diario. Los mensajes de salida necesitan ser empacados para la red. Estas tareas son manejadas por los interruptores de entrada y salida. A diferencia del Procesador Lógico de Negocio, estos son componentes concurrentes, ya que implican operaciones de E / S que son lentas e independientes. Se diseñaron y construyeron especialmente para LMAX, pero ellos (como la arquitectura total) son aplicables en otra parte. Procesador lógico de negocio Mantenerlo todo en la memoria El procesador de lógica de negocios toma los mensajes de entrada secuencialmente (en forma de una invocación de método), ejecuta la lógica de negocios en ella y emite eventos de salida. Funciona totalmente en la memoria, no hay base de datos u otro almacén persistente. Mantener todos los datos en la memoria tiene dos beneficios importantes. En primer lugar su rápido - no hay base de datos para proporcionar IO lento para acceder, ni hay ningún comportamiento transaccional para ejecutar, ya que todo el procesamiento se realiza secuencialmente. La segunda ventaja es que simplifica la programación - no hay mapeo objeto / relacional que hacer. Todo el código se puede escribir usando el modelo de objeto Javas sin tener que hacer concesiones para la asignación a una base de datos. El uso de una estructura en memoria tiene una consecuencia importante: qué sucede si todo se bloquea Incluso los sistemas más resistentes son vulnerables a alguien que está tirando de la energía. El corazón de tratar con esto es Sourcing de eventos - lo que significa que el estado actual del Procesador lógico de negocio es totalmente derivable procesando los eventos de entrada. Siempre y cuando el flujo de eventos de entrada se mantenga en un almacén duradero (que es uno de los trabajos del disruptor de entrada) siempre puede volver a crear el estado actual del motor de lógica de negocio al reproducir los eventos. Una buena manera de entender esto es pensar en un sistema de control de versiones. Los sistemas de control de versiones son una secuencia de compromisos, en cualquier momento se puede construir una copia de trabajo mediante la aplicación de los compromisos. Los VCS son más complicados que el Procesador lógico de negocio porque deben soportar ramificaciones, mientras que el Procesador lógico de negocios es una secuencia simple. Por lo tanto, en teoría, siempre se puede reconstruir el estado del procesador lógico de negocios reprocesando todos los eventos. En la práctica, sin embargo, eso tomaría demasiado tiempo si usted necesita girar uno para arriba. Así, al igual que con los sistemas de control de versiones, LMAX puede hacer instantáneas del estado del procesador de lógica de negocios y restaurarlas desde las instantáneas. Toman una foto cada noche durante los períodos de baja actividad. Reiniciar el Procesador de Lógica de Negocio es rápido, un reinicio completo - incluyendo reiniciar la JVM, cargar una instantánea reciente y reproducir un valor diario de revistas - tarda menos de un minuto. Las instantáneas hacen que la puesta en marcha de un nuevo Procesador lógico de negocio sea más rápida, pero no lo suficientemente rápido si un procesador de lógica de negocios se bloquea a las 2 de la tarde. Como resultado, LMAX mantiene múltiples procesadores lógicos de negocios funcionando todo el tiempo6. Cada evento de entrada es procesado por varios procesadores, pero todos menos un procesador tiene su salida ignorada. Si el procesador en vivo falla, el sistema cambia a otro. Esta capacidad para manejar el fail-over es otro beneficio de usar Event Sourcing. Por evento de abastecimiento en réplicas que pueden cambiar entre los procesadores en cuestión de micro-segundos. Además de tomar fotos cada noche, también reinician los procesadores de lógica de negocios cada noche. La replicación les permite hacer esto sin tiempo de inactividad, por lo que siguen procesando los oficios 24/7. Para obtener más información sobre Sourcing de eventos, vea el patrón de borrador en mi sitio desde hace unos años. El artículo se centra más en el manejo de las relaciones temporales que en los beneficios que utiliza LMAX, pero sí explica la idea central. Event Sourcing es valioso porque permite que el procesador funcione completamente en memoria, pero tiene otra ventaja considerable para los diagnósticos. Si se produce algún comportamiento inesperado, el equipo copia la secuencia de eventos en su entorno de desarrollo y los reproduce allí. Esto les permite examinar lo que sucedió mucho más fácilmente de lo que es posible en la mayoría de los entornos. Esta capacidad de diagnóstico se extiende a los diagnósticos de negocio. Hay algunas tareas empresariales, como en la gestión de riesgos, que requieren un cálculo significativo que no es necesario para procesar los pedidos. Un ejemplo es obtener una lista de los 20 principales clientes por perfil de riesgo en función de sus posiciones comerciales actuales. El equipo se encarga de esto haciendo girar un modelo de dominio replicado y llevando a cabo el cálculo allí, donde no interferirá con el procesamiento de órdenes de núcleo. Estos modelos de dominio de análisis pueden tener variantes de modelos de datos, mantener diferentes conjuntos de datos en la memoria y ejecutar en diferentes máquinas. El rendimiento de la optimización Hasta ahora Ive ha explicado que la clave para la velocidad del procesador de lógica de negocios es hacer todo de forma secuencial, en la memoria. Sólo hacer esto (y nada realmente estúpido) permite a los desarrolladores escribir código que puede procesar 10K TPS7. Entonces descubrieron que concentrarse en los elementos simples de un buen código podría llevarlo a la gama de 100K TPS. Esto sólo necesita código bien factorizado y pequeños métodos - esencialmente esto permite Hotspot para hacer un mejor trabajo de optimización y CPUs para ser más eficiente en el almacenamiento en caché del código como su ejecución. Tomó un poco más de inteligencia para subir otro orden de magnitud. Hay varias cosas que el equipo LMAX encontró útil para llegar allí. Una de ellas era escribir implementaciones personalizadas de las colecciones java que fueron diseñadas para ser compatibles con caché y cuidadas con garbage8. Un ejemplo de esto es el uso de widgets java primitivos como teclas hashmap con una implementación de mapa con respaldo de matriz especialmente escrita (LongToObjectHashMap). En general, han descubierto que la elección de las estructuras de datos a menudo hace una gran diferencia. La mayoría de los programadores simplemente toman cualquier lista que usaron la última vez en lugar de pensar qué implementación es la correcta para este contexto. Atención en las pruebas de rendimiento. Ive notado hace tiempo que la gente habla mucho acerca de las técnicas para mejorar el rendimiento, pero lo único que realmente hace una diferencia es probarlo. Incluso los buenos programadores son muy buenos en la construcción de argumentos de rendimiento que terminan siendo mal, por lo que los mejores programadores prefieren perfiladores y casos de prueba a la especulación.10 El equipo LMAX también ha encontrado que las pruebas de escritura en primer lugar es una disciplina muy eficaz para las pruebas de rendimiento. Modelo de programación Este estilo de procesamiento introduce algunas restricciones en la forma de escribir y organizar la lógica de negocio. El primero de estos es que usted tiene que molestar a cualquier interacción con los servicios externos. Una llamada de servicio externa va a ser lenta, y con un solo hilo se detendrá toda la máquina de procesamiento de pedidos. Como resultado, no puede hacer llamadas a servicios externos dentro de la lógica de negocio. En su lugar, necesita finalizar esa interacción con un evento de salida y esperar a que otro evento de entrada lo recupere de nuevo. Ill utilizar un simple no-LMAX ejemplo para ilustrar. Imagine que está haciendo un pedido de jalea por tarjeta de crédito. Un sistema de venta al por menor simple tomaría su información de la orden, utilice un servicio de la tarjeta de crédito de la validación para comprobar su número de tarjeta de crédito, y después confirmar su orden - todo dentro de una sola operación. El hilo que procesa su orden se bloquearía mientras esperaba que la tarjeta de crédito fuera verificada, pero ese bloque no sería muy largo para el usuario, y el servidor siempre puede ejecutar otro hilo en el procesador mientras espera. En la arquitectura LMAX, dividiría esta operación en dos. La primera operación captaría la información del pedido y finalizará mediante la salida de un evento (validación de tarjeta de crédito solicitada) a la compañía de tarjetas de crédito. El procesador lógico de negocios seguiría procesando eventos para otros clientes hasta que recibiera un evento validado con tarjeta de crédito en su flujo de eventos de entrada. Al procesar ese evento, realizaría las tareas de confirmación para ese pedido. Trabajar en este tipo de estilo asincrónico impulsado por eventos es algo inusual, aunque el uso de la asincronía para mejorar la capacidad de respuesta de una aplicación es una técnica familiar. También ayuda a que el proceso de negocio sea más resistente, ya que hay que ser más explícito al pensar en las diferentes cosas que pueden suceder con la aplicación remota. Una segunda característica del modelo de programación radica en el manejo de errores. El modelo tradicional de sesiones y transacciones de base de datos proporciona una capacidad de manejo de errores útil. En caso de que algo salga mal, es fácil tirar todo lo que sucedió hasta ahora en la interacción. Los datos de la sesión son transitorios, y pueden ser descartados, a costa de alguna irritación al usuario si en medio de algo complicado. Si se produce un error en el lado de la base de datos, puede revertir la transacción. Las estructuras en memoria de LMAX son persistentes en los eventos de entrada, por lo que si hay un error es importante no dejar esa memoria en un estado inconsistente. Sin embargo, no hay instalación automatizada de reversión. Como consecuencia, el equipo LMAX pone mucha atención en asegurar que los eventos de entrada son completamente válidos antes de realizar cualquier mutación del estado persistente en la memoria. Ellos han encontrado que las pruebas son una herramienta clave para eliminar este tipo de problemas antes de entrar en producción. Interruptores de entrada y salida Aunque la lógica empresarial se produce en un único subproceso, hay un número de tareas que deben realizarse antes de poder invocar un método de objeto de negocio. La entrada original para el procesamiento sale del cable en forma de un mensaje, este mensaje necesita ser desmarcado en una forma conveniente para el procesador de Business Logic para usar. Event Sourcing se basa en mantener un diario duradero de todos los eventos de entrada, por lo que cada mensaje de entrada necesita ser registrado en un almacén duradero. Finalmente, la arquitectura se basa en un conjunto de procesadores lógicos de negocios, por lo que tenemos que replicar los mensajes de entrada a través de este clúster. Del mismo modo en el lado de salida, los eventos de salida necesitan ser empacados para la transmisión a través de la red. Figura 2: Las actividades realizadas por el disruptor de entrada (utilizando la notación de diagrama de actividad de UML) El replicador y el diario implican IO y por lo tanto son relativamente lentos. Después de todo la idea central de Business Logic Processor es que evita hacer cualquier IO. Además, estas tres tareas son relativamente independientes, todas ellas deben realizarse antes de que el procesador de lógica de negocios funcione en un mensaje, pero pueden realizarse en cualquier orden. Tan a diferencia de con el Procesador de Lógica de Negocio, donde cada comercio cambia el mercado para operaciones posteriores, hay un ajuste natural para la concurrencia. Para manejar esta concurrencia, el equipo LMAX desarrolló un componente de concurrencia especial, al que llaman un Disruptor 11. El equipo LMAX ha lanzado el código fuente para el Disruptor con una licencia de código abierto. En un nivel crudo se puede pensar en un disruptor como un gráfico de multidifusión de colas en las que los productores ponen objetos en él que se envían a todos los consumidores para el consumo paralelo a través de colas separadas aguas abajo. Cuando miras dentro, ves que esta red de colas es realmente una estructura de datos única - un buffer de anillo. Cada productor y consumidor tiene un contador de secuencia para indicar qué ranura en el búfer está trabajando actualmente. Cada productor / consumidor escribe su propio contador de secuencias pero puede leer los otros contadores de secuencia. De esta manera, el productor puede leer los contadores de los consumidores para asegurarse de que la ranura en la que desea escribir está disponible sin cerraduras en los contadores. Del mismo modo un consumidor puede asegurar que sólo procesa mensajes una vez que otro consumidor se hace con él, mirando los contadores. Figura 3: El disruptor de entrada coordina a un productor ya cuatro consumidores Los disruptores de salida son similares, pero sólo tienen dos consumidores secuenciales para el empaquetado y la salida.12 Los eventos de salida se organizan en varios temas, de modo que los mensajes pueden enviarse solamente a los receptores interesados en ellos. Cada tema tiene su propio disruptor. Los disruptores Ive descrito se utilizan en un estilo con un productor y múltiples consumidores, pero esto no es una limitación del diseño del disruptor. El disruptor puede trabajar con múltiples productores también, en este caso todavía no necesita cerraduras.13 Un beneficio del diseño del disruptor es que facilita a los consumidores ponerse al día rápidamente si se topan con un problema y se quedan atrás. Si el unmarshaler tiene un problema al procesar en la ranura 15 y regresa cuando el receptor está en la ranura 31, puede leer los datos de las ranuras 16-30 en un lote para ponerse al día. Este lote leído de los datos del disruptor hace que sea más fácil para los retardados a los consumidores para ponerse al día rápidamente, lo que reduce la latencia general. Ive descrito las cosas aquí, con uno de cada uno de los diarios, replicador y unmarshaler - esto es lo que hace LMAX. Pero el diseño permitiría que varios de estos componentes se ejecuten. Si usted corrió dos periodistas entonces uno tomaría las ranuras pares y el otro diario tomaría las ranuras impares. Esto permite una mayor concurrencia de estas operaciones de E / S si esto fuera necesario. Las memorias intermedias de anillo son grandes: 20 millones de ranuras para el búfer de entrada y 4 millones de ranuras para cada uno de los búferes de salida. Los contadores de secuencia son enteros de 64 bits de longitud que aumentan monotónicamente incluso cuando la ranura de los anillos se envuelve.14 El búfer se establece en un tamaño que es una potencia de dos para que el compilador pueda realizar una operación de módulo eficiente para mapear desde el número de contador de secuencia al número de ranura . Al igual que el resto del sistema, los disruptores se rebotan durante la noche. Este rebote se hace principalmente para limpiar la memoria de modo que haya menos posibilidades de un costoso evento de recolección de basura durante el comercio. (También creo que es un buen hábito para reiniciar regularmente, de modo que ensayar cómo hacerlo para emergencias.) El trabajo periodistas es almacenar todos los eventos en una forma duradera, para que puedan ser reproducidos en caso de que algo salga mal. LMAX no utiliza una base de datos para esto, sólo el sistema de archivos. Transmiten los eventos al disco. En términos modernos, los discos mecánicos son horriblemente lentos para el acceso aleatorio, pero muy rápidos para la transmisión - por lo tanto, el disco de la línea de etiqueta es la nueva cinta.15 Anteriormente mencioné que LMAX ejecuta varias copias de su sistema en un clúster para admitir una conmutación rápida . El replicador mantiene estos nodos sincronizados. Todas las comunicaciones en LMAX utilizan la multidifusión IP, por lo que los clientes no necesitan saber qué dirección IP es el nodo maestro. Sólo el nodo maestro escucha directamente los eventos de entrada y ejecuta un replicador. El replicador transmite los eventos de entrada a los nodos esclavos. En caso de que el nodo maestro se caiga, se notará su falta de latido, otro nodo se convertirá en maestro, comenzará a procesar eventos de entrada e iniciará su replicador. Cada nodo tiene su propio disruptor de entrada y por lo tanto tiene su propio diario y hace su propio desmarque. Incluso con la multidifusión IP, la replicación todavía es necesaria porque los mensajes IP pueden llegar en un orden diferente en diferentes nodos. El nodo maestro proporciona una secuencia determinística para el resto del procesamiento. El unmarshaler convierte los datos del evento del cable en un objeto java que se puede utilizar para invocar el comportamiento en el procesador de lógica de negocios. Por lo tanto, a diferencia de los otros consumidores, tiene que modificar los datos en el búfer de anillo para que pueda almacenar este objeto unmarshaled. La regla aquí es que los consumidores están autorizados a escribir en el búfer de anillo, pero cada campo de escritura sólo puede tener un consumidor paralelo que se le permite escribir en él. Esto conserva el principio de tener solamente un solo escritor. 16 Figura 4: La arquitectura LMAX con los disruptores expandidos El disruptor es un componente de uso general que se puede utilizar fuera del sistema LMAX. Por lo general, las compañías financieras son muy reservadas sobre sus sistemas, manteniendo tranquilos incluso sobre los artículos que no están relacionados con su negocio. No sólo ha LMAX abierto sobre su arquitectura general, han abierto-sourced el código de disruptor - un acto que me hace muy feliz. Not just will this allow other organizations to make use of the disruptor, it will also allow for more testing of its concurrency properties. Queues and their lack of mechanical sympathy The LMAX architecture caught peoples attention because its a very different way of approaching a high performance system to what most people are thinking about. So far Ive talked about how it works, but havent delved too much into why it was developed this way. This tale is interesting in itself, because this architecture didnt just appear. It took a long time of trying more conventional alternatives, and realizing where they were flawed, before the team settled on this one. Most business systems these days have a core architecture that relies on multiple active sessions coordinated through a transactional database. The LMAX team were familiar with this approach, and confident that it wouldnt work for LMAX. This assessment was founded in the experiences of Betfair - the parent company who set up LMAX. Betfair is a betting site that allows people to bet on sporting events. It handles very high volumes of traffic with a lot of contention - sports bets tend to burst around particular events. To make this work they have one of the hottest database installations around and have had to do many unnatural acts in order to make it work. Based on this experience they knew how difficult it was to maintain Betfairs performance and were sure that this kind of architecture would not work for the very low latency that a trading site would require. As a result they had to find a different approach. Their initial approach was to follow what so many are saying these days - that to get high performance you need to use explicit concurrency. For this scenario, this means allowing orders to be processed by multiple threads in parallel. However, as is often the case with concurrency, the difficulty comes because these threads have to communicate with each other. Processing an order changes market conditions and these conditions need to be communicated. The approach they explored early on was the Actor model and its cousin SEDA. The Actor model relies on independent, active objects with their own thread that communicate with each other via queues. Many people find this kind of concurrency model much easier to deal with than trying to do something based on locking primitives. The team built a prototype exchange using the actor model and did performance tests on it. What they found was that the processors spent more time managing queues than doing the real logic of the application. Queue access was a bottleneck. When pushing performance like this, it starts to become important to take account of the way modern hardware is constructed. The phrase Martin Thompson likes to use is mechanical sympathy. The term comes from race car driving and it reflects the driver having an innate feel for the car, so they are able to feel how to get the best out of it. Many programmers, and I confess I fall into this camp, dont have much mechanical sympathy for how programming interacts with hardware. Whats worse is that many programmers think they have mechanical sympathy, but its built on notions of how hardware used to work that are now many years out of date. One of the dominant factors with modern CPUs that affects latency, is how the CPU interacts with memory. These days going to main memory is a very slow operation in CPU-terms. CPUs have multiple levels of cache, each of which of is significantly faster. So to increase speed you want to get your code and data in those caches. At one level, the actor model helps here. You can think of an actor as its own object that clusters code and data, which is a natural unit for caching. But actors need to communicate, which they do through queues - and the LMAX team observed that its the queues that interfere with caching. The explanation runs like this: in order to put some data on a queue, you need to write to that queue. Similarly, to take data off the queue, you need to write to the queue to perform the removal. This is write contention - more than one client may need to write to the same data structure. To deal with the write contention a queue often uses locks. But if a lock is used, that can cause a context switch to the kernel. When this happens the processor involved is likely to lose the data in its caches. The conclusion they came to was that to get the best caching behavior, you need a design that has only one core writing to any memory location17. Multiple readers are fine, processors often use special high-speed links between their caches. But queues fail the one-writer principle. This analysis led the LMAX team to a couple of conclusions. Firstly it led to the design of the disruptor, which determinedly follows the single-writer constraint. Secondly it led to idea of exploring the single-threaded business logic approach, asking the question of how fast a single thread can go if its freed of concurrency management. The essence of working on a single thread, is to ensure that you have one thread running on one core, the caches warm up, and as much memory access as possible goes to the caches rather than to main memory. This means that both the code and the working set of data needs to be as consistently accessed as possible. Also keeping small objects with code and data together allows them to be swapped between the caches as a unit, simplifying the cache management and again improving performance. An essential part of the path to the LMAX architecture was the use of performance testing. The consideration and abandonment of an actor-based approach came from building and performance testing a prototype. Similarly much of the steps in improving the performance of the various components were enabled by performance tests. Mechanical sympathy is very valuable - it helps to form hypotheses about what improvements you can make, and guides you to forward steps rather than backward ones - but in the end its the testing gives you the convincing evidence. Performance testing in this style, however, is not a well-understood topic. Regularly the LMAX team stresses that coming up with meaningful performance tests is often harder than developing the production code. Again mechanical sympathy is important to developing the right tests. Testing a low level concurrency component is meaningless unless you take into account the caching behavior of the CPU. One particular lesson is the importance of writing tests against null components to ensure the performance test is fast enough to really measure what real components are doing. Writing fast test code is no easier than writing fast production code and its too easy to get false results because the test isnt as fast as the component its trying to measure. Should you use this architecture At first glance, this architecture appears to be for a very small niche. After all the driver that led to it was to be able to run lots of complex transactions with very low latency - most applications dont need to run at 6 million TPS. But the thing that fascinates me about this application, is that they have ended up with a design which removes much of the programming complexity that plagues many software projects. The traditional model of concurrent sessions surrounding a transactional database isnt free of hassles. Theres usually a non-trivial effort that goes into the relationship with the database. Object/relational mapping tools can help much of the pain of dealing with a database, but it doesnt deal with it all. Most performance tuning of enterprise applications involves futzing around with SQL. These days, you can get more main memory into your servers than us old guys could get as disk space. More and more applications are quite capable of putting all their working set in main memory - thus eliminating a source of both complexity and sluggishness. Event Sourcing provides a way to solve the durability problem for an in-memory system, running everything in a single thread solves the concurrency issue. The LMAX experience suggests that as long as you need less than a few million TPS, youll have enough performance headroom. There is a considerable overlap here with the growing interest in CQRS. An event sourced, in-memory processor is a natural choice for the command-side of a CQRS system. (Although the LMAX team does not currently use CQRS.) So what indicates you shouldnt go down this path This is always a tricky questions for little-known techniques like this, since the profession needs more time to explore its boundaries. A starting point, however, is to think of the characteristics that encourage the architecture. One characteristic is that this is a connected domain where processing one transaction always has the potential to change how following ones are processed. With transactions that are more independent of each other, theres less need to coordinate, so using separate processors running in parallel becomes more attractive. LMAX concentrates on figuring the consequences of how events change the world. Many sites are more about taking an existing store of information and rendering various combinations of that information to as many eyeballs as they can find - eg think of any media site. Here the architectural challenge often centers on getting your caches right. Another characteristic of LMAX is that this is a backend system, so its reasonable to consider how applicable it would be for something acting in an interactive mode. Increasingly web application are helping us get used to server systems that react to requests, an aspect that does fit in well with this architecture. Where this architecture goes further than most such systems is its absolute use of asynchronous communications, resulting in the changes to the programming model that I outlined earlier. These changes will take some getting used to for most teams. Most people tend to think of programming in synchronous terms and are not used to dealing with asynchrony. Yet its long been true that asynchronous communication is an essential tool for responsiveness. It will be interesting to see if the wider use of asynchronous communication in the javascript world, with AJAX and node. js, will encourage more people to investigate this style. The LMAX team found that while it took a bit of time to adjust to asynchronous style, it soon became natural and often easier. In particular error handling was much easier to deal with under this approach. The LMAX team certainly feels that the days of the coordinating transactional database are numbered. The fact that you can write software more easily using this kind of architecture and that it runs more quickly removes much of the justification for the traditional central database. For my part, I find this a very exciting story. Much of my goal is to concentrate on software that models complex domains. An architecture like this provides good separation of concerns, allowing people to focus on Domain-Driven Design and keeping much of the platform complexity well separated. The close coupling between domain objects and databases has always been an irritation - approaches like this suggest a way out. if you found this article useful, please share it. I appreciate the feedback and encouragementDissecting the Disruptor: Whats so special about a ring buffer Recently we open sourced the LMAX Disruptor. the key to what makes our exchange so fast. Why did we open source it Well, weve realised that conventional wisdom around high performance programming is. a bit wrong. Weve come up with a better, faster way to share data between threads, and it would be selfish not to share it with the world. Plus it makes us look dead clever. On the site you can download a technical article explaining what the Disruptor is and why its so clever and fast. I even get a writing credit on it, which is gratifying when all I really did is insert commas and re-phrase sentences I didnt understand. However I find the whole thing a bit much to digest all at once, so Im going to explain it in smaller pieces, as suits my NADD audience. First up - the ring buffer. Initially I was under the impression the Disruptor was just the ring buffer. But Ive come to realise that while this data structure is at the heart of the pattern, the clever bit about the Disruptor is controlling access to it. What on earth is a ring buffer Well, it does what it says on the tin - its a ring (its circular and wraps), and you use it as a buffer to pass stuff from one context (one thread) to another: (OK, I drew it in Paint. Im experimenting with sketch styles and hoping my OCD doesnt kick in and demand perfect circles and straight lines at precise angles). So basically its an array with a pointer to the next available slot. As you keep filling up the buffer (and presumable reading from it too), the sequence keeps incrementing, wrapping around the ring: To find the slot in the array that the current sequence points to you use a mod operation: sequence mod array length array index So for the above ring buffer (using Java mod syntax): 12 10 2. Easy. Actually it was a total accident that the picture had ten slots. Powers of two work better because computers think in binary. So what If you look at Wikipedias entry on Circular Buffers. youll see one major difference to the way weve implemented ours - we dont have a pointer to the end. We only have the next available sequence number. This is deliberate - the original reason we chose a ring buffer was so we could support reliable messaging. We needed a store of the messages the service had sent, so when another service sent a nak to say they hadnt received some messages, it would be able to resend them. The ring buffer seems ideal for this. It stores the sequence to show where the end of the buffer is, and if it gets a nak it can replay everything from that point to the current sequence: The difference between the ring buffer as weve implemented it, and the queues we had traditionally been using, is that we dont consume the items in the buffer - they stay there until they get over-written. Which is why we dont need the end pointer you see in the Wikipedia version. Deciding whether its OK to wrap or not is managed outside of the data structure itself (this is part of the producer and consumer behaviour - if you cant wait for me to get round to blogging about it, check out the Disruptor site ). And its so great because. So we use this data structure because it gives us some nice behaviour for reliable messaging. It turns out though that it has some other nice characteristics. Firstly, its faster than something like a linked list because its an array, and has a predictable pattern of access. This is nice and CPU-cache-friendly - at the hardware level the entries can be pre-loaded, so the machine is not constantly going back to main memory to load the next item in the ring. Secondly, its an array and you can pre-allocate it up front, making the objects effectively immortal. This means the garbage collector has pretty much nothing to do here. Again, unlike a linked list which creates objects for every item added to the list - these then all need to be cleaned up when the item is no longer in the list. The missing pieces I havent talked about how to prevent the ring wrapping, or specifics around how to write stuff to and read things from the ring buffer. Youll also notice Ive been comparing it to a data structure like a linked list, which I dont think anyone believes is the answer to the worlds problems. The interesting part comes when you compare the Disruptor with an implementation like a queue. Queues usually take care of all the stuff like the start and end of the queue, adding and consuming items, and so forth. All the stuff I havent really touched on with the ring buffer. Thats because the ring buffer itself isnt responsible for these things, weve moved these concerns outside of the data structure. For more details youre just going to have to read the paper or check out the code. Or watch Mike and Martin at QCon San Francisco last year. Or wait for me to have a spare five minutes to get my head around the rest of it. If you don39t consume elements from your ring buffer then you39re keeping them reachable and preventing them from being deallocated. This can obviously have an adverse effect on the throughput and latency of the garbage collector. Writing references into different locations in your ring buffer incurs the write barrier, which can also adversely affect throughput and latency. I wonder what the trade-offs are concerning these disadvantages and when they come into play. With regards to use of memory, no real trade offs are made by the Disruptor. Unlike a queue, you have a choice about how to make use of memory. If solution is a soft real-time system, reducing GC pauses is paramount. Therefore you can re-use the entries in the ring buffer, e. g. copying byte arrays to and from network I/O buffers in and out of the ring buffer (our most common usage pattern). As the amount of memory used by the system remains static is reduces the frequency of garbage collection. It is also possible to implement an Entry that contains a reference to an immutable object. However in that situation it may be necessary for the consumer to null out the message object to reduce the amount of memory that needs to be promoted from Eden. So a little more effort is required from the programmer to build the most appropriate solution. We believe that the flexibility provided justifies this small bit of extra effort. Considering the write barrier, the primary goal of the Disruptor is to pass messages between threads. We make no trade offs regarding ordering or consistency, therefore it is necessary to use memory barriers in the appropriate places. We39ve done our utmost to keep this to a minimum. However, we are many times faster than the popular alternatives as most of them use locks provide consistency. How does this approach compare to the Pool approach and other approaches used here: cacm. acm. org/magazines/2011/3/105308-data-structures-in-the-multicore-age/fulltext Why not use a Pool instead of a queue Is the LIFO requirement essential Unfortunately I can39t read that article because I don39t have an account at that site. FIFO (not LIFO) is absolutely essential - our exchange depends upon predictable ordering, and if you play the same events into it you will always get the same outcome. The Disruptor ensures this ordering without taking the performance penalties usually associated with FIFO structures. Flying Frog Consultancy said quotIf you don39t consume elements from your ring buffer then you39re keeping them reachable and preventing them from being deallocated. This can obviously have an adverse effect on the throughput and latency of the garbage collector. quot The whole point is to not to invoke the garbage collector. The Disruptor pattern allows data to be passed between CPU39s at pretty much the theoretical maximum of the hardware - its been well thought out I39m new to Disruptor pattern. I have a very basic question. How do I add messages to the Ring Buffer from a Multi Threaded Producer. Should the add calls to the Ring Buffer be synchronized Generally the aim is not to run anything multi-threaded. Producers and consumers should be single-threaded. But you can have more than one producer: mechanitis. blogspot/2011/07/dissecting-disruptor-writing-to-ring. html - this is a slightly out of date post, the naming conventions have changed and the producer barrier is now managed by the ring buffer, but I think this might be a good place to start to think about how to solve your problem. Thanks for interesting the article. I am not sure if I understand, but the concept of holding on to memory and reusing already allocated objects to avoid GC pauses does not seem to be new. How is the ring buffer different from an object pool Avoiding GC is not the main aim of the RingBuffer, although it does help towards the speed of the Disruptor. The interesting characteristics of the RingBuffer are that it39s FIFO, and it enables some really nice batching when you read from it. The RingBuffer is not the secret sauce in the Disruptor39s performance, in fact in the current version of the Disruptor you don39t need it at all. It39s worth noting that there39s nothing new in the Disruptor at all, in fact many of the ideas have been around for years. But I don39t think there are any other frameworks in Java that pull together these concepts in this way to give the kind of performance we see when using the Disruptor. Hi Trisha, From a few days I discovered the LMAX architecture and disruptor also, It39s not so clearly to my how exactly the consumers extract the messages from RingBuffer and how exactly a consumer, for example C1, know which messages are for it and not for other consumer, C2. Thanks Sorin. Actually the messages are for both consumers. The default behaviour is that all consumers (or EventHandlers as they are now) read all messages in the RingBuffer. If you have different types of events that are handled by different consumers, then it39s up to the consumer to decide whether to ignore the event or not. So, if C1 handles all blue messages and C2 handles all red ones (over simplification of course) then C1 needs to check it39s a blue message before proceeding. In terms of extracting the messages - you don39t. Messages live on the ring buffer to be read by (and processed by) all consumers, until every consumer has done what it needs to do with it (i. e. every consumer has incremented its sequence number to at least that message39s number) then it will get over-written when the ring wraps. If you want to do something with that message, then you simply read it and do whatever you want with it, even if that39s passing it on to another Disruptor or another part of the system. Hi Trisha, Thanks for this and other presentations. I have a question regarding the disruptor which is rather basic. The consumers (event processors) are not implementing any of the Callable or Runnable interfaces they implement EventHandler, Then how can they run in parallel, so for example I have a disruptor implementation where there is a diamond pattern like this P1 - c1,c2,c3 - c4 - c5 Where c1 to c3 can work in parallel after p1, and C4 and C5 work after them. So conventionally I39d have something like this (with P1 and C1-C5 being runnables/callables) But in case of the Disruptor none of my event handlers implement Runnable or Callable, so how39d the disruptor framework end up running them in parallel Take following sceanrio: My consumer C2 requires to make a webservice call for some annotation to the Event, In SEDA I can startup 10 threads for such 10 C2 requests for pulling the message out of queue make Webservice Call and update the next SEDA Queue and that will make sure that I don39t sequentially wait for a web service response for each of the 10 requests where as in this case my eventprocessor C2 (if) being the single instance would wait sequentially for 10 C2 requests. In Java, creating an array of Java objects does not allocate memory for the objects. It only allocates memory for references to the objects. How does an array of object references help improve CPU caching efficiency because the actual objects are still scattered in the heap You39re absolutely right, which is why in the LMAX case we have an array of byte arrays, not an array of objects - at least for the high performance instances of the disruptor. An array of object references is still valuable in a lot of cases, but as you say it doesn39t necessarily give you the cache line affinity. This has come up several times in the Google Group discussions (groups. google/forum/forum/lmax-disruptor), I think you39ll find more detailed discussion there. I know I am /very/ late, but an array of byte arrays is by definition an array of objects. Bidimensional byte arrays don39t guarantee locality, especially after a GC pass (the single dimensional arrays that make up the bidimensional one are objects in the heap, so they get moved around). Locality inside the unidimensional byte arrays might be preserved, but not in the whole bidimensional array (i. e. it39s preserve intra-array, lost inter-array). I39ve just spend a good part of my day going through your disruptor and I can see from your example the hundreds of millions of ops per second and also from the presentation 6 million trades per second. I39ve just written an example with the producer retrieving the operation request from a webservice with 2 consumers one for marshaling and the other for business logic and my throughput is just over 1000 ops per second source (github/ejosiah/activemq-vs-distruptor) My question does any of your metrics include I/O operations from other consumers such as those for (Journalling, replication, serialization, etc) No, the metrics quoted are for the business logic only, not for I/O etc. I think if you check the Google Group history you39ll find more specific information about what was measured and how, this question has definitely come up before:

No comments:

Post a Comment