Кейс с собеседования
⬇️ Ниже приведен фрагмент кода. Представьте, что этот код работает у вас на продакшене прямо сейчас.
Один поток
🦥 В какой-то момент вы замечаете, что маппинг не происходит: программа перестает писать в файл info-логи. В логах нет никаких ошибок. Вы проверяете ваш java-процесс с помощью
👩🏻⚕️ Вы не знаете воспроизведется ли в следующий раз такая ситуация, поэтому не хотите останавливать процесс. Ваша задача придумать способ провести диагностику происходящего "на лету", здесь и сейчас. Что бы вы сделали?
🥟 Не спешите открывать ответ, подумайте, какие варианты вы бы могли предложить на собеседовании. Пишите ваши варианты в комментарии. Ответ, который я ожидал бы услышать:thread dump. Подробности в следующем посте.
💡 Попробуйте проанализировать код, попытайтесь найти обстоятельства, которые привели к симптомам, описанным выше. Какие изменения вы бы внесли в код для предотвращения подобных ситуаций? Пишите в комменты!
#java #kotlin #threads #case #interview
⬇️ Ниже приведен фрагмент кода. Представьте, что этот код работает у вас на продакшене прямо сейчас.
Один поток
Producer генерирует последовательность целых чисел. Несколько потоков Consumer получают эти числа из очереди и производят маппинг этих чисел на другие.class Consumer(
private val codesQueue: BlockingQueue<Int>,
private val codeMappings: List<Int>
) {
@Volatile
var isActive: Boolean = true
fun consume() {
while (isActive || codesQueue.isNotEmpty()) {
val code = codesQueue.take()
log.info("Code $code mapped to ${codeMappings[code]}")
// do something with errorCodeMapping
}
}
}
class Producer(private val codesQueue: BlockingQueue<Int>) {
@Volatile
var isActive: Boolean = true
fun supply() {
while (isActive) {
codesQueue.put(Random().nextInt())
}
}
}
🦥 В какой-то момент вы замечаете, что маппинг не происходит: программа перестает писать в файл info-логи. В логах нет никаких ошибок. Вы проверяете ваш java-процесс с помощью
ps -aux | grep java, он жив — то есть программа не завершилась.👩🏻⚕️ Вы не знаете воспроизведется ли в следующий раз такая ситуация, поэтому не хотите останавливать процесс. Ваша задача придумать способ провести диагностику происходящего "на лету", здесь и сейчас. Что бы вы сделали?
🥟 Не спешите открывать ответ, подумайте, какие варианты вы бы могли предложить на собеседовании. Пишите ваши варианты в комментарии. Ответ, который я ожидал бы услышать:
💡 Попробуйте проанализировать код, попытайтесь найти обстоятельства, которые привели к симптомам, описанным выше. Какие изменения вы бы внесли в код для предотвращения подобных ситуаций? Пишите в комменты!
#java #kotlin #threads #case #interview
🔥11👍5❤3🤝1
Thread dump и анализ кейса
📸 Дампом потоков называют снимок состояния всех текущих потоков JVM. Он очень полезен для диагностики проблем с производительностью, блокировками, утечками ресурсов. Дамп представляет из себя текстовый файл, где для каждого из существующих потоков java-процесса вы можете найти его состояние (RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED) и полный стек вызовов.
⬆️ Вспоминаем ситуацию, описанную в предыдущем посте:
🔧 В этом случае для диагностики вы можете прибегнуть к thread dump. Примеры утилит, которые помогут вам сделать снимок потоков: jstack, jvisualvm, JMC (Java Mission Control), jcmd. Ниже пример команды для утилиты jstack, на вход необходимо передать
🧨 Ниже приведен фрагмент thread dump, представляющий интересующий нас поток-producer.
🧐 Как мы видим, поток находится в состоянии перманентного (не ограниченного временем) ожидания (
🔍 Очень подозрительно, но мы не можем найти дамп потока
🕵🏽 Поток завершился в следствие появления неожиданного исключения. Это могло произойти в следующей строке:
🤯 А исключения мы могли не увидеть в логах по нескольким причинам. Например, вызывающий нас код мог поймать исключение, завершить поток и не записать об этом в лог. Или же был использован базовый обработчик исключения в потоке (
🥇 Какие выводы мы можем сделать?
1. Следует оборачивать происходящее в потоке в глобальный обработчик исключений try-catch, логировать перехваченные исключения и поддерживать работу потока, чтобы приложение могло прогрессировать даже вопреки ошибочным ситуациям.
2. Следует явно ограничивать временем методы, которые блокируют поток исполнения или использовать их варианты/альтернативы с таймаутом!
#java #kotlin #threads #interview #case
📸 Дампом потоков называют снимок состояния всех текущих потоков JVM. Он очень полезен для диагностики проблем с производительностью, блокировками, утечками ресурсов. Дамп представляет из себя текстовый файл, где для каждого из существующих потоков java-процесса вы можете найти его состояние (RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED) и полный стек вызовов.
⬆️ Вспоминаем ситуацию, описанную в предыдущем посте:
Программа перестает писать info-логи в файл. В логах нет никаких ошибок. Вы проверяете ваш java-процесс с помощью ps -aux | grep java, - он жив, то есть программа не завершилась.
🔧 В этом случае для диагностики вы можете прибегнуть к thread dump. Примеры утилит, которые помогут вам сделать снимок потоков: jstack, jvisualvm, JMC (Java Mission Control), jcmd. Ниже пример команды для утилиты jstack, на вход необходимо передать
pid java-процесса.jstack <pid> > threaddump.txt
🧨 Ниже приведен фрагмент thread dump, представляющий интересующий нас поток-producer.
"Thread-0" #16 daemon prio=5 os_prio=31 cpu=0.97ms elapsed=3.79s tid=0x00007fd90d0ed800 nid=0x5f03 waiting on condition [0x000070000a578000]
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@17.0.9/Native Method)
. . . . . . .
at java.util.concurrent.LinkedBlockingQueue.put(java.base@17.0.9/LinkedBlockingQueue.java:343)
at ru.quipy.Producer.supply(Example.kt:30)
at ru.quipy.ExampleKt.main$lambda-4(Example.kt:48)
at ru.quipy.ExampleKt$$Lambda$32/0x0000000132014658.run(Unknown Source)
at java.lang.Thread.run(java.base@17.0.9/Thread.java:840)
java.lang.Thread.State: WAITING) в методе LinkedBlockingQueue.put. Если вы находитесь на собеседовании, то можете сказать, что лучше использовать аналогичный метод LinkedBlockingQueue.offer, который не блокируется навечно, а использует ограниченную временем блокировку, что даст нам возможность выйти из ожидания, понять, что операция не удалась и сделать об этом запись в лог. Аналогично и для метода LinkedBlockingQueue.take: вообще все блокирущие методы стоит ограничивать временем, тогда не возникнет ситуация с блокировкой потока при завершении программы, которую вы описали в комментах к предыдущему посту.🔍 Очень подозрительно, но мы не можем найти дамп потока
consumer из чего может следовать вывод, что такового в JVM нет. Вероятно, данный поток уже завершился. Это объясняет, почему он больше не делает записи в логи и почему поток supplier не может поместить ничего в очередь: она достигла максимального размера из-за отсутствия потока-потребителя. 🕵🏽 Поток завершился в следствие появления неожиданного исключения. Это могло произойти в следующей строке:
log.info("Consumed $code. Mapped to ${codeMappings[code]}"), если элемента с индексом code в списке не существует.interface UncaughtExceptionHandler), дефолтная версия которого (class ThreadGroup) просто делает запись в System.err, который мог не перенаправляться в лог.1. Следует оборачивать происходящее в потоке в глобальный обработчик исключений try-catch, логировать перехваченные исключения и поддерживать работу потока, чтобы приложение могло прогрессировать даже вопреки ошибочным ситуациям.
2. Следует явно ограничивать временем методы, которые блокируют поток исполнения или использовать их варианты/альтернативы с таймаутом!
#java #kotlin #threads #interview #case
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥18👍4❤2🤝1
👨🏻🎓 Всем привет! Меня зовут Андрей Суховицкий. Я tech lead и лектор Университета ИТМО. На этом канале я рассказываю про backend. С предложениями тем, фидбеком пишите: @sukhoa
🔥 Основные темы: kotlin, java, coroutines, многопоточное программирование, system design, реализация высоконагруженных и надежных систем.
Вы можете проголосовать за канал
_______________________________
Мой Youtube-канал
Мой курс по event-sourcing. Промокод TG_TEAM на -30%. Сейчас доступен только тариф без ментора.
_______________
Навигация:
#java, #kotlin - посты, релевантные для разработчика на этих языках
#threads - многопоточка и другое полезное о потоках
#highload - все, связанное с реализацией высоконагруженных систем
#coroutines - связанное с kotlin-coroutines, их применением и реализацией
#queues - кейсы, связанные с различными видами очередей - in-memory, брокеры и тд
#case - описание реальных ситуаций и их траблшутинг
#interview - потенциальные вопросы на собеседовании, анализ, разбор
#metrics_basics - основы сбора, хранения и визуализации метрик, prometheus
🔥 Основные темы: kotlin, java, coroutines, многопоточное программирование, system design, реализация высоконагруженных и надежных систем.
Вы можете проголосовать за канал
_______________________________
Мой Youtube-канал
Мой курс по event-sourcing. Промокод TG_TEAM на -30%. Сейчас доступен только тариф без ментора.
_______________
Навигация:
#java, #kotlin - посты, релевантные для разработчика на этих языках
#threads - многопоточка и другое полезное о потоках
#highload - все, связанное с реализацией высоконагруженных систем
#coroutines - связанное с kotlin-coroutines, их применением и реализацией
#queues - кейсы, связанные с различными видами очередей - in-memory, брокеры и тд
#case - описание реальных ситуаций и их траблшутинг
#interview - потенциальные вопросы на собеседовании, анализ, разбор
#metrics_basics - основы сбора, хранения и визуализации метрик, prometheus
Telegram
Канал Андрея про бекенд
Проголосуйте за канал, чтобы он получил больше возможностей.
🤝7🔥1
Канал Андрея про бекенд pinned «👨🏻🎓 Всем привет! Меня зовут Андрей Суховицкий. Я tech lead и лектор Университета ИТМО. На этом канале я рассказываю про backend. С предложениями тем, фидбеком пишите: @sukhoa 🔥 Основные темы: kotlin, java, coroutines, многопоточное программирование, system…»
Давайте потокам осмысленные имена
🚨 Это архиважная вещь в промышленной разработке на java. Вы могли заметить, что необходимый нам поток из поста выше 👆🏼носит название
🔍 Крупные сервисы могут иметь сотни и даже тысячи потоков. Может быть очень сложно найти среди них именно те, что нужны вам. Но задача будет гораздо проще, если вы будете давать осмысленные имена потокам и пулам потоков, которые работают над одним типом задач.
🧵 Давайте начнем с одиночных потоков. Конечно, мы не часто создаем потоки через конструктор класса Thread, но полезно знать, что этот конструктор позволяет вам дать потоку имя.
🏭Теперь давайте перейдем к пулам потоков. Статический метод
🚲 На вход классу🥇 🥈 🥉 . Теперь давайте передадим эту фабрику пулу:
🎯 Вуаля, теперь в thread dump ваши потоки будут иметь осмысленные имена и вам не придется тратить уйму времени на изучение стека ненужных потоков!
#java #kotlin #threads
🚨 Это архиважная вещь в промышленной разработке на java. Вы могли заметить, что необходимый нам поток из поста выше 👆🏼носит название
Thread-0 . Я без труда нашел его в thread dump только потому, что программа была мала. Но даже ее дамп включал 21 поток, подавляющее большинство которых было служебными потоками JVM (в основном потоками сборщика мусора).🔍 Крупные сервисы могут иметь сотни и даже тысячи потоков. Может быть очень сложно найти среди них именно те, что нужны вам. Но задача будет гораздо проще, если вы будете давать осмысленные имена потокам и пулам потоков, которые работают над одним типом задач.
🧵 Давайте начнем с одиночных потоков. Конечно, мы не часто создаем потоки через конструктор класса Thread, но полезно знать, что этот конструктор позволяет вам дать потоку имя.
val producerThread = Thread(null, { producer.supply() }, "producer-thread")
🏭Теперь давайте перейдем к пулам потоков. Статический метод
Executors.newFixedThreadPool, через который удобно создавать пулы потоков, принимает на вход необходимый размер пула и еще один интересный параметр с типом ThreadFactory. У этого интерфейса всего один метод Thread newThread(Runnable task). Пул потоков делегирует переданному объекту ThreadFactory задачу создания потоков. Давайте реализуем свой вариант ThreadFactory, который именует потоки необходимым образом и передадим такой объект нашему пулу:class NamedThreadFactory(private val prefix: String) : ThreadFactory {
private val sequence = AtomicInteger(1)
override fun newThread(r: Runnable): Thread {
val thread = Thread(r)
val seq = sequence.getAndIncrement()
thread.name = prefix + (if (seq > 1) "-$seq" else "")
return thread
}
}
🚲 На вход классу
NamedThreadFactory передаем строку, которая будет являться префиксом имени всех потоков, созданных этой фабрикой. С помощью атомарного каунтера sequence мы сможем создавать уникальную часть имени для наших потоковval httpCallsExecutor = Executors.newFixedThreadPool(16, NamedThreadFactory(“http-calls-pool”))
🎯 Вуаля, теперь в thread dump ваши потоки будут иметь осмысленные имена и вам не придется тратить уйму времени на изучение стека ненужных потоков!
#java #kotlin #threads
Please open Telegram to view this post
VIEW IN TELEGRAM
👍16🔥4🤝1
Использование Thread.sleep для тестирования асинхронных программ
📲 Тестируем сервис отправки push-уведомлений. Внешние процессы вызвают его, чтобы отправить push-уведомление c заданным текстом на мобильное устройство. Сервис возвращает идентификатор уведомления, по которому можно проверить его статус. Само уведомление может быть отправлено позже, для этого наш сервис совершает http-вызов к внешнему провайдеру.
Как будем тестировать?
👺 Если тестирование модульное, то, вероятно, мы хотим сделать
⏳ Между вызовом сервиса и проверкой прошло очень мало времени, запрос мог провести его в буферах-очередях, ждать выполнения http-вызова, или выполнения запроса в БД. Вероятность этого еще выше, если тест интеграционный - сразу после самбита уведомления мы пытаемся получить его статус из базы данных, но он не успевает измениться.
⏰ Чтобы сделать тест "зеленым", частенько выбирают некоторую “взятую с потолка” константу, например, 5 секунд и помещают инструкцию, которая заставляет поток "уснуть", между вызовом сервиса и проверкой:
Как правило, это решает проблему прохождения теста, но генерирует несколько новых:
1. Пяти секунд может быть недостаточно в определенных обстоятельствах и тест будет падать, но не всегда, а, например, в 1% случаев. Не так много, чтобы переписать тест, но достаточно, чтобы с завидной регулярностью фейлить весь билд и раздражать😤 . То есть вы своими руками делаете свои тесты недетерминированными, flucky тестами.
2. Вторая причина часто более заметна для команды. Инструкция🤔 .
Ставьте 🔥, если интересно прочитать про решение данной проблемы. Кто уже сталкивался - кидайте в комментарии ваши методы и интересные библиотеки, которые используются в вашей команде.
#kotlin #java #async
📲 Тестируем сервис отправки push-уведомлений. Внешние процессы вызвают его, чтобы отправить push-уведомление c заданным текстом на мобильное устройство. Сервис возвращает идентификатор уведомления, по которому можно проверить его статус. Само уведомление может быть отправлено позже, для этого наш сервис совершает http-вызов к внешнему провайдеру.
interface NotificationService {
fun submitNotification(deviceId: UUID, text: String): UUID
}
Как будем тестировать?
👺 Если тестирование модульное, то, вероятно, мы хотим сделать
mock для http-client-a, с помощью которого будет посылаться уведомление и проверить, что один из его методов был вызван. Аналогично можно сделать mock классов доступа к базе данных и проверить, что статус оповещения был обновлен. Однако, если написать такой код, то может оказаться, что тест в большом количестве случаев не проходит:notificationService.submitNotification(deviceId, “Hello”)
verify(dbService, times(1)).updateStatus(any());
⏳ Между вызовом сервиса и проверкой прошло очень мало времени, запрос мог провести его в буферах-очередях, ждать выполнения http-вызова, или выполнения запроса в БД. Вероятность этого еще выше, если тест интеграционный - сразу после самбита уведомления мы пытаемся получить его статус из базы данных, но он не успевает измениться.
⏰ Чтобы сделать тест "зеленым", частенько выбирают некоторую “взятую с потолка” константу, например, 5 секунд и помещают инструкцию, которая заставляет поток "уснуть", между вызовом сервиса и проверкой:
notificationService.submitNotification(deviceId, “Hello”)
Thread.sleep(5000L) // 5000 milliseconds
verify(dbService, times(1)).updateStatus(any());
Как правило, это решает проблему прохождения теста, но генерирует несколько новых:
1. Пяти секунд может быть недостаточно в определенных обстоятельствах и тест будет падать, но не всегда, а, например, в 1% случаев. Не так много, чтобы переписать тест, но достаточно, чтобы с завидной регулярностью фейлить весь билд и раздражать
2. Вторая причина часто более заметна для команды. Инструкция
Thread.sleep(5000L) выставляет нижнюю границу выполнения теста равной пяти секундам, тогда как в удачном сценарии тест мог занять 10, 50, 100, 200 миллисекунд, секунду. Что, если у вас 200 тестов? Время выполнения 1000 секунд или около 16 минут, тогда как в удачном сценарии ваш билд мог занять во много раз меньше - 1, 2 … 4 минуты. Часто проблема нарастает очень плавно с увеличением количества тестов и неопытные команды не могут понять, в чем причина увеличения времени сборки Ставьте 🔥, если интересно прочитать про решение данной проблемы. Кто уже сталкивался - кидайте в комментарии ваши методы и интересные библиотеки, которые используются в вашей команде.
#kotlin #java #async
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥68❤2👍1🤝1
Как “не спать” при тестировании асинхронных программ.
🛌 В предыдущем посте я описал мотивацию использования
⏳ Проблему для нас создает тот факт, что мы не знаем, сколько времени в действительности займет тестируемая операция (от выполнения к выполнению ее длительность может варьироваться). Поэтому приходится подбирать некую константу, которая в большинстве случаев превышает максимальную длительность операции. Пример: отправка оповещения в среднем занимает около 150ms, но в 3% случаев превышает 5 секунд.
⏰ Нам бы хотелось, чтобы поток ожидал примерно столько же, сколько в действительности выполняется тестируемая операция. Если ее фактическая длительность 150ms, то поток должен находиться в ожидании (150ms + некоторая фиксированная константа, например, еще 50ms). Если операция выполнялась 5s, то и время ожидания должно быть 5s + 50ms.
Для этого необходим механизм, умеющий с заданной периодичностью проверять является ли некоторое условие истинным. Если условие не выполняется, то “засыпать” на короткий промежуток времени, если условие выполняется, то выходить из ожидания.
🔔 Пример: клиент отправляет уведомление. Мы хотим протестировать, что после отправки его статус в базе данных будет обновлен на
Давайте посмотрим на пример кода, где в качестве такого механизма используется библиотека Awaitility.
⚙️ У библиотеки есть большое количество конфигурируемых параметров. Например:
1. Период времени между проверкой условия (poll interval)
2. Начальная задержка перед первой проверкой
3. Минимальное время ожидания (например, вы хотите, чтобы оповещение отправлялось не раньше, чем через 5 секунд)
4. Максимальное время ожидания, за которое ваше условие должно выполниться
5. Игнорируемые исключения
6. Пул потоков, на котором будет выполняться тестирование
🐈⬛ В библиотеке также присутствует модуль для Kotlin, который помогает использовать API библиотеки в конструкциях, похожих на естественный язык, используя инфиксные функции.
🏋️♀️ Вы можете для тренировки написать собственный небольшой инструмент для ожидания (на java, kotlin, kotlin-coroutines) и поделиться кодом в комментариях 😊
#java #kotlin #async
🛌 В предыдущем посте я описал мотивацию использования
Thread.sleep при тестировании асинхронного кода и связанные с этим проблемы. Однако, пассивное ожидание потока очень помогает нам при тестировании асинхронного кода, ведь тесту нужно дождаться завершения асинхронной операции, чтобы начать проводить различные проверки.Thread.sleep(5_000) даст нам зеленый тест в c 97% вероятностью.Для этого необходим механизм, умеющий с заданной периодичностью проверять является ли некоторое условие истинным. Если условие не выполняется, то “засыпать” на короткий промежуток времени, если условие выполняется, то выходить из ожидания.
processed. Будем с периодичностью в 50ms делать запрос в базу данных, получать статус и, если он еще не изменен на processed, то ждать следующие 50ms. Нам станет известно, что статус обновился, через кратчайший промежуток времени.Давайте посмотрим на пример кода, где в качестве такого механизма используется библиотека Awaitility.
id = service.submitNotification(deviceId, “Hello”)
with()
.pollInterval(50_MILLISECONDS)
.await()
.atMost(8, SECONDS)
.until(notificationStatusIsUpdated());
1. Период времени между проверкой условия (poll interval)
2. Начальная задержка перед первой проверкой
3. Минимальное время ожидания (например, вы хотите, чтобы оповещение отправлялось не раньше, чем через 5 секунд)
4. Максимальное время ожидания, за которое ваше условие должно выполниться
5. Игнорируемые исключения
6. Пул потоков, на котором будет выполняться тестирование
#java #kotlin #async
Please open Telegram to view this post
VIEW IN TELEGRAM
👍25🔥8👎3
Траблшутинг кейс
💣 У вас есть простой сервис, который принимает входящие http-запросы, в среднем около 1к rps (requests per second). Он добавляет к запросу небольшое количество метаданных и пересылает по HTTP далее, в следующий сервис.
👀 Вы заметили, что сервис стал хуже справляться с пиковыми нагрузками: 6k rps. Симптомы следующие:
1. При увеличении входящего трафика начинают скапливаться внутренние очереди
2. Время выполнения http запроса в следующий сервис возрастает в разы или даже на порядки. (В норме среднее
3. Возрастает значение метрики количества используемых JVM файловых дескрипторов (рост примерно со 100 до 170).
4. Возрастают также и другие показатели: RAM, CPU.
🔎 Давайте исследовать:
1. Подозрение падает на следующий в цепочке сервис. Мы предполагаем, что под нагрузкой время его ответа увеличивается, и это отражается на нашем приложении.
🏓Команда сервиса опровергает эту догадку, предъявляя нам метрики, где видно: среднее время обработки запроса на их стороне оставалось стабильным в течение всего времени.
🧑🏻🔬 Мы проводим и свой (не самый репрезентативный) эксперимент - во время пиковой нагрузки, используя какой-нибудь http-client, например,
2. Рост файловых дескрипторов. Вспоминаем, что в Linux TCP коннекция является файлом, то есть с ростом числа подключений растет и количество захваченных дескрипторов.
📡 Кто может раздувать количество соединений? - Http-client. Зачем? Из-за особенности протокола
3. Зная эту особенность HTTP, давайте считать пропускную способность:
📲 Длительность запроса
4. Мы можем посчитать, сколько нужно подключений, чтобы сервис выдержал 6к запросов без накапливания очереди.
⚖️ Итак
⚙️ Что можно сделать
1. Увеличить количество подключений
2. Перейти на протокол HTTP версии 2. Он лишен такого недостатка, как блокирующиеся коннекции. С помощью процесса мультиплексирования HTTP 2 дает возможность одновременно передавать и получать данные большого количества запросов, используя даже единственное соединение. Но переход на него в данном случае возможен только, если вы можете перевести на него и следующий в цепочке сервис, что не всегда в вашей власти.
Ставьте 🔥, если такой формат постов кажется вам интересным :)
#http #case #interview
💣 У вас есть простой сервис, который принимает входящие http-запросы, в среднем около 1к rps (requests per second). Он добавляет к запросу небольшое количество метаданных и пересылает по HTTP далее, в следующий сервис.
1. При увеличении входящего трафика начинают скапливаться внутренние очереди
2. Время выполнения http запроса в следующий сервис возрастает в разы или даже на порядки. (В норме среднее
30ms, при нагрузке начинает нарастать и растет вплоть до 20-30s)3. Возрастает значение метрики количества используемых JVM файловых дескрипторов (рост примерно со 100 до 170).
4. Возрастают также и другие показатели: RAM, CPU.
🔎 Давайте исследовать:
1. Подозрение падает на следующий в цепочке сервис. Мы предполагаем, что под нагрузкой время его ответа увеличивается, и это отражается на нашем приложении.
🏓Команда сервиса опровергает эту догадку, предъявляя нам метрики, где видно: среднее время обработки запроса на их стороне оставалось стабильным в течение всего времени.
🧑🏻🔬 Мы проводим и свой (не самый репрезентативный) эксперимент - во время пиковой нагрузки, используя какой-нибудь http-client, например,
curl, делаем несколько запросов к их сервису с нашей машины и видим, что время ответа около 30ms.2. Рост файловых дескрипторов. Вспоминаем, что в Linux TCP коннекция является файлом, то есть с ростом числа подключений растет и количество захваченных дескрипторов.
📡 Кто может раздувать количество соединений? - Http-client. Зачем? Из-за особенности протокола
HTTP1. Запросы в нем блокируют подключение: пока один запрос не будет выполнен, следующий запрос не отправляется.3. Зная эту особенность HTTP, давайте считать пропускную способность:
📲 Длительность запроса
30ms, то есть одно подключение может за секунду обрабатывать 1000ms/30ms≈33.3 запроса. Чтобы удовлетворить обычную нагрузку на сервер в 1к, нужно около 1000rps/33.3req ≈ 30 подключений к следующему сервису. Мы знаем, что у нас было 100 дескрипторов, но не знаем, какие из них были подключениями.4. Мы можем посчитать, сколько нужно подключений, чтобы сервис выдержал 6к запросов без накапливания очереди.
6000 / 33.33 будет ≈ 180 подключений. У нас же есть около 170 (и мы не знаем, сколько из них действительно наши). В любом случае у нас будет накапливаться очередь внутри http-client, в которой запросы могут проводить долгое время, особенно в период повышенной нагрузки.1. Увеличить количество подключений
per host. Почти у всех http-клиентов есть возможность ограничить количество подключений к одному хосту. Часто это совсем небольшое значение. Например, в OkHttp client или AsyncHttpClient оно по умолчанию равно 5. И все клиенты (почти) дают возможность выставлять кастомное значение этого параметра, например:AsyncHttpClientConfig.Builder.setMaxConnectionsPerHost(maxConnectionsPerHost)
2. Перейти на протокол HTTP версии 2. Он лишен такого недостатка, как блокирующиеся коннекции. С помощью процесса мультиплексирования HTTP 2 дает возможность одновременно передавать и получать данные большого количества запросов, используя даже единственное соединение. Но переход на него в данном случае возможен только, если вы можете перевести на него и следующий в цепочке сервис, что не всегда в вашей власти.
Ставьте 🔥, если такой формат постов кажется вам интересным :)
#http #case #interview
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥118👍8🤝1
Недавно у меня возникла следующая задача: необходимо запросить данные у трех сервисов, но достаточно получить первый валидный ответ от любого их них, остальные 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