Nel post dedicato alla latenza, ho illustrato una delle tecniche per mitigare il piu' possibile i problemi legati al timeout. La tecnica si basa sostanzialmente sul riprovare.
L'algoritmo che ho implementato, NRetryPolicy, e' molto primitivo e presenta parecchie imperfezioni, oltre a non risolvere il problema e potenzialmente peggiorarlo. Il lettore attento infatti si sara' accorto che se riproviamo ad intervalli fissi potremmo avere un effetto a valanga generando, involontariamente, un attacco di DOS (Denial Of Service) al servizio stesso. Come?
Immaginiamo di avere un carico costante di chiamate di 100 RPS. Ad un certo punto il 20% di queste vanno in timeout. Queste riproveranno dopo un certo ammontare, che avete definito voi, di tempo. Scattato questo intervallo il servizio dovra' supportare il 20% in piu' di richieste, quindi 120 RPS. La probabilita' che queste vadano ancora in timeout aumenta, e quindi si ripropone il problema al prossimo intervallo. Immaginiamo ora che 40 di queste richieste siano andate in timeout. Al prossimo giro 140 RPS. Avento un limite di tentativi, diciamo 3, non dovremmo superare 160 RPS considerando un errore (timeout) costante del 20%.
Come risolvere il problema? Cambiando il pattern dei tentativi, passando da un sistema costante e statico ad un sistema casuale. Essendo un problema molto comune, anche nelle telecomunicazioni e nelle reti, ci appoggeremo ad un algoritmo ben conosciuto, denominato Binary Exponential Backoff.
L'algoritmo e' semplice, riprova randomicamente sulla base di un intervallo esponenziale (2n - 1). Immaginando che l'unita' di intervallo (delta backoff) sia 1 secondo, al primo fallimento riprova subito, al secondo un tempo random incluso fra 0 e 1 secondo, la seconda volta un tempo random fra 0 e 3 secondi, la terza volta un tempo rando fra 0 e 7 secondi e cosi' via.
L'implementazione e' abbastanza triviale:
public class ExponentialNRetryPolicy : RetryPolicy
{
int numberOfRetries;
TimeSpan minBackoff;
TimeSpan maxBackoff;
TimeSpan deltaBackoff;
private readonly Random Random = new Random();
/// <summary>
/// Policy that retries a specified number of times with a randomized exponential backoff scheme
/// </summary>
/// <param name="numberOfRetries">The number of times to retry. Should be a non-negative number.</param>
/// <param name="deltaBackoff">The multiplier in the exponential backoff scheme</param>
/// <returns></returns>
/// <remarks>For this retry policy, the minimum amount of milliseconds between retries is given by the
/// StandardMinBackoff constant, and the maximum backoff is predefined by the StandardMaxBackoff constant.
/// Otherwise, the backoff is calculated as random(2^currentRetry) * deltaBackoff.</remarks>
public ExponentialNRetryPolicy(int numberOfRetries, TimeSpan deltaBackoff)
: this (numberOfRetries, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(30), deltaBackoff)
{
}
/// <summary>
/// Policy that retries a specified number of times with a randomized exponential backoff scheme
/// </summary>
/// <param name="numberOfRetries">The number of times to retry. Should be a non-negative number</param>
/// <param name="deltaBackoff">The multiplier in the exponential backoff scheme</param>
/// <param name="minBackoff">The minimum backoff interval</param>
/// <param name="maxBackoff">The maximum backoff interval</param>
/// <returns></returns>
/// <remarks>For this retry policy, the minimum amount of milliseconds between retries is given by the
/// minBackoff parameter, and the maximum backoff is predefined by the maxBackoff parameter.
/// Otherwise, the backoff is calculated as random(2^currentRetry) * deltaBackoff.</remarks>
public ExponentialNRetryPolicy(int numberOfRetries, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff)
{
if (minBackoff > maxBackoff)
{
throw new ArgumentException("The minimum backoff must not be larger than the maximum backoff period.");
}
if (minBackoff < TimeSpan.Zero)
{
throw new ArgumentException("The minimum backoff period must not be negative.");
}
this.numberOfRetries = numberOfRetries;
this.minBackoff = minBackoff;
this.maxBackoff = maxBackoff;
this.deltaBackoff = deltaBackoff;
}
public override void Retry(Action action)
{
int totalNumberOfRetries = numberOfRetries;
int retryCount = numberOfRetries;
TimeSpan backoff;
do
{
try
{
action();
break;
}
catch (RetryException e)
{
if (retryCount == 0)
{
throw e.InnerException;
}
backoff = CalculateCurrentBackoff(minBackoff, maxBackoff, deltaBackoff, totalNumberOfRetries - retryCount);
if (backoff > TimeSpan.Zero)
{
Thread.Sleep(backoff);
}
}
}
while (retryCount-- > 0);
}
private TimeSpan CalculateCurrentBackoff(TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff, int curRetry)
{
long backoff;
if (curRetry > 30)
{
backoff = maxBackoff.Ticks;
}
else
{
try
{
checked
{
// only randomize the multiplier here
// it would be as correct to randomize the whole backoff result
backoff = Random.Next((1 << curRetry) + 1);
backoff *= deltaBackoff.Ticks;
backoff += minBackoff.Ticks;
}
}
catch (OverflowException)
{
backoff = maxBackoff.Ticks;
}
if (backoff > maxBackoff.Ticks)
{
backoff = maxBackoff.Ticks;
}
}
return TimeSpan.FromTicks(backoff);
}
}
E con questo credo che abbiamo concluso l'argomento di come limitare gli effetti indesiderati della latenza.