Сканирование HttpClient приводит к утечке памяти
Я работаю над реализацией WebCrawler, но столкнулся со странной утечкой памяти в HttpClient веб-API ASP.NET.
Таким образом, урезанная версия здесь:
[ОБНОВЛЕНИЕ 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.
Я использую C# 4.0, поэтому здесь нет асинхронного / ожидающего, поэтому используется только TPL 4.0.
Приведенный выше код работает, но не оптимизирован и иногда вызывает истерику, но этого достаточно для воспроизведения эффекта. Дело в том, что я не могу найти ни одной точки, которая могла бы вызвать утечку памяти.
4 ответа
Хорошо, я дошел до сути этого. Спасибо @Tugberk, @Darrel и @youssef за то, что потратили на это время.
По сути, первоначальная проблема заключалась в том, что я создавал слишком много задач. Это начало приносить свои плоды, поэтому мне пришлось урезать это и иметь некоторое состояние, чтобы убедиться, что количество одновременных задач ограничено. Это в основном большая проблема для написания процессов, которые должны использовать TPL для планирования задач. Мы можем контролировать потоки в пуле потоков, но нам также нужно контролировать задачи, которые мы создаем, поэтому нет уровня async/await
поможет это.
Мне удалось воспроизвести утечку только пару раз с этим кодом - в других случаях после увеличения он просто внезапно уменьшался. Я знаю, что в 4.5 было обновление GC, так что, возможно, проблема здесь в том, что GC недостаточно, хотя я смотрел счетчики перфокарт на коллекциях GC поколения 0, 1 и 2.
Таким образом, вынос здесь, что повторное использование HttpClient
НЕ вызывает утечку памяти.
Я плохо разбираюсь в проблемах с памятью, но я попробовал использовать следующий код. Он находится в.NET 4.5 и также использует функцию асинхронного ожидания / ожидания в C#. Похоже, что объем памяти для всего процесса остается на уровне 10–15 МБ (хотя не уверен, что вы считаете, что использование памяти лучше). Но если вы посмотрите # Gen 0 Collections, # Gen 1 Collections и # Gen 2 Collections, то они довольно высоки с приведенным ниже кодом.
Если вы удалите 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();
}
}
}
}
Недавнее сообщение об утечке памяти в нашей среде контроля качества научило нас этому:
Рассмотрим стек TCP
Не думайте, что стек TCP может выполнять то, что запрашивается во время, "подходящее для приложения". Конечно, мы можем выполнять задачи по своему желанию, и мы просто любим асыч, но....
Смотреть стек TCP
Запустите NETSTAT, если считаете, что у вас утечка памяти. Если вы видите остаточные сеансы или полусгоревшие состояния, вы можете переосмыслить свой дизайн в соответствии с повторным использованием HTTPClient и ограничением количества одновременной работы. Вам также может понадобиться использовать балансировку нагрузки на нескольких компьютерах.
Полузакрытые сеансы отображаются в NETSTAT с Fin-Waits 1 или 2 и Time-Waits или даже RST-WAIT 1 и 2. Даже "Установленные" сеансы могут быть практически мертвыми, просто ожидая истечения времени ожидания.
Стек и.NET скорее всего не сломаны
Перегрузка стека переводит машину в спящий режим. Восстановление занимает время и 99% времени стек восстанавливается. Помните также, что.NET не будет освобождать ресурсы раньше времени, и что ни один пользователь не имеет полного контроля над GC.
Если вы закрываете приложение и NETSTAT требует 5 минут, это довольно хороший знак того, что система перегружена. Это также хорошая демонстрация того, как стек не зависит от приложения.
По умолчанию HttpClient
утечки, когда вы используете его как недолговечный объект и создаете новые HttpClients для каждого запроса.
Вот воспроизведение этого поведения.
В качестве обходного пути я смог продолжать использовать HttpClient как недолговечный объект, используя следующий пакет Nuget вместо встроенного System.Net.Http
сборка: https://www.nuget.org/packages/HttpClient
Не уверен, что происхождение этого пакета, однако, как только я ссылался на него, утечка памяти исчезла. Убедитесь, что вы удалили ссылку на встроенный.NET System.Net.Http
библиотека и использовать пакет Nuget вместо.