Microservices Thoughts @microservicesthoughts Channel on Telegram

Microservices Thoughts

@microservicesthoughts


Вопросы и авторские статьи по микросервисам, архитектуре, БД

По сотрудничеству: t.me/qsqnk

Контент по Kotlin/Java: t.me/KotlinThoughts

Microservices Thoughts (Russian)

Добро пожаловать на канал Microservices Thoughts! Здесь вы найдете ответы на ваши вопросы о микросервисах, архитектуре и базах данных. Мы также предоставляем авторские статьи на эти темы, чтобы помочь вам углубить свои знания. Наш канал отличается от других своими качественными материалами и экспертным мнением. nnЕсли у вас есть предложения о сотрудничестве или вы хотите опубликовать свою статью на нашем канале, свяжитесь с нами по ссылке t.me/qsqnk. Мы всегда открыты к новым идеям и готовы рассмотреть любые предложения. nnКроме того, мы предлагаем контент по языкам программирования Kotlin и Java на нашем канале t.me/KotlinThoughts. Здесь вы найдете полезные советы, обзоры и новости о данных языках. nnПрисоединяйтесь к нам на канале Microservices Thoughts, чтобы быть в курсе последних тенденций в мире микросервисов и разработки ПО. Мы ценим обучение и обмен знаниями, поэтому ваше участие на нашем канале будет весьма ценно. До встречи!

Microservices Thoughts

16 Nov, 13:10


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. Когда человек вызывает такси, мы определяем, в каком квадранте он находится, и ищем машины именно в нем

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

Microservices Thoughts

09 Nov, 12:03


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, где они делят пространство на шестиугольники можно почитать здесь

Microservices Thoughts

05 Nov, 14:52


Предлагайте

Идеи для постов в комментах)

Microservices Thoughts

26 Oct, 12:41


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 неактуальную версию и ничего не сделает/кинет ошибку, что сохранит нам инвариант

Microservices Thoughts

19 Oct, 10:31


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 для сохранности при рестартах

Microservices Thoughts

12 Oct, 12:51


Потери данных в Redis

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

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

1. Записали данные в RAM, но не записали на диск

С выключенным redis persistence может случиться крайне неприятная ситуация: данные льются на мастер, записываются в оперативку, реплицируются на слейв узлы. Но в один момент происходит отказ мастера. После рестарта мастер-узла он окажется пустым, т.к. ему не из чего будет восстанавливать данные. Более того, такое может случиться даже в sentinel конфигурации — мастер может упасть и рестартнуться настолько быстро, что sentinel это не задетектит и не переключит мастер

2. Записали данные на мастер, но не успели реплицировать

Предположим, у нас включен redis persistence и данные каким-то образом записываются и на диск. Здесь мы решаем вышеописанную проблему — мастер после рестарта просто восстановит данные с диска. Однако, есть другая проблема — асинхронная репликация. Мы можем записать данные на мастер, клиент получит ок с надеждой, что эти данные асинхронно долетят до реплик. Но здесь случается отказ мастера, данные не успевают дойти до реплик, и происходит переключение мастера. В итоге реплики так и не узнают про эту запись



Итого — первую проблему можем решать с помощью включения redis persistence. Вторую проблему не можем решать, так как редис поддерживает только асинхронную репликацию

Ставьте 👍 на этот пост, если нужен рассказ про варианты использования redis persistence

Microservices Thoughts

05 Oct, 19:08


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 секунд, то снимаем с него все задачи

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

Microservices Thoughts

29 Sep, 13:48


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

Microservices Thoughts

22 Sep, 13:58


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) И наоборот: если сообщения мы сами читаем из топика, но у топика слишком мало партиций, и существующие консюмеры не успевают обрабатывать приходящие сообщения, то можно также их просто сохранить в таблицу, и далее нужным количеством воркеров разгребать эту таблицу

Microservices Thoughts

21 Sep, 15:32


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, но также и переживать отказы некоторых узлов

Microservices Thoughts

14 Sep, 11:47


Вертикальное партицирование

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

Например, из


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. Если фильтрация по каждому условию по отдельности возвращает много строк, то запрос начнет работать сильно медленнее

Microservices Thoughts

07 Sep, 10:45


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, и тд. Правила могут динамически добавлятся, что позволяет управлять нагрузкой, если к примеру мощности шардов не одинаковые

Microservices Thoughts

