⚡Transactional inbox
С помощью transactional outbox мы умеем обеспечивать надежную доставку сообщений до брокера
Но теперь возникает другая проблема — консюмеру нужно ровно один раз обработать сообщение
В случае наивного решения
Может случиться ситуация, что databaseTx закоммитилась, но message.commit() не отработал. Это приведет к тому, что при следующем чтении мы обработаем сообщение еще раз
И здесь нам поможет transactional inbox, у которого я выделяю два вида
1) По-прежнему сначала обрабатываем сообщение, потом коммитим. Но добавляем дедупликацию
В таком случае даже если databaseTx закоммитилась, но message.commit() не отработал, то при повторном чтении мы увидим сохраненный ключ сообщения, и сразу его закоммитим
2) Сохраняем сообщение в таблицу, и фоновые воркеры достают сообщения из таблицы и обрабатывают
Несмотря на то, что такой подход решает ту же проблему, еще и при этом добавляет latency, у него есть весомый плюс — консюмер теперь может балансировать нагрузку на себя
Причем это работает в обе стороны:
1) Например, если сообщения в нас отправляют по http со слишком высоким рейтом, то мы просто сохраняем их в таблицу и процессим с доступной нам скоростью
2) И наоборот: если сообщения мы сами читаем из топика, но у топика слишком мало партиций, и существующие консюмеры не успевают обрабатывать приходящие сообщения, то можно также их просто сохранить в таблицу, и далее нужным количеством воркеров разгребать эту таблицу
С помощью transactional outbox мы умеем обеспечивать надежную доставку сообщений до брокера
Но теперь возникает другая проблема — консюмеру нужно ровно один раз обработать сообщение
В случае наивного решения
processMessage() {
databaseTx {
…
}
message.commit()
}Может случиться ситуация, что databaseTx закоммитилась, но message.commit() не отработал. Это приведет к тому, что при следующем чтении мы обработаем сообщение еще раз
И здесь нам поможет transactional inbox, у которого я выделяю два вида
1) По-прежнему сначала обрабатываем сообщение, потом коммитим. Но добавляем дедупликацию
processMessage() {
databaseTx {
if (!tryInsert(msgKey)) {
message.commit()
return
}
…
}
message.commit()
}В таком случае даже если databaseTx закоммитилась, но message.commit() не отработал, то при повторном чтении мы увидим сохраненный ключ сообщения, и сразу его закоммитим
2) Сохраняем сообщение в таблицу, и фоновые воркеры достают сообщения из таблицы и обрабатывают
processMessage() {
databaseTx {
tryInsert(message)
}
message.commit()
}Несмотря на то, что такой подход решает ту же проблему, еще и при этом добавляет latency, у него есть весомый плюс — консюмер теперь может балансировать нагрузку на себя
Причем это работает в обе стороны:
1) Например, если сообщения в нас отправляют по http со слишком высоким рейтом, то мы просто сохраняем их в таблицу и процессим с доступной нам скоростью
2) И наоборот: если сообщения мы сами читаем из топика, но у топика слишком мало партиций, и существующие консюмеры не успевают обрабатывать приходящие сообщения, то можно также их просто сохранить в таблицу, и далее нужным количеством воркеров разгребать эту таблицу
👍47💅1
⚡Postgres и oversized-атрибуты
В Postgres страница — это базовая единица ввода-вывода данных, хранимых на диске (нельзя прочитать с диска меньше чем одну страницу). По умолчанию размер страницы составляет 8кб
Также Postgres не позволяет хранить одну запись на нескольких страницах. Однако, как известно существует множество типов данных, значения которых могут превышать 8кб (например, varchar). Возникает вопрос — как их уместить в одну страницу
TOAST (The Oversized-Attribute Storage Technique) — техника, когда мы храним значение аттрибута не напрямую в странице рядом с остальными аттрибутами, а перемещаем значения в отдельную toast-таблицу. В основной таблице храним указатель на запись в toast-таблице. Все это скрыто от пользователя, что позволяет не задумываясь хранить большие тексты/жсоны прямо в реляционной таблице (не делайте так), но в то же время имеет несколько неочевидных недостатков:
1. Идентификатор записи в toast-таблице имеет тип oid, который имеет 2^32 уникальных значений, что не так много. В случае когда все идентификаторы “потратятся”, и захочется сделать insert в основную таблицу, подразумевающий вставку в toast, то вставка не сработает
2. На toast-таблицу по-прежнему распространяются правила MVCC — если поредактировать колонку в основной таблице, значение которой лежит в toast, то старое значение в toast-таблице не удалится, а просто пометится удаленным. Физическое удаление и освобождение памяти произойдет после vacuum. Это может приводить к раздуванию toast-таблицы
—
Поэтому если у вас есть потребность хранить большие тексты/жсоны, и у сервиса высокая нагрузка/большой поток данных, то имеет смысл воспользоваться специализированными инструментами. Например, S3 — сам документ храним в S3, а в реляционной таблице храним просто s3_object_id. Получается тоже своего рода TOAST, но без вышеприведенных недостатков
Ставьте 👍 на этот пост, если нужен рассказ про S3
В Postgres страница — это базовая единица ввода-вывода данных, хранимых на диске (нельзя прочитать с диска меньше чем одну страницу). По умолчанию размер страницы составляет 8кб
Также Postgres не позволяет хранить одну запись на нескольких страницах. Однако, как известно существует множество типов данных, значения которых могут превышать 8кб (например, varchar). Возникает вопрос — как их уместить в одну страницу
TOAST (The Oversized-Attribute Storage Technique) — техника, когда мы храним значение аттрибута не напрямую в странице рядом с остальными аттрибутами, а перемещаем значения в отдельную toast-таблицу. В основной таблице храним указатель на запись в toast-таблице. Все это скрыто от пользователя, что позволяет не задумываясь хранить большие тексты/жсоны прямо в реляционной таблице (не делайте так), но в то же время имеет несколько неочевидных недостатков:
1. Идентификатор записи в toast-таблице имеет тип oid, который имеет 2^32 уникальных значений, что не так много. В случае когда все идентификаторы “потратятся”, и захочется сделать insert в основную таблицу, подразумевающий вставку в toast, то вставка не сработает
2. На toast-таблицу по-прежнему распространяются правила MVCC — если поредактировать колонку в основной таблице, значение которой лежит в toast, то старое значение в toast-таблице не удалится, а просто пометится удаленным. Физическое удаление и освобождение памяти произойдет после vacuum. Это может приводить к раздуванию toast-таблицы
—
Поэтому если у вас есть потребность хранить большие тексты/жсоны, и у сервиса высокая нагрузка/большой поток данных, то имеет смысл воспользоваться специализированными инструментами. Например, S3 — сам документ храним в S3, а в реляционной таблице храним просто s3_object_id. Получается тоже своего рода TOAST, но без вышеприведенных недостатков
Ставьте 👍 на этот пост, если нужен рассказ про S3
👍183🔥12💅4 2
⚡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