MCP

вторник, 9 апреля 2019 г.

Странная реализация HttpClient в .NET Core

Представьте что у вас есть приложение. Не очень маленькое, но и не гигантское, работает помаленьку развивается, и тут вы совершенно случайно замечаете в мониторинге, что один из сервисов жрёт 25% CPU. Хм... вроде многовато, но мало ли что. Может боты набежали на сайт и смотрят все странички, или пользователи ходят. Да и 25% немного, запас огромный (400% максимум). Но на всякий случай сервис перепускается и загрузка падает до 1%. Вы думаете — какой-то глюк, но через некоторое время загрузка процессора растёт опять.

Вот с подобной ситуацией я столкнулся несколько недель назад. Есть какая-то утечка CPU (не памяти, а именно процессора), при этом ни истерики в логах ни каких-то явных знаков, указывающих на проблему. Приложение, хоть и не очень большое, но делает кучу разных вещей, отправляет письма, общается через собственную API-платформу, активно кеширует данные, связывается с другими частями и внешними сервисами, генерирует документы, в общем точек для анализа очень много. А утечка небольшая, несколько процентов в день, да и эти несколько процентов в моменте могут быть настоящей нагрузкой от живых пользователей. Т.е. даже не ясно как поступать с профилированием. Да и на деве это не повторяется.

Когда я в очередной раз сдался, я не выдержал, и подцепился дебаггером к боевому серверу напрямую. И... ничего не понял, всё выглядит нормально. Отцепился — загрузка 0%. Тут у меня уже задёргался глаз. Через некоторое время повторил — да, подключение дебаггера явно лечит проблему, как и перезапуск приложения. Но это ведь не выход и не решение. А вдруг это какая-то проблема у нас, и если в этом приложении данную проблему можно замаскировать, то она может выстрелить где-то ещё. Так что пришлось разбираться дальше.

Неожиданно удалось нагуглить данный тред на GitHub'е. Проблема у Microsoft'овской библиотеки и связана она с HttpClient. Утверждалось, что нельзя его создавать на каждый HTTP запрос, а надо использовать статичный. Правда в документации есть пример использования:

В котором его заворачивают в using. Я попробовал покопать в данном направлении и выяснил первую забавную вещь: использование статичного HttpClient в .NET Core быстрее раз в 5, чем создавать его на каждый запрос. Но тут нюансы с самим временем запроса, но смысл в том, что уже есть причина использовать статичную версию. При этом под "большой" .NET разницы нет никакой.

В общем, выложил новую версию приложения, но проблема осталась. (может я не туда копаю?). Попробовал переключиться с .NET Core 2.1 на 2.0 — проблема ушла. Т.е. проблема есть только под core, под Linux и версия должна быть больше чем 2.0 (более свежие не проверял). Читаем документацию и видим, как разработчики Microsoft, светясь от гордости, рекламируют новую реализацию Http Client Factory, через которую и идут все запросы (не может Microsoft просто написать библиотеку, делающие HTTP запросы, внутри обязана быть магия!). В общем теперь всё managed, в 10 раз лучше и в 50 раз быстрее. Ага, и сделано как раз в 2.1

Прикидываю, где ещё скрылись HttpClient'ы и нахожу библиотечку для отправки Push-уведомлений, которая внутри себя создаёт данный класс. Собственно, поэтому в первый раз и не было найдено это использование. Переделываю использование данной библиотеки и проблема исчезает! Всё работает замечательно.

Тут я начинаю громко материться, но нахожу в документации небольшую приписку:
Т.е. они знают о данной проблеме! Но явно про это написать было стыдно, так что засунули всё в сноску, зато не будет никаких претензий от разработчиков. Сноска есть? Есть! Значит читайте внимательнее.

Получается совершенно странная ситуация: Disposable-класс, который надо использовать синглтоном. При том, что все предыдущие реинкарнации подобных классов были честными Disposable-объектами. Но тут решили сделать по-другому. Зачем? Я пока не понял. Но свинью подложили знатную. Представьте, у вас есть базовая библиотека, написанная под большой .NET, вы её мигрируете на Core, пользуетесь под Windows, всё нормально, а потом, спустя длительное время замечаете, что приложение жрёт много процессора. Вам нужно будет пройти квест, подобный моему, чтобы найти проблему, переписать библиотеку и весь остальной код, обновить всё что нужно и всё из-за странного архитектурного решения разработчиков из Microsoft.