在使用PrincipalSearcher.FindAll()时存在内存泄漏问题。

6
我也有一个使用插件和应用程序域的长期运行服务,由于使用directoryservices而导致内存泄漏。请注意,我正在使用system.directoryservices.accountmanagement,但我理解它使用相同的底层ADSI API,因此容易出现相同的内存泄漏问题。
我查看了所有CLR内存计数器,发现内存并没有泄漏,在强制GC或卸载应用程序域时,内存都会被释放。泄漏是在私有字节中不断增长。我在这里搜索过一些与ADSI API相关的内存泄漏问题,但它们似乎表明仅迭代directorysearcher即可解决问题。但是,如下所示的代码中,我正在使用foreach块进行迭代,但内存仍在泄漏。有什么建议吗?以下是我的方法:
public override void JustGronkIT()
{
    using (log4net.ThreadContext.Stacks["NDC"].Push(GetMyMethodName()))
    {
        Log.Info("Inside " + GetMyMethodName() + " Method.");
        System.Configuration.AppSettingsReader reader = new System.Configuration.AppSettingsReader();
        //PrincipalContext AD = null;
        using (PrincipalContext AD = new PrincipalContext(ContextType.Domain, (string)reader.GetValue("Domain", typeof(string))))
        {
            UserPrincipal u = new UserPrincipal(AD);
            u.Enabled = true;
            //u.Surname = "ju*";
            using (PrincipalSearcher ps = new PrincipalSearcher(u))
            {
                myADUsers = new ADDataSet();
                myADUsers.ADUsers.MinimumCapacity = 60000;
                myADUsers.ADUsers.CaseSensitive = false;
                foreach (UserPrincipal result in ps.FindAll())
                {
                     myADUsers.ADUsers.AddADUsersRow(result.SamAccountName, result.GivenName, result.MiddleName, result.Surname, result.EmailAddress, result.VoiceTelephoneNumber,
                            result.UserPrincipalName, result.DistinguishedName, result.Description);
                 }
                 ps.Dispose();
            }
            Log.Info("Number of users: " + myADUsers.ADUsers.Count);
            AD.Dispose();
            u.Dispose();
        }//using AD
    }//Using log4net
}//JustGronkIT

我对foreach循环做了以下修改,效果更好了,但私有字节仍然增长,并且永远不会被回收。
 foreach (UserPrincipal result in ps.FindAll())
 {
     using (result)
     {
         try
         {
             myADUsers.ADUsers.AddADUsersRow(result.SamAccountName, result.GivenName,           result.MiddleName, result.Surname, result.EmailAddress, result.VoiceTelephoneNumber,                                        result.UserPrincipalName, result.DistinguishedName, result.Description);
             result.Dispose();
         }
         catch
         {
             result.Dispose();
         }
     }
 }//foreach

你还应该处理ps.FindAll()返回的Principal对象。或者只在PrincipalSearcher上调用dispose是否足够? - Jehof
你怎么知道有泄漏?你在测量什么?如果你逐步取出和添加东西,行为何时发生? - n8wrl
顺便说一下,当对象在使用语句内实例化时,您不需要显式处理它们 - 请参阅此处。 - Alex Peck
@n8wrl 我正在测量所有的CLR内存计数器,这些计数器保持稳定,不会增长,并且在卸载应用程序域时被释放和回收。然而,私有字节会增长,直到第6次迭代时,它的内存超过1 GB。 - Richard
6个回答

12

我也像你一样写了类似的东西,结果出现了严重的内存泄漏问题...

                foreach (GroupPrincipal result in searcher.FindAll())
                {
                    results.Add(result.Name);
                }

但是诀窍在于FindAll本身返回一个必须被处理的对象...

            using (var searchResults = searcher.FindAll())
            {
                foreach (GroupPrincipal result in searchResults)
                {
                    results.Add(result.Name);
                }
            }

