- PVSM.RU - https://www.pvsm.ru -
Прочитав недавно (1 [1], 2 [2], 3 [3]) с каким трудом даются “космические” процессоры, невольно задался мыслью, раз “цена” за устойчивое железо настолько высока, может быть стоит сделать шаг и с другой стороны — сделать устойчивый к спецфакторам “софт”? Но не прикладной софт, а скорее среду его выполнения: компилятор, ОС. Можно ли сделать так, чтобы выполнение программы в любой момент можно было оборвать, перезагрузить систему и продолжить с того же (или почти с того же) места. Существует же в конце концов гибернация [4].
Почти всё, что прилетает из космоса, способно нарушить работу микросхемы, дело лишь в количестве энергии, которое “это” принесло с собой. Даже фотон, если у него длина волны гамма-кванта, способен преодолеть несколько сантиметров алюминия и ионизировать атом(ы) или даже вызвать ядерный фотоэффект [5]. Электрон не может проникнуть через сколь-нибудь плотное препятствие, но, если его разогнать посильнее, при торможении испустит гамма-квант со всеми вытекающими последствиями. Учитывая, что период полураспада у свободного нейтрона около 10 минут, редкий (и очень быстрый) нейтрон долетает к нам от Солнца. А вот ядра всего чего угодно долетают и тоже способны натворить дел. Нейтрино разве что не замечены ни в чем подобном.
Как тут не вспомнить Пятачка с его: “трудно быть храбрым, когда ты всего лишь Очень Маленькое Существо”.
Последствия попадания космического излучения в полупроводник могут быть разными. Это и ионизация атомов и нарушение кристаллической решетки и ядерные реакции. Вот здесь [6] описывается легирование кремния тепловыми нейтронами в атомном реакторе, когда Si(30) превращается в P(31), при этом достигаются нужные полупроводниковые свойства. Не стоит пересказывать упомянутые замечательные статьи, отметим лишь следующее —
Отметим, что эффекты 2 и 3 типов, если их удалось купировать, приводят к постепенной деградации микросхемы. Например, если в суперскалярном процессоре “выгорел” один из (пусть 4) сумматоров, можно (по крайней мере умозрительно это не трудно) пострадавшему физически отключить питание и пользоваться оставшимися тремя, внешне будет заметно лишь падение производительности. Аналогично, если поврежден один из регистров внутреннего пула, он может быть помечен как “вечно-занятый” и не сможет участвовать в планировании операций. Блок памяти может стать недоступным. … Но вот если испортилось нечто невосполнимое, придётся поднимать холодный резерв. Если он есть.
«Пребывание в холодном резерве не предохраняет кстати микросхему от накопления дозы, и даже от накопления заряда в подзатворном диэлектрике. Более того, известны микросхемы, у которых дозовая деградация без подачи питания даже хуже, чем с ней. А вот все одиночные эффекты, вызывающие жесткие отказы, требуют включенности микросхемы. При отключенном питании могут быть эффекты смещения, но они для цифровой логики не важны». (amartology [7])
Таким образом действуют два фактора
Как же со всем этим живут? За счет резервирования/троирования с голосованием по всей иерархии функциональных блоков. Само по себе троирование не является панацеей, оно нужно для того, чтобы понять какой из результатов правильный при сбое одного из компонентов. Тогда засбоивший компонент можно перезапустить и привести в соответствие с двумя рабочими. Но в случае отказа, когда компонент невозможно привести в рабочее состояние, поможет только холодный резерв, если он есть.
Даже если отказ не выглядит критическим, он может вызывать серьезные проблемы. Допустим, у нас три синхронно работающих компьютера, в одном из них произошел (гипотетический, упомянутый выше) отказ одного из сумматоров. Это не является проблемой с точки зрения компьютера, который остался работоспособным, но это проблема для всей системы т.к. пострадавший компьютер начнет систематически опаздывать и потребуются нешуточные усилия по общей синхронизации.
Еще пример, отказ памяти, в результате которого какой-то ее диапазон (пусть одна страница) пришел в негодность, не является критичным с точки зрения одного компьютера. Операционная система после диагностики способна справиться с этой проблемой, не используя данный диапазон. Но с точки зрения троированной системы, это катастрофа. Теперь, случись сбой (который лечится перезагрузкой), нам потребуется привести засбоивший компьютер в состояние идентичное любому из оставшихся, а это невозможно т.к. на других компьютерах этот диапазон рабочий и вероятно используется. В принципе, возможно запретить этот диапазон на всех трёх компьютерах, правда, не очевидно что удастся это сделать без перезагрузки всех компьютеров по очереди.
Парадоксальная ситуация, когда троированная на верхнем уровне система менее надёжна по сравнению с одиночным компьютером, который умеет приспосабливаться к постепенной деградации.
Стоит упомянуть про подход, который называется Lock-step [8], когда два ядра выполняют одну и ту же задачу со сдвигом в один-два такта, и после этого результаты сравниваются. Если они не равны — какой-то кусок кода пере-выполняется. Это не срабатывает при ошибке в памяти или общем кэше, впрочем, там есть своя защита.
Также есть подход [9], где компилятор повторяет выполнение части команд и сравнивает результаты. Такой софтовый вариант Lock-step.
Оба этих подхода (спасибо amartology [7] за наводку) — попытка обнаружить сбой и попытаться исправить его “малой кровью”, без перезагрузки. Мы же далее скорее будем рассматривать ситуацию, когда произошел серьёзный сбой или некритичный отказ и перезагрузка неизбежна. Как сделать, чтобы программу без особых усилий с её стороны можно было прервать в любой момент, а потом продолжить без серьёзных потерь.
Как научить железо и ОС адаптироваться к постепенной деградации — тема для отдельного разговора.
Сама по себе идея устойчивой/персистентной памяти не нова, вот и уважаемый Дмитрий Завалишин (dzavalishin [10]) предложил свою концепцию персистентной памяти [11]. В его руках это породило целую персистентную ОС “Фантом” [12], фактически виртуальную машину с соответствующими накладными расходами.
Возможно, со временем созреют технологии MRAM [13] или FRAM [14], … пока они сырые.
Существует также легенда про бортовой вычислитель ракеты Р-36М [15] (15Л579 ?), которая умела стартовать сквозь радиоактивное облако непосредственно после близкого ядерного взрыва. Примененная память на ферритовых сердечниках невосприимчива к радиации. Цикл записи у такой памяти порядка единиц мксек [16], так что за время, пока ракета летит считанные дециметры, была физическая возможность сохранить контекст работы процессора — содержимое регистров и флагов. Проснувшись в безопасной обстановке, процессор продолжал работу.
Звучит правдоподобно.
Есть некоторые “но”:
Вообще, восстановление после сбоев, в сущности — аналог обработки исключений. Собственно и сам сбой в большинстве случаев начинается как аппаратное прерывание. Разница в том, что после исключения мы просто можем продолжить работу, а в данном случае прежде требуется восстановить рабочий контекст — память и состояние
ядра операционной системы. Но финальная часть выглядит также.
Для начала о том, как это должно выглядеть со стороны прикладного программиста.
Раз восстановление после сбоев аналогично восстановлению после выброса исключения, то и работа с ним может выглядеть аналогично. Например, в С++ наследуем от std::exception класс std::tremendous_error, ловим его в обычном блоке try/catch и организуем обработку.
Впрочем, автору больше нравится семантика setjmp [17]/longjmp [18] (SJLJ) т.к.:
В своё время SJLJ проиграла [19] технике DWARF [20] (строго говоря, dwarf это просто формат записи информации) в обработке исключений из-за худшей производительности, здесь производительность не столь важна. В любом случае сохранение состояния не будет дешевым, надо подходить к нему ответственно.
Что требуется сохранить, из чего состоит контекст выполнения процесса?
Это означает, что при выделении нового сегмента для процесса, ОС должна резервировать в файле подкачки место для размещения этого сегмента. Не хватало только чтоб при сохранении состояния кончилось место на диске.
Не требуется информация для перекодировки из виртуальной памяти в физическую и обратно, при рестарте эта информация воссоздастся сама, возможно и по другому.
Что касается работы с файловой системой. Среди файловых систем есть и транзакционные. Если прикладной программе требуется именно транзакционное поведение, сохранение контекста процесса следует синхронизировать с подтверждением транзакции файловой системы. С другой стороны, например, для записи текстовых логов логично использование обычной файловой системы, транзакционность здесь была бы странной.
Из всего вышеперечисленного наибольшие вопросы вызывает именно сохранение содержимого памяти, объем всего остального по сравнению с этим незначителен.
Например, runtime [21] библиотека буферизирует выделения памяти, просит их у системы относительно большими кусками и сама занимается раздачей. Поэтому созданий/удалений сегментов относительно немного.
А вот с памятью программы работают беспрестанно, в сущности именно подсистема памяти обычно и является узким местом в расчетах. И всё что может упростить нам жизнь — аппаратная поддержка флагов измененных страниц. Ожидается, что в период между сохранениями состояний, появляется не слишком много измененных страниц.
Исходя из этого в дальнейшем мы будем заниматься именно содержимым памяти.
Желаемое поведение близко к базам данных — СУБД в любой момент может “упасть”, но проделанная работа сохранится вплоть до последнего коммита. Достигается это за счет ведения лога транзакций, попадание в который записи о коммите легализует все изменения, сделанные в транзакции.
Но, поскольку термин “транзакционная память [22]” занят, мы введем другой — “нерушимая память”.
Навскидку видны два метода, которыми эту нерушимую память можно реализовать
Вариант первый, назовём его “незатейливый”.
Основная идея — все измененные в транзакции данные должны помещаться в оперативной памяти. Т.е. в процессе работы механизм подкачки ничего не сохраняет на диск, но во время коммита все измененные страницы сохраняются в файл подкачки.
В лог пишется информация о выделенных сегментах и их связке с местом в файле подкачки. Во время работы эта информация накапливается и записывается во время коммита. При рестарте система имеет возможность создать сегменты заново. Механизм подкачки сможет их подтягивать и прерванная программа магическим образом получит свои данные.
Однако, в таком режиме невозможно, например, выделить calloc [23]-ом массив размером больше доступной памяти (malloc [24]-ом, кстати, можно). Впрочем, это в любом случае была бы не очень хорошая идея.
Пусть даже такой режим распространяется лишь на процессы, которые заявили себя как “нерушимые”, объем памяти, занимаемой текущими транзакциями всех таких процессов не может превосходить физически доступную. Механизм подкачки фактически перестаёт заниматься подкачкой и превращается в механизм хранения последних транзакций.
Всё это накладывает определенную дисциплину на прикладных разработчиков, может приводить к неравномерной нагрузке на диск, в общем, это не совсем то, что мы хотели, но во встраиваемых системах может и сработать.
Существенным недостатком такого варианта является то, что неустранимая ошибка во время коммита, когда записалась только часть страниц, приводит соответствующий процесс в нестабильное состояние, после чего его придётся остановить.
Получается какая-то 50%-ая нерушимость.
Вариант второй, “теневой”
Чтобы действовать как менеджер транзакций, нужно быть менеджером транзакций.
Определимся с сущностями:
Реестр памяти расположен где-то недалеко от TLB [25], т.к. информация в них частично перекрывается.
С точки зрения производительности было бы хорошо иметь для сегментов (по возможности) непрерывные области в файле подкачки. Хорошо и с точки зрения производительности и с точки зрения компактности реестра памяти. Но для этого придётся прибегнуть к аллокатору страниц не склонному к фрагментации, ex: методу близнецов (Buddy Allocator [26]) и заплатить за это перерасходом дискового пространства.
При создании и уничтожении сегментов, соответствующие записи появляются в логе транзакций. Нужны они чтобы при восстановлении после сбоя воссоздать реестр памяти для успешных транзакций.
Стоит отметить — пока программа просто меняет что-то в памяти, ничего не происходит кроме аппаратного взведения у измененных страниц флага “dirty [29]”. Необходимость в порождении теневой страницы возникает лишь при вытеснении её в файл подкачки.
Вопросов два: что делать с теневой страницей при наступлении коммита и что делать, когда эту же страницу поменяют в следующей транзакции.
Про коммит. Пока транзакция не завершена, на диске хранится старая версия и в случае сбоя ей можно пользоваться. Но при коммите мы не можем просто так взять и записать теневую страницу на старое место. Даже модифицированные страницы в памяти, у которых нет теневых номеров, не могут быть записаны на старое место. Таких страниц много и их запись неатомарна. А что если сбой произойдет после того, как записали лишь половину из них? Процесс станет нестабильным, восстановить его не удастся.
Таким образом все измененные страницы должны получить теневые номера, после чего будут сохранены по этим теневым местам.
Каждое создание теневой страницы должно попасть в лог транзакций в виде записи — (тег=создание теневой страницы, номер транзакции, старый номер страницы, новый номер страницы).
Коммит транзакции порождает запись в логе транзакций
(тег=коммит, номер транзакции). Эту запись можно считать атомарной операцией.
При восстановлении после сбоя ОС читает лог транзакций. При чтении записи о начале транзакции, для неё создаётся рекодер страниц. Далее в процессе чтения лога транзакций, все записи, относящиеся к теневым страницам этой транзакции, расширяют содержимое этого рекордера.
Если мы дочитали до конца лога и не встретили записи о коммите, просто уничтожаем все данные этой транзакции. Но если такая запись найдена, текущее состояние рекодера попадает в общий рекодер. А также необходимо изменить состояние аллокатора страниц.
Запись всех изменённых страниц памяти — дорогостоящее мероприятие и блокирование прикладной программы на время выполнения коммита может показаться негуманным. К счастью такое блокирование и не требуется, достаточно того, чтобы ОС поставила все страницы в очередь на запись, а коммит в лог транзакций записала лишь после того, как придут все подтверждения.
В результате возникает странная ситуация, когда вовсю идет следующая транзакция, хотя предыдущая фактически еще не завершилась.
Критическим проблем при этом на данный момент не видно.
Самое простое и неправильное решение — давайте просто освободим старую страницу а информацию о новой внесём в реестр памяти. Общий рекодер в данном случае вообще не нужен, его функцию будет исполнять реестр памяти. Решение неправильное, потому что приведёт к тотальной фрагментации файла подкачки. Ранее мы голосовали за непрерывные интервалы страниц для сегментов, про это можно забыть т.к. очень быстро число интервалов сегмента будет равно числу страниц.
Можно сказать — подумаешь, в наш век SSD дисков фрагментация больше не имеет значения! На самом деле, непрерывное чтение и для SSD дисков (пусть и не настолько драматично как для “дисковых” дисков) заметно быстрее чтения с произвольным доступом.
Кроме того, распухнет реестр памяти и работа с ним изрядно замедлится.
Более разумное поведение — придержать старую страницу до лучших времен. Лучшие времена наступят, когда эту страницу вновь поменяют. Тогда у теневой страницы окажется оригинальный номер, а после коммита эта страница вернется в исходное состояние. И уже тогда можно будет освободить теневую страницу (или придержать до лучших времён).
Но как же так, спросит дотошный читатель, мы ведь боремся с фрагментацией, а это она и есть. Да, риск фрагментации налицо, но это управляемый риск. Начнём с того, что страницы часто модифицируются пачками, не по одной, поэтому достаточно выдавать теневые страницы из последовательных пулов и острота проблемы снизится. Кроме того, возможен экстремальный вариант — при создании сегмента сразу выделять последовательные номера и для теневых страниц тоже. Тогда в реестре памяти придётся хранить еще и теневую начальную страницу.
Сложности две. Первая. Надо что-то делать с активными на момент checkpoint-а транзакциями. Разберем на примере.
Допустим, процесс в пределах транзакции создал новый сегмент и под него выделил аллокатором страницы. Аллокатор страниц обязан учитывать новые страницы просто чтобы не отдать их кому-нибудь еще раз.
И вот наступает момент checkpoint-а. И реестр памяти и аллокатор страниц должны сериализовать своё актуальное состояние без учета открытых транзакций, иначе после вероятного сбоя/восстановления возникнет утечка.
По-видимому, и реестр памяти и аллокатор страниц должны каким-то образом помечать захваченные/освобожденные, но не подтвержденные коммитом ресурсы. Это же касается и других хранителей ресурсов, которые появятся (объектов ядра...). Теперь для сохранения актуального состояния нет преград.
Вторая. Всё должно быть сделано атомарно в то время как сохранить надо состояния нескольких сущностей. Это довольно просто. Все сущности сериализуются в блоб, ссылка на который хранится в заголовке файла подкачки. Поскольку запись заголовка — акт атомарный, можно считать атомарным и весь checkpoint.
Жаль, не существует устройств хранения, совершенно устойчивых к длительной работе в космических условиях. Ферритовые сердечники были устойчивы к радиации, но имели свои специфические проблемы в связи с большим количеством паяных соединений. Плюс низкая ёмкость, малая скорость и высокая трудоёмкость изготовления.
Тем не менее необходимо уметь надёжно писать и читать эти данные.
Очевидным кандидатом можно считать флэш память. Флэш изначально не отличалась высокой надёжностью из-за невысокого числа допустимых циклов записи, поэтому для работы с ней были разработаны специальные методы [31].
Ранее упоминалось, что для работы с ненадёжными элементами используется троирование, здесь достаточно RAID1 [32] т.к. при ошибке записи благодаря контрольным значениям известно какая из двух страниц записалась некорректно и подлежит перезаписи.
Ну вот, теперь у нас на руках все четыре буквы слова ACID [33].
A — атомарность, достигнута
C — согласованность, налицо
I — изоляция, достигается естественным образом. Если не рассматривать случай разделяемой памяти. А мы на данный момент его не рассматриваем.
D — стойкость, единственный раз мы попытались смухлевать, когда отпустили процесс после коммита не дожидаясь физической записи на диск всех данных его памяти. В худшем случае это приведёт к откату на предыдущую транзакцию. Непонятно, насколько это критично, и для производительности и для стойкости.
PS. Просто на заметку. У нас не предусмотрен механизм отката транзакций, откатом может быть только неустранимая ошибка работы. Технически (кажется) нетрудно реализовать программный откат транзакции как аналог longjmp. Но это гораздо более продвинутый вариант longjmp т.к. полностью восстанавливает внутреннее состояние процесса на момент “setjmp”, не допуская утечек памяти, разрешает переход не только снизу вверх по стеку …
PPS. Прототипом менеджера транзакций, пожалуй, можно считать DBMS сервер OpenLink Virtouso [34], доступный и как free software.
PPPS. Спасибо Валерию Шункову (amartology [7]) и Антону Бондареву (abondarev [35]) за содержательное и весьма полезное обсуждение.
PPPPS. Автор иллюстрации Анна Русакова [36].
Автор: Борис Муратшин
Источник [37]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/osrv/347598
Ссылки в тексте:
[1] 1: https://habr.com/ru/post/483016/
[2] 2: https://habr.com/ru/post/482904/
[3] 3: https://habr.com/ru/post/452128/
[4] гибернация: https://ru.wikipedia.org/wiki/%D0%93%D0%B8%D0%B1%D0%B5%D1%80%D0%BD%D0%B0%D1%86%D0%B8%D1%8F_(%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D1%8B%D0%B5_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D1%8B)
[5] ядерный фотоэффект: https://ru.wikipedia.org/wiki/%D0%AF%D0%B4%D0%B5%D1%80%D0%BD%D1%8B%D0%B9_%D1%84%D0%BE%D1%82%D0%BE%D1%8D%D1%84%D1%84%D0%B5%D0%BA%D1%82
[6] здесь: https://habr.com/ru/post/388867/
[7] amartology: https://habr.com/ru/users/amartology/
[8] Lock-step: https://en.wikipedia.org/wiki/Lockstep_(computing)
[9] подход: http://ieeexplore.ieee.org/document/7365660
[10] dzavalishin: https://habr.com/ru/users/dzavalishin/
[11] персистентной памяти: https://habr.com/ru/post/279443/
[12] ОС “Фантом”: https://habr.com/ru/search/?q=%5B%D1%84%D0%B0%D0%BD%D1%82%D0%BE%D0%BC%20%D0%BE%D1%81%5D&target_type=posts
[13] MRAM: https://ru.wikipedia.org/wiki/%D0%9C%D0%B0%D0%B3%D0%BD%D0%B8%D1%82%D0%BE%D1%80%D0%B5%D0%B7%D0%B8%D1%81%D1%82%D0%B8%D0%B2%D0%BD%D0%B0%D1%8F_%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D0%B0%D1%8F_%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D1%8C
[14] FRAM: https://ru.wikipedia.org/wiki/FRAM
[15] Р-36М: https://ru.wikipedia.org/wiki/%D0%A0-36%D0%9C
[16] единиц мксек: http://uzaitsev.blogspot.com/2016/04/blog-post_9.html
[17] setjmp: https://ru.wikipedia.org/wiki/Setjmp.h
[18] longjmp: https://ru.wikipedia.org/wiki/Longjmp
[19] проиграла: https://habr.com/post/267771/
[20] DWARF: http://wiki.dwarfstd.org/index.php?title=Exception_Handling
[21] runtime: https://ru.wikipedia.org/wiki/%D0%91%D0%B8%D0%B1%D0%BB%D0%B8%D0%BE%D1%82%D0%B5%D0%BA%D0%B0_%D1%81%D1%80%D0%B5%D0%B4%D1%8B_%D0%B2%D1%8B%D0%BF%D0%BE%D0%BB%D0%BD%D0%B5%D0%BD%D0%B8%D1%8F
[22] транзакционная память: https://ru.wikipedia.org/wiki/%D0%A2%D1%80%D0%B0%D0%BD%D0%B7%D0%B0%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D0%B0%D1%8F_%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D1%8C
[23] calloc: http://www.opengroup.org/onlinepubs/009695399/functions/calloc.html
[24] malloc: http://www.opengroup.org/onlinepubs/009695399/functions/malloc.html
[25] TLB: https://en.wikipedia.org/wiki/Translation_lookaside_buffer
[26] Buddy Allocator: https://en.wikipedia.org/wiki/Buddy_memory_allocation
[27] Теневые: https://en.wikipedia.org/wiki/Shadow_paging
[28] COW: https://en.wikipedia.org/wiki/Copy-on-write
[29] dirty: https://en.wikipedia.org/wiki/Dirty_bit
[30] checkpoint: https://docs.microsoft.com/en-us/sql/relational-databases/logs/database-checkpoints-sql-server?view=sql-server-ver15
[31] методы: https://ru.wikipedia.org/wiki/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D1%84%D0%B0%D0%B9%D0%BB%D0%BE%D0%B2%D1%8B%D1%85_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC#%D0%A4%D0%B0%D0%B9%D0%BB%D0%BE%D0%B2%D1%8B%D0%B5_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D1%8B_%D0%B4%D0%BB%D1%8F_%D1%84%D0%BB%D0%B5%D1%88-%D0%B4%D0%B8%D1%81%D0%BA%D0%BE%D0%B2_/_%D1%82%D0%B2%D0%B5%D1%80%D0%B4%D0%BE%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D1%8B%D1%85_%D0%BD%D0%BE%D1%81%D0%B8%D1%82%D0%B5%D0%BB%D0%B5%D0%B9
[32] RAID1: https://ru.wikipedia.org/wiki/RAID
[33] ACID: https://ru.wikipedia.org/wiki/ACID
[34] OpenLink Virtouso: https://en.wikipedia.org/wiki/Virtuoso_Universal_Server
[35] abondarev: https://habr.com/ru/users/abondarev/
[36] Анна Русакова: https://moleska.livejournal.com/
[37] Источник: https://habr.com/ru/post/489678/?utm_source=habrahabr&utm_medium=rss&utm_campaign=489678
Нажмите здесь для печати.