Cómo no romper los límites, cómo crear una buena arquitectura y cómo hacer un buen mantenimiento

High Performance Computing (HPC) Basics: Empezando a usar Azure Batch

Uno de los elementos claves en una solución de HPC (High Performance Computing) en Azure es Azure Batch. Azure Batch es un servicio de Azure que nos permite, de forma sencilla, crear y administrar un conjunto de nodos de proceso (agrupados en lo que se llamará un Pool).

Un pool nos permitirá ejecutar de forma paralela y distribuida, una serie de Jobs (o trabajos). Éstos Jobs, estarán compuestos a su vez por un número variable de Tasks (o tareas), las cuales serán realmente las que lleven el programa, elemento o algoritmo a ejecutar.

Batch Pool

El elemento central en Azure Batch es el Pool, o grupo. Dicho Pool estará compuesto de una serie de nodos de proceso, en los cuales se ejecutarán las tareas de nuestros trabajos. Dichos nodos de proceso pueden ser de dos tipos: dedicados o de prioridad baja. Estos últimos son nodos con un coste mucho menor, pero que no nos garantizan la disponibilidad, y por tanto no tendremos garantía del tiempo en el que se va a ejecutar el trabajo.

Para crear un Pool, tendremos los siguientes parámetros o configuraciones:

  • poolId: Identificador del Pool.
  • targetDedicatedComputeNodes: Número de nodos dedicados.
  • targetLowPriorityComputeNodes: Número de nodos de baja prioridad.
  • virtualMachineSize: Tamaño de la máquina virtual.
  • cloudServiceConfiguration/virtualMachineConfiguration: Configuración estándar (SO) o plantilla (genéricas de Azure o una nuestra personalizada) de la máquina virtual a usar.

A su vez, estos nodos pueden escalar, ya sea manualmente (con el método .Resize en .NET), o configurando un grupo habilitado para el escalado automático. Para ello, se omitirían los parámetros del número de nodos, y se configuraría el autoescalado de la siguiente forma:

pool.AutoScaleEnabled = true;
pool.AutoScaleFormula = "$TargetDedicatedNodes = (time().weekday == 1 ? 5:1);";
pool.AutoScaleEvaluationInterval = TimeSpan.FromMinutes(30);

Los intervalos de escalado mínimo y máximo son cinco minutos y 168 horas, respectivamente.
Aquí podemos ver un ejemplo de una creación simple de un Pool:

pool = batchClient.PoolOperations.CreatePool(
            poolId: poolId,
            targetDedicatedComputeNodes: 3, 
            virtualMachineSize: "small",
            cloudServiceConfiguration: new CloudServiceConfiguration(osFamily: "4"));
await pool.CommitAsync();

Batch Job

Una vez tengamos al menos un Pool creado y disponible en nuestro servicio de Azure Batch, lo siguiente será crear nuestro Job.

Como hemos contado anteriormente, un Job, o un trabajo de Batch, es una agrupación lógica de una o varias tareas (Tasks). Dicho Job lleva incluido en sí mismo algunas propiedades comunes para todas sus tareas, como por ejemplo el identificador del Job, el Pool en el que se va a ejecutar, y la prioridad del Job.

Un ejemplo de creación de un Job sería el siguiente:

try
{
    CloudJob job = batchClient.JobOperations.CreateJob();
    job.Id = JobId;
    job.PoolInformation = new PoolInformation { PoolId = PoolId };
    job.Commit(); 
}
...

Batch Task

Seguidamente, y como elemento de más bajo nivel, tenemos el elemento tarea, o Task. Estas tareas, serán cada uno de los elementos individuales que va a ejecutar el Job, sus unidades de cálculo individuales. Las tareas se asignan a un nodo para su ejecución o se ponen en cola hasta que quede libre uno. Es decir, una tarea ejecuta una o más rutinas en un nodo de proceso para llevar a cabo el trabajo necesario.

Para crear una tarea, podemos especificar los siguientes elementos:

  • La línea de comandos de la tarea: Línea de comandos concreta (cmd) que ejecutará la tarea en el nodo de proceso.
  • Archivos de recursos que contiene los datos que se van a procesar. Se copian automáticamente desde el Blob Storage en el que estén.
  • Variables de entorno que requiera la aplicación.
  • Restricciones para la tarea. Por ejemplo: tiempo máximo, número de reintentos…
  • Paquetes de aplicación a desplegar en el nodo que se va a ejecutar la tarea. Para desplegar nuestro código de aplicación personalizado.
  • Referencia a la imagen del contendor Docker, si fuera necesario.

Un ejemplo de cómo generar una serie de tareas y añadirlas a un Job sería el siguiente:

for (int i = 0; i < inputFiles.Count; i++)
{
    string taskId = String.Format("Task{0}", i);
    string inputFilename = inputFiles[i].FilePath;
    string taskCommandLine = String.Format("cmd /c type {0}", inputFilename);

    CloudTask task = new CloudTask(taskId, taskCommandLine);
    task.ResourceFiles = new List<ResourceFile> { inputFiles[i] };
    tasks.Add(task);
}

