H.5. citus — функциональность распределённой базы данных и столбцовое хранение #

citus — это расширение, совместимое с Postgres Pro и предоставляющее такие основные функциональные возможности, как столбцовое хранение и распределённая база данных OLAP, которые можно использовать вместе или раздельно.

citus обладает следующими преимуществами:

  • Столбцовое хранение с возможностью сжатия данных.

  • Возможность масштабировать инсталляцию Postgres Pro до кластера распределённых баз данных.

  • Сегментирование на основе строк или схем.

  • Распараллеливание DML-операций по узлам кластера.

  • Таблицы-справочники, к которым можно обращаться локально на каждом узле.

  • Возможность выполнять DML-запросы на любом узле, что позволяет максимально эффективно использовать кластер для распределённых запросов.

H.5.1. Ограничения #

citus несовместим с некоторыми функциональными возможностями Postgres Pro Enterprise, обратите внимание на эти ограничения при планировании работы с расширением:

  • citus не может использоваться совместно с автономными транзакциями.

  • При использовании citus с параметром enable_group_by_reordering, имеющим значение on, некоторые запросы могут завершаться ошибкой, поэтому рекомендуется поменять значение параметра на off.

  • Если для параметра enable_self_join_removal задано значение on, запросы к распределённым таблицам citus могут возвращать некорректные результаты, поэтому рекомендуется поменять значение параметра на off.

  • citus не следует использовать совместно с перепланированием запросов в реальном времени, так как команда EXPLAIN ANALYZE может работать некорректно.

H.5.2. Установка #

H.5.2.1. Установка citus на одном узле #

Чтобы задействовать citus на одном узле, выполните следующие действия:

  1. Добавьте citus в переменную shared_preload_libraries в файле postgresql.conf:

    shared_preload_libraries = 'citus'

    Расширение citus следует указывать первым в shared_preload_libraries, если планируется использовать его вместе с другими расширениями.

  2. Перезагрузите сервер баз данных для применения изменений. Чтобы убедиться, что библиотека citus установлена правильно, выполните следующую команду:

    SHOW shared_preload_libraries;
  3. Создайте расширение citus, выполнив следующий запрос:

    CREATE EXTENSION citus;

При выполнении команды CREATE EXTENSION в рамках вышеуказанной процедуры также устанавливается расширение citus_columnar. При необходимости задействовать только citus_columnar выполните те же действия, но вместо citus укажите citus_columnar.

H.5.2.2. Установка citus на нескольких узлах #

Чтобы задействовать citus на нескольких узлах, выполните следующие действия на каждом из них:

  1. Добавьте citus в переменную shared_preload_libraries в файле postgresql.conf:

    shared_preload_libraries = 'citus'

    Расширение citus следует указывать первым в shared_preload_libraries, если планируется использовать его вместе с другими расширениями.

  2. Настройте права доступа к серверу баз данных. По умолчанию сервер баз данных принимает подключения от клиентов только через localhost. Установите значение * для параметра listen_addresses, чтобы указать все имеющиеся IP-интерфейсы.

  3. Настройте аутентификацию клиентов, отредактировав файл pg_hba.conf.

  4. Перезагрузите сервер баз данных для применения изменений. Чтобы убедиться, что библиотека citus установлена правильно, выполните следующую команду:

    SHOW shared_preload_libraries;
  5. Создайте расширение citus, выполнив следующий запрос:

    CREATE EXTENSION citus;

Когда все описанные выше шаги будут выполнены на всех узлах, совершите следующие действия на узле-координаторе, чтобы рабочие узлы могли к нему подключиться:

  1. Зарегистрируйте адрес узла, по которому узел-координатор будет принимать подключения от рабочих узлов:

    SELECT citus_set_coordinator_host('имя_узла_координатора', порт_узла_координатора);
  2. Добавьте каждый рабочий узел:

    SELECT * from citus_add_node('имя_рабочего_узла', порт_рабочего_узла);
  3. Убедитесь, что все рабочие узлы заданы:

    SELECT * FROM citus_get_active_worker_nodes();

H.5.3. Когда использовать citus #

H.5.3.1. Многоарендная база данных SaaS #

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

citus поддерживает все возможности SQL для выполнения этих задач и позволяет масштабировать реляционные базы данных для более 100 000 арендаторов. Кроме того, расширение содержит новую функциональность для поддержки многоарендности. Например, поддерживаются таблицы-справочники, позволяющие сократить дублирование данных у разных арендаторов, а также изоляция арендаторов, которая гарантирует высокую производительность системы для крупных арендаторов.

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

citus предоставляет следующие преимущества для многоарендных приложений:

  • Быстрое выполнение запросов всех арендаторов.

  • Логика сегментирования в рамках базы данных, а не приложения.

  • Хранение большего объёма данных на одном узле Postgres Pro.

  • Масштабирование с сохранением возможностей SQL.

  • Сохранение производительности при высококонкурентном доступе.

  • Быстрый анализ показателей по клиентской базе.

  • Масштабирование при увеличении числа клиентов.

  • Изолированное использование ресурсов крупными и небольшими арендаторами.

H.5.3.2. Анализ данных в реальном времени #

citus поддерживает запросы к большим наборам данных в реальном времени. Обычно такие запросы выполняют в быстро развивающихся системах событий или системах с данными временных рядов. Ниже приведены сценарии использования:

  • Аналитические информационные панели с высокой скоростью отклика.

  • Исследовательские запросы по событиям, происходящим в реальном времени.

  • Архивирование больших наборов данных и подготовка отчётов по ним.

  • Анализ сеансов с запросами воронкообразного, сегментного или когортного анализа.

citus распараллеливает выполнение запросов и обеспечивает линейное масштабирование с учётом числа рабочих баз данных кластера. Ниже представлены некоторые преимущества citus для приложений, работающих в реальном времени:

  • Сохранение скорости отклика при увеличении объёма данных.

  • Анализ новых событий и данных в реальном времени.

  • Распараллеливание SQL-запросов.

  • Масштабирование с сохранением возможностей SQL.

  • Сохранение производительности при высококонкурентном доступе.

  • Быстрые ответы на запросы с панели управления.

  • Использование единой базы данных вместо нескольких на разных узлах.

  • Поддержка большого числа типов данных и расширений Postgres Pro.

H.5.3.3. Микросервисы #

citus поддерживает сегментирование на основе схем, которое позволяет распределять обычные схемы баз данных по разным компьютерам. Такой метод сегментирования отлично согласуется с обычной микросервисной архитектурой, в рамках которой сервис полностью владеет хранилищем и у разных арендаторов не может быть одного и того же определения схемы.

Модель сегментирования на основе схем проще в настройке и позволяет создавать новую схему и задавать search_path в микросервисе.

Преимущества использования citus для микросервисов:

  • Возможность горизонтального масштабирования микросервисов.

  • Перенос стратегически важных корпоративных данных из микросервисов в обычные распределённые таблицы для анализа.

  • Эффективное использование аппаратных ресурсов за счёт балансировки микросервисов на разных компьютерах.

  • Изолирование шумных микросервисов на отдельных узлах.

  • Понятная модель сегментирования.

  • Быстрое внедрение.

H.5.3.4. Замечания об использовании #

citus расширяет функциональность Postgres Pro за счёт возможности создания распределённых баз данных, но не является решением, которое масштабирует любые рабочие нагрузки. Для эффективного использования citus следует продумывать используемые модели данных, инструменты и возможности SQL.

Разумно подойти к вопросу используемых инструментов и возможностей SQL следующим образом: если ваши задачи сопоставимы с описанными здесь сценариями использования, но какой-то инструмент или запрос не поддерживается, — попробуйте воспользоваться обходным решением, обычно оно есть.

H.5.3.5. Когда не рекомендуется использовать citus #

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

  • Не предполагается рост нагрузки, требующий использования более одного узла Postgres Pro Enterprise.

  • Данные анализируются без необходимости их передачи и выполнения запросов в реальном времени.

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

  • Выполняются запросы по извлечению, преобразованию и загрузке больших массивов данных в рамках ETL-процессов, а не запрашивается краткая сводная информация.

H.5.4. Краткое руководство #

H.5.4.1. Многоарендные приложения #

В этом руководстве используется пример набора данных для аналитики рекламы, чтобы продемонстрировать, как можно использовать citus для поддержки многоарендного приложения.

Примечание

В этом руководстве предполагается, что расширение citus уже установлено и работает. Если это не так, обратитесь к Подразделу H.5.2.1, чтобы настроить его локально.

H.5.4.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
H.5.4.1.2. Создание таблиц #
  1. Сначала подключитесь к узлу-координатору citus с помощью psql.

    Если citus установлен согласно описанию в Подразделе H.5.2.1, узел-координатор запускается и слушает порт 9700.

    psql -p 9700
  2. Создайте таблицы с помощью команды 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
    );
  3. Создайте индексы первичных ключей для каждой из таблиц по аналогии со стандартной процедурой в 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);
H.5.4.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
H.5.4.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. Для рабочих нагрузок со множеством арендаторов транзакции выполняются более эффективно в распределённых функциях. Эффективность заметно повышается для крупных транзакций, но в качестве примера можно использовать и небольшую транзакцию выше.

  1. Сначала создайте функцию, удаляющую кампании:

    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$;
  2. Затем используйте функцию 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);
  3. Помимо транзакционных операций, также можно выполнять аналитические запросы с использованием стандартного языка SQL. Интересный запрос для предприятия — получить подробную информацию о своих рекламных кампаниях с максимальным бюджетом.

    SELECT name, cost_model, state, monthly_budget
    FROM campaigns
    WHERE company_id = 5
    ORDER BY monthly_budget DESC
    LIMIT 10;
  4. Также можно выполнить запрос соединения по нескольким таблицам, чтобы просмотреть информацию о запущенных рекламных кампаниях 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 для простого многоарендного приложения. В качестве следующего шага можно ознакомиться с описанием многоарендных приложений, чтобы узнать, как моделировать собственные данные для множества арендаторов.

H.5.4.2. Анализ данных в реальном времени #

В этом руководстве показано, как использовать citus для приёма данных о событиях и выполнения аналитических запросов к этим данным в режиме реального времени. Для этого в примере используется образец набора данных событий GitHub.

Примечание

В этом руководстве предполагается, что расширение citus уже установлено и работает. Если это не так, обратитесь к Подразделу H.5.2.1, чтобы настроить его локально.

H.5.4.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
H.5.4.2.2. Создание таблиц #

Для начала подключитесь к узлу-координатору citus с помощью psql.

Если citus установлен согласно описанию в Подразделе H.5.2.1, узел-координатор запускается и слушает порт 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);
H.5.4.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
H.5.4.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;

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

H.5.4.3. Микросервисы #

В этом руководстве показано, как использовать citus в качестве сервера-хранилища для нескольких микросервисов, а также приведён пример настройки и работы в таком кластере.

Примечание

В этом руководстве предполагается, что расширение citus уже установлено и работает. Если это не так, обратитесь к Подразделу H.5.2.1, чтобы настроить его локально.

H.5.4.3.1. Распределённые схемы #

Распределённые схемы можно перемещать внутри кластера citus. Система может перебалансировать их как единое целое между доступными узлами, что позволяет эффективно распределять ресурсы без ручного выделения.

По сути, каждый микросервис самостоятельно управляет своим слоем хранения, и не делается предположений относительно типов таблиц и данных, которые он будет создавать и хранить. Однако для каждого сервиса предоставляется отдельная схема и предполагается, что он использует отдельную роль для подключения к базе данных. При подключении пользователя его роль добавляется в начало search_path, поэтому если имя роли совпадает с именем схемы, не нужно вносить изменения в приложение для корректной установки search_path.

В примере используются три сервиса:

  • сервис user

  • сервис time

  • сервис ping

Для начала подключитесь к узлу-координатору citus с помощью psql.

Если citus установлен согласно описанию в Подразделе H.5.2.1, узел-координатор запускается и слушает порт 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');

    Данный метод позволяет преобразовывать существующие обычные схемы в распределённые.

    Примечание

    Распределять можно только схемы, не содержащие распределённых таблиц и таблиц-справочников.

  • Включить параметр конфигурации citus.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_shards;
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)
H.5.4.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
);
H.5.4.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
}

После внесения изменений сохраните все отредактированные файлы и переходите к следующему этапу запуска микросервисов.

H.5.4.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
H.5.4.3.5. Исследование базы данных #

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

SELECT * FROM citus_shards;
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)

Ожидается, что схемы перенесены и кластер стал более сбалансированным. Эта операция прозрачна для приложений, поэтому нет необходимости их перезапускать, и они продолжают обслуживать запросы.

H.5.5. Архитектурные понятия #

H.5.5.1. Узлы #

citus — это расширение Postgres Pro, которое обеспечивает координацию обычных серверов баз данных (называемых узлами) в архитектуре «без разделения ресурсов». Узлы образуют кластер, который позволяет Postgres Pro хранить больше данных и использовать больше процессорных ядер, чем это возможно на одном компьютере. Эта архитектура также позволяет масштабировать базу данных путём добавления новых узлов в кластер.

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

Узел-координатор либо направляет запрос на один рабочий узел, либо распараллеливает его между несколькими в зависимости от того, находятся ли необходимые данные на одном узле или на нескольких. Чтобы правильно это сделать, узел-координатор проверяет таблицы метаданных. В этих специальных таблицах citus отслеживаются DNS-имена и состояния рабочих узлов, а также распределение данных между узлами. За дополнительной информацией обратитесь к описанию таблиц и представлений citus.

H.5.5.2. Модели сегментирования #

Сегментирование — это метод, используемый в системах управления базами данных и распределённых вычислениях для горизонтального секционирования данных между несколькими серверами или узлами. При этом большая база данных или набор данных разбивается на небольшие, легко управляемые части, называемые сегментами. Каждый сегмент содержит подмножество данных, которые вместе образуют полный набор данных.

citus предлагает два типа сегментирования данных: на основе строк и на основе схем. Каждый вариант имеет собственные особенности сегментирования, что позволяет выбрать подход, который соответствует требованиям конкретного приложения.

H.5.5.2.1. Сегментирование на основе строк #

Традиционный способ сегментирования таблиц citus — это модель общей схемы в единой базе данных, также известная как сегментирование на основе строк, в которой арендаторы сосуществуют как строки одной таблицы. Арендатор определяется путём установления столбца распределения, который позволяет разделить таблицу горизонтально.

Это наиболее эффективный способ сегментирования с точки зрения использования оборудования. Арендаторы оптимально распределены по узлам кластера. Однако данный способ требует, чтобы у всех таблиц в схеме был столбец распределения и чтобы все запросы в приложении фильтровались по этому столбцу. Сегментирование на основе строк отлично подходит для рабочих нагрузок Интернета вещей и для достижения максимально эффективного использования оборудования.

Преимущества:

  • Высокая производительность

  • Эффективное распределение арендаторов по узлам

Недостатки:

  • Необходимость изменять схему

  • Необходимость изменять запросы приложений

  • У всех арендаторов должна быть одна схема

H.5.5.2.2. Сегментирование на основе схем #

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

Преимущества:

Недостатки:

  • На узле располагается меньше арендаторов, чем при сегментировании на основе строк

H.5.5.2.3. Особенности сегментирования #
Сегментирование на основе схемСегментирование на основе строк
Многоарендная модельОтдельная схема для каждого арендатораОбщие таблицы со столбцами идентификаторов арендаторов
Версия citus12.0+Все версии
Дополнительные шаги по сравнению с Postgres ProТолько изменение файла конфигурацииИспользуйте функцию create_distributed_table для каждой таблицы, чтобы распределить и совместить таблицы по tenant_id
Количество арендаторов1 – 10 0001–1 000 000+
Требования к разработке модели данныхОтсутствие внешних ключей в распределённых схемахНеобходимость наличия столбца tenant_id (столбец распределения, также называемый ключ сегментирования) в каждой таблице, а также в первичных и внешних ключах
Требования SQL для запросов к одному узлуИспользование одной распределённой схемы в каждом запросеВ соединения и предложения WHERE должен быть включён столбец tenant_id
Распараллеливание запросов между арендаторамиНетДа
Разные определения таблиц для каждого арендатораДаНет
Управление доступомРазрешения на доступ к схемамРазрешения на доступ к схемам
Обмен данными между арендаторамиДа, с помощью таблиц-справочников (в отдельной схеме)Да, с помощью таблиц-справочников
Изоляция арендаторов в сегментахУ каждого арендатора своя группа сегментов по определениюМожно назначать для определённых идентификаторов арендаторов собственные группы сегментов с помощью функции isolate_tenant_to_new_shard.

H.5.5.3. Распределённые данные #

H.5.5.3.1. Типы таблиц #

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

  • Тип 1: распределённые таблицы.

    Первый и наиболее распространённый тип — распределённые таблицы. SQL-запросы выполняются для них так же, как для обычных таблиц, но их особенность — в горизонтальном распределении по рабочим узлам. За подробностями обратитесь к рисунку ниже.

    Рисунок H.1. Распараллеливание команды SELECT


    Здесь строки таблицы table хранятся в таблицах table_1001, table_1002 и т. д. на рабочих узлах. Таблицы-компоненты на рабочих узлах называются сегментами.

    В citus не только SQL-операторы, но и DDL-операторы запускаются на уровне кластера, поэтому изменение схемы распределённой таблицы приводит к каскадному изменению всех сегментов таблицы на рабочих узлах.

    Чтобы узнать, как создавать распределённые таблицы, обратитесь к разделу Создание и изменение распределённых объектов (DDL).

    Столбец распределения. В citus используется алгоритмическое сегментирование для назначения строк сегментам. Это означает, что назначение выполняется детерминированно — в данном случае на основе значения определённого столбца таблицы, называемого столбцом распределения. Администратор кластера должен назначить такой столбец при распределении таблицы. Правильный выбор важен для производительности и функциональности, за подробностями обратитесь к Подразделу H.5.6.2.

  • Тип 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.

H.5.5.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, значение столбца распределения в строке хешируется и проверяется, какой диапазон сегмента содержит хешированное значение. (Диапазоны определены так, что образ хеш-функции является их несвязным объединением.)

H.5.5.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.

H.5.5.3.3. Совмещение #

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

Одним из примеров является база данных с магазинами, товарами и покупками. Если все три таблицы содержат столбец store_id и распределяются по нему, то все запросы, ограниченные одним хранилищем, могут эффективно выполняться на одном рабочем узле, даже если запрос содержит какую-либо комбинацию этих таблиц.

За подробным описанием и примерами обратитесь к разделу Совмещение таблиц.

H.5.5.3.4. Распараллеливание #

Распределение запросов по нескольким компьютерам позволяет выполнять больше запросов одновременно и увеличивает скорость обработки за счёт добавления новых компьютеров в кластер. Кроме того, разделение одного запроса на фрагменты, как описано в предыдущем разделе, значительно увеличивает выделяемую на запрос вычислительную мощность. Это, в свою очередь, обеспечивает эффективное распараллеливание, то есть использование всех ядер процессора.

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

В разделе Обработка запросов более подробно рассматривается разбиение запросов на фрагменты и управление их выполнением.

H.5.5.4. Выполнение запросов #

При выполнении многосегментных запросов citus должен балансировать выгоды от распараллеливания с накладными расходами от подключений к базам данных (сетевыми задержками и использованием ресурсов рабочего узла). Чтобы настроить оптимальное выполнение запросов citus в рабочей нагрузке конкретной базы данных, необходимо понимать, как citus управляет подключениями к базам данных между узлом-координатором и рабочими узлами.

В citus каждый входящий многосегментный запрос разбивается на запросы для каждого сегмента, называемые задачами. Задачи ставятся в очередь и запускаются, как только появляется возможность подключиться к соответствующим рабочим узлам. Запросы к распределённым таблицам foo и bar показаны на диаграмме управления подключениями.

Рисунок H.2. Управление подключениями


Для каждого сеанса у узла-координатора есть пул соединений. Для каждого запроса (например, 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 выполняет роль защитного механизма. Он ограничивает общее количество подключений для каждого рабочего узла во всех задачах.

H.5.6. Разработка #

H.5.6.1. Определение типа приложения #

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

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

H.5.6.1.1. Краткий обзор #
Многоарендные приложенияПриложения для анализа данных в реальном времени
От десятков до сотен таблиц в схемеМалое количество таблиц
Запросы, относящиеся к одному арендатору (предприятию/магазину)Относительно простые аналитические запросы с агрегированием
OLTP-нагрузки для обслуживания интернет-клиентовБольшой объём в основном неизменяемых входящих данных
OLAP-нагрузки для обслуживания аналитических запросов каждого клиентаРабота в основном с большой таблицей событий
H.5.6.1.2. Примеры и характеристики #
H.5.6.1.2.1. Многоарендные приложения #

Обычно это SaaS-приложения, которые обслуживают другие компании, учётные записи или организации. Большинство SaaS-приложений по сути являются реляционными. У них уже есть критерий, по которому можно распределять данные по узлам: сегментировать по tenant_id.

Расширение citus позволяет масштабировать базу данных до миллионов арендаторов без необходимости перепроектировать приложение. Можно сохранить необходимую реляционную семантику, такую как соединения, ограничения внешнего ключа, транзакции, принципы ACID и целостность.

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

  • Характеристики: запросы, относящиеся к одному арендатору, а не собирающие информацию по нескольким. Сюда входят OLTP-нагрузки для обслуживания интернет-клиентов и OLAP-нагрузки, которые обслуживают аналитические запросы для каждого клиента. Наличие десятков или сотен таблиц в схеме базы данных также является показателем многоарендной модели данных.

Для масштабирования многоарендного приложения с помощью citus также требуются минимальные изменения в коде приложения. citus поддерживает популярные платформы: Ruby on Rails и Django.

H.5.6.1.2.2. Анализ данных в реальном времени #

Обычно это приложения, которым требуется существенное распараллеливание, координирующее сотни ядер для быстрого получения результатов по числовым, статистическим или счётным запросам. Сегментирование и распараллеливание SQL-запросов на несколько узлов позволяет citus выполнять запросы к миллиардам записей в реальном времени менее чем за секунду.

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

  • Характеристики: несколько таблиц, которые часто сосредоточены вокруг большой таблицы событий, связанных с устройством, сайтом или пользователем, при этом поступает большой объём преимущественно неизменяемых данных. Относительно простые (но с большим объёмом вычислений) аналитические запросы, включающие несколько агрегатов и операции GROUP BY.

Если ваш случай похож на один из описанных выше, то на следующем этапе необходимо решить, как сегментировать данные в кластере citus. Как описано в Подраздел H.5.5, в citus строки таблицы назначаются сегментам в соответствии с хешированным значением столбца распределения таблицы. Для обеспечения лучшей производительности администратор баз данных должен выбирать столбцы распределения, соответствующие сценариям использования типичных запросов.

H.5.6.2. Выбор столбца распределения #

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

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

В этом разделе даны советы по выбору столбцов распределения для двух наиболее распространённых сценариев citus. В заключении главы подробно рассматривается «совмещение» — предпочтительное группирование данных на узлах.

H.5.6.2.1. Многоарендные приложения #

В многоарендной архитектуре используется форма иерархического моделирования базы данных для распределения запросов по узлам распределённого кластера. Вершина иерархии данных называется tenant_id и должна храниться в столбце каждой таблицы. В citus проверяется, какое значение tenant_id используется в запросах, и каждый запрос направляется для обработки на один рабочий узел, в частности на узел, который содержит фрагмент данных, связанный с этим tenant_id. Выполнение запроса со всеми соответствующими данными, размещёнными на одном узле, называется совмещением.

На следующей диаграмме показано совмещение в многоарендной модели данных. Она содержит две таблицы: «Accounts» (Учётные записи) и «Campaigns» (Кампании), каждая из которых распределена по account_id. Сегменты обозначены как прямоугольники, цвет которых соответствует цвету содержащего их рабочего узла. Зелёные сегменты хранятся вместе на одном рабочем узле, а синие — на другом. Обратите внимание, что запрос соединения учётных записей и кампаний будет содержать все необходимые данные на одном узле с выборкой по одному account_id.

Рисунок H.3. Совмещение в многоарендной модели


Чтобы применить этот подход к собственной схеме, сначала определите, кто в приложении будет арендатором. Обычно это предприятие, учётная запись, организация или клиент. При этом имя столбца будет примерно таким: company_id или customer_id. Следует проанализировать каждый запрос и ответить на вопрос: сработает ли такой запрос с дополнительным предложением WHERE, ограничивающим все задействованные таблицы строками с одним и тем же tenant_id? Запросы в многоарендной модели обычно ограничиваются арендатором, например, запросы о продажах или наличии товара будут ограничены определённым магазином.

Практические рекомендации:

  • Разделяйте распределённые таблицы по общему столбцу tenant_id. Например, в SaaS-приложении, где арендаторами являются предприятия, роль tenant_id будет выполнять company_id.

  • Преобразовывайте небольшие таблицы на несколько арендаторов в таблицы-справочники. Если несколько арендаторов совместно используют небольшую таблицу данных, распределите её как таблицу-справочник.

  • Фильтруйте все запросы приложения по tenant_id. Каждый запрос должен обращаться только к одному арендатору.

H.5.6.2.2. Приложения для анализа данных в реальном времени #

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

В качестве столбца распределения в многоарендной модели используется идентификатор арендатора, а в модели реального времени — «идентификатор объекта». Типичными объектами являются пользователи, узлы или устройства.

Запросы в реальном времени обычно содержат агрегирования числовых данных, сгруппированные по дате или категории. В citus эти запросы отправляются каждому сегменту для получения части результата, и итоговый результат собирается на узле-координаторе. Запросы выполняются быстрее, когда задействовано максимальное количество узлов и ни один узел не выполняет непропорциональный объём работы.

Практические рекомендации:

  • Выбирайте столбец с большим количеством строк в качестве столбца распределения. Для сравнения: поле «статус» в таблице заказов со значениями «новый», «оплачено» и «отправлено» — плохой выбор для столбца распределения, поскольку оно принимает только эти несколько значений. Количество различных значений ограничивает количество сегментов, которые могут хранить данные, и количество узлов, которые могут их обрабатывать. Среди столбцов с большим количеством строк полезно дополнительно выбрать те, которые часто используются в предложениях группирования или в качестве ключей соединения.

  • Выбирайте столбец с равномерным распределением. Если распределить таблицу по столбцу, ориентированному на некоторые общие значения, то данные в таблице будут накапливаться в конкретных сегментах. Узлы, содержащие эти сегменты, в конечном итоге будут выполнять больше работы, чем прочие.

  • Распределяйте таблицы фактов и измерений по общим столбцам. У таблицы фактов может быть только один ключ распределения. Таблицы, соединяющиеся по другому ключу, не будут совмещаться с таблицей фактов. Выберите один ключ для совмещения в зависимости от частоты соединения и размера соединяемых строк.

  • Преобразуйте некоторые таблицы измерений в таблицы-справочники. Если таблицу измерений невозможно совместить с таблицей фактов, можно повысить производительность запросов, распределив копии таблицы измерений по всем узлам в виде таблицы-справочника.

H.5.6.2.3. Данные временных рядов #

При использовании временных рядов приложения запрашивают последнюю информацию, одновременно архивируя старую информацию.

Самая распространённая ошибка при моделировании информации временных рядов в citus — использование самой метки времени в качестве столбца распределения. При использовании хеш-распределения на основе времени система будет распределять значения временных меток случайным образом по разным сегментам, а не сохранять диапазоны времени в сегментах вместе. Однако запросы по времени обычно ссылаются на диапазоны времени (например, последние данные за неделю), поэтому такое хеш-распределение может привести к увеличению сетевого трафика.

Практические рекомендации:

  • Не выбирайте временную метку в качестве столбца распределения. В многоарендном приложении используйте tenant_id, а в приложении для анализа данных в реальном времени используйте entity_id.

  • Для запросов по времени используйте секционирование таблиц Postgres Pro, чтобы разбить большую таблицу упорядоченных по времени данных на несколько дочерних таблиц, каждая из которых содержит разные временные диапазоны. При распределении секционированной таблицы Postgres Pro в citus создаются сегменты для дочерних таблиц.

H.5.6.2.4. Совмещение таблиц #

Благодаря гибкости и надёжности реляционные базы данных являются очевидным выбором для хранения данных во многих приложениях. Исторически одним из недостатков реляционных баз данных было то, что они могли работать только на одном компьютере, что накладывало внутренние ограничения, когда необходимость хранения данных опережала развитие аппаратных возможностей. Решением для быстрого масштабирования баз данных стало распределение, но при этом возникла проблема производительности: реляционные операции, такие как соединения, затем должны пересекать границы сети. Совмещение — это практика тактического разделения данных, при которой связанная информация хранится на одних и тех же компьютерах для обеспечения эффективности реляционных операций, но при этом используются преимущества горизонтального масштабирования для всего набора данных.

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

H.5.6.2.4.1. Совмещение данных в citus для хеш-распределённых таблиц #

Расширение citus для Postgres Pro позволяет формировать распределённую базу данных из нескольких баз данных. Каждый узел в кластере citus представляет собой полнофункциональную базу данных Postgres Pro, а расширение добавляет возможность работы с единой однородной базой данных. Хотя оно не обеспечивает полную функциональность Postgres Pro в распределённом виде, во многих случаях можно в полной мере использовать преимущества функциональности Postgres Pro на одном компьютере посредством совмещения, включая полную поддержку SQL, транзакций и внешних ключей.

В citus строка сохраняется в сегменте, если хеш значения в столбце распределения попадает в хеш-диапазон сегмента. Чтобы обеспечить совмещение, сегменты с одинаковым хеш-диапазоном всегда размещаются на одном и том же узле даже после перебалансировки, так что одинаковые значения столбца распределения в разных таблицах всегда находятся на одном и том же узле. За дополнительной информацией обратитесь к рисунку ниже.

Рисунок H.4. Сегменты совмещения


Хорошо применимый на практике столбец распределения, как видно из примера, — это tenant_id в многоарендных приложениях. Например, у SaaS-приложений обычно множество арендаторов, но каждый выполняемый ими запрос относится к одному конкретному. Одним из вариантов реализации является предоставление базы данных или схемы для каждого арендатора, но он часто затратен и непрактичен, поскольку может включать множество операций, охватывающих нескольких пользователей (загрузка данных, миграция, агрегирование, аналитика, изменение схемы, резервное копирование и т. д.). С ростом числа арендаторов управлять такой реализацией будет всё сложнее.

H.5.6.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, за последнюю неделю».

H.5.6.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;

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

H.5.6.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;

После этого результаты двух шагов должны быть объединены в приложении.

Данные, необходимые для ответа на запрос, разбросаны по сегментам на разных узлах, и придётся отправить запрос каждому из этих сегментов. За подробностями обратитесь к рисунку ниже.

Рисунок H.5. Совмещение и пример неэффективных запросов


В данном случае распределение данных привносит существенные недостатки:

  • Накладные расходы на запрос к каждому сегменту и выполнение нескольких запросов.

  • Накладные расходы запроса Q1, возвращающего клиенту большое количество строк.

  • Запрос Q2 становится слишком большим.

  • Необходимость писать запросы в несколько этапов, объединять результаты, вносить изменения в приложение.

Потенциальным преимуществом распределения данных является возможность распараллеливания запросов, как в citus. Однако это выгодно только в том случае, если объём работы, выполняемой запросом, существенно превышает накладные расходы на запросы к нескольким сегментам. Как правило, лучше избегать такой большой нагрузки непосредственно из приложения, например с помощью предварительного агрегирования данных.

H.5.6.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. За подробностями обратитесь к рисунку ниже.

Рисунок H.6. Совмещение и пример эффективных запросов


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

Хотя в приведённом выше примере запрос отправляется только одному узлу, поскольку существует конкретный фильтр tenant_id = 6, совмещение также позволяет эффективно выполнять распределённые соединения по tenant_id между всеми узлами, даже с SQL-ограничениями.

H.5.6.2.4.6. Совмещение и поддержка функциональности #

Полный список возможностей citus, доступных при использовании совмещения:

  • Полная поддержка SQL для запросов к одному набору совмещённых сегментов.

  • Поддержка транзакций с несколькими операторами для внесения изменений в один набор совмещённых сегментов.

  • Агрегирование с помощью INSERT...SELECT.

  • Внешние ключи.

  • Распределённые внешние соединения.

  • Вынос наружу общих табличных выражений.

Совмещение данных — мощный метод обеспечения горизонтального масштабирования и поддержки реляционных моделей данных. Стоимость миграции или создания приложений с использованием распределённой базы данных, обеспечивающей реляционные операции посредством совмещения, часто существенно ниже, чем переход к ограничительной модели данных (например, NoSQL), и, в отличие от одноузловой базы данных, её можно масштабировать по мере роста бизнеса. Миграция существующей базы данных более подробно описана в Подразделе H.5.6.3.

H.5.6.2.4.7. Производительность запросов #

В citus входящие запросы распараллеливаются и разбиваются на несколько запросов-фрагментов («задач»), которые выполняются параллельно в сегментах рабочего узла. Это позволяет citus использовать вычислительную мощность всех узлов кластера, а также отдельных ядер на каждом узле для каждого запроса. Благодаря такому распараллеливанию можно получить производительность, которая складывается из вычислительной мощности всех ядер кластера, что приводит к значительному сокращению времени выполнения запросов по сравнению с Postgres Pro на одном сервере.

В citus при планировании SQL-запросов используется двухэтапный оптимизатор. На первом этапе SQL-запросы преобразуются в коммутативную и ассоциативную форму, чтобы их можно было передавать и выполнять на рабочих узлах параллельно. Как обсуждалось в предыдущих разделах, выбор правильного столбца и метода распределения позволяет планировщику распределённых запросов применять несколько оптимизаций. Это может существенно повлиять на производительность запросов за счёт снижения сетевого ввода-вывода.

Распределённый исполнитель расширения citus затем отправляет эти фрагменты запроса рабочим экземплярам Postgres Pro. Некоторые параметры как распределённого планировщика, так и исполнителя можно настроить для повышения производительности. Когда фрагменты запроса отправляются рабочим узлам, начинается второй этап оптимизации. Рабочие узлы запускают серверы Postgres Pro и применяют стандартную логику планирования и исполнения Postgres Pro для выполнения этих фрагментов SQL-запросов. Таким образом, любая оптимизация, которая помогает Postgres Pro, также помогает и citus. В Postgres Pro параметры использования ресурсов по умолчанию имеют неоптимальные значения, так что их оптимизация может значительно сократить время выполнения запросов.

H.5.6.3. Миграция существующего приложения #

Для миграции существующего приложения в citus иногда требуется настройка схемы и запросов для достижения оптимальной производительности. Расширение citus предоставляет для Postgres Pro функциональность распределения, но сегментирование на основе строк не является универсальным решением, позволяющим масштабировать все рабочие нагрузки. Высокая производительность кластера citus предполагает тщательную разработку модели данных, выбор инструментов и используемых SQL-функций.

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

  1. Первые шаги направлены на оптимизацию существующей схемы базы данных для эффективной работы на нескольких компьютерах.

  2. На следующем шаге код приложения и запросы модифицируются для работы с изменённой схемой.

  3. После тестирования изменений в среде разработки последним шагом будет перенос производственных данных в кластер citus и переключение рабочего приложения. Время простоя на данном этапе может быть минимизировано.

H.5.6.3.1. Определение стратегии распределения #
H.5.6.3.1.1. Выбор ключа распределения #

Первым шагом при миграции в citus является определение подходящих ключей распределения и соответствующее планирование распределения таблиц. В многоарендных приложениях это обычно идентификаторы арендаторов самого приложения, в citus называемый tenant_ID. Варианты использования могут различаться, поэтому рекомендуется тщательно продумать этот шаг.

За дополнительными рекомендациями обратитесь к следующим разделам:

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

H.5.6.3.1.2. Определение типов таблиц #

После выбора ключа распределения изучите схему, чтобы определить, как будет обрабатываться каждая таблица и потребуются ли какие-либо изменения в структурах таблиц.

Как правило, таблицы попадают в одну из следующих категорий:

  • Готовые к распределению. Такие таблицы уже содержат ключ распределения.

  • Требующие заполнения. Такие таблицы могут логически распределяться по выбранному ключу, но не содержат явно ссылающегося на него столбца. Соответствующий столбец будет добавлен в них позднее.

  • Таблицы-справочники. Такие таблицы обычно небольшого размера, не содержат ключа распределения, соединяются по распределённым таблицам и /или являются общими для арендаторов. Копия каждой такой таблицы хранится на всех узлах. Примеры таких таблиц: поиск по кодам стран, категориям продуктов и т. п.

  • Локальные таблицы. Такие таблицы обычно не соединяются с другими таблицами и не содержат ключа распределения. Они хранятся только на узле-координаторе. Примеры: поиск по пользователям и прочие вспомогательные таблицы.

Рассмотрим пример многоарендного приложения, подобного Etsy или Shopify, где каждый арендатор — это магазин. Упрощённая схема представлена на диаграмме ниже. (Подчёркнутые элементы — первичные ключи, а выделенные курсивом элементы — внешние ключи.)

Рисунок H.7. Пример упрощённой схемы


В этом примере магазины являются естественными арендаторами. В данном случае tenant_id — это store_id. После распределения таблиц в кластере строки, относящиеся к одному и тому же хранилищу, должны располагаться вместе на одних и тех же узлах.

H.5.6.3.2. Подготовка исходных таблиц к миграции #

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

H.5.6.3.2.1. Добавление ключей распределения #

В нашем примере с витриной у таблиц магазинов и продуктов есть store_id, и они готовы к распределению. После нормализации в таблице line_items отсутствует store_id, но он нужен для распределения.

-- Денормализация таблицы line_items путём добавления столбца store_id

ALTER TABLE line_items ADD COLUMN store_id uuid;

Обязательно проверьте, что столбец распределения имеет один и тот же тип во всех таблицах, например не смешивайте типы int и bigint. Типы столбцов должны совпадать для обеспечения правильного совмещения данных.

H.5.6.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);
H.5.6.3.3. Подготовка приложения для работы с citus #
H.5.6.3.3.1. Настройка кластера разработки citus #

При изменении приложения для работы с citus нужна тестовая база данных. Чтобы настроить расширение, используйте инструкции в Подразделе H.5.2.1.

Затем создайте резервную копию схемы БД пользовательского приложения и восстановите схему в новой БД разработки.

# получение схемы из исходной БД

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 для схемы, обязательно подготовьте исходные таблицы к миграции.

H.5.6.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;

После завершения схема из предыдущего раздела будет выглядеть так (подчёркнутые элементы — первичные ключи, выделенные курсивом элементы — внешние ключи):

Рисунок H.8. Пример упрощённой схемы


Обязательно измените потоки данных, чтобы добавить к входящим данным ключи.

H.5.6.3.3.2. Добавление в запросы ключа распределения #

Как только ключ распределения появится во всех соответствующих таблицах, приложение должно включить его в запросы. Выполните следующие шаги, используя копию приложения в среде разработки, и протестируйте её на сервере citus. После работы приложения с расширением станет понятно, как перенести производственные данные из исходной базы данных в реальный кластер citus.

  • Необходимо изменить код приложения и любые другие процессы приёма данных, записывающие данные в таблицы, чтобы включить новые столбцы.

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

  • Рекомендуется включить журналирование базы данных. Журналы могут помочь обнаружить затерявшиеся межсегментные запросы в многоарендном приложении, которые следует преобразовать в запросы для каждого арендатора.

Межсегментные запросы также поддерживаются, но в многоарендном приложении большинство запросов должно направляться на один узел. Для простых запросов SELECT, UPDATE и DELETE это означает, что предложение WHERE должно фильтроваться по tenant_id. Тогда citus сможет эффективно выполнять эти запросы на одном узле.

Существуют вспомогательные библиотеки для ряда популярных платформ, которые позволяют легко добавлять tenant_id в запросы:

Библиотеки можно использовать сначала для записи данных в базу (включая поглощение данных), а затем для чтения. Например, в пакете activerecord-multi-tenant есть файл write-only mode (режим только для записи), который изменяет только пишущие запросы.

H.5.6.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'
H.5.6.3.3.3. Включение безопасных соединений #

Клиенты должны подключаться к citus с помощью SSL, чтобы защитить информацию и предотвратить атаки посредника.

H.5.6.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.

H.5.6.3.4. Перенос производственных данных #

На данном этапе, после изменения схемы базы данных и запросов приложения для работы с citus, можно приступать к последнему шагу: переносу данных в кластер citus и переключению приложения на новую базу данных. Процедура миграции данных представлена в разделе Миграция базы данных.

H.5.6.3.4.1. Миграция базы данных #

Для небольших сред, допускающих незначительный простой, используйте процессы pg_dump/pg_restore. Для этого выполните следующие шаги:

  1. Сохраните структуру БД из БД разработки:

    pg_dump \
       --format=plain \
       --no-owner \
       --schema-only \
       --file=schema.sql \
       --schema=целевая_схема \
       postgres://user:pass@host:5432/db
  2. Подключитесь к кластеру citus с помощью psql и создайте схему:

    \i schema.sql
  3. Вызовите функции create_distributed_table и create_reference_table. Если при этом выдаётся сообщение об ошибке, связанной с внешними ключами, обычно это вызвано порядком операций. Удалите внешние ключи перед распределением таблиц, а затем добавьте их заново.

  4. Переведите приложение в режим обслуживания и исключите любые операции записи в старую базу данных.

  5. Сохраните данные из исходной производственной базы данных на диск с помощью pg_dump:

    pg_dump \
       --format=custom \
       --no-owner \
       --data-only \
       --file=data.dump \
       --schema=целевая_схема \
       postgres://user:pass@host:5432/db
  6. Импортируйте данные в citus с помощью pg_restore:

    # обратите внимание, что используются данные подключения к citus,
    # а не к исходной БД
    pg_restore  \
       --host=узел \
       --dbname=имя_БД \
       --username=имя_пользователя \
       data.dump
    
    # будет запрошен пароль для подключения
  7. Протестируйте приложение.

H.5.6.4. Справка по SQL #

H.5.6.4.1. Создание и изменение распределённых объектов (DDL) #
H.5.6.4.1.1. Создание и распределение схем #

В citus поддерживается сегментирование на основе схем, позволяющее распределять схемы. Распределённые схемы автоматически связываются с отдельными группами совмещения, так что таблицы, созданные в этих схемах, будут автоматически преобразованы в совмещённые распределённые таблицы без ключа сегментирования.

Есть два способа распределения схемы в citus:

  • Вызовом функции citus_schema_distribute вручную:

    SELECT citus_schema_distribute('user_service');

    Данный метод позволяет преобразовывать существующие обычные схемы в распределённые.

    Примечание

    Распределять можно только схемы, не содержащие распределённых таблиц и таблиц-справочников.

  • Включением параметра конфигурации citus.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 будут перемещены с текущего узла обратно на узел-координатор в кластере.

H.5.6.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.

H.5.6.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('имя_таблицы');
H.5.6.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 и могут вызывать нарушения ограничений на узле-координаторе.

H.5.6.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 показана принадлежность таблиц к группам.

H.5.6.4.1.4. Удаление таблиц #

Чтобы удалить распределённую таблицу, можно использовать стандартную команду Postgres Pro DROP TABLE. Как и в случае с обычными таблицами, команда DROP TABLE удаляет все индексы, правила, триггеры и ограничения, существующие для целевой таблицы, а также удаляет сегменты на рабочих узлах и очищает их метаданные.

DROP TABLE github_events;
H.5.6.4.1.5. Изменение таблиц #

В citus многие виды DDL-операторов автоматически транслируются, то есть при изменении распределённой таблицы на узле-координаторе также изменяются сегменты на рабочих узлах. Другие DDL-операторы требуется транслировать вручную, а некоторые другие запрещены, например изменяющие столбец распределения. Попытка запустить DDL-оператор, который нельзя транслировать автоматически, вызовет ошибку, и таблицы на узле-координаторе не изменятся.

Ниже приводится справочник на категории DDL-операторов, которые транслируются автоматически. Обратите внимание, что трансляцию можно включить или отключить с помощью параметра конфигурации citus.enable_ddl_propagation.

H.5.6.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, изменить старый столбец и переключиться обратно.

H.5.6.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;
H.5.6.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.

H.5.6.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);
H.5.6.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, чтобы отключить распараллеливание для каждого узла. Загрузка данных в той же транзакции может происходить медленнее.

H.5.6.4.1.7. Внесение изменений вручную #

Большинство DDL-команд транслируется автоматически. Для любых других можно транслировать изменения вручную, см. раздел Ручная трансляция запросов.

H.5.6.4.2. Внесение и изменение данных (DML) #
H.5.6.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'
  );
H.5.6.4.2.1.1. Распределённые свёртки #

В citus также поддерживаются операторы INSERT… SELECT, вставляющие строки на основе результатов запроса SELECT. Это удобный способ заполнения таблиц, который также позволяет выполнять команду UPSERT с помощью предложения ON CONFLICT, что является самым простым примером выполнения распределённой свёртки.

В citus существует три способа вставки с помощью оператора SELECT:

  • Первый можно использовать, если исходные таблицы и целевая таблица совмещены и оба оператора SELECT/INSERT включают столбец распределения. В этом случае в citus можно вынести наружу оператор INSERT… SELECT для параллельного выполнения на всех узлах.

  • Второй способ выполнения оператора INSERT… SELECT — это пересекционирование результатов итогового набора на порции и отправка этих порций на рабочие узлы в соответствующие сегменты целевой таблицы. Каждый рабочий узел может вставлять значения в локальные целевые сегменты.

    Оптимизация пересекционирования может произойти, если запрос SELECT не требует этапа слияния на узле-координаторе. Она не работает со следующей функциональностью SQL, для которой требуется этап слияния:

    • ORDER BY

    • LIMIT

    • OFFSET

    • GROUP BY, когда столбец распределения не является частью ключа группирования

    • Оконные функции при секционировании не по столбцу распределения в исходной таблице (таблицах)

    • Соединения между несовмещёнными таблицами (т. е. соединения с пересекционированием)

  • Если исходная и целевая таблицы не совмещены и пересекционирование применить невозможно, citus использует третий способ выполнения INSERT… SELECT. При этом результаты запросов к рабочим узлам передаются на узел-координатор. Узел-координатор перенаправляет строки обратно в соответствующий сегмент. Поскольку все данные должны проходить через один узел, этот метод менее эффективен.

Чтобы узнать, какой способ использует citus в данный момент, используйте команду EXPLAIN. Если в целевой таблице очень большое количество сегментов, возможно, стоит отключить пересекционирование, см. citus.enable_repartitioned_insert_select.

H.5.6.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, а именно помечает размещение как неактивное, если нет других активных размещений. Если после подключения происходит какой-либо другой сбой, транзакция отменяется и, следовательно, метаданные не изменяются.

H.5.6.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;
H.5.6.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;

Эта функция поддерживается только для хеш-распределённых таблиц и таблиц-справочников.

H.5.6.4.3.2. Повышение производительности записи #

Операторы INSERT и UPDATE/DELETE можно масштабировать примерно до 50 000 запросов в секунду на мощных компьютерах. Однако для достижения такой скорости придётся использовать множество параллельных долговременных соединений с учётом блокировок.

H.5.6.4.4. Запросы к распределённым таблицам (SQL) #

Как обсуждалось в предыдущих разделах, citus расширяет возможности Postgres Pro для выполнения распределённых вычислений. Это означает, что можно использовать стандартные запросы Postgres Pro SELECT на координаторе citus. Затем расширение распараллеливает запросы SELECT, включающие сложные выборки, группировки и упорядочивания, а также JOIN, чтобы ускорить их выполнение. На высоком уровне citus разбивает запрос SELECT на более мелкие фрагменты, назначает их рабочим узлам, контролирует их выполнение, объединяет результаты (и сортирует их, если необходимо) и возвращает итоговый результат пользователю.

В следующих разделах будут рассматриваться различные типы запросов, которые можно выполнять в citus.

H.5.6.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';

Обратите внимание, что отключение стратегии агрегирования на узле-координаторе вообще не позволит выполнять агрегированные запросы «третьего типа».

H.5.6.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 на рабочих узлах в результате по сути создают перекрёстное произведение строк, которые должны быть переданы координатору.

Для повышения производительности вместо этого можно выполнить приблизительный подсчёт. Выполните следующие шаги:

  1. Загрузите и установите расширение hll на всех экземплярах Postgres Pro (узел-координатор и все рабочие узлы).

    Найти расширение hll можно в репозитории проекта на GitHub.

  2. Создайте расширение hll на всех экземплярах Postgres Pro, выполнив указанную команду на координаторе:

    CREATE EXTENSION hll;
  3. Включите приближение функций 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).

H.5.6.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.

H.5.6.4.4.1.3. Процентильные вычисления #

Поиск точного процентиля по большому количеству строк может быть чрезвычайно затратным, поскольку все строки необходимо передать узлу-координатору для окончательной сортировки и обработки. С другой стороны, поиск приближённых значений можно выполнять параллельно на рабочих узлах, используя так называемый потоковый алгоритм приближения. Затем узел-координатор объединяет сжатые сводки в окончательный результат, а не читает полные строки.

Популярный алгоритм для процентилей использует сжатую структуру данных под названием t-digest, и доступен для Postgres Pro в расширении tdigest. В citus есть встроенная поддержка этого расширения.

Пример использования tdigest в citus:

  1. Загрузите и установите расширение tdigest на всех узлах Postgres Pro (узле-координаторе и всех рабочих узлах). За инструкциями по установке обратитесь к репозиторию расширения tdigest на сайте GitHub.

  2. Создайте расширение tdigest в базе данных и выполните следующую команду на узле-координаторе:

    CREATE EXTENSION tdigest;

    Узел-координатор передаст команду рабочим узлам.

Когда в запросах используются какие-либо определённые в расширении агрегатные функции, в citus переписываются запросы, чтобы по возможности передавать частичные вычисления tdigest рабочим узлам.

Точностью tdigest можно управлять с помощью аргумента compression, передаваемого в агрегатных функциях. Компромисс — это точность и объём данных, которыми обмениваются рабочие узлы и координатор. Подробное описание использования агрегатных функций в tdigest можно найти в документации расширения.

H.5.6.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;
H.5.6.4.4.3. Представления по распределённым таблицам #

В citus поддерживаются все представления по распределённым таблицам. За дополнительной информацией о синтаксисе и функциональности представлений обратитесь к CREATE VIEW.

Обратите внимание, что некоторые представления приводят к менее эффективным планам запросов.

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

H.5.6.4.4.4. Соединения #

В citus поддерживаются эквивалентные соединения между любым количеством таблиц, независимо от их размера и метода распределения. Планировщик запросов выбирает оптимальный метод и порядок соединения в зависимости от того, как распределены таблицы. Он оценивает несколько возможных порядков соединения и создаёт план соединения, который требует передачи минимального объёма данных по сети.

H.5.6.4.4.4.1. Совмещённые соединения #

Когда две таблицы совмещены, их можно эффективно объединить в общих столбцах распределения. Совмещённое соединение — наиболее эффективный способ соединения двух больших распределённых таблиц.

Внутри citus узел-координатор узнаёт, какие фрагменты совмещённых таблиц могут совпадать с фрагментами другой таблицы, просматривая метаданные столбца распределения. Это позволяет citus отсекать пары сегментов, которые не могут создавать совпадающие ключи соединения. Соединения между оставшимися парами сегментов выполняются параллельно на рабочих узлах, а затем результаты возвращаются координатору.

Примечание

Убедитесь, что таблицы распределены на одинаковое количество сегментов и что столбцы распределения каждой таблицы имеют точно совпадающие типы. Попытка объединить столбцы немного разных типов, например int и bigint, может вызвать ошибку.

H.5.6.4.4.4.2. Соединения таблиц-справочников #

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

Таблицы-справочники также можно соединять с локальными таблицами на узле-координаторе.

H.5.6.4.4.4.3. Соединения с пересекционированием #

В некоторых случаях может потребоваться соединить две таблицы по столбцам, не являющимся столбцами распределения. В таких случаях citus также позволяет объединять столбцы ключей, не предназначенных для распределения, путём динамического пересекционирования таблиц для запроса.

В таких случаях таблицы, подлежащие секционированию, определяются оптимизатором запросов на основе столбцов распределения, ключей соединения и размеров таблиц. Пересекционирование таблиц гарантирует, что соединяются только соответствующие пары сегментов, что значительно сокращает объём передаваемых по сети данных.

В целом, совмещённые соединения более эффективны, чем соединения с пересекционированием, поскольку последние требуют перестановки данных. Поэтому по возможности следует распределять таблицы по общим ключам соединения.

H.5.6.4.5. Обработка запросов #

Кластер citus состоит из экземпляра узла-координатора и нескольких экземпляров рабочих узлов. Данные сегментируются по рабочим узлам, а на узле-координаторе хранятся метаданные об этих сегментах. Все запросы к кластеру выполняются через узел-координатор. Он разбивает запрос на более мелкие фрагменты, где каждый фрагмент может выполняться независимо в отдельном сегменте. Затем узел-координатор назначает фрагменты рабочим узлам, контролирует их выполнение, объединяет их результаты и возвращает конечный результат пользователю. Краткое описание архитектуры обработки запросов представлено на диаграмме ниже.

Рисунок H.9. Архитектура обработки процессов


Конвейер обработки запросов citus содержит два компонента:

  • Планировщик и исполнитель распределённых запросов

  • Планировщик и исполнитель Postgres Pro

Они описаны более подробно в следующих разделах.

H.5.6.4.5.1. Планировщик распределённых запросов #

Планировщик распределённых запросов citus принимает SQL-запрос и планирует его для распределённого выполнения.

Для запросов SELECT планировщик сначала создаёт дерево плана входящего запроса и преобразует его в коммутативную и ассоциативную форму, чтобы его можно было распараллелить. Он также применяет несколько оптимизаций, чтобы обеспечить масштабируемость выполнения запросов и минимизацию сетевых операций ввода-вывода.

Далее планировщик разбивает запрос на две части: запрос, выполняемый на координаторе, и фрагменты запроса, которые выполняются на отдельных сегментах рабочих узлов. Затем планировщик назначает эти фрагменты запроса рабочим узлам для эффективного использования ресурсов. После этого шага план распределённого запроса передаётся исполнителю распределённых запросов для выполнения.

Процесс планирования поиска значений ключа в столбце распределения или запросов на изменение немного отличается, поскольку такие операции затрагивают ровно один сегмент. Как только планировщик получает входящий запрос, ему необходимо определить правильный сегмент, в который следует направить запрос. Для этого он извлекает столбец распределения во входящей строке и просматривает метаданные, чтобы определить правильный сегмент для запроса. Затем планировщик перезаписывает SQL этой команды, чтобы ссылаться на таблицу сегментов вместо исходной таблицы. Этот переписанный план затем передаётся исполнителю распределённых запросов.

H.5.6.4.5.2. Исполнитель распределённых запросов #

Исполнитель распределённых запросов в citus выполняет распределённые планы запросов и обрабатывает ошибки. Исполнитель хорошо подходит для получения быстрых ответов на запросы, включающие фильтры, агрегирования и совмещённые соединения, а также для выполнения одноарендных запросов с полной поддержкой SQL. Он открывает одно соединение на каждый сегмент для рабочих узлов по мере необходимости и отправляет им все фрагменты запросов. Затем он извлекает результаты каждого фрагмента, объединяет их и возвращает конечные результаты пользователю.

H.5.6.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, чтобы изучить выполнение распределённых запросов.

H.5.6.4.5.3. Планировщик и исполнитель Postgres Pro #

Как только исполнитель распределённых запросов отправляет фрагменты запроса рабочим узлам, они обрабатываются как обычные запросы Postgres Pro. Планировщик Postgres Pro этого рабочего узла выбирает наиболее оптимальный план для локального выполнения этого запроса в соответствующей таблице сегментов. Затем исполнитель Postgres Pro запускает этот запрос и возвращает результаты запроса исполнителю распределённых запросов. За подробностями о планировщике и исполнителе Postgres Pro обратитесь к соответствующим разделам документации. Наконец, исполнитель распределённых запросов передаёт результаты узлу-координатору для окончательного агрегирования.

H.5.6.4.6. Ручная трансляция запросов #

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

Однако способ разделения запросов на фрагменты (и какие запросы транслируются) зависит от типа запроса. В некоторых сложных ситуациях полезно управлять этим поведением вручную. В citus есть служебные функции для трансляции SQL на рабочие узлы, сегменты или места совмещения.

Ручная трансляция запросов обходит логику координатора, блокировки и любые другие проверки согласованности. Эти функции можно применять в крайних случаях для выполнения операторов, которые иначе недоступны в citus. Такие функции следует использовать с осторожностью, чтобы избежать несогласованности данных и взаимоблокировок.

H.5.6.4.6.1. Выполнение на всех рабочих узлах #

Наименее детализированный уровень исполнения — трансляция операторов для выполнения на всех рабочих узлах. Это полезно для просмотра свойств всех баз данных на рабочих узлах.

-- Вывод параметра work_mem каждой БД на рабочих узлах
SELECT run_command_on_workers($cmd$ SHOW work_mem; $cmd$);

Для выполнения на всех узлах, как рабочих, так и координаторе, используйте функцию run_command_on_all_nodes.

Примечание

Эту команду не следует использовать для создания объектов БД на рабочих узлах, так как это затруднит автоматическое добавление рабочих узлов.

Примечание

Функция run_command_on_workers и другие команды ручной трансляции в этом разделе могут выполнять только те запросы, которые возвращают один столбец и одну строку.

H.5.6.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$
);
H.5.6.4.6.3. Ограничения #
  • Нет защиты от взаимоблокировок для транзакций с несколькими операторами.

  • Нет защиты от сбоев во время выполнения запроса и возникающей в результате несогласованности.

  • Результаты запроса кешируются в памяти; эти функции не могут работать с очень большими наборами результатов.

  • Функции рано завершаются ошибкой, если не получается подключиться к узлу.

H.5.6.4.7. Поддержка SQL и обходные решения #

Поскольку citus является расширением Postgres Pro, он совместим с её конструкциями. Пользователи могут использовать инструменты и функциональность всей экосистемы Postgres Pro для распределённых таблиц, созданных с помощью citus.

В citus реализована 100% поддержка SQL для любых запросов, которые можно выполнить на одном рабочем узле. Запросы такого типа часто встречаются в многоарендных приложениях при доступе к информации об одном арендаторе.

Даже межузловые запросы (используемые для параллельных вычислений) поддерживают большую часть функциональности SQL. Однако некоторая часть функциональности SQL не поддерживается для запросов, объединяющих информацию из нескольких узлов.

H.5.6.4.7.1. Ограничения #
H.5.6.4.7.1.1. Общие #

Указанные ограничения применяются ко всем моделям операций:

  • Нет поддержки системы правил.

  • Не поддерживаются подзапросы в запросах INSERT.

  • Нет поддержки распределения многоуровневых секционированных таблиц.

  • Функции, используемые в запросах UPDATE к распределённым таблицам, не должны быть VOLATILE.

  • Функции STABLE, используемые в запросах UPDATE, не могут ссылаться на столбцы.

  • Нет поддержки изменения представлений для запросов, содержащих таблицы citus.

В citus идентификатор узла кодируется в последовательности, сгенерированной на каждом узле, это позволяет каждому отдельному узлу напрямую выполнять вставки без пересечения последовательностей. Однако этот метод не работает для последовательностей, меньших, чем bigint, что может привести к сбою вставки на рабочие узлы. В этом случае необходимо удалить столбец и добавить другой типа bigint или направлять вставки через координатор.

H.5.6.4.7.1.2. Межузловые SQL-запросы #
  • SELECT… FOR UPDATE работает только с запросами к одному сегменту.

  • TABLESAMPLE работает только с запросами к одному сегменту.

  • Связанные подзапросы поддерживаются только в том случае, если корреляция находится в столбце распределения.

  • Внешние соединения между распределёнными таблицами поддерживаются только по столбцу распределения.

  • Рекурсивные CTE работают только в запросах к одному сегменту.

  • Наборы группирования работают только в запросах к одному сегменту.

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

  • SQL-команда MERGE поддерживается для следующих комбинаций типов таблиц:

    ЦелеваяSourceПоддерживаетсяКомментарии

    Локальная

    Локальная

    Да

    Локальная

    Справка

    Да

    Локальная

    Распределённая

    Нет

    В разработке

    Распределённая

    Локальная

    Да

    Распределённая

    Распределённая

    Да

    Включая несовмещённые таблицы

    Распределённая

    Справка

    Да

    Справка

    Любая

    Нет

    Целевая таблица не может быть справочником

Подробную информацию о диалекте SQL-команд Postgres Pro (который может применяться пользователями citus в исходном виде) можно найти в Справке: «Команды SQL».

H.5.6.4.7.1.3. SQl-совместимость сегментирования на основе схем #

При использовании сегментирования на основе схем недоступна следующая функциональность:

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

  • На соединения между распределёнными схемами распространяются ограничения межузловых SQL-запросов.

  • Не поддерживается создание распределённых схем и таблиц в одном SQL-операторе.

H.5.6.4.7.2. Обходные решения #

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

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

H.5.6.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 всем рабочим узлам, которым они необходимы для обработки. Таким образом, лучше всего либо добавить во внутренний запрос максимально конкретные фильтры и ограничения, либо агрегировать таблицу, чтобы снизить возможную нагрузку на сеть, вызываемую таким запросом.

H.5.6.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

Создание временной таблицы на координаторе — это крайняя мера. Она ограничена размером диска и мощностью ЦП узла.

H.5.6.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';

H.5.6.5. API в citus #

H.5.6.5.1. Вспомогательные функции citus #

Этот раздел содержит справочную информацию о поддерживаемых в citus пользовательских функциях. Эти функции помогают обеспечить в citus дополнительную функциональность для распределения, помимо стандартных SQL-команд.

H.5.6.5.1.1. DDL таблиц и сегментов #
citus_schema_distribute (schemaname regnamespace) returns void #

Преобразует имеющиеся обычные схемы в распределённые схемы, которые автоматически связываются с отдельными группами совмещения, так что таблицы, созданные в этих схемах, будут автоматически преобразованы в совмещённые распределённые таблицы без ключа сегментирования. В процессе распределения схема будет автоматически назначена и перемещена на существующий узел кластера.

Аргументы:

  • schemaname — имя схемы, которая будет распределена.

В примере ниже показано, как распределить три схемы с именами tenant_a, tenant_b и tenant_c. За дополнительными примерами обратитесь к Подразделу H.5.4.3:

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 — имя схемы, которая будет распределена.

В приведённом ниже примере показано, как преобразовать три разные распределённые схемы обратно в обычные. За дополнительными примерами обратитесь к Подразделу H.5.4.3:

SELECT citus_schema_undistribute('tenant_a');
SELECT citus_schema_undistribute('tenant_b');
SELECT citus_schema_undistribute('tenant_c');
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 подразумевает неявное совмещение. Как поясняется в разделе Совмещение таблиц, это может оказаться полезным, если таблицы связаны или будут соединяться. Однако если две таблицы не связаны, но используют один и тот же тип данных для своих столбцов распределения, их случайное совмещение может снизить производительность во время перебалансировки сегментов. Сегменты таблицы будут без необходимости «каскадно» размещены вместе. Чтобы избавиться от этого неявного совмещения, можно использовать функцию 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.compression

  • columnar.compression_level

  • columnar.stripe_row_count

  • columnar.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'
);
H.5.6.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
--------------------
       10000002055
citus_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. Чтобы сбросить всю статистику, вызовите обе функции.

H.5.6.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_ological — использовать логическую репликацию, даже если таблица не имеет идентификатора реплики. Любые одновременные операторы изменения/удаления таблицы во время репликации завершатся ошибкой.

    • 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_ological — использовать логическую репликацию, даже если таблица не имеет идентификатора реплики. Любые одновременные операторы изменения/удаления таблицы во время репликации завершатся ошибкой.

    • 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_ological — использовать логическую репликацию, даже если таблица не имеет идентификатора реплики. Любые одновременные операторы изменения/удаления таблицы во время репликации завершатся ошибкой.

    • block_writes — использовать команду COPY (блокирующую запись) для таблиц, у которых нет первичного ключа или идентификатора реплики.

  • rebalance_strategy — имя стратегии в таблице pg_dist_rebalance_strategy. Если этот аргумент опущен, функция выбирает стратегию, указанную в таблице как стратегия по умолчанию. Этот необязательный аргумент по умолчанию имеет значение NULL.

Ниже показаны шаги по удалению одного узла (например, «10.0.0.1» на стандартном порту Postgres Pro):

  1. Переместите сегменты с этого узла.

    SELECT * FROM citus_drain_node('10.0.0.1', 5432);
  2. Подождите завершения операции.

  3. Удалите узел.

При перемещении сегментов с нескольких узлов рекомендуется использовать функцию citus_rebalance_start. Это позволит citus заранее планировать перемещение и выполнять его минимальное количество раз.

  1. Выполните указанный запрос на каждом удаляемом узле:

    SELECT * FROM citus_set_node_property(node_hostname, node_port, 'shouldhaveshards', false);
  2. Переместите с этих узлов все сегменты с помощью функции citus_rebalance_start:

    SELECT * FROM citus_rebalance_start(drain_only := true);
  3. Подождите, пока закончится перебалансировка.

  4. Удалите узлы.

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_ological — использовать логическую репликацию, даже если таблица не имеет идентификатора реплики. Любые одновременные операторы изменения/удаления таблицы во время репликации завершатся ошибкой.

    • 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                  │
└────────────────────────────┘
H.5.6.5.2. Таблицы и представления citus #
H.5.6.5.2.1. Метаданные узла-координатора #

В citus каждая распределённая таблица разделяется на несколько логических сегментов на основе столбца распределения. Затем узел-координатор ведёт таблицы метаданных для отслеживания статистики и информации о состоянии и расположении этих сегментов. В этом разделе описывается каждая такая таблица метаданных и их схема. После записи на узел-координатор эти таблицы можно просматривать и обращаться к ним с помощью SQL.

H.5.6.5.2.1.1. Таблица pg_dist_partition #

В таблице pg_dist_partition хранятся метаданные о распределённых таблицах. Для каждой распределённой таблицы также хранится информация о методе распределения и подробная информация о столбце распределения.

NameТипОписание
logicalrelidregclassРаспределённая таблица, которой соответствует эта строка. Это значение ссылается на столбец relfilenode в таблице системного каталога pg_class.
partmethodcharМетод секционирования/распределения. Значения этого столбца соответствуют различным методам распределения: h — хеш, n — таблица-справочник.
partkeytextПодробная информация о столбце распределения, включая номер столбца, тип и т. д.
colocationidintegerГруппа совмещения, к которой принадлежит эта таблица. К таблицам в одной группе можно применять совмещённое соединение и распределённые свёртки, а также другие оптимизации. Это значение ссылается на столбец colocationid в таблице pg_dist_colocation.
repmodelcharМетод репликации данных. Значения этого столбца соответствуют различным методам репликации: 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)
H.5.6.5.2.1.2. Таблица pg_dist_shard #

В таблице pg_dist_shard хранятся метаданные об отдельных сегментах таблицы. Они содержат информацию о том, какой распределённой таблице принадлежит сегмент, и статистику по столбцу распределения для этого сегмента. Метаданные сегментов хеш-распределённых таблиц представляют собой диапазоны хеш-токенов, назначенных этим сегментам. Эта статистика используется для устранения посторонних сегментов во время выполнения запросов SELECT.

NameТипОписание
logicalrelidregclassРаспределённая таблица, которой принадлежит данный сегмент. Это значение ссылается на столбец relfilenode в таблице системного каталога pg_class.
shardidbigintГлобальный уникальный идентификатор, присвоенный этому сегменту.
shardstoragecharТип хранения, используемый для этого сегмента. Различные типы хранения рассматриваются в таблице ниже.
shardminvaluetextДля таблиц с распределением по хешу — минимальное значение хеш-токена, назначенное этому сегменту (включительно).
shardmaxvaluetextДля таблиц с распределением по хешу — максимальное значение хеш-токена, назначенное этому сегменту (включительно).
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).
H.5.6.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 обратитесь к разделу Типы таблиц.

H.5.6.5.2.1.4. Таблица pg_dist_placement #

В таблице pg_dist_placement отслеживается расположение сегментов на рабочих узлах. Каждый сегмент, назначенный определённому узлу, называется размещением сегмента. В этой таблице хранится информация о состоянии и расположении каждого размещения сегмента.

NameТипОписание
placementidbigintУникальный автоматически сгенерированный идентификатор для каждого отдельного размещения.
shardidbigintИдентификатор сегмента, связанный с этим размещением. Это значение ссылается на столбец shardid в таблице каталога pg_dist_shard.
shardstateintОписывает состояние этого размещения. Различные состояния сегментов описаны в следующем разделе.
shardlengthbigintДля таблиц, распределённых по хешу — ноль.
groupidintИдентификатор, используемый для обозначения группы из одного ведущего сервера и нуля или более ведомых серверов.
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
H.5.6.5.2.1.5. Таблица pg_dist_node #

В таблице pg_dist_node содержится информация о рабочих узлах кластера.

NameТипОписание
nodeidintАвтоматически сгенерированный идентификатор отдельного узла.
groupidintИдентификатор, используемый для обозначения группы из одного ведущего сервера и нуля или более ведомых серверов. По умолчанию он равнозначен nodeid.
nodenametextИмя или IP-адрес рабочего узла Postgres Pro.
nodeportintНомер порта, через который рабочий узел Postgres Pro принимает подключения.
noderacktextИнформация о размещении стойки этого рабочего узла. Это необязательный столбец.
hasmetadatabooleanЗарезервирован для внутреннего использования.
isactivebooleanАктивен ли узел, принимающий размещения сегментов.
noderoletextЯвляется ли узел ведущим или ведомым.
nodeclustertextИмя кластера, содержащего этот узел.
metadatasyncedbooleanЗарезервирован для внутреннего использования.
shouldhaveshardsbooleanЕсли установлено значение 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)
H.5.6.5.2.1.6. Таблица citus.pg_dist_object #

В таблице citus.pg_dist_object содержится список объектов, например типов и функций, которые были созданы на узле-координаторе и распространены на рабочие узлы. При добавлении новых рабочих узлов в кластер citus автоматически создаёт копии распределённых объектов на новых узлах (в правильном порядке, чтобы соответствовать зависимостям объектов).

NameТипОписание
classidoidКласс распределённого объекта
objidoidИдентификатор (OID) распределённого объекта
objsubidintegerВложенный идентификатор распределённого объекта, например attnum
typetextЧасть стабильного адреса, используемая во время обновлений с помощью pg_upgrade
object_namestext[]Часть стабильного адреса, используемая во время обновлений с помощью pg_upgrade
object_argstext[]Часть стабильного адреса, используемая во время обновлений с помощью pg_upgrade
distribution_argument_indexintegerТолько для распределённых функций/процедур
colocationidintegerТолько для распределённых функций/процедур

«Стабильные адреса» однозначно идентифицируют объекты независимо от конкретного сервера. Во время обновления 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                |
H.5.6.5.2.1.7. Представление citus_schemas #

В citus поддерживается сегментирование на основе схем и есть представление citus_schemas, содержащее информацию о том, какие схемы в системе были распределены. В представлении показываются только распределённые схемы, но не локальные.

NameТипОписание
schema_nameregnamespaceИмя распределённой схемы
colocation_idintegerИдентификатор совмещения распределённой схемы
schema_sizetextСводная информация о размерах всех объектов в схеме в понятном человеку виде
schema_ownernameРоль, которой принадлежит схема

Например:

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
H.5.6.5.2.1.8. Представление citus_tables #

В представлении citus_tables содержится сводная информация обо всех таблицах, управляемых citus (распределённые и таблицы-справочники). В представлении объединена информация из таблиц метаданных citus, чтобы можно было проще исследовать параметры этих таблиц:

Например:

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          │
└────────────┴──────────────────┴─────────────────────┴───────────────┴────────────┴─────────────┴─────────────┴───────────────┘
H.5.6.5.2.1.9. Представление time_partitions #

В citus поддерживаются пользовательские функции для управления секциями в сценариях использования временных рядов, а также представление time_partitions для проверки управляемых секций.

В представлении есть следующие столбцы:

  • parent_table — секционированная таблица.

  • partition_column — столбец, по которому секционирована родительская таблица.

  • partition — имя секции таблицы.

  • from_value — нижняя граница времени для строк этой секции.

  • to_value — верхняя граница времени для строк этой секции.

  • access_methodheap для строкового хранения и 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          │
└────────────────────────┴──────────────────┴─────────────────────────────────────────┴─────────────────────┴─────────────────────┴───────────────┘
H.5.6.5.2.1.10. Таблица pg_dist_colocation #

В таблице pg_dist_colocation содержится информация о том, какие сегменты таблиц должны быть размещены вместе, или совмещены. Если две таблицы находятся в одной группе совмещения, в citus гарантируется, что сегменты с одинаковыми значениями секций будут размещены на одних и тех же рабочих узлах. Таким образом можно оптимизировать соединения, выполнять некоторые распределённые свёртки и поддерживать внешние ключи. Совмещение сегментов предполагается, когда количество сегментов и типы столбцов секционирования совпадают в двух таблицах; однако при создании распределённой таблицы можно указать пользовательскую группу совмещения, если необходимо.

NameТипОписание
colocationidintУникальный идентификатор группы совмещения, которому соответствует эта строка
shardcountintКоличество сегментов всех таблиц в этой группе совмещения
replicationfactorintКоэффициент репликации всех таблиц в этой группе совмещения. (Устарел)
distributioncolumntypeoidТип столбца распределения всех таблиц в этой группе совмещения
distributioncolumncollationoidПравило сортировки столбца распределения всех таблиц в этой группе совмещения
SELECT * FROM pg_dist_colocation;
  colocationid | shardcount | replicationfactor | distributioncolumntype | distributioncolumncollation
 --------------+------------+-------------------+------------------------+-----------------------------
             2 |         32 |                 1 |                     20 |                           0
  (1 row)
H.5.6.5.2.1.11. Таблица pg_dist_rebalance_strategy #

В этой таблице определяются стратегии, которые могут использоваться функцией citus_rebalance_start для определения целевого узла для перемещения сегментов.

NameТипОписание
namenameУникальное имя стратегии
default_strategybooleanДолжна ли функция citus_rebalance_start выбирать эту стратегию по умолчанию. Чтобы изменить этот столбец, используйте функцию citus_set_default_rebalance_strategy.
shard_cost_functionregprocИдентификатор для функции расчёта стоимости, которая должна принимать shardid типа bigint и возвращать определение стоимости типа real.
node_capacity_functionregprocИдентификатор для функции расчёта ёмкости, которая должна принимать nodeid типа int и возвращать определение ёмкости узла типа real.
shard_allowed_on_node_functionregprocИдентификатор функции, принимающей shardid типа bigint и nodeidarg типа int и возвращающей значение типа boolean, которое показывает, может ли данный сегмент храниться на узле.
default_thresholdfloat4Пороговое значение, по которому узел определяется как переполненный или незаполненный. Если стоимость сегментов больше этого значения, функция citus_rebalance_start должна начать перемещать сегменты с узла, а если меньше — на узел.
minimum_thresholdfloat4Защита от установки слишком низкого порогового значения для аргумента citus_rebalance_start.
improvement_thresholdfloat4Определяет, нужно ли перемещать сегмент во время перебалансировки. Перебалансировщик переместит сегмент, когда отношение производительности с перемещением сегмента к производительности без него пересечёт пороговое значение. Наиболее эффективно при использовании стратегии 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;
H.5.6.5.2.1.12. Таблица citus_stat_statements #

В citus реализована таблица citus_stat_statements для сбора статистики выполнения запросов. Она аналогична представлению pg_stat_statements в Postgres Pro, которое отслеживает статистику скорости выполнения запросов, и может быть соединена с ним.

NameТипОписание
queryidbigintИдентификатор (эффективен для соединений с pg_stat_statements)
useridoidПользователь, выполнивший запрос
dbidoidЭкземпляр БД узла-коррдинатора
querytextСтрока анонимизированного запроса
executortextИсполнитель citus: адаптивный или INSERT-SELECT
ключ_разбиенияtextЗначение столбца распределения в запросах, выполняемых маршрутизатором, иначе NULL
callsbigintКоличество выполнений запроса
-- Создание и заполнение распределённой таблицы
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.

H.5.6.5.2.1.13. Представление citus_stat_tenants #

Представление citus_stat_tenants дополняет таблицу citus_stat_statements, предоставляя информацию о том, сколько запросов выполняется каждым арендатором. Отслеживание запросов по арендаторам может использоваться для принятия решения о выполнении изоляции арендаторов.

В этом представлении подсчитываются последние одноарендные запросы, выполненные в течение настраиваемого периода времени. Количество запросов только для чтения и общее количество запросов за период увеличивается до конца текущего периода. Затем это количество переносятся в статистику за последний период, которая хранится до истечения срока действия. Период подсчёта запросов можно задать в секундах с помощью citus.stats_tenants_ period, значение по умолчанию — 60 секунд.

В представлении отображается до citus.stat_tenants_limit строк (по умолчанию — 100). Учитываются только запросы к одному арендатору, многоарендные запросы игнорируются.

NameТипОписание
nodeidintИдентификатор узла из таблицы pg_dist_node
colocation_idintИдентификатор группы совмещения
tenant_attributetextЗначение в столбце распределения, идентифицирующее арендатора
read_count_in_this_periodintКоличество читающих запросов (SELECT) от арендатора за указанный период
read_count_in_last_periodintКоличество читающих запросов за предпоследний период времени
query_count_in_this_periodintКоличество читающих/пишущих запросов от арендатора за период времени
query_count_in_last_periodintКоличество читающих/пишущих запросов за предпоследний период времени
cpu_usage_in_this_perioddoubleВремя использования процессора в секундах для данного арендатора за указанный период
cpu_usage_in_last_perioddoubleВремя использования процессора в секундах для данного арендатора за последний период

Отслеживание статистики на уровне арендатора увеличивает издержки и по умолчанию отключено. Чтобы включить его, установите для 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
H.5.6.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

В представленном примере запросы выполняются узлом-координатором, но в представлении также могут содержаться блокировки между запросами, выполняемыми рабочими узлами.

H.5.6.5.2.2. Таблицы на всех узлах #

В citus имеются другие информационные таблицы и представления, которые доступны на всех узлах, а не только на координаторе.

H.5.6.5.2.2.1. Таблица pg_dist_authinfo #

Таблица pg_dist_authinfo содержит параметры аутентификации, используемые узлами citus для подключения друг к другу.

NameТипОписание
nodeidintegerИдентификатор узла из таблицы pg_dist_node, 0 или -1
rolenamenameРоль Postgres Pro
authinfotextРазделённые пробелами параметры подключения 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)
H.5.6.5.2.2.2. Таблица pg_dist_poolinfo #

Если нужно использовать пул соединений для подключения к узлу, можно указать параметры пула с помощью pg_dist_poolinfo. В этой таблице метаданных содержится узел, порт и имя базы данных для citus, которые будут использоваться при подключении к узлу через пул.

Если есть информация о пуле, citus попытается использовать эти значения вместо установки прямого подключения. Информация pg_dist_poolinfo в этом случае заменяет собой pg_dist_node.

NameТипОписание
nodeidintegerИдентификатор узла из pg_dist_node
poolinfotextПараметры, разделяемые пробелами: 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');
H.5.6.5.3. Справка по конфигурации #

Существуют различные параметры конфигурации, которые влияют на поведение citus: как стандартные параметры Postgres Pro, так и специальные параметры citus. За подробным описанием параметров конфигурации Postgres Pro обратитесь к Главе 19.

Оставшаяся часть данной справки посвящена обсуждению особых параметров конфигурации citus. Эти параметры можно задать аналогично параметрам Postgres Pro : с помощью команды SET или изменив файл postgresql.conf.

Например можно изменить параметр следующим образом:

ALTER DATABASE citus SET citus.multi_task_query_log_level = 'log';
H.5.6.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 поддерживается только определённое подмножество допустимых параметров, а именно:

  • application_name

  • connect_timeout

  • gsslib (при наличии дополнительной функциональности Postgres Pro во время выполнения)

  • keepalives

  • keepalives_count

  • keepalives_idle

  • keepalives_interval

  • krbsrvname (при наличии дополнительной функциональности Postgres Pro во время выполнения)

  • sslcompression

  • sslcrl

  • sslmode (значение по умолчанию — require)

  • sslrootcert

  • tcp_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 МБ.

H.5.6.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% запросов отслеживаемых арендаторов, а запросы неотслеживаемых арендаторов выбираются только с указанной частотой.

H.5.6.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();

Рекомендуется сначала использовать транзакционный режим и переключаться на нетранзакционный только в случае сбоя памяти.

H.5.6.5.3.4. Конфигурация планировщика #
citus.local_table_join_policy (enum) #

Определяет, как citus перемещает данные при соединении локальных и распределённых таблиц. Настройка политики соединения может помочь уменьшить объём данных, передаваемых между рабочими узлами.

В citus локальные или распределённые таблицы отправляются узлам по мере необходимости для выполнения соединения. Копирование данных таблицы называется «преобразованием». При преобразовании локальной таблицы она отправляется всем рабочим узлам, которым нужны её данные для выполнения соединения. При преобразовании распределённой таблицы она собирается на узле-координаторе для выполнения соединения. Планировщик citus отправляет только строки, необходимые для выполнения преобразования.

Предпочтения по преобразованию отражены с помощью четырёх режимов:

  • autocitus преобразует либо все локальные, либо все распределённые таблицы для поддержки соединений локальных и распределённых таблиц. Решение о том, какие именно таблицы преобразовывать, принимается эвристически. Распределённые таблицы преобразуются, если они соединяются с использованием постоянного фильтра по уникальному индексу (например, первичному ключу). Таким образом между рабочими узлами будет перемещаться меньше данных. Это значение по умолчанию.

  • 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.

H.5.6.5.3.5. Промежуточная передача данных #
citus.max_intermediate_result_size (integer) #

Максимальный размер промежуточных результатов в КБ для CTE, которые невозможно передать на рабочие узлы для выполнения, а также для сложных подзапросов. По умолчанию используется 1 ГБ, а значение -1 означает отсутствие ограничений. Запросы, превышающие лимит, будут отменены с выводом сообщения об ошибке.

H.5.6.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, все созданные схемы будут распределяться по умолчанию. Распределённые схемы автоматически связываются с отдельными группами совмещения, так что таблицы, созданные в этих схемах, будут автоматически преобразованы в совмещённые распределённые таблицы без ключа сегментирования. Этот параметр можно изменять для отдельных сеансов.

Чтобы узнать, как использовать этот параметр конфигурации, обратитесь к Подразделу H.5.4.3.

H.5.6.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 — сортировать по идентификатору задачи.