Эшу быдлокодит
297 subscribers
135 photos
12 videos
7 files
170 links
Дневник C# разработчика.

Личка: @EshuMarabo
Гитхаб: https://github.com/vladzvx

Стек: C#, PostgreSQL
加入频道
Палантир. Часть 14. Дубли в базе. Боль и страдания.
#палантир@eshu_coding

Одной из первых проблем были дубли в данных: одно и то же сообщение засасывалось более одного раза.

В какой-то момент я принял решение просто наплевать на них: ну есть у меня 15% дублирующихся данных, да и черт с ними. Но тут случился эпик фейл.

Запись данных в БД существенно опережала индексацию сообщения в поиске. В какой-то момент механизм защиты от дублей дал сбой и в базу поперли дубли. Некоторые сообщения дублировались по 20 раз.

Я решил проиндексировать вообще все сообщения: загрузка опережала поисковую индексацию на 250 млн сообщений. Удалил индекс со столба с полнотекстовым данными, подождал примерно сутки и решил запускать индексацию (часа 3-4) по моим оценкам.

Через 4.5 часа создание индекса рухнуло с ошибкой "слишком много дублей". И вот я остался с почти терабайтной базой с морем дублей и без рабочего поиска.

Может быть сделать тупое построчное удаление? Добавить сервис, который будет построчно брать данные из основной таблицы, искать к ним дубли и удалять их. Посчитал - обработка займет от 15 до 20 лет.

Нагуглил запрос, который угробит все дубли в таблице. Запустил. Спустя 6 часов PostgreSQL съел 8 Гб оперативки, 10 Гб файла подкачки, после чего ушёл в Out Of Memory.

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

В итоге я пришел к довольно топорному решению: сделать отдельную таблицу, в которую вошли ключи недублирубщихся данных. От дублей - только один, первый встретившийся ключ.

А дальше запрос, который удаляет из исходной таблицы все строки, id которых нет во вспомогательной. Молотило оно больше суток, но мусор почистило.

Итого, в базе осталось около 550 млн уникальных сообщений. Дублей было около 250 тысяч (в какой-то момент закачка обновлений сошла с ума и выкачивала вместо обновления всю историю чата повторно в течение двух недель).

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

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

Итого на борьбу с дублями я потратил дней пять. И хорошо, что это - мой личный проект.

Мораль сей басни такова: всплыла проблема - решай сразу, а не жди когда она выстрелит под нагрузкой на работающем продукте.

#postgresql
Палантир. Часть 15. Окончательное решение вопроса дублей.
#палантир@eshu_coding

Несмотря на локальную победу над дублями, они продолжали потихоньку попадать в базу.

В далёкие времена, когда я проектировал базу данных, я собирался использовать составной primary key в таблице с сообщениями: временная метка сообщения, id чата и порядковый номер сообщения. Но что-то пошло не так. Стандартный способ защиты таблицы с помощью триггера "before insert" не годился на секционированной таблице.

Делать проверку внутри хранимой процедуры для записи оказалось медленно: проверка занимает около 10 мс, на каждое из примерно 1000 сообщений, прилетающих каждую секунду. База мигом захлёбывается.

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

Спасибо доброму человеку @vekhden_speak, он подсказал решение, с помощью которого я окончательно поборол дубли. Как окалось, в самом insert-e можно предотвратить конфликт добавив предложение "on conflict on constraint messages_pkey do nothing".

За сутки вычистив базу от остатков дублей, я дописал хранимую процедуру для записи данных в БД, после чего перешёл к изначальной идее ключей. Вот теперь дубли изжиты окончательно.

#postgresql
Палантир. Часть 16. Клиентская часть для пользователей.
#палантир@eshu_coding

Работа над сборщиком данных в телеграме подошла к финальной стадии:
доделал mvp (минимально жизнеспособный продукт) части для пользователей, на выходе получилось два типа ботов:

Поисковик по телеграму @palantir_search_bot
Сервис оповещений @space_observer_bot

Поисковик просто ищет по скачанной истории сообщений за выбранный интервал времени, выплевывая 1000 самых похожих на запрос результатов.

