Обход сканирования HttpClient приводит к утечке памяти
Я работаю над WebCrawler реализация, но столкнулся с странной утечкой памяти в ASP.NET Web API HttpClient.
Итак, вырезанная версия находится здесь:
[ОБНОВЛЕНИЕ 2]
Я нашел проблему, и это не утечка HttpClient. См. Мой ответ.
[ОБНОВЛЕНИЕ 1]
Я добавил команду без эффекта:
static void Main(string[] args)
{
int waiting = 0;
const int MaxWaiting = 100;
var httpClient = new HttpClient();
foreach (var link in File.ReadAllLines("links.txt"))
{
while (waiting>=MaxWaiting)
{
Thread.Sleep(1000);
Console.WriteLine("Waiting ...");
}
httpClient.GetAsync(link)
.ContinueWith(t =>
{
try
{
var httpResponseMessage = t.Result;
if (httpResponseMessage.IsSuccessStatusCode)
httpResponseMessage.Content.LoadIntoBufferAsync()
.ContinueWith(t2=>
{
if(t2.IsFaulted)
{
httpResponseMessage.Dispose();
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine(t2.Exception);
}
else
{
httpResponseMessage.Content.
ReadAsStringAsync()
.ContinueWith(t3 =>
{
Interlocked.Decrement(ref waiting);
try
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(httpResponseMessage.RequestMessage.RequestUri);
string s =
t3.Result;
}
catch (Exception ex3)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(ex3);
}
httpResponseMessage.Dispose();
});
}
}
);
}
catch(Exception e)
{
Interlocked.Decrement(ref waiting);
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(e);
}
}
);
Interlocked.Increment(ref waiting);
}
Console.Read();
}
Файл, содержащий ссылки, доступен здесь.
Это приводит к постоянному увеличению памяти. Анализ памяти показывает много байтов, которые могут быть у AsyncCallback. Ранее я делал много анализов утечки памяти, но этот, похоже, находится на уровне HttpClient.
![Memory profile of the process showing buffers held possibly by async callbacks]()
Я использую С# 4.0, поэтому нет async/await здесь, поэтому используется только TPL 4.0.
Приведенный выше код работает, но не оптимизирован, а иногда вызывает истерику, но для воспроизведения эффекта достаточно. Точка: я не могу найти точку, которая может вызвать утечку памяти.
Ответы
Ответ 1
Хорошо, я дошел до конца. Спасибо @Tugberk, @Darrel и @youssef за потраченное на это время.
В основном исходная проблема заключалась в том, что я порождал слишком много задач. Это начало снижаться, поэтому мне пришлось отказаться от этого и иметь некоторое состояние, чтобы убедиться, что количество одновременных задач ограничено. Это в основном большая проблема для написания процессов, которые должны использовать TPL для планирования задач. Мы можем управлять потоками в пуле потоков, но нам также нужно контролировать задачи, которые мы создаем, поэтому ни один уровень async/await
не поможет.
Мне удалось воспроизвести утечку всего пару раз с помощью этого кода - в других случаях после его роста он просто внезапно упадет. Я знаю, что была обновленная GC в 4.5, поэтому, возможно, проблема здесь в том, что GC не ударил достаточно, хотя я смотрел на перфомансы на сборниках GC 0, 1 и 2.
Таким образом, вынос здесь заключается в том, что повторное использование HttpClient
НЕ приводит к утечке памяти.
Ответ 2
Я не умею определять проблемы с памятью, но я дал ему попробовать следующий код. Он в .NET 4.5 и использует функцию async/await для С#. Кажется, что память использует около 10-15 МБ для всего процесса (не уверен, что вы видите, что это лучшее использование памяти, хотя). Но если вы наблюдаете # Gen 0 Collections, # Gen 1 Collections и # Gen 2 Collections perf counters, они довольно высоки с приведенным ниже кодом.
Если вы удалите приведенные ниже вызовы GC.Collect
, он будет перемещаться между 30 МБ и 50 МБ для всего процесса. Интересная часть заключается в том, что, когда я запускаю свой код на своей 4-ядерной машине, я тоже не вижу аномального использования памяти этим процессом. У меня установлен .NET 4.5 на моем компьютере, и если вы этого не сделаете, проблема может быть связана с внутренними средами CLR.NET 4.0, и я уверен, что TPL значительно улучшился на .NET 4.5 на основе использования ресурсов.
class Program {
static void Main(string[] args) {
ServicePointManager.DefaultConnectionLimit = 500;
CrawlAsync().ContinueWith(task => Console.WriteLine("***DONE!"));
Console.ReadLine();
}
private static async Task CrawlAsync() {
int numberOfCores = Environment.ProcessorCount;
List<string> requestUris = File.ReadAllLines(@"C:\Users\Tugberk\Downloads\links.txt").ToList();
ConcurrentDictionary<int, Tuple<Task, HttpRequestMessage>> tasks = new ConcurrentDictionary<int, Tuple<Task, HttpRequestMessage>>();
List<HttpRequestMessage> requestsToDispose = new List<HttpRequestMessage>();
var httpClient = new HttpClient();
for (int i = 0; i < numberOfCores; i++) {
string requestUri = requestUris.First();
var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
Task task = MakeCall(httpClient, requestMessage);
tasks.AddOrUpdate(task.Id, Tuple.Create(task, requestMessage), (index, t) => t);
requestUris.RemoveAt(0);
}
while (tasks.Values.Count > 0) {
Task task = await Task.WhenAny(tasks.Values.Select(x => x.Item1));
Tuple<Task, HttpRequestMessage> removedTask;
tasks.TryRemove(task.Id, out removedTask);
removedTask.Item1.Dispose();
removedTask.Item2.Dispose();
if (requestUris.Count > 0) {
var requestUri = requestUris.First();
var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
Task newTask = MakeCall(httpClient, requestMessage);
tasks.AddOrUpdate(newTask.Id, Tuple.Create(newTask, requestMessage), (index, t) => t);
requestUris.RemoveAt(0);
}
GC.Collect(0);
GC.Collect(1);
GC.Collect(2);
}
httpClient.Dispose();
}
private static async Task MakeCall(HttpClient httpClient, HttpRequestMessage requestMessage) {
Console.WriteLine("**Starting new request for {0}!", requestMessage.RequestUri);
var response = await httpClient.SendAsync(requestMessage).ConfigureAwait(false);
Console.WriteLine("**Request is completed for {0}! Status Code: {1}", requestMessage.RequestUri, response.StatusCode);
using (response) {
if (response.IsSuccessStatusCode){
using (response.Content) {
Console.WriteLine("**Getting the HTML for {0}!", requestMessage.RequestUri);
string html = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
Console.WriteLine("**Got the HTML for {0}! Legth: {1}", requestMessage.RequestUri, html.Length);
}
}
else if (response.Content != null) {
response.Content.Dispose();
}
}
}
}
Ответ 3
Недавний отчет "Утечка памяти" в нашей среде QA научил нас этому:
Рассмотрим стек TCP
Не предполагайте, что стек TCP может делать то, что задано в то время, "считающееся подходящим для приложения". Конечно, мы можем отталкивать Задачи по своему усмотрению, и мы просто любим асих, но....
Смотрите TCP-стек
Запустите NETSTAT, если у вас есть утечка памяти. Если вы видите остаточные сеансы или полупеченные состояния, вы можете переосмыслить свой дизайн в соответствии с повторным использованием HTTPClient и ограничить объем одновременной работы. Вам также может потребоваться использовать балансировку нагрузки на нескольких компьютерах.
Получистые сессии отображаются в NETSTAT с помощью Fin-Waits 1 или 2 и Time-Waits или даже RST-WAIT 1 и 2. Даже сеансы "Установленные" могут быть практически мертвы, просто ожидая, чтобы тайм-ауты загорелись.
Стеки и .NET скорее всего не сломаны
Перегрузка стека заставляет машину спать. Восстановление требует времени и 99% времени, когда стек восстановится. Помните также, что .NET не будет выпускать ресурсы до своего времени и что никакой пользователь не имеет полного контроля над GC.
Если вы убьете приложение, и для NETSTAT потребуется 5 минут, это довольно хороший знак, когда система перегружена. Это также хорошее представление о том, как стек не зависит от приложения.
Ответ 4
По умолчанию HttpClient
утечка, когда вы используете его как непродолжительный объект и создаете новые HttpClients для каждого запроса.
Здесь является воспроизведение этого поведения.
В качестве обходного пути я смог использовать HttpClient как непродолжительный объект, используя следующий пакет Nuget вместо встроенной сборки System.Net.Http
:
https://www.nuget.org/packages/HttpClient
Не уверен, что происхождение этого пакета, однако, как только я на него ссылался, пропала утечка памяти. Убедитесь, что вы удалите ссылку на встроенную библиотеку .NET System.Net.Http
и вместо этого используйте пакет Nuget.