One-cloud - ОС рівня дата-центру в Однокласниках

One-cloud - ОС рівня дата-центру в Однокласниках

Алоха, піпл! Мене звуть Олег Анастасьєв, я працюю в Однокласниках у команді Платформи. А окрім мене, в Однокласниках працює купа заліза. У нас є чотири ЦОДи, у них близько 500 стійок з більш ніж 8 тисячами серверів. У певний момент ми зрозуміли, що впровадження нової системи управління дозволить нам більш ефективно завантажити техніку, полегшити управління доступами, автоматизувати (пере)розподіл обчислювальних ресурсів, прискорити запуск нових сервісів, прискорити реакції на масштабні аварії.

Що ж із цього вийшло?

Окрім мене та купи заліза є ще люди, які з цим залізом працюють: інженери, які знаходяться безпосередньо у дата-центрах; мережевики, які налаштовують мережне забезпечення; адміни, або SRE, які забезпечують стійкість до відмови інфраструктури; та команди розробників, кожна з них відповідає за частину функцій порталу. Створюваний ними софт працює якось так:

One-cloud - ОС рівня дата-центру в Однокласниках

Запити користувачів надходять як на фронти основного порталу www.ok.ru, і інші, наприклад на фронти API музики. Вони для обробки бізнес-логіки викликають сервер додатків, який при обробці запиту викликає необхідні спеціалізовані мікросервіси - one-graph (граф соціальних зв'язків), user-cache (кеш профілів користувача) і т.п.

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

Чому так? Такий підхід мав кілька плюсів:

  • Полегшується масове управління. Припустимо, завдання потребує якихось бібліотек, якихось налаштувань. І тоді сервер приписується рівно до однієї певної групи, описується політика cfengine для цієї групи (або вже описана), і ця конфігурація централізовано і автоматично розкочується на всі сервери цієї групи.
  • Спрощується діагностика. Допустимо, ви дивитеся на підвищене навантаження центрального процесора і розумієте, що це навантаження могло згенерувати тільки те завдання, яке працює на цьому залізному процесорі. Пошуки винного закінчуються дуже швидко.
  • Спрощується моніторинг. Якщо з сервером щось не так, монітор про це повідомляє і ви точно знаєте, хто винен.

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

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

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

Усвідомивши, що це, ми вирішили порахувати, наскільки ефективно використовуємо стійки.
Взяли ціну найпотужнішого сервера з економічно виправданих, підрахували, скільки таких серверів можемо помістити у стійки, скільки завдань ми б на них запустили, виходячи зі старої моделі «один сервер = одне завдання» і наскільки такі завдання змогли б утилізувати обладнання. Порахували — розплакалися. Виявилося, що ефективність використання стійок у нас близько 11%. Висновок очевидний: необхідно підвищувати ефективність використання дата-центрів. Здавалося б, рішення очевидне: треба одному сервері запускати відразу кілька завдань. Але тут починаються складнощі.

Масова конфігурація різко ускладнюється – тепер неможливо призначити серверу якусь одну групу. Адже тепер на одному сервері може бути запущено кілька завдань різних команд. Крім того, конфігурація може бути конфліктуючою для різних програм. Діагностика теж ускладнюється: якщо ви бачите підвищене споживання процесорів або дисків на сервері, то не знаєте, яке завдання доставляє неприємності.

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

One-cloud - ОС рівня дата-центру в Однокласниках

Очевидно, потрібно запускати завдання або у контейнерах, або у віртуальних машинах. Оскільки практично всі завдання у нас запускаються під керуванням однієї ОС (Linux) або адаптовані під неї, підтримувати безліч різних операційних систем нам не потрібно. Відповідно, віртуалізація не потрібна, через додаткові накладні витрати вона буде менш ефективна, ніж контейнеризація.

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

Плюс, готовий реєстр та тегування образів у Docker дають нам готові примітиви для версіонування та доставки коду у production.

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

Безпосередній запуск контейнерів на серверах це лише частина проблем. Інша частина пов'язана із розміщенням контейнерів на серверах. Потрібно зрозуміти, який контейнер, на який сервер можна поставити. Це не таке просте завдання, тому що контейнери треба розмістити на серверах якомога щільніше, при цьому не зменшивши швидкість їх роботи. Таке розміщення може бути складним і з погляду стійкості до відмови. Часто ми хочемо розміщувати репліки одного і того ж сервісу у різних стійках або навіть у різних залах дата-центру, щоб при відмові стійки чи зали ми не втрачали відразу всі репліки сервісу.

Розподіляти контейнери вручну — не варіант, коли ти маєш 8 тисяч серверів і 8—16 тисяч контейнерів.

Крім того, ми хотіли дати розробникам більше самостійності у розподілі ресурсів, щоб вони могли самі розміщувати свої сервіси на production без допомоги адміністратора. При цьому ми хотіли зберегти контроль, щоб якийсь другорядний сервіс не спожив усі ресурси наших дата-центрів.

Очевидно, що потрібен керуючий шар, який займався б цим автоматично.

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

One-cloud - ОС рівня дата-центру в Однокласниках

one-cloud masters - відмовостійкий кластер, що відповідає за оркестрацію хмари. Розробник відправляє в майстер маніфест, де міститься вся необхідна для розміщення сервісу інформація. Майстер на її основі дає команди вибраним міньйонам (машинам, призначеним для запуску контейнерів). На міньйонах є наш агент, який отримує команду, віддає свої команди Docker, а Docker конфігурує linux kernel для запуску відповідного контейнера. Окрім виконання команд, агент безперервно повідомляє майстра про зміни стану як машини-міньйону, так і запущених на ній контейнерів.

Розподіл ресурсів

А тепер розберемося із завданням складнішого розподілу ресурсів для безлічі міньйонів.

Обчислювальний ресурс у one-cloud - це:

  • Обчислювальна потужність процесора, споживана конкретним завданням.
  • Об'єм пам'яті, доступний задачі.
  • Мережевий трафік. Кожен з міньйонів має конкретний мережевий інтерфейс з обмеженою пропускною здатністю, тому не можна розподіляти завдання без урахування об'єму даних, що передається ними по мережі.
  • Диски. Крім, очевидно, місця під ці завдання ми також виділяємо тип диска: HDD чи SSD. Диски можуть обслуговувати кінцеву кількість запитів на секунду – IOPS. Тому для завдань, що генерують більше IOPS, ніж може обслужити один диск, ми також виділяємо "шпинделі" - тобто дискові пристрої, які необхідно зарезервувати виключно під завдання.

Тоді для якогось сервісу, наприклад для user-cache, ми можемо записати споживані ресурси у такий спосіб: 400 процесорних ядер, 2,5 Tб пам'яті, 50 Гбіт/с трафіку в обидві сторони, 6 Тб місця на HDD, розміщеного на 100 шпинделях . Або у більш звичній нам формі так:

alloc:
    cpu: 400
    mem: 2500
    lan_in: 50g
    lan_out: 50g
    hdd:100x6T

Ресурси сервісу user-cache споживають лише частину всіх доступних ресурсів у production-інфраструктурі. Тому хочеться зробити так, щоб раптово, через помилку оператора чи ні, user-cache не спожив більше ресурсів, ніж йому виділено. Тобто ми маємо лімітувати ресурси. Але чого ми могли б прив'язати квоту?

Повернемося до нашої спрощеної схеми взаємодії компонентів і перемалюємо з більшою кількістю деталей — ось так:

One-cloud - ОС рівня дата-центру в Однокласниках

Що впадає у вічі:

  • Веб-фронтенд та музика використовують ізольовані кластери одного і того ж сервера додатків.
  • Можна виділити логічні шари, до яких належать ці кластери: фронти, кеші, шар зберігання та управління даними.
  • Фронтенд неоднорідний, це різні багатофункціональні підсистеми.
  • Кеші також можна розкидати по підсистемі, дані якої вони кешують.

Ще раз перемалюємо картинку:

One-cloud - ОС рівня дата-центру в Однокласниках

Ба! Та ми бачимо ієрархію! А значить, можна розподіляти ресурси більшими шматками: призначити відповідального розробника на вузол цієї ієрархії, що відповідає функціональній підсистемі (як «music» на картинці), і до цього рівня ієрархії прив'язати квоту. Така ієрархія також дозволяє нам гнучкіше організовувати послуги для зручності управління. Наприклад, все web, оскільки це дуже велике угруповання серверів, ми поділяємо на кілька дрібніших груп, показаних на картинці як group1, group2.

Забравши зайві лінії, ми можемо записати кожен вузол нашої картинки в більш плоскому вигляді: group1.web.front, api.music.front, user-cache.cache.

Так ми приходимо до поняття «ієрархічна черга». Вона має ім'я, як «group1.web.front». На неї призначається квота на ресурси та права користувачів. Людині з DevOps ми дамо права на відправку сервісу в чергу, і такий співробітник може запускати щось у черзі, а людині з OpsDev — адмінські права, і тепер вона може керувати чергою, призначати туди людей, надавати цим людям права тощо. Сервіси, що запускаються в цій черзі, виконуватимуться в рамках квоти черги. Якщо обчислювальної квоти черги недостатньо для одноразового виконання всіх сервісів, всі вони виконуватимуться послідовно, формуючи в такий спосіб черга.

Розглянемо послуги докладніше. У сервісу є повне ім'я, яке завжди включає ім'я черги. Тоді сервіс web фронту матиме ім'я ok-web.group1.web.front. А обслуговування сервера додатків, до якого він звертається, буде називатися ok-app.group1.web.front. У кожного сервісу є маніфест, в якому вказується вся необхідна інформація для розміщення на конкретних машинах: скільки ресурсів споживає це завдання, яка для неї необхідна конфігурація, скільки реплік має бути, властивості обробки відмов цього сервісу. І після розміщення сервісу безпосередньо на машинах з'являються його екземпляри. Вони теж називаються однозначно - як номер екземпляра та ім'я сервісу: 1.ok-web.group1.web.front, 2.ok-web.group1.web.front, …

Це дуже зручно: дивлячись тільки на ім'я запущеного контейнера, ми одразу можемо багато з'ясувати.

А тепер ближче познайомимося з тим, що ці екземпляри, власне, виконують: із завданнями.

Класи ізоляції завдань

Всі завдання в ОК (та й, напевно, скрізь) можна поділити на групи:

  • Завдання з короткою затримкою - prod. Для таких завдань і сервісів дуже важливою є затримка відповіді (latency), як швидко кожен із запитів буде оброблений системою. Приклади завдань: web-фронти, кеші, сервери додатків, OLTP сховища тощо.
  • Завдання розрахункові - batch. Тут швидкість обробки кожного конкретного запиту не має значення. Їх важливо, скільки всього обчислень за певний (великий) проміжок часу це завдання зробить (throughput). Такими будуть будь-які завдання MapReduce, Hadoop, машинне навчання, статистика.
  • Завдання фонові.. Для таких завдань не дуже важливі ані величезне, ані черезпочаток. Сюди входять різні тести, міграції, перерахунки, конвертації даних із одного формату до іншого. З одного боку, вони схожі на розрахункові, з іншого — нам не дуже важливо, як швидко вони завершаться.

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

Завдання із короткою затримкою. У такої задачі патерн споживання ЦП буде схожий на цей:

One-cloud - ОС рівня дата-центру в Однокласниках

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

Щоб гарантувати мінімальну затримку для такого завдання, ми повинні взяти максимум споживаних нею ресурсів і зарезервувати потрібну кількість ядер на міньйоні (машині, яка виконуватиме завдання). Тоді формула резервації для нашого завдання виявиться такою:

alloc: cpu = 4 (max)

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

Розрахункові завдання. У них патерн буде дещо іншим:

One-cloud - ОС рівня дата-центру в Однокласниках

Середнє споживання ресурсів процесора таких завдань досить високе. Часто ми хочемо, щоб розрахункове завдання виконувалося за певний час, тому потрібно зарезервувати мінімальну кількість процесорів, яку їй необхідно, щоб весь розрахунок закінчився за прийнятний час. Її формула резервування виглядатиме так:

alloc: cpu = [1,*)

"Розмісти, будь ласка, на міньйоні, де є хоча б одне вільне ядро, а далі скільки є - все зжере".

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

One-cloud - ОС рівня дата-центру в Однокласниках

Але як це зробити?

Для початку розберемося з prod та його alloc: cpu = 4. Нам потрібно зарезервувати чотири ядра. У Docker run це можна зробити двома способами:

  • За допомогою опції --cpuset=1-4, тобто виділити задачі чотири певні ядра на машині.
  • використовувати --cpuquota=400_000 --cpuperiod=100_000, Призначити квоту на процесорний час, тобто вказати, що кожні 100 мс реального часу завдання споживає не більше 400 мс процесорного часу. Виходять ті самі чотири ядра.

Але який із цих способів підійде?

Досить привабливо виглядає cpuset. У задачі чотири виділені ядра, отже, процесорні кеші працюватимуть максимально ефективно. У цього є й зворотний бік: нам довелося б взяти на себе завдання розподілом обчислень по незавантаженим ядрам машини замість ОС, а це досить нетривіальне завдання, особливо якщо ми спробуємо розміщувати на такій машині batch-завдання. Тести показали, що тут найкраще підходить варіант із квотою: так у операційної системи більше свободи у виборі ядра для виконання завдання в даний момент і процесорний час розподіляється більш ефективно.

Розберемося, як у docker зробити резервування за мінімальною кількістю ядер. Квота для batch-завдань вже не застосовується, тому що обмежувати максимум не потрібно, достатньо лише гарантувати мінімум. І тут добре підходить опція docker run --cpushares.

Ми домовилися, що якщо batch вимагає гарантію щонайменше на одне ядро, то ми вказуємо --cpushares=1024а якщо мінімум на два ядра, то вказуємо --cpushares=2048. Cpu shares ніяк не втручаються в розподіл процесорного часу, доки його вистачає. Таким чином, якщо prod не використовує на даний момент усі свої чотири ядра – ніщо не обмежує batch-завдання, і вони можуть використовувати додатковий процесорний час. А ось у ситуації нестачі процесора, якщо prod спожив усі свої чотири кори і вперся в квоту — процесорний час, що залишився, буде поділено пропорційно cpushares, тобто в ситуації трьох вільних ядер одне отримає завдання з 1024 cpushares, а решта два — завдання з 2048 cpushares.

Але використання quota та shares недостатньо. Нам потрібно зробити так, щоб завдання з короткою затримкою отримувало пріоритет перед batch-завданням при розподілі процесорного часу. Без такої пріорітизації batch-завдання забиратиме весь процесорний час у момент, коли він необхідний prod. У Docker run немає жодних опцій пріорітизації контейнерів, але на допомогу приходять політики планувальника центрального процесора в Linux. Детально про них можна прочитати тут, а в рамках цієї статті ми пройдемося коротко:

  • SCHED_OTHER
    За замовчуванням отримують всі звичайні процеси користувача на Linux-машині.
  • SCHED_BATCH
    Призначена для ресурсомістких процесів. При розміщенні завдання у процесорі вводиться так званий штраф за активацію: таке завдання з меншою ймовірністю отримає ресурси процесора, якщо його на даний момент використовує завдання із SCHED_OTHER
  • SCHED_IDLE
    Фоновий процес з дуже низьким пріоритетом навіть нижче, ніж nice –19. Ми використовуємо нашу бібліотеку з відкритим кодом one-nio, щоб поставити необхідну політику при запуску контейнера викликом

one.nio.os.Proc.sched_setscheduler( pid, Proc.SCHED_IDLE )

Але навіть якщо ви не програмуєте на Java, те саме можна зробити за допомогою команди chrt:

chrt -i 0 $pid

Зведемо всі наші рівні ізоляції в одну табличку для наочності:

Клас ізоляції
Приклад alloc
Опції Docker run
sched_setscheduler chrt*

Тикати
cpu = 4
--cpuquota=400000 --cpuperiod=100000
SCHED_OTHER

Партія
Cpu = [1, *)
--cpushares=1024
SCHED_BATCH

Idle
Cpu = [2, *)
--cpushares=2048
SCHED_IDLE

*Якщо ви робите chrt зсередини контейнера, може знадобитися capability sys_nice, тому що за умовчанням Docker цей capability забирає під час запуску контейнера.

Але завдання споживають як процесор, а й трафік, який впливає затримку мережевої завдання ще більше, ніж неправильне розподіл ресурсів процесора. Тому ми, природно, хочемо отримати таку саму картинку і для трафіку. Тобто коли prod-завдання відсилає якісь пакети до мережі, ми квотуємо максимальну швидкість (формула alloc: lan=[*,500mbps) ), з якою prod може це робити. А для batch ми гарантуємо лише мінімальну пропускну спроможність, але не обмежуємо максимальну (формула alloc: lan=[10Mbps,*) ) При цьому трафік prod повинен отримати пріоритет перед batch-завданнями.
Тут Docker не має жодних примітивів, які ми могли б використати. Але нам на допомогу приходить Linux Traffic Control. Ми змогли досягти потрібного результату за допомогою дисципліни Hierarchical Fair Service Curve. З її допомогою ми виділяємо два класи трафіку: високопріоритетний prod та низькопріоритетний batch/idle. У результаті конфігурація для вихідного трафіку виходить така:

One-cloud - ОС рівня дата-центру в Однокласниках

тут 1:0 - "кореневий qdisc" дисципліни hsfc; 1:1 — дочірній клас hsfc із загальним лімітом пропускної спроможності 8 Gbit/s, під який поміщаються дочірні класи всіх контейнерів; 1:2 — дочірній клас hsfc загальний для всіх batch та idle завдань із «динамічним» лімітом, про який нижче. Інші дочірні класи hsfc — це виділені класи для prod-контейнерів з лімітами, що відповідають їх маніфестам, — 450 і 400 Mbit/s. Кожному класу hsfc призначена qdisc черга fq або fq_codel, залежно від версії ядра linux, щоб уникнути втрат пакетів при сплесках трафіку.

Зазвичай дисципліни tc служать для пріорітизації лише вихідного трафіку. Але ми хочемо пріоритизувати і вхідний трафік теж - адже якесь batch-завдання може запросто вибрати весь вхідний канал, отримуючи, наприклад, великий пакет вхідних даних для map&reduce. Для цього ми використовуємо модуль якщоб, який створює віртуальний інтерфейс ifbX для кожного мережного інтерфейсу і перенаправляє вхідний трафік з інтерфейсу на ifbX. Далі для ifbX працюють ті самі дисципліни для контролю вихідного трафіку, для якого конфігурація hsfc буде дуже схожою:

One-cloud - ОС рівня дата-центру в Однокласниках

В ході експериментів ми з'ясували, що найкращі результати hsfc показує тоді, коли клас 1:2 непріоритетного batch/idle трафіку обмежується на машинах-міньйонах не більше ніж до деякої вільної смуги. В іншому випадку непріоритетний трафік надто сильно впливає на затримку prod-завдань. Поточну величину вільної смуги miniond визначає кожну секунду, вимірюючи середнє споживання трафіку всіма prod-задачами даного мінійону One-cloud - ОС рівня дата-центру в Однокласниках і віднімаючи її з пропускної спроможності мережного інтерфейсу One-cloud - ОС рівня дата-центру в Однокласниках c невеликим запасом, тобто.

One-cloud - ОС рівня дата-центру в Однокласниках

Смуги визначаються для вхідного та вихідного трафіку незалежно. І відповідно до нових значень miniond переконфігурує ліміт непріоритетного класу 1:2.

Таким чином ми реалізували всі три класи ізоляції: prod, batch та idle. Ці класи сильно впливають на характеристики виконання завдань. Тому ми вирішили помістити цю ознаку наверх ієрархії, щоб при погляді на ім'я ієрархічної черги відразу було зрозуміло, з чим маємо справу:

One-cloud - ОС рівня дата-центру в Однокласниках

Усі наші знайомі Web и музика фронти тоді розміщуються у ієрархії під prod. Для прикладу під batch давайте помістимо сервіс музичний каталог, який періодично складає каталог треків із набору завантажених у «Однокласники» mp3-файлів. А прикладом сервісу під idle може бути music transformer, що нормалізує рівень гучності музики.

Знову прибравши зайві лінії, ми можемо записати імена наших сервісів більш плоско, дописавши клас ізоляції завдання до кінця повного імені сервісу: web.front.prod, catalog.music.batch, transformer.music.idle.

І тепер, дивлячись на ім'я сервісу, ми розуміємо не тільки те, яку функцію він виконує, а й його клас ізоляції, отже, його критичність тощо.

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

Чого нам вдалося досягти: якщо batch інтенсивно споживає лише ресурси процесора, то вбудований планувальник ЦП Linux дуже добре справляється зі своїм завданням і впливу на prod-завдання практично немає. Але якщо це batch-завдання починає активно працювати з пам'яттю, то взаємний вплив вже проявляється. Це тому, що у prod-завдання «вимиваються» процесорні кеші пам'яті — у результаті кеші зростають промахи, і процесор обробляє prod-завдання повільніше. Таке batch-завдання може на 10% підвищити затримки нашого типового prod-контейнера.

Ізолювати трафік ще складніше через те, що сучасні мережеві карти мають внутрішню чергу пакетів. Якщо пакет від batch-завдання туди потрапив першим, значить, він першим і буде переданий кабелем, і тут нічого не поробиш.

До того ж нам поки що вдалося вирішити лише завдання пріорітизації TCP-трафіку: для UDP підхід з hsfc не працює. І навіть у випадку з TCP-трафіком, якщо batch-завдання генерує багато трафіку, це також дає близько 10% збільшення затримки prod-завдання.

Відмовостійкість

Однією з цілей розробки one-cloud було поліпшення отказоустойчивости Однокласників. Тому я хотів би детальніше розглянути можливі сценарії відмов і аварій. Давайте почнемо з простого сценарію - з відмови контейнера.

Контейнер сам по собі може відмовити кількома способами. Це може бути якийсь експеримент, баг чи помилка у маніфесті, через яку prod-завдання починає споживати більше ресурсів, ніж зазначено у маніфесті. У нас був випадок: розробник реалізував один складний алгоритм, багато разів його переробляв, сам себе перемудрив і заплутався так, що, зрештою, завдання дуже нетривіально зациклювалося. А оскільки prod-завдання більш пріоритетне, ніж решта на тих же міньйонах, воно почало споживати всі доступні ресурси процесора. У цій ситуації врятувала ізоляція, а точніше, квота на процесорний час. Якщо задачі виділено квоту, завдання не потребує більше. Тому batch- та інші prod-завдання, які працювали на тій же машині, нічого не помітили.

Друга можлива неприємність – падіння контейнера. І тут нас рятують політики рестарту, всі їх знають, Docker сам чудово справляється. Майже всі prod-завдання мають політику рестарту завжди. Іноді ми використовуємо on_failure для batch-завдань або налагодження prod-контейнерів.

А що можна зробити за недоступності цілого міньйону?

Очевидно, запустити контейнер на іншій машині. Найцікавіше тут - що відбувається з IP-адресою (адресами), призначеними на контейнер.

Ми можемо призначати контейнерам такі ж IP-адреси, як і машин-міньйонів, на яких ці контейнери запускаються. Тоді при запуску контейнера на іншій машині його IP-адреса змінюється, і всі клієнти повинні зрозуміти, що контейнер переїхав, тепер треба ходити на іншу адресу, що потребує окремого сервісу Service Discovery.

Service Discovery – це зручно. На ринку багато рішень різного ступеня стійкості до відмови для організації реєстру сервісів. Часто у таких рішеннях реалізується логіка балансувальника навантаження, зберігання додаткової конфігурації як KV-сторожа тощо.
Однак нам хотілося б обійтися без необхідності впровадження окремого реєстру, адже це означало б введення критичної системи, яка використовується всіма сервісами в production. А значить, це потенційна точка відмови, і потрібно вибирати або розробляти дуже стійке до відмови від рішення, що, очевидно, дуже непросто, довго і дорого.

І ще один великий недолік: щоб наша стара інфраструктура працювала з новою, довелося б переписати всі завдання під використання якоїсь Service Discovery системи. Роботи дуже багато, а місцями до неможливості, коли мова заходить про низькорівневі пристрої, що працюють на рівні ядра ОС або безпосередньо з залізом. Реалізація цієї функціональності за допомогою усталених патернів рішень, як наприклад side-car означала б місцями додаткове навантаження, місцями – ускладнення експлуатації та додаткові сценарії відмов. Ускладнювати нам не хотілося, тому вирішили зробити використання Service Discovery опціональним.

В one-cloud IP слідує за контейнером, тобто у кожного екземпляра завдання є своя власна IP-адреса. Ця адреса «статична»: вона закріплюється за кожним екземпляром у момент першої відправки сервісу в хмару. Якщо протягом життя сервіс мав різну кількість екземплярів, то в результаті за ним буде закріплено стільки IP-адрес, скільки максимально було екземплярів.

Згодом ці адреси не змінюються: вони присвоєні один раз і продовжують існувати протягом усього життя сервісу в production. IP-адреси слідують за контейнерами через мережу. Якщо контейнер переноситься на інший міньйон, то адреса перейде за ним.

Таким чином, зіставлення імені сервісу зі списком його IP-адрес змінюється дуже рідко. Якщо ще раз подивитися на імена екземплярів сервісу, які ми згадували на початку статті (1.ok-web.group1.web.front.prod, 2.ok-web.group1.web.front.prod, …), ми помітимо, що вони нагадують FQDN, що використовуються в DNS. Так, для відображення імен екземплярів сервісів в їх IP-адресах ми використовуємо DNS-протокол. Причому цей DNS повертає всі зарезервовані IP-адреси всіх контейнерів - і працюючих, і зупинених (припустимо, використовується три репліки, а у нас там п'ять адрес зарезервовані - всі п'ять повертатимуться). Клієнти, отримавши цю інформацію, спробують встановити з'єднання з усіма п'ятьма репліками — і таким чином визначать тих, які працюють. Такий варіант визначення доступності значно надійніший, в ньому не беруть участь ні DNS, ні Service Discovery, а отже, немає і задач, що важко вирішуються із забезпеченням актуальності інформації та відмовостійкості цих систем. Більше того, в критичних сервісах, від яких залежить робота всього порталу, ми можемо взагалі не використовувати DNS, а просто забивати в конфігурацію IP-адреси.

Реалізація такого перенесення IP за контейнерами може бути нетривіальною — і ми зупинимося на тому, як це працює на наступному прикладі:

One-cloud - ОС рівня дата-центру в Однокласниках

Допустимо, one-cloud майстер дає команду міньйону M1 запустити 1.ok-web.group1.web.front.prod за адресою 1.1.1.1. На міньйоні працює BIRD, який анонсує цю адресу на спеціальні сервери route reflector. Останні мають BGP-сесію з мережевою залізницею, в яку і транслюється маршрут адреси 1.1.1.1 на M1. M1 маршрутизує пакети всередину контейнера вже засобами Linux. Серверів route reflector три, оскільки це дуже критична частина інфраструктури one-cloud - без них мережа в one-cloud не працюватиме. Ми розміщуємо їх у різних стійках, по можливості, розташованих у різних залах дата-центру, щоб зменшити ймовірність одноразової відмови всіх трьох.

Давайте припустимо, що зв'язок між майстром one-cloud і міньйоном М1 зник. Майстер one-cloud тепер діятиме, виходячи з припущення, що М1 повністю відмовив. Тобто дасть команду міньйону М2 запустити web.group1.web.front.prod з тією самою адресою 1.1.1.1. Тепер у нас є два конфліктуючі маршрути в мережі для 1.1.1.1: М1 і М2. Для того, щоб вирішувати подібні конфлікти, ми використовуємо Multi Exit Discriminator, який вказується в BGP-анонсі. Це число, яке показує вагу анонсованого маршруту. З конфліктуючих буде обрано маршрут із меншим значенням MED. Майстер one-cloud підтримує MED як інтегральну частину IP-адрес контейнерів. Вперше адреса виписується з досить великим MED = 1 000 000. У ситуації такого аварійного перенесення контейнера майстер зменшує MED, і М2 вже отримає команду анонсувати адресу 1.1.1.1 c MED = 999 999. Примірник же, що працює на M1, залишиться при Це без зв'язку, і його подальша доля нас мало цікавить до моменту відновлення зв'язку з майстром, коли його буде зупинено як старий дубль.

аварії

Усі системи керування дата-центрами завжди прийнятно відпрацьовують дрібні відмови. Виліт контейнера – це норма практично скрізь.

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

Що означає аварія для системи керування дата-центром? Насамперед це масована одноразова відмова безлічі машин, і системі управління потрібно одночасно мігрувати дуже багато контейнерів. Але якщо аварія дуже масштабна, то може статися так, що всі завдання не зможуть бути перерозміщені на інших міньйонах, тому що ресурсна ємність дата-центру знижується нижче 100% навантаження.

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

Що можна з цим зробити?

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

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

One-cloud - ОС рівня дата-центру в Однокласниках

Звісно, ​​це процеси, які безпосередньо беруть участь у обробці запитів користувачів, т. е. prod. Ми це вказуємо це за допомогою пріоритету розміщення — числа, які можуть бути призначені черги. Якщо якась черга пріоритет вище, її послуги розміщуються насамперед.

На prod ми призначаємо пріоритети вище, 0; на batch - трохи нижче, 100; на idle ще нижче, 200. Пріоритети застосовуються ієрархічно. У всіх завдань нижче за ієрархію буде відповідний пріоритет. Якщо хочемо, щоб усередині prod кеші запускалися перед фронтендами, то призначаємо пріоритети на cache = 0 і на front підчерги = 1. Якщо ж, наприклад, ми хочемо, щоб з фронтів насамперед запускався основний портал, а music фронт вже потім, то останньому можемо визначити пріоритет нижче - 10.

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

One-cloud - ОС рівня дата-центру в Однокласниках

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

У нашій ієрархії дуже просто вказати такий пріоритет витіснення, щоб prod-і batch-завдання витісняли або зупиняли idle-завдання, але не один одного, вказавши для idle пріоритет, що дорівнює 200. Так само, як і у випадку з пріоритетом розміщення, можемо використовувати нашу ієрархію у тому, щоб описувати складніші правила. Наприклад, вкажемо, що функцією музики ми жертвуємо, якщо нам не вистачить ресурсів для основного веб-порталу, встановивши пріоритет для відповідних вузлів нижче: 10.

Аварії ДЦ цілком

Чому може відмовити весь дата-центр? Стихія. Був гарний піст, як ураган вплинув на роботу дата-центру. Стихією можна вважати бомжів, які спалили якось у колекторі оптику, і дата-центр повністю втратив зв'язок з рештою майданчиків. Причиною виходу з ладу і людський чинник: оператор видасть таку команду, що весь дата-центр впаде. Таке може статися через великий баг. Загалом дата-центри падають — це не рідкість. У нас таке відбувається раз на кілька місяців.

І ось що ми робимо, щоб ніхто #окживі не постив у твіттерах.

Перша стратегія – ізоляція. Кожен інстанс one-cloud ізольований і може керувати машинами лише одного дата-центру. Тобто втрата хмари через баги чи неправильну команду оператора — це втрата лише одного дата-центру. Ми до цього готові: є політика резервування, за якої репліки програми та даних розміщуються у всіх дата-центрах. Ми використовуємо стійкі до відмов бази даних і періодично тестуємо відмови.
Оскільки сьогодні у нас чотири дата-центри, тобто й чотири окремі, повністю ізольовані екземпляри one-cloud.

Такий підхід не лише захищає від фізичної відмови, але може захистити і помилки оператора.

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

One-cloud - ОС рівня дата-центру в Однокласниках

Підсумки

Відмінні риси one-cloud:

  • Ієрархічна та наочна схема іменування сервісів та контейнерівяка дозволяє дуже швидко дізнатися, що це за завдання, до чого вона відноситься і як працює і хто відповідає за неї.
  • Ми застосовуємо свою техніку суміщення prod-і batch-задач на міньйонах, щоб підвищити ефективність спільного використання машин. Замість cpuset ми використовуємо CPU quotas, shares, політики планувальника CPU та Linux QoS.
  • Цілком ізолювати контейнери, що працюють на одній машині, так і не вийшло, але їхній взаємний вплив залишається в межах до 20%.
  • Організація сервісів в ієрархію допомагає за автоматичної ліквідації аварій за допомогою пріоритетів розміщення та витіснення.

Часті запитання

Чому ми не ухвалили готове рішення.

  • Різні класи ізоляції завдань вимагають різної логіки при розміщенні на міньйонах. Якщо prod-завдання можна розміщувати простим резервуванням ресурсів, то batch та idle необхідно розміщувати, відстежуючи реальну утилізацію ресурсів на машинах-міньйонах.
  • Необхідність обліку таких ресурсів, що споживаються завданнями, як:
    • пропускна спроможність мережі;
    • типи та «шпинделі» дисків.
  • Необхідність вказувати пріоритети сервісів при ліквідації аварій, прав та квот команд на ресурси, що вирішується за допомогою ієрархічних черг в one-cloud.
  • Необхідність мати людське найменування контейнерів для зменшення часу реакцій на аварії та інциденти
  • Неможливість одноразового повсюдного впровадження Service Discovery; необхідність тривалий час співіснувати із завданнями, розміщеними на залізних хостах, — те, що вирішується «статичними» IP-адресами, що йдуть за контейнерами, і, як наслідок, необхідність унікальної інтеграції з великою мережевою інфраструктурою.

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

Тим, хто читає останні рядки, – дякую за витримку та увагу!

Джерело: habr.com

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