En muchas aplicaciones modernas tenemos la necesidad de realizar cargas de archivos grandes a Azure. En este post veremos cómo dar alas a tu aplicación para subir archivos a la nube y llevarla a otro nivel.
Veamos antes un ejemplo
Para que podamos observar la comparación, te voy a mostrar un ejemplo de cómo subir un archivo a un Blob Storage e Azure y comprobar tiempos.
En el ejemplo, usaremos la última versión del paquete Nuget WindowsAzure.Storage. Con esto, una aplicación de Consola de .Net Core y un PDF de unos 70MB, tenemos todo lo necesario.
¡Vamos allá!
using Microsoft.WindowsAzure.Storage; using System; using System.Diagnostics; using System.IO; using System.Text; using System.Threading.Tasks; namespace AzureStorageDataMovement { class Program { static async Task Main(string[] args) { var sourcePath = @"C:\Temp\MyLargePDF.pdf"; var containerName = "docs1"; var folderName = "testAzureStorageDataMovement"; Console.WriteLine($"Size of {Path.GetFileName(sourcePath)}: {new FileInfo(sourcePath).Length} bytes"); var newFileUri = await UploadBlobToContainer(containerName, folderName, sourcePath); Console.WriteLine($"The new Azure Blob File id: {newFileUri}"); Console.ReadKey(); } public static async Task<Uri> UploadBlobToContainer(string containerName, string folderName, string sourcePath) { try { var connectionString = "DefaultEndpointsProtocol=https;AccountName=XXXXXX;AccountKey=XXXXXX ==;EndpointSuffix=core.windows.net"; var storageAccount = CloudStorageAccount.Parse(connectionString); var client = storageAccount.CreateCloudBlobClient(); var container = client.GetContainerReference(containerName); // Me gusta aplicar esta condición ya que provoca errores 409 en Application Insights si el container existe if (!await container.ExistsAsync()) { await container.CreateIfNotExistsAsync(); } var folder = container.GetDirectoryReference(folderName); var blobName = $"{Guid.NewGuid().ToString()}{Path.GetExtension(sourcePath)}"; var blockBlob = folder.GetBlockBlobReference(blobName); blockBlob.Metadata.Add("originalFileName", EncodeFileNameToBase64(Path.GetFileName(sourcePath))); var stopwatch = Stopwatch.StartNew(); await blockBlob.UploadFromFileAsync(sourcePath); stopwatch.Stop(); Console.WriteLine("Time elapsed: {0:hh\\:mm\\:ss}", stopwatch.Elapsed); return blockBlob.Uri; } catch (StorageException ex) { throw new AzureStorageDataMovementException($"Error while uploading blob to container {containerName}.", ex); } } private static string EncodeFileNameToBase64(string fileName) { var originBytes = Encoding.UTF8.GetBytes(fileName); return Convert.ToBase64String(originBytes); } } }
Ejecutando nuestro programa obtenemos la siguiente salida
Size of MyLargePDF.pdf: 73303135 bytes
Time elapsed: 00:00:11
The new Azure Blob File id: https://xxxxxxxx.blob.core.windows.net/docs1/testAzureStorageDataMovement/cd2ebb4c-76c7-40d2-aaed-5ed36e418898.pdf
Como podemos observar, ha tardado 11 segundos en subir el archivo a nuestro Blob.
Ahora, la versión vitaminada
Como si nuestra aplicación se tomara la famosa bebida energética que DA ALAS, usaremos el paquete Nuget Microsoft.Azure.Storage.DataMovement
Realizaremos una pequeña modificación en nuestro código.
using Microsoft.Azure.Storage; using Microsoft.Azure.Storage.Blob; using Microsoft.Azure.Storage.DataMovement; using System; using System.Diagnostics; using System.IO; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; namespace AzureStorageDataMovementEnhanced { class Program { static async Task Main(string[] args) { var sourcePath = @"C:\Temp\MyLargePDF.pdf"; var containerName = "docs1"; var folderName = "testAzureStorageDataMovement"; Console.WriteLine($"Size of {Path.GetFileName(sourcePath)}: {new FileInfo(sourcePath).Length} bytes"); var newFileUri = await UploadBlobToContainer(containerName, folderName, sourcePath); Console.WriteLine($"The new Azure Blob File id: {newFileUri}"); Console.ReadKey(); } public static async Task<Uri> UploadBlobToContainer(string containerName, string folderName, string sourcePath) { try { var connectionString = "DefaultEndpointsProtocol=https;AccountName=XXXXXX;AccountKey=XXXXXX ==;EndpointSuffix=core.windows.net"; var storageAccount = CloudStorageAccount.Parse(connectionString); var client = storageAccount.CreateCloudBlobClient(); var container = client.GetContainerReference(containerName); // Me gusta aplicar esta condición ya que provoca errores 409 en Application Insights si el container existe if (!await container.ExistsAsync()) { await container.CreateIfNotExistsAsync(); } var folder = container.GetDirectoryReference(folderName); var blobName = $"{Guid.NewGuid().ToString()}{Path.GetExtension(sourcePath)}"; var blockBlob = folder.GetBlockBlobReference(blobName); blockBlob.Metadata.Add("originalFileName", EncodeFileNameToBase64(Path.GetFileName(sourcePath))); // Aqui está la madre del cordero TransferManager.Configurations.ParallelOperations = ServicePointManager.DefaultConnectionLimit = Environment.ProcessorCount * 8; ServicePointManager.Expect100Continue = false; SingleTransferContext context = new SingleTransferContext { LogLevel = Microsoft.Azure.Storage.LogLevel.Warning }; var stopwatch = Stopwatch.StartNew(); await TransferManager.UploadAsync(sourcePath, blockBlob, null, context, CancellationToken.None); stopwatch.Stop(); Console.WriteLine("Time elapsed: {0:hh\\:mm\\:ss}", stopwatch.Elapsed); return blockBlob.Uri; } catch (StorageException ex) { throw new AzureStorageDataMovementException($"Error while uploading blob to container {containerName}.", ex); } } private static string EncodeFileNameToBase64(string fileName) { var originBytes = Encoding.UTF8.GetBytes(fileName); return Convert.ToBase64String(originBytes); } } }
Y ejecutando nuestro programa modificado, obtenemos la siguiente salida
Size of MyLargePDF.pdf: 73303135 bytes
Time elapsed: 00:00:04
The new Azure Blob File id: https://xxxxxxxx.blob.core.windows.net/docs1/testAzureStorageDataMovement/56ae3f2d-c555-472a-a220c3346142.pdf
Ahora ha tardado 4 segundos en subir el archivo a nuestro Blob. Es una mejora muy significativa ¿verdad?
Ya, pero ¿puedes comentarnos algo de las modificaciones realizadas?
¡Por supuesto! Os explico algunos detalles de las modificaciones.
- Configurations.ParallelOperations
Establecemos el número de operaciones paralelas que podemos realizar.
- ServicePointManager.DefaultConnectionLimit = Environment.ProcessorCount * 8
De forma predeterminada, el límite de conexión HTTP .Net es 2. Esto implica que solo se pueden mantener dos conexiones simultáneas, cosa que impide que más conexiones paralelas accedan al almacenamiento de blobs de Azure desde su aplicación.
Para tener un rendimiento comparable cuando usamos Data Movement Library con nuestro amado AzCopy, es recomendable que establezca también este valor, ya que por defecto AzCopy establece ServicePointManager.DefaultConnectionLimit al mismo valor que estamos indicando en nuestro código.
- Expect100Continue = false;
Expect100Continue esencialmente envía un encabezado Expect al servidor para preguntar si es probable que éste acepte la solicitud de verbos POST, PUT y PATCH
Si el servidor dice que no lo hará por cualquier motivo (por ejemplo, 401 Unauthorized), el paquete no se envía y la solicitud simplemente devuelve la respuesta de rechazo al cliente.
Si el servidor dice que acepta la solicitud, entonces devuelve una respuesta 100-Continue y el cliente envía el paquete.
Este mecanismo permite a los clientes evitar el envío de grandes cantidades de datos a través de la red cuando el servidor, basándose en los encabezados de solicitud, intenta rechazar una solicitud.
Sin embargo, una vez que se recibe todo el payload en el servidor, aún pueden ocurrir otros errores. El SDK de Microsoft Azure está lo suficientemente bien probado como para asegurarnos que no está enviando solicitudes incorrectas, por lo que podemos desactivar 100-Continue para que la solicitud completa se envíe en un solo viaje.
CONCLUSIONES
Como podemos ver en los tiempos obtenidos, utilizando el paquete Nuget Microsoft.Azure.Storage.DataMovement, permite enviar grandes cantidades de datos de una forma muy eficiente y rápida.
HAPPY CODING!