Проблемы параллелизма, вызванные HashSet

Java GitHub
Проблемы параллелизма, вызванные HashSet

задний план

Я только утром приехал в компанию, и когда уже собирался отправиться на дневную рыбалку, мне вдруг пришло письмо из центра мониторинга.

Секрет в моем сердце нехорош, потому что система мониторинга никогда не скажет мне, что приложение идеальноbug, на самом деле система довольно убогая.

Когда я открыл электронное письмо, мне сообщили, что очередь пула потоков приложения достигла порогового значения и вызвала тревогу.

Поскольку у этого приложения есть проблема, которая очень сильно влияет на работу пользователя, поэтому немедленно позвольте эксплуатации и обслуживанию сохранить сцену.dumpПоток и память одновременно перезапустили приложение, но, к счастью, после перезапуска оно вернулось в норму. Итак, начал устранять проблему.

анализировать

Прежде всего, давайте разберемся, о чем это приложение.

Проще говоря, изMQДанные извлекаются, а затем помещаются в следующий пул бизнес-потоков для конкретной бизнес-обработки.

Очередь сигналов тревоги — это в точности очередь этого пула потоков.

Код трассировки обнаружил, что пул потоков был построен следующим образом:

ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize,
              0L, TimeUnit.MILLISECONDS,
              new LinkedBlockingQueue<Runnable>());;
             put(poolName,executor);

используется по умолчаниюLinkedBlockingQueueРазмер не указан (это тоже яма), поэтому размер этой очереди по умолчаниюInteger.MAX_VALUE.

Поскольку приложение было перезапущено, анализ можно выполнить только из оставшихся моментальных снимков потока и моментальных снимков памяти.

анализ памяти

использовать сначалаMATПроанализировав память, я получил следующий отчет.

Среди них есть два относительно больших объекта, один из которых является предыдущим пулом потоков для хранения задач.LinkedBlockingQueue, а другой естьHashSet.

Конечно, очередь занимает много памяти, поэтому сначала проверьте ее.HashSetДо скорого.

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

анализ потоков

Давайте посмотрим на анализ потока, вот использованиеfastthread.ioЭтот сайт проводит анализ потоков.

Поскольку задачи в пуле потоков давно не выполнялись с точки зрения производительности, в основном нужно посмотреть, что они делают.

они все вRUNNABLEсостояние, а стек выглядит следующим образом:

Установлено, что речь идет как раз о вышеупомянутомHashSet, см. этот стек запрашиваетkeyон существует. То же самое можно сказать и о строке 312 бизнес-кода.

Название темы здесь тоже ямка, что заставило меня долго искать.

должность

После анализа стеков памяти и потоков некоторые проблемы действительно были угаданы.

На самом деле есть предпосылка, которую я забыл упомянуть здесь:

Это предупреждение凌晨三点Письмо отправлено, но нет напоминания по телефону и т.п., так что все не знают.

Узнал только когда пошел утром на работу и сразуdumpвышеуказанные доказательства.

Все с одним очень важным фактом:Эти бизнес-потоки запрашиваютHashSetРаботает уже 6-7 часов без возврата.

Это также видно из предыдущей кривой мониторинга:

ОС была под высокой нагрузкой, пока мы не увидели перезагрузку будильника утром, прежде чем он отключился.

В то же время обнаружено, что приложение, работающее в рабочей среде,JDK1.7, поэтому я изначально думал, что его нужно вводить при запросе ключаHashMapКруговой связанный списокCPUВысокая нагрузка также вошла в бесконечный цикл.

Код был снова проверен, чтобы проверить эту проблему.

Псевдокод после завершения выглядит следующим образом:

//线程池
private ExecutorService executor;

private Set<String> set = new hashSet();

private void execute(){
	
	while(true){
		//从 MQ 中获取数据
		String key = subMQ();
		executor.excute(new Worker(key)) ;
	}
}

public class Worker extends Thread{
	private String key ;

	public Worker(String key){
		this.key = key;
	}

	@Override
	private void run(){
		if(!set.contains(key)){

			//数据库查询
			if(queryDB(key)){
				set.add(key);
				return;
			}
		}

		//达到某种条件时清空 set
		if(flag){
			set = null ;
		}
	}	
}

