J.5. citus — функциональность распределённой базы данных и столбцовое хранение #
citus — это расширение, совместимое с Postgres Pro и предоставляющее такие основные функциональные возможности, как столбцовое хранение и распределённая база данных OLAP, которые можно использовать вместе или раздельно.
citus обладает следующими преимуществами:
Столбцовое хранение с возможностью сжатия данных.
Возможность масштабировать инсталляцию Postgres Pro до кластера распределённых баз данных.
Сегментирование на основе строк или схем.
Распараллеливание DML-операций по узлам кластера.
Таблицы-справочники, к которым можно обращаться локально на каждом узле.
Возможность выполнять DML-запросы на любом узле, что позволяет максимально эффективно использовать кластер для распределённых запросов.
J.5.1. Ограничения #
Расширение citus несовместимо с некоторыми функциональными возможностями Postgres Pro Enterprise, обратите внимание на эти ограничения при планировании работы:
citus не может использоваться совместно с автономными транзакциями.
Если для параметра enable_self_join_removal задано значение
on, планирование запроса завершится ошибкой, если оптимизатор сделает запрос распределённым. В противном случае оптимизатор может сформировать ошибочный план распределённого запроса и вернуть некорректные результаты. Поэтому рекомендуется задать для этого параметра значениеoff.citus не следует использовать совместно с перепланированием запросов в реальном времени, так как команда
EXPLAIN ANALYZEможет работать некорректно.citus не может работать с выключенным (
off) параметром конфигурацииstandard_conforming_strings. citus_columnar может, но во избежание ошибок необходимо установить для этого параметра значениеonпри выполнении командCREATE EXTENSIONилиALTER EXTENSION UPDATE. После завершения установки или обновления при необходимости можно изменить значение наoff. Расширение продолжит работать в штатном режиме.
J.5.2. Установка #
Расширение citus поставляется вместе с Postgres Pro Enterprise в виде отдельного пакета citus-ent-16. В зависимости от подключаемого репозитория можно установить citus 12.1 или citus 13.0. Подробные инструкции по установке приведены в Главе 17). После установки Postgres Pro Enterprise следуйте приведённым ниже инструкциям по установке citus.
J.5.2.1. Установка citus на одном узле #
Чтобы задействовать citus на одном узле, выполните следующие действия:
Добавьте
citusв переменнуюshared_preload_librariesв файлеpostgresql.conf:shared_preload_libraries = 'citus'
Расширение citus следует указывать первым в
shared_preload_libraries, если планируется использовать его вместе с другими расширениями.Перезагрузите сервер баз данных для применения изменений. Чтобы убедиться, что библиотека
citusустановлена правильно, выполните следующую команду:SHOW shared_preload_libraries;
Создайте расширение citus, выполнив следующий запрос:
CREATE EXTENSION citus;
При выполнении команды CREATE EXTENSION в рамках вышеуказанной процедуры также устанавливается расширение citus_columnar. При необходимости задействовать только citus_columnar выполните те же действия, но вместо citus укажите citus_columnar.
J.5.2.2. Установка citus на нескольких узлах #
Чтобы задействовать citus на нескольких узлах, выполните следующие действия на каждом из них:
Добавьте
citusв переменнуюshared_preload_librariesв файлеpostgresql.conf:shared_preload_libraries = 'citus'
Расширение citus следует указывать первым в
shared_preload_libraries, если планируется использовать его вместе с другими расширениями.Настройте права доступа к серверу баз данных. По умолчанию сервер баз данных принимает подключения от клиентов только через
localhost. Установите значение*для параметра конфигурацииlisten_addresses, чтобы указать все имеющиеся IP-интерфейсы.Настройте аутентификацию клиентов, отредактировав файл
pg_hba.conf.Перезагрузите сервер баз данных для применения изменений. Чтобы убедиться, что библиотека
citusустановлена правильно, выполните следующую команду:SHOW shared_preload_libraries;
Создайте расширение citus, выполнив следующий запрос:
CREATE EXTENSION citus;
Когда все описанные выше шаги будут выполнены на всех узлах, совершите следующие действия на узле-координаторе, чтобы рабочие узлы могли к нему подключиться:
Зарегистрируйте адрес узла, по которому узел-координатор будет принимать подключения от рабочих узлов:
SELECT citus_set_coordinator_host(
'имя_узла_координатора',порт_узла_координатора);Добавьте каждый рабочий узел:
SELECT * from citus_add_node(
'имя_рабочего_узла',порт_рабочего_узла);Убедитесь, что все рабочие узлы заданы:
SELECT * FROM citus_get_active_worker_nodes();
J.5.3. Обновление citus #
Чтобы обновить citus с версии 12.1 на версию 13.0, выполните следующие шаги:
Установите пакет версии 13.0.
Перезапустите сервер Postgres Pro.
Измените определение расширения, выполнив команду
ALTER EXTENSION:postgres=# ALTER EXTENSION citus UPDATE; ALTER EXTENSION
Проверьте версию расширения после обновления:
postgres=# SELECT * from citus_version(); citus_version ------------------------------------------------------------------------------------------------------ Citus 13.0.3.1 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit (1 row)
J.5.4. Когда использовать citus #
J.5.4.1. Многоарендная база данных SaaS #
Модели данных большинства B2B-приложений уже содержат такие понятия, как арендатор, клиент или учётная запись. В рамках этой модели база данных используется несколькими арендаторами, данные которых обрабатываются отдельно друг от друга.
citus поддерживает все возможности SQL для выполнения этих задач и позволяет масштабировать реляционные базы данных для более 100 000 арендаторов. Кроме того, расширение содержит новую функциональность для поддержки многоарендности. Например, поддерживаются таблицы-справочники, позволяющие сократить дублирование данных у разных арендаторов, а также изоляция арендаторов, которая гарантирует высокую производительность системы для крупных арендаторов.
Эти возможности позволяют использовать несколько компьютеров для масштабирования данных, предоставляя дополнительные вычислительные мощности, память и место на диске. Кроме того, использование одной схемы базы данных разными арендаторами позволяет эффективно распоряжаться аппаратными ресурсами и упрощает управление базами данных.
citus предоставляет следующие преимущества для многоарендных приложений:
Быстрое выполнение запросов всех арендаторов.
Логика сегментирования в рамках базы данных, а не приложения.
Хранение большего объёма данных на одном узле Postgres Pro.
Масштабирование с сохранением возможностей SQL.
Сохранение производительности при высококонкурентном доступе.
Быстрый анализ показателей по клиентской базе.
Масштабирование при увеличении числа клиентов.
Изолированное использование ресурсов крупными и небольшими арендаторами.
J.5.4.2. Анализ данных в реальном времени #
citus поддерживает запросы к большим наборам данных в реальном времени. Обычно такие запросы выполняют в быстро развивающихся системах событий или системах с данными временных рядов. Ниже приведены сценарии использования:
Аналитические информационные панели с высокой скоростью отклика.
Исследовательские запросы по событиям, происходящим в реальном времени.
Архивирование больших наборов данных и подготовка отчётов по ним.
Анализ сеансов с запросами воронкообразного, сегментного или когортного анализа.
citus распараллеливает выполнение запросов и обеспечивает линейное масштабирование с учётом числа рабочих баз данных кластера. Ниже представлены некоторые преимущества citus для приложений, работающих в реальном времени:
Сохранение скорости отклика при увеличении объёма данных.
Анализ новых событий и данных в реальном времени.
Распараллеливание SQL-запросов.
Масштабирование с сохранением возможностей SQL.
Сохранение производительности при высококонкурентном доступе.
Быстрые ответы на запросы с панели управления.
Использование единой базы данных вместо нескольких на разных узлах.
Поддержка большого числа типов данных и расширений Postgres Pro.
J.5.4.3. Микросервисы #
citus поддерживает сегментирование на основе схем, которое позволяет распределять обычные схемы баз данных по разным компьютерам. Такой метод сегментирования отлично согласуется с обычной микросервисной архитектурой, в рамках которой сервис полностью владеет хранилищем и у разных арендаторов не может быть одного и того же определения схемы.
Модель сегментирования на основе схем проще в настройке и позволяет создавать новую схему и задавать search_path в микросервисе.
Преимущества использования citus для микросервисов:
Возможность горизонтального масштабирования микросервисов.
Перенос стратегически важных корпоративных данных из микросервисов в обычные распределённые таблицы для анализа.
Эффективное использование аппаратных ресурсов за счёт балансировки микросервисов на разных компьютерах.
Изолирование шумных микросервисов на отдельных узлах.
Понятная модель сегментирования.
Быстрое внедрение.
J.5.4.4. Замечания об использовании #
citus расширяет функциональность Postgres Pro за счёт возможности создания распределённых баз данных, но не является решением, которое масштабирует любые рабочие нагрузки. Для эффективного использования citus следует продумывать используемые модели данных, инструменты и возможности SQL.
Разумно подойти к вопросу используемых инструментов и возможностей SQL следующим образом: если ваши задачи сопоставимы с описанными здесь сценариями использования, но какой-то инструмент или запрос не поддерживается, — попробуйте воспользоваться обходным решением, обычно оно есть.
J.5.4.5. Когда не рекомендуется использовать citus #
Для одних задач не нужны высокопроизводительные распределённые СУБД, в то время как для других передаётся большой поток данных между рабочими узлами. В первом случае citus не нужен, а во втором — обычно неэффективен. В следующих случаях citus может не подойти:
Не предполагается рост нагрузки, требующий использования более одного узла Postgres Pro Enterprise.
Данные анализируются без необходимости их передачи и выполнения запросов в реальном времени.
Для аналитических приложений, которым не требуется поддержка большого числа пользователей одновременно.
Выполняются запросы по извлечению, преобразованию и загрузке больших массивов данных в рамках ETL-процессов, а не запрашивается краткая сводная информация.
J.5.5. Краткое руководство #
J.5.5.1. Многоарендные приложения #
В этом руководстве используется пример набора данных для аналитики рекламы, чтобы продемонстрировать, как можно использовать citus для поддержки многоарендного приложения.
Примечание
В этом руководстве предполагается, что расширение citus уже установлено и работает. Если это не так, обратитесь к разделу Установка citus на одном узле, чтобы настроить его локально.
J.5.5.1.1. Модель данных с примером #
В этом разделе показано, как создать базу данных для приложения рекламной аналитики, которую компании могут использовать для просмотра, изменения, анализа и управления своими рекламными объявлениями и кампаниями (см. пример приложения). Такое приложение имеет характеристики типичной многоарендной системы. Это означает, что данные разных арендаторов хранятся в центральной базе данных, при этом у каждого арендатора изолированное представление своих данных.
Для представления этих данных будут использоваться три таблицы Postgres Pro. Для начала загрузите пример с данными для этих таблиц:
curl https://examples.citusdata.com/tutorial/companies.csv > companies.csv curl https://examples.citusdata.com/tutorial/campaigns.csv > campaigns.csv curl https://examples.citusdata.com/tutorial/ads.csv > ads.csv
J.5.5.1.2. Создание таблиц #
Сначала подключитесь к узлу-координатору citus с помощью psql.
Если citus установлен согласно описанию в разделе Установка citus на одном узле, узел-координатор запускается и слушает порт
9700.psql -p 9700
Создайте таблицы с помощью команды Postgres Pro
CREATE TABLE:CREATE TABLE companies ( id bigint NOT NULL, name text NOT NULL, image_url text, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL ); CREATE TABLE campaigns ( id bigint NOT NULL, company_id bigint NOT NULL, name text NOT NULL, cost_model text NOT NULL, state text NOT NULL, monthly_budget bigint, blacklisted_site_urls text[], created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL ); CREATE TABLE ads ( id bigint NOT NULL, company_id bigint NOT NULL, campaign_id bigint NOT NULL, name text NOT NULL, image_url text, target_url text, impressions_count bigint DEFAULT 0, clicks_count bigint DEFAULT 0, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL );Создайте индексы первичных ключей для каждой из таблиц по аналогии со стандартной процедурой в Postgres Pro:
ALTER TABLE companies ADD PRIMARY KEY (id); ALTER TABLE campaigns ADD PRIMARY KEY (id, company_id); ALTER TABLE ads ADD PRIMARY KEY (id, company_id);
J.5.5.1.3. Распределение таблиц и загрузка данных #
Теперь можно дать указание citus распределить созданные таблицы по различным узлам кластера. Для этого запустите функцию create_distributed_table и укажите таблицу для сегментирования и столбец, по которому оно будет выполняться. В приведённом ниже примере все таблицы сегментируются по столбцу company_id.
SELECT create_distributed_table('companies', 'id');
SELECT create_distributed_table('campaigns', 'company_id');
SELECT create_distributed_table('ads', 'company_id');Сегментирование всех таблиц по столбцу company_id позволяет citus совмещать таблицы и использовать в кластере первичные ключи, внешние ключи и сложные соединения.
Затем можно продолжить загрузку данных в таблицы с помощью стандартной команды psql \copy. Убедитесь, что указан правильный путь к файлу, если он был загружен не в стандартный каталог загрузки.
\copy companies from 'companies.csv' with csv \copy campaigns from 'campaigns.csv' with csv \copy ads from 'ads.csv' with csv
J.5.5.1.4. Выполнение запросов #
После завершения загрузки данных в таблицы можно выполнить несколько запросов. Расширение citus поддерживает стандартные команды INSERT, UPDATE и DELETE для вставки и изменения строк в распределённой таблице, что является одним из самых частых примеров взаимодействия пользователей с приложениями.
Например, можно добавить новую компанию, выполнив:
INSERT INTO companies VALUES (5000, 'New Company', 'https://randomurl/image.png', now(), now());
Чтобы удвоить бюджет всех кампаний предприятия, выполните команду UPDATE:
UPDATE campaigns SET monthly_budget = monthly_budget*2 WHERE company_id = 5;
Ещё один пример такой операции — запуск транзакций, охватывающих несколько таблиц. Например, можно удалить кампанию и одновременно все связанные с ней рекламные объявления, выполнив:
BEGIN; DELETE FROM campaigns WHERE id = 46 AND company_id = 5; DELETE FROM ads WHERE campaign_id = 46 AND company_id = 5; COMMIT;
Каждый оператор в транзакции инициирует двусторонний обмен данными между узлом-координатором и рабочими узлами в многоузловом кластере citus. Для рабочих нагрузок со множеством арендаторов транзакции выполняются более эффективно в распределённых функциях. Эффективность заметно повышается для крупных транзакций, но в качестве примера можно использовать и небольшую транзакцию выше.
Сначала создайте функцию, удаляющую кампании:
CREATE OR REPLACE FUNCTION delete_campaign(company_id int, campaign_id int) RETURNS void LANGUAGE plpgsql AS $fn$ BEGIN DELETE FROM campaigns WHERE id = $2 AND campaigns.company_id = $1; DELETE FROM ads WHERE ads.campaign_id = $2 AND ads.company_id = $1; END; $fn$;
Затем используйте функцию create_distributed_function, чтобы citus вызывал её непосредственно на рабочих узлах, а не на узле-координаторе (за исключением инсталляции citus с одним узлом, где всё запускается на узле-координаторе). Она вызывает функцию на любом рабочем узле, содержащем сегменты для таблиц
adsиcampaigns, которые соответствуют значениюcompany_id.SELECT create_distributed_function( 'delete_campaign(int, int)', 'company_id', colocate_with := 'campaigns' ); -- Можно вызывать функцию как обычно SELECT delete_campaign(5, 46);
Помимо транзакционных операций, также можно выполнять аналитические запросы с использованием стандартного языка SQL. Интересный запрос для предприятия — получить подробную информацию о своих рекламных кампаниях с максимальным бюджетом.
SELECT name, cost_model, state, monthly_budget FROM campaigns WHERE company_id = 5 ORDER BY monthly_budget DESC LIMIT 10;
Также можно выполнить запрос соединения по нескольким таблицам, чтобы просмотреть информацию о запущенных рекламных кампаниях c наибольшим количеством переходов и показов.
SELECT campaigns.id, campaigns.name, campaigns.monthly_budget, sum(impressions_count) AS total_impressions, sum(clicks_count) AS total_clicks FROM ads, campaigns WHERE ads.company_id = campaigns.company_id AND ads.campaign_id = campaigns.id AND campaigns.company_id = 5 AND campaigns.state = 'running' GROUP BY campaigns.id, campaigns.name, campaigns.monthly_budget ORDER BY total_impressions, total_clicks;
В приведённом выше руководстве показано, как использовать citus для простого многоарендного приложения. В качестве следующего шага можно ознакомиться с описанием многоарендных приложений, чтобы узнать, как моделировать собственные данные для множества арендаторов.
J.5.5.2. Анализ данных в реальном времени #
В этом руководстве показано, как использовать citus для приёма данных о событиях и выполнения аналитических запросов к этим данным в режиме реального времени. Для этого в примере используется образец набора данных событий GitHub.
Примечание
В этом руководстве предполагается, что расширение citus уже установлено и работает. Если это не так, обратитесь к разделу Установка citus на одном узле, чтобы настроить его локально.
J.5.5.2.1. Модель данных с примером #
В этом разделе показано, как создать базу данных для приложения, анализирующего данные в реальном времени. Это приложение будет вставлять большие объёмы данных о событиях и выполнять аналитические запросы к этим данным с задержкой менее секунды. В этом примере используется набор данных событий Github, включающий в себя все общедоступные события на GitHub, такие как commit, fork, new issue и comment.
Для представления этих данных используются две таблицы Postgres Pro. Для начала загрузите образцы данных для этих таблиц:
curl https://examples.citusdata.com/tutorial/users.csv > users.csv curl https://examples.citusdata.com/tutorial/events.csv > events.csv
J.5.5.2.2. Создание таблиц #
Для начала подключитесь к узлу-координатору citus с помощью psql.
Если citus установлен согласно описанию в разделе Установка citus на одном узле, узел-координатор запускается и слушает порт 9700.
psql -p 9700
Затем можно создать таблицы стандартной командой Postgres Pro CREATE TABLE:
CREATE TABLE github_events
(
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
user_id bigint,
org jsonb,
created_at timestamp
);
CREATE TABLE github_users
(
user_id bigint,
url text,
login text,
avatar_url text,
gravatar_id text,
display_login text
);Далее можно создать индексы данных о событиях так же, как это делается в Postgres Pro. В этом примере также показано, как создать индекс GIN, чтобы ускорить обращение к полям JSONB.
CREATE INDEX event_type_index ON github_events (event_type); CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops);
J.5.5.2.3. Распределение таблиц и загрузка данных #
Теперь можно дать указание citus распределить созданные таблицы по узлам кластера. Для этого используйте функцию create_distributed_table и укажите таблицу для сегментирования и столбец, по которому оно будет выполняться. В приведённом ниже примере все таблицы сегментированы по столбцу user_id.
SELECT create_distributed_table('github_users', 'user_id');
SELECT create_distributed_table('github_events', 'user_id');Сегментирование всех таблиц по столбцу user_id позволяет citus совмещать таблицы и эффективно использовать соединения и распределённые наборы группирования.
Затем можно продолжить загрузку данных в таблицы с помощью стандартной команды psql \copy. Убедитесь, что указан правильный путь к файлу, если он был загружен не в стандартный каталог загрузки.
\copy github_users from 'users.csv' with csv \copy github_events from 'events.csv' with csv
J.5.5.2.4. Выполнение запросов #
После загрузки данных в таблицы можно выполнить несколько запросов. Сначала проверьте, сколько пользователей содержится в распределённой базе данных.
SELECT count(*) FROM github_users;
Теперь проанализируйте события Github push в данных. Сначала вычислите количество событий commit в минуту, используя количество отдельных событий commit в каждом событии push.
SELECT date_trunc('minute', created_at) AS minute,
sum((payload->>'distinct_size')::int) AS num_commits
FROM github_events
WHERE event_type = 'PushEvent'
GROUP BY minute
ORDER BY minute;Также есть таблица пользователей. Можно соединить пользователей с событиями и найти десять пользователей, создавших наибольшее количество репозиториев.
SELECT login, count(*)
FROM github_events ge
JOIN github_users gu
ON ge.user_id = gu.user_id
WHERE event_type = 'CreateEvent' AND payload @> '{"ref_type": "repository"}'
GROUP BY login
ORDER BY count(*) DESC LIMIT 10;Расширение citus также поддерживает стандартные команды INSERT, UPDATE и DELETE для вставки и изменения данных. Например, можно изменить отображаемое имя пользователя, выполнив следующую команду:
UPDATE github_users SET display_login = 'no1youknow' WHERE user_id = 24305673;
В качестве следующего шага можно ознакомиться с описанием приложений для анализа данных в реальном времени, чтобы узнать, как моделировать собственные данные и использовать такие приложения.
J.5.5.3. Микросервисы #
В этом руководстве показано, как использовать citus в качестве сервера-хранилища для нескольких микросервисов, а также приведён пример настройки и работы в таком кластере.
Примечание
В этом руководстве предполагается, что расширение citus уже установлено и работает. Если это не так, обратитесь к разделу Установка citus на одном узле, чтобы настроить его локально.
J.5.5.3.1. Распределённые схемы #
Распределённые схемы можно перемещать внутри кластера citus. Система может перебалансировать их как единое целое между доступными узлами, что позволяет эффективно распределять ресурсы без ручного выделения.
По сути, каждый микросервис самостоятельно управляет своим слоем хранения, и не делается предположений относительно типов таблиц и данных, которые он будет создавать и хранить. Однако для каждого сервиса предоставляется отдельная схема и предполагается, что он использует отдельную роль для подключения к базе данных. При подключении пользователя его роль добавляется в начало search_path, поэтому если имя роли совпадает с именем схемы, не нужно вносить изменения в приложение для корректной установки search_path.
В примере используются три сервиса:
сервис
userсервис
timeсервис
ping
Для начала подключитесь к узлу-координатору citus с помощью psql.
Если citus установлен согласно описанию в разделе Установка citus на одном узле, узел-координатор запускается и слушает порт 9700.
psql -p 9700
Теперь можно создавать роли базы данных для каждого микросервиса:
CREATE USER user_service; CREATE USER time_service; CREATE USER ping_service;
В citus схему можно распределять двумя способами:
Вызвать функцию
citus_schema_distribute('вручную:имя_схемы')CREATE SCHEMA AUTHORIZATION user_service; CREATE SCHEMA AUTHORIZATION time_service; CREATE SCHEMA AUTHORIZATION ping_service; SELECT citus_schema_distribute('user_service'); SELECT citus_schema_distribute('time_service'); SELECT citus_schema_distribute('ping_service');Данный метод позволяет преобразовывать существующие обычные схемы в распределённые.
Примечание
Распределять можно только схемы, не содержащие распределённых таблиц и таблиц-справочников.
Включением параметра конфигурации enable_schema_based_sharding:
SET citus.enable_schema_based_sharding TO ON; CREATE SCHEMA AUTHORIZATION user_service; CREATE SCHEMA AUTHORIZATION time_service; CREATE SCHEMA AUTHORIZATION ping_service;
Этот параметр можно изменять для текущего сеанса или глобально в файле
postgresql.conf. Если для параметра установлено значениеON, все созданные схемы по умолчанию распределяются.
Можно вывести список схем, распределённых на данный момент:
SELECT * FROM citus_schemas;
schema_name | colocation_id | schema_size | schema_owner -------------+---------------+-------------+-------------- user_service | 5 | 0 bytes | user_service time_service | 6 | 0 bytes | time_service ping_service | 7 | 0 bytes | ping_service (3 rows)
J.5.5.3.2. Создание таблиц #
Теперь нужно подключить каждый микросервис к узлу-координатору citus. Можно использовать команду \c для замены пользователя в текущем сеансе psql.
\c citus user_service
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL
);\c citus time_service
CREATE TABLE query_details (
id SERIAL PRIMARY KEY,
ip_address INET NOT NULL,
query_time TIMESTAMP NOT NULL
);\c citus ping_service
CREATE TABLE ping_results (
id SERIAL PRIMARY KEY,
host VARCHAR(255) NOT NULL,
result TEXT NOT NULL
);J.5.5.3.3. Настройка микросервисов #
В данном руководстве используется очень простой набор микросервисов. Чтобы использовать их, клонируйте указанный публичный репозиторий:
git clone https://github.com/citusdata/citus-example-microservices.git
Репозиторий содержит следующие микросервисы: ping, time и user. Для каждого из них есть файл запуска app.py.
$ tree
.
├── LICENSE
├── README.md
├── ping
│ ├── app.py
│ ├── ping.sql
│ └── requirements.txt
├── time
│ ├── app.py
│ ├── requirements.txt
│ └── time.sql
└── user
├── app.py
├── requirements.txt
└── user.sqlПеред запуском микросервисов отредактируйте файлы user/app.py, ping/app.py и time/app.py, предоставляющие конфигурации подключений для кластера citus:
# Database configuration
db_config = {
'host': 'localhost',
'database': 'citus',
'user': 'ping_service',
'port': 9700
}После внесения изменений сохраните все отредактированные файлы и переходите к следующему этапу запуска микросервисов.
J.5.5.3.4. Запуск микросервисов #
Для каждого приложения перейдите в его каталог и запустите в отдельной среде
python.cd user pipenv install pipenv shell python app.py
Повторите указанные выше действия для микросервисов
timeиping, после чего можно будет использовать API.Создайте нескольких пользователей:
curl -X POST -H "Content-Type: application/json" -d '[ {"name": "John Doe", "email": "john@example.com"}, {"name": "Jane Smith", "email": "jane@example.com"}, {"name": "Mike Johnson", "email": "mike@example.com"}, {"name": "Emily Davis", "email": "emily@example.com"}, {"name": "David Wilson", "email": "david@example.com"}, {"name": "Sarah Thompson", "email": "sarah@example.com"}, {"name": "Alex Miller", "email": "alex@example.com"}, {"name": "Olivia Anderson", "email": "olivia@example.com"}, {"name": "Daniel Martin", "email": "daniel@example.com"}, {"name": "Sophia White", "email": "sophia@example.com"} ]' http://localhost:5000/usersВыведите список созданных пользователей:
curl http://localhost:5000/users
Запросите текущее время:
curl http://localhost:5001/current_time
Выполните
pingдля сайта example.com:curl -X POST -H "Content-Type: application/json" -d '{"host": "example.com"}' http://localhost:5002/ping
J.5.5.3.5. Исследование базы данных #
После вызова указанных выше функций API данные были сохранены, и можно проверить, соответствует ли представление citus_schemas ожидаемым результатам:
SELECT * FROM citus_schemas;
schema_name | colocation_id | schema_size | schema_owner --------------+---------------+-------------+-------------- user_service | 1 | 112 kB | user_service time_service | 2 | 32 kB | time_service ping_service | 3 | 32 kB | ping_service (3 rows)
Во время создания схем расширение citus не получало явных инструкций, на каком компьютере их создавать. Выбор происходил автоматически. Выполните следующий запрос, чтобы узнать, где находится каждая схема:
SELECT nodename,nodeport, table_name, pg_size_pretty(sum(shard_size)) FROM citus_shards GROUP BY nodename,nodeport, table_name;
nodename | nodeport | table_name | pg_size_pretty -----------+----------+----------------------------+---------------- localhost | 9701 | time_service.query_details | 32 kB localhost | 9702 | user_service.users | 112 kB localhost | 9702 | ping_service.ping_results | 32 kB
Видно, что микросервис time оказался на узле localhost:9701, а user и ping — на втором рабочем узле localhost:9702. Это всего лишь пример, и размерами данных здесь можно пренебречь, но лучше равномерно использовать пространство хранения между узлами. Разумнее разместить два меньших микросервиса time и ping на одном компьютере, а большой микросервис user — отдельно.
Это можно сделать, дав указание citus выполнить перебалансировку кластера по размерам дисков:
SELECT citus_rebalance_start();
NOTICE: Scheduled 1 moves as job 1
DETAIL: Rebalance scheduled as background job
HINT: To monitor progress, run: SELECT * FROM citus_rebalance_status();
citus_rebalance_start
-----------------------
1
(1 row)После выполнения проверьте структуру:
SELECT nodename,nodeport, table_name, pg_size_pretty(sum(shard_size)) FROM citus_shards GROUP BY nodename,nodeport, table_name;
nodename | nodeport | table_name | pg_size_pretty -----------+----------+----------------------------+---------------- localhost | 9701 | time_service.query_details | 32 kB localhost | 9701 | ping_service.ping_results | 32 kB localhost | 9702 | user_service.users | 112 kB (3 rows)
Ожидается, что схемы перенесены и кластер стал более сбалансированным. Эта операция прозрачна для приложений, поэтому нет необходимости их перезапускать, и они продолжают обслуживать запросы.
J.5.6. Сценарии использования #
J.5.6.1. Многоарендные приложения #
Если вы создаёте приложение типа программное обеспечение как услуга (SaaS), вероятно, в модель данных уже встроено понятие аренды. Обычно большая часть информации относится к арендаторам/клиентам/счетам, и таблицы базы данных отражают эту естественную связь.
Для приложений SaaS все данные одного арендатора могут храниться в едином экземпляре базы данных и оставаться изолированными и невидимыми для других арендаторов. Такой подход эффективен в трёх аспектах. Во-первых, улучшения приложения применяются для всех клиентов. Во-вторых, совместное использование базы данных арендаторами снижает нагрузку на оборудование. Наконец, гораздо проще управлять одной общей базой данных, чем отдельным сервером для каждого арендатора.
Однако у одного экземпляра реляционной базы данных традиционно возникают проблемы с масштабированием до объёма данных, необходимого для большого многоарендного приложения. Разработчикам приходилось отказываться от преимуществ реляционной модели, когда данные превышали ёмкость одного узла базы данных.
Расширение citus позволяет пользователям создавать многоарендные приложения так, как если бы они подключались к единой базе данных Postgres Pro, хотя на самом деле база данных представляет собой горизонтально масштабируемый кластер компьютеров. Клиентский код требует минимальных изменений и может продолжать использовать все возможности SQL.
В этом руководстве рассматривается пример многоарендного приложения и описывается, как моделировать его масштабируемость с помощью citus. Попутно обсуждаются типичные проблемы многоарендных приложений, такие как изоляция арендаторов от шумных соседей, масштабирование оборудования для размещения большего количества данных и хранение данных, которые различаются у разных арендаторов. Postgres Pro и citus предоставляют все инструменты, необходимые для решения этих задач.
J.5.6.1.1. Создание приложения для анализа рекламы #
В примере создаётся программная часть приложения, которое отслеживает эффективность онлайн-рекламы и предоставляет аналитическую панель. Такое решение отлично подходит для многоарендного приложения, поскольку запросы данных пользователями одновременно относятся только к одной компании (их собственной). Пример кода готового приложения доступен на Github.
Рассмотрим упрощённую схему этого приложения. Оно должно отслеживать несколько предприятий, каждое из которых проводит рекламные кампании. В кампаниях много объявлений, и с каждым объявлением связаны записи о переходах и показах.
Ниже представлен пример схемы. Позднее будут внесены незначительные изменения, позволяющие эффективно распределять и изолировать данные в распределённой среде.
CREATE TABLE companies ( id bigserial PRIMARY KEY, name text NOT NULL, image_url text, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL ); CREATE TABLE campaigns ( id bigserial PRIMARY KEY, company_id bigint REFERENCES companies (id), name text NOT NULL, cost_model text NOT NULL, state text NOT NULL, monthly_budget bigint, blacklisted_site_urls text[], created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL ); CREATE TABLE ads ( id bigserial PRIMARY KEY, campaign_id bigint REFERENCES campaigns (id), name text NOT NULL, image_url text, target_url text, impressions_count bigint DEFAULT 0, clicks_count bigint DEFAULT 0, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL ); CREATE TABLE clicks ( id bigserial PRIMARY KEY, ad_id bigint REFERENCES ads (id), clicked_at timestamp without time zone NOT NULL, site_url text NOT NULL, cost_per_click_usd numeric(20,10), user_ip inet NOT NULL, user_data jsonb NOT NULL ); CREATE TABLE impressions ( id bigserial PRIMARY KEY, ad_id bigint REFERENCES ads (id), seen_at timestamp without time zone NOT NULL, site_url text NOT NULL, cost_per_impression_usd numeric(20,10), user_ip inet NOT NULL, user_data jsonb NOT NULL );
Можно внести в схему изменения, которые повысят её производительность в распределённой среде, такой как citus. Чтобы понять, как это сделать, нужно ознакомиться с тем, как расширение распределяет данные и выполняет запросы.
J.5.6.1.2. Масштабирование реляционной модели данных #
Реляционная модель данных отлично подходит для приложений. Она защищает целостность данных, обеспечивает гибкость запросов и учитывает изменение данных. Традиционно считалось, что реляционные базы данных не могут масштабироваться до рабочих объёмов больших SaaS-приложений. Чтобы достичь такого размера, разработчикам приходилось прибегать к базам данных NoSQL или наборам серверных служб.
С помощью citus можно сохранить свою модель данных и сделать её масштабируемой. Расширение отображается в приложениях как единая база данных Postgres Pro, но внутри него запросы направляются на настраиваемое количество физических серверов (узлов), которые могут обрабатывать запросы параллельно.
У многоарендных приложений есть удобная особенность: каждый запрос обычно содержит информацию только об одном арендаторе, а не о нескольких. Например, когда продавец ищет информацию о потенциальных клиентах в CRM, результаты поиска относятся только к его работодателю; потенциальные клиенты и примечания других предприятий не включаются.
Поскольку запросы приложений ограничены одним арендатором, например магазином или предприятием, одним из подходов к ускорению запросов многоарендных приложений является хранение всех данных для выбранного клиента на одном узле. Это сводит к минимуму сетевые издержки между узлами и позволяет citus эффективно поддерживать все соединения, ограничения ключей и транзакции пользовательского приложения. Благодаря такому хранению данные можно масштабировать на несколько узлов без необходимости полностью переписывать или перепроектировать приложение. За подробностями обратитесь к рисунку.
Рисунок J.1. Диаграмма маршрутизации рекламы со множеством арендаторов
В citus это можно сделать, убедившись, что в каждой таблице схемы есть столбец, позволяющий чётко обозначить, какому арендатору какие строки принадлежат. В приложении для аналитики рекламы арендаторами являются предприятия, поэтому необходимо убедиться, что во всех таблицах есть столбец company_id.
Можно дать указание citus использовать этот столбец для чтения и записи строк в один и тот же узел, если строки помечены для одного и того же предприятия. В терминологии citus company_id — это столбец распределения, который подробно описан в разделе Выбор столбца распределения.
J.5.6.1.3. Подготовка таблиц и заполнение данными #
В предыдущем разделе был описан корректный столбец распределения для многоарендного приложения: company_id. Даже в базе данных с одним компьютером может быть полезно денормализовать таблицы с добавлением company_id, например в целях обеспечения безопасности на уровне строк или дополнительной индексации. Ещё одним преимуществом является то, что включение дополнительного столбца также помогает при масштабировании на нескольких компьютерах.
Созданная в примере схема использует отдельный столбец id в качестве первичного ключа для каждой таблицы. Для citus требуется, чтобы ограничения первичных и внешних ключей содержали столбец распределения. Такое требование усиливает эффективность этих ограничений в распределённой среде, поскольку для их обеспечения необходимо проверить только один узел.
В SQL такое требование означает объединение первичных и внешних ключей путём включения company_id. Это применимо и для сценария с несколькими арендаторами, поскольку в этом случае необходимо обеспечить уникальность для каждого арендатора.
Подводя итог, ниже представлены изменения для подготовки таблиц к распределению по столбцу company_id.
CREATE TABLE companies (
id bigserial PRIMARY KEY,
name text NOT NULL,
image_url text,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
);
CREATE TABLE campaigns (
id bigserial, -- было: PRIMARY KEY
company_id bigint REFERENCES companies (id),
name text NOT NULL,
cost_model text NOT NULL,
state text NOT NULL,
monthly_budget bigint,
blacklisted_site_urls text[],
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
PRIMARY KEY (company_id, id) -- добавлено
);
CREATE TABLE ads (
id bigserial, -- было: PRIMARY KEY
company_id bigint, -- добавлено
campaign_id bigint, -- было: REFERENCES campaigns (id)
name text NOT NULL,
image_url text,
target_url text,
impressions_count bigint DEFAULT 0,
clicks_count bigint DEFAULT 0,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
PRIMARY KEY (company_id, id), -- добавлено
FOREIGN KEY (company_id, campaign_id) -- добавлено
REFERENCES campaigns (company_id, id)
);
CREATE TABLE clicks (
id bigserial, -- было: PRIMARY KEY
company_id bigint, -- добавлено
ad_id bigint, -- было: REFERENCES ads (id),
clicked_at timestamp without time zone NOT NULL,
site_url text NOT NULL,
cost_per_click_usd numeric(20,10),
user_ip inet NOT NULL,
user_data jsonb NOT NULL,
PRIMARY KEY (company_id, id), -- добавлено
FOREIGN KEY (company_id, ad_id) -- добавлено
REFERENCES ads (company_id, id)
);
CREATE TABLE impressions (
id bigserial, -- было: PRIMARY KEY
company_id bigint, -- добавлено
ad_id bigint, -- было: REFERENCES ads (id),
seen_at timestamp without time zone NOT NULL,
site_url text NOT NULL,
cost_per_impression_usd numeric(20,10),
user_ip inet NOT NULL,
user_data jsonb NOT NULL,
PRIMARY KEY (company_id, id), -- добавлено
FOREIGN KEY (company_id, ad_id) -- добавлено
REFERENCES ads (company_id, id)
);Более подробно миграция из пользовательской модели данных описана в разделе Определение стратегии распределения.
J.5.6.1.3.1. Практический пример #
Примечание
Это руководство составлено таким образом, чтобы пользователь мог следовать ему в собственной базе данных citus. В этом руководстве предполагается, что расширение уже установлено и работает. Если это не так, обратитесь к разделу Установка citus на одном узле, чтобы настроить расширение локально.
На этом этапе можно продолжить работу в собственном кластере citus, загрузив и выполнив код SQL для создания схемы. Как только схема будет готова, можно дать указание citus создать сегменты для рабочих узлов. Выполните на узле-координаторе:
SELECT create_distributed_table('companies', 'id'); SELECT create_distributed_table('campaigns', 'company_id'); SELECT create_distributed_table('ads', 'company_id'); SELECT create_distributed_table('clicks', 'company_id'); SELECT create_distributed_table('impressions', 'company_id');Функция create_distributed_table сообщает citus, что таблица должна быть распределена между узлами и что будущие входящие запросы к таким таблицам следует планировать для распределённого выполнения. Функция также создаёт сегменты для таблицы на рабочих узлах — низкоуровневые единицы хранения данных, которые citus использует для передачи данных узлам.
Следующий шаг — загрузка примера данных в кластер из командной строки:
# Загрузка и передача наборов данных из оболочки for dataset in companies campaigns ads clicks impressions geo_ips; do curl -O https://examples.citusdata.com/mt_ref_arch/${dataset}.csv doneПоскольку citus является расширением Postgres Pro, в нём есть поддержка массовой загрузки с помощью команды
/copy. Используйте её для приёма загруженных данных и проверьте, что указан правильный путь к файлу при загрузке в нестандартный каталог. В psql выполните:\copy companies from 'companies.csv' with csv \copy campaigns from 'campaigns.csv' with csv \copy ads from 'ads.csv' with csv \copy clicks from 'clicks.csv' with csv \copy impressions from 'impressions.csv' with csv
J.5.6.1.4. Интеграция приложений #
После того как будет внесено небольшое изменение схемы, описанное ранее, приложение можно будет масштабировать с минимальными усилиями. Просто подключите приложение к citus и позвольте базе данных позаботиться о скорости выполнения запросов и безопасности данных.
Любые запросы приложений или операторы изменений, которые включают фильтр по company_id, будут продолжать работать в прежнем виде. Как упоминалось ранее, фильтры такого типа часто встречаются в многоарендных приложениях. При использовании объектно-реляционного преобразователя (ORM) можно распознавать эти запросы с помощью таких методов, как where или filter.
ActiveRecord:
Impression.where(company_id: 5).count
Django:
Impression.objects.filter(company_id=5).count()
По сути, если результирующий код SQL, выполняемый в базе данных, содержит предложение WHERE company_id = :value для каждой таблицы (включая таблицы в запросах JOIN), citus распознаёт, что запрос должен быть направлен на один узел, и выполнит его там без изменений. Это гарантирует доступность всех функций SQL. В конце концов, узел представляет собой обычный сервер Postgres Pro.
Кроме того, чтобы упростить процедуру, можно использовать библиотеку activerecord-multi-tenant для Ruby on Rails или django-multitenant для Django, которая автоматически добавит эти фильтры для всех пользовательских запросов, даже самых сложных. Ознакомьтесь с руководствами по миграции для Ruby on Rails и Django.
Данное руководство не привязано к определённой платформе, поэтому некоторые возможности citus показаны с использованием языка SQL. На другом языке эти операторы могут выглядеть иначе.
Ниже показан пример простого запроса и изменения, выполненных для одного арендатора.
-- Кампании с наибольшим бюджетом SELECT name, cost_model, state, monthly_budget FROM campaigns WHERE company_id = 5 ORDER BY monthly_budget DESC LIMIT 10; -- Удвоение бюджета UPDATE campaigns SET monthly_budget = monthly_budget*2 WHERE company_id = 5;
Общей проблемой для пользователей, масштабирующих приложения с помощью баз данных NoSQL, является отсутствие транзакций и соединений. Однако в citus транзакции работают как обычно:
-- Транзакция по перераспределению бюджета кампании BEGIN; UPDATE campaigns SET monthly_budget = monthly_budget + 1000 WHERE company_id = 5 AND id = 40; UPDATE campaigns SET monthly_budget = monthly_budget - 1000 WHERE company_id = 5 AND id = 41; COMMIT;
В качестве ещё одного примера поддержки SQL показан запрос с агрегатными и оконными функциями, который работает в citus так же, как и в Postgres Pro. Запрос упорядочивает объявления в каждой кампании по количеству показов.
SELECT a.campaign_id,
RANK() OVER (
PARTITION BY a.campaign_id
ORDER BY a.campaign_id, count(*) desc
), count(*) as n_impressions, a.id
FROM ads as a
JOIN impressions as i
ON i.company_id = a.company_id
AND i.ad_id = a.id
WHERE a.company_id = 5
GROUP BY a.campaign_id, a.id
ORDER BY a.campaign_id, n_impressions desc;Когда запросы ограничены одним арендатором, команды INSERT, UPDATE, DELETE, сложные команды SQL и транзакции работают как обычно.
J.5.6.1.5. Обмен данными между арендаторами #
До сих пор все таблицы распределялись по company_id, но существуют данные, которые могут использоваться всеми арендаторами и не «принадлежат» какому-либо конкретному арендатору. Например, все предприятия, использующие рекламную платформу из примера, могут получить географическую информацию о своей аудитории на основе IP-адресов. В базе данных одного компьютера это можно сделать с помощью справочной таблицы с geo-IP, как показано ниже. (Обратите внимание, что в настоящей таблице, вероятно, будет использоваться PostGIS.)
CREATE TABLE geo_ips (
addrs cidr NOT NULL PRIMARY KEY,
latlon point NOT NULL
CHECK (-90 <= latlon[0] AND latlon[0] <= 90 AND
-180 <= latlon[1] AND latlon[1] <= 180)
);
CREATE INDEX ON geo_ips USING gist (addrs inet_ops);Чтобы эффективно использовать эту таблицу в распределённой системе, нужно найти способ совместить таблицу geo_ips с переходами не для одного, а для каждого предприятия. То есть, во время запроса не требуется сетевой трафик. В citus для выполнения этой задачи можно назначить geo_ips как справочную таблицу.
-- Создание синхронизированных копий geo_ips на всех рабочих узлах
SELECT create_reference_table('geo_ips');Таблицы-справочники реплицируются на все рабочие узлы, и citus автоматически синхронизирует их при изменении. Обратите внимание, что вызывается функция create_reference_table, а не create_distributed_table.
После указания geo_ips в качестве справочной таблицы, загрузите в неё пример данных:
\copy geo_ips from 'geo_ips.csv' with csv
Теперь соединение по количеству переходов может выполняться эффективно благодаря этой таблице. Например, можно сделать запрос о местоположении каждого, кто нажал на объявление 290.
SELECT c.id, clicked_at, latlon FROM geo_ips, clicks c WHERE addrs >> c.user_ip AND c.company_id = 5 AND c.ad_id = 290;
J.5.6.1.6. Внесение изменений в схему в реальном времени #
Ещё одна сложность при работе с многоарендными системами — синхронизация схем для всех арендаторов. Любое изменение схемы должно согласованно отражаться у всех арендаторов. В citus можно просто использовать стандартные DDL-команды Postgres Pro для изменения схемы таблиц, и расширение будет транслировать их с узла координатора на рабочие узлы, используя протокол двухфазной фиксации.
Например, в рекламных объявлениях в этом приложении могут использоваться сопроводительные надписи. Можно добавить столбец в таблицу, выполнив стандартный SQL-код на узле-координаторе:
ALTER TABLE ads ADD COLUMN caption text;
Это изменение попадёт на все рабочие узлы. После завершения этой команды кластер citus будет принимать запросы, которые читают или записывают данные в новый столбец caption.
За более подробным описанием трансляции DDL-команд по кластеру обратитесь к разделу Изменение таблиц.
J.5.6.1.7. Разные данные у разных арендаторов #
Все арендаторы используют общую схему и аппаратную инфраструктуру, поэтому возникает вопрос: как быть с арендаторами, которые хотят хранить информацию, нужную только им? Например, один из арендаторов, использующих рекламную базу данных, может захотеть хранить в своём приложении информацию о cookie-файлах для отслеживания переходов, а другого арендатора может больше интересовать информация об агентах браузера. Традиционно базы данных, использующие подход с общей схемой для многоарендности, прибегали к созданию фиксированного числа предварительно выделенных «пользовательских» столбцов или к использованию внешних «таблиц расширения». Однако Postgres Pro предоставляет гораздо более простой способ благодаря своим неструктурированным типам столбцов, в частности, JSONB.
Обратите внимание, что в нашей схеме уже есть поле user_data типа JSONB, расположенное в clicks. Каждый арендатор может использовать его для гибкого хранения.
Предположим, предприятие номер пять добавляет в поле информацию, позволяющую отслеживать, использует ли пользователь мобильное устройство. Предприятие может запросить информацию о том, кто совершает больше переходов: пользователи мобильных или стационарных устройств:
SELECT user_data->>'is_mobile' AS is_mobile, count(*) AS count FROM clicks WHERE company_id = 5 GROUP BY user_data->>'is_mobile' ORDER BY count DESC;
Администратор базы данных даже может создать частичный индекс, чтобы повысить скорость обработки шаблонов запросов отдельного арендатора. Ниже представлен один из вариантов улучшения фильтров в компании с company_id = 5 для переходов от пользователей на мобильных устройствах:
CREATE INDEX click_user_data_is_mobile ON clicks ((user_data->>'is_mobile')) WHERE company_id = 5;
Кроме того, в Postgres Pro поддерживаются индексы GIN для JSONB. Создание индекса GIN для столбца JSONB приведёт к созданию индекса для каждого ключа и значения в этом документе JSON. Это ускоряет работу ряда операторов JSONB, таких как ?, ?| и ?&.
CREATE INDEX click_user_data ON clicks USING gin (user_data); -- эта часть ускоряет запросы "у каких переходов -- в user_data есть ключ is_mobile?" SELECT id FROM clicks WHERE user_data ? 'is_mobile' AND company_id = 5;
J.5.6.1.8. Масштабирование аппаратных ресурсов #
Многоарендные базы данных должны проектироваться с учётом масштабирования из-за роста бизнеса или желания арендаторов хранить больше данных. Для масштабирования citus не нужно вносить изменения или останавливать приложение — достаточно добавить новые устройства.
Возможность перебалансировки данных в кластере citus позволяет увеличивать размер данных или количество клиентов, а также повышать производительность по мере необходимости. Добавление новых компьютеров позволяет хранить данные в памяти, даже если их объём намного больше, чем ёмкость одного компьютера.
Кроме того, если объём данных увеличивается только для нескольких крупных арендаторов, можно изолировать именно этих арендаторов на отдельных узлах для повышения производительности.
Чтобы масштабировать кластер citus, сначала добавьте в него новый рабочий узел с помощью функции citus_add_node.
Добавленный узел доступен в системе. Но сейчас на нём не хранятся никакие арендаторы, и citus пока не будет выполнять какие-либо запросы. Чтобы перенести существующие данные, укажите citus выполнить перебалансировку. Эта операция перемещает пакеты строк (сегменты) между активными узлами, чтобы сравнять их объёмы данных.
SELECT citus_rebalance_start();
Приложения не будут простаивать во время перебалансировки сегментов. Читающие запросы выполняются бесперебойно, а записывающие блокируются только тогда, когда затрагивают сегменты, находящиеся в работе. В citus во время перебалансировки блокируется только запись в сегменты, но не чтение.
J.5.6.1.9. Работа с крупными арендаторами #
В предыдущем разделе описан универсальный способ масштабирования кластера по мере увеличения числа арендаторов. Однако у пользователей часто возникают два вопроса: что произойдёт с их крупнейшим арендатором, если он станет слишком большим, и как влияет на производительность размещение крупного арендатора на одном рабочем узле с более мелкими.
Что касается первого вопроса, исследование данных крупных SaaS-сайтов показывает, что с ростом числа арендаторов размер данных арендаторов начинает следовать закону Ципфа. За подробностями обратитесь к рисунку ниже.
Рисунок J.2. Распределение по закону Ципфа
Например, в базе данных из 100 арендаторов прогнозируется, что на долю крупнейшего арендатора будет приходиться около 20% данных. В более реалистичном примере для крупной SaaS-компании с 10 000 арендаторов на долю крупнейшего из них будет приходиться около 2% данных. Даже при объёме данных 10 ТБ самому крупному арендатору потребуется 200 ГБ, для чего должно хватить всего одного узла.
Другой вопрос касается производительности, когда крупные и мелкие арендаторы находятся на одном узле. Стандартная перебалансировка сегментов улучшает общую производительность, но не всегда улучшает объединение крупных и мелких арендаторов. Перебалансировщик только распределяет сегменты, чтобы уравнять использование хранилища на узлах, но не проверяет, какие арендаторы расположены на каждом сегменте.
Чтобы улучшить распределение ресурсов и гарантировать качество обслуживания арендаторов, стоит переместить крупных арендаторов на выделенные узлы. Инструменты расширения citus дают такую возможность.
Представим очень крупное предприятие с company_id=5. Данные для этого арендатора можно изолировать в два этапа. Ниже представлены команды, подробное описание которых можно найти в разделе Изоляция арендаторов.
Сначала изолируйте данные арендатора в выделенном сегменте, пригодном для перемещения. Параметр CASCADE также применяет это изменение к остальным таблицам, распределённым по company_id.
SELECT isolate_tenant_to_new_shard( 'companies', 5, 'CASCADE' );
В результате будет получен идентификатор сегмента, выделенного для хранения company_id=5:
┌─────────────────────────────┐ │ isolate_tenant_to_new_shard │ ├─────────────────────────────┤ │ 102240 │ └─────────────────────────────┘
Затем переместите данные по сети на новый выделенный узел. Создайте новый узел, как описано в предыдущем разделе. Обратите внимание на адрес узла.
-- Найдите узел, на котором сейчас находится новый сегмент
SELECT nodename, nodeport
FROM pg_dist_placement AS placement,
pg_dist_node AS node
WHERE placement.groupid = node.groupid
AND node.noderole = 'primary'
AND shardid = 102240;
-- Переместите сегмент на выбранный рабочий узел (другие сегменты, созданные
-- с параметром CASCADE также будут перемещены)
-- Обратите внимание, что на для всех узлов следует задать для wal_level значение >= logical,
-- чтобы использовать citus_move_shard_placement.
-- Также необходимо перезапустить кластер после изменения wal_level в
-- файлах postgresql.conf.
SELECT citus_move_shard_placement(
102240,
'source_host', source_port,
'dest_host', dest_port);Подтвердите перемещение сегмента, повторно отправив запрос таблице pg_dist_placement.
J.5.6.1.10. Дальнейшие действия #
Теперь вы знаете, как использовать citus для обеспечения масштабируемости многоарендного приложения. Если схема уже существует и её нужно перенести для citus, см. раздел Миграция существующего приложения.
Чтобы настроить клиентское приложение, например Ruby on Rails или Django, используйте руководство по миграции Ruby on Rails или Django.
J.5.6.2. Панели для анализа в реальном времени #
Расширение citus позволяет выполнять запросы в реальном времени к большим наборам данных. Одна из рабочих задач citus заключается в создании информационных панелей с данными о событиях в режиме реального времени.
Рассмотрим пример для поставщика облачных услуг, помогающего другим компаниям отслеживать HTTP-трафик. Каждый раз, когда один из клиентов получает HTTP-запрос, в журнале службы создаётся запись. Необходимо принять все эти записи и создать панель HTTP-аналитики, которая предоставит клиентам нужную информацию, например количество HTTP-ошибок на обслуживаемых ими сайтах. Эти данные должны отображаться с минимальной задержкой, чтобы клиенты могли оперативно устранять проблемы со своими сайтами. Также важно, чтобы на информационной панели отображались графики исторических тенденций.
Рассмотрим другой пример: предприятие создаёт рекламную сеть и хочет показывать клиентам рейтинг переходов по ссылкам в их кампаниях. В этом примере задержка также имеет решающее значение, объём необработанных данных также велик, и важны как исторические, так и текущие данные.
В этом разделе рассмотрена разработка приложения для первого примера, но созданная архитектура так же подходит для работы второго и многих других вариантов использования.
J.5.6.2.1. Модель данных #
Данные в примере представляют собой неизменяемый поток данных журнала. Они будут вставляться непосредственно в citus, но часто эти данные сначала перенаправляются через платформу Kafka или её аналог. Это упрощает предварительное агрегирование, когда объёмы данных становятся неконтролируемо большими.
В примере используется простая схема для приёма данных о событиях HTTP. Эта схема необходима для демонстрации общей архитектуры, в реальной системе могут быть дополнительные столбцы.
-- Выполните на узле-координаторе
CREATE TABLE http_request (
site_id INT,
ingest_time TIMESTAMPTZ DEFAULT now(),
url TEXT,
request_country TEXT,
ip_address TEXT,
status_code INT,
response_time_msec INT
);
SELECT create_distributed_table('http_request', 'site_id');Когда вызывается функция create_distributed_table, citus получает указание распределить по хешу http_request, используя столбец site_id. Это означает, что все данные конкретного сайта будут храниться в одном сегменте.
Пользовательские функции используют конфигурацию со стандартным количеством сегментов. Рекомендуется использовать в 2–4 раза больше сегментов, чем количество ядер ЦП в кластере. Такое количество сегментов позволяет перебалансировать данные в кластере после добавления новых рабочих узлов.
Теперь система готова принимать данные и обслуживать запросы. Запустите следующий цикл в консоли psql в фоновом режиме, продолжая выполнять другие команды из этой статьи. Этот цикл генерирует искусственные данные каждые 1-2 секунды.
DO $$
BEGIN LOOP
INSERT INTO http_request (
site_id, ingest_time, url, request_country,
ip_address, status_code, response_time_msec
) VALUES (
trunc(random()*32), clock_timestamp(),
concat('http://example.com/', md5(random()::text)),
('{China,India,USA,Indonesia}'::text[])[ceil(random()*4)],
concat(
trunc(random()*250 + 2), '.',
trunc(random()*250 + 2), '.',
trunc(random()*250 + 2), '.',
trunc(random()*250 + 2)
)::inet,
('{200,404}'::int[])[ceil(random()*2)],
5+trunc(random()*150)
);
COMMIT;
PERFORM pg_sleep(random() * 0.25);
END LOOP;
END $$;После наполнения данными можно запускать запросы для панели, например:
SELECT
site_id,
date_trunc('minute', ingest_time) as minute,
COUNT(1) AS request_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 1 ELSE 0 END) as success_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 0 ELSE 1 END) as error_count,
SUM(response_time_msec) / COUNT(1) AS average_response_time_msec
FROM http_request
WHERE date_trunc('minute', ingest_time) > now() - '5 minutes'::interval
GROUP BY site_id, minute
ORDER BY minute ASC;Описанный выше вариант работает, но у него есть два недостатка:
Панель HTTP-аналитики должна проходить каждую строку каждый раз, когда необходимо создать график. Например, если клиенты интересуются тенденциями за последний год, запросы будут агрегировать каждую строку за последний год с нуля.
Затраты на хранение будут расти пропорционально скорости заполнения и длине запрашиваемой истории. На практике может потребоваться хранить необработанные данные за более короткий период времени (один месяц) и просматривать исторические графики за более длительный период времени (годы).
J.5.6.2.2. Свёртки #
Оба недостатка можно преодолеть путём группирования данных в предварительно агрегированную форму. Здесь необработанные данные группируются в таблицу, в которой хранятся сводные данные по минутным интервалам. В производственной системе, вероятно, также понадобятся интервалы в 1 час и 1 день, каждый из которых соответствует уровням масштабирования на информационной панели. Если пользователю нужны данные за последний месяц, панель может просто прочитать и составить график значений за каждый из последних 30 дней.
CREATE TABLE http_request_1min (
site_id INT,
ingest_time TIMESTAMPTZ, -- минута, представленная данной строкой
error_count INT,
success_count INT,
request_count INT,
average_response_time_msec INT,
CHECK (request_count = error_count + success_count),
CHECK (ingest_time = date_trunc('minute', ingest_time))
);
SELECT create_distributed_table('http_request_1min', 'site_id');
CREATE INDEX http_request_1min_idx ON http_request_1min (site_id, ingest_time);Этот блок кода очень похож на предыдущий. Самое главное: в нём также сегментируется site_id и используется та же стандартная конфигурация для количества сегментов. Поскольку все три этих элемента совпадают, сегменты http_request и сегменты http_request_1min полностью соответствуют друг другу, и citus разместит соответствующие сегменты на одном рабочем узле. Этот процесс называется совмещением. Он ускоряет выполнение соединений и позволяет использовать свёртки. За более подробным описанием обратитесь к рисунку.
Рисунок J.3. Диаграмма совмещения
Чтобы заполнить http_request_1min, нужно периодически запускать команду INSERT INTO SELECT. Это возможно, поскольку таблицы совмещены. Следующая функция упрощает свёртку.
-- Однострочная таблица для хранения момента последней свёртки
CREATE TABLE latest_rollup (
minute timestamptz PRIMARY KEY,
-- Параметр "minute" должен усекать с точностью до минут
CHECK (minute = date_trunc('minute', minute))
);
-- Инициализация по очень старой дате
INSERT INTO latest_rollup VALUES ('10-10-1901');
-- Функция для выполнения свёртки
CREATE OR REPLACE FUNCTION rollup_http_request() RETURNS void AS $$
DECLARE
curr_rollup_time timestamptz := date_trunc('minute', now() - interval '1 minute');
last_rollup_time timestamptz := minute from latest_rollup;
BEGIN
INSERT INTO http_request_1min (
site_id, ingest_time, request_count,
success_count, error_count, average_response_time_msec
) SELECT
site_id,
date_trunc('minute', ingest_time),
COUNT(1) as request_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 1 ELSE 0 END) as success_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 0 ELSE 1 END) as error_count,
SUM(response_time_msec) / COUNT(1) AS average_response_time_msec
FROM http_request
-- Свёртка только новых данных с момента last_rollup_time
WHERE ingest_time <@ tstzrange(last_rollup_time, curr_rollup_time, '(]')
GROUP BY 1, 2;
-- Обновление значения в latest_rollup, чтобы при следующем запуске
-- свёртки она применялась к данным новее, чем curr_rollup_time
UPDATE latest_rollup SET minute = curr_rollup_time;
END;
$$ LANGUAGE plpgsql;Примечание
Вышеуказанная функция должна вызываться каждую минуту. Для этого добавьте запись crontab на узле-координаторе:
* * * * * psql -c 'SELECT rollup_http_request();'
В качестве альтернативы можно использовать приложение pg_cron (или его аналог), позволяющее составлять расписание повторяющихся запросов прямо из БД.
Уже использовавшийся запрос информационной панели теперь выглядит гораздо лучше:
SELECT site_id, ingest_time as minute, request_count,
success_count, error_count, average_response_time_msec
FROM http_request_1min
WHERE ingest_time > date_trunc('minute', now()) - '5 minutes'::interval;J.5.6.2.3. Срок годности данных #
Свёртки ускоряют запросы, но по-прежнему необходимо избавляться от старых данных, чтобы избежать неограниченного роста стоимости хранения. Нужно установить срок хранения данных для каждого уровня детализации, и использовать стандартные запросы для удаления просроченных данных. В следующем примере необработанные данные хранятся один день, а поминутные агрегации — один месяц:
DELETE FROM http_request WHERE ingest_time < now() - interval '1 day'; DELETE FROM http_request_1min WHERE ingest_time < now() - interval '1 month';
В производственной среде можно обернуть эти запросы в функцию и вызывать её каждую минуту в задании cron.
Истечение срока годности данных может ускориться, если использовать секционирование таблиц по диапазонам помимо распределения по хешу в citus. Подробный пример представлен в разделе Данные временных рядов.
В данной главе были рассмотрены основы архитектуры, которая принимает HTTP-события и объединяет эти события в предварительно агрегированную форму. Таким образом можно как хранить необработанные события, так и использовать их для аналитических панелей с запросами, выполняющимися за доли секунды.
В следующих разделах более подробно рассматривается базовая архитектура и решение часто возникающих вопросов.
J.5.6.2.4. Приблизительное количество уникальных значений #
Распространённый вопрос в HTTP-аналитике связан с приблизительным подсчётом уникальных значений: например сколько уникальных посетителей было на сайте за последний месяц. Чтобы ответить на этот вопрос точно, необходимо хранить в таблицах свёрток список всех замеченных посетителей, а это слишком большой объём данных. Использование приблизительного количества требует гораздо меньших ресурсов.
Тип данных, называемый HyperLogLog или hll, может дать приблизительный ответ на запрос. При этом, чтобы определить приблизительное количество уникальных элементов в наборе требуется совсем немного места. Точность определения можно регулировать. Используя всего 1280 байт можно учесть до десятков миллиардов уникальных посетителей с погрешностью не более 2,2%.
Аналогичная проблема возникает при необходимости выполнить глобальный запрос, например, узнать количество уникальных IP-адресов, посетивших любой из сайтов клиента за последний месяц. Без hll этот запрос предполагает отправку списков IP-адресов с рабочих узлов координатору для исключения дубликатов. Это одновременно и большого сетевой трафик, и большой объём вычислений. При использовании hll можно значительно повысить скорость запросов.
Установите расширение hll, инструкции для которого можно найти в репозитории GitHub, и включите его:
CREATE EXTENSION hll;
Теперь можно отслеживать IP-адреса в свёртке с помощью расширения hll. Сначала добавьте столбец в таблицу свёрток.
ALTER TABLE http_request_1min ADD COLUMN distinct_ip_addresses hll;
Затем используйте пользовательскую агрегацию для заполнения столбца. Для этого добавьте её в запрос в функции свёртки:
@@ -1,10 +1,12 @@
INSERT INTO http_request_1min (
site_id, ingest_time, request_count,
success_count, error_count, average_response_time_msec
+ , distinct_ip_addresses
) SELECT
site_id,
date_trunc('minute', ingest_time),
COUNT(1) as request_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 1 ELSE 0 END) as success_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 0 ELSE 1 END) as error_count,
SUM(response_time_msec) / COUNT(1) AS average_response_time_msec
+ , hll_add_agg(hll_hash_text(ip_address)) AS distinct_ip_addresses
FROM http_requestЗапросы информационной панели немного сложнее: нужно считывать определённое количество IP-адресов, вызывая функцию hll_cardinality:
SELECT site_id, ingest_time as minute, request_count,
success_count, error_count, average_response_time_msec,
hll_cardinality(distinct_ip_addresses) AS distinct_ip_address_count
FROM http_request_1min
WHERE ingest_time > date_trunc('minute', now()) - interval '5 minutes';Тип данных hll не просто позволяет ускорить работу, но и добавляет ранее недоступные возможности. Допустим, были выполнены свёртки, но вместо использования hll сохранилось точное количество уникальных значений. Такой способ тоже работает, но при этом невозможно ответить на такие запросы, как «сколько отдельных сеансов, для которых были удалены необработанные данные, было в течение этого недельного периода в прошлом?».
С hll ответить на такой запрос легко. Можно вычислить количество различных IP-адресов за определённый период времени с помощью следующего запроса:
SELECT hll_cardinality(hll_union_agg(distinct_ip_addresses))
FROM http_request_1min
WHERE ingest_time > date_trunc('minute', now()) - '5 minutes'::interval;Чтобы узнать больше о расширении hll, обратитесь к документации в репозитории проекта.
J.5.6.2.5. Неструктурированные данные с JSONB #
Расширение citus совместимо со встроенной поддержкой Postgres Pro для неструктурированных типов данных. В качестве примера можно вычислить количество посетителей из каждой страны. Использование полуструктурированного типа данных избавляет от необходимости добавлять столбец для каждой отдельной страны и в конечном итоге получать строки с сотнями полупустых столбцов. Рекомендуется использовать формат JSONB, и в примере будет показано, как включить столбцы JSONB в модель данных.
Сначала добавьте новый столбец в таблицу свёрток:
ALTER TABLE http_request_1min ADD COLUMN country_counters JSONB;
Затем включите его в свёртки, изменив соответствующую функцию:
@@ -1,14 +1,19 @@
INSERT INTO http_request_1min (
site_id, ingest_time, request_count,
success_count, error_count, average_response_time_msec
+ , country_counters
) SELECT
site_id,
date_trunc('minute', ingest_time),
COUNT(1) as request_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 1 ELSE 0 END) as success_count
SUM(CASE WHEN (status_code between 200 and 299) THEN 0 ELSE 1 END) as error_count
SUM(response_time_msec) / COUNT(1) AS average_response_time_msec
- FROM http_request
+ , jsonb_object_agg(request_country, country_count) AS country_counters
+ FROM (
+ SELECT *,
+ count(1) OVER (
+ PARTITION BY site_id, date_trunc('minute', ingest_time), request_country
+ ) AS country_count
+ FROM http_request
+ ) hЕсли нужно, чтобы на информационной панели отображалось количество запросов, поступивших из Америки, измените запрос информационной панели следующим образом:
SELECT
request_count, success_count, error_count, average_response_time_msec,
COALESCE(country_counters->>'USA', '0')::int AS american_visitors
FROM http_request_1min
WHERE ingest_time > date_trunc('minute', now()) - '5 minutes'::interval;J.5.6.3. Данные временных рядов #
При использовании временных рядов приложения (например, работающие в реальном времени) запрашивают последнюю информацию, одновременно архивируя старую.
Чтобы справиться с этой задачей, в базе данных Postgres Pro с одним узлом обычно используется секционирование таблиц, чтобы разбить большую таблицу упорядоченных по времени данных на несколько дочерних таблиц, каждая из которых содержит разные временные диапазоны.
Хранение данных в нескольких физических таблицах ускоряет истечение срока годности данных. В одной большой таблице удаление строк включает стоимость сканирования таблицы в поисках строк для удаления, а затем на очистку освобождённого пространства. С другой стороны, удаление секции — быстрая операция, не зависящая от размера данных. Она равнозначна простому удалению с диска файлов с данными. За подробной информацией обратитесь к рисунку.
Рисунок J.4. Диаграмма удаления строки или секции
Секционирование таблицы также уменьшает индексы и ускоряет работу с ними в каждом диапазоне дат. Запросы, работающие с последними данными, скорее всего, будут работать с «горячими» индексами, которые помещаются в памяти, что ускоряет процесс чтения. За подробностями обратитесь к рисунку.
Рисунок J.5. SELECT по нескольким индексам
Команды INSERT также выполняются быстрее, поскольку работают с индексами меньшего объёма. За подробностями обратитесь к рисунку.
Рисунок J.6. INSERT в несколько индексов
Секционирование по времени имеет смысл в следующих случаях:
Большинство запросов обращается к небольшому подмножеству последних данных.
Срок годности старых данных периодически истекает (и они удаляются).
Имейте в виду, что в неподходящих условиях чтение всех этих секций может мешать. Однако при подходящих условиях это весьма полезно, например при хранении данных временных рядов за год и регулярном обращении к данным только за последнюю неделю.
J.5.6.3.1. Масштабирование данных временных рядов в citus #
Можно объединить методы секционирования таблиц с одним узлом с распределённым сегментированием citus, чтобы создать масштабируемую базу данных временных рядов. Такой подход позволит использовать преимущества обоих методов. Особенно элегантно такое решение смотрится при декларативном секционировании таблиц Postgres Pro. За подробностями обратитесь к рисунку.
Рисунок J.7. Сегментирование и секционирование данных временных рядов
В качестве примера распределите и секционируйте таблицу, содержащую исторические данные о событиях GitHub.
Каждая запись в этом наборе данных GitHub представляет собой событие, созданное в GitHub, вместе с ключевой информацией о нём, например типом события, датой создания и пользователем, создавшим событие.
Первым шагом будет создание и секционирование таблицы по времени в базе данных Postgres Pro на одном узле:
-- Декларативно секционированные таблицы CREATE TABLE github_events ( event_id bigint, event_type text, event_public boolean, repo_id bigint, payload jsonb, repo jsonb, actor jsonb, org jsonb, created_at timestamp ) PARTITION BY RANGE (created_at);
Обратите внимание на PARTITION BY RANGE (create_at). Это указание для Postgres Pro, что таблица будет секционирована по столбцу created_at в упорядоченных диапазонах. Но никакие секции для конкретных диапазонов ещё не были созданы.
Прежде чем создавать конкретные секции, сделайте таблицу в citus распределённой. Сегментируйте по repo_id, что означает, что события будут сгруппированы в сегменты для каждого репозитория.
SELECT create_distributed_table('github_events', 'repo_id');На данном этапе расширением citus были созданы сегменты для этой таблицы на рабочих узлах. Внутри каждый сегмент представляет собой таблицу с именем github_events_ для каждого идентификатора сегмента NN. Кроме того, была распространена информация о секционировании, и в каждом из упомянутых сегментов был объявлен ключ секционирования Partition key: RANGE (created_at).
Секционированная таблица не может напрямую содержать данные и больше похожа на представление по всем её секциям. Таким образом, сегменты ещё не готовы к хранению данных. Нужно создать секции и указать их временные диапазоны, после чего можно вставить данные, соответствующие диапазонам.
J.5.6.3.2. Автоматическое создание секций #
Расширение citus предоставляет вспомогательные функции для управления секциями. Можно создать порцию ежемесячных секций, используя функцию create_time_partitions:
SELECT create_time_partitions( table_name := 'github_events', partition_interval := '1 month', end_at := now() + '12 months' );
В citus также есть представление time_partitions, позволяющее легко исследовать секции, созданные расширением.
SELECT partition FROM time_partitions WHERE parent_table = 'github_events'::regclass; ┌────────────────────────┐ │ partition │ ├────────────────────────┤ │ github_events_p2021_10 │ │ github_events_p2021_11 │ │ github_events_p2021_12 │ │ github_events_p2022_01 │ │ github_events_p2022_02 │ │ github_events_p2022_03 │ │ github_events_p2022_04 │ │ github_events_p2022_05 │ │ github_events_p2022_06 │ │ github_events_p2022_07 │ │ github_events_p2022_08 │ │ github_events_p2022_09 │ │ github_events_p2022_10 │ └────────────────────────┘
Со временем потребуется выполнять некоторое обслуживание, чтобы создавать новые секции и удалять старые. Лучше всего настроить периодическое задание для запуска функций обслуживания специальным расширением, например pg_cron:
-- Установите запуск двух заданий cron раз в месяц:
-- 1. Обеспечьте наличие секций на протяжении следующих 12 месяцев
SELECT cron.schedule('create-partitions', '0 0 1 * *', $$
SELECT create_time_partitions(
table_name := 'github_events',
partition_interval := '1 month',
end_at := now() + '12 months'
)
$$);
-- 2. (Необязательно) Обеспечьте хранение данных не более, чем за год
SELECT cron.schedule('drop-partitions', '0 0 1 * *', $$
CALL drop_old_time_partitions(
'github_events',
now() - interval '12 months' /* older_than */
);
$$);Примечание
Обратите внимание, что встроенное секционирование в Postgres Pro появилось недавно и имеет несколько особенностей. Операции обслуживания секционированных таблиц будут получать агрессивные блокировки, которые могут ненадолго приостанавливать выполнение запросов.
J.5.6.3.3. Архивирование со столбцовым хранением #
Некоторые приложения содержат данные, которые логически делятся на небольшую обновляемую часть и большую часть, которая «заморожена». Пример таких данных: журналы, история посещений или записи о продажах. В этом случае можно объединить секционирование с столбцовым хранением таблиц для сжатия старых секций на диске. Столбцовые таблицы citus в настоящее время доступны только для добавления, то есть не могут быть изменены или удалены, но их можно использовать для неизменяемых исторических разделов.
Секционированная таблица может состоять из любой комбинации строковых и столбцовых секций. При использовании секционирования по диапазонам и по ключу временной метки можно сделать новейшую секцию строковой таблицей и периодически переносить новейшую секцию в другую историческую столбцовую секцию.
Вновь рассмотрим пример с событиями GitHub. Создайте новую таблицу с именем github_columnar_events во избежание путаницы с предыдущим примером. Чтобы полностью сосредоточиться на аспекте столбцового хранения, данная таблица не будет распределяться.
Затем загрузите пример с данными:
wget http://examples.citusdata.com/github_archive/github_events-2015-01-01-{0..5}.csv.gz
gzip -c -d github_events-2015-01-01-*.gz >> github_events.csv-- Новая таблица с такой же структурой, как в примере -- из предыдущего раздела CREATE TABLE github_columnar_events ( LIKE github_events ) PARTITION BY RANGE (created_at); -- Создайте секции для хранения данных за два часа в каждой SELECT create_time_partitions( table_name := 'github_columnar_events', partition_interval := '2 hours', start_from := '2015-01-01 00:00:00', end_at := '2015-01-01 08:00:00' ); -- Заполните таблицы загруженными данными -- (обратите внимание, что для этих данных требуется, чтобы БД имела кодировку UTF8) \COPY github_columnar_events FROM 'github_events.csv' WITH (format CSV) -- Выведите список секций, чтобы убедиться, что -- используется строковое хранение (метод доступа heap) SELECT partition, access_method FROM time_partitions WHERE parent_table = 'github_columnar_events'::regclass;
┌─────────────────────────────────────────┬───────────────┐ │ partition │ access_method │ ├─────────────────────────────────────────┼───────────────┤ │ github_columnar_events_p2015_01_01_0000 │ heap │ │ github_columnar_events_p2015_01_01_0200 │ heap │ │ github_columnar_events_p2015_01_01_0400 │ heap │ │ github_columnar_events_p2015_01_01_0600 │ heap │ └─────────────────────────────────────────┴───────────────┘
-- Преобразуйте старые секции для столбцового хранения CALL alter_old_partitions_set_access_method( 'github_columnar_events', '2015-01-01 06:00:00' /* older_than */, 'columnar' ); -- Старые секции теперь столбцовые, а -- для новых используется строковое хранение и они могут быть изменены SELECT partition, access_method FROM time_partitions WHERE parent_table = 'github_columnar_events'::regclass;
┌─────────────────────────────────────────┬───────────────┐ │ partition │ access_method │ ├─────────────────────────────────────────┼───────────────┤ │ github_columnar_events_p2015_01_01_0000 │ columnar │ │ github_columnar_events_p2015_01_01_0200 │ columnar │ │ github_columnar_events_p2015_01_01_0400 │ columnar │ │ github_columnar_events_p2015_01_01_0600 │ heap │ └─────────────────────────────────────────┴───────────────┘
Чтобы увидеть степень сжатия столбцовой таблицы, используйте VACUUM VERBOSE. Степень сжатия для трёх столбцовых секций выглядит так:
VACUUM VERBOSE github_columnar_events;
INFO: statistics for "github_columnar_events_p2015_01_01_0000": storage id: 10000000003 total file size: 4481024, total data size: 4444425 compression rate: 8.31x total row count: 15129, stripe count: 1, average rows per stripe: 15129 chunk count: 18, containing data for dropped columns: 0, zstd compressed: 18 INFO: statistics for "github_columnar_events_p2015_01_01_0200": storage id: 10000000004 total file size: 3579904, total data size: 3548221 compression rate: 8.26x total row count: 12714, stripe count: 1, average rows per stripe: 12714 chunk count: 18, containing data for dropped columns: 0, zstd compressed: 18 INFO: statistics for "github_columnar_events_p2015_01_01_0400": storage id: 10000000005 total file size: 2949120, total data size: 2917407 compression rate: 8.51x total row count: 11756, stripe count: 1, average rows per stripe: 11756 chunk count: 18, containing data for dropped columns: 0, zstd compressed: 18
Одно из преимуществ секционированной таблицы github_columnar_events заключается в том, что к ней можно обращаться как к обычной таблице.
SELECT COUNT(DISTINCT repo_id) FROM github_columnar_events;
┌───────┐ │ count │ ├───────┤ │ 16001 │ └───────┘
Записи могут быть обновлены или удалены при условии, что используется предложение WHERE на ключе секционирования, которое полностью фильтрует данные в секции таблиц строк.
J.5.6.3.3.1. Архивирование строковой секции в столбцовое хранилище #
Когда у строковой секции заполняется диапазон, можно заархивировать её в сжатое столбцовое хранилище. Можно автоматизировать этот процесс с помощью расширения pg_cron:
-- Ежемесячное задание cron
SELECT cron.schedule('compress-partitions', '0 0 1 * *', $$
CALL alter_old_partitions_set_access_method(
'github_columnar_events',
now() - interval '6 months' /* older_than */,
'columnar'
);
$$);За подробностями обратитесь к разделу Столбцовое хранение.
J.5.7. Архитектурные понятия #
J.5.7.1. Узлы #
citus — это расширение Postgres Pro, которое обеспечивает координацию обычных серверов баз данных (называемых узлами) в архитектуре «без разделения ресурсов». Узлы образуют кластер, который позволяет Postgres Pro хранить больше данных и использовать больше процессорных ядер, чем это возможно на одном компьютере. Эта архитектура также позволяет масштабировать базу данных путём добавления новых узлов в кластер.
В каждом кластере есть один специальный узел, называемый координатором (остальные называются рабочими узлами). Приложения отправляют свои запросы на узел-координатор, который передаёт их соответствующим рабочим узлам и собирает результаты.
Узел-координатор либо направляет запрос на один рабочий узел, либо распараллеливает его между несколькими в зависимости от того, находятся ли необходимые данные на одном узле или на нескольких. Чтобы правильно это сделать, узел-координатор проверяет таблицы метаданных. В этих специальных таблицах citus отслеживаются DNS-имена и состояния рабочих узлов, а также распределение данных между узлами. За дополнительной информацией обратитесь к описанию таблиц и представлений citus.
J.5.7.2. Модели сегментирования #
Сегментирование — это метод, используемый в системах управления базами данных и распределённых вычислениях для горизонтального секционирования данных между несколькими серверами или узлами. При этом большая база данных или набор данных разбивается на небольшие, легко управляемые части, называемые сегментами. Каждый сегмент содержит подмножество данных, которые вместе образуют полный набор данных.
citus предлагает два типа сегментирования данных: на основе строк и на основе схем. Каждый вариант имеет собственные особенности сегментирования, что позволяет выбрать подход, который соответствует требованиям конкретного приложения.
J.5.7.2.1. Сегментирование на основе строк #
Традиционный способ сегментирования таблиц citus — это модель общей схемы в единой базе данных, также известная как сегментирование на основе строк, в которой арендаторы сосуществуют как строки одной таблицы. Арендатор определяется путём установления столбца распределения, который позволяет разделить таблицу горизонтально.
Это наиболее эффективный способ сегментирования с точки зрения использования оборудования. Арендаторы оптимально распределены по узлам кластера. Однако данный способ требует, чтобы у всех таблиц в схеме был столбец распределения и чтобы все запросы в приложении фильтровались по этому столбцу. Сегментирование на основе строк отлично подходит для рабочих нагрузок Интернета вещей и для достижения максимально эффективного использования оборудования.
Преимущества:
Высокая производительность
Эффективное распределение арендаторов по узлам
Недостатки:
Необходимость изменять схему
Необходимость изменять запросы приложений
У всех арендаторов должна быть одна схема
J.5.7.2.2. Сегментирование на основе схем #
Сегментирование на основе схем представляет собой модель с общей базой данных и отдельными схемами, при этом схема становится логическим сегментом в базе данных. В многоарендных приложениях для каждого арендатора используется отдельная схема, чтобы легко выполнять сегментирование по арендаторам. Нет необходимости менять запросы, и в приложении обычно требуется лишь установить правильный search_path при переключении арендаторов. Сегментирование на основе схем — оптимальное решение для микросервисов и приложений от независимых поставщиков ПО, в которые невозможно внести изменения, необходимые для реализации сегментирования на основе строк.
Преимущества:
У арендаторов могут быть разнородные схемы
Нет необходимости изменять схему
Нет необходимости изменять запросы приложений
Совместимость с SQL при сегментировании на основе схем выше, чем при сегментировании на основе строк
Недостатки:
На узле располагается меньше арендаторов, чем при сегментировании на основе строк
J.5.7.2.3. Особенности сегментирования #
| Сегментирование на основе схем | Сегментирование на основе строк | |
|---|---|---|
| Многоарендная модель | Отдельная схема для каждого арендатора | Общие таблицы со столбцами идентификаторов арендаторов |
| Версия citus | 12.0+ | Все версии |
| Дополнительные шаги по сравнению с Postgres Pro | Только изменение файла конфигурации | Используйте функцию create_distributed_table для каждой таблицы, чтобы распределить и совместить таблицы по tenant_id |
| Количество арендаторов | 1 – 10 000 | 1–1 000 000+ |
| Требования к разработке модели данных | Отсутствие внешних ключей в распределённых схемах | Необходимость наличия столбца tenant_id (столбец распределения, также называемый ключ сегментирования) в каждой таблице, а также в первичных и внешних ключах |
| Требования SQL для запросов к одному узлу | Использование одной распределённой схемы в каждом запросе | В соединения и предложения WHERE должен быть включён столбец tenant_id |
| Распараллеливание запросов между арендаторами | Нет | Да |
| Разные определения таблиц для каждого арендатора | Да | Нет |
| Управление доступом | Разрешения на доступ к схемам | Разрешения на доступ к схемам |
| Обмен данными между арендаторами | Да, с помощью таблиц-справочников (в отдельной схеме) | Да, с помощью таблиц-справочников |
| Изоляция арендаторов в сегментах | У каждого арендатора своя группа сегментов по определению | Можно назначать для определённых идентификаторов арендаторов собственные группы сегментов с помощью функции isolate_tenant_to_new_shard. |
J.5.7.3. Распределённые данные #
J.5.7.3.1. Типы таблиц #
В кластере citus имеется несколько типов таблиц, каждый из которых используется для разных целей.
Тип 1: распределённые таблицы.
Первый и наиболее распространённый тип — распределённые таблицы. SQL-запросы выполняются для них так же, как для обычных таблиц, но их особенность — в горизонтальном распределении по рабочим узлам. За подробностями обратитесь к рисунку ниже.
Рисунок J.8. Распараллеливание команды
SELECTЗдесь строки таблицы
tableхранятся в таблицахtable_1001,table_1002и т. д. на рабочих узлах. Таблицы-компоненты на рабочих узлах называются сегментами.В citus не только SQL-операторы, но и DDL-операторы запускаются на уровне кластера, поэтому изменение схемы распределённой таблицы приводит к каскадному изменению всех сегментов таблицы на рабочих узлах.
Чтобы узнать, как создавать распределённые таблицы, обратитесь к разделу Создание и изменение распределённых объектов (DDL).
Столбец распределения. В citus используется алгоритмическое сегментирование для назначения строк сегментам. Это означает, что назначение выполняется детерминированно — в данном случае на основе значения определённого столбца таблицы, называемого столбцом распределения. Администратор кластера должен назначить такой столбец при распределении таблицы. Правильный выбор важен для производительности и функциональности, за подробностями обратитесь к разделу Выбор столбца распределения.
Тип 2: таблицы-справочники.
Таблица-справочник — это тип распределённой таблицы, всё содержимое которой сосредоточено в одном сегменте, реплицируемом на каждом рабочем узле. Таким образом, запросы на любом рабочем узле могут получить доступ к справочной информации локально, без сетевых издержек, связанных с запросом строк из другого узла. В таблицах-справочниках нет столбцов распределения, поскольку нет необходимости распознавать отдельные сегменты для каждой строки.
Таблицы-справочники обычно небольшого размера и используются для хранения данных, относящихся к запросам, которые выполняются на любом рабочем узле (например, перечисляемые значения, такие как статусы заказов или категории продуктов).
При взаимодействии с таблицей-справочником автоматически выполняется двухфазная фиксация транзакций. Это означает, что citus гарантирует, что данные всегда находятся в согласованном состоянии, независимо от производимых действий: записи, изменения или удаления.
Подробное описание создания и использования таких таблиц находится в разделе Таблицы-справочники.
Тип 3: локальные таблицы.
При использовании citus узел-координатор, с которым происходит взаимодействие после подключения, представляет собой обычную базу данных Postgres Pro с установленным расширением citus. Таким образом, можно создавать обычные таблицы и не сегментировать их. Это полезно для небольших административных таблиц, которые не участвуют в запросах с соединениями. Примером может служить таблица пользователей для входа и аутентификации в приложении.
Создавать стандартные таблицы Postgres Pro легко, поскольку они и создаются по умолчанию при выполнении
CREATE TABLE. Почти в каждой инсталляции citus стандартные таблицы Postgres Pro используются наряду с распределёнными и таблицами-справочниками. В самом citus локальные таблицы используются для хранения метаданных кластера, как упоминалось ранее.Тип 4: локальные управляемые таблицы.
При включённом параметре конфигурации citus.enable_local_reference_table_foreign_keys citus может автоматически добавлять локальные таблицы в метаданные, если между локальной таблицей и таблицей-справочником есть ссылки на внешние ключи. Кроме того, эти таблицы можно создать вручную, вызвав функцию citus_add_local_table_to_metadata для обычных локальных таблиц. Таблицы в метаданных считаются управляемыми таблицами, и к ним можно обращаться с любого узла, — citus автоматически направит запросы узлу-координатору для получения данных из локальной управляемой таблицы. В представлении citus_tables такие таблицы отображаются как локальные.
Тип 5: таблицы-схемы.
При использовании сегментирования на основе схем распределённые схемы автоматически связываются с отдельными группами совмещения, так что таблицы, создаваемые в этих схемах, автоматически преобразуются в совмещённые распределённые таблицы без ключа сегментирования. Такие таблицы считаются таблицами-схемами и отображаются как схемы в представлении citus_tables.
J.5.7.3.2. Сегменты #
В предыдущем разделе говорилось, что сегмент содержит подмножество строк распределённой таблицы в меньшей таблице внутри рабочего узла. В данном разделе более подробно рассматриваются технические детали.
Таблица метаданных pg_dist_shard на узле-координаторе содержит одну строку для каждого сегмента каждой распределённой таблицы в системе. Строка соответствует shardid с диапазоном целых чисел в хеш-пространстве (shardminvalue, shardmaxvalue):
SELECT * FROM pg_dist_shard; logicalrelid | shardid | shardstorage | shardminvalue | shardmaxvalue ---------------+---------+--------------+---------------+--------------- github_events | 102026 | t | 268435456 | 402653183 github_events | 102027 | t | 402653184 | 536870911 github_events | 102028 | t | 536870912 | 671088639 github_events | 102029 | t | 671088640 | 805306367 (4 rows)
Если узлу-координатору нужно определить, какой сегмент содержит строку github_events, значение столбца распределения в строке хешируется и проверяется, какой диапазон сегмента содержит хешированное значение. (Диапазоны определены так, что образ хеш-функции является их несвязным объединением.)
J.5.7.3.2.1. Размещение сегмента #
Предположим, что сегмент 102027 связан с рассматриваемой строкой. Это означает, что строка должна быть прочитана или записана в таблицу с именем github_events_102027 на одном из рабочих узлов. На каком именно — полностью определяется таблицами метаданных, а сопоставление сегмента с рабочим узлом называется размещением сегмента.
Для этого используется соединение таблиц метаданных. Узел-координатор выполняет поиск такого типа для маршрутизации запросов. Запросы перезаписываются в виде фрагментов, ссылающихся на определённые таблицы, например github_events_102027, и эти фрагменты выполняются на соответствующих рабочих узлах.
SELECT
shardid,
node.nodename,
node.nodeport
FROM pg_dist_placement placement
JOIN pg_dist_node node
ON placement.groupid = node.groupid
AND node.noderole = 'primary'::noderole
WHERE shardid = 102027;┌─────────┬───────────┬──────────┐ │ shardid │ nodename │ nodeport │ ├─────────┼───────────┼──────────┤ │ 102027 │ localhost │ 5433 │ └─────────┴───────────┴──────────┘
В примере github_events было четыре сегмента. Количество сегментов можно настроить для каждой таблицы во время распределения по кластеру. Оптимальный выбор количества сегментов зависит от варианта использования, см. раздел Количество сегментов.
Обратите внимание, что citus позволяет реплицировать сегменты для защиты от потери данных с помощью потоковой репликации Postgres Pro для резервного копирования всей базы данных каждого узла в базу данных узла-последователя. Такая репликация прозрачна и не требует участия таблиц метаданных citus.
J.5.7.3.3. Совмещение #
Поскольку сегменты можно размещать на узлах как угодно, имеет смысл размещать сегменты, содержащие связанные строки связанных таблиц, на одном и том же узле. При таком размещении запросы соединения между этими сегментами позволяют избежать отправки большого количества информации по сети и могут выполняться в пределах одного узла citus.
Одним из примеров является база данных с магазинами, товарами и покупками. Если все три таблицы содержат столбец store_id и распределяются по нему, то все запросы, ограниченные одним хранилищем, могут эффективно выполняться на одном рабочем узле, даже если запрос содержит какую-либо комбинацию этих таблиц.
За подробным описанием и примерами обратитесь к разделу Совмещение таблиц.
J.5.7.3.4. Распараллеливание #
Распределение запросов по нескольким компьютерам позволяет выполнять больше запросов одновременно и увеличивает скорость обработки за счёт добавления новых компьютеров в кластер. Кроме того, разделение одного запроса на фрагменты, как описано в предыдущем разделе, значительно увеличивает выделяемую на запрос вычислительную мощность. Это, в свою очередь, обеспечивает эффективное распараллеливание, то есть использование всех ядер процессора.
Запросы, читающие или влияющие на сегменты, которые равномерно распределены по множеству узлов, могут выполняться в «реальном времени». Обратите внимание, что результаты запроса должны передаваться обратно через узел-координатор, поэтому ускорение работы наиболее очевидно при составных окончательных результатах, например, при использовании агрегатных функций, таких как подсчёт количества значений и описательная статистика.
В разделе Обработка запросов более подробно рассматривается секционирование запросов на фрагменты и управление их выполнением.
J.5.7.4. Выполнение запросов #
При выполнении многосегментных запросов citus должен балансировать выгоды от распараллеливания с накладными расходами от подключений к базам данных (сетевыми задержками и использованием ресурсов рабочего узла). Чтобы настроить оптимальное выполнение запросов citus в рабочей нагрузке конкретной базы данных, необходимо понимать, как citus управляет подключениями к базам данных между узлом-координатором и рабочими узлами.
В citus каждый входящий многосегментный запрос разбивается на запросы для каждого сегмента, называемые задачами. Задачи ставятся в очередь и запускаются, как только появляется возможность подключиться к соответствующим рабочим узлам. Запросы к распределённым таблицам foo и bar показаны на диаграмме управления подключениями.
Рисунок J.9. Управление подключениями
Для каждого сеанса у узла-координатора есть пул соединений. Для каждого запроса (например, SELECT * FROM foo на диаграмме) можно открыть ограниченное количество одновременных соединений для задач на каждый рабочий узел, установленное в параметре конфигурации citus.max_adaptive_executor_pool_size. Значение параметра можно настроить на уровне сеанса для управления приоритетами.
Несколько коротких задач могут выполняться последовательно по одному соединению быстрее по сравнению с параллельным выполнением по отдельным соединениям для каждой. С другой стороны, длительные задачи выгодно выполнять параллельно.
Чтобы сбалансировать выполнение коротких и длинных задач, в citus используется параметр конфигурации citus.executor_slow_start_interval. Он определяет интервал между попытками подключения для задач в многосегментном запросе. Сначала для задач запроса, попадающих в очередь, можно открыть только одно соединение. Если в конце интервала есть ожидающие подключения задачи, citus увеличивает количество возможных одновременных подключений. Чтобы полностью отключить такое поведение, установите для параметра конфигурации значение 0.
Когда задача перестаёт использовать соединение, пул сеансов сохранит соединение открытым для дальнейшего использования. Кеширование соединения позволяет избежать затрат на его восстановление между узлом-координатором и рабочим узлом. Однако параметр конфигурации citus.max_cached_conns_per_worker позволяет ограничить максимальное количество одновременно открытых простаивающих соединений в каждом пуле, чтобы сократить использование ресурсов такими соединениями на рабочем узле.
При этом параметр конфигурации citus.max_shared_pool_size выполняет роль защитного механизма. Он ограничивает общее количество подключений для каждого рабочего узла во всех задачах.
Рекомендации по настройке этих параметров в соответствии с рабочей нагрузкой находятся в разделе Управление подключениями.
J.5.8. Разработка #
J.5.8.1. Определение типа приложения #
Эффективность выполнения запросов в кластере citus требует правильного распределения данных между компьютерами, что зависит от типа приложения и шаблонов его запросов.
В целом существует два типа приложений, лучше всего подходящих для работы с citus. Первый шаг в моделировании данных — определить, какой из них больше соответствует вашему приложению.
J.5.8.1.1. Краткий обзор #
| Многоарендные приложения | Приложения для анализа данных в реальном времени |
|---|---|
| От десятков до сотен таблиц в схеме | Малое количество таблиц |
| Запросы, относящиеся к одному арендатору (предприятию/магазину) | Относительно простые аналитические запросы с агрегированием |
| OLTP-нагрузки для обслуживания интернет-клиентов | Большой объём в основном неизменяемых входящих данных |
| OLAP-нагрузки для обслуживания аналитических запросов каждого клиента | Работа в основном с большой таблицей событий |
J.5.8.1.2. Примеры и характеристики #
J.5.8.1.2.1. Многоарендные приложения #
Обычно это SaaS-приложения, которые обслуживают другие компании, учётные записи или организации. Большинство SaaS-приложений по сути являются реляционными. У них уже есть критерий, по которому можно распределять данные по узлам: сегментировать по tenant_id.
Расширение citus позволяет масштабировать базу данных до миллионов арендаторов без необходимости перепроектировать приложение. Можно сохранить необходимую реляционную семантику, такую как соединения, ограничения внешнего ключа, транзакции, принципы ACID и целостность.
Примеры: веб-сайты, на которых размещены витрины других предприятий, например решения для цифрового маркетинга или инструменты автоматизации продаж.
Характеристики: запросы, относящиеся к одному арендатору, а не собирающие информацию по нескольким. Сюда входят OLTP-нагрузки для обслуживания интернет-клиентов и OLAP-нагрузки, которые обслуживают аналитические запросы для каждого клиента. Наличие десятков или сотен таблиц в схеме базы данных также является показателем многоарендной модели данных.
Для масштабирования многоарендного приложения с помощью citus также требуются минимальные изменения в коде приложения. citus поддерживает популярные платформы: Ruby on Rails и Django.
J.5.8.1.2.2. Анализ данных в реальном времени #
Обычно это приложения, которым требуется существенное распараллеливание, координирующее сотни ядер для быстрого получения результатов по числовым, статистическим или счётным запросам. Сегментирование и распараллеливание SQL-запросов на несколько узлов позволяет citus выполнять запросы к миллиардам записей в реальном времени менее чем за секунду.
Примеры: аналитические панели для клиентов, которым необходимо получать ответ менее, чем за секунду.
Характеристики: несколько таблиц, которые часто сосредоточены вокруг большой таблицы событий, связанных с устройством, сайтом или пользователем, при этом поступает большой объём преимущественно неизменяемых данных. Относительно простые (но с большим объёмом вычислений) аналитические запросы, включающие несколько агрегатов и операции
GROUP BY.
Если ваш случай похож на один из описанных выше, то на следующем этапе необходимо решить, как сегментировать данные в кластере citus. Как описано в разделе Архитектурные понятия, в citus строки таблицы назначаются сегментам в соответствии с хешированным значением столбца распределения таблицы. Для обеспечения лучшей производительности администратор баз данных должен выбирать столбцы распределения, соответствующие сценариям использования типичных запросов.
J.5.8.2. Выбор столбца распределения #
В citus столбец распределения используется в распределённых таблицах для назначения строк таблиц сегментам. Выбор столбца распределения для каждой таблицы является одним из наиболее важных решений при моделировании, поскольку от него зависит, как данные распределяются по узлам.
При корректном выборе столбцов распределения связанные данные будут группироваться на одних и тех же физических узлах, что ускоряет выполнение запросов и добавляет поддержку всех SQL-функций. Если столбцы выбраны некорректно, система будет работать неоправданно медленно и не сможет поддерживать все SQL-функции на узлах.
В этом разделе даны советы по выбору столбцов распределения для двух наиболее распространённых сценариев citus. В заключении главы подробно рассматривается «совмещение» — предпочтительное группирование данных на узлах.
J.5.8.2.1. Многоарендные приложения #
В многоарендной архитектуре используется форма иерархического моделирования базы данных для распределения запросов по узлам распределённого кластера. Вершина иерархии данных называется tenant_id и должна храниться в столбце каждой таблицы. В citus проверяется, какое значение tenant_id используется в запросах, и каждый запрос направляется для обработки на один рабочий узел, в частности на узел, который содержит фрагмент данных, связанный с этим tenant_id. Выполнение запроса со всеми соответствующими данными, размещёнными на одном узле, называется совмещением.
На следующей диаграмме показано совмещение в многоарендной модели данных. Она содержит две таблицы: «Accounts» (Учётные записи) и «Campaigns» (Кампании), каждая из которых распределена по account_id. Сегменты обозначены как прямоугольники, цвет которых соответствует цвету содержащего их рабочего узла. Зелёные сегменты хранятся вместе на одном рабочем узле, а синие — на другом. Обратите внимание, что запрос соединения учётных записей и кампаний будет содержать все необходимые данные на одном узле с выборкой по одному account_id.
Рисунок J.10. Совмещение в многоарендной модели
Чтобы применить этот подход к собственной схеме, сначала определите, кто в приложении будет арендатором. Обычно это предприятие, учётная запись, организация или клиент. При этом имя столбца будет примерно таким: company_id или customer_id. Следует проанализировать каждый запрос и ответить на вопрос: сработает ли такой запрос с дополнительным предложением WHERE, ограничивающим все задействованные таблицы строками с одним и тем же tenant_id? Запросы в многоарендной модели обычно ограничиваются арендатором, например, запросы о продажах или наличии товара будут ограничены определённым магазином.
Практические рекомендации:
Разделяйте распределённые таблицы по общему столбцу
tenant_id. Например, в SaaS-приложении, где арендаторами являются предприятия, рольtenant_idбудет выполнятьcompany_id.Преобразовывайте небольшие таблицы на несколько арендаторов в таблицы-справочники. Если несколько арендаторов совместно используют небольшую таблицу данных, распределите её как таблицу-справочник.
Фильтруйте все запросы приложения по
tenant_id. Каждый запрос должен обращаться только к одному арендатору.
Подробный пример создания такого типа приложений описан в разделе Многоарендные приложения.
J.5.8.2.2. Приложения для анализа данных в реальном времени #
В архитектуре многоарендных систем применяется иерархическая структура, а также метод совмещения данных для направления запросов каждому арендатору. Архитектуры, предназначенные для анализа данных в реальном времени, опираются на конкретные характеристики распределения данных, чтобы обеспечить высокую степень распараллеливания.
В качестве столбца распределения в многоарендной модели используется идентификатор арендатора, а в модели реального времени — «идентификатор объекта». Типичными объектами являются пользователи, узлы или устройства.
Запросы в реальном времени обычно содержат агрегирования числовых данных, сгруппированные по дате или категории. В citus эти запросы отправляются каждому сегменту для получения части результата, и итоговый результат собирается на узле-координаторе. Запросы выполняются быстрее, когда задействовано максимальное количество узлов и ни один узел не выполняет непропорциональный объём работы.
Практические рекомендации:
Выбирайте столбец с большим количеством строк в качестве столбца распределения. Для сравнения: поле «статус» в таблице заказов со значениями «новый», «оплачено» и «отправлено» — плохой выбор для столбца распределения, поскольку оно принимает только эти несколько значений. Количество различных значений ограничивает количество сегментов, которые могут хранить данные, и количество узлов, которые могут их обрабатывать. Среди столбцов с большим количеством строк полезно дополнительно выбрать те, которые часто используются в предложениях группирования или в качестве ключей соединения.
Выбирайте столбец с равномерным распределением. Если распределить таблицу по столбцу, ориентированному на некоторые общие значения, то данные в таблице будут накапливаться в конкретных сегментах. Узлы, содержащие эти сегменты, в конечном итоге будут выполнять больше работы, чем прочие.
Распределяйте таблицы фактов и измерений по общим столбцам. У таблицы фактов может быть только один ключ распределения. Таблицы, соединяющиеся по другому ключу, не будут совмещаться с таблицей фактов. Выберите один ключ для совмещения в зависимости от частоты соединения и размера соединяемых строк.
Преобразуйте некоторые таблицы измерений в таблицы-справочники. Если таблицу измерений невозможно совместить с таблицей фактов, можно повысить производительность запросов, распределив копии таблицы измерений по всем узлам в виде таблицы-справочника.
Подробный пример создания такого типа приложений описан в разделе Панели для анализа в реальном времени.
J.5.8.2.3. Данные временных рядов #
При использовании временных рядов приложения запрашивают последнюю информацию, одновременно архивируя старую информацию.
Самая распространённая ошибка при моделировании информации временных рядов в citus — использование самой метки времени в качестве столбца распределения. При использовании хеш-распределения на основе времени система будет распределять значения временных меток случайным образом по разным сегментам, а не сохранять диапазоны времени в сегментах вместе. Однако запросы по времени обычно ссылаются на диапазоны времени (например, последние данные за неделю), поэтому такое хеш-распределение может привести к увеличению сетевого трафика.
Практические рекомендации:
Не выбирайте временную метку в качестве столбца распределения. В многоарендном приложении используйте
tenant_id, а в приложении для анализа данных в реальном времени используйтеentity_id.Для запросов по времени используйте секционирование таблиц Postgres Pro, чтобы разбить большую таблицу упорядоченных по времени данных на несколько дочерних таблиц, каждая из которых содержит разные временные диапазоны. При распределении секционированной таблицы Postgres Pro в citus создаются сегменты для дочерних таблиц.
Подробный пример создания такого типа приложений описан в разделе Данные временных рядов.
J.5.8.2.4. Совмещение таблиц #
Благодаря гибкости и надёжности реляционные базы данных являются очевидным выбором для хранения данных во многих приложениях. Исторически одним из недостатков реляционных баз данных было то, что они могли работать только на одном компьютере, что накладывало внутренние ограничения, когда необходимость хранения данных опережала развитие аппаратных возможностей. Решением для быстрого масштабирования баз данных стало распределение, но при этом возникла проблема производительности: реляционные операции, такие как соединения, затем должны пересекать границы сети. Совмещение — это практика тактического разделения данных, при которой связанная информация хранится на одних и тех же компьютерах для обеспечения эффективности реляционных операций, но при этом используются преимущества горизонтального масштабирования для всего набора данных.
Принцип совмещения данных заключается в том, что все таблицы в базе данных имеют общий столбец для распределения и одинаково сегментированы между компьютерами. Это означает, что строки с одинаковым значением столбца распределения всегда находятся на одном компьютере, даже если принадлежат разным таблицам. Чтобы выполнять реляционные операции внутри групп, столбец распределения должен обеспечивать осмысленную группировку данных.
J.5.8.2.4.1. Совмещение данных в citus для таблиц, распределённых по хешу #
Расширение citus для Postgres Pro позволяет формировать распределённую базу данных из нескольких баз данных. Каждый узел в кластере citus представляет собой полнофункциональную базу данных Postgres Pro, а расширение добавляет возможность работы с единой однородной базой данных. Хотя оно не обеспечивает полную функциональность Postgres Pro в распределённом виде, во многих случаях можно в полной мере использовать преимущества функциональности Postgres Pro на одном компьютере посредством совмещения, включая полную поддержку SQL, транзакций и внешних ключей.
В citus строка сохраняется в сегменте, если хеш значения в столбце распределения попадает в хеш-диапазон сегмента. Чтобы обеспечить совмещение, сегменты с одинаковым хеш-диапазоном всегда размещаются на одном и том же узле даже после перебалансировки, так что одинаковые значения столбца распределения в разных таблицах всегда находятся на одном и том же узле. За дополнительной информацией обратитесь к рисунку ниже.
Рисунок J.11. Сегменты совмещения
Хорошо применимый на практике столбец распределения, как видно из примера, — это tenant_id в многоарендных приложениях. Например, у SaaS-приложений обычно множество арендаторов, но каждый выполняемый ими запрос относится к одному конкретному. Одним из вариантов реализации является предоставление базы данных или схемы для каждого арендатора, но он часто затратен и непрактичен, поскольку может включать множество операций, охватывающих нескольких пользователей (загрузка данных, миграция, агрегирование, аналитика, изменение схемы, резервное копирование и т. д.). С ростом числа арендаторов управлять такой реализацией будет всё сложнее.
J.5.8.2.4.2. Практический пример совмещения #
Примером части многоарендного SaaS-приложения для веб-аналитики могут служить следующие таблицы:
CREATE TABLE event ( tenant_id int, event_id bigint, page_id int, payload jsonb, primary key (tenant_id, event_id) ); CREATE TABLE page ( tenant_id int, page_id int, path text, primary key (tenant_id, page_id) );
Теперь можно выполнять запросы, поступающие с информационной панели на стороне клиента, например: «Вывести для шестого арендатора количество посещений всех страниц, начинающихся с /blog, за последнюю неделю».
J.5.8.2.4.3. Использование обычных таблиц Postgres Pro #
Если бы наши данные находились на одном узле Postgres Pro, можно было бы легко составить запрос, используя богатый набор реляционных операций, предлагаемых SQL:
SELECT page_id, count(event_id) FROM page LEFT JOIN ( SELECT * FROM event WHERE (payload->>'time')::timestamptz >= now() - interval '1 week' ) recent USING (tenant_id, page_id) WHERE tenant_id = 6 AND path LIKE '/blog%' GROUP BY page_id;
Это решение подходит для многих приложений, поскольку обеспечивает максимальную гибкость, при условии, что рабочее множество для запроса умещается в памяти. Однако даже если масштабирование пока не требуется, может оказаться полезным рассмотреть влияние масштабирования на пользовательскую модель данных.
J.5.8.2.4.4. Распределение таблиц по идентификатору #
По мере роста числа арендаторов и объёма данных, хранящихся для каждого арендатора, время выполнения запросов обычно увеличивается, поскольку рабочий набор больше не помещается в памяти или процессор становится узким местом. В этом случае можно сегментировать данные на множество узлов, используя citus. Первый и самый важный выбор, который нужно сделать при сегментировании, — выбор столбца распределения. Для примера сделаем простой выбор: использовать event_id для таблицы event и page_id для таблицы page:
-- По неопытности для примера используем event_id и page_id в качестве столбцов распределения
SELECT create_distributed_table('event', 'event_id');
SELECT create_distributed_table('page', 'page_id');Учитывая, что данные рассредоточены по разным рабочим узлам, нельзя просто выполнить соединение, как в случае с одним узлом Postgres Pro. Вместо этого нужно будет выполнить два запроса:
Во всех сегментах таблицы страниц (Q1):
SELECT page_id FROM page WHERE path LIKE '/blog%' AND tenant_id = 6;
Во всех сегментах таблицы событий (Q2):
SELECT page_id, count(*) AS count FROM event WHERE page_id IN (/*…page IDs from first query…*/) AND tenant_id = 6 AND (payload->>'time')::date >= now() - interval '1 week' GROUP BY page_id ORDER BY count DESC LIMIT 10;
После этого результаты двух шагов должны быть объединены в приложении.
Данные, необходимые для ответа на запрос, разбросаны по сегментам на разных узлах, и придётся отправить запрос каждому из этих сегментов. За подробностями обратитесь к рисунку ниже.
Рисунок J.12. Совмещение и пример неэффективных запросов
В данном случае распределение данных привносит существенные недостатки:
Накладные расходы на запрос к каждому сегменту и выполнение нескольких запросов.
Накладные расходы запроса Q1, возвращающего клиенту большое количество строк.
Запрос Q2 становится слишком большим.
Необходимость писать запросы в несколько этапов, объединять результаты, вносить изменения в приложение.
Потенциальным преимуществом распределения данных является возможность распараллеливания запросов, как в citus. Однако это выгодно только в том случае, если объём работы, выполняемой запросом, существенно превышает накладные расходы на запросы к нескольким сегментам. Как правило, лучше избегать такой большой нагрузки непосредственно из приложения, например с помощью предварительного агрегирования данных.
J.5.8.2.4.5. Распределение таблиц по ID арендатора #
При внимательном рассмотрении запроса можно увидеть, что во всех необходимых строках есть общий tenant_id. Запросы с информационной панели включают обращения только к собственным данным арендатора. Это означает, что если данные для одного и того же арендатора всегда располагаются на одном узле Postgres Pro, ответ на исходный запрос может быть получен от этого узла за один шаг, с помощью выполнения соединения по tenant_id и page_id.
В citus строки с одинаковым значением столбца распределения гарантированно будут находиться на одном узле. Каждый сегмент в распределённой таблице фактически содержит набор совмещённых сегментов из других распределённых таблиц, которые содержат те же значения столбца распределения (данные для одного и того же арендатора). Начав заново, можно создать таблицы со столбцом распределения tenant_id.
-- Совмещение таблиц с помощью общего столбца распределения
SELECT create_distributed_table('event', 'tenant_id');
SELECT create_distributed_table('page', 'tenant_id', colocate_with => 'event');В этом случае citus может выполнить тот же запрос, который выполнялся бы на одном узле Postgres Pro без изменений (Q1):
SELECT page_id, count(event_id) FROM page LEFT JOIN ( SELECT * FROM event WHERE (payload->>'time')::timestamptz >= now() - interval '1 week' ) recent USING (tenant_id, page_id) WHERE tenant_id = 6 AND path LIKE '/blog%' GROUP BY page_id;
Благодаря фильтру tenant_id и соединению по tenant_id, citus может выполнить запрос, используя набор совмещённых сегментов, которые содержат данные для этого конкретного арендатора, а узел Postgres Pro может ответить на запрос за один шаг, что обеспечивает полную поддержку SQL. За подробностями обратитесь к рисунку ниже.
Рисунок J.13. Совмещение и пример эффективных запросов
В некоторых случаях запросы и схемы таблиц потребуют незначительных изменений, чтобы гарантировать, что tenant_id всегда включается в уникальные ограничения и условия соединения. Однако обычно это простое изменение, позволяющее избежать обширного переписывания, которое потребовалось бы без совмещения.
Хотя в приведённом выше примере запрос отправляется только одному узлу, поскольку существует конкретный фильтр tenant_id = 6, совмещение также позволяет эффективно выполнять распределённые соединения по tenant_id между всеми узлами, даже с SQL-ограничениями.
J.5.8.2.4.6. Совмещение и поддержка функциональности #
Полный список возможностей citus, доступных при использовании совмещения:
Полная поддержка SQL для запросов к одному набору совмещённых сегментов.
Поддержка транзакций с несколькими операторами для внесения изменений в один набор совмещённых сегментов.
Агрегирование с помощью
INSERT...SELECT.Внешние ключи.
Распределённые внешние соединения.
Вынос наружу общих табличных выражений.
Совмещение данных — мощный метод обеспечения горизонтального масштабирования и поддержки реляционных моделей данных. Стоимость миграции или создания приложений с использованием распределённой базы данных, обеспечивающей реляционные операции посредством совмещения, часто существенно ниже, чем переход к ограничительной модели данных (например, NoSQL), и, в отличие от одноузловой базы данных, её можно масштабировать по мере роста бизнеса. Миграция существующей базы данных более подробно описана в разделе Миграция существующего приложения.
J.5.8.2.4.7. Производительность запросов #
В citus входящие запросы распараллеливаются и разбиваются на несколько запросов-фрагментов («задач»), которые выполняются параллельно в сегментах рабочего узла. Это позволяет citus использовать вычислительную мощность всех узлов кластера, а также отдельных ядер на каждом узле для каждого запроса. Благодаря такому распараллеливанию можно получить производительность, которая складывается из вычислительной мощности всех ядер кластера, что приводит к значительному сокращению времени выполнения запросов по сравнению с Postgres Pro на одном сервере.
В citus при планировании SQL-запросов используется двухэтапный оптимизатор. На первом этапе SQL-запросы преобразуются в коммутативную и ассоциативную форму, чтобы их можно было передавать и выполнять на рабочих узлах параллельно. Как обсуждалось в предыдущих разделах, выбор правильного столбца и метода распределения позволяет планировщику распределённых запросов применять несколько оптимизаций. Это может существенно повлиять на производительность запросов за счёт снижения сетевого ввода-вывода.
Распределённый исполнитель расширения citus затем отправляет эти фрагменты запроса рабочим экземплярам Postgres Pro. Некоторые параметры как распределённого планировщика, так и исполнителя можно настроить для повышения производительности. Когда фрагменты запроса отправляются рабочим узлам, начинается второй этап оптимизации. Рабочие узлы запускают серверы Postgres Pro и применяют стандартную логику планирования и исполнения Postgres Pro для выполнения этих фрагментов SQL-запросов. Таким образом, любая оптимизация, которая помогает Postgres Pro, также помогает и citus. В Postgres Pro параметры использования ресурсов по умолчанию имеют неоптимальные значения, так что их оптимизация может значительно сократить время выполнения запросов.
Соответствующие шаги по настройке производительности описаны в разделе Настройка производительности запросов.
J.5.8.3. Миграция существующего приложения #
Для миграции существующего приложения в citus иногда требуется настройка схемы и запросов для достижения оптимальной производительности. Расширение citus предоставляет для Postgres Pro функциональность распределения, но сегментирование на основе строк не является универсальным решением, позволяющим масштабировать все рабочие нагрузки. Высокая производительность кластера citus предполагает тщательную разработку модели данных, выбор инструментов и используемых SQL-функций.
В citus есть ещё один режим работы, называемый сегментирование на основе схем, и хотя сегментирование на основе строк показывает лучшую производительность и эффективность использования оборудования, сегментирование на основе схем может применяться в качестве более универсального подхода.
Первые шаги направлены на оптимизацию существующей схемы базы данных для эффективной работы на нескольких компьютерах.
На следующем шаге код приложения и запросы модифицируются для работы с изменённой схемой.
После тестирования изменений в среде разработки последним шагом будет перенос производственных данных в кластер citus и переключение рабочего приложения. Время простоя на данном этапе может быть минимизировано.
J.5.8.3.1. Определение стратегии распределения #
J.5.8.3.1.1. Выбор ключа распределения #
Первым шагом при миграции в citus является определение подходящих ключей распределения и соответствующее планирование распределения таблиц. В многоарендных приложениях это обычно идентификаторы арендаторов самого приложения, в citus называемый tenant_ID. Варианты использования могут различаться, поэтому рекомендуется тщательно продумать этот шаг.
За дополнительными рекомендациями обратитесь к следующим разделам:
Чтобы выбрать наиболее подходящий ключ распределения, проанализируйте среду. Для этого изучите структуру схем, большие таблицы, длительные и/или проблемные запросы, стандартные варианты использования и т. д.
J.5.8.3.1.2. Определение типов таблиц #
После выбора ключа распределения изучите схему, чтобы определить, как будет обрабатываться каждая таблица и потребуются ли какие-либо изменения в структурах таблиц.
Как правило, таблицы попадают в одну из следующих категорий:
Готовые к распределению. Такие таблицы уже содержат ключ распределения.
Требующие заполнения. Такие таблицы могут логически распределяться по выбранному ключу, но не содержат явно ссылающегося на него столбца. Соответствующий столбец будет добавлен в них позднее.
Таблицы-справочники. Такие таблицы обычно небольшого размера, не содержат ключа распределения, соединяются по распределённым таблицам и /или являются общими для арендаторов. Копия каждой такой таблицы хранится на всех узлах. Примеры таких таблиц: поиск по кодам стран, категориям продуктов и т. п.
Локальные таблицы. Такие таблицы обычно не соединяются с другими таблицами и не содержат ключа распределения. Они хранятся только на узле-координаторе. Примеры: поиск по пользователям и прочие вспомогательные таблицы.
Рассмотрим пример многоарендного приложения, подобного Etsy или Shopify, где каждый арендатор — это магазин. Упрощённая схема представлена на диаграмме ниже. (Подчёркнутые элементы — первичные ключи, а выделенные курсивом элементы — внешние ключи.)
Рисунок J.14. Пример упрощённой схемы
В этом примере магазины являются естественными арендаторами. В данном случае tenant_id — это store_id. После распределения таблиц в кластере строки, относящиеся к одному и тому же хранилищу, должны располагаться вместе на одних и тех же узлах.
J.5.8.3.2. Подготовка исходных таблиц к миграции #
После определения объёма необходимых изменений БД следующим важным шагом является изменение структуры данных существующей БД. Сначала в таблицы, требующие заполнения, добавляется столбец ключа распределения.
J.5.8.3.2.1. Добавление ключей распределения #
В нашем примере с витриной у таблиц магазинов и продуктов есть store_id, и они готовы к распределению. После нормализации в таблице line_items отсутствует store_id, но он нужен для распределения.
-- Денормализация таблицы line_items путём добавления столбца store_id ALTER TABLE line_items ADD COLUMN store_id uuid;
Обязательно проверьте, что столбец распределения имеет один и тот же тип во всех таблицах, например не смешивайте типы int и bigint. Типы столбцов должны совпадать для обеспечения правильного совмещения данных.
J.5.8.3.2.2. Заполнение созданных столбцов #
После изменения схемы добавьте недостающие значения для столбца tenant_id в соответствующих таблицах. В примере в таблице line_items требуются значения для store_id.
Таблица заполняется путём получения недостающих значений из запроса на соединение с заказами:
UPDATE line_items SET store_id = orders.store_id FROM line_items INNER JOIN orders WHERE line_items.order_id = orders.order_id;
Заполнение всей таблицы сразу может значительно увеличить нагрузку на базу данных и препятствовать выполнению других запросов. Во избежание этого можно замедлить заполнение. Например, можно создать функцию, выполняющую заполнение порционно, а затем многократно вызывать её с помощью pg_cron.
-- Функция для заполнения до десяти тысяч
-- строк в таблице line_items
CREATE FUNCTION backfill_batch()
RETURNS void LANGUAGE sql AS $$
WITH batch AS (
SELECT line_items_id, order_id
FROM line_items
WHERE store_id IS NULL
LIMIT 1000
FOR UPDATE
SKIP LOCKED
)
UPDATE line_items AS li
SET store_id = orders.store_id
FROM batch, orders
WHERE batch.line_item_id = li.line_item_id
AND batch.order_id = orders.order_id;
$$;
-- Функция выполняется каждые 15 минут
SELECT cron.schedule('*/15 * * * *', 'SELECT backfill_batch()');
-- Обратите внимание на возвращаемое значение cron.scheduleКак только будут заполнены все строки, задание cron можно отключить:
-- Предполагается, что 42 — полученный идентификатор -- задания cron.schedule SELECT cron.unschedule(42);
J.5.8.3.3. Подготовка приложения для работы с citus #
J.5.8.3.3.1. Настройка кластера разработки citus #
При изменении приложения для работы с citus нужна тестовая база данных. Чтобы настроить расширение, используйте инструкции в разделе Установка citus на одном узле.
Затем создайте резервную копию схемы БД пользовательского приложения и восстановите схему в новой БД разработки.
# получение схемы из исходной БД pg_dump \ --format=plain \ --no-owner \ --schema-only \ --file=schema.sql \ --schema=target_schema \ postgres://user:pass@host:5432/db # загрузка схемы в тестовую БД psql postgres://user:pass@testhost:5432/db -f schema.sql
Схема должна содержать ключ распределения (tenant_id) во всех таблицах, которые планируется распределять. Прежде чем запускать pg_dump для схемы, обязательно подготовьте исходные таблицы к миграции.
J.5.8.3.3.1.1. Добавление столбца распределения в ключи #
Расширение citus не может применять ограничения уникальности, если уникальный индекс или первичный ключ не содержит столбец распределения. Поэтому для используемого примера в первичный и внешний ключи должен быть добавлен store_id.
Некоторые библиотеки, перечисленные в следующем разделе, могут помочь в переносе схемы базы данных путём добавления столбца распределения в ключи. Однако ниже представлен пример базовых SQL-команд для преобразования простых ключей в составные в БД разработки:
BEGIN; -- Удаление простых первичных ключей (с каскадным удалением внешних ключей) ALTER TABLE products DROP CONSTRAINT products_pkey CASCADE; ALTER TABLE orders DROP CONSTRAINT orders_pkey CASCADE; ALTER TABLE line_items DROP CONSTRAINT line_items_pkey CASCADE; -- Пересоздание первичных ключей с будущим столбцом распределения ALTER TABLE products ADD PRIMARY KEY (store_id, product_id); ALTER TABLE orders ADD PRIMARY KEY (store_id, order_id); ALTER TABLE line_items ADD PRIMARY KEY (store_id, line_item_id); -- Пересоздание внешних ключей с будущим столбцом распределения ALTER TABLE line_items ADD CONSTRAINT line_items_store_fkey FOREIGN KEY (store_id) REFERENCES stores (store_id); ALTER TABLE line_items ADD CONSTRAINT line_items_product_fkey FOREIGN KEY (store_id, product_id) REFERENCES products (store_id, product_id); ALTER TABLE line_items ADD CONSTRAINT line_items_order_fkey FOREIGN KEY (store_id, order_id) REFERENCES orders (store_id, order_id); COMMIT;
После завершения схема из предыдущего раздела будет выглядеть так (подчёркнутые элементы — первичные ключи, выделенные курсивом элементы — внешние ключи):
Рисунок J.15. Пример упрощённой схемы
Обязательно измените потоки данных, чтобы добавить к входящим данным ключи.
J.5.8.3.3.2. Добавление в запросы ключа распределения #
Как только ключ распределения появится во всех соответствующих таблицах, приложение должно включить его в запросы. Выполните следующие шаги, используя копию приложения в среде разработки, и протестируйте её на сервере citus. После работы приложения с расширением станет понятно, как перенести производственные данные из исходной базы данных в реальный кластер citus.
Необходимо изменить код приложения и любые другие процессы приёма данных, записывающие данные в таблицы, чтобы включить новые столбцы.
Хороший способ определить, какие части кода нуждаются в изменении, — запустить пакет тестов приложения для модифицированной схемы в citus.
Рекомендуется включить журналирование базы данных. Журналы могут помочь обнаружить затерявшиеся межсегментные запросы в многоарендном приложении, которые следует преобразовать в запросы для каждого арендатора.
Межсегментные запросы также поддерживаются, но в многоарендном приложении большинство запросов должно направляться на один узел. Для простых запросов SELECT, UPDATE и DELETE это означает, что предложение WHERE должно фильтроваться по tenant_id. Тогда citus сможет эффективно выполнять эти запросы на одном узле.
Существуют вспомогательные библиотеки для ряда популярных платформ, которые позволяют легко добавлять tenant_id в запросы:
Библиотеки можно использовать сначала для записи данных в базу (включая поглощение данных), а затем для чтения. Например, в пакете activerecord-multi-tenant есть файл write-only mode (режим только для записи), который изменяет только пишущие запросы.
J.5.8.3.3.2.1. Прочее (принципы SQL) #
Если используется объектно-реляционное отображение (ORM), отличное от приведённых выше, или выполняются многоарендные запросы непосредственно в SQL, следуйте этим общим принципам. Далее будет использоваться предыдущий пример приложения для электронной торговли.
Предположим, необходимо получить подробную информацию о заказе. Распределённые запросы с фильтром по tenant_id наиболее эффективно выполняются в многоарендных приложениях, поэтому приведённое ниже изменение ускоряет запрос (хотя оба запроса возвращают одинаковые результаты):
-- До SELECT * FROM orders WHERE order_id = 123; -- После SELECT * FROM orders WHERE order_id = 123 AND store_id = 42; -- <== added
Столбец tenant_id не только полезен, но и важен для операторов INSERT. Вставки должны включать значение для столбца tenant_id, иначе citus не сможет перенаправить данные в правильный сегмент и выдаст ошибку.
Наконец, при соединении таблиц обязательно используйте фильтр по tenant_id. Например, можно узнать, сколько пар «отличных шерстяных штанов» было продано в данном магазине:
-- Можно включить в соединение store_id и
-- добавить фильтр по этому значению в один из запросов
SELECT sum(l.quantity)
FROM line_items l
INNER JOIN products p
ON l.product_id = p.product_id
AND l.store_id = p.store_id
WHERE p.name='Awesome Wool Pants'
AND l.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'
-- Либо можно не добавлять store_id в условие соединения,
-- но отфильтровать по нему обе таблицы. Это может быть полезно
-- при построении запросов в ORM
SELECT sum(l.quantity)
FROM line_items l
INNER JOIN products p ON l.product_id = p.product_id
WHERE p.name='Awesome Wool Pants'
AND l.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'
AND p.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'J.5.8.3.3.3. Включение безопасных соединений #
Клиенты должны подключаться к citus с помощью SSL, чтобы защитить информацию и предотвратить атаки посредника.
J.5.8.3.3.3.1. Проверка межузлового трафика #
В случае большой и сложной кодовой базы приложения некоторые запросы, генерируемые приложением, зачастую могут быть упущены из виду и, следовательно, к ним не будет применён фильтр tenant_id. Параллельный исполнитель citus будет успешно выполнять эти запросы, поэтому во время тестирования они остаются скрытыми, если приложение работает штатно. Однако если запрос не содержит фильтр tenant_id, исполнитель citus будет обрабатывать сегменты параллельно, но только один из них вернёт данные. Это бесполезная трата ресурсов, которая может стать проблемой при переходе в производственную среду с более высокой пропускной способностью.
Чтобы предотвратить возникновение таких проблем после запуска в производство, можно установить значение параметра конфигурации для журналирования запросов, которые затрагивают более одного сегмента. В правильно настроенном и перенесённом многоарендном приложении каждый запрос должен одновременно обращаться только к одному сегменту.
Во время тестирования можно создать следующую конфигурацию:
-- Задайте нужное имя базы данных ALTER DATABASE citus SET citus.multi_task_query_log_level = 'error';
При таком значении citus выдаст ошибку при появлении запросов, которые относятся к нескольким сегментам. Вывод ошибок во время тестирования позволяет разработчикам приложений находить и переносить такие запросы.
Во время запуска в производство можно настроить тот же параметр для внесения записей в журнал вместо вывода ошибок:
ALTER DATABASE citus SET citus.multi_task_query_log_level = 'log';
Для получения дополнительной информации о допустимых значениях обратитесь к описанию citus.multi_task_query_log_level.
J.5.8.3.4. Перенос производственных данных #
На данном этапе, после изменения схемы базы данных и запросов приложения для работы с citus, можно приступать к последнему шагу: переносу данных в кластер citus и переключению приложения на новую базу данных. Процедура миграции данных представлена в разделе Миграция базы данных.
J.5.8.3.4.1. Миграция базы данных #
Для небольших сред, допускающих незначительный простой, используйте процессы pg_dump/pg_restore. Для этого выполните следующие шаги:
Сохраните структуру БД из БД разработки:
pg_dump \ --format=plain \ --no-owner \ --schema-only \ --file=schema.sql \ --schema=
целевая_схема\ postgres://user:pass@host:5432/dbПодключитесь к кластеру citus с помощью psql и создайте схему:
\i schema.sql
Вызовите функции create_distributed_table и create_reference_table. Если при этом выдаётся сообщение об ошибке, связанной с внешними ключами, обычно это вызвано порядком операций. Удалите внешние ключи перед распределением таблиц, а затем добавьте их заново.
Переведите приложение в режим обслуживания и исключите любые операции записи в старую базу данных.
Сохраните данные из исходной производственной базы данных на диск с помощью pg_dump:
pg_dump \ --format=custom \ --no-owner \ --data-only \ --file=data.dump \ --schema=
целевая_схема\ postgres://user:pass@host:5432/dbИмпортируйте данные в citus с помощью pg_restore:
# обратите внимание, что используются данные подключения к citus, # а не к исходной БД pg_restore \ --host=
узел\ --dbname=имя_БД\ --username=имя_пользователя\ data.dump # будет запрошен пароль для подключенияПротестируйте приложение.
J.5.8.4. Справка по SQL #
J.5.8.4.1. Создание и изменение распределённых объектов (DDL) #
J.5.8.4.1.1. Создание и распределение схем #
В citus поддерживается сегментирование на основе схем, позволяющее распределять схемы. Распределённые схемы автоматически связываются с отдельными группами совмещения, так что таблицы, созданные в этих схемах, будут автоматически преобразованы в совмещённые распределённые таблицы без ключа сегментирования.
Есть два способа распределения схемы в citus:
Вызовом функции citus_schema_distribute вручную:
SELECT citus_schema_distribute('user_service');Данный метод позволяет преобразовывать существующие обычные схемы в распределённые.
Примечание
Распределять можно только схемы, не содержащие распределённых таблиц и таблиц-справочников.
Включением параметра конфигурации enable_schema_based_sharding:
SET citus.enable_schema_based_sharding TO ON; CREATE SCHEMA AUTHORIZATION user_service;
Параметр можно изменять для текущего сеанса или глобально в файле
postgresql.conf. Если для параметра установлено значениеON, все созданные схемы по умолчанию распределяются.В процессе распределения схема будет автоматически назначена существующему узлу в кластере и перемещена на него. Фоновый процесс перебалансировки сегментов использует эти схемы и все таблицы в них при перебалансировке кластера, выполнении оптимальных перемещений и миграции схем между узлами кластера.
Чтобы преобразовать схему обратно в обычную схему Postgres Pro, используйте функцию citus_schema_undistribute:
SELECT citus_schema_undistribute('user_service');Таблицы и данные в схеме user_service будут перемещены с текущего узла обратно на узел-координатор в кластере.
J.5.8.4.1.2. Создание и распределение таблиц #
Чтобы создать распределённую таблицу, сначала необходимо определить схему таблицы. Для этого можно создать таблицу с помощью команды CREATE TABLE, как это обычно делается в Postgres Pro.
CREATE TABLE github_events
(
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
actor jsonb,
org jsonb,
created_at timestamp
);Затем можно использовать функцию create_distributed_table, чтобы указать столбец распределения таблицы и создать сегменты рабочих узлов.
SELECT create_distributed_table('github_events', 'repo_id');Эта функция сообщает citus, что таблица github_events должна быть распределена по столбцу repo_id (путём хеширования значения столбца). Функция также создаёт сегменты на рабочих узлах, используя параметр конфигурации citus.shard_count.
В этом примере будет создано указанное в citus.shard_count количество сегментов, каждый из которых владеет частью пространства хеш-токена. После создания сегментов эта функция сохраняет все распределённые метаданные на узле-координаторе.
Каждому созданному сегменту присваивается уникальный идентификатор shard_id. Каждый сегмент представлен на рабочем узле как обычная таблица Postgres Pro с именем tablename_shardid, где tablename — имя распределённой таблицы, а shardid — уникальный идентификатор, присвоенный этому сегменту. К каждому экземпляру Postgres Pro можно подключаться для просмотра или выполнения команды на отдельных сегментах.
Теперь можно вставлять данные в распределённую таблицу и обращаться к ней. Используемая здесь функция подробно описана в разделе Вспомогательные функции citus.
J.5.8.4.1.2.1. Таблицы-справочники #
Вышеописанный метод распределяет таблицы на несколько горизонтальных сегментов, но есть и другой подход: распределить таблицы в один сегмент и реплицировать этот сегмент на каждый рабочий узел. Таблицы, распределённые таким образом, называются таблицами-справочниками. Они используются для хранения данных, к которым требуется частый доступ со стороны нескольких узлов в кластере.
Следующие таблицы хорошо подходят для преобразования в таблицы-справочники:
Маленькие таблицы, которые нужно соединять с большими распределёнными таблицами.
Таблицы в многоарендных приложениях без столбца
tenant_idили несвязанные с арендатором. (В некоторых случаях, чтобы упростить миграцию, пользователи могут создавать таблицы-справочники даже из тех таблиц, которые связаны с арендатором, но не содержат идентификатора арендатора.)Таблицы небольшого размера, которым необходимы уникальные ограничения для нескольких столбцов.
Например, предположим, что многоарендному сайту для электронной торговли необходимо рассчитать налог с продаж для транзакций в любом из магазинов. Налоговая информация не является уникальной для какого-либо арендатора, поэтому разумно объединить её в общую таблицу. Таблица-справочник, ориентированная на США, может выглядеть так:
-- Таблица-справочник
CREATE TABLE states (
code char(2) PRIMARY KEY,
full_name text NOT NULL,
general_sales_tax numeric(4,3)
);
-- Распределение этой таблицы на все рабочие узлы
SELECT create_reference_table('states');Теперь такие запросы, как расчёт налога для корзины покупок, можно соединять в таблице states без сетевых издержек и добавлять внешний ключ к коду штата для улучшения проверки.
Помимо распределения таблицы как единого реплицированного сегмента, функция create_reference_table помечает её как таблицу-справочник в таблицах метаданных citus. В расширении автоматически выполняется двухфазная фиксация изменений в помеченных таким образом таблицах, что гарантирует согласованность данных.
Существующую распределённую таблицу можно преобразовать в таблицу-справочник так:
SELECT undistribute_table('имя_таблицы');
SELECT create_reference_table('имя_таблицы');Ещё один пример использования таблиц-справочников в многоарендном приложении описан в разделе Обмен данными между арендаторами.
J.5.8.4.1.2.2. Распределение данных узла-координатора #
Если существующую базу данных Postgres Pro преобразовать в узел-координатор для кластера citus, данные в таблицах можно распределить эффективно с минимальными помехами для приложения.
Описанная ранее функция create_distributed_table работает как с пустыми, так и с непустыми таблицами, причём в последних она автоматически распределяет строки таблицы по кластеру. Показателем проведённого распределения служит следующее сообщение: NOTICE: Copying data from local table... (ВНИМАНИЕ: Идёт копирование из локальной таблицы). Например:
CREATE TABLE series AS SELECT i FROM generate_series(1,1000000) i;
SELECT create_distributed_table('series', 'i');
NOTICE: Copying data from local table...
NOTICE: copying the data has completed
DETAIL: The local data in the table is no longer visible, but is still on disk.
HINT: To remove the local data, run: SELECT truncate_local_data_after_distributing_table($$public.series$$)
create_distributed_table
--------------------------
(1 row)Операции записи в таблицу блокируются на время переноса данных, а ожидающие операции записи обрабатываются как распределённые запросы после фиксации функции. (Если функция завершается ошибкой, запросы снова становятся локальными.) Операции чтения могут продолжаться как обычно и станут распределёнными запросами после фиксации функции.
При распределении таблиц A и B, где A ссылается на B по внешнему ключу, сначала распределите таблицу B. Если сделать это в неправильном порядке, возникнет ошибка:
ERROR: cannot create foreign key constraint DETAIL: Referenced table must be a distributed table or a reference table.
Если распределение в правильном порядке невозможно, удалите внешние ключи, распределите таблицы и заново создайте внешние ключи.
После распределения таблиц используйте функцию truncate_local_data_after_distributing_table для удаления локальных данных. Оставшиеся локальные данные в распределённых таблицах недоступны для запросов citus и могут вызывать нарушения ограничений на узле-координаторе.
J.5.8.4.1.3. Совмещение таблиц #
Совмещение (или совместное размещение) — это практика тактического разделения данных и хранения связанной информации на одних и тех же компьютерах для обеспечения эффективных реляционных операций с использованием преимуществ горизонтального масштабирования для всего набора данных. За дополнительной информацией и примерами обратитесь к разделу Совмещение таблиц.
Таблицы совмещаются в группы. Чтобы вручную управлять назначением таблицы к группе для совмещения, используйте необязательный параметр colocate_with функции create_distributed_table. Если неважно, в какую группу будет включена таблица, не указывайте этот параметр. По умолчанию используется значение 'default', при котором таблица группируется с любой другой таблицей с таким же типом столбца распределения и таким же количеством сегментов. Если необходимо отменить или изменить такую автоматическую группировку, можно использовать функцию update_distributed_table_colocation.
-- Эти таблицы неявно совмещены с использованием одного и того же
-- столбца распределения и количества сегментов со стандартной
-- группой совмещения
SELECT create_distributed_table('A', 'столбец_int');
SELECT create_distributed_table('B', 'другой_столбец_int');Если новая таблица не связана с другими таблицами в предполагаемой неявной группе совмещения, укажите colocated_with => 'none'.
-- Не совмещена с другими таблицами
SELECT create_distributed_table('A', 'foo', colocate_with => 'none');Разделение несвязанных таблиц в отдельные группы совмещения улучшит производительность перебалансировки сегментов, поскольку сегменты в одной группе должны перемещаться вместе.
Если таблицы действительно связаны (например, когда они соединяются), разумно явно разместить их вместе. Выгоды от грамотного совмещения важнее, чем любые издержки перебалансировки.
Для явного совмещения нескольких таблиц выполните распределение одной из них, а затем поместите остальные в её группу совмещения. Например:
-- Распределение таблицы с магазинами
SELECT create_distributed_table('stores', 'store_id');
-- Добавление новых таблиц в группу к первой таблице
SELECT create_distributed_table('orders', 'store_id', colocate_with => 'stores');
SELECT create_distributed_table('products', 'store_id', colocate_with => 'stores');Информация о группах совмещения хранится в таблице pg_dist_colocation, а в таблице pg_dist_partition показана принадлежность таблиц к группам.
J.5.8.4.1.4. Удаление таблиц #
Чтобы удалить распределённую таблицу, можно использовать стандартную команду Postgres Pro DROP TABLE. Как и в случае с обычными таблицами, команда DROP TABLE удаляет все индексы, правила, триггеры и ограничения, существующие для целевой таблицы, а также удаляет сегменты на рабочих узлах и очищает их метаданные.
DROP TABLE github_events;
J.5.8.4.1.5. Изменение таблиц #
В citus многие виды DDL-операторов автоматически транслируются, то есть при изменении распределённой таблицы на узле-координаторе также изменяются сегменты на рабочих узлах. Другие DDL-операторы требуется транслировать вручную, а некоторые другие запрещены, например изменяющие столбец распределения. Попытка запустить DDL-оператор, который нельзя транслировать автоматически, вызовет ошибку, и таблицы на узле-координаторе не изменятся.
Ниже приводится справочник на категории DDL-операторов, которые транслируются автоматически. Обратите внимание, что трансляцию можно включить или отключить с помощью параметра конфигурации citus.enable_ddl_propagation.
J.5.8.4.1.5.1. Добавление/изменение столбцов #
В citus большинство команд ALTER TABLE транслируется автоматически. Добавление столбцов или изменение их значений по умолчанию работает так же, как и в базе данных Postgres Pro на одном компьютере:
-- Добавление столбца ALTER TABLE products ADD COLUMN description text; -- Изменение значения по умолчанию ALTER TABLE products ALTER COLUMN price SET DEFAULT 7.77;
Существенные изменения в существующем столбце, например переименование или изменение типа данных, также допустимы. Однако нельзя изменять тип данных столбца распределения. Этот столбец определяет, как данные таблицы распределяются по кластеру citus, и такое изменение потребует перемещения данных.
Попытка изменить тип данных столбца вызовет ошибку:
-- Предположим, что store_id типа integer является столбцом -- распределения для таблицы products ALTER TABLE products ALTER COLUMN store_id TYPE text; /* ERROR: cannot execute ALTER TABLE command involving partition column */
Чтобы обойти это ограничение, можно переключить столбец распределения с помощью функции alter_distributed_table, изменить старый столбец и переключиться обратно.
J.5.8.4.1.5.2. Добавление/удаление ограничений #
Применение citus позволяет пользоваться преимуществами безопасности реляционной базы данных, включая ограничения. Из-за особенностей распределённых систем в citus не будет перекрёстных ссылок на ограничения уникальности или ссылочной целостности между рабочими узлами.
Чтобы настроить внешний ключ между совмещёнными распределёнными таблицами, всегда добавляйте в ключ столбец распределения. Для этого может потребоваться сделать ключ составным.
Внешние ключи могут создаваться в следующих случаях:
Для двух локальных (нераспределённых) таблиц,
Для двух таблиц-справочников,
между таблицами-справочниками и локальными таблицами (по умолчанию включается с помощью параметра конфигурации citus.enable_local_reference_table_foreign_keys),
Между двумя совмещёнными распределёнными таблицами, если ключ содержит столбец распределения, или
Как ссылка из распределённой таблицы на таблицу-справочник.
Внешние ключи от таблиц-справочников к распределённым таблицам не поддерживаются.
В citus поддерживаются все ссылочные действия с внешними ключами от локальных таблиц к таблицам-справочникам, но не поддерживаются ON DELETE/ON UPDATE CASCADE в обратном направлении (ссылки на локальные таблицы).
Примечание
Первичные ключи и ограничения уникальности должны содержать столбец распределения. Добавление столбца, не предназначенного для распределения, приведёт к ошибке создание уникальных индексов для несекционированных столбцов пока не поддерживается.
В примере ниже показано, как создавать первичные и внешние ключи в распределённых таблицах:
--
-- Добавление первичного ключа
-- --------------------
-- Распределите эти таблицы по account_id. Таблицы ads и clicks
-- должны использовать составные ключи, содержащие account_id.
ALTER TABLE accounts ADD PRIMARY KEY (id);
ALTER TABLE ads ADD PRIMARY KEY (account_id, id);
ALTER TABLE clicks ADD PRIMARY KEY (account_id, id);
-- Затем распределите таблицы
SELECT create_distributed_table('accounts', 'id');
SELECT create_distributed_table('ads', 'account_id');
SELECT create_distributed_table('clicks', 'account_id');
--
-- Добавьте внешние ключи
-- -------------------
-- Обратите внимание, что это может произойти как до, так и после распределения, если
-- существует ограничение уникальности на целевые столбцы, которое
-- можно установить только до распределения.
ALTER TABLE ads ADD CONSTRAINT ads_account_fk
FOREIGN KEY (account_id) REFERENCES accounts (id);
ALTER TABLE clicks ADD CONSTRAINT clicks_ad_fk
FOREIGN KEY (account_id, ad_id) REFERENCES ads (account_id, id);Аналогичным образом добавьте столбец распределения в ограничения уникальности:
-- Допустим, в каждой рекламе должно использоваться уникальное изображение. Обратите внимание, -- что при распределении по account_id ограничение можно установить ограничение только по учётной записи. ALTER TABLE ads ADD CONSTRAINT ads_unique_image UNIQUE (account_id, image_url);
Ограничения NOT NULL можно применять к любому столбцу (распределения или нет), поскольку для них не требуется поиск между рабочими узлами.
ALTER TABLE ads ALTER COLUMN image_url SET NOT NULL;
J.5.8.4.1.5.3. Использование ограничений NOT VALID #
В некоторых ситуациях может оказаться полезным применять ограничения для новых строк, оставляя при этом существующие несоответствующие строки неизменными. В citus эта функциональность поддерживается для ограничений CHECK и внешних ключей с использованием ограничения Postgres Pro NOT VALID.
Рассмотрим приложение, которое хранит профили пользователей в таблице-справочнике.
-- Здесь используется столбец типа text, но в настоящем приложении
-- может использоваться citext, который доступен в
-- дополнительном модуле Postgres Pro (contrib)
CREATE TABLE users ( email text PRIMARY KEY );
SELECT create_reference_table('users');Представим, что в таблицу попадает несколько некорректных адресов.
INSERT INTO users VALUES
('foo@example.com'), ('hacker12@aol.com'), ('lol');Нужно проверить адреса, но Postgres Pro обычно не позволяет добавлять ограничение CHECK, которому не удовлетворяют существующие строки. Однако допускается ограничение NOT VALID:
ALTER TABLE users
ADD CONSTRAINT syntactic_email
CHECK (email ~
'^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$'
) NOT VALID;Эти команды выполнятся успешно, и новые строки будут проверяться.
INSERT INTO users VALUES ('fake');
/*
ERROR: new row for relation "users_102010" violates
check constraint "syntactic_email_102010"
DETAIL: Failing row contains (fake).
*/Позднее, в часы низкой нагрузки, администратор базы данных может попытаться исправить некорректные строки и перепроверить ограничение.
-- Попытка проверить все строки позднее ALTER TABLE users VALIDATE CONSTRAINT syntactic_email;
В документации Postgres Pro подробно описаны ограничения NOT VALID и VALIDATE CONSTRAINT в разделе ALTER TABLE.
J.5.8.4.1.5.4. Добавление/удаление индексов #
В citus поддерживается добавление и удаление индексов:
-- Добавление индекса CREATE INDEX clicked_at_idx ON clicks USING BRIN (clicked_at); -- Удаление индекса DROP INDEX clicked_at_idx;
Для добавления индекса требуется блокировка операций записи, что может быть нежелательно в многоарендной «системе записи». Чтобы свести к минимуму время простоя приложения, создавайте индекс параллельно. Этот метод более трудоёмок, чем стандартное построение индекса, и его выполнение занимает значительно больше времени. Однако, поскольку такой метод позволяет продолжать выполнение обычных операций во время построения индекса, он более предпочтителен для добавления новых индексов в производственной среде.
-- Добавление индекса без блокировки операций записи в таблицу CREATE INDEX CONCURRENTLY clicked_at_idx ON clicks USING BRIN (clicked_at);
J.5.8.4.1.6. Типы данных и функции #
Создание пользовательских типов данных и функций транслируется на рабочие узлы. Однако создание таких объектов базы данных в транзакциях с распределёнными операциями имеет некоторые особенности.
В citus такие операции, как create_distributed_table, распараллеливаются между сегментами с использованием нескольких соединений для каждого рабочего узла. Но при создании объекта базы данных citus транслирует эту операцию на рабочие узлы, используя одно соединение для каждого рабочего узла. Проблема возникает при объединении двух операций, поскольку в этом случае параллельные соединения не смогут увидеть объект, который был создан в рамках одного соединения, но ещё не был зафиксирован в базе данных.
Рассмотрим блок транзакции, в котором создаются тип данных и таблица, данные загружаются, и таблица распределяется:
BEGIN;
-- Создание типа в одном соединении:
CREATE TYPE coordinates AS (x int, y int);
CREATE TABLE positions (object_id text primary key, position coordinates);
-- Загрузка данных происходит по одному соединению:
SELECT create_distributed_table('positions', 'object_id');
\COPY positions FROM 'positions.csv'
COMMIT;В поведении citus по умолчанию ставится в приоритет согласованность схемы между узлом-координатором и рабочими узлами. У такого поведения есть недостатки: если трансляция объекта происходит после параллельной команды в той же транзакции, то транзакция не сможет завершиться. На это указывает ERROR в блоке кода ниже:
BEGIN;
CREATE TABLE items (key text, value text);
-- Параллельная загрузка данных:
SELECT create_distributed_table('items', 'key');
\COPY items FROM 'items.csv'
CREATE TYPE coordinates AS (x int, y int);
ERROR: cannot run type command because there was a parallel operation on a distributed table in the transactionЕсли встретилась такая проблема, существует простое решение: используйте параметр citus.multi_shard_modify_mode с установленным значением sequential, чтобы отключить распараллеливание для каждого узла. Загрузка данных в той же транзакции может происходить медленнее.
J.5.8.4.1.7. Внесение изменений вручную #
Большинство DDL-команд транслируется автоматически. Для любых других можно транслировать изменения вручную, см. раздел Ручная трансляция запросов.
J.5.8.4.2. Внесение и изменение данных (DML) #
J.5.8.4.2.1. Вставка данных #
Для вставки данных в распределённые таблицы можно использовать стандартную команду Postgres Pro INSERT. В примере ниже вставляются две случайные строки из набора данных GitHub Archive.
/*
CREATE TABLE github_events
(
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
actor jsonb,
org jsonb,
created_at timestamp
);
*/
INSERT INTO github_events VALUES (2489373118,'PublicEvent','t',24509048,'{}','{"id": 24509048, "url": "https://api.github.com/repos/SabinaS/csee6868", "name": "SabinaS/csee6868"}','{"id": 2955009, "url": "https://api.github.com/users/SabinaS", "login": "SabinaS", "avatar_url": "https://avatars.githubusercontent.com/u/2955009?", "gravatar_id": ""}',NULL,'2015-01-01 00:09:13');
INSERT INTO github_events VALUES (2489368389,'WatchEvent','t',28229924,'{"action": "started"}','{"id": 28229924, "url": "https://api.github.com/repos/inf0rmer/blanket", "name": "inf0rmer/blanket"}','{"id": 1405427, "url": "https://api.github.com/users/tategakibunko", "login": "tategakibunko", "avatar_url": "https://avatars.githubusercontent.com/u/1405427?", "gravatar_id": ""}',NULL,'2015-01-01 00:00:24');При вставке строк в распределённые таблицы необходимо указывать столбец распределения вставляемой строки. На основе столбца распределения citus определяет целевой сегмент, затем перенаправляет запрос в нужный сегмент, и на всех репликах этого сегмента выполняется удалённая команда INSERT.
Иногда удобно объединить несколько операторов INSERT в один, состоящий из нескольких строк, что может быть более эффективно, чем выполнять повторяющиеся запросы к базе данных. Таким образом можно загрузить сразу весь пример из предыдущего раздела:
INSERT INTO github_events VALUES
(
2489373118,'PublicEvent','t',24509048,'{}','{"id": 24509048, "url": "https://api.github.com/repos/SabinaS/csee6868", "name": "SabinaS/csee6868"}','{"id": 2955009, "url": "https://api.github.com/users/SabinaS", "login": "SabinaS", "avatar_url": "https://avatars.githubusercontent.com/u/2955009?", "gravatar_id": ""}',NULL,'2015-01-01 00:09:13'
), (
2489368389,'WatchEvent','t',28229924,'{"action": "started"}','{"id": 28229924, "url": "https://api.github.com/repos/inf0rmer/blanket", "name": "inf0rmer/blanket"}','{"id": 1405427, "url": "https://api.github.com/users/tategakibunko", "login": "tategakibunko", "avatar_url": "https://avatars.githubusercontent.com/u/1405427?", "gravatar_id": ""}',NULL,'2015-01-01 00:00:24'
);J.5.8.4.2.1.1. Распределённые свёртки #
В citus также поддерживаются операторы INSERT… SELECT, вставляющие строки на основе результатов запроса SELECT. Это удобный способ заполнения таблиц, который также позволяет выполнять команду UPSERT с помощью предложения ON CONFLICT, что является самым простым примером выполнения распределённой свёртки.
В citus существует три способа вставки с помощью оператора SELECT:
Первый можно использовать, если исходные таблицы и целевая таблица совмещены и оба оператора
SELECTвключают столбец распределения. В этом случае в citus можно вынести наружу оператор/INSERTINSERT… SELECTдля параллельного выполнения на всех узлах.Второй способ выполнения оператора
INSERT… SELECT— это пересекционирование результатов итогового набора на порции и отправка этих порций на рабочие узлы в соответствующие сегменты целевой таблицы. Каждый рабочий узел может вставлять значения в локальные целевые сегменты.Оптимизация пересекционирования может произойти, если запрос
SELECTне требует этапа слияния на узле-координаторе. Она не работает со следующей функциональностью SQL, для которой требуется этап слияния:ORDER BYLIMITOFFSETGROUP BY, когда столбец распределения не является частью ключа группированияОконные функции при секционировании не по столбцу распределения в исходной таблице (таблицах)
Соединения между несовмещёнными таблицами (т. е. соединения с пересекционированием)
Если исходная и целевая таблицы не совмещены и пересекционирование применить невозможно, citus использует третий способ выполнения
INSERT… SELECT. При этом результаты запросов к рабочим узлам передаются на узел-координатор. Узел-координатор перенаправляет строки обратно в соответствующий сегмент. Поскольку все данные должны проходить через один узел, этот метод менее эффективен.
Чтобы узнать, какой способ в данный момент используется в citus, используйте команду EXPLAIN, как описано в разделе Настройка Postgres Pro. Если в целевой таблице очень большое количество сегментов, возможно, стоит отключить пересекционирование, см. описание параметра конфигурации citus.enable_repartitioned_insert_select.
J.5.8.4.2.1.2. Команда \copy (массовая загрузка) #
Для массовой загрузки данных из файла можно напрямую использовать команду \copy.
Сначала загрузите пример с набором данных github_events:
wget http://examples.citusdata.com/github_archive/github_events-2015-01-01-{0..5}.csv.gz
gzip -d github_events-2015-01-01-*.gzЗатем скопируйте данные с помощью psql. Обратите внимание, что для этих данных требуется база данных с кодировкой UTF-8:
\COPY github_events FROM 'github_events-2015-01-01-0.csv' WITH (format CSV)
Примечание
Понятия изолированных снимков между сегментами не существует, то есть многосегментный запрос SELECT, который выполняется одновременно с командой \copy, может увидеть зафиксированные изменения в одних сегментах, а в других — нет. Если пользователь хранит данные о событиях, он может время от времени наблюдать небольшие пропуски в последних данных. Эта проблема может быть решена на уровне приложений (например, путём исключения последних данных из запросов или использования блокировки).
Если при выполнении команды \copy не удаётся открыть соединение для размещения сегмента, команда ведёт себя так же, как INSERT, а именно помечает размещение как неактивное, если нет других активных размещений. Если после подключения происходит какой-либо другой сбой, транзакция отменяется и, следовательно, метаданные не изменяются.
J.5.8.4.3. Кеширование агрегатных функций с помощью свёрток #
Такие приложения, как конвейеры данных о событиях и информационные панели для анализа в реальном времени, требуют выполнения запросов за доли секунды на больших объёмах данных. Один из способов ускорить эти запросы — заранее вычислить и сохранить агрегированные данные. Это называется «сворачиванием» данных и позволяет избежать затрат на обработку первичных данных во время выполнения запроса. Ещё одно преимущество сворачивания данных временных рядов в почасовую или ежедневную статистику — экономия места. Старые данные могут удаляться, когда больше нет необходимости хранить все подробности и достаточно агрегированных данных.
В примере рассматривается распределённая таблица для отслеживания просмотров страниц по URL:
CREATE TABLE page_views (
site_id int,
url text,
host_ip inet,
view_time timestamp default now(),
PRIMARY KEY (site_id, url)
);
SELECT create_distributed_table('page_views', 'site_id');После заполнения таблицы данными можно запустить запрос агрегирования для подсчёта просмотров страниц по URL за день, ограничивая выборку заданным сайтом и годом.
-- Количество просмотров по каждому URL-адресу за день на сайте 5
SELECT view_time::date AS day, site_id, url, count(*) AS view_count
FROM page_views
WHERE site_id = 5 AND
view_time >= date '2016-01-01' AND view_time < date '2017-01-01'
GROUP BY view_time::date, site_id, url;Описанная выше конфигурация работает, но имеет два недостатка. Во-первых, при многократном выполнении запроса агрегирования должны каждый раз прочитываться все связанные строки и вычисляться результаты для всего набора данных. Если этот запрос используется для отображения информационной панели, быстрее сохранить агрегированные результаты в таблице с ежедневными просмотрами страниц и запрашивать эту таблицу. Во-вторых, стоимость хранения будет расти пропорционально объёму данных и длине периода, за который доступны запросы. На практике лучше сохранять необработанные события в течение короткого периода времени, а для долгосрочного анализа использовать исторические графики.
Для хранения ежедневной статистики можно создать таблицу daily_page_views.
CREATE TABLE daily_page_views (
site_id int,
day date,
url text,
view_count bigint,
PRIMARY KEY (site_id, day, url)
);
SELECT create_distributed_table('daily_page_views', 'site_id');В этом примере таблицы page_views и daily_page_views распределяются по столбцу site_id. При этом гарантируется, что данные, соответствующие конкретному сайту, будут совмещены на одном узле. Сохранение строк двух таблиц вместе на каждом узле минимизирует сетевой трафик между узлами и обеспечивает выполнение с высокой степенью распараллеливания.
После создания новой распределённой таблицы можно выполнить INSERT INTO ... SELECT, чтобы объединить необработанные представления страниц в агрегированную таблицу. Далее агрегируются данные о просмотрах страниц каждый день. Пользователи citus часто ждут некоторое время после окончания дня, чтобы выполнить подобный запрос и учесть поздно поступающие данные.
-- Группирование вчерашних данных
INSERT INTO daily_page_views (day, site_id, url, view_count)
SELECT view_time::date AS day, site_id, url, count(*) AS view_count
FROM page_views
WHERE view_time >= date '2017-01-01' AND view_time < date '2017-01-02'
GROUP BY view_time::date, site_id, url;
-- Теперь результаты доступны прямо из таблицы
SELECT day, site_id, url, view_count
FROM daily_page_views
WHERE site_id = 5 AND
day >= date '2016-01-01' AND day < date '2017-01-01';Приведённый выше запрос со свёрткой агрегирует данные за предыдущий день и вставляет их в таблицу daily_page_views. Выполнение запроса один раз в день означает, что строки таблиц свёртки не нужно изменять, поскольку данные нового дня не влияют на предыдущие строки.
Ситуация меняется, когда приходится иметь дело с данными, поступающими с задержкой, или выполнять запрос со свёрткой более одного раза в день. Если новые строки уже соответствуют дням в таблице свёртки, количество совпадений должно увеличиться. Postgres Pro может справиться с этой ситуацией с помощью ON CONFLICT — встроенным методом выполнения UPSERTS. Ниже представлен пример этого метода.
-- Группирование, начиная с указанной даты,
-- ежедневные просмотры страницы изменяются при необходимости
INSERT INTO daily_page_views (day, site_id, url, view_count)
SELECT view_time::date AS day, site_id, url, count(*) AS view_count
FROM page_views
WHERE view_time >= date '2017-01-01'
GROUP BY view_time::date, site_id, url
ON CONFLICT (day, url, site_id) DO UPDATE SET
view_count = daily_page_views.view_count + EXCLUDED.view_count;J.5.8.4.3.1. Изменение и удаление #
Строки в распредёленных таблицах можно изменять или удалять с помощью стандартных команд Postgres Pro UPDATE и DELETE.
DELETE FROM github_events WHERE repo_id IN (24509048, 24509049); UPDATE github_events SET event_public = TRUE WHERE (org->>'id')::int = 5430905;
Когда операции UPDATE/DELETE затрагивают несколько сегментов, как в приведённом выше примере, citus по умолчанию использует протокол однофазной фиксации. Для повышения безопасности можно включить двухфазную фиксацию, установив параметр конфигурации citus.multi_shard_commit_protocol:
SET citus.multi_shard_commit_protocol = '2pc';
Если операция UPDATE или DELETE влияет только на один сегмент, то она выполняется в пределах одного рабочего узла, и в этом случае включение двухфазной фиксации не требуется. Такое часто происходит, когда операции обновления или удаления фильтруются по столбцу распределения таблицы:
-- Поскольку таблица github_events распределена по столбцу repo_id, -- операция будет выполняться на одном рабочем узле DELETE FROM github_events WHERE repo_id = 206084;
Кроме того, для работы с одним сегментом citus поддерживает метод SELECT… FOR UPDATE. Этот метод иногда используется объектно-реляционными преобразователями (ORM), чтобы безопасно выполнять следующие операции:
Загрузка строк
Вычисления в коде приложения
Изменение строк на основе вычислений
Обращение к строкам для изменения блокирует их для записи, чтобы другие процессы не могли вызвать потерю изменения («lost update»).
BEGIN; -- Обращение к событиям по repo_id, но -- с их блокировкой для записи SELECT * FROM github_events WHERE repo_id = 206084 FOR UPDATE; -- Вычисление нужного значения event_public с использованием -- логики приложения, использующей строки... -- Применение изменений UPDATE github_events SET event_public = :our_new_value WHERE repo_id = 206084; COMMIT;
Эта функция поддерживается только для таблиц, распределённых по хешу, и таблиц-справочников.
J.5.8.4.3.2. Повышение производительности записи #
Операторы INSERT и UPDATE/DELETE можно масштабировать примерно до 50 000 запросов в секунду на мощных компьютерах. Однако для достижения такой скорости придётся использовать множество параллельных долговременных соединений и подумать, как бороться с блокировками. Для получения дополнительной информации можно обратиться к разделу Масштабирование поглощения данных.
J.5.8.4.4. Запросы к распределённым таблицам (SQL) #
Как обсуждалось в предыдущих разделах, citus расширяет возможности Postgres Pro для выполнения распределённых вычислений. Это означает, что можно использовать стандартные запросы Postgres Pro SELECT на координаторе citus. Затем расширение распараллеливает запросы SELECT, включающие сложные выборки, группировки и упорядочивания, а также JOIN, чтобы ускорить их выполнение. На высоком уровне citus разбивает запрос SELECT на более мелкие фрагменты, назначает их рабочим узлам, контролирует их выполнение, объединяет результаты (и сортирует их, если необходимо) и возвращает итоговый результат пользователю.
В следующих разделах будут рассматриваться различные типы запросов, которые можно выполнять в citus.
J.5.8.4.4.1. Агрегатные функции #
В citus поддерживается и распараллеливается большинство агрегатных функций Postgres Pro, включая пользовательские агрегатные функции. Агрегатные функции выполняются с использованием одного из следующих методов в таком порядке приоритета:
Когда запрос агрегирует данные с группировкой по столбцу распределения таблицы, citus может передать выполнение всего запроса каждому рабочему узлу. В этой ситуации все агрегатные функции поддерживаются и выполняются параллельно на рабочих узлах. (Любые пользовательские агрегаты должны быть установлены на рабочих узлах.)
Если запрос агрегирует данные с группировкой не по столбцу распределения, citus всё равно может оптимизировать в зависимости от конкретного случая. В citus есть внутренние правила для определённых агрегатных функций, таких как
sum(),avg()иcount(distinct), которые позволяют перезаписывать запросы для частичного агрегирования на рабочих узлах. Например, чтобы вычислить среднее значение, citus получает сумму и количество от каждого рабочего узла, а затем узел-координатор вычисляет окончательное среднее значение.Полный список поддерживаемых агрегатных функций:
avg,min,max,sum,count,array_agg,jsonb_agg,jsonb_object_agg,json_agg,json_object_agg,bit_and,bit_or,bool_and,bool_or,every,hll_add_agg,hll_union_agg,topn_add_agg,topn_union_agg,any_value,tdigest(double precision, int),tdigest_percentile(double precision, int, double precision),tdigest_percentile(double precision, int, double precision[]),tdigest_percentile(tdigest, double precision),tdigest_percentile(tdigest, double precision[]),tdigest_percentile_of(double precision, int, double precision),tdigest_percentile_of(double precision, int, double precision[]),tdigest_percentile_of(tdigest, double precision),tdigest_percentile_of(tdigest, double precision[])В крайнем случае можно извлечь все строки из рабочих узлов и выполнить агрегирование на узле-координаторе. Если запрос агрегирует данные с группировкой не по столбцу распределения и не является одним из предопределённых особых случаев, citus возвращается к этому подходу. Он может приводить к сетевым издержкам и исчерпать ресурсы координатора, если набор данных, подлежащий агрегированию, слишком велик. (Ниже описано, как можно отключить этот подход.)
Имейте в виду, что небольшие изменения в запросе могут изменить режимы выполнения, приводя к неожиданным результатам. Например, для функции sum(x), с группированием не по столбцу распределения можно использовать распределённое выполнение, а для функции sum(distinct x) — подтянуть полный набор входных записей на узел-координатор.
Чтобы нарушить выполнение всего запроса, достаточно одного столбца. В приведённом ниже примере, если функция sum(distinct value2) должна выполнять группирование на узле-координаторе, то sum(value1) также придётся группировать там, даже если ей это не требовалось.
SELECT sum(value1), sum(distinct value2) FROM distributed_table;
Чтобы избежать случайной передачи данных узлу-координатору, можно установить параметр citus.coordinator_aggregation_strategy:
SET citus.coordinator_aggregation_strategy TO 'disabled';
Обратите внимание, что отключение стратегии агрегирования на узле-координаторе вообще не позволит выполнять агрегированные запросы «третьего типа».
J.5.8.4.4.1.1. Агрегатные функции count(distinct) #
В citus агрегатные функции count(distinct) поддерживаются несколькими способами. Если агрегатная функция count(distinct) выполняет агрегирование по столбцу распределения, citus может напрямую передавать запрос рабочим узлам. В противном случае citus запускает отдельные операторы SELECT на каждом рабочем узле и список возвращается координатору, где производится окончательный подсчёт.
Обратите внимание, что передача этих данных замедляется с увеличением количества отдельных элементов на рабочих узлах. Это особенно актуально для запросов, содержащих несколько агрегатных функций count(distinct), например:
-- Несколько различных функций count в одном запросе обычно выполняются медленно SELECT count(distinct a), count(distinct b), count(distinct c) FROM table_abc;
Для запросов такого типа отдельные операторы SELECT на рабочих узлах в результате по сути создают перекрёстное произведение строк, которые должны быть переданы координатору.
Для повышения производительности вместо этого можно выполнить приблизительный подсчёт. Выполните следующие шаги:
Загрузите и установите расширение hll на всех экземплярах Postgres Pro (узел-координатор и все рабочие узлы).
Найти расширение hll можно в репозитории проекта на GitHub.
Создайте расширение hll на всех экземплярах Postgres Pro, выполнив указанную команду на координаторе:
CREATE EXTENSION hll;
Включите приближение функций
count(distinct), установив параметр конфигурации citus.count_distinct_error_rate. Ожидается, что чем меньше значение этого параметра, тем точнее будут результаты, но для вычислений потребуется больше времени. Рекомендуемое значение —0.005.SET citus.count_distinct_error_rate TO 0.005;
После этого шага агрегатные функции
count(distinct)автоматически переключаются на использование hll без необходимости изменять запросы. При этом есть возможность запускать запросыcount(distinct)с приближением по любому столбцу таблицы.
Столбец HyperLogLog. Некоторые пользователи уже хранят данные в виде столбцов hll. В таких случаях они могут динамически группировать эти данные с помощью функции hll_union_agg(hll_column).
J.5.8.4.4.1.2. Расчёт первых N элементов #
Вычислить первые n элементов множества можно с помощью функций count, sort и limit. Однако по мере увеличения объёма данных этот метод становится медленным и ресурсоёмким. В таком случае эффективнее использовать приближение.
Расширение с открытым исходным кодом topn для Postgres Pro позволяет быстро получать приблизительные результаты для запросов типа «top-n». Расширение материализует первые значения в тип данных json. Расширение topn может постепенно обновлять эти значения или объединять их по мере необходимости по различным временным интервалам.
Прежде чем перейти к реалистичному примеру использования topn, рассмотрим, как работают некоторые его примитивные операции. Сначала topn_add изменяет объект JSON, подсчитывая количество просмотров ключа:
-- Сначала ничего нет, фиксируем, что увидели букву «а»
SELECT topn_add('{}', 'a');
-- => {"a": 1}
-- Фиксируем появление ещё одной буквы «а»
SELECT topn_add(topn_add('{}', 'a'), 'a');
-- => {"a": 2}Расширение также предоставляет агрегатные функции для сканирования нескольких значений:
-- Для normal_rand
CREATE EXTENSION tablefunc;
-- Подсчёт значений из нормального распределения
SELECT topn_add_agg(floor(abs(i))::text)
FROM normal_rand(1000, 5, 0.7) i;
-- => {"2": 1, "3": 74, "4": 420, "5": 425, "6": 77, "7": 3}Если количество уникальных значений превышает предельную величину, агрегирование удаляет информацию о тех значениях, которые встречаются реже всего. Это позволяет контролировать использование пространства. Предельную величину можно установить с помощью параметра конфигурации topn.number_of_counters. Значение по умолчанию — 1000.
Теперь рассмотрим более реалистичный пример работы topn. Для этого возьмём обзоры продуктов Amazon за 2000 год и воспользуемся topn для быстрой генерации запросов. Сначала загрузите набор данных:
curl -L https://examples.citusdata.com/customer_reviews_2000.csv.gz | \ gunzip > reviews.csv
Затем заполните распределённую таблицу этими данными:
CREATE TABLE customer_reviews
(
customer_id TEXT,
review_date DATE,
review_rating INTEGER,
review_votes INTEGER,
review_helpful_votes INTEGER,
product_id CHAR(10),
product_title TEXT,
product_sales_rank BIGINT,
product_group TEXT,
product_category TEXT,
product_subcategory TEXT,
similar_product_ids CHAR(10)[]
);
SELECT create_distributed_table('customer_reviews', 'product_id');
\COPY customer_reviews FROM 'reviews.csv' WITH CSVПосле этого добавьте расширение, создайте целевую таблицу для хранения данных JSON, сгенерированных topn, и вызовите ранее упоминавшуюся функцию topn_add_agg.
-- Выполните указанную команду на узле-кординаторе, она будет транслирована и на рабочие узлы
CREATE EXTENSION topn;
-- Таблица для материализации ежедневно агрегируемых данных
CREATE TABLE reviews_by_day
(
review_date date unique,
agg_data jsonb
);
SELECT create_reference_table('reviews_by_day');
-- Материализовать количество отзывов по каждому продукту за день для каждого покупателя
INSERT INTO reviews_by_day
SELECT review_date, topn_add_agg(product_id)
FROM customer_reviews
GROUP BY review_date;Теперь вместо того, чтобы писать сложную оконную функцию для customer_reviews, можно просто применить topn к reviews_by_day. Например, в следующем запросе находится наиболее часто просматриваемый продукт за каждый из первых пяти дней:
SELECT review_date, (topn(agg_data, 1)).* FROM reviews_by_day ORDER BY review_date LIMIT 5;
┌─────────────┬────────────┬───────────┐ │ review_date │ item │ frequency │ ├─────────────┼────────────┼───────────┤ │ 2000-01-01 │ 0939173344 │ 12 │ │ 2000-01-02 │ B000050XY8 │ 11 │ │ 2000-01-03 │ 0375404368 │ 12 │ │ 2000-01-04 │ 0375408738 │ 14 │ │ 2000-01-05 │ B00000J7J4 │ 17 │ └─────────────┴────────────┴───────────┘
Поля JSON, созданные topn, можно объединить с помощью функций topn_union и topn_union_agg. Последняя может использоваться, чтобы объединить данные за весь первый месяц и составить список из пяти продуктов, получивших наибольшее количество отзывов за этот период.
SELECT (topn(topn_union_agg(agg_data), 5)).* FROM reviews_by_day WHERE review_date >= '2000-01-01' AND review_date < '2000-02-01' ORDER BY 2 DESC;
┌────────────┬───────────┐ │ item │ frequency │ ├────────────┼───────────┤ │ 0375404368 │ 217 │ │ 0345417623 │ 217 │ │ 0375404376 │ 217 │ │ 0375408738 │ 217 │ │ 043936213X │ 204 │ └────────────┴───────────┘
За подробным описанием и примерами обратитесь к файлу readme topn.
J.5.8.4.4.1.3. Процентильные вычисления #
Поиск точного процентиля по большому количеству строк может быть чрезвычайно затратным, поскольку все строки необходимо передать узлу-координатору для окончательной сортировки и обработки. Поиск приближённых значений, напротив, можно выполнять параллельно на рабочих узлах, используя так называемый потоковый алгоритм приближения. Затем узел-координатор объединяет сжатые сводки в окончательный результат, а не читает полные строки.
Популярный алгоритм для процентилей использует сжатую структуру данных под названием t-digest, и доступен для Postgres Pro в расширении tdigest. В citus есть встроенная поддержка этого расширения.
Пример использования tdigest в citus:
Загрузите и установите расширение tdigest на всех узлах Postgres Pro (узле-координаторе и всех рабочих узлах). Инструкции по установке можно найти в репозитории расширения tdigest на сайте GitHub.
Создайте расширение tdigest в базе данных и выполните следующую команду на узле-координаторе:
CREATE EXTENSION tdigest;
Узел-координатор передаст команду рабочим узлам.
Когда в запросах используются определённые в расширении агрегатные функции, в citus переписываются запросы, чтобы по возможности передавать частичные вычисления tdigest рабочим узлам.
Точностью tdigest можно управлять с помощью аргумента compression, передаваемого в агрегатных функциях. Здесь нужно будет искать компромисс между точностью и объёмом данных, которыми обмениваются рабочие узлы и координатор. Подробное описание использования агрегатных функций в tdigest можно найти в документации расширения.
J.5.8.4.4.2. Ограниченный вынос наружу #
В citus также есть вынос наружу ограничительных предложений на сегменты рабочих узлов, где это возможно, чтобы минимизировать объём данных, передаваемых по сети.
Однако в некоторых случаях запросы SELECT с предложениями LIMIT могут потребовать выборки всех строк из каждого сегмента для получения точных результатов. Например, если запрос требует упорядочивания по агрегированному столбцу, для определения окончательного агрегированного значения потребуются результаты этого столбца из всех сегментов. При этом снижается производительность предложения LIMIT из-за передачи большого объёма сетевых данных. В случаях, когда приближение может дать осмысленные результаты, в citus можно использовать не нагружающие сеть предложения LIMIT.
Приближения LIMIT по умолчанию отключены, но их можно включить в параметре конфигурации citus.limit_clause_row_fetch_count. На основании этого значения в citus будет ограничиваться количество строк, возвращаемых каждой задачей для агрегирования на узле-координаторе. Из-за этого ограничения окончательные результаты могут быть приблизительными. Увеличение этого предела повысит точность окончательных результатов, сохраняя при этом верхнюю границу количества строк, полученных от рабочих узлов.
SET citus.limit_clause_row_fetch_count TO 10000;
J.5.8.4.4.3. Представления по распределённым таблицам #
В citus поддерживаются все представления по распределённым таблицам. За дополнительной информацией о синтаксисе и функциональности представлений обратитесь к описанию команды CREATE VIEW.
Обратите внимание, что некоторые представления приводят к менее эффективным планам запросов. За дополнительной информацией об обнаружении и улучшении плохой производительности представлений обратитесь к разделу Сетевая нагрузка подзапросов/общих табличных выражений. (Представления внутри citus обрабатываются как подзапросы.)
В citus также поддерживаются материализованные представления. Они сохраняются как локальные таблицы на узле-координаторе.
J.5.8.4.4.4. Соединения #
В citus поддерживаются эквивалентные соединения между любым количеством таблиц, независимо от их размера и метода распределения. Планировщик запросов выбирает оптимальный метод и порядок соединения в зависимости от того, как распределены таблицы. Он оценивает несколько возможных порядков соединения и создаёт план соединения, который требует передачи минимального объёма данных по сети.
J.5.8.4.4.4.1. Совмещённые соединения #
Когда две таблицы совмещены, их можно объединить в общих столбцах распределения. Совмещённое соединение — наиболее эффективный способ соединения двух больших распределённых таблиц.
Внутри citus узел-координатор узнаёт, какие фрагменты совмещённых таблиц могут совпадать с фрагментами другой таблицы, просматривая метаданные столбца распределения. Это позволяет citus отсекать пары сегментов, которые не могут создавать совпадающие ключи соединения. Соединения между оставшимися парами сегментов выполняются параллельно на рабочих узлах, а затем результаты возвращаются координатору.
Примечание
Убедитесь, что таблицы распределены на одинаковое количество сегментов и что столбцы распределения каждой таблицы имеют точно совпадающие типы. Попытка объединить столбцы немного разных типов, например int и bigint, может вызвать ошибку.
J.5.8.4.4.4.2. Соединения таблиц-справочников #
Таблицы-справочники можно использовать как таблицы «размерностей» для эффективного объединения с большими таблицами «фактов». Поскольку таблицы-справочники полностью реплицируются на все рабочие узлы, справочное соединение можно разложить на локальные соединения на каждом рабочем узле и выполнять параллельно. Справочное соединение похоже на более гибкую версию совмещённого соединения, поскольку таблицы-справочники не распределяются по какому-либо конкретному столбцу и могут свободно соединяться по любому из своих столбцов.
Таблицы-справочники также можно соединять с локальными таблицами на узле-координаторе.
J.5.8.4.4.4.3. Соединения с пересекционированием #
В некоторых случаях может потребоваться соединить две таблицы по столбцам, не являющимся столбцами распределения. В таких случаях citus также позволяет объединять столбцы ключей, не предназначенных для распределения, путём динамического пересекционирования таблиц для запроса.
В таких случаях таблицы, подлежащие секционированию, определяются оптимизатором запросов на основе столбцов распределения, ключей соединения и размеров таблиц. Пересекционирование таблиц гарантирует, что соединяются только соответствующие пары сегментов, что значительно сокращает объём передаваемых по сети данных.
В целом, совмещённые соединения более эффективны, чем соединения с пересекционированием, поскольку последние требуют перестановки данных. Поэтому по возможности следует распределять таблицы по общим ключам соединения.
J.5.8.4.5. Обработка запросов #
Кластер citus состоит из экземпляра узла-координатора и нескольких экземпляров рабочих узлов. Данные сегментируются по рабочим узлам, а на узле-координаторе хранятся метаданные об этих сегментах. Все запросы к кластеру выполняются через узел-координатор. Он разбивает запрос на более мелкие фрагменты, где каждый фрагмент может выполняться независимо в отдельном сегменте. Затем узел-координатор назначает фрагменты рабочим узлам, контролирует их выполнение, объединяет их результаты и возвращает конечный результат пользователю. Краткое описание архитектуры обработки запросов представлено на диаграмме ниже.
Рисунок J.16. Архитектура обработки запросов
Конвейер обработки запросов citus содержит два компонента:
Планировщик и исполнитель распределённых запросов
Планировщик и исполнитель Postgres Pro
Они описаны более подробно в следующих разделах.
J.5.8.4.5.1. Планировщик распределённых запросов #
Планировщик распределённых запросов citus принимает SQL-запрос и планирует его для распределённого выполнения.
Для запросов SELECT планировщик сначала создаёт дерево плана входящего запроса и преобразует его в коммутативную и ассоциативную форму, чтобы его можно было распараллелить. Он также применяет несколько оптимизаций, чтобы обеспечить масштабируемость выполнения запросов и минимизацию сетевых операций ввода-вывода.
Далее планировщик разбивает запрос на две части: запрос, выполняемый на координаторе, и фрагменты запроса, которые выполняются на отдельных сегментах рабочих узлов. Затем планировщик назначает эти фрагменты запроса рабочим узлам для эффективного использования ресурсов. После этого шага план распределённого запроса передаётся исполнителю распределённых запросов для выполнения.
Процесс планирования поиска значений ключа в столбце распределения или запросов на изменение немного отличается, поскольку такие операции затрагивают ровно один сегмент. Как только планировщик получает входящий запрос, ему необходимо определить правильный сегмент, в который следует направить запрос. Для этого он извлекает столбец распределения во входящей строке и просматривает метаданные. Затем планировщик перезаписывает SQL этой команды, чтобы ссылаться на таблицу сегментов вместо исходной таблицы. Этот переписанный план затем передаётся исполнителю распределённых запросов.
J.5.8.4.5.2. Исполнитель распределённых запросов #
Исполнитель распределённых запросов в citus выполняет планы распределённых запросов и обрабатывает ошибки. Исполнитель хорошо подходит для получения быстрых ответов на запросы, включающие фильтры, агрегирования и совмещённые соединения, а также для выполнения одноарендных запросов с полной поддержкой SQL. Он открывает одно подключение на каждый сегмент для рабочих узлов по мере необходимости и отправляет им все запросы-фрагменты. Затем он извлекает результаты каждого фрагмента, объединяет их и возвращает конечные результаты пользователю.
J.5.8.4.5.2.1. Двухэтапное выполнение подзапросов/CTE #
При необходимости citus может собирать результаты подзапросов и CTE на узле-координаторе, а затем передавать их обратно через рабочие узлы для использования во внешнем запросе. Это позволяет citus поддерживать большее разнообразие SQL-конструкций.
Например, подзапросы в предложении WHERE не всегда могут выполняться одновременно с основным запросом и должны обрабатываться отдельно. Допустим, в приложении для веб-аналитики поддерживается таблица page_views, секционированная по page_id. Чтобы запросить количество посетителей на двадцати самых посещаемых страницах, можно использовать подзапрос для формирования списка страниц, а затем внешний запрос для подсчёта посетителей.
SELECT page_id, count(distinct host_ip) FROM page_views WHERE page_id IN ( SELECT page_id FROM page_views GROUP BY page_id ORDER BY count(*) DESC LIMIT 20 ) GROUP BY page_id;
Исполнитель выполнил бы фрагмент этого запроса для каждого сегмента по page_id, подсчитав отдельные host_ips и объединив результаты на узле-координаторе. Однако LIMIT в подзапросе означает, что его нельзя выполнить как часть фрагмента. При рекурсивном планировании запроса citus может запускать подзапрос отдельно, передавать результаты всем рабочим узлам, выполнять основной запрос-фрагмент и возвращать результаты узлу-координатору. Механизм «push-pull» поддерживает подзапросы, подобные описанному выше.
Рассмотрим на рабочем примере выходные данные EXPLAIN для этого запроса:
GroupAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.page_id
-> Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: remote_scan.page_id
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0)
-> Distributed Subplan 6_1
-> Limit (cost=0.00..0.00 rows=0 width=0)
-> Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: COALESCE((pg_catalog.sum((COALESCE((pg_catalog.sum(remote_scan.worker_column_2))::bigint, '0'::bigint))))::bigint, '0'::bigint) DESC
-> HashAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.page_id
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0)
Task Count: 32
Tasks Shown: One of 32
-> Task
Node: host=localhost port=9701 dbname=postgres
-> HashAggregate (cost=54.70..56.70 rows=200 width=12)
Group Key: page_id
-> Seq Scan on page_views_102008 page_views (cost=0.00..43.47 rows=2247 width=4)
Task Count: 32
Tasks Shown: One of 32
-> Task
Node: host=localhost port=9701 dbname=postgres
-> HashAggregate (cost=84.50..86.75 rows=225 width=36)
Group Key: page_views.page_id, page_views.host_ip
-> Hash Join (cost=17.00..78.88 rows=1124 width=36)
Hash Cond: (page_views.page_id = intermediate_result.page_id)
-> Seq Scan on page_views_102008 page_views (cost=0.00..43.47 rows=2247 width=36)
-> Hash (cost=14.50..14.50 rows=200 width=4)
-> HashAggregate (cost=12.50..14.50 rows=200 width=4)
Group Key: intermediate_result.page_id
-> Function Scan on read_intermediate_result intermediate_result (cost=0.00..10.00 rows=1000 width=4)Разобьём план на части и рассмотрим каждую отдельно.
GroupAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.page_id
-> Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: remote_scan.page_idКорень дерева — это операции узла-координатора с результатами, полученными от рабочих узлов. В данном случае они группируются, и GroupAggregate указывает, что сначала они должны быть отсортированы.
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0) -> Distributed Subplan 6_1 .
У выборочного сканирования есть два больших поддерева, начинающихся с «распределённого подплана».
-> Limit (cost=0.00..0.00 rows=0 width=0)
-> Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: COALESCE((pg_catalog.sum((COALESCE((pg_catalog.sum(remote_scan.worker_column_2))::bigint, '0'::bigint))))::bigint, '0'::bigint) DESC
-> HashAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.page_id
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0)
Task Count: 32
Tasks Shown: One of 32
-> Task
Node: host=localhost port=9701 dbname=postgres
-> HashAggregate (cost=54.70..56.70 rows=200 width=12)
Group Key: page_id
-> Seq Scan on page_views_102008 page_views (cost=0.00..43.47 rows=2247 width=4)
.Рабочие узлы выполняют вышеуказанные операции для каждого из тридцати двух сегментов (citus выбирает для отображения один из них). Все части подзапроса IN (…) легко узнаваемы: сортировка, группирование и ограничение. Когда этот запрос завершается на всех рабочих узлах, они отправляют результаты обратно узлу-координатору, который объединяет их как «промежуточные результаты».
Task Count: 32
Tasks Shown: One of 32
-> Task
Node: host=localhost port=9701 dbname=postgres
-> HashAggregate (cost=84.50..86.75 rows=225 width=36)
Group Key: page_views.page_id, page_views.host_ip
-> Hash Join (cost=17.00..78.88 rows=1124 width=36)
Hash Cond: (page_views.page_id = intermediate_result.page_id)
.Расширение citus запускает другое задание для исполнителя во втором поддереве. Он считает отдельных посетителей в page_views и использует JOIN для соединения с промежуточными результатами. Промежуточные результаты помогут ограничиться двадцатью страницами с наибольшими показателями.
-> Seq Scan on page_views_102008 page_views (cost=0.00..43.47 rows=2247 width=36)
-> Hash (cost=14.50..14.50 rows=200 width=4)
-> HashAggregate (cost=12.50..14.50 rows=200 width=4)
Group Key: intermediate_result.page_id
-> Function Scan on read_intermediate_result intermediate_result (cost=0.00..10.00 rows=1000 width=4)
.Рабочий узел внутри расширения получает промежуточные результаты с помощью функции read_intermediate_result, которая загружает данные из файла, скопированного с узла-координатора.
В примере выше показано, как в citus запрос выполняется в несколько этапов с распределённым подпланом и как можно использовать EXPLAIN, чтобы изучить выполнение распределённых запросов.
J.5.8.4.5.3. Планировщик и исполнитель Postgres Pro #
Как только исполнитель распределённых запросов отправляет фрагменты запроса рабочим узлам, они обрабатываются как обычные запросы Postgres Pro. Планировщик Postgres Pro этого рабочего узла выбирает наиболее оптимальный план для локального выполнения этого запроса в соответствующей таблице сегментов. Затем исполнитель Postgres Pro запускает этот запрос и возвращает результаты запроса исполнителю распределённых запросов. За подробностями о планировщике и исполнителе Postgres Pro обратитесь к соответствующим разделам документации. Наконец, исполнитель распределённых запросов передаёт результаты узлу-координатору для окончательного агрегирования.
J.5.8.4.6. Ручная трансляция запросов #
Когда пользователь отправляет запрос, узел-координатор citus разделяет его на более мелкие фрагменты, где каждый фрагмент может выполняться независимо на рабочем узле. Это позволяет citus распределять каждый запрос по кластеру.
Однако способ разделения запросов на фрагменты (и какие запросы транслируются) зависит от типа запроса. В некоторых сложных ситуациях полезно управлять этим поведением вручную. В citus есть служебные функции для трансляции SQL на рабочие узлы, сегменты или места совмещения.
Ручная трансляция запросов обходит логику координатора, блокировки и любые другие проверки согласованности. Эти функции можно применять в крайних случаях для выполнения операторов, которые иначе недоступны в citus. Такие функции следует использовать с осторожностью, чтобы избежать несогласованности данных и взаимоблокировок.
J.5.8.4.6.1. Выполнение на всех рабочих узлах #
Наименее детализированный уровень исполнения — трансляция операторов для выполнения на всех рабочих узлах. Его удобно использовать для просмотра свойств всех баз данных на рабочих узлах.
-- Вывод параметра work_mem каждой БД на рабочих узлах SELECT run_command_on_workers($cmd$ SHOW work_mem; $cmd$);
Для выполнения на всех узлах, как на рабочих, так и на координаторе, используйте функцию run_command_on_all_nodes.
Примечание
Эту команду не следует использовать для создания объектов БД на рабочих узлах, так как это затруднит автоматическое добавление рабочих узлов.
Примечание
Функция run_command_on_workers и другие команды ручной трансляции в этом разделе могут выполнять только те запросы, которые возвращают один столбец и одну строку.
J.5.8.4.6.2. Выполнение во всех сегментах #
Следующий уровень детализации — выполнение команды во всех сегментах конкретной распределённой таблицы. Это может быть полезно для чтения свойств таблицы непосредственно на рабочих узлах. Запросы, выполняемые локально на рабочем узле, имеют полный доступ к метаданным, таким как статистика таблиц.
Функция run_command_on_shards применяет SQL-команду к каждому сегменту, где имя сегмента предоставляется для подстановки в команде. Ниже приведён пример оценки количества строк для распределённой таблицы с использованием таблицы pg_class на каждом рабочем узле, чтобы оценить количество строк для каждого сегмента. Обратите внимание на параметр %s, который будет заменён на имя каждого сегмента.
-- Получить расчётное количество строк для распределённой таблицы, суммируя
-- расчётное количество строк для каждого сегмента.
SELECT sum(result::bigint) AS estimated_count
FROM run_command_on_shards(
'my_distributed_table',
$cmd$
SELECT reltuples
FROM pg_class c
JOIN pg_catalog.pg_namespace n on n.oid=c.relnamespace
WHERE (n.nspname || '.' || relname)::regclass = '%s'::regclass
AND n.nspname NOT IN ('citus', 'pg_toast', 'pg_catalog')
$cmd$
);Полезным дополнением к run_command_on_shards является функция run_command_on_colocated_placements. Она подставляет в запрос имена двух мест размещения совмещённых таблиц. Пары размещения всегда выбираются на одном и том же рабочем узле с полной поддержкой SQL. Таким образом, можно использовать расширенную функциональность SQL, такую как триггеры, для связи таблиц:
-- Предположим, есть две распределённые таблицы
CREATE TABLE little_vals (key int, val int);
CREATE TABLE big_vals (key int, val int);
SELECT create_distributed_table('little_vals', 'key');
SELECT create_distributed_table('big_vals', 'key');
-- Необходимо синхронизировать их, чтобы каждый раз при создании
-- little_vals, также создавалось значение big_vals, равное удвоенному значению little_vals
--
-- Сначала создайте триггерную функцию, которая будет
-- принимать в качестве аргумента размещение целевой таблицы
CREATE OR REPLACE FUNCTION embiggen() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'INSERT') THEN
EXECUTE format(
'INSERT INTO %s (key, val) SELECT ($1).key, ($1).val*2;',
TG_ARGV[0]
) USING NEW;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Затем свяжите совмещённые таблицы с помощью триггерной функции
-- на каждом размещении
SELECT run_command_on_colocated_placements(
'little_vals',
'big_vals',
$cmd$
CREATE TRIGGER after_insert AFTER INSERT ON %s
FOR EACH ROW EXECUTE PROCEDURE embiggen(%L)
$cmd$
);J.5.8.4.6.3. Ограничения #
Нет защиты от взаимоблокировок для транзакций с несколькими операторами.
Нет защиты от сбоев во время выполнения запроса и возникающей в результате несогласованности.
Результаты запроса кешируются в памяти; эти функции не могут работать с очень большими наборами результатов.
Функции завершаются преждевременно (с ошибкой), если не получается подключиться к узлу.
J.5.8.4.7. Поддержка SQL и обходные решения #
Поскольку citus является расширением Postgres Pro, он совместим с её конструкциями. Пользователи могут применять инструменты и функциональность всей экосистемы Postgres Pro для распределённых таблиц, созданных с помощью citus.
В citus реализована 100% поддержка SQL для любых запросов, которые можно выполнить на одном рабочем узле. Запросы такого типа часто встречаются в многоарендных приложениях при доступе к информации об одном арендаторе.
Даже межузловые запросы (используемые для параллельных вычислений) поддерживают большую часть функциональности SQL. Однако некоторая часть функциональности SQL не поддерживается для запросов, объединяющих информацию из нескольких узлов.
J.5.8.4.7.1. Ограничения #
J.5.8.4.7.1.1. Общие #
Указанные ограничения применяются ко всем моделям операций:
Нет поддержки системы правил.
Не поддерживаются подзапросы в запросах
INSERT.Нет поддержки распределения многоуровневых секционированных таблиц.
Функции, используемые в запросах
UPDATEк распределённым таблицам, не должны бытьVOLATILE.Функции
STABLE, используемые в запросахUPDATE, не могут ссылаться на столбцы.Нет поддержки изменения представлений для запросов, содержащих таблицы citus.
В citus идентификатор узла кодируется в последовательности, сгенерированной на каждом узле. Это позволяет каждому отдельному узлу напрямую выполнять вставки без пересечения последовательностей. Однако этот метод не работает для последовательностей, меньших, чем bigint, что может привести к сбою вставки на рабочие узлы. В этом случае необходимо удалить столбец и добавить другой, типа bigint, или направлять вставки через координатор.
J.5.8.4.7.1.2. Межузловые SQL-запросы #
SELECT… FOR UPDATEработает только с запросами к одному сегменту.TABLESAMPLE работает только с запросами к одному сегменту.
Связанные подзапросы поддерживаются только в том случае, если корреляция находится в столбце распределения.
Внешние соединения между распределёнными таблицами поддерживаются только по столбцу распределения.
Рекурсивные CTE работают только в запросах к одному сегменту.
Наборы группирования работают только в запросах к одному сегменту.
Распределять можно только обычные, внешние или секционированные таблицы.
SQL-команда
MERGEподдерживается для следующих комбинаций типов таблиц:Целевая Исходная Поддерживается Комментарии Локальная
Локальная
Да
Локальная
Справка
Да
Локальная
Распределённая
Нет
В разработке
Распределённая
Локальная
Да
Распределённая
Распределённая
Да
Включая несовмещённые таблицы
Распределённая
Справка
Да
Справка
Любая
Нет
Целевая таблица не может быть справочником
Подробную информацию о диалекте SQL-команд Postgres Pro (который может применяться пользователями citus в исходном виде) можно найти в разделе Команды SQL.
J.5.8.4.7.1.3. SQL-совместимость сегментирования на основе схем #
При использовании сегментирования на основе схем недоступна следующая функциональность:
Не поддерживаются внешние ключи в распределённых схемах.
На соединения между распределёнными схемами распространяются ограничения межузловых SQL-запросов.
Не поддерживается создание распределённых схем и таблиц в одном SQL-операторе.
J.5.8.4.7.2. Обходные решения #
Прежде чем применять обходные решения, подумайте, подходит ли citus для данной ситуации. Расширение citus хорошо работает для сценариев использования с аналитикой в реальном времени и множеством арендаторов.
В citus поддерживаются все SQL-операторы в сценариях использования со множеством арендаторов. Даже в случаях использования аналитики в реальном времени, когда запросы охватывают несколько узлов, в citus поддерживается большинство операторов. Несколько типов неподдерживаемых запросов перечислены в разделе Какие функции Postgres Pro не поддерживаются в citus?. Для многих таких ограничений существуют обходные решения, ниже приведены некоторые из наиболее полезных.
J.5.8.4.7.2.1. Обход ограничений с помощью CTE #
Если SQL-запрос не поддерживается, один из способов обойти его — использовать CTE вместе с так называемым двухэтапным выполнением.
SELECT * FROM dist WHERE EXISTS (SELECT 1 FROM local WHERE local.a = dist.a); /* ERROR: direct joins between distributed and local tables are not supported HINT: Use CTEs or subqueries to select from local tables and use them in joins */
Чтобы обойти это ограничение, можно превратить запрос в запрос маршрутизатора, обернув распределённую часть в CTE.
WITH cte AS (SELECT * FROM dist) SELECT * FROM cte WHERE EXISTS (SELECT 1 FROM local WHERE local.a = cte.a);
Помните, что координатор отправит результаты CTE всем рабочим узлам, которым они необходимы для обработки. Таким образом, лучше всего либо добавить во внутренний запрос максимально конкретные фильтры и ограничения, либо агрегировать таблицу, чтобы снизить возможную нагрузку на сеть, вызываемую таким запросом. За подробностями обратитесь к разделу Сетевая нагрузка подзапросов/общих табличных выражений.
J.5.8.4.7.2.2. Временные таблицы: крайние меры #
Существует ещё несколько запросов, которые не поддерживаются даже при использовании двухэтапного выполнения через подзапросы. Один из них — использование наборов группирования в распределённой таблице.
В нашем руководстве по анализу данных в реальном времени создавалась таблица под названием github_events, распределённая по столбцу user_id. Обратимся к ней и найдём самые ранние события для заранее выбранного набора репозиториев, сгруппированные по комбинациям типа и публичности события. Для таких задач удобно использовать наборы группирования. Однако эта функциональность пока не поддерживается в распределённых запросах:
-- Такой запрос не работает
SELECT repo_id, event_type, event_public,
grouping(event_type, event_public),
min(created_at)
FROM github_events
WHERE repo_id IN (8514, 15435, 19438, 21692)
GROUP BY repo_id, ROLLUP(event_type, event_public);ERROR: could not run distributed query with GROUPING HINT: Consider using an equality filter on the distributed table's partition column.
Но есть одна хитрость. Нужную информацию можно передать координатору в виде временной таблицы:
-- Размещение данных без агрегирования в локальную таблицу
CREATE TEMP TABLE results AS (
SELECT repo_id, event_type, event_public, created_at
FROM github_events
WHERE repo_id IN (8514, 15435, 19438, 21692)
);
-- Запуск агрегирования локально
SELECT repo_id, event_type, event_public,
grouping(event_type, event_public),
min(created_at)
FROM results
GROUP BY repo_id, ROLLUP(event_type, event_public); repo_id | event_type | event_public | grouping | min
---------+-------------------+--------------+----------+---------------------
8514 | PullRequestEvent | t | 0 | 2016-12-01 05:32:54
8514 | IssueCommentEvent | t | 0 | 2016-12-01 05:32:57
19438 | IssueCommentEvent | t | 0 | 2016-12-01 05:48:56
21692 | WatchEvent | t | 0 | 2016-12-01 06:01:23
15435 | WatchEvent | t | 0 | 2016-12-01 05:40:24
21692 | WatchEvent | | 1 | 2016-12-01 06:01:23
15435 | WatchEvent | | 1 | 2016-12-01 05:40:24
8514 | PullRequestEvent | | 1 | 2016-12-01 05:32:54
8514 | IssueCommentEvent | | 1 | 2016-12-01 05:32:57
19438 | IssueCommentEvent | | 1 | 2016-12-01 05:48:56
15435 | | | 3 | 2016-12-01 05:40:24
21692 | | | 3 | 2016-12-01 06:01:23
19438 | | | 3 | 2016-12-01 05:48:56
8514 | | | 3 | 2016-12-01 05:32:54Создание временной таблицы на координаторе — это крайняя мера. Она ограничена размером диска и мощностью ЦП узла.
J.5.8.4.7.2.3. Подзапросы в запросах INSERT #
Попробуйте переписать свои запросы с использованием синтаксиса INSERT INTO ... SELECT.
Следующий SQL-код:
INSERT INTO a.widgets (map_id, widget_name)
VALUES (
(SELECT mt.map_id FROM a.map_tags mt WHERE mt.map_license = '12345'),
'Test'
);Станет таким:
INSERT INTO a.widgets (map_id, widget_name) SELECT mt.map_id, 'Test' FROM a.map_tags mt WHERE mt.map_license = '12345';
J.5.8.5. API в citus #
J.5.8.5.1. Вспомогательные функции citus #
Этот раздел содержит справочную информацию о поддерживаемых в citus пользовательских функциях. Эти функции позволяют использовать в citus расширенные возможности распределения, а не только стандартные SQL-команды.
J.5.8.5.1.1. DDL таблиц и сегментов #
citus_schema_distribute (schemaname regnamespace) returns void#Преобразует имеющиеся обычные схемы в распределённые схемы, которые автоматически связываются с отдельными группами совмещения, так что таблицы, созданные в этих схемах, будут автоматически преобразованы в совмещённые распределённые таблицы без ключа сегментирования. В процессе распределения схема будет автоматически назначена и перемещена на существующий узел кластера.
Аргументы:
schemaname— имя схемы, которая будет распределена.
В примере ниже показано, как распределить три схемы с именами
tenant_a,tenant_bиtenant_c. За дополнительными примерами обратитесь к разделу Микросервисы:SELECT citus_schema_distribute('tenant_a'); SELECT citus_schema_distribute('tenant_b'); SELECT citus_schema_distribute('tenant_c');citus_schema_undistribute (schemaname regnamespace) returns void#Преобразует существующую распределённую схему обратно в обычную схему. В результате этого процесса таблицы и данные перемещаются с текущего узла обратно на узел-координатор в кластере.
Аргументы:
schemaname— имя схемы, которая будет распределена.
В приведённом ниже примере показано, как преобразовать три разные распределённые схемы обратно в обычные. За дополнительными примерами обратитесь к разделу Микросервисы:
SELECT citus_schema_undistribute('tenant_a'); SELECT citus_schema_undistribute('tenant_b'); SELECT citus_schema_undistribute('tenant_c');citus_schema_move (schema_id regnamespace, target_node_name text, target_node_port integer, shard_transfer_mode citus.shard_transfer_mode) returns void#Перемещает распределённую схему с одного узла на другой.
Существует два способа перемещения распределённой схемы: блокирующий и неблокирующий. При блокирующем способе все изменения в таблицах схемы приостанавливаются на время перемещения. Второй способ, позволяющий избежать блокировки записи, основан на логической репликации Postgres Pro 10.
Аргументы:
schema_id— идентификатор (OID) распределённой схемы, которая будет перемещена. Если передать имя схемы в виде строкового литерала, он будет автоматически приведён к типу идентификатора объекта.target_node_name— DNS-имя узла, на который будет перемещена распределённая схема («целевой» узел).target_node_port— порт на целевом рабочем узле, через который сервер БД принимает подключения.shard_transfer_mode— указать метод репликации: логическая репликация Postgres Pro или командаCOPYмежду рабочими узлами. Этот необязательный аргумент может принимать следующие значения:auto— требовать идентификатор реплики, если возможна логическая репликация, в противном случае использовать ранее принятое поведение. Это значение по умолчанию.force_logical— использовать логическую репликацию, даже если таблица не имеет идентификатора реплики. Любые одновременные операторы изменения/удаления таблицы во время репликации завершатся ошибкой.block_writes— использовать командуCOPY(блокирующую запись) для таблиц, у которых нет первичного ключа или идентификатора реплики.
Пример использования этой функции:
SELECT citus_schema_move('schema-name', 'to_host', 5432);create_distributed_table (table_name regclass, distribution_column text, distribution_type citus.distribution_type, colocate_with text, shard_count int) returns void#Определяет распределённую таблицу и создаёт её сегменты, если это таблица, распределённая по хешу. Эта функция принимает имя таблицы, столбец распределения и дополнительный метод распределения и вставляет соответствующие метаданные, чтобы пометить таблицу как распределённую. Если метод распределения не указан, по умолчанию функция распределяет по хешу. Если таблица распределена по хешу, функция также создаёт сегменты рабочих узлов на основе значения конфигурации количества сегментов. Если в таблице есть строки, они автоматически распределяются по рабочим узлам.
Аргументы:
table_name— имя таблицы, которая будет распределена.distribution_column— столбец, по которому будет распределяться таблица.distribution_type— метод распределения (необязательный аргумент). Значение по умолчанию —hash.colocate_with— включить текущую таблицу в группу совмещения другой таблицы. Это необязательный аргумент. По умолчанию таблицы совмещаются, если они распределены по столбцам одного типа с одинаковым количеством сегментов. Если позднее понадобится избавиться от этого совмещения, можно использовать функцию update_distributed_table_colocation. Возможные значения этого аргумента:default— значение по умолчанию;none— для создания новой группы совмещения; имя другой таблицы — для совмещения с ней. За подробностями обратитесь к разделу Совмещение таблиц.Имейте в виду, что значение по умолчанию аргумента
colocate_withподразумевает неявное совместное размещение. Как поясняется в разделе Совмещение таблиц, оно может оказаться полезным, если таблицы связаны или будут объединены. Однако если две таблицы не связаны, но используют один и тот же тип данных для своих столбцов распределения, их случайное совмещение может снизить производительность во время перебалансировки сегментов. Сегменты таблицы могут быть совмещены в «CASCADE». Чтобы разорвать это неявное совмещение, можно использовать функцию update_distributed_table_colocation.Если новая распределённая таблица не связана с другими таблицами, лучше всего указать
colocate_with => 'none'.shard_count— количество сегментов, которое необходимо создать для новой распределённой таблицы. Это необязательный аргумент. При указанииshard_countзначениеcolocate_withможет быть толькоnone. Чтобы изменить количество сегментов существующей таблицы или группы совмещения, используйте функцию alter_distributed_table.Допустимые значения для аргумента
shard_count: от1до64000. За рекомендациями по выбору оптимального значения обратитесь к разделу Количество сегментов.
В данном примере база данных получает информацию, что таблица
github_eventsдолжна распределяться по хешу по столбцуrepo_id. За дополнительными примерами обратитесь к разделу Создание и изменение распределённых объектов (DDL):SELECT create_distributed_table('github_events', 'repo_id'); -- Также можно указать совмещение явно: SELECT create_distributed_table('github_events', 'repo_id', colocate_with => 'github_repo');truncate_local_data_after_distributing_table (function_name regclass) returns void#Отбрасывает все локальные строки после распределения таблицы и предотвращает сбой ограничений из-за устаревших локальных записей. Усечение применяется каскадно к таблицам, имеющим внешний ключ к указанной таблице. Если исходные таблицы не являются распределёнными, то усечение запрещено до подтверждения ссылочной целостности:
ERROR: cannot truncate a table referenced in a foreign key constraint by a local table
Усечение данных локальной таблицы узла-координатора безопасно для распределённых таблиц, поскольку имеющиеся в них строки копируются на рабочие узлы во время распределения.
Аргументы:
table_name— имя распределённой таблицы, локальный двойник которой на узле-координаторе должен быть усечён.
Пример использования этой функции:
-- Аргумент должен быть распределённой таблицей SELECT truncate_local_data_after_distributing_table('public.github_events');undistribute_table (table_name regclass, cascade_via_foreign_keys boolean) returns void#Отменяет действие функции create_distributed_table или create_reference_table. При отмене распределения все данные из сегментов перемещаются обратно в локальную таблицу на узле-координаторе (при условии, что данным хватает места), а затем сегменты удаляются.
В citus не отменяется распределение таблиц, которые имеют внешние ключи или на которые ссылаются внешние ключи, кроме случая, когда аргумент
cascade_via_foreign_keysимеет значениеtrue. Если этот аргумент имеет значениеfalse(или опущен), необходимо вручную удалить нарушающие ограничения внешнего ключа перед отменой распределения.Аргументы:
table_name— имя распределённой таблицы или таблицы-справочника, для которой будет отменено распределение.cascade_via_foreign_keys— если для этого необязательного аргумента установлено значениеtrue, функция также отменяет распределение всех таблиц, связанных сtable_nameчерез внешние ключи. Этот аргумент следует использовать с осторожностью, поскольку потенциально он может затронуть множество таблиц. Значение по умолчанию —false.
Пример, как распределить таблицу
github_events, а затем отменить распределение:-- Распределение таблицы SELECT create_distributed_table('github_events', 'repo_id'); -- Отмена распределение и превращение таблицы обратно в локальную SELECT undistribute_table('github_events');alter_distributed_table (table_name regclass, distribution_column text, shard_count int, colocate_with text, cascade_to_colocated boolean) returns void#Изменяет столбец распределения, количество сегментов или параметры совмещения распределённой таблицы.
Аргументы:
table_name— имя изменяемой распределённой таблицы.distribution_column— имя нового столбца распределения. Этот необязательный аргумент по умолчанию имеет значениеNULL.shard_count— новое количество сегментов. Этот необязательный аргумент по умолчанию имеет значениеNULL.colocate_with— таблица, с которой будет совмещена текущая распределённая таблица. Возможные значения:default,none, чтобы создать новую группу совмещения, или имя другой таблицы, с которой будет выполняться совмещение. Этот необязательный аргумент по умолчанию имеет значениеdefault.cascade_to_colocated. Если для этого аргумента установлено значениеtrue, измененияshard_countиcolocate_withтакже будут применены ко всем таблицам, которые ранее были совмещены с указанной, и совмещение будет сохранено. Если задано значениеfalse, текущее совмещение этой таблицы будет нарушено. Этот необязательный аргумент по умолчанию имеет значениеfalse.
Пример использования этой функции:
-- Изменение столбца распределения SELECT alter_distributed_table('github_events', distribution_column:='event_id'); -- Изменение количества сегментов всех таблиц в группах совмещения SELECT alter_distributed_table('github_events', shard_count:=6, cascade_to_colocated:=true); -- Изменение совмещения SELECT alter_distributed_table('github_events', colocate_with:='another_table');alter_table_set_access_method (table_name regclass, access_method text) returns void#Изменяет метод доступа к таблице (например,
heapили columnar).Аргументы:
table_name— имя таблицы, для которой будет изменён метод доступа.access_method— имя нового метода доступа.
Пример использования этой функции:
SELECT alter_table_set_access_method('github_events', 'columnar');remove_local_tables_from_metadata () returns void#Удаляет ненужные локальные таблицы из метаданных расширения citus. (См. параметр конфигурации citus.enable_local_reference_table_foreign_keys.)
Обычно локальная таблица находится в метаданных citus по какой-то причине, например из-за наличия внешних ключей между таблицей и таблицей-справочником. Однако если параметр
citus.enable_local_reference_table_foreign_keysотключён, citus не будет управлять метаданными в такой ситуации, и ненужные метаданные могут сохраняться до тех пор, пока не будут удалены вручную.create_reference_table (table_name regclass) returns void#Определяет небольшую таблицу-справочник или таблицу размеров. Эта функция принимает имя таблицы и создаёт распределённую таблицу с одним сегментом, реплицируемую на каждый рабочий узел.
Аргументы:
table_name— имя небольшой таблицы или таблицы-справочника, которая будет распределяться.
В указанном примере таблица
nationв БД определяется как таблица-справочник:SELECT create_reference_table('nation');citus_add_local_table_to_metadata (table_name regclass, cascade_via_foreign_keys boolean) returns void#Добавляет локальную таблицу Postgres Pro в метаданные citus. Основной вариант использования этой функции — сделать локальные таблицы координатора доступными с любого узла кластера, что особенно полезно для выполнения запросов от других узлов. Данные, связанные с локальной таблицей, остаются на узле-координаторе, а рабочим узлам передаются только её схема и метаданные.
Обратите внимание, что добавление локальных таблиц к метаданным имеет небольшую стоимость. При добавлении таблицы в citus она начинает отслеживаться в pg_dist_partition. Локальные таблицы, добавляемые в метаданные, наследуют те же ограничения, что и таблицы-справочники (см. разделы Создание и изменение распределённых объектов (DDL) и Поддержка SQL и обходные решения).
При использовании функции undistribute_table, в citus из метаданных автоматически удаляются получившиеся локальные таблицы, что убирает такие ограничения для этих таблиц.
Аргументы:
table_name— имя таблицы на узле-координаторе, которая будет добавлена в метаданные citus.cascade_via_foreign_keys— если для этого необязательного аргумента установлено значениеtrue, функция автоматически добавляет в метаданные другие таблицы, находящиеся в связи по внешнему ключу с данной таблицей. Этот аргумент следует использовать с осторожностью, поскольку потенциально он может затронуть множество таблиц. Значение по умолчанию —false.
В примере ниже таблица
nationопределяется как локальная таблица координатора, доступная с любого узла:SELECT citus_add_local_table_to_metadata('nation');update_distributed_table_colocation (table_name regclass, colocate_with text) returns void#Изменяет совмещение распределённой таблицы. Эту функцию также можно использовать для отмены совмещения распределённой таблицы. Расширение citus будет неявно совмещать две таблицы, если столбец распределения имеет один и тот же тип. Это полезно в случае, если таблицы связаны и будут соединяться. Если таблицы
AиBсовмещены и таблицаAперебалансируется, таблицаBтакже будет перебалансирована. Если таблицаBне имеет идентификатора реплики, перебалансировка завершится ошибкой. Таким образом, функция может быть полезна для отмены неявного совмещения. Обратите внимание, что эта функция не перемещает данные физически.Аргументы:
table_name— имя таблицы, совмещение которой будет изменено.colocate_with— таблица, с которой будет совмещена указанная таблица.
Чтобы отменить совмещение таблицы, укажите
colocate_with => 'none'.В примере ниже показано, как совмещение таблицы
Aизменяется вместе с совмещением таблицыB:SELECT update_distributed_table_colocation('A', colocate_with => 'B');Предположим, что таблицы
AиBсовмещены (возможно, неявно). Чтобы отменить совмещение, сделайте следующее:SELECT update_distributed_table_colocation('A', colocate_with => 'none');Теперь предположим, что таблицы
A,B,CиDсовмещены, и необходимо совместить таблицуAсBи таблицуCсD:SELECT update_distributed_table_colocation('C', colocate_with => 'none'); SELECT update_distributed_table_colocation('D', colocate_with => 'C');Чтобы изменить совмещение распределённой по хешу таблицы с именем
none, выполните:SELECT update_distributed_table_colocation('"none"', colocate_with => 'другая_распределённая_по_хешу_таблица');create_distributed_function (function_name regprocedure, distribution_arg_name text, colocate_with text, force_delegation bool) returns void#Транслирует функцию с узла-координатора на рабочие узлы и помечает её для распределённого выполнения. Когда распределённая функция вызывается на узле-координаторе, citus использует значение аргумента
distribution_arg_name, чтобы выбрать рабочий узел для выполнения функции. Вызов этой функции на рабочих узлах повышает уровень распараллеливания и может приблизить код к данным в сегментах для уменьшения задержки.Обратите внимание, что путь поиска Postgres Pro не транслируется с узла-координатора на рабочие узлы во время выполнения распределённой функции, поэтому код распределённой функции должен быть дополнен именами объектов базы данных. Уведомления, выдаваемые функциями, не будут показываться пользователю.
Аргументы:
function_name— имя распределяемой функции. Имя должно включать типы параметров функции в круглых скобках, поскольку несколько функций могут иметь одно и то же имя в Postgres Pro. Например,'foo(int)'отличается от'foo(int, text)'.distribution_arg_name— имя аргумента, по которому осуществляется распределение. Для удобства (или если у аргументов функции нет имён) можно использовать позиционный заполнитель, например'$1'. Если этот аргумент не указан, то на рабочих узлах создастся функция, название которой передано в аргументеfunction_name. Если в будущем будут добавляться новые рабочие узлы, функция будет автоматически создаваться и на них. Это необязательный аргумент.colocate_with— когда распредёленная функция выполняет операции чтения или записи в распределённую таблицу (или, в более общем смысле, совмещаемые таблицы), обязательно укажите эту таблицу, используя этот аргумент. Он гарантирует, что каждый вызов функции выполняется на рабочем узле, содержащем соответствующие сегменты. Это необязательный аргумент.force_delegation. Значение по умолчанию —NULL.
Пример использования этой функции:
-- Пример функции, которая изменяет вымышленную таблицу -- event_responses, которая распределена по event_id CREATE OR REPLACE FUNCTION register_for_event(p_event_id int, p_user_id int) RETURNS void LANGUAGE plpgsql AS $fn$ BEGIN INSERT INTO event_responses VALUES ($1, $2, 'yes') ON CONFLICT (event_id, user_id) DO UPDATE SET response = EXCLUDED.response; END; $fn$; -- Распределение функции по рабочим узлам, используя аргумент p_event_id, -- чтобы определить, на какой сегмент влияет каждый её вызов, и явное -- совмещение с таблицей event_responses, обновляемой этой функцией SELECT create_distributed_function( 'register_for_event(int, int)', 'p_event_id', colocate_with := 'event_responses' );
alter_columnar_table_set (table_name regclass, chunk_group_row_limit int, stripe_row_limit int, compression name, compression_level int) returns void#Изменяет параметры столбцовой таблицы. Вызов этой функции для нестолбцовой таблицы вызывает ошибку. Все аргументы, кроме
table_name, являются необязательными.Чтобы просмотреть текущие параметры для всех столбцовых таблиц, воспользуйтесь этой таблицей:
SELECT * FROM columnar.options;
Значения по умолчанию столбцовых параметров для вновь создаваемых таблиц можно переопределить с помощью следующих параметров конфигурации:
columnar.compressioncolumnar.compression_levelcolumnar.stripe_row_countcolumnar.chunk_row_count
Аргументы:
table_name— имя столбцовой таблицы.chunk_row_count— максимальное количество строк в порции для вставляемых данных. Существующие порции данных не изменяются и могут содержать больше строк, чем указанное максимальное значение. Значение по умолчанию —10000.stripe_row_count— максимальное количество строк на массив для вставляемых данных. Существующие массивы данных не будут изменены и могут содержать больше строк, чем это максимальное значение. Значение по умолчанию —150000.compression— тип сжатия для вставляемых данных. Существующие данные не будут сжаты повторно или распакованы. Значение по умолчанию, которое не рекомендуется изменять, —zstd(если поддержка скомпилирована). Допустимые значения:none,pglz,zstd,lz4иlz4hc.compression_level. Допустимые значения: от 1 до 19. Если выбранный уровень не поддерживается методом сжатия, будет выбран ближайший поддерживаемый уровень.
Пример использования этой функции:
SELECT alter_columnar_table_set( 'my_columnar_table', compression => 'none', stripe_row_count => 10000);
create_time_partitions (table_name regclass, partition_interval interval, end_at timestamptz, start_from timestamptz) returns boolean#Создаёт секции заданного интервала для покрытия заданного временного диапазона. Если создаются новые секции, возвращает
true, иfalse, если секции уже существуют.Аргументы:
table_name— таблица, для которой создаются новые секции. Таблица должна быть секционирована по одному столбцу типаdate,timestampилиtimestamptz.partition_interval— интервал времени, например'2 hours'или'1 month', который будет использоваться при задании диапазонов для новых секций.end_at— создавать секции до указанного времени. Последняя секция будет содержать точкуend_at, после которой не будут создаваться новые секции.start_from— выбрать первую секцию так, чтобы она содержала точкуstart_from. Значение по умолчанию —now().
Пример использования этой функции:
-- Создавать ежемесячные секции в течение года -- в таблице foo, начиная с текущего времени SELECT create_time_partitions( table_name := 'foo', partition_interval := '1 month', end_at := now() + '12 months' );
drop_old_time_partitions (table_name regclass, older_than timestamptz)#Удаляет все секции, интервалы которых совпадают с заданной временной меткой. В дополнение к этой функции можно использовать функцию alter_old_partitions_set_access_method для сжатия старых секций с помощью столбцового хранилища.
Аргументы:
table_name— таблица, для которой удаляются секции. Таблица должна быть секционирована по одному столбцу типаdate,timestampилиtimestamptz.older_than— удалить секции, верхний предел которых меньше или равен значениюolder_than.
В данном примере показано, как использовать эту процедуру:
-- Удаление секций старше года CALL drop_old_time_partitions('foo', now() - interval '12 months');alter_old_partitions_set_access_method (parent_table_name regclass, older_than timestamptz, new_access_method name)#В сценариях использования данных временных рядов таблицы часто секционируются по времени, а старые секции сжимаются в столбцовое хранилище, доступное только для чтения.
Аргументы:
parent_table_name— таблица, для которой изменяются секции. Таблица должна быть секционирована по одному столбцу типаdate,timestampилиtimestamptz.older_than— изменить секции, верхний предел диапазона которых меньше или равен значениюolder_than.new_access_method. Допустимые значения:heapдля хранения на основе строк илиcolumnarдля столбцового хранения.
В данном примере показано, как использовать эту процедуру:
CALL alter_old_partitions_set_access_method( 'foo', now() - interval '6 months', 'columnar' );
J.5.8.5.1.2. Метаданные / информация о конфигурации #
citus_add_node (nodename text, nodeport integer, groupid integer, noderole noderole, nodecluster name) returns integer#Примечание
Для запуска этой функции требуются права суперпользователя БД.
Регистрирует добавление нового узла в кластер в таблице метаданных citus pg_dist_node. Эта функция также копирует таблицы-справочники на новый узел и возвращает столбец
nodeidиз строки, вставленной вpg_dist_node.Если функция вызывается в кластере с одним узлом, сначала следует обязательно вызвать функцию citus_set_coordinator_host.
Аргументы:
nodename— DNS-имя или IP-адрес добавляемого узла.nodeport— порт, через который Postgres Pro принимает подключения на рабочем узле.groupid— группа из одного ведущего сервера и его ведомых серверов, используемая только для потоковой репликации. Обязательно установите для этого аргумента значение больше ноля, поскольку ноль зарезервирован для узла-координатора. Значение по умолчанию —-1.noderole— роль узла. Допустимые значения:primaryиsecondary. Значение по умолчанию —primary.nodecluster— имя кластера. Значение по умолчанию —default.
Пример использования этой функции:
SELECT * FROM citus_add_node('new-node', 12345); citus_add_node ----------------- 7 (1 row)citus_update_node (node_id int, new_node_name text, new_node_port int, force bool, lock_cooldown int) returns void#Примечание
Для запуска этой функции требуются права суперпользователя БД.
Изменяет адрес и порт узла, зарегистрированного в таблице метаданных citus pg_dist_node.
Аргументы:
node_id— идентификатор узла из таблицыpg_dist_node.new_node_name— изменённое DNS-имя или IP-адрес узла.new_node_port— изменённый порт, через который Postgres Pro принимает подключения на рабочем узле.force. Значение по умолчанию —false.lock_cooldown. Значение по умолчанию —10000.
Пример использования этой функции:
SELECT * FROM citus_update_node(123, 'new-address', 5432);
citus_set_node_property (nodename text, nodeport integer, property text, value boolean) returns void#Изменяет параметры в таблице метаданных citus pg_dist_node. На данный момент можно изменить только параметр
shouldhaveshards.Аргументы:
nodename— DNS-имя или IP-адрес узла.nodeport— порт, через который Postgres Pro принимает подключения на рабочем узле.property— столбец, изменяемый вpg_dist_node. На данный момент поддерживается только параметрshouldhaveshard.value— новое значение для столбца.
Пример использования этой функции:
SELECT * FROM citus_set_node_property('localhost', 5433, 'shouldhaveshards', false);citus_add_inactive_node (nodename text, nodeport integer, groupid integer, noderole noderole, nodecluster name) returns integer#Примечание
Для запуска этой функции требуются права суперпользователя БД.
Эта функция, как и citus_add_node, регистрирует новый узел в pg_dist_node. Однако при этом она помечает новый узел как неактивный, то есть в него не будут размещаться никакие сегменты. Кроме того, эта функция не копирует таблицы-справочники на новый узел. Функция возвращает столбец
nodeidиз строки, вставленной вpg_dist_node.Аргументы:
nodename— DNS-имя или IP-адрес добавляемого узла.nodeport— порт, через который Postgres Pro принимает подключения на рабочем узле.groupid— группа из одного ведущего сервера и нуля или более ведомых серверов, используемая только для потоковой репликации. Значение по умолчанию —-1.noderole— роль узла. Допустимые значения:primaryиsecondary. Значение по умолчанию —primary.nodecluster— имя кластера. Значение по умолчанию —default.
Пример использования этой функции:
SELECT * FROM citus_add_inactive_node('new-node', 12345); citus_add_inactive_node -------------------------- 7 (1 row)citus_activate_node (nodename text, nodeport integer) returns integer#Примечание
Для запуска этой функции требуются права суперпользователя БД.
Отмечает узел как активный в таблице метаданных citus pg_dist_node и копирует таблицы-справочники на узел. Эта функция может быть полезна для узлов, добавленных с помощью citus_add_inactive_node. Функция возвращает столбец
nodeidиз строки, вставленной вpg_dist_node.Аргументы:
nodename— DNS-имя или IP-адрес добавляемого узла.nodeport— порт, через который Postgres Pro принимает подключения на рабочем узле.
Пример использования этой функции:
SELECT * FROM citus_activate_node('new-node', 12345); citus_activate_node ---------------------- 7 (1 row)citus_disable_node (nodename text, nodeport integer, synchronous bool) returns void#Примечание
Для запуска этой функции требуются права суперпользователя БД.
Эта функция противоположна citus_activate_node. Она отмечает узел как неактивный в таблице метаданных citus pg_dist_node и временно удаляет его из кластера. Функция также удаляет все размещения таблицы-справочника из отключённого узла. Чтобы повторно активировать узел, вызовите функцию citus_activate_node ещё раз.
Аргументы:
nodename— DNS-имя или IP-адрес отключаемого узла.nodeport— порт, через который Postgres Pro принимает подключения на рабочем узле.synchronous. Значение по умолчанию —false.
Пример использования этой функции:
SELECT * FROM citus_disable_node('new-node', 12345);citus_add_secondary_node (nodename text, nodeport integer, primaryname text, primaryport integer, nodecluster name) returns integer#Примечание
Для запуска этой функции требуются права суперпользователя БД.
Регистрирует новый ведомый узел в кластере для существующего ведущего узла. Функция изменяет таблицу метаданных citus pg_dist_node и возвращает столбец
nodeidдля ведомого узла из строки, вставленной вpg_dist_node.Аргументы:
nodename— DNS-имя или IP-адрес добавляемого узла.nodeport— порт, через который Postgres Pro принимает подключения на рабочем узле.primaryname— DNS-имя или IP-адрес ведущего узла для данного ведомого узла.primaryport— порт, через который Postgres Pro принимает подключения на ведущем узле.nodecluster— имя кластера. Значение по умолчанию —default.
Пример использования этой функции:
SELECT * FROM citus_add_secondary_node('new-node', 12345, 'primary-node', 12345); citus_add_secondary_node --------------------------- 7 (1 row)citus_remove_node (nodename text, nodeport integer) returns void#Примечание
Для запуска этой функции требуются права суперпользователя БД.
Удаляет указанный узел из таблицы метаданных pg_dist_node. Эта функция вызовет ошибку, если на узле есть размещения сегментов. Поэтому прежде чем использовать эту функцию, необходимо удалить сегменты с этого узла.
Аргументы:
nodename— DNS-имя удаляемого узла.nodeport— порт, через который Postgres Pro принимает подключения на рабочем узле.
Пример использования этой функции:
SELECT citus_remove_node('new-node', 12345); citus_remove_node -------------------- (1 row)citus_get_active_worker_nodes () returns setof record#Возвращает адреса и номера портов активных рабочих узлов в виде списка кортежей, где каждый кортеж содержит следующую информацию:
node_name— DNS-имя рабочего узла.node_port— порт на рабочем узле, через который сервер базы данных принимает подключения.
Пример вывода функции показан ниже:
SELECT * FROM citus_get_active_worker_nodes(); node_name | node_port -----------+----------- localhost | 9700 localhost | 9702 localhost | 9701 (3 rows)
citus_backend_gpid () returns bigint#Возвращает глобальный идентификатор процесса (GPID) для сервера Postgres Pro, обслуживающего текущий сеанс. Значение GPID кодирует как узел в кластере citus, так и идентификатор процесса операционной системы Postgres Pro на этом узле. GPID возвращается в следующем виде: (идентификатор узла * 10 000 000 000) + идентификатор процесса.
В citus расширены возможности Postgres Pro функций для передачи сигналов серверу
pg_cancel_backendиpg_terminate_backend: теперь они могут принимать GPID. В citus вызов этих функций на одном узле может взаимодействовать с сервером, работающим на другом узле.Пример вывода функции показан ниже:
SELECT citus_backend_gpid();
citus_backend_gpid -------------------- 10000002055citus_check_cluster_node_health () returns setof record#Проверяет связь между всеми узлами. Если имеется N узлов, эта функция проверяет все N2 соединений между ними. Функция возвращает список кортежей, каждый из которых содержит следующую информацию:
from_nodename— DNS-имя исходного рабочего узла.from_nodeport— порт на исходном рабочем узле, через который сервер БД принимает подключения.to_nodename— DNS-имя целевого рабочего узла.to_nodeport— порт целевого рабочего узла, через который сервер БД принимает подключения.result— может ли быть установлено соединение.
Пример вывода функции показан ниже:
SELECT * FROM citus_check_cluster_node_health();
from_nodename │ from_nodeport │ to_nodename │ to_nodeport │ result ---------------+---------------+-------------+-------------+-------- localhost | 1400 | localhost | 1400 | t localhost | 1400 | localhost | 1401 | t localhost | 1400 | localhost | 1402 | t localhost | 1401 | localhost | 1400 | t localhost | 1401 | localhost | 1401 | t localhost | 1401 | localhost | 1402 | t localhost | 1402 | localhost | 1400 | t localhost | 1402 | localhost | 1401 | t localhost | 1402 | localhost | 1402 | t (9 rows)
citus_set_coordinator_host (host text, port integer, node_role noderole, node_cluster name) returns void#Эта функция нужна для добавления рабочих узлов в кластер citus, который изначально создавался как кластер с одним узлом. Когда узел-координатор регистрирует новый рабочий узел, адрес узла-координатора добавляется из значения параметра конфигурации citus.local_hostname, который по умолчанию имеет значение
localhost. Рабочий узел попытается подключиться кlocalhostдля обмена данными с координатором, но такое поведение некорректно.Таким образом, в кластере с одним узлом системный администратор должен вызвать эту функцию перед вызовом функции citus_add_node.
Аргументы:
host— DNS-имя узла-координатора.port— порт, через который узел-координатор принимает подключения Postgres Pro. Этот необязательный параметр по умолчанию имеет значениеcurrent_setting('port').node_role— роль узла. Этот необязательный параметр по умолчанию имеет значениеprimary.node_cluster— имя кластера. Этот необязательный параметр по умолчанию имеет значениеdefault.
Пример использования этой функции:
-- Допустим, есть кластер с одним узлом -- Сначала установите порт, через который будут подключаться рабочие узлы SELECT citus_set_coordinator_host('coord.example.com', 5432); -- Затем добавьте рабочий узел SELECT * FROM citus_add_node('worker1.example.com', 5432);get_shard_id_for_distribution_column (table_name regclass, distribution_value "any") returns bigint#В citus каждая строка распределённой таблицы назначается сегменту на основе значения столбца распределения строки и метода распределения таблицы. В большинстве случаев точное сопоставление — это низкоуровневая функциональность, которая не всегда нужна администратору БД. Однако определение сегмента строки может оказаться полезным либо для ручного обслуживания базы данных, либо просто для удовлетворения любопытства. Функция
get_shard_id_for_distribution_columnпредоставляет эту информацию для таблиц с распределением по хешу, а также для таблиц-справочников и возвращает идентификатор сегмента, который в citus связывается со значением столбца распределения для данной таблицы.Аргументы:
table_name— имя распределённой таблицы.distribution_value— значение столбца распределения. Значение по умолчанию —NULL.
Пример использования этой функции:
SELECT get_shard_id_for_distribution_column('my_table', 4); get_shard_id_for_distribution_column -------------------------------------- 540007 (1 row)column_to_column_name (table_name regclass, column_var_text text) returns text#Преобразует столбец
partkeyтаблицы pg_dist_partition в текстовое имя столбца. Эта функция полезна для определения столбца распределения распределённой таблицы. Функция возвращает имя столбца распределения таблицыtable_name. За подробностями обратитесь к разделу Выбор столбца распределения для таблицы.Аргументы:
table_name— имя распределённой таблицы.column_var_text— значение столбцаpartkeyв таблицеpg_dist_partition.
Пример использования этой функции:
-- Получить имя столбца распределения для таблицы products SELECT column_to_column_name(logicalrelid, partkey) AS dist_col_name FROM pg_dist_partition WHERE logicalrelid='products'::regclass;
┌───────────────┐ │ dist_col_name │ ├───────────────┤ │ company_id │ └───────────────┘
citus_relation_size (logicalrelid regclass) returns bigint#Возвращает размер дискового пространства, используемого всеми сегментами указанной распределённой таблицы. Сюда входит размер «основного слоя», но не учитываются карта видимости и карта свободного пространства для сегментов.
Аргументы:
logicalrelid— имя распределённой таблицы.
Пример использования этой функции:
SELECT pg_size_pretty(citus_relation_size('github_events'));pg_size_pretty -------------- 23 MB
citus_table_size (logicalrelid regclass) returns bigint#Возвращает размер дискового пространства, используемого всеми сегментами указанной распределённой таблицы, за исключением индексов (но с учётом TOAST, карты свободного пространства и карты видимости).
Аргументы:
logicalrelid— имя распределённой таблицы.
Пример использования этой функции:
SELECT pg_size_pretty(citus_table_size('github_events'));pg_size_pretty -------------- 37 MB
citus_total_relation_size (logicalrelid regclass, fail_on_error boolean) returns bigint#Возвращает общий размер дискового пространства, используемого всеми сегментами указанной распределённой таблицы, включая все индексы и данные TOAST.
Аргументы:
logicalrelid— имя распределённой таблицы.fail_on_error. Значение по умолчанию —true.
Пример использования этой функции:
SELECT pg_size_pretty(citus_total_relation_size('github_events'));pg_size_pretty -------------- 73 MB
citus_stat_statements_reset () returns void#Удаляет все строки из таблицы citus_stat_statements. Обратите внимание, что функция работает независимо от функции
pg_stat_statements_reset. Чтобы сбросить всю статистику, вызовите обе функции.
J.5.8.5.1.3. Функции для управления и восстановления кластера #
citus_move_shard_placement (shard_id bigint, source_node_name text, source_node_port integer, target_node_name text, target_node_port integer, shard_transfer_mode citus.shard_transfer_mode) returns void#Перемещает данный сегмент (и соседние с ним сегменты) с одного узла на другой. Обычно эта функция используется неявно во время перебалансировки сегментов, а не вызывается напрямую администратором базы данных.
Существует два способа перемещения данных: блокирующий и неблокирующий. При блокирующем способе все изменения в сегменте приостанавливаются на время перемещения. Второй способ, позволяющий избежать блокировки записи в сегменты, основан на логической репликации Postgres Pro 10.
После успешной операции перемещения сегменты на исходном узле удаляются. Если перемещение в какой-либо момент прерывается, эта функция выдаёт ошибку и оставляет исходный и целевой узлы без изменений.
Аргументы:
shard_id— идентификатор перемещаемого сегмента.source_node_name— DNS-имя узла, на котором есть работоспособное размещение сегмента («исходный» узел).source_node_port— порт на исходном рабочем узле, через который сервер БД принимает подключения.target_node_name— DNS-имя узла, на котором есть недопустимое размещение сегмента («целевой» узел).target_node_port— порт на целевом рабочем узле, через который сервер БД принимает подключения.shard_transfer_mode— указать метод репликации: логическая репликация Postgres Pro или командаCOPYмежду рабочими узлами. Этот необязательный аргумент может принимать следующие значения:auto— требовать идентификатор реплики, если возможна логическая репликация, в противном случае использовать ранее принятое поведение. Это значение по умолчанию.force_logical— использовать логическую репликацию, даже если таблица не имеет идентификатора реплики. Любые одновременные операторы изменения/удаления таблицы во время репликации завершатся ошибкой.block_writes— использовать командуCOPY(блокирующую запись) для таблиц, у которых нет первичного ключа или идентификатора реплики.
Пример использования этой функции:
SELECT citus_move_shard_placement(12345, '
с_узла', 5432, 'на_узел', 5432);citus_rebalance_start (rebalance_strategy name, drain_only boolean, shard_transfer_mode citus.shard_transfer_mode) returns bigint#Перемещает сегменты таблицы, чтобы равномерно распределить их среди рабочих узлов. Эта функция начинает фоновое задание по перебалансировке и сразу возвращает результат.
В процессе перебалансировки сначала определяется список перемещений, необходимых для гарантии баланса кластера в пределах заданного порога. Затем размещения сегментов перемещаются одно за другим с исходного узла на целевой, и обновляются соответствующие метаданные сегментов, чтобы отразить перемещение.
Каждому сегменту назначается стоимость при определении «равномерности распределения сегментов». По умолчанию каждый сегмент имеет одинаковую стоимость (значение 1), поэтому распределение для выравнивания стоимости между рабочими узлами аналогично выравниванию количества сегментов на каждом узле. Стратегия постоянной стоимости называется
by_shard_countи является стратегией перебалансировки по умолчанию.Стратегия
by_shard_countподходит для использования в следующих условиях:Сегменты примерно одного размера.
Сегменты получают примерно одинаковый объём трафика.
Все рабочие узлы одинакового размера/типа.
Сегменты не прикреплены к конкретным рабочим узлам.
Если какое-либо из этих условий не выполняется, перебалансировка с использованием стратегии
by_shard_countможет привести к созданию неудачного плана.Если какое-либо из этих условий не выполняется, перебалансировка с использованием стратегии
by_shard_countможет привести к созданию неудачного плана.Стратегия перебалансировки по умолчанию —
by_disk_size. Чтобы настроить стратегию, используйте параметрrebalance_strategy.Рекомендуется вызывать функцию get_rebalance_table_shards_plan перед
citus_rebalance_start, чтобы увидеть и проверить действия, которые необходимо выполнить.Аргументы:
rebalance_strategy— имя стратегии в таблице pg_dist_rebalance_strategy. Если этот аргумент опущен, функция выбирает указанную в таблице стратегию по умолчанию. Этот необязательный аргумент по умолчанию имеет значениеNULL.При значении
trueаргументаdrain_onlyсегменты перемещаются только с тех рабочих узлов, у которых в таблице pg_dist_node для параметраshouldhaveshardsустановлено значениеfalse. Этот необязательный аргумент по умолчанию имеет значениеfalse.shard_transfer_mode— указать метод репликации: логическая репликация Postgres Pro или командаCOPYмежду рабочими узлами. Этот необязательный аргумент может принимать следующие значения:auto— требовать идентификатор реплики, если возможна логическая репликация, в противном случае использовать ранее принятое поведение. Это значение по умолчанию.force_logical— использовать логическую репликацию, даже если таблица не имеет идентификатора реплики. Любые одновременные операторы изменения/удаления таблицы во время репликации завершатся ошибкой.block_writes— использовать командуCOPY(блокирующую запись) для таблиц, у которых нет первичного ключа или идентификатора реплики.
В примере показана попытка перебалансировки сегментов:
SELECT citus_rebalance_start(); NOTICE: Scheduling... NOTICE: Scheduled as job 1337. DETAIL: Rebalance scheduled as background job 1337. HINT: To monitor progress, run: SELECT details FROM citus_rebalance_status();
citus_rebalance_status () returns table#Позволяет следить за ходом перебалансировки. Результат возвращается сразу, а перебалансировка продолжается в фоновом режиме.
Чтобы получить общую информацию о перебалансировке, обратитесь к таблице статуса. В результате будет показано базовое состояние задания:
SELECT * FROM citus_rebalance_status();
. job_id | state | job_type | description | started_at | finished_at | details --------+----------+-----------+---------------------------------+-------------------------------+-------------------------------+----------- 4 | running | rebalance | Rebalance colocation group 1 | 2022-08-09 21:57:27.833055+02 | 2022-08-09 21:57:27.833055+02 | { ... }Особенности перебалансировщика находятся в столбце
detailsв формате JSON:SELECT details FROM citus_rebalance_status();
{ "phase": "copy", "phase_index": 1, "phase_count": 3, "last_change":"2022-08-09 21:57:27", "colocations": { "1": { "shard_moves": 30, "shard_moved": 29, "last_move":"2022-08-09 21:57:27" }, "1337": { "shard_moves": 130, "shard_moved": 0 } } }citus_rebalance_stop () returns void#Отменяет выполняющуюся перебалансировку, если она есть.
citus_rebalance_wait () returns void#Выдаёт блокировку до завершения текущей перебалансировки. Если перебалансировка не выполняется во время вызова этой функции, сразу возвращается результат.
Функцию можно использовать в скриптах или для тестирования производительности.
get_rebalance_table_shards_plan () returns table#Выводит запланированные перемещения сегментов citus_rebalance_start, не выполняя их. Есть небольшая вероятность, что эта функция выведет немного другой план, чем при вызове
citus_rebalance_startс теми же аргументами. Это может происходить, потому что функции выполняются не одновременно, так что информация о кластере, например дисковое пространство, может отличаться от вызова к вызову. Функция возвращает кортежи, содержащие следующие столбцы:table_name— таблица, сегменты которой будут перемещаться.shardid— нужный сегмент.shard_size— размер сегмента в байтах.sourcename— адрес исходного узла.sourceport— порт исходного узла.targetname— адрес целевого узла.targetport— порт целевого узла.
Аргументы:
Расширенный набор аргументов для функции citus_rebalance_start:
relation,threshold,max_shard_moves,excluded_shard_listиdrain_only.
get_rebalance_progress () returns table#Как только начинается перебалансировка сегментов, эта функция отображает прогресс каждого задействованного сегмента. Она отслеживает перемещения, запланированные и выполняемые функцией citus_rebalance_start, а также возвращает кортежи, содержащие следующие столбцы:
sessionid— идентификатор серверного процесса (PID) отслеживания перебалансировки в Postgres Pro.table_name— имя таблицы, сегменты которой будут перемещены.shardid— нужный сегмент.shard_size— размер сегмента в байтах.sourcename— адрес исходного узла.sourceport— порт исходного узла.targetname— адрес целевого узла.targetport— порт целевого узла.progress. Могут возвращаться следующие значения:0— ожидает перемещения,1— перемещение,2— перемещение завершено.source_shard_size— размер сегмента на исходном узле в байтах.target_shard_size— размер сегмента на целевом узле в байтах.
Пример использования этой функции:
SELECT * FROM get_rebalance_progress();
┌───────────┬────────────┬─────────┬────────────┬───────────────┬────────────┬───────────────┬────────────┬──────────┬───────────────────┬───────────────────┐ │ sessionid │ table_name │ shardid │ shard_size │ sourcename │ sourceport │ targetname │ targetport │ progress │ source_shard_size │ target_shard_size │ ├───────────┼────────────┼─────────┼────────────┼───────────────┼────────────┼───────────────┼────────────┼──────────┼───────────────────┼───────────────────┤ │ 7083 │ foo │ 102008 │ 1204224 │ n1.foobar.com │ 5432 │ n4.foobar.com │ 5432 │ 0 │ 1204224 │ 0 │ │ 7083 │ foo │ 102009 │ 1802240 │ n1.foobar.com │ 5432 │ n4.foobar.com │ 5432 │ 0 │ 1802240 │ 0 │ │ 7083 │ foo │ 102018 │ 614400 │ n2.foobar.com │ 5432 │ n4.foobar.com │ 5432 │ 1 │ 614400 │ 354400 │ │ 7083 │ foo │ 102019 │ 8192 │ n3.foobar.com │ 5432 │ n4.foobar.com │ 5432 │ 2 │ 0 │ 8192 │ └───────────┴────────────┴─────────┴────────────┴───────────────┴────────────┴───────────────┴────────────┴──────────┴───────────────────┴───────────────────┘
citus_add_rebalance_strategy (name name, shard_cost_function regproc, node_capacity_function regproc, shard_allowed_on_node_function regproc, default_threshold float4, minimum_threshold float4, improvement_threshold float4) returns void#Добавляет строку в таблицу pg_dist_rebalance_strategy.
Аргументы:
name— идентификатор новой стратегии.shard_cost_function— определяет функцию, используемую для расчёта «стоимости» каждого сегмента.node_capacity_function— определяет функцию измерения ёмкости узла.shard_allowed_on_node_function— идентифицирует функцию, которая определяет, какие сегменты и на каких узлах можно разместить.default_threshold— порог с плавающей точкой, настраивающий точность балансировки совокупной стоимости сегментов между узлами.minimum_threshold— столбец защиты, содержащий минимально допустимое значение для задающего порог аргумента функции citus_rebalance_start. Значение по умолчанию —0.improvement_threshold. Значение по умолчанию —0.
citus_set_default_rebalance_strategy (name text) returns void#Стратегия, указанная в аргументе данной функции, устанавливается в таблице pg_dist_rebalance_strategy как выбираемая по умолчанию при перебалансировке сегментов.
Аргументы:
name— имя стратегии в таблицеpg_dist_rebalance_strategy.
Пример использования этой функции:
SELECT citus_set_default_rebalance_strategy('by_disk_size');citus_remote_connection_stats () returns setof record#Показывает количество активных соединений с каждым удалённым узлом.
Пример использования этой функции:
SELECT * FROM citus_remote_connection_stats();
. hostname | port | database_name | connection_count_to_node ----------------+------+---------------+-------------------------- citus_worker_1 | 5432 | postgres | 3 (1 row)citus_drain_node (nodename text, nodeport integer, shard_transfer_mode citus.shard_transfer_mode, rebalance_strategy name) returns void#Перемещает сегменты с указанного узла на другие узлы, у которых параметр
shouldhaveshardsв таблице pg_dist_node имеет значениеtrue. Эту функцию следует вызывать перед удалением узла из кластера, т. е. отключением физического сервера узла.Аргументы:
nodename— DNS-имя узла, с которого перемещаются сегменты.nodeport— номер порта узла, с которого перемещаются сегменты.shard_transfer_mode— указать метод репликации: логическая репликация Postgres Pro или командаCOPYмежду рабочими узлами. Этот необязательный аргумент может принимать следующие значения:auto— требовать идентификатор реплики, если возможна логическая репликация, в противном случае использовать ранее принятое поведение. Это значение по умолчанию.force_logical— использовать логическую репликацию, даже если таблица не имеет идентификатора реплики. Любые одновременные операторы изменения/удаления таблицы во время репликации завершатся ошибкой.block_writes— использовать командуCOPY(блокирующую запись) для таблиц, у которых нет первичного ключа или идентификатора реплики.
rebalance_strategy— имя стратегии в таблице pg_dist_rebalance_strategy. Если этот аргумент опущен, функция выбирает стратегию, указанную в таблице как стратегия по умолчанию. Этот необязательный аргумент по умолчанию имеет значениеNULL.
Ниже показаны шаги по удалению одного узла (например, «10.0.0.1» на стандартном порту Postgres Pro):
Переместите сегменты с этого узла.
SELECT * FROM citus_drain_node('10.0.0.1', 5432);Подождите завершения операции.
Удалите узел.
При перемещении сегментов с нескольких узлов рекомендуется использовать функцию citus_rebalance_start. Это позволит citus заранее планировать перемещение и выполнять его минимальное количество раз.
Выполните указанный запрос на каждом удаляемом узле:
SELECT * FROM citus_set_node_property(node_hostname, node_port, 'shouldhaveshards', false);
Переместите с этих узлов все сегменты с помощью функции citus_rebalance_start:
SELECT * FROM citus_rebalance_start(drain_only := true);
Подождите, пока закончится перебалансировка.
Удалите узлы.
isolate_tenant_to_new_shard (table_name regclass, tenant_id "any", cascade_option text, shard_transfer_mode citus.shard_transfer_mode) returns bigint#Создаёт новый сегмент для хранения строк с определённым значением в столбце распределения. Эта функция особенно полезна для сценариев использования citus с несколькими арендаторами, чтобы крупного арендатора можно было разместить отдельно на собственном сегменте и, в конечном счёте, на собственном физическом узле. За подробностями обратитесь к разделу Изоляция арендаторов. Функция возвращает уникальный идентификатор, присвоенный вновь созданному сегменту.
Аргументы:
table_name— имя таблицы, получающей новый сегмент.tenant_id— значение столбца распределения, которое будет назначено новому сегменту.cascade_option. Если установлено значениеCASCADE, сегмент также изолируется от всех таблиц, совмещённых с текущей.shard_transfer_mode— указать метод репликации: логическая репликация Postgres Pro или командаCOPYмежду рабочими узлами. Этот необязательный аргумент может принимать следующие значения:auto— требовать идентификатор реплики, если возможна логическая репликация, в противном случае использовать ранее принятое поведение. Это значение по умолчанию.force_logical— использовать логическую репликацию, даже если таблица не имеет идентификатора реплики. Любые одновременные операторы изменения/удаления таблицы во время репликации завершатся ошибкой.block_writes— использовать командуCOPY(блокирующую запись) для таблиц, у которых нет первичного ключа или идентификатора реплики.
В примере ниже показано, как создать новый сегмент для хранения позиций для арендатора
135:SELECT isolate_tenant_to_new_shard('lineitem', 135);┌─────────────────────────────┐ │ isolate_tenant_to_new_shard │ ├─────────────────────────────┤ │ 102240 │ └─────────────────────────────┘
citus_create_restore_point (name text) returns pg_lsn#Временно блокирует запись в кластер и создаёт именованную точку восстановления на всех узлах. Эта функция аналогична pg_create_restore_point, но применяется ко всем узлам и обеспечивает согласованность точек восстановления между ними. Она хорошо подходит для восстановления на момент времени и разветвления кластера. Функция возвращает значение
coordinator_lsn, т. е. последовательный номер точки восстановления в WAL узла-координатора.Аргументы:
name— имя создаваемой точки восстановления.
Пример использования этой функции:
SELECT citus_create_restore_point('foo');┌────────────────────────────┐ │ citus_create_restore_point │ ├────────────────────────────┤ │ 0/1EA2808 │ └────────────────────────────┘
J.5.8.5.2. Таблицы и представления citus #
J.5.8.5.2.1. Метаданные узла-координатора #
В citus каждая распределённая таблица разделяется на несколько логических сегментов на основе столбца распределения. Затем узел-координатор ведёт таблицы метаданных для отслеживания статистики и информации о состоянии и расположении этих сегментов. В этом разделе описывается каждая такая таблица метаданных и их схема. После записи на узел-координатор эти таблицы можно просматривать и обращаться к ним с помощью SQL.
J.5.8.5.2.1.1. Таблица pg_dist_partition #
В таблице pg_dist_partition хранятся метаданные о распределённых таблицах. Для каждой распределённой таблицы также хранится информация о методе распределения и подробная информация о столбце распределения.
| Имя | Тип | Описание |
|---|---|---|
| logicalrelid | regclass | Распределённая таблица, которой соответствует эта строка. Это значение ссылается на столбец relfilenode в таблице системного каталога pg_class. |
| partmethod | char | Метод секционирования/распределения. Значения этого столбца соответствуют различным методам распределения: h — хеш, n — таблица-справочник. |
| partkey | text | Подробная информация о столбце распределения, включая номер столбца, тип и т. д. |
| colocationid | integer | Группа совмещения, к которой принадлежит эта таблица. К таблицам в одной группе можно применять совмещённое соединение и распределённые свёртки, а также другие оптимизации. Это значение ссылается на столбец colocationid в таблице pg_dist_colocation. |
| repmodel | char | Метод репликации данных. Значения этого столбца соответствуют различным методам репликации: s — потоковая репликация Postgres Pro, t — двухфазная фиксация (для таблиц-справочников). |
SELECT * FROM pg_dist_partition;
logicalrelid | partmethod | partkey | colocationid | repmodel
---------------+------------+------------------------------------------------------------------------------------------------------------------------+--------------+----------
github_events | h | {VAR :varno 1 :varattno 4 :vartype 20 :vartypmod -1 :varcollid 0 :varlevelsup 0 :varnoold 1 :varoattno 4 :location -1} | 2 | s
(1 row)J.5.8.5.2.1.2. Таблица pg_dist_shard #
В таблице pg_dist_shard хранятся метаданные об отдельных сегментах таблицы. Они содержат информацию о том, какой распределённой таблице принадлежит сегмент, и статистику по столбцу распределения для этого сегмента. Метаданные сегментов таблиц, распределённых по хешу, представляют собой диапазоны хеш-токенов, назначенных этим сегментам. Эта статистика используется для устранения посторонних сегментов во время выполнения запросов SELECT.
| Имя | Тип | Описание |
|---|---|---|
| logicalrelid | regclass | Распределённая таблица, которой принадлежит данный сегмент. Это значение ссылается на столбец relfilenode в таблице системного каталога pg_class. |
| shardid | bigint | Глобальный уникальный идентификатор, присвоенный этому сегменту. |
| shardstorage | char | Тип хранения, используемый для этого сегмента. Различные типы хранения рассматриваются в таблице ниже. |
| shardminvalue | text | Для таблиц с распределением по хешу — минимальное значение хеш-токена, назначенное этому сегменту (включительно). |
| shardmaxvalue | text | Для таблиц с распределением по хешу — максимальное значение хеш-токена, назначенное этому сегменту (включительно). |
SELECT * FROM pg_dist_shard; logicalrelid | shardid | shardstorage | shardminvalue | shardmaxvalue ---------------+---------+--------------+---------------+--------------- github_events | 102026 | t | 268435456 | 402653183 github_events | 102027 | t | 402653184 | 536870911 github_events | 102028 | t | 536870912 | 671088639 github_events | 102029 | t | 671088640 | 805306367 (4 rows)
Столбец shardstorage в таблице pg_dist_shard указывает тип хранения, используемый для сегмента. Краткий обзор различных типов хранения сегментов и их представление показаны ниже.
| Тип хранения | значение shardstorage | Описание |
|---|---|---|
| ТАБЛИЧНОЕ | t | Указывает, что в сегменте хранятся данные, принадлежащие обычной распределённой таблице. |
| СТОЛБЦОВОЕ | c | Указывает, что в сегменте хранятся столбцовые данные. (Используется для распределённых таблиц cstore_fdw). |
| СТОРОННЕЕ | f | Указывает, что в сегменте хранятся сторонние данные. (Используется для распределённых таблиц file_fdw). |
J.5.8.5.2.1.3. Представление citus_shards #
В дополнение к описанной выше таблице низкоуровневых метаданных сегментов, в citus есть представление citus_shards, позволяющее легко получить следующую информацию:
Расположение каждого сегмента (узел и порт),
Таблица, которой принадлежит сегмент,
Размер сегмента.
Это представление помогает исследовать сегменты, например, чтобы найти неравенство размеров узлов.
SELECT * FROM citus_shards;
. table_name | shardid | shard_name | citus_table_type | colocation_id | nodename | nodeport | shard_size ------------+---------+--------------+------------------+---------------+-----------+----------+------------ dist | 102170 | dist_102170 | distributed | 34 | localhost | 9701 | 90677248 dist | 102171 | dist_102171 | distributed | 34 | localhost | 9702 | 90619904 dist | 102172 | dist_102172 | distributed | 34 | localhost | 9701 | 90701824 dist | 102173 | dist_102173 | distributed | 34 | localhost | 9702 | 90693632 ref | 102174 | ref_102174 | reference | 2 | localhost | 9701 | 8192 ref | 102174 | ref_102174 | reference | 2 | localhost | 9702 | 8192 dist2 | 102175 | dist2_102175 | distributed | 34 | localhost | 9701 | 933888 dist2 | 102176 | dist2_102176 | distributed | 34 | localhost | 9702 | 950272 dist2 | 102177 | dist2_102177 | distributed | 34 | localhost | 9701 | 942080 dist2 | 102178 | dist2_102178 | distributed | 34 | localhost | 9702 | 933888
Параметр colocation_id относится к группе совмещения. За дополнительной информацией о citus_table_type обратитесь к разделу Типы таблиц.
J.5.8.5.2.1.4. Таблица pg_dist_placement #
В таблице pg_dist_placement отслеживается расположение сегментов на рабочих узлах. Каждый сегмент, назначенный определённому узлу, называется размещением сегмента. В этой таблице хранится информация о состоянии и расположении каждого размещения сегмента.
| Имя | Тип | Описание |
|---|---|---|
| placementid | bigint | Уникальный автоматически сгенерированный идентификатор для каждого отдельного размещения. |
| shardid | bigint | Идентификатор сегмента, связанный с этим размещением. Это значение ссылается на столбец shardid в таблице каталога pg_dist_shard. |
| shardstate | int | Описывает состояние этого размещения. Различные состояния сегментов описаны в следующем разделе. |
| shardlength | bigint | Для таблиц, распределённых по хешу — ноль. |
| groupid | int | Идентификатор, используемый для обозначения группы из одного ведущего сервера и нуля или более ведомых серверов. |
SELECT * FROM pg_dist_placement;
placementid | shardid | shardstate | shardlength | groupid
-------------+---------+------------+-------------+---------
1 | 102008 | 1 | 0 | 1
2 | 102008 | 1 | 0 | 2
3 | 102009 | 1 | 0 | 2
4 | 102009 | 1 | 0 | 3
5 | 102010 | 1 | 0 | 3
6 | 102010 | 1 | 0 | 4
7 | 102011 | 1 | 0 | 4J.5.8.5.2.1.5. Таблица pg_dist_node #
В таблице pg_dist_node содержится информация о рабочих узлах кластера.
| Имя | Тип | Описание |
|---|---|---|
| nodeid | int | Автоматически сгенерированный идентификатор отдельного узла. |
| groupid | int | Идентификатор, используемый для обозначения группы из одного ведущего сервера и нуля или более ведомых серверов. По умолчанию он равнозначен nodeid. |
| nodename | text | Имя или IP-адрес рабочего узла Postgres Pro. |
| nodeport | int | Номер порта, через который рабочий узел Postgres Pro принимает подключения. |
| noderack | text | Информация о размещении стойки этого рабочего узла. Это необязательный столбец. |
| hasmetadata | boolean | Зарезервирован для внутреннего использования. |
| isactive | boolean | Активен ли узел, принимающий размещения сегментов. |
| noderole | text | Является ли узел ведущим или ведомым. |
| nodecluster | text | Имя кластера, содержащего этот узел. |
| metadatasynced | boolean | Зарезервирован для внутреннего использования. |
| shouldhaveshards | boolean | Если установлено значение false, сегменты будут перемещаться с узла при перебалансировке, а сегменты из новых распределённых таблиц не будут размещаться на узле, если они не совмещены с уже существующими на узле сегментами. |
SELECT * FROM pg_dist_node;
nodeid | groupid | nodename | nodeport | noderack | hasmetadata | isactive | noderole | nodecluster | metadatasynced | shouldhaveshards
--------+---------+-----------+----------+----------+-------------+----------+----------+-------------+----------------+------------------
1 | 1 | localhost | 12345 | default | f | t | primary | default | f | t
2 | 2 | localhost | 12346 | default | f | t | primary | default | f | t
3 | 3 | localhost | 12347 | default | f | t | primary | default | f | t
(3 rows)J.5.8.5.2.1.6. Таблица citus.pg_dist_object #
В таблице citus.pg_dist_object содержится список объектов, например типов и функций, которые были созданы на узле-координаторе и распространены на рабочие узлы. При добавлении новых рабочих узлов в кластер citus автоматически создаёт копии распределённых объектов на новых узлах (в правильном порядке, чтобы соответствовать зависимостям объектов).
| Имя | Тип | Описание |
|---|---|---|
| classid | oid | Класс распределённого объекта |
| objid | oid | Идентификатор (OID) распределённого объекта |
| objsubid | integer | Вложенный идентификатор распределённого объекта, например attnum |
| type | text | Часть стабильного адреса, используемая во время обновлений с помощью pg_upgrade |
| object_names | text[] | Часть стабильного адреса, используемая во время обновлений с помощью pg_upgrade |
| object_args | text[] | Часть стабильного адреса, используемая во время обновлений с помощью pg_upgrade |
| distribution_argument_index | integer | Только для распределённых функций/процедур |
| colocationid | integer | Только для распределённых функций/процедур |
«Стабильные адреса» однозначно идентифицируют объекты независимо от конкретного сервера. Во время обновления Postgres Pro в citus объекты отслеживаются с помощью стабильных адресов, созданных функцией pg_identify_object_as_address.
Ниже представлен пример добавления записи в таблицу citus.pg_dist_object функцией create_distributed_function:
CREATE TYPE stoplight AS enum ('green', 'yellow', 'red');
CREATE OR REPLACE FUNCTION intersection()
RETURNS stoplight AS $$
DECLARE
color stoplight;
BEGIN
SELECT *
FROM unnest(enum_range(NULL::stoplight)) INTO color
ORDER BY random() LIMIT 1;
RETURN color;
END;
$$ LANGUAGE plpgsql VOLATILE;
SELECT create_distributed_function('intersection()');
-- will have two rows, one for the TYPE and one for the FUNCTION
TABLE citus.pg_dist_object;-[ RECORD 1 ]---------------+------ classid | 1247 objid | 16780 objsubid | 0 type | object_names | object_args | distribution_argument_index | colocationid | -[ RECORD 2 ]---------------+------ classid | 1255 objid | 16788 objsubid | 0 type | object_names | object_args | distribution_argument_index | colocationid |
J.5.8.5.2.1.7. Представление citus_schemas #
В citus поддерживается сегментирование на основе схем и есть представление citus_schemas, содержащее информацию о том, какие схемы в системе были распределены. В представлении показываются только распределённые схемы, но не локальные.
| Имя | Тип | Описание |
|---|---|---|
| schema_name | regnamespace | Имя распределённой схемы |
| colocation_id | integer | Идентификатор совмещения распределённой схемы |
| schema_size | text | Сводная информация о размерах всех объектов в схеме в понятном человеку виде |
| schema_owner | name | Роль, которой принадлежит схема |
Например:
schema_name | colocation_id | schema_size | schema_owner --------------+---------------+-------------+-------------- user_service | 1 | 0 bytes | user_service time_service | 2 | 0 bytes | time_service ping_service | 3 | 632 kB | ping_service
J.5.8.5.2.1.8. Представление citus_tables #
В представлении citus_tables содержится сводная информация обо всех таблицах, управляемых citus (распределённые и таблицы-справочники). В представлении объединена информация из таблиц метаданных citus, чтобы можно было проще исследовать параметры этих таблиц:
Идентификатор группы совмещения
Размер в понятном для человека виде
Количество сегментов
Владелец (пользователь БД)
Метод доступа (
heapили columnar)
Например:
SELECT * FROM citus_tables;
┌────────────┬──────────────────┬─────────────────────┬───────────────┬────────────┬─────────────┬─────────────┬───────────────┐ │ table_name │ citus_table_type │ distribution_column │ colocation_id │ table_size │ shard_count │ table_owner │ access_method │ ├────────────┼──────────────────┼─────────────────────┼───────────────┼────────────┼─────────────┼─────────────┼───────────────┤ │ foo.test │ distributed │ test_column │ 1 │ 0 bytes │ 32 │ citus │ heap │ │ ref │ reference │ <none> │ 2 │ 24 GB │ 1 │ citus │ heap │ │ test │ distributed │ id │ 1 │ 248 TB │ 32 │ citus │ heap │ └────────────┴──────────────────┴─────────────────────┴───────────────┴────────────┴─────────────┴─────────────┴───────────────┘
J.5.8.5.2.1.9. Представление time_partitions #
Расширение citus поддерживает пользовательские функции для управления секциями в сценарии использования временных рядов, а также представление time_partitions для исследования управляемых этой функцией секций.
В представлении есть следующие столбцы:
parent_table— секционированная таблица.partition_column— столбец, по которому секционирована родительская таблица.partition— имя секции.from_value— нижняя граница времени для строк этой секции.to_value— верхняя граница времени для строк этой секции.access_method—heapдля строкового хранения иcolumnarдля столбцового хранения.
SELECT * FROM time_partitions;
┌────────────────────────┬──────────────────┬─────────────────────────────────────────┬─────────────────────┬─────────────────────┬───────────────┐ │ parent_table │ partition_column │ partition │ from_value │ to_value │ access_method │ ├────────────────────────┼──────────────────┼─────────────────────────────────────────┼─────────────────────┼─────────────────────┼───────────────┤ │ github_columnar_events │ created_at │ github_columnar_events_p2015_01_01_0000 │ 2015-01-01 00:00:00 │ 2015-01-01 02:00:00 │ columnar │ │ github_columnar_events │ created_at │ github_columnar_events_p2015_01_01_0200 │ 2015-01-01 02:00:00 │ 2015-01-01 04:00:00 │ columnar │ │ github_columnar_events │ created_at │ github_columnar_events_p2015_01_01_0400 │ 2015-01-01 04:00:00 │ 2015-01-01 06:00:00 │ columnar │ │ github_columnar_events │ created_at │ github_columnar_events_p2015_01_01_0600 │ 2015-01-01 06:00:00 │ 2015-01-01 08:00:00 │ heap │ └────────────────────────┴──────────────────┴─────────────────────────────────────────┴─────────────────────┴─────────────────────┴───────────────┘
J.5.8.5.2.1.10. Таблица pg_dist_colocation #
В таблице pg_dist_colocation содержится информация о том, какие сегменты таблиц должны быть размещены вместе, или совмещены. Если две таблицы находятся в одной группе совмещения, в citus гарантируется, что сегменты с одинаковыми значениями секций будут размещены на одних и тех же рабочих узлах. Таким образом можно оптимизировать соединения, выполнять некоторые распределённые свёртки и поддерживать внешние ключи. Совмещение сегментов предполагается, когда количество сегментов и типы столбцов секционирования совпадают в двух таблицах; однако при создании распределённой таблицы можно указать пользовательскую группу совмещения, если необходимо.
| Имя | Тип | Описание |
|---|---|---|
| colocationid | int | Уникальный идентификатор группы совмещения, которому соответствует эта строка |
| shardcount | int | Количество сегментов всех таблиц в этой группе совмещения |
| replicationfactor | int | Коэффициент репликации всех таблиц в этой группе совмещения. (Устарел) |
| distributioncolumntype | oid | Тип столбца распределения всех таблиц в этой группе совмещения |
| distributioncolumncollation | oid | Правило сортировки столбца распределения всех таблиц в этой группе совмещения |
SELECT * FROM pg_dist_colocation;
colocationid | shardcount | replicationfactor | distributioncolumntype | distributioncolumncollation
--------------+------------+-------------------+------------------------+-----------------------------
2 | 32 | 1 | 20 | 0
(1 row)J.5.8.5.2.1.11. Таблица pg_dist_rebalance_strategy #
В этой таблице определяются стратегии, которые могут использоваться функцией citus_rebalance_start для определения целевого узла для перемещения сегментов.
| Имя | Тип | Описание |
|---|---|---|
| name | name | Уникальное имя стратегии |
| default_strategy | boolean | Должна ли функция citus_rebalance_start выбирать эту стратегию по умолчанию. Чтобы изменить этот столбец, используйте функцию citus_set_default_rebalance_strategy. |
| shard_cost_function | regproc | Идентификатор для функции расчёта стоимости, которая должна принимать shardid типа bigint и возвращать определение стоимости типа real. |
| node_capacity_function | regproc | Идентификатор для функции расчёта ёмкости, которая должна принимать nodeid типа int и возвращать определение ёмкости узла типа real. |
| shard_allowed_on_node_function | regproc | Идентификатор функции, принимающей shardid типа bigint и nodeidarg типа int и возвращающей значение типа boolean, которое показывает, может ли данный сегмент храниться на узле. |
| default_threshold | float4 | Пороговое значение, по которому узел определяется как переполненный или незаполненный. Если стоимость сегментов больше этого значения, функция citus_rebalance_start должна начать перемещать сегменты с узла, а если меньше — на узел. |
| minimum_threshold | float4 | Защита от установки слишком низкого порогового значения для аргумента citus_rebalance_start. |
| improvement_threshold | float4 | Определяет, нужно ли перемещать сегмент во время перебалансировки. Перебалансировщик переместит сегмент, когда отношение производительности с перемещением сегмента к производительности без него пересечёт пороговое значение. Наиболее эффективно при использовании стратегии by_disk_size. |
Расширение citus поставляется со следующими стратегиями в таблице:
SELECT * FROM pg_dist_rebalance_strategy;
-[ RECORD 1 ]------------------+--------------------------------- name | by_shard_count default_strategy | f shard_cost_function | citus_shard_cost_1 node_capacity_function | citus_node_capacity_1 shard_allowed_on_node_function | citus_shard_allowed_on_node_true default_threshold | 0 minimum_threshold | 0 improvement_threshold | 0 -[ RECORD 2 ]------------------+--------------------------------- name | by_disk_size default_strategy | t shard_cost_function | citus_shard_cost_by_disk_size node_capacity_function | citus_node_capacity_1 shard_allowed_on_node_function | citus_shard_allowed_on_node_true default_threshold | 0.1 minimum_threshold | 0.01 improvement_threshold | 0.5
При использовании стратегии by_shard_count каждому сегменту назначается одинаковая стоимость. Она применяется для выравнивания количества сегментов на всех узлах. Если используется стратегия по умолчанию, by_disk_size, стоимость каждого сегмента равна его размеру на диске в байтах с прибавлением стоимости совмещённых с ним сегментов. Размер диска рассчитывается с помощью функции pg_total_relation_size, поэтому учитывается размер индексов. Цель этой стратегии — равномерное использование дискового пространства на всех узлах. Обратите внимание, что установка порогового значения 0.1 предотвращает ненужное перемещение сегментов, вызванное незначительными отличиями используемого дискового пространства.
Ниже приведены примеры функций, которые можно использовать в новых стратегиях перебалансировки сегментов и зарегистрировать в таблице pg_dist_rebalance_strategy с помощью функции citus_add_rebalance_strategy.
Установка исключения ёмкости узла по шаблону адреса узла:
-- Пример функции node_capacity_function CREATE FUNCTION v2_node_double_capacity(nodeidarg int) RETURNS real AS $$ SELECT (CASE WHEN nodename LIKE '%.v2.worker.citusdata.com' THEN 2.0::float4 ELSE 1.0::float4 END) FROM pg_dist_node where nodeid = nodeidarg $$ LANGUAGE sql;Перебалансировка по количеству запросов, полученных сегментом, согласно таблице citus_stat_statements:
-- Пример функции shard_cost_function CREATE FUNCTION cost_of_shard_by_number_of_queries(shardid bigint) RETURNS real AS $$ SELECT coalesce(sum(calls)::real, 0.001) as shard_total_queries FROM citus_stat_statements WHERE partition_key is not null AND get_shard_id_for_distribution_column('tab', partition_key) = shardid; $$ LANGUAGE sql;Изоляция конкретного сегмента (10000) на узле (адрес '10.0.0.1'):
-- Пример функции shard_allowed_on_node_function CREATE FUNCTION isolate_shard_10000_on_10_0_0_1(shardid bigint, nodeidarg int) RETURNS boolean AS $$ SELECT (CASE WHEN nodename = '10.0.0.1' THEN shardid = 10000 ELSE shardid != 10000 END) FROM pg_dist_node where nodeid = nodeidarg $$ LANGUAGE sql; -- Следующие два определения рекомендуется использовать в сочетании с указанной выше функцией. -- Таким образом, изолированный сегмент не влияет на среднее заполнение узлов. CREATE FUNCTION no_capacity_for_10_0_0_1(nodeidarg int) RETURNS real AS $$ SELECT (CASE WHEN nodename = '10.0.0.1' THEN 0 ELSE 1 END)::real FROM pg_dist_node where nodeid = nodeidarg $$ LANGUAGE sql; CREATE FUNCTION no_cost_for_10000(shardid bigint) RETURNS real AS $$ SELECT (CASE WHEN shardid = 10000 THEN 0 ELSE 1 END)::real $$ LANGUAGE sql;
J.5.8.5.2.1.12. Таблица citus_stat_statements #
В citus реализована таблица citus_stat_statements для сбора статистики выполнения запросов. Она аналогична представлению pg_stat_statements в Postgres Pro, которое отслеживает статистику скорости выполнения запросов, и может быть соединена с ним.
| Имя | Тип | Описание |
|---|---|---|
| queryid | bigint | Идентификатор (эффективен для соединений с pg_stat_statements) |
| userid | oid | Пользователь, выполнивший запрос |
| dbid | oid | Экземпляр БД узла-коррдинатора |
| query | text | Строка анонимизированного запроса |
| executor | text | Исполнитель citus: адаптивный или INSERT-SELECT |
| ключ_секционирования | text | Значение столбца распределения в запросах, выполняемых маршрутизатором, иначе NULL |
| calls | bigint | Количество выполнений запроса |
-- Создание и заполнение распределённой таблицы
create table foo ( id int );
select create_distributed_table('foo', 'id');
insert into foo select generate_series(1,100);
-- Включение сбора статистики
-- Представление pg_stat_statements должно быть указано в shared_preload_libraries
create extension pg_stat_statements;
SELECT count(*) from foo;
SELECT * FROM foo where id = 42;
SELECT * FROM citus_stat_statements;Результат:
-[ RECORD 1 ]-+---------------------------------------------- queryid | -909556869173432820 userid | 10 dbid | 13340 query | insert into foo select generate_series($1,$2) executor | insert-select partition_key | calls | 1 -[ RECORD 2 ]-+---------------------------------------------- queryid | 3919808845681956665 userid | 10 dbid | 13340 query | select count(*) from foo; executor | adaptive partition_key | calls | 1 -[ RECORD 3 ]-+---------------------------------------------- queryid | 5351346905785208738 userid | 10 dbid | 13340 query | select * from foo where id = $1 executor | adaptive partition_key | 42 calls | 1
Ограничения:
Данные статистики не реплицируются и теряются при сбое или отказе базы данных.
Отслеживание ограниченного количества запросов, заданного параметром конфигурации pg_stat_statements.max. Значение по умолчанию —
5000.Для усечения таблицы используется функция citus_stat_statements_reset.
J.5.8.5.2.1.13. Представление citus_stat_tenants #
Представление citus_stat_tenants дополняет таблицу citus_stat_statements информацией о количестве выполняющихся запросов у каждого арендатора. Отслеживание запросов по арендаторам поможет понять, когда лучше выполнить изоляцию арендаторов.
В этом представлении подсчитываются последние одноарендные запросы за выбранный период времени. Количество запросов только для чтения и общее количество запросов за период увеличивается до конца текущего периода. Затем это количество переносится в статистику за последний период, которая хранится до истечения срока действия. Период подсчёта запросов можно задать в секундах с помощью citus.stats_tenants_ period. Значение по умолчанию — 60 секунд.
В представлении отображается до citus.stat_tenants_limit строк (по умолчанию — 100). Учитываются только запросы к одному арендатору, многоарендные запросы игнорируются.
| Имя | Тип | Описание |
|---|---|---|
| nodeid | int | Идентификатор узла из таблицы pg_dist_node |
| colocation_id | int | Идентификатор группы совмещения |
| tenant_attribute | text | Значение в столбце распределения, идентифицирующее арендатора |
| read_count_in_this_period | int | Количество читающих запросов (SELECT) от арендатора за указанный период |
| read_count_in_last_period | int | Количество читающих запросов за предпоследний период времени |
| query_count_in_this_period | int | Количество читающих/пишущих запросов от арендатора за период времени |
| query_count_in_last_period | int | Количество читающих/пишущих запросов за предпоследний период времени |
| cpu_usage_in_this_period | double | Время использования процессора в секундах для данного арендатора за указанный период |
| cpu_usage_in_last_period | double | Время использования процессора в секундах для данного арендатора за последний период |
Отслеживание статистики на уровне арендатора увеличивает издержки и по умолчанию отключено. Чтобы включить его, установите для citus.stat_tenants_track значение 'all'.
В качестве примера предположим, что есть распределённая таблица с именем dist_table со столбцом распределения tenant_id. Затем выполним несколько запросов:
INSERT INTO dist_table(tenant_id) VALUES (1); INSERT INTO dist_table(tenant_id) VALUES (1); INSERT INTO dist_table(tenant_id) VALUES (2); SELECT count(*) FROM dist_table WHERE tenant_id = 1;
В статистике на уровне арендатора будут отражены только что выполненные запросы:
SELECT tenant_attribute, read_count_in_this_period,
query_count_in_this_period, cpu_usage_in_this_period
FROM citus_stat_tenants;tenant_attribute | read_count_in_this_period | query_count_in_this_period | cpu_usage_in_this_period ------------------+---------------------------+----------------------------+-------------------------- 1 | 1 | 3 | 0.000883 2 | 0 | 1 | 0.000144
J.5.8.5.2.1.14. Активность распределённых запросов #
В некоторых ситуациях к запросам могут применяться блокировки на уровне строк в одном из сегментов рабочего узла. В таком случае эти запросы не будут отображаться в представлении pg_locks на узле-координаторе citus.
В citus реализованы специальные представления для наблюдения за запросами и блокировками по всему кластеру, в том числе запросы к конкретным сегментам, которые используются внутри расширения для построения результатов по распределённым запросам.
citus_stat_activityсодержит информацию о распределённых запросах, которые выполняются на всех узлах, и является расширенным вариантом представления pg_stat_activity, доступным везде, где есть последнее.citus_dist_stat_activity— аналогично представлениюcitus_stat_activity, но ограничено только распределёнными запросами без учёта фрагментов запросов citus.citus_lock_waits— заблокированные запросы на уровне кластера.
Первые два представления включают все столбцы pg_stat_activity, а также глобальный PID рабочего узла, инициировавшего запрос.
Рассмотрим в качестве примера подсчёт строк в распределённой таблице:
-- Выполнение за один сеанс -- (с pg_sleep, чтобы всё было видно) SELECT count(*), pg_sleep(3) FROM users_table;
Запрос появляется в citus_dist_stat_activity:
-- Запуск в другом сеансе SELECT * FROM citus_dist_stat_activity; -[ RECORD 1 ]----+------------------------------------------- global_pid | 10000012199 nodeid | 1 is_worker_query | f datid | 13724 datname | postgres pid | 12199 leader_pid | usesysid | 10 usename | postgres application_name | psql client_addr | client_hostname | client_port | -1 backend_start | 2022-03-23 11:30:00.533991-05 xact_start | 2022-03-23 19:35:28.095546-05 query_start | 2022-03-23 19:35:28.095546-05 state_change | 2022-03-23 19:35:28.09564-05 wait_event_type | Timeout wait_event | PgSleep state | active backend_xid | backend_xmin | 777 query_id | query | SELECT count(*), pg_sleep(3) FROM users_table; backend_type | client backend
В представлении citus_dist_stat_activity скрыты внутренние фрагменты запроса citus. Чтобы их увидеть, можно использовать более подробное представление citus_stat_activity. Например, предыдущий запрос count(*) обращается ко всем сегментам. Часть информации находится в сегменте users_table_102039, который виден в запросе ниже.
SELECT * FROM citus_stat_activity; -[ RECORD 1 ]----+----------------------------------------------------------------------- global_pid | 10000012199 nodeid | 1 is_worker_query | f datid | 13724 datname | postgres pid | 12199 leader_pid | usesysid | 10 usename | postgres application_name | psql client_addr | client_hostname | client_port | -1 backend_start | 2022-03-23 11:30:00.533991-05 xact_start | 2022-03-23 19:32:18.260803-05 query_start | 2022-03-23 19:32:18.260803-05 state_change | 2022-03-23 19:32:18.260821-05 wait_event_type | Timeout wait_event | PgSleep state | active backend_xid | backend_xmin | 777 query_id | query | SELECT count(*), pg_sleep(3) FROM users_table; backend_type | client backend -[ RECORD 2 ]----+----------------------------------------------------------------------------------------- global_pid | 10000012199 nodeid | 1 is_worker_query | t datid | 13724 datname | postgres pid | 12725 leader_pid | usesysid | 10 usename | postgres application_name | citus_internal gpid=10000012199 client_addr | 127.0.0.1 client_hostname | client_port | 44106 backend_start | 2022-03-23 19:29:53.377573-05 xact_start | query_start | 2022-03-23 19:32:18.278121-05 state_change | 2022-03-23 19:32:18.278281-05 wait_event_type | Client wait_event | ClientRead state | idle backend_xid | backend_xmin | query_id | query | SELECT count(*) AS count FROM public.users_table_102039 users WHERE true backend_type | client backend
Поле query показывает строки, подсчитываемые в сегменте 102039.
Ниже представлены примеры информативных запросов, которые можно сформулировать с помощью citus_stat_activity:
-- События ожидания активных запросов SELECT query, wait_event_type, wait_event FROM citus_stat_activity WHERE state='active'; -- Первые события ожидания активных запросов SELECT wait_event, wait_event_type, count(*) FROM citus_stat_activity WHERE state='active' GROUP BY wait_event, wait_event_type ORDER BY count(*) desc; -- Общее количество внутренних подключений, созданных для каждого узла в citus SELECT nodeid, count(*) FROM citus_stat_activity WHERE is_worker_query GROUP BY nodeid;
Следующее представление — citus_lock_waits. Чтобы увидеть его работу, можно вручную создать ситуацию с блокировкой. Сначала настройте тестовую таблицу узла-координатора:
CREATE TABLE numbers AS
SELECT i, 0 AS j FROM generate_series(1,10) AS i;
SELECT create_distributed_table('numbers', 'i');Затем с помощью двух сеансов на узле-координаторе запустите такую последовательность операторов:
-- Сеанс 1 -- Сеанс 2
------------------------------------- -------------------------------------
BEGIN;
UPDATE numbers SET j = 2 WHERE i = 1;
BEGIN;
UPDATE numbers SET j = 3 WHERE i = 1;
-- (это вызывает блокировку)В представлении citus_lock_waits содержится информация о данной ситуации.
SELECT * FROM citus_lock_waits; -[ RECORD 1 ]-------------------------+-------------------------------------- waiting_gpid | 10000011981 blocking_gpid | 10000011979 blocked_statement | UPDATE numbers SET j = 3 WHERE i = 1; current_statement_in_blocking_process | UPDATE numbers SET j = 2 WHERE i = 1; waiting_nodeid | 1 blocking_nodeid | 1
В представленном примере запросы выполняются узлом-координатором, но в представлении также могут содержаться блокировки между запросами, выполняемыми рабочими узлами.
J.5.8.5.2.2. Таблицы на всех узлах #
В citus имеются другие информационные таблицы и представления, доступные на всех узлах, а не только на координаторе.
J.5.8.5.2.2.1. Таблица pg_dist_authinfo #
Таблица pg_dist_authinfo содержит параметры аутентификации, используемые узлами citus для подключения друг к другу.
| Имя | Тип | Описание |
|---|---|---|
| nodeid | integer | Идентификатор узла из таблицы pg_dist_node, 0 или -1 |
| rolename | name | Роль Postgres Pro |
| authinfo | text | Разделённые пробелами параметры подключения libpq |
При установке подключения узел проверяет, существует ли в таблице строка с nodeid и желаемым rolename. Если это так, узел включает соответствующую строку authinfo в строку подключения libpq. Типичным примером является сохранение пароля, например 'password=abc123', но можно ознакомиться с полным списком возможностей.
Параметры в authinfo разделяются пробелами и имеют форму key=val. Чтобы записать пустое значение или значение, содержащее пробелы, заключите его в одинарные кавычки, например, keyword='a value'. Одинарные кавычки и обратные косые черты внутри значения должны экранироваться обратной косой чертой, т. е. \' и \\.
Столбец nodeid также может принимать специальные значения 0 — все узлы и -1 — соединения локального замыкания. Если для данного узла существуют как специальные правила, так и правила на уровне узлов, специальные правила имеют приоритет.
SELECT * FROM pg_dist_authinfo;
nodeid | rolename | authinfo
--------+----------+-----------------
123 | jdoe | password=abc123
(1 row)J.5.8.5.2.2.2. Таблица pg_dist_poolinfo #
Если нужно использовать пул соединений для подключения к узлу, можно указать параметры пула с помощью pg_dist_poolinfo. В этой таблице метаданных содержится узел, порт и имя базы данных для citus, которые будут использоваться при подключении к узлу через пул.
Если есть информация о пуле, citus попытается использовать эти значения вместо установки прямого подключения. Информация pg_dist_poolinfo в этом случае заменяет собой pg_dist_node.
| Имя | Тип | Описание |
|---|---|---|
| nodeid | integer | Идентификатор узла из pg_dist_node |
| poolinfo | text | Параметры, разделяемые пробелами: host, port или dbname |
Примечание
В некоторых ситуациях citus игнорирует параметры в pg_dist_poolinfo. Например, перебалансировка сегментов несовместима с пулами соединений, такими как pgbouncer. В таких сценариях citus будет использовать прямое подключение.
-- Подключение к узлу 1 (согласно идентификации в pg_dist_node)
INSERT INTO pg_dist_poolinfo (nodeid, poolinfo)
VALUES (1, 'host=127.0.0.1 port=5433');J.5.8.5.3. Справка по конфигурации #
Существуют различные параметры конфигурации, которые влияют на поведение citus: как стандартные параметры Postgres Pro, так и специальные параметры citus. За подробным описанием параметров конфигурации Postgres Pro обратитесь к главе Настройка сервера.
Оставшаяся часть данной справки посвящена обсуждению особых параметров конфигурации citus. Эти параметры можно задать аналогично параметрам Postgres Pro : с помощью команды SET или изменив файл postgresql.conf.
Например, можно изменить параметр следующим образом:
ALTER DATABASE citus SET citus.multi_task_query_log_level = 'log';
J.5.8.5.3.1. Общая конфигурация #
citus.max_background_task_executors_per_node(integer) #Определяет, сколько фоновых задач может выполняться параллельно в заданный момент, например задач по перемещению сегментов из/в узел. При увеличении значения этого параметра рекомендуется также увеличивать значения параметров
citus.max_background_task_executorsиmax_worker_processes. Минимальное значение —1(по умолчанию), максимальное —128.citus.max_worker_nodes_tracked(integer) #В citus отслеживается расположение рабочих узлов и их членство в общей хеш-таблице на узле-координаторе. Этот параметр конфигурации ограничивает размер хеш-таблицы и, следовательно, количество рабочих узлов, которые можно отслеживать. Значение по умолчанию —
2048. Этот параметр может быть задан только при запуске сервера и относится только к узлу-координатору.citus.use_secondary_nodes(enum) #Устанавливает политику, используемую при выборе узлов для запросов
SELECT. Если установлено значениеalways, планировщик будет отправлять запросы только узлам со значениемsecondaryдляnoderoleв таблице pg_dist_node. Допустимые значения:never— все данные считываются с ведущих узлов. Это значение по умолчанию.always— все данные считываются с ведомых узлов, операторыINSERT/UPDATEотключены.
citus.cluster_name(text) #Сообщает планировщику узла-координатора, какой кластер координировать. После указания
имени_кластерапланировщик будет отправлять запросы рабочим узлам только в этом кластере.citus.enable_version_checks(boolean) #Для обновления версии citus требуется перезагрузить сервер (чтобы получить новую общую библиотеку), а также выполнить команду
ALTER EXTENSION UPDATE. Невыполнение обоих шагов может привести к ошибкам или сбоям. Таким образом citus проверяет соответствие версии кода и версии расширения и выдаёт ошибку, если они не совпадают.Значение по умолчанию —
true. Этот параметр относится только к узлу-координатору. В редких случаях для сложных процессов обновления требуется установить для этого параметра значениеfalse, чтобы отключить проверку.citus.log_distributed_deadlock_detection(boolean) #Указывает, вносить ли в журнал сервера операции, связанные с обнаружением распределённых взаимоблокировок. Значение по умолчанию —
false.citus.distributed_deadlock_detection_factor(floating point) #Устанавливает время ожидания перед проверкой распределённых взаимоблокировок. В частности, время ожидания будет равно этому значению, умноженному на значение, установленное в параметре Postgres Pro deadlock_timeout. Значение по умолчанию —
2. Значение-1отключает обнаружение распределённых взаимоблокировок.citus.node_connection_timeout(integer) #Указывает максимальное время ожидания для установки подключения в миллисекундах. Если время ожидания истечёт до того, как будет установлено хотя бы одно подключение к рабочему узлу, citus выдаст ошибку. Этот параметр конфигурации влияет на подключения узла-координатора к рабочим узлам и рабочих узлов друг к другу. Минимальное значение —
10миллисекунд, максимальное значение —1час. Значение по умолчанию —30секунд.Ниже показано, как задавать этот параметр:
-- Установить значение 60 секунд ALTER DATABASE foo SET citus.node_connection_timeout = 60000;
citus.node_conninfo(text) #Устанавливает неконфиденциальные параметры подключения libpq, используемые для всех межузловых соединений.
Ниже показано, как задавать этот параметр:
-- пары ключ=значение, разделённые запятыми. -- Например, параметры ssl: ALTER DATABASE foo SET citus.node_conninfo = 'sslrootcert=/path/to/citus.crt sslmode=verify-full';
В расширении citus поддерживается только определённое подмножество допустимых параметров, а именно:
connect_timeoutgsslib(при наличии дополнительной функциональности Postgres Pro во время выполнения)hostkeepaliveskeepalives_countkeepalives_idlekeepalives_intervalkrbsrvname(при наличии дополнительной функциональности Postgres Pro во время выполнения)sslcompressionsslcrlsslmode(значение по умолчанию —require)sslnegotiationsslrootcerttcp_user_timeout
Параметр конфигурации
citus.node_conninfoприменяется только к новым подключениям. Чтобы все подключения использовали новое значение, нужно обязательно перезагрузить конфигурацию Postgres Pro:SELECT pg_reload_conf();
citus.local_hostname(text) #Узлам citus время от времени необходимо подключаться к самим себе для выполнения системных операций. По умолчанию они используют адрес
localhost, чтобы ссылаться на себя, что может приводить к проблемам. Например, если узел требуетsslmode=verify-fullдля входящих подключений, добавлениеlocalhostв качестве альтернативного адреса узла в сертификате SSL не всегда допустимо или возможно.Параметр конфигурации
citus.local_hostnameвыбирает адрес узла, который используется узлом для подключения к самому себе. Значение по умолчанию —localhost.Ниже показано, как задавать этот параметр:
ALTER SYSTEM SET citus.local_hostname TO 'mynode.example.com';
citus.show_shards_for_app_name_prefixes(text) #По умолчанию в citus скрываются сегменты из списка таблиц, которые Postgres Pro предоставляет SQL-клиентам. Это происходит потому, что каждая распределённая таблица состоит из нескольких сегментов, и эти сегменты могут отвлекать SQL-клиента.
Параметр конфигурации
citus.show_shards_for_app_name_prefixesпозволяет отображать сегменты для выбранных клиентов. Значение по умолчанию —''.Ниже показано, как задавать этот параметр:
-- Показывать сегменты только для psql (скрывать для прочих клиентов, например pgAdmin) SET citus.show_shards_for_app_name_prefixes TO 'psql'; -- Также принимает список, разделённый запятыми SET citus.show_shards_for_app_name_prefixes TO 'psql,pg_dump';
citus.rebalancer_by_disk_size_base_cost(integer) #При использовании стратегии перебалансировки
by_disk_sizeкаждая группа сегментов получит эту стоимость в байтах, прибавленную к её фактическому размеру на диске. Эта стратегия используется, чтобы избежать дисбаланса в случаях, когда некоторые сегменты содержат очень мало данных. Предполагается, что даже пустые сегменты имеют некоторую стоимость из-за распараллеливания и потому, что стоимость групп пустых сегментов будет расти в будущем. Значение по умолчанию —100МБ.
J.5.8.5.3.2. Статистика запросов #
citus.stat_statements_purge_interval(integer) #Устанавливает частоту, с которой демон обслуживания удаляет из таблицы citus_stat_statements записи, для которых нет совпадений в таблице pg_stat_statements. Этот параметр конфигурации задаёт временной интервал между очистками в секундах. Значение по умолчанию —
10. Значение0отключает очистку. Этот параметр относится только к узлу-координатору и может быть изменён во время выполнения.Ниже показано, как задавать этот параметр:
SET citus.stat_statements_purge_interval TO 5;
citus.stat_statements_max(integer) #Максимальное количество строк для хранения в таблице citus_stat_statements. Значение по умолчанию —
50000— может быть изменено на любое значение в диапазоне от1000до10 000 000. Обратите внимание, что для каждой строки требуется 140 байт, поэтому при установке дляcitus.stat_statements_maxмаксимального значения 10 МБ потребуется 1,4 ГБ на диске.Изменение этого параметра конфигурации вступит в силу только после перезапуска Postgres Pro.
citus.stat_statements_track(enum) #Запись статистики для citus_stat_statements требует дополнительных ресурсов ЦП. Когда нагрузка на базу данных увеличивается, администратор может отключить отслеживание операторов. Включать и отключать отслеживание можно с помощью параметра конфигурации
citus.stat_statements_track. Допустимые значения:all— отслеживать все операторы.none— отключить отслеживание. Это значение по умолчанию.
citus.stat_tenants_untracked_sample_rate(floating point) #Частота выборки для новых арендаторов в представлении citus_stat_tenants. Частота может находиться в диапазоне от
0.0до1.0. Значение по умолчанию —1.0, то есть проводится выборка 100% запросов неотслеживаемых арендаторов. Установка более низкого значения означает, что проводится выборка 100% запросов отслеживаемых арендаторов, а запросы неотслеживаемых арендаторов выбираются только с указанной частотой.
J.5.8.5.3.3. Загрузка данных #
citus.shard_count(integer) #Устанавливает количество сегментов для разделённых по хешу таблиц. Значение по умолчанию —
32. Это значение используется функцией create_distributed_table при создании таблиц, разделённых по хешу. Этот параметр можно установить во время выполнения и он относится только к узлу-координатору.citus.metadata_sync_mode(enum) #Примечание
Для изменения этого параметра конфигурации требуются права суперпользователя.
Этот параметр конфигурации определяет, как citus синхронизирует метаданные между узлами. По умолчанию citus обновляет все метаданные за одну транзакцию для обеспечения согласованности. Однако в Postgres Pro есть жёсткие ограничения объёма памяти, связанные с аннулированием кеша, и синхронизация метаданных citus для большого кластера может завершиться сбоем из-за нехватки памяти.
В качестве обходного пути в citus реализован дополнительный нетранзакционный режим синхронизации, в котором используется серия небольших транзакций. Хотя этот режим работает с ограниченным объёмом памяти, существует вероятность сбоя транзакций и несогласованного состояния метаданных. Чтобы решить эту потенциальную проблему, нетранзакционная синхронизация метаданных разработана как идемпотентное действие, поэтому при необходимости её можно перезапустить.
Допустимы следующие значения этих параметров конфигурации:
transactional— синхронизировать все метаданные в одной транзакции. Это значение по умолчанию.nontransactional— синхронизировать метаданные с помощью нескольких небольших транзакций.
Ниже показано, как задавать этот параметр:
-- Добавление нового узла и нетранзакционная синхронизация SET citus.metadata_sync_mode TO 'nontransactional'; SELECT citus_add_node(<ip>, <port>); -- Ручная (ре)синхронизация SET citus.metadata_sync_mode TO 'nontransactional'; SELECT start_metadata_sync_to_all_nodes();
Рекомендуется сначала использовать транзакционный режим и переключаться на нетранзакционный только в случае сбоя памяти.
J.5.8.5.3.4. Конфигурация планировщика #
citus.local_table_join_policy(enum) #Определяет, как citus перемещает данные при соединении локальных и распределённых таблиц. Настройка политики соединения поможет уменьшить объём данных, передаваемых между рабочими узлами.
В citus по мере необходимости узлам отправляются либо локальные, либо распределённые таблицы для выполнения соединения. Копирование данных таблицы называется «преобразованием». При преобразовании локальной таблицы она отправляется всем рабочим узлам, которым нужны её данные для выполнения соединения. При преобразовании распределённой таблицы она собирается на узле-координаторе для выполнения соединения. Планировщик citus отправляет только строки, необходимые для выполнения преобразования.
Есть четыре режима настройки преобразования:
auto— citus преобразует либо все локальные, либо все распределённые таблицы для поддержки соединений локальных и распределённых таблиц. Решение о том, какие именно таблицы преобразовывать, принимается эвристически. Распределённые таблицы преобразуются, если они соединяются с использованием постоянного фильтра по уникальному индексу (например, первичному ключу). Таким образом между рабочими узлами будет перемещаться меньше данных. Это значение по умолчанию.never— запрещает citus соединение локальных и распределённых таблиц.prefer-local— предпочтительно преобразование локальных таблиц для выполнения соединения локальных и распределённых таблиц.prefer-distributed— предпочтительно преобразование распределённых таблиц для выполнения соединения локальных и распределённых таблиц. При наличии распределённых таблиц большого размера использование этого режима может привести к перемещению больших объёмов данных между рабочими узлами.
В качестве примера предположим, что
citus_table— это таблица, распределённая по столбцуx, аpostgres_table— локальная таблица:CREATE TABLE citus_table(x int primary key, y int); SELECT create_distributed_table('citus_table', 'x'); CREATE TABLE postgres_table(x int, y int); -- Хотя соединение выполняется по первичному ключу, отсутствует постоянный фильтр, -- поэтому postgres_table отправляется рабочим узлам для выполнения соединения SELECT * FROM citus_table JOIN postgres_table USING (x); -- Есть постоянный фильтр по первичному ключу, поэтому отфильтрованная строка -- из распределённой таблицы забирается на узел-координатор для выполнения соединения SELECT * FROM citus_table JOIN postgres_table USING (x) WHERE citus_table.x = 10; SET citus.local_table_join_policy to 'prefer-distributed'; -- Поскольку стоит настройка для распределённых таблиц, citus_table забирается на узел-координатор -- для выполнения соединения. Учтите, что citus_table может быть огромной. SELECT * FROM citus_table JOIN postgres_table USING (x); SET citus.local_table_join_policy to 'prefer-local'; -- Хотя задан постоянный фильтр по первичному ключу для citus_table, -- postgres_table будет отправляться нужным рабочим узлам, так как используется 'prefer-local'. SELECT * FROM citus_table JOIN postgres_table USING (x) WHERE citus_table.x = 10;citus.limit_clause_row_fetch_count(integer) #Устанавливает количество строк, извлекаемых для каждой задачи в целях оптимизации ограничительных предложений. В некоторых случаях для запросов
SELECTс предложениямиLIMITможет потребоваться извлечение всех строк из каждой задачи для получения результатов. В таких случаях, а также когда осмысленные результаты могут быть получены с помощью приближения, этот параметр определяет количество строк, извлекаемых из каждого сегмента. По умолчанию приближения для LIMIT отключены, и для этого параметра установлено значение-1. Это значение может быть установлено во время выполнения и относится только к узлу-координатору.citus.count_distinct_error_rate(floating point) #В citus приближения
count(distinct)могут вычисляться с помощью расширения Postgres Pro hll. В этом параметре конфигурации устанавливается желаемая частота ошибок при вычисленииcount(distinct): значение0.0(по умолчанию) отключает приближение дляcount(distinct), а1.0не даёт никаких гарантий точности результатов. Для достижения наилучших результатов рекомендуется установить для этого параметра значение0.005. Это значение может быть установлено во время выполнения и относится только к узлу-координатору.citus.task_assignment_policy(enum) #Примечание
Этот параметр конфигурации применяется для запросов к таблицам-справочникам.
Задаёт политику, которая будет использоваться при назначении задач рабочим узлам. Узел-координатор назначает задачи рабочим узлам в зависимости от расположения сегментов. Этот параметр конфигурации определяет политику, которая будет использоваться при выполнении таких назначений. На данный момент доступны три политики назначения задач:
greedyиспользуется для равномерного распределения задач между рабочими узлами. Это значение по умолчанию.round-robin— задачи рабочим узлам назначаются методомround-robin, чередуя разные реплики. При этом обеспечивается оптимальное использование кластера в случае, когда количество сегментов таблицы значительно меньше количества рабочих узлов.first-replicaраспределяет задачи в порядке вставки размещений (реплик) для сегментов. Другими словами, запрос-фрагмент для сегмента просто назначается рабочему узлу, у которого есть первая реплика этого сегмента. Этот метод даёт гарантию, что определённые сегменты будут использоваться на определённых узлах (т. е. более строгие гарантии сохранения объёма памяти).
Этот параметр конфигурации может быть установлен во время выполнения и относится только к узлу- координатору.
citus.enable_non_colocated_router_query_pushdown(boolean) #Включает планировщик маршрутизатора для запросов, которые ссылаются на несовмещённые распределённые таблицы.
Обычно планировщик маршрутизатора включается только для запросов, которые ссылаются на совмещённые распределённые таблицы, поскольку целевые сегменты не всегда находятся на одном и том же узле, например, после перебалансировки сегментов. Поэтому, хотя включение этого флага в некоторой степени оптимизирует запросы, которые ссылаются на несовмещённые распределённые таблицы, не гарантируется, что тот же запрос сможет быть выполнен после перебалансировки сегментов или изменения количества сегментов одной из этих распределённых таблиц. Значение по умолчанию —
off.
J.5.8.5.3.5. Промежуточная передача данных #
citus.max_intermediate_result_size(integer) #Максимальный размер промежуточных результатов в КБ для CTE, которые невозможно передать на рабочие узлы для выполнения, а также для сложных подзапросов. По умолчанию используется
1ГБ, а значение-1означает отсутствие ограничений. Запросы, превышающие лимит, будут отменены с выводом сообщения об ошибке.
J.5.8.5.3.6. DDL #
citus.enable_ddl_propagation(boolean) #Указывает, следует ли автоматически транслировать изменения DDL с узла-координатора на все рабочие узлы. Значение по умолчанию —
true. Поскольку некоторые изменения схемы требуют исключительной блокировки доступа к таблицам, а автоматическая трансляция применяется ко всем рабочим узлам последовательно, это может временно снизить отзывчивость кластера citus. Можно отключить этот параметр и транслировать изменения вручную.Примечание
Список поддерживаемых для трансляции DDL-запросов представлен в разделе Изменение таблиц.
citus.enable_local_reference_table_foreign_keys(boolean) #Позволяет создавать внешние ключи между таблицами-справочниками и локальными таблицами. Чтобы эта функциональность работала, узел-координатор тоже должен быть зарегистрирован с помощью функции citus_add_node. Значение по умолчанию —
true.Обратите внимание, что внешние ключи между таблицами-справочниками и локальными таблицами имеют некоторую стоимость. При создании внешнего ключа citus должен добавить обычную таблицу к метаданным и отслеживать её в таблице pg_dist_partition. Локальные таблицы, добавляемые в метаданные, наследуют те же ограничения, что и таблицы-справочники (см. разделы Создание и изменение распределённых объектов (DDL) и Поддержка SQL и обходные решения).
При удалении внешних ключей citus автоматически удалит такие локальные таблицы из метаданных, поэтому для них отсутствуют такие ограничения.
citus.enable_change_data_capture(boolean) #Заставляет citus изменить модули логического декодирования wal2json и pgoutput для работы с распределёнными таблицами. В частности, имена сегментов (например,
foo_102027) в выходных данных модуля декодирования меняются на базовые имена распределённых таблиц (например,foo). Это также позволяет избежать публикации повторяющихся событий во время изоляции арендатора и операций разделения/перемещения/перебалансировки сегментов. Значение по умолчанию —false.citus.enable_schema_based_sharding(boolean) #Если для параметра установлено значение
ON, все созданные схемы будут распределяться по умолчанию. Распределённые схемы автоматически связываются с отдельными группами совмещения, так что таблицы, созданные в этих схемах, будут автоматически преобразованы в совмещённые распределённые таблицы без ключа сегментирования. Этот параметр можно изменять для отдельных сеансов.Чтобы узнать, как использовать этот параметр конфигурации, обратитесь к подразделу Микросервисы.
J.5.8.5.3.7. Конфигурация исполнителя #
citus.all_modifications_commutative(boolean) #В citus применяются правила независимости от порядка исполнения и запрашиваются соответствующие блокировки для операций изменения, чтобы гарантировать корректность поведения. Например, предполагается, что оператор
INSERTне зависит от порядка исполнения другого оператораINSERT, но зависит от порядка операторовUPDATEилиDELETE. Аналогичным образом предполагается, что операторыUPDATEилиDELETEзависят от порядка исполнения других операторовUPDATEилиDELETE. Это означает, что для операторовUPDATEиDELETEтребуется, чтобы citus запрашивал более сильные блокировки.Если есть операторы
UPDATE, которые не зависят от порядка исполнения операторовINSERTили других операторовUPDATE, можно ослабить эти предположения независимости от порядка исполнения, установив для этого параметра значениеtrue. При значенииtrue, все команды считаются независимыми от порядка исполнения и требующими общей блокировки, что может повысить общую пропускную способность. Этот параметр можно установить во время выполнения, и он относится только к узлу-координатору.citus.multi_task_query_log_level(enum) #Устанавливает уровень записи в журнал для любого запроса, который генерирует более одной задачи (т. е. затрагивает более одного сегмента). Этот параметр полезен во время миграции многоарендного приложения, так как можно выбрать сообщение об ошибке или предупреждение о таких запросах, чтобы найти их и добавить к ним фильтр
tenant_id. Этот параметр можно установить во время выполнения и он относится только к узлу-координатору. Значение по умолчанию —off. Допустимы следующие значения:off— отключает запись в журнал любых запросов, которые генерируют несколько задач (т. е. охватывают несколько сегментов).debug— записывает в журнал операторы на уровне безопасностиDEBUG.log— записывает в журнал операторы на уровне безопасностиLOG. Строка в журнале будет включать выполненный SQL-запрос.notice— записывает в журнал операторы на уровне безопасностиNOTICE.warning— записывает в журнал операторы на уровне безопасностиWARNING.error— записывает в журнал операторы на уровне безопасностиERROR.
Обратите внимание, что уровень
errorможет быть полезен во время тестирования разработки, а более низкий уровень журнала, напримерlog, — в производственной среде. При выборе уровняlogв журнал БД будут записываться многозадачные запросы, которые будут показываться послеSTATEMENT.LOG: multi-task query about to be executed HINT: Queries are split to multiple tasks if they have to be split into several queries on the workers. STATEMENT: SELECT * FROM foo;
citus.propagate_set_commands(enum) #Определяет, какие команды
SETтранслируются рабочим узлам с узла-координатора. Значение по умолчанию —none. Допустимы следующие значения:none— командыSETне транслируются.local— транслируются только командыSET LOCAL.
citus.enable_repartition_joins(boolean) #Обычно попытка выполнить соединения с пересекционированием с помощью адаптивного исполнителя завершается ошибкой с выводом соответствующего сообщения. Но если установить для этого параметра конфигурации значение
true, citus выполнит соединение. Значение по умолчанию —false.citus.enable_repartitioned_insert_select(boolean) #По умолчанию оператор
INSERT INTO… SELECT, который не может быть вынесен наружу, попытается пересекционировать строки из оператораSELECTи передать их рабочим узлам для вставки. Однако если в целевой таблице слишком много сегментов, пересекционирование не будет выполнено должным образом. Издержки на обработку интервалов сегментов при секционировании результатов слишком велики. Пересекционирование можно отключить вручную, установив для этого параметра конфигурации значениеfalse.citus.enable_binary_protocol(boolean) #Установка для этого параметра значения
trueуказывает узлу-координатору использовать формат двоичной сериализации Postgres Pro (если возможно) для передачи данных рабочим узлам. Некоторые типы столбцов не поддерживают двоичную сериализацию.Включение этого параметра наиболее полезно, когда рабочие узлы должны возвращать большие объёмы данных. Примеры: запрашивается много строк, в строках много столбцов или используются типы больших данных, такие как
hllиз расширения hll.Значение по умолчанию —
true. Если установлено значениеfalse, все результаты кодируются и передаются в текстовом формате.citus.max_shared_pool_size(integer) #Указывает максимальное разрешённое количество подключений, которые узел-координатор может устанавливать во всех одновременных сеансах для каждого рабочего узла. Postgres Pro должен выделять фиксированное количество ресурсов для каждого подключения, и этот параметр конфигурации помогает снизить нагрузку на рабочие узлы, связанную с подключением.
Без ограничения скорости подключений каждый запрос с несколькими сегментами создаёт подключения для каждого рабочего узла пропорционально количеству сегментов, к которым он обращается (в частности, до значения
#shards/#workers). Одновременное выполнение десятков многосегментных запросов может легко превысить ограничение max_connections рабочих узлов, что приведёт к сбою.По умолчанию это значение равно значению
max_connectionsна узле-координаторе, которое может не совпадать со значением на рабочих узлах (см. примечание ниже). Значение-1отключает ограничение скорости.Примечание
Существуют определённые операции, которые не подчиняются этому параметру, в первую очередь соединения с пересекционированием. По этой причине разумно сделать значение
max_connectionsдля рабочих узлов немного больше, чем для узла-координатора. Таким образом будет предоставлено дополнительное пространство для подключений, необходимых для запросов с пересекционированием на рабочих узлах.citus.max_adaptive_executor_pool_size(integer) #В отличие от параметра citus.max_shared_pool_size, ограничивающего подключения рабочих узлов во всех сеансах,
citus.max_adaptive_executor_pool_sizeограничивает подключения рабочих узлов только в текущем сеансе. Этот параметр позволяет:Предотвратить получение одним сервером ресурсов всех рабочих узлов.
Обеспечить управление приоритетами: назначать низкий приоритет сеансам с меньшим значением
citus.max_adaptive_executor_pool_sizeи высокий приоритет сеансам с большими значениями.
Значение по умолчанию —
16.citus.executor_slow_start_interval(integer) #Время ожидания между открытием подключений к одному и тому же рабочему узлу в миллисекундах.
Отдельные задачи многосегментного запроса, занимающие очень мало времени, можно выполнить с помощью одного (зачастую уже кешированного) подключения. Чтобы избежать создания лишних подключений, исполнитель ожидает заданное количество миллисекунд. По истечении этого времени увеличивается количество допустимых для следующего раза подключений.
Для длительных запросов (которые занимают более
500мс) медленный запуск может увеличить задержку, но выполнение небольших запросов будет быстрее. Значение по умолчанию —10мс.citus.max_cached_conns_per_worker(integer) #Каждый сервер создаёт подключения к рабочим узлам для выполнения запросов к сегментам. В конце транзакции настроенное количество подключений остаётся открытым для ускорения последующих команд. Увеличение этого значения уменьшит задержку многосегментных запросов, но также увеличит издержки на рабочих узлах.
Значение по умолчанию —
1. Значение2может быть полезно для кластеров с небольшим количеством одновременных сеансов, но указывать большее значение не рекомендуется (16будет слишком большим).citus.force_max_query_parallelization(boolean) #Имитирует устаревший и ныне несуществующий исполнитель реального времени и используется для создания как можно большего количества подключений, чтобы максимизировать распараллеливание запросов.
Когда этот параметр конфигурации включён, citus заставит адаптивный исполнитель использовать как можно больше подключений при выполнении параллельных распределённых запросов. Если этот параметр отключён, исполнитель сможет использовать меньшее количество подключений для оптимизации общей пропускной способности выполнения запроса. Если на внутреннем уровне для этого параметра установить значение
true, для каждой задачи будет использовано одно подключение. Значение по умолчанию —false.Единственное место, где это применимо, — транзакция с небольшим первым запросом, требующим небольшого количества подключений, и последующими запросами, требующими большего количества подключений. В citus решение о количестве используемых в транзакции подключений принимается на основе первого оператора, который может ограничивать скорость других запросов, если не используется параметр конфигурации для предоставления указаний.
Ниже показано, как задавать этот параметр:
BEGIN; --Добавьте следующее указание SET citus.force_max_query_parallelization TO ON; -- Небольшой запрос, для которого не требуется много подключений SELECT count(*) FROM table WHERE filter = x; -- Запрос, который выполняется быстрее с большим количеством подключений и -- может получить их, поскольку выше было включено максимальное распараллеливание SELECT ... very .. complex .. SQL; COMMIT;
citus.explain_all_tasks(boolean) #По умолчанию в citus показывается вывод одной произвольной задачи при выполнении команды
EXPLAINв распределённом запросе. В большинстве случаев выводEXPLAINбудет одинаковым для разных задач. Но некоторые задачи планируются по-разному или выполняются гораздо дольше. В таких случаях может быть полезно включить этот параметр, после чего выводEXPLAINбудет включать все задачи. При этом командаEXPLAINможет выполняться значительно дольше.citus.explain_analyze_sort_method(enum) #Определяет метод сортировки задач в выводе команды
EXPLAIN ANALYZE. Допустимы следующие значения:execution-time— сортировать по времени выполнения.taskId— сортировать по идентификатору задачи.
J.5.9. Администрирование #
J.5.9.1. Управление кластером #
В этом разделе описано добавление и удаление узлов из кластера citus, а также возможные действия в случае сбоев узлов.
Примечание
Чтобы упростить перемещение сегментов между узлами или повторную репликацию сегментов на вышедших из строя узлах, citus поддерживает полную перебалансировку сегментов. В следующих разделах дано краткое описание функций, предоставляемых перебалансировщиком сегментов при необходимости. За дополнительной информацией об этих функциях, их аргументах и использовании обратитесь к разделу Функции для управления и восстановления кластера.
J.5.9.1.1. Выбор размера кластера #
В этом разделе рассматриваются параметры конфигурации для запуска кластера в производственной среде.
J.5.9.1.1.1. Выбор количества сегментов #
При выборе количества сегментов для каждой распределённой таблицы важно соблюдать баланс между количеством сегментов и издержками на планирование и выполнение запросов для них. Чтобы изменить количество сегментов таблицы после распределения, можно использовать функцию alter_distributed_table.
J.5.9.1.1.1.1. Сценарий использования многоарендной БД SaaS #
Оптимальный выбор зависит от пользовательских шаблонов обращения к данным. Например, в случае с многоарендной базой данных SaaS рекомендуется выбирать от 32 до 128 сегментов. При небольшой нагрузке, например менее 100 ГБ, можно начать с 32 сегментов, а при большей — выбрать 64 или 128 сегментов. То есть существует возможность масштабирования с 32 до 128 рабочих узлов.
J.5.9.1.1.1.2. Сценарий использования для анализа данных в реальном времени #
В сценарии использования аналитики в реальном времени количество сегментов должно быть связано с общим количеством ядер рабочих узлов. Для максимального распараллеливания следует создать достаточное количество сегментов на каждом узле, чтобы на каждое ядро ЦП приходился хотя бы один сегмент. Обычно рекомендуется создать большое количество начальных сегментов, например в 2 или 4 раза больше, чем текущее количество ЦП. Это позволит впоследствии провести масштабирование, если будет добавлено больше рабочих узлов и ядер ЦП.
Однако имейте в виду, что для каждого запроса citus открывает одно подключение к базе данных на каждый сегмент, и количество подключений ограничено. Внимательно следите за тем, чтобы количество сегментов оставалось небольшим, чтобы распределённым запросам не приходилось часто ждать подключения. Другими словами, необходимое количество подключений, (max concurrent queries * shard count), как правило, не должно превышать общее количество возможных подключений в системе, (number of workers * max_connections per worker).
J.5.9.1.2. Начальный размер кластера #
Размер кластера с точки зрения количества узлов и их аппаратной мощности можно легко изменить. Однако всё равно нужно выбрать начальный размер нового кластера. Далее приведены несколько советов по выбору начального размера кластера.
J.5.9.1.2.1. Сценарий использования многоарендной БД SaaS #
При переходе на citus из существующего экземпляра базы данных с одним узлом рекомендуется выбрать кластер, в котором общее количество ядер рабочих узлов и оперативной памяти соответствует характеристикам исходного экземпляра. В таких сценариях производительность повышалась в 2–3 раза, поскольку сегментирование оптимизировало использование ресурсов и позволяло строить индексы меньшего размера и т. д.
Узлу-координатору требуется меньше памяти, чем рабочим узлам, поэтому в качестве координатора можно выбрать машину, оптимизированную для вычислений. Требуемое количество ядер зависит от существующей нагрузки (пропускной способности записи/чтения).
J.5.9.1.2.2. Сценарий использования для анализа данных в реальном времени #
Всего ядер:, когда рабочие данные помещаются в ОЗУ, можно ожидать линейного улучшения производительности в citus, пропорционально количеству рабочих ядер. Чтобы определить правильное количество ядер для конкретных задач, сравните текущую скорость выполнения запросов в базе данных с одним узлом и необходимую скорость в citus. Разделите текущую скорость на желаемую скорость и округлите результат.
ОЗУ рабочего узла: в идеальном случае будет предоставляться достаточно памяти для размещения большей части рабочего набора. Тип запросов, которые используется в пользовательском приложении, влияет на требования к памяти. Чтобы определить, сколько памяти требуется запросу, можно выполнить для него EXPLAIN ANALYZE.
J.5.9.1.3. Масштабирование кластера #
Архитектура citus на основе логического сегментирования позволяет масштабировать кластер без простоев. В этом разделе описывается добавление новых узлов в кластер citus, чтобы повысить производительность и масштабируемость запросов.
J.5.9.1.3.1. Добавление рабочего узла #
В citus все данные распределённых таблиц хранятся на рабочих узлах. Поэтому масштабировать кластер за счёт увеличения вычислительной мощности можно с помощью добавления нового рабочего узла.
Чтобы добавить новый узел в кластер, сначала необходимо добавить DNS-имя или IP-адрес этого узла и порт (на котором работает Postgres Pro) в таблицу каталога pg_dist_node. Для этого можно использовать функцию citus_add_node:
SELECT * from citus_add_node('node-name', 5432);Новый узел доступен для сегментов новых распределённых таблиц. Расположение существующих сегментов не изменится, если их не перераспределить, поэтому для повышения производительности при добавлении нового рабочего узла потребуются дополнительные шаги.
Примечание
Кроме того, новые узлы синхронизируют метаданные citus при создании. По умолчанию синхронизация происходит внутри одной транзакции для обеспечения согласованности. Однако в большом кластере с большим объёмом метаданных транзакция может исчерпать память и завершиться ошибкой. Если возникла такая ситуация, можно выбрать нетранзакционный режим синхронизации метаданных с помощью параметра конфигурации citus.metadata_sync_mode.
J.5.9.1.3.2. Перебалансировка сегментов без простоя #
Для перемещения существующих сегментов на добавленный рабочий узел в citus существует функция citus_rebalance_start. Эта функция равномерно распределяет сегменты между рабочими узлами.
Эта функция служит для перебалансировки сегментов согласно одной из стратегий для лучшего соответствия нагрузке пользовательской базы данных. Для выбора подходящей стратегии обратитесь к описанию этой функции. Вот пример перебалансировки сегментов с использованием стратегии по умолчанию:
SELECT citus_rebalance_start();
Многие продукты, такие как многоарендные SaaS приложения, не допускают простоев, и перебалансировка помогает их избежать. То есть чтение и запись из приложения могут продолжаться с минимальными перерывами во время перемещения данных.
J.5.9.1.3.2.1. Параллельная перебалансировка #
По умолчанию эта операция последовательно выполняет несколько перемещений сегментов. В некоторых случаях можно выполнить более быструю перебалансировку за счёт использования большего количества ресурсов, таких как пропускная способность сети. В таких ситуациях пользователи могут настроить операцию перебалансировки для параллельного выполнения нескольких перемещений сегментов.
Параметр конфигурации citus.max_background_task_executors_per_node позволяет выполнять перебалансировку сегментов и прочие схожие задачи параллельно. Для повышения скорости распараллеливания можно увеличить его значение, которое по умолчанию равно 1.
ALTER SYSTEM SET citus.max_background_task_executors_per_node = 2; SELECT pg_reload_conf(); SELECT citus_rebalance_start();
Типичные сценарии использования
Ускорение масштабирования при добавлении новых узлов в кластер.
Ускорение перебалансировки кластера для равномерного использования узлов.
Особые случаи и нетривиальные проблемы
Параметр конфигурации citus.max_background_task_executors_per_node ограничивает количество параллельных исполнителей задач в целом. Кроме того, сегменты в одной и той же группе совмещения всегда будут перемещаться последовательно, поэтому распараллеливание может быть ограничено количеством групп совмещения.
J.5.9.1.3.2.2. Как это работает #
При перебалансировке сегментов в citus используется логическая репликация Postgres Pro для перемещения данных из старого сегмента (называемого «публикующим» в терминах репликации) в новый («подписчика»). Логическая репликация позволяет приложениям непрерывно выполнять чтение и запись при копировании данных сегментов. В citus запись в сегменте блокируется только на короткое время, необходимое для обновления метаданных, чтобы повысить сегмент подписчика до активного.
Согласно документации Postgres Pro для источника необходимо выбрать вариант идентификации реплики:
Опубликованная таблица должна иметь выбранный «вариант идентификации реплики» для репликации операций UPDATE и DELETE и возможности идентификации на стороне подписчика соответствующих изменяемых или удаляемых строк. По умолчанию это первичный ключ, если он есть. Другой уникальный индекс (с некоторыми дополнительными требованиями) также может быть установлен в качестве варианта идентификации реплики.
Другими словами, если в распределённой таблице задан первичный ключ, она готова к перебалансировке сегментов без каких-либо дополнительных шагов. Но если у неё нет первичного ключа или явно выбранного идентификатора реплики, попытка перебалансировки приведёт к ошибке. Это можно исправить следующим образом.
Есть ли у таблицы уникальный индекс?
Если у таблицы, которую нужно реплицировать, уже есть уникальный индекс, включающий столбец распределения, выберите этот индекс в качестве варианта идентификации реплики:
-- Допустим у my_table есть уникальный индекс my_table_idx, -- включающий столбец распределения ALTER TABLE my_table REPLICA IDENTITY USING INDEX my_table_idx;
Примечание
Можно использовать REPLICA IDENTITY USING INDEX, но не рекомендуется добавлять таблице REPLICA IDENTITY FULL. При использовании этого параметра каждая команда UPDATE/DELETE будет выполнять полное сканирование таблицы на стороне подписчика, чтобы найти кортеж с этими строками. В ходе тестирования обнаружено, что это приводит к ухудшению производительности даже по сравнению с четвёртым решением, приведённым ниже.
Можно ли добавлять первичный ключ в других случаях?
Добавьте первичный ключ для таблицы. Если нужным ключом является столбец распределения, просто добавьте ограничение. В противном случае первичный ключ со столбцом, не относящимся к распределению, должен быть составным и содержать также столбец распределения.
J.5.9.1.3.3. Добавление узла-координатора #
Координатор citus хранит только метаданные о сегментах таблицы и не хранит никаких данных. Это означает, что все вычисления передаются рабочим узлам, а координатор выполняет только окончательное агрегирование результатов от рабочих узлов. Поэтому маловероятно, что координатор станет узким местом для производительности чтения. Кроме того, координатор легко усовершенствовать, переместив на более мощную машину.
Однако в некоторых сценариях использования с большим объёмом записи, когда координатор становится узким местом в производительности, можно добавить ещё один узел, как показано ниже, и сбалансировать нагрузку клиентских подключений.
SELECT * FROM citus_add_node(second_coordinator_hostname, second_coordinator_port); SELECT * FROM citus_set_node_property(second_coordinator_hostname, second_coordinator_port, 'shouldhaveshards', false);
Примечание
DDL-запросы можно выполнять только через первый узел-координатор.
J.5.9.1.4. Сбои узлов #
В этом подразделе рассматриваются действия, которые можно предпринять при сбоях узлов, не прерывая работу кластера citus.
J.5.9.1.4.1. Сбои рабочих узлов #
В citus используется потоковая репликация Postgres Pro, что позволяет выдерживать сбои рабочих узлов. При этом варианте рабочие узлы реплицируются целиком, а их записи WAL непрерывно передаются потоком на резервный сервер. Потоковую репликацию можно настроить локально, для этого обратитесь к разделу Потоковая репликация.
J.5.9.1.4.2. Сбои узла-координатора #
Узел-координатор citus поддерживает таблицы метаданных для отслеживания всех узлов кластера и расположения сегментов базы данных на этих узлах. Таблицы метаданных обычно небольшого размера (несколько МБ) и редко изменяются. Это означает, что их можно реплицировать и быстро восстанавливать, если на узле происходит сбой. Существует несколько вариантов действий пользователей в случае сбоя координатора.
Используйте потоковую репликацию Postgres Pro. Можно использовать функциональность потоковой репликации Postgres Pro для создания узла-координатора горячего резерва. Затем, если ведущий узел-координатор выйдет из строя, резервный узел может быть автоматически повышен до ведущего для обслуживания запросов к кластеру. За подробной информацией о такой настройке обратитесь к разделу Потоковая репликация.
Используйте инструменты резервного копирования. Поскольку таблицы метаданных небольшого размера, пользователи могут использовать тома EBS или инструменты резервного копирования Postgres Pro для резервного копирования метаданных. Затем можно легко скопировать эти метаданные на новые узлы для возобновления работы.
J.5.9.1.5. Изоляция арендаторов #
J.5.9.1.5.1. Сегментирование на основе строк #
В citus строки таблицы помещаются в сегменты рабочих узлов на основе хешированных значений столбца распределения строк. Несколько значений столбца распределения часто попадают в один и тот же сегмент. В сценарии использования citus со множеством арендаторов это означает, что зачастую они совместно используют сегменты.
Однако совместное использование сегментов может вызывать борьбу за ресурсы, если арендаторы сильно отличаются по размеру. Это обычная ситуация для систем с большим количеством арендаторов: объём данных арендаторов имеет тенденцию подчиняться закону Ципфа по мере увеличения числа арендаторов. То есть несколько очень крупных арендаторов существуют вместе с множеством более мелких. Чтобы улучшить распределение ресурсов и гарантировать качество обслуживания арендаторов, рекомендуется переместить крупных арендаторов на выделенные узлы.
В citus существуют инструменты для изоляции арендатора на определённом узле. Это происходит в два этапа: сначала данные арендатора изолируются в новом выделенном сегменте, а затем сегмент перемещается на нужный узел. Для лучшего понимания этого процесса необходимо точно знать, как строки данных назначаются сегментам.
Каждый сегмент помечен в метаданных citus диапазоном хешированных значений, содержащихся в нём (за более подробной информацией обратитесь к описанию таблицы pg_dist_shard). Функция isolate_tenant_to_new_shard перемещает арендатора в выделенный сегмент в три этапа:
Создаёт новый сегмент для
table_name, включающий строки, столбец распределения которых имеет значениеtenant_id, и исключающий все остальные строки.Перемещает соответствующие строки из текущего сегмента в новый сегмент.
Разделяет старый сегмент на две части с хеш-диапазонами, которые примыкают к месту разделения сверху и снизу.
Кроме того, функция принимает аргумент CASCADE, и при этом изолирует не только строки таблицы table_name арендатора, но и всех таблиц совмещённых с ней:
-- Запрос создаёт изолированный сегмент для данного tenant_id и
-- возвращает идентификатор нового сегмента.
-- Общая форма:
SELECT isolate_tenant_to_new_shard('table_name', tenant_id);
-- Конкретный пример:
SELECT isolate_tenant_to_new_shard('lineitem', 135);
-- Если у данной таблицы есть совмещённые с ней таблицы, запрос выдаст ошибку с
-- рекомендацией использовать параметр CASCADE
SELECT isolate_tenant_to_new_shard('lineitem', 135, 'CASCADE');Результат:
┌─────────────────────────────┐ │ isolate_tenant_to_new_shard │ ├─────────────────────────────┤ │ 102240 │ └─────────────────────────────┘
Новые сегменты создаются на том же узле, что и сегменты, из которых был удалён арендатор. Для настоящей аппаратной изоляции их можно перенести на отдельный узел в кластере citus. Как уже упоминалось, функция isolate_tenant_to_new_shard возвращает идентификатор нового сегмента, который можно использовать для перемещения этого сегмента:
J.5.9.1.5.2. Сегментирование на основе схем #
При сегментировании на основе схем нет необходимости изолировать арендатора, поскольку каждый арендатор по определению уже находится в собственной схеме. Нужно только получить идентификатор сегмента для схемы, чтобы выполнить перемещение.
Сначала найдите идентификатор совмещения схемы, которая будет перемещаться.
SELECT * FROM citus_schemas;
schema_name | colocation_id | schema_size | schema_owner --------------+---------------+-------------+-------------- user_service | 1 | 0 bytes | user_service time_service | 2 | 0 bytes | time_service ping_service | 3 | 0 bytes | ping_service a | 4 | 128 kB | citus b | 5 | 32 kB | citus with_data | 11 | 6408 kB | citus (6 rows)
На следующем этапе выполняется запрос к citus_shards, в котором будет использоваться идентификатор совмещения 11 из вывода выше:
SELECT * FROM citus_shards where colocation_id = 11;
table_name | shardid | shard_name | citus_table_type | colocation_id | nodename | nodeport | shard_size -----------------+---------+------------------------+------------------+---------------+-----------+----------+------------ with_data.test | 102180 | with_data.test_102180 | schema | 11 | localhost | 9702 | 647168 with_data.test2 | 102183 | with_data.test2_102183 | schema | 11 | localhost | 9702 | 5914624 (2 rows)
Можно выбрать любой shardid из результата, поскольку перемещение также будет распространяться на все совмещённые таблицы, что в случае сегментирования на основе схемы означает перемещение всех таблиц внутри схемы.
J.5.9.1.5.3. Выполнение перемещения #
Зная идентификатор сегмента, указывающий на арендатора, можно выполнить перемещение:
-- Найдите узел, на котором сейчас находится новый сегмент SELECT nodename, nodeport FROM citus_shards WHERE shardid = 102240; -- Посмотрите на список рабочих узлов, в которые можно переместить сегмент SELECT * FROM master_get_active_worker_nodes(); -- Переместите сегмент на выбранный рабочий узел (другие сегменты, -- созданные с параметром CASCADE, также будут перемещены) SELECT citus_move_shard_placement( 102240, 'source_host', source_port, 'dest_host', dest_port);
Обратите внимание, что функция citus_move_shard_placement также перемещает любые сегменты, совмещённые с указанным, чтобы сохранить это совмещение.
J.5.9.1.6. Просмотр статистики запросов #
При администрировании кластера citus полезно знать, какие запросы выполняются пользователями, какие узлы задействованы и какой метод выполнения используется citus для каждого запроса. Расширение записывает статистику запросов в представление метаданных citus_stat_statements, названное аналогично представлению pg_stat_statements в Postgres Pro. В представлении pg_stat_statements хранится информация о продолжительности запроса и вводе-выводе, а в citus_stat_statements — о методах выполнения citus и ключах секционирования сегмента (при их наличии).
Чтобы отслеживать статистику запросов в citus, нужно установить расширение pg_stat_statements. В локальном экземпляре Postgres Pro загрузите расширение в postgresql.conf с помощью shared_preload_libraries, затем создайте расширение в SQL:
CREATE EXTENSION pg_stat_statements;
Предположим, есть хеш-распределённая по столбцу id таблица с именем foo.
-- Создайте и заполните распределённую таблицу
CREATE TABLE foo ( id int );
SELECT create_distributed_table('foo', 'id');
INSERT INTO foo SELECT generate_series(1,100);Выполните ещё два запроса, и citus_stat_statements покажет, каким способом citus выполнил их.
-- Подсчёт всех строк выполняется на всех узлах, -- результаты суммируются на координаторе SELECT count(*) FROM foo; -- Указание строки по столбцу распределения отправляет -- выполнение на отдельный узел SELECT * FROM foo WHERE id = 42;
Чтобы узнать, как были выполнены эти запросы, обратитесь к таблице статистики:
SELECT * FROM citus_stat_statements;
Результат:
-[ RECORD 1 ]-+---------------------------------------------- queryid | -6844578505338488014 userid | 10 dbid | 13340 query | SELECT count(*) FROM foo; executor | adaptive partition_key | calls | 1 -[ RECORD 2 ]-+---------------------------------------------- queryid | 185453597994293667 userid | 10 dbid | 13340 query | INSERT INTO foo SELECT generate_series($1,$2) executor | insert-select partition_key | calls | 1 -[ RECORD 3 ]-+---------------------------------------------- queryid | 1301170733886649828 userid | 10 dbid | 13340 query | SELECT * FROM foo WHERE id = $1 executor | adaptive partition_key | 42 calls | 1
Можно увидеть, что для выполнения запросов citus чаще всего использует адаптивного исполнителя. Он разбивает запрос на несколько запросов для выполнения на соответствующих узлах и объединяет результаты на узле-координаторе. В случае второго запроса (фильтрация по столбцу распределения id = $1) citus определил, что требуются данные только с одного узла. Наконец, можно увидеть, что оператор INSERT INTO foo SELECT… выполняется с помощью исполнителя insert-select, который обеспечивает гибкость при выполнении запросов такого типа.
J.5.9.1.6.1. Статистика по арендаторам #
Пока что это представление не содержит новой информации по сравнению с выводом команды EXPLAIN для данного запроса. Однако, помимо получения информации об отдельных запросах, представление citus_stat_statements позволяет отвечать на такие вопросы, как «какой процент запросов в кластере относится к одному арендатору?»
SELECT sum(calls),
partition_key IS NOT NULL AS single_tenant
FROM citus_stat_statements
GROUP BY 2;. sum | single_tenant -----+--------------- 2 | f 1 | t
Например, в многоарендной базе данных ожидается, что подавляющее большинство запросов будут выполняться одним арендатором. Если слишком много запросов относятся к нескольким арендаторам, возможно, в них нет подходящих фильтров для соответствия одному арендатору и используются ненужные ресурсы.
Чтобы выяснить, какие арендаторы наиболее активны, можно использовать представление citus_stat_tenants.
J.5.9.1.6.2. Срок действия статистики #
Представление pg_stat_statements ограничивает количество отслеживаемых операторов и продолжительность ведения своих записей. Поскольку таблица citus_stat_statements отслеживает строгое подмножество запросов в pg_stat_statements, выбор равных ограничений для двух представлений может привести к несоответствию в хранящихся данных. Несоответствующие записи могут привести к непредсказуемому поведению при объединении представлений.
Существует три способа синхронизации представлений, их можно использовать вместе.
Настройте демон обслуживания так, чтобы он периодически синхронизировал статистику citus и Postgres Pro. В параметре конфигурации citus.stat_statements_purge_interval задаётся время синхронизации в секундах. Значение
0отключает периодическую синхронизацию.Настройте количество записей в
citus_stat_statements. В параметре конфигурации citus.stat_statements_max задаётся пороговое значение, при достижении которого старые записи начинают удаляться. Значение по умолчанию —50000, максимальное допустимое значение —10000000. Обратите внимание, что каждая запись занимает около 140 байт в общей памяти, поэтому задавайте значение обдуманно.Увеличьте значение pg_stat_statements.max. Значение по умолчанию —
5000и может быть увеличено до10000,20000или даже до50000без особых издержек. Это наиболее полезно, когда локальные запросы (т. е. на узле-координаторе) создают большую нагрузку.
Примечание
После изменения pg_stat_statements.max или citus.stat_statements_max требуется перезапуск службы Postgres Pro. Изменение citus.stat_statements_purge_interval вступит в силу после вызова функции pg_reload_conf.
J.5.9.1.7. Экономия ресурсов #
J.5.9.1.7.1. Ограничение длительных запросов #
Длительные запросы могут удерживать блокировки, ставить WAL в очередь или просто потреблять много системных ресурсов, поэтому в производственной среде лучше не допускать их слишком длительного выполнения. Можно установить параметр statement_timeout для узла-координатора и рабочих узлов, чтобы отменять запросы, которые выполняются слишком долго.
-- Ограничить длительность запроса до 5 минут
ALTER DATABASE citus
SET statement_timeout TO 300000;
SELECT run_command_on_workers($cmd$
ALTER DATABASE citus
SET statement_timeout TO 300000;
$cmd$);Тайм-аут задаётся в миллисекундах.
Чтобы настроить тайм-аут для каждого запроса, используйте SET LOCAL в транзакции:
BEGIN; -- это ограничение применяется только к текущей транзакции SET LOCAL statement_timeout TO 300000; -- ... COMMIT;
J.5.9.1.8. Безопасность #
J.5.9.1.8.1. Управление подключениями #
Примечание
Трафик между различными узлами кластера шифруется для новых инсталляций с помощью TLS-протокола с самоподписанными сертификатами. Это означает, что нет защиты от атак посредника, только от пассивного прослушивания в сети.
Кластеры, изначально созданные с помощью citus, не имеют включённого сетевого шифрования между узлами (даже если они будут обновлены позднее). Чтобы настроить самоподписанный TLS-протокол для этого типа установки, выполните действия, описанные в разделе Создание сертификатов, и задайте описанные здесь параметры citus, т. е. измените значение citus.node_conninfo на sslmode=require. Это следует сделать как на узле-координаторе, так и на рабочих узлах.
Когда узлы citus взаимодействуют друг с другом, они обращаются к таблице с учётными данными подключения. Это позволяет администратору базы данных гибко настраивать параметры безопасности и производительности.
Чтобы установить неконфиденциальные параметры подключения libpq, которые будут использоваться для всех подключений узлов, измените параметр конфигурации citus.node_conninfo:
-- Пары ключ=значение, разделённые пробелами. -- Например, параметры ssl: ALTER SYSTEM SET citus.node_conninfo = 'sslrootcert=/path/to/citus-ca.crt sslcrl=/path/to/citus-ca.crl sslmode=verify-full';
Существует белый список значений, которые принимает параметр конфигурации citus.node_conninfo. Значение по умолчанию — sslmode=require, при котором исключается незашифрованная связь между узлами. Если кластер изначально был создан с помощью citus, значение параметра — sslmode=prefer. После настройки самоподписанных сертификатов на всех узлах рекомендуется изменить значение этого параметра на sslmode=require.
После изменения этого параметра важно перезагрузить конфигурацию Postgres Pro. Несмотря на то, что изменённый параметр может быть виден не во всех сеансах, citus обращается к нему только при установке новых подключений. При получении сигнала перезагрузки citus отмечает все существующие подключения для закрытия, что приводит к повторному подключению после завершения запущенных транзакций.
SELECT pg_reload_conf();
-- Доступ к этой таблице есть только у суперпользователей -- Добавление пароля для пользователя jdoe INSERT INTO pg_dist_authinfo (nodeid, rolename, authinfo) VALUES (123, 'jdoe', 'password=abc123');
После выполнения этой команды INSERT любой запрос, которому требуется подключение к узлу 123 от имени пользователя jdoe, будет использовать предоставленный пароль. За дополнительной информацией обратитесь к описанию таблицы pg_dist_authinfo.
-- Изменение пользователя jdoe для использования аутентификации по сертификату UPDATE pg_dist_authinfo SET authinfo = 'sslcert=/path/to/user.crt sslkey=/path/to/user.key' WHERE nodeid = 123 AND rolename = 'jdoe';
Это позволяет пользователю вместо пароля использовать сертификат и файл ключа при подключении к узлу 123. Убедитесь, что сертификат пользователя подписан сертификатом, которому доверяет подключаемый рабочий узел, а параметры аутентификации рабочего узла допускают аутентификацию по сертификату. Полную документацию по использованию клиентских сертификатов можно найти в разделе Клиентские сертификаты.
Изменение таблицы pg_dist_authinfo не приводит к переподключению существующих соединений.
J.5.9.1.8.2. Настройка сертификатов, подписанных центром сертификации #
В этом разделе предполагается, что существует доверенный центр сертификации, который может выдавать сертификаты сервера для всех узлов кластера. Рекомендуется сотрудничать с отделом безопасности организации, чтобы предотвратить неправильную обработку ключевых материалов. В этом руководстве рассматривается только конфигурация, необходимая для citus, а не лучшие практики управления инфраструктурой открытых ключей.
Для всех узлов кластера необходимо получить действительный сертификат, подписанный одним и тем же центром сертификации. Предполагается, что на каждой машине доступны следующие файлы, уникальные для конкретной машины:
/path/to/server.key— закрытый ключ сервера/path/to/server.crt— сертификат сервера или цепочка сертификатов для ключа сервера, подписанная доверенным центром сертификации
Кроме этих файлов, уникальных для каждой машины, нужны следующие файлы кластера или центра сертификации:
/path/to/ca.crt— сертификат центра сертификации/path/to/ca.crl— список отозванных сертификатов центра сертификации
Примечание
Список отозванных сертификатов, вероятно, будет со временем меняться. Рекомендуется настроить механизм своевременного обновления этого списка на всех узлах кластера вместе с отделом безопасности организации. После обновления списка необходимо перезагрузить каждый узел в кластере.
После размещения всех файлов на узлах необходимо настроить следующие параметры в файле конфигурации Postgres:
# Следующие параметры позволяют серверу postgres включить ssl-протокол и # конфигурировать сервер для предоставления сертификата клиентам # при подключении по протоколу tls/ssl ssl = on ssl_key_file = '/path/to/server.key' ssl_cert_file = '/path/to/server.crt' # Указание для citus проверять сертификат сервера, к которому он подключается citus.node_conninfo = 'sslmode=verify-full sslrootcert=/path/to/ca.crt sslcrl=/path/to/ca.crl'
Чтобы изменения вступили в силу, перезагрузите конфигурацию. Кроме того, может потребоваться настроить citus.local_hostname для корректной работы с sslmode=verify-full.
В зависимости от политики используемого центра сертификации может потребоваться изменить sslmode=verify-full в citus.node_conninfo на sslmode=verify-ca. Чтобы узнать разницу между этими вариантами, обратитесь к разделу Описание режимов SSL.
Наконец, чтобы запретить пользователям незашифрованные подключения, необходимо внести изменения в pg_hba.conf. Во многих инсталляциях Postgres Pro будут записи, разрешающие подключения host, подключения по протоколу SSL/TLS, а также обычные TCP-подключения. Чтобы разрешить аутентификацию только зашифрованных подключений в Postgres Pro, замените все записи host на hostssl. За полным описанием этих параметров обратитесь к описанию файла pg_hba.conf.
Примечание
Если доверенный центр сертификации недоступен, можно создать собственный с помощью самоподписанного корневого сертификата. Это нетривиальное решение, и разработчику или оператору следует обратиться за советом к команде безопасности.
Чтобы убедиться, что соединения узла-координатора с рабочими узлами зашифрованы, можно выполнить следующий запрос. Он покажет версию протокола SSL/TLS, используемую для шифрования соединения координатора с рабочим узлом:
SELECT run_command_on_workers($$ SELECT version FROM pg_stat_ssl WHERE pid = pg_backend_pid() $$);
┌────────────────────────────┐ │ run_command_on_workers │ ├────────────────────────────┤ │ (localhost,9701,t,TLSv1.2) │ │ (localhost,9702,t,TLSv1.2) │ └────────────────────────────┘ (2 rows)
J.5.9.1.8.3. Повышение уровня безопасности рабочего узла #
Инструкции для инсталляции с несколькими узлами помогут настроить pg_hba.conf на рабочих узлах с заданным методом аутентификации trust для подключений по локальной сети. Однако может потребоваться более высокий уровень безопасности.
Чтобы все подключения предоставляли хешированный пароль, измените Postgres Pro pg_hba.conf на каждом рабочем узле следующим образом:
# Требовать доступ по паролю и подключение по протоколу SSL/TLS # с узлами в локальной сети. Следующие диапазоны соответствуют # 24-, 20- и 16-битным блокам в закрытых адресных пространствах IPv4. hostssl all all 10.0.0.0/8 md5 # Требовать пароли и подключения по протоколу SSL/TLS, # в том числе, когда узел подключается сам к себе. hostssl all all 127.0.0.1/32 md5 hostssl all all ::1/128 md5
Узлу-координатору необходимо знать пароли ролей, чтобы взаимодействовать с рабочими узлами. В citus информация аутентификации должна храниться в файле .pgpass. Отредактируйте файл в домашнем каталоге пользователя Postgres Pro, добавив в него строку для каждой комбинации рабочего узла и роли:
hostname:port:database:username:password
Иногда рабочим узлам необходимо подключиться друг к другу, например, во время объединений с пересекционированием. То есть для каждого рабочего узла также требуется копия файла .pgpass.
J.5.9.1.8.4. Безопасность на уровне строк #
Безопасность на уровне строк Postgres Pro определяет для каждого пользователя, какие строки могут возвращаться обычными запросами, а также вставляться, изменяться или удаляться соответствующими командами. Она особенно полезна в многоарендном кластере citus, поскольку позволяет отдельным арендаторам иметь полный SQL-доступ к базе данных, скрывая при этом информацию каждого арендатора от других арендаторов.
Разделение данных арендаторов можно реализовать, используя соглашение об именах для ролей базы данных, которое связано с политиками безопасности на уровне строк таблицы. Каждому арендатору назначается роль базы данных в нумерованной последовательности: tenant_1, tenant_2 и т. д. Арендаторы будут подключаться к citus, используя эти отдельные роли. Политики безопасности на уровне строк могут сравнивать имя роли со значениями в столбце распределения tenant_id, чтобы принимать решение о предоставлении доступа.
Ниже описано, как применить этот подход к упрощённой таблице событий, распределённой по tenant_id. Сначала создайте роли tenant_1 и tenant_2. Затем выполните от имени администратора следующие команды:
CREATE TABLE events(
tenant_id int,
id int,
type text
);
SELECT create_distributed_table('events','tenant_id');
INSERT INTO events VALUES (1,1,'foo'), (2,2,'bar');
-- Допустим, роли tenant_1 и tenant_2 существуют
GRANT select, update, insert, delete
ON events TO tenant_1, tenant_2;В нынешнем виде любой, у кого есть разрешение выполнять SELECT для этой таблицы, может видеть обе строки. Пользователи любого арендатора могут видеть и изменять строки другого арендатора. Можно решить эту проблему с помощью политик безопасности таблиц на уровне строк.
Каждая политика состоит из двух предложений: USING и WITH CHECK. Когда пользователь пытается прочитать или записать строки, база данных сравнивает каждую строку с этими условиями. Существующие строки таблицы проверяются на соответствие выражению, указанному в USING, а новые строки, созданные с помощью INSERT или UPDATE, проверяются на соответствие выражению, указанному в WITH CHECK.
-- Сначала зададим политику для администратора системы — пользователя "citus" CREATE POLICY admin_all ON events TO citus -- применяется к этой роли USING (true) -- читать любые существующие строки WITH CHECK (true); -- добавлять или изменять любые строки -- Далее зададим политику, разрешающую роли "tenant_<n>" -- доступ к строкам, где tenant_id = <n> CREATE POLICY user_mod ON events USING (current_user = 'tenant_' || tenant_id::text); -- Отсутствие CHECK означает, что условие совпадает с USING -- Применяем политики ALTER TABLE events ENABLE ROW LEVEL SECURITY;
Теперь роли tenant_1 и tenant_2 получат разные результаты запросов:
При подключении от имени tenant_1:
SELECT * FROM events;
┌───────────┬────┬──────┐ │ tenant_id │ id │ type │ ├───────────┼────┼──────┤ │ 1 │ 1 │ foo │ └───────────┴────┴──────┘
При подключении от имени tenant_2:
SELECT * FROM events;
┌───────────┬────┬──────┐ │ tenant_id │ id │ type │ ├───────────┼────┼──────┤ │ 2 │ 2 │ bar │ └───────────┴────┴──────┘
INSERT INTO events VALUES (3,3,'surprise'); /* ERROR: new row violates row-level security policy for table "events_102055" */
J.5.9.1.9. Расширения Postgres Pro #
Расширение citus обеспечивает Postgres Pro функциональностью распределения с помощью программных интерфейсов и обработчика. Эта функциональность включает, помимо прочего, поддержку широкого спектра типов данных (включая полуструктурированные типы данных, такие как jsonb и hstore), операторы и функции, полнотекстовый поиск и другие расширения, такие как PostGIS и HyperLogLog. Кроме того, правильное использование программных интерфейсов расширения обеспечивает совместимость со стандартными инструментами Postgres Pro, такими как pgAdmin и pg_upgrade.
Поскольку расширение citus можно установить на любой экземпляр Postgres Pro, вместе с ним также могут напрямую использоваться другие расширения, такие как hstore, hll или PostGIS. Однако следует помнить об одной особенности. При включении других расширений в shared_preload_libraries citus всегда должен указываться первым.
Следующие расширения будут особенно полезны при работе с citus:
cstore_fdw — столбцовое хранилище для аналитики. Столбцовое хранение обеспечивает производительность за счёт чтения с диска только соответствующих данных и может сжимать данные в 6–10 раз, уменьшая объём пространства, необходимого для архивирования данных.
pg_cron — запуск повторяющихся заданий напрямую из базы данных.
topn — возвращение значений в базе данных, отвечающих определённым критериям. Использует алгоритм приближения для быстрого получения результатов при небольшом потреблении вычислительных ресурсов и памяти.
hll — структура данных HyperLogLog как отдельный тип данных. Это структура фиксированного размера, подобная множеству, которая используется для подсчёта различных значений с настраиваемой точностью.
J.5.9.1.10. Создание новой базы данных #
Каждый сервер Postgres Pro может содержать несколько баз данных. Однако новые базы данных не наследуют расширения других; все нужные расширения необходимо добавлять заново. Чтобы запустить citus в новой базе данных, нужно создать базу данных на узле-координаторе и рабочих узлах, создать расширение citus в этой базе данных и зарегистрировать рабочие узлы в базе данных узла-координатора.
Подключитесь к каждому рабочему узлу и выполните:
-- На каждом рабочем узле CREATE DATABASE newbie; \c newbie CREATE EXTENSION citus;
Затем выполните на узле-координаторе:
CREATE DATABASE newbie;
\c newbie
CREATE EXTENSION citus;
SELECT * from citus_add_node('node-name', 5432);
SELECT * from citus_add_node('node-name2', 5432);
-- ... для всех узловТеперь новая база данных будет работать как ещё один кластер citus.
J.5.9.2. Управление таблицами #
J.5.9.2.1. Определение размера отношения #
Обычный способ определения размеров таблиц в Postgres Pro, pg_total_relation_size, существенно занижает размер распределённых таблиц. В кластере citus эта функция только показывает размер таблиц на узле-координаторе. На самом деле данные в распределённых таблицах находятся на рабочих узлах (сегментах), а не на узле-координаторе. Реальный размер распределённой таблицы складывается из размеров сегментов. В citus доступны вспомогательные функции для получения этой информации.
| Функция | Возвращает |
|---|---|
| citus_relation_size |
|
| citus_table_size |
|
| citus_total_relation_size |
|
Эти функции аналогичны трём стандартным функциям получения размера объектов БД в Postgres Pro, с небольшим дополнением: если они не могут подключиться к узлу, выдаётся ошибка.
Ниже представлен пример использования одной из вспомогательных функций для вывода размеров всех распределённых таблиц:
SELECT logicalrelid AS name,
pg_size_pretty(citus_table_size(logicalrelid)) AS size
FROM pg_dist_partition;Результат:
┌───────────────┬───────┐ │ name │ size │ ├───────────────┼───────┤ │ github_users │ 39 MB │ │ github_events │ 37 MB │ └───────────────┴───────┘
J.5.9.2.2. Очистка распределённых таблиц #
В Postgres Pro (и других базах данных MVCC) команды UPDATE или DELETE не приводят к немедленному удалению старой версии строки. Накопление устаревших строк называется раздуванием, и от них необходимо избавляться, чтобы избежать снижения производительности запросов и неконтролируемого заполнения дискового пространства. Postgres Pro запускает процесс, называемый демоном автоочистки, который периодически очищает (удаляет) устаревшие строки.
В распределённой базе данных масштабируются не только пользовательские запросы, но и очистка. В Postgres Pro большие загруженные таблицы очень склонны к раздуванию как из-за более низкой чувствительности к параметру порогового значения очистки Postgres Pro, так и в целом из-за уровня циркуляции строк. Разделение таблицы на распределённые сегменты означает, что отдельные сегменты представляют собой таблицы меньшего размера, а также, что процессы автоочистки могут работать параллельно с разными частями таблицы на разных машинах. Обычно автоочистка может запускать только один рабочий процесс на таблицу.
В связи с вышеизложенным операции автоочистки в кластере citus должны быть эффективны в большинстве случаев. Однако для таблиц с определёнными нагрузками или для компаний с установленными «безопасными» часами очистки может быть целесообразнее очищать таблицу вручную, а не оставлять всю работу автоочистке.
Чтобы очистить таблицу, выполните на узле-координаторе:
VACUUM my_distributed_table;
При очистке распределённой таблицы команда VACUUM будет отправлена во все места размещения этой таблицы (одно подключение на каждое место размещения). Это делается параллельно. Поддерживаются все параметры (в том числе список таблица_и_столбцы), за исключением VERBOSE. Команда VACUUM также запускается на узле-координаторе до уведомления каких-либо рабочих узлов. Обратите внимание, что неквалифицированные команды очистки (т. е. команды без указанной таблицы) не транслируются на рабочие узлы.
J.5.9.2.3. Анализ распределённых таблиц #
Команда Postgres Pro ANALYZE собирает статистику о содержимом таблиц в базе данных. Впоследствии планировщик запросов использует эту статистику, чтобы определить наиболее эффективные планы выполнения запросов.
Демон автоочистки, описанный в предыдущем разделе, автоматически выполняет команды ANALYZE каждый раз, когда содержимое таблицы существенно изменяется. Демон планирует выполнение ANALYZE строго в зависимости от количества вставленных или изменённых строк, но при этом неизвестно, приведёт ли это к статистически значимым изменениям. Для администраторов может быть предпочтительнее вручную планировать операции ANALYZE, чтобы они совпадали со статистически значимыми изменениями таблицы.
Чтобы проанализировать таблицу, выполните на узле-координаторе:
ANALYZE my_distributed_table;
В citus команда ANALYZE транслируется на все размещения рабочих узлов.
J.5.9.2.4. Столбцовое хранилище #
В citus реализовано хранилище столбцовых таблиц с возможностью только добавления для аналитики и хранения данных. Когда столбцы (а не строки) хранятся на диске последовательно, данные становятся более сжимаемыми, и запросы могут быстрее обращаться к подмножеству столбцов.
J.5.9.2.4.1. Использование #
Чтобы использовать столбцовое хранилище, укажите USING columnsar при создании таблицы:
CREATE TABLE contestant (
handle TEXT,
birthdate DATE,
rating INT,
percentile FLOAT,
country CHAR(3),
achievements TEXT[]
) USING columnar;Также можно переключаться между строковым хранением (метод доступа heap) и columnar.
-- Преобразование в строковое хранение (метод доступа heap)
SELECT alter_table_set_access_method('contestant', 'heap');
-- Преобразование в столбцовое хранение (индексы будут удалены)
SELECT alter_table_set_access_method('contestant', 'columnar');В citus строки преобразуются для столбцового хранения в «массивы» во время вставки. Каждый массив содержит данные одной транзакции или 150 000 строк, в зависимости от того, что меньше. (Размер массива и другие параметры столбцовой таблицы можно изменить с помощью функции alter_columnar_table_set.)
Например, следующий оператор помещает все пять строк в один массив, поскольку все значения вставляются в одной транзакции:
-- Все эти значения вставляются в один столбцовый массив
INSERT INTO contestant VALUES
('a','1990-01-10',2090,97.1,'XA','{a}'),
('b','1990-11-01',2203,98.1,'XA','{a,b}'),
('c','1988-11-01',2907,99.4,'XB','{w,y}'),
('d','1985-05-05',2314,98.3,'XB','{}'),
('e','1995-05-05',2236,98.2,'XC','{a}');По возможности лучше создавать большие массивы, поскольку citus сжимает столбцовые данные для каждого массива. Чтобы увидеть информацию о столбцовой таблице, например степень сжатия, количество массивов и среднее количество строк в массиве, используйте VACUUM VERBOSE:
VACUUM VERBOSE contestant;
INFO: statistics for "contestant": storage id: 10000000000 total file size: 24576, total data size: 248 compression rate: 1.31x total row count: 5, stripe count: 1, average rows per stripe: 5 chunk count: 6, containing data for dropped columns: 0, zstd compressed: 6
Вывод показывает, что citus использовал алгоритм сжатия zstd, чтобы сжать данные в 1,31 раза. Степень сжатия показывает разницу между размером вставленных данных при их размещении в памяти и размером сжатых данных в конечном массиве.
В зависимости от способа измерения степень сжатия может как соответствовать, так и не соответствовать разнице в размерах между строковым и столбцовым хранением таблицы. Единственный способ выяснить реальную разницу — построить строковую и столбцовую таблицы, содержащие одинаковые данные, и сравнить их.
J.5.9.2.4.2. Измерение степени сжатия #
Создайте новый пример с большим количеством данных, чтобы оценить экономию при сжатии.
-- Сначала создайте большую таблицу со строковым хранением. CREATE TABLE perf_row( c00 int8, c01 int8, c02 int8, c03 int8, c04 int8, c05 int8, c06 int8, c07 int8, c08 int8, c09 int8, c10 int8, c11 int8, c12 int8, c13 int8, c14 int8, c15 int8, c16 int8, c17 int8, c18 int8, c19 int8, c20 int8, c21 int8, c22 int8, c23 int8, c24 int8, c25 int8, c26 int8, c27 int8, c28 int8, c29 int8, c30 int8, c31 int8, c32 int8, c33 int8, c34 int8, c35 int8, c36 int8, c37 int8, c38 int8, c39 int8, c40 int8, c41 int8, c42 int8, c43 int8, c44 int8, c45 int8, c46 int8, c47 int8, c48 int8, c49 int8, c50 int8, c51 int8, c52 int8, c53 int8, c54 int8, c55 int8, c56 int8, c57 int8, c58 int8, c59 int8, c60 int8, c61 int8, c62 int8, c63 int8, c64 int8, c65 int8, c66 int8, c67 int8, c68 int8, c69 int8, c70 int8, c71 int8, c72 int8, c73 int8, c74 int8, c75 int8, c76 int8, c77 int8, c78 int8, c79 int8, c80 int8, c81 int8, c82 int8, c83 int8, c84 int8, c85 int8, c86 int8, c87 int8, c88 int8, c89 int8, c90 int8, c91 int8, c92 int8, c93 int8, c94 int8, c95 int8, c96 int8, c97 int8, c98 int8, c99 int8 ); -- Затем создайте таблицу с идентичными столбцами, но столбцовым хранением. CREATE TABLE perf_columnar(LIKE perf_row) USING COLUMNAR;
Заполните обе таблицы одним и тем же большим набором данных:
INSERT INTO perf_row
SELECT
g % 00500, g % 01000, g % 01500, g % 02000, g % 02500, g % 03000, g % 03500, g % 04000, g % 04500, g % 05000,
g % 05500, g % 06000, g % 06500, g % 07000, g % 07500, g % 08000, g % 08500, g % 09000, g % 09500, g % 10000,
g % 10500, g % 11000, g % 11500, g % 12000, g % 12500, g % 13000, g % 13500, g % 14000, g % 14500, g % 15000,
g % 15500, g % 16000, g % 16500, g % 17000, g % 17500, g % 18000, g % 18500, g % 19000, g % 19500, g % 20000,
g % 20500, g % 21000, g % 21500, g % 22000, g % 22500, g % 23000, g % 23500, g % 24000, g % 24500, g % 25000,
g % 25500, g % 26000, g % 26500, g % 27000, g % 27500, g % 28000, g % 28500, g % 29000, g % 29500, g % 30000,
g % 30500, g % 31000, g % 31500, g % 32000, g % 32500, g % 33000, g % 33500, g % 34000, g % 34500, g % 35000,
g % 35500, g % 36000, g % 36500, g % 37000, g % 37500, g % 38000, g % 38500, g % 39000, g % 39500, g % 40000,
g % 40500, g % 41000, g % 41500, g % 42000, g % 42500, g % 43000, g % 43500, g % 44000, g % 44500, g % 45000,
g % 45500, g % 46000, g % 46500, g % 47000, g % 47500, g % 48000, g % 48500, g % 49000, g % 49500, g % 50000
FROM generate_series(1,50000000) g;
INSERT INTO perf_columnar
SELECT
g % 00500, g % 01000, g % 01500, g % 02000, g % 02500, g % 03000, g % 03500, g % 04000, g % 04500, g % 05000,
g % 05500, g % 06000, g % 06500, g % 07000, g % 07500, g % 08000, g % 08500, g % 09000, g % 09500, g % 10000,
g % 10500, g % 11000, g % 11500, g % 12000, g % 12500, g % 13000, g % 13500, g % 14000, g % 14500, g % 15000,
g % 15500, g % 16000, g % 16500, g % 17000, g % 17500, g % 18000, g % 18500, g % 19000, g % 19500, g % 20000,
g % 20500, g % 21000, g % 21500, g % 22000, g % 22500, g % 23000, g % 23500, g % 24000, g % 24500, g % 25000,
g % 25500, g % 26000, g % 26500, g % 27000, g % 27500, g % 28000, g % 28500, g % 29000, g % 29500, g % 30000,
g % 30500, g % 31000, g % 31500, g % 32000, g % 32500, g % 33000, g % 33500, g % 34000, g % 34500, g % 35000,
g % 35500, g % 36000, g % 36500, g % 37000, g % 37500, g % 38000, g % 38500, g % 39000, g % 39500, g % 40000,
g % 40500, g % 41000, g % 41500, g % 42000, g % 42500, g % 43000, g % 43500, g % 44000, g % 44500, g % 45000,
g % 45500, g % 46000, g % 46500, g % 47000, g % 47500, g % 48000, g % 48500, g % 49000, g % 49500, g % 50000
FROM generate_series(1,50000000) g;
VACUUM (FREEZE, ANALYZE) perf_row;
VACUUM (FREEZE, ANALYZE) perf_columnar;На примере этих данных видно, что в столбцовой таблице они сжаты более, чем в 8 раз.
SELECT pg_total_relation_size('perf_row')::numeric/
pg_total_relation_size('perf_columnar') AS compression_ratio;. compression_ratio -------------------- 8.0196135873627944 (1 row)
J.5.9.2.4.3. Пример #
Столбцовое хранение хорошо работает с секционированием таблиц. За подробностями обратитесь к разделу Архивирование со столбцовым хранением.
J.5.9.2.4.4. Нетривиальные проблемы #
При столбцовом хранении данные сжимаются в каждом массиве. Массивы создаются для каждой транзакции, поэтому при наличии только одной строки в каждой транзакции отдельные строки будут помещены в отдельные массивы. Сжатие и производительность для массивов из одной строки будут хуже, чем у строковой таблицы. Поэтому для столбцовой таблицы всегда следует использовать массовое добавление данных.
Даже если создать столбцовое хранилище с большим количеством маленьких массивов, его можно исправить. Для этого выполните команду
VACUUM (FULL)для таблицы:VACUUM (FULL) foo_table;
В некоторых случаях может быть правильнее создать новую таблицу, перенести данные и удалить старую:
BEGIN; CREATE TABLE foo_compacted (LIKE foo) USING columnar; INSERT INTO foo_compacted SELECT * FROM foo; DROP TABLE foo; ALTER TABLE foo_compacted RENAME TO foo; COMMIT;
Принципиально несжимаемые данные могут помешать, но столбцовое хранение всё равно пригодится, чтобы при выборе определённых столбцов в память загружалось меньше данных.
В секционированной таблице, состоящей из смешанных строковых и столбцовых секций, изменения должны быть тщательно спланированы или отфильтрованы, чтобы затрагивать только строковые секции.
Если операция нацелена на конкретную строковую секцию (например,
UPDATE p2 SET i = i + 1), она завершится успешно; если же она нацелена на столбцовую секцию (например,UPDATE p1 SET i = i + 1), то завершится ошибкой.Если операция выполняется для секционированной таблицы и имеет предложение
WHERE, исключающее все столбцовые секции (например,UPDATE parent SET i = i + 1 WHERE timestamp = '2020-03-15'), она завершится успешно.Если операция выполняется для секционированной таблицы, но не исключает все столбцовые секции, она завершится ошибкой, даже если фактически изменяемые данные содержатся в строковых таблицах (например,
UPDATE parent SET i = i + 1 WHERE n = 300).
J.5.9.2.4.5. Ограничения #
В будущих версиях citus текущие ограничения будут постепенно сниматься:
Только обновление данных (отсутствие поддержки команд
UPDATE/DELETE)Отсутствие очистки пространства (например, отменённые транзакции всё равно могут занимать пространство на диске)
Отсутствие сканирования индексов по битовой карте
Отсутствие сканирования по идентификатору кортежа
Отсутствие сканирования по выборкам
Отсутствие поддержки TOAST (встроенная поддержка больших значений)
Отсутствие поддержки операторов
ON CONFLICT(кроме действийDO NOTHINGбез указания цели)Отсутствие поддержки блокировки кортежей (
SELECT ... FOR SHARE,SELECT ... FOR UPDATE)Отсутствие поддержки уровня изоляции serializable
Поддержка серверов Postgres Pro только версий 12 и выше
Отсутствие поддержки внешних ключей
Отсутствие поддержки логического декодирования
Отсутствие поддержки параллельного сканирования внутри узла
Отсутствие поддержки триггеров
AFTER ... FOR EACH ROWОтсутствие нежурналируемых (
UNLOGGED) столбцовых таблиц
J.5.10. Устранение неполадок #
J.5.10.1. Настройка производительности запросов #
В этом разделе описана настройка кластера citus для максимальной производительности. Сначала объясняется, как выбор правильного столбца распределения влияет на производительность. Затем как можно настроить базу данных для обеспечения высокой производительности на одном сервере Postgres Pro, а затем масштабировать её на все ЦП в кластере. В этом разделе также обсуждается несколько параметров конфигурации, связанных с производительностью.
J.5.10.1.1. Сегменты и распределение таблиц #
Первым шагом при создании распределённой таблицы является выбор правильного столбца распределения. Это поможет citus перенести несколько операций непосредственно на сегменты рабочих узлов и удалить несвязанные сегменты, что значительно ускорит выполнение запросов.
Как правило, в качестве столбца распределения следует выбирать тот столбец, который чаще всего используется как ключ соединения или в фильтрах большинства запросов. Для фильтров citus использует диапазоны столбцов распределения для исключения несвязанных сегментов, гарантируя, что запрос затрагивает только те сегменты, которые пересекаются с диапазонами предложения WHERE. Если ключ соединения совпадает с ключом столбца распределения, citus выполняет соединение только между теми сегментами, которые имеют совпадающие/пересекающиеся диапазоны столбцов распределения. Все эти соединения сегментов могут выполняться параллельно на рабочих узлах и, следовательно, более эффективны.
Кроме того, citus может передать несколько операций непосредственно рабочим узлам с сегментами, если они основаны на столбце распределения. Это значительно снижает как объём вычислений на каждом узле, так и пропускную способность сети, необходимую для передачи данных между узлами.
Как только выбран правильный столбец распределения, можно перейти к следующему шагу — настройке производительности рабочих узлов.
J.5.10.1.2. Настройка Postgres Pro #
Координатор citus разбивает входящий запрос на фрагменты и отправляет их рабочим узлам для параллельной обработки. Рабочие узлы — это просто расширенные серверы Postgres Pro, применяющие стандартную логику планирования и выполнения Postgres Pro для этих запросов. Итак, первый шаг в настройке citus — настройка параметров конфигурации Postgres Pro на рабочих узлах для обеспечения высокой производительности.
Настройка параметров основана на экспериментах и часто требует нескольких попыток для достижения приемлемой производительности. Поэтому при настройке лучше всего использовать небольшую часть данных, чтобы каждая итерация проходила быстрее.
Чтобы начать процесс настройки, создайте кластер citus и загрузите в него данные. На узле-координаторе запустите команду EXPLAIN для репрезентативных запросов, чтобы проверить производительность. В citus команда EXPLAIN также предоставляет информацию о выполнении распределённых запросов. Вывод EXPLAIN показывает, как каждый рабочий узел обрабатывает запрос, а также как узел-координатор объединяет их результаты.
Ниже приведён пример объяснения плана для конкретного примера запроса. Используется флаг VERBOSE, чтобы увидеть фактические запросы, которые были отправлены на рабочие узлы.
EXPLAIN VERBOSE
SELECT date_trunc('minute', created_at) AS minute,
sum((payload->>'distinct_size')::int) AS num_commits
FROM github_events
WHERE event_type = 'PushEvent'
GROUP BY minute
ORDER BY minute;Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: remote_scan.minute
-> HashAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.minute
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0)
Task Count: 32
Tasks Shown: One of 32
-> Task
Query: SELECT date_trunc('minute'::text, created_at) AS minute, sum(((payload OPERATOR(pg_catalog.->>) 'distinct_size'::text))::integer) AS num_commits FROM github_events_102042 github_events WHERE (event_type OPERATOR(pg_catalog.=) 'PushEvent'::text) GROUP BY (date_trunc('minute'::text, created_at))
Node: host=localhost port=5433 dbname=postgres
-> HashAggregate (cost=93.42..98.36 rows=395 width=16)
Group Key: date_trunc('minute'::text, created_at)
-> Seq Scan on github_events_102042 github_events (cost=0.00..88.20 rows=418 width=503)
Filter: (event_type = 'PushEvent'::text)
(13 rows)Результат говорит о следующем. Есть 32 сегмента, и планировщик выбрал адаптивного исполнителя citus для выполнения этого запроса:
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0) Task Count: 32
Затем он выбирает один из рабочих узлов и показывает больше информации о поведении запроса на этом узле. Указывается имя узла, порт, база данных и запрос, который был отправлен рабочему узлу, чтобы можно было напрямую подключиться к рабочему узлу и попробовать выполнить запрос:
Tasks Shown: One of 32
-> Task
Query: SELECT date_trunc('minute'::text, created_at) AS minute, sum(((payload OPERATOR(pg_catalog.->>) 'distinct_size'::text))::integer) AS num_commits FROM github_events_102042 github_events WHERE (event_type OPERATOR(pg_catalog.=) 'PushEvent'::text) GROUP BY (date_trunc('minute'::text, created_at))
Node: host=localhost port=5433 dbname=postgresДалее в распределённой команде EXPLAIN показаны результаты запуска обычной команды Postgres Pro EXPLAIN на этом рабочем узле для запроса-фрагмента:
-> HashAggregate (cost=93.42..98.36 rows=395 width=16)
Group Key: date_trunc('minute'::text, created_at)
-> Seq Scan on github_events_102042 github_events (cost=0.00..88.20 rows=418 width=503)
Filter: (event_type = 'PushEvent'::text)Теперь можно подключиться к рабочему узлу с именем localhost, портом 5433 и настроить производительность запросов для сегмента github_events_102042, используя стандартные Postgres Pro методы. После внесения изменений снова запустите команду EXPLAIN с узла-координатора или прямо на рабочем узле.
Первый набор таких оптимизаций относится к настройкам конфигурации. В Postgres Pro по умолчанию заданы консервативные значения параметров, среди которых наиболее важными для оптимизации производительности чтения являются параметры shared_buffers и work_mem. Их краткое описание приведено ниже. Кроме того, на производительность запросов влияют несколько других параметров конфигурации, которые более подробно описаны в разделе Настройка сервера.
Параметр конфигурации shared_buffers определяет объём памяти, выделяемой базе данных для кеширования, и по умолчанию равен 128 МБ. Если есть рабочий узел с 1 ГБ или более ОЗУ, рекомендуется задавать значение для shared_buffers, равное 1/4 объёма памяти системы. При некоторых нагрузках эффективны даже большие значения shared_buffers, но, учитывая, что Postgres Pro также использует кеш операционной системы, маловероятно, что использование более 25% оперативной памяти будет эффективнее, чем меньшее значение.
Если выполняется много сложных сортировок, увеличение значения work_mem позволит Postgres Pro выполнять более крупные сортировки в памяти, что будет быстрее, чем при использовании диска. Если на рабочем узле заметно активное использование диска, несмотря на значительный объём выделенной оперативной памяти, попробуйте увеличить значение work_mem. Это поможет Postgres Pro выбирать более эффективные планы запросов и выполнять большее количество операций в памяти.
Помимо вышеуказанных параметров конфигурации, планировщик запросов Postgres Pro использует статистическую информацию о содержимом таблиц для создания эффективных планов. Эта статистика собирается при запуске команды ANALYZE, её сбор включён по умолчанию. За дополнительной информацией о планировщике Postgres Pro и команде ANALYZE обратитесь к соответствующему разделу.
Наконец, можно создавать индексы для таблиц, чтобы повысить производительность базы данных. Индексы позволяют базе данных находить и извлекать определённые строки гораздо быстрее. Чтобы выбрать индекс с наилучшей производительностью, запустите запрос с командой EXPLAIN, чтобы просмотреть планы запросов и оптимизировать медленные части запроса. После создания индекса система должна поддерживать его синхронизацию с таблицей, что увеличивает издержки на операции с данными. Поэтому индексы, которые редко или никогда не используются в запросах, должны удаляться.
Для повышения производительности записи, а именно скорости выполнения команды INSERT, можно использовать общую настройку конфигурации Postgres Pro. Обычно рекомендуется увеличить значения параметров checkpoint_timeout и max_wal_size. Кроме того, в зависимости от требований к надёжности вашего приложения можно изменить значения fsync или synchronous_commit.
После пользовательской настройки рабочего узла, пользователю также придётся вручную применить эти изменения на других рабочих узлах. Чтобы убедиться, что все они работают правильно, установите эту переменную конфигурации на узле-координаторе:
SET citus.explain_all_tasks = 1;
После этого команда EXPLAIN будет показывать планы запросов для всех заданий.
EXPLAIN
SELECT date_trunc('minute', created_at) AS minute,
sum((payload->>'distinct_size')::int) AS num_commits
FROM github_events
WHERE event_type = 'PushEvent'
GROUP BY minute
ORDER BY minute;Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: remote_scan.minute
-> HashAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.minute
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0)
Task Count: 32
Tasks Shown: All
-> Task
Node: host=localhost port=5433 dbname=postgres
-> HashAggregate (cost=93.42..98.36 rows=395 width=16)
Group Key: date_trunc('minute'::text, created_at)
-> Seq Scan on github_events_102042 github_events (cost=0.00..88.20 rows=418 width=503)
Filter: (event_type = 'PushEvent'::text)
-> Task
Node: host=localhost port=5434 dbname=postgres
-> HashAggregate (cost=103.21..108.57 rows=429 width=16)
Group Key: date_trunc('minute'::text, created_at)
-> Seq Scan on github_events_102043 github_events (cost=0.00..97.47 rows=459 width=492)
Filter: (event_type = 'PushEvent'::text)
--
-- ... repeats for all 32 tasks
-- alternating between workers one and two
-- (running in this case locally on ports 5433, 5434)
--
(199 rows)Причинами отличий в выполнении запросов рабочими узлами могут быть отличия в настройке конфигурации, неравномерное распределение данных по сегментам или различное оборудование на машинах. Чтобы получить дополнительную информацию о времени выполнения запроса на каждом сегменте, можно использовать команду EXPLAIN ANALYZE.
Примечание
Обратите внимание, что когда включён параметр citus.explain_all_tasks, планы команды EXPLAIN извлекаются последовательно, что может занять много времени при использовании EXPLAIN ANALYZE.
Расширение citus по умолчанию сортирует задачи по времени выполнения в порядке убывания. Если параметр citus.explain_all_tasks отключён, citus показывает одну самую длительную задачу. Обратите внимание, что эту функцию можно использовать только с командой EXPLAIN ANALYZE, поскольку обычная команда EXPLAIN не выполняет запросы и, следовательно, не может показать время их выполнения. Чтобы изменить порядок сортировки, используйте параметр конфигурации citus.explain_analyze_sort_method.
J.5.10.1.3. Масштабирование производительности #
Как уже упоминалось, как только достигнута желаемая производительность для одного сегмента, можно установить аналогичные параметры конфигурации для всех рабочих узлов. Поскольку citus выполняет все запросы-фрагменты на рабочих узлах параллельно, пользователи могут масштабировать производительность своих запросов так, чтобы использовалась совокупная вычислительная мощность всех ядер ЦП в кластере, предполагая, что данные могут поместиться в памяти.
Пользователи должны постараться разместить в памяти как можно большую часть своего рабочего набора данных, чтобы добиться максимальной производительности citus. Если размещение всего набора в памяти невозможно, рекомендуется использовать SSD-накопители вместо HDD. Это связано с тем, что HDD способны показывать достойную производительность при последовательном чтении смежных блоков данных, но имеют значительно меньшую производительность при чтении/записи в случайных местах. Когда выполняется большое количество одновременных запросов, содержащих случайные операции чтения и записи, использование SSD может повысить производительность запросов в несколько раз по сравнению с HDD. Кроме того, если запросы требуют большой вычислительной мощности рекомендуется выбирать машины с более мощными процессорами.
Чтобы измерить использование дискового пространства объектами вашей базы данных, можно авторизоваться на рабочих узлах и использовать функции для системного администрирования Postgres Pro на отдельных сегментах. Чтобы узнать общий объём дискового пространства, используемый таблицей, можно вызвать функцию pg_total_relation_size. Также можно использовать другие функции, упомянутые в документации Postgres Pro, чтобы получить более точную информацию. На основе этой статистики для сегмента и количества сегментов пользователи могут рассчитать требования к оборудованию для своего кластера.
На производительность также влияет количество сегментов на каждом рабочем узле. Расширение citus разделяет входящий запрос на запросы-фрагменты, которые выполняются на отдельных рабочих узлах. Следовательно, степень параллелизма для каждого запроса определяется количеством сегментов, к которым обращается запрос. Чтобы обеспечить максимальное распараллеливание, следует создать достаточное количество сегментов на каждом узле, чтобы на каждое ядро ЦП приходился хотя бы один сегмент. Также следует помнить, что citus будет отсекать несвязанные фрагменты, если в запросе есть фильтры по столбцу распределения. Таким образом, даже если количество сегментов больше, чем количество ядер, отсечение сегментов поможет достичь более высокого уровня параллелизма.
J.5.10.1.4. Настройка производительности распределённых запросов #
После распределения данных по кластеру и оптимизации каждого рабочего узла для максимальной производительности станет заметен большой прирост производительности запросов. Последним этапом является настройка нескольких параметров распределённой производительности.
Прежде чем рассмотреть конкретные параметры конфигурации, рекомендуется измерить и сравнить длительность запросов в распределённом кластере с производительностью одного сегмента. Сделать это можно, сравнив выполнение запроса на узле-координаторе и одного из фрагментов-запросов на рабочем узле. Это поможет вычислить время, затраченное на рабочих узлах на запросы, а также на доставку данных в узел-координатор. Затем можно обнаружить узкое место и соответствующим образом оптимизировать базу данных.
В этом разделе рассматриваются параметры оптимизации планировщика и исполнителя распределённых запросов. Существует несколько соответствующих параметров, и они рассматриваются в двух разделах: общая настройка производительности и расширенная настройка производительности. Для большинства случаев достаточно ознакомиться с первым разделом, поскольку он охватывает все распространённые конфигурации. Во втором разделе описаны параметры для повышения производительности в особых сценариях использования.
J.5.10.1.4.1. Общая настройка производительности #
Уровень распараллеливания больше всего влияет на скорость и производительность оператора INSERT. Попробуйте выполнить несколько операторов INSERT одновременно. Таким образом, можно достичь очень высокой скорости вставки при наличии мощного узла-координатора и совместном использовании всех ядер ЦП на этом узле.
J.5.10.1.4.1.1. Сетевые издержки подзапросов/CTE #
В лучшем случае citus может выполнять запросы, содержащие подзапросы и CTE, за один этап. Обычно это происходит потому, что и основной запрос, и подзапрос фильтруются по столбцу распределения таблиц одинаковым образом и могут быть вместе переданы на рабочие узлы. Но иногда расширение citus вынуждено выполнять подзапросы до выполнения основного запроса, копируя результаты промежуточного подзапроса на другие рабочие узлы для использования в основном запросе. Этот метод называется двухэтапное выполнение подзапросов/CTE.
Важно помнить, что подзапросы выполняются на отдельном этапе, и не отправлять слишком большой объём данных между рабочими узлами. Нагрузка на сеть влияет на производительность. Команда EXPLAIN позволяет узнать, как будут выполняться запросы, в том числе потребуется ли несколько этапов. За подробным примером обратитесь к разделу Двухэтапное выполнение подзапросов/CTE.
Также можно избежать получения больших промежуточных результатов. Настройте ограничение citus.max_intermediate_result_size в новом подключении с узлом-координатором. По умолчанию максимальный размер промежуточного результата составляет 1 ГБ, что допускает выполнение некоторых неэффективных запросов. Попробуйте уменьшить это значение и выполнить запросы:
-- Задайте ограничение для промежуточных результатов SET citus.max_intermediate_result_size = '512kB'; -- Попробуйте выполнить запросы -- SELECT …
Если в запросе есть подзапросы или CTE, превышающие этот предел, он будет отменён с выводом сообщения об ошибке:
ERROR: the intermediate result size exceeds citus.max_intermediate_result_size (currently 512 kB) DETAIL: Citus restricts the size of intermediate results of complex subqueries and CTEs to avoid accidentally pulling large result sets into once place. HINT: To run the current query, set citus.max_intermediate_result_size to a higher value or -1 to disable.
Размер промежуточных результатов и их пункт назначения показываются в выводе команды EXPLAIN ANALYZE:
EXPLAIN ANALYZE WITH deleted_rows AS ( DELETE FROM page_views WHERE tenant_id IN (3, 4) RETURNING * ), viewed_last_week AS ( SELECT * FROM deleted_rows WHERE view_time > current_timestamp - interval '7 days' ) SELECT count(*) FROM viewed_last_week;
Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0) (actual time=570.076..570.077 rows=1 loops=1)
-> Distributed Subplan 31_1
Subplan Duration: 6978.07 ms
Intermediate Data Size: 26 MB
Result destination: Write locally
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0) (actual time=364.121..364.122 rows=0 loops=1)
Task Count: 2
Tuple data received from nodes: 0 bytes
Tasks Shown: One of 2
-> Task
Tuple data received from node: 0 bytes
Node: host=localhost port=5433 dbname=postgres
-> Delete on page_views_102016 page_views (cost=5793.38..49272.28 rows=324712 width=6) (actual time=362.985..362.985 rows=0 loops=1)
-> Bitmap Heap Scan on page_views_102016 page_views (cost=5793.38..49272.28 rows=324712 width=6) (actual time=362.984..362.984 rows=0 loops=1)
Recheck Cond: (tenant_id = ANY ('{3,4}'::integer[]))
-> Bitmap Index Scan on view_tenant_idx_102016 (cost=0.00..5712.20 rows=324712 width=0) (actual time=19.193..19.193 rows=325733 loops=1)
Index Cond: (tenant_id = ANY ('{3,4}'::integer[]))
Planning Time: 0.050 ms
Execution Time: 363.426 ms
Planning Time: 0.000 ms
Execution Time: 364.241 ms
Task Count: 1
Tuple data received from nodes: 6 bytes
Tasks Shown: All
-> Task
Tuple data received from node: 6 bytes
Node: host=localhost port=5432 dbname=postgres
-> Aggregate (cost=33741.78..33741.79 rows=1 width=8) (actual time=565.008..565.008 rows=1 loops=1)
-> Function Scan on read_intermediate_result intermediate_result (cost=0.00..29941.56 rows=1520087 width=0) (actual time=326.645..539.158 rows=651466 loops=1)
Filter: (view_time > (CURRENT_TIMESTAMP - '7 days'::interval))
Planning Time: 0.047 ms
Execution Time: 569.026 ms
Planning Time: 1.522 ms
Execution Time: 7549.308 msВ приведённом выше выводе команды EXPLAIN ANALYZE можно увидеть следующую информацию о промежуточных результатах:
Intermediate Data Size: 26 MB Result destination: Write locally
Виден объём промежуточных результатов и куда они были записаны. В данном случае — на узел, координирующий выполнение запроса, как указано в параметре write locally. Для некоторых других запросов информация может также иметь следующий формат:
Intermediate Data Size: 26 MB Result destination: Send to 2 nodes
Это означает, что промежуточный результат был передан на 2 рабочих узла и потребовалось больше сетевого трафика.
При использовании CTE или соединений CTE и распределённых таблиц можно избежать двухэтапного выполнения, следуя этим правилам:
Таблицы должны быть совмещёнными.
Для CTE-запросов не должно требоваться каких-либо шагов слияния (например
LIMITилиGROUP BYдля нераспределённого ключа).Таблицы и CTE следует соединять по ключам распределения.
Кроме того, Postgres Pro позволяет citus использовать встраивание CTE для передачи CTE рабочим узлам в большем количестве случаев. Поведением встраивания можно управлять с помощью ключевого слова MATERIALIZED. За дополнительной информацией обратитесь к разделу Запросы WITH (Общие табличные выражения).
J.5.10.1.4.2. Расширенная настройка производительности #
В этом разделе рассматриваются расширенные параметры настройки производительности. Эти параметры применимы к особым сценариям использования и могут не потребоваться для всех развёртываний.
J.5.10.1.4.2.1. Управление подключениями #
При выполнении многосегментных запросов citus должен сбалансировать выгоды от распараллеливания с издержками подключений к базам данных. Раздел Выполнение запросов объясняет этапы преобразования запросов в задания рабочих узлов и установки подключений к базе данных для рабочих узлов.
Установите для параметра конфигурации citus.max_adaptive_executor_pool_size небольшое значение, например
1или2для сценария транзакционных нагрузок с короткими запросами (например, задержка < 20 мс). Для аналитических нагрузок, где распараллеливание имеет решающее значение, оставьте для этого параметра значение по умолчанию —16.Установите для параметра конфигурации citus.executor_slow_start_interval большое значение, например
100мс, для сценария использования с транзакционными нагрузками, состоящими из коротких запросов, которые зависят от пропускной способности сети, а не от степени распараллеливания. Для аналитических нагрузок оставьте для этого параметра значение по умолчанию, равное10мс.Значение по умолчанию
1для параметра конфигурации citus.max_cached_conns_per_worker вполне целесообразно. Большее значение, например2, может подойти для кластеров, которые используют небольшое количество параллельных сеансов, но не стоит его сильно увеличивать (например,16будет слишком большим). Если установлено слишком большое значение, сеансы будут удерживать бездействующие соединения и без необходимости использовать ресурсы рабочих узлов.Установите параметр конфигурации citus.max_shared_pool_size в соответствии с параметрами max_connections рабочих узлов. Этот параметр необходим для обеспечения отказоустойчивости.
J.5.10.1.4.2.2. Политика назначения заданий #
Планировщик запросов citus назначает задания рабочим узлам на основе размещения сегментов. Алгоритм выполнения этих назначений можно выбрать с помощью параметра конфигурации citus.task_assignment_policy. Пользователи могут изменить этот параметр конфигурации, чтобы выбрать наиболее подходящую для конкретного сценария использования политику.
Политика greedy направлена на равномерное распределение заданий между рабочими узлами. Эта политика используется по умолчанию и хорошо работает в большинстве случаев. Политика round-robin назначает задачи рабочим узлам по порядку, чередуя разные реплики. Она обеспечивает гораздо лучшее использование кластера, когда количество сегментов таблицы невелико по сравнению с количеством рабочих процессов. Третья политика — first-replica — распределяет задачи на основе порядка вставки размещений (реплик) для сегментов. Эта политика позволяет гарантировать, что определённые сегменты будут использоваться на определённых узлах. Это помогает обеспечить более строгие гарантии сохранения объёма памяти, то есть хранить рабочий набор данных в памяти и использовать его для запросов.
J.5.10.1.4.2.3. Двоичный протокол #
В некоторых случаях большая часть времени запроса тратится на отправку результатов запроса от рабочих узлов узлу-координатору. Чаще всего это происходит, когда запросы запрашивают много строк (например, SELECT * FROM table) или когда столбцы результатов используют типы больших данных (например, hll или tdigest) из расширений hll и tdigest).
В таких случаях бывает полезно установить для citus.enable_binary_protocol значение true, что изменит кодировку результатов с текстовой на двоичную. Двоичная кодировка значительно снижает пропускную способность для типов, имеющих компактное двоичное представление, таких как hll, tdigest, timestamp и double precision. По умолчанию для этого параметра конфигурации уже установлено значение true, поэтому его можно не включать явно.
J.5.10.1.5. Масштабирование поглощения данных #
Расширение citus позволяет значительно масштабировать поглощение данных, но необходимо учитывать несколько компромиссов с точки зрения интеграции приложений, производительности и задержек. В этом разделе рассматриваются различные подходы к поглощению данных и даются рекомендации по ожидаемой производительности и задержкам.
J.5.10.1.5.1. Добавление и изменение данных в реальном времени #
На узле-кооррдинаторе citus можно выполнять команды INSERT, INSERT .. ON CONFLICT, UPDATE и DELETE напрямую для распределённых таблиц. При использовании одной из этих команд изменения сразу видны пользователю.
Когда выполняется INSERT (или другая команда вставки), citus сначала находит правильное размещение сегментов на основе значения в столбце распределения. Затем citus подключается к рабочим узлам, на которых находятся размещения сегментов, и выполняет INSERT для каждого из них. Со стороны пользователя обработка INSERT занимает несколько миллисекунд из-за сетевой задержки для рабочих узлов. Однако узел-координатор citus может обрабатывать несколько команд INSERT одновременно для повышения производительности.
J.5.10.1.5.2. Временное хранение данных #
При загрузке данных для временного хранения рекомендуется использовать нежурналируемые таблицы. Это таблицы, которые игнорируются журналом предзаписи Postgres Pro. При этом ускоряется вставка строк, но такой вариант не подходит для долгосрочного хранения данных. Можно использовать нежурналируемые таблицы для загрузки входящих данных перед их изменением и перемещением в постоянные таблицы.
-- Пример нежурналируемой таблицы
CREATE UNLOGGED TABLE unlogged_table (
key text,
value text
);
-- Её сегменты также будут нежурналируемыми даже
-- после распределения таблицы
SELECT create_distributed_table('unlogged_table', 'key');
-- Можно загружать данныеJ.5.10.1.5.3. Массовое копирования (250 000 - 2 000 000 строк в секунду) #
Для распределённых таблиц поддерживается команда COPY от узла-координатора citus для массового поглощения, что позволяет добиться гораздо более высоких показателей поглощения, чем с помощью операторов INSERT.
Команду COPY можно использовать для загрузки данных непосредственно из приложения с помощью COPY .. FROM STDIN, из файла на сервере или программы, выполняемой на сервере.
COPY pgbench_history FROM STDIN WITH (FORMAT CSV);
В psql команду \copy можно использовать для загрузки данных с локальной машины. Команда \COPY фактически отправляет команду COPY .. FROM STDIN на сервер перед отправкой локальных данных, как и приложение, которое загружает данные напрямую.
psql -c "\COPY pgbench_history FROM 'pgbench_history-2016-03-04.csv' (FORMAT CSV)"
Преимущества команды COPY для распределённых таблиц заключаются в асинхронном копировании данных на рабочие узлы через множество параллельных подключений, по одному для каждого размещения сегмента. Это означает, что данные могут приниматься с параллельным использованием нескольких рабочих узлов и нескольких ядер. При наличии дорогих индексов, таких как GIN, это может значительно повысить производительность по сравнению с поглощением обычной таблицей Postgres Pro.
Ожидаемая производительность при использовании команды COPY — поглощение от 250 000 до 2 000 000 строк в секунду.
Примечание
Проверьте настройку параметров тестирования, чтобы получить оптимальную производительность COPY. Следуйте этим советам:
Рекомендуется задать большой размер порции (~ 50 000-100 000). Можно протестировать несколько файлов (1, 10, 1000, 10 000 и т. д.), каждый из которых соответствует размеру порции.
Используйте параллельное поглощение. Увеличьте количество потоков/поглотителей до 2, 4, 8, 16 и запустите тесты.
Используйте узел-координатор, оптимизированный для вычислений. Для рабочих узлов выбирайте машины с большим объёмом ОЗУ и количеством ЦП.
Выбирайте относительно небольшое количество сегментов: 32 должно быть достаточно, но также можно провести тесты с 64.
Поглощайте данные в течение подходящего периода времени (например, за 2, 4, 8, 24 часа). Чем дольше проходят тесты, тем более они показательны для производственной установки.
J.5.10.2. Полезные диагностические запросы #
J.5.10.2.1. Поиск сегмента, содержащего данные конкретного арендатора #
Строки распределённой таблицы группируются в сегменты, и каждый сегмент размещается на рабочем узле в кластере citus. В многоарендном варианте использования citus можно определить, какой рабочий узел содержит строки конкретного арендатора, соединив две части информации: shard_id, связанный с tenant_id, и размещения сегментов на рабочих узлах. Обе части можно получить в одном запросе. Предположим, что арендаторы нашего многоарендного приложения — магазины, и нужно определить, какой рабочий узел содержит данные для Gap.com (предположим, id=4).
Чтобы найти рабочий узел, содержащий данные для магазина id=4, запросите размещение строк, столбец распределения которых имеет значение 4:
SELECT shardid, shardstate, shardlength, nodename, nodeport, placementid
FROM pg_dist_placement AS placement,
pg_dist_node AS node
WHERE placement.groupid = node.groupid
AND node.noderole = 'primary'
AND shardid = (
SELECT get_shard_id_for_distribution_column('stores', 4)
);Результат содержит имя и порт базы данных рабочего узла.
┌─────────┬────────────┬─────────────┬───────────┬──────────┬─────────────┐ │ shardid │ shardstate │ shardlength │ nodename │ nodeport │ placementid │ ├─────────┼────────────┼─────────────┼───────────┼──────────┼─────────────┤ │ 102009 │ 1 │ 0 │ localhost │ 5433 │ 2 │ └─────────┴────────────┴─────────────┴───────────┴──────────┴─────────────┘
J.5.10.2.2. Поиск узла, на котором размещена распределённая схема #
Распределённые схемы автоматически связываются с отдельными группами совмещения, так что таблицы, созданные в этих схемах, преобразуются в совмещённые распределённые таблицы без ключа сегментирования. Чтобы узнать, где находится распределённая схема, соедините представление citus_shards с представлением citus_schemas:
SELECT schema_name, nodename, nodeport
FROM citus_shards
JOIN citus_schemas cs
ON cs.colocation_id = citus_shards.colocation_id
GROUP BY 1,2,3;schema_name | nodename | nodeport -------------+-----------+---------- a | localhost | 9701 b | localhost | 9702 with_data | localhost | 9702
Также можно отправить запрос напрямую citus_shards, применив фильтр по типу таблиц схемы, чтобы получить подробный список всех таблиц.
SELECT * FROM citus_shards WHERE citus_table_type = 'schema';
table_name | shardid | shard_name | citus_table_type | colocation_id | nodename | nodeport | shard_size | schema_name | colocation_id | schema_size | schema_owner ----------------+---------+-----------------------+------------------+---------------+-----------+----------+------------+-------------+---------------+-------------+-------------- a.cities | 102080 | a.cities_102080 | schema | 4 | localhost | 9701 | 8192 | a | 4 | 128 kB | citus a.map_tags | 102145 | a.map_tags_102145 | schema | 4 | localhost | 9701 | 32768 | a | 4 | 128 kB | citus a.measurement | 102047 | a.measurement_102047 | schema | 4 | localhost | 9701 | 0 | a | 4 | 128 kB | citus a.my_table | 102179 | a.my_table_102179 | schema | 4 | localhost | 9701 | 16384 | a | 4 | 128 kB | citus a.people | 102013 | a.people_102013 | schema | 4 | localhost | 9701 | 32768 | a | 4 | 128 kB | citus a.test | 102008 | a.test_102008 | schema | 4 | localhost | 9701 | 8192 | a | 4 | 128 kB | citus a.widgets | 102146 | a.widgets_102146 | schema | 4 | localhost | 9701 | 32768 | a | 4 | 128 kB | citus b.test | 102009 | b.test_102009 | schema | 5 | localhost | 9702 | 8192 | b | 5 | 32 kB | citus b.test_col | 102012 | b.test_col_102012 | schema | 5 | localhost | 9702 | 24576 | b | 5 | 32 kB | citus with_data.test | 102180 | with_data.test_102180 | schema | 11 | localhost | 9702 | 647168 | with_data | 11 | 632 kB | citus
J.5.10.2.3. Поиск столбца распределения таблицы #
У каждой распределённой таблицы в citus есть «столбец распределения». За дополнительной информацией обратитесь к разделу Выбор столбца распределения. Существует множество ситуаций, когда важно знать, какой именно это столбец. Некоторые операции требуют соединения или фильтрации по столбцу распределения, и можно столкнуться с сообщениями об ошибках, содержащими подсказки, например add a filter to the distribution column (добавьте фильтр по столбцу распределения).
Таблицы pg_dist_* на узле-координаторе содержат разнообразные метаданные о распределённой базе данных. В частности, таблица pg_dist_partition содержит информацию о столбце распределения (ранее называвшемся столбцом partition) каждой таблицы. Можно использовать удобную служебную функцию для поиска имени столбца распределения по низкоуровневым сведениям в метаданных. Пример этой функции:
-- Создайте таблицу для примера
CREATE TABLE products (
store_id bigint,
product_id bigint,
name text,
price money,
CONSTRAINT products_pkey PRIMARY KEY (store_id, product_id)
);
-- Выберите store_id в качестве столбца распределения
SELECT create_distributed_table('products', 'store_id');
-- Узнайте имя столбца распределения для таблицы products
SELECT column_to_column_name(logicalrelid, partkey) AS dist_col_name
FROM pg_dist_partition
WHERE logicalrelid='products'::regclass;Пример вывода:
┌───────────────┐ │ dist_col_name │ ├───────────────┤ │ store_id │ └───────────────┘
J.5.10.2.4. Обнаружение блокировок #
Этот запрос будет выполняться на всех рабочих узлах и обнаруживать блокировки, их длительность и вызвавшие их запросы:
SELECT * FROM citus_lock_waits;
За дополнительной информацией обратитесь к разделу Активность распределённых запросов.
J.5.10.2.5. Определение размеров пользовательских сегментов #
Этот запрос вернёт размер каждого фрагмента данной распределённой таблицы, обозначенной здесь шаблоном моя_таблица:
SELECT shardid, table_name, shard_size
FROM citus_shards
WHERE table_name = 'моя_таблица';Пример вывода:
. shardid | table_name | shard_size ---------+------------+------------ 102170 | my_table | 90177536 102171 | my_table | 90177536 102172 | my_table | 91226112 102173 | my_table | 90177536
Этот запрос использует представление citus_shards.
J.5.10.2.6. Определение размеров всех распределённых таблиц #
Этот запрос возвращает список размеров распределённых таблиц вместе с индексами.
SELECT table_name, table_size FROM citus_tables;
Пример вывода:
┌───────────────┬────────────┐ │ table_name │ table_size │ ├───────────────┼────────────┤ │ github_users │ 39 MB │ │ github_events │ 98 MB │ └───────────────┴────────────┘
Существуют и другие способы получения размеров распределённых таблиц. За дополнительной информацией обратитесь к разделу Определение размера отношения.
J.5.10.2.7. Определение неиспользуемых индексов #
Этот запрос будет выполняться на всех рабочих узлах и определять все неиспользуемые индексы для заданной распределённой таблицы, обозначенной здесь шаблоном моя_распределённая_таблица:
SELECT *
FROM run_command_on_shards('моя_распределённая_таблица', $cmd$
SELECT array_agg(a) as infos
FROM (
SELECT (
schemaname || '.' || relname || '##' || indexrelname || '##'
|| pg_size_pretty(pg_relation_size(i.indexrelid))::text
|| '##' || idx_scan::text
) AS a
FROM pg_stat_user_indexes ui
JOIN pg_index i
ON ui.indexrelid = i.indexrelid
WHERE NOT indisunique
AND idx_scan < 50
AND pg_relation_size(relid) > 5 * 8192
AND (schemaname || '.' || relname)::regclass = '%s'::regclass
ORDER BY
pg_relation_size(i.indexrelid) / NULLIF(idx_scan, 0) DESC nulls first,
pg_relation_size(i.indexrelid) DESC
) sub
$cmd$);Пример вывода:
┌─────────┬─────────┬─────────────────────────────────────────────────────────────────────────────────┐
│ shardid │ success │ result │
├─────────┼─────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ 102008 │ t │ │
│ 102009 │ t │ {"public.my_distributed_table_102009##stupid_index_102009##28 MB##0"} │
│ 102010 │ t │ │
│ 102011 │ t │ │
└─────────┴─────────┴─────────────────────────────────────────────────────────────────────────────────┘J.5.10.2.8. Мониторинг количества клиентских подключений #
Этот запрос вернёт количество открытых на узле-координаторе подключений по каждому типу:
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
Пример вывода:
┌────────┬───────┐ │ state │ count │ ├────────┼───────┤ │ active │ 3 │ │ ∅ │ 1 │ └────────┴───────┘
J.5.10.2.9. Просмотр системных запросов #
J.5.10.2.9.1. Активные запросы #
В представлении citus_stat_activity показаны запросы, выполняющиеся в данный момент. Их можно отфильтровать, чтобы найти активно выполняющиеся процессы, а также идентификатор их сервера:
SELECT global_pid, query, state FROM citus_stat_activity WHERE state != 'idle';
J.5.10.2.9.2. Причины ожидания запросов #
Также можно запросить информацию о наиболее распространённых причинах ожидания активных запросов. Причины такого поведения описаны в таблице Типы событий ожидания.
SELECT wait_event || ':' || wait_event_type AS type, count(*) AS number_of_occurences FROM pg_stat_activity WHERE state != 'idle' GROUP BY wait_event, wait_event_type ORDER BY number_of_occurences DESC;
Пример вывода при параллельном выполнении функции pg_sleep в отдельном запросе:
┌─────────────────┬──────────────────────┐ │ type │ number_of_occurences │ ├─────────────────┼──────────────────────┤ │ ∅ │ 1 │ │ PgSleep:Timeout │ 1 │ └─────────────────┴──────────────────────┘
J.5.10.2.10. Коэффициент попадания в индекс #
Этот запрос показывает частоту попаданий в индекс по всем узлам. Коэффициент попадания в индекс полезен для определения частоты использования индексов в запросах:
-- На узле-координаторе
SELECT 100 * (sum(idx_blks_hit) - sum(idx_blks_read)) / sum(idx_blks_hit) AS index_hit_rate
FROM pg_statio_user_indexes;
-- На рабочих узлах
SELECT nodename, result as index_hit_rate
FROM run_command_on_workers($cmd$
SELECT 100 * (sum(idx_blks_hit) - sum(idx_blks_read)) / sum(idx_blks_hit) AS index_hit_rate
FROM pg_statio_user_indexes;
$cmd$);Пример вывода:
┌───────────┬────────────────┐ │ nodename │ index_hit_rate │ ├───────────┼────────────────┤ │ 10.0.0.16 │ 96.0 │ │ 10.0.0.20 │ 98.0 │ └───────────┴────────────────┘
J.5.10.2.11. Коэффициент попадания в кеш #
Большинство приложений обычно обращаются к небольшой части своих данных за раз. Postgres Pro сохраняет часто используемые данные в памяти, чтобы избежать медленного чтения с диска. За соответствующей статистикой можно обратиться к представлению pg_statio_user_tables.
Важный показатель — какой процент данных в пользовательской нагрузке поступает из кеша памяти, а какой — с диска:
-- На узле-координаторе
SELECT
sum(heap_blks_read) AS heap_read,
sum(heap_blks_hit) AS heap_hit,
100 * sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS cache_hit_rate
FROM
pg_statio_user_tables;
-- На рабочих узлах
SELECT nodename, result as cache_hit_rate
FROM run_command_on_workers($cmd$
SELECT
100 * sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS cache_hit_rate
FROM
pg_statio_user_tables;
$cmd$);Пример вывода:
┌───────────┬──────────┬─────────────────────┐ │ heap_read │ heap_hit │ cache_hit_rate │ ├───────────┼──────────┼─────────────────────┤ │ 1 │ 132 │ 99.2481203007518796 │ └───────────┴──────────┴─────────────────────┘
Если процент значительно ниже 99%, вероятно, стоит подумать об увеличении доступной для базы данных кеш-памяти.
J.5.10.3. Распространённые сообщения об ошибках #
J.5.10.3.1. could not connect to server: Connection refused #
Вызывается, когда узел-координатор не может подключиться к рабочему узлу.
SELECT 1 FROM companies WHERE id = 2928;
ERROR: connection to the remote node localhost:5432 failed with the following error: could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP/IP connections on port 5432?J.5.10.3.1.1. Решение #
Чтобы исправить эту ошибку, проверьте, что рабочий узел принимает подключения и что DNS правильно разрешается.
J.5.10.3.2. canceling the transaction since it was involved in a distributed deadlock #
Взаимные блокировки могут возникать не только в одноузловой, но и в распределённой базе данных, из-за выполнения запросов на нескольких узлах. Расширение citus может обнаруживать распределённые взаимоблокировки и устранять их путём прерывания одного из задействованных запросов.
Можно увидеть взаимоблокировки в действии, распределив строки по рабочим узлам, а затем выполнив две параллельные транзакции с конфликтующими изменениями:
CREATE TABLE lockme (id int, x int);
SELECT create_distributed_table('lockme', 'id');
-- id=1 идёт на один рабочий узел, а id=2 на другой
INSERT INTO lockme VALUES (1,1), (2,2);
--------------- TX 1 ---------------- --------------- TX 2 ----------------
BEGIN;
BEGIN;
UPDATE lockme SET x = 3 WHERE id = 1;
UPDATE lockme SET x = 4 WHERE id = 2;
UPDATE lockme SET x = 3 WHERE id = 2;
UPDATE lockme SET x = 4 WHERE id = 1;ERROR: canceling the transaction since it was involved in a distributed deadlock
J.5.10.3.2.1. Решение #
Обнаружение и устранение взаимоблокировок являются частью обычной обработки распределённых транзакций. Этот механизм позволяет приложению повторять запросы или выполнять другие действия.
J.5.10.3.3. could not connect to server: Cannot assign requested address #
WARNING: connection error: localhost:9703 DETAIL: could not connect to server: Cannot assign requested address
Это происходит при отсутствии доступных сокетов, с помощью которых узел-координатор может отвечать на запросы рабочих узлов.
J.5.10.3.3.1. Решение #
Настройте операционную систему для повторного использования TCP-сокетов. Выполните в оболочке на узле-координаторе:
sysctl -w net.ipv4.tcp_tw_reuse=1
Это позволяет повторно использовать сокеты в состоянии TIME_WAIT для новых подключений, когда это безопасно с точки зрения протокола. Значение по умолчанию — 0 (отключено).
J.5.10.3.4. SSL error: certificate verify failed #
В citus узлы по умолчанию должны взаимодействовать друг с другом по протоколу SSL. Если протокол SSL не был включён на сервере Postgres Pro, он будет включён в процессе первой установки citus, то есть будет создан и самоподписан сертификат SSL.
Однако если существует файл корневого сертификата центра сертификации (обычно в ~/.postgresql/root.crt), то проверка сертификата на соответствие этому центру сертификации во время подключения закончится неудачей.
J.5.10.3.4.1. Решение #
Возможные решения — подписать сертификат, отключить протокол SSL или удалить корневой сертификат. Также у узла могут возникнуть проблемы с подключением к самому себе без помощи citus.local_hostname.
J.5.10.3.5. could not connect to any active placements #
Если все доступные слоты подключений рабочих узлов будут использованы, дальнейшие подключения не будут устанавливаться.
WARNING: connection error: hostname:5432 ERROR: could not connect to any active placements
J.5.10.3.5.1. Решение #
Эта ошибка чаще всего возникает при параллельном копировании данных в citus. Команда COPY открывает одно подключение для каждого сегмента. Если запустить M одновременных операций копирования в место назначения с N сегментами, это приведёт к созданию M*N подключений. Чтобы устранить эту ошибку, уменьшите количество сегментов целевых распределённых таблиц или запускайте меньше команд \copy параллельно.
J.5.10.3.6. remaining connection slots are reserved for non-replication superuser connections #
Эта ошибка возникает, когда у Postgres Pro заканчиваются доступные подключения для обслуживания одновременных клиентских запросов.
J.5.10.3.6.1. Решение #
Это ограничение настраивается параметром конфигурации max_connections и по умолчанию обычно составляет 100 подключений. Обратите внимание, что каждое подключение потребляет ресурсы, поэтому задавайте значение обдуманно. При увеличении значения max_connections обычно рекомендуется также увеличить значения ограничений памяти.
Программа pgbouncer также помогает в упорядочивании запросов на подключение, превышающих заданное ограничение.
J.5.10.3.7. pgbouncer cannot connect to server #
В локальном кластере citus эта ошибка указывает на то, что узел-координатор не отвечает на запросы pgbouncer.
J.5.10.3.7.1. Решение #
Попробуйте подключиться напрямую к серверу с помощью psql, чтобы убедиться, что он работает и принимает подключения.
J.5.10.3.8. creating unique indexes on non-partition columns is currently unsupported #
Поскольку citus — распределённая система, уникальность может гарантироваться только в том случае, если ограничение уникального индекса или первичного ключа включает столбец распределения таблицы. Это связано с разбиением сегментов таким образом, что каждый сегмент содержит непересекающиеся значения столбцов секционирования. Для индекса на каждом рабочем узле может локально применяться своя часть ограничения.
Попытка создать уникальный индекс для столбца, не являющегося столбцом распределения, вызовет ошибку:
ERROR: creating unique indexes on non-partition columns is currently unsupported
Обеспечение уникальности столбца, не являющегося столбцом распределения, потребует от citus проверки каждого сегмента при каждом выполнении команды INSERT, что мешает масштабируемости.
J.5.10.3.8.1. Решение #
Существует два способа обеспечить уникальность столбца, не являющегося столбцом распределением:
Создайте составной уникальный индекс или первичный ключ, включающий нужный столбец (C), а также столбец распределения (D). Это не такое строгое условие, как уникальность только для C, но оно гарантирует, что значения C уникальны для каждого значения D. Например, при распределении по
company_idв многоарендной системе такой подход сделает C уникальным внутри каждой компании.Используйте таблицы-справочники, а не таблицы, распределённые по хешу. Этот совет подходит только для небольших таблиц, так как содержимое таблицы-справочника будет дублироваться на всех узлах.
J.5.10.3.9. function create_distributed_table does not exist #
SELECT create_distributed_table('foo', 'id');
/*
ERROR: function create_distributed_table(unknown, unknown) does not exist
LINE 1: SELECT create_distributed_table('foo', 'id');
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
*/J.5.10.3.9.1. Решение #
Если основные вспомогательные функции недоступны, проверьте правильность установки расширения citus. При выполнении команды \dx в psql будет показан список установленных расширений.
Один из способов избавиться от расширений — создать новую базу данных на сервере Postgres Pro, для которой потребуется переустановить расширения. Чтобы сделать это правильно, ознакомьтесь с разделом Создание новой базы данных.
J.5.10.3.10. STABLE functions used in UPDATE queries cannot be called with column references #
Каждая функция Postgres Pro обладает характеристикой изменчивости, показывающей, может ли функция изменять базу данных и может ли возвращаемое функцией значение меняться с течением времени при тех же входных значениях. Функции с характеристикой STABLE гарантированно возвращают одинаковые результаты при одинаковых аргументах для всех строк в одном операторе, а функции с характеристикой IMMUTABLE всегда гарантированно возвращают одни и те же результаты при одинаковых аргументах.
Использование непостоянных функций в распределённых системах может быть нецелесообразно, поскольку при асинхронном запуске в разных сегментах результаты таких функций могут немного отличаться. Различия в конфигурации базы данных на разных узлах также могут пагубно влиять на непостоянные функции.
Один из наиболее распространённых способов вызвать эту ошибку — использовать метку timestamp в Postgres Pro, которая, в отличие от timestamptz, не содержит информации о часовом поясе. Интерпретация столбца меток времени ссылается на часовой пояс базы данных, который может измениться между запросами, поэтому функции, работающие с метками времени, не являются постоянными.
В citus запрещено выполнение распределённых запросов, фильтрующих результаты с использованием стабильных функций для столбцов. Например:
-- foo_timestamp имеет тип timestamp, а не timestamptz UPDATE foo SET ... WHERE foo_timestamp < now();
ERROR: STABLE functions used in UPDATE queries cannot be called with column references
В данном случае оператор сравнения < между timestamp и timestamptz не является постоянным.
J.5.10.3.10.1. Решение #
Не используйте стабильные функции для столбцов в распределённом операторе UPDATE. В частности, при работе со временем используйте timestamptz, а не timestamp. Наличие часового пояса в timestamptz делает вычисления постоянными.
J.5.11. Часто задаваемые вопросы #
J.5.11.1. Можно ли создавать первичные ключи в распределённых таблицах? #
В настоящее время в citus накладывается ограничение первичного ключа только в том случае, если столбец распределения является частью этого ключа. Таким образом, для гарантии уникальности ограничение необходимо проверять только на одном сегменте.
J.5.11.2. Как добавлять узлы в существующий кластер citus? #
В citus можно добавлять узлы вручную с помощью вызова функции citus_add_node с указанием адреса узла (или IP-адреса) и номера порта нового узла.
После добавления узла в существующий кластер новый узел не будет содержать никаких данных (сегментов). Все новые сегменты в citus будут назначаться этому узлу. Для перебалансировки существующих сегментов со старых узлов на новый узел в citus существует утилита перебалансировки сегментов с открытым исходным кодом. За дополнительной информацией обратитесь к разделу Перебалансировка сегментов без простоя.
J.5.11.3. Как расширение citus обрабатывает сбой рабочего узла? #
Расширение citus использует потоковую репликацию Postgres Pro для репликации всего рабочего узла как есть. Рабочие узлы реплицируются путём непрерывной потоковой передачи их записей WAL на резервный узел. Чтобы самостоятельно настроить потоковую репликацию, обратитесь к разделу Потоковая репликация.
J.5.11.4. Как расширение citus обрабатывает сбой узла-координатора? #
Поскольку узел-координатор citus аналогичен стандартному серверу Postgres Pro, для обеспечения более высокой доступности узла-координатора можно использовать обычную синхронную репликацию и отработку отказа Postgres Pro. За дополнительной информацией об обработке сбоев узла-координатора обратитесь к разделу Сбои узла-координатора.
J.5.11.5. Какие функции Postgres Pro не поддерживаются в citus? #
Поскольку citus является расширением Postgres Pro, он использует стандартные SQL-конструкции Postgres Pro. Поддерживается подавляющее большинство запросов, даже если они объединяют данные по сети из нескольких узлов базы данных, в том числе есть поддержка транзакционной семантики между узлами. Актуальный список поддержки функциональности SQL представлен в разделе Ограничения.
Более того, в citus обеспечена 100% поддержка SQL для запросов, обращающихся к одному узлу в кластере базы данных. Такие запросы часто встречаются в многоарендных приложениях, где на разных узлах хранятся данные разных арендаторов. За дополнительной информацией обратитесь к разделу Когда использовать citus.
Обратите внимание, что даже при такой обширной поддержке SQL моделирование данных может оказывать значительный эффект на производительность запросов. За более подробным описанием выполнения запросов в citus обратитесь к разделу Обработка запросов.
J.5.11.6. Как выбрать количество сегментов при секционировании данных по хешу? #
При первом распределении таблицы нужно выбрать для неё количество сегментов. Для каждой группы совмещения можно задать разные значения этого параметра, а оптимальное значение зависит от конкретного сценария использования. Изменить количество сегментов после создания кластера затруднительно, поэтому при выборе используйте следующие рекомендации.
В сценарии использования многоарендной базы данных SaaS рекомендуется выбирать между 32 и 128 сегментами. Для небольших нагрузок, например менее 100 ГБ, можно начать с 32 сегментов, а для более крупных — с 64 или 128. Таким образом можно масштабироваться с 32 до 128 машин рабочих узлов.
В сценарии использования аналитики в реальном времени количество сегментов должно быть связано с общим количеством ядер рабочих узлов. Чтобы обеспечить максимальный уровень распараллеливания, следует создать достаточное количество сегментов на каждом узле, чтобы на каждое ядро ЦП приходился хотя бы один сегмент. Обычно рекомендуется создать большое количество начальных сегментов, например в 2 или 4 раза больше текущего количества ядер ЦП. Это позволит выполнить масштабирование при добавлении новых рабочих узлов и ядер ЦП.
Чтобы выбрать количество сегментов для распределяемой таблицы, измените параметр конфигурации citus.shard_count. Это повлияет на будущие вызовы функции create_distributed_table. Например:
SET citus.shard_count = 64; -- у всех распределяемых таблиц теперь будет -- шестьдесят четыре сегмента
За более подробной информацией по этой теме обратитесь к разделу Выбор размера кластера.
J.5.11.7. Как изменить количество сегментов для разделённой по хешу таблицы? #
В citus есть функция alter_distributed_table, которая может изменять количество сегментов распределённой таблицы.
J.5.11.8. Как в citus реализованы запросы count(distinct)? #
В citus агрегатные функции count(distinct) могут вычисляться как на одном, так и на нескольких рабочих узлах. Если агрегатная функция count(distinct) выполняется по столбцу распределения, citus может отправлять вычисления на рабочие узлы и получать итоговые результаты. В противном случае отдельные строки передаются узлу-координатору и вычисления выполняются на нём. Если передача данных координатору обходится слишком дорого, доступны и быстрые приблизительные подсчёты. За дополнительной информацией обратитесь к разделу Агрегатные функции count(distinct).
J.5.11.9. В каких случаях поддерживаются ограничения уникальности для распределённых таблиц? #
Расширение citus может применять ограничение первичного ключа или уникальности только в том случае, если столбцы ограничения содержат столбец распределения. Это означает, что если один столбец является первичным ключом, он также должен быть столбцом распределения.
Это ограничение позволяет citus локализовать проверку уникальности в одном сегменте, а Postgres Pro — эффективно выполнять проверку на рабочем узле.
J.5.11.10. Как создавать роли, функции, расширения базы данных и т. п. в кластере citus? #
Некоторые команды, запускаемые на узле-координаторе, не транслируются на рабочие узлы:
CREATE ROLE/USERCREATE DATABASEALTER … SET SCHEMAALTER TABLE ALL IN TABLESPACECREATE TABLE(см. раздел Типы таблиц)
Все другие типы объектов, описанные выше, следует создать явно на всех узлах. В citus реализована функция для выполнения запросов на всех рабочих узлах:
SELECT run_command_on_workers($cmd$ /* the command to run */ CREATE ROLE ... $cmd$);
За более подробной информацией обратитесь к разделу Ручная трансляция запросов. Также обратите внимание, что даже после ручной трансляции команды CREATE DATABASE, всё равно необходимо установить на рабочих узлах citus. См. раздел Создание новой базы данных.
В будущем citus будет автоматически транслировать больше типов объектов. Преимущество автоматической трансляции заключается в том, что citus автоматически будет создавать копию объекта на всех добавляемых рабочих узлах (за дополнительной информацией обратитесь к citus.pg_dist_object).
J.5.11.11. Что произойдёт при изменении адреса рабочего узла? #
Если изменится адрес или IP-адрес рабочего узла, необходимо передать информацию об этом узлу-координатору с помощью функции citus_update_node:
-- Измените метаданные рабочего узла на узле-координаторе -- (не забудьте заменить «старый адрес» и «новый адрес» -- настоящими значениями) SELECT citus_update_node(nodeid, 'new-address', nodeport) FROM pg_dist_node WHERE nodename = 'old-address';
Пока не будет выполнено это изменение, узел-координатор не сможет отправлять запросы этому рабочему узлу.
J.5.11.12. В каком сегменте хранятся данные конкретного арендатора? #
В citus реализованы вспомогательные функции и таблицы метаданных для сопоставления значения столбца распределения с конкретным сегментом и определения размещения сегмента на рабочем узле. За дополнительной информацией обратитесь к разделу Поиск сегмента, содержащего данные конкретного арендатора.
J.5.11.13. Как найти столбец распределения таблицы? #
Эта информация хранится в таблицах метаданных узла-координатора citus. См. раздел Поиск столбца распределения таблицы.
J.5.11.14. Возможно ли распределение таблицы по нескольким ключам? #
Нет, нужно выбрать один столбец таблицы в качестве столбца распределения. Распространённый сценарий для распределения данных по двум столбцам — данные временных рядов. Однако в этом случае рекомендуется использовать хеш-распределение по столбцу, не связанному со временем, вместе с секционированием Postgres Pro по столбцу времени, как описано в разделе Данные временных рядов.
J.5.11.15. Почему функция pg_relation_size показывает нулевой размер в байтах для распределённой таблицы? #
Данные в распределённых таблицах хранятся на рабочих узлах (в сегментах), а не на узле-координаторе. Настоящий размер распределённой таблицы — это сумма размеров её сегментов. В citus доступны вспомогательные функции для получения этих сведений. За дополнительной информацией обратитесь к разделу Определение размера отношения.
J.5.11.16. Почему возникает ошибка, связанная с citus.max_intermediate_result_size? #
Для выполнения некоторых запросов, содержащих подзапросы или CTE, расширению citus требуется более одного этапа. Используя двухэтапное выполнение подзапросов/CTE, оно передаёт результаты подзапроса на все рабочие узлы для использования в основном запросе. Слишком большой размер этих результатов может привести к неприемлемой сетевой нагрузке или даже к недостатку места для хранения на узле-координаторе, который накапливает и распределяет их.
В citus есть настраиваемый параметр citus.max_intermediate_result_size, позволяющий указать размер результата подзапроса, при достижении которого запрос будет отменён. Возникающая при этом ошибка выглядит так:
ERROR: the intermediate result size exceeds citus.max_intermediate_result_size (currently 1 GB) DETAIL: Citus restricts the size of intermediate results of complex subqueries and CTEs to avoid accidentally pulling large result sets into once place. HINT: To run the current query, set citus.max_intermediate_result_size to a higher value or -1 to disable.
Как следует из сообщения об ошибке, можно увеличить предел, изменив значение переменной:
SET citus.max_intermediate_result_size = '3GB';
J.5.11.17. Возможно ли в citus сегментирование по схеме для многоарендных приложений? #
Да, сегментирование нс основе схем доступно.
J.5.11.18. Как citus работает с cstore_fdw? #
Расширение cstore_fdw не требуется в Postgres Pro 12 и более поздних версиях, поскольку столбцовое хранение теперь реализовано непосредственно в citus. В отличие от cstore_fdw, столбцовые таблицы citus поддерживают транзакционную семантику, репликацию и pg_upgrade. Распараллеливание запросов citus, сегментирование и отказоустойчивость эффективно сочетаются с высоким уровнем сжатия и скоростью ввода-вывода столбцового хранилища для архивирования больших наборов данных и составления отчётов.
J.5. citus — distributed database and columnar storage functionality #
citus is an extension that is made compatible with Postgres Pro and provides such major functionalities as columnar data storage and distributed OLAP database, which can be used either together or separately.
citus offers the following benefits:
Columnar storage with data compression.
The ability to scale your Postgres Pro installation to a distributed database cluster.
Row-based or schema-based sharding.
Parallelized DML operations across cluster nodes.
Reference tables, which can be accessed locally on each node.
The ability to execute DML queries on any node, which allows utilizing the full capacity of your cluster for distributed queries.
J.5.1. Limitations #
citus is incompatible with some Postgres Pro Enterprise features, take note of these limitations while arranging your work with the extension:
citus cannot be used together with autonomous transactions.
With enable_self_join_removal set to
on, query planning will result in an error if the optimizer decides to make the query distributed. Otherwise, the optimizer may form an erroneous distributed query plan and produce incorrect query results. Therefore, it is recommended to set this parameter tooff.Real-time query replanning and citus should not be used together. If used, the
EXPLAIN ANALYZEcommand may operate incorrectly.citus cannot operate with
standard_conforming_stringsset tooff. citus_columnar can, but to avoid any errors, it is required to set the configuration parameter toonwhile executing theCREATE EXTENSIONorALTER EXTENSION UPDATEcommands. After the installation or update is completed, you can change the parameter value tooff, if necessary. The extension will continue to operate correctly.
J.5.2. Installation #
The citus extension is provided with Postgres Pro Enterprise as a separate pre-built package citus-ent-16. You can install either citus 12.1 or citus 13.0 depending on the repository you connect. For the detailed installation instructions, see Chapter 17. Once you have Postgres Pro Enterprise installed, follow the citus installation instructions below.
J.5.2.1. Installing citus on a Single Node #
To enable citus on a single node, complete the following steps:
Add
citusto theshared_preload_librariesvariable in thepostgresql.conffile:shared_preload_libraries = 'citus'
If you want to use citus together with other extensions, citus should be the first on the list of
shared_preload_libraries.Reload the database server for the changes to take effect. To verify that the
cituslibrary was installed correctly, you can run the following command:SHOW shared_preload_libraries;
Create the citus extension using the following query:
CREATE EXTENSION citus;
The CREATE EXTENSION command in the procedure above also installs the citus_columnar extension. If you want to enable only citus_columnar, complete the same steps but specify citus_columnar instead.
J.5.2.2. Installing citus on Multiple Nodes #
To enable citus on multiple nodes, complete the following steps on all nodes:
Add
citusto theshared_preload_librariesvariable in thepostgresql.conffile:shared_preload_libraries = 'citus'
If you want to use citus together with other extensions, citus should be the first on the list of
shared_preload_libraries.Set up access permissions to the database server. By default, the database server listens only to clients on
localhost. Set thelisten_addressesconfiguration parameter to*to specify all available IP interfaces.Configure client authentication by editing the
pg_hba.conffile.Reload the database server for the changes to take effect. To verify that the
cituslibrary was installed correctly, you can run the following command:SHOW shared_preload_libraries;
Create the citus extension using the following query:
CREATE EXTENSION citus;
When the above steps have been taken on all nodes, perform the actions below on the coordinator node for worker nodes to be able to connect to it:
Register the hostname that worker nodes use to connect to the coordinator node:
SELECT citus_set_coordinator_host(
'coordinator_name',coordinator_port);Add each worker node:
SELECT * from citus_add_node(
'worker_name',worker_port);Verify that worker nodes are set successfully:
SELECT * FROM citus_get_active_worker_nodes();
J.5.3. Upgrading citus #
To upgrade citus from version 12.1 to version 13.0, take the following steps:
Install the version 13.0 package.
Restart the Postgres Pro server.
Change the extension definition by executing
ALTER EXTENSION:postgres=# ALTER EXTENSION citus UPDATE; ALTER EXTENSION
Check the extension version after the upgrade:
postgres=# SELECT * from citus_version(); citus_version ------------------------------------------------------------------------------------------------------ Citus 13.0.3.1 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit (1 row)
J.5.4. When to Use citus #
J.5.4.1. Multi-Tenant SaaS Database #
Most B2B applications already have the notion of a tenant, customer, or account built into their data model. In this model, the database serves many tenants, each of whose data is separate from other tenants.
citus provides full SQL functionality for this workload and enables scaling out your relational database to more than 100,000 tenants. citus also adds new features for multi-tenancy. For example, citus supports tenant isolation to provide performance guarantees for large tenants, and has the concept of reference tables to reduce data duplication across tenants.
These capabilities allow you to scale out data of your tenants across many computers and add more CPU, memory, and disk resources. Further, sharing the same database schema across multiple tenants makes efficient use of hardware resources and simplifies database management.
citus offers the following advantages for multi-tenant applications:
Fast queries for all tenants.
Sharding logic in the database rather than the application.
Hold more data in single-node Postgres Pro.
Scale out maintaining the SQL functionality.
Maintain performance under high concurrency.
Fast metrics analysis across customer base.
Scale to handle new customer sign-ups.
Isolate resource usage of large and small customers.
J.5.4.2. Real-Time Analytics #
citus supports real-time queries over large datasets. Commonly these queries occur in rapidly growing event systems or systems with time series data. Example use cases include:
Analytic dashboards with sub-second response times.
Exploratory queries on unfolding events.
Large dataset archival and reporting.
Analyzing sessions with funnel, segmentation, and cohort queries.
citus parallelizes query execution and scales linearly with the number of worker databases in a cluster. Some advantages of citus for real-time applications are as follows:
Maintain sub-second responses as the dataset grows.
Analyze new events and new data in real time.
Parallelize SQL queries.
Scale out maintaining the SQL functionality.
Maintain performance under high concurrency.
Fast responses to dashboard queries.
Use one database rather than many on several nodes.
Rich Postgres Pro data types and extensions.
J.5.4.3. Microservices #
citus supports schema-based sharding, which allows distributing regular database schemas across many computers. This sharding methodology aligns well with typical microservices architecture, where storage is fully owned by the service hence cannot share the same schema definition with other tenants.
Schema-based sharding is an easier model to adopt, create a new schema, and set the search_path in your service.
Advantages of using citus for microservices:
Allows distributing horizontally scalable state across services.
Transfer strategic business data from microservices into common distributed tables for analytics.
Efficiently use hardware by balancing services on multiple computers.
Isolate noisy services to their own nodes.
Easy to understand sharding model.
Quick adoption.
J.5.4.4. Considerations for Use #
citus extends Postgres Pro with distributed functionality, but it is not a drop-in replacement that scales out all workloads. A performant citus cluster involves thinking about the data model, tooling, and choice of SQL features used.
A good way to think about tools and SQL features is the following: if your workload aligns with use cases described here and you happen to run into an unsupported tool or query, then there is usually a good workaround.
J.5.4.5. When citus is Inappropriate #
Some workloads do not need a powerful distributed database, while others require a large flow of information between worker nodes. In the first case citus is unnecessary and in the second not generally performant. Below are a few examples when you do not need to use citus:
You do not expect your workload to ever grow beyond a single Postgres Pro Enterprise node.
Offline analytics, without the need for real-time data transfer nor real-time queries.
Analytics apps that do not need to support a large number of concurrent users.
Queries that return data-heavy ETL results rather than summaries.
J.5.5. Quick Tutorials #
J.5.5.1. Multi-Tenant Applications #
In this tutorial a sample ad analytics dataset is used to demonstrate how you can use citus to power your multi-tenant application.
Note
This tutorial assumes that you already have citus installed and running. If not, consult the Installing citus on a Single Node section to set up the extension locally.
J.5.5.1.1. Data Model and Sample Data #
This section shows how to create a database for an ad analytics app, which can be used by companies to view, change, analyze, and manage their ads and campaigns (see an example app). Such an application has good characteristics of a typical multi-tenant system. Data from different tenants is stored in a central database, and each tenant has an isolated view of their own data.
Three Postgres Pro tables to represent this data will be used. To get started, download sample data for these tables:
curl https://examples.citusdata.com/tutorial/companies.csv > companies.csv curl https://examples.citusdata.com/tutorial/campaigns.csv > campaigns.csv curl https://examples.citusdata.com/tutorial/ads.csv > ads.csv
J.5.5.1.2. Creating Tables #
First connect to the citus coordinator using psql.
If you are using citus installed as described in the Installing citus on a Single Node section, the coordinator node will be running on port
9700.psql -p 9700
Create tables by using the standard Postgres Pro
CREATE TABLEcommand:CREATE TABLE companies ( id bigint NOT NULL, name text NOT NULL, image_url text, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL ); CREATE TABLE campaigns ( id bigint NOT NULL, company_id bigint NOT NULL, name text NOT NULL, cost_model text NOT NULL, state text NOT NULL, monthly_budget bigint, blacklisted_site_urls text[], created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL ); CREATE TABLE ads ( id bigint NOT NULL, company_id bigint NOT NULL, campaign_id bigint NOT NULL, name text NOT NULL, image_url text, target_url text, impressions_count bigint DEFAULT 0, clicks_count bigint DEFAULT 0, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL );Create primary key indexes on each of the tables just like you would do in Postgres Pro:
ALTER TABLE companies ADD PRIMARY KEY (id); ALTER TABLE campaigns ADD PRIMARY KEY (id, company_id); ALTER TABLE ads ADD PRIMARY KEY (id, company_id);
J.5.5.1.3. Distributing Tables and Loading Data #
Now you can instruct citus to distribute tables created above across the different nodes in the cluster. To do so, run the create_distributed_table function and specify the table you want to shard and the column you want to shard on. In the example below, all the tables are sharded on the company_id column.
SELECT create_distributed_table('companies', 'id');
SELECT create_distributed_table('campaigns', 'company_id');
SELECT create_distributed_table('ads', 'company_id');
Sharding all tables on the company_id column allows citus to co-locate the tables together and allows for features like primary keys, foreign keys, and complex joins across your cluster.
Then you can go ahead and load the downloaded data into the tables using the standard psql \copy command. Make sure that you specify the correct file path if you downloaded the file to a different location.
\copy companies from 'companies.csv' with csv \copy campaigns from 'campaigns.csv' with csv \copy ads from 'ads.csv' with csv
J.5.5.1.4. Running Queries #
After the data is loaded into the tables, you can run some queries. citus supports standard INSERT, UPDATE, and DELETE commands for inserting and modifying rows in a distributed table, which is the typical way of interaction for a user-facing application.
For example, you can insert a new company by running:
INSERT INTO companies VALUES (5000, 'New Company', 'https://randomurl/image.png', now(), now());
If you want to double the budget for all campaigns of the company, run the UPDATE command:
UPDATE campaigns SET monthly_budget = monthly_budget*2 WHERE company_id = 5;
Another example of such an operation is to run transactions, which span multiple tables. For example, you can delete a campaign and all its associated ads atomically by running:
BEGIN; DELETE FROM campaigns WHERE id = 46 AND company_id = 5; DELETE FROM ads WHERE campaign_id = 46 AND company_id = 5; COMMIT;
Each statement in a transaction causes round-trips between the coordinator and workers in the multi-node citus. For multi-tenant workloads, it is more efficient to run transactions in distributed functions. The efficiency gains become more apparent for larger transactions, but you can use the small transaction above as an example.
First create a function that does the deletions:
CREATE OR REPLACE FUNCTION delete_campaign(company_id int, campaign_id int) RETURNS void LANGUAGE plpgsql AS $fn$ BEGIN DELETE FROM campaigns WHERE id = $2 AND campaigns.company_id = $1; DELETE FROM ads WHERE ads.campaign_id = $2 AND ads.company_id = $1; END; $fn$;
Next use the create_distributed_function function to instruct citus to call the function directly on workers rather than on the coordinator (except on a single-node citus installation, which runs everything on the coordinator). It calls the function on whatever worker holds the shards for the
adsandcampaignstables corresponding to thecompany_idvalue.SELECT create_distributed_function( 'delete_campaign(int, int)', 'company_id', colocate_with := 'campaigns' ); -- You can run the function as usual SELECT delete_campaign(5, 46);
Besides transactional operations, you can also run analytics queries using standard SQL. One interesting query for a company to run is to see details about its campaigns with maximum budget.
SELECT name, cost_model, state, monthly_budget FROM campaigns WHERE company_id = 5 ORDER BY monthly_budget DESC LIMIT 10;
You can also run a join query across multiple tables to see information about running campaigns, which receive the most clicks and impressions.
SELECT campaigns.id, campaigns.name, campaigns.monthly_budget, sum(impressions_count) AS total_impressions, sum(clicks_count) AS total_clicks FROM ads, campaigns WHERE ads.company_id = campaigns.company_id AND ads.campaign_id = campaigns.id AND campaigns.company_id = 5 AND campaigns.state = 'running' GROUP BY campaigns.id, campaigns.name, campaigns.monthly_budget ORDER BY total_impressions, total_clicks;
The tutorial above shows how to use citus to power a simple multi-tenant application. As a next step, you can look at the Multi-Tenant Apps section to see how you can model your own data for multi-tenancy.
J.5.5.2. Real-Time Analytics #
This tutorial demonstrates how to use citus to ingest events data and run analytical queries on that data in human real-time. A sample GitHub events dataset is used to this end in the example.
Note
This tutorial assumes that you already have citus installed and running. If not, consult the Installing citus on a Single Node section to set up the extension locally.
J.5.5.2.1. Data Model and Sample Data #
This section shows how to create a database for a real-time analytics application. This application will insert large volumes of events data and enable analytical queries on that data with sub-second latencies. In this example, the GitHub events dataset is used. This dataset includes all public events on GitHub, such as commits, forks, new issues, and comments on these issues.
Two Postgres Pro tables are used to represent this data. To get started, download sample data for these tables:
curl https://examples.citusdata.com/tutorial/users.csv > users.csv curl https://examples.citusdata.com/tutorial/events.csv > events.csv
J.5.5.2.2. Creating Tables #
To start first connect to the citus coordinator using psql.
If you are using citus installed as described in the Installing citus on a Single Node section, the coordinator node will be running on port 9700.
psql -p 9700
Then you can create the tables by using the standard Postgres Pro CREATE TABLE command:
CREATE TABLE github_events
(
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
user_id bigint,
org jsonb,
created_at timestamp
);
CREATE TABLE github_users
(
user_id bigint,
url text,
login text,
avatar_url text,
gravatar_id text,
display_login text
);
Next you can create indexes on events data just like you do in Postgres Pro. This example also shows how to create a GIN index to make querying on JSONB fields faster.
CREATE INDEX event_type_index ON github_events (event_type); CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops);
J.5.5.2.3. Distributing Tables and Loading Data #
Now you can instruct citus to distribute the tables created above across the nodes in the cluster. To do so, you can call the create_distributed_table function and specify the table you want to shard and the column you want to shard on. In the example below, all the tables are sharded on the user_id column.
SELECT create_distributed_table('github_users', 'user_id');
SELECT create_distributed_table('github_events', 'user_id');
Sharding all tables on the user_id column allows citus to co-locate the tables together and allows for efficient joins and distributed roll-ups.
Then you can go ahead and load the downloaded data into the tables using the standard psql \copy command. Make sure that you specify the correct file path if you downloaded the file to a different location.
\copy github_users from 'users.csv' with csv \copy github_events from 'events.csv' with csv
J.5.5.2.4. Running Queries #
After the data is loaded into the tables, you can run some queries. First check how many users are contained in the distributed database.
SELECT count(*) FROM github_users;
Now analyze GitHub push events in the data. First compute the number of commits per minute by using the number of distinct commits in each push event.
SELECT date_trunc('minute', created_at) AS minute,
sum((payload->>'distinct_size')::int) AS num_commits
FROM github_events
WHERE event_type = 'PushEvent'
GROUP BY minute
ORDER BY minute;
Also, there is a users table. You can also join the users with events and find the top ten users who created the most repositories.
SELECT login, count(*)
FROM github_events ge
JOIN github_users gu
ON ge.user_id = gu.user_id
WHERE event_type = 'CreateEvent' AND payload @> '{"ref_type": "repository"}'
GROUP BY login
ORDER BY count(*) DESC LIMIT 10;
citus also supports standard INSERT, UPDATE, and DELETE commands for inserting and modifying data. For example, you can update the user display login by running the following command:
UPDATE github_users SET display_login = 'no1youknow' WHERE user_id = 24305673;
As a next step, you can look at the Real-Time Apps section to see how you can model your own data and power real-time analytical applications.
J.5.5.3. Microservices #
This tutorial shows how to use citus as the storage backend for multiple microservices and demonstrates a sample setup and basic operation of such a cluster.
Note
This tutorial assumes that you already have citus installed and running. If not, consult the Installing citus on a Single Node section to set up the extension locally.
J.5.5.3.1. Distributed Schemas #
Distributed schemas are relocatable within a citus cluster. The system can rebalance them as a whole unit across the available nodes, which allows for efficient sharing of resources without manual allocation.
By design, microservices own their storage layer, we do not make any assumptions on the type of tables and data that they will create and store. We, however, provide a schema for every service and assume that they use a distinct role to connect to the database. When a user connects, their role name is put at the beginning of the search_path, so if the role matches the schema name, you do not need any application changes to set the correct search_path.
Three services are used in the example:
userservicetimeservicepingservice
To start first connect to the citus coordinator using psql.
If you are using citus installed as described in the Installing citus on a Single Node section, the coordinator node will be running on port 9700.
psql -p 9700
You can now create the database roles for every service:
CREATE USER user_service; CREATE USER time_service; CREATE USER ping_service;
There are two ways to distribute a schema in citus:
Manually by calling the
citus_schema_distribute('function:schema_name')CREATE SCHEMA AUTHORIZATION user_service; CREATE SCHEMA AUTHORIZATION time_service; CREATE SCHEMA AUTHORIZATION ping_service; SELECT citus_schema_distribute('user_service'); SELECT citus_schema_distribute('time_service'); SELECT citus_schema_distribute('ping_service');This method also allows you to convert existing regular schemas into distributed schemas.
Note
You can only distribute schemas that do not contain distributed and reference tables.
Alternative approach is to enable the citus.enable_schema_based_sharding configuration parameter:
SET citus.enable_schema_based_sharding TO ON; CREATE SCHEMA AUTHORIZATION user_service; CREATE SCHEMA AUTHORIZATION time_service; CREATE SCHEMA AUTHORIZATION ping_service;
The parameter can be changed for the current session or permanently in the
postgresql.conffile. With the parameter set toONall created schemas are distributed by default.
You can list the currently distributed schemas:
SELECT * FROM citus_schemas;
schema_name | colocation_id | schema_size | schema_owner -------------+---------------+-------------+-------------- user_service | 5 | 0 bytes | user_service time_service | 6 | 0 bytes | time_service ping_service | 7 | 0 bytes | ping_service (3 rows)
J.5.5.3.2. Creating Tables #
You now need to connect to the citus coordinator for every microservice. You can use the \c command to swap the user within an existing psql instance.
\c citus user_service
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL
);
\c citus time_service
CREATE TABLE query_details (
id SERIAL PRIMARY KEY,
ip_address INET NOT NULL,
query_time TIMESTAMP NOT NULL
);
\c citus ping_service
CREATE TABLE ping_results (
id SERIAL PRIMARY KEY,
host VARCHAR(255) NOT NULL,
result TEXT NOT NULL
);
J.5.5.3.3. Configure Services #
For the purpose of this tutorial a very simple set of services is used. You can obtain them by cloning this public repository:
git clone https://github.com/citusdata/citus-example-microservices.git
The repository contains the ping, time, and user services. All of them have the app.py file, which we run.
$ tree
.
├── LICENSE
├── README.md
├── ping
│ ├── app.py
│ ├── ping.sql
│ └── requirements.txt
├── time
│ ├── app.py
│ ├── requirements.txt
│ └── time.sql
└── user
├── app.py
├── requirements.txt
└── user.sql
Before you run the services, however, edit the user/app.py, ping/app.py, and time/app.py files providing the connection configuration for your citus cluster:
# Database configuration
db_config = {
'host': 'localhost',
'database': 'citus',
'user': 'ping_service',
'port': 9700
}
After making the changes save all modified files and move on to the next step of running the services.
J.5.5.3.4. Running the Services #
Change into every app directory and run them in their own
pythonenvironment.cd user pipenv install pipenv shell python app.py
Repeat the above for the
timeandpingservice, after which you can use the API.Create some users:
curl -X POST -H "Content-Type: application/json" -d '[ {"name": "John Doe", "email": "john@example.com"}, {"name": "Jane Smith", "email": "jane@example.com"}, {"name": "Mike Johnson", "email": "mike@example.com"}, {"name": "Emily Davis", "email": "emily@example.com"}, {"name": "David Wilson", "email": "david@example.com"}, {"name": "Sarah Thompson", "email": "sarah@example.com"}, {"name": "Alex Miller", "email": "alex@example.com"}, {"name": "Olivia Anderson", "email": "olivia@example.com"}, {"name": "Daniel Martin", "email": "daniel@example.com"}, {"name": "Sophia White", "email": "sophia@example.com"} ]' http://localhost:5000/usersList the created users:
curl http://localhost:5000/users
Get current time:
curl http://localhost:5001/current_time
Run the
pingagainst example.com:curl -X POST -H "Content-Type: application/json" -d '{"host": "example.com"}' http://localhost:5002/ping
J.5.5.3.5. Exploring the Database #
Now that we called some API functions, data has been stored and we can check if the citus_schemas view reflects what we expect:
SELECT * FROM citus_schemas;
schema_name | colocation_id | schema_size | schema_owner --------------+---------------+-------------+-------------- user_service | 1 | 112 kB | user_service time_service | 2 | 32 kB | time_service ping_service | 3 | 32 kB | ping_service (3 rows)
At the time of schemas creation you do not instruct citus on which computer to create them. It is done automatically. Execute the following query to see where each schema resides:
SELECT nodename,nodeport, table_name, pg_size_pretty(sum(shard_size)) FROM citus_shards GROUP BY nodename,nodeport, table_name;
nodename | nodeport | table_name | pg_size_pretty -----------+----------+----------------------------+---------------- localhost | 9701 | time_service.query_details | 32 kB localhost | 9702 | user_service.users | 112 kB localhost | 9702 | ping_service.ping_results | 32 kB
We can see that the time service landed on node localhost:9701, while the user and ping services share space on the second worker localhost:9702. This is only an example, and the data sizes here can be ignored, but let us assume that we are annoyed by the uneven storage space utilization between the nodes. It makes more sense to have the two smaller time and ping services reside on one computer, while the large user service resides alone.
We can do this by instructing citus to rebalance the cluster by disk size:
SELECT citus_rebalance_start();
NOTICE: Scheduled 1 moves as job 1
DETAIL: Rebalance scheduled as background job
HINT: To monitor progress, run: SELECT * FROM citus_rebalance_status();
citus_rebalance_start
-----------------------
1
(1 row)
When done, check how the new layout looks:
SELECT nodename,nodeport, table_name, pg_size_pretty(sum(shard_size)) FROM citus_shards GROUP BY nodename,nodeport, table_name;
nodename | nodeport | table_name | pg_size_pretty -----------+----------+----------------------------+---------------- localhost | 9701 | time_service.query_details | 32 kB localhost | 9701 | ping_service.ping_results | 32 kB localhost | 9702 | user_service.users | 112 kB (3 rows)
We expect that the schemas have been moved and the cluster has become more balanced. This operation is transparent for the applications. Therefore, there is no need for a restart, and they will continue serving queries.
J.5.6. Use Case Guides #
J.5.6.1. Multi-Tenant Applications #
If you are building a Software-as-a-service (SaaS) application, you probably already have the notion of tenancy built into your data model. Typically, most information relates to tenants/customers/accounts and the database tables capture this natural relation.
For SaaS applications, each tenant's data can be stored together in a single database instance and kept isolated from and invisible to other tenants. This is efficient in three ways. First, application improvements apply to all clients. Second, sharing a database between tenants uses hardware efficiently. Last, it is much simpler to manage a single database for all tenants than a different database server for each tenant.
However, a single relational database instance has traditionally had trouble scaling to the volume of data needed for a large multi-tenant application. Developers were forced to relinquish the benefits of the relational model when data exceeded the capacity of a single database node.
The citus extension allows users to write multi-tenant applications as if they are connecting to a single Postgres Pro database, when in fact the database is a horizontally scalable cluster of computers. Client code requires minimal modifications and can continue to use full SQL capabilities.
This guide takes a sample multi-tenant application and describes how to model it for scalability with citus. Along the way typical challenges for multi-tenant applications are examined like isolating tenants from noisy neighbors, scaling hardware to accommodate more data, and storing data that differs across tenants. Postgres Pro and citus provide all the tools needed to handle these challenges, so let's get building.
J.5.6.1.1. Let's Make an App: Ad Analytics #
We will build the back-end for an application that tracks online advertising performance and provides an analytics dashboard on top. It is a natural fit for a multi-tenant application because user requests for data concern one company (their own) at a time. Code for the full example application is available on GitHub.
Let's start by considering a simplified schema for this application. The application must keep track of multiple companies, each of which runs advertising campaigns. Campaigns have many ads, and each ad has associated records of its clicks and impressions.
Here is the example schema. We will make some minor changes later, which allow us to effectively distribute and isolate the data in a distributed environment.
CREATE TABLE companies ( id bigserial PRIMARY KEY, name text NOT NULL, image_url text, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL ); CREATE TABLE campaigns ( id bigserial PRIMARY KEY, company_id bigint REFERENCES companies (id), name text NOT NULL, cost_model text NOT NULL, state text NOT NULL, monthly_budget bigint, blacklisted_site_urls text[], created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL ); CREATE TABLE ads ( id bigserial PRIMARY KEY, campaign_id bigint REFERENCES campaigns (id), name text NOT NULL, image_url text, target_url text, impressions_count bigint DEFAULT 0, clicks_count bigint DEFAULT 0, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL ); CREATE TABLE clicks ( id bigserial PRIMARY KEY, ad_id bigint REFERENCES ads (id), clicked_at timestamp without time zone NOT NULL, site_url text NOT NULL, cost_per_click_usd numeric(20,10), user_ip inet NOT NULL, user_data jsonb NOT NULL ); CREATE TABLE impressions ( id bigserial PRIMARY KEY, ad_id bigint REFERENCES ads (id), seen_at timestamp without time zone NOT NULL, site_url text NOT NULL, cost_per_impression_usd numeric(20,10), user_ip inet NOT NULL, user_data jsonb NOT NULL );
There are modifications we can make to the schema, which will give it a performance boost in a distributed environment like citus. To see how, we must become familiar with how the extension distributes data and executes queries.
J.5.6.1.2. Scaling the Relational Data Model #
The relational data model is great for applications. It protects data integrity, allows flexible queries, and accommodates changing data. Traditionally the only problem was that relational databases were not considered capable of scaling to the workloads needed for big SaaS applications. Developers had to put up with NoSQL databases, or a collection of backend services, to reach that size.
With citus you can keep your data model and make it scale. The extension appears to applications as a single Postgres Pro database, but it internally routes queries to an adjustable number of physical servers (nodes), which can process requests in parallel.
Multi-tenant applications have a nice property that we can take advantage of: queries usually always request information for one tenant at a time, not a mix of tenants. For instance, when a salesperson is searching prospect information in a CRM, the search results are specific to his employer; other businesses' leads and notes are not included.
Because application queries are restricted to a single tenant, such as a store or company, one approach for making multi-tenant application queries fast is to store all data for a given tenant on the same node. This minimizes network overhead between the nodes and allows citus to support all your application's joins, key constraints and transactions efficiently. With this, you can scale across multiple nodes without having to totally re-write or re-architect your application. See the figure below to learn more.
Figure J.1. Multi-Tenant Ad Routing Diagram
This can be done in citus by making sure every table in our schema has a column to clearly mark which tenant owns which rows. In the ad analytics application the tenants are companies, so we must ensure all tables have a company_id column.
We can tell citus to use this column to read and write rows to the same node when the rows are marked for the same company. In citus terminology company_id is the distribution column, which you can learn more about in the Choosing Distribution Column section.
J.5.6.1.3. Preparing Tables and Ingesting Data #
In the previous section we identified the correct distribution column for our multi-tenant application: company_id. Even in a single-computer database it can be useful to denormalize tables with the addition of company_id, whether it be for row-level security or for additional indexing. The extra benefit, as we saw, is that including the extra column helps for multi-machine scaling as well.
The schema we have created so far uses a separate id column as primary key for each table. citus requires that primary and foreign key constraints include the distribution column. This requirement makes enforcing these constraints much more efficient in a distributed environment as only a single node has to be checked to guarantee them.
In SQL, this requirement translates to making primary and foreign keys composite by including company_id. This is compatible with the multi-tenant case because what we really need there is to ensure uniqueness on a per-tenant basis.
Putting it all together, here are the changes that prepare the tables for distribution by company_id.
CREATE TABLE companies (
id bigserial PRIMARY KEY,
name text NOT NULL,
image_url text,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
);
CREATE TABLE campaigns (
id bigserial, -- was: PRIMARY KEY
company_id bigint REFERENCES companies (id),
name text NOT NULL,
cost_model text NOT NULL,
state text NOT NULL,
monthly_budget bigint,
blacklisted_site_urls text[],
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
PRIMARY KEY (company_id, id) -- added
);
CREATE TABLE ads (
id bigserial, -- was: PRIMARY KEY
company_id bigint, -- added
campaign_id bigint, -- was: REFERENCES campaigns (id)
name text NOT NULL,
image_url text,
target_url text,
impressions_count bigint DEFAULT 0,
clicks_count bigint DEFAULT 0,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
PRIMARY KEY (company_id, id), -- added
FOREIGN KEY (company_id, campaign_id) -- added
REFERENCES campaigns (company_id, id)
);
CREATE TABLE clicks (
id bigserial, -- was: PRIMARY KEY
company_id bigint, -- added
ad_id bigint, -- was: REFERENCES ads (id),
clicked_at timestamp without time zone NOT NULL,
site_url text NOT NULL,
cost_per_click_usd numeric(20,10),
user_ip inet NOT NULL,
user_data jsonb NOT NULL,
PRIMARY KEY (company_id, id), -- added
FOREIGN KEY (company_id, ad_id) -- added
REFERENCES ads (company_id, id)
);
CREATE TABLE impressions (
id bigserial, -- was: PRIMARY KEY
company_id bigint, -- added
ad_id bigint, -- was: REFERENCES ads (id),
seen_at timestamp without time zone NOT NULL,
site_url text NOT NULL,
cost_per_impression_usd numeric(20,10),
user_ip inet NOT NULL,
user_data jsonb NOT NULL,
PRIMARY KEY (company_id, id), -- added
FOREIGN KEY (company_id, ad_id) -- added
REFERENCES ads (company_id, id)
);
You can learn more about migrating your own data model in the Identify Distribution Strategy section.
J.5.6.1.3.1. Practical Example #
Note
This guide is designed so you can follow along in your own citus database. This tutorial assumes that you already have the extension installed and running. If not, consult the Installing citus on a Single Node section to set up the extension locally.
At this point feel free to follow along in your own citus cluster by downloading and executing the SQL to create the schema. Once the schema is ready, we can tell citus to create shards on the workers. From the coordinator node run:
SELECT create_distributed_table('companies', 'id'); SELECT create_distributed_table('campaigns', 'company_id'); SELECT create_distributed_table('ads', 'company_id'); SELECT create_distributed_table('clicks', 'company_id'); SELECT create_distributed_table('impressions', 'company_id');The create_distributed_table function informs citus that a table should be distributed among nodes and that future incoming queries to those tables should be planned for distributed execution. The function also creates shards for the table on worker nodes, which are low-level units of data storage citus uses to assign data to nodes.
The next step is loading sample data into the cluster from the command line:
# Download and ingest datasets from the shell for dataset in companies campaigns ads clicks impressions geo_ips; do curl -O https://examples.citusdata.com/mt_ref_arch/${dataset}.csv doneBeing an extension of Postgres Pro, citus supports bulk loading with the
/copycommand. Use it to ingest the data you downloaded and make sure that you specify the correct file path if you downloaded the file to some other location. Back inside psql run this:\copy companies from 'companies.csv' with csv \copy campaigns from 'campaigns.csv' with csv \copy ads from 'ads.csv' with csv \copy clicks from 'clicks.csv' with csv \copy impressions from 'impressions.csv' with csv
J.5.6.1.4. Integrating Applications #
Once you have made the slight schema modification outlined earlier, your application can scale with very little work. You will just connect the app to citus and let the database take care of keeping the queries fast and the data safe.
Any application queries or update statements, which include a filter on company_id, will continue to work exactly as they are. As mentioned earlier, this kind of filter is common in multi-tenant apps. When using an Object-Relational Mapper (ORM) you can recognize these queries by methods such as where or filter.
ActiveRecord:
Impression.where(company_id: 5).count
Django:
Impression.objects.filter(company_id=5).count()
Basically when the resulting SQL executed in the database contains a WHERE company_id = :value clause on every table (including tables in JOIN queries), then citus will recognize that the query should be routed to a single node and execute it there as it is. This makes sure that all SQL functionality is available. The node is an ordinary Postgres Pro server after all.
Also, to make it even simpler, you can use our activerecord-multi-tenant library for Ruby on Rails, or django-multitenant for Django, which will automatically add these filters to all your queries, even the complicated ones. Check out our migration guides for Ruby on Rails and Django.
This guide is framework-agnostic, so we will point out some citus features using SQL. Use your imagination for how these statements would be expressed in your language of choice.
Here is a simple query and update operating on a single tenant.
-- Campaigns with highest budget SELECT name, cost_model, state, monthly_budget FROM campaigns WHERE company_id = 5 ORDER BY monthly_budget DESC LIMIT 10; -- Double the budgets! UPDATE campaigns SET monthly_budget = monthly_budget*2 WHERE company_id = 5;
A common pain point for users scaling applications with NoSQL databases is the lack of transactions and joins. However, transactions work as you would expect them to in citus:
-- Transactionally reallocate campaign budget money BEGIN; UPDATE campaigns SET monthly_budget = monthly_budget + 1000 WHERE company_id = 5 AND id = 40; UPDATE campaigns SET monthly_budget = monthly_budget - 1000 WHERE company_id = 5 AND id = 41; COMMIT;
As a final demo of SQL support, we have a query that includes aggregates and window functions and it works the same in citus as it does in Postgres Pro. The query ranks the ads in each campaign by the count of their impressions.
SELECT a.campaign_id,
RANK() OVER (
PARTITION BY a.campaign_id
ORDER BY a.campaign_id, count(*) desc
), count(*) as n_impressions, a.id
FROM ads as a
JOIN impressions as i
ON i.company_id = a.company_id
AND i.ad_id = a.id
WHERE a.company_id = 5
GROUP BY a.campaign_id, a.id
ORDER BY a.campaign_id, n_impressions desc;
In short, when queries are scoped to a tenant then the INSERT, UPDATE, DELETE, complex SQL commands, and transactions all work as expected.
J.5.6.1.5. Sharing Data Between Tenants #
Up until now all tables have been distributed by company_id, but sometimes there is data that can be shared by all tenants and does not “belong” to any tenant in particular. For instance, all companies using this example ad platform might want to get geographical information for their audience based on IP addresses. In a single computer database this could be accomplished by a lookup table for geo-ip, like the following. (A real table would probably use PostGIS, but bear with the simplified example.)
CREATE TABLE geo_ips (
addrs cidr NOT NULL PRIMARY KEY,
latlon point NOT NULL
CHECK (-90 <= latlon[0] AND latlon[0] <= 90 AND
-180 <= latlon[1] AND latlon[1] <= 180)
);
CREATE INDEX ON geo_ips USING gist (addrs inet_ops);
To use this table efficiently in a distributed setup, we need to find a way to co-locate the geo_ips table with clicks for not just one but every company. That way, no network traffic need be incurred at query time. This can be done in citus by designating geo_ips as a reference table.
-- Make synchronized copies of geo_ips on all workers
SELECT create_reference_table('geo_ips');
Reference tables are replicated across all worker nodes, and citus automatically keeps them in sync during modifications. Notice that we call the create_reference_table function rather than the create_distributed_table function.
Now that geo_ips is established as a reference table, load it with example data:
\copy geo_ips from 'geo_ips.csv' with csv
Now joining clicks with this table can execute efficiently. We can ask, for example, the locations of everyone who clicked on ad 290.
SELECT c.id, clicked_at, latlon FROM geo_ips, clicks c WHERE addrs >> c.user_ip AND c.company_id = 5 AND c.ad_id = 290;
J.5.6.1.6. Online Changes to the Schema #
Another challenge with multi-tenant systems is keeping the schemas for all the tenants in sync. Any schema change needs to be consistently reflected across all the tenants. In citus, you can simply use standard Postgres Pro DDL commands to change the schema of your tables, and the extension will propagate them from the coordinator node to the workers using a two-phase commit protocol.
For example, the advertisements in this application could use a text caption. We can add a column to the table by issuing the standard SQL on the coordinator:
ALTER TABLE ads ADD COLUMN caption text;
This updates all the workers as well. Once this command finishes, the citus cluster will accept queries that read or write data in the new caption column.
For a fuller explanation of how DDL commands propagate through the cluster, see the Modifying Tables section.
J.5.6.1.7. When Data Differs Across Tenants #
Given that all tenants share a common schema and hardware infrastructure, how can we accommodate tenants, which want to store information not needed by others? For example, one of the tenant applications using our advertising database may want to store tracking cookie information with clicks, whereas another tenant may care about browser agents. Traditionally databases using a shared schema approach for multi-tenancy have resorted to creating a fixed number of pre-allocated “custom” columns, or having external “extension tables”. However, Postgres Pro provides a much easier way with its unstructured column types, notably JSONB.
Notice that our schema already has a JSONB field in clicks called user_data. Each tenant can use it for flexible storage.
Suppose company five includes information in the field to track whether the user is on a mobile device. The company can query to find who clicks more, mobile or traditional visitors:
SELECT user_data->>'is_mobile' AS is_mobile, count(*) AS count FROM clicks WHERE company_id = 5 GROUP BY user_data->>'is_mobile' ORDER BY count DESC;
The database administrator can even create a partial index to improve speed for an individual tenant's query patterns. Here is one to improve filters for clicks of the company with company_id = 5 from users on mobile devices:
CREATE INDEX click_user_data_is_mobile ON clicks ((user_data->>'is_mobile')) WHERE company_id = 5;
Additionally, Postgres Pro supports GIN indices on JSONB. Creating a GIN index on a JSONB column will create an index on every key and value within that JSON document. This speeds up a number of JSONB operators such as ?, ?|, and ?&.
CREATE INDEX click_user_data ON clicks USING gin (user_data); -- this speeds up queries like, "which clicks have -- the is_mobile key present in user_data?" SELECT id FROM clicks WHERE user_data ? 'is_mobile' AND company_id = 5;
J.5.6.1.8. Scaling Hardware Resources #
Multi-tenant databases should be designed for future scale as business grows or tenants want to store more data. citus can scale out easily by adding new computers without having to make any changes or take application downtime.
Being able to rebalance data in the citus cluster allows you to grow your data size or number of customers and improve performance on demand. Adding new computers allows you to keep data in memory even when it is much larger than what a single computer can store.
Also, if data increases for only a few large tenants, then you can isolate those particular tenants to separate nodes for better performance.
To scale out your citus cluster, first add a new worker node to it with the citus_add_node function.
Once you add the node it is available in the system. However, at this point no tenants are stored on it and citus will not yet run any queries there. To move your existing data, you can ask citus to rebalance the data. This operation moves bundles of rows called shards between the currently active nodes to attempt to equalize the amount of data on each node.
SELECT citus_rebalance_start();
Applications do not need to undergo downtime during shard rebalancing. Read requests continue seamlessly, and writes are locked only when they affect shards, which are currently in flight. In citus writes to shards are blocked during rebalancing but reads are unaffected.
J.5.6.1.9. Dealing with Big Tenants #
The previous section describes a general-purpose way to scale a cluster as the number of tenants increases. However, users often have two questions. The first is what will happen to their largest tenant if it grows too big. The second is what are the performance implications of hosting a large tenant together with small ones on a single worker node.
Regarding the first question, investigating data from large SaaS sites reveals that as the number of tenants increases, the size of tenant data typically tends to follow a Zipfian distribution. See the figure below to learn more.
Figure J.2. Ziphian Distribution
For instance, in a database of 100 tenants, the largest is predicted to account for about 20% of the data. In a more realistic example for a large SaaS company, if there are 10,000 tenants, the largest will account for around 2% of the data. Even at 10TB of data, the largest tenant will require 200GB, which can pretty easily fit on a single node.
Another question is regarding performance when large and small tenants are on the same node. Standard shard rebalancing will improve overall performance but it may or may not improve the mixing of large and small tenants. The rebalancer simply distributes shards to equalize storage usage on nodes, without examining which tenants are allocated on each shard.
To improve resource allocation and make guarantees of tenant QoS it is worthwhile to move large tenants to dedicated nodes. The citus extension provides the tools to do this.
In our case, let's imagine that the company with company_id=5 is very large. We can isolate the data for this tenant in two steps. We will present the commands here, and you can consult the Tenant Isolation section to learn more about them.
First isolate the tenant's data to a dedicated shard suitable to move. The CASCADE option also applies this change to the rest of our tables distributed by company_id.
SELECT isolate_tenant_to_new_shard( 'companies', 5, 'CASCADE' );
The output is the shard ID dedicated to hold company_id=5:
┌─────────────────────────────┐ │ isolate_tenant_to_new_shard │ ├─────────────────────────────┤ │ 102240 │ └─────────────────────────────┘
Next we move the data across the network to a new dedicated node. Create a new node as described in the previous section. Take note of its hostname.
-- Find the node currently holding the new shard
SELECT nodename, nodeport
FROM pg_dist_placement AS placement,
pg_dist_node AS node
WHERE placement.groupid = node.groupid
AND node.noderole = 'primary'
AND shardid = 102240;
-- Move the shard to your choice of worker (it will also move the
-- other shards created with the CASCADE option)
-- Note that you should set wal_level for all nodes to be >= logical
-- to use citus_move_shard_placement
-- You also need to restart your cluster after setting wal_level in
-- postgresql.conf files
SELECT citus_move_shard_placement(
102240,
'source_host', source_port,
'dest_host', dest_port);
You can confirm the shard movement by querying the pg_dist_placement table again.
J.5.6.1.10. Where to Go From Here #
With this, you now know how to use citus to power your multi-tenant application for scalability. If you have an existing schema and want to migrate it for citus, see the Migrating an Existing App section.
To adjust a front-end application, specifically Ruby on Rails or Django, read Ruby on Rails or Django migration guides.
J.5.6.2. Real-Time Dashboards #
citus provides real-time queries over large datasets. One workload we commonly see at citus involves powering real-time dashboards of event data.
For example, you could be a cloud services provider helping other businesses monitor their HTTP traffic. Every time one of your clients receives an HTTP request your service receives a log record. You want to ingest all those records and create an HTTP analytics dashboard that gives your clients insights such as the number HTTP errors their sites served. It is important that this data shows up with as little latency as possible so your clients can fix problems with their sites. It is also important for the dashboard to show graphs of historical trends.
Alternatively, maybe you are building an advertising network and want to show clients clickthrough rates on their campaigns. In this example latency is also critical, raw data volume is also high, and both historical and live data are important.
In this section we will demonstrate how to build part of the first example, but this architecture would work equally well for the second and many other use cases.
J.5.6.2.1. Data Model #
The data we are dealing with is an immutable stream of log data. We will insert directly into citus but it is also common for this data to first be routed through something like Kafka. Doing so has the usual advantages, and makes it easier to pre-aggregate the data once data volumes become unmanageably high.
We will use a simple schema for ingesting HTTP event data. This schema serves as an example to demonstrate the overall architecture; a real system might use additional columns.
-- This is run on the coordinator
CREATE TABLE http_request (
site_id INT,
ingest_time TIMESTAMPTZ DEFAULT now(),
url TEXT,
request_country TEXT,
ip_address TEXT,
status_code INT,
response_time_msec INT
);
SELECT create_distributed_table('http_request', 'site_id');
When we call the create_distributed_table function we ask citus to hash-distribute http_request using the site_id column. That means all the data for a particular site will live in the same shard.
The user defined functions use the default configuration values for shard count. We recommend using 2-4x as many shards as CPU cores in your cluster. Using this many shards lets you rebalance data across your cluster after adding new worker nodes.
With this, the system is ready to accept data and serve queries. Keep the following loop running in a psql console in the background while you continue with the other commands in this article. It generates fake data every second or two.
DO $$
BEGIN LOOP
INSERT INTO http_request (
site_id, ingest_time, url, request_country,
ip_address, status_code, response_time_msec
) VALUES (
trunc(random()*32), clock_timestamp(),
concat('http://example.com/', md5(random()::text)),
('{China,India,USA,Indonesia}'::text[])[ceil(random()*4)],
concat(
trunc(random()*250 + 2), '.',
trunc(random()*250 + 2), '.',
trunc(random()*250 + 2), '.',
trunc(random()*250 + 2)
)::inet,
('{200,404}'::int[])[ceil(random()*2)],
5+trunc(random()*150)
);
COMMIT;
PERFORM pg_sleep(random() * 0.25);
END LOOP;
END $$;
Once you are ingesting data, you can run dashboard queries such as:
SELECT
site_id,
date_trunc('minute', ingest_time) as minute,
COUNT(1) AS request_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 1 ELSE 0 END) as success_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 0 ELSE 1 END) as error_count,
SUM(response_time_msec) / COUNT(1) AS average_response_time_msec
FROM http_request
WHERE date_trunc('minute', ingest_time) > now() - '5 minutes'::interval
GROUP BY site_id, minute
ORDER BY minute ASC;
The setup described above works but has two drawbacks:
Your HTTP analytics dashboard must go over each row every time it needs to generate a graph. For example, if your clients are interested in trends over the past year, your queries will aggregate every row for the past year from scratch.
Your storage costs will grow proportionally with the ingest rate and the length of the queryable history. In practice, you may want to keep raw events for a shorter period of time (one month) and look at historical graphs over a longer time period (years).
J.5.6.2.2. Rollups #
You can overcome both drawbacks by rolling up the raw data into a pre-aggregated form. Here, we will aggregate the raw data into a table, which stores summaries of 1-minute intervals. In a production system, you would probably also want something like 1-hour and 1-day intervals, these each correspond to zoom-levels in the dashboard. When the user wants request times for the last month the dashboard can simply read and chart the values for each of the last 30 days.
CREATE TABLE http_request_1min (
site_id INT,
ingest_time TIMESTAMPTZ, -- which minute this row represents
error_count INT,
success_count INT,
request_count INT,
average_response_time_msec INT,
CHECK (request_count = error_count + success_count),
CHECK (ingest_time = date_trunc('minute', ingest_time))
);
SELECT create_distributed_table('http_request_1min', 'site_id');
CREATE INDEX http_request_1min_idx ON http_request_1min (site_id, ingest_time);
This looks a lot like the previous code block. Most importantly: It also shards on site_id and uses the same default configuration for shard count. Because all three of those match, there is a 1-to-1 correspondence between http_request shards and http_request_1min shards, and citus will place matching shards on the same worker. This is called co-location; it makes queries such as joins faster and our rollups possible. See the figure below to learn more.
Figure J.3. Collocation Diagram
In order to populate http_request_1min we are going to periodically run INSERT INTO SELECT. This is possible because the tables are co-located. The following function wraps the rollup query up for convenience.
-- Single-row table to store when we rolled up last
CREATE TABLE latest_rollup (
minute timestamptz PRIMARY KEY,
-- "minute" should be no more precise than a minute
CHECK (minute = date_trunc('minute', minute))
);
-- Initialize to a time long ago
INSERT INTO latest_rollup VALUES ('10-10-1901');
-- Function to do the rollup
CREATE OR REPLACE FUNCTION rollup_http_request() RETURNS void AS $$
DECLARE
curr_rollup_time timestamptz := date_trunc('minute', now() - interval '1 minute');
last_rollup_time timestamptz := minute from latest_rollup;
BEGIN
INSERT INTO http_request_1min (
site_id, ingest_time, request_count,
success_count, error_count, average_response_time_msec
) SELECT
site_id,
date_trunc('minute', ingest_time),
COUNT(1) as request_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 1 ELSE 0 END) as success_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 0 ELSE 1 END) as error_count,
SUM(response_time_msec) / COUNT(1) AS average_response_time_msec
FROM http_request
-- Roll up only data new since last_rollup_time
WHERE ingest_time <@ tstzrange(last_rollup_time, curr_rollup_time, '(]')
GROUP BY 1, 2;
-- Update the value in latest_rollup so that next time we run the
-- rollup it will operate on data newer than curr_rollup_time
UPDATE latest_rollup SET minute = curr_rollup_time;
END;
$$ LANGUAGE plpgsql;
Note
The above function should be called every minute. You could do this by adding a crontab entry on the coordinator node:
* * * * * psql -c 'SELECT rollup_http_request();'
Alternatively, an extension such as pg_cron allows you to schedule recurring queries directly from the database.
The dashboard query from earlier is now a lot nicer:
SELECT site_id, ingest_time as minute, request_count,
success_count, error_count, average_response_time_msec
FROM http_request_1min
WHERE ingest_time > date_trunc('minute', now()) - '5 minutes'::interval;
J.5.6.2.3. Expiring Old Data #
The rollups make queries faster, but we still need to expire old data to avoid unbounded storage costs. Simply decide how long you would like to keep data for each granularity and use standard queries to delete expired data. In the following example, we decided to keep raw data for one day, and per-minute aggregations for one month:
DELETE FROM http_request WHERE ingest_time < now() - interval '1 day'; DELETE FROM http_request_1min WHERE ingest_time < now() - interval '1 month';
In production you could wrap these queries in a function and call it every minute in a cron job.
Data expiration can go even faster by using table range partitioning on top of citus hash distribution. See the Timeseries Data section for a detailed example.
Those are the basics. We provided an architecture that ingests HTTP events and then rolls up these events into their pre-aggregated form. This way you can both store raw events and also power your analytical dashboards with subsecond queries.
The next sections extend upon the basic architecture and show you how to resolve questions, which often appear.
J.5.6.2.4. Approximate Distinct Counts #
A common question in HTTP analytics deals with approximate distinct counts: How many unique visitors visited your site over the last month? Answering this question exactly requires storing the list of all previously seen visitors in the rollup tables, a prohibitively large amount of data. However, an approximate answer is much more manageable.
A datatype called HyperLogLog, or hll, can answer the query approximately; it takes a surprisingly small amount of space to tell you approximately how many unique elements are in a set. Its accuracy can be adjusted. We will use ones which, using only 1,280 bytes, will be able to count up to tens of billions of unique visitors with at most 2.2% error.
An equivalent problem appears if you want to run a global query, such as the number of unique IP addresses, which visited any of your client's sites over the last month. Without hll this query involves shipping lists of IP addresses from the workers to the coordinator for it to deduplicate. That is both a lot of network traffic and a lot of computation. By using hll you can greatly improve query speed.
You can install the hll extension, whose instructions are available in the GitHub repository, and enable it as follows:
CREATE EXTENSION hll;
Now we are ready to track IP addresses in our rollup with hll. First add a column to the rollup table.
ALTER TABLE http_request_1min ADD COLUMN distinct_ip_addresses hll;
Next use our custom aggregation to populate the column. Just add it to the query in our rollup function:
@@ -1,10 +1,12 @@
INSERT INTO http_request_1min (
site_id, ingest_time, request_count,
success_count, error_count, average_response_time_msec
+ , distinct_ip_addresses
) SELECT
site_id,
date_trunc('minute', ingest_time),
COUNT(1) as request_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 1 ELSE 0 END) as success_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 0 ELSE 1 END) as error_count,
SUM(response_time_msec) / COUNT(1) AS average_response_time_msec
+ , hll_add_agg(hll_hash_text(ip_address)) AS distinct_ip_addresses
FROM http_request
Dashboard queries are a little more complicated, you have to read out the distinct number of IP addresses by calling the hll_cardinality function:
SELECT site_id, ingest_time as minute, request_count,
success_count, error_count, average_response_time_msec,
hll_cardinality(distinct_ip_addresses) AS distinct_ip_address_count
FROM http_request_1min
WHERE ingest_time > date_trunc('minute', now()) - interval '5 minutes';
hll is not just faster, it lets you do things you could not previously. Say we did our rollups, but instead of using hll we saved the exact unique counts. This works fine, but you cannnot answer queries such as “how many distinct sessions were there during this one-week period in the past we've thrown away the raw data for?”.
With hll, this is easy. You can compute distinct IP counts over a time period with the following query:
SELECT hll_cardinality(hll_union_agg(distinct_ip_addresses))
FROM http_request_1min
WHERE ingest_time > date_trunc('minute', now()) - '5 minutes'::interval;
You can find more information about the hll extension in the project's GitHub repository.
J.5.6.2.5. Unstructured Data with JSONB #
The citus extension works well with Postgres Pro built-in support for unstructured data types. To demonstrate this, let's keep track of the number of visitors, which came from each country. Using a semi-structure data type saves you from needing to add a column for every individual country and ending up with rows that have hundreds of sparsely filled columns. It is recommended to use the JSONB format, here we will demonstrate how to incorporate JSONB columns into your data model.
First, add the new column to our rollup table:
ALTER TABLE http_request_1min ADD COLUMN country_counters JSONB;
Next, include it in the rollups by modifying the rollup function:
@@ -1,14 +1,19 @@
INSERT INTO http_request_1min (
site_id, ingest_time, request_count,
success_count, error_count, average_response_time_msec
+ , country_counters
) SELECT
site_id,
date_trunc('minute', ingest_time),
COUNT(1) as request_count,
SUM(CASE WHEN (status_code between 200 and 299) THEN 1 ELSE 0 END) as success_count
SUM(CASE WHEN (status_code between 200 and 299) THEN 0 ELSE 1 END) as error_count
SUM(response_time_msec) / COUNT(1) AS average_response_time_msec
- FROM http_request
+ , jsonb_object_agg(request_country, country_count) AS country_counters
+ FROM (
+ SELECT *,
+ count(1) OVER (
+ PARTITION BY site_id, date_trunc('minute', ingest_time), request_country
+ ) AS country_count
+ FROM http_request
+ ) h
Now, if you want to get the number of requests that came from America in your dashboard, you can modify the dashboard query to look like this:
SELECT
request_count, success_count, error_count, average_response_time_msec,
COALESCE(country_counters->>'USA', '0')::int AS american_visitors
FROM http_request_1min
WHERE ingest_time > date_trunc('minute', now()) - '5 minutes'::interval;
J.5.6.3. Timeseries Data #
In a timeseries workload, applications (such as some real-time apps) query recent information, while archiving old information.
To deal with this workload, a single-node Postgres Pro database would typically use table partitioning to break a big table of time-ordered data into multiple inherited tables with each containing different time ranges.
Storing data in multiple physical tables speeds up data expiration. In a single big table, deleting rows incurs the cost of scanning to find which to delete, and then vacuuming the emptied space. On the other hand, dropping a partition is a fast operation independent of data size. It is the equivalent of simply removing files on disk that contain the data. See the figure below to learn more.
Figure J.4. Delete vs. Drop Diagram
Partitioning a table also makes indices smaller and faster within each date range. Queries operating on recent data are likely to operate on “hot” indices that fit in memory. This speeds up reads. See the figure below to learn more.
Figure J.5. SELECT Across Multiple Indexes
Also inserts have smaller indices to update, so they go faster too. See the figure below to learn more.
Figure J.6. INSERT Across Multiple Indexes
Time-based partitioning makes most sense when:
Most queries access a very small subset of the most recent data.
Older data is periodically expired (deleted/dropped).
Keep in mind that, in the wrong situation, reading all these partitions can hurt overhead more than it helps. However, in the right situations it is quite helpful. For example, when keeping a year of time series data and regularly querying only the most recent week.
J.5.6.3.1. Scaling Timeseries Data on citus #
We can mix the single-node table partitioning techniques with citus distributed sharding to make a scalable time-series database. It is the best of both worlds. It is especially elegant atop Postgres Pro declarative table partitioning. See the figure below to learn more.
Figure J.7. Timeseries Sharding and Partitioning
For example, let's distribute and partition a table holding the historical GitHub events data.
Each record in this GitHub data set represents an event created in GitHub, along with key information regarding the event such as event type, creation date, and the user who created the event.
The first step is to create and partition the table by time as we would in a single-node Postgres Pro database:
-- Declaratively partitioned table CREATE TABLE github_events ( event_id bigint, event_type text, event_public boolean, repo_id bigint, payload jsonb, repo jsonb, actor jsonb, org jsonb, created_at timestamp ) PARTITION BY RANGE (created_at);
Notice the PARTITION BY RANGE (created_at). This tells Postgres Pro that the table will be partitioned by the created_at column in ordered ranges. We have not yet created any partitions for specific ranges, though.
Before creating specific partitions, let's distribute the table in citus. We will shard by repo_id, meaning the events will be clustered into shards per repository.
SELECT create_distributed_table('github_events', 'repo_id');
At this point citus has created shards for this table across worker nodes. Internally each shard is a table with the name github_events_ for each shard identifier NN. Also, citus propagated the partitioning information, and each of these shards has Partition key: RANGE (created_at) declared.
A partitioned table cannot directly contain data, it is more like a view across its partitions. Thus the shards are not yet ready to hold data. We need to create partitions and specify their time ranges, after which we can insert data that match the ranges.
J.5.6.3.2. Automating Partition Creation #
citus provides helper functions for partition management. We can create a batch of monthly partitions using the create_time_partitions function:
SELECT create_time_partitions( table_name := 'github_events', partition_interval := '1 month', end_at := now() + '12 months' );
citus also includes the time_partitions view for an easy way to investigate the partitions it has created.
SELECT partition FROM time_partitions WHERE parent_table = 'github_events'::regclass; ┌────────────────────────┐ │ partition │ ├────────────────────────┤ │ github_events_p2021_10 │ │ github_events_p2021_11 │ │ github_events_p2021_12 │ │ github_events_p2022_01 │ │ github_events_p2022_02 │ │ github_events_p2022_03 │ │ github_events_p2022_04 │ │ github_events_p2022_05 │ │ github_events_p2022_06 │ │ github_events_p2022_07 │ │ github_events_p2022_08 │ │ github_events_p2022_09 │ │ github_events_p2022_10 │ └────────────────────────┘
As time progresses, you will need to do some maintenance to create new partitions and drop old ones. It is best to set up a periodic job to run the maintenance functions with an extension like pg_cron:
-- Set two monthly cron jobs:
-- 1. Ensure we have partitions for the next 12 months
SELECT cron.schedule('create-partitions', '0 0 1 * *', $$
SELECT create_time_partitions(
table_name := 'github_events',
partition_interval := '1 month',
end_at := now() + '12 months'
)
$$);
-- 2. (Optional) Ensure we never have more than one year of data
SELECT cron.schedule('drop-partitions', '0 0 1 * *', $$
CALL drop_old_time_partitions(
'github_events',
now() - interval '12 months' /* older_than */
);
$$);
Note
Be aware that native partitioning in Postgres Pro is still quite new and has a few quirks. Maintenance operations on partitioned tables will acquire aggressive locks that can briefly stall queries.
J.5.6.3.3. Archiving with Columnar Storage #
Some applications have data that logically divides into a small updatable part and a larger part that is “frozen”. Examples include logs, clickstreams, or sales records. In this case we can combine partitioning with columnar table storage to compress historical partitions on disk. citus columnar tables are currently append-only, meaning they do not support updates or deletes, but we can use them for the immutable historical partitions.
A partitioned table may be made up of any combination of row and columnar partitions. When using range partitioning on a timestamp key, we can make the newest partition a row table, and periodically roll the newest partition into another historical columnar partition.
Let's see an example, using GitHub events again. We will create a new table called github_columnar_events for disambiguation from the earlier example. To focus entirely on the columnar storage aspect, we will not distribute this table.
Next, download sample data:
wget http://examples.citusdata.com/github_archive/github_events-2015-01-01-{0..5}.csv.gz
gzip -c -d github_events-2015-01-01-*.gz >> github_events.csv
-- Our new table, same structure as the example in -- the previous section CREATE TABLE github_columnar_events ( LIKE github_events ) PARTITION BY RANGE (created_at); -- Create partitions to hold two hours of data each SELECT create_time_partitions( table_name := 'github_columnar_events', partition_interval := '2 hours', start_from := '2015-01-01 00:00:00', end_at := '2015-01-01 08:00:00' ); -- Fill with sample data -- (note that this data requires the database to have UTF8 encoding) \COPY github_columnar_events FROM 'github_events.csv' WITH (format CSV) -- List the partitions, and confirm they are -- using row-based storage (heap access method) SELECT partition, access_method FROM time_partitions WHERE parent_table = 'github_columnar_events'::regclass;
┌─────────────────────────────────────────┬───────────────┐ │ partition │ access_method │ ├─────────────────────────────────────────┼───────────────┤ │ github_columnar_events_p2015_01_01_0000 │ heap │ │ github_columnar_events_p2015_01_01_0200 │ heap │ │ github_columnar_events_p2015_01_01_0400 │ heap │ │ github_columnar_events_p2015_01_01_0600 │ heap │ └─────────────────────────────────────────┴───────────────┘
-- Convert older partitions to use columnar storage CALL alter_old_partitions_set_access_method( 'github_columnar_events', '2015-01-01 06:00:00' /* older_than */, 'columnar' ); -- The old partitions are now columnar, while the -- latest uses row storage and can be updated SELECT partition, access_method FROM time_partitions WHERE parent_table = 'github_columnar_events'::regclass;
┌─────────────────────────────────────────┬───────────────┐ │ partition │ access_method │ ├─────────────────────────────────────────┼───────────────┤ │ github_columnar_events_p2015_01_01_0000 │ columnar │ │ github_columnar_events_p2015_01_01_0200 │ columnar │ │ github_columnar_events_p2015_01_01_0400 │ columnar │ │ github_columnar_events_p2015_01_01_0600 │ heap │ └─────────────────────────────────────────┴───────────────┘
To see the compression ratio for a columnar table, use VACUUM VERBOSE. The compression ratio for our three columnar partitions is pretty good:
VACUUM VERBOSE github_columnar_events;
INFO: statistics for "github_columnar_events_p2015_01_01_0000": storage id: 10000000003 total file size: 4481024, total data size: 4444425 compression rate: 8.31x total row count: 15129, stripe count: 1, average rows per stripe: 15129 chunk count: 18, containing data for dropped columns: 0, zstd compressed: 18 INFO: statistics for "github_columnar_events_p2015_01_01_0200": storage id: 10000000004 total file size: 3579904, total data size: 3548221 compression rate: 8.26x total row count: 12714, stripe count: 1, average rows per stripe: 12714 chunk count: 18, containing data for dropped columns: 0, zstd compressed: 18 INFO: statistics for "github_columnar_events_p2015_01_01_0400": storage id: 10000000005 total file size: 2949120, total data size: 2917407 compression rate: 8.51x total row count: 11756, stripe count: 1, average rows per stripe: 11756 chunk count: 18, containing data for dropped columns: 0, zstd compressed: 18
One power of the partitioned table github_columnar_events is that it can be queried in its entirety like a normal table.
SELECT COUNT(DISTINCT repo_id) FROM github_columnar_events;
┌───────┐ │ count │ ├───────┤ │ 16001 │ └───────┘
Entries can be updated or deleted, as long as there is a WHERE clause on the partition key, which filters entirely into row table partitions.
Archiving a Row Partition to Columnar Storage #
When a row partition has filled its range, you can archive it to compressed columnar storage. We can automate this with pg_cron like so:
-- A monthly cron job
SELECT cron.schedule('compress-partitions', '0 0 1 * *', $$
CALL alter_old_partitions_set_access_method(
'github_columnar_events',
now() - interval '6 months' /* older_than */,
'columnar'
);
$$);
For more information, see the Columnar Storage section.
J.5.7. Architecture Concepts #
J.5.7.1. Nodes #
citus is a Postgres Pro extension that allows commodity database servers (called nodes) to coordinate with one another in a “shared-nothing” architecture. The nodes form a cluster that allows Postgres Pro to hold more data and use more CPU cores than would be possible on a single computer. This architecture also allows the database to scale by simply adding more nodes to the cluster.
Every cluster has one special node called the coordinator (the others are known as workers). Applications send their queries to the coordinator node, which relays it to the relevant workers and accumulates the results.
For each query, the coordinator either routes it to a single worker node, or parallelizes it across several depending on whether the required data lives on a single node or multiple. The coordinator knows how to do this by consulting its metadata tables. These tables specific to citus track the DNS names and health of worker nodes, and the distribution of data across nodes. For more information, see the citus Tables and Views section.
J.5.7.2. Sharding Models #
Sharding is a technique used in database systems and distributed computing to horizontally partition data across multiple servers or nodes. It involves breaking up a large database or dataset into smaller, more manageable parts called shards. Each shard contains a subset of the data, and together they form the complete dataset.
citus offers two types of data sharding: row-based and schema-based. Each option comes with its own sharding tradeoffs allowing you to choose the approach that best aligns with requirements of your application.
J.5.7.2.1. Row-Based Sharding #
The traditional way in which citus shards tables is the single database, shared schema model also known as row-based sharding, tenants co-exist as rows within the same table. The tenant is determined by defining the distribution column, which allows splitting up a table horizontally.
This is the most hardware efficient way of sharding. Tenants are densely packed and distributed among the nodes in the cluster. This approach, however, requires making sure that all tables in the schema have the distribution column and that all queries in the application filter by it. Row-based sharding shines in IoT workloads and for achieving the best margin out of hardware use.
Benefits:
Best performance
Best tenant density per node
Drawbacks:
Requires schema modifications
Requires application query modifications
All tenants must share the same schema
J.5.7.2.2. Schema-Based Sharding #
Schema-based sharding is the shared database, separate schema model, the schema becomes the logical shard within the database. Multi-tenant apps can a use a schema per tenant to easily shard along the tenant dimension. Query changes are not required and the application usually only needs a small modification to set the proper search_path when switching tenants. Schema-based sharding is an ideal solution for microservices, and for ISVs deploying applications that cannot undergo the changes required to onboard row-based sharding.
Benefits:
Tenants can have heterogeneous schemas
No schema modifications required
No application query modifications required
Schema-based sharding SQL compatibility is better compared to the row-based sharding
Drawbacks:
Fewer tenants per node compared to row-based sharding
J.5.7.2.3. Sharding Tradeoffs #
| Schema-Based Sharding | Row-Based Sharding | |
|---|---|---|
| Multi-tenancy model | Separate schema per tenant | Shared tables with tenant ID columns |
| citus version | 12.0+ | All versions |
| Additional steps compared to Postgres Pro | None, only a config change | Use the create_distributed_table function on each table to distribute and co-locate tables by tenant_id |
| Number of tenants | 1-10k | 1-1M+ |
| Data modelling requirement | No foreign keys across distributed schemas | Need to include the tenant_id column (a distribution column, also known as a sharding key) in each table, and in primary keys, foreign keys |
| SQL requirement for single node queries | Use a single distributed schema per query | Joins and WHERE clauses should include tenant_id column |
| Parallel cross-tenant queries | No | Yes |
| Custom table definitions per tenant | Yes | No |
| Access control | Schema permissions | Schema permissions |
| Data sharing across tenants | Yes, using reference tables (in a separate schema) | Yes, using reference tables |
| Tenant to shard isolation | Every tenant has its own shard group by definition | Can give specific tenant IDs their own shard group via the isolate_tenant_to_new_shard function. |
J.5.7.3. Distributed Data #
J.5.7.3.1. Table Types #
There are several types of tables in a citus cluster, each used for different purposes.
Type 1: Distributed Tables.
The first type, and most common, is distributed tables. These appear to be normal tables to SQL statements, but are horizontally partitioned across worker nodes. See the figure below to learn more.
Figure J.8. Parallel
SELECTDiagramHere the rows of
tableare stored in tablestable_1001,table_1002, etc. on the workers. The component worker tables are called shards.citus runs not only SQL but DDL statements throughout a cluster, so changing the schema of a distributed table cascades to update all the table shards across workers.
To learn how to create a distributed table, see the Creating and Modifying Distributed Objects (DDL) section.
Distribution Column. citus uses algorithmic sharding to assign rows to shards. This means the assignment is made deterministically — in our case based on the value of a particular table column called the distribution column. The cluster administrator must designate this column when distributing a table. Making the right choice is important for performance and functionality, as described in the general topic of the Choosing Distribution Column section.
Type 2: Reference Tables.
A reference table is a type of distributed table whose entire contents are concentrated into a single shard, which is replicated on every worker. Thus queries on any worker can access the reference information locally, without the network overhead of requesting rows from another node. Reference tables have no distribution column because there is no need to distinguish separate shards per row.
Reference tables are typically small and are used to store data that is relevant to queries running on any worker node. For example, enumerated values like order statuses or product categories.
When interacting with a reference table, we automatically perform two-phase commits on transactions. This means that citus makes sure your data is always in a consistent state, regardless of whether you are writing, modifying or deleting it.
The Reference Tables section talks more about these tables and how to create them.
Type 3: Local Tables.
When you use citus, the coordinator node you connect to and interact with is a regular Postgres Pro database with the citus extension installed. Thus you can create ordinary tables and choose not to shard them. This is useful for small administrative tables that do not participate in join queries. An example would be users table for application login and authentication.
Creating standard Postgres Pro tables is easy because it is the default. It is what you get when you run
CREATE TABLE. In almost every citus deployment we see standard Postgres Pro tables co-existing with distributed and reference tables. Indeed, citus itself uses local tables to hold cluster metadata, as mentioned earlier.Type 4: Local Managed Tables.
When the citus.enable_local_reference_table_foreign_keys configuration parameter is enabled, citus may automatically add local tables to metadata if a foreign key reference exists between a local table and a reference table. Additionally this tables can be manually created by calling the citus_add_local_table_to_metadata function on regular local tables. Tables present in metadata are considered managed tables and can be queried from any node, citus will know to route to the coordinator to obtain data from the local managed table. Such tables are displayed as local in the citus_tables view.
Type 5: Schema Tables.
When using schema-based sharding, distributed schemas are automatically associated with individual co-location groups such that the tables created in those schemas are automatically converted to co-located distributed tables without a shard key. Such tables are considered schema tables and are displayed as schema in the citus_tables view.
J.5.7.3.2. Shards #
The previous section described a shard as containing a subset of the rows of a distributed table in a smaller table within a worker node. This section gets more into the technical details.
The pg_dist_shard metadata table on the coordinator contains a row for each shard of each distributed table in the system. The row matches a shardid with a range of integers in a hash space (shardminvalue, shardmaxvalue):
SELECT * FROM pg_dist_shard; logicalrelid | shardid | shardstorage | shardminvalue | shardmaxvalue ---------------+---------+--------------+---------------+--------------- github_events | 102026 | t | 268435456 | 402653183 github_events | 102027 | t | 402653184 | 536870911 github_events | 102028 | t | 536870912 | 671088639 github_events | 102029 | t | 671088640 | 805306367 (4 rows)
If the coordinator node wants to determine which shard holds a row of github_events, it hashes the value of the distribution column in the row, and checks which shard's range contains the hashed value. (The ranges are defined so that the image of the hash function is their disjoint union.)
J.5.7.3.2.1. Shard Placements #
Suppose that shard 102027 is associated with the row in question. This means the row should be read or written to a table called github_events_102027 in one of the workers. Which worker? That is determined entirely by the metadata tables, and the mapping of shard to worker is known as the shard placement.
Joining some metadata tables gives us the answer. These are the types of lookups that the coordinator does to route queries. It rewrites queries into fragments that refer to the specific tables like github_events_102027, and runs those fragments on the appropriate workers.
SELECT
shardid,
node.nodename,
node.nodeport
FROM pg_dist_placement placement
JOIN pg_dist_node node
ON placement.groupid = node.groupid
AND node.noderole = 'primary'::noderole
WHERE shardid = 102027;
┌─────────┬───────────┬──────────┐ │ shardid │ nodename │ nodeport │ ├─────────┼───────────┼──────────┤ │ 102027 │ localhost │ 5433 │ └─────────┴───────────┴──────────┘
In our example of github_events there were four shards. The number of shards is configurable per table at the time of its distribution across the cluster. The best choice of shard count depends on your use case, see the Shard Count section.
Finally note that citus allows shards to be replicated for protection against data loss using Postgres Pro streaming replication to back up the entire database of each node to a follower database. This is transparent and does not require the involvement of citus metadata tables.
J.5.7.3.3. Co-Location #
Since shards can be placed on nodes as desired, it makes sense to place shards containing related rows of related tables together on the same nodes. That way join queries between them can avoid sending as much information over the network, and can be performed inside a single citus node.
One example is a database with stores, products, and purchases. If all three tables contain — and are distributed by — the store_id column, then all queries restricted to a single store can run efficiently on a single worker node. This is true even when the queries involve any combination of these tables.
For the full explanation and examples of this concept, see the Table Co-Location section.
J.5.7.3.4. Parallelism #
Spreading queries across multiple computers allows more queries to run at once, and allows processing speed to scale by adding new computers to the cluster. Additionally splitting a single query into fragments as described in the previous section boosts the processing power devoted to it. The latter situation achieves the greatest parallelism, meaning utilization of CPU cores.
Queries reading or affecting shards spread evenly across many nodes are able to run at “real-time” speed. Note that the results of the query still need to pass back through the coordinator node, so the speedup is most apparent when the final results are compact, such as aggregate functions like counting and descriptive statistics.
The Query Processing section explains more about how queries are broken into fragments and how their execution is managed.
J.5.7.4. Query Execution #
When executing multi-shard queries, citus must balance the gains from parallelism with the overhead from database connections (network latency and worker node resource usage). To configure citus query execution for best results with your database workload, it helps to understand how citus manages and conserves database connections between the coordinator node and worker nodes.
citus transforms each incoming multi-shard query session into per-shard queries called tasks. It queues the tasks, and runs them once it is able to obtain connections to the relevant worker nodes. For queries on distributed tables foo and bar, see the connection management diagram below.
Figure J.9. Executor Overview
The coordinator node has a connection pool for each session. Each query (such as SELECT * FROM foo in the diagram) is limited to opening at most simultaneous connections for its tasks per worker set in the citus.max_adaptive_executor_pool_size configuration parameter. It is configurable at the session level, for priority management.
It can be faster to execute short tasks sequentially over the same connection rather than establishing new connections for them in parallel. Long running tasks, on the other hand, benefit from more immediate parallelism.
To balance the needs of short and long tasks, citus uses the citus.executor_slow_start_interval configuration parameter. It specifies a delay between connection attempts for the tasks in a multi-shard query. When a query first queues tasks, the tasks can acquire just one connection. At the end of each interval where there are pending connections, citus increases the number of simultaneous connections it will open. The slow start behavior can be disabled entirely by setting the GUC to 0.
When a task finishes using a connection, the session pool will hold the connection open for later. Caching the connection avoids the overhead of connection reestablishment between coordinator and worker. However, each pool will hold no more than the number of idle connections open at once set by the citus.max_cached_conns_per_worker configuration parameter, to limit idle connection resource usage in the worker.
Finally, the citus.max_shared_pool_size configuration parameter acts as a fail-safe. It limits the total connections per worker between all tasks.
For recommendations about tuning these parameters to match your workload, see the Connection Management section.
J.5.8. Develop #
J.5.8.1. Determining Application Type #
Running efficient queries on a citus cluster requires that data be properly distributed across computers. This varies by the type of application and its query patterns.
There are broadly two kinds of applications that work very well on citus. The first step in data modeling is to identify which of them more closely resembles your application.
J.5.8.1.1. At a Glance #
| Multi-Tenant Applications | Real-Time Applications |
|---|---|
| Sometimes dozens or hundreds of tables in schema | Small number of tables |
| Queries relating to one tenant (company/store) at a time | Relatively simple analytics queries with aggregations |
| OLTP workloads for serving web clients | High ingest volume of mostly immutable data |
| OLAP workloads that serve per-tenant analytical queries | Often centering around a big table of events |
J.5.8.1.2. Examples and Characteristics #
J.5.8.1.2.1. Multi-Tenant Applications #
These are typically SaaS applications that serve other companies, accounts, or organizations. Most SaaS applications are inherently relational. They have a natural dimension on which to distribute data across nodes: just shard by tenant_id.
citus enables you to scale out your database to millions of tenants without having to re-architect your application. You can keep the relational semantics you need, like joins, foreign key constraints, transactions, ACID, and consistency.
Examples: Websites, which host store-fronts for other businesses, such as a digital marketing solution, or a sales automation tool.
Characteristics: Queries relating to a single tenant rather than joining information across tenants. This includes OLTP workloads for serving web clients, and OLAP workloads that serve per-tenant analytical queries. Having dozens or hundreds of tables in your database schema is also an indicator for the multi-tenant data model.
Scaling a multi-tenant app with citus also requires minimal changes to application code. We have support for popular frameworks like Ruby on Rails and Django.
J.5.8.1.2.2. Real-Time Analytics #
Applications needing massive parallelism, coordinating hundreds of cores for fast results to numerical, statistical, or counting queries. By sharding and parallelizing SQL queries across multiple nodes, citus makes it possible to perform real-time queries across billions of records in under a second.
Examples: Customer-facing analytics dashboards requiring sub-second response times.
Characteristics: Few tables, often centering around a big table of device-, site- or user-events and requiring high ingest volume of mostly immutable data. Relatively simple (but computationally intensive) analytics queries involving several aggregations and
GROUP BYoperations.
If your situation resembles either cases above, then the next step is to decide how to shard your data in the citus cluster. As explained in the Architecture Concepts section, citus assigns table rows to shards according to the hashed value of the table distribution column. The database administrator's choice of distribution columns needs to match the access patterns of typical queries to ensure performance.
J.5.8.2. Choosing Distribution Column #
citus uses the distribution column in distributed tables to assign table rows to shards. Choosing the distribution column for each table is one of the most important modeling decisions because it determines how data is spread across nodes.
If the distribution columns are chosen correctly, then related data will group together on the same physical nodes, making queries fast and adding support for all SQL features. If the columns are chosen incorrectly, the system will run needlessly slowly, and will not be able to support all SQL features across nodes.
This section gives distribution column tips for the two most common citus scenarios. It concludes by going in-depth on “co-location”, the desirable grouping of data on nodes.
J.5.8.2.1. Multi-Tenant Apps #
The multi-tenant architecture uses a form of hierarchical database modeling to distribute queries across nodes in the distributed cluster. The top of the data hierarchy is known as the tenant_id, and needs to be stored in a column on each table. citus inspects queries to see which tenant_id they involve and routes the query to a single worker node for processing, specifically the node that holds the data shard associated with the tenant_id. Running a query with all relevant data placed on the same node is called co-location.
The following diagram illustrates co-location in the multi-tenant data model. It contains two tables, Accounts and Campaigns, each distributed by account_id. The shaded boxes represent shards, each of whose color represents which worker node contains it. Green shards are stored together on one worker node, and blue on another. Notice how a join query between Accounts and Campaigns would have all the necessary data together on one node when restricting both tables to the same account_id.
Figure J.10. Multi-Tenant Co-Location
To apply this design in your own schema the first step is identifying what constitutes a tenant in your application. Common instances include company, account, organization, or customer. The column name will be something like company_id or customer_id. Examine each of your queries and ask yourself: would it work if it had additional WHERE clauses to restrict all tables involved to rows with the same tenant_id? Queries in the multi-tenant model are usually scoped to a tenant, for instance, queries on sales or inventory would be scoped within a certain store.
Best practices are as follows:
Partition distributed tables by the common
tenant_idcolumn. For instance, in a SaaS application where tenants are companies, thetenant_idwill likely becompany_id.Convert small cross-tenant tables to reference tables. When multiple tenants share a small table of information, distribute it as a reference table.
Restrict filter all application queries by
tenant_id. Each query should request information for one tenant at a time.
Consult the Multi-Tenant Applications section for a detailed example of building this kind of application.
J.5.8.2.2. Real-Time Apps #
While the multi-tenant architecture introduces a hierarchical structure and uses data co-location to route queries per tenant, real-time architectures depend on specific distribution properties of their data to achieve highly parallel processing.
We use “entity ID” as a term for distribution columns in the real-time model, as opposed to tenant IDs in the multi-tenant model. Typical entities are users, hosts, or devices.
Real-time queries typically ask for numeric aggregates grouped by date or category. citus sends these queries to each shard for partial results and assembles the final answer on the coordinator node. Queries run fastest when as many nodes contribute as possible, and when no single node must do a disproportionate amount of work.
Best practices are as follows:
Choose a column with high cardinality as the distribution column. For comparison, a “status” field on an order table with values “new”, “paid”, and “shipped” is a poor choice of distribution column because it assumes only those few values. The number of distinct values limits the number of shards that can hold the data, and the number of nodes that can process it. Among columns with high cardinality, it is good additionally to choose those that are frequently used in group-by clauses or as join keys.
Choose a column with even distribution. If you distribute a table on a column skewed to certain common values, then data in the table will tend to accumulate in certain shards. The nodes holding those shards will end up doing more work than other nodes.
Distribute fact and dimension tables on their common columns. Your fact table can have only one distribution key. Tables that join on another key will not be co-located with the fact table. Choose one dimension to co-locate based on how frequently it is joined and the size of the joining rows.
Change some dimension tables into reference tables. If a dimension table cannot be co-located with the fact table, you can improve query performance by distributing copies of the dimension table to all of the nodes in the form of a reference table.
Consult the Real-Time Dashboards section for a detailed example of building this kind of application.
J.5.8.2.3. Timeseries Data #
In a time-series workload, applications query recent information while archiving old information.
The most common mistake in modeling timeseries information in citus is using the timestamp itself as a distribution column. A hash distribution based on time will distribute times seemingly at random into different shards rather than keeping ranges of time together in shards. However, queries involving time generally reference ranges of time (for example, the most recent data), so such a hash distribution would lead to network overhead.
Best practices are as follows:
Do not choose a timestamp as the distribution column. Choose a different distribution column. In a multi-tenant app, use the
tenant_idor in a real-time app use theentity_id.Use Postgres Pro table partitioning for time instead. Use table partitioning to break a big table of time-ordered data into multiple inherited tables with each containing different time ranges. Distributing a Postgres Pro partitioned table in citus creates shards for the inherited tables.
Consult the Timeseries Data section for a detailed example of building this kind of application.
J.5.8.2.4. Table Co-Location #
Relational databases are the first choice of data store for many applications due to their enormous flexibility and reliability. Historically the one criticism of relational databases is that they can run on only a single computer, which creates inherent limitations when data storage needs outpace server improvements. The solution to rapidly scaling databases is to distribute them, but this creates a performance problem of its own: relational operations such as joins then need to cross the network boundary. Co-location is the practice of dividing data tactically, where one keeps related information on the same computers to enable efficient relational operations, but takes advantage of the horizontal scalability for the whole dataset.
The principle of data co-location is that all tables in the database have a common distribution column and are sharded across computers in the same way, such that rows with the same distribution column value are always on the same computer, even across different tables. As long as the distribution column provides a meaningful grouping of data, relational operations can be performed within the groups.
J.5.8.2.4.1. Data Co-Location in citus for Hash-Distributed Tables #
The citus extension for Postgres Pro is unique in being able to form a distributed database of databases. Every node in a citus cluster is a fully functional Postgres Pro database and the extension adds the experience of a single homogenous database on top. While it does not provide the full functionality of Postgres Pro in a distributed way, in many cases it can take full advantage of features offered by Postgres Pro on a single computer through co-location, including full SQL support, transactions, and foreign keys.
In citus a row is stored in a shard if the hash of the value in the distribution column falls within the shard hash range. To ensure co-location, shards with the same hash range are always placed on the same node even after rebalance operations, such that equal distribution column values are always on the same node across tables. See the figure below to learn more.
Figure J.11. Co-Location Shards
A distribution column that we have found to work well in practice is tenant_id in multi-tenant applications. For example, SaaS applications typically have many tenants, but every query they make is specific to a particular tenant. While one option is providing a database or schema for every tenant, it is often costly and impractical as there can be many operations that span across users (data loading, migrations, aggregations, analytics, schema changes, backups, etc). That becomes harder to manage as the number of tenants grows.
J.5.8.2.4.2. Practical Example of Co-Location #
Consider the following tables, which might be part of a multi-tenant web analytics SaaS:
CREATE TABLE event ( tenant_id int, event_id bigint, page_id int, payload jsonb, primary key (tenant_id, event_id) ); CREATE TABLE page ( tenant_id int, page_id int, path text, primary key (tenant_id, page_id) );
Now we want to answer queries that may be issued by a customer-facing dashboard, such as: “Return the number of visits in the past week for all pages starting with /blog in tenant six”.
J.5.8.2.4.3. Using Regular Postgres Pro Tables #
If our data was in a single Postgres Pro node, we could easily express our query using the rich set of relational operations offered by SQL:
SELECT page_id, count(event_id) FROM page LEFT JOIN ( SELECT * FROM event WHERE (payload->>'time')::timestamptz >= now() - interval '1 week' ) recent USING (tenant_id, page_id) WHERE tenant_id = 6 AND path LIKE '/blog%' GROUP BY page_id;
As long as the working set for this query fits in memory, this is an appropriate solution for many applications since it offers maximum flexibility. However, even if you do not need to scale yet, it can be useful to consider the implications of scaling out on your data model.
J.5.8.2.4.4. Distributing Tables by ID #
As the number of tenants and the data stored for each tenant grows, query times will typically go up as the working set no longer fits in memory or CPU becomes a bottleneck. In this case, we can shard the data across many nodes using citus. The first and the most important choice we need to make when sharding is the distribution column. Let's start with a naive choice of using event_id for the event table and page_id for the page table:
-- Naively use event_id and page_id as distribution columns
SELECT create_distributed_table('event', 'event_id');
SELECT create_distributed_table('page', 'page_id');
Given that the data is dispersed across different workers, we cannot simply perform a join as we would on a single Postgres Pro node. Instead, we will need to issue two queries:
Across all shards of the page table (Q1):
SELECT page_id FROM page WHERE path LIKE '/blog%' AND tenant_id = 6;
Across all shards of the event table (Q2):
SELECT page_id, count(*) AS count FROM event WHERE page_id IN (/*…page IDs from first query…*/) AND tenant_id = 6 AND (payload->>'time')::date >= now() - interval '1 week' GROUP BY page_id ORDER BY count DESC LIMIT 10;
Afterwards, the results from the two steps need to be combined by the application.
The data required to answer the query is scattered across the shards on the different nodes and each of those shards will need to be queried. See the figure below to learn more.
Figure J.12. Co-Location With Inefficient Queries
In this case the data distribution creates substantial drawbacks:
Overhead from querying each shard, running multiple queries.
Overhead of Q1 returning many rows to the client.
Q2 becomes very large.
The need to write queries in multiple steps, combine results, requires changes in the application.
A potential upside of the relevant data being dispersed is that the queries can be parallelised, which citus will do. However, this is only beneficial if the amount of work that the query does is substantially greater than the overhead of querying many shards. It is generally better to avoid doing such heavy lifting directly from the application, for example, by pre-aggregating the data.
J.5.8.2.4.5. Distributing Tables by Tenant #
Looking at our query again, we can see that all the rows that the query needs have one dimension in common: tenant_id. The dashboard will only ever query for a tenant's own data. That means that if data for the same tenant is always co-located on a single Postgres Pro node, our original query could be answered in a single step by that node by performing a join on tenant_id and page_id.
In citus, rows with the same distribution column value are guaranteed to be on the same node. Each shard in a distributed table effectively has a set of co-located shards from other distributed tables that contain the same distribution column values (data for the same tenant). Starting over, we can create our tables with tenant_id as the distribution column.
-- Co-locate tables by using a common distribution column
SELECT create_distributed_table('event', 'tenant_id');
SELECT create_distributed_table('page', 'tenant_id', colocate_with => 'event');
In this case, citus can answer the same query that you would run on a single Postgres Pro node without modification (Q1):
SELECT page_id, count(event_id) FROM page LEFT JOIN ( SELECT * FROM event WHERE (payload->>'time')::timestamptz >= now() - interval '1 week' ) recent USING (tenant_id, page_id) WHERE tenant_id = 6 AND path LIKE '/blog%' GROUP BY page_id;
Because of the tenant_id filter and join on tenant_id, citus knows that the entire query can be answered using the set of co-located shards that contain the data for that particular tenant, and the Postgres Pro node can answer the query in a single step, which enables full SQL support. See the figure below to learn more.
Figure J.13. Co-Location With Better Queries
In some cases, queries and table schemas will require minor modifications to ensure that the tenant_id is always included in unique constraints and join conditions. However, this is usually a straightforward change, and the extensive rewrite that would be required without having co-location is avoided.
While the example above queries just one node because there is a specific tenant_id = 6 filter, co-location also allows us to efficiently perform distributed joins on tenant_id across all nodes, be it with SQL limitations.
J.5.8.2.4.6. Co-Location Means Better Feature Support #
The full list of citus features that are unlocked by co-location are:
Full SQL support for queries on a single set of co-located shards.
Multi-statement transaction support for modifications on a single set of co-located shards.
Aggregation through
INSERT...SELECT.Foreign keys.
Distributed outer joins.
Pushdown CTEs.
Data co-location is a powerful technique for providing both horizontal scale and support to relational data models. The cost of migrating or building applications using a distributed database that enables relational operations through co-location is often substantially lower than moving to a restrictive data model (e.g. NoSQL) and, unlike a single-node database, it can scale out with the size of your business. For more information about migrating an existing database, see the Migrating an Existing App.
J.5.8.2.4.7. Query Performance #
citus parallelizes incoming queries by breaking it into multiple fragment queries (“tasks”), which run on the worker shards in parallel. This allows citus to utilize the processing power of all the nodes in the cluster and also of individual cores on each node for each query. Due to this parallelization, you can get performance, which is cumulative of the computing power of all of the cores in the cluster leading to a dramatic decrease in query times versus Postgres Pro on a single server.
citus employs a two stage optimizer when planning SQL queries. The first phase involves converting the SQL queries into their commutative and associative form so that they can be pushed down and run on the workers in parallel. As discussed in previous sections, choosing the right distribution column and distribution method allows the distributed query planner to apply several optimizations to the queries. This can have a significant impact on query performance due to reduced network I/O.
The distributed executor of the citus extension then takes these individual query fragments and sends them to worker Postgres Pro instances. There are several aspects of both the distributed planner and the executor, which can be tuned in order to improve performance. When these individual query fragments are sent to the workers, the second phase of query optimization kicks in. The workers are simply running extended Postgres Pro servers and they apply Postgres Pro standard planning and execution logic to run these fragment SQL queries. Therefore, any optimization that helps Postgres Pro also helps citus. Postgres Pro by default comes with conservative resource settings; and therefore optimizing these configuration settings can improve query times significantly.
We discuss the relevant performance tuning steps in the Query Performance Tuning section.
J.5.8.3. Migrating an Existing App #
Migrating an existing application to citus sometimes requires adjusting the schema and queries for optimal performance. citus extends Postgres Pro with distributed functionality, but row-based sharding is not a drop-in replacement that scales out all workloads. A performant citus cluster involves thinking about the data model, tooling, and choice of SQL features used.
There is another mode of operation in citus called schema-based sharding, and while row-based sharding results in best performance and hardware efficiency, see schema-based sharding if you are in a need for a more drop-in approach.
The first steps are to optimize the existing database schema so that it can work efficiently across multiple computers.
Next, update application code and queries to deal with the schema changes.
After testing the changes in a development environment, the last step is to migrate production data to a citus cluster and switch over the production app. We have techniques to minimize downtime for this step.
J.5.8.3.1. Identify Distribution Strategy #
J.5.8.3.1.1. Pick Distribution Key #
The first step in migrating to citus is identifying suitable distribution keys and planning table distribution accordingly. In multi-tenant applications this will typically be an internal identifier for tenants. We typically refer to it as the tenant_id. The use cases may vary, so we advise being thorough on this step.
For guidance, consult these sections:
Review your environment to be sure that the ideal distribution key is chosen. To do so, examine schema layouts, larger tables, long-running and/or problematic queries, standard use cases, and more.
J.5.8.3.1.2. Identify Types of Tables #
Once a distribution key is identified, review the schema to identify how each table will be handled and whether any modifications to table layouts will be required.
Tables will generally fall into one of the following categories:
Ready for distribution. These tables already contain the distribution key, and are ready for distribution.
Needs backfill. These tables can be logically distributed by the chosen key but do not contain a column directly referencing it. The tables will be modified later to add the column.
Reference table. These tables are typically small, do not contain the distribution key, are commonly joined by distributed tables, and/or are shared across tenants. A copy of each of these tables will be maintained on all nodes. Common examples include country code lookups, product categories, and the like.
Local table. These are typically not joined to other tables, and do not contain the distribution key. They are maintained exclusively on the coordinator node. Common examples include admin user lookups and other utility tables.
Consider an example multi-tenant application similar to Etsy or Shopify where each tenant is a store. A simplified schema is presented in the diagram below. (Underlined items are primary keys, italicized items are foreign keys.)
Figure J.14. Simplified Schema Example
In this example stores are a natural tenant. The tenant_id is in this case the store_id. After distributing tables in the cluster, we want rows relating to the same store to reside together on the same nodes.
J.5.8.3.2. Prepare Source Tables for Migration #
Once the scope of needed database changes is identified, the next major step is to modify the data structure for the application's existing database. First, tables requiring backfill are modified to add a column for the distribution key.
J.5.8.3.2.1. Add Distribution Keys #
In our storefront example the stores and products tables have a store_id and are ready for distribution. Being normalized, the line_items table lacks store_id. If we want to distribute by store_id, the table needs this column.
-- Denormalize line_items by including store_id ALTER TABLE line_items ADD COLUMN store_id uuid;
Be sure to check that the distribution column has the same type in all tables, e.g. do not mix int and bigint. The column types must match to ensure proper data co-location.
J.5.8.3.2.2. Backfill Newly Created Columns #
Once the schema is updated, backfill missing values for the tenant_id column in tables where the column was added. In our example line_items requires values for store_id.
We backfill the table by obtaining the missing values from a join query with orders:
UPDATE line_items SET store_id = orders.store_id FROM line_items INNER JOIN orders WHERE line_items.order_id = orders.order_id;
Doing the whole table at once may cause too much load on the database and disrupt other queries. The backfill can be done more slowly instead. One way to do that is to make a function that backfills small batches at a time, then call the function repeatedly with pg_cron.
-- The function to backfill up to ten
-- thousand rows from line_items
CREATE FUNCTION backfill_batch()
RETURNS void LANGUAGE sql AS $$
WITH batch AS (
SELECT line_items_id, order_id
FROM line_items
WHERE store_id IS NULL
LIMIT 1000
FOR UPDATE
SKIP LOCKED
)
UPDATE line_items AS li
SET store_id = orders.store_id
FROM batch, orders
WHERE batch.line_item_id = li.line_item_id
AND batch.order_id = orders.order_id;
$$;
-- Run the function every quarter hour
SELECT cron.schedule('*/15 * * * *', 'SELECT backfill_batch()');
-- Note the return value of cron.schedule
Once the backfill is caught up, the cron job can be disabled:
-- Assuming 42 is the job id returned -- from cron.schedule SELECT cron.unschedule(42);
J.5.8.3.3. Prepare Application for citus #
J.5.8.3.3.1. Set Up Development citus Cluster #
When modifying the application to work with citus, you will need a database to test against. Follow the instructions in the Installing citus on a Single Node section to set up the extension.
Next dump a copy of the schema from your application's original database and restore the schema in the new development database.
# get schema from source db pg_dump \ --format=plain \ --no-owner \ --schema-only \ --file=schema.sql \ --schema=target_schema \ postgres://user:pass@host:5432/db # load schema into test db psql postgres://user:pass@testhost:5432/db -f schema.sql
The schema should include a distribution key (tenant_id) in all tables you wish to distribute. Before running pg_dump for the schema, be sure to prepare source tables for migration.
Include Distribution Column in Keys #
citus cannot enforce uniqueness constraints unless a unique index or primary key contains the distribution column. Thus we must modify primary and foreign keys in our example to include store_id.
Some of the libraries listed in the next section are able to help migrate the database schema to include the distribution column in keys. However, here is an example of the underlying SQL commands to turn the simple keys composite in the development database:
BEGIN; -- Drop simple primary keys (cascades to foreign keys) ALTER TABLE products DROP CONSTRAINT products_pkey CASCADE; ALTER TABLE orders DROP CONSTRAINT orders_pkey CASCADE; ALTER TABLE line_items DROP CONSTRAINT line_items_pkey CASCADE; -- Recreate primary keys to include would-be distribution column ALTER TABLE products ADD PRIMARY KEY (store_id, product_id); ALTER TABLE orders ADD PRIMARY KEY (store_id, order_id); ALTER TABLE line_items ADD PRIMARY KEY (store_id, line_item_id); -- Recreate foreign keys to include would-be distribution column ALTER TABLE line_items ADD CONSTRAINT line_items_store_fkey FOREIGN KEY (store_id) REFERENCES stores (store_id); ALTER TABLE line_items ADD CONSTRAINT line_items_product_fkey FOREIGN KEY (store_id, product_id) REFERENCES products (store_id, product_id); ALTER TABLE line_items ADD CONSTRAINT line_items_order_fkey FOREIGN KEY (store_id, order_id) REFERENCES orders (store_id, order_id); COMMIT;
Thus completed, our schema from the previous section will look like this (Underlined items are primary keys, italicized items are foreign keys.):
Figure J.15. Simplified Schema Example
Be sure to modify data flows to add keys to incoming data.
J.5.8.3.3.2. Add Distribution Key to Queries #
Once the distribution key is present on all appropriate tables, the application needs to include it in queries. Take the following steps using a copy of the application running in a development environment, and testing against a citus back-end. After the application is working with the extension we will see how to migrate production data from the source database into a real citus cluster.
Application code and any other ingestion processes that write to the tables should be updated to include the new columns.
Running the application test suite against the modified schema on citus is a good way to determine which areas of the code need to be modified.
It is a good idea to enable database logging. The logs can help uncover stray cross-shard queries in a multi-tenant app that should be converted to per-tenant queries.
Cross-shard queries are supported, but in a multi-tenant application most queries should be targeted to a single node. For simple SELECT, UPDATE, and DELETE queries this means that the WHERE clause should filter by tenant_id. citus can then run these queries efficiently on a single node.
There are helper libraries for a number of popular application frameworks that make it easy to include tenant_id in queries:
It is possible to use the libraries for database writes first (including data ingestion) and later for read queries. The activerecord-multi-tenant gem, for instance, has a write-only mode that modifies only the write queries.
Other (SQL Principles) #
If you are using a different ORM than those above or executing multi-tenant queries more directly in SQL, follow these general principles. We will use our earlier example of the e-commerce application.
Suppose we want to get the details for an order. Distributed queries that filter on the tenant_id run most efficiently in multi-tenant apps, so the change below makes the query faster (while both queries return the same results):
-- Before SELECT * FROM orders WHERE order_id = 123; -- After SELECT * FROM orders WHERE order_id = 123 AND store_id = 42; -- <== added
The tenant_id column is not just beneficial but critical for INSERT statements. Inserts must include a value for the tenant_id column or else citus will be unable to route the data to the correct shard and will raise an error.
Finally, when joining tables make sure to filter by tenant_id too. For instance, here is how to inspect how many “awesome wool pants” a given store has sold:
-- One way is to include store_id in the join and also
-- filter by it in one of the queries
SELECT sum(l.quantity)
FROM line_items l
INNER JOIN products p
ON l.product_id = p.product_id
AND l.store_id = p.store_id
WHERE p.name='Awesome Wool Pants'
AND l.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'
-- Equivalently you omit store_id from the join condition
-- but filter both tables by it. This may be useful if
-- building the query in an ORM
SELECT sum(l.quantity)
FROM line_items l
INNER JOIN products p ON l.product_id = p.product_id
WHERE p.name='Awesome Wool Pants'
AND l.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'
AND p.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'
J.5.8.3.3.3. Enable Secure Connections #
Clients should connect to citus with SSL to protect information and prevent man-in-the-middle attacks.
Check for Cross-Node Traffic #
With large and complex application code-bases, certain queries generated by the application can often be overlooked and thus will not have the tenant_id filter on them. citus parallel executor will still execute these queries successfully, and so, during testing, these queries remain hidden since the application still works fine. However, if a query does not contain the tenant_id filter, citus executor will hit every shard in parallel, but only one will return any data. This consumes resources needlessly and may exhibit itself as a problem only when one moves to a higher-throughput production environment.
To prevent encountering such issues only after launching in production, one can set a config value to log queries, which hit more than one shard. In a properly configured and migrated multi-tenant application, each query should only hit one shard at a time.
During testing, one can configure the following:
-- Adjust for your own database's name of course ALTER DATABASE citus SET citus.multi_task_query_log_level = 'error';
citus will then error out if it encounters queries that are going to hit more than one shard. Erroring out during testing allows the application developer to find and migrate such queries.
During a production launch, one can configure the same setting to log, instead of error out:
ALTER DATABASE citus SET citus.multi_task_query_log_level = 'log';
Visit the citus.multi_task_query_log_level section description to learn more about the supported values.
J.5.8.3.4. Migrate Production Data #
At this time, having updated the database schema and application queries to work with citus, you are ready for the final step. It is time to migrate data to the citus cluster and cut over the application to its new database. The data migration procedure is presented in the Database Migration section.
J.5.8.3.4.1. Database Migration #
For smaller environments that can tolerate a little downtime, use a simple pg_dump/pg_restore process. Here are the steps:
Save the database structure from your development database:
pg_dump \ --format=plain \ --no-owner \ --schema-only \ --file=schema.sql \ --schema=
target_schema\ postgres://user:pass@host:5432/dbConnect to the citus cluster using psql and create a schema:
\i schema.sql
Call the create_distributed_table and create_reference_table functions. If you get an error about foreign keys, it is generally due to the order of operations. Drop foreign keys before distributing tables and then re-add them.
Put the application into maintenance mode and disable any other writes to the old database.
Save the data from the original production database to disk with pg_dump:
pg_dump \ --format=custom \ --no-owner \ --data-only \ --file=data.dump \ --schema=
target_schema\ postgres://user:pass@host:5432/dbImport into citus using pg_restore:
# remember to use connection details for citus, # not the source database pg_restore \ --host=
host\ --dbname=dbname\ --username=username\ data.dump # it will prompt you for the connection passwordTest application.
J.5.8.4. SQL Reference #
J.5.8.4.1. Creating and Modifying Distributed Objects (DDL) #
J.5.8.4.1.1. Creating and Distributing Schemas #
citus supports schema-based sharding, which allows a schema to be distributed. Distributed schemas are automatically associated with individual co-location groups such that the tables created in those schemas will be automatically converted to co-located distributed tables without a shard key.
There are two ways in which a schema can be distributed in citus:
Manually by calling the citus_schema_distribute function:
SELECT citus_schema_distribute('user_service');This method also allows you to convert existing regular schemas into distributed schemas.
Note
You can only distribute schemas that do not contain distributed and reference tables.
Alternative approach is to enable the citus.enable_schema_based_sharding configuration parameter:
SET citus.enable_schema_based_sharding TO ON; CREATE SCHEMA AUTHORIZATION user_service;
The parameter can be changed for the current session or permanently in
postgresql.conf. With the parameter set toON, all created schemas are be distributed by default.The process of distributing the schema will automatically assign and move it to an existing node in the cluster. The background shard rebalancer takes these schemas and all tables within them when rebalancing the cluster, performing the optimal moves, and migrating the schemas between the nodes in the cluster.
To convert a schema back into a regular Postgres Pro schema, use the citus_schema_undistribute function:
SELECT citus_schema_undistribute('user_service');
The tables and data in the user_service schema will be moved from the current node back to the coordinator node in the cluster.
J.5.8.4.1.2. Creating and Distributing Tables #
To create a distributed table, you need to first define the table schema. To do so, you can define a table using the CREATE TABLE command in the same way as you would do with a regular Postgres Pro table.
CREATE TABLE github_events
(
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
actor jsonb,
org jsonb,
created_at timestamp
);
Next, you can use the create_distributed_table function to specify the table distribution column and create the worker shards.
SELECT create_distributed_table('github_events', 'repo_id');
This function informs citus that the github_events table should be distributed on the repo_id column (by hashing the column value). The function also creates shards on the worker nodes using the citus.shard_count configuration parameter.
This example would create a total of citus.shard_count number of shards where each shard owns a portion of a hash token space. Once the shards are created, this function saves all distributed metadata on the coordinator.
Each created shard is assigned a unique shard_id. Each shard is represented on the worker node as a regular Postgres Pro table with the tablename_shardid name where tablename is the name of the distributed table and shardid is the unique ID assigned to that shard. You can connect to the worker Postgres Pro instances to view or run commands on individual shards.
You are now ready to insert data into the distributed table and run queries on it. You can also learn more about the function used in this section in the citus Utility Functions section.
Reference Tables #
The above method distributes tables into multiple horizontal shards, but another possibility is distributing tables into a single shard and replicating the shard to every worker node. Tables distributed this way are called reference tables. They are used to store data that needs to be frequently accessed by multiple nodes in a cluster.
Common candidates for reference tables include:
Smaller tables that need to join with larger distributed tables.
Tables in multi-tenant apps that lack a
tenant_idcolumn or which are not associated with a tenant. (In some cases, to reduce migration effort, users might even choose to make reference tables out of tables associated with a tenant but which currently lack a tenant ID.)Tables that need unique constraints across multiple columns and are small enough.
For instance, suppose a multi-tenant eCommerce site needs to calculate sales tax for transactions in any of its stores. Tax information is not specific to any tenant. It makes sense to consolidate it in a shared table. A US-centric reference table might look like this:
-- A reference table
CREATE TABLE states (
code char(2) PRIMARY KEY,
full_name text NOT NULL,
general_sales_tax numeric(4,3)
);
-- Distribute it to all workers
SELECT create_reference_table('states');
Now queries such as one calculating tax for a shopping cart can join on the states table with no network overhead and can add a foreign key to the state code for better validation.
In addition to distributing a table as a single replicated shard, the create_reference_table function marks it as a reference table in the citus metadata tables. citus automatically performs two-phase commits for modifications to tables marked this way, which provides strong consistency guarantees.
If you have an existing distributed table, you can change it to a reference table by running:
SELECT undistribute_table('table_name');
SELECT create_reference_table('table_name');
For another example of using reference tables in a multi-tenant application, see the Sharing Data Between Tenants section.
Distributing Coordinator Data #
If an existing Postgres Pro database is converted into the coordinator node for a citus cluster, the data in its tables can be distributed efficiently and with minimal interruption to an application.
The create_distributed_table function described earlier works on both empty and non-empty tables and for the latter it automatically distributes table rows throughout the cluster. You will know if it does this by the presence of the following message: NOTICE: Copying data from local table.... For example:
CREATE TABLE series AS SELECT i FROM generate_series(1,1000000) i;
SELECT create_distributed_table('series', 'i');
NOTICE: Copying data from local table...
NOTICE: copying the data has completed
DETAIL: The local data in the table is no longer visible, but is still on disk.
HINT: To remove the local data, run: SELECT truncate_local_data_after_distributing_table($$public.series$$)
create_distributed_table
--------------------------
(1 row)
Writes on the table are blocked while the data is migrated, and pending writes are handled as distributed queries once the function commits. (If the function fails, then the queries become local again.) Reads can continue as normal and will become distributed queries once the function commits.
When distributing tables A and B, where A has a foreign key to B, distribute the key destination table B first. Doing it in the wrong order will cause an error:
ERROR: cannot create foreign key constraint DETAIL: Referenced table must be a distributed table or a reference table.
If it is not possible to distribute in the correct order, then drop the foreign keys, distribute the tables, and recreate the foreign keys.
After the tables are distributed, use the truncate_local_data_after_distributing_table function to remove local data. Leftover local data in distributed tables is inaccessible to citus queries and can cause irrelevant constraint violations on the coordinator.
J.5.8.4.1.3. Co-Locating Tables #
Co-location is the practice of dividing data tactically, keeping related information on the same computers to enable efficient relational operations, while taking advantage of the horizontal scalability for the whole dataset. For more information and examples, see the Table Co-Location section.
Tables are co-located in groups. To manually control a table's co-location group assignment use the optional colocate_with parameter of the create_distributed_table function. If you do not care about a table's co-location, then omit this parameter. It defaults to the value 'default', which groups the table with any other default co-location table having the same distribution column type and shard count. If you want to break or update this implicit co-location, you can use the update_distributed_table_colocation function.
-- These tables are implicitly co-located by using the same
-- distribution column type and shard count with the default
-- co-location group
SELECT create_distributed_table('A', 'some_int_col');
SELECT create_distributed_table('B', 'other_int_col');
When a new table is not related to others in its would-be implicit co-location group, specify colocated_with => 'none'.
-- Not co-located with other tables
SELECT create_distributed_table('A', 'foo', colocate_with => 'none');
Splitting unrelated tables into their own co-location groups will improve shard rebalancing performance, because shards in the same group have to be moved together.
When tables are indeed related (for instance when they will be joined), it can make sense to explicitly co-locate them. The gains of appropriate co-location are more important than any rebalancing overhead.
To explicitly co-locate multiple tables, distribute one and then put the others into its co-location group. For example:
-- Distribute stores
SELECT create_distributed_table('stores', 'store_id');
-- Add to the same group as stores
SELECT create_distributed_table('orders', 'store_id', colocate_with => 'stores');
SELECT create_distributed_table('products', 'store_id', colocate_with => 'stores');
Information about co-location groups is stored in the pg_dist_colocation table, while the pg_dist_partition table reveals which tables are assigned to which groups.
J.5.8.4.1.4. Dropping Tables #
You can use the standard Postgres Pro DROP TABLE command to remove your distributed tables. As with regular tables, DROP TABLE removes any indexes, rules, triggers, and constraints that exist for the target table. In addition, it also drops the shards on the worker nodes and cleans up their metadata.
DROP TABLE github_events;
J.5.8.4.1.5. Modifying Tables #
citus automatically propagates many kinds of DDL statements, which means that modifying a distributed table on the coordinator node will update shards on the workers too. Other DDL statements require manual propagation, and certain others are prohibited such as those which would modify a distribution column. Attempting to run DDL that is ineligible for automatic propagation will raise an error and leave tables on the coordinator node unchanged.
Here is a reference of the categories of DDL statements, which propagate. Note that automatic propagation can be enabled or disabled with the citus.enable_ddl_propagation configuration parameter.
Adding/Modifying Columns #
citus propagates most ALTER TABLE commands automatically. Adding columns or changing their default values work as they would in a single-machine Postgres Pro database:
-- Adding a column ALTER TABLE products ADD COLUMN description text; -- Changing default value ALTER TABLE products ALTER COLUMN price SET DEFAULT 7.77;
Significant changes to an existing column like renaming it or changing its data type are fine too. However, the data type of the distribution column cannot be altered. This column determines how table data distributes through the citus cluster, and modifying its data type would require moving the data.
Attempting to do so causes an error:
-- Assuming store_id is the distribution column -- for products and that it has type integer ALTER TABLE products ALTER COLUMN store_id TYPE text; /* ERROR: cannot execute ALTER TABLE command involving partition column */
As a workaround, you can consider changing the distribution column using the alter_distributed_table function, updating it, and changing it back.
Adding/Removing Constraints #
Using citus allows you to continue to enjoy the safety of a relational database, including database constraints. Due to the nature of distributed systems, citus will not cross-reference uniqueness constraints or referential integrity between worker nodes.
To set up a foreign key between co-located distributed tables, always include the distribution column in the key. This may involve making the key compound.
Foreign keys may be created in these situations:
between two local (non-distributed) tables,
between two reference tables,
between reference tables and local tables (by default enabled via the citus.enable_local_reference_table_foreign_keys configuration parameter),
between two co-located distributed tables when the key includes the distribution column, or
as a distributed table referencing a reference table.
Foreign keys from reference tables to distributed tables are not supported.
citus supports all referential actions on foreign keys from local to reference tables but does not support ON DELETE/ON UPDATE CASCADE in the reverse direction (reference to local).
Note
Primary keys and uniqueness constraints must include the distribution column. Adding them to a non-distribution column will generate the creating unique indexes on non-partition columns is currently unsupported error.
This example shows how to create primary and foreign keys on distributed tables:
--
-- Adding a primary key
-- --------------------
-- We will distribute these tables on the account_id. The ads and clicks
-- tables must use compound keys that include account_id
ALTER TABLE accounts ADD PRIMARY KEY (id);
ALTER TABLE ads ADD PRIMARY KEY (account_id, id);
ALTER TABLE clicks ADD PRIMARY KEY (account_id, id);
-- Next distribute the tables
SELECT create_distributed_table('accounts', 'id');
SELECT create_distributed_table('ads', 'account_id');
SELECT create_distributed_table('clicks', 'account_id');
--
-- Adding foreign keys
-- -------------------
-- Note that this can happen before or after distribution, as long as
-- there exists a uniqueness constraint on the target column(s), which
-- can only be enforced before distribution
ALTER TABLE ads ADD CONSTRAINT ads_account_fk
FOREIGN KEY (account_id) REFERENCES accounts (id);
ALTER TABLE clicks ADD CONSTRAINT clicks_ad_fk
FOREIGN KEY (account_id, ad_id) REFERENCES ads (account_id, id);
Similarly, include the distribution column in uniqueness constraints:
-- Suppose we want every ad to use a unique image. Notice we can -- enforce it only per account when we distribute by account_id ALTER TABLE ads ADD CONSTRAINT ads_unique_image UNIQUE (account_id, image_url);
Not-null constraints can be applied to any column (distribution or not) because they require no lookups between workers.
ALTER TABLE ads ALTER COLUMN image_url SET NOT NULL;
Using NOT VALID Constraints #
In some situations it can be useful to enforce constraints for new rows, while allowing existing non-conforming rows to remain unchanged. citus supports this feature for the CHECK constraints and foreign keys using the Postgres Pro NOT VALID constraint designation.
For example, consider an application that stores user profiles in a reference table.
-- We are using the "text" column type here, but a real application
-- might use "citext", which is available in the
-- Postgres Pro contrib module
CREATE TABLE users ( email text PRIMARY KEY );
SELECT create_reference_table('users');
In the course of time imagine that a few non-addresses get into the table.
INSERT INTO users VALUES
('foo@example.com'), ('hacker12@aol.com'), ('lol');
We would like to validate the addresses, but Postgres Pro does not ordinarily allow us to add the CHECK constraint that fails for existing rows. However, it does allow a constraint marked NOT VALID:
ALTER TABLE users
ADD CONSTRAINT syntactic_email
CHECK (email ~
'^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$'
) NOT VALID;
This succeeds, and new rows are protected.
INSERT INTO users VALUES ('fake');
/*
ERROR: new row for relation "users_102010" violates
check constraint "syntactic_email_102010"
DETAIL: Failing row contains (fake).
*/
Later, during non-peak hours, a database administrator can attempt to fix the bad rows and re-validate the constraint.
-- Later, attempt to validate all rows ALTER TABLE users VALIDATE CONSTRAINT syntactic_email;
The Postgres Pro documentation has more information about NOT VALID and VALIDATE CONSTRAINT in the section about the ALTER TABLE command.
Adding/Removing Indices #
citus supports adding and removing indices:
-- Adding an index CREATE INDEX clicked_at_idx ON clicks USING BRIN (clicked_at); -- Removing an index DROP INDEX clicked_at_idx;
Adding an index takes a write lock, which can be undesirable in a multi-tenant “system-of-record”. To minimize application downtime, create the index concurrently instead. This method requires more total work than a standard index build and takes significantly longer to complete. However, since it allows normal operations to continue while the index is built, this method is useful for adding new indexes in a production environment.
-- Adding an index without locking table writes CREATE INDEX CONCURRENTLY clicked_at_idx ON clicks USING BRIN (clicked_at);
J.5.8.4.1.6. Types and Functions #
Creating custom SQL types and user-defined functions propogates to worker nodes. However, creating such database objects in a transaction with distributed operations involves tradeoffs.
citus parallelizes operations such as create_distributed_table across shards using multiple connections per worker. Whereas, when creating a database object, citus propagates it to worker nodes using a single connection per worker. Combining the two operations in a single transaction may cause issues, because the parallel connections will not be able to see the object that was created over a single connection but not yet committed.
Consider a transaction block that creates a type, a table, loads data, and distributes the table:
BEGIN;
-- Type creation over a single connection:
CREATE TYPE coordinates AS (x int, y int);
CREATE TABLE positions (object_id text primary key, position coordinates);
-- Data loading thus goes over a single connection:
SELECT create_distributed_table('positions', 'object_id');
\COPY positions FROM 'positions.csv'
COMMIT;
citus default behaviour prioritizes schema consistency between coordinator and worker nodes. This behavior has a downside: if object propagation happens after a parallel command in the same transaction, then the transaction can no longer be completed, as highlighted by the ERROR in the code block below:
BEGIN;
CREATE TABLE items (key text, value text);
-- Parallel data loading:
SELECT create_distributed_table('items', 'key');
\COPY items FROM 'items.csv'
CREATE TYPE coordinates AS (x int, y int);
ERROR: cannot run type command because there was a parallel operation on a distributed table in the transaction
If you run into this issue, there is a simple workaround: use the citus.multi_shard_modify_mode parameter set to sequential to disable per-node parallelism. Data load in the same transaction might be slower.
J.5.8.4.1.7. Manual Modification #
Most DDL commands are auto-propagated. For any others, you can propagate the changes manually. See the Manual Query Propagation section.
J.5.8.4.2. Ingesting, Modifying Data (DML) #
J.5.8.4.2.1. Inserting Data #
To insert data into distributed tables, you can use the standard Postgres Pro INSERT command. As an example, we pick two rows randomly from the GitHub Archive dataset.
/*
CREATE TABLE github_events
(
event_id bigint,
event_type text,
event_public boolean,
repo_id bigint,
payload jsonb,
repo jsonb,
actor jsonb,
org jsonb,
created_at timestamp
);
*/
INSERT INTO github_events VALUES (2489373118,'PublicEvent','t',24509048,'{}','{"id": 24509048, "url": "https://api.github.com/repos/SabinaS/csee6868", "name": "SabinaS/csee6868"}','{"id": 2955009, "url": "https://api.github.com/users/SabinaS", "login": "SabinaS", "avatar_url": "https://avatars.githubusercontent.com/u/2955009?", "gravatar_id": ""}',NULL,'2015-01-01 00:09:13');
INSERT INTO github_events VALUES (2489368389,'WatchEvent','t',28229924,'{"action": "started"}','{"id": 28229924, "url": "https://api.github.com/repos/inf0rmer/blanket", "name": "inf0rmer/blanket"}','{"id": 1405427, "url": "https://api.github.com/users/tategakibunko", "login": "tategakibunko", "avatar_url": "https://avatars.githubusercontent.com/u/1405427?", "gravatar_id": ""}',NULL,'2015-01-01 00:00:24');
When inserting rows into distributed tables, the distribution column of the row being inserted must be specified. Based on the distribution column, citus determines the right shard to which the insert should be routed to. Then, the query is forwarded to the right shard, and the remote INSERT command is executed on all the replicas of that shard.
Sometimes it is convenient to put multiple INSERT statements together into a single INSERT of multiple rows. It can also be more efficient than making repeated database queries. For instance, the example from the previous section can be loaded all at once like this:
INSERT INTO github_events VALUES
(
2489373118,'PublicEvent','t',24509048,'{}','{"id": 24509048, "url": "https://api.github.com/repos/SabinaS/csee6868", "name": "SabinaS/csee6868"}','{"id": 2955009, "url": "https://api.github.com/users/SabinaS", "login": "SabinaS", "avatar_url": "https://avatars.githubusercontent.com/u/2955009?", "gravatar_id": ""}',NULL,'2015-01-01 00:09:13'
), (
2489368389,'WatchEvent','t',28229924,'{"action": "started"}','{"id": 28229924, "url": "https://api.github.com/repos/inf0rmer/blanket", "name": "inf0rmer/blanket"}','{"id": 1405427, "url": "https://api.github.com/users/tategakibunko", "login": "tategakibunko", "avatar_url": "https://avatars.githubusercontent.com/u/1405427?", "gravatar_id": ""}',NULL,'2015-01-01 00:00:24'
);
Distributed Rollups #
citus also supports INSERT … SELECT statements, which insert rows based on the results of the SELECT query. This is a convenient way to fill tables and also allows UPSERTS with the ON CONFLICT clause, the easiest way to do distributed rollups.
In citus there are three ways that inserting from the SELECT statement can happen:
The first is if the source tables and the destination table are co-located and the
SELECTstatements both include the distribution column. In this case, citus can push the/INSERTINSERT … SELECTstatement down for parallel execution on all nodes.The second way of executing the
INSERT … SELECTstatement is by repartitioning the results of the result set into chunks, and sending those chunks among workers to matching destination table shards. Each worker node can insert the values into local destination shards.The repartitioning optimization can happen when the
SELECTquery does not require a merge step on the coordinator. It does nor work with the following SQL features, which require a merge step:ORDER BYLIMITOFFSETGROUP BYwhen distribution column is not part of the group keyWindow functions when partitioning by a non-distribution column in the source table(s)
Joins between non-colocated tables (i.e. repartition joins)
When the source and destination tables are not co-located and the repartition optimization cannot be applied, then citus uses the third way of executing
INSERT … SELECT. It selects the results from worker nodes and pulls the data up to the coordinator node. The coordinator redirects rows back down to the appropriate shard. Because all the data must pass through a single node, this method is not as efficient.
When in doubt about which method citus is using, use the EXPLAIN command, as described in the Postgres Pro Tuning section. When the target table has a very large shard count, it may be wise to disable repartitioning, see the citus.enable_repartitioned_insert_select configuration parameter.
The \copy Command (Bulk Load) #
To bulk load data from a file, you can directly use the \copy command.
First download our example github_events dataset by running:
wget http://examples.citusdata.com/github_archive/github_events-2015-01-01-{0..5}.csv.gz
gzip -d github_events-2015-01-01-*.gz
Then, you can copy the data using psql. Note that this data requires the database to have UTF-8 encoding:
\COPY github_events FROM 'github_events-2015-01-01-0.csv' WITH (format CSV)
Note
There is no notion of snapshot isolation across shards, which means that a multi-shard SELECT that runs concurrently with the \copy command might see it committed on some shards, but not on others. If the user is storing events data, he may occasionally observe small gaps in recent data. It is up to applications to deal with this if it is a problem (e.g. exclude the most recent data from queries or use some lock).
If \copy fails to open a connection for a shard placement, then it behaves in the same way as INSERT, namely to mark the placement(s) as inactive unless there are no more active placements. If any other failure occurs after connecting, the transaction is rolled back and thus no metadata changes are made.
J.5.8.4.3. Caching Aggregations with Rollups #
Applications like event data pipelines and real-time dashboards require sub-second queries on large volumes of data. One way to make these queries fast is by calculating and saving aggregates ahead of time. This is called “rolling up” the data and it avoids the cost of processing raw data at run-time. As an extra benefit, rolling up timeseries data into hourly or daily statistics can also save space. Old data may be deleted when its full details are no longer needed and aggregates suffice.
For example, here is a distributed table for tracking page views by URL:
CREATE TABLE page_views (
site_id int,
url text,
host_ip inet,
view_time timestamp default now(),
PRIMARY KEY (site_id, url)
);
SELECT create_distributed_table('page_views', 'site_id');
Once the table is populated with data, we can run an aggregate query to count page views per URL per day, restricting to a given site and year.
-- How many views per url per day on site 5?
SELECT view_time::date AS day, site_id, url, count(*) AS view_count
FROM page_views
WHERE site_id = 5 AND
view_time >= date '2016-01-01' AND view_time < date '2017-01-01'
GROUP BY view_time::date, site_id, url;
The setup described above works but has two drawbacks. First, when you repeatedly execute the aggregate query, it must go over each related row and recompute the results for the entire data set. If you are using this query to render a dashboard, it is faster to save the aggregated results in a daily page views table and query that table. Second, storage costs will grow proportionally with data volumes and the length of queryable history. In practice, you may want to keep raw events for a short time period and look at historical graphs over a longer time window.
To receive those benefits, we can create the daily_page_views table to store the daily statistics.
CREATE TABLE daily_page_views (
site_id int,
day date,
url text,
view_count bigint,
PRIMARY KEY (site_id, day, url)
);
SELECT create_distributed_table('daily_page_views', 'site_id');
In this example, we distributed both page_views and daily_page_views on the site_id column. This ensures that data corresponding to a particular site will be co-located on the same node. Keeping the rows of the two tables together on each node minimizes network traffic between nodes and enables highly parallel execution.
Once we create this new distributed table, we can then run INSERT INTO ... SELECT to roll up raw page views into the aggregated table. In the following, we aggregate page views each day. citus users often wait for a certain time period after the end of day to run a query like this, to accommodate late arriving data.
-- Roll up yesterday's data
INSERT INTO daily_page_views (day, site_id, url, view_count)
SELECT view_time::date AS day, site_id, url, count(*) AS view_count
FROM page_views
WHERE view_time >= date '2017-01-01' AND view_time < date '2017-01-02'
GROUP BY view_time::date, site_id, url;
-- Now the results are available right out of the table
SELECT day, site_id, url, view_count
FROM daily_page_views
WHERE site_id = 5 AND
day >= date '2016-01-01' AND day < date '2017-01-01';
The rollup query above aggregates data from the previous day and inserts it into the daily_page_views table. Running the query once each day means that no rollup tables rows need to be updated, because the new day's data does not affect previous rows.
The situation changes when dealing with late arriving data, or running the rollup query more than once per day. If any new rows match days already in the rollup table, the matching counts should increase. Postgres Pro can handle this situation with ON CONFLICT, which is its technique for doing UPSERTS. Here is an example.
-- Roll up from a given date onward,
-- updating daily page views when necessary
INSERT INTO daily_page_views (day, site_id, url, view_count)
SELECT view_time::date AS day, site_id, url, count(*) AS view_count
FROM page_views
WHERE view_time >= date '2017-01-01'
GROUP BY view_time::date, site_id, url
ON CONFLICT (day, url, site_id) DO UPDATE SET
view_count = daily_page_views.view_count + EXCLUDED.view_count;
J.5.8.4.3.1. Updates and Deletion #
You can update or delete rows from your distributed tables using the standard Postgres Pro UPDATE and DELETE commands.
DELETE FROM github_events WHERE repo_id IN (24509048, 24509049); UPDATE github_events SET event_public = TRUE WHERE (org->>'id')::int = 5430905;
When the UPDATE/DELETE operations affect multiple shards as in the above example, citus defaults to using a one-phase commit protocol. For greater safety you can enable two-phase commits by setting the citus.multi_shard_commit_protocol configuration parameter:
SET citus.multi_shard_commit_protocol = '2pc';
If an UPDATE or DELETE operation affects only a single shard, then it runs within a single worker node. In this case enabling 2PC is unnecessary. This often happens when updates or deletes filter by a table's distribution column:
-- Since github_events is distributed by repo_id, -- this will execute in a single worker node DELETE FROM github_events WHERE repo_id = 206084;
Furthermore, when dealing with a single shard, citus supports SELECT … FOR UPDATE. This is a technique sometimes used by object-relational mappers (ORMs) to safely:
Load rows
Make a calculation in application code
Update the rows based on calculation
Selecting the rows for update puts a write lock on them to prevent other processes from causing the “lost update” anomaly.
BEGIN; -- Select events for a repo, but -- lock them for writing SELECT * FROM github_events WHERE repo_id = 206084 FOR UPDATE; -- Calculate a desired value event_public using -- application logic that uses those rows -- Now make the update UPDATE github_events SET event_public = :our_new_value WHERE repo_id = 206084; COMMIT;
This feature is supported for hash distributed and reference tables only.
J.5.8.4.3.2. Maximizing Write Performance #
Both INSERT and UPDATE/DELETE statements can be scaled up to around 50,000 queries per second on large machines. However, to achieve this rate, you will need to use many parallel, long-lived connections and consider how to deal with locking. For more information, you can consult the Scaling Out Data Ingestion section.
J.5.8.4.4. Querying Distributed Tables (SQL) #
As discussed in the previous sections, citus extends the latest Postgres Pro for distributed execution. This means that you can use standard Postgres Pro SELECT queries on the citus coordinator. The extension will then parallelize the SELECT queries involving complex selections, groupings and orderings, and JOINs to speed up the query performance. At a high level, citus partitions the SELECT query into smaller query fragments, assigns these query fragments to workers, oversees their execution, merges their results (and orders them if needed), and returns the final result to the user.
In the following sections, we discuss the different types of queries you can run using citus.
J.5.8.4.4.1. Aggregate Functions #
citus supports and parallelizes most aggregate functions supported by Postgres Pro, including custom user-defined aggregates. Aggregates execute using one of three methods, in this order of preference:
When the aggregate is grouped by a distribution column of a table, citus can push down execution of the entire query to each worker. All aggregates are supported in this situation and execute in parallel on the worker nodes. (Any custom aggregates being used must be installed on the workers.)
When the aggregate is not grouped by a distribution column, citus can still optimize on a case-by-case basis. citus has internal rules for certain aggregates like
sum(),avg(), andcount(distinct)that allow it to rewrite queries for partial aggregation on workers. For instance, to calculate an average, citus obtains a sum and a count from each worker, and then the coordinator node computes the final average.Full list of the special-case aggregates:
avg,min,max,sum,count,array_agg,jsonb_agg,jsonb_object_agg,json_agg,json_object_agg,bit_and,bit_or,bool_and,bool_or,every,hll_add_agg,hll_union_agg,topn_add_agg,topn_union_agg,any_value,tdigest(double precision, int),tdigest_percentile(double precision, int, double precision),tdigest_percentile(double precision, int, double precision[]),tdigest_percentile(tdigest, double precision),tdigest_percentile(tdigest, double precision[]),tdigest_percentile_of(double precision, int, double precision),tdigest_percentile_of(double precision, int, double precision[]),tdigest_percentile_of(tdigest, double precision),tdigest_percentile_of(tdigest, double precision[])Last resort: pull all rows from the workers and perform the aggregation on the coordinator node. When the aggregate is not grouped on a distribution column, and is not one of the predefined special cases, then citus falls back to this approach. It causes network overhead and can exhaust the coordinator resources if the data set to be aggregated is too large. (It is possible to disable this fallback, see below.)
Beware that small changes in a query can change execution modes causing potentially surprising inefficiency. For example, sum(x) grouped by a non-distribution column could use distributed execution, while sum(distinct x) has to pull up the entire set of input records to the coordinator.
All it takes is one column to hurt the execution of a whole query. In the example below, if sum(distinct value2) has to be grouped on the coordinator, then so will sum(value1) even if the latter was fine on its own.
SELECT sum(value1), sum(distinct value2) FROM distributed_table;
To avoid accidentally pulling data to the coordinator, you can set the citus.coordinator_aggregation_strategy parameter:
SET citus.coordinator_aggregation_strategy TO 'disabled';
Note that disabling the coordinator aggregation strategy will prevent “type three” aggregate queries from working at all.
The count(distinct) Aggregates #
citus supports count(distinct) aggregates in several ways. If the count(distinct) aggregate is on the distribution column, citus can directly push down the query to the workers. If not, citus runs SELECT distinct statements on each worker and returns the list to the coordinator where it obtains the final count.
Note that transferring this data becomes slower when workers have a greater number of distinct items. This is especially true for queries containing multiple count(distinct) aggregates, e.g.:
-- Multiple distinct counts in one query tend to be slow SELECT count(distinct a), count(distinct b), count(distinct c) FROM table_abc;
For these kind of queries, the resulting SELECT distinct statements on the workers essentially produce a cross-product of rows to be transferred to the coordinator.
For increased performance you can choose to make an approximate count instead. Follow the steps below:
Download and install the hll extension on all Postgres Pro instances (the coordinator and all the workers).
You can visit the hll GitHub repository for specifics on obtaining the extension.
Create the hll extension on all the Postgres Pro instances by simply running the below command from the coordinator:
CREATE EXTENSION hll;
Enable
count(distinct)approximations by setting the citus.count_distinct_error_rate configuration parameter. Lower values for this configuration setting are expected to give more accurate results but take more time for computation. We recommend setting this to0.005.SET citus.count_distinct_error_rate TO 0.005;
After this step,
count(distinct)aggregates automatically switch to using hll with no changes necessary to your queries. You should be able to run approximatecount(distinct)queries on any column of the table.
HyperLogLog Column. Certain users already store their data as hll columns. In such cases, they can dynamically roll up those data by calling the hll_union_agg(hll_column) function.
Estimating Top N Items #
Calculating the first n elements in a set by applying count, sort, and limit is simple. However, as data sizes increase, this method becomes slow and resource intensive. It is more efficient to use an approximation.
The open source topn extension for Postgres Pro enables fast approximate results to “top-n” queries. The extension materializes the top values into a json data type. The topn extension can incrementally update these top values or merge them on-demand across different time intervals.
Before seeing a realistic example of topn, let's see how some of its primitive operations work. First topn_add updates a JSON object with counts of how many times a key has been seen:
-- Starting from nothing, record that we saw an "a"
SELECT topn_add('{}', 'a');
-- => {"a": 1}
-- Record the sighting of another "a"
SELECT topn_add(topn_add('{}', 'a'), 'a');
-- => {"a": 2}
The extension also provides aggregations to scan multiple values:
-- For normal_rand
CREATE EXTENSION tablefunc;
-- Count values from a normal distribution
SELECT topn_add_agg(floor(abs(i))::text)
FROM normal_rand(1000, 5, 0.7) i;
-- => {"2": 1, "3": 74, "4": 420, "5": 425, "6": 77, "7": 3}
If the number of distinct values crosses a threshold, the aggregation drops information for those seen least frequently. This keeps space usage under control. The threshold can be controlled by the topn.number_of_counters configuration parameter. Its default value is 1000.
Now onto a more realistic example of how topn works in practice. Let's ingest Amazon product reviews from the year 2000 and use topn to query it quickly. First download the dataset:
curl -L https://examples.citusdata.com/customer_reviews_2000.csv.gz | \ gunzip > reviews.csv
Next, ingest it into a distributed table:
CREATE TABLE customer_reviews
(
customer_id TEXT,
review_date DATE,
review_rating INTEGER,
review_votes INTEGER,
review_helpful_votes INTEGER,
product_id CHAR(10),
product_title TEXT,
product_sales_rank BIGINT,
product_group TEXT,
product_category TEXT,
product_subcategory TEXT,
similar_product_ids CHAR(10)[]
);
SELECT create_distributed_table('customer_reviews', 'product_id');
\COPY customer_reviews FROM 'reviews.csv' WITH CSV
Next we will add the extension, create a destination table to store the JSON data generated by topn, and apply the topn_add_agg function we saw previously.
-- Run below command from coordinator, it will be propagated to the worker nodes as well
CREATE EXTENSION topn;
-- A table to materialize the daily aggregate
CREATE TABLE reviews_by_day
(
review_date date unique,
agg_data jsonb
);
SELECT create_reference_table('reviews_by_day');
-- Materialize how many reviews each product got per day per customer
INSERT INTO reviews_by_day
SELECT review_date, topn_add_agg(product_id)
FROM customer_reviews
GROUP BY review_date;
Now, rather than writing a complex window function on customer_reviews, we can simply apply topn to reviews_by_day. For instance, the following query finds the most frequently reviewed product for each of the first five days:
SELECT review_date, (topn(agg_data, 1)).* FROM reviews_by_day ORDER BY review_date LIMIT 5;
┌─────────────┬────────────┬───────────┐ │ review_date │ item │ frequency │ ├─────────────┼────────────┼───────────┤ │ 2000-01-01 │ 0939173344 │ 12 │ │ 2000-01-02 │ B000050XY8 │ 11 │ │ 2000-01-03 │ 0375404368 │ 12 │ │ 2000-01-04 │ 0375408738 │ 14 │ │ 2000-01-05 │ B00000J7J4 │ 17 │ └─────────────┴────────────┴───────────┘
The JSON fields created by topn can be merged with topn_union and topn_union_agg. We can use the latter to merge the data for the entire first month and list the five most reviewed products during that period.
SELECT (topn(topn_union_agg(agg_data), 5)).* FROM reviews_by_day WHERE review_date >= '2000-01-01' AND review_date < '2000-02-01' ORDER BY 2 DESC;
┌────────────┬───────────┐ │ item │ frequency │ ├────────────┼───────────┤ │ 0375404368 │ 217 │ │ 0345417623 │ 217 │ │ 0375404376 │ 217 │ │ 0375408738 │ 217 │ │ 043936213X │ 204 │ └────────────┴───────────┘
For more details and examples, see the topn readme file.
Percentile Calculations #
Finding an exact percentile over a large number of rows can be prohibitively expensive, because all rows must be transferred to the coordinator for final sorting and processing. Finding an approximation, on the other hand, can be done in parallel on worker nodes using a so-called sketch algorithm. The coordinator node then combines compressed summaries into the final result rather than reading through the full rows.
A popular sketch algorithm for percentiles uses a compressed data structure called t-digest, and is available for Postgres Pro in the tdigest extension. citus has integrated support for this extension.
Here is how to use tdigest in citus:
Download and install the tdigest extension on all Postgres Pro nodes (the coordinator and all the workers). The tdigest extension GitHub repository has installation instructions.
Create the tdigest extension within the database. Run the following command on the coordinator:
CREATE EXTENSION tdigest;
The coordinator will propagate the command to the workers as well.
When any of the aggregates defined in the extension are used in queries, citus will rewrite the queries to push down partial tdigest computation to the workers where applicable.
tdigest accuracy can be controlled with the compression argument passed into aggregates. The trade-off is accuracy vs the amount of data shared between workers and the coordinator. For a full explanation of how to use the aggregates in tdigest, have a look at the documentation of the extension.
J.5.8.4.4.2. Limit Pushdown #
citus also pushes down the limit clauses to the shards on the workers wherever possible to minimize the amount of data transferred across network.
However, in some cases, SELECT queries with LIMIT clauses may need to fetch all rows from each shard to generate exact results. For example, if the query requires ordering by the aggregate column, it would need results of that column from all shards to determine the final aggregate value. This reduces performance of the LIMIT clause due to high volume of network data transfer. In such cases, and where an approximation would produce meaningful results, citus provides an option for network efficient approximate LIMIT clauses.
LIMIT approximations are disabled by default and can be enabled by setting the citus.limit_clause_row_fetch_count configuration parameter. On the basis of this configuration value, citus will limit the number of rows returned by each task for aggregation on the coordinator. Due to this limit, the final results may be approximate. Increasing this limit will increase the accuracy of the final results, while still providing an upper bound on the number of rows pulled from the workers.
SET citus.limit_clause_row_fetch_count TO 10000;
J.5.8.4.4.3. Views on Distributed Tables #
citus supports all views on distributed tables. To learn more about syntax and features of views, see the section about the CREATE VIEW command.
Note that some views cause a less efficient query plan than others. For more information about detecting and improving poor view performance, see the Subquery/CTE Network Overhead section. (Views are treated inside the extension as subqueries.)
citus supports materialized views as well and stores them as local tables on the coordinator node.
J.5.8.4.4.4. Joins #
citus supports equi-joins between any number of tables irrespective of their size and distribution method. The query planner chooses the optimal join method and join order based on how tables are distributed. It evaluates several possible join orders and creates a join plan which requires minimum data to be transferred across network.
Co-Located Joins #
When two tables are co-located then they can be joined efficiently on their common distribution columns. A co-located join is the most efficient way to join two large distributed tables.
Internally, the citus coordinator knows which shards of the co-located tables might match with shards of the other table by looking at the distribution column metadata. This allows citus to prune away shard pairs, which cannot produce matching join keys. The joins between remaining shard pairs are executed in parallel on the workers and then the results are returned to the coordinator.
Note
Be sure that the tables are distributed into the same number of shards and that the distribution columns of each table have exactly matching types. Attempting to join on columns of slightly different types such as int and bigint can cause problems.
Reference Table Joins #
Reference tables can be used as “dimension” tables to join efficiently with large “fact” tables. Because reference tables are replicated in full across all worker nodes, a reference join can be decomposed into local joins on each worker and performed in parallel. A reference join is like a more flexible version of a co-located join because reference tables are not distributed on any particular column and are free to join on any of their columns.
Reference tables can also join with tables local to the coordinator node.
Repartition Joins #
In some cases, you may need to join two tables on columns other than the distribution column. For such cases, citus also allows joining on non-distribution key columns by dynamically repartitioning the tables for the query.
In such cases the table(s) to be partitioned are determined by the query optimizer on the basis of the distribution columns, join keys and sizes of the tables. With repartitioned tables, it can be ensured that only relevant shard pairs are joined with each other reducing the amount of data transferred across network drastically.
In general, co-located joins are more efficient than repartition joins as repartition joins require shuffling of data. So, you should try to distribute your tables by the common join keys whenever possible.
J.5.8.4.5. Query Processing #
A citus cluster consists of a coordinator instance and multiple worker instances. The data is sharded on the workers while the coordinator stores metadata about these shards. All queries issued to the cluster are executed via the coordinator. The coordinator partitions the query into smaller query fragments where each query fragment can be run independently on a shard. The coordinator then assigns the query fragments to workers, oversees their execution, merges their results, and returns the final result to the user. The query processing architecture can be described in brief by the diagram below.
Figure J.16. Query Processing Architecture
citus query processing pipeline involves the two components:
Distributed query planner and executor
Postgres Pro planner and executor
We discuss them in greater detail in the subsequent sections.
J.5.8.4.5.1. Distributed Query Planner #
citus distributed query planner takes in a SQL query and plans it for distributed execution.
For SELECT queries, the planner first creates a plan tree of the input query and transforms it into its commutative and associative form so it can be parallelized. It also applies several optimizations to ensure that the queries are executed in a scalable manner, and that network I/O is minimized.
Next, the planner breaks the query into two parts: the coordinator query, which runs on the coordinator, and the worker query fragments, which run on individual shards on the workers. The planner then assigns these query fragments to the workers such that all their resources are used efficiently. After this step, the distributed query plan is passed on to the distributed executor for execution.
The planning process for key-value lookups on the distribution column or modification queries is slightly different as they hit exactly one shard. Once the planner receives an incoming query, it needs to decide the correct shard to which the query should be routed. To do this, it extracts the distribution column in the incoming row and looks up the metadata to determine the right shard for the query. Then, the planner rewrites the SQL of that command to reference the shard table instead of the original table. This re-written plan is then passed to the distributed executor.
J.5.8.4.5.2. Distributed Query Executor #
citus distributed executor runs distributed query plans and handles failures. The executor is well suited for getting fast responses to queries involving filters, aggregations, and co-located joins, as well as running single-tenant queries with full SQL coverage. It opens one connection per shard to the workers as needed and sends all fragment queries to them. It then fetches the results from each fragment query, merges them, and gives the final results back to the user.
Subquery/CTE Push-Pull Execution #
If necessary citus can gather results from subqueries and CTEs into the coordinator node and then push them back across workers for use by an outer query. This allows citus to support a greater variety of SQL constructs.
For example, having subqueries in the WHERE clause sometimes cannot execute inline at the same time as the main query, but must be done separately. Suppose a web analytics application maintains a page_views table partitioned by page_id. To query the number of visitor hosts on the top twenty most visited pages, we can use a subquery to find the list of pages, then an outer query to count the hosts.
SELECT page_id, count(distinct host_ip) FROM page_views WHERE page_id IN ( SELECT page_id FROM page_views GROUP BY page_id ORDER BY count(*) DESC LIMIT 20 ) GROUP BY page_id;
The executor would like to run a fragment of this query against each shard by page_id, counting distinct host_ips, and combining the results on the coordinator. However, the LIMIT in the subquery means the subquery cannot be executed as part of the fragment. By recursively planning the query citus can run the subquery separately, push the results to all workers, run the main fragment query, and pull the results back to the coordinator. The “push-pull” design supports subqueries like the one above.
Let's see this in action by reviewing the EXPLAIN output for this query. It is fairly involved:
GroupAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.page_id
-> Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: remote_scan.page_id
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0)
-> Distributed Subplan 6_1
-> Limit (cost=0.00..0.00 rows=0 width=0)
-> Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: COALESCE((pg_catalog.sum((COALESCE((pg_catalog.sum(remote_scan.worker_column_2))::bigint, '0'::bigint))))::bigint, '0'::bigint) DESC
-> HashAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.page_id
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0)
Task Count: 32
Tasks Shown: One of 32
-> Task
Node: host=localhost port=9701 dbname=postgres
-> HashAggregate (cost=54.70..56.70 rows=200 width=12)
Group Key: page_id
-> Seq Scan on page_views_102008 page_views (cost=0.00..43.47 rows=2247 width=4)
Task Count: 32
Tasks Shown: One of 32
-> Task
Node: host=localhost port=9701 dbname=postgres
-> HashAggregate (cost=84.50..86.75 rows=225 width=36)
Group Key: page_views.page_id, page_views.host_ip
-> Hash Join (cost=17.00..78.88 rows=1124 width=36)
Hash Cond: (page_views.page_id = intermediate_result.page_id)
-> Seq Scan on page_views_102008 page_views (cost=0.00..43.47 rows=2247 width=36)
-> Hash (cost=14.50..14.50 rows=200 width=4)
-> HashAggregate (cost=12.50..14.50 rows=200 width=4)
Group Key: intermediate_result.page_id
-> Function Scan on read_intermediate_result intermediate_result (cost=0.00..10.00 rows=1000 width=4)
Let's break it apart and examine each piece.
GroupAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.page_id
-> Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: remote_scan.page_id
The root of the tree is what the coordinator node does with the results from the workers. In this case, it is grouping them, and GroupAggregate requires they be sorted first.
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0) -> Distributed Subplan 6_1 .
The custom scan has two large sub-trees, starting with a “distributed subplan”.
-> Limit (cost=0.00..0.00 rows=0 width=0)
-> Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: COALESCE((pg_catalog.sum((COALESCE((pg_catalog.sum(remote_scan.worker_column_2))::bigint, '0'::bigint))))::bigint, '0'::bigint) DESC
-> HashAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.page_id
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0)
Task Count: 32
Tasks Shown: One of 32
-> Task
Node: host=localhost port=9701 dbname=postgres
-> HashAggregate (cost=54.70..56.70 rows=200 width=12)
Group Key: page_id
-> Seq Scan on page_views_102008 page_views (cost=0.00..43.47 rows=2247 width=4)
.
Worker nodes run the above for each of the thirty-two shards (citus is choosing one representative for display). We can recognize all the pieces of the IN (…) subquery: the sorting, grouping and limiting. When all workers have completed this query, they send their output back to the coordinator which puts it together as “intermediate results”.
Task Count: 32
Tasks Shown: One of 32
-> Task
Node: host=localhost port=9701 dbname=postgres
-> HashAggregate (cost=84.50..86.75 rows=225 width=36)
Group Key: page_views.page_id, page_views.host_ip
-> Hash Join (cost=17.00..78.88 rows=1124 width=36)
Hash Cond: (page_views.page_id = intermediate_result.page_id)
.
The citus extension starts another executor job in this second subtree. It is going to count distinct hosts in page_views. It uses a JOIN to connect with the intermediate results. The intermediate results will help it restrict to the top twenty pages.
-> Seq Scan on page_views_102008 page_views (cost=0.00..43.47 rows=2247 width=36)
-> Hash (cost=14.50..14.50 rows=200 width=4)
-> HashAggregate (cost=12.50..14.50 rows=200 width=4)
Group Key: intermediate_result.page_id
-> Function Scan on read_intermediate_result intermediate_result (cost=0.00..10.00 rows=1000 width=4)
.
The worker internally retrieves intermediate results using the read_intermediate_result function, which loads data from a file that was copied in from the coordinator node.
This example showed how citus executed the query in multiple steps with a distributed subplan and how you can use EXPLAIN to learn about distributed query execution.
J.5.8.4.5.3. Postgres Pro Planner and Executor #
Once the distributed executor sends the query fragments to the workers, they are processed like regular Postgres Pro queries. The Postgres Pro planner on that worker chooses the most optimal plan for executing that query locally on the corresponding shard table. The Postgres Pro executor then runs that query and returns the query results back to the distributed executor. Learn more about the Postgres Pro planner and executor. Finally, the distributed executor passes the results to the coordinator for final aggregation.
J.5.8.4.6. Manual Query Propagation #
When the user issues a query, the citus coordinator partitions it into smaller query fragments where each query fragment can be run independently on a worker shard. This allows citus to distribute each query across the cluster.
However, the way queries are partitioned into fragments (and which queries are propagated at all) varies by the type of query. In some advanced situations it is useful to manually control this behavior. citus provides utility functions to propagate SQL to workers, shards, or co-located placements.
Manual query propagation bypasses coordinator logic, locking, and any other consistency checks. These functions are available as a last resort to allow statements which citus otherwise does not run natively. Use them carefully to avoid data inconsistency and deadlocks.
J.5.8.4.6.1. Running on All Workers #
The least granular level of execution is broadcasting a statement for execution on all workers. This is useful for viewing properties of entire worker databases.
-- List the work_mem setting of each worker database SELECT run_command_on_workers($cmd$ SHOW work_mem; $cmd$);
To run on all nodes, both workers and the coordinator, use the run_command_on_all_nodes function.
Note
This command should not be used to create database objects on the workers, as doing so will make it harder to add worker nodes in an automated fashion.
Note
The run_command_on_workers function and other manual propagation commands in this section can run only queries that return a single column and single row.
J.5.8.4.6.2. Running on All Shards #
The next level of granularity is running a command across all shards of a particular distributed table. It can be useful, for instance, in reading the properties of a table directly on workers. Queries run locally on a worker node have full access to metadata such as table statistics.
The run_command_on_shards function applies an SQL command to each shard, where the shard name is provided for interpolation in the command. Here is an example of estimating the row count for a distributed table by using the pg_class table on each worker to estimate the number of rows for each shard. Notice the %s, which will be replaced with each shard name.
-- Get the estimated row count for a distributed table by summing the
-- estimated counts of rows for each shard
SELECT sum(result::bigint) AS estimated_count
FROM run_command_on_shards(
'my_distributed_table',
$cmd$
SELECT reltuples
FROM pg_class c
JOIN pg_catalog.pg_namespace n on n.oid=c.relnamespace
WHERE (n.nspname || '.' || relname)::regclass = '%s'::regclass
AND n.nspname NOT IN ('citus', 'pg_toast', 'pg_catalog')
$cmd$
);
A useful companion to run_command_on_shards is the run_command_on_colocated_placements function. It interpolates the names of two placements of co-located distributed tables into a query. The placement pairs are always chosen to be local to the same worker where full SQL coverage is available. Thus we can use advanced SQL features like triggers to relate the tables:
-- Suppose we have two distributed tables
CREATE TABLE little_vals (key int, val int);
CREATE TABLE big_vals (key int, val int);
SELECT create_distributed_table('little_vals', 'key');
SELECT create_distributed_table('big_vals', 'key');
-- We want to synchronize them so that every time little_vals
-- are created, big_vals appear with double the value
--
-- First we make a trigger function, which will
-- take the destination table placement as an argument
CREATE OR REPLACE FUNCTION embiggen() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'INSERT') THEN
EXECUTE format(
'INSERT INTO %s (key, val) SELECT ($1).key, ($1).val*2;',
TG_ARGV[0]
) USING NEW;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Next we relate the co-located tables by the trigger function
-- on each co-located placement
SELECT run_command_on_colocated_placements(
'little_vals',
'big_vals',
$cmd$
CREATE TRIGGER after_insert AFTER INSERT ON %s
FOR EACH ROW EXECUTE PROCEDURE embiggen(%L)
$cmd$
);
J.5.8.4.6.3. Limitations #
There are no safeguards against deadlock for multi-statement transactions.
There are no safeguards against mid-query failures and resulting inconsistencies.
Query results are cached in memory; these functions cannot deal with very big result sets.
The functions error out early if they cannot connect to a node.
J.5.8.4.7. SQL Support and Workarounds #
As citus provides distributed functionality by extending Postgres Pro, it is compatible with Postgres Pro constructs. This means that users can use the tools and features that come with the rich and extensible Postgres Pro ecosystem for distributed tables created with citus.
citus has 100% SQL coverage for any queries it is able to execute on a single worker node. These kind of queries are common in multi-tenant applications when accessing information about a single tenant.
Even cross-node queries (used for parallel computations) support most SQL features. However, some SQL features are not supported for queries, which combine information from multiple nodes.
J.5.8.4.7.1. Limitations #
General #
These limitations apply to all models of operation:
The rule system is not supported.
Subqueries within
INSERTqueries are not supported.Distributing multi-level partitioned tables is not supported.
Functions used in
UPDATEqueries on distributed tables must not beVOLATILE.STABLEfunctions used inUPDATEqueries cannot be called with column references.Modifying views when the query contains citus tables is not supported.
citus encodes the node identifier in the sequence generated on every node, this allows every individual node to take inserts directly without having the sequence overlap. This method however does not work for sequences that are smaller than bigint, which may result in inserts on worker nodes failing, in that case you need to drop the column and add a bigint based one, or route the inserts via the coordinator.
Cross-Node SQL Queries #
SELECT … FOR UPDATEwork in single-shard queries only.TABLESAMPLE work in single-shard queries only.
Correlated subqueries are supported only when the correlation is on the distribution column.
Outer joins between distributed tables are only supported on the distribution column.
Recursive CTEs work in single-shard queries only.
Grouping sets work in single-shard queries only.
Only regular, foreign or partitioned tables can be distributed.
The SQL
MERGEcommand is supported in the following combinations of table types:Target Source Support Comments Local
Local
Yes
Local
Reference
Yes
Local
Distributed
No
Feature in development
Distributed
Local
Yes
Distributed
Distributed
Yes
Including non co-located tables
Distributed
Reference
Yes
Reference
N/A
No
Reference table as target is not allowed
For a detailed reference of the Postgres Pro SQL command dialect (which can be used as is by citus users), you can see the SQL Commands section.
Schema-Based Sharding SQL Compatibility #
When using schema-based sharding the following features are not available:
Foreign keys across distributed schemas are not supported.
Joins across distributed schemas are subject to cross-node SQL queries limitations.
Creating a distributed schema and tables in a single SQL statement is not supported.
J.5.8.4.7.2. Workarounds #
Before attempting workarounds consider whether citus is appropriate for your situation. The citus extension works well for real-time analytics and multi-tenant use cases.
citus supports all SQL statements in the multi-tenant use case. Even in the real-time analytics use cases, with queries that span across nodes, citus supports the majority of statements. The few types of unsupported queries are listed in the Are there any Postgres Pro features not supported by citus? section. Many of the unsupported features have workarounds; below are a number of the most useful.
Work Around Limitations Using CTEs #
When a SQL query is unsupported, one way to work around it is using CTEs, which use what we call pull-push execution.
SELECT * FROM dist WHERE EXISTS (SELECT 1 FROM local WHERE local.a = dist.a); /* ERROR: direct joins between distributed and local tables are not supported HINT: Use CTEs or subqueries to select from local tables and use them in joins */
To work around this limitation, you can turn the query into a router query by wrapping the distributed part in a CTE.
WITH cte AS (SELECT * FROM dist) SELECT * FROM cte WHERE EXISTS (SELECT 1 FROM local WHERE local.a = cte.a);
Remember that the coordinator will send the results of the CTE to all workers which require it for processing. Thus it is best to either add the most specific filters and limits to the inner query as possible, or else aggregate the table. That reduces the network overhead which such a query can cause. More about this in the Subquery/CTE Network Overhead section.
Temp Tables: the Workaround of Last Resort #
There are still a few queries that are unsupported even with the use of push-pull execution via subqueries. One of them is using grouping sets on a distributed table.
In our real-time analytics tutorial we created a table called github_events, distributed by the column user_id. Let's query it and find the earliest events for a preselected set of repos, grouped by combinations of event type and event publicity. A convenient way to do this is with grouping sets. However, as mentioned, this feature is not yet supported in distributed queries:
-- This will not work
SELECT repo_id, event_type, event_public,
grouping(event_type, event_public),
min(created_at)
FROM github_events
WHERE repo_id IN (8514, 15435, 19438, 21692)
GROUP BY repo_id, ROLLUP(event_type, event_public);
ERROR: could not run distributed query with GROUPING HINT: Consider using an equality filter on the distributed table's partition column.
There is a trick, though. We can pull the relevant information to the coordinator as a temporary table:
-- Grab the data, minus the aggregate, into a local table
CREATE TEMP TABLE results AS (
SELECT repo_id, event_type, event_public, created_at
FROM github_events
WHERE repo_id IN (8514, 15435, 19438, 21692)
);
-- Now run the aggregate locally
SELECT repo_id, event_type, event_public,
grouping(event_type, event_public),
min(created_at)
FROM results
GROUP BY repo_id, ROLLUP(event_type, event_public);
repo_id | event_type | event_public | grouping | min
---------+-------------------+--------------+----------+---------------------
8514 | PullRequestEvent | t | 0 | 2016-12-01 05:32:54
8514 | IssueCommentEvent | t | 0 | 2016-12-01 05:32:57
19438 | IssueCommentEvent | t | 0 | 2016-12-01 05:48:56
21692 | WatchEvent | t | 0 | 2016-12-01 06:01:23
15435 | WatchEvent | t | 0 | 2016-12-01 05:40:24
21692 | WatchEvent | | 1 | 2016-12-01 06:01:23
15435 | WatchEvent | | 1 | 2016-12-01 05:40:24
8514 | PullRequestEvent | | 1 | 2016-12-01 05:32:54
8514 | IssueCommentEvent | | 1 | 2016-12-01 05:32:57
19438 | IssueCommentEvent | | 1 | 2016-12-01 05:48:56
15435 | | | 3 | 2016-12-01 05:40:24
21692 | | | 3 | 2016-12-01 06:01:23
19438 | | | 3 | 2016-12-01 05:48:56
8514 | | | 3 | 2016-12-01 05:32:54
Creating a temporary table on the coordinator is a last resort. It is limited by the disk size and CPU of the node.
Subqueries Within INSERT Queries #
Try rewriting your queries with INSERT INTO ... SELECT syntax.
The following SQL:
INSERT INTO a.widgets (map_id, widget_name)
VALUES (
(SELECT mt.map_id FROM a.map_tags mt WHERE mt.map_license = '12345'),
'Test'
);
Would become:
INSERT INTO a.widgets (map_id, widget_name) SELECT mt.map_id, 'Test' FROM a.map_tags mt WHERE mt.map_license = '12345';
J.5.8.5. citus API #
J.5.8.5.1. citus Utility Functions #
This section contains reference information for the user defined functions provided by citus. These functions help in providing additional distributed functionality to citus other than the standard SQL commands.
J.5.8.5.1.1. Table and Shard DDL #
citus_schema_distribute (schemaname regnamespace) returns void#Converts existing regular schemas into distributed schemas, which are automatically associated with individual co-location groups such that the tables created in those schemas will be automatically converted to co-located distributed tables without a shard key. The process of distributing the schema will automatically assign and move it to an existing node in the cluster.
Arguments:
schemaname— the name of the schema, which needs to be distributed.
The example below shows how to distribute three schemas named
tenant_a,tenant_b, andtenant_c. For more examples, see the Microservices section:SELECT citus_schema_distribute('tenant_a'); SELECT citus_schema_distribute('tenant_b'); SELECT citus_schema_distribute('tenant_c');citus_schema_undistribute (schemaname regnamespace) returns void#Converts an existing distributed schema back into a regular schema. The process results in the tables and data being moved from the current node back to the coordinator node in the cluster.
Arguments:
schemaname— the name of the schema, which needs to be distributed.
The example below shows how to convert three different distributed schemas back into regular schemas. For more examples, see the Microservices section:
SELECT citus_schema_undistribute('tenant_a'); SELECT citus_schema_undistribute('tenant_b'); SELECT citus_schema_undistribute('tenant_c');citus_schema_move (schema_id regnamespace, target_node_name text, target_node_port integer, shard_transfer_mode citus.shard_transfer_mode) returns void#Moves a distributed schema from one node to another.
There are two ways to move a distributed schema: blocking or non-blocking. The blocking approach means that during the move all modifications to the tables in the schema are paused. The second way, which avoids blocking writes, relies on Postgres Pro 10 logical replication.
Arguments:
schema_id— object ID of the distributed schema to be moved. If you provide the name of the schema as a string literal, this string is automatically casted to the oid.target_node_name— the DNS name of the node on which the distributed schema will be moved (“target” node).target_node_port— the port on the target worker node on which the database server is listening.shard_transfer_mode— specify the method of replication, whether to use Postgres Pro logical replication or a cross-workerCOPYcommand. The allowed values of this optional argument are:auto— require replica identity if logical replication is possible, otherwise use legacy behaviour. This is the default value.force_logical— use logical replication even if the table does not have a replica identity. Any concurrent update/delete statements to the table will fail during replication.block_writes— useCOPY(blocking writes) for tables lacking primary key or replica identity.
The example below shows how to use the function:
SELECT citus_schema_move('schema-name', 'to_host', 5432);create_distributed_table (table_name regclass, distribution_column text, distribution_type citus.distribution_type, colocate_with text, shard_count int) returns void#Defines a distributed table and create its shards if it is a hash-distributed table. This function takes in a table name, the distribution column, and an optional distribution method and inserts appropriate metadata to mark the table as distributed. The function defaults to hash distribution if no distribution method is specified. If the table is hash-distributed, the function also creates worker shards based on the shard count configuration value. If the table contains any rows, they are automatically distributed to worker nodes.
Arguments:
table_name— the name of the table, which needs to be distributed.distribution_column— the column on which the table is to be distributed.distribution_type— an optional distribution method. The default value ishash.colocate_with— include current table in the co-location group of another table. This is an optional argument. By default tables are co-located when they are distributed by columns of the same type with the same shard count. If you want to break this co-location later, you can use the update_distributed_table_colocation function. Possible values for this argument aredefault, which is the default value,noneto start a new co-location group, or the name of another table to co-locate with the table. To learn more, see the Co-Locating Tables section.Keep in mind that the default value of the
colocate_withargument does implicit co-location. As explained in the Table Co-Location section, this can be a great thing when tables are related or will be joined. However, when two tables are unrelated but happen to use the same datatype for their distribution columns, accidentally co-locating them can decrease performance during shard rebalancing. The table shards will be moved together unnecessarily in a “cascade”. If you want to break this implicit co-location, you can use the update_distributed_table_colocation function.If a new distributed table is not related to other tables, it is best to specify
colocate_with => 'none'.shard_count— the number of shards to create for the new distributed table. This is an optional argument. When specifyingshard_countyou cannot specify a value ofcolocate_withother thannone. To change the shard count of an existing table or co-location group, use the alter_distributed_table function.Allowed values for the
shard_countargument are between1and64000. For guidance on choosing the optimal value, see the Shard Count section.
This example informs the database that the
github_eventstable should be distributed by hash on therepo_idcolumn. For more examples, see the Creating and Modifying Distributed Objects (DDL) section:SELECT create_distributed_table('github_events', 'repo_id'); -- Alternatively, to be more explicit: SELECT create_distributed_table('github_events', 'repo_id', colocate_with => 'github_repo');truncate_local_data_after_distributing_table (function_name regclass) returns void#Truncates all local rows after distributing a table and prevent constraints from failing due to outdated local records. The truncation cascades to tables having a foreign key to the designated table. If the referring tables are not themselves distributed, then truncation is forbidden until they are to protect referential integrity:
ERROR: cannot truncate a table referenced in a foreign key constraint by a local table
Truncating local coordinator node table data is safe for distributed tables because their rows, if they have any, are copied to worker nodes during distribution.
Arguments:
table_name— the name of the distributed table whose local counterpart on the coordinator node should be truncated.
The example below shows how to use the function:
-- Requires that argument is a distributed table SELECT truncate_local_data_after_distributing_table('public.github_events');undistribute_table (table_name regclass, cascade_via_foreign_keys boolean) returns void#Undoes the action of the create_distributed_table or create_reference_table functions. Undistributing moves all data from shards back into a local table on the coordinator node (assuming the data can fit), then deletes the shards.
citus will not undistribute tables that have, or are referenced by, foreign keys, unless the
cascade_via_foreign_keysargument is set totrue. If this argument isfalse(or omitted), then you must manually drop the offending foreign key constraints before undistributing.Arguments:
table_name— the name of the distributed or reference table to undistribute.cascade_via_foreign_keys— when this optional argument is set totrue, the function also undistributes all tables that are related totable_namethrough foreign keys. Use caution with this argument because it can potentially affect many tables. The default value isfalse.
The example below shows how to distribute the
github_eventstable and then undistribute it:-- First distribute the table SELECT create_distributed_table('github_events', 'repo_id'); -- Undo that and make it local again SELECT undistribute_table('github_events');alter_distributed_table (table_name regclass, distribution_column text, shard_count int, colocate_with text, cascade_to_colocated boolean) returns void#Changes the distribution column, shard count or co-location properties of a distributed table.
Arguments:
table_name— the name of the distributed table, which will be altered.distribution_column— the name of the new distribution column. The default value of this optional argument isNULL.shard_count— the new shard count. The default value of this optional argument isNULL.colocate_with— the table that the current distributed table will be co-located with. Possible values aredefault,noneto start a new co-location group, or the name of another table with which to co-locate. The default value of this optional argument isdefault.cascade_to_colocated. When this argument is set totrue,shard_countandcolocate_withchanges will also be applied to all of the tables that were previously co-located with the table, and the co-location will be preserved. If it isfalse, the current co-location of this table will be broken. The default value of this optional argument isfalse.
The example below shows how to use the function:
-- Change distribution column SELECT alter_distributed_table('github_events', distribution_column:='event_id'); -- Change shard count of all tables in colocation group SELECT alter_distributed_table('github_events', shard_count:=6, cascade_to_colocated:=true); -- Change colocation SELECT alter_distributed_table('github_events', colocate_with:='another_table');alter_table_set_access_method (table_name regclass, access_method text) returns void#Changes access method of a table (e.g.
heapor columnar).Arguments:
table_name— the name of the table whose access method will change.access_method— the name of the new access method.
The example below shows how to use the function:
SELECT alter_table_set_access_method('github_events', 'columnar');remove_local_tables_from_metadata () returns void#Removes local tables from metadata of the citus extension that no longer need to be there. (See the citus.enable_local_reference_table_foreign_keys configuration parameter.)
Usually if a local table is in citus metadata, there is a reason, such as the existence of foreign keys between the table and a reference table. However, if
citus.enable_local_reference_table_foreign_keysis disabled, citus will no longer manage metadata in that situation, and unnecessary metadata can persist until manually cleaned.create_reference_table (table_name regclass) returns void#Defines a small reference or dimension table. This function takes in a table name, and creates a distributed table with just one shard, replicated to every worker node.
Arguments:
table_name— the name of the small dimension or reference table, which needs to be distributed.
The example below informs the database that the
nationtable should be defined as a reference table:SELECT create_reference_table('nation');citus_add_local_table_to_metadata (table_name regclass, cascade_via_foreign_keys boolean) returns void#Adds a local Postgres Pro table into citus metadata. A major use case for this function is to make local tables on the coordinator accessible from any node in the cluster. This is mostly useful when running queries from other nodes. The data associated with the local table stays on the coordinator, only its schema and metadata are sent to the workers.
Note that adding local tables to the metadata comes at a slight cost. When you add the table, citus must track it in the pg_dist_partition. Local tables that are added to metadata inherit the same limitations as reference tables (see the Creating and Modifying Distributed Objects (DDL) and SQL Support and Workarounds sections).
If you use the undistribute_table function, citus will automatically remove the resulting local tables from metadata, which eliminates such limitations on those tables.
Arguments:
table_name— the name of the table on the coordinator to be added to citus metadata.cascade_via_foreign_keys— when this optional argument is set totrue, the function adds other tables that are in a foreign key relationship with given table into metadata automatically. Use caution with this argument, because it can potentially affect many tables. The default value isfalse.
The example below informs the database that the
nationtable should be defined as a coordinator-local table, accessible from any node:SELECT citus_add_local_table_to_metadata('nation');update_distributed_table_colocation (table_name regclass, colocate_with text) returns void#Updates co-location of a distributed table. This function can also be used to break co-location of a distributed table. citus will implicitly co-locate two tables if the distribution column is the same type, this can be useful if the tables are related and will do some joins. If table
AandBare co-located and tableAgets rebalanced, tableBwill also be rebalanced. If tableBdoes not have a replica identity, the rebalance will fail. Therefore, this function can be useful breaking the implicit co-location in that case. Note that this function does not move any data around physically.Arguments:
table_name— the name of the table co-location of which will be updated.colocate_with— the table with which the table should be co-located.
If you want to break the co-location of a table, specify
colocate_with => 'none'.The example below shows that co-location of table
Ais updated as co-location of tableB:SELECT update_distributed_table_colocation('A', colocate_with => 'B');Assume that table
Aand tableBare co-located (possibily implicitly). If you want to break the co-location, do the following:SELECT update_distributed_table_colocation('A', colocate_with => 'none');Now, assume that tables
A,B,C, andDare co-located and you want to co-locate tableAwithBand tableCwith tableD:SELECT update_distributed_table_colocation('C', colocate_with => 'none'); SELECT update_distributed_table_colocation('D', colocate_with => 'C');If you have a hash-distributed table named
noneand you want to update its co-location, you can do:SELECT update_distributed_table_colocation('"none"', colocate_with => 'some_other_hash_distributed_table');create_distributed_function (function_name regprocedure, distribution_arg_name text, colocate_with text, force_delegation bool) returns void#Propagates a function from the coordinator node to workers and marks it for distributed execution. When a distributed function is called on the coordinator, citus uses the value of the
distribution_arg_nameargument to pick a worker node to run the function. Calling the function on workers increases parallelism and can bring the code closer to data in shards for lower latency.Note that the Postgres Pro search path is not propagated from the coordinator to workers during distributed function execution, so distributed function code should fully qualify the names of database objects. Also notices emitted by the functions will not be displayed to the user.
Arguments:
function_name— the name of the function to be distributed. The name must include the function parameter types in parentheses because multiple functions can have the same name in Postgres Pro. For instance,'foo(int)'is different from'foo(int, text)'.distribution_arg_name— the argument name by which to distribute. For convenience (or if the function arguments do not have names), a positional placeholder is allowed, such as'$1'. If this argument is not specified, then the function named byfunction_nameis merely created on the workers. If worker nodes are added in the future, the function will automatically be created there too. This is an optional argument.colocate_with— when the distributed function reads or writes to a distributed table (or more generally co-locating tables), be sure to name that table using the this argument. This ensures that each invocation of the function runs on the worker node containing relevant shards. This is an optional argument.force_delegation. The default value isNULL.
The example below shows how to use the function:
-- An example function that updates a hypothetical -- event_responses table, which itself is distributed by event_id CREATE OR REPLACE FUNCTION register_for_event(p_event_id int, p_user_id int) RETURNS void LANGUAGE plpgsql AS $fn$ BEGIN INSERT INTO event_responses VALUES ($1, $2, 'yes') ON CONFLICT (event_id, user_id) DO UPDATE SET response = EXCLUDED.response; END; $fn$; -- Distribute the function to workers, using the p_event_id argument -- to determine which shard each invocation affects, and explicitly -- colocating with event_responses which the function updates SELECT create_distributed_function( 'register_for_event(int, int)', 'p_event_id', colocate_with := 'event_responses' );
alter_columnar_table_set (table_name regclass, chunk_group_row_limit int, stripe_row_limit int, compression name, compression_level int) returns void#Changes settings on a columnar table. Calling this function on a non-columnar table gives an error. All arguments except the
table_nameare optional.To view current options for all columnar tables, consult this table:
SELECT * FROM columnar.options;
The default values for columnar settings for newly created tables can be overridden with these configuration parameters:
columnar.compressioncolumnar.compression_levelcolumnar.stripe_row_countcolumnar.chunk_row_count
Arguments:
table_name— the name of the columnar table.chunk_row_count— the maximum number of rows per chunk for newly inserted data. Existing chunks of data will not be changed and may have more rows than this maximum value. The default value is10000.stripe_row_count— the maximum number of rows per stripe for newly inserted data. Existing stripes of data will not be changed and may have more rows than this maximum value. The default value is150000.compression— the compression type for the newly inserted data. Existing data will not be recompressed or decompressed. The default and generally suggested value iszstd(if support has been compiled in). Allowed values arenone,pglz,zstd,lz4, andlz4hc.compression_level. Allowed values are from 1 to 19. If the compression method does not support the level chosen, the closest level will be selected instead.
The example below shows how to use the function:
SELECT alter_columnar_table_set( 'my_columnar_table', compression => 'none', stripe_row_count => 10000);
create_time_partitions (table_name regclass, partition_interval interval, end_at timestamptz, start_from timestamptz) returns boolean#Creates partitions of a given interval to cover a given range of time. Returns
trueif new partitions are created andfalseif they already exist.Arguments:
table_name— the table for which to create new partitions. The table must be partitioned on one column of typedate,timestamp, ortimestamptz.partition_interval— the interval of time, such as'2 hours', or'1 month', to use when setting ranges on new partitions.end_at— create partitions up to this time. The last partition will contain the pointend_atand no later partitions will be created.start_from— pick the first partition so that it contains the pointstart_from. The default value isnow().
The example below shows how to use the function:
-- Create a year's worth of monthly partitions -- in table foo, starting from the current time SELECT create_time_partitions( table_name := 'foo', partition_interval := '1 month', end_at := now() + '12 months' );
drop_old_time_partitions (table_name regclass, older_than timestamptz)#Removes all partitions whose intervals fall before a given timestamp. In addition to using this function, you might consider the alter_old_partitions_set_access_method function to compress the old partitions with columnar storage.
Arguments:
table_name— the table for which to remove partitions. The table must be partitioned on one column of typedate,timestamp, ortimestamptz.older_than— drop partitions whose upper limit is less than or equal to theolder_thanvalue.
The example below shows how to use the procedure:
-- Drop partitions that are over a year old CALL drop_old_time_partitions('foo', now() - interval '12 months');alter_old_partitions_set_access_method (parent_table_name regclass, older_than timestamptz, new_access_method name)#In the timeseries data use case tables are often partitioned by time and old partitions are compressed into read-only columnar storage.
Arguments:
parent_table_name— the table for which to change partitions. The table must be partitioned on one column of typedate,timestamp, ortimestamptz.older_than— change partitions whose upper limit is less than or equal to theolder_thanvalue.new_access_method. Allowed values areheapfor row-based storage orcolumnarfor columnar storage.
The example below shows how to use the procedure:
CALL alter_old_partitions_set_access_method( 'foo', now() - interval '6 months', 'columnar' );
J.5.8.5.1.2. Metadata / Configuration Information #
citus_add_node (nodename text, nodeport integer, groupid integer, noderole noderole, nodecluster name) returns integer#Note
This function requires database superuser access to run.
Registers a new node addition in the cluster in the citus metadata table pg_dist_node. It also copies reference tables to the new node. The function returns the
nodeidcolumn from the newly inserted row inpg_dist_node.If you call the function on a single-node cluster, be sure to call the citus_set_coordinator_host function first.
Arguments:
nodename— the DNS name or IP address of the new node to be added.nodeport— the port on which Postgres Pro is listening on the worker node.groupid— the group of one primary server and its secondary servers, relevant only for streaming replication. Be sure to set this argument to a value greater than zero, since zero is reserved for the coordinator node. The default value is-1.noderole— the role of the node. Allowed values areprimaryandsecondary. The default value isprimary.nodecluster— the name of the cluster. The default value isdefault.
The example below shows how to use the function:
SELECT * FROM citus_add_node('new-node', 12345); citus_add_node ----------------- 7 (1 row)citus_update_node (node_id int, new_node_name text, new_node_port int, force bool, lock_cooldown int) returns void#Note
This function requires database superuser access to run.
Changes the hostname and port for a node registered in the citus metadata table pg_dist_node.
Arguments:
node_id— the node ID from thepg_dist_nodetable.new_node_name— the updated DNS name or IP address for the node.new_node_port— the updated port on which Postgres Pro is listening on the worker node.force. The default value isfalse.lock_cooldown. The default value is10000.
The example below shows how to use the function:
SELECT * FROM citus_update_node(123, 'new-address', 5432);
citus_set_node_property (nodename text, nodeport integer, property text, value boolean) returns void#Changes properties in the citus metadata table pg_dist_node. Currently it can change only the
shouldhaveshardsproperty.Arguments:
nodename— the DNS name or IP address for the node.nodeport— the port on which Postgres Pro is listening on the worker node.property— the column to change inpg_dist_node, currently only theshouldhaveshardproperty is supported.value— the new value for the column.
The example below shows how to use the function:
SELECT * FROM citus_set_node_property('localhost', 5433, 'shouldhaveshards', false);citus_add_inactive_node (nodename text, nodeport integer, groupid integer, noderole noderole, nodecluster name) returns integer#Note
This function requires database superuser access to run.
Similarly to the citus_add_node function, registers a new node in pg_dist_node. However, it marks the new node as inactive, meaning no shards will be placed there. Also it does not copy reference tables to the new node. The function returns the
nodeidcolumn from the newly inserted row inpg_dist_node.Arguments:
nodename— the DNS name or IP address of the new node to be added.nodeport— the port on which Postgres Pro is listening on the worker node.groupid— the group of one primary server and zero or more secondary servers, relevant only for streaming replication. The default is-1.noderole— the role of the node. Allowed values areprimaryandsecondary. The default value isprimary.nodecluster— the name of the cluster. The default value isdefault.
The example below shows how to use the function:
SELECT * FROM citus_add_inactive_node('new-node', 12345); citus_add_inactive_node -------------------------- 7 (1 row)citus_activate_node (nodename text, nodeport integer) returns integer#Note
This function requires database superuser access to run.
Marks a node as active in the citus metadata table pg_dist_node and copies reference tables to the node. Useful for nodes added via citus_add_inactive_node. The function returns the
nodeidcolumn from the newly inserted row inpg_dist_node.Arguments:
nodename— the DNS name or IP address of the new node to be added.nodeport— the port on which Postgres Pro is listening on the worker node.
The example below shows how to use the function:
SELECT * FROM citus_activate_node('new-node', 12345); citus_activate_node ---------------------- 7 (1 row)citus_disable_node (nodename text, nodeport integer, synchronous bool) returns void#Note
This function requires database superuser access to run.
This function is the opposite from citus_activate_node. It marks a node as inactive in the citus metadata table pg_dist_node, removing it from the cluster temporarily. The function also deletes all reference table placements from the disabled node. To reactivate the node, just call citus_activate_node again.
Arguments:
nodename— the DNS name or IP address of the node to be disabled.nodeport— the port on which Postgres Pro is listening on the worker node.synchronous. The default value isfalse.
The example below shows how to use the function:
SELECT * FROM citus_disable_node('new-node', 12345);citus_add_secondary_node (nodename text, nodeport integer, primaryname text, primaryport integer, nodecluster name) returns integer#Note
This function requires database superuser access to run.
Registers a new secondary node in the cluster for an existing primary node. The function updates the citus pg_dist_node metadata table. The function returns the
nodeidcolumn for the secondary node from the inserted row inpg_dist_node.Arguments:
nodename— the DNS name or IP address of the new node to be added.nodeport— the port on which Postgres Pro is listening on the worker node.primaryname— the DNS name or IP address of the primary node for this secondary.primaryport— the port on which Postgres Pro is listening on the primary node.nodecluster— the name of the cluster. The default value isdefault.
The example below shows how to use the function:
SELECT * FROM citus_add_secondary_node('new-node', 12345, 'primary-node', 12345); citus_add_secondary_node --------------------------- 7 (1 row)citus_remove_node (nodename text, nodeport integer) returns void#Note
This function requires database superuser access to run.
Removes the specified node from the pg_dist_node metadata table. This function will error out if there are existing shard placements on this node. Thus, before using this function, the shards will need to be moved off that node.
Arguments:
nodename— the DNS name of the node to be removed.nodeport— the port on which Postgres Pro is listening on the worker node.
The example below shows how to use the function:
SELECT citus_remove_node('new-node', 12345); citus_remove_node -------------------- (1 row)citus_get_active_worker_nodes () returns setof record#Returns active worker host names and port numbers as a list of tuples where each tuple contains the following information:
node_name— the DNS name of the worker node.node_port— the port on the worker node on which the database server is listening.
The example below shows the output of the function:
SELECT * FROM citus_get_active_worker_nodes(); node_name | node_port -----------+----------- localhost | 9700 localhost | 9702 localhost | 9701 (3 rows)
citus_backend_gpid () returns bigint#Returns the global process identifier (GPID) for the Postgres Pro backend serving the current session. The GPID value encodes both a node in the citus cluster and the operating system process ID of Postgres Pro on that node. The GPID is returned in the following form: (node ID * 10,000,000,000) + process ID.
citus extends the Postgres Pro server signaling functions
pg_cancel_backendandpg_terminate_backendso that they accept GPIDs. In citus, calling these functions on one node can affect a backend running on another node.The example below shows the output of the function:
SELECT citus_backend_gpid();
citus_backend_gpid -------------------- 10000002055citus_check_cluster_node_health () returns setof record#Checks connectivity between all nodes. If there are N nodes, this function checks all N2 connections between them. The function returns the list of tuples where each tuple contains the following information:
from_nodename— the DNS name of the source worker node.from_nodeport— the port on the source worker node on which the database server is listening.to_nodename— the DNS name of the destination worker node.to_nodeport— the port on the destination worker node on which the database server is listening.result— whether a connection could be established.
The example below shows the output of the function:
SELECT * FROM citus_check_cluster_node_health();
from_nodename │ from_nodeport │ to_nodename │ to_nodeport │ result ---------------+---------------+-------------+-------------+-------- localhost | 1400 | localhost | 1400 | t localhost | 1400 | localhost | 1401 | t localhost | 1400 | localhost | 1402 | t localhost | 1401 | localhost | 1400 | t localhost | 1401 | localhost | 1401 | t localhost | 1401 | localhost | 1402 | t localhost | 1402 | localhost | 1400 | t localhost | 1402 | localhost | 1401 | t localhost | 1402 | localhost | 1402 | t (9 rows)
citus_set_coordinator_host (host text, port integer, node_role noderole, node_cluster name) returns void#This function is required when adding worker nodes to a citus cluster, which was created initially as a single-node cluster. When the coordinator registers a new worker, it adds a coordinator hostname from the value of the citus.local_hostname configuration parameter, which is
localhostby default. The worker would attempt to connect tolocalhostto talk to the coordinator, which is obviously wrong.Thus, the system administrator should call this function before calling the citus_add_node function in a single-node cluster.
Arguments:
host— the DNS name of the coordinator node.port— the port on which the coordinator lists for Postgres Pro connections. The default value of this optional argument iscurrent_setting('port').node_role— the role of the node. The default value of this optional argument isprimary.node_cluster— the name of the cluster. The default value of this optional argument isdefault.
The example below shows how to use the function:
-- Assuming we are in a single-node cluster -- First establish how workers should reach us SELECT citus_set_coordinator_host('coord.example.com', 5432); -- Then add a worker SELECT * FROM citus_add_node('worker1.example.com', 5432);get_shard_id_for_distribution_column (table_name regclass, distribution_value "any") returns bigint#citus assigns every row of a distributed table to a shard based on the value of the row's distribution column and the table's method of distribution. In most cases the precise mapping is a low-level detail that the database administrator can ignore. However, it can be useful to determine a row's shard either for manual database maintenance tasks or just to satisfy curiosity. The
get_shard_id_for_distribution_columnfunction provides this info for hash-distributed tables as well as reference tables and returns the shard ID that citus associates with the distribution column value for the given table.Arguments:
table_name— the name of the distributed table.distribution_value— the value of the distribution column. The default value isNULL.
The example below shows how to use the function:
SELECT get_shard_id_for_distribution_column('my_table', 4); get_shard_id_for_distribution_column -------------------------------------- 540007 (1 row)column_to_column_name (table_name regclass, column_var_text text) returns text#Translates the
partkeycolumn of the pg_dist_partition table into a textual column name. This is useful to determine the distribution column of a distributed table. The function returns the distribution column name of thetable_nametable. To learn more, see the Finding the Distribution Column For a Table section.Arguments:
table_name— name of the distributed table.column_var_text— value ofpartkeycolumn in thepg_dist_partitiontable.
The example below shows how to use the function:
-- Get distribution column name for products table SELECT column_to_column_name(logicalrelid, partkey) AS dist_col_name FROM pg_dist_partition WHERE logicalrelid='products'::regclass;
┌───────────────┐ │ dist_col_name │ ├───────────────┤ │ company_id │ └───────────────┘
citus_relation_size (logicalrelid regclass) returns bigint#Returns the disk space used by all the shards of the specified distributed table. This includes the size of the “main fork” but excludes the visibility map and free space map for the shards.
Arguments:
logicalrelid— the name of the distributed table.
The example below shows how to use the function:
SELECT pg_size_pretty(citus_relation_size('github_events'));pg_size_pretty -------------- 23 MB
citus_table_size (logicalrelid regclass) returns bigint#Returns the disk space used by all the shards of the specified distributed table, excluding indexes (but including TOAST, free space map, and visibility map).
Arguments:
logicalrelid— the name of the distributed table.
The example below shows how to use the function:
SELECT pg_size_pretty(citus_table_size('github_events'));pg_size_pretty -------------- 37 MB
citus_total_relation_size (logicalrelid regclass, fail_on_error boolean) returns bigint#Returns the total disk space used by the all the shards of the specified distributed table, including all indexes and TOAST data.
Arguments:
logicalrelid— the name of the distributed table.fail_on_error. The default value istrue.
The example below shows how to use the function:
SELECT pg_size_pretty(citus_total_relation_size('github_events'));pg_size_pretty -------------- 73 MB
citus_stat_statements_reset () returns void#Removes all rows from the citus_stat_statements table. Note that this works independently from the
pg_stat_statements_resetfunction. To reset all stats, call both functions.
J.5.8.5.1.3. Cluster Management And Repair Functions #
citus_move_shard_placement (shard_id bigint, source_node_name text, source_node_port integer, target_node_name text, target_node_port integer, shard_transfer_mode citus.shard_transfer_mode) returns void#Moves a given shard (and shards co-located with it) from one node to another. It is typically used indirectly during shard rebalancing rather than being called directly by a database administrator.
There are two ways to move the data: blocking or non-blocking. The blocking approach means that during the move all modifications to the shard are paused. The second way, which avoids blocking shard writes, relies on Postgres Pro 10 logical replication.
After a successful move operation, shards in the source node get deleted. If the move fails at any point, this function throws an error and leaves the source and target nodes unchanged.
Arguments:
shard_id— the ID of the shard to be moved.source_node_name— the DNS name of the node on which the healthy shard placement is present (“source” node).source_node_port— the port on the source worker node on which the database server is listening.target_node_name— the DNS name of the node on which the invalid shard placement is present (“target” node).target_node_port— the port on the target worker node on which the database server is listening.shard_transfer_mode— specify the method of replication, whether to use Postgres Pro logical replication or a cross-workerCOPYcommand. The allowed values of this optional argument are:auto— require replica identity if logical replication is possible, otherwise use legacy behaviour. This is the default value.force_logical— use logical replication even if the table does not have a replica identity. Any concurrent update/delete statements to the table will fail during replication.block_writes— useCOPY(blocking writes) for tables lacking primary key or replica identity.
The example below shows how to use the function:
SELECT citus_move_shard_placement(12345, '
from_host', 5432, 'to_host', 5432);citus_rebalance_start (rebalance_strategy name, drain_only boolean, shard_transfer_mode citus.shard_transfer_mode) returns bigint#Moves table shards to make them evenly distributed among the workers. It begins a background job to do the rebalancing and returns immediately.
The rebalancing process first calculates the list of moves it needs to make in order to ensure that the cluster is balanced within the given threshold. Then, it moves shard placements one by one from the source node to the destination node and updates the corresponding shard metadata to reflect the move.
Every shard is assigned a cost when determining whether shards are “evenly distributed”. By default each shard has the same cost (a value of 1), so distributing to equalize the cost across workers is the same as equalizing the number of shards on each. The constant cost strategy is called
by_shard_countand is the default rebalancing strategy.The
by_shard_countstrategy is appropriate under these circumstances:The shards are roughly the same size.
The shards get roughly the same amount of traffic.
Worker nodes are all the same size/type.
Shards have not been pinned to particular workers.
If any of these assumptions do not hold, then rebalancing using the
by_shard_countstrategy can result in a bad plan.If any of these assumptions do not hold, then rebalancing using the
by_shard_countstrategy can result in a bad plan.The default rebalancing starategy is
by_disk_size. You can always customize the strategy, using therebalance_strategyparameter.It is advisable to call the get_rebalance_table_shards_plan function before
citus_rebalance_startto see and verify the actions to be performed.Arguments:
rebalance_strategy— name of a strategy in the pg_dist_rebalance_strategy table. If this argument is omitted, the function chooses the default strategy, as indicated in the table. The default value of this optional argument isNULL.drain_only. Whentrue, move shards off worker nodes who haveshouldhaveshardsset tofalsein the pg_dist_node table; move no other shards. The default value of this optional argument isfalse.shard_transfer_mode— specify the method of replication, whether to use Postgres Pro logical replication or a cross-workerCOPYcommand. The allowed values of this optional argument are:auto— require replica identity if logical replication is possible, otherwise use legacy behaviour. This is the default value.force_logical— use logical replication even if the table does not have a replica identity. Any concurrent update/delete statements to the table will fail during replication.block_writes— useCOPY(blocking writes) for tables lacking primary key or replica identity.
The example below will attempt to rebalance shards:
SELECT citus_rebalance_start(); NOTICE: Scheduling... NOTICE: Scheduled as job 1337. DETAIL: Rebalance scheduled as background job 1337. HINT: To monitor progress, run: SELECT details FROM citus_rebalance_status();
citus_rebalance_status () returns table#Allows you to monitor the progress of the rebalance. Returns immediately, while the rebalance continues as a background job.
To get general information about the rebalance, you can select all columns from the status. This shows the basic state of the job:
SELECT * FROM citus_rebalance_status();
. job_id | state | job_type | description | started_at | finished_at | details --------+----------+-----------+---------------------------------+-------------------------------+-------------------------------+----------- 4 | running | rebalance | Rebalance colocation group 1 | 2022-08-09 21:57:27.833055+02 | 2022-08-09 21:57:27.833055+02 | { ... }Rebalancer specifics live in the
detailscolumn, in JSON format:SELECT details FROM citus_rebalance_status();
{ "phase": "copy", "phase_index": 1, "phase_count": 3, "last_change":"2022-08-09 21:57:27", "colocations": { "1": { "shard_moves": 30, "shard_moved": 29, "last_move":"2022-08-09 21:57:27" }, "1337": { "shard_moves": 130, "shard_moved": 0 } } }citus_rebalance_stop () returns void#Cancels the rebalance in progress, if any.
citus_rebalance_wait () returns void#Blocks until a running rebalance is complete. If no rebalance is in progress when this function is called, then the function returns immediately.
The function can be useful for scripts or benchmarking.
get_rebalance_table_shards_plan () returns table#Outputs the planned shard movements of the citus_rebalance_start function without performing them. While it is unlikely, this function can output a slightly different plan than what a
citus_rebalance_startcall with the same arguments will do. This could happen because they are not executed at the same time, so facts about the cluster, e.g. disk space, might differ between the calls. The function returns tuples containing the following columns:table_name— the table whose shards would move.shardid— the shard in question.shard_size— the size, in bytes.sourcename— the hostname of the source node.sourceport— the port of the source node.targetname— the hostname of the destination node.targetport— the port of the destination node.
Arguments:
A superset of the arguments for the citus_rebalance_start function:
relation,threshold,max_shard_moves,excluded_shard_list, anddrain_only.
get_rebalance_progress () returns table#Once the shard rebalance begins, this function lists the progress of every shard involved. It monitors the moves planned and executed by the citus_rebalance_start function. The function returns tuples containing the following columns:
sessionid— the Postgres Pro PID of the rebalance monitor.table_name— the table whose shards are moving.shardid— the shard in question.shard_size— the size of the shard, in bytes.sourcename— the hostname of the source node.sourceport— the port of the source node.targetname— the hostname of the destination node.targetport— the port of the destination node.progress. The following values may be returned:0— waiting to be moved,1— moving,2— complete.source_shard_size— the size of the shard on the source node, in bytes.target_shard_size— the size of the shard on the target node, in bytes.
The example below shows how to use the function:
SELECT * FROM get_rebalance_progress();
┌───────────┬────────────┬─────────┬────────────┬───────────────┬────────────┬───────────────┬────────────┬──────────┬───────────────────┬───────────────────┐ │ sessionid │ table_name │ shardid │ shard_size │ sourcename │ sourceport │ targetname │ targetport │ progress │ source_shard_size │ target_shard_size │ ├───────────┼────────────┼─────────┼────────────┼───────────────┼────────────┼───────────────┼────────────┼──────────┼───────────────────┼───────────────────┤ │ 7083 │ foo │ 102008 │ 1204224 │ n1.foobar.com │ 5432 │ n4.foobar.com │ 5432 │ 0 │ 1204224 │ 0 │ │ 7083 │ foo │ 102009 │ 1802240 │ n1.foobar.com │ 5432 │ n4.foobar.com │ 5432 │ 0 │ 1802240 │ 0 │ │ 7083 │ foo │ 102018 │ 614400 │ n2.foobar.com │ 5432 │ n4.foobar.com │ 5432 │ 1 │ 614400 │ 354400 │ │ 7083 │ foo │ 102019 │ 8192 │ n3.foobar.com │ 5432 │ n4.foobar.com │ 5432 │ 2 │ 0 │ 8192 │ └───────────┴────────────┴─────────┴────────────┴───────────────┴────────────┴───────────────┴────────────┴──────────┴───────────────────┴───────────────────┘
citus_add_rebalance_strategy (name name, shard_cost_function regproc, node_capacity_function regproc, shard_allowed_on_node_function regproc, default_threshold float4, minimum_threshold float4, improvement_threshold float4) returns void#Append a row to the pg_dist_rebalance_strategy table.
Arguments:
name— the identifier for the new strategy.shard_cost_function— identifies the function used to determine the “cost” of each shard.node_capacity_function— identifies the function to measure node capacity.shard_allowed_on_node_function— identifies the function that determines which shards can be placed on which nodes.default_threshold— floating point threshold that tunes how precisely the cumulative shard cost should be balanced between nodes.minimum_threshold— safeguard column that holds the minimum value allowed for the threshold argument of the citus_rebalance_start function. The default value is0.improvement_threshold. The default value is0.
citus_set_default_rebalance_strategy (name text) returns void#Update the pg_dist_rebalance_strategy table changing the strategy named by its argument to be the default chosen when rebalancing shards.
Arguments:
name— the name of the strategy in thepg_dist_rebalance_strategytable.
The example below shows how to use the function:
SELECT citus_set_default_rebalance_strategy('by_disk_size');citus_remote_connection_stats () returns setof record#Shows the number of active connections to each remote node.
The example below shows how to use the function:
SELECT * FROM citus_remote_connection_stats();
. hostname | port | database_name | connection_count_to_node ----------------+------+---------------+-------------------------- citus_worker_1 | 5432 | postgres | 3 (1 row)citus_drain_node (nodename text, nodeport integer, shard_transfer_mode citus.shard_transfer_mode, rebalance_strategy name) returns void#Moves shards off the designated node and onto other nodes who have
shouldhaveshardsset totruein the pg_dist_node table. This function is designed to be called prior to removing a node from the cluster, i.e. turning the node's physical server off.Arguments:
nodename— the DNS name of the node to be drained.nodeport— the port number of the node to be drained.shard_transfer_mode— specify the method of replication, whether to use Postgres Pro logical replication or a cross-workerCOPYcommand. The allowed values of this optional argument are:auto— require replica identity if logical replication is possible, otherwise use legacy behaviour. This is the default value.force_logical— use logical replication even if the table does not have a replica identity. Any concurrent update/delete statements to the table will fail during replication.block_writes— useCOPY(blocking writes) for tables lacking primary key or replica identity.
rebalance_strategy— the name of a strategy in the pg_dist_rebalance_strategy table. If this argument is omitted, the function chooses the default strategy, as indicated in the table. The default value of this optional argument isNULL.
Here are the typical steps to remove a single node (for example '10.0.0.1' on a standard Postgres Pro port):
Drain the node.
SELECT * FROM citus_drain_node('10.0.0.1', 5432);Wait until the command finishes.
Remove the node.
When draining multiple nodes it is recommended to use the citus_rebalance_start function instead. Doing so allows citus to plan ahead and move shards the minimum number of times.
Run this for each node that you want to remove:
SELECT * FROM citus_set_node_property(node_hostname, node_port, 'shouldhaveshards', false);
Drain them all at once with the citus_rebalance_start function:
SELECT * FROM citus_rebalance_start(drain_only := true);
Wait until the draining rebalance finishes.
Remove the nodes.
isolate_tenant_to_new_shard (table_name regclass, tenant_id "any", cascade_option text, shard_transfer_mode citus.shard_transfer_mode) returns bigint#Creates a new shard to hold rows with a specific single value in the distribution column. It is especially handy for the multi-tenant citus use case, where a large tenant can be placed alone on its own shard and ultimately its own physical node. To learn more, see the Tenant Isolation section. The function returns the unique ID assigned to the newly created shard.
Arguments:
table_name— the name of the table to get a new shard.tenant_id— the value of the distribution column which will be assigned to the new shard.cascade_option. When set toCASCADE, also isolates a shard from all tables in the current table's co-locating tables.shard_transfer_mode— specify the method of replication, whether to use Postgres Pro logical replication or a cross-workerCOPYcommand. The allowed values of this optional argument are:auto— require replica identity if logical replication is possible, otherwise use legacy behaviour. This is the default value.force_logical— use logical replication even if the table does not have a replica identity. Any concurrent update/delete statements to the table will fail during replication.block_writes— useCOPY(blocking writes) for tables lacking primary key or replica identity.
The example below shows how to create a new shard to hold the lineitems for tenant
135:SELECT isolate_tenant_to_new_shard('lineitem', 135);┌─────────────────────────────┐ │ isolate_tenant_to_new_shard │ ├─────────────────────────────┤ │ 102240 │ └─────────────────────────────┘
citus_create_restore_point (name text) returns pg_lsn#Temporarily blocks writes to the cluster, and creates a named restore point on all nodes. This function is similar to pg_create_restore_point, but applies to all nodes and makes sure the restore point is consistent across them. This function is well suited to doing point-in-time recovery, and cluster forking. The function returns the
coordinator_lsnvalue, i.e. the log sequence number of the restore point in the coordinator node WAL.Arguments:
name— the name of the restore point to create.
The example below shows how to use the function:
SELECT citus_create_restore_point('foo');┌────────────────────────────┐ │ citus_create_restore_point │ ├────────────────────────────┤ │ 0/1EA2808 │ └────────────────────────────┘
J.5.8.5.2. citus Tables and Views #
J.5.8.5.2.1. Coordinator Metadata #
citus divides each distributed table into multiple logical shards based on the distribution column. The coordinator then maintains metadata tables to track statistics and information about the health and location of these shards. In this section, we describe each of these metadata tables and their schema. You can view and query these tables using SQL after logging into the coordinator node.
The pg_dist_partition Table #
The pg_dist_partition table stores metadata about which tables in the database are distributed. For each distributed table, it also stores information about the distribution method and detailed information about the distribution column.
| Name | Type | Description |
|---|---|---|
| logicalrelid | regclass | Distributed table to which this row corresponds. This value references the relfilenode column in the pg_class system catalog table. |
| partmethod | char | The method used for partitioning / distribution. The values of this column corresponding to different distribution methods are: hash — h, reference table — n. |
| partkey | text | Detailed information about the distribution column including column number, type, and other relevant information. |
| colocationid | integer | Co-location group to which this table belongs. Tables in the same group allow co-located joins and distributed rollups among other optimizations. This value references the colocationid column in the pg_dist_colocation table. |
| repmodel | char | The method used for data replication. The values of this column corresponding to different replication methods are: Postgres Pro streaming replication — s, two-phase commit (for reference tables) — t. |
SELECT * FROM pg_dist_partition;
logicalrelid | partmethod | partkey | colocationid | repmodel
---------------+------------+------------------------------------------------------------------------------------------------------------------------+--------------+----------
github_events | h | {VAR :varno 1 :varattno 4 :vartype 20 :vartypmod -1 :varcollid 0 :varlevelsup 0 :varnoold 1 :varoattno 4 :location -1} | 2 | s
(1 row)
The pg_dist_shard Table #
The pg_dist_shard table stores metadata about individual shards of a table. This includes information about which distributed table the shard belongs to and statistics about the distribution column for that shard. In case of hash distributed tables, they are hash token ranges assigned to that shard. These statistics are used for pruning away unrelated shards during SELECT queries.
| Name | Type | Description |
|---|---|---|
| logicalrelid | regclass | Distributed table to which this shard belongs. This value references the relfilenode column in the pg_class system catalog table. |
| shardid | bigint | Globally unique identifier assigned to this shard. |
| shardstorage | char | Type of storage used for this shard. Different storage types are discussed in the table below. |
| shardminvalue | text | For hash distributed tables, minimum hash token value assigned to that shard (inclusive). |
| shardmaxvalue | text | For hash distributed tables, maximum hash token value assigned to that shard (inclusive). |
SELECT * FROM pg_dist_shard; logicalrelid | shardid | shardstorage | shardminvalue | shardmaxvalue ---------------+---------+--------------+---------------+--------------- github_events | 102026 | t | 268435456 | 402653183 github_events | 102027 | t | 402653184 | 536870911 github_events | 102028 | t | 536870912 | 671088639 github_events | 102029 | t | 671088640 | 805306367 (4 rows)
The shardstorage column in pg_dist_shard indicates the type of storage used for the shard. A brief overview of different shard storage types and their representation is below.
| Storage Type | shardstorage value | Description |
|---|---|---|
| TABLE | t | Indicates that shard stores data belonging to a regular distributed table. |
| COLUMNAR | c | Indicates that shard stores columnar data. (Used by distributed cstore_fdw tables). |
| FOREIGN | f | Indicates that shard stores foreign data. (Used by distributed file_fdw tables). |
The citus_shards View #
In addition to the low-level shard metadata table described above, citus provides the citus_shards view to easily check:
Where each shard is (node and port),
What kind of table it belongs to, and
Its size.
This view helps you inspect shards to find, among other things, any size imbalances across nodes.
SELECT * FROM citus_shards;
. table_name | shardid | shard_name | citus_table_type | colocation_id | nodename | nodeport | shard_size ------------+---------+--------------+------------------+---------------+-----------+----------+------------ dist | 102170 | dist_102170 | distributed | 34 | localhost | 9701 | 90677248 dist | 102171 | dist_102171 | distributed | 34 | localhost | 9702 | 90619904 dist | 102172 | dist_102172 | distributed | 34 | localhost | 9701 | 90701824 dist | 102173 | dist_102173 | distributed | 34 | localhost | 9702 | 90693632 ref | 102174 | ref_102174 | reference | 2 | localhost | 9701 | 8192 ref | 102174 | ref_102174 | reference | 2 | localhost | 9702 | 8192 dist2 | 102175 | dist2_102175 | distributed | 34 | localhost | 9701 | 933888 dist2 | 102176 | dist2_102176 | distributed | 34 | localhost | 9702 | 950272 dist2 | 102177 | dist2_102177 | distributed | 34 | localhost | 9701 | 942080 dist2 | 102178 | dist2_102178 | distributed | 34 | localhost | 9702 | 933888
The colocation_id refers to the colocation group. For more info about citus_table_type, see the Table Types section.
The pg_dist_placement Table #
The pg_dist_placement table tracks the location of shards on worker nodes. Each shard assigned to a specific node is called a shard placement. This table stores information about the health and location of each shard placement.
| Name | Type | Description |
|---|---|---|
| placementid | bigint | Unique auto-generated identifier for each individual placement. |
| shardid | bigint | Shard identifier associated with this placement. This value references the shardid column in the pg_dist_shard catalog table. |
| shardstate | int | Describes the state of this placement. Different shard states are discussed in the section below. |
| shardlength | bigint | For hash distributed tables, zero. |
| groupid | int | Identifier used to denote a group of one primary server and zero or more secondary servers. |
SELECT * FROM pg_dist_placement;
placementid | shardid | shardstate | shardlength | groupid
-------------+---------+------------+-------------+---------
1 | 102008 | 1 | 0 | 1
2 | 102008 | 1 | 0 | 2
3 | 102009 | 1 | 0 | 2
4 | 102009 | 1 | 0 | 3
5 | 102010 | 1 | 0 | 3
6 | 102010 | 1 | 0 | 4
7 | 102011 | 1 | 0 | 4
The pg_dist_node Table #
The pg_dist_node table contains information about the worker nodes in the cluster.
| Name | Type | Description |
|---|---|---|
| nodeid | int | Auto-generated identifier for an individual node. |
| groupid | int | Identifier used to denote a group of one primary server and zero or more secondary servers. By default it is the same as the nodeid. |
| nodename | text | Host name or IP Address of the Postgres Pro worker node. |
| nodeport | int | Port number on which the Postgres Pro worker node is listening. |
| noderack | text | Rack placement information for the worker node. This is an optional column. |
| hasmetadata | boolean | Reserved for internal use. |
| isactive | boolean | Whether the node is active accepting shard placements. |
| noderole | text | Whether the node is a primary or secondary. |
| nodecluster | text | The name of the cluster containing this node. |
| metadatasynced | boolean | Reserved for internal use. |
| shouldhaveshards | boolean | If false, shards will be moved off node (drained) when rebalancing, nor will shards from new distributed tables be placed on the node, unless they are co-located with shards already there. |
SELECT * FROM pg_dist_node;
nodeid | groupid | nodename | nodeport | noderack | hasmetadata | isactive | noderole | nodecluster | metadatasynced | shouldhaveshards
--------+---------+-----------+----------+----------+-------------+----------+----------+-------------+----------------+------------------
1 | 1 | localhost | 12345 | default | f | t | primary | default | f | t
2 | 2 | localhost | 12346 | default | f | t | primary | default | f | t
3 | 3 | localhost | 12347 | default | f | t | primary | default | f | t
(3 rows)
The citus.pg_dist_object Table #
The citus.pg_dist_object table contains a list of objects such as types and functions that have been created on the coordinator node and propagated to worker nodes. When an administrator adds new worker nodes to the cluster, citus automatically creates copies of the distributed objects on the new nodes (in the correct order to satisfy object dependencies).
| Name | Type | Description |
|---|---|---|
| classid | oid | Class of the distributed object |
| objid | oid | Object ID of the distributed object |
| objsubid | integer | Object sub-ID of the distributed object, e.g. attnum |
| type | text | Part of the stable address used during upgrades with pg_upgrade |
| object_names | text[] | Part of the stable address used during upgrades with pg_upgrade |
| object_args | text[] | Part of the stable address used during upgrades with pg_upgrade |
| distribution_argument_index | integer | Only valid for distributed functions/procedures |
| colocationid | integer | Only valid for distributed functions/procedures |
“Stable addresses” uniquely identify objects independently of a specific server. citus tracks objects during a Postgres Pro upgrade using stable addresses created with the pg_identify_object_as_address function.
Here is an example of how the create_distributed_function function adds entries to the citus.pg_dist_object table:
CREATE TYPE stoplight AS enum ('green', 'yellow', 'red');
CREATE OR REPLACE FUNCTION intersection()
RETURNS stoplight AS $$
DECLARE
color stoplight;
BEGIN
SELECT *
FROM unnest(enum_range(NULL::stoplight)) INTO color
ORDER BY random() LIMIT 1;
RETURN color;
END;
$$ LANGUAGE plpgsql VOLATILE;
SELECT create_distributed_function('intersection()');
-- Will have two rows, one for the TYPE and one for the FUNCTION
TABLE citus.pg_dist_object;
-[ RECORD 1 ]---------------+------ classid | 1247 objid | 16780 objsubid | 0 type | object_names | object_args | distribution_argument_index | colocationid | -[ RECORD 2 ]---------------+------ classid | 1255 objid | 16788 objsubid | 0 type | object_names | object_args | distribution_argument_index | colocationid |
The citus_schemas View #
citus supports schema-based sharding and provides the citus_schemas view that shows which schemas have been distributed in the system. The view only lists distributed schemas, local schemas are not displayed.
| Name | Type | Description |
|---|---|---|
| schema_name | regnamespace | Name of the distributed schema |
| colocation_id | integer | Co-location ID of the distributed schema |
| schema_size | text | Human-readable size summary of all objects within the schema |
| schema_owner | name | Role that owns the schema |
Here is an example:
schema_name | colocation_id | schema_size | schema_owner --------------+---------------+-------------+-------------- user_service | 1 | 0 bytes | user_service time_service | 2 | 0 bytes | time_service ping_service | 3 | 632 kB | ping_service
The citus_tables View #
The citus_tables view shows a summary of all tables managed by citus (distributed and reference tables). The view combines information from citus metadata tables for an easy, human-readable overview of these table properties:
Human-readable size
Shard count
Owner (database user)
Access method (
heapor columnar)
Here is an example:
SELECT * FROM citus_tables;
┌────────────┬──────────────────┬─────────────────────┬───────────────┬────────────┬─────────────┬─────────────┬───────────────┐ │ table_name │ citus_table_type │ distribution_column │ colocation_id │ table_size │ shard_count │ table_owner │ access_method │ ├────────────┼──────────────────┼─────────────────────┼───────────────┼────────────┼─────────────┼─────────────┼───────────────┤ │ foo.test │ distributed │ test_column │ 1 │ 0 bytes │ 32 │ citus │ heap │ │ ref │ reference │ <none> │ 2 │ 24 GB │ 1 │ citus │ heap │ │ test │ distributed │ id │ 1 │ 248 TB │ 32 │ citus │ heap │ └────────────┴──────────────────┴─────────────────────┴───────────────┴────────────┴─────────────┴─────────────┴───────────────┘
The time_partitions View #
citus provides user defined functions to manage partitions for the timeseries use case. It also maintains the time_partitions view to inspect the partitions it manages.
The columns of this view are as follows:
parent_table— the table which is partitioned.partition_column— the column on which the parent table is partitioned.partition— the name of a partition.from_value— lower bound in time for rows in this partition.to_value— upper bound in time for rows in this partition.access_method—heapfor row-based storage andcolumnarfor columnar storage.
SELECT * FROM time_partitions;
┌────────────────────────┬──────────────────┬─────────────────────────────────────────┬─────────────────────┬─────────────────────┬───────────────┐ │ parent_table │ partition_column │ partition │ from_value │ to_value │ access_method │ ├────────────────────────┼──────────────────┼─────────────────────────────────────────┼─────────────────────┼─────────────────────┼───────────────┤ │ github_columnar_events │ created_at │ github_columnar_events_p2015_01_01_0000 │ 2015-01-01 00:00:00 │ 2015-01-01 02:00:00 │ columnar │ │ github_columnar_events │ created_at │ github_columnar_events_p2015_01_01_0200 │ 2015-01-01 02:00:00 │ 2015-01-01 04:00:00 │ columnar │ │ github_columnar_events │ created_at │ github_columnar_events_p2015_01_01_0400 │ 2015-01-01 04:00:00 │ 2015-01-01 06:00:00 │ columnar │ │ github_columnar_events │ created_at │ github_columnar_events_p2015_01_01_0600 │ 2015-01-01 06:00:00 │ 2015-01-01 08:00:00 │ heap │ └────────────────────────┴──────────────────┴─────────────────────────────────────────┴─────────────────────┴─────────────────────┴───────────────┘
The pg_dist_colocation Table #
The pg_dist_colocation table contains information about which tables' shards should be placed together, or co-located. When two tables are in the same co-location group, citus ensures shards with the same partition values will be placed on the same worker nodes. This enables join optimizations, certain distributed rollups, and foreign key support. Shard co-location is inferred when the shard counts, and partition column types all match between two tables; however, a custom co-location group may be specified when creating a distributed table, if so desired.
| Name | Type | Description |
|---|---|---|
| colocationid | int | Unique identifier for the co-location group this row corresponds to |
| shardcount | int | Shard count for all tables in this co-location group |
| replicationfactor | int | Replication factor for all tables in this co-location group. (Deprecated) |
| distributioncolumntype | oid | The type of the distribution column for all tables in this co-location group |
| distributioncolumncollation | oid | The collation of the distribution column for all tables in this co-location group |
SELECT * FROM pg_dist_colocation;
colocationid | shardcount | replicationfactor | distributioncolumntype | distributioncolumncollation
--------------+------------+-------------------+------------------------+-----------------------------
2 | 32 | 1 | 20 | 0
(1 row)
The pg_dist_rebalance_strategy Table #
This table defines strategies that the citus_rebalance_start function can use to determine where to move shards.
| Name | Type | Description |
|---|---|---|
| name | name | Unique name for the strategy |
| default_strategy | boolean | Whether citus_rebalance_start should choose this strategy by default. Use citus_set_default_rebalance_strategy to update this column. |
| shard_cost_function | regproc | Identifier for a cost function, which must take a shardid as bigint and return its notion of a cost, as type real. |
| node_capacity_function | regproc | Identifier for a capacity function, which must take a nodeid as int and return its notion of node capacity as type real. |
| shard_allowed_on_node_function | regproc | Identifier for a function that given shardid bigint and nodeidarg int, returns boolean for whether the shard is allowed to be stored on the node. |
| default_threshold | float4 | Threshold for deeming a node too full or too empty, which determines when the citus_rebalance_start function should try to move shards. |
| minimum_threshold | float4 | A safeguard to prevent the threshold argument of citus_rebalance_start from being set too low. |
| improvement_threshold | float4 | Determines when moving a shard is worth it during a rebalance. The rebalancer will move a shard when the ratio of the improvement with the shard move to the improvement without crosses the threshold. This is most useful with the by_disk_size strategy. |
A citus installation ships with these strategies in the table:
SELECT * FROM pg_dist_rebalance_strategy;
-[ RECORD 1 ]------------------+--------------------------------- name | by_shard_count default_strategy | f shard_cost_function | citus_shard_cost_1 node_capacity_function | citus_node_capacity_1 shard_allowed_on_node_function | citus_shard_allowed_on_node_true default_threshold | 0 minimum_threshold | 0 improvement_threshold | 0 -[ RECORD 2 ]------------------+--------------------------------- name | by_disk_size default_strategy | t shard_cost_function | citus_shard_cost_by_disk_size node_capacity_function | citus_node_capacity_1 shard_allowed_on_node_function | citus_shard_allowed_on_node_true default_threshold | 0.1 minimum_threshold | 0.01 improvement_threshold | 0.5
The by_shard_count strategy assigns every shard the same cost. Its effect is to equalize the shard count across nodes. The default strategy, by_disk_size, assigns a cost to each shard matching its disk size in bytes plus that of the shards that are co-located with it. The disk size is calculated using pg_total_relation_size, so it includes indices. This strategy attempts to achieve the same disk space on every node. Note the threshold of 0.1 — it prevents unnecessary shard movement caused by insigificant differences in disk space.
Here are examples of functions that can be used within new shard rebalancer strategies, and registered in the pg_dist_rebalance_strategy table with the citus_add_rebalance_strategy function.
Setting a node capacity exception by hostname pattern:
-- Example of node_capacity_function CREATE FUNCTION v2_node_double_capacity(nodeidarg int) RETURNS real AS $$ SELECT (CASE WHEN nodename LIKE '%.v2.worker.citusdata.com' THEN 2.0::float4 ELSE 1.0::float4 END) FROM pg_dist_node where nodeid = nodeidarg $$ LANGUAGE sql;Rebalancing by number of queries that go to a shard, as measured by the citus_stat_statements table:
-- Example of shard_cost_function CREATE FUNCTION cost_of_shard_by_number_of_queries(shardid bigint) RETURNS real AS $$ SELECT coalesce(sum(calls)::real, 0.001) as shard_total_queries FROM citus_stat_statements WHERE partition_key is not null AND get_shard_id_for_distribution_column('tab', partition_key) = shardid; $$ LANGUAGE sql;Isolating a specific shard (10000) on a node (address '10.0.0.1'):
-- Example of shard_allowed_on_node_function CREATE FUNCTION isolate_shard_10000_on_10_0_0_1(shardid bigint, nodeidarg int) RETURNS boolean AS $$ SELECT (CASE WHEN nodename = '10.0.0.1' THEN shardid = 10000 ELSE shardid != 10000 END) FROM pg_dist_node where nodeid = nodeidarg $$ LANGUAGE sql; -- The next two definitions are recommended in combination with the above function. -- This way the average utilization of nodes is not impacted by the isolated shard CREATE FUNCTION no_capacity_for_10_0_0_1(nodeidarg int) RETURNS real AS $$ SELECT (CASE WHEN nodename = '10.0.0.1' THEN 0 ELSE 1 END)::real FROM pg_dist_node where nodeid = nodeidarg $$ LANGUAGE sql; CREATE FUNCTION no_cost_for_10000(shardid bigint) RETURNS real AS $$ SELECT (CASE WHEN shardid = 10000 THEN 0 ELSE 1 END)::real $$ LANGUAGE sql;
The citus_stat_statements Table #
citus provides the citus_stat_statements table for stats about how queries are being executed, and for whom. It is analogous to (and can be joined with) the pg_stat_statements view in Postgres Pro, which tracks statistics about query speed.
| Name | Type | Description |
|---|---|---|
| queryid | bigint | Identifier (good for pg_stat_statements joins) |
| userid | oid | User who ran the query |
| dbid | oid | Database instance of coordinator |
| query | text | Anonymized query string |
| executor | text | citus executor used: adaptive, or INSERT-SELECT |
| partition_key | text | Value of distribution column in router-executed queries, else NULL |
| calls | bigint | Number of times the query was run |
-- Create and populate distributed table
CREATE TABLE foo ( id int );
SELECT create_distributed_table('foo', 'id');
INSERT INTO foo select generate_series(1,100);
-- Enable stats
-- pg_stat_statements must be in shared_preload_libraries
CREATE EXTENSION pg_stat_statements;
SELECT count(*) from foo;
SELECT * FROM foo where id = 42;
SELECT * FROM citus_stat_statements;
Results:
-[ RECORD 1 ]-+---------------------------------------------- queryid | -909556869173432820 userid | 10 dbid | 13340 query | insert into foo select generate_series($1,$2) executor | insert-select partition_key | calls | 1 -[ RECORD 2 ]-+---------------------------------------------- queryid | 3919808845681956665 userid | 10 dbid | 13340 query | select count(*) from foo; executor | adaptive partition_key | calls | 1 -[ RECORD 3 ]-+---------------------------------------------- queryid | 5351346905785208738 userid | 10 dbid | 13340 query | select * from foo where id = $1 executor | adaptive partition_key | 42 calls | 1
Caveats:
The stats data is not replicated and will not survive database crashes or failover.
Tracks a limited number of queries set by the pg_stat_statements.max configuration parameter. The default value is
5000.To truncate the table, use the citus_stat_statements_reset function.
The citus_stat_tenants View #
The citus_stat_tenants view augments the citus_stat_statements table with information about how many queries each tenant is running. Tracing queries to originating tenants helps, among other things, for deciding when to do tenant isolation.
This view counts recent single-tenant queries happening during a configurable time period. The tally of read-only and total queries for the period increases until the current period ends. After that, the counts are moved to last period's statistics, which stays constant until expiration. The period length can be set in seconds using citus.stats_tenants_period, and is 60 seconds by default.
The view displays up to citus.stat_tenants_limit rows (by default 100). It counts only queries filtered to a single tenant, ignoring queries that apply to multiple tenants at once.
| Name | Type | Description |
|---|---|---|
| nodeid | int | Node ID from the pg_dist_node |
| colocation_id | int | ID of the co-location group |
| tenant_attribute | text | Value in the distribution column identifying tenant |
| read_count_in_this_period | int | Number of read (SELECT) queries for tenant in period |
| read_count_in_last_period | int | Number of read queries one period of time ago |
| query_count_in_this_period | int | Number of read/write queries for tenant in time period |
| query_count_in_last_period | int | Number of read/write queries one period of time ago |
| cpu_usage_in_this_period | double | Seconds of CPU time spent for this tenant in period |
| cpu_usage_in_last_period | double | Seconds of CPU time spent for this tenant last period |
Tracking tenant level statistics adds overhead, and by default is disabled. To enable it, set citus.stat_tenants_track to 'all'.
By way of example, suppose we have a distributed table called dist_table, with distribution column tenant_id. Then we make some queries:
INSERT INTO dist_table(tenant_id) VALUES (1); INSERT INTO dist_table(tenant_id) VALUES (1); INSERT INTO dist_table(tenant_id) VALUES (2); SELECT count(*) FROM dist_table WHERE tenant_id = 1;
The tenant-level statistics will reflect the queries we just made:
SELECT tenant_attribute, read_count_in_this_period,
query_count_in_this_period, cpu_usage_in_this_period
FROM citus_stat_tenants;
tenant_attribute | read_count_in_this_period | query_count_in_this_period | cpu_usage_in_this_period ------------------+---------------------------+----------------------------+-------------------------- 1 | 1 | 3 | 0.000883 2 | 0 | 1 | 0.000144
Distributed Query Activity #
In some situations, queries might get blocked on row-level locks on one of the shards on a worker node. If that happens then those queries would not show up in pg_locks on the citus coordinator node.
citus provides special views to watch queries and locks throughout the cluster, including shard-specific queries used internally to build results for distributed queries.
citus_stat_activity— shows the distributed queries that are executing on all nodes. A superset of pg_stat_activity usable wherever the latter is.citus_dist_stat_activity— the same ascitus_stat_activitybut restricted to distributed queries only, and excluding citus fragments queries.citus_lock_waits— blocked queries throughout the cluster.
The first two views include all columns of pg_stat_activity plus the global PID of the worker that initiated the query.
For example, consider counting the rows in a distributed table:
-- Run in one session -- (with a pg_sleep so we can see it) SELECT count(*), pg_sleep(3) FROM users_table;
We can see the query appear in citus_dist_stat_activity:
-- Run in another session SELECT * FROM citus_dist_stat_activity; -[ RECORD 1 ]----+------------------------------------------- global_pid | 10000012199 nodeid | 1 is_worker_query | f datid | 13724 datname | postgres pid | 12199 leader_pid | usesysid | 10 usename | postgres application_name | psql client_addr | client_hostname | client_port | -1 backend_start | 2022-03-23 11:30:00.533991-05 xact_start | 2022-03-23 19:35:28.095546-05 query_start | 2022-03-23 19:35:28.095546-05 state_change | 2022-03-23 19:35:28.09564-05 wait_event_type | Timeout wait_event | PgSleep state | active backend_xid | backend_xmin | 777 query_id | query | SELECT count(*), pg_sleep(3) FROM users_table; backend_type | client backend
The citus_dist_stat_activity view hides internal citus fragment queries. To see those, we can use the more detailed citus_stat_activity view. For instance, the previous count(*) query requires information from all shards. Some of the information is in shard users_table_102039, which is visible in the query below.
SELECT * FROM citus_stat_activity; -[ RECORD 1 ]----+----------------------------------------------------------------------- global_pid | 10000012199 nodeid | 1 is_worker_query | f datid | 13724 datname | postgres pid | 12199 leader_pid | usesysid | 10 usename | postgres application_name | psql client_addr | client_hostname | client_port | -1 backend_start | 2022-03-23 11:30:00.533991-05 xact_start | 2022-03-23 19:32:18.260803-05 query_start | 2022-03-23 19:32:18.260803-05 state_change | 2022-03-23 19:32:18.260821-05 wait_event_type | Timeout wait_event | PgSleep state | active backend_xid | backend_xmin | 777 query_id | query | SELECT count(*), pg_sleep(3) FROM users_table; backend_type | client backend -[ RECORD 2 ]----+----------------------------------------------------------------------------------------- global_pid | 10000012199 nodeid | 1 is_worker_query | t datid | 13724 datname | postgres pid | 12725 leader_pid | usesysid | 10 usename | postgres application_name | citus_internal gpid=10000012199 client_addr | 127.0.0.1 client_hostname | client_port | 44106 backend_start | 2022-03-23 19:29:53.377573-05 xact_start | query_start | 2022-03-23 19:32:18.278121-05 state_change | 2022-03-23 19:32:18.278281-05 wait_event_type | Client wait_event | ClientRead state | idle backend_xid | backend_xmin | query_id | query | SELECT count(*) AS count FROM public.users_table_102039 users WHERE true backend_type | client backend
The query field shows rows being counted in shard 102039.
Here are examples of useful queries you can build using citus_stat_activity:
-- Active queries' wait events SELECT query, wait_event_type, wait_event FROM citus_stat_activity WHERE state='active'; -- Active queries' top wait events SELECT wait_event, wait_event_type, count(*) FROM citus_stat_activity WHERE state='active' GROUP BY wait_event, wait_event_type ORDER BY count(*) desc; -- Total internal connections generated per node by citus SELECT nodeid, count(*) FROM citus_stat_activity WHERE is_worker_query GROUP BY nodeid;
The next view is citus_lock_waits. To see how it works, we can generate a locking situation manually. First we will set up a test table from the coordinator:
CREATE TABLE numbers AS
SELECT i, 0 AS j FROM generate_series(1,10) AS i;
SELECT create_distributed_table('numbers', 'i');
Then, using two sessions on the coordinator, we can run this sequence of statements:
-- Session 1 -- Session 2
------------------------------------- -------------------------------------
BEGIN;
UPDATE numbers SET j = 2 WHERE i = 1;
BEGIN;
UPDATE numbers SET j = 3 WHERE i = 1;
-- (this blocks)
The citus_lock_waits view shows the situation.
SELECT * FROM citus_lock_waits; -[ RECORD 1 ]-------------------------+-------------------------------------- waiting_gpid | 10000011981 blocking_gpid | 10000011979 blocked_statement | UPDATE numbers SET j = 3 WHERE i = 1; current_statement_in_blocking_process | UPDATE numbers SET j = 2 WHERE i = 1; waiting_nodeid | 1 blocking_nodeid | 1
In this example the queries originated on the coordinator, but the view can also list locks between queries originating on workers.
J.5.8.5.2.2. Tables on All Nodes #
citus has other informational tables and views which are accessible on all nodes, not just the coordinator.
The pg_dist_authinfo Table #
The pg_dist_authinfo table holds authentication parameters used by citus nodes to connect to one another.
| Name | Type | Description |
|---|---|---|
| nodeid | integer | Node ID from pg_dist_node, or 0, or -1 |
| rolename | name | Postgres Pro role |
| authinfo | text | Space-separated libpq connection parameters |
Upon beginning a connection, a node consults the table to see whether a row with the destination nodeid and desired rolename exists. If so, the node includes the corresponding authinfo string in its libpq connection. A common example is to store a password, like 'password=abc123', but you can review the full list of possibilities.
The parameters in authinfo are space-separated, in the form key=val. To write an empty value, or a value containing spaces, surround it with single quotes, e.g., keyword='a value'. Single quotes and backslashes within the value must be escaped with a backslash, i.e., \' and \\.
The nodeid column can also take the special values 0 and -1, which mean all nodes or loopback connections, respectively. If, for a given node, both specific and all-node rules exist, the specific rule has precedence.
SELECT * FROM pg_dist_authinfo;
nodeid | rolename | authinfo
--------+----------+-----------------
123 | jdoe | password=abc123
(1 row)
The pg_dist_poolinfo Table #
If you want to use a connection pooler to connect to a node, you can specify the pooler options using pg_dist_poolinfo. This metadata table holds the host, port and database name for citus to use when connecting to a node through a pooler.
If pool information is present, citus will try to use these values instead of setting up a direct connection. The pg_dist_poolinfo information in this case supersedes pg_dist_node.
| Name | Type | Description |
|---|---|---|
| nodeid | integer | Node ID from pg_dist_node |
| poolinfo | text | Space-separated parameters: host, port, or dbname |
Note
In some situations citus ignores the settings in pg_dist_poolinfo. For instance shard rebalancing is not compatible with connection poolers such as pgbouncer. In these scenarios citus will use a direct connection.
-- How to connect to node 1 (as identified in pg_dist_node)
INSERT INTO pg_dist_poolinfo (nodeid, poolinfo)
VALUES (1, 'host=127.0.0.1 port=5433');
J.5.8.5.3. Configuration Reference #
There are various configuration parameters that affect the behaviour of citus. These include both standard Postgres Pro parameters and citus specific parameters. To learn more about Postgres Pro configuration parameters, you can visit the Server Configuration chapter.
The rest of this reference aims at discussing citus specific configuration parameters. These parameters can be set similar to Postgres Pro parameters by modifying postgresql.conf or by using the SET command.
As an example you can update a setting with:
ALTER DATABASE citus SET citus.multi_task_query_log_level = 'log';
J.5.8.5.3.1. General Configuration #
citus.max_background_task_executors_per_node(integer) #Determines how many background tasks can be executed in parallel at a given time. For instance, these tasks are for shard moves from/to a node. When increasing the value of this parameter, you will often also want to increase the value of the
citus.max_background_task_executorsandmax_worker_processesparameters. The minimum value is1, the maximum value is128. The default value is1.citus.max_worker_nodes_tracked(integer) #citus tracks worker nodes' locations and their membership in a shared hash table on the coordinator node. This configuration parameter limits the size of the hash table and consequently the number of worker nodes that can be tracked. The default value is
2048. This parameter can only be set at server start and is effective on the coordinator node.citus.use_secondary_nodes(enum) #Sets the policy to use when choosing nodes for the
SELECTqueries. If set toalways, the planner will query only nodes whosenoderoleis marked assecondaryin the pg_dist_node table. The allowed values are:never— all reads happen on primary nodes. This is the default value.always— reads run against secondary nodes instead andINSERT/UPDATEstatements are disabled.
citus.cluster_name(text) #Informs the coordinator node planner which cluster it coordinates. Once
cluster_nameis set, the planner will query worker nodes in that cluster alone.citus.enable_version_checks(boolean) #Upgrading citus version requires a server restart (to pick up the new shared library), as well as running the
ALTER EXTENSION UPDATEcommand. The failure to execute both steps could potentially cause errors or crashes. citus thus validates the version of the code and that of the extension match, and errors out if they do not.The default value is
true, and the parameter is effective on the coordinator. In rare cases, complex upgrade processes may require setting this parameter tofalse, thus disabling the check.citus.log_distributed_deadlock_detection(boolean) #Specifies whether to log distributed deadlock detection related processing in the server log. The default value is
false.citus.distributed_deadlock_detection_factor(floating point) #Sets the time to wait before checking for distributed deadlocks. In particular the time to wait will be this value multiplied by the value set in the Postgres Pro deadlock_timeout parameter. The default value is
2. The value of-1disables distributed deadlock detection.citus.node_connection_timeout(integer) #Sets the maximum duration to wait for connection establishment, in milliseconds. citus raises an error if the timeout elapses before at least one worker connection is established. This configuration parameter affects connections from the coordinator to workers and workers to each other. The minimum value is
10milliseconds, the maximum value is1hour. The default value is30seconds.The example below shows how to set this parameter:
-- Set to 60 seconds ALTER DATABASE foo SET citus.node_connection_timeout = 60000;
citus.node_conninfo(text) #Sets non-sensitive libpq connection parameters used for all inter-node connections.
The example below shows how to set this parameter:
-- key=value pairs separated by spaces. -- For example, ssl options: ALTER DATABASE foo SET citus.node_conninfo = 'sslrootcert=/path/to/citus.crt sslmode=verify-full';
citus supports only a specific subset of the allowed options, namely:
connect_timeoutgsslib(subject to the runtime presence of optional Postgres Pro features)hostkeepaliveskeepalives_countkeepalives_idlekeepalives_intervalkrbsrvname(subject to the runtime presence of optional Postgres Pro features)sslcompressionsslcrlsslmode(defaults torequire)sslnegotiationsslrootcerttcp_user_timeout
The
citus.node_conninfoconfiguration parameter takes effect only on newly opened connections. To force all connections to use the new settings, make sure to reload the Postgres Pro configuration:SELECT pg_reload_conf();
citus.local_hostname(text) #citus nodes need occasionally to connect to themselves for systems operations. By default, they use the
localhostaddress to refer to themselves, but this can cause problems. For instance, when a host requiressslmode=verify-fullfor incoming connections, addinglocalhostas an alternative hostname on the SSL certificate is not always desirable or even feasible.The
citus.local_hostnameconfiguration parameter selects the hostname a node uses to connect to itself. The default value islocalhost.The example below shows how to set this parameter:
ALTER SYSTEM SET citus.local_hostname TO 'mynode.example.com';
citus.show_shards_for_app_name_prefixes(text) #By default, citus hides shards from the list of tables Postgres Pro gives to SQL clients. It does this because there are multiple shards per distributed table, and the shards can be distracting to the SQL client.
The
citus.show_shards_for_app_name_prefixesconfiguration parameter allows shards to be displayed for selected clients that want to see them. The default value is''.The example below shows how to set this parameter:
-- Show shards to psql only (hide in other clients, like pgAdmin) SET citus.show_shards_for_app_name_prefixes TO 'psql'; -- Also accepts a comma-separated list SET citus.show_shards_for_app_name_prefixes TO 'psql,pg_dump';
citus.rebalancer_by_disk_size_base_cost(integer) #When using the
by_disk_sizerebalance strategy each shard group will get this cost in bytes added to its actual disk size. This is used to avoid creating a bad balance when there is very little data in some of the shards. The assumption is that even empty shards have some cost, because of parallelism and because empty shard groups will likely grow in the future. The default value is100MB.
J.5.8.5.3.2. Query Statistics #
citus.stat_statements_purge_interval(integer) #Sets the frequency at which the maintenance daemon removes records from the citus_stat_statements table that are unmatched in the pg_stat_statements view. This configuration parameter sets the time interval between purges in seconds, with the default value of
10. The value of0disables the purges. This parameter is effective on the coordinator and can be changed at runtime.The example below shows how to set this parameter:
SET citus.stat_statements_purge_interval TO 5;
citus.stat_statements_max(integer) #The maximum number of rows to store in the citus_stat_statements table. The default value is
50000and may be changed to any value in the range of1000-10000000. Note that each row requires 140 bytes of storage, so settingcitus.stat_statements_maxto its maximum value of 10M would consume 1.4GB of memory.Changing this configuration parameter will not take effect until Postgres Pro is restarted.
citus.stat_statements_track(enum) #Recording statistics for citus_stat_statements requires extra CPU resources. When the database is experiencing load, the administrator may wish to disable statement tracking. The
citus.stat_statements_trackconfiguration parameter can turn tracking on and off. The allowed values are:all— track all statements.none— disable tracking. This is the default value.
citus.stat_tenants_untracked_sample_rate(floating point) #Sampling rate for new tenants in the citus_stat_tenants view. The rate can be of range between
0.0and1.0. The default value is1.0meaning 100% of untracked tenant queries are sampled. Setting it to a lower value means that the already tracked tenants have 100% queries sampled, but tenants that are currently untracked are sampled only at the provided rate.
J.5.8.5.3.3. Data Loading #
citus.shard_count(integer) #Sets the shard count for hash-partitioned tables and defaults to
32. This value is used by the create_distributed_table function when creating hash-partitioned tables. This parameter can be set at runtime and is effective on the coordinator.citus.metadata_sync_mode(enum) #Note
This configuration parameter requires superuser access to change.
This configuration parameter determines how citus synchronizes metadata across nodes. By default, citus updates all metadata in a single transaction for consistency. However, Postgres Pro has a hard memory limit related to cache invalidations, and citus metadata syncing for a large cluster can fail from memory exhaustion.
As a workaround, citus provides an optional nontransactional sync mode, which uses a series of smaller transactions. While this mode works in limited memory, there is a possibility of transactions failing and leaving metadata in an inconsistency state. To help with this potential problem, nontransactional metadata sync is designed as an idempotent action, so you can re-run it repeatedly if needed.
There allowed values for this configiration parameters are as follows:
transactional— synchronize all metadata in a single transaction. This is the default value.nontransactional— synchronize metadata using multiple small transactions.
The example below shows how to set this parameter:
-- To add a new node and sync nontransactionally SET citus.metadata_sync_mode TO 'nontransactional'; SELECT citus_add_node(<ip>, <port>); -- To manually (re)sync SET citus.metadata_sync_mode TO 'nontransactional'; SELECT start_metadata_sync_to_all_nodes();
We advise trying transactional mode first and switching to nontransactional only if a memory failure occurs.
J.5.8.5.3.4. Planner Configuration #
citus.local_table_join_policy(enum) #Determines how citus moves data when doing a join between local and distributed tables. Customizing the join policy can help reduce the amount of data sent between worker nodes.
citus will send either the local or distributed tables to nodes as necessary to support the join. Copying table data is referred to as a “conversion”. If a local table is converted, then it will be sent to any workers that need its data to perform the join. If a distributed table is converted, then it will be collected in the coordinator to support the join. The citus planner will send only the necessary rows doing a conversion.
There are four modes available to express conversion preference:
auto— citus will convert either all local or all distributed tables to support local and distributed table joins. citus decides which to convert using a heuristic. It will convert distributed tables if they are joined using a constant filter on a unique index (such as a primary key). This ensures less data gets moved between workers. This is the default value.never— citus will not allow joins between local and distributed tables.prefer-local— citus will prefer converting local tables to support local and distributed table joins.prefer-distributed— citus will prefer converting distributed tables to support local and distributed table joins. If the distributed tables are huge, using this option might result in moving lots of data between workers.
For example, assume
citus_tableis a distributed table distributed by the columnx, and thatpostgres_tableis a local table:CREATE TABLE citus_table(x int primary key, y int); SELECT create_distributed_table('citus_table', 'x'); CREATE TABLE postgres_table(x int, y int); -- Even though the join is on primary key, there isn't a constant filter -- hence postgres_table will be sent to worker nodes to support the join SELECT * FROM citus_table JOIN postgres_table USING (x); -- There is a constant filter on a primary key, hence the filtered row -- from the distributed table will be pulled to coordinator to support the join SELECT * FROM citus_table JOIN postgres_table USING (x) WHERE citus_table.x = 10; SET citus.local_table_join_policy to 'prefer-distributed'; -- Since we prefer distributed tables, citus_table will be pulled to coordinator -- to support the join. Note that citus_table can be huge SELECT * FROM citus_table JOIN postgres_table USING (x); SET citus.local_table_join_policy to 'prefer-local'; -- Even though there is a constant filter on primary key for citus_table -- postgres_table will be sent to necessary workers because we are using 'prefer-local' SELECT * FROM citus_table JOIN postgres_table USING (x) WHERE citus_table.x = 10;citus.limit_clause_row_fetch_count(integer) #Sets the number of rows to fetch per task for limit clause optimization. In some cases,
SELECTqueries withLIMITclauses may need to fetch all rows from each task to generate results. In those cases, and where an approximation would produce meaningful results, this configuration parameter sets the number of rows to fetch from each shard. Limit approximations are disabled by default and this parameter is set to-1. This value can be set at runtime and is effective on the coordinator.citus.count_distinct_error_rate(floating point) #citus can calculate
count(distinct)approximates using the Postgres Pro hll extension. This configuration parameter sets the desired error rate when calculatingcount(distinct):0.0, which is the default value, disables approximations forcount(distinct), and1.0, which provides no guarantees about the accuracy of results. We recommend setting this parameter to0.005for best results. This value can be set at runtime and is effective on the coordinator.citus.task_assignment_policy(enum) #Note
This configuration parameter is applicable for queries against reference tables.
Sets the policy to use when assigning tasks to workers. The coordinator assigns tasks to workers based on shard locations. This configuration parameter specifies the policy to use when making these assignments. Currently, there are three possible task assignment policies, which can be used:
greedy— aims at evenly distributing tasks across workers. This is the default value.round-robin— assigns tasks to workers in around-robinfashion alternating between different replicas. This enables much better cluster utilization when the shard count for a table is low compared to the number of workers.first-replica— assigns tasks on the basis of the insertion order of placements (replicas) for the shards. In other words, the fragment query for a shard is simply assigned to the worker which has the first replica of that shard. This method allows you to have strong guarantees about which shards will be used on which nodes (i.e. stronger memory residency guarantees).
This configuration parameter can be set at runtime and is effective on the coordinator.
citus.enable_non_colocated_router_query_pushdown(boolean) #Enables router planner for the queries that reference non-colocated distributed tables.
Normally, router planner is only enabled for the queries that reference co-located distributed tables because it is not guaranteed to have the target shards always on the same node, e.g., after rebalancing the shards. For this reason, while enabling this flag allows some degree of optimization for the queries that reference non-colocated distributed tables, it is not guaranteed that the same query will work after rebalancing the shards or altering the shard count of one of those distributed tables. The default value is
off.
J.5.8.5.3.5. Intermediate Data Transfer #
citus.max_intermediate_result_size(integer) #The maximum size in KB of intermediate results for CTEs that are unable to be pushed down to worker nodes for execution, and for complex subqueries. The default is
1GB and a value of-1means no limit. Queries exceeding the limit will be canceled and produce an error message.
J.5.8.5.3.6. DDL #
citus.enable_ddl_propagation(boolean) #Specifies whether to automatically propagate DDL changes from the coordinator to all workers. The default value is
true. Because some schema changes require an access exclusive lock on tables and because the automatic propagation applies to all workers sequentially it can make a citus cluster temporarily less responsive. You may choose to disable this setting and propagate changes manually.Note
For a list of DDL propagation support, see the Modifying Tables section.
citus.enable_local_reference_table_foreign_keys(boolean) #Allows foreign keys to be created between reference and local tables. For the feature to work, the coordinator node must be registered with itself, using the citus_add_node function. The default value is
true.Note that foreign keys between reference tables and local tables come at a slight cost. When you create the foreign key, citus must add the plain table to its metadata and track it in the pg_dist_partition table. Local tables that are added to metadata inherit the same limitations as reference tables (see the Creating and Modifying Distributed Objects (DDL) and SQL Support and Workarounds sections).
If you drop the foreign keys, citus will automatically remove such local tables from metadata, which eliminates such limitations on those tables.
citus.enable_change_data_capture(boolean) #Causes citus to alter the wal2json and pgoutput logical decoders to work with distributed tables. Specifically, it rewrites the names of shards (e.g.
foo_102027) in decoder output to the base names of the distributed tables (e.g.foo). It also avoids publishing duplicate events during tenant isolation and shard split/move/rebalance operations. The default value isfalse.citus.enable_schema_based_sharding(boolean) #With the parameter set to
ONall created schemas will be distributed by default. Distributed schemas are automatically associated with individual co-location groups such that the tables created in those schemas will be automatically converted to co-located distributed tables without a shard key. This parameter can be modified for individual sessions.To learn how to use this configuration parameter, see the Microservices section.
J.5.8.5.3.7. Executor Configuration #
citus.all_modifications_commutative(boolean) #citus enforces commutativity rules and acquires appropriate locks for modify operations in order to guarantee correctness of behavior. For example, it assumes that an
INSERTstatement commutes with anotherINSERTstatement, but not with anUPDATEorDELETEstatement. Similarly, it assumes that anUPDATEorDELETEstatement does not commute with anotherUPDATEorDELETEstatement. This means thatUPDATEandDELETEstatements require citus to acquire stronger locks.If you have
UPDATEstatements that are commutative with yourINSERTs or otherUPDATEs, then you can relax these commutativity assumptions by setting this parameter totrue. When this parameter is set totrue, all commands are considered commutative and claim a shared lock, which can improve overall throughput. This parameter can be set at runtime and is effective on the coordinator.citus.multi_task_query_log_level(enum) #Sets a log-level for any query which generates more than one task (i.e. which hits more than one shard). This is useful during a multi-tenant application migration, as you can choose to error or warn for such queries, to find them and add the
tenant_idfilter to them. This parameter can be set at runtime and is effective on the coordinator. The default value for this parameter isoff. The following values are supported:off— turns off logging any queries, which generate multiple tasks (i.e. span multiple shards).debug— logs statement at theDEBUGseverity level.log— logs statement at theLOGseverity level. The log line will include the SQL query that was run.notice— logs statement at theNOTICEseverity level.warning— logs statement at theWARNINGseverity level.error— logs statement at theERRORseverity level.
Note that it may be useful to use
errorduring development testing and a lower log-level likelogduring actual production deployment. Choosinglogwill cause multi-task queries to appear in the database logs with the query itself shown afterSTATEMENT.LOG: multi-task query about to be executed HINT: Queries are split to multiple tasks if they have to be split into several queries on the workers. STATEMENT: SELECT * FROM foo;
citus.propagate_set_commands(enum) #Determines which
SETcommands are propagated from the coordinator to workers. The default value isnone. The following values are supported:none— noSETcommands are propagated.local— onlySET LOCALcommands are propagated.
citus.enable_repartition_joins(boolean) #Ordinarily, attempting to perform repartition joins with the adaptive executor will fail with an error message. However, setting this configuration parameter to
trueallows citus to perform the join. The default value isfalse.citus.enable_repartitioned_insert_select(boolean) #By default, an
INSERT INTO … SELECTstatement that cannot be pushed down will attempt to repartition rows from theSELECTstatement and transfer them between workers for insertion. However, if the target table has too many shards then repartitioning will probably not perform well. The overhead of processing the shard intervals when determining how to partition the results is too great. Repartitioning can be disabled manually by setting this configuration parameter tofalse.citus.enable_binary_protocol(boolean) #Setting this parameter to
trueinstructs the coordinator node to use Postgres Pro binary serialization format (when applicable) to transfer data with workers. Some column types do not support binary serialization.Enabling this parameter is mostly useful when the workers must return large amounts of data. Examples are when a lot of rows are requested, the rows have many columns, or they use big types such as
hlltype from the hll extension.The default value is
true. When set tofalse, all results are encoded and transferred in text format.citus.max_shared_pool_size(integer) #Specifies the maximum number of connections that the coordinator node, across all simultaneous sessions, is allowed to make per worker node. Postgres Pro must allocate fixed resources for every connection and this configuration parameter helps ease connection pressure on workers.
Without connection throttling, every multi-shard query creates connections on each worker proportional to the number of shards it accesses (in particular, up to
#shards/#workers). Running dozens of multi-shard queries at once can easily hit worker nodes' max_connections limit, causing queries to fail.By default, the value is automatically set equal to the coordinator's own
max_connections, which is not guaranteed to match that of the workers (see the note below). The value-1disables throttling.Note
There are certain operations that do not obey this parameter, most importantly repartition joins. That is why it can be prudent to increase the
max_connectionson the workers a bit higher thanmax_connectionson the coordinator. This gives extra space for connections required for repartition queries on the workers.citus.max_adaptive_executor_pool_size(integer) #Whereas citus.max_shared_pool_size limits worker connections across all sessions, the
citus.max_adaptive_executor_pool_sizelimits worker connections from just the current session. This parameter is useful for:Preventing a single backend from getting all the worker resources.
Providing priority management: designate low priority sessions with low
citus.max_adaptive_executor_pool_sizevalue and high priority sessions with higher values.
The default value is
16.citus.executor_slow_start_interval(integer) #Time to wait between opening connections to the same worker node, in milliseconds.
When the individual tasks of a multi-shard query take very little time, they can often be finished over a single (often already cached) connection. To avoid redundantly opening additional connections, the executor waits between connection attempts for the configured number of milliseconds. At the end of the interval, it increases the number of connections it is allowed to open next time.
For long queries (those taking >
500ms), slow start might add latency, but for short queries it is faster. The default value is10ms.citus.max_cached_conns_per_worker(integer) #Each backend opens connections to the workers to query the shards. At the end of the transaction, the configured number of connections is kept open to speed up subsequent commands. Increasing this value will reduce the latency of multi-shard queries but will also increase overhead on the workers.
The default value is
1. A larger value such as2might be helpful for clusters that use a small number of concurrent sessions, but it's not wise to go much further (e.g.16would be too high).citus.force_max_query_parallelization(boolean) #Simulates the deprecated and now nonexistent real-time executor. This is used to open as many connections as possible to maximize query parallelization.
When this configuration parameter is enabled, citus will force the adaptive executor to use as many connections as possible while executing a parallel distributed query. If not enabled, the executor might choose to use fewer connections to optimize overall query execution throughput. Internally, setting this parameter to
truewill end up using one connection per task. The default value isfalse.One place where this is useful is in a transaction whose first query is lightweight and requires few connections, while a subsequent query would benefit from more connections. citus decides how many connections to use in a transaction based on the first statement, which can throttle other queries unless we use the configuration parameter to provide a hint.
The example below shows how to set this parameter:
BEGIN; -- Add this hint SET citus.force_max_query_parallelization TO ON; -- A lightweight query that doesn't require many connections SELECT count(*) FROM table WHERE filter = x; -- A query that benefits from more connections, and can obtain -- them since we forced max parallelization above SELECT ... very .. complex .. SQL; COMMIT;
citus.explain_all_tasks(boolean) #By default, citus shows the output of a single arbitrary task when running the
EXPLAINcommand on a distributed query. In most cases, theEXPLAINoutput will be similar across tasks. Occasionally, some of the tasks will be planned differently or have much higher execution times. In those cases, it can be useful to enable this parameter, after which theEXPLAINoutput will include all tasks. This may cause theEXPLAINto take longer.citus.explain_analyze_sort_method(enum) #Determines the sort method of the tasks in the output of
EXPLAIN ANALYZE. The following values are supported:execution-time— sort by execution time.taskId— sort by task ID.
J.5.9. Administer #
J.5.9.1. Cluster Management #
In this section, we discuss how you can add or remove nodes from your citus cluster and how you can deal with node failures.
Note
To make moving shards across nodes or re-replicating shards on failed nodes easier, citus supports fully online shard rebalancing. We discuss briefly the functions provided by the shard rebalancer when relevant in the sections below. You can learn more about these functions, their arguments, and usage in the Cluster Management And Repair Functions section.
J.5.9.1.1. Choosing Cluster Size #
This section explores configuration settings for running a cluster in production.
J.5.9.1.1.1. Shard Count #
Choosing the shard count for each distributed table is a balance between the flexibility of having more shards and the overhead for query planning and execution across them. If you decide to change the shard count of a table after distributing, you can use the alter_distributed_table function.
Multi-Tenant SaaS Use Case #
The optimal choice varies depending on your access patterns for the data. For instance, in the multi-tenant SaaS database use case we recommend choosing between 32 and 128 shards. For smaller workloads, say <100GB, you could start with 32 shards and for larger workloads you could choose 64 or 128 shards. This means that you have the leeway to scale from 32 to 128 worker machines.
Real-Time Analytics Use Case #
In the real-time analytics use case, shard count should be related to the total number of cores on the workers. To ensure maximum parallelism, you should create enough shards on each node such that there is at least one shard per CPU core. We typically recommend creating a high number of initial shards, e.g. 2x or 4x the number of current CPU correspond. This allows for future scaling if you add more workers and CPU cores.
However, keep in mind that for each query citus opens one database connection per shard, and these connections are limited. Be careful to keep the shard count small enough that distributed queries will not often have to wait for a connection. Put another way, the connections needed, (max concurrent queries * shard count), should generally not exceed the total connections possible in the system, (number of workers * max_connections per worker).
J.5.9.1.2. Initial Hardware Size #
The size of a cluster, in terms of number of nodes and their hardware capacity, is easy to change. However, you still need to choose an initial size for a new cluster. Here are some tips for a reasonable initial cluster size.
J.5.9.1.2.1. Multi-Tenant SaaS Use Case #
For those migrating to citus from an existing single-node database instance, we recommend choosing a cluster where the number of worker cores and RAM in total equals that of the original instance. In such scenarios we have seen 2-3x performance improvements because sharding improves resource utilization, allowing smaller indices, etc.
The coordinator node needs less memory than workers, so you can choose a compute-optimized machine for running the coordinator. The number of cores required depends on your existing workload (write/read throughput).
J.5.9.1.2.2. Real-Time Analytics Use Case #
Total cores: when working data fits in RAM, you can expect a linear performance improvement on citus proportional to the number of worker cores. To determine the right number of cores for your needs, consider the current latency for queries in your single-node database and the required latency in citus. Divide current latency by desired latency, and round the result.
Worker RAM: the best case would be providing enough memory that the majority of the working set fits in memory. The type of queries your application uses affect memory requirements. You can run EXPLAIN ANALYZE on a query to determine how much memory it requires.
J.5.9.1.3. Scaling the Cluster #
citus logical sharding based architecture allows you to scale out your cluster without any downtime. This section describes how you can add more nodes to your citus cluster in order to improve query performance / scalability.
J.5.9.1.3.1. Adding a Worker #
citus stores all the data for distributed tables on the worker nodes. Hence, if you want to scale out your cluster by adding more computing power, you can do so by adding a worker.
To add a new node to the cluster, you first need to add the DNS name or IP address of that node and port (on which Postgres Pro is running) in the pg_dist_node catalog table. You can do so using the citus_add_node function. Example:
SELECT * from citus_add_node('node-name', 5432);
The new node is available for shards of new distributed tables. Existing shards will stay where they are unless redistributed, so adding a new worker may not help performance without further steps.
Note
Also, new nodes synchronize citus metadata upon creation. By default, the sync happens inside a single transaction for consistency. However, in a big cluster with large amounts of metadata, the transaction can run out of memory and fail. If you encounter this situation, you can choose a non-transactional metadata sync mode with the citus.metadata_sync_mode configuration parameter.
J.5.9.1.3.2. Rebalancing Shards Without Downtime #
If you want to move existing shards to a newly added worker, citus provides the citus_rebalance_start function to make it easier. This function will distribute shards evenly among the workers.
The function is configurable to rebalance shards according to a number of strategies, to best match your database workload. See the function reference to learn which strategy to choose. Here is an example of rebalancing shards using the default strategy:
SELECT citus_rebalance_start();
Many products like multi-tenant SaaS applications cannot tolerate downtime, and rebalancing is able to honor this requirement. This means reads and writes from the application can continue with minimal interruption while data is being moved.
Parallel Rebalancing #
This operation carries out multiple shard moves in a sequential order by default. There are some cases where you may prefer to rebalance faster at the expense of using more resources such as network bandwidth. In those situations, customers are able to configure a rebalance operation to perform a number of shard moves in parallel.
The citus.max_background_task_executors_per_node configuration parameter allows tasks such as shard rebalancing to operate in parallel. You can increase it from its default value of 1 as desired to boost parallelism.
ALTER SYSTEM SET citus.max_background_task_executors_per_node = 2; SELECT pg_reload_conf(); SELECT citus_rebalance_start();
What are the typical use cases?
Scaling out faster when adding new nodes to the cluster.
Rebalancing the cluster faster to even out the utilization of nodes.
Corner Cases and Gotchas
The citus.max_background_task_executors_per_node configuration parameter limits the number of parallel task executors in general. Also, shards in the same colocation group will always move sequentially so parallelism may be limited by the number of colocation groups.
How it Works #
citus shard rebalancing uses Postgres Pro logical replication to move data from the old shard (called the “publisher” in replication terms) to the new (the “subscriber”). Logical replication allows application reads and writes to continue uninterrupted while copying shard data. citus puts a brief write-lock on a shard only during the time it takes to update metadata to promote the subscriber shard as active.
As the Postgres Pro documentation explains, the source needs a replica identity configured:
A published table must have a “replica identity” configured in order to be able to replicate UPDATE and DELETE operations, so that appropriate rows to update or delete can be identified on the subscriber side. By default, this is the primary key, if there is one. Another unique index (with certain additional requirements) can also be set to be the replica identity.
In other words, if your distributed table has a primary key defined then it is ready for shard rebalancing with no extra work. However, if it does not have a primary key or an explicitly defined replica identity, then attempting to rebalance it will cause an error. Here is how to fix it.
First, does the table have a unique index?
If the table to be replicated already has a unique index, which includes the distribution column, then choose that index as a replica identity:
-- Supposing my_table has unique index my_table_idx -- which includes distribution column ALTER TABLE my_table REPLICA IDENTITY USING INDEX my_table_idx;
Note
While REPLICA IDENTITY USING INDEX is fine, we recommend against adding REPLICA IDENTITY FULL to a table. This setting would result in each UPDATE/ DELETE doing a full-table-scan on the subscriber side to find the tuple with those rows. In our testing we have found this to result in worse performance than even solution four below.
Otherwise, can you add a primary key?
Add a primary key to the table. If the desired key happens to be the distribution column, then it's quite easy, just add the constraint. Otherwise, a primary key with a non-distribution column must be composite and contain the distribution column too.
J.5.9.1.3.3. Adding a Coordinator #
The citus coordinator only stores metadata about the table shards and does not store any data. This means that all the computation is pushed down to the workers and the coordinator does only final aggregations on the result of the workers. Therefore, it is not very likely that the coordinator becomes a bottleneck for read performance. Also, it is easy to boost up the coordinator by shifting to a more powerful machine.
However, in some write-heavy use cases where the coordinator becomes a performance bottleneck, you can add another node as described below and load balance the client connections.
SELECT * FROM citus_add_node(second_coordinator_hostname, second_coordinator_port); SELECT * FROM citus_set_node_property(second_coordinator_hostname, second_coordinator_port, 'shouldhaveshards', false);
Note
DDL queries can only be run though the first coordinator node.
J.5.9.1.4. Dealing With Node Failures #
In this subsection, we discuss how you can deal with node failures without incurring any downtime on your citus cluster.
J.5.9.1.4.1. Worker Node Failures #
citus uses Postgres Pro streaming replication, allowing it to tolerate worker-node failures. This option replicates entire worker nodes by continuously streaming their WAL records to a standby. You can configure streaming replication on-premise yourself by consulting the Streaming Replication section.
J.5.9.1.4.2. Coordinator Node Failures #
The citus coordinator maintains metadata tables to track all of the cluster nodes and the locations of the database shards on those nodes. The metadata tables are small (typically a few MBs in size) and do not change very often. This means that they can be replicated and quickly restored if the node ever experiences a failure. There are several options on how users can deal with coordinator failures.
Use Postgres Pro streaming replication. You can use Postgres Pro streaming replication feature to create a hot standby of the coordinator. Then, if the primary coordinator node fails, the standby can be promoted to the primary automatically to serve queries to your cluster. For details on setting this up, please refer to the Streaming Replication section.
Use backup tools. Since the metadata tables are small, users can use EBS volumes, or Postgres Pro backup tools to backup the metadata. Then, they can easily copy over that metadata to new nodes to resume operation.
J.5.9.1.5. Tenant Isolation #
J.5.9.1.5.1. Row-Based Sharding #
citus places table rows into worker shards based on the hashed value of the rows' distribution column. Multiple distribution column values often fall into the same shard. In the citus multi-tenant use case this means that tenants often share shards.
However, sharing shards can cause resource contention when tenants differ drastically in size. This is a common situation for systems with a large number of tenants — we have observed that the size of tenant data tend to follow a Zipfian distribution as the number of tenants increases. This means there are a few very large tenants, and many smaller ones. To improve resource allocation and make guarantees of tenant QoS it is worthwhile to move large tenants to dedicated nodes.
citus provides the tools to isolate a tenant on a specific node. This happens in two phases: firstly, isolating the tenant's data to a new dedicated shard, then moving the shard to the desired node. To understand the process, it helps to know precisely how rows of data are assigned to shards.
Every shard is marked in citus metadata with the range of hashed values it contains (more info in the reference for the pg_dist_shard table). The isolate_tenant_to_new_shard function moves a tenant into a dedicated shard in three steps:
Creates a new shard for
table_name, which includes rows whose distribution column has valuetenant_idand excludes all other rows.Moves the relevant rows from their current shard to the new shard.
Splits the old shard into two with hash ranges that abut the excision above and below.
Furthermore, the function takes the CASCADE option, which isolates the tenant rows of not just table_name but of all tables co-located with it. Here is an example:
-- This query creates an isolated shard for the given tenant_id and
-- returns the new shard id.
-- General form:
SELECT isolate_tenant_to_new_shard('table_name', tenant_id);
-- Specific example:
SELECT isolate_tenant_to_new_shard('lineitem', 135);
-- If the given table has co-located tables, the query above errors out and
-- advises to use the CASCADE option
SELECT isolate_tenant_to_new_shard('lineitem', 135, 'CASCADE');
Output:
┌─────────────────────────────┐ │ isolate_tenant_to_new_shard │ ├─────────────────────────────┤ │ 102240 │ └─────────────────────────────┘
The new shard(s) are created on the same node as the shard(s) from which the tenant was removed. For true hardware isolation they can be moved to a separate node in the citus cluster. As mentioned, the isolate_tenant_to_new_shard function returns the newly created shard ID, and this ID can be used to move the shard:
J.5.9.1.5.2. Schema-Based Sharding #
In schema-based sharding, the act of isolating a tenant is not required as by definition each tenant already resides in its own schema. The only thing that is needed is obtaining a shard identifier for a schema to perform a move.
First find the colocation ID of the schema you want to move.
SELECT * FROM citus_schemas;
schema_name | colocation_id | schema_size | schema_owner --------------+---------------+-------------+-------------- user_service | 1 | 0 bytes | user_service time_service | 2 | 0 bytes | time_service ping_service | 3 | 0 bytes | ping_service a | 4 | 128 kB | citus b | 5 | 32 kB | citus with_data | 11 | 6408 kB | citus (6 rows)
The next step is to query citus_shards, we will use co-location identifier 11 from the output above:
SELECT * FROM citus_shards where colocation_id = 11;
table_name | shardid | shard_name | citus_table_type | colocation_id | nodename | nodeport | shard_size -----------------+---------+------------------------+------------------+---------------+-----------+----------+------------ with_data.test | 102180 | with_data.test_102180 | schema | 11 | localhost | 9702 | 647168 with_data.test2 | 102183 | with_data.test2_102183 | schema | 11 | localhost | 9702 | 5914624 (2 rows)
You can pick any shardid from the output as making the move will also propagate to all co-located tables, which in case of schema-based sharding means moving all tables within the schema.
J.5.9.1.5.3. Make the Move #
Knowing the shard ID that denotes the tenant, you can execute the move:
-- Find the node currently holding the new shard SELECT nodename, nodeport FROM citus_shards WHERE shardid = 102240; -- List the available worker nodes that could hold the shard SELECT * FROM master_get_active_worker_nodes(); -- Move the shard to your choice of worker -- (it will also move any shards created with the CASCADE option) SELECT citus_move_shard_placement( 102240, 'source_host', source_port, 'dest_host', dest_port);
Note that the citus_move_shard_placement function will also move any shards which are co-located with the specified one, to preserve their co-location.
J.5.9.1.6. Viewing Query Statistics #
When administering a citus cluster it is useful to know what queries users are running, which nodes are involved, and which execution method citus is using for each query. The extension records query statistics in a metadata view called citus_stat_statements, named analogously to Postgres Pro pg_stat_statements. Whereas pg_stat_statements stores info about query duration and I/O, citus_stat_statements stores info about citus execution methods and shard partition keys (when applicable).
citus requires the pg_stat_statements extension to be installed in order to track query statistics. On a self-hosted Postgres Pro instance load the extension in postgresql.conf via shared_preload_libraries, then create the extension in SQL:
CREATE EXTENSION pg_stat_statements;
Let's see how this works. Assume we have a table called foo that is hash-distributed by its id column.
-- Create and populate distributed table
CREATE TABLE foo ( id int );
SELECT create_distributed_table('foo', 'id');
INSERT INTO foo SELECT generate_series(1,100);
We will run two more queries and citus_stat_statements will show how citus chooses to execute them.
-- Counting all rows executes on all nodes, and sums -- the results on the coordinator SELECT count(*) FROM foo; -- Specifying a row by the distribution column routes -- execution to an individual node SELECT * FROM foo WHERE id = 42;
To find how these queries were executed, ask the stats table:
SELECT * FROM citus_stat_statements;
Results:
-[ RECORD 1 ]-+---------------------------------------------- queryid | -6844578505338488014 userid | 10 dbid | 13340 query | SELECT count(*) FROM foo; executor | adaptive partition_key | calls | 1 -[ RECORD 2 ]-+---------------------------------------------- queryid | 185453597994293667 userid | 10 dbid | 13340 query | INSERT INTO foo SELECT generate_series($1,$2) executor | insert-select partition_key | calls | 1 -[ RECORD 3 ]-+---------------------------------------------- queryid | 1301170733886649828 userid | 10 dbid | 13340 query | SELECT * FROM foo WHERE id = $1 executor | adaptive partition_key | 42 calls | 1
We can see that citus uses the adaptive executor most commonly to run queries. This executor fragments the query into constituent queries to run on relevant nodes and combines the results on the coordinator node. In the case of the second query (filtering by the distribution column id = $1), citus determined that it needed the data from just one node. Lastly, we can see that the INSERT INTO foo SELECT… statement ran with the insert-select executor that provides flexibility to run these kind of queries.
J.5.9.1.6.1. Tenant-Level Statistics #
So far the information in this view does not give us anything we could not already learn by running the EXPLAIN command for a given query. However, in addition to getting information about individual queries, the citus_stat_statements view allows us to answer questions such as “what percentage of queries in the cluster are scoped to a single tenant?”
SELECT sum(calls),
partition_key IS NOT NULL AS single_tenant
FROM citus_stat_statements
GROUP BY 2;
. sum | single_tenant -----+--------------- 2 | f 1 | t
In a multi-tenant database, for instance, we would expect the vast majority of queries to be single tenant. Seeing too many multi-tenant queries may indicate that queries do not have the proper filters to match a tenant, and are using unnecessary resources.
To investigate which tenants in particular are most active, you can use the citus_stat_tenants view.
J.5.9.1.6.2. Statistics Expiration #
The pg_stat_statements view limits the number of statements it tracks and the duration of its records. Because the citus_stat_statements table tracks a strict subset of the queries in pg_stat_statements, a choice of equal limits for the two views would cause a mismatch in their data retention. Mismatched records can cause joins between the views to behave unpredictably.
There are three ways to help synchronize the views, and all three can be used together.
Have the maintenance daemon periodically sync the citus and Postgres Pro statistics. The citus.stat_statements_purge_interval configuration parameter sets time in seconds for the sync. A value of
0disables periodic syncs.Adjust the number of entries in
citus_stat_statements. The citus.stat_statements_max configuration parameter removes old entries when new ones cross the threshold. The default value is50000, and the highest allowable value is10000000. Note that each entry costs about 140 bytes in shared memory so set the value wisely.Increase pg_stat_statements.max. Its default value is
5000and could be increased to10000,20000or even50000without much overhead. This is most beneficial when there is more local (i.e. coordinator) query workload.
Note
Changing pg_stat_statements.max or citus.stat_statements_max requires restarting the Postgres Pro service. Changing citus.stat_statements_purge_interval, on the other hand, will come into effect with a call to the pg_reload_conf function.
J.5.9.1.7. Resource Conservation #
J.5.9.1.7.1. Limiting Long-Running Queries #
Long running queries can hold locks, queue up WAL, or just consume a lot of system resources, so in a production environment it is good to prevent them from running too long. You can set the statement_timeout parameter on the coordinator and workers to cancel queries that run too long.
-- Limit queries to five minutes
ALTER DATABASE citus
SET statement_timeout TO 300000;
SELECT run_command_on_workers($cmd$
ALTER DATABASE citus
SET statement_timeout TO 300000;
$cmd$);
The timeout is specified in milliseconds.
To customize the timeout per query, use SET LOCAL in a transaction:
BEGIN; -- this limit applies to just the current transaction SET LOCAL statement_timeout TO 300000; -- ... COMMIT;
J.5.9.1.8. Security #
J.5.9.1.8.1. Connection Management #
Note
The traffic between the different nodes in the cluster is encrypted for new installations. This is done by using TLS with self-signed certificates. This means that this does not protect against man-in-the-middle attacks. This only protects against passive eavesdropping on the network.
Clusters originally created with citus do not have any network encryption enabled between nodes (even if upgraded later). To set up self-signed TLS on this type of installation follow the steps in the Creating Certificates section together with the citus specific settings described here, i.e. changing the citus.node_conninfo parameter to sslmode=require. This setup should be done on the coordinator and workers.
When citus nodes communicate with one another they consult a table with connection credentials. This gives the database administrator flexibility to adjust parameters for security and efficiency.
To set non-sensitive libpq connection parameters to be used for all node connections, update the citus.node_conninfo configuration parameter:
-- key=value pairs separated by spaces. -- For example, ssl options: ALTER SYSTEM SET citus.node_conninfo = 'sslrootcert=/path/to/citus-ca.crt sslcrl=/path/to/citus-ca.crl sslmode=verify-full';
There is a whitelist of options that the citus.node_conninfo configuration parameter accepts. The default value is sslmode=require, which prevents unencrypted communication between nodes. If your cluster was originally created with citus, the value will be sslmode=prefer. After setting up self-signed certificates on all nodes it is recommended to change this setting to sslmode=require.
After changing this setting it is important to reload the Postgres Pro configuration. Even though the changed setting might be visible in all sessions, the setting is only consulted by citus when new connections are established. When a reload signal is received, citus marks all existing connections to be closed which causes a reconnect after running transactions have been completed.
SELECT pg_reload_conf();
-- Only superusers can access this table -- Add a password for user jdoe INSERT INTO pg_dist_authinfo (nodeid, rolename, authinfo) VALUES (123, 'jdoe', 'password=abc123');
After this INSERT, any query needing to connect to node 123 as the user jdoe will use the supplied password. To learn more, see the section about the pg_dist_authinfo table.
-- Update user jdoe to use certificate authentication UPDATE pg_dist_authinfo SET authinfo = 'sslcert=/path/to/user.crt sslkey=/path/to/user.key' WHERE nodeid = 123 AND rolename = 'jdoe';
This changes the user from using a password to use a certificate and keyfile while connecting to node 123 instead. Make sure the user certificate is signed by a certificate that is trusted by the worker you are connecting to and authentication settings on the worker allow for certificate based authentication. Full documentation on how to use client certificates can be found in the Client Certificates section.
Changing the pg_dist_authinfo table does not force any existing connection to reconnect.
J.5.9.1.8.2. Setup Certificate Authority Signed Certificates #
This section assumes you have a trusted Certificate Authority that can issue server certificates to you for all nodes in your cluster. It is recommended to work with the security department in your organization to prevent key material from being handled incorrectly. This guide covers only citus specific configuration that needs to be applied, not best practices for PKI management.
For all nodes in the cluster you need to get a valid certificate signed by the same Certificate Authority. The following machine-specific files are assumed to be available on every machine:
/path/to/server.key— Server Private Key/path/to/server.crt— Server Certificate or Certificate Chain for Server Key, signed by trusted Certificate Authority
Next to these machine-specific files you need these cluster or Certificate Authority wide files available:
/path/to/ca.crt— Certificate of the Certificate Authority/path/to/ca.crl— Certificate Revocation List of the Certificate Authority
Note
The Certificate Revocation List is likely to change over time. Work with your security department to set up a mechanism to update the revocation list on to all nodes in the cluster in a timely manner. A reload of every node in the cluster is required after the revocation list has been updated.
Once all files are in place on the nodes, the following settings need to be configured in the Postgres configuration file:
# The following settings allow the postgres server to enable ssl, and # configure the server to present the certificate to clients when # connecting over tls/ssl ssl = on ssl_key_file = '/path/to/server.key' ssl_cert_file = '/path/to/server.crt' # This will tell citus to verify the certificate of the server it is connecting to citus.node_conninfo = 'sslmode=verify-full sslrootcert=/path/to/ca.crt sslcrl=/path/to/ca.crl'
After changing, reload the configuration to apply these changes. Also, adjusting citus.local_hostname may be required for proper functioning with sslmode=verify-full.
Depending on the policy of the Certificate Authority used you might need or want to change sslmode=verify-full in citus.node_conninfo to sslmode=verify-ca. For the difference between the two settings, consult the SSL Mode Descriptions section.
Lastly, to prevent any user from connecting via an un-encrypted connection, changes need to be made to pg_hba.conf. Many Postgres Pro installations will have entries allowing host connections which allow SSL/TLS connections as well as plain TCP connections. By replacing all host entries with hostssl entries, only encrypted connections will be allowed to authenticate to Postgres Pro. For full documentation on these settings take a look at the section about the pg_hba.conf file.
Note
When a trusted Certificate Authority is not available, one can create their own via a self-signed root certificate. This is non-trivial and the developer or operator should seek guidance from their security team when doing so.
To verify the connections from the coordinator to the workers are encrypted you can run the following query. It will show the SSL/TLS version used to encrypt the connection that the coordinator uses to talk to the worker:
SELECT run_command_on_workers($$ SELECT version FROM pg_stat_ssl WHERE pid = pg_backend_pid() $$);
┌────────────────────────────┐ │ run_command_on_workers │ ├────────────────────────────┤ │ (localhost,9701,t,TLSv1.2) │ │ (localhost,9702,t,TLSv1.2) │ └────────────────────────────┘ (2 rows)
J.5.9.1.8.3. Increasing Worker Security #
For your convenience getting started, our multi-node installation instructions direct you to set up the pg_hba.conf on the workers with its authentication method set to trust for local network connections. However, you might desire more security.
To require that all connections supply a hashed password, update the Postgres Pro pg_hba.conf on every worker node with something like this:
# Require password access and a ssl/tls connection to nodes in the local # network. The following ranges correspond to 24, 20, and 16-bit blocks # in Private IPv4 address spaces. hostssl all all 10.0.0.0/8 md5 # Require passwords and ssl/tls connections when the host connects to # itself as well. hostssl all all 127.0.0.1/32 md5 hostssl all all ::1/128 md5
The coordinator node needs to know roles' passwords in order to communicate with the workers. In citus the authentication information has to be maintained in the .pgpass file. Edit the file in the Postgres Pro user home directory, with a line for each combination of worker address and role:
hostname:port:database:username:password
Sometimes workers need to connect to one another, such as during repartition joins. Thus each worker node requires a copy of the .pgpass file as well.
J.5.9.1.8.4. Row-Level Security #
Postgres Pro row-level security policies restrict, on a per-user basis, which rows can be returned by normal queries or inserted, updated, or deleted by data modification commands. This can be especially useful in a multi-tenant citus cluster because it allows individual tenants to have full SQL access to the database while hiding each tenant's information from other tenants.
We can implement the separation of tenant data by using a naming convention for database roles that ties into table row-level security policies. We will assign each tenant a database role in a numbered sequence: tenant_1, tenant_2, etc. Tenants will connect to citus using these separate roles. Row-level security policies can compare the role name to values in the tenant_id distribution column to decide whether to allow access.
Here is how to apply the approach on a simplified events table distributed by tenant_id. First create the roles tenant_1 and tenant_2. Then run the following as an administrator:
CREATE TABLE events(
tenant_id int,
id int,
type text
);
SELECT create_distributed_table('events','tenant_id');
INSERT INTO events VALUES (1,1,'foo'), (2,2,'bar');
-- Assumes that roles tenant_1 and tenant_2 exist
GRANT select, update, insert, delete
ON events TO tenant_1, tenant_2;
As it stands, anyone with SELECT permissions for this table can see both rows. Users from either tenant can see and update the row of the other tenant. We can solve this with row-level table security policies.
Each policy consists of two clauses: USING and WITH CHECK. When a user tries to read or write rows, the database evaluates each row against these clauses. Existing table rows are checked against the expression specified in USING, while new rows that would be created via INSERT or UPDATE are checked against the expression specified in WITH CHECK.
-- First a policy for the system admin "citus" user CREATE POLICY admin_all ON events TO citus -- apply to this role USING (true) -- read any existing row WITH CHECK (true); -- insert or update any row -- Next a policy which allows role "tenant_<n>" to -- access rows where tenant_id = <n> CREATE POLICY user_mod ON events USING (current_user = 'tenant_' || tenant_id::text); -- Lack of CHECK means same condition as USING -- Enforce the policies ALTER TABLE events ENABLE ROW LEVEL SECURITY;
Now roles tenant_1 and tenant_2 get different results for their queries:
Connected as tenant_1:
SELECT * FROM events;
┌───────────┬────┬──────┐ │ tenant_id │ id │ type │ ├───────────┼────┼──────┤ │ 1 │ 1 │ foo │ └───────────┴────┴──────┘
Connected as tenant_2:
SELECT * FROM events;
┌───────────┬────┬──────┐ │ tenant_id │ id │ type │ ├───────────┼────┼──────┤ │ 2 │ 2 │ bar │ └───────────┴────┴──────┘
INSERT INTO events VALUES (3,3,'surprise'); /* ERROR: new row violates row-level security policy for table "events_102055" */
J.5.9.1.9. Postgres Pro extensions #
citus provides distributed functionality by extending Postgres Pro using the hook and extension APIs. This allows users to benefit from the features that come with the rich Postgres Pro ecosystem. These features include, but are not limited to, support for a wide range of data types (including semi-structured data types like jsonb and hstore), operators and functions, full text search, and other extensions such as PostGIS and HyperLogLog. Further, proper use of the extension APIs enable compatibility with standard Postgres Pro tools such as pgAdmin and pg_upgrade.
As citus is an extension which can be installed on any Postgres Pro instance, you can directly use other extensions such as hstore, hll, or PostGIS with citus. However, there is one thing to keep in mind. While including other extensions in shared_preload_libraries, you should make sure that citus is the first extension.
There are several extensions, which may be useful when working with citus:
cstore_fdw — columnar store for analytics. The columnar nature delivers performance by reading only relevant data from disk, and it may compress data 6x-10x to reduce space requirements for data archival.
pg_cron — run periodic jobs directly from the database.
topn — returns the top values in a database according to some criteria. Uses an approximation algorithm to provide fast results with modest compute and memory resources.
hll — HyperLogLog data structure as a native data type. It is a fixed-size, set-like structure used for distinct value counting with tunable precision.
J.5.9.1.10. Creating a New Database #
Each Postgres Pro server can hold multiple databases. However, new databases do not inherit the extensions of any others; all desired extensions must be added afresh. To run citus on a new database, you will need to create the database on the coordinator and workers, create the citus extension within that database, and register the workers in the coordinator database.
Connect to each of the worker nodes and run:
-- On every worker node CREATE DATABASE newbie; \c newbie CREATE EXTENSION citus;
Then, on the coordinator:
CREATE DATABASE newbie;
\c newbie
CREATE EXTENSION citus;
SELECT * from citus_add_node('node-name', 5432);
SELECT * from citus_add_node('node-name2', 5432);
-- ... for all of them
Now the new database will be operating as another citus cluster.
J.5.9.2. Table Management #
J.5.9.2.1. Determining Table and Relation Size #
The usual way to find table sizes in Postgres Pro, pg_total_relation_size, drastically under-reports the size of distributed tables. All this function does on a citus cluster is reveal the size of tables on the coordinator node. In reality the data in distributed tables lives on the worker nodes (in shards), not on the coordinator. A true measure of distributed table size is obtained as a sum of shard sizes. citus provides helper functions to query this information.
| Function | Returns |
|---|---|
| citus_relation_size |
|
| citus_table_size |
|
| citus_total_relation_size |
|
These functions are analogous to three of the standard Postgres Pro object size functions, with the additional note that if they cannot connect to a node, they error out.
Here is an example of using one of the helper functions to list the sizes of all distributed tables:
SELECT logicalrelid AS name,
pg_size_pretty(citus_table_size(logicalrelid)) AS size
FROM pg_dist_partition;
Output:
┌───────────────┬───────┐ │ name │ size │ ├───────────────┼───────┤ │ github_users │ 39 MB │ │ github_events │ 37 MB │ └───────────────┴───────┘
J.5.9.2.2. Vacuuming Distributed Tables #
In Postgres Pro (and other MVCC databases), an UPDATE or DELETE of a row does not immediately remove the old version of the row. The accumulation of outdated rows is called bloat and must be cleaned to avoid decreased query performance and unbounded growth of disk space requirements. Postgres Pro runs a process called the auto-vacuum daemon that periodically vacuums (removes) outdated rows.
It is not just user queries which scale in a distributed database, vacuuming does too. In Postgres Pro big busy tables have great potential to bloat, both from lower sensitivity to Postgres Pro vacuum scale factor parameter, and generally because of the extent of their row churn. Splitting a table into distributed shards means both that individual shards are smaller tables and that auto-vacuum workers can parallelize over different parts of the table on different machines. Ordinarily auto-vacuum can only run one worker per table.
Due to the above, auto-vacuum operations on a citus cluster are probably good enough for most cases. However, for tables with particular workloads, or companies with certain “safe” hours to schedule a vacuum, it might make more sense to manually vacuum a table rather than leaving all the work to auto-vacuum.
To vacuum a table, simply run this on the coordinator node:
VACUUM my_distributed_table;
Using vacuum against a distributed table will send the VACUUM command to every one of that table's placements (one connection per placement). This is done in parallel. All options are supported (including the table_and_columns list) except for VERBOSE. The VACUUM command also runs on the coordinator, and does so before any workers nodes are notified. Note that unqualified vacuum commands (i.e. those without a table specified) do not propagate to worker nodes.
J.5.9.2.3. Analyzing Distributed Tables #
Postgres Pro ANALYZE command collects statistics about the contents of tables in the database. Subsequently, the query planner uses these statistics to help determine the most efficient execution plans for queries.
The auto-vacuum daemon, discussed in the previous section, will automatically issue ANALYZE commands whenever the content of a table has changed sufficiently. The daemon schedules ANALYZE strictly as a function of the number of rows inserted or updated; it has no knowledge of whether that will lead to meaningful statistical changes. Administrators might prefer to manually schedule ANALYZE operations instead, to coincide with statistically meaningful table changes.
To analyze a table, run this on the coordinator node:
ANALYZE my_distributed_table;
citus propagates the ANALYZE command to all worker node placements.
J.5.9.2.4. Columnar Storage #
citus provides append-only columnar table storage for analytic and data warehousing workloads. When columns (rather than rows) are stored contiguously on disk, data becomes more compressible, and queries can request a subset of columns more quickly.
J.5.9.2.4.1. Usage #
To use columnar storage, specify USING columnar when creating a table:
CREATE TABLE contestant (
handle TEXT,
birthdate DATE,
rating INT,
percentile FLOAT,
country CHAR(3),
achievements TEXT[]
) USING columnar;
You can also convert between row-based (heap) and columnar storage.
-- Convert to row-based (heap) storage
SELECT alter_table_set_access_method('contestant', 'heap');
-- Convert to columnar storage (indexes will be dropped)
SELECT alter_table_set_access_method('contestant', 'columnar');
citus converts rows to columnar storage in “stripes” during insertion. Each stripe holds one transaction's worth of data, or 150000 rows, whichever is less. (The stripe size and other parameters of a columnar table can be changed with the alter_columnar_table_set function.)
For example, the following statement puts all five rows into the same stripe, because all values are inserted in a single transaction:
-- Insert these values into a single columnar stripe
INSERT INTO contestant VALUES
('a','1990-01-10',2090,97.1,'XA','{a}'),
('b','1990-11-01',2203,98.1,'XA','{a,b}'),
('c','1988-11-01',2907,99.4,'XB','{w,y}'),
('d','1985-05-05',2314,98.3,'XB','{}'),
('e','1995-05-05',2236,98.2,'XC','{a}');
It is best to make large stripes when possible, because citus compresses columnar data separately per stripe. We can see facts about our columnar table like compression rate, number of stripes, and average rows per stripe by using VACUUM VERBOSE:
VACUUM VERBOSE contestant;
INFO: statistics for "contestant": storage id: 10000000000 total file size: 24576, total data size: 248 compression rate: 1.31x total row count: 5, stripe count: 1, average rows per stripe: 5 chunk count: 6, containing data for dropped columns: 0, zstd compressed: 6
The output shows that citus used the zstd compression algorithm to obtain 1.31x data compression. The compression rate compares the size of inserted data as it was staged in memory against the size of that data compressed in its eventual stripe.
Because of how it is measured, the compression rate may or may not match the size difference between row and columnar storage for a table. The only way to truly find that difference is to construct a row and columnar table that contain the same data and compare.
J.5.9.2.4.2. Measuring Compression #
Let's create a new example with more data to benchmark the compression savings.
-- First a wide table using row storage CREATE TABLE perf_row( c00 int8, c01 int8, c02 int8, c03 int8, c04 int8, c05 int8, c06 int8, c07 int8, c08 int8, c09 int8, c10 int8, c11 int8, c12 int8, c13 int8, c14 int8, c15 int8, c16 int8, c17 int8, c18 int8, c19 int8, c20 int8, c21 int8, c22 int8, c23 int8, c24 int8, c25 int8, c26 int8, c27 int8, c28 int8, c29 int8, c30 int8, c31 int8, c32 int8, c33 int8, c34 int8, c35 int8, c36 int8, c37 int8, c38 int8, c39 int8, c40 int8, c41 int8, c42 int8, c43 int8, c44 int8, c45 int8, c46 int8, c47 int8, c48 int8, c49 int8, c50 int8, c51 int8, c52 int8, c53 int8, c54 int8, c55 int8, c56 int8, c57 int8, c58 int8, c59 int8, c60 int8, c61 int8, c62 int8, c63 int8, c64 int8, c65 int8, c66 int8, c67 int8, c68 int8, c69 int8, c70 int8, c71 int8, c72 int8, c73 int8, c74 int8, c75 int8, c76 int8, c77 int8, c78 int8, c79 int8, c80 int8, c81 int8, c82 int8, c83 int8, c84 int8, c85 int8, c86 int8, c87 int8, c88 int8, c89 int8, c90 int8, c91 int8, c92 int8, c93 int8, c94 int8, c95 int8, c96 int8, c97 int8, c98 int8, c99 int8 ); -- Next a table with identical columns using columnar storage CREATE TABLE perf_columnar(LIKE perf_row) USING COLUMNAR;
Fill both tables with the same large dataset:
INSERT INTO perf_row
SELECT
g % 00500, g % 01000, g % 01500, g % 02000, g % 02500, g % 03000, g % 03500, g % 04000, g % 04500, g % 05000,
g % 05500, g % 06000, g % 06500, g % 07000, g % 07500, g % 08000, g % 08500, g % 09000, g % 09500, g % 10000,
g % 10500, g % 11000, g % 11500, g % 12000, g % 12500, g % 13000, g % 13500, g % 14000, g % 14500, g % 15000,
g % 15500, g % 16000, g % 16500, g % 17000, g % 17500, g % 18000, g % 18500, g % 19000, g % 19500, g % 20000,
g % 20500, g % 21000, g % 21500, g % 22000, g % 22500, g % 23000, g % 23500, g % 24000, g % 24500, g % 25000,
g % 25500, g % 26000, g % 26500, g % 27000, g % 27500, g % 28000, g % 28500, g % 29000, g % 29500, g % 30000,
g % 30500, g % 31000, g % 31500, g % 32000, g % 32500, g % 33000, g % 33500, g % 34000, g % 34500, g % 35000,
g % 35500, g % 36000, g % 36500, g % 37000, g % 37500, g % 38000, g % 38500, g % 39000, g % 39500, g % 40000,
g % 40500, g % 41000, g % 41500, g % 42000, g % 42500, g % 43000, g % 43500, g % 44000, g % 44500, g % 45000,
g % 45500, g % 46000, g % 46500, g % 47000, g % 47500, g % 48000, g % 48500, g % 49000, g % 49500, g % 50000
FROM generate_series(1,50000000) g;
INSERT INTO perf_columnar
SELECT
g % 00500, g % 01000, g % 01500, g % 02000, g % 02500, g % 03000, g % 03500, g % 04000, g % 04500, g % 05000,
g % 05500, g % 06000, g % 06500, g % 07000, g % 07500, g % 08000, g % 08500, g % 09000, g % 09500, g % 10000,
g % 10500, g % 11000, g % 11500, g % 12000, g % 12500, g % 13000, g % 13500, g % 14000, g % 14500, g % 15000,
g % 15500, g % 16000, g % 16500, g % 17000, g % 17500, g % 18000, g % 18500, g % 19000, g % 19500, g % 20000,
g % 20500, g % 21000, g % 21500, g % 22000, g % 22500, g % 23000, g % 23500, g % 24000, g % 24500, g % 25000,
g % 25500, g % 26000, g % 26500, g % 27000, g % 27500, g % 28000, g % 28500, g % 29000, g % 29500, g % 30000,
g % 30500, g % 31000, g % 31500, g % 32000, g % 32500, g % 33000, g % 33500, g % 34000, g % 34500, g % 35000,
g % 35500, g % 36000, g % 36500, g % 37000, g % 37500, g % 38000, g % 38500, g % 39000, g % 39500, g % 40000,
g % 40500, g % 41000, g % 41500, g % 42000, g % 42500, g % 43000, g % 43500, g % 44000, g % 44500, g % 45000,
g % 45500, g % 46000, g % 46500, g % 47000, g % 47500, g % 48000, g % 48500, g % 49000, g % 49500, g % 50000
FROM generate_series(1,50000000) g;
VACUUM (FREEZE, ANALYZE) perf_row;
VACUUM (FREEZE, ANALYZE) perf_columnar;
For this data, you can see a compression ratio of better than 8X in the columnar table.
SELECT pg_total_relation_size('perf_row')::numeric/
pg_total_relation_size('perf_columnar') AS compression_ratio;
. compression_ratio -------------------- 8.0196135873627944 (1 row)
J.5.9.2.4.3. Example #
Columnar storage works well with table partitioning. For example, see the Archiving with Columnar Storage section.
J.5.9.2.4.4. Gotchas #
Columnar storage compresses per stripe. Stripes are created per transaction, so inserting one row per transaction will put single rows into their own stripes. Compression and performance of single row stripes will be worse than a row table. Always insert in bulk to a columnar table.
Even if you mess up and columnarize a bunch of tiny stripes, it is possible to repair it. Simply run
VACUUM (FULL)on the table like so:VACUUM (FULL) foo_table;
In some cases it might be more desirable to create a new table, move the data and drop the old one. You can do it like so:
BEGIN; CREATE TABLE foo_compacted (LIKE foo) USING columnar; INSERT INTO foo_compacted SELECT * FROM foo; DROP TABLE foo; ALTER TABLE foo_compacted RENAME TO foo; COMMIT;
Fundamentally non-compressible data can be a problem, although it can still be useful to use columnar so that less is loaded into memory when selecting specific columns.
On a partitioned table with a mix of row and column partitions, updates must be carefully targeted or filtered to hit only the row partitions.
If the operation is targeted at a specific row partition (e.g.
UPDATE p2 SET i = i + 1), it will succeed; if targeted at a specified columnar partition (e.g.UPDATE p1 SET i = i + 1), it will fail.If the operation is targeted at the partitioned table and has a
WHEREclause that excludes all columnar partitions (e.g.UPDATE parent SET i = i + 1 WHERE timestamp = '2020-03-15'), it will succeed.If the operation is targeted at the partitioned table, but does not exclude all columnar partitions, it will fail; even if the actual data to be updated only affects row tables (e.g.
UPDATE parent SET i = i + 1 WHERE n = 300).
J.5.9.2.4.5. Limitations #
Future versions of citus will incrementally lift the current limitations:
Append-only (no
UPDATE/DELETEsupport)No space reclamation (e.g. rolled-back transactions may still consume disk space)
No bitmap index scans
No TID scan
No sample scans
No TOAST support (large values supported inline)
No support for
ON CONFLICTstatements (exceptDO NOTHINGactions with no target specified)No support for tuple locks (
SELECT ... FOR SHARE,SELECT ... FOR UPDATE)No support for serializable isolation level
Support for Postgres Pro server versions 12+ only
No support for foreign keys
No support for logical decoding
No support for intra-node parallel scans
No support for
AFTER ... FOR EACH ROWtriggersNo
UNLOGGEDcolumnar tables
J.5.10. Troubleshoot #
J.5.10.1. Query Performance Tuning #
In this section, we describe how you can tune your citus cluster to get maximum performance. We begin by explaining how choosing the right distribution column affects performance. We then describe how you can first tune your database for high performance on one Postgres Pro server and then scale it out across all the CPUs in the cluster. In this section, we also discuss several performance related configuration parameters wherever relevant.
J.5.10.1.1. Table Distribution and Shards #
The first step while creating a distributed table is choosing the right distribution column. This helps citus push down several operations directly to the worker shards and prune away unrelated shards, which lead to significant query speedups.
Typically, you should pick that column as the distribution column which is the most commonly used join key or on which most queries have filters. For filters, citus uses the distribution column ranges to prune away unrelated shards, ensuring that the query hits only those shards which overlap with the WHERE clause ranges. For joins, if the join key is the same as the distribution column, then citus executes the join only between those shards, which have matching / overlapping distribution column ranges. All these shard joins can be executed in parallel on the workers and hence are more efficient.
In addition, citus can push down several operations directly to the worker shards if they are based on the distribution column. This greatly reduces both the amount of computation on each node and the network bandwidth involved in transferring data across nodes.
Once you choose the right distribution column, you can then proceed to the next step, which is tuning worker node performance.
J.5.10.1.2. Postgres Pro Tuning #
The citus coordinator partitions an incoming query into fragment queries and sends them to the workers for parallel processing. The workers are just extended Postgres Pro servers and they apply Postgres Pro standard planning and execution logic for these queries. So, the first step in tuning citus is tuning the Postgres Pro configuration parameters on the workers for high performance.
Tuning the parameters is a matter of experimentation and often takes several attempts to achieve acceptable performance. Thus it is best to load only a small portion of your data when tuning to make each iteration go faster.
To begin the tuning process create a citus cluster and load data in it. From the coordinator node, run the EXPLAIN command on representative queries to inspect performance. citus extends the EXPLAIN command to provide information about distributed query execution. The EXPLAIN output shows how each worker processes the query and also a little about how the coordinator node combines their results.
Here is an example of explaining the plan for a particular example query. We use the VERBOSE flag to see the actual queries, which were sent to the worker nodes.
EXPLAIN VERBOSE
SELECT date_trunc('minute', created_at) AS minute,
sum((payload->>'distinct_size')::int) AS num_commits
FROM github_events
WHERE event_type = 'PushEvent'
GROUP BY minute
ORDER BY minute;
Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: remote_scan.minute
-> HashAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.minute
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0)
Task Count: 32
Tasks Shown: One of 32
-> Task
Query: SELECT date_trunc('minute'::text, created_at) AS minute, sum(((payload OPERATOR(pg_catalog.->>) 'distinct_size'::text))::integer) AS num_commits FROM github_events_102042 github_events WHERE (event_type OPERATOR(pg_catalog.=) 'PushEvent'::text) GROUP BY (date_trunc('minute'::text, created_at))
Node: host=localhost port=5433 dbname=postgres
-> HashAggregate (cost=93.42..98.36 rows=395 width=16)
Group Key: date_trunc('minute'::text, created_at)
-> Seq Scan on github_events_102042 github_events (cost=0.00..88.20 rows=418 width=503)
Filter: (event_type = 'PushEvent'::text)
(13 rows)
This tells you several things. To begin with there are 32 shards, and the planner chose the citus adaptive executor to execute this query:
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0) Task Count: 32
Next it picks one of the workers and shows you more about how the query behaves there. It indicates the host, port, database, and the query that was sent to the worker so you can connect to the worker directly and try the query if desired:
Tasks Shown: One of 32
-> Task
Query: SELECT date_trunc('minute'::text, created_at) AS minute, sum(((payload OPERATOR(pg_catalog.->>) 'distinct_size'::text))::integer) AS num_commits FROM github_events_102042 github_events WHERE (event_type OPERATOR(pg_catalog.=) 'PushEvent'::text) GROUP BY (date_trunc('minute'::text, created_at))
Node: host=localhost port=5433 dbname=postgres
Distributed EXPLAIN next shows the results of running a normal Postgres Pro EXPLAIN on that worker for the fragment query:
-> HashAggregate (cost=93.42..98.36 rows=395 width=16)
Group Key: date_trunc('minute'::text, created_at)
-> Seq Scan on github_events_102042 github_events (cost=0.00..88.20 rows=418 width=503)
Filter: (event_type = 'PushEvent'::text)
You can now connect to the worker at localhost, port 5433 and tune query performance for the shard github_events_102042 using standard Postgres Pro techniques. As you make changes run EXPLAIN again from the coordinator or right on the worker.
The first set of such optimizations relates to configuration settings. Postgres Pro by default comes with conservative resource settings; and among these settings shared_buffers and work_mem are probably the most important ones in optimizing read performance. We discuss these parameters in brief below. Apart from them, several other configuration settings impact query performance. These settings are covered in more detail in the Server Configuration chapter.
The shared_buffers configuration parameter defines the amount of memory allocated to the database for caching data and defaults to 128MB. If you have a worker node with 1GB or more RAM, a reasonable starting value for shared_buffers is 1/4 of the memory in your system. There are some workloads where even larger settings for shared_buffers are effective, but given the way Postgres Pro also relies on the operating system cache, it is unlikely you will find using more than 25% of RAM to work better than a smaller amount.
If you do a lot of complex sorts, then increasing work_mem allows Postgres Pro to do larger in-memory sorts, which will be faster than disk-based equivalents. If you see lot of disk activity on your worker node inspite of having a decent amount of memory, then increasing work_mem to a higher value can be useful. This will help Postgres Pro in choosing more efficient query plans and allow for greater amount of operations to occur in memory.
Other than the above configuration settings, the Postgres Pro query planner relies on statistical information about the contents of tables to generate good plans. These statistics are gathered when ANALYZE is run, which is enabled by default. You can learn more about the Postgres Pro planner and the ANALYZE command in greater detail in the relevant section.
Lastly, you can create indexes on your tables to enhance database performance. Indexes allow the database to find and retrieve specific rows much faster than it could do without an index. To choose which indexes give the best performance, you can run the query with the EXPLAIN command to view query plans and optimize the slower parts of the query. After an index is created, the system has to keep it synchronized with the table which adds overhead to data manipulation operations. Therefore, indexes that are seldom or never used in queries should be removed.
For write performance, you can use general Postgres Pro configuration tuning to increase INSERT rates. We commonly recommend increasing checkpoint_timeout and max_wal_size settings. Also, depending on the reliability requirements of your application, you can choose to change fsync or synchronous_commit values.
Once you have tuned a worker to your satisfaction you will have to manually apply those changes to the other workers as well. To verify that they are all behaving properly, set this configuration variable on the coordinator:
SET citus.explain_all_tasks = 1;
This will cause EXPLAIN to show the query plan for all tasks, not just one.
EXPLAIN
SELECT date_trunc('minute', created_at) AS minute,
sum((payload->>'distinct_size')::int) AS num_commits
FROM github_events
WHERE event_type = 'PushEvent'
GROUP BY minute
ORDER BY minute;
Sort (cost=0.00..0.00 rows=0 width=0)
Sort Key: remote_scan.minute
-> HashAggregate (cost=0.00..0.00 rows=0 width=0)
Group Key: remote_scan.minute
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0)
Task Count: 32
Tasks Shown: All
-> Task
Node: host=localhost port=5433 dbname=postgres
-> HashAggregate (cost=93.42..98.36 rows=395 width=16)
Group Key: date_trunc('minute'::text, created_at)
-> Seq Scan on github_events_102042 github_events (cost=0.00..88.20 rows=418 width=503)
Filter: (event_type = 'PushEvent'::text)
-> Task
Node: host=localhost port=5434 dbname=postgres
-> HashAggregate (cost=103.21..108.57 rows=429 width=16)
Group Key: date_trunc('minute'::text, created_at)
-> Seq Scan on github_events_102043 github_events (cost=0.00..97.47 rows=459 width=492)
Filter: (event_type = 'PushEvent'::text)
--
-- ... repeats for all 32 tasks
-- alternating between workers one and two
-- (running in this case locally on ports 5433, 5434)
--
(199 rows)
Differences in worker execution can be caused by tuning configuration differences, uneven data distribution across shards, or hardware differences between the machines. To get more information about the time it takes the query to run on each shard you can use EXPLAIN ANALYZE.
Note
Note that when citus.explain_all_tasks is enabled, EXPLAIN plans are retrieved sequentially, which may take a long time for EXPLAIN ANALYZE.
citus, by default, sorts tasks by execution time in descending order. If citus.explain_all_tasks is disabled, then citus shows the single longest-running task. Please note that this functionality can be used only with EXPLAIN ANALYZE, since regular EXPLAIN does not execute the queries, and therefore does not know any execution times. To change the sort order, you can use the citus.explain_analyze_sort_method configuration parameter.
J.5.10.1.3. Scaling Out Performance #
As mentioned, once you have achieved the desired performance for a single shard you can set similar configuration parameters on all your workers. As citus runs all the fragment queries in parallel across the worker nodes, users can scale out the performance of their queries to be the cumulative of the computing power of all of the CPU cores in the cluster assuming that the data fits in memory.
Users should try to fit as much of their working set in memory as possible to get best performance with citus. If fitting the entire working set in memory is not feasible, we recommend using SSDs over HDDs as a best practice. This is because HDDs are able to show decent performance when you have sequential reads over contiguous blocks of data, but have significantly lower random read / write performance. In cases where you have a high number of concurrent queries doing random reads and writes, using SSDs can improve query performance by several times as compared to HDDs. Also, if your queries are highly compute-intensive, it might be beneficial to choose machines with more powerful CPUs.
To measure the disk space usage of your database objects, you can log into the worker nodes and use Postgres Pro administration functions for individual shards. The pg_total_relation_size function can be used to get the total disk space used by a table. You can also use other functions mentioned in the Postgres Pro documentation to get more specific size information. On the basis of these statistics for a shard and the shard count, users can compute the hardware requirements for their cluster.
Another factor that affects performance is the number of shards per worker node. citus partitions an incoming query into its fragment queries which run on individual worker shards. Hence, the degree of parallelism for each query is governed by the number of shards the query hits. To ensure maximum parallelism, you should create enough shards on each node such that there is at least one shard per CPU core. Another consideration to keep in mind is that citus will prune away unrelated shards if the query has filters on the distribution column. So, creating more shards than the number of cores might also be beneficial so that you can achieve greater parallelism even after shard pruning.
J.5.10.1.4. Distributed Query Performance Tuning #
Once you have distributed your data across the cluster, with each worker optimized for best performance, you should be able to see high performance gains on your queries. After this, the final step is to tune a few distributed performance tuning parameters.
Before we discuss the specific configuration parameters, we recommend that you measure query times on your distributed cluster and compare them with the single shard performance. This can be done by enabling, timing, and running the query on the coordinator node and running one of the fragment queries on the worker nodes. This helps in determining the amount of time spent on the worker nodes and the amount of time spent in fetching the data to the coordinator node. Then, you can figure out what the bottleneck is and optimize the database accordingly.
In this section, we discuss the parameters that help optimize the distributed query planner and executor. There are several relevant parameters and we discuss them in two sections about general performance tuning and advanced performance tuning. The first section is sufficient for most use cases and covers all the common configs. The second covers parameters that may provide performance gains in specific use cases.
J.5.10.1.4.1. General Performance Tuning #
For higher INSERT performance, the factor that impacts insert rates the most is the level of concurrency. You should try to run several concurrent INSERT statements in parallel. This way you can achieve very high insert rates if you have a powerful coordinator node and are able to use all the CPU cores on that node together.
Subquery/CTE Network Overhead #
In the best case citus can execute queries containing subqueries and CTEs in a single step. This is usually because both the main query and subquery filter by distribution column of tables in the same way and can be pushed down to worker nodes together. However, citus is sometimes forced to execute subqueries before executing the main query, copying the intermediate subquery results to other worker nodes for use by the main query. This technique is called subquery/CTE push-pull execution.
It is important to be aware when subqueries are executed in a separate step and avoid sending too much data between worker nodes. The network overhead will hurt performance. The EXPLAIN command allows you to discover how queries will be executed, including whether multiple steps are required. For a detailed example, see the Subquery/CTE Push-Pull Execution section.
Also you can defensively set a safeguard against large intermediate results. Adjust the citus.max_intermediate_result_size limit in a new connection to the coordinator node. By default the max intermediate result size is 1 GB, which is large enough to allow some inefficient queries. Try turning it down and running your queries:
-- Set a restrictive limit for intermediate results SET citus.max_intermediate_result_size = '512kB'; -- Attempt to run queries -- SELECT …
If the query has subqueries or CTEs that exceed this limit, the query will be canceled and you will see an error message:
ERROR: the intermediate result size exceeds citus.max_intermediate_result_size (currently 512 kB) DETAIL: Citus restricts the size of intermediate results of complex subqueries and CTEs to avoid accidentally pulling large result sets into once place. HINT: To run the current query, set citus.max_intermediate_result_size to a higher value or -1 to disable.
The size of intermediate results and their destination is available in EXPLAIN ANALYZE output:
EXPLAIN ANALYZE WITH deleted_rows AS ( DELETE FROM page_views WHERE tenant_id IN (3, 4) RETURNING * ), viewed_last_week AS ( SELECT * FROM deleted_rows WHERE view_time > current_timestamp - interval '7 days' ) SELECT count(*) FROM viewed_last_week;
Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0) (actual time=570.076..570.077 rows=1 loops=1)
-> Distributed Subplan 31_1
Subplan Duration: 6978.07 ms
Intermediate Data Size: 26 MB
Result destination: Write locally
-> Custom Scan (Citus Adaptive) (cost=0.00..0.00 rows=0 width=0) (actual time=364.121..364.122 rows=0 loops=1)
Task Count: 2
Tuple data received from nodes: 0 bytes
Tasks Shown: One of 2
-> Task
Tuple data received from node: 0 bytes
Node: host=localhost port=5433 dbname=postgres
-> Delete on page_views_102016 page_views (cost=5793.38..49272.28 rows=324712 width=6) (actual time=362.985..362.985 rows=0 loops=1)
-> Bitmap Heap Scan on page_views_102016 page_views (cost=5793.38..49272.28 rows=324712 width=6) (actual time=362.984..362.984 rows=0 loops=1)
Recheck Cond: (tenant_id = ANY ('{3,4}'::integer[]))
-> Bitmap Index Scan on view_tenant_idx_102016 (cost=0.00..5712.20 rows=324712 width=0) (actual time=19.193..19.193 rows=325733 loops=1)
Index Cond: (tenant_id = ANY ('{3,4}'::integer[]))
Planning Time: 0.050 ms
Execution Time: 363.426 ms
Planning Time: 0.000 ms
Execution Time: 364.241 ms
Task Count: 1
Tuple data received from nodes: 6 bytes
Tasks Shown: All
-> Task
Tuple data received from node: 6 bytes
Node: host=localhost port=5432 dbname=postgres
-> Aggregate (cost=33741.78..33741.79 rows=1 width=8) (actual time=565.008..565.008 rows=1 loops=1)
-> Function Scan on read_intermediate_result intermediate_result (cost=0.00..29941.56 rows=1520087 width=0) (actual time=326.645..539.158 rows=651466 loops=1)
Filter: (view_time > (CURRENT_TIMESTAMP - '7 days'::interval))
Planning Time: 0.047 ms
Execution Time: 569.026 ms
Planning Time: 1.522 ms
Execution Time: 7549.308 ms
In the above EXPLAIN ANALYZE output, you can see the following information about the intermediate results:
Intermediate Data Size: 26 MB Result destination: Write locally
It tells us how large the intermediate results were and where the intermediate results were written to. In this case, they were written to the node coordinating the query execution, as specified by Write locally. For some other queries it can also be of the following format:
Intermediate Data Size: 26 MB Result destination: Send to 2 nodes
Which means the intermediate result was pushed to 2 worker nodes and it involved more network traffic.
When using CTEs, or joins between CTEs and distributed tables, you can avoid push-pull execution by following these rules:
Tables should be co-located.
The CTE queries should not require any merge steps (e.g.,
LIMITorGROUP BYon a non-distribution key).Tables and CTEs should be joined on distribution keys.
Also Postgres Pro allows citus to take advantage of CTE inlining to push CTEs down to workers in more circumstances. The inlining behavior can be controlled with the MATERIALIZED keyword. To learn more, see the WITH Queries (Common Table Expressions) section.
J.5.10.1.4.2. Advanced Performance Tuning #
In this section, we discuss advanced performance tuning parameters. These parameters are applicable to specific use cases and may not be required for all deployments.
Connection Management #
When executing multi-shard queries, citus must balance the gains from parallelism with the overhead from database connections. The Query Execution section explains the steps of turning queries into worker tasks and obtaining database connections to the workers.
Set the citus.max_adaptive_executor_pool_size configuration parameter to a low value like
1or2for transactional workloads with short queries (e.g. < 20ms of latency). For analytical workloads where parallelism is critical, leave this setting at its default value of16.Set the citus.executor_slow_start_interval configuration parameter to a high value like
100ms for transactional workloads comprised of short queries that are bound on network latency rather than parallelism. For analytical workloads, leave this setting at its default value of10ms.The default value of
1for the citus.max_cached_conns_per_worker configuration parameter is reasonable. A larger value such as2might be helpful for clusters that use a small number of concurrent sessions, but it is not wise to go much further (e.g.16would be too high). If set too high, sessions will hold idle connections and use worker resources unnecessarily.Set the citus.max_shared_pool_size configuration parameter to match the max_connections setting of your worker nodes. This setting is mainly a fail-safe.
Task Assignment Policy #
The citus query planner assigns tasks to the worker nodes based on shard locations. The algorithm used while making these assignments can be chosen by setting the citus.task_assignment_policy configuration parameter. Users can alter this configuration parameter to choose the policy, which works best for their use case.
The greedy policy aims to distribute tasks evenly across the workers. This policy is the default and works well in most of the cases. The round-robin policy assigns tasks to workers in a round-robin fashion alternating between different replicas. This enables much better cluster utilization when the shard count for a table is low compared to the number of workers. The third policy is the first-replica policy that assigns tasks on the basis of the insertion order of placements (replicas) for the shards. With this policy, users can be sure of which shards will be accessed on each machine. This helps in providing stronger memory residency guarantees by allowing you to keep your working set in memory and use it for querying.
Binary protocol #
In some cases, a large part of query time is spent in sending query results from workers to the coordinator. This mostly happens when queries request many rows (such as SELECT * FROM table), or when result columns use big types (like hll or tdigest from the hll and tdigest extensions).
In those cases it can be beneficial to set citus.enable_binary_protocol to true, which will change the encoding of the results to binary, rather than using text encoding. Binary encoding significantly reduces bandwidth for types that have a compact binary representation, such as hll, tdigest, timestamp and double precision. The default value for this configuration parameter is already true. So explicitly enabling it has no effect.
J.5.10.1.5. Scaling Out Data Ingestion #
citus lets you scale out data ingestion to very high rates, but there are several trade-offs to consider in terms of application integration, throughput, and latency. In this section, we discuss different approaches to data ingestion, and provide guidelines for expected throughput and latency numbers.
J.5.10.1.5.1. Real-Time Insert and Updates #
On the citus coordinator, you can perform INSERT, INSERT .. ON CONFLICT, UPDATE, and DELETE commands directly on distributed tables. When you issue one of these commands, the changes are immediately visible to the user.
When you run the INSERT (or another ingest command), citus first finds the right shard placements based on the value in the distribution column. citus then connects to the worker nodes storing the shard placements, and performs an INSERT on each of them. From the perspective of the user, the INSERT takes several milliseconds to process because of the network latency to worker nodes. The citus coordinator node, however, can process concurrent INSERTs to reach high throughputs.
J.5.10.1.5.2. Staging Data Temporarily #
When loading data for temporary staging, consider using an unlogged table. These are tables which are not backed by the Postgres Pro write-ahead log. This makes them faster for inserting rows but not suitable for long term data storage. You can use an unlogged table as a place to load incoming data, prior to manipulating the data and moving it to permanent tables.
-- Example unlogged table
CREATE UNLOGGED TABLE unlogged_table (
key text,
value text
);
-- Its shards will be unlogged as well when
-- the table is distributed
SELECT create_distributed_table('unlogged_table', 'key');
-- Ready to load data
J.5.10.1.5.3. Bulk Copy (250K - 2M/s) #
Distributed tables support the COPY from the citus coordinator for bulk ingestion, which can achieve much higher ingestion rates than INSERT statements.
COPY can be used to load data directly from an application using COPY .. FROM STDIN, from a file on the server, or program executed on the server.
COPY pgbench_history FROM STDIN WITH (FORMAT CSV);
In psql, the \copy command can be used to load data from the local machine. The \COPY command actually sends a COPY .. FROM STDIN command to the server before sending the local data, as would an application that loads data directly.
psql -c "\COPY pgbench_history FROM 'pgbench_history-2016-03-04.csv' (FORMAT CSV)"
A powerful feature of COPY for distributed tables is that it asynchronously copies data to the workers over many parallel connections, one for each shard placement. This means that data can be ingested using multiple workers and multiple cores in parallel. Especially when there are expensive indexes such as a GIN, this can lead to major performance boosts over ingesting into a regular Postgres Pro table.
From a throughput standpoint, you can expect data ingest ratios of 250K - 2M rows per second when using COPY.
Note
Make sure your benchmarking setup is well configured so you can observe optimal COPY performance. Follow these tips:
We recommend a large batch size (~ 50000-100000). You can benchmark with multiple files (1, 10, 1000, 10000, etc), each of that batch size.
Use parallel ingestion. Increase the number of threads/ingestors to 2, 4, 8, 16 and run benchmarks.
Use a compute-optimized coordinator. For the workers choose memory-optimized boxes with a decent number of vCPUs.
Go with a relatively small shard count, 32 should suffice, but you could benchmark with 64, too.
Ingest data for a suitable amount of time (say 2, 4, 8, 24 hrs). Longer tests are more representative of a production setup.
J.5.10.2. Useful Diagnostic Queries #
J.5.10.2.1. Finding Which Shard Contains Data For a Specific Tenant #
The rows of a distributed table are grouped into shards, and each shard is placed on a worker node in the citus cluster. In the multi-tenant citus use case we can determine which worker node contains the rows for a specific tenant by putting together two pieces of information: the shard_id associated with the tenant_id, and the shard placements on workers. The two can be retrieved together in a single query. Suppose our multi-tenant application's tenants are stores, and we want to find which worker node holds the data for gap.com (id=4, suppose).
To find the worker node holding the data for store id=4, ask for the placement of rows whose distribution column has value 4:
SELECT shardid, shardstate, shardlength, nodename, nodeport, placementid
FROM pg_dist_placement AS placement,
pg_dist_node AS node
WHERE placement.groupid = node.groupid
AND node.noderole = 'primary'
AND shardid = (
SELECT get_shard_id_for_distribution_column('stores', 4)
);
The output contains the host and port of the worker database.
┌─────────┬────────────┬─────────────┬───────────┬──────────┬─────────────┐ │ shardid │ shardstate │ shardlength │ nodename │ nodeport │ placementid │ ├─────────┼────────────┼─────────────┼───────────┼──────────┼─────────────┤ │ 102009 │ 1 │ 0 │ localhost │ 5433 │ 2 │ └─────────┴────────────┴─────────────┴───────────┴──────────┴─────────────┘
J.5.10.2.2. Finding Which Node Hosts a Distributed Schema #
Distributed schemas are automatically associated with individual co-location groups such that the tables created in those schemas are converted to co-located distributed tables without a shard key. You can find where a distributed schema resides by joining the citus_shards view with the citus_schemas view:
SELECT schema_name, nodename, nodeport
FROM citus_shards
JOIN citus_schemas cs
ON cs.colocation_id = citus_shards.colocation_id
GROUP BY 1,2,3;
schema_name | nodename | nodeport -------------+-----------+---------- a | localhost | 9701 b | localhost | 9702 with_data | localhost | 9702
You can also query citus_shards directly filtering down to schema table type to have a detailed listing for all tables.
SELECT * FROM citus_shards WHERE citus_table_type = 'schema';
table_name | shardid | shard_name | citus_table_type | colocation_id | nodename | nodeport | shard_size | schema_name | colocation_id | schema_size | schema_owner ----------------+---------+-----------------------+------------------+---------------+-----------+----------+------------+-------------+---------------+-------------+-------------- a.cities | 102080 | a.cities_102080 | schema | 4 | localhost | 9701 | 8192 | a | 4 | 128 kB | citus a.map_tags | 102145 | a.map_tags_102145 | schema | 4 | localhost | 9701 | 32768 | a | 4 | 128 kB | citus a.measurement | 102047 | a.measurement_102047 | schema | 4 | localhost | 9701 | 0 | a | 4 | 128 kB | citus a.my_table | 102179 | a.my_table_102179 | schema | 4 | localhost | 9701 | 16384 | a | 4 | 128 kB | citus a.people | 102013 | a.people_102013 | schema | 4 | localhost | 9701 | 32768 | a | 4 | 128 kB | citus a.test | 102008 | a.test_102008 | schema | 4 | localhost | 9701 | 8192 | a | 4 | 128 kB | citus a.widgets | 102146 | a.widgets_102146 | schema | 4 | localhost | 9701 | 32768 | a | 4 | 128 kB | citus b.test | 102009 | b.test_102009 | schema | 5 | localhost | 9702 | 8192 | b | 5 | 32 kB | citus b.test_col | 102012 | b.test_col_102012 | schema | 5 | localhost | 9702 | 24576 | b | 5 | 32 kB | citus with_data.test | 102180 | with_data.test_102180 | schema | 11 | localhost | 9702 | 647168 | with_data | 11 | 632 kB | citus
J.5.10.2.3. Finding the Distribution Column For a Table #
Each distributed table in citus has a “distribution column”. For more information about what this is and how it works, see the Choosing Distribution Column section. There are many situations where it is important to know which column it is. Some operations require joining or filtering on the distribution column, and you may encounter error messages with hints like add a filter to the distribution column.
The pg_dist_* tables on the coordinator node contain diverse metadata about the distributed database. In particular the pg_dist_partition table holds information about the distribution column (formerly called partition column) for each table. You can use a convenient utility function to look up the distribution column name from the low-level details in the metadata. Here is an example and its output:
-- Create example table
CREATE TABLE products (
store_id bigint,
product_id bigint,
name text,
price money,
CONSTRAINT products_pkey PRIMARY KEY (store_id, product_id)
);
-- Pick store_id as distribution column
SELECT create_distributed_table('products', 'store_id');
-- Get distribution column name for products table
SELECT column_to_column_name(logicalrelid, partkey) AS dist_col_name
FROM pg_dist_partition
WHERE logicalrelid='products'::regclass;
Example output:
┌───────────────┐ │ dist_col_name │ ├───────────────┤ │ store_id │ └───────────────┘
J.5.10.2.4. Detecting Locks #
This query will run across all worker nodes and identify locks, how long they have been open, and the offending queries:
SELECT * FROM citus_lock_waits;
For more information, see the Distributed Query Activity section.
J.5.10.2.5. Querying the Size of Your Shards #
This query will provide you with the size of every shard of a given distributed table, designated here with the placeholder my_table:
SELECT shardid, table_name, shard_size
FROM citus_shards
WHERE table_name = 'my_table';
Example output:
. shardid | table_name | shard_size ---------+------------+------------ 102170 | my_table | 90177536 102171 | my_table | 90177536 102172 | my_table | 91226112 102173 | my_table | 90177536
This query uses the citus_shards view.
J.5.10.2.6. Querying the Size of All Distributed Tables #
This query gets a list of the sizes for each distributed table plus the size of their indices.
SELECT table_name, table_size FROM citus_tables;
Example output:
┌───────────────┬────────────┐ │ table_name │ table_size │ ├───────────────┼────────────┤ │ github_users │ 39 MB │ │ github_events │ 98 MB │ └───────────────┴────────────┘
There are other ways to measure distributed table size as well. To learn more, see the Determining Table and Relation Size section.
J.5.10.2.7. Identifying Unused Indices #
This query will run across all worker nodes and identify any unused indexes for a given distributed table, designated here with the placeholder my_distributed_table:
SELECT *
FROM run_command_on_shards('my_distributed_table', $cmd$
SELECT array_agg(a) as infos
FROM (
SELECT (
schemaname || '.' || relname || '##' || indexrelname || '##'
|| pg_size_pretty(pg_relation_size(i.indexrelid))::text
|| '##' || idx_scan::text
) AS a
FROM pg_stat_user_indexes ui
JOIN pg_index i
ON ui.indexrelid = i.indexrelid
WHERE NOT indisunique
AND idx_scan < 50
AND pg_relation_size(relid) > 5 * 8192
AND (schemaname || '.' || relname)::regclass = '%s'::regclass
ORDER BY
pg_relation_size(i.indexrelid) / NULLIF(idx_scan, 0) DESC nulls first,
pg_relation_size(i.indexrelid) DESC
) sub
$cmd$);
Example output:
┌─────────┬─────────┬─────────────────────────────────────────────────────────────────────────────────┐
│ shardid │ success │ result │
├─────────┼─────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ 102008 │ t │ │
│ 102009 │ t │ {"public.my_distributed_table_102009##stupid_index_102009##28 MB##0"} │
│ 102010 │ t │ │
│ 102011 │ t │ │
└─────────┴─────────┴─────────────────────────────────────────────────────────────────────────────────┘
J.5.10.2.8. Monitoring Client Connection Count #
This query will give you the connection count by each type that are open on the coordinator:
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
Example output:
┌────────┬───────┐ │ state │ count │ ├────────┼───────┤ │ active │ 3 │ │ ∅ │ 1 │ └────────┴───────┘
J.5.10.2.9. Viewing System Queries #
J.5.10.2.9.1. Active Queries #
The citus_stat_activity shows which queries are currently executing. You can filter to find the actively executing ones, along with the process ID of their backend:
SELECT global_pid, query, state FROM citus_stat_activity WHERE state != 'idle';
J.5.10.2.9.2. Why Are Queries Waiting #
We can also query to see the most common reasons that non-idle queries that are waiting. For an explanation of the reasons, see the Wait Event Types table.
SELECT wait_event || ':' || wait_event_type AS type, count(*) AS number_of_occurences FROM pg_stat_activity WHERE state != 'idle' GROUP BY wait_event, wait_event_type ORDER BY number_of_occurences DESC;
Example output when executing the pg_sleep function in a separate query concurrently:
┌─────────────────┬──────────────────────┐ │ type │ number_of_occurences │ ├─────────────────┼──────────────────────┤ │ ∅ │ 1 │ │ PgSleep:Timeout │ 1 │ └─────────────────┴──────────────────────┘
J.5.10.2.10. Index Hit Rate #
This query will provide you with your index hit rate across all nodes. Index hit rate is useful in determining how often indices are used when querying:
-- On coordinator
SELECT 100 * (sum(idx_blks_hit) - sum(idx_blks_read)) / sum(idx_blks_hit) AS index_hit_rate
FROM pg_statio_user_indexes;
-- On workers
SELECT nodename, result as index_hit_rate
FROM run_command_on_workers($cmd$
SELECT 100 * (sum(idx_blks_hit) - sum(idx_blks_read)) / sum(idx_blks_hit) AS index_hit_rate
FROM pg_statio_user_indexes;
$cmd$);
Example output:
┌───────────┬────────────────┐ │ nodename │ index_hit_rate │ ├───────────┼────────────────┤ │ 10.0.0.16 │ 96.0 │ │ 10.0.0.20 │ 98.0 │ └───────────┴────────────────┘
J.5.10.2.11. Cache Hit Rate #
Most applications typically access a small fraction of their total data at once. Postgres Pro keeps frequently accessed data in memory to avoid slow reads from disk. You can see statistics about it in the pg_statio_user_tables view.
An important measurement is what percentage of data comes from the memory cache vs the disk in your workload:
-- On coordinator
SELECT
sum(heap_blks_read) AS heap_read,
sum(heap_blks_hit) AS heap_hit,
100 * sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS cache_hit_rate
FROM
pg_statio_user_tables;
-- On workers
SELECT nodename, result as cache_hit_rate
FROM run_command_on_workers($cmd$
SELECT
100 * sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS cache_hit_rate
FROM
pg_statio_user_tables;
$cmd$);
Example output:
┌───────────┬──────────┬─────────────────────┐ │ heap_read │ heap_hit │ cache_hit_rate │ ├───────────┼──────────┼─────────────────────┤ │ 1 │ 132 │ 99.2481203007518796 │ └───────────┴──────────┴─────────────────────┘
If you find yourself with a ratio significantly lower than 99%, then you likely want to consider increasing the cache available to your database.
J.5.10.3. Common Error Messages #
J.5.10.3.1. could not connect to server: Connection refused #
Caused when the coordinator node is unable to connect to a worker.
SELECT 1 FROM companies WHERE id = 2928;
ERROR: connection to the remote node localhost:5432 failed with the following error: could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP/IP connections on port 5432?
J.5.10.3.1.1. Resolution #
To fix, check that the worker is accepting connections, and that DNS is correctly resolving.
J.5.10.3.2. canceling the transaction since it was involved in a distributed deadlock #
Deadlocks can happen not only in a single-node database, but in a distributed database, caused by queries executing across multiple nodes. citus has the intelligence to recognize distributed deadlocks and defuse them by aborting one of the queries involved.
We can see this in action by distributing rows across worker nodes and then running two concurrent transactions with conflicting updates:
CREATE TABLE lockme (id int, x int);
SELECT create_distributed_table('lockme', 'id');
-- id=1 goes to one worker, and id=2 another
INSERT INTO lockme VALUES (1,1), (2,2);
--------------- TX 1 ---------------- --------------- TX 2 ----------------
BEGIN;
BEGIN;
UPDATE lockme SET x = 3 WHERE id = 1;
UPDATE lockme SET x = 4 WHERE id = 2;
UPDATE lockme SET x = 3 WHERE id = 2;
UPDATE lockme SET x = 4 WHERE id = 1;
ERROR: canceling the transaction since it was involved in a distributed deadlock
J.5.10.3.2.1. Resolution #
Detecting deadlocks and stopping them is part of normal distributed transaction handling. It allows an application to retry queries or take another course of action.
J.5.10.3.3. could not connect to server: Cannot assign requested address #
WARNING: connection error: localhost:9703 DETAIL: could not connect to server: Cannot assign requested address
This occurs when there are no more sockets available by which the coordinator can respond to worker requests.
J.5.10.3.3.1. Resolution #
Configure the operating system to re-use TCP sockets. Execute this on the shell in the coordinator node:
sysctl -w net.ipv4.tcp_tw_reuse=1
This allows reusing sockets in TIME_WAIT state for new connections when it is safe from a protocol viewpoint. Default value is 0 (disabled).
J.5.10.3.4. SSL error: certificate verify failed #
In citus, nodes are required talk to one another using SSL by default. If SSL is not enabled on a Postgres Pro server when citus is first installed, the install process will enable it, which includes creating and self-signing an SSL certificate.
However, if a root certificate authority file exists (typically in ~/.postgresql/root.crt), then the certificate will be checked unsuccessfully against that Certificate Authority at connection time.
J.5.10.3.4.1. Resolution #
Possible solutions are to sign the certificate, turn off SSL, or remove the root certificate. Also a node may have trouble connecting to itself without the help of the citus.local_hostname configuration parameter.
J.5.10.3.5. could not connect to any active placements #
When all available worker connection slots are in use, further connections will fail.
WARNING: connection error: hostname:5432 ERROR: could not connect to any active placements
J.5.10.3.5.1. Resolution #
This error happens most often when copying data into citus in parallel. The COPY command opens up one connection per shard. If you run M concurrent copies into a destination with N shards, that will result in M*N connections. To solve the error, reduce the shard count of target distributed tables, or run fewer \copy commands in parallel.
J.5.10.3.6. remaining connection slots are reserved for non-replication superuser connections #
This occurs when Postgres Pro runs out of available connections to serve concurrent client requests.
J.5.10.3.6.1. Resolution #
The max_connections configuration parameter adjusts the limit, with a typical default of 100 connections. Note that each connection consumes resources, so adjust sensibly. When increasing max_connections it is usually a good idea to increase memory limits too.
Using pgbouncer can also help by queueing connection requests, which exceed the connection limit.
J.5.10.3.7. pgbouncer cannot connect to server #
In a self-hosted citus cluster, this error indicates that the coordinator node is not responding to pgbouncer.
J.5.10.3.7.1. Resolution #
Try connecting directly to the server with psql to ensure it is running and accepting connections.
J.5.10.3.8. creating unique indexes on non-partition columns is currently unsupported #
As a distributed system, citus can guarantee uniqueness only if a unique index or primary key constraint includes a table distribution column. That is because the shards are split so that each shard contains non-overlapping partition column values. The index on each worker node can locally enforce its part of the constraint.
Trying to make a unique index on a non-distribution column will generate an error:
ERROR: creating unique indexes on non-partition columns is currently unsupported
Enforcing uniqueness on a non-distribution column would require citus to check every shard on every INSERT to validate, which defeats the goal of scalability.
J.5.10.3.8.1. Resolution #
There are two ways to enforce uniqueness on a non-distribution column:
Create a composite unique index or primary key that includes the desired column (C), but also includes the distribution column (D). This is not quite as strong a condition as uniqueness on C alone, but will ensure that the values of C are unique for each value of D. For instance if distributing by
company_idin a multi-tenant system, this approach would make C unique within each company.Use a reference table rather than a hash-distributed table. This is only suitable for small tables, since the contents of the reference table will be duplicated on all nodes.
J.5.10.3.9. function create_distributed_table does not exist #
SELECT create_distributed_table('foo', 'id');
/*
ERROR: function create_distributed_table(unknown, unknown) does not exist
LINE 1: SELECT create_distributed_table('foo', 'id');
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
*/
J.5.10.3.9.1. Resolution #
When basic utility functions are not available, check whether the citus extension is properly installed. Running \dx in psql will list installed extensions.
One way to end up without extensions is by creating a new database in a Postgres Pro server, which requires extensions to be re-installed. See the Creating a New Database section to learn how to do it right.
J.5.10.3.10. STABLE functions used in UPDATE queries cannot be called with column references #
Each Postgres Pro function has a volatility classification, which indicates whether the function can update the database and whether the function's return value can vary over time given the same inputs. A STABLE function is guaranteed to return the same results given the same arguments for all rows within a single statement, while an IMMUTABLE function is guaranteed to return the same results given the same arguments forever.
Non-immutable functions can be inconvenient in distributed systems because they can introduce subtle changes when run at slightly different times across shards. Differences in database configuration across nodes can also interact harmfully with non-immutable functions.
One of the most common ways this can happen is using the timestamp in Postgres Pro, which unlike timestamptz does not keep a record of time zone. Interpreting a timestamp column makes reference to the database timezone, which can be changed between queries, hence functions operating on timestamps are not immutable.
citus forbids running distributed queries that filter results using stable functions on columns. For instance:
-- foo_timestamp is timestamp, not timestamptz UPDATE foo SET ... WHERE foo_timestamp < now();
ERROR: STABLE functions used in UPDATE queries cannot be called with column references
In this case the comparison operator < between timestamp and timestamptz is not immutable.
J.5.10.3.10.1. Resolution #
Avoid stable functions on columns in a distributed UPDATE statement. In particular, whenever working with times use timestamptz rather than timestamp. Having a time zone in timestamptz makes calculations immutable.
J.5.11. Frequently Asked Questions #
J.5.11.1. Can I create primary keys on distributed tables? #
Currently citus imposes primary key constraint only if the distribution column is a part of the primary key. This assures that the constraint needs to be checked only on one shard to ensure uniqueness.
J.5.11.2. How do I add nodes to an existing citus cluster? #
With citus, you can add nodes manually by calling the citus_add_node function with the hostname (or IP address) and port number of the new node.
After adding a node to an existing cluster, the new node will not contain any data (shards). citus will start assigning any newly created shards to this node. To rebalance existing shards from the older nodes to the new node, citus provides an open source shard rebalancer utility. You can find more information in the Rebalancing Shards Without Downtime section.
J.5.11.3. How does citus handle failure of a worker node? #
citus uses Postgres Pro streaming replication to replicate the entire worker-node as-is. It replicates worker nodes by continuously streaming their WAL records to a standby. You can configure streaming replication on-premise yourself by consulting the Streaming Replication section.
J.5.11.4. How does citus handle failover of the coordinator node? #
As the citus coordinator node is similar to a standard Postgres Pro server, regular Postgres Pro synchronous replication and failover can be used to provide higher availability of the coordinator node. To learn more about handling coordinator node failures, see the Coordinator Node Failures section.
J.5.11.5. Are there any Postgres Pro features not supported by citus? #
Since citus provides distributed functionality by extending Postgres Pro, it uses the standard Postgres Pro SQL constructs. The vast majority of queries are supported, even when they combine data across the network from multiple database nodes. This includes transactional semantics across nodes. For an up-to-date list of SQL coverage, see the Limitations section.
What's more, citus has 100% SQL support for queries that access a single node in the database cluster. These queries are common, for instance, in multi-tenant applications where different nodes store different tenants. To learn more, see the When to Use citus section.
Remember that even with this extensive SQL coverage data modeling can have a significant impact on query performance. See the Query Processing section for details on how citus executes queries.
J.5.11.6. How do I choose the shard count when I hash-partition my data? #
One of the choices when first distributing a table is its shard count. This setting can be set differently for each co-location group, and the optimal value depends on the use case. It is possible, but difficult, to change the count after cluster creation, so use these guidelines to choose the right size.
In the multi-tenant SaaS database use case we recommend choosing between 32 and 128 shards. For smaller workloads, say <100GB, you could start with 32 shards and for larger workloads you could choose 64 or 128. This means that you have the leeway to scale from 32 to 128 worker machines.
In the real-time analytics use case, shard count should be related to the total number of cores on the workers. To ensure maximum parallelism, you should create enough shards on each node such that there is at least one shard per CPU core. We typically recommend creating a high number of initial shards, e.g. 2x or 4x the number of current CPU cores. This allows for future scaling if you add more workers and CPU cores.
To choose a shard count for a table you wish to distribute, update the citus.shard_count configuration parameter. This affects subsequent calls to the create_distributed_table function. For example:
SET citus.shard_count = 64; -- any tables distributed at this point will have -- sixty-four shards
For more guidance on this topic, see the Choosing Cluster Size section.
J.5.11.7. How do I change the shard count for a hash-partitioned table? #
citus has a function called alter_distributed_table that can change the shard count of a distributed table.
J.5.11.8. How does citus support count(distinct) queries? #
citus can evaluate count(distinct) aggregates both in and across worker nodes. When aggregating on a table's distribution column, citus can push the counting down inside worker nodes and total the results. Otherwise it can pull distinct rows to the coordinator and calculate there. If transferring data to the coordinator is too expensive, fast approximate counts are also available. More details in The count(distinct) Aggregates section.
J.5.11.9. In which situations are uniqueness constraints supported on distributed tables? #
citus is able to enforce a primary key or uniqueness constraint only when the constrained columns contain the distribution column. In particular this means that if a single column constitutes the primary key then it has to be the distribution column as well.
This restriction allows citus to localize a uniqueness check to a single shard and let Postgres Pro on the worker node do the check efficiently.
J.5.11.10. How do I create database roles, functions, extensions etc in a citus cluster? #
Certain commands, when run on the coordinator node, do not get propagated to the workers:
CREATE ROLE/USERCREATE DATABASEALTER … SET SCHEMAALTER TABLE ALL IN TABLESPACECREATE TABLE(see the Table Types section)
For the other types of objects above, create them explicitly on all nodes. citus provides a function to execute queries across all workers:
SELECT run_command_on_workers($cmd$ /* the command to run */ CREATE ROLE ... $cmd$);
Learn more in the Manual Query Propagation section. Also note that even after manually propagating CREATE DATABASE, citus must still be installed there. See the Creating a New Database section.
In the future citus will automatically propagate more kinds of objects. The advantage of automatic propagation is that citus will automatically create a copy on any newly added worker nodes (see the citus.pg_dist_object table to learn more).
J.5.11.11. What if a worker node's address changes? #
If the hostname or IP address of a worker changes, you need to let the coordinator know using the citus_update_node function:
-- Update worker node metadata on the coordinator -- (remember to replace 'old-address' and 'new-address' -- with the actual values for your situation) SELECT citus_update_node(nodeid, 'new-address', nodeport) FROM pg_dist_node WHERE nodename = 'old-address';
Until you execute this update, the coordinator will not be able to communicate with that worker for queries.
J.5.11.12. Which shard contains data for a particular tenant? #
citus provides utility functions and metadata tables to determine the mapping of a distribution column value to a particular shard, and the shard placement on a worker node. See the Finding Which Shard Contains Data For a Specific Tenant section for more details.
J.5.11.13. I forgot the distribution column of a table, how do I find it? #
The citus coordinator node metadata tables contain this information. See the Finding the Distribution Column For a Table section.
J.5.11.14. Can I distribute a table by multiple keys? #
No, you must choose a single column per table as the distribution column. A common scenario where people want to distribute by two columns is for timeseries data. However, for this case we recommend using a hash distribution on a non-time column, and combining this with Postgres Pro partitioning on the time column, as described in the Timeseries Data section.
J.5.11.15. Why does pg_relation_size report zero bytes for a distributed table? #
The data in distributed tables lives on the worker nodes (in shards), not on the coordinator. A true measure of distributed table size is obtained as a sum of shard sizes. citus provides helper functions to query this information. See the Determining Table and Relation Size section to learn more.
J.5.11.16. Why am I seeing an error about citus.max_intermediate_result_size? #
citus has to use more than one step to run some queries having subqueries or CTEs. Using the subquery/CTE push-pull execution, it pushes subquery results to all worker nodes for use by the main query. If these results are too large, this might cause unacceptable network overhead, or even insufficient storage space on the coordinator node which accumulates and distributes the results.
citus has a configurable setting citus.max_intermediate_result_size to specify a subquery result size threshold at which the query will be canceled. If you run into the error, it looks like:
ERROR: the intermediate result size exceeds citus.max_intermediate_result_size (currently 1 GB) DETAIL: Citus restricts the size of intermediate results of complex subqueries and CTEs to avoid accidentally pulling large result sets into once place. HINT: To run the current query, set citus.max_intermediate_result_size to a higher value or -1 to disable.
As the error message suggests, you can (cautiously) increase this limit by altering the variable:
SET citus.max_intermediate_result_size = '3GB';
J.5.11.17. Can I shard by schema on citus for multi-tenant applications? #
Yes, schema-based sharding is available.
J.5.11.18. How does cstore_fdw work with citus? #
The cstore_fdw extension is no longer needed on Postgres Pro 12 and above, because columnar storage is now implemented directly in citus. Unlike cstore_fdw, columnar tables of the citus support transactional semantics, replication, and pg_upgrade. citus query parallelization, seamless sharding, and high-availability benefits combine powerfully with the superior compression and I/O utilization of columnar storage for large dataset archival and reporting.