Avec HdInsight, il est possible de modifier le nombre de worker node depuis le portail azure. Mais une fois que c'est fait, il faut encore modifier les configurations dans Ambari pour que les modifications Hardware soient prises en compte au niveau software.
Cela peut s'avérer fastidieux si on le fait souvent, nous allons voir comment automatiser tout ça.

I. Mise à l'échelle de HdInsight

Grâce au package nuget Microsoft.Azure.Management.HDInsight, nous pouvons accéder facilement au paramétrage du cluster.

Tout d'abord, l'authentification. Nous optons ici pour une authentification par un "app registered" dont nous avons le clientId et la secret key. Les packages nuget nécessaires sont Microsoft.Azure.Common et Microsoft.IdentityModel.Clients.ActiveDirectory :

private async Task<TokenCloudCredentials> Authenticate()
{
    var authContext = new AuthenticationContext("https://login.microsoftonline.com/" + _tenantId);
    ClientCredential clientCredential = new ClientCredential(_clientId, _secretKey);
    var tokenAuthResult = await authContext.AcquireTokenAsync("https://management.core.windows.net/", clientCredential);
    return new TokenCloudCredentials(_subscriptionId, tokenAuthResult.AccessToken);
}

Une fois authentifié, nous pouvons utiliser la classe HDInsightManagementClient qui permet de gérer le cluster. Voici comment récupérer les informations des clusters :

public async Task<ClusterGetResponse> GetClusterDetailAsync(string resourceGroupeName, string clusterName)
{
    var authToken = await Authenticate();
    HDInsightManagementClient managementClient = new HDInsightManagementClient(authToken);
    return await managementClient.Clusters.GetAsync(resourceGroupeName, clusterName);
}

Pour avoir le nombre de worker node, il faut récupérer les informations du rôle "workernode" :

var cluster = await HdinsightService.GetClusterDetailAsync(_resourceGroupName, _clusterName);
foreach (var role in cluster.Cluster.Properties.ComputeProfile.Roles)
{
    if (role.Name == "workernode")
    {
        return role.TargetInstanceCount;
    }
}

Pour augmenter ou diminuer ce nombre d'instance nous pouvons le faire dans l'interface Azure

mais ici, ce qui nous intéresse c'est de le faire dynamiquement :

public async Task ScaleClusterAsync(string resourceGroupeName, string clusterName, int instanceCount)
{
    var authToken = await Authenticate();
    HDInsightManagementClient managementClient = new HDInsightManagementClient(authToken);
    await managementClient.Clusters.ResizeAsync(resourceGroupeName, clusterName, instanceCount);
}

Attention, cette méthode a beau s'appeler ResizeAsync, ne vous y trompez pas, elle est synchrone. C'est-à-dire qu'elle ne rend la main que lorsque le cluster est complètement mis à l'échelle, ce qui peut prendre quelques minutes.
Si vous voulez le faire de manière asynchrone, il faut utiliser la méthode BeginResizingAsync.

Maintenant que notre cluster est mis à l'échelle, ce n'est pas fini car hadoop ne sait pas qu'il dispose de plus de noeuds. Et même pire, il risque de ne plus fonctionner du tout si vous avez diminuer le nombre de noeud sans ensuite, changer la configuration.

II. Mise à jour des propriétés dans Ambari

Ambari fonctionne par API REST. Tout ce que l'on peut faire sur le portail Web peut être fait par des requêtes REST. Ainsi, en affichant les requêtes qui passent dans le portail Web de Amabri (F12 dans le navigateur :-)), on peut les récupérer pour les reproduire.

Pour mettre à jour une configuration il faut :
- récupérer toutes les propriétés du fichier de configuration désiré
- mettre à jour ces propriétés
- redémarrer les services impactés
- vérifier le redémarrage des services

a. Récupération des propriétés actuelles

Ambari historise les configurations à chaque modification. Il faut récupérer dans un premier temps le numéro de version de la configuration actuelle.

En appelant l'URL suivante, on récupère les dernières versions de chaque fichier de configuration : https://{_clusterName}.azurehdinsight.net/api/v1/clusters/{_clusterName}?fields=Clusters/desired_configs
Cela nous renvoie un JSON de la forme suivante :

{
  href : "http://hn0-URL.internal.cloudapp.net:8080/api/v1/clusters/ClusterName?fields=Clusters/desired_configs",
  Clusters : {
    cluster_name : "ClusterName",
    version : "HDP-2.6",
    desired_configs : {
      ...
      hive-interactive-env : {
        tag : "version1528703467",
        version : 48
      },
      hive-interactive-site : {
        tag : "version1528703467",
        version : 48
      },
      ...
    }
  }
}

 

