Microservices Thoughts
7.77K subscribers
32 photos
56 links
Вопросы и авторские статьи по микросервисам, архитектуре, БД

Сотрудничество: t.iss.one/qsqnk
Download Telegram
⚡️Принцип работы snapshot isolation (aka repeatable read) в postgres

Изоляция repeatable read избавляет от неповторяющегося чтения — ситуации, когда одна и та же строка запрашивается дважды в рамках транзакции, но результаты чтения получаются разными

begin;

select * from t where id = 1 <- отдает одно значение

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

select * from t where id = 1 <- отдает уже другое значение
...


Как это работает в postgres:

- Каждой транзакции присваивается xid — монотонно возрастающий идентификатор транзакции
- MVCC: одновременно поддерживаются несколько версий строк
- У каждой версии строки есть два системных поля: xmin, xmax

xmin — идентификатор транзакции, который создал версию строки
xmax — идентификатор транзакции, который удалил версию строки (т.е. сделал update либо delete)

---

Отсюда возникает довольно логичная концепция — при начале repeatable read транзакции "берем снапшот":

1. Назначаем текущей транзакции некоторый xid
2. В транзакции работаем только с версиями строк, где
- либо xmin < xid < xmax — версия строки создана до текущей транзакции, а удалена уже после начала текущей
- либо xmin < xid && xmax = 0 — версия строки создана до текущей транзакции, но еще никем не удалена

---

Однако возникает следующая проблема — на момент взятия снапшота может быть активная транзакция с xid меньшим, чем у снапшота. Когда она закоммитится, то для новосозданных строк будет выполняться условие xmin < xid && xmax = 0, и мы в текущей repeatable read транзакции увидим эту версию строки. Хотя при взятии снапшота этой версии еще не было — снова можем получить неповторяющееся чтение

Это решается следующим образом:

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

Таким образом, условие "видимости записей" будет таким

1. Берем снапшот: xid + active_xids
2. В транзакции работаем только с версиями строк, где

(xmin < xid < xmax || xmin < xid && xmax = 0)
&&
(xmin not in active_xids)


Хорошая статья по теме https://mbukowicz.github.io/databases/2020/05/01/snapshot-isolation-in-postgresql.html
👍64🔥23
⚡️3 как не надо

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

1. Надежда, что простой if-чик даст идемпотентность

Встречали когда нибудь такие проверки?

def do_something(idempotency_key) {
if db.exists(idempotency_key) {
return
}
...
}


Такая конструкция очевидно рушится race condition-ом, который рано или поздно произойдет — когда два почти одновременных запроса получат отрицательный результат на db.exists(idempotency_key) и пойдут выполнять логику

2. Слишком широкие границы транзакций

def do_something() {
transaction {
get_from_cache()
call_first_service()
call_second_service()
update_db()
...
}
}


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

3. Пагинация через offset + limit

Довольно закономерное желание сделать пагинацию через такой запрос, база же поддерживает

select * from orders
order by created_at desc
limit 50
offset 100000


Но увы несмотря на наличие индекса на (created_at), база не может за log(offset) найти место, откуда нужно стартовать, и будет честно проходить и скипать эти 100000 строк

🔥 — если нужен пост про "3 как надо"
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥529💅61
⚡️Пара способов, как обеспечить идемпотентность

В продолжение к https://t.iss.one/MicroservicesThoughts/144

Возьму за пример предметную область, где щас работаю:

1. Есть ticket — обращение пользователя в поддержку
2. Есть article — одно сообщение в рамках обращения

Хочется идемпотентно выполнять операцию addArticle(ticketId), чтобы в переписке не было дублей сообщений из-за сетевых проблем и т.п.

1. Через ключ идемпотентности + unique constraint

В запрос на добавление article добавляется параметр idempotency_key

В той же транзакции, где делаем insert into article, мы делаем insert в таблицу с ключами идемпотентности — у этой таблицы должен быть unique constraint

begin;

insert into idempotency_keys; // ошибка если уже существует

insert into article;

commit;


Атомарность — либо оба insert-а выполнятся, либо никакой
Ограничение unique constraint — insert в таблицу с ключами выполняется не более одного раза

Складывая эти факты, получаем что то что нужно: insert в таблицу article выполняется не более одного раза

