Дизайн поискового движка
aviasales.ru
http://avs.io/highload2013
Каплуновский Борис
bk@aviasales.ru
@bskaplou
facebook.com/boris.kaplounovsky
Agenda
Задачи метопоисковика авиабилетов
●
Предыдущек решение и его недостатки
●
Требования к новой системе
●
DSL “Ясеня” Юниты и цепочки
●
Отказоустойчивость и базы данных
●
Организация кластера
●
Что мы делаем?

Получаем запрос от пользвателя
Посылаем 60 запросов по 2 килобайта
Получаем 40 ответов по ~1 одному мегабайту
20 000 запросов в час
13 Гигабайт в минуту
~6000 проданных билетов в сутки (15 x Boeing 747)
Конфигурирование
Разные наборы гейтов в зависимости от:
●
Геоположения пользователя
●
Пунктов вылета и назначения
●
Набора пассажиров
●
Источника трафика
●
Локали пользователя
●
Работоспособности гейтов
●
Фазы луны
●
А также комбинаций всех вычеперечисленных параметров
Новые гейты подключаются по несколько раз в неделю
Правила выбора набора гейтов меняются по несколько раз в час
Как это было
●
●

Ruby Passenger

MySQL
Slave
●

Ruby Passenger

Ruby Passenger

●

MySQL
Master

●

Машинные ресурсы
Рабочий процесс занимает 300mb памяти
Занимающий ресурсы процесс 20 секунд
ничего не делает
Человеческие ресурсы
Запуск процесса RoR ~5-10 секунд
Высокая сложность системы, на введение в
проект нового программиста требовалось
несколько дней
Некоторые участки кода были “сложными
для модификации”. Этот код обычно
источал баги и тормоза
Функциональная декомпозиция вместо обьектной
ozon_gate
params_validator

eviterra_gate
airbaltic_gate

●
●
●
●

merge

{"s": [
"params_validator",
{"p": [
"ozon_gate",
"eviterra_gate",
"airbaltic_gate"
]},
"merge"
]}

Строим систему из независимых компонент которые:
Имеют один входной аргумент и один выходной аргумент
Изолированы друг от друга
Не имеют зависимостей от среды исполнения
Могут быть легко протестированы как внутри так и вне системы
Требования к системе
●

Простота разработки и низкий порог вхождения

●

Высокая отказоустойчивость

●

Хорошая масштабируемость

●

Простота конфигурирования
Примеры юнитов
from random import random
class Throttler:
def __init__(self):
self.config = 1
def __call__(self, request):
if random() <= self.config:
return request
else:
return None

class CurrencyRatesExtender:
def __init__(self):
self.config = {}
def __call__(self, request):
request['currency_rates'] = self.config
request['currency_rates']['rub'] = 1
return request
Юниты — рабочие лошадки системы
●
●
●

●
●
●
●
●

Обращаемся к любому юниту системы через http
{“price”: 100, “currency”: “usd”}
Для кодирования данных используем json
call
Каждый юнит имеет 3 url для
●
Вызова юнита
●
Загрузки конфигурации юнита {“usd”: 31.93, set_config
CurrencyConverterUnit
●
Выгрузки конфигурации юнита “eur”: 43.66}
Какие бывают юниты?
Запрос билетов у гейтов
Добавление в результат поисков информации об аэропортах
Удаление дубликатов предложений от разных агентств
Отправка данных в RabbitMQ
Расчёт цены в рублях для гейтов отдающих билеты в другой
валюте

{“price”: 3193, “currency”: “rub”}
Обьединяем юниты в цепочки
Последовательные Цепочки
●
Входной аргумента цепочки подаётся в первый юнит цепочки
●
Если юнит возвращает NIL последующие юниты не вызываются и NIL обьявляется
результатом работы цепочки
●
Результат работы первого юнита передаётся во второй юнит
●
Результат работа второго юнита передаётся в третий юнит
●
…
●
Результатом работы цепочки является значение возвращаемое последним юнитом
input
цеопчки
Sequential
eviterra_gate

add_airports

add_airlines

output
Что будет если?

0

+1

+5

?
Что будет если?

0

+1

+5

6
Обьединяем юниты в цепочки

●

●

●

