Русский
Русский
English
Статистика
Реклама

Блог компании флант

Перевод Обратная сторона Open Source-славы как угрожают автору curl

23.02.2021 14:15:47 | Автор: admin

Прим. перев.: уникальная история, что всколыхнула интернет в эти дни, показывает неожиданную сторону того, что могут заслужить авторы самых популярных Open Source-проектов. Ниже представлен перевод недавней заметки из блога шведского программиста Daniel Stenberg оригинального автора и главного разработчика curl, обладателя премии Polhem Prize (вручается в Швеции за выдающиеся инженерные достижения).

Как вам, наверное, известно, в своем блоге я публикую забавные электронные письма, которые периодически приходят мне на адрес. Ну, знаете, когда мне пишут о проблемах с каким-то своим устройством (вроде автомобиля), потому что мой email где-то там засветился.

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

Увы, не все эти письма забавны.

Категория: не смешно

Сегодня я получил следующее письмо:

От: Al Nocai <[скрыто]@icloud.com>

Дата: Пт, 19 Фев 2021 03:02:24 -0600

Тема: Я тебя убью

Да, именно такая тема (I will slaughter you).

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

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

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

Никаких объяснений; только тема и семь изображений, поэтому мне оставалось только догадываться о цели этого послания.

Я решил, что имя в письме выдуманное, а адрес электронной почты, скорее всего, одноразовый. Часовой пояс в строке с датой намекал на Центральное стандартное время США, но, конечно, мог быть подделкой, как и все остальное.

Мой ответ

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

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

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

Очевидно, что вы не достойны моего кода.

Впрочем, я не надеялся, что мой ответ будет прочитан или что-то изменит.

Примечание: Обновление ниже добавлено после первоначальной публикации.

Ответ Al Nocai

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

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

Они использовали твое дерьмовое ПО, чтобы украсть root у меня и у многих других. Из-за этого только на прототипировании я потерял более 15 тысяч долларов.

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

Знаешь, что я сделал, чтобы заслужить это? Я пытался разработать торговый путь в сфере технологий и разработать методики обучения на основе проектов, чтобы детки были при деле. И знаешь, кто во всех этих файлах? Ты. Это отвратительно. Меня взломали в октябре 2020, перехватив трафик федерального сервера, и всем этим я обязан тебе.

Мне пришлось сидеть и смотреть вот на это:

1. fireeye в октябре 2020;
2. Solarwinds в октябре 2020;
3. взлом модемов Zyxel в октябре 2020;
4. множество векторов атаки на Sigover с помощью XML injection;
5. стохастическая шаблонизация в JS с использованием выражений сравнения для записи в регистры данных;
6. 50-миллиардные компании охотятся за мной, поскольку я разоблачил их дерьмовое вредоносное ПО.

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

Ты построил гоночный болид для Формулы-1, а ключи бросил шпане с раздутым эго. А теперь из-за всего этого мусора мне приходится разбираться с уязвимостями нулевого дня в Win10.

Я потерял свою семью, свою страну, своих друзей, свой дом и 6 лет работы, пытаясь построить лучшее будущее для потомков. И во всем этом виноват твой код. Этот код используется для того, чтобы похищать root и внедрять эксплойты. Этот код используется для шантажа.

Так что я ни капельки не расстроен. Ты прекрасно представлял себе возможности этого кода. И думал, что все это большая шутка. А я не смеюсь. Я уже давно прошел эту точку. /- Al

Al продолжает

Через девять часов после публикации оригинальной заметки в блоге, Al прислал мне еще два электронных письма. Вот они:

Письмо 3:

https://davidkrider.com/i-will-slaughter-you-daniel-haxx-se/

Валяй. Ты меня не испугаешь. Что привело меня сюда? 5-е покушение на мою жизнь. Условия предоставления услуг Apple? Иди к черту со своей платформой.

Забавно, что он нашел публикацию о заметке в моем блоге.

Письмо 4:

Есть проект: Анализ риска военных операций в урбанизированной местности с помощью широкополосного анализа электромагнитного спектра с использованием различных преобразований Фурье.Ты и этот David Krider, кем бы он ни был, вы часть всего этого.

- Взломы федеральных серверов;
- соучастие в покушение на убийство;
- мошенничество;
и т.д.

На данный момент я разговаривал с ФБР, региональным отделением ФБР, министерством по делам ветеранов США (VA), офисом генерального инспектора VA, Федеральной комиссией по связи (FCC), Комиссией по ценным бумагам (SEC), АНБ, Минздравом (DOH), Управлением служб общего назначения (GSA), МВД, ЦРУ, Бюро финансовой защиты потребителей (CFPB), Министерством жилищного строительства и городского развития (HUD), MS, Convercent; по состоянию на сегодня представители 22 отдельных ведомств вызванивают меня и тратят мое время.

И отвечаете за это вы с чертовым Дэйвом. Мне насрать, звони в полицию. Я их обматерю, когда они позвонят, и они заткнутся.

(Прим. перев.: в английском варианте сохранено своеобразное форматирование и опечатки.)

К письму 4 также был приложен PDF-файл с названием BustyBabes 4.pdf. Он представляет собой 13-страничный документ о NERVEBUS NERVOUS SYSTEM. Первый параграф гласит: NerveBus Nervous System призвана быть универсальной платформой для всестороннего и комплексного анализа, предоставляя конечному пользователю целостную, связную и реальную информацию о среде, которую тот мониторит. В документе не упоминается ни curl, ни мое имя.

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

Дополнительная информация

Тема на Hacker news и Reddit.

Я сообщил об угрозе в шведскую полицию (по месту жительства).

P.S. от переводчика

В то время, как в комментариях Hacker News обсуждают, что приведенный в письме список обвинений (где упомянуты FireEye, SolarWinds и т.п.) набор бессмысленных фраз (что, возможно, указывает на психические отклонения), в комментарии к публикации в блоге Daniel Krider можно найти некоторые уточнения по этому поводу от самого автора:

I have since October, had my entire life sandboxed by individual Daniel Patrick Ehrlich to find software loopholes to aid in development of SCNR, an aggregation bot for Open Source Intelligence tied to Subverse Media and Timothy Pool, the Journalist.

I know the following:
1. React JS primary utility is to obfuscate URIs to inject malicious code
2. This code is commonly down my user agents, KHTML, primarily Favicons as they are the SVG type, and carry injectable XML.
3. Qualcom BT Adapters and drivers are routinely hacked via the SigOver attack vector through malicious sub packet injection.

P.P.S.

Читайте также в нашем блоге:

Подробнее..

Перевод Угон домена Perl.com

05.03.2021 10:06:07 | Автор: admin

Прим. перев.: в конце января стало известно о том, что один из основных доменов языка программирования Perl Perl.com был угнан. Это вызвало смешанную реакцию в сообществе как любителей языка, так и его противников. Теперь, когда всё уже позади и справедливость восстановлена, один из самых известных сторонников Perl brian d foy рассказал о том, что же произошло и как сообщество добилось положительного исхода событий. Представляем вниманию перевод его заметки.

На неделю мы потеряли контроль над доменом Perl.com. Теперь, когда проблема устранена, можно подробно рассказать о том, что произошло и как мы с этим справились. Инцидент затронул только домен Perl.com, никакие другие ресурсы сообщества не пострадали. Сам сайт никуда не делся, но DNS выдавал другие IP-адреса.

Во-первых, следует отметить, что проблема не связана с продлением домена. Для нас подобная забывчивость была бы даже лучше, поскольку есть так называемый grace period (в течение месяца после expiration его владелец все еще может продлить прим. перев.).

Во-вторых, хочу подчеркнуть, что я всего лишь редактор сайта, который использует домен Perl.com. То есть юридически меня нельзя назвать пострадавшей стороной. Владельцем домена является Tom Christiansen, и если делу будет дан ход, мне или кому-либо другому вовсе не обязательно знать все подробности. Однако я общался со многими людьми, вовлеченными в процесс.

Реакция на инцидент

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

Ранним утром 27 января Perl NOC (Network Operations Center) в рамках повседневного мониторинга заметил, что с доменом происходит нечто странное. Параллельно пользователи начали жаловаться, что сайт недоступен. По мере обновления записей DNS по всему миру все большее число пользователей сообщали о проблемах. Мы понятия не имели, что происходит и почему.

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

Я завел Google-документ и пригласил нужных людей. Мы начали заполнять детали, классифицируя их с помощью цвета: зеленого, янтарного, красного. Зеленым помечалась информация, которую мы считали достоверной например, прямое взаимодействие с регистратором; янтарный обозначал непроверенную информацию, которая могла быть правдивой, а красным маркировались недостоверные сведения. Вся информация сопровождалась временем и источником. Первое правило борьбы со слухами запоминать, от кого и когда вы их услышали. Когда это все сведено в документ, любой имеет возможность сравнить имеющуюся у него информацию с общедоступной и понять, является ли она более ценной и актуальной. Иногда информация, которая считалась правдивой, оказывалась недостоверной. Тогда мы вносили изменения в документ.

Также был составлен список дел, и все, кто мог, занялись ими. Например, мы начали проверять другие ресурсы сообщества. Elaine Ashton проверила регистрацию cpan.org (там была странность в контактной информации, но после телефонного разговора с регистратором все оказалось в порядке). Robert Spier, участник Perl NOC, проверил различные сетевые аспекты, хронологию и т.п. Rik Signes вызвался завести закрытый список рассылки на TopicBox (в конце концов, он же CTO). Тонкость здесь в том, чтобы не делать работу, которую может сделать кто-то другой (и часто лучше). Аналогичным образом, если кто-то уже что-то делает, не стоит тратить свое время впустую, переделывая за ним или заново "изобретая велосипед". Децентрализация это классно, но ей необходим координатор. В этом случае координатором стал я, поскольку очень многое вложил в сайт Perl.com. Кроме того, я отлично сработался с Томом, ведь до этого мы целый год трудились над книгой Programming Perl.

Мой твит и комментарии в Reddit привлекли внимание обеих сторон в "регистрационном уравнении", так что на самом старте процесса я смог переговорить как с Network Solutions, так и с Key Systems. Нам очень повезло, что Perl штука довольно известная, а мы с Tom Christiansen, в свою очередь, занимаем не последнее место в сообществе Perl. Первое правило успеха уже быть успешным. Сотрудники различных вовлеченных в процесс организаций предлагали нам свою помощь и давали советы. Увы, другим жертвам повезло меньше, и помощи они не дождались. Все эти организации, вероятно, на определенных этапах своего существования использовали Perl, и с теплотой вспоминали старые добрые времена.

Я в основном отвечал за связь с Томом и помогал ему управляться со всеми игроками. В таких случаях огромный объем работы частенько подавляет людей, и у тех попросту опускаются руки. Тому надо было сосредоточиться на взаимодействии с регистраторами, так что я взял на себя максимум остальной работы, чтобы не отвлекать его.

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

Как только все стороны обменялись актуальной контактной информацией, процесс пошел как по маслу. Мы не были уверены, что все получится, но по мере развития ситуации наша убежденность крепла. Впрочем, уверенность это субъективная информация. Какой смысл говорить о том, что, по вашему мнению, может или должно произойти, если в процесс постоянно вмешиваются всевозможные задержки и заминки?

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

Поэтому мы решили публиковать всю актуальную информацию в одном месте в блоге The Perl NOC. Иногда между получением информации и ее публикацией проходило несколько часов мы внимательно проверяли все сведения. Пользователям не было необходимости рыскать по социальным сетям и т.п. в поисках новостей, поскольку все они были собраны в одном месте. Мы по-прежнему транслировали обновления на всех доступных ресурсах, при этом ссылаясь на первоисточник блог NOC. Единый источник информации очень помогает.

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

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

Это не первое мое "родео", и я взял на себя роль пресс-секретаря. Несмотря на кропотливую работу по проверке всего и вся, многие люди со стороны просто выдумывали всякую ерунду. Что ж, бывает и такое (что вполне предсказуемо). Издательский принцип "Если мать говорит, что любит тебя, найди второй источник" не применим в эпоху Twitter. Часто не нужен даже первый источник.

Важно, чтобы сообщество представлял один человек. Авторы доброй половины "новостных" публикаций не потрудились провести базовые исследования, а у некоторых из них отсутствовала контактная информация (серьезно? Журналист, с которым нельзя связаться?). Некоторые авторы исправили свои материалы, поговорив со мной.

The Register с самого начала выдавал точную информацию, как и Paul Ducklin на сайте Sophos.

Примерно через неделю после изменений на серверах имен, я пришел к заключению, что возврат домена после взлома может растянуться на несколько недель. Поскольку в него были вовлечены разные страны со своими законами и правилами, процесс продвигался гораздо медленнее, чем нам хотелось. В эпоху Интернета "завтра" равносильно "вечности". David Farrell выдвинул идею о переименовании сайта, и мы стали использовать perldotcom.perl.org в качестве временного домена. Robert смог все быстро настроить, и мы классно провели время, анализируя pull request'ы от сообщества. В них пользователи указывали на некоторые моменты, которые мы за'hardcode'или, хотя не должны были (любой человек может предложить PR по любому поводу, имеющему отношение к сайту!). Основой для всей этой работы выступал процесс на базе GitHub, который разработал David (нам приятно получать даже самые незначительные PR от сообщества!).

Затем, в начале февраля, обходными путями я получил надежную (зеленую) информацию о том, что домен вернется к нам через пару дней. Я сперва не поверил, но это действительно произошло! Опять же, нам очень повезло: люди, в сердцах которых Perl занимает особое место, сильно нам помогли. Все стороны понимали, что Perl.com принадлежит Тому, и воплотить это понимание в жизнь было несложно. Владельцам менее известного доменного имени было бы гораздо труднее доказать свои права на него.

Впрочем, с возвращением домена история не закончилась. Пока домен был скомпрометирован, различные продукты в сфере безопасности внесли Perl.com в черный список, а некоторые DNS-серверы занесли его в sinkhole. Мы решили, что постепенно все придет в норму, и отложили празднование возвращения Perl.com до более подходящих времен. Хотелось, чтобы он стал доступен для всех. И, наконец, этот знаменательный момент наступил! Если у кого-то проблемы с Perl.com, пожалуйста, заведите issue, чтобы мы знали, что для некоторой части интернета домен не работает.

Что, по нашему мнению, произошло

В этой части мы поделимся некоторыми догадками (кстати, Perl.com оказался не единственной жертвой). По всей видимости, имела место атака социальной инженерии на Network Solutions, включающая подделку документов и т.п. У Network Solutions нет причин раскрывать мне какие-либо подробности (опять же, я не являюсь потерпевшей стороной), но я поговорил с другим владельцами доменов, и те рассказали мне о наиболее вероятной схеме.

John Berryhill опубликовал в Twitter результаты своего расследования, которое показало, что взлом на самом деле произошел в сентябре. В декабре домен был передан регистратору BizCN, но серверы имен остались прежними. В январе домен вновь был передан другому регистратору Key Systems, GmbH. Подобная задержка помешала выявлению проблемы на ранних этапах, а передача домена от одного регистратора другому значительно осложнила его восстановление.

Обратите внимание на длительную задержку до момента первой передачи. Домен взломан в сентябре, а передача произошла в декабре. Для этого имеется веская причина: 60-дневное правило ICANN. Домен нельзя передать в течение 60 дней после обновления контактной информации. Мы думаем, что регистрация была изменена злоумышленниками одновременно с продлением домена на несколько лет (первоначально домен истекал в 2029 году).

После передачи домена Key Systems в конце января, новый владелец-мошенник выставил его (и другие домены) на продажу на Afternic (рынок доменов). Perl.com можно было купить за 190 тыс. долларов. После запросов The Register домен был снят с продажи.

Некоторые уроки

Очевидно, ситуация неприятная, но ее нельзя назвать уникальной. Домен был зарегистрирован в начале 1990-х, вскоре после этого Tom Christiansen получил над ним контроль, и с тех пор продолжал платить сборы за продление. Как обычно бывает, ничего не менялось "пока гром не грянул". Механизмы вроде двухфакторной авторизации, пожалуй, избавили бы нас от большей части проблем (хотя атаки социальной инженерии умудряются обходить защитные меры).

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

Очень важно вести переговоры с одним и тем же человеком, иначе вы рискуете посеять путаницу из-за многообразия сообщений, даже если в них говорится одно и то же. Также необходимо демонстрировать уверенность и компетентность, чтобы озвучиваемая вами информация воспринималась как достоверная; если разные каналы озвучивают разные послания, риск ошибок возрастает. Perl Foundation настоял на публикации собственного заявления вместо подготовленного нами. Хотя оно было крайне лаконичным, ссылка на блог Perl NOC не работала несколько дней. Не рискуйте понапрасну.

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

Текущее состояние дел

Домен Perl.com был благополучно возвращен Tom Christiansen. Ведутся работы над различными мерами, способными предотвратить повторение подобной ситуации. Сайт вернулся к нормальной жизни и стал чуть ярче благодаря всей той помощи, которую мы получили.

В рамках реагирования на инцидент The Perl Foundation Infrastructure Working Group изучила другие важные домены сообщества. Будет проведена соответствующая работа по их защите. Желающие помочь могут связаться с ними.

P.S. от переводчика

Читайте также в нашем блоге:

Подробнее..

Как мы собираем общие сведения о парке из Kubernetes-кластеров

16.06.2021 10:13:29 | Автор: admin

Имея в обслуживании большой (более 150) парк Kubernetes-кластеров, всегда хотелось иметь удобное представление их общего состояния, в том числе и для того, чтобы поддерживать их гомогенными. В первую очередь нас интересовали следующие данные:

  • версия Kubernetes чтобы все кластеры были on the edge;

  • версия Deckhouse (наша Kubernetes-платформа) для лучшего планирования релизных циклов;

  • количество узлов с разбивкой по типам (управляющие, виртуальные и статические) для отдела продаж;

  • количество ресурсов (CPU, memory) на управляющих узлах;

  • на какой инфраструктуре запущен кластер (виртуальные облачные ресурсы, bare metal или гибридная конфигурация);

  • какой облачный провайдер используется.

И вот каким был наш путь к тому, чтобы превратить эту потребность в наглядную реальность

Истоки и проверка концепции

В какой-то момент времени мы стали использовать Terraform для раскатки инфраструктуры в облака и вопрос отслеживания соответствия желаемых конфигураций реальности встал еще острее. Мы храним Terraform state в самих кластерах и проверку соответствия их с реальностью проверяет отдельно написанный Prometheus exporter. Хотя ранее у нас уже была информация для реагирования на изменения (через соответствующие алерты в системе управления инцидентами), хотелось ещё иметь полное представление о ситуации в отдельной аналитической системе.

Итак, изначально в качестве PoC был несложный Bash-скрипт, которым мы вручную время от времени собирали интересующие данные с K8s-кластеров по SSH. Он выглядел примерно так:

((kubectl -n d8-system get deploy/deckhouse -o json | jq .spec.template.spec.containers[0].image -r | cut -d: -f2 | tr "\n" ";") &&(kubectl get nodes -l node-role.kubernetes.io/master="" -o name | wc -l | tr "\n" ";") &&(kubectl get nodes -l node-role.kubernetes.io/master="" -o json | jq "if .items | length > 0 then .items[].status.capacity.cpu else 0 end" -r | sort -n | head -n 1 | tr "\n" ";") &&(kubectl get nodes -l node-role.kubernetes.io/master="" -o json | jq "if .items | length > 0 then .items[].status.capacity.memory else \"0Ki\" end | rtrimstr(\"Ki\") | tonumber/1000000 | floor" | sort -n | head -n 1 | tr "\n" ";") &&(kubectl version -o json | jq .serverVersion.gitVersion -r | tr "\n" ";") &&(kubectl get nodes -o wide | grep -v VERSION | awk "{print \$5}" | sort -n | head -n 1 | tr "\n" ";") &&echo "") | tee res.csvsed -i '1ideckhouse_version;mastersCount;masterMinCPU;masterMinRAM;controlPlaneVersion;minimalKubeletVersion' res.csv

(Здесь приведен лишь фрагмент для демонстрации общей идеи.)

Однако количество клиентов и кластеров росло стало ясно, что дальше так жить нельзя. Мы ведь инженеры, поэтому всё, что может быть автоматизировано, должно быть автоматизировано.

Так начался наш путь разработки волшебного агента для кластеров, который бы:

  • собирал желаемую информацию,

  • агрегировал ее,

  • отправлял в какое-то централизованное хранилище.

а заодно соответствовал каноном высокой доступности и cloud native.

Этот путь дал начало истории модуля в Kubernetes-платформе Deckhouse, развёрнутой на всех наших кластерах, и сопутствующего ему хранилища.

Реализация

Хуки на shell-operator

В первой итерации источником данных в клиентских кластерах служили Kubernetes-ресурсы, параметры из ConfigMap/Deckhouse, версия образа Deckhouse и версия control-plane из вывода kubectl version. Для соответствующей реализации лучше всего подходил shell-operator.

Были написаны хуки (да, снова на Bash) с подписками на ресурсы и организована передача внутренних values. По результатам работы этих хуков мы получали список желаемых Prometheus-метрик (их экспорт поддерживается в shell-operator из коробки).

Вот пример хука, генерирующего метрики из переменных окружения, он прост и понятен:

#!/bin/bash -efor f in $(find /frameworks/shell/ -type f -iname "*.sh"); do  source $fdonefunction __config__() {  cat << EOF    configVersion: v1    onStartup: 20EOF}function __main__() {  echo '  {    "name": "metrics_prefix_cluster_info",    "set": '$(date +%s)',    "labels": {      "project": "'$PROJECT'",      "cluster": "'$CLUSTER'",      "release_channel": "'$RELEASE_CHANNEL'",      "cloud_provider": "'$CLOUD_PROVIDER'",      "control_plane_version": "'$CONTROL_PLANE_VERSION'",      "deckhouse_version": "'$DECKHOUSE_VERSION'"    }  }' | jq -rc >> $METRICS_PATH}hook::run "$@"

Отдельно хочу обратить ваше внимание на значение метрики (параметр set). Изначально мы писали туда просто 1, но возник резонный вопрос: Как потом получить через PromQL именно последние, свежие labels, включая те series, которые уже две недели не отправлялась? Например, в том же MetricsQL от VictoriaMetrics для этого есть специальная функция last_over_time. Оказалось, достаточно в значение метрики отправлять текущий timestamp число, которое постоянно инкрементируется во времени. Вуаля! Теперь стандартная функция агрегации max_over_time из Prometheus выдаст нам самые последние значения labels по всем series, которые приходили хоть раз в запрошенном периоде.

Чуть позже к источникам данных добавились метрики из Prometheus в кластерах. Для их получения был написан еще один хук, который через curl ходил в кластерный Prometheus, подготавливал полученные данные и экспортировал их в виде метрик.

Чтобы вписаться в парадигму cloud-native и обеспечить HA агента, мы запустили его в несколько реплик на управляющих узлах кластера.

Grafana Agent

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

Выбор пал на разработку Grafana Labs, а именно Grafana Agent. Он умеет делать scrape метрик с endpointов, отправлять их по протоколу Prometheus remote write, а также (что немаловажно!) ведет свой WAL на случай недоступности принимающей стороны.

Задумано сделано: и вот приложение из shell-operator и sidecarом с grafana-agent уже способно собирать необходимые данные и гарантировать их поступление в центральное хранилище.

Конфигурация агента делается довольно просто благо, все параметры подробно описаны в документации. Вот пример нашего итогового конфига:

server:  log_level: info  http_listen_port: 8080prometheus:  wal_directory: /data/agent/wal  global:    scrape_interval: 5m  configs:  - name: agent    host_filter: false    max_wal_time: 360h    scrape_configs:    - job_name: 'agent'      params:        module: [http_2xx]      static_configs:      - targets:        - 127.0.0.1:9115      metric_relabel_configs:      - source_labels: [__name__]        regex: 'metrics_prefix_.+'      - source_labels: [job]        action: keep        target_label: cluster_uuid        replacement: {{ .Values.clusterUUID }}      - regex: hook|instance        action: labeldrop    remote_write:    - url: {{ .Values.promscale.url }}      basic_auth:        username: {{ .Values.promscale.basic_auth.username }}        password: {{ .Values.promscale.basic_auth.password }}

Пояснения:

  • Директория /data это volumeMount для хранения WAL-файлов;

  • Values.clusterUUID уникальный идентификатор кластера, по которому мы его идентифицируем при формировании отчетов;

  • Values.promscale содержит информацию об endpoint и параметрах авторизации для remote_write.

Хранилище

Разобравшись с отправкой метрик, необходимо было решить что-то с централизованным хранилищем.

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

NB. Справедливости ради, хочется отметить, что на данный момент Cortex выглядит уже вполне жизнеспособным, оформленным как конечный продукт. Очень вероятно, что через какое-то время вернемся к нему и будем использовать. Уж очень сладко при мысли о generic S3 как хранилище для БД: никаких плясок с репликами, бэкапами и растущим количеством данных

К тому времени у нас была достаточная экспертиза по PostgreSQL и мы выбрали Promscale как бэкенд. Он поддерживает получение данных по протоколу remote-write, а нам казалось, что получать данные используя pure SQL это просто, быстро и незатратно: сделал VIEWхи и обновляй их, да выгружай в CSV.

Разработчики Promscale предоставляют готовый Docker-образ, включающий в себя PostgreSQL со всеми необходимыми extensions. Promscale использует расширение TimescaleDB, которое, судя по отзывам, хорошо справляется как с большим количеством данных, так и позволяет скейлиться горизонтально. Воспользовались этим образом, задеплоили connector данные полетели!

Далее был написан скрипт, создающий необходимые views, обновляющий их время от времени и выдающий на выход желаемый CSV-файл. На тестовом парке dev-кластеров всё работало отлично: мы обрадовались и выкатили отправку данных со всех кластеров.

Но с хранилищем всё не так просто

Первую неделю всё было отлично: данные идут, отчет генерируется. Сначала время работы скрипта составляло около 10 минут, однако с ростом количества данных это время увеличилось до получаса, а однажды и вовсе достигло 1 часа. Смекнув, что что-то тут не так, мы пошли разбираться.

Как оказалось, ходить в таблицы базы данных мимо магических оберток, предоставляемых Promscale (в виде своих функций и views, опирающихся в свою очередь на функции TimescaleDB), невероятно неэффективно.

Было решено перестать ковыряться в потрохах данных и положиться на мощь и наработки разработчиков Promscale. Ведь их connector может не только складывать данные в базу через remote-write, но и позволяет получать их привычным для Prometheus способом через PromQL.

Одним Bashем уже было не обойтись мы окунулись в мир аналитики данных с Python. К нашему счастью, в сообществе уже были готовы необходимые инструменты и для походов с PromQL! Речь про замечательный модуль prometheus-api-client, который поддерживает представление полученных данных в формате Pandas DataFrame.

В этот момент еще сильнее повеяло взрослыми инструментами из мира аналитики данных Мотивированные на пощупать интересное и доселе неизведанное, мы двинулись в этом направлении и не прогадали. Лаконичность и простота верчения этой кучей данных через Pandas DataFrame доставила массу позитивных эмоций. И по сей день поддержка полученной кодовой базы, добавление новых параметров и всевозможные правки отображения финальных данных воспринимаются как праздник программиста и не требуют большого количества времени.

Изначально мы выбрали период скрейпинга данных grafana-agentом равным одной минуте, что отразилось на огромных аппетитах конечной БД в диске: ~800 мегабайт данных в день. Это, конечно, не так много в масштабах одного кластера (~5 мегабайт), но когда кластеров много суммарный объём начинает пугать. Решение оказалось простым: увеличили период scrapeа в конфигах grafana-agentов до одного раза в 5 минут. Прогнозируемый суммарный объем хранимых данных с retentionом в 5 лет уменьшился с 1,5 Тб до 300 Гб, что, согласитесь, уже выглядит не так ужасающе.

Некоторый профит от выбора PostgreSQL как конечного хранилища мы уже получили: для успешного переезда хранилища в финальный production-кластер достаточно было отреплицировать базу. Единственное текущий недостаток пока не получилось самостоятельно собрать свой кастомный PostgreSQL с необходимыми расширениями. После пары неудачных попыток мы остались на готовом образе от разработчиков Promscale.

Получившаяся архитектура выглядит так:

Итоги и перспективы

Мы смотрим в будущее и планируем отказаться от отчётов в формате CSV в пользу красивого интерфейса собственной разработки. А по мере разработки собственной биллинговой системы начнём отгружать данные и туда для нужд отдела продаж и развития бизнеса. Но даже те CSV, что мы получили сейчас, уже сильно упрощают рабочие процессы всего Фланта.

А пока не дошли руки до фронтенда, мы сделали dashboard для Grafana (почему бы и нет, раз всё в стандартах Prometheus?..). Вот как это выглядит:

Общая сводная таблица по кластерам с Terraform-состояниямиОбщая сводная таблица по кластерам с Terraform-состояниямиРаспределение кластеров по облачным провайдерамРаспределение кластеров по облачным провайдерамРазбивка по используемым Inlet в Nginx Ingress-контроллерахРазбивка по используемым Inlet в Nginx Ingress-контроллерахКоличество podов Nginx Ingress-контроллеров с разбивкой по версиямКоличество podов Nginx Ingress-контроллеров с разбивкой по версиям

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

P.S.

Читайте также в нашем блоге:

Подробнее..

Перевод HTTPWTF. Необычное в обычном протоколе

20.04.2021 12:16:22 | Автор: admin

Прим. перев.: эту статью написал автор Open Source-утилиты HTTP Toolkit, предназначенной для исследования и модификации HTTP(S)-трафика для нужд отладки и тестирования. В материале собраны примечательные особенности стандарта HTTP, которые долгие годы живут вместе с нами, однако не каждый догадывается об их существовании.

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

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

No-cache на самом деле означает кэшируй

Кэширование никогда не было легким занятием, но кэш-заголовки HTTP в этом смысле особенно преуспели. Худшие примеры no-cache и private. Как вы думаете, что делает приведенный ниже HTTP-заголовок ответа?

Cache-Control: private, no-cache

Нигде не храни этот ответ, так? Ха-ха-ха, а вот и нет!

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

В частности, no-cache означает, что контент обязательно кэшируется, но всякий раз, когда браузер или CDN хотят его использовать, они должны отправить запрос с If-Match или If-Modified-Since, спросив сначала у сервера, актуален ли кэш. Между тем private означает, что контент можно кэшировать, но только в браузерах конечных пользователей, а не в CDN или на прокси-серверах.

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

Если послать ответ с заголовком Cache-Control: no-store, его никто не будет кэшировать, и каждый раз он будет поступать прямо с сервера. Единственный нюанс связан с тем, что клиент уже может хранить ответ в кэше в этом случае, он не будет удален. Чтобы удалить существующий кэш, добавьте к заголовку max-age=0.

Примечательно, что Twitter уже наступал на эти грабли. Они использовали Pragma: no-cache (устаревшую версию того же самого заголовка) вместо Cache-Control: no-store, в результате чего личные сообщения (DM) пользователей оставались в кэшах их браузеров. В случае личного компьютера это не проблема, но если к ПК имеют доступ несколько пользователей, или вы воспользовались публичной машиной, то ваши личные сообщения остались на жестком диске в незашифрованном и доступном для чтения виде. Упс.

HTTP Trailers

Вероятно, вы уже знаете об HTTP-заголовках (headers). HTTP-сообщение начинается с первой строки, которая содержит метод и URL (для запросов) или код состояния/сообщение (для ответов), затем идет ряд пар ключ/значение для метаданных, называемых заголовками (headers), а затем идет тело (body).

Но знаете ли вы, что trailer'ы позволяют добавлять метаданные после тела сообщения?

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

Они применяются в некоторых API-протоколах вроде gRPC и больше всего подходят для метаданных о самом ответе. Например, с помощью trailer'ов можно включать метаданные Server-Timing, чтобы дать клиенту метрики о производительности сервера во время запроса. В этом случае они будут добавляться после полной готовности ответа. Trailer'ы особенно полезны в случае затяжных ответов, например, чтобы включить метаданные о конечном статусе после продолжительного HTTP-потока.

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

  • Для trailer'ов в ответе сервера клиент должен объявить об их поддержке с помощью заголовка TE: trailers в первоначальном запросе.

  • Заголовки исходного запроса должны включать поля trailer'ов, которые будут использоваться впоследствии: Trailer: <field names>.

  • Некоторые заголовки нельзя использовать в trailer'ах, в том числе Content-Length, Cache-Control, Authorization, Host и другие стандартные заголовки, которые необходимы для парсинга, аутентификации или маршрутизации запросов.

Для отправки трейлеров в HTTP/1.1 также потребуется кодировка chunked. В свою очередь, HTTP/2 использует отдельные фреймы для тела и заголовков, так что в этом нет необходимости.

Полный ответ с trailer'ами по HTTP/1.1 может выглядеть следующим образом:

HTTP/1.1 200 OKTransfer-Encoding: chunkedTrailer: My-Trailer-Field[...chunked response body...]My-Trailer-Field: some-extra-metadata

Коды HTTP 1XX

Знаете ли вы, что HTTP-запрос может получать несколько кодов состояния ответа? Сервер может отправлять неограниченное число кодов 1ХХ перед конечным статусом (200, 404 или любым другим). Они выполняют функцию промежуточных ответов и могут включать свои собственные независимые заголовки.

Семейство 1ХХ включает в себя следующие коды: 100, 101, 102, и 103. Они редко используются, но незаменимы в некоторых нишевых сценариях:

HTTP 100

HTTP 100 это ответ сервера о том, что запрос на данный момент в порядке и клиент может продолжать.

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

Он становится полезен в случае, если запрос включает заголовок Expect: 100-continue. Этот заголовок сообщает серверу, что клиент ожидает код 100 и что полное тело запроса не будет отправлено, пока этот код не получен.

Отправка Expect: 100-continue позволяет серверу решить, следует ли получать все тело сообщения (что может занять продолжительное время и съесть массу трафика). Если URL-адреса и заголовков достаточно для того, чтобы отправить ответ (например, отклонить загрузку файла), HTTP 100 быстрый и эффективный способ сделать это. Если сервер действительно хочет получить полное тело, он отправляет промежуточный ответ 100, после чего клиент продолжает пересылку. После завершения процесса передачи запрос обрабатывается как обычный.

HTTP 101

HTTP 101 используется для переключения протоколов. Он означает: Я послал тебе URL и заголовки, а теперь хочу сделать с этим соединением нечто совершенно другое. А именно переключиться на совершенно другой протокол.

Этот код преимущественно используется для организации веб-сокета. Клиент посылает запрос, содержащий следующие два заголовка:

Connection: upgradeUpgrade: websocket

Если сервер согласен, он посылает следующий ответ:

HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: upgrade

После этого обе стороны переходят с HTTP на обмен raw-данными веб-сокета по данному соединению.

Статус 101 также используется для перехода с HTTP/1.1 на HTTP/2 на том же соединении. Также его можно использовать для переключения HTTP-соединений на любые другие протоколы на основе TCP.

Следует отметить, что HTTP/2 не поддерживает данный статус: в нем иной механизм согласования протоколов и абсолютно другой подход к организации веб-сокетов (который практически нигде не поддерживается в настоящее время веб-сокеты всегда базируются на HTTP/1.1).

HTTP 102

HTTP 102 сообщает клиенту, что сервер все еще обрабатывает запрос и ответ скоро будет готов. От ситуации с кодом 100 он отличается тем, что запрос в данном случае был получен полностью и сервер его обрабатывает, а клиент просто ждет.

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

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

HTTP 103

В отличие от остальных кодов семейства, HTTP 103 новый (и модный) статус, предназначенный для частичной замены push-функционала серверов в HTTP/2 (который в настоящее время удаляется из Chrome).

В рамках HTTP 103 сервер может отправить некоторые заголовки заранее до того, как полностью обработает запрос и отправит его. В первую очередь он предназначен для доставки заголовков со ссылками, таких как Link: </style.css>; rel=preload; as=style, тем самым давая клиенту знать о дополнительном контенте (вроде таблиц стилей, JS-скриптов и изображений в запрашиваемых веб-страницах), который можно начать загружать одновременно с полным ответом.

Когда сервер получает запрос, обработка которого занимает некоторое время, он часто не может полностью отправить заголовки ответа до окончания его подготовки. HTTP 103 позволяет серверу незамедлительно передать клиенту список ресурсов для загрузки, не дожидаясь окончания обработки запроса.

Referer

HTTP-заголовок Referer сообщает серверу, с какой страницы был осуществлен переход или какой URL-адрес вызвал загрузку ресурса. Этот заголовок используется практически повсеместно, хотя у него и есть некоторые проблемы с конфиденциальностью.

Примечательная черта referer его неправильное написание. Он появился на заре Интернета, и тогдашняя проверка орфографии Unix не смогла отличить referer от referrer (правильного написания). К моменту, когда на это обратили внимание, заголовок активно использовался в инфраструктуре и инструментах по всему миру, так что изменить его уже не представлялось возможным, и теперь нам приходится жить с заголовком, написанным с ошибкой.

Впрочем, это не особенно важно (если вы, конечно, не пишите код для обработки этого заголовка), но прекрасно характеризует вызовы в области сетевой совместимости.

Чтобы жизнь не казалась медом, новые заголовки конфиденциальности/безопасности, связанные с этим, такие как Referrer-Policy, используют правильное написание.

Случайный UUID веб-сокетов

XKCD в тему. Комментарий в коде гласит: Получено подбрасыванием кости. Это гарантированно случайное значениеXKCD в тему. Комментарий в коде гласит: Получено подбрасыванием кости. Это гарантированно случайное значение

Ранее рассказывалось о том, как запросы HTTP 101 используются для организации веб-сокетов. Полный такой запрос может выглядеть так:

GET /chat HTTP/1.1Host: server.example.comUpgrade: websocketConnection: upgradeSec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==Sec-WebSocket-Protocol: chat, superchatSec-WebSocket-Version: 13Origin: http://example.com

а ответ, запускающий соединение по веб-сокету, следующим образом:

HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: upgradeSec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=Sec-WebSocket-Protocol: chat

Особый интерес здесь вызывает ключSec-WebSocket-Accept. Он предотвращает случайное использование кэширующими прокси websocket-ответов, которые те не понимают, требуя, чтобы ответ включал заголовок, соответствующий заголовку клиента. А именно:

  • Сервер получает от клиента ключ веб-сокета, закодированный в base64;

  • Сервер добавляет к нему UUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11;

  • Сервер хэширует полученную строку, кодирует хэш в base64 и отправляет его обратно.

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

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

Подобный подход отлично работает. Он широко применяется, а его реализация проста и доступна (что отлично), но все же странно, что каждое websocket-соединение в мире основано на одном магическом UUID.

Веб-сокеты и CORS

Коль скоро речь зашла о веб-сокетах: знали ли вы, что они игнорируют все политики CORS и single-origin, которые обычно применяются к HTTP-запросам?

CORS гарантирует, что JavaScript на a.com не может считывать данные с b.com, если только последний явно не разрешает это в своих заголовках ответа.

Это важно по многим причинам. Например, актуально для серверов в локальной сети (публичная веб-страница не должна иметь доступа к маршрутизатору) и состояния браузера (запросы от одного домена не должны иметь доступа к файлам cookies от другого).

К сожалению, веб-сокеты полностью игнорируют CORS, вместо этого предполагая, что все websocket-серверы достаточно современны и продвинуты, чтобы самостоятельно проверять заголовок Origin. Но серверы, как правило, этого не умеют, а многие разработчики понятия об этом не имели, пока я им не рассказал.

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

Короче говоря, при использовании WebSocket API, проверяйте заголовок Origin и/или используйте токены CSRF, прежде чем доверять входящим соединениям.

Заголовки X-*

Давным-давно (в 1982-м) в RFC было заявлено, что использование префикса X- для заголовков сообщений отличный способ отличить кастомные расширения от стандартизированных имен.

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

Паттерн распространен до сих пор часто его можно встретить в HTTP-запросах:

  • X-Shenanigans: none встречается в каждом ответе от API Twilio. Понятия не имею, почему, но приятно знать, что на этот раз никаких махинаций точно не будет.

  • X-Clacks-Overhead: GNU Terry Pratchett дань уважения Терри Пратчетту; название заимствовано из серии книг писателя Плоский мир.

  • X-Requested-With: XMLHttpRequest добавляется различными JS-фреймворками, включая jQuery, чтобы четко отличать AJAX-запросы от запросов ресурсов (они не могут включать кастомные заголовки вроде этого).

  • X-Recruiting: <сообщение-приманка для потенциального сотрудника> многие компании используют подобные заголовки в попытке привлечь специалистов, которые настолько увлечены процессом, что читают заголовки HTTP.

  • X-Powered-By: <фреймворк> рекламирует фреймворк, используемый сервером (или соответствующую технологию). Как правило, это плохая затея.

  • X-Http-Method-Override указывает метод, который по какой-либо причине не может использоваться в качестве метода для запроса (обычно это связано с ограничениями клиента/сети). Плохая идея в наши дни, однако она до сих пор популярна и многие фреймворки ее поддерживают.

  • X-Forwarded-For: <ip> используется многими прокси-серверами и балансировщиками нагрузки для включения исходного IP-адреса запроса в upstream-запросы.

Каждый из этих заголовков по-своему странен и прекрасен, но сам подход нельзя назвать хорошим, поэтому в новых RFC (2011) его применение формально не рекомендуется.

Проблема в том, что многие нестандартные заголовки со временем становятся стандартными. Когда это происходит, приходится либо менять название (убирая префикс X-), ломая все существующие реализации, либо включать название с префиксом X- в стандарт (полностью нивелируя изначальный смысл префикса и навсегда фиксируя в названии лишний шум).

Это крайне неприятно, и некоторые формальные стандарты уже пострадали от этого:

  • Почти все веб-формы в Интернете пересылают данные, используя излишне мудреный и пространный заголовок Content-Type: application/x-www-form-url-encoded.

  • В RFC к HTTP от 1997 года в разделе, где определяются правила парсинга для content-encoding, предписывается, что все реализации считали x-gzip и x-compress эквивалентами gzip и compress соответственно.

  • Стандартным заголовком для настройки фреймов на веб-странице теперь навсегда останетсяX-Frame-Options вместо Frame-Options.

  • Также у нас теперь есть X-Content-Type-Options, X-DNS-Prefetch-Control, X-XSS-Protection и различные заголовки X-Forwarded-* от CDN/прокси. Все они широко используются и уже формально или фактически стали стандартными заголовками для повсеместного применения.

Если необходимо использовать кастомный заголовок, достаточно выбрать имя, не занятое каким-либо стандартом. Чтобы избежать любых возможных коллизий, рассмотрите возможность сделать его уникальным, включив в некое пространство имен (namespacing).


Стандартизация непростое занятие, и HTTP полон нелепых сюрпризов и странных деталей (стоит только пристально на него посмотреть). Жду ваших мыслей/замечаний в Twitter.

P.S. от переводчика

Подробнее..

Перевод Поддержание аккуратной истории в Git с помощью интерактивного rebase

12.01.2021 10:14:24 | Автор: admin

Прим. перев.: эта статья была написана автором Git-клиента Tower, Tobias Gnther, и опубликована в блоге GitLab. В ней просто и наглядно рассказывается об основных возможностях интерактивного rebase'а, что может стать отличным введением для тех, кто только начинает им пользоваться.

Interactive rebase один из самых универсальных инструментов Git'а. В этой статье мы поговорим о том, как с его помощью корректировать сообщения при коммитах, исправлять ошибки, и о многом другом.

Интерактивный rebase иногда называют швейцарским армейским ножом Gitа, поскольку он объединяет в себе так много различных инструментов для совершенно разных сценариев применения. При этом главным вариантом использования, без сомнения, является очистка локальной истории коммитов.

Обратите внимание на слово локальной: rebase следует использовать только для очистки локальной истории коммитов (например, перед включением одной из ваших локальных feature-веток в общую ветку команды). И наоборот, этот мощный инструмент НЕ следует использовать для исправления коммитов в ветке, которая уже загружена и открыта для совместной работы в удаленном репозитории. Интерактивный rebase инструмент для переписывания истории Git, и его не следует использовать для редактирования коммитов, которые уже открыты для других.

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

Примечание: для визуализации сценариев и последовательностей операций для некоторых скриншотов я использовал GUI к Git под названием Tower.

Редактирование сообщения в старом коммите

Иногда вы замечаете опечатку в старом коммите или вспоминаете, что забыли упомянуть нечто важное в его описании. Если бы речь шла о самом последнем коммите, можно было бы воспользоваться опцией --amend команды git commit. Но в случае более старых коммитов придется воспользоваться интерактивным rebaseом.

Вот пример описания к коммиту, которое мы будем исправлять:

Плохое сообщение к коммиту, которое мы будем исправлятьПлохое сообщение к коммиту, которое мы будем исправлять

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

Начинаем с родительского коммитаНачинаем с родительского коммита

Теперь нужно скормить хэш родительского коммита команде:

$ git rebase -i 0023cddd

Откроется окно редактора со списком коммитов для изменения. Не удивляйтесь тому, что они приведены в обратном порядке: в рамках интерактивного rebaseа Git будет повторно применять прошлые коммиты один за другим. Другими словами, с точки зрения Git коммиты выстроены в правильном порядке.

Окно редактора со списком выбранных коммитовОкно редактора со списком выбранных коммитов

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

Редактирование описания старого коммитаРедактирование описания старого коммита

Сохраните изменения и закройте окно. Поздравляю сессия интерактивного rebaseа завершена, сообщение к коммиту успешно отредактировано!

Объединение нескольких коммитов в один

Rebase также можно использовать для объединения нескольких старых коммитов в один. При этом, конечно, актуальным остается золотое правило систем управления версиями: в большинстве случаев лучше создавать множество мелких коммитов, нежели несколько крупных. Однако, как и во всем остальном, мы можем внезапно обнаружить, что несколько перестарались со следованием этому правилу, и решить, что было бы хорошо объединить несколько старых коммитов в один.

Давайте предположим, что нужно объединить следующие выбранные коммиты в один:

Объединяем несколько коммитов в одинОбъединяем несколько коммитов в один

Как и в первом случае, процесс начинается с запуска сессии интерактивного rebaseа на коммите-предшественнике тех, что мы хотим изменить.

$ git rebase -i 2b504bee

Снова откроется окно редактора с историей коммитов, которые мы хотим объединить:

Помечаем нужные строки кодовым словом squashПомечаем нужные строки кодовым словом squash

Действию, которое мы собираемся произвести над коммитами, соответствует кодовое слово squash. В данном случае следует помнить лишь об одной тонкости: строка, помеченная squash, будет объединена со строкой, которая находится выше нее. Именно поэтому на скриншоте выше я пометил словом squash строку 2 (она будет объединена со строкой 1).

Сохраните изменения и закройте окно редактора. Как и в первом случае, появится новое окно с просьбой ввести сообщение для нового, объединенного коммита:

Вводим сообщение для нового коммитаВводим сообщение для нового коммита

Сохраните сообщение и закройте окно. Будет создан новый коммит, содержащий изменения обоих старых коммитов. Вуаля!

Исправление ошибок

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

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

Именно для таких случаев и предназначен fixup. Этот инструмент берет коммит с быстрым исправлением, применяет его изменения к оригинальном коммиту (тем самым исправляя его), и удаляет корректирующий коммит.

Как работает fixupКак работает fixup

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

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

Следующий шаг сделать коммит этих изменений в репозиторий, но с небольшой добавкой: делать коммит надо с флагом --fixup, попутно указав хэш плохого коммита:

$ git add corrections.txt

$ git commit --fixup 2b504bee

Если теперь посмотреть на историю, вы увидите, что был создан ничем не примечательный коммит (разве вы ожидали чего-то иного?). Но при более внимательном взгляде становятся заметны некоторые особенности: к новому коммиту были автоматически добавлены пометка fixup! и описание старого плохого коммита:

Оригинальный коммит и корректирующий коммит (fixup)Оригинальный коммит и корректирующий коммит (fixup)

Теперь пора запускать interactive rebase. Опять же, в качестве отправной точки выбираем коммит, предшествующий плохому:

$ git rebase -i 0023cddd autosquash

А вторым ингредиентом нашего секретного соуса выступает флаг --autosquash. Он позволяет не вносить дополнительных правок в открывшемся окне редактора. Внимательно посмотрите на скриншот:

Корректирующий коммит помечен как fixup и размещен в правильном порядкеКорректирующий коммит помечен как fixup и размещен в правильном порядке

Git автоматически сделал две вещи:

  1. Он пометил новый коммит как fixup.

  2. И переупорядочил строки так, чтобы fixup-коммит оказался непосредственно под плохим коммитом. Дело в том, что fixup работает в точности как squash: он объединяет выделенный коммит с коммитом выше.

Таким образом, нам ничего делать не надо. Сохраните изменения и закройте окно редактора.

Давайте еще раз взглянем на историю коммитов:

Счастливый финал!Счастливый финал!

Мало того, что оригинальный коммит теперь содержит правки из вспомогательного, но и некрасивый вспомогательный коммит (с исправлениями) исчез из истории. Все красиво, словно никогда и не было никаких проблем!

Откройте для себя возможности interactive rebase

Существует множество различных вариантов использования интерактивного rebaseа: большинство из них связаны с исправлением ошибок. Подробнее узнать о других способах можно в бесплатном (англоязычном) курсе First Aid Kit for Git коллекции коротких видео (по 2-3 минуты на эпизод).

Примечание оригинального редактора: забавно, но мне пришлось воспользоваться interactive rebase при редактировании этой статьи! Один из коммитов включал изображение, размер которого превышал 1 Мб (что противоречит правилам сайта GitLab). Пришлось вернуться к этому коммиту и включить в него изображение подходящего размера. Спасибо за урок, Вселенная! ?

P.S. от переводчика

Читайте также в нашем блоге:

Подробнее..

Перевод Почему в InVision затаскивают микросервисы обратно в монолит

02.02.2021 12:18:59 | Автор: admin

Прим. перев.: автор этой статьи Ben Nadel, сооснователь и главный инженер InVision App Inc. Миссию своей команды, поддерживающей серверную инфраструктуру компании, он сам характеризует как advocate for the users, т.к. её главная цель гарантировать пользователям InVision получение опыта, который они заслуживают. Его путь яркая иллюстрация того, что микросервисы не серебряная пуля.

Если вы следите за мной в Твиттере, то наверняка заметили, что периодически я публикую победные твиты о возвращении одного из наших микросервисов в InVision обратно в монолит. Обычно эти твиты сопровождаются гифкой, в которой Танос вставляет последний Камень бесконечности в Перчатку бесконечности. Нахожу ее вполне уместной, поскольку воссоединение камней придает Таносу невероятную силу так же, как воссоединение микросервисов придает силу мне и моей команде. Меня много раз спрашивали, почему я избавляюсь от микросервисов. Настало время поделиться своими мыслями по поводу этого путешествия в мире разработки веб-приложений.

Я не против микросервисов!

Хочу предельно ясно обозначить свою позицию и начать статью с заявления о том, что я не против микросервисов! Возвращение микросервисов в монолит вовсе не представляется мне этаким крестовым походом. На самом деле вся идея в определении правильного размера монолита. По сути, я просто убираю очередную болевую точку для своей команды. Все затевается ради уменьшения числа проблем: иначе я бы не стал тратить время (и другие ресурсы) на подъем, перемещение и рефакторинг старого кода.

Еще один микросервис успешно вернулся в монолит. Сервис почти достиг своего правильного размера! На это ушли 3 недели и около 40 тикетов JIRA.Еще один микросервис успешно вернулся в монолит. Сервис почти достиг своего правильного размера! На это ушли 3 недели и около 40 тикетов JIRA.

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

Микросервисы решают как технические проблемы, так и проблемы людей

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

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

С другой стороны, проблемы людей почти не имеют отношения к приложению и связаны с тем, как организована команда. Чем больше специалистов работают над некой частью приложения, тем более медленным и подверженным ошибкам становится процесс разработки. Например, стремление трех десятков инженеров провести непрерывное развертывание (CD) одного и того же сервиса приведет к образованию очереди. То есть вместо того, чтобы работать, большинство инженеров будут сидеть без дела в ожидании своей очереди на деплой.

Первые микросервисы в InVision решали преимущественно проблемы людей

В самом начале, 8 лет назад, InVision была монолитной системой, над которой работало всего 3 инженера. По мере роста и развития компании число систем почти не менялось, но размер команды начал быстро увеличиваться. Всего за несколько лет в компании появились десятки новых инженеров как бэкенда, так и фронтенда. И все они работали над одной и той же кодовой базой, деплоили в рамках одной и той же очереди обслуживания.

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

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

Закон Конвея хорош, если хороши границы

Работая с микросервисами, вы наверняка слышали о законе Конвея, сформулированном Мелвином Конвеем в 1967 году. Он гласит:

Любая организация, которая разрабатывает систему (в широком смысле), вынуждена создавать проекты, структуры которых являются копией структуры связей организации.

Этот закон часто иллюстрируется примером с компилятором:

Если четыре команды работают над компилятором, то на выходе вы получите четырехпроходной компилятор.

Идея здесь заключается в том, что решение оптимизировано с учетом командных структур (и командной коммуникации) и не обязательно предназначено для решения каких-либо конкретных технических или эксплуатационных проблем.

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

Конечно, преимущества закона Конвея сильно зависят от того, как провести границы и как они эволюционируют со временем. И вот тут я и моя команда, Rainbow Team, вступаем в игру.

В течение всех этих лет InVision приходилось эволюционировать как с точки зрения организации, так и инфраструктуры. Это означает, что под капотом скрыта как старая legacy-платформа, так и растущая современная. По мере миграции команд на современную платформу сервисы, за которые отвечали эти команды, приходится передавать остающимся legacy-командам.

Сегодня в 2020-м моя команда выступает legacy-командой. Число сервисов, за которые она отвечает, медленно, но верно растет. И это означает: меньше людей, но больше репозиториев, языков программирования, баз данных, панелей мониторинга, журналов ошибок и бессонных ночей.

Короче говоря, все преимущества закона Конвея для организации со временем превратились в обязательства для моей legacy-команды. И поэтому мы пытаемся привести в норму нашу сферу ответственности, вернув баланс в закон Конвея. Другими словами, мы меняем границы сервисов так, чтобы они совпали с границей зоны ответственности нашей команды. А именно, мы возвращаем их в монолит.

Микросервисы это не микро. У них правильный размер

Пожалуй, худшее, что случилось с микросервисной архитектурой это термин микро, которым ее наградили. Микро бессмысленный, но сильно нагруженный термин, пропитанный историческими коннотациями и человеческими предубеждениями. Гораздо более правильным был бы термин правильный размер (right sized). Микросервисы никогда не задумывались как маленькие; на самом деле подразумевалось, что они имеют адекватный, правильный размер.

Приставка микро ни к чему не относится; она ничего не означает, не влечет за собой никаких последствий. С другой стороны, правильный размер указывает на то, что сервис был разработан так, чтобы удовлетворять требованиям, предъявляемым к нему. То есть он отвечает за правильный объем функциональности. И эта правильность не отлита в металле она зависит от команды, ее навыков, состояния организации, расчетной рентабельности инвестиций, стоимости владения и момента времени, когда этот сервис работает.

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

Цель одна, пути ее достижения кардинально отличаются.

И я очень горжусь своей командой и нашими усилиями по обслуживанию legacy-платформы. Наш отряд невелик, но его достижения с использованием имеющихся ресурсов заслуживают уважения. Подобный успех я приписываю глубокому знанию устройства legacy-платформы, агрессивному прагматизму и постоянным усилиям, направленным на разработку системы, которая соответствует нашим возможностям (вместо того, чтобы пытаться раздуть возможности до уровня существующей системы). Этот подход может показаться ограниченным, но он единственный подходит нам с учетом ресурсов, которыми мы располагаем в настоящее время.

Эпилог: большинству технологий не требуется независимое масштабирование

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

Из всех аргументов в пользу независимых сервисов этот встречается наиболее часто, но по моему (весьма ограниченному) мнению, обычно он не актуален. Заботиться о независимой масштабируемости, пожалуй, имеет смысл только в том случае, если некая часть функциональности привязана к процессору, памяти или вводу/выводу. Большую часть времени серверы пребывают в ожидании работы; добавление новых обработчиков HTTP-маршрутов в приложение вряд ли приведет к внезапному исчерпанию всех ресурсов.

Если бы я мог вернуться в момент, когда мы проводили разбивку на микросервисы, я бы наверняка начал со всей завязанной на процессор деятельности: обработки и изменения размера изображений, генерации миниатюр, экспорту/импорту PDF, управления версиями файлов с rdiff, генерации zip-архивов. Я бы разбил команды с учетом этих границ и заставил бы сделать чистые сервисы, которые бы имели дело только со входами и выходами (другими словами, никаких интеграционных баз данных, никаких общих файловых систем), чтобы все остальные сервисы могли их использовать, сохраняя слабую связанность.

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

Эпилог: у микросервисов также есть вполне ощутимая финансовая сторона

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

Усилия моей команды по возвращению микросервисов в монолит благоприятно сказались на расходах компании. Конечно, нельзя сказать, что вклад огромен: ведь речь идет всего лишь о нескольких небольших сервисах, но он явно не нулевой. Таким образом, в дополнение к человеческим преимуществам от объединения систем, мы получаем еще и финансовую выгоду.

P.S. от переводчика

Читайте также в нашем блоге:

Подробнее..

Прогресс shell-operator и addon-operator хуки как admission webhooks, Helm 3, OpenAPI, хуки на Go и многое другое

18.01.2021 10:10:41 | Автор: admin

Shell-operator и addon-operator Open Source-проекты компании Флант для администраторов Kubernetes, представленные в апреле 2019 года. Первый призван упростить создание K8s-операторов: для этого с ним достаточно писать простые скрипты (на Bash, Python и т.п.) или любые бинарники, которые будут вызываться в случае наступления определённых событий в Kubernetes API. Второй (addon-operator) его старший брат, цель которого упростить установку Helm-чартов в кластер, используя для их настройки хуки shell-operatorа.

В последний раз мы рассказывали о возможностях shell-operator по состоянию на релиз v1.0.0-beta.11 (летом прошлого года), если не считать последовавшего доклада на KubeCon EU2020, который знакомил с проектом тех, кто о нём ещё не знает. (К слову, этот доклад мы по-прежнему рекомендуем всем желающим разобраться, как shell-operator облегчает жизнь при создании операторов, и увидеть наглядные примеры его применения.)

За минувшее время и shell-operator, и addon-operator получили множество интересных новшеств, которым и посвящена эта статья.

Изменения в shell-operator v1.0.0-rc1

Хуки для shell-operator теперь можно использовать как обработчики для ValidatingWebhookConfiguration одного из вариантов admission webhook. Т.е. хук может проверить ресурс Kubernetes во время создания или редактирования и отклонить операцию, если ресурс не соответствует каким-то правилам. Таким правилом может быть такая политика: можно создавать только ресурсы с образом из repo.example.com. Пример реализации подобной политики можно посмотреть в директории 204-validating-webhook. Shell-operator поддерживает такие хуки для кластеров Kubernetes с версией не ниже 1.16.

Иллюстрация того, как происходит конфигурация такого хука (фрагмент shell-хука из примера выше):

function __config__(){    cat <<EOFconfigVersion: v1kubernetesValidating:- name: private-repo-policy.example.com  namespace:    labelSelector:      matchLabels:        # helm adds a 'name' label to a namespace it creates        name: example-204  rules:  - apiGroups:   ["stable.example.com"]    apiVersions: ["v1"]    operations:  ["CREATE", "UPDATE"]    resources:   ["crontabs"]    scope:       "Namespaced"EOF}

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

{"group":"group_name_1", "action":"expire"}

Это удобно, когда пропадают объекты, сведения о которых были в метриках. Подробный пример разобран в документации.

Остальные значимые нововведения в shell-operator разбиты на категории:

1. Улучшения в потреблении ресурсов и производительности

  • Период ресинхронизации информеров теперь случайно распределён во времени. Без распределения все информеры одновременно обращались к API-серверу, что периодически создавало лишнюю нагрузку.

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

  • В операциях на чтение в очередях сделаны read-only-локи вместо общего лока и на запись, и на чтение.

  • Добавлены метрики с процессорным временем и с потреблением памяти для каждого хука (см. shell_operator_hook_run_sys_cpu_seconds в METRICS).

2. Изменения в сборке образа

  • Теперь flant/shell-operator это образ с поддержкой архитектур AMD64, ARM и ARM64 (привет любителям Raspberry Pi!).

  • Бинарный файл shell-operator собирается статически и должен работать в любом Linux-дистрибутиве.

  • Образ flant/shell-operator с Bash, kubectl и jq теперь только на основе Alpine. Если требуется другой дистрибутив, то бинарный файл можно взять из основного образа, а Dockerfile есть в примерах.

  • Убрана директория .git, попавшая в образ по ошибке.

  • Обновлены версии инструментов: Alpine 3.12, kubectl 1.19.4, Go 1.15.

  • Бинарный файл jq собран из того же коммита, что и libjq*, чтобы устранить проблемы производительности jq-1.6 (#206).

* Кстати, libjq-go это наш небольшой Open Source-проект, предлагающий CGO bindings для jq. Он был создан для нужд shell-operator, но недавно мы встретили и другой пример его использования в проекте Xbus. Это платформа французской компании для интеграции enterprise-систем, построенная поверх NATS. Здорово видеть, когда Open Source сам делает своё полезное дело даже в небольших проектах, от которых ничего особого не ожидаешь.

3. Менее значимые изменения

  • В лог при старте записываются предупреждения про файлы хуков без флага исполнения (+x).

  • Проект можно собирать без включения CGO. Теперь удобно использовать shell-operator в других проектах, если быстрый обработчик jqFilter не нужен.

  • Добавлен shell_lib.sh, чтобы подключать shell framework одной строкой. Пример использования этой библиотеки мы демонстрировали в уже упомянутом докладе на KubeCon.

Новости addon-operator

Последний релиз addon-operator состоялся в начале прошлого года, с тех пор в нем было по-настоящему много изменений.

Одно из главных поддержка схем OpenAPI для values. Можно задавать контракты для values, которые нужны для Helm, и для config values, которые хранятся в ConfigMap и используются для конфигурации модулей пользователем.

Например, такая схема определяет два обязательных поля для глобальных values (project и clusterName), а также два опциональных поля (строка clusterHostname и объект discovery без ограничений по ключам):

# /global/openapi/config-values.yamltype: objectadditionalProperties: falserequired:  - project  - clusterNameminProperties: 2properties:  project:    type: string  clusterName:    type: string  clusterHostname:    type: string  discovery:    type: object

Подробнее см. в документации.

Ещё одно знаковое событие экспериментальная поддержка написания хуков на языке Go. Для их работы придётся компилировать свой addon-operator, добавив импорты с путями к хукам. Пример их использования можно найти в каталоге 700-go-hooks.

Иллюстрация глобального хука на Go из примера выше:

package global_hooksimport "github.com/flant/addon-operator/sdk"var _ = sdk.Register(&GoHook{})type GoHook struct {sdk.CommonGoHook}func (h *GoHook) Metadata() sdk.HookMetadata {return h.CommonMetadataFromRuntime()}func (h *GoHook) Config() *sdk.HookConfig {return h.CommonGoHook.Config(&sdk.HookConfig{YamlConfig: `configVersion: v1onStartup: 10`,MainHandler: h.Main,})}func (h *GoHook) Main(input *sdk.BindingInput) (*sdk.BindingOutput, error) {input.LogEntry.Infof("Start Global Go hook")return nil, nil}

Реализация соответствующего SDK пока находится на уровне альфа-версии и не может похвастать достаточной документацией, но если вас заинтересовала такая возможность смело спрашивайте в комментариях, а лучше приходите в Tg-канал @kubeoperator.

Среди других ключевых изменений в addon-operator выделим следующие:

  • Поддержка установки модулей с помощью Helm 3.

  • Введены понятия сходимости и сходимости при старте это название цикла рестарта всех модулей. Добавлен endpoint для readiness-пробы: Pod addon-operatorа переводится в состояние Ready, когда прошёл первый старт всех модулей, т.е. сходимость при старте достигнута.

  • Возможность включать модули из глобальных хуков, благодаря чему теперь проще регулировать состав модулей (ранее отключить модуль можно было только enabled-скриптом самого модуля).

  • Запуск информеров и запуск Synchronization для Kubernetes-хуков теперь производится в отдельных очередях, а также появилась возможность отключить ожидание выполнения таких хуков при старте.

  • Сборка образа изменена аналогично shell-operatorу: Alpine в качестве основы, мультиплатформенный образ, статический бинарный файл и т.д.

  • Доступно больше метрик для мониторинга состояния подробнее в METRICS.

Также в addon-operator перекочевали многие улучшения из shell-operator, была актуализирована документация и сделаны другие мелкие правки. В данный момент заканчиваются работы по поддержке схем OpenAPI, после чего будет опубликован релиз v1.0.0-rc1.

Новые применения shell-operator

За минувшее время shell-operator обрёл не только новые возможности, но и новых пользователей. Среди них отметим следующие известные нам проекты:

  • В Confluent сделали проект Kafka DevOps. В нём реализована модель production-окружения, в котором запущено streaming-приложение, пишущее в Apache Kafka на Confluent Cloud. Это окружение построено на основе Kubernetes, приложения и ресурсы в котором управляются в духе декларативной инфраструктуры. В частности, для этого реализованы операторы (Confluent Cloud Operator и Kafka Connect Operator) на основе на shell-operator. Подробнее об этом проекте можно почитать в блоге авторов, а совсем недавно они даже выпустили подкаст, где рассказывают о своём Kafka DevOps.

  • Образовательный проект edukates подготовил практическое занятие по shell-operator, однако его дальнейшая судьба осталась для нас под вопросом (найти его в опубликованном виде на сайте проекта нам не удалось).

  • Docker Captain из Германии создал специальный контроллер для обновления DNS-записей при рестарте podа с Traefik. Вскоре после этого он узнал про shell-operator и перевел свою разработку на его использование.

  • Solution Architect из Red Hat занялся созданием r53-operator оператора для кастомных доменов, который управляет доменами Ingress в AWS Route 53.

Если у вас тоже есть опыт применения shell-operator, будем рады соответствующим рассказам в GitHub Discussions проекта: собирая такие примеры, мы надеемся помочь широкому сообществу инженеров. Случаи использования addon-operator гораздо более редкое явление, так что им мы будем рады вдвойне.

Заключение

Shell-operator и addon-operator давно используются нами в ежедневной работе. Основные проблемы изучены и устранены, а сейчас в проекты преимущественно добавляются новые возможности. В ближайших планах для shell-operator поддержка conversion webhook и возможность писать хуки без побочных эффектов, т.е. не вызывать kubectl для изменений в кластере, а возвращать в shell-operator набор действий (см. #94, #239).

Фактически оба проекта давно вышли из статуса beta, поэтому мы решили синхронизироваться с реальностью и представляем их версии rc1, а следующий релиз shell-operator в этом году может стать окончательным v1.0.0.

P.S. В ноябре прошлого года shell-operator преодолел рубеж в 1000 звёзд на GitHub, а addon-operator более скромные 250. Спасибо всем, кто заинтересовался проектами!

P.P.S.

Читайте также в нашем блоге:

Подробнее..

Аварии как опыт 2. Как развалить Elasticsearch при переносе внутри Kubernetes

28.01.2021 10:04:30 | Автор: admin

В нашей внутренней production-инфраструктуре есть не слишком критичный участок, на котором периодически обкатываются различные технические решения, в том числе и различные версии Rook для stateful-приложений. На момент проведения описываемых работ эта часть инфраструктуры работала на основе Kubernetes-кластера версии 1.15, и возникла потребность в его обновлении.

За заказ persistent volumes в кластере отвечал Rook версии 0.9. Мало того, что этот оператор сам по себе был старой версии, его Helm-релиз содержал ресурсы с deprecated-версиями API, что препятствовало обновлению кластера. Решив не возиться с обновлением Rook вживую, мы стали полностью разбирать его.

Внимание! Это история провала: не повторяйте описанные ниже действия в production, не прочитав внимательно до конца.

Итак, вынос данных в хранилища StorageClassов, не управляемых Rookом, шел уже несколько часов успешно


Беспростойная миграция данных Elasticsearch

когда дело дошло до развернутого в Kubernetes кластера Elasticsearch из 3-х узлов:

~ $ kubectl -n kibana-production get po | grep elasticsearchelasticsearch-0                               1/1     Running     0         77d2helasticsearch-1                               1/1     Running     0         77d2helasticsearch-2                               1/1     Running     0         77d2h

Для него было принято решение осуществить переезд на новые PV без простоя. Конфиг в ConfigMap был проверен и сюрпризов не ожидалось. Хотя в алгоритме действий по миграции и присутствует пара опасных поворотов, чреватых аварией при выпадении узлов Kubernetes-кластера, эти узлы работают стабильно да и вообще: Я сто раз так делал, так что поехали!

1. Вносим изменения в StatefulSet в Helm-чарте для Elasticsearch (es-data-statefulset.yaml):

apiVersion: apps/v1kind: StatefulSetmetadata:  labels:    component: {{ template "fullname" . }}    role: data  name: {{ template "fullname" . }}spec:  serviceName: {{ template "fullname" . }}-data volumeClaimTemplates:  - metadata:      name: data      annotations:        volume.beta.kubernetes.io/storage-class: "high-speed"

В последней строчке (с определением storage class) было ранее указано значение rbd вместо нынешнего high-speed.

2. Удаляем существующий StatefulSet с cascade=false. Это опасный поворот, потому что наличие podов с ES больше не контролируется StatefulSetом и в случае внезапного отказа какого-либо K8s-узла, на котором запущен pod с ES, этот pod не возродится автоматически. Однако операция некаскадного удаления StatefulSet и его редеплоя с новыми параметрами занимает секунды, поэтому риски относительны (т.е. зависят от конкретного окружения, конечно).

Приступим:

 $ kubectl -n kibana-production delete sts elasticsearch --cascade=falsestatefulset.apps "elasticsearch" deleted

3. Деплоим заново наш Elasticsearch, а затем масштабируем StatefulSet до 6 реплик:

~ $ kubectl -n kibana-production scale sts elasticsearch --replicas=6statefulset.apps/elasticsearch scaled

и смотрим на результат:

~ $ kubectl -n kibana-production get po | grep elasticsearchelasticsearch-0                               1/1     Running     0         77d2helasticsearch-1                               1/1     Running     0         77d2helasticsearch-2                               1/1     Running     0         77d2helasticsearch-3                               1/1     Running     0         11melasticsearch-4                               1/1     Running     0         10melasticsearch-5                               1/1     Running     0         10m~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.33.142  8 98 49 7.89 4.86 3.45 dim - elasticsearch-410.244.33.118 26 98 35 7.89 4.86 3.45 dim - elasticsearch-210.244.33.140  8 98 60 7.89 4.86 3.45 dim - elasticsearch-310.244.21.71   8 93 58 8.53 6.25 4.39 dim - elasticsearch-510.244.33.120 23 98 33 7.89 4.86 3.45 dim - elasticsearch-010.244.33.119  8 98 34 7.89 4.86 3.45 dim * elasticsearch-1

Картина с хранилищем данных:

~ $ kubectl -n kibana-production get get pvc | grep elasticsearchNAME                   STATUS        VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS    AGEdata-elasticsearch-0   Bound   pvc-a830fb81-...   12Gi       RWO            rbd             77ddata-elasticsearch-1   Bound   pvc-02de4333-...   12Gi       RWO            rbd             77ddata-elasticsearch-2   Bound   pvc-6ed66ff0-...   12Gi       RWO            rbd             77ddata-elasticsearch-3   Bound   pvc-74f3b9b8-...   12Gi       RWO            high-speed      12mdata-elasticsearch-4   Bound   pvc-16cfd735-...   12Gi       RWO            high-speed      12mdata-elasticsearch-5   Bound   pvc-0fb9dbd4-...   12Gi       RWO            high-speed      12m

Отлично!

4. Добавим бодрости переносу данных.

Если в вас еще жив дух авантюризма и неудержимо влечет к приключениям (т.е. данные в окружении не столь критичны), можно ускорить процесс, оставив одну реплику индексов:

~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -H "Content-Type: application/json" -X PUT -sk https://localhost:9200/my-index-pattern-*/_settings -d '{"number_of_replicas": 0}'{"acknowledged":true}

но мы, конечно, так делать не будем:

~ $ ^C~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -H "Content-Type: application/json" -X PUT -sk https://localhost:9200/my-index-pattern-*/_settings -d '{"number_of_replicas": 2}'{"acknowledged":true}

Иначе утрата одного podа приведет к неконсистентности данных до его восстановления, а утрата хотя бы одного PV в случае ошибки приведет к потере данных.

Увеличим лимиты перебалансировки:

[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -XPUT -H 'Content-Type: application/json' -sk https://localhost:9200/_cluster/settings?pretty -d '{>   "transient" :{>     "cluster.routing.allocation.cluster_concurrent_rebalance" : 20,>     "cluster.routing.allocation.node_concurrent_recoveries" : 20,>     "cluster.routing.allocation.node_concurrent_incoming_recoveries" : 10,>     "cluster.routing.allocation.node_concurrent_outgoing_recoveries" : 10,>     "indices.recovery.max_bytes_per_sec" : "200mb">   }> }'{  "acknowledged" : true,  "persistent" : { },  "transient" : {    "cluster" : {      "routing" : {        "allocation" : {          "node_concurrent_incoming_recoveries" : "10",          "cluster_concurrent_rebalance" : "20",          "node_concurrent_recoveries" : "20",          "node_concurrent_outgoing_recoveries" : "10"        }      }    },    "indices" : {      "recovery" : {        "max_bytes_per_sec" : "200mb"      }    }  }}

5. Выгоним шарды с первых трех старых узлов ES:

[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -XPUT -H 'Content-Type: application/json' -sk https://localhost:9200/_cluster/settings?pretty -d '{>   "transient" :{>       "cluster.routing.allocation.exclude._ip" : "10.244.33.120,10.244.33.119,10.244.33.118">    }> }'{  "acknowledged" : true,  "persistent" : { },  "transient" : {    "cluster" : {      "routing" : {        "allocation" : {          "exclude" : {            "_ip" : "10.244.33.120,10.244.33.119,10.244.33.118"          }        }      }    }  }}

Вскоре получим первые 3 узла без данных:

[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/shards | grep 'elasticsearch-[0..2]' | wc -l0

6. Пришла пора по одной убить старые узлы Elasticsearch.

Готовим вручную три PersistentVolumeClaim такого вида:

~ $ cat pvc2.yaml---apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: data-elasticsearch-2spec:  accessModes: [ "ReadWriteOnce" ]  resources:    requests:      storage: 12Gi  storageClassName: "high-speed"

Удаляем по очереди PVC и pod у реплик 0, 1 и 2, друг за другом. При этом создаем PVC вручную и убеждаемся, что экземпляр ES в новом podе, порожденном StatefulSetом, успешно запрыгнул в кластер ES:

~ $ kubectl -n kibana-production delete pvc data-elasticsearch-2 persistentvolumeclaim "data-elasticsearch-2" deleted^C~ $ kubectl -n kibana-production delete po elasticsearch-2pod "elasticsearch-2" deleted~ $ kubectl -n kibana-production apply -f pvc2.yamlpersistentvolumeclaim/data-elasticsearch-2 created~ $ kubectl -n kibana-production get po | grep elasticsearchelasticsearch-0                               1/1     Running     0         77d3helasticsearch-1                               1/1     Running     0         77d3helasticsearch-2                               1/1     Running     0         67selasticsearch-3                               1/1     Running     0         42melasticsearch-4                               1/1     Running     0         41melasticsearch-5                               1/1     Running     0         41m~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.21.71  21 97 38 3.61 4.11 3.47 dim - elasticsearch-510.244.33.120 17 98 99 8.11 9.26 9.52 dim - elasticsearch-010.244.33.140 20 97 38 3.61 4.11 3.47 dim - elasticsearch-310.244.33.119 12 97 38 3.61 4.11 3.47 dim * elasticsearch-110.244.34.142 20 97 38 3.61 4.11 3.47 dim - elasticsearch-410.244.33.89  17 97 38 3.61 4.11 3.47 dim - elasticsearch-2

Наконец, добираемся до ES-узла 0: удаляем pod elasticsearch-0, после чего он успешно запускается с новым storageClass, заказывает себе PV. Результат:

~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.33.151 17 98 99 8.11 9.26 9.52 dim * elasticsearch-0

При этом в соседнем podе:

~ $ kubectl -n kibana-production exec -ti elasticsearch-1 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.21.71  16 97 27 2.59 2.76 2.57 dim - elasticsearch-510.244.33.140 20 97 38 2.59 2.76 2.57 dim - elasticsearch-310.244.33.35  12 97 38 2.59 2.76 2.57 dim - elasticsearch-110.244.34.142 20 97 38 2.59 2.76 2.57 dim - elasticsearch-410.244.33.89  17 97 98 7.20 7.53 7.51 dim * elasticsearch-2

Поздравляю: мы получили split-brain в production! И сейчас новые данные случайным образом сыпятся в два разных кластера ES!

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

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

Может, скинуть label у podа elasticsearch-0, чтобы исключить его из балансировки на уровне Service? Но ведь, исключив pod, мы не сможем его затолкать обратно в кластер ES, потому что при формировании кластера обнаружение членов кластера происходит через тот же Service!

За это отвечает переменная окружения:

        env:        - name: DISCOVERY_SERVICE          value: elasticsearch

и ее использование в ConfigMapе elasticsearch.yaml (см. документацию):

discovery:      zen:        ping.unicast.hosts: ${DISCOVERY_SERVICE}

В общем, по такому пути мы не пойдем. Вместо этого лучше срочно остановить workers, которые пишут данные в ES в реальном времени. Для этого отмасштабируем все три нужных deploymentа в 0. (К слову, хорошо, что приложение придерживается микросервисной архитектуры и не надо останавливать весь сервис целиком.)

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

Причина аварии и восстановление

В чем же дело? Почему узел 0 не присоединился к кластеру? Еще раз проверяем конфигурационные файлы: с ними все в порядке.

Проверяем внимательно еще раз Helm-чарты вот же оно! Итак, проблемный es-data-statefulset.yaml:

apiVersion: apps/v1kind: StatefulSetmetadata:  labels:    component: {{ template "fullname" . }}    role: data  name: {{ template "fullname" . }}     containers:      - name: elasticsearch        env:        {{- range $key, $value :=  .Values.data.env }}        - name: {{ $key }}          value: {{ $value | quote }}        {{- end }}        - name: cluster.initial_master_nodes     # !!!!!!          value: "{{ template "fullname" . }}-0" # !!!!!!        - name: CLUSTER_NAME          value: myesdb        - name: NODE_NAME          valueFrom:            fieldRef:              fieldPath: metadata.name        - name: DISCOVERY_SERVICE          value: elasticsearch        - name: KUBERNETES_NAMESPACE          valueFrom:            fieldRef:              fieldPath: metadata.namespace        - name: ES_JAVA_OPTS          value: "-Xms{{ .Values.data.heapMemory }} -Xmx{{ .Values.data.heapMemory }} -Xlog:disable -Xlog:all=warning:stderr:utctime,level,tags -Xlog:gc=debug:stderr:utctime -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.host=127.0.0.1 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.port=9099 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"...

Зачем же так определены initial_master_nodes?! Здесь задано (см. документацию) жесткое ограничение, что при первичном старте кластера в выборах мастера участвует только 0-й узел. Так и произошло: pod elasticsearch-0 поднялся с пустым PV, начался процесс бутстрапа кластера, а мастер в podе elasticsearch-2 был благополучно проигнорирован.

Ок, добавим в ConfigMap на живую:

~ $ kubectl -n kibana-production edit cm elasticsearchapiVersion: v1data:  elasticsearch.yml: |-    cluster:      name: ${CLUSTER_NAME}      initial_master_nodes:        - elasticsearch-0        - elasticsearch-1        - elasticsearch-2...

и удалим эту переменную окружения из StatefulSet:

~ $ kubectl -n kibana-production edit sts elasticsearch...      - env:        - name: cluster.initial_master_nodes          value: "elasticsearch-0"...

StatefulSet начинает перекатывать все podы по очереди согласно стратегии RollingUpdate, и делает это, разумеется, с конца, т.е. от 5-го podа к 0-му:

~ $ kubectl -n kibana-production get poNAME              READY   STATUS        RESTARTS   AGEelasticsearch-0   1/1     Running       0          11melasticsearch-1   1/1     Running       0          13melasticsearch-2   1/1     Running       0          15melasticsearch-3   1/1     Running       0          67melasticsearch-4   1/1     Running       0          67melasticsearch-5   0/1     Terminating   0          67m

Что произойдет, когда перекат дойдет до конца? Как отработает бутстрап кластера? Ведь перекат StatefulSet идет быстро как пройдут выборы в таких условиях, если даже в документации заявляется, что auto-bootstrapping is inherently unsafe? Не получим ли мы кластер, забустрапленный из 0-го узла с огрызком индекса?Примерно из-за таких мыслей спокойно наблюдать за происходящим у меня ну никак не получалось.

Забегая вперёд: нет, в заданных условиях всё будет хорошо. Однако 100% уверенности в тот момент не было. А ведь это production, данных много, они критичны для бизнеса, а это чревато дополнительной возней с бэкапами.

Поэтому, пока перекат не докатился до 0-го podа, сохраним и убьем сервис, отвечающий за discovery:

~ $ kubectl -n kibana-production get svc elasticsearch -o yaml > elasticsearch.yaml~ $ kubectl -n kibana-production delete svc elasticsearch service "elasticsearch" deleted

и оторвем PVC у 0-го podа:

~ $ kubectl -n kibana-production delete pvc data-elasticsearch-0 persistentvolumeclaim "data-elasticsearch-0" deleted^C

Теперь, когда перекат прошел, elasticsearch-0 в состоянии Pending из-за отсутствия PVC, а кластер полностью развален, т.к. узлы ES потеряли друг друга:

~ $ kubectl -n kibana-production exec -ti elasticsearch-1 bash[root@elasticsearch-1 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodesOpen Distro Security not initialized.

На всякий случай исправим ConfigMap вот так:

~ $ kubectl -n kibana-production edit cm elasticsearchapiVersion: v1data:  elasticsearch.yml: |-    cluster:      name: ${CLUSTER_NAME}      initial_master_nodes:        - elasticsearch-3        - elasticsearch-4        - elasticsearch-5...

После этого создадим новый пустой PV для elasticsearch-0, создав PVC:

 $ kubectl -n kibana-production apply -f pvc0.yamlpersistentvolumeclaim/data-elasticsearch-0 created

И перезапустим узлы для применения изменений в ConfigMap:

~ $ kubectl -n kibana-production delete po elasticsearch-0 elasticsearch-1 elasticsearch-2 elasticsearch-3 elasticsearch-4 elasticsearch-5pod "elasticsearch-0" deletedpod "elasticsearch-1" deletedpod "elasticsearch-2" deletedpod "elasticsearch-3" deletedpod "elasticsearch-4" deletedpod "elasticsearch-5" deleted

Можно возвращать на место сервис из недавно сохраненного нами YAML-манифеста:

~ $ kubectl -n kibana-production apply -f elasticsearch.yaml service/elasticsearch created

Посмотрим, что получилось:

~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.98.100  11 98 32 4.95 3.32 2.87 dim - elasticsearch-010.244.101.157 12 97 26 3.15 3.00 2.10 dim - elasticsearch-310.244.107.179 10 97 38 1.66 2.46 2.52 dim * elasticsearch-110.244.107.180  6 97 38 1.66 2.46 2.52 dim - elasticsearch-210.244.100.94   9 92 36 2.23 2.03 1.94 dim - elasticsearch-510.244.97.25    8 98 42 4.46 4.92 3.79 dim - elasticsearch-4[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/indices | grep -v green | wc -l0

Ура! Выборы прошли нормально, кластер собрался полностью, индексы на месте.

Осталось только:

  1. Снова вернуть в ConfigMap значения initial_master_nodes для elasticsearch-0..2;

  2. Еще раз перезапустить все podы;

  3. Аналогично шагу, описанному в начале статьи, выгнать все шарды на узлы 0..2 и отмасштабировать кластер с 6 до 3-х узлов;

  4. Наконец, сделанные вручную изменения донести до репозитория.

Заключение

Какие уроки можно извлечь из данного случая?

Работая с переносом данных в production, всегда следует иметь в виду, что что-то может пойти не так: будет допущена ошибка в конфигурации приложения или сервиса, произойдет внезапная авария в ЦОД, начнутся сетевые проблемы да все что угодно! Соответственно, перед началом работ должны быть предприняты меры, которые позволят либо предотвратить аварию, либо максимально купировать ее последствия. Обязательно должен быть подготовлен План Б.

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

  1. Выполнить переезд в тестовом окружении с production-конфигурацией ES.

  2. Запланировать простой сервиса. Либо временно переключить нагрузку на другой кластер. (Предпочтительный путь зависит от требований к доступности.) В случае варианта с простоем следовало предварительно остановить workers, пишущие данные в Elasticsearch, снять затем свежую резервную копию, а после этого приступить к работам по переносу данных в новое хранилище.

P.S.

Читайте также в нашем блоге:

Подробнее..

Мониторим основные сервисы в AWS с Prometheus и exporterами для CloudWatch

12.02.2021 14:21:36 | Автор: admin

Если вы используете в облачных провайдерах managed-инсталляции серверных служб вроде RDS или ElastiCache от AWS, то скорее всего уже задавались темой мониторинга инфраструктуры, а главное оповещений по произошедшим инцидентам. При реализации возникают понятные вопросы:

  1. Как можно настроить сбор данных с endpointов в систему мониторинга?

  2. Если использовать Prometheus, то какие экспортеры использовать и где их можно запускать?

  3. Какие есть варианты готовых алертов для покрытия основных причин аварий/частичной недоступности?

Эта статья в большей степени рассчитана на начинающих инженеров: на примере Prometheus и CloudWatch мы рассмотрим одно из самых простых и понятных решений с помощью cloudwatch_exporter и prometheus_aws_cost_exporter в AWS, напишем для них Helm-чарт и задеплоим его в Kubernetes. (K8s будет выступать удобной площадкой для разворачивания экспортеров.) А также посмотрим, как можно мониторить текущие и ежедневные затраты всей вашей инфраструктуры.


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

У AWS есть отдельные типы инстансов с бюджетами кредитов по CPU и дисков с кредитами по IO. Они позволяют накапливать бюджет кредитов во время сниженной нагрузки и тратить его в том случае, если она резко выросла. Но прогнозировать, когда нагрузка может уйти, проблематично. Поэтому есть риск выработать весь бюджет и перейти на режим ограниченного потребления ресурсов. Данный вариант развития событий сложно диагностировать, так как в мониторинге сервера это напрямую не будет отражено. Поэтому очень полезно мониторить кредиты CPU/IO, чтобы понимать, какое количество кредитов доступно в данный момент, какова динамика их потребления и предвидеть их исчерпание.

Итак, возвращаясь к выбранным инструментам: prometheus_aws_cost_exporter будет использоваться для мониторинга потребления финансов, так как cloudwatch_exporter возвращает информацию только за предыдущий день. Зато cloudwatch_exporter позволяет снимать гораздо больше метрик.

Приступим к непосредственной реализации!

1. Настраиваем IAM

Поскольку рассматривать мы будем два экспортера с немного различным функционалом, потребуются два разных аккаунта в IAM (AWS Identity and Access Management). Ниже представлен список ролей, которые нужны обоим аккаунтам:

  • cloudwatch:ListMetrics

  • cloudwatch:GetMetricStatistics

  • tag:GetResources

Для работы prometheus_aws_cost_exporter требуется больший набор прав: необходимо создать отдельную роль и назначить её пользователю. Для удобства роль можно создать из JSON:

{  "Effect": "Allow",  "Action": [    "cloudwatch:PutMetricData",    "ec2:DescribeVolumes",    "ec2:DescribeTags",    "logs:PutLogEvents",    "logs:DescribeLogStreams",    "logs:DescribeLogGroups",    "logs:CreateLogStream",    "logs:CreateLogGroup",    "ce:GetCostAndUsage"  ],  "Resource": "*"},{  "Effect": "Allow",  "Action": [    "ssm:GetParameter"  ],  "Resource": [    "arn:aws:ssm:*:*:parameter/AmazonCloudWatch-*",    "arn:aws:ce:*:*:/GetCostAndUsage"  ]}

Для учётных записей экспортеров также требуется создать access key ID и secret access key, которые будут передаваться в виде переменных (AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY).

2. Создаем IAM-роли через веб-интерфейс

Авторизуемся в консоли управления AWS и перейдем в раздел IAM, где для примера создадим пользователя под названием cloudwatch_users.

Создание пользователя в IAMСоздание пользователя в IAM

В поле Access Type включим опцию Programmatic access, которая позволит сгенерировать упомянутые access key ID и secret access key (они потребуются для работы с API, сохраните их куда-нибудь в надёжное хранилище). Следующая вкладка Attach existing policies directly, где мы создадим новую политику. Для данной IAM-роли требуются права ListMetrics и GetMetricStatistics.

Создание пользовательского policyСоздание пользовательского policy

Если удобнее создавать роль через API, можно воспользоваться JSON-сниппетом:

{  "Version": "2012-10-17",  "Statement": [    {      "Sid": "VisualEditor0",      "Effect": "Allow",      "Action": [        "cloudwatch:GetMetricStatistics",        "cloudwatch:ListMetrics"      ],      "Resource": "*"    }  ]}

После нажатия на Review policy указываем название для Policy и создаем её (Create policy). Дальнейшие пункты не влияют на функции созданной роли. Однако потребуется вернуться на этап создания IAM-роли, чтобы добавить созданный нами Policy. На самом последнем этапе станут доступны для просмотра значения для переменных AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY, которые надо записать в values.yaml нашего будущего Helm-чарта.

Если вы используете Terraform, то по данной ссылке есть готовый Terraform receipt для создания IAM-роли и пользователей. Ключи API из terraform.tfstate можно получить с помощью jq:

jq '.resources[].instances[].attributes | {(.id): .secret}'

Примечание: запросы к CloudWatch тарифицируются; актуальные цены можно изучить здесь. Исходя из этой информации можете выбрать, как часто выполнять запросы к API например, раз в час или в сутки в зависимости от задачи.

3. Helm-чарт для экспортеров

Перейдем к деплою cloudwatch-exporter и cost-exporter в Kubernetes. Потребуется написать очень простой Helm-чарт, который будет состоять из нескольких простых объектов.

В самом начале объявим необходимые переменные в values.yaml:

---aws_access_key_id: <AWS_ACCESS_KEY_ID>aws_secret_access_key: <AWS_SECRET_ACCESS_KEY>region: eu-central-1replicas:  1resources:  requests:    cpu: 1m    memory: 512Mienv:  metric_today_daily_costs: "yes"  metric_yesterday_daily_costs: "yes"  query_period: "1800"  metric_today_daily_usage: "yes"  metric_today_daily_usage_norm: "yes"

Подробнее о содержимом этого файла:

  • В переменных aws_access_key_id и aws_secret_access_key объявляются значения, полученные при создании IAM-роли;

  • region регион, в котором требуется выполнять мониторинг ресурсов;

  • query_period периодичность запроса метрик из AWS (в секундах);

  • metric_today_daily_costs, metric_yesterday_daily_costs, metric_today_daily_usage, metric_today_daily_usage_norm включение/выключение запрашивания метрик затрат (costs) и потребления (usage) за вчера и за сегодня (по умолчанию имеет значение no);

  • Параметры из блока env используются cost-exporterом (на работу cloudwatch-exporter не влияют).

Ниже пример листинга с Deployment для cloudwatch-exporter, который носит иллюстративный характер (содержит только основную структуру для удобства чтения). Полная версия доступна здесь.

apiVersion: apps/v1kind: Deploymentmetadata:  name: cloudwatch-exporterspec:  selector:    matchLabels:      app: cloudwatch-exporter  template:    metadata:      labels:        app: cloudwatch-exporter    spec:      containers:      - name: cloudwatch-exporter        image: prom/cloudwatch-exporter:cloudwatch_exporter-0.9.0        env:        - name: AWS_ACCESS_KEY_ID          value: "{{ .Values.aws_access_key_id }}"        - name: AWS_SECRET_ACCESS_KEY          value: "{{ .Values.aws_secret_access_key }}"        volumeMounts:        - name: config          subPath: config.yml          mountPath: /config/config.yml      volumes:      - name: config        configMap:          name: config

Следующий Deployment (точнее, снова его фрагмент) для cost-exporter:

apiVersion: apps/v1kind: Deploymentmetadata:  name: cost-exporterspec:  selector:    matchLabels:      app: cost-exporter  template:    metadata:      labels:        app: cost-exporter    spec:      containers:      - name: cost-exporter        image: nachomillangarcia/prometheus_aws_cost_exporter:latest        args:        - --host        - 0.0.0.0        env:        - name: AWS_ACCESS_KEY_ID          value: "{{ .Values.aws_access_key_id }}"        - name: AWS_SECRET_ACCESS_KEY          value: "{{ .Values.aws_secret_access_key }}"        - name: METRIC_TODAY_DAILY_COSTS          value: "{{ .Values.env.metric_today_daily_costs }}"        - name: METRIC_YESTERDAY_DAILY_COSTS          value: "{{ .Values.env.metric_yesterday_daily_costs }}"        - name: QUERY_PERIOD          value: "{{ .Values.env.query_period }}"        - name: METRIC_TODAY_DAILY_USAGE          value: "{{ .Values.env.metric_today_daily_usage }}"        - name: METRIC_TODAY_DAILY_USAGE_NORM          value: "{{ .Values.env.metric_today_daily_usage_norm }}"

4. Настраиваем метрики для мониторинга

Остается настроить самое главное те метрики, которые будут собираться в Prometheus и на основании значений которых мы хотим делать алерты.

Здесь есть пример конфигурационного файла для cloudwatch_exporter с перечислением необходимых для мониторинга метрик.Посмотреть все доступные для мониторинга метрики например, для EC2 можно с помощью консольной утилиты aws:

aws cloudwatch list-metrics --namespace EC2

Все параметры конфига берутся из вывода консольной утилиты aws. Типичный фрагмент конфига:

  - aws_namespace: AWS/NetworkELB    aws_metric_name: HealthyHostCount    aws_dimensions:    - LoadBalancer    - TargetGroup    aws_statistics:    - Sum    period_seconds: 60

Этот фрагмент указывает экспортеру брать из AWS/NetworkELB метрику HealthyHostCount с периодичностью 60 секунд, агрегировать её по LoadBalancer и TargetGroup, выдавать значение Sum.

Бонус! Несколько примеров алертов

Вот так выглядит алерт на использование CPU в Redis у ElastiCache:

  - alert: RedisCPUUsage    annotations:    description: |      Redis CPU utilization on {{`{{$labels.cache_cluster_id}}`}} in cluster is over than 60%    summary: Redis CPU utilization on {{`{{$labels.cache_cluster_id}}`}} in cluster is over than 60%    expr: |      aws_elasticache_cpuutilization_average >= 60    for: 5m

Алерт на количество target у LoadBalancer:

  - alert: LBTargetGroupIsUnhealthy    annotations:    description: Some hosts are target group {{`{{$labels.target_group}}`}} in cluster is unhealthy!    summary: Some hosts are target group {{`{{$labels.target_group}}`}} in cluster is unhealthy!    expr: |      aws_networkelb_healthy_host_count_sum{load_balancer=~".*someservice.*",target_group=~".*someservice.*"} < 3    for: 1m

Алерт на исчерпание EBS Burst balance:

  - alert: EBSBurst_balance    annotations:         description: EBS Burst balance in cluster is less than 60%                     summary: EBS Burst balance in cluster is less than 60%    expr: |      aws_ebs_burst_balance_average <= 60             for: 5m

В репозитории представлен более обширный список примеров для конфигурации метрик и алертов.

Примеры, как могут выглядеть графики в Prometheus:

AWS EC2 EBS IO balance (average)AWS EC2 EBS IO balance (average)AWS ElastiCache CPU utilization (average)AWS ElastiCache CPU utilization (average)

Заключение

В статье рассмотрен мониторинг сервисов AWS с помощью известных Prometheus exporterов. Настроив его описанным образом, можно получить удобный инструмент для анализа состояния managed-инфраструктуры и ее стоимости, что поможет скорректировать расходы и вовремя получать информацию о потребляемых финансовых ресурсах.

За рамками материала остались некоторые вопросы взаимодействия с Prometheus (как ему сообщать, откуда и куда scrapeить метрики), а также создание панелей в Grafana. (Кстати, для prometheus_aws_cost_exporter есть dashboard от создателя.) Разобравшись и с ними для своего конкретного случая, можно получить более полное, законченное решение.

P.S.

Читайте также в нашем блоге:

Подробнее..

Перевод Вертикальное автомасштабирование podов в Kubernetes полное руководство

16.02.2021 10:21:44 | Автор: admin

Прим перев.: месяц назад Povilas Versockas, CNCF Ambassador и software engineer из Литвы, написал очень подробную статью о том, как работает и как использовать VPA в Kubernetes. Рады поделиться её переводом для русскоязычной аудитории!

Это полное руководство по вертикальному автомасштабированию pod'ов (Vertical Pod Autoscaling, VPA) в Kubernetes. Вот его краткое содержание:

  • Зачем нам VPA?

  • Модель ресурсных требований Kubernetes;

  • Что такое вертикальное автомасштабирование pod'ов?

  • Работа с рекомендациями;

  • Когда использовать VPA?

  • Ограничения VPA;

  • Реальные примеры использования;

  • Как работает VPA?

  • Модель рекомендаций VPA;

  • Дополнительная информация.

Схема работы Kubernetes VPA от Banzai CloudСхема работы Kubernetes VPA от Banzai Cloud

Что ж, давайте приступим.

Зачем нам VPA?

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

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

Кроме того, со временем модель использования приложения может меняться. Некоторым приложениям потребуется больше CPU и памяти. У других, менее популярных, требования к ресурсам, наоборот, снизятся.

  • С недостатком заявленных ресурсов обычно разбираются DevOps- или SRE-инженеры при поступлении соответствующих оповещений. SRE-инженеры видят, что приложение отбрасывает запросы конечных пользователей из-за убийств pod'ов, вызванных ошибкой Out-of-Memory, или оно начинает медленно работать из-за троттлинга процессора.

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

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

В свою очередь, вертикальное масштабирование определяет оптимальные требования к CPU и памяти. В этой статье пойдет речь исключительно о вертикальном автомасштабировании pod'ов.

Но сначала давайте поговорим о модели ресурсных требований Kubernetes.

Модель ресурсных требований Kubernetes

Kubernetes требует от пользователей указывать заявки на ресурсы с помощью resource requests (запросов на ресурсы) и resource limits (лимитов на ресурсы). Давайте начнем с запросов:

Запросы на ресурсы резервируют некоторое количество ресурсов за приложением. Можно определять запросы для контейнеров в pod'е. Планировщик использует эту информацию, чтобы определить, куда разместить pod. Запросы можно представить как некоторый минимальный объем ресурсов, который требуется pod'у для нормальной работы.

Тут важно отметить, что приложение может задействовать больше ресурсов, если узел располагает свободными мощностями. А максимальный объем ресурсов, которыми может воспользоваться контейнер, устанавливается в лимитах. Если потребление памяти окажется больше указанного предела, pod будет убит. Если контейнер использует больше процессорной мощности, чем позволяет лимит, начинается троттлинг.

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

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

Кроме того, если вы не определите запросы, Kubernetes автоматически приравняет их к лимитам pod'а.

Многие ограничиваются заданием запросов на ресурсы, и это распространенная ошибка. Пользователи надеются, что в этом случае приложение будет располагать неограниченными ресурсами и ему не придется иметь дело с нехваткой памяти или троттлингом. Однако Kubernetes этого не допустит. Поэтому обязательно задавайте как запросы на ресурсы (resource requests), так и лимиты (limits).

Более того, эту ресурсную модель можно расширить. Могут быть и другие вычислительные ресурсы, такие как эфемерное хранилище, GPU, huge pages в Linux.

В статье же мы ограничимся процессорными мощностями и памятью, поскольку на данный момент Vertical Pod Autoscaler работает только с ними. Тем, кто желает узнать больше, рекомендую обратиться к соответствующему разделу документации Kubernetes (Managing Resources for Containers).

Что такое вертикальное автомасштабирование pod'ов?

Как следует из названия, вертикальное автомасштабирование pod'ов (VPA) позволяет автоматически устанавливать запросы на ресурсы и лимиты для контейнеров. Решения принимаются на основе прошлых данных об использовании CPU и памяти.

Основная цель VPA уменьшить потери ресурсов и минимизировать риск снижения производительности из-за троттлинга CPU или ошибок, вызванных убийством pod'ов из-за Out Of Memory.

Поддержкой VPA занимаются инженеры Google. Система называется Autopilot и основана на опыте создания соответствующей внутренней системы для оркестратора контейнеров Borg. Результаты Google от использования Autopilot в production следующие:

На практике избыток ресурсов для заданий под управлением Autopilot составил всего 23% по сравнению с 46% для заданий, управляемых вручную. Кроме того, Autopilot на порядок сократил количество заданий, пострадавших от OOM.

Autopilot: workload autoscaling at Google

Дополнительную информацию можно почерпнуть из самой публикации (Autopilot: workload autoscaling at Google).

VPA вводит несколько Custom Resource Definitions (CRD) для управления поведением автоматических рекомендаций. Как правило, разработчикам требуется добавить объект VerticalPodAutoscaler в свои deploymentы.

Давайте разберемся, как его использовать.

Как использовать VPA?

Ресурс VPA предоставляют массу возможностей для управления рекомендациями. Чтобы получить лучшее представление об использовании VPA, посмотрим на сам объект VerticalPodAutoscaler:

apiVersion: autoscaling.k8s.io/v1kind: VerticalPodAutoscalermetadata:  name: prometheus-vpaspec:  targetRef:    apiVersion: "apps/v1"    kind: StatefulSet    name: prometheus  updatePolicy:     updateMode: "Recreate"    containerPolicies:      - containerName: "*"        minAllowed:          cpu: 0m          memory: 0Mi        maxAllowed:          cpu: 1          memory: 500Mi        controlledResources: ["cpu", "memory"]        controlledValues: RequestsAndLimits

Настройка VerticalPodAutoscaler начинается с задания targetRef, указывающего на некий контроллер-объект Kubernetes, отвечающий за управление pod'ами.

VPA поддерживает все распространенные типы контроллеров: Deployment, StatefulSet, DaemonSet, CronJobs. Он также должен работать с любыми кастомными типами, реализующими подресурс scale. VPA получает набор pod'ов с помощью метода контроллера ScaleStatus. В примере выше мы автомасштабируем StatefulSet с именем prometheus.

Поле updateMode позволяет выбрать режим работы контроллера. Есть несколько вариантов:

  • Off VPA не будет автоматически изменять ресурсные требования. Autoscaler подсчитывает рекомендации и хранит их в поле status объекта VPA;

  • Initial VPA устанавливает запросы на ресурсы только при создании pod'а и не меняет их потом;

  • Recreate VPA устанавливает запросы на ресурсы при создании pod'ов и обновляет их для существующих pod'ов, вытесняя (evict) в случаях, когда запрашиваемые ресурсы значительно отличаются от новой рекомендации;

  • Auto в настоящее время делает то же самое, что и Recreate. В будущем возможно использование обновлений без перезапуска (restart-free updates), когда этот механизм станет доступен (подробнее о нем рассказывается, например, в этом видео прим. перев.).

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

Вы определяете список resource policies, которые фильтруются по containerName. Можно выбрать конкретный контейнер в pod'е и сопоставить его с некой resource policy. Также можно указать * в качестве значения containerName этим вы определите resource policy по умолчанию (на случай, если ни одна другая resource policy не соответствует containerName).

Resource policies позволяют ограничить ресурсные рекомендации диапазоном, лежащем между minAllowed и maxAllowed. В случае, если minAllowed и maxAllowed не заданы, ресурсы не ограничены.

С помощью controlledResources можно выбрать ресурсы для рекомендаций. Пока поддерживаются только CPU и память. Если типы ресурсов не указаны, то VPA будет давать рекомендации как по использованию процессора, так и по использованию памяти.

Наконец, controlledValues позволяет выбрать, какие параметры будут контролироваться: RequestsOnly (только запросы на ресурсы) или RequestsAndLimits (запросы на ресурсы и лимиты). Значение по умолчанию RequestsAndLimits.

Если выбрать RequestsAndLimits, то запросы будут вычисляться на основе фактического использования. Тем временем, лимиты будут вычисляться на основе текущего соотношения между запросами и лимитами pod'а. Например, если pod изначально запрашивает 1 CPU, а его лимит установлен на 2 CPU, то VPA будет устанавливать лимит таким образом, чтобы тот всегда в два раза превышал запрос. Аналогичный способ расчета применяется и к памяти. Поэтому в режиме RequestsAndLimits рассматривайте изначально заданные для приложения запросы на ресурсы и лимиты как некий шаблон.

Объект VPA можно упростить, используя режим Auto и вычисляя рекомендации для CPU и памяти. А именно:

apiVersion: autoscaling.k8s.io/v1kind: VerticalPodAutoscalermetadata:  name: vpa-recommenderspec:  targetRef:    apiVersion: "apps/v1"    kind: Deployment    name: vpa-recommender  updatePolicy:     updateMode: "Auto"  resourcePolicy:    containerPolicies:      - containerName: "*"        controlledResources: ["cpu", "memory"]

Теперь давайте посмотрим на рекомендации, которые VPA записывает в поле status соответствующего CRD.

Работа с рекомендациями

Как только вы примените (apply) объект VeritcalPodAutoscaler, VPA начнет собирать данные об использовании ресурсов и вычислять рекомендации по ним. Спустя некоторое время в поле status объекта VerticalPodAutoscaler должны появиться рекомендации.

Просмотреть их можно с помощью:

kubectl describe vpa NAME

Давайте проанализируем пример отчета о состоянии:

Status:   Conditions:    Last Transition Time:  2020-12-23T08:03:07Z    Status:                True    Type:                  RecommendationProvided  Recommendation:      Container Recommendations:      Container Name:  prometheus      Lower Bound:          Cpu:     25m          Memory:  380220488      Target:          Cpu:     410m          Memory:  380258472      Uncapped Target:          Cpu:     410m          Memory:  380258472      Upper Bound:          Cpu:     704m          Memory:  464927423

Как видно, для контейнера prometheus предлагаются четыре различные оценки. При этом оценки объема памяти приводятся в байтах. Оценки CPU в миллиядрах (m, millicores). Давайте разберемся, что означают эти оценки:

  • Lower bound (нижняя граница) минимальная оценка для контейнера. Это значение не гарантирует, что приложение сможет стабильно работать. Такие минимальные запросы на CPU и память, скорее всего, окажут значительное влияние на производительность и доступность.

  • Upper bound (верхняя граница) это максимальный рекомендованный объем ресурсов для контейнера. Запросы выше этих значений, скорее всего, будут приводить к тому, что ресурсы будут расходоваться впустую.

  • Оценку Target (цель) мы будем использовать для задания запросов на ресурсы.

Все эти оценки ограничены значениями minAllowed / maxAllowed в containerPolicies.

  • Uncapped target (неограниченная цель) это целевая оценка, которая получилась бы, если бы ограничения minAllowed и maxAllowed не были заданы.

Зачем нам четыре оценки? Vertical Pod Autoscaler использует Lower и Upper bound для вытеснения (eviction) pod'ов. Если текущий resource request ниже, чем lower bound, или выше, чем upper bound, и происходит 10%-ное изменение ресурсных запросов по сравнению с target-оценкой, то может произойти вытеснение.

Классно то, что VPA добавляет аннотации к pod'у при изменении требований к ресурсам. Если сделать describe pod'а, контролируемого VPA, то можно увидеть аннотации вроде vpaObservedContainers (перечисление отслеживаемых контейнеров) или vpaUpdates (описание предпринятых действий). Также здесь можно увидеть, ограничена ли рекомендация параметрами minAllowed/maxAllowed или Kubernetes-объектом LimitRange. Вот пример аннотаций pod'а:

apiVersion: v1kind: Podmetadata:  annotations:    vpaObservedContainers: recommender    vpaUpdates: 'Pod resources updated by vpa-recommender: container 0: cpu request, memory request, cpu limit, memory limit' 

Давайте разберемся, в каких случаях следует использовать Vertical Pod Autoscaler.

Когда использовать VPA?

Во-первых, можно добавить VPA к базам данных и stateful-нагрузкам при их запуске в Kubernetes. Как правило, stateful-нагрузки тяжелее поддаются горизонтальному масштабированию, поэтому автоматический способ, позволяющий отмасштабировать потребляемые ресурсы или точно оценить потребность в них, помогает решить многие проблемы с недостатком мощностей. Если база данных не настроена как высокодоступная или не готова к перерывам в работе, можно включить режимы Initial или Off. В этом режиме VPA не будет вытеснять pod'ы и ограничится рекомендациями запросов или их обновлением при перевыкате приложения.

Во-вторых, VPA хорошо подходит для CronJobs. Vertical Pod Autoscaler способен проанализировать потребление ресурсов повторяющимися заданиями и применить рекомендации, полученные на основе этих данных, к очередному запланированному запуску. Для этого нужно установить режим рекомендаций в Initial. В таком случае каждое только что запущенное задание будет получать рекомендации, подсчитанные на основе прошлого запуска того же задания. Важно отметить, что это не работает для кратковременных (менее 1 минуты) заданий.

В-третьих, stateless-нагрузки отличный кандидат для Vertical Pod Autoscaling. Stateless-приложения обычно менее чувствительны к перерывам в работе и вытеснению, так что это отличный кандидат для старта. На них можно протестировать режимы Auto и Recreate. Одно существенное ограничение состоит в том, что VPA не будет работать совместно с горизонтальным автомасштабированием, если оно производится по тем же самым метрикам: CPU или памяти. Как правило, VPA используют с приложениями с предсказуемым потреблением ресурсов, а также в том случае, если запуск более чем нескольких реплик не имеет смысла. Подобный тип приложений не имеет смысла масштабировать горизонтально, и для них VPA правильный выбор.

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

Ограничения VPA

  1. Прежде всего, не используйте VPA с рабочими нагрузками на базе JVM. Дело в том, что JVM не позволяет установить объем фактически используемой памяти, поэтому рекомендации могут сильно отклоняться от адекватных значений.

  2. Также не стоит использовать VPA совместно с горизонтальным автомасштабированием (HPA), основанном на тех же метриках (CPU или памяти). В то же время два этих типа можно применять совместно, если HPA работает с кастомными метриками.

  3. Рекомендации VPA могут превысить доступные ресурсы, такие как ресурсы кластера или квота вашей команды. Недостаток ресурсов может привести к тому, что pod'ы окажутся в состоянии Pending. С помощью объектов LimitRange можно ограничивать запросы ресурсов для конкретного пространства имен. Также можно устанавливать максимальные допустимые рекомендации по ресурсам для pod'а в объекте VerticalPodAutoscaler.

  4. VPA в режиме Auto или Recreate не будет выселять pod'ы с единственной репликой, так как это приведет к простою в работе. Однако желающие включить автоматические рекомендации для приложений с единственной репликой могут изменить такое поведение. Для этого в компоненте updater имеется флаг --min-replicas.

  5. При работе в режиме RequestsAndLimits устанавливайте первичные лимиты для CPU таким образом, чтобы они многократно превышали request'ы. Связано это с известной проблемой Kubernetes/ядра Linux, которая в некоторых случаях может приводить к излишнему троттлингу (ситуация подробно разобрана в этой статье прим. перев.). Многие пользователи Kubernetes либо полностью отключают троттлинг, либо устанавливают огромные лимиты CPU, чтобы обойти проблему. Как правило, это не приводит к плохим последствиям, поскольку использование CPU на узлах кластера обычно невелико.

  6. Не все рекомендации VPA достигают своей цели. Предположим, что у вас имеется высокодоступная система из двух реплик, и один из контейнеров решает быстро нарастить объем используемой памяти. Такое стремительное увеличение потребляемой памяти может привести к тому, что контейнер будет убит из-за Out of Memory. Поскольку pod'ы, убитые Out Of Memory, не планируются заново, VPA не сможет применить новые рекомендации для ресурсов. Вытеснение pod'а также не произойдет, поскольку один pod всегда либо Not Ready, либо попал в crash loop. То есть вы оказались в тупике. Единственный способ разрешить эти ситуации убить pod и позволить новым рекомендациям вступить в силу.

Теперь давайте рассмотрим несколько примеров из реальной жизни.

Реальные примеры использования

Кластер MongoDB

Давайте начнем с кластера MongoDB, состоящего из трех реплик. Первоначальные требования StatefulSet'а к ресурсам таковы:

resources:  limits:    memory: 10Gi  requests:    memory: 6Gi

Pod Disruption Budget допускает отключение только одной реплики.

Далее мы разворачиваем StatefulSet без Vertical Pod Autoscaling и даем ему поработать некоторое время.

На этом графике показано использование памяти кластером MongoDB. Каждая из линий соответствует отдельной реплике. Видно, что фактическое использование памяти для двух реплик близко к 3 Гб, а для одной около 1,5 Гб.

Спустя некоторое время мы включаем автоматизацию ресурсных требований, устанавливая объект Vertical Pod Autoscaler в режим Auto (автоматическое масштабирование ресурсов CPU и памяти). VPA рассчитывает рекомендацию и последовательно выселяет pod'ы. Вот как может выглядеть рекомендация:

Container Recommendations:   Container Name:  mongodb  Lower Bound:    Cpu:     12m    Memory:  3480839981  Target:    Cpu:     12m    Memory:  3666791614  Uncapped Target:    Cpu:     12m    Memory:  3666791614  Upper Bound:    Cpu:     12m    Memory:  3872270071

VPA установил запросы на память на 3,41 Гб, лимит на 5,6 Гб (такое же отношение, как у 6 Гб и 10 Гб), запросы и лимиты для CPU на 12 миллиядер.

Давайте посмотрим, как это соотносится с первоначальными оценками. Мы запросили на 1,6 Гб меньше памяти на каждый pod. Таким образом, в общей сложности мы сэкономили 4,8 Гб памяти. Разница может показаться не особо существенной, но в случае большого числа кластеров MongoDB объем сэкономленной памяти стремительно возрастает.

etcd

Другой пример etcd. Это высокодоступная база данных, использующая Raft в качестве алгоритма выбора лидера. Первоначально запрашиваются только ресурсы CPU:

limits:  cpu: 7requests:  cpu: 10m

Далее мы разворачиваем StatefulSet без Vertical Pod Autoscaling и даем ему поработать некоторое время:

На графике показано использование памяти кластером etcd. Каждая из линий соответствует отдельной реплике. Как видно, одна реплика использует около 500 Мб, две другие по 300 Мб.

А этот график показывает использование CPU. Видно, что оно относительно постоянно и равно 0,03 ядра процессора.

Вот как выглядит рекомендация VPA:

Recommendation:    Container Recommendations:      Container Name:  etcd      Lower Bound:        Cpu:     25m        Memory:  587748019      Target:        Cpu:     93m        Memory:  628694953      Uncapped Target:        Cpu:     93m        Memory:  628694953      Upper Bound:        Cpu:     114m        Memory:  659017860

VPA запросил 599 Мб памяти (без лимитов) и 93 миллиядра CPU (0.093 ядра) с лимитом в 65 ядер (придерживаясь первоначально установленного соотношения запрос/лимит 1 к 700).

Таким образом, VPA зарезервировал предостаточно ресурсов для полноценной работы etcd. Изначально мы не запрашивали память для данного pod'а, что может привести к его планированию на слишком загруженный узел и вызвать проблемы. Аналогичным образом, запрошенные ресурсы CPU оказались недостаточны для работы etcd.

В нашем случае интересным открытием стало то, что текущий лидер использует значительно больше памяти, чем остальные реплики. Как видно, VPA рекомендовал одинаковый объем памяти всем репликам. Таким образом, существует разрыв между запрошенным и используемым объемом памяти. Поскольку вторичные узлы не будут использовать более 300 Мб памяти, пока не станут первичными, на каждом из этих узлов будут оставаться невостребованные ресурсы.

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

Резервирование с CronJob

В заключительном примере пойдет речь о простом задании, которое запускается по расписанию, снимает копию базы MongoDB и сохраняет ее в S3. Задание запускается ежедневно и обычно занимает около 12 минут.

Изначально запросы на ресурсы установлены не были. Объект VPA установлен в режим Initial, автомасштабируются память и CPU.

Первая пара запусков прошла без запросов на ресурсы: VPA собирал данные об их использовании. В это время VPA выводил ошибку No pods match this VPA object. Для третьего запуска VPA предложил следующие рекомендации:

Recommendation:    Container Recommendations:      Container Name:  backupjob      Lower Bound:        Cpu:     25m        Memory:  262144k      Target:        Cpu:     25m        Memory:  262144k      Uncapped Target:        Cpu:     25m        Memory:  262144k      Upper Bound:        Cpu:     507m        Memory:  530622257

И при очередном запуске задания был создан pod с запросами на 25m CPU и 262144k памяти. Главный плюс всего этого в том, что поскольку VPA работает в режиме Initial, никаких вытеснений или перебоев в работе не происходит.

Теперь давайте разберемся, как работает Vertical Pod Autoscaling.

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

Vertical Pod Autoscaler состоит из трех различных компонентов:

  • Recommender использует некоторые эвристики для подсчета рекомендаций;

  • Updater отвечает за вытеснение pod'ов в случае, когда происходит значительное изменение ресурсных требований;

  • Компонент Admission Controller задает ресурсные требования pod'а.

Теоретически любой из компонентов можно заменить кастомным. И все должно по-прежнему работать. Давайте рассмотрим компоненты подробнее:

Recommender

Recommender содержит основную логику для оценки требующихся ресурсов. Он отслеживает их фактическое потребление и события out-of-memory, выдает рекомендации для запросов на ресурсы CPU и памяти для контейнеров. Текущие рекомендации хранятся в поле status объекта VerticalPodAutoscaler.

Можно выбрать, как именно Recommender будет получать начальную статистику по использованию CPU и памяти. Он поддерживает контрольные точки (checkpoints; установлены по умолчанию) и Prometheus. Изменить это можно с помощью флага --storage.

Контрольные точки хранят агрегированные метрики для CPU и памяти в CRD-объектах VerticalPodAutoscalerCheckpoint. Просмотреть сохраненные значения можно с помощью describe. Recommender поддерживает контрольные точки на основе сигналов, поступающих в реальном времени, которые он начинает собирать после загрузки исторических метрик.

При работе с Prometheus Recommender выполняет запрос на PromQL, в котором используются метрики cAdvisor. Recommender позволяет настроить лейблы, используемые в запросе. Можно менять пространство имен, имена pod'ов/контейнеров, лейблы имен заданий Prometheus. В общем, он будет посылать запросы, похожие на этот:

rate(container_cpu_usage_seconds_total{job="kubernetes-cadvisor"}[8d]

и на этот:

container_memory_working_set_bytes{job="kubernetes-cadvisor"}

Результатом таких запросов станет информация об использовании CPU и памяти. Recommender проанализирует результаты и будет использовать их для рекомендаций ресурсов.

После загрузки исторических метрик он начнет в реальном времени собирать метрики с API-сервера Kubernetes через Metrics API (аналогично команде kubectl top). Кроме того, он будет следить за событиями Out Of Memory, чтобы сразу адаптироваться к таким ситуациям. Далее VPA подсчитывает рекомендации, сохраняет их в объекты VPA и отслеживает контрольные точки. Интервал опроса можно настроить с помощью флага --recommender-interval.

О том, как VPA подсчитывает рекомендации, рассказано в следующем разделе (Модель рекомендаций VPA).

Updater

Updater отвечает за соответствие ресурсных требований pod'ов рекомендациям. Если VerticalPodAutoscaler работает в режиме Recreate или Auto, Updater может вытеснить pod, чтобы пересоздать его с новыми ресурсами. В будущем режим Auto скорее всего воспользуется преимуществами обновлений на месте (in-place updates), что позволит избежать вытеснения. Впрочем, работа над этой функцией пока не завершена. За ходом работ можно последить в этом issue на GitHub.

При этом в Updater встроен ряд защитных механизмов, ограничивающих вытеснение pod'ов:

  • Он не будет вытеснять pod, у которого нет по крайней мере двух реплик. Изменить такое поведение можно с помощью флага --min-replicas.

  • Поскольку используется API Kubernetes для вытеснения, Updater соблюдает Pod Disruption Budgets. PDB позволяют задать требования к доступности, чтобы предотвратить вытеснение слишком большого числа pod'ов. Например, если установить максимальное число недоступных (max unavailable) pod'ов равным единице, то компонент сможет вытеснять только один pod. Подробнее о PDB здесь.

  • По умолчанию вытесняется не более 50% pod'ов одного ReplicaSet. Даже если PDB не используются, Updater все равно будет вытеснять pod'ы медленно. Изменить это можно с помощью флага --eviction-tolerance.

  • Также можно настроить глобальный ограничитель скорости вытеснения с помощью флагов --eviction-rate-limit и --eviction-rate-burst. По умолчанию они отключены.

Updater принимает решение о вытеснении podов на основе нижней и верхней границ. Он вытеснит pod, если запрос на ресурсы меньше нижней границы или больше верхней, а также присутствует значительное изменение запросов на ресурсы по сравнений с целевой оценкой. В настоящее время пороговая разница составляет 10%.

После вытеснения pod'а в игру вступает последний компонент Admission Controller. Он отвечает за создание pod'а и применение рекомендаций.

Admission Controller

Компонент Admission Controller задает ресурсные требования pod'а.

Перед планированием pod'а Admission Controller получает webhook-запрос от API-сервера Kubernetes на обновление спецификации pod'а. Admission Controller делает это через конфигурацию mutating webhookа (подробнее в документации к Kubernetes Admission Control). Просмотреть mutating webhookи можно с помощью следующей команды:

kubectl get mutatingwebhookconfigurations

Если VPA установлен правильным образом, вы увидите конфигурацию mutating webhookа для Admission Controller'а VPA.

Как только Admission Controller получает запрос, он сопоставляет его с объектом VerticalPodAutoscaler. Если они не совпадают, pod остается без изменений. Если pod соответствует объекту VPA, Admission Controller (в зависимости от настроек объекта VPA) может обновить или только запросы на ресурсы pod'а, или запросы вместе с лимитами. Обратите внимание, что изменения в ресурсные требования pod'а не будут вноситься, если режим обновления установлен в Off.

Давайте теперь разберемся, как VPA рекомендует ресурсы.

Модель рекомендаций VPA для CPU

Предположим, у нас есть контейнер, и мы снимали данные об использовании CPU каждую минуту в течение 48 часов. График загрузки CPU выглядит следующим образом:

Для подсчета рекомендации для CPU мы создаем гистограмму с экспоненциально растущими границами интервалов. Первый интервал начинается от 0,01 ядра (1 миллиядра) и заканчивается примерно на 1000 ядрах CPU. Каждый интервал растет экспоненциально со скоростью 5%.

При добавлении данных об использовании CPU в гистограмму мы находим интервал, в который попадает фактическое использование процессора, и добавляем вес, зависящий от текущего запрошенного значения для контейнера.

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

Кроме того, мы уменьшаем вес со временем (по умолчанию период его полураспада равен 24 часам). Таким образом, при добавлении в гистограмму данных, с получения которых прошли сутки, их вес составит половину от запрошенных контейнером ресурсов в то время. Подобный распад позволяет увеличить значимость более поздних выборок (то есть они оказывают большее влияние на предсказания, нежели ранние данные). Период полураспада можно изменить с помощью флага --cpu-histogram-decay-half-life.

Давайте превратим график использования CPU, приведенный выше, в подобную гистограмму (с экспоненциальным ростом интервалов и данными, взвешенными с учетом распада). Предположим, что в течение всех 48 часов запрос на мощности CPU составляет 1 ядро.

Гистограмма будет выглядеть следующим образом:

Примечание: мы построили график только для первых 36 интервалов, поскольку остальные интервалы пусты. Значения интервалов варьируются в диапазоне от 0 до 0,958 ядра CPU (округленно). 37-й интервал имеет значение 1,016. Поскольку наш график никогда не достигает этого значения, он пуст.Примечание: мы построили график только для первых 36 интервалов, поскольку остальные интервалы пусты. Значения интервалов варьируются в диапазоне от 0 до 0,958 ядра CPU (округленно). 37-й интервал имеет значение 1,016. Поскольку наш график никогда не достигает этого значения, он пуст.

Далее VPA подсчитывает три различных оценки: target (цель), lower bound (нижняя граница), upper bound (верхняя граница). Мы используем 90-й процентиль для цели, 50-й процентиль для нижней границы и 95-й процентиль для верхней.

Давайте подсчитаем значения для примера, приведенного на первом рисунке:

Нижняя граница

0,5467

Цель

1,0163

Верхняя граница

1,0163

Примечание: красная линия показывает, где проходит нижняя граница; зеленая линия показывает на местоположение цели и верхней границы. В нашем примере два последних значения оказались одинаковыми.Примечание: красная линия показывает, где проходит нижняя граница; зеленая линия показывает на местоположение цели и верхней границы. В нашем примере два последних значения оказались одинаковыми.

После подсчета к начальным границам прибавляется некоторых резерв, чтобы оставить контейнеру пространство для маневра, если тот, например, внезапно решит съесть больше ресурсов, чем раньше. VPA добавляет некоторую долю от рассчитанной рекомендации. По умолчанию она равна 15%. Скорректировать ее можно с помощью флага --recommendation-margin-fraction.

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

оценка = оценка * (1 + 1/продолжительность сбора данных в днях)

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

5 минут

289

1 час

25,4

1 день

2

2 дня

1,5

1 неделя

1,14

1 неделя и 1 день

1,125

В нашем примере статистика велась в течение двух дней, поэтому доверительный множитель для верхней границы равен 1,5.

Аналогичным образом нижнюю границу мы умножаем на доверительный интервал. Однако в этот раз формула немного другая:

оценка = оценка * (1 + 0.001/продолжительность сбора данных в днях)^-2

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

5 минут

0,6

1 час

0,9537

1 день

0,9980

2 дня

0,0990

Как видно, он стремительно приближается к 1. В нашем примере статистика велась в течение двух дней. Поэтому доверительный множитель для нижней границы почти равен единице.

Далее VPA проверяет, превысили ли оценки некоторое минимальное пороговое значение. Если нет, VPA установит их на минимум. В настоящее время минимум для CPU равен 25 миллиядрам, но его можно изменить с помощью флага --pod-recommendation-min-cpu-millicores.

После добавления резерва к нашим оценкам и учета доверительных множителей конечные значения выглядят следующим образом:

Нижняя граница

0,626

Цель

1,168

Верхняя граница

1,752

Окончательные оценкиОкончательные оценки

Наконец, VPA масштабирует границы таким образом, чтобы вписаться в диапазон minAllowed/maxAllowed, заданный в объекте VerticalPodAutoscaler. Кроме того, если pod находится в пространстве имен с настроенным LimitRange, рекомендация корректируется в соответствии с его правилами.

Модель рекомендаций VPA для памяти

Хотя большинство шагов одинаковы, существуют и значительные отклонения от алгоритма для CPU. Начнем с потребления памяти. Оно выглядит следующим образом:

Обратите внимание, что на графике показано использование памяти за семь дней. Более длительный интервал в данном случае имеет принципиальное значение, поскольку оценка требуемой памяти начинается с вычисления пикового значения для каждого интервала. Используется пиковое значение, а не все распределение, поскольку обычно стараются выделить объем памяти, близкий к пиковому потреблению: ведь ее недостаток приведет к прекращению задач по OOM. В то же время алгоритм выделения CPU-мощностей не так чувствителен к данной проблеме, поскольку при недостатке ресурсов pod'ы сталкиваются с троттлингом, а не убиваются.

По умолчанию интервал агрегации равен 24 часам. Его можно изменить с помощью флага --memory-aggregation-interval. Кроме того, мы сохраняем только восемь интервалов (этот параметр можно изменить с помощью --memory-aggregation-interval-count). Таким образом, у нас имеется информация о пиковом спросе на память за 8 * 24 часа = 8 суток.

Давайте посмотрим, как эти пиковые агрегации выглядят в нашем примере:

Агрегация пиковых нагрузок на памятьАгрегация пиковых нагрузок на память

Кроме того, если в течение этого времени возникает событие Out Of Memory, мы анализируем использование памяти данным pod'ом, берем максимальное значение и прибавляем к нему 20% или 100 Мб (в зависимости от того, что больше). Этот метод позволяет VPA быстро адаптироваться к OOM-инцидентам.

После того, как пиковые значения установлены, их можно свести в гистограмму. VPA создает гистограмму с экспоненциально растущими границами интервалов. Первый интервал начинается с 10 Мб и заканчивается примерно на 1 Тб. Каждый интервал растет экспоненциально со скоростью 5%.

Как и в случае CPU, вес данных уменьшается со временем (по умолчанию его период полураспада равен 24 часам). Если добавить новые данные в гистограмму, которым 24 часа, их вес будет равен 0,5. Подобный распад позволяет увеличить значимость более поздних выборок (то есть они оказывают большее влияние на предсказания, нежели ранние данные). Период полураспада можно скорректировать с помощью флага --memory-histogram-decay-half-life.

Давайте посмотрим, как выглядит гистограмма для пиковых значений из нашего примера:

Примечание: мы построили график только для интервалов с 16 по 38, поскольку остальные интервалы пусты. Значения интервалов варьируются от 225,62 Мб до 969,20 Мб (округленно). Значение 39-го интервала составляет 1088,10. Он пуст, так как наш график никогда не достигает этого значения.Примечание: мы построили график только для интервалов с 16 по 38, поскольку остальные интервалы пусты. Значения интервалов варьируются от 225,62 Мб до 969,20 Мб (округленно). Значение 39-го интервала составляет 1088,10. Он пуст, так как наш график никогда не достигает этого значения.

Далее VPA подсчитывает три различных оценки: target (цель), lower bound (нижняя граница), upper bound (верхняя граница). Мы использует 90-й процентиль для цели, 50-й процентиль для нижней границы и 95-й процентиль для верхней.

В нашем примере все три оценки одинаковы: 1027,2 Мб.

Оценки после вычисления 50-го, 90-го и 95-го процентиляОценки после вычисления 50-го, 90-го и 95-го процентиля

После подсчета к начальным границам добавляется некоторых резерв, чтобы оставить контейнеру пространство для маневра. Если, например, он внезапно решит съесть больше ресурсов, чем раньше. VPA добавляет некоторую долю от рассчитанной рекомендации. По умолчанию она равна 15%. Скорректировать ее можно с помощью флага --recommendation-margin-fraction.

Затем к обеим границам добавляется доверительный множитель. Доверительный множитель зависит от того, сколько дней собирались данные. Формулы те же, что и при выведении оценок для CPU.

Далее VPA проверяет, превысили ли оценки некоторое минимальное пороговое значение. Если нет, VPA установит их на минимум. В настоящее время минимальный объем памяти составляет 250 Мб. Его можно изменить с помощью флага --pod-recommendation-min-memory-mb.

После добавления резерва и учета доверительных множителей конечные значения выглядят следующим образом:

Нижняя граница

1237422043 байт = 1,15 Гб

Цель

1238659775 байт = 1,15 Гб

Верхняя граница

1857989662 байт = 1,73 Гб

Обратите внимание, что зеленая линия это верхняя граница, красная нижняя. Цель не видна, так как она близка к красной линии (разница между ними составляет всего 1,18 Мб)

Наконец, VPA масштабирует границы таким образом, чтобы вписаться в диапазон minAllowed/maxAllowed, заданный в объекте VerticalPodAutoscaler. Кроме того, если pod находится в пространстве имен с настроенным LimitRange, рекомендация корректируется в соответствии с его правилами.

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

P.S. от переводчика

Читайте также в нашем блоге:

Подробнее..

Лучшие практики для деплоя высокодоступных приложений в Kubernetes. Часть 1

03.03.2021 16:23:30 | Автор: admin

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

Функциональность, которая не доступна в Kubernetes из коробки, здесь почти не будет затрагиваться. Также мы не будем привязываться к конкретным CD-решениям и опустим вопросы шаблонизации/генерации Kubernetes-манифестов. Рассмотрены только общие правила, касающиеся того, как Kubernetes-манифесты могут выглядеть в конечном итоге при деплое в кластер.

1. Количество реплик

Вряд ли получится говорить о какой-либо доступности, если приложение не работает по меньшей мере в двух репликах. Почему при запуске приложения в одной реплике возникают проблемы? Многие сущности в Kubernetes (Node, Pod, ReplicaSet и др.) эфемерны, т. е. при определенных условиях они могут быть автоматически удалены/пересозданы. Соответственно, кластер Kubernetes и запущенные в нём приложения должны быть к этому готовы.

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

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

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

2. Стратегия обновления

Стратегия обновления у Deployment'а по умолчанию такая, что почти до конца обновления только 75% Pod'ов старого+нового ReplicaSet'а будут в состоянии Ready. Таким образом, при обновлении приложения его вычислительная способность может падать до 75%, что может приводить к частичному отказу. Отвечает за это поведение параметр strategy.rollingUpdate.maxUnavailable. Поэтому убедитесь, что приложение не теряет в работоспособности при отказе 25% Pod'ов, либо увеличьте maxUnavailable. Округление maxUnavailable происходит вверх.

Также у стратегии обновления по умолчанию (RollingUpdate) есть нюанс: приложение некоторое время будет работать не только в несколько реплик, но и в двух разных версиях разворачивающейся сейчас и развернутой до этого. Поэтому, если приложение не может даже непродолжительное время работать в нескольких репликах и нескольких разных версиях, то используйте strategy.type: Recreate. При Recreate новые реплики будут подниматься только после того, как удалятся старые. Очевидно, здесь у приложения будет небольшой простой.

Альтернативные стратегии деплоя (blue-green, canary и др.) часто могут быть гораздо лучшей альтернативой RollingUpdate, но здесь мы не будем их рассматривать, так как их реализация зависит от того, какое ПО вы используете для деплоя. Это выходит за рамки текущей статьи. (См. также статью Стратегии деплоя в Kubernetes: rolling, recreate, blue/green, canary, dark (A/B-тестирование) в нашем блоге.)

3. Равномерное распределение реплик по узлам

Очень важно разносить Pod'ы приложения по разным узлам, если приложение работает в нескольких репликах. Для этого рекомендуйте планировщику не запускать несколько Pod'ов одного Deployment'а на одном и том же узле:

      affinity:        podAntiAffinity:          preferredDuringSchedulingIgnoredDuringExecution:          - podAffinityTerm:              labelSelector:                matchLabels:                  app: testapp              topologyKey: kubernetes.io/hostname

Предпочитайте preferredDuringScheduling вместо requiredDuringScheduling, который может привести к невозможности запустить новые Pod'ы, если доступных узлов окажется меньше, чем новым Pod'ам требуется. Тем не менее, requiredDuringScheduling может быть полезен, когда количество узлов и реплик приложения точно известно и необходимо быть уверенным, что два Pod'а не смогут оказаться на одном и том же узле.

4. Приоритет

priorityClassName влияет на то, какие Pod'ы будут schedule'иться в первую очередь, а также на то, какие Pod'ы могут быть вытеснены (evicted) планировщиком, если места для новых Pod'ов на узлах не осталось.

Потребуется создать несколько ресурсов типа PriorityClass и ассоциировать их с Pod'ами через priorityClassName. Набор PriorityClass'ов может выглядеть примерно так:

  • Cluster. Priority > 10000. Критичные для функционирования кластера компоненты, такие как kube-apiserver.

  • Daemonsets. Priority: 10000. Обычно мы хотим, чтобы Pod'ы DaemonSet'ов не вытеснялись с узлов обычными приложениями.

  • Production-high. Priority: 9000. Stateful-приложения.

  • Production-medium. Priority: 8000. Stateless-приложения.

  • Production-low. Priority: 7000. Менее критичные приложения.

  • Default. Priority: 0. Приложения для окружений не категории production.

Это предохранит нас от внезапных evict'ов важных компонентов и позволит более важным приложениям вытеснять менее важные при недостатке узлов.

5. Остановка процессов в контейнерах

При остановке контейнера всем процессам в нём отправляется сигнал, указанный в STOPSIGNAL (обычно это TERM). Но не все приложения умеют правильно реагировать на него и делать graceful shutdown, который бы корректно отработал и для приложения, запущенного в Kubernetes.

Например, чтобы сделать корректную остановку nginx, нам понадобится preStop-хук вроде этого:

lifecycle:  preStop:    exec:      command:      - /bin/sh      - -ec      - |        sleep 3        nginx -s quit
  1. sleep 3 здесь для страховки от race conditions, связанных с удалением endpoint.

  2. nginx -s quit инициирует корректное завершение работы для nginx. Хотя в свежих образах nginx эта строка больше не понадобится, т. к. там STOPSIGNAL: SIGQUIT установлен по умолчанию.

(Более подробно про graceful shutdown для nginx в связке с PHP-FPM вы можете узнать из другой нашей статьи.)

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

Ещё один важный параметр, связанный с остановкой приложения, terminationGracePeriodSeconds. Он отвечает за то, сколько времени будет у приложения на корректное завершение. Если приложение не успеет завершиться в течение этого времени (30 секунд по умолчанию), то приложению будет послан сигнал KILL. Таким образом, если вы ожидаете, что выполнение preStop-хука и/или завершение работы приложения при получении STOPSIGNAL могут занять более 30 секунд, то terminationGracePeriodSeconds нужно будет увеличить. Например, такое может потребоваться, если некоторые запросы у клиентов веб-сервиса долго выполняются (вроде запросов на скачивание больших файлов).

Стоит заметить, что preStop-хук выполняется блокирующе, т. е. STOPSIGNAL будет послан только после того, как preStop-хук отработает. Тем не менее, отсчет terminationGracePeriodSeconds идёт и в течение работы preStop-хука. А процессы, запущенные в хуке, равно как и все процессы в контейнере, получат сигнал KILL после того, как terminationGracePeriodSeconds закончится.

Также у некоторых приложений встречаются специальные настройки, регулирующие время, в течение которого приложение должно завершить свою работу (к примеру, опция --timeout у Sidekiq). Оттого для каждого приложения надо убеждаться, что если у него есть подобная настройка, то она выставлена в значение немного меньшее, чем terminationGracePeriodSeconds.

6. Резервирование ресурсов

Планировщик на основании resources.requests Pod'а принимает решение о том, на каком узле этот Pod запустить. К примеру, Pod не будет schedule'иться на узел, на котором свободных (т. е. non-requested) ресурсов недостаточно, чтобы удовлетворить запросам (requests) нового Pod'а. А resources.limits позволяют ограничить потребление ресурсов Pod'ами, которые начинают расходовать ощутимо больше, чем ими было запрошено через requests. Лучше устанавливать лимиты равные запросам, так как если указать лимиты сильно выше, чем запросы, то это может лишить другие Pod'ы узла выделенных для них ресурсов. Это может приводить к выводу из строя других приложений на узле или даже самого узла. Также схема ресурсов Pod'а присваивает ему определенный QoS class: например, он влияет на порядок, в котором Pod'ы будут вытесняться (evicted) с узлов.

Поэтому необходимо выставлять и запросы, и лимиты и для CPU, и для памяти. Единственное, что можно/нужно опустить, так это CPU-лимит, если версия ядра Linux ниже 5.4 (для EL7/CentOS7 версия ядра должна быть ниже 3.10.0-1062.8.1.el7).

(Подробнее о том, что такое requests и limits, какие бывают QoS-классы в Kubernetes, мы рассказывали в этой статье.)

Также некоторые приложения имеют свойство бесконтрольно расти в потреблении оперативной памяти: к примеру, Redis, использующийся для кэширования, или же приложение, которое течёт просто само по себе. Чтобы ограничить их влияние на остальные приложения на том же узле, им можно и нужно устанавливать лимит на количество потребляемой памяти. Проблема только в том, что, при достижении этого лимита приложение будет получать сигнал KILL. Приложения не могут ловить/обрабатывать этот сигнал и, вероятно, не смогут корректно завершаться. Поэтому очень желательно использовать специфичные для приложения механизмы контроля за потреблением памяти в дополнение к лимитам Kubernetes, и не доводить эффективное потребление памяти приложением до limits.memory Pod'а.

Конфигурация для Redis, которая поможет с этим:

maxmemory 500mb   # если данные начнут занимать 500 Мб...maxmemory-policy allkeys-lru   # ...Redis удалит редко используемые ключи

А для Sidekiq это может быть Sidekiq worker killer:

require 'sidekiq/worker_killer'Sidekiq.configure_server do |config|  config.server_middleware do |chain|    # Корректно завершить Sidekiq при достижении им потребления в 500 Мб    chain.add Sidekiq::WorkerKiller, max_rss: 500  endend

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

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

7. Пробы

В Kubernetes пробы (healthcheck'и) используются для того, чтобы определить, можно ли переключить на приложение трафик (readiness) и не нужно ли приложение перезапустить (liveness). Они играют большую роль при обновлении Deployment'ов и при запуске новых Pod'ов в целом.

Сразу общая рекомендация для всех проб: выставляйте высокий timeoutSeconds. Значение по умолчанию в одну секунду слишком низкое. Особенно критично для readinessProbe и livenessProbe. Слишком низкий timeoutSeconds будет приводить к тому, что при увеличении времени ответов у приложений в Pod'ах (что обычно происходит для всех Pod'ов сразу благодаря балансированию нагрузки с помощью Service) либо перестанет приходить трафик почти во все Pod'ы (readiness), либо, что ещё хуже, начнутся каскадные перезапуски контейнеров (liveness).

7.1 Liveness probe

На практике вам не так часто нужна liveness probe (дословно: проверка на жизнеспособность), насколько вы думаете. Её предназначение перезапустить контейнер с приложением, когда livenessProbe перестаёт отрабатывать, например, если приложение намертво зависло. На практике подобные deadlockи скорее исключение, чем правило. Если же приложение работает, но не полностью (например, приложение не может само восстановить соединение с БД, если оно оборвалось), то это нужно исправлять в самом приложении, а не накручивать костыли с livenessProbe.

И хотя как временное решение можно добавить в livenessProbe проверку на подобные состояния, по умолчанию livenessProbe лучше вообще не использовать. Как альтернативу её полному отсутствию можно рассмотреть простейшую livenessProbe вроде проверки на то, открыт ли TCP-порт (обязательно выставьте большой таймаут). В таком случае это поможет приложению перезапуститься при возникновении очевидного deadlock'а, но при этом приложение не подвергнется риску войти в цикл перезапусков, когда перезапуск не может помочь.

И риски, которые плохая livenessProbe несёт, весьма серьезные. Самые частые случаи: когда livenessProbe перестаёт отрабатывать по таймауту из-за повышенной нагрузки на приложение, а также когда livenessProbe перестаёт работать, т. к. проверяет (прямо или косвенно) состояние внешних зависимостей, которые сейчас отказали. В последнем случае последует перезагрузка всех контейнеров, которая при лучшем раскладе ни к чему не приведет, а при худшем приведет к полной (и, возможно, длительной) недоступности приложения. Полная длительная недоступность приложения может происходить, если при большом количестве реплик контейнеры большинства Pod'ов начнут перезагружаться в течение короткого промежутка времени. При этом какие-то контейнеры, скорее всего, поднимутся быстрее других, и на это ограниченное количество контейнеров теперь придется вся нагрузка, которая приведет к таймаутам у livenessProbe и заставит контейнеры снова перезапускаться.

Также, если все-таки используете livenessProbe, убедитесь, что она не перестает отвечать, если у вашего приложения есть лимит на количество установленных соединений и этот лимит достигнут. Чтобы этого избежать, обычно требуется зарезервировать под livenessProbe отдельный тред/процесс самого приложения. Например, запускайте приложение с 11 тредами, каждый из которых может обрабатывать одного клиента, но не пускайте извне в приложение более 10 клиентов, таким образом гарантируя для livenessProbe отдельный незанятый тред.

И, конечно, не стоит добавлять в livenessProbe проверки внешних зависимостей.

(Подробнее о проблемах с liveness probe и рекомендациях по предотвращению таких проблем рассказывалось в этой статье.)

7.2 Readiness probe

Дизайн readinessProbe (дословно: проверка на готовность [к обслуживанию запросов]), пожалуй, оказался не очень удачным. Она сочетает в себе две функции: проверять, что приложение в контейнере стало доступным при запуске контейнера, и проверять, что приложение остаётся доступным уже после его запуска. На практике первое нужно практически всегда, а второе примерно настолько же часто, насколько оказывается нужной livenessProbe. Проблемы с плохими readinessProbe примерно те же самые, что и с плохими livenessProbe, и в худшем случае также могут приводить к длительной недоступности приложения.

Когда readinessProbe перестаёт отрабатывать, то на Pod перестаёт приходить трафик. В большинстве случаев такое поведение мало помогает, т. к. трафик обычно балансируется между Pod'ами более-менее равномерно. Таким образом, чаще всего readinessProbe либо работает везде, либо не работает сразу на большом количестве Pod'ов. Есть ситуации, когда подобное поведение readinessProbe может понадобиться, но в моей практике это скорее исключение.

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

Получается странная ситуация, что одна функция readinessProbe обычно очень нужна, а другая очень не нужна. Эта проблема была решена введением startupProbe, которая появилась в Kubernetes 1.16 и перешла в Beta в 1.18. Таким образом, рекомендую для проверки готовности приложения при его запуске в Kubernetes < 1.18 использовать readinessProbe, а в Kubernetes >= 1.18 использовать startupProbe. readinessProbe всё ещё можно использовать в Kubernetes >= 1.18, если у вас есть необходимость останавливать трафик на отдельные Pod'ы уже после старта приложения.

7.3 Startup probe

startupProbe (дословно: проверка на запуск) реализует первоначальную проверку готовности приложения в контейнере для того, чтобы пометить текущий Pod как готовый к приёму трафика, или же для того, чтобы продолжить обновление/перезапуск Deployment'а. В отличие от readinessProbe, startupProbe прекращает работать после запуска контейнера. Проверять внешние зависимости в startupProbe не лучшая идея, потому что если startupProbe не отработает, то контейнер будет перезапущен, что может приводить к переходу Pod'а в состояние CrashLoopBackOff. При этом состоянии между попытками перезапустить неподнимающийся контейнер будет делаться задержка до пяти минут. Это может означать простой в том случае, когда приложение уже может подняться, но контейнер всё ещё выжидает CrashLoopBackOff перед тем, как снова попробовать запуститься.

Обязательна к использованию, если ваше приложение принимает трафик и у вас Kubernetes >= 1.18.

Также предпочитайте увеличение failureTreshold вместо использования initialDelaySeconds. Это позволит контейнеру становиться доступным настолько быстро, насколько это возможно.

8. Проверка внешних зависимостей

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

С помощью initContainers можно проверять внешние зависимости до того, как начнут запускаться startupProbe/readinessProbe основных контейнеров. В readinessProbe, соответственно, проверки внешних зависимостей уже не понадобится. Подобные initContainers не требуют изменений в коде приложения, не требуют собирать контейнеры приложения с дополнительными утилитами для проверок внешних зависимостей, а также в целом довольно просты в реализации:

      initContainers:      - name: wait-postgres        image: postgres:12.1-alpine        command:        - sh        - -ec        - |          until (pg_isready -h example.org -p 5432 -U postgres); do            sleep 1          done        resources:          requests:            cpu: 50m            memory: 50Mi          limits:            cpu: 50m            memory: 50Mi      - name: wait-redis        image: redis:6.0.10-alpine3.13        command:        - sh        - -ec        - |          until (redis-cli -u redis://redis:6379/0 ping); do            sleep 1          done        resources:          requests:            cpu: 50m            memory: 50Mi          limits:            cpu: 50m            memory: 50Mi

Полный пример

Резюмируя, привёдем полный пример того, как уже с учётом всех вышеописанных рекомендаций может выглядеть Deployment stateless-приложения при его боевом развертывании.

Требования: Kubernetes >= 1.18, на узлах Ubuntu/Debian с версией ядра >= 5.4.

apiVersion: apps/v1kind: Deploymentmetadata:  name: testappspec:  replicas: 10  selector:    matchLabels:      app: testapp  template:    metadata:      labels:        app: testapp    spec:      affinity:        podAntiAffinity:          preferredDuringSchedulingIgnoredDuringExecution:          - podAffinityTerm:              labelSelector:                matchLabels:                  app: testapp              topologyKey: kubernetes.io/hostname      priorityClassName: production-medium      terminationGracePeriodSeconds: 40      initContainers:      - name: wait-postgres        image: postgres:12.1-alpine        command:        - sh        - -ec        - |          until (pg_isready -h example.org -p 5432 -U postgres); do            sleep 1          done        resources:          requests:            cpu: 50m            memory: 50Mi          limits:            cpu: 50m            memory: 50Mi      containers:      - name: backend        image: my-app-image:1.11.1        command:        - run        - app        - --trigger-graceful-shutdown-if-memory-usage-is-higher-than        - 450Mi        - --timeout-seconds-for-graceful-shutdown        - 35s        startupProbe:          httpGet:            path: /simple-startup-check-no-external-dependencies            port: 80          timeoutSeconds: 7          failureThreshold: 12        lifecycle:          preStop:            exec:              ["sh", "-ec", "#command to shutdown gracefully if needed"]        resources:          requests:            cpu: 200m            memory: 500Mi          limits:            cpu: 200m            memory: 500Mi

В следующий раз

Осталось ещё немало важных вещей, о которых обязательно надо рассказать, таких как PodDisruptionBudget, HorizontalPodAutoscaler и VerticalPodAutoscaler, чем мы непременно займемся во второй части этой статьи. А пока предлагаем вам поделиться своими лучшими практиками по деплою, либо исправить/дополнить уже описанные.

P.S.

Читайте также в нашем блоге:

Подробнее..

Эксплуатация MongoDB в Kubernetes решения, их плюсы и минусы

26.03.2021 10:08:08 | Автор: admin

MongoDB одна из самых популярных NoSQL/документоориентированных баз данных в мире веб-разработки, поэтому многие наши клиенты используют её в своих продуктах, в том числе и в production. Значительная их часть функционирует в Kubernetes, так что хотелось бы поделиться накопленным опытом: какие варианты для запуска Mongo в K8s существуют? В чем их особенности? Как мы сами подошли к этому вопросу?

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

Главные вызовы

В частности, при размещении Mongo в кластере важно учитывать:

  1. Хранилище. Для гибкой работы в Kubernetes для Mongo лучше всего подойдут удаленные хранилища, которые можно переключать между узлами, если понадобится переместить Mongo при обновлении узлов кластера или их удалении. Однако удаленные диски обычно предоставляются с более низким показателем iops (в сравнении с локальными). Если база является высоконагруженной и требуются хорошие показания по latency, то на это стоит обратить внимание в первую очередь.

  2. Правильные requests и limits на podах с репликами Mongo (и соседствующих с ними podами на узле). Если не настроить их правильно, то поскольку Kubernetes более приветлив к stateless-приложениям можно получить нежелательное поведение, когда при внезапно возросшей нагрузке на узле Kubernetes начнет убивать podы с репликами Mongo и переносить их на соседние, менее загруженные. Это вдвойне неприятно по той причине, что перед тем, как pod с Mongo поднимется на другом узле, может пройти значительное время. Всё становится совсем плохо, если упавшая реплика была primary, т.к. это приведет к перевыборам: вся запись встанет, а приложение должно быть к этому готово и/или будет простаивать.

  3. В дополнение к предыдущему пункту: даже если случился пик нагрузки, в Kubernetes есть возможность быстро отмасштабировать узлы и перенести Mongo на узлы с большими ресурсами. Потому не стоит забывать про podDisruptionBudget, что не позволит удалять или переносить podы разом, старательно поддерживая указанное количество реплик в рабочем состоянии.

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

К счастью, на данный момент практически любой провайдер может предоставить любой тип хранилища на ваш выбор: от сетевых дисков до локальных с внушительным запасом по iops. Для динамического расширения кластера MongoDB подойдут только сетевые диски, но мы должны учитывать, что они всё же проигрывают в производительности локальным. Пример из Google Cloud:

А также они могут зависеть от дополнительных факторов:

В AWS картина чуть лучше, но всё ещё далека от производительности, что мы видим для локального варианта:

В нашем практике ещё ни разу не понадобилось добиваться такой производительности для MongoDB: в большинстве случаев хватает того, что предоставляют провайдеры.

Каким образом можно поднять MongoDB в Kubernetes?

Очевидно, что в любой ситуации (и Mongo здесь не будет исключением) можно обойтись самописными решениями, подготовив несколько манифестов со StatefulSet и init-скриптом. Но далее мы рассмотрим уже существующие подходы, которые давно придумали за нас.

1. Helm-чарт от Bitnami

И первое, что привлекает внимание, это Helm-чарт от Bitnami. Он довольно популярен, создан и поддерживается значительно долгое время.

Чарт позволяет запускать MongoDB несколькими способами:

  1. standalone;

  2. Replica Set (здесь и далее по умолчанию подразумевается терминология MongoDB; если речь пойдет про ReplicaSet в Kubernetes, на это будет явное указание);

  3. Replica Set + Arbiter.

Используется свой (т.е. неофициальный) образ.

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

Минимальная конфигурация, которая понадобится для поднятия, это:

1. Указать архитектуру (Values.yaml#L58-L60). По умолчанию это standalone, но нас интересует replicaset:

...architecture: replicaset...

2. Указать тип и размер хранилища (Values.yaml#L442-L463):

...persistence:  enabled: true  storageClass: "gp2" # у нас это general purpose 2 из AWS  accessModes:    - ReadWriteOnce  size: 120Gi...

После этого через helm install мы получаем готовый кластер MongoDB с инструкцией, как к нему подключиться из Kubernetes:

NAME: mongobitnamiLAST DEPLOYED: Fri Feb 26 09:00:04 2021NAMESPACE: mongodbSTATUS: deployedREVISION: 1TEST SUITE: NoneNOTES:** Please be patient while the chart is being deployed **MongoDB(R) can be accessed on the following DNS name(s) and ports from within your cluster:    mongobitnami-mongodb-0.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017    mongobitnami-mongodb-1.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017    mongobitnami-mongodb-2.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017To get the root password run:    export MONGODB_ROOT_PASSWORD=$(kubectl get secret --namespace mongodb mongobitnami-mongodb -o jsonpath="{.data.mongodb-root-password}" | base64 --decode)To connect to your database, create a MongoDB(R) client container:    kubectl run --namespace mongodb mongobitnami-mongodb-client --rm --tty -i --restart='Never' --env="MONGODB_ROOT_PASSWORD=$MONGODB_ROOT_PASSWORD" --image docker.io/bitnami/mongodb:4.4.4-debian-10-r0 --command -- bashThen, run the following command:    mongo admin --host "mongobitnami-mongodb-0.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017,mongobitnami-mongodb-1.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017,mongobitnami-mongodb-2.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017" --authenticationDatabase admin -u root -p $MONGODB_ROOT_PASSWORD

В пространстве имен увидим готовый кластер с арбитром (он enabled в чарте по умолчанию):

Но такая минимальная конфигурация не отвечает главным вызовам, перечисленным в начале статьи. Поэтому советую включить в неё следующее:

1. Установить PDB (по умолчанию он выключен). Мы не хотим терять кластер в случае drainа узлов можем позволить себе недоступность максимум 1 узла (Values.yaml#L430-L437):

...pdb:  create: true  maxUnavailable: 1...

2. Установить requests и limits (Values.yaml#L350-L360):

...resources:  limits:    memory: 8Gi  requests:     cpu: 4    memory: 4Gi...

В дополнение к этому можно повысить приоритет у podов с базой относительно других podов (Values.yaml#L326).

3. По умолчанию чарт создает нежесткое anti-affinity для podов кластера. Это означает, что scheduler будет стараться не назначать podы на одни и те же узлы, но если выбора не будет, то начнет размещать туда, где есть место.

Если у нас достаточно узлов и ресурсов, стоит сделать так, чтобы ни в коем случае не выносить две реплики кластера на один и тот же узел (Values.yaml#L270):

...podAntiAffinityPreset: hard...

Сам же запуск кластера в чарте происходит по следующему алгоритму:

  1. Запускаем StatefulSet с нужным числом реплик и двумя init-контейнерами: volume-permissions и auto-discovery.

  2. Volume-permissions создает директорию для данных и выставляет права на неё.

  3. Auto-discovery ждёт, пока появятся все сервисы, и пишет их адреса в shared_file, который является точкой передачи конфигурации между init-контейнером и основным контейнером.

  4. Запускается основной контейнер с подменой command, определяются переменные для entrypointа и run.sh.

  5. Запускается entrypoint.sh, который вызывает каскад из вложенных друг в друга Bash-скриптов с вызовом описанных в них функций.

  6. В конечном итоге инициализируется MongoDB через такую функцию:

      mongodb_initialize() {        local persisted=false        info "Initializing MongoDB..."        rm -f "$MONGODB_PID_FILE"        mongodb_copy_mounted_config        mongodb_set_net_conf        mongodb_set_log_conf        mongodb_set_storage_conf        if is_dir_empty "$MONGODB_DATA_DIR/db"; then                info "Deploying MongoDB from scratch..."                ensure_dir_exists "$MONGODB_DATA_DIR/db"                am_i_root && chown -R "$MONGODB_DAEMON_USER" "$MONGODB_DATA_DIR/db"                mongodb_start_bg                mongodb_create_users                if [[ -n "$MONGODB_REPLICA_SET_MODE" ]]; then                if [[ -n "$MONGODB_REPLICA_SET_KEY" ]]; then                        mongodb_create_keyfile "$MONGODB_REPLICA_SET_KEY"                        mongodb_set_keyfile_conf                fi                mongodb_set_replicasetmode_conf                mongodb_set_listen_all_conf                mongodb_configure_replica_set                fi                mongodb_stop        else                persisted=true                mongodb_set_auth_conf                info "Deploying MongoDB with persisted data..."                if [[ -n "$MONGODB_REPLICA_SET_MODE" ]]; then                if [[ -n "$MONGODB_REPLICA_SET_KEY" ]]; then                        mongodb_create_keyfile "$MONGODB_REPLICA_SET_KEY"                        mongodb_set_keyfile_conf                fi                if [[ "$MONGODB_REPLICA_SET_MODE" = "dynamic" ]]; then                        mongodb_ensure_dynamic_mode_consistency                fi                mongodb_set_replicasetmode_conf                fi        fi        mongodb_set_auth_conf        }

2. Устаревший чарт

Если поискать чуть глубже, можно обнаружить еще и старый чарт в главном репозитории Helm. Ныне он deprecated (в связи с выходом Helm 3 подробности см. здесь), но продолжает поддерживаться и использоваться различными организациями независимо друг от друга в своих репозиториях например, здесь им занимается норвежский университет UiB.

Этот чарт не умеет запускать Replica Set + Arbiter и использует маленький сторонний образ в init-контейнерах, но в остальном достаточно прост и отлично выполняет задачу деплоя небольшого кластера.

Мы стали использовать его в своих проектах задолго до того, как он стал deprecated (а это произошло не так давно 10 сентября 2020 года). За минувшее время чарт сильно изменился, однако в то же время сохранил основную логику работы. Для своих задач мы сильно урезали чарт, сделав его максимально лаконичным и убрав всё лишнее: шаблонизацию и функции, которые неактуальны для наших задач.

Минимальная конфигурация сильно схожа с предыдущим чартом, поэтому подробно останавливаться на ней не буду только отмечу, что affinity придется задавать вручную (Values.yaml#L108):

      affinity:        podAntiAffinity:          requiredDuringSchedulingIgnoredDuringExecution:          - labelSelector:              matchLabels:               app: mongodb-replicaset

Алгоритм его работы схож с чартом от Bitnami, но менее нагружен (нет такого нагромождения маленьких скриптов с функциями):

1. Init-контейнер copyconfig копирует конфиг из configdb-readonly (ConfigMap) и ключ из секрета в директорию для конфигов (emptyDir, который будет смонтирован в основной контейнер).

2. Секретный образ unguiculus/mongodb-install копирует исполнительный файл peer-finder в work-dir.

3. Init-контейнер bootstrap запускает peer-finder с параметром /init/on-start.sh этот скрипт занимается поиском поднятых узлов кластера MongoDB и добавлением их в конфигурационный файл Mongo.

4. Скрипт /init/on-start.sh отрабатывает в зависимости от конфигурации, передаваемой ему через переменные окружения (аутентификация, добавление дополнительных пользователей, генерация SSL-сертификатов), плюс может исполнять дополнительные кастомные скрипты, которые мы хотим запускать перед стартом базы.

5. Список пиров получают как:

          args:            - -on-start=/init/on-start.sh            - "-service=mongodb"log "Reading standard input..."while read -ra line; do    if [[ "${line}" == *"${my_hostname}"* ]]; then        service_name="$line"    fi    peers=("${peers[@]}" "$line")done

6. Выполняется проверка по списку пиров: кто из них primary, а кто master.

  • Если не primary, то пир добавляется к primary в кластер.

  • Если это самый первый пир, он инициализирует себя и объявляется мастером.

7. Конфигурируются пользователи с правами администратора.

8. Запускается сам процесс MongoDB.

3. Официальный оператор

В 2020 году вышел в свет официальный Kubernetes-оператор community-версии MongoDB. Он позволяет легко разворачивать, обновлять и масштабировать кластер MongoDB. Кроме того, оператор гораздо проще чартов в первичной настройке.

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

Архитектура оператора:

В отличие от обычной установки через Helm в данном случае понадобится установить сам оператор и CRD (CustomResourceDefinition), что будет использоваться для создания объектов в Kubernetes.

Установка кластера оператором выглядит следующим образом:

  1. Оператор создает StatefulSet, содержащий podы с контейнерами MongoDB. Каждый из них член ReplicaSetа в Kubernetes.

  2. Создается и обновляется конфиг для sidecar-контейнера агента, который будет конфигурировать MongoDB в каждом podе. Конфиг хранится в Kubernetes-секрете.

  3. Создается pod с одним init-контейнером и двумя основными.

    1. Init-контейнер копирует бинарный файл хука, проверяющего версию MongoDB, в общий empty-dir volume (для его передачи в основной контейнер).

    2. Контейнер для агента MongoDB выполняет управление основным контейнером с базой: конфигурация, остановка, рестарт и внесение изменений в конфигурацию.

  4. Далее контейнер с агентом на основе конфигурации, указанной в Custom Resource для кластера, генерирует конфиг для самой MongoDB.

Вся установка кластера укладывается в:

---apiVersion: mongodb.com/v1kind: MongoDBCommunitymetadata:  name: example-mongodbspec:  members: 3  type: ReplicaSet  version: "4.2.6"  security:    authentication:      modes: ["SCRAM"]  users:    - name: my-user      db: admin      passwordSecretRef: # ссылка на секрет ниже для генерации пароля юзера        name: my-user-password      roles:        - name: clusterAdmin          db: admin        - name: userAdminAnyDatabase          db: admin      scramCredentialsSecretName: my-scram# учетная запись пользователя генерируется из этого секрета# после того, как она будет создана, секрет больше не потребуется---apiVersion: v1kind: Secretmetadata:  name: my-user-passwordtype: OpaquestringData:  password: 58LObjiMpxcjP1sMDW

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

Но в то же время он уступает предыдущим вариантам тем, что у него нет встроенной возможности отдачи метрик в Prometheus, а вариант запуска только один Replica Set (нельзя создать арбитра). Кроме того, данный способ развертывания не получится сильно кастомизировать, т.к. практически все параметры регулируются через кастомную сущность для поднятия кластера, а сама она ограничена.

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

Как уже упоминалось, существует и enterprise-версия оператора, которая предоставляет большие возможности в том числе, установку не только Replica Setов, но и shared-кластеров с настройками шардирования, конфигурации для доступа извне кластера (с указанием имен, по которым он будет доступен извне), дополнительные способы аутентификации т.д. И, конечно же, документация к нему описана гораздо лучше.

Заключение

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

У разных вариантов запуска MongoDB есть разные плюсы. Чарты легко модифицировать под ваши нужды, но вы столкнетесь с проблемами при обновлении MongoDB или при добавлении узлов, т.к. всё равно потребуются ручные операции с кластером. Способ с оператором в этом смысле лучше, но ограничен по другим параметрам (по крайней мере, в своей community-редакции). Также мы не нашли ни в одном из описанных вариантов возможность из коробки запускать скрытые реплики.

Наконец, не стоит забывать, что есть и managed-решения для Mongo, однако мы в своей практике стараемся не привязываться к определенным провайдерам и предпочитаем варианты для чистого Kubernetes. Мы также не рассматривали Percona Kubernetes Operator for PSMDB, потому что он ориентирован на вариацию MongoDB от одноимённой компании (Percona Server for MongoDB).

P.S.

Читайте также в нашем блоге:

Подробнее..

Ещё три утилиты, упрощающие работу с kubectl fubectl, Kubelive, Web Kubectl

23.04.2021 10:10:48 | Автор: admin

Какая утилита чаще всего встречается в .bash_history SRE/DevOps-инженера, работающего с Kubernetes? Конечно, kubectl. Это привело к тому, что в сообществе нашлось вдохновение для тех, захотел её улучшить, принести новый опыт использования или даже создать некие производные, нацеленные на более удобное взаимодействие. В рамках этого обзора рассмотрены три таких проекта, которые, возможно, покажутся интересными и вам или хотя бы откроют какие-то новые подходы в решении типовых задач.

1. fubectl

Для начала посмотрим на fubectl, который сам автор называет fancy-kubectl. Этот проект сходу не назовёшь самостоятельной утилитой, т.к. на первый взгляд это лишь подготовленный набор алиасов для kubectl. Как тут не вспомнить похожий kubectl-aliases, про который мы писали 3,5 года назад, когда fubectl еще даже не существовало?.. Возможно, именно в простых вещах кроется сермяжная правда. Итак, автор обещает, что fubectl сделает вашу работу с кластером K8s более эффективной давайте попробуем её в действии.

Установка и запуск. Для полноценной работы утилиты потребуется установить пакет fzf, а также для выполнения отдельных команд (kexp, ktree, neat) понадобятся плагины к kubectl (они ставятся через kubectl krew, для этого есть свой alias об этом чуть ниже).

Основной процесс установки прост и заключается в скачивании одного файла:

curl -LO https://rawgit.com/kubermatic/fubectl/master/fubectl.source

и его добавлению в .bashrc или .zshrc:

[ -f <path-to>/fubectl.source ] && source <path-to>/fubectl.source

Кроме того, для ZSH есть альтернативный путь установки с помощью предпочтительного менеджера плагинов.

Применение. Главный и основной алиас это банальный k, заменяющий команду kubectl. Соответственно, все команды, которыми мы привыкли пользоваться, сокращаются до:

  • k get nodes

  • k get deploy -A

  • k -n mynamespace get pods

Это удобно и практически не требует привыкания. Помимо этого, присутствует набор дополнительных алиасов на команды, которые могут часто использоваться в работе. Например:

  • kw алиас для watch:

    • kw nodes

    • kw pods

    • kw nodes,pods,services

  • kdes describe ресурсов;

  • kdel удаление ресурсов;

  • klog вывод логов контейнера podа.

Обратите внимание, что документация в README проекта не совсем актуальна. В fubectl можно найти и специальные команды, определенные как shell-функции. Среди них, например:

  • ksec для декодирования значения из секрета,

  • kex для выполнения команды в контейнере,

  • konsole для создания и запуска контейнера с root shell.

(Для работы первых двух потребуется дополнительно поставить плагины к kubectl, что делается также отдельной командой kinstall).

Как работает kex в fubectlКак работает kex в fubectl

Полный список поддерживаемых команд можно увидеть, вызвав khelp:

Последний приятный нюанс: благодаря уже упомянутой fzf в утилиту повсеместно встроен механизм fuzzy-поиска, который упрощает ввод команд.

Опыт использования: Из простого source-файла мы получаем весьма функциональное и удобное решение, к которому быстро привыкаешь и открываешь для себя новые возможности.

2. Kubelive

Утилита создана с целью упрощения вывода kubectl get pods -w с наглядным отображением статуса podа в режиме реального времени. Под капотом этого детища используется Node.js-библиотека @kubernetes/client-node.

Установка и запуск. Инсталляция:

npm install -g kubelive

Для работы потребуется Node.js не ниже версии v10.

Если kubectl уже настроен на хосте, то больше ничего не требуется.По аналогии с kubectl будет использоваться стандартный .kube/config (или путь, указанный в переменной окружения KUBECONFIG).

Применение. При запуске kubelive мы получаем интерактивную консоль с возможностью переключения между пространствами имен с помощью стрелок на клавиатуре. Также доступны с помощью быстрых клавиш некоторые базовые операции: рестарт podа, копирование его имени, выход из приложения/интерактивного режима:

[D]: Delete [C]: Copy [Q]: Quit

Для получения общего представления об интерфейсе достаточно посмотреть на демонстрацию из README проекта:

Список поддерживаемых команд на текущий момент так ограничен, что можно полностью привести его в статье:

  • kubelive get pods (или просто kubelive) список podов в кластере;

  • kubelive get services список сервисов;

  • kubelive get replicationcontrollers список ReplicationController;

  • kubelive get nodes список узлов;

  • kubelive get <resource> --context <name> список ресурсов в разных контекстах.

Опыт использования. Мы имеем простой по функциям инструмент, по сути реализующий единственную вещь (отображение статуса), вокруг которой уже строятся всевозможные дополнения (возможность перезагрузки podов и т.п.). Однако при работе с kubelive я столкнулся с рядом проблем. В частности, при большом количестве namespaceов их имена в шапке схлопываются:

Если количество namespaceов превышает 30, шапка просто перестает отображать наименования (разработчик об этом знает), а переход между ними добавляет заметную (секунд на 3-5) задержку.

Кроме того, количество podов также имеет значение, т.к. на экране мы увидим только те, которые влезли, это добавляет неудобств Также на данный момент нет возможности фильтрации/указания нужного пространства имен, хотя и существуют несколько issue на эту тему. Ещё не хватает возможности отображения других сущностей Kubernetes (Deployment, StatefulSet и т.п.) и их манифестов.

Ко всему прочему, не стоит забывать, что для своей работы утилита требует установки Node.js со всеми ее зависимостями (а их не так мало). Используя kubelive на больших кластерах, я заметил, как растёт потребление CPU этой утилитой при беглом переключении по вкладкам namespaceов.

Подводя итог: этот инструмент, существующий с августа 2019 года, показался недостаточно зрелым, но сами реализованные в нем идеи (удобное представление информации о Kubernetes-кластере в консоли), пожалуй, еще найдут свой отклик. Наконец, хотя заявленный проектом Roadmap и может внушить некоторый оптимизм в смысле функциональных перспектив его единственный разработчик утратил реальную активность с прошлого года, что говорит не в пользу будущего Kubelive.

3. Web Kubectl

Это решение иного толка: оно позволяет работать с kubectl прямо из браузера и посему не требует его установки на десктопе или серверах. Для этого в Web Kubectl используют собственный форк проекта gotty, что позволяет запускать веб-терминал на базе JavaScript.

Установка и запуск. Для запуска приложения предлагается использовать Docker-образ:

docker run --name="webkubectl" -p 8080:8080 -d --privileged kubeoperator/webkubectl

После этого достаточно перейти на localhost:8080 и загрузить kubeconfig-файл или указать service account tokens для доступа к Kubernetes-кластеру:

Применение. Зайдя в одну из сессий, мы получаем возможность использовать терминал с kubectl прямо в браузере:

При этом есть поддержка одновременного использования, т.к. присутствует изоляция сеанса на уровне сессии. У каждого сеанса под капотом свое изолированное пространство (подпроцесс запускается в новом пространстве имен Linux через unshare), которое не доступно для других. Там и создается файл .kube/config. При завершении сеанса предоставленное пространство имен и хранилище удаляются. По умолчанию время сеанса не ограничено по времени, но это можно изменить через gotty.

Помимо штатного kubectl в вашем распоряжении также Helm, k9s (о нем мы уже писали), kubectx и даже уже упомянутый в нашем прошлом обзоре набор алисов.

Отдельно выделю наличие API, с помощью которого можно осуществлять доступ к веб-терминалу. Для этого потребуется сформировать токен с содержимым kubeconfig в base64 (cat ~/.kube/config.yaml | base64 -w0):

$ curl http://<webkubectl-address>:<port>/api/kube-config -X POST -d '{"name":"k8s-cluster-bj1","kubeConfig":"<kubeconfig file content base64 encoded>"}'

Ту же самую операцию можно осуществить через запрос к Kubernetes API server и bearer token:

$ curl http://<webkubectl-address>:<port>/api/kube-token -X POST -d '{"name":"gks-hk-dev","apiServer":"https://k8s-cluster:6443","token":"token-content"}'

Если все сделано правильно, в ответе будет получен токен:

{"success":true,"token":"lgfgbp1wkjxzmmbuypbj","message":""}

Его можно разово использовать для терминала с доступом в кластер:

http://<webkubectl-address>:<port>/terminal/?token=<token fetched from api>

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

Если же вы захотите запускать Web Kubectl не локально, обратите внимание, что по умолчанию трафик между сервером и клиентом не защищен: использовать SSL-сертификат и добавлять базовую авторизацию рекомендуется через все тот же gotty. Таким образом, например, можно настроить доступ к изолированному кластеру в VPC вот схема из документации проекта:

_______________________________________________________________________|   Local Network     |          DMZ           |      VPC/Datacenter  ||                     |                        |                      ||                     |    _______________     |   ----------------   ||   ---------------   |    |             |  /~~~~~>| Kubernetes A |   ||   | Your Laptop |~~~~~~~>| Web Kubectl | /   |   ----------------   ||   ---------------   |    |             | \   |                      ||                     |    ---------------  \  |   ----------------   ||                     |                      \~~~~>| Kubernetes B |   ||                     |                        |   ----------------   |-----------------------------------------------------------------------

Для запуска Web Kubectl также доступны дополнительные переменные среды с говорящими за себя названиями:

  • SESSION_STORAGE_SIZE

  • KUBECTL_INSECURE_SKIP_TLS_VERIFY

  • GOTTY_OPTIONS

  • WELCOME_BANNER

Опыт использования. Инструмент показал себя с хорошей стороны и оставил приятные впечатления по быстродействию. Наличие API дает возможность интегрировать webkubectl в свое приложение, а частота релизов на GitHub показывает интерес авторов к своему проекту. Во многом этот интерес объясняется тем, что webkubectl часть более крупного проекта, готового для production дистрибутива Kubernetes под названием KubeOperator от той же китайской компании. Из личных пожеланий проекту отмечу, что было бы здорово иметь возможность группировки сессий, а также административную панель для управления доступом.

Заключение

Популярность Kubernetes с каждым днем растет, как и количество приложений и сервисов вокруг этой технологии. Всё это формирует развитую экосистему и даёт нам возможность пользоваться удобными инструментами для решения практически любых задач. Этот обзор был посвящен трем проектам, объединенным одной темой kubectl, и разным не только по своему назначению, но и тем впечатлениям, что оставили при первом знакомстве с ними. Надеюсь, какие-то из них могут оказаться полезными и для вас.

P.S.

Читайте также в нашем блоге:

Подробнее..

Werf vs. Helm корректно ли их вообще сравнивать?

29.04.2021 10:23:13 | Автор: admin

Эта статья развернутый ответ на вопрос, который нам периодически задают: чем werf отличается от Helm? На первый взгляд можно предположить, что задача у них примерно одинаковая: автоматизировать деплой приложений в Kubernetes. Но всё, конечно, немного сложнее

Роль в CI/CD

Если упрощенно показать утилиты в рамках полного цикла CI/CD, то их функции значительно отличаются:

Helm

werf

Сборка приложения (в Docker-образ)

Публикация образов в container registry и их автоматическая очистка со временем

Деплой в Kubernetes

Деплой в Kubernetes (на базе Helm), расширенный трекингом ресурсов, интеграцией с образами, встроенной поддержкой Giterminism и другими фичами

Как видно, в рамках CI/CD-пайплайна werf делает гораздо больше, участвуя в полном жизненном цикле приложения, от сборки до выката в Kubernetes.

Helm хороший инструмент, но довольно низкоуровневый. Для реального использования в CI/CD он требует надстроек, интеграции с другими инструментами в общем, заметного усложнения инфраструктуры и процессов.

Поэтому мы говорим, что werf это следующий уровень доставки приложений в Kubernetes. Утилита использует Helm как один из компонентов и интегрирует его с другими стандартными инструментами: Git, Docker и Kubernetes. Благодаря этому werf выступает в роли клея, который упрощает, унифицирует организацию CI/CD-пайплайнов на базе специализированных инструментов, уже ставших стандартом в индустрии, и выбранной вами CI-системы.

Что умеет werf (и не умеет Helm)

Мы упомянули, что werf это утилита, которая не только про деплой. Но даже если посмотреть только на данный этап, то и здесь у werf есть ряд доработок:

Возможности

Helm

werf

Ожидание готовности ресурсов во время деплоя

+

+

Трекинг ресурсов и обнаружение ошибок

+

Fail-fast во время деплоя

+

Защита от параллельных запусков деплоя одного и того же релиза

+

Интеграция с собираемыми образами

+

Поддержка автодобавления аннотаций и лейблов во все ресурсы релиза

+ /

+

Публикация файлов конфигурации и образов приложения в container registry (бандлы)

+ /

+

Базовая поддержка секретных values

+

Поддержка Giterminism и GitOps

+ /

+

Подробнее мы разберем каждый пункт таблицы ниже (см. Детальное сравнение werf и Helm ниже). Но сначала об истоках, которые привели к таким следствиям. Расскажем о том, к чему мы стремились, создавая werf.

Четыре благородные истины werf

1. werf должна использоваться в CI/CD-пайплайне как единый инструмент

В утилите необходима поддержка работы в любой существующей CI/CD-системе и легкой интеграции. Сейчас werf работает из коробки с GitLab CI/CD и GitHub Actions. Другие CI-системы тоже поддерживаются для интеграции достаточно написать скрипт, следуя инструкции.

2. werf должна оптимальным образом доставлять приложения в Kubernetes

В процессе доставки собираются только недостающие образы из container registry или недостающие слои для этих образов, а всё старое берется из прошлых сборок или кэша. Так экономится и время сборки, и место для хранения всех образов.

После доставки [обновлённого приложения] текущее состояние ресурсов в Kubernetes приводится к новому требуемому состоянию, которое определено в Git (мы назвали это словом гитерминизм, от слов Git + детерминизм) и здесь снова речь идёт о том, что для такой синхронизации вычисляются изменения и применяются только они.

3. werf должна давать четкую обратную связь

Итоговый отчет werf в идеале должен быть достаточным для диагностики проблемы. Если что-то пошло не так в процессе доставки и развертывания, пользователю не нужно запускать kubectl и искать информацию в кластере. werf сразу показывает и постоянно обновляет актуальный статус процесса деплоя и даёт достаточно информации для решения проблемы без привлечения системного администратора.

4. werf должна поддерживать GitOps-подход

GitOps в общем виде это подход, при котором для развертывания приложений в Kubernetes используется единственный источник правды Git-репозиторий. Через декларативные действия в Git вы управляете реальным состоянием инфраструктуры, запущенной в Kubernetes. Этот паттерн из коробки работает в werf.

У нас есть собственный взгляд на то, как должен быть реализован GitOps для CI/CD (уже упомянутый Giterminism). GitOps в werf поддерживает не только хранение Kubernetes-манифестов в Git, но и версионирование собираемых образов, связанное с Git. Откат до предыдущей версии приложения выполняется без сборки выкатывавшихся ранее образов (при условии, что версия учитывается политиками очистки). Другие существующие реализации GitOps не дают этой гарантии.

Зачем и как werf использует Helm

werf использует и расширяет Helm, чтобы следовать вышеприведенным принципам.

Helm популярный и проверенный инструмент. У него есть собственный шаблонизатор, чарты и т.п. Мы не стали переизобретать то, что уже отлично работает. Вместо этого сфокусировались на фичах, которые помогают оптимизировать CI/CD.

С точки зрения совместимости важно, что кодовая база Helm вкомпилирована в werf. Обновления из upstream приходят регулярно, руками Helm обновлять не нужно. (Как, впрочем, и у самой werf, у которой есть встроенный version manager multiwerf с 5 каналами стабильности и поддержкой автообновления.)

Мы даже стараемся по возможности участвовать в улучшении Helm через upstream. Один из примеров нашего вклада аннотация helm.sh/hook-delete-policy=before-hook-creation (Удалить предыдущий ресурс перед запуском нового хука), которая пришла в Helm из werf.

Плюсы и минусы Helm

У Helm есть ряд преимуществ:

  • Это стандартный package manager в K8s. Helm используют [практически] все, кто работает с Kubernetes; он стал стандартом индустрии.

  • Шаблонизация. Шаблоны Helm удобны для тиражирования манифестов при работе с несколькими окружениями. (Пусть и не все согласятся, что Go-templates удобный шаблонизатор, но это уже другой вопрос...)

  • Удобное управление чартами. Общепринятый формат описания ресурсов и объединения их в чарты (charts это пакеты для Helm).

  • Переиспользование чартов. Helm поддерживает публикацию переиспользуемых чартов в Chart Repository или container registry.

  • Удобное управление жизненным циклом релизов. В Helm легко управлять релизами и откатываться на нужные версии.

Что до минусов, то основной в том, что Helm это маленькое звено в CI/CD-цепи, которое мало или совсем не связано с другими важными звеньями.

CI/CD приложения предполагает непрерывное слияние изменений в основную кодовую базу проекта и непрерывную доставку этих изменений до пользователя. Это включает периодическую сборку новых образов приложения с обновлениями, тестирование этих образов и выкат новой версии приложения. В CI/CD у нас обычно несколько окружений (production, staging, development и т. д.). И конфигурация приложения может отличаться для разных окружений.

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

Детальное сравнение werf иHelm

А теперь вернемся к таблице и разберем подробнее каждый её пункт.

1. Трекинг ресурсов и обнаружение ошибок

И werf, и Helm умеют ждать готовности ресурсов в процессе развертывания. Но werf дает обратную связь и более проактивна. Три главных отличия:

1. В процессе деплоя werf выводит логи по выкатываемым ресурсам. Для отдельных ресурсов логирование отключается автоматически по мере их перехода в состояние готовности. Можно отключить трекинг конкретных контейнеров или логировать вывод только по определенным контейнерам ресурса, отключив остальные; можно включать и выключать вывод сервисной информации Kubernetes. Всё это настраивается с помощью аннотаций, которые объявляются в шаблонах чарта.

2. Как только werf замечает проблему в одном из ресурсов, он завершается с ошибкой и ненулевым кодом выхода. werf следует принципу fail-fast. Он выдает наиболее полезную информацию по ошибке, чтобы пользователь сразу мог понять, в чём проблема, по выводу в CI/CD job.

Helm, в отличие от werf, в случае проблем с конфигурацией ждет истечения таймаута. А такие проблемы распространенная ситуация в CI/CD. Выяснить, в чём их причина, можно только после подключения к кластеру через kubectl. Это неудобно:

  • нужно настраивать права доступа к kubectl;

  • kubectl требует знаний и умений: куда смотреть, что искать и как это интерпретировать.

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

3. Защита от параллельных запусков деплоя одного и того же релиза. Helm может упасть с ошибкой, если будут работать одновременно два выката одного и того же релиза. werf автоматически предотвращает одновременный выкат: эти процессы работают по очереди.

2. Интеграция со сборкой образов

В Helm приходится явно передавать полные имена образов через values и заботиться об актуализации имен при изменениях.

werf решает эту проблему за пользователя: не нужно думать о способах передачи имен образов, т. к. они передаются через те же values автоматически. Более того, werf оптимальным образом решает проблему именования образов так, чтобы в процессе деплоя менялись имена только у изменившихся образов.

К тому же, werf дает возможность откатиться на старую версию приложения со старыми образами без повторной пересборки этих образов. Часто при использовании Helm в CI/CD через values для упрощения передаются статические имена образов (вроде registry.example.com/myproject:production) в этом случае имя образа ссылается только на последнюю собранную версию образа. При такой схеме тегирования приходиться пересобирать старый образ, чтобы откатиться до предыдущей версии. werf использует схему с content-based-тегами, которые связаны с историей Git. Помимо прочего, такая схема тегирования полностью решает вопрос с откатом на старую версию.

Что дает интеграция с собранными образами:

  • werf может использовать уже существующие Dockerfile'ы в своей конфигурации.

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

  • Нет лишних перевыкатов компонентов приложения в Kubernetes и, как следствие, лишнего простоя компонентов. Перевыкат происходит быстро и только для измененных компонентов, а не приложения целиком.

  • Со стороны конфигурации Helm остается только использовать имена образов, которые werf предоставляет через специальные values.

  • Можно откатиться на образы старой версии приложения.

3. Добавление аннотаций и лейблов в ресурсы релиза

В Helm есть механизм post rendering, чтобы дописывать в ресурсы аннотации или лейблы и менять другие поля. Однако в Helm нет простой встроенной функции для добавления конкретно лейблов и аннотаций.

werf добавляет во все ресурсы релиза автоматические аннотации вроде ссылки на CI/CD job, из которого ресурс был в последний раз выкачен, или ссылки на Git-коммит.

Также в werf через CLI-опции можно указать произвольные аннотации или лейблы, которые будут добавлены во все ресурсы релиза. Пример такой команды:

werf converge --add-annotation pipeline-id=$CI_PIPELINE_ID --add-annotation git-branch=$CI_COMMIT_REF_NAME

Это удобно:

  • для интроспекции, когда надо понять, с каким CI/CD job связана версия ресурса;

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

4. Бандлы: публикация файлов конфигурации и образов приложения в container registry

Helm поддерживает публикацию чартов в OCI container registry либо Chart Repository, однако не отвечает за образы, которые нужны этому чарту для выката. Такие образы должны быть отдельно собраны и правильно протегированы, а опубликованный чарт должен быть настроен на использование образов по правильным именам. Эти проблемы пользователю Helm приходиться решать самостоятельно.

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

Пользователь не думает об именах образов, публикуемых вместе с чартом: werf делает это автоматически и оптимальным способом. Неизменные образы будут переиспользованы. Публикуются лишь те слои, которые требуются для текущего коммита. Пользователю werf достаточно выбрать версию бандла и обеспечить его публикацию в требуемом Git-коммите.

5. Встроенная базовая поддержка значений секретов

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

werf из коробки дает возможность закодировать значения values через алгоритмы AES-128, AES-192 и AES-256. Ключи шифрования можно менять.

6. Поддержка Giterminism и GitOps

Helm не регулирует привязку используемых конфигурационных файлов к Git-коммитам.

werf (с версии v1.2) форсирует использование конфигурации из текущего Git-коммита и реализует режим гитерминизма, в том числе и для конфигурации Helm.

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

Подытожим

Прямое противопоставление werf vs Helm не совсем корректно, потому что утилита werf реализует не только деплой, а нацелена на поддержку полного жизненного цикла доставки приложений. Сам по себе Helm хороший инструмент для деплоя в Kubernetes, поэтому он (с некоторыми улучшениями) встроен в werf для решения этой задачи. Однако и в контексте деплоя у werf есть ряд преимуществ, общий смысл которых сводится к более полной и удобной интеграции с CI/CD-системами.

P.S.

Читайте также в нашем блоге:

Подробнее..

Практические истории из наших SRE-будней. Часть 3

25.12.2020 10:05:30 | Автор: admin
Рады продолжить цикл статей с подборками из недавних вызовов, случившихся в нашей повседневной практике эксплуатации. Для этого мы описываем свои мысли и действия, которые привели к их успешному преодолению.



Новый выпуск посвящён опыту с неожиданно затянувшейся миграцией одного Linux-сервера, знакомству с Kubernetes-оператором для ClickHouse, способу ускорить восстановление данных в сломавшейся реплике PostgreSQL и последствиями обновления CockroachDB. Если вы тоже думаете, что это может быть полезно или хотя бы просто интересно, добро пожаловать под кат!

История 1. Затянувшийся перенос сервера в виртуальную машину


План миграции


Казалось, что может пойти не так, если требуется перенести legacy-приложение с железного сервера в виртуальную машину? У приложения и его инфраструктуры привычный, хорошо понятный стек: Linux, PHP, Apache, Gearman, MySQL. Причины для миграции тоже обычны: клиент захотел уменьшить плату за хостинг, отказавшись от реального сервера, на котором остался только вспомогательный сервис (парсер соцсетей).

Вообще говоря, конечно, бывают и другие причины для миграции (например, многочисленные удобства в последующем обслуживании инфраструктуры и её масштабирования), но не буду заострять на этом внимание.

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

Общий план выглядит следующим образом:

  1. Произвести очистку сервера, поняв, сколько ресурсов требуется.
  2. Подготовить виртуальный сервер, выделить память, ядра, зарезервировать IP-адреса.
  3. Если требуется минимальный простой организовать внешний балансировщик, который можно переключить на свежесозданный виртуальный сервер, или же запустить копию приложения.
  4. Произвести начальную загрузку с образа выбранной ОС/дистрибутива, содержащего все необходимые драйверы, чтобы скопировать данные в виртуальную машину тем или иным способом.
  5. Создать chroot, чтобы исправить загрузчик системы.
  6. Переключить пользовательские запросы или сервисные задачи на новую систему.

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

Подготовка к миграции


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

Отдельно хочется рассказать про похудение MySQL. Дело в том, что MySQL изначально была версии 5.5 и настроена без innodb_file_per_table. Из-за этого, как многие могут догадаться, файл ibdata1 разросся до 40 Гб. В таких ситуациях нам всегда помогает pt-online-schema-change (входит в состав Percona Toolkit).

Достаточно проверить таблицы, которые находятся в shared innodb tablespace:

SELECT i.name FROM information_schema.INNODB_SYS_TABLES i WHERE i.space = 0;

после чего запустить упомянутую команду pt-online-schema-change, которая позволяет совершать различные действия над таблицами без простоя и поможет нам совершить OPTIMIZE без простоя для всех найденных таблиц:

pt-online-schema-change --alter "ENGINE=InnoDB" D=mydb,t=test --execute

Если файл ibdata1 не слишком велик, то его можно оставить. Чтобы полностью избавиться от мусора в файле ibdata1, потребуется сделать mysqldump со всех баз, оставив только базы mysql и performance_schema. Теперь можно остановить MySQL и удалить ibdata1.

После перезапуска MySQL создаст недостающие файлы системного namespace InnoDB. Загружаем данные в MySQL и готово.

Подготовка дисков и копирование


Казалось бы, теперь можно произвести перенос данных с помощью dd, однако в данном случае это не представлялось возможным. На сервере был созданный с md RAID 1, который не хотелось бы видеть на виртуальной машине, так как её разделы создаются в Volume Group, которая создана на RAID 10. Кроме того, разделы были очень большие, хотя занято было не более 15% места. Поэтому было принято решение переносить виртуальную машину, используя rsync. Такая операция нас не пугает: мы часто мигрировали серверы подобным образом, хотя это и несколько сложнее, чем перенос всех разделов с использованием dd.

Что потребуется сделать? Тут нет особой тайны, так как некоторые шаги полностью соответствуют действиям при копировании диска с dd:

  1. Создаем виртуальную машину нужного размера и загружаемся с systemrescuecd.
  2. Делаем разбивку диска, аналогичную серверу. Обычно нужен root-раздел и boot с этим поможет parted. Допустим, у нас есть диск /dev/vda:

    parted /dev/vdamklabel gptmkpart P1 ext3 1MiB 4MiB t 1 bios_grubmkpart P2 ext3 4MiB 1024MiBmkpart P3 ext3 1024MiB 100%t 3 lvm 
    
  3. Создадим на разделах файловые системы. Обычно мы используем ext3 для boot и ext4 для root.
  4. Монтируем разделы в /mnt, в который будем chroot'иться:

    mount /dev/vda2 /mntmkdir -p /mnt/bootmount /dev/vda1 /mnt/boot
    
  5. Подключим сеть. Актуальные версии systemrescuecd построены на ArchLinux и предполагают настройку системы через nmcli:

    nmcli con add con-name lan1 ifname em1 type ethernet ip4 192.168.100.100/24 gw4 192.168.100.1 ipv4.dns "8.8.8.8 8.8.4.4"nmcli con up lan1
    
  6. Копируем данные: rsync -avz --delete --progress --exclude "dev/*" --exclude "proc/*" --exclude "sys/*" rsync://old_ip/root/ /mnt/
  7. Затем монтируем dev, proc, sys:

    mount -t proc proc /mnt/procmount -t sysfs sys /mnt/sysmount --bind /dev /mnt/dev
    
  8. Зайдем в полученный chroot: chroot /mnt bash
  9. Поправим fstab, изменив адреса точек монтирование на актуальные.
  10. Теперь надо восстановить загрузчик:
    1. Восстановим загрузочный сектор: grub-install /dev/vda
    2. Обновим конфиг grub: update-grub
  11. Обновим initramfs: update-initramfs -k all -u
  12. Перезагрузим виртуалку и загрузим перенесенную систему.

Используя этот алгоритм, мы перенесли сотни виртуальных машин и серверов, однако в этот раз что-то пошло не так

Проблема и её решение


Система упорно помнила различные дисковые подразделы, которые были до переноса на сервере. Проблем разобраться с mdadm не было достаточно просто удалить файл /etc/mdadm/mdadm.conf и запустить update-initramfs.



Однако система все равно пыталась найти еще и /dev/mapped/vg0-swap. Оказалось, что initrd пытается подключить swap из-за конфига, который добавляет Debian installer. Удаляем лишний файл, собираем initramfs, перезагружаемся и снова попадаем в консоль busybox.

Поинтересуемся у системы, видит ли она наши диски. lsblk выдает пустоту, да и поиск файлов устройств в /dev/disk/by-uuid/ не даёт результатов. Выяснилось, что ядро Debian Jessie 3.16 скомпилировано без поддержки virtio-устройств (точнее, сама поддержка, конечно, доступна, но для этого нужно загрузить соответствующие модули).

К счастью, модули добавляются в initrd без проблем: нужные модули можно либо прописать в /etc/initramfs-tools/modules, либо изменить политику добавления модулей в /etc/initramfs-tools/initramfs.conf на MODULES=most.



Однако магии и в этот раз не произошло. Даже несмотря на наличие модулей система не видела диски:



Пришлось в настройках виртуальной машины переключить диски с шины Virtio на SCSI такое действие позволило загрузить виртуальную машину.

В загруженной системе отсутствовала сеть. Попытки подключить сетевые драйверы (модуль virtio_net) ни к чему не привели.



Дабы не усложнять задачу и не затягивать переключение, было решено переключить и сетевые адаптеры на эмуляцию реального железа сетевой карты Intel e1000e. Виртуальная машина была остановлена, драйвер изменён, однако при запуске мы получили ошибку: failed to find romfile "efi-e1000.rom".



Поиск дал интересный результат: ROM-файл был потерян в Debian некоторое время назад и возвращать его в пакет коллеги не собирались. Однако этот же файл фигурирует в пакете ipxe-qemu, откуда и был с успехом взят. Оказалось, достаточно распаковать этот пакет (ipxe-qemu) и скопировать /usr/lib/ipxe/qemu/efi-e1000.rom в /usr/share/qemu/efi-e1000e.rom. После этого виртуальная машина с эмулированным адаптером начала стартовать.

Вы думаете, это всё? Конечно же, нет, когда в деле замешан e1000e Данный драйвер известен тем, что может под нагрузкой начать перезапускать сетевой адаптер. Именно это и произошло, когда мы стали загружать базу данных для приложения. Пришлось прибегнуть к старому способу с отключение аппаратного offload:

ethtool -K eth0 gso off gro off tso off

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

История 2. Безопасность для Kubernetes-оператора ClickHouse


Не так давно мы начали использовать ClickHouse operator от Altinity. Данный оператор позволяет гибко разворачивать кластеры ClickHouse в Kubernetes:

  • с репликацией для повышенной надёжности;
  • с шардами для горизонтального масштабирования.

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

[2020-11-25 15:00:20] Code: 516, e.displayText() = DB::Exception: Received from chi-cluster-cluster-0-0:9000. DB::Exception: default: Authentication failed: password is incorrect or there is no user with such name.

К счастью, ClickHouse позволяет сделать whitelist с использованием rDNS, IP, host regexp Так можнодобавить в конфиг кластера следующее:

      users:        default/networks/host_regexp: (chi-cluster-[^.]+\d+-\d+|clickhouse\-cluster)\.clickhouse\.svc\.cluster\.local$

Тогда кластер сможет нормально функционировать. В репозитории оператора есть issue по этому поводу (мы не забыли добавить туда и свой workaround). Однако не похоже, что там будут какие-то движения в ближайшее время из-за того, что потребуется хранить пароли в конфигурации remote_servers.

История 3. Ускоренная перезаливка реплик PostgreSQL


К сожалению, ничто не вечно и любая техника стареет. А это приводит к различным сбоям. Один из таких сбоев произошел на реплике баз данных PostgreSQL: отказал один из дисков и массив перешёл в режим read only.

После замены диска и восстановления работы сервера встал вопрос: как же быстро ввести его в строй, учитывая, что база у проекта довольно объемна (более 2 терабайт)?

Дело осложнялось тем, что репликация была заведена без слотов репликации, а за время, пока сервер приводили в чувство, все необходимые WAL-сегменты были удалены. Архивацией WAL в проекте никто не озаботился и момент для её включения был упущен. К слову, сами слоты репликации представляют угрозу в версиях PostgreSQL ниже 13, т.к. могут занять всё место на диске (а неопытный инженер о них даже не вспомнит). С 13-й версии PgSQL размер слота уже можно ограничить директивой max_slot_wal_keep_size.

Итак, казалось бы, надо вооружаться pg_basebackup и переливать базу с нуля, но по нашим подсчетам такая операция заняла бы 9 дней, и всё это время основной сервер БД работал бы без резерва. Что же делать? У нас же есть почти актуальные файлы, некоторые из которых база вообще не трогает, так как это старые партиции партицированных таблиц Но pg_basebackup требует чистой директории для начала копирования. Вот бы изобрести метод, который бы позволил докачать базу!..

И тут я вспомнил про исходный метод, которым мы снимали бэкапы еще во времена PostgreSQL 9.1. Он описывается в статье документации про Continuous Archiving and Point-in-Time Recovery. Суть его крайне проста и основана на том, что можно копировать файлы PgSQL, если вызвать команду pg_start_backup, а после процедуры копирования pg_stop_backup. В голове созрел следующий план:

  1. Создадим слот репликации для реплики командой на мастере:

    SELECT pg_create_physical_replication_slot('replica', true);
    

    Важно, чтобы при создании второй аргумент функции был именно true тогда база начнёт немедленно собирать сегменты WAL в этот слот, а не будет ждать первого подключения к нему.
  2. Выполним команду на мастере:

    SELECT pg_start_backup('copy', true);
    

    Снова важно, чтобы при создании второй аргумент функции был именно true тогда база немедленно выполнит checkpoint и можно будет начать копирование.
  3. Скопируем базу на реплику. Мы для этой цели использовали rsync:

    rsynс -avz --delete --progress rsync://leader_ip/root/var/lib/postgresql/10/main/ /var/lib/postgresql/10/main/
    

    С такими параметрами запуска rsync заменит изменившиеся файлы.
  4. По окончании копирования на мастере выполним:

    SELECT pg_stop_backup();
    
  5. На реплике положим такой recovery.conf с указанием нашего слота:

    standby_mode = 'on'primary_conninfo = 'user=rep host=master_ip port=5432 sslmode=prefer sslcompression=1 krbsrvname=postgres target_session_attrs=any'recovery_target_timeline = 'latest'primary_slot_name = replica
    
  6. Запустим реплику.
  7. Удалим слот репликации на реплике, так как он так же скопируется с мастера:

    SELECT pg_drop_replication_slot('replica');
    
  8. Проверим, что она появилась в системной таблице pg_stat_replication.

Однако один момент я всё-таки упустил. Мы выполнили копирование всех WAL-файлов, которые были на мастере. А значит даже тех, которые не требовались. Поэтому на следующий день после перелива реплики место на сервере с репликой начало заканчиваться. И пришлось думать над тем, как удалить бесхозные сегменты WAL.

Мы знаем, что checkpoint_timeout равен 1 часу. Следовательно, надо удалить все файлы старше 1 часа, но от какого момента? Для этого на мастере делаем запрос:

SELECT pg_walfile_name(replay_lsn) from pg_stat_replication;     pg_walfile_name      -------------------------- 0000000200022107000000C8(1 row)

Исходя из него сверяем временную метку файла:

stat /var/lib/postgresql/10/main/pg_wal/0000000200022107000000C8...Access: 2020-12-02 13:11:20.409309421 +0300Modify: 2020-12-02 13:11:20.409309421 +0300Change: 2020-12-02 13:11:20.409309421 +0300

у удаляем все файлы старше. С этим помогут find и bash:

# Вычислим смещениеdeleteBefore=`expr $(date --date='2020-12-02 13:11:20' +%s) - 3600`mins2keep=`expr $(expr $(expr $(date +%s) - $deleteBefore) / 60) + 1`# Удалим файлы размером 16 МБ (стандартный размер сегмента WAL),# которые старше, чем mins2keepfind /var/lib/postgresql/10/main/pg_wal/ -size 16M -type f -mmin +$mins2keep -delete

Вот и всё: реплика была перелита за 12 часов (вместо 9 дней), функционирует и очищена от мусора.

История 4. CockroachDB не тормозит?


После обновления CockroachDB до версии 20.2.x мы столкнулись с проблемами производительности. Они выражались в долгом старте приложения и общем снижении производительности некоторых типов запросов. На CockroachDB 20.1.8 подобного поведения не наблюдалось.

Изначально имелось предположение, что дело в сетевых проблемах в кластере Kubernetes. Однако подтвердить его не удалось: cеть чувствовала себя отлично.



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

SET CLUSTER SETTING sql.log.slow_query.latency_threshold = '100ms';SET CLUSTER SETTING sql.log.slow_query.internal_queries.enabled = 'true';

Благодаря этому стало ясно, что используемый в приложении драйвер PostgreSQL JDBC при старте делает запросы к pg_catalog, а наличие базы Keyсloak сильно влияет на скорость работы этих запросов. Мы пробовали загрузить несколько копий базы и с каждый загруженным экземпляром скорость работы pg_catalog падала всё ниже и ниже:

I201130 10:52:27.993894 5920071 sql/exec_log.go:225  [n3,client=10.111.7.3:38470,hostssl,user=db1] 3 112.396ms exec "PostgreSQL JDBC Driver" {} "SELECT typinput = 'array_in'::REGPROC AS is_array, typtype, typname FROM pg_catalog.pg_type LEFT JOIN (SELECT ns.oid AS nspoid, ns.nspname, r.r FROM pg_namespace AS ns JOIN (SELECT s.r, (current_schemas(false))[s.r] AS nspname FROM ROWS FROM (generate_series(1, array_upper(current_schemas(false), 1))) AS s (r)) AS r USING (nspname)) AS sp ON sp.nspoid = typnamespace WHERE typname = $1 ORDER BY sp.r, pg_type.oid DESC" {$1:"'jsonb'"} 1 "" 0 { LATENCY_THRESHOLD }

Вот тот же запрос, но с загруженной проблемной базой:

I201130 10:36:00.786376 5085793 sql/exec_log.go:225  [n2,client=192.168.114.18:21850,hostssl,user=db1] 67 520.064ms exec "PostgreSQL JDBC Driver" {} "SELECT typinput = 'array_in'::REGPROC AS is_array, typtype, typname FROM pg_catalog.pg_type LEFT JOIN (SELECT ns.oid AS nspoid, ns.nspname, r.r FROM pg_namespace AS ns JOIN (SELECT s.r, (current_schemas(false))[s.r] AS nspname FROM ROWS FROM (generate_series(1, array_upper(current_schemas(false), 1))) AS s (r)) AS r USING (nspname)) AS sp ON sp.nspoid = typnamespace WHERE typname = $1 ORDER BY sp.r, pg_type.oid DESC" {$1:"'jsonb'"} 1 "" 0 { LATENCY_THRESHOLD }

Получается, что тормозили системные таблицы CockroachDB.

После того, как клиент подтвердил проблемы с производительностью уже в облачной инсталляции CockroachDB, источник проблемы стал проясняться: было похоже на улучшенную поддержку SQL, что появилась в релизе 20.2. План запросов к схеме pg_catalog заметно отличался от 20.1.8, и мы стали свидетелями регрессии.

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

ОБНОВЛЕНО (уже после написания статьи): Проблемы были исправлены в релизе CockroachDB 20.2.3 в Pull Request 57574.

Заключение


Как видно, иногда даже очевидные и простые операции могут повлечь за собой головную боль. Но выход всё равно можно найти, не так ли?.. Надеюсь, эти истории помогут и другим инженерам в повседневной работе. Stay tuned!

P.S.


Читайте также в нашем блоге:

Подробнее..

Представляем ovpn-admin веб-интерфейс для управления пользователями OpenVPN

17.03.2021 16:04:48 | Автор: admin

Обслуживая большое количество проектов, мы пришли к необходимости простого управления OpenVPN (сертификатами и маршрутами для пользователей), подразумевая под этим веб-интерфейс вместо того, чтобы ходить по серверам/контейнерам и выполнять в них вручную команды. Поскольку существующие для этого решения (вроде Pritunl и OpenVPN AS) коммерческие, несколько лет назад мы создали (и используем по сей день) свой интерфейс.

Недавно мы его переписали с Python на Go и обновили внешний вид*, что и навело на мысль поделиться разработкой с более широким сообществом. Итак, встречайте ovpn-admin!

* За первоначальный вариант на Python благодарю коллегу @vitaliy-sn, а за нескучные обои обновлённый интерфейс erste.

Возможности и интерфейс

Ovpn-admin это Open Source-проект, реализующий веб-интерфейс для управления OpenVPN. В настоящий момент утилита поддерживает только Linux и умеет:

  • добавлять пользователей (генерировать сертификаты для них);

  • отзывать/восстанавливать сертификаты пользователей;

  • выдавать готовый файл конфига;

  • отдавать метрики для Prometheus: срок действия сертификатов, количество пользователей (всего/подключенных), информация о подключенных пользователях;

  • (опционально) прописывать CCD (client-config-dir) для каждого пользователя;

  • (опционально) работать в режиме master/slave (синхронизация сертификатов и CCD с другим сервером);

  • (опционально) задавать/менять пароль для дополнительной авторизации в OpenVPN.

Вот как выглядит интерфейс ovpn-admin:

Список пользователей и возможные действия с нимиСписок пользователей и возможные действия с нимиОкно добавления кастомных маршрутов для пользователяОкно добавления кастомных маршрутов для пользователяПример панели на основе метрик, полученных от ovpn-adminПример панели на основе метрик, полученных от ovpn-admin

Как попробовать

Ovpn-admin можно установить в систему или запускать в Docker-контейнере. Инструкции описаны в README проекта.

Исходный код проекта распространяется на условиях свободной лицензии (Apache License 2.0). Будем рады новым фичам, а также, конечно, ожидаем ваши issues и просто обсуждения на GitHub или в комментариях к этому посту.

Планы по развитию

Что бы хотелось улучшить в проекте? На данный момент у нас такой список доделок:

  • добавить возможность дополнительной авторизации через OTP;

  • добавить Helm-чарт как вариант установки;

  • добавить группы пользователей;

  • уйти от вызова утилиты easyrsa для генерации сертификатов;

  • уйти от вызова bash.

P.S.

Читайте также в нашем блоге:

Подробнее..

Бэкапы для HashiCorp Vault с разными бэкендами

26.05.2021 10:15:20 | Автор: admin

Недавно мы публиковали статью про производительность Vault с разными бэкендами, а сегодня расскажем, как делать бэкапы и снова на разных бэкендах: Consul, GCS (Google Cloud Storage), PostgreSQL и Raft.

Как известно, HashiCorp предоставляет нативный метод бэкапа только для одного бэкенда Integrated Storage (Raft Cluster), представленного как GA в апреле прошлого года. В нем можно снять снапшот всего одним curlом и не беспокоиться о каких-либо нюансах. (Подробности смотрите в tutorial и документации по API.)

В остальных же случаях придется выкручиваться самим, придумывая, как правильно реализовывать бэкап. Очевидно, что во время резервного копирования Vault часть данных может меняться, из-за чего мы рискуем получить некорректные данные. Поэтому важно искать пути для консистентного бэкапа.

1. Consul

В Consul существует встроенный механизм для создания и восстановления снапшотов. Он поддерживает point-in-time backup, поэтому за консистентность данных можно не переживать. Однако есть существенный недостаток: на время снапшота происходит блокировка, из-за чего могут возникнуть проблемы с операциями на запись.

Для создания такого снимка необходимо:

1. Подключиться к инстансу с Consul и выполнить команду:

# consul snapshot save backup.snapSaved and verified snapshot to index 199605#

2. Забрать файл backup.snap (заархивировать и перенести в место для хранения бэкапов).

Для восстановления будет схожий алгоритм действий с выполнением другой команды на сервере с Consul:

# consul snapshot restore backup.snap

В HA Vault-кластере активный узел будет являться лидером во время экспорта данных или моментального снимка. После восстановления данных Vault в Consul необходимо вручную снять эту блокировку, чтобы кластер Vault мог выбрать нового лидера.

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

# consul kv delete vault/core/lock

Работа со снапшотами обычно происходит быстро (см. примеры в конце статьи) и обычно не вызывает трудностей.

2. Google Cloud Storage

С GCS всё оказалось сложнее: подходящих вариантов для создания снапшотов/бэкапов бакета найти не удалось. Предусмотрена поддержка версионирования, но с ним восстановить сразу весь бакет на определенный момент времени нельзя можно только по одному файлу. В теории это решается скриптом для взаимодействия со всеми файлами, но если учесть размер Vaultа и количества файлов в нем, скорее всего такой скрипт будет работать слишком долго. Если мы хотим получить консистентные данные, то снятия дампа придется на время останавливать Vault.

А для копирования данных в GCS предусмотрен удобный способ Data Transfer. С его помощью можно создать полную копию бакета, а дальше уже работать с ним по своему усмотрению.

Последовательность действий:

  1. Выключаем Vault.

  2. Заходим в Data Transfer (https://console.cloud.google.com/transfer/cloud).

  3. Выбираем исходный бакет, вводим название бакета, куда переносить данные, и выбираем разовую операцию. Здесь возможно установить и операцию по расписанию, но она не учитывает специфики Vault (тот факт, что сервис необходимо остановить Vault на время её выполнения), поэтому оставляем разовый запуск.

  4. После успешного копирования запускаем Vault.

  5. С созданным бакетом можно работать: например, скачать его содержимое при помощи gsutil, заархивировать все данные и отправить на долгосрочное хранение.

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

Скриншоты страницы при создании трансфера и после завершения:

3. PostgreSQL

Базу в PostgreSQL достаточно забэкапить любыми доступными для этой СУБД способами не сомневаюсь, что они хорошо известны инженерам, уже работающим с PgSQL. Инструкции по выполнению операций для настройки бэкапов и восстановления данных на нужный момент времени (PITR, Point-in-Time Recovery) описаны в официальной документации проекта. Также хорошую инструкцию можно найти в этой статье от Percona.

Не останавливаясь на технических деталях по этим операциям (они уже раскрыты в многочисленных статьях), отмечу, что у PostgreSQL получился заметно более быстрый бэкап, чем у вариантов выше. Для простоты действий, мы замеряли бэкап и восстановление обычными pg_dump и pg_restore (да, это упрощенное измерение, однако использование схемы с PITR не повлияет значительно на скорость).

4. Integrated Storage (Raft)

Однако обзор не был бы полным без самого удобного и простого на сегодняшний день бэкенда Raft. Благодаря тому, что Raft полностью дублирует все данные на каждый из узлов, выполнять снапшот можно на любом сервере, где запущен Vault. А сами действия для этого предельно просты.

Для создания снимка необходимо:

. Зайти на сервер/pod, где запущен Vault, и выполнить команду:

# vault operator raft snapshot save backup.snapshot
  1. Забрать файл backup.snapshot (заархивировать и перенести в место для хранения бэкапов).

Для восстановления команду на сервере с Vault надо заменить на:

# vault operator raft snapshot restore backup.snapshot

Как работать с Raft и со снапшотами, хорошо описано в официальной документации.

Простой бенчмарк

Не претендуя на полноценное исследование о производительности, сделаем простой замер скорости создания бэкапов и восстановления из них для всех упомянутых бэкендов для Vault.

Загрузка данных в Vault

Перед началом тестирования загрузим данные в Vault. Мы для этого использовали официальный репозиторий от HashiCorp: в нем есть скрипты для вставки ключей в Vault.

Протестируем на двух коллекциях: 100 тысяч и 1 млн ключей. Команды для первого теста:

export VAULT_ADDR=https://vault.service:8200export VAULT_TOKEN=YOUR_ROOT_TOKENvault secrets enable -path secret -version 1 kvnohup wrk -t1 -c16 -d50m -H "X-Vault-Token: $VAULT_TOKEN" -s write-random-secrets.lua $VAULT_ADDR -- 100000

Эту операцию проделаем для всех инсталляций Vault, а затем повторим её для другого количества ключей.

Результаты

После загрузки данных мы сделали бэкап для всех 4 бэкендов. Бэкап для каждого hosted-бэкенда снимался на однотипной машине (4 CPU, 8 GB RAM).

Результаты сравнения по бэкапу и восстановлению:

100k backup

100k restore

1kk backup

1kk restore

Consul

3.31s

2.50s

36.02s

27.58s

PosgreSQL

0.739s

1.820s

4.911s

24.837s

GCS*

1h

1h24m

12h

16h

Raft

1.96s

0.36s

22.66s

4.94s

* Восстановление бэкапа в GCS может происходить в нескольких вариантах:

  1. Мы просто переключаем Vault на бэкапный бакет. В таком случае достаточно перезапустить Vault с новым конфигом и все сразу же заработает.

  2. Загрузить файлы бэкапа в бакет через утилиту gsutil и уже после этого переключать. Результаты для такого варианта будут зависеть от количества файлов и их размера в таблице приведен результат именно для такого варианта.

NB. В этом мини-тестировании сравниваются решения разного рода: single-экземпляр PostgreSQL против кластеров Consul и Raft против сетевого распределённого хранилища (GCS). Это может показаться не совсем корректным, потому что у таких бэкендов и разные возможности/свойства (отказоустойчивость и т.п.). Однако данное сравнение приведено исключительно как примерный ориентир для того, чтобы дать понимание порядковой разницы в производительности. Ведь это зачастую является одним из факторов при выборе того или иного способа.

Выводы

PostgreSQL занимает уверенное первое место по скорости создания бэкапа. Raft не так сильно отстает от лидера на небольшом объеме секретов, но разница заметно возрастает при увеличении количества данных. Однако в то же время Raft явно лидирует в скорости восстановления.

Если сравнивать удобство, то Raft и Consul максимально простые: для выполнения бэкапа и восстановления достаточно выполнить буквально одну команду. GCS же предоставляет встроенный функционал в UI для копирования бакетов: на мой вкус, это немного сложнее, однако для других пользователей может быть плюсом, что все действия выполняются мышкой в браузере. Но в GCS есть существенная проблема с отсутствием гарантий по времени создания снапшотов: одинаковый набор данных может бэкапиться как за 1 час, так и за 3-4 часа.

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

  • В Consul можно ожидать проблем с чтением и записью при бэкапе/восстановлении данных.

  • А у PostgreSQL при восстановлении.

  • У GCS проблема другого характера: нет гарантий на скорость копирования.

Получается, что все эти решения имеют серьёзные недостатки, которые могут быть недопустимы в production. Понимая это, в HashiCorp и создали своё оптимальное решение Integrated Storage (Raft). В нём получается делать бэкапы полностью беспростойно и при этом быстро.

В итоге: если вам важна максимальная скорость для снятия и восстановления бэкапов, то подойдут PostgreSQL и Raft. В первом случае, однако, надо повозиться с удобством: чтобы все правильно настроить и автоматизировать, придется потратить время (и иметь экспертизу). Наш текущий выбор пал на Integrated Storage (Raft) как самый простой в использовании и надежный бэкенд.

P.S.

Читайте также в нашем блоге:

Подробнее..

Аварии как опыт 3. Как мы спасали свой мониторинг во время аварии в OVH

09.06.2021 12:21:23 | Автор: admin

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

О мониторинге у нас

Начать стоит с того, как во Фланте вообще устроен мониторинг клиентских приложений и инфраструктуры в целом. Для этих целей задействованы три разных вида мониторинга:

1. Blackbox-мониторинг для проверки состояния сайтов. Цель проста собирать статистику с определенных endpointов и проверять их состояние по тем условиям, которые требуется установить. Например, это может быть health page в виде JSON-страницы, где отражено состояние всех важных элементов инфраструктуры, или же мониторинг отдельных страниц сайта. У нас принято считать, что данный вид мониторинга самый критичный, так как следит за работоспособностью сайта/сервисов клиента для внешних пользователей.

Вкратце о технической стороне этого мониторинга: он выполняет запросы разного уровня сложности по HTTP/HTTPS. Есть возможность определять размер страницы, ее содержимое, работать с JSON (они обычно используются для построения status page-страниц приложений клиента). Он географически распределен для повышения отказоустойчивости и во избежание всевозможных блокировок (например, от РКН).

Работа данного мониторинга не была затронута аварией благодаря геораспределенности (к тому же, у него вообще нет инсталляций в OVH).

2. Мониторинг Kubernetes-инфраструктуры и приложений клиента, запущенных в ней. С технической точки зрения этот мониторинг основан на связке Prometheus + Grafana + Alertmanager, которые устанавливаются локально на кластеры клиента. Часть данных, собираемых на данных системах мониторинга (например, метрики, напрямую связанные с Kubernetes и Deckhouse), по умолчанию отправляется в нашу общую систему, а остальные могут отправляться опционально (например, для мониторинга приложений и реакции от наших дежурных инженеров).

3. Мониторинг ресурсов, которые находятся вне кластеров Kubernetes. Например, это железные (bare metal) серверы, виртуальные машины и СУБД, запущенные на них. Данную зону мониторинга мы покрываем с помощью стороннего сервиса Okmeter (а с недавних пор уже не очень-то для нас и стороннего). Именно его работа была фатально нарушена в момент аварии OVH.

(А о том, как осуществляется дальнейшая обработка инцидентов, поступающих от всех упомянутых систем, мы рассказывали в этом докладе.)

Таким образом, все проблемы, описанные ниже, связаны с недоступностью одной из используемых систем Okmeter. То есть на некоторое время мы потеряли информацию о некоторой инфраструктуре (вне Kubernetes-кластеров), сохранив при этом доступность blackbox-мониторинга (который гарантирует нам, что главные бизнес-функции работают).

Как мы справлялись с этой аварией? Какие шаги предпринимали во время инцидента? И какие после?

Первые признаки аварии

Проверка доступности внешних средств мониторинга осуществляется с применением метода Dead mans switch (DMS). По своей сути это мониторинг, который работает наоборот:

  • Создается поддельный алерт OK, означающий, что всё хорошо, и он постоянно отправляется с локальных мониторингов (Prometheus, Okmeter и т.п.) в нашу систему инцидент-менеджмента.

  • Пока с мониторингом действительно всё хорошо, алерт OK активен, но в системе не показывается.

  • В обратной ситуации, когда возникают проблемы с мониторингом, алерт OK перестает быть активным, из-за чего автоматически создаётся алерт ERROR о неработающем мониторинге. И вот уже на него надо реагировать.

Данная проверка очень эффективна в ситуациях, которая произошла в роковой день (10 марта): первый алерт о проблеме (ERROR) мы получили как раз от DMS.

События разворачивались следующим образом:

  • Первое сообщение о проблеме алерт от DMS, полученный приблизительно в 3:20 ночи по Москве.

  • Связываемся с коллегами из Okmeter и узнаем, что они испытывают проблемы с ЦОД, в котором расположены. Детальной информации на данный момент не получаем, из-за чего масштабы аварии нам неизвестны. Кроме того, дежурный инженер видит, что у нас корректно работают другие системы мониторинга (blackbox и Kubernetes). Поэтому принимается решение отложить инцидент до утра.

  • Утром (в 8:14) становится известно, что ЦОД сгорел, а Okmeter не будет доступен до тех пор, пока не восстановит свою инфраструктуру в другом ЦОД.

Здесь также стоит остановиться подробнее на том, как была устроена инфраструктура у Okmeter. Основные её компоненты находились в дата-центрах OVH:

  • SBG-2 который был полностью уничтожен пожаром;

  • SBG-1 который был частично поврежден и обесточен.

Так уж получилось, что два формально разных дата-центра OVH оказались соседними зданиями. И пожар, случившийся в одном из них, в скором временем затронул и другое здание тоже.

Руководствуясь этой информацией утром 10 марта, мы сделали вывод, что нужно срочно замещать важные для нас функции Okmeter хоть какими-то решениями на временной основе.

Первые действия

В компании была собрана специальная команда, основными целями которой являлись:

  1. Определение масштабов аварии;

  2. Поэтапное планирование, как устранять последствия инцидента;

  3. Дальнейшее внедрение полученных решений.

В неё вошли тимлиды наших DevOps-команд, инженеры нашей платформенной команды и CTO компании. Позже к ней также добавились отдельные инженеры, помогавшие со срочной разработкой и тестированием новых инструментов, заменивших критичные проверки от Okmeter.

Как уже упоминалось, Okmeter использовался у нас как одно из средств мониторинга. Какую же роль он занимал в общей системе, насколько критичен был? Для определения критичности инцидентов мы опираемся на следующую матрицу:

Матрица критичности алертовМатрица критичности алертов

Эта матрица распространяется на события из всех 3 используемых у нас систем мониторинга. Каждому инциденту, зафиксированному в любой из систем, назначается критичность от S1 (полная потеря работоспособности компонента системы) до S9 (диагностическая информация). Большая часть алертов с критичностью S1 это blackbox-мониторинг, который проверяет состояние сайтов клиента. Однако также к этой категории относится и незначительная часть алертов, отправляемых со стороны Okmeter (подробнее о них см. ниже). Другая незначительная часть приходится на S2, а все остальные имеют более низкую критичность (S3 и т.п.).

Проведя учет количества и видов алертов с S1 и S2 от Okmeter, мы приняли очевидное решение заняться ими в первую очередь, полностью компенсировав другим временным решением. А уже после этого можно было переходить на алерты с меньшей критичностью.

Оповестив всех клиентов, инфраструктуру которых задела авария у Okmeter, мы приступили к составлению плана восстановления.

Порядок и план восстановления алертов от Okmeter

Первая очередь алертов: S1-S2

Итак, какие именно критичные алерты мы потеряли с недоступностью Okmeter? Чтобы оперативно собрать эту информацию, каждая команда инженеров тех, что сопровождают проекты с мониторингом от Okmeter, подготовила статистику алертов, приходивших с 1 июля 2020 года.

Получился следующий список:

  1. Различные алерты для СУБД.

    1. Проверка работоспособности СУБД в целом (запущен ли процесс).

    2. Состояние репликации.

    3. Состояние запросов к БД (например, сколько запросов находятся в ожидании).

  2. Дисковое потребление на виртуальных машинах.

  3. Доступность виртуальных машин в целом.

Вторая очередь алертов: S3 и далее

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

По итогам этого аудита получилась вторая очередь. Она в основном состояла из проверок доступности менее критичных серверных приложений, таких как ZooKeeper.

Немного магического Bash

Чтобы реализовать замену проверкам и алертам Okmeter, требовалось уметь деплоить на все серверы актуальные скрипты мониторинга, чтобы поддерживать их в актуальном состоянии. Реализация: с помощью Ansible-плейбуков мы задеплоили все необходимые скрипты, среди которых был скрипт автообновления. Последний раз в 10 минут загружал новые версии других скриптов. Из-за очень сжатых сроков основную часть скриптов мы писали на Bash.

Общий подход получился следующим:

  1. Сделали shell-скрипты, которые запускались на виртуальных машинах и bare metal-серверах. Помимо своей основной функции (проверка работоспособности некоторых компонентов) они должны были доставлять сообщения в наш мониторинг с такими же лейблами и другими элементами, как и у Okmeter: имя триггера, его severity, другие лейблы и т.д. Это требовалось для сохранения той логики работы мониторинга, которая была и до падения. В общем, чтобы процесс работы дежурных инженеров оставался неизменным и чтобы работали прежние правила управления инцидентами.

    Быстрой реализации этого этапа способствовал тот факт, что у нас уже были готовые инструменты для работы с API мониторинга внутренний инструмент под названием flint (flant integration).

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

  3. В течение некоторого времени мы дополнительно проверяли, что алерты корректные, а сами скрипты выполняются правильно.

Результаты

На полную реализацию этого временного решения нам понадобился один рабочий день для алертов типа S1-S2 и еще один для S3. Все остальные инциденты (с более низким приоритетом) команды добавляли в индивидуальном порядке по мере необходимости.

В общей сложности новые скрипты были задеплоены примерно на 3000 хостов.

На каждом этапе решения проблемы мы подробно информировали клиентов о ходе восстановительных работ:

Позже статус по аварии и ликвидации её последствий обновлялся как в текстовом виде, так и на регулярных еженедельных встречах. Клиенты отнеслись к происходящему с пониманием и даже уважением:

В тот момент, когда Okmeter стал доступен и функционировал полностью корректно, мы отключили свой кастомный мониторинг, после чего удалили все его следы, оставшиеся на серверах.

Выводы

Для нас данная авария стала показательным случаем и проверкой основных процессов, которые мы выстраиваем в компании: инцидент затронул все команды, практически каждого инженера и сотрудника компании. Основной вывод оказался банален: слаженная командная работа вместе с четким, структурированным планом действий (DRP) позволяет решать задачи любой сложности в очень сжатые сроки. И это по-настоящему ценно в те редкие моменты, когда разваливается гораздо больше, чем вы могли себе представить запланировать.

Более практичные выводы таковы:

  1. Если сервис требует повышенной отказоустойчивости, всегда важно разделять инфраструктуру не только на уровне разных ЦОД, но и географически. Казалось бы, разные ЦОДы у OVH? А в реальной жизни они горели вместе

  2. В процессе подобного инцидента могут возникнуть проблемы и недопонимания в коммуникациях как между Флантом и Okmeter, так и между инженерами внутри нашей компании: недостаточное информирование о масштабах инцидента, медленная реакция и т.п. Хотя в этот раз нам повезло (их не возникло), мы пересмотрели свой регламент эскалации и еще немного ужесточили его, чтобы исключить вероятность появления таких проблем в будущем.

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

P.S.

Читайте также в нашем блоге:

Подробнее..

Перевод Ваша устаревшая база данных перерастает сама себя. Опыт chess.com

08.04.2021 10:11:09 | Автор: admin

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

Примечание: первоначально эта статья была опубликована в блоге моего хорошего друга unstructed.tech.

База данных становится слишком большой или старой? Ее тяжело обслуживать? Что ж, надеюсь, я смогу немного помочь. Текст, который вы собираетесь прочитать, содержит реальный опыт масштабирования монолитной базы данных, лежащей в основе одного из сайтов Топ-250 (согласно alexa.com). На момент написания этой статьи chess.com занимал 215 место в мире по популярности. Ежедневно к нам заглядывали более 4 млн уникальных пользователей, а наши MySQL-базы обрабатывали в общей сложности более 7 млрд запросов. Год назад сайт ежедневно посещали 1 млн уникальных пользователей; в марте прошлого года их число увеличилось до 1,3 млн; сегодня более 4 млн человек заходят на chess.com ежедневно, а число сыгранных партий превышает 8 млн. Я, конечно, знаю, что это не сопоставимо с самыми крупными игроками на рынке, однако наш опыт все же может помочь в такой сложной задаче, как исправление монолитной базы данных и ее вывод на новый уровень производительности.

Примечание: Это моя первая статья, и она довольно длинная (и это при том, что мне пришлось вырезать примерно половину текста, чтобы сделать ее читаемой). Так что некоторые вещи могут оказаться не слишком понятными и недостаточно объясненными, и я прошу за это прощения. Свяжитесь со мной в LinkedIn, и мы сможем обсудить все вопросы более подробно.

Обновление: прочитав массу комментариев [к оригинальной публикации прим. перев.], я хотел бы добавить/уточнить несколько моментов. Мы широко используем кэширование иначе не продержались бы и дня. И да, мы используем Redis (зачастую выжимая из него максимум). Мы пробовали MongoDB и Vitess, но они нам не подошли.

Состояние, в котором мы находились пару лет назад

Где-то в середине 2019-го мы начали замечать, что основной кластер БД потихоньку становится чрезмерно громоздким. У нас также имелись три меньших по размеру и менее загруженных базы данных, но все данные в конечном итоге всегда оказывались в основной БД. Удивительно, но она была в довольно приличном состоянии для базы, которая начала свою работу более 12 лет назад. Не так много неиспользуемых/лишних индексов (существующие были преимущественно хороши). Мы постоянно отслеживали и оптимизировали тяжелые/медленные запросы. Значительная часть данных была денормализована. И речь идет не о каких-то посторонних ключах многие вещи были реализованы в самом коде (фильтрация, сортировка и т.п. с тем, чтобы БД работала только с самыми эффективными индексами), работающем на последней версии MySQL, и т.д., и т.п. Мы о ней не забывали, и в результате со временем она эволюционировала во вполне хороший инструмент.

Предостережение: я никого не призываю заниматься подобными микро-оптимизациями. Они работают для chess.com, для его масштаба. Любое решение перед реализацией тщательно тестируется, результаты внимательно оцениваются. Так что мы знаем, что оно работает для нас.

Самая большая проблема, с которой мы столкнулись в тот момент, состояла в том, что для изменения почти любой таблицы требовалось вывести половину хостов из ротации, провести ALTER, вернуть их в ротацию. А затем все это повторить для другой половины. Нам приходилось проводить эти операции в часы затишья, поскольку исключение половины хостов из работы в пиковое время, скорее всего, обрушило бы вторую половину. С ростом сайта старые функции получали новые воплощения, и нам часто приходилось проводить ALTER'ы (те, кто в теме, поймут, о чем я). Процесс был бы гораздо менее напряженным, если бы мы могли исключить из ротации только небольшой набор таблиц, а не всю БД. Поэтому мы разработали 5-летний план для основного кластера (ох, какими наивными мы были...), в котором прописали шаги по разбиению базы на множество более мелких. Это должно было упростить и облегчить обслуживание (ну, хоть с этим угадали...). План исходил из годовых темпов роста в ~25% (именно такие темпы наблюдались на тот момент).

Где мы были около 2 лет назадГде мы были около 2 лет назад

РЕАЛЬНАЯ проблема вносит коррективы в наш план

Все вы наверняка знаете о COVID-19 и суматохе, с ним связанной. Легко догадаться и о том, что мы вовсе не были ко всему этому готовы (не ожидали того воздействия, которое локдаун окажет на трафик). Интерес к шахматам взлетел до небес, как только (большая) часть Европы отправилась на самоизоляцию. Забавно, но можно было сказать, какая страна ввела локдаун, просто глядя на количество регистрирующихся пользователей по странам это было так очевидно... И все наши показатели взлетели до небес. Как ни странно, базы данных работали нормально (не супер, конечно, но вполне справлялись с трафиком). Но в то же время мы заметили, что хост reports не поспевал за хостами production (то есть частенько он отставал на 30-60 секунд в репликации), что заставило обратить внимание на поток репликации и его доступную пропускную способность. И она была практически полностью исчерпана (на пике потребления оставалось не более 5%). В тот момент мы уже понимали, что в США (откуда основная часть наших игроков) скоро также введут самоизоляцию. Это означало бы, что наши реплики не смогут обработать все операции записи в мастер (впрочем, это случилось бы, даже если мы просто продолжали медленно, но верно расти). Это был бы конец chess.com, поскольку код не готов к значительным задержкам репликации при чтении данных с реплик, а отправка всех SELECT'ов на мастер привела бы к его падению. Цель стала ясна: снизить число операций записи в главный кластер, и сделать это как можно скорее. На самом деле это входило в наш изначальный план, только тот был растянут на пять лет. А у нас на все была пара месяцев...

Решение

Как снизить количество операций записи в БД? На первый взгляд все просто: определить самые нагруженные в этом смысле таблицы и выкинуть их из базы данных. Так мы просто разбиваем операции записи на два отдельных потока, не меняя их число. Это могут быть таблицы с большим числом INSERT'ов, или таблицы без множества INSERT'ов, но с записями, которые часто обновляются. Их просто надо определить в какое-нибудь другое место. Но как сделать это без простоя? Как вы догадываетесь, не все так просто.

Сначала мы выделили таблицы с наибольшим числом обновлений (INSERT, DELETE или UPDATE). Основная их часть была красиво сгруппирована в зависимости от функции, для которой использовалась (в большинстве таблиц, связанных с игрой, запись велась примерно с одинаковой скоростью и т. п.). В итоге мы получили список из 10-15 таблиц для 3 различных функций сайта. При их изучении проявилась еще одна проблема: поскольку мы не можем делать JOIN'ы между базами на разных хостах, надо перенести все возможные таблицы, отвечающие за определенный функционал, чтобы упростить проект. Впрочем, этот момент был нам известен заранее, поскольку когда план только появился, мы уже имели успешный опыт похожей миграции для трех небольших, слабо задействованных таблиц в рамках проверки концепции (PoC).

Три упомянутых выше нагруженных функции это logs (не совсем функция и не совсем логи просто неудачное название), games (чего и следовало ожидать от шахматной платформы) и puzzles (шахматные задачи-пазлы). В случае логов мы выделили три сильно изолированных таблицы (что означало минимальные изменения в запросах/коде). То же самое и для игр. Но в пазлах было задействовано более 15 таблиц, и запросы к ним включали массу JOIN-операций с таблицами, которые должны были остаться в основной БД. Мы мобилизовали свои войска, привлекли более половины бэкенд-разработчиков, разбили их на команды и совместными усилиями приступили к миграции.

Одна неделя ушла на перемещение логов в выделенную базу данных, работающую на двух хостах, что дало нам время на передышку, поскольку основная часть операций записи касалась именно их. Еще месяц потребовался на перемещение игровых баз (было очень страшно: любая ошибка привела бы к катастрофе, учитывая, что вся суть сайта именно в шахматных партиях). Два месяца ушло на перемещение пазлов. После всех этих действий загруженность канала репликации снизилась до 80% на пике то есть у нас появилось время перегруппироваться и распланировать предстоящие проекты немного тщательнее.

Исполнение

Итак, как же нам это удалось?

Как вы догадываетесь, у подобного предприятия имеются два аспекта: на стороне кода и на стороне базы данных, и оба требуют большой работы. На стороне кода есть несколько предварительных условий. Прежде всего, нужна система feature flag (она же feature toggle) либо внутренняя, либо сторонняя. Наша разработана на заказ и весьма обстоятельна (пожалуй, это отличная тема для новой статьи). Абсолютный минимум, который должен быть у такой системы, возможность открывать или запрещать доступ к тестовой функции в диапазоне от 0 до 100 процентов. Другая полезная вещь хорошее покрытие тестами. Несколько лет назад вся наша кодовая база была переписана с нуля, так что нам повезло (пару раз этот факт сильно выручал).

Что касается изменений кода и базы, кое-что можно делать параллельно, другие операции приходится делать последовательно. Проще всего начать с создания новой БД. Все наши новые базы данных извлекаются из основной: мы называем их partitions (партициями или разделами), а сам процесс partitioning (партиционированием, разбиением на разделы). Для размещения используется 2-хостовая схема (мастер и failover-мастер, т.е. реплика), но в принципе схема может быть любой.

В новом кластере создается база данных со схемой, идентичной схеме той базы, которую мы пытаемся разделить (именуя ее в соответствии с потребностями; в нашем случае первая называлась logs). Затем импортируется резервная копия основной базы данных, после чего мы делаем мастер partition-кластера репликой основного мастера (именно поэтому копируем схему и импортируем резервную копию). В результате новый кластер становится очередной репликой основного кластера, только база данных называется иначе. На некоторое время оставляем все как есть новая база просто реплицирует трафик и обновляется в связке с остальным кластером, пока мы работаем над кодовой частью проекта.

Так выглядит кластер после добавления новых хостовТак выглядит кластер после добавления новых хостов

До начала работы над данным проектом у нас, по сути, было открыто два подключения к базе данных из кода: read-only для работы с репликами и read/write для работы с мастером. Оба подключения проходили через HAProxy, чтобы попасть туда, куда требовалось. Первое, что мы сделали, создали параллельный набор подключений. В нем read/write идет к Partition Master, а read-only к Partition Replica.

Chess.com написан на PHP, так что я буду использовать PHP-примеры для иллюстрации необходимых изменений. При этом постараюсь максимально приблизить его к псевдокоду, чтобы все смогли понять, что именно происходит (вы будете удивлены, узнав, как много веб-сайтов из Топ-1000 написаны на PHP и насколько легко масштабировать PHP до этих высот. Пожалуй, хорошая тема для еще одной статьи).

Изменения кода сводятся к 3 вещам:

  • Удаление JOIN-запросов к таблицам, которые теперь обитают в разных базах данных.

  • Агрегирование, слияние и сортировка данных в коде (поскольку теперь не получится проводить некоторые из этих операций в БД)

  • Использование feature flag-системы для определения того, на какие хосты идут запросы.

Чтение данных

Хотя удаление JOIN'ов и кажется простым, на самом деле таким не является. В следующих примерах я предполагаю, что таблицы games и additional_game_data перемещаются в базу данных в новом partition (примеры на псевдокоде, так что не ожидайте, что они будут идеальными). Итак, запрос, который выглядел следующим образом:

SELECT u.user_id, u.username, u.rating, ugd.start_rating, ugd.end_rating, gd.resultFROM user_game_data ugd INNER JOIN game_data gd on ugd.game_id = g.game_idINNER JOIN users u on u.user_id = ugd.user_id WHERE     g.finished = 1 AND     g.tournament_id = 1234 AND     u.banned = 0ORDER BY u.rating DESCLIMIT 5;

теперь будет выглядеть так (поскольку мы больше не можем сделать JOIN с таблицей users):

SELECT ugd.user_id, ugd.start_rating, ugd.end_rating, gd.resultFROM user_game_data ugdINNER JOIN game_data gd on ugd.game_id = g.game_idWHERE    g.finished = 1 AND    g.tournament_id = 1234;

Мы убрали JOIN с таблицей users, выбор столбцов из users, условие для столбца из users в выражении с WHERE, сортировку и LIMIT по очевидным причинам. Теперь все это надо реализовать в коде. Прежде всего, нужна информация о пользователях. Давайте ее получим:

SELECT u.user_id, u.username, u.ratingFROM users uWHERE     u.user_id IN (:userIDs) AND     u.banned = 0ORDER BY u.rating DESCLIMIT 5;

Здесь :userIDs это список идентификаторов пользователей, которые были получены в первом запросе. Теперь остается только объединить два набора данных и поместить их в проверочное условие feature flag. В конечном итоге мы получим нечто вроде приведенного ниже (псевдо-)кода. Оба return возвращают один и тот же результат:

if ($this->features->hasAccess('read_logs_partition')) {    // предположим, что этот результирующий набор сопоставлен по user_id; на самом деле не имеет значения, как именно это сделано    $partitionData = $this->partitionConnection->query('        SELECT ugd.user_id, ugd.start_rating, ugd.end_rating, gd.result        FROM user_game_data ugd        INNER JOIN game_data gd on ugd.game_id = g.game_id        WHERE            g.finished = 1 AND            g.tournament_id = 1234;    ');    $mainDbData = $this->mainConnection->query('        SELECT u.user_id, u.username, u.rating        FROM users u        WHERE             u.user_id IN (:userIDs) AND             u.banned = 0        ORDER BY u.rating DESC        LIMIT 5;    ');    $result = [];    foreach ($mainDbData as $singleRecord) {        $result[] = array_merge($singleRecord, $partitionData[$singleRecord['user_id']]);    }    return $result;}return $this->mainConnection->query('    SELECT u.user_id, u.username, u.rating, ugd.start_rating, ugd.end_rating, gd.result    FROM user_game_data ugd    INNER JOIN game_data gd on ugd.game_id = g.game_id    INNER JOIN users u on u.user_id = ugd.user_id    WHERE        g.finished = 1 AND        g.tournament_id = 1234 AND        u.banned = 0    ORDER BY u.rating DESC    LIMIT 5;');

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

Запись данных

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

Начну с решения проблемы автоинкрементов: все операции записи идут либо в основную БД, либо в новый partition-кластер (то есть feature flag разом принимает значение 0% или 100%). Никаких промежуточных вариантов. Если произойдет сбой и хотя бы один INSERT попадет в новый partition, partition-хосты полностью прекратят репликацию, а ряд запросов придется пропускать вручную или же начинать всю работу на уровне БД заново (удалять и воссоздавать partition-БД).

Например, если автоинкремент в мигрируемой таблице дошел до 1000 и случился сбой, из-за которого INSERT'ы попали в новую partition, автоинкремент там будет принимать значения 1001, 1002 и т.д. Теперь предположим, что сбой устранен, и записи снова поступают в главную БД с инкрементами 1001, 1002... При репликации данных записей на новые хосты MySQL обнаружит, что там уже есть записи с такими инкрементами, и выдаст ошибку unique constraint violation. И нам придется все исправлять.

Все это не проблема, если используются UUID. Подробнее об этом речь пойдет далее в статье.

Пример кода (он еще проще, если используется обычный SQL просто надо использовать разные объекты для подключения к БД в if/else):

$game = new Game($user, $result);if ($this->features->hasAccess('write_logs_partition')) {    $this->logsEntityManager->persist($game);    $this->logsEntityManager->flush();} else {    $this->mainEntityManager->persist($game);    $this->mainEntityManager->flush();}

Само переключение

Это как раз та ситуация, когда хороший инструмент для мониторинга БД (вроде PMM), мониторинг ошибок (типа Sentry) и feature toggling творят чудеса. Начинаем с простого: посылаем минимально возможный процент SELECT'ов на новые partition-хосты (процент зависит от возможностей feature flag-системы). Смотрим на ошибки/исключения. Повторяем процесс, пока процент не вырастет до 100 (обычно на это уходит более недели). С помощью инструментов для мониторинга БД проверяем, что SELECT'ы больше не затрагивают таблицы в основном кластере БД.

Постепенно переключаем операции чтения на partition-кластерПостепенно переключаем операции чтения на partition-кластер

Если мигрируемые таблицы базируются на UUID считайте, что вам повезло. Мы просто повторяем процесс, описанных выше, для записи. Это сработает, поскольку к этому моменту все SELECT'ы будут направляться на новые partition-хосты. И если мы будем писать прямиком в них (независимо от процента), то данные будут полными (одна часть попадет туда напрямую, другая будет реплицирована с мастера основной БД). При этом в главной БД не будет данных, напрямую записанных на новые хосты (это проблему можно было бы решить с помощью двунаправленной репликации, но это просто повысило бы сложность всего проекта). Это означает, что мы уже не можем перенаправить туда SELECT'ы, поскольку данные неполны (ниже поговорим подробнее о том, как отыграть все назад). И аналогично тому, что сделано с чтением, продолжайте увеличивать процент запросов, пока он не достигнет 100%, а инструменты мониторинга не подтвердят, что переключение завершено. Теперь просто отключаем репликацию между мастером нового partition-кластера и основным кластером. Вот и все.

Увы, процесс не так прост, если используются первичные ключи с автоинкрементами. Или же прост в зависимости от того, как на него посмотреть. Но он определенно более напряженный. Нужно мгновенно переключить feature flag с нуля до 100%. В результате все либо будет работать, как ожидалось, либо нет: наличие нескольких запасных хостов для тестирования процесса переключения очень помогает; настоятельно рекомендую. Если все выглядит отлично (или хотя бы приемлемо), то репликация останавливается (как и в случае с UUID), а любые незначительные проблемы исправляются позже. Если же все пошло не по сценарию, придется вернуть все назад.

Мгновенное переключение операций чтения с мастера основного кластера на мастер partition-кластера (для первичных ключей с автоинкрементом)Мгновенное переключение операций чтения с мастера основного кластера на мастер partition-кластера (для первичных ключей с автоинкрементом)Конечный результатКонечный результат

Как вернуть все назад

За последние пару лет мы создали 6 таких partitions, и только однажды наш план провалился (на самом деле, это была вина feature flag-системы вкупе с автоинкрементами). Опять же, трудоемкость отката зависит от того, используете вы автоинкременты или UUID.

В случае UUID отмена довольно проста. Просто перенаправьте операции записи и чтения (с помощью feature flag) обратно в основной кластер. Остановите репликацию на partition-хосты. Теперь, в зависимости от трафика, есть два пути: можно вручную установить, какие данные отсутствуют в главном кластере, и скопировать их туда, либо закопаться в бинлоги (если они включены), засучить рукава и скопировать недостающие данные. Могло быть и хуже (просто прочитайте следующий абзац).

Этот абзац про автоинкременты. Как и выше, перенаправьте операции записи и чтения в основной кластер. В данном случае просто скопировать данные не получится, поскольку основной кластер уже начал заполняться данными с теми же PK (автоинкрементами), которые существуют в новом partition. Так что придется копнуть поглубже. О простом копировании данных, вероятно, не может быть и речи, если PK хранятся в другом месте в качестве внешних ключей (FK). Возможно, вам больше повезет найти все это в бинлогах (если они включены).

К сожалению, хотя мы всегда были готовы отыграть любой потенциальный бардак еще до того, как у нас появился шанс познакомиться с ним впервые, когда он все-таки наступил, мы просто решили его проигнорировать. Нам повезло. Поскольку feature flag-система прокололась только на одном хосте, после переключения всех запросов на главный кластер потерялось несколько десятков тысяч автоматических сообщений, отправленных системой пользователям. И мы посчитали, что нет никакого смысла восстанавливать все эти записи, поскольку они использовали автоинкременты. Сбой продолжался минут пять, и пользователи от него не пострадали. Так что да, нам реально очень сильно повезло!

Результаты

А теперь о вкусняшках. Мы смогли настроить все новые partitions (я полагаю) таким образом, чтобы 95% запросов шли в мастер, а оставшиеся 5% в реплику/резервный (failover) мастер просто для того, чтобы держать MySQL наготове (на случай, если резерву внезапно придется заработать в полную силу). Это означает, что при мониторинге кластера нам достаточно наблюдать за состоянием мастера. Проводить операции ALTER очень просто, так как не нужно переживать о том, сможет ли один хост (пока на другом работает ALTER) обработать весь трафик.

С кодом не так все радужно необходимо удалить все условия, разбросанные по кодовой базе. На стороне БД нужно провести DROP всех мигрированных таблиц из основной базы данных, а в новом partition'е, наоборот, оставить только их. Теперь это безопасно, ведь репликация между ними не работает.

Закончить хочу комментарием от нашего легендарного сисадмина после того, как минисериал Ход королевы (The Queens Gambit), снятый по заказу Netflix, вызвал еще одну огромную волну трафика (подробнее об этом здесь):

Отвечаю: если бы мы не разбили наши базы, то последние два месяца (минимум) оказались бы для нас очень веселыми.Отвечаю: если бы мы не разбили наши базы, то последние два месяца (минимум) оказались бы для нас очень веселыми.

Бонус 1: Тонкости на что обратить внимание

  • Необходимо продолжать отправлять запросы SELECT на мастер главного кластера (если они изначально отправлялись на него) до тех пор, пока операции записи не будут переключены на мастер partition-кластера. Это связано с задержкой репликации, которая могла возникнуть между двумя мастерами.

  • Всегда проверяйте, что feature flag'и переключились на каждом из хостов. Как я упоминал выше, однажды случилось так, что на одном хосте они не сработали во время переключения записи на одном из partition'ов. Это причинило нам немало страданий.

  • Нам пришлось дублировать сущности Doctrine, чтобы убрать зависимость от сущностей в других базах данных.

  • Дублирование было необходимо, поскольку мы хотели оставить старый (legacy) код за feature flag'ом и в то же время использовать новые сущности без JOIN'ов.

  • Из-за специфики работы Doctrine нам пришлось поместить новые сущности в отдельное пространство имен.

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

Бонус 2: Немного статистики

Наверное, вы, как и я, обожаете графики. Что ж, наслаждайтесь (все они получены на мастере в основном кластере):

Число команд MySQL (мы начали работать над разделением данных в марте). Как видно, нам удалось сократить число запросов, хотя трафик продолжал расти.Число команд MySQL (мы начали работать над разделением данных в марте). Как видно, нам удалось сократить число запросов, хотя трафик продолжал расти.Статистика I/O диска. В июле нам удалось добиться того, что запросы почти не приводили к дисковым операциям (благодаря разбиению и оптимизации запросов, которые ранее приводили к появлению временных таблиц на дискеСтатистика I/O диска. В июле нам удалось добиться того, что запросы почти не приводили к дисковым операциям (благодаря разбиению и оптимизации запросов, которые ранее приводили к появлению временных таблиц на дискеИспользование диска выглядит прелестно, не так ли?Использование диска выглядит прелестно, не так ли?Использование дискаИспользование диска

P.S. от переводчика

Читайте также в нашем блоге:

Подробнее..

Кому-то Okmeter даже сможет заменить людей. Как будет развиваться сервис мониторинга после его покупки Флантом

31.05.2021 10:13:24 | Автор: admin

Флант и Okmeter сотрудничают с 2017 года. Для Фланта Okmeter один из основных инструментов мониторинга инфраструктуры клиентов; на протяжении этих лет компании сообща улучшают его возможности.

В середине мая Флант объявил о покупке Okmeter. Несмотря на то, что для некоторых участников DevOps-рынка сделка стала новостью, во многом это естественный результат плодотворного сотрудничества двух компаний.

Чтобы ответить на накопившиеся в сообществе вопросы, мы представляем интервью с Николаем Сивко, сооснователем и теперь уже бывшим владельцем Okmeter, и Андреем Колаштовым, совладельцем и управляющим партнёром Фланта, который будет заниматься развитием проекта Okmeter. Николай и Андрей рассказали, почему компании решились на эту сделку, как она повлияет на существующих и будущих клиентов Okmeter, а также о том, в каком направлении теперь будет развиваться сервис и какую функциональность получит в ближайшее время.

Николай Сивко выступает на одной из конференций HighLoad++Николай Сивко выступает на одной из конференций HighLoad++

О сделке и немного предыстории

Как компании к этому пришли?

Николай: Началось всё с того, что года тричетыре назад Флант пришли к нам и сказали: Вы бежите быстро, мы вас устанем догонять. И мониторинг не наш фокус.

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

Андрей: Мы тогда очень устали от того, чтобы настраивать разные небезызвестные системы мониторинга, приделывать к ним какие-то экспортеры и ждать, что у нас что-то будет действительно замониторено. И у нас не было команды, которая бы занималась сутью мониторинга: какую метрику замониторить, как это реализовать, как ее вывести на графики и так далее. Конечно, собственный мониторинг у нас как-то развивался и работал, но сказать, чтобы это было сделано круто, нет. А у ребят это было именно так.

А как появилась сама идея сделки?

Николай: Флант был у нас самым большим клиентом. В какой-то момент им стало интереснее больше влиять на продукт, быстрее закрывать конкретно свои потребности. Они захотели контролировать Okmeter. Нам эта идея понравилась.

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

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

Андрей Колаштов (в центре) на HighLoad++ Весна 2021Андрей Колаштов (в центре) на HighLoad++ Весна 2021

Николай как-то будет участвовать в дальнейшем развитии Okmeter?

Андрей: Мы бы хотели использовать огромный опыт Николая для помощи в реализации некоторых функций мониторинга. На начальном этапе точно будем прибегать к его консультациям, и достаточно часто.

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

Дело в том, что Флант уже многое умеет и многое знает про Okmeter. Это для них не новые ворота, не blackbox какой-то. У Фланта сформировалось собственное видение того, как развивать сервис, и они уже начали его воплощать. Скажем так, у них уже руки чешутся сделать всё как надо.

Как сделка повлияет на рынок и на клиентов

Этот кейс может как-то повлиять на местный рынок мониторинга?

Андрей: Сейчас на рынке мало игроков, которые делают мониторинг и при этом имеют огромный опыт в эксплуатации, игроков, которые понимают, как делать мониторинг правильно. Сделка даст рынку возможность получить реальный опыт людей, эксплуатирующих серверы.

Николай: Я думаю, сделка повлияет не столько на рынок мониторинга, сколько на рынок эксплуатации и всего, что связано с DevOps. Потому что часть того, что делает сейчас Флант как сервисная компания эту экспертизу, можно заложить в продукт. И экспертиза будет, естественно, дешевле, чем люди. Кому-то Okmeter даже сможет заменить людей.

Андрей: Да, потому что Okmeter это мониторинг, который многое делает сам. Допустим, вы поставили 5 нод, и Okmeter вам сразу всё сам замониторил. Он автоматически нашел какой-нибудь Postgres, нарисовал по нему графики, тут же зажег алерты, что у вас есть какие-то проблемы. То есть это не вы создаете и добавляете эти алерты. За вас уже подумали, в какие точки стукнуться, чтобы проверить, что у вас всё хорошо или наоборот, и почему.

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

А Okmeter делает решение, которое самостоятельно работает, причем из коробки. Это то же самое, как если бы опытный человек взглянул на графики, на алерты, и сказал: болит вот здесь.

А что касается существующих клиентов Okmeter, как продажа сервиса скажется на них?

Николай: Положительно, конечно. У Фланта есть экспертиза, которой у Okmeter не было, и клиенты теперь смогут ее получать. Флант сидит на таком потоке знаний, которого в России реально ни у кого нет. Если всю эту экспертизу привнести в мониторинг, он станет квадратично более классным.

Андрей: С точки зрения тарифов пока мы ничего кардинально менять не планируем. С технической же стороны сделка повлияет только позитивно. Мы сейчас будем развивать продукт, добавлять туда новые функции, и планируем двигаться очень активно у нас набирается большая команда под задачи развития.

Связь с инцидентом OVH

Были слухи, что продажа Okmeter связана с недавним пожаром в дата-центрах французского провайдера OVH, когда мониторинг на некоторое время вышел из строя. Насколько это близко к реальности?

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

Пожар в дата-центрах OVH в марте этого года; автор фото Xavier GarreauПожар в дата-центрах OVH в марте этого года; автор фото Xavier Garreau

В чем была главная проблема с OVH?

Николай: В том, что OVH особо не рассказывал про свои дата-центры. Мы считали, что три дата-центра, в которых была размещена инфраструктура Okmeter в Страсбурге, это настоящая availability-зона, что дата-центры независимые. Мы на это полагались, и это было главной нашей ошибкой.

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

Прим. ред.: кстати, о событиях этого пожара и вынужденных действиях инженеров компании Флант мы ещё планируем опубликовать статью в скором времени.

Ближайшие планы

Как будет развиваться сервис?

Андрей: Мы видим Okmeter как несколько взаимодополняющих продуктов: хранилище, платформа и insights (идеи).

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

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

Insights это история про готовые метрики, дашборды и алерты для популярных технологий и частых кейсов. Мы создадим удобную базу из коробки. Можно сказать, что команда insights будет заниматься смыслом. Она будет разбираться с тем, что именно нужно замониторить так, чтобы действительно понимать, что происходит в сервисах, корректно ли они работают и не собираются ли упасть. Плюс эта команда будет заниматься тем, чтобы правильно строить дашборды так, чтобы была нормальная наблюдаемость (observability). Чтобы можно было посмотреть на проблему и сразу понять, как это починить, не залезая в логи. То есть, чтобы максимально ускорить процесс решения проблем.

Расскажите чуть подробнее о платформе и, в частности, об интерфейсе Okmeter: что планируете улучшить в первую очередь?

Андрей: Мы планируем сделать более современные способы визуализации тех или иных компонентов, в зависимости от того, что потребуется нам и нашим клиентам. Также расширим интеграцию с Open Source-сервисами мониторинга, добавим возможность подключать к Okmeter сторонние плагины, добавлять в интерфейс графики и дашборды из других инструментов. По сути, в Okmeter уже есть почти все графики и всё, что нужно для мониторинга, но нам самим нужно больше, и мы это сделаем.

В командах будут только разработчики?

Андрей: В командах хранилища и платформы да. В insights и разработчики, и опытные SRE-инженеры; они будут разбираться, что у клиентов упало, почему мы не смогли вовремя это спрогнозировать и что надо замониторить, чтобы подобные проблемы предотвращать. Это будет большая команда, которая займется доработками как интерфейса, так и агента (пользовательская программа-клиент Okmeter прим. ред.), и которая будет помогать быстрее находить проблемы.

То есть это будут действующие инженеры?

Андрей: Не совсем. Они будут приходить на аварию, но не для того, чтобы починить ее, а чтобы проанализировать, почему Okmeter не предсказал эту аварию. Допустим, лежала база данных. У нее постепенно переполнялся некоторый буфер. Но графика и алертов на это переполнение не было, хотя мы могли бы за неделю до этого узнать, что буфер начал переполняться. И вот команда insights разбирается с этим, думает, как этот буфер замониторить и в итоге помочь администраторам базы сделать так, чтобы база потом не упала.

Планируете ли расширять количество интеграций, с теми же базами данных, например?

Андрей: Конечно. Будет очень хорошее расширение. Мы будем добавлять всё самое популярное, чего сейчас нет: MongoDB, ClickHouse, ProxySQL, HAProxy, Ceph и расширять функционал существующих. И не только базы данных. Также будем сильно расширять историю с мониторингом Kubernetes.

У Фланта, кажется, уже достаточно много наработок по мониторингу Kubernetes? Как это теперь будет совмещаться с Okmeter?

Андрей: Да, Kubernetes у нас уже замониторен своими силами. Это сделано на базе Prometheus и кучи кастомных и собственных экспортеров, чем занималась специальная команда внутри Фланта, отвечающая за платформу Kubernetes. Много сил было вложено в правильный мониторинг Kubernetes. В то же время своя интеграция с Kubernetes есть и у Okmeter. Okmeter уже работает с Kubernetes аналогично тому, как с другими сервисами и софтом на обычных узлах. То есть его можно поставить внутрь Kubernetes, он сам определит весь софт и попытается к нему подключиться, чтобы снимать метрики. Мы будем объединять эту интеграцию и наши наработки в более мощное и универсальное решение на базе Okmeter, добавляя в него наши дашборды, экспортеры и опыт в целом.

Наша детальная статистика потребления трафика по конкретному пространству имен Kubernetes-кластераНаша детальная статистика потребления трафика по конкретному пространству имен Kubernetes-кластера

А что касается инсталляций on-premises какие здесь планы?

Андрей: Это направление мы тоже будем активно развивать. Okmeter раньше был почти для всех облачным, сейчас же появилась возможность устанавливать его on-premises силами Фланта. У нас уже есть опыт в таких инсталляциях Okmeter.

То есть будет два варианта установки?

Андрей: Да, можно будет выбирать облачную или on-premises-версию. Если вам надо, например, замониторить пару серверов и у вас нет жестких требований по безопасности, оптимальный вариант облачная версия. Если требования по ИБ высокие, можно установить Okmeter on-premises в свой закрытый контур. Хотя принцип работы тот же: ставится агент, который отправляет данные не в облако, а в локальное хранилище.

Планируете ли создание Open Source-компонентов Okmeter?

Андрей: Да. Хранилище Okmeter с большой вероятностью будет основано на Open Source-компонентах. Соответственно, все эти компоненты мы будем выкладывать на GitHub, будем в них контрибьютить и добавлять что-то, что нам помогает улучшать хранилище. Планов по открытию кода платформы и Insights в настоящий момент нет, но всё может измениться. И, конечно, это не касается тех компонентов, которые уже являются Open Source-проектами и в upstream которых мы будем приносить свои улучшения.

Николай упоминал о планах по повышению отказоустойчивости инфраструктуры Okmeter. Как именно это будет реализовано?

Андрей: Это одна из первоочередных наших задач, мы очень сфокусированы на том, чтобы сделать отказоустойчивый storage.

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

Второй шаг разместить инфраструктуру в нескольких ЦОДах, в разных странах. Клиенты смогут сами выбирать уровень отказоустойчивости, то есть сколько копий данных хранить, в каких ЦОДах.

Глобальные планы

Какова стратегия Фланта в плане повышения конкурентоспособности Okmeter на международном рынке?

Андрей: Во-первых, расширение функциональности. Обязательно будем улучшать UХ, чтобы сделать платформу реально удобной и функциональной, дополнить всем, что у нас самих болит, всем, что мы хотим замониторить.

Во-вторых, активный маркетинг по всем направлениям. У Okmeter уже есть английская версия сайта и зарубежные клиенты, но этого явно недостаточно. Маркетинг будет очень серьезно усилен.

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

P.S.

Читайте также в нашем блоге:

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru