etcd vs PostgreSQL
Jinhua Luo
March 17, 2023
Историческая справка
PostgreSQL
PostgreSQL был первоначально разработан в 1986 году под руководством профессора Майкла Стоунбрейкера в Калифорнийском университете в Беркли. За несколько десятилетий развития PostgreSQL стал ведущей системой управления реляционными базами данных с открытым исходным кодом, доступной сегодня. Его либеральная лицензия позволяет любому свободно использовать, модифицировать и распространять PostgreSQL, независимо от того, предназначено ли это для частного, коммерческого или академического исследования.
PostgreSQL предлагает мощную поддержку как для аналитической обработки в реальном времени (OLAP), так и для оперативной обработки транзакций (OLTP), обладая мощными возможностями SQL-запросов и широким спектром расширений, которые позволяют ему удовлетворять практически все коммерческие потребности. В результате он привлекает все больше внимания в последние годы. Фактически, масштабируемость и высокая производительность PostgreSQL позволяют ему воспроизводить функциональность практически любого другого типа базы данных.
Источник изображения (согласно лицензии CC 3.0 BY-SA): https://en.wikibooks.org/wiki/PostgreSQL/Architecture
etcd
Как появился etcd и какую проблему он решает?
В 2013 году стартап-команда CoreOS разработала продукт под названием Container Linux. Это операционная система с открытым исходным кодом, легковесная и ориентированная на автоматизацию и быстрое развертывание сервисов приложений. Container Linux требует, чтобы приложения запускались в контейнерах, и предоставляет решение для управления кластерами, что упрощает управление сервисами, как если бы они работали на одной машине.
Чтобы гарантировать, что сервисы пользователей не будут простаивать из-за перезапуска узла, CoreOS необходимо было запускать несколько реплик. Но как координировать работу между несколькими репликами и избежать ситуации, когда все реплики становятся недоступными во время изменений?
Чтобы решить эту проблему, команде CoreOS потребовался сервис координации, который мог бы хранить информацию о конфигурации сервисов и предоставлять возможности распределенных блокировок и многое другое. Какой подход они выбрали? Сначала они проанализировали бизнес-сценарий, болевые точки и ключевые цели. Затем они выбрали решение, соответствующее их целям, оценивая, стоит ли выбирать решение из сообщества с открытым исходным кодом или разрабатывать собственный инструмент. Этот подход является универсальным методом решения проблем, который часто применяется при столкновении с трудными задачами, и команда CoreOS следовала тому же принципу.
Идеальный сервис координации должен удовлетворять следующим пяти целям:
- Высокая доступность с несколькими репликами данных.
- Согласованность данных с проверкой версий между репликами.
- Минимальный объем хранилища: сервис координации должен хранить только критически важные метаданные конфигурации сервисов и узлов, относящиеся к конфигурации управляющей плоскости, а не данные пользователей. Такой подход минимизирует необходимость шардинга данных для хранения и избегает избыточного проектирования.
- Функциональность для CRUD (создание, чтение, обновление и удаление), а также механизм отслеживания изменений данных. Он должен хранить информацию о состоянии сервисов, и при изменениях или аномалиях в сервисах быстро передавать событие изменения в управляющую плоскость. Это помогает повысить доступность сервисов и снизить ненужные затраты на производительность сервиса координации.
- Простота эксплуатации: сервис координации должен быть простым в эксплуатации, обслуживании и устранении неполадок. Удобный интерфейс может снизить риск ошибок, уменьшить затраты на обслуживание и минимизировать время простоя.
С точки зрения теоремы CAP, etcd относится к системе CP (Consistency & Partition Tolerance).

Как центральный компонент кластера Kubernetes, kube-apiserver использует etcd в качестве хранилища.
С одной стороны, etcd используется для сохранения ресурсов при создании объектов в кластере k8s. С другой стороны, именно механизм отслеживания данных etcd управляет работой всего кластера через Informer, обеспечивая непрерывную оркестрацию контейнеров.
Таким образом, с технической точки зрения, основные причины, по которым Kubernetes использует etcd, заключаются в следующем:
- etcd написан на языке Go, что соответствует технологическому стеку k8s, имеет низкое потребление ресурсов и чрезвычайно прост в развертывании.
- Сильная согласованность, механизмы watch, lease и другие функции etcd являются ключевыми зависимостями k8s.
В итоге, etcd — это распределенная база данных ключ-значение, разработанная специально для управления и распределения конфигураций. Как облачное программное обеспечение, оно предлагает готовую к использованию функциональность и высокую производительность, что делает его превосходящим традиционные базы данных в этой конкретной области.
Чтобы объективно сравнить etcd и PostgreSQL, которые представляют собой два разных типа баз данных, важно оценить их в контексте одних и тех же требований. Поэтому в этой статье мы обсудим только различия между ними с точки зрения их способности удовлетворять требованиям управления конфигурациями.
Модель данных
Разные базы данных представляют пользователям разные модели данных, и этот фактор определяет пригодность базы данных для различных сценариев.
Ключ-значение vs SQL
Модель данных ключ-значение является популярной моделью в NoSQL, которую также использует etcd. Как эта модель сравнивается с SQL и каковы ее преимущества?
Сначала рассмотрим SQL.
Реляционные базы данных хранят данные в таблицах и предоставляют эффективный, интуитивно понятный и гибкий способ хранения и доступа к структурированной информации.
Таблица, также известная как отношение, состоит из столбцов, содержащих одну или несколько категорий данных, и строк, также известных как записи таблицы, которые включают набор данных, определяющих категории. Приложения извлекают данные с помощью запросов, использующих операции, такие как "проекция" для идентификации атрибутов, "выбор" для идентификации кортежей и "соединение" для объединения отношений. Реляционная модель управления базами данных была разработана в 1970 году Эдгаром Коддом, ученым в области компьютерных наук из IBM.

Источник изображения (согласно лицензии CC 3.0 BY-SA): https://en.wikipedia.org/wiki/Associative_entity
Записи в таблице не имеют уникальных идентификаторов, так как таблицы предназначены для хранения нескольких дублирующихся строк. Чтобы обеспечить запросы по ключу-значению, необходимо добавить уникальный индекс к полю, которое служит ключом в таблице. По умолчанию в PostgreSQL используется индекс btree, который, как и etcd, может выполнять диапазонные запросы по ключам.
Язык структурированных запросов (SQL) — это язык программирования для хранения и обработки информации в реляционной базе данных. Реляционная база данных хранит информацию в табличной форме, где строки и столбцы представляют различные атрибуты данных и различные отношения между значениями данных. Вы можете использовать SQL-запросы для хранения, обновления, удаления, поиска и извлечения информации из базы данных. Вы также можете использовать SQL для обслуживания и оптимизации производительности базы данных.
PostgreSQL расширил SQL множеством расширений, сделав его тьюринг-полным языком. Это означает, что SQL может выполнять любые сложные операции, что облегчает выполнение логики обработки данных полностью на стороне сервера.
В сравнении, etcd разработан как инструмент управления конфигурациями, где данные конфигурации обычно представлены в виде хэш-таблицы. Именно поэтому его модель данных структурирована в формате ключ-значение, что фактически создает одну большую глобальную таблицу. На этой таблице можно выполнять операции CRUD, и она имеет только два поля: уникальный ключ с информацией о версии и нетипизированное значение. В результате клиенты должны извлекать полное значение для дальнейшей обработки.
В целом, структура ключ-значение etcd упрощает SQL и является более удобной и интуитивно понятной для конкретной задачи управления конфигурациями.
MVCC (Многовариантное управление параллелизмом)
MVCC — это важная функция для управления версиями данных в управлении конфигурациями. Она позволяет:
- Запрашивать исторические данные.
- Определять возраст данных, сравнивая версии.
- Отслеживать данные, что требует управления версиями для обеспечения инкрементных уведомлений.
И etcd, и PostgreSQL поддерживают MVCC, но в чем их различия?
etcd использует глобальный инкрементируемый 64-битный счетчик версий для управления своей системой MVCC. Не нужно беспокоиться о переполнении. Счетчик рассчитан на обработку огромного количества обновлений, даже если они происходят со скоростью миллионов в секунду. Каждый раз, когда создается или обновляется пара ключ-значение, ей присваивается номер версии. Когда пара ключ-значение удаляется, создается "надгробный камень" с номером версии, сброшенным на 0. Это означает, что каждое изменение создает новую версию, а не перезаписывает предыдущую.
Кроме того, etcd сохраняет все версии пары ключ-значение и делает их видимыми для пользователей. Данные ключ-значение никогда не перезаписываются, и новые версии хранятся вместе с существующими. Реализация MVCC в etcd также обеспечивает разделение чтения и записи, что позволяет пользователям читать данные без блокировок, что делает ее подходящей для сценариев с интенсивным чтением.
Реализация MVCC в PostgreSQL отличается от etcd тем, что она не ориентирована на предоставление инкрементируемых номеров версий, а скорее на реализацию транзакций и различных уровней изоляции, прозрачных для пользователя. MVCC — это оптимистичный механизм блокировок, который позволяет выполнять параллельные обновления. Каждая строка в таблице имеет запись идентификатора транзакции, где xmin представляет идентификатор транзакции создания строки, а xmax — идентификатор транзакции обновления строки.
- Транзакции могут читать только данные, которые уже были зафиксированы до них.
- При обновлении данных, если возникает конфликт версий, PostgreSQL повторяет попытку с использованием механизма сопоставления, чтобы определить, следует ли продолжать обновление.
Пример можно посмотреть по ссылке: https://devcenter.heroku.com/articles/postgresql-concurrency
К сожалению, использование идентификаторов транзакций для управления версиями данных конфигурации в PostgreSQL невозможно по нескольким причинам:
- Идентификаторы транзакций назначаются всем строкам, участвующим в одной транзакции, что означает, что управление версиями не может быть применено на уровне строки.
- Исторические запросы не могут быть выполнены, и доступна только последняя версия строки.
- Из-за своей 32-битной природы идентификаторы транзакций подвержены переполнению и сбросу во время вакуумирования.
- Невозможно реализовать функциональность отслеживания на основе идентификаторов транзакций.
В результате PostgreSQL требует альтернативных методов для управления версиями данных конфигурации, так как встроенная поддержка отсутствует.
Интерфейс клиента
Дизайн интерфейса клиента является критическим аспектом, когда речь идет о стоимости и потреблении ресурсов, связанных с его использованием. Анализируя различия между интерфейсами, можно сделать обоснованный выбор при выборе наиболее подходящего варианта.
API etcd kv/watch/lease оказались особенно эффективными для управления конфигурациями. Однако как можно реализовать эти API в PostgreSQL?
К сожалению, PostgreSQL не предоставляет встроенной поддержки для этих API, и для их реализации требуется инкапсуляция. Чтобы проанализировать их реализацию, мы рассмотрим проект pg_watch_demo, разработанный мной: pg_watch_demo.
gRPC/HTTP vs TCP
PostgreSQL следует архитектуре с несколькими процессами, где каждый процесс обрабатывает только одно TCP-соединение за раз. Он использует собственный протокол для предоставления функциональности через SQL-запросы и следует модели взаимодействия запрос-ответ (аналогично HTTP/1.1, который обрабатывает только один запрос за раз и требует конвейеризации для обработки нескольких запросов одновременно). Однако, учитывая высокое потребление ресурсов и относительно низкую эффективность, прокси-пул соединений (например, pgbouncer) имеет решающее значение для повышения производительности, особенно в сценариях с высоким QPS.
С другой стороны, etcd разработан на архитектуре с несколькими корутинами в Golang и предлагает два удобных интерфейса: gRPC и RESTful. Эти интерфейсы легко интегрируются и эффективны с точки зрения потребления ресурсов. Кроме того, каждое gRPC-соединение может обрабатывать несколько параллельных запросов, что обеспечивает оптимальную производительность.
Определение данных
etcd
message KeyValue { bytes key = 1; // Номер ревизии при создании ключа int64 create_revision = 2; // Номер ревизии при последнем изменении ключа int64 mod_revision = 3; // Инкрементируемый счетчик, который увеличивается каждый раз при обновлении ключа. // Этот счетчик сбрасывается в ноль при удалении ключа и используется как "надгробный камень". int64 version = 4; bytes value = 5; // Объект аренды, используемый ключом для TTL. Если значение равно 0, то TTL отсутствует. int64 lease = 6; }
PostgreSQL
PostgreSQL должен использовать таблицу для имитации глобального пространства данных etcd:
CREATE TABLE IF NOT EXISTS config ( key text, value text, -- Эквивалент `create_revision` и `mod_revision` -- Здесь используется тип последовательности с большим целым числом для имитации ревизии revision bigserial, -- "Надгробный камень" tombstone boolean NOT NULL DEFAULT false, -- Составной индекс, поиск сначала по ключу, затем по ревизии primary key(key, revision) );
get
etcd
API get в etcd имеет широкий диапазон параметров:
- Диапазонные запросы, например, установка
keyкак/abcиrange_endкак/abdизвлечет все пары ключ-значение с префиксом/abc. - Исторические запросы, указание
revisionили диапазонаmod_revision. - Сортировка и ограничение количества возвращаемых результатов.
message RangeRequest { ... bytes key = 1; // Диапазонные запросы bytes range_end = 2; int64 limit = 3; // Исторические запросы int64 revision = 4; // Сортировка SortOrder sort_order = 5; SortTarget sort_target = 6; bool serializable = 7; bool keys_only = 8; bool count_only = 9; // Исторические запросы int64 min_mod_revision = 10; int64 max_mod_revision = 11; int64 min_create_revision = 12; int64 max_create_revision = 13; }
PostgreSQL
PostgreSQL может выполнять функцию get etcd через SQL и даже предоставлять более сложные функции. Поскольку SQL сам по себе является языком, а не интерфейсом с фиксированными параметрами, он обладает высокой универсальностью. Здесь мы покажем простой пример извлечения последней пары ключ-значение. Поскольку первичный ключ является составным индексом, он может быть быстро найден по диапазону, что обеспечивает высокую скорость поиска.
CREATE FUNCTION get1(kk text) RETURNS table(r bigint, k text, v text, c bigint) AS $$ SELECT revision, key, value, create_time FROM config where key = kk and tombstone = false ORDER BY key, revision desc limit 1 $$ LANGUAGE sql;
put
etcd
message PutRequest { bytes key = 1; bytes value = 2; int64 lease = 3; // следует ли отвечать данными пары ключ-значение до обновления из этого запроса `Put`. bool prev_kv = 4; bool ignore_value = 5; bool ignore_lease = 6; }
PostgreSQL
Как и в etcd, PostgreSQL не выполняет изменения на месте. Вместо этого вставляется новая строка, и ей присваивается новая ревизия.
CREATE FUNCTION set(k text, v text) RETURNS bigint AS $$ insert into config(key, value) values(k, v) returning revision; $$ LANGUAGE SQL;
delete
etcd
message DeleteRangeRequest { bytes key = 1; bytes range_end = 2; bool prev_kv = 3; }
PostgreSQL
Как и в etcd, удаление в PostgreSQL не изменяет данные на месте. Вместо этого вставляется новая строка с полем tombstone, установленным в true, чтобы указать, что это "надгробный камень".
CREATE FUNCTION del(k text) RETURNS bigint AS $$ insert into config(key, tombstone) values(k, true) returning revision; $$ LANGUAGE SQL;
watch
etcd
message WatchCreateRequest { bytes key = 1; // Указывает диапазон ключей для отслеживания bytes range_end = 2; // Начальная ревизия для отслеживания int64 start_revision = 3; ... } message WatchResponse { ResponseHeader header = 1; ... // Для эффективности может быть возвращено несколько событий repeated mvccpb.Event events = 11; }
PostgreSQL
PostgreSQL не имеет встроенной функции watch, и вместо этого требуется комбинация триггеров и каналов для достижения аналогичной функциональности. Используя pg_notify, данные могут быть отправлены всем приложениям, которые слушают определенный канал.
-- функция триггера для распределения событий put/delete CREATE FUNCTION notify_config_change() RETURNS TRIGGER AS $$ DECLARE data json; channel text; is_channel_exist boolean; BEGIN IF (TG_OP = 'INSERT') THEN -- использование JSON для кодирования data = row_to_json(NEW); -- Извлечение имени канала для распределения из ключа channel = (SELECT SUBSTRING(NEW.key, '/(.*)/')); -- Если приложение отслеживает канал, отправляет событие через него is_channel_exist = NOT pg_try_advisory_lock(9080); IF is_channel_exist THEN PERFORM pg_notify(channel, data::text); ELSE PERFORM pg_advisory_unlock(9080); END IF; END IF; RETURN NULL; -- Результат игнорируется, так как это триггер AFTER END; $$ LANGUAGE plpgsql; CREATE TRIGGER notify_config_change AFTER INSERT ON config FOR EACH ROW EXECUTE FUNCTION notify_config_change();
Поскольку функция watch инкапсулирована, клиентские приложения также должны реализовать соответствующую логику. Используя Golang в качестве примера, необходимо выполнить следующие шаги:
- Начать прослушивание: Когда прослушивание начинается, все данные уведомлений будут кэшироваться как на уровне PostgreSQL, так и на уровне канала Golang.
- Получить все данные с помощью get_all(key_prefix, revision): Эта функция читает все существующие данные, начиная с указанной ревизии. Для каждого ключа будет возвращена только последняя версия данных, а удаленные данные будут автоматически удалены. Если ревизия не указана, возвращаются последние данные для всех ключей с указанным
key_prefix. - Отслеживать новые данные, включая любые уведомления, которые могли быть кэшированы между первым и вторым шагами, чтобы избежать пропуска новых данных, которые могут произойти в этом временном окне. Игнорировать любые ревизии, которые уже были прочитаны на втором шаге.
func watch(l *pq.Listener) { for { select { case n := <-l.Notify: if n == nil { log.Println("listener reconnected") log.Printf("get all routes from rev %d including tombstones...\n", latestRev) // При повторном подключении возобновить передачу на основе ревизии до отключения. str := fmt.Sprintf(`select * from get_all_from_rev_with_stale('/routes/', %d)`, latestRev) rows, err := db.Query(str) ... continue } ... // поддерживать состояние, которое записывает последнюю ревизию, которую оно получило updateRoute(cfg) case <-time.After(15 * time.Second): log.Println("Received no events for 15 seconds, checking connection") go func() { // Если события не поступают в течение длительного времени, проверить состояние соединения if err := l.Ping(); err != nil { log.Println("listener ping error: ", err) } }() } } } log.Println("get all routes...") // При инициализации приложение должно получить все текущие пары ключ-значение, а затем инкрементально отслеживать обновления через watch rows, err := db.Query(`select * from get_all('/routes/')`) ... go watch(listener)
transaction
etcd
Транзакции в etcd — это набор нескольких операций с условными проверками, и изменения, внесенные транзакцией, фиксируются атомарно.
message TxnRequest { // Указывает условие выполнения транзакции repeated Compare compare = 1; // Операции, которые будут выполнены, если условие выполнено repeated RequestOp success = 2; // Операции, которые будут выполнены, если условие не выполнено repeated RequestOp failure = 3; }
PostgreSQL
Команда DO в PostgreSQL позволяет выполнять любые команды, включая хранимые процедуры. Она поддерживает несколько языков, включая встроенные языки, такие как PL/pgSQL и Python. С этими языками можно реализовать любые условные проверки, циклы и другую управляющую логику, что делает ее более универсальной, чем etcd.
DO LANGUAGE plpgsql $$ DECLARE n_plugins int; BEGIN SELECT COUNT(1) INTO n_plugins FROM get_all('/plugins/'); IF n_plugins = 0 THEN perform set('/routes/1', 'foobar'); perform set('/upstream/1', 'foobar'); ... ELSE ... END IF; END; $$;
lease
etcd
В etcd можно создать объект аренды, который приложения должны периодически обновлять, чтобы предотвратить его истечение. Каждая пара ключ-значение может быть связана с объектом аренды, и когда объект аренды истекает, все связанные пары ключ-значение также истекают, автоматически удаляясь.
message LeaseGrantRequest { // TTL аренды int64 TTL = 1; int64 ID = 2; } // Обновление аренды message LeaseKeepAliveRequest { int64 ID = 1; } message PutRequest { bytes key = 1; bytes value = 2; // ID аренды, используется для реализации TTL int64 lease = 3; ... }
PostgreSQL
- В PostgreSQL аренду можно поддерживать через внешний ключ. При запросе, если есть связанный объект аренды, который истек, он считается "надгробным камнем".
- Запросы keepalive обновляют временную метку
last_keepaliveв таблице аренды.
CREATE TABLE IF NOT EXISTS config ( key text, value text, ... -- Использование внешнего ключа для указания связанного объекта аренды. lease int64 references lease(id), ); CREATE TABLE IF NOT EXISTS lease ( id text, ttl int, last_keepalive timestamp; );
Сравнение производительности
PostgreSQL должен имитировать различные API etcd через инкапсуляцию. Какова его производительность? Вот результаты простого теста: https://github.com/kingluo/pg_watch_demo#benchmark.

Результаты показывают, что производительность чтения и записи почти идентична, причем PostgreSQL даже превосходит etcd. Кроме того, задержка от момента обновления до получения события приложением определяет эффективность распределения обновлений, и PostgreSQL и etcd показывают схожие результаты. При тестировании на одной машине для клиента и сервера задержка watch составляла менее 1 миллисекунды.
Однако у PostgreSQL есть некоторые недостатки, которые стоит упомянуть:
- WAL-лог для каждого обновления больше, что приводит к удвоенному объему дискового ввода-вывода по сравнению с etcd.
- Он потребляет больше CPU по сравнению с etcd.
- Уведомления на основе каналов — это концепция уровня транзакций. При обновлении одного и того же типа ресурса обновление отправляется в один и тот же канал, и запросы на обновление конкурируют за мьютексы, что приводит к сериализации запросов. Другими словами, использование каналов для реализации watch влияет на параллелизм операций put.
Это подчеркивает, что для достижения тех же требований необходимо больше инвестировать в изучение и оптимизацию PostgreSQL.
Хранение
Производительность определяется базовым хранилищем, и то, как данные хранятся, определяет требования базы данных к памяти, диску и другим ресурсам.
etcd
Схема архитектуры хранилища etcd:

etcd сначала записывает обновления в журнал упреждающей записи (WAL) и сбрасывает их на диск, чтобы гарантировать, что обновления не будут потеряны. Как только журнал успешно записан и подтвержден большинством узлов, результаты могут быть возвращены клиенту. etcd также асинхронно обновляет TreeIndex и BoltDB.
Чтобы избежать бесконечного роста журнала, etcd периодически создает снимок хранилища, и журналы до снимка могут быть удалены.
etcd индексирует все ключи в памяти (TreeIndex), записывая информацию о версии каждого ключа, но сохраняет только указатель на BoltDB (ревизию) для значения.
Значение, соответствующее ключу, хранится на диске и поддерживается с использованием BoltDB.
И TreeIndex, и BoltDB используют структуру данных btree, которая известна своей эффективностью при поиске и диапазонном поиске.
Схема структуры TreeIndex:

(Источник изображения: https://blog.csdn.net/H_L_S/article/details/112691481, лицензия CC 4.0 BY-SA)
Каждый ключ разделен на разные поколения, и каждое удаление отмечает конец поколения.
Указатель на значение состоит из двух целых чисел. Первое целое число main — это идентификатор транзакции etcd, а второе целое число sub представляет идентификатор обновления этого ключа в рамках этой транзакции.
Boltdb поддерживает транзакции и снимки и хранит значение, соответствующее ревизии.

(Источник изображения: https://blog.csdn.net/H_L_S/article/details/112691481, лицензия CC 4.0 BY-SA)
Пример записи данных:
Запись key="key1", revision=(12,1), value="keyvalue5". Обратите внимание на изменения в красных частях treeIndex и BoltDB:

(Источник изображения: https://blog.csdn.net/H_L_S/article/details/112691481, лицензия CC 4.0 BY-SA)
Удаление key="key", revision=(13,1) создает новое пустое поколение в treeIndex и генерирует пустое значение в BoltDB с key="13_1t".
Здесь t означает "надгробный камень". Это означает, что вы не можете прочитать надгробный камень, потому что указатель в treeIndex — это (13,1), но в BoltDB это 13_1t, что не может быть сопоставлено.

(Источник изображения: https://blog.csdn.net/H_L_S/article/details/112691481, лицензия CC 4.0 BY-SA)
Стоит отметить, что etcd планирует как чтение, так и запись в BoltDB с использованием одной горутины, чтобы уменьшить случайный ввод-вывод на диск и повысить производительность ввода-вывода.
PostgreSQL
Схема архитектуры хранилища PostgreSQL:

Как и etcd, PostgreSQL сначала добавляет обновления в файл журнала и ждет, пока журнал будет успешно сброшен на диск, прежде чем считать транзакцию завершенной. Тем временем обновления записываются в память shared_buffer.
Shared_buffer — это область памяти, которая используется всеми таблицами и индексами в PostgreSQL, и она служит отображением для этих объектов.
В PostgreSQL каждая таблица состоит из нескольких страниц, каждая из которых имеет размер 8 КБ и содержит несколько строк.
Помимо таблиц, индексы (например, btree-индексы) также состоят из страниц таблиц в том же формате. Однако эти страницы являются специальными и связаны между собой, образуя древовидную структуру.
PostgreSQL оснащен процессом checkpointer, который периодически сбрасывает все измененные страницы таблиц и индексов на диск. Перед каждой контрольной точкой файлы журналов могут быть удалены и переработаны, чтобы предотвратить бесконечный рост журнала.
Структура страницы:

(Источник изображения: https://en.wikibooks.org/wiki/PostgreSQL/Page_Layout, лицензия CC 3.0 BY-SA)
Структура btree-индекса:
![Btree-индекс PostgreSQL](https://