Pull to refresh

Несколько советов по организации Python-приложения на сервере

Reading time6 min
Views31K


В этой статье я хочу поделиться несколькими удобными способами организации вашего проекта на рабочем (даже продакшен) сервере.


Я работаю, в основном, с Python/Django стеком, поэтому все примеры будут, в первую очередь, применительно к этому набору. Также ключевые технологии: Ubuntu (17.10), Python3 (3.6).


Содержание:


  • Логи (logrotate)
  • Демоны (systemd)
  • локальные настройки

Предполагается что вы делаете все грамотно, приложение хранится в репозитории, деплоится в отдельную папку на сервере, используется, например, virtualenv. Для запуска используется отдельно созданный юзер, который имеет достаточно прав, но не слишком много (например не имеет sudo и не разрешен логин по ssh).


Для начала я повторю прописные истины, что все что хранится в репозитории — это только чистый код и статичные данные. Все настройки содержат только дефолты, никаких паролей и ключей, даже в неиспользуемых файлах.


Даже на рабочем компьютере (ноутбуке) где вы пишете код у вас в папке проекта не должно быть ничего что бы вы не могли закачать на продакшен. Имеется в виду порочная практика использования файлика "local_settings.py" в папке settings внутри проекта (как вариант — development_settings.py). Я разберу этот пример ниже.


Логи


Наверняка вы используете логирование. Встроенный модуль logging очень хорош, но не всегда стоит изощряться и использовать его для всего на свете.


Например, ротация логов. В интернете попадаются сложные и изощренные способы начиная от стандартного RotatingFileHandler и заканчивая написанием собственного демона на сокетах для записи логов из нескольких источников. Проблемы начинаются из-за желания делать все на "чистом Python". Это глупо и неэффективно, зато приносит кучу возможных мест возникновения ошибок.


Используйте сервис logrotate. Ниже приводится простой конфиг для логов celery.


Стандартными средствами пишем файлик /var/log/myproject/celery.log, ежедневно он кладется в папку /var/log/myproject/archive/ и к имени добавляется суффикс предыдущего дня.


/var/log/myproject/celery.log {
    size 1
    su myuser myuser
    copytruncate
    create
    rotate 10
    missingok
    postrotate
        timeext=`date -d '1 day ago' "+%Y-%m-%d"`  # daily
#        timeext=$(date +%Y-%m-%d_%H)  # hourly
        mv /var/log/myproject/celery.log.1 /var/log/myproject/archive/celery_$timeext.log
    endscript
}

Если у вас лог пишется очень быстро и вы хотите его ротировать каждый час, то в конфиге перекомментируйте строчки "daily" и "hourly". Также нужно настроить logrotate чтобы он запускался каждый час (по умолчанию обычно ежедневно). Выполните в bash:


sudo cp /etc/cron.daily/logrotate /etc/cron.hourly/logrotate 
sudo sed -i -r "s/^[[:digit:]]*( .+cron.hourly)/0\1/" /etc/crontab

Конфиг (файлик myservice) надо положить в папку logrotate


sudo cp config/logrotate/myservice /etc/logrotate.d/myservice

важные моменты:


  • конфиг надо именно скопировать, симлинки работать не будут
  • в принципе, конфиг почти взят из доки по logrotate, но очень важно поставить copytruncate директиву
  • (добавлено из комментария rusnasonov ) logrotate туповат и использует простейшую систему ротирования, без буферов. Ротирование проходит в два шага — сначала копируется старый файл, а потом он обрезается на старом месте. Это может привести к потере логов, которые были записаны в промежутке (отражено в документации)

copytruncate важна по той причине, что при ротировании файл не будет закрыт. Поскольку мы ротируем файл на "живой" системе, файл открыт и сервисы, которые в него пишут, делают это по какому-то файловому дескриптору. Если вы просто переместите файл и создадите новый пустой, то он не будет использоваться. copytruncate говорит что нужно скопировать содержимое, а потом очистить файл, но не закрывать.


Сервисы


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


  • запускаем screen/tmux и внутри запускаем в интерактивном режиме скрипт
  • режим демона типа "-D" для gunicorn или celery
  • supervisord
  • init.d скрипт
  • Docker

Все они имеют свои плюсы, но, на мой взгляд, они имеют еще больше минусов.


Я не буду здесь рассматривать Docker. Во-первых, у меня не так много опыта работы с ним. А во-вторых, если вы используете контейнеры, то вам и остальные советы из этой статьи не очень нужны. Там подход уже другой.


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


В Ubuntu начиная с версии 15.04 по-умолчанию поставляется systemd для управления сервисами (и не только).


systemd очень удобен тем, что самостоятельно делает все правильно:


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

Конечно, если у вас нет systemd, то можно смотреть в сторону supervisord, но у меня довольно большая нелюбовь к этому инструменту и я избегаю его использовать.
Я надеюсь не будет людей, кто будет сомневаться что при наличии systemd использовать supervisord вредно.


Ниже я приведу пример конфига для запуска.


Запуск gunicorn (проксируется через локальный nginx, но это здесь неважно).


[Unit]
Description=My Web service
Documentation=
StartLimitIntervalSec=11

