En la empresa siempre nos esforzamos por aumentar la capacidad de mantenimiento de nuestro código, utilizando prácticas generalmente aceptadas, incluso en asuntos de subprocesamiento múltiple. Esto no resuelve todas las dificultades que conlleva una carga cada vez mayor, pero simplifica el soporte: también gana legibilidad de código y la velocidad de desarrollo de nuevas funciones.
Ahora tenemos 47,000 usuarios diarios, alrededor de 30 servidores en producción, 2,000 solicitudes de API por segundo y lanzamientos diarios. El servicio Miro se ha desarrollado desde 2011, y en la implementación actual, las solicitudes de los usuarios son procesadas en paralelo por un grupo de servidores heterogéneos.

Subsistema de control de acceso competitivo
El valor principal de nuestro producto son las placas colaborativas de usuarios, por lo que la carga principal recae sobre ellas. El subsistema principal que controla la mayor parte del acceso competitivo es el sistema con estado de sesiones de usuario en el tablero.
Para cada placa que se puede abrir en uno de los servidores, el estado aumenta. Almacena los datos de tiempo de ejecución de la aplicación necesarios para garantizar la colaboración y la visualización del contenido, así como los datos del sistema, como la vinculación a subprocesos de procesamiento. La información sobre en qué servidor se almacena el estado se escribe en una estructura distribuida y es accesible para el clúster mientras el servidor se esté ejecutando, y al menos un usuario esté en el tablero. Utilizamos Hazelcast para proporcionar esta parte del subsistema. Todas las conexiones nuevas a la placa se envían al servidor con este estado.
Cuando se conecta al servidor, el usuario ingresa a la secuencia receptora, cuya única tarea es vincular la conexión al estado de la placa correspondiente, en cuyo flujo se realizará todo el trabajo adicional.
Hay dos transmisiones asociadas a la placa: la red, las conexiones de procesamiento y el "negocio", responsable de la lógica empresarial. Esto le permite transformar la ejecución de tareas heterogéneas de procesamiento de paquetes de red y ejecución de comandos comerciales de serie a paralelo. Los comandos de red procesados de los usuarios forman tareas empresariales aplicadas y los dirigen a la secuencia empresarial, donde se procesan secuencialmente. Esto evita la sincronización innecesaria al desarrollar el código de la aplicación.
La división del código en negocio / aplicación y sistema es nuestra convención interna. Le permite distinguir entre el código responsable de las características y capacidades para los usuarios, de los detalles de bajo nivel de comunicación, eliminación y almacenamiento, que son la herramienta de servicio.
Si la secuencia receptora detecta que no hay estado para la placa, se establece la tarea de inicialización correspondiente. La inicialización de estado es manejada por un tipo separado de hilo.
Los tipos de tareas y su dirección se pueden representar de la siguiente manera:

Dicha implementación nos permite resolver los siguientes problemas:
- No hay lógica de negocios en la secuencia de recepción que pueda ralentizar la nueva conexión. Este tipo de subproceso en el servidor existe en una sola copia, por lo que los retrasos afectarán inmediatamente el tiempo de apertura de las placas, y si hay un error en el código comercial, puede colgarlo fácilmente.
- La inicialización del estado no se realiza en el flujo comercial de las juntas y no afecta el tiempo de procesamiento de los comandos comerciales de los usuarios. Puede llevar algo de tiempo, y los flujos comerciales procesan varias juntas a la vez, por lo que la apertura de nuevas juntas no afecta directamente a las existentes.
- Analizar los comandos de red a menudo es más rápido que ejecutarlos directamente, por lo que la configuración del grupo de subprocesos de red puede ser diferente de la configuración del grupo de subprocesos de negocios para utilizar eficientemente los recursos del sistema.
Coloración de flujo
El subsistema descrito anteriormente en la implementación es bastante no trivial. El desarrollador debe tener en cuenta el esquema del sistema y tener en cuenta el proceso inverso de cierre de paneles. Al cerrar, es necesario eliminar todas las suscripciones, eliminar entradas de los registros y hacer esto en las mismas secuencias en las que se inicializaron.
Notamos que los errores y las complejidades de la modificación del código que surgieron en este subsistema a menudo se asociaron con una falta de comprensión del contexto de ejecución. El malabarismo de hilos y tareas dificultó la respuesta a la pregunta en qué hilo en particular se está ejecutando un código en particular.
Para resolver este problema, utilizamos el método de colorear hilos: esta es una política dirigida a regular el uso de hilos en el sistema. Los colores se asignan a los hilos y los métodos definen el alcance para la ejecución dentro de los hilos. El color aquí es una abstracción, puede ser cualquier entidad, por ejemplo, una enumeración. En Java, las anotaciones pueden servir como lenguaje de marcado de color:
@Color @IncompatibleColors @AnyColor @Grant @Revoke
Se agregan anotaciones al método, utilizándolas puede establecer la validez del método. Por ejemplo, si la anotación de un método permite el amarillo y el rojo, entonces el primer hilo puede llamar al método, y para el segundo, dicha llamada será errónea.

Se pueden especificar colores no válidos:

Puede agregar y eliminar privilegios de subproceso en la dinámica:

La ausencia de anotación o anotación como en el ejemplo a continuación dice que el método se puede ejecutar en cualquier hilo:

Los desarrolladores de Android pueden estar familiarizados con este enfoque para anotaciones MainThread, UiThread, WorkerThread, etc.
La coloración de hilos utiliza el principio del código autodocumentado, y el método en sí mismo se presta bien para el análisis estático. Con el análisis estático, puede decir antes de ejecutar el código que está escrito correctamente o no. Si excluimos las anotaciones Grant y Revoke y asumimos que la secuencia después de la inicialización ya tiene un conjunto de privilegios inmutable, entonces este será un análisis insensible al flujo, una versión simple del análisis estático que no tiene en cuenta el orden de las llamadas.
En el momento de la implementación del método de coloración de flujo, no había soluciones listas para el análisis estático en nuestra infraestructura de desarrollo, por lo que fuimos de la manera más simple y económica: presentamos nuestras anotaciones, que están asociadas de manera única con cada tipo de flujos. Comenzamos a verificar su corrección con la ayuda de aspectos en tiempo de ejecución.
@Aspect public class ThreadAnnotationAspect { @Pointcut("if()") public static boolean isActive() { …
Para los aspectos, utilizamos la biblioteca aspectoj y el complemento maven, que proporciona tejido al compilar el proyecto. El tejido se configuró inicialmente para el tiempo de carga al cargar clases con ClassLoader. Sin embargo, nos enfrentamos al hecho de que el tejedor a veces se comportó incorrectamente al cargar la misma clase de manera competitiva, como resultado de lo cual el código fuente de la clase permaneció sin cambios. Como resultado, esto resultó en un comportamiento de producción muy impredecible y difícil de reproducir. Quizás en las versiones actuales de la biblioteca no haya tal problema.
La solución en aspectos nos permitió encontrar rápidamente la mayoría de los problemas en el código.
Es importante no olvidar mantener siempre las anotaciones actualizadas: se pueden eliminar, agregar pereza, los aspectos de tejido se pueden desactivar por completo; en este caso, la coloración perderá rápidamente su relevancia y valor.
Guardado
Una de las variedades de coloración es la anotación GuardedBy de java.util.concurrent. Delimita el acceso a los campos y métodos, indicando qué bloqueos son necesarios para el acceso correcto.
public class PrivateLock { private final Object lock = Object(); @GuardedBy (“lock”) Widget widget; void method() { synchronized (lock) {
Los IDE modernos incluso admiten el análisis de esta anotación. Por ejemplo, IDEA muestra este mensaje si algo está mal con el código:
El método de colorear hilos no es nuevo, pero parece que en lenguajes como Java, donde el acceso multiproceso a menudo va a objetos mutables, su uso no solo como parte de la documentación, sino también en la etapa de compilación, el ensamblaje podría simplificar enormemente el desarrollo de código multiproceso.
Todavía usamos la implementación en aspectos. Si está familiarizado con una solución más elegante o una herramienta de análisis que le permita aumentar la estabilidad de este enfoque ante los cambios del sistema, por favor compártalo en los comentarios.