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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • citus не может работать с выключенным (off) параметром конфигурации standard_conforming_strings. citus_columnar может, но во избежание ошибок необходимо установить для этого параметра значение on при выполнении команд CREATE EXTENSION или ALTER EXTENSION UPDATE. После завершения установки или обновления при необходимости можно изменить значение на off. Расширение продолжит работать в штатном режиме.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Примечание

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

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

    Если citus установлен согласно описанию в разделе Установка citus на одном узле, узел-координатор запускается и слушает порт 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);
J.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
J.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 для простого многоарендного приложения. В качестве следующего шага можно ознакомиться с описанием многоарендных приложений, чтобы узнать, как моделировать собственные данные для множества арендаторов.

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

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

Примечание

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

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

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

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

psql -p 9700

Затем можно создать таблицы стандартной командой Postgres Pro CREATE TABLE:

CREATE TABLE github_events
(
    event_id bigint,
    event_type text,
    event_public boolean,
    repo_id bigint,
    payload jsonb,
    repo jsonb,
    user_id bigint,
    org jsonb,
    created_at timestamp
);

CREATE TABLE github_users
(
    user_id bigint,
    url text,
    login text,
    avatar_url text,
    gravatar_id text,
    display_login text
);

Далее можно создать индексы данных о событиях так же, как это делается в Postgres Pro. В этом примере также показано, как создать индекс GIN, чтобы ускорить обращение к полям JSONB.

CREATE INDEX event_type_index ON github_events (event_type);
CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops);
J.5.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
J.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;

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

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

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

Примечание

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

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

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

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

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

  • сервис user

  • сервис time

  • сервис ping

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

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

psql -p 9700

Теперь можно создавать роли базы данных для каждого микросервиса:

CREATE USER user_service;
CREATE USER time_service;
CREATE USER ping_service;

В citus схему можно распределять двумя способами:

  • Вызвать функцию citus_schema_distribute('имя_схемы') вручную:

    CREATE SCHEMA AUTHORIZATION user_service;
    CREATE SCHEMA AUTHORIZATION time_service;
    CREATE SCHEMA AUTHORIZATION ping_service;
    
    SELECT citus_schema_distribute('user_service');
    SELECT citus_schema_distribute('time_service');
    SELECT citus_schema_distribute('ping_service');

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

    Примечание

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

  • Включением параметра конфигурации enable_schema_based_sharding:

    SET citus.enable_schema_based_sharding TO ON;
    
    CREATE SCHEMA AUTHORIZATION user_service;
    CREATE SCHEMA AUTHORIZATION time_service;
    CREATE SCHEMA AUTHORIZATION ping_service;

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

Можно вывести список схем, распределённых на данный момент:

SELECT * FROM citus_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)
J.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
);
J.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
}

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

J.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
J.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)

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

J.5.5. Сценарии использования #

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

Если вы создаёте приложение типа программное обеспечение как услуга (SaaS), вероятно, в модель данных уже встроено понятие аренды. Обычно большая часть информации относится к арендаторам/клиентам/счетам, и таблицы базы данных отражают эту естественную связь.

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

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

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

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

J.5.5.1.1. Создание приложения для анализа рекламы #

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

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

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

CREATE TABLE companies (
  id bigserial PRIMARY KEY,
  name text NOT NULL,
  image_url text,
  created_at timestamp without time zone NOT NULL,
  updated_at timestamp without time zone NOT NULL
);

CREATE TABLE campaigns (
  id bigserial PRIMARY KEY,
  company_id bigint REFERENCES companies (id),
  name text NOT NULL,
  cost_model text NOT NULL,
  state text NOT NULL,
  monthly_budget bigint,
  blacklisted_site_urls text[],
  created_at timestamp without time zone NOT NULL,
  updated_at timestamp without time zone NOT NULL
);

CREATE TABLE ads (
  id bigserial PRIMARY KEY,
  campaign_id bigint REFERENCES campaigns (id),
  name text NOT NULL,
  image_url text,
  target_url text,
  impressions_count bigint DEFAULT 0,
  clicks_count bigint DEFAULT 0,
  created_at timestamp without time zone NOT NULL,
  updated_at timestamp without time zone NOT NULL
);

CREATE TABLE clicks (
  id bigserial PRIMARY KEY,
  ad_id bigint REFERENCES ads (id),
  clicked_at timestamp without time zone NOT NULL,
  site_url text NOT NULL,
  cost_per_click_usd numeric(20,10),
  user_ip inet NOT NULL,
  user_data jsonb NOT NULL
);

CREATE TABLE impressions (
  id bigserial PRIMARY KEY,
  ad_id bigint REFERENCES ads (id),
  seen_at timestamp without time zone NOT NULL,
  site_url text NOT NULL,
  cost_per_impression_usd numeric(20,10),
  user_ip inet NOT NULL,
  user_data jsonb NOT NULL
);

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

J.5.5.1.2. Масштабирование реляционной модели данных #

Реляционная модель данных отлично подходит для приложений. Она защищает целостность данных, обеспечивает гибкость запросов и учитывает изменение данных. Традиционно считалось, что реляционные базы данных не могут масштабироваться до рабочих объёмов больших SaaS-приложений. Чтобы достичь такого размера, разработчикам приходилось прибегать к базам данных NoSQL или наборам серверных служб.

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

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

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

Рисунок J.1. Диаграмма маршрутизации рекламы со множеством арендаторов


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

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

J.5.5.1.3. Подготовка таблиц и заполнение данными #

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

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

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

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

CREATE TABLE companies (
  id bigserial PRIMARY KEY,
  name text NOT NULL,
  image_url text,
  created_at timestamp without time zone NOT NULL,
  updated_at timestamp without time zone NOT NULL
);

CREATE TABLE campaigns (
  id bigserial,       -- было: PRIMARY KEY
  company_id bigint REFERENCES companies (id),
  name text NOT NULL,
  cost_model text NOT NULL,
  state text NOT NULL,
  monthly_budget bigint,
  blacklisted_site_urls text[],
  created_at timestamp without time zone NOT NULL,
  updated_at timestamp without time zone NOT NULL,
  PRIMARY KEY (company_id, id) -- добавлено
);

CREATE TABLE ads (
  id bigserial,       -- было: PRIMARY KEY
  company_id bigint,  -- добавлено
  campaign_id bigint, -- было: REFERENCES campaigns (id)
  name text NOT NULL,
  image_url text,
  target_url text,
  impressions_count bigint DEFAULT 0,
  clicks_count bigint DEFAULT 0,
  created_at timestamp without time zone NOT NULL,
  updated_at timestamp without time zone NOT NULL,
  PRIMARY KEY (company_id, id),         -- добавлено
  FOREIGN KEY (company_id, campaign_id) -- добавлено
    REFERENCES campaigns (company_id, id)
);

CREATE TABLE clicks (
  id bigserial,        -- было: PRIMARY KEY
  company_id bigint,   -- добавлено
  ad_id bigint,        -- было: REFERENCES ads (id),
  clicked_at timestamp without time zone NOT NULL,
  site_url text NOT NULL,
  cost_per_click_usd numeric(20,10),
  user_ip inet NOT NULL,
  user_data jsonb NOT NULL,
  PRIMARY KEY (company_id, id),      -- добавлено
  FOREIGN KEY (company_id, ad_id)    -- добавлено
    REFERENCES ads (company_id, id)
);

CREATE TABLE impressions (
  id bigserial,         -- было: PRIMARY KEY
  company_id bigint,    -- добавлено
  ad_id bigint,         -- было: REFERENCES ads (id),
  seen_at timestamp without time zone NOT NULL,
  site_url text NOT NULL,
  cost_per_impression_usd numeric(20,10),
  user_ip inet NOT NULL,
  user_data jsonb NOT NULL,
  PRIMARY KEY (company_id, id),       -- добавлено
  FOREIGN KEY (company_id, ad_id)     -- добавлено
    REFERENCES ads (company_id, id)
);

Более подробно миграция из пользовательской модели данных описана в разделе Определение стратегии распределения.

J.5.5.1.3.1. Практический пример #

Примечание

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

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

    SELECT create_distributed_table('companies',   'id');
    SELECT create_distributed_table('campaigns',   'company_id');
    SELECT create_distributed_table('ads',         'company_id');
    SELECT create_distributed_table('clicks',      'company_id');
    SELECT create_distributed_table('impressions', 'company_id');

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

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

    # Загрузка и передача наборов данных из оболочки
    
    for dataset in companies campaigns ads clicks impressions geo_ips; do
      curl -O https://examples.citusdata.com/mt_ref_arch/${dataset}.csv
    done
  3. Поскольку citus является расширением Postgres Pro, в нём есть поддержка массовой загрузки с помощью команды /copy. Используйте её для приёма загруженных данных и проверьте, что указан правильный путь к файлу при загрузке в нестандартный каталог. В psql выполните:

    \copy companies from 'companies.csv' with csv
    \copy campaigns from 'campaigns.csv' with csv
    \copy ads from 'ads.csv' with csv
    \copy clicks from 'clicks.csv' with csv
    \copy impressions from 'impressions.csv' with csv
J.5.5.1.4. Интеграция приложений #

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

Любые запросы приложений или операторы изменений, которые включают фильтр по company_id, будут продолжать работать в прежнем виде. Как упоминалось ранее, фильтры такого типа часто встречаются в многоарендных приложениях. При использовании объектно-реляционного преобразователя (ORM) можно распознавать эти запросы с помощью таких методов, как where или filter.

ActiveRecord:

Impression.where(company_id: 5).count

Django:

Impression.objects.filter(company_id=5).count()

По сути, если результирующий код SQL, выполняемый в базе данных, содержит предложение WHERE company_id = :value для каждой таблицы (включая таблицы в запросах JOIN), citus распознаёт, что запрос должен быть направлен на один узел, и выполнит его там без изменений. Это гарантирует доступность всех функций SQL. В конце концов, узел представляет собой обычный сервер Postgres Pro.

Кроме того, чтобы упростить процедуру, можно использовать библиотеку activerecord-multi-tenant для Ruby on Rails или django-multitenant для Django, которая автоматически добавит эти фильтры для всех пользовательских запросов, даже самых сложных. Ознакомьтесь с руководствами по миграции для Ruby on Rails и Django.

Данное руководство не привязано к определённой платформе, поэтому некоторые возможности citus показаны с использованием языка SQL. На другом языке эти операторы могут выглядеть иначе.

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

-- Кампании с наибольшим бюджетом

SELECT name, cost_model, state, monthly_budget
  FROM campaigns
 WHERE company_id = 5
 ORDER BY monthly_budget DESC
 LIMIT 10;

-- Удвоение бюджета

UPDATE campaigns
   SET monthly_budget = monthly_budget*2
 WHERE company_id = 5;

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

-- Транзакция по перераспределению бюджета кампании

BEGIN;

UPDATE campaigns
   SET monthly_budget = monthly_budget + 1000
 WHERE company_id = 5
   AND id = 40;

UPDATE campaigns
   SET monthly_budget = monthly_budget - 1000
 WHERE company_id = 5
   AND id = 41;

COMMIT;

В качестве ещё одного примера поддержки SQL показан запрос с агрегатными и оконными функциями, который работает в citus так же, как и в Postgres Pro. Запрос упорядочивает объявления в каждой кампании по количеству показов.

SELECT a.campaign_id,
       RANK() OVER (
         PARTITION BY a.campaign_id
         ORDER BY a.campaign_id, count(*) desc
       ), count(*) as n_impressions, a.id
  FROM ads as a
  JOIN impressions as i
    ON i.company_id = a.company_id
   AND i.ad_id      = a.id
 WHERE a.company_id = 5
GROUP BY a.campaign_id, a.id
ORDER BY a.campaign_id, n_impressions desc;

Когда запросы ограничены одним арендатором, команды INSERT, UPDATE, DELETE, сложные команды SQL и транзакции работают как обычно.

J.5.5.1.5. Обмен данными между арендаторами #

До сих пор все таблицы распределялись по company_id, но существуют данные, которые могут использоваться всеми арендаторами и не «принадлежат» какому-либо конкретному арендатору. Например, все предприятия, использующие рекламную платформу из примера, могут получить географическую информацию о своей аудитории на основе IP-адресов. В базе данных одного компьютера это можно сделать с помощью справочной таблицы с geo-IP, как показано ниже. (Обратите внимание, что в настоящей таблице, вероятно, будет использоваться PostGIS.)

CREATE TABLE geo_ips (
  addrs cidr NOT NULL PRIMARY KEY,
  latlon point NOT NULL
    CHECK (-90  <= latlon[0] AND latlon[0] <= 90 AND
           -180 <= latlon[1] AND latlon[1] <= 180)
);
CREATE INDEX ON geo_ips USING gist (addrs inet_ops);

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

-- Создание синхронизированных копий geo_ips на всех рабочих узлах

SELECT create_reference_table('geo_ips');

Таблицы-справочники реплицируются на все рабочие узлы, и citus автоматически синхронизирует их при изменении. Обратите внимание, что вызывается функция create_reference_table, а не create_distributed_table.

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

\copy geo_ips from 'geo_ips.csv' with csv

Теперь соединение по количеству переходов может выполняться эффективно благодаря этой таблице. Например, можно сделать запрос о местоположении каждого, кто нажал на объявление 290.

SELECT c.id, clicked_at, latlon
  FROM geo_ips, clicks c
 WHERE addrs >> c.user_ip
   AND c.company_id = 5
   AND c.ad_id = 290;
J.5.5.1.6. Внесение изменений в схему в реальном времени #

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

Например, в рекламных объявлениях в этом приложении могут использоваться сопроводительные надписи. Можно добавить столбец в таблицу, выполнив стандартный SQL-код на узле-координаторе:

ALTER TABLE ads
  ADD COLUMN caption text;

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

За более подробным описанием трансляции DDL-команд по кластеру обратитесь к разделу Изменение таблиц.

J.5.5.1.7. Разные данные у разных арендаторов #

Все арендаторы используют общую схему и аппаратную инфраструктуру, поэтому возникает вопрос: как быть с арендаторами, которые хотят хранить информацию, нужную только им? Например, один из арендаторов, использующих рекламную базу данных, может захотеть хранить в своём приложении информацию о cookie-файлах для отслеживания переходов, а другого арендатора может больше интересовать информация об агентах браузера. Традиционно базы данных, использующие подход с общей схемой для многоарендности, прибегали к созданию фиксированного числа предварительно выделенных «пользовательских» столбцов или к использованию внешних «таблиц расширения». Однако Postgres Pro предоставляет гораздо более простой способ благодаря своим неструктурированным типам столбцов, в частности, JSONB.

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

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

SELECT
  user_data->>'is_mobile' AS is_mobile,
  count(*) AS count
FROM clicks
WHERE company_id = 5
GROUP BY user_data->>'is_mobile'
ORDER BY count DESC;

Администратор базы данных даже может создать частичный индекс, чтобы повысить скорость обработки шаблонов запросов отдельного арендатора. Ниже представлен один из вариантов улучшения фильтров в компании с company_id = 5 для переходов от пользователей на мобильных устройствах:

CREATE INDEX click_user_data_is_mobile
ON clicks ((user_data->>'is_mobile'))
WHERE company_id = 5;

Кроме того, в Postgres Pro поддерживаются индексы GIN для JSONB. Создание индекса GIN для столбца JSONB приведёт к созданию индекса для каждого ключа и значения в этом документе JSON. Это ускоряет работу ряда операторов JSONB, таких как ?, ?| и ?&.

CREATE INDEX click_user_data
ON clicks USING gin (user_data);

-- эта часть ускоряет запросы "у каких переходов
-- в user_data есть ключ is_mobile?"

SELECT id
  FROM clicks
 WHERE user_data ? 'is_mobile'
   AND company_id = 5;
J.5.5.1.8. Масштабирование аппаратных ресурсов #

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

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

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

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

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

SELECT citus_rebalance_start();

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

J.5.5.1.9. Работа с крупными арендаторами #

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

Что касается первого вопроса, исследование данных крупных SaaS-сайтов показывает, что с ростом числа арендаторов размер данных арендаторов начинает следовать закону Ципфа. За подробностями обратитесь к рисунку ниже.

Рисунок J.2. Распределение по закону Ципфа


Например, в базе данных из 100 арендаторов прогнозируется, что на долю крупнейшего арендатора будет приходиться около 20% данных. В более реалистичном примере для крупной SaaS-компании с 10 000 арендаторов на долю крупнейшего из них будет приходиться около 2% данных. Даже при объёме данных 10 ТБ самому крупному арендатору потребуется 200 ГБ, для чего должно хватить всего одного узла.

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

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

Представим очень крупное предприятие с company_id=5. Данные для этого арендатора можно изолировать в два этапа. Ниже представлены команды, подробное описание которых можно найти в разделе Изоляция арендаторов.

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

SELECT isolate_tenant_to_new_shard(
  'companies', 5, 'CASCADE'
);

В результате будет получен идентификатор сегмента, выделенного для хранения company_id=5:

┌─────────────────────────────┐
│ isolate_tenant_to_new_shard │
├─────────────────────────────┤
│                      102240 │
└─────────────────────────────┘

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

-- Найдите узел, на котором сейчас находится новый сегмент

SELECT nodename, nodeport
  FROM pg_dist_placement AS placement,
       pg_dist_node AS node
 WHERE placement.groupid = node.groupid
   AND node.noderole = 'primary'
   AND shardid = 102240;

-- Переместите сегмент на выбранный рабочий узел (другие сегменты, созданные
-- с параметром CASCADE также будут перемещены)

-- Обратите внимание, что на для всех узлов следует задать для wal_level значение >= logical,
-- чтобы использовать citus_move_shard_placement.
-- Также необходимо перезапустить кластер после изменения wal_level в
-- файлах postgresql.conf.

SELECT citus_move_shard_placement(
  102240,
  'source_host', source_port,
  'dest_host', dest_port);

Подтвердите перемещение сегмента, повторно отправив запрос таблице pg_dist_placement.

J.5.5.1.10. Дальнейшие действия #

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

Чтобы настроить клиентское приложение, например Ruby on Rails или Django, используйте руководство по миграции Ruby on Rails или Django.

J.5.5.2. Панели для анализа в реальном времени #

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

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

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

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

J.5.5.2.1. Модель данных #

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

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

-- Выполните на узле-координаторе

CREATE TABLE http_request (
  site_id INT,
  ingest_time TIMESTAMPTZ DEFAULT now(),

  url TEXT,
  request_country TEXT,
  ip_address TEXT,

  status_code INT,
  response_time_msec INT
);

SELECT create_distributed_table('http_request', 'site_id');

Когда вызывается функция create_distributed_table, citus получает указание распределить по хешу http_request, используя столбец site_id. Это означает, что все данные конкретного сайта будут храниться в одном сегменте.

Пользовательские функции используют конфигурацию со стандартным количеством сегментов. Рекомендуется использовать в 2–4 раза больше сегментов, чем количество ядер ЦП в кластере. Такое количество сегментов позволяет перебалансировать данные в кластере после добавления новых рабочих узлов.

Теперь система готова принимать данные и обслуживать запросы. Запустите следующий цикл в консоли psql в фоновом режиме, продолжая выполнять другие команды из этой статьи. Этот цикл генерирует искусственные данные каждые 1-2 секунды.

DO $$
  BEGIN LOOP
    INSERT INTO http_request (
      site_id, ingest_time, url, request_country,
      ip_address, status_code, response_time_msec
    ) VALUES (
      trunc(random()*32), clock_timestamp(),
      concat('http://example.com/', md5(random()::text)),
      ('{China,India,USA,Indonesia}'::text[])[ceil(random()*4)],
      concat(
        trunc(random()*250 + 2), '.',
        trunc(random()*250 + 2), '.',
        trunc(random()*250 + 2), '.',
        trunc(random()*250 + 2)
      )::inet,
      ('{200,404}'::int[])[ceil(random()*2)],
      5+trunc(random()*150)
    );
    COMMIT;
    PERFORM pg_sleep(random() * 0.25);
  END LOOP;
END $$;

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

SELECT
  site_id,
  date_trunc('minute', ingest_time) as minute,
  COUNT(1) AS request_count,
  SUM(CASE WHEN (status_code between 200 and 299) THEN 1 ELSE 0 END) as success_count,
  SUM(CASE WHEN (status_code between 200 and 299) THEN 0 ELSE 1 END) as error_count,
  SUM(response_time_msec) / COUNT(1) AS average_response_time_msec
FROM http_request
WHERE date_trunc('minute', ingest_time) > now() - '5 minutes'::interval
GROUP BY site_id, minute
ORDER BY minute ASC;

Описанный выше вариант работает, но у него есть два недостатка:

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

  • Затраты на хранение будут расти пропорционально скорости заполнения и длине запрашиваемой истории. На практике может потребоваться хранить необработанные данные за более короткий период времени (один месяц) и просматривать исторические графики за более длительный период времени (годы).

J.5.5.2.2. Свёртки #

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

CREATE TABLE http_request_1min (
  site_id INT,
  ingest_time TIMESTAMPTZ, -- минута, представленная данной строкой

  error_count INT,
  success_count INT,
  request_count INT,
  average_response_time_msec INT,
  CHECK (request_count = error_count + success_count),
  CHECK (ingest_time = date_trunc('minute', ingest_time))
);

SELECT create_distributed_table('http_request_1min', 'site_id');

CREATE INDEX http_request_1min_idx ON http_request_1min (site_id, ingest_time);

Этот блок кода очень похож на предыдущий. Самое главное: в нём также сегментируется site_id и используется та же стандартная конфигурация для количества сегментов. Поскольку все три этих элемента совпадают, сегменты http_request и сегменты http_request_1min полностью соответствуют друг другу, и citus разместит соответствующие сегменты на одном рабочем узле. Этот процесс называется совмещением. Он ускоряет выполнение соединений и позволяет использовать свёртки. За более подробным описанием обратитесь к рисунку.

Рисунок J.3. Диаграмма совмещения


Чтобы заполнить http_request_1min, нужно периодически запускать команду INSERT INTO SELECT. Это возможно, поскольку таблицы совмещены. Следующая функция упрощает свёртку.

-- Однострочная таблица для хранения момента последней свёртки
CREATE TABLE latest_rollup (
  minute timestamptz PRIMARY KEY,

  -- Параметр "minute" должен усекать с точностью до минут
  CHECK (minute = date_trunc('minute', minute))
);

-- Инициализация по очень старой дате
INSERT INTO latest_rollup VALUES ('10-10-1901');

-- Функция для выполнения группирования
CREATE OR REPLACE FUNCTION rollup_http_request() RETURNS void AS $$
DECLARE
  curr_rollup_time timestamptz := date_trunc('minute', now() - interval '1 minute');
  last_rollup_time timestamptz := minute from latest_rollup;
BEGIN
  INSERT INTO http_request_1min (
    site_id, ingest_time, request_count,
    success_count, error_count, average_response_time_msec
  ) SELECT
    site_id,
    date_trunc('minute', ingest_time),
    COUNT(1) as request_count,
    SUM(CASE WHEN (status_code between 200 and 299) THEN 1 ELSE 0 END) as success_count,
    SUM(CASE WHEN (status_code between 200 and 299) THEN 0 ELSE 1 END) as error_count,
    SUM(response_time_msec) / COUNT(1) AS average_response_time_msec
  FROM http_request
  -- roll up only data new since last_rollup_time
  WHERE date_trunc('minute', ingest_time) <@
          tstzrange(last_rollup_time, curr_rollup_time, '(]')
  GROUP BY 1, 2;

  -- Обновление значения в latest_rollup, чтобы при следующем запуске
  -- свёртки она применялась к данным, новее, чем curr_rollup_time
  UPDATE latest_rollup SET minute = curr_rollup_time;
END;
$$ LANGUAGE plpgsql;

Примечание

Вышеуказанная функция должна вызываться каждую минуту. Для этого добавьте запись crontab на узле-координаторе:

* * * * * psql -c 'SELECT rollup_http_request();'

В качестве альтернативы можно использовать приложение pg_cron (или его аналог), позволяющее составлять расписание повторяющихся запросов прямо из БД.

Уже использовавшийся запрос информационной панели теперь выглядит гораздо лучше:

SELECT site_id, ingest_time as minute, request_count,
       success_count, error_count, average_response_time_msec
  FROM http_request_1min
 WHERE ingest_time > date_trunc('minute', now()) - '5 minutes'::interval;
J.5.5.2.3. Срок годности данных #

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

DELETE FROM http_request WHERE ingest_time < now() - interval '1 day';
DELETE FROM http_request_1min WHERE ingest_time < now() - interval '1 month';

В производственной среде можно обернуть эти запросы в функцию и вызывать её каждую минуту в задании cron.

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

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

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

J.5.5.2.4. Приблизительное количество уникальных значений #

Распространённый вопрос в HTTP-аналитике связан с приблизительным подсчётом уникальных значений: например сколько уникальных посетителей было на сайте за последний месяц. Чтобы ответить на этот вопрос точно, необходимо хранить в таблицах свёрток список всех замеченных посетителей, а это слишком большой объём данных. Использование приблизительного количества требует гораздо меньших ресурсов.

Тип данных, называемый HyperLogLog или hll, может дать приблизительный ответ на запрос. При этом, чтобы определить приблизительное количество уникальных элементов в наборе требуется совсем немного места. Точность определения можно регулировать. Используя всего 1280 байт можно учесть до десятков миллиардов уникальных посетителей с погрешностью не более 2,2%.

Аналогичная проблема возникает при необходимости выполнить глобальный запрос, например, узнать количество уникальных IP-адресов, посетивших любой из сайтов клиента за последний месяц. Без hll этот запрос предполагает отправку списков IP-адресов с рабочих узлов координатору для исключения дубликатов. Это одновременно и большого сетевой трафик, и большой объём вычислений. При использовании hll можно значительно повысить скорость запросов.

Установите расширение hll, инструкции для которого можно найти в репозитории GitHub, и включите его:

CREATE EXTENSION hll;

Теперь можно отслеживать IP-адреса в свёртке с помощью расширения hll. Сначала добавьте столбец в таблицу свёрток.

ALTER TABLE http_request_1min ADD COLUMN distinct_ip_addresses hll;

Затем используйте пользовательскую агрегацию для заполнения столбца. Для этого добавьте её в запрос в функции свёртки:

@@ -1,10 +1,12 @@
  INSERT INTO http_request_1min (
    site_id, ingest_time, request_count,
    success_count, error_count, average_response_time_msec
+   , distinct_ip_addresses
  ) SELECT
    site_id,
    date_trunc('minute', ingest_time),
    COUNT(1) as request_count,
    SUM(CASE WHEN (status_code between 200 and 299) THEN 1 ELSE 0 END) as success_count,
    SUM(CASE WHEN (status_code between 200 and 299) THEN 0 ELSE 1 END) as error_count,
    SUM(response_time_msec) / COUNT(1) AS average_response_time_msec
+   , hll_add_agg(hll_hash_text(ip_address)) AS distinct_ip_addresses
  FROM http_request

Запросы информационной панели немного сложнее: нужно считывать определённое количество IP-адресов, вызывая функцию hll_cardinality:

SELECT site_id, ingest_time as minute, request_count,
       success_count, error_count, average_response_time_msec,
       hll_cardinality(distinct_ip_addresses) AS distinct_ip_address_count
  FROM http_request_1min
 WHERE ingest_time > date_trunc('minute', now()) - interval '5 minutes';

Тип данных hll не просто позволяет ускорить работу, но и добавляет ранее недоступные возможности. Допустим, были выполнены свёртки, но вместо использования hll сохранилось точное количество уникальных значений. Такой способ тоже работает, но при этом невозможно ответить на такие запросы, как «сколько отдельных сеансов, для которых были удалены необработанные данные, было в течение этого недельного периода в прошлом?».

С hll ответить на такой запрос легко. Можно вычислить количество различных IP-адресов за определённый период времени с помощью следующего запроса:

SELECT hll_cardinality(hll_union_agg(distinct_ip_addresses))
FROM http_request_1min
WHERE ingest_time > date_trunc('minute', now()) - '5 minutes'::interval;

Чтобы узнать больше о расширении hll, обратитесь к документации в репозитории проекта.

J.5.5.2.5. Неструктурированные данные с JSONB #

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

Сначала добавьте новый столбец в таблицу свёрток:

ALTER TABLE http_request_1min ADD COLUMN country_counters JSONB;

Затем включите его в свёртки, изменив соответствующую функцию:

@@ -1,14 +1,19 @@
  INSERT INTO http_request_1min (
    site_id, ingest_time, request_count,
    success_count, error_count, average_response_time_msec
+   , country_counters
  ) SELECT
    site_id,
    date_trunc('minute', ingest_time),
    COUNT(1) as request_count,
    SUM(CASE WHEN (status_code between 200 and 299) THEN 1 ELSE 0 END) as success_count
    SUM(CASE WHEN (status_code between 200 and 299) THEN 0 ELSE 1 END) as error_count
    SUM(response_time_msec) / COUNT(1) AS average_response_time_msec
- FROM http_request
+   , jsonb_object_agg(request_country, country_count) AS country_counters
+ FROM (
+   SELECT *,
+     count(1) OVER (
+       PARTITION BY site_id, date_trunc('minute', ingest_time), request_country
+     ) AS country_count
+   FROM http_request
+ ) h

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

SELECT
  request_count, success_count, error_count, average_response_time_msec,
  COALESCE(country_counters->>'USA', '0')::int AS american_visitors
FROM http_request_1min
WHERE ingest_time > date_trunc('minute', now()) - '5 minutes'::interval;

J.5.5.3. Данные временных рядов #

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

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

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

Рисунок J.4. Диаграмма удаления строки или секции


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

Рисунок J.5. SELECT по нескольким индексам


Команды INSERT также выполняются быстрее, поскольку работают с индексами меньшего объёма. За подробностями обратитесь к рисунку.

Рисунок J.6. INSERT в несколько индексов


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

  1. Большинство запросов обращается к небольшому подмножеству последних данных.

  2. Срок годности старых данных периодически истекает (и они удаляются).

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

J.5.5.3.1. Масштабирование данных временных рядов в citus #

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

Рисунок J.7. Сегментирование и секционирование данных временных рядов


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

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

Первым шагом будет создание и секционирование таблицы по времени в базе данных Postgres Pro на одном узле:

-- Декларативно секционированные таблицы
CREATE TABLE github_events (
  event_id bigint,
  event_type text,
  event_public boolean,
  repo_id bigint,
  payload jsonb,
  repo jsonb,
  actor jsonb,
  org jsonb,
  created_at timestamp
) PARTITION BY RANGE (created_at);

Обратите внимание на PARTITION BY RANGE (create_at). Это указание для Postgres Pro, что таблица будет секционирована по столбцу created_at в упорядоченных диапазонах. Но никакие секции для конкретных диапазонов ещё не были созданы.

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

SELECT create_distributed_table('github_events', 'repo_id');

На данном этапе расширением citus были созданы сегменты для этой таблицы на рабочих узлах. Внутри каждый сегмент представляет собой таблицу с именем github_events_N для каждого идентификатора сегмента N. Кроме того, была распространена информация о секционировании, и в каждом из упомянутых сегментов был объявлен ключ секционирования Partition key: RANGE (created_at).

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

J.5.5.3.2. Автоматическое создание секций #

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

SELECT create_time_partitions(
  table_name         := 'github_events',
  partition_interval := '1 month',
  end_at             := now() + '12 months'
);

В citus также есть представление time_partitions, позволяющее легко исследовать секции, созданные расширением.

SELECT partition
  FROM time_partitions
 WHERE parent_table = 'github_events'::regclass;

┌────────────────────────┐
│       partition        │
├────────────────────────┤
│ github_events_p2021_10 │
│ github_events_p2021_11 │
│ github_events_p2021_12 │
│ github_events_p2022_01 │
│ github_events_p2022_02 │
│ github_events_p2022_03 │
│ github_events_p2022_04 │
│ github_events_p2022_05 │
│ github_events_p2022_06 │
│ github_events_p2022_07 │
│ github_events_p2022_08 │
│ github_events_p2022_09 │
│ github_events_p2022_10 │
└────────────────────────┘

Со временем потребуется выполнять некоторое обслуживание, чтобы создавать новые секции и удалять старые. Лучше всего настроить периодическое задание для запуска функций обслуживания специальным расширением, например pg_cron:

-- Установите запуск двух заданий cron раз в месяц:

-- 1. Обеспечьте наличие секций на протяжении следующих 12 месяцев

SELECT cron.schedule('create-partitions', '0 0 1 * *', $$
  SELECT create_time_partitions(
      table_name         := 'github_events',
      partition_interval := '1 month',
      end_at             := now() + '12 months'
  )
$$);

-- 2. (Необязательно) Обеспечьте хранение данных не более, чем за год

SELECT cron.schedule('drop-partitions', '0 0 1 * *', $$
  CALL drop_old_time_partitions(
      'github_events',
      now() - interval '12 months' /* older_than */
  );
$$);

Примечание

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

J.5.5.3.3. Архивирование со столбцовым хранением #

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

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

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

Затем загрузите пример с данными:

wget http://examples.citusdata.com/github_archive/github_events-2015-01-01-{0..5}.csv.gz
gzip -c -d github_events-2015-01-01-*.gz >> github_events.csv
-- Новая таблица с такой же структурой, как в примере
-- из предыдущего раздела

CREATE TABLE github_columnar_events ( LIKE github_events )
PARTITION BY RANGE (created_at);

-- Создайте секции для хранения данных за два часа в каждой

SELECT create_time_partitions(
  table_name         := 'github_columnar_events',
  partition_interval := '2 hours',
  start_from         := '2015-01-01 00:00:00',
  end_at             := '2015-01-01 08:00:00'
);

-- Заполните таблицы загруженными данными
-- (обратите внимание, что для этих данных требуется, чтобы БД имела кодировку UTF8)

\COPY github_columnar_events FROM 'github_events.csv' WITH (format CSV)

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

SELECT partition, access_method
  FROM time_partitions
 WHERE parent_table = 'github_columnar_events'::regclass;
┌─────────────────────────────────────────┬───────────────┐
│                partition                │ access_method │
├─────────────────────────────────────────┼───────────────┤
│ github_columnar_events_p2015_01_01_0000 │ heap          │
│ github_columnar_events_p2015_01_01_0200 │ heap          │
│ github_columnar_events_p2015_01_01_0400 │ heap          │
│ github_columnar_events_p2015_01_01_0600 │ heap          │
└─────────────────────────────────────────┴───────────────┘
-- Преобразуйте старые секции для столбцового хранения

CALL alter_old_partitions_set_access_method(
  'github_columnar_events',
  '2015-01-01 06:00:00' /* older_than */,
  'columnar'
);

-- Старые секции теперь столбцовые, а
-- для новых используется строковое хранение и они могут быть изменены

SELECT partition, access_method
  FROM time_partitions
 WHERE parent_table = 'github_columnar_events'::regclass;
┌─────────────────────────────────────────┬───────────────┐
│                partition                │ access_method │
├─────────────────────────────────────────┼───────────────┤
│ github_columnar_events_p2015_01_01_0000 │ columnar      │
│ github_columnar_events_p2015_01_01_0200 │ columnar      │
│ github_columnar_events_p2015_01_01_0400 │ columnar      │
│ github_columnar_events_p2015_01_01_0600 │ heap          │
└─────────────────────────────────────────┴───────────────┘

Чтобы увидеть степень сжатия столбцовой таблицы, используйте VACUUM VERBOSE. Степень сжатия для трёх столбцовых секций выглядит так:

VACUUM VERBOSE github_columnar_events;
INFO:  statistics for "github_columnar_events_p2015_01_01_0000":
storage id: 10000000003
total file size: 4481024, total data size: 4444425
compression rate: 8.31x
total row count: 15129, stripe count: 1, average rows per stripe: 15129
chunk count: 18, containing data for dropped columns: 0, zstd compressed: 18

INFO:  statistics for "github_columnar_events_p2015_01_01_0200":
storage id: 10000000004
total file size: 3579904, total data size: 3548221
compression rate: 8.26x
total row count: 12714, stripe count: 1, average rows per stripe: 12714
chunk count: 18, containing data for dropped columns: 0, zstd compressed: 18

INFO:  statistics for "github_columnar_events_p2015_01_01_0400":
storage id: 10000000005
total file size: 2949120, total data size: 2917407
compression rate: 8.51x
total row count: 11756, stripe count: 1, average rows per stripe: 11756
chunk count: 18, containing data for dropped columns: 0, zstd compressed: 18

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

SELECT COUNT(DISTINCT repo_id)
  FROM github_columnar_events;
┌───────┐
│ count │
├───────┤
│ 16001 │
└───────┘

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

J.5.5.3.3.1. Архивирование строковой секции в столбцовое хранилище #

Когда у строковой секции заполняется диапазон, можно заархивировать её в сжатое столбцовое хранилище. Можно автоматизировать этот процесс с помощью расширения pg_cron:

-- Ежемесячное задание cron

SELECT cron.schedule('compress-partitions', '0 0 1 * *', $$
  CALL alter_old_partitions_set_access_method(
    'github_columnar_events',
    now() - interval '6 months' /* older_than */,
    'columnar'
  );
$$);

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

J.5.6. Архитектурные понятия #

J.5.6.1. Узлы #

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

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

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

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

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

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

J.5.6.2.1. Сегментирование на основе строк #

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

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

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

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

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

Недостатки:

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

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

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

J.5.6.2.2. Сегментирование на основе схем #

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

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

Недостатки:

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

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

J.5.6.3. Распределённые данные #

J.5.6.3.1. Типы таблиц #

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

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

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

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


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

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

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

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

  • Тип 2: таблицы-справочники.

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

    Таблицы-справочники обычно небольшого размера и используются для хранения данных, относящихся к запросам, которые выполняются на любом рабочем узле (например, перечисляемые значения, такие как статусы заказов или категории продуктов).

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

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

  • Тип 3: локальные таблицы.

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

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

  • Тип 4: локальные управляемые таблицы.

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

  • Тип 5: таблицы-схемы.

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

J.5.6.3.2. Сегменты #

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

Таблица метаданных pg_dist_shard на узле-координаторе содержит одну строку для каждого сегмента каждой распределённой таблицы в системе. Строка соответствует shardid с диапазоном целых чисел в хеш-пространстве (shardminvalue, shardmaxvalue):

SELECT * FROM pg_dist_shard;
 logicalrelid  | shardid | shardstorage | shardminvalue | shardmaxvalue
---------------+---------+--------------+---------------+---------------
 github_events |  102026 | t            | 268435456     | 402653183
 github_events |  102027 | t            | 402653184     | 536870911
 github_events |  102028 | t            | 536870912     | 671088639
 github_events |  102029 | t            | 671088640     | 805306367
 (4 rows)

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

J.5.6.3.2.1. Размещение сегмента #

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

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

SELECT
    shardid,
    node.nodename,
    node.nodeport
FROM pg_dist_placement placement
JOIN pg_dist_node node
  ON placement.groupid = node.groupid
 AND node.noderole = 'primary'::noderole
WHERE shardid = 102027;
┌─────────┬───────────┬──────────┐
│ shardid │ nodename  │ nodeport │
├─────────┼───────────┼──────────┤
│  102027 │ localhost │     5433 │
└─────────┴───────────┴──────────┘

В примере github_events было четыре сегмента. Количество сегментов можно настроить для каждой таблицы во время распределения по кластеру. Оптимальный выбор количества сегментов зависит от варианта использования, см. раздел Количество сегментов.

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

J.5.6.3.3. Совмещение #

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

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

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

J.5.6.3.4. Распараллеливание #

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

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

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

J.5.6.4. Выполнение запросов #

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

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

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


Для каждого сеанса у узла-координатора есть пул соединений. Для каждого запроса (например, SELECT * FROM foo на диаграмме) можно открыть ограниченное количество одновременных соединений для задач на каждый рабочий узел, установленное в параметре конфигурации citus.max_adaptive_executor_pool_size. Значение параметра можно настроить на уровне сеанса для управления приоритетами.

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

Чтобы сбалансировать выполнение коротких и длинных задач, в citus используется параметр конфигурации citus.executor_slow_start_interval. Он определяет интервал между попытками подключения для задач в многосегментном запросе. Сначала для задач запроса, попадающих в очередь, можно открыть только одно соединение. Если в конце интервала есть ожидающие подключения задачи, citus увеличивает количество возможных одновременных подключений. Чтобы полностью отключить такое поведение, установите для параметра конфигурации значение 0.

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

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

Рекомендации по настройке этих параметров в соответствии с рабочей нагрузкой находятся в разделе Управление подключениями.

J.5.7. Разработка #

J.5.7.1. Определение типа приложения #

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

Подробный пример создания такого типа приложений описан в разделе Многоарендные приложения.

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

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

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

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

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

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

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

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

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

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

J.5.7.2.3. Данные временных рядов #

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

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

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

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

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

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

J.5.7.2.4. Совмещение таблиц #

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

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

J.5.7.2.4.1. Совмещение данных в citus для таблиц, распределённых по хешу #

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

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

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


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

J.5.7.2.4.2. Практический пример совмещения #

Примером части многоарендного SaaS-приложения для веб-аналитики могут служить следующие таблицы:

CREATE TABLE event (
  tenant_id int,
  event_id bigint,
  page_id int,
  payload jsonb,
  primary key (tenant_id, event_id)
);

CREATE TABLE page (
  tenant_id int,
  page_id int,
  path text,
  primary key (tenant_id, page_id)
);

Теперь можно выполнять запросы, поступающие с информационной панели на стороне клиента, например: «Вывести для шестого арендатора количество посещений всех страниц, начинающихся с /blog, за последнюю неделю».

J.5.7.2.4.3. Использование обычных таблиц Postgres Pro #

Если бы наши данные находились на одном узле Postgres Pro, можно было бы легко составить запрос, используя богатый набор реляционных операций, предлагаемых SQL:

SELECT page_id, count(event_id)
FROM
  page
LEFT JOIN  (
  SELECT * FROM event
  WHERE (payload->>'time')::timestamptz >= now() - interval '1 week'
) recent
USING (tenant_id, page_id)
WHERE tenant_id = 6 AND path LIKE '/blog%'
GROUP BY page_id;

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

J.5.7.2.4.4. Распределение таблиц по идентификатору #

По мере роста числа арендаторов и объёма данных, хранящихся для каждого арендатора, время выполнения запросов обычно увеличивается, поскольку рабочий набор больше не помещается в памяти или процессор становится узким местом. В этом случае можно сегментировать данные на множество узлов, используя citus. Первый и самый важный выбор, который нужно сделать при сегментировании, — выбор столбца распределения. Для примера сделаем простой выбор: использовать event_id для таблицы event и page_id для таблицы page:

-- По неопытности для примера используем event_id и page_id в качестве столбцов распределения

SELECT create_distributed_table('event', 'event_id');
SELECT create_distributed_table('page', 'page_id');

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

Во всех сегментах таблицы страниц (Q1):

SELECT page_id FROM page WHERE path LIKE '/blog%' AND tenant_id = 6;

Во всех сегментах таблицы событий (Q2):

SELECT page_id, count(*) AS count
FROM event
WHERE page_id IN (/*…page IDs from first query…*/)
  AND tenant_id = 6
  AND (payload->>'time')::date >= now() - interval '1 week'
GROUP BY page_id ORDER BY count DESC LIMIT 10;

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

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

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


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

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

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

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

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

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

J.5.7.2.4.5. Распределение таблиц по ID арендатора #

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

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

-- Совмещение таблиц с помощью общего столбца распределения
SELECT create_distributed_table('event', 'tenant_id');
SELECT create_distributed_table('page', 'tenant_id', colocate_with => 'event');

В этом случае citus может выполнить тот же запрос, который выполнялся бы на одном узле Postgres Pro без изменений (Q1):

SELECT page_id, count(event_id)
FROM
  page
LEFT JOIN  (
  SELECT * FROM event
  WHERE (payload->>'time')::timestamptz >= now() - interval '1 week'
) recent
USING (tenant_id, page_id)
WHERE tenant_id = 6 AND path LIKE '/blog%'
GROUP BY page_id;

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Соответствующие шаги по настройке производительности описаны в разделе Настройка производительности запросов.

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

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

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

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

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

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

J.5.7.3.1. Определение стратегии распределения #
J.5.7.3.1.1. Выбор ключа распределения #

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

ALTER TABLE line_items ADD COLUMN store_id uuid;

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

J.5.7.3.2.2. Заполнение созданных столбцов #

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

Таблица заполняется путём получения недостающих значений из запроса на соединение с заказами:

UPDATE line_items
   SET store_id = orders.store_id
  FROM line_items
 INNER JOIN orders
 WHERE line_items.order_id = orders.order_id;

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

-- Функция для заполнения до тысячи
-- строк в таблице line_items

CREATE FUNCTION backfill_batch()
RETURNS void LANGUAGE sql AS $$
  WITH batch AS (
    SELECT line_items_id, order_id
      FROM line_items
     WHERE store_id IS NULL
     LIMIT 1000
       FOR UPDATE
      SKIP LOCKED
  )
  UPDATE line_items AS li
     SET store_id = orders.store_id
    FROM batch, orders
   WHERE batch.line_item_id = li.line_item_id
     AND batch.order_id = orders.order_id;
$$;

-- Функция выполняется каждые 15 минут
SELECT cron.schedule('*/15 * * * *', 'SELECT backfill_batch()');

-- Обратите внимание на возвращаемое значение cron.schedule

Как только будут заполнены все строки, задание cron можно отключить:

-- Предполагается, что 42 — полученный идентификатор
-- задания cron.schedule

SELECT cron.unschedule(42);
J.5.7.3.3. Подготовка приложения для работы с citus #
J.5.7.3.3.1. Настройка кластера разработки citus #

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

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

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

pg_dump \
   --format=plain \
   --no-owner \
   --schema-only \
   --file=schema.sql \
   --schema=target_schema \
   postgres://user:pass@host:5432/db

# загрузка схемы в тестовую БД

psql postgres://user:pass@testhost:5432/db -f schema.sql

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

J.5.7.3.3.1.1. Добавление столбца распределения в ключи #

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

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

BEGIN;

-- Удаление простых первичных ключей (с каскадным удалением внешних ключей)

ALTER TABLE products   DROP CONSTRAINT products_pkey CASCADE;
ALTER TABLE orders     DROP CONSTRAINT orders_pkey CASCADE;
ALTER TABLE line_items DROP CONSTRAINT line_items_pkey CASCADE;

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

ALTER TABLE products   ADD PRIMARY KEY (store_id, product_id);
ALTER TABLE orders     ADD PRIMARY KEY (store_id, order_id);
ALTER TABLE line_items ADD PRIMARY KEY (store_id, line_item_id);

-- Пересоздание внешних ключей с будущим столбцом распределения

ALTER TABLE line_items ADD CONSTRAINT line_items_store_fkey
  FOREIGN KEY (store_id) REFERENCES stores (store_id);
ALTER TABLE line_items ADD CONSTRAINT line_items_product_fkey
  FOREIGN KEY (store_id, product_id) REFERENCES products (store_id, product_id);
ALTER TABLE line_items ADD CONSTRAINT line_items_order_fkey
  FOREIGN KEY (store_id, order_id) REFERENCES orders (store_id, order_id);

COMMIT;

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

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


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

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

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

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

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

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

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

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

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

J.5.7.3.3.2.1. Прочее (принципы SQL) #

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

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

-- До
SELECT *
  FROM orders
 WHERE order_id = 123;

-- После
SELECT *
  FROM orders
 WHERE order_id = 123
   AND store_id = 42; -- <== added

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

Наконец, при соединении таблиц обязательно используйте фильтр по tenant_id. Например, можно узнать, сколько пар «отличных шерстяных штанов» было продано в данном магазине:

-- Можно включить в соединение store_id и
-- добавить фильтр по этому значению в один из запросов

SELECT sum(l.quantity)
  FROM line_items l
 INNER JOIN products p
    ON l.product_id = p.product_id
   AND l.store_id = p.store_id
 WHERE p.name='Awesome Wool Pants'
   AND l.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'

-- Либо можно не добавлять store_id в условие соединения,
-- но отфильтровать по нему обе таблицы. Это может быть полезно
-- при построении запросов в ORM

SELECT sum(l.quantity)
  FROM line_items l
 INNER JOIN products p ON l.product_id = p.product_id
 WHERE p.name='Awesome Wool Pants'
   AND l.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'
   AND p.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'
J.5.7.3.3.3. Включение безопасных соединений #

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

J.5.7.3.3.3.1. Проверка межузлового трафика #

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

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

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

-- Задайте нужное имя базы данных

ALTER DATABASE citus SET citus.multi_task_query_log_level = 'error';

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

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

ALTER DATABASE citus SET citus.multi_task_query_log_level = 'log';

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

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

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

J.5.7.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. Протестируйте приложение.

J.5.7.4. Справка по SQL #

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

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

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

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

    SELECT citus_schema_distribute('user_service');

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

    Примечание

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

  • Включением параметра конфигурации enable_schema_based_sharding:

    SET citus.enable_schema_based_sharding TO ON;
    
    CREATE SCHEMA AUTHORIZATION user_service;

    Параметр можно изменять для текущего сеанса или глобально в файле postgresql.conf. Если для параметра установлено значение ON, все созданные схемы по умолчанию распределяются.

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

Чтобы преобразовать схему обратно в обычную схему Postgres Pro, используйте функцию citus_schema_undistribute:

SELECT citus_schema_undistribute('user_service');

Таблицы и данные в схеме user_service будут перемещены с текущего узла обратно на узел-координатор в кластере.

J.5.7.4.1.2. Создание и распределение таблиц #

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

CREATE TABLE github_events
(
    event_id bigint,
    event_type text,
    event_public boolean,
    repo_id bigint,
    payload jsonb,
    repo jsonb,
    actor jsonb,
    org jsonb,
    created_at timestamp
);

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

SELECT create_distributed_table('github_events', 'repo_id');

Эта функция сообщает citus, что таблица github_events должна быть распределена по столбцу repo_id (путём хеширования значения столбца). Функция также создаёт сегменты на рабочих узлах, используя параметр конфигурации citus.shard_count.

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

Каждому созданному сегменту присваивается уникальный идентификатор shard_id. Каждый сегмент представлен на рабочем узле как обычная таблица Postgres Pro с именем tablename_shardid, где tablename — имя распределённой таблицы, а shardid — уникальный идентификатор, присвоенный этому сегменту. К каждому экземпляру Postgres Pro можно подключаться для просмотра или выполнения команды на отдельных сегментах.

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

J.5.7.4.1.2.1. Таблицы-справочники #

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

Следующие таблицы хорошо подходят для преобразования в таблицы-справочники:

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

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

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

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

-- Таблица-справочник

