Традиционная архитектура баз данных предполагает наличие одного или двух крупных кластеров баз данных на приложение, обычно дополняемых решением для пула соединений. Платформы разработки с поддержкой ИИ и vibe coding переворачивают эту модель с ног на голову и подталкивают приложения к использованию множества небольших баз данных: по одной на каждого пользователя, приложение, среду предварительного просмотра или тенанта.
Эта модель особенно хорошо подходит для D1. D1 — это управляемая бессерверная база данных от Cloudflare, основанная на SQLite. Она позволяет создавать миллионы баз данных и платить только за то, чем вы пользуетесь.
Возникает вопрос: как масштабировать плоскость управления (control plane) для миллионов баз данных так, чтобы не замедлять каждый запрос, пропуская его через централизованные узкие места?
Мы внесли множество архитектурных изменений, чтобы масштабировать D1 и обеспечить поддержку платформ с миллионами созданных баз данных, сотни тысяч из которых активны в любой момент времени.
Эта статья представляет собой закулисный глубокий разбор того, как работает плоскость управления D1. Многие из описываемых архитектурных паттернов могут использоваться всеми, кто строит свои решения на базе платформы разработчиков Cloudflare. Статья также дает представление о том, как мы создаем собственные плоскости управления.
Рост показателей
Когда мы только начинали планировать эти изменения, использование D1 уже демонстрировало устойчивый рост, но мы понимали, что нам потребуется поддержка гораздо больших масштабов, как только крупные платформы начнут создавать свои продукты на базе D1.
Вот как обстоят дела на данный момент.
Количество баз данных в D1 превысило 5,5 миллиона, при этом за последний год число активных (неудаленных) баз данных выросло в 15 раз, а за последние 2 года — в 65 раз (по состоянию на 8 октября 2026 года).
Мы также зафиксировали 18-кратный рост количества ежедневно активных баз данных за последний год (по состоянию на 8 октября 2026 года). База данных считается активной, если в течение этого дня она выполнила хотя бы один запрос.
Устоявшиеся и успешные платформы уже создают продукты на базе D1, используя те же инструменты, которые доступны всем пользователям Cloudflare.
"ChatGPT Sites позволяет каждому превратить идеи в доступные для шеринга проекты. D1 предоставляет нашим пользователям простой и масштабируемый способ добавления таких функций, как пользовательские метрики, таблицы лидеров и комментарии, в их дашборды, игры и инструменты." — Джон Абрамс (Jon Abrams), ChatGPT Sites @ OpenAI
"ChatGPT Sites позволяет каждому превратить идеи в доступные для шеринга проекты. D1 предоставляет нашим пользователям простой и масштабируемый способ добавления таких функций, как пользовательские метрики, таблицы лидеров и комментарии, в их дашборды, игры и инструменты."
- Джон Абрамс, ChatGPT Sites @ OpenAI
Пока мы вносили изменения, описанные в этом посте блога, мы периодически проводили нагрузочное тестирование с целевыми показателями, значительно превышающими наш ожидаемый рост. Это помогло определить, на чем сосредоточить наши усилия по оптимизации.
Руководящие инженерные принципы
Приложения, созданные на платформе разработчиков Cloudflare, варьируются от полностековых приложений до API сервисов, от внутренних однопользовательских порталов до масштабных многопользовательских платформ vibe coding, а также всего, что находится между ними.
При создании плоскости управления D1 для поддержки миллионов баз данных мы руководствовались следующими принципами и постулатами, которые также будут полезны всем, кто создает свои плоскости управления и данных на нашей платформе.
- Проектируйте все процессы с учетом возможных сбоев и идемпотентности. Сбои будут происходить, поэтому система должна самовосстанавливаться там, где это возможно, и продолжать работу.
- Избегайте централизованных компонентов в одном регионе на любом критическом пути для операций плоскости данных (в нашем случае это все запросы на выборку).
- Используйте Workers KV, глобально распределенное хранилище «ключ-значение», для критических метаданных маршрутизации, чтобы кэш-промахи лишь в редких случаях требовали запросов к центральным базам данных.
- Используйте Hyperdrive для доступа к центральным базам данных Postgres с целью обеспечения эффективного пулинга соединений, а также оптимизированной маршрутизации и кэширования, используя реплики чтения везде, где это возможно.
- Периодически заранее вычисляйте агрегации в базе данных, чтобы избегать запросов со сканированием (scan queries) во время обработки пользовательских запросов.
- Используйте итоговую (eventual) согласованность и очереди между компонентами, не требующими строгой согласованности, для повышения надежности и доступности.
- Проводите нагрузочное тестирование приложения с реалистичными рабочими нагрузками, чтобы выявлять узкие места и неэффективность, и сосредоточьтесь на их оптимизации в первую очередь.
Существуют и другие инженерные принципы, которым мы следуем внутри компании при создании наших систем, но приведенные выше достаточно универсальны и применимы ко всем, кто строит решения на Cloudflare.
В следующих разделах мы подробно рассмотрим основные системные компоненты и процессы плоскости управления D1, связанные с жизненным циклом базы данных D1, а также покажем, как применить эти принципы на практике в реальной системе.
Высокоуровневая архитектура
Давайте начнем с представления основных компонентов D1.
На схеме не хватает нескольких стрелок и блоков, отображающих вспомогательные процессы D1, однако представленные здесь компоненты обеспечивают публичный набор функций D1. Остальные мы рассмотрим в отдельных разделах до конца этого поста в блоге.
Прежде всего, каждая база данных D1 поддерживается одним экземпляром SQLite Durable Object, что сильно упрощает создание миллионов таких баз.
Блок «HTTP API» представляет все наши поверхности API, включая дашборд Cloudflare, Wrangler и CLI Cloudflare. Все запросы к API проходят через граничный шлюз (edge gateway) Cloudflare и обрабатываются воркером D1 Edge API.
На схеме в качестве «Query endpoints» показаны HTTP-эндпоинты, которые позволяют пользователям выполнять SQL-запросы к своим базам данных. Остальные эндпоинты включают привычные операции с базами данных, такие как создание, удаление и получение списка баз данных, а также функции импорта/экспорта и возврата к прошлому состоянию (time travel). Они обозначены как «CRUD endpoints».
Эндпоинты CRUD используют Hyperdrive для запросов к кластеру Postgres в наших центральных дата-центрах (называемых «Core»). Предусмотрено две конфигурации. Одна конфигурация имеет включенное кэширование и направлена на реплики чтения Postgres. Другая конфигурация имеет отключенное кэширование и направлена на первичную базу данных Postgres.
Воркер Edge API может вызывать воркер D1 Eyeball (в зависимости от типа запроса). Воркер Eyeball является нашим прокси для всех запросов, которые должны обрабатываться лежащими в основе объектами D1 Durable Objects (DO). Это позволяет нам перенести всю обработку запросов внутрь воркера Eyeball, включая наблюдаемость (observability), автоматические повторы, обработку ошибок и другие специфичные для запросов поведения.
Workers KV выступает в качестве нашего граничного слоя кэширования для метаданных с интенсивным чтением и доступен для чтения всем воркерам D1.
Блок «Worker Binding» представляет воркеры всех пользователей с помощью D1 Worker binding. Запросы от биндинга D1 поступают напрямую к воркеру D1 Eyeball, который, в свою очередь, пересылает запрос в Durable Object, поддерживающий базу данных, и проксирует ответ обратно пользовательскому воркеру.
У нас есть два метода выполнения пользователями запросов к своим базам данных. Первый — через эндпоинты запросов HTTP API, а второй — через воркер, который они развертывают в своих аккаунтах с помощью биндинга D1.
Что касается операций, мы подразделяем каждую операцию на одну из следующих категорий:
- Плоскость данных (Data plane): любой запрос, связанный с запросами к базе данных; должен выполняться максимально быстро и всегда на «границе» (edge).
- Плоскость управления (Control plane): любая операция HTTP API или внутренние потоки, запускаемые в ответ на пользовательский запрос, не являющийся запросом к БД; они могут зависеть от централизованных однорегиональных компонентов.
- Фоновая асинхронная плоскость: любой рабочий процесс, связанный с аналитикой и биллингом; может быть в конечном итоге согласованным и запускаться где угодно.
Под капотом скрыто гораздо больше, поэтому давайте углубимся в ключевые процессы.
Быстрое создание базы данных по требованию рядом с пользователем
Мультиарендные платформы и разработка на основе ИИ-агентов требуют создания множества экземпляров баз данных быстрым, экономичным и надежным способом.
Ниже приведен пример команды Wrangler для создания базы данных D1:
Когда пользователь создает базу данных D1, должно произойти несколько событий, прежде чем база данных будет готова к приему запросов.
- Проверка того, может ли учетная запись создавать дополнительные базы данных в рамках лимита бесплатного уровня или лимитов учетной записи.
- Генерация уникального UUID базы данных D1, уникального идентификатора Durable Object (newUniqueId()) и инициализация базового Durable Object (DO) с информацией об учетной записи и базе данных.
- Сохранение информации о вновь созданной базе данных в Postgres (одна строка на базу данных, включая метаданные и привязку к инициализированному идентификатору Durable Object).
- Запись привязки идентификатора базы данных к идентификатору Durable Object в Workers KV (в дальнейшем используется плоскостью данных и конечными точками запросов).
Давайте рассмотрим фактический процесс и то, как мы восстанавливаемся после сбоев на каждом этапе.
Как только поступает запрос на создание базы данных, вызывается наш обработчик Edge API Worker в ближайшем к пользователю расположении Cloudflare на границе сети. Edge API запрашивает наши реплики чтения Postgres через Hyperdrive с включенным кэшированием (с TTL кэша в несколько секунд), чтобы получить количество неудаленных баз данных для данной учетной записи.
Edge API Worker перенаправляет запросы плоскости управления в центры обработки данных рядом с нашим кластером Postgres для конечных точек, которые выполняют несколько циклов обмена данными с Postgres (шаг 2), и конечная точка «создать базу данных» — одна из них. Причина в том, что цикл обмена данными издалека может занимать сотни миллисекунд, тогда как нахождение рядом с Postgres — всего несколько миллисекунд. Для конечных точек, которые выполняют не более одного запроса к Postgres, нам не нужно выполнять эту оптимизацию, поскольку Hyperdrive уже обеспечивает эффективную маршрутизацию через сеть Cloudflare.
Создание базы данных D1 без каких-либо параметров по умолчанию размещает базу данных рядом с местоположением запроса. Вы можете повлиять на расположение базы данных, предоставив явную подсказку о местоположении или юрисдикцию. Подсказки о местоположении и параметры юрисдикции идентичны тем, что поддерживают Durable Objects, поэтому при их предоставлении они просто передаются напрямую в инициализацию базового Durable Object.
При создании по умолчанию мы хотим, чтобы базы данных D1 создавались рядом с исходным местоположением запроса, близко к пользователю. Чтобы достичь этого, во время шага 4 диаграммы мы внутренне перенаправляем выполнение запроса в центр обработки данных, близкий к исходному местоположению запроса, и вызываем оттуда базовый Durable Object. Это гарантирует, что базовый Durable Object будет размещен рядом с пользователем.
Вы можете реализовать аналогичные методы маршрутизации, используя явные подсказки о размещении Worker и во время выполнения вызывая нужный Worker через Service Bindings.
Конечная точка создания — это немного странный случай с точки зрения размещения, потому что, хотя мы запрашиваем Postgres как минимум дважды, мы также делаем вызов на большое расстояние к базовым Durable Objects, которые различаются для каждого запроса, поэтому выполнение запроса в любую сторону света может быть неэффективным. Вот почему мы склонились к перенаправлению первоначального запроса рядом с Postgres.
Следующим шагом является генерация UUID базы данных вместе с новым уникальным идентификатором Durable Object с использованием API newUniqueId() и вызов RPC-функции init(options) на заглушке нового Durable Object. Durable Object сохраняет информацию в своем постоянном хранилище, чтобы каждый входящий запрос с этого момента имел все метаданные для связанной базы данных.
Как только Edge API Worker получает ответ об инициализации, он сохраняет метаданные базы данных в нашей центральной основной базе данных Postgres через Hyperdrive, по одной строке метаданных на базу данных.
Приведенные выше шаги обернуты в цикл повторных попыток (шаги 5-7 на диаграмме). Когда инициализация DO или запись в Postgres завершаются неудачей, мы повторяем попытку с другими идентификаторами (это важно, как мы увидим в следующем разделе).
Запись в Postgres — это «контрольная точка создания», где база данных считается созданной, даже если окончательный ответ не доходит до пользователя. С этого момента база данных включается во все операции плоскости управления и может быть привязана к Worker для обслуживания запросов.
На этом этапе база данных D1 готова к приему запросов.
Окончательный RPC-вызов подтверждения создания к Durable Object происходит в waitUntil() и может завершиться неудачей без последствий. Этот вызов сигнализирует DO, что база данных теперь активна, и DO обновляет Workers KV метаданными базы данных, чтобы любой последующий запрос мог немедленно прочитать информацию из кэша Workers KV без необходимости запрашивать Postgres.
Часто задаваемый вопрос: почему процесс создания не может выполняться полностью внутри транзакции Postgres для упрощения процесса? Ответ прост: он не может масштабироваться, так как это приводит к удержанию соединений во время инициализации Durable Object, который может находиться на другом конце света.
Пульсы (heartbeats) для самокорректирующихся баз данных
Каждый Durable Object D1 периодически отправляет пульс (heartbeat) плоскости управления, чтобы получить актуальное состояние базы данных (активна или удалена) и лимиты учетной записи (подробнее об этом в разделах о принудительном применении ниже). Интервал составляет 30 секунд с добавлением джиттера для каждой базы данных. У нас также есть регуляторы для изменения интервала по базе данных или учетной записи, либо для полного отключения пульсов.
Пульсы могут допускать использование слегка устаревших данных, что означает, что они могут обслуживаться несколькими уровнями кэширования Hyperdrive или репликой чтения Postgres. Этот поток пульсов позволяет Durable Object самостоятельно корректироваться, чтобы отражать источник истины (Postgres) в отношении создания и удаления баз данных.
Одна уточняющая деталь, прежде чем мы продолжим. Причина, по которой мы используем разные UUID базы данных и идентификаторы Durable Object для каждой повторной попытки в процессе создания из предыдущего раздела (шаг 5), заключается в том, что если бы мы повторно использовали UUID базы данных с новыми идентификаторами Durable Object, несколько Durable Objects могли бы посчитать, что они владеют одной и той же базой данных, что привело бы к ситуациям, которые трудно отлаживать. Периодические пульсы также действуют как очистка для осиротевших объектов.
Также важно, чтобы мы сначала инициализировали Durable Object, прежде чем сохранять привязку базы данных в Postgres, иначе запросы на выборку могли бы начать поступать к Durable Object и были бы отклонены из-за неинициализированного состояния.
Например, давайте предположим следующий сценарий сбоя и почему он является самокорректирующимся:
- Мы выполнили все из предыдущего раздела до момента инициализации Durable Object A, а затем запись в Postgres завершилась неудачей.
- Worker повторяет цикл и инициализирует новый DO с другим идентификатором B, и на этот раз он сохраняет данные в Postgres.
- Теперь у нас есть два инициализированных и работающих Durable Objects как часть этого запроса, но только один из них является действительным.
- Пользователь может отправлять запросы только к Durable Object 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 самостоятельно записывает свое состояние «удален» в Workers KV, опять же по принципу «наилучших усилий» во время самой операции удаления, и если это не удастся, сигнал в конечном итоге исправит это. Durable Object также хранит состояние «активен» или «удален» в своем хранилище, поэтому он может немедленно отклонять или принимать запросы, даже если данные в Workers KV устарели и пропускают запросы.
В целом, сигналы базы данных являются резервным механизмом для перехода в согласованное состояние, когда операции должны записывать данные во внешнее хранилище, а также обновлять сам Durable Object.
Развязка «горячего» пути: исключение запросов из центральной базы данных
Ключевой принцип для конечных точек запросов заключается в том, чтобы избегать взаимодействия с Postgres или любым другим централизованным однорегиональным компонентом. Каждый запрос по HTTP выполняется нашим Edge API Worker рядом с местоположением исходного запроса пользователя, а затем направляется прямо к D1 Durable Object через Eyeball Worker.
Edge API Worker отвечает за выполнение стандартной аутентификации HTTP API, авторизацию на основе ресурсов, ограничение частоты запросов (rate limiting) на уровне учетной записи и базы данных, проверку запросов, проверку прав доступа учетной записи и другие функции, связанные с API, перед выполнением реальной бизнес-логики.
Eyeball Worker, с другой стороны, пропускает эти дополнительные проверки (помимо собственного выделенного ограничения частоты), поскольку по своей сути привязка D1 (D1 Binding) является привязкой к службе (Service Binding) к D1 Eyeball Worker. Каждая привязка D1 пользовательского Worker содержит закодированную информацию, которая авторизует доступ к одной конкретной базе данных D1, которую D1 Eyeball Worker проверяет и разрешает выполнение запроса.
На следующей схеме показано выполнение запроса через HTTP API.
- Пользователь отправляет запрос через HTTP API.
- Edge API Worker сначала пытается загрузить сопоставление UUID D1 с метаданными базы данных из Workers KV.
- В редких случаях элемент не удается найти в Workers KV — либо из-за сбоя записи, описанного ранее, либо из-за проблем с доступностью. Затем мы переходим к чтению сопоставления из Hyperdrive, нацеленного на реплики Postgres. Перед запросом к фактическим репликам Hyperdrive сначала проверяет свой кэш в местоположении вызывающего объекта, затем свой кэш рядом с местоположением кластера Postgres, и только если в обоих случаях происходит промах кэша, запрашивает реплики Postgres для получения сопоставления.
- Как только сопоставление получено, Edge API Worker вызывает Eyeball Worker через привязки служб Worker (Worker Service Bindings), передавая соответствующие идентификаторы.
- Eyeball Worker выполняет проверку и пересылает запрос к D1 Durable Object.
- D1 Durable Object выполняет SQL-запрос и возвращает результат через Eyeball Worker к Edge API Worker, который возвращает его пользователю.
Если вам интересно, почему мы используем newUniqueId() во время создания базы данных, а не обращаемся к Durable Objects по «имени», чтобы исключить шаги 2 и 3 выше, ответ заключается в том, чтобы избежать кругового поиска в центральном сопоставлении «имени» с «идентификатором Durable Object» во время запросов. Одного идентификатора Durable Object было бы недостаточно, так как нам также нужны другие метаданные, чтобы помочь нам выбрать правильное пространство имен Durable Object для использования в нашем Eyeball Worker. Следовательно, мы бы заменили один поиск двумя.
В большинстве случаев, даже во многих наших собственных приложениях, обращение к Durable Objects по имени работает отлично, и это устраняет необходимость поиска от ваших собственных идентификаторов к идентификатору Durable Object, поэтому используйте это в качестве отправной точки и переходите к уникальным идентификаторам DO только в случае необходимости.
Вы также можете задаться вопросом, почему мы используем Workers KV, если у нас уже есть кэширование Hyperdrive.
Workers KV — это долговечное хранилище данных, а не просто кэш: если элемент выпадает из пограничного кэша KV, чтение обслуживается из собственного хранилища KV, а не из Postgres, что исключает Postgres из плоскости данных. В сценарии катастрофической очистки кэша миллион баз данных, пытающихся получить свою информацию о маршрутизации, привел бы к метастабильным сбоям.
Напоминаем, что запросы от пользовательского Worker с привязкой D1 не требуют разрешения метаданных сопоставления UUID D1, поскольку сама привязка содержит эту информацию внутри, поэтому запросы идут прямо к Eyeball Worker и Durable Object.
Для получения информации о самом выполнении запроса и о том, как Durable Objects реализованы «под капотом», прочитайте наш пост в блоге о репликации чтения D1, где подробно описаны все тонкости.
Далее давайте рассмотрим асинхронные процессы, которые поддерживают D1.
Принудительное соблюдение лимитов использования хранилища
Все платформы, достигающие определенного масштаба, в конечном итоге должны обеспечивать соблюдение лимитов биллинга и использования. На момент написания статьи D1 имеет лимит 5 ГБ на бесплатном тарифе и 1 ТБ на тарифе Workers Paid (может быть увеличен по запросу).
В целом, системы обеспечения соблюдения лимитов использования и биллинга являются хорошими кандидатами для согласованности в конечном счете и асинхронной обработки, и в следующих разделах мы рассмотрим, как D1 реализует соблюдение этих лимитов.
Асинхронная обработка обновлений с помощью очередей
Первым шагом является то, что каждый Durable Object публикует обновления использования хранилища в очередь, подобную Kafka, для пакетной и асинхронной обработки. Плоскости управления не нужно обрабатывать каждое обновление синхронно: ничего страшного, если всплеск нагрузки не отразится немедленно и наша информация об использовании будет устаревшей. Важно то, что это не должно перегружать Postgres и влиять на пользователей D1. Мы обеспечиваем это путем ограничения количества потребителей очереди и, следовательно, нагрузки на запись в Postgres.
Система потребителей очереди работает внутри центральных центров обработки данных Cloudflare («Core»), тогда как сами Durable Objects распределены по всему миру на границе сети. Использование агрегируется локально на каждом пограничном сервере, а затем публикуется во внутреннюю систему очередей.
Руководящий принцип здесь заключается в том, что мы используем очередь для сглаживания и пакетирования записей в наше центральное региональное хранилище данных Postgres.
Пакетная обработка дельт использования и предварительная агрегация
Потребитель очереди записывает последний сообщенный размер для каждой базы данных в таблицу Postgres. Однако для принятия решений об обеспечении соблюдения ограничений нам нужно не это. Для таких решений нам нужна сумма всего объема хранилища по всей учетной записи. Когда мы считываем данные об использовании в момент проверки ограничений, мы не можем позволить себе выполнять запрос SELECT sum(storage) FROM database_stats WHERE account_id = ?. Стоимость этого запроса линейно масштабируется в зависимости от количества баз данных в каждой учетной записи. Это не масштабируется для учетных записей с миллионами баз данных D1, как в случае с некоторыми платформами для агентного программирования, построенными на базе D1.
В идеале нам нужна справочная таблица, которая позволит нам переходить непосредственно от идентификатора учетной записи к общему объему хранилища баз данных. Решение заключается в использовании инкрементного обновления представлений для предварительного вычисления суммы и поддержания ее в актуальном состоянии при изменении объема хранилища баз данных.
Назовем эту таблицу account_database_stats.
Теоретически мы могли бы заставить потребителя очереди обновлять database_stats и саму таблицу account_database_stats при каждом поступающем обновлении. На практике это привело бы к большому количеству конфликтов блокировок записи для одной строки в крупных учетных записях. Вместо этого мы добавляем третью таблицу под названием account_database_stats_changes, которая действует как очередь изменений. Это журнал всех изменений, в котором может быть несколько записей для одной учетной записи. Затем мы периодически агрегируем обновления из этой таблицы в account_database_stats. Процесс выглядит следующим образом:
- Потребитель очереди записывает информацию об использовании в таблицу database_stats для конкретной базы данных.
- Триггер вычисляет разницу в объеме хранилища для этой базы данных и помещает дельту в таблицу account_database_stats_changes (одна строка на дельту).
- Запланированное задание выполняется каждую секунду, чтобы обработать все данные в account_database_stats_changes и обновить account_database_stats суммой этих дельт.
Конкретно, запланированное задание выполняет следующий запрос:
Он получает список обновлений из account_database_stats_changes, агрегирует их по учетной записи, а затем записывает дельты в таблицу для каждой учетной записи. Это позволяет избежать конфликтов записи для любой конкретной строки, поскольку мы агрегируем изменения перед их записью.
Конструкция Common Table Expression (CTE) DELETE ... RETURNING атомарно потребляет ожидающие изменения и передает удаленные строки в следующее CTE. Postgres выполняет CTE, изменяющее данные, ровно один раз и до завершения. Затем запрос группирует эти изменения по учетной записи и применяет одно агрегированное обновление на учетную запись, уменьшая конфликты записи.
Эта схема проектирования является шаблоном, используемым многими другими командами Cloudflare, и она позволяет нам масштабироваться до миллионов ресурсов на учетную запись, не делая общие операции более затратными при добавлении каждого нового ресурса.
Стоит отметить, что итоговое значение является согласованным в конечном счете. Это не первоклассное инкрементное обновление представлений, которое можно найти в некоторых других базах данных. Мы уже используем очередь для получения обновлений из Durable Objects в Postgres, поэтому нас вполне устраивает согласованность в конечном счете.
Кэшированное чтение через Hyperdrive
Теперь у нас есть справочная таблица для общего объема хранилища каждой учетной записи. Что дальше?
Каждый объект D1 Durable Object периодически получает данные об использовании учетной записи через упомянутый ранее периодический heartbeat. Мы запрашиваем Postgres с помощью SELECT storage FROM account_database_stats WHERE account_id = ? и сравниваем это значение с лимитами прав и тарифов учетной записи. Если общая сумма превышает лимит, мы запрещаем все операции записи, которые увеличили бы размер базы данных.











