Una mala línea de código puede tirar un servidor

Somos Roberto y Francisco, un pequeño equipo dedicado al desarrollo web especializado en las cuestiones técnicas de las plataformas WordPress y WooCommerce. Realizamos consultoría y formación, desarrollamos plugins y temas, optimizamos, securizamos, respondemos a incidentes de seguridad y mantenemos sitios. Fundada en 2012, somos el apoyo técnico de más de un centenar de proyectos web en España y Alemania, además formamos parte activa de la comunidad WordPress.

En programación hay que saber en todo momento qué se está haciendo, un pequeño desliz puede ocasionar grandes problemas que supongan pérdidas económicas y mala imagen. Hoy os voy a comentar un caso real.

Este caso versa sobre una web sobre WordPress que fue creciendo y creciendo en visitas, llegando fácilmente a cantidades de 250 usuarios conectados al mismo tiempo navegando por la web.

servidor-error

Paralelamente a su crecimiento ocurría un error extraño en el servidor. El proceso MySQL (la base de datos) se colapsaba de repente, esto provocaba que dejara de funcionar la web, y obligaba a un administrador a actuar instantáneamente para volver a poner en píe el servidor.
Era un error difícil de detectar, ocurría de forma muy esporádica, no se pudo replicar, y ha estado ocurriendo durante meses, causando problemas y quebraderos de cabeza.

Descubrir el fallo

Lo primero que se hace en estas situaciones es desconfiar de todo, no puedes saber si el problema puede estar en la configuración del servidor, en el software de la web, en un virus, en otro software que interfiere, en un ataque, en el servicio de correo (que también hace uso de la base de datos). Nosotros hicimos lo siguiente:

  • Analizar logs del servidor observando eventos cercanos a la caída del mismo
    • Detectamos irregularidades en el servicio de correo, fueron subsanadas, pero el fallo no venía de ahí.
  • Analizar los tiempos de carga de plugins con herramientas propias de WordPress
    • Se veía cómo algunos plugins lastraban la carga, los desactivamos o sustituimos por otros, tampoco venía de ahí el problema.
  • callgraphRecrear la instalación en otro servidor y analizar los tiempos de carga de funciones PHP (con xhprof) y sentencias SQL
    • Detectamos problemas de eficiencia en algunos plugins, concretamente plugins que mandaban información a servidores de terceros (esta conexión era síncrona y provocaba una espera sobre la carga de la página). Arreglamos estos problemas, pero tampoco eran la causa del problema «grande».
  • Analizar procesos en ejecución de cada usuario de MySQL (no teníamos permisos de superusuario en el servidor, por tanto no podíamos acceder a algunos logs o herramientas más adecuadas de MySQL)

En este último paso fue cuando dimos en hueso

SHOW PROCESSLIST;

mysql_show_processlist

La segunda línea era sospechosa, veíamos cómo tardaba mucho tiempo en ejecutarse, y haciendo algo tan peligroso como una copia a tabla temporal (esto ocurre cuando se hacen uniones de tablas, y las uniones de tablas pueden ser tareas computacionalmente muy pesadas). Buscamos la sentencia SQL directamente en el código, sin muchas esperanzas, pero la encontramos.

Descubrir qué ocurría

Esa sentencia SQL se encontraba en un Widget del Theme sobre el que se había construido la página (Un theme premium o de pago), hacía una cosa tan simple como mostrar los últimos 10 comentarios publicados, y lo hacía bien en el sentido de que funcionaba, pero la pega es que lo hacía de una forma muy poco ortodoxa.

global $wpdb;
$sql = "SELECT DISTINCT ID, post_title, post_password, comment_ID,
comment_post_ID, comment_author, comment_author_email, comment_date_gmt, comment_approved,
comment_type,comment_author_url,
SUBSTRING(comment_content,1,65) AS com_excerpt
FROM $wpdb->comments
LEFT OUTER JOIN $wpdb->posts ON ($wpdb->comments.comment_post_ID =
$wpdb->posts.ID)
WHERE comment_approved = '1' AND comment_type = '' AND
post_password = ''
ORDER BY comment_date_gmt DESC LIMIT ".$posts;

$comments = $wpdb->get_results($sql);

Analicemos los problemas de este código:

  • WordPress tiene funciones estándares para obtener una lista de comentarios de forma eficiente y cacheando los datos (lo cual evita sobrecargar la base de datos llamándola y llamándola otra vez). Implementar una sentencia SQL para esto, es absurdo y es buscarte problemas tu solo.
  • Si analizamos el SQL, hace una unión directa de dos tablas sin prefiltrar nada (comments y posts) y luego toma las 10 primeras ordenadas por fecha.
    Suponed que en un colegio queremos conocer a los 10 alumnos más jóvenes y sus padres, tal como está hecho este SQL es como si juntáramos en el colegio a todos los niños de todas las edades con todos sus padres, para luego elegir entre toda esta gente a los 10 alumnos más jóvenes y sus padres (¿No es más lógico ver primero cuales son los 10 alumnos más jóvenes, y una vez obtenida esta lista, ver quienes son sus respectivos padres?).
    Cuando tienes 100 posts y 500 comentarios esto no se nota mucho, pero en este caso se unían 84.000 posts con 13.000 comentarios, provocando un esfuerzo computacional completamente absurdo, para luego quedarse solo con 10.
  • Esta sentencia tarda la friolera de 4 segundos en ejecutarse (lo razonable en estos casos es que tarde 0.01 segundos, para que la web pueda cargar en menos de 3 segundos).
    ejecucion_sentencia_sql

Algo tan tonto como una llamada a la base de datos, que a primera vista parece correcta y no problemática, ha estado meses tirando un servidor dedicado de 8 núcleos.

Una simple solución

Se podría reformular la sentencia SQL, pero es más sencillo que todo eso, si nos vamos al Codex de WordPress encontramos una función para obtener listados de comentarios, solo tenemos que leernos el libro de instrucciones y aplicarlo.

$argscomments = array(
'status' => 'approve',
'number' => $posts,
'count' => false,
);

$comments = get_comments( $argscomments );

Aquí pedimos que nos de los últimos X comentarios aprobados, por defecto nos da los últimos en orden cronológico. Tan simple como esto.

El trabajo de un programador no es solo «picar código» sino también tener en cuenta este tipo de eventualidades, es decir, conocer lo que está haciendo más allá de que funcione o no. Este es un trabajo que requiere planificación, inspiración, seguridad, tranquilidad y pensamiento profundo. De lo contrarío, un programador puede equivocarse, y como veis son errores que a la larga pueden ser perjudiciales.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *