Хабрахабр

FAQ по архитектуре и работе ВКонтакте

История создания ВКонтакте есть в Википедии, её рассказывал сам Павел. Кажется, что ее знают уже все. Про внутренности, архитектуру и устройство сайта на HighLoad++ Павел рассказывал еще в 2010 году. Много серверов утекло с тех пор, поэтому мы обновим информацию: препарируем, вытащим внутренности, взвесим — посмотрим на устройство ВК с технической точки зрения.

Расшифровка этого доклада — собирательный ответ на часто задаваемые вопросы про работу платформы, инфраструктуры, серверов и взаимодействия между ними, но не про разработку, а именно про железо. Алексей Акулович (AterCattus) бэкенд-разработчик в команде ВКонтакте. Подробности под катом. Отдельно — про базы данных и то, что вместо них у ВК, про сбор логов и мониторинг всего проекта в целом.

Больше четырех лет я занимаюсь всевозможными задачами, связанными с бэкендом.

  • Загрузка, хранение, обработка, раздача медиа: видео, live стриминг, аудио, фото, документы.
  • Инфраструктура, платформа, мониторинг со стороны разработчика, логи, региональные кэши, CDN, собственный протокол RPC.
  • Интеграция с внешними сервисами: рассылки пушей, парсинг внешних ссылок, лента RSS.
  • Помощь коллегам по разным вопросам, за ответами на которые приходится погружаться в неизвестный код.

За это время я приложил руку к многим компонентам сайта. Этим опытом и хочу поделиться.

Общая архитектура

Все, как обычно, начинается с сервера или группы серверов, которые принимают запросы.

Front-сервер

Front-сервер принимает запросы по HTTPS, RTMP и WSS.

У нас есть прием RTMP-трафика для Live-трансляций с отдельными front-серверами и WSS-соединения для Streaming API. HTTPS — это запросы для основной и мобильной веб-версий сайта: vk.com и m.vk.com, и другие официальные и неофициальные клиенты нашего API: мобильные клиенты, мэссенджеры.

Для RTMP трансляций мы недавно перешли на собственное решение kive, но оно за пределами доклада. Для HTTPS и WSS на серверах стоит nginx. Для HTTPS и WSS эти же серверы занимаются шифровкой трафика, чтобы забирать часть нагрузки по CPU на себя. Эти серверы для отказоустойчивости анонсируют общие IP-адреса и выступают группами, чтобы в случае проблемы на одном из серверов, запросы пользователей не терялись.

Дальше не будем говорить про WSS и RTMP, а только про стандартные запросы HTTPS, которые обычно ассоциируются с веб-проектом.

Backend

За front обычно стоят backend-серверы. Они обрабатывают запросы, которые получает front-сервер от клиентов.

kPHP — это сервер, который работает по prefork-модели: запускает мастер-процесс, пачку дочерних процессов, передает им слушающие сокеты и они обрабатывают свои запросы. Это kPHP-серверы, на которых работает HTTP-демон, потому что HTTPS уже расшифрован. При этом процессы не перезапускаются между каждым запросом от пользователя, а просто сбрасывают свое состояние в первоначальное zero-value состояние — запрос за запросом, вместо перезапуска.

Распределение нагрузки

Все наши бэкенды — это не огромный пул машин, которые могут обработать любой запрос. Мы их разделяем на отдельные группы: general, mobile, api, video, staging… Проблема на отдельной группе машин, не повлияет на все остальные. В случае проблем с видео пользователь, который слушает музыку, даже не узнает о проблемах. На какой backend отправить запрос, решает nginx на front'е по конфигу.

Сбор метрик и перебалансировка

Чтобы понять, сколько машин нужно иметь в каждой группе, мы не опираемся на QPS. Бэкенды разные, у них разные запросы, у каждого запроса разная сложность вычисления QPS. Поэтому мы оперируем понятием нагрузки на сервер в целом — на CPU и perf.

На каждом физическом сервере запущена группа kPHP, чтобы утилизировать все ядра (потому что kPHP однопоточные). Таких серверов у нас тысячи.

Content Server

CS или Content Server — это хранилище. CS — это сервер, который хранит файлы, а также обрабатывает залитые файлы, всевозможные фоновые синхронные задачи, которые ему ставит основной веб-фронтенд.

Пользователи любят загружать файлы, а мы любим их хранить и раздавать. У нас десятки тысяч физических серверов, которые хранят файлы. Часть этих серверов закрыты специальными pu/pp серверами.

pu/pp

Если вы открывали в VK вкладку network, то видели pu/pp.

Если мы закрываем один сервер за другой, то есть два варианта отдачи и загрузки файла на сервер, который был закрыт: напрямую через http://cs100500.userapi.com/path или через промежуточный серверhttp://pu.vk.com/c100500/path. Что такое pu/pp?

То есть один сервер, чтобы загружать фото, а другой — отдавать. Pu — это исторически сложившееся название для photo upload, а pp — это photo proxy. Теперь загружаются уже не только фотографии, но название сохранилось.

Также, так как на этих серверах обрабатываются пользовательские файлы, то чем меньше чувствительной информации хранится на этих машинах, тем лучше. Эти серверы терминируют HTTPS-сессии, чтобы снять процессорную нагрузку с хранилища. Например, ключи от шифрования HTTPS.

Так сэкономили на пуле IP и гарантированно защитили машины от доступа извне — просто нет IP, чтобы на неё попасть. Так как машины закрыты другими нашими машинами, то мы можем позволить себе не давать им «белые» внешние IP, и даем «серые».

В плане отказоустойчивости схема работает так же — несколько физических серверов имеют общий физический IP, и железка перед ними выбирает, куда отправить запрос. Отказоустойчивость через общие IP. Позже я расскажу о других вариантах.

При наличии одинакового IP на несколько машин — с одинаковым их хостом: pu.vk.com или pp.vk.com, браузер клиента имеет ограничение на количество одновременных запросов к одному хосту. Спорный момент в том, что в этом случае клиент держит меньше соединений. Но во время повсеместного HTTP/2, я считаю, что это уже не так актуально.

Так как мы прокачиваем трафик через машины, то пока не можем прокачивать по такой же схеме тяжелый трафик, например, видео. Явный минус схемы в том, что приходится прокачивать весь трафик, который идет в хранилище, через еще один сервер. Более легкий контент мы передаем через proxy. Его мы его передаем напрямую — отдельное прямое соединение для отдельных хранилищ именно для видео.

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

Sun

В сентябре 2017 компания Oracle, которая до этого купила компанию Sun, уволила огромное количество сотрудников Sun. Можно сказать, что в этот момент компания прекратила свое существование. Выбирая название для новой системы, наши админы решили отдать дань уважения и памяти этой компании, и назвали новую систему Sun. Между собой мы называем ее просто «солнышки».

Один IP на группу — неэффективный кэш. У pp было несколько проблем. Поэтому если разные пользователи приходят за одним и тем же файлом, то при наличии кэша на этих серверах, файлик оседает в кэше каждого сервера. Несколько физических серверов имеют общий IP-адрес, и нет возможности контролировать, на какой сервер придет запрос. Это очень неэффективная схема, но ничего нельзя было сделать.

Также по некоторым внутренним причинам у нас не было возможности ставить такие серверы в регионы. Как следствие — мы не можем шардировать контент, потому что не можем выбрать конкретный сервер этой группы — у них общий IP. Они стояли только в Санкт-Петербурге.

Теперь у нас anycast маршрутизация: dynamic routing, anycast, self-check daemon. С солнышками мы изменили систему выбора. Все настроено так, что в случае выпадения одного сервера трафик размазывается по остальным серверам той же группы автоматически. У каждого сервера свой собственный индивидуальный IP, но при этом общая подсеть. Теперь есть возможность выбрать конкретный сервер, нет избыточного кэширования, и надежность не пострадала.

Теперь мы можем себе позволить ставить машины разной мощности по необходимости, а также в случае временных проблем менять веса работающим «солнышкам» для уменьшения нагрузки на них, чтобы они «отдохнули» и снова заработали. Поддержка весов.

Забавная вещь, связанная с шардированием: обычно мы шардируем контент так, что разные пользователи идут за одним файлом через одно «солнышко», чтобы у них был общий кэш. Шардирование по id контента.

Это онлайн-викторина в live-трансляции, где ведущий задает вопросы, а пользователи отвечают в реальном времени, выбирая варианты. Недавно мы запустили приложение “Клевер”. К трансляции одновременно могут подключаться больше 100 тысяч человек. В приложении есть чат, где пользователи могут пофлудить. Если 100 тысяч человек приходит за одной аватаркой в одно «солнышко», то оно может иногда закатиться за тучку. Они все пишут сообщения, которые рассылаются всем участникам, вместе с сообщением приходит еще аватарка.

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

