Las ventajas que te ofrece Microsoft Azure y el mundo.NET

Cómo dar alas a tu aplicación a la hora de subir archivos a Azure

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

Tiempo de subida del archivo a nuestro Blob, 11 segundos

Tiempo de subida del archivo a nuestro Blob, 11 segundos

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

Tiempo de subida del archivo al Blob, 4 segundos

Tiempo de subida del archivo al Blob, 4 segundos

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!

mm

Sobre Sergio Parra Guerra

Sergio Parra es Ingeniero Técnico en Informática de Sistemas por la UPSAM. Tiene a sus espaldas muchísimas certificaciones entre las cuales Microsoft Certified Professional y ex Microsoft MVP Visual Studio and Development Technologies. Actualmente es un magnífico Software & Cloud Architect en ENCAMINA.
Esta entrada ha sido publicada en .NET, Microsoft Azure. Enlace permanente.
Suscríbete a Piensa en Sofware desarrolla en Colores

Suscríbete a Piensa en Sofware desarrolla en Colores

Recibe todas las actualizaciones semanalmente de nuestro blog

You have Successfully Subscribed!

ENCAMINA, piensa en colores