⚡️Шардирование без решардирования (pt. 2)
В посте https://xn--r1a.website/MicroservicesThoughts/138 разобрали, что для шардирования без необходимости решардинга нужно персистентное хранилище маппингов entity_id => shard
Суть в том, что при добавлении сущности в бд мы сразу записываем, к какому шарду она относится, и эту запись больше никогда не трогаем. Соотв-но если добавится новый шард, то это не принесет никаких проблем — шард для сущности уже зафиксирован
Очевидная проблема такого подхода — жирная таблица с маппингами, которая к тому же никогда не чистится. Соотв-но с какого-то момента полностью закешировать такое станет невозможно => будет много кеш миссов => походов в базу с маппингами (которая к тому же является spof-ом)
---
И далее идут нюансы
Если у вас autoincremented ids, то эту проблему можно решить достаточно просто — давайте хранить маппинги не для каждой entity_id, а для какого-то ренжа этих entity_id
Получается примерно такая схема (aka range-based mapping)
Правила:
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.: пост предполагался про виртуальные бакеты, но чуть не туда понесло
Предыдущая часть
Следующая часть
В посте https://xn--r1a.website/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.: пост предполагался про виртуальные бакеты, но чуть не туда понесло
Предыдущая часть
Следующая часть
Telegram
Microservices Thoughts
⚡️Шардирование без решардирования
Одна из основных проблем шардирования — решардинг, то есть когда при добавлении нового шарда нужно перераскидать данные между шардами. Почему так случается?
Представьте есть 3 шарда => shard_count = 3
Шард выбирается как…
Одна из основных проблем шардирования — решардинг, то есть когда при добавлении нового шарда нужно перераскидать данные между шардами. Почему так случается?
Представьте есть 3 шарда => shard_count = 3
Шард выбирается как…
👍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 заказа (чтобы вообще понять в каком шарде лежат эти дочерние сущности). Подход выше такую проблему нивелирует, потому что и для дочерних сущностей сразу будет понятно, в каком шарде они лежат
Предыдущая часть
🔥 — если было полезно
Возьмем максимально наглые требования
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.: сам я этот канал читаю уже больше года, поэтому могу с чистой совестью рекомендовать его как тимлидам, так и амбициозным разработчикам, нацеленным на рост
🔥 — если подписались
А что значит "расти"? Я бы выделил два направления:
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 годовой давности. Если фейспалмите, то все хорошо
Занимательный способ, как проверить растете вы или нет — вновь прочитать статью, которую вы не до конца понимали пару лет назад
У меня так внезапно получилось с 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/, где каждый из типов выше можно просто описать текстом и зарендерить прямо в браузере
У некоторых людей кубики и стрелочки в миро — основной инструмент для визуалиации архитектуры. В простых случаях с этим нет вообще никаких проблем
Но когда нужно описать что-то более менее объемное и/или сложное, зачастую приходим к двум ключевым проблемам:
- На одной диаграмме есть вообще все, и ее трудно осознать
- Неочевиден порядок, в котором взаимодействуют компоненты этой диаграммы
В таких случаях бывает удобно подробить одну большую диаграмму на несколько меньших
Имхо, джентльменский набор, которого хватает для большинства случаев:
C4
Разбивает диаграмму на 4 слоя: системы, контейнеры, компоненты, код (обычно не рисуют, тк часто меняется). Переход с n-го на n+1-ый уровень это "зум" в какой-то кусочек диаграммы. Например, на диаграмме контейнеров выбираем контейнер, смотрим диаграмму компонентов по этому контейнеру
Разумеется, не всегда нужны все 4 слоя, это просто удобный способ обозначить, элементы какого уровня абстракции мы хотим видеть на конкретной диаграмме
Sequence diagram
Это как раз про порядок взаимодействий. На "плоской" C4 диаграмме зачастую сложно понять в какой последовательности кто кого вызывает. Диаграммки последовательностей прекрасно закрывают эту потребность
State diagram
Часто в рамках приложения есть какая-то сущность с каким-то жизненным циклом. Чтобы ответить на вопрос, какие бывают статусы у сущности, и кто инициирует переходы между ними, можно воспользоваться диаграммой состояния. Это буквально визуализация конечного автомата
---
Если вы как и я не любите собирать диаграммы в визуальном редакторе, то есть https://plantuml.com/ru/, где каждый из типов выше можно просто описать текстом и зарендерить прямо в браузере
👍58🔥6💅3
Прохладная история про то, как легко положить приложение
Есть некоторая запись в базе, запрос на обновление выглядит так:
Такая транзакция выполняется ~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, если не получается сразу взять лок на сущность
Есть некоторая запись в базе, запрос на обновление выглядит так:
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 во время инцидентов: проблема с какой-то бизнес функцией => увидели на каком стыке проблема => посмотрели дашборд по конкретному сервису => нашли проблему
---
Рассказывайте в комментах, какая у вас структура мониторингов, будет интересно почитать)
В продакт менеджменте есть концепция дерева метрик — грубо говоря когда метрики выстраиваются в иерархию, и метрика-родитель зависит от метрик-детей
В технических же мониторингах такой структуры зачастую нет, и на один дашборд навалено всё что можно. Но на самом деле ничего не мешает перенести эту идею с иерархией и на технические мониторинги
Пример, как такое можно устроить:
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
Такое крутится либо постоянно в фоне (условно раз в секунду удаляется небольшая устаревшая пачка) либо запускается по крону и в while (true) удаляется за раз большое количество пачек
2. Партицирование и drop partition
Таблица партицируется по created_at. И вместо удаления отдельных записей удаляется целая партиция
---
В чем трейдофф?
Первый вариант легко реализовать, но образуется bloat при удалении из-за MVCC
Второй вариант реализовать сложнее, но никакого bloat-а нет. Удаление партиции — это просто удаление физического файла
Встроенной функциональности, чтобы удалять записи по истечении некоторого срока, в постгресе нет. Поэтому такое делается вручную
Есть два подхода со своим трейдоффом
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🤔2✍1
Привет. Мне тут закинули фидбек, что порой в постах мало деталей, из-за чего могут упускаться важные риски/границы применимости/...
В этой связи у меня к вам вопрос — какой формат постов более предпочтителен?
В этой связи у меня к вам вопрос — какой формат постов более предпочтителен?
⚡️Про 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
При обновлениях и удалениях строк 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
Telegram
Microservices Thoughts
⚡️Принцип работы snapshot isolation (aka repeatable read) в postgres
Изоляция repeatable read избавляет от неповторяющегося чтения — ситуации, когда одна и та же строка запрашивается дважды в рамках транзакции, но результаты чтения получаются разными
begin;…
Изоляция repeatable read избавляет от неповторяющегося чтения — ситуации, когда одна и та же строка запрашивается дважды в рамках транзакции, но результаты чтения получаются разными
begin;…
👍232🔥6✍1 1
⚡️Про 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
По традиции, 👍 — если нужен пост про них
Перфоманс ревью почти закончилось, поэтому я снова в строю
В предыдущем посте посте обсудили, что таблица может блоатиться из-за фрагментации, и один из способов борьбы — это пересбор таблицы с помощью 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
По традиции, 👍 — если нужен пост про них
Telegram
Microservices Thoughts
⚡️Про bloat в pg и как с ним бороться
При обновлениях и удалениях строк postgres физически не удаляет/изменяет старую версию строки, а просто создает новую. У каждой такой версии есть поля:
1. xmin — номер транзакции, который создал версию строки
2. xmax…
При обновлениях и удалениях строк postgres физически не удаляет/изменяет старую версию строки, а просто создает новую. У каждой такой версии есть поля:
1. xmin — номер транзакции, который создал версию строки
2. xmax…
👍172🔥3 1
⚡️Про неопределенности в продуктовой разработке
При разработке продуктовых фичей не всегда на старте есть четкие ФТ, НФТ — иногда есть просто пользовательский юзкейс, который хочется реализовать. А дальше смотрим, насколько он вписывается в систему, насколько долго / сложно такое делать, какие дает перспективы на будущее и т.д.
Побочный эффект — на старте можно предложить примерно бесконечное множество решений, что в свою очередь может приводить к бесконечным встречам и обсуждениям одного и того же по кругу
---
И чтобы это минимизировать, я люблю такой метод:
1. Берем любое (обычно самое простое) решение, которое реализует юзкейс
2. Собираем возражения от продакта / разработчиков
3. Проводим gap-анализ:
Желаемое решение = Текущее решение + Отклонение. Если отклонение небольшое, то докручиваем текущее решение. Если же отклонение сильное, придумываем новое решение, которое сразу закроет это отклонение
*Повторяем, пока не дойдем до решения, с которым согласен некий кворум участников встречи
---
Это работает по одной простой причине — убивает страх чистого листа. Вместо того чтобы с нуля придумывать решение, вы уже работаете с некоторой "базой", которую докручиваете
При разработке продуктовых фичей не всегда на старте есть четкие ФТ, НФТ — иногда есть просто пользовательский юзкейс, который хочется реализовать. А дальше смотрим, насколько он вписывается в систему, насколько долго / сложно такое делать, какие дает перспективы на будущее и т.д.
Побочный эффект — на старте можно предложить примерно бесконечное множество решений, что в свою очередь может приводить к бесконечным встречам и обсуждениям одного и того же по кругу
---
И чтобы это минимизировать, я люблю такой метод:
1. Берем любое (обычно самое простое) решение, которое реализует юзкейс
2. Собираем возражения от продакта / разработчиков
3. Проводим gap-анализ:
Желаемое решение = Текущее решение + Отклонение. Если отклонение небольшое, то докручиваем текущее решение. Если же отклонение сильное, придумываем новое решение, которое сразу закроет это отклонение
*Повторяем, пока не дойдем до решения, с которым согласен некий кворум участников встречи
---
Это работает по одной простой причине — убивает страх чистого листа. Вместо того чтобы с нуля придумывать решение, вы уже работаете с некоторой "базой", которую докручиваете
👍52🤔21 2
⚡️Немного про векторные базы данных
Учитывая что аббревиатуры LLM, AI и подобные я стал слышать мучительно часто, внутренний голос заставил что-то почитать на эту тему. Поэтому ловите нетипичный пост
tl;dr зачем нужны векторные базы данных
1. Добавлять в базу данных числовые вектора
2. По заданному вектору находить похожие
---
Если чуть подробнее, то сложные объекты (текст, картинки, ...) можно закодировать в виде числовых векторов (эмбеддингов). От эмбеддингов ожидается, что они в себе сохраняют какую-то семантическую информацию об объекте, и как следствие, ожидается, что если эмбеддинги двух объектов похожи, то скорее всего сами объекты похожи
В целом, это позволяет осуществлять поиск похожих объектов, даже если сами объекты "сложные" — объект -> эмбеддинг -> похожие эмбеддинги -> похожие объекты:
- Поиск схожих картинок
- Рекомендательные системы
- RAG — где мы по запросу пользователя ищем подходящие знания в базе знаний
... и так далее
И встает вопрос, как по числовому вектору находить ближайшие
---
Конечно, интересен случай, когда данных много. В таких ситуациях зачастую обменивают точный поиск на приближенный в пользу скорости
Один из частоиспользуемых приближенных алгоритмов — HNSW: можно почитать прекрасную статью с картинками вот тут
Если вкратце:
Есть более простая версия — NSW, где определенным образом строится граф (каждый вектор — вершина). И чтобы найти ближайший вектор выполняется такой жадный алгоритм:
1. Берется некоторая начальная вершина
2. Выбираем вершину соседа, которая ближе всего к целевой
3. Повторяем пока не дойдем до "локального минимума"
HNSW — это развитие этой идеи, где появляется иерархия. Есть основной граф, это самый нижний слой. Каждый следующий слой является все меньшим подмножеством графа, при этом содержит все более "длинные" ребра. Концептуально очень похоже на skiplist
Принцип работы примерно такой, что
1. Делаем NSW на верхнем слое, пока не упремся в локальный минимум
2. Из этой вершины уходим на предыдущий слой
3. Повторяем, пока не дойдем до самого нижнего слоя
Утверждается, что такая эвристика позволяет сильно лучше обходить локальные минимумы => показывает лучше качество
Например, этот алгоритм используется по дефолту в одной из самых популярных векторных БД — Qdrant
---
Тема весьма специфичная для привычного бэкенда, поэтому если кому-то вдруг доводилось такое применять, обязательно пишите!
Учитывая что аббревиатуры LLM, AI и подобные я стал слышать мучительно часто, внутренний голос заставил что-то почитать на эту тему. Поэтому ловите нетипичный пост
tl;dr зачем нужны векторные базы данных
1. Добавлять в базу данных числовые вектора
2. По заданному вектору находить похожие
---
Если чуть подробнее, то сложные объекты (текст, картинки, ...) можно закодировать в виде числовых векторов (эмбеддингов). От эмбеддингов ожидается, что они в себе сохраняют какую-то семантическую информацию об объекте, и как следствие, ожидается, что если эмбеддинги двух объектов похожи, то скорее всего сами объекты похожи
В целом, это позволяет осуществлять поиск похожих объектов, даже если сами объекты "сложные" — объект -> эмбеддинг -> похожие эмбеддинги -> похожие объекты:
- Поиск схожих картинок
- Рекомендательные системы
- RAG — где мы по запросу пользователя ищем подходящие знания в базе знаний
... и так далее
И встает вопрос, как по числовому вектору находить ближайшие
---
Конечно, интересен случай, когда данных много. В таких ситуациях зачастую обменивают точный поиск на приближенный в пользу скорости
Один из частоиспользуемых приближенных алгоритмов — HNSW: можно почитать прекрасную статью с картинками вот тут
Если вкратце:
Есть более простая версия — NSW, где определенным образом строится граф (каждый вектор — вершина). И чтобы найти ближайший вектор выполняется такой жадный алгоритм:
1. Берется некоторая начальная вершина
2. Выбираем вершину соседа, которая ближе всего к целевой
3. Повторяем пока не дойдем до "локального минимума"
HNSW — это развитие этой идеи, где появляется иерархия. Есть основной граф, это самый нижний слой. Каждый следующий слой является все меньшим подмножеством графа, при этом содержит все более "длинные" ребра. Концептуально очень похоже на skiplist
Принцип работы примерно такой, что
1. Делаем NSW на верхнем слое, пока не упремся в локальный минимум
2. Из этой вершины уходим на предыдущий слой
3. Повторяем, пока не дойдем до самого нижнего слоя
Утверждается, что такая эвристика позволяет сильно лучше обходить локальные минимумы => показывает лучше качество
Например, этот алгоритм используется по дефолту в одной из самых популярных векторных БД — Qdrant
---
Тема весьма специфичная для привычного бэкенда, поэтому если кому-то вдруг доводилось такое применять, обязательно пишите!
www.pinecone.io
Hierarchical Navigable Small Worlds (HNSW)
Hierarchical Navigable Small World (HNSW) graphs are among the top-performing indexes for vector similarity search. HNSW is a hugely popular technology that time and time again produces state-of-the-art performance with super fast search speeds and fantastic…
👍45🔥9 2💅1
Учитывая, что подавлющая часть аудитории — действующие разработчики и руководители, стало интересно а что по деньгам
Число в опросе — усредненный total (с премиями и бонусами), NET, в тыс. рублей
Число в опросе — усредненный 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-ы
---
Вообще, этот подход охватывается умной вещью, называемой системной динамикой, а неплохую статью по теме можно почитать тут
На мой взгляд, одни из самых интересных проблем — это ситуации, когда результат действий начинает усиливать сам себя. Причем такое случается во всех сферах:
В технике: "сервер долго отвечает => клиенты падают по таймауту => клиенты ретраят => сервер еще хуже отвечает"
В процессах: "низкая скорость разработки => забьем на кодревью => качество кода ниже => скорость разработки еще ниже"
В обычной жизни: "плохой сон => выше тревожность => еще хуже сон"
И так далее
---
В простых случаях влияния довольны очевидны, и их можно представить буквально в голове. Но самое интересное, когда переменных не 2, 3, 4, а несколько десятков
Как такое дебажить? В общем-то простого способа нет — нужно по честному взять (а сначала найти) все переменные и далее
1. Построить граф, где ребро (X, Y) ~ переменная X влияет на Y. Влияние может быть как положительным, так и отрицательным. Такая диаграмма называется Causal loop diagram
2. Циклы с четным числом "отрицательных влияний" — это и будут feedback loop-ы
---
Вообще, этот подход охватывается умной вещью, называемой системной динамикой, а неплохую статью по теме можно почитать тут
👍47🔥6💅2 2✍1
⚡️Почему алгосы — это полезно
Сразу отбросим вариант, когда под алгоритмами понимается "я наизусть выучил решение 50 литкод задач перед собесом"))
Решение алгоритмических задач с условного кодфорсеса развивает два важных навыка:
1. Отделять суть от формы
Из абстрактного "на Машу падали яблоки" на "были такие-то данные, которые так-то будут меняться, нужно научиться быстро отвечать на такие запросы"
2. Проводить аналогии
Это примерно про следующее: понять, что текущая задача (или ее часть) концептуально похожа на ту, что ты уже решал, либо на готовый алгоритм / структуру данных / etc. И из этого синтезировать новое решение
---
Как это помогает в обычной разработке?
Отделение сути от формы — в переводе бизнесовых хотелок на технический язык
Проведение аналогий — в придумывании решения технической задачи и видения, а как вообще концептуально должна быть устроена архитектура
p.s.: все вышесказанное относится не только к алгоритмам, а примерно к любой сложной математике/информатике/etc
Решение алгоритмических задач с условного кодфорсеса развивает два важных навыка:
1. Отделять суть от формы
Из абстрактного "на Машу падали яблоки" на "были такие-то данные, которые так-то будут меняться, нужно научиться быстро отвечать на такие запросы"
2. Проводить аналогии
Это примерно про следующее: понять, что текущая задача (или ее часть) концептуально похожа на ту, что ты уже решал, либо на готовый алгоритм / структуру данных / etc. И из этого синтезировать новое решение
---
Как это помогает в обычной разработке?
Отделение сути от формы — в переводе бизнесовых хотелок на технический язык
Проведение аналогий — в придумывании решения технической задачи и видения, а как вообще концептуально должна быть устроена архитектура
p.s.: все вышесказанное относится не только к алгоритмам, а примерно к любой сложной математике/информатике/etc
🤔54👍41🔥12😁5 4💅2
⚡️Флапающие алерты
Представьте, что у вас есть алерт на кол-во ошибок — если 5хх ответов больше 1%, то алерт "загорается"
Обычно это выглядит так:
1. Берется окошко, скажем 3 мин
2. Считается доля 5хх: sum(requests[status = 5xx]) / sum(requests)
3. Алармим, если доля > 0.01
Так мы по сути считаем усредненную долю ошибок за последние 3 минуты
---
Что может поломать такую схему? Короткие всплески, когда процент 5хх пару секунд очень высокий. Это может происходить по разным причинам: случайные/локальные сбои, переключения мастера, накатка миграции etc
Поскольку мы считаем avg по окошку, такие короткие всплески скорее всего будут триггерить алерт, пока наблюдаемое окошко "не перескачет" этот всплеск
Пример: 100 rps; окошко 3 минуты; пятисотили 2 секунды
доля 5хх = (100rps * 2 сек) / (100rps * 180сек) = 1/90 > 0.01 — алерт загорелся
Хотите ли вы, чтобы алерт загорался в такой ситуации? depends
Далее рассмотрим случай, когда не хотим
---
Из банальных советов:
1. Увеличить окошко (3min -> 5min)
Шире окно, выше шанс, что пик размажется. Но тут идет трейдофф с риском появлений false-negative, когда пропустим реальную проблему
2. Увеличить трешхолд (0.01 -> 0.03)
Примерно та же история, что с окошком
Из менее банальных:
3. Пересмотреть алерт (avg -> percentile)
Например, так
1) Берем такую метрику requests[status = 5xx] / requests — это график доли пятисоток
2) Берем p75 от нее — получаем число. Это число означает, что 25% времени окошка доля пятисоток была выше, чем это число
3) Поджигаем если p75 > threshold
То есть мы ушли от "среднее кол-во пятисоток в окне нарушает трешхолд" до ">25% времени нарушался трешхолд пятисоток"
Частные случаи:
p0 (минимум) — трешхолд нарушался 100% времени (в каждой точке окна)
p50 (медиана) — трешхолд нарушался половину времени
p100 (максимум) — трешхолд нарушился буквально в одной точке
4. Добавить сглаживание
Вместо того, чтобы смотреть на исходную метрику requests[status = 5xx] / sum(requests), можно добавить сглаживание: например, каждая точка будет показывать не текущее значение, а усредненное за предыдущую минуту (moving avg). Либо какой-то перцентиль за предыдущую минуту
Де-факто этот способ часто взаимозаменяем с предыдущим
---
Зачастую эмпирический тюнинг алерта приводит к использованию комбинации этих способов
Представьте, что у вас есть алерт на кол-во ошибок — если 5хх ответов больше 1%, то алерт "загорается"
Обычно это выглядит так:
1. Берется окошко, скажем 3 мин
2. Считается доля 5хх: sum(requests[status = 5xx]) / sum(requests)
3. Алармим, если доля > 0.01
Так мы по сути считаем усредненную долю ошибок за последние 3 минуты
---
Что может поломать такую схему? Короткие всплески, когда процент 5хх пару секунд очень высокий. Это может происходить по разным причинам: случайные/локальные сбои, переключения мастера, накатка миграции etc
Поскольку мы считаем avg по окошку, такие короткие всплески скорее всего будут триггерить алерт, пока наблюдаемое окошко "не перескачет" этот всплеск
Пример: 100 rps; окошко 3 минуты; пятисотили 2 секунды
доля 5хх = (100rps * 2 сек) / (100rps * 180сек) = 1/90 > 0.01 — алерт загорелся
Хотите ли вы, чтобы алерт загорался в такой ситуации? depends
Далее рассмотрим случай, когда не хотим
---
Из банальных советов:
1. Увеличить окошко (3min -> 5min)
Шире окно, выше шанс, что пик размажется. Но тут идет трейдофф с риском появлений false-negative, когда пропустим реальную проблему
2. Увеличить трешхолд (0.01 -> 0.03)
Примерно та же история, что с окошком
Из менее банальных:
3. Пересмотреть алерт (avg -> percentile)
Например, так
1) Берем такую метрику requests[status = 5xx] / requests — это график доли пятисоток
2) Берем p75 от нее — получаем число. Это число означает, что 25% времени окошка доля пятисоток была выше, чем это число
3) Поджигаем если p75 > threshold
То есть мы ушли от "среднее кол-во пятисоток в окне нарушает трешхолд" до ">25% времени нарушался трешхолд пятисоток"
Частные случаи:
p0 (минимум) — трешхолд нарушался 100% времени (в каждой точке окна)
p50 (медиана) — трешхолд нарушался половину времени
p100 (максимум) — трешхолд нарушился буквально в одной точке
4. Добавить сглаживание
Вместо того, чтобы смотреть на исходную метрику requests[status = 5xx] / sum(requests), можно добавить сглаживание: например, каждая точка будет показывать не текущее значение, а усредненное за предыдущую минуту (moving avg). Либо какой-то перцентиль за предыдущую минуту
Де-факто этот способ часто взаимозаменяем с предыдущим
---
Зачастую эмпирический тюнинг алерта приводит к использованию комбинации этих способов
👍54🔥8💅1 1
⚡️По какой сущности шардировать
Недавно у коллег возникла необходимость шардировать большую pg базу, и для этого нужно определиться с сущностью, по которой надо шардировать. И задумался, можно ли придумать базовый универсальный алгоритм, как отправную точку, от которой можно рассуждать
Для простоты представим, что модель данных системы представлена в виде одного дерева
Исходное состояние: всё лежит в одном шарде, никаких кроссшардовых операций. Все прекрасно, кроме момента, что объем данных бесконечно растет
---
И далее начинаем спускаться по дереву сущностей, начиная от корня, примеряя каждую сущность как кандидата на шардирование
Корень — корневые сущности максимально независимы => будет минимальное количество кроссшардовых операций. При этом общий объем сущности (корневая сущность + все ее дочерние) скорее всего может бесконечно расти
Берем сущность на уровень ниже — получаем сущность, экземпляры который более зависимы друг от друга, при этом вероятность разрастания до бесконечности становится ниже
И с каждым понижением уровня мы получаем все более зависимые сущности, но которые все сильнее ограничены в объеме
И задача заключается в том, чтобы поймать баланс, когда сущности настолько независимы, чтобы не требовали (или почти не требовали) кросс-шардовых операций, при этом были достаточно ограничены в объеме
---
Один из успешных примеров с работы:
queue — определенная очередь обработки обращений в поддержку
ticket — конкретное обращение
article — сообщение в рамках обращения
queue — две очереди максимально независимы друг от друга, но могут расти бесконечно и неравномерно — тикетов может появится сколько угодно, и где-то их много, где-то мало
ticket — два тикета все еще достаточно независимы (это два отдельных обращения в поддержку), при этом два тикета могут участвовать в какой-то общей выборке (например, выборка открытых тикетов, на которых нет исполнителя). А вот максимальный объем тикета уже становится ограничен
article — два сообщения уже сильно зависимы (часто выбираются вместе), а объем сильно ограничен, и он гарантированно небольшой
Overall, что мы видим:
queue vs ticket — чуть чуть теряем в независимости, при этом сильно выигрываем, что сущность становится ограничена в объеме
ticket vs article — сильно проигрываем в независимости, при этом чуть чуть выигрываем в максимальном размере сущности
Такими рассуждениями, ticket выглядит максимально привлекательным кандидатом для шардирования
Недавно у коллег возникла необходимость шардировать большую pg базу, и для этого нужно определиться с сущностью, по которой надо шардировать. И задумался, можно ли придумать базовый универсальный алгоритм, как отправную точку, от которой можно рассуждать
Для простоты представим, что модель данных системы представлена в виде одного дерева
Исходное состояние: всё лежит в одном шарде, никаких кроссшардовых операций. Все прекрасно, кроме момента, что объем данных бесконечно растет
---
И далее начинаем спускаться по дереву сущностей, начиная от корня, примеряя каждую сущность как кандидата на шардирование
Корень — корневые сущности максимально независимы => будет минимальное количество кроссшардовых операций. При этом общий объем сущности (корневая сущность + все ее дочерние) скорее всего может бесконечно расти
Берем сущность на уровень ниже — получаем сущность, экземпляры который более зависимы друг от друга, при этом вероятность разрастания до бесконечности становится ниже
И с каждым понижением уровня мы получаем все более зависимые сущности, но которые все сильнее ограничены в объеме
И задача заключается в том, чтобы поймать баланс, когда сущности настолько независимы, чтобы не требовали (или почти не требовали) кросс-шардовых операций, при этом были достаточно ограничены в объеме
---
Один из успешных примеров с работы:
queue — определенная очередь обработки обращений в поддержку
ticket — конкретное обращение
article — сообщение в рамках обращения
queue — две очереди максимально независимы друг от друга, но могут расти бесконечно и неравномерно — тикетов может появится сколько угодно, и где-то их много, где-то мало
ticket — два тикета все еще достаточно независимы (это два отдельных обращения в поддержку), при этом два тикета могут участвовать в какой-то общей выборке (например, выборка открытых тикетов, на которых нет исполнителя). А вот максимальный объем тикета уже становится ограничен
article — два сообщения уже сильно зависимы (часто выбираются вместе), а объем сильно ограничен, и он гарантированно небольшой
Overall, что мы видим:
queue vs ticket — чуть чуть теряем в независимости, при этом сильно выигрываем, что сущность становится ограничена в объеме
ticket vs article — сильно проигрываем в независимости, при этом чуть чуть выигрываем в максимальном размере сущности
Такими рассуждениями, ticket выглядит максимально привлекательным кандидатом для шардирования
👍37🤔9💅2🔥1 1
⚡️Что под капотом у Cursor?
Хорошая статья, рассказывающая базовые принципы работы AI идеешек (и в целом агентных систем)
---
tl;dr:
Базовый минимум для агента, который будет уметь писать код:
1. Хорошая моделька (claude 3.7, gpt 4.1, ...)
2. Функции для работы с файлами (read_file, write_file) — чтобы моделька могла взаимодействовать с внешним (относительно нее) миром, т.е. нашей кодовой базой
3.1. Эмбеддинги кода в векторной базе — чтобы был семантический поиск по коду (найди места, где происходит вызов апи сервиса X).
3.2. Альтернатива: научить агента итеративно искать обычным поиском по коду — примерно как действуют люди, погружаясь в новую кодовую базу (так сделано в Claude Code)
4. Качественные промты — например, внутри Cursor используется такой промт
И на картинке показано, как эти компоненты друг с другом взаимодействуют
---
И несколько полезных моментов про написание правил для код-агентов (rules):
1. Не нужно писать что-то в стиле "ты опытный бэкенд разработчик на Java" — это будет странно для агента, тк у него уже есть built-in промт, рассказывающий, кто он такой
2. Правила лучше писать в формате энциклопедии (а не в формате четкого алгоритма) и с ссылками на код. Это позволяет агенту проще находить контекст, который нужен для исполнения пользовательского запроса, и переиспользовать одно и то же правило в разных ситуациях
3. Стоит вложиться в качественные описания правил. По описанию моделька лучше сматчит, применимо ли конкретное правило в конкретной ситуации
---
Следующий наворот — это подключение различных MCP-шек, которые позволяют агенту ходить во внешние системы: таск-трекер, внутреннюю вики или просто что-то гуглить. Ставьте классы, если был бы интересен пост про такое
Хорошая статья, рассказывающая базовые принципы работы AI идеешек (и в целом агентных систем)
---
tl;dr:
Базовый минимум для агента, который будет уметь писать код:
1. Хорошая моделька (claude 3.7, gpt 4.1, ...)
2. Функции для работы с файлами (read_file, write_file) — чтобы моделька могла взаимодействовать с внешним (относительно нее) миром, т.е. нашей кодовой базой
3.1. Эмбеддинги кода в векторной базе — чтобы был семантический поиск по коду (найди места, где происходит вызов апи сервиса X).
3.2. Альтернатива: научить агента итеративно искать обычным поиском по коду — примерно как действуют люди, погружаясь в новую кодовую базу (так сделано в Claude Code)
4. Качественные промты — например, внутри Cursor используется такой промт
И на картинке показано, как эти компоненты друг с другом взаимодействуют
---
И несколько полезных моментов про написание правил для код-агентов (rules):
1. Не нужно писать что-то в стиле "ты опытный бэкенд разработчик на Java" — это будет странно для агента, тк у него уже есть built-in промт, рассказывающий, кто он такой
2. Правила лучше писать в формате энциклопедии (а не в формате четкого алгоритма) и с ссылками на код. Это позволяет агенту проще находить контекст, который нужен для исполнения пользовательского запроса, и переиспользовать одно и то же правило в разных ситуациях
3. Стоит вложиться в качественные описания правил. По описанию моделька лучше сматчит, применимо ли конкретное правило в конкретной ситуации
---
Следующий наворот — это подключение различных MCP-шек, которые позволяют агенту ходить во внешние системы: таск-трекер, внутреннюю вики или просто что-то гуглить. Ставьте классы, если был бы интересен пост про такое
👍174😁9🔥5✍4💅2