optimization - Pipelining vs Batching en Stackexchange.Redis
(1)
Detrás de escena, SE.Redis hace bastante trabajo para tratar de evitar la fragmentación de paquetes, por lo que no es sorprendente que sea bastante similar en su caso. La principal diferencia entre el procesamiento por lotes y la tubería plana son:
- un lote nunca se intercalará con las operaciones de la competencia en el mismo multiplexor (aunque se puede intercalar en el servidor; para evitar que necesite usar una transacción
multi
/exec
o un script Lua) - un lote siempre evitará la posibilidad de paquetes de tamaño insuficiente, ya que conoce de antemano todos los datos
- pero al mismo tiempo, se debe completar todo el lote antes de que se pueda enviar algo, por lo que esto requiere más almacenamiento en búfer en la memoria y puede introducir latencia artificialmente
En la mayoría de los casos, lo hará mejor evitando los procesos por lotes, ya que SE.Redis logra la mayor parte de lo que hace automáticamente cuando simplemente agrega trabajo.
Como nota final; Si desea evitar los gastos generales locales, un enfoque final podría ser:
redisDB.SetAdd(string.Format(keyFormat, row.Field<int>("Id")),
row.Field<int>("Value"), flags: CommandFlags.FireAndForget);
Esto envía todo por el cable, ni espera respuestas ni asigna Task
incompletas para representar valores futuros. Es posible que desee hacer algo como un Ping
al final sin disparar y olvidar, para comprobar que el servidor todavía está hablando con usted. Tenga en cuenta que usar Fire-and-forget significa que no notará ningún error de servidor que se informe.
Estoy tratando de insertar un gran número de elementos (-ish) en el menor tiempo posible y probé estas dos alternativas:
1) Canalización:
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) Dosificación:
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);
No estoy notando ninguna diferencia de tiempo significativa (en realidad esperaba que el método de procesamiento por lotes fuera más rápido): para aproximadamente 250 K de inserciones obtengo aproximadamente 7 segundos para la canalización, aproximadamente 8 segundos para el procesamiento por lotes.
Leyendo de la documentación sobre la canalización,
"El uso de la canalización nos permite obtener ambas solicitudes en la red inmediatamente, eliminando la mayor parte de la latencia. Además, también ayuda a reducir la fragmentación de paquetes: 20 solicitudes enviadas individualmente (en espera de cada respuesta) requerirán al menos 20 paquetes, pero 20 solicitudes enviadas en una tubería podría caber en muchos menos paquetes (tal vez incluso uno solo) ".
Para mí, esto se parece mucho a un comportamiento por lotes. Me pregunto si entre bastidores hay una gran diferencia entre los dos, porque en una simple comprobación con procmon
veo casi la misma cantidad de TCP Send
en ambas versiones.