Подтвердить что ты не робот

Утечка памяти при использовании DirectorySearcher.FindAll()

У меня есть длительный процесс, который часто требует много запросов в Active Directory. Для этого я использовал пространство имен System.DirectoryServices, используя классы DirectorySearcher и DirectoryEntry. Я заметил утечку памяти в приложении.

Его можно воспроизвести с помощью этого кода:

while (true)
{
    using (var de = new DirectoryEntry("LDAP://hostname", "user", "pass"))
    {
        using (var mySearcher = new DirectorySearcher(de))
        {
            mySearcher.Filter = "(objectClass=domain)";
            using (SearchResultCollection src = mySearcher.FindAll())
            {
            }            
         }
    }
}

Документация для этих классов говорит, что они будут утечки памяти, если Dispose() не вызывается. В этом случае я попытался без использования, он просто утечки больше. Я тестировал это с обеих версий 2.0 и 4.0. Кто-нибудь сталкивался с этим раньше? Есть ли способы обхода?

Обновление: я попытался запустить код в другой AppDomain, и он тоже не помог.

4b9b3361

Ответ 1

Как ни странно, кажется, что утечка памяти происходит только в том случае, если вы ничего не делаете с результатами поиска. Изменение кода в вопросе следующим образом не утечки памяти:

using (var src = mySearcher.FindAll())
{
   var enumerator = src.GetEnumerator();
   enumerator.MoveNext();
}

Это, по-видимому, вызвано внутренним полем searchObject, имеющим ленивую инициализацию, глядя на SearchResultCollection с Reflector:

internal UnsafeNativeMethods.IDirectorySearch SearchObject
{
    get
    {
        if (this.searchObject == null)
        {
            this.searchObject = (UnsafeNativeMethods.IDirectorySearch) this.rootEntry.AdsObject;
        }
        return this.searchObject;
    }
}

Утилита не будет закрывать неуправляемый дескриптор, если инициализируется searchObject.

protected virtual void Dispose(bool disposing)
{
    if (!this.disposed)
    {
        if (((this.handle != IntPtr.Zero) && (this.searchObject != null)) && disposing)
        {
            this.searchObject.CloseSearchHandle(this.handle);
            this.handle = IntPtr.Zero;
        }
    ..
   }
}

Вызов MoveNext на ResultsEnumerator вызывает объект SearchObject в коллекции, таким образом, он также правильно настроен.

public bool MoveNext()
{
  ..
  int firstRow = this.results.SearchObject.GetFirstRow(this.results.Handle);
  ..
}

Утечка в моем приложении была вызвана тем, что какой-то другой неуправляемый буфер не был выпущен должным образом, и тест, который я сделал, вводит в заблуждение. Теперь проблема решена.

Ответ 2

Управляемая оболочка ничего не течет. Если вы не вызываете Dispose, неиспользуемые ресурсы все равно будут исправлены во время сбора мусора.

Однако управляемый код является оболочкой поверх COM-интерфейса ADSI API, и при создании DirectoryEntry базовый код вызовет ADsOpenObject функция. Возвращаемый COM-объект освобождается, когда DirectoryEntry находится или завершается.

При использовании API ADsOpenObject вместе с набором учетных данных и провайдером WinNT существует документированная утечка памяти.

  • Эта утечка памяти происходит во всех версиях Windows XP, Windows Server 2003, Windows Vista, Windows Server 2008, Windows 7 и Windows Server 2008 R2.
  • Эта утечка памяти происходит только при использовании поставщика WinNT вместе с учетными данными. Поставщик LDAP не теряет память таким образом.

Однако утечка составляет всего 8 байтов, и насколько я вижу, вы используете поставщик LDAP, а не поставщик WinNT.

Вызов DirectorySearcher.FindAll будет выполнять поиск, требующий значительной очистки. Эта очистка выполняется в DirectorySearcher.Dispose. В вашем коде эта очистка выполняется на каждой итерации цикла, а не во время сбора мусора.

Если в LDAP ADSI API действительно отсутствует недокументированная утечка памяти, единственным объяснением, которое я могу придумать, является фрагментация неуправляемой кучи. API ADSI реализуется COM-сервером in-process, и каждый поиск, вероятно, выделяет некоторую память на неуправляемой куче вашего процесса. Если эта память будет фрагментирована, куча может увеличиться, когда пространство будет выделено для новых поисков.

Если моя гипотеза верна, одним из вариантов будет запуск поисков в отдельном AppDomain, который затем может быть восстановлен, чтобы выгрузить ADSI и переработать память. Однако, хотя фрагментация памяти может увеличить потребность в неуправляемой памяти, я бы ожидал, что будет верхний предел того, сколько неуправляемой памяти требуется. Если, конечно, у вас нет утечки.

Кроме того, вы можете попробовать поиграть с DirectorySearcher.CacheResults property. Устанавливает ли это значение false удаление утечки?

Ответ 3

Из-за ограничений реализации класс SearchResultCollection не может освободить все свои неуправляемые ресурсы, когда он собрал мусор. Чтобы предотвратить утечку памяти, вы должны вызвать метод Dispose, когда объект SearchResultCollection больше не нужен.

http://msdn.microsoft.com/en-us/library/system.directoryservices.directorysearcher.findall.aspx

EDIT:

Я смог воспроизвести кажущуюся утечку с помощью perfmon и добавить счетчик для частных байтов в имя процесса тестового приложения (Experiments.vshost для меня)

счетчик личных байтов будет постоянно расти, пока приложение зацикливается, оно начинается около 40 000 000, а затем растет примерно на миллион байт каждые несколько секунд. Хорошей новостью является то, что счетчик возвращается к нормальному состоянию (35,237,888), когда вы завершаете приложение, поэтому после этого происходит какая-то очистка.

Я прикрепил скриншот того, что выглядит perfmon, когда его утечка perfmon screenshot of memory leak

Update:

Я пробовал несколько обходных решений, например, отключение кэширования на объекте DirectoryServer, и это не помогло.

Команда FindOne() не утечка памяти, но я не уверен, что вам нужно сделать, чтобы эта опция работала для вас, возможно, постоянно редактируйте фильтр на моем контроллере AD, есть только один домена, так что findall и findone дают тот же результат.

Я также попытался поставить в очередь 10 000 работников threadpool, чтобы сделать тот же DirectorySearcher.FindAll(). Он завершился намного быстрее, однако он все еще просочился в память, и фактически частные байты выросли примерно до 80 МБ, вместо 48 МБ для "нормальной" утечки.

Итак, для этой проблемы, если вы можете заставить FindOne() работать для вас, у вас есть обходной путь. Удачи!

Ответ 4

Вы пробовали using и Dispose()? Информация из здесь

Update

Попробуйте позвонить de.Close(); до конца использования.

На самом деле у меня нет Active Active Directory для проверки этого, извините.

Ответ 5

Нашел быстрый и грязный способ обойти это.

У меня была аналогичная проблема в моей программе с ростом памяти, но с помощью изменения .GetDirectoryEntry(). Свойства ( "cn" ). Значение для

.GetDirectoryEntry(). Свойства ( "cn" ). Value.ToString с помощью команды if before, чтобы убедиться, что значение не было null

Я смог сообщить GC.Collect, чтобы избавиться от временного значения в моем foreach. Похоже, что .value фактически оставил объект живым, а затем позволял ему собираться.