Параллельные Цепочки
input
Входной аргумента цепочки подаётся во все
юниты цепочки
Результаты работы всех юнитов цепочки
собираются в массив
Массив с результатами работы всех юнитов
цепочки является результатом работы цепочки

Parallel

eviterra_gate
ozon_gate
clickavia_gate
output
Что будет если?

0

+1
+5

[? , ?]
Что будет если?

0

+1
+5

[1 , 5]
Обьединяем юниты в цепочки
●
●
●
●

Отложенные Цепочки
Выходным аргументом цепочки является входной аргумент цепочки
Входной аргумент цепочки подаётся в первый юнит цепочки
Результат работы первого юнита цепочки подаётся во второй юнит цепочки
And so on …

input
Delayed
send_to_rabbitmq

output
Что будет если?

0

+1

?

+5
Что будет если?

0

+1

0

+5
Пример сложного workflow
output
Sequential

input

Parallel
eviterra_gate
Delayed
airbaltic_gate
params_validator
ozon_gate
clickavia_gate

merge

send_to_queue
Что нам даёт DSL
Простота локальноый и удалённой отладки приложения
●
Контроль за скоростью выполнения юнитов и цепочек
●
Свобода менять модель выполнения цепочек
●
В одном процессе
●
В несколькких процессах
●
На нескольких машинах
●
У нас есть DSL для описания workflow!
Что дальше? Базы данных!
●

●

●

Типы данных
Справочники – часто читаются, редко обновляются
●
Курсы валют
●
Аэропорты
●
Авиакомпании
Логи – часто пишутся, редко читаются
●
Поиски
●
Клики
●
Информация о поведении пользователей
Динамические данные – часто читаются, часто пишутся
●
Ссылки для переходов на страницу покупки
●
Результаты поиска
Справочники
●

●

●

●

Небольшие справочники храним в памяти
каждого рабочего процесса
Если справочник большой >1mb складываем
его в файловую базу данных (kyotocabinet).
Файл базы данных mmapится в адресное
пространство всех рабочих процессов ноды
Информация для справочников хранится на
файловой системе, при обновлении файлов
через inotify данные закидываются в рабоче
процессы
Между нодами файлы со справочниками
раскидываются lsyncd (inotify)
Логи
●

●

Рабочие процессы не могут писать данные в
глобальное хранилище непосредственно – в
этом случае отказ хранилища повредит
работоспособности приложения или приведёт к
потере данных
Пусть рабочие процессы складывают данные в
локальное хранилище в пределах ноды, и
перетаскивают данные в общее хранилище по
мере возможности
Динамическе данные
●

●

●

●

Скорость доступа и на чтение и на запись
критичны
Данные должны быть одинаково доступны со
всех нод кластера
In-memory key-value хранилище – ничего
быстрее быть не может!
Избыточность для обеспечения
отказоустойчивости
Детали Реализации и производительность

●
●
●
●
●

●
●

Язык программирования Python 3
(
)
Цепочки и юниты внутри цепочек исполняются асинхронно с помощью Tornado
Парсер xml - lxml
Файловая база данных kyotocabinet
Локальное хранилище данных redis
Рабочий процесс занимает 60mb ram
Виртуальная машина 8 ядер 16gb обрабатывает до 150 поисковых запросов
одновременно
Рабочие процессы
●

●

●

На ноде живут
Пчёлы
●
Обрабатывают запросы пользователей
●
Пишут только в local strage
●
Могут читать из глобального хранилища
Муравьи
●
Переносят данные из локального
хранилища во внешние
(rabbitmq/redis/mysql)
Local Storage
●
Хранилище способное придержать
данные до восстановления
работоспособности внешних хранилищ

Local
Storage

Redis

Redis

MySQL
Write Only
Storage

RabbitMQ
Что может пойти не так?

●

●

Отказ MySQL/RabbitMQ
Данные сохраняются в local
storage до восстановления
работоспособности
внешних серверов
После восстановленя
муравьи перенесут данные

Local
Storage

Redis

Redis

MySQL
Write Only
Storage
Что может пойти не так?

●

●

Отказ Redis
И запись и чтение
осуществляются в оба redis
одновременно
Если один из серверов
выходит из строя второй
берёт нагрузку на себя

