]> Untitled Git - articles.git/blob
2274768b310bdea345dd0b22a886ac9172d7144f
[articles.git] /
1 ## PostgreSQL
2
3 PostgreSQL -- мощная современная СУБД со свободным и открытым исходным кодом.
4 В отличие от предыдущих двух исследуемых проектов, то, как устроен внутри себя PostgreSQL, я знаю не понаслышке.
5 Поэтому у нас будет возможность сравнить результаты которые покажет Natch с фактическим знанием о том, как распространяются данные через подсистемы проекта.
6
7 В PostgreSQL поддерживаются различные сложные, составные, типы данных.
8 В рамках данного исследования предлагается взять строковое представление одного их таких типов данных и проследить как строковые константы из этого типа перейдут из текстового представление в представление внутреннее и как это внутреннее представление будет сохранено на диск.
9
10 ### План эксперимента
11
12 * Соберем PostgreSQL с отладочной информацией;
13 * В качестве исследуемого типа данных, выберем тип данных [`tsvector`](https://www.postgresql.org/docs/current/datatype-textsearch.html), так как он с одной стороны содержит в себе набор отдельных строковых констант, за которыми будет удобно наблюдать в режиме помеченных данных, а с другой стороны тип данных сам по себе не является излишне сложным;
14 * Для проведения эксперимента сохраним строковое представление константы типа `tsvector` в отдельный файл. Данные из этого файла в нашем эксперименте Natch будет отслеживать как "помеченные";
15 * Напишем несложный скрипт заворачивающий строку из вышеупомянутого файла в SQL-запрос осуществляющий вставку константы типа `tsvector` в ранее созданную таблицу;
16 * Под контролем Natch, запустим вышеупомянутый скрипт, осуществим вставку, после чего дадим команду `CHECKPOINT`, чтобы принудить postgres сбросить все закешированные страницы хранилища на диск.
17
18 ### Сборка и установка
19
20
21 Перед началом работы убеждаемся что системный PostgreSQL остановлен:
22
23 ```bash
24 $ sudo service postgresql stop
25 ```
26
27
28 #### Установка зависимостей
29
30 Как и в пошлый раз установим зависимости используя зависимости сборки пакета `postgres` из debain и добавим несколько пакетов которые необходимы при сборке dev-ветки.
31
32
33 ```bash
34 $ sudo apt-get build-dep postgresql
35 $ sudo apt-get install libicu-dev bison flex libreadline-dev zlib1g-dev
36 ```
37
38 #### Получение исходников
39
40 Для проведения эксперимента используем текущую стабильную ветку PostgreSQL 17.
41 ```bash
42 $ git clone  https://git.postgresql.org/git/postgresql.git -b REL_17_STABLE ~/postgres/REL_17_STABLE
43 ```
44 На момент проведения эксперимента, последний минорный релиз ветки был 17.4, векта находилась на коммите `03faf38`.
45
46
47 #### Сборка и установка
48 ```bash
49 cd ~/postgres/REL_17_STABLE
50 $ ./configure --enable-debug --prefix=$HOME/postgres/.install/REL_17_STABLE
51 $ make -j8
52 $ make install
53 ```
54
55 #### Создание управляющих скриптов
56
57 Скрипты для запуска и подключения к базе данных размещаем в директории `~/postgres/.install/REL_17_STABLE-scripts`
58
59 `base._sh` - скрипт определяющий глобальные переменные:
60
61 ```bash
62 export MY_PG_BIN=$HOME/postgres/.install/REL_17_STABLE/bin
63 export MY_PG_DATA=$HOME/postgres/.install/REL_17_STABLE-data
64 ```
65
66 `initdb.sh` - скрипт для инициализации кластера БД:
67
68 ```bash
69 #!/bin/sh
70
71 source ./base._sh
72
73 rm -r $MY_PG_DATA
74
75 $MY_PG_BIN/initdb -D $MY_PG_DATA
76
77 ```
78
79 `restart.sh` - скрипт для запуска/перезапуска нашего экземпляра PostgreSQL
80
81 ```bash
82 #!/bin/bash
83
84 pkill postgres
85 sleep 2
86
87 source ./base._sh
88
89 $MY_PG_BIN/pg_ctl start -D $MY_PG_DATA -l $MY_PG_DATA/log
90 ```
91
92 `psql.sh` - скрипт для подключения к БД через коммандно-строчный интерфейс psql
93
94 ```bash
95 #!/bin/bash
96
97 source ./base._sh
98
99 $MY_PG_BIN/psql postgres
100 ```
101
102 `psql_con.sh` - скрипт перенаправляющий поток стандартного вывода на вход нашего psql
103
104 ```
105 #!/bin/bash
106
107 source ./base._sh
108
109 cat - | $MY_PG_BIN/psql postgres
110 ```
111
112 #### Инициализация кластера БД и создание тестовой таблицы
113
114 При помощи скрипта `initdb.sh` создаем кластер БД:
115
116 ```bash
117 $ cd ~/postgres/.install/REL_17_STABLE-scripts
118 $ ./initdb.sh
119 ```
120
121 Запускаем СУБД
122
123 ```bash
124 $ ./restart.sh
125 ```
126
127 далее подключаемся к базе данных с помощь скрипта `psql.sh`
128
129 ```bash
130 $ ./psql.sh
131 ```
132
133 создаем таблицу для вставки помеченных данных
134
135 ```sql
136 create table test (t tsvector);
137 ```
138
139 выходим из psql
140
141 ```sql
142 exit
143 ```
144
145 ### Подготовка помеченных данных
146
147 В качестве основы для наших помеченных данных, возьмем основной [пример из документации](https://www.postgresql.org/docs/current/datatype-textsearch.html) расширив его дополнительными синтаксическими конструкциями описанными ниже по тексту документации.
148 В результате получим файл вида
149
150 ```
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
152 ```
153
154 #### Тестирование вставки тестовых данных
155
156 Перед записью трассы следует убедиться, что мы умеем правильно вставлять в таблицу данные из файла IN.
157 Вставку предполагается делать командой
158
159 ```
160 $ cd ~/postgres/.install/REL_17_STABLE-scripts
161 $ echo INSERT INTO test VALUES \( \'`cat IN`\'::tsvector\)\; CHECKPOINT | ./psql_con.sh
162
163 ```
164
165 Как вы можете видеть в этой команде на стандартный вывод печатается SQL-запрос вставки, в который добавляется содержимое файла с помеченными данными.
166 За запросом вставки следует команда `CHECKPOINT`, которая инициирует принудительный сброс на диск кэша страниц хранилища PostgreSQL.
167 Полученная строка направляется в `psql`.
168 Таким образом помеченными оказываются только данные из файла `IN`, но не сам текст запроса.
169
170 Убедитесь, что команда отрабатывает без ошибок и данные действительно попадают в таблицу `test`
171
172 #### Очистка окружения перед проведением эксперимента
173
174 Перед проведением эксперимента таблицу `test` рекомендуется очистить от старых данных, чтобы они не смущали нас своим видом.
175
176 ```sql
177 DELETE FROM test;
178 ```
179
180 ### Проведение исследования
181
182 #### Создание проекта natch
183
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/`
188
189 #### Запись трассы
190
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.
203
204 #### Генерация отчета
205
206 Далее следуя штатной инструкции при помощи команды `natch replay` снимаем трассу между первым и вторым снепшотом и загружаем ее в snatch.
207
208 ### Анализ результатов
209
210 #### Ввод/вывод помеченных данных
211
212 Помеченные данные должны через стандартный ввод попасть в `psql`, после чего попасть в бэкенд PostgreSQL через unix-сокет, через который `psql` по умолчанию подключается к базе.
213 После вставки мы не перезагружали сервер, данные все еще будут закешированы, и следовательно останутся помеченными.
214 Поэтому запросив данные из таблицы мы по факту должны получить значение из страничного кеша, которое все еще сохранит пометку.
215
216 Посмотрим как это выглядит на практике
217
218 ##### Ввод `psql`
219
220 ![...](3. PostgreSQL.imgs/in_psql_resources.png)
221
222 На картинке выше мы видим, что процесс `psql` получил помеченные данные из сокета `pipe` (это наше перенаправление вывода) и произвел какие-то операции с сокетом `unix` (видимо передал данные в backend).
223 Посмотрим на содержание этих обменов.
224
225 В `pipe` мы видим наш SQL запрос содержащий помеченные данные
226
227 ![...](3. PostgreSQL.imgs/in_psql_pipe-read.png)
228
229 В `unix` сокете мы видим как тот же самый запрос с помеченными данными отправляется на запись в сокет, через который `psql` подключен к PostgreSQL 
230
231 ![...](3. PostgreSQL.imgs/in_psql_unix-write.png)
232
233
234 #### Ввод `postgres`
235
236 В процессе получения запроса вставки принимает участие и процесс `postgresql`
237
238 ![...](3. PostgreSQL.imgs/in_postgres_resources.png)
239
240 На картинке выше мы видим что процесс обменивается помеченными данными через unix сокет (спойлер: он их чиатет) и кроме этого производит запись этих данных на диск (об этом ниже).
241
242 Данные принимаемые процессом `postgres` полностью (что неудивительно) совпадают с данными которые отправлял процесс `psql`, с точностью до направления движения данных.
243 То что `psql` писал, `postgres` читает.
244
245 ![...](3. PostgreSQL.imgs/in_postgres_unix-read.png)
246
247 #### Вывод помеченных данных
248
249 Вывод в нашем случае работает похожим образом что и ввод.
250 `psql` получает запрос из эмуляротра терминала, пренаправляет запрос бэкенду `postgres` через unix-сокет, получает обратно ответ и выводит его в консоль.
251
252 ##### Вывод `psql`
253
254 ![...](3. PostgreSQL.imgs/out_psql_resources.png)
255
256 На этом скриншоте мы видим что помеченные данные проходят через unix-сокет и устройство виртуального терминала
257
258 Ниже мы видим как происходит обмен данных через unix socket.
259 Видим отправленный запрос `select` и до этого более ранний `checkpoint`.
260 Видим как `psql` получает через сокет помеченные данные в ответ на отправленный запрос запрос
261
262 ![...](3. PostgreSQL.imgs/out_psql_unix-read.png)
263
264 Далее мы видим как помеченные данные полученные от backend'а в человекочитаемом виде отправляются на консоль:
265
266 ![...](3. PostgreSQL.imgs/out_psql_tty1-write.png)
267
268 ##### Вывод `postgres`
269
270 В процессе `postgres` обмен данными через unix-сокет ожидаемо происходит полностью аналогично с `pslq`, с точностью до направления движения данных:
271
272 ![...](3. PostgreSQL.imgs/out_postgres_resources.png)
273
274 ![...](3. PostgreSQL.imgs/out_postgres_unix-write.png)
275
276 #### Запись на диск
277
278 С точки зрения операций чтения/записи помеченные данные должны быть записаны на диск дважды.
279 Первый раз в момент фиксации изменений, вносимая правка должна быть записана в т.н. Write Ahead Log (WAL).
280 WAL -- файл содержащий все последние изменения данных в базе, по которому в случае аварии данные базы могут быть восстановлены до актуального состояние.
281 Второй раз, данные будут записаны в хранилище в котором они будут храниться на постоянной основе.
282 Гарантированной записи в хранилище мы и добивались передавая базе данных команду `CHECKPOINT`.
283
284 ##### Запись в Write Ahead Log
285
286 Запись WAL  происходит в процессе выполнения транзакции.
287 Поэтому запись в WAL мы уже видели в том процессе `postgres` который принимал запрос на вставку помеченных данных:
288
289 ![...](3. PostgreSQL.imgs/in_postgres_resources.png)
290
291 Можем теперь посмотреть на то, какие данные были в него записаны, и как в этом случае выглядят наши помеченные данные:
292
293 ![...](3. PostgreSQL.imgs/disk_wal.png)
294
295 Мы явно видим помеченные данные, теперь уже во внутреннем представлении.
296 Строковые константы перемешаны и местами визуально склеены.
297 Так же мы видим, что в записанном образце WAL-лога внутреннее представление вставляемых данных повторено несколько раз, при этом помеченным оказывается один комплект.
298
299 Излишне экземпляры в логе происходят из-за того, что мы были не аккуратны при проведении эксперимента, и производили вставку в не "девственную" базу в которой мы до этого создавали и удаляли аналогичные записи.
300 Записи удаленные с точки зрения MVCC, с точки зрения хранилища данных продолжают существовать, требуют обслуживания и все эти действия отражены в логах операций чтения и записи.
301 При этом следует обратить внимание, что помеченным остается только один экземпляр, только что вставленный, а следы оставшиеся от предыдущих вставок остаются не помеченными.
302
303
304 ##### Запись в основное хранилище.
305
306 Финальная запись в основное хранилище происходит в отдельном процессе.
307 Этот процесс так же как и другие сервисные процессы (воркеры) является форком основного процесса `postgres`, только он выполняет особую роль и называется в соответствии с этой ролью checkpointer.
308 Поэтому в результатах нашего анализа запись в основное хранилище происходит в отдельном процессе так же именуемом `postgres`, но мы знаем, что он судя по всему является воркером checkpointer.
309
310 ![...](3. PostgreSQL.imgs/disk_checkpointer_resources.png)
311
312 Здесь мы видим, что запись помеченных данных происходит в файл `/base/5/16384`, это один из файлов основного хранилища.
313 Если мы загляним внутрь, то увидим следующую картину:
314
315 ![...](3. PostgreSQL.imgs/disk_checkpointer_file.png)
316
317 Здесь мы во-первых вдимем внутреннее представление вставленного tsvector'а, данные которого отмечены как помеченные.
318 Во вторых мы наблюдаем еще один экземпляр наших данных уже не помеченный.
319 Этот экземпляр остался от предыдущих экспериментов и попал в записанные данные так как страница была сброшена из памяти на диск целиком.
320 Можно так же обратить внимание на то, что "старый" экземпляр находится после нового.
321 Это происходит от того, что в PostgreSQL записи добавляются на страницу с конца, это штатное, ожидаемое поведение.
322
323 #### Стек вызовов
324
325 Далее рассмотрим как помеченные данные передаются внутри процессов.
326 Процесс `psql` мы из рассмотрения исключим, с формулировкой "это тема для отдельного исследования".
327 Если посмотреть на историю стека вызовов, то становиться видно, что с помеченными данными происходит много манипуляций вроде копирования и какой-то валидации.
328 Однако как-то содержательно прокомментировать происходящее я не в состоянии, поэтому оставим эту часть реальности за скобками.
329 Поэтому сосредоточимся на том, что происходит с помеченными данными в процессе `postgresql` при добавлении новой записи, и при выводе содержимого таблицы с помеченными данными на экран.
330
331 ##### Стек вызовов процесса `postgres`, ввод
332
333 История стека вызовов `postgres` при вставке помеченных данных в таблицу делится на три принципиальные части.
334
335 В первой части происходит общий "парсинг" запроса вставки:
336
337 ![...](3. PostgreSQL.imgs/callstack_in_1.png)
338
339 На этом скриншоте мы видим как помеченные данные попадают в backed вместе с запросом, проходят предварительную проверку и после чего производится синтаксический разбор запроса.
340 Сами помеченные данные при этом никак отдельно не обрабатываются, только туда-сюда копируются и принимают участие в других групповых операциях.
341
342 За синтаксическим разбором следует разбор константных аргументов входящих в состав запроса.
343 Это как раз то место в котором наши помеченные данные будут парситься.
344
345 ![...](3. PostgreSQL.imgs/callstack_in_2.png)
346
347 И действительно, в глубине стека вызовов мы наблюдаем функцию `tsvectorin`, которая занимается переводом констант типа `tsvector` из внешнего, текстового, представления в представление в котором она будет обрабатываться внутри СУБД.
348 Внутри этой функции происходит какая-то достаточно развесистая обработка, тип данных-то сложный.
349
350 Далее идет блок вызовов посвященный выполнению запроса:
351
352 ![...](3. PostgreSQL.imgs/callstack_in_3.png)
353
354 В первой подветке идет формирование записи (`heap_fill_tuple`).
355 Помеченные данные судя по стеку вызовов копируются куда-то в формируемую запись.
356
357 Во второй подветке производится вставка сформированной записи в таблицу (`heapam_tuple_insert`).
358 Тут тоже помеченные данные просто копируются из одного места в другое
359
360 И в третьей, последней подветке происходит запись произведенных изменений в WAL-лог (`XLogInsert`).
361 Помеченные данные принимают участие в вычислении контрольной суммы, и копируются вместе со всей остальнойдельтой изменений произошедших во время транзакции.
362
363 ##### Стек вызовов процесса `postgres`, вывод
364
365 Вывод помеченных данных из базы данных обратно в терминал производится в отдельном backend-процессе (мы для вывода установили нов:ое соединение и оно обслуживается новым процессом)
366
367 ![...](3. PostgreSQL.imgs/callstack_out.png)
368
369 Согласно дереву вызовов чтение из БД устроено заметно проще чем запись.
370 На скриншоте мы наблюдаем ту же последовательность обработки входящего запроса, что была выше, только в этом случае при обработке этого запроса работы с помеченным данными не происходит (помеченные данные уже в хранилище БД, а не в запросе).
371 Помеченные данные появляются в нашем графе вызовов только в момент их приведение к человекочитаемому виду в функции `tsvectorout`.
372 Далее за преобразованием следует отправка полученного результата в сокет, на стандартный вывод.
373
374 Глядя на это дерево вызовов, стоит отметить крайне эффективное использование данных их страничного кэша.
375 Ни на одном этапе выполнения запроса выводимые данные во внутреннем представлении никуда не копируются, `tsvectorout` судя по всему принимает на вход значение прямо из страничного кэша, без каких либо предварительных перемещений данных в памяти.
376
377 ### Итог
378
379 Не смотря на то, что эксперимент был проведен не очень чисто, и наши данные оказались замусорены артефактами от предыдущих запусков, нам удалось отследить маршрут движения помеченных данных данных, и наблюдаемая картина совпала с нашими теоретическими представлениями.
380
381 В качестве развития темы читателю предлагается во-первых повторить предложенный эксперимент полностью пересоздав кластер базы данных перед проведением эксперимента, и убедиться что мы в этом случае артефакты предыдущих вставок в отчете natch не наблюдаются.
382
383 Так же читателю предлагается повторить эксперимент с перезагрузкой базы данных между вставкой помеченных данных и их получением.
384 Предположительно, при прохождении через дисковое хранилище данные должны утратить пометку.
385 Однако в случае, если они останутся в дисковом кеше уровня ОС, пометка может и сохраниться.
386 Интересно будет выяснить каково будет фактическое поведение, и как поставить эксперимент, чтобы дисковый кеш тоже оказался бы гарантированно сброшен. 
387
388
389 **FIXME**
390 sudo su
391 echo "deb-src http://deb.debian.org/debian trixie main" >>/etc/apt/sources.list
392 apt-get update
393