...

суббота, 4 марта 2017 г.

[Из песочницы] Apache Ant – быстрый старт

Разработка веб-скрапера для извлечения данных с портала открытых данных России data.gov.ru

О чём молчат авторы «Hello, World!»-ов

Copyleft под угрозой: Github теряет совместимость с GPL?

Haskell. Монады. Монадные трансформеры. Игра в типы

Еще одно введение в монады для совсем совсем начинающих.

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

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

С обычными функциями все понятно. Если имеется функция типа «a->b», то подставив в неё аргумент типа «a», вы получите результат типа «b».

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


1. Do-конструкция

Начнем с простого примера.
main = do 
        putStr "Enter your name\n"
        name <- getLine      
        putStr $ "Hello " ++ name


Каждая do-конструкция имеет тип «m a», где «m» — это монада. В нашем случае это монада IO.

Каждая строчка в do-конструкции так же имеет тип «m a». Значение «a» в каждой строчке может быть разным.

Символ "<-", как бы, преобразует тип «IO String» в тип «String».

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

return :: a -> m a

main = do 
        text <- getLine 
        doubleText <- return $ text ++ text
        putStr doubleText 


Функция return заворачивает любой тип «a» в монадический тип «m a».

В данном примере, с помощью return выражение типа «String» преобразуется к типу «IO String», которое потом обратно разворачивается в «String». Как вариант, внутри do-конструкции можно использовать ключевое слово let.
main = do 
        text <- getLine 
        let doubleText = text ++ text
        putStr doubleText


Вся do-конструкция принимает тип последней строчки.

Допустим, мы хотим прочитать содержимое файла. Для этого у нас имеется функция readFile.

readFile :: FilePath -> IO String


Как видим, функция возвращает «IO String». Но нам нужно содержимое файла в виде «String». Это значит, что мы должны выполнить нашу функцию внутри do-конструкции.
printFileContent = do
        fileContent <- readFile "someFile.txt" 
        putStr fileContent


Здесь переменная fileContent имеет тип «String», и мы можем работать с ней, как с обычной строкой (например, вывести на экран). Обратите внимание, что получившаяся функция printFileContent имеет тип «IO ()»
printFileContent :: IO ()

2. Монады и монадные трансформеры

Я приведу следующую простую аналогию. Представьте, что монада — это пространство, внутри которого можно производить некоторые, специфичные для данного пространства, действия.
Например, в монаде «IO» можно выводить текст в консоль.

main = do 
        print "Hello"

В монаде «State» есть некоторое внешнее состояние, которое мы можем модифицировать.

main = do 
        let r = runState (do 
                modify (+1)
                modify (*2)
                modify (+3)
                ) 5
        print r

-- OUTPUT: 
--      ((), 15)


В этом примере мы взяли число 5, прибавили к нему 1, умножили результат на 2, затем прибавили еще 3. В результате получили число 15.

С помощью функции runState

runState :: State s a -> s -> (a, s)


мы «запускаем» нашу монаду.

На монаду можно посмотреть с двух сторон: изнутри и снаружи. Изнутри мы можем выполнить некоторые, специфичные для данной монады, действия. А снаружи — мы можем её «запустить», «распечатать», преобразовать к некоторому немонадическому типу.

Это позволяет нам вкладывать одну do-конструкцию в другую, как в приведенном выше примере. Монада IO — это единственная монада, на которую нельзя посмотреть «снаружи». Все в конечном итоге оказывается вложенным в IO. Монада IO — это наш фундамент.

Приведенный выше пример имеет определенные ограничения. Внутри монады State мы не можем выполнять действия, доступные в IO.

Мы оказались «подвешенными в воздухе», потеряли связь с землей.

Для решения этой проблемы существуют монадные трансформеры.

main = do 
        r <- runStateT (do 
                modify (+1)
                modify (*2)
                s <- get
                lift $ print s 
                modify (+3)
                ) 5
        print r

-- OUTPUT:
--    12 
--    ((), 15)


Данная программа делает то же самое, что и предыдущая. Мы заменили State на StateT и добавили две строчки,
s <- get
lift $ print s 


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

Здесь runStateT запускает монаду StateT, а функция lift «поднимает» операцию, доступную в IO, до монады StateT.

runStateT :: StateT s m a -> s -> m (a, s)
lift :: IO a -> StateT s IO a

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

Операция «print s» имеет тип «IO ()».
С помощью lift мы «поднимаем» его до типа «StateT Int IO ()».
Внутренняя do-конструкция теперь имеет тип «StateT Int IO ()».
Мы «запускаем» её и получаем тип «Int -> IO ((), Int)».
Затем мы подставляем значение «5» и получаем тип «IO ((), Int)».
Поскольку, мы получили тип «IO», то мы можем использовать его во внешней do-конструкции.
Стрелочка "<-" снимает монадический тип и возвращает "((), Int)".
В консоль выводится результат "((), 15)".

Внутри StateT мы можем менять внешнее состояние и выполнять операции ввода/вывода. Т.е. монада StateT не «болтается в воздухе», как State, а осталась связанной с внешней монадой IO.

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

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

Комментарии (0)

    Let's block ads! (Why?)

    Программисты не могут написать алгоритмы без помощи: ещё раз про интервью

    Интерактивный UX-прототип: разбор на реальном примере

    Статья о том как получить пользовательский опыт на стадии проектирования. Показываю это на текущей версии рабочего UX-прототипа. Создан в программе UXPin, смотреть по ссылке только на десктопе, мобильная версия отдельно.

    На его примере подробно: о терминологии, чем интерактивный прототип лучше традиционного (10 моментов), как его использовать + несколько выводов.

    Что дает UX-прототип?


    • Значительно сокращает ресурсы по разработке (на 20-40% меньше программно-дизайнерских доработок и изменений).
    • Сокращает количество версий продукта (бета может быть уже фактически 3-4 версией).
    • Снижает количество ошибок проектирования.
    • Дает возможность команде, партнерам и инвесторам быстро показать как будет выглядеть продукт.

    Но, самое главное он дает тот самый, вожделенный UX.

    Что это за зверь?


    Сайт можно собрать без контента и дизайна. Как? Сделать UX-прототип. Это будут html-страницы шаблонов с всеми проставленными ссылками. Выглядеть он будет реалистично: на всех страницах кликабельный логотип, работающие пункты меню, полная структура (разделы, этапы), все рабочие кнопки, формы и т.д.
    Выложить это в веб, чтобы можно было открывать в браузере и передавать друг другу ссылки. Открывать этот прообраз сайта, пробовать пользоваться им на разных устройствах, то есть, тестировать.

    Такой прототип называют по-разному: полным, точным, интерактивным и т.п. Например, Нильсен Норман Групп называет его «кликабельным». Мне ближе название в соответствии с назначением: интерактивный UX-прототип (или просто UX-прототип). Ведь именно такой подход позволяет получить тот самый настоящий UX. «Пользовательский опыт» перевод некорректный. «Экспириэнс» это не просто опыт, а нечто эмоциональное, переживание пользователем взаимодействия с продуктом.
    Комплекты графических картинок-эскизов или блок-схем этого не дают. А именно такие наборы мы чаще всего видим под названием прототип. Назовем его традиционным.

    В чем отличие UX-прототипа от традиционного


    1) Акценты и приоритеты

    Можно управлять вниманием пользователя, создавая акценты:

    • размером элементов;
    • цветом;
    • активностью побуждающих текстов;

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

    Пример: Картинка выше. Показываем на сайте авиабилетов первым делом виде текста УТП — продажа билетов в кредит. Если результаты тестов покажут, что решение неудачное — перенесем вниз.

    2) Адаптивный или фикс? Размер рабочей области.

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


    Пример: Видим какова оптимальная ширина страницы и что показывать на мониторах, которые шире (фоновая картинка).

    3) Элементы навигации по сайту + фильтры, списки и т.п.

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

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


    Пример: CTA-кнопка с реальным текстом и моделированием моментов взаимодействия (изменение цвета по наведению — картинка справа).

    4) Прелоадеры и подгрузка контента

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


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

    5) Контент и структура сайта

    Использование реального контента (не в полном объеме, естественно, а в рамках прототипа) существенно снижает возможные изменения как в дизайне, так и в функционале.
    Для этого в разделы можно поместить по несколько реальных новостей, карточек товаров и контент-страниц с оформлением. Тогда прототип будет приближен к реальному сайту, но содержать будет не более 20-30 страниц.

    6) Картинки, их размер и качество

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

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

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

    7) Служебные сообщения

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


    Пример: Ответы системы в прототипе на последнем последнем этапе.

    8) Формы и поля ввода

    Работая с формами мы можем определить:

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

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


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

    9) Тактильность и мобильность

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

    10) Технические возможности

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


    Пример: Чекбокс «открытая дата». При его нажатии может показываться специальная форма с датами на второй странице. Сотрудники партнерской системы Амадеос могут оценить насколько реализуема эта схема.

    Как дальше работать с UX-прототипом?


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

    Что будем делать с MVP?


    У прототипа и MVP (минимальной версии продукта) разные задачи. Минимальной версией продукта может быть простой лендинг с формой заявки. Этого достаточно, чтобы проверить бизнес-модель (купить трафик, собрать заявки, посчитать с них маржу). Даже в нашем случае с авиабилетами это тоже работает. То есть MVP не требует прототипа и может быть сделана до или параллельно с ним.
    Либо MVP будет первой версией продукта, созданного на основе прототипа, но это долгий путь.

    Как меняется роль дизайнера, программистов и ТЗ?


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

    ТЗ в этом случае помещается на одну страницу с основными техническими вводными, все детали в прототипе.

    Как получать обратную связь?


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

    Системы для создания UX-прототипов


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

    Для каких проектов подходит?


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

    Итого


    Создание интерактивного UX-прототипа требует много времени. Но он сокращает общие временные и трудовые ресурсы по проекту за счет следующих моментов:
    1. Позволяет выявить сложности (с контентом, согласованием, терминологией, функционалом и т.д.) на начальном этапе и решать их параллельно с другими процессами.
    2. Дает быструю обратную связь, на основе которой можно внести коррективы без дизайнера и программиста.
    3. Помогает сразу определиться с важными моментами в интерфейсах и конфигурации. Меньше ответвлений и версий продукта.

    Но, самое главное, он дает настоящий UX и драйв! Все, что мы так любим в наших проектах.

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

    Комментарии (0)

      Let's block ads! (Why?)

      Интернационализация (i18n) в Angular 2

      пятница, 3 марта 2017 г.

      Микросервисы: опыт использования в нагруженном проекте

      На конференции HighLoad++ 2016 руководитель разработки «М-Тех» Вадим Мадисон рассказал о росте от системы, для которой сотня микросервисов казалась огромным числом, до нагруженного проекта, где пара тысяч микросервисов — обыденность.

      Тема моего доклада — то, как мы запускали в продакшн микросервисы на достаточно нагруженном проекте. Это некий агрегированный опыт, но поскольку я работаю в компании «M-Tех», то давайте я пару слов расскажу о том, кто мы.

      Если коротко, то мы занимаемся видеоотдачей — отдаём видео в реальном времени. Мы являемся видеоплатформой для «НТВ-Плюс» и «Матч ТВ». Это 300 тысяч одновременных пользователей, которые прибегают за 5 минут. Это 300 терабайт контента, который мы отдаем в час. Это такая интересная задача. Как это всё обслужить?

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

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

      Если посмотреть на то, что было в нашей инфраструктуре основного, то это Docker и TeamCity как система доставки кода, сборки и так далее.

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

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

      Сейчас на экране вы видите схему. Это один небольшой кусок нашей системы. Это штука, которая позволяет нарезать видео. Похожую схему я показывал полгода назад на «РИТ++». Тогда зеленых микросервисов было, по-моему, 17 штук. Сейчас их здесь 28. Если примерно посмотреть, это 1/20 нашей системы. Можно представить себе примерные масштабы.

      Подробности


      Один из интересных моментов — это транспорт между нашими сервисами. Классически начинают с того, что транспорт должен быть максимально эффективным. Мы тоже про это подумали, решили, что protobuf — это наше всё.

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

      Запрос через Load Balancer приходит на фронтовые микросервисы. Это либо Frontend, либо сервисы, которые предоставляют непосредственно API, они работали через JSON. А ко внутренним сервисам запросы шли уже через protobuf.

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

      Но если посмотреть в разрезе микросервисов, то заметно, что у вас между сервисами получается некое подобие проприетарного протокола. Пока у вас 1, 2 или 5 сервисов, вы под каждый микросервис можете спокойно выпускать консольную утилиту, которая будет позволять вам обращаться к конкретному сервису и проверять, что она возвращает. Если он что-то затупил — дёрнуть его и посмотреть. Это несколько усложняет работу с этими сервисами именно с точки зрения поддержки.

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

      Здесь тоже достаточно интересная деталь в реализации. Эта штука — на базе HTTP/2. Это вещь, которая реально работает из коробки. Если у вас не очень динамичная среда, если у вас не меняются инстансы, не переезжают по машинам достаточно часто, то это, в общем-то, хорошая штука. Тем более, что в данный момент есть поддержка под кучу языков — как серверных, так и клиентских.

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

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

      Как вы уже, наверное, догадались, в итоге мы пришли к тому, что мы стали задумываться про JSON. Причём мы сами долгое время не верили в то, что после какого-то компактного, условно бинарного протокола мы вдруг вернёмся в JSON, пока не наткнулись на статью ребят из DailyMotion, которые писали примерно про то же самое: «Блин, мы тоже умеем готовить JSON, его умеют готовить все на свете, зачем мы создаём себе дополнительные сложности?»

      В итоге мы постепенно начали мигрировать с gRPC на JSON в некой своей реализации. То есть да, мы оставили HTTP/2, мы взяли достаточно быстрые реализации для работы с JSON.

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

      По большому счету, если посмотреть на JSON, то единственный реальный минус, который ему можно предъявить прямо сейчас — это недостаточная компактность этого описания. Те 30%, которые, как статистически утверждается, являются разницей между тем же MessagePack или еще чем-то, на самом деле по нашим замерам, разница не настолько большая, а также это не всегда настолько критично, когда мы говорим о поддерживаемой системе.

      Плюс с переходом на JSON мы получили дополнительные плюшки. Такие, как, например, версионирование протокола. В какой-то момент у нас начала складываться ситуация, что через тот же protobuf мы описываем какую-то новую версию протокола. Соответственно, клиенты, потребители данного конкретного сервиса, тоже должны на неё переехать. Получается, что если у вас несколько сотен сервисов, даже 10% из них должны переехать. Это уже большой каскадный эффект. Вы в одном сервисе поменяли, а ещё 10 нужно переделать.

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

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

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

      Вместо 1, 2 и 3 у нас стала версия 1 и та схема, которая к ней относится.

      Вот типичный ответ одного из наших сервисов. Это Content Manager. Он выдал информацию о трансляции. Вот, например, схема одного из потребителей.

      Здесь самая интересная строчка — нижняя, где у нас блок required. Если мы посмотрим, то мы увидим, что этому сервису на самом деле из всех этих данных нужно только 4 поля — id, content, date, status. Если реально применить эту схему, то в итоге сервису потребителя нужны только эти данные.

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

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

      Когда у вас цепочка вызовов 1—2 сервиса, то особых проблем нет. Какую-то глобальную разницу между монолитным и распределённым приложением вы не видите. Но когда у вас цепочка разрастается до 5—7, на каком-то этапе у вас что-то отвалилось. Вы реально не знаете, почему оно отвалилось, что с этим делать. Отлаживать это достаточно сложно. Если на уровне монолитного приложения вы включили debugger, просто прошлись по шагам и нашли эту ошибку, то здесь у вас накладываются такие факторы, как нестабильность сети, нестабильное проведение под нагрузкой и еще что-то. И вот такие вещи — в такой распределенной системе, с кучей таких нод — становятся очень заметны.

      Тогда, в начале, мы пошли классическим путём. Мы решили все замониторить, понять, что и где ломается, пытаться с этим как-то оперативно бороться. Мы стали отправлять метрики с наших микросервисов, собирать их в единую базу. Мы через Diamond стали собирать данные по машинам, что на них происходит через сAdvisor. Мы стали собирать информацию по Docker-контейнерам, все это сливать в InfluxDB и строить dashboards в Grafana.

      И вот у нас появилось еще 3 кирпичика в нашей инфраструктуре, которая постепенно разрастается.

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

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

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

      Первый из важных моментов. Мы стали вводить в каждый наш сервис ограничение по входящим запросам. Каждый сервис у нас стал знать сколько он способен обслужить клиентов. Откуда он это знает, я ещё чуть позже расскажу. Все те запросы, что сверх этого лимита или около его границ, он перестаёт принимать. Он выдаёт честные 503 Service Unavailable. Тот, кто к нему обращается, понимает, что нужно выбрать другую ноду — эта неспособна обслужить.

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

      Второй момент. Если rate limiting на стороне сервиса назначения, то второй паттерн, который мы стали повсеместно вводить — Circuit Breaker. Это паттерн, который мы, грубо говоря, реализуем на клиенте.

      Сервис А, у него есть в качестве возможных точек обращения, например, 4 инстанса сервиса B. Вот он сходил в registry, сказал: «Дай мне адреса этих сервисов». Получил, что их 4 штуки. Сходил к первому, тот ему ответил, что всё ок. Сервис пометил «да», к нему можно ходить. По Round Robin он разбрасывает обращения. Пошел ко второму, тот ему не ответил за нужное время. Всё, мы его баним на какое-то время и идём к следующему. Тот, например, у нас возвращает некорректную версию протокола — неважно, почему. Он его тоже банит. Идёт к четвертому.

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

      Это позволило нам достаточно серьезно повысить стабильность работы в целом. С сервисом что-то не так — мы его отстреливаем, поднимается алерт на то, что сервис отстрелили, и мы дальше разбираемся, что могло быть не так.

      В ответ на введение паттерна Circuit Breaker у нас появилась еще одна штуковина в нашей инфраструктуре – это Hystrix.

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

      Здесь размер этого кружочка показывает, насколько у вас много трафика относительно других. Цвет показывает, насколько системе хорошо или плохо. Если у вас зеленый кружочек, то, наверное, у вас всё хорошо. Если красный — не всё так радужно.

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

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

      Вот стандартный запрос:

      Такая цепочка исполнения. От пользователя пришел запрос на первый сервис, потом на второй, со второго он разошёлся веткой на третий и четвёртый.

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

      Выглядит это вот так:

      Сразу скажу, это была штука именно на попробовать, понять, то ли это. Имплементировать её в нашу систему было проще всего, потому что мы к тому моменту достаточно плотно перешли на Go. Appdash имел готовую библиотеку для подключения. Мы посмотрели, что да, эта штука нам помогает, но сама реализация нас не очень устраивает.

      Тогда вместо Appdash у нас появился Zipkin. Эта та штука, которую сделали ребята из Twitter. Выглядит это примерно так:

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

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

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

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

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

      У нас есть логирование. Да, это достаточно стандартная штука, это ELK. Может быть, в небольшой нашей вариации.

      Мы не собираем напрямую через кучу forward в виде Logstash. Мы сначала передаём это в Syslog, с помощью Syslog мы это агрегируем на собирающих машинах. Оттуда уже через forward кладём в ElasticSearch и в Kibana. Относительно стандартная штука. В чём фишка?

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

      В итоге, мы в логах видим на Dashboard в Kibana полный контекст исполнения по конкретному пользователю. Очевидно, что если сервис попал в prod, то он условно уже рабочий. Он прошёл автотесты, на него уже посмотрели тестировщики, если надо. Он должен работать. Если он в какой-то конкретной ситуации не работает, то, видимо, были какие-то предпосылки. Эти предпосылки в этом подробном логе, который мы видим при такой фильтрации по конкретному trace для конкретного запроса, помогают гораздо быстрее понять, что конкретно в этой ситуации не так. В итоге у нас достаточно серьёзно сократилось время понимания причин проблемы.

      Следующий интересный момент. Мы ввели динамический debug mod. В принципе, у нас сейчас не такое дикое количество логов — порядка 100—150 гигабайт, не помню точную цифру. Но это — в базовом режиме логирования. Если бы мы писали вообще супер-подробно, это были бы терабайты. Обрабатывать их было бы безумно дорого.

      Поэтому, когда мы видим, что у нас проявилась какая-то проблема, мы заходим на конкретные сервисы, включаем на них через API debug mod и смотрим, что происходит. Иногда мы сначала смотрим, что происходит. Иногда мы отстреливаем сервис, который создает у нас проблему, не выключая его, включаем на нем debug mod и тогда уже разбираемся, что с ним было не так.

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

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

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

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

      Попутно с ней появился Consul, как registry для service discovery. Также Vault, в котором мы храним секретные данные: пароли, ключи, всё секретное, что нельзя хранить в Git.

      Таким образом у нас получилось, что все машины стали условно одинаковы. На машине есть Docker, на ней есть Consul-агент, Nomad-агент. Это, по большому счёту, готовая машина, которую можно брать и копировать один в один, в нужный момент вводить в строй. Когда они становятся не нужны, можно выводить из эксплуатации. Тем более, если у вас cloud, то вы в пиковые моменты машину заранее можете подготовить, включить. А когда нагрузка упала обратно, выключить. Это достаточно серьезная экономия.

      В какой-то момент Nomad мы переросли. Переехали на Kubernetes, а Consul стал играть роль системы центральной конфигурации для наших сервисов со всеми вытекающими.

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

      Первый шаг. Мы ввели некоторые лимиты по трём характеристикам: память, процессор, сеть.

      Мы зафиксировали три градации по каждой из этих величин. Нарезаем некие кирпичики. Как пример:

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

      Мы вводим именно мнемоники, потому что конкретные значения мы можем динамически подкручивать достаточно в широком диапазоне уже в нашей системе, который мы называем decision service. На текущий момент эти значения примерно такие:

      На самом деле у нас есть еще C4, R4, но это те значения, которые совсем выходят за рамки вот этих стандартов. Они оговариваются отдельно.
      Это выглядит примерно так:

      Следующий подготовительный этап. Мы смотрим, какой тип масштабируемости у этого сервиса.

      Самый простой — это когда у вас сервис полностью независим. Вы этот сервис можете линейно клепать. Пришло в 2 раза больше пользователей — вы в 2 раза больше инстансов запустили. У вас снова всё хорошо.

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

      И третий, самый интересный вариант — это когда вы ограничены какой-то внешней системой. Как пример — внешний биллинг. Вы знаете, что более 500 запросов он никак не обслужит. И хоть ты 100 своих сервисов запусти, всё равно 500 запросов в биллинг, и привет!

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

      Стандартно мы на CI-сервере собрали, запустили какие-то юнит-тесты. На тестовом окружении у нас у нас прошли интеграционные тесты, у нас тестировщики что-то проверили. Дальше мы перешли к нагрузочному тестированию в пре-продакшне.

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

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

      Если же мы говорим о сервисе третьего типа, то мы знаем его предел, мы запускаем одну копию нашего сервиса, точно так же даём нагрузку и смотрим, сколько этот сервис может обслужить. Если мы знаем, например, что предел того же биллинга — 1000 запросов, 1 инстанс обслуживает 200, то мы понимаем, что 5 инстансов — это тот максимум, который сможет это корректно обслужить.

      Всю эту информацию в InfluxDB мы сохранили. Появляется Decision service. Он смотрит 2 границы: верхнюю и нижнюю. По переходу за верхнюю границу он понимает, что нужно добавлять инстансы, а может, даже машины для этих инстансов. Также верно и обратное. Когда нагрузка падает (ночь), нам не нужно так много машин, мы можем на каких-то сервисах уменьшить количество инстансов, выключить машины и тем самым сэкономить немножко денежек.

      Общая схема выглядит примерно так:

      Каждый сервис через свои метрики регулярно говорит, какая на нём текущая нагрузка. Она уходит в ту же InfluxDB. Когда Decision service видит, что для этой конкретной версии этого конкретного инстанса мы подходим к порогу, он уже даёт команду Nomad или Kubernetes на то, что нужно добавлять новые инстансы. Возможно, он перед этим инициирует новый сервис в cloud, возможно, ещё какую-то подготовительную работу делает. Но суть в том, что он инициирует то, что нужно поднять новый инстанс.

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

      Это вот то, что касается масштабирования в каких-то общих вещах. И вся эта петрушка с кучей сервисов, она в итоге привела к тому, что мы посмотрели на еще такую штуку немного сбоку — это Gitlab CI.

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

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

      Во-первых, это то, что если мы говорим о разработке именно микросервисов, то первое, что я бы посоветовал — это начинать сразу с какой-то системы оркестрации. Пусть самой простой, как тот же Nomad, который вы запускаете с командой nomad agent -dev и получаете полностью готовую систему оркестрации, сразу с поднятым Consul, с самим Nomad и всей этой кухней.

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

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

      Следующий момент — это, конечно же, некие архитектурные вещи. В рамках микросервисов одна из самых важных таких вещей — это шина сообщений.

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

      Гораздо проще, надёжнее и эффективнее это сделать по-другому. Оставить 1 сервис, который делает регистрацию. Он зарегистрировал пользователя. Затем вы в эту общую шину выбрасываете event «я зарегистрировал пользователя, ID такой-то, минимальная информация такая-то». И это получат все сервисы, которым эта информация полезна. Один пойдёт аккаунт в биллинге сделает, другой приветственное письмо отошлёт.

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

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

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

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

      Вы понимаете, что какая-то метрика вам нужна — начинайте её собирать. Что-то не нужно — не собирайте. Это очень сильно упрощает оперирование этими данными, потому что их реально очень быстро становится безумно много.

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

      Ну собственно всё. Спасибо.


      Микросервисы: опыт использования в нагруженном проекте

      Комментарии (0)

        Let's block ads! (Why?)