Лабораторная №3 — ручной сценарий команд из demo.sh¶
Запускать из корня проекта
lab3. Команды даны в том порядке, в котором их удобно вводить руками.
Подготовка окружения¶
cd /Users/ramil/Downloads/рсхд_итмо/lab3
# Подключить ограниченный persistent-диск для pg1
bash ./scripts/storage/ensure-pg1-disk.sh
# Поднять стек
docker compose up -d
# Настроить PCP-доступ внутри pgpool
docker exec pgpool bash -c "echo 'localhost:9898:postgres:postgres' > /root/.pcppass && chmod 600 /root/.pcppass"
Этап 1. Конфигурация¶
# Показать контейнеры
docker compose ps
# Показать узлы pgpool
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SHOW pool_nodes;\""
# Показать репликацию на pg1
docker exec pg1 bash -c "psql -U postgres -d testdb -c \"SELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, sync_state FROM pg_stat_replication;\""
# Показать роли pg1 и pg2 напрямую
docker exec pg1 bash -c "psql -U postgres -d testdb -c \"SELECT pg_is_in_recovery() AS is_standby;\""
docker exec pg2 bash -c "psql -U postgres -d testdb -c \"SELECT pg_is_in_recovery() AS is_standby;\""
Балансировка нагрузки¶
# До
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SHOW pool_nodes;\""
# 12 SELECT через pgpool
for i in $(seq 1 12); do
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT d.id, LEFT(d.data, 60) AS data_preview, a.action, a.actor FROM test_data d JOIN test_audit a ON a.test_data_id = d.id ORDER BY a.id DESC LIMIT 1;\""
done
# После
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SHOW pool_nodes;\""
Этап 2.1. Подготовка¶
# Показать структуру двух таблиц
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_schema = 'public' AND table_name IN ('test_data', 'test_audit') ORDER BY table_name, ordinal_position;\""
# Показать начальные строки test_data
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT id, CASE WHEN length(data) > 80 THEN left(data, 80) || '...' ELSE data END AS data, created_at, source FROM test_data ORDER BY id;\""
# Показать начальные строки test_audit
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT id, test_data_id, action, CASE WHEN length(details) > 80 THEN left(details, 80) || '...' ELSE details END AS details, actor, created_at FROM test_audit ORDER BY id;\""
Фоновые клиентские процессы через pgpool¶
# Клиент 1: 40 итераций, интервал 3 с
docker exec pgpool bash -c "
for i in \$(seq 1 40); do
psql -q -h localhost -p 9999 -U postgres -d testdb \
-c \"SELECT COUNT(*) AS test_data_rows_before_insert FROM test_data;
SELECT COUNT(*) AS test_audit_rows_before_insert FROM test_audit;
BEGIN;
WITH new_row AS (
INSERT INTO test_data(data,source)
VALUES('bg_write_\$i','client_1')
RETURNING id, data, source
)
INSERT INTO test_audit(test_data_id, action, details, actor)
SELECT id, 'bg_insert', data, source
FROM new_row;
COMMIT;\" >/dev/null 2>&1
sleep 3
done
" &
# Клиент 2: 25 итераций, интервал 5 с
docker exec pgpool bash -c "
for i in \$(seq 1 25); do
psql -q -h localhost -p 9999 -U postgres -d testdb \
-c \"SELECT COUNT(*) AS test_data_rows_before_insert FROM test_data;
SELECT COUNT(*) AS test_audit_rows_before_insert FROM test_audit;
BEGIN;
WITH new_row AS (
INSERT INTO test_data(data,source)
VALUES('bg_write_\$i','client_2')
RETURNING id, data, source
)
INSERT INTO test_audit(test_data_id, action, details, actor)
SELECT id, 'bg_insert', data, source
FROM new_row;
COMMIT;\" >/dev/null 2>&1
sleep 5
done
" &
# Клиент 3: 60 SELECT COUNT из test_data, интервал 2 с
docker exec pgpool bash -c "
for i in \$(seq 1 60); do
psql -q -h localhost -p 9999 -U postgres -d testdb \
-c 'SELECT COUNT(*) FROM test_data;' >/dev/null 2>&1
sleep 2
done
" &
# Клиент 4: 60 SELECT COUNT из test_audit, интервал 2 с
docker exec pgpool bash -c "
for i in \$(seq 1 60); do
psql -q -h localhost -p 9999 -U postgres -d testdb \
-c 'SELECT COUNT(*) FROM test_audit;' >/dev/null 2>&1
sleep 2
done
" &
# Проверить, что строки прибавляются
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT COUNT(*) FROM test_data;\""
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT COUNT(*) FROM test_audit;\""
# Показать активность в pg_stat_activity
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT pid, usename, client_addr, state, LEFT(query, 55) AS current_query FROM pg_stat_activity WHERE datname='testdb' AND pid != pg_backend_pid() ORDER BY query_start;\""
# Ручная транзакция записи через pgpool в две таблицы
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"BEGIN;
WITH new_row AS (
INSERT INTO test_data(data, source)
VALUES ('manual_write_demo', 'operator')
RETURNING id, data, source
),
audit_row AS (
INSERT INTO test_audit(test_data_id, action, details, actor)
SELECT id, 'manual_write', data, source
FROM new_row
RETURNING id AS audit_id, test_data_id, actor
)
SELECT
(SELECT id FROM new_row) AS test_data_id,
(SELECT audit_id FROM audit_row) AS audit_id,
(SELECT actor FROM audit_row) AS actor;
COMMIT;\""
# Показать связанные строки из двух таблиц
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT d.id, CASE WHEN length(d.data) > 80 THEN left(d.data, 80) || '...' ELSE d.data END AS data, d.source, a.id AS audit_id, a.action, a.actor, a.created_at FROM test_data d JOIN test_audit a ON a.test_data_id = d.id ORDER BY d.id DESC, a.id DESC LIMIT 5;\""
# Проверить репликацию на pg2
docker exec pg2 bash -c "psql -U postgres -d testdb -c \"SELECT (SELECT COUNT(*) FROM test_data) AS data_rows_on_pg2, (SELECT COUNT(*) FROM test_audit) AS audit_rows_on_pg2;\""
Этап 2.2. Симуляция сбоя¶
# Проверить ограниченный диск pg1 на хосте
df -h "/tmp/lab3-pg1-data"
hdiutil info | grep -F "/tmp/lab3-pg1-data"
du -sh "/tmp/lab3-pg1-data/"
# Заполнить диск мусорным файлом на pg1
docker exec pg1 bash -c "dd if=/dev/zero of=/var/lib/postgresql/data/junk bs=1M 2>&1; echo '[dd завершён]'"
# Проверить статус контейнера
docker inspect -f '{{.State.Status}}' pg1
# Если pg1 ещё жив — форсировать запись / WAL / checkpoint
df -h "/tmp/lab3-pg1-data"
docker exec pg1 bash -lc "psql -U postgres -d testdb -v ON_ERROR_STOP=0 \
-c \"INSERT INTO test_data(data, source) VALUES (repeat('X', 1048576), 'enospc_trigger');\" \
-c \"SELECT pg_switch_wal();\" \
-c \"CHECKPOINT;\""
# Повторять до падения pg1
# (каждая итерация может занимать несколько секунд из-за ENOSPC — PostgreSQL
# пытается записать WAL на полный диск и зависает в retry-цикле)
for i in $(seq 1 20); do
docker exec pg1 bash -lc "psql -U postgres -d testdb -v ON_ERROR_STOP=0 \
-c \"INSERT INTO test_data(data, source) VALUES (repeat('X', 1048576), 'enospc_trigger');\" \
-c \"SELECT pg_switch_wal();\" \
-c \"CHECKPOINT;\"" >/dev/null 2>&1 || true
docker inspect -f '{{.State.Status}}' pg1
sleep 1
done
Этап 2.3. Обработка сбоя¶
# Показать ошибки в логах pg1
docker logs pg1 2>&1 | grep -iE "(PANIC|FATAL|ERROR|no space|enospc|disk full|cannot|failed)" | tail -20
docker logs pg1 2>&1 | tail -20
# Ждать автоматический failover через pgpool
for i in $(seq 1 45); do
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SHOW pool_nodes;\"" 2>/dev/null | grep " 0 " | grep -c "down" || true
docker exec pg2 bash -c "psql -U postgres -d testdb -c \"SELECT pg_is_in_recovery();\"" 2>/dev/null
docker inspect -f '{{.State.Status}}' pg1 2>/dev/null
sleep 1
done
# Если авто-failover не произошёл — выполнить вручную
docker exec pg2 bash -c "psql -U postgres -d testdb -c \"SELECT pg_promote();\""
docker exec pgpool bash -c "pcp_detach_node -h localhost -p 9898 -U postgres -n 0"
# Показать состояние после failover
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SHOW pool_nodes;\""
docker exec pg2 bash -c "psql -U postgres -d testdb -c \"SELECT pg_is_in_recovery() AS is_standby, now() - pg_postmaster_start_time() AS uptime_since_failover;\""
# Показать данные после failover
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT d.id, CASE WHEN length(d.data) > 80 THEN left(d.data, 80) || '...' ELSE d.data END AS data, d.source, a.id AS audit_id, a.action, a.actor FROM test_data d JOIN test_audit a ON a.test_data_id = d.id ORDER BY d.id DESC, a.id DESC LIMIT 5;\""
# Две записи через новый primary pg2
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"BEGIN;
WITH new_row AS (
INSERT INTO test_data(data, source)
VALUES ('after_failover_1', 'pg2-primary')
RETURNING id, data, source
),
audit_row AS (
INSERT INTO test_audit(test_data_id, action, details, actor)
SELECT id, 'after_failover_write', data, source
FROM new_row
RETURNING id AS audit_id, test_data_id
)
SELECT
(SELECT id FROM new_row) AS test_data_id,
(SELECT audit_id FROM audit_row) AS audit_id;
COMMIT;\""
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"BEGIN;
WITH new_row AS (
INSERT INTO test_data(data, source)
VALUES ('after_failover_2', 'pg2-primary')
RETURNING id, data, source
),
audit_row AS (
INSERT INTO test_audit(test_data_id, action, details, actor)
SELECT id, 'after_failover_write', data, source
FROM new_row
RETURNING id AS audit_id, test_data_id
)
SELECT
(SELECT id FROM new_row) AS test_data_id,
(SELECT audit_id FROM audit_row) AS audit_id;
COMMIT;\""
# Итоговое состояние после failover
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT (SELECT COUNT(*) FROM test_data) AS total_data_rows, (SELECT COUNT(*) FROM test_audit) AS total_audit_rows, (SELECT MAX(created_at) FROM test_data) AS last_data_write, (SELECT MAX(created_at) FROM test_audit) AS last_audit_write;\""
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT COUNT(*) AS total_data_rows, MAX(id) AS max_data_id, MAX(id) - COUNT(*) AS skipped_ids_due_to_failed_inserts FROM test_data;\""
# Остановить фоновые процессы на хосте
# Подставить реальные PID из jobs / ps
# kill 12345 12346 12347 12348
Восстановление pg1¶
# Остановить pg1 и удалить junk
docker stop pg1
docker run --rm -v "/tmp/lab3-pg1-data:/data" --entrypoint "" postgres:16 bash -c \
"rm -f /data/junk && ls -lah /data && df -h /data"
# Очистить PGDATA pg1
docker run --rm -v "/tmp/lab3-pg1-data:/data" --entrypoint "" postgres:16 bash -c \
"rm -rf /data/pgdata && mkdir -p /data/pgdata && chown -R postgres:postgres /data/pgdata"
# Скопировать pg2 → pg1 через pg_basebackup
docker run --rm --network "lab3_pgnet" \
-v "/tmp/lab3-pg1-data:/var/lib/postgresql/data" \
--user postgres --entrypoint "" postgres:16 \
pg_basebackup -h pg2 -p 5432 -U replicator \
-D /var/lib/postgresql/data/pgdata -Fp -Xs -P -R
# Гарантировать standby.signal и primary_conninfo на pg1
docker run --rm -v "/tmp/lab3-pg1-data:/data" --entrypoint "" postgres:16 bash -c \
"mkdir -p /data/pgdata && touch /data/pgdata/standby.signal && \
if ! grep -q primary_conninfo /data/pgdata/postgresql.auto.conf 2>/dev/null; then \
printf \"%s\n\" \"primary_conninfo = 'host=pg2 port=5432 user=replicator application_name=pg1'\" \
>> /data/pgdata/postgresql.auto.conf; \
fi && chown postgres:postgres /data/pgdata/standby.signal /data/pgdata/postgresql.auto.conf"
# Проверить подготовку pg1 как standby
docker run --rm -v "/tmp/lab3-pg1-data:/data" --entrypoint "" postgres:16 bash -c \
"ls -l /data/pgdata/standby.signal && grep primary_conninfo /data/pgdata/postgresql.auto.conf"
# Запустить pg1 и дождаться готовности
docker start pg1
for i in $(seq 1 30); do
docker exec pg1 pg_isready -U postgres && break
sleep 2
done
for i in $(seq 1 15); do
docker exec pg1 psql -U postgres -d testdb -Atqc "SELECT 1" && break
sleep 2
done
# Показать роль pg1 и репликацию от pg2 к pg1
docker exec pg1 bash -c "psql -U postgres -d testdb -c \"SELECT pg_is_in_recovery() AS is_standby, pg_postmaster_start_time() AS started;\""
docker exec pg2 bash -c "psql -U postgres -d testdb -c \"SELECT client_addr, state, sent_lsn, replay_lsn, sync_state FROM pg_stat_replication;\""
Возврат исходной конфигурации: pg1 primary, pg2 standby¶
# Остановить pgpool и pg2
docker stop pgpool
docker stop pg2
# Промоутировать pg1 обратно в primary
docker exec pg1 bash -c "psql -U postgres -d testdb -c \"SELECT pg_promote(true, 60);\""
docker exec pg1 bash -c "psql -U postgres -d testdb -c \"SELECT pg_is_in_recovery() AS is_standby, now() - pg_postmaster_start_time() AS uptime_after_promote;\""
# Очистить PGDATA pg2
docker run --rm -v "lab3_pg2_data:/data" --entrypoint "" postgres:16 bash -c \
"rm -rf /data/pgdata && mkdir -p /data/pgdata && chown -R postgres:postgres /data/pgdata"
# Скопировать pg1 → pg2 через pg_basebackup
docker run --rm --network "lab3_pgnet" \
-v "lab3_pg2_data:/var/lib/postgresql/data" \
--user postgres --entrypoint "" postgres:16 \
pg_basebackup -h pg1 -p 5432 -U replicator \
-D /var/lib/postgresql/data/pgdata -Fp -Xs -P -R
# Запустить pg2 как standby
docker start pg2
for i in $(seq 1 30); do
docker exec pg2 pg_isready -U postgres && break
sleep 2
done
# Проверить роли напрямую
docker exec pg1 bash -c "psql -U postgres -d testdb -c \"SELECT pg_is_in_recovery() AS is_standby;\""
docker exec pg2 bash -c "psql -U postgres -d testdb -c \"SELECT pg_is_in_recovery() AS is_standby;\""
# Дождаться подключения standby к pg1
for i in $(seq 1 20); do
docker exec pg1 psql -U postgres -d testdb -Atqc "SELECT COUNT(*) FROM pg_stat_replication;" | grep -q '^[1-9]' && break
sleep 2
done
# Показать репликацию pg1 → pg2
docker exec pg1 bash -c "psql -U postgres -d testdb -c \"SELECT application_name, state, sync_state FROM pg_stat_replication;\""
docker exec pg2 bash -c "psql -U postgres -d testdb -c \"SELECT status, conninfo FROM pg_stat_wal_receiver;\""
Финальная проверка через pgpool¶
# Вернуть pgpool
docker start pgpool
docker exec pgpool bash -c "echo 'localhost:9898:postgres:postgres' > /root/.pcppass && chmod 600 /root/.pcppass"
# Показать pool_nodes
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SHOW pool_nodes;\""
# Показать финальные данные
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT d.id, CASE WHEN length(d.data) > 80 THEN left(d.data, 80) || '...' ELSE d.data END AS data, d.source, a.id AS audit_id, a.action, a.actor, a.created_at FROM test_data d JOIN test_audit a ON a.test_data_id = d.id ORDER BY d.id DESC, a.id DESC LIMIT 10;\""
# Финальная сверка строк и sequence
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT (SELECT COUNT(*) FROM test_data) AS total_data_rows, (SELECT COUNT(*) FROM test_audit) AS total_audit_rows, (SELECT MAX(id) FROM test_data) AS max_data_id, (SELECT MAX(id) FROM test_data) - (SELECT COUNT(*) FROM test_data) AS skipped_data_ids_due_to_failed_inserts;\""
# Финальная запись через восстановленный primary pg1
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"BEGIN;
WITH new_row AS (
INSERT INTO test_data(data, source)
VALUES ('recovery_complete', 'pg1-restored-primary')
RETURNING id, data, source
),
audit_row AS (
INSERT INTO test_audit(test_data_id, action, details, actor)
SELECT id, 'recovery_complete', data, source
FROM new_row
RETURNING id AS audit_id, test_data_id
)
SELECT
(SELECT id FROM new_row) AS test_data_id,
(SELECT audit_id FROM audit_row) AS audit_id;
COMMIT;\""
# 12 SELECT для финальной балансировки
for i in $(seq 1 12); do
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SELECT d.id, LEFT(d.data, 60) AS data_preview, a.action, a.actor FROM test_data d JOIN test_audit a ON a.test_data_id = d.id ORDER BY a.id DESC LIMIT 1;\"" >/dev/null
done
# Финальный pool_nodes
docker exec pgpool bash -c "psql -h localhost -p 9999 -U postgres -d testdb -c \"SHOW pool_nodes;\""
# Финальные counts напрямую на pg1 и pg2
docker exec pg1 bash -c "psql -U postgres -d testdb -c \"SELECT (SELECT COUNT(*) FROM test_data) AS data_rows, (SELECT COUNT(*) FROM test_audit) AS audit_rows;\""
docker exec pg2 bash -c "psql -U postgres -d testdb -c \"SELECT (SELECT COUNT(*) FROM test_data) AS data_rows, (SELECT COUNT(*) FROM test_audit) AS audit_rows;\""