Демонизация бота в systemd

В этом туториале вы запустите бота в продакшн-режиме. Если в боте возникнет страшная ошибка, systemd запустит его заново. И даже если весь сервер выключат и включат заново, то systemd сам снова запустит вашего бота.

Прежде чем приступить

Этот туториал требует некоторых знаний:

  • Как запускать команды из консоли.
  • Как подключиться к серверу по SSH.

1. Загрузите бота на сервер

В качестве примера выступит эхобот. Это бот, который отвечает вам тем же, что вы ему написали. Зайдите на сервер с помощью ssh, перейдите в папку /opt и скачайте заготовку бота:

# cd /opt
# git clone https://github.com/devmanorg/echobot-example
# cd echobot-example
 Почему /opt?

Куда класть код — это старый и холиварный вопрос. Столько времени прошло, сколько форумов исписано, а одного простого ответа так и нет…

Вам точно не стоит класть код в домашний каталог суперпользователя /root/. Туда вы обычно попадаете, заходя на сервер по ssh. Если положите код в /root, то не избежите приключений с настройкой прав пользователей. Операционная система оберегает домашний каталог рута от посягательств других пользователей и запрещает им чтение файлов оттуда. Это станет проблемой при запуске Nginx, Gunicorn и других программ, часто работающих под другим пользователем.

Я советую класть код в папку /opt/{project}/. Это место специально выделено в ОС для программ, что собираются из исходников и живут обособлено от каталогов bin, lib и прочих.

Затем проверьте, что файлы скачались командой ls:

2. Запустите бота

Запустите бота по инструкциям из README. В том числе придётся создать токен от бота.

Проверьте, заработал ли ваш бот:

image

Бот работает! Но всё ли так гладко, как кажется?..

Теперь выйдите из терминала. Просто закройте окошко терминала или введите exit и нажмите Enter. Бот перестал работать 😦

Всё потому что вы закрыли ssh-соединение, и теперь сервер остановил работу бота.

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

Нужно запустить бота в фоновом режиме работы. Тогда он продолжит работать, даже если вы выйдете с сервера.

3. Запустите бота в фоне

Программы, которые работают в фоновом режиме, называются демонами или программами в режиме демона.

Утилита systemd как раз умеет демонизировать программы, то есть запускать их в фоне. Но сначала systemd нужно настроить: сказать ему что и как демонизировать. Это делается в специальных файлах с инструкциями для systemd. Такие файлы называют юнитами.

Юниты хранятся в папке /etc/systemd/system. Перейдите в неё и создайте новый файл с названием echobot-example.service. Это и будет ваш юнит.

Запишите в него этот текст:

[Service]
ExecStart=python /opt/echobot-example/bot.py

Все юниты выглядят примерно вот так:

[Название секции в квадратных скобках]
имя_переменной=значение

В переменной ExecStart юнит ждёт команду, которую systemd должен запустить.

Теперь запустите этот юнит:

# systemctl start echobot-example

Получилась ошибка:

Failed to start echobot-example.service: Unit echobot-example.service has a bad unit file setting.
See system logs and 'systemctl status echobot-example.service' for details.

Авторский перевод: Не удалось запустить файл echobot-example.service: в нём неверные настройки. Запустите команду systemctl status echobot-example.service , чтобы узнать что случилось.

Давайте узнаем! Запустите команду systemctl status echobot-example:

image

Юнит явно пишет всё о себе: Loaded: bad-setting (Reason: Unit echobot-example.service has a bad unit file setting.). Тут снова написано о той же ошибке, что была при запуске юнита.

Строка Active: inactive (dead) значит что юнит неактивен/“мёртв”. То есть его либо не запускали, либо запустили, но он поломался. Чуть ниже виден трейсбек поломки:

/etc/systemd/system/echobot-example.service:2: Executable "python" not found in path "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

Значит, что systemd не смог найти python в папке /usr/bin/.

Действительно, в этой папке есть только python3 и python3.8.

Эту проблему легко исправить. Нужно дать программе путь до питона в вашем виртуальном окружении:

[Service]
ExecStart=/opt/echobot-example/venv/bin/python3 /opt/echobot-example/bot.py

Повторите запуск юнита:

# systemctl start echobot-example

Терминал молчит. Это хороший знак? Снова проверьте состояние файла:

# systemctl status echobot-example

image

Самая важная здесь строка Active: active (running). Это значит, что файл запущен. Обратите внимание, что и текст выделен зелёным, а в прошлый раз был красным. Это хороший знак.

Зелёный цвет - это хороший знак.

Проверьте бота в Telegram. Убедитесь, что он работает. Затем закройте терминал или наберите команду exit и затем Enter.

# exit
logout
Connection to 84.38.180.108 closed.

Проверьте бота в Telegram. Теперь он должен работать даже если вы закрыли терминал.

4. Добавьте в автозапуск

Бота иногда приходится перезапускать из-за обновления ПО и изменения настроек. Иногда владелец сервера, который вы арендуете, сам его выключает на пару минут, чтобы обносить софт, закрыть свежие уязвимости, и самостоятельно перезапускает сервер. Такое бывает даже на крупных хостингах, они отключают сервер всего на пару секунд и прислают письма с извинениями на почту.

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

# reboot now

Проверьте бота:

image

Бот снова перестал работать 😦

Это потому что вы не сказали systemd запускать ваш файл, если сервер включается или перезагружается. Это можно исправить:

[Service]
ExecStart=/opt/echobot-example/venv/bin/python3 /opt/echobot-example/bot.py

[Install]
WantedBy=multi-user.target

Строка [Install] Означает блок настроек при запуске юнита. В этом блоке мы добавили одну настройку: WantedBy=multi-user.target.

Таргет — это группа процессов, которые запустятся вместе, пачкой. multi-user.target — это таргет, который запускается сразу при старте сервера. Получается, что настройка WantedBy=multi-user.target добавляет ваш юнит в таргет, который запустится при рестарте сервера.

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

# systemctl enable echobot-example
Created symlink /etc/systemd/system/multi-user.target.wants/echobot-example.service → /etc/systemd/system/echobot-example.service.

Эта команда создаёт симлинк вашего юнита. Симлинк — это прямая ссылка на файл, что-то вроде ярлыка .exe в Windows. Теперь, если вы захотите убрать ваш файл из автозагрузки, то будет достаточно написать systemctl disable echobot-example. Это удобно, потому что не надо лезть в файл юнита и чего-то исправлять, всё отключается одной командой.

Теперь снова перезагрузите сервер с помощью команды reboot now и проверьте бота. Он должен быть запущен, даже после перезагрузки.

5. Имитация сбоя

Плохая новость: программы иногда ломаются. Такое бывает даже с очень опытными разработчиками и даже в очень больших компаниях.

Чтобы понять, работает автозапуск или нет, нужно как-то сымитировать поломку программы. На Windows это довольно просто сделать: нужно нажать Ctrl+Shift+Esc, откроется диспетчер задач. Там выбираете любой процесс и жмёте “снять задачу”.

В Linux можно провернуть то же самое, причём из терминала. Для этого есть специальная команда kill, но сначала нужно узнать PID вашего юнита. PID — это ProcessID, то есть id процесса. Все процессы можно получить в терминале этой командой:

# ps -aux

Выведется полотно из всех процессов, что запущены на сервере:

В таком полотне совершенно невозможно что-либо найти. Поможет поиск по ключевым словам, тоже команда linux, которая называется grep. Вместе эти команды запускаются так:

# ps -aux | grep "ключевые слова для поиска"

Попробуйте найти PID вашего процесса по названию юнита:

# ps -aux | grep echobot-example

Вот что получилось у нас:

Нашлось 2 процесса. Первый — наш юнит, а второй — собственно процесс поиска через grep. Судя по всему, нужный PID — это 2194277.

Теперь, когда у вас есть PID вашего юнита, убейте его процесс:

# kill 2194277

