Аргументы в пользу вертикального масштабирования
Если вы читали какие-либо статьи или книги по проектированию систем, то, вероятно, знаете, что такое вертикальное и горизонтальное масштабирование и каковы преимущества горизонтального масштабирования. Прежде чем я объясню, как правильно настроить горизонтальное масштабирование в Postgres, позвольте мне привести пример, когда этого делать не следует.
- Простота: База данных на одном узле означает, что вы можете запустить свою базу данных «из коробки». Хотя я рекомендую использовать PGTune для быстрой настройки или посетить postconf для полного описания.
- Более простое резервное копирование и восстановление: Нет необходимости думать о состоянии реплик при создании или применении резервных копий.
- Отсутствие сетевой нагрузки, особенно при операциях с интенсивной записью.
- Временное решение: Если вам нужно решение прямо сейчас, это то, что нужно.
Предварительная подготовка
Необходимо установить следющий тулинг
Для следования этому руководству вам потребуется базовое понимание Kubernetes, CRD и Helm. Ничего сложного, достаточно краткого обзора.
Репликация
Репликация означает хранение нескольких копий данных на нескольких серверах соединенных по сети. Вот почему вам может понадобиться это сделать:
- Это позволяет держать данные рядом с пользователями.
- Это действует как резервная копия на случай сбоя основного сервера.
- Это помогает масштабировать систему, если большая часть вашей рабочей нагрузки приходится на операции чтения (что характерно для большинства OLTP )
Здесь pg-pool выступает в роли балансировщика нагрузки, равномерно распределяя запросы на чтение между подписчиками и запросы на изменение данных — к лидеру. Обратите внимание, что лидер периодически синхронизирует свой WAL со своими подписчиками.
Ставим StackGres и включение балансировщика
minikube addons enable metallb
minikube tunnelhelm install stackgres-operator stackgres-charts/stackgres-operator\ --namespace stackgres-operator \
--create-namespaceЗадаем CRD для реплицирующего кластера
# replication.yaml
apiVersion: stackgres.io/v1
kind: SGCluster
metadata:
name: cluster
spec:
instances: 3 # 1 primary + 2 replicas
postgres:
version: "15"
pods:
persistentVolume:
size: "1Gi"
profile: development
postgresServices:
primary:
type: LoadBalancer
replicas:
type: LoadBalancerПрименяем CRD
kubectl apply -f ./replication.yaml
kubectl get pods -wПолучаем учетные данные
PG_PASSWORD=$(kubectl -n default get secret cluster --template '{{ printf "%s" (index .data "superuser-password" | base64decode) }}')
echo "The superuser password is: $PG_PASSWORD"Смотрим, что получилось
kubectl exec -it cluster-0 -c patroni -- patronictl listВыключаем лидера
kubectl exec -it cluster-0 -c patroni -- patronictl listСмотрим, кто теперь главный
Patroni уже давно должен был избрать нового лидера.
kubectl exec -it cluster-1 -c patroni -- patronictl listСообщайте что-либо только основным адресатам
PRIMARY=$(kubectl exec -it cluster-1 -c patroni -- patronictl list | grep Leader | awk '{print $2}')
kubectl exec -it $PRIMARY -c patroni -- psql -U postgres -c "CREATE TABLE replication_test_table (id SERIAL PRIMARY KEY, data TEXT);"
kubectl exec -it $PRIMARY -c patroni -- psql -U postgres -c "INSERT INTO replication_test_table (data) VALUES ('Spread the word about our lord savior PostgreSQL!');"Лидер оповещает свои реплики
kubectl exec -it cluster-0 -c patroni -- psql -U postgres -c "SELECT * FROM replication_test_table;"
kubectl exec -it cluster-1 -c patroni -- psql -U postgres -c "SELECT * FROM replication_test_table;"
kubectl exec -it cluster-2 -c patroni -- psql -U postgres -c "SELECT * FROM replication_test_table;"Как видите, информация распространилась очень быстро. Это стало возможным благодаря тому, что StackGres использует Patroni для координации всего процесса копирования.
Парцирование (Разделение)
Разделение данных (в нашем случае таблицы) на более мелкие, управляемые части. Это делается в рамках одного экземпляра базы данных. Postgres поддерживает это по умолчанию. Разделение определяется на уровне определения данных, и наличие нескольких реплик делает его высокодоступным. Лучше всего подходит для временных рядов, журналов или сегментации по регионам.
Типы разделения
Разделение по диапазонам — данные разделяются на основе диапазонов значений (например, диапазонов дат).
Разделение по спискам — разделение на основе списка значений (например, регионов или категорий).
Хэш-разделение — данные распределяются с использованием хэш-функции (например, MOD(user_id, 4)).
Приведенный ниже код создает таблицу orders и наследует от нее три таблицы, используя иерархическое разделение по диапазонам, спискам и хешу. Таблица orders разделяется по годам, год далее разделяется на регионы, а регион, наконец, разделяется по хешу.
Обратите внимание, что только разделы, созданные на основе хеширования, гарантируют, что все разделы будут одинакового размера.
Настройка StackGres и включение балансировщика
helm install stackgres-operator stackgres-charts/stackgres-operator \
--namespace stackgres-operator \
--create-namespace
minikube addons enable metallb
minikube tunnelПолучаем учетные данные
PG_PASSWORD=$(kubectl -n default get secret cluster --template '{{ printf "%s" (index .data "superuser-password" | base64decode) }}')
echo "The superuser password is: $PG_PASSWORD"Теперь ваша база данных должна быть доступна по адресу:
postgresql://postgres:<password>:localhost:5432
Теперь откройте редактор SQL, например pgAdmin , и выполните следующую команду.
-- Parent table
CREATE TABLE orders (
order_id INT,
customer_id INT,
order_date DATE,
region TEXT,
amount INT,
PRIMARY KEY (order_id, order_date, region, customer_id)
) PARTITION BY RANGE (order_date);
-- Range: Year 2024
CREATE TABLE orders_2024 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01')
PARTITION BY LIST (region);
-- Range: Year 2025
CREATE TABLE orders_2025 PARTITION OF orders
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01')
PARTITION BY LIST (region);
-- 2024 - US region
CREATE TABLE orders_2024_us PARTITION OF orders_2024
FOR VALUES IN ('US')
PARTITION BY HASH (customer_id);
-- 2024 - EU region
CREATE TABLE orders_2024_eu PARTITION OF orders_2024
FOR VALUES IN ('EU')
PARTITION BY HASH (customer_id);
-- 2024 - US - Hash partitions
CREATE TABLE orders_2024_us_0 PARTITION OF orders_2024_us FOR VALUES WITH (MODULUS 2, REMAINDER 0);
CREATE TABLE orders_2024_us_1 PARTITION OF orders_2024_us FOR VALUES WITH (MODULUS 2, REMAINDER 1);
-- 2024 - EU - Hash partitions
CREATE TABLE orders_2024_eu_0 PARTITION OF orders_2024_eu FOR VALUES WITH (MODULUS 2, REMAINDER 0);
CREATE TABLE orders_2024_eu_1 PARTITION OF orders_2024_eu FOR VALUES WITH (MODULUS 2, REMAINDER 1);Наполняем синтетическими данных
-- Generate 1000 random orders
INSERT INTO orders (order_id, customer_id, order_date, region, amount)
SELECT
-- Generate order IDs between 1000 and 9999
1000 + floor(random() * 9000)::int AS order_id,
-- Generate customer IDs between 1000 and 9999
1000 + floor(random() * 9000)::int AS customer_id,
-- Generate dates in 2024 (to fit the 2024 partition)
DATE '2024-01-01' + (floor(random() * 366)::int * INTERVAL '1 day') AS order_date,
-- Randomly select region
(ARRAY['US', 'EU'])[1 + floor(random() * 2)::int] AS region,
-- Generate random amounts between 10 and 1000
10 + floor(random() * 990)::int AS amount
FROM
generate_series(1, 1000) AS i; -- 1k rowsSELECT * FROM orders
WHERE order_date = '2024-06-10'
AND region = 'US'Для запроса информации о заказах вам не нужно знать номер раздела.
Шардирование с репликацией
Шардинг разделяет большую базу данных на небольшие части, называемые шардами. Каждый шард затем распределяется между несколькими серверами чтобы наша база данных могла продолжать функционировать, даже если мы потеряем несколько машин. Маршрутизация запросов к нужному серверу осуществляется координатором, и, как и в примере с репликацией, балансировка нагрузки внутри шарда будет выполняться с помощью pg-pool.
Типы шардинга
Построчный подход: Представьте, что вы делите очень толстую книгу на множество томов (фрагментов) и создаете новые тома только для отслеживания содержания (координатора). Представьте таблицу, схема которой проста, но количество строк и операций записи резко возросло. При таком методе операции чтения/записи для каждого фрагмента могут масштабироваться по мере необходимости.
Подход на основе схемы: Как и в прошлый раз, мы снова делим книгу, но на этот раз берем несколько связанных глав и превращаем их в книгу по подтемам. Представьте, как очень толстый учебник по физике можно разделить на оптику, термодинамику и квантовую механику. Представьте таблицу с большим количеством столбцов, но вам не нужны все столбцы каждый раз при выполнении запроса. Поэтому вы делите таблицу на фрагменты таким образом, чтобы связанные столбцы располагались вместе.
Обратите внимание на отказоустойчивость этой архитектуры: у нас есть множество реплик не только для шардов, но и для координатора. Пока у нас есть как минимум 3 машины для работы нашего шардированного кластера, отказ одной машины не приведет к остановке нашей базы данных.
Настройка StackGres и включение балансировщика
helm install stackgres-operator stackgres-charts/stackgres-operator \
--namespace stackgres-operator \
--create-namespace
minikube addons enable metallb
minikube tunnelЗадаем CRD для шардированного кластера
# shard.yaml
apiVersion: stackgres.io/v1alpha1
kind: SGShardedCluster
metadata:
name: cluster
spec:
type: citus
database: mydatabase
postgres:
version: 'latest'
coordinator:
instances: 2 # Number of coordinator instances
pods:
persistentVolume:
size: '1Gi'
shards:
clusters: 3 # Number of shards
instancesPerCluster: 3 # 1 primary and 2 replicas
pods:
persistentVolume:
size: '1Gi'
postgresServices:
coordinator:
primary:
type: LoadBalancer
profile: developmentПрименяем Citus CRD
kubectl apply -f ./shard.yamlПолучаем учетные данные
PG_PASSWORD=$(kubectl -n default get secret cluster --template '{{ printf "%s" (index .data "superuser-password" | base64decode) }}')
echo "The superuser password is: $PG_PASSWORD"Ваша база данных теперь доступно по адресу
postgresql://postgres:<password>:localhost:5432
Теперь откройте приложение, например, редактор SQL, такое как pgAdmin, и выполните следующую команду:
Создайте некоторую распределенную таблицу
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name TEXT
);
SELECT create_distributed_table('users', 'id');
CREATE TABLE orders (
id BIGINT,
user_id BIGINT,
product_id BIGINT,
amount INTEGER,
PRIMARY KEY (user_id, id)
);
SELECT create_distributed_table('orders', 'user_id');
CREATE TABLE products (
id BIGINT PRIMARY KEY,
name TEXT,
price NUMERIC
);
SELECT create_reference_table('products');Вставляем некоторые данные
INSERT INTO users (id, name) VALUES
(1, 'Alice'),
(2, 'Bob'),
(3, 'Charlie');
INSERT INTO orders (id, user_id, product_id, amount) VALUES
(1, 1, 1, 2),
(2, 1, 2, 3),
(3, 2, 1, 1),
(4, 3, 3, 5);
INSERT INTO products (id, name, price) VALUES
(1, 'Product A', 10.00),
(2, 'Product B', 20.00),
(3, 'Product C', 30.00);Смотрим как распределились шарды
SELECT * FROM citus_shards
WHERE table_name = 'orders'::regclass;Определите, какой узел является хостом какого сегмента.
SELECT
s.shardid,
n.nodename,
n.nodeport
FROM pg_dist_shard s
JOIN pg_dist_shard_placement p ON s.shardid = p.shardid
JOIN pg_dist_node n ON p.nodename = n.nodename
WHERE s.logicalrelid = 'orders'::regclass;Найдите строку, содержащую определенный текст.
SELECT get_shard_id_for_distribution_column('orders', 1);Присоединяйтесь к распределенной системе (расположенной в одном месте)
SELECT
o.id AS order_id,
u.name AS customer,
o.amount
FROM orders o
JOIN users u ON o.user_id = u.id;Это эффективно, потому что заказы и пользователи сегментируются с использованием одного и того же ключа (user_id и id).
Присоединиться к распределенной ссылке
SELECT
o.id AS order_id,
u.name AS customer,
p.name AS product,
o.amount
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id;Это хорошо работает, потому что данные о продуктах реплицируются на всех узлах.


