Telegram Web Link
Палантир, Часть 5. Питоновские клиенты-сборщики.
#палантир@eshu_coding

Сборщиков сейчас работает 4 штуки, написаны они на питоне, с использованием библиотеки telethon. Крутятся по два, каждый в своем докер контейнере на машинах с 0.5 Гб оперативки и 1 ядром 2.2 ГГц (слабее витуалки не нарезает хостер).

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

Добрые люди создали удаленное хранилище сессий: ставишь пакет, указываешь тип бд и connection string к ней, а дальше все работает само.

В итоге при старте slave стучится к мастеру: есть сессия для коннекта? Если есть, master отдает ему данные для подключения к хранилищу сессий и номер телефона реги, slave цепляется к бд, берет данные, коннектится, работает.

Написаны сборщики за один день и написаны ужасно: там используется god-class и несколько вложенных друг в друга циклов while True:

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

P.S. У меня сегодня небольшой юбилей: в базе сейчас 91 тыс каналов и групп, из них выгружено 57 тыс, 1.7 млн юзеров и
150 млн сообщений.
Палантир. Часть 6. Out of memory kill. Моя борьба.
#палантир@eshu_coding

Основной сервер существенно мощнее сборщиков: 4гб оперативки, 2 ядра 2.2 ГГц. Крутятся там два компонента системы: база данных - postgresql и сервис-обертка на ней. Сервис принимает подключения по grpc, генерирует и отдаёт приказы и укладывает пришедшие данные в бд.

Как только вся система начала работать в штатном режиме, ещё на 2 Гб оперативки, у меня возникла проблема: в какой-то момент Линуксу перестает хватать памяти и он пишет следующее: Out of memory: kill process X or sacrifice child. И убивает процесс.

Добавил файл подкачки в 3 Гб, накинул оперативку до 4гб, понизил приоритет моих процессов для OOM киллера. Вроде проблема прошла.

Смотрю утром: Out of memory: kill process postgresql or sacrifice child.

К тому же сервер начал негодовать, что бд нету и отложил 25 Гб ругательных логов, продублированных и в системные логи убунты.

По логам восстановил произошедшее: все 4 сборщика наткнулись на жирные каналы с большим количеством объемных (по 2кб в среднем) постов.

База не успевала всё переварить, скопилась очередь в 2 млн сообщений (2 Гб оперативки), OOM грохнул базу и все сошло с ума. Добавил приказы сборщикам на торможение: если в очереди на запись больше 100 тыс постов - сборщики получают команды поспать 10 секунд пока ситуация не разрешится. Также снизил лимит размера кэша у Постгреса с 1Гб до 500 мб. Запустил.

Утром: Out of memory: kill process postgresql or sacrifice child.

Покурил мануалы по постгресу, покрутил настройки, снизил лимит сна до 60 тыс сообщений.

На следующее утро: Out of memory: kill process postgresql or sacrifice child.

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

Out of memory: kill process postgresql or sacrifice child.

Продолжил искать корень зла. Можно было бы конечно накинуть оперативки до 16 Гб, добавить пару ядер в цпу и перейти на SSD, но это во-первых не спортивно, во-вторых нет никакой гарантии, что проблема решится, а в-третьих - проект учебный. Люди когда-то жили без неограниченных ресурсов, значит и мне было бы неплохо научиться писать эффективный код.

И тут в какой-то момент я решил посмотреть, какой процесс сколько потребляет памяти в реальном времени.

Мой сервер-надстройка над базой - 500 мб-1.5 Гб по ситуации.

Постгрес основной процесс - 500 мб, как и прописал к конфиге. И ещё - 7 процессов по 250-300 мб. 3 - подключения моего сервера к БД: для записи сообщений, для записи юзеров и для записи чатов и каналов. 4 - подключения сборщиков, ведь именно на эту БД я повесил удаленное хранилище сессий, вот они и пользуются им.

Началась оптимизация: хранилище сессий уехало в другое место, все подключения сервера к БД я засунул в одно. А ещё добавил более адекватную тормозилку разогнавшихся сборщиков: при заходе за лимит (80 тыс сообщений в очереди на запись), сервер устанавливает паузу на прием следующего сообщения в 1 с. Когда очередь разгружается до 40 тысяч прием возобновляется в полном объеме.

Так и поборол я OOM Killer. Все крутится уже недели полторы без единого чиха.
Dependency injection (DI).

В процессе написания сборщика я осознал логику современного подхода к проектированию приложений: использования внедрения зависимостей (DI).

Ранее я многократно использовал этот подход для подключения готовых компонентов из ASP. Net Core. Но как-то не задумывался о сути подхода, лежащего в основе.

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

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

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

Элементы программы - сервисы - могут иметь один из четырех вариантов жизненного цикла:
1. Transient. Каждый раз когда есть необходимость в экземпляре класса будет создан новый экземпляр, а по выполнении работы - удален.
2. Scoped. Один экземпляр класса на одну порождающую причину.
3. Singleton. На веки будет лишь один экземпляр класса, с ним и работайте. CLR проследит за его единственностью, если использовать его только через DI.
4. Hosted Service. В отличие от первых трёх видов, создаваемых "по пинку" из внешнего мира, создаётся сразу при старте приложения. Обычно выполняет какую-то постоянную/периодическую работу, в остальном - как Singleton.

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

Преимущества использования внедрения зависимостей:
1. Программа превращается
в конструктор, можно
многократно и без мучений переиспользовать удачный код, вынеся его в библиотеки и подключая по мере необходимости.
2. Исключается часть ошибок по невнимательности: нельзя забыть создать класс или создать его с неверными параметрами. В обычном случае проблема может всплыть нескоро и вызвать много затруднений. А в случае с DI программа завалится на старте и сообщит, где и чего ей не хватает для счастья.
3. Удобство тестирования и разработки "по частям". Можно прямо со старта накидать архитектуру, понатыкав заглушек. И реализовывать их постепенно, меняя одно слово при регистрации класса. Также удобно покрывать тестами отдельные сервисы.

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

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

P.S. Внедрение зависимостей есть и для питона.

#кодинг
Палантир. Часть 7. Dependency injection в master-сервере.
#палантир@eshu_coding

В продолжение
предыдущего поста и в завершение серии про сборщик.

Регистрация всех элементов gRPC осуществляется через внедрение зависимостей: даётся указание использовать grpc, а также - какие сервисы использовать.

Кроме того, остальные компоненты, используемые мной также подключены.

Сервис, пишущий в БД через одну очередь - singleton. Кроме него, singleton-ами являются классы "State" (хранится всё взаимодействие со сборшиками slave-ами) и LoadManager, притормаживающий сборщики при риске перегрузки.

Также используется два Hosted Service: генератор приказов и периодически синхронизирующийся с базой менеджер учёток сборщиков.

В виде Transient зарегистрированы всякие служебные штуки, а кроме того, класс Report. В него через DI заведены основные синглтоны. При http запросе на контроллер, выведенный мной для мониторинга, создаётся экземпляр Report.

В конструкторе он сохраняет информацию о состоянии синглтонов в свои поля, после чего сериализуется и отправляется мне,а потом - уничтожается.

Scoped не нашли себе места в моем сервере. При других обстоятельствах в качестве Scoped я мог бы объявить запись в БД, чтобы информация каждого вида писалась через своё подключение. Но, во-первых я экономлю память, а во-вторых, при использовании HDD в качестве хранилища, выигрыш от распараллеливания записи крайне сомнителен.

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

Проблема у ботов Когана скорее всего следующей природы: у телеграма у ботов есть лимиты на число отправляемых сообщений. Если много человек одновременно нажмет "Start", бот им одновременно ответит, скорее всего - каким-то меню, в ответ на которое он должен тоже что-то получать.

Люди тыкают, бот - отвечает. Выхватывает спам - ограничение, начинает тупить. Психующие люди тыкают больше кнопок, бот всё активнее пытается отвечать, в итоге очередь обработки запросов растягивается до бесконечности.

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

Угробил я на решение проблемы спам-блокировок ботов дней 5, но решил её навсегда, в виде небольшого модуля, который легко воткнуть в любое место в боте. А самое прикольное, что пару месяцев назад мне по работе понадобилось делать примерно то же самое, в результате чего я сэкономил несколько дней, просто скопировав свой код.
Палантир. Часть 8. Переезд между хостингами.
#палантир@eshu_coding

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

Я начал пилить нечто вроде поисковика по постам телеги, скорость работы HDD на базе размером 250 ГБ меня совершенно не устраивает.

Перешёл в облако к Яндексу из-за гуманных цен на SSD. Сразу подкупила встроенная в личный кабинет система мониторинга. Не Zabbix конечно, но позволяет понять, что там внутри происходит. На старом хостере инструмент мониторинга существенно хуже.

Я снял виртуалку, на которую поставил master-сервер и postgres. Вообще, яндекс предоставляет и Postgresql as service. Суть услуги - конфигурируем из личного кабинета постгрес, оплачиваем и пользуемся.

Без переписывания конфиг файлов и установки Postgres на Линукс. Но цена х1.5 от цены виртуалки с теми же параметрами меня отпугнула. К тому же, мне нужно куда-то ставить master-сервер.

Порадовал процесс клонирования и репликации в postgres. Сначала утилитой перетягиваем всё содержимое БД на другую машину.

Потом дописываются несколько строчек в конфигурации и у нас появляется репликация вида master-slave. Фантастически удобно, привет MySQL!

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

Скорость чтения и записи даже на шустрых SSD конечна. При росте нагрузки на базу рано или поздно она начинает тормозить.

Одно из ухищрений в данной ситуации - репликация. Самый простой вариант - одна база на запись (master) рассылает обновления по readonly slave-ам. В master - только пишут, со slave-ов - только читают.

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

#кодинг
#палантир
Палантир. Часть 9. Боль и страдания с организацией полнотекстового поиска.
#палантир@eshu_coding

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

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

Первое - оформленные в пакет словари от OpenOffice поставилось легко: просто ставим очередной лмнуксовый пакет.

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

Сначала мне не хватало репозитория со всем постгресом. Скачал.

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

Перешел к запуску сборки самого расширения. Не работает. Не хватает компилятора для С++, поставил. Сборка началась!
fatal error: namespace.h: no such file...

Действительно, файла в папке нет, но в компилятор передан параметр "искать файлы в дополнительной папке, где он есть". Перебил все инклюды с #include <namespace.h> на #include "../libdict/namespace.h". Билд прошел!

Только не в ту папку, куда я хотел, потому расширение не устанавливается. Переместил всё куда надо... Извините, расширение сделано для 14 бета версии PostgreSQL, потому работать на 13, стоящей у меня не будет.

Так и не нашел, как скомпилиться под определенную версию, потому пошел на колхоз: перешел на коммит годичной давности и сборался под него... И не помогло.

Удалил всё к черту, заново склонировал конкретную ветку с нужным мне релизом постгреса (13.3), заново склонировал репозиторий с расширением, поправил перебил в нем инклюды, удостоверился, что от старых компиляций не осталось и следа и ... о чудо! Расширение подключилось к базе данных, теперь можно тестировать два словаря в сравнении.

Похоже где-то что-то кешировалось при компиляции...
Как разные словари для полнотекстового поиска в постгресе (для #палантир@eshu_coding) разбирают текст.

Структура таблицы:

1. Исходный текст
2. Текст, разобранный дефолтным парсером
3. Текст, разобранный парсером со словарем от OpenOffice
4. Текст, разобранный парсером со словарем, который я компилировал вчера.

#postgresql
Палантир. Часть 10. Полнотекстовый поиск.
#палантир@eshu_coding

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

Словари имеют некоторые особенности. Дефолтный коряво отрезает окончания, не умеет в синонимы.

Тот, который я компилировал из исходников очень неплохо справляется с падежами и склонениями. Но при этом он бьёт имена собственные: человека по фамилии Медведев он превратит в медведя.

К тому же у него есть особенность: при попадании на разбор "слова" из 80 и более символов (что-то типа ахахахаха....) он пытается съесть 10^19 бит памяти, которых на сервере естественно нет. Ещё этот словарь крашит постгрес при попадании какого-нибудь экзотического символа, например иероглифа.

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

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

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

Вообще, готовое решение для полностекстового поиска на русском языке есть встроенное в Postgres Pro, но он платный, лицензия около 200к рублей.

Впрочем, Yandex.Cloud предоставляет Postgrespro как сервис, примерно за 10 тыс рублей в месяц, потому лицензию покупать не обязательно, можно арендовать пробный сервер и поиграться с ним.
Палантир. Часть 11. Быстродействие поиска.
#палантир@eshu_coding

После запуска индексирования всего массива данных всплыла проблема быстродействия поиска: в тот момент в базе было около 600 млн сообщений (сейчас за 750) и поиск по ним работал, но мееедленно, минут 10-15 на запрос.

Перепробовал все варианты, которые предлагали на хабре, после чего совершенно случайно обнаружил у себя "небольшой" косяк.

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

В ходе экспериментов я совершенно случайно обнаружил, что очистка над текстом запроса выполнялась для КАЖДОЙ проверяемой записи в БД отдельно.

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

После этого задействовал все трюки, которые вычитал на хабре, добившись скорости поиска в секунды.

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

В целом, получилось неплохо, через какое-то время запущу для подписчиков тестового бота. Проект, кстати, получил от уважаемого @ssleg название "Палантир", так и запишем:)
Палантир. Часть 12. Юзер интерфейс.
#палантир@eshu_coding

Тестировать поиск через запросы, как напрямую в БД, так и отправляемые через Postman на API - это прекрасно, но удобно только для разработчика, т.е. меня.

Как только проект обрёл признаки MVP - появилась необходимость подключать к тестированию других людей, а стало быть пришлось заняться пользовательским интерфейсом.

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

Ботов я раньше писал, и часть, связанная с тем, чтобы сделать использование бота УДОБНЫМ, да ещё и без глюков, всегда была кошмаром.

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

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

С этой абстракцией я познакомился после какого-то заваленного собеседования, подумал "что за дичь? Мне оно не упёрлось" (тогда я пилил ботов, бгг). И только сейчас, спустя 8 месяцев я осознал, зачем всё это.

Я быстренько выделил 5 основных состояний у бота, за вечер описал каждое из них, не пытаясь засунуть в голову ВСЕ варианты сочетаний раздражителей и реакций одновременно, и с ходу получил MVP, не без глюков конечно, но без фатальных проблем.

Кстати, очень удобным для реализации поиска, оказалось использование gRPC в качестве протокола общения. Бот посылает одиночный запрос, сервер в ответ начинает стрим и передаёт результаты по мере нахождения, а бот и соответственно отображает. Для пользователя это ещё сильнее увеличивает быстродействие: первый результат появляется практически сразу, а что находится потом - долетает отдельно.

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

#кодинг
Палантир. Часть 13. Жизненный цикл приказов, эпизод черт знает какой.
#палантир@eshu_coding

Изначальная идея взаимодействия мастера и сборщиков заключалась в следующем:

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

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

Решением стала полная перекройка логики. При работе клиент телеграма сохраняет "сессию" - данные для упрощения повторных логинов и повторных запросов к чатам и каналам, которые уже встречались аккаунту на просторах телеграма. В моем случае база централизовано хранится в отдельном сервере Postgres с помощью очень удобного инструмента, ссылку на который я обнаружил, читая документацию к telethon (да, я иногда это делаю:)).

У всех чатов и канало в базе добавилась колонка Finder, где содержится массив всех номеров телефонов сборщиков, "знакомых" с этим каналом. Сборщик, когда просит данные, "представляется" и сообщает, заблокированы ли у него тяжелые запросы.

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

У одного чата "Finder"-ов может быть много, хоть все сборщики сразу. Потому, во избежание размножения дублирующихся записей, пришлось распихивать приказы по всем соответствующим очередям одновременно и делать потокобезопасное поле-флаг, где отмечено состояние приказа. Если была попытка получить приказ, находящийся в состоянии отличном от "требуется исполнение" - приказ выбрасывается из очереди, а на его место забирается следующий.
Эшу быдлокодит
Палантир. Часть 12. Юзер интерфейс. #палантир@eshu_coding Тестировать поиск через запросы, как напрямую в БД, так и отправляемые через Postman на API - это прекрасно, но удобно только для разработчика, т.е. меня. Как только проект обрёл признаки MVP - появилась…
К вопросу о конченных конечных автоматах.

Сегодня, обсуждали с коллегой - программистом по образованию - довольно сложную штуку: работу аснхронности (в разрезе многопоточности) в c#. Про неё написаны книги, огромное количество статей на хабре.

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

- Знаешь что такое конечный автомат?
- Да.
- Ну вот всё приложение в целом представляется средой выполнения чем-то похожим. А используемые await-ы - границы между состояниями . И вот оно по ним щелк-щелк, используя потоки из пула.
- Аааа, понятно!
Палантир. Часть 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
Forwarded from Библиотека программиста | программирование, кодинг, разработка
Егор Рогов из Postgres Professional подробно и доступно рассказывает теорию и практику работы с PostgreSQL:

📌 Индексы

- Механизм индексирования
- Интерфейс метода доступа, классы и семейства операторов
- Hash
- B-tree
- GiST
- SP-GiST
- GIN
- RUM
- BRIN
- Bloom

📌 Изоляция и многоверсионность

- Изоляция, как ее понимают стандарт и PostgreSQL
- Слои, файлы, страницы — что творится на физическом уровне
- Версии строк, виртуальные и вложенные транзакции
- Снимки данных и видимость версий строк, горизонт событий
- Внутристраничная очистка и HOT-обновления
- Обычная очистка (vacuum)
- Автоматическая очистка (autovacuum)
- Переполнение счетчика транзакций и заморозка

📌 Журналирование

- Буферный кеш
- Журнал предзаписи — как устроен и как используется при восстановлении
- Контрольная точка и фоновая запись — зачем нужны и как настраиваются
- Настройка журнала — уровни и решаемые задачи, надежность и производительность.

📌 Блокировки:

- Блокировки отношений
- Блокировки строк
- Блокировки других объектов и предикатные блокировки
- Блокировки в оперативной памяти

📌 Запросы

- Этапы выполнения запросов
- Статистика
- Последовательное сканирование
- Индексное сканирование
- Соединение вложенным циклом, а также будет продолжение про соединение хешированием / слиянием и сортировку

А еще у Postgres Professional есть учебные курсы, которые доступны всем желающим.
2025/07/10 12:25:28
Back to Top
HTML Embed Code: