Compromis énergétique de serveurs

Dans ce DM, on compare les performances de trois serveurs S1, S2 et S3 et l’énergie qu’ils consomment; afin de pouvoir mettre en évidence un compromis énergétique intéressant, puisqu’un serveur plus rapide consomme beaucoup plus d’énergie.

Comparaison de performances entre les serveurs 1 et 2

Nous commençons par créer une fonction qui calcule la vitesse moyenne d’un serveur (parmi les serveurs 1 ou 2 qui ont le même fonctionnement), soit le temps moyen que les tâches prennent à s’exécuter, ainsi que l’énergie totale consommée pendant le temps t. Notre fonction prend un taux d’inter-arrivées lambda, un taux de service mu, un nombre de tâches N et un vecteur de consommation énergétique E où E[1] correspond à la consommation du serveur lorsqu’il est en veille et E[2] à la consommation lorsqu’il est actif.

Serveur1_2 = function(lambda,mu,N,E,Markovien){
  arrival = c(0,cumsum(rexp(n=N-1, rate=lambda))) #Date d'arrivée dans le système
  if(!Markovien){
    service=rexp(n=N, rate=mu)
  }
  else{
    service=rep(1/mu, times=N)
  }
  completion = rep(NA,times =N) #Date de sortie
  EnergyCons = 0 #Energie totale consommée par le serveur
  
  t = 0 #Date du système
  task = 1 #Indice de la prochaine tâche à exécuter
  
  
  while(task<=N){ #Le serveur tourne tant qu'il lui reste des tâches à traiter
    if(arrival[task]<=t){ #Si une tâche est arrivée au temps courant t, on la traite
      t = t + service[task] #On avance le temps courant à la fin du traitement de la tâche
      completion[task] = t #La date de sortie de la tâche correspond au temps courant
      EnergyCons = EnergyCons + E[2]*service[task] #On calcule l'énergie consommée par le serveur actif pendant la durée de traitement de la tâche
      task = task +1 #On incrémente l'indice de la prochaine tâche à exécuter
    } else {
    #Sinon on avance le temps jusqu'à la prochaine arrivée, et on ajoute l'énergie consommée par le serveur passif pendant qu'il n'avait aucune tâche à traiter
      EnergyCons = EnergyCons + E[1]*(arrival[task]-t)
      t = arrival[task]
    }
  }
  return (c(mean(completion-arrival),EnergyCons/t))
}
ES1 = c(0.5,1) #Consommation énergétique du serveur 1
ES2 = c(0.25,4) #Consommation énergétique du serveur 2
ES3 = c(ES1,ES2) #Consommation énergétique des 2 serveurs
mu1 = 1 #taux de traitement du serveur 1
mu2 = 2 #taux de traitement du serveur 2
N = 100 #nombre de tâches
t = 100 #intervalle de temps

a = data.frame()

for(i in 1:t){ #On fait varier lambda entre 0 et 1
  for(j in 1:200){ #Pour chaque valeur de lambda on effectue 500 simulations
    #A chaque simulation, on calcule les valeurs pour les deux serveurs
    a = rbind(a,c(1,i/t,Serveur1_2(i/t,mu1,N,ES1,FALSE)))
    a = rbind(a,c(2,i/t,Serveur1_2(i/t,mu2,N,ES2,FALSE)))
  }
}

names(a)[1] = "serveur"
names(a)[2] = "lambda"
names(a)[3] = "Meantime"
names(a)[4] = "Energy"

serveur1 = data.frame()
serveur1 = a %>% filter(serveur == 1) %>% group_by(lambda) %>% summarise(meantime = mean(Meantime),energy = mean(Energy),ict = sd(Meantime)/sqrt(N),ice = sd(Energy)/sqrt(N))

serveur2 = data.frame()
serveur2 = a %>% filter(serveur == 2) %>% group_by(lambda) %>% summarise(meantime = mean(Meantime),energy = mean(Energy),ict = sd(Meantime)/sqrt(N),ice = sd(Energy)/sqrt(N))

