Java 21 в стиле «клятый энтерпрайз» на одноплатном компьютере возрастом 13 лет

от автора

Неторопливая (ревизии 000e) «Малинка» в картонной коробке практикует «клятый энтерпрайз»

Неторопливая (ревизии 000e) «Малинка» в картонной коробке практикует «клятый энтерпрайз»

Можно ли успешно эксплуатировать Java-приложение на Raspberry Pi Model B? А если Java — 21‑я, а Spring Boot, на котором основано приложение — версии 4.0.5? Когда сборка «автомагически» оформляет .jar-файл размером под 40 мегабайт, а у «Малинки» тех мегабайт всего 512, и не забываем про операционную систему и рантайм Java. И как «до такого» [соотношения имеющихся и используемых ресурсов] можно «докатиться»?

TL;DR;-версия: можно. Если допустимо время старта около 5-и минут и расход примерно 40% доступной оперативной памяти, то приложение может работать в режиме 24/7 месяцами, обеспечивая вполне достойное (с учётом производительности «Малинки») время отклика. А «докатиться» оказалось легко, однажды «поскользнувшись» на DIY-рецептах из Интернета, и в качестве альтернативы опробовав методологию «клятого энтерпрайза» в домашнем pet-проекте.


Здравствуйте! Меня зовут Юрий, и я «немного Java-программист». У меня есть домашний pet-проект на Raspberry Pi в стиле «клятый энтерпрайз», ставший за 13 лет эксплуатации технологическим экспериментом: как долго «Малинка» сможет обеспечивать приемлемое качество работы приложений на Java, создаваемых с элементами «энтерпрайзных» технологий, под тривиальную задачу удалённого мониторинга температуры? Написать очерк меня вдохновила задорная речь одного интернет-подкастера, сетовавшего в своей передаче на грядущее прекращение поддержки ядром Linux процессоров линейки 486, случайно прослушанная поверх свежих впечатлений от очередного этапа эксперимента, случившегося на длинных мартовских выходных.

Пропустить «лирику» и смотреть цифры

Предмет и длинная предыстория

Большую часть времени Raspberry Pi Model B провел в картонной коробке из первого фото, будучи включенным занимаясь опросом датчиков температуры по 1-Wire и представлением результатов измерений в разных видах по протоколу HTTP. Коробка много лет (и несколько зим, с выключенным устройством) находилась в нерегулярно отапливаемой теплице с диапазонами температур от -35 зимой и до +35 летом. Своеобразный эксперимент по выживанию электроники: в конце первого сезона осенью её просто выключили и про неё забыли, а после первой успешной зимовки в конце второго и затем нескольких последующих сезонов, уже по привычке повторяли. В этой коробке «Малинка» пережила несколько отказавших датчиков, одну SD-карту, один отказавший (и заменённый на такой-же) USB Wi-Fi адаптер TL-WN722N (Atheros) и два или три блока питания. Весьма живучий кусочек электроники. Возможно, мне просто повезло и попался удачный экземпляр. Со временем плата получила «хвост» с разъёмом для получения питания и соединения с линией 1-Wire и с тех пор, на всякий случай, вместе с коробкой и Wi-Fi адаптером зимовала дома в тепле. А в начале прошлой кампании измерений 2025-го года, после инцидента с основательным заливанием коробки водой, переехала в новый корпус из PET-бутылки.

Raspberry Pi Model B с USB Wi-Fi адаптером в новом корпусе

Raspberry Pi Model B с USB Wi-Fi адаптером в новом корпусе
Что «скрывает» боке на предыдущем фото
 Элементы советской эстетики в DIY-проекте

Элементы советской эстетики в DIY-проекте

На примере разъёма резервного устройства поновее и побыстрее, которое всё время пылится на полке.

Однако, не всё безоблачно. Возможно сказался возраст устройства, или 15 метров линии 1‑Wire, или бракованный датчик, или же вовсе просто «кривые руки»™ автора, но в начале этого сезона выяснилось, что один из датчиков температуры отказал (перестал читаться и стал сильно греться при подаче питания), а у платы после 13-и успешных кампаний измерений отказал GPIO, используемый по умолчанию для интерфейса 1‑Wire. Датчики выходили из строя и раньше, но GPIO отказал впервые. Разбор этой ситуации и послужил отправной точкой для углубления «энтерпрайзинга Малинки» и стал одной из причин написания очерка.

Всё начиналось с малого

История с постепенным «энтерпрайзингом Малинки» началась ранней весной 2013-го года с нескольких Python-скриптов для чтения датчиков температуры DS1820 по шине 1‑Wire и представления результатов измерений через HTTP. Простая web-страница с показаниями термометров и обновлением пару раз в минуту. Тогда, при первоначальном создании схемы больше времени заняла настройка Wi-Fi адаптера (сейчас в Raspberry Pi OS адаптер работает «из коробки»), нежели чем адаптация Python-скриптов по «DIY-рецептам» под задачу. Быстро выяснилось, что работало это сильно «так себе» — большая нагрузка (load average около 0.9) и частые «зависания» скриптов, требовавшие перезапуска. Выглядело хрупко и некрасиво. Зато, в чистом виде «DIY из Интернета», как мы его любим.

«Работает — не трогай», или как перестать бояться и полюбить Java-bloat

Так как я «немного Java-программист», то спустя пару месяцев не особо успешной эксплуатации, Python-скрипты были заменены JSP-приложением с эквивалентной функциональностью, выполнявшимся под управлением Apache Tomcat на 7-й Java. Томкэт с приложением стартовал очень неторопливо, что-то около полутора минут. По меркам embedded-систем — целую вечность. Отчасти это заслуга ревизии 000e платы, но по большей степени медленный старт является «фирменной фишкой» Джавы. Зато после «прогрева» сервиса отклик по сравнению с Python-скриптами был заметно быстрее, load average на плате снизился до ~0.5 и работало приложение в последствии стабильно годами. Случилась весьма наглядная иллюстрация того, как Java JIT >> Python. Происходившие за годы эксплуатации перебои с электричеством, отказы блоков питания, адаптера Wi-Fi и датчиков температуры никак не влияли на картину — после восстановления питания или замены отказавшего элемента всё по-прежнему работало «как часы» (не швейцарские). Включали весной и выключали в конце лета. Оно «просто работало» и разумеется, никто это не трогал. Так характерный для «клятого энтерпрайза» Java-bloat (размеры Томкэта, рантайма Джавы и потребное на запуск и прогрев время) стал неотъемлемой частью этого pet-проекта. И сам собой начался эксперимент по «энтерпрайзингу Малинки» Java-приложением в «тепличных» (pun intended) условиях при минимуме обслуживания и внимания.

«Дальше — больше» или уже настоящий «клятый энтерпрайз»

На дворе весна 2022-го года, включаем адаптер питания в сеть и ждем результата в браузере, изредка нажимая «обновить». Хм, не работает. Несём домой и выясняем, что перестала работать SD-карта. Ок, меняем карточку, восстанавливаем подключения. Всё работает как обычно. Всё бы ничего, однако этот инцидент послужил отправной точкой для очередного апгрейда системы. К тому времени уже хотелось чуть больше функциональности, а также исправления хоть и мелких, но досадных недоработок. Например, строить графики значений. Или иметь возможность на странице легко отличать «залипшие» по той или иной причине значения от актуальных.

Всё как в «настоящем энтерпрайзе»: говорим «графики», подразумеваем Grafana, и прочие максимально типовые решения с минимумом кастомизации и затрачиваемых усилий. С такой мотивацией я адаптировал механизмы из JSP-приложения под фреймворк Spring Boot с реализацией REST для UI и экспортом метрик для Grafana. Примитивная web-страница с <meta http-equiv="refresh" была заменена новой, использующей React для отображения значений так, чтобы можно было заметить их «залипание». Элемент UI пришлось «состряпать» свой, так как ничего подходящего относительно быстро нагуглить не удалось. Нетривиальные запросы иногда требуют собственных велосипедов с квадратными колёсами.

В итоге получилось:

  • хоть и состоящий из одного GET’а, но «всё-таки REST»;

  • React UI для резервного механизма наблюдения значений непосредственно с устройства;

  • экспорт метрик через Actuator для цепочки Prometheus -> Grafana.

