django

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

  • вторник, 20 марта 2018 г. в 12:49:54
https://habrahabr.ru/post/351566/
  • Программирование
  • Python
  • Django



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


Я работаю, в основном, с 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 директиву

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.


Еще советую отключить вывод в консоль, иначе все будет попадать в 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!


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