#On affiche le temps moyen de réponse des deux serveurs avec un intervalle de confiance pour chaque valeur de lambda, représenté par geom_ribbon (plus joli que geom_errorbar)
ggplot() + geom_ribbon(data = serveur1, aes(ymin = meantime - 2*ict, ymax = meantime + 2*ict,x = lambda), fill="green", alpha=0.4, linetype=0) + geom_ribbon(data = serveur2, aes(ymin = meantime - 2*ict, ymax = meantime + 2*ict,x = lambda), fill="blue", alpha=0.4, linetype=0) + geom_line(data = serveur1,aes(x = lambda,y = meantime), color="darkgreen") + geom_line(data = serveur2,aes(x = lambda,y = meantime), color="darkblue")

On remarque que le serveur 1, dont la courbe est affichée en vert, a un temps moyen de traitement des tâches beaucoup plus lent que celui du serveur 2, surtout lorsque le taux d’inter arrivée des tâches augmente (ce qui est logique puisque son taux de service est de 1 donc plus les tâches arrivent vite, moins il est en capacité de les traiter rapidement).

On vérifie cela en faisant varier le lambda entre 0 et 2 (taux de service du serveur 2).

On remarque donc bien que plus le taux d’inter-arrivées augmente, plus la différence en efficacité entre les serveurs 1 et 2 s’accroît, avec un temps moyen de traitement des tâches 5 fois plus élevé pour le serveur 1 lorsque le taux d’inter-arrivées est à 2.

De plus au niveau de l’énergie, on remarque que la plupart du temps, le serveur 1 consomme moins que le serveur 2, à part si le taux d’arrivée des tâches est très faible. On remarque également que la consommation des serveurs augmente avec le taux d’arrivée des tâches mais que le serveur 1 atteint le maximum de sa consommation en 2 (avec lamda à 1). Ceci s’explique par son taux de traitement, qui est à 1. C’est à dire que si les tâches arrivent plus vite qu’il ne peut les traiter, il sera toujours actif et jamais en veille, et ceci peu importe la vitesse à laquelle les tâches arrivent (que lambda soit à 2 ou 3), donc sa consommation d’énergie n’augmentera pas plus.

#On affiche l'énergie moyenne consommée par les deux serveurs

ggplot() + geom_ribbon(data = serveur1_2, aes(ymin = energy - 2*ice, ymax = energy + 2*ice,x = lambda), fill="green", alpha=0.4, linetype=0) + geom_ribbon(data = serveur2_2, aes(ymin = energy - 2*ice, ymax = energy + 2*ice,x = lambda), fill="blue", alpha=0.4, linetype=0) + geom_line(data = serveur1_2,aes(x = lambda,y = energy),color="darkgreen") + geom_line(data = serveur2_2,aes(x = lambda,y = energy),color="darkblue")

On refait nos calculs dans l’intervalle entre 0,05 et 0.25 (graphiquement on voit que la valeur est autour de lambda=0.2) pour trouver pour quelle valeur de lambda le serveur 2 devient moins intéressant par rapport à sa consommation d’énergie:

On peut donc dire que pour lambda<=0.175 le serveur 2 est plus intéressant au niveau énergétique, et pour lambda>0.175 le serveur 1 est plus intéressant. Ceci dit l’efficacité au niveau temps de traitement des tâches du serveur 2 est indéniablement plus élevé, surtout pour lambda plus élevé (lambda>1).

Etude du serveur 3

Nous simulons le fonctionnement du serveur 3, qui utilise les deux précédents en parallèle afin de maximiser ses performances. On calcule sa vitesse moyenne, soit le temps moyen que les tâches prennent à s’exécuter, ainsi que l’énergie totale consommée par S1 et S2 sur le temps t. Notre fonction prend un taux d’inter-arrivées lambda, deux taux de service mu1 et mu2 correspondant aux taux de service de S1 et S2, un nombre de tâches N et un vecteur de consommation énergétique E où E[1] correspond à la consommation du serveur 1 lorsqu’il est en veille, E[2] à sa consommation lorsqu’il est actif, E[3] correspond à la consommation du serveur 2 lorsqu’il est en veille et E[4] à sa consommation lorsqu’il est actif

Serveur3 = function(lambda,mu1,mu2,N,E,Markovien){
  
  arrival = c(0,cumsum(rexp(n=N-1, rate=lambda))) #Date d'arrivée dans le système
  if(!Markovien){
    service1 = rexp(n=N, rate=mu1)#Temps de traitement de chaque tâche pour le serveur 1, exponentiel ou déterministe
    service2 = rexp(n=N, rate=mu2) #Temps de traitement de chaque tâche pour le serveur 2, exponentiel ou déterministe
  } else{
    service1 = rep(1/mu1, times=N)
    service2 = rep(1/mu2, times=N)
  }

  completion = rep(NA,times=N) #Date de sortie
  EnergyCons = 0 #Energie totale consommée
  enservice1 = FALSE #Etat du serveur 1
  enservice2 = FALSE #Etat du serveur 2
  currenttask1 = 0 #Tâche courante traitée par le serveur 1
  currenttask2 = 0 #Tâche courante traitée par le serveur 2
  
  t = 0 #Date du système
  task = 1 #Indice de la prochaine tâche à exécuter
  
  while(task<=N){ #Le serveur tourne tant qu'il lui reste des tâches à traiter
    if(arrival[task]<=t){ #Si une tâche est arrivée au temps courant t, on la traite
      if(!enservice1){ #Si le serveur 1 est libre, on lui alloue la tâche
        completion[task] = t + service1[task] #La date de sortie de la tâche correspond au temps courant incrémenté du temps de service de la tâche
        currenttask1 = task #Au temps t le serveur 1 traite la tâche d'indice task
        enservice1 = TRUE #Donc le serveur 1 est en service
        task = task + 1 #La tâche task a été traité, on passe à la suivante
        if(!enservice2){ #Si le serveur 2 ne travaille pas, le prochain événement est l'arrivée de la prochaine tache
          #On travaille sur l'intervalle de temps entre t et la prochaine arrivée
          if(task<=N & arrival[task]>t){
            EnergyCons = EnergyCons + (arrival[task]-t)*E[3] + ifelse(((arrival[task]-t)>service1[task-1]),(arrival[task]-t)*E[2],service1[task-1]*E[2]+(arrival[task]-t-service1[task-1])*E[1])
            t = arrival[task] # On avance le temps courant à la prochaine arrivée
          }
        } else { #Si les deux serveurs travaillent, le prochain événement est la prochaine date de sortie
          if(completion[currenttask1]<completion[currenttask2]){ #Si la prochaine date de sortie est celle de la tâche traitée par le serveur 1, on avance le temps courant à cette date et on indique que le serveur 1 ne travaille plus
            EnergyCons = EnergyCons + (completion[currenttask1]-t)*(E[2]+E[4])
            t = completion[currenttask1]
            enservice1 = FALSE
          } else { #Sinon on fait pareil avec le serveur 2
            EnergyCons = EnergyCons + (completion[currenttask2]-t)*(E[2]+E[4])
            t = completion[currenttask2]
            enservice2 = FALSE
          }
        }
      #Si le serveur 1 est déjà occupé, le serveur 2 s'occupe de la tâche
      } else if (!enservice2) {
        completion[task] = t + service2[task]
        currenttask2 = task
        task = task +1
        enservice2 = TRUE
        #Les deux serveurs sont occupés donc le prochain événement est la prochaine date de sortie
        if(completion[currenttask1]<completion[currenttask2]){
          EnergyCons = EnergyCons + (completion[currenttask1]-t)*(E[2]+E[4])
          t = completion[currenttask1]
          enservice1 = FALSE
        } else {
          EnergyCons = EnergyCons + (completion[currenttask2]-t)*(E[2]+E[4])
          t = completion[currenttask2]
          enservice2 = FALSE
        }
      }
    } else {
    #Si les deux serveurs sont occupés, on avance juste le temps jusqu'à la prochaine arrivée
      if(enservice1){
        if(completion[currenttask1]<arrival[task]){
          enservice1 = FALSE
          EnergyCons = EnergyCons + (completion[currenttask1]-t)*E[2] + (arrival[task]-completion[currenttask1])*E[1]
        } else {
          EnergyCons = EnergyCons + (arrival[task]-t)*E[2]
        }
        EnergyCons = EnergyCons + E[3]*(arrival[task]-t)
      } else if (enservice2){
         if(completion[currenttask2]<arrival[task]){
          enservice2 = FALSE
          EnergyCons = EnergyCons + (completion[currenttask2]-t)*E[4] + (arrival[task]-completion[currenttask2])*E[3]
        } else {
          EnergyCons = EnergyCons + (arrival[task]-t)*E[4]
        }
        EnergyCons = EnergyCons + E[1]*(arrival[task]-t)
      } else {
        EnergyCons = EnergyCons + (E[1]+E[3])*(arrival[task]-t)
      }
      t = arrival[task]
    }
  }
  return (c(mean(completion-arrival),EnergyCons/t))
}
b = data.frame()

for(i in 1:t*2){ #On fait varier lambda entre 0 et 2
  for(j in 1:200){ #Pour chaque valeur de lambda on effectue 500 simulations
    #A chaque simulation, on calcule les valeurs pour le troisième serveur
    b = rbind(b,c(i/t,Serveur3(i/t,mu1,mu2,N,ES3,FALSE)))
  }
}

names(b)[1] = "lambda"
names(b)[2] = "Meantime"
names(b)[3] = "Energy"

serveur3 = data.frame()
serveur3 = b %>% group_by(lambda) %>% summarise(meantime = mean(Meantime),energy = mean(Energy),ict = sd(Meantime)/sqrt(N),ice = sd(Energy)/sqrt(N))

ggplot() + geom_ribbon(data = serveur3, aes(ymin = meantime - 2*ict, ymax = meantime + 2*ict,x = lambda),fill="red", alpha=0.4, linetype=0) + geom_line(data = serveur3,aes(x = lambda,y = meantime),color="darkred")+ geom_ribbon(data = serveur2_2, aes(ymin = meantime - 2*ict, ymax = meantime + 2*ict,x = lambda),fill="blue", alpha=0.4, linetype=0) + geom_line(data = serveur2_2,aes(x = lambda,y = meantime),color="darkblue") + geom_ribbon(data = serveur1_2, aes(ymin = meantime - 2*ict, ymax = meantime + 2*ict,x = lambda), fill="green", alpha=0.4, linetype=0) + geom_line(data = serveur1_2,aes(x = lambda,y = meantime),color="darkgreen")

On remarque que le serveur 3, dont la courbe est affichée en rouge, a un temps moyen de traitement des tâches plus rapide que les deux serveurs précédent, ce qui correspond à nos intuitions et semble logique puisque le serveur 3 fait travailler ces deux serveurs en parallèle, il a donc plus de capacité de traitement des tâches et est donc plus efficace (surtout par rapport au serveur 1).

ggplot() + geom_ribbon(data = serveur3, aes(ymin = energy - 2*ice, ymax = energy + 2*ice,x = lambda), fill="red", alpha=0.4, linetype=0)+geom_line(data = serveur3,aes(x = lambda,y = energy),color="darkred") + geom_ribbon(data = serveur2_2, aes(ymin = energy - 2*ice, ymax = energy + 2*ice,x = lambda), fill="blue", alpha=0.4, linetype=0) + geom_line(data = serveur2_2,aes(x = lambda,y = energy),color="darkblue") + geom_ribbon(data = serveur1_2, aes(ymin = energy - 2*ice, ymax = energy + 2*ice,x = lambda), fill="green", alpha=0.4, linetype=0) + geom_line(data = serveur1_2,aes(x = lambda,y = energy),color="darkgreen")

En revanche au niveau énergétique il consomme aussi plus que le serveur 1 mais en général moins que le serveur 2. En effet, si pour un taux d’inter-arrivées de tâches faible il est moins efficace au niveau énergie que le serveur 2 puisqu’il fait travailler deux serveurs, dès que lambda devient supérieur à ~0.9 il devient plus intéressant que le second serveur puisqu’il traite aussi les tâches sur un serveur moins rapide mais consommant moins (le serveur 1).

Pour estimer la valeur limite de lambda à laquelle le serveur 3 devient plus intéressant au niveau énergétique, on refait nos calculs pour lambda entre 0.6 et 1.0 :

Donc on peut en conclure que le serveur 3 devient plus intéressant énergétiquement que le serveur 2 pour lambda>=0.9

Influence de l’hypothèse Markovienne

Jusqu’ici, on estimait que les temps de service étaient exponentiels. A présent, on cherche à refaire la comparaison de performances pour des temps de service déterministes de durées 1/mu1 et 1/mu2. Pour cela on reprend nos deux fonctions et on introduit un booléen Markovien, qui indique s’il est à faux que le temps de service est exponentiel, et s’il est à vrai que le temps de service est déterministe.

En comparant les courbes obtenues en utilisant un taux de traitement exponentiel et un taux d’arrivée déterministe, on observe peu de différences. On pourrait en conclure que le temps de traitement moyen et l’énergie dépensée par le serveur dépendent du taux d’arrivée et non pas du taux de traitement.