Question 1

Fonction simulant les serveurs S1 et S2

simul_1_server = function(N,arrivalRate,processRate,activeConso,passiveConso){
  
  #N le nombre total de taches à traiter
  
  #Date d'arrivée des taches sur la file d'attente, selon le taux d'arrivée
  Arrival = cumsum(rexp(n=N, rate=arrivalRate)); 
  #Temps de traitement nécessaire à chaque tache, selon le taux de service
  Process = rexp(n=N, rate =processRate);
  #Table indiquant les taches terminées et leur date de fin
  Completion = rep(N, x=NA);
  #Table indiquant le temps restant d'exécution pour chaque tache arrivée
  Remaining = rep(N, x=NA);
  
  #Consommation totale du serveur
  Consommation = 0
  
  #Initialisation du temps, de la tache courante et de la prochaine tache
  t = 0;
  CurrentTask = NA;
  NextArrival = 1;
  
  while(TRUE){
    
    dtA = NA; # temps jusqu'à la prochaine arrivée
    dtC = NA; # temps jusqu'à la prochaine terminaison
    
    if(length(Arrival[Arrival>t])>0) {
        dtA = head(Arrival[Arrival>t],n=1)-t  
    }
    if(!is.na(CurrentTask)) {
        dtC = Remaining[CurrentTask]; 
    }
    
     #Condition de fin : s'il ne reste plus de tache à exécuter et en cours d'exécution
    if(is.na(dtA) & is.na(dtC)) {
      break;
    } 
    
    dt = min(dtA,dtC,na.rm=T)
    
    # Mettre à jour comme il faut:
    
    #   la date
    t = t + dt;
    
    #   la consommation
    if(is.na(CurrentTask)){
      Consommation = Consommation + dt*passiveConso;
    } else{
      Consommation = Consommation + dt*activeConso;
    }
    
    #   les arrivées
    if((NextArrival <=N) & (Arrival[NextArrival] <= t)) { #On met un <= et non pas un == pour la seconde condition, afin d'éviter les erreurs d'arrondies
        Remaining[NextArrival] = Process[NextArrival];
        NextArrival = NextArrival + 1;
    }
    
    #   le remaining 
    if(!is.na(CurrentTask)) {
        Remaining[CurrentTask] = Remaining[CurrentTask] - dt ;
        if(Remaining[CurrentTask] <= 0) {
            Completion[CurrentTask] = t;
            Remaining[CurrentTask] = NA;
        }
        CurrentTask = NA;
    }
    #   et currentTask (politique d'ordonnancement: FIFO)
    WaitingList=(1:N)[!is.na(Remaining)];
    if(length(WaitingList)>0) {
        CurrentTask = head(WaitingList,n=1);
    }
  }
  
  return(data.frame(
        arrivalRate = arrivalRate,
        Consommation = Consommation/t,
        Arrival = Arrival,
        Process = Process,
        Completion = Completion
        ))
}

Remplissage des différentes dataframes

S1 = data.frame()

S2 = data.frame()

#Nous stoppons la simulation à un arrivalRate de 1.5, le processRate étant de 1, les données qu'on obtiendrait en saturant davantage le serveur n'aurait pas de sens
for(i in seq(from = 0.1, to = 1.5, by = 0.1)){ 
  for(j in 1:10){ #On répète 10 fois chaque configuration, afin d'éviter les mesures divergentes
    S1 = rbind(S1,simul_1_server(N=300,arrivalRate=i,processRate=1,activeConso=1,passiveConso=0.5))
  }
}

#Nous stoppons la simulation à un arrivalRate de 2.5, le processRate étant de 2
for(i in seq(from = 0.1, to = 2.5, by = 0.1)){
  for(j in 1:10){
    S2 = rbind(S2,simul_1_server(N=300,arrivalRate=i,processRate=2,activeConso=4,passiveConso=0.25))
  }
}

#Poissonnage selon le serveur d'origine
S1 = cbind(S1,data.frame(Config="S1"))
S2 = cbind(S2,data.frame(Config="S2"))

#Dataframe regroupant tout les données
S = rbind(S1,S2)

#Dataframe pour l'affichage, on calcule le temps de réponse, on garde les colonnes interressantes et on calcule les moyennes
S_print <-S %>% mutate(ResponseTime = Completion - Arrival - Process) %>% select(arrivalRate,Consommation,Config,ResponseTime) %>% group_by(arrivalRate,Config) %>% summarise(meanCons = mean(Consommation), sdCons = sd(Consommation), meanResp = mean(ResponseTime), sdResp = sd(ResponseTime))

