© Dragon Ball. GokuEl programador defensor en cualquier momento y en cualquier parte del código espera la aparición de posibles problemas y escribe el código de tal manera que se proteja de ellos por adelantado. Y si el problema no se puede defender, al menos asegúrese de que sus consecuencias e impacto en los usuarios sean mínimos.
Recuerdo el efecto
FlashForward de los éxitos de taquilla de Hollywood cuando el protagonista ve la catástrofe inminente y se mantiene extremadamente tranquilo, porque sabe de antemano que sucederá y tiene protección contra él. La idea detrás de la programación defensiva es protegerse de los problemas que son difíciles o imposibles de prever. Un programador de seguridad espera que se produzcan errores en cualquier parte del sistema y en cualquier momento para evitarlos antes de que causen daños. Sin embargo, el objetivo no es crear un sistema que nunca falle, sigue siendo imposible. El objetivo es crear un sistema que se
bloquee con gracia en caso de
cualquier problema imprevisto.
Comprendamos con más detalle lo que se incluye en el concepto de "caer con gracia".
- Cae rápido. En caso de un error inesperado, todas las operaciones deben completarse de inmediato, especialmente si los cálculos posteriores son difíciles o pueden conducir a la corrupción de datos.
- Caer prolijamente. Si se produce un error, el programa debe liberar todos los recursos, eliminar bloqueos, eliminar archivos temporales y medio grabados, cerrar conexiones. Espere a que se completen las operaciones críticas, cuya interrupción puede conducir a resultados impredecibles. O una forma segura de bloquear estas operaciones.
- Cayendo clara y bellamente. Si algo se rompe, el mensaje de error debe ser simple, conciso y contener detalles importantes del contexto del sistema donde ocurrió el error. Esto ayudará al equipo responsable del sistema a resolver el problema lo más rápido posible y solucionarlo.
Pero puede que tengas una pregunta.
¿Por qué perder el tiempo en los problemas que puedan surgir en el futuro?
Ahora no están allí, el código funciona simplemente perfecto. Además, es posible que nunca ocurran problemas. Después de todo, ¡los profesionales no hacen ingeniería por el bien de la ingeniería (
YAGNI - ¡No la necesitarás!)
Lo principal es el pragmatismo.
Andrew Hunt en el libro "Programador-pragmático" da la siguiente definición de programación defensiva: "
paranoia pragmática ".
Protege tu código de:
- errores propios
- errores de otras personas;
- errores y fallas en otros sistemas con los cuales está integrado;
- errores de hierro, entornos y plataformas en los que funciona su aplicación.
Analicemos varios métodos tácticos y estratégicos de programación defensiva, los cuales crearán un sistema confiable y predecible que sea resistente a fallas arbitrarias.
Algunos consejos pueden parecer "capitanes", pero en la práctica, muchos desarrolladores ni siquiera los siguen. Pero si se adhiere a prácticas y enfoques simples, esto aumentará significativamente la estabilidad de su sistema.
No confíes en nadie
Los datos del usuario no son confiables por defecto. Los usuarios a menudo malinterpretan lo que nos parece obvio (como desarrolladores de sistemas). Espere una entrada incorrecta y siempre verifíquela.
También verifique la cantidad de entrada. Puede ser que el usuario envíe demasiados. Al mismo tiempo, desde el punto de vista de la lógica empresarial, este es el escenario correcto. Pero puede conducir a un procesamiento demasiado largo. ¿Qué se puede hacer con esto? Por ejemplo, ejecútelo de forma asíncrona, si la cantidad de datos de entrada excede un cierto umbral y los detalles de la empresa le permiten procesar los datos en segundo plano.
La configuración de la aplicación (por ejemplo, los archivos de configuración) también está sujeta a la aparición de datos incorrectos en ellos. A menudo, la configuración del programa se almacena en JSON, YAML, XML, INI y otros formatos. Dado que todos estos son archivos de texto, uno debería esperar que tarde o temprano alguien cambie algo en ellos, y su programa comenzará a funcionar incorrectamente. Puede ser un usuario final o alguien de su equipo.
Bases de datos, archivos, almacenamientos centralizados de configuraciones, registro: todas estas personas pueden acceder a todos estos lugares y, tarde o temprano, cambiarán algo allí (
ley de Murphy ).
Entrada de basura → entrada de basura
Las entradas que pasan la validación y comienzan a procesarse deben estar limpias si desea que su código haga exactamente lo que espera de él.
Sin embargo, es una buena práctica hacer verificaciones de validación de datos adicionales, incluso cuando ya han comenzado a procesarse. En lugares críticos (facturación, autorización, datos personales y confidenciales, etc.) esto es casi un requisito obligatorio. Esto es necesario para que, en caso de errores en el código o problemas con el validador de datos de entrada, detenga el flujo de ejecución lo más rápido posible. Es difícil hacer una validación de calidad con la comprobación de todos los posibles escenarios de error, por lo que puede utilizar métodos más simples para validar que el programa todavía se está ejecutando correctamente: afirmaciones y excepciones.
La paranoia saludable es un rasgo característico de todos los desarrolladores profesionales. Pero es muy importante buscar el equilibrio óptimo y comprender cuándo la solución ya es lo suficientemente buena.
Configuraciones separadas alrededor de los entornos.
Una causa común de problemas es la separación insuficiente de configuraciones entre entornos o la ausencia de dicha separación.
Esto puede conducir a muchos problemas, por ejemplo:
- el entorno de prueba comienza a leer y / o escribir datos de producción, bases de datos, colas y otros recursos;
- el entorno de prueba utiliza integraciones y servicios externos con una cuenta de producción;
- mezcla de estadísticas, métricas, errores de diferentes entornos;
- violación de seguridad (desarrolladores, probadores y otros miembros del equipo obtienen acceso a recursos de producción);
- errores difíciles de investigar en la producción (por ejemplo, parte de los mensajes en la cola se pierden debido a que el entorno de prueba comienza a leerlo).
Estos son solo ejemplos, una lista completa de problemas que pueden ser causados por una separación insuficientemente responsable de las configuraciones es casi interminable y depende de los detalles del proyecto.
La separación responsable de los datos de configuración por entorno puede reducir significativamente la probabilidad de que se presente inmediatamente toda una clase de problemas asociados con:
- seguridad
- fiabilidad
- soporte e implementación (los ingenieros de DevOps se lo agradecerán).
Además, es una buena práctica almacenar datos secretos (claves, tokens, contraseñas) en un lugar separado especialmente diseñado para almacenar y procesar secretos. Dichos sistemas cifran datos de manera segura, tienen medios flexibles para administrar los derechos de acceso y también le permiten cambiar rápidamente las claves si se han visto comprometidas. En este caso, no necesita realizar cambios en el código y volver a implementar la aplicación. Esto es especialmente importante para sistemas que trabajan con transacciones financieras, datos confidenciales o personales.
Recuerda el efecto en cascada
Una causa común de la caída de sistemas grandes y complejos es el efecto en cascada. Se produce el colapso o la degradación de la funcionalidad de una de las partes del sistema, y uno por uno los otros subsistemas asociados con él comienzan a fallar. En cascada hasta que todo el sistema se vuelva completamente inaccesible.
Algunos trucos protectores:
- usar tiempos de espera progresivos (exponenciales) con un elemento aleatorio;
- establecer valores razonables para el tiempo de espera de conexión y el tiempo de espera del socket;
- prever un retroceso por adelantado en caso de falla de los servicios individuales. Es mejor degradar temporalmente algunas de las funciones, deshabilitar los servicios por completo, pero no arriesgarse a romper todo el sistema. Pero imagine que en este caso el usuario ve un mensaje comprensible y no aterrador, y el equipo de soporte y desarrollo lo antes posible se entera del problema.
Informar problemas rápidamente
Todos los sistemas fallan. A veces sucede algo extraño en ellos que los creadores esperan "una vez cada 10 años". Las integraciones y las API externas no están disponibles periódicamente o responden incorrectamente. Hacer una reserva para todos estos casos suele ser difícil, largo o simplemente imposible. Anticipe esta situación con anticipación e infórmela lo más rápido posible. Iniciar sesión en el nivel ERROR o en el sistema de monitoreo, por supuesto. Agregar validación adicional al chequeo de salud es aún mejor. Enviar un mensaje desde el código a Slack, Telegram, PagerDuty u otro servicio que notifique instantáneamente a su equipo sobre el problema es ideal.
Pero es importante comprender claramente cuándo tiene sentido enviar mensajes directamente. Solo si hay un error, una situación sospechosa o atípica está asociada con los procesos comerciales y es importante que una persona o grupo de personas en un equipo reciba una notificación lo más rápido posible y pueda responder.
Todos los demás problemas técnicos y desviaciones deben manejarse por medios estándar: monitoreo, alertas, registro.
Caché de uso frecuente y / o datos recientes
Los programas y las personas tienen una cosa en común: tienden a reutilizar datos que a menudo se usan o se encuentran recientemente. En sistemas muy cargados, siempre debe recordar esto y almacenar en caché los datos en los lugares más calientes del sistema.
La estrategia de almacenamiento en caché depende en gran medida de los detalles del proyecto y los datos. Si los datos son mutables, es necesario invalidar la memoria caché. Por lo tanto, considere de antemano cómo lo hará. Y también piense en los riesgos que pueden existir si los datos obsoletos aparecen en la memoria caché, la memoria caché se descompone, etc.
Reemplazar operaciones costosas por operaciones baratas
Trabajar con cadenas es una de las operaciones más comunes en cualquier programa. Y si esto no se hace de manera óptima, puede ser una operación costosa. En diferentes lenguajes de programación, los detalles del trabajo con cadenas pueden variar, pero siempre debe recordarlo.
En aplicaciones grandes con una gran base de código, a menudo se encuentra código escrito hace muchos años que funciona sin errores, pero no es óptimo en términos de rendimiento. A menudo, un cambio banal en la estructura de datos de una matriz / lista a una tabla hash da un gran impulso (incluso si solo está en un lugar local en el código).
A veces puede mejorar el rendimiento reescribiendo el algoritmo para usar operaciones bit a bit. Pero incluso en esos casos raros cuando está justificado, el código es muy complejo. Por lo tanto, al tomar una decisión, tenga en cuenta la legibilidad del código y el hecho de que deberá ser compatible. Lo mismo ocurre con otras optimizaciones difíciles: casi siempre, dicho código se vuelve difícil de leer y muy difícil de mantener. Si aún decide optimizaciones complicadas, no olvide escribir comentarios que describan
lo que desea que haga este código y
por qué está escrito de esa manera.
Al mismo tiempo, la optimización debe tratarse con un pragmatismo saludable:
- si te lleva, como desarrollador, unos segundos o minutos, tiene sentido hacerlo de inmediato;
- si es más, es razonable hacerlo de inmediato solo cuando esté 100% seguro de su necesidad. En todos los demás casos, tiene sentido posponerlo, escribir el código TODO, recopilar más información, consultar con colegas, etc.
La optimización prematura es la raíz de todo mal (Donald Knuth)
Reescribe en un idioma de nivel inferior
Esta es una medida extrema. Los idiomas de bajo nivel son casi siempre más rápidos en comparación con los idiomas de alto nivel. Pero esta solución tiene un precio: desarrollar dicho programa es más largo y más difícil. A veces, al reescribir partes críticas del sistema en un lenguaje de bajo nivel, puede lograr un aumento considerable de la productividad. Pero hay efectos secundarios: por lo general, tales soluciones pierden en plataformas cruzadas y su soporte es más costoso. Por lo tanto, tome una decisión con cuidado.
Solo en el campo no es un guerrero.
En conclusión, me gustaría señalar una cosa más importante, quizás la más importante. Las medidas que consideramos en los párrafos anteriores funcionarán solo si todos los miembros del equipo se adhieren a ellas y todos comprenden quién es responsable de qué y qué se debe hacer en caso de una situación crítica. Después de solucionar el problema, es importante celebrar una reunión (Post Mortem) con todas las personas interesadas y averiguar por qué surgió este problema y qué se puede hacer para evitar que ocurra en el futuro. En muchos casos, se requieren cambios tanto técnicos como de proceso. Con cada nuevo Post Mortem, su sistema será más confiable, el equipo tendrá más experiencia y cohesión, y la entropía en el universo será un poco menor;)
El artículo utiliza parcialmente materiales de Por qué la programación defensiva es la mejor forma de codificación robusta (Ravi Shankar Rajan).