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

Блог компании домклик

Перевод Модели угроз в дифференциальной приватности

10.11.2020 12:07:03 | Автор: admin


Это перевод второй статьи из серии публикаций по дифференциальной приватности.

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

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

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

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

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

Центральная модель дифференциальной приватности


Наиболее часто используемая модель угроз в работах по дифференциальной приватности центральная модель дифференциальной приватности (или просто центральная дифференциальная приватность).

Главный компонент доверенное хранилище данных (trusted data curator). Каждый источник передаёт ему свои конфиденциальные данные, а оно собирает их в одном месте (например, на сервере). Хранилище является доверенным, если мы предполагаем, что оно обрабатывает наши чувствительные данные самостоятельно, никому их не передает и не может быть никем скомпрометировано. Иными словами, мы считаем, что сервер с конфиденциальными данными не может быть взломан.

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


Рисунок 1: Центральная модель дифференциальной приватности.

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

Локальная модель дифференциальной приватности


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


Рисунок 2: Локальная модель дифференциальной приватности.

Локальная модель дифференциальной приватности позволяет избежать основной проблемы центральной модели: если хранилище данных будет взломано, то хакеры получат доступ только к зашумленным данным, которые уже соответствуют требованиям дифференциальной приватности. Это основная причина, из-за которой локальная модель была выбрана для таких систем, как Google RAPPOR [1] и система сбора данных Apple [2].

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

В конечном счете, такой подход оправдан только для запросов с очень стойким трендом (сигналом). Apple, например, использует локальную модель для оценки популярности эмодзи, но результат оказывается полезен только для наиболее популярных эмодзи (где тренд наиболее выраженный). Обычно эту модель не применяют для более сложных запросов, вроде тех, что используются в Бюро переписи США [10] или машинном обучении.

Гибридные модели


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

Например, можно использовать модель с перемешивателем (shuffling model), реализованную в системе Prochlo [4]. Она содержит недоверенное хранилище данных, много отдельных владельцев данных и несколько частично доверенных перемешивателей. Каждый источник сначала добавляет небольшое количество шума в свои данные, а затем отправляет их перемешивателю, который добавляет еще шума перед отправкой в хранилище данных. Суть в том, что перемешиватели вряд ли вступят в сговор (или будут взломаны одновременно) с хранилищем данных или друг с другом, так что небольшого шума, добавленного источниками, будет достаточно для гарантии приватности. Каждый перемешиватель может работать с несколькими источниками, как и в центральной модели, так что небольшое количество его шума будет гарантировать приватность и для результирующего массива данных.

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

Также можно комбинировать дифференциальную приватность с криптографией, как в проколе конфиденциального вычисления (secure multiparty computation, MPC) или в полностью гомоморфном шифровании (fully homomorphic encryption, FHE). FHE позволяет выполнять вычисления с зашифрованными данными без их предварительной расшифровки, а MPC позволяет группе участников безопасно выполнять запросы по распределённым источникам без раскрытия их данных. Вычисление дифференциально приватных функций с помощью криптобезопасных (или просто безопасных) вычислений многообещающий путь по достижению точности центральной модели со всеми преимуществами локальной. Причем в этом случае использование безопасных вычислений снимает необходимость иметь доверенное хранилище. Недавние работы [5] демонстрируют обнадеживающие результаты от комбинирования MPC и дифференциальной приватности, вбирая в себя большинство достоинств обоих подходов. Правда, в большинстве случаев безопасные вычисления на несколько порядков медленнее, чем локально выполненные, что особенно важно для больших массивов данных или сложных запросов. Сейчас безопасные вычисления находятся в активной фазе разработки, так что их производительность стремительно возрастает.

А дальше?


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

Подписывайтесь на наш блог и не пропустите перевод следующей статьи. Уже совсем скоро.

Источники


[1] Erlingsson, lfar, Vasyl Pihur, and Aleksandra Korolova. Rappor: Randomized aggregatable privacy-preserving ordinal response. In Proceedings of the 2014 ACM SIGSAC conference on computer and communications security, pp. 1054-1067. 2014.

[2] Apple Inc. Apple Differential Privacy Technical Overview. Accessed 7/31/2020. https://www.apple.com/privacy/docs/Differential_Privacy_Overview.pdf

[3] Garfinkel, Simson L., John M. Abowd, and Sarah Powazek. Issues encountered deploying differential privacy. In Proceedings of the 2018 Workshop on Privacy in the Electronic Society, pp. 133-137. 2018.

[4] Bittau, Andrea, lfar Erlingsson, Petros Maniatis, Ilya Mironov, Ananth Raghunathan, David Lie, Mitch Rudominer, Ushasree Kode, Julien Tinnes, and Bernhard Seefeld. Prochlo: Strong privacy for analytics in the crowd. In Proceedings of the 26th Symposium on Operating Systems Principles, pp. 441-459. 2017.

[5] Roy Chowdhury, Amrita, Chenghong Wang, Xi He, Ashwin Machanavajjhala, and Somesh Jha. Crypt: Crypto-Assisted Differential Privacy on Untrusted Servers. In Proceedings of the 2020 ACM SIGMOD International Conference on Management of Data, pp. 603-619. 2020.
Подробнее..

Сервис, позволяющий контролировать процесс сбора согласий на обработку персональных данных

26.01.2021 12:18:23 | Автор: admin
image

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

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

В соответствии с Федеральным законом от 27.07.2006 152-ФЗ О персональных данных субъект этих самых данных должен самостоятельно принять решение об их предоставлении и согласии на их обработку. Согласие на обработку персональных данных должно быть конкретным, информированным, сознательным и не может навязываться субъекту персональных данных со стороны оператора. Согласие должно быть дано субъектом или его представителем в письменной форме, либо в форме электронного документа, подписанного электронной подписью. И оператор обязан предоставить доказательства получения такого согласия.

Для чего и с какой целью?


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

  1. Не было единого подхода к сбору согласий на обработку персональных данных, в том числе:
    • отсутствовала централизованная система, регламентирующая процесс сбора (каждый сервис собирал согласия как хотел и хранил артефакты сбора как мог);
    • применялись разные способы подтверждений (в одной подсистеме сайта ДомКлик использовался чекбокс, в другой кнопка подтверждения);
    • использовались разнообразны виды и формы согласий.
  2. Были сложности с определением принадлежности полученного согласия конкретному пользователю ДомКлик (субъекту персональных данных).
  3. Поиск доказательств факта получения согласий трудоёмкий процесс, который не всегда был результативным.

В результате не в полной мере выполнялись требования законодательства по обработке и защите персональных данных.

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

Что удалось сделать и как это работает?


В рамках разработки сервиса Согласий перед командой ДомКлик стояли основные задачи:

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

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

Что мы сделали:

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


Дополнительно сервис может регистрировать следующие данные:

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

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

image

image

На этапе сбора мы предусмотрели несколько сценариев:

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

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

Кроме того, в сервисе предусмотрена поддержка в актуальном состоянии форм согласий (ранее все согласия были в формате .pdf и не имели версионности). Для этого применяется формат .html, а версионность документов позволяет DPO оперативно корректировать формы согласий (например, в связи с изменениями в законодательстве или в бизнес-процессе). При этом не нарушается клиентский путь.

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

В серверной части сервиса Согласий использован Kotlin, а фронтендная часть написана на React.

Новый сервис принял уже более 8 000 000 согласий от пользователей ДомКлик.

Какие планы на будущее?


Для пользователей:

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

Для сервисов ДомКлик:

  • Начнём применять сервис Согласий в иных целях и для других документов (например, для согласий на получение рекламно-информационных материалов о продуктах, товарах и услугах ДомКлик, и т.д.).
  • Будем использовать в тексте согласий динамические параметры (например, наименование третьего лица, чьи персональные данные были переданы).
  • Автоматизируем и упростим процесс отзыва согласий на обработку персональных данных.
Подробнее..

Как стать фронтендером? Пошаговый гид в мир фронтенда

03.11.2020 12:06:29 | Автор: admin


Приветствую всех! Меня зовут Пучнина Анастасия, я ведущий разработчик в компании ДомКлик, занимаюсь фронтендом Витрины объявлений. Сегодня я хотела бы поделиться с вами своим мнением на тему того, что важно знать фронтенд-разработчику. Эта статья будет полезна тем, кто только начинает свой путь в разработке, или имеет опыт программирования в другой области и решил перейти на сторону фронтенда.

Содержание:

  1. Кто такой фронтендер и чем он занимается?
  2. С чего начать и что читать? Чек-лист обучения
  3. Какие трудности могут быть? Ошибки в начале пути
  4. Подготовка к собеседованию на Junior-разработчика


Кто такой фронтендер и чем он занимается?


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

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

Фронтенд-разработчиками называют программистов, которые отвечают за создание такой внешней стороны (англ. front end) веб-сайтов. Это клиентская часть сайта, с которой пользователь непосредственно взаимодействует на своем компьютере или телефоне (клиенте).

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


Фронтенд-разработчик:

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

В разных командах фронтенд-разработчики могут решать совершенно разные задачи, например:

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

С чего начать и что читать? Чек-лист обучения


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

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



Три кита разработки под браузеры это HTML, CSS и JavaScript, с которых и стоит начать.

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

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

Сохраняйте в закладки и пользуйтесь.

1. Как работает Веб


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


2. Среда разработки


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

  • VSCode бесплатный быстрый редактор с большим количеством дополнений для разработки;
  • JetBrains WebStorm полноценная IDE, есть пробный период и возможность получить доступ по студенческой лицензии;
  • Если у вас возникает потребность отправить другому человеку фрагмент кода, быстро проверить или сохранить код в сети, можно воспользоваться онлайн-редактором, например, Codepen.

3. Основы HTML


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


4. CSS



Добавление стилей для элементов страницы. Изучаем блочную модель, позиционирование, каскадирование стилей, специфичность селекторов, псевдоэлементы, адаптивную верстку (для компьютеров, планшетов и телефонов). Учимся верстать современные макеты с помощью Flexbox и Grid.


5. Система контроля версий Git


Git поможет вам выйти на новый уровень в процессе написания кода. Это незаменимый инструмент для разработчиков, который позволяет поэтапно сохранять информацию обо всех изменениях в коде, возвращаться к разному состоянию, и многое другое. Желательно научиться работать с Git в терминале, а также попрактиковаться: создать репозиторий на Github или Gitlab и опробовать основные возможности Git на своём тестовом репозитории.


Практика. Практика. Практика!



На этом этапе я предлагаю побольше попрактиковаться и сделать свой небольшой проект, чтобы запомнить пройденный материал. Необязательно самостоятельно придумывать дизайн сайта, вы можете взять готовый макет. Создайте landing page или, например, сверстайте сайт-портфолио, сохраняйте код на Github в процессе разработки и попробуйте разместить сайт на бесплатном хостинге для статических сайтов Github Pages. Если вам недостаточно практики, попробуйте посмотреть, как верстают другие, и повторить. На YouTube достаточно видеоуроков на эту тему (например, по запросу верстка сайта по макету figma).

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

6. Язык программирования JavaScript


Не путайте JavaScript с Java. Изучите основы языка: переменные, объекты, типы данных, функции, контекст и замыкания, классы. Сравните отличия спецификаций EcmaScript старых и новых версий. После понимания основ переходите к более сложным вещам: тонкостям асинхронного программирования (коллбеки, промисы, async-await) и выполнению запросов на сервер (XmlHttpRequest, Ajax, Fetch, Cookie).


7. DOM (Document Object Model)


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

Подробно тема DOM также описана в книге Выразительный JavaScript, автор Марейн Хавербек.

8. Node.js, NPM


Благодаря программной платформе Node.js язык JavaScript можно использовать не только в браузере. С помощью этого инструмента можно написать консольную программу или серверную часть приложения.

Попробуйте написать свой собственный небольшой сервер.

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

9. Babel


Babel Js компилятор JavaScript-кода, который позволяет использовать последние возможности языка, например, стрелочные функции, классы, optional chaining, не дожидаясь их полной поддержки браузерами. Вам необходимо лишь правильно сконфигурировать Babel под нужную версию EcmaScript или список поддерживаемых вами браузеров, исходный код будет преобразован автоматически.

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

10. Сборщики модулей, Webpack


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

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

Webpack один из самых популярных сборщиков модулей. Это помощник разработчиков, несмотря на то, что по началу он может показаться достаточно сложным. Его гибкость позволяет настроить сборку с использованием огромного множества плагинов и загрузчиков (пре- и пост- процессоров CSS и HTML, Babel и др.), оптимизировать ресурсы, быстро подгружать изменения в процессе разработки с помощью hot module replacement, и многое другое.


11. Препроцессоры CSS


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

Наиболее популярные препроцессоры:


Основными преимуществами использования препроцессоров CSS-кода являются:

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

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

12. Препроцессоры HTML (Шаблонизаторы)


Препроцессоры HTML имеют те же преимущества, что и CSS-препроцессоры. Они позволяют более эффективно писать HTML-разметку, разбивать код на модули, а также использовать условия, циклы, миксины, наследование.

Популярные шаблонизаторы:


Настройте свой проект, добавив в webpack-конфигурацию загрузчики для обработки CSS- и HTML-кода препроцессорами.

13. Стиль кода и линтеры


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

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

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


Ознакомьтесь с документацией инструментов, попробуйте подключить их к своему проекту и IDE, следуйте определенному стилю кода в своём проекте, чтобы сделать приложение лучше!

14. Изучение фреймворка/UI-библиотеки


React, Angular или Vue? На 2020 год основная борьба идет между этими библиотеками. Вы можете выбрать любую из них. Если вам симпатизирует какая-то конкретная компания, в которой вы хотели бы работать, можете выбрать используемый ими фреймворк.

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

Начните изучать React с официальной документации, она достаточно подробная. Если её вам покажется недостаточно, можно найти на Udemy полноценный курс (например, Modern React and Redux на английском языке, с очень понятным и подробным объяснением для начинающих, практическими заданиями и всем необходимым материалом по React и библиотекам).

Уделите внимание описанию типов входных параметров для React-компонентов (проверка типов с помощью PropTypes), а также написанию комментариев по стандарту JSDoc (цикл статей по использованию JSDoc).

По мере вашего продвижения необходимо будет научиться управлять состоянием приложения. Библиотеки, которые помогают в этом: Redux и Mobx. Начать рекомендую с Redux это наиболее популярная библиотека в связке с React. Ознакомьтесь с официальной документацией или переводом. Также советую курс от одного из авторов библиотеки (Getting Started with Redux от Дэна Абрамова).

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

15. Автоматическое тестирование


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

16. Углубленное



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




Изучение фреймворков вместо базовых знаний


Иногда будет казаться, что лучше сразу изучать какой-нибудь популярный фреймворк или библиотеку. Это достаточно частая ошибка, особенно во фронтенде: люди начинают изучать React или верстают с помощью Bootstrap и Material UI, не разобравшись в основах и не получив достаточных знаний по HTML, CSS и JavaScript. Можно использовать такой подход, если вы бежите на короткую дистанцию и вам нужно быстро сделать какой-нибудь проект. Но если вы планируете стать разработчиком, это не принесет нужного результата.


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

Обучение это труд, самодисциплина и много практики


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

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

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

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

Копирование чужого кода


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

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

Не доверяйте на 100% коду, который вы находите


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

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

Подготовка к собеседованию на Junior-разработчика


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

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

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

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

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

Сайты с задачами по программированию:



В проверке знаний также могут помочь Telegram-каналы с задачками и тестами по JavaScript (например, @js_test).

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

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


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

Желаю удачи в покорении новых вершин!
Подробнее..

Зачем нужна выделенная Frontend Core команда и как мы внедряли дизайн систему

01.10.2020 12:15:50 | Автор: admin


Всем привет, меня зовут Ростислав, я занимаю должность Front Lead в компании ДомКлик. Хочу поделиться с вами опытом создания Front Core команды и сразу ответить на следующие вопросы:


  • Необходима ли такая команда в компании?

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


  • Выгодно ли внедрять такую команду?

Безусловно. Изначально было сложно измерить и спрогнозировать выгоду от её создания, все расчеты, P&L были на словах, в цифрах только примерные предположения. Спустя год мы можем посчитать сэкономленное время, профиты, и все расчеты говорят о том, что это было не зря.


  • На долгую перспективу ли эта команда?

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


  • Чем эта команда занимается?

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


Предыстория


Создание Front Core команды началось с потребности разработать дизайн систему и библиотеку компонентов с последующей поддержкой и развитием, а также внедрить стандарты для frontend разработки.


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


За всю историю ДомКлик было много попыток собрать библиотеку компонентов для их переиспользования во всех командах. Сначала пытались это делать в лояльном режиме: выделяли из своих проектов компоненты, которые можно переиспользовать, в отдельный репо и публиковали в npm-репозиторий. Затем компания энтузиастов решила собрать базу лучших компонентов в один проект со storybook, с единообразным API, и, по возможности, её поддерживать и дополнять. Но все эти попытки были тщетны до создания выделенной команды Web Core, у которой есть свои цели, обязанности и компетенции.


Дизайн-система и UI-KIT


Какие цели мы хотели достичь при создании дизайн системы:


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

Наша дизайн система будет состоять из таких элементов:


  • Гайдлайны и руководство по стилю сетка, типографика, цвета, брейкпоинты.
  • UI-KIT набор элементов пользовательского интерфейса для дизайнеров, основанный на гайдлайнах.
  • Шаблоны (паттерны) композиция элементов UI-KIT для описания типового интерфейса.
  • Библиотека компонентов кодовая база для использования в проектах. Включает в себя сами компоненты, документацию к ним, историю изменений, примеры использования с рекомендациями и правилами применения.

Дизайн


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


Основа всей дизайн системы гайдлайны.



На основе гайдлайнов строятся базовые элементы. А на основе базовых элементов строятся более сложные элементы.



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


Разработка


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


Перед началом разработки мы задали себе несколько задач:


Версионирование


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


Есть несколько способов поставки библиотеки компонентов конечному потребителю продуктовым разработчикам.



Самый популярный вариант: один пакет UI-KIT, который ставишь целиком в проект и в дальнейшем обновляешь его при появлении новой версии. В этом случае должен безукоризненно работать tree shaking, иначе при сборке проекта мы получим даже те зависимости, которые не используем.


Достоинства:


  • Консистентное обновление всех компонентов разом, при котором не нарушается целостность UX в проекте. При патч- и минор-релизах со спокойной душой обновляем UI-KIT, если мы доверяем Web Core-команде.

Недостатки:


  • При мажорном обновлении версии UI-KIT разработчику необходимо потратить немало времени, чтобы проверить все сценарии использования компонентов на своем проекте, а также поддержать новое API использования. Затраченное время при мажорном обновлении можно рассчитать по формуле кол-во используемых компонентов * _кол-во мест использования _и если вы хотите получить новое обновление, в котором может появиться нужный вам компонент.
  • Потребителям придется заложить немало времени на обновление.


Второй вариант каждый компонент UI-KIT как отдельный пакет. В этом случае каждый отдельно взятый компонент версионируется по SemVer.


Достоинства:


  • При появлении мажорных версий компонентов потребитель может обновлять итеративно.
  • После выхода нового компонента нет необходимости обновлять весь UI-KIT в проекте.

Недостатки:


  • Если потребителю нужно обновить все компоненты в рамках мажорной версии, то придется обновлять их по отдельности. Но в этом случае нам помогает полезная команда ncu --filter /@ui-kit/ -mu && npm i, где @ui-kit scope библиотеки компонентов.

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


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


Если смотреть на статистику, то Rush наименее популярная библиотека. Так почему же мы выбрали её? Lerna была очень непредсказуемой и магической, нигде не было нормально описано, как она работает. Процесс релиза был недетерминирован. В Rush это сделано более системно: благодаря change-файлам ты всегда видишь, что поедет в релиз, и библиотека автоматически генерирует читабельные changelog, на основе которых бампит версии так, как тебе нужно. Также Rush работает с Git как надо. В целом эта библиотека показалась более зрелым инструментом, поэтому мы выбрали её.


Вот как мы работаем с Rush:



Сборка


Для сборки наших компонентов в формате ESM и ESNext с поддержкой tree-shaking мы используем Rollup, и ожидаем релиза Webpack 5, в котором нам обещают отличную поддержку этой функциональности. Чтобы у нас во всех пакетах была идентичная сборка и мы могли бы ее поддерживать и улучшать, мы можем везде ссылаться на один файл с конфигурацией. Но мы пошли дальше: создали библиотеку, которая этим занимается. Она гибкая, масштабируемая, и её можно использовать в других проектах для своих нужд. В ней уже соблюдены некоторые стандарты нашей компании: browserlist со списком поддерживаемых браузеров, сборка стилей, оптимизации для сборки библиотек и многое другое. Наши компоненты можно использовать в SSR-приложениях, при этом мы расширили поддержку старых версий NodeJS, собирая наши пакеты еще и в CommonJS-формате.


Стили


В наших компонентах мы используем CSS-модули и Sass как препроцессор.


О чем стараемся не забывать при этом подходе:


Версионирование стилей. Так как мы выбрали путь поставки компонентов по отдельности, а не целой библиотеки компонентов, потребитель может обновить только один пакет, не обновляя другие. В этом случае могут возникнуть конфликты между стилями разных версий компонентов. Поэтому при сборке пакетов в настройках для css-modules прописываем generateScopedName:`[local]-${projectManifest.version}````, гдеprojectManifest``` файл package.json. Так у нас решаются все возможные конфликты.


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


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


Иконки


