Недавно у меня возникла следующая задача: необходимо запросить данные у трех сервисов, но достаточно получить первый валидный ответ от любого их них, остальные 2 вызова можно не ожидать.
🤝 Интерфейс вызова следующий:
🤯 Я попытылся написать это на java, но ничего адекватного по количеству строк и общей читаемости кода у меня не получилось.
🐣 Решение я нашел в использовании корутин. Первое, что я сделал: превратил
🪢 Далее я использовал интересную функцию -
🍎 Таким образом, моя задача сводится к довольно простой вещи - поместим все наши вызовы в список и передадим функции
💩 В реальности сервисы могут кидать исключения или возвращать пустой результат
🚨 И тут надо помнить еще один интересный нюанс: если вы снова передадите в
Финальное решение выглядит как-то так (за исключением try-catch, чтобы сократить):
☄️ Отмечу еще, что select бывает невероятно полезен в сочетании с использованием каналов (kotlinx.coroutines.Channel). Канал - аналог BlockingQueue в мире корутин, позже выложу пост о них. Если у вас есть задача мониторить множество очередей на предмет новых элементов и сразу же передавать их на обработку, то лучше средства не найти.
🪐 Интересно, что функция
Ставьте 🔥, если было интересно. Если вы знаете код на java, который решает аналогичную задачу и является плюс-минус таким же лаконичным, делитесь в комментах!
#kotlin #coroutines #java
🤝 Интерфейс вызова следующий:
fun performCall(key: Key) : CompletableFuture<Value>
🐣 Решение я нашел в использовании корутин. Первое, что я сделал: превратил
CompletableFuture<Value> в Deferred<Value>, используя extension-функцию public fun <T> CompletionStage<T>.asDeferred(): Deferred<T>. Это необходимо, чтобы переместить наш код в плоскость корутин. 🪢 Далее я использовал интересную функцию -
select. Ей на вход можно передать любое количество “ожидающих (suspended)” функций и select будет ждать, пока любая из этих функций не вернет результат. Тогда исполнение продолжится и select вернет вам результат этой "победившей" корутины. 🍎 Таким образом, моя задача сводится к довольно простой вещи - поместим все наши вызовы в список и передадим функции
select, а она вернет нам первый полученный результат. Это код, который нам нужен:val calls = services.map { performCall(key).asDeferred() }.toList()
val res = select {
calls.forEach { call ->
call.onAwait { it } // it это результат вызова
}
}
null. А нам необходимо дождаться именно первого валидного результата. Мы можем сделать следующее - проанализируем результат и в случае null будем снова заходить в select. 🚨 И тут надо помнить еще один интересный нюанс: если вы снова передадите в
select тот же самый список, то он повторно выдаст вам результат ошибочного вызова, ведь он и правда завершен :) Поэтому давайте удалять из списка те вызовы, которые выполнились и не являются валидными. Только не забывайте поместить их в thread-safe контейнер, иначе легко словите ConcurrentModificationException. Финальное решение выглядит как-то так (за исключением try-catch, чтобы сократить):
val calls = services.map { performCall(key).asDeferred() }.toCollection(ConcurrentHashMap.newKeySet())
var res: Value? = null
while (calls.isNotEmpty()) {
val res = select {
calls.forEach { call ->
call.onAwait { callRes ->
calls.remove(call)
callRes
}
}
}
if (res != null) break
}
☄️ Отмечу еще, что select бывает невероятно полезен в сочетании с использованием каналов (kotlinx.coroutines.Channel). Канал - аналог BlockingQueue в мире корутин, позже выложу пост о них. Если у вас есть задача мониторить множество очередей на предмет новых элементов и сразу же передавать их на обработку, то лучше средства не найти.
🪐 Интересно, что функция
select по свой сути является аналогом системного вызова select на основе которого реализован "неблокирующий" ввод-вывод. Он позволяет мониторить множество TCP соединений на предмет новых событий IO и превращать их в единый "уплотненный" или иначе "мультиплексированный" поток событий, что позволяет очень эффективно использовать ресурсы системы. О "мультиплексировании" уже говорили тут и будем говорить еще.Ставьте 🔥, если было интересно. Если вы знаете код на java, который решает аналогичную задачу и является плюс-минус таким же лаконичным, делитесь в комментах!
#kotlin #coroutines #java
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥37👍7❤3🆒1
🗒 У нас есть следующий фрагмент кода:
👀 Функция
🎃 При исполнении на одном потоке функция
🐌 Наша задача: увеличить пропускную способность, чтобы функция
🎈 На эту задачу есть несколько правильных ответов. Какие-то можно обосновать теоретическими расчетами, какие-то практическим экспериментом. Но гораздо интереснее, как бы вы стали рассуждать, если бы получили такую задачу на собеседовании. Так что пиши ваши рассуждения в комменты!
#interview #threads
fun task() {
val data = prepareData()
process(data)
}
👀 Функция
prepareData производит получение и подготовку данных для их последующей обработки функцией process. 🎃 При исполнении на одном потоке функция
prepareData занимает 5% времени выполнения, process 95%. Единственный поток позволяет выполнять 10 операций в секунду. Важно отметить, что функция prepare работает с разделяемыми данными и должна выполняться под локом в многопоточной среде.🐌 Наша задача: увеличить пропускную способность, чтобы функция
task могла выполняться 100 раз в секунду. Сколько потоков понадобится, чтобы достичь желаемого прироста?🎈 На эту задачу есть несколько правильных ответов. Какие-то можно обосновать теоретическими расчетами, какие-то практическим экспериментом. Но гораздо интереснее, как бы вы стали рассуждать, если бы получили такую задачу на собеседовании. Так что пиши ваши рассуждения в комменты!
#interview #threads
🔥11🤔4👍3
Ответ 20 на предыдущий опрос может быть не очевидным с первого взгляда. Мы получаем его, воспользовшись законом Амдала
🧶 Итак, в этом посте будем вычислять ответ ответ, опираясь только на теорию.
🎯 Нам нужно увеличить пропускную способность в 10 раз (с 10 операций в секунду до 100). Кажется, что, если один поток может выполнять 10 операций в секунду, то нам нужно увеличить количество потоков пропорционально, до 10.
🔩 Однако, в данной задаче есть нюанс, который делает рост производительности не линейным относительно количества потоков. Иными словами, увеличение потоков в 10 раз не дает пропорциональный выигрыш в производительности.
🔓 Этим нюансом является наличие функции
🎁 Зато мы можем получать линейный выигрыш от параллелизации остальных 95%, которые занимает функция
Доля кода, который никак не получится распараллелить (последовательного) 0.05 (5%). Доля параллелизуемого - 0.95 (95%). Давайте считать:
1. Если у вас
2. Если у вас
3. Если у вас
📈 Данный пост демонстрирует нам закон Амдала, который бывает чрезмерно пессимистичным в своей оценке и рассматривает худший сценарий исполнения. Однако, стоит вынести из него важную идею:
❗️❗️❗️Производительность вашего кода не всегда растет пропорционально увеличению количества потоков. Чем больше в вашем коде фрагментов, требующих последовательного (однопоточного) выполнения, тем меньше прирост производительности при параллелизации.
⏬ В следующих постах проверим, выполняется ли закон Амдала на практике для данного кода и увидим еще один способ увеличить пропускную способность нашей функции.
🧶 Итак, в этом посте будем вычислять ответ ответ, опираясь только на теорию.
🎯 Нам нужно увеличить пропускную способность в 10 раз (с 10 операций в секунду до 100). Кажется, что, если один поток может выполнять 10 операций в секунду, то нам нужно увеличить количество потоков пропорционально, до 10.
🔩 Однако, в данной задаче есть нюанс, который делает рост производительности не линейным относительно количества потоков. Иными словами, увеличение потоков в 10 раз не дает пропорциональный выигрыш в производительности.
🔓 Этим нюансом является наличие функции
prepareData, которая требует выполнения под локом. Фактически, функция не параллелизуется. Сколько бы потоков мы не добавили, у нас не получится получить для нее никакой выигрыш, говорит закон Амдала. То есть время, которое prepareData занимает от общего времени выполнения, всегда будут оставаться прежним.🎁 Зато мы можем получать линейный выигрыш от параллелизации остальных 95%, которые занимает функция
process.Доля кода, который никак не получится распараллелить (последовательного) 0.05 (5%). Доля параллелизуемого - 0.95 (95%). Давайте считать:
1. Если у вас
10 потоков, то вы можете в 10 раз ускорить параллелизуемый код, то есть из 0.95 превратить его в 0.095. Сложим теперь доли ускоренного и неускоряемого кода: 0.05 + 0.095 = 0.145. (14.5%, то есть мы стали быстрее на 85.5%). Чтобы понять ВО сколько раз мы ускорились надо единицу поделить на 0.145, получится 6.89. А нам необходим прирост в 10 раз.2. Если у вас
15 потоков, то вы можете в 15 раз ускорить параллелизуемый код. 0.95 -> 0.063. 0.05 + 0.063 = 0.113. 1 / 0.113 = 8.84. Все еще не 10!3. Если у вас
20 потоков, то вы можете в 20 раз ускорить параллелизуемый код. 0.95 -> 0.0475. 0.05 + 0.0475 = 0.0975. 1 / 0.0975 = 10.25. Ура! 🏆 Получается, что нам нужно 20 потоков (19 тоже хватит на самом деле).📈 Данный пост демонстрирует нам закон Амдала, который бывает чрезмерно пессимистичным в своей оценке и рассматривает худший сценарий исполнения. Однако, стоит вынести из него важную идею:
❗️❗️❗️Производительность вашего кода не всегда растет пропорционально увеличению количества потоков. Чем больше в вашем коде фрагментов, требующих последовательного (однопоточного) выполнения, тем меньше прирост производительности при параллелизации.
⏬ В следующих постах проверим, выполняется ли закон Амдала на практике для данного кода и увидим еще один способ увеличить пропускную способность нашей функции.
🔥34👍6👌3💯2
🧑💻 Как и обещал, я провел небольшой эксперимент для проверки теоретических расчетов и постарался найти объяснение полученным на практике результатам.
Подробнее об этом, а также о выводах и практических рекомендациях читайте по ссылке
Please open Telegram to view this post
VIEW IN TELEGRAM
Telegraph
Проводим эксперимент
Чтобы проверить на практике, сколько потребуется потоков в предыдущем кейсе, я написал такой кусочек кода: class Test { private val lock = ReentrantLock(true) private val parallelWorkers = Executors.newFixedThreadPool(1) fun task() { parallelWorkers.submit…
🔥17❤1👍1🆒1
Последний пост из серии "как ускорить функцию
⬆️ Напоминаю, мы с вами задавались вопросом, сколько потребуется потоков, чтобы в 10 раз ускорить функцию task, где 5% кода выполняется под локом. Вычисленный с помощью закона Амдала ответ в 20 потоков был позже поставлен под сомнение практическими экспериментами. Сегодня ставим точку в этой серии постов.
🧱 Предлагаю немного переработать архитектуру данного нам кода:
1. Выделить один отдельный поток, который будет заниматься исключительно препроцессингом данных (функцией
2. Выделить группу потоков, которая будет заниматься параллельной обработкой подготовленных данных.
💡 Какие у нас ожидания производительности от данного рефакторинга?
🔎 Вся функция
🧑🏽💻 Модифицируем код следующим образом:
🚀 Здесь используется
⚖️ Безусловно, данное решение не всегда применимо. Часто блокировки "раскиданы" по всему коду и мы не имеем возможности вынести их в отдельную группу потоков. Но в данном случае, когда препроцессинг всегда осуществляется в одном месте перед вычислениями, мы можем использовать такую архитектуру 🔥.
#threads
task" 😁⬆️ Напоминаю, мы с вами задавались вопросом, сколько потребуется потоков, чтобы в 10 раз ускорить функцию task, где 5% кода выполняется под локом. Вычисленный с помощью закона Амдала ответ в 20 потоков был позже поставлен под сомнение практическими экспериментами. Сегодня ставим точку в этой серии постов.
🧱 Предлагаю немного переработать архитектуру данного нам кода:
1. Выделить один отдельный поток, который будет заниматься исключительно препроцессингом данных (функцией
prepareData, которая требует исполнения под локом). 2. Выделить группу потоков, которая будет заниматься параллельной обработкой подготовленных данных.
💡 Какие у нас ожидания производительности от данного рефакторинга?
🔎 Вся функция
task целиком требует 100ms на выполнение. Функция prepareData занимает 5% этого времени (5ms). Получается, что если поток будет последовательно заниматься только выполнением prepareData, то он сможет делать целых 200 исполнений за одну секунду. Кстати, лок теперь можно убирать, ведь код выполняется на одном потоке.🧑🏽💻 Модифицируем код следующим образом:
class Test {
private val lock = ReentrantLock()
private val sequentialWorker = Executors.newSingleThreadExecutor()
private val parallelWorkers = Executors.newFixedThreadPool(10)
fun task() {
sequentialWorker.submit {
val data = prepareData()
parallelWorkers.submit {
process(data)
}
}
}
private fun prepareData(): Data {
Thread.sleep(5L)
Data(1)
}
private fun process(data: Data) {
Thread.sleep(95L)
}
data class Data(val d: Int)
}
🚀 Здесь используется
11 потоков - один отдельный поток выполняет последовательную часть кода и передает подготовленные данные в группу из оставшихся 10 потоков, которые выполняют обработку подготовленных данных. Как и ожидалось, такой код выполняет около 100 операций в секунду. Более того, при увеличении группы потоков с 10 до 20 этот код выполняет около 200 операций в секунду, что является его максимальным возможным ускорением.⚖️ Безусловно, данное решение не всегда применимо. Часто блокировки "раскиданы" по всему коду и мы не имеем возможности вынести их в отдельную группу потоков. Но в данном случае, когда препроцессинг всегда осуществляется в одном месте перед вычислениями, мы можем использовать такую архитектуру 🔥.
#threads
🔥15👍4👌1🏆1
Аналог блокирующей очереди для корутин
⛓ В одном из предыдущих постов мы обсуждали удобство использования блокирующий очереди (интерфейс
Почему бы нам не использовать такую же очередь для общения корутин друг с другом?
🏈 Дело в том, что операции
- Получение элемента блокируется, если очередь пуста
- Вставка в очередь может блокироваться, если максимальный размер очереди уже достигнут
🧨 Но использовать внутри корутин операции, блокирующие поток, антипаттерн. Поэтому, например, существует отдельный набор мьютексов, которые не блокируют поток, а реализуют взаимное исключение именно для корутин.
🥁 Есть ли неблокирующий аналог для
✍️ Как и очередь, ченнел может иметь ограниченный размер буфера элементов. Вы можете определить его, передав в конструктор. Но есть специальные, зарезервированные значения длины буфера, в зависимости от которых вам будет предоставлена наиболее оптимальная реализация канала. Например, в качестве размера вы можете передать следующие константы:
1. Channel.RENDEZVOUS: в этом случае вам будет создан канал не имеющий никакого буфера. Каждый вызов метода
2. Channel.UNLIMITED: тут все понятно, буфер будет “бесконечным”, что фактически можно читать, как “сколько памяти хватит”. Метод
3. Channel.CONFLATED: довольно интересная реализация канала. Поддерживает буфер размером в один элемент. Каждый последний вызов метода
Создание ченнела с размером буфера равным десяти, запись и чтение из него:
Ставьте 🔥, если было полезно и отвечайте на опрос ниже ⬇️
#kotlin #coroutines
⛓ В одном из предыдущих постов мы обсуждали удобство использования блокирующий очереди (интерфейс
java.util.concurrent.BlockingQueue), для коммуникации между потоками. Одна группа потоков помещают данные в хвост очереди, другая группа получает данные из ее головы. Почему бы нам не использовать такую же очередь для общения корутин друг с другом?
🏈 Дело в том, что операции
BlockingQueue могут заблокировать поток исполнения:- Получение элемента блокируется, если очередь пуста
- Вставка в очередь может блокироваться, если максимальный размер очереди уже достигнут
🧨 Но использовать внутри корутин операции, блокирующие поток, антипаттерн. Поэтому, например, существует отдельный набор мьютексов, которые не блокируют поток, а реализуют взаимное исключение именно для корутин.
🥁 Есть ли неблокирующий аналог для
BlockingQueue в мире корутин? Да, интерфейс kotlinx.coroutines.channels.Channel и его реализации концептуально являются воплощениями блокирующей очереди. "Канал" предоставляет методы send и receive, которые тоже обладают блокирующей семантикой, но блокируют они корутину, а не поток, на котором она исполняется. ✍️ Как и очередь, ченнел может иметь ограниченный размер буфера элементов. Вы можете определить его, передав в конструктор. Но есть специальные, зарезервированные значения длины буфера, в зависимости от которых вам будет предоставлена наиболее оптимальная реализация канала. Например, в качестве размера вы можете передать следующие константы:
1. Channel.RENDEZVOUS: в этом случае вам будет создан канал не имеющий никакого буфера. Каждый вызов метода
send будет блокировать корутину, пока на встречу (на рандеву) с ним не придет соответствующий метод receive.2. Channel.UNLIMITED: тут все понятно, буфер будет “бесконечным”, что фактически можно читать, как “сколько памяти хватит”. Метод
send в этой реализации не заблокирует корутину никогда.3. Channel.CONFLATED: довольно интересная реализация канала. Поддерживает буфер размером в один элемент. Каждый последний вызов метода
send подменяет то, что было записано ранее. Метод receive соответственно всегда прочитает последнее записанное значение. Как и в прошлом пункте метод send никогда не блокируется.Создание ченнела с размером буфера равным десяти, запись и чтение из него:
val channel = Channel<Element>(capacity)
// Producer code
channel.send(elementToSend)
// Consumer code
val elementReceived = channel.receive()
Ставьте 🔥, если было полезно и отвечайте на опрос ниже ⬇️
#kotlin #coroutines
🔥13❤3👍2
Используете ли Channel в вашем коде?
Anonymous Poll
69%
Не пишу на Котлин
8%
Вообще не использую корутины
17%
Корутины использую, но Channels не приходилось
7%
Активно используем Channels в разработке
3%
Не знаю, как бы жил без Channels
🤝3
Все еще в шоке, от того что моя аудитория по большей части не пишет на котлин 😁 Но для тех, кто пишет, продолжу делать посты о нем и о корутинах, очень сильно их люблю.
📝 Напишите в коментах, почему так сложилось, что не используете котлин или корутины в разработке: может быть что-то понравилось / в компании используют другой язык / просто не было возможности попробовать?
📝 Напишите в коментах, почему так сложилось, что не используете котлин или корутины в разработке: может быть что-то понравилось / в компании используют другой язык / просто не было возможности попробовать?
❤14👍5🔥1🥱1🐳1
Observability
🔍 Конечно, мы хотим, чтобы наше приложение было "наблюдаемым" (observable). Что мы вкладываем в это понятие и так ли это нам необходимо?
🔦 Под observability обычно понимается способность системы продюсировать достаточное количество информативных данных о себе, по которым мы можем делать обоснованные выводы о ее состоянии.
📝 Такие данные еще называют телеметрией. Это могут быть метрики приложения, логи, трейсы и другие данные, полезные для:
- Понимания состояния системы и ее здоровья
- Понимания соответствия ее поведения ожидаемому, корректности
- Понимания уровня производительности системы
- Поиска и локализации неисправностей
- Установления хронологии событий при траблшутинге, временных промежутков инцидентов, простоев, отказов
- Установления начальной, корневой причины (root cause) неисправностей
- Установления длительностей взаимодействия между компонентами системы и тд
💡 Любое приложение должно быть в какой-то мере observable, однако понятно, что требования к observability наиболее высоки для систем, которые:
- Не терпят простоя: теряют деньги и лояльность клиентов каждую секунду и минуту, пока недоступны
- Не терпят потерь трафика, задержек в обработке
👁 В таких системах важно почти мгновенно (и желательно автоматически) понять, что проблема есть, в кратчайшие сроки выяснить источник, локализовать проблему и принять меры по ее устранению. Конечно, инструменты observability в даных кейсах это глаза и уши инженеров.
❓ Хочу переодически делать посты про мониторинг и observability приложений. В основном про метрики: что это, какие есть типы, как ими пользоваться и чем они могут нам помочь и тд. Что думаете?
🔥 - тема очень важная и интересная, пиши
👍 - не на 100% понимаю зачем все это, но давай почитаем
🤔 - этим вообще кто-то пользуется?
🔍 Конечно, мы хотим, чтобы наше приложение было "наблюдаемым" (observable). Что мы вкладываем в это понятие и так ли это нам необходимо?
🔦 Под observability обычно понимается способность системы продюсировать достаточное количество информативных данных о себе, по которым мы можем делать обоснованные выводы о ее состоянии.
📝 Такие данные еще называют телеметрией. Это могут быть метрики приложения, логи, трейсы и другие данные, полезные для:
- Понимания состояния системы и ее здоровья
- Понимания соответствия ее поведения ожидаемому, корректности
- Понимания уровня производительности системы
- Поиска и локализации неисправностей
- Установления хронологии событий при траблшутинге, временных промежутков инцидентов, простоев, отказов
- Установления начальной, корневой причины (root cause) неисправностей
- Установления длительностей взаимодействия между компонентами системы и тд
💡 Любое приложение должно быть в какой-то мере observable, однако понятно, что требования к observability наиболее высоки для систем, которые:
- Не терпят простоя: теряют деньги и лояльность клиентов каждую секунду и минуту, пока недоступны
- Не терпят потерь трафика, задержек в обработке
❓ Хочу переодически делать посты про мониторинг и observability приложений. В основном про метрики: что это, какие есть типы, как ими пользоваться и чем они могут нам помочь и тд. Что думаете?
🔥 - тема очень важная и интересная, пиши
👍 - не на 100% понимаю зачем все это, но давай почитаем
🤔 - этим вообще кто-то пользуется?
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥80👍4❤1
🔬 Метрики - числовые данные, отражающие какой-то аспект вашего приложения. Они формируются путем регулярных подсчетов и замеров интересующих параметров сервиса. Это могут быть размеры очередей, количество потоков, количество совершенных http запросов или отправленных / полученных байтов.
📌 У метрики есть название, обычно отражающее предмет измерения (task completed / request served). Кроме того, к метрике могут прикрепляться метаданные, которые представляют из себя набор пар ключ-значение.
💡 Пример: метрика с названием
http_requests_served_total. - Отражает общее количество http запросов, которые были обслужены
- Обычный счетчик ⏱️, который увеличивается каждый раз, когда очередной запрос обслужен. Скажем, в начале их было 0, через минуту 5, еще через минуту 10.
http_requests_served_status_200_total. Это сработает. Хуже будет, если вы хотите получить какую-нибудь табличку, где отображается сколько запросов было выполнено с каждым из возможных статусов: 404, 500, 200, 302 и тд, мы ведь никогда не знаем наверняка с какими кодами могут завершиться ваши запросы - придется делать метрики для каждого из них!http_requests_served_total, но добавим к ней метаданные в виде пары ключ-значение, где есть ключ с названием http_status_code и значение, которое подставляется в зависимости от того, какой код мы получили.http_requests_served_total{} 0.0
http_requests_served_total{"http_status_code"="200"} 3.0
http_requests_served_total{"http_status_code"="500"} 2.0
http_requests_served_total{"http_status_code"="200"} 7.0
http_requests_served_total{"http_status_code"="500"} 2.0
http_requests_served_total{"http_status_code"="302"} 1.0
🔐 Эти пары ключ-значения называют метками (
label) или измерениями (только не measurement, а dimension, тут не путать). Dimension, потому что мы ведь по факту увеличиваем размерность данных. Обратите внимание, чем больше у нас различных кодов ответа, тем больше строк при изменении метрики :) 🖇️ Туда же в мета-информацию можно, например, добавить идентификатор конкретной виртуальной машины или "инстанса" приложения, чтобы мы видели, как ведет себя каждая из них в отдельности. Давайте добавим label
instance. Посмотрите, что получилось в конце третьей минуты:http_requests_served_total{"http_status_code"="200", "instance"="i_1"} 7.0
http_requests_served_total{"http_status_code"="500", "instance"="i_1"} 2.0
http_requests_served_total{"http_status_code"="302", "instance"="i_1"} 1.0
http_requests_served_total{"http_status_code"="200", "instance"="i_2"} 1.0
http_requests_served_total{"http_status_code"="500", "instance"="i_2"} 12.0
http_requests_served_total{"http_status_code"="302", "instance"="i_2"} 2.0
🤯 Строк уже 6! Это не удивительно, ведь у нас два инстанса и каждый отправляет метрики. Смотрите, мы опять увеличили "размерность данных". 2 инстанса и 3 различных http кода дают нам количество строк равное 2*3=6.
💎 Обращайте внимание на размерность метрик. Не стоит делать лэйблами переменные, которые могут принимать значения из большого диапазона. Это сильно увеличит объем собираемых метрик. Если вы добавите к нашей метрике еще метаданные, обозначающие, скажем, некий идентификатор клиента, которых у вас 1000, то наши 6 строк превратятся в 6000. Сохранять и визуализировать такую метрику станет гораздо сложнее.
👀 Кстати, по нашей метрике уже видно, что второй инстанс ведет себя нехорошо, за прошедшее время, он только один раз успешно обслужил запрос :)
#metrics_basics
Please open Telegram to view this post
VIEW IN TELEGRAM
👍28🔥13❤5
📌 Metrics basics - часть 2
🔆 Что такое метрики мы немного обсудили. Но как собираемые нами измерения превращаются в полезные графики?
🦊 Представим, что у нас есть несколько инстансов (экземпляров) одного приложения. Само приложение обслуживает http-запросы от клиентов. И мы хотим где-то видеть график, который показывал бы нам сколько запросов в секунду обслуживает наш сервис.
🧵 Итак, цепочка выглядит следующем образом
1. Каждый инстанс ведет подсчет выполненных им запросов
2. Данные с каждого инстанса помещаются в некое хранилище
3. Мы создаем запрос к хранилищу, который подходит под наши требования (скажем, "сколько запросов в секунду обрабатывало наше приложение последние 3 часа")
4. Выполняя этот запрос в каком-либо инструменте для визуализации данных, получаем соответствующий график.
Конечно, реализация каждого пункта зависит от конкретных технологий, которые вы используете для сбора, хранения и визуализации метрик. Мы будем рассматривать все на примере Prometheus (сбор, хранение, запросы и агрегации) и соответствующих клиентских библиотек для JVM: Prometheus Java client и часто используемого со Spring Framework фасада для сбора метрик Micrometer.
🟢 Давайте в этом посте посмотрим на первый пункт нашего плана, как наш сервис будет подсчитывать и хранить значения метрики локально.
1️⃣ Для начала нам требуется создать некое локальное "хранилище" изменений. Это просто in-memory cache, где будут временно находится измеренные вами значения метрик (некоторые пока приложение не остановится, некоторые пока значения не будут отправлены в хранилище). Буду называть его Registry, как оно и зовется в наших библиотеках.
✅ В клиенте Prometheus для Java есть реджистри по-умолчанию, но вы можете создать его и сами:
✅ Micrometer же является фасадом, библиотека не привязана к конкретной реализации хранилища метрик, поэтому в ее составе есть целый ряд реализаций Registry, каждое из которых является адаптером для какой-то из систем сбора/хранения метрик. Т. к. нас интересует прометей, то будем использовать соответствующую реализацию:
✅ Вообще, если вы используете Spring-boot, то при наличии необходимой зависимости в classpath реджистри создадут и за вас :)
2️⃣ Окей, теперь у нас есть локальное хранилище для метрик и мы готовы их собирать. Для этого нам вполне подойдет тип метрики, который называется Counter, - обычный монотонно увеличивающийся счетчик выполненных http-запросов. Создаем:
✅ Заметьте, что для создания метрики я указал ее имя, справочную информацию, а также label, метаинформацию, о чем мы говорили в прошлом посте. Дело за малым, нужно использовать созданную переменную-счетчик, увеличивая ее после каждого выполненного http-запроса:
✅ Для Micrometer все чуточку сложнее, потому что он требует передавать ему значения метаинформации в момент создания, когда они еще не всегда известны. Поэтому напишем такую функцию:
☄️ Вот так достаточно просто начать производить замеры на стороне приложения. Расскажите в комментариях, какие вы используете библиотеки и хранилища для метрик.
Продолжаем обсуждать основы метрик? - 🔥
#metrics_basics
🔆 Что такое метрики мы немного обсудили. Но как собираемые нами измерения превращаются в полезные графики?
🦊 Представим, что у нас есть несколько инстансов (экземпляров) одного приложения. Само приложение обслуживает http-запросы от клиентов. И мы хотим где-то видеть график, который показывал бы нам сколько запросов в секунду обслуживает наш сервис.
🧵 Итак, цепочка выглядит следующем образом
1. Каждый инстанс ведет подсчет выполненных им запросов
2. Данные с каждого инстанса помещаются в некое хранилище
3. Мы создаем запрос к хранилищу, который подходит под наши требования (скажем, "сколько запросов в секунду обрабатывало наше приложение последние 3 часа")
4. Выполняя этот запрос в каком-либо инструменте для визуализации данных, получаем соответствующий график.
Конечно, реализация каждого пункта зависит от конкретных технологий, которые вы используете для сбора, хранения и визуализации метрик. Мы будем рассматривать все на примере Prometheus (сбор, хранение, запросы и агрегации) и соответствующих клиентских библиотек для JVM: Prometheus Java client и часто используемого со Spring Framework фасада для сбора метрик Micrometer.
🟢 Давайте в этом посте посмотрим на первый пункт нашего плана, как наш сервис будет подсчитывать и хранить значения метрики локально.
PrometheusRegistry myRegistry = new PrometheusRegistry();
val prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
Counter reqTotal = Counter.builder()
.name("http_requests_served")
.help("Total number of served http requests")
.labelNames("status_code")
.register();
reqTotal.labelValues(statusCode).inc()
fun increaseRequestCounter(status: String) = Counter
.builder("http_requests_served")
.description("Total number of served http requests")
.tags("status_code", status)
.register(registry).increase();
increaseRequestCounter(statusCode)
☄️ Вот так достаточно просто начать производить замеры на стороне приложения. Расскажите в комментариях, какие вы используете библиотеки и хранилища для метрик.
Продолжаем обсуждать основы метрик? - 🔥
#metrics_basics
Please open Telegram to view this post
VIEW IN TELEGRAM
1🔥25👍8
🟢 Metrics basics - часть 3
🔼 В прошлом посте под вторым пунктом плана значилось "Данные с каждого инстанса помещаются в некое хранилище".
❓ Но как именно метрики "помещаются" в хранилище? Есть два основных пути, по которому идут разработчики:
1️⃣ Клиент (ваш сервис, с которого вы хотите собирать метрики) устанавливает соединение с хранилищем и отправляет ему собранные данные.
2️⃣ Сервис выстявляет (expose) наружу API, с помощью которого можно получить собранные на текущий момент данные, а хранилище умеет опрашивать (poll) сервисы с некой регулярностью, "забирать" данные и сохранять у себя.
💡 Первый способ является стандартным для общения с базами данных, вероятно, поэтому большинство хранилищ работают именно по этому принципу. Но Prometheus не таков, он идет по второму пути. По этой ссылке можно найти классификацию метрик-систем, в том числе и по способу "публикации" метрик.
📂 Возвращаясь к нашему Прометею: процесс сбора метрик в его терминологии назваыется скрейпом (scrape), а собственно сервер, с которого собираются метрики, обозначается, как "цель" (target). В конфигурационном файле можно определить:
✏️ Параметры самой сборки (например, как часто ее производить / таймаут на процесс сборки). Тут же можно определить параметр
✍️ Какие сервера подлежат сборке (можно использовать статический набор серверов или использовать интеграцию с множеством service-discovery механизмов, которые будут возвращать вам набор код для скрейпа)
🏷️ Какие метки (labels) добавить/убрать/изменить в готовых метриках (например, на этом этапе очень удобно автоматически добавлять метку конкретного инстанса/адрес-порт, чтобы визуализировать метрики по каждому экземпляру сервиса индивидуально, видеть, где присутствует какая-то проблема и тд)
💡 В одном из следующих постов подумаем, каким образом визуализировать нашу метрику http-запросов, напишем запрос к прометею с помощью языка PromQL (Prometheus Query Language) и познакомимся с его полезными функциями.
#metrics_basics
🔼 В прошлом посте под вторым пунктом плана значилось "Данные с каждого инстанса помещаются в некое хранилище".
📂 Возвращаясь к нашему Прометею: процесс сбора метрик в его терминологии назваыется скрейпом (scrape), а собственно сервер, с которого собираются метрики, обозначается, как "цель" (target). В конфигурационном файле можно определить:
metrics_path - http-путь, по которому прометей сможет получить метрики, по-умолчанию это /metrics. Если вы используете Spring Actuator, то по-умолчанию значение будет /actuator/prometheus. Перейдя по данному пути вы сможете увидеть список метрик и их текущие значения, которые будут собираться прометеем:# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="heap",id="G1 Survivor Space",} 3382248.0
jvm_memory_used_bytes{area="heap",id="G1 Old Gen",} 3.2279552E7
jvm_memory_used_bytes{area="nonheap",id="Metaspace",} 4.8777184E7
jvm_memory_used_bytes{area="nonheap",id="CodeCache",} 1.1440768E7
jvm_memory_used_bytes{area="heap",id="G1 Eden Space",} 2.097152E7
jvm_memory_used_bytes{area="nonheap",id="Compressed Class Space",} 6788528.0
# HELP process_cpu_usage The "recent cpu usage" for the Java Virtual Machine process
# TYPE process_cpu_usage gauge
process_cpu_usage 0.008264797399683058
🏷️ Какие метки (labels) добавить/убрать/изменить в готовых метриках (например, на этом этапе очень удобно автоматически добавлять метку конкретного инстанса/адрес-порт, чтобы визуализировать метрики по каждому экземпляру сервиса индивидуально, видеть, где присутствует какая-то проблема и тд)
#metrics_basics
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥14👍9❤4
🚰 Разбавлю немного череду постов про метрики.
💪 Может быть, вам будет интересно посмотреть доклад Андрея Паньгина о своем детище: профайлере для JVM-based приложений Async-profiler.
🔥 Это не какой-то пет-проект, это развитый тул с большим количеством пользователей. Он много раз помогал нашей команде находить узкие места в рабочих проектах. Дополнительный плюс в том, что async-profiler потребляет сравнительно немного ресурсов. В общем, крутая штука
💪 Может быть, вам будет интересно посмотреть доклад Андрея Паньгина о своем детище: профайлере для JVM-based приложений Async-profiler.
🔥 Это не какой-то пет-проект, это развитый тул с большим количеством пользователей. Он много раз помогал нашей команде находить узкие места в рабочих проектах. Дополнительный плюс в том, что async-profiler потребляет сравнительно немного ресурсов. В общем, крутая штука
YouTube
Advanced performance analysis with async-profiler by Andrei Pangin
For updates and more, join our community 👉 https://www.linkedin.com/company/devoxx-united-kingdom
Thanks to its accuracy and low overhead, Async-profiler became a go-to tool for Java performance engineers. Besides regular CPU and heap allocation sampling…
Thanks to its accuracy and low overhead, Async-profiler became a go-to tool for Java performance engineers. Besides regular CPU and heap allocation sampling…
👍10🔥7❤5
Батчинг (пакетная обработка). Часть 1.
🏛 Ситуация: в моменты повышенной нагрузки время выполнения запросов к БД начинает расти и сказываться на производительности. Профайлер показывает вам, что значительную часть длительности операции занимает ожидание получения JDBC соединения. У вашего приложения уже 20 подключений к базе и больше подключений вам выделять не хотят. Что делать?
🏕 Осознаем природу проблемы:
🎼 Паттерн работы с JDBC подключением следующий
1. Запрашиваем свободное подключение из пула
2. Посылаем в него команду
3. Ожидаем, когда придет ответ из этого же подключения
4. Возвращаем его в пул
🛑 То есть коннекция блокируется на все время выполнения одного конкретного запроса. Давайте представим, что время на доставку запроса до базы по сети составляет
📦 Батчинг (пакетная обработка) - процесс выполнения нескольких запросов/команд в рамках одного сетевого запроса.
✅ Давайте отправлять в коннекцию не один запрос, а объединять запросы на вставку/обновление данных в “пачки” или “батчи” по 10 и отправлять вместе. Сетевые расходы на один “пакет” остаются более-менее неизменными, зато теперь вместо 10 сетевых обменов мы будем иметь всего один. Считаем: без батчинга
✅ Батчинг позволяет вам увеличить пропускную способность приложения. Однако, не стоит забывать и о том, что он может в худшую сторону повлиять на время выполнения отдельно взятой операции, ведь в нее будет входить время, затраченное на формирование пакета. Время формирование батча - один из важнейших параметров при реализации подхода наряду с размерами формируемых пакетов.
🚩 Пример выше не является чем-то каноническим. Применимость батчинга довольно ограничена, не для всех типов операций его легко реализовать. Но для insert/update операций он реализуется достаточно просто.
🧠 Буду переодически в постах рассматривать детали этого подхода, границы его применимости, связанные метрики и нюансы реализации.
#highload #architecture
🏛 Ситуация: в моменты повышенной нагрузки время выполнения запросов к БД начинает расти и сказываться на производительности. Профайлер показывает вам, что значительную часть длительности операции занимает ожидание получения JDBC соединения. У вашего приложения уже 20 подключений к базе и больше подключений вам выделять не хотят. Что делать?
🏕 Осознаем природу проблемы:
🎼 Паттерн работы с JDBC подключением следующий
1. Запрашиваем свободное подключение из пула
2. Посылаем в него команду
3. Ожидаем, когда придет ответ из этого же подключения
4. Возвращаем его в пул
🛑 То есть коннекция блокируется на все время выполнения одного конкретного запроса. Давайте представим, что время на доставку запроса до базы по сети составляет
40 ms, еще 40 ms занимает доставка ответа от базы. И только 20 ms занимает сам запрос. Получается из 100 ms мы тратим 80% на сетевое взаимодействие. Именно эти расходы мы и будем оптимизировать.📦 Батчинг (пакетная обработка) - процесс выполнения нескольких запросов/команд в рамках одного сетевого запроса.
100 ms * 10 = 1000 ms, с батчингом 40 ms + 20 ms * 10 + 40 ms = 280 ms. То есть вы сэкономили уже 72%!🚩 Пример выше не является чем-то каноническим. Применимость батчинга довольно ограничена, не для всех типов операций его легко реализовать. Но для insert/update операций он реализуется достаточно просто.
🧠 Буду переодически в постах рассматривать детали этого подхода, границы его применимости, связанные метрики и нюансы реализации.
#highload #architecture
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥31❤7👍5👌1
Батчинг (пакетная обработка). Часть 2.
🌐 Аналогично батчинг применяют для уменьшения сетевых расходов при HTTP взаимодействии (aka REST API). Проектируя свои сервисы и имея высокие требования по пропускной способности/времени отклика, вы можете предусмотреть использование батчинга. Скажем, у вас есть API для обновления статуса заказа по его id:
🌪 Давайте изменим API таким образом, чтобы можно было обновить статусы сразу для нескольких заказов в рамках одного запроса.
🍴 Это изменение позволяет не только сократить расходы на сетевое взаимодействие, но и использовать возможности параллелизма на стороне сервера. Например, если в предыдущем примере на сетевой обмен мы тратили❗️ Здесь стоит упомянуть, что, существует предел прироста производительности при параллелизации кода. Но в посте мы рассматриваем идеальную ситуацию. Если вам интересна эта тема, то можете прочитать пост про закон Амдала.
✍️ Важно отметить, что, поскольку батчинг является способом экономить именно на сетевых задержках, то его использование не даст сколько-нибудь значительный прирост при использовании HTTP2 и других протоколов, умеющих в мультиплексирование. Хотя и здесь нельзя совсем отметать экономию, например, на объеме метаданных.
🚨 При самостоятельной реализации батчинга, я бы советовал опираться на два параметра:
1. Максимальный размер батча, который должен быть отправлен. Пример: скопилось 50 команд - отправляем.
2. Максимальное время накопления батча. Например: прошло 30ms, за это время скопилось только 20 запросов - все равно отправляем.
⚖️ Нужно найти баланс между этими двумя параметрами. Чем больше батч, тем больше экономия на сети. Однако у слишком большого сетевого запроса тоже может возрастать время передачи в том числе из-за большей вероятности повторной пересылки пакетов. Второй параметр абсолютно необходим, чтобы не случалось ситуаций вечного накопления батча. Кроме того, он устанавливает максимальный оверхед для каждой отдельной операции при использовании батчинга.
☀️ Тема батчинга кажется мне настолько важной и базовой, что в одном из заданий моего курса по устойчивым системам студенты-выпускники ИТМО должны обнаружить потенциальное узкое место для его применения, и реализовать собственный батчер. Работаем над тем, чтобы запустить курс на широкую аудиторию💪
#highload #architecture
🌐 Аналогично батчинг применяют для уменьшения сетевых расходов при HTTP взаимодействии (aka REST API). Проектируя свои сервисы и имея высокие требования по пропускной способности/времени отклика, вы можете предусмотреть использование батчинга. Скажем, у вас есть API для обновления статуса заказа по его id:
POST /orders/{orderId}/status
Content-Type: application/json
{
"status": "delivered",
"timestamp": "2021-08-01T00:00:00Z"
}
🌪 Давайте изменим API таким образом, чтобы можно было обновить статусы сразу для нескольких заказов в рамках одного запроса.
POST /orders/status
Content-Type: application/json
{
"statuses": [
{
"orderId": "1",
"status": "delivered",
"timestamp": "2021-08-01T00:00:00Z"
},
….
]
}
🍴 Это изменение позволяет не только сократить расходы на сетевое взаимодействие, но и использовать возможности параллелизма на стороне сервера. Например, если в предыдущем примере на сетевой обмен мы тратили
80 ms, а на выполнение 20 ms, то для десяти отдельных запросов время выполнения составит (80 ms + 20 ms) * 10 = 1000 ms. А если объединим в батч размером 10 и выполним параллельно на стороне сервера, то время составит (80 ms + 20 ms) = 100 ms, что в 10!!! раз меньше, чем без батчинга. ✍️ Важно отметить, что, поскольку батчинг является способом экономить именно на сетевых задержках, то его использование не даст сколько-нибудь значительный прирост при использовании HTTP2 и других протоколов, умеющих в мультиплексирование. Хотя и здесь нельзя совсем отметать экономию, например, на объеме метаданных.
🚨 При самостоятельной реализации батчинга, я бы советовал опираться на два параметра:
1. Максимальный размер батча, который должен быть отправлен. Пример: скопилось 50 команд - отправляем.
2. Максимальное время накопления батча. Например: прошло 30ms, за это время скопилось только 20 запросов - все равно отправляем.
⚖️ Нужно найти баланс между этими двумя параметрами. Чем больше батч, тем больше экономия на сети. Однако у слишком большого сетевого запроса тоже может возрастать время передачи в том числе из-за большей вероятности повторной пересылки пакетов. Второй параметр абсолютно необходим, чтобы не случалось ситуаций вечного накопления батча. Кроме того, он устанавливает максимальный оверхед для каждой отдельной операции при использовании батчинга.
☀️ Тема батчинга кажется мне настолько важной и базовой, что в одном из заданий моего курса по устойчивым системам студенты-выпускники ИТМО должны обнаружить потенциальное узкое место для его применения, и реализовать собственный батчер. Работаем над тем, чтобы запустить курс на широкую аудиторию
#highload #architecture
Please open Telegram to view this post
VIEW IN TELEGRAM
👍19🔥6👌3❤1🤔1
Ждать вечно не лучший выбор
☠️ Помните задачку про перевод денег с одного аккаунта на другой? Одно из проблемных мест там - возможность взаимной блокировки (deadlock), когда первый поток выполняет перевод с аккаунта с id = 1 на аккаунт с id = 2, а второй поток переводит наоборот со второго на первый аккаунт. Соответственно, может возникнуть ситуация захвата ресурса “крест-накрест”. Один поток захватил блокировку на аккаунт 1 и ждет аккаунта 2, второй поток захватил второй аккаунт и ждет первого.
📌 На собеседовании могут спросить, как разрешить данную ситуацию. Каноничным ответом будет: “использовать иерархическую блокировку”. Суть подхода заключается в том, чтобы всегда захватывать и освобождать блокировки в одинаковом порядке. В данном случае, мы могли бы всегда сначала захватывать блокировку для аккаунта с меньшим ID, потом с большим. Это гарантирует отсутствие взаимной блокировки. Можете почитать про эту технику в интернете. Знать про иерархическую блокировку, конечно, стоит. Однако на практике я не видел ее использование в промышленном (не библиотечном) коде.
⏲ Есть более простая техника, которая по моему мнению должна применяться разработчиками гораздо чаще. Суть техники - не блокировать поток навечно в ожидании мьютекса. Большинство примитивов синхронизации включают в свой API методы, которые ограничивают максимальное время ожидания.
Пример 1: интерфейс
Аналогичный метод выставляет и
Пример 2: интерфейс
✅ Само по себе использование блокировок с ограниченным временем ожидания уже поможет вам избежать deadlock. И все, что для этого придется сделать - внести минимальное изменение в код, добавив желаемый таймаут.
❓ Может возникнуть логичный вопрос: “Но мне ведь нужен лок, а тут метод вернет false и что дальше?”. Если вам нужен лок, то вы можете пытаться захватить его в цикле (spin lock), делая несколько попыток. Таймаут даст вам возможность подсветить неудачные попытки захвата лока (логировать, писать метрики, сколько в среднем времени проходит до его успешного захвата). Иногда проблема заключается не в deadlock, а в банальном скоплении очереди на использование лока.
⚡️ Если проблема будет подсвечена, вы сможете перейти к ее решению более дорогостоящими, но и более эффективными методами. Например, реализовывать иерархическую блокировку.
🐕 Мой совет: поток - не Хатико, он не должен ждать вечно. Всегда используйте возможность ограничения максимального времени ожидания при использовании блокирующих 🔥 вызовов.
#java #kotlin #threads
☠️ Помните задачку про перевод денег с одного аккаунта на другой? Одно из проблемных мест там - возможность взаимной блокировки (deadlock), когда первый поток выполняет перевод с аккаунта с id = 1 на аккаунт с id = 2, а второй поток переводит наоборот со второго на первый аккаунт. Соответственно, может возникнуть ситуация захвата ресурса “крест-накрест”. Один поток захватил блокировку на аккаунт 1 и ждет аккаунта 2, второй поток захватил второй аккаунт и ждет первого.
📌 На собеседовании могут спросить, как разрешить данную ситуацию. Каноничным ответом будет: “использовать иерархическую блокировку”. Суть подхода заключается в том, чтобы всегда захватывать и освобождать блокировки в одинаковом порядке. В данном случае, мы могли бы всегда сначала захватывать блокировку для аккаунта с меньшим ID, потом с большим. Это гарантирует отсутствие взаимной блокировки. Можете почитать про эту технику в интернете. Знать про иерархическую блокировку, конечно, стоит. Однако на практике я не видел ее использование в промышленном (не библиотечном) коде.
⏲ Есть более простая техника, которая по моему мнению должна применяться разработчиками гораздо чаще. Суть техники - не блокировать поток навечно в ожидании мьютекса. Большинство примитивов синхронизации включают в свой API методы, которые ограничивают максимальное время ожидания.
Пример 1: интерфейс
java.util.concurrent.locks.Lock. Метод tryLock позволяет ограничивать максимальное время, за которое должна быть получена блокировка. Если этого не происходит, то метод возвращает false и поток может продолжать свое выполнение.boolean tryLock(long timeout, TimeUnit unit)
Аналогичный метод выставляет и
java.util.concurrent.Semaphore.Пример 2: интерфейс
java.util.concurrent.BlockingQueue. Методы offer и poll позволяют ограничить максимальное время ожидания при помещении и изъятии элемента из очереди соответственно.boolean offer(E e, long timeout, TimeUnit unit)
boolean poll(long timeout, TimeUnit unit)
✅ Само по себе использование блокировок с ограниченным временем ожидания уже поможет вам избежать deadlock. И все, что для этого придется сделать - внести минимальное изменение в код, добавив желаемый таймаут.
❓ Может возникнуть логичный вопрос: “Но мне ведь нужен лок, а тут метод вернет false и что дальше?”. Если вам нужен лок, то вы можете пытаться захватить его в цикле (spin lock), делая несколько попыток. Таймаут даст вам возможность подсветить неудачные попытки захвата лока (логировать, писать метрики, сколько в среднем времени проходит до его успешного захвата). Иногда проблема заключается не в deadlock, а в банальном скоплении очереди на использование лока.
⚡️ Если проблема будет подсвечена, вы сможете перейти к ее решению более дорогостоящими, но и более эффективными методами. Например, реализовывать иерархическую блокировку.
🐕 Мой совет: поток - не Хатико, он не должен ждать вечно. Всегда используйте возможность ограничения максимального времени ожидания при использовании блокирующих 🔥 вызовов.
#java #kotlin #threads
5👍24🔥11💯4❤2
К посту выше 📤
Вспомнил давний случай из практики, когда моя любовь к таймаутам на взятие лока вышла мне боком.
🚦 Понадобился мне для чего-то семафор. Напомню, semaphore - это такой мьютекс, который позволяет получать доступ к критической секции одновременно нескольким (N) потокам.
Поскольку проект на Kotlin coroutines, я использовал специальный корутиновый семафор и обнаружил, что в его API отсутствует метод, который позволяет передать таймаут и не блокироваться вечно на
🛑 Мысль понятная - в цикле вызываем обычный блокирующий корутину
⬇️ Кто хочет - подумайте сами, почему так случилось, дальше спойлеры.
⏱️ Функция withTimeout - пример кооперативныймногозадачности. Одна корутинка выставила условный флаг "cancelled", вторая в какой-то момент времени это заметила, да и завершилась. В моем случае, к сожалению, исключение TimeoutCancellationException иногда выбрасывалось ужетогда, когда семафор был захвачен, но мы все равно шли на второй круг захватывать его снова. В итоге все слоты семафора с течением времени были заблокированы и никем не освобождались..
🪤 Не один я попадал в эту ловушку, вот тред, где Роман Елизаров объясняет,что это ожидаемое поведение функции withTimeout и надо учитывать это при программировании на корутинах.
Вспомнил давний случай из практики, когда моя любовь к таймаутам на взятие лока вышла мне боком.
🚦 Понадобился мне для чего-то семафор. Напомню, semaphore - это такой мьютекс, который позволяет получать доступ к критической секции одновременно нескольким (N) потокам.
Поскольку проект на Kotlin coroutines, я использовал специальный корутиновый семафор и обнаружил, что в его API отсутствует метод, который позволяет передать таймаут и не блокироваться вечно на
acquire(). Остановить меня таким невозможно, поэтому я накидал какого-то крокодила-декоратора семафора, где метод acquire выглядел похоже на:suspend fun acquire() {
while (true) {
try {
withTimeout(1000) {
semaphoreDelegate.acquire()
}
return
} catch (e: TimeoutCancellationException) {
tracer.warn("Bla-bla, long wait...")
}
}
}
🛑 Мысль понятная - в цикле вызываем обычный блокирующий корутину
acquire , но ограничиваем его withTimeout, чтобы хотя бы зарепортить в логи о долгом ожидании. Непонятно тут другое, как я, имея немалый опыт программирования на корутинах, попался в эту ловушку: через какое-то время мой код намертво заблокировался и вообще перестал подавать признаки жизни.🪤 Не один я попадал в эту ловушку,
Please open Telegram to view this post
VIEW IN TELEGRAM
5🔥13👍8❤5
Head of line blocking
🔒 На собеседовании вас могут спросить, чем протокол HTTP2 лучше HTTP1. В этом посте я опишу проблему Head of line blocking (блокировка головы очереди), которая снижает потенциальную производительность HTTP1 и которую эффективно решает протокол следующей версии.
🐢🚶🏼♂️🚶🏻♀️🚶🏻 Опишем суть явления. Скажем, у вас есть очередь задач, которые должны выполняться последовательно друг за другом. Тогда, любая задача, требующая много времени на выполнение задерживает (на это же время) все последующие задачи в очереди. Это кажется очевидным? Но как это связано с HTTP и вообще, зачем это знать?
HTTP1 HOL (head of line) blocking
🚥 Запросы в HTTP1 обрабатываются в порядке FIFO: записываем запрос в TCP подключение, ждем, пока придет ответ и только получив его записываем следующий запрос. Можно сказать, что подключение “блокируется” на все время выполнения запроса.
🐢 При этом "среднее" время обработки запросов со стороны сервера может быть приемлемым, но любые "выбросы" требующие длительного времени на выполнение будут задерживать и увеличивать время выполнения всех кто стоит в очереди за ними. Конечно, это сильно снижает пропускную способность.
🏄🏻♀️ Есть оптимизации, например, pipelining, однако она никак не влияет на порядок обслуживания, а значит не сильно помогает в глобальном смысле, оптимизирует только транспортные расходы.
TCP HOL blocking
💡 Проблема не специфична именно для HTTP1, ее можно продемонстрировать и на примере TCP. Протокол TCP был создан, чтобы “ненадежная” сеть, которая может терять сетевые пакеты, умела становиться надежной. Для этого протокол умеет:
1. На стороне отправителя “нарезать” один большой массив данных на части и присваивать каждому из них свой порядковый номер.
2. На стороне получателя TCP “собирать” эти отдельные пакеты в единой целое в соответствии с “номерами” пакетов. Если на стороне отправителя у вас были пакеты
📡 Если TCP на стороне получателя “недосчитался” какого-то пакета, то он ждет, пока нужный пакет не будет передоставлен. Вот тут-то на сцене и появляется HOL blocking. Представьте, все пакеты от
⬇️ В следующем посте опишу, как HTTP2 решает проблему HOL на прикладном уровне и какой технический прием для этого используется.
#theory #interview
🔒 На собеседовании вас могут спросить, чем протокол HTTP2 лучше HTTP1. В этом посте я опишу проблему Head of line blocking (блокировка головы очереди), которая снижает потенциальную производительность HTTP1 и которую эффективно решает протокол следующей версии.
🐢🚶🏼♂️🚶🏻♀️🚶🏻 Опишем суть явления. Скажем, у вас есть очередь задач, которые должны выполняться последовательно друг за другом. Тогда, любая задача, требующая много времени на выполнение задерживает (на это же время) все последующие задачи в очереди. Это кажется очевидным? Но как это связано с HTTP и вообще, зачем это знать?
HTTP1 HOL (head of line) blocking
🚥 Запросы в HTTP1 обрабатываются в порядке FIFO: записываем запрос в TCP подключение, ждем, пока придет ответ и только получив его записываем следующий запрос. Можно сказать, что подключение “блокируется” на все время выполнения запроса.
🐢 При этом "среднее" время обработки запросов со стороны сервера может быть приемлемым, но любые "выбросы" требующие длительного времени на выполнение будут задерживать и увеличивать время выполнения всех кто стоит в очереди за ними. Конечно, это сильно снижает пропускную способность.
🏄🏻♀️ Есть оптимизации, например, pipelining, однако она никак не влияет на порядок обслуживания, а значит не сильно помогает в глобальном смысле, оптимизирует только транспортные расходы.
TCP HOL blocking
1. На стороне отправителя “нарезать” один большой массив данных на части и присваивать каждому из них свой порядковый номер.
2. На стороне получателя TCP “собирать” эти отдельные пакеты в единой целое в соответствии с “номерами” пакетов. Если на стороне отправителя у вас были пакеты
1, 2, 3, 4, 5, то и на стороне получателя все должно быть аналогично.📡 Если TCP на стороне получателя “недосчитался” какого-то пакета, то он ждет, пока нужный пакет не будет передоставлен. Вот тут-то на сцене и появляется HOL blocking. Представьте, все пакеты от
2 до 5 пришли, а 1 (то есть голова очереди) потерялся. Все остальные пакеты будут ждать, пока первый будет переправлен. Конечно, время обработки пакетов 2-5 увеличится на время переотправки пакета 1.#theory #interview
Please open Telegram to view this post
VIEW IN TELEGRAM
5👍36🔥15❤5🆒1