Политики хранения Veeam B&R, — бэкапы, цепочки и магнитные ленты
В предыдущей части мы разобрали, как работает политика хранения для заданий первоначального бэкапа и создания его архивной копии. В этой части мы продолжим начатое и рассмотрим хранение на магнитных лентах.
Ретеншен магнитных лент может быть довольно сложен для понимания, потому что находится на стыке сразу трех сущностей — конечное содержимое магнитных лент зависит от настройки исходного задания, от настройки задания записи на ленту, а также от ретеншена медиа пула. Неправильная настройка может привести как к излишнему использованию ценных ленточек, так и к преждевременной перезаписи кассеты с данными. В лучшем случае вас ждут лишние траты, в худшем же — при необходимости восстановления может оказаться, что нужная кассета была перезаписана.
При написании этого текста я буду исходить, что читатель ознакомился с режимами работы бэкапа из предыдущей части, а также читал статью про тейпы в Veeam Backup & Replication 9.5 Update 4. Для интересующихся темой могу также порекомендовать статью от наших тестировщиков.
Как и ранее, информация актуальна для версии VBR 10. Если вы используете более старую версию (или более новую…), ожидайте возможные отличия в деталях.
Backup to tape + Стандартный медиа пул
Настройка медиа пула
Начнем с первой части уравнения — создания медиа пула. Его настройки определяют, на какие кассеты будут записаны бэкапы и как долго это нужно хранить. В этом разделе мы рассмотрим стандартные медиа пулы (в противоположность GFS, о которых речь пойдет дальше).
На содержимое кассет будут влиять настройки Media set и Retention. Медиа сет — это последовательная серия кассет, где новая кассета не берется, пока предыдущая не записана полностью. Пока медиа сет открыт, в него могут писать разные задания, причем как Backup to tape, так и File to tape. Когда медиа сет закрывается на кассете, то дозаписать ее уже нельзя, сколько бы на ней ни оставалось свободного места. Можно только переписать ее заново, удалив предыдущее содержимое.
Медиа сет можно создать для каждой сессии (2). В этом случае каждое задание, использующее кассеты из медиа пула, будет открывать новый медиа сет и закрывать его после завершения копирования. Представить это можно вот так:
Естественно, большое количество медиа сетов скорее всего увеличит количество недописанных кассет.
Второй вариант — закрывать предыдущий медиа сет и создавать новый по расписанию (3). Например, можно выставить создание на 0.00 понедельника. Тогда в условном месяце медиа сеты могут выглядеть так:
Наконец, можно использовать «бесконечный» медиа сет, который будет оставаться открытым, пока есть возможность (1):
«Бесконечный» медиа сет полезен для экономии кассет (они все будут записываться полностью), но периодически создавать новые медиа сеты и разделять кассеты по группам может быть удобнее с точки зрения менеджмента.
Если у ленточной библиотеки есть слот импорта/экспорта кассет, то можно использовать еще один способ создания медиа сета. Включается он не в настройках пула, а в самом тейповом задании (о них подробно поговорим чуть ниже):
Опция «Export current media set upon job completion» закроет медиа сет и переместит использованные кассеты в слот импорта/экспорта. Оттуда их можно взять и перенести в укромное место на хранение.
Следующий интересующий нас шаг визарда называется Retention. Тут возможны такие варианты:
Не сохранять данные вообще (1) — это опция для самых отважных, VBR позволяется сразу после сессии взять ту же кассету и перетереть все содержимое. Учтите, что в рамках единой сессии VBR перетирать кассеты не будет. Встречались запросы на такую реализацию: если задание записало кассеты 1, 2 и 3 и места не хватило, то VBR не должен ждать новую кассету, а брать снова первую и писать поверх только что записанного. Ретеншена же нет, а там разберемся! Но это заведет нас в логическую ловушку — если скопированный файл исчез с кассеты, но остался на репозитории, мы должны его скопировать снова, перетерев уже следующий, и так далее. Так что всему все-таки должна быть мера, и было решено выставить небольшое ограничение.
Никогда не перезаписывать (3) — полная противоположность. Задание само никогда не перепишет кассету. Однако ее можно стереть или пометить как свободную вручную. Если нужна гарантия, что данные нельзя удалить, то используйте кассеты WORM (write once — read many) и соответствующие медиа пулы. Там ретеншен вообще нельзя установить, потому что с WORM это не имеет смысла.
Наконец, самый рациональный вариант — выставить ретеншен на определенное количество дней (2).
В данном случае настройка определяет срок, в течение которого данные должны храниться на кассете. Обратите внимание: фактическая дата, когда кассету можно перезаписать, будет определена временем последней записи и для каждой кассеты будет высчитываться индивидуально. Эту дату можно увидеть в колонке EXPIRES IN:
Например, если ретеншен выставлен на неделю и кассета используется каждый день, то в понедельник ретеншен будет выставлен до следующего понедельника, но на следующий день будет продлен до вторника, и так до последней записи.
Таким образом, ретеншен и медиа сеты влияют друг на друга. Если новый медиа сет не создается, то на кассету можно дозаписывать данные, а значит, ее ретеншен будет продлен после каждой сессии. Если же медиа сет закрывается, то на такие кассеты уже ничего не дозаписывается, и ретеншен будет определен последней записью. Иными словами, если в каждой сессии записывается много данных и/или медиа сеты регулярно закрываются, то можно ожидать что ретеншен будет близок к установленному в настройках. Если же в каждую сессию данные записываются по чуть-чуть и используется «бесконечный» медиа сет, то ретеншен на кассете будет постоянно продлеваться.
Правильная настройка ретеншена сводится к двум простым принципам:
- Срок хранения точек на кассетах не должен быть короче, чем срок хранения этих точек на репозитории.
- Помните, что кассеты содержат цепочки, а значит, переписав содержимое старой кассеты, есть шанс испортить содержимое всех остальных кассет.
Теперь подробнее и с примерами. Ретеншен медиа пула выставляется в днях, неделях или месяцах. Ретеншен исходного задания — в днях или точках восстановления, но в конечном итоге все это можно привести к количеству дней. В итоге может получиться ситуация, когда B&R копирует точки на кассете, через некоторое время кассета переписывается, и скопированные точки исчезают. Между тем как минимум часть точек все еще хранится на репозитории. Тейповому заданию ничего не остается, как начать копировать их заново. Так будет повторяться постоянно — одни и те же файлы будут писаться по нескольку раз.
Схожая проблема имеется и с переписыванием кассеты, содержащей начальную часть цепочки (например, VBK). Это может случиться по ряду причин:
- Даже если ретеншен тейпового задания не короче исходного, но на кассетах хранится длинная цепь инкрементов, то переписывание старой кассеты с VBK «убьет» всю остальную цепь.
- Поскольку тейповые задания можно настроить на использование отдельных медиа пулов для полных бэкапов и инкрементов, есть риск не уследить и выставить нужный ретеншен только для одного из медиа пулов (в таких случаях опять же медиа пул для полных бэкапов лучше от греха подалее выставить на ретеншен подлиннее. Рекомендация от RnD — срок хранения данных на кассетах должен быть минимум в два раз дольше срока хранения данных на репозиториях).
- Наконец, по недосмотру данные с кассеты могут быть уничтожены пользователем вручную.
Рассмотрим, как это выглядит в консоли (пример интерфейса смотрите на скриншоте выше). Когда у кассеты заканчивается ретеншен, ее статус будет показываться как Expired. Это еще ничего не означает, все бэкапы на кассете останутся доступными. Если необходимо, можно даже изменить ретеншен медиа пула и B&R спросит, следует ли применить настройки к текущим кассетам. Отвечаете «да» — и кассета больше не Expired. Но Expired кассета может быть переписана в любой момент, а если это произойдет, то сохраненные на ней бэкапы исчезнут. Может возникнуть такая ситуация:
На кассетах присутствуют VIBы от 26 сентября, но им еще должен предшествовать VBK от более ранней даты, однако его нет — кассета с VBK была переписана. Попытка восстановиться с такой цепочки выдаст ошибку об отсутствии VBK.
Что касается пользовательских операций с кассетами, тут есть несколько вариантов. Если кассета была отмечена как свободная (mark as free), перемещена в другой пул или же удалена из каталога, то бэкапы исчезнут из консоли. К счастью, такая ситуация обратима — достаточно каталогизировать (catalog) кассету. VBR прочтет ее содержимое, найдет файлы и поместит их обратно куда надо. А вот если кассету стерли (erase), то это уже не поможет, так как при этом перетирается заголовок.
Возможна еще одна крайне неприятная ситуация. Если VBR записывает файл на кассету и на ней заканчивается место, то остатки файла будут записаны на следующую. Таким образом, один файл может быть «размазан» по двум и более кассетам. Естественно, стирание/переписывание даже одной кассеты повреждает файл и делает его бесполезным. В свойствах (properties) кассеты вы увидите следующее:
Наконец, расскажу о редком (к счастью), но тоже встречающимся сценарии. Допустим, сисадмину нужно срочно восстановиться с кассеты, но VBR выдает следующую ошибку: «Cannot use tape ХХХХХХХ: unknown media (possibly in use by another application)». Инженер техподдержки проверяет бесконечные репорты и логи, из которых следует что после записи бэкапов действий с кассетой не было. Сисадмин гарантирует, что кассета лежала у него под подушкой все это время и никто ее не трогал. Как Виму доказать свою невиновность? С помощью специальной утилиты инженер техподдержки считает с кассеты первые несколько сотен килобайт, в которых хранится заголовок, и откроет файл с помощью hex-редактора. И вместо ожидаемого VEEAM увидит там Symantec или нечто схожее. Ой.
Настройка задания
Создав медиа пул, разберемся с заданиями. Тейповые задания можно настроить копировать только полные бэкапы (VBK) или же как полные, так и инкрементальные (VIB). Копия полного бэкапа включена всегда (нужно только выбрать медиа пул), копия инкрементов включается на шаге Incremental backup (опять же, нужно выбрать тот же либо отдельный медиа пул):
Что именно будет записано на ленту — зависит от режима работы исходного задания (я буду исходить из того, что тейповое задание выставлено копировать полные и инкрементальные точки). При этом в одном тейповое задание можно добавить несколько исходных заданий, работающих в разных режимах. Каждое такое задание будет обрабатываться по соответствующим правилам.
Forward incremental
При таком исходном задании цепочка копируется на ленту 1 к 1 (или максимально близко к этому).
Для примера: исходное задание работает каждый день, с полными бэкапами в понедельник и пятницу. Тейповое задание работает по средам и воскресеньям. В среду будут скопированы точки с понедельника по среду, а в воскресенье — с четверга по воскресенье.
Из этого принципа есть одно исключение — опция «Process latest full backup chain only»:
Представьте ситуацию: возьмем условия как выше, но пусть тейповое задание запускается только в воскресенье. Без этой галочки была бы скопирована вся цепочка за прошедшую неделю, а с этой галочкой будут скопированы только самый последний VBK и его инкременты. Разница значительна.
Forever forward incremental
В первый запуск на ленту записывается вся цепочка — VBK и его VIBы (из этого правила есть одно исключение, о нем будет сказано чуть ниже). В последующие копируются только созданные инкрементальные точки. Однако, бесконечно так продолжаться не может: исходное задание держит общее количество точек под контролем путем объединения VBK и VIBов, но для цепочки на ленте такое невозможно. Чтобы не оказаться в ситуации, когда для восстановления нужно будет скопировать с кассет VBK и немыслимое количество инкрементов, был придуман «виртуальный полный бэкап». Для его настройки на шаге Media Pool нужно кликнуть по кнопке Schedule…
Виртуальный полный бэкап создаст в назначенный день VBK прямо на ленте, не занимая лишнего места на репозитории. Таким образом, бесконечно-инкрементальная исходная цепочка на репозитории де-факто превращается в инкрементальную с периодическим полным бэкапом (Forward incremental) на ленте.
Пример: задание за неделю создавало только инкременты. Тейповое задание запускается в среду и в пятницу, при этом в среду назначено создание виртуального бэкапа.
Обратите внимание на следующие свойства виртуального бэкапа:
- Виртуальный VBK создается только для заданий, работающих в бесконечно-инкрементальном режиме. Для других типов исходных заданий эта настройка просто игнорируется.
- Бэкап можно назначить на день, когда задание не работает вовсе. Это будет отмечено, и в день запуска задание скопирует созданные с последнего запуска инкременты, а также синтезирует пропущенный «VBK из прошлого» (используя вовсе не машину времени, а инкремент соответствующего дня).
- Если было пропущено несколько запланированных VBK, то скопируется только последний.
Пример: исходное задание работает каждый день, для тейпового задания виртуальный VBK назначен на вторник и четверг, тейповое задание запускается в воскресенье. В этом случае будет синтезирован только VBK четверга, для остальных дней будут просто скопированы инкременты.
- Уже упомянутая опция «Process latest full backup chain only» работает и тут. Если ее включить, тейповое задание будет копировать только точки после последнего виртуального VBK. Используя пример выше, разница будет следующей:
- В начале я сказал, что в первый запуск тейповая джоба перенесет бэкапную цепочку как она есть. Я умолчал про один факт — в первый запуск может быть сделан и виртуальный VBK. Дело в том, что расписание виртуального VBK работает и при первой синхронизации — если оно попадает на какой-то день в прошлом, то задание попытается синтезировать VBK. Приведу пример: исходное задание успело создать цепочку с VBK в понедельник и VIB со вторника по пятницу. Тейповое задание настроено создавать виртуальный VBK в четверг, опция «process latest full backup» отключена. В таком случае при первой синхронизации будет скопирована полностью цепочка, а в четверг вместо копии VIB будет синтезирован VBK. Если опция «process latest full backup» включена, то будет синтезирован только VBK четверга и скопирован VIB пятницы.
- Существует еще один сценарий, когда задание может начать копировать цепочку заново. Это происходит, если VBK на репозитории становится «новее», чем виртуальный VBK на ленте. Возьмем такой пример: виртуальный VBK создается раз в два месяца, а изначальное задание настроено хранить 30 точек и работает ежедневно. Из-за того, что на репозитории старые VIB постоянно объединяются с VBK, через некоторое время полная точка на репозитории станет «новее», чем хранящаяся на ленте. Как следствие, VBK и его VIBы будут внепланово скопированы на кассету.
Reverse incremental
Как известно, последняя точка в таких цепочках — полный бэкап. Вот его мы и скопируем. Каждый раз. Роллбэки будут проигнорированы. Допустим, как исходное, так и тейповое задания работают каждый день. Под конец недели ситуация будет следующей:
Backup copy
Без GFS ретеншена такие задания работают в бесконечно-инкрементальном режиме, поэтому бэкап на ленты следует уже рассмотренной схеме. Единственная особенность — при использовании «классического» режима BCJ с интервалом синхронизации (periodic copy/pruning) последняя точка в цепочке считается незавершенной и скопирована быть не может, поэтому тейповое задание всегда будет чуть отставать от исходного. В случае же использования новой «немедленной копии» и эта разница исчезает.
Backup copy с синтетическим GFS
Синтетический GFS ретеншен особых сложностей не добавляет. По умолчанию тейповое задание будет копировать как основную цепочку, так и GFS точки (это помимо синтезирования виртуальных полных бэкапов). Если столько полных точек на кассетах вам не нужно — на помощь снова приходит известная нам опция «Process latest full backup chain only». При ее включении GFS точки будут игнорироваться исходя из логики, что создаваемый с задержкой синтетический GFS никак не может считаться «latest». Использование «немедленной копии» описанную схему не меняет.
Backup copy с активным GFS
По внешним признакам BCJ с активным GFS работает в режиме forward incremental: тут есть периодический полный бэкап, а ретеншен применяется удалением старых точек. Но для тейповых заданий оно продолжает быть «бесконечно-инкрементальным», поэтому виртуальные полные точки тут будут продолжать создаваться согласно расписанию. Можно их использовать, чтобы дополнительно разделить длинную инкрементальную цепочку (если GFS точки не создаются очень часто). Если этого не требуется, то просто выставьте виртуальный VBK создаваться пореже (планировщик позволяет выставить интервал на 1 раз в год).
Возьмем для примера BCJ с интервалом синхронизации в 1 день, которое создает недельный GFS в понедельник. Тейповое задание также работает каждый день и создает виртуальный полный бэкап по четвергам. В течение недели ситуация будет следующей (обратите внимание, что последнюю точку скопировать пока нельзя):
Включение «Process latest full backup chain only» в таких условиях ничего не поменяет. Поэтому возьмем другой пример. Пусть тейповое задание работает раз в неделю по воскресеньям, и опция «Process latest full backup chain only» будет включена. В этом случае ситуация будет следующей:
Backup to tape + GFS медиа пул
Для начала я еще раз напомню о рекомендации прочесть статью про тейпы в VBR 9.5 U4, где объясняется общий принцип работы тейпового GFS. Уже прочли? Тогда поехали.
В отличие от обычного медиа пула, в GFS медиа пуле нет строгого разделения на настройки медиа сета и ретеншена. Вместо этого все сводится к одному шагу:
Каждый включенный GFS интервал создает свой медиа сет. На вкладке Advanced можно настроить VBR дозаписывать кассеты (галочка append), что где-то аналогично опции «always continue» из обычных медиа пулов. Иначе медиа сет будет создаваться для каждой сессии.
Поскольку недельные, месячные, квартальные и годовые GFS точки — это полные независимые бэкапы, то сложностей в настройке ретеншена тут обычно не возникает. Выставленный ретеншен определяет количество GFS точек, которые необходимо хранить. В настройках изначального задания тоже нет особых хитростей — оно может работать в любом режиме, если потребуется, Veeam Backup & Replication может сделать виртуальный VBK даже из роллбэка.
Особняком стоит дневной медиа сет. Во-первых, дневной медиа сет может скопировать больше одной точки за раз. Во-вторых, скорее всего это будут инкрементальные точки, поэтому они требуют наличия VBK, на который можно «опереться». Рекомендация тут классическая — не выставляйте ретеншен для дневного медиа сета длиннее, чем для недельного. Собственно, дневной медиа сет даже требует включения недельного, потому что инкременты скорее всего будут использовать его VBK, но все равно остается риск неудачной настройки. Например, если выставить недельный медиа сет хранить только 1 точку, то каждую неделю VBK будет переписываться, оставляя VIBы «осиротевшими».
File to tape
File to tape задания используют стандартные медиа пулы. Эти задания строят цепочки из полных и инкрементальных бэкапов (и могут использовать отдельные медиа пулы для каждого типа), но их цепочки не аналогичны заданиям бэкапа виртуальных машин. Для ВМ мы делаем бэкапы на уровне «блоков», тут же делается бэкап на уровне файлов: полный бэкап это просто копия всех файлов, инкрементальный — изменившихся. Если кассета с полной копией будет переписана, то данные на «инкрементальной» кассете могут быть восстановлены. Правда, в следующий раз вас ждет большой инкрементальный бэкап, поскольку заданию нужно перенести все отсутствующие файлы
Для таких заданий желательно делать периодический полный бэкап и настроить ретеншен, чтобы кассета со старым полным бэкапом не переписывалась, пока не сделан новый.
Немного особняком стоит бэкап NDMP, который позволяет добавить только тома целиком, но в конце концов является все-таки бэкапом на уровне файлов. Поэтому даже если кассета с полным бэкапом была удалена, инкременты остаются валидными, и что-то восстановить получится. Между тем, при бэкапе NDMP каждая 10я точка должна быть полным бэкапом, поэтому ретеншен также следует настроить, чтобы на кассетах хранился хотя бы один полный бэкап.
Типовые сценарии
Для завершения темы я попробую придумать несколько сценариев, чтобы показать на практике логику настройки, по возможности задействовав максимальное количество опций.
Сценарий 1
Исходное задание:
Исходное задание работает в инкрементальном режиме с бэкапом каждую ночь и с полным бэкапом в понедельник. Задание настроено хранить 14 точек.
Задача:
Хранить дубликат исходной цепочки на кассетах в течение месяца. Тейповое задание должно работать по выходным, в понедельник использованные кассеты вынимаются из библиотеки и помещаются в сейф.
Настройки:
Раз тейповое задание должно работать по выходным, пусть оно запускается в воскресенье после того, как будет создан бэкап. Таким образом за сессию оно скопирует VBK и 6 VIB, созданных за прошедшую неделю. Поскольку иметь разный ретеншен для полных и инкрементальных бэкапов не нужно, то можно использовать единый медиа пул. Если у библиотеки есть слот экспорта/импорта, можно воспользоваться опцией «export current media set» — медиа сет будет закрыт, а в понедельник работник просто заберет кассеты из слота. Если такого слота нет, то нужно выставить создание нового медиа сета в настройках пула, а работнику нужно уметь читать, чтобы понять какие кассеты пора забрать. На последней из использованных кассет скорее всего останется свободное место, но что поделаешь. Наконец, ретеншен. Поскольку кассеты не дозаписываются, то можно просто выставить ретеншен на месяц, по прошествии которого кассеты можно загрузить в библиотеку и использовать заново.
Сценарий 2
Наш начальник — жуткий эконом и хочет, чтобы использование дискового пространства под бэкапы и кассет под их копии было минимальным.
Исходное задание:
Исходное задание работает в бесконечно-инкрементальном режиме с бэкапом каждую ночь. Задание настроено хранить 14 точек.
Задача:
Хранить не меньше месяца бэкапов на кассетах. Кассеты остаются в библиотеке и переписываются после истечения ретеншена.
Настройки:
Нас поставили в жесткие рамки, придется выкручиваться. Самый важный вопрос — как часто делать виртуальный полный бэкап и как долго его хранить. Кажется, что самый очевидный ответ — делать его раз в месяц и хранить тоже месяц. Но тогда возникают сразу две проблемы. Во-первых, перетерев кассету с VBK, мы лишаемся всей цепочки. Кроме того, мы попадаем в описанную ранее ситуацию, когда VBK на репозитории станет «новее», чем VBK на кассете, что вызовет внеплановую копию цепочки. Поэтому если мы хотим создавать виртуальный VBK пореже, нам по-любому будет необходимо увеличить прежде всего ретеншен изначального задания.
Допустим, это невозможно. Что тогда? Почему бы нам не попробовать использовать GFS медиа пул с дневным медиа сетом? Прежде всего, уясним что тип цепочки на кассетах будет «forward incremental», а значит если мы хотим месяц бэкапов на кассетах, то по факту нам нужно держать цепочку длиннее.
В дневном медиа пуле настроим хранить 30 дней. Для этого медиа сета выставим опцию «append», чтобы кассеты записывались полностью. Помните, что фактический ретеншен определяется последней записью на кассету, поэтому скорее всего данные на кассете будет храниться дольше, чем ровно 30 дней. Для недельного медиа сета выставим ретеншен на 5 недель. Если полный бэкап занимает примерно целую кассету, то лучше «append» не включать для простоты менеджмента.
Сценарий 3
Скряга директор нам окончательно надоел, и мы нашли новую работу. И, чтобы забыть прошлый кошмар, мы устроились на фабрику по производству кассет. Наконец-то, можно забыть об экономии! Наш новый начальник хочет, чтобы на кассетах каждый день оказывался полный бэкап, а записанные кассеты больше не использовались.
Исходное задание:
Нам разрешили самим настроить исходное задание и тут у нас есть выбор: инкрементальное задание, которое делает полный бэкап каждый день; бесконечно-инкрементальное задание; реверсивно-инкрементальное задание; задание создания архивных копий, работающее в режиме «mirroring».
Настройки:
Раз уж нам нужны только полные бэкапы, то копию инкрементальных бэкапов можно отключить совсем. Для медиа пула настроим создавать медиа сет каждую сессию, а ретеншен выставим на «не переписывать» кассеты (для гарантии сохранности данных стоило бы использовать WORM, но нам таких не завезли). Осталось настроить само тейповое задание. Эта часть зависит от задания исходного:
- Исходное задание делает полный бэкап каждый день. Возможно, не самый рациональный вариант, но тут ничего особо думать не надо — тейповое задание должно запускаться после исходного каждый день. Оно скопирует VBK этого дня.
- Исходное задание работает в «бесконечно-инкрементальном» режиме. Тейповое задание также должно запускаться после исходного (и в рамках одних суток!), но нам также необходимо включить виртуальный полный бэкап на каждый день. Кстати, в десятой версии этот метод может быть самым быстрым при создании виртуального VBK благодаря асинхронному чтению.
- Исходное задание работает в «реверсивно-инкрементальном» режиме. Опять же, тейповое задание просто должно работать после исходного. Виртуальный бэкап не требуется, будет скопирован просто последний VBK.
- Наконец, с Backup copy нам нужно, чтобы исходное задание (исходное-исходное, а не копия) работало каждый день, а BCJ должно работать в режиме «немедленной копии», иначе последнюю точку нельзя будет использовать. В остальном настройки должны быть аналогичны как с работой с «бесконечно-инкрементальным» заданием.
Заключение
На этом серию, посвященную ретеншену B&R я считаю завершенной. Конечно, остались нерассмотренными множество видов заданий, но их ретеншен по большей части описывается в одну строчку и на полноценный пост этого не хватит. Если они все же вызывают какие-то сомнения, пишите в комментарии, разберемся вместе.
Я бы хотел поблагодарить своих коллег Loxmatiymamont, lyapkost, polarowl, которые оказали неоценимую помощь на каждом этапе подготовки этой серии.