почему так медленно со 100 000 записей при использовании конвейера в Redis?

Говорят, что pipeline — лучший способ, когда в Redis требуется много set/get, так что это мой тестовый код:

public class TestPipeline {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        JedisShardInfo si = new JedisShardInfo("127.0.0.1", 6379);
        List<JedisShardInfo> list = new ArrayList<JedisShardInfo>();
        list.add(si);
        ShardedJedis jedis = new ShardedJedis(list);
        long startTime = System.currentTimeMillis();
        ShardedJedisPipeline pipeline = jedis.pipelined();
        for (int i = 0; i < 100000; i++) {
            Map<String, String> map = new HashMap<String, String>();
            map.put("id", "" + i);
            map.put("name", "lyj" + i);
            pipeline.hmset("m" + i, map);
        }
        pipeline.sync();
        long endTime = System.currentTimeMillis();
        System.out.println(endTime - startTime);
    }
}

Когда я его запускал, с этой прогой какое-то время нет отклика, но когда я не работаю с pipe, он занимает всего 20073 мс, поэтому я в замешательстве, почему без pipeline еще лучше и какой большой разрыв!

Спасибо за ответ, несколько вопросов, как вы рассчитываете 6 МБ данных? Когда я отправляю 10 КБ данных, конвейер всегда работает быстрее, чем в обычном режиме, но при 100 КБ конвейер не отвечает. Я думаю, что 100-1000 операций — это рекомендуемый выбор, как сказано ниже. Есть ли что-нибудь с JIT, поскольку я этого не понимаю?


person znlyj    schedule 22.05.2013    source источник
comment
Даже 20 секунд кажутся долгим сроком. Я предполагаю, что канал позволяет отправлять асинхронные обновления, а без канала обновления являются синхронными.   -  person Peter Lawrey    schedule 22.05.2013
comment
+1 Вы вдохновили меня на написание этого теста Вставьте миллион в Chronicle, и он напечатает 1,000,000 inserts took 1.208 seconds на моем ноутбуке.   -  person Peter Lawrey    schedule 22.05.2013


Ответы (1)


Перед написанием такого бенчмарка (и особенно бенчмарка с использованием JVM) необходимо учесть несколько моментов:

  • на большинстве (физических) машин Redis может обрабатывать более 100 000 операций в секунду при использовании конвейерной обработки. Ваш тест работает только с 100 000 элементов, поэтому он не длится достаточно долго, чтобы давать значимые результаты. Кроме того, нет времени для запуска последовательных этапов JIT.

  • абсолютное время не очень релевантная метрика. Отображение пропускной способности (т. е. количества операций в секунду) при сохранении работы теста в течение не менее 10 секунд было бы лучшим и более стабильным показателем.

  • ваш внутренний цикл генерирует много мусора. Если вы планируете проводить бенчмаркинг Jedis+Redis, вам нужно поддерживать низкую нагрузку на собственную программу.

  • поскольку вы определили все в основную функцию, ваш цикл не будет скомпилирован JIT (в зависимости от используемой вами JVM). Могут быть только внутренние вызовы методов. Если вы хотите, чтобы JIT был эффективным, обязательно инкапсулируйте свой код в методы, которые могут быть скомпилированы JIT.

  • при желании вы можете добавить фазу прогрева перед выполнением фактического измерения, чтобы избежать накладных расходов на выполнение первых итераций с помощью базового интерпретатора и стоимости самой JIT.

Теперь, что касается конвейеризации Redis, ваш конвейер слишком длинный. 100 000 команд в конвейере означают, что Jedis должны создать буфер размером 6 МБ, прежде чем отправлять что-либо в Redis. Это означает, что буферы сокетов (на стороне клиента и, возможно, на стороне сервера) будут заполнены, и что Redis также придется иметь дело с коммуникационными буферами размером 6 МБ.

Кроме того, ваш тест по-прежнему синхронен (использование конвейера не делает его волшебным образом асинхронным). Другими словами, Jedis не начнет читать ответы, пока последний запрос вашего конвейера не будет отправлен в Redis. Когда конвейер слишком длинный, он может заблокировать работу.

Рассмотрите возможность ограничения размера конвейера до 100–1000 операций. Конечно, это будет генерировать больше циклов, но нагрузка на коммуникационный стек будет снижена до приемлемого уровня. Например, рассмотрим следующую программу:

import redis.clients.jedis.*;
import java.util.*;

public class TestPipeline {

    /**
     * @param args
     */

    int i = 0; 
    Map<String, String> map = new HashMap<String, String>();
    ShardedJedis jedis;  

    // Number of iterations
    // Use 1000 to test with the pipeline, 100 otherwise
    static final int N = 1000;

    public TestPipeline() {
      JedisShardInfo si = new JedisShardInfo("127.0.0.1", 6379);
      List<JedisShardInfo> list = new ArrayList<JedisShardInfo>();
      list.add(si);
      jedis = new ShardedJedis(list);
    } 

    public void push( int n ) {
     ShardedJedisPipeline pipeline = jedis.pipelined();
     for ( int k = 0; k < n; k++) {
      map.put("id", "" + i);
      map.put("name", "lyj" + i);
      pipeline.hmset("m" + i, map);
      ++i;
     }
     pipeline.sync(); 
    }

    public void push2( int n ) {
     for ( int k = 0; k < n; k++) {
      map.put("id", "" + i);
      map.put("name", "lyj" + i);
      jedis.hmset("m" + i, map);
      ++i;
     }
    }

    public static void main(String[] args) {
      TestPipeline obj = new TestPipeline();
      long startTime = System.currentTimeMillis();
      for ( int j=0; j<N; j++ ) {
       // Use push2 instead to test without pipeline
       obj.push(1000); 
       // Uncomment to see the acceleration
       //System.out.println(obj.i);
     }
     long endTime = System.currentTimeMillis();
     double d = 1000.0 * obj.i;
     d /= (double)(endTime - startTime);
     System.out.println("Throughput: "+d);
   }
 }

С помощью этой программы вы можете тестировать с конвейерной обработкой или без нее. Обязательно увеличьте количество итераций (параметр N) при использовании конвейерной обработки, чтобы она выполнялась не менее 10 секунд. Если вы раскомментируете println в цикле, вы поймете, что программа работает медленно в начале и будет работать быстрее, когда JIT начнет оптимизировать вещи (поэтому программа должна работать как минимум несколько секунд, чтобы дать значимый результат).

На моем оборудовании (старый Athlon) я могу получить в 8-9 раз больше пропускной способности при использовании конвейера. Программу можно улучшить, оптимизировав форматирование ключей/значений во внутреннем цикле и добавив фазу прогрева.

person Didier Spezia    schedule 23.05.2013
comment
Не могли бы вы рассказать мне, как оптимизировать формат ключ/значение и добавить фазу прогрева? - person znlyj; 23.05.2013
comment
Форматирование: например, используйте Integer.toString(i) вместо +i - person Didier Spezia; 24.05.2013
comment
Фаза прогрева: просто добавьте несколько итераций при отправке (и не забудьте установить obj.i на ноль сразу после этого) до того, как будет назначен startTime, . - person Didier Spezia; 24.05.2013