PHP, sesiones y alta concurrencia

Hoy voy a escribir brevemente sobre cierta problemática asociada al manejo de sesiones en PHP que surge cuando nuestra aplicación web debe soportar un alto nivel de concurrencia.

PHP guarda las sesiones en archivos por defecto, aunque hay otros mecanismos posibles: por ejemplo usar bases de datos como MySQL (o bien SQLite), tirar de Memcache, guardar la información cifrada dentro de cookies, etc.

Ninguno de estos sistemas alternativos está exento de problemas. Las bases de datos al uso añaden una latencia inaceptable en la mayoría de casos, SQLite permite lecturas muy rápidas, pero como contrapartida bloquea la base de datos entera cuando se escribe, Memcache puede sobrecargar nuestra memoria y tiene el riesgo potencial de pérdida de datos por la volatilidad de su sistema de almacenaje, y por último, guardar las sesiones en cookies aumenta ligeramente el riesgo de robo de datos y puede añadir un overhead importante a todas las peticiones HTTP.

¿Y qué pasa con las sesiones en ficheros? Pues también tienen su problemática. Cada petición HTTP llega acompañada de la cookie con el ID de sesión, cuando se llama al método session_start se abre un descriptor de fichero que guarda la información asociada a la sesión con el ID ya mencionado y... se bloquea el archivo para poder escribir sobre él los cambios que hagamos en las variables de sesión sin que haya conflictos entre peticiones distintas que usan la misma sesión.

El bloqueo que menciono desaparece por defecto en cuanto finaliza la ejecución del script PHP. Esto significa que si en cierto sitio web realizamos (por ejemplo) 5 llamadas AJAX de forma paralela a scripts que hacen uso de la información de sesión, el resultado que obtendremos es una serie de respuestas serializadas en el tiempo, una detrás de otra.

Una solución parcial para conseguir aumentar el nivel de paralelización de las llamadas AJAX (o cualquier otro tipo de petición HTTP) es desbloquear el archivo de sesión lo antes posible, es decir, cuando sepamos que ya no vamos a modificar las variables de sesión y solo vamos a realizar operaciones de lectura. Para ello podemos usar la función session_write_close, en algunos casos se puede conseguir una reducción impresionante de los tiempos de carga, por lo que os recomiendo tenerlo muy en cuenta.

Otro detalle importante: es desaconsejable permitir que PHP inicie la sesión de forma automática (es preferible iniciarla solo cuando sea estrictamente necesario). Si podéis, intentad que la opción session.auto_start esté a false en el fichero de configuración php.ini.

Espero que os haya servido :) .

Enviar archivos mediante HTTP desde PHP

A raíz de mi trabajo en Bananity recientemente he estado trasteando con la subida de archivos al servidor (hasta ese momento nunca había tenido que tratar con eso) y el envío de archivos entre servidores. Y habiéndome parecido interesante lo aprendido estos dos días he creído conveniente comentarlo por aquí :) .

En primer lugar comentaré la parte más típica: cómo hacer un formulario en HTML que permita archivos y como recibir archivos desde PHP.

En segundo lugar comentaré la parte que a mí me ha parecido más interesante (aunque nada complicada): enviar archivos a otros servidores desde PHP.

Enviar archivos desde un formulario HTML

Enviar un archivo a un servidor web desde un formulario HTML es muy sencillo, resumiendo mucho: se tiene que usar el método POST, usar un elemento input de tipo file, y añadir la propiedad enctype al elemento form con el valor "multipart/form-data". Ejemplo:

1
2
3
4
<form method="post" enctype="multipart/form-data" action="http://sitiodeproceso">
    <input type="file" name="filetoupload">
    <input type="submit" value="Envia!">
</form>

Recibir archivos usando PHP

Muy bien, el visitante de nuestro sitio web ya nos ha enviado su archivo... ¿como lo procesamos para guardarlo? PHP nos provee de unas cuantas variables globales bastante útiles, entre las que se encuentra $_FILES, que es la que utilizaremos. El servidor se encargará de dejar el archivo subido en un directorio temporal, y $_FILES nos dará información sobre su ruta y posibles errores en el proceso de envío.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
 
// Notemos que uso como índice el nombre que le dí al elemento input
// en el formulario HTML
$fileinfo = $_FILES['filetoupload'];
 
// Obtenemos la ruta donde está guardado temporalmente el fichero
$filepath = $fileinfo['tmp_name'];
 
// Recuperamos el nombre que tenía el fichero cuando se envió
$filename = $fileinfo['name'];
 
// Movemos el fichero a donde creamos conveniente
move_uploaded_file ($filepath, '/ruta/donde/queremos/guardarlo/'.$filename);
 
?>

En este ejemplo no he tenido en cuenta la gestión de errores ni otros valores de $_FILES['filetoupload'] interesantes como type, error o size.

Enviar archivos con PHP (1ª opción: CURL)

Ahora sí, llegamos a la parte que me parecía más interesante :) , aunque empezaré por la "vía complicada": usar CURL. Este ejemplo lo he extraído de [1].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_VERBOSE, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_USERAGENT, "AgenteQueNosVengaEnGana");
    curl_setopt($ch, CURLOPT_URL, 'http://urlhaciaotroservidor');
    curl_setopt($ch, CURLOPT_POST, true);
 
    // Igual que <input type="file" name="filetoupload">
    // Notad la @ antes del path, es clave.
    $post = array(
        "filetoupload"=>"@/path/to/myfile.jpg",
    );
 
    curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
    $response = curl_exec($ch);
?>

La verdad es que puede ser un poco engorroso subir archivos siguiendo este procedimiento, no porque sea mucho código, sino porque se tienen que memorizar (o tener a mano) que parámetros debemos establecer y con que valores.

Evidentemente siempre nos queda la opción de encapsular un código similar a éste dentro de un método, pero a mi este "problema" me servirá como excusa para introducir una solución alternativa :D .

Enviar archivos con PHP (2ª vía: Requests)

Gracias a mi compañero Carles Iborra descubrí la biblioteca libre Requests, a la que actualmente estoy dando muchísimo uso, especialmente para realizar testeo unitario, aunque también para enviar archivos usando PHP. El siguiente ejemplo lo dice todo.

1
2
3
4
5
6
7
8
9
10
11
<?php
 
include('Requests.php');
Requests::register_autoloader();
 
$post = array(
    'filetoupload' => '@/path/to/file'
);
$request = Requests::post('http://dondeenviamoselarchivo', array(), $post);
 
?>

Referencias

  1. DTBAKER: Uploading a file using curl in PHP

Mejorar eficiencia en peticiones AJAX

JSON logoLa mayoría de desarrolladores de aplicaciones web desean que éstas sean usadas por miles de personas (y si es a la vez, mejor). Cuando se llega a un determinado número de usuarios, es difícil que la aplicación aguante esa carga si no se han tomado ciertas decisiones. La más simple (y probablemente más cara) pasa por añadir más prestaciones a los servidores con los que trabaja nuestra app, pero evidentemente podemos encontrar soluciones ligeramente mejores.

Hoy en día es extraño encontrar una aplicación web que no haga uso de AJAX, unas lo hacen de manera inteligente y otras no tanto. Vamos a ver qué podemos hacer para reducir un poco(en caso de no haberlo hecho ya) la carga que deben soportar nuestros servidores.

El consejo básico es simple: es preferible que el contenido dinámico sea generado en el lado del cliente con Javascript a que el trabajo lo hagan nuestras máquinas. Es claro que no siempre se podrá hacer eso, pero por lo general sí, como ejemplo tenemos datos que se actualizan periódicamente o que se estructuran de forma tabular o en listados.

La justificación del consejo radica en 2 puntos, en primer lugar podemos reducir el tiempo de CPU en el servidor consumido por petición, en segundo lugar podemos reducir el ancho de banda usado si enviamos datos como JSON en vez de contenido HTML pregenerado. Así dicho suena creíble, pero tenemos que comprobarlo (me fijaré en el caso concreto de PHP).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
 
// Codigo que sirve petición con JSON
$link = mysql_connect('xxx', 'xxx', 'xxx');
 
mysql_select_db('xxx');
 
$dbresult = mysql_query('SELECT nick, passwd, name FROM json');
 
$outresult = array();
 
while ($outresult[] = mysql_fetch_row($dbresult));
array_pop($outresult);
 
echo json_encode($outresult);
 
mysql_close($link);
 
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
 
// Codigo que sirve la petición con HTML
 
$link = mysql_connect('xxx', 'xxx', 'xxx');
 
mysql_select_db('xxx');
 
$dbresult = mysql_query('SELECT nick, passwd, name FROM json');
 
while ($row = mysql_fetch_row($dbresult)) {
    echo '<tr><td>',$row[0],'</td><td>',$row[1],'</td><td>',$row[2],'</td></tr>';
}
 
mysql_close($link);
 
?>

Antes de hablar sobre como se procesa la petición en el lado del cliente haré algunas observaciones. Aunque en este código no está, añadí código para medir cuantos milisegundos tardaba cada script en ejecutarse. Para aumentar la fiabilidad de las pruebas conté solo el tiempo que se tarda en procesar el resultado de la consulta a la base de datos y enviarla. Así pues excluyo el acceso a la base de datos y el cierre de la conexión de las medidas efectuadas, que son las siguientes (había 200 registros en la base de datos):

  • Petición servida en JSON (10 muestras):
    • Número de bytes enviados: 18830
    • Pico de memoria consumida: 305128 bytes ~ 297.976 Kb
    • Tiempo mínimo: 0.62799 ms
    • Tiempo máximo: 1.2610 ms
    • Tiempo medio: 0.9064 ms
    • Desviación típica: 0.1436 ms
  • Petición servida en HTML (10 muestras):
    • Número de bytes enviados: 23854
    • Pico de memoria consumida: 100008 bytes ~ 97.664 Kb
    • Tiempo mínimo: 0.7529 ms
    • Tiempo máximo: 2.4579 ms
    • Tiempo medio: 1.2909 ms
    • Desviación típica: 0.4956 ms

Lo más destacable de estas medidas es el número de bytes enviados, con JSON podemos ahorrar (como mínimo) un 21.06% de ancho de banda. Digo como mínimo porque en este ejemplo el código HTML generado es más bien austero, ya que no incorpora clases CSS ni identificadores para facilitar el manejo del DOM.

El segundo dato nos muestra la cantidad máxima de memoria consumida durante la petición procesada. Como era esperable, la primera versión del código consume más memoria debido al array que va construyendo y a la cadena JSON que ensambla finalmente a partir de la lista generada. En este punto hay que tener en cuenta que, si somos listos, esto no tiene por qué suponer un gran problema ya que ese mismo array podríamos (y de hecho es recomendable) guardarlo usando (por ejemplo) Memcached de forma que ahorramos posteriores consultas a la base de datos.

Respecto a los otros datos tengo que decir que no son significativos en lo tocante al asunto JSON vs HTML, pues la comparativa no es del todo justa. Sin embargo, lo que sí podemos ver es qué supone usar repetidas veces la primitiva echo frente a un solo uso. Por un lado aumentan los tiempos de ejecución, y por otro aumenta la dispersión de las medidas (ver desviación típica), lo que puede tocar un poco las narices cuando queremos hacer cálculos de cara a escalar nuestra infraestructura.

Otra versión del código que genera HTML es la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
 
$link = mysql_connect('xxx', 'xxx', 'xxx');
 
mysql_select_db('xxx');
 
$dbresult = mysql_query('SELECT nick, passwd, name FROM json');
 
$outresult = '';
 
while ($row = mysql_fetch_row($dbresult)) {
    $outresult .= '<tr><td>'.$row[0].'</td><td>'.$row[1].'</td><td>'.$row[2].'</td></tr>';
}
 
echo $outresult;
 
mysql_close($link);
 
?>

Los datos que arroja esta versión son los siguientes:

  • Petición servida en HTML (2ª versión, 10 muestras):
    • Número de bytes enviados: 23854
    • Pico de memoria consumida: 126432 bytes ~ 123.469 Kb
    • Tiempo mínimo: 0.3531 ms
    • Tiempo máximo: 0.8111 ms
    • Tiempo medio:  0.5435 ms
    • Desviación típica: 0.1577 ms

Podemos extraer algunas conclusiones más de esta tercera lista de cifras. En primer lugar se nos podría ocurrir adoptar el mismo enfoque para la generación de JSON, es decir, olvidarnos de construir un array y pasar a construir un string directamente (de hecho obtendríamos mejores resultados, pues la cadena JSON es más corta que la cadena HTML). Si no vamos a usar más esos datos en un lapso de tiempo breve parece lo más acertado, pero si tenemos planeado acceder a ellos otra vez puede que sí nos convenga construir la lista como en el primer ejemplo de código.

Un detalle interesante es que teniendo un array probablemente sea más rápido generar un texto JSON con la función json_encode que concatenando strings en un bucle. El problema que tenemos (en este caso) es que el driver nativo de MySQL para PHP no permite obtener una lista con todas las filas directamente, y consecuentemente nos vemos forzados a construirla nosotros (que es menos eficiente que usar un método nativo de PHP programado en C, y de ahí que el tercer ejemplo rinda mejor que el segundo).

Tengo que admitir que no conozco por qué no hay un método nativo de PHP para obtener en forma de array todos los resultados de una consulta a una base de datos MySQL, pero creo que, en caso de no haber razones de peso... puede ser una buena jugada implementar en C una extensión del lenguaje que haga precisamente eso para ahorrar un valioso tiempo de ejecución.

En el lado del cliente las diferencias no han sido estadísticamente significativas para mi ejemplo, aunque es de esperar que los tiempos de descarga sean menores con JSON, mientras que los de proceso sean menores con HTML. El tiempo total dependerá de la potencia de cómputo del cliente y del ancho de banda de la conexión, por lo que algunos usuarios saldrán beneficiados mientras que otros puede que tarden algunos milisegundos más en obtener la página web completamente renderizada.

Sin más rodeos, resumen rápido: JSON permite reducir el ancho de banda consumido en más de un 21%, hacer un solo echo reduce el tiempo de ejecución (la reducción puede sobrepasar fácilmente el 50%, en el caso del ejemplo es un 57.89%) y la dispersión estadística de esos mismos tiempos (lo que nos puede ayudar a realizar mejores cálculos para escalar la plataforma o puede servir para que los balanceadores de carga tengan datos más fiables con los que trabajar).

Como apunte final solo quiero destacar que es importante realizar experimentos y comprobar los hechos por uno mismo, pues la tecnología evoluciona constantemente y es probable que ciertas afirmaciones no se puedan sostener en el tiempo. Además me he dejado algunas técnicas en el tintero, como usar los métodos ob_start y ob_end_clean, que nos permiten aprovechar el buffer de escritura para evitar la necesidad de concatenaciones. Supongo que viendo la extensión del artículo comprenderéis que no lo haya comentado en profundidad. Espero que os haya sido útil :) .

Añadir filtro de búsqueda en una tabla con jQuery

Gnome System Search IconPara completar el anterior artículo que escribí sobre ordenación de tablas con jQuery [1] hoy os hablaré sobre la creación de un filtro de búsqueda para nuestras tablas, algo prácticamente imprescindible hoy en día en multitud de aplicaciones de gestión que presentan parte de sus datos de forma tabular.

El código que veréis a continuación es bastante sencillo, no permite búsquedas con operadores lógicos (AND, OR, NOT), ni que se restrinjan a columnas concretas, sin embargo sí que aprovecha toda la potencia de las expresiones regulares para realizar búsquedas relativamente complejas. Para mi gusto es más importante la característica de los operadores lógicos y las restricciones a columnas concretas que la posibilidad de usar expresiones regulares, pero creo que no vale la pena complicar el artículo por algo que no tiene demasiada miga a nivel técnico.

El código es tal que así:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function setup_searchable (selector) {
	var search_input = $("#"+selector+"-searcher");
 
	search_input.keyup ((function () {
		var tbody = $("#"+selector+"-table").find('tbody');
		var hidden_rows = tbody.find('tr');
 
		var searcher = function () {
			var search_text = search_input.val().toUpperCase();
 
			tbody.html('');
 
			var shown_rows = hidden_rows.filter (function () {
				var re = new RegExp (search_text),
					 matched = false;
 
				$(this).find('td').each(function (i2) {
					matched = matched || ($(this).text().toUpperCase().match(re) != null);
				});
 
				return matched;
			});
 
			tbody.append (shown_rows);
		};
 
		return searcher;
	})());
}

En este ejemplo trabajamos con dos elementos básicos, un elemento input de tipo texto y una tabla. El código asume que el selector se referirá a su identificador único id, y que los respectivos ids tendrán una estructura del tipo selector-searcher para el elemento input, y selector-table para la tabla.

Sobre el código, puede ser interesante notar que uso el evento keyup en vez del evento change, esto permite que la tabla se actualice en tiempo real, en contraposición a lo que sucedería con change, que necesita que apretemos la tecla Enter y cambiemos o cambiemos el foco de nuestro cursor. Otro detalle importante es que guardamos en memoria la totalidad de las filas antes de efectuar el filtrado, de modo que evitamos perderlas, y cuando cambiamos los criterios de búsqueda no tenemos que recargar ningún dato desde el servidor. Este último hecho implica que conviene llamar a la función setup_searchable cada vez que recarguemos la tabla con nuevos datos desde el servidor, pues de lo contrario nuestro buscador trabajará con datos "antiguos".

Algunas mejoras obvias para este código aparte de las mencionadas anteriormente serían las siguientes:

  • Posibilidad de trabajar con selectores más generales (lo que puede permitir, por ejemplo, tener dos campos de búsqueda para una misma tabla, uno en la cabecera y otro en el pie).
  • Añadir un parámetro callback que permita actualizar ciertos datos no mostrados en la tabla en función de lo que haya filtrado nuestra búsqueda. Un ejemplo muy simple sería la actualización de un contador, o en casos más complejos podríamos estar tratando datos como totales, medias, varianzas, etc.

Espero que esto le resulte útil a alguien, agradeceré cualquier comentario y propuesta de mejora, saludos! :)

  1. Ordenación de elementos del DOM con Javascript y jQuery

Ordenación de elementos del DOM con Javascript y jQuery

Shellsort gráfico (from Wikipedia)Hace unos días tuve que lidiar con un problema común en el diseño de aplicaciones web: ordenar bajo demanda las filas de una tabla según el valor de una columna concreta. Evidentemente lo primero que hice fue buscar por Internet alguna solución que me permitiera no tener que programar demasiado... y tengo que decir que lo encontré, pero por pequeños detalles nada de eso me sirvió.

Así que en este artículo explicaré el código que acabé picando yo por mi cuenta. Lo que he escrito depende de jQuery, aunque en realidad se puede sustituir por su "subconjunto" Sizzle, o incluso por la microbiblioteca Zepto.JS (que tiene una sintaxis compatible con la de jQuery).

Vayamos pues al código directamente, he creado un fichero llamado tablesorter.js en el que está todo el código necesario, salvo la llamada a una función, que se hace allí donde nos convenga :) .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
function row_comparer (row1, row2, index) {
	var r1 = $(row1).find('td').filter(function(){
                        return $(this).index() === index;
        }).text().toUpperCase();
	var r2 = $(row2).find('td').filter(function(){
                        return $(this).index() === index;
        }).text().toUpperCase();
 
	return  (r1 > r2)? 1:
	       ((r1 < r2)?-1:0);
}
 
function shsort (list, comparer, order) { // Shell sort (with Marcin Ciura's gaps)
	var gaps = [701, 301, 132, 57, 23, 10, 4, 1],
	    s = list.length,
	    t;
 
	for (var k=0; k<8; k++) { // 8 is gaps size
		for (var g = gaps[k], i=g; i<s; i++) {
			t = list[i];
			for (var j = i; j >= g && order*comparer(list[j-g], t) == 1; j-= g) {
				list[j] = list[j - g];
			}
			list[j] = t;
		}
	}
}
 
function sort_table (table, index, order) {
	var tbody = table.find ('tbody');
	var rows = tbody.find('tr');
 
	var comparer = function (row1, row2) {
		return row_comparer (row1, row2, index);
	}
 
	shsort (rows, comparer, order);
 
	tbody.html('');
	tbody.append (rows);
}
 
function setup_sortable (selector) {
	$(selector).each (function (i1) {
		$(this).find('th').each (function (i2) {
			var order = 1;
 
			$(this).click ( function () {
				sort_table ($(this).closest('table'), i2, order);
 
				order *= -1;
			})
		});
	});
}

Supongo que algunos de los que leéis esto preferiríais que hubiese creado un plugin para jQuery, siento decir que no es así. Eso no es por ninguna razón en particular, simplemente pasa que todavía no me he tomado la molestia de aprender como se hace, y no sé si es complicado o no, supongo que en breve habrá una versión en forma de plugin para jQuery :) .

En cuanto a como usarlo, la función setup_sortable debe ser llamada utilizando como parámetro un selector para las tablas a las que queramos aplicar ordenación "automática" (con este script las tablas se ordenan cuando se hace click en los elementos th de la tabla, básicamente las cabeceras). En mi caso añadí la clase sortable a todas las tablas que quería añadir esta característica y usé el selector ".sortable".

En cuanto al código, supongo que es destacable que hago un uso intensivo de las funciones anónimas y las clausuras, aunque no es nada que deba sorprender a nadie. En mi humilde opinión, lo más interesante es el algoritmo de ordenación, un "simple" Shell sort (basado en Insertion Sort).

Escogí este algoritmo por las siguientes razones:

  • Es estable, es decir, se mantiene el orden relativo entre elementos con la misma clave (el valor que se usa para realizar las comparaciones) que había en la lista antes de la ordenación. Esto es útil cuando queremos ordenar por distintos criterios a la vez.
  • Es de fácil implementación, solo tenéis que ver el código. Aquí puede parecer un poco más complicado porque uso la función comparer en vez del típico operador de comparación, pero no es una gran dificultad.
  • No usa recursión, ni pilas: los requisitos de memoria son pequeños y constantes.
  • Dependiendo de la implementación (y en particular con esta se consigue), se tiene que para ordenar una lista se deben realizar solo una cantidad de operaciones del orden de O(n·log²(n)).
  • Se trata de una rara avis, le cogí cariño. Aunque no se trata de un algoritmo destacable por su rendimiento, resulta que es tremendamente complicado a nivel teórico. Tanto que su velocidad depende fuertemente de una serie de parámetros de los que casi no se sabe nada a día de hoy. Yo he usado los de Marcin Ciura, que hasta donde yo sé, son los mejores encontrados hasta el momento.

Bien, hasta aquí el apunte de hoy :) . Espero que os pueda resultar útil, agradeceré cualquier comentario.. y si a alguien le interesa, estoy abierto a aceptar ayuda en la jqueryzación de esta minilibrería. Saludos!