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

Сотрудничество: t.me/qsqnk
Download Telegram
Визуализация архитектуры (C4 + PlantUML)

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

C4 — это модель представления архитектуры в виде четырех слоев:

1. Диаграмма контекста: взаимодействие системы с пользователем, с другими системами

2. Диаграмма контейнеров: как в рамках системы взаимодействуют контейнеры (единицы, которые независимо деплоятся). Например, взаимодействие микросов

3. Диаграмма компонентов: как в рамках контейнера взаимодействуют его отдельные части. Например, модули в рамках микроса

4. Диаграмма кода: как в рамках компонента написан код. Обычно это просто диаграмма классов

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

При этом существует PUML (PlantUML) — инструмент описания UML с помощью текста. Конкретно для C4 существует "плагин", который позволяет удобно описывать диаграмму в терминах C4

Базовый пример:
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml

Person(user, "User")
System(system, "Marketplace")

Rel(user, system, "Make order")
@enduml


Попробовать отрисовать его можно тут
👍31🔥8🤔3💅2🙏11
Архитектура и оргструктура

Закон Конвея: «организации проектируют системы, которые копируют структуру коммуникаций в этой организации»

Если возникает несоответствие, то на практике это приводит

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

---

Предположим есть две небольшие команды: первая занимается сервисом X, вторая — сервисом Y. Команды ведут бэклог и приоритизируют задачи независимо друг от друга. И последнее время стало заметно, что time to market сильно вырос — почти каждая фича требует доработок в обоих сервисах и требует координации/синхронизации между командами

Пишите в комментах, какие способы уменьшения TTM здесь видите
👍18🔥4💅41
С новым годом!

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

И топаем дальше!
👍46🔥21💅74🙏2
Поиск по иерархичным данным в elasticsearch (и не только)

Представьте, что у вас есть две сущности, которые связаны внешними ключами

Например, в нашем случае такими сущностями выступают
Ticket — обращение пользователя в поддержку: содержит кучу разной метаинфы в виде тегов, кастомных полей
Article — сообщение в рамках одного тикета

Одним из кейсов для поиска является запрос "полнотекстово поискать по тексту артиклов среди тикетов, у которых такие-то метаданные". То есть в поиске участвует как родительская сущность (ticket), так и дочерняя (article)

Если это делать на реляционной БД, то все достаточно просто: накидываем нужные индексы на обе таблицы, и далее в запросах джойним и фильтруем по обоим таблицам. Особо тут ничего не оптимизируешь

Однако если для поиска выделяется отдельная БД (elastic, sphinx, etc.), то появляется проблема — надо как-то перекидывать данные из основной бд в поиск для индексации, и как-то располагать эти данные в индексе

---

Есть два ключевых трейдоффа:

1. Раздельное хранение

В поисковом индексе parent и child сущности хранятся независимо друг от друга

Плюс: быстрая индексация

Если меняется родительская сущность, не надо переиндексировать дочерние

Минус: медленный поиск

Нужно как-то джойнить документы: либо parent-child запросы в эластик, либо application-side джойны. Это довольно слабо масштабируется, и может оочень сильно тормозить поисковые запросы

2. Денормализация

В каждую дочернюю сущность кладем также всю инфу про родительскую сущность

Плюс: быстрый поиск

Вся нужная инфа хранится в одном документе, соотв-но не нужно думать про иерархию, теперь иерархия никак не аффектит скорость поисковых запросов

Минус: долгая индексация, больше данных

Поменялась родительская сущность => нужно переиндексировать все дочерние. Также из-за денормализации в индексе хранится больше данных, тк для каждой дочерней сущности мы дублируем всю инфу про родительскую

---

Поскольку первый вариант не очень масштабируется, чаще всего на больших объемах приходится жить с денормализацией. Пишите в комменты, какой подход используете вы в своих проектах с поиском
👍27💅33🔥2
Где может помочь circuit breaker

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

Здесь может помочь circuit breaker (CB)

Запрос из сервиса X в сервис Y обвязывается в CB, который работает так:

У CB есть три состояния:

1. Closed — с внешним сервисом все ок, запросы идут как обычно
2. Open — внешний сервис не доступен, не делаем запросы, сразу возвращаем ошибку
3. Half open — "flapping" состояние. Пока не знаем доступен ли сервис, проверяем его

И переходы между состояниями:

Изначально в Closed

Closed -> Open: внешний сервис стал недоступен. Например, по кол-ву ошибок либо по времени ответа
Open -> Half open: прошло некоторое время, пока висим в Open. Начинаем по-тихоньку проверять внешний сервис
Half open -> Open: проверили, внешний сервис еще мертв
Half open -> Closed: проверили, внешний сервис жив

---

Почему бы просто не использовать паттерн timeout, где запрос отваливается, если не отработал за некоторый таймаут? Можно. При этом используя CB мы также оказываем услугу внешнему сервису — мы не начинаем в него неистово ретраить, если знаем, что он скорее всего недоступен. Поэтому если у вас микросервисная архитектура, и все межсервисные походы обвязаны в CB, то это может помочь предотвратить каскадные падения
👍52🔥7
Верхнеуровный принцип работы полнотекстового поиска

Представим у нас магазин товаров, товар описывается тремя полями
{
"id": 17
"name": "big black box",
"type": "box_type"
}


Мы хотим уметь полнотекстово поискать по названию, а также пофильтровать товары по типу

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

1. Берем поле name
2. Бьем исходное имя на токены: ["big", "black", "box"]
3. Добавляем инфу, что токены "big", "black" и "box" встречаются в документе 17

Обратный будет представлять собой мапку token -> [document_id] и для поля name будет выглядить так:
{
"big": [17],
"black": [17],
"box": [17]
}


Для остальных индексируемых полей строим аналогично

---

Загрузив несколько документов, получится примерно такое:

Для поля name:
{
"big": [17, 23],
"black": [15, 17, 29],
"box": [17, 18, 22],
"ball": [13, 21],
"small: [13, 29]",
"white: [1, 5]"
}


Для поля type:
{
"box_type": [17, 18, 22],
"ball_type": [13, 21]
}


---

И наконец, чтобы сделать поиск name ~ "small" and type = ball_type, нужно

1. Посмотреть в обратный индекс name на токен small: получим документы [13, 29]
2. Посмотреть в обратный индекс type на токен ball_type: получим документы [13, 21]
3. Пересечь выборки: получим документы [13]

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

Покажите реакцией 👍, если нужно еще постов на тему поиска
👍249🔥10💅22
⚡️Как чему-то учиться

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

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

Почему это работает:

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

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

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

---

И можно посмотреть на эту схему на синтетическом примере. Допустим мне очень хочется научиться писать task-scheduler-ы на БДшке

1. Копирую чье-то решение, верхнеуровнего смотрю, какие сущности там мелькают

2. Предлагаю свое наивное решение: "а давайте просто селектить все зашедуленные задачи и в форике выполнять". И мне начинают накидывать: "а что если несколько инстансов возьмут одинаковые задачи?", "а что если скопилось млн задач?", "а насколько это масштабируется?"

3. Начинаю смотреть работающее решение. Вижу "select for update" — открываю доку, вижу что эта штука берет блокировки. Иду читать про блокировки, зачем они нужны. Окей, тут вроде разобрались: это нужно, чтобы только один инстанс брал задачу. Потом осознаю что запросы выполняются быстро, даже если задач скопилось очень много — мне подсказывают про индексы. Иду читать про индексы до тех пор, пока не пойму, почему они помогают ускорить. И так далее

---

Итого после этих трех пунктов у вас в голове останется: пример хорошего решения (и почему оно хорошее), пример плохого решения (и почему оно плохое) и теория, которая за всем этим стоит. На основе этого можно осознанно придумать что-то свое и новое
👍85🔥178💅21
Мысли про сеньорность

Как можно охарактеризовать человека с лычкой сеньора? Годами опыта? Кругозором в технологиях? Сложностью решаемых задач?

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

1. Человека сразу наняли на сеньорский грейд

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

2. Человек вырос внутри компании с мидла

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

И при переходе в другую компанию человек может как сохранить эту лычку, если там ожидания от сеньоров +- такие же, либо не сохранить

А вы что думаете?
👍803
⚡️Как искать боттлнеки

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

1. Мониторинги времени обработки запроса/сообщения из очереди

Графики по перцентилю response time апишек/по обработке сообщений в условной кафке. Позволяют локализовать проблему от "все плохо" до "все плохо в конкретном сервисе/бд/очереди"

2. Мониторинги потребления ресурсов

Графики по CPU/RAM/Disk/Network. Позволяют локализовать проблему от "все плохо в конкретном сервисе" до "все плохо с CPU в конкретном сервисе"

3. Профилирование

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

---

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

Ставьте 👍 на этот пост, если интересен рассказ про то, как оптимизировать найденные узкие места
👍104🔥1
⚡️Tail latency и hedged запросы

Некоторым системам важно иметь почти реалтаймовые ответы (~100мс). Например, саджесты в поиске: когда вы набираете очередной символ, то ожидаете очень быстро получить новый список вариантов

При этом зачастую происходит так:
- Среднее время запроса — 100мс
- Время запроса в p99 — 1 секунда

Эти задержки на больших перцентилях (p95, p99, p99.9) называются tail latency. Они могут происходить абсолютно по разным причинам от конкуренции за ресурсы на определенной машинке до сетевых проблем

---

Один из способов борьбы с такими задержками — техника hedged requests

Пусть у нашей системы p90 = 100ms, p99 = 200ms

На больших данных, считаем что
- Вероятность ответа >100ms = 0.1
- Вероятность ответа >200ms = 0.01

1. Посылаем первый запрос
2. Если спустя 100мс не получили ответ, посылаем второй запрос


0ms 100ms 200ms
rq1 |-------------|-------------->|
rq2 | |-------------->|


Итого получаем (считая что запросы независимые), вероятность, что такая совокупность не уложится в 200ms = (вероятность, что первый не уложится в 200ms) * (вероятность, что второй не уложится в 100ms) = 0.01 * 0.1 = 0.001

Таким образом, вероятность ответить дольше 200ms стала 0.001, а значит p99.9 = 200ms

А вероятность, что придется отправлять второй запрос = вероятность, что первый отвечал >100ms = 0.1. То есть в 10% случаев придется слать два запроса

---

Таким образом, увеличив нагрузку на систему на 10% мы смогли перейти от p99 = 200ms до p99.9 = 200ms, то есть уменьшили "вероятность таймаутнуться" в 10 раз
🔥76👍16🤔113🤯3💅11
⚡️Как жить с нестабильной интеграцией

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

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

1. Если ваш сервис только забирает данные из внешней системы

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

- Либо раз в какое-то время подгружать полный слепок данных и сохранять к себе
- Либо кешировать результаты отдельных запросов с каким-то TTL
- Либо обновлять кеш по эвентам от внешней системы (если она их отправляет)

2. Если ваш сервис только отправляет данные во внешнюю систему

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

3. Если ваш сервис отправляет данные во внешнюю систему и ожидает чего-то в ответ

Пример: создание сущности во внешней системе и получение id в ответе

Пишите в комментах, какие варианты повышения стабильности здесь видите
👍28🔥9💅32
⚡️Загадка про постгрес

1. Есть табличка с колонками a, b

create table tbl
(
a bigint,
b timestamp
);


2. Есть 10 млн записей, примерно у половины записей в колонке a лежит null

3. Есть btree индекс на (a, b)

Угадайте план запроса
select *
from tbl
where a is null
order by b
limit 1;


Верно, секскан + сортировка

---

Как выяснилось (после инцидента), постгрес не умеет в фильтрацию по null/is null + сортировку одновременно. Если только фильтрация — будет ок. Поэтому для такого кейса нужно создавать отдельный частичный индекс on tbl (b) where a is null;

proof: https://dbfiddle.uk/Bgob-orb
🤯52👍34🤔7💅33
⚡️Какие-то мысли про Redis Streams

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

Как оно работает (см картинку):

1. Сообщение добавляется в стрим через XADD
2. Читаем сообщение из стрима и "назначаем" на определенного консюмера из определенной группы консюмеров через XGROUPREAD
3. После того как сообщение прочитано оно попадает в PEL (Pending Entries List) — список прочитанных, но не обработанных сообщений консюмера
4. Консюмер делает XACK на сообщение, оно помечается обработанным, т.е. удаляется из PEL

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

Общее правило такое:

- 1 stream, 1 consumer: соблюдается порядок
- 1 stream, n consumers: не соблюдается порядок, конкурентная обработка
- n stream, n consumers: "масштабированный" первый вариант. Для каждого из n стримов есть консюмер, который в одиночку из него вычитывает. Т.е. некоторые изобретение партицированного топика на коленке

Обработка ошибок:

После чтение консюмер может по разным причинам умереть и недообработать сообщение. Эта проблема решается поллингом PEL с помощью XPENDING и переназначением зависших сообщений на другого консюмера через XCLAIM. Что мне показалось странным с точки зрения дизайна — нельзя просто вернуть сообщение в "общую очередь", нужно переназначить его именно на конкретного консюмера

---

Overall кажется, что redis streams хорошо подходят для ситуаций, где нужна
- небольшая и быстрая in memory очередь
- сообщения редко могут теряться (специфика редиса, см пост)
- порядок обработки либо не важен, либо вы готовы заморочиться и вручную реализовать партицирование
👍223💅11
⚡️Про что подумать, когда пишете свой outbox

Вместо того чтобы делать

dbUpdate()
sendToBroker()


Мы сохраняем сообщение в бд в рамках транзакции

tx {
dbUpdate()
dbSaveMessage()
}


И фоновый воркер допушит это сообщение до брокера

---

Что мы обычно хотим от аутбокса:
- Низкое latency проброса от бд до брокера
- Сохранение порядка сообщений для бизнес-сущностей
- Возможность масштабировать пропускную способность

Оговорка: можно взять готовый CDC типа Debezium, который читает лог базы. Однако надо помнить, что это дополнительный компонент в вашей инфре, который надо поддерживать + дополнительная точка отказа + нужна доп экспертиза "а че там может сломаться". Иногда это ок, иногда не ок

---

Далее посмотрим, как можно рассуждать на пути к целевому решению, если вы делаете аутбокс сами

Идея 1: несколько воркеров поллят записи через select for update skip locked

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

Идея 2: однотредово забираем пачку записей с order by

Поскольку у предыдущего решения нет упорядоченности, давайте ее добавим. Теперь нарушение порядка невозможно, потому что записи достает только один тред. Масштабируемость и низкое latency — спорно. При рейте в 10к+ сообщений в секунду один тредик может перестать справляться

Идея 3: попробуем распараллелить предыдущий вариант

Давайте при сохранении сообщения в базу сразу указывать "виртуальную партицию" так, чтобы два эвента по одной бизнес-сущности были в одной партиции (entity_id % partition_number). Так мы сможем запустить предыдущую однотредовую логику для каждой партиции, чем добьемся масштабируемости

Но: представим, что мы захотим увеличить число виртуальных партиций с 5 до 7. Сначала сущность с id = 123 попадала в партицию 123 % 5 = 3, после увеличения будет попадать в 123 % 7 = 4. То есть в момент масштабирования сообщения по одной бизнес-сущности могут начать обрабатываться параллельно в партициях 3 и 4, из-за чего нарушится порядок

Идея 4: добавляем маппинг entity_id <-> partition_id

Берем идею 3 и добавляем явный маппинг между id бизнес сущности и id партиции. Это позволит бизнес-сущность "прилеплять" к определенной партиции. И в момент масштабирования числа партиций у нас ничего не поломается, потому что у нас было зафиксировано, что сообщения по entity_id = 123 попадают в партицию 3

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

В итоге такое решение удовлетворяет всем требованиям
- Низкое latency
- Сохранение порядка
- Возможность безопасно масштабироваться

---

Вывод: написать хороший аутбокс — задача явно не простая. При этом такие изощрения точно нужны не всегда. Зачастую либо нагрузка низкая (<100 сообщений в секунду), либо порядок не требуется. Эти вводные могут существенно упростить итоговое решение
👍46🔥14💅22
⚡️Ускорение индексации денормализованных документов

В посте про подходы к поиску иерархичных данных был небольшой рассказ про денормализацию — это когда есть родительский документ { ticket } и есть дочерние { article_1 }, { article_2 }, { article_3 }. И далее мы уплощаем эту структуру в три документа, где child-ы обогащены данными от родителя

Псевдокод:
{ ticket, article_1 },
{ ticket, article_2 },
{ ticket, article_3 }


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

Какие есть способы ускорить такую индексацию в elasticsearch:

0. Наивный неэффективный способ

Получили эвент обновления ticket => запрашиваем все артиклы => строим n документов для обновления артиклов => делаем n запросов в эластик

Если у тикета 500 артиклов, то к примеру при изменении статуса тикета, нам нужно будет сделать 500 запросов на переиндексацию — звучит крайне неэффективно

Псевдокод:
query 1: { ticket: { all fields }, article { all fields }
query 2: { ticket: { all fields }, article { all fields }
query 3: { ticket: { all fields }, article { all fields }
...


1. Использование bulk update + partial update

Нам приходится делать кучу запросов, мы это можем решить с помощью использования bulk update, передав в одном запросе список того, что хотим переиндексировать

Вторая проблема — что приходится передавать полный документ, хотя у него изменился маленький кусочек. Эластик поддерживает partial update, и поэтому можно передать только тот кусок документа, который изменился

Псевдокод:
query: [
{ article_id: 1, ticket: { changed fields } }
{ article_id: 2, ticket: { changed fields } }
{ article_id: 3, ticket: { changed fields } }
...
]


Важное НО: эластик делает shallow partial update, т.е. вложенные объекты обновляются целиком сразу, а не частично. К примеру, если бы мы захотели обновить вложенное поле ticket.custom_fields.some_field, то нам мы пришлось полностью передать объект ticket. В противном случае мы бы потеряли данные

2. Использование update_by_query + noscript

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

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

Псевдокод:
query: {
condition: ticket_id = 123,
noscript: ticket.custom_fields.some_field = "abc"
}


Такой подход идеально подходит для переиндексации дочерних документов, и позволяет делать максимально компактные запросы и обеспечивать гибкость с помощью кастомных скриптов (по дефолту на внутреннем языке эластика — painless)
👍17
⚡️Expand and contract (zero downtime migrations)

Когда вам нужно мигрировать приложение на новую схему данных, у вас есть два пути

Первый вариант — за один релиз задеплоить новый код и миграцию схемы и самих данных. Если у вас данных не очень много и/или вы можете позволить себе даунтайм, это прекрасно

Второй вариант — страдания

Для таких миграций без даунтайма есть весьма понятный подход, называемый expand and contract

Посмотрим на примере:
Есть — одна колонка в бд содержащая и имя, и фамилию пользака user = "Walter Black"
Хочется — две отдельные колонки name = "Walter", surname = "Black"

1. Добавляем новые нуллабельные колонки

Отдельный релиз — накатываем миграцию схемы данных

2. Начинаем писать и в старую, и в новую схему

Отдельный релиз — при апдейтах/инсертах user = "Walter Black" также проставляем name = "Walter", surname = "Black"

3. Мигрируем старые данные

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

4. Начинаем читать из новой схемы

Отдельный релиз — в коде приложения начинаем читать данные не из старой колонки, а из новых

5. Выпиливаем запись в старую схему

Отдельный релиз — перестаем писать в старую колонку

6. Выпиливаем старую схему

Отдельный релиз — миграция, которая дропает старую колонку user

---

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

Шаги в виде картинок в комментах
🔥41👍27💅21
⚡️Cache stampede

Представим, код метода getData() выглядит так

def getData():
dataFromCache = getFromCache()
if dataFromCache:
return dataFromCache
dataFromDb = getFromDb()
updateCache(dataFromDb)
return dataFromDb


Что произойдет если два потока почти одновременно не найдут данные в кеше? Пойдут загружать из БД

А если 100 потоков?

Понять масштабы можно на таком примере:
- В кеш может поступать до 300 запросов в секунду
- Время загрузки из бд — 2 секунды

И получается, если ключ в кеше протухнет, то за первые две секунды в базу попрутся 600 параллельных тяжелых запросов. Учитывая, что из-за этих запросов БД может деградировать, запросы могут начать выполняться еще дольше

---

Что с этим делать?

1. Локи

В примере выше хорошо бы зашел double-checked locking

def getData():
dataFromCache = getFromCache()
if dataFromCache:
return dataFromCache
withLock {
dataFromCache = getFromCache()
if dataFromCache:
return dataFromCache
dataFromDb = getFromDb()
updateCache(dataFromDb)
return dataFromDb
}


Можно конечно сразу лочить, но тогда для подавляющего большинства запросов добавится лишний оверхед. А в случае дабл чека будет лочиться только когда кеш протух

2. Прогрев кеша / фоновые апдейты

Если уверены, что значение скорее всего понадобится, то просто заранее его туда положите и в фоне обновляйте. Если будет всплеск нагрузки, в кеше уже будет все что нужно

3. Вероятностные рефреши

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

---

Пишите в комментах, что из этого используете)
👍43🔥71🤔1
System Design & Highload (Alexey Rybak)

Если вы ищете практический канал про Highload, БД, System Design, то вам точно стоит заглянуть в канал к Алексею — ex-CTO Badoo, а сейчас основателю R&D-платформы devhands.io и сооснователю софта для автоматизации перфоманс ревью teamwork360.io

Он делится на своём канале уникальными мыслями и опытом работы с реальными высоконагруженными системами, сравнивает перфоманс БД, описывает некоторые теоретические аспекты

Список постов которые лично мне понравились у него:

- Простое объяснение CAP теоремы
- Про блокировки в СУБД
- Почему важно замерять P99
- Один день из жизни CTO
- 1.000.000 RPS на PostgreSQL и MySQL
- Нужен ли BFF?

Ставьте 👍, если подписались
👍31🤔3
⚡️Почему из-за долгих транзакций могут тормозить другие запросы

Представьте, что у вас есть табличка с колонкой created_ts. И на эту таблицу есть некоторый TTL — фоновый воркер в порядке created_ts удаляет записи, которые старше 7 дней

Посмотрим, как это заафектит производительность запроса

select * from tbl
order by created_ts
limit 1


1. Сначала все хорошо, запрос идет по индексу на created_ts, берет первую запись

2. Далее начинается долгая транзакция с участием tbl

3. Далее блокируется автовакуум tbl

4. Параллельно с этим работает фоновая удалялка. Но поскольку автовакуум заблокирован, удаленные записи просто продолжают висеть как dead tuples

5. Спустя какое-то время набирается несколько тысяч dead tuple-ов

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

И чтобы выполнить запрос

select * from tbl
order by created_ts
limit 1


Нам нужно будет сначала пройти все эти ссылки на мертвые кортежи, и только потом взять первую активную запись. Если таких кортежей наберется под миллион — будет очень больно (основано на реальных событиях)
👍50🔥13💅3🤔2
⚡️Обработка ошибок в консюмерах

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

Предлагаю такой фреймворк

Отвечаем на два вопроса:
- Критична ли потеря сообщений?
- Критичен ли порядок обработки сообщений?

Сценарий 1: Потеря сообщений НЕ критична, порядок НЕ критичен

Потери не критичны => можем вообще не ретраить

Порядок не критичен => можем обрабатывать сообщения параллельно, можем ретраить в любом удобном формате

В общем, самый простой случай — делайте что хотите)

Сценарий 2: Потеря сообщений НЕ критична, порядок критичен

Потеря сообщений не критична => можем вообще не ретраить

Порядок критичен => обрабатываем все сообщение либо одним батчем, либо последовательно в один поток. Можем ретраить только синхронно, ибо ретраи в фоне могут нарушить порядок

Тоже простой случай — обрабатываем сообщения последовательно (или батчем). Опиционально можем поретраить, но синхронно

Сценарий 3: Потеря сообщений критична, порядок НЕ критичен

Потеря сообщений критична => обязательно нужны ретраи

Порядок не критичен => можем обрабатывать сообщения параллельно, можем ретраить в любом удобном формате

Здесь на что у вас хватит фантазии. Варианты (можно комбинировать):
- Синхронные ретраи
- Retry-topics + DLQ — в случае ошибки в основном топике переписываем сообщение в retry-topic-1. Из него с некоторой задержкой пытаемся обработать, не получилось — пишем в retry-topic-2 и т.д. Если все ретраи не увенчались успехом — пишем в dead letter queue и поджигаем алерт
- Очередь на БД — тоже вполне удобная вещь. В случае ошибки пишем сообщение в очередь на БД и спустя некоторое время обрабатываем. Из плюсов — не надо плодить топики + легко настроить кастомные задержки

Сценарий 4: Потеря сообщений критична, порядок критичен

Потеря сообщений критична => обязательно нужны ретраи

Порядок критичен => обрабатываем все сообщение либо одним батчем, либо последовательно в один поток. Можем ретраить только синхронно, ибо ретраи в фоне могут нарушить порядок

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

Ставьте 👍, если было полезно
👍118🤔3
⚡️Шардирование без решардирования

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

Представьте есть 3 шарда => shard_count = 3

Шард выбирается как entity_id % shard_count

1. Для entity_id = 7 получаем шард 7 % 3 = 1
2. Добавляем новый шард => shard_count = 4
3. Для entity_id = 7 получаем шард 7 % 4 = 3

Поэтому нам нужно будет перераспределить сущности, чтобы они соответствовали корректным шардам

Один из способов уменьшить количество "перераспределений" — consistent hashing. Но можно ли вообще обойтись без решардирования?

---

Функция выбора шарда определяется как

f: (shard_key, shard_count) -> shard_number

Которая по ключу шарда и текущему кол-ву шардов в системе отдает чиселку — номер шарда, где должна лежать сущность

Чтобы не было необходимости в решардинге, что мы хотим от этой функции? Чтобы при добавлении нового шарда значение не изменялось, то есть для любого shard_key и shard_count

f(shard_key, shard_count) = f(shard_key, shard_count + 1)

Что в сущности означает, что f(shard_key, shard_count) = f(shard_key, shard_count + 1) = f(shard_key, shard_count + 2) = ...

То есть как будто функция вообще никак не зависит от shard_count, что странно

---

Возможно ли такое, если функция чистая (т.е. не делает сайд-эффектов и использует только переданные аргументы shard_key и shard_count)?

Возьмем f(shard_key, 1), который всегда константно равен 0 (потому что shard_count = 1)

Но отсюда следует, что 0 = f(shard_key, 1) = f(shard_key, 2) = f(shard_key, 3) = ... То есть такое возможно, если функция всегда будет отдавать 0, что очевидно нам не подходит

---

Значит функция должна делать какие-то сайд-эффекты

И самый простой сайд-эффект, который позволяет этого достичь — это getOrPut в хранилище маппингов entity_id <=> shard_number

Пусть снова shard_count = 3

1. Пусть entity_id = 7 => shard_number = mappings.getOrPut(7, 7 % 3) = 1
2. Добавляем новый шард => shard_count = 4
3. Для entity_id = 7 получаем шард mappings.getOrPut(7, 7 % 4) = 1 (а не 3, потому что в mappings уже есть запись с таким ключом)

Таким образом это позволяет "прилипать" сущности к определенному шарду, что убирает необходимость решардинга при добавлении новых шардов

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

Следующая часть

Ставьте 👍, если нужен пост про виртуальные бакеты в шардировании
👍160🔥2