Рассмотрим технологии которые обеспечивают высокую производительность и безопасность работы сервера PostgreSQL, а именно «Буферный кэш» и «Журнал предзаписи WAL«. В документации об этом можете почитать тут.

Устройство буферного кэша

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

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

Есть 3 варианта размера страниц:

  • 8 КБ
  • 16 КБ
  • 32 КБ

Буферный кэш занимает большую часть общей памяти.

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

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

Рисунок двойной буферизации Postgresql

Алгоритм вытеснения

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

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

Журнал предзаписи (WAL)

То что у нас данные находятся в оперативной памяти — это хорошо. Но при сбое эти данные теряются, если не успели записаться на диск.

После сбоя наша база становится рассогласованной. Какие-то страницы менялись и были записаны, другие не успели записаться.

Журнал предварительной записи (WAL) — механизм, которым позволит нам восстановить согласованность данных после сбоя.

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

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

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

WAL защищает всё что попадает в буферный кэш:

  • страницы таблиц, индексов;
  • статусы транзакций.

WAL не защищает:

  • временные таблицы;
  • нежурналируемые таблицы.

Производительность WAL

По умолчанию, каждый раз, когда транзакция фиксирует изменения, результат должен быть сброшен на диск. Для этого вначале страница сбрасывается из буферной памяти на дисковый кэш. А затем выполняется операция fsync для записи страницы на диск. То есть частые COMMIT приводят к частым fsync.

В PostgreSQL есть другой режим работы — асинхронный. При этом каждый COMMIT не приводит к fsync. Вместо этого страницы сбрасываются на диск по расписанию специальным процессом — WALWRITER. Этот процесс периодически просыпается и записывает на диск всё что изменилось за время пока он спал и опять засыпает.

В асинхронном режиме postgresql работает быстрее, но вы можете потерять некоторые данные при сбое.

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

Следующие параметры отвечают за режим работы WAL:

  • synchronous_commit — on/off — синхронный или асинхронный режим работы;
  • wal_writer_delay = 200ms — период сброса на диск wal записей при асинхронном режиме.

Контрольная точка

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

Сброс данных при контрольной точке не проходит моментально, это бы сильно нагрузило наш сервер. Чтобы не было пиковых нагрузок сброс идет примерно 50% времени от времени между контрольными точками. Например, контрольные точки делаются каждую минуту, тогда сброс осуществляют плавно в течении 30 секунд. Это регулируется и вы можете установить например 90% времени.

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

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

  • checkpoint_timeout — период выполнения контрольных точек в секундах;
  • max_wal_size — максимальный размер, до которого может вырастать WAL во время автоматических контрольных точек;
  • checkpoint_completion_target — время для завершения процедуры контрольной точки, как коэффициент для общего времени между контрольными точками. По умолчанию это значение равно 0.5. 

Значения по умолчанию: 5 минут и 1 Гбайт, соответственно. Если после предыдущей контрольной точки новые записи WAL не добавились, следующие контрольные точки будут пропущены, даже если проходит время checkpoint_timeout. Также можно выполнить контрольную точку принудительно, воспользовавшись SQL-командой CHECKPOINT.

Уменьшение значений checkpoint_timeout и max_wal_size приводит к учащению контрольных точек. Но появляется дополнительная нагрузка на сервер.

Процессы, связанные с буферным кэшем и журналом

  • WALWRITER — процесс записи журнала, обеспечивает асинхронный режим.
  • CHECPOINT — процесс контрольной точки.
  • BGWRITER — смотрит какие страницы скоро будут вытеснены из буферного кэша и скидывает грязные страницы на диск, делая их чистыми.
  • BACKEND — обслуживающий процесс с которым работает приложение. При чтении из кэша иногда нужно вначале вытеснить одну страницу и поместить туда другую. Так как BGWRITER записывает такие страницы заранее, то BACKEND уже видит чистую страницу и без записи её на диск меняет на другую. Но если BGWRITER не успеет, то на себя эту работу возьмет BACKEND.

Уровни журналов

  • Minimal — гарантия восстановления после сбоя.
  • Replica (по умолчанию) — используется для резервного копирования и для репликации.
  • Logical — используется для логической репликации.

Практика

В журнале WAL каждая запись имеет номер LSN (Log Sequence Number). С помощью функции pg_current_wal_lsn() можно посмотреть номер текущей записи:

postgres@s-pg13:~$ psql
Timing is on.
psql (13.3)
Type "help" for help.

postgres@postgres=# SELECT pg_current_wal_lsn();
 pg_current_wal_lsn
--------------------
 0/17BD9A0
(1 row)

Time: 1,602 ms

Запомним номер текущей записи WAL в переменной «pos1», затем создадим табличку с 1000 строк и опять сохраним текущий LSN в переменной «pos2». Затем посмотрим разницу между этими числами, но в формате LSN, и получим число байт записанных в WAL при создании таблички с 1000 строками:

postgres@postgres=# SELECT pg_current_wal_lsn() AS pos1 \gset
Time: 0,224 ms

