Introduction :

Le problème qui nous est donné est le suivant : est-il raisonnable d’acheter systématiquement la machine la plus puissante possbile, sachant qu’elle coûtera certainement plus cher à l’achat et également plus cher en électricité sur le long terme ?

Pour répondre à ce problème il nous est proposé trois serveurs avec des caractéristiques différentes. Nous devons donc analyser les performances de ces 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 λ. Pour cela nous devons observer l’espérance du temps de réponse R des tâches et l’espérance de la consommation énergétique E instantanée du serveur.

Nous avons les trois serveurs suivants :

\(S_1\) : Un serveur un peu ancien, peu puissant, avec un temps de service exponentiel de taux μ1 = 1, une consommation énergétique instantanée E1 = 1 lorsqu’il est en train de traiter une tâche et E0 = 0.5 lorsqu’il est en veille.

\(S_2\) : Un serveur moderne, assez puissant, avec un temps de service exponentiel de taux μ2 = 2, une consommation énergétique instantanée E2 = 4 lorsqu’il est en train de traiter une tâche et E0 = 0.25 lorsqu’il est en veille.

\(S_3\) : Un serveur combinant les deux serveurs précédents. Les deux serveurs travaillent en parallèle et lorsqu’un processeur termine de traiter une tâche, il prend la première en attente. La consommation énergétique est la somme de la consommation énergétique des deux serveurs.

Question 1: Comparaison de la performance des serveurs \(S_1\) et \(S_2\)

simul_serveur = function(N=100, arrival_rate=.2, processing_rate=1, consProd = 1, consVeille = 0.3, mode = 1, debug=FALSE) {
    Arrival = cumsum(rexp(n=N, rate=arrival_rate));  
    
    if(mode == 1){
      Service = rexp(n=N, rate =processing_rate); # rep(N,x=1); rgamma(N, shape=.1,rate=.1)
    }
    else{
      Service = rep(N, x = (1/processing_rate));
    }
    
    Remaining = rep(N, x=NA);
    Completion = rep(N, x=NA);
    t = 0;
    Consommation = 0;
    CurrentTask = NA;
    NextArrival = 1;
    while (TRUE) {
        if(debug) print(t);
        if(debug) print(Arrival);
        if(debug) print(Service);
        if(debug) print(Remaining);
        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)
        
        # Mettre à jour comme il faut:
        #   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)) {
            Consommation = Consommation + consProd*dt;
            Remaining[CurrentTask] = Remaining[CurrentTask] - dt ;
            if(Remaining[CurrentTask] <= 0) {
                Completion[CurrentTask] = t;
                Remaining[CurrentTask] = NA;
            }
            CurrentTask = NA;
        }
        else{
          Consommation = Consommation + consVeille*dt;
        }
        #   et currentTask (politique d'ordonnancement: FIFO)
        WaitingList=(1:N)[!is.na(Remaining)];
        if(length(WaitingList)>0) {
            CurrentTask = head(WaitingList,n=1);
        }
    }
    if(debug) print(Completion)
    return(data.frame(
        Arrival_rate=arrival_rate,
        Arrival = Arrival,
        Completion = Completion,
        Comsumption = Consommation / t
        ))
        
}

# Set la seed pour toujours effectuer les mêmes tirages
set.seed(1)
  
df = data.frame()
for (r in  seq(from=.1, to=.9, by=.1)) {
  df = rbind(df, data.frame(simul_serveur(N = 2000, arrival_rate = r, processing_rate = 1, consProd = 1, consVeille = 0.5, mode = 1, debug = FALSE), Serveur = "S1"));
}

for (r in seq(from=.1, to=.9, by=.1)) {
  df = rbind(df, data.frame(simul_serveur(N = 2000, arrival_rate = r, processing_rate = 2, consProd = 4, consVeille = .25, mode = 1, debug = FALSE), Serveur = "S2")); 
}

