- PVSM.RU - https://www.pvsm.ru -

© Dragon Ball. Goku.
Программист-защитник в любой момент и в любом месте кода ожидает появления потенциальных проблем и пишет код таким образом, чтобы заранее от них защититься. А если от проблемы нельзя защититься, то хотя бы сделать так, чтобы её последствия и влияние на пользователей были минимальными.
Вспоминается эффект FlashForward из голливудских блокбастеров, когда главный герой видит грядущую катастрофу и остаётся предельно спокойным, потому что заранее знает, что она произойдёт, и имеет от неё защиту. Идея защитного программирования в том, чтобы защититься от проблем, которые сложно или вовсе невозможно предвидеть. Программист-защитник ожидает появления ошибок в любом месте системы и в любой момент времени, чтобы предотвратить их до того, как они нанесут ущерб. При этом цель не в том, чтобы создать систему, которая никогда не падает, это всё равно невозможно. Цель в том, чтобы создать систему, которая падает изящно в случае любой непредвиденной проблемы.
Давайте разберёмся подробнее, что входит в понятие «падать изящно».
Но у вас может появиться вопрос.
Зачем тратить время на проблемы, которые могут возникнуть в будущем? Сейчас же их нет, код работает просто идеально. К тому же проблемы могут и вовсе никогда не произойти. Ведь профессионалы не занимаются инженерией ради инженерии (YAGNI [1] — You aren't gonna need it)!
Эндрю Хант в книге «Программист-прагматик» даёт следующее определение защитному программированию — «прагматическая паранойя».
Защищайте свой код от:
Давайте обсудим несколько тактических и стратегических приёмов защитного программирования, следование которым позволит создать надёжную и предсказуемую систему, устойчивую к произвольным сбоям.
Некоторые советы могут показаться «капитанскими», но на практике многие разработчики не следуют даже им. А ведь если придерживаться простых практик и подходов, это значительно повысит стабильность вашей системы.
Данные пользователей по умолчанию ненадёжны. Пользователи часто неверно понимают то, что нам (как разработчикам системы) кажется очевидным. Ожидайте на входе некорректные данные и всегда проверяйте их.
Также проверяйте объём входных данных. Может быть такое, что пользователь отправляет их слишком много. При этом, с точки зрения бизнес-логики, это корректный сценарий. Но он может привести к слишком долгой их обработке. Что с этим можно сделать? Например, запустить её асинхронно, в случае если объём входных данных превышает определённый порог и специфика бизнеса позволяет обработать данные в фоновом режиме.
Настройки программ (например, конфигурационные файлы) также подвержены появлению в них некорректных данных. Часто настройки программ хранятся в JSON, YAML, XML, INI и других форматах. Поскольку всё это текстовые файлы, стоит ожидать, что рано или поздно кто-то что-то в них поменяет, и ваша программа станет работать некорректно. Это может быть как конечный пользователь, так и кто-то из вашей команды.
Базы данных, файлы, централизованные хранилища конфигов, реестр — ко всем этим местам может быть доступ у других людей, и рано или поздно они что-то там поменяют (Murphy's law [2]).
Входные данные, которые прошли валидацию и начинают обрабатываться, должны быть чистыми, если вы хотите, чтобы ваш код делал именно то, что вы от него ожидаете.
Однако, хороший тон — делать дополнительные проверки корректности данных, в том числе когда они уже начали обрабатываться. В критических местах (биллинг, авторизация, персональные и конфиденциальные данные и т.д.) это практически обязательное требование. Это необходимо, чтобы в случае появления багов в коде или проблем с валидатором входных данных остановить поток исполнения как можно быстрее. Сложно сделать качественную валидацию с проверкой всех возможных сценариев ошибок, поэтому можно использовать более простые способы валидации того, что программа всё ещё исполняется корректно — assertions и exceptions.
Здоровая параноидальность — это характерная черта всех профессиональных разработчиков. Но очень важно искать оптимальный баланс и понимать, когда решение уже достаточно хорошее.
Частая причина появления проблем — недостаточное разделение конфигов по окружениям или вовсе отсутствие такого разделения.
Это может привести к множеству проблем, например:
Это лишь примеры, полный список проблем, к которым может привести недостаточно ответственное разделение конфигов, практически бесконечен и зависит от специфики проекта.
Ответственное разделение конфигурационных данных по окружениям позволяет значительно уменьшить вероятность сразу целого класса проблем, связанных с:
Помимо этого, хорошей практикой является хранение секретных данных (ключей, токенов, паролей) в отдельном месте, специально предназначенном для хранения и обработки секретов. Такие системы надёжно шифруют данные, имеют гибкие средства для управления правами доступа, а также позволяют быстро сменить ключи, если они были скомпрометированы. При этом не потребуется вносить изменения в код и вновь разворачивать приложение. Это особенно важно для систем, которые работают с финансовыми транзакциями, конфиденциальными или персональными данными.
Распространённая причина падения больших и сложных систем — каскадный эффект. Происходит поломка или деградация функциональности одной из частей системы, и одна за другой начинают отказывать другие подсистемы, связанные с ней. Каскадно, пока вся система не станет полностью недоступна.
Несколько защитных трюков:
Все системы сбоят. В них иногда происходит нечто странное, что создатели ожидают «раз в 10 лет». Интеграции и внешние API периодически становятся недоступны или отвечают некорректно. Сделать fallback для всех таких случаев зачастую сложно, долго или просто невозможно. Предусмотрите заранее такую ситуацию и сообщайте о ней как можно быстрее. Запись в лог с уровнем ERROR или в систему мониторинга — как само собой разумеющееся. Добавить дополнительную проверку в healthcheck — ещё лучше. Отправить из кода сообщение в Slack, Telegram, PagerDuty или другой сервис, который мгновенно оповестит вашу команду о проблеме, — идеально.
Но важно чётко понимать, когда есть смысл отправлять сообщения напрямую. Только в случае, если возникшая ошибка, подозрительная или нетипичная ситуация связана с бизнес-процессами и важно, чтобы конкретный человек или группа людей в команде как можно быстрее получили нотификацию и могли отреагировать.
Все остальные технические проблемы и отклонения должны обрабатываться стандартными средствами — monitoring, alerting, logging.
У программ и людей есть одна схожая черта — они склонны переиспользовать те данные, которые часто используются или недавно встречались. В высоконагруженных системах всегда следует об этом помнить и кешировать данные в самых горячих местах системы.
Стратегия кеширования сильно зависит от специфики проекта и данных. Если данные мутабельные, появляется необходимость инвалидации кешей. Поэтому заранее обдумайте, как вы будете делать это. А также подумайте о том, какие риски могут быть в случае появления в кеше устаревших данных, выхода кеша из строя и т.д.
Работа со строками — одна из самых частых операций в любой программе. И если делать это не оптимально, это может быть дорогая операция. В разных языках программирования специфика работы со строками может различаться, но нужно всегда помнить о ней.
В крупных приложениях с большой кодовой базой часто встречается код, написанный много лет назад, который работает без ошибок, но не оптимален с точки зрения производительности. Зачастую банальное изменение структуры данных с массива/списка на хеш-таблицу даёт серьёзный буст (пусть лишь в локальном месте кода).
Иногда можно улучшить производительность, переписав алгоритм на использование побитовых операций. Но даже в тех редких случаях, когда это оправдано, код получается весьма сложным. Поэтому при принятии решения учитывайте читабельность кода и то, что его нужно будет поддерживать. То же самое касается и других хитрых оптимизаций: почти всегда такой код становится трудным для чтения и очень сложным для поддержки. Если вы всё же решились на хитрые оптимизации, не забывайте писать комментарии с описанием того, что вы хотите, чтобы этот код делал, и почему он написан именно так.
При этом к оптимизации стоит относиться со здоровым прагматизмом:
Преждевременная оптимизация — корень всех зол (Дональд Кнут)
Это крайняя мера. Языки низкого уровня почти всегда быстрее по сравнению с языками более высокого уровня. Но у этого решения есть цена — разрабатывать такую программу дольше и сложнее. Иногда, переписав критические части системы на языке низкого уровня, можно добиться серьёзного увеличения производительности. Но есть и побочные эффекты — обычно такие решения теряют в кросс-платформенности и их поддержка стоит дороже. Поэтому принимайте решение взвешено.
В заключение хочется отметить ещё одну важную вещь, возможно, самую главную. Меры, которые мы рассмотрели в предыдущих пунктах, будут работать только в том случае, если все участники команды придерживаются их и у каждого есть понимание, кто за что отвечает и что нужно делать в случае критической ситуации. Важно после исправления проблемы провести встречу (Post Mortem) со всеми заинтересованными людьми и выяснить, почему эта проблема возникла и что можно сделать, чтобы этой же проблемы не было в будущем. Во многих случаях требуются как технические изменения, так и изменения в процессах. С каждым новым Post Mortem’ом ваша система будет становится надёжнее, команда опытнее и сплочённее, а энтропии во вселенной чуть меньше ;)
В статье частично использованы материалы из Why Defensive Programming is the Best Way for Robust Coding [3] (Ravi Shankar Rajan).
Автор: Илья
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/334703
Ссылки в тексте:
[1] YAGNI: https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it
[2] Murphy's law: https://en.wikipedia.org/wiki/Murphy%27s_law
[3] Why Defensive Programming is the Best Way for Robust Coding: https://medium.com/swlh/why-defensive-programming-is-the-best-way-for-robust-coding-cfa790fe04cd
[4] Источник: https://habr.com/ru/post/472922/?utm_source=habrahabr&utm_medium=rss&utm_campaign=472922
Нажмите здесь для печати.