DEV Community

Cover image for StackExchange.Redis: Pipeling
Paulo Walraven
Paulo Walraven

Posted on

StackExchange.Redis: Pipeling

Introdução

Pipelining é um conceito extremamente importante para maximizar a taxa de transferência para o Redis. Quando você precisa executar vários comandos no Redis e os resultados intermediários podem ser temporariamente ignorados, o pipelining pode reduzir drasticamente o número de idas e voltas necessárias para o Redis, o que pode aumentar drasticamente o desempenho, pois muitas operações são centenas de vezes mais rápidas que o Round Trip Tempo (RTT).

  • Round Trip Tempo (RTT).

    É uma medida do tempo que leva para um cliente enviar uma solicitação ao servidor e receber uma resposta de volta. O RTT geralmente é medido em milissegundos (ms) e depende de fatores como a distância entre o cliente e o servidor, a velocidade da conexão de rede e a ocupação do servidor.

Com StackExchange.Redis, há duas maneiras de canalizar comandos, implicitamente com a API Async e explicitamente com a API IBatch.

Pipelining implícito com API Async

Se você usar a versão assíncrona de um comando, o comando será automaticamente canalizado para o Redis. Se você usar await para aguardar o resultado de uma tarefa despachada por um desses comandos assíncronos, ele aguardará até que o comando seja concluído antes de retornar o controle. No entanto, se você agrupar um conjunto de tarefas despachadas pelos métodos assíncronos e aguardar todas de uma vez, o ConnectionMultiplexer encontrará automaticamente uma maneira eficiente de canalizar os comandos para o Redis, para que você possa reduzir significativamente o número de viagens de ida e volta .

Pipelining explícito com IBatch

Você também pode ser muito mais explícito sobre os comandos de pipelining. A API IBatch fornece apenas a interface assíncrona para comandos. Você pode configurar quantos comandos quiser canalizar com esses métodos assíncronos, preservando as tarefas, pois elas fornecerão os resultados. Quando você tiver todos os comandos que deseja enviar por pipeline, poderá chamar Execute para executá-los. Isso canalizará todos os seus comandos em um bloco contíguo para o Redis. Usando esse método, nenhum outro comando será intercalado com seus comandos em lote do cliente. No entanto, se houver outros clientes enviando comandos para o Redis, é possível que seus comandos sejam intercalados com os comandos em lote.

Na prática

O pipelining pode ser crucial para aumentar a taxa de transferência em sua instância do Redis. Veremos como executar a mesma coleção de tarefas em três modos.

  • Em série e sem pipeline
  • Pipeline implícito
  • Pipeline explicitamente com IBatch

Preparando ambiente

Antes de executarmos nossos exemplos, vamos inicializar um Multiplexer, obter uma instância de um IDatabase e criar e iniciar um StopWatch:

using System.Diagnostics;
using StackExchange.Redis;

var options = new ConfigurationOptions
{
    EndPoints = new EndPointCollection { "localhost:6379" }
};

var muxer = ConnectionMultiplexer.Connect(options);
var db = muxer.GetDatabase();

var stopwatch = Stopwatch.StartNew();
Enter fullscreen mode Exit fullscreen mode

Sem Pipeline

Enviar uma cadeia serial de comandos que não estão em pipeline é bastante simples. Por uma questão de consistência com os outros exemplos, usaremos a versão assíncrona do comando Ping. Vamos apenas ligar e aguardar o resultado 1000 vezes. Isso fará com que o multiplexador aguarde a conclusão do comando entre as execuções e o impedirá de canalizar os comandos automaticamente.

for (var i = 0; i < 1000; i++)
    await database.PingAsync();
Console.WriteLine($"1000 un-pipelined commands took: {stopwatch.ElapsedMilliseconds}ms to execute");
Enter fullscreen mode Exit fullscreen mode
// output
1000 un-pipelined commands took: 497ms to execute
Enter fullscreen mode Exit fullscreen mode

Pipeline implícito

Não vamos aguardar cada tarefa conforme ela é despachada. Em vez disso, coletaremos todas as tarefas e as aguardaremos em massa no final. Cada tarefa é responsável por conter os resultados do comando após a conclusão do comando:

var pingTasks = new List<Task<TimeSpan>>();
for (var i = 0; i < 1000; i++)
    pingTasks.Add(database.PingAsync());
await Task.WhenAll(pingTasks);
Console.WriteLine($"1000 automatically pipelined tasks took: {stopwatch.ElapsedMilliseconds}ms to execute, first result: {pingTasks[0].Result}");
Enter fullscreen mode Exit fullscreen mode
// output
1000 automatically pipelined tasks took: 6ms to execute, first result: 00:00:00.0011550
Enter fullscreen mode Exit fullscreen mode

Pipelining explícito com IBatch

Vamos fazer com que os nossos pings utilizem um IBatch. Um IBatch garantirá que o cliente envie todo o lote para o Redis de uma só vez, sem outros comandos intercalados no pipeline. Este é um comportamento ligeiramente diferente do nosso pipelining implícito, pois no caso de pipelining implícito, os comandos podem ser intercalados com quaisquer outros comandos que o cliente estava executando no momento.

Seguiremos um padrão semelhante, neste caso, no entanto, usaremos o método IDatabase.CreateBatch() para criar o lote e usaremos os métodos assíncronos do lote para 'despachar' as tarefas. É importante observar aqui que, ao contrário do nosso caso implícito, as tarefas não serão realmente despachadas até que o método IBatch.Execute() seja chamado. Se você tentar aguardar qualquer uma das tarefas antes disso, poderá travar acidentalmente seu comando. Depois de chamar Execute, você pode aguardar todas as tarefas.

var pingTasks = new List<Task<TimeSpan>>();
var batch = database.CreateBatch();
for (var i = 0; i < 1000; i++)
    pingTasks.Add(batch.PingAsync());
batch.Execute();
await Task.WhenAll(pingTasks);
Console.WriteLine($"1000 batched commands took: {stopwatch.ElapsedMilliseconds}ms to execute, first result: {pingTasks[0].Result}");
Enter fullscreen mode Exit fullscreen mode
// output
1000 batched commands took: 7ms to execute, first result: 00:00:00.0011708
Enter fullscreen mode Exit fullscreen mode

Código final

using StackExchange.Redis;
using System.Diagnostics;

var options = new ConfigurationOptions
{
    EndPoints = new EndPointCollection { "localhost:6379" }
};

var conn = ConnectionMultiplexer.Connect(options);
var database = conn.GetDatabase();

await RedisPipelining.Unpipelined(database);
await RedisPipelining.ImplicitlyPipelined(database);
await RedisPipelining.ExplicitPipeliningWithIBatch(database);

public static class RedisPipelining
{
    public static async Task Unpipelined(IDatabase database)
    {
        var stopwatch = Stopwatch.StartNew();
        for (var i = 0; i < 1000; i++)
            await database.PingAsync();
        Console.WriteLine($"1000 un-pipelined commands took: {stopwatch.ElapsedMilliseconds}ms to execute");
    }

    public static async Task ImplicitlyPipelined(IDatabase database)
    {
        var stopwatch = Stopwatch.StartNew();
        var pingTasks = new List<Task<TimeSpan>>();
        for (var i = 0; i < 1000; i++)
            pingTasks.Add(database.PingAsync());
        await Task.WhenAll(pingTasks);
        Console.WriteLine($"1000 automatically pipelined tasks took: {stopwatch.ElapsedMilliseconds}ms to execute, first result: {pingTasks[0].Result}");
    }

    public static async Task ExplicitPipeliningWithIBatch(IDatabase database)
    {
        var stopwatch = Stopwatch.StartNew();
        var pingTasks = new List<Task<TimeSpan>>();
        var batch = database.CreateBatch();
        for (var i = 0; i < 1000; i++)
            pingTasks.Add(batch.PingAsync());
        batch.Execute();
        await Task.WhenAll(pingTasks);
        Console.WriteLine($"1000 batched commands took: {stopwatch.ElapsedMilliseconds}ms to execute, first result: {pingTasks[0].Result}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusão

Ao utilizar a biblioteca em série e sem pipeline, o desempenho pode ser afetado negativamente, especialmente em cenários em que são necessárias várias operações de leitura ou escrita.

No modo com pipeline implícito, as operações de leitura e escrita são agrupadas em um pipeline automaticamente, melhorando significativamente o desempenho. Isso é particularmente útil para aplicativos que realizam muitas operações de leitura e escrita em sequência.

Já no modo com pipeline explicitamente usando a interface IBatch, é possível agrupar manualmente as operações em um pipeline e executá-las de forma assíncrona, o que pode melhorar ainda mais o desempenho. Este modo é especialmente útil em aplicativos que precisam realizar operações de cache em segundo plano, para evitar atrasos no processamento principal do aplicativo.

O uso da biblioteca StackExchange.Redis é uma ótima opção para gerenciar o cache em aplicativos .NET, e o modo de utilização mais adequado depende do cenário de uso e dos requisitos de desempenho do aplicativo.

Top comments (0)