Конвейерная обработка против дозирования в Stackexchange.Redis

Я пытаюсь вставить большое (-ish) количество элементов в кратчайшие возможные сроки, и я попробовал эти две альтернативы:

1) Конвейеризация:

List<Task> addTasks = new List<Task>();
for (int i = 0; i < table.Rows.Count; i++)
{
    DataRow row = table.Rows[i];
    Task<bool> addAsync = redisDB.SetAddAsync(string.Format(keyFormat, row.Field<int>("Id")), row.Field<int>("Value"));
    addTasks.Add(addAsync);
}
Task[] tasks = addTasks.ToArray();
Task.WaitAll(tasks);

2) Пакетирование:

List<Task> addTasks = new List<Task>();
IBatch batch = redisDB.CreateBatch();
for (int i = 0; i < table.Rows.Count; i++)
{
    DataRow row = table.Rows[i];
    Task<bool> addAsync = batch.SetAddAsync(string.Format(keyFormat, row.Field<int>("Id")), row.Field<int>("Value"));
    addTasks.Add(addAsync);
}
batch.Execute();
Task[] tasks = addTasks.ToArray();
Task.WaitAll(tasks);

Я не замечаю существенной разницы во времени (на самом деле я ожидал, что пакетный метод будет более быстрым): для примерно 250 тыс. вставок я получаю около 7 сек для конвейерной обработки и около 8 сек для дозирования.

Чтение из документации по конвейерной обработке,

"Использование конвейерной обработки позволяет нам получить оба запроса в сети немедленно, устраняя большую часть задержки. Кроме того, он также помогает уменьшить фрагментацию пакетов: 20 запросов, отправленных индивидуально (ожидание каждого ответа) потребует не менее 20 пакетов, но 20 запросы, отправленные в конвейере, могут входить в гораздо меньшее количество пакетов (возможно, даже один).

Для меня это звучит очень похоже на поведение пакетной обработки. Интересно, есть ли за кулисами какая-то большая разница между ними, потому что при простой проверке с procmon я вижу почти то же количество TCP Send в обеих версиях.

Ответы

Ответ 1

За кулисами SE.Redis выполняет небольшую работу, пытаясь избежать фрагментации пакетов, поэтому неудивительно, что в вашем случае это очень похоже. Основное различие между дозированием и плоской конвейерной обработкой:

  • пакет никогда не будет чередоваться с конкурирующими операциями на одном и том же мультиплексоре (хотя он может чередоваться на сервере, чтобы избежать необходимости использовать транзакцию multi/exec или Lua script)
  • пакет всегда будет избегать случайных пакетов, поскольку он знает обо всех данных раньше времени
  • но в то же время вся партия должна быть завершена до того, как что-либо может быть отправлено, поэтому для этого требуется больше буферизации в памяти и может искусственно вводить задержку.

В большинстве случаев вы будете делать лучше, избегая пакетной обработки, поскольку SE.Redis достигает большей части того, что он делает автоматически, просто добавляя работу.

В качестве окончательного замечания; если вы хотите избежать локальных накладных расходов, один заключительный подход может быть:

redisDB.SetAdd(string.Format(keyFormat, row.Field<int>("Id")),
    row.Field<int>("Value"), flags: CommandFlags.FireAndForget);

Это отправляет все по проводам, не дожидаясь ответов и не выделяя неполные Task для представления будущих значений. Возможно, вы захотите сделать что-то вроде Ping в конце без огня и забыть, чтобы проверить, что сервер все еще разговаривает с вами. Обратите внимание, что использование fire-and-forget означает, что вы не заметите никаких сообщений об ошибках сервера.