На базе современного тогда Spring Boot v2.6.3. Постарался сделать «просто и без затей» с минимумом кода и затраченных усилий. И даже с элементом обратной совместимости — в браузерах на нескольких компьютерах, планшетах и телефонах пользователей уже прижились закладки, которые менять все и сразу было «не с руки». Так появился редирект пути «/thc» в приложении, по названию (и, соответственно, пути в URL) предыдущего JSP-приложения на Томкэте — ну чем не «энтерпрайз»? Шутка, разумеется, но доля шутки не 100%. Особенно, если обратить внимание на размер .jar-файла приложения после сборки. ZOMG! 23 мегабайта! Но что поделать, таков «путь энтерпрайза». ОС на плате не трогал — отдельная история, из-за чего JRE пришлось разворачивать из подходящего .tar.gz. Разворачиваю Java 11 JRE для armhf, копирую .jar-файл приложения на устройство, создаю конфигурацию, запускаю — стартует 170 секунд. Плюс-минус. Почти три минуты (три, Карл!). С другой стороны, всего-то в два раза дольше, чем Томкэт почти десятилетней давности на 7-й Джаве. «И тааак сойдет!» [голосом героя мультфильма про Тридевятое царство]

Вот так, Java-bloat стал ещё больше bloat, вместе с возросшим потреблением ресурсов и значительно выросшим временем запуска приложения. Epic Win! /s С другой стороны, приложение после апгрейда продолжило «просто работать» по-прежнему, годами не доставляя никаких хлопот. Но уже с обновлённой функциональностью и стильным-модным дэшбордом из связки Prometheus->Grafana, размещенной на другом устройстве по-современнее. Стало гораздо лучше: красивые графики, обновлённый UI, где сразу заметно «залипание» показаний, что особенно удобно в браузерах на мобильных устройствах. И резко возросшие (хоть и необязательные, если выключить Grafana) инфраструктурные издержки — всё как положено в «настоящем энтерпрайзе».

Дух авантюризма

Этой весной, перед началом очередной кампании измерений, произошёл отказ. «Малинка» запустилась, продемонстрировала web-интерфейс, но показания со всех датчиков температуры отсутствовали. Для упрощения отладки плату унёс домой и подключил к монитору и клавиатуре. У платы отказал GPIO7, а на линии один из датчиков. Устранил, переместив 1-Wire на находящийся рядом пин GPIO17 — перекинул проводок «хвоста» на нужный пин и поправил загрузочную конфигурацию платы.

Отряд (почти) не заметил потери бойца. Нестандартный GPIO для 1-Wire

Отряд (почти) не заметил потери бойца. Нестандартный GPIO для 1-Wire

Датчик заменил на новый и обновил адрес в конфигурации приложения. Дело сделано, оно и дальше будет работать как прежде. Включаю питание и наблюдаю на мониторе мельтешение строк при загрузке операционной системы, уже ставшей «винтажной» за время эксплуатации. И тут появляется шальная мысль: «а что будет, если обновить операционную систему, рантайм Java и Spring Boot?» Оно бы и дальше нормально работало без «этого всего». WTF, dude!? Но дух авантюризма и любопытство победили здравый консерватизм.

По возможности воздерживайтесь от созерцания мельтешащих строк при загрузке ОС Linux. Это опасно. Заставки при загрузке ОС придуманы не зря.

Сила инерции

Как сделать из 23-х мегабайтного .jar-файла 40-ка мегабайтный? Очень легко: обновить версии, добавить зависимость (закрыть уязвимость), поправить имя одного класса и собрать приложение. Следует отметить, что в «клятом энтерпрайзе» так бывает далеко не всегда — быстрее наоборот. Под спойлером спрятана разница в содержательной части исходного кода приложения между версией на Spring Boot v2.6.3 / Java 11 (old) и версией на Spring Boot v4.0.5 / Java 21 (cur). Позволяет оценить потребовавшееся количество правок под четыре года развития фреймворка.

diff содержательной части исходного кода
diff -urN old-therm/pom.xml cur-therm/pom.xml--- old-therm/pom.xml2022-03-05 19:23:17.959791395 +0700+++ cur-therm/pom.xml2026-03-08 14:54:51.243651927 +0700@@ -6,17 +6,17 @@      <groupId>ru.ykdy</groupId>     <artifactId>therm-server</artifactId>-    <version>1.0-SNAPSHOT</version>+    <version>1.1</version>      <properties>-        <maven.compiler.source>11</maven.compiler.source>-        <maven.compiler.target>11</maven.compiler.target>+        <maven.compiler.source>21</maven.compiler.source>+        <maven.compiler.target>21</maven.compiler.target>     </properties>      <parent>         <artifactId>spring-boot-starter-parent</artifactId>         <groupId>org.springframework.boot</groupId>-        <version>2.6.3</version>+        <version>4.0.5</version>     </parent>      <dependencies>@@ -27,7 +27,7 @@         <dependency>             <groupId>org.projectlombok</groupId>             <artifactId>lombok</artifactId>-            <version>1.18.22</version>+            <version>1.18.44</version>         </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>@@ -36,7 +36,12 @@         <dependency>             <groupId>io.micrometer</groupId>             <artifactId>micrometer-registry-prometheus</artifactId>-            <version>1.8.2</version>+            <version>1.16.4</version>+        </dependency>+        <dependency>+            <groupId>io.netty</groupId>+            <artifactId>netty-codec-http2</artifactId>+            <version>4.2.12.Final</version>         </dependency>     </dependencies> @@ -45,7 +50,7 @@             <plugin>                 <groupId>org.springframework.boot</groupId>                 <artifactId>spring-boot-maven-plugin</artifactId>-                <version>2.6.3</version>+                <version>4.0.5</version>             </plugin>             <plugin>                 <groupId>org.apache.maven.plugins</groupId>diff -urN old-therm/src/main/java/ru/ykdy/therm/server/Core.java cur-therm/src/main/java/ru/ykdy/therm/server/Core.java--- old-therm/src/main/java/ru/ykdy/therm/server/Core.java2022-03-05 19:23:17.503562339 +0700+++ cur-therm/src/main/java/ru/ykdy/therm/server/Core.java2026-03-08 15:02:52.157217374 +0700@@ -1,5 +1,5 @@ /*- * Copyright (C) 2022 Yury Danilyuk+ * Copyright (C) 2022,2026 Yury Danilyuk  *  * This program is free software: you can redistribute it and/or modify  * it under the terms of the GNU General Public License as published by@@ -17,7 +17,7 @@ package ru.ykdy.therm.server;  import com.fasterxml.jackson.annotation.JsonIgnore;-import io.micrometer.prometheus.PrometheusMeterRegistry;+import io.micrometer.core.instrument.MeterRegistry; import lombok.Data; import lombok.Getter; import org.springframework.scheduling.annotation.Scheduled;@@ -39,7 +39,7 @@     @Getter     private final List<Thermometer> thermometers; -    public Core(Config config, PrometheusMeterRegistry registry, Reader reader) {+    public Core(Config config, MeterRegistry registry, Reader reader) {         cutOffSeconds = config.getCutOffSeconds();         updateSeconds = config.getUpdateSeconds();         Map<String, String> configMap = config.getThermometers();

Небольшое лирическое отступление: почему 21 а не 25? Попробую оправдать свою лень и моральную неготовность к затягиванию процесса переезда на «настолько новую» платформу тем, что это якобы «не в духе энтерпрайза». Новая платформа: надо чтобы «всё утряслось», первопроходцы набили и описали все шишки, разные ошибки выявили и исправили — типовой набор отговорок в корпоративной среде. «Может быть, позже»™

Итак, после апгрейда ОС на последний доступный на тот момент дистрибутив от декабря 2025 года Raspbian GNU/Linux 13 (trixie), отмечаю заметно возросшее время загрузки ОС. Если раньше приглашение «login:» появлялось примерно на 110-й секунде после включения устройства в сеть, то после обновления этих секунд стало ~260. Ауч! Проделываю небольшой трюк: sudo apt-get purge cloud-init rpi-connect-lite и получаю ~155 с старта. В 1,4 раза, но таков «путь энтерпрайза». Продолжаю эксперимент — ставлю Java 21, копирую приложение и конфигурацию, проверяю работоспособность приложения, сохраняю лог первого запуска, настраиваю и проверяю автостарт. Возвращаю устройство в теплицу.

Производительность и ресурсоёмкость

Очень простой план: Apache JMeter, 5 потоков по тысяче запросов. К эндпоинтам /thermometers со значениями измерений в JSON и /actuator/prometheus с метриками для цепочки Prometheus->Grafana. В процессе выполнения второй части плана стало очевидно, что «Малинка» на 5-и потоках заметно «буксует». Что было подтверждено (заметно по KB/s) дополнительным замером в 2 потока, показавшим уже более-менее приемлемый отклик для эндпоинта метрик. Результаты в таблице:

Label

#Samples

Failed

Average ms

Min ms

Max ms

Median ms

90th pct ms

95th pct ms

99th pct ms

Transactions/s

Received KB/sec

Sent KB/sec

thermometers 5Tx1k

5000

0

100.79

17

597

80.00

178.00

226.95

331.96

44.76

12.46

6.56

prometheus 5Tx1k

5000

0

909.13

158

32370

842.00

964.00

1011.00

1157.99

5.49

76.88

0.8

prometheus 2Tx1k

2000

0

376.58

159

28627

331.00

441.00

487.95

609.86

5.30

74.23

0.81

Для Java 21 в стиле «клятый энтерпрайз» на 13-и летнем одноплатном компьютере результаты выглядят неплохо. Особенно с учётом того, что эксперимент проводится на медленной версии платы — ревизии 000e, которая примерно в полтора раза медленнее более распространённых вариантов. Даже эндпоинт с метриками не выглядит плохо, если учесть, что в обычных сценариях метрики читаются с гораздо меньшей интенсивностью. Частота запросов к JSON-значениям из web‑UI несколько выше, но учитывая результаты JMeter по /thermometers, ей можно смело пренебречь. Отметим значения «Max ms» для эндпоинта с метриками, которые выглядят «очень не очень», но даже здесь ситуация с «хвостами» на самом деле не так ужасна, как может показаться на первый взгляд:

$ cat res-prom5.jtl | awk -F ',' -e '{if($2 > 1500) print $2}'elapsed17892700828727289632969232370186528953289292890729781299621959$ cat res-prom2.jtl | awk -F ',' -e '{if($2 > 1500) print $2}'elapsed88571687528627$

Оказывается, что «хвостов» 13 и 3 в результатах нагрузки в 5 и 2 потока, соответственно. То есть, в пересчёте на запросы всего 0,26% и 0,15%.

280 секунд запуска JVM и приложения. Серьёзная заявка на успех
root@glaho:~# journalctl -u thermмар 09 00:28:01 glaho systemd[1]: Started therm.service - Thermometers service.мар 09 00:29:54 glaho java[612]:   .   ____          _            __ _ _мар 09 00:29:54 glaho java[612]:  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \мар 09 00:29:54 glaho java[612]: ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \мар 09 00:29:54 glaho java[612]:  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )мар 09 00:29:54 glaho java[612]:   '  |____| .__|_| |_|_| |_\__, | / / / /мар 09 00:29:54 glaho java[612]:  =========|_|==============|___/=/_/_/_/мар 09 00:29:55 glaho java[612]:  :: Spring Boot ::                (v4.0.5)мар 09 00:30:01 glaho java[612]: 2026-03-09T00:30:01.220+07:00  INFO 612 --- [           main] ru.ykdy.therm.server.Application         : Starting Application v1.1 using Java 21.0.10 with PID 612 (/home/pi/therm-server-1.1.jar started by pi in /home/pi)мар 09 00:30:01 glaho java[612]: 2026-03-09T00:30:01.417+07:00  INFO 612 --- [           main] ru.ykdy.therm.server.Application         : No active profile set, falling back to 1 default profile: "default"мар 09 00:32:24 glaho java[612]: 2026-03-09T00:32:24.157+07:00  INFO 612 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint beneath base path '/actuator'мар 09 00:32:53 glaho java[612]: 2026-03-09T00:32:53.887+07:00  INFO 612 --- [           main] o.s.boot.reactor.netty.NettyWebServer    : Netty started on port 8080 (http)мар 09 00:32:54 glaho java[612]: 2026-03-09T00:32:54.750+07:00  INFO 612 --- [           main] ru.ykdy.therm.server.Application         : Started Application in 217.13 seconds (process running for 279.872)

Почти пять минут запуска, всё как мы любим в «клятом энтерпрайзе».

Java-процесс в работе использует порядка 40% (35,6) RAM
root@glaho:~# ps xav | sed -n '1p; /java/p'  PID TTY      STAT   TIME  MAJFL   TRS   DRS   RSS %MEM COMMAND  541 ?        Ssl  903:29    281     2 395265 156328 35.6 java -Xms192m -Xmx256m -jar therm-server-1.1.jar 3783 pts/0    S+     0:00      0    92  7587  2108  0.4 sed -n 1p; /java/proot@glaho:~# free               total        used        free      shared  buff/cache   availableMem:          437936      234728       30996         680      224400      203208Swap:         437244           0      437244root@glaho:~#

Забыл отключить Swap. Возможно, часть «хвостов» на нагрузке растёт оттуда. «Может быть, позже»™

Немного общей информации
root@glaho:~# w | sed -n '1p;' 13:21:58 up 36 days, 21:50,  1 user,  load average: 0,56, 0,46, 0,46root@glaho:~# cat /etc/os-release | sed -n '1p;'PRETTY_NAME="Raspbian GNU/Linux 13 (trixie)"root@glaho:~# cat /proc/cpuinfoprocessor       : 0model name      : ARMv6-compatible processor rev 7 (v6l)BogoMIPS        : 697.95Features        : half thumb fastmult vfp edsp java tlsCPU implementer : 0x41CPU architecture: 7CPU variant     : 0x0CPU part        : 0xb76CPU revision    : 7Hardware        : BCM2835Revision        : 000eSerial          : 00000000redactedModel           : Raspberry Pi Model B Rev 2root@glaho:~# ls -la /home/pi/therm-server-1.1.jar-rw-r--r-- 1 pi pi 42067151 мар  9 00:14 /home/pi/therm-server-1.1.jarroot@glaho:~#

Для большей полноты картины.

Результаты обновления

Приложение после старта использует около 40% (sic!) памяти устройства. И запускается уже не три минуты, а почти пять. Java-bloat, в соответствии с концепцией «клятого энтерпрайза», на новом витке эволюции стал ещё больше bloat. Ещё обновлённая ОС загружается заметно дольше, чем «винтажная». С другой стороны, это не повлияло на работоспособность системы, так как время отклика по-прежнему приемлемое, а две-три дополнительных минуты на старте в этом кейсе допустимы и «погоды не делают». Максимальный аптайм пока наблюдался всего в 36 суток, но ограничен он был отключениями питания и перезагрузками на обновления.

В обновлённых версиях ОС, рантайма Java и компонентах фреймворка исправлено много разных уязвимостей и багов. Это вполне может оправдать возросшее время старта. Ведь никогда не угадаешь, какая «дрянь» может оказаться на том или ином устройстве в сети.

Немного очевидных банальностей

Возвращаясь к отправной точке очерка — задорной речи подкастера по поводу поддержки старого железа. Да, это может быть полезно (и местами даже забавно) иметь возможность запускать современную ОС, рантайм Java 21 и не самые легковесные приложения на таком старом одноплатном компьютере, как Raspberry Pi Model B. В редких кейсах, когда после старта и «прогрева» приложения, разница для пользователей не заметна и есть возможность «долго подождать на старте», это работает. С другой стороны, в сравнении с предыдущими версиями компонентов системы тенденция на замедление очевидна. Почти всё стало заметно медленнее: загрузка системы, старт приложения и даже открытие ssh-соединения к «Малинке». Или всё медленнее и медленнее, или старый «дырявый» софт в качестве «альтернативы» — неприглядная картина, как бы кому ни хотелось запускать современный софт на компьютере с процессором 486.


Тем временем эксперимент с эксплуатацией Java-приложения в стиле «клятый энтерпрайз» на старом одноплатном компьютере продолжается. [«— Продолжайте вести наблюдение.»]

Благодарю за внимание!

Приложение

Talk is cheap. Show me the code.

ссылка на оригинал статьи https://habr.com/ru/articles/1038874/