[Service]
Type=simple
Environment=DJANGO_SETTINGS_MODULE=myservice.settings.production
ExecStart=/opt/venv/bin/python3 -W ignore /opt/venv/bin/gunicorn -c /opt/myservice/config/gunicorn/gunicorn.conf.py --chdir /opt/myservice myservice.wsgi:application
Restart=always
RestartSec=2
StartLimitBurst=5
User=myuser
Group=myuser

ExecStop=/bin/kill -s TERM $MAINPID

WorkingDirectory=/opt/myservice
ReadWriteDirectories=/opt/myservice

[Install]
WantedBy=multi-user.target
Alias=my-web.service

Здесь важно не использовать режим демонизации. Мы запускаем gunicorn обычным процессом, а демонизирует его сам systemd, он же и следит за перезапуском при падениях.


Обратите внимание, что мы используем путь к python и gunicorn относительно virtualenv-папки.


Для celery все будет таким же, но строку запуска я рекомендую такой (пути и значения поставьте свои):


ExecStart=/opt/venv/bin/celery worker -A myservice.settings.celery_settings -Ofair --concurrency=3 --queues=celery --logfile=/var/log/myservice/celery.log --max-tasks-per-child 1 --pidfile=/tmp/celery_myservice.pid -n main.%h -l INFO -B

Стоит обратить внимание на параметры для перезапуска:


StartLimitIntervalSec=11
RestartSec=2
StartLimitBurst=5

Вкратце это означает следующее: если сервис упал, то запусти его снова через 2 секунды, но не больше 5 раз за 11 секунд. Важно понимать, что если значение в StartLimitIntervalSec будет, например, 9 секунд, то в случае если сервис остановится 5 раз подряд (сразу после запуска), то после пятого падения systemd сдастся и не будет его больше поднимать (2 * 5). Значение 11 выбрано именно с тем, чтобы исключить такой вариает. Если, например, у вас был сетевой сбой на 15 секунд и приложение падает сразу после старта (без таймаута), то пусть уж лучше оно долбит до победного, чем просто останавливается.


Чтобы установить этот конфиг в систему, можно просто сделать симлинк из рабочей папки:


~~sudo ln -s /opt/myservice/config/systemd/*.service /etc/systemd/system/~~
sudo systemctl daemon-reload

Однако, с симлинками надо быть осторожными — если у вас проект лежит не на системном диске, то есть вероятность что он может монтироваться после старта сервисов (например, сетевой диск или memory-mapped). В этом случае он просто не запустится. Здесь вам придется гуглить как правильно настроить зависимости, да и вообще конфиг тогда лучше скопировать в папку systemd.
Update: после замечания andreymal я думаю что будет правильнее копировать конфиги в папку и ставить им правильные права:


sudo chown root: /opt/myservice/config/systemd/*.service
sudo chmod 770 /opt/myservice/config/systemd/*.service
sudo cp /opt/myservice/config/systemd/*.service /etc/systemd/system/
sudo systemctl daemon-reload

Еще советую отключить вывод в консоль, иначе все будет попадать в syslog.


Когда у вас все компоненты заведены в systemd, то использование каждого из них сводится к:


sudo systemctl stop my-web.service
sudo systemctl stop my-celery.service
sudo systemctl start my-web.service
sudo systemctl start my-celery.service

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


bash manage.sh migrate
bash manage.sh start

Для удаленной отладки через консоль (запустит shell_plus из django-extensions):


bash manage.sh debug

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


Локальные настройки


Это тема для холивара и я хочу заметить, что этот параграф — исключительно моя точка зрения, которая базируется на моем опыте. Делайте как хотите, я просто советую.


Суть проблемы в том, что как ни пиши приложение, ему, скорее всего, придется иметь дело с данными, которые где-то хранятся. Список юзеров, путь к ключам, пароли, путь к базе, и тп… Все эти настройки хранить в репозитории нельзя. Кто-то хранит, но это порочная практика. Небезопасно и негибко.


Какие есть самые частые способы хранения таких настроек и какие проблемы с ними:


  • файлик local_settings.py, который хранится в папке проекта рядом с дефолтными setting.py
    Проблема: можно случайно закоммитить файл, можно его затереть при копировании/обновлении папки (rsync или из архива)
  • переменные окружения. Тоже не очень безопасно, не очень удобно (при soft-reload например)
  • отдельный файл вне папки проекта

Я рекомендую именно этот способ. Обычно я для проекта создаю yaml-файл в папке "/usr/local/etc/". У меня написан небольшой модуль, который используя магию хаки загружает переменные из файлика в locals() или globals() импортирующего модуля.


Используется очень просто. Где-то в глубинах settings.py для Django (лучше ближе к концу) достаточно вызвать:


import_settings("/usr/local/etc/myservice.yaml")

И все содержимое будет замешано в глобальные settings. У меня используется merge для списков и словарей, это может быть не всем удобно. Важно помнить, что Django импортирует только UPPERCASE константы, то есть в файлике настройки первого уровня у вас сразу должны быть в верхнем регистре.


That's all folks!


Остальное обсудим в комментариях.


Tags:
Hubs:
Total votes 23: ↑19 and ↓4+15
Comments79

Articles