Основы ZFS: система хранения и производительность

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

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

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

Zpool, vdev и device


Эта диаграмма полного пула включает в себя три вспомогательных vdev’а, по одному каждого класса, и четыре для RAIDz2


Обычно нет причин создавать пул из несоответствующих типов и размеров vdev — но если хотите, ничто не мешает вам это сделать

Чтобы действительно понять файловую систему ZFS, нужно внимательно посмотреть на её фактическую структуру. Во-первых, ZFS объединяет традиционные уровни управления томами и файловой системы. Во-вторых, она использует транзакционный механизм копирования при записи. Эти особенности означают, что система структурно очень отличается от обычных файловых систем и RAID-массивов. Первый набор основных строительных блоков для понимания: это пул хранения (zpool), виртуальное устройство (vdev) и реальное устройство (device).

zpool

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

Избыточность ZFS находится на уровне виртуальных устройств, а не на уровне пулов. На уровне пулов нет абсолютно никакой избыточности — если какой-либо накопитель vdev или специальный vdev теряется, то вместе с ним теряется и весь пул.

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

Есть распространённое заблуждение, что «полосы данных» (страйпы) ZFS записываются через весь пул. Это неверно. Zpool — вовсе не забавный RAID0, это скорее забавный JBOD со сложным изменчивым механизмом распределения.

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

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

vdev

Каждый пул хранения состоит из одного или нескольких виртуальных устройств (virtual device, vdev). В свою очередь, каждый vdev включает одно или несколько реальных устройств. Большинство виртуальных устройств используются для простого хранения данных, но существует несколько вспомогательных классов vdev, включая CACHE, LOG и SPECIAL. Каждый из этих типов vdev может иметь одну из пяти топологий: единое устройство (single-device), RAIDz1, RAIDz2, RAIDz3 или зеркало (mirror).

RAIDz1, RAIDz2 и RAIDz3 — это особые разновидности того, что олды назовут RAID двойной (диагональной) чётности. 1, 2 и 3 относятся к тому, сколько блоков чётности выделено для каждой полосы данных. Вместо отдельных дисков для обеспечения чётности виртуальные устройства RAIDz полуравномерно распределяют эту чётность по дискам. Массив RAIDz может потерять столько дисков, сколько у него блоков чётности; если он потеряет ещё один, то выйдет из строя и заберет с собой пул хранения.

В зеркальных виртуальных устройствах (mirror vdev) каждый блок хранится на каждом устройстве в vdev. Хотя наиболее распространённые двойные зеркала (two-wide), в зеркале может быть любое произвольное количество устройств — в больших установках для повышения производительности чтения и отказоустойчивости часто используются тройные. Зеркало vdev может пережить любой сбой, пока продолжает работать хотя бы одно устройство в vdev.

Одиночные vdev по своей сути опасны. Такое виртуальное устройство не переживёт ни одного сбоя — и если используется в качестве хранилища или специального vdev, то его сбой приведёт к уничтожению всего пула. Будьте здесь очень, очень осторожны.

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

device

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

Диски — магнитные или твёрдотельные — являются наиболее распространёнными блочными устройствами, которые используются в качестве строительных блоков vdev. Однако подойдёт любой девайс с дескриптором в /dev — так что в качестве отдельных устройств можно использовать целые аппаратные RAID-массивы.

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


Можете создать тестовый пул из разреженных файлов всего за несколько секунд — но не забудьте потом удалить весь пул и его компоненты

Допустим, вы хотите поставить сервер на восемь дисков и планируете использовать диски по 10 ТБ (~9300 ГиБ) — но вы не уверены, какая топология лучше всего соответствует вашим потребностям. В приведённом выше примере мы за считанные секунды строим тестовый пул из разреженных файлов — и теперь знаем, что RAIDz2 vdev из восьми дисков по 10 ТБ обеспечивает 50 ТиБ полезной ёмкости.

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

После подключения к пострадавшему vdev запасной девайс начинает получать копии или реконструкции данных, которые должны быть на отсутствующем устройстве. В традиционном RAID это называется восстановлением (rebuilding), а в ZFS это «восстановление избыточности» (resilvering).

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

Наборы данных, блоки и секторы

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

Набор данных (dataset)


Когда мы впервые создаём набор данных, он показывает всё доступное пространство пула. Затем мы устанавливаем квоту — и меняем точку монтирования. Магия!


Zvol — это по большей части просто набор данных, лишённый своего слоя файловой системы, который мы заменяем здесь совершенно нормальной файловой системой ext4

Набор данных ZFS примерно аналогичен стандартной смонтированной файловой системе. Как и обычная файловая система, на первый взгляд он кажется «просто ещё одной папкой». Но также, как и у обычных монтируемых файловых систем, у каждого набора данных ZFS собственный набор базовых свойств.

Прежде всего, у набора данных может быть назначенная квота. Если установить zfs set quota=100G poolname/datasetname, то вы не сможете записать в смонтированную папку /poolname/datasetname больше, чем 100 ГиБ.

Заметили наличие — и отсутствие — слэшей в начале каждой строки? У каждого набора данных своё место как в иерархии ZFS, так и в иерархии системного монтирования. В иерархии ZFS нет ведущего слэша — вы начинаете с имени пула, а затем пути от одного набора данных к следующему. Например, pool/parent/child для набора данных с именем child под родительским набором данных parent в пуле с креативным названием pool.

По умолчанию, точка монтирования набора данных будет эквивалентна его имени в иерархии ZFS, со слэшем в начале — пул с названием pool примонтируется как /pool, набор данных parent монтируется в /pool/parent, а дочерний набор данных child смонтируется в /pool/parent/child. Однако системную точку монтирования набора данных можно изменить.

Если мы укажем zfs set mountpoint=/lol pool/parent/child, то набор данных pool/parent/child смонтируется в систему как /lol.

В дополнение к наборам данных, мы должны упомянуть тома (zvols). Том примерно аналогичен набору данных, за исключением того, что в нём фактически нет файловой системы — это просто блочное устройство. Вы можете, например, создать zvol с именем mypool/myzvol, затем отформатировать его с файловой системой ext4, а затем смонтировать эту файловую систему — теперь у вас есть файловая система ext4, но с поддержкой всех функций безопасности ZFS! Это может показаться глупым на одном компьютере, но имеет гораздо больше смысла в качестве бэкенда при экспортировании устройства iSCSI.

Блоки


Файл представлен одним или несколькими блоками. Каждый блок хранится на одном виртуальном устройстве. Размер блока обычно равен параметру recordsize, но может быть уменьшен до 2^ashift, если содержит метаданные или небольшой файл.


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

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

Если не определено иное, то текущий размер записи по умолчанию равен 128 КиБ. Это своего рода непростой компромисс, в котором производительность будет не идеальной, но и не ужасной в большинстве случаев. Recordsize можно установить на любое значение от 4K до 1M (с дополнительными настройками recordsize можно установить ещё больше, но это редко бывает хорошей идеей).

Любой блок ссылается на данные только одного файла — вы не можете втиснуть два разных файла в один блок. Каждый файл состоит из одного или нескольких блоков, в зависимости от размера. Если размер файла меньше размера записи, он сохранится в блоке меньшего размера — например, блок с файлом 2 КиБ займёт только один сектор 4 КиБ на диске.

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

У томов zvol нет свойства recordsize — вместо этого у них есть эквивалентное свойство volblocksize.

Секторы

Последний, самый базовый строительный блок — сектор. Это наименьшая физическая единица, которая может быть записана или считана с базового устройства. В течение нескольких десятилетий в большинстве дисков использовались секторы по 512 байт. В последнее время большинство дисков настроено на сектора 4 КиБ, а в некоторых — особенно SSD — сектора 8 КиБ или даже больше.

В системе ZFS есть свойство, которое позволяет вручную установить размер сектора. Это свойство ashift. Несколько запутанно, что ashift является степенью двойки. Например, ashift=9 означает размер сектора 2^9, или 512 байт.

ZFS запрашивает у операционной системы подробную информацию о каждом блочном устройстве, когда оно добавляется в новый vdev, и теоретически автоматически устанавливает ashift должным образом на основе этой информации. К сожалению, многие диски лгут о своём размере сектора, чтобы сохранить совместимость с Windows XP (которая была неспособна понять диски с другими размерами секторов).

Это означает, что администратору ZFS настоятельно рекомендуется знать фактический размер сектора своих устройств и вручную устанавливать ashift. Если установлен слишком маленький ashift, то астрономически увеличивается количество операций чтения/записи. Так, запись 512-байтовых «секторов» в реальный сектор 4 КиБ означает необходимость записать первый «сектор», затем прочитать сектор 4 КиБ, изменить его со вторым 512-байтовым «сектором», записать его обратно в новый сектор 4 КиБ и так далее для каждой записи.

В реальном мире такой штраф бьёт по твёрдотельным накопителям Samsung EVO, для которых должен действовать ashift=13, но эти SSD врут о своём размере сектора, и поэтому по умолчанию устанавливается ashift=9. Если опытный системный администратор не изменит этот параметр, то этот SSD работает медленнее обычного магнитного HDD.

Для сравнения, за слишком большой размер ashift нет практически никакого штрафа. Реального снижения производительности нет, а увеличение неиспользуемого пространства бесконечно мало (или равно нулю при включённом сжатии). Поэтому мы настоятельно рекомендуем даже тем дискам, которые действительно используют 512-байтовые секторы, установить ashift=12 или даже ashift=13, чтобы уверенно смотреть в будущее.

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

