Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Как мы переходили на реалтайм-индекс

Alex Sladkov
February 28, 2019

Как мы переходили на реалтайм-индекс

Как и зачем мы пришли от регулярного перестроения индексов для полнотекстового поиска и отправки обновлений в коде «по месту» к realtime-индексам и автоматической синхронизации состояния индекса и базы данных MariaDB.

Alex Sladkov

February 28, 2019
Tweet

Other Decks in Programming

Transcript

  1. • Поиск резюме • Поиск вакансий • Поиск компаний •

    Исправление опечаток • Подписки на новые вакансии • Подписки по вакансиям на новые резюме 3 Полнотекстовый поиск в SuperJob
  2. 1. Резюме и вакансии появляются в поиске слишком медленно 2.

    Сложности с синхронизацией MariaDB - Sphinx • Резюме иногда пропадают из поиска Зачем? 4
  3. Индекс резюме «Архивный» Документы давно не менялись Перестраиваем раз в

    сутки «Активный» Документы менялись недавно Перестраиваем раз в 15 минут 6 WHERE published_at < @d SET @d = DATE(NOW() - INTERVAL 60 DAY); WHERE published_at >= @d
  4. Почему резюме пропадают? 7 время Начало индексации «архивного» Завершение индексации

    «архивного» Есть резюме, которых уже нет в архивном и еще нет в активном Индексация «активного»
  5. 8 Индекс резюме (исправленный) «Архивный» Документы давно не менялись Перестраиваем

    раз в сутки «Активный» Документы менялись недавно Перестраиваем раз в 15 минут WHERE published_at < @d SET @d = DATE(NOW() - INTERVAL 60 DAY); SET @e = @d - INTERVAL 1 HOUR; WHERE published_at >= @e
  6. Зачем? 9 1. Резюме и вакансии появляются в поиске слишком

    медленно 2. Сложности с синхронизацией MySQL - Sphinx • Резюме иногда пропадают из поиска • Резюме перенесли в закрытый доступ, а оно все равно показывается в поиске
  7. 10 Убираем документы из поиска вовремя UPDATE resume_active SET published

    = 0 WHERE id = ?; UPDATE resume_archive SET published = 0 WHERE id = ?; SELECT id, WEIGHT() FROM resume_archive,resume_active WHERE ... AND published = 1
  8. Распределенный индекс index resume_propagate { type = distributed agent =

    searcher01:9312:resume agent = searcher02:9312:resume agent = searcher03:9312:resume agent = searcher04:9312:resume agent = searcher05:9312:resume } searcher2 12
  9. PHP haproxy searcher1 searcher2 searcher3 searcher4 searcher5 13 UPDATE resume_propagate

    ... Доставка обновлений атрибутов
  10. 15 Перенос в закрытый доступ во время перестроения индекса Начало

    индексации Завершение индексации вернулись к состоянию на момент начала индексации Резюме перенесли в закрытый доступ обновили атрибут SQL-запрос Построение индекса время
  11. Зачем? 16 1. Резюме и вакансии появляются в поиске слишком

    медленно 2. Сложности с синхронизацией MySQL - Sphinx • Резюме иногда пропадают из поиска • Резюме перенесли в закрытый доступ, а оно все равно показывается в поиске 3. End-to-end тесты с участием поиска
  12. 1. Размещаем вакансию 2. ??? 3. Выполняем поиск 4. Проверяем,

    что вакансия есть в выдаче 17 Сценарий автотеста
  13. параллельный запуск тестов 18 Сценарий автотеста 1. Размещаем вакансию 2.

    Перестраиваем индекс 3. Выполняем поиск 4. Проверяем, что вакансия есть в выдаче
  14. • Уменьшить задержку между обновлением базы и индекса • Упростить

    синхронизацию базы данных и индекса • Предотвратить ошибки • Сделать так, чтобы для тестов не надо было перестраивать индекс 19 Надо что-то менять, но что мы хотим?
  15. • Реалтайм-индекс • Единый канал передачи изменений между MariaDB и

    Sphinx • Список изменений, произошедших с момента перестроения индекса 20 Надо что-то менять, но что именно?
  16. 21 Надо что-то менять, но как именно? • Реалтайм-индекс •

    Единый канал передачи изменений между MariaDB и Sphinx - соединение к MariaDB по протоколу репликации • Список изменений, произошедших с момента перестроения индекса - binlog MariaDB
  17. Сервис-репликатор • Соединение с MariaDB по протоколу репликации • Сохранение

    текущей позиции • Выяснение, какие документы нужно обновить в индексе • Запрос недостающих данных из MariaDB • Формирование и выполнение запросов на обновление индекса 23
  18. MySQL replication libslave https://github.com/Begun/libslave C++ php-mysql-replication https://github.com/krowinski/php-mysql-replication php python-mysql-replication https://github.com/noplay/python-mysql-replication

    python mysql-binlog-connector-java https://github.com/shyiko/mysql-binlog-connector-java java mysql-haskell https://github.com/winterland1989/mysql-haskell haskell go-mysql https://github.com/siddontang/go-mysql go 24
  19. Выбираем на чем писать • Поддержка MariaDB 10.1 • Количество

    потенциальных контрибьюторов среди наших разработчиков • Хорошая поддержка конкурентного и параллельного исполнения кода • Отсутствие необходимости управлять памятью 25
  20. MySQL replication libslave https://github.com/Begun/libslave C++ php-mysql-replication https://github.com/krowinski/php-mysql-replication php python-mysql-replication https://github.com/noplay/python-mysql-replication

    python mysql-binlog-connector-java https://github.com/shyiko/mysql-binlog-connector-java java mysql-haskell https://github.com/winterland1989/mysql-haskell haskell go-mysql https://github.com/siddontang/go-mysql go 26
  21. DocID uint64 Index string Table *schema.Table Action string Rows [][]interface{}

    Header *replication.EventHeader ROWS_EVENT Routing table = "vacancy" id_field = "id" index = "vacancy" row change 27 Определение id документов - основная таблица
  22. 28 Определение id документов - связанная таблица ROWS_EVENT Routing table

    = "vacancy_language" id_field = "vacancy_id" index = "vacancy" row change 28 row change row change row change row change
  23. Получение недостающих данных из MariaDB 29 [data_source.vacancy] parts = 4

    query = """ SELECT vacancy.id AS `id`, vacancy.profession AS `profession_text`, GROUP_CONCAT(DISTINCT vacancy_language.language_id) AS `languages`, GROUP_CONCAT(DISTINCT vacancy_metro_station.metro_station_id) AS `metro` FROM vacancy LEFT JOIN vacancy_language ON vacancy_language.vacancy_id = vacancy.id LEFT JOIN vacancy_metro_station ON vacancy_metro_station.vacancy_id = vacancy.id GROUP BY vacancy.id """
  24. Сохранение текущей позиции GTID 11-2005-318267 trans ## BEGIN Table_map: `hr`.`vacancy`

    mapped to number 1334 Write_rows: table id 1334 <row data> Xid = 3072389 ## COMMIT 33
  25. MySQL [(none)]> desc sync_state; +-----------------+--------+ | Field | Type |

    +-----------------+--------+ | id | bigint | | dummy_field | field | | binlog_position | uint | | binlog_name | string | | gtid | string | | flavor | string | +-----------------+--------+ 34 Сохранение текущей позиции
  26. startup checks OK? reindex start reading from saved position YES

    YES NO NO start sync 36 Проверки на старте сервиса COM_BINLOG_DUMP OK?
  27. BEGIN; UPDATE vacancy SET edited_at = NOW() WHERE id =

    123; DELETE FROM vacancy_language WHERE vacancy_id = 123; INSERT INTO vacancy_language (vacancy_id, language_id, level) VALUES (123, 1, "fluent"), (123, 2, "technical"); DELETE FROM vacancy_metro_station WHERE vacancy_id = 123; INSERT INTO vacancy_metro_station (vacancy_id, metro_station_id) VALUES (123, 55); ... COMMIT; 37 Обновление документа
  28. Накопление изменений • Уменьшить количество «лишних» обновлений • Избегать «незавершенных»

    обновлений • Объединять отправку изменений в Sphinx • Делать UPDATE вместо REPLACE (если в документе изменились только числовые атрибуты) 38
  29. 39 Action string Columns []string DocID uint64 Index string OldRow

    []interface{} NewRow []interface{} PK PrimaryKey TableName string TS time.Time row change Накопление изменений
  30. index vacancy#1 row change 1 row change 2 row change

    3 row change 4 vacancy#2 row change 5 row change 6 row change 7 row change 8 vacancy#3 row change 9 row change 10 40 Накопление изменений
  31. index vacancy#1 row change 1 row change 3 row change

    8 row change 10 vacancy#2 row change 2 row change 4 row change 7 vacancy#3 row change 5 row change 6 row change 9 41 Накопление изменений
  32. index Обработка накопленных изменений vacancy#1 row change 1 row change

    3 row change 8 row change 10 vacancy#2 row change 2 row change 4 row change 7 vacancy#3 row change 5 row change 6 row change 9 TS TS TS TS TS TS TS T TS TS TS TS 42 MAX(TS) < Time.Now() - 100*Time.Millisecond
  33. vacancy#1 row change 1 row change 3 vacancy vacancy_language Изменения

    в таблице vacancy Изменения в таблице vacancy_language 43 Обработка накопленных изменений row change 8 row change 10 row change 1 row change 3 row change 8 row change 10
  34. 44 - {id: 1, date_edited: “2018-11-07”, profession: “Программист”} + {id:

    1, date_edited: “2019-02-20”, profession: “Web-developer”} Изменения в таблице row change 1 Изменения в таблице vacancy - {id: 1, date_edited: “2018-11-07”, profession: “Программист”} + {id: 1, date_edited: “2019-02-20”, profession: “Web-developer”}
  35. 45 vacancy#1 row change 1 row change 3 vacancy vacancy_language

    Изменения в таблице vacancy Изменения в таблице vacancy_language Обработка накопленных изменений row change 8 row change 10 row change 1 row change 3 row change 8 row change 10
  36. 46 Изменения в таблице Изменения в таблице vacancy_language - {vacancy_id:

    1, language_id: 1, level: “fluent”} row change 3 + {vacancy_id: 1, language_id: 1, level: “fluent”} row change 8 + {vacancy_id: 1, language_id: 3, level: “technical”} row change 10 + {vacancy_id: 1, language_id: 3, level: “technical”}
  37. table = "vacancy" [ingest.column_map] user_id = ["user_id"] edited_at = ["date_edited"]

    profession = ["profession"] lat = ["lat_deg", "lat_rad"] lon = ["lon_deg", "lon_rad"] ... 47 Поля в БД ⟶ Поля в индексе Изменения в таблице vacancy Набор измененных полей и атрибутов [ “date_edited”, “profession”, ] [ “edited_at”, “profession” ]
  38. table = "vacancy_language" [ingest.column_map] language_id = [ "languages" ] level

    = [ "languages" ] 48 Поля в БД ⟶ Поля в индексе Изменения в таблице vacancy_language Набор измененных полей и атрибутов [ “languages” ] [ “language_id”, “level” ]
  39. Что нужно изменить в индексе Набор измененных полей и атрибутов

    [ “date_edited”, “profession”, “languages” ] 49 Набор измененных полей и атрибутов (таблица vacancy) [ “date_edited”, “profession” ] Набор измененных полей и атрибутов (таблица vacancy_language) [ “languages” ]
  40. index vacancy#1 vacancy#2 vacancy#3 [ “date_edited”, “profession”, “languages” ] [

    “date_edited”, “metro” ] [] 50 Что изменилось
  41. [data_source.vacancy] parts = 4 query = """ SELECT vacancy.id AS

    `:id`, vacancy.profession AS `profession_text:field`, GROUP_CONCAT(DISTINCT vacancy_language.language_id) AS `languages:attr_multi`, GROUP_CONCAT(DISTINCT vacancy_metro_station.metro_station_id) AS `metro:attr_multi` FROM vacancy LEFT JOIN vacancy_language ON vacancy_language.vacancy_id = vacancy.id LEFT JOIN vacancy_metro_station ON vacancy_metro_station.vacancy_id = vacancy.id GROUP BY vacancy.id """ 51 Типы полей в индексе
  42. { “date_edited”: “uint”, “profession”: “field”, “languages”: “mva”, “metro”: “mva”, }

    Набор измененных полей и атрибутов [ “date_edited”, “profession”, “languages” ] REPLACE DELETE record exists in db? 52 Обновление индекса (есть изменения текстовых полей)
  43. { “date_edited”: “uint”, “profession”: “field”, “languages”: “mva”, “metro”: “mva”, }

    53 Обновление индекса (нет изменений текстовых полей) Набор измененных полей и атрибутов [ “date_edited”, “metro” ] UPDATE DELETE record exists in db?
  44. { “date_edited”: “uint”, “profession”: “field”, “languages”: “mva”, “metro”: “mva”, }

    54 Обновление индекса (нет изменений) Набор измененных полей и атрибутов []
  45. index vacancy#1 row change 1 vacancy#2 row change 2 row

    change 4 row change 7 vacancy#3 row change 5 row change 6 row change 9 55 Сохранение текущей позиции v2 row change 3 row change 8 row change 10
  46. 56 Action string Columns []string DocID uint64 GTID mysql.GTIDSet Index

    string OldRow []interface{} NewRow []interface{} PK PrimaryKey TableName string TS time.Time row change Сохранение текущей позиции v2
  47. index Применены не все изменения в очереди vacancy#1 row change

    1 row change 3 row change 8 row change 10 vacancy#2 row change 2 row change 4 row change 7 vacancy#3 row change 5 row change 6 row change 9 GTID GTID GTID GTID GTID GTID GTID GTID GTID GTID earliest 57
  48. index Применены все изменения в очереди last completed GTID 58

    vacancy#1 row change 1 row change 3 row change 8 row change 10 vacancy#2 row change 4 row change 7 vacancy#3 row change 5 row change 6 row change 9 GTID GTID GTID GTID GTID GTID GTID GTID GTID GTID row change 2 GTID
  49. Фрагментация 59 disk chunk disk chunk RAM chunk disk chunk

    disk chunk disk chunk FLUSH RAMCHUNK disk chunk disk chunk disk chunk OPTIMIZE INDEX disk chunk
  50. OPTIMIZE INDEX 60 Sphinx 2.3.2-id64-beta (4409612) Handling signal 11 --------------

    backtrace begins here --------------- Program compiled with gcc 4.8.5 Configured with flags: '--build=x86_64-redhat-linux-gnu' '--host=x86_64-redhat-l 'LDFLAGS=-Wl,-z,relro ' 'CXXFLAGS=-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fex Host OS is Linux druj-ts2.sj-dev.local 3.10.0-327.el7.x86_64 #1 SMP Thu Nov 19 22 Stack bottom = 0x7fffce1024a7, thread stack size = 0x100000 Trying manual backtrace: Frame pointer is null, manual backtrace failed (did you build with -fomit-frame-p Trying system backtrace: begin of system symbols: searchd[0x58c5de] searchd[0x40eb19] /lib64/libpthread.so.0(+0xf5d0)[0x7fd30f1da5d0] /lib64/libc.so.6(__select+0x33)[0x7fd30d31df73] searchd[0x4944d4] searchd[0x43b26c] searchd[0x45d82a]
  51. Reindex 1. Генерируем конфиг для indexer 2. Сохраняем текущую позицию

    MariaDB 3. Запускаем indexer 4. Загружаем файлы индекса по серверам 5. Останавливаем синхронизацию 6. Заменяем старый индекс на новый 7. Возобновляем синхронизацию с сохраненной позиции 61
  52. Indexer config [data_source.vacancy] parts = 4 query = """ SELECT

    vacancy.id AS `:id`, vacancy.profession AS `profession_text:field`, GROUP_CONCAT(DISTINCT vacancy_language.language_id) AS `languages:attr_multi`, GROUP_CONCAT(DISTINCT vacancy_metro_station.metro_station_id) AS `metro:attr_multi` FROM vacancy LEFT JOIN vacancy_language ON vacancy_language.vacancy_id = vacancy.id LEFT JOIN vacancy_metro_station ON vacancy_metro_station.vacancy_id = vacancy.id GROUP BY vacancy.id """ 62
  53. [data_source.vacancy.indexer] mem_limit = ... write_buffer = ... group_concat_max_len = ...

    [data_source.vacancy.indexer.tokenizer] charset_table = ... morphology = ... [data_source.vacancy.indexer.dictionaries] exceptions = ... stopwords = ... wordforms = ... Indexer config 63
  54. Построение plain-индекса indexer --config tmp.vacancy.indexer.0.conf --all indexer --config tmp.vacancy.indexer.1.conf --all

    indexer --config tmp.vacancy.indexer.2.conf --all indexer --config tmp.vacancy.indexer.3.conf --all 64
  55. [index_uploader] executable = "rsync" arguments = [ "--files-from=-", "--log-file=<<.DataDir>>/rsync.<<.Host>>.log", "--no-relative",

    "--times", "--delay-updates", ".", "rsync://<<.Host>>/index/vacancy/", ] 65 Загрузка файлов индекса
  56. Замена индекса 1. Haproxy: set server sphinx/sphinx01 state maint 2.

    Sphinx: RELOAD INDEX vacancy_plain_part_0; TRUNCATE INDEX vacancy_part_0; ATTACH INDEX vacancy_plain_part_0 TO vacancy_part_0; ... 3. Haproxy: set server sphinx/sphinx01 state ready для каждого сервера 66
  57. Синхронизация нового индекса 67 загрузка файлов завершена начали чтение с

    сохраненной позиции закончили применять изменения
  58. Сценарий автотеста 74 1. Размещаем вакансию 2. Перестраиваем индекс 3.

    Выполняем поиск 4. Проверяем, что вакансия есть в выдаче
  59. Синхронный режим для автотестов 1. Размещаем вакансию 2. Перестраиваем индекс

    Ждем синхронизации i. SELECT @@gtid_current_pos; ii. Передаем результат в endpoint сервиса iii. Ждем соответствующего GTID 3. Выполняем поиск 4. Проверяем, что вакансия есть в выдаче 75
  60. Итог • Все обновления доходят до всех серверов • Примерно

    в одно и то же время • Гораздо быстрее, чем раньше • Можно писать end-to-end тесты с участием поиска 76
  61. Что дальше • Sphinx 3 ◦ CREATE TABLE ◦ ATTACH

    WITH TRUNCATE ◦ Индексы по атрибутам ◦ Производительность 77