Acerca de los errores que aparecen de la nada y en los que no hay nadie a quien culpar: el fenómeno de la falta de responsabilidad

Mikher multimedia

El artículo no hablará sobre empleados irresponsables, como se podría sugerir por el título del artículo. Discutiremos un peligro técnico real que puede esperar si crea sistemas distribuidos.

En un sistema Enterprise, había un componente. Este componente recopila datos de los usuarios sobre un determinado producto y los registra en un banco de datos. Y constaba de tres partes estándar: la interfaz de usuario, la lógica de negocios en el servidor y las tablas en la base de datos.

El componente funcionó bien y durante varios años nadie tocó su código.

Pero una vez, sin razón, cosas extrañas comenzaron a sucederle al componente.

Al trabajar con algunos usuarios, un componente en medio de una sesión de repente comenzó a arrojar errores. Sucedió con poca frecuencia, pero como de costumbre, en el momento más inoportuno. Y lo que es más incomprensible, los primeros errores aparecieron en una versión estable del sistema en producción. En la versión en la que durante varios meses no se cambió ningún componente.

Comenzamos a analizar la situación y verificamos el componente bajo una carga pesada. Funciona bien Repetidas pruebas de integración bastante extensas. En las pruebas de integración, nuestro componente funcionó bien.

En una palabra, el error no quedó claro cuándo ni dónde.

Comenzaron a cavar más profundo. Un análisis detallado y una comparación de los archivos de registro mostraron que la causa de los mensajes de error que se muestran al usuario es la violación de restricciones en la clave primaria en la tabla ya mencionada en la base de datos.

El componente escribió datos en la tabla usando Hibernate, y a veces Hibernate, cuando intentaba escribir la siguiente fila, informaba una violación de restricción.

No aburriré a los lectores con más detalles técnicos e inmediatamente le contaré sobre la esencia del error. Resultó que no solo nuestro componente escribe en la tabla anterior, sino que a veces (extremadamente raramente) algún otro componente. Y lo hace de manera muy simple, con una simple instrucción SQL INSERT. Un Hibernate funciona de manera predeterminada cuando se escribe de la siguiente manera. Para optimizar el proceso de escritura, consulta el índice de la siguiente clave principal una vez en el índice, y luego escribe varias veces simplemente aumentando el valor de la clave (10 veces por defecto). Y si sucedió que después de la solicitud, el segundo componente se atascó en el proceso y escribió datos en la tabla usando el siguiente valor de clave primaria, entonces el intento posterior de escribir desde Hibernate condujo a una violación de la restricción.
Si está interesado en detalles técnicos, véalos a continuación.

Detalles técnicos
.
El código de clase comenzó así:

@Entity @Table(name="PRODUCT_XXX") public class ProductXXX {                               @Id                @Basic(optional=false)                @Column(                                name="PROD_ID",                                columnDefinition="integer not null",                                insertable=true,                                updatable=false)                @SequenceGenerator(                                name="GEN_PROD_ID",                                sequenceName="SEQ_PROD_ID",                                allocationSize=10)                @GeneratedValue(                                strategy=GenerationType.SEQUENCE,                                generator="GEN_PROD_ID")                private long prodId; 

Una discusión sobre un problema similar en Stackoverflow:
https://stackoverflow.com/questions/12745751/hibernate-sequencegenerator-and-allocationsize

Y sucedió que durante muchos meses después de cambiar el segundo componente e implementar las entradas en la tabla, los procesos de escritura del primer y segundo componente nunca se superponen en el tiempo. Y comenzaron a cruzarse cuando, en una de las unidades que usaban el sistema, el horario de trabajo cambió ligeramente.

Bueno, las pruebas de integración se realizaron sin problemas, ya que los intervalos de tiempo para probar ambos componentes dentro de las pruebas de integración tampoco se entrecruzaron.

En cierto modo, podemos decir que nadie fue realmente el culpable del error.

¿O no es así?

Observaciones y pensamientos


Después de descubrir la verdadera causa del error, se corrigió.

Pero no con este final feliz, me gustaría terminar este artículo, pero reflexionar sobre este error como representante de la amplia categoría de errores que han ganado popularidad después de la transición de sistemas monolíticos a sistemas distribuidos.

Desde el punto de vista de los componentes o servicios individuales en el sistema Enterprise descrito, todo se hizo, todo parece estar bien. Todos los componentes o servicios tenían ciclos de vida independientes. Y cuando surgió la necesidad de escribir en la tabla en el segundo componente, debido a la insignificancia de la operación, se tomó la decisión pragmática de implementar esto directamente en este componente de la manera más simple, y no tocar el primer componente estable de trabajo.

Pero, por desgracia, sucedió lo que sucedió a menudo en sistemas distribuidos (y relativamente menos a menudo en sistemas monolíticos): la responsabilidad de realizar operaciones en un objeto específico se extendió entre los subsistemas. Seguramente, si ambas operaciones de escritura se implementaran en el mismo microservicio, se elegiría una sola tecnología para su implementación. Y entonces el error descrito no habría ocurrido.

Los sistemas distribuidos, especialmente el concepto de microservicios, han ayudado efectivamente a resolver una serie de problemas inherentes a los sistemas monolíticos. Sin embargo, paradójicamente, la separación de responsabilidades para los servicios individuales provoca el efecto contrario. Los componentes ahora "viven" lo más independientes posible. E inevitablemente hay una tentación, hacer grandes cambios en un componente, para "atornillar aquí" una pequeña funcionalidad que se implementaría mejor en otro componente. Esto logra rápidamente el efecto final, reduce el volumen de aprobaciones y pruebas. Entonces, de un cambio a otro, los componentes están cubiertos de características inusuales para ellos, los mismos algoritmos internos y funciones están duplicados, surge la multivariancia de la resolución de problemas (y a veces su no determinismo). En otras palabras, un sistema distribuido se degrada con el tiempo, pero de manera diferente que uno monolítico.

La responsabilidad "difusa" de los componentes en sistemas grandes que consisten en muchos servicios es uno de los problemas típicos y dolorosos de los sistemas distribuidos modernos. La situación se complica aún más y se confunde con los subsistemas de optimización compartidos, como el almacenamiento en caché, la predicción de las siguientes operaciones (predicción), así como la orquestación de servicios, etc.

Centralizando el acceso a la base de datos, al menos a nivel de una sola biblioteca, el requisito es bastante obvio. Sin embargo, muchos sistemas distribuidos modernos han crecido históricamente en torno a bases de datos y utilizan los datos almacenados en ellas directamente (a través de SQL) en lugar de a través de servicios de acceso.

"Ayudando" a la difusión de la responsabilidad y los marcos y bibliotecas ORM como Hibernate. Utilizándolos, muchos desarrolladores de servicios de acceso a bases de datos, sin darse cuenta, desean entregar la mayor cantidad posible de objetos como resultado de la solicitud. Un ejemplo típico es la solicitud de datos del usuario para mostrarlos en un saludo o en el campo con el resultado de la autenticación. En lugar de devolver el nombre de usuario en forma de tres variables de texto (nombre_medio, nombre_medio, apellido_), dicha solicitud a menudo devuelve un objeto de usuario completo con docenas de atributos y objetos conectados, como la lista de roles del usuario solicitado. Esto, a su vez, complica la lógica de procesar el resultado de la solicitud, genera dependencias innecesarias del manejador en el tipo del objeto devuelto y ... - provoca una falta de responsabilidad debido a la posibilidad de implementar la lógica asociada con el objeto desde el exterior responsable de este objeto de servicio.

Que hacer (Recomendaciones)


Desgraciadamente, la responsabilidad en algunos casos a veces es forzada, y a veces incluso inevitable y justificada.

Sin embargo, si es posible, debe tratar de cumplir con el principio de distribución de responsabilidad entre los componentes. Un componente es una responsabilidad.

Bueno, si es imposible concentrar las operaciones en ciertos objetos estrictamente en un sistema, dicha mancha debe registrarse con mucho cuidado en la documentación de todo el sistema ("supercomponente") como la dependencia específica de los componentes en el elemento de datos, en el objeto de dominio o entre sí.

Sería interesante conocer su opinión sobre este asunto, así como los casos de la práctica que confirma o refuta las tesis de este artículo.

Gracias por leer el artículo hasta el final.

Ilustración "Multimedia Mikher" del autor del artículo.

Source: https://habr.com/ru/post/458270/


All Articles