Многие задаются вопросом, каким способом предоставлять SVG-иконки. Изначально мы попробовали выгружать их на CDN по отдельности, но в этом случае получали перерисовку контента и много потоков скачивания. При использовании спрайта потребитель получает огромную (и тяжеловесную) портянку иконок, из которых он может использовать только несколько. Поэтому мы решили предоставлять React-компонентами, которые содержат в себе инлайново SVG-иконки. При сборке релиза запускается скрипт, который перегоняет SVG-файлы в IconComponent, заменяя все свойства fill на currentColor и применяя SVG-oптимизацию, поэтому нашим иконкам легко изменить цвет, задав его родителю.


Кроссбраузерность


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



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



В зависимости от заголовка user-agent сервис отдает тот или иной набор полифилов. Мы выделили три набора для трех сегментов браузеров.



Размеры наборов составляют непосредственно:



В итоге мы получаем:


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

Внедрение


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


  • Понятное API взаимодействия с компонентами с хорошей документацией.
  • Отсутствие избыточности. Мы все знаем open source-библиотеки, которые устанавливаем ради одного или нескольких сценариев использования, а вместе с этим получаем тонну ненужного кода. Поэтому мы стараемся создавать отдельно компоненты с базовой функциональностью, и на их основе отдельно создаём другие, с дополнительными хотелками.
  • Небольшой вес пакета. Для нас это имеет значение, мы не хотим, чтобы продукты из-за наших компонентов прибавляли в весе.
  • Высокая производительность. Под этим мы понимаем гладкость анимации и наименьшее количество перерисовок.
  • Хорошая техническая поддержка. Не всё всегда бывает гладко. У кого-то появились вопросы по работе API, у кого-то выявился баг, а у кого-то расхождение с макетами при использования нашего компонента. На эти случаи мы создали тех. поддержку, где наши разработчики в любую секунду помогут в беде.

Витрина компонентов


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



Здесь мы можем увидеть список всех наших компонентов, информацию по ним, показать все их состояния и возможности.



Тут мы можем увидеть нашу историю изменений. Эта информация берется из файлов CHANGELOG.md, которые генерируются с помощью Rush.



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


Численность и состав


Изначально наша команда состояла из двух frontend (React)-разработчиков, одного дизайнера и одного product ownerа. Сразу хочется пояснить необходимость PO в команде. Человек с этой ролью не только выстраивает процесс и координирует работу команды, но и формирует позиционирование всей дизайн-системы, собирает потребности от других команд для дальнейшего внедрения в UI-KIT и исследует пользовательский опыт с интерфейсами, построенных на наших компонентах, для улучшения UX/UI. С увеличением кодовой базы, численности и масштабности компонентов мы расширились до пяти разработчиков, и не планируем на этом останавливаться.


Заключение


На дизайн системе список активностей команды Web Core не заканчивается. Мы также создаем омниканальные виджеты, такие как навигация по сайту, выбор региона и т.д. Стараемся выработать правила для стандартизации всей кодовой базы в нашей компании. Разрабатываем библиотеки для сбора метрик, авторизации и регистрации. И этот список можно дополнять и дополнять. Я надеюсь, что вы почерпнули что-то новое для себя и своей команды. Очень буду рад обратной связи! Всем спасибо!

Подробнее..

WorkBox ваш toolkit в мире сервис-воркеров

13.10.2020 12:18:27 | Автор: admin

Привет, Хабр!

Меня зовут Святослав. Я работаю в компании ДомКлик и отвечаю за развитие сервисов оформления ипотеки. В начале года мы взяли курс на внедрение философии Progressive Web Application (PWA) в наших клиентских приложениях.

Одним из важных аспектов PWA является использование технологии Service Worker API, выполняющей роль прокси между браузером и сервером. Это позволяет поддерживать работу приложения в оффлайн-режиме, управлять кэшированием сетевых данных с их фоновой синхронизацией, а также работать с push-уведомлениями.

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

Workbox это разработанный в Google набор инструментов, предоставляющих высокоуровневый API для работы с такими браузерными технологиями, как Service Worker API и Cache Storage API. Инструментарий состоит из набора изолированных модулей, которые помогут вам сделать полноценное PWA-приложение.

Входящие в состав Workbox модули.Входящие в состав Workbox модули.

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

Управление кэшированием

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

Network Only

При получении запроса сервис-воркер перенаправляет его в сеть. Кэш не используется.

Cache Only

Сервис-воркер формирует ответ на запрос только из кэша. Сеть не используется. Эта стратегия будет полезна, если у вас используется предварительное кэширование.

Network First

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

Cache First

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

Stale While Revalidate

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

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

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

import {registerRoute} from 'workbox-routing';import {CacheFirst} from 'workbox-strategies';import {CacheableResponsePlugin} from 'workbox-cacheable-response';import {ExpirationPlugin} from 'workbox-expiration';registerRoute(  ({request}) => request.destination === 'image',  new CacheFirst({    cacheName: 'assets',    plugins: [      new CacheableResponsePlugin({        statuses: [0, 200]      }),      new ExpirationPlugin({        maxEntries: 60,        maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days      })    ]  }));

Кэширование потоковых аудио- и видеоданных

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

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

Простой пример работы с этим модулем:

import {registerRoute} from 'workbox-routing';import {CacheFirst} from 'workbox-strategies';import {RangeRequestsPlugin} from 'workbox-range-requests';registerRoute(  ({url}) => url.pathname.endsWith('.mp4'),  new CacheFirst({    plugins: [      new RangeRequestsPlugin(),    ]  }););

Журналирование

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

Кроссбраузерная работа

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

К примеру:

  • Модуль оповещения об обновлении кэшированных данных (workbox-broadcast-cache-update) использует под капотом Broadcast Channel API. А если браузер его не поддерживает, то переключается на механизм postMessage.

  • Модуль фоновой синхронизации данных (workbox-background-sync) использует Background Sync API. При отсутствии браузерной поддержки модуль попытается повторить запрос из очереди событий во время следующего запуска сервис-воркера.

Интеграция с Google Analytics

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

Модуль Workbox Google Analytics создан для решения этой проблемы. При оффлайн-работе он отлавливает неудачные запросы и сохраняет их в локальную базу данных браузера IndexedDB. А при возобновлении интернет-соединения запросы повторно отправляются на серверы Google Analytics.

Простой пример подключения этого модуля:

import * as googleAnalytics from 'workbox-google-analytics';googleAnalytics.initialize();

Способы использования

Workbox предлагает следующие варианты использования:

Работа с Webpack

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

npm install workbox-webpack-plugin --save-dev

Далее нужно подключить плагин в конфигурационном файле webpack.config.js, чтобы webpack автоматически сгенерировал воркер-файл на этапе сборки приложения.

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

// Inside of webpack.config.js:const WorkboxPlugin = require('workbox-webpack-plugin');module.exports = {  // Other webpack config...  plugins: [    // Other plugins...    new WorkboxPlugin.GenerateSW({      // Do not precache images      exclude: [/\.(?:png|jpg|jpeg|svg)$/],      // Define runtime caching rules.      runtimeCaching: [{        // Match any request that ends with .png, .jpg, .jpeg or .svg.        urlPattern: /\.(?:png|jpg|jpeg|svg)$/,        // Apply a cache-first strategy.        handler: 'CacheFirst',        options: {          // Use a custom cache name.          cacheName: 'images',          // Only cache 10 images.          expiration: {            maxEntries: 10,          }        }      }]    })  ]};

На выходе мы получим сгенерированный файл sw.js с определенными правилами кэширования сетевых данных.

Резюме

Workbox делает работу с сервис-воркерами более комфортной. Этот инструмент позволяет декларативно определить правила кэширования ресурсов приложения, взяв низкоуровневую работу на себя. К тому же Workbox уже интегрирован с такими инструментами разработки, как react-create-app, vue-cli, preact-cli, next.js, что говорит о его признании со стороны сообщества разработчиков.

Подробнее..

Использование Effector в стеке React TypeScript

09.12.2020 12:08:12 | Автор: admin

Всем привет! Меня зовут Елизавета Добрянская, я frontend-разработчик в компании ДомКлик. Моя команда занимается разработкой сервисов, предназначенных для коммуникаций с клиентом.

В этой статье я поделюсь своим кратким обзором внедрения стейт-менеджера Effector в продуктовый проект на стеке React + TypeScript, а также покажу на примере, как легко это можно сделать.

Содержание:

  1. Немного предыстории

  2. Первая встреча с Effector

  3. Боль как начало

  4. Выходим на новый уровень получаем удовольствие

  5. Best practices

  6. Итоги

  7. Вместо послесловия

Немного предыстории

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

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

Выбирали между Redux, Mobx и Effector. Первые два мы пробовали, и впечатления остались очень неоднозначные. И как ясно из статьи, выбрали последний, потому что любопытно было узнать, что же за зверь такой этот Effector и чем он может помочь нам. К тому же новый проект создавался для внутренних нужд и на нем вполне можно было поэкспериментировать.

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

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

Первая встреча с Effector

Что есть Effector? Модный, молодежный реактивный стейт-менеджер :) А потому понять его базовые принципы оказалось довольно просто. В его основе лежат три простых базовых сущности:

  • Хранилище (Store) это место, где мы храним наши данные.

  • Событие (Event) это действие, которое каким-то образом модифицирует хранилище.

  • Эффект (Effect) это асинхронное действие, связанное с хранилищем.

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

Основная идея, лежащая в основе Effector подписка на события. У нас есть хранилище, мы подписываемся на его обновления, вешая определенный обработчик. Например:

// Создаем хранилище, в котором будет лежать массив пользователей// IUser  интерфейс, описывающий пользователя (имя, фамилия и т.п.)export const $users = createStore<IUser[]>([]);// Создаем событие, принимающее параметр IUserexport const update = createEvent<IUser>();// Обычный хендлер на обновление. Добавляем или изменяем пользователяconst updateStore = (state: IUser[], data: IUser) => {  const userIndex = state.findIndex((user) => user.id === data.id);  // Изменяем стейт  if (userIndex > -1) {    state.splice(userIndex, 1, data);  } else {    state.push(data);  }  // Возвращаем измененный стейт  return [...state];};// Подписываемся на событие в хранилище$users  .on(update, updateStore);

Effector позволяет работать с разными типами приложений, таких как React, React Native, Vue, Node.js. Кроме того, он поддерживает TypeScript.

Для работы с React есть удобный пакет effector-react, предоставляющий несколько интерфейсов взаимодействия React-компонентов с Effector. Самый простой способ использовать хук useStore для максимально лаконичной работы с хранилищами Effector. Вот пример работы с описанным выше хранилищем $users, где по нажатию на кнопку мы добавляем в хранилище пользователя-заглушку:

import { useStore } from 'effector-react';import { $users, update } from 'models/users';export const UserList = () => {  const users = useStore($users);  const mockUser = {    id: 1111,    name: 'Peter',    surname: 'Jonson',    age: 25,    gender: 'male',  };  const usersItems = users.map((user) => (    <div key={user.id}>      <div>Name: {user.name}</div>      <div>Surname: {user.surname}</div>      <div>Age: {user.age}</div>      <div>Gender: {user.gender}</div>      <br/>    </div>  ));  return (    <div>      {usersItems}      <button onClick={() => update(mockUser)}>        Add mock user to Effector store      </button>    </div>  );};

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

import { useList } from 'effector-react';import { $users, update } from 'models/users';export const UserList2 = () => {    // Можно преобразовать в массив нод сразу при подключении.    // Не нужно использовать пропс key, как было с map()  const users = useList($users, (user) => (    <div>      <div>Name: {user.name}</div>      <div>Surname: {user.surname}</div>      <div>Age: {user.age}</div>      <div>Gender: {user.gender}</div>      <br/>    </div>  ));  const mockUser = {    id: 2222,    name: 'Diana',    surname: 'Gregory',    age: 22,    gender: 'female',  };  return (    <div>      {users}      <button onClick={() => update(mockUser)}>        Add mock user to Effector store      </button>    </div>  );};

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

Боль как начало

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

1) TypeScript

Да, самым сложным для меня оказалась поддержка такого же модного и молодежного, как и Effector, языка программирования TypeScript. В официальной документации Effector-а все примеры приведены на чистом JavaScript. Есть, конечно, маленькая робкая вкладка "TypeScript", которая, в основном, даёт только понимание того, куда нужно добавить типы в описании основных сущностей, но на этом всё. Поэтому сначала я использовала any, а под конец пришлось очень много страдать с расстановкой правильных типов (особенно касательно эффектов).

Так, например, родились следующие интерфейсы функций (слабонервным не смотреть):

// Создаем эффекты для получения и изменения данных о пользователях// IUserPayload - интерфейс пользователя, приходящий с сервераexport const getUsersFx = createEffect<void, IUserPayload[], Error>();export const updateUserFx = createEffect<IUserPayload, IUserPayload, Error>();// Изменяем формат данных из хранилища в формат, необходимый для отправки запросаconst serializeDataBeforeFetch = attach<  IUser,  Store<IUser[]>,  typeof updateUserFx  >({  effect: updateUserFx,  source: $users,  mapParams: (params: IUser, data: IUser[]) => {    const user = data.find((item) => item.id === params.id)!;    const userCopy = { ...user };    delete userCopy?.onlineStatus;    return userCopy;  },});

Небольшие пояснения по коду.

Эффекты имеют следующий формат типизации:

  1. Тип передаваемого в эффект значения.

  2. Тип возвращаемого из эффекта значения.

  3. Тип ошибки для случая, если что-то пошло не так.

Про функцию serializeDataBeforeFetch расскажу ниже, а пока стоит обратить внимание на типы метода attach, предоставляемого Effector:

  1. Тип передаваемого значения.

  2. Тип данных хранилища.

  3. Тип эффекта, используемого внутри attach.

2) Асинхронные события

Поначалу было очень сложно это понять и принять. Представьте ситуацию, что вы написали код, и при тестировании он выдает неожиданные результаты и ошибку. Вы пытаетесь отладить ошибкоопасное место с помощью точек останова, но видите, что в дебаг-режиме всё работает, как нужно. А вот в обычном режиме (и на самом деле) всё не так, ничего не работает. То есть в режиме отладки вы как бы притормаживаете свой код, и поэтому он отрабатывает корректно, а на самом деле есть проблемы. Собственно, это просто нужно принять к сведению торопливому разработчику действия в Effector происходят асинхронно (подобно setState в React).

3) Получение доступа к текущему состоянию

Этот пункт про то, что нужно внимательно смотреть документацию :)

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

4) Четкий интерфейс работы с сущностями

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

  • Хранилище readonly. В компоненте мы на него подписываемся, и все изменения считываем реактивно.

  • Событие по сути, setter. Мы говорим измени моё хранилище, добавь в него эти данные и удали те. Событие ничего не возвращает. Поэтому его нельзя использовать как getter и получить отфильтрованные данные из хранилища напрямую (об этом будет далее).

  • Эффект аналогичен событию, но имеет свойства .done, .fail, .pending и .finally, с которыми можно взаимодействовать (об этом тоже будет далее).

5) Отсутствие геттеров

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

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

Пример нового хранилища, зависимого от основного:

// Учебный пример.// Предположим, на клиенте нужно дополнительное поле со статусом пользователя.// Оно не приходит с сервера, и мы добавляем его искусственно.// Добавляем поле Статус каждому пользователюconst serializeUsers = (state: IUser[]) =>state.map((user) => ({ ...user, onlineStatus: true }));/** * Новое хранилище, зависимое от хранилища $users.  * Данные из $users прогоняются через функцию serializeUsers * и сохраняются в новое хранилище, которое можно использовать в компоненте */export const $usersWithStatus = $users.map(serializeUsers);

6) Отслеживание статуса эффектов

У эффектов есть промисоподобные свойства .done, .fail, .pending и .finally. Поэтому кажется, что очень удобно отслеживать статус. Но обычно он важен для отображения данных в компоненте: когда мы послали запрос на данные и ожидаем ответа, нужно показывать лоадер; когда данные загружены с ошибкой нужно показать ошибку. Поэтому необходимо каким-то образом прокидывать эти статусы в компонент. Как было сказано выше, геттеров нет. Но есть хранилища! Можно создать хранилище, сочетающее в себе все статусы:

/* МОДЕЛЬ В EFFECTOR */// Создаем эффект, который делает GET-запрос на бекexport const getUsersFx = createEffect<void, IUserPayload[], Error>();// Создаем хранилище, в котором будет лежать ошибка, если GET-запрос зафейлится// I вариантexport const $fetchError = restore<Error>(getUsersFx.failData, null);// Создаем другое хранилище, содержащий всю информацию по GET-запросуexport const $usersGetStatus = combine({  loading: getUsersFx.pending,  error: $fetchError,  data: $users,});
/* КОМПОНЕНТ, ИСПОЛЬЗУЮЩИЙ ХРАНИЛИЩЕ */export const UserList3 = () => {  // Подключаем хранилище в компонент  const { loading, error, data } = useStore($usersGetStatus);  // Делаем запрос на бек на didMount  useEffect(() => {    getUsersFx();  }, []);  if (loading) {    return (      <div>Загрузка...</div>    );  }  if (error) {    return (      <div>        <span><b>Произошла ошибка: </b></span>        <span>{error.message}</span>      </div>    );  }  const usersItems = data.map((user) => (    <div key={user.id}>      <div>Name: {user.name}</div>      <div>Surname: {user.surname}</div>      <div>Age: {user.age}</div>      <div>Gender: {user.gender}</div>      <br/>    </div>  ));  return (    <div>      {usersItems}    </div>  );};

В приведённом выше варианте создания хранилища $fetchError был использован еще один метод Effector restore. Он позволяет создать хранилище, содержимое которого будет зависеть от события наступления события. Очень удобно использовать для очистки (сброса в начальное состояние) хранилища.

Создать хранилище $fetchError можно и через стандартный createStore :

// II вариантexport const $fetchError = createStore<Error | null>(null);$fetchError  .on(getUsersFx.fail, (_, { error }) => error)  .reset(getUsersFx.done);

Выходим на новый уровень - получаем удовольствие

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

1) Никаких лишних телодвижений для подписки на хранилище

При грамотно созданных моделях в компоненте не нужно страдать и отслеживать все свои телодвижения по обновлению хранилища. Подключили его в компонент он всегда актуален и перерисовывается при каждом обновлении хранилища. Никаких тебе Mobx-овых @action, @computed и прочей ручной настройки. Каеф :)

2) Меньше кода (и меньше размер)

Нет надобности создавать отдельные классы-модели, прописывать им интерфейсы. Создали хранилище, создали событие, подписали событие на хранилище готово!

И да, размер двух подключенных библиотек effector и effector-react составляет около 8 Кб (у Mobx сумма подключенных библиотек около 15-20 Кб)!

3) Минимальное взаимодействие компонента и хранилища

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

  1. Из компонента посылаем запрос на бек (потому что нужно отслеживать статус запроса).

  2. Здесь же получили данные и положили их в хранилище.

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

Effector позволяет реализовать работу напрямую: из хранилища послал запрос, в хранилище положил ответ. И наоборот. Это, например, очень удобно делается с помощью метода forward. Мы перенаправляем выход эффекта на вход события.

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

/* СОЗДАНИЕ СОБТИЯ */// Создаем событие на обновление хранилищаexport const update = createEvent<IUser>();// Хендлер на обновление хранилища (был описан выше)const updateStore = (state: IUser[], data: IUser) => {  const userIndex = state.findIndex((user) => user.id === data.id);  // Изменяем стейт  if (userIndex > -1) {    state.splice(userIndex, 1, data);  } else {    state.push(data);  }  // Возвращаем измененный стейт  return [...state];};// Подписываемся на обновление хранилища через хендлер$users  .on(update, updateStore)/**********************************************************//* СОЗДАНИЕ ЭФФЕКТА */// Создаем эффект для изменения данных о пользователе (Запрос на бек)export const updateUserFx = createEffect<IUserPayload, IUserPayload, Error>();// Асихронная функция запроса на бекconst updateUser = async (data: IUserPayload): Promise<IUserPayload> => {  const res = await axios({    url: `/users/${data.id}`,    method: 'PATCH',  });  return res.data;}// Привязываем к эффектуupdateUserFx.use(updateUser);/**********************************************************//* ПРЕОБРАЗОВАНИЕ ДАННХ */// Изменяем формат данных из хранилища в формат, необходимый для отправки запроса// (Удаляем искусственное поле onlineStatus)const serializeDataBeforeFetch = attach<  IUser,  Store<IUser[]>,  typeof updateUserFx  >({  effect: updateUserFx,  source: $users,  mapParams: (params: IUser, data: IUser[]) => {    const user = data.find((item) => item.id === params.id)!;    const userCopy = { ...user };    delete userCopy?.onlineStatus;    return userCopy;  },});// Связываем событие и функцию-преобразовательforward({  from: update,  to: serializeDataBeforeFetch,});

Пояснения по коду.

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

  1. update(...) вызываем событие на обновление хранилища из компонента.

  2. updateStore хранилище обновляется согласно переданному хендлеру.

  3. serializeDataBeforeFetch после обновления хранилища вызывается функция преобразования его данных в пейлоад. В ней используется метод Effector attach, позволяющий сделать forward с модификацией.

  4. updateUserFx вызываем эффект на обновление.

  5. updateUser делаем запрос на бек.

Вуаля!

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

4) Крутое и отзывчивое сообщество

Когда я поняла, что в документации и гугле нужных мне примеров нет от слова совсем, я решила действовать радикально и пойти в сообщество Effector в Telegram. Я задала один вопрос от хлебушка, на который я получила за один вечер... 5 разных вариантов решений от разных разработчиков! Причём решения были разные по уровню сложности, я могла выбрать любое из них, или скомбинировать и создать своё. Некоторые решения были очень хорошо расписаны и объяснены, некоторые содержали продуктовый код с примерами прямо на GitHub, некоторые содержали ссылки на воркшопы по Effector. В общем, я приятно удивлена, что есть такое классное сообщество, где ребята всячески поддерживают друг друга :)