batchClient.JobOperations.AddTask(JobId, tasks);

Automatización de ejecuciones: Batch y Azure Functions

Una instancia de Azure Batch cualquiera, se puede gestionar desde la propia web, ya que el portal permite realizar todas las acciones. Sin embargo, para un entorno productivo esto no es manejable.

Azure Batch pone a disposición de los desarrolladores una serie de APIs, en lenguajes como .NET, Python, Node.js, Java, e incluso una API REST. Nosotros nos vamos a centrar en la API de .NET, que es la que hemos ido viendo en los ejemplos de los distintos elementos.

La API .NET de Batch la podemos usar desde una simple aplicación de consola, pero sigue siendo poco manejable o flexible. Así pues, una arquitectura que se propone, mucho más automatiza y flexible, es la siguiente:

Tendremos tres elementos principales:

  • Un Blob Storage. En él alojaremos nuestros paquetes de aplicación (ejecutables), así como los ficheros de datos de entrada y de salida, e incluso ficheros temporales si los hubiera.
  • El propio Azure Batch.
  • Una Azure Function, cuyo trigger será el Blob Storage.

El flujo será el siguiente: cuando se cargue un fichero de datos al contenedor de Blob Storage designado, se desencadenará la ejecución de la Azure Function. Ésta Function se encargará de leer y procesar el fichero de datos de entrada, trocearlo y dividirlo si fuera necesario, y crear un Job para ejecutar en el Pool de Azure Batch con las tareas correspondientes. Dichas tareas deberán ir cada una correctamente parametrizadas.

Finalmente, nuestro Pool de Azure Batch ejecutará el Job, y podremos ir viendo nuestros resultados en el contenedor del Blob Storage designado, o en donde corresponda según el ejecutable de nuestras tareas.
El código de la Azure Function que realiza toda ésta operación sería similar al siguiente:

[FunctionName("BatchFunction")]
        public static async Task Run([BlobTrigger("input/{name}", Connection = "AzureWebJobsDashboard")]Stream batchInputBlob, string name, TraceWriter log)
        {
            tWriter = log;
            tWriter.Info($"C# Blob trigger function Processed blob with Name: {name}...");
            BatchSharedKeyCredentials cred = new BatchSharedKeyCredentials(Common.Constants.BatchAccountUrl, Common.Constants.BatchAccountName, Common.Constants.BatchAccountKey);
            string storageConnectionString = string.Format("DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}",
                                             Common.Constants.StorageAccountName, Common.Constants.StorageAccountKey);
            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(storageConnectionString);
            CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
            using (BatchClient batchClient = BatchClient.Open(cred))
            {
                var jobId = $"DemoJob{name.Replace(".", "")}{DateTime.Now.Ticks}";
                tWriter.Info($"BatchClient created and opened successfully!");
                string outputContainerSasUrl = GetContainerSasUrl(blobClient, Common.Constants.OutputContainerName, SharedAccessBlobPermissions.Write);
                string tempContainerSasUrl = GetContainerSasUrl(blobClient, Common.Constants.TemporalContainerName, SharedAccessBlobPermissions.Write);
                await CreateJobAsync(batchClient, jobId, Common.Constants.PoolId);
                tWriter.Info("Job Created successfully!");
                var serializer = new JsonSerializer();
                using (var streamReader = new StreamReader(batchInputBlob))
                using (var jsonTextReader = new JsonTextReader(streamReader))
                {
                    var customerList = serializer.Deserialize<List<Driver>>(jsonTextReader);
                    var index = 1;
                    foreach (var customer in customerList)
                    {
                        string singleCustomerFile = $"customer_{customer.Id}.json";
                        var resourceFile = await UploadFileToContainerAsync(blobClient, Common.Constants.TemporalContainerName, singleCustomerFile, JsonConvert.SerializeObject(customer));
                        await AddTaskAsync(batchClient, jobId, resourceFile, index, outputContainerSasUrl);
                        index++;
                    }
                }
                tWriter.Info($"Processing finished successfully!");
            }
        }

Como hemos visto, la API de .NET de Batch nos proporciona un control total sobre nuestro servicio de Azure Batch, y combinando esto con Azure Functions y un Blob Storage, podemos realizar ejecuciones de nuestros trabajos por lotes de forma muy rápida y sencilla.

En el siguiente repositorio, podéis ver un ejemplo concreto de aplicación de consola que trata todo lo comentado, con todo conectado, y control de errores, excepciones… Además, ejecuta una serie de trabajos directamente desde la propia aplicación de consola.

¡Espero que os resulte de utilidad! 🙂

mm

Sobre Adrián Del Rincón López

Ingeniero Superior de Telecomunicaciones por la Universidad Politécnica de Valencia. Desde hace varios años trabajo en el departamento de desarrollo de ENCAMINA. Sobre todo, me apasiona estar al tanto de lo último en tecnología, y si es en el mundo/ecosistema Microsoft y .NET, mejor que mejor. Además, también participo de las redes sociales, si quieres encontrarme, búscame con el usuario @adderin
Esta entrada ha sido publicada en Azure. Enlace permanente.
ENCAMINA, piensa en colores