CREATE TABLE states (
  code char(2) PRIMARY KEY,
  full_name text NOT NULL,
  general_sales_tax numeric(4,3)
);

-- Распределение этой таблицы на все рабочие узлы

SELECT create_reference_table('states');

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

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

Существующую распределённую таблицу можно преобразовать в таблицу-справочник так:

SELECT undistribute_table('имя_таблицы');
SELECT create_reference_table('имя_таблицы');

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

J.5.7.4.1.2.2. Распределение данных узла-координатора #

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

Описанная ранее функция create_distributed_table работает как с пустыми, так и с непустыми таблицами, причём в последних она автоматически распределяет строки таблицы по кластеру. Показателем проведённого распределения служит следующее сообщение: NOTICE: Copying data from local table... (ВНИМАНИЕ: Идёт копирование из локальной таблицы). Например:

CREATE TABLE series AS SELECT i FROM generate_series(1,1000000) i;
SELECT create_distributed_table('series', 'i');
NOTICE:  Copying data from local table...
NOTICE:  copying the data has completed
DETAIL:  The local data in the table is no longer visible, but is still on disk.
HINT:  To remove the local data, run: SELECT truncate_local_data_after_distributing_table($$public.series$$)
 create_distributed_table
 --------------------------

 (1 row)

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

При распределении таблиц A и B, где A ссылается на B по внешнему ключу, сначала распределите таблицу B. Если сделать это в неправильном порядке, возникнет ошибка:

ERROR:  cannot create foreign key constraint
DETAIL:  Referenced table must be a distributed table or a reference table.

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

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

J.5.7.4.1.3. Совмещение таблиц #

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

Таблицы совмещаются в группы. Чтобы вручную управлять назначением таблицы к группе для совмещения, используйте необязательный параметр colocate_with функции create_distributed_table. Если неважно, в какую группу будет включена таблица, не указывайте этот параметр. По умолчанию используется значение 'default', при котором таблица группируется с любой другой таблицей с таким же типом столбца распределения и таким же количеством сегментов. Если необходимо отменить или изменить такую автоматическую группировку, можно использовать функцию update_distributed_table_colocation.

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

SELECT create_distributed_table('A', 'столбец_int');
SELECT create_distributed_table('B', 'другой_столбец_int');

Если новая таблица не связана с другими таблицами в предполагаемой неявной группе совмещения, укажите colocated_with => 'none'.

-- Не совмещена с другими таблицами

SELECT create_distributed_table('A', 'foo', colocate_with => 'none');

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

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

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

-- Распределение таблицы с магазинами
SELECT create_distributed_table('stores', 'store_id');

-- Добавление новых таблиц в группу к первой таблице
SELECT create_distributed_table('orders', 'store_id', colocate_with => 'stores');
SELECT create_distributed_table('products', 'store_id', colocate_with => 'stores');

Информация о группах совмещения хранится в таблице pg_dist_colocation, а в таблице pg_dist_partition показана принадлежность таблиц к группам.

J.5.7.4.1.4. Удаление таблиц #

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

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

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

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

J.5.7.4.1.5.1. Добавление/изменение столбцов #

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

-- Добавление столбца

ALTER TABLE products ADD COLUMN description text;

-- Изменение значения по умолчанию

ALTER TABLE products ALTER COLUMN price SET DEFAULT 7.77;

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

Попытка изменить тип данных столбца вызовет ошибку:

-- Предположим, что store_id типа integer является столбцом
-- распределения для таблицы products

ALTER TABLE products
ALTER COLUMN store_id TYPE text;

/*
ERROR:  cannot execute ALTER TABLE command involving partition column
*/

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

J.5.7.4.1.5.2. Добавление/удаление ограничений #

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

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

Внешние ключи могут создаваться в следующих случаях:

  • Для двух локальных (нераспределённых) таблиц,

  • Для двух таблиц-справочников,

  • между таблицами-справочниками и локальными таблицами (по умолчанию включается с помощью параметра конфигурации citus.enable_local_reference_table_foreign_keys),

  • Между двумя совмещёнными распределёнными таблицами, если ключ содержит столбец распределения, или

  • Как ссылка из распределённой таблицы на таблицу-справочник.

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

В citus поддерживаются все ссылочные действия с внешними ключами от локальных таблиц к таблицам-справочникам, но не поддерживаются ON DELETE/ON UPDATE CASCADE в обратном направлении (ссылки на локальные таблицы).

Примечание

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

В примере ниже показано, как создавать первичные и внешние ключи в распределённых таблицах:

--
-- Добавление первичного ключа
-- --------------------

-- Распределите эти таблицы по account_id. Таблицы ads и clicks
-- должны использовать составные ключи, содержащие account_id.

ALTER TABLE accounts ADD PRIMARY KEY (id);
ALTER TABLE ads ADD PRIMARY KEY (account_id, id);
ALTER TABLE clicks ADD PRIMARY KEY (account_id, id);

-- Затем распределите таблицы

SELECT create_distributed_table('accounts', 'id');
SELECT create_distributed_table('ads',      'account_id');
SELECT create_distributed_table('clicks',   'account_id');

--
-- Добавьте внешние ключи
-- -------------------

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

ALTER TABLE ads ADD CONSTRAINT ads_account_fk
  FOREIGN KEY (account_id) REFERENCES accounts (id);
ALTER TABLE clicks ADD CONSTRAINT clicks_ad_fk
  FOREIGN KEY (account_id, ad_id) REFERENCES ads (account_id, id);

Аналогичным образом добавьте столбец распределения в ограничения уникальности:

-- Допустим, в каждой рекламе должно использоваться уникальное изображение. Обратите внимание,
-- что при распределении по account_id ограничение можно установить ограничение только по учётной записи.

ALTER TABLE ads ADD CONSTRAINT ads_unique_image
  UNIQUE (account_id, image_url);

Ограничения NOT NULL можно применять к любому столбцу (распределения или нет), поскольку для них не требуется поиск между рабочими узлами.

ALTER TABLE ads ALTER COLUMN image_url SET NOT NULL;
J.5.7.4.1.5.3. Использование ограничений NOT VALID #

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

Рассмотрим приложение, которое хранит профили пользователей в таблице-справочнике.

-- Здесь используется столбец типа text, но в настоящем приложении
-- может использоваться citext, который доступен в
-- дополнительном модуле Postgres Pro (contrib)

CREATE TABLE users ( email text PRIMARY KEY );
SELECT create_reference_table('users');

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

INSERT INTO users VALUES
   ('foo@example.com'), ('hacker12@aol.com'), ('lol');

Нужно проверить адреса, но Postgres Pro обычно не позволяет добавлять ограничение CHECK, которому не удовлетворяют существующие строки. Однако допускается ограничение NOT VALID:

ALTER TABLE users
ADD CONSTRAINT syntactic_email
CHECK (email ~
   '^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$'
) NOT VALID;

Эти команды выполнятся успешно, и новые строки будут проверяться.

INSERT INTO users VALUES ('fake');

/*
ERROR:  new row for relation "users_102010" violates
        check constraint "syntactic_email_102010"
DETAIL:  Failing row contains (fake).
*/

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

-- Попытка проверить все строки позднее
ALTER TABLE users
VALIDATE CONSTRAINT syntactic_email;

В документации Postgres Pro подробно описаны ограничения NOT VALID и VALIDATE CONSTRAINT в разделе ALTER TABLE.

J.5.7.4.1.5.4. Добавление/удаление индексов #

В citus поддерживается добавление и удаление индексов:

-- Добавление индекса

CREATE INDEX clicked_at_idx ON clicks USING BRIN (clicked_at);

-- Удаление индекса

DROP INDEX clicked_at_idx;

Для добавления индекса требуется блокировка операций записи, что может быть нежелательно в многоарендной «системе записи». Чтобы свести к минимуму время простоя приложения, создавайте индекс параллельно. Этот метод более трудоёмок, чем стандартное построение индекса, и его выполнение занимает значительно больше времени. Однако, поскольку такой метод позволяет продолжать выполнение обычных операций во время построения индекса, он более предпочтителен для добавления новых индексов в производственной среде.

-- Добавление индекса без блокировки операций записи в таблицу

CREATE INDEX CONCURRENTLY clicked_at_idx ON clicks USING BRIN (clicked_at);
J.5.7.4.1.6. Типы данных и функции #

Создание пользовательских типов данных и функций транслируется на рабочие узлы. Однако создание таких объектов базы данных в транзакциях с распределёнными операциями имеет некоторые особенности.

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

Рассмотрим блок транзакции, в котором создаются тип данных и таблица, данные загружаются, и таблица распределяется:

BEGIN;

-- Создание типа в одном соединении:
CREATE TYPE coordinates AS (x int, y int);
CREATE TABLE positions (object_id text primary key, position coordinates);

-- Загрузка данных происходит по одному соединению:
SELECT create_distributed_table('positions', 'object_id');
\COPY positions FROM 'positions.csv'

COMMIT;

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

BEGIN;
CREATE TABLE items (key text, value text);
-- Параллельная загрузка данных:
SELECT create_distributed_table('items', 'key');
\COPY items FROM 'items.csv'
CREATE TYPE coordinates AS (x int, y int);

ERROR:  cannot run type command because there was a parallel operation on a distributed table in the transaction

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

J.5.7.4.1.7. Внесение изменений вручную #

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

J.5.7.4.2. Внесение и изменение данных (DML) #
J.5.7.4.2.1. Вставка данных #

Для вставки данных в распределённые таблицы можно использовать стандартную команду Postgres Pro INSERT. В примере ниже вставляются две случайные строки из набора данных GitHub Archive.

/*
CREATE TABLE github_events
(
  event_id bigint,
  event_type text,
  event_public boolean,
  repo_id bigint,
  payload jsonb,
  repo jsonb,
  actor jsonb,
  org jsonb,
  created_at timestamp
);
*/

INSERT INTO github_events VALUES (2489373118,'PublicEvent','t',24509048,'{}','{"id": 24509048, "url": "https://api.github.com/repos/SabinaS/csee6868", "name": "SabinaS/csee6868"}','{"id": 2955009, "url": "https://api.github.com/users/SabinaS", "login": "SabinaS", "avatar_url": "https://avatars.githubusercontent.com/u/2955009?", "gravatar_id": ""}',NULL,'2015-01-01 00:09:13');

INSERT INTO github_events VALUES (2489368389,'WatchEvent','t',28229924,'{"action": "started"}','{"id": 28229924, "url": "https://api.github.com/repos/inf0rmer/blanket", "name": "inf0rmer/blanket"}','{"id": 1405427, "url": "https://api.github.com/users/tategakibunko", "login": "tategakibunko", "avatar_url": "https://avatars.githubusercontent.com/u/1405427?", "gravatar_id": ""}',NULL,'2015-01-01 00:00:24');

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

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

INSERT INTO github_events VALUES
  (
    2489373118,'PublicEvent','t',24509048,'{}','{"id": 24509048, "url": "https://api.github.com/repos/SabinaS/csee6868", "name": "SabinaS/csee6868"}','{"id": 2955009, "url": "https://api.github.com/users/SabinaS", "login": "SabinaS", "avatar_url": "https://avatars.githubusercontent.com/u/2955009?", "gravatar_id": ""}',NULL,'2015-01-01 00:09:13'
  ), (
    2489368389,'WatchEvent','t',28229924,'{"action": "started"}','{"id": 28229924, "url": "https://api.github.com/repos/inf0rmer/blanket", "name": "inf0rmer/blanket"}','{"id": 1405427, "url": "https://api.github.com/users/tategakibunko", "login": "tategakibunko", "avatar_url": "https://avatars.githubusercontent.com/u/1405427?", "gravatar_id": ""}',NULL,'2015-01-01 00:00:24'
  );
J.5.7.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, как описано в разделе Настройка Postgres Pro. Если в целевой таблице очень большое количество сегментов, возможно, стоит отключить пересекционирование, см. описание параметра конфигурации citus.enable_repartitioned_insert_select.

J.5.7.4.2.1.2. Команда \copy (массовая загрузка) #

Для массовой загрузки данных из файла можно напрямую использовать команду \copy.

Сначала загрузите пример с набором данных github_events:

wget http://examples.citusdata.com/github_archive/github_events-2015-01-01-{0..5}.csv.gz
gzip -d github_events-2015-01-01-*.gz

Затем скопируйте данные с помощью psql. Обратите внимание, что для этих данных требуется база данных с кодировкой UTF-8:

\COPY github_events FROM 'github_events-2015-01-01-0.csv' WITH (format CSV)

Примечание

Понятия изолированных снимков между сегментами не существует, то есть многосегментный запрос SELECT, который выполняется одновременно с командой \copy, может увидеть зафиксированные изменения в одних сегментах, а в других — нет. Если пользователь хранит данные о событиях, он может время от времени наблюдать небольшие пропуски в последних данных. Эта проблема может быть решена на уровне приложений (например, путём исключения последних данных из запросов или использования блокировки).

Если при выполнении команды \copy не удаётся открыть соединение для размещения сегмента, команда ведёт себя так же, как INSERT, а именно помечает размещение как неактивное, если нет других активных размещений. Если после подключения происходит какой-либо другой сбой, транзакция отменяется и, следовательно, метаданные не изменяются.

J.5.7.4.3. Кеширование агрегатных функций с помощью свёрток #

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

В примере рассматривается распределённая таблица для отслеживания просмотров страниц по URL:

CREATE TABLE page_views (
  site_id int,
  url text,
  host_ip inet,
  view_time timestamp default now(),

  PRIMARY KEY (site_id, url)
);

SELECT create_distributed_table('page_views', 'site_id');

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

-- Количество просмотров по каждому URL-адресу за день на сайте 5
SELECT view_time::date AS day, site_id, url, count(*) AS view_count
  FROM page_views
  WHERE site_id = 5 AND
    view_time >= date '2016-01-01' AND view_time < date '2017-01-01'
  GROUP BY view_time::date, site_id, url;

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

Для хранения ежедневной статистики можно создать таблицу daily_page_views.

CREATE TABLE daily_page_views (
  site_id int,
  day date,
  url text,
  view_count bigint,
  PRIMARY KEY (site_id, day, url)
);

SELECT create_distributed_table('daily_page_views', 'site_id');

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

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

-- Группирование вчерашних данных
INSERT INTO daily_page_views (day, site_id, url, view_count)
  SELECT view_time::date AS day, site_id, url, count(*) AS view_count
  FROM page_views
  WHERE view_time >= date '2017-01-01' AND view_time < date '2017-01-02'
  GROUP BY view_time::date, site_id, url;

-- Теперь результаты доступны прямо из таблицы
SELECT day, site_id, url, view_count
  FROM daily_page_views
  WHERE site_id = 5 AND
    day >= date '2016-01-01' AND day < date '2017-01-01';

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

Ситуация меняется, когда приходится иметь дело с данными, поступающими с задержкой, или выполнять запрос со свёрткой более одного раза в день. Если новые строки уже соответствуют дням в таблице свёртки, количество совпадений должно увеличиться. Postgres Pro может справиться с этой ситуацией с помощью ON CONFLICT — встроенным методом выполнения UPSERTS. Ниже представлен пример этого метода.

-- Группирование, начиная с указанной даты,
-- ежедневные просмотры страницы изменяются при необходимости
INSERT INTO daily_page_views (day, site_id, url, view_count)
  SELECT view_time::date AS day, site_id, url, count(*) AS view_count
  FROM page_views
  WHERE view_time >= date '2017-01-01'
  GROUP BY view_time::date, site_id, url
  ON CONFLICT (day, url, site_id) DO UPDATE SET
    view_count = daily_page_views.view_count + EXCLUDED.view_count;
J.5.7.4.3.1. Изменение и удаление #

Строки в распредёленных таблицах можно изменять или удалять с помощью стандартных команд Postgres Pro UPDATE и DELETE.

DELETE FROM github_events
WHERE repo_id IN (24509048, 24509049);

UPDATE github_events
SET event_public = TRUE
WHERE (org->>'id')::int = 5430905;

Когда операции UPDATE/DELETE затрагивают несколько сегментов, как в приведённом выше примере, citus по умолчанию использует протокол однофазной фиксации. Для повышения безопасности можно включить двухфазную фиксацию, установив параметр конфигурации citus.multi_shard_commit_protocol:

SET citus.multi_shard_commit_protocol = '2pc';

Если операция UPDATE или DELETE влияет только на один сегмент, то она выполняется в пределах одного рабочего узла, и в этом случае включение двухфазной фиксации не требуется. Такое часто происходит, когда операции обновления или удаления фильтруются по столбцу распределения таблицы:

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

DELETE FROM github_events
WHERE repo_id = 206084;

Кроме того, для работы с одним сегментом citus поддерживает метод SELECT… FOR UPDATE. Этот метод иногда используется объектно-реляционными преобразователями (ORM), чтобы безопасно выполнять следующие операции:

  • Загрузка строк

  • Вычисления в коде приложения

  • Изменение строк на основе вычислений

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

BEGIN;

  -- Обращение к событиям по repo_id, но
  -- с их блокировкой для записи
  SELECT *
  FROM github_events
  WHERE repo_id = 206084
  FOR UPDATE;

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

  -- Применение изменений
  UPDATE github_events
  SET event_public = :our_new_value
  WHERE repo_id = 206084;

COMMIT;

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

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

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

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

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

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

J.5.7.4.4.1. Агрегатные функции #

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

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

  • Если запрос агрегирует данные с группировкой не по столбцу распределения, citus всё равно может оптимизировать в зависимости от конкретного случая. В citus есть внутренние правила для определённых агрегатных функций, таких как sum(), avg() и count(distinct), которые позволяют перезаписывать запросы для частичного агрегирования на рабочих узлах. Например, чтобы вычислить среднее значение, citus получает сумму и количество от каждого рабочего узла, а затем узел-координатор вычисляет окончательное среднее значение.

    Полный список поддерживаемых агрегатных функций:

    avg, min, max, sum, count, array_agg, jsonb_agg, jsonb_object_agg, json_agg, json_object_agg, bit_and, bit_or, bool_and, bool_or, every, hll_add_agg, hll_union_agg, topn_add_agg, topn_union_agg, any_value, tdigest(double precision, int), tdigest_percentile(double precision, int, double precision), tdigest_percentile(double precision, int, double precision[]), tdigest_percentile(tdigest, double precision), tdigest_percentile(tdigest, double precision[]), tdigest_percentile_of(double precision, int, double precision), tdigest_percentile_of(double precision, int, double precision[]), tdigest_percentile_of(tdigest, double precision), tdigest_percentile_of(tdigest, double precision[])

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

Имейте в виду, что небольшие изменения в запросе могут изменить режимы выполнения, приводя к неожиданным результатам. Например, для функции sum(x), с группированием не по столбцу распределения можно использовать распределённое выполнение, а для функции sum(distinct x) — подтянуть полный набор входных записей на узел-координатор.

Чтобы нарушить выполнение всего запроса, достаточно одного столбца. В приведённом ниже примере, если функция sum(distinct value2) должна выполнять группирование на узле-координаторе, то sum(value1) также придется группировать там, даже если ей это не требовалось.

SELECT sum(value1), sum(distinct value2) FROM distributed_table;

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

SET citus.coordinator_aggregation_strategy TO 'disabled';

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

J.5.7.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).

J.5.7.4.4.1.2. Расчёт первых N элементов #

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

Расширение с открытым исходным кодом topn для Postgres Pro позволяет быстро получать приблизительные результаты для запросов типа «top-n». Расширение материализует первые значения в тип данных json. Расширение topn может постепенно обновлять эти значения или объединять их по мере необходимости по различным временным интервалам.

Прежде чем перейти к реалистичному примеру использования topn, рассмотрим, как работают некоторые его примитивные операции. Сначала topn_add изменяет объект JSON, подсчитывая количество просмотров ключа:

-- Сначала ничего нет, фиксируем, что увидели букву «а»
SELECT topn_add('{}', 'a');
-- => {"a": 1}

-- Фиксируем появление ещё одной буквы «а»
SELECT topn_add(topn_add('{}', 'a'), 'a');
-- => {"a": 2}

Расширение также предоставляет агрегатные функции для сканирования нескольких значений:

-- Для normal_rand
CREATE EXTENSION tablefunc;

-- Подсчёт значений из нормального распределения
SELECT topn_add_agg(floor(abs(i))::text)
  FROM normal_rand(1000, 5, 0.7) i;
-- => {"2": 1, "3": 74, "4": 420, "5": 425, "6": 77, "7": 3}

Если количество уникальных значений превышает предельную величину, агрегирование удаляет информацию о тех значениях, которые встречаются реже всего. Это позволяет контролировать использование пространства. Предельную величину можно установить с помощью параметра конфигурации topn.number_of_counters. Значение по умолчанию — 1000.

Теперь рассмотрим более реалистичный пример работы topn. Для этого возьмём обзоры продуктов Amazon за 2000 год и воспользуемся topn для быстрой генерации запросов. Сначала загрузите набор данных:

curl -L https://examples.citusdata.com/customer_reviews_2000.csv.gz | \
  gunzip > reviews.csv

Затем заполните распределённую таблицу этими данными:

CREATE TABLE customer_reviews
(
    customer_id TEXT,
    review_date DATE,
    review_rating INTEGER,
    review_votes INTEGER,
    review_helpful_votes INTEGER,
    product_id CHAR(10),
    product_title TEXT,
    product_sales_rank BIGINT,
    product_group TEXT,
    product_category TEXT,
    product_subcategory TEXT,
    similar_product_ids CHAR(10)[]
);

SELECT create_distributed_table('customer_reviews', 'product_id');

\COPY customer_reviews FROM 'reviews.csv' WITH CSV

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

-- Выполните указанную команду на узле-кординаторе, она будет транслирована и на рабочие узлы
CREATE EXTENSION topn;

-- Таблица для материализации ежедневно агрегируемых данных
CREATE TABLE reviews_by_day
(
  review_date date unique,
  agg_data jsonb
);

SELECT create_reference_table('reviews_by_day');

-- Материализовать количество отзывов по каждому продукту за день для каждого покупателя
INSERT INTO reviews_by_day
  SELECT review_date, topn_add_agg(product_id)
  FROM customer_reviews
  GROUP BY review_date;

Теперь вместо того, чтобы писать сложную оконную функцию для customer_reviews, можно просто применить topn к reviews_by_day. Например, в следующем запросе находится наиболее часто просматриваемый продукт за каждый из первых пяти дней:

SELECT review_date, (topn(agg_data, 1)).*
FROM reviews_by_day
ORDER BY review_date
LIMIT 5;
┌─────────────┬────────────┬───────────┐
│ review_date │    item    │ frequency │
├─────────────┼────────────┼───────────┤
│ 2000-01-01  │ 0939173344 │        12 │
│ 2000-01-02  │ B000050XY8 │        11 │
│ 2000-01-03  │ 0375404368 │        12 │
│ 2000-01-04  │ 0375408738 │        14 │
│ 2000-01-05  │ B00000J7J4 │        17 │
└─────────────┴────────────┴───────────┘

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

SELECT (topn(topn_union_agg(agg_data), 5)).*
FROM reviews_by_day
WHERE review_date >= '2000-01-01' AND review_date < '2000-02-01'
ORDER BY 2 DESC;
┌────────────┬───────────┐
│    item    │ frequency │
├────────────┼───────────┤
│ 0375404368 │       217 │
│ 0345417623 │       217 │
│ 0375404376 │       217 │
│ 0375408738 │       217 │
│ 043936213X │       204 │
└────────────┴───────────┘

За подробным описанием и примерами обратитесь к файлу readme topn.

J.5.7.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 можно найти в документации расширения.

J.5.7.4.4.2. Ограниченный вынос наружу #

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

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

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

SET citus.limit_clause_row_fetch_count TO 10000;
J.5.7.4.4.3. Представления по распределённым таблицам #

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

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

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

J.5.7.4.4.4. Соединения #

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

J.5.7.4.4.4.1. Совмещённые соединения #

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

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

Примечание

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

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

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

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

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

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

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

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

J.5.7.4.5. Обработка запросов #

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

Рисунок J.16. Архитектура обработки запросов


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

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

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

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

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

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

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

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

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

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

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

J.5.7.4.5.2.1. Двухэтапное выполнение подзапросов/CTE #

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

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

SELECT page_id, count(distinct host_ip)
FROM page_views
WHERE page_id IN (
  SELECT page_id
  FROM page_views
  GROUP BY page_id
  ORDER BY count(*) DESC
  LIMIT 20
)
GROUP BY page_id;

Исполнитель выполнил бы фрагмент этого запроса для каждого сегмента по page_id, подсчитав отдельные host_ips и объединив результаты на узле-координаторе. Однако LIMIT в подзапросе означает, что его нельзя выполнить как часть фрагмента. При рекурсивном планировании запроса citus может запускать подзапрос отдельно, передавать результаты всем рабочим узлам, выполнять основной запрос-фрагмент и возвращать результаты узлу-координатору. Механизм «push-pull» поддерживает подзапросы, подобные описанному выше.

Рассмотрим на рабочем примере выходные данные EXPLAIN для этого запроса:

GroupAggregate  (cost=0.00..0.00 rows=0 width=0)
  Group Key: remote_scan.page_id
  ->  Sort  (cost=0.00..0.00 rows=0 width=0)
    Sort Key: remote_scan.page_id
    ->  Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=0 width=0)
      ->  Distributed Subplan 6_1
        ->  Limit  (cost=0.00..0.00 rows=0 width=0)
          ->  Sort  (cost=0.00..0.00 rows=0 width=0)
            Sort Key: COALESCE((pg_catalog.sum((COALESCE((pg_catalog.sum(remote_scan.worker_column_2))::bigint, '0'::bigint))))::bigint, '0'::bigint) DESC
            ->  HashAggregate  (cost=0.00..0.00 rows=0 width=0)
              Group Key: remote_scan.page_id
              ->  Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=0 width=0)
                Task Count: 32
                Tasks Shown: One of 32
                ->  Task
                  Node: host=localhost port=9701 dbname=postgres
                  ->  HashAggregate  (cost=54.70..56.70 rows=200 width=12)
                    Group Key: page_id
                    ->  Seq Scan on page_views_102008 page_views  (cost=0.00..43.47 rows=2247 width=4)
      Task Count: 32
      Tasks Shown: One of 32
      ->  Task
        Node: host=localhost port=9701 dbname=postgres
        ->  HashAggregate  (cost=84.50..86.75 rows=225 width=36)
          Group Key: page_views.page_id, page_views.host_ip
          ->  Hash Join  (cost=17.00..78.88 rows=1124 width=36)
            Hash Cond: (page_views.page_id = intermediate_result.page_id)
            ->  Seq Scan on page_views_102008 page_views  (cost=0.00..43.47 rows=2247 width=36)
            ->  Hash  (cost=14.50..14.50 rows=200 width=4)
              ->  HashAggregate  (cost=12.50..14.50 rows=200 width=4)
                Group Key: intermediate_result.page_id
                ->  Function Scan on read_intermediate_result intermediate_result  (cost=0.00..10.00 rows=1000 width=4)

Разобьём план на части и рассмотрим каждую отдельно.

GroupAggregate  (cost=0.00..0.00 rows=0 width=0)
  Group Key: remote_scan.page_id
  ->  Sort  (cost=0.00..0.00 rows=0 width=0)
    Sort Key: remote_scan.page_id

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

->  Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=0 width=0)
  ->  Distributed Subplan 6_1
.

У выборочного сканирования есть два больших поддерева, начинающихся с «распределённого подплана».

->  Limit  (cost=0.00..0.00 rows=0 width=0)
  ->  Sort  (cost=0.00..0.00 rows=0 width=0)
    Sort Key: COALESCE((pg_catalog.sum((COALESCE((pg_catalog.sum(remote_scan.worker_column_2))::bigint, '0'::bigint))))::bigint, '0'::bigint) DESC
    ->  HashAggregate  (cost=0.00..0.00 rows=0 width=0)
      Group Key: remote_scan.page_id
      ->  Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=0 width=0)
        Task Count: 32
        Tasks Shown: One of 32
        ->  Task
          Node: host=localhost port=9701 dbname=postgres
          ->  HashAggregate  (cost=54.70..56.70 rows=200 width=12)
            Group Key: page_id
            ->  Seq Scan on page_views_102008 page_views  (cost=0.00..43.47 rows=2247 width=4)
.

Рабочие узлы выполняют вышеуказанные операции для каждого из тридцати двух сегментов (citus выбирает для отображения один из них). Все части подзапроса IN (…) легко узнаваемы: сортировка, группирование и ограничение. Когда этот запрос завершается на всех рабочих узлах, они отправляют результаты обратно узлу-координатору, который объединяет их как «промежуточные результаты».

Task Count: 32
Tasks Shown: One of 32
->  Task
  Node: host=localhost port=9701 dbname=postgres
  ->  HashAggregate  (cost=84.50..86.75 rows=225 width=36)
    Group Key: page_views.page_id, page_views.host_ip
    ->  Hash Join  (cost=17.00..78.88 rows=1124 width=36)
      Hash Cond: (page_views.page_id = intermediate_result.page_id)
.

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

->  Seq Scan on page_views_102008 page_views  (cost=0.00..43.47 rows=2247 width=36)
->  Hash  (cost=14.50..14.50 rows=200 width=4)
  ->  HashAggregate  (cost=12.50..14.50 rows=200 width=4)
    Group Key: intermediate_result.page_id
    ->  Function Scan on read_intermediate_result intermediate_result  (cost=0.00..10.00 rows=1000 width=4)
.

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

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

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

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

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

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

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

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

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

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

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

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

Примечание

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

Примечание

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

J.5.7.4.6.2. Выполнение во всех сегментах #

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

Функция run_command_on_shards применяет SQL-команду к каждому сегменту, где имя сегмента предоставляется для подстановки в команде. Ниже приведён пример оценки количества строк для распределённой таблицы с использованием таблицы pg_class на каждом рабочем узле, чтобы оценить количество строк для каждого сегмента. Обратите внимание на параметр %s, который будет заменён на имя каждого сегмента.

-- Получить расчётное количество строк для распределённой таблицы, суммируя
-- расчётное количество строк для каждого сегмента.
SELECT sum(result::bigint) AS estimated_count
  FROM run_command_on_shards(
    'my_distributed_table',
    $cmd$
      SELECT reltuples
        FROM pg_class c
        JOIN pg_catalog.pg_namespace n on n.oid=c.relnamespace
       WHERE (n.nspname || '.' || relname)::regclass = '%s'::regclass
         AND n.nspname NOT IN ('citus', 'pg_toast', 'pg_catalog')
    $cmd$
  );

Полезным дополнением к run_command_on_shards является функция run_command_on_colocated_placements. Она подставляет в запрос имена двух мест размещения совмещённых таблиц. Пары размещения всегда выбираются на одном и том же рабочем узле с полной поддержкой SQL. Таким образом, можно использовать расширенную функциональность SQL, такую как триггеры, для связи таблиц:

-- Предположим, есть две распределённые таблицы
CREATE TABLE little_vals (key int, val int);
CREATE TABLE big_vals    (key int, val int);
SELECT create_distributed_table('little_vals', 'key');
SELECT create_distributed_table('big_vals',    'key');

-- Необходимо синхронизировать их, чтобы каждый раз при создании
-- little_vals, также создавалось значение big_vals, равное удвоенному значению little_vals
--
-- Сначала создайте триггерную функцию, которая будет
-- принимать в качестве аргумента размещение целевой таблицы
CREATE OR REPLACE FUNCTION embiggen() RETURNS TRIGGER AS $$
  BEGIN
    IF (TG_OP = 'INSERT') THEN
      EXECUTE format(
        'INSERT INTO %s (key, val) SELECT ($1).key, ($1).val*2;',
        TG_ARGV[0]
      ) USING NEW;
    END IF;
    RETURN NULL;
  END;
$$ LANGUAGE plpgsql;

-- Затем свяжите совмещённые таблицы с помощью триггерной функции
-- на каждом размещении
SELECT run_command_on_colocated_placements(
  'little_vals',
  'big_vals',
  $cmd$
    CREATE TRIGGER after_insert AFTER INSERT ON %s
      FOR EACH ROW EXECUTE PROCEDURE embiggen(%L)
  $cmd$
);
J.5.7.4.6.3. Ограничения #
  • Нет защиты от взаимоблокировок для транзакций с несколькими операторами.

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

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

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

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

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

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

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

J.5.7.4.7.1. Ограничения #
J.5.7.4.7.1.1. Общие #

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Локальная

    Локальная

    Да

    Локальная

    Справка

    Да

    Локальная

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

    Нет

    В разработке

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

    Локальная

    Да

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

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

    Да

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

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

    Справка

    Да

    Справка

    Любая

    Нет

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

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

J.5.7.4.7.1.3. SQL-совместимость сегментирования на основе схем #

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

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

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

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

J.5.7.4.7.2. Обходные решения #

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

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

J.5.7.4.7.2.1. Обход ограничений с помощью CTE #

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

SELECT * FROM dist WHERE EXISTS (SELECT 1 FROM local WHERE local.a = dist.a);
/*
ERROR:  direct joins between distributed and local tables are not supported
HINT:  Use CTEs or subqueries to select from local tables and use them in joins
*/

Чтобы обойти это ограничение, можно превратить запрос в запрос маршрутизатора, обернув распределённую часть в CTE.

WITH cte AS (SELECT * FROM dist)
SELECT * FROM cte WHERE EXISTS (SELECT 1 FROM local WHERE local.a = cte.a);

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

J.5.7.4.7.2.2. Временные таблицы: крайние меры #

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

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

-- Такой запрос не работает

  SELECT repo_id, event_type, event_public,
         grouping(event_type, event_public),
         min(created_at)
    FROM github_events
   WHERE repo_id IN (8514, 15435, 19438, 21692)
GROUP BY repo_id, ROLLUP(event_type, event_public);
ERROR:  could not run distributed query with GROUPING
HINT:  Consider using an equality filter on the distributed table's partition column.

Но есть одна хитрость. Нужную информацию можно передать координатору в виде временной таблицы:

-- Размещение данных без агрегирования в локальную таблицу

CREATE TEMP TABLE results AS (
  SELECT repo_id, event_type, event_public, created_at
    FROM github_events
       WHERE repo_id IN (8514, 15435, 19438, 21692)
    );

-- Запуск агрегирования локально

  SELECT repo_id, event_type, event_public,
         grouping(event_type, event_public),
         min(created_at)
    FROM results
GROUP BY repo_id, ROLLUP(event_type, event_public);
 repo_id |    event_type     | event_public | grouping |         min
---------+-------------------+--------------+----------+---------------------
    8514 | PullRequestEvent  | t            |        0 | 2016-12-01 05:32:54
    8514 | IssueCommentEvent | t            |        0 | 2016-12-01 05:32:57
   19438 | IssueCommentEvent | t            |        0 | 2016-12-01 05:48:56
   21692 | WatchEvent        | t            |        0 | 2016-12-01 06:01:23
   15435 | WatchEvent        | t            |        0 | 2016-12-01 05:40:24
   21692 | WatchEvent        |              |        1 | 2016-12-01 06:01:23
   15435 | WatchEvent        |              |        1 | 2016-12-01 05:40:24
    8514 | PullRequestEvent  |              |        1 | 2016-12-01 05:32:54
    8514 | IssueCommentEvent |              |        1 | 2016-12-01 05:32:57
   19438 | IssueCommentEvent |              |        1 | 2016-12-01 05:48:56
   15435 |                   |              |        3 | 2016-12-01 05:40:24
   21692 |                   |              |        3 | 2016-12-01 06:01:23
   19438 |                   |              |        3 | 2016-12-01 05:48:56
    8514 |                   |              |        3 | 2016-12-01 05:32:54

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

J.5.7.4.7.2.3. Подзапросы в запросах INSERT #

Попробуйте переписать свои запросы с использованием синтаксиса INSERT INTO ... SELECT.

Следующий SQL-код:

INSERT INTO a.widgets (map_id, widget_name)
VALUES (
    (SELECT mt.map_id FROM a.map_tags mt WHERE mt.map_license = '12345'),
    'Test'
);

Станет таким:

INSERT INTO a.widgets (map_id, widget_name)
SELECT mt.map_id, 'Test'
  FROM a.map_tags mt
 WHERE mt.map_license = '12345';

J.5.7.5. API в citus #

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

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

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

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

Аргументы:

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

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

SELECT citus_schema_distribute('tenant_a');
SELECT citus_schema_distribute('tenant_b');
SELECT citus_schema_distribute('tenant_c');
citus_schema_undistribute (schemaname regnamespace) returns void #

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

Аргументы:

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

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

SELECT citus_schema_undistribute('tenant_a');
SELECT citus_schema_undistribute('tenant_b');
SELECT citus_schema_undistribute('tenant_c');
create_distributed_table (table_name regclass, distribution_column text, distribution_type citus.distribution_type, colocate_with text, shard_count int) returns void #

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

Аргументы:

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

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

  • distribution_type — метод распределения (необязательный аргумент). Значение по умолчанию — hash.

  • colocate_with — включить текущую таблицу в группу совмещения другой таблицы. Это необязательный аргумент. По умолчанию таблицы совмещаются, если они распределены по столбцам одного типа с одинаковым количеством сегментов. Если позднее понадобится избавиться от этого совмещения, можно использовать функцию update_distributed_table_colocation. Возможные значения этого аргумента: default — значение по умолчанию;none — для создания новой группы совмещения; имя другой таблицы — для совмещения с ней. За подробностями обратитесь к разделу Совмещение таблиц.

    Имейте в виду, что значение по умолчанию аргумента colocate_with подразумевает неявное совместное размещение. Как поясняется в разделе Совмещение таблиц, оно может оказаться полезным, если таблицы связаны или будут объединены. Однако если две таблицы не связаны, но используют один и тот же тип данных для своих столбцов распределения, их случайное совмещение может снизить производительность во время перебалансировки сегментов. Сегменты таблицы могут быть совмещены в «CASCADE». Чтобы разорвать это неявное совмещение, можно использовать функцию update_distributed_table_colocation.

    Если новая распределённая таблица не связана с другими таблицами, лучше всего указать colocate_with => 'none'.

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

    Допустимые значения для аргумента shard_count: от 1 до 64000. За рекомендациями по выбору оптимального значения обратитесь к разделу Количество сегментов.

В данном примере база данных получает информацию, что таблица github_events должна распределяться по хешу по столбцу repo_id. За дополнительными примерами обратитесь к разделу Создание и изменение распределённых объектов (DDL):

SELECT create_distributed_table('github_events', 'repo_id');

-- Также можно указать совмещение явно:
SELECT create_distributed_table('github_events', 'repo_id',
                                colocate_with => 'github_repo');
truncate_local_data_after_distributing_table (function_name regclass) returns void #

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

ERROR:  cannot truncate a table referenced in a foreign key constraint by a local table

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

Аргументы:

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

Пример использования этой функции:

-- Аргумент должен быть распределённой таблицей
SELECT truncate_local_data_after_distributing_table('public.github_events');
undistribute_table (table_name regclass, cascade_via_foreign_keys boolean) returns void #

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

В citus не отменяется распределение таблиц, которые имеют внешние ключи или на которые ссылаются внешние ключи, кроме случая, когда аргумент cascade_via_foreign_keys имеет значение true. Если этот аргумент имеет значение false (или опущен), необходимо вручную удалить нарушающие ограничения внешнего ключа перед отменой распределения.

Аргументы:

  • table_name — имя распределённой таблицы или таблицы-справочника, для которой будет отменено распределение.

  • cascade_via_foreign_keys — если для этого необязательного аргумента установлено значение true, функция также отменяет распределение всех таблиц, связанных с table_name через внешние ключи. Этот аргумент следует использовать с осторожностью, поскольку потенциально он может затронуть множество таблиц. Значение по умолчанию — false.

Пример, как распределить таблицу github_events, а затем отменить распределение:

-- Распределение таблицы
SELECT create_distributed_table('github_events', 'repo_id');

-- Отмена распределение и превращение таблицы обратно в локальную
SELECT undistribute_table('github_events');
alter_distributed_table (table_name regclass, distribution_column text, shard_count int, colocate_with text, cascade_to_colocated boolean) returns void #

Изменяет столбец распределения, количество сегментов или параметры совмещения распределённой таблицы.

Аргументы:

  • table_name — имя изменяемой распределённой таблицы.

  • distribution_column — имя нового столбца распределения. Этот необязательный аргумент по умолчанию имеет значение NULL.

  • shard_count — новое количество сегментов. Этот необязательный аргумент по умолчанию имеет значение NULL.

  • colocate_with — таблица, с которой будет совмещена текущая распределённая таблица. Возможные значения: default, none, чтобы создать новую группу совмещения, или имя другой таблицы, с которой будет выполняться совмещение. Этот необязательный аргумент по умолчанию имеет значение default.

  • cascade_to_colocated. Если для этого аргумента установлено значение true, изменения shard_count и colocate_with также будут применены ко всем таблицам, которые ранее были совмещены с указанной, и совмещение будет сохранено. Если задано значение false, текущее совмещение этой таблицы будет нарушено. Этот необязательный аргумент по умолчанию имеет значение false.

Пример использования этой функции:

-- Изменение столбца распределения
SELECT alter_distributed_table('github_events', distribution_column:='event_id');

-- Изменение количества сегментов всех таблиц в группах совмещения
SELECT alter_distributed_table('github_events', shard_count:=6, cascade_to_colocated:=true);

-- Изменение совмещения
SELECT alter_distributed_table('github_events', colocate_with:='another_table');
alter_table_set_access_method (table_name regclass, access_method text) returns void #

Изменяет метод доступа к таблице (например, heap или columnar).

Аргументы:

  • table_name — имя таблицы, для которой будет изменён метод доступа.

  • access_method — имя нового метода доступа.

Пример использования этой функции:

SELECT alter_table_set_access_method('github_events', 'columnar');
remove_local_tables_from_metadata () returns void #

Удаляет ненужные локальные таблицы из метаданных расширения citus. (См. параметр конфигурации citus.enable_local_reference_table_foreign_keys.)

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

create_reference_table (table_name regclass) returns void #

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

Аргументы:

  • table_name — имя небольшой таблицы или таблицы-справочника, которая будет распределяться.

В указанном примере таблица nation в БД определяется как таблица-справочник:

SELECT create_reference_table('nation');
citus_add_local_table_to_metadata (table_name regclass, cascade_via_foreign_keys boolean) returns void #

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

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

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

Аргументы:

  • table_name — имя таблицы на узле-координаторе, которая будет добавлена в метаданные citus.

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

В примере ниже таблица nation определяется как локальная таблица координатора, доступная с любого узла:

SELECT citus_add_local_table_to_metadata('nation');
update_distributed_table_colocation (table_name regclass, colocate_with text) returns void #

Изменяет совмещение распределённой таблицы. Эту функцию также можно использовать для отмены совмещения распределённой таблицы. Расширение citus будет неявно совмещать две таблицы, если столбец распределения имеет один и тот же тип. Это полезно в случае, если таблицы связаны и будут соединяться. Если таблицы A и B совмещены и таблица A перебалансируется, таблица B также будет перебалансирована. Если таблица B не имеет идентификатора реплики, перебалансировка завершится ошибкой. Таким образом, функция может быть полезна для отмены неявного совмещения. Обратите внимание, что эта функция не перемещает данные физически.

Аргументы:

  • table_name — имя таблицы, совмещение которой будет изменено.

  • colocate_with — таблица, с которой будет совмещена указанная таблица.

Чтобы отменить совмещение таблицы, укажите colocate_with => 'none'.

В примере ниже показано, как совмещение таблицы A изменяется вместе с совмещением таблицы B:

SELECT update_distributed_table_colocation('A', colocate_with => 'B');

Предположим, что таблицы A и B совмещены (возможно, неявно). Чтобы отменить совмещение, сделайте следующее:

SELECT update_distributed_table_colocation('A', colocate_with => 'none');

Теперь предположим, что таблицы A, B, C и D совмещены, и необходимо совместить таблицу A с B и таблицу C с D:

SELECT update_distributed_table_colocation('C', colocate_with => 'none');
SELECT update_distributed_table_colocation('D', colocate_with => 'C');

Чтобы изменить совмещение распределённой по хешу таблицы с именем none, выполните:

SELECT update_distributed_table_colocation('"none"', colocate_with => 'другая_распределённая_по_хешу_таблица');
create_distributed_function (function_name regprocedure, distribution_arg_name text, colocate_with text, force_delegation bool) returns void #

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

Обратите внимание, что путь поиска Postgres Pro не транслируется с узла-координатора на рабочие узлы во время выполнения распределённой функции, поэтому код распределённой функции должен быть дополнен именами объектов базы данных. Уведомления, выдаваемые функциями, не будут показываться пользователю.

Аргументы:

  • function_name — имя распределяемой функции. Имя должно включать типы параметров функции в круглых скобках, поскольку несколько функций могут иметь одно и то же имя в Postgres Pro. Например, 'foo(int)' отличается от 'foo(int, text)'.

  • distribution_arg_name — имя аргумента, по которому осуществляется распределение. Для удобства (или если у аргументов функции нет имён) можно использовать позиционный заполнитель, например '$1'. Если этот аргумент не указан, то на рабочих узлах создастся функция, название которой передано в аргументе function_name. Если в будущем будут добавляться новые рабочие узлы, функция будет автоматически создаваться и на них. Это необязательный аргумент.

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

  • force_delegation. Значение по умолчанию — NULL.

Пример использования этой функции:

-- Пример функции, которая изменяет вымышленную таблицу
-- event_responses, которая распределена по event_id
CREATE OR REPLACE FUNCTION
  register_for_event(p_event_id int, p_user_id int)
RETURNS void LANGUAGE plpgsql AS $fn$
BEGIN
  INSERT INTO event_responses VALUES ($1, $2, 'yes')
  ON CONFLICT (event_id, user_id)
  DO UPDATE SET response = EXCLUDED.response;
END;
$fn$;

-- Распределение функции по рабочим узлам, используя аргумент p_event_id,
-- чтобы определить, на какой сегмент влияет каждый её вызов, и явное
-- совмещение с таблицей event_responses, обновляемой этой функцией
SELECT create_distributed_function(
  'register_for_event(int, int)', 'p_event_id',
  colocate_with := 'event_responses'
);
alter_columnar_table_set (table_name regclass, chunk_group_row_limit int, stripe_row_limit int, compression name, compression_level int) returns void #

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

Чтобы просмотреть текущие параметры для всех столбцовых таблиц, воспользуйтесь этой таблицей:

SELECT * FROM columnar.options;

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

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

J.5.7.5.1.3. Функции для управления и восстановления кластера #
citus_move_shard_placement (shard_id bigint, source_node_name text, source_node_port integer, target_node_name text, target_node_port integer, shard_transfer_mode citus.shard_transfer_mode) returns void #

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

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

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

Аргументы:

  • shard_id — идентификатор перемещаемого сегмента.

  • source_node_name — DNS-имя узла, на котором есть работоспособное размещение сегмента («исходный» узел).

  • source_node_port — порт на исходном рабочем узле, через который сервер БД принимает подключения.

  • target_node_name — DNS-имя узла, на котором есть недопустимое размещение сегмента («целевой» узел).

  • target_node_port — порт на целевом рабочем узле, через который сервер БД принимает подключения.

  • shard_transfer_mode — указать метод репликации: логическая репликация Postgres Pro или команда COPY между рабочими узлами. Этот необязательный аргумент может принимать следующие значения:

    • auto — требовать идентификатор реплики, если возможна логическая репликация, в противном случае использовать ранее принятое поведение. Это значение по умолчанию.

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

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

Пример использования этой функции:

SELECT citus_move_shard_placement(12345, 'с_узла', 5432, 'на_узел', 5432);
citus_rebalance_start (rebalance_strategy name, drain_only boolean, shard_transfer_mode citus.shard_transfer_mode) returns bigint #

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

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

Каждому сегменту назначается стоимость при определении «равномерности распределения сегментов». По умолчанию каждый сегмент имеет одинаковую стоимость (значение 1), поэтому распределение для выравнивания стоимости между рабочими узлами аналогично выравниванию количества сегментов на каждом узле. Стратегия постоянной стоимости называется by_shard_count и является стратегией перебалансировки по умолчанию.

Стратегия by_shard_count подходит для использования в следующих условиях:

  • Сегменты примерно одного размера.

  • Сегменты получают примерно одинаковый объём трафика.

  • Все рабочие узлы одинакового размера/типа.

  • Сегменты не прикреплены к конкретным рабочим узлам.

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

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

Стратегия перебалансировки по умолчанию — by_disk_size. Чтобы настроить стратегию, используйте параметр rebalance_strategy.

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

Аргументы:

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

  • При значении true аргумента drain_only сегменты перемещаются только с тех рабочих узлов, у которых в таблице pg_dist_node для параметра shouldhaveshards установлено значение false. Этот необязательный аргумент по умолчанию имеет значение false.

  • shard_transfer_mode — указать метод репликации: логическая репликация Postgres Pro или команда COPY между рабочими узлами. Этот необязательный аргумент может принимать следующие значения:

    • auto — требовать идентификатор реплики, если возможна логическая репликация, в противном случае использовать ранее принятое поведение. Это значение по умолчанию.

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

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

В примере показана попытка перебалансировки сегментов:

SELECT citus_rebalance_start();
NOTICE:  Scheduling...
NOTICE:  Scheduled as job 1337.
DETAIL:  Rebalance scheduled as background job 1337.
HINT:  To monitor progress, run: SELECT details FROM citus_rebalance_status();
citus_rebalance_status () returns table #

Позволяет следить за ходом перебалансировки. Результат возвращается сразу, а перебалансировка продолжается в фоновом режиме.

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

SELECT * FROM citus_rebalance_status();
.
 job_id |  state   | job_type  |           description           |          started_at           |          finished_at          | details
--------+----------+-----------+---------------------------------+-------------------------------+-------------------------------+-----------
      4 | running  | rebalance | Rebalance colocation group 1    | 2022-08-09 21:57:27.833055+02 | 2022-08-09 21:57:27.833055+02 | { ... }

Особенности перебалансировщика находятся в столбце details в формате JSON:

SELECT details FROM citus_rebalance_status();
{
    "phase": "copy",
    "phase_index": 1,
    "phase_count": 3,
    "last_change":"2022-08-09 21:57:27",
    "colocations": {
        "1": {
            "shard_moves": 30,
            "shard_moved": 29,
            "last_move":"2022-08-09 21:57:27"
        },
        "1337": {
            "shard_moves": 130,
            "shard_moved": 0
        }
    }
}
citus_rebalance_stop () returns void #

Отменяет выполняющуюся перебалансировку, если она есть.

citus_rebalance_wait () returns void #

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

Функцию можно использовать в скриптах или для тестирования производительности.

get_rebalance_table_shards_plan () returns table #

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

  • table_name — таблица, сегменты которой будут перемещаться.

  • shardid — нужный сегмент.

  • shard_size — размер сегмента в байтах.

  • sourcename — адрес исходного узла.

  • sourceport — порт исходного узла.

  • targetname — адрес целевого узла.

  • targetport — порт целевого узла.

Аргументы:

  • Расширенный набор аргументов для функции citus_rebalance_start: relation, threshold, max_shard_moves, excluded_shard_list и drain_only.

get_rebalance_progress () returns table #

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

  • sessionid — идентификатор серверного процесса (PID) отслеживания перебалансировки в Postgres Pro.

  • table_name — имя таблицы, сегменты которой будут перемещены.

  • shardid — нужный сегмент.

  • shard_size — размер сегмента в байтах.

  • sourcename — адрес исходного узла.

  • sourceport — порт исходного узла.

  • targetname — адрес целевого узла.

  • targetport — порт целевого узла.

  • progress. Могут возвращаться следующие значения: 0 — ожидает перемещения, 1 — перемещение, 2 — перемещение завершено.

  • source_shard_size — размер сегмента на исходном узле в байтах.

  • target_shard_size — размер сегмента на целевом узле в байтах.

Пример использования этой функции:

SELECT * FROM get_rebalance_progress();
┌───────────┬────────────┬─────────┬────────────┬───────────────┬────────────┬───────────────┬────────────┬──────────┬───────────────────┬───────────────────┐
│ sessionid │ table_name │ shardid │ shard_size │  sourcename   │ sourceport │  targetname   │ targetport │ progress │ source_shard_size │ target_shard_size │
├───────────┼────────────┼─────────┼────────────┼───────────────┼────────────┼───────────────┼────────────┼──────────┼───────────────────┼───────────────────┤
│      7083 │ foo        │  102008 │    1204224 │ n1.foobar.com │       5432 │ n4.foobar.com │       5432 │        0 │           1204224 │                 0 │
│      7083 │ foo        │  102009 │    1802240 │ n1.foobar.com │       5432 │ n4.foobar.com │       5432 │        0 │           1802240 │                 0 │
│      7083 │ foo        │  102018 │     614400 │ n2.foobar.com │       5432 │ n4.foobar.com │       5432 │        1 │            614400 │            354400 │
│      7083 │ foo        │  102019 │       8192 │ n3.foobar.com │       5432 │ n4.foobar.com │       5432 │        2 │                 0 │              8192 │
└───────────┴────────────┴─────────┴────────────┴───────────────┴────────────┴───────────────┴────────────┴──────────┴───────────────────┴───────────────────┘
citus_add_rebalance_strategy (name name, shard_cost_function regproc, node_capacity_function regproc, shard_allowed_on_node_function regproc, default_threshold float4, minimum_threshold float4, improvement_threshold float4) returns void #

Добавляет строку в таблицу pg_dist_rebalance_strategy.

Аргументы:

  • name — идентификатор новой стратегии.

  • shard_cost_function — определяет функцию, используемую для расчёта «стоимости» каждого сегмента.

  • node_capacity_function — определяет функцию измерения ёмкости узла.

  • shard_allowed_on_node_function — идентифицирует функцию, которая определяет, какие сегменты и на каких узлах можно разместить.

  • default_threshold — порог с плавающей точкой, настраивающий точность балансировки совокупной стоимости сегментов между узлами.

  • minimum_threshold — столбец защиты, содержащий минимально допустимое значение для задающего порог аргумента функции citus_rebalance_start. Значение по умолчанию — 0.

  • improvement_threshold. Значение по умолчанию — 0.

citus_set_default_rebalance_strategy (name text) returns void #

Стратегия, указанная в аргументе данной функции, устанавливается в таблице pg_dist_rebalance_strategy как выбираемая по умолчанию при перебалансировке сегментов.

Аргументы:

  • name — имя стратегии в таблице pg_dist_rebalance_strategy.

Пример использования этой функции:

SELECT citus_set_default_rebalance_strategy('by_disk_size');
citus_remote_connection_stats () returns setof record #

Показывает количество активных соединений с каждым удалённым узлом.

Пример использования этой функции:

SELECT * FROM citus_remote_connection_stats();
.
    hostname    | port | database_name | connection_count_to_node
----------------+------+---------------+--------------------------
 citus_worker_1 | 5432 | postgres      |                        3
(1 row)
citus_drain_node (nodename text, nodeport integer, shard_transfer_mode citus.shard_transfer_mode, rebalance_strategy name) returns void #

Перемещает сегменты с указанного узла на другие узлы, у которых параметр shouldhaveshards в таблице pg_dist_node имеет значение true. Эту функцию следует вызывать перед удалением узла из кластера, т. е. отключением физического сервера узла.

Аргументы:

  • nodename — DNS-имя узла, с которого перемещаются сегменты.

  • nodeport — номер порта узла, с которого перемещаются сегменты.

  • shard_transfer_mode — указать метод репликации: логическая репликация Postgres Pro или команда COPY между рабочими узлами. Этот необязательный аргумент может принимать следующие значения:

    • auto — требовать идентификатор реплики, если возможна логическая репликация, в противном случае использовать ранее принятое поведение. Это значение по умолчанию.

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

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

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

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

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

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

В примере ниже показано, как создать новый сегмент для хранения позиций для арендатора 135:

SELECT isolate_tenant_to_new_shard('lineitem', 135);
┌─────────────────────────────┐
│ isolate_tenant_to_new_shard │
├─────────────────────────────┤
│                      102240 │
└─────────────────────────────┘
citus_create_restore_point (name text) returns pg_lsn #

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

Аргументы:

  • name — имя создаваемой точки восстановления.

Пример использования этой функции:

SELECT citus_create_restore_point('foo');
┌────────────────────────────┐
│ citus_create_restore_point │
├────────────────────────────┤
│ 0/1EA2808                  │
└────────────────────────────┘
J.5.7.5.2. Таблицы и представления citus #
J.5.7.5.2.1. Метаданные узла-координатора #

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

J.5.7.5.2.1.1. Таблица pg_dist_partition #

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

ИмяТипОписание
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)
J.5.7.5.2.1.2. Таблица pg_dist_shard #

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

ИмяТипОписание
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).
J.5.7.5.2.1.3. Представление citus_shards #

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

  • Расположение каждого сегмента (узел и порт),

  • Таблица, которой принадлежит сегмент,

  • Размер сегмента.

Это представление помогает исследовать сегменты, например, чтобы найти неравенство размеров узлов.

SELECT * FROM citus_shards;
.
 table_name | shardid | shard_name   | citus_table_type | colocation_id | nodename  | nodeport | shard_size
------------+---------+--------------+------------------+---------------+-----------+----------+------------
 dist       |  102170 | dist_102170  | distributed      |            34 | localhost |     9701 |   90677248
 dist       |  102171 | dist_102171  | distributed      |            34 | localhost |     9702 |   90619904
 dist       |  102172 | dist_102172  | distributed      |            34 | localhost |     9701 |   90701824
 dist       |  102173 | dist_102173  | distributed      |            34 | localhost |     9702 |   90693632
 ref        |  102174 | ref_102174   | reference        |             2 | localhost |     9701 |       8192
 ref        |  102174 | ref_102174   | reference        |             2 | localhost |     9702 |       8192
 dist2      |  102175 | dist2_102175 | distributed      |            34 | localhost |     9701 |     933888
 dist2      |  102176 | dist2_102176 | distributed      |            34 | localhost |     9702 |     950272
 dist2      |  102177 | dist2_102177 | distributed      |            34 | localhost |     9701 |     942080
 dist2      |  102178 | dist2_102178 | distributed      |            34 | localhost |     9702 |     933888

Параметр colocation_id относится к группе совмещения. За дополнительной информацией о citus_table_type обратитесь к разделу Типы таблиц.

J.5.7.5.2.1.4. Таблица pg_dist_placement #

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

ИмяТипОписание
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
J.5.7.5.2.1.5. Таблица pg_dist_node #

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

ИмяТипОписание
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)
J.5.7.5.2.1.6. Таблица citus.pg_dist_object #

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

ИмяТипОписание
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                |
J.5.7.5.2.1.7. Представление citus_schemas #

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

ИмяТипОписание
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
J.5.7.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          │
└────────────┴──────────────────┴─────────────────────┴───────────────┴────────────┴─────────────┴─────────────┴───────────────┘
J.5.7.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          │
└────────────────────────┴──────────────────┴─────────────────────────────────────────┴─────────────────────┴─────────────────────┴───────────────┘
J.5.7.5.2.1.10. Таблица pg_dist_colocation #

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

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

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

ИмяТипОписание
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;
J.5.7.5.2.1.12. Таблица citus_stat_statements #

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

ИмяТипОписание
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.

J.5.7.5.2.1.13. Представление citus_stat_tenants #

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

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

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

ИмяТипОписание
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
J.5.7.5.2.1.14. Активность распределённых запросов #

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

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

  • citus_stat_activity содержит информацию о распределённых запросах, которые выполняются на всех узлах, и является расширенным вариантом представления pg_stat_activity, доступным везде, где есть последнее.

  • citus_dist_stat_activity — аналогично представлению citus_stat_activity, но ограничено только распределёнными запросами без учёта фрагментов запросов citus.

  • citus_lock_waits — заблокированные запросы на уровне кластера.

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

Рассмотрим в качестве примера подсчёт строк в распределённой таблице:

-- Выполнение за один сеанс
-- (с pg_sleep, чтобы всё было видно)

SELECT count(*), pg_sleep(3) FROM users_table;

Запрос появляется в citus_dist_stat_activity:

-- Запуск в другом сеансе

SELECT * FROM citus_dist_stat_activity;

-[ RECORD 1 ]----+-------------------------------------------
global_pid       | 10000012199
nodeid           | 1
is_worker_query  | f
datid            | 13724
datname          | postgres
pid              | 12199
leader_pid       |
usesysid         | 10
usename          | postgres
application_name | psql
client_addr      |
client_hostname  |
client_port      | -1
backend_start    | 2022-03-23 11:30:00.533991-05
xact_start       | 2022-03-23 19:35:28.095546-05
query_start      | 2022-03-23 19:35:28.095546-05
state_change     | 2022-03-23 19:35:28.09564-05
wait_event_type  | Timeout
wait_event       | PgSleep
state            | active
backend_xid      |
backend_xmin     | 777
query_id         |
query            | SELECT count(*), pg_sleep(3) FROM users_table;
backend_type     | client backend

В представлении citus_dist_stat_activity скрыты внутренние фрагменты запроса citus. Чтобы их увидеть, можно использовать более подробное представление citus_stat_activity. Например, предыдущий запрос count(*) обращается ко всем сегментам. Часть информации находится в сегменте users_table_102039, который виден в запросе ниже.

SELECT * FROM citus_stat_activity;

-[ RECORD 1 ]----+-----------------------------------------------------------------------
global_pid       | 10000012199
nodeid           | 1
is_worker_query  | f
datid            | 13724
datname          | postgres
pid              | 12199
leader_pid       |
usesysid         | 10
usename          | postgres
application_name | psql
client_addr      |
client_hostname  |
client_port      | -1
backend_start    | 2022-03-23 11:30:00.533991-05
xact_start       | 2022-03-23 19:32:18.260803-05
query_start      | 2022-03-23 19:32:18.260803-05
state_change     | 2022-03-23 19:32:18.260821-05
wait_event_type  | Timeout
wait_event       | PgSleep
state            | active
backend_xid      |
backend_xmin     | 777
query_id         |
query            | SELECT count(*), pg_sleep(3) FROM users_table;
backend_type     | client backend
-[ RECORD 2 ]----+-----------------------------------------------------------------------------------------
global_pid       | 10000012199
nodeid           | 1
is_worker_query  | t
datid            | 13724
datname          | postgres
pid              | 12725
leader_pid       |
usesysid         | 10
usename          | postgres
application_name | citus_internal gpid=10000012199
client_addr      | 127.0.0.1
client_hostname  |
client_port      | 44106
backend_start    | 2022-03-23 19:29:53.377573-05
xact_start       |
query_start      | 2022-03-23 19:32:18.278121-05
state_change     | 2022-03-23 19:32:18.278281-05
wait_event_type  | Client
wait_event       | ClientRead
state            | idle
backend_xid      |
backend_xmin     |
query_id         |
query            | SELECT count(*) AS count FROM public.users_table_102039 users WHERE true
backend_type     | client backend

Поле query показывает строки, подсчитываемые в сегменте 102039.

Ниже представлены примеры информативных запросов, которые можно сформулировать с помощью citus_stat_activity:

-- События ожидания активных запросов

SELECT query, wait_event_type, wait_event
  FROM citus_stat_activity
 WHERE state='active';

-- Первые события ожидания активных запросов

SELECT wait_event, wait_event_type, count(*)
  FROM citus_stat_activity
 WHERE state='active'
 GROUP BY wait_event, wait_event_type
 ORDER BY count(*) desc;

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

SELECT nodeid, count(*)
  FROM citus_stat_activity
 WHERE is_worker_query
 GROUP BY nodeid;

Следующее представление — citus_lock_waits. Чтобы увидеть его работу, можно вручную создать ситуацию с блокировкой. Сначала настройте тестовую таблицу узла-координатора:

CREATE TABLE numbers AS
  SELECT i, 0 AS j FROM generate_series(1,10) AS i;
SELECT create_distributed_table('numbers', 'i');

Затем с помощью двух сеансов на узле-координаторе запустите такую последовательность операторов:

-- Сеанс 1                           -- Сеанс 2
-------------------------------------  -------------------------------------
BEGIN;
UPDATE numbers SET j = 2 WHERE i = 1;
                                       BEGIN;
                                       UPDATE numbers SET j = 3 WHERE i = 1;
                                       -- (это вызывает блокировку)

В представлении citus_lock_waits содержится информация о данной ситуации.

SELECT * FROM citus_lock_waits;

-[ RECORD 1 ]-------------------------+--------------------------------------
waiting_gpid                          | 10000011981
blocking_gpid                         | 10000011979
blocked_statement                     | UPDATE numbers SET j = 3 WHERE i = 1;
current_statement_in_blocking_process | UPDATE numbers SET j = 2 WHERE i = 1;
waiting_nodeid                        | 1
blocking_nodeid                       | 1

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

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

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

J.5.7.5.2.2.1. Таблица pg_dist_authinfo #

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

ИмяТипОписание
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)
J.5.7.5.2.2.2. Таблица pg_dist_poolinfo #

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

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

ИмяТипОписание
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');
J.5.7.5.3. Справка по конфигурации #

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

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

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

ALTER DATABASE citus SET citus.multi_task_query_log_level = 'log';
J.5.7.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 МБ.

J.5.7.5.3.2. Статистика запросов #
citus.stat_statements_purge_interval (integer) #

Устанавливает частоту, с которой демон обслуживания удаляет из таблицы citus_stat_statements записи, для которых нет совпадений в таблице pg_stat_statements. Этот параметр конфигурации задаёт временной интервал между очистками в секундах. Значение по умолчанию — 10. Значение 0 отключает очистку. Этот параметр относится только к узлу-координатору и может быть изменён во время выполнения.

Ниже показано, как задавать этот параметр:

SET citus.stat_statements_purge_interval TO 5;
citus.stat_statements_max (integer) #

Максимальное количество строк для хранения в таблице citus_stat_statements. Значение по умолчанию — 50000 — может быть изменено на любое значение в диапазоне от 1000 до 10 000 000. Обратите внимание, что для каждой строки требуется 140 байт, поэтому при установке для citus.stat_statements_max максимального значения 10 МБ потребуется 1,4 ГБ на диске.

Изменение этого параметра конфигурации вступит в силу только после перезапуска Postgres Pro.

citus.stat_statements_track (enum) #

Запись статистики для citus_stat_statements требует дополнительных ресурсов ЦП. Когда нагрузка на базу данных увеличивается, администратор может отключить отслеживание операторов. Включать и отключать отслеживание можно с помощью параметра конфигурации citus.stat_statements_track. Допустимые значения:

  • all — отслеживать все операторы. Это значение по умолчанию.

  • none — отключить отслеживание.

citus.stat_tenants_untracked_sample_rate (floating point) #

Частота выборки для новых арендаторов в представлении citus_stat_tenants. Частота может находиться в диапазоне от 0.0 до 1.0. Значение по умолчанию — 1.0, то есть проводится выборка 100% запросов неотслеживаемых арендаторов. Установка более низкого значения означает, что проводится выборка 100% запросов отслеживаемых арендаторов, а запросы неотслеживаемых арендаторов выбираются только с указанной частотой.

J.5.7.5.3.3. Загрузка данных #
citus.shard_count (integer) #

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

citus.metadata_sync_mode (enum) #

Примечание

Для изменения этого параметра конфигурации требуются права суперпользователя.

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

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

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

  • transactional — синхронизировать все метаданные в одной транзакции. Это значение по умолчанию.

  • nontransactional — синхронизировать метаданные с помощью нескольких небольших транзакций.

Ниже показано, как задавать этот параметр:

-- Добавление нового узла и нетранзакционная синхронизация

SET citus.metadata_sync_mode TO 'nontransactional';
SELECT citus_add_node(<ip>, <port>);

-- Ручная (ре)синхронизация

SET citus.metadata_sync_mode TO 'nontransactional';
SELECT start_metadata_sync_to_all_nodes();

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

J.5.7.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.

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

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

J.5.7.5.3.6. DDL #
citus.enable_ddl_propagation (boolean) #

Указывает, следует ли автоматически транслировать изменения DDL с узла-координатора на все рабочие узлы. Значение по умолчанию — true. Поскольку некоторые изменения схемы требуют исключительной блокировки доступа к таблицам, а автоматическая трансляция применяется ко всем рабочим узлам последовательно, это может временно снизить отзывчивость кластера citus. Можно отключить этот параметр и транслировать изменения вручную.

Примечание

Список поддерживаемых для трансляции DDL-запросов представлен в разделе Изменение таблиц.

citus.enable_local_reference_table_foreign_keys (boolean) #

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

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

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

citus.enable_change_data_capture (boolean) #

Заставляет citus изменить модули логического декодирования wal2json и pgoutput для работы с распределёнными таблицами. В частности, имена сегментов (например, foo_102027) в выходных данных модуля декодирования меняются на базовые имена распределённых таблиц (например, foo). Это также позволяет избежать публикации повторяющихся событий во время изоляции арендатора и операций разделения/перемещения/перебалансировки сегментов. Значение по умолчанию — false.

citus.enable_schema_based_sharding (boolean) #

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

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

J.5.7.5.3.7. Конфигурация исполнителя #
citus.all_modifications_commutative (boolean) #

В citus применяются правила независимости от порядка исполнения и запрашиваются соответствующие блокировки для операций изменения, чтобы гарантировать корректность поведения. Например, предполагается, что оператор INSERT не зависит от порядка исполнения другого оператора INSERT, но зависит от порядка операторов UPDATE или DELETE. Аналогичным образом предполагается, что операторы UPDATE или DELETE зависят от порядка исполнения других операторов UPDATE или DELETE. Это означает, что для операторов UPDATE и DELETE требуется, чтобы citus запрашивал более сильные блокировки.

Если есть операторы UPDATE, которые не зависят от порядка исполнения операторов INSERT или других операторов UPDATE, можно ослабить эти предположения независимости от порядка исполнения, установив для этого параметра значение true. При значении true, все команды считаются независимыми от порядка исполнения и требующими общей блокировки, что может повысить общую пропускную способность. Этот параметр можно установить во время выполнения, и он относится только к узлу-координатору.

citus.multi_task_query_log_level (enum) #

Устанавливает уровень записи в журнал для любого запроса, который генерирует более одной задачи (т. е. затрагивает более одного сегмента). Этот параметр полезен во время миграции многоарендного приложения, так как можно выбрать сообщение об ошибке или предупреждение о таких запросах, чтобы найти их и добавить к ним фильтр tenant_id. Этот параметр можно установить во время выполнения и он относится только к узлу-координатору. Значение по умолчанию — off. Допустимы следующие значения:

  • off — отключает запись в журнал любых запросов, которые генерируют несколько задач (т. е. охватывают несколько сегментов).

  • debug — записывает в журнал операторы на уровне безопасности DEBUG.

  • log — записывает в журнал операторы на уровне безопасности LOG. Строка в журнале будет включать выполненный SQL-запрос.

  • notice — записывает в журнал операторы на уровне безопасности NOTICE.

  • warning — записывает в журнал операторы на уровне безопасности WARNING.

  • error — записывает в журнал операторы на уровне безопасности ERROR.

Обратите внимание, что уровень error может быть полезен во время тестирования разработки, а более низкий уровень журнала, например log, — в производственной среде. При выборе уровня log в журнал БД будут записываться многозадачные запросы, которые будут показываться после STATEMENT.

LOG:  multi-task query about to be executed
HINT:  Queries are split to multiple tasks if they have to be split into several queries on the workers.
STATEMENT:  SELECT * FROM foo;
citus.propagate_set_commands (enum) #

Определяет, какие команды SET транслируются рабочим узлам с узла-координатора. Значение по умолчанию — none. Допустимы следующие значения:

  • none — команды SET не транслируются.

  • local — транслируются только команды SET LOCAL.

citus.enable_repartition_joins (boolean) #

Обычно попытка выполнить соединения с пересекционированием с помощью адаптивного исполнителя завершается ошибкой с выводом соответствующего сообщения. Но если установить для этого параметра конфигурации значение true, citus выполнит соединение. Значение по умолчанию — false.

citus.enable_repartitioned_insert_select (boolean) #

По умолчанию оператор INSERT INTO… SELECT, который не может быть вынесен наружу, попытается пересекционировать строки из оператора SELECT и передать их рабочим узлам для вставки. Однако если в целевой таблице слишком много сегментов, пересекционирование не будет выполнено должным образом. Издержки на обработку интервалов сегментов при секционировании результатов слишком велики. Пересекционирование можно отключить вручную, установив для этого параметра конфигурации значение false.

citus.enable_binary_protocol (boolean) #

Установка для этого параметра значения true указывает узлу-координатору использовать формат двоичной сериализации Postgres Pro (если возможно) для передачи данных рабочим узлам. Некоторые типы столбцов не поддерживают двоичную сериализацию.

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

Значение по умолчанию — true. Если установлено значение false, все результаты кодируются и передаются в текстовом формате.

citus.max_shared_pool_size (integer) #

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

Без ограничения скорости подключений каждый запрос с несколькими сегментами создаёт подключения для каждого рабочего узла пропорционально количеству сегментов, к которым он обращается (в частности, до значения #shards/#workers). Одновременное выполнение десятков многосегментных запросов может легко превысить ограничение max_connections рабочих узлов, что приведёт к сбою.

По умолчанию это значение равно значению max_connections на узле-координаторе, которое может не совпадать со значением на рабочих узлах (см. примечание ниже). Значение -1 отключает ограничение скорости.

Примечание

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

citus.max_adaptive_executor_pool_size (integer) #

В отличие от параметра citus.max_shared_pool_size, ограничивающего подключения рабочих узлов во всех сеансах, citus.max_adaptive_executor_pool_size ограничивает подключения рабочих узлов только в текущем сеансе. Этот параметр позволяет:

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

  • Обеспечить управление приоритетами: назначать низкий приоритет сеансам с меньшим значением citus.max_adaptive_executor_pool_size и высокий приоритет сеансам с большими значениями.

Значение по умолчанию — 16.

citus.executor_slow_start_interval (integer) #

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

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

Для длительных запросов (которые занимают более 500 мс) медленный запуск может увеличить задержку, но выполнение небольших запросов будет быстрее. Значение по умолчанию — 10 мс.

citus.max_cached_conns_per_worker (integer) #

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

Значение по умолчанию — 1. Значение 2 может быть полезно для кластеров с небольшим количеством одновременных сеансов, но указывать большее значение не рекомендуется (16 будет слишком большим).

citus.force_max_query_parallelization (boolean) #

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

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

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

Ниже показано, как задавать этот параметр:

BEGIN;
--Добавьте следующее указание
SET citus.force_max_query_parallelization TO ON;

-- Небольшой запрос, для которого не требуется много подключений
SELECT count(*) FROM table WHERE filter = x;

-- Запрос, который выполняется быстрее с большим количеством подключений и
-- может получить их, поскольку выше было включено максимальное распараллеливание
SELECT ... very .. complex .. SQL;
COMMIT;
citus.explain_all_tasks (boolean) #

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

citus.explain_analyze_sort_method (enum) #

Определяет метод сортировки задач в выводе команды EXPLAIN ANALYZE. Допустимы следующие значения:

  • execution-time — сортировать по времени выполнения.

  • taskId — сортировать по идентификатору задачи.

J.5.8. Администрирование #

J.5.8.1. Управление кластером #

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

Примечание

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

J.5.8.1.1. Выбор размера кластера #

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

J.5.8.1.1.1. Выбор количества сегментов #

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

J.5.8.1.1.1.1. Сценарий использования многоарендной БД SaaS #

Оптимальный выбор зависит от пользовательских шаблонов обращения к данным. Например, в случае с многоарендной базой данных SaaS рекомендуется выбирать от 32 до 128 сегментов. При небольшой нагрузке, например менее 100 ГБ, можно начать с 32 сегментов, а при большей — выбрать 64 или 128 сегментов. То есть существует возможность масштабирования с 32 до 128 рабочих узлов.

J.5.8.1.1.1.2. Сценарий использования для анализа данных в реальном времени #

В сценарии использования аналитики в реальном времени количество сегментов должно быть связано с общим количеством ядер рабочих узлов. Для максимального распараллеливания следует создать достаточное количество сегментов на каждом узле, чтобы на каждое ядро ЦП приходился хотя бы один сегмент. Обычно рекомендуется создать большое количество начальных сегментов, например в 2 или 4 раза больше, чем текущее количество ЦП. Это позволит впоследствии провести масштабирование, если будет добавлено больше рабочих узлов и ядер ЦП.

Однако имейте в виду, что для каждого запроса citus открывает одно подключение к базе данных на каждый сегмент, и количество подключений ограничено. Внимательно следите за тем, чтобы количество сегментов оставалось небольшим, чтобы распределённым запросам не приходилось часто ждать подключения. Другими словами, необходимое количество подключений, (max concurrent queries * shard count), как правило, не должно превышать общее количество возможных подключений в системе, (number of workers * max_connections per worker).

J.5.8.1.2. Начальный размер кластера #

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

J.5.8.1.2.1. Сценарий использования многоарендной БД SaaS #

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

Узлу-координатору требуется меньше памяти, чем рабочим узлам, поэтому в качестве координатора можно выбрать машину, оптимизированную для вычислений. Требуемое количество ядер зависит от существующей нагрузки (пропускной способности записи/чтения).

J.5.8.1.2.2. Сценарий использования для анализа данных в реальном времени #

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

ОЗУ рабочего узла: в идеальном случае будет предоставляться достаточно памяти для размещения большей части рабочего набора. Тип запросов, которые используется в пользовательском приложении, влияет на требования к памяти. Чтобы определить, сколько памяти требуется запросу, можно выполнить для него EXPLAIN ANALYZE.

J.5.8.1.3. Масштабирование кластера #

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

J.5.8.1.3.1. Добавление рабочего узла #

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

Чтобы добавить новый узел в кластер, сначала необходимо добавить DNS-имя или IP-адрес этого узла и порт (на котором работает Postgres Pro) в таблицу каталога pg_dist_node. Для этого можно использовать функцию citus_add_node:

SELECT * from citus_add_node('node-name', 5432);

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

Примечание

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

J.5.8.1.3.2. Перебалансировка сегментов без простоя #

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

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

SELECT citus_rebalance_start();

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

J.5.8.1.3.2.1. Параллельная перебалансировка #

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

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

ALTER SYSTEM SET citus.max_background_task_executors_per_node = 2;
SELECT pg_reload_conf();

SELECT citus_rebalance_start();

Типичные сценарии использования

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

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

Особые случаи и нетривиальные проблемы

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

J.5.8.1.3.2.2. Как это работает #

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

Согласно документации Postgres Pro для источника необходимо выбрать вариант идентификации реплики:

Опубликованная таблица должна иметь выбранный «вариант идентификации реплики» для репликации операций UPDATE и DELETE и возможности идентификации на стороне подписчика соответствующих изменяемых или удаляемых строк. По умолчанию это первичный ключ, если он есть. Другой уникальный индекс (с некоторыми дополнительными требованиями) также может быть установлен в качестве варианта идентификации реплики.

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

Есть ли у таблицы уникальный индекс?

Если у таблицы, которую нужно реплицировать, уже есть уникальный индекс, включающий столбец распределения, выберите этот индекс в качестве варианта идентификации реплики:

-- Допустим у my_table есть уникальный индекс my_table_idx,
-- включающий столбец распределения

ALTER TABLE my_table REPLICA IDENTITY
  USING INDEX my_table_idx;

Примечание

Можно использовать REPLICA IDENTITY USING INDEX, но не рекомендуется добавлять таблице REPLICA IDENTITY FULL. При использовании этого параметра каждая команда UPDATE/DELETE будет выполнять полное сканирование таблицы на стороне подписчика, чтобы найти кортеж с этими строками. В ходе тестирования обнаружено, что это приводит к ухудшению производительности даже по сравнению с четвёртым решением, приведённым ниже.

Можно ли добавлять первичный ключ в других случаях?

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

J.5.8.1.3.3. Добавление узла-координатора #

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

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

SELECT * FROM citus_add_node(second_coordinator_hostname, second_coordinator_port);
SELECT * FROM citus_set_node_property(second_coordinator_hostname, second_coordinator_port, 'shouldhaveshards', false);

Примечание

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

J.5.8.1.4. Сбои узлов #

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

J.5.8.1.4.1. Сбои рабочих узлов #

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

J.5.8.1.4.2. Сбои узла-координатора #

Узел-координатор citus поддерживает таблицы метаданных для отслеживания всех узлов кластера и расположения сегментов базы данных на этих узлах. Таблицы метаданных обычно небольшого размера (несколько МБ) и редко изменяются. Это означает, что их можно реплицировать и быстро восстанавливать, если на узле происходит сбой. Существует несколько вариантов действий пользователей в случае сбоя координатора.

  • Используйте потоковую репликацию Postgres Pro. Можно использовать функциональность потоковой репликации Postgres Pro для создания узла-координатора горячего резерва. Затем, если ведущий узел-координатор выйдет из строя, резервный узел может быть автоматически повышен до ведущего для обслуживания запросов к кластеру. За подробной информацией о такой настройке обратитесь к разделу Потоковая репликация.

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

J.5.8.1.5. Изоляция арендаторов #
J.5.8.1.5.1. Сегментирование на основе строк #

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

Однако совместное использование сегментов может вызывать борьбу за ресурсы, если арендаторы сильно отличаются по размеру. Это обычная ситуация для систем с большим количеством арендаторов: объём данных арендаторов имеет тенденцию подчиняться закону Ципфа по мере увеличения числа арендаторов. То есть несколько очень крупных арендаторов существуют вместе с множеством более мелких. Чтобы улучшить распределение ресурсов и гарантировать качество обслуживания арендаторов, рекомендуется переместить крупных арендаторов на выделенные узлы.

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

Каждый сегмент помечен в метаданных citus диапазоном хешированных значений, содержащихся в нём (за более подробной информацией обратитесь к описанию таблицы pg_dist_shard). Функция isolate_tenant_to_new_shard перемещает арендатора в выделенный сегмент в три этапа:

  1. Создаёт новый сегмент для table_name, включающий строки, столбец распределения которых имеет значение tenant_id, и исключающий все остальные строки.

  2. Перемещает соответствующие строки из текущего сегмента в новый сегмент.

  3. Разделяет старый сегмент на две части с хеш-диапазонами, которые примыкают к месту разделения сверху и снизу.

Кроме того, функция принимает аргумент CASCADE, и при этом изолирует не только строки таблицы table_name арендатора, но и всех таблиц совмещённых с ней:

-- Запрос создаёт изолированный сегмент для данного tenant_id и
-- возвращает идентификатор нового сегмента.

-- Общая форма:

SELECT isolate_tenant_to_new_shard('table_name', tenant_id);

-- Конкретный пример:

SELECT isolate_tenant_to_new_shard('lineitem', 135);

-- Если у данной таблицы есть совмещённые с ней таблицы, запрос выдаст ошибку с
-- рекомендацией использовать параметр CASCADE

SELECT isolate_tenant_to_new_shard('lineitem', 135, 'CASCADE');

Результат:

┌─────────────────────────────┐
│ isolate_tenant_to_new_shard │
├─────────────────────────────┤
│                      102240 │
└─────────────────────────────┘

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

J.5.8.1.5.2. Сегментирование на основе схем #

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

Сначала найдите идентификатор совмещения схемы, которая будет перемещаться.

SELECT * FROM citus_shards;
schema_name  | colocation_id | schema_size | schema_owner
--------------+---------------+-------------+--------------
user_service |             1 | 0 bytes     | user_service
time_service |             2 | 0 bytes     | time_service
ping_service |             3 | 0 bytes     | ping_service
a            |             4 | 128 kB      | citus
b            |             5 | 32 kB       | citus
with_data    |            11 | 6408 kB     | citus
(6 rows)

На следующем этапе выполняется запрос к citus_shards, в котором будет использоваться идентификатор совмещения 11 из вывода выше:

SELECT * FROM citus_shards where colocation_id = 11;
  table_name    | shardid |       shard_name       | citus_table_type | colocation_id | nodename  | nodeport | shard_size
-----------------+---------+------------------------+------------------+---------------+-----------+----------+------------
with_data.test  |  102180 | with_data.test_102180  | schema           |            11 | localhost |     9702 |     647168
with_data.test2 |  102183 | with_data.test2_102183 | schema           |            11 | localhost |     9702 |    5914624
(2 rows)

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

J.5.8.1.5.3. Выполнение перемещения #

Зная идентификатор сегмента, указывающий на арендатора, можно выполнить перемещение:

-- Найдите узел, на котором сейчас находится новый сегмент
SELECT nodename, nodeport
  FROM citus_shards
 WHERE shardid = 102240;

-- Посмотрите на список рабочих узлов, в которые можно переместить сегмент
SELECT * FROM master_get_active_worker_nodes();

-- Переместите сегмент на выбранный рабочий узел (другие сегменты,
-- созданные с параметром CASCADE, также будут перемещены)
SELECT citus_move_shard_placement(
  102240,
  'source_host', source_port,
  'dest_host', dest_port);

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

J.5.8.1.6. Просмотр статистики запросов #

При администрировании кластера citus полезно знать, какие запросы выполняются пользователями, какие узлы задействованы и какой метод выполнения используется citus для каждого запроса. Расширение записывает статистику запросов в представление метаданных citus_stat_statements, названное аналогично представлению pg_stat_statements в Postgres Pro. В представлении pg_stat_statements хранится информация о продолжительности запроса и вводе-выводе, а в citus_stat_statements — о методах выполнения citus и ключах секционирования сегмента (при их наличии).

Чтобы отслеживать статистику запросов в citus, нужно установить расширение pg_stat_statements. В локальном экземпляре Postgres Pro загрузите расширение в postgresql.conf с помощью shared_preload_libraries, затем создайте расширение в SQL:

CREATE EXTENSION pg_stat_statements;

Предположим, есть хеш-распределённая по столбцу id таблица с именем foo.

-- Создайте и заполните распределённую таблицу
CREATE TABLE foo ( id int );
SELECT create_distributed_table('foo', 'id');

INSERT INTO foo SELECT generate_series(1,100);

Выполните ещё два запроса, и citus_stat_statements покажет, каким способом citus выполнил их.

-- Подсчёт всех строк выполняется на всех узлах,
-- результаты суммируются на координаторе
SELECT count(*) FROM foo;

-- Указание строки по столбцу распределения отправляет
-- выполнение на отдельный узел
SELECT * FROM foo WHERE id = 42;

Чтобы узнать, как были выполнены эти запросы, обратитесь к таблице статистики:

SELECT * FROM citus_stat_statements;

Результат:

-[ RECORD 1 ]-+----------------------------------------------
queryid       | -6844578505338488014
userid        | 10
dbid          | 13340
query         | SELECT count(*) FROM foo;
executor      | adaptive
partition_key |
calls         | 1
-[ RECORD 2 ]-+----------------------------------------------
queryid       | 185453597994293667
userid        | 10
dbid          | 13340
query         | INSERT INTO foo SELECT generate_series($1,$2)
executor      | insert-select
partition_key |
calls         | 1
-[ RECORD 3 ]-+----------------------------------------------
queryid       | 1301170733886649828
userid        | 10
dbid          | 13340
query         | SELECT * FROM foo WHERE id = $1
executor      | adaptive
partition_key | 42
calls         | 1

Можно увидеть, что для выполнения запросов citus чаще всего использует адаптивного исполнителя. Он разбивает запрос на несколько запросов для выполнения на соответствующих узлах и объединяет результаты на узле-координаторе. В случае второго запроса (фильтрация по столбцу распределения id = $1) citus определил, что требуются данные только с одного узла. Наконец, можно увидеть, что оператор INSERT INTO foo SELECT… выполняется с помощью исполнителя insert-select, который обеспечивает гибкость при выполнении запросов такого типа.

J.5.8.1.6.1. Статистика по арендаторам #

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

SELECT sum(calls),
       partition_key IS NOT NULL AS single_tenant
FROM citus_stat_statements
GROUP BY 2;
.
 sum | single_tenant
-----+---------------
   2 | f
   1 | t

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

Чтобы выяснить, какие арендаторы наиболее активны, можно использовать представление citus_stat_tenants.

J.5.8.1.6.2. Срок действия статистики #

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

Существует три способа синхронизации представлений, их можно использовать вместе.

  1. Настройте демон обслуживания так, чтобы он периодически синхронизировал статистику citus и Postgres Pro. В параметре конфигурации citus.stat_statements_purge_interval задаётся время синхронизации в секундах. Значение 0 отключает периодическую синхронизацию.

  2. Настройте количество записей в citus_stat_statements. В параметре конфигурации citus.stat_statements_max задаётся пороговое значение, при достижении которого старые записи начинают удаляться. Значение по умолчанию — 50000, максимальное допустимое значение — 10000000. Обратите внимание, что каждая запись занимает около 140 байт в общей памяти, поэтому задавайте значение обдуманно.

  3. Увеличьте значение pg_stat_statements.max. Значение по умолчанию — 5000 и может быть увеличено до 10000, 20000 или даже до 50000 без особых издержек. Это наиболее полезно, когда локальные запросы (т. е. на узле-координаторе) создают большую нагрузку.

Примечание

После изменения pg_stat_statements.max или citus.stat_statements_max требуется перезапуск службы Postgres Pro. Изменение citus.stat_statements_purge_interval вступит в силу после вызова функции pg_reload_conf.

J.5.8.1.7. Экономия ресурсов #
J.5.8.1.7.1. Ограничение длительных запросов #

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

-- Ограничить длительность запроса до 5 минут
ALTER DATABASE citus
  SET statement_timeout TO 300000;
SELECT run_command_on_workers($cmd$
  ALTER DATABASE citus
    SET statement_timeout TO 300000;
$cmd$);

Тайм-аут задаётся в миллисекундах.

Чтобы настроить тайм-аут для каждого запроса, используйте SET LOCAL в транзакции:

BEGIN;
-- это ограничение применяется только к текущей транзакции
SET LOCAL statement_timeout TO 300000;

-- ...
COMMIT;
J.5.8.1.8. Безопасность #
J.5.8.1.8.1. Управление подключениями #

Примечание

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

Кластеры, изначально созданные с помощью citus, не имеют включённого сетевого шифрования между узлами (даже если они будут обновлены позднее). Чтобы настроить самоподписанный TLS-протокол для этого типа установки, выполните действия, описанные в разделе Создание сертификатов, и задайте описанные здесь параметры citus, т. е. измените значение citus.node_conninfo на sslmode=require. Это следует сделать как на узле-координаторе, так и на рабочих узлах.

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

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

-- Пары ключ=значение, разделённые пробелами.
-- Например, параметры ssl:

ALTER SYSTEM SET citus.node_conninfo =
  'sslrootcert=/path/to/citus-ca.crt sslcrl=/path/to/citus-ca.crl sslmode=verify-full';

Существует белый список значений, которые принимает параметр конфигурации citus.node_conninfo. Значение по умолчанию — sslmode=require, при котором исключается незашифрованная связь между узлами. Если кластер изначально был создан с помощью citus, значение параметра — sslmode=prefer. После настройки самоподписанных сертификатов на всех узлах рекомендуется изменить значение этого параметра на sslmode=require.

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

SELECT pg_reload_conf();
-- Доступ к этой таблице есть только у суперпользователей

-- Добавление пароля для пользователя jdoe
INSERT INTO pg_dist_authinfo
  (nodeid, rolename, authinfo)
VALUES
  (123, 'jdoe', 'password=abc123');

После выполнения этой команды INSERT любой запрос, которому требуется подключение к узлу 123 от имени пользователя jdoe, будет использовать предоставленный пароль. За дополнительной информацией обратитесь к описанию таблицы pg_dist_authinfo.

-- Изменение пользователя jdoe для использования аутентификации по сертификату
UPDATE pg_dist_authinfo
SET authinfo = 'sslcert=/path/to/user.crt sslkey=/path/to/user.key'
WHERE nodeid = 123 AND rolename = 'jdoe';

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

Изменение таблицы pg_dist_authinfo не приводит к переподключению существующих соединений.

J.5.8.1.8.2. Настройка сертификатов, подписанных центром сертификации #

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

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

  • /path/to/server.key — закрытый ключ сервера

  • /path/to/server.crt — сертификат сервера или цепочка сертификатов для ключа сервера, подписанная доверенным центром сертификации

Кроме этих файлов, уникальных для каждой машины, нужны следующие файлы кластера или центра сертификации:

  • /path/to/ca.crt — сертификат центра сертификации

  • /path/to/ca.crl — список отозванных сертификатов центра сертификации

Примечание

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

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

# Следующие параметры позволяют серверу postgres включить ssl-протокол и
# конфигурировать сервер для предоставления сертификата клиентам
# при подключении по протоколу tls/ssl
ssl = on
ssl_key_file = '/path/to/server.key'
ssl_cert_file = '/path/to/server.crt'

# Указание для citus проверять сертификат сервера, к которому он подключается
citus.node_conninfo = 'sslmode=verify-full sslrootcert=/path/to/ca.crt sslcrl=/path/to/ca.crl'

Чтобы изменения вступили в силу, перезагрузите конфигурацию. Кроме того, может потребоваться настроить citus.local_hostname для корректной работы с sslmode=verify-full.

В зависимости от политики используемого центра сертификации может потребоваться изменить sslmode=verify-full в citus.node_conninfo на sslmode=verify-ca. Чтобы узнать разницу между этими вариантами, обратитесь к разделу Описание режимов SSL.

Наконец, чтобы запретить пользователям незашифрованные подключения, необходимо внести изменения в pg_hba.conf. Во многих инсталляциях Postgres Pro будут записи, разрешающие подключения host, подключения по протоколу SSL/TLS, а также обычные TCP-подключения. Чтобы разрешить аутентификацию только зашифрованных подключений в Postgres Pro, замените все записи host на hostssl. За полным описанием этих параметров обратитесь к описанию файла pg_hba.conf.

Примечание

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

Чтобы убедиться, что соединения узла-координатора с рабочими узлами зашифрованы, можно выполнить следующий запрос. Он покажет версию протокола SSL/TLS, используемую для шифрования соединения координатора с рабочим узлом:

SELECT run_command_on_workers($$
  SELECT version FROM pg_stat_ssl WHERE pid = pg_backend_pid()
$$);
┌────────────────────────────┐
│   run_command_on_workers   │
├────────────────────────────┤
│ (localhost,9701,t,TLSv1.2) │
│ (localhost,9702,t,TLSv1.2) │
└────────────────────────────┘
(2 rows)
J.5.8.1.8.3. Повышение уровня безопасности рабочего узла #

Инструкции для инсталляции с несколькими узлами помогут настроить pg_hba.conf на рабочих узлах с заданным методом аутентификации trust для подключений по локальной сети. Однако может потребоваться более высокий уровень безопасности.

Чтобы все подключения предоставляли хешированный пароль, измените Postgres Pro pg_hba.conf на каждом рабочем узле следующим образом:

# Требовать доступ по паролю и подключение по протоколу SSL/TLS
# с узлами в локальной сети. Следующие диапазоны соответствуют
# 24-, 20- и 16-битным блокам в закрытых адресных пространствах IPv4.
hostssl    all             all             10.0.0.0/8              md5

# Требовать пароли и подключения по протоколу SSL/TLS, 
# в том числе, когда узел подключается сам к себе.
hostssl    all             all             127.0.0.1/32            md5
hostssl    all             all             ::1/128                 md5

Узлу-координатору необходимо знать пароли ролей, чтобы взаимодействовать с рабочими узлами. В citus информация аутентификации должна храниться в файле .pgpass. Отредактируйте файл в домашнем каталоге пользователя Postgres Pro, добавив в него строку для каждой комбинации рабочего узла и роли:

hostname:port:database:username:password

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

J.5.8.1.8.4. Безопасность на уровне строк #

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

Разделение данных арендаторов можно реализовать, используя соглашение об именах для ролей базы данных, которое связано с политиками безопасности на уровне строк таблицы. Каждому арендатору назначается роль базы данных в нумерованной последовательности: tenant_1, tenant_2 и т. д. Арендаторы будут подключаться к citus, используя эти отдельные роли. Политики безопасности на уровне строк могут сравнивать имя роли со значениями в столбце распределения tenant_id, чтобы принимать решение о предоставлении доступа.

Ниже описано, как применить этот подход к упрощённой таблице событий, распределённой по tenant_id. Сначала создайте роли tenant_1 и tenant_2. Затем выполните от имени администратора следующие команды:

CREATE TABLE events(
  tenant_id int,
  id int,
  type text
);

SELECT create_distributed_table('events','tenant_id');

INSERT INTO events VALUES (1,1,'foo'), (2,2,'bar');

-- Допустим, роли tenant_1 и tenant_2 существуют
GRANT select, update, insert, delete
  ON events TO tenant_1, tenant_2;

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

Каждая политика состоит из двух предложений: USING и WITH CHECK. Когда пользователь пытается прочитать или записать строки, база данных сравнивает каждую строку с этими условиями. Существующие строки таблицы проверяются на соответствие выражению, указанному в USING, а новые строки, созданные с помощью INSERT или UPDATE, проверяются на соответствие выражению, указанному в WITH CHECK.

-- Сначала зададим политику для администратора системы — пользователя "citus"
CREATE POLICY admin_all ON events
  TO citus           -- применяется к этой роли
  USING (true)       -- читать любые существующие строки
  WITH CHECK (true); -- добавлять или изменять любые строки

-- Далее зададим политику, разрешающую роли "tenant_<n>"
-- доступ к строкам, где tenant_id = <n>
CREATE POLICY user_mod ON events
  USING (current_user = 'tenant_' || tenant_id::text);
  -- Отсутствие CHECK означает, что условие совпадает с USING

-- Применяем политики
ALTER TABLE events ENABLE ROW LEVEL SECURITY;

Теперь роли tenant_1 и tenant_2 получат разные результаты запросов:

При подключении от имени tenant_1:

SELECT * FROM events;
┌───────────┬────┬──────┐
│ tenant_id │ id │ type │
├───────────┼────┼──────┤
│         1 │  1 │ foo  │
└───────────┴────┴──────┘

При подключении от имени tenant_2:

SELECT * FROM events;
┌───────────┬────┬──────┐
│ tenant_id │ id │ type │
├───────────┼────┼──────┤
│         2 │  2 │ bar  │
└───────────┴────┴──────┘
INSERT INTO events VALUES (3,3,'surprise');
/*
ERROR:  new row violates row-level security policy for table "events_102055"
*/
J.5.8.1.9. Расширения Postgres Pro #

Расширение citus обеспечивает Postgres Pro функциональностью распределения с помощью программных интерфейсов и обработчика. Эта функциональность включает, помимо прочего, поддержку широкого спектра типов данных (включая полуструктурированные типы данных, такие как jsonb и hstore), операторы и функции, полнотекстовый поиск и другие расширения, такие как PostGIS и HyperLogLog. Кроме того, правильное использование программных интерфейсов расширения обеспечивает совместимость со стандартными инструментами Postgres Pro, такими как pgAdmin и pg_upgrade.

Поскольку расширение citus можно установить на любой экземпляр Postgres Pro, вместе с ним также могут напрямую использоваться другие расширения, такие как hstore, hll или PostGIS. Однако следует помнить об одной особенности. При включении других расширений в shared_preload_libraries citus всегда должен указываться первым.

Следующие расширения будут особенно полезны при работе с citus:

  • cstore_fdw — столбцовое хранилище для аналитики. Столбцовое хранение обеспечивает производительность за счёт чтения с диска только соответствующих данных и может сжимать данные в 6–10 раз, уменьшая объём пространства, необходимого для архивирования данных.

  • pg_cron — запуск повторяющихся заданий напрямую из базы данных.

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

  • hll — структура данных HyperLogLog как отдельный тип данных. Это структура фиксированного размера, подобная множеству, которая используется для подсчёта различных значений с настраиваемой точностью.

J.5.8.1.10. Создание новой базы данных #

Каждый сервер Postgres Pro может содержать несколько баз данных. Однако новые базы данных не наследуют расширения других; все нужные расширения необходимо добавлять заново. Чтобы запустить citus в новой базе данных, нужно создать базу данных на узле-координаторе и рабочих узлах, создать расширение citus в этой базе данных и зарегистрировать рабочие узлы в базе данных узла-координатора.

Подключитесь к каждому рабочему узлу и выполните:

-- На каждом рабочем узле

CREATE DATABASE newbie;
\c newbie
CREATE EXTENSION citus;

Затем выполните на узле-координаторе:

CREATE DATABASE newbie;
\c newbie
CREATE EXTENSION citus;

SELECT * from citus_add_node('node-name', 5432);
SELECT * from citus_add_node('node-name2', 5432);
-- ... для всех узлов

Теперь новая база данных будет работать как ещё один кластер citus.

J.5.8.2. Управление таблицами #

J.5.8.2.1. Определение размера отношения #

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

ФункцияВозвращает
citus_relation_size
  • Размер фактических данных в таблице («основной слой»)

  • Отношение может быть именем таблицы или индекса

citus_table_size
citus_total_relation_size
  • Вывод функции citus_table_size вместе с:

    • размером индексов

Эти функции аналогичны трём стандартным функциям получения размера объектов БД в Postgres Pro, с небольшим дополнением: если они не могут подключиться к узлу, выдаётся ошибка.

Ниже представлен пример использования одной из вспомогательных функций для вывода размеров всех распределённых таблиц:

SELECT logicalrelid AS name,
       pg_size_pretty(citus_table_size(logicalrelid)) AS size
  FROM pg_dist_partition;

Результат:

┌───────────────┬───────┐
│     name      │ size  │
├───────────────┼───────┤
│ github_users  │ 39 MB │
│ github_events │ 37 MB │
└───────────────┴───────┘
J.5.8.2.2. Очистка распределённых таблиц #

В Postgres Pro (и других базах данных MVCC) команды UPDATE или DELETE не приводят к немедленному удалению старой версии строки. Накопление устаревших строк называется раздуванием, и от них необходимо избавляться, чтобы избежать снижения производительности запросов и неконтролируемого заполнения дискового пространства. Postgres Pro запускает процесс, называемый демоном автоочистки, который периодически очищает (удаляет) устаревшие строки.

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

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

Чтобы очистить таблицу, выполните на узле-координаторе:

VACUUM my_distributed_table;

При очистке распределённой таблицы команда VACUUM будет отправлена во все места размещения этой таблицы (одно подключение на каждое место размещения). Это делается параллельно. Поддерживаются все параметры (в том числе список таблица_и_столбцы), за исключением VERBOSE. Команда VACUUM также запускается на узле-координаторе до уведомления каких-либо рабочих узлов. Обратите внимание, что неквалифицированные команды очистки (т. е. команды без указанной таблицы) не транслируются на рабочие узлы.

J.5.8.2.3. Анализ распределённых таблиц #

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

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

Чтобы проанализировать таблицу, выполните на узле-координаторе:

ANALYZE my_distributed_table;

В citus команда ANALYZE транслируется на все размещения рабочих узлов.

J.5.8.2.4. Столбцовое хранилище #

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

J.5.8.2.4.1. Использование #

Чтобы использовать столбцовое хранилище, укажите USING columnsar при создании таблицы:

CREATE TABLE contestant (
    handle TEXT,
    birthdate DATE,
    rating INT,
    percentile FLOAT,
    country CHAR(3),
    achievements TEXT[]
) USING columnar;

Также можно переключаться между строковым хранением (метод доступа heap) и columnar.

-- Преобразование в строковое хранение (метод доступа heap)
SELECT alter_table_set_access_method('contestant', 'heap');

-- Преобразование в столбцовое хранение (индексы будут удалены)
SELECT alter_table_set_access_method('contestant', 'columnar');

В citus строки преобразуются для столбцового хранения в «массивы» во время вставки. Каждый массив содержит данные одной транзакции или 150 000 строк, в зависимости от того, что меньше. (Размер массива и другие параметры столбцовой таблицы можно изменить с помощью функции alter_columnar_table_set.)

Например, следующий оператор помещает все пять строк в один массив, поскольку все значения вставляются в одной транзакции:

-- Все эти значения вставляются в один столбцовый массив

INSERT INTO contestant VALUES
  ('a','1990-01-10',2090,97.1,'XA','{a}'),
  ('b','1990-11-01',2203,98.1,'XA','{a,b}'),
  ('c','1988-11-01',2907,99.4,'XB','{w,y}'),
  ('d','1985-05-05',2314,98.3,'XB','{}'),
  ('e','1995-05-05',2236,98.2,'XC','{a}');

По возможности лучше создавать большие массивы, поскольку citus сжимает столбцовые данные для каждого массива. Чтобы увидеть информацию о столбцовой таблице, например степень сжатия, количество массивов и среднее количество строк в массиве, используйте VACUUM VERBOSE:

VACUUM VERBOSE contestant;
INFO:  statistics for "contestant":
storage id: 10000000000
total file size: 24576, total data size: 248
compression rate: 1.31x
total row count: 5, stripe count: 1, average rows per stripe: 5
chunk count: 6, containing data for dropped columns: 0, zstd compressed: 6

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

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

J.5.8.2.4.2. Измерение степени сжатия #

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

-- Сначала создайте большую таблицу со строковым хранением.
CREATE TABLE perf_row(
  c00 int8, c01 int8, c02 int8, c03 int8, c04 int8, c05 int8, c06 int8, c07 int8, c08 int8, c09 int8,
  c10 int8, c11 int8, c12 int8, c13 int8, c14 int8, c15 int8, c16 int8, c17 int8, c18 int8, c19 int8,
  c20 int8, c21 int8, c22 int8, c23 int8, c24 int8, c25 int8, c26 int8, c27 int8, c28 int8, c29 int8,
  c30 int8, c31 int8, c32 int8, c33 int8, c34 int8, c35 int8, c36 int8, c37 int8, c38 int8, c39 int8,
  c40 int8, c41 int8, c42 int8, c43 int8, c44 int8, c45 int8, c46 int8, c47 int8, c48 int8, c49 int8,
  c50 int8, c51 int8, c52 int8, c53 int8, c54 int8, c55 int8, c56 int8, c57 int8, c58 int8, c59 int8,
  c60 int8, c61 int8, c62 int8, c63 int8, c64 int8, c65 int8, c66 int8, c67 int8, c68 int8, c69 int8,
  c70 int8, c71 int8, c72 int8, c73 int8, c74 int8, c75 int8, c76 int8, c77 int8, c78 int8, c79 int8,
  c80 int8, c81 int8, c82 int8, c83 int8, c84 int8, c85 int8, c86 int8, c87 int8, c88 int8, c89 int8,
  c90 int8, c91 int8, c92 int8, c93 int8, c94 int8, c95 int8, c96 int8, c97 int8, c98 int8, c99 int8
);

-- Затем создайте таблицу с идентичными столбцами, но столбцовым хранением.
CREATE TABLE perf_columnar(LIKE perf_row) USING COLUMNAR;

Заполните обе таблицы одним и тем же большим набором данных:

INSERT INTO perf_row
  SELECT
    g % 00500, g % 01000, g % 01500, g % 02000, g % 02500, g % 03000, g % 03500, g % 04000, g % 04500, g % 05000,
    g % 05500, g % 06000, g % 06500, g % 07000, g % 07500, g % 08000, g % 08500, g % 09000, g % 09500, g % 10000,
    g % 10500, g % 11000, g % 11500, g % 12000, g % 12500, g % 13000, g % 13500, g % 14000, g % 14500, g % 15000,
    g % 15500, g % 16000, g % 16500, g % 17000, g % 17500, g % 18000, g % 18500, g % 19000, g % 19500, g % 20000,
    g % 20500, g % 21000, g % 21500, g % 22000, g % 22500, g % 23000, g % 23500, g % 24000, g % 24500, g % 25000,
    g % 25500, g % 26000, g % 26500, g % 27000, g % 27500, g % 28000, g % 28500, g % 29000, g % 29500, g % 30000,
    g % 30500, g % 31000, g % 31500, g % 32000, g % 32500, g % 33000, g % 33500, g % 34000, g % 34500, g % 35000,
    g % 35500, g % 36000, g % 36500, g % 37000, g % 37500, g % 38000, g % 38500, g % 39000, g % 39500, g % 40000,
    g % 40500, g % 41000, g % 41500, g % 42000, g % 42500, g % 43000, g % 43500, g % 44000, g % 44500, g % 45000,
    g % 45500, g % 46000, g % 46500, g % 47000, g % 47500, g % 48000, g % 48500, g % 49000, g % 49500, g % 50000
  FROM generate_series(1,50000000) g;

INSERT INTO perf_columnar
  SELECT
    g % 00500, g % 01000, g % 01500, g % 02000, g % 02500, g % 03000, g % 03500, g % 04000, g % 04500, g % 05000,
    g % 05500, g % 06000, g % 06500, g % 07000, g % 07500, g % 08000, g % 08500, g % 09000, g % 09500, g % 10000,
    g % 10500, g % 11000, g % 11500, g % 12000, g % 12500, g % 13000, g % 13500, g % 14000, g % 14500, g % 15000,
    g % 15500, g % 16000, g % 16500, g % 17000, g % 17500, g % 18000, g % 18500, g % 19000, g % 19500, g % 20000,
    g % 20500, g % 21000, g % 21500, g % 22000, g % 22500, g % 23000, g % 23500, g % 24000, g % 24500, g % 25000,
    g % 25500, g % 26000, g % 26500, g % 27000, g % 27500, g % 28000, g % 28500, g % 29000, g % 29500, g % 30000,
    g % 30500, g % 31000, g % 31500, g % 32000, g % 32500, g % 33000, g % 33500, g % 34000, g % 34500, g % 35000,
    g % 35500, g % 36000, g % 36500, g % 37000, g % 37500, g % 38000, g % 38500, g % 39000, g % 39500, g % 40000,
    g % 40500, g % 41000, g % 41500, g % 42000, g % 42500, g % 43000, g % 43500, g % 44000, g % 44500, g % 45000,
    g % 45500, g % 46000, g % 46500, g % 47000, g % 47500, g % 48000, g % 48500, g % 49000, g % 49500, g % 50000
  FROM generate_series(1,50000000) g;

VACUUM (FREEZE, ANALYZE) perf_row;
VACUUM (FREEZE, ANALYZE) perf_columnar;

На примере этих данных видно, что в столбцовой таблице они сжаты более, чем в 8 раз.

SELECT pg_total_relation_size('perf_row')::numeric/
       pg_total_relation_size('perf_columnar') AS compression_ratio;
.
 compression_ratio
--------------------
 8.0196135873627944
(1 row)
J.5.8.2.4.3. Пример #

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

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

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

    BEGIN;
    CREATE TABLE foo_compacted (LIKE foo) USING columnar;
    INSERT INTO foo_compacted SELECT * FROM foo;
    DROP TABLE foo;
    ALTER TABLE foo_compacted RENAME TO foo;
    COMMIT;
  • Принципиально несжимаемые данные могут помешать, но столбцовое хранение всё равно пригодится, чтобы при выборе определённых столбцов в память загружалось меньше данных.

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

    • Если операция нацелена на конкретную строковую секцию (например, UPDATE p2 SET i = i + 1), она завершится успешно; если же она нацелена на столбцовую секцию (например, UPDATE p1 SET i = i + 1), то завершится ошибкой.

    • Если операция выполняется для секционированной таблицы и имеет предложение WHERE, исключающее все столбцовые секции (например, UPDATE parent SET i = i + 1 WHERE timestamp = '2020-03-15'), она завершится успешно.

    • Если операция выполняется для секционированной таблицы, но не исключает все столбцовые секции, она завершится ошибкой, даже если фактически изменяемые данные содержатся в строковых таблицах (например, UPDATE parent SET i = i + 1 WHERE n = 300).

J.5.8.2.4.5. Ограничения #

В будущих версиях citus текущие ограничения будут постепенно сниматься:

  • Только обновление данных (отсутствие поддержки команд UPDATE/DELETE)

  • Отсутствие очистки пространства (например, отменённые транзакции всё равно могут занимать пространство на диске)

  • Поддержка только хеш-индексов и индексов B-деревьев

  • Отсутствие обычного сканирования или сканирования по битовой карте индексов

  • Отсутствие сканирования по идентификатору кортежа

  • Отсутствие сканирования по выборкам

  • Отсутствие поддержки TOAST (встроенная поддержка больших значений)

  • Отсутствие поддержки операторов ON CONFLICT (кроме действий DO NOTHING без указания цели)

  • Отсутствие поддержки блокировки кортежей (SELECT ... FOR SHARE, SELECT ... FOR UPDATE)

  • Отсутствие поддержки уровня изоляции serializable

  • Поддержка серверов Postgres Pro только версий 12 и выше

  • Отсутствие поддержки внешних ключей, ограничений уникальности и ограничений-исключений

  • Отсутствие поддержки логического декодирования

  • Отсутствие поддержки параллельного сканирования внутри узла

  • Отсутствие поддержки триггеров AFTER ... FOR EACH ROW

  • Отсутствие нежурналируемых (UNLOGGED) столбцовых таблиц

  • Отсутствие временных (TEMPORARY) столбцовых таблиц

J.5.9. Устранение неполадок #

J.5.9.1. Настройка производительности запросов #

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

J.5.9.1.1. Сегменты и распределение таблиц #

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

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

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

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

J.5.9.1.2. Настройка Postgres Pro #

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

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

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

Ниже приведён пример объяснения плана для конкретного примера запроса. Используется флаг VERBOSE, чтобы увидеть фактические запросы, которые были отправлены на рабочие узлы.

EXPLAIN VERBOSE
 SELECT date_trunc('minute', created_at) AS minute,
        sum((payload->>'distinct_size')::int) AS num_commits
   FROM github_events
  WHERE event_type = 'PushEvent'
  GROUP BY minute
  ORDER BY minute;
Sort  (cost=0.00..0.00 rows=0 width=0)
  Sort Key: remote_scan.minute
  ->  HashAggregate  (cost=0.00..0.00 rows=0 width=0)
    Group Key: remote_scan.minute
    ->  Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=0 width=0)
      Task Count: 32
      Tasks Shown: One of 32
      ->  Task
        Query: SELECT date_trunc('minute'::text, created_at) AS minute, sum(((payload OPERATOR(pg_catalog.->>) 'distinct_size'::text))::integer) AS num_commits FROM github_events_102042 github_events WHERE (event_type OPERATOR(pg_catalog.=) 'PushEvent'::text) GROUP BY (date_trunc('minute'::text, created_at))
        Node: host=localhost port=5433 dbname=postgres
        ->  HashAggregate  (cost=93.42..98.36 rows=395 width=16)
          Group Key: date_trunc('minute'::text, created_at)
          ->  Seq Scan on github_events_102042 github_events  (cost=0.00..88.20 rows=418 width=503)
            Filter: (event_type = 'PushEvent'::text)
(13 rows)

Результат говорит о следующем. Есть 32 сегмента, и планировщик выбрал адаптивного исполнителя citus для выполнения этого запроса:

->  Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=0 width=0)
  Task Count: 32

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

Tasks Shown: One of 32
->  Task
  Query: SELECT date_trunc('minute'::text, created_at) AS minute, sum(((payload OPERATOR(pg_catalog.->>) 'distinct_size'::text))::integer) AS num_commits FROM github_events_102042 github_events WHERE (event_type OPERATOR(pg_catalog.=) 'PushEvent'::text) GROUP BY (date_trunc('minute'::text, created_at))
  Node: host=localhost port=5433 dbname=postgres

Далее в распределённой команде EXPLAIN показаны результаты запуска обычной команды Postgres Pro EXPLAIN на этом рабочем узле для запроса-фрагмента:

->  HashAggregate  (cost=93.42..98.36 rows=395 width=16)
  Group Key: date_trunc('minute'::text, created_at)
  ->  Seq Scan on github_events_102042 github_events  (cost=0.00..88.20 rows=418 width=503)
    Filter: (event_type = 'PushEvent'::text)

Теперь можно подключиться к рабочему узлу с именем localhost, портом 5433 и настроить производительность запросов для сегмента github_events_102042, используя стандартные Postgres Pro методы. После внесения изменений снова запустите команду EXPLAIN с узла-координатора или прямо на рабочем узле.

Первый набор таких оптимизаций относится к настройкам конфигурации. В Postgres Pro по умолчанию заданы консервативные значения параметров, среди которых наиболее важными для оптимизации производительности чтения являются параметры shared_buffers и work_mem. Их краткое описание приведено ниже. Кроме того, на производительность запросов влияют несколько других параметров конфигурации, которые более подробно описаны в разделе Настройка сервера.

Параметр конфигурации shared_buffers определяет объём памяти, выделяемой базе данных для кеширования, и по умолчанию равен 128 МБ. Если есть рабочий узел с 1 ГБ или более ОЗУ, рекомендуется задавать значение для shared_buffers, равное 1/4 объёма памяти системы. При некоторых нагрузках эффективны даже большие значения shared_buffers, но, учитывая, что Postgres Pro также использует кеш операционной системы, маловероятно, что использование более 25% оперативной памяти будет эффективнее, чем меньшее значение.

Если выполняется много сложных сортировок, увеличение значения work_mem позволит Postgres Pro выполнять более крупные сортировки в памяти, что будет быстрее, чем при использовании диска. Если на рабочем узле заметно активное использование диска, несмотря на значительный объём выделенной оперативной памяти, попробуйте увеличить значение work_mem. Это поможет Postgres Pro выбирать более эффективные планы запросов и выполнять большее количество операций в памяти.

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

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

Для повышения производительности записи, а именно скорости выполнения команды INSERT, можно использовать общую настройку конфигурации Postgres Pro. Обычно рекомендуется увеличить значения параметров checkpoint_timeout и max_wal_size. Кроме того, в зависимости от требований к надёжности вашего приложения можно изменить значения fsync или synchronous_commit.

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

SET citus.explain_all_tasks = 1;

После этого команда EXPLAIN будет показывать планы запросов для всех заданий.

EXPLAIN
 SELECT date_trunc('minute', created_at) AS minute,
        sum((payload->>'distinct_size')::int) AS num_commits
   FROM github_events
  WHERE event_type = 'PushEvent'
  GROUP BY minute
  ORDER BY minute;
Sort  (cost=0.00..0.00 rows=0 width=0)
  Sort Key: remote_scan.minute
  ->  HashAggregate  (cost=0.00..0.00 rows=0 width=0)
    Group Key: remote_scan.minute
    ->  Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=0 width=0)
      Task Count: 32
      Tasks Shown: All
      ->  Task
        Node: host=localhost port=5433 dbname=postgres
        ->  HashAggregate  (cost=93.42..98.36 rows=395 width=16)
          Group Key: date_trunc('minute'::text, created_at)
          ->  Seq Scan on github_events_102042 github_events  (cost=0.00..88.20 rows=418 width=503)
            Filter: (event_type = 'PushEvent'::text)
      ->  Task
        Node: host=localhost port=5434 dbname=postgres
        ->  HashAggregate  (cost=103.21..108.57 rows=429 width=16)
          Group Key: date_trunc('minute'::text, created_at)
          ->  Seq Scan on github_events_102043 github_events  (cost=0.00..97.47 rows=459 width=492)
            Filter: (event_type = 'PushEvent'::text)
      --
      -- ... repeats for all 32 tasks
      --     alternating between workers one and two
      --     (running in this case locally on ports 5433, 5434)
      --

(199 rows)

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

Примечание

Обратите внимание, что когда включён параметр citus.explain_all_tasks, планы команды EXPLAIN извлекаются последовательно, что может занять много времени при использовании EXPLAIN ANALYZE.

Расширение citus по умолчанию сортирует задачи по времени выполнения в порядке убывания. Если параметр citus.explain_all_tasks отключён, citus показывает одну самую длительную задачу. Обратите внимание, что эту функцию можно использовать только с командой EXPLAIN ANALYZE, поскольку обычная команда EXPLAIN не выполняет запросы и, следовательно, не может показать время их выполнения. Чтобы изменить порядок сортировки, используйте параметр конфигурации citus.explain_analyze_sort_method.

J.5.9.1.3. Масштабирование производительности #

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

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

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

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

J.5.9.1.4. Настройка производительности распределённых запросов #

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

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

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

J.5.9.1.4.1. Общая настройка производительности #

Уровень распараллеливания больше всего влияет на скорость и производительность оператора INSERT. Попробуйте выполнить несколько операторов INSERT одновременно. Таким образом, можно достичь очень высокой скорости вставки при наличии мощного узла-координатора и совместном использовании всех ядер ЦП на этом узле.

J.5.9.1.4.1.1. Сетевые издержки подзапросов/CTE #

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

Важно помнить, что подзапросы выполняются на отдельном этапе, и не отправлять слишком большой объём данных между рабочими узлами. Нагрузка на сеть влияет на производительность. Команда EXPLAIN позволяет узнать, как будут выполняться запросы, в том числе потребуется ли несколько этапов. За подробным примером обратитесь к разделу Двухэтапное выполнение подзапросов/CTE.

Также можно избежать получения больших промежуточных результатов. Настройте ограничение citus.max_intermediate_result_size в новом подключении с узлом-координатором. По умолчанию максимальный размер промежуточного результата составляет 1 ГБ, что допускает выполнение некоторых неэффективных запросов. Попробуйте уменьшить это значение и выполнить запросы:

-- Задайте ограничение для промежуточных результатов
SET citus.max_intermediate_result_size = '512kB';

-- Попробуйте выполнить запросы
-- SELECT …

Если в запросе есть подзапросы или CTE, превышающие этот предел, он будет отменён с выводом сообщения об ошибке:

ERROR:  the intermediate result size exceeds citus.max_intermediate_result_size (currently 512 kB)
DETAIL:  Citus restricts the size of intermediate results of complex subqueries and CTEs to avoid accidentally pulling large result sets into once place.
HINT:  To run the current query, set citus.max_intermediate_result_size to a higher value or -1 to disable.

Размер промежуточных результатов и их пункт назначения показываются в выводе команды EXPLAIN ANALYZE:

EXPLAIN ANALYZE
WITH deleted_rows AS (
  DELETE FROM page_views WHERE tenant_id IN (3, 4) RETURNING *
), viewed_last_week AS (
  SELECT * FROM deleted_rows WHERE view_time > current_timestamp - interval '7 days'
)
SELECT count(*) FROM viewed_last_week;
Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=0 width=0) (actual time=570.076..570.077 rows=1 loops=1)
  ->  Distributed Subplan 31_1
        Subplan Duration: 6978.07 ms
        Intermediate Data Size: 26 MB
        Result destination: Write locally
        ->  Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=0 width=0) (actual time=364.121..364.122 rows=0 loops=1)
              Task Count: 2
              Tuple data received from nodes: 0 bytes
              Tasks Shown: One of 2
              ->  Task
                    Tuple data received from node: 0 bytes
                    Node: host=localhost port=5433 dbname=postgres
                    ->  Delete on page_views_102016 page_views  (cost=5793.38..49272.28 rows=324712 width=6) (actual time=362.985..362.985 rows=0 loops=1)
                          ->  Bitmap Heap Scan on page_views_102016 page_views  (cost=5793.38..49272.28 rows=324712 width=6) (actual time=362.984..362.984 rows=0 loops=1)
                                Recheck Cond: (tenant_id = ANY ('{3,4}'::integer[]))
                                ->  Bitmap Index Scan on view_tenant_idx_102016  (cost=0.00..5712.20 rows=324712 width=0) (actual time=19.193..19.193 rows=325733 loops=1)
                                      Index Cond: (tenant_id = ANY ('{3,4}'::integer[]))
                        Planning Time: 0.050 ms
                        Execution Time: 363.426 ms
        Planning Time: 0.000 ms
        Execution Time: 364.241 ms
 Task Count: 1
 Tuple data received from nodes: 6 bytes
 Tasks Shown: All
 ->  Task
       Tuple data received from node: 6 bytes
       Node: host=localhost port=5432 dbname=postgres
       ->  Aggregate  (cost=33741.78..33741.79 rows=1 width=8) (actual time=565.008..565.008 rows=1 loops=1)
             ->  Function Scan on read_intermediate_result intermediate_result  (cost=0.00..29941.56 rows=1520087 width=0) (actual time=326.645..539.158 rows=651466 loops=1)
                   Filter: (view_time > (CURRENT_TIMESTAMP - '7 days'::interval))
           Planning Time: 0.047 ms
           Execution Time: 569.026 ms
Planning Time: 1.522 ms
Execution Time: 7549.308 ms

В приведённом выше выводе команды EXPLAIN ANALYZE можно увидеть следующую информацию о промежуточных результатах:

Intermediate Data Size: 26 MB
Result destination: Write locally

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

Intermediate Data Size: 26 MB
Result destination: Send to 2 nodes

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

При использовании CTE или соединений CTE и распределённых таблиц можно избежать двухэтапного выполнения, следуя этим правилам:

  • Таблицы должны быть совмещёнными.

  • Для CTE-запросов не должно требоваться каких-либо шагов слияния (например LIMIT или GROUP BY для нераспределённого ключа).

  • Таблицы и CTE следует соединять по ключам распределения.

Кроме того, Postgres Pro позволяет citus использовать встраивание CTE для передачи CTE рабочим узлам в большем количестве случаев. Поведением встраивания можно управлять с помощью ключевого слова MATERIALIZED. За дополнительной информацией обратитесь к разделу Запросы WITH (Общие табличные выражения).

J.5.9.1.4.2. Расширенная настройка производительности #

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

J.5.9.1.4.2.1. Управление подключениями #

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

  • Установите для параметра конфигурации citus.max_adaptive_executor_pool_size небольшое значение, например 1 или 2 для сценария транзакционных нагрузок с короткими запросами (например, задержка < 20 мс). Для аналитических нагрузок, где распараллеливание имеет решающее значение, оставьте для этого параметра значение по умолчанию — 16.

  • Установите для параметра конфигурации citus.executor_slow_start_interval большое значение, например 100 мс, для сценария использования с транзакционными нагрузками, состоящими из коротких запросов, которые зависят от пропускной способности сети, а не от степени распараллеливания. Для аналитических нагрузок оставьте для этого параметра значение по умолчанию, равное 10 мс.

  • Значение по умолчанию 1 для параметра конфигурации citus.max_cached_conns_per_worker вполне целесообразно. Большее значение, например 2, может подойти для кластеров, которые используют небольшое количество параллельных сеансов, но не стоит его сильно увеличивать (например, 16 будет слишком большим). Если установлено слишком большое значение, сеансы будут удерживать бездействующие соединения и без необходимости использовать ресурсы рабочих узлов.

  • Установите параметр конфигурации citus.max_shared_pool_size в соответствии с параметрами max_connections рабочих узлов. Этот параметр необходим для обеспечения отказоустойчивости.

J.5.9.1.4.2.2. Политика назначения заданий #

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

Политика greedy направлена на равномерное распределение заданий между рабочими узлами. Эта политика используется по умолчанию и хорошо работает в большинстве случаев. Политика round-robin назначает задачи рабочим узлам по порядку, чередуя разные реплики. Она обеспечивает гораздо лучшее использование кластера, когда количество сегментов таблицы невелико по сравнению с количеством рабочих процессов. Третья политика — first-replica — распределяет задачи на основе порядка вставки размещений (реплик) для сегментов. Эта политика позволяет гарантировать, что определённые сегменты будут использоваться на определённых узлах. Это помогает обеспечить более строгие гарантии сохранения объёма памяти, то есть хранить рабочий набор данных в памяти и использовать его для запросов.

J.5.9.1.4.2.3. Двоичный протокол #

В некоторых случаях большая часть времени запроса тратится на отправку результатов запроса от рабочих узлов узлу-координатору. Чаще всего это происходит, когда запросы запрашивают много строк (например, SELECT * FROM table) или когда столбцы результатов используют типы больших данных (например, hll или tdigest) из расширений hll и tdigest).

В таких случаях бывает полезно установить для citus.enable_binary_protocol значение true, что изменит кодировку результатов с текстовой на двоичную. Двоичная кодировка значительно снижает пропускную способность для типов, имеющих компактное двоичное представление, таких как hll, tdigest, timestamp и double precision. По умолчанию для этого параметра конфигурации уже установлено значение true, поэтому его можно не включать явно.

J.5.9.1.5. Масштабирование поглощения данных #

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

J.5.9.1.5.1. Добавление и изменение данных в реальном времени #

На узле-кооррдинаторе citus можно выполнять команды INSERT, INSERT .. ON CONFLICT, UPDATE и DELETE напрямую для распределённых таблиц. При использовании одной из этих команд изменения сразу видны пользователю.

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

J.5.9.1.5.2. Временное хранение данных #

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

-- Пример нежурналируемой таблицы
CREATE UNLOGGED TABLE unlogged_table (
  key text,
  value text
);

-- Её сегменты также будут нежурналируемыми даже
-- после распределения таблицы
SELECT create_distributed_table('unlogged_table', 'key');

-- Можно загружать данные
J.5.9.1.5.3. Массовое копирования (250 000 - 2 000 000 строк в секунду) #

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

Команду COPY можно использовать для загрузки данных непосредственно из приложения с помощью COPY .. FROM STDIN, из файла на сервере или программы, выполняемой на сервере.

COPY pgbench_history FROM STDIN WITH (FORMAT CSV);

В psql команду \copy можно использовать для загрузки данных с локальной машины. Команда \COPY фактически отправляет команду COPY .. FROM STDIN на сервер перед отправкой локальных данных, как и приложение, которое загружает данные напрямую.

psql -c "\COPY pgbench_history FROM 'pgbench_history-2016-03-04.csv' (FORMAT CSV)"

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

Ожидаемая производительность при использовании команды COPY — поглощение от 250 000 до 2 000 000 строк в секунду.

Примечание

Проверьте настройку параметров тестирования, чтобы получить оптимальную производительность COPY. Следуйте этим советам:

  • Рекомендуется задать большой размер порции (~ 50 000-100 000). Можно протестировать несколько файлов (1, 10, 1000, 10 000 и т. д.), каждый из которых соответствует размеру порции.

  • Используйте параллельное поглощение. Увеличьте количество потоков/поглотителей до 2, 4, 8, 16 и запустите тесты.

  • Используйте узел-координатор, оптимизированный для вычислений. Для рабочих узлов выбирайте машины с большим объёмом ОЗУ и количеством ЦП.

  • Выбирайте относительно небольшое количество сегментов: 32 должно быть достаточно, но также можно провести тесты с 64.

  • Поглощайте данные в течение подходящего периода времени (например, за 2, 4, 8, 24 часа). Чем дольше проходят тесты, тем более они показательны для производственной установки.

J.5.9.2. Полезные диагностические запросы #

J.5.9.2.1. Поиск сегмента, содержащего данные конкретного арендатора #

Строки распределённой таблицы группируются в сегменты, и каждый сегмент размещается на рабочем узле в кластере citus. В многоарендном варианте использования citus можно определить, какой рабочий узел содержит строки конкретного арендатора, соединив две части информации: shard_id, связанный с tenant_id, и размещения сегментов на рабочих узлах. Обе части можно получить в одном запросе. Предположим, что арендаторы нашего многоарендного приложения — магазины, и нужно определить, какой рабочий узел содержит данные для Gap.com (предположим, id=4).

Чтобы найти рабочий узел, содержащий данные для магазина id=4, запросите размещение строк, столбец распределения которых имеет значение 4:

SELECT shardid, shardstate, shardlength, nodename, nodeport, placementid
  FROM pg_dist_placement AS placement,
       pg_dist_node AS node
 WHERE placement.groupid = node.groupid
   AND node.noderole = 'primary'
   AND shardid = (
     SELECT get_shard_id_for_distribution_column('stores', 4)
   );

Результат содержит имя и порт базы данных рабочего узла.

┌─────────┬────────────┬─────────────┬───────────┬──────────┬─────────────┐
│ shardid │ shardstate │ shardlength │ nodename  │ nodeport │ placementid │
├─────────┼────────────┼─────────────┼───────────┼──────────┼─────────────┤
│  102009 │          1 │           0 │ localhost │     5433 │           2 │
└─────────┴────────────┴─────────────┴───────────┴──────────┴─────────────┘
J.5.9.2.2. Поиск узла, на котором размещена распределённая схема #

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

SELECT schema_name, nodename, nodeport
  FROM citus_shards
  JOIN citus_schemas cs
    ON cs.colocation_id = citus_shards.colocation_id
 GROUP BY 1,2,3;
schema_name | nodename  | nodeport
-------------+-----------+----------
a           | localhost |     9701
b           | localhost |     9702
with_data   | localhost |     9702

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

SELECT * FROM citus_shards WHERE citus_table_type = 'schema';
  table_name   | shardid |      shard_name       | citus_table_type | colocation_id | nodename  | nodeport | shard_size | schema_name | colocation_id | schema_size | schema_owner
----------------+---------+-----------------------+------------------+---------------+-----------+----------+------------+-------------+---------------+-------------+--------------
a.cities       |  102080 | a.cities_102080       | schema           |             4 | localhost |     9701 |       8192 | a           |             4 | 128 kB      | citus
a.map_tags     |  102145 | a.map_tags_102145     | schema           |             4 | localhost |     9701 |      32768 | a           |             4 | 128 kB      | citus
a.measurement  |  102047 | a.measurement_102047  | schema           |             4 | localhost |     9701 |          0 | a           |             4 | 128 kB      | citus
a.my_table     |  102179 | a.my_table_102179     | schema           |             4 | localhost |     9701 |      16384 | a           |             4 | 128 kB      | citus
a.people       |  102013 | a.people_102013       | schema           |             4 | localhost |     9701 |      32768 | a           |             4 | 128 kB      | citus
a.test         |  102008 | a.test_102008         | schema           |             4 | localhost |     9701 |       8192 | a           |             4 | 128 kB      | citus
a.widgets      |  102146 | a.widgets_102146      | schema           |             4 | localhost |     9701 |      32768 | a           |             4 | 128 kB      | citus
b.test         |  102009 | b.test_102009         | schema           |             5 | localhost |     9702 |       8192 | b           |             5 | 32 kB       | citus
b.test_col     |  102012 | b.test_col_102012     | schema           |             5 | localhost |     9702 |      24576 | b           |             5 | 32 kB       | citus
with_data.test |  102180 | with_data.test_102180 | schema           |            11 | localhost |     9702 |     647168 | with_data   |            11 | 632 kB      | citus
J.5.9.2.3. Поиск столбца распределения таблицы #

У каждой распределённой таблицы в citus есть «столбец распределения». За дополнительной информацией обратитесь к разделу Выбор столбца распределения. Существует множество ситуаций, когда важно знать, какой именно это столбец. Некоторые операции требуют соединения или фильтрации по столбцу распределения, и можно столкнуться с сообщениями об ошибках, содержащими подсказки, например add a filter to the distribution column (добавьте фильтр по столбцу распределения).

Таблицы pg_dist_* на узле-координаторе содержат разнообразные метаданные о распределённой базе данных. В частности, таблица pg_dist_partition содержит информацию о столбце распределения (ранее называвшемся столбцом partition) каждой таблицы. Можно использовать удобную служебную функцию для поиска имени столбца распределения по низкоуровневым сведениям в метаданных. Пример этой функции:

-- Создайте таблицу для примера

CREATE TABLE products (
  store_id bigint,
  product_id bigint,
  name text,
  price money,

  CONSTRAINT products_pkey PRIMARY KEY (store_id, product_id)
);

-- Выберите store_id в качестве столбца распределения

SELECT create_distributed_table('products', 'store_id');

-- Узнайте имя столбца распределения для таблицы products

SELECT column_to_column_name(logicalrelid, partkey) AS dist_col_name
  FROM pg_dist_partition
 WHERE logicalrelid='products'::regclass;

Пример вывода:

┌───────────────┐
│ dist_col_name │
├───────────────┤
│ store_id      │
└───────────────┘
J.5.9.2.4. Обнаружение блокировок #

Этот запрос будет выполняться на всех рабочих узлах и обнаруживать блокировки, их длительность и вызвавшие их запросы:

SELECT * FROM citus_lock_waits;

За дополнительной информацией обратитесь к разделу Активность распределённых запросов.

J.5.9.2.5. Определение размеров пользовательских сегментов #

Этот запрос вернёт размер каждого фрагмента данной распределённой таблицы, обозначенной здесь шаблоном моя_таблица:

SELECT shardid, table_name, shard_size
FROM citus_shards
WHERE table_name = 'моя_таблица';

Пример вывода:

.
 shardid | table_name | shard_size
---------+------------+------------
  102170 | my_table   |   90177536
  102171 | my_table   |   90177536
  102172 | my_table   |   91226112
  102173 | my_table   |   90177536

Этот запрос использует представление citus_shards.

J.5.9.2.6. Определение размеров всех распределённых таблиц #

Этот запрос возвращает список размеров распределённых таблиц вместе с индексами.

SELECT table_name, table_size
  FROM citus_tables;

Пример вывода:

┌───────────────┬────────────┐
│  table_name   │ table_size │
├───────────────┼────────────┤
│ github_users  │ 39 MB      │
│ github_events │ 98 MB      │
└───────────────┴────────────┘

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

J.5.9.2.7. Определение неиспользуемых индексов #

Этот запрос будет выполняться на всех рабочих узлах и определять все неиспользуемые индексы для заданной распределённой таблицы, обозначенной здесь шаблоном моя_распределённая_таблица:

SELECT *
FROM run_command_on_shards('моя_распределённая_таблица', $cmd$
  SELECT array_agg(a) as infos
  FROM (
    SELECT (
      schemaname || '.' || relname || '##' || indexrelname || '##'
                 || pg_size_pretty(pg_relation_size(i.indexrelid))::text
                 || '##' || idx_scan::text
    ) AS a
    FROM  pg_stat_user_indexes ui
    JOIN  pg_index i
    ON    ui.indexrelid = i.indexrelid
    WHERE NOT indisunique
    AND   idx_scan < 50
    AND   pg_relation_size(relid) > 5 * 8192
    AND   (schemaname || '.' || relname)::regclass = '%s'::regclass
    ORDER BY
      pg_relation_size(i.indexrelid) / NULLIF(idx_scan, 0) DESC nulls first,
      pg_relation_size(i.indexrelid) DESC
  ) sub
$cmd$);

Пример вывода:

┌─────────┬─────────┬─────────────────────────────────────────────────────────────────────────────────┐
│ shardid │ success │                            result                                               │
├─────────┼─────────┼─────────────────────────────────────────────────────────────────────────────────┤
│  102008 │ t       │                                                                                 │
│  102009 │ t       │ {"public.my_distributed_table_102009##stupid_index_102009##28 MB##0"}           │
│  102010 │ t       │                                                                                 │
│  102011 │ t       │                                                                                 │
└─────────┴─────────┴─────────────────────────────────────────────────────────────────────────────────┘
J.5.9.2.8. Мониторинг количества клиентских подключений #

Этот запрос вернёт количество открытых на узле-координаторе подключений по каждому типу:

SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;

Пример вывода:

┌────────┬───────┐
│ state  │ count │
├────────┼───────┤
│ active │     3 │
│ ∅     │     1 │
└────────┴───────┘
J.5.9.2.9. Просмотр системных запросов #
J.5.9.2.9.1. Активные запросы #

В представлении citus_stat_activity показаны запросы, выполняющиеся в данный момент. Их можно отфильтровать, чтобы найти активно выполняющиеся процессы, а также идентификатор их сервера:

SELECT global_pid, query, state
  FROM citus_stat_activity
 WHERE state != 'idle';
J.5.9.2.9.2. Причины ожидания запросов #

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

SELECT wait_event || ':' || wait_event_type AS type, count(*) AS number_of_occurences
  FROM pg_stat_activity
 WHERE state != 'idle'
GROUP BY wait_event, wait_event_type
ORDER BY number_of_occurences DESC;

Пример вывода при параллельном выполнении функции pg_sleep в отдельном запросе:

┌─────────────────┬──────────────────────┐
│      type       │ number_of_occurences │
├─────────────────┼──────────────────────┤
│ ∅              │                    1 │
│ PgSleep:Timeout │                    1 │
└─────────────────┴──────────────────────┘
J.5.9.2.10. Коэффициент попадания в индекс #

Этот запрос показывает частоту попаданий в индекс по всем узлам. Коэффициент попадания в индекс полезен для определения частоты использования индексов в запросах:

-- На узле-координаторе
SELECT 100 * (sum(idx_blks_hit) - sum(idx_blks_read)) / sum(idx_blks_hit) AS index_hit_rate
  FROM pg_statio_user_indexes;

-- На рабочих узлах
SELECT nodename, result as index_hit_rate
FROM run_command_on_workers($cmd$
  SELECT 100 * (sum(idx_blks_hit) - sum(idx_blks_read)) / sum(idx_blks_hit) AS index_hit_rate
    FROM pg_statio_user_indexes;
$cmd$);

Пример вывода:

┌───────────┬────────────────┐
│ nodename  │ index_hit_rate │
├───────────┼────────────────┤
│ 10.0.0.16 │ 96.0           │
│ 10.0.0.20 │ 98.0           │
└───────────┴────────────────┘
J.5.9.2.11. Коэффициент попадания в кеш #

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

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

-- На узле-координаторе
SELECT
  sum(heap_blks_read) AS heap_read,
  sum(heap_blks_hit)  AS heap_hit,
  100 * sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS cache_hit_rate
FROM
  pg_statio_user_tables;

-- На рабочих узлах
SELECT nodename, result as cache_hit_rate
FROM run_command_on_workers($cmd$
  SELECT
    100 * sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS cache_hit_rate
  FROM
    pg_statio_user_tables;
$cmd$);

Пример вывода:

┌───────────┬──────────┬─────────────────────┐
│ heap_read │ heap_hit │   cache_hit_rate    │
├───────────┼──────────┼─────────────────────┤
│         1 │      132 │ 99.2481203007518796 │
└───────────┴──────────┴─────────────────────┘

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

J.5.9.3. Распространённые сообщения об ошибках #

J.5.9.3.1. could not connect to server: Connection refused #

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

SELECT 1 FROM companies WHERE id = 2928;
ERROR:  connection to the remote node localhost:5432 failed with the following error: could not connect to server: Connection refused
        Is the server running on host "localhost" (127.0.0.1) and accepting
        TCP/IP connections on port 5432?
J.5.9.3.1.1. Решение #

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

J.5.9.3.2. canceling the transaction since it was involved in a distributed deadlock #

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

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

CREATE TABLE lockme (id int, x int);
SELECT create_distributed_table('lockme', 'id');

-- id=1 идёт на один рабочий узел, а id=2 на другой
INSERT INTO lockme VALUES (1,1), (2,2);

--------------- TX 1 ----------------  --------------- TX 2 ----------------
BEGIN;
                                       BEGIN;
UPDATE lockme SET x = 3 WHERE id = 1;
                                       UPDATE lockme SET x = 4 WHERE id = 2;
UPDATE lockme SET x = 3 WHERE id = 2;
                                       UPDATE lockme SET x = 4 WHERE id = 1;
ERROR:  canceling the transaction since it was involved in a distributed deadlock
J.5.9.3.2.1. Решение #

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

J.5.9.3.3. could not connect to server: Cannot assign requested address #
WARNING:  connection error: localhost:9703
DETAIL:  could not connect to server: Cannot assign requested address

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

J.5.9.3.3.1. Решение #

Настройте операционную систему для повторного использования TCP-сокетов. Выполните в оболочке на узле-координаторе:

sysctl -w net.ipv4.tcp_tw_reuse=1

Это позволяет повторно использовать сокеты в состоянии TIME_WAIT для новых подключений, когда это безопасно с точки зрения протокола. Значение по умолчанию — 0 (отключено).

J.5.9.3.4. SSL error: certificate verify failed #

В citus узлы по умолчанию должны взаимодействовать друг с другом по протоколу SSL. Если протокол SSL не был включён на сервере Postgres Pro, он будет включён в процессе первой установки citus, то есть будет создан и самоподписан сертификат SSL.

Однако если существует файл корневого сертификата центра сертификации (обычно в ~/.postgresql/root.crt), то проверка сертификата на соответствие этому центру сертификации во время подключения закончится неудачей.

J.5.9.3.4.1. Решение #

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

J.5.9.3.5. could not connect to any active placements #

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

WARNING:  connection error: hostname:5432
ERROR:  could not connect to any active placements
J.5.9.3.5.1. Решение #

Эта ошибка чаще всего возникает при параллельном копировании данных в citus. Команда COPY открывает одно подключение для каждого сегмента. Если запустить M одновременных операций копирования в место назначения с N сегментами, это приведёт к созданию M*N подключений. Чтобы устранить эту ошибку, уменьшите количество сегментов целевых распределённых таблиц или запускайте меньше команд \copy параллельно.

J.5.9.3.6. remaining connection slots are reserved for non-replication superuser connections #

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

J.5.9.3.6.1. Решение #

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

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

J.5.9.3.7. pgbouncer cannot connect to server #

В локальном кластере citus эта ошибка указывает на то, что узел-координатор не отвечает на запросы pgbouncer.

J.5.9.3.7.1. Решение #

Попробуйте подключиться напрямую к серверу с помощью psql, чтобы убедиться, что он работает и принимает подключения.

J.5.9.3.8. creating unique indexes on non-partition columns is currently unsupported #

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

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

ERROR:  creating unique indexes on non-partition columns is currently unsupported

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

J.5.9.3.8.1. Решение #

Существует два способа обеспечить уникальность столбца, не являющегося столбцом распределением:

  1. Создайте составной уникальный индекс или первичный ключ, включающий нужный столбец (C), а также столбец распределения (D). Это не такое строгое условие, как уникальность только для C, но оно гарантирует, что значения C уникальны для каждого значения D. Например, при распределении по company_id в многоарендной системе такой подход сделает C уникальным внутри каждой компании.

  2. Используйте таблицы-справочники, а не таблицы, распределённые по хешу. Этот совет подходит только для небольших таблиц, так как содержимое таблицы-справочника будет дублироваться на всех узлах.

J.5.9.3.9. function create_distributed_table does not exist #
SELECT create_distributed_table('foo', 'id');
/*
ERROR:  function create_distributed_table(unknown, unknown) does not exist
LINE 1: SELECT create_distributed_table('foo', 'id');
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
*/
J.5.9.3.9.1. Решение #

Если основные вспомогательные функции недоступны, проверьте правильность установки расширения citus. При выполнении команды \dx в psql будет показан список установленных расширений.

Один из способов избавиться от расширений — создать новую базу данных на сервере Postgres Pro, для которой потребуется переустановить расширения. Чтобы сделать это правильно, ознакомьтесь с разделом Создание новой базы данных.

J.5.9.3.10. STABLE functions used in UPDATE queries cannot be called with column references #

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

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

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

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

-- foo_timestamp имеет тип timestamp, а не timestamptz
UPDATE foo SET ... WHERE foo_timestamp < now();
ERROR:  STABLE functions used in UPDATE queries cannot be called with column references

В данном случае оператор сравнения < между timestamp и timestamptz не является постоянным.

J.5.9.3.10.1. Решение #

Не используйте стабильные функции для столбцов в распределённом операторе UPDATE. В частности, при работе со временем используйте timestamptz, а не timestamp. Наличие часового пояса в timestamptz делает вычисления постоянными.

J.5.10. Часто задаваемые вопросы #

J.5.10.1. Можно ли создавать первичные ключи в распределённых таблицах? #

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

J.5.10.2. Как добавлять узлы в существующий кластер citus? #

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

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

J.5.10.3. Как расширение citus обрабатывает сбой рабочего узла? #

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

J.5.10.4. Как расширение citus обрабатывает сбой узла-координатора? #

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

J.5.10.5. Какие функции Postgres Pro не поддерживаются в citus? #

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

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

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

J.5.10.6. Как выбрать количество сегментов при секционировании данных по хешу? #

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

В сценарии использования многоарендной базы данных SaaS рекомендуется выбирать между 32 и 128 сегментами. Для небольших нагрузок, например менее 100 ГБ, можно начать с 32 сегментов, а для более крупных — с 64 или 128. Таким образом можно масштабироваться с 32 до 128 машин рабочих узлов.

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

Чтобы выбрать количество сегментов для распределяемой таблицы, измените параметр конфигурации citus.shard_count. Это повлияет на будущие вызовы функции create_distributed_table. Например:

SET citus.shard_count = 64;
-- у всех распределяемых таблиц теперь будет
-- шестьдесят четыре сегмента

За более подробной информацией по этой теме обратитесь к разделу Выбор размера кластера.

J.5.10.7. Как изменить количество сегментов для разделённой по хешу таблицы? #

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

J.5.10.8. Как в citus реализованы запросы count(distinct)? #

В citus агрегатные функции count(distinct) могут вычисляться как на одном, так и на нескольких рабочих узлах. Если агрегатная функция count(distinct) выполняется по столбцу распределения, citus может отправлять вычисления на рабочие узлы и получать итоговые результаты. В противном случае отдельные строки передаются узлу-координатору и вычисления выполняются на нём. Если передача данных координатору обходится слишком дорого, доступны и быстрые приблизительные подсчёты. За дополнительной информацией обратитесь к разделу Агрегатные функции count(distinct).

J.5.10.9. В каких случаях поддерживаются ограничения уникальности для распределённых таблиц? #

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

Это ограничение позволяет citus локализовать проверку уникальности в одном сегменте, а Postgres Pro — эффективно выполнять проверку на рабочем узле.

J.5.10.10. Как создавать роли, функции, расширения базы данных и т. п. в кластере citus? #

Некоторые команды, запускаемые на узле-координаторе, не транслируются на рабочие узлы:

  • CREATE ROLE/USER

  • CREATE DATABASE

  • ALTER … SET SCHEMA

  • ALTER TABLE ALL IN TABLESPACE

  • CREATE TABLE (см. раздел Типы таблиц)

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

SELECT run_command_on_workers($cmd$
  /* the command to run */
  CREATE ROLE ...
$cmd$);

За более подробной информацией обратитесь к разделу Ручная трансляция запросов. Также обратите внимание, что даже после ручной трансляции команды CREATE DATABASE, всё равно необходимо установить на рабочих узлах citus. См. раздел Создание новой базы данных.

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

J.5.10.11. Что произойдёт при изменении адреса рабочего узла? #

Если изменится адрес или IP-адрес рабочего узла, необходимо передать информацию об этом узлу-координатору с помощью функции citus_update_node:

-- Измените метаданные рабочего узла на узле-координаторе
-- (не забудьте заменить «старый адрес» и «новый адрес»
-- настоящими значениями)

SELECT citus_update_node(nodeid, 'new-address', nodeport)
  FROM pg_dist_node
 WHERE nodename = 'old-address';

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

J.5.10.12. В каком сегменте хранятся данные конкретного арендатора? #

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

J.5.10.13. Как найти столбец распределения таблицы? #

Эта информация хранится в таблицах метаданных узла-координатора citus. См. раздел Поиск столбца распределения таблицы.

J.5.10.14. Возможно ли распределение таблицы по нескольким ключам? #

Нет, нужно выбрать один столбец таблицы в качестве столбца распределения. Распространённый сценарий для распределения данных по двум столбцам — данные временных рядов. Однако в этом случае рекомендуется использовать хеш-распределение по столбцу, не связанному со временем, вместе с секционированием Postgres Pro по столбцу времени, как описано в разделе Данные временных рядов.

J.5.10.15. Почему функция pg_relation_size показывает нулевой размер в байтах для распределённой таблицы? #

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

J.5.10.16. Почему возникает ошибка, связанная с citus.max_intermediate_result_size? #

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

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

ERROR:  the intermediate result size exceeds citus.max_intermediate_result_size (currently 1 GB)
DETAIL:  Citus restricts the size of intermediate results of complex subqueries and CTEs to avoid accidentally pulling large result sets into once place.
HINT:  To run the current query, set citus.max_intermediate_result_size to a higher value or -1 to disable.

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

SET citus.max_intermediate_result_size = '3GB';

J.5.10.17. Возможно ли в citus сегментирование по схеме для многоарендных приложений? #

Да, сегментирование нс основе схем доступно.

J.5.10.18. Как citus работает с cstore_fdw? #

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