1
我应该补充一下,无论你处理GroupPrincipals还是UserPrincipals,这都适用,并且据我所知,在循环内部放置using并不需要,因为像我建议的那样处理迭代器会释放整个结果集。 - Xyzzy
我们在使用FindAll方法时遇到了相同的问题,即使我们对集合中的每个Principal对象(*PrincipalSearchResult <Principal>*)进行了释放,但它并没有释放该集合。在循环遍历每个Principal结果时,我们也发现了相同的泄漏问题-我们在使用GroupPrincipal.GetMembers()方法时出现了泄漏。 - SliverNinja - MSFT
太棒了!使用FindAll的结果也为我解决了这个泄漏问题。 - Karlth

3

我知道这个已知的泄漏,可以在这里看到:https://dev59.com/uG035IYBdhLWcg3wGL5T?rq=1 但是那似乎表明仅使用 foreach 遍历结果集就可以修复泄漏,而这正是我正在做的,然而每个迭代中私有字节仍然会增长。 - Richard
真正的答案是使用System.DirectoryServices.Protocols,避免System.DirectoryServices和System.DirectoryService.AccountManagement中固有的内存泄漏问题。 - Richard

2

我说得太早了,仅仅是对Dispose()方法进行强制调用并不能长期解决问题。真正的解决方案是停止使用directoryservices和directoryservices.accountmanagement,而是使用System.DirectoryServices.Protocols,并对我的域进行分页搜索,因为该程序集没有泄漏。

按照要求,这里有一些代码来说明我想出的解决方案。请注意,我还使用了插件架构和appDomain,并在完成后卸载了appDomain,但我认为由于DirectoryServices.Protocols中没有泄漏,您不必这样做。我只是这样做是因为我以为使用appDomains会解决我的问题,但由于这不是托管代码中的泄漏,而是非托管代码中的泄漏,所以它没有起到任何作用。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.DirectoryServices.Protocols;
using System.Data.SqlClient;
using System.Data;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Text.RegularExpressions;
using log4net;
using log4net.Config;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.IO;

namespace ADImportPlugIn {

    public class ADImport : PlugIn
    {

        private ADDataSet myADUsers = null;
        LdapConnection _LDAP = null;
        MDBDataContext mdb = null;
        private Orgs myOrgs = null;

        public override void JustGronkIT()
        {
            string filter = "(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))";
            string tartgetOU = @"yourdomain.com";
            string[] attrs = {"sAMAccountName","givenName","sn","initials","description","userPrincipalName","distinguishedName",
            "extentionAttribute6","departmentNumber","wwwHomePage","manager","extensionName", "mail","telephoneNumber"};
            using (_LDAP = new LdapConnection(Properties.Settings.Default.Domain))
            {
                myADUsers = new ADDataSet();
                myADUsers.ADUsers.MinimumCapacity = 60000;
                myADUsers.ADUsers.CaseSensitive = false;

                try
                {
                    SearchRequest request = new SearchRequest(tartgetOU, filter, System.DirectoryServices.Protocols.SearchScope.Subtree, attrs);
                    PageResultRequestControl pageRequest = new PageResultRequestControl(5000);
                    request.Controls.Add(pageRequest);
                    SearchOptionsControl searchOptions = new SearchOptionsControl(System.DirectoryServices.Protocols.SearchOption.DomainScope);
                    request.Controls.Add(searchOptions);

                    while (true)
                    {
                        SearchResponse searchResponse = (SearchResponse)_LDAP.SendRequest(request);
                        PageResultResponseControl pageResponse = (PageResultResponseControl)searchResponse.Controls[0];
                        foreach (SearchResultEntry entry in searchResponse.Entries)
                        {
                            string _myUserid="";
                            string _myUPN="";
                            SearchResultAttributeCollection attributes = entry.Attributes;
                            foreach (DirectoryAttribute attribute in attributes.Values)
                            {
                                if (attribute.Name.Equals("sAMAccountName"))
                                {
                                    _myUserid = (string)attribute[0] ?? "";
                                    _myUserid.Trim();
                                }
                                if (attribute.Name.Equals("userPrincipalName"))
                                {
                                    _myUPN = (string)attribute[0] ?? "";
                                    _myUPN.Trim();
                                }
                                //etc with each datum you return from AD
                        }//foreach DirectoryAttribute
                        //do something with all the above info, I put it into a dataset
                        }//foreach SearchResultEntry
                        if (pageResponse.Cookie.Length == 0)//check and see if there are more pages
                            break; //There are no more pages
                        pageRequest.Cookie = pageResponse.Cookie;
                   }//while loop
              }//try
              catch{}
            }//using _LDAP
        }//JustGronkIT method
    }//ADImport class
} //namespace

