Compromis énergétiques de serveurs

L’objectif de ce DM est d’analyser les performances de serveurs modélisés par une file d’attente simple de type M/M/1 en mode FIFO en fonction du taux d’inter-arrivées λ. On va étudier l’espérance du temps de réponse des tâches ainsi que l’espérance de la consommation énergétique de différents serveurs.

Simulateurs

Pour pouvoir générer des données que l’on peut analyser, on a créé deux simulateurs. Un premier qui simule le fonctionnement d’un seul serveur, et un deuxième qui simule le fonctionnement de deux serveurs en parallèle.

set.seed(37)

### Ce simulateur simule le fonctionnement d'un seul serveur

simul = function(N=100, arrival_rate=.2, processing_rate=1, debug=FALSE, deterministic=FALSE, energy_working, energy_idle,number) {
    Arrival = cumsum(rexp(n=N, rate=arrival_rate))
    if(deterministic) {
      Service = rep(N, x=1/processing_rate)
    } else {
      Service = rexp(n=N, rate=processing_rate)
    }
    Remaining = rep(N, x=NA)
    Completion = rep(N, x=NA)
    t = 0
    CurrentTask = NA
    NextArrival = 1
    WorkingTime = 0
    while (TRUE) {
        dtA = NA
        dtC = NA
        if(length(Arrival[Arrival>t])>0) {
            dtA = head(Arrival[Arrival>t],n=1)-t  # temps jusqu'à la prochaine arrivée
        }
        if(!is.na(CurrentTask)) {
            dtC = Remaining[CurrentTask] # temps jusqu'à la prochaine terminaison
        }
        if(is.na(dtA) & is.na(dtC)) {
            break
        } 
        dt = min(dtA,dtC,na.rm=T)
        
        # Mise à jour :
        #   la date
        t = t + dt
        #   les arrivées
        if((NextArrival <=N) & (Arrival[NextArrival] <= t)) { ## je met un <= et pas un == car 3-2.9!=0.1 ...
            Remaining[NextArrival] = Service[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
            WorkingTime = WorkingTime + dt
        }
        #   et currentTask (politique d'ordonnancement: FIFO)
        WaitingList=(1:N)[!is.na(Remaining)]
        if(length(WaitingList)>0) {
            CurrentTask = head(WaitingList,n=1)
        }
    }
    
    # calcul de la consommation d'énergie totale
    totalEnergy = WorkingTime*energy_working + (tail(Completion,1)-WorkingTime)*energy_idle
    totalEnergy = totalEnergy/t
    return(data.frame(
        Arrival_rate=arrival_rate,
        Processing_rate=processing_rate,
        Arrival = Arrival,
        Completion = Completion,
        Energy = totalEnergy,
        Server = number
        ))
        
}

### Ce simulateur simule le fonctionne de deux serveurs en parallèle

simul2 = function(N=100, arrival_rate=0.2, processing_rate1, processing_rate2, debug=FALSE, deterministic=FALSE, energy_working1, energy_idle1, energy_working2, energy_idle2,number) {
    Arrival = cumsum(rexp(n=N, rate=arrival_rate))
    if(deterministic){
      Service1 = rep(N,x=1/processing_rate1)
      Service2 = rep(N,x=1/processing_rate2)
    } else {
      Service1 = rexp(n=N, rate =processing_rate1)
      Service2 = rexp(n=N, rate =processing_rate2)
    }
    Remaining = rep(N, x=NA)
    Completion = rep(N, x=NA)
    t = 0
    CurrentTask1 = NA
    CurrentTask2= NA
    NextArrival = 1
    WorkingTime1 = 0
    WorkingTime2 = 0
    while (TRUE) {
        dtA = NA
        dtC = NA
        if(length(Arrival[Arrival>t])>0) {
            dtA = head(Arrival[Arrival>t],n=1)-t  # temps jusqu'à la prochaine arrivée
        }
        if(!is.na(CurrentTask1) || (!is.na(CurrentTask2))) {
            dtC = min(Remaining[CurrentTask1],Remaining[CurrentTask2],na.rm=T) # temps jusqu'à la prochaine terminaison
        }
        if(is.na(dtA) & is.na(dtC)) {
            break
        } 
        dt = min(dtA,dtC,na.rm=T)
        
        # Mise à jour :
        #   la date
        t = t + dt
        #   les arrivées
        if((NextArrival <=N) & (Arrival[NextArrival] <= t)) { ## je met un <= et pas un == car 3-2.9!=0.1 ...
            Remaining[NextArrival] = 999  #on met une valeur pour que ça n'ait plus la valeur NA
            NextArrival = NextArrival + 1
        }
        
        #   le remaining du serveur 1
        if(!is.na(CurrentTask1)) {
            Remaining[CurrentTask1] = Remaining[CurrentTask1] - dt
            if(Remaining[CurrentTask1] <= 0) {
                Completion[CurrentTask1] = t
                Remaining[CurrentTask1] = NA
                CurrentTask1=NA
            }
            WorkingTime1 = WorkingTime1 + dt
        }
        #   le remaining du serveur 2
        if(!is.na(CurrentTask2)) {
            Remaining[CurrentTask2] = Remaining[CurrentTask2] - dt
            if(Remaining[CurrentTask2] <= 0) {
                Completion[CurrentTask2] = t
                Remaining[CurrentTask2] = NA
                CurrentTask2=NA
            }
            WorkingTime2 = WorkingTime2 + dt
        }
        
        #   et currentTask (politique d'ordonnancement: FIFO), sur les deux serveurs
        WaitingList=(1:N)[!is.na(Remaining)]
        
        if(length(WaitingList)>0) {
          
            if (is.na(CurrentTask1)) {
              if(!is.na(CurrentTask2)) {
                CurrentTask1 = WaitingList[2]
              } else {
                CurrentTask1 = head(WaitingList,n=1)
              }
              Remaining[CurrentTask1] = Service1[CurrentTask1]
            }
          
            if (is.na(CurrentTask2)) {
              if(!is.na(CurrentTask1)){
                CurrentTask2 = WaitingList[2]
              } else {
                CurrentTask2 = head(WaitingList,n=1)
              }
              Remaining[CurrentTask2] = Service2[CurrentTask2]
            }
        }
    }
    
    # calcul de la consommation d'énergie totale
    totalEnergy = WorkingTime1*energy_working1 + (tail(Completion,1)-WorkingTime1)*energy_idle1 + WorkingTime2*energy_working2 + (tail(Completion,1)-WorkingTime2)*energy_idle2
    totalEnergy = totalEnergy/t
    
    return(data.frame(
        Arrival_rate=arrival_rate,
        Processing_rate1=processing_rate1,
        Processing_rate2=processing_rate2,
        Arrival = Arrival,
        Completion = Completion,
        Energy = totalEnergy,
        Server = number
        ))
        
}

Question 1 : Comparaison de la performance de S1 et de S2

On commence par s’intéresser aux serveurs S1 et S2 :

### Simulation de s1, s2

s1 = data.frame()
for (r in  seq(from=.1, to=.9, by=.1)) {
    s1 = rbind(s1,simul(N=1000, arrival_rate=r, processing_rate=1, debug=FALSE, deterministic=FALSE, energy_working=1, energy_idle=0.5,'S1'))
}

s2 = data.frame()
for (r in  seq(from=.1, to=.9, by=.1)) {
    s2 = rbind(s2,simul(N=1000, arrival_rate=r, processing_rate=2, debug=FALSE, deterministic=FALSE, energy_working = 4, energy_idle = 0.25,'S2'))
}

### Espérance du temps de réponse de s1 et s2

responses1 = s1 %>% group_by(Arrival_rate, Server) %>%
    summarize(Response = mean(Completion - Arrival), 
              Response_se = sd(Completion - Arrival)/sqrt(n()))

responseGlobal = responses1

responses2 = s2 %>% group_by(Arrival_rate, Server) %>%
    summarize(Response = mean(Completion - Arrival), 
              Response_se = sd(Completion - Arrival)/sqrt(n()))

responseGlobal = rbind(responseGlobal,responses2)

responsePlot = ggplot(responseGlobal, aes(x=Arrival_rate, y=Response, group=Server)) + geom_line() + ylim(0,7) + geom_line(aes(colour=Server,group=Server)) + geom_errorbar(aes(ymin = Response - 2*Response_se, ymax = Response + 2*Response_se)) + labs(x = "Taux d'inter-arrivées", y="Temps de réponse", title ="Temps de réponse en fonction du taux d'inter-arrivées")

responsePlot

On voit sur ce graphique l’espérance du temps de réponse des deux serveurs. Celle du S2 est bien plus basse étant donné que S2 a un meilleur temps de service que S1. Il traite donc les tâches bien plus rapidement et a donc un meilleur temps de réponse. Et cette différence s’accentue lorsque le taux d’inter-arrivées croît.

### Consommation énergie de s1 et s2

energys1 = s1 %>% group_by(Arrival_rate, Server) %>%
    summarize(EnergyCons = mean(Energy))

energyGlobal = energys1

energys2 = s2 %>% group_by(Arrival_rate, Server) %>%
    summarize(EnergyCons = mean(Energy))

energyGlobal = rbind(energyGlobal,energys2)

energyPlot = ggplot(energyGlobal, aes(x=Arrival_rate, y=EnergyCons, group=Server)) + geom_line() + geom_line(aes(colour=Server,group=Server)) + labs(x = "Taux d'inter-arrivées", y="Consommation énergétique", title ="Consommation énergétique en fonction du taux d'inter-arrivées")
energyPlot

Sur ce graphique on voit la consommation énergétique des serveurs S1 et S2. Pour des taux d’inter-arrivées très bas, le serveur S2 est plus intéressant car il consomme moins lorsqu’il est en veille, et que dans une telle situation les serveurs passent la plupart de leur temps en veille.

Néanmoins lorsque les taux d’inter-arrivées dépassent 0,15 le serveur S1 devient plus intéressant, car il y a moins de veille et plus de traitements de tâches. Et le serveur S1 consomme 4 fois moins que le S2 lorsqu’il traite une tâche.

Malheureusement cette si faible consommation ne fait pas de S1 un serveur parfait, car en contreparti son temps de réponse devient exponentiellement plus élevé lorsque les taux d’inter-arrivées sont supérieurs à 0,20.

On est alors en droit de se demander s’il pourrait exister une solution hybride pour profiter des avantages de ces deux serveurs. Et c’est ce qu’on va voir dans la prochaine partie.

Question 2 : Étude du serveur S3

Dans cette partie on va utiliser notre deuxième simulateur pour simuler ce qui se passerait si nos deux serveurs S1 et S2 travaillaient en parallèle pour former le serveur S3.

### Simulation de s3

s3 = data.frame()
for (r in  seq(from=.1, to=.9, by=.1)) {
    s3 = rbind(s3,simul2(N=1000, arrival_rate=r, processing_rate1=1, processing_rate2=2, debug=FALSE, deterministic=FALSE, energy_working1=1, energy_idle1=0.5, energy_working2=4, energy_idle2=0.25,'S3'))
}

### Espérance du temps de réponse de s3

responses3 = s3 %>% group_by(Arrival_rate, Server) %>%
    summarize(Response = mean(Completion - Arrival), 
              Response_se = sd(Completion - Arrival)/sqrt(n()))

responseGlobal = rbind(responseGlobal, responses3)

responsePlot = ggplot(responseGlobal, aes(x=Arrival_rate, y=Response, group=Server)) + geom_line() + ylim(0,7) + geom_line(aes(colour=Server,group=Server)) + geom_errorbar(aes(ymin = Response - 2*Response_se, ymax = Response + 2*Response_se)) + labs(x = "Taux d'inter-arrivées", y="Temps de réponse", title ="Temps de réponse en fonction du taux d'inter-arrivées")

responsePlot

Pour des taux d’inter-arrivées faibles, le serveur S3 a un taux de réponse proche de S1, puisque dans ce cas le serveur S1 peut presque gérer toutes les tâches à lui tout seul et le serveur S2 est donc utilisé rarement.

Lorsque les taux d’inter-arrivées augmentent, le temps de réponse du serveur S3 se rapproche de celui de S2, puisque le serveur S1 commence à ne plus pouvoir tout gérer et que le serveur S2 gère donc de plus en plus de tâche. Et on constate que le temps de réponse de S3 est même meilleur que celui de S2 pour un taux d’inter-arrivées de 0,9. Ce qui est normal puisque c’est une situation dans laquelle il y a beaucoup de tâches dans la file d’attente. Cette attente est donc réduite par la contribution du serveur S1, diminuant alors le temps de réponse.

### Consommation énergie de s3

energys3 = s3 %>% group_by(Arrival_rate, Server) %>%
    summarize(EnergyCons = mean(Energy))

energyGlobal = rbind(energyGlobal, energys3)

energyPlot = ggplot(energyGlobal, aes(x=Arrival_rate, y=EnergyCons, group=Server)) + geom_line() + geom_line(aes(colour=Server,group=Server)) + labs(x = "Taux d'inter-arrivées", y="Consommation énergétique", title ="Consommation énergétique en fonction du taux d'inter-arrivées")
energyPlot

Le serveur S3 est une meilleure alternative que S2 lorsque le taux d’inter-arrivées est supérieur à 0,60 puisqu’il consomme moins d’énergie et a un temps de réponse équivalent voire meilleur. Néanmoins il consomme beaucoup d’énergie lorsqu’il y a des taux d’inter-arrivées faibles, et son temps de réponse n’est que légèrement meilleur que S1 dans ces situations.

Le serveur S3 serait donc bien adapté à des situations où il y a un fort taux d’inter-arrivées. Supérieur à 0,60 par exemple.

Question Subsidiaire : Influence de l’hypothèse Markovienne

Dans cette partie on va refaire les simulations précédentes mais avec des temps de service déterministes de durée 1/λ.

### Simulation de s1, s2 et s3 avec des temps de services déterministes

s1d = data.frame()
for (r in  seq(from=.1, to=.9, by=.1)) {
    s1d = rbind(s1d,simul(N=1000, arrival_rate=r, processing_rate=1, debug=FALSE, deterministic=TRUE, energy_working=1, energy_idle=0.5,'S1'))
}

s2d = data.frame()
for (r in  seq(from=.1, to=.9, by=.1)) {
    s2d = rbind(s2d,simul(N=1000, arrival_rate=r, processing_rate=2, debug=FALSE, deterministic=TRUE, energy_working = 4, energy_idle = 0.25,'S2'))
}

s3d = data.frame()
for (r in  seq(from=.1, to=.9, by=.1)) {
    s3d = rbind(s3d,simul2(N=1000, arrival_rate=r, processing_rate1=1, processing_rate2=2, debug=FALSE, deterministic=TRUE, energy_working1=1, energy_idle1=0.5, energy_working2=4, energy_idle2=0.25,'S3'))
}

## Espérance du temps de réponse de s1, s2 et s3

responses1d = s1d %>% group_by(Arrival_rate, Server) %>%
    summarize(Response = mean(Completion - Arrival), 
              Response_se = sd(Completion - Arrival)/sqrt(n()))

responseGlobald = responses1d

responses2d = s2d %>% group_by(Arrival_rate, Server) %>%
    summarize(Response = mean(Completion - Arrival), 
              Response_se = sd(Completion - Arrival)/sqrt(n()))

responseGlobald = rbind(responseGlobald,responses2d)

responses3d = s3d %>% group_by(Arrival_rate, Server) %>%
    summarize(Response = mean(Completion - Arrival), 
              Response_se = sd(Completion - Arrival)/sqrt(n()))

responseGlobald = rbind(responseGlobald, responses3d)

responsePlot = ggplot(responseGlobald, aes(x=Arrival_rate, y=Response, group=Server)) + geom_line() + geom_line(aes(colour=Server,group=Server)) + geom_errorbar(aes(ymin = Response - 2*Response_se, ymax = Response + 2*Response_se)) + labs(x = "Taux d'inter-arrivées", y="Temps de réponse", title ="Temps de réponse en fonction du taux d'inter-arrivées")

responsePlot

## Consommation énergie de s1, s2 et s3

energys1d = s1d %>% group_by(Arrival_rate, Server) %>%
    summarize(EnergyCons = mean(Energy))

energyGlobald = energys1d

energys2d = s2d %>% group_by(Arrival_rate, Server) %>%
    summarize(EnergyCons = mean(Energy))

energyGlobald = rbind(energyGlobald,energys2d)

energys3d = s3d %>% group_by(Arrival_rate, Server) %>%
    summarize(EnergyCons = mean(Energy))

energyGlobald = rbind(energyGlobald, energys3d)

energyPlot = ggplot(energyGlobald, aes(x=Arrival_rate, y=EnergyCons, group=Server)) + geom_line() + geom_line(aes(colour=Server,group=Server)) + labs(x = "Taux d'inter-arrivées", y="Consommation énergétique", title ="Consommation énergétique en fonction du taux d'inter-arrivées")
energyPlot

On constate que les graphiques obtenus sont très similaires à ceux obtenus avec des temps de service exponentiels.

On peut l’expliquer par le fait que l’espérance d’une loi exponentielle de paramètre λ est 1/λ. Ce qui veut dire que lorsque l’on simule nos serveurs avec des temps de service exponentiels un grand nombre de fois, la moyenne des temps de service tend vers 1/λ. Et on retrouve donc des temps de réponse et une consommation énergétique similaire.