Механизм копирования при записи


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


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


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


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

Механизм копирования при записи (Copy on Write, CoW) — фундаментальная основа того, что делает ZFS настолько потрясающей системой. Основная концепция проста — если вы попросите традиционную файловую систему изменить файл, она сделает именно то, что вы просили. Если вы попросите файловую систему с копированием при записи сделать то же самое, она скажет «хорошо» — но соврёт вам.

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

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

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

ZIL: журнал намерений ZFS


Система ZFS обрабатывает синхронные записи особым образом — она временно, но немедленно сохраняет их в ZIL, прежде чем позже записать их на постоянной основе вместе с асинхронными записями


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


SLOG, или вторичное LOG-устройство, — это просто специальный — и, желательно, очень быстрый — vdev, где ZIL может храниться отдельно от основного хранилища


После сбоя все грязные данные в ZIL воспроизводятся — в данном случае ZIL находится на SLOG, так что они воспроизводятся именно оттуда

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

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

ZFS обрабатывает синхронные записи иначе, чем обычные файловые системы — вместо того, чтобы немедленно заливать их в обычное хранилище, ZFS фиксирует их в специальной области хранения, которая называется журнал намерений ZFS — ZFS Intent Log, или ZIL. Хитрость в том, что эти записи также остаются в памяти, будучи агрегированными вместе с обычными асинхронными запросами на запись, чтобы позже быть сброшенными в хранилище как совершенно нормальные TXG (группы транзакций, Transaction Groups).

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

Если происходит сбой ZFS — сбой операционной системы или отключение питания — когда в ZIL есть данные, эти данные будут считаны во время следующего импорта пула (например, при перезапуске аварийной системы). Всё, что находится в ZIL, будет считано, объединено в группы TXG, зафиксировано в основном хранилище, а затем отсоединено от ZIL в процессе импорта.

Один из вспомогательных классов vdev называется LOG или SLOG, вторичное устройство LOG. У него одна задача — обеспечить пул отдельным и, желательно, гораздо более быстрым, с очень высокой устойчивостью к записи, устройством vdev для хранения ZIL, вместо хранения ZIL на главном хранилище vdev. Сам ZIL ведёт себя одинаково независимо от места хранения, но если у vdev с LOG очень высокая производительность записи, то синхронные записи будут происходить быстрее.

Добавление vdev с LOG в пул никак не может улучшить производительность асинхронной записи — даже если вы принудительно выполняете все записи в ZIL с помощью zfs set sync=always, они всё равно будут привязаны к основному хранилищу в TXG таким же образом и в том же темпе, что и без журнала. Единственным прямым улучшением производительности является задержка синхронной записи (поскольку бóльшая скорость журнала ускоряет выполнение операций sync).

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

Снапшоты

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

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

Репликация


Моя библиотека Steam в 2015 году занимала 158 ГиБ и включала 126 927 файлов. Это довольно близко к оптимальной ситуации для rsync — репликация ZFS по сети была «всего лишь» на 750% быстрее.


В той же сети репликация одного 40-гибибайтного файла образа виртуальной машины Windows 7 — совершенно другая история. Репликация ZFS происходит в 289 раз быстрее, чем rsync — или «всего» в 161 раз быстрее, если вы достаточно подкованы, чтобы вызвать rsync с ключом —inplace.


Когда образ виртуальной машины масштабируется, проблемы rsync масштабируются вместе с ним. Размер 1,9 ТиБ не такой большой для современного образа виртуальной машины — но он достаточно велик, чтобы репликация ZFS оказалась в 1148 раз быстрее, чем rsync, даже с аргументом rsync —inplace

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

Всё становится ещё интереснее на втором zfs send. Теперь у нас две системы, каждая из которых содержит poolname/datasetname@1, а вы снимаете новый снапшот poolname/datasetname@2. Поэтому в исходном пуле у вас datasetname@1 и datasetname@2, а в целевом пуле пока только первый снапшот datasetname@1.

Поскольку между источником и целью у нас есть общий снапшот datasetname@1, мы можем сделать инкрементальный zfs send поверх него. Когда мы говорим системе zfs send -i poolname/datasetname@1 poolname/datasetname@2, она сравнивает два дерева указателей. Любые указатели, которые существуют только в @2, очевидно, ссылаются на новые блоки — поэтому нам понадобится содержимое этих блоков.

В удалённой системе обработка инкрементального send такая же простая. Сначала мы записываем все новые записи, включённые в поток send, а затем добавляем указатели на эти блоки. Вуаля, у нас @2 в новой системе!

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

Встроенное сжатие

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

Если рассмотреть фрагмент данных в середине файла, который начинает свою жизнь как мегабайт нулей от 0x00000000 и так далее — его очень легко сжать до одного сектора на диске. Но что произойдёт, если мы заменим этот мегабайт нулей на мегабайт несжимаемых данных, таких как JPEG или псевдослучайный шум? Неожиданно этому мегабайту данных потребуется не один, а 256 секторов по 4 КиБ, а в этом месте на диске зарезервирован только один сектор.

У ZFS нет такой проблемы, так как изменённые записи всегда записываются в неиспользуемое пространство — исходный блок занимает только один сектор 4 КиБ, а новая запись займёт 256, но это не проблема — недавно изменённый фрагмент из «середины» файла был бы записан в неиспользуемое пространство независимо от того, изменился его размер или нет, поэтому для ZFS это вполне штатная ситуация.

Встроенное сжатие ZFS отключено по умолчанию, и система предлагает подключаемые алгоритмы — сейчас среди них LZ4, gzip (1-9), LZJB и ZLE.

  • LZ4 — это потоковый алгоритм, предлагающий чрезвычайно быстрое сжатие и декомпрессию и выигрыш в производительности для большинства случаев использования — даже на довольно медленных CPU.
  • GZIP — почтенный алгоритм, который знают и любят все пользователи Unix-систем. Он может быть реализован с уровнями сжатия 1-9, с увеличением степени сжатия и использования CPU по мере приближения к уровню 9. Алгоритм хорошо подходит для всех текстовых (или других чрезвычайно сжимаемых) вариантов использования, но в противном случае часто вызывает проблемы c CPU — используйте его с осторожностью, особенно на более высоких уровнях.
  • LZJB — оригинальный алгоритм в ZFS. Он устарел и больше не должен использоваться, LZ4 превосходит его по всем показателям.
  • ZLE — кодировка нулевого уровня, Zero Level Encoding. Она вообще не трогает нормальные данные, но сжимает большие последовательности нулей. Полезно для полностью несжимаемых наборов данных (например, JPEG, MP4 или других уже сжатых форматов), так как он игнорирует несжимаемые данные, но сжимает неиспользуемое пространство в итоговых записях.

Мы рекомендуем сжатие LZ4 практически для всех вариантов использования; штраф за производительность при встрече с несжимаемыми данными очень мал, а прирост производительности для типичных данных значителен. Копирование образа виртуальной машины для новой инсталляции операционной системы Windows (свежеустановленная ОС, никаких данных внутри ещё нет) с compression=lz4 прошло на 27% быстрее, чем с compression=none, в этом тесте 2015 года.

ARC — кэш адаптивной замены

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

Хотя собственный кэш не лишён своих проблем — ZFS не может реагировать на новые запросы о выделении памяти так же быстро, как ядро, поэтому новый вызов mallocate() может потерпеть неудачу, если ему потребуется оперативная память, занятая в настоящее время ARC. Но есть веские причины использовать собственный кэш, по крайней мере сейчас.

Все известные современные ОС, включая MacOS, Windows, Linux и BSD, для реализации кэша страниц используют алгоритм LRU (Least Recently Used). Это примитивный алгоритм, который поднимает кэшированный блок «вверх очереди» после каждого чтения и вытесняет блоки «вниз очереди» по мере необходимости, чтобы добавить новые промахи кэша (блоки, которые должны были быть прочитаны с диска, а не из кэша) вверх.

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

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

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

Заключение

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

В следующей части мы рассмотрим фактическую производительность пулов с зеркальными vdev и RAIDz, друг по сравнению с другом, а также по сравнению с традиционными RAID-топологиями ядра Linux, которые мы исследовали ранее.

Сначала мы хотели рассмотреть только основы — сами топологии ZFS — но после такого будем готовы говорить о более продвинутой настройке и тюнинге ZFS, включая использование вспомогательных типов vdev, таких как L2ARC, SLOG и Special Allocation.

ссылка на оригинал статьи https://habr.com/ru/post/504692/

Сказ о том, как я настраивал Azure AD B2C на React и React Native Часть 3 (Туториал)

image

Предисловие

Продолжение цикла по работе с Azure B2C. В данной статье я расскажу о том, как подключить аутентификацию на React JS.

Ссылки на связанные посты

ШАГ 1

Необходимо установить react-aad-msal (npm i react-aad-msal).

Добавить в каталог public пустой файл auth.html

ШАГ 2

Создать файл auth-provider.ts в папке ./src

  import { MsalAuthProvider, LoginType } from 'react-aad-msal';   import { Configuration } from 'msal/lib-commonjs/Configuration';      // Msal Configurations   export const config = (azurePolicy: string): Configuration => ({     auth: {       authority: `https://yourcompany.b2clogin.com/yourcompany.onmicrosoft.com/${azurePolicy}`, // azurePolicy = Название политики, на которую нужно перенести пользователя (об этом дальше)       validateAuthority: false,       clientId: '777aaa77a-7a77-7777-bb77-8888888aabc', // ClientID вашего приложения Azure AD B2C     },     cache: {       cacheLocation: 'localStorage',       storeAuthStateInCookie: false, // Лучше оставить false иначе кукисы имеют свойство накапливаться, что приводит к ошибке 431     },   });      // Authentication Parameters   const authenticationParameters = {     scopes: [       'openid', 'profile',     ],   };      // Options   export const options = {     loginType: LoginType.Redirect,     tokenRefreshUri: `${window.location.origin}/auth.html`,   };      export const authProvider = (customConfig: Configuration): MsalAuthProvider => new MsalAuthProvider(customConfig, authenticationParameters, options); 

Где брать названия политик вы можете увидеть на скриншоте
image

ШАГ 3