0
经过长时间的挣扎和从这里收集到的一些提示,我想出了一个解决方案。我还发现了一个有趣的事情,就是在使用代码片段中所述的DirectoryServices资源与DataContext时,使用using块的方式存在差异。我可能不需要使用Finalizer,但为了安全起见,我还是这样做了。我发现通过按照下面概述的步骤进行操作,我的内存在运行期间保持稳定,而以前我每天都要杀掉应用程序两次才能释放资源。
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;

namespace myPlugins
{
    public class ADImport : Plugin
    {
        //I defined these outside my method so I can call a Finalizer before unloading the appDomain 
        private PrincipalContext AD = null; 
        private PrincipalSearcher ps = null;
        private DirectoryEntry _LDAP = null; //used to get underlying LDAP properties for a user
        private MDBDataContext _db = null; //used to connect to a SQL server, also uses unmanaged resources

        public override GronkIT()
        {
            using (AD = new PrincipalContext(ContextType.Domain,"my.domain.com"))
            {
                UserPrincipal u = new UserPrincipal(AD);
                u.Enabled=true;
                using(ps = new PrincipalSearcher(u))
                {
                    foreach(UserPrincipal result in ps.FindAll())
                    {
                        using (result)
                        {
                            _LDAP = (DirectoryEntry)result.GetUnderlyingObject();
                            //do stuff with result
                            //do stuff with _LDAP
                            result.Dispose(); //even though I am using a using block, if I do not explicitly call Dispose, it's never disposed of
                            _LDAP.Dispose(); //even though I am using a using block, if I do not explicitly call Dispose, it's never disposed of
                        }
                    }
                }
            }
        }

        public override JustGronkIT()
        {
            using(_db = new MDBDataContext("myconnectstring"))
            {
                //do stuff with SQL
                //Note that I am using a using block and connections to SQL are properly disposed of when the using block ends
            }
        }

        ~ADImport()
        {
            AD.Dispose(); //This works, does not throw an exception
            AD = null;
            ps.Dispose(); //This works, does not throw an exception
            ps = null;
            _LDAP.Dispose(); //This works, does not throw an exception
            _LDAP = null;
            _db.Dispose(); //This throws an exception saying that you can not call Dispose on an already disposed of object
        }
    }
}

0

UserPrincipal 实现了 IDisposable 接口。尝试在 foreach 循环内调用 result 的 Dispose 方法。

我还发现了 this 这个 Stack Overflow 的问题,但是对答案没有达成一致意见。


查看我的更改,有所改善但仍未解决...感谢您的帮助! - Richard

0

对于我来说,那段代码可以正常运行。我仅仅释放了每个实例。在我的项目中,我每两分钟调用一次这个方法。之后再调用垃圾收集器。

public class AdUser
{
    public string SamAccountName { get; set; }
    public string DisplayName { get; set; }
    public string Mail { get; set; }
}

public List<AdUser> GetAllUsers()
{
    List<AdUser> users = new List<AdUser>();

    using (PrincipalContext context = new PrincipalContext(ContextType.Domain, Environment.UserDomainName))
    {
        using PrincipalSearcher searcher = new PrincipalSearcher(new UserPrincipal(context));
        using PrincipalSearchResult<Principal> allResults = searcher.FindAll();

        foreach (Principal result in allResults)
        {
            using DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;

            AdUser user = new AdUser()
            {
                SamAccountName = (string)de.Properties["samAccountName"].Value,
                DisplayName = (string)de.Properties["displayName"].Value,
                Mail = (string)de.Properties["mail"].Value
            };

            users.Add(user);
            result.Dispose();
        }
    }

    return users;
}

第一次调用上述方法时,似乎有一些内存分配,但之后就不再泄漏了。 考虑在每次迭代后调用内存清理。
GC.Collect();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接