Telegram Web Link
Больше года я ходил вокруг стека ELK, думая, что вот надо, надо. Но никак не складывалось. И вот стукнуло: пора.

ELK - стандартное средство для сбора, хранения и визуализации логов. Расшифровывается как Elasticsearch (поиск и хранение) + Logstash (собор из разнородных источников) + Kibana (Визуализация).

Попытался завести в докере вместе со всем остальным решением образ sebp/elk. Не взлетело: не хватило оперативки (у меня было 16 Гб). Ну чтож, добавил её, стало 64.

Взлетело! Процесс vmmem, который по сути является внутренним линуксом винды, стал есть 36 Гб (!).

Относительно быстро разобрался, как писать логи стандартным инструментом Serilog из c# напрямую в эластик, соответственно Logstash стал мне не нужен, отключил его запуск в контейнере с elk.

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

#кодинг
Как нормальные люди пишут тесты? Выявляют кейсы, которые надо протестировать, пилят в той или иной форме генератор тестовых данных, покрывают кейсы тестами, а потом гоняют их и радуются.

Если структура данных сложная, а вникать в суть происходящего лень - велик соблазн выгрузить пул данных из базы, сохранить в каком-то виде (например - штук 500 json файликов в специальной папочке в проекте с тестами) и на этой сохраненке и гонять тесты.

Я попробовал, больше не буду идти на поводу у низменных желаний. Тесты написались мгновенно, закрыли существенную часть бизнес логики, всё просто отлично. Я продолжил работу над проектом и каждый раз при провале теста мне приходилось разбирать происшествие с двух сторон: что я сломал? Хм, вроде ничего. А теперь посмотрим юзеркейс и добьёмся воспроизводимости.

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

#кодинг
Палантир. Часть 25. Рефлексия год спустя.
#палантир@eshu_coding

Описание получившейся системы можно посмотреть по ссылке. Одной из некритичных ошибок была попытка сделать шину данных из костылей и велосипедов на gRPC вместо использования стандартного решения типа RabbitMQ.

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

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

Мне пришлось вкрячивать внутрь приемника данных от сборщиков сервис подписок, чтобы данные сразу по получении рассылались всем кто подключен. Персистентность? Гарантированная доставка? Не, это не наш метод.

В принципе, ошибка крайне неэстетичная, но на функциональность особого влияния не оказывающая.
Палантир. Часть 26. Рефлексия год спустя.
#палантир@eshu_coding

Описание получившейся системы можно посмотреть по ссылке. Намного менее приятной ошибкой было скатывание к монолиту. Центральный сервис (БД на postgresql + надстройка на шарпе) у меня выполнял четыре основные функции:
1. Прием и укладка данных в БД
2. Анализ БД и выдача заданий сборщикам
3. Поисковые кэш и запросы к нему.
4. Анализ потока входящей информации на наличие ключевых слов и выдача оповещений при обнаружении.

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

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

Как на мой нынешний взгляд надо было сделать:
1. Использовать RabbitMQ вместо самопала на gRPC (см прошлый пост)
2. Отработать запись данных в БД. Накрыть тестами функционал.
3. Запустить отдельным сервисом постановку задач сборщикам. Накрыть тестами функционал. Скорее всего, после окончательной отладки этот сервис сольётся с п. 2. Я начал делать их вместе, в итоге так и не смог до конца искоренить дублирование выгружаемых сообщений.
4. Завести отдельную пару сервис + бд для поискового кэша. Сделать как обновление в реальном времени, отведя с помощью RabbitMQ поток данных в сторону + предусмотреть функционал "перелива" данных из основной базы. Теперь я не привязан в PostgreSQL rum индексу на базе дефолтного словаря! И можно экспериментировать с поисковыми движками как душе угодно, не нарушая функциональность основного сервера. Хоть эластик попробовать, хоть сделать прослойку на питоне для умной обработки текста готовыми инструментами.
5. Отвести поток данных для анализа в реальном времени (для выдачи оповещений пользователям) и экспериментировать сколько душе угодно.

P.S. "Отвести поток данных" в случае RabbitMQ значит добавить несколько символов в месте подключения клиентской библиотеки.

P.P.S. Никто не мешает после отладки функционала по отдельности вернуться к монолиту, например для повышения быстродействия. Если закладывать такую возможность на старте - это дело 1-2 дней.
Выложил на гитхаб небольшой проектик - переделку бота-модератора телеграммных чатов. При написании его использовал несколько новых для себя подходов: TDD - test driven development, отказ от хранения состояния в приложении, использование тарантула в качестве персистентной очереди команд.

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

Тарантул полностью живет в оперативной памяти, для связи по дефолту использует достаточно компактный и шустрый протокол - MessagePack. Это располагает к оптимизациям и работе с отдельными байтиками. Так, задача в очереди - это просто массив байт. Первый байт указывает на тип задачи, в соответствии с которым она и парсится и исполняется. Обычно задачи содержат, кроме первого байта, еще 2-3 int64 айдишника, то есть объем задачи получается во основном 17 или 25 байт. Такие объемы информации летают очень шустро.

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

В репозитории по сути выложен шарповый стартер для экспериментов с конфигурацией кластера не выходя из Visual Studio: при запуске из докерфайла билдится контейнер, содержащих заданную конфигурацию кластера (по дефолту - мастер + реплика + балансировщик) и уже готовый к работе.

#tarantool
Получил лвлап и кучу экспириенса по работе с тарантулом, что характерно - на ровном месте.

Рассмотрим кейс: есть некая запись, относящаяся к человеку (user_id, unsigned(UInt64)). У нее есть время создания (creation_time, integer (Int64)) и время закрытия (cancelled_at, integer (Int64)).

Нужно дёрнуть все записи, относящиеся к человеку, актуальные на определенный момент времени. То есть запрос вида:
Входящие параметры: id, time.
user_id == id && creation_time <= time && (cancelled_at == nil || cancelled_at >= time)

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

В тарантуле есть составные индексы, туда можно передавать запросы из нескольких частей. Но вот беда: запрос, объединяющий сравнения == <= и >= он выполнить не может.
Решил зайти с необычной стороны: сделать условием >= и построить следующий индекс: creation_time, user_id, - user_id, - cancellation_time

То есть добавить в индекс инвертированный id пользователя и инвертировать время закрытия. Тогда в теории все должно выцепляться одним условием ">=". Что могло пойти не так? Всё. Как оказалось, для составных условий используется логика обработки частей запроса, отличающаяся от той, что заложена в схожей ситуации в MongoDB или PostgreSQL.
Сравнение происходит не по частям индекса. В сравнении участвует весь массив целиком как единая сущность. Говорят, что логика сравнения где-то была описана, но с ходу страница документации не ищется и где она лежит никто не помнит.

В тарантуле есть специальный инструментарий для работы с пространственными данными. Казалось бы, при чём тут они?
Поднимемся по лестнице абстракций на пару ступеней. Представим себе запись о пользователе прямоугольником в системе координат (id пользователя/время). Одна точка прямоугольника - создание записи, имеет координаты (user_id, creation_time). Вторая - (user_id, cancelled_at).
Соответственно, проверка "существовала ли запись о пользователе в определенный момент времени"? Сводится к задаче о принадлежности точки прямоугольнику. Готовое решение для запроса по индексу - в наличии. При желании, можно добавить ещё измерений (до 20), но что-то пока не хочется.

Проблема решена? Фииигушки. Индекс не ест мои значения в Int64 и UInt64, ему подавай Float64. При ковертации "в лоб" хвостики id теряются, из-за ограничений точности Float64. Добавляем очередной велосипед.

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

А вот теперь - всё. Ну или мне так кажется:)

P.S. В монге решил эту же задачу за 25 (!) минут, запрос + индексы, всё работает с первого раза.

#tarantool
Эшу быдлокодит
Получил лвлап и кучу экспириенса по работе с тарантулом, что характерно - на ровном месте. Рассмотрим кейс: есть некая запись, относящаяся к человеку (user_id, unsigned(UInt64)). У нее есть время создания (creation_time, integer (Int64)) и время закрытия…
Решил добавить в индекс еще пару измерений. Теперь главное не пытаться представить, как выглядит запись в базе данных в качестве 4х мерного прямоугольника-параллелепипеда (термин из тарантуловской документации), где измерения - id пользователя, время, ранг записи в древовидной структуре и её тип.
Тут хороший человек работу ищет:
CV_StolbovV_short.pdf
90.2 KB
А разнесите резюму по своим каналам, что ли.
Эшу быдлокодит
И сегодня, на эти же грабли, в этом же самом месте, два раза, хренак, хренак! Да, я так и не написал генератор данных, поленился.
И в четвертый раз, на те же грабли, в том же месте!

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

Но стоит сделать шаг в темноту сторону каких-то статистических запросов - все перестает быть столь уютным.

Для человека, два года пользующегося монгой и казалось бы привычного к ней, первое составление запроса вида (дико извиняюсь за псевдо-sql):

select distinct field1, field2 from table where (пара условий) order by field2 limit 100 skip 100

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

Сначала, как обычно, пытаешься слепить запрос прямо в c#, используя синтаксический сахар, предоставляемый библиотекой-конектором.

После полутора дней мучений в с# я отправился в дефолтную графическую оболочку над базой, где часа за 2 в конструкторе составил нужный мне запрос.

