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

Історія створення ВКонтакті є у Вікіпедії, її розповідав сам Павло. Здається, її знають уже всі. Про нутрощі, архітектуру та влаштування сайту на HighLoad++ Павло розповідав ще 2010 року. Багато серверів вибігло з того часу, тому ми оновимо інформацію: препаруємо, витягнемо начинки, зважимо — подивимося на пристрій ВК з технічного погляду.

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

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



Понад чотири роки я займаюся всілякими завданнями, пов'язаними з бекендом.

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

За цей час я приклав руку до багатьох компонент сайту. Цим досвідом хочу поділитися.

Загальна архітектура

Все зазвичай починається з сервера або групи серверів, які приймають запити.

Front-сервер

Front-сервер приймає запити щодо HTTPS, RTMP та WSS.

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

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

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

Backend

За front зазвичай стоять backend-сервери. Вони обробляють запити, які одержує front-сервер від клієнтів.

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

розподіл навантаження

Всі наші бекенди - це не величезний пул машин, які можуть опрацьовувати будь-який запит. Ми їх поділяємо на окремі групи: general, mobile, api, video, staging… Проблема на окремій групі машин не вплине на всі інші. У разі проблем із відео користувач, який слухає музику, навіть не дізнається про проблеми. На який backend відправити запит, вирішує nginx на frontі конфігу.

Збір метрик та перебалансування

Щоб зрозуміти, скільки машин потрібно мати у кожній групі, ми не спираємось на QPS. Бекенди різні, у них різні запити, кожен запит має різну складність обчислення QPS. Тому ми оперуємо поняттям навантаження на сервер в цілому - на CPU та perf.

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

Сервер вмісту

CS або Content Server - це сховище. CS - це сервер, який зберігає файли, а також обробляє залиті файли, всілякі синхронні фонові завдання, які йому ставить основний веб-фронтенд.

Ми маємо десятки тисяч фізичних серверів, які зберігають файли. Користувачі люблять завантажувати файли, а ми любимо їх зберігати та роздавати. Частина цих серверів закрита спеціальними pu/pp серверами.

pu/pp

Якщо ви відкривали вкладку network у VK, то бачили pu/pp.

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

Що таке pu/pp? Якщо ми закриваємо один сервер за іншим, то є два варіанти віддачі та завантаження файлу на сервер, який був закритий: безпосередньо через http://cs100500.userapi.com/path або через проміжний сервер - http://pu.vk.com/c100500/path.

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

Ці сервери термінують HTTPS-сесії, щоб зняти процесорне навантаження зі сховища. Також, оскільки на цих серверах обробляються файли користувача, то чим менше чутливої ​​інформації зберігається на цих машинах, тим краще. Наприклад, ключі від шифрування HTTPS.

Так як машини закриті іншими нашими машинами, ми можемо дозволити собі не давати їм «білі» зовнішні IP, і даємо «сірі». Так заощадили на пулі IP і гарантовано захистили машини від доступу ззовні - просто немає IP, щоб на неї потрапити.

Відмовостійкість через загальні IP. У плані стійкості до відмови схема працює так само — кілька фізичних серверів мають загальний фізичний IP, і залізниця перед ними вибирає, куди відправити запит. Пізніше я розповім про інші варіанти.

Спірний момент у тому, що в цьому випадку клієнт тримає менше з'єднань. За наявності однакового IP на кілька машин — з однаковим хостом: pu.vk.com або pp.vk.com, браузер клієнта має обмеження на кількість одночасних запитів до одного хоста. Але під час повсюдного HTTP/2 я вважаю, що це вже не так актуально.

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

Нещодавно у нас з'явилася покращена версія proxy. Зараз розповім, чим вони відрізняються від звичайних, і навіщо це потрібно.

Sun

У вересні 2017 року компанія Oracle, яка до цього купила компанію Sun, звільнила величезну кількість співробітників Sun. Можна сміливо сказати, що у цей час компанія припинила своє існування. Вибираючи назву для нової системи, наші адміни вирішили віддати шану та пам'ять цієї компанії, і назвали нову систему Sun. Між собою ми називаємо її просто сонечки.

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

У pp було кілька проблем. Один IP на групу – неефективний кеш. Декілька фізичних серверів мають спільну 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.

Кеш

До нашої архітектурної схеми ми додаємо ще один вузол — середовище кешування.

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

Нижче схема розташування регіональних кешівїх приблизно 20 штук. Це місця, де стоять саме кеші та «сонячки», які можуть кешувати трафік через себе.

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

Це кешування мультимедіа-контенту, тут не зберігаються дані користувача — просто музика, відео, фото.

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

Як це працює?

Вважаємо популярність файлів по регіонах. Є номер регіонального кешу, де знаходиться користувач, і ідентифікатор файлу – беремо цю пару та інкрементуємо рейтинг при кожній завантаженні.

При цьому демони - сервіси в регіонах - час від часу приходять в API і кажуть: "Я кеш такий, дай мені список найпопулярніших файлів мого регіону, яких на мені ще немає". API віддає пачку файлів, відсортованих за рейтингом, демон їх викачує, відносить у регіони та звідти віддає файли. Це важлива відмінність pu/pp і Sun від кешів: ті віддають файл через себе відразу, навіть якщо в кеші цього файлу немає, а кеш спочатку викачує файл на себе, а потім уже починає його віддавати.

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

Але є проблеми. сервери кешів не гумові. Для суперпопулярного контенту іноді не вистачає мережі окремий сервер. Сервери кешів у нас 40-50 Гбіт/с, але буває контент, який повністю забиває такий канал. Ми йдемо до того, щоб реалізувати зберігання більш ніж однієї копії популярних файлів у регіоні. Сподіваюся, що до кінця року реалізуємо.

Ми розглянули загальну архітектуру.

  • Front-сервери, які приймають запити.
  • Бекенди, що обробляють запити.
  • Сховища, які закриті двома видами прокси.
  • Регіональні кеші.

Чого у цій схемі не вистачає? Звичайно, бази даних, в яких ми зберігаємо дані.

Бази даних або движки

Ми називаємо їх не базами, а двигунами - Engines, тому що баз даних у загальноприйнятому сенсі у нас майже немає.

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

Це вимушений захід. Так вийшло, тому що в 2008-2009 році, коли VK мав вибухове зростання популярності, проект повністю працював на MySQL і Memcache і були проблеми. MySQL любив впасти та зіпсувати файли, після чого не піднімався, а Memcache поступово деградував за продуктивністю, і доводилося його перезапускати.

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

Рішення виявилося успішним. Можливість це було, як і крайня необхідність, оскільки інших способів масштабування тоді не існувало. Не було купи баз, NoSQL ще не існував, були тільки MySQL, Memcache, PostrgreSQL і все.

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

Типи двигунів

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

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

У нас є окремий двигун memcached, що схожий на звичайний, але з купою плюшок, і який не гальмує. Чи не ClickHouse, але теж працює. Є окремо pmemcached - це персистентний memcached, який вміє зберігати дані ще й на диску, причому більше, ніж влазить в оперативну пам'ять, щоб не втрачати дані під час перезапуску. Є різноманітні движки під окремі завдання: черги, списки, сети – все, що потрібно нашому проекту.

Кластери

З погляду коду немає необхідності уявляти собі движки чи бази даних як процеси, сутності чи інстанції. Код працює саме з кластерами, з групами двигунів. один тип на кластер. Допустимо, є кластер memcached – це просто група машин.

Код взагалі не потрібно знати фізичне розташування, розмір і кількість серверів. Він ходить до кластера за певним ідентифікатором.

Щоб це працювало, потрібно додати ще одну сутність, яка знаходиться між кодом та двигунами. повноваження.

RPC-proxy

Proxy - сполучна шина, де працює практично весь сайт. При цьому у нас немає service discovery — замість нього є конфіг цього proxy, який знає розташування всіх кластерів та всіх шардів цього кластера. Цим займаються адміни.

Програмістам взагалі не важливо скільки, де і що коштує — вони просто ходять до кластера. Це нам багато що дозволяє. При отриманні запиту proxy перенаправляє запит, знаючи куди він сам це визначає.

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

При цьому proxy – точка захисту від відмови сервісу. Якщо якийсь двигун гальмує або впав, то proxy це розуміє і відповідає клієнтській стороні. Це дозволяє зняти таймаут - код не чекає відповіді движка, а розуміє, що він не працює і треба якось по-іншому поводитися. Код має бути готовим до того, що бази не завжди працюють.

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

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

Для MySQL, який у нас ще подекуди використовуємо db-proxy, а для ClickHouse - Kittenhouse.

Це працює загалом так. Є якийсь сервер, на ньому працює kPHP, Go, Python - взагалі будь-який код, який вміє ходити по нашому RPC-протоколу. Код ходить локально на RPC-proxy — кожному сервері, де є код, запущений свій локальний proxy. За запитом proxy розуміє, куди потрібно йти.

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

Якщо один двигун хоче піти в інший, навіть якщо це сусід, то йде через прокси, тому що сусід може стояти в іншому дата-центрі. Двигун не повинен зав'язуватися на знання розташування чогось, крім самого себе — це стандартне рішення. Але винятки, звичайно, бувають 🙂

Приклад 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 часто запитують.

UDP допомагає уникнути проблеми величезної кількості з'єднань між серверами. Якщо на кожному сервері стоїть RPC-proxy і він, у загальному випадку, може піти в будь-який двигун, то виходить десятки тисяч TCP-з'єднань на сервер. Навантаження є, але воно марне. У разі UDP цієї проблеми немає.

Ні надмірного TCP-handshake. Це типова проблема: коли піднімається новий движок або новий сервер, встановлюється відразу багато з'єднань TCP. Для невеликих легковажних запитів, наприклад, UDP payload, все спілкування коду з двигуном - це два UDP-пакети: один летить в один бік, другий в інший. Один round trip - і код отримав відповідь від движка без handshake.

Так, це все працює тільки при дуже невеликому відсотку втрат пакетів. У протоколі є підтримка для ретрансміттів, таймаутів, але якщо ми втрачатимемо багато, то отримаємо практично TCP, що не вигідно. Через океани UDP не ганяємо.

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

Персистентне зберігання даних

Двигуни пишуть бінлоги. Бінлог — це файлик, наприкінці якого дописується подія зміну стану чи даних. У різних рішеннях називається по-різному: binary log, WAL, AOFале принцип один.

Щоб двигун при перезапуску не перечитував весь бінлог за багато років, двигуни пишуть снапшоти - стан на поточний момент. За потреби вони читають спочатку з нього, а потім дочитують уже з бінлогу. Усі бінлоги пишуться в однаковому бінарному форматі — за TL-схемою, щоб адміни могли їх своїми інструментами однаково адмініструвати. Для снапшотів такої потреби немає. Там є загальний заголовок, який вказує чий снапшот - int, magic движка, а яке тіло - нікому не важливо. Це проблема движка, який записав снапшот.

Бігло опишу принцип роботи. Є сервер, на якому працює двигун. Він відкриває на запис новий порожній бінлог, пише у нього подію зміну.

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

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

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

У якийсь момент, коли двигун перезапустився, на диску буде і бінлог і снапшот. Двигун повністю читає снапшот, піднімає свій стан на певний момент.

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

Вираховує позицію, яка була на момент створення снапшота, та розмір бінлогу.

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

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

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

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

Ця ж схема використовується не просто для реплікації, але ще й для створення бекапів. У нас є движок - майстер, який пише в бінлог. У будь-якому іншому місці, куди налаштують адміни, піднімається копіювання цього бінлогу, і все — у нас є бекап.

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

Якщо потрібна читаюча репліка, щоб знизити навантаження на читання по CPU - просто піднімається движок, що читає кінець бінлогу і виконує ці команди в собі локально.

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

Шардування даних у RPC-proxy

Як працює шардування? Як proxy розуміє, який шард кластера відправити? Код не повідомляє: «Відправ на 15 шард!» - Ні, це робить proxy.

Найпростіша схема - firstint - Перше число в запиті.

get(photo100_500) => 100 % N.

Це приклад для простого memcached текстового протоколу, але, звичайно, запити бувають складні, структуровані. У прикладі береться перше число у запиті та залишок від поділу на розмір кластера.

Це корисно, коли хочемо мати локальність даних однієї сутності. Припустимо, 100 - користувач або ID групи, і ми хочемо, щоб для складних запитів всі дані однієї сутності були на одному шарді.

Якщо нам байдуже, як запити розмазані за кластером, є інший варіант — хешування шарда цілком.

hash(photo100_500) => 3539886280 % N

Також отримуємо хеш, залишок від поділу та номер шарда.

Обидва ці варіанти працюють тільки якщо ми готові до того, що при нарощуванні розміру кластера ми дробитимемо або збільшуватимемо його в кратну кількість разів. Наприклад, у нас було 16 шардів, нам не вистачає, хочемо більше – можна безболісно отримати 32 без даунтайму. Якщо хочемо нарощувати не раз — буде даунтайм, тому що не вдасться акуратно передробити все без втрат. Ці варіанти корисні, але не завжди.

Якщо нам потрібно додавати або прибирати довільну кількість серверів, використовується консистентне хешування на кільці a la Ketama. Але при цьому повністю втрачаємо локальність даних, доводиться робити merge запиту на кластер, щоб кожен шматочок повернув свою маленьку відповідь, і вже об'єднувати відповіді на proxy.

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

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

Список

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

ring-buffer: prefix.idx = line

Є префікс ключа – ім'я лога, рядок, і є розмір цього лога – кількість рядків. Беремо випадкове число від 0 до числа рядків мінус 1. Ключ у memcache - це префікс сконтактенований з цим випадковим числом. У значення зберігаємо рядок лога та поточний час.

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

Для надійного зберігання логів у нас є двигун logs-engine. Саме для цього він і створювався, і широко використовується у величезній кількості кластерів. Найбільший відомий кластер зберігає 600 Тбайт запакованих логів.

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

Збір логів у ClickHouse

Ця схема показує, як ми ходимо в наші двигуни.

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

Є код, який RPC локально ходить в RPC-proxy, а той розуміє, куди піти в двигун. Якщо ми хочемо писати логи в ClickHouse, нам потрібно в цій схемі змінити дві частини:

  • замінити якийсь двигун на ClickHouse;
  • замінити RPC-proxy, який не вміє ходити в ClickHouse, на якесь рішення, яке вміє, причому по RPC.

З двигуном легко - замінюємо його на сервер або на кластер серверів з ClickHouse.

А щоб ходити в ClickHouse, ми зробили KittenHouse. Якщо ми підемо безпосередньо з KittenHouse в ClickHouse - він не впорається. Навіть без запитів, від HTTP-з'єднань величезної кількості машин він складається. Щоб схема працювала, на сервері з ClickHouse піднімається локальний reverse proxy, що написаний так, що витримує потрібні обсяги з'єднань. Також він може відносно надійно буферизувати дані у собі.

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

Іноді ми не хочемо реалізовувати RPC-схему в нестандартних рішеннях, наприклад, nginx. Тому в KittenHouse є можливість отримувати логи по UDP.

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

Якщо відправник та одержувач логів працюють на одній машині, то ймовірність втратити UDP-пакет у межах локального хоста досить низька. Як компроміс між необхідністю реалізувати RPC у сторонньому рішенні і надійністю, ми використовуємо просто відправку по UDP. До цієї схеми ми ще повернемось.

моніторинг

У нас два види логів: ті, які збирають адміністратори на своїх серверах і ті, які пишуть розробники з коду. Їм відповідають два типи метрик: системні та продуктові.

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

На всіх серверах у нас працює Нетдані, яка збирає статистику та відсилає її до 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 трильйона на добу. При цьому ми хочемо зберігати їх хоча б кілька років, щоб зрозуміти тенденції зміни метрик. Склеїти це все докупи — велика проблема, яку ми ще не вирішили. Розповім, як це працює останні кілька років.

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

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

За потреби ми можемо писати безпосередньо в logs-collectors.

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

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

Далі logs-collectors зливають статистику у meowDB — це наша база, котра ще й метрики вміє зберігати.

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

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

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

Експеримент

Влітку 2018 року у нас був внутрішній хакатон, і з'явилася ідея спробувати замінити червону частину схеми на щось, що може зберігати метрики в ClickHouse. У нас є логи на ClickHouse - чому б не спробувати?

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

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

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

Ми вирішили додати до схеми ще один «House», який прийматиме саме метрики у тому форматі, як їх пише наш код за UDP. Далі цей *House перетворює їх на inserts, як логи, які розуміє KittenHouse. Ці логі він вміє чудово доставляти до ClickHouse, який має їх вміти читати.

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

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

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

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

  • Тут є відправка з коду, який локально пишеться в StatsHouse.
  • StatsHouse пише в KittenHouse UDP-метрики, вже перетворені на SQL-inserts, пачками.
  • KittenHouse надсилає їх у ClickHouse.
  • Якщо ми хочемо їх прочитати, то читаємо вже в обхід StatsHouse - безпосередньо з ClickHouse звичайними SQL.

Це все ще експериментале нам подобається, що виходить. Якщо виправимо проблеми схеми, то можливо повністю перейдемо на неї. Особисто я сподіваюся.

Схема не заощаджує залізо. Потрібно менше серверів, не потрібні локальні stats-daemons та logs-collectors, але ClickHouse вимагає сервера жирніше, ніж ті, що стоять на поточній схемі. Серверів потрібно менше, але вони мають бути дорожчими і потужнішими.

Деплой

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

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

Ми також багато деплоїмо kPHP і в нього теж є власна розробка на мерзотник за схемою вище. Так як це бінарник HTTP-сервера, то ми не можемо виробляти diff – релізний бінарник важить сотні Мбайт. Тому тут варіант інший - версія записується в binlog copyfast. З кожним білдом вона інкрементується і при відкаті вона теж збільшується. Версія реплікується на сервери. Локальні copyfast'и бачать, що в binlog потрапила нова версія, і тим самим gossip replication забирають собі свіжу версію бінарника, не стомлюючи наш майстер-сервер, а акуратно розмазуючи навантаження по мережі. Далі слідує graceful перезапуск на нову версію.

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

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

Різниця в тому, що бінарник у нас запаковується до архівів. дебютантка, і при викачуванні вони dpkg -i ставляться на систему. Чому у нас kPHP деплоїться бінарником, а движки - dpkg? Так склалось. Працює – не чіпаємо.

Корисні посилання:

Олексій Акулович один із тих, хто у складі Програмного комітету допомагає PHP Ukrainian вже 17 травня стати наймасштабнішою за останній час подією для PHP-розробників. Подивіться, який у нас крутий ПК, які спікери (Двоє з них розробляють ядро ​​PHP!) - Здається, що якщо ви пишите на PHP, це не можна пропустити.

Джерело: habr.com

Додати коментар або відгук