Зайдите в бота. Кажется, он упал 😦

6. Автозапуск на случай сбоя

Теперь стало понятно, что в случае сбоя ваша программа просто упадёт. Бот не будет работать, пока кто-нибудь не зайдёт на сервер и не запустит его заново. Это совсем не дело, почему он не может перезапуститься сам?..

Надо сказать юниту: “Если файл перестал работать, то файл нужно перезапустить”. Это довольно просто поправить:

[Service]
ExecStart=/opt/echobot-example/venv/bin/python3 /opt/echobot-example/bot.py
Restart=always

[Install]
WantedBy=multi-user.target

Настройка Restart=always значит “перезапускай юнит, если он не работает”. Эта настройка хороша только для сайтов, чатботов и прочих программ, которые никогда не останавливаются. Если же вам нужно создать юнит с обычной программой, которая однажды закончится, то вам больше подойдёт Restart=on-abort. Тогда systemd перезапустит юнит только в том случае, если он упал с ошибкой, а если он просто завершился, программа закончилась, то systemd оставит юнит в покое.

Теперь, когда вы поправили файл юнита, перезапустите его в systemd:

# systemctl start echobot-example
Warning: The unit file, source configuration file or drop-ins of echobot-example.service changed on disk. Run 'systemctl daemon-reload' to reload units.

Авторский перевод: Внимание: юнит-файл echobot-example.service был изменен. Запустите команду systemctl daemon-reload, чтобы перезагрузить ваш файл.

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

Прим. автора

Очень мило со стороны systemd просто подсказать вам следующую команду: systemctl daemon-reload. Вы можете узнать что она делает на SO, но если вкратце, то это “мягкая перезагрузка”. При обычной перезагрузке systemd полностью останавливает юнит и запускает его заново. При “мягкой” же перезагрузке он не останавливает юнит, а пытается подключить к нему изменения прямо “на ходу”. Например, чтобы добавить настройку Restart=always, на самом деле юнит перезапускать не надо. systemd просто сообщит процессу юнита, что если что-то случится, то стоит перезапуститься.

Мы и до этого изменяли в файл, но ни разу не запускали эту команду. В те разы мы полностью перезагружали сервер, поэтому systemd тоже перезапускался и запускал уже обновленный файл.

Запустите команду, которую просил systemd:

# systemctl daemon-reload

Повторите запуск юнита:

# systemctl start echobot-example

Ещё раз найдите PID вашего процесса (PID поменялся!) и прервите работу файла:

# ps -aux | grep echobot-example
# kill 2194285

Проверьте бота. Он должен работать. Круто! Теперь бот работает во всех трёх случаях: после закрытия терминала, после перезагрузки сервера и даже после сбоя в демоне.

7. Что делать, если всё плохо

В этом туториале вы работали с простой программой, где и поломаться-то было почти нечему. Но всё бывает куда сложнее. Тут и там вылазят баги, которых не ждёшь. А где посмотреть трейсбек? Раньше он был в консоли, но теперь-то вашу программу запускает systemd!

Systemd всегда пишет логи. Их можно посмотреть в утилите journalctl. Загляните, что там происходит: image

Это все логи. Прямо все-все-все. На продакшн-сервере их будет так много, что ориентироваться в них не получится. Для этого логам добавили фильтры:

  • journalctl -b — записи с последнего запуска системы.
  • journalctl -u echobot-example — записи по юниту echobot-example
  • journalctl -f -u echobot-example — история работы по юниту в режиме реального времени. Периодически на экране будут появляться новые записи.

Попробуйте отсортированный вывод, он гораздо полезнее:

Читать дальше

  1. SystemD Target. Группируем unit’ы
  2. Туториал от 4te.me
  3. Туториал от DigitalOcean
  4. Статья на Хабрахабре
  5. Документация systemd
  6. How to enable a virtualenv in a systemd service unit?

Попробуйте бесплатные уроки по Python

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

Переходите на страницу учебных модулей «Девмана» и выбирайте тему.