3 PostgreSQL -- мощная современная СУБД со свободным и открытым исходным кодом.
4 В отличие от предыдущих двух исследуемых проектов, то, как устроен внутри себя PostgreSQL, я знаю не понаслышке.
5 Поэтому у нас будет возможность сравнить результаты которые покажет Natch с фактическим знанием о том, как распространяются данные через подсистемы проекта.
7 В PostgreSQL поддерживаются различные сложные, составные, типы данных.
8 В рамках данного исследования предлагается взять строковое представление одного их таких типов данных и проследить как строковые константы из этого типа перейдут из текстового представление в представление внутреннее и как это внутреннее представление будет сохранено на диск.
12 * Соберем PostgreSQL с отладочной информацией;
13 * В качестве исследуемого типа данных, выберем тип данных [`tsvector`](https://www.postgresql.org/docs/current/datatype-textsearch.html), так как он с одной стороны содержит в себе набор отдельных строковых констант, за которыми будет удобно наблюдать в режиме помеченных данных, а с другой стороны тип данных сам по себе не является излишне сложным;
14 * Для проведения эксперимента сохраним строковое представление константы типа `tsvector` в отдельный файл. Данные из этого файла в нашем эксперименте Natch будет отслеживать как "помеченные";
15 * Напишем несложный скрипт заворачивающий строку из вышеупомянутого файла в SQL-запрос осуществляющий вставку константы типа `tsvector` в ранее созданную таблицу;
16 * Под контролем Natch, запустим вышеупомянутый скрипт, осуществим вставку, после чего дадим команду `CHECKPOINT`, чтобы принудить postgres сбросить все закешированные страницы хранилища на диск.
18 ### Сборка и установка
21 Перед началом работы убеждаемся что системный PostgreSQL остановлен:
24 $ sudo service postgresql stop
28 #### Установка зависимостей
30 Как и в пошлый раз установим зависимости используя зависимости сборки пакета `postgres` из debain и добавим несколько пакетов которые необходимы при сборке dev-ветки.
34 $ sudo apt-get build-dep postgresql
35 $ sudo apt-get install libicu-dev bison flex libreadline-dev zlib1g-dev
38 #### Получение исходников
40 Для проведения эксперимента используем текущую стабильную ветку PostgreSQL 17.
42 $ git clone https://git.postgresql.org/git/postgresql.git -b REL_17_STABLE ~/postgres/REL_17_STABLE
44 На момент проведения эксперимента, последний минорный релиз ветки был 17.4, векта находилась на коммите `03faf38`.
47 #### Сборка и установка
49 cd ~/postgres/REL_17_STABLE
50 $ ./configure --enable-debug --prefix=$HOME/postgres/.install/REL_17_STABLE
55 #### Создание управляющих скриптов
57 Скрипты для запуска и подключения к базе данных размещаем в директории `~/postgres/.install/REL_17_STABLE-scripts`
59 `base._sh` - скрипт определяющий глобальные переменные:
62 export MY_PG_BIN=$HOME/postgres/.install/REL_17_STABLE/bin
63 export MY_PG_DATA=$HOME/postgres/.install/REL_17_STABLE-data
66 `initdb.sh` - скрипт для инициализации кластера БД:
75 $MY_PG_BIN/initdb -D $MY_PG_DATA
79 `restart.sh` - скрипт для запуска/перезапуска нашего экземпляра PostgreSQL
89 $MY_PG_BIN/pg_ctl start -D $MY_PG_DATA -l $MY_PG_DATA/log
92 `psql.sh` - скрипт для подключения к БД через коммандно-строчный интерфейс psql
99 $MY_PG_BIN/psql postgres
102 `psql_con.sh` - скрипт перенаправляющий поток стандартного вывода на вход нашего psql
109 cat - | $MY_PG_BIN/psql postgres
112 #### Инициализация кластера БД и создание тестовой таблицы
114 При помощи скрипта `initdb.sh` создаем кластер БД:
117 $ cd ~/postgres/.install/REL_17_STABLE-scripts
127 далее подключаемся к базе данных с помощь скрипта `psql.sh`
133 создаем таблицу для вставки помеченных данных
136 create table test (t tsvector);
145 ### Подготовка помеченных данных
147 В качестве основы для наших помеченных данных, возьмем основной [пример из документации](https://www.postgresql.org/docs/current/datatype-textsearch.html) расширив его дополнительными синтаксическими конструкциями описанными ниже по тексту документации.
148 В результате получим файл вида
151 a:1A fat:2B,4C cat:5D sat:4 on:5 a:6 mat:7 and:8 ate:9 a:10 fat:11 rat:12
154 #### Тестирование вставки тестовых данных
156 Перед записью трассы следует убедиться, что мы умеем правильно вставлять в таблицу данные из файла IN.
157 Вставку предполагается делать командой
160 $ cd ~/postgres/.install/REL_17_STABLE-scripts
161 $ echo INSERT INTO test VALUES \( \'`cat IN`\'::tsvector\)\; CHECKPOINT | ./psql_con.sh
165 Как вы можете видеть в этой команде на стандартный вывод печатается SQL-запрос вставки, в который добавляется содержимое файла с помеченными данными.
166 За запросом вставки следует команда `CHECKPOINT`, которая инициирует принудительный сброс на диск кэша страниц хранилища PostgreSQL.
167 Полученная строка направляется в `psql`.
168 Таким образом помеченными оказываются только данные из файла `IN`, но не сам текст запроса.
170 Убедитесь, что команда отрабатывает без ошибок и данные действительно попадают в таблицу `test`
172 #### Очистка окружения перед проведением эксперимента
174 Перед проведением эксперимента таблицу `test` рекомендуется очистить от старых данных, чтобы они не смущали нас своим видом.
180 ### Проведение исследования
182 #### Создание проекта natch
184 При создании проекта natch, уже на host-машине следуем штатной инструкции.
185 В качестве источника помеченных данных указываем файл `IN` о котором мы говорили выше.
186 В моем случае `/home/nataraj/postgres/.install/REL_17_STABLE-scripts/IN`.
187 В качестве директории содержащий дополнительные модули с отладочной информацией указать место в которое были установлены бинарные файлы нашей сборки PostgreSQL: `/home/nataraj/postgres/.install/REL_17_STABLE/bin` и `/home/nataraj/postgres/.install/REL_17_STABLE/lib/`
191 - Запускаем штатным образом запись трассы используя команду `natch record`;
192 - В появившемся окне виртуальной машины логинимся в виртуалку;
193 - Останавливаем системный PostgreSQL: `sudo service postgresql stop`;
194 - Переходим в каталог с нашей скриптовой обвязкой: `cd ~/postgres/.install/REL_17_STABLE-scripts`;
195 - Запускаем нашу сборку PostgreSQL: `./restart.sh`;
196 - В консоли Natch делаем первый снапшот (см. штатный tutorial);
197 - Запускаем команду вставки помеченных данных в базу: ``echo INSERT INTO test VALUES \( \'`cat IN`\'::tsvector\) | ./psql_con.sh``
198 - Запускаем утилиту `psql` в командно-строчном режиме: `./psql.sh`;
199 - Даем в ней команду `CHECKPOINT;` для гарантированного сброса страничного кеша на диск;
200 - Получаем содержимое таблицы `test` выполнив запрос: `SELECT * FROM test;`;
201 - В консоли Natch делаем второй снапшот;
202 - Завершаем работу Natch.
204 #### Генерация отчета
206 Далее следуя штатной инструкции при помощи команды `natch replay` снимаем трассу между первым и вторым снепшотом и загружаем ее в snatch.
208 ### Анализ результатов
210 #### Ввод/вывод помеченных данных
212 Помеченные данные должны через стандартный ввод попасть в `psql`, после чего попасть в бэкенд PostgreSQL через unix-сокет, через который `psql` по умолчанию подключается к базе.
213 После вставки мы не перезагружали сервер, данные все еще будут закешированы, и следовательно останутся помеченными.
214 Поэтому запросив данные из таблицы мы по факту должны получить значение из страничного кеша, которое все еще сохранит пометку.
216 Посмотрим как это выглядит на практике
220 
222 На картинке выше мы видим, что процесс `psql` получил помеченные данные из сокета `pipe` (это наше перенаправление вывода) и произвел какие-то операции с сокетом `unix` (видимо передал данные в backend).
223 Посмотрим на содержание этих обменов.
225 В `pipe` мы видим наш SQL запрос содержащий помеченные данные
227 
229 В `unix` сокете мы видим как тот же самый запрос с помеченными данными отправляется на запись в сокет, через который `psql` подключен к PostgreSQL
231 
236 В процессе получения запроса вставки принимает участие и процесс `postgresql`
238 
240 На картинке выше мы видим что процесс обменивается помеченными данными через unix сокет (спойлер: он их чиатет) и кроме этого производит запись этих данных на диск (об этом ниже).
242 Данные принимаемые процессом `postgres` полностью (что неудивительно) совпадают с данными которые отправлял процесс `psql`, с точностью до направления движения данных.
243 То что `psql` писал, `postgres` читает.
245 
247 #### Вывод помеченных данных
249 Вывод в нашем случае работает похожим образом что и ввод.
250 `psql` получает запрос из эмуляротра терминала, пренаправляет запрос бэкенду `postgres` через unix-сокет, получает обратно ответ и выводит его в консоль.
254 
256 На этом скриншоте мы видим что помеченные данные проходят через unix-сокет и устройство виртуального терминала
258 Ниже мы видим как происходит обмен данных через unix socket.
259 Видим отправленный запрос `select` и до этого более ранний `checkpoint`.
260 Видим как `psql` получает через сокет помеченные данные в ответ на отправленный запрос запрос
262 
264 Далее мы видим как помеченные данные полученные от backend'а в человекочитаемом виде отправляются на консоль:
266 
268 ##### Вывод `postgres`
270 В процессе `postgres` обмен данными через unix-сокет ожидаемо происходит полностью аналогично с `pslq`, с точностью до направления движения данных:
272 
274 
278 С точки зрения операций чтения/записи помеченные данные должны быть записаны на диск дважды.
279 Первый раз в момент фиксации изменений, вносимая правка должна быть записана в т.н. Write Ahead Log (WAL).
280 WAL -- файл содержащий все последние изменения данных в базе, по которому в случае аварии данные базы могут быть восстановлены до актуального состояние.
281 Второй раз, данные будут записаны в хранилище в котором они будут храниться на постоянной основе.
282 Гарантированной записи в хранилище мы и добивались передавая базе данных команду `CHECKPOINT`.
284 ##### Запись в Write Ahead Log
286 Запись WAL происходит в процессе выполнения транзакции.
287 Поэтому запись в WAL мы уже видели в том процессе `postgres` который принимал запрос на вставку помеченных данных:
289 
291 Можем теперь посмотреть на то, какие данные были в него записаны, и как в этом случае выглядят наши помеченные данные:
293 
295 Мы явно видим помеченные данные, теперь уже во внутреннем представлении.
296 Строковые константы перемешаны и местами визуально склеены.
297 Так же мы видим, что в записанном образце WAL-лога внутреннее представление вставляемых данных повторено несколько раз, при этом помеченным оказывается один комплект.
299 Излишне экземпляры в логе происходят из-за того, что мы были не аккуратны при проведении эксперимента, и производили вставку в не "девственную" базу в которой мы до этого создавали и удаляли аналогичные записи.
300 Записи удаленные с точки зрения MVCC, с точки зрения хранилища данных продолжают существовать, требуют обслуживания и все эти действия отражены в логах операций чтения и записи.
301 При этом следует обратить внимание, что помеченным остается только один экземпляр, только что вставленный, а следы оставшиеся от предыдущих вставок остаются не помеченными.
304 ##### Запись в основное хранилище.
306 Финальная запись в основное хранилище происходит в отдельном процессе.
307 Этот процесс так же как и другие сервисные процессы (воркеры) является форком основного процесса `postgres`, только он выполняет особую роль и называется в соответствии с этой ролью checkpointer.
308 Поэтому в результатах нашего анализа запись в основное хранилище происходит в отдельном процессе так же именуемом `postgres`, но мы знаем, что он судя по всему является воркером checkpointer.
310 
312 Здесь мы видим, что запись помеченных данных происходит в файл `/base/5/16384`, это один из файлов основного хранилища.
313 Если мы загляним внутрь, то увидим следующую картину:
315 
317 Здесь мы во-первых вдимем внутреннее представление вставленного tsvector'а, данные которого отмечены как помеченные.
318 Во вторых мы наблюдаем еще один экземпляр наших данных уже не помеченный.
319 Этот экземпляр остался от предыдущих экспериментов и попал в записанные данные так как страница была сброшена из памяти на диск целиком.
320 Можно так же обратить внимание на то, что "старый" экземпляр находится после нового.
321 Это происходит от того, что в PostgreSQL записи добавляются на страницу с конца, это штатное, ожидаемое поведение.
325 Далее рассмотрим как помеченные данные передаются внутри процессов.
326 Процесс `psql` мы из рассмотрения исключим, с формулировкой "это тема для отдельного исследования".
327 Если посмотреть на историю стека вызовов, то становиться видно, что с помеченными данными происходит много манипуляций вроде копирования и какой-то валидации.
328 Однако как-то содержательно прокомментировать происходящее я не в состоянии, поэтому оставим эту часть реальности за скобками.
329 Поэтому сосредоточимся на том, что происходит с помеченными данными в процессе `postgresql` при добавлении новой записи, и при выводе содержимого таблицы с помеченными данными на экран.
331 ##### Стек вызовов процесса `postgres`, ввод
333 История стека вызовов `postgres` при вставке помеченных данных в таблицу делится на три принципиальные части.
335 В первой части происходит общий "парсинг" запроса вставки:
337 
339 На этом скриншоте мы видим как помеченные данные попадают в backed вместе с запросом, проходят предварительную проверку и после чего производится синтаксический разбор запроса.
340 Сами помеченные данные при этом никак отдельно не обрабатываются, только туда-сюда копируются и принимают участие в других групповых операциях.
342 За синтаксическим разбором следует разбор константных аргументов входящих в состав запроса.
343 Это как раз то место в котором наши помеченные данные будут парситься.
345 
347 И действительно, в глубине стека вызовов мы наблюдаем функцию `tsvectorin`, которая занимается переводом констант типа `tsvector` из внешнего, текстового, представления в представление в котором она будет обрабатываться внутри СУБД.
348 Внутри этой функции происходит какая-то достаточно развесистая обработка, тип данных-то сложный.
350 Далее идет блок вызовов посвященный выполнению запроса:
352 
354 В первой подветке идет формирование записи (`heap_fill_tuple`).
355 Помеченные данные судя по стеку вызовов копируются куда-то в формируемую запись.
357 Во второй подветке производится вставка сформированной записи в таблицу (`heapam_tuple_insert`).
358 Тут тоже помеченные данные просто копируются из одного места в другое
360 И в третьей, последней подветке происходит запись произведенных изменений в WAL-лог (`XLogInsert`).
361 Помеченные данные принимают участие в вычислении контрольной суммы, и копируются вместе со всей остальнойдельтой изменений произошедших во время транзакции.
363 ##### Стек вызовов процесса `postgres`, вывод
365 Вывод помеченных данных из базы данных обратно в терминал производится в отдельном backend-процессе (мы для вывода установили нов:ое соединение и оно обслуживается новым процессом)
367 
369 Согласно дереву вызовов чтение из БД устроено заметно проще чем запись.
370 На скриншоте мы наблюдаем ту же последовательность обработки входящего запроса, что была выше, только в этом случае при обработке этого запроса работы с помеченным данными не происходит (помеченные данные уже в хранилище БД, а не в запросе).
371 Помеченные данные появляются в нашем графе вызовов только в момент их приведение к человекочитаемому виду в функции `tsvectorout`.
372 Далее за преобразованием следует отправка полученного результата в сокет, на стандартный вывод.
374 Глядя на это дерево вызовов, стоит отметить крайне эффективное использование данных их страничного кэша.
375 Ни на одном этапе выполнения запроса выводимые данные во внутреннем представлении никуда не копируются, `tsvectorout` судя по всему принимает на вход значение прямо из страничного кэша, без каких либо предварительных перемещений данных в памяти.
379 Не смотря на то, что эксперимент был проведен не очень чисто, и наши данные оказались замусорены артефактами от предыдущих запусков, нам удалось отследить маршрут движения помеченных данных данных, и наблюдаемая картина совпала с нашими теоретическими представлениями.
381 В качестве развития темы читателю предлагается во-первых повторить предложенный эксперимент полностью пересоздав кластер базы данных перед проведением эксперимента, и убедиться что мы в этом случае артефакты предыдущих вставок в отчете natch не наблюдаются.
383 Так же читателю предлагается повторить эксперимент с перезагрузкой базы данных между вставкой помеченных данных и их получением.
384 Предположительно, при прохождении через дисковое хранилище данные должны утратить пометку.
385 Однако в случае, если они останутся в дисковом кеше уровня ОС, пометка может и сохраниться.
386 Интересно будет выяснить каково будет фактическое поведение, и как поставить эксперимент, чтобы дисковый кеш тоже оказался бы гарантированно сброшен.
391 echo "deb-src http://deb.debian.org/debian trixie main" >>/etc/apt/sources.list