RedisTemplate.delete(Collection<K> keys)
метод, то вам нужно обратить внимание.
ps: Если вы хотите увидеть причину, просто перейдите к последней части объяснения.
задний план
В ходе мониторинга на прошлой неделе было установлено, что в системе внезапно появилось большое количествоlong-service
, по логу трассировки обнаружено, что трудоемкость работы redis в этом запросе достигает 500мс!
После дальнейшего изучения журнала я обнаружил, что было много записей, которые в последнее время выполнялись слишком долго, и что странно, долгое времяdel
операция, при этомset
,get
Это может быть завершено за 0~1 мс.Тут в принципе проблему с сетью можно исключить
А дальше еще страннее:
Когда я связался с администратором базы данных, брат администратора базы данных сказал с определенным лицом: сервер подаст сигнал тревоги, если одна операция превысит 10 мс, и теперь с кластером все в порядке.
Чтобы подтвердить, что горшок не сервер, я вытягиваю SlowLog Redis, и, к сожалению, я этого не ожидаю.del
записывать.Итак, я должен признать, что сервер тоже в порядке.
минимальная среда воспроизведения
Чтобы лучше проиллюстрировать проблему, я локально создал минимальную среду воспроизведения.
кластер Redis
Он должен быть в режиме кластера.Я запустил 3 мастера и 3 слейва локально, и порты с 6380 по 6385.
Подробности следующие:
127.0.0.1:6380> cluster nodes
1e5df367880965ead5b7064a2e4880db21b7aa94 127.0.0.1:6385@16385 slave 3f8598d3ec376d6f732c9921c2da9d48c825b1ad 0 1586069946810 6 connected
a136c7ed8206f0fa6fe73c02bf5f5d864fbc0f29 127.0.0.1:6383@16383 slave f658a0138f70acbd880928930efa19eebba1098a 0 1586069945000 4 connected
f658a0138f70acbd880928930efa19eebba1098a 127.0.0.1:6381@16381 master - 0 1586069946000 2 connected 5461-10922
3872ab46cf6f316bc1d2a12c2d18e59e5d3a7e83 127.0.0.1:6384@16384 slave a3a7431d8bbaa1bc795433d59ab0dd8bc3fa2d2f 0 1586069947000 5 connected
3f8598d3ec376d6f732c9921c2da9d48c825b1ad 127.0.0.1:6380@16380 myself,master - 0 1586069945000 1 connected 0-5460
a3a7431d8bbaa1bc795433d59ab0dd8bc3fa2d2f 127.0.0.1:6382@16382 master - 0 1586069947821 3 connected 10923-16383
Тестовые задания
В основном для операции удаления в redisTemplate я настроил драйвер redis как jedis (независимый от версии).
Для сравнения я написал здесь три разных метода обслуживания:
1 звонок напрямуюRedisTemplate.delete(Collection<K> keys)
метод.
@Override
public void batchCollDel(List<String> list){
redisTemplate.delete(list);
}
2 Используйте обычный метод forEach и вызовите его в циклеRedisTemplate.delete(K key)
метод.
@Override
public void batchNormalDel(List< String> list){
list.stream().forEach(redisTemplate::delete);
}
3 Используя параллельные потоки, вызовитеRedisTemplate.delete(K key)
метод.
@Override
public void batchParallelDel(List<String> list){
list.parallelStream().forEach(redisTemplate::delete);
}
Чтобы наблюдать за тем, сколько времени занимает выполнение трех вышеперечисленных операций, был написан аспект вокруг, чтобы вывести время, необходимое для выполнения удаления всего списка:
@Around(value = "methodPointCut()")
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
String name = proceedingJoinPoint.getSignature().getName();
StopWatch watch = new StopWatch();
watch.start();
Object proceed = null;
try {
proceed = proceedingJoinPoint.proceed();
watch.stop();
log.info("method:{} spend : {}",name,watch.getTime());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return proceed;
}
Что ж, остальная часть кода состоит в том, чтобы обернуть три вышеуказанных метода в контроллер и выставить их, что не включено в статью.
стресс тест
Прежде всего, позвольте мне констатировать, что производительность кластера Redis достаточна, rps может легко подняться до 10 Вт, вы можете использовать его самостоятельно.redis-benchmark
бегать.
Команда AB, сначала нажав обычное удаление цикла Foreach:
ab -c 50 -n 500 http://localhost:8080/redis/batchNormalDel?size=1000
Ниже приведены статистические данные, возвращаемые ab, включая время генерации случайной строки, каждый запрос выполняется в течение 700 мс:
Percentage of the requests served within a certain time (ms)
50% 527
66% 545
75% 559
80% 568
90% 595
95% 613
98% 632
99% 658
100% 663 (longest request)
Взгляните на время, необходимое для выполнения пакетных удалений redis, напечатанных в журнале:
Затем нажмите еще раз и используйте его напрямуюRedisTemplate.delete(Collection<K> keys)
Эффект пакетного удаления:
ab -c 50 -n 500 http://localhost:8080/redis/batchCollDel?size=1000
собирается взорваться. . В начале стресс-теста потребление времени увеличивается линейно, и, наконец, каждое пакетное удаление поддерживается на уровне около 1500 мс.
Percentage of the requests served within a certain time (ms)
50% 1576
66% 1757
75% 1785
80% 1854
90% 2172
95% 2308
98% 2340
99% 2399
100% 2446 (longest request)
Наконец, чтобы увидеть, как производительность параллельных потоков:
ab -c 50 -n 500 http://localhost:8080/redis/batchParallelDel?size=1000
Кажется, параллелизм — не панацея, ха-ха.
Percentage of the requests served within a certain time (ms)
50% 383
66% 490
75% 572
80% 639
90% 797
95% 989
98% 1213
99% 1462
100% 2206 (longest request)
Наконец, одна операция вниз, проверьтеslowlog
(Единица времени здесь тонкая).
127.0.0.1:6380> config get slowlog-log-slower-than
1) "slowlog-log-slower-than"
2) "10000"
127.0.0.1:6380> slowlog get
(empty list or set)
127.0.0.1:6380>
объяснить часть
spring-data:2.2.6.realease
jedis:3.2.0
Давайте посмотрим на код, связанный с RedisTemplate:
@Override
public Long delete(Collection<K> keys) {
if (CollectionUtils.isEmpty(keys)) {
return 0L;
}
byte[][] rawKeys = rawKeys(keys);
return execute(connection -> connection.del(rawKeys), true);
}
Ничего особенного, продолжайте читать:
Поскольку это кластерный режим, соответствующий классJedisClusterKeyCommands
.
@Override
public Long del(byte[]... keys) {
Assert.notNull(keys, "Keys must not be null!");
Assert.noNullElements(keys, "Keys must not contain null elements!");
if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) {
try {
return connection.getCluster().del(keys);
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
return (long) connection.getClusterCommandExecutor()
.executeMultiKeyCommand((JedisMultiKeyClusterCommandCallback<Long>) (client, key) -> client.del(key),
Arrays.asList(keys))
.resultsAsList().size();
}
Считается, что входящие ключи — это тот же слот в узле Redis,ClusterSlotHashUtil.isSameSlotForAllKeys
Он рассчитает значение слота для всех переданных ключей.
На самом деле, поскольку слоты в кластере в целом распределены равномерно, вероятность того, что пакетное удаление попадет в один и тот же слот, очень мала (если только не сгруппировано специально), все, в большинстве случаев, идут нижеexecuteMultiKeyCommand
филиал.
public <S, T> MultiNodeResult<T> executeMultiKeyCommand(MultiKeyClusterCommandCallback<S, T> cmd,
Iterable<byte[]> keys) {
Map<RedisClusterNode, PositionalKeys> nodeKeyMap = new HashMap<>();
int index = 0;
for (byte[] key : keys) {
for (RedisClusterNode node : getClusterTopology().getKeyServingNodes(key)) {
nodeKeyMap.computeIfAbsent(node, val -> PositionalKeys.empty()).append(PositionalKey.of(key, index++));
}
}
Map<NodeExecution, Future<NodeResult<T>>> futures = new LinkedHashMap<>();
for (Entry<RedisClusterNode, PositionalKeys> entry : nodeKeyMap.entrySet()) {
if (entry.getKey().isMaster()) {
for (PositionalKey key : entry.getValue()) {
futures.put(new NodeExecution(entry.getKey(), key),
executor.submit(() -> executeMultiKeyCommandOnSingleNode(cmd, entry.getKey(), key.getBytes())));
}
}
}
return collectResults(futures);
}
Затем он будет сгруппирован в соответствии с узлом redis, где находится каждый ключ.Результаты вычислений следующие:
Затем используйтеFuture
Асинхронно отправляют команды на основном узле, соответствующем соответственно.
После отправки всех команд используйтеcollectResults(futures)
Метод ожидает возврата всех результатов, то есть асинхронных, а затем синхронных.
Этот код сюда ставиться не будет, основная логика судить исполнились ли фьючерсы, если нет, тоThread.sleep(10)
Продолжайте ждать.
Ключевая проблема заключается в том, где задача передается на выполнение
executor.submit(() -> executeMultiKeyCommandOnSingleNode(cmd, entry.getKey(), key.getBytes()))
давайте посмотрим на этоexecutor
Объект:
Он являетсяAsyncTaskExecutor
объект, то егоcorePoolSize
Сколько это стоит?
существуетClusterCommandExecutor
, есть две противоположные парыexecutor
Для инициализации:
1 Метод строительства:
public ClusterCommandExecutor(ClusterTopologyProvider topologyProvider, ClusterNodeResourceProvider resourceProvider,
ExceptionTranslationStrategy exceptionTranslation, @Nullable AsyncTaskExecutor executor) {
this(topologyProvider, resourceProvider, exceptionTranslation);
this.executor = executor;
}
Однако этот конструктор нигде не используется.
2 блока кода
{
if (executor == null) {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.initialize();
this.executor = threadPoolTaskExecutor;
}
}
Честно говоря, такое редко встретишь.
Созданный таким образом ThreadPoolTaskExecutor имеет несколько ключевых атрибутов:
private int corePoolSize = 1;
private int maxPoolSize = Integer.MAX_VALUE;
private int keepAliveSeconds = 60;
private int queueCapacity = Integer.MAX_VALUE;
private boolean allowCoreThreadTimeOut = false;
Количество основных потоков равно 1.Максимальная длина очереди ожидания — Integer.MAX_VALUE.
Максимальное количество потоков — Integer.MAX_VALUE.
在这种配置下,线程池始终只有一个线程在处理任务,只有等待队列满后才会创建第二个worker线程,然而,这个队列太大了。 . .
На самом деле, что еще более раздражает, так это то, что этоexecutor
У нас нет возможности настроить атрибут, класс где он находитсяClusterCommandExecutor
Это также не класс, управляемый контейнером Spring.
Итак, проблема найдена. Из-за инкапсуляции джедаев Spring в кластерном режиме задачи будут отправляться только в один основной поток при использовании пакетного метода удаления.executor
В сценариях с высоким параллелизмом большое количество задач в приложении будет поставлено в очередь и не может быть выполнено.
разное
Связанный код был загружен в GithubGitHub.com/Вернуться к Фрэнку/Горячие…
Содержит скрипты, относящиеся к Redis для создания кластеров, которые можно запускать с помощью docker или непосредственно на хосте.
Если это Mac, не используйте докер для создания кластера Redis. . Сетевой режим хоста docker не поддерживается под mac (поддерживается только linux).Если используется мост, из-за весеннего выполненияcluster nodes
То, что вы получаете, это сетевой адрес в докере, Хотя вы настроили адрес, который выполнил сопоставление портов, Spring все равно будет использовать адрес интрасети, полученный самостоятельно, и запуск будет неудачным.
Я долго работал и бросил. . .