Entendiendo la estrategia de particionado de Azure Storage Blobs

Todos los servicios de Azure de una manera o de otra utilizan el concepto de particionado a la hora de operar. Es importante entender cómo operan las estrategias de particionado para que el software y servicios que se construyan por encima de ellos funcionen de la mejor manera.

Cómo funciona el servicio de Azure Storage Blobs

Como cualquier otro servicio de Azure Storage utiliza una API REST que permite interactuar con el servicio. La URL define el recurso al que se quiere acceder.

clip_image002

Como se puede observar en la imagen hay tres tipos de artefactos. La cuenta, el contendor y el nombre del fichero. Estos tres artefactos serán los que tengan la responsabilidad de orquestar correctamente el acceso al servicio, y en el caso de este artículo, el particionado.

¿Por qué es importante el particionado?

Cuando se construye un servicio en la nube, uno de los aspectos importantes es la escalabilidad y el rendimiento del mismo. Cuando se tiene un servicio que acepta millones de peticiones cada hora, hay que tener en cuenta como el servicio va a responder bajo esa demanda de operación.

En ese sentido, la misma lógica impera cuando se diseña un servicio para on-premises. Imagina por un momento que todas peticiones de tu aplicación web están siendo servidas por la misma máquina, en vez de tener un sistema que distribuya las peticionas a través de unos recursos preexistentes de máquinas que contengan tu mismo recurso. Ese mismo concepto es el que hay detrás del particionado, es involucrar a diferentes servidores físicos para que ejecuten la operación que el usuario ha solicitado. Conforme más servidores, menos será la latencia y mayor la capacidad de respuesta del servicio.

El esquema de las direcciones de Storage Blob

En formato de las urls de Azure Storage Blob se puede resumir en dos opciones principalmente.

 https://myaccount.blob.core.windows.net/mycontainer/myblob.jpg

En esta primera opción el nombre del contenedor de mycontiner y el nombre del blob es myblob.jpg. Los nombres de los contenedores no pueden contener caracteres en mayúscula, así que solo puede ser en minúscula.

 https://myaccount.blob.core.windows.net/mycontainer/1/2/3/file.txt 

En este ejemplo el nombre del contenedor es igual mycontainer, pero el nombre del fichero es 1/2/3/file.txt. Es decir, el nombre del blob contiene también las barras simulando un sistema de archivo.

¿Cómo son las particiones calculadas?

Si se lee la documentación oficinal de producto, https://blogs.msdn.com/b/windowsazurestorage/archive/2010/05/10/windows-azure-storage-abstractions-and-their-scalability-targets.aspx, se comenta que la partición para lo blobs se calcula en base al nombre del contenedor + nombre del blob. Es decir, que, para nuestro ejemplo anterior, sería mycontainer + myblob.jpg.

Así que peticiones que tengan diferentes llaves de partición serán distribuidas a través de diferentes servidores. El único inconveniente es que pierde la capacidad de hacer transacciones atómicas entre múltiples blobs.

Entendiendo esto, ahora surge la siguiente pregunta, que es clave para entender el algoritmo de particionado.

¿Cómo tengo que nombrar mis blobs para maximizar el uso de particiones?

La estrategia de nombre debería de ser algo como esto:

a/b/c/d/e/f/{n}/file.txt

Siendo {n} el elemento que se va incrementando en el tiempo y que permite a los servidores de partición de Azure Storage almacenar correctamente los blobs de tu cuenta de almacenamiento. El orden de {n} es lexicográfico, con lo que te interesa que vaya incrementándose en base de los caracteres, también tiene como consecuencia que se no se calcula en base a un hash. Por cambiar un valor no vas a tener una partición completamente diferente, sino que posiblemente será la misma.

En el siguiente ejemplo se muestra un posible esquema y cómo utilizarlo.

Si para generar esa secuencia de elemento de {n} se utiliza la fecha para ir incrementando el nombre de los blobs, se tendría algo como esto:

 
30 de agosto del 2015
mycontainer/1/2/20150830/(Guid.NewGuid()).txt <- Todas las transacciones 
de Storage irían aquí, a un servidor de particiones
31 de agosto del 2015
mycontainer/1/2/20150830/(Guid.NewGuid()).txt <- este path ya no es usado 
por el servidor de particiones
mycontainer/1/2/20150831/(Guid.NewGuid()).txt <- Todas las transacciones 
de Storage irían aquí, a un servidor de particiones
1 de septiembre del 2015
mycontainer/1/2/20150830/(Guid.NewGuid()).txt <- este path ya no es usado 
por el servidor de particiones
mycontainer/1/2/20150831/(Guid.NewGuid()).txt <- este path ya no es usado 
por el servidor de particiones
mycontainer/1/2/20150701/(Guid.NewGuid()).txt <- Todas las transacciones 
de Storage irían aquí, a un servidor de particiones

Las peticiones se distribuirían de la manera antes expuesta, y aunque se ha utilizado un valor incremental para el nombre del blob, es se te ha hecho de manera que los servidores de particionado no lo utilizan y al final utiliza todos el mismo servidor. Con lo que si se tuviera que haber leído esos tres ficheros en el mismo día, el mismo servidor respondería y el rendimiento del servicio se vería afectado.

Sin embargo, si se utiliza este otro mecanismo para nombrar a los blobs:

 
mycontainer/1/2/1/(Guid.NewGuid).txt
mycontainer/1/2/2/(Guid.NewGuid).txt
mycontainer/1/2/3/(Guid.NewGuid).txt
mycontainer/1/2/4/(Guid.NewGuid).txt

Incluso poniendo un prefijo previo al nombre del fichero:

 
mycontainer/1/2/1/Storage(Guid.NewGuid).txt
mycontainer/1/2/2/Storage(Guid.NewGuid).txt
mycontainer/1/2/3/Storage(Guid.NewGuid).txt
mycontainer/1/2/4/Storage(Guid.NewGuid).txt

Se conseguiría que, para acceder a esos cuatro ficheros, que hasta 4 servidores se involucrasen en las respuestas haciendo que los tiempos de repuesta fuera mucho menores.

También se puede utilizar una estrategia con la fecha del día, pero en vez de poner solamente el año, el mes y el día, se debería de utilizar el valor en ticks de la fecha, pero para asegurarte de que se hace una distribución sobre un 10% del total, se le da la vuelta al valor del string que representa los ticks, y se tendría algo como esto:

 
Valor en Ticks de la hora: 635767287458393685
Valor del revés:           586393854782767536
Valor en Ticks de la hora: 635767287468351710
Valor del revés:           017153864782767536

Se puede observar que la parte de la izquierda de los valores del revés, son los caracteres de la derecha de la fecha, que son los valores que más oportunidad tienen de cambiar conforme avanza el tiempo.

De esta manera se tiene una evolución de que todos los ficheros que se agreguen tendrán una distribución del 10% sobre los demás.

Conclusión

Elegir una buena estrategia de particionado lo es todo para conseguir el mejor resultado del servicio. Dependiendo del tipo de aplicación que se esté desarrollando eso puede ser crítico o no, así que merece la pena dedicarle un poco de tiempo para entender cómo afectará eso al servicio.

Luis Guerrero.

Technical Evangelist Microsoft Azure

@guerrerotook