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

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

Метод распределения данных между несколькими серверами БД, где логика распределения хранится на уровне приложения. При этом само хранилище может ничего не знать про шардирование

Логика работы:

1. Выбираем ключ шардирования

2. Определяем правила раутинга — как по ключу шардирования понять, на какой шард нужно отправить запрос

3. Приложению нужно исполнить некоторый запрос в БД

4. По запросу определяем, на какой(-ие) шард(-ы) его нужно отправить

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

1. Важно, чтобы было по минимуму читающих запросов, задействующих >1 шардов

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

3. Менять ключ шардирования и правила раутинга очень больно

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

Определив ключ шардирования нужно определить правила раутинга. Здесь можно выделить

1. Stateless подход — грубо говоря, когда правила раутинга задаются чистой функцией, не зависящей от состояния системы. Например, выбор шарда определяется как hash(entityId) % n, где n - фиксированное число шардов

2. Stateful подход — есть некоторое изменяемое хранилище метаданных, которое определяет, куда раутить запросы по определенным ключам. Например, таблица с динамически расширяющимися диапазонами: по entityId от 0 до 9999 идем в шард 1, по entityId от 10000 до 19999 идем в шард 2, и тд. Правила могут динамически добавлятся, что позволяет управлять нагрузкой, если к примеру мощности шардов не одинаковые
👍41💅8🤯11
Вертикальное партицирование

В отличие от горизонтального партицирования / шардирования таблица разделяется не по строкам, а по столбцам

Например, из


create table ticket
(
id bigserial primary key not null,
status varchar not null,
assignee varchar null
);


Получается


create table ticket
(
id bigserial primary key not null,
status varchar not null,
);

create table ticket_assignee
(
ticket_id bigserial not null,
assignee varchar null
);


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

Но стоит быть аккуратным, если есть запросы, задействующие одновременно и status, и assignee. Поскольку пока это хранится в одной таблице, можно сделать многоколоночный индекс на (status, assignee) и быстрым индекс сканом выполнять запрос. Если таблица поделится, то такое уже станет невозможным: нужно будет либо 1) поискать по таблице ticket по условию на status, а затем поджойнить с ticket_assignee, либо 2) поискать по ticket_assignee по условию assignee, а затем поджойнить с ticket. Если фильтрация по каждому условию по отдельности возвращает много строк, то запрос начнет работать сильно медленнее
👍3210🔥3💅22
n/2 + 1

Кворум — подмножество нод в распределенной системе

Quorum-based Consistency — модель, использумая для обеспечения Consistency из CAP-теоремы “Every read receives the most recent write or an error”, т.е. гарантирует, что не возникнет ситуаций, что один клиент получил успешное подтверждение о записи, а второй клиент эту запись некоторое время не видит. Обеспечивается за счет того, что для подтверждения операции записи/чтения нужно подтверждение от некоторого количества остальных узлов

Обычно разделяют кворум на запись и кворум на чтение
Пусть общее кол-во узлов в системе = N
Размер кворума на чтение = R
Размер кворума на запись = W

Возникает вопрос, как выбрать R и W, чтобы обеспечить consistency. Как минимум нужно, чтобы R + W > N, поскольку это обеспечивает пересечение множеств узлов на запись и на чтение — это нам гарантирует, что при чтении, мы обязательно увидим узел, на который успешно произошла запись

Однако вариаций, как обеспечить R + W > N довольно много, рассмотрим некоторые из них

1) R = 1, W = N

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

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

2) R = N, W = 1

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

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

3) R = N / 2 + 1, W = N / 2 + 1

Записываем изменения на большинство узлов
При чтении опрашиваем большинство узлов

Здесь мы по прежнему сохраняем условие, что кворумы на запись и чтение пересекаются, однако мы можем пережить отказы N - (N / 2 + 1) узлов, поскольку в случае отказа кворумы могут просто перегруппироваться



Пример:

Узлы {A, B, C}, N = 3
Кворум на чтение: {A, B}, R = 2
Кворум на запись: {B, C}, W = 2

Узел B отказывает, кворумы просто перераспределяются
Кворум на чтение: {A, C}
Кворум на запись: {A, C}

Таким образом, n/2 + 1 используется как компромисс между availability и consistency, поскольку позволяет сохранять consistency, но также и переживать отказы некоторых узлов
👍52🤯6💅41
Transactional inbox

С помощью 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
👍183🔥12💅42
Heartbeat pattern

Предположим у нас есть простая очередь задач

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💅61🤔11
Потери данных в Redis

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

Можно выделить два сценария потери успешно (со стороны клиента) записанных данных

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 для сохранности при рестартах
👍40🔥15
CQRS и check-then-act

В 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 неактуальную версию и ничего не сделает/кинет ошибку, что сохранит нам инвариант
👍505💅5🔥21
Предлагайте

Идеи для постов в комментах)
💅15🤔41
Geohash

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

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

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

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

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

И так далее

---

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

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

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

---

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

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

---

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

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

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

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

--

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

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

--

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

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

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

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

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

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

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

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

Локализовать проблему могут помочь грамотные мониторинги и профилирование. Ставьте 👍, если нужен пост на эту тему
👍206💅7🔥321
Визуализация архитектуры (C4 + PlantUML)

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

---

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

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

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

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

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

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

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

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

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

---

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

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

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

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

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

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

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

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

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

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

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

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

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

---

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

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

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

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

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

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

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

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

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

---

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

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


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

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

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

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


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

---

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

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


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


---

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

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

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

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

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

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

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

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

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

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

---

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

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

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

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

---

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

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

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

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

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

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

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

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

А вы что думаете?
👍803