В файле index.tsx нужно обработать сценарии по которым пользователь будет заходить в ваше приложение.

  import * as React from 'react';   import * as ReactDOM from 'react-dom';   import { Provider } from 'react-redux';   import { AzureAD, AuthenticationState, IAzureADFunctionProps } from 'react-aad-msal';   import { authProvider, config } from './auth-provider';   import App from './App';    const store = configureStore();    const unauthenticatedFunction = () => (   // Переносим пользователя на страничку "Восстановление пароля"    // указав соответствующую политику   <AzureAD provider={authProvider(config('B2C_1A_PasswordReset'))}>     {       ({         login, logout, authenticationState, error, accountInfo,       }: IAzureADFunctionProps): React.ReactElement | void => {         switch (authenticationState) {           default:             login();             return <h1>Loading...</h1>;         }       }     }   </AzureAD> ); ReactDOM.render(   <Provider store={store}>     // Переносим пользователя на страницу авторизации     <AzureAD provider={authProvider(config('B2C_1A_signup_signin'))} reduxStore={store}>       {         ({           login, logout, authenticationState, error, accountInfo,         }: IAzureADFunctionProps): React.ReactElement | void => {           switch (authenticationState) {             case AuthenticationState.Authenticated:               console.log(accountInfo); // Данные пользователя + JWT Token               return <App />;             case AuthenticationState.Unauthenticated:               if (!accountInfo && !error) {                 login();               }               if (!accountInfo && error) {                 // Переносим пользователя на восстановление пароля если                  // он вернулся с ошибкой AADB2C90118                 if (error.errorMessage.includes('AADB2C90118')) {                   return unauthenticatedFunction();                 }                 // Действие, когда пользователь возвращается не авторизированным                 // (Например когда нажмет кнопку "Забыл пароль" а потом нажал кнопку "Отмена")                 if (error.errorMessage.includes('AADB2C90091')) {                   login();                 }               }               console.log('ERROR', error);               return <h1>Not authorized</h1>;             case AuthenticationState.InProgress:               return <h1>In progress</h1>;             default:               return <h1>Default</h1>;           }         }       }     </AzureAD>   </Provider>,   document.getElementById('root'), );  registerServiceWorker(); 

ШАГ 4

Переходим на Azure AD B2C во вкладку «Регистрация приложений» и выбираем приложение которое Вы хотите использовать, кроме:

IdentityExperienceFramework и ProxyIdentityExperienceFramework.

Если вы еще не создали приложение, то пройдите шаг «Базовые пользовательские потоки»

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

Заключение

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

Спасибо за внимание!

ссылка на оригинал статьи https://habr.com/ru/post/504690/

Люди не хотят знать английский

Попала в поле моего зрения интересная статья с парадоксальным тезисом о том, что люди, внезапно, НЕ ХОТЯТ больше зарабатывать.

Благодаря ей удалось сформулировать то, что уже давно маячило где-то в предсознании: см. заголовок.

Знакомый как-то рассказал мне, что уже 2 года ходит в школу английского языка. Он кое в чём мне помог, и я расплатился с ним местом в моей школе на курсе английского. С первых же занятий выяснилось, что он абсолютно ничего не понимает про английский на самом базовом уровне, и ему это ОЧЕНЬ наглядно продемонстрировали – полные абсолютные нулевики давали больше правильных ответов, чем он, уже буквально на 3-м занятии. Курс он в итоге не закончил, потому что не делал дз, был невнимателен на занятиях и перестал что-либо понимать уже к середине потока.

После того, как он рассказал мне, что ВЕРНУЛСЯ В ТУ ШКОЛУ, которая ему за 2 (!) года ничего не дала, я пару дней пребывал в шоке. А потом понял, что ему не нужен английский – ему нужно жить в зоне комфорта с ощущением, что он куда-то движется. Выражения-то он какие-то запоминает, ощущение прогресса есть.

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

1. Работаю с теми, кто работает, даю тем, кто берёт, никого не заставляю.

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

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

Дальнейший текст для наших выпускников, остальные не очень в теме.

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

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

За 2 месяца и 3 недели мы даём человеку с уровнем способностей НИЖЕ СРЕДНЕГО сложнейший материал, к осознанию которого я лично шёл 10 лет. Вот сейчас остановитесь и вдумайтесь. Я, c уровнем способностей к языкам выше среднего. Шёл. К осознанию всех этих вещей. Десять (10) лет.

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

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

На базовом ситуация принципиально иная. Мы всегда очень жёстко ограничены по времени (курс нельзя прерывать) студенческими графиками. Например, осенний поток начинается в сентябре, выпустить его нужно до 20 декабря, иначе КАТАСТРОФА: потом все заняты сессией и предновогодними хлопотами. Зимний поток запускается, как только студенты приезжают с каникул в феврале, раньше начать не можем в принципе. Выпустить его нужно до начала мая. Иначе потом уедут, как ни кричи, что последние занятия – самые важные на курсе.

И вот в этот промежуток нужно вместить весь английский язык – это сверхсложная боевая задача на грани возможностей и студентов, и преподавателей. Люди, естественно, поначалу отвлекаются – не слышат вопроса, думают о чём-то своём. ВСЯ ГРУППА тормозится из-за чьей-то персональной безответственности и пофигизма. Но я быстро задаю правила игры, и этот беспредел резко снижается.

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

Только не надо мне говорить, что я неадекватно оцениваю возможности студентов и слишком многого требую – многолетний опыт за спиной. Система разрабатывалась для, и обкатывалась на средних детях 10-11 лет. Вопросы, которые задаются на курсе, не превышают уровня интеллектуальной компетенции среднестатистического человека 10-11 лет – проверено, работает: habr.com/ru/post/497822

Я не ожидаю от людей, пришедших на курс, ни знания английских слов, ни способности обрабатывать информацию даже на уровне 11 лет – начинаем с азов. Исхожу из того, что передо мной 9-10-летние дети. Требование одно – следить за ходом занятия и не выключать мозг. Некоторые не дают мне даже этого. Церемониться некогда, включаем аварийные механизмы контроля ситуации.

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

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

Да и разные есть мнения: кто-то сам признаёт, что с ним только за счёт давления и сработало. А в отзывах часто удивляются моей стрессоустойчивости и терпению: «Я бы убил» (из частных разговоров со студентами).

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

P.S. Условно людей можно поделить на 2 категории:

  1. Тех, кто занят делом и реально что-то куда-то двигает. У таких нет времени продумывать, как их случайный шаг отзовётся в общественном поле.
  2. Функционеров, организующих бесчисленные и бессмысленные «мероприятия» – конкурсы, дни открытых дверей, встречи. Они постоянно оглядываются: а как это будет воспринято? Ах, не наступить бы кому-нибудь на ножку.

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

image

ссылка на оригинал статьи https://habr.com/ru/post/504702/

Автоматическое управление питанием бытового Wi-Fi

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

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

Тогда я озадачился поиском реле, которое могло бы управлять питанием и одновременно могло бы иметь длительную задержку по возврату в исходное состояние. В качестве основы я использовал реле времени РВО-1М. С продукцией этого производителя я познакомился благодаря публикациям «Электрошамана», и также использовал другие модификации для защиты ввода от отклонений нормали. Возможно некоторые из вас сочтут мой пост рекламным. Но я правда не получил пока от этого прибыли, кроме тех плюсов что обозначу ниже. В качестве реле движения подойдет любое устройство которое способно замыкать контакты нагрузки при срабатывании датчика, и питается от однофазной бытовой сети напрямую, без использования блоков питания. Я выбрал DR-06.

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

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

Черный цвет это фаза, синий это ноль, желтый земля. Фаза этот как раз тот провод, в котором протекает энергия. Сначало вибрации электронов передаются от ввода до автоматического выключателя, далее проходя через автоматический выключатель поступают в небольшую клеммную коробку и распределяются до реле движения и реле времени. Реле движения в этот момент начинает работать благодаря тому что подключено контактом N к вводу. Но наше реле движения не замкнет котакты N и L` пока рядом с ним не будет движения. На этом моменте мы примем факт, что движения в нашей системе пока нет и все спят, или никого нет в помещении, и вернемся к тому, что рассмотрим что же сейчас проихсходит с реле времени. Вибрации электронов дошли до контакта A1 и благодаря тому что наше реле времени подключено контактом N к вводу устройство готово к работе. Контакты 15 и 18 нормально разомкнуты, то есть пока реле времени не сработало 15 и 18 не соединены. Чтобы соединить контакты 15 и 18 необходимо соединить контакты A1 и Y1, то есть заставить вибрировать электроны на контакте Y1. Теперь давайте примем тот факт, что движение в квартире началось и датчик движения сработал. Контакты L и L` в датчике движения замкнулись. А этот факт как раз вызовет то, что на контакте Y1 реле движения появилось напряжение, то есть электроны там начали вибрировать. Не углубляясь в те процессы что происходят внутри реле времени случилось то, что контакты 15 и 18 соединились. И мы получили то, что контакт 15 передал напряжение на розетку Шуко, в которую и подключено наше беспроводное оборудование. На этом по схеме всё.

Реле времени имеет разные режимы работы.

Я выбрал режим №24. Временной интервал при этом задается переключателями на самом реле. А регулировка внутри интервала производится круговым регулятором на корпусе реле. Временной интервал можно регулировать от 0.3 секунд до 10 часов.

Выбрал интервал 10-100 минут.

И установил регулятор внутри диапазона примерно на 4.

В режиме №24 при замыкании управляющего контакта Y1 контакты 15 и 18 замыкаются и начинается отсчёт заданной выдержки времени. Отсчёт времени прерывается повторным замыканием управляющего контакта Y1. Контакты 15 и 18 разомкнутся если интервал между командами внешнего запуска больше установленной выдержки времени. Реле включается при поступлении очередной команды внешнего
запуска.

Цитата о работе режима из инструкции:

При замыкании управляющего контакта реле включается и начинается отсчёт заданной выдержки времени. Отсчёт времени прерывается повторной командой внешнего запуска.

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

Какие плюсы у этого решения:

  • Экономия электроэнергии. Так как мы используем беспроводную сеть для мобильных устройств, мы не пользуемся ей когда мы спим или покинули квартиру. Мой беспроводной роутер потребляет около 4 Ватт в час. При 12 часовом простое, это соответствует примерно 1.4 КВт в месяц. В год 17 КВт. Таким образом на примере 100 тысяч устройств мы сэкономим около 1.7 МВт энергии в год.
  • Возможность раньше уснуть. Мы столкнулись с тем фактом, что мы ленивы. И когда мы лениво лежим в кровати с телефоном, через 15 минут интернет у нас отключается. Выбор в этом случае встать и пойти подвигаться в кородоре или спать. Чаще всего мы выбираем сон.
  • Больше движения. В те моменты когда мы не выбираем сон, кто-то один, чаще всего я, идет двигаться в коридор.
  • Уменьшение вероятности взлома. Тут все просто. Нет беспроводной сети — общая вероятность взлома понижается.

Минусы решения:

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

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

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

А вы отключаете Wi-Fi на ночь? Хотели бы чтобы он отключался автоматически?

ссылка на оригинал статьи https://habr.com/ru/post/504706/

Как скомпилировать декоратор — C++, Python и собственная реализация. Часть 1

Данная серия статей будет посвящена возможности создания декоратора в языке С++, особенностям их работы в Python, а также будет рассмотрен один из вариантов реализации данного функционала в собственном компилируемом языке, посредством применения общего подхода для создания замыканий — closure conversion и модернизации синтаксического дерева.

Дисклеймер

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

Декоратор в С++

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

Однако перед тем, как перейти к непосредственному описанию моей реализации, я бы хотел рассказать немного про то, как устроены лямбы и замыкания в С++ и какая между ними разница. Сразу оговорюсь, что если нет никаких упоминаний конкретного стандарта, то по умолчанию имеется в виду С++20. Если говорить коротко, то лямбды – это анонимные функции, а замыкания, функции, которые используют объекты из своего окружения. Так например, начиная с С++11, лямбду можно объявить и вызвать так:

int main()  {     [] (int a)      {         std::cout << a << std::endl;     }(10); } 

Или присвоить ее значение переменной и вызвать потом.

int main()  {     auto lambda = [] (int a)      {         std::cout << a << std::endl;     };     lambda(10); }

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

class __lambda_60_19 { public:      inline void operator()(int a) const     {         std::cout.operator<<(a).operator<<(std::endl);     }          using retType_60_19 = void (*)(int);     inline operator retType_60_19 () const noexcept     {         return __invoke;     };      private:      static inline void __invoke(int a)     {         std::cout.operator<<(a).operator<<(std::endl);     }     }; 

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

void (*p_lambda) (int) = lambda; p_lambda(10); 

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

int main() {     int a = 10;     auto closure = [&a] () { a += 1; };     closure(); } 

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

Но вернемся к внутреннему представлению замыкания в С++.

class __lambda_61_20 { public:     inline void operator()()     {         a += 1;     } private:     int & a; public:     __lambda_61_20(int & _a)     : a{_a}     {} };

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

int main() {     int a = 10;     auto closure = [&a] () { a += 1; };     closure();     void (*ptr)(int) = closure; }

Однако, если вам все же нужно куда-то передать замыкание, никто не отменял использование std::function.

std::function<void()> function = closure; function();

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

Итак, декоратор должен принимать на вход нашу функцию или метод, добавлять к ней необходимый нам функционал (для примера это будет опущено) и возвращать новую функцию при вызове которой происходит выполнение нашего кода и кода функции/метода. На этом моменте любой уважающий себя питонист скажет: «Но как же! Декоратор должен заменять исходный объект и любое обращение к нему по имени должно вызвать уже новую функцию!». Как раз в этом и есть основное ограничение С++, мы не можем никак помешать пользователю вызвать старую функцию. Конечно, есть вариант получить ее адрес в памяти и перетереть (в таком случае обращение к ней приведет к аварийному завершению программы) или заменить ее тело на вывод в консоль предупреждения о том, что ее не нужно использовать, но это чревато серьезными последствиями. Если первый вариант вообще кажется довольно жестким, то второй, при использовании различных оптимизаций компилятора, тоже может привести к падению, а поэтому их мы использовать не будем. Также, использование любой макросной магии здесь я считаю излишним.

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

namespace Decorator {     template<typename R, typename ...Args>     static auto make(const std::function<R(Args...)>& f)     {         std::cout << "Do something" << std::endl;         return [=](Args... args)          {             return f(std::forward<Args>(args)...);         };     } }; 

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

Давайте создадим простую функцию, которую мы хотим задекорировать.

void myFunc(int a) {     std::cout << "here" << std::endl; }

И наш main будет выглядеть следующим образом:

int main() {     std::function<void(int)> f = myFunc;     auto decorated = Decorator::make(f);     decorated(10); }

Все работает, все прекрасно и вообще Ура.

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

  1. Этот код может собраться только при версии С++14 и выше, так как невозможно узнать заранее возвращаемый тип. К сожалению, с этим придется жить и я не нашел других вариантов.
  2. make требует, чтобы ему передавали именно std::function, а передача функции по имени приводит к ошибкам компиляции. И это совсем не так удобно, как хотелось бы! Мы не можем писать код наподобие этого:
    Decorator::make([](){}); Decorator::make(myFunc); void(*ptr)(int) = myFunc; Decorator::make(ptr);

  3. Также, невозможно задекорировать метод класса.

Поэтому, после небольшого разговора с коллегами, был придуман следующий вариант для С++17 и выше:

namespace Decorator {     template<typename Function>     static auto make(Function&& func)     {         return [func = std::forward<Function>(func)] (auto && ...args)          {             std::cout << "Do something" << std::endl;             return std::invoke(                 func,                 std::forward<decltype(args)>(args)...             );         };     } };

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

Варианты применения

int main() {     auto decorated_1 = Decorator::make(myFunc);     decorated_1(1,2);      auto my_lambda = [] (int a, int b)      {          std::cout << a << " " << b <<std::endl;      };     auto decorated_2 = Decorator::make(my_lambda);     decorated_2(3,4);      int (*ptr)(int, int) = myFunc;     auto decorated_3 = Decorator::make(ptr);     decorated_3(5,6);      std::function<void(int, int)> fun = myFunc;     auto decorated_4 = Decorator::make(fun);     decorated_4(7,8);      auto decorated_5 = Decorator::make(decorated_4);     decorated_5(9, 10);      auto decorated_6 = Decorator::make(&MyClass::func);     decorated_6(MyClass(10)); }

Кроме того, этот код можно собрать с С++14 если есть расширение для использования std::invoke, который нужно заменить на std::__invoke. Если же расширения нет – то придется отказаться от возможности декорировать методы класса, а данный функционал станет недоступным.

Чтобы не писать громоздкое «std::forward<decltype(args)>(args)…» можно воспользоваться функционалом, доступным с С++20 и сделать нашу лямбду шаблонной!

namespace Decorator {     template<typename Function>     static auto make(Function&& func)     {         return [func = std::forward<Function>(func)]          <typename ...Args> (Args && ...args)          {             return std::invoke(                 func,                 std::forward<Args>(args)...             );         };     } }; 

Все прекрасно-безопасно и даже работает так, как мы хотим (или, по крайней мере, делает вид). Данный код собирается и под gcc и под clang 10-x версий и найти его можно вот здесь. Там же будут лежать реализации для различных стандартов.

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

ссылка на оригинал статьи https://habr.com/ru/post/504694/