company_banner

Эволюция архитектуры: от «самописных» сервисов к HandlerSocket



    Сегодня мы расскажем о том, как в Badoo изменился подход к проектированию нагруженных “key-value” сервисов. Вы узнаете, по какой схеме такие сервисы создавались нами несколько лет назад (использование БД в качестве репозиториев и специализированного демона как интерфейса к данным), с какими трудностями мы при этом столкнулись и к какой архитектуре в результате пришли, разрешив появившиеся проблемы.

    Современные интернет-проекты активно используют внутренние сервисы, позволяющие обращаться к значениям по ключу. Это могут быть как готовые решения, так и собственные разработки. Начиная с 2006 года, специалистами компании Badoo был создан ряд таких сервисов, в числе которых:
    • сервис, сообщающий о месторасположении данных пользователя по его идентификатору;
    • сервис, хранящий информацию о количестве обращений к профайлу пользователя;
    • список интересов пользователей;
    • несколько “геосервисов”, позволяющих определить местонахождение пользователя.


    Несмотря на их разнообразие, применялся единый подход к проектированию, согласно которому сервис должен был состоять из следующих компонент:
    1. Базы-репозитория, которая хранит эталонную версию данных.
    2. Быстрого демона на C или C++, который обрабатывает запросы на получение данных и обновляется вместе с базой-репозиторием.
    3. PHP-классов, обеспечивающих работу с демоном и базой-репозиторием.

    Нам было важно, чтобы любой из сервисов мог обрабатывать большое количество одновременных запросов, поэтому решение на базе только одного MySQL не подходило. Отсюда и появилась дополнительная компонента в виде быстрого демона, которую не получилось заменить системой memcached, т.к. нам нужно было использовать специфические индексы данных.

    В конце 2010 года все большую популярность стал набирать HandlerSocket MySQL plug-in, написанный японским умельцем, предоставляющий NoSQL интерфейс к данным, хранимым в MySQL. Уже весной 2011 года специалисты Badoo обратили своё внимание на новую технологию, надеясь с её помощью упростить разработку и поддержку “key-value” сервисов компании.

    “Первая жертва”


    В сети для поиска новых друзей Badoo зарегистрировано огромное количество пользователей, которые получают различные уведомления по электронной почте. И у каждого из этих пользователей есть возможность выбрать тип уведомлений, которые он хотел бы получать. Соответственно, нужно где-то хранить почтовые настройки и предоставлять к этим данным доступ. Более 99% запросов к ним – запросы на чтение. Основными «читателями» являются скрипты-генераторы и отправители электронной почты, которые на основании этих данных принимают решение о возможности или невозможности отправлять корреспонденцию определенного типа конкретному пользователю. Сначала для хранения данных использовалась только БД, однако она перестала справляться с постоянно растущим числом запросов на чтение. Для снятия этой нагрузки был создан специальный демон — EmailNotification.

    “Old school” реализация сервиса


    Ключевой составляющей сервиса был С-демон, хранящий в памяти все настройки и предоставляющий к ним доступ на чтение и запись по простейшему протоколу поверх TCP. К тому же данный демон собирал статистику обращений, по которой мы строили графики.

    Вначале архитектура сервиса была достаточно простой и выглядела так:


    Настройки постоянно хранились в базе на одном DB-сервере, а С-демон работал на другом. При старте демон выбирал из базы все данные и строил по ним индекс (judy arrays). Процесс инициализации занимал около 15 минут, но так как данная операция потребовалась всего лишь несколько раз за все время существования сервиса, то это не являлось существенным недостатком. В процессе работы клиенты (CLI-скрипты, веб и другие сервисы) через специальный API обращались к демону, например, с вопросом, можно ли этому пользователю отправить это письмо или нет, и демон по встроенной логике искал у себя в памяти настройку и выдавал ответ. «Клиенты-писатели» отдавали демону команду поменять определенные настройки для определенного пользователя.

    Задача записи данных в MySQL была целиком возложена на EmailNotification API. При этом потенциально могла возникнуть рассинхронизация данных, например, когда запись успешно прошла в базу, но не прошла в демон, или наоборот. Тем не менее сервис прекрасно работал. До тех пор, пока в 2007 году в Badoo не произошла «маленькая неприятность», а именно — появился второй дата-центр, географически удаленный от первого и предназначенный для обслуживания пользователей Нового Света. Сразу стало понятно, что обычным дублированием архитектурного решения на новой площадке обойтись не удастся. Так как письма одному и тому же пользователю могут отправляться с обоих площадок, требуется, чтобы оба сервиса оперировали одинаковыми данными.

    К счастью, специально для таких случаев внутри компании имеется система CPQ-событий (CPQ — Cross Platform Queue, кроссплатформенные очереди. — Прим. автора), которая позволяет оперативно, а главное — гарантированно и в заданной последовательности передавать информацию о произошедших событиях между площадками. В результате на двух площадках архитектура сервиса приняла следующий вид:


    Теперь любые запросы на запись шли не только в базу и С-демон локальной площадки, но и в CPQ. CPQ передавала запрос в смежную очередь другой площадки, а та уже воспроизводила запрос на запись через EmailNotification API на этой же площадке.

    Система усложнилась, но тем не менее продолжала стабильно работать на протяжении нескольких лет. И все было бы хорошо, если бы не появилось расхождение данных на площадках. В двух базах имеющихся площадок было разное количество настроек. И хотя разница была менее 0.1%, ощущения «чистоты и защищенности» как не бывало. Более того, мы обнаружили, что разница появилась не только между базами площадок, но она присутствовала между базой и С-демоном в рамках одной площадки. Пришлось задуматься над тем, как сделать сервис более надёжным.

    Новый подход


    Изначально основных требований к сервису EmailNotification было два: первое — высокая скорость обработки запросов на чтение, с чем прекрасно справлялся С-демон; второе — идентичность данных на обоих площадках, с которой были проблемы. Вместо того, чтобы биться над синхронизацией, мы решили полностью переделать архитектуру сервиса, пойдя по пути ее упрощения:


    Прежде всего, мы подключили плагин HandlerSocket к MySQL и научили наш API работать через него с базой. Благодаря этому мы смогли отказаться от использования С-демона. Затем, упростив API, мы убрали из схемы CPQ-сервис, заменив его хорошо зарекомендовавшей себя “master-master” репликацией между площадками. В результате мы получили очень простую и надежную схему, которая обладает следующими преимуществами:
    1. Репликация осуществляется прозрачно, не требуется написание кода, работающего с внутренним CPQ-сервисом. При этом задержка при переносе обновлений между площадками сократилась от нескольких секунд до долей секунды.
    2. Атомарность записи данных (наконец-то!). Если запрос EmailNotification API на запись в HandlerSocket завершился успешно, значит, задача выполнена, запись точно продублируется на другой площадке, и нам не нужно информировать об этом никакие другие компоненты.

    Были ли у нас проблемы при переходе на новую схему? Серьезных — нет. AUTO_INCREMENT уже поддерживается плагином HandlerSocket, составные индексы работают, ENUM-ы тоже, пришлось только отказаться от CURRENT_TIMESTAMP-значения по умолчанию для одного из timestamp полей.

    Как известно, преимущество HandlerSocket не в скорости его работы — она скорее приемлемая, нежели впечатляющая, а в его возможности стабильно работать под большим количеством запросов в единицу времени. Учитывая, что сейчас сервис обслуживает всего 2 — 2.5 тысячи запросов в секунду на одной площадке, то у нас есть большой запас прочности.

    Но для особо интересующихся скоростью работы плагина HandlerSocket приведем график со средним временем выполнения трех команд: подключению к HandlerSocket, открытию индекса и выборке по нему данных (значения по оси Y в миллисекундах):


    Заключительное слово


    За последний год в Badoo наметилась тенденция использовать HandlerSocket в качестве “key-value” репозитория с постоянным хранением данных. Это позволяет нам писать более простой и понятный код, избавляет С-программистов от работы над тривиальными задачами и заметно упрощает поддержку. И пока все говорит о том, что движение в данном направлении продолжится.

    Но не стоит думать, что использование HandlerSocket внутри Badoo ограничивается простейшими задачами. К примеру, у нас имеется большой опыт его использования для решения задач с преобладанием функции записи, где под действительно большой нагрузкой проявляется ряд нюансов. Если вам интересны детали — комментируйте, спрашивайте, и мы обязательно продолжим эту тему новыми статьями.

    Спасибо!
    Badoo
    399,00
    Big Dating
    Поделиться публикацией

    Похожие публикации

    Комментарии 34

      +3
      Кстати, первая картинка кликабельна :)
        –1
        Странно, что анимация гифа не запускается
          +18


          Их еще можно выстраивать в ряд в любую сторону. :)
          +4
          Загипнотизировал первой картинкой :)
            +3
            А почему не выбрали NoSQL хранилище заточеное под хранение key-value?
              +1
              Присоединяюсь к вопросу. Или что помешало использовать два хранилища — SQL и NoSQL?
                +2
                По видимому из-за индексов:

                Отсюда и появилась дополнительная компонента в виде быстрого демона, которую не получилось заменить системой memcached, т.к. нам нужно было использовать специфические индексы данных.
                  +6
                  А потому что еще никто и не мешает потом сделать mysql connect и поиграть как угодно с данными повыбирать че нравится, поискать like по любым значениям.
                    –2
                    Из любого NoSql тоже можно сделать любые выборки, перебрав все имеющиеся ключи(при вашем like ведь произойдет то же самое). А скорость работы в режиме key/value(те в основном режиме) будет выше на порядок. Конечно это не так удобно, но за то в бою работает быстрее.
                    Но самое главное — непонятно, зачем вам одна база данных с настройками, когда эти настройки легко раскладываются по шардам юзеров. Из статьи не очень понятно, для чего конкретно нужна единая таблица.

                      +2
                      При чем тут перебор ключей? Почему нельзя их в handler socket сделать? Да и mysql умеет это делать. Перебор это не бенефит nosql это способ работать с данными. Далее я както не понял а как вы все ключи то переберете? Гдето списочек будете хранить а вот в mysql range scan по первичному ключу будет всегда хранить не надо список.

                      Если говорить о nosql вы какой именно имеете в виду? В реальных условиях все что мы тестировали в случае когда данные перестают помещаться в память превращаются в ходячий и тормозной мусор умирающий на синхронной записи на диск. Это редис, это мемкешдб. А вот mysql в этих же условиях работает в 10ки если не в сотни раз быстрее. В частности со 120 серверов в 40 потоков с каждого через hadnler socket на чтение получил 600.000 запросов в секунду. Дальше просто шел упор в сетевушку, это при том что разумеется конекты были keep alive. без этого изза особенностей tcp ip выжать не смог более 70К запросов в секунду. После чего сделал 120К открытых конектов через которые прогнал теже самые 600.000 запросов в секунду с одного сервера.
                        0
                        Про перебор ключей — это я имел в виду эмуляцию селекта с like на nosql про который вы упомянули.
                        например kyoto cabinet умеет искать по префиксам ключей — так можно вынуть весь список нобходимых ключей.
                        Да noSql начинает тормозить когда упирается в диск. но использование hs никак от этого не уберегает: он так же встанет на большом колличестве одновременных запросов. Дело то не в типе бд, а в том что много одновременных запросов не пролезет через диск — только память. ваши 600к запросов то не с диска же все-таки?

                0
                у нас имеется большой опыт его использования для решения задач с преобладанием функции записи, где под действительно большой нагрузкой проявляется ряд нюансов.
                с этого места и поподробнее.
                  +15
                  Хотите отдельную статью с кучей примеров, цифр, перечислением грабель и т.п. про Handler Socket?
                    +7
                    Да!
                      +3
                      Постараемся сделать, друзья. Там много интересного.
                        0
                        +1 к «Да». Интересно и познавательно, с практической точки зрения.
                          0
                          Хотим!
                        0
                        Как после insert'а получить значение auto increment id?
                          0
                          третье поле в Handlersocket-ответе должно быть. При условии, что используется относительно свежий handlersocket.so
                            0
                            Да, тертье
                            if (sizeof($r) == 3) {
                            return $r[2]; //last_insert_id
                            }
                              0
                              пример сессии:

                              P 1 somedb sometable PRIMARY col1,col2
                              0 1
                              1 + 2 tmp1 tmp2
                              0 1 14968


                              14968 — и есть last insert id
                                0
                                попытка номер два:

                                P 1 somedb sometable PRIMARY col1,col2
                                0 1
                                1 + 2 tmp1 tmp2
                                0 1 14968
                            0
                            В вашем случае MMR скорее всего применима, но в общем случае — MMR в Mysql нельзя использовать в нагруженном сервисе: помимо специфики автоинкремента есть еще не решаемые проблемы конкурентных апдейтов одних и тех же записей.
                            Еще цифры в тестах очень большие: у нас на серваке трехлетней давности общее время запроса (коннект/индекс/запрос) — 0.6ms те в 10 раз меньше. причем это еще далеко не предел — есть куда крутить настройки.
                            на нашей базе в 12 миллионов записей и 3к запросов в секунду все 3 операции занимали примерно однинаковое время — по 0.2ms — так что у вас еще и косяк где-то на коннекте происходит. не может коннект выполнятся дольше всех остальных операций
                              +3
                              Позволю не согласиться. MMR можно и нужно использовать, ка ки любые технологии, если делаешь это с умом.
                              — Конкурентные update — организуйте и подумайте над данными и структурой изменения так, чтобы изменения одной записи делались на одном из мастеров.
                              — Автоинкремента проблемы нет никакой это сказка в mysql нативная настройка offset & total задаем число инстансов и офсет даем каждому инстансу, наслаждаемся.
                              — Про время операций, вы видимо про native mysql говорите цифры, не так ли? в HS порядки цифры в 10-100 раз выше в частности на connect, изза того что он дерагет напрямую сразу первичный ключ, без проверок авторизации, без разбора sql и т.п. и именнов этом выйгрыш, вы на своем старом сервере hs то поднимите и подергайте запросы, только по первичному ключу, удивитесь.
                              — запись на диск в mysql будет работать быстрее чем в любом быстросамописном nosql, по крайней мере я пока придерживаюсь такой позиции. пока сам не увижу обратного, хотябы потому что mysql уже лет 10 занимаетс оптимизацией записи на диск, а в nosql как правило просто тупо пишем на диск не используя ничего для оптимизации.
                                0
                                1.конкурентные апдейты — ну вы же согласны, что надо за ними очень тщательно следить и, на мой взгляд, лучше вообще отказаться от MMR в пользу шардинга — как-то спокойнее.
                                2. Ну согласитесь же, что настройка автоинкреимента на автоинкремент 2+1 выглядит каким-то костылем. И что вы будете делать, когда у вас появится 3я площадка?
                                3. про время операций: я тестировал hs на mysql 5.5. естественно мне было интересно, какой же прирост по сравнению с native mysql — прирост был примерно в 20% Это объясняется даже в статье автора плагина (той самой, про 750к запросов), что в mysql 5.1 было куча локов еще до innodb, которые они, чтением напрямую из innodb обошли. В 5.5 их очень много выпили, поэтому разница сильно сократилась. Согласитель же, что парсинг запроса и проверка авторизации это не тотальные по быстродействию операции. и не могут они занимать 90% времени запроса. Вы сами говорите, их же 10 лет разрабатывают.
                                4. про запись на диск. Частично согласен, но модель Редиса — переодические снапшоты + лог апдейтов мне кажется более быстрым чем синк страниц мускула в рандомном порядке
                                  0
                                  мускул, очевидно, синкает не в рандомном порядке, ну и плюс ядро сортирует страницы и пытается писать последовательно, а там дальше софт в самом диске, ncq всякое.

                                  насчет редиса: упс случается, когда много пишут + запускается процесс снапшота или rewriteaof, особенно, если редис заполнен близко к общему размеру памяти на машине.
                                  если повезет и настроена overcommit memory, то он сможет форкнуться без свопа, но т.к. записи много, и папке и дочке нужно еще памяти они оба вываливаются в своп и машина умирает. Предполагаю по этой причине народ извращается с 16 инстансами редиса на машину по 4 гига максимум каждый.
                                    0
                                    я делаю апдейты в рандомные старницы памяти. соответственно они должны быть синкануты на диск. Хорошо что ядро может их отсортировать в нужном порядке. но апдейты мускула идут в разные страницы, а апдейты редиса в одну. одну страницу синкать явно сильно быстрее чем разные.

                                    я так понимаю, что редис надо использовать на малой нагрузке на запись чтоб не вылести в своп из-за copy-on-write? те в качестве кеша?
                                +1
                                Еще цифры в тестах очень большие

                                На всякий случай отмечу, что это цифры из продакшена.

                                Плюс у меня пара вопросов:
                                1. Как и чем вы измеряли общее время запроса (коннект+индекс+запрос)?
                                2. Доступ к «машине трёхлетней давности» осуществлялся с удалённого хоста или локального?
                                Я вот почему про это спрашиваю — различие в 10 раз по скорости это, скорее всего, по причине различия способов измерения или условий работы с HS.
                                0
                                заменив его хорошо зарекомендовавшей себя “master-master” репликацией между площадками

                                Меня интересуют два вопроса:
                                1. как вы делаете «master-master» репликацию между mysql базами?
                                2. как синхронизируются дынные между территориально удаленными серверами?

                                Можно об этом рассказать или хотя бы дать источники. Сколько я этим не интересовался. Везде говорили, что «master-master» в mysql дает кашу в данных. А на территориально удаленных серверах есть задержка.

                                  +1
                                  1. в реплицируемой таблице есть автоинкрементный праймари.
                                  на одном сервере:
                                  auto_increment_increment = 2
                                  auto_increment_offset = 1
                                  на другом:
                                  auto_increment_increment = 2
                                  auto_increment_offset = 2
                                  соответственно в первом получаем нечетные, а во втором четные записи и никакой каши.
                                  2. наверное, я не понял вопроса. Синхронизируем именно «мастер-мастер» репликацией. Задержку в доли секунды специально не замеряли (красивых графиков ради), т.к. на фоне старого решения это однозначно на порядки быстрее.
                                    0
                                    Ответ тут
                                      0
                                      Мне вот что интересно — у вас синхронная репликация получилась на МайЭскуэеле?
                                    +1
                                    1. А я думал, что такой способ не используют в крупных проектах. Часто вижу комментарии, где это называют костылем.

                                    С вопросом я немного не так выразился. Как я понял в данном случае базы синхронизируются на прямую, как бы они это делали и в условиях расположения на одной стойке.

                                    А как вообще синхронизируются данные (базы, файлы) между дата центрами? Ведь, например, знать о определенном юзере (его аватарке, настройках и.т.д) порой нужно в обоих центрах. Да и запись в них тоже по идее может быть из любого места. Что будет при одновременной записи в двух местах.
                                      +3
                                      В общих чертах система межплощадочной репликации выглядит так:
                                      1. Все запросы, изменяющие данные на шардах, логгируются.
                                      2. Периодически (минимум раз в минуту) все залоггированные на шардах запросы собираются, упаковываются и пересылаются на другую (другие) площадки.
                                      3. На площадке-получателе запросы применяются к соответствующим бэкап-шардам.

                                      Данная система репликации написана на PHP, очень проста и надёжно работает на протяжении нескольких лет, обеспечивая минимальную задержку между обновлением данных на мастер-шарде и его репликах. Да, мы очень любим PHP и умеем хорошо с его помощью делать даже те задачи, которые, как некоторым кажется, с его помощью делать нельзя / не получится.

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                    Самое читаемое