library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
df2 = df %>% group_by(Arrival_rate, Serveur) %>%
    summarize(Response = mean(Completion - Arrival), 
              Response_se = sd(Completion - Arrival)/sqrt(n()),
              Comsumption = mean(Comsumption),
              Comsumption_se = sd(Comsumption)/sqrt(n()))

library(ggplot2)

ggplot(df2, aes(x=Arrival_rate, y=Response, group = Serveur, color = Serveur)) + geom_line() + theme_bw() + 
    geom_errorbar(aes(ymin = Response - 2*Response_se,
                      ymax = Response + 2*Response_se))

ggplot(df2, aes(x=Arrival_rate, y=Comsumption, group=Serveur, color=Serveur)) + geom_line() + theme_bw()

ggplot(df2, aes(x=Arrival_rate, y=Comsumption, group=Serveur, color=Serveur)) + geom_line() + theme_bw() + coord_cartesian(xlim = c(.14, .2), ylim = c(.5, .75))

Nous pouvons voir avec le graphique ci-dessus que le croisement entre les deux courbes s’effectue vers un λ à peu près égal à 0.18. Ainsi, pour des valeurs de λ inférieures à 0.18, il est plus intéressant sur le plan énergétique d’utiliser le serveur 2 et pour des valeurs supérieures à 0.18 il devient nettement plus intéressant sur le plan énergétique d’utiliser le serveur 1.

Cela semble plutôt logique car avec un λ assez faible le serveur 2 sera très souvent en veille et donc consommera deux fois moins d’énergie que le serveur 1 en étant en veille. Alors que lorsque λ devient plus élevé et que les tâches arrivent de manière plus rapprochée, le serveur 2, travaillera plus de manière continu. Ainsi, ayant une consommation plus élevée en fonctionnement que le serveur 1, sa consommation énergéntique total, sera donc plus élevée que le serveur 1.

Question 2: Etude du serveur \(S_3\)

simul_serveur3 = function(N=100, arrival_rate=.2, processing_rate1=1, consProd1 = 1, consVeille1 = 0.5, processing_rate2=2, consProd2 = 4, consVeille2 = 0.25, mode = 1, debug=FALSE) {
    Arrival = cumsum(rexp(n = N, rate = arrival_rate));  
    
    if(mode == 1){
      Service1 = rexp(n=N, rate = processing_rate1); # rep(N,x=1); rgamma(N, shape=.1,rate=.1)
      Service2 = rexp(n=N, rate = processing_rate2);
    }
    else{
      Service1 = rep(N, x = (1/processing_rate1));
      Service2 = rep(N, x = (1/processing_rate2));
    }
    
    Remaining1 = rep(N, x=NA);
    Remaining2 = rep(N, x = NA);
    Completion = rep(N, x=NA);
    t = 0;
    Consommation = 0;
    CurrentTask1 = NA;
    CurrentTask2 = NA;
    NextArrival = 1;
    while (TRUE) {
        if(debug) print(t);
        if(debug) print(Arrival);
        if(debug) print(Service);
        if(debug) print(Remaining);
        dtA = NA;
        dtC1 = NA;
        dtC2 = 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)) {
            dtC1 = Remaining1[CurrentTask1]; # temps jusqu'à la prochaine terminaison du serveur 1
        }
        if(!is.na(CurrentTask2)) {
            dtC2 = Remaining2[CurrentTask2]; # temps jusqu'à la prochaine terminaison du serveur 2
        }
        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;
        
        #   les arrivées
        if((NextArrival <= N) & (Arrival[NextArrival] <= t)) { ## je met un <= et pas un == car 3-2.9!=0.1 ...
            Remaining1[NextArrival] = Service1[NextArrival];
            Remaining2[NextArrival] = Service2[NextArrival];
            NextArrival = NextArrival + 1;
        }
        
        #   le remaining 
        if(!is.na(CurrentTask1)) {
            Consommation = Consommation + consProd1 * dt;
            Remaining1[CurrentTask1] = Remaining1[CurrentTask1] - dt ;
            if(Remaining1[CurrentTask1] <= 0) {
                Completion[CurrentTask1] = t;
                Remaining1[CurrentTask1] = NA;
            }
            CurrentTask1 = NA;
        }
        else{
          Consommation = Consommation + consVeille1 * dt;
        }
        
        if(!is.na(CurrentTask2)) {
            Consommation = Consommation + consProd2 * dt;
            Remaining2[CurrentTask2] = Remaining2[CurrentTask2] - dt ;
            if(Remaining2[CurrentTask2] <= 0) {
                Completion[CurrentTask2] = t;
                Remaining2[CurrentTask2] = NA;
            }
            CurrentTask2 = NA;
        }
        else{
          Consommation = Consommation + consVeille2 * dt;
        }
        
        #   et currentTask (politique d'ordonnancement: FIFO)
        WaitingList1=(1:N)[!is.na(Remaining1)];
        if(length(WaitingList1)>0) {
            CurrentTask1 = head(WaitingList1,n=1);
            Remaining2[CurrentTask1] = NA;
        }
        
        WaitingList2 = (1:N)[!is.na(Remaining2)];
        if(length(WaitingList2)>0) {
            CurrentTask2 = head(WaitingList2,n=1);
            Remaining1[CurrentTask2] = NA;
        }
    }
    if(debug) print(Completion)
    return(data.frame(
        Arrival_rate = arrival_rate,
        Arrival = Arrival,
        Completion = Completion,
        Comsumption = Consommation / t
        ))
        
}

# Set la seed pour toujours effectuer les mêmes tirages
set.seed(1)
  
df = data.frame()
for (r in  seq(from=.1, to=.9, by=.1)) {
  df = rbind(df, data.frame(simul_serveur(N = 2000, arrival_rate = r, processing_rate = 1, consProd = 1, consVeille = 0.5, mode = 1, debug = FALSE), Serveur = "S1"));
}

for (r in seq(from=.1, to=.9, by=.1)) {
  df = rbind(df, data.frame(simul_serveur(N = 2000, arrival_rate = r, processing_rate = 2, consProd = 4, consVeille = .25, mode = 1, debug = FALSE), Serveur = "S2")); 
}

for (r in  seq(from=.1, to=.9, by=.1)) {
  df = rbind(df, data.frame(simul_serveur3(N=2000, arrival_rate=r, processing_rate1 = 1, consProd1 = 1, consVeille1 = 0.5, processing_rate2 = 2, consProd2 = 4, consVeille2 = 0.25, mode = 1, debug=FALSE), Serveur = "S3"));
}

library(dplyr)
df2 = df %>% group_by(Arrival_rate, Serveur) %>%
    summarize(Response = mean(Completion - Arrival), 
              Response_se = sd(Completion - Arrival)/sqrt(n()),
              Comsumption = mean(Comsumption))

library(ggplot2)

ggplot(df2, aes(x=Arrival_rate, y=Response, group = Serveur, color = Serveur)) + geom_line() + theme_bw() +
    geom_errorbar(aes(ymin = Response - 2*Response_se,
                      ymax = Response + 2*Response_se))

ggplot(df2, aes(x=Arrival_rate, y=Comsumption, group=Serveur, color=Serveur)) + geom_line() + theme_bw()

ggplot(df2, aes(x=Arrival_rate, y=Comsumption, group=Serveur, color=Serveur)) + geom_line() + theme_bw() + coord_cartesian(xlim = c(.48, .52), ylim = c(1.1, 1.35))

Nous pouvons voir avec le graphique ci-dessus que le croisement entre les deux courbes s’effectue vers un λ à peu près égal à 0.50. Ainsi, pour des valeurs de λ inférieures à 0.50, il est plus intéressant sur le plan énergétique d’utiliser le serveur 2 et pour des valeurs supérieures à 0.50, le serveur 3 devient le plus intéressant

Cela semble plutôt logique car avec un λ inférieur à 0.5, le serveur 2 est suffisamment puissant pour contenir le flux d’arrivée des tâches aussi bien que le serveur 3, et termine de travailler aussi vite que le serveur 3. Du coté du serveur 3, comme les tâches arrivent de manière plus espacées, les deux serveurs en parallèles sont souvent en veille. On a donc deux consommations en veille coté serveur 3 et une seule consommation en veille coté serveur 2, pour un même temps de travail. Ce qui explique la consommation plus élevée pour le serveur 3. En revanche, pour un λ supérieur à 0.5, le serveur 2 n’arrive plus à contenir le flux d’arrivée aussi bien que le serveur 3. Le serveur 2 travail alors plus longtemps en continu que le serveur 3. Il consomme donc plus que le serveur 3.

Question subsidiaire:

# Set la seed pour toujours effectuer les mêmes tirages
set.seed(1)
  
df = data.frame()
for (r in  seq(from=.1, to=.9, by=.1)) {
  df = rbind(df, data.frame(simul_serveur(N = 2000, arrival_rate = r, processing_rate = 1, consProd = 1, consVeille = 0.5, mode = 2, debug = FALSE), Serveur = "S1"));
}

for (r in seq(from=.1, to=.9, by=.1)) {
  df = rbind(df, data.frame(simul_serveur(N = 2000, arrival_rate = r, processing_rate = 2, consProd = 4, consVeille = .25, mode = 2, debug = FALSE), Serveur = "S2")); 
}

for (r in  seq(from=.1, to=.9, by=.1)) {
  df = rbind(df, data.frame(simul_serveur3(N=2000, arrival_rate=r, processing_rate1 = 1, consProd1 = 1, consVeille1 = 0.5, processing_rate2 = 2, consProd2 = 4, consVeille2 = 0.25, mode = 2, debug=FALSE), Serveur = "S3"));
}

library(dplyr)
df2 = df %>% group_by(Arrival_rate, Serveur) %>%
    summarize(Response = mean(Completion - Arrival), 
              Response_se = sd(Completion - Arrival)/sqrt(n()),
              Comsumption = mean(Comsumption),
              Comsumption_se = sd(Comsumption)/sqrt(n()))

library(ggplot2)

ggplot(df2, aes(x=Arrival_rate, y=Response, group = Serveur, color = Serveur)) + geom_line() + theme_bw() + 
    geom_errorbar(aes(ymin = Response - 2*Response_se,
                      ymax = Response + 2*Response_se))

ggplot(df2, aes(x=Arrival_rate, y=Comsumption, group=Serveur, color=Serveur)) + geom_line() + theme_bw()

ggplot(df2, aes(x=Arrival_rate, y=Comsumption, group=Serveur, color=Serveur)) + geom_line() + theme_bw() + coord_cartesian(xlim = c(.14, .2), ylim = c(.5, .75))

ggplot(df2, aes(x=Arrival_rate, y=Comsumption, group=Serveur, color=Serveur)) + geom_line() + theme_bw() + coord_cartesian(xlim = c(.48, .52), ylim = c(1.1, 1.35))

On constate une très nette baisse du temps de réponse des serveurs dans cette configuration. En effet, pour le serveur 1, on passe d’environ 6 pour λ = 0.75 à 2.5 et d’un maximum de 11 à un maximum de 3.7. La baisse est moins remarquable pour les serveur 2 et 3 mais elle est tout de même présente. Cette baisse est assez logique étant donné que le temps de service est constant (1 pour le serveur 1 et 1/2 pour le serveur 2). Ceci a pour effet d’éviter les temps de service trop long et donc la congestion qui en découle. Ici, le maximum de 3.7 pour le serveur 1 est dû seulement aux arrivées successives de plusieurs tâches. Les consommations ne semblent pas avoir subi de changement majeur. Cependant, on peut remarquer que la consommation du serveur 3 ne devient intéressante qu’à partir de λ = 0.51 (contrairement à 0.50 précédemment). Ceci est sûrement dû au fait que le temps de service moyen du serveur 2 a été divisé par 2 alors que celui du serveur 1 est rester le même. Ainsi, l’avance du serveur 3 est retardée par le serveur 1.