Kubernetes, Feign і Spring Boot 3.5.5: як апгрейд зламав балансування навантаження
В нас є Kubernetes кластер і в ньому умовно 2 мікросервіси про які піде мова. Один працює, як composite api і відправляє запит на internal api. SLA композита десь в районі 1000 запитів на секунду. Комопзита у нас 8 реплік, а інтернал сервісу 12 з можливим скейлінгом до 24. Інтернал мікросервіс виставляє API через сервіс кубера з типом ClusterIP.
І ось у нас була задача про масовий апгрейд мікросервісів на Spring Boot 3.5.5 (з 2.7.4).
Після апгрейду проводили навантажувальний тест на стейджі і QA помітили, що помітно зросло використання CPU на internal api (але ще не достатньо зріс, щоб відпрацював скейлінг) й від того зріс 95 процентиль респонс тайму. Здавалося би, як тут може впливати апгрейд спринг бута...
Так от, після аналізу метрик з’ясувалося, що проблема була викликана нерівномірним навантаженням на internal api: деякі репліки отримували у

Розслідування показало, що після міграції на Spring Boot 3.5.5 порушилася конфігурація Spring Cloud Feign — HTTP-клієнта для виклику внутрішніх сервісів з composite api. Через неправильну конфігурацію Feign почав використовувати стандартну реалізацію низькорівневого HTTP-клієнта — feign.Client.Default. Цей клієнт отримує з’єднання через такий код:
public HttpURLConnection getConnection(final URL url) throws IOException {
return (HttpURLConnection) url.openConnection();
}Попри назву, метод openConnection не створює нове з’єднання при кожному виклику. Натомість він повторно використовує TCP-з’єднання операційної системи, яке створюється один раз і живе до закінчення TTL.
Це підтверджує документація HttpURLConnection HttpURLConnection (Java Platform SE 8 )
«Each HttpURLConnection instance is used to make a single request but the underlying network connection to the HTTP server may be transparently shared by other instances. Calling the close() methods on the InputStream or OutputStream of an HttpURLConnection after a request may free network resources associated with this instance but has no effect on any shared persistent connection. Calling the disconnect() method may close the underlying socket if a persistent connection is otherwise idle at that time.»
Стандартний Feign-клієнт не викликає disconnect після виклику, тому з’єднання використовується повторно.
Балансування навантаження в Kubernetes працює на рівні TCP-з’єднань, а не окремих HTTP-запитів. Коли з’єднання встановлене з конкретним подом, всі наступні запити через це з’єднання йдуть до того самого поду. Оскільки з’єднання кешується, більшість запитів потрапляє на одну репліку, створюючи перекіс навантаження.
Рішення: додали залежність Apache HTTP Client 5 (більш потужний і гнучкий клієнт, який Feign автоматично використовує, якщо знаходить його в classpath.
Ще додатково, налаштували TTL з’єднання на 5 секунд, щоб запити на довго не прив’язувалися до одного поду.
Результат: значно рівномірніший розподіл навантаження та використання CPU.

Для мене було відкриттям, що правильний http клієнт теж має значення для рівномірного розподілу навантаження, і що не можна просто сподіватися на сервіс кубера, що він усе магічно розрулить.
На усе розслідування пішов 1 день, і ось як це відбувалося (для тих кому цікаво):
Хід розслідування
- Побачивши такий дивний розподіл, я відразу подумав, що щось не так з feign на композиті.
Ми підіймали його версію під час апгрейду Spring Boot, тож я подумав, що можливо змінилась його дефолтна конфігурація. - Порадившись з Claude прийшов до висновку, що ймовірно треба сконфігурувати TTL для з’єднань у пулі.
Сконфігурував, перезапустив тест — нічого не змінилося. - Тоді я почав дебажити feign в спринговому інтеграційному тесті, щоб переконатися, що конфігурація TTL застосовується, і побачив, що використовується примітивний клієнт feign.Client.Default.
- Подивився, що для відкриття з’єднання у нас url.openConnection() і на цьому моменті я дуже засмутився, адже виходить що начебто нове з’єднання має бути під кожен запит, а отже моя теорія була не вірною.
- Попитав Claude про конфігурацію Service в кубері — нічого цікавого не знайшлось, крім підтвердження того, як саме відбувається балансування.
- На цьому робочий день закінчився, і я спантеличений виключив ноут, але проблема не давала спокою.
- Того ж дня ввечері знову повернувся до проблеми, подивився документацію по HttpURLConnection, і все почало ставало на свої місця!
- В чому ж була помилка конфігурації feign? В нових версіях ми вони перейшли за замовчуванням на клієнт Apache HTTP 5, і якщо його нема в classpath, то брався feign.Client.Default. А не було його тому, що ми використовували раніше 4 версію. Тож я прибрав залежність на 4, і додав залежність на 5.
- Після цього в дебагу інтеграційного тесту впевнився, що клієнт підхопився, і конфіг TTL для з’єднання застосовується.

Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів