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

Performance

Из песочницы Node.js MongoDB перформанс транзакций

18.07.2020 14:05:59 | Автор: admin
Иногда мы платим больше всего за то, что получаем бесплатно. А.Эйнштейн

Не так давно в MongoDB версии 4+ появилась поддержка мульти-документных транзакций.

А поскольку наш проект как раз мигрировал на версию 4.2, закономерно возникли вопросы:

  • Что будет с перформансом?
  • На сколько операции замедлятся?
  • Готовы ли мы пожертвовать скоростью ради (хоть какой-то) точности?

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

  • Все ли операции будут замедлены за счет транзакций?
  • На сколько замедлятся комбинации операций?

Давайте попробуем узнать.

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

Для простоты восприятия разделю имплементацию на 3 шага:

  1. Выбор инструментов
  2. Описание комбинаций операций и получения результатов
  3. Анализ результатов

Теперь о каждом шаге отдельно.

Выбор инструментов:

  1. Необходима тестовая MongoDB (реплика с минимальным количеством mongod процессов) и драйвер для нее: mongodb-memory-server, mongodb.
  2. Для простоты измерения времени я выбрал модуль microseconds
  3. Для анализа полученных результатов и визуализации используем следующее: ttest, stdlib.

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

Имплементируем каждую (из основных) отдельную операцию insertOne, updateOne, deleteOne, findOne, insertMany * updateMany * deleteMany * find * и их комбинации insertOne + updateOne + deleteOne, insertOne + updateOne + deleteOne + findOne, insertMany * + updateMany * + deleteMany * insertMany * + updateMany * + deleteMany * + find * с, и без использования транзакций.

Измерить время выполнения каждой операции.

Для примера insertMany + updateMany + deleteMany с, и без транзакции





Каждую операцию / измерение повторим 300 раз (для анализа будем использовать 100 результатов посередине, то есть с 101-го по 200-й) ** назовем это микроитерациямы (итерациями отдельных операций или комбинаций).

Теперь, постоянно меняя последовательность, проведем 100 макроитерации (1 макроитерация = общее количество микроитараций * 300) *
* количество 300 выбрано абсолютно эмпирически
** для более полной информации об имплементации приглашаю посетить github репозиторий (ссылка ниже по тексту)

Анализ результатов:

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



Далее необходимо провести некоторые расчеты.

Обрезать результаты, которые явно выпадают за пределы выборки



Вычислить среднее значение



Вычислить стандартное отклонение



Определить существование статистически достоверной разницы между выборками с помощью ttest (подтверждение или опровержение нулевой гипотезы)



С помощью простых графиков визуализируем результаты. Для примера возьмем комбинацию insertMany + updateMany + deleteMany и отдельно insertOne (все остальные результаты будут изложены в текстовом формате в разделе Выводы). В результате в сгенерированных html-файлах есть график, название которого соответствует названию операции или комбинации операции (бирюзовым цветом обозначены безтранзакционные итерации, оранжевым транзакционные). Is statistically significant (true / false) говорит о том, была ли вообще какая-то статистически значимая разница. Все остальное абсолютные и относительные значения в микросекундах и процентах соответственно.





Выводы:

  1. Вообще нет никакой разницы между операциями с использованием транзакций и без: insertMany + updateMany + deleteMany (ищите иллюстрацию выше)
  2. Существует небольшая разница (до 7%): updateMany, find, insertOne + updateOne + deleteOne + findOne, insertMany + updateMany + deleteMany + find
  3. Транзакции проходят медленнее, но не так критично (91%): updateOne, deleteMany, findOne
  4. Транзакции значительно медленнее (от 197% до 792%): insertOne, insertMany, deleteOne, insertOne + updateOne + deleteOne

Для получения дополнительной информации и возможности проверить результаты, запустив сценарии самостоятельно, посетите github.

Спасибо за то, что прочитали.

Не стесняйтесь комментировать, надеюсь, что у нас выйдет хорошая дискуссия.

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

Полезные ссылки:
medium.com/cashpositive/the-hitchhikers-guide-to-mongodb-transactions-with-mongoose-5bf8a6e22033
blog.yugabyte.com/are-mongodb-acid-transactions-ready-for-high-performance-applications
medium.com/@Alibaba_Cloud/multi-document-transactions-on-mongodb-4-0-eebd662ac237
www.mongodb.com/blog/post/mongodb-multi-document-acid-transactions-general-availability
docs.mongodb.com/manual/core/write-operations-atomicity
www.dbta.com/Columns/MongoDB-Matters/Limitations-in-MongoDB-Transactions-127057.aspx
dzone.com/articles/multi-document-transactions-on-mongodb-40
www.dbta.com/Columns/MongoDB-Matters/MongoDB-Transactions-In-Depth-125890.aspx
www.codementor.io/@christkv/mongodb-transactions-vs-two-phase-commit-u6blq7465
docs.mongodb.com/manual/core/read-isolation-consistency-recency
mathworld.wolfram.com/Outlier.html
support.minitab.com/en-us/minitab-express/1/help-and-how-to/basic-statistics/inference/how-to/two-samples/2-sample-t/interpret-the-results/key-results
Подробнее..

Перевод Производительность TypeScript

02.12.2020 16:10:54 | Автор: admin

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

1. Написание легкокомпилируемого кода


1.1. Предпочтение интерфейсам, а не пересечениям (intersection)


Чаще всего простой псевдоним для типа объекта действует так же, как интерфейс.

interface Foo { prop: string }type Bar = { prop: string };

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

Интерфейсы создают единый flatten-тип объекта, выявляющий конфликты свойств, которые обычно важно разрешать! А пересечения просто рекурсивно объединяют свойства, и в некоторых случаях генерируют never. Также интерфейсы лучше отображаются, в то время как псевдонимы типов к пересечениям нельзя отобразить в части других пересечений. Взаимосвязи типов между интерфейсами кешируются, в отличие от типов пересечений. И последнее важное различие в том, что при проверке на соответствие целевому типу пересечения каждый компонент проверяется до того, как будет выполнена проверка на соответствие типу effective/flattened.

Поэтому рекомендуется расширять типы с помощью interface/extends, а не создавать пересечения типов.

- type Foo = Bar & Baz & {-     someProp: string;- }+ interface Foo extends Bar, Baz {+     someProp: string;+ }

1.2. Использование аннотирования типов


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

