Верхняя полка📝
332 subscribers
162 photos
6 videos
3 files
81 links
Путёвые заметки программного инженера, атлета-любителя, отца, домохозяина.

Автор: Владимир @Toparvion Плизга

Домашняя страница: https://toparvion.pro/
加入频道
Друзья, в ближайший понедельник (26 мая) стартует новый сезон Podlodka Java Crew — онлайн конференции, проходящей на протяжении недели в формате 1-2 докладов в день (утром и вечером по московскому времени) 🤿

Тема сезона — performance приложений на JVM.
Среди выступающих не только автор этих строк, но и настоящие эксперты по производительности #Java, например Григорий Кошелев, Алексей Рагозин, Михаил Поливаха 🥷🏼🥷🏼🥷🏼

Всю программу и расписание можно посмотреть здесь: https://podlodka.io/javacrew 🗃

Участие платное, однако для вас у меня есть два бесплатных билетика; приходите за ними в комментарии 🎟
👍7
В это сложно поверить, но сегодня языку программирования #Java, с которым тесно связана почти вся моя профессиональная карьера, исполняется 30 лет 🎂

Из них 14 я пишу на нём коммерческие приложения, а познакомился с ним и того раньше. Хотя не настолько рано, чтобы застать его под именем Oak (дуб) — так он назывался в самом начале. Зато помню версию, которая тогда была в зените — 1.6 👨‍🦳

За эти годы я не раз касался других языков и их экосистем, но ни к одной не почувствовал большего желания погрузиться, чем к JVM. И это при том, что Java не была моим первым рабочим языком: сначала я писал на ассемблере и С, потом на языках для ПЛК, а первую совсем взрослую работу искал на... PHP 🙈

Сегодня в Сети много всякого про этот юбилей, но мне пока больше всех понравилась затея JetBrains — они запилили прикольную страничку с небольшим квизом, а к ней — специальный юбилейный плагин для среды разработки IDEA. Если пройти квиз, получить в его финале специальный код и вписать его в плагин, то IDEA будет запускаться со splash-экраном, на котором будет изображаться Дюк (маскот Java) в соответствующем вам амплуа 😎

У меня это "Prompt Perfectionist". А у вас? 🙂
🎉82
При подготовке доклада на конференцию Saint HighLoad++ (24 июня в Петербурге) я совершил очередной заход на поиск легкого, удобного и красивого инструмента для визуализации графов 🎓

На этом пути впервые узнал об утилите Gephi, созданной как раз для работы с графами. Правда, акцент в ней сделан не на красоту, а на возможности анализа, фильтрации и поиска — тут ей есть что предложить. Также порадовала скорость: например, граф классов JDK (1,5К узлов и 8К связей) открылся за пару секунд и очень легко поддавался манипуляциям вроде масштабирования, выделения отдельных узлов со связями и т.п. Любопытно, что сама программа при этом тоже написана на #Java, а всю эту графику рисует на OpenGL ⚙️

Граф классов JDK, кстати, весьма любопытно изучать, хоть в первом приближении он и выглядит как безнадёжный клубок. На ряду с классом Object почти такое же рекордное количество связей оказалось у класса String, объяснимо. А вот на третьем месте, вопреки моим ожиданиям увидеть класс Class, оказалось исключение IllegalArgumentException. Ну хорошо хоть не NullPointerException 🤭

#инструменты
👍4
Наверняка эта задачка входит в число хрестоматийных и встречается на собеседованиях по #Java, но поскольку я по ним не хожу, пришлось убить часок на разбирательство с ней. Поделюсь с вами; вдруг кто-то тоже встретит.

🎓 Дано
Ванильный пул потоков на голом JDK (аналоги из класса Executors не подходят):
var threadPool = new ThreadPoolExecutor(
corePoolSize, // 3
maxPoolSize, // 30
keepAliveTime, // 60
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(maxQueueLength), // 100
Executors.defaultThreadFactory(),
new DiscardPolicy()); // could also be a logging implementation


И некий метод, который сначала просто напихивает задачки в этот пул:
List<Future> futures = new ArrayList<>();
futures.add(threadPool.submit(task));


, потом идёт по своим делам, а когда заканчивает с ними, проверяет готовность задачек и, если надо, дожидается завершения каждой, чтобы продолжить работу дальше:
for (Future future : futures) {
try {
future.get();
}
catch (Exception e) {
log.error("Failed to get task result from future", e);
}
}


🔍 Найти
Какого _🙊_ в некоторых случаях метод намертво зависает на вызове future.get()?
При этом в дампе потоков нет никаких следов задач, которые бы зависли/зациклились/заблокированы.

🔑 Решение
Вариант 1. Добавить таймаут в метод future.get(). Да, это решит проблему, но не даст понять, почему она появилась.

Вариант 2. Извернуться как-нибудь так, чтобы прийти к вызову вида CompletableFuture.allOf(c1, c2, c3).join(). Наверняка так можно (не проверял), но выглядит избыточно сложно, должно же работать и так.

Вариант 3. Обернуть вызов future.get() вот в такое условие:
    if (future.isDone()) {
future.get();
}

Казалось бы, смысла в нём нет, ведь если задача не завершена (isDone() == false), то мы просто дождёмся её завершения при вызове get(). Но нет. Когда мы имеем дело с ThreadPoolExecutor, то при вызове submit() он в качестве имплементации Future возвращает экземпляр FutureTask, у которого метод isDone() выглядит так:
    public boolean isDone() {
return state != NEW;
}

, то есть "сделанной" считается любая не новая задача (в том числе когда она ещё в работе).

А метод FutureTask.get() устроен так:
    public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}

, то есть ожидание наступит для любого состояния, предшествующего завершению, в том числе для вышеупомянутого NEW (на самом деле только для NEW и COMPLETING).

Но если бы задача, на которой завис наш поток, была в состоянии COMPLETING, мы наверняка увидели бы соответствующий ей поток в дампе, а раз его нет, значит, она в NEW.
А как она могла оказаться NEW, если все задачи вроде как были переданы пулу на исполнение? Видимо, он её не взял. А почему не взял? Правильно — из-за переполнения очереди, ведь она ограничена (new LinkedBlockingQueue<>(maxQueueLength)). А почему тогда это не привело к ошибкам и прерыванию всего процесса? Верно, из-за с виду безобидной new DiscardPolicy(), которая просто дропает не влезшие задачки (и логирует их, как было в нашем случае).

Вся цепочка кратко:
— не влезающие в очередь задачи молча отбрасываются политикой пула, оставляя им статус NEW;
— видя этот статус, метод future.get() впадает в бесконечное ожидание;
— исключить ожидание можно добавлением проверки на NEW в виде вызова future.isDone().
⚠️ Если очередь будет большой, а задачи — долгоиграющими, можно нарваться на случай, когда задача уже в очереди и имеет шанс быть выполненной, но цикл с вызовом get() её не подождёт. В этом случае решение должно быть другим.

Мораль: пишите однопоточные приложения🤪
😁12🫡42👍2
Лайфхак для performance-инженеров 👷‍♂️

Коротко: Парсить большие heap-дампы для анализа в Eclipse MAT лучше в recovery mode: получается быстрее и стабильнее

Подробно:


Давеча потребовалось распарсить 30 ГБ-ый дамп памяти JVM-приложения для анализа в Eclipse Memory Analyzer Tool (MAT). Для этого в MAT есть отдельный консольный парсер, который тоже, разумеется, работает на #Java 🤭

И поскольку у меня на рабочей машине (Ubuntu 24.04) всего 32 ГБ ОЗУ, встал вопрос, сколько выставить -Xmx (максимальный размер кучи) парсеру, чтобы он (а) не притеснял другие приложения и (б) сам не скукожился от OutOfMemory: Java Heap Space?

Первая попытка выставить 28 ГБ провалилась: парсер был жестко прибит несокрушимым OutOfMemory Killer’ом за попытку аллоцировать слишком много памяти, причем нативной. Для тех, кто не в курсе, почему так происходит, у меня был доклад “Скажите «Ой»: JVM и OOM Killer” – там описан этот механизм ядра Linux и способы избегать столкновения с ним 🔫

Второй была попытка сторговаться на 26 ГБ, и она удалась, правда, ждать пришлось минут 15-20. Оно и понятно, ведь при таком потреблении парсер солидную (если не бОльшую) часть времени провёл не за делом, а в обнимку с GC, пытаясь на лету подчищать за собой объекты. Как бы там ни было, задача была решена, и это клёво

Но уже через пару дней от того же заказчика пришёл новый дамп, на сей раз на 55 ГБ, и стало ясно, что так просто я уже не отскочу 🫠

Не особо веря в успех, я всё же решил попробовать отдать парсеру вообще всю память машины. Но для этого надо как-то заставить ОС не мешаться. А как это сделать? Правильно – Recovery Mode: максимально голый режим командной строки, используемый обычно для восстановления после жестких аварий ОС (в Windows есть его аналог под названием Safe Mode, хотя там графическая оболочка всё равно запускается)

Выставив в этом режиме -Xmx30G (с отступом на аллокации в нативной части), я был приятно удивлен тем, что парсинг успешно завершился. Больше того, он завершился гораздо быстрее, минуты за три. Видимо, возможность беспрепятственно бросить все 16 ядер CPU на борьбу с мусором позволили JVM максимально эффективно распорядиться памятью и не аффектить производительность прикладного кода ⛲️

Разумеется, здесь есть щепотка везения, ведь пиковое потребление памяти парсером во многом зависит от характера графа объектов в обрабатываемом дампе, а значит, в следующий раз такой трюк может и не пройти 🤹

Тем не менее, Recovery Mode — достаточно простой и эффективный способ превратить ваш ноутбук в настоящую Java-машину, поэтому предлагаю взять на заметку ✍️
🔥15👍32👏1
SSH-туннели для #Java Dev&Ops 🔭

Ни один из последних моих проектов не обходился без подключения по SSH, чтобы что-то посмотреть/настроить/отладить/пофиксить и т.д. Но часто для этого нужно подключиться к определённому TCP-порту удалённого сервера, а он недоступен: то открыт лишь на localhost, то где-то маршрута не хватает, то безопасники запрещают. Во всех этих случаях на выручку приходили SSH-туннели. Расскажу о них чуть подробнее, вдруг кто-то не знал или недооценивал 🧐

Что это?
Фича SSH клиента и сервера, позволяющая "пробросить" порт с удалённого сервера на свою локальную машину (где запущен SSH клиент). То есть сделать так, чтобы обращение на этот порт у localhost'а волшебным образом приводило к обращению на этот же (или другой) порт у сервера, но при этом весь траффик оставался закрытым внутри SSH-соединения.
По смыслу это похоже на port-forwarding у kubectl и docker, но может применяться в бОльшем числе случаев ⚒️

Зачем?
Чтобы мультиплексировать множество соединений с удалённым сервером по одному SSH-каналу без открытия портов на внешних сетевых интерфейсах сервера. Проще говоря, чтобы завернуть весь траффик внутрь SSH независимо от протокола. Например:
• чтобы удалённо отлаживать JVM-приложение, не выпячивая порт отладчика наружу;
• чтобы мониторить приложение по JMX, не заморачиваясь с настройкой SSL для этого;
• чтобы дергать Spring-приложение за actuator, не затеваясь с получением сертификатов для HTTPS 📌

Как включить?
В общем случае включение сводится примерно к такой команде в консоли:
ssh -L <local-port>:<remote-address>:<remote-port> -N -f <ssh-host>

, где:
-L — команда на включение локального туннеля;
local-port — порт, на котором вы хотите видеть удалённый сервис на своей машине (как правило, равен remote-port);
remote-port — порт удаленного сервера, который нужно сделать доступным у себя (как правило, равен local-port);
remote-address — IP или имя сетевого интерфейса, на котором открыт удалённый пробрасываемый порт; чаще всего это localhost, но могут быть и другие;
-N — признак отсутствия команды, которую нужно выполнить на удалённом сервере (обычно она не нужна);
-f — признак запуска туннеля в фоновом режиме; если не добавить, управление не вернётся в консоль, и туннель будет висеть в ней;
<ssh-host> — либо имя хоста для подключения (может быть с именем пользователя, например, [email protected]), либо название настроек подключения из файла ~/.ssh/config, например:
Host my-sandbox-vps
Hostname 55.555.555.0
User root
IdentityFile ~/.ssh/id_dsa
IdentitiesOnly yes

С такими настройками включение SSH-туннеля для удалённой отладки может выглядеть так:
ssh -L 5005:localhost:5005 -N -f my-sandbox-vps

Причём и эта команда, и файл настроек останутся точно такими же даже под Windows, если у вас установлен OpenSSH 🪟

Как выключить?
Поскольку каждый туннель — это отдельный процесс, для его выключения достаточно этот процесс остановить. На Linux/MacOS это может выглядеть примерно так:
$ ss -tulpn | grep 5005
tcp LISTEN 0 128 127.0.0.1:5005 0.0.0.0:* users:(("ssh",pid=307017,fd=5))
$ kill 307017

А если туннель запущен без флага -f, то в его консоли достаточно нажать Ctrl+C✖️

Какие недостатки?
• быстродействие: как правило, скорость передачи заметно падает, поэтому смотреть видосики в UHD по такой схеме будет не комфортно;
• искажение цепочки вызовов: все запросы в целевой сервис будут приходить с localhost, где запущен SSH-сервер; это может создавать проблемы;
• непрозрачность: если целевой хост перестанет быть доступен, туннель просто молча упадёт, не подавая явных признаков и не пытаясь переподключиться (это всё преодолимо, но потребует усилий) 🧟

P.S. Картинка взята из этой статьи Ивана Величко, где он также объясняет и другие варианты построения туннелей на SSH. Почитайте, и пусть вам это никогда не пригодится 🫠
🔥171
This media is not supported in your browser
VIEW IN TELEGRAM
На заметку troubleshooter'ам ✍️

На днях вышел alpha-релиз четвертой версии Recaf — среды для реверс-инжениринга приложений на #Java. Она позволяет получить множество сведений о готовом приложении (или его процессе) даже без доступа к исходному коду 👨‍💻

Вот примеры применений из моего опыта:
• проверить, что недавние изменения в определённом классе действительно попали в финальный артефакт приложения — это бывает полезно при косячном CI/CD или путанице с версиями приложения;
• выяснить значение определённого свойства или флага у запущенной JVM — пригождается при анализе проблем производительности и расследовании некорректного поведения;
• определить classLoader, которым был загружен такой-то класс в runtime — бывает полезно при разбирательствах с ClassNotFoundException, NoClassDefFoundError, ClassCastException и прочими непотребствами 🐞

Разумеется, всё это можно сделать и другими средствами (я так и делал), но, согласитесь, удобнее и быстрее работать, когда есть единый инструмент с интуитивно понятным интерфейсом, который не надо каждый раз вспоминать (в отличие от синтаксиса под-команд jcmd) 🛠

Из любопытного: Recaf также позволяет менять байт-код (в том числе структуру классов) и применять эти изменения на лету. Интуитивно кажется, что это нужно, например, чтобы обманывать локальный Minecraft и накручивать себе очки и жизни (такой ArtMoney для Java). Если знаете реальные кейсы применения такой фичи в неигровых приложениях (и с законными целями), то поделитесь, пожалуйста — будет интересно 🤲
👍5🔥2