Cейчас на работе столкнулся с тем, что запускать Apache ActiveMQ с помощью Ansible нормально не получается (он останавливается, но обратно не стартует), и потребовалось перевести его на systemd-сервис. Запускается, конечно, ActiveMQ по классике костылестроения: вызывается штатный скрипт bin/activemq
в 700 строк с аргументами start
или stop
, и создаётся PID-файл с номером процесса.
Ранее точно такой же запуск я встретил в Apache Tomcat. Та же песня со скриптами, но там ещё есть чудовищный лог-файл catalina.out и сопутствующие логи, для которых я делал правила logrotate.
Главное, Apache и не пытается эту проблему решить, их всё, видимо, устраивает. На дворе 2025 год, systemd есть везде, его преимущества уже понятны, кажется, всем, но в компании Apache до сих пор пребывают в реальности начала 2000-х и продолжают писать «очень функциональные скрипты инициализации», которые «делают ненужными сервисы», создают PID-файлы и предпочитают свой «Poor Man’s Daemon Supervisor»™ из спичек и желудей. Конечно, и startup.sh с shutdown.sh из Томката никуда не делись, все так и продолжают управлять этим приложением по старинке и совать скрипты в юнит-файлы systemd. Ладно, хотите своих «функциональных скриптов» — шут с вами, но хоть документацию по созданию нормального сервиса оставьте! Нет, дорогие коллеги, у нас только традиционные ценности:
Сообщение на сайте с документацией по Apache ActiveMQ
Недавно, ища способы создать нормальный сервис для Томката, я обнаружил блестящую статью, написанную ещё в прошлом десятилетии, мой примерный и чуть сокращённый перевод которой привожу ниже (с поправкой на то, что некоторые ссылки на примеры в оригинале уже не работают и т. п.). Там разбирается дикое нагромождение костылей при запуске Томката и корявые, но, тем не менее, широко распространённые способы решить проблему перевода этого приложения под управление systemd.
Не сказать, чтобы я был большим специалистом в java-приложениях, но, во всяком случае, после прочтения статьи хотя бы становится понятно, как правильно делать сервисы для них и в каком направлении двигаться. Лучше поздно, чем никогда, большое спасибо автору. У него на сайте есть ещё много интересных материалов на странице «Frequently Given Answers». Будем учиться делать правильно и красиво, пока есть возможность. Итак, статья:
Ужасы systemd: оборачивание Apache Tomcat во множество бесполезных слоёв
Jonathan de Boyne Pollard
Wrapping Apache Tomcat in many pointless extra layers
В ролях
В составе дистрибутива Tomcat в каталоге bin/
есть файлы startup.sh и shutdown.sh.
Многие используют эти скрипты для запуска и остановки Томката, когда они настраивают сервис systemd. Выглядит это примерно так:
[Unit]
Description=Apache Tomcat 8
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/tomcat8/bin/startup.sh
ExecStop=/usr/tomcat8/bin/shutdown.sh
RemainAfterExit=yes
User=tomcat8
Group=tomcat8
[Install]
WantedBy=multi-user.target
Мэтт Куинн идёт дальше и пишет ещё один скрипт, объединяющий эти два, чтобы вызывать его с параметрами start
и stop
.
[Unit]
Description=Tomcat 7 service
After=network.target
[Service]
WorkingDirectory=/usr/local/tomcat/bin
RemainAfterExit=yes
ExecStart=/bin/sh systemd-tomcat start
Type=forking
User=tomcat
Group=tomcat
[Install]
WantedBy=multi-user.target
Скрипт systemd-tomcat:
#!/bin/sh
case $1 in
start)
/usr/local/tomcat/bin/startup.sh
;;
stop)
/usr/local/tomcat/bin/shutdown.sh
;;
*)
echo "Unkown action '$1'"
;;
esac
Также, есть люди, использующие скрипт daemon.sh в том же каталоге bin/
, думая, что так лучше:
[Unit]
Description=Apache Tomcat Web Application Container
After=network.target
[Service]
Type=forking
ExecStart=/opt/java/tomcat/bin/daemon.sh start
ExecStop=/opt/java/tomcat/bin/daemon.sh stop
[Install]
WantedBy=multi-user.target
Всё это никуда не годится.
В чём проблема
Первый юнит-файл, приведённый выше, настраивает тип сервиса как oneshot, что неприменимо к Томкату, который должен постоянно работать после того, как будет запущен — это не сценарий, который отработал и больше не висит в процессах. Так как такой тип сервиса, очевидно, не работал как положено, автор добавил костыль в виде RemainAfterExit=true
, который заставляет systemd продолжать рассматривать сервис как активный после завершения всех его процессов.
Простая истина, на которую указал Адам Янг ещё в 2011 году, заключается в том, что Томкат — это Java-приложение. Чтобы запустить его, нужно задать несколько переменных окружения и запустить /usr/bin/java
с классом org.apache.catalina.startup.Bootstrap
, параметром start
и некоторыми прочими аргументами Java, которые представляют собой всего лишь устаревшие способы передачи переменных окружения в приложение. «Но», — можно спросить — «что тогда делают startup.sh, shutdown.sh и daemon.sh?» Ответ заключается в том, что это просто обёртки, и они не только практически совершенно излишни, но и в некоторых случаях значительно усложняют конструкцию и делают её неустойчивой.
Если посмотреть на содержимое скриптов startup.sh и shutdown.sh, то становится понятно, что они оба запускают третий скрипт catalina.sh с соответствующим аргументом start или stop. Если вспомнить подход Мэтта Куинна, то он становится абсолютно избыточным: его собственный общий скрипт вызывает startup.sh и shutdown.sh, а те, в свою очередь, вызывают другой общий скрипт catalina.sh. «Значит, можно просто убрать эти две лишних ступени и вызывать catalina.sh напрямую?» Нет, это не так. Catalina.sh содержит очень много лишнего (логика для Cygwin, MacOS и т. д., не имеющая отношения к systemd) или неприменимого, потому что к этому файлу обращение всегда идёт через startup.sh и shutdown.sh. Если отбросить всё это и оставить только суть, то catalina.sh:
- Читает setenv.sh (если он есть) и setclasspath.sh, чтобы задать переменные окружения.
- Устанавливает несколько собственных переменных окружения.
- Для старта приложения запускается процесс
/usr/bin/java
с переменными и аргументомstart
, а также пишется PID-файл, использующийся для страховки от двойного запуска. - Для остановки приложения запускается процесс
/usr/bin/java
с переменными и аргументомstop
, идёт ожидание остановки процесса и стирается PID-файл.
Оказывается, catalina.sh — тоже посредник! Это ни что иное, как «диспетчер служб для бедных», написанный (как всегда, плохо) на скриптах командной оболочки. Но у нас уже есть гораздо лучший диспетчер — systemd, под управлением которого мы и пытаемся изначально запустить Томкат, так что всё это ни к чему. Совершенно не нужен бессмысленный PID-файл — верный спутник костылей из скриптов оболочки; это опасный, шаткий и ненадежный механизм. Нормальные менеджеры сервисов не нуждаются в этом. Они просто запоминают ID процесса, который сами и порождают.
Вернёмся опять к скрипту М. Куинна, который вызывает штатные скрипты Томката как дочерние процессы вместо их непосредственного исполнения. Есть 3 модели, которым должен следовать демон, управляемый systemd, и этот подход не соответствует ни одной из них.
Type=simple
— процесс, порождаемый systemd, является демоном. Но здесь это эфемерный процесс, завершающийся, как толькоcatalina.sh start
запустит свой дочерний процесс и запишет PID-файл.Type=forking
— демоном является дочерний процесс, непосредственно порождённый, в свою очередь, процессом, порождаемым systemd. Опять же, не в этом случае. Здесь дочерний процесс — это startup.sh, и остаётся пройти ещё один дочерний процесс и выход из родительского для достижения реального демона.Type=notify
— гораздо более гибкая модель в плане определения, какой процесс в конечном итоге будет демоном. Но для работы этого режима нужен механизм прямой связи между systemd и демоном — протоколsd_notify
, который в Томкате не реализован.
Непосредственный вызов catalina.sh мог бы соответствовать варианту Type=forking
, но здесь всё равно остаётся кривой, ненадёжный и совершенно ненужный механизм с PID-файлом. Можно сделать лучше: использовать сервис Type=simple
, где нет никаких PID-файлов, с которыми запускаемое Java-приложение никак не связано.
Ещё один ужас catalina.sh — это логирование. Потоки стандартного вывода и вывода ошибок демона перенаправлены в лог-файл, что является заведомо плохим методом: невозможно ни ограничить размер лога, ни сделать ротацию, ни сжать его. Файл лога растёт до бесконечности, пока демон работает. Нелепо так делать при наличии systemd, который может направлять вывод в разные файлы журнала, при этом они будут ограничены по размеру и могут быть ротированы.
Что касается последнего способа с вызовом daemon.sh, то это аналогичный случай для другой грустной истории. Дело в том, что Apache в своей документации рекомендует использовать jsvc
для создания демона. Но, как выясняется, официальная документация не всегда верна и актуальна.
Как сделать правильно
Правильный подход убирает все нагромождения. Единственное, что нужно сделать — и это некоторые люди, применяющие вышеописанные подходы, уже сделали — создать файл, например, /etc/default/tomcat
, со всеми локальными настройками переменных окружения, описывающих вещи типа «где на этой неделе находится среда выполнения Java».
CATALINA_HOME=/usr/share/tomcat
CATALINA_BASE=/usr/share/tomcat
CATALINA_TMPDIR=/var/tmp/tomcat
JAVA_HOME=/usr/share/java/jre-x.y.z
Systemd умеет читать переменные окружения из файла и запускать Java-приложение. Если приложение вызывается с параметром start
, то оно становится демоном. Systemd будет корректно отслеживать PID безо всякого создания дурацких PID-файлов, организует логирование потоков стандартного вывода и вывода ошибок, а также обеспечит одновременный запуск только одного демона.
[Unit]
Description=Apache Tomcat Web Application Container
[Service]
User=tomcat
Group=tomcat
EnvironmentFile=-/etc/default/tomcat
ExecStart=/usr/bin/env ${JAVA_HOME}/bin/java \
$JAVA_OPTS $CATALINA_OPTS \
-classpath ${CLASSPATH} \
-Dcatalina.base=${CATALINA_BASE} \
-Dcatalina.home=${CATALINA_HOME} \
-Djava.endorsed.dirs=${JAVA_ENDORSED_DIRS} \
-Djava.io.tmpdir=${CATALINA_TMPDIR} \
-Djava.util.logging.config.file=${CATALINA_BASE}/conf/logging.properties \
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager \
org.apache.catalina.startup.Bootstrap \
start
ExecStop=/usr/bin/env ${JAVA_HOME}/bin/java \
$JAVA_OPTS \
-classpath ${CLASSPATH} \
-Dcatalina.base=${CATALINA_BASE} \
-Dcatalina.home=${CATALINA_HOME} \
-Djava.endorsed.dirs=${JAVA_ENDORSED_DIRS} \
-Djava.io.tmpdir=${CATALINA_TMPDIR} \
-Djava.util.logging.config.file=${CATALINA_BASE}/conf/logging.properties \
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager \
org.apache.catalina.startup.Bootstrap \
stop
[Install]
WantedBy=multi-user.target
Эти 32 строки юнит-файла заменяют почти 800 строк вложенных друг в друга скриптов, и то — 32 строки набралось только потому, что они были разделены для удобства чтения.
Может быть, в будущем авторы Томката, наконец, заметят, что Адам Янг писал в 2011 году, и научат свой Томкат просто самостоятельно читать эти чёртовы переменные непосредственно из окружения — ведь программы, написанные на Java, могут это делать. Также, возможно, люди найдут способ, чтобы сама Java всегда находилась в одном и том же месте. В таком будущем всё станет гораздо проще.
[Unit]
Description=Apache Tomcat Web Application Container
[Service]
User=tomcat
Group=tomcat
EnvironmentFile=-/etc/default/tomcat
ExecStart=/usr/bin/java $JAVA_OPTS $CATALINA_OPTS org.apache.catalina.startup.Bootstrap start
ExecStop=/usr/bin/java $JAVA_OPTS org.apache.catalina.startup.Bootstrap stop
[Install]
WantedBy=multi-user.target
Так как вместо вызова конечного файла мы вызываем /usr/bin/java
(или /usr/bin/env ${JAVA_HOME}/bin/java
для того, чтобы узнать, где на этой неделе установлена java), в логах systemd из-за этого имя демона отображается как «java» или «env». Исправить эту ситуацию можно, указав опцию SyslogIdentifier=tomcat
в разделе [Service]
.
Бонус-трек
Оказывается, запуск Томката под daemontools (набор инструментов для управления UNIX-сервисами, появившийся задолго до systemd — прим. перев.) даёт подсказку, как запустить его и под systemd. Там catalina.sh запускается не с параметром start
, а с параметром run
— в этом случае catalina.sh напрямую становится демоном, а не создаёт его.
Этот путь с run
, очевидно, предназначен для систем, сделанных для запуска (exec) демонов (как семейство daemontools), а не для их создания (spawn), и тут нет возни с PID-файлом и путаницы с логированием, потому что демоны, работающие в daemontools с 1997 года, не занимались подобным. Тем не менее, этот подход можно применять и для systemd.
[Unit]
Description=Apache Tomcat Web Application Container
[Service]
User=tomcat
Group=tomcat
ExecStart=/usr/share/tomcat/bin/catalina.sh run
[Install]
WantedBy=multi-user.target
P. S. Сервис для ActiveMQ я создал. Для этого я скопировал файл bin/setenv
в conf/env
и оставил там только переменные. Недостающими параметрами я «нарастил» переменную ACTIVEMQ_OPTS
в последней строке.
# Active MQ installation dirs
ACTIVEMQ_HOME="/home/user/apache-activemq-6.1.7"
ACTIVEMQ_BASE="$ACTIVEMQ_HOME"
ACTIVEMQ_CONF="$ACTIVEMQ_BASE/conf"
ACTIVEMQ_DATA="$ACTIVEMQ_BASE/data"
ACTIVEMQ_TMP="$ACTIVEMQ_BASE/tmp"
ACTIVEMQ_OPTS_MEMORY="-Xms64M -Xmx1G"
ACTIVEMQ_OPTS="$ACTIVEMQ_OPTS_MEMORY -Djava.util.logging.config.file=logging.properties -Djava.security.auth.login.config=$ACTIVEMQ_CONF/login.config"
ACTIVEMQ_OUT="/dev/null"
ACTIVEMQ_SUNJMX_START="$ACTIVEMQ_SUNJMX_START -Dcom.sun.management.jmxremote"
ACTIVEMQ_SUNJMX_CONTROL=""
ACTIVEMQ_QUEUEMANAGERURL="--amqurl tcp://localhost:61616"
ACTIVEMQ_SSL_OPTS=""
ACTIVEMQ_KILL_MAXSECONDS=30
ACTIVEMQ_USER=""
JAVACMD="auto"
ACTIVEMQ_OPTS="$ACTIVEMQ_OPTS -Djava.awt.headless=true -Djava.io.tmpdir=tmp -Dactivemq.classpath=${ACTIVEMQ_CONF}: -Djolokia.conf=file:$ACTIVEMQ_CONF/jolokia-access.xml --add-reads=java.xml=java.logging"
Юнит-файл с указанием файла с переменными:
[Unit]
Description=ActiveMQ
After=network.target
[Service]
Type=exec
EnvironmentFile=/home/user/apache-activemq-6.1.7/conf/env
SyslogIdentifier=activemq
ExecStart=/usr/bin/java -jar /home/user/apache-activemq-6.1.7/bin/activemq.jar start
ExecStop=/usr/bin/java -jar /home/user/apache-activemq-6.1.7/bin/activemq.jar stop
Restart=on-failure
RestartSec=3s
[Install]
WantedBy=multi-user.target
Результат: служба работает без скриптов и PID-файлов! Ошибки в логе не в счёт, просто тестовое приложение на моей домашней виртуалке совершенно не настроено.
user@k1:~/ansible$ systemctl --user status activemq --no-pager
● activemq.service - ActiveMQ
Loaded: loaded (/home/user/.config/systemd/user/activemq.service; enabled; preset: enabled)
Active: active (running) since Sun 2025-10-12 10:58:11 MSK; 2min 31s ago
Main PID: 9913 (java)
Tasks: 49 (limit: 4604)
Memory: 170.9M (peak: 171.4M)
CPU: 6.372s
CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/activemq.service
└─9913 /usr/bin/java -jar /home/user/apache-activemq-6.1.7/bin/activemq.jar start
Oct 12 10:58:14 k1 activemq[9913]: at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63) [activemq-console-6.1.7.jar:6.1.7]
Oct 12 10:58:14 k1 activemq[9913]: at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:154) [activemq-console-6.1.7.jar:6.1.7]
Oct 12 10:58:14 k1 activemq[9913]: at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63) [activemq-console-6.1.7.jar:6.1.7]
Oct 12 10:58:14 k1 activemq[9913]: at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104) [activemq-console-6.1.7.jar:6.1.7]
Oct 12 10:58:14 k1 activemq[9913]: at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
Oct 12 10:58:14 k1 activemq[9913]: at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[?:?]
Oct 12 10:58:14 k1 activemq[9913]: at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
Oct 12 10:58:14 k1 activemq[9913]: at java.base/java.lang.reflect.Method.invoke(Method.java:569) ~[?:?]
Oct 12 10:58:14 k1 activemq[9913]: at org.apache.activemq.console.Main.runTaskClass(Main.java:262) [activemq.jar:6.1.7]
Oct 12 10:58:14 k1 activemq[9913]: at org.apache.activemq.console.Main.main(Main.java:115) [activemq.jar:6.1.7]