Общий процесс выглядит следующим образом:

  • Постоянный поток данных получается из MQ.
  • Бросьте данные в пул бизнес-потоков.
  • Определить, были ли записаны данныеSet.
  • Если нет, запросите базу данных.
  • затем напишитеSetсередина.

Тут явная проблема,То есть Установить как общий ресурс не делает никакой синхронизации.

Одновременно будет работать несколько потоков, потому чтоHashSetНа самом деле, это по существуHashMap, так что это определенно небезопасно для потоков, поэтому возникают две проблемы:

  • Данные в наборе перезаписываются во время параллельной записи, что приводит к неточным данным.
  • Круговой связанный список будет сформирован во время расширения.

Первый вопрос приемлем относительно второго.

Благодаря приведенному выше анализу памяти мы уже знаем, что в этом наборе много данных. В то же время, поскольку размер не указывается при инициализации, это всего лишь значение по умолчанию, поэтому он будет вызывать частое расширение при большом количестве одновременных операций записи, а при условии 1.7 может образовыватьсякруговой связанный список.

К сожалению, в коде также есть операции запроса (contains()), обратите внимание на ситуацию со стеком выше:

обнаружено, что он работает наHashMap465 строк, посмотрим, что там творится в 1.7:

Это уже очевидно. Здесь мы просматриваем связанный список, и это вызвано формированием кругового связанного списка.e.nextникогда не бывает пустым, поэтому цикл никогда не завершается.

Здесь проблема действительно найдена, но остается вопрос, почему очередь задач в пуле потоков будет накапливаться все больше и больше. Мой первый инстинкт заключается в том, что выполнение задачи слишком медленное.

Внимательно изучив код, я обнаружил, что есть только одно место, где он может быть медленным: этозапрос к базе данных.

Выносить этот SQL в продакшн-среду на выполнение действительно не быстро, и при проверке индекса обнаруживаются хиты.

Но я посмотрел на данные в таблице и обнаружил, что это почти так.7000WДанные.同时经过运维得知MySQLэтот серверIOДавление тоже больше.

Итак, причина более очевидна:

Поскольку к базе данных необходимо обращаться каждый раз, когда потребляется часть данных, сама MySQL находится под большим давлением, а количество данных также велико, поэтому этот ответ ввода-вывода медленный, что приводит к более медленной обработке всей задачи.

Но есть еще одна причина, которую нельзя игнорировать: так как все бизнес-потоки вошли в бесконечный цикл в определенный момент времени, шансов завершить задачу нет вообще, а следующие данные все еще поступают непрерывно, поэтому эта очередь будет только быть больше и больше Чем больше вы нагромождаете!

На самом деле это старое приложение, и некоторые люди могут спросить, почему раньше не было проблем.

Это связано с тем, что раньше объем данных был относительно небольшим, и не было параллельного расширения для формирования кругового связанного списка даже для одновременных операций записи. Всплеск объема бизнеса в этот период только что выявил эту скрытую мину. Так что я все еще должен верить словам Мерфи.

Суммировать

На этом все расследование завершено, и наши последующие меры корректировки примерно следующие:

  • HashSetне является потокобезопасным, замените его наConcurrentHashMapзаодно поставилvalueЭтого можно достичь, написав до смертиsetЭффект.
  • Согласно нашему более позднему мониторингу, инициализироватьConcurrentHashMapРазмер должен быть как можно больше, чтобы избежать частого расширения.
  • MySQLМногие данные уже не используются, и проводится холодная термообработка. Минимизируйте количество данных в одной таблице. В то же время подтаблица будет рассмотрена позже.
  • Данные запроса корректируются в кэше запросов для повышения эффективности запросов.
  • Имя пула потоков должно быть осмысленным, иначе оно усложнит вам задачу.
  • Настройте размер очереди пула потоков на определенное значение в соответствии с мониторингом и примените политику отклонения.
  • обновитесь доJDK1.8.
  • Во-вторых, электронное письмо с сигналом тревоги следует рассматривать как уведомление по телефону.

HashMapПроблема бесконечного цикла Интернета бесконечна, но я не ожидал, что столкнусь с ней. В настоящее время это условие встречается довольно редко, например, ниже 1,8.JDKС этим может не столкнуться большинство людей, что еще раз подтверждает закон Мерфи.

В то же время я обновлю статью здесь, чтобы вам было удобно читать и запрашивать.

crossoverjie.top/JCSprout/

Ваши лайки и репост - лучшая поддержка для меня