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, машыннае навучанне, статыстыка.
  • Задачы фонавыя - idle. Для такіх задач не вельмі важныя ні latency, ні throughput. Сюды ўваходзяць розныя тэсты, міграцыі, пералікі, канвертацыі дадзеных з аднаго фармату ў іншы. З аднаго боку, яны падобныя на разліковыя, з другога - нам не вельмі важна, як хутка яны завершацца.

Паглядзім, як такія задачы спажываюць рэсурсы, напрыклад, цэнтральнага працэсара.

Задачы з кароткай затрымкай. У такой задачы патэрн спажывання ЦП будзе падобны на гэты:

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

ўхаластую
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-задачы маюць палітыку рэстарту always. Часам мы выкарыстоўваем 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. Асобнік жа, які працуе на M999, застанецца пры гэтым без сувязі, і яго далейшы лёс нас мала цікавіць да моманту ўзнаўлення сувязі з майстрам, калі ён і будзе спынены як стары дубль.

аварыі

Усе сістэмы кіравання дата-цэнтрамі заўсёды прымальна адпрацоўваюць дробныя адмовы. Вылет кантэйнера - гэта норма практычна ўсюды.

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

Што азначае аварыя для сістэмы кіравання дата-цэнтрам? У першую чаргу гэта масіраваная аднаразовая адмова мноства машын, і сістэме кіравання трэба адначасова міграваць вельмі шмат кантэйнераў. Але калі аварыя вельмі маштабная, то можа здарыцца так, што ўсе задачы не змогуць быць пераразмешчаны на іншых мін'ёнах, таму што рэсурсная ёмістасць дата-цэнтра падае ніжэй за 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

Дадаць каментар