¿Es tu software como esto?

12:33 0 Comments

He visto el link a la imagen en reddit.com, qué os parece??



0 comentarios:

Demasiada memoria gracias al Garbage Collector

16:03 0 Comments

Parece que está claro que programar en algo como C#/.NET (incluso C#/Mono) es mucho más rápido que intentarlo con C/C++: unas librerías potentes, un lenguaje sencillo y (lo que todos dicen) no más mallocs ni frees.

(Nota: hace un par de semanas en Game Developers Conference 2010 había una mesa rendonda en la que precisamente hablaban de las limitaciones de C++ (su complejidad básicamente) para el desarrollo de videojuegos y cómo les gustaría tener GarbageCollector… aunque al final todo el mundo coincidía en que les gustaba C++ a pesar de todo…)

Bien, volviendo a .NET: Garbage Collector, uno de los elementos claves de framework (y lo mismo se puede decir en Java) proporciona muchas ventajas: escribes y listo, te olvidas de líos. Pero… ¿es todo positivo? En Java estamos acostumbrados a ver aplicaciones consumiendo cantidades astronómicas de memoria (incluso hay IDEs como IntelliJ con un botoncito para llamar al GC, ni más ni menos) y lo mismo puede ocurrir con .NET. En muchas ocasiones no es un problema sino un tema estético: como el GC sabe que hay memoria de sobra disponible, y para mejorar el rendimiento, no recolecta la basura, así que el proceso sigue creciendo.

Pero para mí el mayor problema no es ese sino la sensación que va creando en todos los programadores de que la memoria es gratis. ¿Qué quiero decir con esto? Pues que acabas creando objetos y objetos y objetos nuevos porque es rápido y sencillo… aunque puedes estar estresando al GC más de la cuenta.


Un ejemplo práctico: servidor y blobs

Un ejemplo claro de cómo me encontré con el problema y cómo creo que en C jamás hubiera ocurrido.

Tengo un proceso servidor en .NET que procesa peticiones de clientes realizadas a través de la red, lee datos (blobs) de una base de datos y los devuelve al cliente. Muy sencillo.

Bien, cuando el nivel de carga del servidor no es muy alto todo funciona perfectamente: el servidor lee objetos, los devuelve, libera memoria de vez en cuando… todo perfecto.

Pero, ¿qué ocurre cuando ponemos unos cuantos cientos de clientes contra el mismo servidor?