Вариации:
1.1. Ключ идемпотентности может лежать не в отдельной таблице, а просто как колонка в article
1.2. Можно делать insert on conflict do nothing, чтобы обойтись без эксепшнов

2. Через оптимистические блокировки

В запрос на добавление article добавляется параметр ticket_version

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

begin

insert into article;

update ticket
set version = version + 1
where version = {version}
returning *; // из приложения кидаем ошибку, если не смогли произвести апдейт

commit;


Атомарность — либо и insert, и update версии выполнятся, либо не выполнится ничего
Обновление версии — если в бд лежит ticket с ticket_version = 1, то из двух параллельных запросов на обновление версии выполнится только один. Просто потому что бд гарантирует, что не будет аномалии lost update

И снова складывая эти факты, получаем требуемое
👍35🔥8💅7
⚡️Шардирование без решардирования (pt. 2)

В посте https://t.iss.one/MicroservicesThoughts/138 разобрали, что для шардирования без необходимости решардинга нужно персистентное хранилище маппингов entity_id => shard

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

Очевидная проблема такого подхода — жирная таблица с маппингами, которая к тому же никогда не чистится. Соотв-но с какого-то момента полностью закешировать такое станет невозможно => будет много кеш миссов => походов в базу с маппингами (которая к тому же является spof-ом)

---

И далее идут нюансы

Если у вас autoincremented ids, то эту проблему можно решить достаточно просто — давайте хранить маппинги не для каждой entity_id, а для какого-то ренжа этих entity_id

Получается примерно такая схема (aka range-based mapping)


[10000..19999] -> shard 2
[20000..29999] -> shard 1
...


Правила:
1. Ренжи не пересекаются
2. Если для сущности нет подходящего ренжа, то создается новый

Btw, из приложения можно корректировать, как размазывать данные между шардами просто с помощью длины ренжей. К примеру, для шарда 1 ренжи создаются длиной 5000, а для шарда 2 — длиной 10000. Соотв-но нагрузка будет распределяться примерно как 1:2

---

Пара доводов, почему это может быть ок подходом (или не ок в некоторых случаях):

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

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

Пример 2: сущность долгоживущая. Ренжи по 10к. В таком случае нагрузка уже будет достаточно мягко распределяться по шардам, и не будет burst-ов на конкретный шард

2. Такие ренжи легко закешировать

К примеру, если у вас 1млрд сущностей и ренжи по 10к, то это выльется в 100000 маппингов, которые займут ~5мб, что легко влезает в оперативку приложения

---

А теперь не про autoincremented ids

Тут уже скорее всего без решардинга не обойтись, и все выльется в те самые виртуальные бакеты + решардинг именно между этими виртуальными бакетами (однако еще есть способ с вшиванием id шарда в id сущности)

p.s.: пост предполагался про виртуальные бакеты, но чуть не туда понесло

Предыдущая часть

Следующая часть
👍35💅1
⚡️Шардирование без решардирования (pt. 3)

Возьмем максимально наглые требования

1. Хотим шардирование
2. Логика шардирования описывается внутри приложения и может меняться
3. Добавление новых шардов происходит без решардирования
4. Нет spof-а в виде базы с маппингами entity_id -> shard

---

И несмотря на противоречивость, это вполне себе достижимо

Возьмем отсюда шаги

1. Нам приходит запрос по entity_id
2. Мы идем в хранилище маппингов, выясняем в каком shard лежит этот entity_id

И склеим их в один — внутри идентификатора сущности entity_id уже будет номер шарда, где она лежит

Например, для идентификатора entity_id = 1_765
Номер шарда = 1
Локальный id в рамках шарда = 765

Локальный — в том смысле, что можно использовать локальный для шарда сиквенс, т.е. могут быть айдишники 1_765 (в первом шарде) и 2_765 (во втором шарде)

Всё это убирает необходимость где-то отдельно хранить маппинги — они уже вклеены в id сущности

---

Главный минус такого подхода — переложить сущность в другой шард невозможно, иначе нам придется менять id сущности

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

- Нет spof-а в виде базы с маппингами
- Нет промежуточного шага с выяснением шарда
- Нет решардинга
- Можно делать логику шардирования любой сложности и менять ее в любой момент
- Вполне себе скейлится на огромные объемы