Sun изнутри

Реверс proxy на nginx, кэш либо в RAM, либо на быстрые диски Optane/NVMe. Пример: http://sun4-2.userapi.com/c100500/path — ссылка на «солнышко», которое стоит в четвертом регионе, второй сервер-группы. Он закрывает собой файл path, который физически лежит на сервере 100500.

Cache

В нашу архитектурную схему мы добавляем еще один узел — среду кэширования.

Это места, где стоят именно кэши и «солнышки», которые могут кэшировать трафик через себя. Ниже схема расположения региональных кэшей, их примерно 20 штук.

Это кэширование мультимедиа-контента, здесь не хранятся пользовательские данные — просто музыка, видео, фото.

В случае fallback у нас есть еще парсинг базы geoip, если мы не смогли найти IP по префиксам. Чтобы определить регион пользователя, мы собираем анонсированные в регионах BGP-префиксы сетей. В коде мы можем посмотреть один или несколько регионов пользователя — те точки, к которым он ближе всего географически. По IP пользователя определяем регион.

Как это работает?

Считаем популярность файлов по регионам. Есть номер регионального кэша, где находится пользователь, и идентификатор файла — берем эту пару и инкрементируем рейтинг при каждом скачивании.

API отдает пачку файлов, отсортированных по рейтингу, демон их выкачивает, уносит в регионы и оттуда отдает файлы. При этом демоны — сервисы в регионах — время от времени приходят в API и говорят: «Я кэш такой-то, дай мне список самых популярных файлов моего региона, которых на мне еще нет». Это принципиальное отличие pu/pp и Sun от кэшей: те отдают файл через себя сразу, даже если в кэше этого файла нет, а кэш сначала выкачивает файл на себя, а потом уже начинает его отдавать.

Например, только с московского кэша мы раздаем больше 1 Тбит/с в часы наибольшей нагрузки. При этом мы получаем контент ближе к пользователям и размазывание сетевой нагрузки.

Для суперпопулярного контента иногда не хватает сети на отдельный сервер. Но есть проблемы — серверы кэшей не резиновые. Мы идем к тому, чтобы реализовать хранение более одной копии популярных файлов в регионе. Серверы кэшей у нас 40-50 Гбит/с, но бывает контент, который забивает такой канал полностью. Надеюсь, что до конца года реализуем.

Мы рассмотрели общую архитектуру.

  • Front-серверы, которые принимают запросы.
  • Бэкенды, которые обрабатывают запросы.
  • Хранилища, которые закрыты двумя видами proxy.
  • Региональные кэши.

Чего в этой схеме не хватает? Конечно, баз данных, в которых мы храним данные.

Базы данных или движки

Мы называем их не базами, а движками — Engines, потому что баз данных в общепринятом смысле у нас практически нет.

Так получилось, потому что в 2008-2009 году, когда у VK был взрывной рост популярности, проект полностью работал на MySQL и Memcache и были проблемы. Это вынужденная мера. MySQL любил упасть и испортить файлы, после чего не поднимался, а Memcache постепенно деградировал по производительности, и приходилось его перезапускать.

В таких условиях развивать растущий проект тяжело. Получается, что в набирающем популярность проекте было персистентное хранилище, которое портит данные, и кэш, который тормозит. Было принято решение попробовать переписать критические вещи, в которые проект упирался, на собственные велосипеды.

Возможность это сделать была, как и крайняя необходимость, потому что других способов масштабирования в то время не существовало. Решение оказалось успешным. Не было кучи баз, NoSQL еще не существовал, были только MySQL, Memcache, PostrgreSQL — и все.

Разработку вела наша команда C-разработчиков и все было сделано единообразно. Универсальная эксплуатация. С ростом движков админам удобно эксплуатировать систему — нет зоопарка, который надо поддерживать, и заново учиться эксплуатировать каждую новую стороннюю базу, что позволяло быстро и удобно наращивать их количество. Независимо от движка, везде был примерно одинаковый формат файлов, записываемых на диск, одинаковые параметры запуска, одинаково обрабатывались сигналы и примерно одинаково вели себя в случае краевых ситуаций и проблем.

