Compromis énergétique de serveurs.

Question 1. Comparaison de la performance de \(S_1\) et de \(S_2\)

On utilise la fonction suivante pour modéliser le comportement d’un serveur (file d’attente M/M/1 en mode FIFO). La valeur \(\lambda\) correspond au taux d’inter-arrivés et \(\mu\) le taux des temps de service.

mm1_sim <- function(lambda,mu,n, norm_power, sleep_power){
  
  # Dates d'arrivée dans le système
  Arrival = c(0,cumsum(rexp(n=n-1,rate=lambda)))
  
  # Temps de traitement des tâches
  Service = rexp(n=n,rate=mu)
  
  # Dates de fin de traitement
  Completion = rep(NA,times = n)
  
  #==#==#==#==#==#==#==#==#==#
  
  # Temps restant à la tâche n
  Remaining = Service
  
  # Traitement en cours
  CurrClient = NA
  
  LastClient = 0
  
  # Prochain à arriver
  NextArrival = 1
  
  t = 0; # Temps courant  
  
  InstPower = 0
  
  while(TRUE){
    
    # Temps jusqu'à la prochaine arrivée
    dtA = ifelse(NextArrival > n, NA, Arrival[NextArrival] - t)
    
    # Temps jusqu'à la prochaine terminaison
    dtC = ifelse(is.na(CurrClient),NA,Remaining[CurrClient])
    
    
    # On s'arrête lorsque tout est fini, et qu'il n'y a plus d'arrivée attendue
    if (is.na(dtA) & is.na(dtC)) {break;}
    
    # Le prochain événement dans le temps est dans dt "secondes"
    dt = min(dtA,dtC,na.rm = TRUE);
    
    # Puissance instantanée pour l'inter-temps
    if (is.na(CurrClient)){
      InstPower = InstPower + (dt * sleep_power) # Traitement en cours
    } else {
      InstPower = InstPower + (dt * norm_power) # Serveur en veille
    }
    
    # On passe au prochain événement
    # Soit une arrivée, soit une terminaison, soit les deux
    t = t + dt
    
    
    # Tâche en cours
    if (!is.na(CurrClient)){
      Remaining[CurrClient] = Remaining[CurrClient] - dt
      
      #Tâche finie
      if (Remaining[CurrClient]<=0) {
        Completion[CurrClient] = t
        #LastClient = CurrClient
        CurrClient = NA
      }
    }
    
    # Arrivée d'une tâche
    if (NextArrival <= n && Arrival[NextArrival] <= t){
       NextArrival = NextArrival + 1
    }
    
    # Sélection du prochain client, si il est arrivé
    if (is.na(CurrClient) && LastClient < n && Arrival[LastClient+1] <= t) {
        
      CurrClient = LastClient + 1
      LastClient = LastClient + 1  
    }
    
  }
  
  InstPower = InstPower/t
  
  #==#==#==#==#==#==#==#==#==#==#
  
  return(data.frame(lambda,mu,Arrival,Completion, InstPower))
}

On réalise la simulation sur l’intervalle [0.05,1] avec un pas de 0.05. On donne 1000 tâches à effectuer au serveur.

#-- Temps de services pour les serveurs S1 et S2
mu_1 = 1
mu_2 = 2

#-- Profils énergétiques
V_1 = 0.5 # Veille sur S1
V_2 = 0.25 # Veille sur S2
  
E_1 = 1   # Traitement sur S1
E_2 = 4   # Traitement sur S2

#-- Durée de la simulation
n = 1000

#==#==#==#==#==#==#==#==#==#==#==#

lambdas = seq.default(from=0.05,to=1.0,by=0.05)

df <- data.frame()
for (lambda in lambdas){
  
  df <- rbind(df,mm1_sim(lambda = lambda,mu = mu_1,n = n,norm_power = E_1,sleep_power = V_1)) 
  
  df <- rbind(df,mm1_sim(lambda = lambda,mu = mu_2,n = n, norm_power= E_2, sleep_power = V_2))
}

 df <- (df %>% mutate(Response = Completion - Arrival))
 df <- (df %>% group_by(lambda,mu,InstPower) %>% summarise(resp_avg = mean(Response),resp_sd = sd(Response)/sqrt(n)))
  1. Estimation de l’espérance du temps de réponse des deux serveurs
 ggplot(data = df, aes(x = lambda, y = resp_avg,group = factor(mu), color = factor(mu))) + geom_line(size = 0.5) + geom_smooth(method = 'loess')  + xlab(label = expression(paste(lambda," Arrival Rate"))) + ylab("Response Time") + labs(color = "Server") 

  1. Estimation de l’espérance de la consommation énergétique des deux serveurs
 ggplot(data = df, aes(x = lambda, y = InstPower, group = factor(mu), color = factor(mu)))  + labs(color = "Server") + geom_line() + geom_smooth(method = 'loess') + xlab(label = expression(paste(lambda," Arrival Rate"))) + ylab("Power Consumption")

  1. On voit que pour une valeur \(\lambda\) aux alentours de 0.2, le serveur 1 devient plus intéressant que le serveur 2. Intuitivement, on s’attendait à ce résultat. En effet, au bout d’une certaine vitesse d’arrivée des tâches, les deux serveurs se mettent à fonctionner en permanence. A ce moment là, le serveur 2 consomme alors 4 fois (!) plus d’énergie que le serveur 1. On va réaliser une nouvelle simulation entre 0.15 et 0.20 pour obtenir un encadrement plus précis de la valeur charnière. Pour augmenter la précision, on va cette fois-ci simuler un travail à réaliser de 10000 tâches.
lambdas = seq.default(from=0.15,to=0.20,by=0.001)
n = 10000

df <- data.frame()
for (lambda in lambdas){
  
  df <- rbind(df,mm1_sim(lambda = lambda,mu = mu_1,n = n,norm_power = E_1,sleep_power = V_1)) 
  
  df <- rbind(df,mm1_sim(lambda = lambda,mu = mu_2,n = n, norm_power= E_2, sleep_power = V_2))
}

 df <- (df %>% mutate(Response = Completion - Arrival))
 df <- (df %>% group_by(lambda,mu,InstPower) %>% summarise(resp_avg = mean(Response),resp_sd = sd(Response)/sqrt(n)))

  ggplot(data = df, aes(x = lambda, y = InstPower, group = factor(mu), color = factor(mu))) + geom_line() + geom_smooth(method = 'loess') + xlab(label = expression(paste(lambda," Arrival Rate"))) + ylab("Power Consumption")

On peut voir que la valeur \(\lambda\) charnière se situe un peu après 0.18. On a donc \(\lambda_{pivot} = 0.18 \pm 0.1\)

Question 2. Etude du serveur \(S_3\)

On utilise la fonction suivante pour modéliser le comportement de deux serveurs fonctionnant en parallèle.

# - Simulation file MM1
# - pour 2 serveurs 
# - avec calcul de puissance instantanée
mm1_sim_parallel <- function(lambda,mu_1,mu_2,n, norm_power_1, sleep_power_1,norm_power_2,sleep_power_2){
  
  # Dates d'arrivée dans le système
  Arrival = c(0,cumsum(rexp(n=n-1,rate=lambda)))
  
  # Temps de traitement des tâches
  Service_1 = rexp(n=n,rate=mu_1) # Pour le serveur 1
  Service_2 = rexp(n=n,rate=mu_2) # Pour le serveur 2
  
  # Dates de fin de traitement
  Completion = rep(NA,times = n)
  
  #==#==#==#==#==#==#==#==#==#
  
  # Temps restant à la tâche n
  Remaining_1 = Service_1 # Si la tâche n tourne sur le serveur 1
  Remaining_2 = Service_2 # Si la tâche n tourne sur le serveur 2
  
  
  # Traitement en cours
  CurrClient_1 = NA
  CurrClient_2 = NA
  
  LastClient = 0
  
  # Prochain à arriver
  NextArrival = 1
  
  # Temps courant
  t = 0   
  
  InstPower = 0
  
  while(TRUE){
    
    # Temps jusqu'à la prochaine arrivée
    dtA = ifelse(NextArrival > n, NA, Arrival[NextArrival] - t)
    
    # Temps jusqu'à la prochaine terminaison
    if (is.na(CurrClient_1)) {
        if (is.na(CurrClient_2)){
          dtC = NA  # Aucun des deux serveurs ne fonctionne, pas de terminaison prévue
        } else {
          dtC = Remaining_2[CurrClient_2]  # Prochaine terminaison sur le serveur 2
        }
    } else {
        if (is.na(CurrClient_2)){
          dtC  = Remaining_1[CurrClient_1] # Prochaine terminaison sur le serveur 1
        } else {
          dtC = min(Remaining_1[CurrClient_1],Remaining_2[CurrClient_2]) # Prochaine terminaison sur 1 ou 2
        }
    }
    
    # On s'arrête lorsque tout est fini, et qu'il n'y a plus d'arrivée attendue
    if (is.na(dtA) & is.na(dtC)) {break}
    
    # Le prochain événement dans le temps est dans "dt" secondes
    dt = min(dtA,dtC,na.rm = TRUE)
    
    
    # Puissance instantanée pour l'inter-temps
    # Serveur 1
    if (is.na(CurrClient_1)){
      InstPower = InstPower + (dt * sleep_power_1) # Traitement en cours sur 1
    } else {
      InstPower = InstPower + (dt * norm_power_1) # Serveur 1 en veille
    }
    
    # Serveur 2
    if (is.na(CurrClient_2)){
      InstPower = InstPower + (dt * sleep_power_2) # Traitement en cours sur 2
    } else {
      InstPower = InstPower + (dt * norm_power_2) # Serveur 2 en veille
    }
    
    
    # On passe au prochain événement
    # Soit une arrivée, soit une terminaison, soit les deux
    t = t + dt
    
    
    # Tâche en cours sur le premier serveur
    if (!is.na(CurrClient_1)){
      Remaining_1[CurrClient_1] = Remaining_1[CurrClient_1] - dt
      #Tâche finie
      if (Remaining_1[CurrClient_1]<=0) {
        Completion[CurrClient_1] = t
        CurrClient_1 = NA
      }
    }
    
    # Tâche en cours sur le deuxième serveur
    if (!is.na(CurrClient_2)){
      Remaining_2[CurrClient_2] = Remaining_2[CurrClient_2] - dt
      #Tâche finie
      if (Remaining_2[CurrClient_2]<=0) {
        Completion[CurrClient_2] = t
        CurrClient_2 = NA
      }
    }
    
    # Arrivée d'une tâche
    if (NextArrival <= n && Arrival[NextArrival] <= t){
       NextArrival = NextArrival + 1
    }
    
    # Sélection du prochain client pour le serveur 1, si il est arrivé
    if (is.na(CurrClient_1) && LastClient < n && Arrival[LastClient+1] <= t) {
      LastClient = LastClient + 1
      CurrClient_1 = LastClient
    }
    
    # Sélection du prochain client pour le serveur 2, si il est arrivé
    if (is.na(CurrClient_2) && LastClient < n && Arrival[LastClient+1] <= t) {
      LastClient = LastClient + 1  
      CurrClient_2 = LastClient
    }
  }
  
  InstPower = InstPower/t
  
  #==#==#==#==#==#==#==#==#==#==#
  
  return(data.frame(lambda,mu = mu_1+mu_2,Arrival,Completion,InstPower))
}
  1. Performances du serveur \(S_3\)
lambdas = seq.default(from=0.05,to=3.0,by=0.1)

df <- data.frame()
for (lambda in lambdas){
  
  df <- rbind(df,mm1_sim(lambda = lambda,mu = mu_1,n = n,norm_power = E_1,sleep_power = V_1)) 
  
  df <- rbind(df,mm1_sim(lambda = lambda,mu = mu_2,n = n, norm_power= E_2, sleep_power = V_2))
  
  df <- rbind(df,mm1_sim_parallel(lambda=lambda, mu_1=mu_1,mu_2=mu_2,n=n,norm_power_1 = E_1,norm_power_2 = E_2,sleep_power_1 = V_1, sleep_power_2 = V_2))
  
}

 df <- (df %>% mutate(Response = Completion - Arrival))
 df <- (df %>% group_by(lambda,mu,InstPower) %>% summarise(resp_avg = mean(Response),resp_sd = sd(Response)/sqrt(n)))

  ggplot(data = df, aes(x = lambda, y = resp_avg,group = factor(mu), color = factor(mu))) + geom_line(size = 0.5) + geom_smooth(method = 'loess')  + xlab(label = expression(paste(lambda," Arrival Rate"))) + ylab("Response Time") + labs(color = "Server") 

  ggplot(data = df, aes(x = lambda, y = InstPower, group = factor(mu), color = factor(mu))) + geom_line() + geom_smooth(method = 'loess') + xlab(label = expression(paste(lambda," Arrival Rate"))) + ylab("Power Consumption") + labs(color = "Server")

  1. Pour une valeur \(\lambda\) comprise grossièrement entre 0.5 et 2.5, il est plus intéressant d’utiliser le serveur 3 que le serveur 2 énergétiquement parlant. Pour un \(\lambda\) petit, dans notre cas, le serveur 3 utilise le petit serveur 1 à faible consommation pendant que le serveur 2 reste majoritairement en pause, mais le serveur 1 étant plus lent que le 2, sa consommation finale est plus grande. Quand \(\lambda\) continue d’augmenter, le serveur 3 devient alors plus efficace énergétiquement que le serveur 2, car le fait d’utiliser ses 2 sous-serveurs permet d’augmenter le temps global pendant lequel les serveurs sont en veille. Lorsque \(\lambda\) devient plus grand, le serveur 2 finit par atteindre un stade de fonctionnement à plein régime en continu. Sa consommation énergétique est alors bornée. Et il redevient plus intéressant de l’utiliser (en revanche, cela coïncide aussi avec l’envolée du temps de réponse pour le serveur 2).