¿Cómo reproduce las condiciones de error y ve lo que sucede mientras se ejecuta la aplicación?
¿Cómo visualiza las interacciones entre las diferentes partes concurrentes de la aplicación?
Según mi experiencia, la respuesta a estos dos aspectos es la siguiente:
Rastreo distribuido
El rastreo distribuido es una tecnología que captura datos de tiempo para cada componente concurrente individual de su sistema y se los presenta en formato gráfico. Las representaciones de ejecuciones concurrentes siempre se entrelazan, lo que le permite ver qué se ejecuta en paralelo y qué no.
El rastreo distribuido debe sus orígenes en (por supuesto) los sistemas distribuidos, que por definición son asíncronos y altamente concurrentes. Un sistema distribuido con rastreo distribuido permite a las personas:
a) identifique cuellos de botella importantes, b) obtenga una representación visual de las 'ejecuciones' ideales de su aplicación, yc) proporcione visibilidad sobre el comportamiento concurrente que se está ejecutando, d) obtenga datos de tiempo que pueden usarse para evaluar las diferencias entre los cambios en su sistema (extremadamente importante si tiene SLA fuertes).
Sin embargo, las consecuencias del rastreo distribuido son:
Agrega sobrecarga a todos sus procesos concurrentes, ya que se traduce en más código para ejecutar y enviar potencialmente a través de una red. En algunos casos, esta sobrecarga es muy significativa, incluso Google solo usa su sistema de seguimiento Dapper en un pequeño subconjunto de todas las solicitudes para no arruinar la experiencia del usuario.
Existen muchas herramientas diferentes, no todas las cuales son interoperables entre sí. Esto está algo mejorado por estándares como OpenTracing, pero no está completamente resuelto.
No le dice nada sobre los recursos compartidos y su estado actual. Es posible que pueda adivinar, según el código de la aplicación y lo que le muestra el gráfico que ve, pero no es una herramienta útil a este respecto.
Las herramientas actuales suponen que tiene memoria y almacenamiento de sobra. El alojamiento de un servidor de series de tiempo puede no ser barato, dependiendo de sus limitaciones.
Software de seguimiento de errores
Me conecto a Sentry arriba principalmente porque es la herramienta más utilizada y por una buena razón: software de seguimiento de errores como Sentry secuestra la ejecución en tiempo de ejecución para reenviar simultáneamente una traza de la pila de los errores encontrados a un servidor central.
El beneficio neto de dicho software dedicado en código concurrente:
- Los errores duplicados no se duplican . En otras palabras, si uno o más sistemas concurrentes encuentran la misma excepción, Sentry incrementará un informe de incidente, pero no enviará dos copias del incidente.
Esto significa que puede averiguar qué sistema concurrente está experimentando qué tipo de error sin tener que pasar por innumerables informes de error simultáneos. Si alguna vez ha sufrido correo no deseado desde un sistema distribuido, ya sabe cómo se siente el infierno.
Incluso puede 'etiquetar' diferentes aspectos de su sistema concurrente (aunque esto supone que no tiene trabajo entrelazado sobre exactamente un hilo, lo que técnicamente no es concurrente de todos modos ya que el hilo simplemente salta entre las tareas de manera eficiente pero aún debe procesar los controladores de eventos hasta completarlo) y ver un desglose de los errores por etiqueta.
- Puede modificar este software de manejo de errores para proporcionar detalles adicionales con sus excepciones de tiempo de ejecución. ¿Qué recursos abiertos tuvo el proceso? ¿Existe un recurso compartido que este proceso tenía? ¿Qué usuario experimentó este problema?
Esto, además de los rastros meticulosos de la pila (y los mapas de origen, si tiene que proporcionar una versión reducida de sus archivos), hace que sea fácil determinar qué va mal una gran parte del tiempo.
- (Específico de Sentry) Puede tener un panel de informes de Sentry separado para las ejecuciones de prueba del sistema, lo que le permite detectar errores en las pruebas.
Las desventajas de dicho software incluyen:
Como todo, agregan a granel. Es posible que no desee dicho sistema en hardware integrado, por ejemplo. Recomiendo encarecidamente realizar una ejecución de prueba de dicho software, comparando una ejecución simple con y sin muestra de más de unos cientos de ejecuciones en una máquina inactiva.
No todos los idiomas son igualmente compatibles, ya que muchos de estos sistemas dependen de la captura implícita de una excepción y no todos los idiomas presentan excepciones robustas. Dicho esto, hay clientes para una gran cantidad de sistemas.
Pueden plantearse como un riesgo de seguridad, ya que muchos de estos sistemas son esencialmente de código cerrado. En tales casos, haga su debida diligencia al investigarlos o, si lo prefiere, haga lo suyo.
Es posible que no siempre le brinden la información que necesita. Esto es un riesgo con todos los intentos de agregar visibilidad.
La mayoría de estos servicios fueron diseñados para aplicaciones web altamente concurrentes, por lo que no todas las herramientas pueden ser perfectas para su caso de uso.
En resumen : tener visibilidad es la parte más crucial de cualquier sistema concurrente. Los dos métodos que describo anteriormente, junto con paneles de control dedicados sobre hardware y datos para obtener una imagen holística del sistema en cualquier punto de tiempo dado, son ampliamente utilizados en toda la industria precisamente para abordar ese aspecto.
Algunas sugerencias adicionales
He pasado más tiempo del que me importa para arreglar el código de personas que intentaron resolver problemas concurrentes de manera terrible. Cada vez, he encontrado casos en los que las siguientes cosas podrían mejorar en gran medida la experiencia del desarrollador (que es tan importante como la experiencia del usuario):
Confíe en los tipos . La escritura existe para validar su código, y se puede usar en tiempo de ejecución como protección adicional. Donde no exista la escritura, confíe en aserciones y un controlador de errores adecuado para detectar errores. El código concurrente requiere un código defensivo, y los tipos sirven como el mejor tipo de validación disponible.
- Pruebe los enlaces entre los componentes del código , no solo el componente en sí. No confunda esto con una prueba de integración completa, que prueba cada enlace entre cada componente, e incluso entonces solo busca una validación global del estado final. Esta es una forma terrible de detectar errores.
Una buena prueba de enlace verifica si, cuando un componente habla con otro componente de forma aislada , el mensaje recibido y el mensaje enviado son los mismos que usted espera. Si tiene dos o más componentes que dependen de un servicio compartido para comunicarse, hágalos girar, haga que intercambien mensajes a través del servicio central y vea si todos obtienen lo que espera al final.
Romper las pruebas que involucran una gran cantidad de componentes en una prueba de los componentes mismos y una prueba de cómo cada uno de los componentes también se comunica le brinda una mayor confianza en la validez de su código. Tener un cuerpo de pruebas tan riguroso le permite hacer cumplir los contratos entre servicios, así como detectar errores inesperados que ocurren cuando se ejecutan a la vez.
- Use los algoritmos correctos para validar el estado de su aplicación. Estoy hablando de cosas simples, como cuando tienes un proceso maestro esperando a que todos sus trabajadores terminen una tarea y solo quieres pasar al siguiente paso si todos los trabajadores están completamente hechos; este es un ejemplo de detección global terminación, para lo cual existen metodologías conocidas como el algoritmo de Safra.
Algunas de estas herramientas vienen con idiomas: Rust, por ejemplo, garantiza que su código no tendrá condiciones de carrera en tiempo de compilación, mientras que Go presenta un detector de punto muerto incorporado que también se ejecuta en tiempo de compilación. Si puede detectar problemas antes de que lleguen a la producción, siempre es una victoria.
Una regla general: diseño para fallas en sistemas concurrentes . Anticipe que los servicios comunes colapsarán o se romperán. Esto va incluso para el código que no se distribuye entre las máquinas: el código concurrente en una sola máquina puede depender de dependencias externas (como un archivo de registro compartido, un servidor Redis, un maldito servidor MySQL) que podrían desaparecer o eliminarse en cualquier momento .
La mejor manera de hacer esto es validar el estado de la aplicación de vez en cuando: tener controles de salud para cada servicio y asegurarse de que los consumidores de ese servicio sean notificados de un mal estado. Las herramientas de contenedores modernas como Docker lo hacen bastante bien, y deberían usarse para hacer sandbox.
¿Cómo calculas qué se puede hacer concurrente y qué se puede hacer secuencial?
Una de las lecciones más importantes que he aprendido trabajando en un sistema altamente concurrente es esta: nunca puedes tener suficientes métricas . Las métricas deberían controlar absolutamente todo en su aplicación: no es un ingeniero si no está midiendo todo.
Sin métricas, no puede hacer algunas cosas muy importantes:
Evaluar la diferencia hecha por los cambios en el sistema. Si no sabe si la perilla de ajuste A hizo que la métrica B subiera y la métrica C bajara, no sabrá cómo arreglar su sistema cuando las personas empujan código inesperadamente maligno en su sistema (y empujarán el código a su sistema) .
Comprenda lo que debe hacer a continuación para mejorar las cosas. Hasta que sepa que las aplicaciones se están quedando sin memoria, no puede discernir si debería obtener más memoria o comprar más disco para sus servidores.
Las métricas son tan cruciales y esenciales que he hecho un esfuerzo consciente para planificar lo que quiero medir antes de pensar en lo que requerirá un sistema. De hecho, las métricas son tan cruciales que creo que son la respuesta correcta a esta pregunta: solo se sabe qué se puede hacer secuencial o concurrente cuando se mide lo que están haciendo los bits en su programa. El diseño adecuado utiliza números, no conjeturas.
Dicho esto, ciertamente hay algunas reglas generales:
Secuencial implica dependencia. Dos procesos deben ser secuenciales si uno depende del otro de alguna manera. Los procesos sin dependencias deben ser concurrentes. Sin embargo, planifique una forma de manejar las fallas ascendentes que no impidan que los procesos posteriores esperen indefinidamente.
Nunca mezcle una tarea vinculada de E / S con una tarea vinculada a la CPU en el mismo núcleo. No (por ejemplo) escriba un rastreador web que inicie diez solicitudes simultáneas en el mismo hilo, las raspe tan pronto como entren y espere escalar a quinientas: las solicitudes de E / S van a una cola en paralelo, pero la CPU aún los revisará en serie. (Este modelo impulsado por eventos de un solo subproceso es popular, pero es limitado debido a este aspecto; en lugar de comprender esto, la gente simplemente se retuerce las manos y dice que Node no se escala, para darle un ejemplo).
Un solo hilo puede hacer mucho trabajo de E / S. Pero para usar completamente la simultaneidad de su hardware, use grupos de subprocesos que juntos ocupen todos los núcleos. En el ejemplo anterior, el lanzamiento de cinco procesos Python (cada uno de los cuales puede usar un núcleo en una máquina de seis núcleos) solo para el trabajo de la CPU y un sexto hilo Python solo para el trabajo de E / S se escalará mucho más rápido de lo que piensa.
La única forma de aprovechar la concurrencia de la CPU es a través de un conjunto de subprocesos dedicado. Un solo subproceso suele ser lo suficientemente bueno para un montón de trabajo de E / S. Esta es la razón por la cual los servidores web controlados por eventos como Nginx escalan mejor (hacen un trabajo limitado de E / S) que Apache (que combina el trabajo vinculado de E / S con algo que requiere CPU y lanza un proceso por solicitud), pero por qué usar Node para realizar Decenas de miles de cálculos de GPU recibidos en paralelo es una idea terrible .