И еще одно неочевидное преимущество — шардирование зачастую делается по "основным агрегатам" в системе, например, Order. И чтобы запросить какую-то дочернюю сущность заказа, нужно в запросе передавать id заказа (чтобы вообще понять в каком шарде лежат эти дочерние сущности). Подход выше такую проблему нивелирует, потому что и для дочерних сущностей сразу будет понятно, в каком шарде они лежат

Предыдущая часть

🔥 — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥61👍1
Как расти разработчику в компании

А что значит "расти"? Я бы выделил два направления:

1. Рост по хард скиллам
2. Рост по карьере внутри компании

И на практике оказывается, что первое не всегда влечет второе

---

Почему так происходит?

У каждого разработчика есть своя зона ответственности — полянка, за которую он отвечает: в рамках нее задачи делаются в срок и с приемлемым уровнем качества, не плодится техдолг, и в целом "полянка" работает стабильно

И рост по карьере коррелирует именно с размером и сложностью этой зоны ответственности. Понять это можно на таком примере

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

Кого из них повысят, думаю, очевидно

---

Так в чем же проблема просто взять и расширить зону ответственности? А в том, что начинают возникать ситуации, которых раньше не было

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

2. Появляется много мелких задач, которые физически нельзя переварить за один день — здесь поможет навык приоритизации

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

---

И к сожалению, таким вещам обычно не учат — у кого-то они получаются сами по себе, а кому не повезло — не получаются. Закрыть пробелы по таким скиллам поможет канал Андрея — Head of Product Development в Яндекс Лавке. Он рассказывает про то, как себя вести в подобных "менеджерских" ситуациях

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

🔥 — если подписались
🔥44👍5🤔4
Полу-оффтоп к посту выше

Занимательный способ, как проверить растете вы или нет — вновь прочитать статью, которую вы не до конца понимали пару лет назад

У меня так внезапно получилось с https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html, на которую я натыкался еще будучи стажером. Btw рекомендую почитать, статья из серии "как еще больше бояться программировать"

Для владельцев тг каналов есть еще один способ — почитайте свои посты 1-2 годовой давности. Если фейспалмите, то все хорошо
🔥27😁10👍6
⚡️Немного про визуализацию архитектуры

У некоторых людей кубики и стрелочки в миро — основной инструмент для визуалиации архитектуры. В простых случаях с этим нет вообще никаких проблем

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

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

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

C4

Разбивает диаграмму на 4 слоя: системы, контейнеры, компоненты, код (обычно не рисуют, тк часто меняется). Переход с n-го на n+1-ый уровень это "зум" в какой-то кусочек диаграммы. Например, на диаграмме контейнеров выбираем контейнер, смотрим диаграмму компонентов по этому контейнеру

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

Sequence diagram

Это как раз про порядок взаимодействий. На "плоской" C4 диаграмме зачастую сложно понять в какой последовательности кто кого вызывает. Диаграммки последовательностей прекрасно закрывают эту потребность

State diagram

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

---

Если вы как и я не любите собирать диаграммы в визуальном редакторе, то есть https://plantuml.com/ru/, где каждый из типов выше можно просто описать текстом и зарендерить прямо в браузере
👍58🔥6💅3
Прохладная история про то, как легко положить приложение

Есть некоторая запись в базе, запрос на обновление выглядит так:

begin;
-- че то поделали 50мс
update;
commit;


Такая транзакция выполняется ~50мс, ничего аномального

И представим, что такие транзакции бьются в одну и ту же сущность
1. rps = 100
2. размер connection pool-а = 500
3. таймаут на получение connection-а = 10с

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

Пропускная способность получается 1000 / 50 = 20 транзакций в секунду => ежесекундно "очередь на блокировку" будут увеличиваться на 100 - 20 = 80 транзакций

То есть наш пул в 500 соединений через 500 / 80 = 6.25 секунд полностью забьется (даже не дошли до connection timeout-а) => приложение будет пятисотить / дольше отвечать, ожидая коннекшна

---

Че с этим делать? (пункты не упорядочены)
- Смотреть на бизнес логику, какого хера по одной сущности идет 100rps
- Тюнить connection timeout
- Ставить нормальную очередь перед апдейтами
- Распред локи (чтобы ждали лока в условном редисе, а не занимали конекшн)
- rps-лимитер
- отдавать 4xx, если не получается сразу взять лок на сущность
👍74💅7
⚡️Как мы мониторим приложения

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

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

Пример, как такое можно устроить:

1 уровень

5хх/тайминги на балансере или api gateway — основная метрика

2 уровень

Выписываем основные бизнес-функции, и для каждой строим dataflow — как данные ходят между сервисами. На каждый такой межсервисный стык заводим график

Например, если данные ходят a -> b -> c, то заводим два графика

a -> b
b -> c

Утверждается, что если на каждом таком стыке нет ошибок и задержек, то скорее всего бизнес-функция работает нормально

3 уровень

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

---

Такое разделение позволяет

1. Очень компактно разместить графики. Можно сделать один дашборд, где будут метрики 1 и 2 уровня, а также ссылки на подробные дашборды 3-го уровня

2. Быстро искать root cause во время инцидентов: проблема с какой-то бизнес функцией => увидели на каком стыке проблема => посмотрели дашборд по конкретному сервису => нашли проблему

---

Рассказывайте в комментах, какая у вас структура мониторингов, будет интересно почитать)
👍43💅11
⚡️Data retention в постгресе

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

Есть два подхода со своим трейдоффом

1. Обычное удаление старых записей через delete


delete from tbl
where id in (
select id
from tbl
where created_at < now() - interval '7 days'
order by created_at
limit 1000
);


Такое крутится либо постоянно в фоне (условно раз в секунду удаляется небольшая устаревшая пачка) либо запускается по крону и в while (true) удаляется за раз большое количество пачек

2. Партицирование и drop partition

Таблица партицируется по created_at. И вместо удаления отдельных записей удаляется целая партиция

---

В чем трейдофф?

Первый вариант легко реализовать, но образуется bloat при удалении из-за MVCC

Второй вариант реализовать сложнее, но никакого bloat-а нет. Удаление партиции — это просто удаление физического файла
🔥40👍11💅3🤔21
Привет. Мне тут закинули фидбек, что порой в постах мало деталей, из-за чего могут упускаться важные риски/границы применимости/...

В этой связи у меня к вам вопрос — какой формат постов более предпочтителен?
⚡️Про bloat в pg и как с ним бороться

При обновлениях и удалениях строк postgres физически не удаляет/изменяет старую версию строки, а просто создает новую. У каждой такой версии есть поля:

1. xmin — номер транзакции, который создал версию строки
2. xmax — номер транзакции, который удалил версию строки

Чтобы проникнуться идеей xmin/xmax, можно почитать пост, как это позволяет обеспечить snapshot isolation

---

Окей, мы пообновляли строку, у нас появилось несколько версий строки со своими xmin/xmax. Интуитивно кажется, что нам не нужны все эти версии, ведь мы хотим видить только последнее, актуальное состояние строки. Так и есть — если xmax != 0 (версия строки кем-то удалена) и xmax < минимальный xid, среди активных транзакций (версия строки не видна ни для одной живой транзакции), то эта версия больше не нужна и ее можно удалить. Такие версии строк называются dead tuples

"Удалением" dead tuples занимается autovacuum: он помечает, что фрагменты страниц, где раньше лежали dead tuples, можно переиспользовать для записи новых данных. Важно отметить, что автовакуум никак не "двигает" живые данные и не освобождает физическое место. Он просто говорит, что в текущих страницах есть вот такие дырки, куда теперь можно что-то записать

К слову, минимальный xid, среди активных транзакций называется горизонтом базы. То есть автовакуум может удалять только те версии строк, которые "старше" горизонта базы. Это еще один аргумент, почему долгие транзакции — зло: из-за них автовакуум встает, так как такие транзакции долго держат горизонт

---

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

Несмотря на то, что у нас есть "дырки" в страницах, куда можно записать новые данные, этого не всегда хватает. Например, "дырка" может быть слишком маленькой, чтобы туда записать версию строки. Либо таких дырок в моменте может быть недостаточно (например, при массовых апдейтах/удалениях) — все это приводит к тому, что постгрес вынужден аллоцировать новые страницы => растет физический размер таблицы

---

Суммаризируя, table bloating — это ситуация, когда физический размер таблицы существенно превосходит размер датасета. Это происходит из-за:

1. Накопления dead tuples
2. Фрагментации таблицы, когда текущих "дырок" не хватает для записи новых данных и приходится выделять новые страницы