Le numéro de version se trouve dans l'objet "tag". Dans cet exemple, "hive-interactive-env" est sur la version "version1528703467".
Nous pouvons maintenant récupérer les propriétés de cette version par cette URL : https://{_clusterName}.azurehdinsight.net/api/v1/clusters/{_clusterName}/configurations?type=hive-interactive-env&tag=version1528703467
Cela nous renvoie un JSON de la forme suivante :

{
  href : "http://hn0-URL:8080/api/v1/clusters/ClusterName/configurations?type=hive-interactive-env&tag=version1527173112371",
  items : [{
      href : "http://hn0-URL:8080/api/v1/clusters/ClusterName/configurations?type=hive-interactive-env&tag=version1528703467",
      tag : "version1528703467",
      type : "hive-interactive-env",
      version : 5,
      Config : {
        cluster_name : "ClusterName",
        stack_id : "HDP-2.6"
      },
      properties : {
        ...
        hive_heapsize : "2048",
        llap_app_name : "llap0",
        llap_extra_slider_opts : "",
        llap_headroom_space : "12288",
        llap_heap_size : "26214",
        ...
      }
    }
  ]
}

Maintenant comment faire ça en C#?

Tout d'abord l'authentification :

HttpClient Client = new HttpClient();
var byteArray = Encoding.ASCII.GetBytes($"{_login}:{_password}");
Client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));

puis la récupération de la version actuelle :

    string url =
          $"https://{_clusterName}.azurehdinsight.net/api/v1/clusters/{_clusterName}?fields=Clusters/desired_configs";

    Client.DefaultRequestHeaders.Clear();
    Client.DefaultRequestHeaders.Add("X-Requested-By", "C#");
    HttpResponseMessage response = await Client.GetAsync(url);
    response.EnsureSuccessStatusCode();
    string content = await response.Content.ReadAsStringAsync();
    var json = JObject.Parse(content);
    string version = "";
    if (json["Clusters"]?["desired_configs"]?[configFileName]?["tag"] != null)
    {
        version = json["Clusters"]["desired_configs"][configFileName]["tag"].ToString();
    }

et enfin, la récupération de la valeur d'une propriété :

string url =
    $"https://{_clusterName}.azurehdinsight.net/api/v1/clusters/{_clusterName}/configurations?type={configFileName}&tag={version}";

Client.DefaultRequestHeaders.Clear();
Client.DefaultRequestHeaders.Add("X-Requested-By", "C#");
HttpResponseMessage response = await Client.GetAsync(url);
response.EnsureSuccessStatusCode();
string content = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(content);
if (json["items"] != null && json["items"].Any() && json["items"][0]["properties"]?[configKey] != null)
{
    return json["items"][0]["properties"][configKey].ToString();
}

b. Mise à jour des configurations

Pour la mise à jour, il faut envoyer les propriétés en utilisant le format suivant de JSON :

[{
    "Clusters" : {
      "desired_config" : [{
          "type" : "hive-interactive-env",
          "tag" : "version1480557385509",
          "properties" : {
            ...
            hive_heapsize : "2048",
            llap_app_name : "llap0",
            llap_extra_slider_opts : "",
            llap_headroom_space : "12288",
            llap_heap_size : "26214",
            ...
          },
          "service_config_version_note" : "New config version from C#"
        }
      ]
    }
  }
]

Dans l'objet "properties", il faut remettre toutes les propriétés actuelles même si vous ne les changez pas, sous peine d'avoir quelques problèmes par la suite.
Pour la version, il faut en générer une unique, pour cela, il est préférable de faire comme Ambari, c'est-à-dire de générer un timestamp.

Voici ce que ça donne en C# pour générer le json :

Dictionary<string, string>> configurationList;//Contient toutes les propriétés de tous les fichiers de configuration que l'on souhaite modifier

long timeStamp = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds; string version = $"version{timeStamp}";
dynamic json = new JArray(new object[] { new JObject() });
json[0].Clusters = new JObject();
json[0].Clusters.desired_config = new JArray();
foreach (var config in configurationList)
{
    dynamic propertyList = new JObject();
    foreach (var item in config.Value)
    {
        propertyList.Add(item.Key, item.Value);
    }

    dynamic config = new JObject();
    config.type = config.Key;
    config.tag = version;
    config.properties = propertyList;
    config.service_config_version_note = "Mise à jour par C#";
    json[0].Clusters.desired_config.Add(config);
}

et voici comment envoyer la mise à jour :

