⚡️Как искать боттлнеки
В одном из прошлых постов поговорили, что такое боттлнеки, и где они могут находиться. Теперь посмотрим, какие мониторинги и инструменты могут помочь их найти
1. Мониторинги времени обработки запроса/сообщения из очереди
Графики по перцентилю response time апишек/по обработке сообщений в условной кафке. Позволяют локализовать проблему от "все плохо" до "все плохо в конкретном сервисе/бд/очереди"
2. Мониторинги потребления ресурсов
Графики по CPU/RAM/Disk/Network. Позволяют локализовать проблему от "все плохо в конкретном сервисе" до "все плохо с CPU в конкретном сервисе"
3. Профилирование
Сбор инфы, на что конкретное приложение тратит ресурсы. Позволяет локализовать проблему от "все плохо с CPU" до "все плохо с этим методом, который делает в цикле какую-то хрень"
---
И отдельно можно выделить трейсинг. Он будет полезен, когда какая-то бизнес-функция пронизывает кучу сервисов/компонентов/etc, и хочется быстро диагностировать, на каком этапе что-то пошло не так
Ставьте 👍 на этот пост, если интересен рассказ про то, как оптимизировать найденные узкие места
В одном из прошлых постов поговорили, что такое боттлнеки, и где они могут находиться. Теперь посмотрим, какие мониторинги и инструменты могут помочь их найти
1. Мониторинги времени обработки запроса/сообщения из очереди
Графики по перцентилю response time апишек/по обработке сообщений в условной кафке. Позволяют локализовать проблему от "все плохо" до "все плохо в конкретном сервисе/бд/очереди"
2. Мониторинги потребления ресурсов
Графики по CPU/RAM/Disk/Network. Позволяют локализовать проблему от "все плохо в конкретном сервисе" до "все плохо с CPU в конкретном сервисе"
3. Профилирование
Сбор инфы, на что конкретное приложение тратит ресурсы. Позволяет локализовать проблему от "все плохо с CPU" до "все плохо с этим методом, который делает в цикле какую-то хрень"
---
И отдельно можно выделить трейсинг. Он будет полезен, когда какая-то бизнес-функция пронизывает кучу сервисов/компонентов/etc, и хочется быстро диагностировать, на каком этапе что-то пошло не так
Ставьте 👍 на этот пост, если интересен рассказ про то, как оптимизировать найденные узкие места
Telegram
Microservices Thoughts
⚡Про боттлнеки
Согласно вики, узкое место (bottlneck) — явление, при котором производительность или пропускная способность системы ограничена одним или несколькими компонентами или ресурсами
В контексте микросервисов самый банальный пример - есть N сервисов…
Согласно вики, узкое место (bottlneck) — явление, при котором производительность или пропускная способность системы ограничена одним или несколькими компонентами или ресурсами
В контексте микросервисов самый банальный пример - есть N сервисов…
👍104🔥1
⚡️Tail latency и hedged запросы
Некоторым системам важно иметь почти реалтаймовые ответы (~100мс). Например, саджесты в поиске: когда вы набираете очередной символ, то ожидаете очень быстро получить новый список вариантов
При этом зачастую происходит так:
- Среднее время запроса — 100мс
- Время запроса в p99 — 1 секунда
Эти задержки на больших перцентилях (p95, p99, p99.9) называются tail latency. Они могут происходить абсолютно по разным причинам от конкуренции за ресурсы на определенной машинке до сетевых проблем
---
Один из способов борьбы с такими задержками — техника hedged requests
Пусть у нашей системы p90 = 100ms, p99 = 200ms
На больших данных, считаем что
- Вероятность ответа >100ms = 0.1
- Вероятность ответа >200ms = 0.01
1. Посылаем первый запрос
2. Если спустя 100мс не получили ответ, посылаем второй запрос
Итого получаем (считая что запросы независимые), вероятность, что такая совокупность не уложится в 200ms = (вероятность, что первый не уложится в 200ms) * (вероятность, что второй не уложится в 100ms) = 0.01 * 0.1 = 0.001
Таким образом, вероятность ответить дольше 200ms стала 0.001, а значит p99.9 = 200ms
А вероятность, что придется отправлять второй запрос = вероятность, что первый отвечал >100ms = 0.1. То есть в 10% случаев придется слать два запроса
---
Таким образом, увеличив нагрузку на систему на 10% мы смогли перейти от p99 = 200ms до p99.9 = 200ms, то есть уменьшили "вероятность таймаутнуться" в 10 раз
Некоторым системам важно иметь почти реалтаймовые ответы (~100мс). Например, саджесты в поиске: когда вы набираете очередной символ, то ожидаете очень быстро получить новый список вариантов
При этом зачастую происходит так:
- Среднее время запроса — 100мс
- Время запроса в p99 — 1 секунда
Эти задержки на больших перцентилях (p95, p99, p99.9) называются tail latency. Они могут происходить абсолютно по разным причинам от конкуренции за ресурсы на определенной машинке до сетевых проблем
---
Один из способов борьбы с такими задержками — техника hedged requests
Пусть у нашей системы p90 = 100ms, p99 = 200ms
На больших данных, считаем что
- Вероятность ответа >100ms = 0.1
- Вероятность ответа >200ms = 0.01
1. Посылаем первый запрос
2. Если спустя 100мс не получили ответ, посылаем второй запрос
0ms 100ms 200ms
rq1 |-------------|-------------->|
rq2 | |-------------->|
Итого получаем (считая что запросы независимые), вероятность, что такая совокупность не уложится в 200ms = (вероятность, что первый не уложится в 200ms) * (вероятность, что второй не уложится в 100ms) = 0.01 * 0.1 = 0.001
Таким образом, вероятность ответить дольше 200ms стала 0.001, а значит p99.9 = 200ms
А вероятность, что придется отправлять второй запрос = вероятность, что первый отвечал >100ms = 0.1. То есть в 10% случаев придется слать два запроса
---
Таким образом, увеличив нагрузку на систему на 10% мы смогли перейти от p99 = 200ms до p99.9 = 200ms, то есть уменьшили "вероятность таймаутнуться" в 10 раз
🔥76👍16🤔11✍3🤯3💅1 1
⚡️Как жить с нестабильной интеграцией
Иногда возникают ситуации, что ваш сервис ходит в какую-то внешнюю (относительно команды или вообще компании) систему, которая работает нестабильно. Из-за этого может страдать стабильность и вашего сервиса
Представим, что внешнюю систему чинить не сильно торопятся, а альтернатив у вас нет. Что можно сделать в такой ситуации в рамках вашего сервиса, чтобы повысить стабильность?
1. Если ваш сервис только забирает данные из внешней системы
В таком случае можно сделать локальную копию данных, которая вам нужна от внешней системы. Например, сделать кеш. Кеш можно устроить по разному в зависимости от контекста:
- Либо раз в какое-то время подгружать полный слепок данных и сохранять к себе
- Либо кешировать результаты отдельных запросов с каким-то TTL
- Либо обновлять кеш по эвентам от внешней системы (если она их отправляет)
2. Если ваш сервис только отправляет данные во внешнюю систему
Пример: отправка каких-нибудь нотификашек. В таком случае можно уменьшить связность между вашим сервисом и внешней системой, заменив синхронную интеграцию на асинхронную. Например, добавив очередь сообщений. Это позволит в случае недоступностей внешней системы доставить сообщение "когда-нибудь потом"
3. Если ваш сервис отправляет данные во внешнюю систему и ожидает чего-то в ответ
Пример: создание сущности во внешней системе и получение id в ответе
Пишите в комментах, какие варианты повышения стабильности здесь видите
Иногда возникают ситуации, что ваш сервис ходит в какую-то внешнюю (относительно команды или вообще компании) систему, которая работает нестабильно. Из-за этого может страдать стабильность и вашего сервиса
Представим, что внешнюю систему чинить не сильно торопятся, а альтернатив у вас нет. Что можно сделать в такой ситуации в рамках вашего сервиса, чтобы повысить стабильность?
1. Если ваш сервис только забирает данные из внешней системы
В таком случае можно сделать локальную копию данных, которая вам нужна от внешней системы. Например, сделать кеш. Кеш можно устроить по разному в зависимости от контекста:
- Либо раз в какое-то время подгружать полный слепок данных и сохранять к себе
- Либо кешировать результаты отдельных запросов с каким-то TTL
- Либо обновлять кеш по эвентам от внешней системы (если она их отправляет)
2. Если ваш сервис только отправляет данные во внешнюю систему
Пример: отправка каких-нибудь нотификашек. В таком случае можно уменьшить связность между вашим сервисом и внешней системой, заменив синхронную интеграцию на асинхронную. Например, добавив очередь сообщений. Это позволит в случае недоступностей внешней системы доставить сообщение "когда-нибудь потом"
3. Если ваш сервис отправляет данные во внешнюю систему и ожидает чего-то в ответ
Пример: создание сущности во внешней системе и получение id в ответе
Пишите в комментах, какие варианты повышения стабильности здесь видите
👍28🔥9💅3 2
⚡️Загадка про постгрес
1. Есть табличка с колонками a, b
2. Есть 10 млн записей, примерно у половины записей в колонке a лежит null
3. Есть btree индекс на (a, b)
Угадайте план запроса
Верно,секскан + сортировка
---
Как выяснилось (после инцидента), постгрес не умеет в фильтрацию по null/is null + сортировку одновременно. Если только фильтрация — будет ок. Поэтому для такого кейса нужно создавать отдельный частичный индекс
proof: https://dbfiddle.uk/Bgob-orb
1. Есть табличка с колонками a, b
create table tbl
(
a bigint,
b timestamp
);
2. Есть 10 млн записей, примерно у половины записей в колонке a лежит null
3. Есть btree индекс на (a, b)
Угадайте план запроса
select *
from tbl
where a is null
order by b
limit 1;
Верно,
---
Как выяснилось (после инцидента), постгрес не умеет в фильтрацию по null/is null + сортировку одновременно. Если только фильтрация — будет ок. Поэтому для такого кейса нужно создавать отдельный частичный индекс
on tbl (b) where a is null;proof: https://dbfiddle.uk/Bgob-orb
🤯52👍34🤔7💅3 3
⚡️Какие-то мысли про Redis Streams
Недавно возникла необходимость в простенькой очереди, чтобы на запрос быстро отвечать двухсоткой и далее асинхронно обрабатывать. Решили поэкспериментировать с redis streams, тк тащить что-то кафкоподобное казалось оверкиллом, а редис можно поднять за полчаса, и он устраивал нас по гарантиям/нагрузке/etc
Как оно работает (см картинку):
1. Сообщение добавляется в стрим через XADD
2. Читаем сообщение из стрима и "назначаем" на определенного консюмера из определенной группы консюмеров через XGROUPREAD
3. После того как сообщение прочитано оно попадает в PEL (Pending Entries List) — список прочитанных, но не обработанных сообщений консюмера
4. Консюмер делает XACK на сообщение, оно помечается обработанным, т.е. удаляется из PEL
Что уже можно сказать — стрим больше похож не на топик в кафке, а на конкретную партицию. При том в redis stream из этой партиции могут конкурентно брать сообщения несколько консюмеров, т.е. порядок не обязательно будет соблюдаться
Общее правило такое:
- 1 stream, 1 consumer: соблюдается порядок
- 1 stream, n consumers: не соблюдается порядок, конкурентная обработка
- n stream, n consumers: "масштабированный" первый вариант. Для каждого из n стримов есть консюмер, который в одиночку из него вычитывает. Т.е. некоторые изобретение партицированного топика на коленке
Обработка ошибок:
После чтение консюмер может по разным причинам умереть и недообработать сообщение. Эта проблема решается поллингом PEL с помощью XPENDING и переназначением зависших сообщений на другого консюмера через XCLAIM. Что мне показалось странным с точки зрения дизайна — нельзя просто вернуть сообщение в "общую очередь", нужно переназначить его именно на конкретного консюмера
---
Overall кажется, что redis streams хорошо подходят для ситуаций, где нужна
- небольшая и быстрая in memory очередь
- сообщения редко могут теряться (специфика редиса, см пост)
- порядок обработки либо не важен, либо вы готовы заморочиться и вручную реализовать партицирование
Недавно возникла необходимость в простенькой очереди, чтобы на запрос быстро отвечать двухсоткой и далее асинхронно обрабатывать. Решили поэкспериментировать с redis streams, тк тащить что-то кафкоподобное казалось оверкиллом, а редис можно поднять за полчаса, и он устраивал нас по гарантиям/нагрузке/etc
Как оно работает (см картинку):
1. Сообщение добавляется в стрим через XADD
2. Читаем сообщение из стрима и "назначаем" на определенного консюмера из определенной группы консюмеров через XGROUPREAD
3. После того как сообщение прочитано оно попадает в PEL (Pending Entries List) — список прочитанных, но не обработанных сообщений консюмера
4. Консюмер делает XACK на сообщение, оно помечается обработанным, т.е. удаляется из PEL
Что уже можно сказать — стрим больше похож не на топик в кафке, а на конкретную партицию. При том в redis stream из этой партиции могут конкурентно брать сообщения несколько консюмеров, т.е. порядок не обязательно будет соблюдаться
Общее правило такое:
- 1 stream, 1 consumer: соблюдается порядок
- 1 stream, n consumers: не соблюдается порядок, конкурентная обработка
- n stream, n consumers: "масштабированный" первый вариант. Для каждого из n стримов есть консюмер, который в одиночку из него вычитывает. Т.е. некоторые изобретение партицированного топика на коленке
Обработка ошибок:
После чтение консюмер может по разным причинам умереть и недообработать сообщение. Эта проблема решается поллингом PEL с помощью XPENDING и переназначением зависших сообщений на другого консюмера через XCLAIM. Что мне показалось странным с точки зрения дизайна — нельзя просто вернуть сообщение в "общую очередь", нужно переназначить его именно на конкретного консюмера
---
Overall кажется, что redis streams хорошо подходят для ситуаций, где нужна
- небольшая и быстрая in memory очередь
- сообщения редко могут теряться (специфика редиса, см пост)
- порядок обработки либо не важен, либо вы готовы заморочиться и вручную реализовать партицирование
👍22✍3💅1 1
⚡️Про что подумать, когда пишете свой outbox
Вместо того чтобы делать
Мы сохраняем сообщение в бд в рамках транзакции
И фоновый воркер допушит это сообщение до брокера
---
Что мы обычно хотим от аутбокса:
- Низкое latency проброса от бд до брокера
- Сохранение порядка сообщений для бизнес-сущностей
- Возможность масштабировать пропускную способность
Оговорка: можно взять готовый CDC типа Debezium, который читает лог базы. Однако надо помнить, что это дополнительный компонент в вашей инфре, который надо поддерживать + дополнительная точка отказа + нужна доп экспертиза "а че там может сломаться". Иногда это ок, иногда не ок
---
Далее посмотрим, как можно рассуждать на пути к целевому решению, если вы делаете аутбокс сами
Идея 1: несколько воркеров поллят записи через select for update skip locked
Самый простой вариант очереди. Масштабируемость — есть, просто увеличиваем число воркеров. Низкое latency — есть, достаточно увеличить частоту поллинга. Сохранение порядка — очевидно, нет
Идея 2: однотредово забираем пачку записей с order by
Поскольку у предыдущего решения нет упорядоченности, давайте ее добавим. Теперь нарушение порядка невозможно, потому что записи достает только один тред. Масштабируемость и низкое latency — спорно. При рейте в 10к+ сообщений в секунду один тредик может перестать справляться
Идея 3: попробуем распараллелить предыдущий вариант
Давайте при сохранении сообщения в базу сразу указывать "виртуальную партицию" так, чтобы два эвента по одной бизнес-сущности были в одной партиции (entity_id % partition_number). Так мы сможем запустить предыдущую однотредовую логику для каждой партиции, чем добьемся масштабируемости
Но: представим, что мы захотим увеличить число виртуальных партиций с 5 до 7. Сначала сущность с id = 123 попадала в партицию 123 % 5 = 3, после увеличения будет попадать в 123 % 7 = 4. То есть в момент масштабирования сообщения по одной бизнес-сущности могут начать обрабатываться параллельно в партициях 3 и 4, из-за чего нарушится порядок
Идея 4: добавляем маппинг entity_id <-> partition_id
Берем идею 3 и добавляем явный маппинг между id бизнес сущности и id партиции. Это позволит бизнес-сущность "прилеплять" к определенной партиции. И в момент масштабирования числа партиций у нас ничего не поломается, потому что у нас было зафиксировано, что сообщения по entity_id = 123 попадают в партицию 3
Здесь также надо предусмотреть удаление маппингов по ttl. Скажем, если неделю по одной бизнес-сущности не поступало сообщений, то консюмеры предыдущие сообщения скорее всего обработали, и маппинг можно безопасно дропнуть
В итоге такое решение удовлетворяет всем требованиям
- Низкое latency
- Сохранение порядка
- Возможность безопасно масштабироваться
---
Вывод: написать хороший аутбокс — задача явно не простая. При этом такие изощрения точно нужны не всегда. Зачастую либо нагрузка низкая (<100 сообщений в секунду), либо порядок не требуется. Эти вводные могут существенно упростить итоговое решение
Вместо того чтобы делать
dbUpdate()
sendToBroker()
Мы сохраняем сообщение в бд в рамках транзакции
tx {
dbUpdate()
dbSaveMessage()
}И фоновый воркер допушит это сообщение до брокера
---
Что мы обычно хотим от аутбокса:
- Низкое latency проброса от бд до брокера
- Сохранение порядка сообщений для бизнес-сущностей
- Возможность масштабировать пропускную способность
Оговорка: можно взять готовый CDC типа Debezium, который читает лог базы. Однако надо помнить, что это дополнительный компонент в вашей инфре, который надо поддерживать + дополнительная точка отказа + нужна доп экспертиза "а че там может сломаться". Иногда это ок, иногда не ок
---
Далее посмотрим, как можно рассуждать на пути к целевому решению, если вы делаете аутбокс сами
Идея 1: несколько воркеров поллят записи через select for update skip locked
Самый простой вариант очереди. Масштабируемость — есть, просто увеличиваем число воркеров. Низкое latency — есть, достаточно увеличить частоту поллинга. Сохранение порядка — очевидно, нет
Идея 2: однотредово забираем пачку записей с order by
Поскольку у предыдущего решения нет упорядоченности, давайте ее добавим. Теперь нарушение порядка невозможно, потому что записи достает только один тред. Масштабируемость и низкое latency — спорно. При рейте в 10к+ сообщений в секунду один тредик может перестать справляться
Идея 3: попробуем распараллелить предыдущий вариант
Давайте при сохранении сообщения в базу сразу указывать "виртуальную партицию" так, чтобы два эвента по одной бизнес-сущности были в одной партиции (entity_id % partition_number). Так мы сможем запустить предыдущую однотредовую логику для каждой партиции, чем добьемся масштабируемости
Но: представим, что мы захотим увеличить число виртуальных партиций с 5 до 7. Сначала сущность с id = 123 попадала в партицию 123 % 5 = 3, после увеличения будет попадать в 123 % 7 = 4. То есть в момент масштабирования сообщения по одной бизнес-сущности могут начать обрабатываться параллельно в партициях 3 и 4, из-за чего нарушится порядок
Идея 4: добавляем маппинг entity_id <-> partition_id
Берем идею 3 и добавляем явный маппинг между id бизнес сущности и id партиции. Это позволит бизнес-сущность "прилеплять" к определенной партиции. И в момент масштабирования числа партиций у нас ничего не поломается, потому что у нас было зафиксировано, что сообщения по entity_id = 123 попадают в партицию 3
Здесь также надо предусмотреть удаление маппингов по ttl. Скажем, если неделю по одной бизнес-сущности не поступало сообщений, то консюмеры предыдущие сообщения скорее всего обработали, и маппинг можно безопасно дропнуть
В итоге такое решение удовлетворяет всем требованиям
- Низкое latency
- Сохранение порядка
- Возможность безопасно масштабироваться
---
Вывод: написать хороший аутбокс — задача явно не простая. При этом такие изощрения точно нужны не всегда. Зачастую либо нагрузка низкая (<100 сообщений в секунду), либо порядок не требуется. Эти вводные могут существенно упростить итоговое решение
👍46🔥14💅2 2
⚡️Ускорение индексации денормализованных документов
В посте про подходы к поиску иерархичных данных был небольшой рассказ про денормализацию — это когда есть родительский документ { ticket } и есть дочерние { article_1 }, { article_2 }, { article_3 }. И далее мы уплощаем эту структуру в три документа, где child-ы обогащены данными от родителя
Псевдокод:
Такой подход позволяет добиться хорошего перфоманса при поиске (поскольку все необходимые данные находятся в рамках одного документа), но теперь нужно на каждое изменение родительского документа переиндексировать всех его детей
Какие есть способы ускорить такую индексацию в elasticsearch:
0. Наивный неэффективный способ
Получили эвент обновления ticket => запрашиваем все артиклы => строим n документов для обновления артиклов => делаем n запросов в эластик
Если у тикета 500 артиклов, то к примеру при изменении статуса тикета, нам нужно будет сделать 500 запросов на переиндексацию — звучит крайне неэффективно
Псевдокод:
1. Использование bulk update + partial update
Нам приходится делать кучу запросов, мы это можем решить с помощью использования bulk update, передав в одном запросе список того, что хотим переиндексировать
Вторая проблема — что приходится передавать полный документ, хотя у него изменился маленький кусочек. Эластик поддерживает partial update, и поэтому можно передать только тот кусок документа, который изменился
Псевдокод:
Важное НО: эластик делает shallow partial update, т.е. вложенные объекты обновляются целиком сразу, а не частично. К примеру, если бы мы захотели обновить вложенное поле
2. Использование update_by_query + noscript
Нивелируем проблемы предыдущего решения, а именно: shallow апдейты и то, что мы можем заранее не знать id-шники всех дочерних документов
Для этой цели есть update_by_query, который позволяет обновить документы, которые соответствуют некоторому условию. И обновить их с помощью скрипта
Псевдокод:
Такой подход идеально подходит для переиндексации дочерних документов, и позволяет делать максимально компактные запросы и обеспечивать гибкость с помощью кастомных скриптов (по дефолту на внутреннем языке эластика — painless)
В посте про подходы к поиску иерархичных данных был небольшой рассказ про денормализацию — это когда есть родительский документ { ticket } и есть дочерние { article_1 }, { article_2 }, { article_3 }. И далее мы уплощаем эту структуру в три документа, где child-ы обогащены данными от родителя
Псевдокод:
{ ticket, article_1 },
{ ticket, article_2 },
{ ticket, article_3 }Такой подход позволяет добиться хорошего перфоманса при поиске (поскольку все необходимые данные находятся в рамках одного документа), но теперь нужно на каждое изменение родительского документа переиндексировать всех его детей
Какие есть способы ускорить такую индексацию в elasticsearch:
0. Наивный неэффективный способ
Получили эвент обновления ticket => запрашиваем все артиклы => строим n документов для обновления артиклов => делаем n запросов в эластик
Если у тикета 500 артиклов, то к примеру при изменении статуса тикета, нам нужно будет сделать 500 запросов на переиндексацию — звучит крайне неэффективно
Псевдокод:
query 1: { ticket: { all fields }, article { all fields }
query 2: { ticket: { all fields }, article { all fields }
query 3: { ticket: { all fields }, article { all fields }
...1. Использование bulk update + partial update
Нам приходится делать кучу запросов, мы это можем решить с помощью использования bulk update, передав в одном запросе список того, что хотим переиндексировать
Вторая проблема — что приходится передавать полный документ, хотя у него изменился маленький кусочек. Эластик поддерживает partial update, и поэтому можно передать только тот кусок документа, который изменился
Псевдокод:
query: [
{ article_id: 1, ticket: { changed fields } }
{ article_id: 2, ticket: { changed fields } }
{ article_id: 3, ticket: { changed fields } }
...
]
Важное НО: эластик делает shallow partial update, т.е. вложенные объекты обновляются целиком сразу, а не частично. К примеру, если бы мы захотели обновить вложенное поле
ticket.custom_fields.some_field, то нам мы пришлось полностью передать объект ticket. В противном случае мы бы потеряли данные2. Использование update_by_query + noscript
Нивелируем проблемы предыдущего решения, а именно: shallow апдейты и то, что мы можем заранее не знать id-шники всех дочерних документов
Для этой цели есть update_by_query, который позволяет обновить документы, которые соответствуют некоторому условию. И обновить их с помощью скрипта
Псевдокод:
query: {
condition: ticket_id = 123,
noscript: ticket.custom_fields.some_field = "abc"
}Такой подход идеально подходит для переиндексации дочерних документов, и позволяет делать максимально компактные запросы и обеспечивать гибкость с помощью кастомных скриптов (по дефолту на внутреннем языке эластика — painless)
Telegram
Microservices Thoughts
⚡Поиск по иерархичным данным в elasticsearch (и не только)
Представьте, что у вас есть две сущности, которые связаны внешними ключами
Например, в нашем случае такими сущностями выступают
Ticket — обращение пользователя в поддержку: содержит кучу разной…
Представьте, что у вас есть две сущности, которые связаны внешними ключами
Например, в нашем случае такими сущностями выступают
Ticket — обращение пользователя в поддержку: содержит кучу разной…
👍17
⚡️Expand and contract (zero downtime migrations)
Когда вам нужно мигрировать приложение на новую схему данных, у вас есть два пути
Первый вариант — за один релиз задеплоить новый код и миграцию схемы и самих данных. Если у вас данных не очень много и/или вы можете позволить себе даунтайм, это прекрасно
Второй вариант — страдания
Для таких миграций без даунтайма есть весьма понятный подход, называемый expand and contract
Посмотрим на примере:
Есть — одна колонка в бд содержащая и имя, и фамилию пользака user = "Walter Black"
Хочется — две отдельные колонки name = "Walter", surname = "Black"
1. Добавляем новые нуллабельные колонки
Отдельный релиз — накатываем миграцию схемы данных
2. Начинаем писать и в старую, и в новую схему
Отдельный релиз — при апдейтах/инсертах user = "Walter Black" также проставляем name = "Walter", surname = "Black"
3. Мигрируем старые данные
Отдельный релиз/запуск скрипта — пишем скрипт, который в старых записях проставит значения новым колонкам. После завершения миграции хорошо бы чекнуть, что данные в двух схемах действительно эквивалиентны друг другу. После этого этапа у нас есть все данные одновременно в двух схемах
4. Начинаем читать из новой схемы
Отдельный релиз — в коде приложения начинаем читать данные не из старой колонки, а из новых
5. Выпиливаем запись в старую схему
Отдельный релиз — перестаем писать в старую колонку
6. Выпиливаем старую схему
Отдельный релиз — миграция, которая дропает старую колонку user
---
Итого: за 5-6 релизов получаем безопасную миграцию схемы данных. Важно, что здесь при неудачном релизе мы всегда можем безопасно его откатить. Конечно, такие жесткие гарантии требуются не всегда, и зачастую происходит слияние каких-то шагов, если допустим короткий даунтайм
Шаги в виде картинок в комментах
Когда вам нужно мигрировать приложение на новую схему данных, у вас есть два пути
Первый вариант — за один релиз задеплоить новый код и миграцию схемы и самих данных. Если у вас данных не очень много и/или вы можете позволить себе даунтайм, это прекрасно
Второй вариант — страдания
Для таких миграций без даунтайма есть весьма понятный подход, называемый expand and contract
Посмотрим на примере:
Есть — одна колонка в бд содержащая и имя, и фамилию пользака user = "Walter Black"
Хочется — две отдельные колонки name = "Walter", surname = "Black"
1. Добавляем новые нуллабельные колонки
Отдельный релиз — накатываем миграцию схемы данных
2. Начинаем писать и в старую, и в новую схему
Отдельный релиз — при апдейтах/инсертах user = "Walter Black" также проставляем name = "Walter", surname = "Black"
3. Мигрируем старые данные
Отдельный релиз/запуск скрипта — пишем скрипт, который в старых записях проставит значения новым колонкам. После завершения миграции хорошо бы чекнуть, что данные в двух схемах действительно эквивалиентны друг другу. После этого этапа у нас есть все данные одновременно в двух схемах
4. Начинаем читать из новой схемы
Отдельный релиз — в коде приложения начинаем читать данные не из старой колонки, а из новых
5. Выпиливаем запись в старую схему
Отдельный релиз — перестаем писать в старую колонку
6. Выпиливаем старую схему
Отдельный релиз — миграция, которая дропает старую колонку user
---
Итого: за 5-6 релизов получаем безопасную миграцию схемы данных. Важно, что здесь при неудачном релизе мы всегда можем безопасно его откатить. Конечно, такие жесткие гарантии требуются не всегда, и зачастую происходит слияние каких-то шагов, если допустим короткий даунтайм
Шаги в виде картинок в комментах
🔥41👍27💅2 1
⚡️Cache stampede
Представим, код метода getData() выглядит так
Что произойдет если два потока почти одновременно не найдут данные в кеше? Пойдут загружать из БД
А если 100 потоков?
Понять масштабы можно на таком примере:
- В кеш может поступать до 300 запросов в секунду
- Время загрузки из бд — 2 секунды
И получается, если ключ в кеше протухнет, то за первые две секунды в базу попрутся 600 параллельных тяжелых запросов. Учитывая, что из-за этих запросов БД может деградировать, запросы могут начать выполняться еще дольше
---
Что с этим делать?
1. Локи
В примере выше хорошо бы зашел double-checked locking
Можно конечно сразу лочить, но тогда для подавляющего большинства запросов добавится лишний оверхед. А в случае дабл чека будет лочиться только когда кеш протух
2. Прогрев кеша / фоновые апдейты
Если уверены, что значение скорее всего понадобится, то просто заранее его туда положите и в фоне обновляйте. Если будет всплеск нагрузки, в кеше уже будет все что нужно
3. Вероятностные рефреши
Есть еще техника, когда кеш обновляется с некоторой вероятностью, даже если значение еще не протухло. Это может быть полезно, если полностью прогреть кеш невозможно, но вероятность cache stampede хочется снизить
---
Пишите в комментах, что из этого используете)
Представим, код метода getData() выглядит так
def getData():
dataFromCache = getFromCache()
if dataFromCache:
return dataFromCache
dataFromDb = getFromDb()
updateCache(dataFromDb)
return dataFromDb
Что произойдет если два потока почти одновременно не найдут данные в кеше? Пойдут загружать из БД
А если 100 потоков?
Понять масштабы можно на таком примере:
- В кеш может поступать до 300 запросов в секунду
- Время загрузки из бд — 2 секунды
И получается, если ключ в кеше протухнет, то за первые две секунды в базу попрутся 600 параллельных тяжелых запросов. Учитывая, что из-за этих запросов БД может деградировать, запросы могут начать выполняться еще дольше
---
Что с этим делать?
1. Локи
В примере выше хорошо бы зашел double-checked locking
def getData():
dataFromCache = getFromCache()
if dataFromCache:
return dataFromCache
withLock {
dataFromCache = getFromCache()
if dataFromCache:
return dataFromCache
dataFromDb = getFromDb()
updateCache(dataFromDb)
return dataFromDb
}
Можно конечно сразу лочить, но тогда для подавляющего большинства запросов добавится лишний оверхед. А в случае дабл чека будет лочиться только когда кеш протух
2. Прогрев кеша / фоновые апдейты
Если уверены, что значение скорее всего понадобится, то просто заранее его туда положите и в фоне обновляйте. Если будет всплеск нагрузки, в кеше уже будет все что нужно
3. Вероятностные рефреши
Есть еще техника, когда кеш обновляется с некоторой вероятностью, даже если значение еще не протухло. Это может быть полезно, если полностью прогреть кеш невозможно, но вероятность cache stampede хочется снизить
---
Пишите в комментах, что из этого используете)
👍43🔥7✍1🤔1
System Design & Highload (Alexey Rybak)
Если вы ищете практический канал про Highload, БД, System Design, то вам точно стоит заглянуть в канал к Алексею — ex-CTO Badoo, а сейчас основателю R&D-платформы devhands.io и сооснователю софта для автоматизации перфоманс ревью teamwork360.io
Он делится на своём канале уникальными мыслями и опытом работы с реальными высоконагруженными системами, сравнивает перфоманс БД, описывает некоторые теоретические аспекты
Список постов которые лично мне понравились у него:
- Простое объяснение CAP теоремы
- Про блокировки в СУБД
- Почему важно замерять P99
- Один день из жизни CTO
- 1.000.000 RPS на PostgreSQL и MySQL
- Нужен ли BFF?
Ставьте 👍, если подписались
Если вы ищете практический канал про Highload, БД, System Design, то вам точно стоит заглянуть в канал к Алексею — ex-CTO Badoo, а сейчас основателю R&D-платформы devhands.io и сооснователю софта для автоматизации перфоманс ревью teamwork360.io
Он делится на своём канале уникальными мыслями и опытом работы с реальными высоконагруженными системами, сравнивает перфоманс БД, описывает некоторые теоретические аспекты
Список постов которые лично мне понравились у него:
- Простое объяснение CAP теоремы
- Про блокировки в СУБД
- Почему важно замерять P99
- Один день из жизни CTO
- 1.000.000 RPS на PostgreSQL и MySQL
- Нужен ли BFF?
Ставьте 👍, если подписались
Telegram
System Design & Highload (Alexey Rybak)
Архитектура больших проектов и управление продуктово-инженерными организациями; статьи, выступления по теме управление и разработка больших IT-проектов. Https://DevHands.io - хайлоад-прокачка бекендеров. ЛС: @alexeyrybak.
👍31🤔3
⚡️Почему из-за долгих транзакций могут тормозить другие запросы
Представьте, что у вас есть табличка с колонкой created_ts. И на эту таблицу есть некоторый TTL — фоновый воркер в порядке created_ts удаляет записи, которые старше 7 дней
Посмотрим, как это заафектит производительность запроса
1. Сначала все хорошо, запрос идет по индексу на created_ts, берет первую запись
2. Далее начинается долгая транзакция с участием tbl
3. Далее блокируется автовакуум tbl
4. Параллельно с этим работает фоновая удалялка. Но поскольку автовакуум заблокирован, удаленные записи просто продолжают висеть как dead tuples
5. Спустя какое-то время набирается несколько тысяч dead tuple-ов
И далее интересный момент — поскольку удалялка удаляет записи в порядке created_ts, то в начале индекса будет огромная пачка ссылок, которые ведут на мертвые кортежи
И чтобы выполнить запрос
Нам нужно будет сначала пройти все эти ссылки на мертвые кортежи, и только потом взять первую активную запись. Если таких кортежей наберется под миллион — будет очень больно (основано на реальных событиях)
Представьте, что у вас есть табличка с колонкой created_ts. И на эту таблицу есть некоторый TTL — фоновый воркер в порядке created_ts удаляет записи, которые старше 7 дней
Посмотрим, как это заафектит производительность запроса
select * from tbl
order by created_ts
limit 1
1. Сначала все хорошо, запрос идет по индексу на created_ts, берет первую запись
2. Далее начинается долгая транзакция с участием tbl
3. Далее блокируется автовакуум tbl
4. Параллельно с этим работает фоновая удалялка. Но поскольку автовакуум заблокирован, удаленные записи просто продолжают висеть как dead tuples
5. Спустя какое-то время набирается несколько тысяч dead tuple-ов
И далее интересный момент — поскольку удалялка удаляет записи в порядке created_ts, то в начале индекса будет огромная пачка ссылок, которые ведут на мертвые кортежи
И чтобы выполнить запрос
select * from tbl
order by created_ts
limit 1
Нам нужно будет сначала пройти все эти ссылки на мертвые кортежи, и только потом взять первую активную запись. Если таких кортежей наберется под миллион — будет очень больно (основано на реальных событиях)
👍50🔥13💅3🤔2
⚡️Обработка ошибок в консюмерах
Когда консюмер топика пытается обработать очередную пачку сообщений, но получает ошибку — каждый раз возникает вопрос "а че делать?"
Предлагаю такой фреймворк
Отвечаем на два вопроса:
- Критична ли потеря сообщений?
- Критичен ли порядок обработки сообщений?
Сценарий 1: Потеря сообщений НЕ критична, порядок НЕ критичен
Потери не критичны => можем вообще не ретраить
Порядок не критичен => можем обрабатывать сообщения параллельно, можем ретраить в любом удобном формате
В общем, самый простой случай — делайте что хотите)
Сценарий 2: Потеря сообщений НЕ критична, порядок критичен
Потеря сообщений не критична => можем вообще не ретраить
Порядок критичен => обрабатываем все сообщение либо одним батчем, либо последовательно в один поток. Можем ретраить только синхронно, ибо ретраи в фоне могут нарушить порядок
Тоже простой случай — обрабатываем сообщения последовательно (или батчем). Опиционально можем поретраить, но синхронно
Сценарий 3: Потеря сообщений критична, порядок НЕ критичен
Потеря сообщений критична => обязательно нужны ретраи
Порядок не критичен => можем обрабатывать сообщения параллельно, можем ретраить в любом удобном формате
Здесь на что у вас хватит фантазии. Варианты (можно комбинировать):
- Синхронные ретраи
- Retry-topics + DLQ — в случае ошибки в основном топике переписываем сообщение в retry-topic-1. Из него с некоторой задержкой пытаемся обработать, не получилось — пишем в retry-topic-2 и т.д. Если все ретраи не увенчались успехом — пишем в dead letter queue и поджигаем алерт
- Очередь на БД — тоже вполне удобная вещь. В случае ошибки пишем сообщение в очередь на БД и спустя некоторое время обрабатываем. Из плюсов — не надо плодить топики + легко настроить кастомные задержки
Сценарий 4: Потеря сообщений критична, порядок критичен
Потеря сообщений критична => обязательно нужны ретраи
Порядок критичен => обрабатываем все сообщение либо одним батчем, либо последовательно в один поток. Можем ретраить только синхронно, ибо ретраи в фоне могут нарушить порядок
На самом деле это тоже простой случай, потому что ограничения нас вгоняют в жесткие рамки. Здесь возможен единственный вариант — обрабатываем сообщения последовательно (или батчем). Синхронно ретраим. Все ретраи упали — останавливаем вычитку, поджигаем алерт, идем вручную разбираться
Ставьте 👍, если было полезно
Когда консюмер топика пытается обработать очередную пачку сообщений, но получает ошибку — каждый раз возникает вопрос "а че делать?"
Предлагаю такой фреймворк
Отвечаем на два вопроса:
- Критична ли потеря сообщений?
- Критичен ли порядок обработки сообщений?
Сценарий 1: Потеря сообщений НЕ критична, порядок НЕ критичен
Потери не критичны => можем вообще не ретраить
Порядок не критичен => можем обрабатывать сообщения параллельно, можем ретраить в любом удобном формате
В общем, самый простой случай — делайте что хотите)
Сценарий 2: Потеря сообщений НЕ критична, порядок критичен
Потеря сообщений не критична => можем вообще не ретраить
Порядок критичен => обрабатываем все сообщение либо одним батчем, либо последовательно в один поток. Можем ретраить только синхронно, ибо ретраи в фоне могут нарушить порядок
Тоже простой случай — обрабатываем сообщения последовательно (или батчем). Опиционально можем поретраить, но синхронно
Сценарий 3: Потеря сообщений критична, порядок НЕ критичен
Потеря сообщений критична => обязательно нужны ретраи
Порядок не критичен => можем обрабатывать сообщения параллельно, можем ретраить в любом удобном формате
Здесь на что у вас хватит фантазии. Варианты (можно комбинировать):
- Синхронные ретраи
- Retry-topics + DLQ — в случае ошибки в основном топике переписываем сообщение в retry-topic-1. Из него с некоторой задержкой пытаемся обработать, не получилось — пишем в retry-topic-2 и т.д. Если все ретраи не увенчались успехом — пишем в dead letter queue и поджигаем алерт
- Очередь на БД — тоже вполне удобная вещь. В случае ошибки пишем сообщение в очередь на БД и спустя некоторое время обрабатываем. Из плюсов — не надо плодить топики + легко настроить кастомные задержки
Сценарий 4: Потеря сообщений критична, порядок критичен
Потеря сообщений критична => обязательно нужны ретраи
Порядок критичен => обрабатываем все сообщение либо одним батчем, либо последовательно в один поток. Можем ретраить только синхронно, ибо ретраи в фоне могут нарушить порядок
На самом деле это тоже простой случай, потому что ограничения нас вгоняют в жесткие рамки. Здесь возможен единственный вариант — обрабатываем сообщения последовательно (или батчем). Синхронно ретраим. Все ретраи упали — останавливаем вычитку, поджигаем алерт, идем вручную разбираться
Ставьте 👍, если было полезно
👍118🤔3
⚡️Шардирование без решардирования
Одна из основных проблем шардирования — решардинг, то есть когда при добавлении нового шарда нужно перераскидать данные между шардами. Почему так случается?
Представьте есть 3 шарда => shard_count = 3
Шард выбирается как entity_id % shard_count
1. Для entity_id = 7 получаем шард 7 % 3 = 1
2. Добавляем новый шард => shard_count = 4
3. Для entity_id = 7 получаем шард 7 % 4 = 3
Поэтому нам нужно будет перераспределить сущности, чтобы они соответствовали корректным шардам
Один из способов уменьшить количество "перераспределений" — consistent hashing. Но можно ли вообще обойтись без решардирования?
---
Функция выбора шарда определяется как
f: (shard_key, shard_count) -> shard_number
Которая по ключу шарда и текущему кол-ву шардов в системе отдает чиселку — номер шарда, где должна лежать сущность
Чтобы не было необходимости в решардинге, что мы хотим от этой функции? Чтобы при добавлении нового шарда значение не изменялось, то есть для любого shard_key и shard_count
f(shard_key, shard_count) = f(shard_key, shard_count + 1)
Что в сущности означает, что f(shard_key, shard_count) = f(shard_key, shard_count + 1) = f(shard_key, shard_count + 2) = ...
То есть как будто функция вообще никак не зависит от shard_count, что странно
---
Возможно ли такое, если функция чистая (т.е. не делает сайд-эффектов и использует только переданные аргументы shard_key и shard_count)?
Возьмем f(shard_key, 1), который всегда константно равен 0 (потому что shard_count = 1)
Но отсюда следует, что 0 = f(shard_key, 1) = f(shard_key, 2) = f(shard_key, 3) = ... То есть такое возможно, если функция всегда будет отдавать 0, что очевидно нам не подходит
---
Значит функция должна делать какие-то сайд-эффекты
И самый простой сайд-эффект, который позволяет этого достичь — это getOrPut в хранилище маппингов entity_id <=> shard_number
Пусть снова shard_count = 3
1. Пусть entity_id = 7 => shard_number = mappings.getOrPut(7, 7 % 3) = 1
2. Добавляем новый шард => shard_count = 4
3. Для entity_id = 7 получаем шард mappings.getOrPut(7, 7 % 4) = 1 (а не 3, потому что в mappings уже есть запись с таким ключом)
Таким образом это позволяет "прилипать" сущности к определенному шарду, что убирает необходимость решардинга при добавлении новых шардов
p.s.: в качестве оптимизации, чтобы не хранить маппинги для всех entity_id, можно использовать идею с "виртуальными бакетами"
Следующая часть
Ставьте 👍, если нужен пост про виртуальные бакеты в шардировании
Одна из основных проблем шардирования — решардинг, то есть когда при добавлении нового шарда нужно перераскидать данные между шардами. Почему так случается?
Представьте есть 3 шарда => shard_count = 3
Шард выбирается как entity_id % shard_count
1. Для entity_id = 7 получаем шард 7 % 3 = 1
2. Добавляем новый шард => shard_count = 4
3. Для entity_id = 7 получаем шард 7 % 4 = 3
Поэтому нам нужно будет перераспределить сущности, чтобы они соответствовали корректным шардам
Один из способов уменьшить количество "перераспределений" — consistent hashing. Но можно ли вообще обойтись без решардирования?
---
Функция выбора шарда определяется как
f: (shard_key, shard_count) -> shard_number
Которая по ключу шарда и текущему кол-ву шардов в системе отдает чиселку — номер шарда, где должна лежать сущность
Чтобы не было необходимости в решардинге, что мы хотим от этой функции? Чтобы при добавлении нового шарда значение не изменялось, то есть для любого shard_key и shard_count
f(shard_key, shard_count) = f(shard_key, shard_count + 1)
Что в сущности означает, что f(shard_key, shard_count) = f(shard_key, shard_count + 1) = f(shard_key, shard_count + 2) = ...
То есть как будто функция вообще никак не зависит от shard_count, что странно
---
Возможно ли такое, если функция чистая (т.е. не делает сайд-эффектов и использует только переданные аргументы shard_key и shard_count)?
Возьмем f(shard_key, 1), который всегда константно равен 0 (потому что shard_count = 1)
Но отсюда следует, что 0 = f(shard_key, 1) = f(shard_key, 2) = f(shard_key, 3) = ... То есть такое возможно, если функция всегда будет отдавать 0, что очевидно нам не подходит
---
Значит функция должна делать какие-то сайд-эффекты
И самый простой сайд-эффект, который позволяет этого достичь — это getOrPut в хранилище маппингов entity_id <=> shard_number
Пусть снова shard_count = 3
1. Пусть entity_id = 7 => shard_number = mappings.getOrPut(7, 7 % 3) = 1
2. Добавляем новый шард => shard_count = 4
3. Для entity_id = 7 получаем шард mappings.getOrPut(7, 7 % 4) = 1 (а не 3, потому что в mappings уже есть запись с таким ключом)
Таким образом это позволяет "прилипать" сущности к определенному шарду, что убирает необходимость решардинга при добавлении новых шардов
p.s.: в качестве оптимизации, чтобы не хранить маппинги для всех entity_id, можно использовать идею с "виртуальными бакетами"
Следующая часть
Ставьте 👍, если нужен пост про виртуальные бакеты в шардировании
👍160🔥2
⚡️Принцип работы snapshot isolation (aka repeatable read) в postgres
Изоляция repeatable read избавляет от неповторяющегося чтения — ситуации, когда одна и та же строка запрашивается дважды в рамках транзакции, но результаты чтения получаются разными
Как это работает в postgres:
- Каждой транзакции присваивается xid — монотонно возрастающий идентификатор транзакции
- MVCC: одновременно поддерживаются несколько версий строк
- У каждой версии строки есть два системных поля: xmin, xmax
xmin — идентификатор транзакции, который создал версию строки
xmax — идентификатор транзакции, который удалил версию строки (т.е. сделал update либо delete)
---
Отсюда возникает довольно логичная концепция — при начале repeatable read транзакции "берем снапшот":
1. Назначаем текущей транзакции некоторый xid
2. В транзакции работаем только с версиями строк, где
- либо xmin < xid < xmax — версия строки создана до текущей транзакции, а удалена уже после начала текущей
- либо xmin < xid && xmax = 0 — версия строки создана до текущей транзакции, но еще никем не удалена
---
Однако возникает следующая проблема — на момент взятия снапшота может быть активная транзакция с xid меньшим, чем у снапшота. Когда она закоммитится, то для новосозданных строк будет выполняться условие xmin < xid && xmax = 0, и мы в текущей repeatable read транзакции увидим эту версию строки. Хотя при взятии снапшота этой версии еще не было — снова можем получить неповторяющееся чтение
Это решается следующим образом:
При взятии снапшота берется не только xid, но и также снапшотится список текущих транзакций. Это позволяет в снапшоте игнорировать записи, которые были закомиченны транзакциями, которые еще были активны на момент взятия снапшота
Таким образом, условие "видимости записей" будет таким
1. Берем снапшот: xid + active_xids
2. В транзакции работаем только с версиями строк, где
Хорошая статья по теме https://mbukowicz.github.io/databases/2020/05/01/snapshot-isolation-in-postgresql.html
Изоляция repeatable read избавляет от неповторяющегося чтения — ситуации, когда одна и та же строка запрашивается дважды в рамках транзакции, но результаты чтения получаются разными
begin;
select * from t where id = 1 <- отдает одно значение
-- другая транзакция обновляет запись
select * from t where id = 1 <- отдает уже другое значение
...
Как это работает в postgres:
- Каждой транзакции присваивается xid — монотонно возрастающий идентификатор транзакции
- MVCC: одновременно поддерживаются несколько версий строк
- У каждой версии строки есть два системных поля: xmin, xmax
xmin — идентификатор транзакции, который создал версию строки
xmax — идентификатор транзакции, который удалил версию строки (т.е. сделал update либо delete)
---
Отсюда возникает довольно логичная концепция — при начале repeatable read транзакции "берем снапшот":
1. Назначаем текущей транзакции некоторый xid
2. В транзакции работаем только с версиями строк, где
- либо xmin < xid < xmax — версия строки создана до текущей транзакции, а удалена уже после начала текущей
- либо xmin < xid && xmax = 0 — версия строки создана до текущей транзакции, но еще никем не удалена
---
Однако возникает следующая проблема — на момент взятия снапшота может быть активная транзакция с xid меньшим, чем у снапшота. Когда она закоммитится, то для новосозданных строк будет выполняться условие xmin < xid && xmax = 0, и мы в текущей repeatable read транзакции увидим эту версию строки. Хотя при взятии снапшота этой версии еще не было — снова можем получить неповторяющееся чтение
Это решается следующим образом:
При взятии снапшота берется не только xid, но и также снапшотится список текущих транзакций. Это позволяет в снапшоте игнорировать записи, которые были закомиченны транзакциями, которые еще были активны на момент взятия снапшота
Таким образом, условие "видимости записей" будет таким
1. Берем снапшот: xid + active_xids
2. В транзакции работаем только с версиями строк, где
(xmin < xid < xmax || xmin < xid && xmax = 0)
&&
(xmin not in active_xids)
Хорошая статья по теме https://mbukowicz.github.io/databases/2020/05/01/snapshot-isolation-in-postgresql.html
👍64🔥23
⚡️3 как не надо
В отпуске совершенно лень писать посты про что-то умное, поэтому держите пост про 3 рандомные ошибки, которые я относительно часто встречаю
1. Надежда, что простой if-чик даст идемпотентность
Встречали когда нибудь такие проверки?
Такая конструкция очевидно рушится race condition-ом, который рано или поздно произойдет — когда два почти одновременных запроса получат отрицательный результат на db.exists(idempotency_key) и пойдут выполнять логику
2. Слишком широкие границы транзакций
Это ситуация, когда в транзакцию оборачивают не только работу с базой, а вообще всю логику обработки, включая запросы во внешние системы, кеши и т.д. Итог: долгие транзакции, висящие блокировки и вот это все
3. Пагинация через offset + limit
Довольно закономерное желание сделать пагинацию через такой запрос, база же поддерживает
Но увы несмотря на наличие индекса на (created_at), база не может за log(offset) найти место, откуда нужно стартовать, и будет честно проходить и скипать эти 100000 строк
🔥 — если нужен пост про "3 как надо"
В отпуске совершенно лень писать посты про что-то умное, поэтому держите пост про 3 рандомные ошибки, которые я относительно часто встречаю
1. Надежда, что простой if-чик даст идемпотентность
Встречали когда нибудь такие проверки?
def do_something(idempotency_key) {
if db.exists(idempotency_key) {
return
}
...
}Такая конструкция очевидно рушится race condition-ом, который рано или поздно произойдет — когда два почти одновременных запроса получат отрицательный результат на db.exists(idempotency_key) и пойдут выполнять логику
2. Слишком широкие границы транзакций
def do_something() {
transaction {
get_from_cache()
call_first_service()
call_second_service()
update_db()
...
}
}Это ситуация, когда в транзакцию оборачивают не только работу с базой, а вообще всю логику обработки, включая запросы во внешние системы, кеши и т.д. Итог: долгие транзакции, висящие блокировки и вот это все
3. Пагинация через offset + limit
Довольно закономерное желание сделать пагинацию через такой запрос, база же поддерживает
select * from orders
order by created_at desc
limit 50
offset 100000
Но увы несмотря на наличие индекса на (created_at), база не может за log(offset) найти место, откуда нужно стартовать, и будет честно проходить и скипать эти 100000 строк
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥528💅6✍1
⚡️Пара способов, как обеспечить идемпотентность
В продолжение к https://news.1rj.ru/str/MicroservicesThoughts/144
Возьму за пример предметную область, где щас работаю:
1. Есть ticket — обращение пользователя в поддержку
2. Есть article — одно сообщение в рамках обращения
Хочется идемпотентно выполнять операцию addArticle(ticketId), чтобы в переписке не было дублей сообщений из-за сетевых проблем и т.п.
1. Через ключ идемпотентности + unique constraint
В запрос на добавление article добавляется параметр idempotency_key
В той же транзакции, где делаем insert into article, мы делаем insert в таблицу с ключами идемпотентности — у этой таблицы должен быть unique constraint
Атомарность — либо оба insert-а выполнятся, либо никакой
Ограничение unique constraint — insert в таблицу с ключами выполняется не более одного раза
Складывая эти факты, получаем что то что нужно: insert в таблицу article выполняется не более одного раза
Вариации:
1.1. Ключ идемпотентности может лежать не в отдельной таблице, а просто как колонка в article
1.2. Можно делать insert on conflict do nothing, чтобы обойтись без эксепшнов
2. Через оптимистические блокировки
В запрос на добавление article добавляется параметр ticket_version
В той же транзакции, где делаем insert into article, мы проверяем что в бд лежит действительно та версия ticket, которую мы хотим обновить. Если это не так, то кидаем ошибку
Атомарность — либо и insert, и update версии выполнятся, либо не выполнится ничего
Обновление версии — если в бд лежит ticket с ticket_version = 1, то из двух параллельных запросов на обновление версии выполнится только один. Просто потому что бд гарантирует, что не будет аномалии lost update
И снова складывая эти факты, получаем требуемое
В продолжение к https://news.1rj.ru/str/MicroservicesThoughts/144
Возьму за пример предметную область, где щас работаю:
1. Есть ticket — обращение пользователя в поддержку
2. Есть article — одно сообщение в рамках обращения
Хочется идемпотентно выполнять операцию addArticle(ticketId), чтобы в переписке не было дублей сообщений из-за сетевых проблем и т.п.
1. Через ключ идемпотентности + unique constraint
В запрос на добавление article добавляется параметр idempotency_key
В той же транзакции, где делаем insert into article, мы делаем insert в таблицу с ключами идемпотентности — у этой таблицы должен быть unique constraint
begin;
insert into idempotency_keys; // ошибка если уже существует
insert into article;
commit;
Атомарность — либо оба insert-а выполнятся, либо никакой
Ограничение unique constraint — insert в таблицу с ключами выполняется не более одного раза
Складывая эти факты, получаем что то что нужно: insert в таблицу article выполняется не более одного раза
Вариации:
1.1. Ключ идемпотентности может лежать не в отдельной таблице, а просто как колонка в article
1.2. Можно делать insert on conflict do nothing, чтобы обойтись без эксепшнов
2. Через оптимистические блокировки
В запрос на добавление article добавляется параметр ticket_version
В той же транзакции, где делаем insert into article, мы проверяем что в бд лежит действительно та версия ticket, которую мы хотим обновить. Если это не так, то кидаем ошибку
begin
insert into article;
update ticket
set version = version + 1
where version = {version}
returning *; // из приложения кидаем ошибку, если не смогли произвести апдейт
commit;
Атомарность — либо и insert, и update версии выполнятся, либо не выполнится ничего
Обновление версии — если в бд лежит ticket с ticket_version = 1, то из двух параллельных запросов на обновление версии выполнится только один. Просто потому что бд гарантирует, что не будет аномалии lost update
И снова складывая эти факты, получаем требуемое
Telegram
Microservices Thoughts
⚡️3 как не надо
В отпуске совершенно лень писать посты про что-то умное, поэтому держите пост про 3 рандомные ошибки, которые я относительно часто встречаю
1. Надежда, что простой if-чик даст идемпотентность
Встречали когда нибудь такие проверки?
def…
В отпуске совершенно лень писать посты про что-то умное, поэтому держите пост про 3 рандомные ошибки, которые я относительно часто встречаю
1. Надежда, что простой if-чик даст идемпотентность
Встречали когда нибудь такие проверки?
def…
👍34🔥8💅7
⚡️Шардирование без решардирования (pt. 2)
В посте https://news.1rj.ru/str/MicroservicesThoughts/138 разобрали, что для шардирования без необходимости решардинга нужно персистентное хранилище маппингов entity_id => shard
Суть в том, что при добавлении сущности в бд мы сразу записываем, к какому шарду она относится, и эту запись больше никогда не трогаем. Соотв-но если добавится новый шард, то это не принесет никаких проблем — шард для сущности уже зафиксирован
Очевидная проблема такого подхода — жирная таблица с маппингами, которая к тому же никогда не чистится. Соотв-но с какого-то момента полностью закешировать такое станет невозможно => будет много кеш миссов => походов в базу с маппингами (которая к тому же является spof-ом)
---
И далее идут нюансы
Если у вас autoincremented ids, то эту проблему можно решить достаточно просто — давайте хранить маппинги не для каждой entity_id, а для какого-то ренжа этих entity_id
Получается примерно такая схема (aka range-based mapping)
Правила:
1. Ренжи не пересекаются
2. Если для сущности нет подходящего ренжа, то создается новый
Btw, из приложения можно корректировать, как размазывать данные между шардами просто с помощью длины ренжей. К примеру, для шарда 1 ренжи создаются длиной 5000, а для шарда 2 — длиной 10000. Соотв-но нагрузка будет распределяться примерно как 1:2
---
Пара доводов, почему это может быть ок подходом (или не ок в некоторых случаях):
1. С помощью длины ренжа можно балансировать трейдоф между "стоимость хранения ренжей" и "насколько мы не хотим грузить конкретный шард"
Пример 1: у сущности быстрый жизненный цикл, в рамках которого она генерит много нагрузки на базу. Тогда если у вас будут длинные ренжи (например, 1млн), то весь этот поток из миллиона новых сущностей польется на один шард, что может его прибить
Пример 2: сущность долгоживущая. Ренжи по 10к. В таком случае нагрузка уже будет достаточно мягко распределяться по шардам, и не будет burst-ов на конкретный шард
2. Такие ренжи легко закешировать
К примеру, если у вас 1млрд сущностей и ренжи по 10к, то это выльется в 100000 маппингов, которые займут ~5мб, что легко влезает в оперативку приложения
---
А теперь не про autoincremented ids
Тут уже скорее всего без решардинга не обойтись, и все выльется в те самые виртуальные бакеты + решардинг именно между этими виртуальными бакетами (однако еще есть способ с вшиванием id шарда в id сущности)
p.s.: пост предполагался про виртуальные бакеты, но чуть не туда понесло
Предыдущая часть
Следующая часть
В посте https://news.1rj.ru/str/MicroservicesThoughts/138 разобрали, что для шардирования без необходимости решардинга нужно персистентное хранилище маппингов entity_id => shard
Суть в том, что при добавлении сущности в бд мы сразу записываем, к какому шарду она относится, и эту запись больше никогда не трогаем. Соотв-но если добавится новый шард, то это не принесет никаких проблем — шард для сущности уже зафиксирован
Очевидная проблема такого подхода — жирная таблица с маппингами, которая к тому же никогда не чистится. Соотв-но с какого-то момента полностью закешировать такое станет невозможно => будет много кеш миссов => походов в базу с маппингами (которая к тому же является spof-ом)
---
И далее идут нюансы
Если у вас autoincremented ids, то эту проблему можно решить достаточно просто — давайте хранить маппинги не для каждой entity_id, а для какого-то ренжа этих entity_id
Получается примерно такая схема (aka range-based mapping)
[10000..19999] -> shard 2
[20000..29999] -> shard 1
...
Правила:
1. Ренжи не пересекаются
2. Если для сущности нет подходящего ренжа, то создается новый
Btw, из приложения можно корректировать, как размазывать данные между шардами просто с помощью длины ренжей. К примеру, для шарда 1 ренжи создаются длиной 5000, а для шарда 2 — длиной 10000. Соотв-но нагрузка будет распределяться примерно как 1:2
---
Пара доводов, почему это может быть ок подходом (или не ок в некоторых случаях):
1. С помощью длины ренжа можно балансировать трейдоф между "стоимость хранения ренжей" и "насколько мы не хотим грузить конкретный шард"
Пример 1: у сущности быстрый жизненный цикл, в рамках которого она генерит много нагрузки на базу. Тогда если у вас будут длинные ренжи (например, 1млн), то весь этот поток из миллиона новых сущностей польется на один шард, что может его прибить
Пример 2: сущность долгоживущая. Ренжи по 10к. В таком случае нагрузка уже будет достаточно мягко распределяться по шардам, и не будет burst-ов на конкретный шард
2. Такие ренжи легко закешировать
К примеру, если у вас 1млрд сущностей и ренжи по 10к, то это выльется в 100000 маппингов, которые займут ~5мб, что легко влезает в оперативку приложения
---
А теперь не про autoincremented ids
Тут уже скорее всего без решардинга не обойтись, и все выльется в те самые виртуальные бакеты + решардинг именно между этими виртуальными бакетами (однако еще есть способ с вшиванием id шарда в id сущности)
p.s.: пост предполагался про виртуальные бакеты, но чуть не туда понесло
Предыдущая часть
Следующая часть
Telegram
Microservices Thoughts
⚡️Шардирование без решардирования
Одна из основных проблем шардирования — решардинг, то есть когда при добавлении нового шарда нужно перераскидать данные между шардами. Почему так случается?
Представьте есть 3 шарда => shard_count = 3
Шард выбирается как…
Одна из основных проблем шардирования — решардинг, то есть когда при добавлении нового шарда нужно перераскидать данные между шардами. Почему так случается?
Представьте есть 3 шарда => shard_count = 3
Шард выбирается как…
👍35💅1
⚡️Шардирование без решардирования (pt. 3)
Возьмем максимально наглые требования
1. Хотим шардирование
2. Логика шардирования описывается внутри приложения и может меняться
3. Добавление новых шардов происходит без решардирования
4. Нет spof-а в виде базы с маппингами entity_id -> shard
---
И несмотря на противоречивость, это вполне себе достижимо
Возьмем отсюда шаги
1. Нам приходит запрос по entity_id
2. Мы идем в хранилище маппингов, выясняем в каком shard лежит этот entity_id
И склеим их в один — внутри идентификатора сущности entity_id уже будет номер шарда, где она лежит
Например, для идентификатора entity_id = 1_765
Номер шарда = 1
Локальный id в рамках шарда = 765
Локальный — в том смысле, что можно использовать локальный для шарда сиквенс, т.е. могут быть айдишники 1_765 (в первом шарде) и 2_765 (во втором шарде)
Всё это убирает необходимость где-то отдельно хранить маппинги — они уже вклеены в id сущности
---
Главный минус такого подхода — переложить сущность в другой шард невозможно, иначе нам придется менять id сущности
Однако мы получаем гору плюсов, особенно учитывая что реализовать такое практически бесплатно
- Нет spof-а в виде базы с маппингами
- Нет промежуточного шага с выяснением шарда
- Нет решардинга
- Можно делать логику шардирования любой сложности и менять ее в любой момент
- Вполне себе скейлится на огромные объемы
И еще одно неочевидное преимущество — шардирование зачастую делается по "основным агрегатам" в системе, например, Order. И чтобы запросить какую-то дочернюю сущность заказа, нужно в запросе передавать id заказа (чтобы вообще понять в каком шарде лежат эти дочерние сущности). Подход выше такую проблему нивелирует, потому что и для дочерних сущностей сразу будет понятно, в каком шарде они лежат
Предыдущая часть
🔥 — если было полезно
Возьмем максимально наглые требования
1. Хотим шардирование
2. Логика шардирования описывается внутри приложения и может меняться
3. Добавление новых шардов происходит без решардирования
4. Нет spof-а в виде базы с маппингами entity_id -> shard
---
И несмотря на противоречивость, это вполне себе достижимо
Возьмем отсюда шаги
1. Нам приходит запрос по entity_id
2. Мы идем в хранилище маппингов, выясняем в каком shard лежит этот entity_id
И склеим их в один — внутри идентификатора сущности entity_id уже будет номер шарда, где она лежит
Например, для идентификатора entity_id = 1_765
Номер шарда = 1
Локальный id в рамках шарда = 765
Локальный — в том смысле, что можно использовать локальный для шарда сиквенс, т.е. могут быть айдишники 1_765 (в первом шарде) и 2_765 (во втором шарде)
Всё это убирает необходимость где-то отдельно хранить маппинги — они уже вклеены в id сущности
---
Главный минус такого подхода — переложить сущность в другой шард невозможно, иначе нам придется менять id сущности
Однако мы получаем гору плюсов, особенно учитывая что реализовать такое практически бесплатно
- Нет spof-а в виде базы с маппингами
- Нет промежуточного шага с выяснением шарда
- Нет решардинга
- Можно делать логику шардирования любой сложности и менять ее в любой момент
- Вполне себе скейлится на огромные объемы
И еще одно неочевидное преимущество — шардирование зачастую делается по "основным агрегатам" в системе, например, Order. И чтобы запросить какую-то дочернюю сущность заказа, нужно в запросе передавать id заказа (чтобы вообще понять в каком шарде лежат эти дочерние сущности). Подход выше такую проблему нивелирует, потому что и для дочерних сущностей сразу будет понятно, в каком шарде они лежат
Предыдущая часть
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥61👍1
Как расти разработчику в компании
А что значит "расти"? Я бы выделил два направления:
1. Рост по хард скиллам
2. Рост по карьере внутри компании
И на практике оказывается, что первое не всегда влечет второе
---
Почему так происходит?
У каждого разработчика есть своя зона ответственности — полянка, за которую он отвечает: в рамках нее задачи делаются в срок и с приемлемым уровнем качества, не плодится техдолг, и в целом "полянка" работает стабильно
И рост по карьере коррелирует именно с размером и сложностью этой зоны ответственности. Понять это можно на таком примере
- Вася очень крутой разработчик, при этом отвечает лишь за небольшой сервис
- Коля не настолько крут по хардам, но успешно тянет на себе 10 сервисов и закрывает собой огромный пласт работы
Кого из них повысят, думаю, очевидно
---
Так в чем же проблема просто взять и расширить зону ответственности? А в том, что начинают возникать ситуации, которых раньше не было
1. Чем шире зона ответственности, тем чаще надо с кем-то о чем-то договориться — появляются новые менеджеры, появляются новые смежники, все чего-то хотят от тебя, ты чего-то хочешь от них. Поэтому навык переговоров и умение доносить свою позицию — один из ключевых
2. Появляется много мелких задач, которые физически нельзя переварить за один день — здесь поможет навык приоритизации
3. И наоборот — начинают появляться ситуации, где нужно принять сложное решение. Очень часто это вызывает страх и прокрастинацию, потому что не понятно, а с чего начать
---
И к сожалению, таким вещам обычно не учат — у кого-то они получаются сами по себе, а кому не повезло — не получаются. Закрыть пробелы по таким скиллам поможет канал Андрея — Head of Product Development в Яндекс Лавке. Он рассказывает про то, как себя вести в подобных "менеджерских" ситуациях
p.s.: сам я этот канал читаю уже больше года, поэтому могу с чистой совестью рекомендовать его как тимлидам, так и амбициозным разработчикам, нацеленным на рост
🔥 — если подписались
А что значит "расти"? Я бы выделил два направления:
1. Рост по хард скиллам
2. Рост по карьере внутри компании
И на практике оказывается, что первое не всегда влечет второе
---
Почему так происходит?
У каждого разработчика есть своя зона ответственности — полянка, за которую он отвечает: в рамках нее задачи делаются в срок и с приемлемым уровнем качества, не плодится техдолг, и в целом "полянка" работает стабильно
И рост по карьере коррелирует именно с размером и сложностью этой зоны ответственности. Понять это можно на таком примере
- Вася очень крутой разработчик, при этом отвечает лишь за небольшой сервис
- Коля не настолько крут по хардам, но успешно тянет на себе 10 сервисов и закрывает собой огромный пласт работы
Кого из них повысят, думаю, очевидно
---
Так в чем же проблема просто взять и расширить зону ответственности? А в том, что начинают возникать ситуации, которых раньше не было
1. Чем шире зона ответственности, тем чаще надо с кем-то о чем-то договориться — появляются новые менеджеры, появляются новые смежники, все чего-то хотят от тебя, ты чего-то хочешь от них. Поэтому навык переговоров и умение доносить свою позицию — один из ключевых
2. Появляется много мелких задач, которые физически нельзя переварить за один день — здесь поможет навык приоритизации
3. И наоборот — начинают появляться ситуации, где нужно принять сложное решение. Очень часто это вызывает страх и прокрастинацию, потому что не понятно, а с чего начать
---
И к сожалению, таким вещам обычно не учат — у кого-то они получаются сами по себе, а кому не повезло — не получаются. Закрыть пробелы по таким скиллам поможет канал Андрея — Head of Product Development в Яндекс Лавке. Он рассказывает про то, как себя вести в подобных "менеджерских" ситуациях
p.s.: сам я этот канал читаю уже больше года, поэтому могу с чистой совестью рекомендовать его как тимлидам, так и амбициозным разработчикам, нацеленным на рост
🔥 — если подписались
🔥44👍5🤔4
Полу-оффтоп к посту выше
Занимательный способ, как проверить растете вы или нет — вновь прочитать статью, которую вы не до конца понимали пару лет назад
У меня так внезапно получилось с https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html, на которую я натыкался еще будучи стажером. Btw рекомендую почитать, статья из серии "как еще больше бояться программировать"
Для владельцев тг каналов есть еще один способ — почитайте свои посты 1-2 годовой давности. Если фейспалмите, то все хорошо
Занимательный способ, как проверить растете вы или нет — вновь прочитать статью, которую вы не до конца понимали пару лет назад
У меня так внезапно получилось с https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html, на которую я натыкался еще будучи стажером. Btw рекомендую почитать, статья из серии "как еще больше бояться программировать"
Для владельцев тг каналов есть еще один способ — почитайте свои посты 1-2 годовой давности. Если фейспалмите, то все хорошо
🔥27😁10👍6
⚡️Немного про визуализацию архитектуры
У некоторых людей кубики и стрелочки в миро — основной инструмент для визуалиации архитектуры. В простых случаях с этим нет вообще никаких проблем
Но когда нужно описать что-то более менее объемное и/или сложное, зачастую приходим к двум ключевым проблемам:
- На одной диаграмме есть вообще все, и ее трудно осознать
- Неочевиден порядок, в котором взаимодействуют компоненты этой диаграммы
В таких случаях бывает удобно подробить одну большую диаграмму на несколько меньших
Имхо, джентльменский набор, которого хватает для большинства случаев:
C4
Разбивает диаграмму на 4 слоя: системы, контейнеры, компоненты, код (обычно не рисуют, тк часто меняется). Переход с n-го на n+1-ый уровень это "зум" в какой-то кусочек диаграммы. Например, на диаграмме контейнеров выбираем контейнер, смотрим диаграмму компонентов по этому контейнеру
Разумеется, не всегда нужны все 4 слоя, это просто удобный способ обозначить, элементы какого уровня абстракции мы хотим видеть на конкретной диаграмме
Sequence diagram
Это как раз про порядок взаимодействий. На "плоской" C4 диаграмме зачастую сложно понять в какой последовательности кто кого вызывает. Диаграммки последовательностей прекрасно закрывают эту потребность
State diagram
Часто в рамках приложения есть какая-то сущность с каким-то жизненным циклом. Чтобы ответить на вопрос, какие бывают статусы у сущности, и кто инициирует переходы между ними, можно воспользоваться диаграммой состояния. Это буквально визуализация конечного автомата
---
Если вы как и я не любите собирать диаграммы в визуальном редакторе, то есть https://plantuml.com/ru/, где каждый из типов выше можно просто описать текстом и зарендерить прямо в браузере
У некоторых людей кубики и стрелочки в миро — основной инструмент для визуалиации архитектуры. В простых случаях с этим нет вообще никаких проблем
Но когда нужно описать что-то более менее объемное и/или сложное, зачастую приходим к двум ключевым проблемам:
- На одной диаграмме есть вообще все, и ее трудно осознать
- Неочевиден порядок, в котором взаимодействуют компоненты этой диаграммы
В таких случаях бывает удобно подробить одну большую диаграмму на несколько меньших
Имхо, джентльменский набор, которого хватает для большинства случаев:
C4
Разбивает диаграмму на 4 слоя: системы, контейнеры, компоненты, код (обычно не рисуют, тк часто меняется). Переход с n-го на n+1-ый уровень это "зум" в какой-то кусочек диаграммы. Например, на диаграмме контейнеров выбираем контейнер, смотрим диаграмму компонентов по этому контейнеру
Разумеется, не всегда нужны все 4 слоя, это просто удобный способ обозначить, элементы какого уровня абстракции мы хотим видеть на конкретной диаграмме
Sequence diagram
Это как раз про порядок взаимодействий. На "плоской" C4 диаграмме зачастую сложно понять в какой последовательности кто кого вызывает. Диаграммки последовательностей прекрасно закрывают эту потребность
State diagram
Часто в рамках приложения есть какая-то сущность с каким-то жизненным циклом. Чтобы ответить на вопрос, какие бывают статусы у сущности, и кто инициирует переходы между ними, можно воспользоваться диаграммой состояния. Это буквально визуализация конечного автомата
---
Если вы как и я не любите собирать диаграммы в визуальном редакторе, то есть https://plantuml.com/ru/, где каждый из типов выше можно просто описать текстом и зарендерить прямо в браузере
👍58🔥6💅3