Este mes se cumplen diez años de aquella vez en que mi equipo y yo pensamos por un rato que nos íbamos a ver envueltos en un incidente diplomático internacional.
Era mayo de 2010, y hacía unos meses que me había incorporado al equipo del servidor de gadgets de Google en Mountain View (California), procedente de la oficina de Google en Dublín (Irlanda).
Si os acordáis de iGoogle, seguro que también recordaréis los gadgets. iGoogle era el portal personalizable de Google y tenía unas ventanitas, llamadas “gadgets”, que los usuarios podían seleccionar y poner en su página de iGoogle según su antojo. Había gadgets para ver el email, las noticias o el pronóstico del tiempo, e incluso había uno que hacía conversiones de unidades de medida (de metros a pies, de pintas a litros, etc.). Nuestro equipo se encargaba del sistema que mostraba los gadgets a los usuarios.
Un día nos llegó un aviso que decía que, en Taiwán, el gadget de conversión de unidades no aparecía en caracteres chinos tradicionales, que son los que se utilizan en Taiwán, sino en caracteres chinos simplificados, que son los que se utilizan en la China continental.
En aquellas fechas, Google y China no estaban atravesando el mejor momento de su relación: unos meses antes, Google había descubierto una serie de ataques por parte de hackers procedentes de China y anunció que cerraba el buscador especial para China que censuraba ciertos resultados. A partir de entonces, los usuarios chinos usarían el buscador normal sin censura. A China no le gustó esta reacción, ni a Google le había gustado la acción original.
Con tanta tensión entre Google y China, la noticia de que los usuarios en Taiwán veían un gadget como si estuviesen en China tampoco fue del agrado del equipo del servidor de gadgets. ¿Tal vez había alguien en China interceptando el tráfico de Google para Taiwán? Deseábamos de todo corazón que no fuera cierto, y aunque no pensábamos de verdad verdadera que eso era lo que estaba pasando, necesitábamos llegar al fondo del asunto.
El primer paso cuando se recibe un aviso de error es intentar reproducirlo: no es posible investigar un error que no se puede ver. Sin embargo, por más que lo intentaba, yo no era capaz de reproducir el error. Enviaba peticiones para ver el gadget como si fuera un usuario de Taiwán, pero siempre lo veía en chino tradicional, como si no hubiera ningún error. Lo intenté de una y otra manera, pero sin ningún resultado.
Después se me ocurrió enviar la misma petición a todos los servidores al mismo tiempo y ver si había diferencias entre ellos. Nuestro servicio recibía peticiones de todo el mundo, así que también teníamos servidores repartidos por todo el planeta y las peticiones llegaban automáticamente al servidor más cercano. Cuando yo hacía mis pruebas, mis peticiones iban a un servidor situado en los EEUU, pero las peticiones procedentes de Taiwán iban a un servidor ubicado en Asia. En teoría, todos los servidores eran idénticos, pero ¿y si no lo fueran?
Preparé una página web que enviaba peticiones directamente a todos los servidores, la cargué en mi navegador, y entonces vi que algunos servidores daban respuestas diferentes. La mayoría de los servidores en Europa y América respondían con el gadget en chino tradicional, que era el resultado correcto; sin embargo, la mayoría de los servidores en Asia respondían en chino simplificado.
Para hacerlo más misterioso, no todos los servidores en cada lugar me daban el mismo resultado: algunos daban la respuesta correcta y otros la respuesta incorrecta, pero la proporción entre uno y otro era distinta dependiendo de la ubicación.
Después de hacer muchas pruebas, me di cuenta de que había una especie de efecto memoria. Durante varios minutos después de enviar una petición para ver el gadget en chino simplificado, los servidores respondían en chino simplificado a todas las peticiones en chino tradicional. También ocurría al contrario: tras una petición en chino tradicional, los servidores respondían en chino tradicional a las peticiones en chino simplificado.
Esto explicaba por qué la mayor parte de los servidores en Asia respondían en chino simplificado: la mayoría de los usuarios hablantes de chino viven en China, por lo que usan caracteres simplificados. La mayoría de las peticiones procedentes de China iba a servidores en Asia, por lo que éstos recibían peticiones en chino simplificado. Entonces estos servidores se quedaban “atascados” en chino simplificado y, cuando recibían una petición en chino tradicional, daban una respuesta en chino simplificado.
Sentí un gran alivio cuando di con esta explicación, ya que significaba que el problema no había sido causado por una acción de interceptación de tráfico al nivel de una nación-estado, sino que era un error de programación normal y corriente. No obstante, todavía necesitábamos arreglarlo, y los síntomas sugerían que estaba causado por un problema con una caché.
Los gadgets estaban definidos en un fichero XML. Los gadgets también podían estar traducidos a varios idiomas, así que el texto del gadget en cada idioma estaba guardado en otro fichero XML, y el fichero de definición del gadget contenía una lista que indicaba qué idioma aparecía en qué fichero de traducción.
Cada vez que alguien quería ver un gadget, el servidor tenía que descargar el fichero XML de definición, interpretarlo, determinar qué fichero de traducción tenía que usar, descargar el fichero XML de traducción e interpretarlo también. Algunos gadgets tenían millones de usuarios, por lo que el servidor tendría que descargar e interpretar los mismos ficheros una y otra vez. Para evitarlo, el servidor de gadgets tenía una “caché”.
Una caché es una estructura de datos que almacena el resultado de una operación para evitar tener que realizar esa operación repetidamente. En el servidor de gadgets, la caché almacenaba ficheros XML que había descargado e interpretado anteriormente. Cuando el servidor necesitaba un fichero, primero miraba si ya estaba en la caché; si lo estaba, podía utilizarlo directamente sin necesidad de descargarlo e interpretarlo. Si no, el servidor descargaba el fichero, lo interpretaba y almacenaba el resultado en la caché para poder utilizarlo en el futuro.
Mi teoría inicial era que, de alguna manera, la caché podría estar confundiendo los ficheros de traducción al chino simplificado y chino tradicional. Me pasé varios días inspeccionando el código y el contenido de la caché, pero no pude ver ningún problema. Hasta donde yo podía ver, la caché de ficheros XML estaba implementada correctamente y funcionaba perfectamente. Si no fuera porque podía verlo con mis propios ojos, habría jurado que era imposible que el gadget mostrara el idioma incorrecto.
Durante el tiempo que invertí en inspeccionar el código, también estuve intentando reproducir el problema en mi ordenador. Los servidores de producción se quedaban “atascados” durante unos minutos en chino simplificado o chino tradicional, pero esto no sucedía nunca cuando ejecutaba el servidor en mi ordenador: si enviaba peticiones mezcladas, también recibía respuestas mezcladas. Por lo tanto, una vez más, no podía reproducir el problema de forma controlada.
Por este motivo tomé una decisión drástica: iba a conectar un depurador a un servidor en la red de producción y reproducir el error allí.
A buen seguro, no iba a hacer eso con un servidor de producción. En aquel momento teníamos varios tipos de servidores: no sólo servidores de producción, que recibían las peticiones procedentes de usuarios normales; también teníamos servidores “sandbox”, que no tenían usuarios externos, sino que los usábamos para que iGoogle y otros servicios que usaban gadgets pudiesen hacer pruebas sin afectar a los usuarios. Yo no iba, ni de lejos, a conectar un depurador a un servidor de producción y arriesgarme a afectar a usuarios externos; lo haría todo en un servidor “sandbox”.
Así pues, elegí uno de los servidores “sandbox”, lo preparé, le conecté un depurador, reproduje el error, lo investigué y, finalmente, dejé todo como estaba antes. Tras mi investigación confirmé que, como yo pensaba, era un problema de caché, pero no el problema de caché que yo me esperaba.
Según mi teoría, el programa acudiría a la caché para obtener el fichero con la traducción al chino tradicional pero la caché respondería con el fichero incorrecto. Mi intención era interrumpir el programa justo antes de que solicitara el fichero XML y ver qué ocurría. Para mi sorpresa, la caché funcionó correctamente: el programa solicitó el fichero con la traducción al chino tradicional y la caché proporcionó la traducción al chino tradicional, tal como debía ser. Obviamente, el problema tenía que estar en otro sitio.
Después de obtener la traducción, el programa la aplicó al gadget. En los gadgets con traducciones, el fichero de definición no incluía ningún texto en ningún idioma; en su lugar, incluía unas marcas que el servidor sustituía por el texto que aparecía en el fichero de traducción. Y, en efecto, eso es lo que hizo el servidor: tomó el fichero XML de definición del gadget, buscó las marcas y, dondequiera que hubiese una marca, la sustituyó por el correspondiente texto en chino tradicional.
El siguiente paso era interpretar el fichero XML resultante.
Había muchísima gente que utilizaba el gadget de conversión de unidades traducido a caracteres chinos tradicionales. Esto significa que, después de sustituir las marcas por texto en chino, el servidor tendría que interpretar el mismo código XML resultante una y otra vez. Ya que el servidor tenía que interpretar el mismo código varias veces al día, éste utilizaba una caché para ahorrarse todo ese trabajo redundante, y hasta ese momento yo no tenía ni idea de que esa caché existía.
Ésta era la caché que daba el resultado incorrecto: la caché recibía el fichero XML con textos en chino tradicional y devolvía el resultado de interpretar el mismo fichero XML con textos en chino simplificado.
Lo que necesitaba determinar era por qué sucedía eso.
Las cachés funcionan asociando una clave a un valor. Por ejemplo, la primera caché de la que hablé en este artículo, que evitaba tener que descargar e interpretar ficheros XML repetidamente, usaba el URL del fichero como clave y el fichero interpretado como valor.
La nueva caché, que se utilizaba para evitar tener que interpretar
repetidamente ficheros de definición con una traducción aplicada, usaba
de clave el fichero XML representado como un array de bytes. Para obtener
la clave, el servidor llamaba a la función
String.getBytes()
,
que convierte una cadena a un array de bytes utilizando la codificación
predeterminada.
En mi ordenador, la codificación predeterminada era UTF-8. Esta codificación
representa cada carácter chino en dos o tres bytes. Por ejemplo, UTF-8
representa el texto “你好” como los bytes
{0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd}
.
En los servidores, sin embargo, la codificación predeterminada era US-ASCII.
Esta codificación es muy antigua (1963) y sólo contempla el alfabeto latino
utilizado en el idioma inglés, por lo que no puede codificar los caracteres
chinos. Cuando getBytes()
encuentra un carácter que no puede codificar,
lo sustituye por un signo de interrogación. Por lo tanto, el texto “你好”
queda convertido en “??”.
Y aquí estaba el problema: cuando el servidor, que utilizaba US-ASCII, generaba una clave, ésta consistía en un fichero XML con cada carácter chino sustituido por un signo de interrogación. Como las traducciones al chino simplificado y chino tradicional usaban el mismo número de caracteres, aunque éstos fuesen distintos, las claves resultaban ser idénticas, por lo que el servidor respondía con el valor que había en la caché aunque fuese en la variante de chino incorrecta.
En cambio, este problema no era reproducible en mi ordenador, ya que usaba UTF-8, que sí contempla los caracteres chinos. Por lo tanto, las claves eran diferentes y la caché respondía con el valor correcto.
Después de varias semanas probando esto y aquello, inspeccionando el
código, batiéndome en vano con la caché y, finalmente, tomando medidas
desesperadas, la solución a este error consistió en sustituir todas
las llamadas a getBytes()
para usar la codificación UTF-8 de forma
explícita.
Esta historia comenzó como una intriga de espionaje a nivel internacional y terminó cambiando una función para especificar la codificación. Supongo que es un final un poco anticlimático, pero al menos todos los miembros del equipo estábamos contentos de no tener que acudir a testificar al Congreso de los EEUU ni nada parecido.
Aún así, este episodio grabó a fuego en mi mente la importancia de especificar siempre todos los parámetros de los que depende el programa y no dejar nada implícito o dependiente del entorno. Nuestro servidor falló porque dependía de un parámetro de configuración que era diferente en nuestros ordenadores y en producción; si hubiésemos especificado la codificación UTF-8 de forma explícita desde el principio, esto nunca nos habría pasado.
Y yo no tendría una historia tan chula para contar.
No se puede tener todo.