codefest 2014. Каплуновский Б. — Использование асинхронного...
TRANSCRIPT
Использование асинхронного I/O для снижения потребления ресурсов в
движке aviasalesКаплуновский Борис
aviasales.ru
facebook.com/boris.kaplounovsky@bskaplou
Agenda
● Скриптовые языки и ресурсы● Асинхронная модель выполнения● Оптимизации и отзывчивость● Странности Tornado● Странности Python● Tornado/Python в production● И ещё пару советов по повышению
производительности...
Что делает движок aviasales
Модель памяти нативной программы
process one
stack
data
process two
code
libdl
libc
data
stack
● Одна и та-же память с исполняемым кодом используется всеми процессами
● Разделяемые библиотеки грузятся в память один раз
● Не разделяются другими процессами только сегменты данных и стек
Модель памяти скрипта
runtime code
libdllibc
stackstack
data data
script libs script libs
● Нативные код и библиотеки разделяются
● AST и байткод скриптовых библиотек хранятся в сегменте данных и поэтому НЕ разделяются
● Скриптовый код не так компактен как нативный и обычно занимает в разы больше памяти
code code
Сферический CGI Сервер в вакууме
stack
data
native code
libc
libdl
datadatadatadata data
stack stackstack stack stack stack
Скриптовый CGI Сервер
stackstack
data data
script libs script libs
code code
stackstack
data data
script libs script libs
code code
stackstack
data data
script libs script libs
code code
native code
libclibdl
Оптимизации над CGI
● fastcgi - Не порождаем отдельный процесс для каждого запроса – экономим процессорного времени на загрузку скриптов j2ee/rails/etc
● process pool - запуск и инициализация процесса до прихода запроса – снижение времени отклика
● master -Запуск родительского процесса загружающего код и делающего инициализацию. Родительский процесс порождает обработчиков клонируя себя. Процесс обрабатывающий запрос уже имеет в памяти всё необходимое. unicorn/dalvik/etc
Copy on write
● После вызова fork() состояние памяти и родителя и потомка одинаковые
● Делать полную копию адресного пространства при fork() расточительно
● В момент вызова fork() страницы данных родителя и потомка метятся как read-only
parent childcode
libdl
libc
data
stack
fork()
Copy on write
● Как только один из процесс записывает данные – операционная система делает личную копию страницы в пространстве процесса
● Страницы памяти в которые не пишут могут разделяться вечно
parent child
data
stack stack
data
clone pages
master process & copy on write● После старта мастер процесс
грузит библиотеки и подготавливает всё для исполнения скрипта
● По мере необходимости мастер порождает рабочие процессы клонируя себя
● Так как в мастере уже были загружены все библиотеки дочерний процесс готов к работе мгновенно
● COW позволяет не создавать собственную копию кода в памяти
master child
stackstack
data data
script libs
code
native code
libclibdl
Copy on writeНЕ РАБОТАЕТ!
COW не работает потому что
● GC скриптовой среды меняют данные неиспользуемых обьектов в ходе своей работы
● Скриптовые языки со счётчиками ссылок модифицируют счётчики ссылок при создании новой ссылки на обьект, даже если сам обьект неизменен
master child
stackstack
data data
scriptlibs
code
native codelibclibdl
code
scriptlibs
COW не просто заставить работать
● В ruby 2.0 обещали сделать cow friendly gc. Не получилось!
● COW работает у google в dalvik, но для этого им пришлось заменить jvm на dalvik
master child
stackstack
data data
scriptlibs
code
native codelibclibdl
code
scriptlibs
Типичное web приложение
Значительную часть времени веб приложения ждут ответов внешних сервисов таких как
– SQL сервер
– Внешний API
– Файловый ввод вывод
Всё это время ничего не происходит!
Но память занята...
запрос ответ
logic SQL logicAPI
Rails приложение aviasales
● Ожидание ответа внешних API до 30 секунд
● Работа с SQL ~1 секунда
● Потребляемая память ~300mb (одним процессом)
● Разделяемая память ~4mb (код интерпретатора)
● ~300 одновременных поисков
87GB RAM/6 серверов
И вся эта память простаивала!
запрос ответ
logic SQL logicAPI
Синхронная модель VS Асинхронная модель
cgi worker
stack
data
code
scriptlibs
cgi worker
stack
data
code
scriptlibs
async worker
stack
scriptlibs
code
native code
libc
threaddata
threaddata
threaddata
cgi worker
stack
data
code
scriptlibs
native code
libc
Асинхронная модель
Минусы● Кооперативная многозадачность● Если падает процесс падают все потоки● Не для всего есть библиотеки● Отсутствие изоляции● Примитивный планировщик● Нет готовых решений
Асинхронная модель
Плюсы● Эффективное использование памяти● Эффективное использование памяти● Эффективное использование памяти● Эффективное использование памяти● Эффективное использование памяти
Почему Python
– Большое и доброе community– Обилие библиотек– Tornado живёт в python– Реклама google– Хотелось попробовать
Почему Tornado
– Низкий порог вхождения– Асинхронный– @gen.coroutine – отличная альтернатива
колбекам– Казался зрелым
Приложение на python/tornado
● Один процесс:
– занимает 267mb памяти
– из них 162mb разделяемой
– обрабатывает до 10 одновременных запросов
– больше не ждёт SQL сервер, все данные в адресном пространстве процесса
– ~ 500 одновременных исходящих соединений
– 2 сервера/8GB памяти
async worker
stack
scriptlibs
code
native codelibc
data data data
При работе с tornado помни!
● Как только вы начинаете использовать синхронный IO всё останавливается
● Переключение контекста происходит ТОЛЬКО на I/O и yield внутри @gen.coroutine
● Неделимый кусок кода не должен исполняться больше XXXms (мы выбрали 100ms)
При работе с tornado помни!
● Декоратор @gen.coroutine не бесплатен
● Tornado/Python приложение может умирать
● У Tornado/Python приложения может течь память
● Только профилировщик точно покажет кто ест CPU
● Python используется как клей для нативных библиотек, сложные алгоритмы на python реализовывать не надо
Странности Tornado
● Из коробки нет способа остановить приложение без обрыва соединений
● Есть рецепты костылей на StackOverflow
● Но этого мало – пришлось изобретать ещё костылей
Резольвер
www.aviasales.ru → 194.87.255.204● “Родные” резольверы операционных систем
синхронны● Для асинхронный модели исполнения нужен
асинхронный резольвер
Странности Tornado – Резольвер● tornado.netutil.BlockingResolver
– Используется по умолчанию– Использует синхронный getaddrinfo– Не кеширует результаты– Обращение к DNS при каждом HTTP
запросе– Пока DNS сервер не ответил всё стоит
Странности Tornado – Резольвер
● tornado.netutil.ThreadedResolver
– Вызывает getaddrinfo в отдельном потоке python
– Overhead на потоки: память, cpu, GIL– Работает но выглядит как костыль
Странности Tornado – Резольвер
● Мы написали простой асинхронный резольвер для Tornado IOLoop
– Только TCP– Только записи A и CNAME– Кеширование ответов DNS по
TTL– Большинство
преобразований делается без системных вызовов
Странности Tornado – HTTPClient
● HTTPClient создаёт не больше 10 исходящих соединений по умолчанию
● HTTPClient умеет стримить ответ сервера только если ответ chunked
Странности Tornado
● Документация зачастую избегает описывать узкие места
● Будьте готовы читать исходный код tornado чтобы понять поведение системы
Странности Python
● Сторонние библиотеки с нативным кодом текут и валят приложение через одну
● Найти утечку памяти в нативном коде крайне сложно
● Встроенная библиотеку xml.etree может приводить к SEGFAULT, мы используем lxml
● Сложные регулярные выражения могут остановить приложение busy-wait
tornado/python в productionMONIT
– Убивает рабочие процессы если они выедают CPU
– Убивает рабочие процессы если они превысили лимит по памяти
– Стартует рабочие процессы если те умерли сами или были убиты
– Простой и удобный web интерфейс
tornado/python в productionHAPROXY
– Раскидывает приходящие запросы по доступным рабочим процессам
– Балансирует нагрузку отправляя запросы к процессам с наименьшим количеством активных соединений
– Адски быстрый и простой
– Простой и удобный веб интерфейс
tornado/python в productionBENCHMARKS
– Тотальное логирование времени выполения участков кода
– Визуализация бенчмарков на видном месте
– Немедленная реакция на аномалии в скорости ответов сервера
Что делать с ожиданием ответов SQL сервера
Удалённый сервер DB
request
response
worker remote db
parse request
load value
build response
recv(syscall)
send(syscall)
Файловое key-value хранилище
● Содержимое файла должно быть смаплено в адресное пространство процесса mmap
● Рабочий обьём должен умещаться в оперативной памяти
● База должна позволять нескольким процессам одновременно читать данные без блокировок
● Мы используем kyoto cabinet и он прекрасен
Быстрее чем redis и memcached
worker
load value
Плюсы● Не нужен внешний
сервер● Непревзойдённая
скорость● Не нужно переключать
контекст и делать syscall
● Высокая отказоустойчивость
Быстрее чем redis и memcached
worker
load value
Минусы● Медленный update
данных● Избыточность при
работе в кластере● Работает только для
небольшого кол-ва данных
Q&Afacebook.com/boris.kaplounovsky
@bskaplou
Используйте потоковую обработку для разбора XML
● Опция streaming_callback у AsyncHTTPClient fetch позволяет получать данные по мере поступления
● Метод lxml.etree.XMLParser.feed позволяет парсить xml по кускам
● Если и это не помогает, делаем IOLoop.instance().add_timeout(time()) чтобы разбить поток исполнения
tornado/python в productionПриоритеты
● У разных запросов разные требования к скорости ответ
● Рабочие процессы привязываются к одной или нескольким группа приоритета
● Haproxy отправляет запросы в соответствующую группу рабочих процессов