CodeFest 2014. Каплуновский Б. — Использование асинхронного I/O для снижения потребления ресурсов в движке Aviasales.ru
1. Использование асинхронного I/O для
снижения потребления ресурсов в
движке aviasales
Каплуновский Борис
aviasales.ru
facebook.com/boris.kaplounovsky
@bskaplou
2. Agenda
● Скриптовые языки и ресурсы
● Асинхронная модель выполнения
● Оптимизации и отзывчивость
● Странности Tornado
● Странности Python
● Tornado/Python в production
● И ещё пару советов по повышению
производительности...
4. Модель памяти нативной
программы
process one
stack
data
process two
code
libdl
libc
data
stack
● Одна и та-же память с
исполняемым кодом
используется всеми
процессами
● Разделяемые
библиотеки грузятся в
память один раз
● Не разделяются
другими процессами
только сегменты
данных и стек
5. Модель памяти скрипта
runtime code
libdl
libc
stackstack
data data
script libs script libs
● Нативные код и
библиотеки
разделяются
● AST и байткод
скриптовых библиотек
хранятся в сегменте
данных и поэтому НЕ
разделяются
● Скриптовый код не так
компактен как
нативный и обычно
занимает в разы
больше памяти
code code
6. Сферический CGI Сервер в
вакууме
stack
data
native code
libc
libdl
datadatadatadata data
stack stackstack stack stack stack
7. Скриптовый 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
libc
libdl
8. Оптимизации над CGI
● fastcgi - Не порождаем отдельный процесс для каждого
запроса – экономим процессорного времени на
загрузку скриптов j2ee/rails/etc
● process pool - запуск и инициализация процесса до
прихода запроса – снижение времени отклика
● master -Запуск родительского процесса загружающего
код и делающего инициализацию. Родительский
процесс порождает обработчиков клонируя себя.
Процесс обрабатывающий запрос уже имеет в памяти
всё необходимое. unicorn/dalvik/etc
9. Copy on write
● После вызова fork() состояние
памяти и родителя и потомка
одинаковые
● Делать полную копию
адресного пространства при
fork() расточительно
● В момент вызова fork()
страницы данных родителя и
потомка метятся как read-only
parent childcode
libdl
libc
data
stack
fork()
10. Copy on write
● Как только один из процесс
записывает данные –
операционная система делает
личную копию страницы в
пространстве процесса
● Страницы памяти в которые не
пишут могут разделяться вечно
parent child
data
stack stack
data
clone pages
11. master process & copy on write
● После старта мастер процесс
грузит библиотеки и
подготавливает всё для
исполнения скрипта
● По мере необходимости мастер
порождает рабочие процессы
клонируя себя
● Так как в мастере уже были
загружены все библиотеки
дочерний процесс готов к работе
мгновенно
● COW позволяет не создавать
собственную копию кода в памяти
master child
stackstack
data data
script libs
code
native code
libc
libdl
14. COW не работает потому что
● GC скриптовой среды
меняют данные
неиспользуемых обьектов в
ходе своей работы
● Скриптовые языки со
счётчиками ссылок
модифицируют счётчики
ссылок при создании новой
ссылки на обьект, даже
если сам обьект неизменен
master child
stackstack
data data
script
libs
code
native code
libc
libdl
code
script
libs
15. COW не
просто заставить работать
● В ruby 2.0 обещали сделать
cow friendly gc. Не
получилось!
● COW работает у google в
dalvik, но для этого им
пришлось заменить jvm на
dalvik
master child
stackstack
data data
script
libs
code
native code
libc
libdl
code
script
libs
16. Типичное web приложение
Значительную часть времени веб приложения
ждут ответов внешних сервисов таких как
– SQL сервер
– Внешний API
– Файловый ввод вывод
Всё это время ничего не происходит!
Но память занята...
запрос ответ
logic SQL logicAPI
17. Rails приложение aviasales
● Ожидание ответа внешних API до 30 секунд
● Работа с SQL ~1 секунда
● Потребляемая память ~300mb (одним процессом)
● Разделяемая память ~4mb (код интерпретатора)
● ~300 одновременных поисков
87GB RAM/6 серверов
И вся эта память простаивала!
запрос ответ
logic SQL logicAPI
18. Синхронная модель VS
Асинхронная модель
cgi worker
stack
data
code
script
libs
cgi worker
stack
data
code
script
libs
async worker
stack
script
libs
code
native code
libc
thread
data
thread
data
thread
data
cgi worker
stack
data
code
script
libs
native code
libc
19. Асинхронная модель
Минусы
● Кооперативная многозадачность
● Если падает процесс падают все потоки
● Не для всего есть библиотеки
● Отсутствие изоляции
● Примитивный планировщик
● Нет готовых решений
20. Асинхронная модель
Плюсы
● Эффективное использование памяти
● Эффективное использование памяти
● Эффективное использование памяти
● Эффективное использование памяти
● Эффективное использование памяти
21. Почему Python
– Большое и доброе community
– Обилие библиотек
– Tornado живёт в python
– Реклама google
– Хотелось попробовать
23. Приложение на python/tornado
● Один процесс:
– занимает 267mb памяти
– из них 162mb разделяемой
– обрабатывает до 10
одновременных запросов
– больше не ждёт SQL сервер, все
данные в адресном пространстве
процесса
– ~ 500 одновременных исходящих
соединений
– 2 сервера/8GB памяти
async worker
stack
script
libs
code
native code
libc
data data data
24. При работе с tornado помни!
● Как только вы начинаете использовать синхронный IO
всё останавливается
● Переключение контекста происходит ТОЛЬКО на I/O и
yield внутри @gen.coroutine
● Неделимый кусок кода не должен исполняться больше
XXXms (мы выбрали 100ms)
25. При работе с tornado помни!
● Декоратор @gen.coroutine не бесплатен
● Tornado/Python приложение может умирать
● У Tornado/Python приложения может течь память
● Только профилировщик точно покажет кто ест CPU
● Python используется как клей для нативных библиотек,
сложные алгоритмы на python реализовывать не надо
26. Странности Tornado
● Из коробки нет способа остановить приложение без
обрыва соединений
● Есть рецепты костылей на StackOverflow
● Но этого мало – пришлось изобретать ещё костылей
28. Странности Tornado – Резольвер
● tornado.netutil.BlockingResolver
– Используется по умолчанию
– Использует синхронный getaddrinfo
– Не кеширует результаты
– Обращение к DNS при каждом HTTP
запросе
– Пока DNS сервер не ответил всё стоит
29. Странности Tornado – Резольвер
● tornado.netutil.ThreadedResolver
– Вызывает getaddrinfo в отдельном потоке
python
– Overhead на потоки: память, cpu, GIL
– Работает но выглядит как костыль
30. Странности Tornado – Резольвер
● Мы написали простой
асинхронный резольвер для
Tornado IOLoop
– Только TCP
– Только записи A и CNAME
– Кеширование ответов DNS по
TTL
– Большинство
преобразований делается без
системных вызовов
31. Странности Tornado – HTTPClient
● HTTPClient создаёт не больше 10 исходящих
соединений по умолчанию
● HTTPClient умеет стримить ответ сервера только
если ответ chunked
32. Странности Tornado
● Документация зачастую избегает описывать узкие
места
● Будьте готовы читать исходный код tornado чтобы
понять поведение системы
33. Странности Python
● Сторонние библиотеки с нативным кодом текут и валят
приложение через одну
● Найти утечку памяти в нативном коде крайне сложно
● Встроенная библиотеку xml.etree может приводить к
SEGFAULT, мы используем lxml
● Сложные регулярные выражения могут остановить
приложение busy-wait
34. tornado/python в production
MONIT
– Убивает рабочие процессы если они
выедают CPU
– Убивает рабочие процессы если они
превысили лимит по памяти
– Стартует рабочие процессы если те
умерли сами или были убиты
– Простой и удобный web интерфейс
35. tornado/python в production
HAPROXY
– Раскидывает приходящие запросы по
доступным рабочим процессам
– Балансирует нагрузку отправляя
запросы к процессам с наименьшим
количеством активных соединений
– Адски быстрый и простой
– Простой и удобный веб интерфейс
36. tornado/python в production
BENCHMARKS
– Тотальное логирование времени выполения участков
кода
– Визуализация бенчмарков на видном месте
– Немедленная реакция на аномалии в скорости
ответов сервера
39. Файловое key-value хранилище
● Содержимое файла должно быть смаплено в
адресное пространство процесса mmap
● Рабочий обьём должен умещаться в оперативной
памяти
● База должна позволять нескольким процессам
одновременно читать данные без блокировок
● Мы используем kyoto cabinet и он прекрасен
40. Быстрее чем redis и memcached
worker
load value
Плюсы
● Не нужен внешний
сервер
● Непревзойдённая
скорость
● Не нужно переключать
контекст и делать
syscall
● Высокая
отказоустойчивость
41. Быстрее чем redis и memcached
worker
load value
Минусы
● Медленный update
данных
● Избыточность при
работе в кластере
● Работает только для
небольшого кол-ва
данных
43. Используйте потоковую обработку
для разбора XML
● Опция streaming_callback у AsyncHTTPClient
fetch позволяет получать данные по мере
поступления
● Метод lxml.etree.XMLParser.feed позволяет
парсить xml по кускам
● Если и это не помогает, делаем
IOLoop.instance().add_timeout(time()) чтобы
разбить поток исполнения
44. tornado/python в production
Приоритеты
● У разных запросов разные требования к скорости ответ
● Рабочие процессы привязываются к одной или
нескольким группа приоритета
● Haproxy отправляет запросы в соответствующую группу
рабочих процессов