Pues que la memoria del servidor comienza a crecer muchísimo, vemos como el tiempo haciendo GC aumenta también (se puede ver con el Process Explorer de SysInternals, http://technet.microsoft.com/es-es/sysinternals/bb896653.aspx, por ejemplo, donde te da una medida del tiempo que tu proceso (.Net 2 o superior) está gastando en GC).

¿Por qué ocurre esto?

Lo que yo estaba haciendo para procesar la petición era algo como esto (tipo pseudocódigo):
public byte[] GetData(long dataId)
{
byte[] result = new
byte[GetDataLen(dataId)];
ReadData(dataId, result);
return result;
}

¿Veis el problema?

Ahora me parece totalmente evidente pero la verdad es que tardé un rato en darme cuenta porque con el profiler de memoria (usaba la herramienta de RedGate Ants, http://www.red-gate.com/products/ants_memory_profiler) no detectaba nada de nada, parecía que todo estaba bien.


Objetos en tránsito

¿Cómo funciona el servidor? Cada método GetData se va a invocar en un thread (realmente no creando un thread nuevo, que parece un pecado, sino usando un thread pool) diferente, hace su trabajo y termina.

Entonces, ¿por qué tanta memoria? El siguiente gráfico lo explica claramente:

El método GetData está funcionando bien pero lo está haciendo en "modo .NET", es decir, lo he escrito como si la memoria fuera gratis.

Cuando se acumulan peticiones la cantidad de memoria inútil reservada será enorme dependiendo de cuántas llamadas se estén procesando (de la carga del servidor). Viendo el gráfico está claro que la memoria que se reservó para procesar la primera petición en el instante 0 en el primer thread no vale para nada ya en el instante 2 y sin embargo sigue ocupando sitio en el proceso…

Y este es precisamente el problema: los objetos en tránsito, objetos que todavía no se han liberado pero que siguen ocupando sitio.


Pensamiento tipo C

Esto nunca le hubiera pasado a un desarrollador C/C++ por dos motivos: primero porque un free es un free de verdad y liberaría la memoria inmediatamente. Segundo porque estando acostumbrado a una vida mucho más dura, trataría de evitar tanto malloc/free (o new/dispose).

¿Qué pasa si creamos un buffer pool con unos cuantos buffers pre-creados que reutilizamos en cada petición? El código pasaría a ser algo como esto:

public byte[] GetData(long dataId)
{
byte[] result = GetBuffer(GetDataLen(dataId));
ReadData(dataId, result);
return result;
}

public void ProcessRequest(long dataId)
{
byte[] result = GetData(dataId);
ReturnData(result);
ReleaseBuffer(result);
}

Varios puntos a tener en cuenta:

  • Para simplificar algo como esto ayuda mucho si los datos son de tamaño fijo, o al menos tienen un tamaño máximo. En mi caso un bloque de datos nunca era mayor de 4Mb, por lo que es muy sencillo de manejar el buffer pool porque todos los buffers serán del mismo tamaño (el máximo).

  • Si todos los buffers son del mismo tamaño habrá que manejar cuál es el tamaño usado de verdad, es decir, si estás leyendo 1024bytes en un buffer de 4Mb, luego no envíes 4Mb por la red!!! Envía sólo lo que estás usando. Esto posiblemente requerirá un código del tipo:
    public void ProcessRequest(long dataId)
    {
    byte[] result = GetBuffer();
    try{
    int size = ReadData(dataId, result);
    ReturnData(size, result);
    }
    finally
    {
    ReleaseBuffer(result);
    }
    }

Que por otro lado es mucho más limpio.

  • Hay que acordarse de liberar el buffer (tipo C) que realmente no hace nada más que devolverlo al buffer pool. Si el código de obtención y liberación del buffer está en el mismo método y con un try/finally, mucho mejor: más fácil de leer y menos propenso a fallos (no me gusta cuando el código de obtener y liberar un recurso o memoria, o lo que sea, no está en el mismo bloque).

Lógicamente hay que implementar el buffer pool de forma que:

  • Sincronice el acceso de varios threads

  • Se pueda limitar el número máximo de buffers: de este modo se puede controlar cuál va a ser el uso de memoria de nuestro servidor: no ocurrirá NUNCA que se descontrole como pasaba antes, el consumo será plano, el GC no tendrá trabajo y todo será más rápido. ¿Cómo limitarlo? Por ejemplo con un sencillo semáforo que bloquee en la llamada GetBuffer hasta que queden buffers libres. NOTA IMPORTANTE! En caso de usar un semáforo es muy importante considerar el riesgo de deadlocks si empezamos a usar múltiples pools (después del primero vendrán más) y los threads intentan acceder a ellos sin un orden muy estricto (la vieja regla: reserva SIEMPRE en el mismo orden y nunca tendrás deadlocks, reserva en orden diferente y espera el desastre).

Remoting: más complicado


En mi caso real (no descubrí esto en un programa de 5 líneas con un GetData, desgraciadamente) el servidor no estaba usando sockets directamente sino .NET Remoting, así que en la película entraban más complicaciones como la serialización de objetos y las múltiples copias que Remoting hace hasta llegar a la capa de red. Para evitarlo no quedó más remedio que escribir un custom tcp channel capaz de manejar el buffer pool reservado en la capa de aplicación y liberarlo justo después de poner los datos en la red, y evitando también la lenta serialización estándar de .Net… pero esto ya es otra historia para otro día.

0 comentarios:

Plastic SCM 2.9 ya está aquí!!

0:09 0 Comments

Plastic SCM llega a su versión 2.9 y está listo para descargarse desde nuestra web: www.plasticscm.com.

La nueva versión viene cargada con un buen número de nuevas características y también es mucho más rápida, especialmente bajo carga.

Estas son algunas de las nuevas características:

  • Soporte de Oracle: hemos añadido Oracle a la lista de backends con los que PLlastic puede funcionar. Hasta ahora soportábamos MySql, SQL Server y Firebird (tanto servidor como embebido). Nos han pedido Oracle varias veces, especialmente desde grandes equipos así que por fin está disponible. Se trata posíblemente del gestor de bases de datos más conocido tanto por rendimiento como estabilidad y escalabilidad y puede usarse con servidores Plastic en Windows, Linux y MacOS X.

  • Integración continua con Pulse: hemos desarrollado un plugin para el producto de integración continua de Zutubi. Hasta ahora ya nos integrábamos con productos como CruiseControl y FinalBuilder, pero Pulse es quizá mi favorito y estamos trabajando en ampliar la integración para que soporte patrones como rama por tarea, y así poder implantarlo internamente. Otros sistemas, como Hudson, también funcionan con Plastic.

  • Servidor proxy: otra de las características más esperadas. Ahora se puede configurar un servidor proxy de Plastic para reducir el uso de red mientras se trabaja conectado a un servidor central. El servidor proxy es muy sencillo: símplemente descarga datos bajo demanda que le solicitan los clientes y los cachea para el siguiente uso. Con esto se ahorra uso de red lo que puede ser crítico cuando se acceede al servidor a través de VPN o conexiones lentas. Nota: Plastic puede funcionar en modo distribuido pero algunas empresas prefieren llevar ciertos proyectos de forma centralizada, y es ahí donde el proxy server juega un papel importante.

  • Sparse tree y cloaking: que se traduce por algo como árboles parciales y evitar que ciertos ficheros se actualicen. Mediante un nuevo fichero de configuración llamado cloaked.conf es posible definir reglas para indicar qué ficheros se pueden descargar y qué ficheros no, es algo complementario a lo que ya se puede hacer con los selectores, pero permite funcionalidades diferentes. Por ejemplo, se puede descargar un árbol de directorios y luego indicar que ese árbol está cloaked lo que quiere decir que no se volverá a actualizar. Es útil para mejorar el rendimiento cuando se trabaja con proyectos muy grandes. Sólo para usuarios avanzados.

  • Uso de memoria en la replicación. El uso de memoria en todo el proceso de replicación se ha reducido considerablemente, haciendo que el proceso sea mucho más rápido y eficiente que antes. Ahora se puede replicar un repositorio completo sin que el servidor se entere a nivel de uso de memoria!

  • ¡Eliminación del workspace server! Sí, como leeéis, el hemos eliminado el componente servidor de workspaces del mapa. Bueno, de hecho no se ha ido sino que se ha trasladado a cada cliente, de modo que el rendimiento aumenta de forma espectacular. ¿Qué significa esto? Echa un vistazo a un nuevo workspace y verás que contiene un directorio (oculto) denominado .plastic. Contiene información sobre el workspace como el selector y el árbol de la copia de trabajo. Toda esa información solía estar en el servidor y ahora se ha movido al cliente, lo que supone que se reduce significativamente el uso de red, lo que podrán apreciar especialmente los usuarios de redes VPN (en LAN la velocidad ya era tan alta que no se apreciará mucha diferencia). Nota importante: cuando te actualices de Plastic 2.8 a 2.9 verás, en el primer arranque, un diálogo como el siguiente que indicará el progreso de la conversión de workspaces.


  • Workspaces compartidos: tras las modificaciones realizadas en los workspaces ahora se pueden compartir: se puede ubicar un espacio de trabajo en un directorio compartido (por Samba o por NFS, por ejemplo) de forma que más de un usuario pueda acceder a él e incluso actualizarlo y acceder a información de Plastic desde varias ubicaciones. Antes se podía hacer registrando el workspace en diferentes máquinas, pero se podía llegar fácilmente a condiciones en las que el sistema no se comportase bien porque para Plastic eran workspaces diferentes. Lógicamente es una característica para usar con cuidado: es útil para usarlo con sistemas de compilación, o de paso a producción, no para trabajar a diario ya que el objetivo de un control de versiones es precísamente coordinar diferentes copias de trabajo, ¡¡no trabajar sobre una misma!!

  • Exploración de changesets: una de las nuevas características más potentes. Ahora se puede caminar por las ramas cambio a cambio (changeset a changeset) leyendo la historia que cuentan. Escribiré otro post sobre esto pero para hacer una breve introducción: en tu propia rama de tarea eres libre de hacer tantos checkins como quieras, pero todos esos checkins deberían de tener un significado, ir describiendo cambios uno a uno, de modo que todos tengan una lógica. Supongamos que vamos a hacer un refactor importante, de muchos ficheros, si comparas el contenido de la rama una vez que has terminado verás tantísimos cambios que será difícil de entender lo que has hecho, sin embargo si has podido descomponer el refactor en pequeñas operaciones y has ido haciendo checkin de cada una de ellas, se podrá revisar posteriormente los cambios que has hecho, como si fuera ver una repetición de la jugada. El mecanismo de commit no es nuevo, por supuesto, pero la herramienta de recorrido de cambios permite que se usen para describir cambios casi contando cómo lo has ido haciendo, dejando registrado no sólo lo que haces sino cómo lo haces, con todas las aplicaciones que eso tiene.



  • Mejoras en el branch explorer: siempre en evolución, hemos añadido la capacidad de búsqueda que permite ir navegando las ramas que cumplan el filtro, lo que es mucho más útil que antes. También la posibilidad de hacer clic sobre los link de merge y ver de dónde vienen y hacia dónde van. Y por último el editor de visibilidad que tantas veces nos han pedido y que permite ocultar ramas que no interese ver.





    Y estas son todas las funcionalidades importantes en 2.9 junto con un buen número de correcciones.

    Y por supuesto mejoras de rendimiento: probamos Plastic cada semana en un cluster con 100 nodos y bajo esa carga 2.9 es un 30% más rápida que 2.8. Recordad que Plastic 2.8 era ya 10 veces más rápido que SVN... así que calculad 2.9!!

    Y por supuesto a pesar de seguir creciendo Plastic es todavía fácil de usar y de instalar. Echad un vistazo al siguiente vídeo en el que se configura un servidor en menos de 45 segundos!! Y después se hace una importación inicial de código (algo que es muy común en cualquier evaluacion) durante el siguiente minuto!

  • 0 comentarios: