Бессерверные вычисления применяются сейчас в различных сферах — от создания чат-ботов и приложений Интернета вещей до самостоятельных API доступа к сервисам по протоколу HTTP. Платформы для развертывания бессерверных вычислений имеются как у большинства поставщиков облачных решений (Yandex Cloud Functions (cloud.yandex.ru/docs/functions), Amazon Web Services Lambda, Google Functions), так и у сообщества Open Source.

Переход от «железных» серверов к виртуализации и контейнеризации в свое время привел к появлению культуры DevOps, рождению подходов Infrastructure as Code, предоставив возможность больше времени уделять бизнес-задачам, а не обслуживанию инфраструктуры. Кроме того, был снижен достаточно высокий «налог» на инфраструктуру, который невольно приходилось платить пользователям.

Переход к эпохе бессерверных вычислений (serverless computing) — следующий шаг в направлении дальнейшего снижения налога на инфраструктуру, исключения необходимости ее обслуживания, планирования и оптимизации использования ресурсов, решения задач масштабируемости, а также заботы о системном ПО и среде выполнения. У разработчиков появилась возможность сконцентрироваться только на бизнес-логике. При необходимости обрабатывать больше запросов сервис сам предоставляет возможности по масштабированию. Если, например, приходит рекламный трафик или одновременно происходит множество внешних событий, то облако начинает автоматически увеличивать количество одновременно запускаемых копий функций, выстраивать запросы к ним и обрабатывать ответы в соответствии с настроенными квотами, разворачивая «мини-контейнеры» с функциями. Более того, появилась возможность строить и разворачивать глобальные сервисы, выбирая в какую зону доступности и регион развернуть код для уменьшения задержки между пользователем и приложением. Как следствие, при работе в такой бессерверной среде возникает необходимость в базе данных, способной самостоятельно эластично подстраиваться под нагрузку.

Однако оказалось, что для экосистемы бессерверных вычислений почти нет баз данных, особенно реляционных. Такие системы хранения данных, как S3, не заменяют базу данных, а использование традиционных СУБД несовместимо с бессерверной парадигмой. Большинство решений базируются на принципе take or pay: сначала оплачиваются и выделяются ресурсы (обычно впрок), а потом оказывается, что эти ресурсы так и не использовались. Что делать, если нагрузка непредсказуема? Как автоматически менять необходимые ресурсы базы данных вместе с изменением нагрузки и при этом не переплачивать за них?

История

По мере расширения Интернета возникла необходимость в распределенных базах данных, решающих задачу масштабирования нагрузки и объемов хранимых данных. По данным аналитиков, в 2020 году во всем мире было почти 5 млрд пользователей Сети, из которых 60% постоянно находятся в режиме онлайн, причем объем данных, созданных в течение следующих трех лет, будет больше, чем весь объем данных, созданных за последние 30 лет. Растет число датчиков Интернета вещей, что означает лавинообразный рост собираемых ими данных, которые вскоре превзойдут по объемам все другие типы данных. Все это стимулирует создание и развитие технологий распределенных баз данных.

Большинство классических СУБД исторически создавались как нераспределенные системы: методы обеспечения отказоустойчивости и масштабирования у таких баз данных были реализованы поверх существующей нераспределенной системы. Стандартные технологии (такие как репликация) на традиционных СУБД хорошо работают, но так как они появились позже, то с точки зрения нагрузки на запись стандартные базы данных масштабируются весьма ограниченно либо совсем не масштабируются.

В 2008 году для хранения данных поисковой базы веб-робота «Яндекс» использовалось более сотни серверов: данные индексировались, выкладывались в базу поиска, по которой отрабатывались поисковые запросы. Для того чтобы система была отказоустойчивой и процессы не падали при отказах оборудования, необходимо было делать репликацию. Большинство задач выполнялось локально на конкретной машине, но когда требовалось подсчитать рейтинг страниц, для обмена данными нужно было использовать все серверы. По мере роста объемов данных в Сети росла и поисковая база, приходилось постоянно увеличивать ресурсы, а для обеспечения надежности базы — периодически перераспределять ее по множеству узлов (пересегментировать), причем подготовка к этой операции могла занимать несколько месяцев. Чтобы увеличить парк серверов, требовалось перенести часть данных со старых серверов на новые, а также переместить данные с одних старых серверов на другие. В результате возникала новая схема данных, отличная от старой, что вынуждало обновлять программы, работающие с данными, вплоть до их полного переписывания.

В 2011 году была создана новая система, получившая название KiWi и призванная решить проблему роста объемов данных, обеспечения отказоустойчивости и балансировки нагрузки. Основа KiWi — это архитектура СУБД «ключ-значение» и решения, работающие одновременно в нескольких ЦОДах. Для распределения данных по узлам кластера использовалось консистентное хеширование. Репликация и обработка данных в KiWi выполнялись во время фонового процесса слияния изменений в основной базе. Процесс был достаточно эффективным и существенно экономил вычислительные ресурсы. Удаление данных происходило в соответствии с принятыми политиками: давно не обновлявшиеся данные удаляли в зависимости от принятого TTL (Time To Live), хранили последние несколько версий объекта. Когда систему запустили в эксплуатацию, кластер составлял более 600 серверов. Для сравнения, самая крупная публичная инсталляция на сайте Cassandra на тот момент была вдвое меньше.

Однако при активном использовании системы начали проявляться аномалии, связанные с согласованностью в конечном счете (eventual consistency). Например, в задачах анализа данных требовался согласованный срез, что в KiWi провести было сложно: пока читается срез данных с одного набора реплик, другие реплики тех же данных могли уже измениться и возникала вероятность чтения устаревших данных другой версии. Даже две программы, запущенные после завершения изменения данных, могли прочесть разные состояния — согласованность обязательно («в конечном счете») наступает, однако ждать можно неопределенно долго.

Все эти проблемы ложились на плечи разработчиков, которые пользовались KiWi, — в прикладном коде, работавшем с базой, приходилось предусматривать все возможные варианты раскладки данных. Сложность инфраструктуры перешла на сторону клиентского приложения. Одновременно с этим взаимодействие с базой носило императивный характер: разработчикам приходилось осваивать новый для них язык и учиться правильно формулировать запросы. Но они хотели писать не программы, управляющие базой данных, а свои приложения и использовать уже готовые и привычные им инструменты.

 

Вопросы терминологии

Согласованность данных (data consistency) — согласованность (консистентность) данных друг с другом, целостность данных, а также их внутренняя непротиворечивость.

Согласованность в конечном счете (eventual consistency) — одна из моделей согласованности, используемая в распределенных системах для достижения высокой доступности, в рамках которой гарантируется, что, при отсутствии изменений данных, через какой-то промежуток времени после их последнего обновления («в конечном счете») все запросы будут возвращать именно последнее обновленное значение.

Персистентные структуры данных (persistent data structure) — структуры данных, которые при внесении в них каких-то изменений сохраняют все свои предыдущие состояния и доступ к ним.

Пересегментация (перешардирование, resharding) — создание и удаление сегментов данных (шардов, shard) и в общем случае перемещение части данных из старых сегментов в новые.

Фиксация (коммит, сommit) — операция отправки последних изменений, сделанных от начала транзакции и ранее невидимых для других транзакций, которая фиксируется в базе данных.

Упорядочиваемость (serializability) — самый высокий уровень изолированности, при котором транзакции полностью изолируются друг от друга и каждая выполняется так, как будто параллельных транзакций не существует.

 

Yandex Database

Требовалась новая платформа, способная решать задачи хранения временных рядов, организации хранимых очередей; предоставить хорошо масштабируемую реляционную базу данных и поддерживать хранение «ключ-значение». Основные требования к такой платформе:

  • отказоустойчивость — сохранение работоспособности при выходе из строя одного из трех ЦОДов;
  • поддержка упорядочивания (serializable) уровня транзакционной изолированности;
  • ACID-транзакции между любыми ключами, строками, таблицами;
  • удобный язык запросов;
  • возможность масштабирования до объемов в сотни петабайт и по нагрузке до десятков миллионов запросов в секунду;
  • мультиарендность;
  • интерактивность — время обработки действия должно составлять не более десятков миллисекунд.

Одно из решений — воспользоваться готовыми продуктами с открытым исходным кодом и с широким набором функциональных возможностей. Этот подход мог бы стать успешным, если бы не было проблемы пересегментации: ни одно из существующих решений не было жизнеспособным, когда количество сегментов составляло сотни и более. Требовалось вручную управлять разбивкой данных по сегментам и балансировкой данных, причем в одних узлах данных оказывалось больше, в других — меньше. Например, если в качестве ключа сегментирования выступает идентификатор пользователя, то вся информация, относящаяся к пользователю, хорошо сегментируется и помещается в одну базу, но если надо построить вторичный индекс по свойству этого пользователя, то первоначальный вариант сегментирования базы становится некорректным. Для разработчика приложения это означает необходимость самостоятельно реализовывать механизм распределенных транзакций с учетом проблем быстродействия и несогласованности. Либо ему надо создать аналитическую систему в архитектуре MapReduce, которая будет асинхронно строить этот индекс и синхронизировать его с базами. Кроме того, одновременно с обработкой онлайн-запросов нужно запускать процесс пересегментирования данных, однако для этого надо уметь назначать онлайн-запросам более высокий приоритет, чем процессам перешардирования.

В итоге было принято решение о создании собственной платформы Yandex Database (YDB, cloud.yandex.ru/docs/ydb), решающей перечисленные задачи.

Архитектура YDB

YDB состоит из большого количества маленьких объектов: нет локальных узлов, отвечающих за терабайты данных, которые хранятся локально, но есть «таблетка», которая предназначена для хранения в среднем одного гигабайта данных. Вся система состоит из таких таблеток и теоретически может расти бесконечно. Изначально таблетка маленькая и управляет единицами гигабайтов данных. Внутри системы — множество таблеток, все вместе они потребляют достаточно мало ресурсов, и их легко перемещать между узлами кластера.

Кроме таких очевидных преимуществ, как масштабируемость, адаптируемость и возможность почти мгновенно на несколько порядков наращивать ресурсы, данная архитектура позволяет минимизировать стоимость сервиса, когда нагрузки незначительны. Здесь уместна аналогия с виртуализацией. Для запуска любой системы нужно купить сервер с сотнями ядер и запустить виртуальную машину на 2, 4, 6, 8 и так далее ядер, в том числе предоставляя возможность использовать лишь долю тактов этих ядер. Это удобно и существенно снижает стоимость владения. Фактически база данных становится виртуальным ресурсом — можно сказать, что запущен «мини-контейнер» с базой данных, который находится в облаке и может перемещаться между узлами кластера. Такие микросущности выгодны с точки зрения оплаты, которая производится только за совершенные запросы, а используются лишь ресурсы, необходимые для обработки запросов.

Современная СУБД — сложный программный комплекс, отдельные части которого действуют независимо, взаимодействуют асинхронно, эффективно и прогнозируемо используют доступные ресурсы и физического сервера, и кластера из тысяч узлов. При разработке кода Yandex Database применяются концепция обмена сообщениями и модель акторов. Акторы — это однопоточные конечные автоматы, которые могут обмениваться между собой сообщениями и «живут» на различных серверах кластера. YDB — это распределенная актор-система, которая знает местоположение каждого актора, может быстро «убить» его на одном узле и поднять на другом, например, для балансировки нагрузки или в случае сбоев.

Рис. 1. Технологический стек YDB

Yandex Database внутри имеет стековую архитектуру (рис. 1) и состоит из нескольких слоев.

Рис. 2. Взаимодействие таблетки и распределенного хранилища

Надежность хранения состояния таблетки обеспечивает распределенная система хранения (Distributed storage), в которой хранятся все данные таблеток (рис. 2), включая журналы (логи) и снимки (snapshots). По своей природе Distributed storage — это специальное хранилище «блобов» (Binary Large Object — двоичный большой объект), гарантирующее надежную запись, которая успешно завершается, только когда блоб полностью реплицирован и во всех репликах записан на все необходимые диски.

Network Interconnect — слой, который обеспечивает взаимодействие всех других слоев между собой. Это собственная разработка Yandex Database поверх TCP. Интерконнект позволяет передавать сообщения между акторами, которые находятся в разных процессах операционной системы или на разных узлах.

При проектировании распределенных баз данных часто отказываются от поддержки ACID-транзакций. В ряде систем, например в Cassandra, полностью отсутствует ACID, в некоторых ACID транзакционность поддерживается только на уровне изменений одной строки или в пределах одного сегмента. В других СУБД (например, в VoltDB) гарантируется ACID, но ограничивается параллельное выполнение транзакций, затрагивающих несколько шардов, что существенно упрощает задачу построения линейно масштабируемой системы хранения данных для приложений, которые могут логически делиться на разделы (секционироваться). Однако для всех остальных случаев контроль за атомарностью, согласованностью и изолированностью целиком ложится на разработчика, что усложняет код, замедляет разработку и снижает производительность транзакций на стороне клиентского приложения.

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

Известно несколько способов выполнения распределенных транзакций. Во многих распределенных системах традиционно используется хорошо проверенный и зарекомендовавший себя алгоритм двухфазной фиксации (2PC), однако у него есть недостатки, связанные с ограничениями пропускной способности и высокой вероятностью отката при конфликте. В Yandex Database реализован механизм планируемых транзакций (deterministic transactions), которые обеспечивают транзакционную изоляцию уровня упорядочиваемости (serializable) и основываются на подходе Calvin: когда множество участников должны договориться, как обрабатывать ту или иную транзакцию, то они делают это до того, как поставят блокировки и начнут выполнять транзакцию. Когда транзакция запланирована, она выполняется в соответствии с планом. Так как все узлы договариваются о том, какие транзакции и в каком порядке будут выполняться, то можно отказаться от протоколов распределенной фиксации, уменьшить пересечение конкурирующих транзакций и увеличить пропускную способность системы.

Пользователям Yandex Database доступен язык YQL (Yandex Query Language) — строго типизированный диалект SQL с поддержкой сложных типов: list, tuple, struct, dict. Обычно разработчики приложений для реляционных баз данных ожидают от среды наличия именно диалекта SQL.

Query Processor — это слой обработки YQL-запросов. Компилятор запросов анализирует пришедший в систему запрос на языке YQL и создает на его основе направленный ациклический граф (DAG) с информацией о типах. Оптимизатор запроса переписывает этот граф, основываясь на структуре выражений и свойствах данных. Так создается физический план выполнения запроса.

И наконец — самый верхний слой gRPC proxy (рис. 1). Взаимодействие с базой данных происходит по протоколу gRPC (удаленный вызов процедур с открытым исходным кодом, первоначально предложенный Google). YDB API публично открыт, что позволяет разработать собственные SDK для Yandex Database.

Бессерверный режим в YDB

В классических распределенных системах всегда есть сегменты с данными, и если все данные не умещаются на один узел, для масштабируемости добавляется второй узел, третий и т. д. Но в Yandex Database сегмент очень маленький и таблица всегда разбивается на маленькие сегменты в среднем по 1–2 Гбайт. В результате, даже в варианте с выделенными ресурсами, когда для базы данных выделяются виртуальные машины, удобно размещать и управлять балансировкой именно таких маленьких сущностей — таблеток. Они могут перемещаться между узлами, активироваться за доли секунды — обработка отказа происходит быстро. Если создать таблицу в 100 Мбайт, то она поместится в одну таблетку, которую будут обслуживать несколько акторов. Если потребуется записать на порядок больше данных, то все эти сегменты автоматически поделятся, как клетки. Аналогично, если сегмент длительное время будет обрабатывать достаточно большую вычислительную нагрузку, то он также поделится на несколько сегментов. Так достигается фактически бесконечная масштабируемость.

Данная архитектура позволила реализовать в Yandex Database режим бессерверных вычислений (cloud.yandex.ru/docs/ydb/concepts/serverless_and_dedicated), что дает возможность применять эту базу не только в крупных сервисах компании «Яндекс», но и для решения менее масштабных задач, делая технологию доступной множеству пользователей, решающих различные бизнес-задачи.

В бессерверном режиме все запросы пользователей пересчитываются в условные единицы (request unit) — это фактически стоимость чтения по ключу нескольких килобайтов данных. Если запрос более сложный, чем просто чтение по ключу, то его стоимость пропорционально увеличивается. Для знакомства с YDB предусмотрен бесплатный режим Free Tier: пользователю предоставляется выполнение миллиона request units в месяц, чего вполне достаточно для многих сервисов. При этом пользователь, помимо бесконечной масштабируемости, получает полностью управляемую базу данных.

Благодаря бессерверным вычислениям менеджер проекта может точно посчитать стоимость услуги. Выполнение бизнес-процесса состоит из запуска определенного количества функций, обработки файлов в объектной системе хранения (Object Storage), использования очереди и обращения к базе данных. Стоимость каждого из этапов процесса, в том числе запроса к базе, можно оценить и получить стоимость всего бизнес-процесса для ее последующего учета в плате за обслуживания клиента.

 

YDB на практике

Для водителей автомобилей, пешеходов и велосипедистов сервис Auto.ru планировал социальный проект «Большой экзамен ПДД». Цель — мотивировать участников дорожного движения изучить действующие правила дорожного движения, что могло бы повысить безопасность на дорогах.

Проект предлагалось сделать в виде отдельного от основного сайта динамического сервиса. Прогнозировались пики нагрузки, обусловленные маркетинговыми активностями. Для обеспечения автоматического масштабирования выбрали бессерверный стек технологий от Yandex.Cloud — реализацию на Yandex Cloud Functions для запуска кода в безопасном, отказоустойчивом и автоматически масштабируемом окружении без создания и обслуживания виртуальных машин. При увеличении количества участников «сдачи» экзамена на знание ПДД, автоматически создаются дополнительные экземпляры функции, выполняемые параллельно. В качестве базы данных использовалась Yandex Database в режиме бессерверных вычислений. Разработчики занимались только кодом, не заботясь о масштабировании вычислений и ресурсов хранения. Приложение выдержало пиковые нагрузки и позволило провести тестирование более 100 тыс. участников.

Решения Yandex Cloud Functions и Yandex Database Serverless помогают в ситуациях, когда трафик приложения или нагрузка на его компоненты могут вырасти на порядки, усложняя решение задачи масштабирования традиционными методами. После завершения проекта командам Auto.ru не пришлось предпринимать каких-либо действий по его заморозке, так как плата берется только за реально обработанные вызовы.

Сегодня, кроме сервиса Auto.ru, на мультиарендных кластерах Yandex Database работают «Алиса», «Яндекс.Услуги», «Яндекс.Репетитор», «Яндекс.Коллекции», «Яндекс.Турбо», «Яндекс.Толока», «Яндекс.Директ», «Яндекс.Дзен», «Яндекс.Погода» и др.

 

YDB с точки зрения пользователя

С точки зрения пользователя, YDB представляет древовидную файловую систему: каталоги в нелистовых узлах и объекты различного типа с данными в листовых узлах. Ключевой компонент базы — таблица (рис. 3), в которой хранятся все данные, набор строк, удовлетворяющих схеме данных. Строка — множество значений колонок определенных типов. Некоторые из колонок должны составлять первичный ключ (primary key), по которому отсортирована таблица. Таблицы сегментируются горизонтально, благодаря чему можно хранить таблицы произвольного размера.

Рис. 3. Таблица YDB

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

Для работы с реляционной моделью данных используется YQL API, позволяющий применять YDB как полноценную OLTP-СУБД с транзакциями, SQL-запросами и соединениями (join). Для работы с моделью «ключ-значение» в YDB реализован слой совместимости (Document API) с Amazon DynamoDB API — одной из самых популярных в мире бессерверных СУБД, что позволяет использовать AWS SDK и AWS CLI.

Квазиструктурированные данные обычно трудно отразить в строгой схеме, поэтому для работы с ними все чаще применяются документоориентированные СУБД, предназначенные для хранения и поиска данных, оформленных в виде документов в JSON-подобном формате. В Yandex Database реализована поддержка JsonAPI, а также специальный тип для хранения JSON — JsonDocument, позволяющий обходить документную модель с использованием JsonPath без необходимости разбора всего содержимого. Это дает возможность эффективно выполнять операции из JsonAPI, уменьшая задержки и стоимость пользовательских запросов.

***

Современная распределенная СУБД должна уметь поддерживать различные типы нагрузки, удовлетворяя запросы совершенно разных пользователей. На кластерах Yandex Database сейчас хранятся петабайты данных и выполняются миллионы запросов в секунду. На основе этой СУБД, предоставляющей режим бессерверных вычислений, основаны проекты с различными типами нагрузки: ключ-значение; традиционные веб-приложения, использующие SQL; запись логов; документоориентированные базы данных с поддержкой JSON API и временные ряды.

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

Андрей Фомичев (fomichev@yandex-team.ru) — руководитель отдела разработки систем хранения и обработки данных, Олег Бондарь (olegbondar@yandex-team.ru)  —  технический менеджер проектов, «Яндекс» (Москва).

DOI: 10.51793/OS.2021.86.20.003