Неправильное использование RedisTemplate приводит к увеличению времени обслуживания.

Java

Если ваш Redis развернут в режиме кластера, используйте jedis в качестве пакета драйвера, а проект использует пакет, предоставленный Spring-data.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 все равно будет использовать адрес интрасети, полученный самостоятельно, и запуск будет неудачным.

Я долго работал и бросил. . .