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

Jsonnet

Путь разработчика в SRE зачем идти в инфраструктуру и что из этого выйдет

30.06.2020 20:09:15 | Автор: admin
Около года назад я переквалифицировался из .NET-разработчика в SRE. В этой статье делюсь историей о том, как группа опытных разработчиков отложила в сторону C# и пошла изучать Linux, Terraform, Packer, рисовать NALSD и строить IaC, как мы применяли практики экстремального программирования для управления инфраструктурой компании, и что из этого вышло.




В Додо Пицце больше 600 пиццерий в 13 странах мира, а большая часть процессов в пиццериях управляется с помощью информационной системы Dodo IS, которую мы сами пишем и поддерживаем. Поэтому надёжность и стабильность системы важны для выживания.

Сейчас стабильность и надёжность информационной системы в компании поддерживает команда SRE (Site Reliability Engineering), но так было не всегда.

Предыстория: параллельные миры разработчиков и инфраструктуры


Много лет я развивался как типичный fullstack-разработчик (и немного scrum-мастер), учился писать хороший код, применял практики из Extreme Programming и старательно уменьшал количество WTF в проектах, к которым прикасался. Но чем больше появлялось опыта в разработке ПО, тем больше я осознавал важность надёжных систем мониторинга и трейсинга приложений, качественных логов, тотального автоматического тестирования и механизмов, обеспечивающих высокую надёжность сервисов. И всё чаще стал заглядывать через забор к команде инфраструктуры.

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

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

Бермудский треугольник проблем


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

Проблемы разработчиков


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

  • собрали классную команду;
  • за полгода написали удобное и красивое приложение;
  • подкрепили торжественный запуск вкусными промоакциями;
  • и в первый же день благополучно упали под нагрузкой.



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

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

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

Сейчас это не сложно. В последние годы появилось огромное количество инструментов, которые позволяют программистам заглянуть в мир эксплуатации и ничего не сломать: Prometheus, Zipkin, Jaeger, ELK стек, Kusto.

Тем не менее у многих разработчиков до сих пор есть серьёзные проблемы с теми, кого называют инфраструктурой/DevOpsами/SRE. В итоге программисты:

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

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

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

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

Проблемы инфраструктуры


Сложности есть и на другой стороне.

Сложно управлять десятками сервисов и окружений без качественного кода. У нас в GitHub сейчас больше 450 репозиториев. Часть из них не требует операционной поддержки, часть мертва и сохраняется для истории, но значительная часть содержит сервисы, которые нужно поддерживать. Им нужно где-то хоститься, нужен мониторинг, сбор логов, единообразные CI/CD-пайплайны.

Чтобы всем этим управлять, мы ещё недавно активно использовали Ansible. В нашем Ansible-репозитории было:

  • 60 ролей;
  • 102 плейбука;
  • обвязка на Python и Bash;
  • тесты в Vagrant, запускаемые вручную.

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

Причина крылась в том, что этот код не использовал многие стандартные практики в мире разработки ПО. В нём не было CI/CD-пайплайна, а тесты были сложными и медленными, поэтому всем было лень или некогда запускать их вручную, а уж тем более писать новые. Такой код обречён, если над ним работает более одного человека.

Без знания кода сложно эффективно реагировать на инциденты. Когда в 3 часа ночи в PagerDuty приходит алерт, приходится искать программиста, который объяснит что и как. Например, что вот эти ошибки 500 аффектят пользователя, а другие связаны со вторичным сервисом, конечные клиенты его не видят и можно оставить всё так до утра. Но в три часа ночи программистов разбудить сложно, поэтому желательно самому понимать, как работает код, который ты поддерживаешь.

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

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

Проблемы бизнеса


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

Прямые потери от нестабильности системы, связанной с надёжностью и доступностью.
В 2018 году у нас произошёл 51 критический инцидент, а критичные элементы системы не работали в сумме больше 20 часов. В деньгах это 25 млн. рублей прямых потерь из-за несозданных и недоставленных заказов. А сколько мы потеряли на доверии сотрудников, клиентов и франчайзи, подсчитать невозможно, в деньгах это не оценивается.

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

  • с одной стороны, для решения таких задач нужны программисты с глубокими знаниями инфраструктуры;
  • с другой стороны, нужны operations (назовём их баззвордом DevOps), которые умеют программировать на хорошем промышленном уровне;
  • с третьей стороны, бизнесу нужен баланс между надёжностью и доступностью этих систем и их стоимостью.

И что с этим делать?


Как решить все эти проблемы? Решение мы нашли в книге Site Reliability Engineering от Google. Когда прочли, поняли это то, что нам нужно.

Но есть нюанс чтобы всё это внедрить нужны годы, и с чего-то надо начинать. Рассмотрим исходные данные, которые у нас были изначально.

Вся наша инфраструктура почти полностью живет в Microsoft Azure. Есть несколько независимых кластеров для прода, которые разнесены по разным континентам: Европа, Америка и Китай. Есть нагрузочные стенды, которые повторяют продакшн, но живут в изолированной среде, а также десятки DEV-окружений для команд разработчиков.

Из хороших практик SRE у нас уже были:

  • механизмы мониторинга приложений и инфраструктуры (спойлер: это мы в 2018 думали, что они хорошие, а сейчас уже всё переписали);
  • процессы для дежурств 24/7 on-call;
  • практика ведения постмортемов по инцидентам и их анализ;
  • нагрузочное тестирование;
  • CI/CD-пайплайны для прикладного софта;
  • хорошие программисты, которые пишут хороший код;
  • евангелист SRE в команде инфраструктуры.

Но были и проблемы, которые хотелось решить в первую очередь:

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

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

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

Онбординг SRE-команды


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

На проект мы выделили 4 месяца и поставили три цели:

  1. Обучить программистов тем знаниям и навыкам, которые необходимы для дежурств и операционной деятельности в команде инфраструктуры.
  2. Написать IaC описание всей инфраструктуры в коде. Причём это должен быть полноценный программный продукт с CI/CD, тестами.
  3. Пересоздать всю нашу инфраструктуру из этого кода и забыть про ручное накликивание виртуалок мышкой в Azure.

Состав участников: 9 человек, 6 из них из команды разработки, 3 из инфраструктуры. На 4 месяца они должны были уйти из обычной работы и погрузиться в обозначенные задачи. Чтобы поддерживать жизнь в бизнесе, ещё 3 человека из инфраструктуры остались дежурить, заниматься операционкой и прикрывать тылы. В итоге проект заметно растянулся и занял больше пяти месяцев (с мая по октябрь 2019-го года).

Две составляющие онбординга: обучение и практика


Онбординг состоял из двух частей: обучения и работы над инфраструктурой в коде.

Обучение. На обучение выделялось минимум 3 часа в день:

  • на чтение статей и книг из списка литературы: Linux, сети, SRE;
  • на лекции по конкретным инструментам и технологиям;
  • на клубы по технологиям, например, по Linux, где мы разбирали сложные случаи и кейсы.

Ещё один инструмент обучения внутреннее демо. Это еженедельная встреча, на которой каждый (кому есть, что сказать) за 10 минут рассказывал о технологии или концепции, которую он внедрил в нашем коде за неделю. Например, Вася поменял пайплайн работы с Terraform-модулями, а Петя переписал сборку образов на Packer.

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



Практика. Вторая часть онбординга создание/описание инфраструктуры в коде. Эту часть разделили на несколько этапов.



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

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

Написание кода. Сюда входило само написание кода, создание CI/CD-пайплайнов, тестов и построение процессов вокруг всего этого. Мы написали код, который описывал и умел создавать с нуля нашу дев-инфраструктуру.

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

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

Наши инструменты для IaC
  • Terraform для описания текущей инфраструктуры.
  • Packer и Ansible для создания образов виртуальных машин.
  • Jsonnet и Python как основные языки разработки.
  • Облако Azure, потому что у нас там хостинг.
  • VS Code IDE, для которой создали единые настройки, расширенный набор плагинов, линтеров и прочего, чтобы писать унифицированный код и расшарили их между всеми разработчиками.
  • Практики разработки одна из основных вещей, ради которой затевался весь этот карнавал.


Практики Extreme Programming в инфраструктуре


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

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

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

Всё могло бы сложиться хорошо, но так не бывает.

Технические и антропогенные проблемы на пути


В рамках проекта было два вида проблем:

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

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

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

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

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

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

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

Итоги онбординга


По итогам проекта онбординга (он завершился в октябре 2019 года) мы:

  • Создали полноценный программный продукт, который управляет нашей DEV-инфраструктурой, с собственным CI-пайплайном, с тестами и прочими атрибутами качественного программного продукта.
  • Удвоили количество людей, которые готовы дежурить и сняли нагрузку с текущей команды. Спустя ещё полгода эти люди стали полноценными SRE. Теперь они могут потушить пожар на проде, проконсультировать команду программистов по НФТ, или написать свою библиотеку для разработчиков.
  • Сместили майндсет в сторону идей SRE. Не только у участников проекта онбординга, но и у тех программистов из продуктовых команд, которые теперь могут разговаривать с нами на одном языке.
  • Сильно устали: и те, кто участвовал в онбординге, и те, кто участвовал в дежурствах.

Вместо выводов: инсайты, не наступайте на наши грабли


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

Инфраструктура пока в прошлом. Когда я учился на первом курсе (15 лет назад) и начинал изучать JavaScript, у меня из инструментов были NotePad ++ и Firebug для отладки. C этими инструментами уже тогда нужно было делать какие-то сложные и красивые вещи.

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

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

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

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

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

Часто под кодом скрываются обычные конфиги и DSL. При этом вся логика происходит где-то глубже, куда нет доступа. Это сильно меняет подход к коду, тестированию и работе с ним.

Не бойтесь пускать разработчиков в инфраструктуру. Они могут привнести полезные (и свежие) практики и подходы из мира разработки ПО. Пользуйтесь практиками и подходами от Google, описанными в книге про SRE, получайте пользу и будьте счастливы.

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

PPS: Эта статья написана по моему выступлению на DevOpsConf осенью 2019 года. С тех пор прошло довольно много времени, и теперь уже точно понятно, что всё было не зря: тойл теперь не съедает бОльшую часть времени инженеров, наша команда теперь может реализовывать крупные долгосрочные проекты по улучшению инфраструктуры в широком смысле, а программисты почти не жалуются на безумных DevOps-инженеров, которые только мешают жить.

PPPS: В этом году конференция, посвящённая DevOps-практикам, будет называться DevOps Live 2020. Изменения коснутся не только названия: в программе будет меньше докладов и больше интерактивных обсуждений, мастер-классов и воркшопов. Рецепты о том, как расти и перестраивать процессы с помощью DevOps-практик. Формат также изменится два блока по два дня и домашние задания между ними.

Чтобы узнать подробнее о том, что будет происходить на DevOps Live и что полезного вынесут инженеры, безопасники, тимлиды и CTO, подписывайтесь на рассылку и следите за публикациями в блоге.
Подробнее..

Мониторинг Tarantool логи, метрики и их обработка

28.12.2020 18:17:25 | Автор: admin

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


Мониторинг Tarantool


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


Настройка логов в Tarantool


Базовое конфигурирование и использование логов


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


Каждое сообщение лога имеет свой уровень детализации. Уровень логирования Tarantool характеризуется значением параметра log_level (целое число от 1 до 7):


  1. SYSERROR.
  2. ERROR сообщения log.error(...).
  3. CRITICAL.
  4. WARNING сообщения log.warn(...).
  5. INFO сообщения log.info(...).
  6. VERBOSE сообщения log.verbose(...).
  7. DEBUG сообщения log.debug(...).

Значение параметра log_level N соответствует логу, в который попадают сообщения уровня детализации N и всех предыдущих уровней детализации < N. По умолчанию log_level имеет значение 5 (INFO). Чтобы настроить этот параметр при использовании Cartridge, можно воспользоваться cartridge.cfg:


cartridge.cfg( { ... }, { log_level = 6, ... } )

Для отдельных процессов настройка производится при помощи вызова box.cfg:


box.cfg{ log_level = 6 }

Менять значение параметра можно непосредственно во время работы программы.


Стандартная стратегия логирования: писать об ошибках в log.error() или log.warn() в зависимости от их критичности, отмечать в log.info() основные этапы работы приложения, а в log.verbose() писать более подробные сообщения о предпринимаемых действиях для отладки. Не стоит использовать log.debug() для отладки приложения, этот уровень диагностики в первую очередь предназначен для отладки самого Tarantool. Не рекомендуется также использовать уровень детализации ниже 5 (INFO), поскольку в случае возникновения ошибок отсутствие информационных сообщений затруднит диагностику. Таким образом, в режиме отладки приложения рекомендуется работать при log_level 6 (VERBOSE), в режиме штатной работы при log_level 5 (INFO).


local log = require('log')log.info('Hello world')log.verbose('Hello from app %s ver %d', app_name, app_ver) -- https://www.lua.org/pil/20.htmllog.verbose(app_metainfo) -- type(app_metainfo) == 'table'

В качестве аргументов функции отправки сообщения в лог (log.error/log.warn/log.info/log.verbose/log.debug) можно передать обычную строку, строку с плейсхолдерами и аргументы для их заполнения (аналогично string.format()) или таблицу (она будет неявно преобразована в строку методом json.encode()). Функции лога также работают с нестроковыми данными (например числами), приводя их к строке c помощью tostring().


Tarantool поддерживает два формата логов: plain и json:


2020-12-15 11:56:14.923 [11479] main/101/interactive C> Tarantool 1.10.8-0-g2f18757b72020-12-15 11:56:14.923 [11479] main/101/interactive C> log level 52020-12-15 11:56:14.924 [11479] main/101/interactive I> mapping 268435456 bytes for memtx tuple arena...

{"time": "2020-12-15T11:56:14.923+0300", "level": "CRIT", "message": "Tarantool 1.10.8-0-g2f18757b7", "pid": 5675 , "cord_name": "main", "fiber_id": 101, "fiber_name": "interactive", "file": "\/tarantool\/src\/main.cc", "line": 514}{"time": "2020-12-15T11:56:14.923+0300", "level": "CRIT", "message": "log level 5", "pid": 5675 , "cord_name": "main", "fiber_id": 101, "fiber_name": "interactive", "file": "\/tarantool\/src\/main.cc", "line": 515}{"time": "2020-12-15T11:56:14.924+0300", "level": "INFO", "message": "mapping 268435456 bytes for memtx tuple arena...", "pid": 5675 , "cord_name": "main", "fiber_id": 101, "fiber_name": "interactive", "file": "\/tarantool\/src\/box\/tuple.c", "line": 261}

Настройка формата происходит через параметр log_format так же, как для параметра log_level. Подробнее о форматах можно прочитать в соответствующем разделе документации.


Tarantool позволяет выводить логи в поток stderr, в файл, в конвейер или в системный журнал syslog. Настройка производится с помощью параметра log. О том, как конфигурировать вывод, можно прочитать в документации.


Обёртка логов


Язык Lua предоставляет широкие возможности для замены на ходу практически любого исполняемого кода. Здесь я хотел бы поделиться способом помещения стандартных методов логирования в функцию-обёртку, который мы использовали при реализации сквозного логирования в своих приложениях. Стоит отметить, что способ этот расположен в "серой" зоне легальности, так что прибегать к нему стоит только при отсутствии возможностей решить проблему более элегантно.


local log = require('log')local context = require('app.context')local function init()    if rawget(_G, "_log_is_patched") then        return    end    rawset(_G, "_log_is_patched", true)    local wrapper = function(level)        local old_func = log[level]        return function(fmt, ...)            local req_id = context.id_from_context()            if select('#', ...) ~= 0 then                local stat                stat, fmt = pcall(string.format, fmt, ...)                if not stat then                    error(fmt, 3)                end            end            local wrapped_message            if type(fmt) == 'string' then                wrapped_message = {                    message = fmt,                    request_id = req_id                }            elseif type(fmt) == 'table' then                wrapped_message = table.copy(fmt)                wrapped_message.request_id = req_id            else                wrapped_message = {                    message = tostring(fmt),                    request_id = req_id                }            end            return old_func(wrapped_message)        end    end    package.loaded['log'].error = wrapper('error')    package.loaded['log'].warn = wrapper('warn')    package.loaded['log'].info = wrapper('info')    package.loaded['log'].verbose = wrapper('verbose')    package.loaded['log'].debug = wrapper('debug')    return trueend

Данный код обогащает информацию, переданную в лог в любом поддерживаемом формате, идентификатором запроса request_id.


Настройка метрик в Tarantool


Подключение метрик


Для работы с метриками в приложениях Tarantool существует пакет metrics. Это модуль для создания коллекторов метрик и взаимодействия с ними в разнообразных сценариях, включая экспорт метрик в различные базы данных (InfluxDB, Prometheus, Graphite). Материал основан на функционале версии 0.6.0.


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


tarantoolctl rocks install metrics 0.6.0

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


dependencies = {    ...,    'metrics == 0.6.0-1',}

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


Встроенные метрики


Сбор встроенных метрик уже включён в состав роли cartridge.roles.metrics.


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


local metrics = require('metrics')metrics.enable_default_metrics()

Достаточно выполнить её единожды на старте приложения, например поместив в файл init.lua.


В список метрик по умолчанию входят:


  • информация о потребляемой Lua-кодом RAM;
  • информация о текущем состоянии файберов;
  • информация о количестве сетевых подключений и объёме сетевого трафика, принятого и отправленного процессом;
  • информация об использовании RAM на хранение данных и индексов (в том числе метрики slab-аллокатора);
  • информация об объёме операций на спейсах;
  • характеристики репликации спейсов Tarantool;
  • информация о текущем времени работы процесса и другие метрики.

Подробнее узнать о метриках и их значении можно в соответствующем разделе документации.


Для пользователей Cartridge также существует специальный набор встроенных метрик для мониторинга состояния кластера. На данный момент он включает в себя метрику о количестве проблем в кластере, разбитых по уровням критичности (аналогична Issues в WebUI Cartridge).


Плагины для экспорта метрик


Пакет metrics поддерживает три формата экспорта метрик: prometheus, graphite и json. Последний можно использовать, например, в связке Telegraf + InfluxDB.


Чтобы настроить экспорт метрик в формате json или prometheus для процессов с ролью cartridge.roles.metrics, добавьте соответствующую секцию в конфигурацию кластера:


metrics:  export:    - path: '/metrics/json'      format: json    - path: '/metrics/prometheus'      format: prometheus

Экспорт метрик в формате json или prometheus без использования кластерной конфигурации настраивается средствами модуля http так же, как любой другой маршрут.


local json_metrics = require('metrics.plugins.json')local prometheus = require('metrics.plugins.prometheus')local httpd = require('http.server').new(...)httpd:route(    { path = '/metrics/json' },    function(req)        return req:render({            text = json_metrics.export()        })    end)httpd:route( { path = '/metrics/prometheus' }, prometheus.collect_http)

Для настройки graphite необходимо добавить в код приложения следующую секцию:


local graphite = require('metrics.plugins.graphite')graphite.init{    host = '127.0.0.1',    port = 2003,    send_interval = 60,}

Параметры host и port соответствуют конфигурации вашего сервера Graphite, send_interval периодичность отправки данных в секундах.


При необходимости на основе инструментов пакета metrics можно создать собственный плагин для экспорта. Советы по написанию плагина можно найти в соответствующем разделе документации.


Добавление пользовательских метрик


Ядро пакета metrics составляют коллекторы метрик, созданные на основе примитивов Prometheus:


  • counter предназначен для хранения одного неубывающего значения;
  • gauge предназначен для хранения одного произвольного численного значения;
  • summary хранит сумму значений нескольких наблюдений и их количество, а также позволяет вычислять перцентили по последним наблюдениям;
  • histogram агрегирует несколько наблюдений в гистограмму.

Cоздать экземпляр коллектора можно следующей командой:


local gauge = metrics.gauge('balloons')

В дальнейшем получить доступ к объекту в любой части кода можно этой же командой.


Каждый объект коллектора способен хранить сразу несколько значений метрики с разными лейблами. Например, в результате кода


local gauge = metrics.gauge('balloons')gauge:set(1, { color = 'blue' })gauge:set(2, { color = 'red' })

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


gauge:inc(11, { color = 'blue' }) -- increase 1 by 11

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


В программе есть модуль server, который принимает запросы и способен сам их отправлять. Вместо того, чтобы использовать две различных метрики server_requests_sent и server_requests_received для хранения данных о количестве отправленных и полученных запросов, следует использовать общую метрику server_requests с лейблом type, который может принимать значения sent и received.


Подробнее о коллекторах и их методах можно прочитать в документации пакета.


Заполнение значений пользовательских метрик


Пакет metrics содержит полезный инструмент для заполнения коллекторов метрик коллбэки. Рассмотрим принцип его работы на простом примере.


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


local metrics = require('metrics')local buffer = require('app.buffer')metrics.register_callback(function()    local gauge = metrics.gauge('buffer_count')    gauge.set(buffer.count())end)

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


Мониторинг HTTP-трафика


Пакет metrics содержит набор инструментов для подсчёта количества входящих HTTP-запросов и измерения времени их обработки. Они предназначены для работы со встроенным пакетом http, и подход будет отличаться в зависимости от того, какую версию вы используете.


Чтобы добавить HTTP-метрики для конкретного маршрута при использовании пакета http 1.x.x, вам необходимо обернуть функцию-обработчик запроса в функцию http_middleware.v1:


local metrics = require('metrics')local http_middleware = metrics.http_middlewarehttp_middleware.build_default_collector('summary', 'http_latency')local route = { path = '/path', method = 'POST' }local handler = function() ... endhttpd:route(route, http_middleware.v1(handler))

Для хранения метрик можно использовать коллекторы histogram и summary.


Чтобы добавить HTTP-метрики для маршрутов роутера при использовании пакета http 2.x.x, необходимо воспользоваться следующим подходом:


local metrics = require('metrics')local http_middleware = metrics.http_middlewarehttp_middleware.build_default_collector('histogram', 'http_latency')router:use(http_middleware.v2(), { name = 'latency_instrumentation' })

Рекомендуется использовать один и тот же коллектор для хранения всей информации об обработке HTTP-запросов (например, выставив в начале коллектор по умолчанию функцией build_default_collector или set_default_collector). Прочитать больше о возможностях http_middleware можно в документации.


Глобальные лейблы


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


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


local metrics = require('metrics')local global_labels = {}-- постоянное значениеglobal_labels.app = 'MyTarantoolApp'-- переменные конфигурации кластера (http://personeltest.ru/aways/www.tarantool.io/ru/doc/latest/book/cartridge/cartridge_api/modules/cartridge.argparse/)local argparse = require('cartridge.argparse')local params, err = argparse.parse()assert(params, err)global_labels.alias = params.alias-- переменные окружения процессаlocal host = os.getenv('HOST')assert(host)global_labels.host = hostmetrics.set_global_labels(global_labels)

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


Роль cartridge.roles.metrics по умолчанию выставляет alias процесса Tarantool в качестве глобального лейбла.


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


Мониторинг внешних параметров


Tarantool не только позволяет организовывать сбор внутренних метрик о работе приложения, но и способен выступать как агент мониторинга внешних параметров. Подмодуль psutils пакета metrics является примером реализации такого поведения.


С помощью psutils можно настроить сбор метрик об использовании CPU процессами Tarantool. Его информация основывается на данных /proc/stat и /proc/self/task. Подключить сбор метрик можно с помощью следующего кода:


local metrics = require('metrics')metrics.register_callback(function()    local cpu_metrics = require('metrics.psutils.cpu')    cpu_metrics.update()end)

Возможность писать код на Lua делает Tarantool гибким инструментом, позволяющим обходить различные препятствия. Например, psutils возник из необходимости следить за использованием CPU вопреки отказу администраторов со стороны заказчика "подружить" в правах файлы /proc/* процессов Tarantool и плагин inputs.procstat Telegraf, который использовался на местных машинах в качестве основного агента.


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


Визуализация метрик


Пример из tarantool/grafana-dashboard


Хранение метрик в Prometheus


Настройка пути для экспорта метрик Tarantool в формате Prometheus описана в пункте "Плагины для экспорта метрик". Ответ запроса по такому маршруту выглядит следующим образом:


...# HELP tnt_stats_op_total Total amount of operations# TYPE tnt_stats_op_total gaugetnt_stats_op_total{alias="tnt_router",operation="replace"} 1tnt_stats_op_total{alias="tnt_router",operation="select"} 57tnt_stats_op_total{alias="tnt_router",operation="update"} 43tnt_stats_op_total{alias="tnt_router",operation="insert"} 40tnt_stats_op_total{alias="tnt_router",operation="call"} 4...

Чтобы настроить сбор метрик в Prometheus, необходимо добавить элемент в массив scrape_configs. Этот элемент должен содержать поле static_configs с перечисленными в targets URI всех интересующих процессов Tarantool и поле metrics_path, в котором указан путь для экспорта метрик Tarantool в формате Prometheus.


scrape_configs:  - job_name: "tarantool_app"    static_configs:      - targets:         - "tarantool_app:8081"        - "tarantool_app:8082"        - "tarantool_app:8083"        - "tarantool_app:8084"        - "tarantool_app:8085"    metrics_path: "/metrics/prometheus"

В дальнейшем найти метрики в Grafana вы сможете, указав в качестве job соответствующий job_name из конфигурации.


Пример готового docker-кластера Tarantool App + Prometheus + Grafana можно найти в репозитории tarantool/grafana-dashboard.


Хранение метрик в InfluxDB


Чтобы организовать хранение метрик Tarantool в InfluxDB, необходимо воспользоваться стеком Telegraf + InfluxDB и настроить на процессах Tarantool экспорт метрик в формате json (см. пункт "Плагины для экспорта метрик"). Ответ формируется следующим образом:


{    ...    {        "label_pairs": {            "operation": "select",            "alias": "tnt_router"        },        "timestamp": 1606202705935266,        "metric_name": "tnt_stats_op_total",        "value": 57    },    {        "label_pairs": {            "operation": "update",            "alias": "tnt_router"        },        "timestamp": 1606202705935266,        "metric_name": "tnt_stats_op_total",        "value": 43    },    ...}

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


[[inputs.http]]    urls = [        "http://tarantool_app:8081/metrics/json",        "http://tarantool_app:8082/metrics/json",        "http://tarantool_app:8083/metrics/json",        "http://tarantool_app:8084/metrics/json",        "http://tarantool_app:8085/metrics/json"    ]    timeout = "30s"    tag_keys = [        "metric_name",        "label_pairs_alias",        "label_pairs_quantile",        "label_pairs_path",        "label_pairs_method",        "label_pairs_status",        "label_pairs_operation"    ]    insecure_skip_verify = true    interval = "10s"    data_format = "json"    name_prefix = "tarantool_app_"    fieldpass = ["value"]

Список urls должен содержать URL всех интересующих процессов Tarantool, настроенные для экспорта метрик в формате json. Обратите внимание, что лейблы метрик попадают в Telegraf и, соответственно, InfluxDB как теги, название которых состоит из префикса label_pairs_ и названия лейбла. Таким образом, если ваша метрика имеет лейбл с ключом mylbl, то для работы с ним в Telegraf и InfluxDB необходимо указать в пункте tag_keys соответствующего раздела [[inputs.http]] конфигурации Telegraf значение ключа label_pairs_mylbl, и при запросах в InfluxDB ставить условия на значения лейбла, обращаясь к тегу с ключом label_pairs_mylbl.


В дальнейшем найти метрики в Grafana вы сможете, указав measurement в формате <name_prefix>http (например, для указанной выше конфигурации значение measurement tarantool_app_http).


Пример готового docker-кластера Tarantool App + Telegraf + InfluxDB + Grafana можно найти в репозитории tarantool/grafana-dashboard.


Стандартный дашборд Grafana


Для визуализации метрик Tarantool с помощью Grafana на Official & community built dashboards опубликованы стандартные дашборды. Шаблон состоит из панелей для мониторинга HTTP, памяти для хранения данных вместе с индексами и операций над спейсами Tarantool. Версию для использования с Prometheus можно найти здесь, а для InfluxDB здесь. Версия для Prometheus также содержит набор панелей для мониторинга состояния кластера, агрегированной нагрузки и потребления памяти.



Чтобы импортировать шаблон дашборды, достаточно вставить необходимый id или ссылку в меню Import на сервере Grafana. Для завершения процесса импорта необходимо задать переменные, определяющие место хранения метрик Tarantool в соответствующей базе данных.


Генерация дашбордов Grafana с grafonnet


Стандартные дашборды Grafana были созданы с помощью инструмента под названием grafonnet. Что это за заморский зверь и как мы к нему пришли?


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


Одним из первых способов решить большинство возникающих проблем было использование механизма динамических переменных (Variables) в Grafana. Например, он позволяет объединить дашборды с метриками из разных зон в одну с удобным переключателем. К сожалению, мы слишком быстро столкнулись с проблемой: использование механизма оповещений (Alert) не совместимо с запросами, использующими динамические переменные.


Любой дашборд в Grafana по сути представляет собой некоторый json. Более того, платформа позволяет без каких-либо затруднений экспортировать в таком формате существующие дашборды. Работать с ним в ручном режиме несколько затруднительно: размер даже небольшого дашборда составляет несколько тысяч строк. Первым способом решения проблемы был скрипт на Python, который заменял необходимые поля в json, по сути превращая один готовый дашборд в другой. Когда разработка библиотеки скриптов пришла к задаче добавления и удаления конкретных панелей, мы начали осознавать, что пытаемся создать генератор дашбордов. И что эту задачу уже кто-то до нас решал.


В открытом доступе можно найти несколько проектов, посвящённых данной теме. К счастью или несчастью, проблема выбора решилась быстро: на контуре заказчика для хранения метрик мы безальтернативно пользовались InfluxDB, а поддержка запросов к InfluxDB хоть в какой-то форме присутствовала только в grafonnet.


grafonnet opensource-проект под эгидой Grafana, предназначенный для программной генерации дашбордов. Он основан на языке программирования jsonnet языке для генерации json. Сам grafonnet представляет собой набор шаблонов для примитивов Grafana (панели и запросы разных типов, различные переменные) с методами для объединения их в цельный дашборд.


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


Начав с форка проекта и сборки нашей дашборды на основе этого форка, впоследствии мы оформили несколько Pull Request-ов в grafonnet на основе наших изменений. Например, один из них добавил поддержку запросов в InfluxDB на основе визуального редактора.


Визуальный редактор запросов InfluxDB


Код наших стандартных дашбордов расположен в репозитории tarantool/grafana-dashboard. Здесь же находится готовый docker-кластер, состоящий из стеков Tarantool App + Telegraf + InfluxDB + Grafana, Tarantool App + Prometheus + Grafana. Его можно использовать для локальной отладки сбора и обработки метрик в вашем собственном приложении.


При желании из стандартных панелей можно собрать дашборд, расширенный панелями для отображения собственных метрик приложения.


На что смотреть?


В первую очередь, стоит следить за состоянием самих процессов Tarantool. Для этого подойдёт, например, стандартный up Prometheus. Можно соорудить простейший healthcheck самостоятельно:


httpd:route(    { path = '/health' },    function(req)        local body = { app = app, alias = alias, status = 'OK' }        local resp = req:render({ json = body })        resp.status = 200        return resp    end)

Рекомендации по мониторингу внешних параметров ничем принципиально не отличаются от ситуации любого другого приложения. Необходимо следить за потреблением памяти на хранение логов и служебных файлов на диске. Заметьте, что файлы с данными .snap и .xlog возникают даже при использовании движка memtx (в зависимости от настроек). При нормальной работе нагрузка на CPU не должна быть чересчур большой. Исключение составляет момент восстановления данных после рестарта процесса: построение индексов может загрузить все доступные потоки процессора на 100 % на несколько минут.


Потребление RAM удобно разделить на два пункта: Lua-память и память, потребляемая на хранение данных и индексов. Память, доступная для выполнения кода на Lua, имеет ограничение в 2 Gb на уровне Luajit. Обычно приближение метрики к этой границе сигнализирует о наличии какого-то серьёзного изъяна в коде. Более того, зачастую такие изъяны приводят к нелинейному росту используемой памяти, поэтому начинать волноваться стоит уже при переходе границы в 512 Mb на процесс. Например, при высокой нагрузке в наших приложениях показатели редко выходили за предел 200-300 Mb Lua-памяти.


При использовании движка memtx потреблением памяти в рамках заданного лимита memtx_memory (он же метрика quota_size) заведует slab-аллокатор. Процесс происходит двухуровнево: аллокатор выделяет в памяти ячейки, которые после занимают сами данные или индексы спейсов. Зарезервированная под занятые или ещё не занятые ячейки память отображена в quota_used, занятая на хранение данных и индексов arena_used (только данных items_used). Приближение к порогу arena_used_ratio или items_used_ratio свидетельствует об окончании свободных зарезервированных ячеек slab, приближение к порогу quota_used_ratio об окончании доступного места для резервирования ячеек. Таким образом, об окончании свободного места для хранения данных свидетельствует приближение к порогу одновременно метрик quota_used_ratio и arena_used_ratio. В качестве порога обычно используют значение 90 %. В редких случаях в логах могут появляться сообщения о невозможности выделить память под ячейки или данные даже тогда, когда quota_used_ratio, arena_used_ratio или items_used_ratio далеки от порогового значения. Это может сигнализировать о дефрагментации данных в RAM, неудачном выборе схем спейсов или неудачной конфигурации slab-аллокатора. В такой ситуации необходима консультация специалиста.


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


Заключение


Как этот материал, так и пакет metrics назвать "всеохватными" и "универсальными" на данный момент нельзя. Открытыми или находящимися на данный момент в разработке являются вопросы метрик репликации, мониторинга движка vinyl, метрики event loop и полная документация по уже существующим методам metrics.


Не стоит забывать о том, что metrics и grafana-dashboard являются opensource-разработками. Если при работе над своим проектом вы наткнулись на ситуацию, которая не покрывается текущими возможностями пакетов, не стесняйтесь внести предложение в Issues или поделиться вашим решением в Pull Requests.


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


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


Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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