Local
Storage

Redis

MySQL
Write Only
Storage

RabbitMQ
Что может пойти не так?

●

●

Отказ Всех Redis
Пчёлы не смогут читать
данные и часть запросов не
будет работать
Данные не потеряются, как
только сервера redis
восстановятся муравье
перетещут туда данные

Local
Storage

Redis

MySQL
Write Only
Storage

RabbitMQ
Что может пойти не так?
Local
Storage

●

●

Отказ LocalStorage
Нода целиком выводится из
кластера
Нагрузка распределяется
по остальным нодам
кластера

Redis

Local
Storage

Redis

MySQL
Write Only
Storage
Local
Storage

RabbitMQ
Ясень и все все все

Local
Storage

Redis

Redis

MySQL
Write Only
Storage

RabbitMQ
Итого
●

●

●

●

●
●
●

Юниты и цепочки могут быть независимо
сконфигурированы в рантайме
Среда исполнения позволяет контролировать скорость
выполнения юнитов и цепочек
Отладка системы осуществляется через http, просто и
наглядно
Разработка юнитов не требует специальной подготовки и
даже знакомства с “Ясенем”
Скорость запуска системы 0.1 секунды
Сократили количество серверов в два раза
Код в ясень пишут программисты из соседних проектов
Q&A
Модели исполнения
●

●

Sequential

Точки распараллеливания
- Отложенные цепочки
- Параллельные цепочки

Выполнение параллельных
операций:
- В разных потоках
- В разных процессах
- Асинхронное выполнение
- На разных серверах

Delayed

Parallel
Почему HTTP? Почему JSON?
●
●

●
●
●
●

●

Возможность работать с системой с любого компьютера где есть браузер
Если нет браузера то достаточно curl
Хорошая производительность библиотек работы с JSON
Возможность править конфигурацию в текстовом редакторе
Универсальный UI для редактирования JSON
Если конфигурация сложна – создаём специализированный редактор
Веб приложение обязано работать по протоколу HTTP, зачем искать что-то
ещё?
Почему так не могло продолжаться
●

●

●

●
●

●

Мы теряли деньги когда
MySQL выходил из строя или был
перегружен запросами
Под нагрузкой окзывалось что в новой
версии Rails тормоза
Нас показывали по первому каналу и люди
начинали искать билеты
Деплоились в штатном режиме
Для внесения изменений в систему
требовалась работа программистов
Passenger ы начинали массово
рестартовать под нагрузкой

Борис Каплуновский, Aviasales.ru

  • 1.
  • 2.
    Agenda Задачи метопоисковика авиабилетов ● Предыдущекрешение и его недостатки ● Требования к новой системе ● DSL “Ясеня” Юниты и цепочки ● Отказоустойчивость и базы данных ● Организация кластера ●
  • 3.
    Что мы делаем? Получаемзапрос от пользвателя Посылаем 60 запросов по 2 килобайта Получаем 40 ответов по ~1 одному мегабайту 20 000 запросов в час 13 Гигабайт в минуту ~6000 проданных билетов в сутки (15 x Boeing 747)
  • 4.
    Конфигурирование Разные наборы гейтовв зависимости от: ● Геоположения пользователя ● Пунктов вылета и назначения ● Набора пассажиров ● Источника трафика ● Локали пользователя ● Работоспособности гейтов ● Фазы луны ● А также комбинаций всех вычеперечисленных параметров Новые гейты подключаются по несколько раз в неделю Правила выбора набора гейтов меняются по несколько раз в час
  • 5.
    Как это было ● ● RubyPassenger MySQL Slave ● Ruby Passenger Ruby Passenger ● MySQL Master ● Машинные ресурсы Рабочий процесс занимает 300mb памяти Занимающий ресурсы процесс 20 секунд ничего не делает Человеческие ресурсы Запуск процесса RoR ~5-10 секунд Высокая сложность системы, на введение в проект нового программиста требовалось несколько дней Некоторые участки кода были “сложными для модификации”. Этот код обычно источал баги и тормоза
  • 6.
    Функциональная декомпозиция вместообьектной ozon_gate params_validator eviterra_gate airbaltic_gate ● ● ● ● merge {"s": [ "params_validator", {"p": [ "ozon_gate", "eviterra_gate", "airbaltic_gate" ]}, "merge" ]} Строим систему из независимых компонент которые: Имеют один входной аргумент и один выходной аргумент Изолированы друг от друга Не имеют зависимостей от среды исполнения Могут быть легко протестированы как внутри так и вне системы
  • 7.
    Требования к системе ● Простотаразработки и низкий порог вхождения ● Высокая отказоустойчивость ● Хорошая масштабируемость ● Простота конфигурирования
  • 8.
    Примеры юнитов from randomimport random class Throttler: def __init__(self): self.config = 1 def __call__(self, request): if random() <= self.config: return request else: return None class CurrencyRatesExtender: def __init__(self): self.config = {} def __call__(self, request): request['currency_rates'] = self.config request['currency_rates']['rub'] = 1 return request
  • 9.
    Юниты — рабочиелошадки системы ● ● ● ● ● ● ● ● Обращаемся к любому юниту системы через http {“price”: 100, “currency”: “usd”} Для кодирования данных используем json call Каждый юнит имеет 3 url для ● Вызова юнита ● Загрузки конфигурации юнита {“usd”: 31.93, set_config CurrencyConverterUnit ● Выгрузки конфигурации юнита “eur”: 43.66} Какие бывают юниты? Запрос билетов у гейтов Добавление в результат поисков информации об аэропортах Удаление дубликатов предложений от разных агентств Отправка данных в RabbitMQ Расчёт цены в рублях для гейтов отдающих билеты в другой валюте {“price”: 3193, “currency”: “rub”}
  • 10.
    Обьединяем юниты вцепочки Последовательные Цепочки ● Входной аргумента цепочки подаётся в первый юнит цепочки ● Если юнит возвращает NIL последующие юниты не вызываются и NIL обьявляется результатом работы цепочки ● Результат работы первого юнита передаётся во второй юнит ● Результат работа второго юнита передаётся в третий юнит ● … ● Результатом работы цепочки является значение возвращаемое последним юнитом input цеопчки Sequential eviterra_gate add_airports add_airlines output
  • 11.
  • 12.
  • 13.
    Обьединяем юниты вцепочки ● ● ● Параллельные Цепочки input Входной аргумента цепочки подаётся во все юниты цепочки Результаты работы всех юнитов цепочки собираются в массив Массив с результатами работы всех юнитов цепочки является результатом работы цепочки Parallel eviterra_gate ozon_gate clickavia_gate output
  • 14.
  • 15.
  • 16.
    Обьединяем юниты вцепочки ● ● ● ● Отложенные Цепочки Выходным аргументом цепочки является входной аргумент цепочки Входной аргумент цепочки подаётся в первый юнит цепочки Результат работы первого юнита цепочки подаётся во второй юнит цепочки And so on … input Delayed send_to_rabbitmq output
  • 17.
  • 18.
  • 19.
  • 20.
    Что нам даётDSL Простота локальноый и удалённой отладки приложения ● Контроль за скоростью выполнения юнитов и цепочек ● Свобода менять модель выполнения цепочек ● В одном процессе ● В несколькких процессах ● На нескольких машинах ●
  • 21.
    У нас естьDSL для описания workflow! Что дальше? Базы данных! ● ● ● Типы данных Справочники – часто читаются, редко обновляются ● Курсы валют ● Аэропорты ● Авиакомпании Логи – часто пишутся, редко читаются ● Поиски ● Клики ● Информация о поведении пользователей Динамические данные – часто читаются, часто пишутся ● Ссылки для переходов на страницу покупки ● Результаты поиска
  • 22.
    Справочники ● ● ● ● Небольшие справочники хранимв памяти каждого рабочего процесса Если справочник большой >1mb складываем его в файловую базу данных (kyotocabinet). Файл базы данных mmapится в адресное пространство всех рабочих процессов ноды Информация для справочников хранится на файловой системе, при обновлении файлов через inotify данные закидываются в рабоче процессы Между нодами файлы со справочниками раскидываются lsyncd (inotify)
  • 23.
    Логи ● ● Рабочие процессы немогут писать данные в глобальное хранилище непосредственно – в этом случае отказ хранилища повредит работоспособности приложения или приведёт к потере данных Пусть рабочие процессы складывают данные в локальное хранилище в пределах ноды, и перетаскивают данные в общее хранилище по мере возможности
  • 24.
    Динамическе данные ● ● ● ● Скорость доступаи на чтение и на запись критичны Данные должны быть одинаково доступны со всех нод кластера In-memory key-value хранилище – ничего быстрее быть не может! Избыточность для обеспечения отказоустойчивости
  • 25.
    Детали Реализации ипроизводительность ● ● ● ● ● ● ● Язык программирования Python 3 ( ) Цепочки и юниты внутри цепочек исполняются асинхронно с помощью Tornado Парсер xml - lxml Файловая база данных kyotocabinet Локальное хранилище данных redis Рабочий процесс занимает 60mb ram Виртуальная машина 8 ядер 16gb обрабатывает до 150 поисковых запросов одновременно
  • 26.
    Рабочие процессы ● ● ● На нодеживут Пчёлы ● Обрабатывают запросы пользователей ● Пишут только в local strage ● Могут читать из глобального хранилища Муравьи ● Переносят данные из локального хранилища во внешние (rabbitmq/redis/mysql) Local Storage ● Хранилище способное придержать данные до восстановления работоспособности внешних хранилищ Local Storage Redis Redis MySQL Write Only Storage RabbitMQ
  • 27.
    Что может пойтине так? ● ● Отказ MySQL/RabbitMQ Данные сохраняются в local storage до восстановления работоспособности внешних серверов После восстановленя муравьи перенесут данные Local Storage Redis Redis MySQL Write Only Storage
  • 28.
    Что может пойтине так? ● ● Отказ Redis И запись и чтение осуществляются в оба redis одновременно Если один из серверов выходит из строя второй берёт нагрузку на себя Local Storage Redis MySQL Write Only Storage RabbitMQ
  • 29.
    Что может пойтине так? ● ● Отказ Всех Redis Пчёлы не смогут читать данные и часть запросов не будет работать Данные не потеряются, как только сервера redis восстановятся муравье перетещут туда данные Local Storage Redis MySQL Write Only Storage RabbitMQ
  • 30.
    Что может пойтине так? Local Storage ● ● Отказ LocalStorage Нода целиком выводится из кластера Нагрузка распределяется по остальным нодам кластера Redis Local Storage Redis MySQL Write Only Storage Local Storage RabbitMQ
  • 31.
    Ясень и всевсе все Local Storage Redis Redis MySQL Write Only Storage RabbitMQ
  • 32.
    Итого ● ● ● ● ● ● ● Юниты и цепочкимогут быть независимо сконфигурированы в рантайме Среда исполнения позволяет контролировать скорость выполнения юнитов и цепочек Отладка системы осуществляется через http, просто и наглядно Разработка юнитов не требует специальной подготовки и даже знакомства с “Ясенем” Скорость запуска системы 0.1 секунды Сократили количество серверов в два раза Код в ясень пишут программисты из соседних проектов
  • 33.
  • 34.
    Модели исполнения ● ● Sequential Точки распараллеливания -Отложенные цепочки - Параллельные цепочки Выполнение параллельных операций: - В разных потоках - В разных процессах - Асинхронное выполнение - На разных серверах Delayed Parallel
  • 35.
    Почему HTTP? ПочемуJSON? ● ● ● ● ● ● ● Возможность работать с системой с любого компьютера где есть браузер Если нет браузера то достаточно curl Хорошая производительность библиотек работы с JSON Возможность править конфигурацию в текстовом редакторе Универсальный UI для редактирования JSON Если конфигурация сложна – создаём специализированный редактор Веб приложение обязано работать по протоколу HTTP, зачем искать что-то ещё?
  • 36.
    Почему так немогло продолжаться ● ● ● ● ● ● Мы теряли деньги когда MySQL выходил из строя или был перегружен запросами Под нагрузкой окзывалось что в новой версии Rails тормоза Нас показывали по первому каналу и люди начинали искать билеты Деплоились в штатном режиме Для внесения изменений в систему требовалась работа программистов Passenger ы начинали массово рестартовать под нагрузкой