Баги есть, отказов нет
Saturday, 4 Nov 2023
первая часть «четырех главных навыков разработчика»
умение гарантировать отказоустойчивость при наличии багов в коде
Large systems will probably always be delivered containing a number of errors in the software, nevertheless such systems are expected to behave in a reasonable manner.
Joe Armstrong
С появлением понятия «программного обеспечения», люди вынуждены жить с тем, что оно не всегда работает так, как ожидается. Будь это таракан в системном блоке, перегрызшие кабель крысы, перерубивший его экскаваторной лопатой норвежский строитель, или даже — страшно и произнести — ошибки в ПО, допущенные разработчиком.
Люди отчаянно боролись с ошибками в исходном коде, Дональд Кнут даже учредил премию за найденные в TeX проблемы. К сожалению, опыт Кнута очень плохо масштабируется. Иными словами, мы не можем попросить его написать весь софт в мире.
Чтобы минимизировать число ошибок проникающих к пользователям, человечество придумало тесты, типы, кодревью, автоматический анализ кода, черта в ступе. Но достаточно провести пару часов, просто бродя по интернету, чтобы воочию убедиться: это все ни хрена не работает. Нет, конечно, если считать кипиай, или что они там считают, то первая производная показывает уменьшение числа ошибок, а вторая — скорость этого процесса. А если просто возомнить себя простым человеком — то ничего не работает.
Бывают ошибки некритичные, хотя и досадные. Буквально третьего дня я стал свидетелем того, как пользователь забыл переключить раскладку при вводе пароля в почту гугла, и гугл показал ему страницу Ошибка 403
. Вроде, все нормально: логин не прошел, на то есть специальный код ошибки, на, получи свою гранату. Пользователя же эта страница ввела в ступор, он решил, что сделал что-то недозволенное — и написал мне, чуть ли не в панике. Брюзжание на предмет «пользовательской грамотности» — я брезгливо оставляю недоумкам, которые не чураются при этом менять дворники в автосервисе, ужинать в ресторанах, а подкрутить кран — зовут электрика.
Пользователи будут ошибаться, это нормально. Ненормально — показывать им 403
вместо того, чтобы сразу перекинуть обратно на страницу логина. Но это все, повторюсь, ошибки некритичные.
Основная проблема заключается в том, что и программисты ошибаются. И будут ошибаться. И никакие тесты-шместы и типы-шмипы их (нас) от этого не спасут. И мы рано или поздно принесем критичную ошибку. И наша цель — превентивно от этого защититься.
Человеческий мозг устроен сложно, но нам доподлинно известно: он искажает объективную реальность, чаще всего — в нужную нам сторону (обратное поведение требует хотя бы амбулаторного лечения). Когда я тестирую поведение моего куска …эээ… кода, я сознательно обманываю себя, что вот эти два очевидных и три неочевидных случая покроют все возможные варианты. Потом приходит тестер и пробует передать в качестве параметра ящерицу. Программист чинит код. А потом продукт выкатывается к пользователям.
Я в каждом тексте длиннее лимерика рекомендую Property Based Testing, порекомендую и тут — эта техника поможет разгрузить тестера и оставить его ящериц дома. Но от неочевидной ошибки в коде, проявляющейся, например, при переводе часов на летнее время, она не защитит.
Так что же делать? — Да просто вслушаться в то, о чем Джо Армстронг говорил более тридцати лет назад: ошибки в коде будут всегда. Пытаться упразднить их с помощью тестов и типов — все равно, что защищаться от ливня полиэтиленовым пакетом, надетым на голову: голова сухая, но выглядите вы по-идиотски, да и ниже шеи — насквозь промокли.
Как же быть? Ну, для начала, смиритесь с тем, что ошибки будут всегда. Вы не сможете починить их все (если вы не Дональд Кнут, конечно). После стадии «принятия» — ну можно попробовать почитать, что там Джо говорит дальше по этому поводу.
А говорит он примерно вот что. Во всех неожиданных случаях — останавливайте выполнение. Не пытайтесь покрыть все возможные пути развития ситуации. Обработайте успех и ожидаемую ошибку, если это осмысленно (например, при неудачном логине — перейдите на форму логина). Во всех остальных случаях, включая приползшую по сети ящерицу, — останавливайте выполнение. В эрланге этот принцип подарил языку знаменитый слоган «Let it crash!». Который означает: если что-то пошло не так — немедленный отказ. Кодируйте только узкую — как трасса Формулы-1 — дорогу правильного ожидаемого выполнения.
Тогда ошибки, принесенные в обработку вами, — тоже будут обработаны (не все, но многие).
Теперь просто перезапустите выполнение, которое привело к отказу, с теми же входными данными. Там могло произойти что-то непредвиденное, типа лимит соединений превышен, сторонний сервис не ответил, да что угодно. Не время выяснять, просто попробуйте повторить. Да, это как знаменитое «выйти и войти» — оно ведь иногда работает. Если от вашего куска зависело что-то еще — перезапустите и это. Автоматизируйте этот перезапуск, чтобы не приходилось все каждый раз писать руками, или копипастить из предыдущего проекта. Если спустя какое-то количество попыток, оно все равно не сработало — добросовестно выплюньте ошибку в лог и пропустите этот конкретный набор входных данных (не теряя).
Поздравляю, мы только что заново написали неспецифицированную, глючную и медленную реализацию половины дерева супервизоров эрланга. Ребята из кубернетиса шли примерно по этому пути: им свою жалкую копию OTP даже удалось втюхать людям, плавающим исключительно в мейнстриме.
Таким нехитрым способом можно навсегда себя обезопасить от неожиданных отказов: если отказ — это нормальное, ожидаемое поведение вашей программы — он автоматически превращается из противной гусеницы — в прекрасную бабочку. Самое удивительное, что такая экосистема в принципе не слишком сложна в реализации даже на го, но концепции этого языка изначально предполагают насилие над программистом вместо облегчения его труда — поэтому я сомневаюсь, что когда-либо что-то похожее там появится. Особенно, учитывая, что есть кубер и можно не заморачиваться, а просто перезапустить весь мир, охлаждая неуместный пыл кэша, показывая 503
и вообще всячески измываясь над потребителями, но вся индустрия десятилетими работала как раз над тем, чтобы пользователь привык к тому, что его ненавидят, и ничего не работает.
90% всего трафика в интернете — обеспечивается эрлангом — и это вовсе не случайность.
Позволю себе привести еще одну цитату, объясняющую, какую именно задачу решал (и решил) Джо Армстронг.
At the time, Ericsson built large telephone exchanges that had hundreds of thousands of users, and a key requirement in building these was that they should never go down. In other words, they had to be completely fault-tolerant.
Как все гениальные решения в жизни, это тоже оказалось довольно простым. Чтобы упростить его еще больше — надо полностью запретить изменяемость объектов, реализовать легковесные процессы и разрешить им общение исключительно посредством асинхронных сообщений. Тогда все описанное выше — просто достанется вам в виде подарка, деревья супервизоров — практически не нуждаются в какой-то специальной реализации, они получаются сами почти из коробки. Но об этом, наверное, лучше поговорить в следующий раз.