Tres ejemplos de errores que no se esconden de nadie





Escribo mucho sobre la investigación de errores complicados: errores de CPU, errores del kernel, asignación de memoria intermedia de 4 GB, pero la mayoría de los errores no son tan exóticos. A veces, para encontrar un error, solo necesita mirar el panel del servidor, pasar unos minutos en el generador de perfiles o leer las advertencias del compilador.



En este artículo, cubriré tres errores importantes que he encontrado y solucionado; todos ellos no se escondieron en absoluto y solo esperaron a que alguien los notara.



Sorpresa en el procesador del servidor







Hace varios años, pasé unas semanas estudiando el comportamiento de la memoria en servidores de juegos en vivo. Los servidores ejecutaban Linux en centros de datos remotos, por lo que la mayor parte del tiempo se dedicó a obtener los permisos necesarios para poder acceder a los servidores, además de aprender a trabajar eficazmente con perf y otras herramientas de diagnóstico de Linux. Descubrí una serie de errores que provocaban que el consumo de memoria fuera tres veces mayor de lo necesario y los solucioné:



  • Encontré una falta de coincidencia en el ID del mapa, lo que provocó que cada juego no usara la misma copia de aproximadamente 20 MB de datos, sino que cargara una nueva.
  • Encontré una variable global (!!) de 50 MB sin usar (!), Para la cual se configuró un memset cero (!!!), lo que hizo que consumiera RAM física en cada proceso.
  • Varios errores menos graves.


Pero nuestra historia no se tratará de eso.



Después de tomarme el tiempo para aprender cómo perfilar nuestros servidores de juegos, me di cuenta de que podía investigar esto un poco más a fondo. Por lo tanto, ejecuté perf en los servidores de uno de nuestros juegos. El primer proceso de servidor que describí fue ... extraño. Al ver los datos del procesador muestreados "en vivo", vi que una sola función consumía el 100% del tiempo de la CPU. Sin embargo, solo se ejecutaron catorce instrucciones en esta función. No tiene sentido.



Al principio asumí que estaba usando perf incorrectamenteo malinterpretar los datos. Miré algunos de los otros procesos del servidor y descubrí que aproximadamente la mitad de ellos estaban en un estado extraño. La segunda mitad tuvo un perfil de CPU más normal.



La función que nos interesa pasó por la lista enlazada de nodos de navegación. Le pregunté a mis colegas y encontré a un programador que dijo que los problemas de precisión del punto flotante podrían hacer que el juego generara listas de navegación en bucle. Siempre quisieron limitar la cantidad máxima de nodos que podían omitirse, pero nunca llegaron a hacerlo.



¿Entonces el rompecabezas está resuelto? La inestabilidad de los cálculos de punto flotante provoca bucles en las listas de navegación, lo que hace que el juego los omita sin cesar; eso es todo, se explica el comportamiento.



Pero ... tal explicación significaría que cuando esto sucede, el proceso del servidor entra en un bucle sin fin, todos los jugadores tendrán que desconectarse de él y el proceso del servidor consumirá sin cesar todo el núcleo del procesador. Si ese fuera el caso, ¿no nos quedaríamos finalmente sin recursos en nuestros servidores? ¿Nadie se habría dado cuenta de esto?



Busqué datos de monitoreo del servidor y encontré algo como esto:







Durante todo el período de monitoreo (uno a dos años), observé fluctuaciones diarias y semanales en la carga del servidor, que se superpusieron con el patrón mensual. El nivel de utilización del procesador aumentó gradualmente y luego bajó a cero. Después de preguntar un poco más, descubrí que los servidores se reiniciaban una vez al mes. Y finalmente, la lógica apareció en todo esto:



  • , .
  • , , .
  • CPU , 50%.
  • .


El error se solucionó agregando algunas líneas de código que dejaron de atravesar la lista después de veinte nodos de navegación, lo que supuestamente ahorró varios millones de dólares en costos de servidor y energía. No encontré este error al mirar los gráficos de monitoreo, pero cualquiera que los mirara podría hacerlo.



Me encanta el hecho de que la frecuencia del error coincidió perfectamente con la maximización del costo del mismo; al mismo tiempo, nunca causó problemas lo suficientemente serios como para ser encontrados. Esto es similar a la acción de un virus que evoluciona para hacer que las personas estornuden, no las maten.



Carga lenta







La productividad del desarrollador de software está estrechamente relacionada con la velocidad del ciclo de edición / compilación / enlace / depuración. En otras palabras, depende de cuánto tiempo lleve después de realizar un cambio en el archivo fuente para ejecutar el nuevo binario con el cambio realizado. A lo largo de los años, he hecho un gran trabajo reduciendo los tiempos de compilación / enlace, pero los tiempos de carga también son importantes. Algunos juegos hacen una gran cantidad de trabajo cada vez que comienzan. Estoy impaciente y, por lo tanto, a menudo soy el primero en pasar horas o días para que el juego se cargue unos segundos más rápido.



En este caso, ejecuté mi generador de perfiles favorito y miré el gráfico de uso de la CPU durante la fase de carga inicial del juego. Un paso parecía el más prometedor: se necesitaron unos diez segundos para inicializar algunos datos de iluminación. Tenía la esperanza de encontrar alguna forma de acelerar estos cálculos ahorrando unos cinco segundos en el inicio. Antes de sumergirme en el estudio, consulté con un especialista en gráficos. Dijo:



“No usamos estos datos de iluminación en el juego. Simplemente elimine este desafío ".



Oh, genial. Fue fácil.



Al dedicar media hora a perfilar y cambiar una línea, pude reducir a la mitad el tiempo de carga del menú principal y no requirió ningún esfuerzo extraordinario.



Salida inoportuna



Debido al número arbitrario de argumentos en el formato, es printfmuy fácil obtener un error de falta de coincidencia de tipos. En la práctica, los resultados pueden variar mucho:



  1. printf ("0x% 08lx", p); // Imprime el puntero como int - truncar o peor en 64 bits
  2. printf ("% d,% f", f, i); // Cambiar los lugares de float e int - puede mostrar una tontería, o puede funcionar (!)
  3. printf ("% s% d", i, s); // Cambiar el orden de cadena e int - lo más probable es que provoque un bloqueo


El estándar dice que tales desajustes de tipos son un comportamiento indefinido, y algunos compiladores generan código que falla deliberadamente con cualquiera de estos desajustes, sin embargo, lo anterior enumera los resultados más probables (nota: la pregunta de por qué el segundo párrafo a menudo produce los resultados deseados es bueno Rompecabezas de conocimiento ABI ).



Estos errores son muy fáciles de cometer, por lo que todos los compiladores modernos tienen la capacidad de advertir a los desarrolladores que se ha producido un desajuste. Tanto gcc como clang tienen anotaciones de funciones de estilo printf y pueden advertir de discrepancias (sin embargo, desafortunadamente, las anotaciones no funcionan con funciones de estilo wprintf). VC ++ tiene anotaciones (desafortunadamente otras) que / analyse puede usar para advertir sobre desajustes, pero si no usa / analiza, solo advertirá sobre las funciones de estilo CRT de printf / wprintf, no sus funciones personalizadas ...



La empresa para la que trabajé anotó sus funciones en estilo printf para que gcc / clang emitiera advertencias, pero luego decidió ignorar las advertencias. Esta es una decisión extraña, porque tales advertencias son indicadores perfectamente precisos de errores: la relación señal / ruido es infinita.



Decidí comenzar a limpiar estos errores con VC ++ y / analizar anotaciones para encontrar todos los errores exactamente. Trabajé en la mayoría de los errores e hice un gran cambio esperando que se verificara el código antes de enviarlo.







Ese fin de semana hubo un corte de energía en el centro de datos y todos nuestros servidores fallaron (probablemente debido a errores en la configuración de energía). El personal de emergencia se apresuró a reconstruir y arreglar todo antes de que se perdiera demasiado dinero.



El aspecto divertido de los errores de printf es que se comportan mal el 100% del tiempo. Es decir, si van a mostrar datos incorrectos o hacer que el programa se bloquee, esto sucede siempre. Por lo tanto, pueden permanecer en el programa solo si están en un código de registro que nunca se lee o en un código de manejo de errores que rara vez se ejecuta.



Resultó que el evento de "reinicio simultáneo de todos los servidores" hizo que el código se moviera por rutas que normalmente no se ejecutarían. Los servidores de inicio comenzaron a buscar otros servidores, no pudieron encontrarlos y mostraron algo como el siguiente mensaje:



fprintf (log, "No se puede encontrar el servidor% s. Código de error% d. \ n", err, nombre_servidor);


¡Ups! Falta de coincidencia de tipos para un número arbitrario de argumentos. Y salida.



Los socorristas tienen un problema adicional. Los servidores debían reiniciarse, pero esto no se pudo hacer antes de que se examinaran los volcados por caída, se descubriera un error, no se reconstruyeran los binarios del servidor y se lanzara una nueva compilación. Fue un proceso bastante rápido; al parecer, no más de unas pocas horas, pero bien podría haberse evitado.



Pensé que esta historia demuestra perfectamente por qué deberíamos dedicar tiempo a solucionar las causas de estas advertencias: ¿por qué ignorar las advertencias que les dicen que el código definitivamente fallará o se comportará mal cuando se ejecute? Sin embargo, a nadie le molestó que la eliminación de esta clase de advertencias pudiera ahorrarnos varias horas de inactividad. De hecho, la cultura de la empresa no parecía interesada en ninguna de estas soluciones. Pero fue este último error el que me hizo darme cuenta de que era hora de mudarme a otra empresa.



¿Qué lecciones se pueden aprender de esto?



Si todos los involucrados están trabajando arduamente en las características del producto y corrigiendo errores conocidos, entonces probablemente haya errores muy simples que estén en exhibición pública. Dedique un poco de tiempo a estudiar los registros, a limpiar las advertencias del compilador (aunque, de hecho, si tiene advertencias del compilador, probablemente valga la pena repensar las decisiones que ha tomado en la vida), ejecute el generador de perfiles durante unos minutos. Obtienes puntos extra si agregas tu propio sistema de registro, habilitas nuevas advertencias o usas un generador de perfiles que nadie más usa excepto tú.



Si está haciendo arreglos excelentes que mejoran el uso o la estabilidad de la memoria / cpu, y a nadie le importa, busque una empresa que lo valore.



Discusión de Hacker News aquí , discusión de Reddit aquí , discusión de Twitter aquí .






Publicidad



Un servidor confiable en alquiler y la elección correcta de un plan de tarifas le permitirán distraerse menos con notificaciones de monitoreo desagradables: todo funcionará sin problemas y con un tiempo de actividad muy alto.









All Articles