Фэйл с RabbitMQ и Kubernetes API
Сегодня будет немного личная история, но с моралью в конце. На самом деле заголовок не совсем точный, но на момент, когда произошел описываемый казус, всё выглядело именно так.
Ранее я давал пояснения, почему Kafka плохо подходит для случаев, когда время обработки однотипных сообщений существенно варьируется. По этой причине мы сделали выбор в пользу Queue-based-брокера и начали миграцию с Kafka на RabbitMQ. Поскольку для нас не была важна история событий, а работа с очередью была инкапсулирована в рамках соответствующего порта, технический переход на RabbitMQ прошел быстро и безболезненно. Самое интересное нас ждало впереди.
Спустя какое-то время после релиза мы начали замечать, что некоторые обработчики стали отваливаться с ошибкой Delivery Acknowledgement Timeout. Если коротко, то данная ошибка происходит в том случае, если сообщение, взятое из очереди, не получило подтверждения обработки спустя длительное время (по умолчанию 30 минут). В этом случае сообщение возвращается обратно в очередь брокера, соединение со старым обработчиком разрывается, и происходит переназначение обработчика сообщения.
Всё выглядело очень странно, и мы никак не могли добиться воспроизведения злосчастного бага. Код обработчика не менялся очень давно, поэтому все подозрения пали на RabbitMQ. Сначала мы перепроверили Helm Chart нашего кластера RabbitMQ; параллельно с этим перепотрошили все настройки RabbitMQ-клиента. В итоге сделали более тонкую и точную конфигурацию, которая соответствовала всем справочным рекомендациям по работе с RabbitMQ. Не скажу, что всё было напрасно, но ситуацию это не исправило.
В тот момент, когда я начал замечать косые взгляды и намеки в стиле: «A вот с Kafka у нас всё работало, пока ты не пришёл со своим RabbitMQ!» — я понял, что дело не в брокере, а в самом обработчике. Я сделал концептуально правильный выбор, он полностью соответствовал нашим текущим потребностям, и в этом не было никаких сомнений. А раз дело в обработчике, значит, нужно искать виновника зависаний. Поскольку я хорошо знал алгоритм обработки, я практически сразу понял, с чем это может быть связано...
Во время обработки происходит обращение к Kubernetes API (такова специфика нашей прикладной задачи). И вот на этом шаге всё и зависало, поскольку не был установлен таймаут ожидания результатов вызова. Дело в том, что для вызова Kubernetes API мы используем fabric8io/kubernetes-client, у которого, как выяснилось, по умолчанию был установлен некоторый Rate Limit. Превышение этого лимита приводило к зависанию при обращении к Kubernetes API. В результате проблема была решена добавлением таймаута ожидания результатов, подбором и установкой подходящих значений для Rate Limit, выносом этих настроек в конфигурационный файл приложения.
По итогу я был рад не столько тому, что нашёл и исправил проблему, сколько тому, что моё архитектурное решение было правильным, а возникшая ситуация подтвердила это ещё раз. В Kafka всё работало, и проблема не проявлялась только потому, что она не приспособлена к ситуациям, когда время обработки может варьироваться. В итоге пропускная способность системы на базе Kafka была ниже, мы никогда не упирались в Rate Limit для Kubernetes API и даже не знали о существовании этой проблемы. С переходом на RabbitMQ пропускная способность увеличилась, проблема материализовалась, опустившись на тот уровень, где она всегда и была, а мы стали упираться в Rate Limit.
Из этой истории можно извлечь, как минимум, два урока.
- Если в коде приложения есть синхронные обращения к сторонним службам, то всегда нужно контролировать время ожидания (timeout). Также не забывать и про другие шаблоны устойчивости, а именно: не злоупотреблять шаблоном "Повтор" (Retry), в нужные моменты использовать "Предохранитель" (Circuit Breaker), и понимать, что "Ограничитель скорости" (Rate Limiter) может быть установлен с двух сторон (на клиенте и сервере). Однажды я не уделил этому должного внимания, спровоцировав неприятности в дальнейшем.
- Не нужно сомневаться в том, как на концептуальном уровне работают инструменты, в частности брокеры. Если что-то идет не так, проблема может быть в развертывании, но, скорей всего, в коде приложения. Нужно убедиться, что настройки клиентского драйвера выполнены в соответствии с рекомендациями; не осталось ни одной настройки по умолчанию, в которых у вас есть сомнения. Я начал поиски проблемы не с того и поплатился временем.
Делитесь своими фэйлами, будем учиться на ошибках друг друга. 😃
P.s. Если вам интересна данная тематика, присоединяйтесь к моей новостной ленте в Telegram или здесь. Буду рад поделиться опытом. ;-)