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

Сотрудничество: t.me/qsqnk
Download Telegram
Geohash

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

Другим представлением координат является geohash:

1. Плоскость земли делится на 32 участка
2. Каждому участку приписывается определенная буква/цифра из Base32

Мы научились одним символом делить пространство на 32 участка. Чтобы получить бОльшую точность, выбираем один из участков и делаем ту же процедуру

1. Плоскость участка делится на 32 под-участка
2. Каждому под-участку приписывается определенная буква/цифра из Base32

И так далее

---

Точность получается примерно следующая:

1 символ ~ 1000 км2
2 символа ~ 1000 км2 / 32
...
n+1 символов ~ 1000 км2 / 32^n

При n=6 точность будет будет около 1м2

---

Основные плюсы такого представления:

1. Не пара чисел, а одна строка - легче индексировать
2. Если пара точек имеет одинаковый префикс geohash-а, то они находятся в одном участке. Это позволяет очень быстро выполнять запросы на поиск ближайших точек с нужной точностью
3. Можно выбрать любую требуемую для конкретной задачи точность и экономить место на хранение, если скажем точности в 1м2 будет достаточно

---

Идея "Hierarchical Spatial Index", когда пространство делится на большие участки, большие участки делятся на участки поменьше и тд, весьма популярна и используется многими компаниями. Про подход Uber, где они делят пространство на шестиугольники можно почитать здесь
👍88🔥25🤯4💅31
Quad-trees в гео-приложениях

Пост про geohash хорошо зашел, поэтому сегодня поговорим про еще один подход к индексированию гео-данных - Quad-trees

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

Пока очень похоже на geohash: если бы мы делили пространство на 32 ячейки, и если все квадранты имели бы одинаковый размер, то он бы и получился, просто в формате дерева

--

Основная разница в том, что деревья квадрантов динамические:

1. Выбирается bucket size - максимальное число точек, которое может находится в определенном квадранте
2. При вставке новой точки в дерево проходимся по нему, находим нужный квадрант. Если там уже находится bucket size точек, то этот квадрант делится еще на 4, и точки распределяются по ним

--

Такая специфика может быть полезна в некоторых приложениях, например, поиск такси:

1. Позиции машин индексируются и складываются в quad tree (периодически обновляясь), допустим с bucket size = 5 - это значит, что в одном квадранте находится не более 5 машин
2. Когда человек вызывает такси, мы определяем, в каком квадранте он находится, и ищем машины именно в нем

Когда человек находится в какой-то глуши, искать энивей будем в одном квадранте, но по большой площади из-за низкой плотности машин. В то же время, когда человек вызывает такси в городе, искать будем уже по меньшей площади, потому что плотность машин сильно больше
👍40🔥92💅21
Про боттлнеки

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

В контексте микросервисов самый банальный пример - есть N сервисов, есть отдельный сервис аутентификации, куда все ходят, общая нагрузка на систему 1k rps, сервис аутентификации держит лишь 100 rps - это и есть боттлнек, тк он будет тормозить работу всей системы

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

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

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

Локализовать проблему могут помочь грамотные мониторинги и профилирование. Ставьте 👍, если нужен пост на эту тему
👍206💅7🔥321
Визуализация архитектуры (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