Affichage

1)

# Le ruban autour des courbes indique l'intervalle de confiance
ggplot(data = S_print) + geom_line(aes(x = arrivalRate, y = meanResp,colour = Config)) + geom_ribbon(aes(x = arrivalRate, ymin = meanResp - sdResp, ymax = meanResp + sdResp, fill = Config), alpha = 0.2 , linetype = 0) + xlab("Arrival Rate") + ylab("Mean Response Time")

Les courbes ont les trajectoires attendues, leur temps de réponse moyen s’accroit rapidement quand on s’approche et qu’on dépasse leur capacité de traitement et elles s’arrêtent aux taux d’arrivée prévus. On observe que de manière générale, S2 est plus réactif que S1. Pour des taux d’arrivée inférieurs à 0.5, la différence n’est pas très importante mais s’accroit par la suite. On remarque cependant, grâce aux intervalles de confiance, qu’on peut rencontrer des situations où S1 est plus performant que S2 pour un taux d’arrivée proche de 1 (l’intervalle rouge de S1 s’étendant en dessous du bleu de S2, en lambda = 0.75 notamment). A l’inverse, pour lambda supérieure à 1, S2 est toujours plus performant que S1.

2)

ggplot(data = S_print) + geom_line(aes(x = arrivalRate, y = meanCons,colour = Config)) + geom_ribbon(aes(x = arrivalRate, ymin = meanCons - sdCons, ymax = meanCons + sdCons, fill = Config), alpha = 0.2 , linetype = 0) + xlab("Arrival Rate") + ylab("Mean Consommation")

Pour la consommation énergétique, S1 semble beaucoup plus économique que S2 à partir d’un certain lambda. Celà est dû au fait que S2 a une consommation passive plus faible que S1 mais une consommation active plus élevée donc dès que le taux d’arrivée est assez élevé, S2 est suffisamment actif pour que sa consommation dépasse celle de S1.

3)

ggplot(data = S_print) + geom_line(aes(x = arrivalRate, y = meanCons,colour = Config)) + geom_ribbon(aes(x = arrivalRate, ymin = meanCons - sdCons, ymax = meanCons + sdCons, fill = Config), alpha = 0.2 , linetype = 0) + xlab("Arrival Rate") + ylab("Mean Consommation") + coord_cartesian(xlim=c(0.150,0.200),ylim = c(0.45,0.7))

Il est plus rentable d’un point de vue énergétique d’utiliser le S1 plutot que le S2, pour un taux d’arrivée supérieure ou égal à environs 0.18, comme le zoom ci-dessus l’illuste.

Question 2

Fonction simulant le serveur S3

simul_2_servers = function(N,arrivalRate,processRate1,activeConso1,passiveConso1,processRate2,activeConso2,passiveConso2){
  
  #N le nombre total de taches à traiter
  
  #Date d'arrivée des taches sur la file d'attente, selon le taux d'arrivée
  Arrival = cumsum(rexp(n=N, rate=arrivalRate)); 
  #Temps de traitement nécessaire à chaque tache, pour le serveur 1
  Process = rexp(n=N, rate =processRate1);
  #Ratio de temps pour passer d'un temps de traitement pour le serveur 1 vers un temps de traitement pour le serveur 2
  Rate1_2 = processRate1 / processRate2
  #Table indiquant les taches terminées et leur date de fin
  Completion = rep(N, x=NA);
  #Table indiquant le temps restant d'exécution pour chaque tache arrivée
  Remaining = rep(N, x=NA);
  
  #Consommation totale du serveur
  Consommation = 0
  
  #Initialisation du temps et de la prochaine tache
  t = 0;
  NextArrival = 1;
  #Initialisation de la tache courante de chaque serveur
  CurrentTask1 = NA;
  CurrentTask2 = NA;
  
  while(TRUE){
    
    dtA = NA; # temps jusqu'à la prochaine arrivée
    dtC1 = NA; # temps jusqu'à la prochaine terminaison du serveur 1
    dtC2 = NA; # temps jusqu'à la prochaine terminaison du serveur 2
    
    if(length(Arrival[Arrival>t])>0) {
        dtA = head(Arrival[Arrival>t],n=1)-t  
    }
    if(!is.na(CurrentTask1)) {
        dtC1 = Remaining[CurrentTask1]; 
    }
    if(!is.na(CurrentTask2)) {
        dtC2 = Remaining[CurrentTask2]; 
    }
    
     #Condition de fin : s'il ne reste plus de tache à exécuter et en cours d'exécution
    if(is.na(dtA) & is.na(dtC1) & is.na(dtC2)) {
      break;
    } 
    
    dt = min(dtA,dtC1,dtC2,na.rm=T)
    
    # Mettre à jour comme il faut:
    
    #   la date
    t = t + dt;
    
    #   la consommation
    if(is.na(CurrentTask1)){
      Consommation = Consommation + dt*passiveConso1;
    } else{
      Consommation = Consommation + dt*activeConso1;
    }
    
    if(is.na(CurrentTask2)){
      Consommation = Consommation + dt*passiveConso2;
    } else{
      Consommation = Consommation + dt*activeConso2;
    }
    
    #   les arrivées
    if((NextArrival <=N) & (Arrival[NextArrival] <= t)) { #On met un <= et non pas un == pour la seconde condition, afin d'éviter les erreurs d'arrondies
        Remaining[NextArrival] = Process[NextArrival]; #Par défaut, le temps d'exécution est celui du serveur 1, on effectura le changement à l'attribution de la tache
        NextArrival = NextArrival + 1;
    }
    
    #   le remaining 
    if(!is.na(CurrentTask1)) {
      Remaining[CurrentTask1] = Remaining[CurrentTask1] - dt ;
      if(Remaining[CurrentTask1] <= 0) {
          Completion[CurrentTask1] = t;
          Remaining[CurrentTask1] = NA;
          CurrentTask1 = NA;
      }
    }
    
    if(!is.na(CurrentTask2)) {
      Remaining[CurrentTask2] = Remaining[CurrentTask2] - dt ;
      if(Remaining[CurrentTask2] <= 0) {
          Completion[CurrentTask2] = t;
          Remaining[CurrentTask2] = NA;
          CurrentTask2 = NA;
      }
    }
    
    #   et les currentTask (politique d'ordonnancement: FIFO)
    WaitingList=(1:N)[!is.na(Remaining)];
    if(length(WaitingList)>0 & (is.na(CurrentTask1) | is.na(CurrentTask2))) { #On vérifie que la WaitingList n'est pas vide et qu'un des 2 serveurs est libre
      
      if(is.na(CurrentTask1) & !is.na(CurrentTask2) & length(WaitingList)>1){ #Si un seul serveur est libre et qu'une tache est libre, on lui attribue celle-ci
        CurrentTask1 = WaitingList[2];
      } else if(!is.na(CurrentTask1) & is.na(CurrentTask2) & length(WaitingList)>1){
        CurrentTask2 = WaitingList[2];
        Remaining[CurrentTask2] = Remaining[CurrentTask2] * Rate1_2 #On ajuste le temps d'exécution
        Process[CurrentTask2] = Process[CurrentTask2] * Rate1_2
      } else{ #Sinon, si les 2 serveurs sont libres
        if(length(WaitingList)==1 & is.na(CurrentTask1) & is.na(CurrentTask2)){ #Soit on attribue l'unique tache disponible, au hasard entre les 2 serveurs libres
          if(runif(1)<0.5){
            CurrentTask1 = head(WaitingList,n=1);
          }else{
            CurrentTask2 = head(WaitingList,n=1);
            Remaining[CurrentTask2] = Remaining[CurrentTask2] * Rate1_2 #On ajuste le temps d'exécution
            Process[CurrentTask2] = Process[CurrentTask2] * Rate1_2
          }
        } else if(length(WaitingList)>1) {
          #Soit on attribue les 2 premières taches dans un ordre ou l'inverse, au hasard aussi
          if(runif(1)<0.5){
            CurrentTask1 = WaitingList[1];
            CurrentTask2 = WaitingList[2];
          }else{
            CurrentTask1 = WaitingList[2];
            CurrentTask2 = WaitingList[1];
          }
          Remaining[CurrentTask2] = Remaining[CurrentTask2] * Rate1_2 #On ajuste le temps d'exécution
          Process[CurrentTask2] = Process[CurrentTask2] * Rate1_2
        }
      }
    }
  }
  
  return(data.frame(
        arrivalRate = arrivalRate,
        Consommation = Consommation/t,
        Arrival = Arrival,
        Process = Process,
        Completion = Completion
  ));
}

Remplissage des différentes dataframes

S3 = data.frame()

#On simule jusqu'à un lambda de 3.5, S3 ayant un taux de traitement de 3
for(i in seq(from = 0.1, to = 3.5, by = 0.1)){ 
  for(j in 1:10){
    S3 = rbind(S3,simul_2_servers(N=300,arrivalRate=i,processRate1=1,activeConso1=1,passiveConso1=0.5,processRate2=2,activeConso2=4,passiveConso2=0.25))
  }
}

