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

Пайплайн

Тонкости настройки CICD как работает GitLab runner, когда использовать Docker-in-Docker и где пригодится Argo CD

21.01.2021 08:19:12 | Автор: admin


В конце прошлого года в Слёрме вышел видеокурс по CI/CD. Авторы курса инженер Southbridge Александр Швалов и старший системный инженер Tinkoff Тимофей Ларкин ответили на вопросы первых студентов.


В частности, обсудили:


  • Как работает GitLab runner: сколько задач берёт и сколько ресурсов потребляет, где его лучше размещать и как настроить шаринг между проектами?
  • Как настраиваются пайплайны для проектов в монорепозитории? А как в ситуации, когда для каждого микросервиса свой репозиторий?
  • Как бороться с тем, что во время сборки артефакта в Docker очень быстро забивается свободное место на диске?
  • Когда лучше использовать подход Docker-in-Docker?
  • Как организовать доставку и развёртывание сервисов в закрытые окружения заказчика?

Видео с ответами на вопросы смотрите на YouTube. Под катом текстовая версия разговора.


С версии 20.10 Docker Engine стал rootless. Раньше эта фича была экспериментальной, а теперь оказалась в проде. Изменится ли что-то с точки зрения безопасности и сборки Docker-образов без root-привилегий?


Тимофей Ларкин: Я не думаю, что это сильно на что-то повлияет. Возможно, они идут к тому, чтобы появился отдельный сборщик образов от Docker. Но пока мы используем Docker-in-Docker, и, скорее всего, этот Docker-in-Docker в режиме rootless просто не будет запускаться.


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


Александр Швалов: В документации Gitlab есть открытый баг (issue), мол, давайте включим режим rootless, но официальной поддержки пока нет. Для сборки поддерживается kaniko, и мы добавили пример с kaniko в наш курс.


Дайте пример реального размещения репозиториев с кодом, секретами и helm-чартами где всё должно лежать в жизни? Как выглядит по умолчанию шаблон? Боевой deployment.yml не должен быть в репозитории сервиса?


Тимофей Ларкин: Ответ на такие вопросы всегда it depends зависит от ситуации. Если это open source проект, то там может и не быть деплойментов, там может быть makefile, который покажет, как собрать артефакт, как из него собрать Docker-образ. Но это репозиторий на Github, в лучшем случае он через github actions делает регулярные билды и кладёт их на Docker Hub или другой репозиторий образов контейнеров. Оно и понятно: это просто open source проект, куда его деплоить.


Другое дело, если это проект, который вы деплоите на инфраструктуре: своей, облачной неважно. Например, это приложение, которое разрабатывается и используется у вас в компании. Действительно, довольно простой способ держать и код, и скрипты сборки артефактов, и какой-нибудь helm-чарт в одном репозитории. Разнести по разным папкам, а GitLab CI будет и собирать, и сохранять артефакты, и пушить изменения в Kubernetes.


Подход не очень масштабируемый: когда приложений много, становится тяжело отслеживать, что задеплоено в Kubernetes. Чтобы решить проблему, подключают сервисы вроде Argo CD, а код и описание конфигурации хранят в разных репозиториях. Деплоят через CI (push-модель) или через кубы в Argo.


Когда компания занимается заказной разработкой, всё ещё сложнее. Встаёт вопрос, как деплоить код на инфраструктуру заказчика, и готов ли он принять Continuous Deployment исполнителя или хочет больше контроля? Надо смотреть на специфику.


Александр Швалов: У каждой команды свои стандарты, некоторые сложились исторически, некоторые появились на основе шаблонов или документации. В нашем курсе по CI/CD есть примеры, они рабочие можете адаптировать под свой проект. Исходите из своих потребностей.


Есть ли краткий справочник по полям gitlab-ci файла?


Александр Швалов: Краткого справочника нет, если нужна конкретная фича, лучше идти в документацию и смотреть, что там есть. Ну а базовые принципы как из большого набора кирпичиков собрать свой gitlab-ci.yml вы можете почерпнуть из курса или документации. Если в курсе нет, в документации точно будет.


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


Как можно привязать Jira к GitLab?


Александр Швалов: У бесплатной версии GitLab есть интеграция с Jira, ищите в соответствующем разделе.


Тимофей Ларкин: Более того, мы работали одновременно с двумя issue-трекерами: все проекты вязались в Jira, но отдельные команды настраивали для своих репозиториев привязку к YouTrack. Это было не очень удобно, потому что надо было ходить по каждому репозиторию, который хочешь привязать. Но да, такая функциональность действительно есть даже в бесплатном GitLab.


Почему joba release триггерится при изменении тега, хотя в родительском пайплайне стоит only changes?


Александр Швалов: Я провёл небольшое исследование и выяснил: это не баг, это фича. В GitLab был заведён баг, и его закрыли с комментарием, что так задумано. Если вам нужно обойти это поведение, используйте один из двух вариантов, которые предлагает GitLab.


GitLab не обновляет статусы дочерних пайплайнов. Что с этим делать?


Александр Швалов: А вот это уже баг. Возможно, когда-нибудь починят.


Есть ли в GitLab профили переменных? Например, я хочу сделать переменную host, и чтобы она приезжала разная в зависимости от окружения. Есть ли какое-нибудь профилирование? Например, я не хочу называть переменную host_dev, host_prod и host_test, а хочу указать окружение, и оно определённый набор переменных вытащит? Можно ли такое сделать?


Тимофей Ларкин: С ходу на ум мало что приходит. Можно, наверное, какие-то env-файлы держать в репозитории и просто их сорсить в пайплайне.


Нормальная практика называть host_dev, host_prod, host_test и т. д?


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


Тимофей Ларкин: Я не сталкивался с таким наименованием переменных. Это кажется оправданным, только если один пайплайн должен работать одновременно с несколькими разными окружениями. Если просто хочется разного поведения пайплайна в зависимости от ветки или чего угодно ещё


У меня в пайплайне может быть стадия deploy и стадия release, и тогда эти переменные должны быть разные, иначе как?


Тимофей Ларкин: То есть сначала один job деплоит на stage, а потом следующий job деплоит на prod?


Нет, job, который на prod работает, он срабатывает, когда only text. Всё это описано в одном пайплайне.


Тимофей Ларкин: Я решал это ямловскими (YAML прим. редактора) якорями, у меня были очень однотипные jobы из трёх строчек. Или можно теми же extends, как в примере с Docker. А дальше в каждом job пишешь свой блок variables, поэтому основное тело jobа работает с одними и теми же скриптами, но в зависимости от значения переменной host или переменной environment, оно деплоит на разное окружение.


Мы не имеем разные переменные для разных jobов, мы используем одни и те же названия переменных, просто с разными значениями. Возможно, оправдано в репозиторий поместить какие-то скрипты, которые сами внутри себя это разруливают, чтобы не раздувать gitlab-ci.yml.


SSH executor создаётся по одному на сервер?


Тимофей Ларкин: Фактически да. Можно, разве что, поставить какой-нибудь tcp-балансировщик и рандомно попадать на тот или иной сервер.


Имеет смысл разделять раннеры, которые деплоят на test и staging, и раннеры, которые деплоят на prod?


Тимофей Ларкин: Наверное, можно. Действительно, запустить раннеры в разных сетевых сегментах, которые не могут общаться друг с другом. В моей практике это не было нужно. У нас была ориентированная на Kubernetes система, а если речь идёт про SSH на тот сервер, SSH на этот сервер наверное, это будет оправдано.


В Kubernetes вы, наверное, по разным namespace деплоили и всё?


Тимофей Ларкин: Да, но при этом все раннеры в одном и том же месте запускались. Был отдельный namespace для раннеров.


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


Тимофей Ларкин: Необязательно, это зависит от того, какой executor работает.


Александр Швалов: Есть параметр concurrency. Если раннер один и идёт долгий пайплайн, то получается, что остальные девелоперы сидят и курят бамбук мы такое проходили, и для обхода настраивали concurrency. В зависимости от ресурсов раннера: сколько jobов он потянет одновременно, можно настраивать.


Он под каждую jobу своё окружение создаст?


Тимофей Ларкин: Да, он либо свой инстанс в bash запустит, либо несколько SSH-подключений, либо несколько Docker-контейнеров или подов в Kubernetes.


Есть ли в Argo CD и других GitOps-инструментах возможность параметризации реакции на изменения? Например, обновлять prod окружение, только если мастер + тэг или если фича, то в dev окружении менять состояние/производить обновления?


Тимофей Ларкин: В вопросе есть очень распространённое заблуждение, я и сам на нём спотыкался. Надо запомнить: не бывает, чтобы у нас был тег на мастер-ветке. Тег никогда на ветке не бывает. Где-то в GitLab даже есть issue, который это очень подробно объясняет. Но это лирическое отступление.


В принципе, Argo CD может что-то похожее сделать. Но надо понимать, что он не совсем про это. Его основная и довольно простая функция это чтобы в таком-то месте (namespace или кластере Kubernetes) была задеплоена такая-то ветка, такой-то тег определённого репозитория.


Как мне показалось, в вопросе речь была о пайплайнах и CI/CD. Но это не основная функциональность Argo, и кажется, он такого не поддерживает. Можно посмотреть на другие GitOps-инструменты. По-моему, у werf от Фланта есть функционал отслеживания что там меняется в Docker-репозитории. Но в целом GitOps это не совсем про это. Вот как в гите опишите, то и будет задеплоено.


На коммит Argo увидит: О! Что-то поменялось, значит надо поменять это и в Kubernetes, но без какой-то сильно ветвистой логики.


Александр Швалов: Я добавлю, что тэг это не ветка, а по смыслу ближе к коммиту. Тут поможет семантическое версионирование, и можно настраивать шаблоны для Argo CD. Если для продакшена, то конкретный релиз: 1.2.0, 1.2.1. Для stage будет 1.2. любая циферка в конце приедет на stage. Для QA это 1. всё остальное приедет. Для совсем свежего, для локальной разработки просто звёздочка *, любой тег Argo CD будет сразу подтягивать.


Какой сканер посоветуете для Docker-образов? Мне trivy понравился, но может что удобнее есть?


Александр Швалов: Я использовал Trivy, нареканий не было.


Как настраиваются пайплайны для проектов в монорепе, и как в ситуации, когда для каждого микросервиса свой репозиторий?


Александр Швалов: Возможно, мы добавим такой пример в курс. Спасибо за запрос! Вообще, ходят слухи, что у Google и Microsoft всё хранилось или хранится в монорепах миллиарды строк кода. Здесь многое зависит от человеческого фактора, а не от инструментов. Если говорить о GitLab CI/CD, то всё просто: в шаге сборки какой-то части, например, фронтенда используем only changes, выбираем каталог и дальше поехали. Что-то изменилось, GitLab производит деплой. С тестами будет посложнее, потому что фронтенд (или какая-то часть, особенно, если это микросервисы) запустится и будет неполноценным. Поэтому для тестов придётся поднимать минимальные зависимости.


Тимофей Ларкин: Я не видел open source систем контроля версий, которые поддерживают такую модель работы, как монорепозиторий. У больших игроков свои системы, и разработчики даже рассказывают: Вот, я работал в Google/Amazon/Facebook, а потом я ушёл оттуда, пошёл в среднего размера компанию, и я не знаю, что делать. Нигде нет магии этих больших систем, которые сами решают все проблемы с версионированием кода. Внезапно я должен работать с тем же GitLab.


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


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


Ну и как всегда: если хотите оставаться в парадигме один репозиторий один микросервис, тогда где-то храните метаданные (какой хеш коммита соответствует какому тегу, какому релизу) и с помощью Argo оркестрируйте всё это.


Какие существуют best practices по размещению раннеров? Как лучше организовать размещение нескольких раннеров на одной машине, чтобы можно было добавлять новые? Стоит ли размещать раннеры в Docker-контейнерах, или лучше использовать виртуальные машины, например, kvm?


Александр Швалов: GitLab для раннеров по запросу предлагает использовать Docker Machine, и соответственно использует драйверы оттуда это всяческие облака и виртуализации (AWS, Azure, VirtualBox, Hyper V, vmWare). KVM в списке нет. Для множественных раннеров можно настраивать также шареный кэш. Например, в AWS S3 хранилище.


Однако этот подход через Docker Machine сам GitLab считает малость устаревшим. Есть открытый баг, где разработчики размышляют, куда лучше перейти, какие-то варианты есть. Самый очевидный перейти в Kubernetes. Ну а best practice в общем не размещать раннер на одном хосте с GitLab, чтобы они друг друга не аффектили. Ну и на продакшене раннер тоже лучше не размещать, потому что вдруг туда что-то прилетит критичное.


Для небольших проектов достаточно одного раннера (на своих проектах я так и делал). Просто настроить ему concurrency, чтобы разработчики не стояли в очереди друг за другом, и дать какое-то количество процессора и памяти, настроить опытным путём и так жить.


Тимофей Ларкин: Когда мы пайплайны гоняли в Kubernetes, то мы просто на несколько хостов вешали taint с эффектом PreferNoSchedule, чтобы пользовательские нагрузки приложения запускались преимущественно где-то на других хостах. Но при этом на раннеры мы вешали nodeSelector как раз на эти же самые хосты. Это к вопросу о разделении нагрузки от приложения и нагрузки от раннера.


Да, есть беда, что у сборок совершенно другой профиль нагрузки, чем у обычных приложений. Как правило, сборка это хорошая нагрузка на CPU (обычно именно на CPU, а не на память). Затем идёт сборка Docker-образа, (если мы более-менее всё аккуратно делаем: сначала собрали артефакт, затем его как артефакт передали в следующий этап пайплайна, а затем в Docker-образ кладём готовые бинарники), то идёт нагрузка на диск, потому что там все эти слои формируются, пишутся и, наконец, после этого мы загружаем сеть, перекачивая собранный Docker-образ в наш registry.


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


С Kubernetes всё понятно. Мне даже хотелось убрать раннеры из Kubernetes и где-то отжать два хоста, которые использовать как build-серверы, чтобы совсем всё отдельно было. Всякие деплои, понятно, это очень легковесная задача. Запушить ямлик в Kubernetes никаких ресурсов не требует. Ну а если у нас SSH или shell-раннер, то тогда сама ситуация диктует, где их размещать. А если вопрос про бинарь GitLab Runner, то он очень мало ресурсов потребляет, его можно где угодно расположить. Тут больше зависит от требований сетевой доступности.


Когда лучше использовать подход Docker-in-Docker? Какие еще есть инфраструктурные идиомы, связанные с GitLab?


Александр Швалов: Скажу очевидную вещь: использовать Docker-in-Docker стоит, когда у вас сам раннер запущен в Docker. Ну а если у вас нужно запускать какие-то команды в Docker как это задумывалось вообще: если раннер запущен в Docker, то вы можете просто в Docker-in-Docker брать другой Docker-образ (Python, например, и в нём выполнять какие-то действия из кода).


Тимофей Ларкин: Я буду чуть более категоричен. Docker-in-Docker стоит использовать почти никогда. Бывают случаи, когда мне надо собрать кастомный образ kaniko, но когда я пытаюсь собрать его через kaniko, то всё уходит в бесконечную рекурсию и падает (есть такие интересные особенности). Тогда приходится использовать Docker-in-Docker. Кроме того, Docker-in-Docker можно использовать на какой-нибудь виртуалке, которой мы сделали хорошую изоляцию ото всего, чтобы там вообще нельзя было дотянуться ни до инфраструктуры, ни до ещё чего-то, чтобы там можно было только Docker-образы собирать.


В остальных ситуациях Docker-in-Docker это огромная зияющая дыра в безопасности. Очень легко использовать: у тебя есть root-права, у тебя есть привилегии, ты можешь монтировать хостовую файловую систему. Накатал Dockerfile, в котором первым шагом устанавливаешь SSH-демон, прокидываешь туннель туда куда надо, потом заходишь на эту машину и с root-правами монтируешь на эту машину dev/sda1 и всё, у тебя доступ к хосту, ты делаешь что хочешь.


Александр Швалов: Лучше посмотреть в сторону новомодных Podman, Buildah и kaniko. Совсем недавно были новости, что Kubernetes хочет отказаться от Docker все схватились за голову, но это в принципе ожидаемо. И сам Docker (мы с этого начали) уже выкатил rootless mode. Поэтому всеми силами стоит уходить от выполнения от root.


Как можно бороться с тем, что когда происходит сборка артефакта в Docker, очень быстро забивается свободное место на диске (ну кроме docker prune -a)?


Александр Швалов: Только одно решение выделить больше диска, чтобы хватало этого запаса на тот период, когда у вас срабатывает по расписанию сборка мусора. Либо использовать одноразовые раннеры где-то в облаках.


Тимофей Ларкин: Регулярно подчищать за собой: docker prune -a. Совершенно точно плохая практика использовать хостовый Docker-демон для этих сборок. Потому что доступ к хостовому демону это огромная дыра в безопасности, мы можем делать всё что угодно на хосте от имени рута. Ну и плюс, если мы для сборки используем хостовый Docker-демон, то он моментально забивается всяким мусором.


Допустим, даже не используя никакой хостовый Docker-демон, даже имея политику подчистки Docker-образов в GitLab registry, когда мы только стартовали, у нас был раздел под GitLab на 250 Гб. Потом мы стали упираться, сделали отдельный раздел под GitLab на 250 Гб, а под GitLab registry ещё один на 250 Гб. У нас GitLab Omnibus подключал два persistent volume одновременно. Потом раздел под registry разросся до 500 Гб, сейчас он, кажется, 750 Гб и надо узнать у бывших коллег, что у них там происходит хватает места или надо ещё что-то придумывать. И это при том, что есть политика удаления всех, кроме последних пяти тегов какого-то образа. И это без всяких артефактов сборок, это просто конечные образы, которые дальше запускаются на каких-то окружениях.


Как организовать мирроринг стороннего репозитория (например, из GitHub) в GitLab средствами самого GitLab? То есть чтобы автоматически в GitLab подтягивались все изменения, обновления из стороннего репозитория, новые теги и т. д. Без необходимости периодически делать pull руками, без использования скриптов или сторонних решений для автоматизации этого процесса. Если нельзя обойтись без сторонних решений, то какое из них вы бы порекомендовали?


Александр Швалов: Сразу скажу, что полная поддержка этой функциональности есть в платной версии Starter. Для ускорения автоматики можно дополнительно использовать вебхуки в GitHub, чтобы он при каждом чихе тыкал палочкой в GitLab и GitLab в ответ на это делал pull из GitHub. Если надо обойтись исключительно бесплатной версией, то мне не приходилось этим заниматься, скорее всего, придётся использовать дополнительные сторонние скрипты. Сходу можно порекомендовать настроить для этого CI/CD пайплайн: грубо говоря, можно делать операции с гитом на уровне раннера и запускать это всё по расписанию. Но это, конечно, костыль.


Тимофей Ларкин: Я бы не брал этот подход. Это очень способствует плохим практикам. Чаще всего проблемы возникали, когда мы работали с внешними подрядчиками, которые упорно не хотели хранить свой код в нашем GitLab, а хранили его в своём. Поскольку это большая корпорация, собственные TLS-сертификаты самоподписные и так далее, то подрядчик не знает, как их себе в системное хранилище добавить, или ещё какая-то беда в результате всегда было довольно тяжело получить от подрядчика не просто артефакт, а код. Потому что а зачем? а мы попытаемся помиррорить! не работает ладно, тогда будем на своём GitLab работать. Возможно, есть ситуации, когда это важная и нужная функциональность, но частенько это абьюзится.


Какой аппаратный ресурс наиболее востребован для инстанса GitLab в docker-контейнере: процессор, оперативная память или хранилище? А для раннеров?


В случае, если есть только один мощный сервер с мощным процессором, большим объемом оперативной памяти и большим хранилищем и еще один-два сервера меньшей мощности с процессорами послабее, как наиболее оптимально задействовать первый сервер для развертывания GitLab-инфраструктуры
(то есть самого GitLab и раннеров) и что лучше перенести на сервера меньшей мощности? Как целесообразно в этом случае размещать раннеры: в Docker-контейнерах на хосте или в виртуальных машинах (например, kvm)?
Ориентировочная нагрузка на инстанс GitLab: 100 пользователей, 200 проектов.


Александр Швалов: Как адепт классических решений, я бы предложил KVM как более проверенное решение. Docker-контейнеры для меня это до сих пор что-то эфемерное: сейчас запустил, через 15 минут можно грохнуть. GitLab же должен работать и работать, там вы храните свою конфигурацию. Зачем его поднимать, гасить?


Требования по железу есть у самого GitLab. Для 100 пользователей нужно 2 ядра (хватит до 500 юзеров) и 4 Гб памяти (до 100 юзеров). При расчёте объема диска лучше исходить из простой математики: объём всех репозиториев, которые есть, умножить на 2. И не забыть продумать, как вы будете добавлять к серверу новые диски, если репозитории разрастутся.


Железные требования для раннеров предсказать сложно. Зависит от проектов, что вы там собираете: html-страницы или java-код. Надо взять изначальные требования к сборке и от них отталкиваться. Возможно, стоит взять что-то виртуальное, докинуть ресурсов и настраивать по необходимости.


Тимофей Ларкин: Увидев этот вопрос, я специально попросил у коллег графики по потреблению GitLab. Там всё не так весело. Их инстанс GitLab так-то на 500 пользователей, но реально что-то разрабатывают не более 200 человек. Там безупречно ровная полка ну как, колеблется от 1,5 до 2 ядер на протяжении нескольких дней, возможно, по ночам чутка потише. Полка по памяти в районе 50 Гб тоже довольно стабильно.


То есть возвращаясь к рекомендациям: по ядрам, наверное, они реальны, а по памяти занижены. 4 Гб хватит только для запуска, а для активной работы понадобится гораздо больше. И это даже вместе с тем, что базы данных PostgreSQL, которые под этим GitLab, они сейчас живут на отдельных хостах. Раньше, по моим наблюдениям, процессор загружался гораздо сильнее.


Независимо от способа деплоя я бы запустил GitLab на жирном хосте: нам важна надёжность GitLab, многие его компоненты достаточно прожорливы. На других хостах поменьше, наверное, можно было бы гонять Docker executor.


Интересует деплой не в Kubernetes. Допустим, по SSH или же docker\docker-compose.


Александр Швалов: Да, это популярный запрос. Мы планируем добавить это в наш курс (на момент публикации статьи уже добавили прим. редактора) деплой в простой Docker. Всё делается очень просто: раннер с предварительно настроенными ключами заходит по SSH на хост, делает там docker stop, docker rm (удаляет старый контейнер) и docker run с прямым указанием на конкретный образ, который мы только что собрали. В результате поднимается новый образ.


Голый Docker это не оркестратор, и репликации там нет, поэтому при таком CI/CD у вас будет перерыв в обслуживании. Если у вас нет образа контейнера, в моём примере лучше его запустить самостоятельно.


Тимофей Ларкин: Если интересует совсем голый SSH, то пишите скрипты и запускайте. Можем, наверное, минимальный пример в курс добавить. Но надо понимать, что Kubernetes уйму проблем с оркестрацией решает, ну и Docker тоже достаточно можно решает (перезапуски, healthcheck, что угодно).


Если я был вынужден описывать голый SSH, наверное, я бы запускал что-нибудь через systemd. Да, можно Ansible использовать, но опять же, через тот же systemd.


Александр Швалов:Если ещё нет образа контейнера на хосте (я вспомнил, как это у меня делалось), там тоже через Bash проверяется, есть что-нибудь или нет. Если нет, то делаем docker run без всего; docker run, и конкретный образ из registry, который только что создан. Если что-то есть, то сначала останавливаем это всё, и после этого docker run.


Можно ли контейнер с раннером создавать динамически (только на момент сборки)?


Александр Швалов: Да. Очень популярно брать дешёвые инстансы AWS и запускать раннеры там, а потом их глушить по прошествии какого-то времени. Пошла активная сборка, пошёл деплой, насоздавались раннеры и через какое-то время, когда нагрузки нет, они сами по себе схлопнутся. Это всё реализуется через Docker compose.


Тимофей Ларкин: Мы говорим про GitLab runner, который управляющий бинарник, или мы про сами пайплайны? Ну да, пайплайны, наверное. А сам управляющий бинарь? Тогда что будет триггерить создание этого самого бинаря? Опять возникают проблемы курицы и яйца.


Александр Швалов: В Kubernetes, насколько я знаю, можно через какие-то метрики, когда нагрузка есть, он создаёт Так же для OpenShift я нашёл, есть оператор, который управляет раннерами. Как-то можно автоматизировать, люди движутся в этом направлении. Но, как правило, на простых проектах, если что-то нужно, мы берём и виртуалке добавляем ресурсов, а когда проходит час пик убираем ресурсы.


Тимофей Ларкин: Автоскейлинг нод можно делать. Потому что так-то Docker-контейнеры с пайплайнами создаются автоматически только на время существования пайплайна по дефолту. Управляющий бинарь должен существовать по дефолту. Иначе как кто-то узнает, что надо создавать управляющий бинарь?


Как можно настроить шаринг раннера только между определённым количеством проектов?


Александр Швалов: Для этого в GitLab есть группы, создаёте группу, привязываете раннер и в эту группу добавляете проекты. Доступ юзеров, соответственно, распределяется. Всё просто!


Тимофей Ларкин: Ссылка на issue, где описывается, как это делать. Необязательно даже, чтобы это был раннер на группу. Можно делать раннер на конкретный список репозиториев. Первый создаётся через регистрационный токен на какой-то конкретный репозиторий, но потом, через UI GitLab можно добавить его ещё нескольким. Можно ещё тегами всё это разрулить.


Прошу рассказать, если есть опыт, о практике организации доставки и развертывания сервисов в закрытые окружения заказчика, когда нет возможности прорубить доступ до внутренних репозиториях заказчика. Как при этом упростить доставку артефактов и по максимуму автоматизировать развертывание в условиях, когда Git находится далеко снаружи runtime-окружения?


Александр Швалов: У меня, к сожалению, не было такого опыта. Я знаю, что в серьезных организациях такое сплошь и рядом практикуется. Я могу лишь придумать такой способ: взять артефакт, сделать архив с релизной веткой репозитория, принести на флешке, там есть внутренний GitLab, сделать push в нужную ветку и сделать CI/CD как обычно, только в локальной сети.


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


Раннер умеет работать за NAT, умеет постучаться во внешний GitLab. Главное, чтобы сам GitLab не был за NAT, чтобы была нормальная доступность до GitLab. Поэтому да, раннер может изнутри контура заказчика сходить в ваш GitLab, стянуть код и делать сборку уже внутри инфраструктуры заказчика. И тогда чуть легче: артефакт сборки кладётся во внутренний репозиторий заказчика и оттуда уже деплоится всё хорошо. Не исключено, что там будет много сложностей. Наверняка, у заказчика свои самоподписные TLS-сертификаты, у него интернет недоступен на большинстве хостов (надо будет согласовать proxy, которая позволит раннеру ходить до вашего GitLab) и так далее.


Александр Швалов: Если proxy, NAT недопустимы, то в таком варианте остаётся паковать всё на своей стороне, собирать в инсталлятор, приходить к заказчику и обновлять приложение инсталлятором. Это уже другая задача, к CI/CD она вряд ли относится. Хотя можно настроить CI/CD, чтобы на выходе получался инсталлятор.


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


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


Может нам как-то помочь CI/CD GitLab, если поставщик сам присылает собранные бинари в zip-архиве, и эти бинари необходимо распределить на нужное количество нод? Где это будет работать?


Александр Швалов:Речь о том, что есть в качестве исходного кода бинари в zip-архиве, и GitLab CI будет их каким-то образом распределять? В принципе, такое возможно. Почему нет? Можно это как-то сканировать, тестировать и деплоить, просто по SSH закидывать. В принципе, можно обойтись и без GitLab, одними скриптами.


Тимофей Ларкин: Можно какую-нибудь регулярную jobу запилить, которая, допустим, смотрит на папку, проверяет сумму у zip-архива, если обновилась, распаковывает, раскладывает его на внутренние nexus (приватный docker registry прим. редактора) в виде артефактов. Если надо, деплоит. Да, я думаю, GitLab может помочь в плане автоматизации этого процесса.


Узнать больше о курсе по CI/CD

Подробнее..

От эскиза до релиза пайплайн регулярного создания контента на примере идеи для оружия от игрока

07.04.2021 20:14:16 | Автор: admin

Огромное количество игр построено на сервисной поддержке, будь то тактический шутер Rainbow Six Siege или большая ролевая World of Warcraft. Игроков постоянно вовлекают ивентами, игровыми режимами, картами, персонажами или перками. Но когда в проекте уже сотни и тысячи единиц контента, а релизы ежемесячно это может стать проблемой для разработчиков.

В какой-то момент мы тоже с этим столкнулись в мобильном PvP-шутере и пришлось искать решение. В итоге полностью пересмотрели пайплайн создания контента и оптимизировали процессы так, чтобы одна гугл-таблица заменила нам таск-трекеры. О нашем опыте на проекте со 170 млн инсталлов и 1 млн DAU подробно расскажу под катом.

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

Весь пайплайн разработки контента делится на несколько этапов:

  1. Разработка идеи. Ищем, отсеиваем и собираем лучшие идеи контента.

  2. Создание 2D-концепта. Рисуем эскизы и концепты.

  3. Создание 3D-концепта. Превращаем концепты в 3D-модели.

  4. Оптимизация 3D-модели. Оптимизируем и готовим к анимации.

  5. Анимирование. Делаем риггинг и оживляем контент.

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

  7. Тестирование. Проводим плейтесты, даем фидбек и устраняем баги.

  8. Релиз и сбор статистики. Выпускаем в сторы, работаем с аудиторией, метриками и, если остались, отлавливаем баги.

  9. Ревью контента. Следим за игровым балансом и проверяем параметры.

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

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

Сейчас над нашим мобильным шутером Pixel Gun 3D работают более 80 человек. У проекта свыше тысячи единиц контента, сотня карт, десяток онлайн-режимов (включая батлрояль на двух огромных картах) и даже сюжетная кампания. Все разберем на его примерах и кейсах.

1. Разработка идеи

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

В любых играх-сервисах к каждому обновлению игры принято готовить цикличный пак контента (сейчас это называют сезоном). Он состоит из тематики, сюжета, батлпасса, пушек, скинов, карт и много чего еще. Например, недавно Activision выпустила большой апдейт Call of Duty Black Ops: Cold War, в котором главной темой стал Зомби-ивент.

Зомби-режим Outbreak в Call of Duty Black Ops: Cold WarЗомби-режим Outbreak в Call of Duty Black Ops: Cold War

Сезоны в Pixel Gun 3D продуманы на 2-3 вперед. Составляется фиксированная сетка, где расписывается, какая тема за какой следует, и что будет внутри: механики, новые режимы, карты и прочее. Контент должен максимально отличаться от представленного в предыдущем сезоне так он намного привлекательнее для игроков.

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

  1. Анализ популярных трендов на рынке.

  2. Проведение арт-конкурсов среди комьюнити.

  3. Разработка уникального сеттинга для привлечения внимания.

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

