прогулочная доска с полостью отходов
Это река Цяньтан в семь часов утра.Идеальный сплав современного города и природных пейзажей сформировал этот прекрасный пейзаж. Берег реки — это взлетно-посадочная полоса, и по утрам там бегает много людей. Я также не тренировался в течение долгого времени, поэтому я немного побегал и почувствовал, что все мое тело чувствует себя намного более комфортно после упражнений.
Мы по-прежнему должны уделять больше внимания упражнениям в обычное время, в конце концов, тело — это столица революции!
Сегодняшняя статья посвящена оптимизации производительности. Некоторое время назад я оптимизировал программу и почувствовал, что выигрыш был довольно большим, поэтому я обобщил некоторые использованные идеи по оптимизации, в основном на уровне кода, и надеялся пообщаться и обсудить с вами.
До оптимизации
У нас есть временная задача, которая извлекает пакет данных из базы данных в цикле (называемый ресурсами в бизнесе) для обработки и извлекает 1000 элементов за раз. Поток обработки длительный, и необходимо запрашивать различную связанную информацию об этом пакете ресурсов, а также запрашивать пакет пользователей в соответствии с организацией, вычислять, какому пользователю необходимо распределить каждый ресурс в соответствии с определенным алгоритмом, и, наконец, выполнить раздачу, а затем поместить результаты раздачи в базу данных и отправить уведомления DingTalk.
Выполнение задания низкое. Для распределения всего 1000 ресурсов требуется более десяти минут. Текущий бизнес обычно распределяет около 2w ресурсов в день, и иногда для завершения распределения требуется несколько часов, что очень неразумно.
Позиционирование отнимающих время ссылок
Поэтому мы намерены оптимизировать эту задачу. Первое, что нужно сделать, это прояснить логику и архитектуру кода, а второй шаг — найти в программе трудоемкие ссылки, чтобы можно было провести целевую оптимизацию.
Использование АртасаtraceКоманда может запрашивать внутренний путь вызова метода и выводить время, затраченное на каждый узел пути метода. Идеально подходит для трудоемких частей поиска задач.
❝Старайтесь не выполнять команду трассировки на машинах в производственной среде, особенно для предприятий с большим количеством вызовов.
❞
# 使用trace
trace demo.MathGame run
# 过滤掉jdk的函数
trace -j demo.MathGame run
# 根据调用耗时过滤
trace demo.MathGame run '#cost > 10'
После использования команды трассировки очевидно, что время некоторых ссылок не является нормальным.Например, каждый ресурс будет получать соответствующий список пользователей некоторых организаций, около нескольких тысяч пользователей, а затем проходить запрос и собирать информацию каждого пользователя. Этот набор занимает около 3 секунд.
оптимизация
В процессе анализа кода обнаруживается, что есть и другие неразумные конструкции, и эти неразумные места в определенной степени были оптимизированы.
Используйте кеш, чтобы избежать дублирования запросов
Мы обнаружили, что сбор информации о пользователях занимает много времени, потому что нам приходилось запрашивать другие службы, а затем нам нужно было обращаться к базе данных, чтобы получить данные.
Но каждый ресурс должен делать одно и то же: брать информацию обо всех пользователях определенной организации, что приводит к множеству повторных запросов.
Понятно, что делать запрос нужно только в первый раз, после того, как запрос получен, он помещается в кеш, а последующие ресурсы нужно только извлекать из кеша.
Здесь мы используем кеш Redis, а не кеш памяти. Поскольку мы изменим информацию о пользователе после того, как распространение будет завершено, а затем планируем распространить ее, несколько компьютеров совместно используют информацию о пользователе, поэтому кеш памяти отсутствует.
последовательный к параллельному
В процессе анализа кода обнаруживается, что многие выполняются последовательно через циклы. Например, при запросе подробной информации пользователя и ее сборке, а также при распределении ресурсов и выполнении некоторых расчетов.
Несмотря на то, что в некоторых аспектах программы используется многопоточность, все еще есть некоторые трудоемкие части, которые сериализуются, что замедляет работу всей программы. Наша машина имеет 4 ядра, поэтому мы можем повторно использовать преимущества многоядерности и использовать многопоточность для оптимизации производительности.
Существует два основных сценария, в которых последовательный режим можно изменить на параллельный:
цикл
Для коллекции мы обычно подсознательно используем цикл for, чтобы перебрать ее и сделать что-то вроде:
List<Result> list = new LinkedList<>();
for(Resource resource : resources) {
User user = computeTarget(resource, users);
Result result = distribute(resource, user);
list.add(result);
}
Если операции в теле цикла занимают много времени, такой последовательный цикл выполняется относительно медленно. В этом случае вы можете просто использовать параллельный поток Java 8 (основной уровень — это инфраструктура Fork/Join) для достижения эффекта параллельного выполнения. Однако, если он изменен на параллельный, вам нужно обратить внимание на вопрос безопасности потоков.Например, в приведенном выше коде мы добавим результат в список, а в исходном сериале используем простой ArrayList. Но наши обычно используемые ArrayList и LinkedList не являются потокобезопасными. Итак, здесь необходимо заменить высокопроизводительный потокобезопасный список:
List<Result> list = new CopyOnWriteArrayList<>();
resources.parallelStream().forEach(resource -> {
User user = computeTarget(resource, users);
Result result = distribute(resource, user);
list.add(result);
})
Существует проблема с использованием параллельного потока Java 8, то есть внутри программы используется один и тот же пул потоков Fork/Join, и пользователям сложно настроить параметры. Таким образом, мы можем использовать пользовательский пул потоков для достижения требований последовательного к параллельному:
List<Result> list = new CopyOnWriteArrayList<>();
List<Callable<Void>> callables = resources.stream()
.map(s -> (Callable<Void>) () -> {
User user = computeTarget(resource, users);
Result result = distribute(resource, user);
list.add(result);
return null;
}).collect(Collectors.toList());
executorService.invokeAll(callables, 20, TimeUnit.SECONDS);
Отнимающие много времени операции без контекста
Другим типичным последовательным подходом является вызов нескольких API в вашем коде, но они могут не обязательно находиться в контексте друг друга. Например, нам может потребоваться вызвать несколько сервисов или запросить базу данных, чтобы окончательно собрать их в одно целое, но атрибуты, которые должны быть собраны в каждой операции, не зависят друг от друга.В это время мы также можем перейти на параллельный.
Например, код перед преобразованием может выглядеть так:
// 串行方式:
OneDTO one = oneService.get();
TwoDTO two = twoService.get();
ThreeDTO three = threeService.get();
nextHandle(new Result(one, two, three));
Мы используем артефакт CompletableFuture, поставляемый с JDK, для выполнения простого преобразования:
// 并行方式:
Result result = new Result();
CompletableFuture oneFuture = CompletableFuture.runAsync(
() -> result.setOne(oneService.get()));
CompletableFuture twoFuture = CompletableFuture.runAsync(
() -> result.setTwo(twoService.get()));
CompletableFuture threeFuture = CompletableFuture.runAsync(
() -> result.setThree(threeService.get()));
CompletableFuture.allOf(oneFuture, twoFuture, threeFuture)
.thenRun( () -> nextHandle(result))
Синхронный к асинхронному
Переход от синхронного к асинхронному иногда может привести к значительному повышению производительности. Независимо от того, сколько времени занимает операция в синхронном режиме, как только я изменяю ее на асинхронную, она бесконечно близка к 0 для текущей программы.
При каких обстоятельствах синхронный можно изменить на асинхронный? Это фактически определяется бизнес-сценарием. В моем сценарии некоторые пост-операции после завершения распределения ресурсов могут быть напрямую изменены на асинхронные, такие как: уведомление пользователя, запись результатов распределения в базу данных и т. д.
Также очень просто перейти от синхронного к асинхронному, просто добавьте его в пул потоков, чтобы сделать это, и все готово.
executorService.submit(() -> {
someAction();
});
От одной машины к распределенной
Последовательность к параллельной была введена ранее. Например, если у нас есть 1000 ресурсов, если один из них выполняется в течение 1 секунды, является ли серийный номер 1000 секунд? Если вы используете 10 потоков для распараллеливания, вы запрограммируете 100 секунд.
Количество потоков ограничено машиной и не может быть увеличено в значительной степени. Но машины могут, и несколько машин, и количество потоков на каждой машине можно умножить.
Наш сервис предполагает, что есть 10 машин, а затем каждая машина использует 10 потоков для распараллеливания, а 1000 ресурсов распределяются по 10 машинам для обработки, что занимает всего 10 секунд. Если мы масштабируемся до 100 машин, это займет всего 1 секунду.
Давайте посмотрим на режим работы под одной машиной: мы берем 1000 ресурсов из библиотеки, а потом сами обрабатываем, остальные машины относительно простаивают.
На самом деле изменить одну машину на распределенную систему очень просто, нам нужно только изменить ее на входе. После того, как ресурсы выловлены, они отправляются через сообщения, а затем другие машины получают сообщения и начинают обрабатывать ресурсы.
❝Или отправитель не ищет ресурсы, а напрямую вырезает начало и смещение каждого сообщения, отправляет его через сообщение и позволяет получателю ловить ресурсы.
❞
В моей предыдущей статье «Процедуры оптимизации производительности, о которых следует упомянуть» я представил дополнительные процедуры оптимизации производительности, в том числе некоторые идеи оптимизации производительности от внешнего интерфейса до серверной части, базы данных и уровня архитектуры. может узнать.
Об авторе
Я Ясин, публичный аккаунт WeChat:"запрограммировал программу"
Персональный сайт: https://yasinshaw.com
Подписывайтесь на мой официальный аккаунт и развивайтесь вместе со мной~
Существуют также учебные ресурсы, группы технического обмена и рекламные акции на крупных заводах.
В этой статье используетсяmdniceнабор текста