Для борьбы с фрагментацией у постгреса есть vacuum full — он берет эксклюзивную блокировку на таблицу и полностью ее перезаписывает в новый файл "без дырок". Однако на практике он редко применим, поскольку он буквально вызывает даунтайм сервиса (возможно на несколько часов, если таблица большая)

Для борьбы с фрагментацией без даунтайма есть утилита pg_repack

👍 — если нужен пост про принцип работы pg_repack
👍234🔥611
⚡️Про bloat в pg и как с ним бороться (pt. 2)

Перфоманс ревью почти закончилось, поэтому я снова в строю

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

Одна из альтернатив — расширение pg_repack, которое позволяет пересобрать таблицы и индексы без долгих блокировок

---

Можно отдельно рассмотреть два режима

1. Пересбор только индексов (--only-indexes)

Это простой случай — репак просто подменяет индекс:

1) Создает копию индекса через create index concurrently
2) Удаляет старый индекс, а новый переименовывает в старый

В целом такое спокойно делается и без репака

2. Пересбор таблиц

Тут уже сложнее — нужно создать копию таблицы, при этом по дороге не просыпать данные, и не провоцировать долгие блокировки

Конкретные DDL/DML можно почитать тут, если в общих чертах, то шаги такие:

1) Под access exclusive блокировкой: создаем лог-таблицу для трекинга изменений и триггер, который будет "реплицировать" изменения из основной таблицы в лог-таблицу
2) Создаем новую таблицу-копию (буквально через insert into new select * from old)
3) Создаем индексы на копию
4) Накатываем на копию "лаг", который за это время образовался в лог-таблице
5) Под access exclusive блокировкой: атомарно подменяем старую и новую таблицы, старую удаляем

Итого имеем полностью пересобранную таблицу и индексы без bloat-а

---

Что важно знать и что может пойти не так

1. Во write-intensive базах может быть проблематично взять access exclusive блокировки

2. Репак создает копию таблицы через insert .. select *, что может генерить резкую IO нагрузку на диск. Встроенной функциональности ее ограничить в репаке нет, но можно придумать workaround-ы наподобие такого через cgroup
2.1. — надо иметь на диске свободного пространства ~x2 от размера таблицы
2.2. — резко накапливается WAL, что приводит ко всем сопутствующим проблемам
2.3. — такое действие провоцирует долгую транзакцию, которая держит горизонт => встает автовакуум => вновь копится bloat

---

Overall, если у вас нагруженная система, и при этом вы можете выделить окно обслуживания, в котором нагрузка будет сильно меньше — pg_repack скорее всего вам подойдет

Если же система нагруженная, но окошек с низкой нагрузкой нет, то pg_repack вероятно вам просто положит базу. Поэтому стоит рассмотреть более "онлайновые" инструменты — pg_squeeze и pgcompacttable

По традиции, 👍 — если нужен пост про них
👍172🔥31
⚡️Про неопределенности в продуктовой разработке

При разработке продуктовых фичей не всегда на старте есть четкие ФТ, НФТ — иногда есть просто пользовательский юзкейс, который хочется реализовать. А дальше смотрим, насколько он вписывается в систему, насколько долго / сложно такое делать, какие дает перспективы на будущее и т.д.

Побочный эффект — на старте можно предложить примерно бесконечное множество решений, что в свою очередь может приводить к бесконечным встречам и обсуждениям одного и того же по кругу

---

И чтобы это минимизировать, я люблю такой метод:

1. Берем любое (обычно самое простое) решение, которое реализует юзкейс

2. Собираем возражения от продакта / разработчиков

3. Проводим gap-анализ:
Желаемое решение = Текущее решение + Отклонение. Если отклонение небольшое, то докручиваем текущее решение. Если же отклонение сильное, придумываем новое решение, которое сразу закроет это отклонение

*Повторяем, пока не дойдем до решения, с которым согласен некий кворум участников встречи

---

Это работает по одной простой причине — убивает страх чистого листа. Вместо того чтобы с нуля придумывать решение, вы уже работаете с некоторой "базой", которую докручиваете
👍52🤔212
Как-то даже не верится, что канал, созданный на спор и по приколу дошел до 7к

Всем спасибо, кто читает и поддерживает!
👍209🔥66💅14🙏91
⚡️Немного про векторные базы данных

Учитывая что аббревиатуры LLM, AI и подобные я стал слышать мучительно часто, внутренний голос заставил что-то почитать на эту тему. Поэтому ловите нетипичный пост

tl;dr зачем нужны векторные базы данных
1. Добавлять в базу данных числовые вектора
2. По заданному вектору находить похожие

---

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

В целом, это позволяет осуществлять поиск похожих объектов, даже если сами объекты "сложные" — объект -> эмбеддинг -> похожие эмбеддинги -> похожие объекты:

- Поиск схожих картинок
- Рекомендательные системы
- RAG — где мы по запросу пользователя ищем подходящие знания в базе знаний
... и так далее

И встает вопрос, как по числовому вектору находить ближайшие

---

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

Один из частоиспользуемых приближенных алгоритмов — HNSW: можно почитать прекрасную статью с картинками вот тут

Если вкратце:

Есть более простая версия — NSW, где определенным образом строится граф (каждый вектор — вершина). И чтобы найти ближайший вектор выполняется такой жадный алгоритм:

1. Берется некоторая начальная вершина
2. Выбираем вершину соседа, которая ближе всего к целевой
3. Повторяем пока не дойдем до "локального минимума"

HNSW — это развитие этой идеи, где появляется иерархия. Есть основной граф, это самый нижний слой. Каждый следующий слой является все меньшим подмножеством графа, при этом содержит все более "длинные" ребра. Концептуально очень похоже на skiplist

Принцип работы примерно такой, что

1. Делаем NSW на верхнем слое, пока не упремся в локальный минимум
2. Из этой вершины уходим на предыдущий слой
3. Повторяем, пока не дойдем до самого нижнего слоя

Утверждается, что такая эвристика позволяет сильно лучше обходить локальные минимумы => показывает лучше качество

Например, этот алгоритм используется по дефолту в одной из самых популярных векторных БД — Qdrant

---

Тема весьма специфичная для привычного бэкенда, поэтому если кому-то вдруг доводилось такое применять, обязательно пишите!
👍45🔥92💅1
Учитывая, что подавлющая часть аудитории — действующие разработчики и руководители, стало интересно а что по деньгам

Число в опросе — усредненный total (с премиями и бонусами), NET, в тыс. рублей
Anonymous Poll
8%
<100
12%
100-200
21%
200-300
26%
300-400
17%
400-500
7%
500-600
3%
600-700
6%
>700
⚡️Про петли положительной обратной связи

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

В технике: "сервер долго отвечает => клиенты падают по таймауту => клиенты ретраят => сервер еще хуже отвечает"

В процессах: "низкая скорость разработки => забьем на кодревью => качество кода ниже => скорость разработки еще ниже"

В обычной жизни: "плохой сон => выше тревожность => еще хуже сон"

И так далее

---

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

Как такое дебажить? В общем-то простого способа нет — нужно по честному взять (а сначала найти) все переменные и далее

1. Построить граф, где ребро (X, Y) ~ переменная X влияет на Y. Влияние может быть как положительным, так и отрицательным. Такая диаграмма называется Causal loop diagram

2. Циклы с четным числом "отрицательных влияний" — это и будут feedback loop-ы

---

Вообще, этот подход охватывается умной вещью, называемой системной динамикой, а неплохую статью по теме можно почитать тут
👍48🔥6💅221
⚡️Почему алгосы — это полезно

Сразу отбросим вариант, когда под алгоритмами понимается "я наизусть выучил решение 50 литкод задач перед собесом"))

Решение алгоритмических задач с условного кодфорсеса развивает два важных навыка:

1. Отделять суть от формы

Из абстрактного "на Машу падали яблоки" на "были такие-то данные, которые так-то будут меняться, нужно научиться быстро отвечать на такие запросы"

2. Проводить аналогии

Это примерно про следующее: понять, что текущая задача (или ее часть) концептуально похожа на ту, что ты уже решал, либо на готовый алгоритм / структуру данных / etc. И из этого синтезировать новое решение

---

Как это помогает в обычной разработке?

Отделение сути от формы — в переводе бизнесовых хотелок на технический язык

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

p.s.: все вышесказанное относится не только к алгоритмам, а примерно к любой сложной математике/информатике/etc
🤔55👍41🔥12😁54💅2