Трендом может стать не только сеттинг или отдельно взятый праздник, но и игровой режим. Например, Imposter по мотивам Among Us. Про него выпустим отдельный материал расскажем, как добавляли режим, зачем вообще разработчикам тренды, откуда они берутся и как их применять в своем проекте.

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

Подобные активности проводятся у нас с 2014 года, когда в игре было порядка 30 пушек. За это время механику проведения конкурсов особо не меняли, поэтому опытные игроки готовятся к ним заранее, а потом отправляют практически готовое ТЗ с описанием механик, параметрами DPS и даже анимациями. Авторы потом получают свои пушки уже в игре вместе с призовой валютой.

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

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

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

Офис Lightmap в Ростове-на-ДонуОфис Lightmap в Ростове-на-ДонуКарта Офис в Pixel Gun 3DКарта Офис в Pixel Gun 3D

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

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

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

Скриншот концепт-докаСкриншот концепт-дока

Затем документ передается в концепт-отдел, где стартует реализация.

2. Создание 2D-концепта

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

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

Эскизы от руки практически не рисуются, обычно только, когда нужно придумать что-то совсем нестандартное. Из-за особенностей пиксельной стилистики художникам удобнее работать сразу в Photoshop: открыть картинку 100100 и прямо в ней попиксельно рисовать.

Создание 2D-концепта в PhotoshopСоздание 2D-концепта в Photoshop

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

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

Кроме того, при концептировании художники сразу прикидывают уникальные моменты по анимированию. Если нужно что-то быстро переделать или заменить, 2D-художники примеряют на себя роль специалистов по 3D, что помогает параллельно прокачивать скиллы. Те, кто не знает анимацию потихоньку учат ее основы; кто не работал с текстурами пробует текстурировать, и так далее. Разумеется, у каждого своя основная специализация, но при этом любой умеет выдавить, затекстурировать и анимировать, например, оружие. Так не только интереснее работать, но и позволяет специалистам развиваться в разных направлениях.

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

Но вернемся к 2D-концептам. Когда художники заканчивают работу, то на выходе получается примерно такое полотно:

Полотно концептовПолотно концептов

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

Эскиз (слева) и готовый 2D-концепт (справа)Эскиз (слева) и готовый 2D-концепт (справа)

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

  • Настраивается плотность пикселей чтобы концепт не был слишком детальным или, наоборот, грубым.

  • Дорабатывается цвет чтобы не было плавных градиентов (из-за специфики графики используется по 3-4 оттенка на 3-4 цвета для одной пушки).

  • Подгоняется по цветовой палитре чтобы ничего не выбивалось (результат сравнивается с рефами по визуалу и будущим эффектам).

Когда модель откалибрована создается подробное ТЗ. Оно, как правило, включает сюжет сезона, связь с предыдущим, лор и описание контента (2D-концепт, механики, свойства, референсы по визуальному стилю и анимациям).

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

Пример ТЗ на оружие:

Название, класс, тег: Maximum Cruelty (Максимальная Жестокость), Heavy, Weapon1282.
Механика: Заряжаем выстрел, создавая шар перед дулом (пушка раскрывается). После этого происходит мгновенный взрыв в точке прицела. Убитые цели взрываются, нанося дополнительный урон, но уже в меньше области (возможна цепная реакция).
Свойства: Charge Shot / Area Damage / Targets Explode.
Оформление: Нижний подвес (держим, как миниган), энергия внутри пушки постоянно светится (ставим дополнительный материал для свечения в темноте). При перезарядке энергия тухнет до установки батареи.

Референсы к ТЗ на оружиеРеференсы к ТЗ на оружие

Концепты выбраны, документ составлен, вижен синхронизирован. Значит, можно переходить к созданию 3D-модели.

3. Создание 3D-концепта

Утвержденные 2D-концепты попадают в руки 3D-отдела.

Раньше мы использовали 3ds Max, но с ним было много проблем, особенно у новичков. Очень утрированный пример: делали модель не 50 пикселей, а 48 с половиной потом текстура в 50 пикселей просто не ложилась как надо.

Сейчас перешли на два воксельных редактора: MagicaVoxel и Qubicle (но к 3ds Max еще вернемся). Моделирование в них происходит не сложными формами, а вокселями (что идеально подходит для нашей стилистики). Можно быстро и удобно посмотреть модель в объеме с разных ракурсов и найти проблемные места. В итоге скорость значительно выросла теперь 3-4 модели проверяются всего за час.

Воксельные редакторы сразу дают правильную фигуру на выходе получается 3D-модель с текстурой (ее потом проще оптимизировать и запечь).

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

Выдавленный 2D-концептВыдавленный 2D-концепт

Не всегда все идет гладко. Иногда пушка в 2D-концепте выглядит круто, а в 3D вообще не цепляет. Именно воксельный редактор помогает ускорить процесс и понять, что делать с моделью: либо довести до ума, либо вернуть на этап 2D-концепта.

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

Модель оружия Метатель Топоров и рефыМодель оружия Метатель Топоров и рефы

В итоге сделали вместо неё Мьёльнир с совершенно другой механикой и получили совсем другие эмоции от игры.

Модель оружия Молот Тора и рефыМодель оружия Молот Тора и рефы

4. Оптимизация 3D-модели

От 3ds Max, конечно, полностью не отказались. Его вместе с Blender и Maya используем дальше для оптимизации и анимации. На этом этапе максимально упрощаем оружие, скашиваем углы и правим ту геометрию, которую не позволял менять воксельный редактор.

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

Проверка на габариты с персонажемПроверка на габариты с персонажем

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

Чтобы этого не происходило, есть ограничения. Например, для оружия рекомендовано 1000 полигонов. В редких случаях на крутую пушку можем выделить 1500-2000 полигонов.

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

Оптимизированная модельОптимизированная модель

После запекается текстура. Она должна быть правильной квадратной формы, и чем меньше, тем лучше. Здесь пиксельная стилистика работает в плюс можно сделать небольшую текстуру 6464 или 128128 и вложить туда кучу информации.

Тот, кто занимается оптимизацией, заранее думает, какие объекты будут двигаться и что будет задействовано, чтобы впоследствии разделить эти объекты на отдельные меши и зариггать. Обязательно стараемся минимизировать количество костей (пушка, обойма, затвор, гильза и так далее) для аниматоров. У самых сложных пушек доходит до 20, но обычно укладываемся в 10 или меньше.

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

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

Часть таблицы с контент-планомЧасть таблицы с контент-планом

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

  • оранжевый контент в работе;

  • зеленый готов к тестированию;

  • синий протестирован и готов к релизу.

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

С организационным моментом разобрались, возвращаемся к разработке.

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

5. Анимирование

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

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

  • idle (когда персонаж стоит и ничего не делает);

  • стрельба;

  • перезарядка;

  • пустой магазин (стрельба без боеприпасов);

  • лоадинг (взятие пушки из арсенала);

  • анимация профайла (взаимодействие персонажа с пушкой в магазине: крутит, перезаряжает, полирует и прочее).

Анимация профайлаАнимация профайла

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


Жестких условий по анимации обычно нет, есть рефы и тематика (но бывает и строго прописанное ТЗ). Главное техническое условие в анимировании попасть в тайминги, указанные кор-отделом. Это скорость перезарядки, скорость стрельбы и прочие параметры. Иногда приходится возвращаться к кор-отделу, когда в тайминги не укладывается клевая анимация.

Анимация стрельбыАнимация стрельбыАнимация перезарядкиАнимация перезарядки

Если есть жесткие критерии, то они будут заранее указаны в ТЗ. Например, ракету взрывать с эффектом фаербола, из которого разлетаются улыбки. Такие детали прописаны давно, и когда они доходят до своих этапов и нужных специалистов переносятся в таблицу с контент-планом и берутся в работу.

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






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


В конце этого этапа получается готовый к добавлению в проект контент. У пушки есть основное анимированная 3D-модель. Теперь осталось навести дополнительную красоту.

6. Подключение к проекту

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

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

Когда все подключено, сообщает об этом геймдизайнеру по кору, FX-дизайнеру и саунд-дизайнеру. Дальше процесс такой:

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

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

  3. Дальше модель забирает FX-дизайнер и в Unity добавляет эффекты, партиклы взрывов, выстрелов, вылет гильз и остальную красоту.

  4. Под конец пушка возвращается к саунд-дизайнеру, который на основе настроенных механик и эффектов корректирует звук.

К концу этапа получается готовая единица контента она замоделена, анимирована, настроена, озвучена и готова к плейтестам.

7. Тестирование

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

Поэтому важно иметь запас на тестирование. Если нет времени или появились сложности, то в первую очередь нужно браться за то, с чем точно проблем не будет. Например, пока 3D-шники моделят карты, 2D-концептеры перерисовывают проблемные пушки и апдейтят ТЗ. За один день так можно переделать 10-20 вариантов, утвердить и заново отправить в пайплайн.

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

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

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

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

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

В целом, при таком формате стало меньше бюрократии и больше творчества. Поэтому 70-80% контента доходит до плейтеста уже в отличном качестве.

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

8. Релиз и сбор статистики

Работа над одной пушкой от идеи до финала занимает около месяца. Это не человекочасы, а длина полного цикла пайплайна. Тайминги такие:

  • 2 недели концептирование и моделирование;

  • 1 неделя фидбек и правки;

  • 1 неделя финальные тесты и полировка.

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

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

За этим в том числе следит отдел комьюнити. Собирает фидбеки, работает с аудиторией в отзывах, комментариях, комнатах в Discord. Обязательно нужно учитывать мнение аудитории и работать с лояльностью.

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

9. Ревью-контента

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

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

Вместо заключения

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

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

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

Подробнее..

Пайплайны и частичное применения функций, зачем это в Python

04.09.2020 14:06:16 | Автор: admin


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


Кратко о ФП в Python и почему не хватает пайплайнов на примере


В Python из базовых средств есть довольно удобные map(), reduce(), filter(), лямбда-функции, итераторы и генераторы. Малознакомым с этим всем советую данную статью. В целом это оно всё позволяет быстро и естественно описывать преобразования над списками, кортежами, и тд. Очень часто(у меня и знакомых питонистов) то, что получается однострочник по сути набор последовательных преобразований, фильтраций, например:
Kata с CodeWars: Найти


$\forall n \in [a,b] : n=\sum_0^{len(n)} n_i ^ i, \text{ } n_i\text{ - i-й разряд числа n}$


Задачка довольно простая, к сожалению(но к счастью для этого поста), решений лучше чем в лоб нет.


Моё решение:


def sum_dig_pow(a, b): # range(a, b + 1) will be studied by the function    powered_sum = lambda x: sum([v**(i+1) for i,v in enumerate(map(lambda x: int(x), list(str(x))))])    return [i for i in range(a,b+1) if powered_sum(i)==i]

С использованием средств ФП как есть получается скобочный ад "изнутри наружу". Это мог бы исправить пайплайн.


Пайплайны функций


Под сим я подразумеваю такое в идеальном случае (оператор "|" личное предпочтение):


# f3(f2(f1(x)))f1 | f2 | f3 >> xpipeline = f1 | f2 | f3 pipeline(x)pipeline2 = f4 | f5pipeline3 = pipeline | pipeline2 | f6...

Тогда powered_sum может стать(код не рабочий):


powered_sum = str | list | map(lambda x: int(x), *args) | enumerate | [v**(i+1) for i,v in *args] | sum

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


from copy import deepcopyclass CreatePipeline:    def __init__(self, data=None):        self.stack = []        if data is not None:            self.args = data    def __or__(self, f):        new = deepcopy(self)        new.stack.append(f)        return new    def __rshift__(self, v):        new = deepcopy(self)        new.args = v        return new    def call_logic(self, *args):        for f in self.stack:            if type(args) is tuple:                args = f(*args)            else:                args = f(args)        return args    def __call__(self, *args):        if 'args' in self.__dict__:            return self.call_logic(self.args)        else:            return self.call_logic(*args)

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


pipe = CreatePipeline()powered_sum = pipe | str | list | (lambda l: map(lambda x: int(x), l)) | enumerate | (lambda e: [v**(i+1) for i,v in e]) | sum

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


Частичное применение функций


Рассмотрим на примере простейшей функции(код не рабочий):


def f_partitial (x,y,z):    return x+y+zv = f_partial(1,2)# type(v) = что-нибудь частично применённая функция f_partial, оставшиеся аргументы: ['z']print(v(3))# Эквивалентprint(f_partial(1,2,3))

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


powered_sum = pipe | str | list | map(lambda x: int(x)) | enumerate | (lambda e: [v**(i+1) for i,v in e]) | sum# map будет вызван ещё раз со вторым аргументом# map(lambda x: int(x))(данные) при вызове

map(lambda x: int(x)) в пайплайне выглядит более лаконично в целом и в терминах последовательных преобразований данных.
Кривенькая неполная реализация на уровне языка:


from inspect import getfullargspecfrom copy import deepcopyclass CreatePartFunction:    def __init__(self, f):        self.f = f        self.values = []    def __call__(self, *args):        args_f = getfullargspec(self.f)[0]        if len(args) + len(self.values) < len(args_f):            new = deepcopy(self)            new.values = new.values + list(args)            return new        elif len(self.values) + len(args) == len(args_f):            return self.f(*tuple(self.values + list(args)))

Реализация примера с учётом данного костыля дополнения:


# костыль для обхода поломки inspect над встроенным mapm = lambda f, l: map(f, l)# создаём частично применяемую функцию на основе обычной питоньейpmap = CreatePartFunction(m)powered_sum = pipe | str | list | pmap(lambda x: int(x)) | enumerate | (lambda e: [v**(i+1) for i,v in e]) | sum

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


def f (x,y,z):    return x+y+zf = CreatePartFunction(f)# работаетprint(f(1,2,3))# работаетprint(f(1,2)(3))print(f(1)(2,3))# не работает# 2(3) - int не callableprint(f(1)(2)(3))# работаетprint((f(1)(2))(3))

Итоги


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

Подробнее..

Проблемы мониторинга дата-пайплайнов и как я их решал

16.06.2021 00:20:01 | Автор: admin

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

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

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

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

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

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

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

Вот примеры:

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

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

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

ETL как он естьETL как он есть

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

Чего не хватает во встроенных мониторингах систем работы с данными:

  • Бизнес не может просто посмотреть в модный мониторинг типа того же Airflow или ELK и понять, можно или нельзя доверять данным, актуальность состояния данных непрозрачна.

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

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

Все это превращается в такие вот проблемы:

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

  2. Люди из бизнеса хотят более наглядного отображения состояния данных и системы, чем оно представлено в технических мониторингах.

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

Концепция

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

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

Почему вообще вебхуки?

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

После того, как мы начали отсылать из процессов события, нужно их проанализировать и получить ответы на всякие важные вопросы, например (все числа абстрактны):

  • запустилась ли наша задача 10 раз за последний день?

  • не превышает ли количество падений (определяем падение, если полученное значение > 0, например) 15% от всех запусков за сегодня?

  • нет ли процессов, которые длятся больше 20 минут?

  • не прошло ли больше часа с момента последнего успешного завершения?

  • стартовало ли событие по планировщику в нужное время?

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

Реализация

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

Дашборд состояния серверов Sensorpad средствами SensorpadДашборд состояния серверов Sensorpad средствами Sensorpad

Каждый монитор на этом дашборде - это комбинация метрик, которые должны быть понятны и бизнесу, и инженерам.

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


Для инженера тут все понятно:

  • скрипт отрабатывает быстро (еще бы, простая крон-джоба);

  • монитор вполне живой, 25 минут назад обновился;

  • места еще с запасом (цифра 53 в левом нижнем углу - это последнее принятое значение);

Для людей из бизнеса тут тоже все просто:

  • монитор зеленый;

  • статус прописан в первой же строчке;

  • никакой лишней информации;

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

И насколько просто такое настроить?

  1. Создаем вебхук в самом сервисе, они там называются сенсорами, по аналогии со всякими штуками из физического мира.

  2. Настраиваем крон-джобу на сервере, которая будет отсылать события со значением свободного места в процентах:

    df -h |grep vda1 | awk  '{ print $5 }'| sed 's/.$//' | xargs -I '{}' curl -G "https://sensorpad.link/<уникальный ID>?value={}" > /dev/null 2>&1
    
  3. Присоединяем к этому вебхуку монитор, называем его: количество свободного места (но можно еще и другие, например, то, что события уходят по графику означает, что сервер не упал)

  4. Настраиваем правила, по которым монитор меняет свой статус.

  5. Присоединяем каналы для отправки уведомлений.

  6. Добавляем монитор на один или несколько дашбордов.

А можно поподробнее?

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

  • базовый вебхук, который будет нужен для 80% проектов;

  • cron-вебхук, который ожидает события в заданное через cron-синтаксис время;

  • chain-вебхук, который умеет отслеживать события от процессов, соединенных в цепочки;

главное в нашем деле - не усложнять интерфейсыглавное в нашем деле - не усложнять интерфейсы

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

Догфудинг в действииДогфудинг в действии

Дальше создаем тот самый монитор - квадратик, меняющий статус и цвет.

Можно даже иконку выбратьМожно даже иконку выбрать

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

Теперь, собственно то, из-за чего я и написал эту балалайку: правила и гибкая логика.

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

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

На скриншоте выше видно уже созданные правила, но я покажу как они создаются.

Например правило, которое можно сформулировать так: "установи статус Warning, если за последний день было больше 5 джоб, которые работали дольше 10 секунд".

А вот какие вообще можно выбирать проверки в каждом из пунктов:

И какие реальные кейсы можно покрыть этими правилами?

У каждого свои кейсы. Дата-инженерия вообще весьма специфичное для каждой компании направление. Если у вас есть дата-пайплайны или cron jobs, сервис оповестит вас, если (все цифры, разумеется, конфигурируемы):

  • Cron job, Airflow DAG или любой другой процесс не запустился по расписанию;

  • 20% задач одного и того же пайплайна за день не отработали как надо;

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

  • интервал между запусками двух задач меньше 1 минуты (похоже, у нас две конкурентные джобы);

  • с момента последнего успешного завершения пайплайна прошло 2 часа (а данные должны считаться каждый час);

  • время работы пайплайна уже целых 20 минут (а должен был отработать за 5, что-то подвисло).

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

А теперь - статистика!

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

Немного полезных и не очень графиковНемного полезных и не очень графиков

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

Вот такой концепт. Чего не хватает?


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

Потыкайте его вживую, заодно зацените, какой я у мамы дизайнер лендингов: https://sensorpad.io

Подробнее..

Категории

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

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