Сервис оповещений проверяет все входящие сообщения (не старше 6 часов), и если они совпадают с заранее введенным запросом - оповещает пользователей.

Пока что реализована только демо версия, которая отрабатывает по следующему запросу:
'(илон <-> маск) | рогозин | космос | ракета | космическая <-> станция | астероид | галактика | солнечная <-> система | комета | марс | юпитер | сатурн | плутон |венера | солнечные <-> пятна | солнечный <-> ветер | байконур | роскосмос | space <-> x | spacex | орбита | космический <-> мусор | МКС | космонавт | астронавт'

Оператор | означает "или", оператор <-> - объединение слов по бокам в фразу. За первый же день работы на небольшую группу бета-тестеров стало очевидно, что все поисковые запросы и оповещения нужно приправлять блокировкой порнухи, ставок на спорт, крипты, политоты и экстремизма, чтобы случайно не заработать себе статью.

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

В итоге родилась идея: приправить основную таблицу триггером after insert, который будет пытаться вставить сообщение, если оно свежее 6 часов, в другую таблицу, получившую название spotter (наводчик).

На таблице spotter висит триггер, который делает select из таблицы queries (хранящей запросы), давая ответ: подходит под запрос или нет.

После этого вызывается функция pg_notify("test", "информация о сообщении"), которая передает информацию о сообщении всем, кто выполнил команду listen "test" и продолжает висеть на связи. В сообщении отправляется ссылка на сообщение и коротенькое превью из 200 первых символов.

Бот-слушатель соответственно рассылает сообщения подписантам.

Теперь для адекватной работы оповещалки (сокращения времени от опубликования до нахождения сообщения до 30-60 минут) нужно в очередной (в 8й) раз переделать менеджер команд сборщикам.

#postgresql
Палантир. Часть 17. Оптимизация базы данных.
#палантир@eshu_coding

В ближайшее время я планирую запустить поисковик @palantir_search_bot в общее пользование. Пришло время нагрузочного тестирования. Для этого я изобразил отдельный проектик, который имитирует поискового бота.

Для формирования тестовых поисковых запросов я использовал следущий поход: набил штук 100 разных слов (поток сознания там получился занятный, от "фавызм" (именно в таком написании) до хурмы и дыни.

Из этих слов случайным образом формируется пул запросов.

Запрос отправляется на сервер, ожидается ответ. Сразу по получении ответа - посылается следущий запрос. И так в N потоков.

Запустил в 1 поток: всё отлично.
Запустил в 10 потоков: всё отлично, постгрес не замечает нагрузки.
Запустил в 30 потоков. Постгрес не замечает нагрузки. Проходит пара минут и всё виснет.

Полез смотреть на сервер - 8 Гб оперативки кончились, вместе с 10 Гб файла подкачки, все съел постгрес, у которого установлен размер кэша в 2 Гб. Но каждое отдельное подключение, через которое шел поток данных, отъелось и в итоге память кончилась.

Оставил на ночь - постгрес не отвис: память утекла.

Итого, корень зла оказался в том, что подключения к постгресу, когда живут долгое время, не освобождают съеденную оператианую память (или освобождают не всю).

#postgresql
Палантир. Часть 19. Результаты нагрузочного тестирования.
#палантир@eshu_coding

В настоящий момент Master сервер,на котором осуществляется поиск, представляет собой следующее:
8 Гб оперативки, 4 ядра CPU, 1.5 Тб SSD диск. ОС - 18я серверная убунта, база данных - PostgreSQL 13.3. Объем базы - чуть больше 1 Тб, около 800 млн строк в основной таблице, по которой и осуществляется полнотекстовый поиск. Принцип формирования запросов я рассказывал выше.

После устранения утечки памяти (рассказывал выше) и оптимизации конфигурации Postgres по части памяти, сервис свободно держит 100 RPS (запросов в секунду).

В рамках экспериментов я выкручивал мощность машины до 16 ядер и 64 Гб оперативки. В таком сетапе удерживается нагрузка в 500-750 RPS.

Постгрес могёт однако.

#postgresql
1
Палантир. Часть 20. Ускорение поиска.
#палантир@eshu_coding

Оценить результат вы можете в боте: @palantir_search_bot

В какой-то момент, читая про то, как работает постгрес по одной из ссылок в этом посте, я наткнулся на упоминание, что можно указать держать в оперативной памяти (кешировать) конкретную таблицу. Для этого используется расширение pg_prewarm.

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

А потом в какой-то момент мне стукнуло в голову решение: основная таблица - messages - у меня секционирована по месяцам.

Секционирование - разбиение одной большой таблицы на группу по-меньше. Для каждой из таблиц существует отдельный индекс, что позволяет существенно ускорить работу: работать со 100 индексами 10-ти гигабайтных таблиц быстрее, чем с одним монстроиндексом терабайтной таблицы.

Соответственно, таблица messages представляет собой около 200 таблиц вида messages_01_2021, messages_02_2021 и так далее, с 2014 по 2030 год.

Для того, чтобы поиск для пользователя выглядел мгновенным, я держу в оперативной памяти данные за последние 2 месяца, а остальное - пусть лежит на диске и используется по мере необходимости. Пользователь сразу получает хоть какой-то результат, а "хвост" долетит со временем.

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

Также был доработан поисковый запрос на уровне c# - теперь он проводится в 3 этапа:
1. Запрос в последний месяц
2. Запрос в предпоследний месяц
3. Запрос во всю остальную базу, если нужно.

Результаты замеров скорости отклика на поиске глубиной в месяц:
1. Без кеширования - среднее время 200 мс, максимальное - 15 секунд
2. С закешированными индексами последних двух месяцев - среднее время 120 мс, максимальное - 6 секунд
3. С закешированными таблицами messages_10_2021 и messages_11_2021 - среднее время 80 мс, максимальное - 1.5 секунды

Под вариант "удобно использовать" подходит исключительно 3й, потому у сервера теперь 32 Гб оперативки (+3 тысячи к месячной плате)

#postgresql
Произошло моё первое практическое знакомство с оптимизацией запросов. Нормально пользоваться планом я пока не научился, но первый медленный запрос к базе успешно оптимизирован.

Делал я полнотекстовый поиск в базе проекта #палантир@eshu_coding, приправленный дополнительным параметром: id юзера или канала, по которому осуществляется поиск. Эти поля у меня проиндексированы индексом hash, потому выборка записей по конкретным значениям этих полей мгновенная. Полнотекстовый поиск же штука относительно медленная.

Запрос вида

select * from messages where full_text_req @@ post_text and chat_id = id;

где @@ , грубо говоря, оператор соответствия текста запросу, на всей базе выполняется секунд 20.

Судя по всему, Postgres проверяет все записи на соответствие запросу, а затем уже применяет фильтр по id.

Для ускорения запроса мне на помощь пришли Common Table Expressions (CTE). Суть их - в поэтапном выполнении сложного запроса. Если переписать запрос с использованием CTE, скорость выполнения становится стабильно около 300 мс.

Переписанный запрос:

with sel as (select * from messages where chat_id = id) select * from sel where text @@ full_text_request;

#кодинг
#postgresql
🔥1
Оказалось довольно удобно совмещать ORM с хранением jsonb в постгресе. Делаешь модельку вида:

class Model
{
Int Field1,
String Field2,
String Field3,
InnerModel ClassField
}

В обычные поля таблицы кладешь то, по чему будут строиться индексы и какую-то важнейшую информацию, к которой нужен максимально быстрый доступ.

А остальные поля, которые надо будет только читать, в т.ч. содержащие подклассы и массивы, уезжают в поле, содержащее класс (ClassField в примере). Помечяешь поле атрибутом "хранить как jsonb" и ORM сама сериализует и десериализует его. В итоге, вместо веера из десятка таблиц, разложенных по нормальным формам, получаем одну, время чтения и модификации данных соответственно уменьшается.

Итого, лёгким движением руки Postgres превращается, Postgres превращается...
в мутанта, содержащего признаки реляционки и монги.

/Напевает себе под нос/ Денормализация! Денормализация!

#postgresql
😁3
Послушал открытый урок Otus по некоторым особенностям оптимизации работы кластеров PostgreSQL. В целом, получилось очень познавательно: некоторые из услышанных вещей, упомянутых лишь краем, вызвали реакцию "хм, а так было можно?"

Вот перечень запомнившихся моментов:
1. Оптимизация скорости работы базы данных за счёт разнесения разных таблиц по разным дискам с помощью указания табличных пространств (tablespace). Я про что-то подобное мельком читал во время работы над Палантиром #палантир@eshu_coding, но воспринимал эти махинации исключительно с т.з. размещения большой базы, не влезающей на диск.

2. Оптимизация путем указания стоимости операции одиночного чтения с произвольного места диска. С классических hdd намного быстрее читать последовательно записанные данные. Потому в случае соотношения размера таблицы к размеру индекса 4 к 1 читать с hdd всю таблицу может быть быстрее, чем сканировать индекс и брать нужное из таблицы. А вот на SSD такой разницы нет, и выставив параметр конфигурации random_page_cost=1 мы ощутимо ускорим чтение данных.

3. Махинации с типом репликации. В случае, когда производительность на запись/изменение совсем поджимает, можно пожертвовать надёжностью реплицирования, но выиграть до порядка по быстродействию, переключив репликацию с синхронной на асинхронную. А ещё можно делать реплику, на которую изменения с мастера будут применяться с установленной задержкой, например - час. И в случае катастрофы, например - удаления базы целиком, будет какое-то время оживить систему с меньшими потерями, чем из вчерашнего бэкапа.

4. Узнал про возможность конкуррентного переиндексирования. Предположим, индекс раздулся и начал тупить и есть много места. Можно его удалить и перестроить с нуля. Но "на живую" это приведет к большим проблемам. А оказывается можно запустить фоновое переиндексирование.

5. Узнал, что в постгрес таки есть механизм подсказок планировщику запросов, что ему делать. Для этого нужно расширение pg_hint_plan.

6. Услышал оговорку "обычно балансировку запросов между мастером и репликами у нас делают сами приложения". А так было можно?! Полез читать - оказывается, у стандартного шарпового коннектора Npgsql и балансировщик и координатор выборов нового мастера в случае сбоя есть под капотом.

Курс я конечно покупать не буду, но за 1.5 часа времени информация получена очень полезная.

#postgresql
🔥7
#dotnext, просмотр в записи.

B-Tree индексы в PostgreSQL.

Доклад получился крайне полезным: в связной легкодоступной форме пересказали то, что я в общем-то знал по устройству внутрянки BTree индексов.

Кроме того, в процессе доклада всплыла ещё масса полезностей, которые ускользнули от моего внимания ранее;

1. explain (analyze, costs off, buffers) - синтаксис для просмотра плана запроса с прогнозом числа чтений страниц. Heap fetches - тот самый прогноз походов в таблицу после сканирования индекса для проверки наличия записи в таблице (если она ещё не зачищена от мусора вакуумом).

2. Спикер заострил внимание на разнице index scan и index only scan в плане выполнения запросов (последний не полезет читать доп информацию из таблицы после выполнения, ему для выполнения хватит инфы их индекса).

3. Спикер рассмотрел разницу по работе с BTree индексом в случае индексирования по bigint и по uuid. bigint при добавлении растит индекс в одну сторону. uuid - растит хаотично, расходует больше оперативной памяти в процессе работы. Разница по быстродействию может доходить до 7 раз. Отчасти проблему решает использование uiid v7, в котором используется временная метка. Но это не про стандартную библиотеку c#:)

4. Удаленные из таблицы данные остаются какое-то время залипать в индексе и накручивают Heap fetches в плане выполнения запроса. Помогает не дожидаться долго и тяжёлого вакуума (процесс очистки от мусора) фича из PostgreSQL 14 - bottom-up deletion - зачистка индекса от мусора в процессе вставки новых данных.

5. Спикер упомянул интересный путь оптимизации запросов: учитывать сортировку в индексе и идти к результату с определенной стороны с помощью CTE, например вызовом рекурсивного подзапроса > treshold_value, с постепенным нарастанием treshold_value.

#postgresql
#conf
🔥1