S3 = cbind(S3,data.frame(Config="S3"))

#Nouvelle matrice de résultat S
S = rbind(S1,S2,S3)

#Nouvelle matrice d'affichage, avec les mêmes opérations que précédemment
S_print <-S %>% mutate(ResponseTime = Completion - Arrival - Process) %>% select(arrivalRate,Consommation,Config,ResponseTime) %>% group_by(arrivalRate,Config) %>% summarise(meanCons = mean(Consommation), sdCons = sd(Consommation), meanResp = mean(ResponseTime), sdResp = sd(ResponseTime))

Affichage

1)

ggplot(data = S_print) + geom_line(aes(x = arrivalRate, y = meanResp,colour = Config)) + geom_ribbon(aes(x = arrivalRate, ymin = meanResp - sdResp, ymax = meanResp + sdResp, fill = Config), alpha = 0.2 , linetype = 0) + xlab("Arrival Rate") + ylab("Mean Response Time")

ggplot(data = S_print) + geom_line(aes(x = arrivalRate, y = meanCons,colour = Config)) + geom_ribbon(aes(x = arrivalRate, ymin = meanCons - sdCons, ymax = meanCons + sdCons, fill = Config), alpha = 0.2 , linetype = 0) + xlab("Arrival Rate") + ylab("Mean Consommation")

S3 est beaucoup plus performant que S1 et S2, du point de vue du temps de réponse moyen. Cependant sa consommation moyenne croise à 2 reprises celle de S2, indiquant que S3 est rentable sur le plan énergétique pour certains lambdas seulement, par rapport à S2.

2)

ggplot(data = S_print) + geom_line(aes(x = arrivalRate, y = meanCons,colour = Config)) + geom_ribbon(aes(x = arrivalRate, ymin = meanCons - sdCons, ymax = meanCons + sdCons, fill = Config), alpha = 0.2 , linetype = 0) + xlab("Arrival Rate") + ylab("Mean Consommation") + coord_cartesian(xlim=c(0.5,2.5),ylim = c(1.5,4))

ggplot(data = S_print) + geom_line(aes(x = arrivalRate, y = meanCons,colour = Config)) + geom_ribbon(aes(x = arrivalRate, ymin = meanCons - sdCons, ymax = meanCons + sdCons, fill = Config), alpha = 0.2 , linetype = 0) + xlab("Arrival Rate") + ylab("Mean Consommation") + coord_cartesian(xlim=c(0.7,0.78),ylim = c(1.6,1.8))

ggplot(data = S_print) + geom_line(aes(x = arrivalRate, y = meanCons,colour = Config)) + geom_ribbon(aes(x = arrivalRate, ymin = meanCons - sdCons, ymax = meanCons + sdCons, fill = Config), alpha = 0.2 , linetype = 0) + xlab("Arrival Rate") + ylab("Mean Consommation") + coord_cartesian(xlim=c(2.30,2.35),ylim = c(3.9,4.1))

Il est donc plus rentable d’un point de vue énergétique d’utiliser le S3 plutot que le S2, pour un taux d’arrivée compris entre 0.74 et 2.30. Celà s’explique par le fait que S3 utilise 2 serveurs, il est donc plutot rare, pour des lambdas assez faibles, que les serveurs tournent en même temps donc que sa consommation soit maximale. Cependant, pour des taux d’arrivée trop faible, on utilise parfois les 2 serveurs de S3 en même temps alors qu’on pourrait n’en utiliser qu’un et étaler les traitements dans le temps.

Conclusion

Pour des taux d’arrivée entre 0 et 1, si on n’a pas besoin de temps de réponses très réduits, S1 semble une bonne solution en terme de consommation et de performances. Pour des lambdas plus élevés, S3 est une meilleure solution que S2 en terme de performance mais n’est pas toujours la plus rentable économiquement. Il faut donc évaluer l’importance que l’on donne aux performances et à la consommation, ainsi que le lambda moyen. On pourrait également améliorer la configuration S3, en calculant une estimation du temps d’arrivée, afin de définir quel serveur utiliser en priorité (le serveur S1 en priorité pour des lambdas faibles), plutôt que de choisir un des 2 serveurs de S3 au hasard, comme je l’ai fait. On pourrait également utiliser seulement 1 seul des 2 serveurs pour des lambdas très faibles. Cette amélioration pourait réduire la consommation de S3 mais n’est efficace que pour des taux d’arrivée assez constant (un lambda trop variable empêcherait une bonne estimation).