Типы движков

Команда написала довольно много движков. Вот лишь часть из них: friend, hints, image, ipdb, letters, lists, logs, memcached, meowdb, news, nostradamus, photo, playlists, pmemcached, sandbox, search, storage, likes, tasks, …

Почему бы и нет. Под каждую задачу, которая требует специфическую структуру данных или обрабатывает нетипичные запросы, C-команда пишет новый движок.

Не ClickHouse, но тоже работает. У нас есть отдельный движок memcached, который похож на обычный, но с кучей плюшек, и который не тормозит. Есть разнообразные движки под отдельные задачи: очереди, списки, сеты — все, что требуется нашему проекту. Есть отдельно pmemcached — это персистентный memcached, который умеет хранить данные еще и на диске, причем больше, чем влезает в оперативную память, чтобы не терять данные при перезапуске.

Кластеры

С точки зрения кода нет необходимости представлять себе движки или базы данных как некие процессы, сущности или инстанции. Код работает именно с кластерами, с группами движков — один тип на кластер. Допустим, есть кластер memcached — это просто группа машин.

Он ходит в кластер по некому идентификатору. Коду вообще не нужно знать физическое расположение, размер и количество серверов.

Чтобы это работало, требуется добавить еще одну сущность, которая находится между кодом и движками — proxy.

RPC-proxy

Proxy — связующая шина, на которой работает практически весь сайт. При этом у нас нет service discovery — вместо него есть конфиг этого proxy, который знает расположение всех кластеров и всех шардов этого кластера. Этим занимаются админы.

Это нам многое позволяет. Программистам вообще не важно, сколько, где и что стоит — они просто ходят в кластер. При получении запроса proxy перенаправляет запрос, зная куда — он сам это определяет.

Если какой-то движок тормозит или упал, то proxy это понимает и соответствующе отвечает клиентской стороне. При этом proxy — точка защиты от отказа сервиса. Код должен быть готов к тому, что базы не всегда работают. Это позволяет снять таймаут — код не ждет ответа движка, а понимает, что он не работает и надо как-то по-другому себя вести.

Специфичные реализации

Иногда мы все-таки очень хотим иметь какое-то нестандартное решение в качестве движка. При этом было принято решение, не использовать наш готовый rpc-proxy, созданный именно для наших движков, а сделать отдельный proxy под задачу.

Для MySQL, который у нас еще кое-где есть используем db-proxy, а для ClickHouse — Kittenhouse.

Есть некий сервер, на нем работает kPHP, Go, Python — вообще любой код, который умеет ходить по нашему RPC-протоколу. Это работает в целом так. По запросу proxy понимает куда нужно идти. Код ходит локально на RPC-proxy — на каждом сервере, где есть код, запущен свой локальный proxy.

Движок не должен завязываться на знание расположения чего-либо, кроме самого себя — у нас это стандартное решение. Если один движок хочет пойти в другой, даже если это сосед, то идет через proxy, потому что сосед может стоять в другом дата-центре. Но исключения, конечно, бывают 🙂

Пример TL-схемы, по которой работают все движки.

memcache.not_found = memcache.Value;
memcache.strvalue value:string flags:int = memcache.Value;
memcache.addOrIncr key:string flags:int delay:int value:long = memcache.Value; tasks.task fields_mask:# flags:int tag:%(Vector int) data:string id:fields_mask.0?long retries:fields_mask.1?int scheduled_time:fields_mask.2?int deadline:fields_mask.3?int = tasks.Task; tasks.addTask type_name:string queue_id:%(Vector int) task:%tasks.Task = Long;

Это бинарный протокол, ближайший аналог которого protobuf. В схеме заранее описаны опциональные поля, сложные типы — расширения встроенных скаляров, и запросы. Все работает по этому протоколу.

RPC over TL over TCP/UDP… UDP?

У нас есть RPC-протокол выполнения запросов движка, который работает поверх TL-схемы. Это все работает поверх TCP/UDP соединения. TCP — понятно, а зачем нам UDP часто спрашивают.

Если на каждом сервере стоит RPC-proxy и он, в общем случае, может пойти в любой движок, то получается десятки тысяч TCP-соединений на сервер. UDP помогает избежать проблемы огромного количества соединений между серверами. В случае UDP этой проблемы нет. Нагрузка есть, но она бесполезна.

Это типичная проблема: когда поднимается новый движок или новый сервер, устанавливается сразу много TCP-соединений. Heт избыточного TCP-handshake. Один round trip — и код получил ответ от движка без handshake. Для маленьких легковесных запросов, например, UDP payload, все общение кода с движком — это два UDP-пакета: один летит в одну сторону, второй в другую.

В протоколе есть поддержка для ретрансмиттов, таймаутов, но если мы будем терять много, то получим практически TCP, что не  выгодно. Да, это все работает только при очень небольшом проценте потерь пакетов. Через океаны UDP не гоняем.

В основном они однопоточные, чтобы работать максимально быстро без блокировок, и шардируются как однопоточные решения. Таких серверов у нас тысячи, и там та же самая схема: на каждый физический сервер ставится пачка движков . При этом у нас нет ничего более надежного, чем эти движки, и персистентному хранению данных уделяется очень большое внимание.

Персистентное хранение данных

Движки пишут бинлоги. Бинлог — это файлик, в конце которого дописывается событие на изменение состояния или данных. В разных решениях называется по-разному: binary log, WAL, AOF, но принцип один.

При необходимости они читают сначала из него, а потом дочитывают уже из бинлога. Чтобы движок при перезапуске не перечитывал весь бинлог за много лет, движки пишут снапшоты — состояние на текущий момент. Для снапшотов такой необходимости нет. Все бинлоги пишутся в одинаковом бинарном формате — по TL-схеме, чтобы админы могли их своими инструментами одинаково администрировать. Это проблема движка, который записал снапшот. Там есть общий заголовок, который указывает чей снапшот — int, magic движка, а какое тело — никому не важно.

Есть сервер, на котором работает движок. Бегло опишу принцип работы. Он открывает на запись новый пустой бинлог, пишет в него событие на изменение.

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

Движок читает полностью снапшот, поднимает свое состояние на определенный момент. В какой-то момент, когда движок перезапустился, на диске будет и бинлог и снапшот.

Вычитывает позицию, которая была на момент создания снапшота, и размер бинлога.

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

Репликация данных

В результате репликация данных у нас statement-based — мы пишем в бинлог не какие-нибудь изменения страниц, а именно запросы на изменения. Очень похоже на то, что приходит по сети, только чуть измененное.

У нас есть движок — пишущий мастер, который пишет в бинлог. Эта же схема используется не просто для репликации, но еще и для создания бэкапов. В каком угодно другом месте, куда настроят админы, поднимается копирование этого бинлога, и все — у нас есть бэкап.

Если нужна читающая реплика, чтобы снизить нагрузку на чтение по CPU — просто поднимается читающий движок, который дочитывает конец бинлога и выполняет эти команды в себе локально.

Отставание здесь очень маленькое, и есть возможность узнать, насколько реплика отстает от мастера.

Шардирование данных в RPC-proxy

Как работает шардирование? Как proxy понимает, на какой шард кластера отправить? Код не сообщает : «Отправь на 15 шард!» — нет, это делает proxy.

Самая простая схема — firstint — первое число в запросе.

get(photo100_500) => 100 % N.

В примере берется первое число в запросе и остаток от деления на размер кластера. Это пример для простого memcached текстового протокола, но, конечно, запросы бывают сложные, структурированные.

Допустим, 100 — юзер или ID группы, и мы хотим, чтобы для сложных запросов все данные одной сущности были на одном шарде. Это полезно, когда мы хотим иметь локальность данных одной сущности.

Если нам все равно, как запросы размазаны по кластеру, есть другой вариант — хеширование шарда целиком.

hash(photo100_500) => 3539886280 % N

Также получаем хеш, остаток от деления и номер шарда.

Например, у нас было 16 шардов, нам не хватает, хотим больше — можно безболезненно получить 32 без даунтайма. Оба этих варианта работают только если мы готовы к тому, что при наращивании размера кластера мы будем дробить или увеличивать его в кратное количество раз. Эти варианты полезны, но не всегда. Если хотим наращивать не кратно — будет даунтайм, потому что не получится аккуратно передробить все без потерь.

Но при этом мы полностью теряем локальность данных, приходится делать merge запроса на кластер, чтобы каждый кусочек вернул свой маленький ответ, и уже объединять ответы на proxy. Если нам нужно добавлять или убирать произвольное количество серверов, используется консистентное хеширование на кольце a la Ketama.

Это выглядит так: RPC-proxy получает запрос, определяет, в какой кластер пойти и определяет шард. Есть супер-специфические запросы. Этим всем занимается proxy. Тогда есть либо пишущие мастеры, либо, если кластер имеет поддержку реплик, он отсылает в реплику по запросу.

Логи

Мы пишем логи несколькими способами. Самый очевидный и простой — пишем логи в memcache.

ring-buffer: prefix.idx = line

Берем случайное число от 0 до числа строк минус 1. Есть префикс ключа — имя лога, строка, и есть размер этого лога — количество строчек. В значение сохраняем строчку лога и текущее время. Ключ в memcache — это префикс сконкатенированный с этим случайным числом.

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

Именно для этого он и создавался, и широко используется в огромном количестве кластеров. Для надежного хранения логов у нас есть движок logs-engine. Самый большой известный мне кластер хранит 600 Тбайт запакованных логов.

С ним есть проблемы, которые мы пытаемся решить, например, начали активно использовать ClickHouse для хранения логов. Движок очень старый, есть кластеры, которым уже по 6–7 лет.

Сбор логов в ClickHouse

Эта схема показывает, как мы ходим в наши движки.

Если мы хотим писать логи в ClickHouse, нам нужно в этой схеме поменять две части: Есть код, который по RPC локально ходит в RPC-proxy, а тот понимает, куда пойти в движок.

  • заменить какой-то движок на ClickHouse;
  • заменить RPC-proxy, который не умеет ходить в ClickHouse, на какое-то решение, которое умеет, причем по RPC.

С движком просто — заменяем его на сервер или на кластер серверов с ClickHouse.

Если мы пойдем напрямую из KittenHouse в ClickHouse — он не справится. А чтобы ходить в ClickHouse, мы сделали KittenHouse. Чтобы схема работала, на сервере с ClickHouse поднимается локальный reverse proxy, который написан так, что выдерживает нужные объемы соединений. Даже без запросов, от HTTP-соединений огромного количества машин он складывается. Также он может относительно надежно буферизировать данные в себе.

Поэтому в KittenHouse есть возможность получать логи по UDP. Иногда мы не хотим реализовывать RPC-схему в нестандартных решениях, например, в nginx.

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

Мониторинг

У нас два вида логов: те, которые собирают администраторы по своим серверам и те, которые пишут разработчики из кода. Им соответствуют два типа метрик: системные и продуктовые.

Системные метрики

На всех серверах у нас работает Netdata, которая собирает статистику и отсылает ее в Graphite Carbon. Поэтому как система хранения используется ClickHouse, а не Whisper, например. При необходимости можно напрямую читать из ClickHouse, или использовать Grafana для метрик, графиков и отчетов. Как разработчикам, доступа к Netdata и Grafana нам хватает.

Продуктовые метрики

Для удобства мы написали много всего. Например, есть набор обычных функций, которые позволяют записать Counts, UniqueCounts значения в статистику, которые отсылаются куда-то дальше.

statlogsCountEvent ( ‘stat_name’, $key1, $key2, …)
statlogsUniqueCount ( ‘stat_name’, $uid, $key1, $key2, …)
statlogsValuetEvent ( ‘stat_name’, $value, $key1, $key2, …) $stats = statlogsStatData($params)

Впоследствии мы можем использовать фильтры сортировки, группировки и сделать все, что хотим от статистики — построить графики, настроить Watсhdogs.

При этом мы хотим хранить их хотя бы пару лет, чтобы понимать тенденции изменения метрик. Мы пишем очень много метрик, количество событий от 600 миллиардов до 1 триллиона в сутки. Расскажу, как это работает последние несколько лет. Склеить это все воедино — большая проблема, которую мы еще не решили.

Один раз в небольшой промежуток времени локально запущенный stats-daemon собирает все записи. У нас есть функции, которые пишут эти метрики в локальный memcache, чтобы уменьшить количество записей. Дальше демон сливает метрики в два слоя серверов logs-collectors, которые агрегирует статистику с кучи наших машин, чтобы слой за ними не умирал.

По необходимости мы можем писать напрямую в logs-collectors.

Решение подойдет, только если по какой-то причине на машине мы не можем поднять memcache stats-daemon, либо он упал, и мы пошли напрямую. Но запись из кода напрямую в коллекторы в обход stas-daemom — плохо масштабируемое решение, потому что увеличивает нагрузку на collector.

Дальше logs-collectors сливают статистику в meowDB — это наша база, которая еще и метрики умеет хранить.

Потом из кода можем бинарным «около-SQL» производить выборки.

Эксперимент

Летом 2018 у нас был внутренний хакатон, и появилась идея попробовать заменить красную часть схемы на что-то, что может хранить метрики в ClickHouse. У нас есть логи на ClickHouse — почему бы не попробовать?

У нас была схема, которая писала логи через KittenHouse.

Дальше этот *House превращает их в inserts, как логи, которые понимает KittenHouse. Мы решили добавить в схему еще один «*House», который будет принимать именно метрики в том формате, как их пишет наш код по UDP. Эти логи он умеет прекрасно доставлять до ClickHouse, который должен их уметь читать.

Схема с memcache, stats-daemon и logs-collectors базы заменяется на такую.

Схема с memcache, stats-daemon и logs-collectors базы заменяется на такую.

  • Здесь есть отправка из кода, которая локально пишется в StatsHouse.
  • StatsHouse пишет в KittenHouse UDP-метрики, уже превращенные в SQL-inserts, пачками.
  • KittenHouse отсылает их в ClickHouse.
  • Если мы хотим их прочитать, то читаем уже в обход StatsHouse — напрямую из ClickHouse обычными SQL.

Это все еще эксперимент, но нам нравится, что получается. Если исправим проблемы схемы, то, возможно, полностью перейдем на нее. Лично я на это надеюсь.

Нужно меньше серверов, не нужны локальные stats-daemons и logs-collectors, но ClickHouse требует сервера жирнее, чем те, что стоят на текущей схеме. Схема не экономит железо. Серверов нужно меньше, но они должны быть дороже и мощнее.

Деплой

Сначала посмотрим на деплой PHP. Разработку ведем в git: используем GitLab и TeamCity для деплоя. Ветки разработчиков вливаются в мастер-ветку, из мастера для тестирования вливаются в стейджинг, из стейджинга — в продакшн.

Это изменение записывается в binlog специального движка copyfast, который может быстро реплицировать изменения на весь наш парк серверов. Перед деплоем берутся текущая ветка продакшна и предыдущая,  в них считается diff файлов — изменение: создан, удален, изменен. Это позволяет обновлять код за десятки и единицы секунд на всем парке. Здесь используется не копирование напрямую, а gossip replication, когда один сервер рассылает изменения ближайшим соседям, те — своим соседям, и так далее. По этой же схеме производится и откат. Когда изменение доезжает до локальной реплики, она применяет эти патчи на своей локальной файловой системе.

Так как это бинарник HTTP-сервера, то мы не можем производить diff — релизный бинарник весит сотни Мбайт. Мы также много деплоим kPHP и у него тоже есть собственная разработка на git по схеме выше. С каждым билдом она инкрементируется, и при откате она тоже увеличивается. Поэтому здесь вариант другой — версия записывается в binlog copyfast. Локальные copyfast’ы видят, что в binlog попала новая версия, и тем же самым gossip replication забирают себе свежую версию бинарника, не утомляя наш мастер-сервер, а аккуратно размазывая нагрузку по сети. Версия реплицируется на серверы. Дальше следует graceful перезапуск на новую версию.

Для наших движков, которые тоже по сути бинарники, схема очень похожа:

  • git master branch;
  • бинарник в .deb;
  • версия записывается в binlog copyfast;
  • реплицируется на серверы;
  • сервер вытягивает себе свежий .dep;
  • dpkg -i;
  • graceful перезапуск на новую версию.

Разница в том, что бинарник у нас запаковывается в архивы .deb, и при выкачивании они dpkg -i ставятся на систему. Почему у нас kPHP деплоится бинарником, а движки — dpkg? Так сложилось. Работает — не трогаем.

Полезные ссылки:

Посмотрите, какой у нас крутой ПК, какие спикеры (двое из них разрабатывают ядро PHP!) — кажется, что если вы пишите на PHP, это нельзя пропустить. Алексей Акулович один из тех, кто в составе Программного комитета помогает PHP Russia уже 17 мая стать самым масштабным за последнее время событием для PHP-разработчиков.

Показать больше

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»