LINUX.ORG.RU

Сообщения lostghost1

 

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

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

Имеем проблему создания типичного транзакционного сервиса - банковское приложение, интернет-магазин, аренда чего-либо, т.д. Как сделать его устойчивым как с точки зрения корректности - запросы не теряются, дважды не повторяются, если не могут примениться - откатываются с отображением ошибки пользователю - так и с точки зрения 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
()

Написание собственного высокоуровневого ЯП

Привет, ЛОР. В очередной раз в голову приходят идеи по созданию своего ЯП (но я не открываю), однако в этот раз, кажется, я придумал что-то более-менее стоящее, чем и хочу поделиться.

Собственно, наткнулся я на функционально-распределенные Erlang и Elixir, и это оказались реально крутые языки. Однако мне кажется, что я смогу лучше - а если не получится лучше, то только прибавится уважение к тому, что есть, то есть везде одни плюсы.

Собственно, как с наименьшими потерями написать высокоуровневый язык? Я посмотрел на LLVM, QBE, FASM, GNU Assembler и другие крутые, продуманные вещи. И понял, что всё не то. После долгого поиска я задался правильным (на мой взгляд) вопросом - а на каком уровне абстракции ПО происходит вся высокоуровневая магия? И понял - уровень абстракции, интересующий меня - это уровень системных вызовов. Структура исполняемого файла, кодировка машинных инструкций, экспорт символов и релокации - это всё мне не интересно, интересна сама манипуляция данными, без раскуривания мануалов по AMD64 ISA. А с помощью какого инструмента проще, естественней всего взаимодействовать с системными вызовами? Да с помощью того, на котором и под который они были написаны - язык Си.

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

Вопрос - какие подводные камни? Насколько будут Си компиляторы терпеть мою LibC, функций стандартов Си не реализующую? И как бы вы подошли/уже подходили к задаче написания высокоуровневого ЯП?

 , ,

lostghost1
()

Свой маня-дистрибутив

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

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

Итак, кто предпринимал создание собственного дистрибутива - как оно было/есть? Какие цели ставили? Насколько цели (были) достигнуты? Как велась разработка, как собирали feedback, кто кроме вас дистрибутивом пользовался? Что делать надо/не надо? Что бы посоветовали себе, начинавшему данный проект?

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

 , ,

lostghost1
()

RSS подписка на новые темы