Да и в целом в проекте я использовала версию Effector 21.5.0. То есть ребята мажорно обновляли свой проект 20 раз. Это очень существенно!

Best practices

[Для тех, кто хочет знать больше] Об этом есть статья в самой документации, но я кратко продублирую.

  • Названия хранилищ содержат символ $. Например, $users.

  • Названия эффектов содержат суффикс Fx. Например, getUsersFx.

  • Файловая структура. В корне исходников создается папка models, внутри которой лежат все модели, работающие с Effector. У каждой модели есть два файла:

    • index.ts файл, где мы объявляем все хранилища, события, эффекты. Это файл начального объявления;

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

Итоги

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

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

Вместо послесловия

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

Подробнее..

Мониторинг бизнес-процессов Camunda

07.01.2021 18:21:18 | Автор: admin

Привет, Хабр.

Меня зовут Антон и я техлид в компании ДомКлик. Создаю и поддерживаю микросервисы позволяющие обмениваться данными инфраструктуре ДомКлик с внутренними сервисами Сбербанка.

Это продолжение цикла статей о нашем опыте использования движка для работы с диаграммами бизнес-процессов Camunda. Предыдущая статья была посвящена разработке плагина для Bitbucket позволяющего просматривать изменения BPMN-схем. Сегодня я расскажу о мониторинге проектов, в которых используется Camunda, как с помощью сторонних инструментов (в нашем случае это стек Elasticsearch из Kibana и Grafana), так и родного для Camunda Cockpit. Опишу сложности, возникшие при использовании Cockpit, и наши решения.

Когда у тебя много микросервисов, то хочется знать об их работе и текущем статусе всё: чем больше мониторинга, тем увереннее ты себя чувствуешь как в штатных, так и внештатных ситуациях, во время релиза и так далее. В качестве средств мониторинга мы используем стек Elasticsearch: Kibana и Grafana. В Kibana смотрим логи, а в Grafana метрики. Также в БД имеются исторические данные по процессам Camunda. Казалось бы, этого должно хватать для понимания, работает ли сервис штатно, и если нет, то почему. Загвоздка в том, что данные приходится смотреть в трёх разных местах, и они далеко не всегда имеют четкую связь друг с другом. На разбор и анализ инцидента может уходить много времени. В частности, на анализ данных из БД: Camunda имеет далеко не очевидную схему данных, некоторые переменные хранит в сериализованном виде. По идее, облегчить задачу может Cockpit инструмент Camunda для мониторинга бизнес-процессов.


Интерфейс Cockpit.

Главная проблема в том, что Cockpit не может работать по кастомному URL. Об этом на их форуме есть множество реквестов, но пока такой функциональности из коробки нет. Единственный выход: сделать это самим. У Cockpit есть Sring Boot-автоконфигурация CamundaBpmWebappAutoConfiguration, вот её-то и надо заменить на свою. Нас интересует CamundaBpmWebappInitializer основной бин, который инициализирует веб-фильтры и сервлеты Cockpit.

Нам необходимо передать в основной фильтр (LazyProcessEnginesFilter) информацию об URL, по которому он будет работать, а в ResourceLoadingProcessEnginesFilter информацию о том, по каким URL он будет отдавать JS- и CSS-ресурсы.

Для этого в нашей реализации CamundaBpmWebappInitializer меняем строчку:

registerFilter("Engines Filter", LazyProcessEnginesFilter::class.java, "/api/*", "/app/*")

на:

registerFilter("Engines Filter", CustomLazyProcessEnginesFilter::class.java, singletonMap("servicePath", servicePath), *urlPatterns)

servicePath это наш кастомный URL. В самом же CustomLazyProcessEnginesFilter указываем нашу реализацию ResourceLoadingProcessEnginesFilter:

class CustomLazyProcessEnginesFilter:       LazyDelegateFilter<ResourceLoaderDependingFilter>       (CustomResourceLoadingProcessEnginesFilter::class.java)

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

override fun replacePlaceholder(       data: String,       appName: String,       engineName: String,       contextPath: String,       request: HttpServletRequest,       response: HttpServletResponse) = data.replace(APP_ROOT_PLACEHOLDER, "$contextPath$servicePath")           .replace(BASE_PLACEHOLDER,                   String.format("%s$servicePath/app/%s/%s/", contextPath, appName, engineName))           .replace(PLUGIN_PACKAGES_PLACEHOLDER,                   createPluginPackagesString(appName, contextPath))           .replace(PLUGIN_DEPENDENCIES_PLACEHOLDER,                   createPluginDependenciesString(appName))

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

Но ведь не может быть всё так просто? В нашем случае Cockpit не способен работать из коробки на нескольких экземплярах приложения (например, в подах Kubernetes), так как вместо OAuth2 и JWT используется старый добрый jsessionid, который хранится в локальном кэше. Это значит, что если попытаться залогиниться в Cockpit, подключенный к Camunda, запущенной сразу в нескольких экземплярах, имея на руках ей же выданный jsessionid, то при каждом запросе ресурсов от клиента можно получить ошибку 401 с вероятностью х, где х = (1 1/количество_под). Что с этим можно сделать? У Cockpit во всё том же CamundaBpmWebappInitializer объявлен свой Authentication Filter, в котором и происходит вся работа с токенами; надо заменить его на свой. В нём из кеша сессии берём jsessionid, сохраняем его в базу данных, если это запрос на авторизацию, либо проверяем его валидность по базе данных в остальных случаях. Готово, теперь мы можем смотреть инциденты по бизнес-процессам через удобный графический интерфейс Cockpit, где сразу видно stacktrace-ошибки и переменные, которые были у процесса на момент инцидента.

И в тех случаях, когда причина инцидента ясна по stacktrace исключения, Cockpit позволяет сократить время разбора инцидента до 3-5 минут: зашел, посмотрел, какие есть инциденты по процессу, глянул stacktrace, переменные, и вуаля инцидент разобран, заводим баг в JIRA и погнали дальше. Но что если ситуация немного сложнее, stacktrace является лишь следствием более ранней ошибки или процесс вообще завершился без создания инцидента (то есть технически всё прошло хорошо, но, с точки зрения бизнес-логики, передались не те данные, либо процесс пошел не по той ветке схемы). В этом случае надо снова идти в Kibana, смотреть логи и пытаться связать их с процессами Camunda, на что опять-таки уходит много времени. Конечно, можно добавлять к каждому логу UUID текущего процесса и ID текущего элемента BPMN-схемы (activityId), но это требует много ручной работы, захламляет кодовую базу, усложняет рецензирование кода. Весь этот процесс можно автоматизировать.

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

Во-первых, необходимо зарегистрировать customPreBPMNParseListeners в текущем processEngine Camunda. В слушателе переопределить методы parseStartEvent (добавление слушателя на событие запуска верхнеуровневого процесса) и parseServiceTask (добавление слушателя на событие запуска ServiceTask).

В первом случае мы создаем Sleuth-контекст:

customContext[X_B_3_TRACE_ID] = businessKeycustomContext[X_B_3_SPAN_ID] = businessKeyHalfcustomContext[X_B_3_PARENT_SPAN_ID] = businessKeyHalfcustomContext[X_B_3_SAMPLED] = "0" val contextFlags: TraceContextOrSamplingFlags = tracing.propagation()       .extractor(OrcGetter())       .extract(customContext)val newSpan: Span = tracing.tracer().nextSpan(contextFlags)tracing.currentTraceContext().newScope(newSpan.context())

и сохраняем его в переменную бизнес-процесса:

execution.setVariable(TRACING_CONTEXT, sleuthService.tracingContextHeaders)

Во втором случае мы его из этой переменной восстанавливаем:

val storedContext = execution       .getVariableTyped<ObjectValue>(TRACING_CONTEXT)       .getValue(HashMap::class.java) as HashMap<String?, String?>val contextFlags: TraceContextOrSamplingFlags = tracing.propagation()       .extractor(OrcGetter())       .extract(storedContext)val newSpan: Span = tracing.tracer().nextSpan(contextFlags)tracing.currentTraceContext().newScope(newSpan.context())

Нам нужно трейсить логи вместе с дополнительными параметрами, такими как activityId (ID текущего BPMN-элемента), activityName (его бизнес-название) и scenarioId (ID схемы бизнес-процесса). Такая возможность появилась только с выходом Sleuth 3.

Для каждого параметра нужно объявить BaggageField:

companion object {   val HEADER_BUSINESS_KEY = BaggageField.create("HEADER_BUSINESS_KEY")   val HEADER_SCENARIO_ID = BaggageField.create("HEADER_SCENARIO_ID")   val HEADER_ACTIVITY_NAME = BaggageField.create("HEADER_ACTIVITY_NAME")   val HEADER_ACTIVITY_ID = BaggageField.create("HEADER_ACTIVITY_ID")}

Затем объявить три бина для обработки этих полей:

@Beanopen fun propagateBusinessProcessLocally(): BaggagePropagationCustomizer =       BaggagePropagationCustomizer { fb ->           fb.add(SingleBaggageField.local(HEADER_BUSINESS_KEY))           fb.add(SingleBaggageField.local(HEADER_SCENARIO_ID))           fb.add(SingleBaggageField.local(HEADER_ACTIVITY_NAME))           fb.add(SingleBaggageField.local(HEADER_ACTIVITY_ID))       }/** [BaggageField.updateValue] now flushes to MDC  */@Beanopen fun flushBusinessProcessToMDCOnUpdate(): CorrelationScopeCustomizer =       CorrelationScopeCustomizer { builder ->           builder.add(SingleCorrelationField.newBuilder(HEADER_BUSINESS_KEY).flushOnUpdate().build())           builder.add(SingleCorrelationField.newBuilder(HEADER_SCENARIO_ID).flushOnUpdate().build())           builder.add(SingleCorrelationField.newBuilder(HEADER_ACTIVITY_NAME).flushOnUpdate().build())           builder.add(SingleCorrelationField.newBuilder(HEADER_ACTIVITY_ID).flushOnUpdate().build())       }/** [.BUSINESS_PROCESS] is added as a tag only in the first span.  */@Beanopen fun tagBusinessProcessOncePerProcess(): SpanHandler =       object : SpanHandler() {           override fun end(context: TraceContext, span: MutableSpan, cause: Cause): Boolean {               if (context.isLocalRoot && cause == Cause.FINISHED) {                   Tags.BAGGAGE_FIELD.tag(HEADER_BUSINESS_KEY, context, span)                   Tags.BAGGAGE_FIELD.tag(HEADER_SCENARIO_ID, context, span)                   Tags.BAGGAGE_FIELD.tag(HEADER_ACTIVITY_NAME, context, span)                   Tags.BAGGAGE_FIELD.tag(HEADER_ACTIVITY_ID, context, span)               }               return true           }       }

После чего мы можем сохранять дополнительные поля в контекст Sleuth:

HEADER_BUSINESS_KEY.updateValue(businessKey)HEADER_SCENARIO_ID.updateValue(scenarioId)HEADER_ACTIVITY_NAME.updateValue(activityName)HEADER_ACTIVITY_ID.updateValue(activityId)

Когда мы можем видеть логи отдельно по каждому бизнес-процессу по его ключу, разбор инцидентов проходит гораздо быстрее. Правда, всё равно приходится переключаться между Kibana и Cockpit, вот бы их объединить в рамках одного UI.

И такая возможность имеется. Cockpit поддерживает пользовательские расширения плагины, в Kibana есть Rest API и две клиентские библиотеки для работы с ним: elasticsearch-rest-low-level-client и elasticsearch-rest-high-level-client.

Плагин представляет из себя проект на Maven, наследуемый от артефакта camunda-release-parent, с бэкендом на Jax-RS и фронтендом на AngularJS. Да-да, AngularJS, не Angular.

У Cockpit есть подробная документация о том, как писать для него плагины.

Уточню лишь, что для вывода логов на фронтенде нас интересует tab-панель на странице просмотра информации о Process Definition (cockpit.processDefinition.runtime.tab) и странице просмотра Process Instance (cockpit.processInstance.runtime.tab). Для них регистрируем наши компоненты:

ViewsProvider.registerDefaultView('cockpit.processDefinition.runtime.tab', {   id: 'process-definition-runtime-tab-log',   priority: 20,   label: 'Logs',   url: 'plugin://log-plugin/static/app/components/process-definition/processDefinitionTabView.html'});ViewsProvider.registerDefaultView('cockpit.processInstance.runtime.tab', {   id: 'process-instance-runtime-tab-log',   priority: 20,   label: 'Logs',   url: 'plugin://log-plugin/static/app/components/process-instance/processInstanceTabView.html'});

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

<div cam-searchable-area (1)    config="searchConfig" (2)    on-search-change="onSearchChange(query, pages)" (3)    loading-state="Loading..." (4)    text-empty="Not found"(5)    storage-group="'ANU'"    blocked="blocked">   <div class="col-lg-12 col-md-12 col-sm-12">       <table class="table table-hover cam-table">           <thead cam-sortable-table-header (6)                  default-sort-by="time"                  default-sort-order="asc" (7)                  sorting-id="admin-sorting-logs"                  on-sort-change="onSortChanged(sorting)"                  on-sort-initialized="onSortInitialized(sorting)" (8)>           <tr>               <!-- headers -->           </tr>           </thead>           <tbody>           <!-- table content -->           </tbody>       </table>   </div></div>

  1. Атрибут для объявления компонента поиска.
  2. Конфигурация компонента. Здесь имеем такую структуру:

    tooltips = { //здесь мы объявляем плейсхолдеры и сообщения,                    //которые будут выводиться в поле поиска в зависимости от результата   'inputPlaceholder': 'Add criteria',   'invalid': 'This search query is not valid',   'deleteSearch': 'Remove search',   'type': 'Type',   'name': 'Property',   'operator': 'Operator',   'value': 'Value'},operators =  { //операторы, используемые для поиска, нас интересует сравнение строк     'string': [       {'key': 'eq',  'value': '='},       {'key': 'like','value': 'like'}   ]},types = [// поля, по которым будет производится поиск, нас интересует поле businessKey   {       'id': {           'key': 'businessKey',           'value': 'Business Key'       },       'operators': [           {'key': 'eq', 'value': '='}       ],       enforceString: true   }]
    

  3. Функция поиска данных используется как при изменении параметров поиска, так и при первоначальной загрузке.
  4. Какое сообщение отображать во время загрузки данных.
  5. Какое сообщение отображать, если ничего не найдено.
  6. Атрибут для объявления таблицы отображения данных поиска.
  7. Поле и тип сортировки по умолчанию.
  8. Функции сортировок.

На бэкенде нужно настроить клиент для работы с Kibana API. Для этого достаточно воспользоваться RestHighLevelClient из библиотеки elasticsearch-rest-high-level-client. Там указать путь до Kibana, данные для аутентификации: логин и пароль, а если используется протокол шифрования, то надо указать подходящую реализацию X509TrustManager.

Для формирования запроса поиска используем QueryBuilders.boolQuery(), он позволяет составлять сложные запросы вида:

val boolQueryBuilder = QueryBuilders.boolQuery();KibanaConfiguration.ADDITIONAL_QUERY_PARAMS.forEach((key, value) ->       boolQueryBuilder.filter()               .add(QueryBuilders.matchPhraseQuery(key, value)));if (!StringUtils.isEmpty(businessKey)) {   boolQueryBuilder.filter()           .add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.BUSINESS_KEY, businessKey));}if (!StringUtils.isEmpty(procDefKey)) {   boolQueryBuilder.filter()           .add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.SCENARIO_ID, procDefKey));}if (!StringUtils.isEmpty(activityId)) {   boolQueryBuilder.filter()           .add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.ACTIVITY_ID, activityId));}

Теперь мы прямо из Cockpit можем просматривать логи отдельно по каждому процессу и по каждой activity. Выглядит это так:


Таб для просмотра логов в интерфейсе Cockpit.

Но нельзя останавливаться на достигнутом, в планах идеи о развитии проекта. Во-первых, расширить возможности поиска. Зачастую в начале разбора инцидента business key процесса на руках отсутствует, но имеется информация о других ключевых параметрах, и было бы неплохо добавить возможность настройки поиска по ним. Также таблица, в которую выводится информация о логах, не интерактивна: нет возможности перехода в нужный Process Instance по клику в соответствующей ему строке таблицы. Словом, развиваться есть куда. (Как только закончатся выходные, я опубликую ссылку на Github проекта, и приглашаю туда всех заинтересовавшихся.)
Подробнее..

Экосистема JavaScript тренды в 2021 году. Всё ли так однозначно?

01.04.2021 12:23:08 | Автор: admin

В конце прошлого года на сайте State of JS 2020 было опубликовано исследование о состоянии экосистемы JavaScript в 2020 году с ретроспективой на предыдущие годы развития. Исследование основывалось на многочисленных опросах, в которых суммарно приняли участие более 23 тысяч человек из 137 стран мира.

Географическое распределение числа опрошенных.Географическое распределение числа опрошенных.

По результатам исследования можно сделать ряд прогнозов о том, какой будет экосистема JavaScript в 2021 году.

Языки, расширяющие возможности JavaScript

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

Тренды использования языка.Тренды использования языка.

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

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

Фреймворки

Наверно, многие помнят, когда в начале бурного развития фронтенд-экосистемы количество фреймворков росло, словно грибы после дождя. Часть из них уже канула в лету (Press F for Backbone.js, Marrionete.js, Prototype.js, [type anything].js), и за последние годы мы могли наблюдать стабилизацию позиций трёх основных конкурентов: React, Angular, Vue. И с каждым годом их доля присутствия на рынке только росла.

Тренды использования технологии.Тренды использования технологии.

Однако здесь не всё так однозначно. В 2019 году ворвался молодой Svelte, который за 2020 год в два раза увеличил свою долю использования среди разработчиков. И при этом в рейтингах проявления интереса и удовлетворённости от использования со стороны IT-сообщества Svelte занимает первое место. Фреймворк стал глотком свежего воздуха в подходе к созданию веб-приложений, и поэтому следует ожидать, что он будет наращивать своё присутствие в 2021 году всё больше и больше.

Тренды интереса к технологии.Тренды интереса к технологии.Тренды удовлетворённости от использования технологии.Тренды удовлетворённости от использования технологии.

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

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

Управление данными

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

Тренды использования технологии.Тренды использования технологии.

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

Тренды удовлетворённости от использования технологии.Тренды удовлетворённости от использования технологии.Тренды интереса к технологии.Тренды интереса к технологии.

Инструменты для тестирования

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

Mocha по-прежнему является достойной альтернативной. Но отсутствие явной привязанности к конкретному фреймворку смещает её на вторую позицию.

Jasmine является инструментом тестирования по умолчанию для проектов на Angular. И, возможно, спад его популярности связан с определенным спадом самого Angular.

Тренды использования технологии.Тренды использования технологии.

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

Тренды интереса к технологии.Тренды интереса к технологии.

Заключение

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

Подробнее..

Каверзные вопросы по Python

18.11.2020 12:16:11 | Автор: admin

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



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


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


Первый пример


Очень короткий. Про операторы и порядок их вычислений.


11 > 0 is True

С ходу можно интерпретировать это выражение следующим образом:


  • Приоритет операторов сравнения и is одинаков.
  • 11 > 0 это True.
  • Упрощаем выражение, получая True is True.
  • Всё выражение в итоге станет True.

На самом же деле это выражение вернёт False.


Вот еще пара похожих выражений.


0 < 0 == 0             # False1 in range(2) == True  # False

Также обратите внимание, что расстановка скобок изменит результат.


(11 > 0) is True         # True(0 < 0) == 0             # True(1 in range(2)) == True  # True

Как всегда, никакой магии в этих примерах нет. Приведенные выражения это chained comparisons, которые следует читать так: a op1 b op2 c ... y opN z эквивалентно a op1 b and b op2 c and ... y opN z.


Итак, ответ: исходное выражение эквивалентно (11 > 0) and (0 is True), что, очевидно, является ложью.


Остался вопрос про скобки? Расстановка скобок превращает выражение в обычное, не chained comparisons. То есть благодаря скобкам приоритет смещается на выражение в них, оно вычисляется первым, а затем выполняется вторая операция.


Второй пример


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


a = 123b = 123a == ba is b

Двойное равно проверяет объекты на равенство (и очевидно, что 123 == 123). А оператор is проверяет, что переменные ссылаются на один и тот же объект. a и b разные объекты, поэтому a is b вернёт False.


На самом деле в Python есть оптимизация, касающаяся небольших int-ов (от -5 до 256 включительно). Эти объекты загружаются в память интерпретатора при его запуске. Получается небольшой кеш. Из-за этого объект получается один, и результат будет True.


Аналогичный пример для числа > 256 сработает ожидаемо:


a = 257b = 257a == b  # Truea is b  # False

Второй пример. Продолжение


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


def test():    a = 257    b = 257    print(a is b)test()

257 не входит в кеш, и должно отобразиться False.


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


import disdis.dis(test)

Мы увидим следующие инструкции:


  2           0 LOAD_CONST               1 (257)              2 STORE_FAST               0 (a)  3           4 LOAD_CONST               1 (257)              6 STORE_FAST               1 (b)  4           8 LOAD_GLOBAL              0 (print)             10 LOAD_FAST                0 (a)             12 LOAD_FAST                1 (b)             14 COMPARE_OP               8 (is)             16 CALL_FUNCTION            1             18 POP_TOP             20 LOAD_CONST               0 (None)             22 RETURN_VALUE

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


Получается, что интерпретатор способен на подобные оптимизации: код предварительно анализируется, и некоторые константы переиспользуются (float-ы тоже, но не tuple-ы).
Итак, исходный код выведет True.


Третий пример


Этот вопрос однажды встретился мне на собеседовании. Он про классы и методы.


class C:    a = lambda self: self.b()    def __init__(self):        self.b = lambda self: Nonec = C()c.a()

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


Вспоминаем, что вызов метода применительно к экземпляру класса c.method() это то же самое, что вызов метода применительно к классу с первым аргументом в качестве экземпляра: C.method(c).


Теперь проверим, во что превратились параметры a и b класса C.


type(c.a)  # <class 'method'>type(c.b)  # <class 'function'>

Параметр а превратился в метод класса, такой же, как при определении метода внутри класса через def a(self).


А вот b это обычная функция, потому что она присваивается атрибуту экземпляра класса, а не определяется (как a) в момент создания класса.


Получается, что при вызове c.a() мы получаем C.a(c). Тут в качестве аргумента self в метод валидно передастся экземпляр класса. Далее внутри a вызывается функция b. Поскольку это обычная функция, то автоматической передачи экземпляра в качестве первого аргумента не произойдёт. И получается, что функция b вызовется без аргументов. Но она требует аргумент! Ведь она задана как lambda self: None. Не обращайте внимание, что аргумент называется self. Это сделано для дополнительного запутывания.


Итак, ответ:


Traceback (most recent call last):  File "<input>", line 1, in <module>  File "<input>", line 2, in <lambda>TypeError: <lambda>() missing 1 required positional argument: 'self'

Это происходит потому, что функции b не передан аргумент.


Четвёртый пример


Он про определение переменных в замыкании. Взят из списка хитрых вопросов с toptall:


def create_multipliers():    return [lambda x : i * x for i in range(5)]for multiplier in create_multipliers():    print(multiplier(2))

Кажется, ничего сложного. create_multipliers вернёт список из 5 функций (назовём их list_lamba_f). Каждая list_lamba_f будет умножать свой аргумент на свой индекс в результирующем массиве.


Получается, что на экране мы увидим:


02468

Дальнейший разбор предполагает, что вам знакомо замыкание (closure) при использовании вложенных функций (nested functions).


Свои коррективы в наивное объяснение выше вносит позднее связывание. Согласно ему, значение переменной из замыкания (это переменная i) вычисляется в тот момент, когда вызывается внутренняя функция (наши list_lamba_f).


Получается, что значение i в list_lamba_f вычисляется в момент вызова multiplier(2) в пятой строчке. Но в этот момент create_multipliers уже отработала целиком. и значение i это 4. То есть для всех list_lamba_f значение i равно 4.


Итак, ответ:


88888

Надеюсь, вам было интересно и понятно. С удовольствием почитаю комментарии с вопросами и задачами, которые кажутся вам полезными для понимания Python и просто занятными. Только не пишите ответ сразу!

Подробнее..

Как работает память в Python

03.12.2020 12:07:50 | Автор: admin

Что такое память и зачем она нужна?

Ни одна компьютерная программа не может работать без данных. А данные, чтобы программа имела к ним доступ, должны располагаться в оперативной памяти вашего компьютера. Но что такое оперативная память на самом деле? Когда произносишь это словосочетание, многие сразу представляют железную плашку, вставленную в материнскую плату, на которой написано что-то типа 16Gb DDR4 2666MHz. И они, разумеется, правы это действительно физический блок оперативной памяти, в котором, в итоге, все данные и оказываются. Но прежде, чем стать доступной внутри вашей программы, на память (как и на всё остальное аппаратное обеспечение) накладывается куча абстракций.

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

Во-вторых, внутри операционной системы, есть абстракция, называемая процесс, которая, в том числе, предназначена для изоляции и упрощения работы с ресурсами отдельными программами. Именно процесс делает вид, что независимо от количества запущенных программ и объема установленной в компьютер оперативной памяти, вашей программе доступно 4 ГБ RAM (на 32-разрядных системах) или сильно больше (на 64-разрядных).

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

  1. Попросите у процесса (ОС) выделить вам немного (одну или несколько страниц) оперативной памяти.

  2. Поработайте с ней.

  3. Верните ее в операционную систему.

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

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

Механизм памяти в Python

Python это язык с управляемой памятью. Причем для управления ею он использует несколько механизмов. Я постараюсь коротко осветить основные из них. Для зануд сразу подчеркну, что под Pythonом я понимаю конкретную реализацию CPython, а не какие-то другие версии, в которых всё может работать по-другому :)

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

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

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

Ремарка. Для больших объектов (больше 512 байт) Python выделяет память напрямую у ОС. Обычно таких объектов не очень много в рамках программы, и создаются они нечасто. Поэтому накладные расходы на создание таких объектов напрямую в RAM не так высоки.

Как же устроен аллокатор внутри? Он состоит из частей трёх видов:

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

  • Пул одна страница виртуальной памяти (обычно 4 килобайта).

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

Давайте поговорим о них подробнее. Когда Pythonу нужно расположить какой-то объект в оперативной памяти он ищет подходящую арену. Если такой нету, он запрашивает новую арену у операционной системы. Что значит подходящую? Арены организованы в двусвязный список и отсортированы от самой заполненной к самой свободной. Для добавления нового объекта выбирается САМАЯ ЗАПОЛНЕННАЯ арена. Почему такая странная логика? Это связано с политикой освобождения памяти. Арены единственное место, в котором происходит запрос и освобождение памяти в Python. Если какая-то арена полностью освобождается от объектов, она возвращается назад операционной системе и память освобождается. Таким образом, чем компактнее живут объекты в рамках арен, тем ниже общее потребление оперативной памяти программой.

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

Каждый пул может быть в одном из трех состояний used, full и empty. Full означает, что пул полностью занят и не может принять новые объекты. Empty что пул полностью пустой и может быть использован для хранения новых объектов (при этом пустой пул может быть использован для хранения объектов любого размера). Used это пул, который используется для хранения объектов, но при этом еще не заполнен полностью.

Python поддерживает списки пулов каждого из размеров и отдельно список пустых пулов. Если вы хотите создать новый объект, то Python сначала пытается найти used-пул с нужным размером блока и расположить объект там. Если used-пула нет, тогда берется empty-пул, и объект создается в нем (а сам пул из статуса empty переводится в статус used). Если и empty-пулов нет, то запрашивается новая арена.

Внутри пула живут блоки. Каждый блок предназначен для хранения одного объекта. В целях унификации размеры блоков фиксированы. Они могут быть размером 8, 16, 24, 32 . 512 байт. Если Вам нужно 44 байта для хранения объекта, то он будет расположен в блоке на 48 байт. Каждый пул содержит список свободных и занятых блоков (на самом деле, есть еще untouched-блоки, в которых никогда не жили данные, но по сценариям использования они похожи на free-блоки, поэтому не будем на них останавливаться подробно). Когда вы хотите разместить новый объект, берется первый свободный блок, и объект располагается в нем. Когда объект удаляется, его блок помещается в список свободных.

Время жизни объекта и причем тут GIL

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

  • Счетчик ссылок.

  • Механизм сборки мусора.

Каждый объект в Python это, в первую очередь, объект, унаследованный от базового класса PyObject. Этот самый PyObject содержит всего два поля: ob_refcnt количество ссылок, и ob_type указатель на другой объект, тип данного объекта.

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

К сожалению, счетчик ссылок подвержен проблемам в многопоточной среде. Состояния гонки могут приводит к некорректности обновления этого счетчика из разных потоков. Чтобы этого избежать CPython использует GIL Global Interpreter Lock. Каждый раз, когда происходит работа с памятью, GIL как глобальная блокировка препятствует выполнению этих действий одновременно из двух потоков. Он гарантирует, что сначала отработает один, потом другой.

Второй механизм очистки памяти это сборщик мусора (garbage collector), основанный на идее поколений. Зачем он нам нужен, если есть счетчик ссылок? Дело в том, что счетчик ссылок не позволяет отслеживать ситуации с кольцевыми зависимостями, когда объект A содержит ссылку на B, а B на A. В таком случае, даже если никто больше не ссылается на объекты A и B, их счетчик всё равно никогда не станет равным нулю и они никогда не удалятся через механизм счетчика ссылок. Для борьбы с такими зависимостями и появился второй механизм (как модуль gc, начиная с версии Python 1.5).

Работает он следующим образом: GC отслеживает объекты (объекты-контейнеры, которые могут содержать ссылки на другие объекты) и смотрит, доступны ли они из основного кода на Python. Если нет, то сборщик их удаляет. Если да оставляет.

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

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

Итоги

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

Что осталось за рамками статьи?

Мы не поговорили еще о многих вещах:

Оставлю эти вопросы заделом на следующие статьи :)

Подробнее..

Перевод Мониторинг и управление потоком задач в рамках взаимодействия микросервисов

14.01.2021 14:20:31 | Автор: admin


Ключевые тезисы:

  • Взаимодействие между компонентами напрямую друг с другом может привести к неожиданному поведению, в котором сложно будет разобраться разработчикам, операторам и бизнес-аналитикам.
  • Чтобы обеспечить устойчивость бизнеса, вам нужно видеть все возникающие в системе взаимодействия.
  • Добиться этого позволяют разные подходы: распределённая трассировка, обычно не учитывающая бизнес-аспекты; озёра данных, требующие заметных усилий по настройке получаемых срезов данных; отслеживание процессов, когда вам приходится моделировать интересующий поток задач; контроль и анализ процессов (process mining), позволяющие исследовать поток задач; и вплоть до оркестрации, в которой прозрачность процессов уже имеется.
  • Мы поговорим о том, что вам нужно балансировать между оркестрацией и хореографией микросервисной архитектуры, чтобы понимать, управлять и менять свою систему.


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

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


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


Хореографируемый поток событий.

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


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

Потеря прозрачности потока событий


Я хочу сосредоточиться на вопросе, возникающем чаще всего, когда я участвую в обсуждениях этой архитектуры: Как избежать потери прозрачности (и, вероятно, контроля) потока событий? В одном из исследований сотрудники Camunda (где я работаю) спрашивали об использовании микросервисов. 92 % респондентов как минимум рассматривали их, а 64 % уже использовали в том или ином виде. Это уже не хайп. Но в том исследовании мы также спрашивали и о трудностях, и получили чёткое подтверждение наших опасений: чаще всего нам говорили о потере сквозной прозрачности бизнес-процессов, задействующих многочисленные сервисы.


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

Обеспечение прозрачности


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

  1. Распределённая трассировка (например, Zipkin или Jaeger).
  2. Озёра данных или аналитические инструменты (например, Elastic).
  3. Контроль и анализ процессов (process mining) (например, ProM).
  4. Отслеживание с помощью автоматизации потоков задач (например, Camunda).

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

Распределённая трассировка


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


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

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

  • В трассировке трудно разобраться неспециалистам. Все мои попытки показать трассировки людям, далёким от техники, с треском провалились. Оказалось гораздо лучше потратить время на перерисовку той же информации в виде блоков и стрелок. И даже если вся эта информация о вызовах методов и сообщениях очень полезна для понимания схем взаимодействия, она оказывается слишком подробна для понимания ситуации с межсервисными бизнес-процессами.
  • Для управления избыточным объёмом подробных данных в распределённой трассировке применяется так называемое сэмплирование. Это означает, что собирается лишь небольшая доля запросов, и обычно больше 90 % всех запросов вообще не регистрируется. Об этом хорошо написано в статье Three Pillars with Zero Answers towards a New Scorecard for Observability. Так что у вас никогда не будет полной картины происходящего.

Озёра данных или аналитические инструменты


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


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


Пример интерфейса мониторинга событий.

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


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

Инструменты для контроля и анализа процессов (process mining)


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

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


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


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

Отслеживание с помощью автоматизации потоков задач


Другой интересный подход моделирование потоков задач с последующим развёртыванием и исполнением посредством модуля управления. Эта модель особенная в том смысле, что она только отслеживает события, а сама ничего активно не делает. То есть ничем не управляет только регистрирует. Я рассказывал об этом на Kafka Summit San Francisco 2018, используя для демонстрации Apache Kafka и open source-модуль управления рабочими процессами Zeebe.


Эта возможность особенно интересна. В сфере модулей управления немало инноваций, что приводит к появлению инструментов, которые компактны, удобны для разработки и высокомасштабируемы. Я писал об этом в статье Events, Flows and Long-Running Services: A Modern Approach to Workflow Automation. К очевидным недостаткам относится необходимость предварительного моделирования потока задач. Но зато эту модель можно исполнять с помощью модуля управления процессами, в отличие от мониторинга событий. По сути, вы запускаете экземпляры процессов для входящих событий или соотносите события с каким-либо экземпляром. Также это позволяет проверить, соответствует ли реальность вашей модели.

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


Пример мониторинга потока задач.

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

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

Перспективы для бизнеса


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

От отслеживания к управлению


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

Например, вы можете начать с мониторинга таймаутов в сквозных процессах. Когда происходит таймаут, автоматически выполняется некое действие. В примере ниже, спустя 14 дней мы уведомим клиента о задержке, но всё ещё будем ждать. А через 21 день отменим заказ.


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

Оркестрация


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

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

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

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


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

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


Заключение


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

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

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

Микросервисы на монолите

15.12.2020 12:20:26 | Автор: admin

Всем привет!

Скажу сразу, эта статья не про очередное переписывание монолита на микросервисы, а о применении микросервисных практик в рамках существующего проекта с использованием интересных, как мне кажется, подходов. Наверное, уже нет смысла объяснять, почему многие проекты активно используют микросервисную архитектуру. Сегодня в IT возможности таких инструментов как Docker, Kubernetes, Service Mesh и прочих сильно меняют наше представление об архитектуре современного приложения, вынуждая пересматривать подходы и переписывать целые проекты на микросервисы. Но так ли это необходимо для всех частей проекта?

В нашем проекте есть несколько систем, которые писались в те времена, когда преимущества микросервисного подхода были не столь очевидны, а инструментов, позволяющих использовать такой подход, было очень мало, и переписывать системы полностью просто нецелесообразно. Для адаптации к новой архитектуре мы решили в части задач использовать асинхронный подход, а также перейти к хранению части данных на общих сервисах. Само приложение при этом осталось на Django (API для SPA). При переходе на k8s деплой приложения был разбит на несколько команд: HTTP-часть (API), Celery-воркеры и RabbitMQ-консьюмеры. Причем было именно три развёртывания, то есть все воркеры, как и консьюмеры, крутились в одном контейнере. Быстрое и простое решение. Но этого оказалось недостаточно, так как это решение не обеспечивало нужный уровень надежности.

Начнем с RabbitMQ-консьюмеров. Основная проблема была в том, что внутри контейнера стартовал воркер, который запускал много потоков на каждый консьюмер, и пока их было пару штук, всё было хорошо, но сейчас их уже десятки. Решение нашли простое: каждый консьюмер вывели в отдельную команду manage.py и деплоим отдельно. Таким образом, у нас несколько десятков k8s-развёртываний на одном образе, с разными параметрами запуска. Ресурсы мы тоже выставляем для каждого консьюмера отдельно, и реальное потребление у консьюмеров достаточно невысокое. В результате для одного репозитория у нас десятки реальных отдельных сервисов в k8s, которые можно масштабировать, и при этом разработчикам намного удобнее работать только с одним репозиторием. То же самое и для развёртываний Сelery.

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

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

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

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

Проанализировав наш API, мы разбили его на несколько групп:

  • Внешний API (методы для других сервисов).

  • Внутренний API (методы для фронтенда).

  • Некритичные API-методы, которые зависимы от внешних сервисов, но не влияют на работу системы (статистика, счетчики и т. п.)

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

В конечном итоге наша система, имея один репозиторий и Django под капотом, раздроблена благодаря Kubernetes на 42 сервиса, 5 из которых делят HTTP-трафик, а остальные 37 консьюмеры и Celery-задачи. И в таком виде она может быть актуальна еще пару лет, несмотря на использование относительно старого стека технологий.

Подробнее..

Перевод Почему нужно автоматизировать эксплуатацию

04.03.2021 12:16:30 | Автор: admin

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

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

Gartner, 2019-2021 I&O Automation Benchmark Report

Пять преимуществ автоматизации инфраструктуры и эксплуатации

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

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

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

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

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

Создание основы для современной эксплуатации

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

Gartner, Predicts 2019: IT Operations

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

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

Оркестрация и инфраструктура-как-код

Универсальные коннекторы, прямые интеграции и возможности бесшовно исполнять API (.NET-сборки, RESTful-сервисы, командные строки, хранимые процедуры и т.д.) всё это лежит в основе оркестрации, то есть способности централизованно интегрировать и управлять практически любым процессом.

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

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

AIOps

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

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

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

Гиперавтоматизация

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

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

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

NoOps будущее ИТ-эксплуатации?

Такие технологии, как AIOps, облачные вычисления (SaaS/IaaS) и гиперавтоматизация позволяют ИТ-департаментам сильно уменьшать количество ручных процессов и делают возможными полностью автоматизированные автономные среды.

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

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

Deloitte, NoOps In A Serverless World

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

Как сказал один из авторов статьи в Deloitte: NoOps на самом деле недостижима, но это эффективный лозунг.

Подробнее..

PGHero дашборд для мониторинга БД PostgeSQL

23.03.2021 12:19:49 | Автор: admin

Всем привет. Сегодня я бы хотел поделиться рецептом установки утилиты PGHero с подключением нескольких баз данных. PGHero это простенькая утилита, написанная на Ruby, с минималистичным дашбордом для мониторинга производительности БД PostgreSQL.

Что может показать нам PGHero:

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

  • активные в данный момент запросы;

  • информацию о таблицах: занимаемое на диске место, даты последних запусков VACUUM и ANALYSE;

  • информацию об индексах: занимаемое на диске место, наличие дублируемых/неиспользуемых индексов. Также может порекомендовать добавить индекс при наличии сложных запросов с Seq Scan;

  • статистику по открытым подключениям к БД;

  • вывод основных настроек БД, влияющих на производительность (shared_buffers, work_mem, maintenance_work_mem и т.д.)


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

Выглядит это в интерфейсе PGHero вот так:


Настройка баз данных

Следующие шаги нужно проделать для каждой БД, которую мы собираемся подключать к PGHero.

Запросы нужно выполнять под суперпользователем.

  1. Устанавливаем расширение pg_stat_statements (если еще не установлено):

Откройте файл postgresql.conf в текстовом редакторе и измените строку shared_preload_libraries:

shared_preload_libraries = 'pg_stat_statements'pg_stat_statements.track_utility = false

Перезапускаем сервер PostgreSQL:

sudo service postgresql restart

Создаем расширение и сбрасываем статистику:

create extension pg_stat_statements;select pg_stat_statements_reset();
  1. Создаем в БД отдельного пользователя для PGHero (чтобы не давать утилите полные права над базой).

В следующем запросе заменяем эти значения в угловых скобках на свои:

<pghero_password> пароль для пользователя pghero;

<db_name> имя вашей БД;

<migrations_user> имя основной роли с доступом к текущей БД.

CREATE SCHEMA pghero;-- view queriesCREATE OR REPLACE FUNCTION pghero.pg_stat_activity() RETURNS SETOF pg_stat_activity AS$$  SELECT * FROM pg_catalog.pg_stat_activity;$$ LANGUAGE sql VOLATILE SECURITY DEFINER;CREATE VIEW pghero.pg_stat_activity AS SELECT * FROM pghero.pg_stat_activity();-- kill queriesCREATE OR REPLACE FUNCTION pghero.pg_terminate_backend(pid int) RETURNS boolean AS$$  SELECT * FROM pg_catalog.pg_terminate_backend(pid);$$ LANGUAGE sql VOLATILE SECURITY DEFINER;-- query statsCREATE OR REPLACE FUNCTION pghero.pg_stat_statements() RETURNS SETOF pg_stat_statements AS$$  SELECT * FROM public.pg_stat_statements;$$ LANGUAGE sql VOLATILE SECURITY DEFINER;CREATE VIEW pghero.pg_stat_statements AS SELECT * FROM pghero.pg_stat_statements();-- query stats resetCREATE OR REPLACE FUNCTION pghero.pg_stat_statements_reset() RETURNS void AS$$  SELECT public.pg_stat_statements_reset();$$ LANGUAGE sql VOLATILE SECURITY DEFINER;-- improved query stats reset for Postgres 12+ - delete for earlier versionsCREATE OR REPLACE FUNCTION pghero.pg_stat_statements_reset(userid oid, dbid oid, queryid bigint) RETURNS void AS$$  SELECT public.pg_stat_statements_reset(userid, dbid, queryid);$$ LANGUAGE sql VOLATILE SECURITY DEFINER;-- suggested indexesCREATE OR REPLACE FUNCTION pghero.pg_stats() RETURNSTABLE(schemaname name, tablename name, attname name, null_frac real, avg_width integer, n_distinct real) AS$$  SELECT schemaname, tablename, attname, null_frac, avg_width, n_distinct FROM pg_catalog.pg_stats;$$ LANGUAGE sql VOLATILE SECURITY DEFINER;CREATE VIEW pghero.pg_stats AS SELECT * FROM pghero.pg_stats();-- create userCREATE ROLE pghero WITH LOGIN ENCRYPTED PASSWORD '<pghero_password>';GRANT CONNECT ON DATABASE <db_name> TO pghero;ALTER ROLE pghero SET search_path = pghero, pg_catalog, public;GRANT USAGE ON SCHEMA pghero TO pghero;GRANT SELECT ON ALL TABLES IN SCHEMA pghero TO pghero;-- grant permissions for current sequencesGRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO pghero;-- grant permissions for future sequencesALTER DEFAULT PRIVILEGES FOR ROLE <migrations_user> IN SCHEMA public GRANT SELECT ON SEQUENCES TO pghero;

Установка и запуск PGHero

Допустим, у нас есть три таблицы: db_one, db_two и db_three. Мы хотим по всем трем отображать статистику в PGHero (вместе с историей запросов и размеров таблиц). Важный момент: для хранения истории запросов и размеров таблиц нужно завести в одной из баз данных отдельные таблицы, где будет храниться эта статистика.

CREATE TABLE "pghero_query_stats" (  "id" bigserial primary key,  "database" text,  "user" text,  "query" text,  "query_hash" bigint,  "total_time" float,  "calls" bigint,  "captured_at" timestamp);CREATE INDEX ON "pghero_query_stats" ("database", "captured_at");CREATE TABLE "pghero_space_stats" (  "id" bigserial primary key,  "database" text,  "schema" text,  "relation" text,  "size" bigint,  "captured_at" timestamp);CREATE INDEX ON "pghero_space_stats" ("database", "captured_at");

Мы будем хранить эти таблицы в БД db_one (хотя можно завести отдельную базу для этой статистики). Далее создаем на сервере файл конфигурации pghero.yml со следующим содержимым (подставляем актуальные настройки):

# Конфигурационные урлы для наших БДdatabases:  db_one:    url: postgres://pghero:secret_pass@mydomain.ru:53001/db_one  db_two:    url: postgres://pghero:secret_pass@mydomain.ru:53001/db_two    capture_query_stats: db_one  db_three:    url: postgres://pghero:secret_pass@mydomain.ru:53001/db_three    capture_query_stats: db_one# Минимальная длительность запросов (в секундах), которые будут считаться долгимиlong_running_query_sec: 60# Минимальная длительность запросов (в миллисекундах), которые будут считаться медленнымиslow_query_ms: 250# Минимальное кол-во вызовов запросов, которые будут считаться медленнымиslow_query_calls: 100# Минимальное количество соединений для показа предупрежденияtotal_connections_threshold: 100# Таймаут для explain-запросовexplain_timeout_sec: 10# Нормализация запросов (замена значений запроса нумерованными параметрами)filter_data: true# Basic авторизацияusername: pgheropassword: secret_pass# Таймзонаtime_zone: "Europe/Moscow"

Переходим к установке. Документация предлагает нам несколько способов:

  1. Docker-контейнер;

  2. отдельная служба на Linux;

  3. gem-пакет Ruby;

Мы будем использовать первый способ запуск в виде Docker-контейнера. Для этого в папке с файлом конфигурации pghero.yml нужно добавить Docker-файл с таким содержимым:

docker build -t mypghero .docker run -ti -p 12345:8080 mypghero

Теперь собираем образ на основе Docker-файла и запускаем контейнер на нужном порту:

docker build -t mypghero .docker run -ti -p 12345:8080 mypghero

Теперь дашборд должен быть доступен по адресу http://123.45.67.89/12345. Не забывайте про basic-авторизацию, логин и пароль мы указывали в pghero.yml.


Запуск cron-jobs для сохранения истории

Последний этап: нужно настроить автозапуск по крону скриптов для сохранения в БД истории по запросам (capture_query_stats) и размерам таблиц (capture_space_stats).

Документация рекомендует запускать capture_query_stats раз в 5 минут, а capture_space_stats раз в сутки (но тут нужно решать по ситуации). Запускаем в командной строке crontab -e и добавляем строки для запуска скриптов:

*/5 * * * *     /usr/bin/docker run --rm my-pghero bin/rake pghero:capture_query_stats15 2 * * *     /usr/bin/docker run --rm my-pghero bin/rake pghero:capture_space_stats

Вот и всё. Спасибо за внимание.

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

Подробнее..

Перевод BFcache

01.12.2020 14:07:50 | Автор: admin
BFcache технология оптимизации работы браузера, обеспечивающая мгновенную отдачу ранее просмотренной страницы при помощи кнопок Вперед и Назад. Этот паттерн значительно улучшает пользовательский опыт, особенно у пользователей, обладающих слабенькими устройствами или просматривающих сайт из-под медленных сетей. Люди пользуются кнопкой возврата, возможно, даже чаще, чем вы думаете. А если так, то зачем сразу выбрасывать страницу из памяти браузера, а спустя мгновение тратить трафик на её повторное открытие?

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

Поддержка браузерами


BFcache уже много лет поддерживается как в Firefox, так и в Safari на всех устройствах.

Начиная с версии 86, Chrome поддерживает BFcache для межсайтовой навигации на Android для небольшого процента пользователей. В Chrome 87 поддержка BFcache будет развернута для всех пользователей Android при работе с межсайтовой навигацией, с планами поддерживать навигацию на том же сайте в ближайшем будущем.

Основы BFcache


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

BFcache это in-memory кеш, в котором хранится снепшот страницы (включая js-heap), после того как пользователь покидает url этой страницы. Когда вся страница находится в памяти, браузер может быстро и легко восстановить ее, если пользователь решит вернуться.

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

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

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

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


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

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

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

API для работы с BFcache


Не смотря на то, что BFcache это оптимизация, выполняемая браузерами автоматически, разработчикам важно знать, когда это происходит, чтобы они могли оптимизировать свои страницы для этого и соответствующим образом скорректировать любые показатели при измерения производительности. Основными событиями, используемыми для работы BFcache, являются события перехода страницы: pageshow и pagehide. Они поддерживаются почти во всех современных браузерах. Новые события жизненного цикла страницы, freeze и resume, также отправляются, когда страницы записываются или считываются из BFcache, а также в некоторых других ситуациях. Например, когда фоновая вкладка замораживается для минимизации использования ЦП.

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

Восстановление страницы из BFcache


Событие pageshow срабатывает сразу после события загрузки, когда страница загружается изначально, и каждый раз, когда страница восстанавливается из BFcache. У события pageshow есть свойство persisted, которое будет истинным, если страница была восстановлена из BFcache (и false, если нет). Вы можете использовать свойство persisted, чтобы отличить обычную загрузку страницы от восстановления BFcache. Например:

window.addEventListener ('pageshow', function (event) {  if (event.persisted) {    console.log ('Эта страница восстановлена из BFcache.');  } else {    console.log ('Эта страница загрузилась по запросу.');  }});

В браузерах, поддерживающих Page Lifecycle API, событие resume также срабатывает при восстановлении страниц из BFcache (непосредственно перед событием pageshow), хотя оно также запускается, когда пользователь повторно посещает замороженную фоновую вкладку. Если вы хотите восстановить состояние страницы после ее замораживания (включая страницы в BFcache), вы можете использовать событие возобновления, но если вы хотите измерить процент попаданий BFcache вашего сайта, вам нужно будет использовать событие pageshow. В некоторых случаях вам может понадобиться использовать оба события.

Запись страницы в BFcache


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

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

window.addEventListener('pagehide', function(event) {  if (event.persisted === true) {   console.log('*Возможно*, страница записана в BFcache.');  } else {    console.log('Эта страница будет выгружена нормально и будет удалена из памяти.');  }});

Как и в предыдущем примере для браузеров с поддержкой Page Lifecycle API, событие freeze срабатывает сразу после события pagehide (если свойство persisted события истинно), но опять же это означает, что браузер намерен закэшировать страницу. Возможно, кеширование не произойдет.

Помните про оптимизацию страниц


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

Никогда не используйте событие unload


Первый шаг при оптимизации страниц для BFcache отказ от использования события unload.

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

FireFox не добавляет страницу в BFcache, если в ней используется событие unload. Safari же пытается кешировать такие страницы, но оперирует этим событием только когда пользователь действительно покидает страницу. Chorme при событии unload ведет себя примерно как Safari.

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

Lighthouse v6.2.0 добавил в аудит пункт no-unload-listeners, предупреждающий разработчиков, если какой-либо JavaScript на их страницах (в том числе из сторонних библиотек) добавляет прослушивание события unload.

Иными словами, если вы хотите повысить скорость работы вашего сайта в FireFox, Safari, Chrome, перестаньте слушать событие unload. Вместо него слушайте событие pagehide.

С осторожностью используйте событие beforeunload


Событие beforeunload не воспрепятствует попаданию страницы в BFcache в Chrome и Safari, зато воспрепятствует в случае FireFox. Используйте это событие только при необходимости.
Однако, у прослушивания этого события есть вполне конкретные кейсы применения. Например, предупредить не сохранившего форму пользователя о том, что он будет вынужден заполнить форму снова, если покинет страницу. В этом примере рекомендуется слушать событие только при заполнении пользователем данных.

Анти-паттерн. Событие прослушивается безусловно.

window.addEventListener ('beforeunload', (событие) => {  if (pageHasUnsavedChanges ()) {    event.preventDefault ();    return event.returnValue = 'Вы уверены, что хотите выйти?';  }});

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

unction beforeUnloadListener (событие) {  event.preventDefault ();  return event.returnValue = 'Вы уверены, что хотите выйти?';};// Функция, которая запускает коллбек, когда на странице есть несохраненные изменения.onPageHasUnsavedChanges (() => {  window.addEventListener ('beforeunload', beforeUnloadListener);});// Функция, которая запускает коллбек, когда несохраненные изменения страницы разрешены.onAllChangesSaved (() => {  window.removeEventListener ('beforeunload', beforeUnloadListener);});

Избегайте использования window.opener


В некоторых браузерах (включая Chrome версии 86), если страница была открыта с помощью window.open () или по ссылке с target="_blank" без указания rel=noopener, открывающая страница будет содержать ссылку на оконный объект открытой страницы. Такие страницы не попадут в BFcache, т. к. существует угроза безопасности и из-за вероятности поломки других страниц, пытающихся получить к ним доступ.

Закрывайте соединения перед уходом пользователя со страницы


Как упоминалось выше, при записи страницы в BFcache, все запланированные js-задачи приостанавливаются, а затем возобновляются, когда страница считывается от туда. Если эти задачи обращаются только к API-интерфейсам, изолированным только для текущей страницы (например, DOM), то приостановка выполнения этих задач, пока страница не видна пользователю, не вызовет никаких проблем. Но, если имеют место задачи связаны с API-интерфейсами, которые также доступны с других страниц того же домена (например, IndexedDB, Web Locks, WebSockets и т. д.), можно столкнуться с проблемой, т. к. поскольку приостановка этих задач может помешать запуску кода на других вкладках.

Иными словами, браузер даже не будет пытаться записать в BFcache страницы, где:

  • незавершена транзакция к IndexDB;
  • выполняется fetch () или XMLHttpRequest;
  • открыто соединение по WebSocket или WebRTC.

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

Тесты вам в помощь


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

В настоящее время в Chrome страница может оставаться в BFcache до трех минут, что должно быть достаточно времени для запуска теста (с использованием такого инструмента, как Puppeteer или WebDriver), чтобы убедиться, что свойство события pageshow истинно после перехода. от страницы, а затем нажмите кнопку назад.

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

В Chrome в настоящее время BFcache включен только на мобильных устройствах. Чтобы протестировать bfcache на десктопе, вам необходимо включить флаг # back-forward-cache.

Отключение BFcache


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

Cache-Control: no-store

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

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

Кроме того, в Chrome в настоящее время возможен отказ на уровне пользователя с помощью флага # back-forward-cache, а также отказ на основе корпоративной политики.

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

Влияние на аналитику


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

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

Если вы не хотите, чтобы количество просмотров страниц уменьшалось из-за включения BFcache, вы можете сообщить о восстановлении BFcache как о просмотрах страниц (рекомендуется), прослушав событие pageshow и проверив свойство persisted.

В следующем примере показано, как это сделать с помощью Google Analytics (или любых аналогичных инструментов):

// Отправляем просмотр страницы при первой загрузке страницы.gtag ('event', 'page_view')window.addEventListener ('pageshow', function (event) {  if (event.persisted === true) {    // Отправить еще один просмотр страницы, если страница восстановлена из bfcache.    gtag ('event', 'page_view')  }});

Измерения производительности


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

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

Есть несколько способов справиться с этой проблемой. Один из них аннотировать все показатели загрузки страницы соответствующим типом навигации: навигация, перезагрузка, back_forward или предварительная визуализация. Это позволит вам продолжать следить за своей производительностью в рамках этих типов навигации. Этот подход рекомендуется для показателей загрузки страницы, не ориентированных на пользователя, таких как время до первого байта (TTFB). Для показателей, ориентированных на пользователя, таких как Core Web Vitals, лучший вариант сообщить значение, которое более точно отражает то, что испытывает пользователь.

Внимание: тип навигации back_forward в Navigation Timing API не следует путать с восстановлением BFcache. Navigation Timing API только аннотирует загрузку страниц, тогда как считывание из BFcache повторно использует страницу, загруженную из предыдущей навигации.

Влияние на Core Web Vitals


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

Такие инструменты, как отчет о пользовательском опыте Chrome, которые собирают и предоставляют отчеты о показателях Core Web Vitals, скоро будут обновлены, чтобы обрабатывать считывание BFcache как отдельные посещения страницы в наборе данных.

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

  • Для Largest Contentful Paint (LCP) вы можете использовать дельту между отметкой времени события pageshow и отметкой времени следующего нарисованного кадра (поскольку все элементы в кадре будут нарисованы одновременно). Обратите внимание, что в случае восстановления bfcache LCP и FCP будут одинаковыми.
  • Для First Input Delay (FID) вы можете повторно добавить подписчики на события (те же, что используются полифилом FID) в событии pageshow и сообщить FID как задержку первого ввода после считывается из BFcache.
  • Для Cumulative Layout Shift (CLS) вы можете продолжать использовать существующий Performance Observer; все, что вам нужно сделать, это доиться текущее значение CLS, близкого (а лучше равного) 0.

Для получения дополнительной информации о том, как BFcache влияет на каждую метрику, обратитесь к отдельным страницам руководств по метрикам Core Web Vitals. А конкретный пример того, как реализовать версии этих показателей для bfcache в коде, можно найти в PR, добавив их в библиотеку JS web-vitals.

Начиная с версии 1, библиотека JavaScript web-vitals поддерживает восстановление BFcache в отчетных показателях. Разработчикам, использующим v1 или выше, не нужно обновлять свой код.
Подробнее..

Выдерни шнур, выдави стекло

06.04.2021 12:11:10 | Автор: admin

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

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

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

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

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

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

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

Ниже я распишу каждый шаг схемы и инструментарий, применяемый нами для каждого шага.

Шаг 1. Проверка нагрузки ядер процессоров на сервере БД (Load Average)

В большинстве случаев внешним проявлением проблемы в работе базы данных является возросшая нагрузка на ядра процессоров сервера БД. Лучшей метрикой для диагностирования этого факта я считаю LOAD AVERAGE. При этом мне больше нравится эта метрика в пересчете на ядро процессора сервера БД. LA (Load Average) на ядро более 1 плохо. Это значит, что запрос к БД ожидает какое-то время, прежде чем выполниться.

Для отслеживания этой метрики мы применяем систему мониторинга Zabbix с возможностью настройки оповещений в различные каналы. Пример такой метрики при возникновении аварии:

Шаг 2. Нагрузка на ядро процессора (LA) изменилась более чем на 30 % по отношению к тому же времени того же дня недели?

При возникновении серьёзной аварии LA обычно растёт лавинообразно. Тем не менее, даже повышение на 30 % признак какой-то аномалии. Если рост LA заметен и превышает заданный лимит, то самое время перейти к шагу 3 и проверить количество подключений к базе данных. Если же роста LA нет, то лучше переходить к шагу 27.

ШАГ 3. Проверка количества подключений приложений к БД и динамика его роста

Это ещё один маркер, который даёт чёткое понимание, что с БД что-то пошло не так. Тут в большей степени интересна динамика роста количества подключений к базе.

В качестве инструмента мониторинга используем всё тот же Zabbix. Максимально разрешенное количество подключений к БД можно узнать, выполнив команду SHOW_MAX_CONNECTIONS.

Шаг 4.Количество подключений к БД резко выросло за короткий промежуток времени?

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

Шаг 5. Увеличить количество подключений, поставив пулер подключений

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

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

Если же приложение работает всё-таки оптимально, то стоит задуматься над использованием пулера подключений (самый распространенный PGBouncer). Этот сервис берёт на себя управление подключениями и делает это достаточно эффективно. Приложение для взаимодействия с БД будет обращаться к PGBouncer, а тот в свою очередь будет перенаправлять запрос к БД через свои подключения. Подробнее об этой технологии можно почитать тут: http://personeltest.ru/aways/habr.com/ru/company/okmeter/blog/420429/.

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

Шаг 7. Проверка наличия блокировок и запросов в ожидании в базе данных

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

Способ проверки этой гипотезы динамика с количеством запросом к БД в статусе "IDLE IN TRANSACTION". Мы используем механизм периодического замера количества запросов в этом статусе, с выводом соответствующего графика в Grafana.

Шаг 8. В БД много блокировок и запросов в ожидании

Если ответ на этот вопрос да, то мы уже на полпути к решению. Отправляемся пробовать быстрое решение (шаг 9). Если же большого количества блокировок (повторяюсь большого количества! Блокировки это нормально, если их стандартное количество) и запросов В ожидании не наблюдается, то переходим к шагу 11.

ШАГ 9. Убить все блокировки в БД

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

Сделать это можно устранив процесс, вызвавший блокировку pg_terminate_backend(pid);.

Шаг 10. Ситуация исправилась?

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

Шаг 11. Получение списка выполняемых запросов, отсортированных по частоте и стоимости

Для этого необходимо выполнить команду PG_STAT_STAEMENTS. При вызове этой утилиты мы соберём журнал всех выполняемых в БД запросов и сможем оценить, какие из них на текущий момент времени занимают больше всего ресурсов (по частоте и стоимости выполнения). Рекомендуется сбросить ранее накопленную статистику командой PG_STAT_STAEMENTS_RESET () и начать её собирать заново, уже во время инцидента. Таким образом мы получим чистую статистику со стоимостью запросов уже на этапе инцидента.

Шаг 12. Изучить SlowLog (запросы, которые выполняются дольше критичного времени)

Эта операция, возможно, сразу даст ответ, какие запросы нужно оптимизировать, без более трудоемкого анализа результатов PG_STAT_STATEMENTS. Обычно мы оба шага (11 и 12) выполняем параллельно, чтобы сразу получить представление о нагрузке на БД. Еще один плюс SlowLog получив выборку из него за более длительный период времени, мы сможем вычленить запросы, которых раньше не было (т. е. они выполнялись быстро), но в какой-то момент стали выполняться медленно, скорее всего из-за аварии.

Шаг 13. Есть дорогие запросы, которые ранее выполнялись быстро?

На этом шаге главной проблемой является определение, с какой скоростью выполнялись запросы ранее (до аварии). Утилиты SlowLog и PG_STAT_STATEMENTS дадут понимание о скорости выполнения запросов сейчас. Но чтобы найти проблемный запрос, нужно понимать, с какой скоростью запросы выполнялись ранее. Для этой цели я рекомендую использовать дополнительные утилиты PGHero или NewRelic. Это достаточно удобные инструменты, которые позволяют понять динамику скорости выполнения запросов.

Если видна деградация в скорости выполнения каких-то определенных запросов, то переходим к шагу 14. Если зафиксировать снижение скорости выполнения какого-то запроса не удалось, то переходим к шагу 30.

Шаг 14. Проверка плана по дорогим запросам

Если вам удалось выделить конкретный запрос с ярко выраженной негативной динамикой скорости выполнения, то следующий шаг детальный анализ запроса посредством утилиты EXPLAIN ANALYZE. Подробно о том, как работает эта команда и как интерпретировать результаты её выполнения, можно ознакомиться тут: https://thoughtbot.com/blog/reading-an-explain-analyze-query-plan.

Шаг 15. Требуется новый индекс?

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

Шаг 16. Индекс есть, но не используется (а раньше использовался)

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

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

ШАГ 17. Обновление статистики таблиц базы данных

Простая операция, которая может спасти положение. Для сбора статистики большей точности достаточно выполнить команду ANALYZE илиset default_statistics_target = 500; ANALYZE . 500 это количественный коэффициент образца таблицы, который PostgreSQL выбирает для расчёта статистики. Подробнее о механизме работы статистики и карт видимости можно почитать тут: https://postgrespro.ru/docs/postgrespro/10/routine-vacuuming.

Шаг 18. Ситуация исправилась?

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

Шаг 19. Пересоздание индекса

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

Пересоздаем индекс. Сделать это можно командой REINDEX (подробнее читайте тут: https://postgrespro.ru/docs/postgrespro/9.5/sql-reindex).

Переходим к шагу 31.

Шаг 20. Профиль работы с кешем (Hit/Miss)

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

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

Шаг 21. Расширить размер буфера БД

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

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

Шаг 22. Изменились параметры запроса?

Иногда может быть так, что скорость выполнения запроса снижается изза дополнительных параметров, которые выступают в качестве условия выборки данных. Например, если в роли фильтра выступал какой-то атрибут, идентификаторы которого перечислялись в предикате запроса (where atribut_id in (id1, id2, id3)), и какой-то момент вместо 23 идентификаторов стали передаваться несколько сотен, то очевидно, что скорость выполнения запросов упадёт. Возможно, понадобится создание нового индекса, который учитывал бы такой вариант выборки данных. Такое может случиться после очередного релиза смежной системы, читающей данные из вашего сервиса.

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

Шаг 23. Изменилось ли количество и размер временных файлов?

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

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

Шаг 24. Пересоздать индекс

Пересоздаём индекс. Сделать это можно командой REINDEX (подробнее можно почитать тут: https://postgrespro.ru/docs/postgrespro/9.5/sql-reindex).

Переходим к шагу 31.

Шаг 25. Увеличить размер Work_mem, либо оптимизировать запрос на стороне приложения

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

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

Переходим к шагу 31.

Шаг 26. Создать новый индекс с учётом новых входных параметров

Быстрее всего в этой ситуации будет создание нового индекса (возможно, составного), который облегчил бы выборку данных с учётом новых входных параметров запроса. Если это возможно, то переходим к шагу 27. Иначе к шагу 28.

Шаг 27. Создать новый индекс с учётом новых входных параметров запроса

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

Шаг 28. Исправление самого запроса на стороне приложения, исключение новых параметров

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

Шаг 29. Создать новый индекс

В том случае, если по результатам EXPLAIN ANALYZE пришло понимание, что базе нужен новый индекс, то проще всего его досоздать. Это можно сделать командой CREATE INDEX с указанием параметра CONCURRENTLYдля неблокирующего построения (подробнее можно почитать тут: https://postgrespro.ru/docs/postgresql/11/sql-createindex). Далее переходим к шагу 31.

Шаг 30. Проверка влияния дополнительных факторов (взаимодействие с жёстким диском, сторонние процессы на сервере, работа других БД)

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

  • работа жёстких дисков/систем хранения данных;

  • нагруженные запросы на другие базы данных, находящиеся на том же сервере что и ваша;

  • сторонние процессы, выполняемые на сервере БД (например, работа антивируса);

  • создание репликационных баз данных, в процессе чего происходит создание слепка данных с вашего мастера;

  • сетевые проблемы и потери при взаимодействии приложения и базы данных.

Если таковые факторы найдены, то постарайтесь их устранить и переходите к шагу 31.

Шаг 31. Ситуация исправилась?

Ситуация стабилизировалась и проблема решена? Мы молодцы! В противном случае пора собирать всю королевскую рать и обращаться к DB-администратору.

Шаг 32. В БД много блокировок и запросов в ожидании?

По аналогии с Шагом 8 - проверяем динамику количества запросов к БД в статусе "IDLE IN TRANSACTION". Мы используем механизм периодического замера количества запросов в этом статусе, с выводом соответствующего графика в Grafana. Подчеркну, что блокировки нормальное явление, они периодически будут возникать. Но вот если у вас произошло резкое увеличение количества блокировок, то переходите к шагу 37. Если же диагностировать увеличение количества блокировок и запросов в ожидании не удалось, то переходите к шагу 33.

Шаг 33. Проверка логов PG Bouncer и логов приложений при подключении к PG Bouncer

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

Шаг 34. Есть проблемы с PGBouncer?

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

Шаг 35. Исправление проблемы с работой PGBouncer/подключение приложения напрямую к БД, в обход PGBouncer

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

Это временная мера для быстрого восстановления доступности сервиса. Это не решение проблемы!

Переходим к шагу 31.

Шаг 36. Требуется консультация аналитика

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

Шаг 37. Было ли недавнее изменение схемы данных БД?

Ситуация, когда неожиданно, без объявления войны в базе данных растёт аномальное количество блокировок и запросов в ожидании, крайне редка. Чаще всего это следствие релиза или миграции с изменением схемы данных БД. Если такое было в последнее время, то переходим к шагу 45. Если изменений в схеме данных не было, то переходим к шагу 38.

Шаг 38. Проверка журналов БД (или PG_STAT_ACTIVITY) для выявления запроса, вызвавшего блокировку

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

Шаг 39. Выявление сервисаинициатора запроса, вызвавшего блокировку

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

Шаг 40. Сервис, инициировавший запрос, работает штатно?

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

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

Шаг 41. Были ли недавно релизы сервиса, инициировавшего блокирующий запрос?

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

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

Шаг 42. Диагностика и починка сервиса, инициировавшего блокирующие запросы

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

После починки сервиса переходим к шагу 31.

Шаг 43. Откат последнего релиза

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

Шаг 44. Требуется консультация аналитика

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

Шаг 45. Откатить миграцию возможно?

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

Шаг 46. Откатить миграцию

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

Шаг 47. Ожидать завершения миграции

Лучше всего дождаться завершения миграции. Меньше проблем будет в дальнейшем. Затем переходим к шагу 31.

Заключение

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

Хочу выразить огромную благодарность Алексею Попову за помощь в подготовке статьи. Без его рецензии и консультаций статья была бы не столь полной и точной.

Подробнее..

Перечислимый тип и PostgreSQL

29.10.2020 18:20:36 | Автор: admin


Пролог


Под перечислимым типом обычно понимают тип данных, который может принимать ограниченное и, как правило, небольшое число значений. Его выделяет то, что эти значения часто хардкодятся программистами в исходный код. И, как следствие, пользователи и операторы приложения не могут менять множество значений перечислимого типа. Их меняют только разработчики, зачастую с соответствующими исправлениями в коде и бизнес-логике приложения. Примерами перечислимых типов могут быть: времена года, месяцы, направление типа въезда/выезд или in/out, какие-нибудь типы или категории чего-нибудь, и так далее. В PostgreSQL подобную функциональность могут и реализуют различными способами. Этому посвящена статья.


Лирическое отступление (или почему не boolean)


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


Что гораздо важнее, такой тип пола, как "other", стандартизирован ИКАО для официальных документов, и встречается в официальных документах, предъявляемых на пограничных пунктах, к сожалению, гораздо чаще, чем того требует природа человека. А когда люди с такими документами пересекают российскую границу, наши православные пограничники тоже вынуждены указывать такой пол уже во внутрироссийских документах. И для этой цели нельзя использовать значение null в типе boolean. Значение null означает значение неизвестно, например, не была заполнена графа "sex" в документе, и в действительности пол может оказаться неизвестно каким. А вот пол "other" это совершенно точно известный факт, что человек чувствует и записывает в документах, что он особенный. Поэтому для sex надо использовать не boolean, а перечислимый тип.


Варианты


Enum встроенный в PostgreSQL официальный тип


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


CREATE TYPE sex AS ENUM ('мужчина', 'женщина', 'иное');

Пример использования:


select id from table where sex='женщина';

То, что везде в примерах ищется женщина, это не сексизм, а олицетворение поговорки: "Cherchez la femme".


Текстовые обозначения не могут быть длиннее 63 байт (если используем русский язык и UTF-8, то делите на два). В самой таблице значения будут занимать 4 байта. Потому что, по сути, этот тип данных синтаксический сахар. На самом деле этот тип реализуется с помощью внешней таблицы, но планировщик выполняет некоторые оптимизации. Текстовые значения хранятся в таблице pg_enum, а ключом являются четырёхбайтные OID. Но это лучше, чем простое использование внешней таблицы. В запросах можно применять текстовые обозначения напрямую. И если в случае ошибки будет указано несуществующее значение, то будет поднят syntax error, в то время как при обычном использовании внешней таблицы никакой ошибки не было бы, запрос попросту вернул пустой результат.


Также этот тип безопасен в том смысле, что его нельзя сравнивать не только с другими типами, но даже с разными типами enum. В качестве бонуса, этот тип поддерживает упорядочивание его элементов (определены операции сравнения и сортировки), и этим порядком можно управлять (например, менять с помощью ALTER TYPE). Недостатки: использовать 4 байта там, где можно было бы обойтись одним, кажется расточительством. И когда я написал Тому Лэйну об этом недостатке существующего решения, то получил обычный в мире Open Source ответ: Раз ты такой умный, реализуй сам как считаешь лучше.


Char внутренний перечислимый тип PostgreSQL


Но не смотря на то, что в PostgreSQL есть специальный перечислимый тип для пользователей, во внутренних таблицах используется тип "char" в качестве перечислимого типа. Кавычки обязательны, потому что без них он превратится в широко известный тип char(много букв). В тип "char" помещается ровно 1 байт в символьном виде, т.е. размер в 4 раза меньше, чем официальный enum. При кодировке UTF-8 в него влезут английские буквы, цифры и символы, а вот русские буквы нет. Тип можно использовать, прямо указывая обозначения в виде букв, подобрав их по какому-нибудь мнемоническому правилу или стандарту. В нашем случае, в соответствии со стандартом ИКАО это будет m, f, x. Но это пока не так интересно: буквы, конечно, удобно хардкодить, но хочется иметь возможность работать и с текстовыми обозначениями. Для этого можно написать простые функции. Также можно усилить проверку типов, использовав domain с указанием допустимых значений.


CREATE DOMAIN sex_char AS "char" CHECK (VALUE in ('m','f','x'));CREATE FUNCTION sex(txt varchar, OUT ch sex_char) LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE AS$sex$BEGIN    ch:= case txt        when 'мужчина' then 'm'::sex_char        when 'женщина' then 'f'::sex_char        when 'иное' then 'x'::sex_char        else null        end;    if ch is null then        raise invalid_parameter_value;    end if;END$sex$;CREATE FUNCTION sex(ch sex_char, OUT txt varchar) LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE AS$sex$BEGIN    txt:= case ch        when 'm'::sex_char then 'мужчина'        when 'f'::sex_char then 'женщина'        when 'x'::sex_char then 'иное'        else null        end;    if txt is null then        raise invalid_parameter_value;    end if;END$sex$;

Две этих функции, по сути, составляют одну полиморфную функцию. Примеры использования:


=> select sex(ch=>'f'); sex--------- женщина(1 row)=> select sex(txt=>'женщина'); sex----- f(1 row)

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


=> select sex(sex(txt=>'женщина'));sex---------женщина(1 row)

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


select id from table where sex='f';select id from table where sex=sex(txt=>'женщина');

Из достоинств этого метода: занимает 1 байт, нет внешних таблиц и ожидается хорошее быстродействие.


Классическая внешняя таблица


Классика нормализации.


create table sex_t (   sex_t_id smallint primary key,   sex varchar not null unique);

И эта таблица подключается куда надо как внешняя. Пример использования:


select id from table join sex_t using (sex_t_id) where sex='женщина';

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


Экзотика


Можно еще упомянуть способы, к которым я не имею ни малейшего отношения. Но они встречаются. Видел пример, который выглядит как классическая внешняя таблица, но для ключа вместо smallint использовался serial. Причем в связанном с ним sequence шаг умышленно выставлялся в 0 (чтобы вызвать ошибки при его использовании), и это не баг, а идеологическая фича (как мне объяснил разработчик): поскольку значения ключа захардкожены, при добавлении новых значений значения ключа должны были явно указываться программистом. И значений там было не больше 10.


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


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


Всё это вызывает у меня скепсис, но такие варианты интересно рассмотреть при тестировании не потому, что они хороши, а потому, что интересно узнать, насколько они плохи.


Описание эксперимента


Идея


Предположим, есть девелоперская контора, в которой трудится 75 % мужчин, 24 % женщин и еще 1 % неопределившихся существ. Отделу кадров на 23 февраля надо получить количество мужчин, чтобы закупить для них подарки, потом 8 марта получить количество женщин. А после кадровики задумываются, что меньшинство дискриминировать и оставлять без подарков нехорошо. И нужно количество иных, чтобы 1 апреля подарить подарки и им. Создам разные варианты таблиц, имитирующих список сотрудников с указанием пола, и замерю время выполнения всех трех запросов.


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


В каждой таблице 10 000 000 записей, содержимое всех таблиц одинаковое (по составу). И поскольку запросы должны символизировать фильтрацию по полю перечисления и выдачу полезных данных из других полей, я решил отключить index only scan. Сделаю я это, изменив в запросах count(*) на count(id), т.е. явно укажу, что нужны данные, не входящие в индекс.


Описание стенда


Стенд сделал из того, что было: ноут MSI, операционка сообщает о 8 ядрах процессора, 16 Гб ОЗУ (hugepages 2 Мб на 14 Гб), 0 swap. Но поскольку тут интересно лишь относительное сравнение результатов измерений друг с другом, а не абсолютные значения, подробно расписывать железо не буду. CentOS 8, PostgreSQL 13 с shared_buffers (кэшем PostgreSQL) на 14 Гб.


Было сделано 100 серий экспериментов, в каждой серии по 100 замеров каждого варианта, итого 10 000 замеров каждого варианта. Чтобы каждый мог повторить эксперимент, привожу все скрипты.


postgresql.conf


Этот файл инклюдится в стандартный postgresql.conf.


# Минимальный уровень WAL чтобы уменьшить время на создание таблицwal_level = minimalmax_wal_senders = 0# Поскольку работаем с закэшированными таблицами, издержек на "случайный" доступ нет.random_page_cost = 1# отключаем распараллеливаниеmax_parallel_workers_per_gather=0# Кэш PostgreSQLshared_buffers = 14GB

prewarm.sql


Прогреваю БД с помощью pg_prewarm.


Код
select pg_prewarm('sex1');select pg_prewarm('sex1_btree');select pg_prewarm('sex2');select pg_prewarm('sex2_btree');select pg_prewarm('sex3');select pg_prewarm('sex3_btree');select pg_prewarm('sex4');select pg_prewarm('sex4_btree');select pg_prewarm('sex5');select pg_prewarm('sex5_btree');select pg_prewarm('sex5h');select pg_prewarm('sex5h_hash');select pg_prewarm('sex6');select pg_prewarm('sex6_gin');select pg_prewarm('sex6h');select pg_prewarm('sex6h_gin_hash');

test.sql


Такими запросами проводится тестирование. И эти же запросы используются для дополнительного прогрева (pg_prewarm недостаточно). Напомню, что я использую count(id), чтобы отключить index only scan.


Код
select count(id) from sex1 where sex='мужчина';select count(id) from sex1 where sex='женщина';select count(id) from sex1 where sex='иное';select count(id) from sex2 where sex_char=sex(txt=>'мужчина');select count(id) from sex2 where sex_char=sex(txt=>'женщина');select count(id) from sex2 where sex_char=sex(txt=>'иное');select count(id) from sex3 join sex_t using (sex_t_id) where sex='мужчина';select count(id) from sex3 join sex_t using (sex_t_id) where sex='женщина';select count(id) from sex3 join sex_t using (sex_t_id) where sex='иное';select count(id) from sex3 where sex_t_id=(select t.sex_t_id from sex_t t where sex='мужчина');select count(id) from sex3 where sex_t_id=(select t.sex_t_id from sex_t t where sex='женщина');select count(id) from sex3 where sex_t_id=(select t.sex_t_id from sex_t t where sex='иное');select count(id) from sex4 join sex_t4 using (sex_t4_id) where sex='мужчина';select count(id) from sex4 join sex_t4 using (sex_t4_id) where sex='женщина';select count(id) from sex4 join sex_t4 using (sex_t4_id) where sex='иное';select count(id) from sex4 where sex_t4_id=(select t.sex_t4_id from sex_t4 t where sex='мужчина');select count(id) from sex4 where sex_t4_id=(select t.sex_t4_id from sex_t4 t where sex='женщина');select count(id) from sex4 where sex_t4_id=(select t.sex_t4_id from sex_t4 t where sex='иное');select count(id) from sex5 where sex='мужчина';select count(id) from sex5 where sex='женщина';select count(id) from sex5 where sex='иное';select count(id) from sex5h where sex='мужчина';select count(id) from sex5h where sex='женщина';select count(id) from sex5h where sex='иное';select count(id) from sex6 where jdoc@>'{"sex":"мужчина"}';select count(id) from sex6 where jdoc@>'{"sex":"женщина"}';select count(id) from sex6 where jdoc@>'{"sex":"иное"}';select count(id) from sex6h where jdoc@>'{"sex":"мужчина"}';select count(id) from sex6h where jdoc@>'{"sex":"женщина"}';select count(id) from sex6h where jdoc@>'{"sex":"иное"}';

init.sql


Скрипт первоначального создания БД для экспериментов:


Код
-- заполняем таблицы, во всех таблицах одинаковые данные\set table_size 10000000-- удобный view для посмотра размера таблиц после их заполненияcreate or replace view disk as SELECT n.nspname AS schema,    c.relname,    pg_size_pretty(pg_relation_size(c.oid::regclass)) AS size,    pg_relation_size(c.oid::regclass)/1024 AS size_KiB   FROM pg_class c     LEFT JOIN pg_namespace n ON n.oid = c.relnamespace  ORDER BY (pg_relation_size(c.oid::regclass)) DESC LIMIT 20;begin;-- sex1 официальный enumCREATE TYPE sex_enum AS ENUM ('мужчина', 'женщина', 'иное');create table sex1 (id float, sex sex_enum not null);-- sex2 "char"CREATE DOMAIN sex_char AS "char" CHECK (VALUE in ('m','f','x'));CREATE FUNCTION sex(txt varchar, OUT ch sex_char) LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE AS$sex$BEGIN    ch:= case txt        when 'мужчина' then 'm'::sex_char        when 'женщина' then 'f'::sex_char        when 'иное' then 'x'::sex_char        else null        end;    if ch is null then        raise invalid_parameter_value;    end if;END$sex$;CREATE FUNCTION sex(ch sex_char, OUT txt varchar) LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE AS$sex$BEGIN    txt:= case ch        when 'm'::sex_char then 'мужчина'        when 'f'::sex_char then 'женщина'        when 'x'::sex_char then 'иное'        else null        end;    if txt is null then        raise invalid_parameter_value;    end if;END$sex$;create table sex2 (id float, sex_char "char" not null);-- sex3 внешняя таблица c ключом smallintcreate table sex_t (  sex_t_id smallint primary key,  sex varchar not null unique);insert into sex_t (sex_t_id,sex) values (1,'мужчина'),(0,'женщина'),(-1,'иное');create table sex3 (id float, sex_t_id smallint not null references sex_t);-- sex4 с serial, как бы это странно не выглядело, повторяю то, что видел в одной уважаемой компанииcreate table sex_t4 (  sex_t4_id serial primary key,  sex varchar not null unique);insert into sex_t4 (sex_t4_id,sex) values (1,'мужчина'),(0,'женщина'),(-1,'иное');create table sex4 (id float, sex_t4_id integer not null references sex_t4);-- текстовое полеcreate table sex_t5 (  sex varchar primary key);insert into sex_t5 (sex) values ('мужчина'),('женщина'),('иное');-- для btree индексаcreate table sex5 (id float, sex varchar not null references sex_t5);-- для hash индексаcreate table sex5h (id float, sex varchar not null references sex_t5);-- jsonb-- для обычного gin индексаcreate table sex6 (id float, jdoc jsonb not null);-- для gin индекса с хэш по ключам и значениямcreate table sex6h (id float, jdoc jsonb not null);-- вставка данныхinsert into sex1 (id,sex) select random, case when random<0.75 then 'мужчина'::sex_enum when random<0.99 then 'женщина'::sex_enum else 'иное'::sex_enum end from (select random() as random, generate_series(1,:table_size)) as subselect;insert into sex5 (id,sex) select id,sex::varchar from sex1;insert into sex2 (id,sex_char) select id,sex(sex) from sex5;insert into sex3 (id,sex_t_id) select id,sex_t_id from sex5 join sex_t using (sex);insert into sex4 (id,sex_t4_id) select id,sex_t4_id from sex5 join sex_t4 using (sex);insert into sex5h (id,sex) select id,sex from sex5;insert into sex6 (id,jdoc) select id,('{"sex": "'||sex||'"}')::jsonb from sex5;insert into sex6h (id,jdoc) select id,jdoc from sex6;-- создаем индексыcreate index sex1_btree on sex1(sex);create index sex2_btree on sex2(sex_char);create index sex3_btree on sex3(sex_t_id);create index sex4_btree on sex4(sex_t4_id);create index sex5_btree on sex5(sex);-- для текста используем hashcreate index sex5h_hash on sex5h using hash(sex);create index sex6_gin on sex6 using gin(jdoc);-- тут тоже, по сути, hashcreate index sex6h_gin_hash on sex6h using gin(jdoc jsonb_path_ops);commit;set role postgres;-- экстеншин для прогрева (заполнения кэша PostgreSQL)create extension if not exists pg_prewarm;-- удобный экстеншин для мониторинга заполнения кэшаcreate extension if not exists pg_buffercache;create or replace view cache as SELECT n.nspname AS schema,    c.relname,    pg_size_pretty(count(*) * 8192) AS buffered,    count(*) * 8 AS buffered_KiB,    round(100.0 * count(*)::numeric / ((( SELECT pg_settings.setting           FROM pg_settings          WHERE pg_settings.name = 'shared_buffers'::text))::integer)::numeric, 1) AS buffer_percent,    round(100.0 * count(*)::numeric * 8192::numeric / pg_table_size(c.oid::regclass)::numeric, 1) AS percent_of_relation   FROM pg_class c     JOIN pg_buffercache b ON b.relfilenode = c.relfilenode     JOIN pg_database d ON b.reldatabase = d.oid AND d.datname = current_database()     LEFT JOIN pg_namespace n ON n.oid = c.relnamespace  GROUP BY c.oid, n.nspname, c.relname  ORDER BY buffered_kib DESC LIMIT 20;-- заключительный vacuumvacuum freeze analyze;

test


Скрипт для тестирования:


Код
#!/bin/shset -o errexit -o noclobber -o nounset -o pipefail#set -o errexit -o noclobber -o nounset -o pipefail -o xtrace# for pgbenchPATH="$PATH:/usr/pgsql-13/bin"# config# database connection parametersreadonly PGDATABASE='sex'readonly PGPORT=5432export PGDATABASE PGPORT# output data filereadonly data_csv='data.csv'# init data filesreadonly header='sex:,male,female,other'if [ ! -s "$data_csv" ]then    echo "$header" >|"$data_csv"fi# prewarm to the cachepsql --quiet -f prewarm.sql >/dev/null# more prewarmpgbench --no-vacuum --transaction 100 --file test.sql >/dev/nullfor i in $(seq 1 100)do   echo -n "$i "   date --iso-8601=seconds    pgbench --no-vacuum --transaction 100 --report-latencies --file 'test.sql' | \        awk "            /from sex1 where sex='мужчина';\$/ {printf \"enum,%s,\", \$1 >>\"$data_csv\";}            /from sex1 where sex='женщина';\$/ {printf \"%s,\", \$1 >>\"$data_csv\";}            /from sex1 where sex='иное';\$/ {printf \"%s\\n\", \$1 >>\"$data_csv\";}            /from sex2 where sex_char=sex\(txt=>'мужчина'\);\$/ {printf \"\\\"char\\\",%s,\", \$1 >>\"$data_csv\";}            /from sex2 where sex_char=sex\(txt=>'женщина'\);\$/ {printf \"%s,\", \$1 >>\"$data_csv\";}            /from sex2 where sex_char=sex\(txt=>'иное'\);\$/ {printf \"%s\\n\", \$1 >>\"$data_csv\";}            /from sex3 join sex_t using \(sex_t_id\) where sex='мужчина';\$/ {printf \"smallint(join),%s,\", \$1 >>\"$data_csv\";}            /from sex3 join sex_t using \(sex_t_id\) where sex='женщина';\$/ {printf \"%s,\", \$1 >>\"$data_csv\";}            /from sex3 join sex_t using \(sex_t_id\) where sex='иное';\$/ {printf \"%s\\n\", \$1 >>\"$data_csv\";}            /from sex3 where sex_t_id=\(select t.sex_t_id from sex_t t where sex='мужчина'\);\$/ {printf \"smallint(subsel),%s,\", \$1 >>\"$data_csv\";}            /from sex3 where sex_t_id=\(select t.sex_t_id from sex_t t where sex='женщина'\);\$/ {printf \"%s,\", \$1 >>\"$data_csv\";}            /from sex3 where sex_t_id=\(select t.sex_t_id from sex_t t where sex='иное'\);\$/ {printf \"%s\\n\", \$1 >>\"$data_csv\";}            /from sex4 join sex_t4 using \(sex_t4_id\) where sex='мужчина';\$/ {printf \"integer(join),%s,\", \$1 >>\"$data_csv\";}            /from sex4 join sex_t4 using \(sex_t4_id\) where sex='женщина';\$/ {printf \"%s,\", \$1 >>\"$data_csv\";}            /from sex4 join sex_t4 using \(sex_t4_id\) where sex='иное';\$/ {printf \"%s\\n\", \$1 >>\"$data_csv\";}            /from sex4 where sex_t4_id=\(select t.sex_t4_id from sex_t4 t where sex='мужчина'\);\$/ {printf \"integer(subsel),%s,\", \$1 >>\"$data_csv\";}            /from sex4 where sex_t4_id=\(select t.sex_t4_id from sex_t4 t where sex='женщина'\);\$/ {printf \"%s,\", \$1 >>\"$data_csv\";}            /from sex4 where sex_t4_id=\(select t.sex_t4_id from sex_t4 t where sex='иное'\);\$/ {printf \"%s\\n\", \$1 >>\"$data_csv\";}            /from sex5 where sex='мужчина';\$/ {printf \"varchar(btree),%s,\", \$1 >>\"$data_csv\";}            /from sex5 where sex='женщина';\$/ {printf \"%s,\", \$1 >>\"$data_csv\";}            /from sex5 where sex='иное';\$/ {printf \"%s\\n\", \$1 >>\"$data_csv\";}            /from sex5h where sex='мужчина';\$/ {printf \"varchar(hash),%s,\", \$1 >>\"$data_csv\";}            /from sex5h where sex='женщина';\$/ {printf \"%s,\", \$1 >>\"$data_csv\";}            /from sex5h where sex='иное';\$/ {printf \"%s\\n\", \$1 >>\"$data_csv\";}            /from sex6 where jdoc@>'{\"sex\":\"мужчина\"}';\$/ {printf \"jsonb(gin),%s,\", \$1 >>\"$data_csv\";}            /from sex6 where jdoc@>'{\"sex\":\"женщина\"}';\$/ {printf \"%s,\", \$1 >>\"$data_csv\";}            /from sex6 where jdoc@>'{\"sex\":\"иное\"}';\$/ {printf \"%s\\n\", \$1 >>\"$data_csv\";}            /from sex6h where jdoc@>'{\"sex\":\"мужчина\"}';\$/ {printf \"jsonb(gin+hash),%s,\", \$1 >>\"$data_csv\";}            /from sex6h where jdoc@>'{\"sex\":\"женщина\"}';\$/ {printf \"%s,\", \$1 >>\"$data_csv\";}            /from sex6h where jdoc@>'{\"sex\":\"иное\"}';\$/ {printf \"%s\\n\", \$1 >>\"$data_csv\";}            "doneecho 'Done'

Размер таблиц и индексов


=> \dt+                          List of relations Schema |  Name  | Type  | Owner | Persistence |  Size  | Description--------+--------+-------+-------+-------------+--------+------------- public | sex1   | table | olleg | permanent   | 422 MB | public | sex2   | table | olleg | permanent   | 422 MB | public | sex3   | table | olleg | permanent   | 422 MB | public | sex4   | table | olleg | permanent   | 422 MB | public | sex5   | table | olleg | permanent   | 498 MB | public | sex5h  | table | olleg | permanent   | 498 MB | public | sex6   | table | olleg | permanent   | 651 MB | public | sex6h  | table | olleg | permanent   | 651 MB | public | sex_t  | table | olleg | permanent   | 48 kB  | public | sex_t4 | table | olleg | permanent   | 48 kB  | public | sex_t5 | table | olleg | permanent   | 48 kB  |(11 rows) => \di+                                   List of relations Schema |      Name      | Type  | Owner | Table  | Persistence |  Size  | Description--------+----------------+-------+-------+--------+-------------+--------+------------- public | sex1_btree     | index | olleg | sex1   | permanent   | 66 MB  | public | sex2_btree     | index | olleg | sex2   | permanent   | 66 MB  | public | sex3_btree     | index | olleg | sex3   | permanent   | 66 MB  | public | sex4_btree     | index | olleg | sex4   | permanent   | 66 MB  | public | sex5_btree     | index | olleg | sex5   | permanent   | 67 MB  | public | sex5h_hash     | index | olleg | sex5h  | permanent   | 448 MB | public | sex6_gin       | index | olleg | sex6   | permanent   | 21 MB  | public | sex6h_gin_hash | index | olleg | sex6h  | permanent   | 10 MB  | public | sex_t4_pkey    | index | olleg | sex_t4 | permanent   | 16 kB  | public | sex_t4_sex_key | index | olleg | sex_t4 | permanent   | 16 kB  | public | sex_t5_pkey    | index | olleg | sex_t5 | permanent   | 16 kB  | public | sex_t_pkey     | index | olleg | sex_t  | permanent   | 16 kB  | public | sex_t_sex_key  | index | olleg | sex_t  | permanent   | 16 kB  |(13 rows)

Заметно, что при использовании типов данных размером 1 байт или 2 байта вместо типов данных размером 4 байта нет выигрыша ни в размере таблицы, ни в размере индекса. Видимо, это как-то связанно с выравниванием данных PostgreSQL по границам слов. Более того, даже при использовании текстового поля проигрыш по размерам оказался не так велик, как ожидалось. Наверное, это связано с тем, что такое текстовое поле было одно (и строки небольших длин) и дополнительно есть много служебных полей в строке таблицы.


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


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


Удивительно маленькие размеры у индексов, построенных на базе GIN (по сравнению с btree). Но результаты их использования, как покажу потом, наихудшие. Где-то читал, что GIN-индексы активно используют внутри себя сжатие данных, возможно, этим можно всё объяснить.


Результаты


Выборка 75% должна быть характерна тем, что тут планировщик должен предпочитать поиск последовательным чтением таблицы, а не использовать индекс. При выборке 24% он предпочитает использовать индекс, но это довольно экстремальный случай. Выборка 1% более типичный поиск по индексу.


Данные потом были залиты в M$ Exel и там преобразованы в диаграммы коробочки с усиками (удобно, можно смотреть не только среднее значение или медиану, но также и распределение данных). То, что коробочки с усиками выглядят как горизонтальные полоски, говорит о том, что точность (повторяемость) замеров очень хорошая, разброса данных практический нет.


75%24%1%


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


enum и "char"


  • Лидеры этого теста выполняются примерно одинаково, хотя я ожидал, что "char" будет в четыре раза быстрее. Возможно, это связано с тем, что PostgreSQL предпочитает выравнивать данные по размерам слов. Поскольку выигрыша от "char" нет, значительно проще использовать enum.
  • Планировщик на значениях гистограммы может правильно оценить размер выборки, при 75% работает последовательное чтение, а при 24% и 1% индексы.
  • По сути, внутренняя реализация enum представляет собой случай с внешней таблицей и integer (четырёхбайтным) ключом. Но видно, что работают какие-то оптимизации: например, при 75% работает последовательное чтение, а при внешней таблице с integerключом поиск по индексу, поэтому при 75% выборке enum заметно быстрее; при 24% и 1% выборках enum быстрее, чем select с внешней таблицей с помощью join, и сравним по скорости с select с подзапросом.

Пример планов запросов:


=> explain (costs false) select count(id) from sex1 where sex='женщина';                   QUERY PLAN------------------------------------------------- Aggregate   ->  Index Scan using sex1_btree on sex1         Index Cond: (sex = 'женщина'::sex_enum)(3 rows)=> explain (costs false) select count(id) from sex2 where sex_char=sex(txt=>'женщина');                  QUERY PLAN---------------------------------------------- Aggregate   ->  Index Scan using sex2_btree on sex2         Index Cond: (sex_char = 'f'::"char")(3 rows)

smallint и integer


  • Между двухбайтовым smallint и четырёхбайтовым integer (serial) нет разницы с точки зрения времени выполнения. Возможно, это связано с тем, что PostgreSQL как-то выравнивает данные.
  • Если в enum и "char" планировщик предпочел при выборке 75% использовать последовательное чтение таблицы, то в этом случае ошибочно идет поиск по индексу и виден проигрыш по производительности. Возможно, причина в том, что планировщик без выполнения запроса не может в этом случае предугадать, какая будет выборка. В случае с 1% и 24% он угадывает использовать индекс.
  • При объединении таблиц с помощью join (Nested Loop) результат почему-то заметно хуже, чем в случае с подзапросом. Хотя, насколько я знаю, алгоритм там должен быть такой же. Т.е. это практически синонимы: подзапрос и Nested Loop. Наверное, тут есть окно возможностей для оптимизации Nested Loop до уровня подзапроса.

Для наглядности приведу планы запроса для 75% выборки, чтобы показать, что там не используется последовательное чтение. И план для запроса с подзапросом. Для 1% и 24% выборки планы точно такие же.


=> explain (costs false) select count(id) from sex3 join sex_t using (sex_t_id) where sex='мужчина';                      QUERY PLAN------------------------------------------------------- Aggregate   ->  Nested Loop         ->  Seq Scan on sex_t               Filter: ((sex)::text = 'мужчина'::text)         ->  Index Scan using sex3_btree on sex3               Index Cond: (sex_t_id = sex_t.sex_t_id)(6 rows)=> explain (costs false) select count(id) from sex3 where sex_t_id=(select t.sex_t_id from sex_t t where sex='мужчина');                    QUERY PLAN--------------------------------------------------- Aggregate   InitPlan 1 (returns $0)     ->  Seq Scan on sex_t t           Filter: ((sex)::text = 'мужчина'::text)   ->  Index Scan using sex3_btree on sex3         Index Cond: (sex_t_id = $0)(6 rows)

varchar


  • В отличие от предыдущего случая, планировщик работает, как ожидалось: при 75% последовательное чтение, при 1% и 24% поиск по индексу.
  • Результат поиска по текстовому полю с помощью btree-индекса заметно быстрее, чем при использовании объединения с внешней таблицей при помощи join, и сопоставим с объединением таблиц с помощью подзапроса. Бальзам на душу для любителей денормализации.
  • Hash-индекс работает заметно хуже, чем btree (при таком распределении данных). Хотя ожидалось, что наоборот: в теории, hash-индекс именно в таком случае можно сделать очень быстрым. В теории, надо было бы создать три корзины с tuple ID и специальную hash-функцию. которая возвращала бы 1, 2 или 3, т.е. номер корзины. Видимо, что-то не так с hash-индексами у PostgreSQL, и более длительный результат как-то связана с очень большими размерами самого hash-индекса.

План для btree и hash-индекса.


=> explain (costs false) select count(id) from sex5 where sex='женщина';                     QUERY PLAN----------------------------------------------------- Aggregate   ->  Index Scan using sex5_btree on sex5         Index Cond: ((sex)::text = 'женщина'::text)(3 rows)=> explain (costs false) select count(id) from sex5h where sex='женщина';                     QUERY PLAN----------------------------------------------------- Aggregate   ->  Index Scan using sex5h_hash on sex5h         Index Cond: ((sex)::text = 'женщина'::text)(3 rows)

json


  • Здесь при 75% тоже поиск идёт последовательным чтением. Не знаю, как планировщик догадался, что здесь распределение будет 75%. Неужели строит гистограммы для внутренностей JSON? В старых версиях PostgreSQL в этом случае ошибочно использовался поиск по индексу. При 1% и 24% выборке PostgreSQL ожидаемо использует поиск по индексу.
  • Поиск по хэшированным путям и значениям (индекс с jsonb_path_ops) заметно быстрее (в случае 1% более, чем в полтора раза), чем по обычному GIN для JSON.
  • Но, тем не менее, оба варианта с JSON далеко отстающие аутсайдеры.

=> explain (costs false) select count(id) from sex6 where jdoc@>'{"sex":"мужчина"}';                      QUERY PLAN------------------------------------------------------- Aggregate   ->  Seq Scan on sex6         Filter: (jdoc @> '{"sex": "мужчина"}'::jsonb)(3 rows)=> explain (costs false) select count(id) from sex6 where jdoc@>'{"sex":"женщина"}';                           QUERY PLAN----------------------------------------------------------------- Aggregate   ->  Bitmap Heap Scan on sex6         Recheck Cond: (jdoc @> '{"sex": "женщина"}'::jsonb)         ->  Bitmap Index Scan on sex6_gin               Index Cond: (jdoc @> '{"sex": "женщина"}'::jsonb)(5 rows)

Выводы


Как ни странно, несмотря на всю кажущуюся неэффективность, официальный enum лучшее решение для перечислений, он один из самых быстрых, и в то же время самый удобный в использовании. Но, я думаю, так получилось не потому, что 4 байтный enum очень хорошо продуман и оптимизирован, а потому, что поиск по таким типам данных как 1 байтный "char" и 2 байтный smallint недостаточно хорошо оптимизирован, как мог бы быть.

Подробнее..

Кеш бывает разным

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

PostgreSQL хранит данные на каких-то носителях. И между PostgreSQL и, например, магнитной поверхностью диска находится несколько кешей: кеш самого винчестера, кеш RAID-контроллера или винчестерной полки, кеш файловой системы на уровне операционной системы и кеш самого PostgreSQL. Если первыми перечисленными кешами мы практический не можем управлять, то последними, находящимися в ОЗУ сервера, управлять можем: например, выделяя больше ОЗУ под кеш PostgreSQL в ущерб кешу ОС, или наоборот. В официальной документации можно прочитать ничем не подтвержденные рекомендации, типа выделять под PostgreSQL четверть ОЗУ. Это вызывает сомнения. PostgreSQL в виде Postgres95 впервые появился в 1995 году и, кто знает, быть может и эти рекомендации относятся к тому же году. Поэтому появилась идея эксперимента с целью разобраться, как лучше распределять ОЗУ.


Сразу оговорюсь, что речь пойдет про выделенный под БД сервер, на котором помимо самого PostgreSQL и обслуживающей его инфраструктуры ничего нет. Если рассмотреть эту задачу умозрительно, то интуитивно понятно, что своя рубашка ближе к телу, то есть со своим собственным кешем PostgreSQL должен работать оптимальнее. Например, в своем кеше PostgreSQL хранит информацию постранично, а размер страницы в PostgreSQL по умолчанию 8 Кб. В кеше ОС информация хранится тоже постранично, но размер страницы в памяти ОС и, обычно, файловой системы равен 4 Кб, то есть возможна фрагментация страницы PostgreSQL. Но на практике такие различия, связанные с тем, что в собственном кеше данные хранятся в более оптимальном виде, малозаметны. Другое отличие кешей заключается в том, как выбирается для удаления страница, чтобы записать новую. Кеш ОС работает просто: он удаляет ту страницу, к которой дольше всего не обращались. А кеш PostgreSQL пытается вести себя умнее, ведь данные бывают разными более или менее полезными. Например, индексы или данные за последний месяц, к которым обращаются чаще, более полезны, чем данные, скажем, годичной давности, к которым обращаются редко. Поэтому PostgreSQL выставляет данным своеобразную оценку полезности и в первую очередь высвобождает наименее полезную информацию. А если PostgreSQL видит, что намечается последовательное чтение большой таблицы, которое может вымыть остальные данные из кеша, то он принимает меры, чтобы этого не случилось. Вот такую оптимизацию, связанную с интеллектуальным анализом полезности кеша, и должен был продемонстрировать эксперимент. Также с его помощью решался вопрос о том, насколько влияют на производительность PostgreSQL HugePages.


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


Описание эксперимента


Идея проста. Создаю одну таблицу, размер которой больше ОЗУ сервера, и индекс к этой таблице, который занимает где-то 10% от размера таблицы. Мне показалось, что такая пропорция реалистична. Cначала прогреваю таблицу, потом индекс, чтобы они по максимуму находились в кеше. Измеряю время индексированного поиска. Запускаю неиндексированный поиск, который выполняет последовательное чтение всей таблицы, измеряю длительность его работы. А после этого повторяю эксперимент по индексированному поиску. Предполагаю, что в случае работы кеша PostgreSQL разница между поисками по индексу до и после поиска последовательным чтением таблицы будет минимальна, а в случае кеша ОС заметна. Эксперимент буду повторять с различными пропорциями кешей ОС и PostgreSQL, а также с использованием HugePages и без них.


Описание стенда


Стенд сделал из того, что было: ноут MSI, операционка сообщает о 8 ядрах процессора, 16 Гб ОЗУ (HugePages 2 Мб на 14 Гб), 0 swap. К тому же Linux ругался о том, что процессор перегревается и приходится сбрасывать частоту, что наверняка сказалось на повторяемости эксперимента. Софт: CentOS 8 и PostgreSQL 13.0.


Подробно опишу эксперимент, чтобы любой желающий мог его повторить в своих условиях. Прогревание выполняется с помощью функции pg_prewarm(), но поскольку в результате эксперимента выяснилось, что pg_prewarm хоть и заполняет кеш, но прогревает недостаточно, после неё дополнительно прогреваю с помощью pgbench. Потом через тот же pgbench замеряю длительность индексированного поиска, поиска через последовательное чтение и снова индексированного. Результаты складываю в CSV-файлы, которые потом импортирую в Exсel, там обрабатываю и строю графики. Видимо, из-за фрагментирования памяти иногда невозможно создавать HugePages большого размера. Поэтому цикл тестирования выглядит так: перезагружаю машину, из rc.local вызываю тестирующий скрипт, он прогоняет тесты с включенными HugePages от максимального размера кеша PostgreSQL к минимальному, потом скрипт повторяет то же самое с выключенными HugePages, после чего перезагружает машину. Размер HugePage равен 2 Мб.


Все файлы лежат в одной директории, вот их список:


postgresql.conf
include = 'shared_buffers.conf'# при max_parallel_workers_per_gather>6 ошибка выполнения при размере кеша PostgreSQL 128 Кбmax_parallel_workers_per_gather=6

Этот файл инклюдится в postgresql.conf базы данных с помощью директивы include. Он нужен для проведения эксперимента с разными настройками PostgreSQL.


shared_buffers.conf
shared_buffers=128kB

Скриптом, который выполняет тестирование, в этот файл записывается размер кеша PostgreSQL.


init.sql
-- Размер таблицы, которую нужно создать. Подбирается методом проб и ошибок так, чтобы таблица имела размер, равный ОЗУ.\set table_size  75000000-- Создаю таблицу с именем random, там лежат случайные данные. :)drop table if exists random;create table random(random real, data float[]);insert into random select random,array_fill(random,ARRAY[20]) from (select random() as random, generate_series(1,:table_size)) as subselect;-- И индекс к ней.create index on random(random);-- Дальнейшие операции требуют роль суперпользователя, мой пользователь имеет эту роль, поэтому переключиться несложно.set role postgres;-- Модуль для функции pg_prewarmcreate extension if not exists pg_prewarm;-- Модуль, в котором можно наблюдать структуру кеша PostgreSQLcreate extension if not exists pg_buffercache;-- View на базе предыдущего модуля для наблюдением за кешем. Для эксперимента не используется, и во время эксперимента лучше не использовать, т.к. pg_buffercache вел себя нестабильно и иногда рушил процесс PostgreSQL.create or replace view cache as SELECT n.nspname AS schema,    c.relname,    pg_size_pretty(count(*) * 8192) AS buffered,    count(*) * 8 AS buffered_KiB,    round(100.0 * count(*)::numeric / ((( SELECT pg_settings.setting           FROM pg_settings          WHERE pg_settings.name = 'shared_buffers'::text))::integer)::numeric, 1) AS buffer_percent,    round(100.0 * count(*)::numeric * 8192::numeric / pg_table_size(c.oid::regclass)::numeric, 1) AS percent_of_relation   FROM pg_class c     JOIN pg_buffercache b ON b.relfilenode = c.relfilenode     JOIN pg_database d ON b.reldatabase = d.oid AND d.datname = current_database()     LEFT JOIN pg_namespace n ON n.oid = c.relnamespace  GROUP BY c.oid, n.nspname, c.relname  ORDER BY (round(100.0 * count(*)::numeric / ((( SELECT pg_settings.setting           FROM pg_settings          WHERE pg_settings.name = 'shared_buffers'::text))::integer)::numeric, 1)) DESC LIMIT 5;-- View, в котором можно посмотреть размер таблицы на диске, использовался для подгона размера таблицы под ОЗУ.create or replace view disk as SELECT n.nspname AS schema,    c.relname,    pg_size_pretty(pg_relation_size(c.oid::regclass)) AS size,    pg_relation_size(c.oid::regclass)/1024 AS size_KiB   FROM pg_class c     LEFT JOIN pg_namespace n ON n.oid = c.relnamespace  ORDER BY (pg_relation_size(c.oid::regclass)) DESC LIMIT 5;vacuum full freeze analyze;

SQL-скрипт, который создает окружение в БД, выполняется перед экспериментом. Запускается с помощью psql база -f init.sql.


test_idx.sql
select random from random where random=(select random()::real);

test_seq.sql
select count(data) from random;

Здесь находятся SQL-запросы, которые выполняет pgbench во время тестирования.


config.sh
# database connection parametersexport PGDATABASE='pgcache'# Добавляю путь к бинарникам PostgreSQL, это было нужно на CentOS и, возможно, других редхатоподобных дистрибутивах.PATH=$PATH:/usr/pgsql-13/bin# Через пробел указаны значения для конфигурационной переменной PostgreSQL shared_buffers, т.е. собственно размеры кеша на которых будет производится тестирование. Порядок по уменьшению величин важен. Дело в том, что HugePages уменьшить можно всегда, а вот увеличить  не всегда, видимо, из-за фрагментации памяти. Поэтому тестирование ведется при уменьшении размера кеша.readonly shared_buffers='14GB 13GB 12GB 11GB 10GB 9GB 8GB 7GB 6GB 5GB 4GB 3GB 2GB 1GB 512MB 256MB 128MB 64MB 32MB 16MB 8MB 4MB 2MB 1MB 512kB 256kB 128kB';

get_huge_pages
#!/bin/bash. config.shreadonly postgresql_service='postgresql-13' postgresql_pid='/var/lib/pgsql/13/data/postmaster.pid'readonly huge_pages_sh='huge_pages.sh' shared_buffers_conf='shared_buffers.conf'sudo systemctl start "$postgresql_service"echo 'declare -r -A huge_pages_size=( \' >"$huge_pages_sh"for s_b in $shared_buffersdo   echo "shared_buffers=$s_b" >"$shared_buffers_conf"   sleep 5   sudo systemctl restart "$postgresql_service"   pid=$(sudo head -1 "$postgresql_pid")   echo -n '   [' >>"$huge_pages_sh"   psql --expanded --quiet --tuples-only -c "show shared_buffers" | awk '/^shared_buffers \|/{printf "%s", $3}' >>"$huge_pages_sh"   echo -n ']=' >>"$huge_pages_sh"   sudo awk '/^VmPeak:/ {printf "%i", $2/2048+1}' /proc/"$pid"/status >>"$huge_pages_sh"   echo ' \' >>"$huge_pages_sh"doneecho ')' >>"$huge_pages_sh"sudo systemctl stop "$postgresql_service"

Вспомогательный скрипт. Он берет из config.sh значения для shared_buffers, запускает с этими настройками PostgreSQL, замеряет, сколько нужно HugePages для его работы в данной конфигурации, и записывает результат работы в файл huge_pages.sh.


huge_pages.sh
declare -r -A huge_pages_size=( \   [14GB]=7416 \   [13GB]=6893 \   [12GB]=6370 \   [11GB]=5847 \   [10GB]=5323 \   [9GB]=4800 \   [8GB]=4277 \   [7GB]=3750 \   [6GB]=3226 \   [5GB]=2703 \   [4GB]=2180 \   [3GB]=1655 \   [2GB]=1131 \   [1GB]=607 \   [512MB]=345 \   [256MB]=209 \   [128MB]=142 \   [64MB]=108 \   [32MB]=91 \   [16MB]=82 \   [8MB]=78 \   [4MB]=76 \   [2MB]=75 \   [1MB]=74 \   [512kB]=74 \   [256kB]=74 \   [128kB]=74 \)

Здесь лежат результаты работы get_huge_pages, они используются в скрипте test.


dataHP.csv и dataNHP.csv
Результаты работы скрипта test, используются потом в Excel.


output.log
Вывод из скрипта test полезен, если там содержатся ошибки, а если их нет он пустой.


Пример запуска тестирующего скрипта. Отредактировал файл, который уже был в CentOS:


/etc/rc.local
#!/bin/bash# THIS FILE IS ADDED FOR COMPATIBILITY PURPOSES## It is highly advisable to create own systemd services or udev rules# to run scripts during boot instead of using this file.## In contrast to previous versions due to parallel execution during boot# this script will NOT be run after all other services.## Please note that you must run 'chmod +x /etc/rc.d/rc.local' to ensure# that this script will be executed during boot.sudo -u myuser /home/myuser/experiment/pgcache/test &>/home/myuser/experiment/pgcache/output.log &touch /var/lock/subsys/local

Собственно, тестирующий скрипт:


test
#!/bin/bashset -eCu -o pipefailpostgresql_service="postgresql-13"# перехожу в директорию, где лежит этот скриптcd "$( dirname "${BASH_SOURCE[0]}" )" || exit $?# настроечные опции. config.sh# ассоциативный массив, связывающий размер кеша PostgreSQL и количество нужных ему hugepages. huge_pages.sh# заголовок для CSV-файловreadonly header='huge_pages(2MiB),pg_cache,pg_cache(KiB),idx1(ms),seq(ms),idx2(ms),heap_blks_read,heap_blks_hit,idx_blks_read,idx_blks_hit,finish'date --iso-8601=seconds# цикл по включенным и отключенным hugepagesfor hp in 'HP' 'NHP'do# Цикл по всем "ступенькам" размера кеша, по которым надо пройтись    for s_b in $shared_buffers   do      data_csv="data${hp}.csv"      if [ ! -s "$data_csv" ]      then         echo "$header" >|"$data_csv"      fi      echo "shared_buffers=$s_b" >|'shared_buffers.conf'      if [ "$hp" = 'HP' ]      then         set_huge_pages=${huge_pages_size["$s_b"]}      else         set_huge_pages=0      fi      huge_pages=$(awk '/^HugePages_Total:/ {print $2}' /proc/meminfo)      if [ $huge_pages -ne $set_huge_pages ]      then         sudo sysctl --quiet --write vm.nr_hugepages="$set_huge_pages"         while [ $(awk '/^HugePages_Total:/ {print $2}' /proc/meminfo) -gt $set_huge_pages ]         do            sleep 1         done      fi      huge_pages=$(awk '/^HugePages_Total:/ {print $2}' /proc/meminfo)      if [ $set_huge_pages -ne $huge_pages ]      then         echo -n 'hugepage_error,' >>"$data_csv"         echo "$(date --iso-8601=seconds)" >>"$data_csv"         continue      fi      echo -n "$huge_pages," >>"$data_csv"      sudo systemctl start "$postgresql_service"      psql --expanded --quiet --tuples-only -c "show shared_buffers" | awk '/^shared_buffers \|/ {printf "%s,", $3}' >>"$data_csv"      psql --expanded --quiet --tuples-only -c "select setting::int*8 as shared_buffers from pg_settings where name='shared_buffers'" | awk '/^shared_buffers \|/ {printf "%s,", $3}' >>"$data_csv"      # prewarm to the cache      psql --quiet -c "select pg_prewarm('random')" -c "select pg_prewarm('random_random_idx')" >/dev/null      # more prewarm      pgbench --no-vacuum --time 100 --file test_idx.sql >/dev/null      # reset stats      psql --quiet -c "select pg_stat_reset_single_table_counters('random'::regclass),pg_stat_reset_single_table_counters('random_random_idx'::regclass)" >/dev/null      # first test index search      pgbench --no-vacuum --transaction 100 --report-latencies --file test_idx.sql | awk '/select random from random where random=\(select random\(\)::real\);$/ {printf "%s,", $1 >>"'"$data_csv"'";}'      # test sequence scan      pgbench --no-vacuum --transaction 1 --report-latencies --file test_seq.sql | awk '/select count\(data\) from random;$/ {printf "%s,", $1 >>"'"$data_csv"'"}'      # second test index search      pgbench --no-vacuum --transaction 100 --report-latencies --file test_idx.sql | awk '/select random from random where random=\(select random\(\)::real\);$/ {printf "%s,", $1 >>"'"$data_csv"'";}'      # get stats      sleep 10      psql --quiet --tuples-only --field-separator=' ' --no-align -c "select heap_blks_read,heap_blks_hit,idx_blks_read,idx_blks_hit from pg_statio_user_tables where relname='random'"| head -1 | awk '{printf "%s,", $1 >>"'"$data_csv"'";printf "%s,", $2 >>"'"$data_csv"'";printf "%s,", $3 >>"'"$data_csv"'";printf "%s,", $4 >>"'"$data_csv"'";}'      sudo systemctl stop "$postgresql_service"      echo "$(date --iso-8601=seconds)" >>"$data_csv"   donedonesudo systemctl reboot

Результаты


Здесь и далее по оси X отложены значения кеша PostgreSQL, ОЗУ около 16 Гб, вся неиспользуемая память используется, разумеется, как файловый кеш ОС. По оси Y отложено время выполнения запроса в миллисекундах. Синим цветом без использования HugePages, оранжевым с HugePages. Вид графика коробочки с усиками, подробно про них можно прочитать в документации Excel; удобны тем, что показывают не только усредненные значения, но и разброс, и распределение данных. В каждой итерации было 27 тестов с HugePages и 27 без, итераций было 251, всего было 13554 тестов.


idx1


С увеличением кеша PostgreSQL график раздваивается. Без HugePages скорость выполнения запроса а это простой поиск с использованием индекса падает. С увеличением кеша PostgreSQL только с использованием HugePages PostgreSQL кеш начинает незначительно выигрывать у кеша файловой системы. Когда я тестировал два года назад на CentOS 7 и PostgreSQL 10, кеш файловой системы работал примерно с такой же скоростью, что и кеш PostgreSQL без HugePages, а добавление HugePages давало значимый выигрыш над кешем файловой системы. Из чего я могу сделать вывод, что за последние годы Linux научился гораздо эффективнее использовать свой файловый кеш.


seq


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


idx2


Повторный поиск по индексу. Здесь и демонстрируется тот эффект, который я описывал в самом начале. Если использовать кеш файловой системы (задан небольшой размер кеша PostgreSQL), то при последовательном чтении таблицы индекс вымывается из кеша, что демонстрирует длительный повторный поиск по индексу. Этот эффект пропадает примерно тогда, когда кеш PostgreSQL становится достаточно большим, чтобы индекс хранился в нём, а не в кеше файловой системы, и график становится похож на график, который был до последовательного чтения таблицы: примерно 0,08 мс с HugePages и 0,12 мс без HugePages.


Выводы


Выводы каждый сделает сам :) Я считаю, что не стоит верить ничем не подтвержденным древним рекомендациям каких-то мохнатых годов о том, что кеш PostgreSQL должен быть равен четверти ОЗУ. Со своим кешем он работает заметно лучше.

Подробнее..

Перевод Стать инженером DevOps в 2021 году подробное руководство

21.01.2021 12:12:35 | Автор: admin

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

В этом блоге я попытаюсь ответить на него на примере своего собственного опыта работы DevOps в различных организациях.

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

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

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

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

Вот интересный график тенденций, показывающий популярность DevOps за последние 5 лет.

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

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

Как стать инженером DevOps

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

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

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

Поймите культуру DevOps

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

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

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

Как только вы начнете практиковать культуру DevOps, вы перестанете говорить, что это синоним CI/CD и автоматизации.

Узнайте больше о системах *nix

Мы живем в эпоху, когда не можем жить без систем Linux/Unix. Вы должны лучше понять и получить практические знания о различных дистрибутивах Linux, широко используемых организациями (RHEL, Centos, Ubuntu, CoreOS и т.д.).

Согласно тематическому исследованию Linux Foundation, 90 % рабочей нагрузки в публичных облаках обрабатывается на Linux.

Вот еще одно интересное исследование Redhat, в котором показаны различные дистрибутивы Linux, используемые в публичных облаках:

Теперь у вас есть достаточно причин, по которым вам стоит сосредоточиться на Linux.

Когда дело доходит до Linux, это всё про терминал, графический интерфейс менее предпочтителен в мире *nix.

Для запуска Linux-серверов вы можете использовать VirtualBox или AWS/GCP/Azure и множество других облачных платформ.

Начать изучение можно со следующего:

  • Разберитесь в процессе загрузки Linux.

  • Установите и настройте веб-серверы (Apache, Nginx, Tomcat и т.д.). И узнайте, как работают веб-серверы.

  • Узнайте, как работают процессы Linux.

  • Узнайте, как работает SSH.

  • Почитайте о различных файловых системах.

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

  • Узнайте о важных протоколах (SSL, TLS, TCP, UDP, FTP, SFTP, SCP, SSH).

  • Научитесь управлять сервисами и попробуйте создать сервис самостоятельно (Initd, Systemd).

  • Попробуйте разместить статические и динамические сайты на веб-серверах.

  • Настройте балансировщики нагрузки и реверс-прокси (Nginx, HAproxy и т.д.).

  • Сломайте что-нибудь и научитесь устранять неполадки.

Разберитесь, как работают компоненты инфраструктуры

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

  • Сети

    • Подсеть

    • Публичная сеть

    • Частная сеть

    • Обозначения CIDR

    • Статические/динамические IP-адреса

    • Firewall

    • Прокси

    • NAT

    • Внешний и локальный DNS

    • Диагностика сети

    • VPN

  • Хранилище

    • SAN

    • Бэкапы

    • NFS

  • Отказоустойчивость(HA)

    • Кластер

    • Механизмы отказоустойчивости

    • Аварийное восстановление

  • Безопасность

    • PKI Infrastructure

    • SSL-сертификаты

  • Технология единого входа(SSO)

    • Active Directory/LDAP

  • Балансировщики нагрузки

    • Балансировка на разных уровнях модели OSI (L4, L7)

    • Алгоритмы балансировки нагрузки

    • Reverse Proxy

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

Научитесь автоматизировать

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

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

  • Для среды разработки

    • Vagrant

    • Docker Desktop

    • Minikube

    • Minishift

  • Для обслуживания инфраструктуры

    • Terraform

    • CLI (соответствующего облачного провайдера)

  • Для управления конфигурацией

    • Ansible

    • Chef

    • Puppet

    • Saltstack

  • Управление образами ВМ

    • Packer

Контейнеры, распределенные системы и Service Mesh

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

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

Вот интересная тенденция использования Kubernetes по данным Datadog:

А вот пятилетняя тенденция роста поисковых запросов по Kubernetes:

Кроме того, многие инженеры проявляют интерес к изучению Kubernetes, и в 2021 году немало людей получат сертификаты по этой технологии (CA, CKD и CKD).

Service mesh это выделенный слой инфраструктуры с низкой задержкой для обеспечения взаимодействия между сервисами. Он даёт массу возможностей для межсервисного взаимодействия: балансировки нагрузки, шифрования трафика, авторизации, трассировки, обнаружения сервисов (service discovery) и использования паттерна автоматического выключения (circuit breaker), с которым можно ознакомиться тут. Service mesh это сложная тема, когда дело касается распределенных систем. Если вы новичок в работе с инструментами для контейнеров, вы можете изучить это после получения хороших знаний об архитектуре на основе микросервисов.

Журналирование и мониторинг

Журналирование и мониторинг очень важные аспекты инфраструктуры.

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

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

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

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

Также на основании правил, настроенных в системах мониторинга, будут срабатывать оповещения. Например, оповещение может быть в виде уведомления в Slack или Telegram, задачи в Jira, простого email, SMS-сообщения или даже звонка на телефон. Все схемы оповещения могут разниться от компании к компании.

Как инженер DevOps, вы должны иметь доступ к журналам и уметь устранять неполадки во всех средах (Dev, QA, Stage, Prod). Понимание регулярных выражений очень важно для построения запросов в любом инструменте централизованного хранилища журналов.

Понимание лучших практик в сфере кибербезопасности (DevSecOps)

DevSecOps ещё одна область, связанная с интеграцией практик безопасности на каждом этапе DevOps. Википедия говорит:

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

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

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

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

А вот основные стандартные практики DevSecOps, опубликованные Redhat:

Hashicorp Vault отличный инструмент для управления секретами. Существует множество рабочих процессов для управления секретами среды.

Изучайте программирование

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

Например, для написания конвейера Jenkins в декларативном виде (как код) требуется знания Groovy; кастомный модуль Ansible требует знания Python; для написания оператора Kubernetes требуется опыт работы с Go.

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

  • Bash/Shell

  • Python

  • Go

Go действительно становится популярным в сфере DevOps. Его используют многие инструменты. Например, Kubernetes и Terraform написаны на Go. JFrog исследовал внедрение Go во время GopherCon, и 18 % респондентов заявили, что используют этот язык для работы, связанной с DevOps.

Изучите Git, научитесь документировать, узнайте о GitOps

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

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

А как только вы поймете Git, изучите GitOps. Что этот такое?

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

Ещё очень важно документировать всё, что вы делаете. Каждый репозиторий должен иметь файл README, лучше объясняющий ваш код. Хорошая документация поможет не только вам, но и тем, кто попытается использовать вашу кодовую базу.

Освойте непрерывный жизненный цикл доставки приложений

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

  • Continuous Integration (Непрерывная интеграция)

  • Continuous Delivery (Непрерывная доставка)

  • Continuous Deployment (Непрерывное развертывание)

Научитесь использовать инструменты CI/CD, такие как Jenkins, Gitlab CI, Travis CI и т.д. Вот хорошее графическое представление процесса CI/CD:

DevOps vs SRE

SRE еще одна развивающаяся тема в сообществе DevOps. Это набор практик и философий, разработанных Google. Вот что компания говорит о DevOps и SRE:

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

Я рекомендую изучить официальные документы от Google:

  1. What is SRE?

  2. SRE vs. DevOps: competing standards or close friends?

Читать, читать и еще раз читать

Нет ничего лучше для приобретения знаний, чем чтение. Прочтите хотя бы один технический блог DevOps, связанный с инженерией. Следите за всеми значимыми инженерными блогами, такими как Netflix, Twitter, Google и т.д. Узнайте, как они используют правильный набор инструментов, стратегии развертывания и свои последние проекты с открытым исходным кодом.

Заключение

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

А теперь мне интересно услышать от вас:

Что из культуры DevOps вы применяете на практике у себя?

Каков путь становления DevOps инженером вы прошли?

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

Подробнее..

Быть тимлидом, ч2 Технологии

13.04.2021 12:15:42 | Автор: admin

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


Об уровне владения технологиями



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


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


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


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

Должен ли тимлид постоянно писать код?


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



Я отнёс к блоку технологий в обязанностях тимлида четыре области:


  • инженерная культура;
  • Agile-процессы;
  • SLA;
  • постоянное улучшение качества.

Что я понимаю под инженерной культурой?


На мой взгляд, инженерная культура это комплексное понятие, которое состоит из двух частей: осязаемой (инженерной) и неосязаемой (культурной).


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


  • архитектурные стандарты;
  • типовые решения;
  • правила оформления кода (stylе guides);
  • стандарты code-review;
  • автомацизация рутинных операций;
  • практики сообщества;
  • работа с техдолгом;
  • документация.

Ко второй части относятся неосязаемые понятия, которые также зависят от тимлида:


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

Agile-процессы


Почему я пишу Agile? Почему не рассматриваю другие подходы к разработке? Потому что, на мой взгляд, ничего лучше, чем Extreme Programming и принципы Agile Manifesto ещё не придумали. Как еще нам действовать в условиях неопределённости, в которых работает большинство из нас?


Почему налаживание процессов проблема тимлида? Я уверен, что невозможно выпускать стабильный продукт вовремя без налаженных процессов в разработке. Как мы передаём задачи от продукта в разработку? Как проверяем гипотезы? Как уменьшаем риски вкладывания наших усилий не туда? Как релизим? Как даём быструю обратную связь заказчику? Если на эти вопросы не будет чётких ответов, на каждом этапе разработки будут потери времени и усилий на их прояснение. Поэтому, процессы должны сидеть в подкорке мозга каждого члена команды, включая бизнес, дизайн, разработку. И работа тимлида как раз состоит в том, чтобы эти процессы наладить и закрепить. Для этого хорошо подойдёт cookbook свод правил, который стоит обсудить со всеми членами команды и выложить на Confluence/вики/базу знаний команды, показывать новым членам команды при введении в работу, и постоянно совершенствовать его.


SLA


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



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


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

Мониторинг сервисов


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


Архитектурная схема


Когда я говорю архитектурная схема, я не имею в виду чертёж размером со стену, распечатанный на листах А1 (доводилось видеть и такое). Я, скорее, подразумеваю, что тимлид должен знать все компоненты (микросервисы) в своём владении, бизнес-логику, по которой эти компоненты обмениваются данными, типовые сценарии использования сервиса. Также тимлиду нужно знать путь запроса до приложения со всеми балансировщиками, API-гейтвеями, waf-ами, IPS-ками, ингрессами и прочими проксями это позволит быстро диагностировать проблему и найти ответственных.


Настроенный доступ


Даже при хорошо налаженной схеме дежурства может случиться так, что дежурный недоступен (сел телефон, отключили интернет, крепко спит и т.д.). И в таком случае всё равно реагировать придётся тимлиду. Поэтому важно иметь настроенный доступ во все критические системы, которые могут пригодиться при диагностировании и решении проблемы. Для меня это реплики баз данных, поды Kubernetes/kubectl, ELK, CI/CD, UI RabbitMQ, и конечно VPN, чтобы добраться до всего этого.


Отношения со смежниками


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


Постоянное улучшение качества


Не секрет, что в погоне за time-to-market мы часто принимаем быстрые решения и встраиваем костыли в наш код (конечно же, с намерением вернуться и всё отрефакторить, когда будет время!).



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


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


Заключение


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

Подробнее..

Категории

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

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