LINUX.ORG.RU

Создание устойчивого серверного ПО

 , ,


2

3

Добрый день, ЛОР! Давно не постил, потому что нечего было сказать. А сейчас, кажется, есть.

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

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

Итак, начнём с идемпотентности - свойства запроса обладать возможностью быть исполненным несколько раз, и иметь эффект как от одного исполнения. Ценность в следующем - допустим, клиент производил запрос, и соединение оборвалось. Был запрос принят или нет - не понятно. Если клиент переводил на другой счёт деньги - может ли он обновить страницу и не бояться, что деньги спишутся дважды? Проще всего достижимо добавлением id запроса в его тело - если запрос уже был исполнен, операция не производится, и возвращается сохранённый результат.

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

Как достигнуть устойчивости? Имеем парк серверов, каждый из них выполняет свою операцию, с возможностью её отменить - их необходимо координировать в условиях риска выхода из строя серверов, ПО и сети. При выходе из строя сервера необходимо, чтобы он возобновил исполнение присвоенных ему операций - нужно сохранять их в реплицированную БД. Кажется, любая простая БД типа Ключ-Значение подойдёт. Когда исполнение возобновится - оно должно продолжить с момента последней транзакции в цепочке - то есть после каждой транзакции в цепочке, промежуточное состояние должно сохраняться в БД - с возможностью возобновления операции другим исполнителем.

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

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

Но какой должен быть лимит по ресурсам для абстрактного запроса? Поскольку если запрос - «Исполнить SQL выражение» (в случае, если наш сервис - база данных) - у него может быть самое разное потребление памяти и процессорного времени. Решением, на мой взгляд, будет разделение непредсказуемого запроса на предсказуемые - и заранее зная их количество и стоимость, можно оценить затраты на изначальный запрос. Для SQL запроса это будет сначала запрос типа PLAN - сам по себе достаточно предсказуемый - а затем проверка наличия ресурсов и исполнение для каждого шага плана. Но если верхнеуровневый запрос, не имеющий достаточно ресурсов, имеет смысл отбросить сразу - то порождённые запросы стоит выстраивать в очередь.

Зная ограничения по ресурсам, необходимо реализовать их соблюдение. В этом нам поможет ядро ОС - оно отслеживает объём памяти, закреплённый за процессом, и процессорное время, им затраченное. В Linux порог можно выставить командой ulimit и системным вызовом setrlimit. Вводить ограничения необходимо именно на уровне процесса, поскольку память по факту принадлежит не потоку и не корутине, а именно процессу (стек у каждого свой, но куча - общая для процесса), и если память кончается - OOM Killer убивает процесс. Ситуация сложнее для cgroup, в которых выставлено убийство всей cgroup сразу (как для подов в k8s), но это отдельная история, и ulimit можно ставить чуть ниже суммарного ограничения для cgroup. Теперь касательно процессорного времени - оно также действует на процесс целиком (может есть лимит на конкретный поток?). Конечно, запуск процесса под каждый запрос - схема плохо масштабируемая (хотя с идеологической точки зрения, кажется, наиболее правильная), поэтому можно использовать пул процессов, поднимающих свои лимиты для каждого запроса. Также можно запускать N корутин в каждом процессе - суммируя их лимиты. Если одна корутина перейдёт порог, убиты будут все N - но это лучше, чем если бы все запросы жили корутинами в одном процессе, и умерли бы все.

Я вполне представляю, как это можно реализовать для Reverse-proxy (nginx, apache), Python и gRPC. Для разных методов gRPC, каждый из которых предсказуем, указываем в Reverse Proxy свой обработчик - пул процессов. Python достаточно низкоуровневый для общения с ядром, чтобы каждый процесс мог менять свои лимиты. И реализация gRPC имеет параметр max_concurrent_rpcs - чтобы предотвратить DDOS (только на L7, или ещё на L3+4 - не знаю. Но кажется в любом случае, AntiDDOS на L3+4 умеет Reverse Proxy). Если вам подход показался разумным - как бы вы реализовывали на своём ЯП?



Последнее исправление: lostghost1 (всего исправлений: 1)

Какой ддос? Ты даже не вдупляешь как tcp/ip работает. Тебе пакет приходит и ты не знаешь от кого он, пока не распарсишь его, не прочитаешь из заголовков айпи-адрес отправителя… Те это говно все равно канал забьет в итоге. Не заманачивайся. Это не проблемы разраба

anonymous
()

Ну и каша.

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

книгами, блогами и выступлениями с конференций

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

neumond
()
Ответ на: комментарий от neumond

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

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

Pwner
()
Ответ на: комментарий от Pwner

Рад, что тоже не пофиг)

Мне не пофиг потому, что в принципе IT интересно - и построение устойчивых систем к этому относится.

Ну и во вторых хочу стать лучшим инженером, даже если не применимо в моменте.

lostghost1
() автор топика

Как-то всё выглядит наивно и по верхушкам.

Итак, начнём с идемпотентности - свойства запроса обладать возможностью быть исполненным несколько раз, и иметь эффект как от одного исполнения. Ценность в следующем - допустим, клиент производил запрос, и соединение оборвалось. Был запрос принят или нет - не понятно. Если клиент переводил на другой счёт деньги - может ли он обновить страницу и не бояться, что деньги спишутся дважды? Проще всего достижимо добавлением id запроса в его тело - если запрос уже был исполнен, операция не производится, и возвращается сохранённый результат.

И куда этот id девать? Что делать с ответом? Тут же миллион вопросов дальше.

Как достигнуть устойчивости? Имеем парк серверов, каждый из них выполняет свою операцию, с возможностью её отменить - их необходимо координировать в условиях риска выхода из строя серверов, ПО и сети. При выходе из строя сервера необходимо, чтобы он возобновил исполнение присвоенных ему операций - нужно сохранять их в реплицированную БД. Кажется, любая простая БД типа Ключ-Значение подойдёт. Когда исполнение возобновится - оно должно продолжить с момента последней транзакции в цепочке - то есть после каждой транзакции в цепочке, промежуточное состояние должно сохраняться в БД - с возможностью возобновления операции другим исполнителем.

Какой ещё парк серверов? Какие ещё операции? Все давно сидят на кубернетесе.

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

vbr ★★★
()
Последнее исправление: vbr (всего исправлений: 1)

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

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

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

с серверами и вычислениями - примерно такое-же, это только менеджеры рассказывают про «100% надёжность, неотвергаемость, достоверность»..на самом деле старинные переповторы, бекапы, двойные рукопожатия и рестарты. Особо критичные участки дублируются (не про пиццу и ваши потребности), но не более того.

MKuznetsov ★★★★★
()
Ответ на: комментарий от Pwner

Такой себе аргумент, одна бабка сказала, где-то слышал, что вот так всё плохо. Как по мне, звучит как раз как нагромождение микросервисов и очередей, которое никто не может нормально отдебажить.

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

Ну и судя по ОП-посту, человек нормально не может сформулировать что он пытается сделать, как будто просто перечислил баззворды. И начал почему-то не с fsync, а с микросервисов и распределённых транзакций, хотя задачи масштабировать ещё не было.

neumond
()
Ответ на: комментарий от vbr

Какой ещё парк серверов? Какие ещё операции? Все давно сидят на кубернетесе.

Видно, что ты ни в зуб ногой.

Как достигнуть устойчивости?

Такое, как ты набрасываешь, ТС, да только слабенько. Кто в теме, те давно порешали все твои высказывания. Как пример достижения консистентности данных в микросервисах, используют костыли в виде доп фреймворков - https://masstransit.io.

Короче, говно тема и ТС говнит. А то, что на ЛОРе 90% мнение имеют МЫ ЗНАЕМ и без этой темы переписи несмышлёнышей.

anonymous
()

Как достигнуть устойчивости?

Набрать морально устойчивых погромистов с физиологически правильным расположением рук. Погромисты с физиологически неправильным расположением рук неустойчивы и проецируют свою неустойчивость на создаваемый ими код. Ваш К.О.;-)

AntonI ★★★★
()
Последнее исправление: AntonI (всего исправлений: 1)
Ответ на: комментарий от MKuznetsov

Да и если уже взяли деньги можно сделать возврат.

для операции

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

в реальном мире ни на одном из шагов здесь нельзя гарантировтаь атомарность.

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

Большинство проблем решаются административными методами.

Оп, на то ты и инженер, чтобы в каждом конкретном случае находить оптимальное решение из всех возможных.

shimshimshim
()
Последнее исправление: shimshimshim (всего исправлений: 1)
Ответ на: комментарий от neumond

Такой себе аргумент, одна бабка сказала, где-то слышал, что вот так всё плохо.

В смысле, какая бабка сказала? Сам лично деньги через банкомат пытался положить: он их несколько раз просил перевернуть другой стороной, в результате закрыл «забрало» с бабками, выдав «сессия истекла» и вернувшись на главный экран. В банке сотрудники многократно пересматривали видео (а не логи), прежде чем принять заявление, после чего заключили, что если (!) при инкассации найдутся лишние, их вернут. О методах разработки у них сужу по типу багов, требованиях в вакансиях, заработным платам и лору бывших сотрудников. Мой вывод - читать нужно больше заранее, а не решать задачи по мере поступления.

Pwner
()
Ответ на: комментарий от Pwner

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

Банкоматами занимаются далеко не всегда сами банки и уж тем более не отделы, где набирают кого угодно, лишь бы быстрее. Я бы вывод сделал что тестировать нужно нормально, в моей схеме ещё требуется 99,9% беспроблемных продаж. Но если нравится фиксить и оптимизировать заранее всё, что никогда бы и не случилось — то пожалуйста, каждый распоряжается своей жизнью и деньгами как ему нравится.

neumond
()

это был 1 вопрос? надо формулировать короче.

чтоб не было OOM Kill надо делать стресстестирование вот и все. и настраивать под стабильную работу.

jura12
()
Ответ на: комментарий от neumond

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

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

Pwner
()
Ответ на: комментарий от Pwner

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

neumond
()

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

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

Как достигнуть устойчивости?

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

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

MOPKOBKA ★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 2)