- import { otherFunc } from "other";+ import { otherFunc, otherType } from "other";- export function func() {+ export function func(): otherType {      return otherFunc();  }

1.3. Предпочтение базовым типам, а не множественным


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

interface WeekdaySchedule {  day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday";  wake: Time;  startWork: Time;  endWork: Time;  sleep: Time;}interface WeekendSchedule {  day: "Saturday" | "Sunday";  wake: Time;  familyMeal: Time;  sleep: Time;}declare function printSchedule(schedule: WeekdaySchedule | WeekendSchedule);

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

interface Schedule {  day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday";  wake: Time;  sleep: Time;}interface WeekdaySchedule extends Schedule {  day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday";  startWork: Time;  endWork: Time;}interface WeekendSchedule extends Schedule {  day: "Saturday" | "Sunday";  familyMeal: Time;}declare function printSchedule(schedule: Schedule);

Более реалистичный пример: когда пытаешься смоделировать все встроенные типы элементов DOM. В этом случае предпочтительно создавать базовый тип HtmlElement с частыми элементами, который расширяется с помощью DivElement, ImgElement и т. д., а не создавать тяжёлое множество DivElement | /*...*/ | ImgElement | /*...*/.

2. Использование проектных ссылок


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

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

             ------------              |          |              |  Shared  |              ^----------^             /            \            /              \------------                ------------|          |                |          ||  Client  |                |  Server  |-----^------                ------^-----

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

             ------------              |          |              |  Shared  |              ^-----^----^             /      |     \            /       |      \------------  ------------  ------------|          |  |  Shared  |  |          ||  Client  |  |  Tests   |  |  Server  |-----^------  ------------  ------^-----     |                            |     |                            |------------                ------------|  Client  |                |  Server  ||  Tests   |                |  Tests   |------------                ------------

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

Почитать о межпроектных ссылках можно здесь.

3. Конфигурирование tsconfig.json или jsconfig.json


Пользователи TypeScript и JavaScript всегда могут настроить свои компиляции с помощью файла tsconfig.json. Для настройки редактирования JavaScript также можно использовать файлы jsconfig.json.

3.1. Определение файлов


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

В tsconfig.json можно определять файлы проекта двумя способами:

  • списком files;
  • списками include и exclude;

Основное различие между ними в том, что files получает список путей к исходным файлам, а include/exclude используют шаблоны подстановки (globbing patterns) для определения соответствующих файлов.

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

include/exclude не требуют определять все эти файлы, однако система должна обнаружить их, пройдясь по добавленным директориям. И если их много, компиляция может замедлиться. К тому же иногда в компиляцию включают многочисленные ненужные .d.ts и тестовые файлы, что тоже может снизить скорость и повысить потребление памяти. Наконец, хотя в exclude есть подходящие значения по умолчанию, в некоторых конфигурациях наподобие монорепозиториев есть тяжёлые папки вроде node_modules, которые тоже будут добавлены при компиляции.

Лучше всего поступать так:

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

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

Вот пример tsconfig.json:

{    "compilerOptions": {        // ...    },    "include": ["src"],    "exclude": ["**/node_modules", "**/.*/"],}

3.2. Контроль за добавлением @types


По умолчанию TypeScript автоматически добавляет все найденные в папке node_modules пакеты @types, вне зависимости от того, импортировали вы их или нет. Это сделано для того, чтобы определённые функции просто работали при использовании Node.js, Jasmine, Mocha, Chai и т. д., так как эти инструменты/пакеты не импортируются, а загружаются в глобальное окружение.

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

Duplicate identifier 'IteratorResult'.Duplicate identifier 'it'.Duplicate identifier 'define'.Duplicate identifier 'require'.

Если глобальные пакеты не нужны, то можно в опции types в tsconfig.json/jsconfig.json определить пустую папку:

// src/tsconfig.json{   "compilerOptions": {       // ...       // Don't automatically include anything.       // Only include `@types` packages that we need to import.       "types" : []   },   "files": ["foo.ts"]}

Если же вам нужны глобальные пакеты, внесите их в поле types.

// tests/tsconfig.json{   "compilerOptions": {       // ...       // Only include `@types/node` and `@types/mocha`.       "types" : ["node", "mocha"]   },   "files": ["foo.test.ts"]}

3.3. Инкрементальное генерирование проекта


Флаг --incremental позволяет TypeScript сохранять состояние последней компиляции в файл .tsbuildinfo. Он используется для определения минимального набора файлов, которые могут быть перепроверены/перезаписаны с последнего запуска, по примеру режима --watch в TypeScript.

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

3.4. Пропуск проверки .d.ts


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

TypeScript позволяет с помощью флага skipDefaultLibCheck пропускать проверку типов в поставляемых .d.ts файлах (например, в lib.d.ts).

Также вы можете включить флаг skipLibCheck для пропуска проверки всех .d.ts файлов в компиляции.

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

3.5. Более быстрые вариативные проверки


Список собак или животных? Можно ли присвоить List<Dоg> к List<Animаls>? Простой способ найти ответ заключается в структурном сравнении типов, элемент за элементом. К сожалению, это решение может оказаться очень дорогим. Но если мы достаточно узнаем о List<Т>, то сможем свести проверки на возможность присваивания к определению допустимо ли отнести Dog к Animal (то есть без проверки каждого элемента List<Т>). В частности, нам нужно узнать вариативность типа параметра T. Компилятор может извлечь всю пользу из оптимизации только при включённом флаге strictFunctionTypes (иначе будет использовать более медленную, но более снисходительную структурную проверку). Поэтому рекомендуется собирать с флагом --strictFunctionTypes (который по умолчанию включён под --strict).

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


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

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


4.1. Одновременная проверка типов


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

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

В качестве примера можно привести плагин fork-ts-checker-webpack-plugin для Webpack, или аналогичный awesome-typescript-loader.

4.2. Изолированное создание файлов


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

Нам достаточно редко нужны фичи, которым требуется нелокальная информация. Обычные enum можно использовать вместо const enum, а модули вместо namespace. Поэтому в TypeScript есть флаг isolatedModules для выдачи ошибок на фичах, которым требуется нелокальная информация. С этим флагом вы сможете безопасно применять инструменты, использующие API TypeScript вроде transpileModule или альтернативные компиляторы вроде Babel.

Этот код не будет корректно работать в runtime с изолированным преобразованием файлов, потому что должны быть инлайнены значения const enum. К счастью, isolatedModules заранее предупредит нас.

// ./src/fileA.tsexport declare const enum E {    A = 0,    B = 1,}// ./src/fileB.tsimport { E } from "./fileA";console.log(E.A);//          ~// error: Cannot access ambient const enums when the '--isolatedModules' flag is provided.

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

Изолированно создавать файлы можно с помощью таких инструментов:


5. Расследование проблем


Есть разные способы разобраться в причинах, когда что-то идёт не так.

5.1. Отключение плагинов редактора


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

Для некоторых редакторов есть свои рекомендации по повышению производительности, почитайте их. Например, у Visual Studio Code есть отдельная страница с советами.

5.2. extendedDiagnostics


Можно запустить TypeScript с --extendedDiagnostics, чтобы увидеть, на что тратится время работы компилятора:

Files:                         6Lines:                     24906Nodes:                    112200Identifiers:               41097Symbols:                   27972Types:                      8298Memory used:              77984KAssignability cache size:  33123Identity cache size:           2Subtype cache size:            0I/O Read time:             0.01sParse time:                0.44sProgram time:              0.45sBind time:                 0.21sCheck time:                1.07stransformTime time:        0.01scommentTime time:          0.00sI/O Write time:            0.00sprintTime time:            0.01sEmit time:                 0.01sTotal time:                1.75s

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

Самая релевантная для большинства пользователей информация:

Поле Значение
Files
Количество файлов, входящих в программу (что это за файлы, можно посмотреть с помощью --listFiles).
I/O Read time
Время, потраченное на чтение из файловой системы. Сюда входит и чтение папок из include.
Parse time
Время, потраченное на сканирование и парсинг программы.
Program time
Общее время на чтение из файловой системы, сканирование и парсинг программы, а также прочие вычисления графа. Эти этапы комбинируются здесь, потому что они должны быть разрешены и загружены, как только будут добавлены через import и export.
Bind time
Время, потраченное на сборку разной семантической информации, локальной по отношению к конкретному файлу.
Check time
Время, потраченное на проверку типов в программе.
transformTime time
Время, потраченное на переписывание TypeScript AST (деревьев, представляющих исходные файлы) в виде форм, работающих в старых runtime-средах.
commentTime
Время, потраченное на вычисление комментариев в генерируемых файлах.
I/O Write time
Время, потраченное на запись и обновление файлов на диске.
printTime
Время, потраченное на вычисление строкового представления генерируемого файла и сохранение его на диск.

Что вам может понадобиться с учётом этих входных данных:

  • Соответствует ли примерно количество файлов/строк кода количеству файлов в проекте? Если нет, попробуйте использовать --listFiles.
  • Значения Program time или I/O Read time выглядят большими? Проверьте корректность настроек include/exclude

Похоже, с другими таймингами что-то не так? Можете заполнить отчёт о проблеме! Что вам поможет в диагностике:

  • Запуск с emitDeclarationOnly, если значение printTime высокое.
  • Инструкции по Отчётам о проблемах с производительностью компилятора


5.3. showConfig


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

tsc --showConfig# or to select a specific config file...tsc --showConfig -p tsconfig.json

5.4. traceResolution


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

tsc --traceResolution > resolution.txt

Если вы нашли файл, которого быть не должно, то можете поправить список include/exclude в файле tsconfig.json, или скорректировать настройки вроде types, typeRoots или paths.

5.5. Запуск одного tsc


Часто пользователи сталкиваются с низкой производительностью сторонних сборочных инструментов вроде Gulp, Rollup, Webpack и др. Запуск tsc --extendedDiagnostics для поиска основных расхождений между TypeScript и сторонним инструментом может указать на ошибки внешних настроек или неэффективность работы.

О чём нужно себя спросить:

  • Сильно ли различается время сборки с помощью tsc и инструмента, интегрированного с TypeScript?
  • Если сторонний инструмент имеет средства диагностики, то различается ли решение у TypeScript и стороннего инструмента?
  • Есть ли у инструмента собственная конфигурация, которая может быть причиной низкой производительности?
  • Есть ли у инструмента конфигурация для его интеграции с TypeScript, которая может быть причиной низкой производительности (например, опции для ts-loader)?

5.6. Обновление зависимостей


Иногда на проверку типов в TypeScript могут повлиять вычислительно сложные файлы .d.ts. Редко, но так бывает. Обычно это решается с помощью обновления на новую версию TypeScript (эффективнее) или новую версию пакета @types (который мог обратить регрессию).

6. Частые проблемы


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

6.1. Неправильная настройка include и exclude


Как уже упоминалось, опции include/exclude могут применяться ошибочно.

Проблема Причина Исправление
node_modules был случайно добавлен из глубже вложенной папки. Не был настроен exclude "exclude": ["**/node_modules", "**/.*/"]
node_modules был случайно добавлен из глубже вложенной папки. "exclude": ["node_modules"]
"exclude": ["**/node_modules", "**/.*/"]
Случайно добавлены скрытые файлы с точкой (например, .git). "exclude": ["**/node_modules"]
"exclude": ["**/node_modules", "**/.*/"]
Добавлены неожиданные файлы. Не был настроен include "include": ["src"]

7. Заполнение отчётов о проблемах


Если ваш проект уже правильно и оптимально сконфигурирован, то можно заполнить отчёт о проблеме.

Хороший отчёт содержит доступное и легковоспроизводимое описание проблемы. То есть содержит кодовую базу из нескольких файлов, которую можно легко клонировать через Git. При этом нет необходимости во внешней интеграции со сборочными инструментами, их можно вызвать через tsc, либо использовать изолированный код, применяющий TypeScript API. Нельзя приоритизировать кодовые базы, которые требуют сложных вызовов и настроек.

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

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

7.1. Отчёт о проблемах с производительностью компилятора


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

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

npm install --save-dev typescript@next# oryarn add typescript@next --dev

Описание проблемы с производительностью должно содержать:

  • Установленную версию TypeScript (npx tsc -v или yarn tsc -v).
  • Версию Node, в которой запускался TypeScript (node -v).
  • Результат запуска с опцией extendedDiagnostics (tsc --extendedDiagnostics -p tsconfig.json).
  • В идеале, нужен сам проект, демонстрирующий возникшую проблему.
  • Журнал профилировщика компилятора (файлы isolate-*-*-*.log и *.cpuprofile).

Профилирование компилятора

Важно предоставить нам диагностическую трассировку, запустив Node.js v10+ с флагом --trace-ic и TypeScript с флагом --generateCpuProfile:

node --trace-ic ./node_modules/typescript/lib/tsc.js --generateCpuProfile profile.cpuprofile -p tsconfig.json

Здесь ./node_modules/typescript/lib/tsc.js можно заменить любым путём, по которому установлена ваша версия компилятора TypeScript. А вместо tsconfig.json может быть любой конфигурационный файл TypeScript. Вместо profile.cpuprofile выходной файл на ваш выбор.

Будет сгенерировано два файла:

  • --trace-ic сохранит данные в файл вида isolate-*-*-*.log (например, isolate-00000176DB2DF130-17676-v8.log).
  • --generateCpuProfile сохранит данные в файл с наименованием по вашему выбору. В примере выше это profile.cpuprofile.

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

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

7.2. Отчёт о проблемах с производительностью редактора


У низкой производительности при редактировании может быть много причин. И команда создателей TypeScript может повлиять только на производительность языкового сервиса JavaScript/TypeScript, а также на интеграцию между языковым сервисом и определёнными редакторами (например, Visual Studio, Visual Studio Code, Visual Studio for Mac и Sublime Text). Убедитесь, что в вашем редакторе выключены все сторонние плагины. Это позволит убедиться, что проблема связана с самим TypeScript.

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

Всегда приветствуется добавление данных из tsc --extendedDiagnostics, но ещё лучше, если будет трассировка TSServer.

Получение журналов TSServer в Visual Studio Code

  1. Откройте командную панель, затем:
  2. Задайте опцию typescript.tsserver.log: verbose,.
  3. Перезапустите VS Code и воспроизведите проблему.
  4. В VS Code выполните команду TypeScript: Open TS Server log.
  5. Должен открыться файл tsserver.log.

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

Процессор, эмулирующий сам себя может быть быстрее самого себя

04.06.2021 04:18:40 | Автор: admin

Современный мир ПО содержит настолько много слоёв, что оптимизации могут быть в самых неожиданных местах. Знакомьтесь - год 2000, проект HP Dynamo. Это эмулятор процессора PA-8000, работающий на этом же процессоре PA-8000, но с технологией JIT. И реальные программы, запускающиеся в эмуляторе - в итоге работают быстрее, чем на голом процессоре.

td;dr - всё сказано в заголовке

Программистам из HP Labs стало интересно, а что будет, если написать оптимизирующий JIT компилятор под ту же платформу, на которой он работает. Работа заняла несколько лет. Под эмулятором можно было запускать немодифицированные родные бинари. И результаты оказались несколько неожиданными.

В эмуляторе они искали "hot paths" и оптимизировали ход исполнения кода. Таким образом уменьшались расходы на джампы, вызов функций, динамических библиотек, оптимизации работы с кешем процессора. Результаты повышения производительности доходили до +22%, в среднем по тестам получалось +9%.

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

Если кому интересны подробности:

1. http://cseweb.ucsd.edu/classes/sp00/cse231/dynamopldi.pdf
2. https://stackoverflow.com/questions/5641356/why-is-it-that-bytecode-might-run-faster-than-native-code/5641664#5641664
3. https://en.wikipedia.org/wiki/Just-in-time_compilation

Подробнее..

Перевод Повышение производительности дебажных билдов в два-три раза

18.06.2021 02:21:22 | Автор: admin

Нам удалось добиться значительного повышения производительности рантайма для дебажной (отладочной) конфигурации по умолчанию Visual Studio в компиляторе C++ для x86/x64. Для программ, скомпилированных в режиме дебага в Visual Studio 2019 версии 16.10 Preview 2, мы отмечаем ускорение в 23 раза. Эти улучшения связаны с уменьшением накладных расходов на проверки ошибок в рантайме (/RTC), которые включены по умолчанию.

Дебажная конфигурация по умолчанию (Default debug configuration)

Когда вы компилируете свой код в Visual Studio с дебажной конфигурацией, по умолчанию компилятору C++ передаются некоторые флаги. Наиболее релевантными для этой статьи являются /RTC1, /JMC и /ZI.

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

Рассмотрим следующую простую функцию:

int foo() {    return 32;}

и сборку для x64, сгенерированную компилятором 16.9 при компиляции с флагами /RTC1 /JMC /ZI (ссылка Godbolt):

int foo(void) PROC                  $LN3:        push rbp        push rdi        sub rsp, 232                ; дополнительное пространство, выделенное из-за /ZI, /JMC        lea rbp, QWORD PTR [rsp+32]        mov rdi, rsp        mov ecx, 58                 ; (= x)        mov eax, -858993460         ; 0xCCCCCCCC        rep stosd                   ; записать 0xCC в стек для x DWORDов        lea rcx, OFFSET FLAT:__977E49D0_example@cpp        ; вызов из-за /JMC        call __CheckForDebuggerJustMyCode        mov eax, 32        lea rsp, QWORD PTR [rbp+200]        pop rdi        pop rbp        ret 0 int foo(void) ENDP

В показанной выше сборке флаги /JMC и /ZI добавляют в сумме 232 дополнительных байта в стек (строка 5). Это пространство в стеке не всегда необходимо. В сочетании с флагом /RTC1, который инициализирует выделенное пространство стека (строка 10), это потребляет много тактов ЦП. В этом конкретном примере, хоть выделенное пространство стека необходимо для правильного функционирования /JMC и /ZI, его инициализация - нет. Мы можем убедиться во время компиляции, что в этих проверках нет необходимости. Таких функций предостаточно в любой реальной кодовой базе на C++ - отсюда и выигрыш в производительности.

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

/RTC1

Использование флага /RTC1 эквивалентно использованию обоих флагов /RTCs и /RTCu. /RTCs инициализирует стек функций с 0xCC для выполнения различных проверок в рантайме, а именно обнаружения неинициализированных локальных переменных, обнаружения переполнения или недозаполнения массива и проверки указателя стека (для x86). Вы можете посмотреть код, раздутый /RTC, здесь.

Как видно из приведенного выше ассемблерного кода (строка 10), инструкция rep stosd, внесенная /RTCs, является основной причиной замедления работы. Ситуация усугубляется, когда /RTC (или /RTC1) используется вместе с /JMC, /ZI или обоими.

Взаимодействие с /JMC

/JMC означает Just My Code Debugging (функциональ дебага только моего кода), и во время отладки он автоматически пропускает функции, написанные не вами (например, фреймворк, библиотека и другой непользовательский код). Он работает, вставляя вызов функции в пролог, который вызывает рантайм библиотеку. Это помогает дебагеру различать пользовательский и непользовательский код. Проблема здесь в том, что вставка вызова функции в пролог каждой функции в вашем проекте означает, что во всем вашем проекте больше не будет листовых функций (leaf functions). Если функции изначально не нужен какой-либо стек фрейм, теперь он ей будет нужен, потому что в соответствии с AMD64 ABI для Windows платформ нам нужно иметь по крайней мере четыре слота стека, доступные для параметров функции (так называемая домашняя область параметров - Param Home area). Это означает, что все функции, которые ранее не инициализировались /RTC, потому что они были листовыми функциями и не имели стек фрейма, теперь будут инициализированы. Наличие множества листовых функций в вашей программе - это нормально, особенно если вы используете сильно шаблонную библиотеку, такую как STL. В этом случае /JMC с радостью съест часть ваших тактов ЦП. Это не относится к x86 (32 бит), потому что там у нас нет домашней области параметров. Вы можете посмотреть эффекты /JMC здесь.

Взаимодействие с /ZI

Следующее взаимодействие, о котором мы поговорим, будет с /ZI. Он позволяет вашему коду использовать функцию Edit and Continue (изменить и продолжить), что означает, что вам не нужно перекомпилировать всю программу во время дебага для небольших изменений.

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

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

Решение

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

Ситуация немного усложняется, когда вы компилируете с edit-and-continue, потому что теперь вы можете добавлять неинициализированные переменные в процессе дебага, которые могут быть обнаружены только в том случае, если мы инициализируем область стека. А мы, скорее всего, этого не сделали. Чтобы решить эту проблему, мы включили необходимые биты в дебажную информацию и предоставили ее через Debug Interface Access SDK. Эта информация сообщает дебагеру, где область заполнения, введенная /ZI начинается и заканчивается. Она также сообщает дебагеру, нужна ли функции инициализация стека. Если да, то отладчик безоговорочно инициализирует область стека в этом диапазоне памяти для функций, которые вы редактировали во время сеанса дебаггинга. Новые переменные всегда размещаются поверх этой инициализированной области, и наши проверки в рантайме теперь могут определить, безопасен ли ваш недавно добавленный код или нет.

Результаты

Мы скомпилировали следующие проекты в конфигурации отладки по умолчанию, а затем использовали сгенерированные исполняемые файлы для проведения тестов. Мы заметили 23-кратное улучшение во всех проектах, которые мы тестировали. Для проектов с сильным использованием STL могут потребоваться более значительные улучшения. Сообщите нам в комментариях о любых улучшениях, которые вы заметили в своих проектах. Проект 1 и Проект 2 предоставлены пользователями.

Расскажите нам, что вы думаете!

Мы надеемся, что это ускорение сделает ваш рабочий процесс дебаггинга эффективным и приятным. Мы постоянно прислушиваемся к вашим отзывам и работаем над улучшением вашего рабочего цикла. Мы хотели бы услышать о вашем опыте в комментариях ниже. Вы также можете связаться с нами в сообществе разработчиков, по электронной почте (visualcpp@microsoft.com) и в Twitter (@VisualC).


Напоминаем о том, что сегодня, в рамках курса "C++ Developer. Basic" пройдет второй день бесплатного интенсива по теме: "HTTPS и треды в С++. От простого к прекрасному".

Подробнее..

Как мы нарисовали на карте несколько тысяч интерактивных объектов без вреда для перформанса

29.07.2020 10:12:50 | Автор: admin

Привет, меня зовут Дарья, и я Frontend-разработчик юнита Гео вАвито. Хочу поделиться опытом того, как мы сделали навебе новый поиск покарте, заменив кластеры более удобным решением и сняв ограничение наколичество отображаемых объектов.


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



Чем нас не устраивал старый поиск покарте


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


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


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



Что мы решили изменить


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


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


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


$limit = viewPort.width viewPort.height / (pinDiameter^2 9)$


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


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


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

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



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



Выбраны фильтры, которые дают небольшую выдачу показываем пины



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


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


Как нарисовали на карте несколько тысяч точек


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



Поиск двухкомнатных квартир повсей России


Мы используем Яндекс-карты, API которых предоставляет разные способы отрисовки объектов. Например, кластеры мы рисовали черезинструмент ObjectManager, и он отлично подходит дляслучаев, когда количество объектов накарте не превышает 1000. Если попробовать нарисовать сего помощью, например, 3000объектов, карта начинает подтормаживать привзаимодействии сней.


Мы понимали, что может появиться необходимость отрисовать несколько тысяч объектов безвреда дляпроизводительности. Поэтому посмотрели всторону ещё одного API Яндекс-карт картиночного слоя, длякоторого используется класс Layer.


Основной принцип этого API заключается втом, что вся карта делится натайлы (изображения вpng или svg формате) фиксированного размера, которые маркируются через номера X, Y и зум Z. Эти тайлы накладываются поверх самой карты, и витоге каждая область представляется каким-то количеством изображений взависимости отразмера области и разрешения экрана. Собственно, API берёт насебя всю фронтовую часть, запрашивая нужные тайлы привзаимодействии скартой (изменении уровня зума и сдвиге), а бэкенд-часть нужно писать самостоятельно.



Разбиение натайлы. Длянаглядности их границы выделены черными линиями. Каждый квадрат отдельное изображение


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


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


func (*Tile) Deg2num(t *Tile) (x int, y int) {    x = int(math.Floor((t.Long + 180.0) / 360.0 * (math.Exp2(float64(t.Z)))))    y = int(math.Floor((1.0 - math.Log(math.Tan(t.Lat*math.Pi/180.0)+1.0/math.Cos(t.Lat*math.Pi/180.0))/math.Pi) / 2.0 * (math.Exp2(float64(t.Z)))))    return}func (*Tile) Num2deg(t *Tile) (lat float64, long float64) {    n := math.Pi - 2.0*math.Pi*float64(t.Y)/math.Exp2(float64(t.Z))    lat = 180.0 / math.Pi * math.Atan(0.5*(math.Exp(n)-math.Exp(-n)))    long = float64(t.X)/math.Exp2(float64(t.Z))*360.0 - 180.0    return lat, long}

Приведённый код написан наGo, формулы длядругих языков впроекции Меркатора можно найти поссылке.


Нарисовав svg поданным, мы получили подобные изображения тайлов:



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


const createTilesUrl = (tileNumber, tileZoom) => {// params - выбранные фильтры в формате строки параметров, которые можно передать в GET-запросreturn `/web/1/map/tiles?${params}&z=${tileZoom}&x=${tileNumber[0]}&y=${tileNumber[1]}`;};const tilesLayer = new window.ymaps.Layer(createTilesUrl, { tileTransparent: true });ymap.layers.add(tilesLayer);

В итоге карта стайлами, подготовленными набэкенде и отрисованными черезLayer, выглядит так:



Как оптимизировали бэкенд


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


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


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



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


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


Время жизни разных запросов вRedis отличается. Дляшироких и популярных запросов, например, повсей России безфильтров, оно больше, чем длязапросов сузкими фильтрами. Кроме долгого кэша вRedis есть быстрый кэш непопулярных запросов вin-memory. Он позволяет уменьшить нагрузку насеть и освободить место вRedis.


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



Сейчас среднее время ответа запроса тайла на99перцентиле составляет ~140ms. То есть 99% измеряемых запросов выполняются за это или меньшее время. Длясравнения: вреализации черезкластеры запрос выполнялся ~230ms натом же перцентиле.


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



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


Кликабельность, ховер и просмотренность точек


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


Мы поняли, что стабильнее будет реализовать кликабельность нафронте. Помимо запросов затайлами, унас всегда отправлялся один запрос запинами. Пины мы рисуем нафронте, и фронт ничего не знает прото, рисовать вданный момент времени пины или точки. Запросы запинами и тайлами уходят всегда накаждый сдвиг или изменение уровня зума. Чтобы не усложнять и рисовать всё быстрее, тайлы длянепустых выдач всегда возвращаются сточками. Если нужно рисовать пины, они рисуются поверх точек, перекрывая их. Поэтому всё, что нам оставалось длякликабельности точек, добавить вответ запроса запинами объект точек, который будет содержать координаты, id и количество объявлений вэтой точке. Этих данных достаточно для того, чтобы нарисовать поклику наточку пин.



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



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


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


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


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



Просмотренный пин выделен бледно-голубым



Просмотренная точка выделена бледно-голубым


Заключение


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

Подробнее..

Перевод Оценка производительности CNI для Kubernetes по 10G сети (август 2020)

12.09.2020 00:10:18 | Автор: admin


TL;DR: Все CNI работают как надо, за исключением Kube-Router и Kube-OVN, Calico за исключением автоматического определения MTU лучше всех.


Статья-обновление моих прошлых проверок (2018 и 2019), на момент тестирования я использую Kubernetes 1.19 в Ubuntu 18.04 с обновленными CNI на август 2020 года.


Прежде чем погрузиться в метрики...


Что нового с апреля 2019?


  • Можно протестировать на собственном кластере: можно запускать тесты на собственном кластере с использованием нашего инструмента Kubernetes Network Benchmark: knb
  • Появились новые участники
  • Новые сценарии: текущие проверки запускают тесты производительности сети "Pod-to-Pod", также добавлен новый сценарий "Pod-to-Service", запускающий тесты, приближенные к реальным условиям. На практике ваш Pod с API работает с базой в виде сервиса, а не через Pod ip-адрес (конечно же мы проверяем и TCP и UDP для обоих сценариев).
  • Потребление ресурсов: каждый тест сейчас имеет собственное сравнение ресурсов
  • Удаление тестов приложений: мы больше не делаем тесты HTTP, FTP и SCP, поскольку наше плодотворное сотрудничество с сообществом и сопровождающими CNI обнаружило пробел между результатами iperf по TCP и результатами curl из-за задержки в запуске CNI (первые несколько секунд при запуске Pod, что не характерно в реальных условиях).
  • Открытый исходный код: все источники тестов (скрипты, настройки yml и исходные "сырые" данные) доступны здесь

Эталонный протокол тестирования


Протокол подробно описан здесь, обратите внимание, что эта статья посвящена Ubuntu 18.04 с ядром по умолчанию.


Выбор CNI для оценки


Это тестирование нацелено на сравнение CNI, настраиваемых одним файлом yaml (поэтому исключены все, устанавливаемые скриптами, типа VPP и прочих).


Выбранные нами CNI для сравнения:


  • Antrea v.0.9.1
  • Calico v3.16
  • Canal v3.16 (Flannel network + Calico Network Policies)
  • Cilium 1.8.2
  • Flannel 0.12.0
  • Kube-router latest (20200825)
  • WeaveNet 2.7.0

Настройка MTU для CNI


В первую очередь проверяем влияние автоматического определения MTU на производительность TCP:



Влияние MTU на производительность TCP


Еще больший разрыв обнаруживается при использовании UDP:



Влияние MTU на производительность UDP


С учетом ОГРОМЕННОГО влияния на производительность, раскрытого в тестах, мы хотели бы отправить письмо-надежду всем сопровождающим CNI: пожалуйста, добавьте автоматическое определение MTU в CNI. Вы спасете котяток, единорожек и даже самого симпатичного: маленького девопсика.
Тем не менее если вам по-любому надо взять CNI без поддержки автоматического определения MTU можно настроить его руками для получения производительности. Обратите внимание, что это относится к Calico, Canal и WeaveNet.



Моя маленькая просьба к сопровождающим CNI...


Тестирование CNI: необработанные данные


В этом разделе мы сравним CNI с правильным MTU (определенным автоматически, либо выставленным руками). Основная цель здесь показать в виде графиков необработанные данные.


Цветовая легенда:


  • серый образец (т.е. голое железо)
  • зеленый пропускная способность выше 9500 мбит\с
  • желтый пропускная способность выше 9000 мбит\с
  • оранжевый пропускная способность выше 8000 мбит\с
  • красный пропускная способность ниже 8000 мбит\с
  • синий нейтральный (не связано с пропускной способностью)

Потребление ресурсов без нагрузки


В первую очередь проверка потребления ресурсов, когда кластер "спит".



Потребление ресурсов без нагрузки


Pod-to-Pod


Этот сценарий подразумевает, что клиентский Pod подключается напрямую к серверному Pod по его ip-адресу.



Сценарий Pod-to-Pod


TCP


Результаты Pod-to-Pod TCP и соответствующее потребление ресурсов:




UDP


Результаты Pod-to-Pod UDP и соответствующее потребление ресурсов:




Pod-to-Service


Этот раздел актуален для реальных случаев использования, клиентский Pod соединяется с серверным Pod через сервис ClusterIP.



Сценарий Pod-to-Service


TCP


Результаты Pod-to-Service TCP и соответствующее потребление ресурсов:




UDP


Результаты Pod-to-Service UDP и соответствующее потребление ресурсов:




Поддержка политик сети


Среди всех вышеперечисленных, единственный, не поддерживающий политики, это Flannel. Все остальные правильно реализуют сетевые политики, включая входящие и исходящие. Отличная работа!


Шифрование CNI


Среди проверяемых CNI есть те, что могут шифровать обмен по сети между Pod:


  • Antrea с помощью IPsec
  • Calico с помощью wireguard
  • Cilium с помощью IPsec
  • WeaveNet с помощью IPsec

Пропускная способность


Поскольку осталось меньше CNI сведем все сценарии в один график:



Потребление ресурсов


В этом разделе мы будет оценивать ресурсы, используемые при обработке связи Pod-to-Pod в TCP и UDP. Нету смысла рисовать график Pod-to-Service, поскольку он не дает дополнительной информации.




Сводим все вместе


Давайте попробуем повторить все графики, мы тут немного привнесли субъективности, заменяя фактические значения словами "vwry fast", "low" и т.п.



Заключение и мои выводы


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


Я рад, что появились новые CNI, Antrea выступил неплохо, реализовано много функций даже в ранних версиях: автоматическое определение MTU, шифрование и простая установка.


Если сравнивать производительность все CNI работают хорошо, кроме Kube-OVN и Kube-Router. Kube-Router также не смог определить MTU, я не нашел нигде в документации способа его настройки (тут открыт запрос на эту тему).


Что касается потребления ресурсов, то по-прежнему Cilium использует больше оперативной памяти, чем другие, но компания-производитель явно нацелена на крупные кластеры, что явно не то же самое, как тест на трехузловом кластере. Kube-OVN также потребляет много ресурсов процессорного времени и оперативной памяти, но это молодой CNI, основанный на Open vSwitch (как и Antrea, работающий лучше и с меньшим потреблением).


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


Также кроме прочего, производительность шифрования настроящий восторг. Calico один из старейших CNI, но шифрование было добавлено всего пару недель назад. Они выбрали wireguard вместо IPsec, и проще говоря, все работает великолепно и потрясно, полностью вытесняя другие CNI в этой части тестирования. Конечно же растет потребление ресурсов из-за шифрования, но достигаемая пропускная способность того стоит (Calico в тесте с шифрованием показал шестикратное превосходство по сравнению с Cilium, занимающим второе место). Больше того, можно включить wireguard в любое время после развертывания Calico в кластере, а также, если пожелаете, можете отключить его на короткое время или навсегда. Это невероятно удобно, но! Мы напоминаем, что Calico не умеет сейчас автоматически определять MTU (эта функция запланирована в следующих версиях), так что не забывайте настраивать MTU, если ваша сеть поддерживает Jumbo Frames (MTU 9000).


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


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


  • Нужен CNI для сильно мелкого кластера, ИЛИ мне не нужна безопасность: работайте с Flannel, наиболее легким и стабильным CNI (он же один из старейших, его по легенде изобрел Homo Kubernautus или Homo Contaitorus). Также вас возможно заинтересует гениальнейший проект k3s, проверьте!
  • Нужен CNI для обычного кластера: Calico ваш выбор, но не забывайте настроить MTU, если оно нужно. Легко и непринужденно можно играть с сетевыми политиками, включать и выключать шифрование и т.п.
  • Нужен CNI для (очень) крупномасштабного кластера: ну, тест не показывает поведение больших кластеров, я был бы рад провести тесты, но у нас нету сотен серверов с подключением 10гбит\с. Так что лучший вариант запуск модифицированного теста на ваших узлах, хотябы с Calico и Cilium.
Подробнее..

Дорогой DELETE. Николай Самохвалов (Postgres.ai)

15.10.2020 10:04:28 | Автор: admin


Когда-нибудь в далёком будущем автоматическое удаление ненужных данных будет одной из важных задач СУБД [1]. Пока же нам самим нужно заботиться об удалении или перемещении ненужных данных на менее дорогие системы хранения. Допустим, вы решили удалить несколько миллионов строк. Довольно простая задача, особенно если известно условие и есть подходящий индекс. "DELETE FROM table1 WHERE col1 = :value" что может быть проще, да?


Видео:




  • Я в программном комитете Highload с первого года, т. е. с 2007-го.


  • И с Postgres я с 2005-го года. Использовал его во многих проектах.


  • Группа с RuPostges тоже с 2007-го года.


  • Мы на Meetup доросли до 2100+ участников. Это второе место в мире после Нью-Йорка, обогнали Сан-Франциско уже давно.


  • Несколько лет я живу в Калифорнии. Занимаюсь больше американскими компаниями, в том числе крупными. Они активные пользователи Postgres. И там возникают всякие интересные штуки.




https://postgres.ai/ это моя компания. Мы занимаемся тем, что автоматизируем задачи, которые устраняют замедление разработки.


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



https://www.seagate.com/files/www-content/our-story/trends/files/idc-seagate-dataage-whitepaper.pdf


Я был недавно на VLDB в Лос-Анджелесе. Это самая большая конференция по базам данных. И там был доклад о том, что в будущем СУБД будут не только хранить, а еще и автоматически удалять данные. Это новая тема.


Данных все больше в мире зетабайт это 1 000 000 петабайт. И сейчас уже оценивается, что у нас больше 100 зетабайт данных в мире хранится. И их становится все больше и больше.



https://vldb2019.github.io/files/VLDB19-keynote-2-slides.pdf


И что с этим делать? Понятно, что надо удалять. Вот ссылка на этот интересный доклад. Но пока что в СУБД это не реализовано.


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



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



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



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



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



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



БД растет и растет. Ежесуточный DELETE немножко медленней работать начинает.



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



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


Он проверил на dev, на staging все Ок. Естественно, нужно еще почистить то, что накопилось. Он проверил, все работает.



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



Что пошло не так? Вот здесь дан список того, что могло пойти не так. Что из этого самое важное?


  • Например, не было review, т. е. DBA-эксперт не посмотрел. Он бы опытным взглядом сразу нашел бы проблему, к тому же у него есть доступ к prod, где накопилось несколько миллионов строчек.


  • Может быть, проверяли как-то не так.


  • Может быть железо устарело и нужно апгрейд для этой базы делать.


  • Или что-то с сомой базой данных не так, и нам с Postgres на MySQL надо переехать.


  • Или, может быть, с операцией что-то не так.


  • Может быть, какие ошибки в организации работы и нужно кого-то уволить, а нанять лучших людей?




Не было проверки DBA. Если DBA был бы, он увидел бы эти несколько миллионов строчек и даже без всяких экспериментов сказал бы: Так не делают. Допустим, если бы этот код был в GitLab, GitHub и был бы процесс code review и не было такого, что без утверждения DBA эта операция пройдет на prod, то очевидно DBA сказал бы: Так нельзя делать.



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



http://bit.ly/nancy-hl2018-2


Вторая ошибка проверяли не там. Мы постфактум увидели, что мусорных данных накопилось на prod много, а у разработчика не было в этой базе накопленных данных, да и на staging особо никто этот мусор не создавал. Соответственно, там было 1 000 строчек, которые быстро отработали.


Мы понимаем, что наши тесты слабые, т. е. процесс, который выстроен, не ловит проблемы. Не проводили адекватный БД-эксперимент.


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



Может быть, у нас оборудование плохое? Если посмотреть, то latency подскочило. Мы увидели, что утилизация 100 %. Конечно, если это были бы современные NVMe диски, то, наверное, нам было бы намного легче. И, возможно, мы бы не легли от этого.


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



А можно ли как-то поменьше диски трогать? И тут как раз с помощью DBA мы ныряем в некоторую тему, которая называется checkpoint tuning. Выясняется, что у нас не был проведен checkpoint tuning.


Что такое checkpoint? Это есть в любой СУБД. Когда у вас данные в памяти меняются, они не сразу записываются на диски. Информация о том, что данные изменились, сначала записывается в опережающий журнал, в write-ahead log. И в какой-то момент СУБД решает, что пора уже реальные страницы на диск скинуть, чтобы, если у нас будет сбой, поменьше делать REDO. Это как в игрушке. Если нас убьют, мы будем начинать игру с последнего checkpoint. И все СУБД это реализуют.



Настройки в Postgres отстают. Они рассчитана на 10-15-летней давности объемы данных и операций. И checkpoint не исключение.


Вот эта информация из нашего отчета c Postgres check-up, т. е. автоматическая проверка здоровья. И вот какая-то база в несколько терабайт. И видно хорошо, что принудительные checkpoints почти в 90 % случаев.


Это что значит? Там есть две настройки. Checkpoint может по timeout наступить, например, в 10 минут. Или он может наступить, когда наполнилось довольно много данных.


И по умолчанию max_wal_saze выставлен в 1 гигабайт. По факту, это реально случается в Postgres через 300-400 мегабайт. Вы поменяли столько данных и у вас checkpoint случается.


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


И нам нужно сделать так, чтобы он наступал пореже. Т. е. мы можем поднять max_wal_size. И он будет наступать реже.


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



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


Первая серия мы меняем max_wal_size. И проводим массовую операцию. Сначала делаем ее на дефолтной настройке в 1 гигабайт. И делаем массовый DELETE многих миллионов строчек.


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


Дальше мы увеличиваем max_wal_size. Повторяем. Увеличиваем, повторяем. И так много раз. В принципе, 10 точек это хорошо, где 1, 2, 4, 8 гигабайт. И смотрим поведение конкретной системы. Понятно, что здесь оборудование должно быть как на prod. У вас должны быть те же диски, сколько же памяти и настройки Postgres такие же.


И таким образом мы обменяем нашу систему, и знаем, как будет себя вести СУБД при плохом массовом DELETE, как будет она checkpointиться.


Checkpoint по-русски это контрольные точки.


Пример: DELETE несколько млн строк по индексу, строки разбросаны по страницам.



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


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



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



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


Почему так?



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


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


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



Но это еще не все. В Postgres странички весят 8 килобайт, а в Linux 4 килобайт. И есть настройка full_page_writes. По умолчанию она включена. И это правильно, потому что, если мы ее выключим, то есть опасность, что при сбое только половинка странички сохранится.


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


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


И, соответственно, если checkpoint еще раз случился, то мы должны снова с нуля все начинать и всю страничку запихивать. При частых checkpoints, когда мы гуляем по одним и тем же страничкам, full_page_writes = on будет больше, чем могло бы быть, т. е. мы больше WAL генерируем. Больше отправляется на реплики, в архив, на диск.


И, соответственно, две избыточности у нас возникают.



Если мы увеличиваем max_wal_size, получается, что мы облегчаем работу и checkpoint, и wal writer. И это классно.


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


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


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


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


Мы замеряем такую ситуацию для разного размера max_wal_size и понимаем, что, если max_wal_size 64 гигабайта, то в двойной худшей ситуации мы будем подниматься 10 минут. И думаем устраивает нас это или нет. Это бизнес-вопрос. Мы должны показать эту картину тем, кто отвечает за бизнес-решения и спросить: Сколько мы можем пролежать максимум в случае проблемы? Можем ли мы полежать в худшей ситуации 3-5 минут?. И принимаете решение.


И тут есть интересный момент. У нас на конференции есть пара докладов про Patroni. И, возможно, вы его используете. Это autofailover для Postgres. GitLab и Data Egret об этом рассказывали.


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


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


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



Вам, чтобы итерации делать, например, max_wal_size =1, 8, нужно повторять массовую операцию много раз. Вы ее сделали. И на той же базе хотите ее еще раз сделать, но вы же уже все удалили. Что делать?


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


Но в данном случае нам повезло. Если, как здесь написано BEGIN, DELETE, ROLLBACK, то мы можем DELETE повторять. Т. е. если мы его отменили сами, то мы можем его повторять. И физически у вас данные будут там же лежать. У вас даже bloat никакого не образуется. Вы можете итерировать на таких DELETE.


Такой DELETE c ROLLBACK идеальный для checkpoint tuning, даже если у вас нет нормально развернутой database labs.



Мы сделали табличку с одной колонкой i. У Postgres есть служебные колонки. Они невидимые, если их специально не попросить. Это: ctid, xmid, xmax.


Ctid это физический адрес. Нулевая страница, первый кортеж в странице.


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



Xmax это время смерти кортежа. Он проставился, но Postgres знает, что эта транзакция была откачена, поэтому, что 0, что откаченная транзакция неважно. Это говорит о том, что по DELETE можно итерировать и проверять массовые операции поведения системы. Можно сделать database labs для бедных.



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


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


Почему важно разбивать?


  • Если мы видим, что диску тяжело, то давайте замедлим. И если у нас разбито, то мы можем паузы добавить, можем замедлить throttling.


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




https://postgres.ai/products/joe/


Это интересно. Я часто встречаю, что разработчики спрашивают: Какой размер пачки выбрать?.


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


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


Почему секунду? Объяснение очень простое и понятное всем, даже не техническим людям. Мы видим реакцию. Возьмем 50 миллисекунд. Если что-то изменилось, то глаз наш среагирует. Если меньше, то сложнее. Если что-то отвечает через 100 миллисекунд, например, вы мышкой нажали, и оно вам через 100 миллисекунд ответило, вы уже чувствуете эту небольшую задержку. Секунда уже воспринимается как тормоза.


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


Мы подбираем размер пачки. В каждом случае можем по-разному это делать. Можно автоматизировать. И убеждаемся в эффективности работы обработки одной пачки. Т. е. мы делаем DELETE одной пачки или UPDATE.


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


И мы смотрим, что план отличный. Видно index scan, еще лучше index only scan. И у нас небольшое количество данных задействовано. И все меньше секунды отрабатывает. Супер.


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


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


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



https://docs.gitlab.com/ee/development/background_migrations.html


Какие есть стратегии разбиения? Я вижу 3 разные стратегии разбиения, которые используют разработчики на пачке.


Первая очень простая. У нас есть айдишник числовой. И давайте мы разобьем на разные интервалы, и будем работать с этим. Минус понятен. В первом отрезке у нас реального мусора может попасть 100 строчек, во втором 5 строчек или вообще не попасть, или все 1 000 строчек окажутся мусором. Очень неравномерная работа, но зато разбивать легко. Взяли максимальный ID и разбили. Это наивный подход.


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


В первой стратегии тоже, кстати, можно это делать в несколько потоков. Это не сложно.



https://medium.com/@samokhvalov/how-partial-indexes-affect-update-performance-in-postgres-d05e0052abc


Но есть более классный и оптимальный подход. Это третья стратегия. И когда это возможно, то лучше ее выбирать. Мы на основе специального индекса это делаем. В данном случае это будет, скорее всего, индекс по нашему условию мусора и ID. Мы включим ID, чтобы это был index only scan, чтобы мы в heap не ходили.


Как правило, index only scan это быстрее, чем index scan.



И мы быстро находим наши айдишники, которые мы хотим удалить. BATCH_SIZE мы подбираем заранее. И мы их не только получаем, мы их получаем специальным образом и тут же лочим. Но так лочим, что, если они уже залочены, мы их не лочим, а едем дальше и берем следующие. Это for update skip locked. Эта суперфича Postgres нам позволяет в несколько потоков работать, если мы хотим. Можно в один поток. И тут есть CTE это один запрос. И у нас во втором этаже этого CTE происходит реальное удаление returning *. Можно returning id, но лучше *, если у вас данных немного в каждой строчке.



Зачем нам это нужно? Этом нам нужно для того, чтобы отчитаться. Мы сейчас удалили столько строчек по факту. И у нас границы по ID или по created_at вот такие-то. Можно min, max сделать. Еще что-то можно сделать. Тут можно многое запихать. И это для мониторинга очень удобно.


Насчет индекса есть еще одно замечание. Если мы решили, что нам именно для этой задачи нужен специальный индекс, то нужно убедиться, что он не испортит heap only tuples updates. Т. е. в Postgres есть такая статистика. Это можно посмотреть в pg_stat_user_tables для вашей таблицы. Вы можете посмотреть используется ли hot updates или нет.


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


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



Долгие транзакции https://gitlab.com/snippets/1890447


Blocked autovacuum https://gitlab.com/snippets/1889668


Blocking issue https://gitlab.com/snippets/1890428


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


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


Если большое IO, то понятно, что это нехорошо.


Долгие транзакции тоже. На OLTP долгие транзакции не стоит пускать. И вот здесь ссылка на сниппет, которая позволяет взять этот сниппет и уже сделать некоторое слежение за долгими транзакциями.


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


Если у нас много таблиц не вакуумятся, то нужно alert иметь. Тут возможна такая ситуация как раз. Мы косвенно можем повлиять на работу autovacuum. Это сниппет от Avito, который я немножко улучшил. И получился интересный инструмент, чтобы посмотреть, что у нас с autovacuum. Например, там ждут какие-то таблицы и не дождутся своей очереди. Тоже надо засунуть в мониторинг и alert иметь.


И блоки issues. Лес деревьев блокировок. Я люблю у кого-то что-то взять и улучшить. Здесь от Data Egret взял классный рекурсивный CTE, который показывает лес деревьев блокировок. Это хорошая штука для диагностики. И на ее основе тоже можно соорудить мониторинг. Но это надо аккуратно делать. Нужно самому себе statement_timeout маленький сделать. И lock_timeout желательно.



Какие ошибки мы здесь увидели?


  • DBA не проверил.


  • Проверяли не там.


  • Не провели checkpoint tuning.


  • Не разбили на части.


  • Мониторинг был слабый.



Иногда все это встречается в сумме.


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


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


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


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


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



То, что мы делаем, это open source. Это выложено на GitLab. И мы делаем так, чтобы люди могли проверять даже без DBA. Мы делаем database lab, т. е. мы так называем базовый компонент, на котором сейчас работает Joe. И вы можете взять копию production. Сейчас есть реализация Joe для slack, вы можете там сказать: explain такой-то запрос и тут же получить результат для вашей копии базы. Вы можете там даже DELETE сделать, и никто этого не заметит.



Допустим, у вас 10 терабайта, мы делаем database lab тоже 10 терабайт. И с одновременными 10-ти терабайтными базами могут работать одновременно 10 разработчиков. Каждый может делать то, что хочет. Может удалять, дропать и т. д. Вот такая фантастика. Об этом мы завтра будем говорить.



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


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


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


Вопросы


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


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


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


Если вы pg_repack на GitHub посмотрите, то там, когда была задача конвертировать айдишник с int 4 на int 8, то была идея сам pg_repack использовать. Это тоже возможно, но это немножко хакерский метод, но он тоже для этого подойдет. Вы можете вмешаться в триггер, который использует pg_repack и там сказать: Нам эти данные не нужны, т. е. мы переливаем только то, что нам нужно. И потом он просто переключится и все.


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


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


Я просто немножко из мира MySQL, поэтому я пришел послушать. И мы пользуемся таким подходом.


Но он только, если у нас 90 %. Если у нас 5 %, то не очень хорошо его применять.


Спасибо за доклад! Если нет ресурсов сделать полную копию prod, есть ли какой-то алгоритм или формула для того, чтобы просчитать нагрузку или размер?


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


Спасибо за доклад! Вы вначале начали про то, что есть крутой Postgres, у которого вот такие-то ограничения, но он развивается. А это все костыль по большему счету. Не идет ли это все в противоречие с развитием самого Postgres, в котором какой-нибудь DELETE deferent появится или что-нибудь еще, что должно поддерживать на низком уровне то, что мы пытаемся здесь обмазать какими-то своими странными средствами?


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


С индексами же сделали.


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


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


Я рассказал про принципы, которые можно использовать сейчас. Есть другой бот Nancy, с помощью этого можно сделать автоматизированный checkpoint tuning. Будет ли это когда-то в Postgres? Не знаю, это пока что даже не обсуждается. Мы пока далеки от этого. Но есть ученые, которые делают новые системы. И они нас пихают в автоматические индексы. Есть разработки. Например, auto tuning можете посмотреть. Он подбирает параметры автоматически. Но он вам checkpoint tuning пока не сделает. Т. е. он подберет для performance, shell buffer и т. д.


А для checkpoint tuning можно сделать такую штуку: если у вас тысяча кластеров и разные железки, разные виртуальные машины в cloud, вы можете с помощью нашего бота Nancy автоматизацию сделать. И будет подбираться max_wal_size по вашим целевым установкам автоматически. Но пока этого в ядре даже близко нет, к сожалению.


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


Autovacuum это, может быть, не самая большая проблема здесь. А то, что долгая транзакция может залочить другие транзакции, эта возможность более опасная. Она может встретиться, а может и не встретиться. Если она встретилась, то очень плохо может быть. И с autovacuum это тоже проблема. Тут две проблемы с долгими транзакциями в OLTP: локи и autovacuum. И если у вас hot standby feedback включен на реплике, то вам еще блокировка autovacuum прилетит на мастер, она прилетит с реплики. Но, по крайней мере, там локов не будет. А здесь будут локи. Мы говорим об изменениях данных, поэтому локи это важный здесь момент. И если это все долго-долго, то все больше транзакций лочится. Они могут лочить другие. И появляются деревья локов. Я приводил ссылку на сниппет. И эта проблема быстрее становится заметней, чем проблема с autovacuum, который может только накапливаться.


Спасибо за доклад! Вы начали свой доклад с того, что неправильно тестировали. Продолжили свою идею, что нужно взять оборудование одинаковое, с базой точно так же. Допустим, мы дали разработчику базу. И он выполнил запрос. И у него вроде бы все хорошо. Но он же не проверяет на live, а на live, например, у нас нагрузке на 60-70 %. И даже если мы используем этот тюнинг, получается не очень


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


Когда мы уже делаем garbage select и у нас есть, к примеру, deleted flag


Это то, что autovacuum делает автоматически в Postgres.


А, он это делает?


Autovacuum это и есть garbage collector.


Спасибо!


Спасибо за доклад! Есть ли вариант сразу проектировать базу данных с партицированием таким, чтобы весь мусор отпачковывался от основной таблицы куда-нибудь в сторону?


Конечно, есть.


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


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

Подробнее..

DBA-бот Joe. Анатолий Станслер (Postgres.ai)

22.10.2020 10:15:06 | Автор: admin


Как backend-разработчик понимает, что SQL-запрос будет хорошо работать на проде? В крупных или быстро растущих компаниях доступ к проду есть далеко не у всех. Да и с доступом далеко не все запросы можно безболезненно проверить, а создание копии БД часто занимает часы. Чтобы решить эти проблемы, мы создали искусственного DBA Joe. Он уже успешно внедрен в несколько компаний и помогает не одному десятку разработчиков.


Видео:




Всем привет! Меня зовут Анатолий Станслер. Я работаю в компании Postgres.ai. Мы занимаемся тем, что ускоряем процесс разработки, убирая задержки, связанные с работой Postgres, у разработчиков, DBA и QA.


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



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


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



Кто когда-нибудь прямо на prod делал индексы или какие-то изменения вносил? Довольно много. А у кого это приводило к тому, что данные терялись или простои были? Тогда вам знакома эта боль. Слава богу, бэкапы есть.



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



Это больно, это дорого. Наверное, так лучше не делать.


А как лучше сделать?



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


Это нам позволит какую-то часть ошибок убрать, т. е. не допустить на prod.


Какие есть проблемы?


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


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


Это лучше, чем предыдущий подход, но все равно есть большая вероятность, что какая-то ошибка уйдет на production.



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


У кого база данных больше, чем терабайт? Больше, чем у половины зала.


И понятно, что держать машины для каждого разработчика, когда такой большой production, это очень дорого, а к тому же еще и долго.


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



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


Но они этот подход используют, потому что это позволяет держать prod надежным.


Что мы здесь можем сделать? Давайте сделаем так, чтобы тестовые стенды были дешевыми и будем каждому разработчику давать свой собственный тестовый стенд.


И такое возможно.



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



Реальный пример:


  • БД 4,5 терабайта.


  • Мы можем получать независимые копии за 30 секунд.



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


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



В нашем случае это работает с помощью системы OpenZFS.



OpenZFS это copy-on-write файловая система, которая сама из коробки поддерживает снапшоты и клоны. Она надежная и масштабируемая. Ей очень просто управлять. Ее буквально в две команды можно развернуть.


Есть другие варианты:


  • LVM,


  • СХД (например, Pure Storage).



Database Lab, про который я рассказываю, он модульный. Можно реализовать при использовании таких вариантов. Но пока мы сосредоточились на OpenZFS, потому что конкретно с LVM были проблемы.



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


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


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


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



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


  • Первая это источник данных, откуда вы будете их брать. Можно настроить репликацию с production. Можно использовать уже бэкапы, которые у вас настроены, я надеюсь. WAL-E, WAL-G или Barman. И даже, если вы используете какое-то Cloud-решение, например, RDS или Cloud SQL, то вы можете использовать логические дампы. Но мы все-таки вам советуем использовать бэкапы, потому что при таком подходе у вас сохранится еще и физическая структура файлов, что позволит быть еще более ближе к тем метрикам, которые вы бы увидели на production, чтобы отлавливать те проблемы, которые есть.


  • Вторая это место, где вы хотите похостить Database Lab. Это может быть Cloud, это может быть On-premise. Здесь важно сказать, что ZFS поддерживает сжатие данных. И достаточно хорошо это делает.



Представьте, что у каждого такого клона в зависимости от тех операций, которые мы с базой делаем, будет нарастать какой-то dev. Для этого dev тоже нужно будет место. Но за счет того, что мы взяли базу в 4,5 терабайта, ZFS ее сожмет до 3,5 терабайт. В зависимости от настроек это можно варьировать. И у нас еще для dev останется место.


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


  • Это разработчики, DBA для проверки запросов, для оптимизации.


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


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




С таким подходом:


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


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


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



  • Будет меньше рефакторинга. Меньше багов попадет в prod. Мы меньше их отрефакторим потом.


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



  1. Это выгодно, потому что мы делим ресурсы тестовых стендов.

Уже хорошо, а что еще можно было бы ускорить?



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


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


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



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


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


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



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


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


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


Круто было бы иметь такое же железо как на production, но оно может отличаться.



Давайте вспомним как Postgres работает с памятью. У нас есть два кэша. Один от файловой системы и один собственный Postgres, т. е. Shared Buffer Cache.


Важно отметить, что Shared Buffer Cache аллоцируется при старте Postgres в зависимости оттого, какой размер вы зададите в конфигурации.


А второй кэш используется все доступное пространство.



И когда мы делаем несколько клонов на одной машине, получается, что мы постепенно память заполняем. И по-хорошему Shared Buffer Cache это 25 % от всего объема памяти, который на машине доступен.


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


Но с другой стороны, Buffer Cache используется для выполнения запросов, для индексов, т. е. план зависит оттого, какого размера у нас кэши. И если мы просто так возьмем этот параметр и уменьшим, то у нас планы могут сильно поменяться.


Например, если на prod у нас кэш большой, то у нас Postgres будет предпочитать использовать индекс. А если нет, то тогда будет SeqScan. И какой был бы смысл, если у нас эти планы не совпадали бы?


Но здесь мы приходим к такому решению, что на самом деле план в Postgres не зависит от конкретного заданного в Shared Buffer размера в плане, он зависит от effective_cache_size.



Effective_cache_size это предполагаемый объем кэша, который нам доступен, т. е. в сумме Buffer Cache и кэш файловой системы. Это задается конфигом. И эта память не аллоцируется.


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


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


  • Он зависит от той нагрузки, которая сейчас есть на prod.


  • Он зависит от характеристик самой машины.



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


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


Давайте разберем, как конкретно с Joe проходит оптимизация.



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



Мы пишем сообщение в канал, развернулся для нас клон. И мы увидим, что такой запрос отработает за 2,5 минуты. Это первое, что мы заметим.


B Joe покажет автоматические рекомендации, основанные на плане и метриках.


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



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



И, казалось бы, у нас здесь index scan и должно было быстро отработать, но, поскольку мы отфильтровали слишком много строк (нам пришлось их считать), то запрос медленно отработал.



И это произошло в плане из-за того, что частично не совпадают условия в запросе и условия в индексе.



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



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



И теперь у нас используется index only scan.


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



Это другой запрос, более насыщенный. И Flame Graphs мы строим по двум параметрам: это количество данных, которые конкретная нода в плане считала и тайминг, т. е. время выполнения ноды.


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



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


И разработчики, которые еще не углублялись в эту тему, тоже пользуются explain.depesz.com, потому что как раз им проще разобраться какие метрики важны, а какие нет.



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


Коллаборация



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



Нам кажется, что важно тестировать на полноразмерных данных. Для этого мы сделали инструмент Update Database Lab, который доступен в open source. Вы можете использовать бот Joe тоже. Вы можете брать его прямо сейчас и внедрять у себя. Все гайды там доступны.


Важно также отметить, что само по себе решение не является каким-то революционным, потому что есть Delphix, но это enterprise-решение. Оно полностью закрыто, стоит очень дорого. Мы именно специализируемся на Postgres. Это все продукты open source. Присоединяйтесь к нам!


На этом я заканчиваю. Спасибо!


Вопросы


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


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


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


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


Есть какой-то ttl у каждого клона. В принципе, у нас фиксированный ttl.


Какой, если не секрет?


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


Мне по поводу выбора технологий тоже интересно, потому что мы, например, параллельно используем несколько способов по тем или иным причинам. Почему именно ZFS? Почему вы не использовали LVM? Вы упомянули, что c LVM были проблемы. Какие были проблемы? На мой взгляд, наиболее оптимальным является вариант с СХД, с точки зрения производительности.


В чем главная проблема с ZFS? В том, что ты должен запускать на одном хосте, т. е. все instances будут жить в рамка одной операционки. А в случае с СХД, ты можешь подключать разное оборудование. И узким местом являются только те блоки, которые на СХД. И интересен вопрос именно выбора технологий. Почему не LVM?


Конкретно про LVM сможем обсудить на meetup. Про СХД это просто дорого. Систему ZFS мы можем внедрить где угодно. Вы можете ее у себя на машине развернуть. Вы можете просто скачать репозиторий и развернуть ее. ZFS ставится практически везде, если мы про Linux говорим. Т. е. мы получаем очень гибкое решение. И сам по себе ZFS из коробки очень многое дает. Можно загружать туда сколько угодно данных, подключать большое количество дисков, есть снапшоты. И, как я уже говорил, его просто администрировать. Т. е. он кажется очень приятным в использовании. Он проверен, ему много лет. У него есть очень большое community, которое растет. ZFS очень надежное решение.


Николай Самохвалов: Можно я еще прокомментирую? Меня Николай зовут, мы вместе с Анатолием работаем. Я согласен, что СХД это классно. И у некоторых наших клиентов есть Pure Storage и т. д.


Анатолий правильно отметил, что мы нацелены на модульность. И в будущем можно реализовать один интерфейс сделай снапшот, сделай клон, уничтожь клон. Это все легко. И СХД классно, если он есть.


Но ZFS доступен всем. Уже хватит DelPhix, у них 300 клиентов. Из них в fortune 100 50 клиентов, т. е. они нацелены на NASA и т. д. Пора получить эту технологию всем. И поэтому у нас open source Core. У нас есть часть интерфейсная, которая не open source. Это платформа, которую мы покажем. Но мы хотим, чтобы это было доступно каждому. Мы хотим сделать революцию, чтобы все тестировщики перестали гадать на ноутбуках. Мы должны писать SELECT и сразу видеть, что он медленный. Хватить ждать, когда DBA об этом расскажет. Вот это главная цель. И я думаю, что мы к этому придем все. И эту штуку мы делаем, чтобы она была у всех. Поэтому ZFS, потому что он будет доступен везде. Спасибо community за решение проблем и за то, что там open source лицензия и т. д.*


Приветствую! Спасибо за доклад! Меня Максим зовут. Мы решали такие же проблемы. У себя порешали. Как вы разделяете ресурсы между этими клонами? Каждый клон в каждый момент времени может заниматься своим: один одно тестирует, другой другое, у кого-то индекс строится, у кого-то job работает тяжелая. И если по CPU можно еще разделить, то по IO, как вы делите? Это первый вопрос.


И второй вопрос про непохожесть стендов. Допустим, у меня здесь ZFS и все классно, а у клиента на prod не ZFS, а ext4, например. Как в этом случае?


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


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


У меня два вопроса. Это очень крутая штука. Были ли кейсы, когда данные на production критично важные, например, номера кредитных кард? Есть ли уже что-то готовое или это отдельная задача? И второй вопрос есть ли что-то такое для MySQL?


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


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


Но поскольку система расширяемая, ее можно будет также использовать для MySQL. И такие примеры есть. Похожая штука есть у Яндекса, но они ее не публикуют нигде. Они ее используют внутри Яндекс.Метрики. И там как раз про MySQL история. Но технологии те же самые, ZFS.


Спасибо за доклад! У меня тоже пара вопросов. Вы упомянули, что клонирование можно использовать для аналитики, например, чтобы строить там дополнительные индексы. Можете немножко подробней рассказать, как это работает?


И второй вопрос сразу задам на счет одинаковости стендов, одинаковости планов. План зависит в том числе от статистики, собранной Postgres. Как вы эту проблему решаете?


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


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


Индекс будет каждый раз создаваться?


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


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


Тут другая проблема. Если у вас cloud-решение используется, то там только логические дампы доступны, потому что Google, Amazon не дают взять физическую копию. Там такая проблема будет.


Спасибо про доклад. Здесь прозвучало два хороших вопроса про MySQL и про разделение ресурсов. Но, по сути, все сводится к тому, что это тема не конкретных СУБД, а в целом файловой системы. И, соответственно, вопросы разделения ресурсов тоже должны решаться оттуда, не в конце, что это Postgres, а в файловой системе, в сервере, в instance.


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


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


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


Из предыдущих слоев, которые были с предыдущих репликаций.


Предыдущие слои отвалятся, но они будут ссылаться на старый слой, а новые образы они будут с последнего слоя брать, который был получен в обновлении?


В общем, да.


Тогда как следствие у нас будет до фига слоев. И со временем их надо будет сжимать?


Да, все правильно. Есть какое-то окно. Мы сохраняем недельные снапшоты. Это зависит оттого, какой у вас есть ресурс. Если у вас есть возможность хранить много данных, можно за долгое время хранить снапшоты. Они сами не удалятся. Никакого data corruption не будет. Если снапшоты устарели, как нам кажется, т. е. это зависит от политики в компании, то мы можем их просто удалить и освободить место.


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


Все так. Например, мы можем написать: SELECT FROM WHERE email = тому то. Т. е. мы не увидим сами данные, но какие-то косвенные признаки мы можем посмотреть. Это нужно понимать. Но с другой стороны это все видно. У нас есть аудит логов, у нас есть контроль других коллег, которые тоже видят, чем занимаются разработчики. И если кто-то попробует так сделать, то к ним служба безопасности придет и поработает над этим вопросом.


Добрый день! Спасибо за доклад! У меня короткий вопрос. Если в компании Slack не используется, то какая-то к нему привязка сейчас есть или можно для разработчиков развернуть instances, чтобы подключить к базам приложение тестовое?


Сейчас есть привязка к Slack, т. е. нет никакого другого мессенджера, но очень хочется сделать поддержку других мессенджеров тоже. Что вы можете сделать? Вы можете развернуть у себя DB Lab без Joe, ходить с помощью REST API или с помощью нашей платформы и создавать клоны, и подключаться PSQLем. Но так можно сделать, если вы готовы дать своим разработчикам доступ к данным, т. к. тут уже никакого экрана не будет.


Мне эта прослойка не нужна, а нужна такая возможность.


Тогда да, это можно сделать.

Подробнее..

Avito iOS meetup 8 CI-лайфхаки, санитайзеры, IndexStore, перформанс

21.07.2020 12:10:27 | Автор: admin

Привет, Хабр! Всреду 29июля мы проводим восьмой посчёту митап дляiOS-разработчиков. Впрограмме два доклада отинженеров Авито онашем CI и интересных аспектах перформанса, рассказ протехники нормализации отразработчика изSigma Software и выступление англоязычного гостя изLyft проIndexStore.


Тезисы и ссылка нарегистрацию подкатом. Приходите смотреть трансляцию сами и приглашайте коллег.



Доклады


iOS CI as a Service in da House Владислав Алексеев, Авито


image


Однажды вы поймете, что собирать приложение локально вXcode уже не то. Вам захочется истории сборок, хранения релизных бинарей и проверок наPR. Вам потребуется CI и CD. Помере роста команды будет расти нагрузка и наваш CI. Вам потребуется масштабировать сборочную ферму, ускорять компиляцию, заставлять тесты работать быстрее и стабильнее. Сразвитием ваших мобильных приложений вам рано или поздно потребуется познать лучшие практики CI/CD вiOS.

В Авито есть всё, что нужно, дляразработки iOS-приложений: дебажные и релизные сборки, юнит- и UI-тесты, ферма. Мы постоянно добавляем всё больше и больше проверок тысячи юнит-тестов, сотни нативных UI-тестов, множество performance-тестов, различные дополнительные проверки. Но всё это добро занимает почти30минут на pull request-е уже два года подряд. Киллер фича унас нет очередей насборки, они стартуют вместе соткрытием PR! Вдокладе я расскажу, как мы достигли этого. Надеюсь, что вы научитесь нанаших фейлах и воодушевитесь нашими идеями!

Затрагиваемые темы: TeamCity, bash, Python, билды и тесты, CocoaPods, build tracing, Puppet, ферма, Xcode, импакт анализ.

О спикере: Владислав работает винфраструктурных проектах, связанных сосборками и тестированием. Начал свою карьеру вЯндексе, где работал надприложениями Яндекс.Карты и Яндекс.Браузер подiOS. Затем работал вФейсбуке надпроизводительностью основного приложения и системной сборки Buck. С2017 года работает вАвито, занимается инфраструктурой мобильных приложений.



Укрощение нормализованного состояния. Граф объекты и санитайзеры Алексей Демедецкий, Sigma Software


image


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

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

О спикере: я занимаюсь мобильной разработкой около10лет. Заэто время успел попробовать много разных подходов. Последние 5лет практикую и рассказываю прооднонаправленные подходы (redux, flux, mvi) вмобильной разработке. Всвободное время пишу свой карманный язык Arrow. Задать мне вопросы можно втвиттере.



What the IndexStore Has To Say Dave Lee, Lyft


image


Code is data, but what kind ofdata? For a given token, a language server can give a JSON object ofrelevant info. For a file, a parser can provide an AST. Both of these scopes are optimized fordifferent use cases. Other use cases can benefit from having data for all the code ina project. Swift and Clang both provide a project wide view ofthe code, we know it as Xcode's index. The IndexStore has a lot ofpotential formaking tools. This talk will explore and demonstrate some uses forthe IndexStore

Dave Lee is a software engineer inthe Bay Area working onsoftware for other software engineers. Dave is a dad to two daughters who show no interest incode, except that one time I used Python to do word scramble homework.



Абстрактные техники перформанса Тимур Юсипов, Авито


image


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

О спикере: руководитель команды Performance вАвито. Люблю iOS, футбол, походы, велосипед и ролики.



Пароли и явки


Онлайн-трансляция нанашем ютуб-канале стартует 29июля в18:00 поМоскве. Закончить планируем к20:30. На трансляции можно сразу нажать кнопку напомнить, чтобы ничего не пропустить.


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


До встречи вонлайне!

Подробнее..

System.Threading.Channels высокопроизводительный производитель-потребитель и асинхронность без алокаций и стэк дайва

07.07.2020 14:13:48 | Автор: admin
И снова здравствуй. Какое-то время назад я писал о другом малоизвестном инструменте для любителей высокой производительности System.IO.Pipelines. По своей сути, рассматриваемый System.Threading.Channels (в дальнейшем каналы) построен по похожим принципам, что и Пайплайны, решает ту же задачу Производитель-Потребитель. Однако имеет в разы более простое апи, которое изящно вольется в любого рода enterprise-код. При этом использует асинхронность без алокаций и без stack-dive даже в асинхронном случае! (Не всегда, но часто).



Оглавление




Введение


Задача Производитель/Потребитель встречается на пути программистов довольно часто и уже не первый десяток лет. Сам Эдсгер Дейкстра приложил руку к решению данной задачи ему принадлежит идея использования семафоров для синхронизации потоков при организации работы по принципу производитель/потребитель. И хотя ее решение в простейшем виде известно и довольно тривиально, в реальном мире данный шаблон (Производитель/Потребитель) может встречаться в гораздо более усложненном виде. Также современные стандарты программирования накладывают свои отпечатки, код пишется более упрощенно и разбивается для дальнейшего переиспользования. Все делается для понижения порога написания качественного кода и упрощения данного процесса. И рассматриваемое пространство имен System.Threading.Channels очередной шаг на пути к этой цели.

Какое-то время назад я рассматривал System.IO.Pipelines. Там требовалось более внимательная работа и глубокое осознание дела, в ход шли Span и Memory, а для эффективной работы требовалось не вызывать очевидных методов (чтобы избежать лишних выделений памяти) и постоянно думать в байтах. Из-за этого программный интерфейс Пайплайнов был нетривиален и не интуитивно понятен.

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

Каналы являются обобщенными, тип обобщения, как несложно догадаться тип, экземпляры которого будут производиться и потребляться. Интересно то, что реализация класса Channel, которая помещается в 1 строку (источник github):

namespace System.Threading.Channels{    public abstract class Channel<T> : Channel<T, T> { }}

Таким образом основной класс каналов параметризован 2 типами отдельно под канал производитель и канал потребитель. Но для реализованых каналов это не используется.
Для тех, кто знаком с Пайплайнами, общий подход для начала работы покажется знакомым. А именно. Мы создаем 1 центральный класс, из которого вытаскиваем отдельно производителей(CannelWriter) и потребителей(ChannelReader). Несмотря на названия, стоит помнить, что это именно производитель/потребитель, а не читатель/писатель из еще одной классической одноименной задачи на многопоточность. ChannelReader изменяет состояние общего channel (вытаскивает значение), которое более становится недоступно. А значит он скорее не читает, а потребляет. Но с реализацией мы ознакомимся позже.

Начало работы. Channel


Начало работы с каналами начинается с абстрактного класса Channel<T> и статического класса Channel, который создает наиболее подходящую реализацию. Далее из этого общего Channel можно получать ChannelWriter для записи в канал и ChannelReader для потребления из канала. Канал является хранилищем общей информации для ChannelWriter и ChannelReader, так, именно в нем хранятся все данные. А уже логика их записи или потребления рассредоточения в ChannelWriter и ChannelReader, Условно каналы можно разделить на 2 группы безграничные и ограниченные. Первые более простые по реализации, в них можно писать безгранично (пока память позволяет). Вторые же ограничены неким максимальным значением количества записей.

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

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

Статический класс Channel содержит 4 метода для создания вышеперечисленных каналов:

Channel<T> CreateUnbounded<T>();Channel<T> CreateUnbounded<T>(UnboundedChannelOptions options);Channel<T> CreateBounded<T>(int capacity);Channel<T> CreateBounded<T>(BoundedChannelOptions options);

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

UnboundedChannelOptions содержит 3 свойства, значение которых по умолчанию false:

  1. AllowSynchronousContinuations просто сумасводящая опция, которая позволяет выполнить продолжение асинхронной операции тому, кто ее разблокирует. А теперь по-простому. Допустим, мы писали в заполненный канал. Соответственно, операция прерывается, поток освобождается, а продолжение будет выполнено по завершению на новом потоке из пула. Но если включить эту опцию, продолжение выполнит тот, кто разблокирует операцию, то есть в нашем случае читатель. Это серьезно меняет внутреннее поведение и позволяет более экономно и производительно распоряжаться ресурсами, ведь зачем нам слать какие-то продолжения в какие-то потоки, если мы можем сами его выполнить;
  2. SingleReader указывает, что будет использоваться один потребитель. Опять же, это позволяет избавиться от некоторой лишней синхронизации;
  3. SingleWriter то же самое, только для писателя;

BoundedChannelOptions содержит те же 3 свойства и еще 2 сверху

  1. AllowSynchronousContinuations то же;
  2. SingleReader то же;
  3. SingleWriter то же;
  4. Capacity количество вмещаемых в канал записей. Данный параметр также является параметром конструктора;
  5. FullMode перечисление BoundedChannelFullMode, которое имеет 4 опции, определяет поведение при попытке записи в заполненный канал:
    • Wait ожидает освобождения места для завершения асинхронной операции
    • DropNewest записываемый элемент перезаписывает самый новый из существующих, завершается синхронно
    • DropOldest записываемый элемент перезаписывает самый старый из существующих завершается синхронно
    • DropWrite записываемый элемент не записывается, завершается синхронно


В зависимости от переданных параметров и вызванного метода будет создана одна из 3 реализаций: SingleConsumerUnboundedChannel, UnboundedChannel, BoundedChannel. Но это не столь важно, ведь пользоваться каналом мы будем через базовый класс Channel<TWrite, TRead>.

У него есть 2 свойства:

  • ChannelReader<TRead> Reader { get; protected set; }
  • ChannelWriter<TWrite> Writer { get; protected set; }

А также, 2 оператора неявного приведения типа к ChannelReader<TRead> и ChannelWriter<TWrite>.

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

Channel<int> channel = Channel.CreateUnbounded<int>();//Можно делать такChannelWriter<int> writer = channel.Writer;ChannelReader<int> reader = channel.Reader; //Или такChannelWriter<int> writer = channel;ChannelReader<int> reader = channel;

Данные хранятся в очереди. Для 3 типов используются 3 разные очереди ConcurrentQueue<T>, Deque<T> и SingleProducerSingleConsumerQueue<T>. На этом моменте мне показалось, что я устарел и пропустил кучу новых простейших коллекций. Но спешу огорчить они не для всех. Помечены internal, так что использовать их не получится. Но если вдруг они понадобятся на проде их можно найти здесь (SingleProducerConsumerQueue) и здесь (Deque). Реализация последней весьма проста. Советую ознакомится, ее очень быстро можно изучить.

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

ChannelReader потребитель


При запросе объекта потребителя возвращается одна из реализаций абстрактного класса ChannelReader<T>. Опять же в отличие от Пайплайнов АПИ несложное и методов немного. Достаточно просто знать список методов, чтобы понять, как использовать это на практике.

Методы:

  1. Виртуальное get-only свойство Task Completion { get; }
    Обьект типа Task, который завершается, когда закрывается канал;
  2. Виртуальное get-only свойство int Count { get; }
    Тут сделает заострить внимание, что возвращается текущее количество доступных для чтения объектов;
  3. Виртуальное get-only свойство bool CanCount { get; }
    Показывает, доступно ли свойство Count;
  4. Абстрактный метод bool TryRead(out T item)
    Пытается потребить объект из канала. Возвращает bool, показывающий, получилось ли у него прочитать. Результат помещается в out параметр (или null, если не получилось);
  5. Абстрактный ValueTask<bool> WaitToReadAsync(CancellationToken cancellationToken = default)
    Возвращается ValueTask со значением true, когда в канале появятся доступные для чтения данные, до тех пор задача не завершается. Возвращает ValueTask со значением false, когда канал закрывается(данных для чтения больше не будет);
  6. Виртуальный метод ValueTask<T> ReadAsync(CancellationToken cancellationToken = default)
    Потребляет значение из канала. Если значение есть, возвращается синхронно. В противном случае асинхронно ждет появления доступных для чтения данных и возвращает их.

    У данного метода в абстрактном классе есть реализация, которая основана на методах TryRead и WaitToReadAsync. Если опустить все инфраструктурные нюансы (исключения и cancelation tokens), то логика примерно такая попытаться прочитать объект с помощью TryRead. Если не удалось, то в цикле while(true) проверять результат метода WaitToReadAsync. Если true, то есть данные есть, вызвать TryRead. Если TryRead получается прочитать, то вернуть результат, в противном случае цикл по новой. Цикл нужен для неудачных попыток чтения в результате гонки потоков, сразу много потоков могут получить завершение WaitToReadAsync, но объект будет только один, соответственно только один поток сможет прочитать, а остальные уйдут на повторный круг.
    Однако данная реализация, как правило, переопределена на что-то более завязанное на внутреннем устройстве.


ChannelWriter производитель


Все аналогично потребителю, так что сразу смотрим методы:

  1. Виртуальный метод bool TryComplete(Exception? error = null)
    Пытается пометить канал как завершенный, т.е. показать, что в него больше не будет записано данных. В качестве необязательного параметра можно передать исключение, которое вызвало завершение канала. Возвращает true, если удалось завершить, в противном случае false (если канал уже был завершен или не поддерживает завршение);
  2. Абстрактный метод bool TryWrite(T item)
    Пытается записать в канал значение. Возвращает true, если удалось и false, если нет
  3. Абстрактный метод ValueTask<bool> WaitToWriteAsync(CancellationToken cancellationToken = default)
    Возвращает ValueTask со значением true, который завершится, когда в канале появится место для записи. Значение false будет в том случае, если записи в канал более не будут разрешены;
  4. Виртуальный метод ValueTask WriteAsync(T item, CancellationToken cancellationToken = default)
    Асинхронно пишет в канал. Например, в случае, если канал заполнен, операция будет реально асинхронной и завершится только после освобождения места под данную запись;
  5. Метод void Complete(Exception? error = null)
    Просто пытается пометить канал как завершенный с помощью TryComplete, а в случае неудачи кидает исключение.

Небольшой пример вышеописанного (для легкого начала ваших собственных экспериментов):

Channel<int> unboundedChannel = Channel.CreateUnbounded<int>();//Объекты ниже можно отправить в разные потоки, которые будут использовать их независимо в своих целяхChannelWriter<int> writer = unboundedChannel;ChannelReader<int> reader = unboundedChannel;//Первый поток может писать в каналint objectToWriteInChannel = 555;await writer.WriteAsync(objectToWriteInChannel);//И завершить его, при исключении или в случае, когда записал все, что хотелwriter.Complete();//Второй может читать данные из канала по мере их доступностиint valueFromChannel = await reader.ReadAsync();

А теперь перейдем к самой интересной части.

Асинхронность без алллокаций


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

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

Интерфейс IValueTaskSource


Начнем наш путь с истоков структуры ValueTask, которая была добавлена в .net core 2.0 и дополнена в 2.1. Внутри этой структуры скрывается хитрое поле object _obj. Несложно догадаться, опираясь на говорящее название, что в этом поле может скрываться одна из 3 вещей null, Task/Task<T> или IValueTaskSource. На самом деле, это вытекает из способов создания ValueTask.

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

Я уже упомянул интерфейс IValueTaskSource. Именно он помогает сэкономить память. Делается это с помощью переиспользования самого IValueTaskSource несколько раз для множества задач. Но именно из-за этого переиспользования и нет возможности баловаться с ValueTask.

Итак, IValueTaskSource. Данный интерфейс имеет 3 метода, реализовав которые вы будете успешно экономить память и время на выделении тех заветных байт.

  1. GetResult Вызывается единожды, когда в стейт машине, образованной на рантайме для асинхронных методов, понадобится результат. В ValueTask есть метод GetResult, который и вызывает одноименный метод интерфейса, который, как мы помним, может хранится в поле _obj.
  2. GetStatus Вызывается стейт машиной для определения состояния операции. Также через ValueTask.
  3. OnCompleted Опять же, вызывается стейт машиной для добавления продолжения к невыполненной на тот момент задаче.

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

CompareExchange


Довольно популярный метод популярного класса, позволяющий избежать накладных расходов на классические примитивы синхронизации. Думаю, большинство знакомы с ним, но все же стоит описать в 3 словах, ведь данная конструкция используется довольно часто в AsyncOperation.
В массовой литературе данную функцию называют compare and swap (CAS). В .net она доступна в классе Interlocked.

Сигнатура следующая:

public static T CompareExchange<T>(ref T location1, T value, T comparand) where T : class;

Имеются также перегрузи с int, long, float, double, IntPtr, object.

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

Допустим, вы хотите инкрементировать переменную, если ее значение меньше 10.

Далее идут 2 потока.

Поток 1 Поток 2
Проверяет значение переменной на некоторое условие (то есть меньше ли оно 10), которое срабатывает -
Между проверкой и изменением значения Присваивает переменной значение, не удовлетворяющее условию (например, 15)
Изменяет значение, хотя не должен, ведь условие уже не соблюдается -


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

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

Stack dive


Рассматривая весь этот код я не раз наткнулся на упоминания Stack Dive. Это очень крутая и интересная штука, которая на самом деле очень нежелательна. Суть в том, что при синхронном выполнении продолжений мы можем исчерпать ресурсы стека.

Допустим, мы имеем 10000 задач, в стиле

//code1await ...//code2

Допустим, первая задача завершает выполнение и этим освобождает продолжение второй, которое мы начинаем тут же выполнять синхронно в этом потоке, то есть забирая кусок стека стек фреймом данного продолжения. В свою очередь, данное продолжение разблокирует продолжение третей задачи, которое мы тоже начинаем сразу выполнять. И так далее. Если в продолжении больше нет await'ов или чего-то, что как-то сбросит стек, то мы просто будем потреблять стековое пространство до упора. Что может вызвать StackOverflow и крах приложения. В рассмотрении кода я упомяну, как с этим борется AsyncOperation.

AsyncOperation как реализация IValueTaskSource


Source code.

Внутри AsyncOperation есть поле _continuation типа Action<object>. Поле используется для, не поверите, продолжений. Но, как это часто бывает в слишком современном коде, у полей появляются дополнительные обязанности (как сборщик мусора и последний бит в ссылке на таблицу методов). Поле _continuation из той же серии. Есть 2 специальных значения, которые могут хранится в этом поле, кроме самого продолжения и null. s_availableSentinel и s_completedSentinel. Данные поля показывают, что операция доступна и завершена соответственно. Доступна она бывает как раз для переиспользования для совершенно асинхронной операции.

Также AsyncOperation реализует IThreadPoolWorkItem с единственным методом void Execute() => SetCompletionAndInvokeContinuation(). Метод SetCompletionAndInvokeContinuation как раз и занимается выполнением продолжения. И данный метод вызывается либо напрямую в коде AsyncOperation, либо через упомянутый Execute. Ведь типы реализующие IThreadPoolWorkItem можно забрасывать в тред пул как-то вот так ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false).

Метод Execute будет выполнен тред пулом.

Само выполнение продолжения довольно тривиально.

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

Первый метод интерфейса IValueTaskSource GetResult (github).

Все просто, он:

  1. Инкрементирует _currentId.
    _currentId то, что идентифицирует конкретную операцию. После инкремента она уже не будет ассоциирована с этой операцией. Поэтому не следует получать результат дважды и тп;
  2. помещает в _continuation делегат-марионетку s_availableSentinel. Как было упомянуто, это показывает, что этот экземпляр AsyncOperation можно испоьзовать повторно и не выделять лишней памяти. Делается это не всегда, а лишь если это было разрешено в конструкторе (pooled = true);
  3. Возвращает поле _result.
    Поле _result просто устанавливается в методе TrySetResult который описан ниже.

Метод TrySetResult (github).

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

Метод SignalCompletion (github).

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

В самом начале, если _comtinuation == null, мы записываем марионетку s_completedSentinel.

Далее метод можно разделить на 4 блока. Сразу скажу для простоты понимания схемы, 4 блок просто синхронное выполнение продолжения. То есть тривиальное выполнение продолжения через метод, как я описано в абзаце про IThreadPoolWorkItem.

  1. Если _schedulingContext == null, т.е. нет захваченного контекста (это первый if).
    Далее необходимо проверить _runContinuationsAsynchronously == true, то есть явно указано, что продолжения нужно выполнять как все привыкли асинхронно (вложенный if).
    При соблюдении данный условий в бой идет схема с IThreadPoolWorkItem описанная выше. То есть AsyncOperation добавляется в очередь на выполнение потоком тред пула. И выходим из метода.
    Следует обратить внимание, что если первый if прошел (что будет очень часто, особенно в коре), а второй нет, то мы не попадем в 2 или 3 блок, а спустимся сразу на синхронное выполнение продолжения т.е. 4 блок;
  2. Если _schedulingContext is SynchronizationContext, то есть захвачен контекст синхронизации (это первый if).
    По аналогии мы проверяем _runContinuationsAsynchronously = true. Но этого не достаточно. Необходимо еще проверить, контекст потока, на котором мы сейчас находимся. Если он отличен от захваченного, то мы тоже не можем просто выполнить продолжение. Поэтому если одно из этих 2 условий выполнено, мы отправляем продолжение в контекст знакомым способом:
    sc.Post(s => ((AsyncOperation<TResult>)s).SetCompletionAndInvokeContinuation(), this);
    

    И выходим из метода. опять же, если первая проверка прошла, а остальные нет (то есть мы сейчас находимся на том же контексте, что и был захвачен), мы попадем сразу на 4 блок синхронное выполнение продолжения;
  3. Выполняется, если мы не зашли в первые 2 блока. Но стоит расшифровать это условие.
    Хитрость в том, что _schedulingContext может быть на самом деле захваченным TaskScheduler, а не непосредственно контекстом. В этом случае мы поступаем также, как и в блоке 2, т.е. проверяем флаг _runContinuationsAsynchronously = true и TaskScheduler текущего потока. Если планировщик не совпадает или флаг не тот, то мы сетапим продолжение через Task.Factory.StartNew и передаем туда этот планировщик. И выходим из метода.
  4. Как и сказал в начале просто выполняем продолжение на текущем потоке. Раз мы до сюда дошли, то все условия для этого соблюдены.

Второй метод интерфейса IValueTaskSource GetStatus (github)
Просто как питерская пышка.

Если _continuation != _completedSentinel, то возвращаем ValueTaskSourceStatus.Pending
Если error == null, то возвращаем ValueTaskSourceStatus.Succeeded
Если _error.SourceException is OperationCanceledException, то возвращаем ValueTaskSourceStatus.Canceled
Ну а коль уж до сюда дошли, то возвращаем ValueTaskSourceStatus.Faulted

Третий и последний, но самый сложный метод интерфейса IValueTaskSource OnCompleted (github)

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

При необходимости захватывает ExecutionContext и SynchronizationContext.

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

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

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

Таким образом проверяем возвращенное значение, равно ли оно s_completedSentinel именно оно было бы записано в случае завершения.

  • Если это не s_completedSentinel, то нас использовали не по плану попытались добавить более одного продолжения. То есть то, которое уже записано, и то, которое пишем мы. А это исключительная ситуация;
  • Если это s_completedSentinel, то это один из допустимых исходов, операция уже завершена и продолжение должны вызвать мы, здесь и сейчас. И оно будет выполнено асинхронно в любом случае, даже если _runContinuationsAsynchronously = false.
    Сделано это так, потому что если мы дошли до этого места, значит мы внутри метода OnCompleted, внутри awaiter'а. А синхронное выполнение продолжений именно здесь грозит упомянутым стек дайвом. Сейчас вспомним, для чего нам нужна эта AsyncOperation System.Threading.Channels. А там ситуация может быть очень легко достигнута, если о ней не задуматься. Допустим, мы читатель в ограниченном канале. Мы читаем элемент и разблокируем писателя, выполняем его продолжение синхронно, что разблокирует очередного читателя(если читатель очень быстр или их несколько) и так далее. Тут стоит осознать тонкий момент, что именно внутри awaiter'а возможна эта ситуация, в других случаях продолжение выполнится и завершится, что освободит занятый стек фрейм. А постоянный зацеп новых продолжений вглубь стека порождается постоянным выполнением продолжения внутри awaiter'а.
    В целях избежания данной ситуации, несмотря ни на что необходимо запустить продолжение асинхронно. Выполняется по тем же схемам, что и первые 3 блока в методе SignalCompleteion просто в пуле, на контексте или через фабрику и планировщик

А вот и пример синхронных продолжений:

class Program    {        static async Task Main(string[] args)        {            Channel<int> unboundedChannel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions            {                AllowSynchronousContinuations = true            });            ChannelWriter<int> writer = unboundedChannel;            ChannelReader<int> reader = unboundedChannel;            Console.WriteLine($"Main, before await. Thread id: {Thread.CurrentThread.ManagedThreadId}");            var writerTask = Task.Run(async () =>            {                Thread.Sleep(500);                int objectToWriteInChannel = 555;                Console.WriteLine($"Created thread for writing with delay, before await write. Thread id: {Thread.CurrentThread.ManagedThreadId}");                await writer.WriteAsync(objectToWriteInChannel);                Console.WriteLine($"Created thread for writing with delay, after await write. Thread id: {Thread.CurrentThread.ManagedThreadId}");            });            //Blocked here because there are no items in channel            int valueFromChannel = await reader.ReadAsync();            Console.WriteLine($"Main, after await (will be processed by created thread for writing). Thread id: {Thread.CurrentThread.ManagedThreadId}");            await writerTask;            Console.Read();        }    }

Output:

Main, before await. Thread id: 1
Created thread for writing with delay, before await write. Thread id: 4
Main, after await (will be processed by created thread for writing). Thread id: 4
Created thread for writing with delay, after await write. Thread id: 4
Подробнее..

Вам показалось! Все о Perceived Performance

21.01.2021 14:15:10 | Автор: admin

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

В большинстве случаев с ростом реальной производительности улучшается и Perceived Performance. А когда реальная производительность не может быть с легкостью увеличена, существует возможность поднять видимую. В своем докладе на Frontend Live 2020 бывший разработчик Avito Frontend Architecture Алексей Охрименко рассказал о приемах, которые улучшают ощущение скорости там, где ускорить уже нельзя.

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

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

  • беспокойство;

  • неуверенность;

  • дискомфорт;

  • раздражение;

  • скуку.

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

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

Не игнорируйте этот момент: вы обязательно должны его учитывать.

Для этого есть множество разнообразных техник, в том числе:

  • lighthouse;

  • devTools profiler.

Но даже если все сделать идеально, этого может оказаться недостаточно.

Есть интересная история про аэропорт Huston. Она отлично вписывается и в современные реалии разработчиков.

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

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

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

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

Perceived Performance

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

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

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

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

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

Не блокируйте пользователя

Первый совет: ни в коем случае не стоит блокировать пользователя. Вы можете сейчас сказать: Я и не блокирую!.

Попробуйте узнать себя в этом примере:

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

Все равно не узнаете? А так?

Спиннеры это не выход! Хоть они и могут стать ленивым способом разобраться с неудобной ситуацией.

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

Можно нажать на кнопку УДАЛИТЬ и показать статус этой кнопки (item удаляется только для одного элемента), не демонстрируя спиннер. В дальнейшем можно отправить запрос на сервер, и когда он придет, показать, что элемент удалился, либо при ошибке передать данные о ней. Вы можете возразить: Но я могу делать только 1 запрос за раз! это ограничение бэкенда. С помощью RxJs и оператора concat можно буквально одной строчкой кода создать минимальную очередь:

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

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

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

В Angular есть ngx-spinner, который поддерживает такой функционал.

Это, как минимум, уменьшит время ожидания, и пользователь сможет в этот момент сделать хоть что-то.

Обманывайте

Обман зачастую базируется на иллюзиях скорости.

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

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

Где можно применить такую методологию?

  • Progress Bar;

Есть исследование, показывающее, что если добавить полоски, которые идут в обратном направлении внутри Progress Bar, то визуально он выглядит как более быстрый. В такой настройке можно получить до 12% ускорения, просто применив дополнительный скин. И пользователи воспримут работу, прошедшую под этим Progress Bar, на 12% быстрее. Вот пример того, как можно реализовать такой слайдер на CSS.

  • Скелетон;

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

Скелетон это некое схематическое отображение сайта до момента его загрузки:

В этом примере мы видим, где будут расположены чаты.

Существует исследование, которое показывает, что люди воспринимают скелетоны быстрее от 10 до 20%.

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

Существует огромное количество нужных компонентов для Angular, React, View. К примеру, для Angular есть skeleton-loader, в котором можно прописать внешний вид и сконфигурировать его. После чего мы получим наш скелетон:

  • Экспоненциальная выдержка.

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

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

Это одна из best practice в энтерпрайз-приложениях, потому что бэкенд может не работать по разным причинам. Например, происходит деплой или хитрая маршрутизация. В любом случае очень хорошее решение: попробовать повторить. Ведь никакое API не дает 100% гарантии, 99,9% uptime.

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

Но даже с этим сценарием мы сами себе можем сделать DDOS (Distributed Denial of Service). На это попадались многие компании. Например, Apple с запуском своего сервиса MobileMe.

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

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

Best practice: применять exponential backoff. В rxjs есть хороший дополнительный npm пакет backoff-rxjs, который за вас имплементирует данный паттерн.

Имплементация очень простая, 10 строчек кода. Здесь вы можете обойтись одной. Указываете интервал, через который начнутся повторы, количество попыток, и сбрасывать ли увеличивающийся таймер. То есть вы увеличиваете по экспоненте каждую следующую попытку: первую делаете через 1 с, следующую через 2 с, потом через 4 с и т.д.

Играя с этими настройками, вы можете настраивать их под ваше API.

Следующий совет очень простой добавить Math.random() для initialInterval:

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

Предугадывайте!

Как уменьшить ожидание, когда невозможно ускорить процесс?

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

  • Предзагрузка картинок;

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

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

  • Предзагрузка 18+

Наверняка вы сталкивались в HTML со стеком link, который позволяет переподключить stylesheets:

Немного поменяв атрибуты, мы можем применить его для предзагрузки:

Можно указать атрибут rel ="preload", сослаться в ссылке на наш элемент (href="styles/main.css"), и в атрибуте as описать тип предзагружаемого контента.

  • prefetch.

Еще один вариант это prefetch:

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

ОК, это уже лучше, но есть одна маленькая проблема.

Если взять какой-нибудь среднестатистический сайт и начать префетчить все JavaScript модули, то средний рост по больнице составляет 3 МБ. Если мы будем префетчить только то, что видим на странице, получаем примерно половину 1,2 МБ. Ситуация получше, но все равно не очень.

Что же делать?

Давайте добавим Machine Learning

Сделать это можно с помощью библиотеки Guess.js. Она создана разработчиками Google и интегрирована с Google Analytics.

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

При этом эта библиотека будет точна на 90%. Загрузив всего 7%, она угадает желания 90% пользователей. В результате вы выигрываете и от prefetching/preloading, и от того, что не загружаете все подряд. Guess.js это идеальный баланс.

Сейчас Guess.js работает из коробки с Angular, Nuxt js, Next.js и Gatsby. Подключение очень легкое.

Поговорим о Click-ах

Что можно сделать, чтобы уменьшить ожидание?

Как предугадать, на что кликнет пользователь? Есть очевидный ответ.

У нас есть событие, которое называется mousedown. Оно срабатывает в среднем на 100-200 мс раньше, чем событие Click.

Применяется очень просто:

Просто поменяв click на mousedown, мы можем выиграть 100-200 мс.

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

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

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

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

Библиотека называется futurelink. Ее можно использовать абсолютно с любым фреймворком:

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

Что пользователь хочет получить при переходе на страницу? В большинстве сценариев: HTML, CSS и немного картинок.

Все это можно реализовать за счет серверного рендеринга SSR.

В Angular для этого достаточно добавить одну команду:

ng add @nguniversal/express-engine

В большинстве случаев это работает замечательно, и у вас появится Server-Side Rendering.

Но что, если вы не на Angular? Или у вас большой проект, и вы понимаете, что внедрение серверного рендеринга потребует довольно большого количества усилий?

Здесь можно воспользоваться статическим prerender: отрендерить страницы заранее, превратить их в HTML. Для этого есть классный плагин для webpack, который называется PrerenderSPAPlugin:

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

Но вы можете сделать все еще проще: зайти в свое SPA приложение и написать:

document.documentElement.outerHTML,

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

Заключение

Несмотря на то что Perceived Performance очень субъективная метрика, ее можно и нужно измерять. Об этом говорилось в докладе Виктора Русаковича на FrontendConf 2019.

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

Сделать это можно при помощи сервиса под названием Яндекс.Толока.

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

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

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

Конференция, посвященная всем аспектам разработки клиентской части веб проектов, FrontendConf 2021 пройдет 29 и 30 апреля. Билеты можно приобрести здесь. Вы можете успеть купить их до повышения цены!

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

Черновик

Подробнее..

PHP-SPX простой профайлер трейсер для PHP

16.05.2021 14:08:59 | Автор: admin
Работая с различными PHP проектами часто приходится дебажить приложение чтобынайти и исправить ошибку. Во многих случаях вполне хватает xDebug, однако он не подходит для всех задач. Иногда нужно понять почему та или иная страница долго загружается, что съедает так много памяти или просто как работает большой и запутанный код.
php-spx logo webmageic

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

Визуализация в QCacheGrind справлялась лучше, но не всегда давала понять что не так.

xHprof был достаточно быстрым но имел тот же недостаток с удобством визуализацией данных.

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

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

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

Поэтому как то в поисках хорошего визуализатора трейсов в виде таймлайнов для PHP я наткнулся на репозиторий PHP-SPX (Simple Profiling eXtension)

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

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

По кратким просмотре документации, я решил попробовать установить профайлер.
Доки нам говорят что достаточно скачать исходники и собрать все с помощью phpize и make, а далее подключить в php.ini

git clone https://github.com/NoiseByNorthwest/php-spx.gitcd php-spxphpize./configuremakesudo make install

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

RUN cd /usr/lib && git clone https://github.com/NoiseByNorthwest/php-spx.gitRUN cd /usr/lib/php-spx && phpize && ./configure && make && make install

Добавляем расширение в конфиг php.ini, тут можем подстроить некоторые параметры:

extension = /usr/lib/php-spx/modules/spx.sospx.http_enabled = 1spx.http_key = "dev" #ключ доступа к панели и триггерspx.http_ip_whitelist = "*" #список IP через запятую с которых разрешено профилированиеspx.data_dir = /var/www/html/spx_dumps #место сохранение дампов, по умолчанию - /tmp/spx


Поскольку профилирование разрешено со всех IP, то просто переходим по такому URL в php проекте.
http://localhost/?SPX_KEY=dev&SPX_UI_URI=/ 


Если все работает правильно то расширение перехватит запрос к приложению и подменит ответ своей панелью управления.
image

Далее отмечаем чекбокс Enabled и открываем любой URL приложения в новой вкладке (например главную страницу).
Профилировать можно и с помощью curl, достаточно добавить соответствующие куки.
curl --cookie "SPX_ENABLED=1; SPX_KEY=dev" http://localhost/


Подобным способом можно дебажить и консольные команды:
SPX_ENABLED=1 SPX_REPORT=full ./bin/console


После чего обновляем страницу панели управления и смотрим что есть в списке дампов. Следует отметить, что SPX имеет довольно низкий оверхед, что почти не влияет на время выполнения приложения.
php-spx dumps list

Открываем дамп, ждем конца загрузки:


Видим визуализацию всех вызовов PHP функций и методов в трех вариантах.
Timeline, Flat profile и Flame Graph.
php-spx webmageic

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


Либо изменения цветов по паттерну регулярного выражения во вкладке category


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


Часто можно увидеть такой паттерн-пилу, обычно это означает загрузку данных либо другие операции в цикле. В данном примере это проблема N+1 загрузки URL категорий.


Либо другой пример, когда после выполнения SQL запроса приложение создает PHP объекты.


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


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

Еще несколько примечаний от главного разработчика PHP-SPX:
  • автор не считает что расширение готово к использованию в продакшене, поэтому лучше устанавливать его только локально и на тестовые сервера
  • единственное ограничение доступа к профилированию через веб-интерфейс это белый список IP и параметр SPX_KEY

Итоги:


Из своего опыта использования, могу сказать, что на данный момент PHP-SPX это самый удобный Open-Source профайлер-трейсер для PHP с хорошей визуализацией. Много раз выполнял роль чит-кода позволяющего быстрее разобраться с новым кодом, найти и исправить различные проблемы производительности (особенно в Magento 2), а так же самому писать более надежный и эффективный код.

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

Однако в случае с таймлайном более подробным есть PHP-SPX т.к. он записывает все вызовы (по умолчанию семплинг не используется) в то время как Blackfire ограничивает нас до 1% времени выполнения:
The Timeline Threshold is not an absolute value. It is relative to the way your application performs. The threshold value is calculated by Blackfire as 1% of the duration of the profiled page/script
источник


Может быть кто-то из читателей поделиться в комментариях своим опытом использования PHP-SPX либо других инструментов при разработке PHP приложений?
Демо PHP-SPX можно посмотреть тут
Более подробную информацию о всех настройках и возможностях можно найти в репозитории.
Подробнее..

Из песочницы Хроники пэйджинга

04.09.2020 18:19:39 | Автор: admin

Вот и меня посетило желание что-нибудь написать для читателей {абра. Чем же ещё заняться в отпуске?


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


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


Лично я поднимаю БД для тестов в dockerе, где уже подготовлены тестовые данные. Берём здесь. Запускаем из каталога docker командой docker-compose up -d. СУБД PostgreSQL стартует вместе с web-версией утилиты pgadmin. Её надо немного настроить. В браузере заходим по адресу http://localhost:5050/. Логин/пароль: cop/postgres. В дереве навигации уже должен быть сервер cop. Нужно будет только ввести пароль postgres.


Теперь мы готовы погонять тесты.


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


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



Итак, оффсетный пейджинг


Его недостатки, которые почти сразу приходят на ум, это:



  1. Появление дубликатов записей или пропуск записей при пролистывании страниц, то есть неконсистентность результата.


    Посмотрим, как это происходит. Имеем следующую табличку.

    id name age
    11
    12
    13
    14
    15
    Jane
    Peter
    Margarett
    Manuel
    Richard
    25
    36
    41
    21
    49
    16
    17
    18
    19
    20
    Elliot
    Helen
    Katrine
    Elvis
    Joan
    61
    53
    19
    33
    69

    Записи отсортированы по идентификатору.


    Один пользователь просматривает первую страницу записи с идентификаторами 11-15. Для него значения OFFSET и LIMIT равны 0 и 5 соответственно. В это время второй пользователь удаляет, например, запись о Peter с идентификатором 12. Далее, первый пользователь решает перейти ко второй странице со значениями OFFSET и LIMIT, равными 5. Для него отобразятся 4 записи с 17 по 20, а не с 16 по 20, как можно было бы ожидать. Таким образом, запись об Эллиоте с идентификатором 16 пользователь пропустит.


    Аналогично выглядит пример с дублированием: если второй пользователь не удалит запись, а добавит новую, например, с идентификатором 10, то первый пользователь, перейдя на вторую страницу, увидит записи с 15 по 19. Таким образом запись о Ричарде с идентификатором 15 станет дублем.


    Теперь представьте, что может произойти, если активных пользователей станет пара тысяч


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

  2. Линейный рост сложности при выполнении запроса с увеличением номера страницы или неэффективность смещения.


    Проиллюстрируем это на нашей тестовой базе данных.


    План выполнения запроса для получения второй страницы (10 записей на страницу):


    SELECT * FROM test_paging OFFSET 10 LIMIT 10;
    
    Limit(cost=0.17..<b>0.34</b> rows=10 width=25) (actual time=0.011..0.013 rows=10 loops=1)->  Seq Scan on test_paging  (cost=0.00..17190.00 rows=1000000 width=25)(actual time=0.009..0.010 rows=<b>20</b> loops=1)Planning Time: 0.043 msExecution Time: <b>0.023 ms</b>
    

    Сотой страницы:


    SELECT * FROM test_paging OFFSET 990 LIMIT 10;
    
    Limit(cost=17.02..<b>17.19</b> rows=10 width=25) (actual time=0.136..0.138 rows=10 loops=1)->  Seq Scan on test_paging  (cost=0.00..17190.00 rows=1000000 width=25)(actual time=0.009..0.094 rows=<b>1000 </b>loops=1)Planning Time: 0.044 msExecution Time: <b>0.149 ms</b>
    

    Видим рост времени выполнения с 0,023 мс до 0,149 мс в 6,5 раз.


С помощью простого теста можно получить следующую картинку:



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


Сортировка


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


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


Есть ещё один любопытный момент. При сортировке по неуникальному полю снова может возникнуть эффект проскальзывания записей между страниц, так как реляционные СУБД и SQL не гарантируют в этом случае воспроизводимый порядок возвращаемых записей. И в целом реляционная модель определяет таблицу как неупорядоченный набор записей. Для решения этой проблемы в оффсетном пейджинге обычно используют дополнительную сортировку по уникальному полю. Иногда такое поле предоставляется самой СУБД, например, rowid в случае Oracle. Иногда такого псевдо-поля нет. Например, в PostgreSQL ctid непостоянно и может изменять своё значение. То есть в некоторых случаях уникальное поле нужно создавать, индексировать и хранить. Казалось бы, плёвая задача: первичный ключ с типом, например, bigint, значение которого заполнено с помощью sequence. Но в распределённых системах это может создать некоторые проблемы, так как ключи генерируются независимо в нескольких точках системы. Обычно в этом случае используется UUID, занимающий в два раза больше места относительно bigint, практически гарантирующий уникальность значения для всей системы в целом и не требующий соперничества за общий ресурс. Этот тип не задумывался для генерации упорядоченных значений, что влечёт за собой проблему фрагментации данных. Возможно, стоит обратить внимание на младшего брата UUID ULID, который даёт нам упорядоченные значения, и использовать его.


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



SELECT * FROM test_paging ORDER BY first_name, id OFFSET 0 LIMIT 10;

Limit(cost=0.42..<b>1.02</b> rows=10 width=25) (actual time=0.013..0.021 rows=10 loops=1)->  Index Scan using ix_test_paging_first_name_id on test_paging(cost=0.42..59804.38 rows=1000000 width=25)(actual time=0.013..0.019 rows=10 loops=1)Planning Time: 0.054 msExecution Time: <b>0.030 ms</b>

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


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



SELECT * FROM test_paging ORDER BY last_name, id OFFSET 0 LIMIT 10;

Limit  (cost=21360.71..21361.88 rows=10 width=25) (actual time=140.228..140.232 rows=10 loops=1)->  Gather Merge  (cost=21360.71..118589.80 rows=833334 width=25)(actual time=140.227..142.041 rows=10 loops=1)Workers Planned: 2Workers Launched: 2->  Sort  (cost=20360.69..21402.36 rows=416667 width=25)(actual time=136.528..136.529 rows=10 loops=3)Sort Key: last_name, idSort Method: <b>top-N heapsort</b>  Memory: 26kBWorker 0:  Sort Method: top-N heapsort  Memory: 26kBWorker 1:  Sort Method: top-N heapsort  Memory: 26kB->  Parallel Seq Scan on test_paging  (cost=0.00..11356.67 rows=416667 width=25)(actual time=0.009..49.571 rows=333333 loops=3)Planning Time: 0.051 msExecution Time: <b>142.063 ms</b>

СУБД приходится сортировать все элементы, чтобы выбрать нужные 10.


Вот как это выглядит при листании страниц:



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


В варианте без использования индекса любопытен порог, возникший для при смещениях в 20154-20155 записей. Причина в том, что в этот момент верхние сортируемые значения перестают умещаться в памяти (настройка PostgreSQL work_mem, равная по умолчанию 4Мб) и СУБД переключается с алгоритма быстрой сортировки в памяти на сортировку с сохранением промежуточных значений на диск.



SELECT * FROM test_paging ORDER BY last_name, id OFFSET 20155 LIMIT 10;

Limit(cost=46582.39..46583.56 rows=10 width=25) (actual time=849.134..849.137 rows=10 loops=1)->  Gather Merge  (cost=44230.81..141459.90 rows=833334 width=25)(actual time=842.371..857.326 rows=20165 loops=1)Workers Planned: 2Workers Launched: 2->  Sort  (cost=43230.79..44272.46 rows=416667 width=25)(actual time=786.262..788.072 rows=7207 loops=3)Sort Key: last_name, idSort Method: <b>external merge</b>  Disk: 11184kBWorker 0:  Sort Method: external merge  Disk: 11600kBWorker 1:  Sort Method: external merge  Disk: 14488kB->  Parallel Seq Scan on test_paging  (cost=0.00..11356.67 rows=416667 width=25)(actual time=0.009..38.524 rows=333333 loops=3)Planning Time: 0.055 msExecution Time: 860.577 ms

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

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

Курсорный пейджинг


Оффсетный пейджинг достаточно популяризирован и обычно по умолчанию есть во всяких ORM и пр. Его можно рассматривать как доступ к элементам массива по их номеру. При этом алгоритмическая сложность составляет O(offset + limit). Мы это видели выше на графике.


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


Типичный запрос выглядит так:


SELECT * FROM test_paging WHERE id >= ? ORDER BY id LIMIT ?;

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


План запроса для первой и любой другой страницы:


Limit(cost=0.42..<b>0.85</b> rows=12 width=25) (actual time=0.019..0.023 rows=12 loops=1)->  Index Scan using ix_test_paging_id on test_paging(cost=0.42..17789.08 rows=498552 width=25)(actual time=0.018..0.021 rows=12 loops=1)Index Cond: (id >= 500000)Planning Time: 0.063 msExecution Time: <b>0.034 ms</b>


Тот случай, когда добавить нечего: O(limit). Подкупают и абсолютные значения, и тенденция. Нет проблем с проскальзыванием страниц. Таким должен быть пейджинг!


Но чем мы за это платим?


  1. Мы просто так не можем перейти на произвольную страницу. Можно, конечно, выполнить финт с использованием OFFSET, но мне лично кажется это в принципе избыточным, так как сложно представить, для чего вдруг пользователю может понадобиться перейти сходу на страницу с номером, например, 1234. Но это ограничение нужно учитывать.
  2. По умолчанию мы можем перейти только на следующую и предыдущую страницы, так как всегда имеем их опорные идентификаторы. Немного усложнив логику приложения, можно сохранять шлейф идентификаторов большего количества страниц, на которых пользователь уже побывал, и организовать возможность перехода на счётное количество соседних страниц. По мне так это вполне допустимое решение.
  3. Вообще, реализация курсорного пейджинга несколько более сложная задача по сравнению с оффсетным пейджингом.
  4. Мы должны всегда сортировать записи относительно уникальных и последовательных значений. Соответственно, для этого нужно такие значения иметь и построить для них индексы. По умолчанию эту роль всегда может выполнить первичный ключ он уникален, индексирован и при любом раскладе можно обеспечить монотонное возрастание его значений (вспоминаем ULID). Экзотические случаи таблиц без первичных ключей мы не рассматриваем.

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


Итак, как будет выглядеть запрос в курсорном пейджинге с учётом сортировки по одному полю? Очевидно, что в сортировке должно участвовать ещё и уникальное поле. Основная сортировка выполняется по неуникальному полю, а внутри диапазонов записей с повторяющимися значениями выполняется сортировка по уникальному полю. Таким образом обеспечивается воспроизводимая упорядоченная последовательность записей, которую можно разбить на страницы и листать. Условие отбора должно работать сразу для пары полей. В некоторых источниках всерьёз рассматривается вариант применения конкатенации двух полей в выражении условия (CONCAT(first_name, id) >= CONCAT(Peter, 17)). Можно, конечно, создать функциональный индекс для этих целей, но нужно понимать, что результат конкатенации неоднозначен. Например, `a`+`bcd` даст такой же результат, как и `abc`+`d`. То есть в общем случае подход не применим.


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


SELECT * FROM test_paging WHERE (first_name, id) >= ('Abagail', 911281) ORDER BY first_name, id LIMIT 12;

Limit(cost=0.42..<b>1.17</b> rows=12 width=25) (actual time=0.014..0.023 rows=12 loops=1)->  Index Scan using ix_test_paging_first_name_id on test_paging(cost=0.42..62184.70 rows=996361 width=25)(actual time=0.013..0.021 rows=12 loops=1)Index Cond: (ROW((first_name)::text, id) >= ROW('Abagail'::text, 911281))Planning Time: 0.095 msExecution Time: <b>0.044 ms</b>

Как видно из плана выполнения запроса и собранной статистики, выросла оценка и время выполнения. Заметим, что успешно используется композитный индекс, и время выполнения всё ещё несопоставимо мало по сравнению с общим временем выполнения, включающим передачу результатов по сети (условно 20 мс и более).



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


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


SELECT * FROM test_paging WHERE (first_name, last_name, id) >= (?, ?, ?) ORDER BY first_name, last_name, id LIMIT ?;

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


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


SELECT * FROM test_pagingWHERE (first_name > ?)   OR (first_name = ? AND last_name < ?)   OR (first_name = ? AND last_name = ? AND id >= ?)ORDER BY first_name, last_name DESC, id;

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



Sort(cost=420.14..420.41 rows=110 width=25) (actual time=0.272..0.278 rows=100 loops=1)Sort Key: first_name, last_name DESC, idSort Method: quicksort  Memory: 32kB->  Bitmap Heap Scan on test_paging (cost=14.48..416.41 rows=110 width=25)(actual time=0.060..0.154 rows=100 loops=1)Recheck Cond: (((first_name)::text > 'Zula'::text) OR (((first_name)::text = 'Zula'::text) AND ((last_name)::text < 'Harber'::text)) OR (((first_name)::text = 'Zula'::text) AND ((last_name)::text = 'Harber'::text) AND (id >= 228020)))Heap Blocks: exact=100->  BitmapOr  (cost=14.48..14.48 rows=110 width=0)(actual time=0.048..0.048 rows=0 loops=1)->  Bitmap Index Scan on ix_test_paging_fn_ln_id  (cost=0.00..4.43 rows=1 width=0)(actual time=0.005..0.005 rows=0 loops=1)Index Cond: ((first_name)::text > 'Zula'::text)->  Bitmap Index Scan on ix_test_paging_fn_ln_id  (cost=0.00..5.52 rows=110 width=0)(actual time=0.037..0.037 rows=98 loops=1)Index Cond: (((first_name)::text = 'Zula'::text) AND ((last_name)::text < 'Harber'::text))->  Bitmap Index Scan on ix_test_paging_fn_ln_id  (cost=0.00..4.44 rows=1 width=0)(actual time=0.005..0.005 rows=2 loops=1)Index Cond: (((first_name)::text = 'Zula'::text) AND ((last_name)::text = 'Harber'::text) AND (id >= 228020))Planning Time: 0.297 msExecution Time: <b>0.328 ms</b>

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


Sort  (cost=155731.58..158230.75 rows=999670 width=25) (actual time=3357.573..4437.847 rows=999902 loops=1)Sort Key: first_name DESC, last_name, idSort Method: external merge  Disk: 37240kB->  Seq Scan on test_paging  (cost=0.00..32190.00 rows=999670 width=25)(actual time=4.289..288.607 rows=999902 loops=1)Filter: (((first_name)::text < 'Zula'::text) OR (((first_name)::text = 'Zula'::text) AND ((last_name)::text > 'Harber'::text)) OR (((first_name)::text = 'Zula'::text) AND ((last_name)::text = 'Harber'::text) AND (id >= 228020)))Rows Removed by Filter: 98Planning Time: 0.177 msJIT:Functions: 2Options: Inlining false, Optimization false, Expressions true, Deforming trueTiming: Generation 0.926 ms, Inlining 0.000 ms, Optimization 0.640 ms, Emission 3.253 ms, Total 4.819 msExecution Time: <b>4500.573 ms</b>

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


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


  1. Однозначно не стоит давать пользователю выполнять сложные сортировки. Наоборот, имеет смысл ограничить состав полей, доступных для для сортировки.
  2. Имеет смысл устроить интерфейс пользователя таким образом, чтобы пользователю приходилось применять достаточно селективный предикат, естественным образом ограничивающий выборку. Например, это может быть выбор некоторого периода времени, с которым пользователь собирается работать.
  3. Имеет смысл убрать возможность перехода пользователя к произвольной странице. Можно оставить возможность перехода только на соседние страницы.
  4. Возможность подсчёта общего количества страниц имеет смысл ограничить: убрать совсем или предусмотреть только при явном указании пользователя выполнить этот подсчёт, так как такой подсчёт будет означать как минимум вычитывание индекса по первичному ключу целиком или частичное вычитывание индекса при фильтрации с селективным предикатом.
  5. Запросы к БД имеет смысл выполнять только с ограничением по времени (например, setQueryTimeout(int) в случае JDBC). По истечении времени выводить пользователю соответствующую ошибку.

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


Спецификация GraphQL Connection


Не могу не упомянуть спецификацию от facebook как одно из красивых (на мой личный вкус и цвет, конечно же) решений для курсорного пейджинга. Чтобы не тратить время на чтение описания, посмотрим сразу на пример. Заходим на https://developer.github.com/v4/explorer/, вводим текст запроса:


{  repository(name: "engine", owner: "docker") {    pullRequests(      first: 3,                                                          # 1      after: "Y3Vyc29yOnYyOpK5MjAyMC0wMS0yMlQyMDowNjozNCswMzowMM4V0DI3", # 2      orderBy: {field: CREATED_AT, direction: DESC}                      # 3    ) {      edges {        node {                                                           # 4          url          title        }        cursor                                                           # 5      }      pageInfo {                                                         # 6        hasPreviousPage        hasNextPage      }    }  }}

Жмём Ctrl-Enter. Вуаля! Что-то произошло


  1. Здесь мы указали количество записей, которые нам нужно запросить.
  2. Это опорный идентификатор записи, которую мы уже просмотрели на предыдущей странице. Если убрать этот параметр, то мы получим первую страницу.
  3. Требуемая сортировка. Кстати, тут можно указать только одно поле и только из очень ограниченного списка.
  4. Перечень запрашиваемых полей.
  5. По сути это идентификатор для каждой записи. Это значение для одной из записей мы указали в п. 2.
  6. Сведения о текущей странице. Нас интересует два значения: существует ли предыдущая и следующая страница.

В ответе сервера мы получили несколько pull-requestов популярного репозитория на githubе docker/engine:



{  "data": {    "repository": {      "pullRequests": {        "edges": [          {            "node": {              "url": "https://github.com/docker/engine/pull/453",              "title": "[19.03] vendor: update buildkit to 926935b5"            },            "cursor": "Y3Vyc29yOnYyOpK5MjAyMC0wMS0yMlQwMjo1MjowNSswMzowMM4VynV6"          },          {            "node": {              "url": "https://github.com/docker/engine/pull/452",              "title": "[19.03] Bump Golang 1.12.15"            },            "cursor": "Y3Vyc29yOnYyOpK5MjAyMC0wMS0xN1QxNzoxNzoxNSswMzowMM4VtKzr"          },          {            "node": {              "url": "https://github.com/docker/engine/pull/451",              "title": "[19.03 backport] assorted swagger / API docs fixes"            },            "cursor": "Y3Vyc29yOnYyOpK5MjAyMC0wMS0xN1QxMzoyNTozOCswMzowMM4Vs0nl"          }        ],        "pageInfo": {          "hasPreviousPage": true,          "hasNextPage": true        }      }    }  }}

Почти каждая коллекция запрашивается таким образом. Лично я в своё время взял подход на вооружение.


Ещё пэйджинг!


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


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


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


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


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


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

Подробнее..
Категории: Postgresql , Sql , Performance , Paging

Troubleshooting в Oracle

08.05.2021 18:17:24 | Автор: admin

Этот пост навеян статьями Часть 1. Логирование событий в Oracle PL/SQL и Часть 2. Идентификация событий происходящих в Oracle PL/SQL. В первую очередь, как специалисту по performance tuning и troubleshooting, хотелось бы прокомментировать некоторые нюансы.

1. Уровни детализации логгирования

В показанной системе не хватает гибкости настройки логгирования: как уровня детализации, так и места, куда их выводить. Можно было позаимствовать функциональность из широко известных систем логгирования а-ля java.util.logging (SLF4j, log4j и его вариации для других языков/систем, и тд), гибкую настройку для какого кода с какого уровня сообщений и куда их сохранять. Например, в том же log4plsql можно настроить вывод и в alert.log, и в trace file (с помощью `dbms_system.ksdwrt()`)

2. Пользовательские ошибки и сообщения

Из самой внутренней системы ошибок Оракл можно было позаимствовать использование UTL_LMS.FORMAT_MESSAGE. Кстати, сами ошибки(и events) можно посмотреть с помощью sys.standard.sqlerrm(N):

SQL> select sys.standard.sqlerrm(-1476) errmsg from dual;ERRMSG-------------------------------------ORA-01476: divisor is equal to zero

Примеры: err_by_code.sql, trace_events.sql

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

3. Что же делать в случае незалоггированных ошибок

Естественно, может случиться так, что существующая система логгирования не регистрирует какие-то неординарные ошибки, или даже ее совсем нет в базе. Тут могут быть полезны триггеры `after servererror on database/schema`. Простой минимальный пример: https://github.com/xtender/xt_scripts/blob/master/error_logging/on_database.sql

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

Например, недавно Nenad Noveljic расследовал проблему c "TNS-12599: TNS:cryptographic checksum mismatch" для чего ему нужно было получить callstack:

К счастью, помимо использованного у него в статье "ERRORSTACK", есть еще большой список "ACTIONS", включающий в себя и "CALLSTACK":

В этой команде 12599 - это номер события(event), callstack - инструктирует сделать дамп call стека, level 2 - указывает вывести еще и аргументы функций, lifetime 1 - только один раз.

Более подробно об этом у Tanel Poder с примерами:
- http://tech.e2sn.com/oracle/troubleshooting/oradebug-doc
- https://tanelpoder.com/2010/06/23/the-full-power-of-oracles-diagnostic-events-part-2-oradebug-doc-and-11g-improvements/

Мало того, как сам Танел и посоветовал, можно еще воспользоваться и "trace()" для форматированного вывода shortstack():

Так что этим же мы можем воспользоваться этим для вывода callstack:

alter system set events '12599 trace("stack is: %\n", shortstack())';

Или в более новом формате:

alter system set events 'kg_event[12599]{occurence: start_after 1, end_after 1} trace("stack is: %\n", shortstack())';

Как вы видите, здесь я еще добавил фильтр на количество срабатываний: после первого выполнения и только 1 раз.

Покажу на примере "ORA-01476: divisor is equal to zero":

alter system set events 'kg_event[1476]{occurence: start_after 1, end_after 1} trace("stack is: %\n", shortstack())';

Здесь kg_event - это Kernel Generic event, 1476 - ORA-1476. После этого запускаем в своей сессии:

SQL> alter session set events 'kg_event[1476]{occurence: start_after 1, end_after 1} trace("stack is: %\n", shortstack())';Session altered.SQL> select 1/0 x from dual;select 1/0 x from dual        *ERROR at line 1:ORA-01476: divisor is equal to zeroSQL> select 1/0 x from dual;select 1/0 x from dual        *ERROR at line 1:ORA-01476: divisor is equal to zeroSQL> select 1/0 x from dual;select 1/0 x from dual        *ERROR at line 1:ORA-01476: divisor is equal to zero

И в трейсфайле получаем:

# cat ORA19_ora_12981.trcTrace file /opt/oracle/diag/rdbms/ora19/ORA19/trace/ORA19_ora_12981.trcOracle Database 19c Enterprise Edition Release 19.0.0.0.0 - ProductionVersion 19.9.0.0.0Build label:    RDBMS_19.9.0.0.0DBRU_LINUX.X64_200930ORACLE_HOME:    /opt/oracle/product/19c/dbhome_1System name:    LinuxNode name:      b7c493c7f9b0Release:        3.10.0-1062.12.1.el7.x86_64Version:        #1 SMP Tue Feb 4 23:02:59 UTC 2020Machine:        x86_64Instance name: ORA19Redo thread mounted by this instance: 1Oracle process number: 66Unix process pid: 12981, image: oracle@b7c493c7f9b0*** 2021-05-08T14:12:27.000816+00:00 (PDB1(3))*** SESSION ID:(251.9249) 2021-05-08T14:12:27.000846+00:00*** CLIENT ID:() 2021-05-08T14:12:27.000851+00:00*** SERVICE NAME:(pdb1) 2021-05-08T14:12:27.000855+00:00*** MODULE NAME:(sqlplus.exe) 2021-05-08T14:12:27.000859+00:00*** ACTION NAME:() 2021-05-08T14:12:27.000862+00:00*** CLIENT DRIVER:(SQL*PLUS) 2021-05-08T14:12:27.000865+00:00*** CONTAINER ID:(3) 2021-05-08T14:12:27.000868+00:00stack is: dbgePostErrorKGE<-dbkePostKGE_kgsf<-kgeade<-kgeselv<-kgesecl0<-evadiv<-kpofcr<-qerfiFetch<-opifch2<-kpoal8<-opiodr<-ttcpip<-opitsk<-opiino<-opiodr<-opidrv<-sou2o<-opimai_real<-ssthrdmain<-main<-__libc_start_main

Или, например, недавно я посоветовал использовать alter system set events 'trace[sql_mon.*] [SQL: ...] disk=high,memory=high,get_time=highres'; для выяснения причин, почему конкретный запрос не мониторится/сохраняется real-time SQL монитором (RTSM - Real Time SQL Monitor).

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

Подробнее..
Категории: Sql , Oracle , Logging , Performance , Troubleshooting

Производительность распределенного хранилища препродакшн тесты

17.03.2021 12:07:26 | Автор: admin

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

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

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

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

Задачи


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

Подготовка


Клиенты


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

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

Лимиты


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

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

  1. Лимиты на количество операций (IOPS).
  2. Лимиты на количество переданных данных (MB/s).

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

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

Набор тестов


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

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

Мы отталкиваемся от предположения, что наиболее требовательный к задержкам клиент будет работать с диском синхронно. Значит, что на каждую операцию записи будет поступать операция синхронизации кеша(flush). И только дождавшись завершения такой операции, клиентское ПО будет считать запись успешной. Так работают, например, классические БД с WAL (Write-Ahead Log).

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

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

Запись и чтение тестируются разными размерами блока 4КиБ и 4МиБ. Маленький размер блока позволяет нагрузить кластер большим количеством операций, а большой объемом передаваемых данных. Так мы проверяем его работу в двух крайностях. Можно добавлять и промежуточные варианты размеров блока, так как различное ПО оперирует разным размером блока.

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

Статистика


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

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

  • гипервизоров,
  • серверов хранилища,
  • самого софта,
  • сетевого оборудования.

При тестах дисков VM в идеальном варианте нужно иметь статистику и с них.

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

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

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

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

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

Окружение


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

Примеры того, что нужно собрать:

  • версия ОС;
  • версия ядра;
  • версия используемых драйверов;
  • версия софта хранилища;
  • модель, количество и частота процессоров;
  • модель, количество, объем и частота оперативной памяти;
  • модель материнской платы;
  • модель сетевых карт и сетевая топология;
  • модель и количество дисков;
  • количество хостов в кластере;
  • количество гипервизоров;
  • количество и характеристики VM.

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

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


Пустой кластер


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

Заполнение


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

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

Первый тест


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

Оценка задержки


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

Выбор эталонной глубины очереди


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

  1. Cкорость обработки каждой операции.
  2. Количество параллельно выполняемых операций.

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

По результатам тестов нужно строить графики, показывающие распределение latency и IOPS. Задача найти такую глубину очереди, при которой распределение задержки еще не растет, при этом показатели IOPS или MB/s уже находятся максимально близко к заданному лимиту. Максимальная эффективная глубина очереди для тестов с разными параметрами может отличаться. Берем ее за эталон для тестов и используем на следующем этапе. Это и есть наши самые эффективные клиенты.

Тестирование кластера


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

По результатам нужно дать оценку для каждого теста:

  1. Максимальное количество максимально эффективных клиентов до деградации производительности кластера.
  2. Скорость деградации производительности при дальнейшем росте количества клиентов.

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

Сопровождение процесса


Подконтрольный запуск


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

  1. Тест упал сразу после запуска по причине неправильной конфигурации.
  2. Тест упал в середине работы из-за нехватки какого-либо ресурса.
  3. Тест частично упал или частично не запустился.
  4. Автоматизированные тесты наложились один на другой.

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

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

Узкие места


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

  1. Клиенты конкурировали за локальный ресурс. Например, CPU гипервизора.
  2. Локальные перегрузки на сети из-за неудачной балансировки.
  3. Что угодно, что могло быть замечено в процессе теста или сразу после первого прогона и влияет на результат.

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

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

Документирование


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

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

Интерпретация результатов


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

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

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

  1. Утилизация CPU, как на хостах хранилища, так и у клиентов. Нужно смотреть не только общую нагрузку, но и распределение нагрузки по ядрам, отсутствия длительного ожидания в очереди на CPU. Что происходит, когда система начинает деградировать? Не ошибка ли это в выборе железа или количества сервисов на хост? Или CPU тратит время в ожидании какого-то ресурса?
  2. Утилизация и перегрузка сети. Раз мы работаем с сетевым хранилищем, то стоит посмотреть, не оказалась ли сеть бутылочным горлышком. Потери пакетов, ошибки и утилизация основные параметры.
  3. Утилизация и latency дисков. Опять же это ведь хранилище. В идеальном мире самым медленным элементом системы должен быть конечный элемент. Т.е. не должно быть узких мест, система должна быть настроена так, чтобы реализовать потенциал дисков. В реальности, во-первых, нужно опять-таки учитывать профиль нагрузки, а во-вторых, реализовать потенциал дисков иногда невозможно.

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

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

В итоге


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

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

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

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

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

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

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



Подробнее..

Перевод Flame-графики огонь из всех движков

12.10.2020 18:16:09 | Автор: admin

Всем снова привет! Приглашаем на онлайн-встречу с Василием Кудрявцевым (директором департамента обеспечения качества в АО РТЛабс), которая пройдёт в рамках курса Нагрузочное тестирование. И публикуем перевод статьи Michael Hunger software developer and editor ofNeo4j Developer BlogandGRANDstack!

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

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

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

Flame-график бенчмарка заполнения непредаллоцированного ArrayListFlame-график бенчмарка заполнения непредаллоцированного ArrayList

Flame'ы снизу вверх отражают прогрессию от точки входа программы или потока (main или цикл событий) до листьев выполнения на концах flame'ов.

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

Для устранения недостатков стандартных профайлеров многие современные инструменты используют внутреннюю функцию JVM (AsyncGetCallTrace), которая позволяет собирать трассировки стека независимо от безопасных точек. Помимо этого, они объединяют измерение JVM-операций с нативным кодом и системных вызовов операционной системы, так что время, проведенное в сети, ввод/вывод или сборка мусора, также может стать частью flame-графа.

Такие инструменты, как Honest Profiler, perf-map-agent, async-profiler или даже IntelliJ IDEA, умеют захватывать информацию и с легкостью делать flame-графики.

В большинстве случаев вы просто скачиваете инструмент, предоставляете PID вашего Java-процесса и говорите инструменту работать в течение определенного периода времени и генерировать интерактивный SVG.

# download and unzip async profiler for your OS from:# https://github.com/jvm-profiling-tools/async-profiler./profiler.sh -d <duration> -f flamegraph.svg -s -o svg <pid> && \open flamegraph.svg  -a "Google Chrome"

SVG, создаваемый этими инструментами, не просто красочный, но и интерактивный. Вы можете увеличивать секции, искать посимвольно и т.д.

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


Интересно развиваться в данном направлении? Запишитесь на бесплатный Demo-урок Скрипты и сценарии нагрузочного тестирования- Performance center (PC) и Vugen!

Подробнее..

Почему мой NVMe медленнее SSD?

30.09.2020 14:10:05 | Автор: admin

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

Пару недель назад я столкнулся с вопросом, почему NVMe на одном сервере медленнее, чем SATA на другом. Посмотрел в характеристики серверов и понял, что это был вопрос с подвохом: NVMe был из пользовательского сегмента, а SSD из серверного.

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

Что такое fsync и где он используется


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

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

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

#include <fcntl.h>#include <unistd.h>#include <sys/stat.h>#include <sys/types.h>int main(void) {    /* Открываем файл answer.txt на запись, если его нет -- создаём */    int fd = open("answer.txt", O_WRONLY | O_CREAT);    /* Записываем первый набор данных */    write(fd, "Answer to the Ultimate Question of Life, The Universe, and Everything: ", 71);    /* Делаем вид, что проводим вычисления в течение 10 секунд */    sleep(10);    /* Записываем результат вычислений */    write(fd, "42\n", 3);     return 0;}

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

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

На что влияет частое использование fsync


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

Продемонстрируем влияние использования fsync на конкретном примере. В качестве испытуемых у нас следующие твердотельные накопители:

  • Intel DC SSD S4500 480 GB, подключен по SATA 3.2, 6 Гбит/с;
  • Samsung 970 EVO Plus 500GB, подключен по PCIe 3.0 x4, ~31 Гбит/с.

Тесты проводятся на Intel Xeon W-2255 под управлением ОС Ubuntu 20.04. Для тестирования дисков используется sysbench 1.0.18. На дисках создан один раздел, отформатированный как ext4. Подготовка к тесту заключается в создании файлов объемом в 100 ГБ:

sysbench --test=fileio --file-total-size=100G prepare

Запуск тестов:

# Без fsyncsysbench --num-threads=16 --test=fileio --file-test-mode=rndrw --file-fsync-freq=0 run# С fsync после каждой записиsysbench --num-threads=16 --test=fileio --file-test-mode=rndrw --file-fsync-freq=1 run

Результаты тестов представлены в таблице.
Тест Intel S4500 Samsung 970 EVO+
Чтение без fsync, МиБ/с 5734.89 9028.86
Запись без fsync, МиБ/с 3823.26 6019.24
Чтение с fsync, МиБ/с 37.76 3.27
Запись с fsync, МиБ/с 25.17 2.18
Нетрудно заметить, что NVMe из клиентского сегмента уверенно лидирует, когда операционная система сама решает, как работать с дисками, и проигрывает, когда используется fsync. Отсюда возникает два вопроса:

  1. Почему в тесте без fsync скорость чтения превышает физическую пропускную способность канала?
  2. Почему SSD из серверного сегмента лучше обрабатывает большое количество запросов fsync?

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

Если ставить под сомнение все результаты sysbench, то можно воспользоваться fio.

# Без fsyncfio --name=test1 --blocksize=16k --rw=randrw --iodepth=16 --runtime=60 --rwmixread=60 --fsync=0 --filename=/dev/sdb# С fsync после каждой записиfio --name=test1 --blocksize=16k --rw=randrw --iodepth=16 --runtime=60 --rwmixread=60 --fsync=1 --filename=/dev/sdb
Тест Intel S4500 Samsung 970 EVO+
Чтение без fsync, МиБ/с 45.5 178
Запись без fsync, МиБ/с 30.4 119
Чтение с fsync, МиБ/с 32.6 20.9
Запись с fsync, МиБ/с 21.7 13.9
Тенденция к просадке производительности у NVMe при использовании fsync хорошо заметна. Можно переходить к ответу на второй вопрос.

Оптимизация или блеф


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

  • программный;
  • аппаратный.

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

Так как SSD показывает лучшие результаты, то можно сделать два предположения:

  • диск спроектирован под нагрузку подобного плана;
  • диск блефует и игнорирует команду.

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

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

# Запускается на сервере./diskchecker.pl -l [port]# Запускается на клиенте./diskchecker.pl -s <server[:port]> create <file> <size_in_MB>

После запуска скрипта необходимо обесточить клиента и не возвращать питание в течение нескольких минут. Важно именно отключить тестируемого от электричества, а не просто выполнить жесткое выключение. По прошествии некоторого времени сервер можно подключать и загружать в ОС. После загрузки ОС необходимо снова запустить diskchecker.pl, но с аргументом verify.

./diskchecker.pl -s <server[:port]> verify <file>

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

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

Заключение


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

А как вы тестируете комплектующие cерверов при аренде у IaaS-провайдера?
Ждем вас в комментариях.

Подробнее..

Категории

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

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