You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Решаем две задачи: с одной стороны отказоустойчивый и масштабируемый класстер на базе cockroachdb, а с другой, авто генерируемая админка к его табличкам
по скольку у нас тут не совсем большая дата, но и сервер малюсенький, результат интересный, после добавления индекса запрос выполняется менее 8 сек
любопытный факт mysql из коробки дал приблизительно такой же результат как и затюненная постргря
Export
Экспорт делает csv где то по 255мб каждый, соотв для 10гб нам понадоибиться первых 40 файлов, а остальные можно удалять
Команда gsutil du gs://otus-hw10 распечатает размер и название файла, осталось сложить это в файл и написать скрипт который посчитает сколько точно файлов нам понадобиться
примечание: поленился написать по нормальному в скрипте ошибка, под звездочку попадают файлы 1хх и 2хх, как следствие удаляется сразу 40, 140 и 240 из за чего под конец насыпал варнингов, но не суть, файлы почистили
дальше нам эти файлы нужно объединить в один, но при этом есть одна сложность - в csv зашит заголовок от которого нам надо избавиться, делаем следующий финт ушами:
sudo mysql -vv -u root -D demo -e "SELECT payment_type, round(sum(tips)/sum(trip_total)*100, 0) + 0 as tips_percent, count(*) as cFROM taxi_tripsgroup by payment_typeorder by 3"# 11 rows in set (3 min 41.71 sec)
Любопытный факт, mysql из коробки дал такой же результат как и затюненная постгря
-- Реализовать прямое соединение двух или более таблиц-- top 5 городовselecta.aircraft_code, dep.city departure, arr.city arrive, count(*)
frombookings.aircrafts a
joinbookings.flights f ona.aircraft_code=f.aircraft_codejoinbookings.airports dep ondep.airport_code=f.departure_airportjoinbookings.airports arr onarr.airport_code=f.departure_airportgroup by1, 2, 3order by4desclimit5-- Реализовать левостороннее (или правостороннее) соединение двух или более таблиц-- заполняемость при перелетахselectf.flight_id, count(tf.*) /count(s.*) *100frombookings.flights f
joinbookings.aircrafts a onf.aircraft_code=a.aircraft_codejoinbookings.seats s ona.aircraft_code=s.aircraft_codeleft joinbookings.ticket_flights tf onf.flight_id=tf.flight_idgroup by1-- Реализовать кросс соединение двух или более таблиц-- Реализовать полное соединение двух или более таблиц-- самые дальние друг от друга аэеропортыselecta.city, b.city, (point(a.longitude,a.latitude) <@>point(b.longitude,b.latitude)) as distance frombookings.airports a
cross joinbookings.airports b
order by distance desclimit1
Metrics
Топ самых быстро меняемых табличек которые скорее всего потребуют отдельной настройки для автовакуума
select n_mod_since_analyze + n_ins_since_vacuum / EXTRACT(EPOCH FROM (now() - last_autovacuum)), relname from pg_stat_user_tables
where last_autovacuum is not nullorder by1desc
pid'ы которые скорее всего в скором времени завесят базу
select
EXTRACT(EPOCH FROM (now() - state_change)) as idle_seconds,
pid
from pg_stat_activity
where state ='idle'and EXTRACT(EPOCH FROM (now() - state_change)) >60
какая из баз более нагруженна на запись, а какая на чтение
select
(tup_inserted + tup_deleted + tup_updated) / EXTRACT(EPOCH FROM (now() - stats_reset)) writes_per_second,
tup_returned / EXTRACT(EPOCH FROM (now() - stats_reset)) reads_per_second,
datname
from pg_stat_database
Из всего что перепробовал, мне больше всего нравиться старый добрый inherits, с ним получилось поразвлекаться, в последнем примере я делаю не только партиционирование но еще и денормализацию (партиции в двух разных разрезах, да это х2 данных, но при этом более эффективная работа с ними), так же с таким подходом в любой момент времени можно переиграть и переделать партиции без потенциальной потери данных (в новых вариантах нужен detach и на время работ вставка данных будет невозможна)
✅ Adding new partitions
❌ Create partitions with trigger
✅ Generating partitions programmatically
❌ pg_partman
🎉 Semi-auto partitioning
🤔 Good old partitioning
💡 Denormalized partitions
✅ Adding new partitions
Пример как добавлять новые партиции и переезжать туда данные
Примечания:
важно в процессе создания новой партиции, вставка в default будет не возможна т.к. мы вынужденны ее отключить, из-за чего будет потеря новых данных
то как быстро мы создадим новую партицию, зависит от того сколько данных уже успело накопиться в партиции по умолчанию (время необходимое на их перенос в новую партицию)
DROPTABLE IF EXISTS temperatures;
CREATETABLEtemperatures (
id SERIAL,
dateTIMESTAMPNOT NULL,
continent VARCHARNOT NULLCHECK (continent in ('Africa','Antarctica','Asia','Europe','North America','South America','Australia')),
value INT
) PARTITION BY LIST (continent);
CREATETABLEtemperatures_africa PARTITION OF temperatures FOR VALUESIN ('Africa');
CREATETABLEtemperatures_antarctica PARTITION OF temperatures FOR VALUESIN ('Antarctica');
-- this one will catch everything else except africa and antarctica defined aboveCREATETABLEtemperatures_default PARTITION OF temperatures DEFAULT;
INSERT INTO temperatures (date, continent, value) VALUES
(now(), 'Africa', 1),
(now(), 'Antarctica', 2),
(now(), 'Asia', 3); -- this one should land in 'temperatures_default' tableSELECT*FROM temperatures; -- all 3 rowsSELECT*FROM temperatures_africa; -- 1 rowSELECT*FROM temperatures_antarctica; -- 1 rowSELECT*FROM temperatures_default; -- 1 row-- create new partition for asiaCREATETABLEtemperatures_asia PARTITION OF temperatures FOR VALUESIN ('Asia');
-- ERROR: updated partition constraint for default partition "temperatures_default" would be violated by some rowALTERTABLE temperatures DETACH PARTITION temperatures_default; -- WARNING: while we are doing this inserts for everything except africa and antarctica wont workINSERT INTO temperatures (date, continent, value) VALUES (now(), 'Asia', 4); -- ERROR: no partition of relation "temperatures" found for row Detail: Partition key of the failing row contains (continent) = (Asia).CREATETABLEtemperatures_asia PARTITION OF temperatures FOR VALUESIN ('Asia'); -- create partitionINSERT INTO temperatures_asia SELECT*FROM temperatures_default WHERE continent ='Asia'; -- sync dataDELETEFROM temperatures_default WHERE continent ='Asia'; -- cleanupALTERTABLE temperatures ATTACH PARTITION temperatures_default DEFAULT; -- attach default partition backINSERT INTO temperatures (date, continent, value) VALUES (now(), 'Asia', 4);
SELECT*FROM temperatures; -- 4 rowsSELECT*FROM temperatures_default; -- 0 rowsSELECT*FROM temperatures_asia; -- 2 rows
❌ Create partitions with trigger
К сожалению этот вариант не сработал
Идея была такой:
записи попадают в default партицию
вешаем на нее on before insert trigger
поскольку запись попала сюда, партиции нет
создаем ее
вставляем в нее строку
на производительность нагрузка минимальная т.к. все это происходит только при первой вставке, все последующие пойдут в новую партицию
технически можно выкрутиться и вставлять записи через хранимку в которой сделать все это, но в этом уже нет особого смысла и получиться то же самое что отслеживать со стороны приложения
DROPTABLE IF EXISTS temperatures;
CREATETABLEtemperatures (
id SERIAL,
dateTIMESTAMPNOT NULL,
continent VARCHARNOT NULLCHECK (continent in ('Africa','Antarctica','Asia','Europe','North America','South America','Australia')),
value INT
) PARTITION BY RANGE (date);
CREATETABLEtemperatures_default PARTITION OF temperatures DEFAULT;
-- this one will be used as a on before insert trigger to default partition-- if record lands here it means that we do not have partition yet-- we are going to create it, so all next inserts will land into new partition-- because of that it wont affect performance - we are running just once per partitionCREATE OR REPLACEFUNCTIONinsert_row() RETURNS TRIGGER LANGUAGE plpgsql AS
$$
DECLARE
year INT;
slug TEXT;
sql TEXT;
BEGIN
slug := replace(lower(NEW.continent), '', '_'); -- 'South America' -> 'south_america'
year := extract(year fromNEW.date); -- '2021-03-02 12:32:49' -> 2021-- Create partitionSELECT format('CREATE TABLE IF NOT EXISTS temperatures_%s_%s PARTITION OF temperatures FOR VALUES from (''%s-01-01'') to (''%s-01-01'');', slug, year, year, year +1) INTO sql;
EXECUTE sql;
-- Insert data into new partitionSELECT format('INSERT INTO temperatures_%s_%s values ($1.*)', slug, year) into sql;
EXECUTE sql USING NEW;
RETURN NEW;
END;
$$;
CREATETRIGGERbefore_insert_row_trigger
BEFORE INSERT ON temperatures_default
FOR EACH ROW EXECUTE PROCEDURE insert_row();
INSERT INTO temperatures (date, continent, value) VALUES ('2020-01-01 12:00:00', 'Africa', 45);
-- ERROR: cannot CREATE TABLE .. PARTITION OF "temperatures" because it is being used by active queries in this session-- Where: SQL statement "CREATE TABLE IF NOT EXISTS temperatures_africa_2020 PARTITION OF temperatures FOR VALUES from ('2020-01-01') to ('2021-01-01');"
✅ Generating partitions programmatically
В этом примере мы строим следующую структуру:
temperatures
temperatures_asia
temperatures_asia_2020
temperatures_asia_2021
temperatures_asia_default
temperatures_africa
temperatures_africa_2020
temperatures_africa_2021
temperatures_africa_default
...
Поинятное дело табличек таких будет много, а еще им не плохо бы навесить индексы и прочее, делать это руками - unreal, поэтому делаем простенькую хранимку которая нам все это дело сгенерирует
DROPTABLE IF EXISTS temperatures;
CREATETABLEtemperatures (
id SERIAL,
dateTIMESTAMPNOT NULL,
continent VARCHARNOT NULLCHECK (continent in ('Africa','Antarctica','Asia','Europe','North America','South America','Australia')),
value INT
) PARTITION BY LIST (continent);
CREATE OR REPLACEFUNCTIONDemo() RETURNS BOOLEAN LANGUAGE plpgsql AS
$$
DECLARE
continent VARCHAR;
year INT;
slug VARCHAR;
sql TEXT;
dry BOOLEAN= false;
BEGIN
FOR continent IN (SELECT'Africa'AS continent UNIONSELECT'Antarctica'UNIONSELECT'Asia'UNIONSELECT'Europe'UNIONSELECT'North America'UNIONSELECT'South America'UNIONSELECT'Australia') LOOP
raise notice 'Continent: %', continent;
-- Split temperatures by continents
slug = replace(lower(continent), '', '_');
SELECT format('CREATE TABLE IF NOT EXISTS temperatures_%s PARTITION OF temperatures FOR VALUES IN (''%s'') PARTITION BY RANGE(date);', slug, continent) INTO sql;
raise notice 'SQL: %', sql;
IF dry = false THEN
EXECUTE sql;
END IF;
FOR year IN2020..2022 LOOP
raise notice 'Year: %', year;
-- Inner split continents by yearsSELECT format('CREATE TABLE IF NOT EXISTS temperatures_%s_%s PARTITION OF temperatures_%s FOR VALUES from (''%s-01-01'') to (''%s-01-01'');', slug, year, slug, year, year +1) INTO sql;
raise notice 'SQL: %', sql;
IF dry = false THEN
EXECUTE sql;
END IF;
-- House keeping (just for demo purposes we are creating primary key and unique index)SELECT format('ALTER TABLE temperatures_%s_%s ADD PRIMARY KEY (id);', slug, year) INTO sql;
raise notice 'SQL: %', sql;
IF dry = false THEN
EXECUTE sql;
END IF;
SELECT format('CREATE UNIQUE INDEX idx_temperatures_%s_%s_date_continent ON temperatures_%s_%s(date, continent);', slug, year, slug, year) INTO sql;
raise notice 'SQL: %', sql;
IF dry = false THEN
EXECUTE sql;
END IF;
END LOOP;
-- Just in case, default partitions for future yearsSELECT format('CREATE TABLE IF NOT EXISTS temperatures_%s_default PARTITION OF temperatures_%s DEFAULT;', slug, slug) INTO sql;
raise notice 'SQL: %', sql;
IF dry = false THEN
EXECUTE sql;
END IF;
END LOOP;
RETURN true;
END
$$;
-- Initialize tableselect Demo();
-- 58 objectsSELECT relname FROM pg_class WHERE relname ='temperatures'OR relname LIKE'temperatures_%'ORDER BY relname;
-- Insert demo data (3m)INSERT INTO temperatures (date, continent, value)
SELECTtimestamp, continent, floor(random() *100)::int-50FROM generate_series(timestamp'2020-12-01', timestamp'2021-02-01', interval '1 second') AStimestamps(timestamp),
(SELECT'Africa'AS continent UNIONSELECT'Antarctica'UNIONSELECT'Asia'UNIONSELECT'Europe'UNIONSELECT'North America'UNIONSELECT'South America'UNIONSELECT'Australia') AS continents(continent);
-- 37.5M, 2sSELECTcount(*) FROM temperatures;
-- 2.6M, 153msSELECTcount(*) FROM temperatures_europe_2021;
-- Some report, 42sSELECT continent, extract(year fromdate) as year, avg(value) as avg_temperature
FROM temperatures
GROUP BY CUBE(1,2);
-- Some report, 4sSELECT continent, extract(year fromdate) as year, avg(value) as avg_temperature
FROM temperatures_europe
GROUP BY CUBE(1,2);
❌ pg_partman
pg_partman - partition management extension for PostgreSQL
Оказалась бестолковой поделкой, по сути, с тем же у спехом можно по расписанию раз в день создавать партиции.
FROM postgres:13
RUN apt -qq update && apt -qq -y install build-essential postgresql-server-dev-all postgresql-server-dev-13 wget unzip
WORKDIR /partman
RUN wget https://github.com/pgpartman/pg_partman/archive/refs/tags/v4.5.1.zip && unzip v4.5.1.zip
WORKDIR /partman/pg_partman-4.5.1
RUN make install
SELECT*FROM pg_available_extensions WHERE name ='pg_partman';
CREATE EXTENSION pg_partman;
DROPTABLE IF EXISTS temperatures;
CREATETABLEtemperatures (
id SERIAL,
dateTIMESTAMPNOT NULL,
continent VARCHARNOT NULLCHECK (continent in ('Africa','Antarctica','Asia','Europe','North America','South America','Australia')),
value INT
) PARTITION BY RANGE (date);
CREATEINDEXON temperatures (date);
SELECT create_parent('public.temperatures', 'date', 'native', 'daily');
-- 10 tablesSELECT relname FROM pg_stat_user_tables WHERE relname ='temperatures'OR relname LIKE'temperatures_%'ORDER BY relname;
INSERT INTO temperatures (date, continent, value) VALUES ('2021-07-01 12:00:00', 'Europe', 1);
SELECT*FROM temperatures;
SELECT*FROM temperatures_p2021_07_01;
-- trying to insert data from pastINSERT INTO temperatures (date, continent, value) VALUES ('2001-01-01 12:00:00', 'Europe', 2);
SELECT*FROM temperatures;
SELECT*FROM temperatures_default;
Данные вставились в default партицию, т.к. pg_partman не создает партиции на лету, а просто по расписнию 🤔
🎉 Semi-auto partitioning
В чем заключается идея: заводим табличку пустышку и треггер на нее
Внутри триггера проворачиваем трюк с созданием партиции
DROPTABLE IF EXISTS temperatures;
DROPTABLE IF EXISTS temperatures_partitioned;
CREATETABLEtemperatures (
id SERIAL,
dateTIMESTAMPNOT NULL,
continent VARCHARNOT NULLCHECK (continent in ('Africa','Antarctica','Asia','Europe','North America','South America','Australia')),
value INT
);
CREATETABLEtemperatures_partitioned (
id SERIAL,
dateTIMESTAMPNOT NULL,
continent VARCHARNOT NULLCHECK (continent in ('Africa','Antarctica','Asia','Europe','North America','South America','Australia')),
value INT
) PARTITION BY RANGE (date);
CREATE OR REPLACEFUNCTIONinsert_row() RETURNS TRIGGER LANGUAGE plpgsql AS
$$
DECLARE
year INT;
slug TEXT;
sql TEXT;
BEGIN
year := extract(year fromNEW.date); -- '2021-03-02 12:32:49' -> 2021-- Create partitionSELECT format('CREATE TABLE IF NOT EXISTS temperatures_partitioned_%s PARTITION OF temperatures_partitioned FOR VALUES from (''%s-01-01'') to (''%s-01-01'');', year, year, year +1) INTO sql;
EXECUTE sql;
-- Insert data into new partitionSELECT format('INSERT INTO temperatures_partitioned_%s values ($1.*)', year) into sql;
EXECUTE sql USING NEW;
RETURN NEW;
END;
$$;
CREATETRIGGERbefore_insert_row_trigger
BEFORE INSERT ON temperatures
FOR EACH ROW EXECUTE PROCEDURE insert_row();
INSERT INTO temperatures (date, continent, value) VALUES ('2020-01-01 12:00:00', 'Europe', 1);
SELECT relname FROM pg_stat_user_tables WHERE relname like'temperatures%';
SELECT*FROM temperatures; -- 1SELECT*FROM temperatures_partitioned; -- 1SELECT*FROM temperatures_partitioned_2020; -- 1INSERT INTO temperatures (date, continent, value) VALUES ('2021-01-01 12:00:00', 'Europe', 2);
SELECT relname FROM pg_stat_user_tables WHERE relname like'temperatures%';
SELECT*FROM temperatures; -- 2SELECT*FROM temperatures_partitioned; -- 2SELECT*FROM temperatures_partitioned_2020; -- 1SELECT*FROM temperatures_partitioned_2021; -- 1
🤔 Good old partitioning
Чего то мне этот вариант сильно больше всего остального нравиться, т.к. тут можно будет и создвать таблички и в случае чего мигрировать данные
DROPTABLE IF EXISTS temperatures_default;
DROPTABLE IF EXISTS temperatures_2020;
DROPTABLE IF EXISTS temperatures_2021;
DROPTABLE IF EXISTS temperatures;
CREATETABLEtemperatures (
id SERIAL,
dateTIMESTAMPNOT NULL,
continent VARCHARNOT NULLCHECK (continent in ('Africa','Antarctica','Asia','Europe','North America','South America','Australia')),
value INT
);
CREATETABLEtemperatures_default () INHERITS (temperatures);
CREATETABLEtemperatures_2020 (like temperatures including all) INHERITS (temperatures);
ALTERTABLE temperatures_2020 add check (date between date'2020-01-01'anddate'2021-01-01'-1);
CREATETABLEtemperatures_2021 () INHERITS (temperatures);
ALTERTABLE temperatures_2021 add check (date between date'2021-01-01'anddate'2022-02-01'-1);
CREATE OR REPLACEFUNCTIONtemperatures_insert_row()
RETURNS TRIGGER AS $$
BEGIN
if new.date between date'2020-01-01'anddate'2021-01-01'-1 then
INSERT INTO temperatures_2020 VALUES (NEW.*);
elsif new.date between date'2021-01-01'anddate'2022-02-01'-1 then
INSERT INTO temperatures_2021 VALUES (NEW.*);
else
-- raise exception 'this date not in your partitions. add partition';INSERT INTO temperatures_default VALUES (NEW.*);
end if;
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
CREATETRIGGERtemperatures_insert_row
BEFORE INSERT ON temperatures
FOR EACH ROW EXECUTE PROCEDURE temperatures_insert_row();
INSERT INTO temperatures (date, continent, value) VALUES ('2020-01-01 12:00:00', 'Europe', 1);
SELECT*FROM temperatures;
SELECT*FROM temperatures_2020;
SELECT*FROM temperatures_2021;
INSERT INTO temperatures (date, continent, value) VALUES ('2021-01-01 12:00:00', 'Europe', 2);
SELECT*FROM temperatures;
SELECT*FROM temperatures_2020;
SELECT*FROM temperatures_2021;
SELECT relname, n_tup_ins, n_live_tup FROM pg_stat_user_tables WHERE relname like'temperatures%';
INSERT INTO temperatures (date, continent, value) VALUES ('2019-01-01 12:00:00', 'Europe', 3);
SELECT*FROM temperatures;
SELECT*FROM temperatures_2020;
SELECT*FROM temperatures_2021;
SELECT*FROM temperatures_default;
SELECT relname, n_tup_ins, n_live_tup FROM pg_stat_user_tables WHERE relname like'temperatures%';
-- New partitionCREATETABLEtemperatures_2019 () INHERITS (temperatures);
ALTERTABLE temperatures_2019 add check (date between date'2019-01-01'anddate'2020-02-01'-1);
-- Move data
with delta as (
deletefrom temperatures_default where extract(year fromdate) =2019
returning *
)
insert into temperatures_2019
select*from delta;
-- TestSELECT*FROM temperatures_default;
SELECT*FROM temperatures_2019;
💡 Denormalized partitions
Идея такая - мы ж делаем все это партиционирование для ускорения выборки
А почему бы не сделать партиции сразу в нескольких разрезах, тем самым ускоряя выборки в этих самых разрезах
DROPTABLE IF EXISTS temperatures_year_2018;
DROPTABLE IF EXISTS temperatures_year_2020;
DROPTABLE IF EXISTS temperatures_year_2021;
DROPTABLE IF EXISTS temperatures_year_other;
DROPTABLE IF EXISTS temperatures_continent_asia;
DROPTABLE IF EXISTS temperatures_continent_europe;
DROPTABLE IF EXISTS temperatures_continent_other;
DROPTABLE IF EXISTS temperatures_year;
DROPTABLE IF EXISTS temperatures_continent;
DROPTABLE IF EXISTS temperatures;
CREATETABLEtemperatures (
id SERIAL,
dateTIMESTAMPNOT NULL,
continent VARCHARNOT NULLCHECK (continent in ('Africa','Antarctica','Asia','Europe','North America','South America','Australia')),
value INT
);
-- by yearsCREATETABLEtemperatures_year () INHERITS (temperatures);
CREATETABLEtemperatures_year_2020 (like temperatures including all) INHERITS (temperatures_year);
ALTERTABLE temperatures_year_2020 add check (date between date'2020-01-01'anddate'2020-12-31');
CREATETABLEtemperatures_year_2021 () INHERITS (temperatures_year);
ALTERTABLE temperatures_year_2021 add check (date between date'2021-01-01'anddate'2021-12-31');
CREATETABLEtemperatures_year_other () INHERITS (temperatures_year);
-- by continentsCREATETABLEtemperatures_continent () INHERITS (temperatures);
CREATETABLEtemperatures_continent_europe () INHERITS (temperatures_continent);
ALTERTABLE temperatures_continent_europe add check (continent ='Europe');
CREATETABLEtemperatures_continent_other () INHERITS (temperatures_continent);
-- triggerCREATE OR REPLACEFUNCTIONtemperatures_insert_row()
RETURNS TRIGGER AS $$
BEGIN
if new.date between date'2020-01-01'anddate'2020-12-31' then
INSERT INTO temperatures_year_2020 VALUES (NEW.*);
elsif new.date between date'2021-01-01'anddate'2021-12-31' then
INSERT INTO temperatures_year_2021 VALUES (NEW.*);
else
INSERT INTO temperatures_year_other VALUES (NEW.*);
end if;
if new.continent='Europe' then
INSERT INTO temperatures_continent_europe VALUES (NEW.*);
else
INSERT INTO temperatures_continent_other VALUES (NEW.*);
end if;
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
CREATETRIGGERtemperatures_insert_row
BEFORE INSERT ON temperatures
FOR EACH ROW EXECUTE PROCEDURE temperatures_insert_row();
-- DEMOSELECT relname, n_tup_ins, n_live_tup FROM pg_stat_user_tables WHERE relname like'temperatures%';
INSERT INTO temperatures (date, continent, value) VALUES ('2020-01-01 12:00:00', 'Europe', 1);
SELECT relname, n_tup_ins, n_live_tup FROM pg_stat_user_tables WHERE relname like'temperatures%';
SELECT*FROM temperatures_year_2020; -- 1 rowSELECT*FROM temperatures_continent_europe; -- 1 rowINSERT INTO temperatures (date, continent, value) VALUES ('2018-01-01 12:00:00', 'Asia', 2);
SELECT*FROM temperatures_year_other; -- 1 rowSELECT*FROM temperatures_continent_other; -- 1 rowSELECT*FROM temperatures_year; -- 2 rowsSELECT*FROM temperatures_continent; -- 2 rows-- REPARTITIONING-- ----------------------------------- Create new tablesCREATETABLEtemperatures_year_2018 () INHERITS (temperatures_year);
ALTERTABLE temperatures_year_2018 add check (date between date'2018-01-01'anddate'2018-12-31');
CREATETABLEtemperatures_continent_asia () INHERITS (temperatures_continent);
ALTERTABLE temperatures_continent_asia add check (continent ='Asia');
-- Modify trigger so new records will land in new tablesCREATE OR REPLACEFUNCTIONtemperatures_insert_row()
RETURNS TRIGGER AS $$
BEGIN
if new.date between date'2020-01-01'anddate'2020-12-31' then
INSERT INTO temperatures_year_2020 VALUES (NEW.*);
elsif new.date between date'2021-01-01'anddate'2021-12-31' then
INSERT INTO temperatures_year_2021 VALUES (NEW.*);
elsif new.date between date'2018-01-01'anddate'2018-12-31' then
INSERT INTO temperatures_year_2018 VALUES (NEW.*);
else
INSERT INTO temperatures_year_other VALUES (NEW.*);
end if;
if new.continent='Europe' then
INSERT INTO temperatures_continent_europe VALUES (NEW.*);
elsif new.continent='Asia' then
INSERT INTO temperatures_continent_asia VALUES (NEW.*);
else
INSERT INTO temperatures_continent_other VALUES (NEW.*);
end if;
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
-- check that new trigger worksINSERT INTO temperatures (date, continent, value) VALUES ('2018-01-01 12:00:00', 'Asia', 3);
SELECT*FROM temperatures_year_other; -- old row still hereSELECT*FROM temperatures_continent_other; -- old row still hereSELECT*FROM temperatures_year_2018; -- new row landed hereSELECT*FROM temperatures_continent_asia; -- new row landed hereSELECT*FROM temperatures_year WHERE extract(YEAR fromdate) =2018; -- but we still see bothSELECT*FROM temperatures_continent WHERE continent ='Asia'; -- but we still see both-- move old data
with delta as (
deletefrom temperatures_year_other where extract(year fromdate) =2018
returning *
)
insert into temperatures_year_2018
select*from delta;
with delta as (
deletefrom temperatures_continent_other where continent ='Asia'
returning *
)
insert into temperatures_continent_asia
select*from delta;
-- TESTSELECT*FROM temperatures_year_other; -- 0 rows, data movedSELECT*FROM temperatures_continent_other; -- 0 rows, data movedSELECT*FROM temperatures_year_2018; -- 2 rows, data movedSELECT*FROM temperatures_continent_asia; -- 2 rows, data movedSELECT*FROM temperatures_year WHERE extract(YEAR fromdate) =2018; -- 2 rowsSELECT*FROM temperatures_continent WHERE continent ='Asia'; -- 2 rows
использовались файлы из ветки v1.6.3 т.к. из мастера забирать все как то стремно
в оригинальных файлах зачем то выключен сервис для мастера что по началу сбило с толку
все еще стремная штука, честно говоря я бы не рискнул на ней подымать что то боевое, а для чего то маленького - сильно избыточное (имею в виду и сам кубер и кол-во виртуалок, еще и реплики и т.д. и т.п.)
интереса ради поковырял pg_auto_failover но практически моментально уперся в это - с одной стороны выглядит как раз тем что надо, а с другой, пока еще как то не очень готовое
pg_auto_failover
крайне странная но все же рабочая штука
# NETWORK - без этого не завдеться демка
docker network create my
# MONITOR - тут и далее мы используем аутентификацию по паролю за вместо trust - наибольших головняк
docker run --network=my --hostname=monitor -it --rm -e PGDATA=/tmp/data -e PGPORT=5000 -p 5000:5000 --name=monitor citusdata/pg_auto_failover:v1.6.1 pg_autoctl create monitor --ssl-self-signed --auth md5 --run
# пароль надо добавлять руками
docker exec -it monitor psql "postgres://localhost:5000/pg_auto_failover" -c "ALTER USER autoctl_node PASSWORD 'autoctl_node_password'"# убедиться что пускаем по паролю
docker exec -it monitor cat /tmp/data/pg_hba.conf
# NODE1 - подключиться автоматом, т.к. передали пароль в connection string к monitor
docker run --network=my --hostname=node1 -it --rm -e PGDATA=/tmp/data -e PGPORT=5001 -p 5001:5001 --link=monitor --name=node1 citusdata/pg_auto_failover:v1.6.1 pg_autoctl create postgres --ssl-self-signed --auth md5 --pg-hba-lan --username demo --dbname demo --monitor postgresql://autoctl_node:autoctl_node_password@monitor:5000/pg_auto_failover --run
# задаем в настройках для pg_auto_failover пароль от pgautofailover_replicator пользователя
docker exec -it node1 pg_autoctl config set replication.password replication_password
# дублируем его же в базе
docker exec -it node1 psql "postgres://localhost:5001/demo" -c "ALTER USER pgautofailover_replicator PASSWORD 'replication_password'"# и еще один для пользователя pgautofailover_monitor
docker exec -it node1 psql "postgres://localhost:5001/demo" -c "ALTER USER pgautofailover_monitor PASSWORD 'monitor_password'"# демо табличка с данными
docker exec -it node1 psql "postgres://localhost:5001/demo" -c "CREATE TABLE t1(a int)"
docker exec -it node1 psql "postgres://localhost:5001/demo" -c "INSERT INTO t1 VALUES (1), (2)"# проверим что мы все еще пускаем всех по паролям
docker exec -it node1 cat /tmp/data/pg_hba.conf
# NODE2 - один в один та же команда что и для node1, за исключением портов, имени контейнера и т.п.
docker run --network=my --hostname=node2 -it --rm -e PGPASSWORD=replication_password -e PGDATA=/tmp/data -e PGPORT=5002 -p 5002:5002 --link=monitor --name=node2 citusdata/pg_auto_failover:v1.6.1 pg_autoctl create postgres --ssl-self-signed --auth md5 --pg-hba-lan --username demo --dbname demo --monitor postgresql://autoctl_node:autoctl_node_password@monitor:5000/pg_auto_failover --run
# опять же задаем пароль для pgautofailover_replicator
docker exec -it node2 pg_autoctl config set replication.password replication_password
# смотрим что у нас по авторизации
docker exec -it node2 cat /tmp/data/pg_hba.conf
# что с кластером
docker exec -it monitor pg_autoctl show state
# приехали ли данные
docker exec -it node2 psql "postgres://localhost:5002/demo" -c "select * from t1"# должно вывести connection string, но при авторизации по паролю не работает# docker exec -it monitor pg_autoctl show uri# пробуем подключиться "снаружи"
docker run -it --rm --network=my postgres:13-alpine psql "postgres://node1:5001,node2:5002/demo?target_session_attrs=read-write&sslmode=require" -U demo -c "select * from t1"# по скольку у нас вход по паролю, нужно его задать
docker exec -it node1 psql "postgres://localhost:5001/demo" -c "ALTER USER demo PASSWORD 'demo'"# SWITCHOVER
docker exec -it monitor pg_autoctl show state
docker exec -it monitor pg_autoctl perform switchover
Имеем базочку в 76Гб и 196 млн записей. Сверять будем следующий запрос:
SELECT payment_type, round(sum(tips)/sum(tips+fare)*100) tips_persent, count(*)
FROM taxi_trips
group by payment_type
order by3desc;
TLDR: безоговорочный лидер - bigquery
🥇 bigquery - 1 сек
🥈 postgres - 22 сек
🥉 cockroach - 147 сек
Bonus
postgres -
Примечания:
запросы выполнялись по 3 раза, брался лучший результат
импорт в postgres быстрее чем в cockroach
ни postgres ни cockroach не выедали все ресурсы
если похимичить, результаты для cockroach и postgres радикально меняются
интереса ради решил упороться с партициями и попробовать еще больше соков выжать из постгри, см. bonus но по факту оно дает результат только для сценария без индексов
Roadmap:
cockroachdb cluster in kubernetes
single node postgres
bonus
Cockroach
Cockroach будем подымать в Kubernetes с помощью их оператора
Примечание: по сколькуо сам кубер так же забирает какие то ресурсы, мы не можем зарезервировать все 4 ядра и 16 оперативки, оставляем чуть чуть для системы
поскольку мы экспортнули данные из bigquery в тот же проект что и kubernetes имеем безпроблемный доступ к google storage без возни с паролями
к сожалению не сработал импорт по wildcard и пришлось перечислять все 294 файла, не стал разбираться так как натурально время деньги, кластер же работает :)
Проверяем запрос:
SELECT payment_type, round(sum(tips)/sum(tips+fare)*100) tips_persent, count(*)
FROM taxi_trips
group by payment_type
order by3desc;
-- Time: 602.006s total (execution 602.006s / network 0.001s)-- Time: 578.319s total (execution 578.318s / network 0.001s)-- Time: 565.098s total (execution 565.097s / network 0.001s)
Химичим
CREATEINDEXpayment_type_idxON taxi_trips (payment_type, tips, fare);
-- Time: 1575.747s total (execution 0.060s / network 1575.688s)SELECT payment_type, round(sum(tips)/sum(tips+fare)*100) tips_persent, count(*)
FROM taxi_trips
group by payment_type
order by3desc;
-- Time: 161.803s total (execution 161.803s / network 0.001s)-- Time: 147.007s total (execution 147.007s / network 0.000s)-- Time: 148.132s total (execution 148.132s / network 0.000s)
Postgres
Для честного эксперимента, я делаю машинку похожую на мощностя которые были доступны cockroach, а именно 12cpu, 48ram
sudo -u postgres psql demo -c "\timing" -c "CREATE INDEX payment_type_idx ON taxi_trips (payment_type, tips, fare)"# Time: 691848.946 ms (11:31.849)
sudo -u postgres psql demo -c "\timing" -c "SELECT payment_type, round(sum(tips)/sum(tips+fare)*100) tips_persent, count(*) FROM taxi_trips group by payment_type order by 3 desc"# Time: 22759.725 ms (00:22.760)# Time: 22507.007 ms (00:22.507)# Time: 23075.058 ms (00:23.075)
Bonus
Судя по результатам у нас три большие групы поездок за наличку, карту и все остальное
По началу думал прям добавить три диска и отдельные tablespaces, но затем, посмотрев на телеметрию понял что в фактический диск мы не упираемся (где то 150 mbs) да и как оказалось я выел квоту и мне гугла просто напросто не дал резервировать еще диски, потому решил остановаиться просто на варианте с партиционированной табличкой
droptable taxi_trips;
createtabletaxi_trips (
unique_key text,
taxi_id text,
trip_start_timestamp TIMESTAMP,
trip_end_timestamp TIMESTAMP,
trip_seconds bigint,
trip_miles numeric,
pickup_census_tract bigint,
dropoff_census_tract bigint,
pickup_community_area bigint,
dropoff_community_area bigint,
fare numeric,
tips numeric,
tolls numeric,
extras numeric,
trip_total numeric,
payment_type text,
company text,
pickup_latitude numeric,
pickup_longitude numeric,
pickup_location text,
dropoff_latitude numeric,
dropoff_longitude numeric,
dropoff_location text
) PARTITION BY LIST (payment_type);
CREATETABLEtaxi_trips_cash PARTITION OF taxi_trips FOR VALUESIN ('Cash');
CREATETABLEtaxi_trips_credit_card PARTITION OF taxi_trips FOR VALUESIN ('Credit Card');
CREATETABLEtaxi_trips_other PARTITION OF taxi_trips DEFAULT;
COPY taxi_trips FROM'/data.csv' (FORMAT csv, null'', DELIMITER ',');
-- Time: 1642310.568 ms (27:22.311)
VACUUM ANALYZE;
-- Time: 346734.163 ms (05:46.734)SELECT payment_type, round(sum(tips)/sum(tips+fare)*100) tips_persent, count(*) FROM taxi_trips group by payment_type order by3desc;
-- Time: 192897.569 ms (03:12.898)-- Time: 194734.589 ms (03:14.735)-- Time: 275556.895 ms (04:35.557)CREATEINDEXcash_payment_type_idxON taxi_trips_cash (payment_type, tips, fare);
-- Time: 385334.999 ms (06:25.335)CREATEINDEXcredit_card_payment_type_idxON taxi_trips_credit_card (payment_type, tips, fare);
-- Time: 256547.121 ms (04:16.547)CREATEINDEXother_payment_type_idxON taxi_trips_other (payment_type, tips, fare);
-- Time: 11146.934 ms (00:11.147)SELECT payment_type, round(sum(tips)/sum(tips+fare)*100) tips_persent, count(*) FROM taxi_trips group by payment_type order by3desc;
-- Time: 25717.701 ms (00:25.718)-- Time: 26760.766 ms (00:26.761)-- Time: 25084.045 ms (00:25.084)
на выходе получаем gateway, worker-1, ..., worker-N
каждый воркер запускаясь, регистрирует себя в кластере, соотв работает scale
не стал упарываться с секретами и паролями, т.к. и так yml большой получился
настройки из pgtune для 3cpu, 10gb
после применения yml дожидаемся пока создадутся все поды и проверям что кластер собрался до кучи, выполняя kubectl exec -it gateway -- psql -U postgres -d demo -c "SELECT citus_get_active_worker_nodes()"
После эпопеи с aurora и spanner тестировать azure sql никакого желания нет, очевидно, что если он сможет пережевать весь объем и создать колончный индекс он всех уделает, но это надо буквально целый день времени
Spanner разочаровал производительностью, получилось загнать всего 5гб данных и при этом скорость отработки запроса 3 секунды, что не есть удовлетворительно.
Aurora при этом поглотила весь датасет, но так и не отработала запрос, после 12+ часов сумарного ожидания, не дождавшись погасил все инстанцы
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Bigquery.v2.Data;
using Google.Cloud.BigQuery.V2;
using Google.Cloud.Spanner.Data;
using Importer;
using Newtonsoft.Json;
using Enumerable = System.Linq.Enumerable;
using QueryOptions = Google.Cloud.BigQuery.V2.QueryOptions;
namespace importer
{
public static class Program
{
private static int totalRows = 0;
public static async Task Main()
{
Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", "/Users/mac/OneDrive/Desktop/importer/credentials.json");
var timer = Stopwatch.StartNew();
var queue = new ConcurrentQueue<string>();
var spanner = new SpannerConnection("Data Source=projects/pgotus/instances/pgotus/databases/chicago_taxi_trips");
spanner.Open();
var writers = new List<Task>();
for (var i = 0; i < 40; i++)
{
writers.Add(Task.Run(() =>
{
Thread.Sleep(5000);
string sql;
while (queue.TryDequeue(out sql))
{
try
{
var rows = spanner.CreateDmlCommand(sql).ExecuteNonQuery();
Interlocked.Add(ref totalRows, rows);
Console.WriteLine($"{totalRows} rows inserted in {timer.Elapsed}");
}
catch (Exception ex)
{
Console.WriteLine($"{ex.Message}");
queue.Enqueue(sql);
Thread.Sleep(1000);
}
}
}));
}
File.ReadLines("D:/taxi_trips/data10.csv").Skip(1).Select(line => line
.Split(",")
.Select(cell => cell.Trim())
.Select(cell => string.IsNullOrEmpty(cell) ? "NULL" : double.TryParse(cell, out _) ? cell : $"'{cell}'"))
.Select(cells => $"({string.Join(", ", cells)})")
.Chunk(100)
.Select(values => $"INSERT INTO taxi_trips (unique_key, taxi_id, trip_start_timestamp, trip_end_timestamp, trip_seconds, trip_miles, pickup_census_tract, dropoff_census_tract, pickup_community_area, dropoff_community_area, fare, tips, tolls, extras, trip_total, payment_type, company, pickup_latitude, pickup_longitude, pickup_location, dropoff_latitude, dropoff_longitude, dropoff_location) VALUES {string.Join(", ", values)}")
.AsParallel().ForAll(sql => queue.Enqueue(sql));
await Task.WhenAll(writers);
Console.WriteLine($"DONE IN {timer.Elapsed}");
}
}
}
Примечания:
получилось разогнать спаннер до вставки 1млн записей за 30мин, что совсем не удовлетворительно, при этом в панеле вижу полную утилизацию, при том что я на тысяче юнитов и стоить все это будет $1K/mo 🤔
скрипт по вставке один поток вычитывает и обрабатывает сроки из csv, затем через очередь в 40 потоков проивзодит вставки по 100 строк, соотв за один проход мы вставляем 4К строк
утилизация машины с которой производил вставку - никакая - можно было бы пробовать еще больше потоков, но нет смысла из-за того что мы в полку положили спаннер
после полутора часа поднял до 2К процессорных юнитов, но значимого эффекта не дало
через почти 4 часа, вставилось 10млн строк и приблизительно 5гб из десяти
Пробую прогнать наш запрос:
SELECT payment_type, round(sum(tips)/sum(tips+fare)*100) tips_persent, count(*) FROM taxi_trips group by payment_type order by3desc-- 15.43 s elapsed-- 11.21 s elapsed-- 5.73 s elapsed-- 4.11 s elapsed-- 4.93 s elapsed-- 3.55 s elapsed-- 2.93 s elapsed-- 10.29 s elapsed-- 3.76 s elapsed-- 3.96 s elapsed
Примечаниe: пришлось повозиться с ролями и параметрами что бы достучаться до s3 и побороть ошибку "Both aurora_load_from_s3_role and aws_default_s3_role are not specified, please see documentation for more details", нужно было создать отдельную групу параметров, так как група по умолчанию не меняется и уже там указать новую роль для обоих настроек и перекрикрепить к базе
Так ну и наш запрос:
SELECT payment_type, round(sum(tips)/sum(tips+fare)*100) tips_persent, count(*) FROM taxi_trips group by payment_type order by3desc;
--
Примечания:
в самом начале загонял файлик на 256мб и запрос там работал минутами
после импорта выполнял optimize table taxi_trips
datagrip почему то ни в какую не хотел отправлять запрос один стейтментом, пришлось мудрить с докерами и консольным клиентом
в процессе импорта mysql клиент отвалился, но судя по графикам мониторинга запрос все еще фигачит
импорт занял около 2-3 часов
прогон optimize - занимает бесконечность, я так и не понял доделался или нет, после 3х часов уже забил
первых пару запросов не отработали и mysql отвалился, а cpu ушел в полку
✅ добавьте свеже-созданный диск к виртуальной машине
✅ проинициализируйте диск (см. disk)
✅ сделайте пользователя postgres владельцем /mnt/data - нужно выполнять из под sudo
✅ перенесите содержимое /var/lib/postgres/13 в /mnt/data - примечание, в 20.04 из apt 12-я версия, как следствие пути чуть другие, так же требует привилегий sudo mv /var/lib/postgresql/12 /mnt/data
примечание: пришлось отвязать диск от hw2-1, только после этого дало его привязать к этой машинке
проверим что система видит добавленный диск - sudo lsblk --fs
запись в fstab - echo "LABEL=hw2-2 /var/lib/postgresql ext4 defaults 0 2" | sudo tee -a /etc/fstab
нужно создать папку - mkdir /var/lib/postgresql
монтируем - sudo mount -a
проверяем что файлы на месте - sudo ls -la /var/lib/postgresql/12/main/
запускаемся - sudo systemctl start postgresql
проверяем что с базой - pg_lsclusters - все ок, мы online
проверяем данные - sudo -u postgres psql sample -c "select count(*) from pgbench_accounts" - 🎉
Если работать с виртуальными машинами, это наиболее предпочтительный способ, т.к. внешние диски будет сильно проще переносить между машинками в случае такой необходимости.
disk
sudo apt update && sudo apt install -y parted
sudo parted -l | grep Error # /dev/sdb
lsblk # will show that sdb has no partitions
sudo parted /dev/sdb mklabel gpt # our disk will be partitioned via gpt standard
sudo parted -a opt /dev/sdb mkpart primary ext4 0% 100% # create partition
sudo mkfs.ext4 -L hw2-2 /dev/sdb1 # format partition and give it `hw2-2` label# sudo e2label /dev/sdb1 newlabel # change label example
sudo lsblk --fs # list partitions with labels# sudo lsblk -o NAME,FSTYPE,LABEL,UUID,MOUNTPOINT # alternative approach
sudo mkdir -p /mnt/data # directory where partition will be mounted# sudo mount -o defaults /dev/sdb1 /mnt/data # temporary mountecho"LABEL=hw2-2 /mnt/data ext4 defaults 0 2"| sudo tee -a /etc/fstab # permanent mount config
sudo mount -a # remount everything
df -h # check that disk present
ls -f /mnt/data # there will be `lost+found` directory which means that we are at ext4 rootecho"success"| sudo tee /mnt/data/test_file # check write
cat /mnt/data/test_file # check read
sudo rm /mnt/data/test_file # check delete
constexpress=require('express')const{ Pool }=require('pg');constpool=newPool({user: process.env.POSTGRES_USERNAME||'demo',host: process.env.POSTGRES_HOSTNAME||'localhost',database: process.env.POSTGRES_DATABASE||'demo',password: process.env.POSTGRES_PASSWORD||'demo',port: process.env.POSTGRES_PORT||5432,})constapp=express()app.use(express.json());constwellKnownPeopleQueryStringParametersMiddleware=allowedFields=>(req,res,next)=>{const{query: {fields =allowedFields, limit ='10', offset ='0', name}}=reqreq.fields=Array.isArray(fields) ? fields : [fields]req.fields=req.fields.filter(f=>allowedFields.includes(f))req.limit=isNaN(parseInt(limit))||parseInt(limit)>10 ? 10 : parseInt(limit)req.offset=isNaN(parseInt(offset)) ? 0 : parseInt(offset)req.name=name ? `${name}%` : undefinednext()}constpeopleMiddleware=wellKnownPeopleQueryStringParametersMiddleware(['id','name','age'])app.get('/people/:id',peopleMiddleware,async({params: {id}, fields},res)=>{try{const{rows}=awaitpool.query(`SELECT ${fields.join(',')} FROM people WHERE id = $1`,[id])constrow=rows.shift()res.status(row ? 200 : 404).json(row)}catch(error){res.status(500).json(error)}})app.delete('/people/:id',async(req,res)=>{try{awaitpool.query(`DELETE FROM people WHERE id = $1`,[id])res.sendStatus(200)}catch(error){res.status(200).json(error)}})app.get('/people',peopleMiddleware,async({fields, limit, offset, name},res)=>{console.log({fields, limit, offset, name})try{const{rows}=awaitpool.query(`SELECT ${fields.join(',')} FROM people WHERE $1::text IS NULL OR name LIKE $1::text LIMIT $2 OFFSET $3`,[name,limit,offset])res.json(rows)}catch(error){res.status(500).json(error)}})app.post('/people',peopleMiddleware,async({body: {name, age}, fields},res)=>{age=isNaN(parseInt(age)) ? 0 : parseInt(age)if(!name||!age){returnres.status(400).json({message: 'name and age are required body parameters'})}try{const{rows}=awaitpool.query(`INSERT INTO people(name, age) VALUES($1, $2) RETURNING ${fields.join(',')}`,[name,age])res.json(rows.shift())}catch(error){console.log(error)res.status(500).json(error)}})app.listen(process.env.PORT||3000)
CREATETABLEIF NOT EXISTS people (
id SERIALPRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INTNOT NULL
);
-- INSERT INTO people(name, age) VALUES ('Michael', 7), ('Kira', 4);INSERT INTO people(name, age)
SELECT'Michael', 7UNION ALLSELECT'Kira', 4WHERE NOT EXISTS (SELECT*FROM people);
docker-compose up -d
docker run -it --rm --network=rest_default httpd:alpine ab -n 1000 -c 100 http://web:3000/people
Server Hostname: web
Server Port: 3000
Document Path: /people
Document Length: 66 bytes
Concurrency Level: 100
Time taken for tests: 0.789 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 273000 bytes
HTML transferred: 66000 bytes
Requests per second: 1267.77 [#/sec] (mean)
Time per request: 78.879 [ms] (mean)
Time per request: 0.789 [ms] (mean, across all concurrent requests)
Transfer rate: 337.99 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.4 0 2
Processing: 37 77 34.9 66 214
Waiting: 5 67 34.1 57 203
Total: 37 77 35.3 66 215
2 зайдите в созданный кластер под пользователем postgres
sudo -u postgres psql
3 создайте новую базу данных testdb
CREATEDATABASEtestdb;
4 зайдите в созданную базу данных под пользователем postgres
\c testdb
5 создайте новую схему testnm
CREATESCHEMAtestnm;
6 создайте новую таблицу t1 с одной колонкой c1 типа integer
CREATETABLEt1(c1 integer);
7 вставьте строку со значением c1=1
INSERT INTO t1 VALUES(1);
8 создайте новую роль readonly
CREATE ROLE readonly;
9 дайте новой роли право на подключение к базе данных testdb
ALTER ROLE readonly WITH LOGIN;
Примечание: попытка залогиниться sudo -u postgres psql -U readonly -d testdb ожидаемо не работает и падает с ошибкой Peer authentication failed for user "readonly", если мы хотим оставить без парольный, локальный вход, необходимо завести пользователя в системе или завести пароль для роли readonly и подключаться к localhost за вместо юниксового сокета
Примечание: так не получилось, \du выводит новую роль и показывает что она member of readonly, но при этом пишет что не может логиниться, причем что любопытно, попытка изнутри постгри \c - testread дает тот же результат
🤦♂️ это жесть, пришлось смотреть в шпаргалку, так бы долго искал
После этих пунктов уже сложно сориентироваться по пунктам домашки и соотнести их с шпаргалкой, но в целом все получилось
Выводы:
вся история с правами требует очень хорошего понимания, если такого понимания нет - лучше вообще не трогать
очень удивило история с тем что права фиксируются только к созданным табличкам, это черевато проблемами, т.к. в "живом" проекте нон стоп что то меняется
выглядит так что подобный трюк как в шпаргалке можно попробовать сделать в template1 что бы все последующие базы имели такие настройки
любопытно вот еще что, можно drop schema public в базе сделать, по сути эффект будет похожим, так же, что бы обезопаситься убрать из search_path схему $user
Задача: покрутить ручки автовакуума и собрать статистику
По скольку эксперимент долгий (целый час) будем запускать в паралель.
План действий - запускаем сразу четыре виртуалки:
настройки по умолчанию default
настройки заниженые по минимуму min
настройки завышенные по максимуму max
настройки со слайдов slieds
После завершения эксперимента, собираем статистику и подготавливаем графики
Затем попытаюсь сделать выводы, аргументированно предложить свой вариант
Результаты
Так же прикрепляю скрин с v1 в которой настройки базы по умолчанию
autovacuum_max_workers = 3 -> 10 - в нашем случае вероятно не имеет смысла больше 4, т.к. pgbench создал столько табличек, а как следствие 5-му и выше будет просто нечего чистить (плюс не стоить забывать что на нашей виртуалке всего 2 ядра)
autovacuum_naptime = 60s -> 15s - сложно сказать не нашел способа посмотреть как долго выполнялись автовакуумы, так же в виду того что у нас нагрузка не типичная (долбим нон стоп) этот показатель стоит сделать по меньше, но сделав его сильно малым (условно 1 секунду) сервер ничем другим не будет заниматься кроме как автовакуумом
autovacuum_vacuum_threshold = 50 -> 25 - учитывая что у нас в секунду получается в районе 400 tps, из которых только третья часть это обновления и удаления (пускай для ровного счата будет 100 tps) получается мы важдую секунду все равно превышаем этот порог, и судя по всему его можн было бы выставить в 100 и даже больше (мы ведь не хотим что бы автовакуум прям каждую секунду фигачил)
autovacuum_vacuum_scale_factor = 0.2 -> 0.1 - тут аналогично предыдущей настройке
autovacuum_vacuum_cost_delay = 2 -> 10 и autovacuum_vacuum_cost_limit = -1 -> 1000 - учитывая очень малый размер табличек, мы скорее всего их все за раз перерабатываем т.к. скорее всего они уже в памяти и не нужно ходить на диск
в целом настройки увеличили кол-во прогонов автовакуума в 4 раза, а вот tsp особо не поменялся и даже стал чуть хуже, если было 403 с настройками по умолчанию стало 398
Примечания
у меня в шаблоне ssd диск, за вместо предложенного standard, возможно всюду результаты будут чуть выше чем ожидаемые
всякие кибаны и т.п. это круто, но объективно не успею, сбор статистику будет выполнен на основе вывода прогресса pgbench и затем обработан скриптом
любопытный факт, впервые за все время столкнулся с тем что отобрали preemtible виртуалку :)
в предложенных настройках maintanence 512mb что по идее половину запусков отменило, т.к. попытка выставить 10 автовакуумов уже будет больше чем есть памяти всего
Касаемо pgbench_accounts, она не вакуумировалась т.к. в обоих сценариях не был привышен порог, считал таким запросом:
select
n_dead_tup as fact,
autovacuum_vacuum_threshold::decimal+ autovacuum_vacuum_scale_factor::decimal* reltuples::decimalas threshold,
n_dead_tup >= autovacuum_vacuum_threshold::decimal+ autovacuum_vacuum_scale_factor::decimal* reltuples::decimalas should_be_vacuumed
from (
select
n_dead_tup,
(select setting from pg_settings where name ='autovacuum_vacuum_threshold') as autovacuum_vacuum_threshold,
(select setting from pg_settings where name ='autovacuum_vacuum_scale_factor') as autovacuum_vacuum_scale_factor,
(select reltuples from pg_class where relname ='pgbench_accounts') as reltuples
from pg_stat_all_tables where relname ='pgbench_accounts'
) as x;
Алгоритм действий на каждой виртуалке
Создается виртуалка hw5-experiment-name, где experiment-name будет одно из: default, min, max, slides
Настройки:
echo"max_connections = 40"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"shared_buffers = 1GB"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"effective_cache_size = 3GB"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"maintenance_work_mem = 512MB"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"checkpoint_completion_target = 0.9"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"wal_buffers = 16MB"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"default_statistics_target = 500"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"random_page_cost = 4"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"effective_io_concurrency = 2"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"work_mem = 6553kB"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"min_wal_size = 4GB"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"max_wal_size = 16GB"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
Далее серия вызовов по типу:
echo"autovacuum_analyze_scale_factor = 0.2"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
После чего перезапуск базы
sudo systemctl restart postgresql
И проверка что настройки применились
sudo -u postgres psql -c "SELECT name, setting FROM pg_settings WHERE category like '%Autovacuum%'"
Причина в более эффективном сбрасывании на диск, теперь за вместо того что бы писать на диск на каждый чих, мы делаем это отдельным процессом, по расписанию и пачками.
Создайте новый кластер с включенной контрольной суммой страниц. Создайте таблицу. Вставьте несколько значений. Выключите кластер. Измените пару байт в таблице. Включите кластер и сделайте выборку из таблицы.
Настройте сервер так, чтобы в журнал сообщений сбрасывалась информация о блокировках, удерживаемых более 200 миллисекунд. Воспроизведите ситуацию, при которой в журнале появятся такие сообщения.
подготовка
sudo -u postgres psql -c "ALTER SYSTEM SET deadlock_timeout TO 200"
sudo -u postgres psql -c "select pg_reload_conf()"
sudo -u postgres psql -c "show deadlock_timeout"
sudo -u postgres psql -c "create table messages(id int primary key,message text)"
sudo -u postgres psql -c "insert into messages values (1, 'hello')"
sudo -u postgres psql -c "insert into messages values (2, 'world')"
session 1
sudo -u postgres psql <<EOFBEGIN;SELECT message FROM messages WHERE id = 1 FOR UPDATE;SELECT pg_sleep(10);UPDATE messages SET message = 'message from session 1' WHERE id = 2;COMMIT;EOF
session 2
sudo -u postgres psql <<EOFBEGIN;SELECT message FROM messages WHERE id = 2 FOR UPDATE;UPDATE messages SET message = 'message from session 2' WHERE id = 1;COMMIT;EOF
результат
sudo -u postgres psql -c "select * from messages"
id
message
2
world
1
message from session 2
Первая сессия оборвалась с ошибкой
ERROR: deadlock detected
DETAIL: Process 5777 waits for ShareLock on transaction 516; blocked by process 5787.
Process 5787 waits for ShareLock on transaction 515; blocked by process 5777.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,2) in relation "messages"
ROLLBACK
Вторая при этом успешно завершилась
В логе при этом
cat /var/log/postgresql/postgresql-12-main.log | grep "deadlock detected" -A 10
2021-05-30 07:12:01.541 UTC [5777] postgres@postgres ERROR: deadlock detected
2021-05-30 07:12:01.541 UTC [5777] postgres@postgres DETAIL: Process 5777 waits for ShareLock on transaction 516; blocked by process 5787.
Process 5787 waits for ShareLock on transaction 515; blocked by process 5777.
Process 5777: UPDATE messages SET message = 'message from session 1' WHERE id = 2;
Process 5787: UPDATE messages SET message = 'message from session 2' WHERE id = 1;
2021-05-30 07:12:01.541 UTC [5777] postgres@postgres HINT: See server log for query details.
2021-05-30 07:12:01.541 UTC [5777] postgres@postgres CONTEXT: while updating tuple (0,2) in relation "messages"
2021-05-30 07:12:01.541 UTC [5777] postgres@postgres STATEMENT: UPDATE messages SET message = 'message from session 1' WHERE id = 2;
Смоделируйте ситуацию обновления одной и той же строки тремя командами UPDATE в разных сеансах. Изучите возникшие блокировки в представлении pg_locks и убедитесь, что все они понятны. Пришлите список блокировок и объясните, что значит каждая.
SELECTblocked_locks.pidAS blocked_pid,
blocked_activity.usenameAS blocked_user,
blocking_locks.pidAS blocking_pid,
blocking_activity.usenameAS blocking_user,
blocked_activity.queryAS blocked_statement,
blocking_activity.queryAS current_statement_in_blocking_process,
blocked_activity.application_nameAS blocked_application,
blocking_activity.application_nameAS blocking_application
FROMpg_catalog.pg_locks blocked_locks
JOINpg_catalog.pg_stat_activity blocked_activity ONblocked_activity.pid=blocked_locks.pidJOINpg_catalog.pg_locks blocking_locks
ONblocking_locks.locktype=blocked_locks.locktypeANDblocking_locks.DATABASE IS NOT DISTINCT FROMblocked_locks.DATABASEANDblocking_locks.relation IS NOT DISTINCT FROMblocked_locks.relationANDblocking_locks.page IS NOT DISTINCT FROMblocked_locks.pageANDblocking_locks.tuple IS NOT DISTINCT FROMblocked_locks.tupleANDblocking_locks.virtualxid IS NOT DISTINCT FROMblocked_locks.virtualxidANDblocking_locks.transactionid IS NOT DISTINCT FROMblocked_locks.transactionidANDblocking_locks.classid IS NOT DISTINCT FROMblocked_locks.classidANDblocking_locks.objid IS NOT DISTINCT FROMblocked_locks.objidANDblocking_locks.objsubid IS NOT DISTINCT FROMblocked_locks.objsubidANDblocking_locks.pid!=blocked_locks.pidJOINpg_catalog.pg_stat_activity blocking_activity ONblocking_activity.pid=blocking_locks.pidWHERE NOT blocked_locks.GRANTED;
blocked_pid
blocked_user
blocking_pid
blocking_user
blocked_statement
current_statement_in_blocking_process
blocked_application
blocking_application
6274
postgres
6261
postgres
UPDATE messages SET message = 'message from session 2' WHERE id = 1;
UPDATE messages SET message = 'message from session 1' WHERE id = 1;
psql
psql
6284
postgres
6274
postgres
UPDATE messages SET message = 'message from session 3' WHERE id = 1;
UPDATE messages SET message = 'message from session 2' WHERE id = 1;
psql
psql
6261 блокирует 6274, а та в свою очередь - 6284
SELECT
row_number() over(ORDER BY pid, virtualxid, transactionid::text::bigint) as n,
CASE
WHEN locktype ='relation' THEN 'отношение'
WHEN locktype ='extend' THEN 'расширение отношения'
WHEN locktype ='frozenid' THEN 'замороженный идентификатор'
WHEN locktype ='page' THEN 'страница'
WHEN locktype ='tuple' THEN 'кортеж'
WHEN locktype ='transactionid' THEN 'идентификатор транзакции'
WHEN locktype ='virtualxid' THEN 'виртуальный идентификатор'
WHEN locktype ='object' THEN 'объект'
WHEN locktype ='userlock' THEN 'пользовательская блокировка'
WHEN locktype ='advisory' THEN 'рекомендательная'
END AS locktype,
relation::regclass,
-- CASE WHEN relation IS NULL THEN 'цель блокировки — не отношение или часть отношения' ELSE CAST(relation::regclass AS TEXT) END AS relation,
CASE WHEN page IS NOT NULLAND tuple IS NOT NULL THEN (select message from messages m wherem.ctid::text='('|| page ||','|| tuple ||')'limit1) ELSE NULL END AS row, -- page, tuple,
virtualxid, transactionid, virtualtransaction,
pid,
CASE WHEN pid =6261 THEN 'session1' WHEN pid =6274 THEN 'session2' WHEN pid =6284 THEN 'session3' END AS session,
mode,
CASE WHEN granted = true THEN 'блокировка получена' ELSE 'блокировка ожидается' END AS granted,
CASE WHEN fastpath = true THEN 'блокировка получена по короткому пути' ELSE 'блокировка получена через основную таблицу блокировок' END AS fastpath
FROM pg_locks WHERE pid in (6261, 6274,6284)
ORDER BY pid, virtualxid, transactionid::text::bigint;
n
locktype
relation
row
virtualxid
transactionid
virtualtransaction
pid
session
mode
granted
fastpath
1
виртуальный идентификатор
4/202
4/202
6261
session1
ExclusiveLock
блокировка получена
блокировка получена по короткому пути
2
идентификатор транзакции
530
4/202
6261
session1
ExclusiveLock
блокировка получена
блокировка получена через основную таблицу блокировок
3
отношение
messages_pkey
4/202
6261
session1
RowExclusiveLock
блокировка получена
блокировка получена по короткому пути
4
отношение
messages
4/202
6261
session1
RowExclusiveLock
блокировка получена
блокировка получена по короткому пути
5
виртуальный идентификатор
5/22
5/22
6274
session2
ExclusiveLock
блокировка получена
блокировка получена по короткому пути
6
идентификатор транзакции
530
5/22
6274
session2
ShareLock
блокировка ожидается
блокировка получена через основную таблицу блокировок
7
идентификатор транзакции
531
5/22
6274
session2
ExclusiveLock
блокировка получена
блокировка получена через основную таблицу блокировок
8
кортеж
messages
hello
5/22
6274
session2
ExclusiveLock
блокировка получена
блокировка получена через основную таблицу блокировок
9
отношение
messages_pkey
5/22
6274
session2
RowExclusiveLock
блокировка получена
блокировка получена по короткому пути
10
отношение
messages
5/22
6274
session2
RowExclusiveLock
блокировка получена
блокировка получена по короткому пути
11
виртуальный идентификатор
6/6
6/6
6284
session3
ExclusiveLock
блокировка получена
блокировка получена по короткому пути
12
идентификатор транзакции
532
6/6
6284
session3
ExclusiveLock
блокировка получена
блокировка получена через основную таблицу блокировок
13
кортеж
messages
hello
6/6
6284
session3
ExclusiveLock
блокировка ожидается
блокировка получена через основную таблицу блокировок
14
отношение
messages
6/6
6284
session3
RowExclusiveLock
блокировка получена
блокировка получена по короткому пути
15
отношение
messages_pkey
6/6
6284
session3
RowExclusiveLock
блокировка получена
блокировка получена по короткому пути
Примечание:
каждый сеанс держит эксклюзивные (exclusive lock) блокировки на номера своих транзакций (transactionid - 2, 7, 12 строки) и виртуальной транзакции (virtualxid - 1, 5, 11 - строки)
первый сеанс захватил эксклюзивную блокировку строки для ключа и самой строки, строки 3, 4
оставшиеся два запроса хоть и ожидают блокировки так же повесили row exclusive lock на ключ и строку, строки - 9, 10 и 14, 15
так же оставшиеся два сеанса повесили экслоюзивную блокировку на сам кортеж, т.к. хотят обновить именно его, а он уже обновлен в первом сеансе, строки 8 и 13
оставшаяся блокировка share lock в 6 строке вызванна тем что мы пытаемся обновить ту же строку что и в первом сеансе у которого уже захвачен row exclusive lock
Воспроизведите взаимоблокировку трех транзакций. Можно ли разобраться в ситуации постфактум, изучая журнал сообщений?
SELECT pg_backend_pid() as pid, txid_current() as tid;
SELECT pg_backend_pid() as pid, txid_current() as tid;
SELECT pg_backend_pid() as pid, txid_current() as tid;
3
SELECT message FROM messages WHERE id = 1 FOR UPDATE;
4
SELECT message FROM messages WHERE id = 2 FOR UPDATE;
5
SELECT message FROM messages WHERE id = 3 FOR UPDATE;
6
UPDATE messages SET message = 'message from session 1' WHERE id = 2;
7
UPDATE messages SET message = 'message from session 2' WHERE id = 3;
8
UPDATE messages SET message = 'message from session 3' WHERE id = 1;
результаты
первый сеанс висит на апдейте
второй сеанс обновил строку
третий сеанс вылетел с ошибкой
ERROR: deadlock detected
DETAIL: Process 6966 waits for ShareLock on transaction 538; blocked by process 6946.
Process 6946 waits for ShareLock on transaction 539; blocked by process 6956.
Process 6956 waits for ShareLock on transaction 540; blocked by process 6966.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,1) in relation "messages"
лог
2021-05-30 08:56:27.777 UTC [6966] postgres@postgres ERROR: deadlock detected
2021-05-30 08:56:27.777 UTC [6966] postgres@postgres DETAIL:
Process 6966 waits for ShareLock on transaction 538; blocked by process 6946.
Process 6946 waits for ShareLock on transaction 539; blocked by process 6956.
Process 6956 waits for ShareLock on transaction 540; blocked by process 6966.
Process 6966: UPDATE messages SET message = 'message from session 3' WHERE id = 1;
Process 6946: UPDATE messages SET message = 'message from session 1' WHERE id = 2;
Process 6956: UPDATE messages SET message = 'message from session 2' WHERE id = 3;
2021-05-30 08:56:27.777 UTC [6966] postgres@postgres HINT: See server log for query details.
2021-05-30 08:56:27.777 UTC [6966] postgres@postgres CONTEXT: while updating tuple (0,1) in relation "messages"
2021-05-30 08:56:27.777 UTC [6966] postgres@postgres STATEMENT: UPDATE messages SET message = 'message from session 3' WHERE id = 1;
В логе мы видим что процесс № 6966 (третий сеанс) споймал deadlock
Детали нам говорят о том что:
третий сенас ждал первого и второго, сеанс два при этом ждал третьего (кольцо)
далее приведены запросы, но из-за того что блокировку мы вызвали ранее по ним можно только сказать что мы пытались обновить, но не причину блокировки
Могут ли две транзакции, выполняющие единственную команду UPDATE одной и той же таблицы (без where), заблокировать друг друга?
подготовка
sudo -u postgres psql -c "drop table test"
sudo -u postgres psql -c "create table test(id integer primary key generated always as identity, n float)"
sudo -u postgres psql -c "insert into test(n) select random() from generate_series(1,1000000)"
session 1
sudo -u postgres psql <<EOFBEGIN ISOLATION LEVEL REPEATABLE READ;UPDATE test SET n = (select id from test order by id asc limit 1 for update);COMMIT;EOF
session 2
sudo -u postgres psql <<EOFBEGIN ISOLATION LEVEL REPEATABLE READ;UPDATE test SET n = (select id from test order by id desc limit 1 for update);COMMIT;EOF
В моем случае первый сеанс отвалился с ошибкой:
ERROR: deadlock detected
DETAIL: Process 8056 waits for ShareLock on transaction 608; blocked by process 8066.
Process 8066 waits for ShareLock on transaction 607; blocked by process 8056.
HINT: See server log for query details.
CONTEXT: while updating tuple (15554,62) in relation "test"
ROLLBACK
Примечание: мы забираем id for update в первом сеансе отсортированные во возрастанию, а во втором по убыванию из-за чего по началу вроде как все ок, но когда два запроса "пересекаются" начинаются проблемы
Перед запуском база перезапускается, т.к. меняются настройки, что безусловно влияет на результат, но т.к. это относиться ко всем экспериментам то и результат будет у всех искажен одинаково
Defaults - 579 tps
result
SQL statistics:
queries performed:
read: 4865126
write: 1389986
other: 695048
total: 6950160
transactions: 347499 (579.02 per sec.)
queries: 6950160 (11580.68 per sec.)
ignored errors: 10 (0.02 per sec.)
reconnects: 0 (0.00 per sec.)
General statistics:
total time: 600.1494s
total number of events: 347499
Threads fairness:
events (avg/stddev): 9930.2344/46.32
execution time (avg/stddev): 600.0147/0.03
Примечания:
результат - х2 от настроек по умолчанию
очень забавно, т.к. в слепую скопировал max connections и в тесте мы запускаем 64 потока, то в процессе эксперимента не смог подключиться что бы посмотреть что внутри базы происходит
pgtune.leopard.in.ua: OLTP - 1270 tps
config
# DB Version: 13
# OS Type: linux
# DB Type: oltp
# Total Memory (RAM): 32 GB
# CPUs num: 8
# Connections num: 100
# Data Storage: ssd
Threads fairness:
events (avg/stddev): 11912.4531/50.89
execution time (avg/stddev): 600.0004/0.03
Примечания:
в процессе прогона теста видны просадки в tps но они не такие большие как в случае с web application, причина в том что мы увеличили размеры для wal size а как следствие реже и большим батчем работаем с ним
результат еще лучше чем в предыдущий 1059 vs 1270 tps
а вот work mem я себе думаю вернуть назад к 32мб
все последующие эксперименты будут от этого конфига плюс новые параметры
Threads fairness:
events (avg/stddev): 12626.7656/108.28
execution time (avg/stddev): 599.9971/0.02
Примечания:
очень странно, у меня выбило сервер (preemtible), прогон по началу показывал заниженные цифры хотя 100% должно быть лучше, только после пары минут начал выдавать 1.3K tps, пришлось перезапустить
выключение синхронных комитов конретно в этом сетапе дает очень не значительный выигрыш, вот если бы мы это сделали на этапе инициализации эксперимента там было бы значимое ускорение
System - 1315
Согласно рекомендациям postgresqltuner.pl и паре статей, тюним саму систему:
Threads fairness:
events (avg/stddev): 12338.7656/147.22
execution time (avg/stddev): 600.0029/0.02
Примечания:
в целом стало даже хуже, конретно для этого стенда эти настройки не дают значимого эффекта, хоть и имеют смысл в целом
лишний раз доказывает о том что в настройки особенно системы стоит лезть только если понимаешь что делаешь
Indexes - 1405
postgresqltuner.pl указал на одну очень важную штуку, а именно то что созданные индексы не используются
первым делом грохнул индексы
dropindex k_1;
...
dropindex k_100;
посмотрел на сами запросы, покрутил разными тулами не похоже что можно добавить какой то индекс, но по идее удаление не используемых индексов должно дать прирост
откатил настройки системы (т.к. там все равно хуже результат) перезапустил систему и прогнал тест еще раз, пришлось перезапускать, совсем холодный старт, очень медленно работало
result
SQL statistics:
queries performed:
read: 11808314
write: 3373790
other: 1686914
total: 16869018
transactions: 843450 (1405.53 per sec.)
queries: 16869018 (28110.59 per sec.)
ignored errors: 1 (0.00 per sec.)
reconnects: 0 (0.00 per sec.)
General statistics:
total time: 600.0928s
total number of events: 843450
Threads fairness:
events (avg/stddev): 13178.9062/132.15
execution time (avg/stddev): 599.9906/0.02
Примечания:
Верно пишут в статьях мол не забывайте смотреть на сами запросы, индексы и вот это все, т.к. это может дать значимы прирост, в нашем случае удаление не нужных индексов дало еще 60 tps
echo"listen_addresses = '*'"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"wal_level = 'logical'"| sudo tee -a /etc/postgresql/12/main/postgresql.conf
echo"host all all 0.0.0.0/0 md5"| sudo tee -a /etc/postgresql/12/main/pg_hba.conf
echo"host replication all 0.0.0.0/0 md5"| sudo tee -a /etc/postgresql/12/main/pg_hba.conf
sudo systemctl restart postgresql
sudo -u postgres psql -c "CREATE USER demo SUPERUSER encrypted PASSWORD '123'"
На 1 ВМ создаем таблицы test для записи, test2 для запросов на чтение.
it will ask for 123 password of replica user created earlier
it might take some time to backup restore 500mb of data
it will wait for a checkpoint before starting, so run sudo -u postgres psql -c "checkpoint" on a master (somehow it did not happened in my case after few minutes)