01 Sep, 20:32


Channel name was changed to «Microservices Thoughts»

Microservices Thoughts

01 Sep, 11:24


Entity state transfer

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

1. Есть ticket service, хранящий обращения пользователей. Тикет задается тремя полями: id — идентификатор обращения, status — текущий статус OPEN / CLOSED и assignee — текущий оператор, обрабатыващий тикет

{
“id”: 123,
“status”: “OPEN”,
“assignee”: null
}


2. Есть chat service, который связывает обращения пользователей с оператором, ботом и т.д. Он должен как-то реагировать на изменения тикета

Возникает вопрос — как передавать изменения стейта

1. Синхронный подход

При изменении тикета ticket service синхронно дергает ручку в chat service, которая обработает изменения. Самый простой в реализации подход, но возникает проблема с high-coupling — в случае недоступности chat service будет недоступен и ticket service. Однако они должны уметь существовать в отдельности друг от друга, и доступность одного сервиса не должна влиять на доступность другого

2.1. Асинхронный подход: id

{
“id”: 123
}


Ticket service отправляет эвент, говорящий, что “что-то изменилось у тикета с определенным id”. Далее chat service, получая этот эвент, синхронно идет в ticket service и получает актуальный стейт. Такой подход хорош тем, что он охватывает сразу все изменения тикета, а также позволяет особо не думать про порядок эвентов — тк мы все равно сами ходим за актуальным стейтом. Из минусов — повышенная нагрузка на чтение на ticket service

2.2. Асинхронный подход: only change

{
“id”: 123,
“status”: {
“old”: “OPEN”,
“new”: “CLOSED”
}
}


Ticket service отправляет эвент, говорящий, что конкретно изменилось. Например status: OPEN -> CLOSED. Такой подход хорош тем, что отправляются лишь минимальный необходимый набор данных, но здесь уже нужно думать про порядок эвентов — потому что может быть важно обрабатывать изменения status и изменения assignee ровно в том порядке, в котором они произошли в тикетной системе

2.3. Асинхронный подход: snapshot

{
“id”: 123”,
“status”: “CLOSED”,
“assignee”: “operator_login”,
“snapshotTimestamp”: “2024-11-12T13:14:15Z”
}


Ticket service отправляет эвент со снапшотом текущего тикета. Из плюсов — не нужно синхронно ходить за стейтом тикета, он уже полностью есть в эвенте; особо не нужно думать про порядок — можно хранить timestamp последнего обработанного снапшота, и если приходит более ранний, то просто игнорируем. Из минусов — хранение в брокере большего объема информации

Microservices Thoughts

29 Aug, 09:20


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

Часто можно встретиться со следующим:

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

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

- Разрозненные правила разработки: отсутствие стандартизированных процессов и инструментов затрудняет работу и тормозит развитие проектов, поскольку каждая команда вынуждена изобретать собственные методы и подходы

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

Одним из решений, которое помогает минимизировать описанные выше проблемы, являются sensible defaults — внутренние стандарты команды/подразделения, описывающие кто чем занимается, как надо тестировать, каким правилам должна соответствовать архитектура проекта и так далее. Заранее установленные стандарты делают процесс разработки более предсказуемым и позволяют заранее иметь ответы на большинство возникающих вопросов

Microservices Thoughts

23 Aug, 18:14


Монотонность и PostgreSQL

Когда возникает потребность в монотонно возрастающих колонках обычно используются bigserial, timestamp и т.п.

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

Для примера возьмем bigserial (bigint, который берет значения из определенного сиквенса)

1. tx1: begin
2. tx1: save(entity) // взяли id=1 из сиквенса
3. tx2: begin
4. tx2: save(entity) // взяли id=2 из сиквенса
5. tx2: commit
6. tx1: commit


Получается ситуация, что после шага 5 у нас коммитится сущность с id=2, и только после шага 6 коммитится с id=1. Иными словами, для внешнего наблюдателя айдишники будут добавляться не в монотонном порядке

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

Как с таким можно бороться:

1. Ограничивать временной промежуток

При выгрузках делать условие на created_time < now() - interval ’n seconds’, у которого цель — гарантировать, что в данном промежутке появление новых айдишников/таймстемпов будет выглядить монотонно. Подразумевается, что за n секунд все “старые” транзакции закоммитятся. Вполне рабочий способ, если реалтаймовость необязательна

2. Брать айдишники не из секвенса, а из таблицы

create table id (
value bigint not null
);


И получать айдишник как

update id set value = value + 1 returning *;


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

3. Использование transaction id

Основная суть:

1) В сущности добавляем колонку transaction_id - xid8 транзакции, которая последняя ее проапдейтила. PG гарантирует, что он строго возрастает (подобно значениям из сиквенса)

2) В выгрузках делаем условие transaction_id < pg_snapshot_xmin(pg_current_snapshot()). Оно гарантирует, что в выбранных сущностях не появится изменений с меньшим transaction_id (т.е. которые логически произошли раньше)

Такой подход при отсутствии долгих транзакций в БД позволяет получить одновременно и “монотонность” и почти реалтаймовость. Однако требует дополнительных усилий с добавлением transaction_id, навешеванием триггера, который будет ее обновлять, и написанием довольно странных запросов с pg_current_snapshot()

Microservices Thoughts

17 Aug, 22:18


Microservices Thoughts pinned «Привет! Внезапно осознал, что канал уже почти как год ведется под некоторой пеленой анонимности, пора исправлять Меня зовут Александр Федькин, мне 21, сейчас руковожу командой бэкендеров в Яндексе Мы разрабатываем: - No-Code платформу для автоматизации…»

Microservices Thoughts

17 Aug, 12:49


Привет! Внезапно осознал, что канал уже почти как год ведется под некоторой пеленой анонимности, пора исправлять

Меня зовут Александр Федькин, мне 21, сейчас руковожу командой бэкендеров в Яндексе

Мы разрабатываем:

- No-Code платформу для автоматизации клиентского сервиса: сейчас в ней можно в UI настраивать чат-ботов и пайплайны обработки пользовательских обращений, а также привязывать их запуск к некоторым событиям

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

До этого успел окончить программную инженерию матмеха СПбГУ и поучиться в питерском CSC до его закрытия

В канал обычно пишу про какие-то занятные вещи с работы либо когда прочитал/нашел что-то интересное



Для новичков: топ постов за прошедший год:

- Каскадное удаление за один запрос
- Индексирование больших таблиц
- Consistent hashing
- Почему батчевые update/delete не безопасны
- Почему Redis быстрый, несмотря на однопоточность

Microservices Thoughts

10 Aug, 14:02


Частые архитектурные паттерны

При разработке небольших сервисов можно часто встретить повторяющиеся архитектурные паттерны. Мой личный топ 3:

1. Отсутствие архитектуры

Никаких правил нет. Обычно нет разграничения на модули, что приводит к тому, что всё доступно из всего. Например, эндпоинты могут напрямую вызывать бд и работать с энтитями. Несмотря на всю диковатость, такая “архитектура” вполне может подходить для простейших сервисов/прототипирования

2. Слоеная

Идет логическое разбиение на слои. Какого-то общепринятого разбиения нет, но обычно это что-то в духе:

web -> application -> domain -> DB

где web - контроллеры, application - бизнес логика, domain - модель данных + репозитории, DB - база данных

Также часто применяются прямые зависимости, т.е. высокоуровневые слои зависят от низкоуровневых, что противоречит dependency inversion principle в SOLID. Такая архитектура вполне жизнеспособна для сервисов до среднего размера с не очень сложной бизнес-логикой. Но стоит оговориться, что в команде должно быть сразу установлено, как именно делим на слои, могут ли быть “сквозные” вызовы (web -> domain) и т.п.

3. Гексагональная

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

1) которые бизнес логика предоставляет во вне (in-порты)
2) интерфейсы, которые необходимы для реализации бизнес логики (out-порты), например, интерфейс доступа к данным

И далее “по бокам” располагаются компоненты, которые

1) используют интерфейсы предоставляемые бизнес логикой, например, web-контроллеры
2) реализации интерфейсов (адаптеры), нужных для бизнес логики, например, реализация интерфейсов доступа к данным через postgres

При этом важно отметить, что именно “побочные” компоненты зависят от бизнес логики, а не наоборот. Это позволяет реализовывать основную логику приложения без использования каких-либо зависимостей (framework Agnostic). Такая архитектура хорошо подходит для относительно больших проектов со сложной логикой, но и накладывает некоторый оверхед

А какой вид архитектуры вы обычно используете в своих проектах и почему?

5,275

subscribers

24

photos

23

videos