Xochitl está a veces bajo ataque. En general son ataques idiotas que tratan de entrar por SSH usando combinaciones de usuario/clave del tipo “root/root” o “user1/user1”; evidentemente eso casi nunca funciona, y además esos ataques son automáticamente detenidos después de tres intentos fallidos con denyhosts.
Esos ataques no me dan problemas; me dan problemas los ataques dirigidos específicamente contra mi blog y/o galería en línea. No porque alguna vez hayan logrado nada (los mantengo actualizados); el problema es que a veces generan una cantidad tal de solicitudes que Apache comienza a sobrecargar MySQL, la base de datos queda trabada, y Apache entonces se queda atorado sirviendo páginas. Si los atacantes solicitan muchísimas solicitudes a la vez, esto causa que MySQL quede atorado con cientos de consultas en su cola, y por lo tanto que Apache quede atorado con cientos (o miles) de páginas que quieren ser servidas.
Como Apache trata de no tirar conexiones, y cada una de ellas utiliza procesador, esto hace que el CPU de Xochitl de repente se encuentre utilizado al 117%. Aquí es donde debo mencionar que Xochitl es una pobre Pentium 4 a 2.40 Ghz; es posible (y de hecho probable) que la mayoría de los teléfonos celulares inteligentes que han salido este año sean más rápidos (y tengan más memoria) que Xochitl.
De todo lo anterior no es esta entrada.
Esta entrada es acerca de una situación que encontré mientras buscaba qué poder hacer para aliviar a la pobre de Xochitl. La más sencilla es ver qué IPs están solicitando más conexiones HTTP, y agregarlas a /etc/hosts.deny (teniendo cuidado de no negarme acceso a mí y mis máquinas, o a los robots rastreros de Google). Suele funcionar; sobre todo considerando que estos “ataques” (la verdad ya no sé si son ataques o sólo lectores ligeramente stalkeadores de mi blog/álbum) no ocurren muy seguido.
Así que hice un programita que leyera los logs (o bitácora, si quisiera usar español correcto) de acceso de Apache, sacara las IPs, y contara cuántas veces aparece cada una. Como lo primero que aparece en cada línea es la IP solicitante seguida de un espacio, con el siguiente comando obtengo todas las IPs que solicitan páginas a Apache:
cat /var/log/apache2/access_log | cut -d " " -f 1
Hasta ahí vamos bien; ahora, ¿cómo saco de ahí cuántas veces se repite una IP?, porque sabiendo eso ya puedo saber cuáles IP solicitan un número ridículo de conexiones. Siendo, como soy, programador, escribí un programita que hiciera esto por mí. Lo escribí en Python, porque lo quería rápido, y esto me salió:
#!/usr/bin/env python
import sys
if __name__ == '__main__':
ips = {}
for line in sys.stdin.readlines():
line = line.strip()
if line in ips.keys():
ips[line] = ips[line] + 1
else:
ips[line] = 1
for ip in ips.keys():
print('%d: %s' % (ips[ip], ip))
Esas son 14 líneas de Python, incluyendo el shebang y dos líneas en blanco. El programa lee línea a línea la entrada estándar, y usa un hash table para ir contando cada aparición de una IP.
Muy contento con mi programa lo corrí… y el maldito programa corrió, y corrió, y corrió, y siguió corriendo. Al minuto lo detuve, incrédulo de que pudiera ser tan endiabladamente lento. Lo revisé, lo puse a imprimir resultados intermedios, y el resultado era el mismo: es lentísimo.
Estúpido Python.
Me subí las mangas y lo reescribí en C, usando glib porque no me iba a a poner a escribir mi propio hash table (been there, done that). Esto me salió:
#include <stdio.h>
#include <string.h>
#include <glib.h>
typedef struct _integer integer;
struct _integer {
int n;
};
static integer*
integer_new(int n)
{
integer* i = g_new(integer, 1);
i->n = n;
return i;
}
static char*
read_line(FILE* file)
{
char line[4096];
int i = 0;
line[i] = (char)0;
int c;
while (TRUE) {
c = fgetc(file);
if (c == EOF || c == NEW_LINE)
return strdup(line);
line[i++] = c;
line[i] = char(0);
}
return strdup(line);
}
void
print_key_value(char* key, integer* value, gpointer user_data)
{
printf("%d: %s\n", value->n, key);
}
int
main(int argc, char* argv[])
{
GHashTable* h;
h = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
do {
char* line = read_line(stdin);
if (!strcmp(line, "")) {
free(line);
continue;
}
char* key;
integer* value;
if (g_hash_table_lookup_extended(h, line, &key, &value)) {
value->n++;
} else {
value = integer_new(1);
g_hash_table_insert(h, line, value);
}
} while (!feof(stdin));
g_hash_table_foreach(h, print_key_value, NULL);
g_hash_table_destroy(h);
return 0;
}
Esas son 65 líneas en C, incluyendo la definición medio redundante de una estructura integer porque no quise usar las macros GINT_TO_POINTER y GPOINTER_TO_INT. No es elegante.
Ya que tuve mis dos versiones, según yo, equivalentes, las corrí ambas. La salida que producen es idéntica, así que me parece que sí son equivalentes. La versión en Python tarda más o menos 1 minuto 58 segundos (más/menos dos segundos, en todas las ocasiones en que lo corrí). La versión en C tarda 0.285 segundos, consistentemente debajo de 0.290. Esto para una bitácora de 95,080 líneas, de 12 Megabytes de tamaño.
La versión en C es unas 5 veces más larga en líneas que la de Python (de hecho 4.333, pero no importa), además de que las líneas tienen más caracteres; y sin embargo tarda (en tiempo de ejecución) del orden de 400 veces menos.
Ahí está el código si alguien quiere tratar de mejorar el resultado en Python. Yo estoy sumamente decepcionado; creí que las hash tables de Python estaban decentemente optimizadas: estoy usando cadenas como llaves al fin y al cabo. Y lo peor es que la versión en C ni siquiera me tomó mucho más tiempo en escribir.
Actualización: Gracias a Omar, ya vi que estaba cometiendo un errosote al buscar la llave en la hash table de Python; no tenía porqué buscarla en .keys() cuando puedo hacerlo directamente en la tabla. Con la sugerencia de Omar, el código Python es solamente el doble de lento que el código C. Lo que de hecho tiene sentido.
Imprimir entrada