string url =
    $"https://{_clusterName}.azurehdinsight.net/api/v1/clusters/{_clusterName}";
HttpContent content = new StringContent(json.ToString(), Encoding.UTF8, "application/x-www-form-urlencoded");
Client.DefaultRequestHeaders.Clear();
Client.DefaultRequestHeaders.Add("X-Requested-By", "C#");
var response = await Client.PutAsync(url, content);

c. redémarrer les services impactés

Pour que les modifications soient prises en compte, il faut maintenant redémarrer les services impactées par les modifications. Pour cela, il faut envoyer un JSON de la forme suivante :

{
  "RequestInfo": {
    "command": "RESTART",
    "context": "Restart all required services by C#",
    "operation_level": "host_component"
  },
  "Requests/resource_filters": [
    {
      "hosts_predicate": "HostRoles/stale_configs=true"
    }
  ]
}

Voici le redémarrage en C# :

string id = null;
string url =
    $"https://{_clusterName}.azurehdinsight.net/api/v1/clusters/{_clusterName}/requests";

dynamic json = new JObject();
json.RequestInfo = new JObject();
json.RequestInfo.command = "RESTART";
json.RequestInfo.context = "Restart all required services by WebJob";
json.RequestInfo.operation_level = "host_component";

dynamic resourceFilters = new JArray();
resourceFilters.Add(new JObject());
resourceFilters[0].hosts_predicate = "HostRoles/stale_configs=true";
json.Add("Requests/resource_filters", resourceFilters);

Client.DefaultRequestHeaders.Clear();
Client.DefaultRequestHeaders.Add("X-Requested-By", "C#");
HttpContent content = new StringContent(json.ToString(), Encoding.UTF8, "application/x-www-form-urlencoded");
var response = await Client.PostAsync(url, content);
response.EnsureSuccessStatusCode();

var responseContent = await response.Content.ReadAsStringAsync();
if (!string.IsNullOrWhiteSpace(responseContent))
{
    dynamic jsonResponse = JObject.Parse(responseContent);
    id = jsonResponse["Requests"]["id"];
}
return id;

Cet identifiant récupéré va nous permettre de vérifier le statut du redémarrage. Ainsi, nous pouvons savoir lorsque le redémarrage est terminé et s'il s'est terminé en succès. Car si de mauvais paramètres sont envoyés, certains services peuvent ne pas redémarrer.

d. Récupération de l'état de redémarrage des services

En conservant l'identifiant de l'opération de redémarrage, on peut savoir où en est ce redémarrage en effectuant l'appel suivant :

string status = null;
string url =
    $"https://{_clusterName}.azurehdinsight.net/api/v1/clusters/{_clusterName}/requests/{id}";

Client.DefaultRequestHeaders.Clear();
Client.DefaultRequestHeaders.Add("X-Requested-By", "C#");
var response = await Client.GetAsync(url);
response.EnsureSuccessStatusCode();

var responseContent = await response.Content.ReadAsStringAsync();
if (!string.IsNullOrWhiteSpace(responseContent))
{
    dynamic jsonResponse = JObject.Parse(responseContent);
    status = jsonResponse.Requests.request_status;
}
return status;

Lorsque le rédémarrage est terminé, le statut est soit "COMPLETED" soit "FAILED"

Conclusion

Maintenant que nous pouvons mettre à l'échelle automatiquement HdInsight, nous pouvons mettre, par exemple, en base de données les configurations Ambari que l'on souhaite modifier en fonction du nombre de Worker node.

On peut ensuite automatiser les scale up et scale down en fonction des heures d'utilisation, ce qui permet d'économiser des coûts.

ANNEXES

Exemple de configurations Ambari :

 

Nombre de worker node Nom de fichier Clé de configuration Valeur
1 hive-interactive-site hive.llap.daemon.yarn.container.mb 50176
1 hive-interactive-site hive.llap.io.memory.size 1024
1 hive-interactive-env num_llap_nodes 1
1 hive-interactive-env num_llap_nodes_for_llap_daemons 1
2 hive-interactive-site hive.llap.daemon.yarn.container.mb 51200
2 hive-interactive-site hive.llap.io.memory.size 2048
2 hive-interactive-env num_llap_nodes 2
2 hive-interactive-env num_llap_nodes_for_llap_daemons 2

Quelques liens :

https://cwiki.apache.org/confluence/display/AMBARI/Modify+configurations
https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=41812517
https://cwiki.apache.org/confluence/display/AMBARI/Restarting+host+components+via+the+API

https://docs.microsoft.com/en-us/azure/hdinsight/hdinsight-administer-use-dotnet-sdk


Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *