Под капотом, вне горячего пути: как D1 масштабируется до миллионов баз данных

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

Эта модель особенно хорошо подходит для D1. D1 — это управляемая serverless-база данных Cloudflare на базе SQLite. Она позволяет создавать миллионы баз данных и платить только за то, что используется.

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

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

Этот материал — закулисный подробный разбор того, как работает контрольная плоскость D1. Множество архитектурных паттернов, которые мы опишем, могут использоваться любыми разработчиками, строящими решения на платформе Cloudflare Developer Platform. Пост также даёт представление о том, как мы строим собственные контрольные плоскости.

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

Вот как это развивалось.

По состоянию на 8 октября 2026 года количество «живых» (неудалённых) баз данных D1 выросло в 15 раз за последний год и в 65 раз за последние два года. Также мы наблюдали рост числа ежедневных активных баз данных в 18 раз за последний год (под активной базой данных понимается база, выполнившая хотя бы один запрос в течение дня).

Установившиеся и успешные платформы уже строят решения на D1, используя те же инструменты, что доступны всем пользователям Cloudflare.

«ChatGPT Sites позволяет каждому превращать идеи в шарируемые экспериенсы. D1 даёт нашим пользователям простой, масштабируемый способ добавить такие возможности, как кастомные метрики, таблицы лидеров и комментарии в их дашборды, игры и инструменты.» — Джон Эбрамс, ChatGPT Sites @ OpenAI

Пока мы вносили описанные здесь изменения, мы периодически прогоняли нагрузочные тесты с целевыми уровнями значительно выше ожидаемого роста. Это помогало определить, на чём сосредоточить оптимизацию.

Приложения, построенные на Cloudflare Developer Platform, варьируются от full-stack приложений до сервисных API, от внутренних одно-пользовательских порталов до масштабных многопользовательских «vibe coding» платформ и всего, что между ними.

При создании контрольной плоскости D1 для поддержки миллионов баз данных нас направляли следующие принципы, которые будут полезны любому, кто строит свои контрольные и дата-плоскости на нашей платформе:

  • Проектируйте все процессы с учётом отказов и идемпотентности. Отказы будут происходить, поэтому система должна по возможности самоисправляться и продолжать прогресс.
  • Избегайте централизованных одно-региональных компонентов на горячем пути операций дата-плоскости (в нашем случае — всех запросов).
  • Используйте Workers KV, глобально распределённое KV-хранилище, для критичной маршрутизационной метадаты, чтобы при промахе кеша лишь редко приходилось опрашивать центральные базы данных.
  • Используйте Hyperdrive для доступа к центральным Postgres-базам данных для эффективного пуллинга соединений и оптимизированной маршрутизации и кеширования, используя реплики для чтения где возможно.
  • Периодически предварительно вычисляйте агрегации в базе данных, чтобы избежать сканирующих запросов во время обработки пользовательских запросов.
  • Используйте эвентуальную согласованность и очереди между компонентами, которым не нужна сильная согласованность, чтобы повысить надёжность и доступность.
  • Нагрузочно тестируйте приложение с реалистичными нагрузками, чтобы обнаружить узкие места и неэффективности, и сначала оптимизируйте их.

Есть и другие внутренние инженерные принципы, но перечисленные достаточно общие и применимы любому, кто строит на Cloudflare.

Дальнейшие разделы углубятся в основные компоненты системы и процессы контрольной плоскости D1, связанные с жизненным циклом базы D1, и покажут, как применять эти принципы на реальной системе.

Начнём с представления основных компонентов D1.

Каждая база D1 поддерживается одним Durable Object на основе SQLite, что делает создание миллионов баз очень простым.

Форма «HTTP API» представляет все наши API-интерфейсы, включая Cloudflare dashboard, Wrangler и Cloudflare CLI. Все API-запросы проходят через пограничный шлюз Cloudflare и обслуживаются D1 Edge API Worker.

Диаграмма показывает HTTP-эндпоинты, позволяющие пользователям выполнять SQL-запросы к их базам как «Query endpoints». Остальные эндпоинты включают обычные операции с базами данных: создание, удаление, листинг, импорт/экспорт и функции time travel — они показаны как «CRUD endpoints».

CRUD-эндпоинты используют Hyperdrive для запросов к Postgres-кластеру в наших центральных дата-центрах («Core»). Есть две конфигурации: одна включает кеширование и нацелена на реплики Postgres для чтения; другая отключает кеш и нацелена на primary Postgres.

Edge API Worker может вызвать D1 Eyeball Worker (в зависимости от типа запроса). Eyeball Worker — наш прокси для всех запросов, которые должны обрабатываться нижележащими D1 Durable Objects (DO). Это позволяет нам поместить всю обработку запросов внутрь Eyeball Worker, включая наблюдаемость, автоматические ретраи, обработку ошибок и другие поведения, специфичные для запросов.

Workers KV выступает в роли кеширующего слоя на краю для метаданных с высокой долей чтений и может читаться всеми D1 Workers.

Форма «Worker Binding» представляет Workers пользователей с привязкой D1 Worker. Запросы от D1 Binding идут напрямую к D1 Eyeball Worker, который, в свою очередь, пересылает запрос в Durable Object, поддерживающий базу, и проксирует ответ обратно в пользовательский Worker.

У нас есть два метода для выполнения запросов пользователями: через HTTP API query endpoints и через Worker, развёрнутый в их аккаунте с D1 Binding.

С точки зрения операций, мы разделяем их на следующие категории:

  • Плоскость данных (data plane): любые запросы, связанные с базой данных; должны быть максимально быстрыми и всегда исполняться на краю.
  • Контрольная плоскость (control plane): любые HTTP API-операции или внутренние потоки, запускаемые в ответ на не-запросные пользовательские действия; они могут зависеть от централизованных одно-региональных компонентов.
  • Фоновая асинхронная плоскость: аналитические и биллинговые рабочие процессы; могут быть евентуально консистентными и запускаться где угодно.

Под капотом гораздо больше, так что перейдём к ключевым потокам.

Быстрое создание базы данных по требованию близко к пользователю

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

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

  • Проверить, может ли аккаунт создавать ещё базы в рамках бесплатного тарифа или его лимитов.
  • Сгенерировать уникальный UUID базы D1, уникальный Durable Object ID (newUniqueId()) и инициализировать соответствующий Durable Object с информацией об аккаунте и базе.
  • Сохранить информацию о только что созданной базе в Postgres (по одной строке на базу, включая метаданные и сопоставление с инициализированным Durable Object ID).
  • Записать маппинг от database ID к Durable Object ID в Workers KV (позже используется в плоскости данных и в query-эндпоинтах).

Рассмотрим реальный поток и то, как мы восстанавливаемся после сбоев на каждом шаге.

Когда приходит запрос на создание базы, обработчик Edge API Worker вызывается в ближайшем узле Cloudflare на краю. Edge API обращается к репликам Postgres через Hyperdrive с включённым кешированием (TTL кеша — несколько секунд), чтобы получить число не-удалённых баз для данного аккаунта.

Edge API Worker пересылает control plane запросы в дата-центры рядом с нашим Postgres-кластером для эндпоинтов, которые делают несколько кругов к Postgres (шаг 2), и «create database» — один из них. Причина в том, что круговая поездка через весь мир может занять сотни миллисекунд, в то время как рядом с Postgres — несколько миллисекунд. Для эндпоинтов, которые делают не более одного запроса к Postgres, такая оптимизация не нужна, поскольку Hyperdrive уже обеспечивает эффективную маршрутизацию через сеть Cloudflare.

Создание D1 базы без опций по умолчанию размещает базу рядом с местом запроса. Вы можете повлиять на размещение, указав подсказку по локации или юрисдикцию. Эти опции идентичны тому, что поддерживают Durable Objects, поэтому при их наличии они просто передаются в инициализацию Durable Object.

При создании по умолчанию мы хотим, чтобы D1 базы создавались рядом с исходным местом запроса, близко к пользователю. Для этого, на шаге 4, мы внутренне пересылаем выполнение запроса в дата-центр, близкий к исходному местоположению запроса, и вызываем Durable Object оттуда. Это гарантирует, что Durable Object будет размещён рядом с пользователем.

Аналогичные техники маршрутизации можно реализовать, используя явные подсказки размещения Workers и в рантайме вызывая нужный Worker через Service Bindings.

Эндпоинт создания — немного нетипичен по размещению, потому что, хотя мы и опрашиваем Postgres как минимум дважды, мы также делаем удалённый вызов в Durable Objects, которые различаются для каждого запроса, поэтому выполнение запроса с любых краёв мира может быть неэффективным. По этой причине мы склонялись к пересылке начального запроса рядом с Postgres.

Далее генерируется UUID базы вместе с новым уникальным Durable Object ID через newUniqueId() и вызывается RPC-функция init(options) у заглушки нового Durable Object. Durable Object сохраняет информацию в своём durable-хранилище, чтобы каждый входящий запрос с этого момента имел метаданные, связанные с базой.

Когда Edge API Worker получает ответ инициализации, он сохраняет метаданные базы в наш центральный primary Postgres через Hyperdrive — одна строка метаданных на базу.

Эти шаги обёрнуты в цикл повторных попыток (ретраев). Когда инициализация DO или запись в Postgres терпит неудачу, мы повторяем попытку с другими ID (это важно, как будет объяснено ниже).

Запись в Postgres является «контрольной точкой создания», где база считается созданной, даже если итоговый ответ не дошёл до пользователя. С этого момента база включается во все операции контрольной плоскости и может быть привязана к Worker для обслуживания запросов.

После этого D1 база готова принимать запросы.

Финальное подтверждение создания через RPC к Durable Object выполняется в waitUntil() и может завершиться неудачей. Этот вызов сигнализирует DO, что база теперь активна, и DO обновляет Workers KV с метаданными базы, чтобы последующие запросы могли сразу читать информацию из кеша Workers KV без необходимости опрашивать Postgres.

Часто спрашивают, почему создание не может выполняться полностью внутри транзакции Postgres для упрощения процесса. Ответ в том, что это не масштабируется: это привело бы к удержанию соединений, пока инициализируется Durable Object, который может находиться на другом конце света.

Хартбиты для самоисправляющихся баз

Каждый D1 Durable Object периодически отправляет heartbeat в контрольную плоскость, чтобы получить текущее состояние базы (активна или удалена) и лимиты аккаунта. Интервал — 30 секунд с пер-базовым джиттером. У нас также есть возможности менять интервал для конкретной базы или аккаунта, либо полностью отключать хартбиты.

Хартбиты могут допускать слегка устаревшие данные, то есть их можно обслуживать через кеши Hyperdrive или реплики чтения Postgres. Этот поток heartbeat позволяет Durable Object самоисправляться и синхронизировать состояние с источником правды (Postgres) относительно создания и удаления баз.

Ещё одно уточнение: причина, по которой мы используем разные UUID базы и Durable Object ID при каждой ретраи в потоке создания (шаг 5), в том, что при повторном использовании UUID с новыми Durable Object ID несколько DO могли бы полагать, что они владеют одной и той же базой, что привело бы к трудно отлаживаемым ситуациям. Периодические хартбиты также служат для очистки сиротских объектов.

Также важно, что мы сначала инициализируем Durable Object, а затем сохраняем маппинг базы в Postgres; иначе запросы могли бы начать приходить к Durable Object до его инициализации и получали бы отказ.

Пример сценария отказа и почему он самокорректируется:

  • Мы выполнили всё до инициализации Durable Object A, затем запись в Postgres упала.
  • Worker повторяет цикл и инициализирует новый DO с другим ID B, и на этот раз успешно записывает в Postgres.
  • Теперь у нас есть два инициализированных Durable Object, но только один из них валиден.
  • Пользователь сможет обращаться только к DO B, потому что только он доступен в Postgres — нашем источнике правды.
  • В течение 30 секунд DO A отправляет следующий heartbeat в контрольную плоскость и получает ответ, что его базы не существует. Это заставит DO A пометить себя как удалённый.

Аналогичный поток происходит для эндпоинта удаления.

Ещё одно самоисправляющее поведение хартбитов — это обеспечение того, чтобы данные в Workers KV в конечном итоге отражали реальность: активна ли база или удалена.

Финальный шаг в потоке создания по обновлению Workers KV «best-effort» и может провалиться без проблем. Это евентуально консистентно, и при следующем хартбите Durable Object убедится, что запись в Workers KV выполнена корректно. Поскольку Workers KV эвентуально консистентен и не предоставляет способа выполнить согласованное чтение, Durable Object выступает точкой сериализации для записей в элементы Workers KV.

Удаление работает зеркально: Durable Object отвечает за запись состояния «deleted» в Workers KV, опять же в режиме best-effort во время реальной операции удаления, и если это проваливается, хартбит исправит ситуацию. Durable Object хранит состояние active/deleted и в своём хранилище, так что он может сразу отвергать или принимать запросы, даже если Workers KV ещё устарел и пропускает запросы.

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

Отсекание горячего пути: держим запросы в стороне от центральной БД

Ключевой принцип для query-эндпоинтов — избегать обращения к Postgres или любому другому централизованному одно-региональному компоненту. Каждый запрос over HTTP исполняется нашим Edge API Worker, близко к исходному местоположению пользователя, а затем идёт прямо к D1 Durable Object через Eyeball Worker.

Edge API Worker отвечает за стандартную аутентификацию HTTP API, авторизацию по ресурсам, rate limiting на уровне аккаунта и базы, валидацию запроса, проверку прав аккаунта и другие API-функции перед выполнением бизнес-логики.

Eyeball Worker, с другой стороны, пропускает эти дополнительные проверки (кроме своего собственного rate limiting), поскольку под капотом D1 Binding — это Service Binding к D1 Eyeball Worker. Каждая D1 Binding в Worker пользователя содержит закодированную информацию, которая авторизует доступ к одной конкретной D1 базе; Eyeball Worker валидирует эту информацию и пропускает запрос.

Поток выполнения запроса через HTTP API выглядит так:

  1. Пользователь отправляет запрос на выполнение SQL через HTTP API.
  2. Edge API Worker сначала пытается загрузить маппинг от D1 UUID к метаданным базы из Workers KV.
  3. В редких случаях элемент может не быть найден в Workers KV (либо потому, что запись провалилась, как описано выше, либо из-за проблемы доступности). Тогда мы падаем обратно на чтение маппинга через Hyperdrive, нацеливаясь на Postgres-реплики. Перед запросом к репликам Hyperdrive сначала проверяет свой кеш в локации вызывающего, затем кеш рядом с кластером Postgres, и только при промахах по кешу запрашивает реплики Postgres для маппинга.
  4. Получив маппинг, Edge API Worker вызывает Eyeball Worker через Worker Service Bindings, передавая релевантные ID.
  5. Eyeball Worker проводит свою валидацию и пересылает запрос на D1 Durable Object.
  6. D1 Durable Object выполняет SQL-запрос и возвращает результат через Eyeball Worker обратно к Edge API Worker, который возвращает его пользователю.

Если вас интересует, почему мы используем newUniqueId() при создании базы, а не адресуем Durable Objects по «имени», чтобы устранить шаги 2 и 3: причина в том, чтобы избежать круговой поездки на lookup в центральном маппинге «имя» → Durable Object ID во время query-запросов. Сам Durable Object ID был бы недостаточен, потому что нам также нужны другие метаданные, помогающие выбрать правильное пространство имён Durable Object в Eyeball Worker. Иначе мы бы заменили один lookup на два.

В большинстве случаев, даже для многих собственных приложений, адресация Durable Objects по имени отлично работает и убирает необходимость в lookup из ваших собственных ID к Durable Object ID, так что начните с этого и переходите к уникальным DO ID только если необходимо.

Возможно, вы также спрашиваете, зачем мы используем Workers KV, если у нас есть кеш Hyperdrive и реплики Postgres. (Текст статьи продолжает разбор детализации по этому вопросу, но на этом месте заканчивается предоставленный исходный фрагмент.)