python meetup
TRANSCRIPT
Оптимизация работы веб сервера с
базой данных на примере Django
Одесса Python meetup
Кто я?
Евгений Гетманский
Разработка веб сайтов на протяжении более пяти лет
Работаю тимлидом в Steelkiwi inc.
Часть 1
Что такое оптимизация и зачем она нужна?
Требования к проектам
Требования к проектам
Функциональные требования
Требования к проектам
Функциональные требования
Соблюдение стандартов
Требования к проектам
Функциональные требования
Соблюдение стандартов
Безопасность
Требования к проектам
Функциональные требования
Соблюдение стандартов
Безопасность
Скорость работы
504 Gateway Timeout Error
Увеличение таймаута
Асинхронная обработка данных
Оптимизация Python кода
Оптимизация запросов к базе данных
Часть 2
Оптимизация работы с базой данных
Когда нужно
оптимизировать
Бывает ли преждевременная оптимизация
Когда нужно
оптимизировать
Бывает ли преждевременная оптимизация
Анализ требований и проектирование до старта проекта
Когда нужно
оптимизировать
Бывает ли преждевременная оптимизация
Анализ требований и проектирование до старта проекта
Во время написания кода
Когда нужно
оптимизировать
Бывает ли преждевременная оптимизация
Анализ требований и проектирование до старта проекта
Во время написания кода
Когда уже не работает
Приемы и примеры кода
Как в Django писать оптимизированный код
Немного кодаclass Category(models.Model):
name = models.CharField(max_length=50)
class Product(models.Model):
name = models.CharField(max_length=50)
category = models.ForeignKey(Category,
related_name='products')
brand = models.ForeignKey(Brand, related_name='products')
class CartItem(models.Model):
product = models.ForeignKey(Product)
class Brand(models.Model):
title = models.CharField(max_length=50)
Использование ID
связанного объекта
используйте
product.category_id
вместо
product.category.id
Использование ID связанного
объекта
for item in CartItem.objects.filter(
product__category_id__in={
p.category.id for p in products}):
# Do something
Использование ID связанного
объекта
for item in CartItem.objects.filter(
product__category_id__in={
p.category_id for p in products}):
# Do something
Бывает и такое
if parent.invoices.filter(status=Invoice.PAID).count():
return
parent.invoices.filter(status=Invoice.PAID).order_by(
'payed_period_end').last()
return None
Проверки
if queryset
len(queryset)
count
exists
Используем select_related
for product in Product.objects.select_related('category'):
data.append(
{'product': product.name,
'category': product.category.name})
Используем select_related
Что это и зачем нужно?
Что происходит когда выполняется?
Как это помогает оптимизировать работу сервера
Используем prefetch_related
for category in Category.objects.prefetch_related('products'):
data.append({'category': category,
'products': category.products.all()})
Используем prefetch_related
Что это и зачем нужно?
Что происходит когда выполняется?
Как это помогает оптимизировать работу сервера
А что можно добавить?
Используем prefetch_related
prefetch = Prefetch('products',
queryset=Product.objects.select_related('brand'))
for category in Category.objects.only('name').prefetch_related(prefetch):
data.append({'category': category.name,
'products': [(product.name, product.brand.name) for
product in category.products.all()]})
Неужели все так хорошо или
есть что то еще?
Join на таблице в 300000 записей
Иногда лучше проверить
Django debug toolbar и debug panel
Создание записей
Все те же грабли с циклами
А что же делать?
Используем bulk_create
parsed_data = parse_data(data)
batch_size = 100
items = []
for row in parsed_data:
items.append(Item(**row))
if len(items) >= batch_size:
Item.objects.bulk_create(items)
items = []
if items:
Item.objects.bulk_create(items)
Редактирование записей
Вы не поверите, но тут опять все хорошо пока нет циклов
bulk_update?
Нет, но есть кое что получше - F object
Вот это чаще всего не хорошо
for product in category.products.all():
product.priority += 1
product.save()
А это уже неплохо
category.products.update(field=F('priority')+1)
Читайте документацию
https://docs.djangoproject.com/en/1.10/topics/db/optimization/
Часть 3
Немного примеров из жизни
Пересчет рейтинга
Приложение по работе с данными о авиакомпаниях, самолетах и
поиске маршрутов
Решения
увеличить таймаут
увеличить таймаут
прикручивать асинхронную обработку?
что делать???
Загрузка данных из CSV
много записей и много проверок
А теперь можно задавать вопросы
Немного ссылок
https://www.facebook.com/evgeniy.getmansky
http://steelkiwi.com
https://github.com/steelkiwi/meetup.demo
Шаблон проекта.
Использование Vagrant, VirtualEnv и
Ansible provisioner. Зачем это
необходимо?
О себе
1. Степанов Александр
2. Python Team Lead in SteelKiwi Inc.
3. Участие в 15+ проектах
4. AngularJS, BackBoneJS
5. Ionic Framework (не вздумайте)
Что мы хотели бы получить?
1. Скорость инициализации
2. Удобен при командной разработке
3. Прост с точки зрения деплоя
4. Легко масштабируется
5. Приятная структура
6. Имеет изолированное окружение
Какие мы знаем шаблоны?
- Default django template
- Two Scoops of Django
- Cookiecutter Django
Default django template
$ django-admin.py startproject myproject
myproject/manage.pymyproject/
__init__.pysettings.pyurls.pywsgi.py
myapp/
$ python manage.py startapp myapp
Django two scoops
$ django-admin.py startproject myproject2 --template=https://github.com/twoscoops/django-twoscoops-project/archive/develop.zip
myproject_2/docs/requirements/myproject_2/
manage.pystatic/templates/myproject_2/
__init__.pysettings/__init__.pylocal.pybase.pyproduction.pytest.py
urls.pywsgi.py
Сookiecutter django
$ pip install "cookiecutter>=1.4.0"
$ cookiecutter https://github.com/pydanny/cookiecutter-django
myproject_3/manage.pyconfig/
__init__.pysettings/
__init__.pylocal.pycommon.pyproduction.pytest.py
urls.pywsgi.py
requirements/myproject_3/static/templates/myapp/
Сравнение структуры
myproject_3/manage.pyconfig/
__init__.pysettings/
__init__.pylocal.pycommon.pyproduction.pytest.py
urls.pywsgi.py
requirements/base.txtlocal.txtproduction.txttest.txt
myproject_3/static/templates/myapp/
myproject_2/docs/requirements/
base.txtlocal.txtproduction.txttest.txt
myproject_2/manage.py
static/templates/myproject_2/
__init__.pysettings/
__init__.pylocal.pybase.pyproduction.pyTest.py
urls.pywsgi.py
myproject/manage.py
myproject/__init__.pysettings.pyurls.pyWsgi.py
myapp/
Default Django Two Scoops of Django Cookiecutter Django
Что мы сделали?
1. Только необходимые настройки
2. Изменили скрипт инициализации проекта
3. Разные database backend
4. Единый файл настроек
5. Переменные окружения по умолчанию
Скрипт инициализации проекта
{
"project_name":
"project_name",
"repo_name": "project_name",
"python_version": ["2", "3"],
"use_postgresql": "y",
"use_postgis": "n",
"use_mysql": "n",
"use_celery": "n",
"use_allauth": "n",
"use_redis": "n",
"use_rabbitmq": "n",
"use_supervisor": "n",
"use_apache": "n",
"use_nginx": "n"
}
Единый файл settings.py
env = environ.Env(
DJANGO_DEBUG=(bool, False),
DJANGO_SECRET_KEY=(str, 'CHANGEME!!!8l=tkv+t@)ilij-w=hogt8qgs-zf(i3or-k7w_d(7_1=&l1##l'),
DJANGO_ADMINS=(list, []),
DJANGO_ALLOWED_HOSTS=(list, []),
DJANGO_STATIC_ROOT=(str, str(APPS_DIR('staticfiles'))),
DJANGO_MEDIA_ROOT=(str, str(APPS_DIR('media'))),
DJANGO_DATABASE_URL=(str, 'postgres:///myproject4'),
DJANGO_EMAIL_URL=(environ.Env.email_url_config, 'consolemail://'),
DJANGO_DEFAULT_FROM_EMAIL=(str, '[email protected]'),
DJANGO_EMAIL_BACKEND=(str, 'django.core.mail.backends.smtp.EmailBackend'),
DJANGO_SERVER_EMAIL=(str, '[email protected]'),
DJANGO_CELERY_BROKER_URL=(str, 'redis://localhost:6379/0'),
DJANGO_CELERY_BACKEND=(str, 'redis://localhost:6379/0'),
DJANGO_CELERY_ALWAYS_EAGER=(bool, False),
DJANGO_USE_DEBUG_TOOLBAR=(bool, False),
DJANGO_TEST_RUN=(bool, False),
)
DJANGO DATABASE URL
Engine URL
PostgreSQL postgres://USER:PASSWORD@HOST:PORT/NAME
PostGIS postgis://USER:PASSWORD@HOST:PORT/NAME
MySQL mysql://USER:PASSWORD@HOST:PORT/NAME
MySQL (GIS) mysqlgis://USER:PASSWORD@HOST:PORT/NAME
SQLite sqlite:///PATH
DJANGO EMAIL URL
SMTP: smtp://
SMTP+SSL: smtp+ssl://
SMTP+TLS: smtp+tls://
Console mail: consolemail://
File mail: filemail://
LocMem mail: memorymail://
Dummy mail: dummymail://
# Example
smtp+tls://user:SuperSecretPassword@smtp_server.com:587`
А как же локальная разработка и тесты?
if env.bool('DJANGO_USE_DEBUG_TOOLBAR'):
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware', )
INSTALLED_APPS += ('debug_toolbar', )
DEBUG_TOOLBAR_CONFIG = {
'DISABLE_PANELS': [
'debug_toolbar.panels.redirects.RedirectsPanel',
],
'SHOW_TEMPLATE_CONTEXT': True,
}
if env.bool('DJANGO_TEST_RUN'):
pass
Virtual environment
$ pip install virtualenv
$ cd my_project_folder
$ virtualenv venv
$ source venv/bin/activate
$ pip install requests
$ pip install -r requirements.txt
Версия Python$ virtualenv -p /usr/bin/python2.7 venv
Установка пакетов
Vagrant
Vagrant это cli для VirtalBox, VMWare, Amazon EC2, LXC
Преимущества
1. Установка утилит в изолированное окружение
2. Уникальное окружение в контексте проекта
3. Поддержка в любой операционной системеПочему не Docker
1. См. Пункт 3 преимуществ
2. Изолированное окружение, повторяющее реальную рабочую среду
Настройки Vagrant1. Используемый box
2. Маппинг портов и папок
3. Способ наполнения изолированного окружения
4. Используемые ресурсы#### Box
develop.vm.box = "develop"develop.vm.box_url = "http://vagrant.steel.kiwi/vagrant/develop/"
#### Networkdevelop.vm.network :forwarded_port, guest: 8000, host: 8000develop.vm.network :private_network, ip: '192.168.80.50'develop.vm.synced_folder ".", "/vagrant", type: "nfs"
#### ansible_provisiondevelop.vm.provision "ansible" do |ansible|ansible.verbose = "v"ansible.playbook = "ansible/vagrant.yml"end
#### system resourcesdevelop.vm.hostname = "develop"
develop.vm.provider "virtualbox" do |v|v.memory = 1024v.cpus = 1
Как наполнить окружение?
#!/bin/bash
echo "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main" >>
/etc/apt/sources.list.d/pgdg.list
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | \
apt-key add -
apt-get update
apt-get install -y postgresql-9.4 postgresql-contrib libgeos-dev
Ansible ubuntu/trusty64---
- name: Configure the PostgreSQL APT key
apt_key: url=https://www.postgresql.org/media/keys/ACCC4CF8.asc state=present
- name: Configure the PostgreSQL APT repositories
apt_repository: repo="deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main"
state=present
- name: Update PostgreSQL Cache
apt: update_cache=yes
- name: Install PostgreSQL
apt: name={{ item }} force=yes state=installed
with_items:
- postgresql-9.4
- postgresql-contrib-9.4
- python-psycopg2
tags: packages
Ansible steelkiwi.box---
- name: enable postgresql
service: >
name=postgresql
enabled=yes
runlevel=default
state=started
tags: postgresql
- name: Create database
postgresql_db: >
login_user={{ db_user }}
name={{ db_name }}
owner={{ db_user }}
encoding='UTF-8'
lc_collate='en_US.UTF-8'
lc_ctype='en_US.UTF-8'
state=present
become: yes
become_user: vagrant
tags: create_db
В чем же преимущество Ansible?
Всё работает через SSH;
Написан на Python;
YAML;
Хорошая, постоянно обновляемая документация;
Последовательное обновление узлов.
Variables---
project_name: myproject4
application_name: myproject4
# Database settings.
db_user: "vagrant"
db_name: "{{ application_name }}"
db_password: ""
# Application settings.
virtualenv_path: "/home/vagrant/venv"
project_path: "/vagrant"
requirements_file: "{{ project_path }}/requirements/local.txt"
django_settings_path: "{{ project_path }}/config"
django_settings: "config.settings"
#Python version - change if you needed
python_version: 3
Связь Variables c Vagrant playbook# vars/vagrant.ymlmysql: noapache: nonginx: nopostgresql: truerabbitmq: noredis: truesupervisor: noapp: true
# vagrant.yml
vars_files:- vars/vagrant.yml
roles:- { role: mysql, when: "mysql == true" }- { role: apache, when: "apache == true" }- { role: nginx, when: "nginx == true" }- { role: postgresql, when: "postgresql == true" }- { role: rabbitmq, when: "rabbitmq == true" }- { role: redis, when: "redis == true" }- { role: supervisor, when: "supervisor == true" }- { role: app, when: "app == true" }
Ansible & Django
---
- name: Install packages required by the Django app inside virtualenv
pip: >
virtualenv={{ virtualenv_path }}
requirements={{ requirements_file }}
- name: Run Django database migrations
django_manage: >
command=migrate
app_path="{{ project_path }}"
virtualenv="{{ virtualenv_path }}"
settings="{{ django_settings }}"
when: run_django_db_migrations is defined and run_django_db_migrations
become: yes
become_user: vagrant
tags: django.migrate
Старт нового проекта
$ cookiecutter https://github.com/steelkiwi/django-template.git
$ vagrant up
$ vagrant ssh
$ cd /vagrant
$ cp env.example ./config/.env
$ python manage.py runserver 0.0.0.0:8000
А если проект уже в разработке?
$ git clone https://github.com/steelkiwi/old_poject.git
$ vagrant up
$ vagrant ssh
$ cd /vagrant
$ cp env.example ./config/.env
$ python manage.py runserver 0.0.0.0:8000
Выводы
1. Получили легко настраиваемый и гибкий шаблон проекта
2. Добавили возможность использовать разные database backend and
python version
3. Каждый проект находится в изолированном окружение
4. Одинаковая среда разработки
5. Добавили удобный способ наполнения и разворачивания проекта.
Useful links
https://github.com/steelkiwi/django-template
https://www.vagrantup.com/
https://www.ansible.com/
https://github.com/kennethreitz/dj-database-url
https://github.com/joke2k/django-environ