Problem Statement

We consider a system composed of \(K\) servers working in parallel, each with its own queue. In the following, “queues” and “servers” are synonyms. Queues have an infinite buffer and operate under the First-In First-Out (FIFO) scheduling discipline. Server \(k\) operates with speed \(\mu_k\). Jobs join the network via an exogenous process with rate \(\lambda\) and are dispatched to queue \(k\) according to some dispatching algorithm; see below. After processing, jobs leave the system and never return.

Job sizes are independent and classified as either long, i.e., of size \(x_M\), or short, i.e., of size \(x_m\), with \(0< x_m < x_M <\infty\). Let \(X\) denote the random variable of the job sizes. We also assume that job sizes have a “heavy-tailed” distribution in the sense that

\[ \mathbb{P}(\mbox{Long job}) =\mathbb{P}(X=x_M)=\alpha\, x_M^{-\beta},\quad \alpha>0, \, 0\le \beta\le 1 \]

with \(x_M\) “large”. Here, the tail of \(X\) decays polynomially and we will be particularly interested in the case where \(\beta\le1\) as this implies high variance when \(x_M\) grows; say \(x_m=1\) and \(x_M=10^3\).

We are interested in evaluating the performance induced by the following dispatching algorithms:

The performance metric of interest is the waiting time, i.e., the average time that jobs spend waiting before receiving service.

The objective is to understand which one works better and, if possible, to extract some quantitative insight. Towards this purpose, we rely on simulation and initially assume that servers operate with speed one, i.e., \(\mu_k=1\).

Implementation in R

We write a code to simulate the waiting times induced by the job dispatching schemes above. We rely on Lindley’s recursion.

knitr::opts_chunk$set(echo = TRUE)

LB_random <- function(N,K,mu,InterArrival,JobSize) {      

  WaitingTime=rep(0,K);
  WaitingTimeCum=rep(0,K);
  
  for (n in 1:N) #for each job
  {
    # Dispatching decision of RANDOM
    U = sample(x=1:K,1); 
    for (k in 1:K) #for each server
    {
      # Update the waiting time of server k (Lindley's recursion)
      WaitingTime[k] = max(WaitingTime[k] + ifelse(U==k,1,0)*JobSize[n]/mu[k] - InterArrival[n], 0);
    }
    WaitingTimeCum[U] = WaitingTimeCum[U]+WaitingTime[U];
  }

 return(sum(WaitingTimeCum)/N);
}

LB_RR <- function(N,K,mu,InterArrival,JobSize) {      

  WaitingTime=rep(0,K);
  WaitingTimeCum=rep(0,K);

  for (n in 1:N) #for each job
  {
    # Dispatching decision of Round Robin
    U = n%%K; 
    U = U+1;
    for (k in 1:K) #for each server
    {
      # Update the waiting time of server k (Lindley's recursion)
      WaitingTime[k] = max(WaitingTime[k] + ifelse(U==k,1,0)*JobSize[n]/mu[k] - InterArrival[n], 0);
    }
    WaitingTimeCum[U] = WaitingTimeCum[U]+WaitingTime[U];
  }

 return(sum(WaitingTimeCum)/N);
}

LB_LLd <- function(N,K,mu,InterArrival,JobSize,d) {      

  WaitingTime=rep(0,K);
  WaitingTimeCum=rep(0,K);

  for (n in 1:N) #for each job
  {
    # Dispatching decision of Least Loaded - d
    s=sample(x=1:K, d, replace = F);
    k_win=s[which.min(WaitingTime[s])];

    for (k in 1:K) #for each server
    {
      # Update the waiting time of server k (Lindley's recursion)
      WaitingTime[k] = max(WaitingTime[k] + ifelse(k_win==k,1,0)*JobSize[n]/mu[k_win] - InterArrival[n], 0);
    }

    WaitingTimeCum[k_win] = WaitingTimeCum[k_win]+WaitingTime[k_win];

  }

 return(sum(WaitingTimeCum)/N);
}

LB_SITAc <- function(N,K,mu,InterArrival,JobSize,c,xm) {      

  WaitingTime=rep(0,K);
  WaitingTimeCum=rep(0,K);
  
  for (n in 1:N) #for each job
  {
    
    # Dispatching decision of SITA-c
    k_win=sample(x=(c+1):K);
    if(JobSize[n]==xm)
    {
      # job n is short
      k_win=sample(x=1:c);
    }

    for (k in 1:K) #for each server
    {
      # Update the waiting time of server k (Lindley's recursion)
      WaitingTime[k] = max(WaitingTime[k] + ifelse(k_win==k,1,0)*JobSize[n]/mu[k_win] - InterArrival[n], 0);
    }

    WaitingTimeCum[k_win] = WaitingTimeCum[k_win]+WaitingTime[k_win];

  }

 return(sum(WaitingTimeCum)/N)

}

################################################################################
N=2e6; # number of jobs to simulate
K=50;  # number of servers
mu = sample(x=1:1, K, replace = TRUE); # service rates

set.seed(17); # Set seed for reproducibility

beta=0.8;
xM=1e3;
xm=1;
JobSize=sample(c(xm,xM),N,replace=T,prob=c(1-1/xM^beta,1/xM^beta)); # Job sizes

EX=xm*(1-1/xM^beta) + xM^(1-beta); # Expected value of job sizes

B=10;
xdata=(0.5:(B-0.5))/B;
W_RANDOM=rep(0,length(xdata));
W_RR=rep(0,length(xdata));
W_LLd=rep(0,length(xdata));

cnt=1;
for (rho in xdata) 
{
  # For stability
  lambda=K * rho/EX; #"overall arrival rate"<"overall service rate"=(mu[1]+...+ mu[K])/EX
  InterArrival = rexp(n=N, rate = lambda);    # Inter-arrival times

  W_RANDOM[cnt]=LB_random(N,K,mu,InterArrival,JobSize);
  W_RR[cnt]=LB_RR(N,K,mu,InterArrival,JobSize);
  W_LLd[cnt]=LB_LLd(N,K,mu,InterArrival,JobSize,2);
  
  print(paste("Waiting Times with rho =",format(round(rho, 2), nsmall = 2),"and K =",K))
  
  print(paste("Random: ", W_RANDOM[cnt]))
  print(paste("RR: ", W_RR[cnt]))
  print(paste("LL-d: ", W_LLd[cnt]))

  #c=floor(K/2); # making sure that c is an integer
  #print(paste("SITA-c: ", LB_SITAc(N,K,mu,InterArrival,JobSize,c,xm)))

  cnt=cnt+1;
  cat("\n")
}
## [1] "Waiting Times with rho = 0.05 and K = 50"
## [1] "Random:  24.9407391177727"
## [1] "RR:  23.0829875179501"
## [1] "LL-d:  4.72371764317381"
## 
## [1] "Waiting Times with rho = 0.15 and K = 50"
## [1] "Random:  74.8361655570211"
## [1] "RR:  72.682005354119"
## [1] "LL-d:  9.4317051164116"
## 
## [1] "Waiting Times with rho = 0.25 and K = 50"
## [1] "Random:  134.66345694938"
## [1] "RR:  135.451641230803"
## [1] "LL-d:  18.6189321444192"
## 
## [1] "Waiting Times with rho = 0.35 and K = 50"
## [1] "Random:  215.498936756677"
## [1] "RR:  216.550889872638"
## [1] "LL-d:  33.3977975647431"
## 
## [1] "Waiting Times with rho = 0.45 and K = 50"
## [1] "Random:  333.154659102608"
## [1] "RR:  325.490107048932"
## [1] "LL-d:  55.1474898744794"
## 
## [1] "Waiting Times with rho = 0.55 and K = 50"
## [1] "Random:  500.662539471911"
## [1] "RR:  485.258878268817"
## [1] "LL-d:  85.9999186207679"
## 
## [1] "Waiting Times with rho = 0.65 and K = 50"
## [1] "Random:  728.358773357734"
## [1] "RR:  731.22991791053"
## [1] "LL-d:  132.188519279443"
## 
## [1] "Waiting Times with rho = 0.75 and K = 50"
## [1] "Random:  1187.48012625492"
## [1] "RR:  1171.1901540522"
## [1] "LL-d:  206.983161390235"
## 
## [1] "Waiting Times with rho = 0.85 and K = 50"
## [1] "Random:  2089.34184001803"
## [1] "RR:  2040.60518620151"
## [1] "LL-d:  335.984144695851"
## 
## [1] "Waiting Times with rho = 0.95 and K = 50"
## [1] "Random:  4398.69389561897"
## [1] "RR:  4296.29082343331"
## [1] "LL-d:  675.476173668452"
length(xdata)
## [1] 10
xdata
##  [1] 0.05 0.15 0.25 0.35 0.45 0.55 0.65 0.75 0.85 0.95
length(W_RANDOM)
## [1] 10
plot(xdata, W_RANDOM, xlim=c(0,1), ylim = c(0, max(W_RANDOM)*1.1), type="o",
     col="blue", pch="o", lty=1, ylab="",xlab="rho",
     main=paste("Average Waiting Times - K=",K))
points(xdata, W_RR, col="red", pch="*")
lines(xdata, W_RR, col="red",lty=2)
points(xdata, W_LLd, col="dark red", pch="*")
lines(xdata, W_LLd, col="dark red",lty=2)
legend("topleft",legend=c("RANDOM","RR","LL2"), col=c("blue","red","black"),
                                   pch=c("o","*","+"),lty=c(1,2,3), ncol=1)


Therefore, LL-2 provides a much better performance than RR and RANDOM, which provide more or less the same performance.

Homework