⚡Heartbeat pattern
Предположим у нас есть простая очередь задач
Чтобы брать задачи из этой очереди и гарантировать эксклюзивность, можно использовать стандартный подход с
Окей, попробуем не брать блокировку, сделаем что-то типа такого
Казалось бы все ок, но что если воркер, взявший задачу, умрет перед execute task? Таска навечно повиснет в статусе running и никто не будет с ней ничего делать
—
Ровно эту проблему решают хартбиты:
1. Воркер раз в n секунд пингует базу, что позволяет нам убедиться что воркер жив и имеет конекшн до базы
2. Супервизор наблюдает за воркерами: если какой-то воркер не пинговал базу m секунд, то снимаем с него все задачи
В итоге задача снимется с умершего воркера, и ее возьмет любой другой свободный
Предположим у нас есть простая очередь задач
create table task
(
id bigint not null,
status varchar not null,
data jsonb not null
);
Чтобы брать задачи из этой очереди и гарантировать эксклюзивность, можно использовать стандартный подход с
select for update. Однако, это вынуждает нас держать блокировку и транзакцию на все время исполнения задачи. В итоге получаем long-running transactions, которые негативно влияют на перфоманс базыОкей, попробуем не брать блокировку, сделаем что-то типа такого
val task = tx {
get scheduled task with lock;
set task status running;
}
execute task;
set task status finished;Казалось бы все ок, но что если воркер, взявший задачу, умрет перед execute task? Таска навечно повиснет в статусе running и никто не будет с ней ничего делать
—
Ровно эту проблему решают хартбиты:
1. Воркер раз в n секунд пингует базу, что позволяет нам убедиться что воркер жив и имеет конекшн до базы
2. Супервизор наблюдает за воркерами: если какой-то воркер не пинговал базу m секунд, то снимаем с него все задачи
В итоге задача снимется с умершего воркера, и ее возьмет любой другой свободный
👍63🔥6💅6✍1🤔1 1
⚡Потери данных в Redis
В случаях, когда на присутствие элемента в редисе завязана какая-то бизнес логика, бОльшая чем просто ускорение доступа к данным, хорошо бы задуматься о гарантиях, которые редис предоставляет
Можно выделить два сценария потери успешно (со стороны клиента) записанных данных
1. Записали данные в RAM, но не записали на диск
С выключенным redis persistence может случиться крайне неприятная ситуация: данные льются на мастер, записываются в оперативку, реплицируются на слейв узлы. Но в один момент происходит отказ мастера. После рестарта мастер-узла он окажется пустым, т.к. ему не из чего будет восстанавливать данные. Более того, такое может случиться даже в sentinel конфигурации — мастер может упасть и рестартнуться настолько быстро, что sentinel это не задетектит и не переключит мастер
2. Записали данные на мастер, но не успели реплицировать
Предположим, у нас включен redis persistence и данные каким-то образом записываются и на диск. Здесь мы решаем вышеописанную проблему — мастер после рестарта просто восстановит данные с диска. Однако, есть другая проблема — асинхронная репликация. Мы можем записать данные на мастер, клиент получит ок с надеждой, что эти данные асинхронно долетят до реплик. Но здесь случается отказ мастера, данные не успевают дойти до реплик, и происходит переключение мастера. В итоге реплики так и не узнают про эту запись
—
Итого — первую проблему можем решать с помощью включения redis persistence. Вторую проблему не можем решать, так как редис поддерживает только асинхронную репликацию
Ставьте 👍 на этот пост, если нужен рассказ про варианты использования redis persistence
В случаях, когда на присутствие элемента в редисе завязана какая-то бизнес логика, бОльшая чем просто ускорение доступа к данным, хорошо бы задуматься о гарантиях, которые редис предоставляет
Можно выделить два сценария потери успешно (со стороны клиента) записанных данных
1. Записали данные в RAM, но не записали на диск
С выключенным redis persistence может случиться крайне неприятная ситуация: данные льются на мастер, записываются в оперативку, реплицируются на слейв узлы. Но в один момент происходит отказ мастера. После рестарта мастер-узла он окажется пустым, т.к. ему не из чего будет восстанавливать данные. Более того, такое может случиться даже в sentinel конфигурации — мастер может упасть и рестартнуться настолько быстро, что sentinel это не задетектит и не переключит мастер
2. Записали данные на мастер, но не успели реплицировать
Предположим, у нас включен redis persistence и данные каким-то образом записываются и на диск. Здесь мы решаем вышеописанную проблему — мастер после рестарта просто восстановит данные с диска. Однако, есть другая проблема — асинхронная репликация. Мы можем записать данные на мастер, клиент получит ок с надеждой, что эти данные асинхронно долетят до реплик. Но здесь случается отказ мастера, данные не успевают дойти до реплик, и происходит переключение мастера. В итоге реплики так и не узнают про эту запись
—
Итого — первую проблему можем решать с помощью включения redis persistence. Вторую проблему не можем решать, так как редис поддерживает только асинхронную репликацию
Ставьте 👍 на этот пост, если нужен рассказ про варианты использования redis persistence
👍201🔥10
⚡Redis persistence
В продолжение к предыдущему посту. В редисе есть две политики отлива данных на диск
1. RDB File (Redis Database File) — компактный снапшот-файл, хранящий всю информацию про ключи и значения на определенный момент времени
Поскольку снапшоты хранят весь снимок базы, брать их часто не очень целесообразно. Поэтому если страшно потерять данные, скажем, за последний час — то для восстановления данных после рестарта этот режим не очень подходит. Однако, этот режим удобно использовать для бэкапов: например, брать снапшот каждый час и отливать в S3
+: Компактный
+: Быстрое восстановление данных из снапшота
-: Не получится делать снимки часто, соотв-но режим не подходит, если хочется минимизировать риски потери данных
2. AOF (Append Only File) — лог write-операций. При рестарте редис просто проходится по логу и выполняет записанные операции
Когда мы делаем write-операцию, редис для быстроты делает запись только в оперативную память. Возникает вопрос — когда данные отливать на диск? Здесь есть несколько режимов:
2.1. appendfsync always: при write-операциях синхронно записываем данные и на диск. Риски потери данных минимальны, но и просадка в производительности на порядок (можно погуглить бенчмарки). Если вам нужен этот режим, вам точно нужен редис?
2.2. appendfsync no: запись на диск происходит, когда заполняется buffer memory. Периодичность отлива данных недетерменирована, поэтому не можем давать никаких гарантий
2.3. appendfsync everysec: бекграунд тредик каждую секунду сбрасывает данные на диск. Таким образом, операции записи по прежнему будут быстрыми (пишем только в ram), но будут возможны потери данных за последнюю секунду
+: Подходит, если нужно минимизировать риски потери данных
-: Менее компактный нежели RDB
-: Дольше восстановление данных нежели из RDB
Также никто не запрещает комбинировать эти подходы — RDB для бэкапов, а AOF для сохранности при рестартах
В продолжение к предыдущему посту. В редисе есть две политики отлива данных на диск
1. RDB File (Redis Database File) — компактный снапшот-файл, хранящий всю информацию про ключи и значения на определенный момент времени
Поскольку снапшоты хранят весь снимок базы, брать их часто не очень целесообразно. Поэтому если страшно потерять данные, скажем, за последний час — то для восстановления данных после рестарта этот режим не очень подходит. Однако, этот режим удобно использовать для бэкапов: например, брать снапшот каждый час и отливать в S3
+: Компактный
+: Быстрое восстановление данных из снапшота
-: Не получится делать снимки часто, соотв-но режим не подходит, если хочется минимизировать риски потери данных
2. AOF (Append Only File) — лог write-операций. При рестарте редис просто проходится по логу и выполняет записанные операции
Когда мы делаем write-операцию, редис для быстроты делает запись только в оперативную память. Возникает вопрос — когда данные отливать на диск? Здесь есть несколько режимов:
2.1. appendfsync always: при write-операциях синхронно записываем данные и на диск. Риски потери данных минимальны, но и просадка в производительности на порядок (можно погуглить бенчмарки). Если вам нужен этот режим, вам точно нужен редис?
2.2. appendfsync no: запись на диск происходит, когда заполняется buffer memory. Периодичность отлива данных недетерменирована, поэтому не можем давать никаких гарантий
2.3. appendfsync everysec: бекграунд тредик каждую секунду сбрасывает данные на диск. Таким образом, операции записи по прежнему будут быстрыми (пишем только в ram), но будут возможны потери данных за последнюю секунду
+: Подходит, если нужно минимизировать риски потери данных
-: Менее компактный нежели RDB
-: Дольше восстановление данных нежели из RDB
Также никто не запрещает комбинировать эти подходы — RDB для бэкапов, а AOF для сохранности при рестартах
Telegram
Microservices Thoughts
⚡Потери данных в Redis
В случаях, когда на присутствие элемента в редисе завязана какая-то бизнес логика, бОльшая чем просто ускорение доступа к данным, хорошо бы задуматься о гарантиях, которые редис предоставляет
Можно выделить два сценария потери успешно…
В случаях, когда на присутствие элемента в редисе завязана какая-то бизнес логика, бОльшая чем просто ускорение доступа к данным, хорошо бы задуматься о гарантиях, которые редис предоставляет
Можно выделить два сценария потери успешно…
👍40🔥15
⚡CQRS и check-then-act
В CQRS (Command and Query Responsibility Segregation) мы разделяем запросы на чтение (queries) и запросы на модификацию (commands). Зачастую это ведет к тому, что у нас есть две базы данных: куда пишем, и откуда читаем. Данные из write-базы асинхронно реплицируются на read-базу
Далее представим, что мы хотим сделать какую-то выборку, проверить ее на соответствие условиям (check), и далее сделать модифицирующий запрос (then act)
Для примера возьмем модель данных, которая уже была в постах выше - с ticket и assignee
Хотим назначить тикет на оператора, если на него сейчас назначено меньше 4х тикетов:
1. read-db: считаем кол-во тикетов
2. write-db: если count < 4, то назначаем новый тикет по assignee_id
Далее представим что приходят два конкурентных запроса
[rq1] read-db: считаем кол-во тикетов, count = 3
[rq2] read-db: считаем кол-во тикетов, count = 3
[rq1] write-db: назначаем новый тикет
[rq2] write-db: назначаем новый тикет
... Данные асинхронно реплицируются, count = 4 ...
... Данные асинхронно реплицируются, count = 5 ...
Итого получаем 5 назначенных тикетов, и мы нарушили инвариант
---
Решить эту проблему можно с помощью оптимистических блокировок. К сущности assignee добавляется поле update_id:
В таком случае назначение будет происходить не по assignee_id, а по паре (assignee_id, update_id)
[rq1] read-db: считаем кол-во тикетов, count = 3, update_id = 0
[rq2] read-db: считаем кол-во тикетов, count = 3, update_id = 0
[rq1] write-db: назначаем новый тикет по update_id = 0, получаем update_id = 1
[rq2] write-db: назначаем новый тикет по update_id = 0, ошибка, 0 != 1
... Данные асинхронно реплицируются, count = 4, update_id = 1 ...
Таким образом, когда [rq2] попытается назначить тикет, он увидит в write-db неактуальную версию и ничего не сделает/кинет ошибку, что сохранит нам инвариант
В CQRS (Command and Query Responsibility Segregation) мы разделяем запросы на чтение (queries) и запросы на модификацию (commands). Зачастую это ведет к тому, что у нас есть две базы данных: куда пишем, и откуда читаем. Данные из write-базы асинхронно реплицируются на read-базу
Далее представим, что мы хотим сделать какую-то выборку, проверить ее на соответствие условиям (check), и далее сделать модифицирующий запрос (then act)
Для примера возьмем модель данных, которая уже была в постах выше - с ticket и assignee
assignee(id);
ticket(id, assignee_id);
Хотим назначить тикет на оператора, если на него сейчас назначено меньше 4х тикетов:
1. read-db: считаем кол-во тикетов
2. write-db: если count < 4, то назначаем новый тикет по assignee_id
Далее представим что приходят два конкурентных запроса
[rq1] read-db: считаем кол-во тикетов, count = 3
[rq2] read-db: считаем кол-во тикетов, count = 3
[rq1] write-db: назначаем новый тикет
[rq2] write-db: назначаем новый тикет
... Данные асинхронно реплицируются, count = 4 ...
... Данные асинхронно реплицируются, count = 5 ...
Итого получаем 5 назначенных тикетов, и мы нарушили инвариант
---
Решить эту проблему можно с помощью оптимистических блокировок. К сущности assignee добавляется поле update_id:
assignee(id, update_id);
ticket(id, assignee_id);
В таком случае назначение будет происходить не по assignee_id, а по паре (assignee_id, update_id)
[rq1] read-db: считаем кол-во тикетов, count = 3, update_id = 0
[rq2] read-db: считаем кол-во тикетов, count = 3, update_id = 0
[rq1] write-db: назначаем новый тикет по update_id = 0, получаем update_id = 1
[rq2] write-db: назначаем новый тикет по update_id = 0, ошибка, 0 != 1
... Данные асинхронно реплицируются, count = 4, update_id = 1 ...
Таким образом, когда [rq2] попытается назначить тикет, он увидит в write-db неактуальную версию и ничего не сделает/кинет ошибку, что сохранит нам инвариант
👍50✍5💅5🔥2 1
⚡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, где они делят пространство на шестиугольники можно почитать здесь
Обычно координаты задаются парой чисел - долготой и широтой. Это позволяет описать любую точку на земле, однако работать с таким представлением весьма сложно (см. например формулу расстояния между двумя точками)
Другим представлением координат является 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💅3 1
⚡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. Когда человек вызывает такси, мы определяем, в каком квадранте он находится, и ищем машины именно в нем
Когда человек находится в какой-то глуши, искать энивей будем в одном квадранте, но по большой площади из-за низкой плотности машин. В то же время, когда человек вызывает такси в городе, искать будем уже по меньшей площади, потому что плотность машин сильно больше
Пост про 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🔥9✍2💅2 1
⚡Про боттлнеки
Согласно вики, узкое место (bottlneck) — явление, при котором производительность или пропускная способность системы ограничена одним или несколькими компонентами или ресурсами
В контексте микросервисов самый банальный пример - есть N сервисов, есть отдельный сервис аутентификации, куда все ходят, общая нагрузка на систему 1k rps, сервис аутентификации держит лишь 100 rps - это и есть боттлнек, тк он будет тормозить работу всей системы
Где могут находиться боттлнеки? Везде. Современное приложение зачастую включает в себя кучу компонентов: балансировщик нагрузки, само приложение, бд, распределенный кеш, брокер сообщений, etc. Каждый из этих компонентов может быть узким местом, иногда даже его определенный аспект: например, пропускная способность диска
Пример из жизни: иногда сервис начинает долго отвечать и пятисотить. Смотрим графики базы - видим повышение avg transaction time. Смотрим логи приложения - видим ошибки похода во внешний сервис, и ошибки, что не можем получить конекшн до бд. Связываем эти два факта, и понимаем, что походы во внешний сервис делаются в рамках транзакции, и у приложения просто напросто кончаются свободные конекшны до бд
Таким образом, основная задача поиска узких мест - это постоянная локализация проблемы. Потому что как в примере выше проблема может заключаться в паре неверных строчках кода
Локализовать проблему могут помочь грамотные мониторинги и профилирование. Ставьте 👍, если нужен пост на эту тему
Согласно вики, узкое место (bottlneck) — явление, при котором производительность или пропускная способность системы ограничена одним или несколькими компонентами или ресурсами
В контексте микросервисов самый банальный пример - есть N сервисов, есть отдельный сервис аутентификации, куда все ходят, общая нагрузка на систему 1k rps, сервис аутентификации держит лишь 100 rps - это и есть боттлнек, тк он будет тормозить работу всей системы
Где могут находиться боттлнеки? Везде. Современное приложение зачастую включает в себя кучу компонентов: балансировщик нагрузки, само приложение, бд, распределенный кеш, брокер сообщений, etc. Каждый из этих компонентов может быть узким местом, иногда даже его определенный аспект: например, пропускная способность диска
Пример из жизни: иногда сервис начинает долго отвечать и пятисотить. Смотрим графики базы - видим повышение avg transaction time. Смотрим логи приложения - видим ошибки похода во внешний сервис, и ошибки, что не можем получить конекшн до бд. Связываем эти два факта, и понимаем, что походы во внешний сервис делаются в рамках транзакции, и у приложения просто напросто кончаются свободные конекшны до бд
Таким образом, основная задача поиска узких мест - это постоянная локализация проблемы. Потому что как в примере выше проблема может заключаться в паре неверных строчках кода
Локализовать проблему могут помочь грамотные мониторинги и профилирование. Ставьте 👍, если нужен пост на эту тему
👍206💅7🔥3✍2 1
⚡Визуализация архитектуры (C4 + PlantUML)
Одна из основных проблем в визуализации — это что каждый разработчик по разному себе ее представляет. Кто-то считает одни детали важными, кто-то другие. В итоге это приводит к набору разностортных картинок, которые проблематично склеить вместе (либо к супер перегруженным диаграммам)
C4 — это модель представления архитектуры в виде четырех слоев:
1. Диаграмма контекста: взаимодействие системы с пользователем, с другими системами
2. Диаграмма контейнеров: как в рамках системы взаимодействуют контейнеры (единицы, которые независимо деплоятся). Например, взаимодействие микросов
3. Диаграмма компонентов: как в рамках контейнера взаимодействуют его отдельные части. Например, модули в рамках микроса
4. Диаграмма кода: как в рамках компонента написан код. Обычно это просто диаграмма классов
Такая модель позволяет очертить уровень детализации в каждом слое, и дать общее понимание, что мы хотим видеть в конкретной диаграмме. А визуализировать это можно с помощью обычных UML-диаграмм
При этом существует PUML (PlantUML) — инструмент описания UML с помощью текста. Конкретно для C4 существует "плагин", который позволяет удобно описывать диаграмму в терминах C4
Базовый пример:
Попробовать отрисовать его можно тут
Одна из основных проблем в визуализации — это что каждый разработчик по разному себе ее представляет. Кто-то считает одни детали важными, кто-то другие. В итоге это приводит к набору разностортных картинок, которые проблематично склеить вместе (либо к супер перегруженным диаграммам)
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🙏1 1
⚡Архитектура и оргструктура
Закон Конвея: «организации проектируют системы, которые копируют структуру коммуникаций в этой организации»
Если возникает несоответствие, то на практике это приводит
- Либо к рефакторингу: архитектура бьется под то, как устроены команды
- Либо к реоргу: команды перемешиваются так, чтобы было комфортно общаться в рамках текущей архитектуры
---
Предположим есть две небольшие команды: первая занимается сервисом X, вторая — сервисом Y. Команды ведут бэклог и приоритизируют задачи независимо друг от друга. И последнее время стало заметно, что time to market сильно вырос — почти каждая фича требует доработок в обоих сервисах и требует координации/синхронизации между командами
Пишите в комментах, какие способы уменьшения TTM здесь видите
Закон Конвея: «организации проектируют системы, которые копируют структуру коммуникаций в этой организации»
Если возникает несоответствие, то на практике это приводит
- Либо к рефакторингу: архитектура бьется под то, как устроены команды
- Либо к реоргу: команды перемешиваются так, чтобы было комфортно общаться в рамках текущей архитектуры
---
Предположим есть две небольшие команды: первая занимается сервисом X, вторая — сервисом Y. Команды ведут бэклог и приоритизируют задачи независимо друг от друга. И последнее время стало заметно, что time to market сильно вырос — почти каждая фича требует доработок в обоих сервисах и требует координации/синхронизации между командами
Пишите в комментах, какие способы уменьшения TTM здесь видите
👍18🔥4💅4 1
⚡Поиск по иерархичным данным в elasticsearch (и не только)
Представьте, что у вас есть две сущности, которые связаны внешними ключами
Например, в нашем случае такими сущностями выступают
Ticket — обращение пользователя в поддержку: содержит кучу разной метаинфы в виде тегов, кастомных полей
Article — сообщение в рамках одного тикета
Одним из кейсов для поиска является запрос "полнотекстово поискать по тексту артиклов среди тикетов, у которых такие-то метаданные". То есть в поиске участвует как родительская сущность (ticket), так и дочерняя (article)
Если это делать на реляционной БД, то все достаточно просто: накидываем нужные индексы на обе таблицы, и далее в запросах джойним и фильтруем по обоим таблицам. Особо тут ничего не оптимизируешь
Однако если для поиска выделяется отдельная БД (elastic, sphinx, etc.), то появляется проблема — надо как-то перекидывать данные из основной бд в поиск для индексации, и как-то располагать эти данные в индексе
---
Есть два ключевых трейдоффа:
1. Раздельное хранение
В поисковом индексе parent и child сущности хранятся независимо друг от друга
Плюс: быстрая индексация
Если меняется родительская сущность, не надо переиндексировать дочерние
Минус: медленный поиск
Нужно как-то джойнить документы: либо parent-child запросы в эластик, либо application-side джойны. Это довольно слабо масштабируется, и может оочень сильно тормозить поисковые запросы
2. Денормализация
В каждую дочернюю сущность кладем также всю инфу про родительскую сущность
Плюс: быстрый поиск
Вся нужная инфа хранится в одном документе, соотв-но не нужно думать про иерархию, теперь иерархия никак не аффектит скорость поисковых запросов
Минус: долгая индексация, больше данных
Поменялась родительская сущность => нужно переиндексировать все дочерние. Также из-за денормализации в индексе хранится больше данных, тк для каждой дочерней сущности мы дублируем всю инфу про родительскую
---
Поскольку первый вариант не очень масштабируется, чаще всего на больших объемах приходится жить с денормализацией. Пишите в комменты, какой подход используете вы в своих проектах с поиском
Представьте, что у вас есть две сущности, которые связаны внешними ключами
Например, в нашем случае такими сущностями выступают
Ticket — обращение пользователя в поддержку: содержит кучу разной метаинфы в виде тегов, кастомных полей
Article — сообщение в рамках одного тикета
Одним из кейсов для поиска является запрос "полнотекстово поискать по тексту артиклов среди тикетов, у которых такие-то метаданные". То есть в поиске участвует как родительская сущность (ticket), так и дочерняя (article)
Если это делать на реляционной БД, то все достаточно просто: накидываем нужные индексы на обе таблицы, и далее в запросах джойним и фильтруем по обоим таблицам. Особо тут ничего не оптимизируешь
Однако если для поиска выделяется отдельная БД (elastic, sphinx, etc.), то появляется проблема — надо как-то перекидывать данные из основной бд в поиск для индексации, и как-то располагать эти данные в индексе
---
Есть два ключевых трейдоффа:
1. Раздельное хранение
В поисковом индексе parent и child сущности хранятся независимо друг от друга
Плюс: быстрая индексация
Если меняется родительская сущность, не надо переиндексировать дочерние
Минус: медленный поиск
Нужно как-то джойнить документы: либо parent-child запросы в эластик, либо application-side джойны. Это довольно слабо масштабируется, и может оочень сильно тормозить поисковые запросы
2. Денормализация
В каждую дочернюю сущность кладем также всю инфу про родительскую сущность
Плюс: быстрый поиск
Вся нужная инфа хранится в одном документе, соотв-но не нужно думать про иерархию, теперь иерархия никак не аффектит скорость поисковых запросов
Минус: долгая индексация, больше данных
Поменялась родительская сущность => нужно переиндексировать все дочерние. Также из-за денормализации в индексе хранится больше данных, тк для каждой дочерней сущности мы дублируем всю инфу про родительскую
---
Поскольку первый вариант не очень масштабируется, чаще всего на больших объемах приходится жить с денормализацией. Пишите в комменты, какой подход используете вы в своих проектах с поиском
👍27💅3 3🔥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, то это может помочь предотвратить каскадные падения
Представим, есть сервис агрегатор, который собирает данные с других сервисов. Если внешний сервис не доступен, мы не хотим долго ждать, а хотим дать ответ пользователю с тем, что есть
Здесь может помочь 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
⚡Верхнеуровный принцип работы полнотекстового поиска
Представим у нас магазин товаров, товар описывается тремя полями
Мы хотим уметь полнотекстово поискать по названию, а также пофильтровать товары по типу
Для полнотекстового поиска обычно используется обратный индекс. Посмотрим, как происходит добавление документа
1. Берем поле name
2. Бьем исходное имя на токены: ["big", "black", "box"]
3. Добавляем инфу, что токены "big", "black" и "box" встречаются в документе 17
Обратный будет представлять собой мапку token -> [document_id] и для поля name будет выглядить так:
Для остальных индексируемых полей строим аналогично
---
Загрузив несколько документов, получится примерно такое:
Для поля name:
Для поля type:
---
И наконец, чтобы сделать поиск name ~ "small" and type = ball_type, нужно
1. Посмотреть в обратный индекс name на токен small: получим документы [13, 29]
2. Посмотреть в обратный индекс type на токен ball_type: получим документы [13, 21]
3. Пересечь выборки: получим документы [13]
Конечно, в реальности добавляется куча нюансов: запросы не всегда настолько простые, непонятно как эффективно пересекать выборки из миллионов документов, выборки нужно ранжировать и т.д. При этом общий принцип использования обратного индекса сохраняется
Покажите реакцией 👍, если нужно еще постов на тему поиска
Представим у нас магазин товаров, товар описывается тремя полями
{
"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💅2 2
⚡️Как чему-то учиться
Внезапно вспомнил про пост, где вы предлагали темы для постов и самый залайканный коммент. Хочу раскрыть тему чуть шире — принципы обучения, которые считаю эффективными
Чтобы начать создавать что-то новое и классное, сначала нужно
1. Научиться копировать того, кто это уже умеет делать
2. Параллельно думать "а как бы я это сделал"
3. Постепенно реверс-инжинирить чужие решения. От частных деталей к общей теории
Почему это работает:
1 — по крайней мере мы научимся как-то решать задачу. Неосознанно, но научимся. Для меня это убирает "страх чистого листа" и добавляет желание разобраться, а почему оно сделано именно так
2 — если человек только начинает знакомиться с каким-то инструментом/технологией, то высока вероятность, что на вопрос "а как бы я это сделал" он придумает полную хрень. Однако в этом и смысл — после совершения ошибок инфа легче запоминается. Обвалидировать свои догадки насчет решения можно об более опытных коллег (либо об gpt, если коллеги с вами не разговаривают)
3 — здесь смысл в том, что смотрим на частные детали уже работающего решения (которое в п.1 мы просто копировали), читаем связанную теорию на эту тему до тех пор, пока не поймем, почему оно сделано именно так
---
И можно посмотреть на эту схему на синтетическом примере. Допустим мне очень хочется научиться писать task-scheduler-ы на БДшке
1. Копирую чье-то решение, верхнеуровнего смотрю, какие сущности там мелькают
2. Предлагаю свое наивное решение: "а давайте просто селектить все зашедуленные задачи и в форике выполнять". И мне начинают накидывать: "а что если несколько инстансов возьмут одинаковые задачи?", "а что если скопилось млн задач?", "а насколько это масштабируется?"
3. Начинаю смотреть работающее решение. Вижу "select for update" — открываю доку, вижу что эта штука берет блокировки. Иду читать про блокировки, зачем они нужны. Окей, тут вроде разобрались: это нужно, чтобы только один инстанс брал задачу. Потом осознаю что запросы выполняются быстро, даже если задач скопилось очень много — мне подсказывают про индексы. Иду читать про индексы до тех пор, пока не пойму, почему они помогают ускорить. И так далее
---
Итого после этих трех пунктов у вас в голове останется: пример хорошего решения (и почему оно хорошее), пример плохого решения (и почему оно плохое) и теория, которая за всем этим стоит. На основе этого можно осознанно придумать что-то свое и новое
Внезапно вспомнил про пост, где вы предлагали темы для постов и самый залайканный коммент. Хочу раскрыть тему чуть шире — принципы обучения, которые считаю эффективными
Чтобы начать создавать что-то новое и классное, сначала нужно
1. Научиться копировать того, кто это уже умеет делать
2. Параллельно думать "а как бы я это сделал"
3. Постепенно реверс-инжинирить чужие решения. От частных деталей к общей теории
Почему это работает:
1 — по крайней мере мы научимся как-то решать задачу. Неосознанно, но научимся. Для меня это убирает "страх чистого листа" и добавляет желание разобраться, а почему оно сделано именно так
2 — если человек только начинает знакомиться с каким-то инструментом/технологией, то высока вероятность, что на вопрос "а как бы я это сделал" он придумает полную хрень. Однако в этом и смысл — после совершения ошибок инфа легче запоминается. Обвалидировать свои догадки насчет решения можно об более опытных коллег (либо об gpt, если коллеги с вами не разговаривают)
3 — здесь смысл в том, что смотрим на частные детали уже работающего решения (которое в п.1 мы просто копировали), читаем связанную теорию на эту тему до тех пор, пока не поймем, почему оно сделано именно так
---
И можно посмотреть на эту схему на синтетическом примере. Допустим мне очень хочется научиться писать task-scheduler-ы на БДшке
1. Копирую чье-то решение, верхнеуровнего смотрю, какие сущности там мелькают
2. Предлагаю свое наивное решение: "а давайте просто селектить все зашедуленные задачи и в форике выполнять". И мне начинают накидывать: "а что если несколько инстансов возьмут одинаковые задачи?", "а что если скопилось млн задач?", "а насколько это масштабируется?"
3. Начинаю смотреть работающее решение. Вижу "select for update" — открываю доку, вижу что эта штука берет блокировки. Иду читать про блокировки, зачем они нужны. Окей, тут вроде разобрались: это нужно, чтобы только один инстанс брал задачу. Потом осознаю что запросы выполняются быстро, даже если задач скопилось очень много — мне подсказывают про индексы. Иду читать про индексы до тех пор, пока не пойму, почему они помогают ускорить. И так далее
---
Итого после этих трех пунктов у вас в голове останется: пример хорошего решения (и почему оно хорошее), пример плохого решения (и почему оно плохое) и теория, которая за всем этим стоит. На основе этого можно осознанно придумать что-то свое и новое
Telegram
Andrei in Microservices Thoughts Chat
Поделишь откуда черпаешь знания, помимо практики. Что из интересного рекомендуешь почитать?
👍85🔥17✍8💅2 1
⚡Мысли про сеньорность
Как можно охарактеризовать человека с лычкой сеньора? Годами опыта? Кругозором в технологиях? Сложностью решаемых задач?
Общеизвестный факт — все зависит от компании. А в рамках одной компании я бы выделил две ситуации:
1. Человека сразу наняли на сеньорский грейд
Это значит ни больше ни меньше, что человек научился проходить собесы на уровне сеньора в представлении конкретного нанимающего менеджера (или какого-то кворума, который определяет грейд). То есть если человек научился рассказывать (или придумывать) про свой опыт, а также неплохо решать алгосы, сисдиз и отвечать на бихейв вопросы, то формально он сеньор в конкретной компании. Даже если у него год фактического опыта
2. Человек вырос внутри компании с мидла
Это значит, что человек соответствует ожиданиям человека(-ов), которые могут повысить ему грейд. Это может быть:
- Широкая зона ответственности
- Скорость решаемых задач
- Сложность решаемых задач/уникальная экспертиза
- И тд и тп в зависимости от компании
И при переходе в другую компанию человек может как сохранить эту лычку, если там ожидания от сеньоров +- такие же, либо не сохранить
А вы что думаете?
Как можно охарактеризовать человека с лычкой сеньора? Годами опыта? Кругозором в технологиях? Сложностью решаемых задач?
Общеизвестный факт — все зависит от компании. А в рамках одной компании я бы выделил две ситуации:
1. Человека сразу наняли на сеньорский грейд
Это значит ни больше ни меньше, что человек научился проходить собесы на уровне сеньора в представлении конкретного нанимающего менеджера (или какого-то кворума, который определяет грейд). То есть если человек научился рассказывать (или придумывать) про свой опыт, а также неплохо решать алгосы, сисдиз и отвечать на бихейв вопросы, то формально он сеньор в конкретной компании. Даже если у него год фактического опыта
2. Человек вырос внутри компании с мидла
Это значит, что человек соответствует ожиданиям человека(-ов), которые могут повысить ему грейд. Это может быть:
- Широкая зона ответственности
- Скорость решаемых задач
- Сложность решаемых задач/уникальная экспертиза
- И тд и тп в зависимости от компании
И при переходе в другую компанию человек может как сохранить эту лычку, если там ожидания от сеньоров +- такие же, либо не сохранить
А вы что думаете?
👍80✍3
⚡️Как искать боттлнеки
В одном из прошлых постов поговорили, что такое боттлнеки, и где они могут находиться. Теперь посмотрим, какие мониторинги и инструменты могут помочь их найти
1. Мониторинги времени обработки запроса/сообщения из очереди
Графики по перцентилю response time апишек/по обработке сообщений в условной кафке. Позволяют локализовать проблему от "все плохо" до "все плохо в конкретном сервисе/бд/очереди"
2. Мониторинги потребления ресурсов
Графики по CPU/RAM/Disk/Network. Позволяют локализовать проблему от "все плохо в конкретном сервисе" до "все плохо с CPU в конкретном сервисе"
3. Профилирование
Сбор инфы, на что конкретное приложение тратит ресурсы. Позволяет локализовать проблему от "все плохо с CPU" до "все плохо с этим методом, который делает в цикле какую-то хрень"
---
И отдельно можно выделить трейсинг. Он будет полезен, когда какая-то бизнес-функция пронизывает кучу сервисов/компонентов/etc, и хочется быстро диагностировать, на каком этапе что-то пошло не так
Ставьте 👍 на этот пост, если интересен рассказ про то, как оптимизировать найденные узкие места
В одном из прошлых постов поговорили, что такое боттлнеки, и где они могут находиться. Теперь посмотрим, какие мониторинги и инструменты могут помочь их найти
1. Мониторинги времени обработки запроса/сообщения из очереди
Графики по перцентилю response time апишек/по обработке сообщений в условной кафке. Позволяют локализовать проблему от "все плохо" до "все плохо в конкретном сервисе/бд/очереди"
2. Мониторинги потребления ресурсов
Графики по CPU/RAM/Disk/Network. Позволяют локализовать проблему от "все плохо в конкретном сервисе" до "все плохо с CPU в конкретном сервисе"
3. Профилирование
Сбор инфы, на что конкретное приложение тратит ресурсы. Позволяет локализовать проблему от "все плохо с CPU" до "все плохо с этим методом, который делает в цикле какую-то хрень"
---
И отдельно можно выделить трейсинг. Он будет полезен, когда какая-то бизнес-функция пронизывает кучу сервисов/компонентов/etc, и хочется быстро диагностировать, на каком этапе что-то пошло не так
Ставьте 👍 на этот пост, если интересен рассказ про то, как оптимизировать найденные узкие места
Telegram
Microservices Thoughts
⚡Про боттлнеки
Согласно вики, узкое место (bottlneck) — явление, при котором производительность или пропускная способность системы ограничена одним или несколькими компонентами или ресурсами
В контексте микросервисов самый банальный пример - есть N сервисов…
Согласно вики, узкое место (bottlneck) — явление, при котором производительность или пропускная способность системы ограничена одним или несколькими компонентами или ресурсами
В контексте микросервисов самый банальный пример - есть N сервисов…
👍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мс не получили ответ, посылаем второй запрос
Итого получаем (считая что запросы независимые), вероятность, что такая совокупность не уложится в 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 раз
Некоторым системам важно иметь почти реалтаймовые ответы (~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🤔11✍3🤯3💅1 1
⚡️Как жить с нестабильной интеграцией
Иногда возникают ситуации, что ваш сервис ходит в какую-то внешнюю (относительно команды или вообще компании) систему, которая работает нестабильно. Из-за этого может страдать стабильность и вашего сервиса
Представим, что внешнюю систему чинить не сильно торопятся, а альтернатив у вас нет. Что можно сделать в такой ситуации в рамках вашего сервиса, чтобы повысить стабильность?
1. Если ваш сервис только забирает данные из внешней системы
В таком случае можно сделать локальную копию данных, которая вам нужна от внешней системы. Например, сделать кеш. Кеш можно устроить по разному в зависимости от контекста:
- Либо раз в какое-то время подгружать полный слепок данных и сохранять к себе
- Либо кешировать результаты отдельных запросов с каким-то TTL
- Либо обновлять кеш по эвентам от внешней системы (если она их отправляет)
2. Если ваш сервис только отправляет данные во внешнюю систему
Пример: отправка каких-нибудь нотификашек. В таком случае можно уменьшить связность между вашим сервисом и внешней системой, заменив синхронную интеграцию на асинхронную. Например, добавив очередь сообщений. Это позволит в случае недоступностей внешней системы доставить сообщение "когда-нибудь потом"
3. Если ваш сервис отправляет данные во внешнюю систему и ожидает чего-то в ответ
Пример: создание сущности во внешней системе и получение id в ответе
Пишите в комментах, какие варианты повышения стабильности здесь видите
Иногда возникают ситуации, что ваш сервис ходит в какую-то внешнюю (относительно команды или вообще компании) систему, которая работает нестабильно. Из-за этого может страдать стабильность и вашего сервиса
Представим, что внешнюю систему чинить не сильно торопятся, а альтернатив у вас нет. Что можно сделать в такой ситуации в рамках вашего сервиса, чтобы повысить стабильность?
1. Если ваш сервис только забирает данные из внешней системы
В таком случае можно сделать локальную копию данных, которая вам нужна от внешней системы. Например, сделать кеш. Кеш можно устроить по разному в зависимости от контекста:
- Либо раз в какое-то время подгружать полный слепок данных и сохранять к себе
- Либо кешировать результаты отдельных запросов с каким-то TTL
- Либо обновлять кеш по эвентам от внешней системы (если она их отправляет)
2. Если ваш сервис только отправляет данные во внешнюю систему
Пример: отправка каких-нибудь нотификашек. В таком случае можно уменьшить связность между вашим сервисом и внешней системой, заменив синхронную интеграцию на асинхронную. Например, добавив очередь сообщений. Это позволит в случае недоступностей внешней системы доставить сообщение "когда-нибудь потом"
3. Если ваш сервис отправляет данные во внешнюю систему и ожидает чего-то в ответ
Пример: создание сущности во внешней системе и получение id в ответе
Пишите в комментах, какие варианты повышения стабильности здесь видите
👍28🔥9💅3 2
⚡️Загадка про постгрес
1. Есть табличка с колонками a, b
2. Есть 10 млн записей, примерно у половины записей в колонке a лежит null
3. Есть btree индекс на (a, b)
Угадайте план запроса
Верно,секскан + сортировка
---
Как выяснилось (после инцидента), постгрес не умеет в фильтрацию по null/is null + сортировку одновременно. Если только фильтрация — будет ок. Поэтому для такого кейса нужно создавать отдельный частичный индекс
proof: https://dbfiddle.uk/Bgob-orb
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💅3 3
⚡️Какие-то мысли про 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 очередь
- сообщения редко могут теряться (специфика редиса, см пост)
- порядок обработки либо не важен, либо вы готовы заморочиться и вручную реализовать партицирование
Недавно возникла необходимость в простенькой очереди, чтобы на запрос быстро отвечать двухсоткой и далее асинхронно обрабатывать. Решили поэкспериментировать с 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 очередь
- сообщения редко могут теряться (специфика редиса, см пост)
- порядок обработки либо не важен, либо вы готовы заморочиться и вручную реализовать партицирование
👍22✍3💅1 1