postgres@postgres=# CREATE TABLE t(n integer);
CREATE TABLE
Time: 2,113 ms

postgres@postgres=# INSERT INTO t SELECT gen.id FROM generate_series(1,1000) AS gen(id);
INSERT 0 1000
Time: 2,242 ms

postgres@postgres=# SELECT pg_current_wal_lsn() AS pos2 \gset
Time: 0,179 ms

postgres@postgres=# SELECT :'pos2'::pg_lsn - :'pos1'::pg_lsn;
 ?column?
----------
   138968
(1 row)

Time: 1,193 ms

У нас вышло 138 KB! Так много получилось из за создания таблички, создание 1000 строк почти не нагрузит WAL.

Физически журнал хранится в файлах по 16 МБ в отдельном каталоге (PGDATA/pg_wal). На журнал можно взглянуть не только из файловой системы, но и с помощью функцию pg_ls_waldir():

postgres@postgres=# SELECT * FROM pg_ls_waldir();
           name           |   size   |      modification
--------------------------+----------+------------------------
 000000010000000000000001 | 16777216 | 2021-06-25 16:28:49+03
(1 row)

Time: 8,770 ms

Посмотреть на выполняемые процессы сервера postgres можно так:

postgres@postgres=# \q

postgres@s-pg13:~$ ps -o pid,command --ppid `head -n 1 $PGDATA/postmaster.pid`
  PID COMMAND
24122 postgres: checkpointer
24123 postgres: background writer
24124 postgres: walwriter
24125 postgres: autovacuum launcher
24126 postgres: stats collector
24127 postgres: logical replication launcher

К процессам, обслуживающим буферный кэш и журнал, можно отнести:

  • checkpointer;
  • background writer;
  • walwriter;

Теперь давайте остановим сервер корректно и посмотрим в лог файл:

postgres@s-pg13:~$ rm /home/postgres/logfile

postgres@s-pg13:~$ pg_ctl -w -l /home/postgres/logfile -D /usr/local/pgsql/data restart
waiting for server to shut down.... done
server stopped
waiting for server to start.... done
server started

postgres@s-pg13:~$ cat /home/postgres/logfile
2021-06-25 15:31:18.747 MSK [29370] LOG:  starting PostgreSQL 13.3 on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
2021-06-25 15:31:18.747 MSK [29370] LOG:  listening on IPv6 address "::1", port 5432
2021-06-25 15:31:18.747 MSK [29370] LOG:  listening on IPv4 address "127.0.0.1", port 5432
2021-06-25 15:31:18.747 MSK [29370] LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
2021-06-25 15:31:18.748 MSK [29372] LOG:  database system was shut down at 2021-06-25 15:31:18 MSK
2021-06-25 15:31:18.749 MSK [29370] LOG:  database system is ready to accept connections

Как видим сервер просто открыл соединения на сокете и tcp порту 5432 и начал работу.

Теперь завершим работу сервера некорректно, используя опцию immediate:

postgres@s-pg13:~$ rm /home/postgres/logfile

postgres@s-pg13:~$ pg_ctl -w -D /usr/local/pgsql/data stop -m immediate
waiting for server to shut down.... done
server stopped

postgres@s-pg13:~$ pg_ctl -w -l /home/postgres/logfile -D /usr/local/pgsql/data start
waiting for server to start.... done
server started

postgres@s-pg13:~$ cat /home/postgres/logfile
2021-06-25 15:32:37.988 MSK [29389] LOG:  starting PostgreSQL 13.3 on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
2021-06-25 15:32:37.988 MSK [29389] LOG:  listening on IPv6 address "::1", port 5432
2021-06-25 15:32:37.988 MSK [29389] LOG:  listening on IPv4 address "127.0.0.1", port 5432
2021-06-25 15:32:37.989 MSK [29389] LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
2021-06-25 15:32:37.991 MSK [29391] LOG:  database system was interrupted; last known up at 2021-06-25 15:31:18 MSK
2021-06-25 15:32:38.006 MSK [29391] LOG:  database system was not properly shut down; automatic recovery in progress
2021-06-25 15:32:38.009 MSK [29391] LOG:  redo starts at 0/17E3938
2021-06-25 15:32:38.009 MSK [29391] LOG:  invalid record length at 0/17E59D8: wanted 24, got 0
2021-06-25 15:32:38.009 MSK [29391] LOG:  redo done at 0/17E59A0
2021-06-25 15:32:38.013 MSK [29389] LOG:  database system is ready to accept connections

Как видим журнал изменился! Перед тем, как начать принимать соединения, СУБД выполнила восстановление (automatic recovery in progress).


Сводка
Буферный кэш и журнал WAL в PostgreSQL
Имя статьи
Буферный кэш и журнал WAL в PostgreSQL
Описание
Рассмотрим технологии обеспечивающие высокую производительность и безопасность работы сервера PostgreSQL, а именно "Буферный кэш" и "Журнал предзаписи WAL"

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *