¿Entrando en la "optimización extrema"?

Siempre fui renuente a todos los artículos sobre "optimización extrema", ya que en PHP es muy poco probable que sirva hilar tan fino como para decir que:

++$a es más rápido que $a++ (aunque encontraremos opiniones opuestas)

Que foreach es más lento que un for, o que para imprimir varios valores en un echo es "más rápido" hacerlo con "," que con ".", etcétera.

No será la primera vez que diré que me parece una locura este tipo de optimizaciones, no tienen sentido en un ambiente web, en un entorno LAMP, y particularmente para PHP... a menos que estemos usando este lenguaje para hacer grandes "cálculos científicos" en memoria, pero estimo que si necesitas esto, erraste el entorno y el lenguaje para hacerlo.

"Optimizaciones Reales"


Para el que no lo sepa, en la actualidad trabajo durante la semana en una empresa argentina relacionada con productos / servicios vinculados a los celulares, y nuestra principal tarea es -muy resumidamente- enviar y recibir SMS (aunque ahora estamos empezando a trabajar con MMS).

También desarrollamos sistemas de administración y estadísticas para nosotros y para nuestros clientes (que son sistemas que ahora se hacen mayormente con Zend Framework), pero para estos sistemas nuestro tráfico es "reducido y controlado", lo opuesto a un servicio público como podría ser una red social donde los usuarios van creciendo día a día.

En este caso, de los sistemas web comunes, la "optimización extrema" no tienen ningún tipo de efecto, más que intentar hacer las cosas bien dentro de los "parámetros conocidos" y muchas veces estamos hablando de hacer las consultas sql de forma correcta (algo que no es tampoco común para la media de los desarrolladores PHP).

Pero en el primer caso, nosotros somos una empresa que podemos "estar orgullosos" que hacemos el envío de SMS/MMS a través de un ambiente web que incluye Apache, MySQL y PHP (aunque muchos podría discutir que no es la mejor tecnología, tal vez sería mejor usar arquitecturas como Java y puedo llegar a estar de acuerdo con ellos).

Desde hace unas semanas estamos haciendo funcionar una aplicación masiva en la que existen miles de usuarios enviando miles de SMS para juntar puntos en modalidad de "trivia" (se envían preguntas y se deben responder con opciones como "A" o "B") para un concurso que durará unos meses. Cada MO (mensaje originado por el usuario) enviado equivale a una instancia de apache evaluando la información recibida, registrando todos los datos y respondiendo a continuación con un MT (mensaje originado por el sistema). El juego cambia de comportamiento de acuerdo al tipo de jugador, tiene incentivadores que motivan al jugador seguir en carrera, le informa los puntos, le envía nuevos desafíos, lo recaptura si deja de jugar por distintas unidades de tiempo, etc.

Y aquí es donde entra la optimización que yo le llamaría "extrema". Les comparto algunas prácticas que fui aprendiendo sobre la marcha para que las discutamos juntos a ver que les parece y si no son tan esotéricas como las "++$a versus $a++":

Evitar el acceso a la base de datos

Generalmente el "cuello de botella" de los servidores está en el acceso a disco, el consumo de procesador es mínimo, pero sí es crítico el acceso a la base de datos y el principal recurso es acceder al disco. Una decisión de diseño fue evitar en algunos casos acceder a una tabla para traer datos y los dejamos "estáticos" en un array dentro de una clase (evitamos conectar, consultar, traer datos, desconectar).

Desventajas: perdemos "flexibilidad" ya que en vez de tener un admin que acceda a una tabla hay que modificar el código en el fuente (en nuestro caso son datos que no cambiarán frecuentemente y más que flexibilidad buscamos rendimiento y que el sistema no quede sin recursos).

Evitar repetir el acceso a base de datos

Nuevamente, nuestro cuello de botella. Algo muy común cuando desarrollamos orientado a objetos es que cada objeto es independiente del otro, y si lo hacemos "3 capas", en algún momento cada objeto terminará accediendo a la base de datos por su cuenta.

Me ha pasado de implementar un log que activo con todas las consultas del sistema y ver que se repiten algunas consultas dentro del mismo acceso al sistema, una y otra vez, cuando en sí están trayendo la misma información.

Por ejemplo (el ejemplo no es real, pero lo que importa es el concepto), imaginen que tengo el nombre del usuario en la base de datos y cada vez que hago algo como $jugador->getNombre() hace un SELECT nombre FROM jugadores WHERE id = 100;. Como la instancia se usa en varias partes del sistema y requiere verificar información del nombre en distintas oportunidades, esta consulta se repite "n" veces. Para evitar esto se cambia la operativa por una "carga tardía" (lazy load), nunca cargar nada hasta que lo necesite (no cargues toda la información en el constructor del objeto), y cuando ya tenga el valor, y si es un valor que no tiende a cambiar/actualizarse, retornar su contenido sin ir nuevamente a la base (parece tonto, pero hasta que no lo detectas que lo estás haciendo no tomas conciencia de que debes evitarlo).

Antes lo podrías tener así:



Luego lo podrías cambiar por:



Mejorar el acceso a base de datos

Algo que es poco común que un desarrollador domine, hacer correctas consultas a las bases de datos, agregar índices según lo que necesitemos hacer, evitar los índices en los lugares que no conviene, verificar el tipo de cada campo y si realmente es el que necesitamos.

Algunas pautas que recuerdo ahora:
  • Revisa por qué campos haces normalmente las condiciones de tus WHERE y crea un índice (recuerda que las claves primarias generan implícitamente un índice).
  • Recuerda que por cada índice que agregues tu tabla se recarga si esta tienen que actualizar los datos constantemente, ya que hará un "insert" en la tabla real y luego deberá actualizar el índice (como si fueran dos tablas). Si tienes 100 índices, y es una tabla grande y con muchas actualizaciones, será mejor que te tomes el tiempo y veas cual puedes eliminar.
  • Diferencia las tablas de "lectura/escritura" sobre las de "solo lectura". Si tu tabla necesita hacer ambas y sabes que crecerá, limita la cantidad de índices (ver punto anterior), pero si es de solo lectura, puedes tomarte la libertad de agregar más índices que en el primer caso.
  • Usa los mismos tipos y largos en los campos de tus tablas: si creas jugador_id y es un int(11), que así sea en todas las demás tablas, ya que unir campos que son de distintos tamaños tendrá un costo de conversión al compararlos.
  • Diferencia lo que es un varchar de un char: si tus valores de texto serán fijos, es decir, tienes un campo de 100 caracteres que será probable que siempre esté lleno, usa char(100), si puede que esté lleno de caracteres o existan casos donde no tenga nada, o pocos (10 caracteres), define que es de tipo varchar (var = variable).
  • etc
En estos casos (bueno, siempre) el manual del motor de base de datos es tu amigo.

No tengas miedo de desnormalizar una base de datos

Lo aprendí con el tiempo, no ser extremista y purista, luego de aprender las reglas, descubre cuando se justifica romperlas para sacar más provecho ;-)

Nos pasó que ante un tráfico masivo de datos estábamos teniendo un crecimiento desmedido en tablas que registran la entrega y salida de SMS, y que cuando queríamos hacer un cruzamiento de datos entre otras tablas, las consultas nos generaban pérdidas de segundos muy valiosos.

Por ejemplo, si nosotros tenemos un sistema que maneja múltiples carriers, por lógica deberíamos tener en la tabla de participantes el número de celular y el número de carrier al cual corresponde. En sistemas donde las tablas de participantes pueden crecer exponencialmente no es conveniente que por cada MO tengamos que acceder a una tabla extra para saber esta información, por lo que en tablas importantes "desnormalizamos" su acceso y duplicamos estos campos para evitar tener que hacer un join contra una tabla extra (y así con otros datos).

Duplica campos en distintas tablas (número de teléfono, carrier, id de usuario, etc) para evitar tener que hacer joins contra una tabla que crece y se actualiza constantemente.

Evita acceder a los datos de producción para hacer consultas

Aunque puede ser obvio, evita tener un sistema de estadísticas para tu clientes que hagan consultas directas contra una base de datos en producción. Cualquier consulta compleja puede tirar el rendimiento de tu sistema. Puedes intentar duplicar la base en otro servidor una vez al día, o simplemente crear "tablas resúmenes" con un proceso que corre cada "x" horas y en vez de sumar todos los datos cada vez que se consulta, ya lo haces en la carga y el sistema de estadísticas muestra solo los totales.

Si quieres darte un lujo, solo accede a producción para dar datos "reales" con un "count", y si quieres acceder a tablas, que estas no sean las más usadas.

En resumen

Fuera que descarto que debemos diseñar bien nuestro sistema pensado cual será el contexto en el cual funcionará, pensar en las clases y sus relaciones, y ver si estamos programando correctamente, a menos que estemos hablando de muchas clases por cada vez que hay una invocación en nuestro sistema (estoy hablando de 50 clases o más), lo más probable es que la optimización no se deba hacer en el código PHP, probablemente esté más orientado hacia nuestra comunicación con la base de datos (y descarto que ya estamos usando algún sistema de caché del lado del servidor).

Sé que no es de todos los días tener este tipo de situaciones de cantidades masivas de tráfico, pero cuando ocurren, estos detalles afectan el rendimiento o directamente la estabilidad del servidor.

¿Tienes algún tips para compartir? ¿cual fue tu experiencia en estos casos? ;-)

Artículos relacionados:

13 comentarios:

Jorge Eduardo Olaya dijo...

Muy buen articulo, las recomedaciones tienen alto conocimiento, son aplicables en tosas las diferentes aplicaciones que desarrollemos.

Pero, no estoy de acuerdo con la desnormalizacion de una base de datos. Si el motor de la BD esta bien afinado no tiene porque generar perdidas de segundos en las transacciones. Para cambiar el conocimiento que tengo al respecto, tendría que probarlo en un laboratorio. Claro que no me cierro a la posibilidad de que esa hipótesis sea cierta.

Joan dijo...
Este comentario ha sido eliminado por el autor.
Joan dijo...

Yo sín embargo, y no por crear polémica, si creo fervientemente en la desnormalización de las bases de datos.

Evidentemente no en todos los casos, pero si en muchos de ellos. Creo que es un muy buen ejercicio para que la web realmente "vuele" en cuanto a cliente.

Acerca del artículo, muy bien explicado, quizá le faltó un poquito, sobretodo por lo que nos tienes acostumbrados... Muy bueno :)

Unknown dijo...

Voto también por la desnormalización en pro de la eficiencia, claro siempre que sea realmente justificada y probada. Posiblemente la marca del motor de BD, la versión y otros factores influyan a la hora de efectuar un cambio como este, se necesitaría un estudio más formal para adquirir argumentos más técnicos y contundentes.

Desde mi experiencia por lo menos con la versión 5 de MySQL, trabajando con tablas grandes (de más de 2 Millones de registros) y con consultas complejas, si claro que fue útil la desnormalización. Obviamente no fue que deje todo en una sola tabla jeje… solo lo necesario.

Y por el artículo, claro muy bueno. Muchas gracias por compartir ese tipo de experiencias que nos enriquecen a todos.

pepeluis76 dijo...

Muy buen articulo, solo añadir la siguiente web:
http://code.google.com/intl/es/speed/articles/optimizing-php.html

Respecto a la desnormalización no lo comparto del todo, una política menos agresiva sería crear una vista por cada posible consulta en la web. (crear un nivel intermedio por así decirlo y dejar a bajo nivel la base de datos normalizada)

Edgar dijo...

Tremendo articulo Enrique !!!

Como duda, la siguiente, en aplicaciones de escritorio (S.O. Windows+aplicaciones Cliente Microsoft), es totalmente normal y recomendado hacer uso de Procedimientos almacenados, mas no trabajar en el lado del cliente con sentencias SQL (quemadas en codigo)

En el Web y para aplicaciones como por ejemplo, en mi pais que cada afiliado al seguro social crea solicitudes para prestamos hpotecarios, quirografarios, etc, donde la concurrencia es tal, que ha terminado por bloquear el servidor.

En fin en este tipo de aplicaciones ( y en cualquiera), optimizar sería tambien utilizar procedimientos almacenados o sentencias sql del lado del cliente?

Experiencias con esto ??

Josepzin dijo...

Muy interesante

AV4TAr dijo...

Hablando de desnormalizar / optimización en el acceso a la base de datos.

En un proyecto que estoy trabajando, se hacen muchísimas busquedas, sobre bases de datos realmente enormes. En estos casos no otra opción que usar un motor de busqueda full text como sphinx o lucene.

Se va a notar de forma increible el aumento de rendimiento.

Anónimo dijo...

Bueno, la verdad que estoy en contra de lo que decis.

1- Desnormalizar la Base de Datos, si bien puede traer una mejora en el rendimiento lo hacemos a costa de que en el futuro sea más complicado mantener el sistema y ni hablar de si en más adelante hay cambios de requisitos. Puede ser un probema, Realmente creo que un buen Modelo de objetos cohesivo puede ganar en ventajas.

2- La optimización extrema, en parte tenes razón, ningun sistema va a cambiar por si pusimos $i++ o ++$i. Aunque puede cambiar en por ejemplo "$a$b$c Hola $d" y $a.$b.$c.' Hola '.$d
En fin son muchos tips de optimización y dependen mucho del caso. El caso es lo que los justifica.

Josepzin dijo...

A mi me gusta aplicar esos chiches de echo 'xxx:' , $a; y tonterías de esas, pero soy consciente que mi programa no va a ir mas rapido por eso, simplemente me gusta usar lo que se supone es lo correcto.

Sobre desnormalizar las BD, mis proyectos nunca fueron tan grandes como para nececitas hacerlo, llegado el caso si es una solucion...

Isra dijo...

Hola, interesante post. Me parece sensata tu manera de trabajar (no reinventar la rueda, etc), pero me extraña que un programador tan "enterprise" como tú...

a) Siga haciendo consultas SQL. Por qué no utilizas alguna tecnología de persistencia ORM o congelación de objetos (http://www.slideshare.net/sebastian_bergmann/cool-objects-sleep-on-the-couch?type=powerpoint)? (sobre este último, estoy haciendo un motor de oersistencia para MySQL).

b) No sé si entendí bien, pero para mejorar el rendimiento con MySQL implementas una especie de caché. Por qué no utilizar memcached o similares? Zend no provee algún tipo de caching?

Siento curiosidad... ;-)

Xava dijo...

Probaste con memcache?
http://php.net/manual/en/book.memcache.php

y con yogurth? :)

Enrique Place dijo...

Que tal Xava ;-)

Sí, memcache lo he usado en varios proyectos, pero lo que comento va un poco más allá de las herramientas de este estilo, es no perder el tiempo en discutir optimizaciones de código estériles, cuando en realidad lo importante está en otro lado.

Abrazos! ;-)

Entradas populares