Mi nombre es Danil Mukhametzyanov y he trabajado como desarrollador backend en Badoo durante siete años. Durante este tiempo, logré crear y cambiar una gran cantidad de código. Tan grande que un día se me acercó un gerente y me dijo: “Se acabó la cuota. Para agregar algo, debes eliminar algo ".
Está bien, eso es solo una broma, no dijo eso. ¡Es una lástima! Durante toda la existencia de la empresa, Badoo ha acumulado más de 5,5 millones de líneas de código empresarial lógico, excluidas las líneas en blanco y los corchetes de cierre.
La cantidad en sí no es tan terrible: miente, no pide comida. Pero hace dos o tres años, comencé a notar que leo cada vez más y trato de descubrir un código que realmente no funciona en un entorno de producción. Eso es, de hecho, muerto.
Esta tendencia fue notada no solo por mí. Badoo se dio cuenta de que nuestros ingenieros altamente remunerados están constantemente perdiendo el tiempo en códigos muertos.
Di esta charla en Badoo PHP Meetup # 4
¿De dónde viene el código muerto?
Empezamos a buscar las causas de los problemas. Los dividimos en dos categorías:
- proceso - aquellos que surgen como resultado del desarrollo;
- histórico - código heredado.
En primer lugar, decidimos desmontar las fuentes del proceso para evitar la aparición de nuevos problemas.
Pruebas A / B
Badoo comenzó a utilizar activamente las pruebas A / B hace cuatro años. Ahora tenemos alrededor de 200 pruebas en ejecución constante y todas las funciones del producto pasan por este procedimiento.
Como resultado, se acumularon alrededor de 2000 pruebas completadas en cuatro años, y esta cifra está en constante crecimiento. Nos asustó que cada prueba es un fragmento de código muerto que ya no se ejecuta y no es necesario en absoluto.
La solución al problema llegó rápidamente: comenzamos a crear automáticamente un ticket para cortar el código al completar la prueba A / B.
Un ejemplo de boleto
Pero el factor humano se activaba periódicamente. Una y otra vez, encontramos el código de prueba que continuó ejecutándose, pero nadie pensó en él y completó la prueba.
Luego hubo un marco rígido: cada prueba debe tener una fecha de finalización. Si el gerente olvidó leer los resultados de la prueba, automáticamente se detendría y se apagaría. Y, como ya mencioné, se creó automáticamente un ticket para eliminarlo manteniendo la versión original de la lógica de la función.
Con la ayuda de un mecanismo tan simple, nos deshicimos de una gran capa de trabajo.
Diversidad de clientes
En nuestra empresa se admiten varias marcas, pero el servidor es una. Cada marca está representada en tres plataformas: web, iOS y Android. En iOS y Android, tenemos un ciclo de desarrollo semanal: una vez a la semana, junto con una actualización, recibimos una nueva versión de la aplicación en cada plataforma.
Es fácil adivinar que con este enfoque, en un mes tendremos alrededor de una docena de nuevas versiones que necesitan soporte. El tráfico de usuarios se distribuye de forma desigual entre ellos: los usuarios cambian gradualmente de una versión a otra. Algunas versiones anteriores tienen tráfico, pero es tan pequeño que es difícil mantenerlo. Es duro e inútil.
Entonces comenzamos a contar la cantidad de versiones que queremos admitir. Hay dos límites para el cliente: el límite suave y el límite estricto.
Cuando se alcanza el límite suave (cuando ya se han lanzado tres o cuatro versiones nuevas y la aplicación aún no está actualizada), el usuario ve una pantalla con una advertencia de que su versión está desactualizada. Cuando se alcanza el límite estricto (esto es aproximadamente 10-20 versiones "perdidas", según la aplicación y la marca), simplemente eliminamos la opción para omitir esta pantalla. Se convierte en bloqueo: no puedes usar la aplicación con él.
Pantalla para el límite estricto
En este caso, es inútil continuar procesando las solicitudes provenientes del cliente, no verá nada más que una pantalla.
Pero aquí, como en el caso de las pruebas A / B, surgió un matiz. Los desarrolladores de clientes también son personas. Usan nuevas tecnologías, chips de sistemas operativos y, después de un tiempo, la versión de la aplicación ya no es compatible con la siguiente versión del sistema operativo. Sin embargo, el servidor continúa sufriendo porque debe continuar procesando estas solicitudes.
Se nos ocurrió una solución separada para el caso cuando finalizó el soporte para Windows Phone. Preparamos una pantalla que informaba al usuario: “¡Te queremos mucho! ¡Eres muy genial! ¿Pero puedes empezar a usar otra plataforma? Nuevas funciones interesantes estarán disponibles para usted, pero aquí no podemos hacer nada ". Como norma, ofrecemos una plataforma web como plataforma alternativa, que siempre está disponible.
Con un mecanismo tan simple, hemos limitado el número de versiones de cliente que soporta el servidor: aproximadamente 100 versiones diferentes de todas las marcas, de todas las plataformas.
Banderas de funciones
Sin embargo, al deshabilitar el soporte para plataformas más antiguas, no entendimos completamente si era posible eliminar por completo el código que estaban usando. ¿O las plataformas que quedan para versiones anteriores del sistema operativo continúan usando la misma funcionalidad?
El problema es que nuestra API no se construyó en la parte versionada, sino en el uso de indicadores de funciones. Cómo llegamos a esto, puede averiguarlo en este informe .
Teníamos dos tipos de banderas de funciones. Te los cuento con ejemplos.
Características menores
El cliente le dice al servidor: “Hola, soy yo. Apoyo las publicaciones de fotos ". El servidor lo mira y responde: “¡Genial, soporte! Ahora lo sé y te enviaré mensajes con fotos ". La característica clave aquí es que el servidor no puede influir en el cliente de ninguna manera, simplemente acepta mensajes de él y se ve obligado a escucharlos.
Llamamos a estas banderas características menores. En este momento tenemos más de 600.
¿Cuál es la desventaja de usar estas banderas? Periódicamente, existe una gran funcionalidad que no se puede cubrir solo desde el lado del cliente; también desea controlarla desde el lado del servidor. Para ello, introdujimos otro tipo de banderas.
Características de la aplicación
El mismo cliente, el mismo servidor. El cliente dice: “Servidor, he aprendido a admitir la transmisión de video. ¿Encenderlo? " El servidor responde: "Gracias, lo tendré en cuenta". Y agrega: “Genial. Vamos a mostrarle a nuestro querido usuario esta funcionalidad, se alegrará ". O: "Está bien, pero no lo incluiremos todavía".
A estas funciones las denominamos funciones de aplicación. Son más pesados, por lo que tenemos menos, pero aún son suficientes: más de 300.
Entonces, los usuarios pasan de una versión del cliente a otra. Algún tipo de bandera está comenzando a ser compatible con todas las versiones activas de aplicaciones. O, por el contrario, no admitido. No está del todo claro cómo controlar esto: ¡100 versiones de cliente, 900 banderas! Para lidiar con esto, creamos un tablero.
Un cuadrado rojo significa que todas las versiones de esta plataforma no admiten esta función; verde: todas las versiones de esta plataforma admiten esta bandera. Si la bandera se puede apagar y encender, parpadeará periódicamente. Podemos ver qué pasa en qué versión.
Pantalla del tablero
Justo en esta interfaz, comenzamos a crear tareas para eliminar la funcionalidad. Cabe señalar que no es necesario completar todas las celdas rojas o verdes de cada fila. Hay banderas que solo se ejecutan en una plataforma. Hay banderas que se rellenan para una sola marca.
Automatizar el proceso no es tan conveniente, pero, en principio, no es necesario, solo necesita establecer una tarea y mirar periódicamente el tablero. En la primera iteración, logramos cortar más de 200 banderas. ¡Eso es casi una cuarta parte de las banderas que usamos!
Aquí es donde terminaron las fuentes del proceso. Aparecieron como resultado de nuestro flujo de desarrollo y hemos integrado con éxito el trabajo con ellos en este proceso.
Qué hacer con el código heredado
Hemos detenido la aparición de nuevos problemas en las fuentes de proceso. Y nos enfrentamos a una pregunta difícil: ¿qué hacer con el código heredado acumulado a lo largo de los años? Abordamos la solución desde el punto de vista de la ingeniería, es decir, decidimos automatizar todo. Pero no estaba claro cómo encontrar el código que no se estaba utilizando. Se escondió en su pequeño y acogedor mundo: no se llama de ninguna manera, no deja que nadie se entere de sí mismo.
Tuvimos que ir desde el otro lado: tomar todo el código que teníamos, recopilar información sobre qué piezas se ejecutan exactamente y luego hacer la inversión.
Luego, lo juntamos y lo implementamos al nivel más mínimo: en archivos. De esta manera, podríamos obtener fácilmente una lista de archivos del repositorio ejecutando el comando UNIX apropiado.
Quedaba por recopilar una lista de archivos que se utilizan en producción. Es bastante simple: para cada solicitud en el apagado, llame a la función PHP correspondiente. La única optimización que hemos hecho aquí es comenzar a solicitar OPCache en lugar de solicitar cada solicitud. De lo contrario, la cantidad de datos sería muy grande.
Como resultado, descubrimos muchos artefactos interesantes. Pero con un análisis más profundo, nos dimos cuenta de que nos faltaban métodos no utilizados: la diferencia en su número era de tres a siete veces.
Resultó que el archivo podía cargarse, ejecutarse y compilarse por el bien de una sola constante o un par de métodos. Todo lo demás quedó inútil para yacer en este mar sin fondo.
Armar una lista de métodos
Sin embargo, resultó lo suficientemente rápido como para recopilar una lista completa de métodos. Simplemente tomamos el analizador sintáctico de Nikita Popov , lo alimentamos con nuestro repositorio y obtuvimos todo lo que tenemos en el código.
La pregunta sigue siendo: ¿cómo ensamblar lo que se está reproduciendo en producción? Estamos interesados en la producción, porque las pruebas pueden cubrir lo que no necesitamos en absoluto. Sin pensarlo dos veces, tomamos XHProf. Ya se ha ejecutado en producción para parte de las consultas, por lo que teníamos muestras de perfil que se almacenan en las bases de datos. Fue suficiente con ir a estas bases de datos, analizar las instantáneas generadas y obtener una lista de archivos.
Desventajas de XHProf
Repetimos este proceso en otro clúster donde XHProf no se inició, pero era muy necesario. Este es un clúster para ejecutar scripts en segundo plano y procesamiento asincrónico, lo cual es importante para cargas elevadas, ejecuta mucha lógica.
Y luego nos aseguramos de que XHProf sea inconveniente para nosotros.
- Requiere cambiar el código PHP. Debe insertar el código de inicio de seguimiento, finalizar el seguimiento, obtener los datos recopilados y escribirlos en un archivo. Después de todo, este es un generador de perfiles, pero tenemos producción, es decir, hay muchas solicitudes, también debe pensar en el muestreo. En nuestro caso, esto se vio agravado por una gran cantidad de clústeres con diferentes puntos de entrada.
- . . , OPCache. : XHProf, . , core- .
- . . XHProf . ( XHProf): CPU, , . , , . - XHProf aggregator ( XHProf Live Profiler, open-source) , , , . , : «, , », CPU , , Live Profiler . , , .
- XHProf. , . . , . : , ( , youROCK, esto no es requerido por lsd , pero era más conveniente mantener una sola envoltura sobre él). Parchear XHProf no es lo que queríamos hacer, porque es un generador de perfiles bastante grande (¿y si rompemos algo sin darnos cuenta?).
Había una idea más: excluir ciertos espacios de nombres, por ejemplo, los espacios de nombres de proveedores del compositor, que se ejecutan en producción, porque son inútiles: no refactorizaremos los paquetes de proveedores ni eliminaremos código adicional de ellos.
Requisitos de la solución
Una vez más nos reunimos y miramos qué soluciones existen. Y formularon la lista final de requisitos.
Primero: gastos generales mínimos. Para nosotros, XHProf fue el listón: no más de lo que requiere.
En segundo lugar, no queríamos cambiar el código PHP.
En tercer lugar, queríamos que la solución funcionara en todas partes, tanto en FPM como en la CLI.
Cuarto, queríamos manejar las horquillas. Se utilizan activamente en CLI, en servidores en la nube. No quería crear una lógica específica para ellos dentro de PHP.
Quinto: muestreo fuera de la caja. De hecho, esto se deriva del requisito de no cambiar el código PHP. A continuación, explicaré por qué necesitábamos muestreo.
Sexto y último:la capacidad de forzar desde el código. Nos encanta cuando todo funciona automáticamente, pero a veces es más conveniente iniciar, ajustar y mirar manualmente. Necesitábamos la capacidad de habilitar y deshabilitar todo directamente desde el código, y no por decisión aleatoria del mecanismo más general del módulo PHP, que establece la probabilidad de inclusión a través de la configuración.
Cómo funciona funcmap
Como resultado, tenemos una solución que llamamos funcmap.
Funcmap es esencialmente una extensión de PHP. En términos PHP, este es un módulo PHP. Para entender cómo funciona, echemos un vistazo a cómo funciona el proceso PHP y el módulo PHP.
Entonces, comienzas un proceso. PHP permite suscribirse a hooks al construir un módulo. El proceso comienza, se lanza el gancho GINIT (Global Init), donde puede inicializar los parámetros globales. Luego, se inicializa el módulo. Las constantes se pueden crear y asignar allí, pero solo para un módulo específico, y no para una solicitud, de lo contrario, se disparará en el pie.
Luego entra la solicitud del usuario, se llama al gancho RINIT (Request Init). Una vez completada la solicitud, se apaga y, al final, se apaga el módulo: MSHUTDOWN y GSHUTDOWN. Todo es lógico.
Si hablamos de FPM, entonces cada solicitud de usuario llega a un trabajador ya existente. Básicamente, RINIT y RSHUTDOWN simplemente funcionan en círculos hasta que FPM decide que el trabajador está desactualizado, es hora de dispararle y crear uno nuevo. Si hablamos de CLI, entonces es solo un proceso lineal. Todo se llamará una vez.
Cómo funciona funcmap
De este conjunto, estábamos interesados en dos ganchos. El primero es RINIT . Comenzamos a establecer la bandera de recopilación de datos: este es un tipo de aleatorio que se llamó para muestrear los datos. Si funcionó, procesamos esta solicitud: recopilamos estadísticas para llamadas a funciones y métodos para ello. Si no funcionó, la solicitud no se procesó.
Lo siguiente es crear una tabla hash si no existe. La tabla hash la proporciona el propio PHP internamente. No necesitas inventar nada aquí, solo tómalo y úsalo.
A continuación, inicializamos el temporizador. Hablaré de él a continuación, por ahora, solo recuerda que él es importante y necesario.
El segundo gancho es MSHUTDOWN... Quiero señalar que es MSHUTDOWN, no RSHUTDOWN. No queríamos resolver algo para cada solicitud, estábamos interesados en todo el trabajador. En MSHUTDOWN, tomamos nuestra tabla hash, la revisamos y escribimos un archivo (¿qué podría ser más confiable, más conveniente y versátil que el buen archivo antiguo?).
La tabla hash se llena de manera bastante simple mediante el mismo gancho PHP zend_execute_ex, que se llama cada vez que se llama a una función definida por el usuario. El registro contiene parámetros adicionales mediante los cuales puede comprender qué tipo de función es, su nombre y clase. Lo aceptamos, leemos el nombre, lo escribimos en la tabla hash y luego llamamos al gancho predeterminado.
Este gancho no escribe funciones en línea. Si desea anular las funciones integradas, existe una funcionalidad separada para eso llamada zend_execute_internal.
Configuración
¿Cómo puedo configurar esto sin cambiar el código PHP? La configuración es muy simple:
- habilitado: si está habilitado o no.
- El archivo en el que estamos escribiendo. Hay un marcador de posición pid para excluir una condición de carrera cuando diferentes procesos PHP escriben en el mismo archivo al mismo tiempo.
- Base de probabilidad: nuestra bandera de probabilidad. Si lo establece en 0, no se escribirá ninguna solicitud; si es 100, significa que todas las solicitudes se registrarán y se incluirán en las estadísticas.
- flush_interval. Esta es la frecuencia con la que volcamos todos los datos en un archivo. Queremos que la recopilación de datos se ejecute en la CLI, pero existen scripts que pueden tardar mucho en ejecutarse, consumiendo memoria si utiliza una gran cantidad de funciones.
Además, si tenemos un clúster que no está tan cargado, FPM entiende que el trabajador está listo para procesar más y no mata el proceso, vive y se come una parte de la memoria. Después de un cierto período de tiempo, descargamos todo en el disco, restablecemos la tabla hash y comenzamos a llenarla de nuevo. Sin embargo, si aún no se ha alcanzado el tiempo de espera, se activa el gancho MSHUTDOWN, donde escribimos todo finalmente.
Lo último que queríamos era la capacidad de llamar a funcmap desde código PHP. La extensión correspondiente proporciona el único método que le permite habilitar o deshabilitar la recopilación de estadísticas independientemente de cómo funcionó la probabilidad.
Gastos generales
Nos preguntamos cómo afecta todo esto a nuestros servidores. Hemos construido un gráfico que muestra la cantidad de solicitudes que llegan a una máquina de combate real de uno de los clústeres de PHP más cargados.
Puede haber muchas máquinas de este tipo, por lo que el gráfico muestra el número de solicitudes, no la CPU. El equilibrador se da cuenta de que la máquina ha comenzado a consumir más recursos de lo habitual e intenta igualar las demandas para que las máquinas se carguen de manera uniforme. Esto fue suficiente para comprender cómo se degradaba el servidor.
Activamos nuestra extensión secuencialmente al 25%, 50% y 100% y vimos la siguiente imagen:
La línea de puntos es la cantidad de solicitudes que esperamos. La línea principal es la cantidad de solicitudes que ingresan. Vimos una degradación de aproximadamente 6%, 12% y 23%: este servidor comenzó a procesar casi una cuarta parte menos de solicitudes entrantes.
En primer lugar, este gráfico demuestra que el muestreo es importante para nosotros: no podemos gastar el 20% de los recursos del servidor en recopilar estadísticas.
Resultado falso
El muestreo tiene un efecto secundario: algunos métodos no están incluidos en las estadísticas, pero de hecho se utilizan. Intentamos combatir esto de varias maneras:
- . -, . , , , , .
- . , : , , .
Probamos dos soluciones para el manejo de errores. La primera es permitir la recopilación de estadísticas de forma forzada a partir del momento en que se generó el error: recopile el registro de errores y analícelo. Pero aquí hay una trampa: cuando un recurso cae, el número de errores aumenta instantáneamente. Empiezas a procesarlos, hay muchos más trabajadores y el grupo comienza a morir lentamente. Por lo tanto, hacerlo no es del todo correcto.
¿Cómo hacerlo de otra manera? Leímos y, usando el analizador sintáctico de Nikita Popov, revisamos lo que está en juego y notamos qué métodos se llaman allí. Por lo tanto, hemos eliminado la carga en el servidor y reducido el número de falsos positivos.
Pero todavía había métodos que rara vez se llamaban y sobre los que no estaba claro si eran necesarios o no. Hemos agregado un ayudante que ayuda a determinar el hecho de usar tales métodos: si el muestreo ya ha demostrado que el método rara vez se llama, entonces puede activar el procesamiento al 100% y no pensar en lo que está sucediendo. Se registrará cualquier ejecución de este método. Lo sabrás.
Si está seguro de que se está utilizando el método, puede que sea excesivo. Quizás esta sea una funcionalidad necesaria, pero rara. Imagine que tiene la opción "Quejarse", que rara vez se utiliza, pero es importante: no puede eliminarla. Para tales casos, hemos aprendido a etiquetar manualmente dichos métodos.
Hemos creado una interfaz que muestra qué métodos están en uso (están sobre un fondo blanco) y cuáles potencialmente no se utilizan (están sobre un fondo rojo). Aquí también puede marcar los métodos necesarios.
Pantalla de interfaz
La interfaz es genial, pero volvamos al principio, que es el problema que estábamos resolviendo. Consistía en el hecho de que nuestros ingenieros leían código muerto. ¿Dónde lo leen? En el IDE. ¡Imagínese cómo sería obligar a un fanático de su oficio a dejar el mundo IDE en algún tipo de interfaz web y hacer algo allí! Decidimos que teníamos que encontrarnos con nuestros colegas a mitad de camino.
Hemos creado un complemento para PhpStorm que carga toda la base de datos de los métodos no utilizados y muestra si este método se utiliza o no. Además, puede marcar el método como utilizado en la interfaz. Todo esto irá al servidor y estará disponible para el resto de los colaboradores de la base de código.
Con esto concluye la parte principal de nuestro trabajo con Legacy. Comenzamos a notar más rápido que no estamos ejecutando, respondemos más rápido y no perdemos tiempo buscando el código no utilizado manualmente.
La extensión funcmap está disponible en GitHub . Estaremos encantados si es útil para alguien.
Alternativas
Desde fuera puede parecer que en Badoo no sabemos qué hacer con nosotros mismos. ¿Por qué no echar un vistazo a lo que hay en el mercado?
Ésta es una pregunta justa. Miramos, y no había nada en el mercado en ese momento. Fue solo cuando comenzamos a implementar activamente nuestra solución que descubrimos que, al mismo tiempo, un hombre llamado Joe Watkins, que vive en la brumosa Gran Bretaña, implementó una idea similar y creó la extensión Tombs.
No lo estudiamos con mucho cuidado, porque ya teníamos nuestra propia solución, pero sin embargo encontramos varios problemas:
- Falta de muestreo. Arriba, expliqué por qué lo necesitamos.
- . , APCu ( ), .
- CLI. , , CLI-, .
- . Tombs, , , , , , . funcmap («» , ): , . Tombs , , FPM CLI. - , .
En primer lugar, piense de antemano cómo eliminará la funcionalidad que se implementa durante un corto período de tiempo, especialmente si el desarrollo es muy activo. En nuestro caso, se trataba de pruebas A / B. Si no lo piensa de antemano, tendrá que limpiar los escombros.
Segundo: conozca a sus clientes de vista. No importa si son internos o externos, debes conocerlos. En algún momento, debes decirles: “¡Querido, detente! No".
Tercero: limpia tu API. Esto conduce a una simplificación de todo el sistema.
Y cuarto: puedes automatizar todo, incluso la búsqueda de código muerto. Que hicimos.