Теперь я имею json в 30 строк, который при выполнении в монге выдает нужный мне результат. Осталось корректно перенести его в с#.

#mongodb
Эшу быдлокодит
Пока монга выполняет роль хранилища-помойки, вида: положил кучку - забрал по id или паре простых запросов - она прекрасна и крайне дружелюбна. Но стоит сделать шаг в темноту сторону каких-то статистических запросов - все перестает быть столь уютным. Для…
Вот примерный json, генерируемый запросом. Сравните по читаемости с псевдо-sql версией в прошлом посте.

[{
$match: {
Field3: "qwerty123",
},
},
{
$group: {
_id: "$Field1",
id: {
$first: "$Field1",
},
Field2: {
$first: "$Field2",
},
},
},
{
$sort: {
Field2: -1,
},
},
{
$skip: 0,
},
{
$limit: 2,
},
{
$project: {
_id: 1,
},
},
]

group в данном случае используется для получения эффекта distinct из sql запроса.

Монга умеет в distinct запросы, но туда не завезли пагинации (постраничной отдачи), потому идём окольным путём 🤡

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

_mongoCollection.Find(record=>record.Field3=="qwerty123").ToList();

В примере используется трансляция в монговский формат внутришарпового языка запросов к коллекциям LINQ, потому всё максимально прозрачно.

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

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

В посгресе такого механизма "из коробки" нет. Но есть широко распространенное средство организации отказоустойчивых кластеров: Patroni. С год назад я к сожалению не осознавал, зачем оно мне.

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

Теперь надо бы для общего развития поднять кластер где-нибудь у себя.

#mongo
В прошлом году итоги я подвёл в ноябре при запуске #палантир@eshu_coding, после него ничего особо интересного не было.

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

2. Нырнул в MongoDB на уровне, отличном от "положил как в помойку, достал по id/однострочному запросу". Многоступенчатые запросы, подписки на обновления коллекций, TTL, транзакции, materialised view. Впервые завел реплика-сет вместо отдельностоящего инстанса.

3. Построил пару обменников на RabbitMQ, пришло осознание проглоченного и применённого на коленке в 2021.

4. Влюбился в систему сбора метрик Prometheus. Если раскидать их по приложению и заодно подключить сбор с компонентов системы (баз данных и т.д.), получается крайне информативно. Дефолтный визуализатор так себе, но основные функции выполняет.

5. Познакомился с EKL стеком - сбор и визуализация логов. Особой любви не случилось, уж больно оно огромно и прожорливо. Можно кстати использовать ELK для построения красивых дашбордов по данным из Prometheus-а, но это мне пока не особо нужно.

6. Прогресс как шарписта у меня вышел так себе. Научился красиво описывать rest api с помощью Swagger, да в общем-то и всё. Ну ещё окончательно освоил разработку с использованием докера: приложение сразу запускается и отлаживается в контейнере, окружённое соседними сервисами. При прогоне тестов также активно использую песочницу, поднятую в docker-compose.

P.S. NoSQL я накушался досыта, 2023 - время вернуться к истокам - C# + PostgreSQL.
Ну чтож, надо посмешить богов, озвучить свои планы.

Чего хочу я в этом году:
1. Немного обмазаться фронтенд разработкой, но без фанатизма. Пока думаю о связке React + Typescript.

2. Попробовать на практике стандартную шарповую ORM: Entity Framework.

3. Углубиться в PostgreSQL, поиграться с уровнями изоляции транзакций, подтянуть оптимизацию запросов, возможно попробовать пособирать разные конфигурации кластеров на foreign table.

4. Прочитать несколько умных книжек, что-то типа:
a) Рихтер
b) Высоконагруженные приложения Фаулера
c) Ещё что-нибудь по архитектуре.

5. Начать подтягивать базу по Computer Science, мб копнуть алго задачи.

Вот такие планы, интересно, что исполнится к концу года:)
Эшу быдлокодит
Ну чтож, надо посмешить богов, озвучить свои планы. Чего хочу я в этом году: 1. Немного обмазаться фронтенд разработкой, но без фанатизма. Пока думаю о связке React + Typescript. 2. Попробовать на практике стандартную шарповую ORM: Entity Framework. 3.…
Ну что же, фронтенд мой пока закончился на попытке нормально подружить Visual Studio и фронтенд разработку.

Я стремительно расширяю сознание в части строительства всякого интересного на базе MongoDB, скоро буду выходить на новый уровень в части работы с RabbitMQ.

PostgreSQL? Entity Framework? Фронтенд? Планирование? Не, это для лохов.
2025/07/07 13:08:12
Back to Top
HTML Embed Code: