Тест производительности Redis: время выполнения многопоточной вставки миллиардов данных в Redis (1)

Redis

предисловие

Публичный аккаунт WeChat:Сяолэй

Когда вы упорно трудитесь до определенной степени, удача встретит вас неожиданно.

задний план

Выбор технологии базы данных компании использует Redis, который ежедневно генерирует 800 миллионов фрагментов данных для фильтрации. Следовательно, необходимо измерить производительность хранилища волны Redis. Ниже приводится запись моего собственного процесса тестирования, а также некоторых ям, на которые я наступил.

требования к испытаниям

В тесте используется многопоточность для вставки миллиардов данных в Redis.Ожидается, что в Redis будет вставлено 800 миллионов фрагментов данных с помощью 10 потоков.

Производительность сервера

В тесте используется собственный тест виртуальной машины:

параметр линукс команда стоимость
система cat /etc/redhat-release CentOS Linux release 7.5.1804 (Core)
ОЗУ free -h всего: 3,7 г доступно: 3,3 г
Количество процессоров cat /proc/cpuinfo cpu cores:2
HZ cat /proc/cpuinfo ] grep MHz ] uniq 1991.999

1. Джедаи вставляют тест в одиночку

Вставить данные за 1 нед.

1. Один поток:

@Test
void exec() throws InterruptedException {
    Jedis jedis = new Jedis("192.168.44.101", 6379);
    jedis.flushDB();
    new Thread() {
            @Override
            public void run() {
                String key = "32021420001:90000300009999:10001:1601198414621:";
                long startTime = System.currentTimeMillis();
                for (int j = 1; j <= 10000; j++) {
                   jedis.set(key+j+"",key+j+"");
                }
                long endTime = System.currentTimeMillis();
                System.out.println("exec time : " + currentThread().getName()+":"+(endTime - startTime));
            }
     }.start();
    System.out.println(Thread.currentThread().getName());
    Thread.sleep(40000);
}

Время, необходимое для вставки данных 1 Вт в один поток, составляет 4,7 с.

main
exec time : Thread-2:4784

2, две нити

@Test
void exec() throws InterruptedException {
    Jedis jedis = new Jedis("192.168.44.101", 6379);
    Jedis jedis2 = new Jedis("192.168.44.101", 6379);
    jedis.flushDB();
    new Thread() {
            @Override
            public void run() {
                String key = "32021420001:90000300009999:10001:1601198414621:";
                long startTime = System.currentTimeMillis();
                for (int j = 1; j <= 5000; j++) {
                   jedis.set(key+j+"",key+j+"");
                }
                long endTime = System.currentTimeMillis();
                System.out.println("exec time : " + currentThread().getName()+":"+(endTime - startTime));
            }
     }.start();
    new Thread() {
        @Override
        public void run() {
            String key = "32021420001:90000300009999:10001:1601198414621:";
            long startTime = System.currentTimeMillis();
            for (int j = 5001; j <= 10000; j++) {
                jedis2.set(key+j+"",key+j+"");
            }
            long endTime = System.currentTimeMillis();
            System.out.println("exec time : " + currentThread().getName()+":"+(endTime - startTime));
        }
    }.start();
    System.out.println(Thread.currentThread().getName());
    Thread.sleep(40000);
}

Двум потокам требуется 1,2 с, чтобы вставить в общей сложности 1 Вт фрагментов данных.

main
exec time : Thread-2:1266
exec time : Thread-3:1277

3. Четыре потока вставляют 1W кусочки данных для более чем 800 миллисекунд

main
exec time : Thread-4:877
exec time : Thread-2:878
exec time : Thread-3:879
exec time : Thread-5:884

4. Десять потоков вставляют 1 Вт данных: более 500 миллисекунд.

main
exec time : Thread-2:573
exec time : Thread-4:573
exec time : Thread-3:575
exec time : Thread-8:574
exec time : Thread-11:572
exec time : Thread-5:576
exec time : Thread-7:575
exec time : Thread-10:573
exec time : Thread-6:576
exec time : Thread-9:588

【Анализ заключения】: Вы можете видеть, что временная кривая является нормальной. Чем больше потоков, тем ниже будет конечное потребление времени. Однако Redis утверждает, что имеет пропускную способность 10 Вт.При использовании этого распространенного метода вставки эффект очень плохой! ! Основная причина в том, что когда мы вставляем, будет несколько операций соединения, и для создания соединения потребуется время.В то же время соединение будет иметь пакеты данных, и сеть передачи нескольких пакетов данных не может быть гарантированно последователен, что влияет на производительность нашей вставки больших данных.

2. Jedis использует конвейер для вставки 1w фрагментов данных

2.1 Предыстория появления трубопровода:

Клиент Redis выполняет команду в 4 процессах:Отправить команду -> очередь команд -> выполнение команды -> вернуть результат

Этот процесс называется RTT (время прохождения туда и обратно). Такие команды, как mget и mset, эффективно экономят RTT. Лично я понимаю, что это аналогично подключению mysql для получения данных. Данные io, вызванные несколькими подключениями, снизят производительность . Однако большинство команд не поддерживают пакетные операции и должны использовать RTT раз N. В настоящее время появляется конвейер для решения этой проблемы.

2.2 Производительность трубопровода

1. Принципиальная схема выполнения N команд без использования конвейера:

2. Используйте конвейер для выполнения команды N раз:

2.3 Тест: вставьте 1w фрагментов данных

один поток:

@Test
void aaaa() throws InterruptedException {
    Jedis jedis = new Jedis("192.168.44.101", 6379);
    Pipeline pipelined = jedis.pipelined();
    jedis.flushDB();
    new Thread() {
        @Override
        public void run() {
            String key = "32021420001:90000300009999:10001:1601198414621:";
            long startTime = System.currentTimeMillis();
            IntStream.range(0,10000).forEach(i->pipelined.set(key+i+"",i+""));
            pipelined.syncAndReturnAll();
            long endTime = System.currentTimeMillis();
            System.out.println("exec time : " + currentThread().getName()+":"+(endTime - startTime));
        }
    }.start();
    Thread.sleep(40000);
}

Время подошло к уровню десятков миллисекунд:

exec time : Thread-2:55

Два теста потока:

При вставке 10 000 фрагментов данных время становится равным 70 миллисекундам.

exec time : Thread-3:71
exec time : Thread-2:72

Четыре потока:

Вы можете видеть, что время стало больше:

exec time : Thread-4:100
exec time : Thread-5:101
exec time : Thread-2:103
exec time : Thread-3:103

Предположение: возможно, объем данных невелик, что приводит к тому, что накладные расходы ввода-вывода занимают слишком много времени при многопоточной обработке. Продолжайте тестировать!

2.4 увеличить данные до теста 1000 Вт

/**
 * jedis 单线程测试
**/
@Test
void aaa() throws InterruptedException {
    Jedis jedis = new Jedis("192.168.44.101", 6379);
    jedis.flushDB();
    Pipeline pipeline=jedis.pipelined();
    new Thread() {
        @Override
        public void run() {
            String key = "32021420001:90000300009999:10001:1601198414621:";
            long startTime = System.currentTimeMillis();
            IntStream.range(0,10000000).forEach(i->pipeline.set(key+i+"",i+""));
            pipeline.syncAndReturnAll();
            long endTime = System.currentTimeMillis();
            System.out.println("exec time : " + currentThread().getName()+":"+(endTime - startTime));
        }
    }.start();
    Thread.sleep(500000);
}

После трех тестов делается окончательный вывод: одному потоку требуется около 70 секунд, чтобы вставить 10 миллионов фрагментов данных.

2.5 Многопоточный тест 1000 Вт данных

Два потока: jedis хочет закрыть соединение

@Test
void aaa() throws InterruptedException {
    Jedis jedis = new Jedis("192.168.44.101", 6379);
    Jedis jedis2 = new Jedis("192.168.44.101", 6379);
    jedis.flushDB();
    jedis.select(3);
    Pipeline pipeline=jedis.pipelined();
    Pipeline pipeline2=jedis2.pipelined();
    new Thread() {
        @Override
        public void run() {
            String key = "32021420001:90000300009999:10001:1601198414621:";
            long startTime = System.currentTimeMillis();
            IntStream.range(0,5000000).forEach(
                    i->pipeline.set(key+i+"",i+"")
            );
            pipeline.syncAndReturnAll();
            long endTime = System.currentTimeMillis();
            if(jedis != null){
                jedis.close();
            }
            System.out.println("总 exec time : " + currentThread().getName()+":"+(endTime - startTime));
        }
    }.start();
    new Thread() {
        @Override
        public void run() {
            String key = "32021420001:90000300009999:10001:1601198414621:";
            long startTime = System.currentTimeMillis();
            IntStream.range(5000000,10000000).forEach(
                    i->pipeline2.set(key+i+"",i+"")
            );
            pipeline2.syncAndReturnAll();
            long endTime = System.currentTimeMillis();
            if(jedis2 != null){
                jedis2.close();
            }
            System.out.println("总 exec time : " + currentThread().getName()+":"+(endTime - startTime));
        }
    }.start();
    Thread.sleep(500000);
}
总 exec time : Thread-2:65198
总 exec time : Thread-3:65197

Пять тем:

总 exec time : Thread-2:41193
总 exec time : Thread-6:42956
总 exec time : Thread-4:44909
总 exec time : Thread-5:44900
总 exec time : Thread-3:44944

[Заключение анализа]: многопоточность действительно может в определенной степени сократить время хранения.

2.6 Расширение мышления

Анализ: Между циклом for и циклом IntStream.forEach() нет разрыва в производительности. Однако, если к IntStream добавить операцию параллельного потока parallel(), скорость может быть удвоена в зависимости от количества ядер процессора. Но операции, выполняемые в stream.parallel.forEach(), не являются потокобезопасными. Исключение выхода за пределы массива произошло, когда он только что был выполнен, поэтому оно не рассматривалось.

IntStream.parallel().forEach()