性能问题 - 从 .net 中的大型 Active Directory 组添加/删除用户

Performance Issue - Adding / Removing users from large Active Directory groups in .net

我们的 Active Directory 组包含 50 万用户,其中一个用户甚至超过一百万。

我们正在使用 System.DirectoryServices.AccountManagement 命名空间在组中添加和删除用户,如下所述:

代码本身运行良好,除了超级慢之外,添加用户最多需要一分钟,有时甚至更多!

我想出了下面这行代码,似乎触发了.net中的延迟加载机制:

adGroupPrincipal.Members.Add(userPrincipal);

我在调用 GroupPrincipal.Members.Add(UserPrincipal) 时使用 Wireshark 查看发生了什么,我看到 很多 网络流量。我的假设:访问成员 属性 会触发延迟加载方法以获取组的所有成员。

Members-属性 (https://msdn.microsoft.com/en-us/library/system.directoryservices.accountmanagement.groupprincipal.members(v=vs.110).aspx) 的官方文档中没有关于其行为的信息。

比较 "old school" 添加用户的方式与 System.DirectoryServices 命名空间的 DirectoryEntry 方式:

DirectoryEntry groupEntry = new DirectoryEntry("LDAP://server/CN=GROUPNAME,OU=Groups,OU=_CUSTOMERS,DC=srv,DC=tld", "USERNAME", "PASSWORD");
string userDn = String.Concat("LDAP://server/CN=", samAccountName, ",OU=Groups,OU=_CUSTOMERS,DC=srv,DC=tld"));    
groupEntry.Invoke("Add", new object[] { userDn });
groupEntry.CommitChanges();

大约需要 50 毫秒。

请注意,这篇 Whosebug 文章 Server is unwilling to process the request - Active Directory - Add User via C# 推荐我使用的 Invoke("Add", new object[] { userDn }) 方法,以避免 "Server is unwilling to process the request" 异常

所以基本上我的解决方法可以完成工作,但不知何故我不是 100% 满意,因为我实际上更喜欢使用 System.DirectoryServices.AccountManagement 命名空间,有什么想法如何避免使用该命名空间的性能问题?

我为这个问题在微软开了一个 "Advisory Call",这是他们的答案(下面是德语,英语):

S.DS.AM (System.DirectoryServices.Accountmanagement) ist nun nicht der Renner unter den Programmierschnittstellen, Bequemlichkeit ist Trumpf, perf-issues mit großen Gruppen sind also by Design. Wenn er auf Performance aus ist, sollte er S.DS.P (System.DirectoryServices.Protocols) oder plain LDAP verwenden.“

有意义的英文翻译是:

Comparing the APIs, S.DS.AM (System.DirectoryServices.Accountmanagement) is not a "racer", but comfort is trump. Performance issues for larger groups is by design. When performance matters, use S.DS.P (System.DirectoryServices.Protocols) or plain LDAP.

我创建了一个控制台应用程序以衡量以毫秒为单位在组中添加和删除用户的差异。

账户管理

public static void InsertGroupAccountManagement(UserPrincipal userPrincipal)
{
    using (GroupPrincipal adGroup = GroupPrincipal.FindByIdentity(_principalGroupContext, IdentityType.Guid, PRODUCT_USER_GROUP_ID))
    {
        adGroup.Members.Add(userPrincipal);
        adGroup.Save();
        adGroup.Members.Remove(userPrincipal);
        adGroup.Save();
    }
}

目录服务

public static void InsertGroupDirectoryServices(string samAccountName)
{
    DirectoryEntry groupEntry = new DirectoryEntry("LDAP://server.address/CN=PSO_PRODUCT_USER,OU=PSO_,OU=Groups,OU=_PRODUCT,DC=address,DC=server", "USERNAME", "PASSWORD");
    string userDn = String.Concat("LDAP://server.address/CN=", samAccountName, ",OU=Users,OU=_PRODUCT,DC=address,DC=server");
    DirectoryEntry userEntry = new DirectoryEntry(userDn, "USERNAME", "PASSWORD");
    groupEntry.Invoke("Add", new object[] { userDn });
    groupEntry.CommitChanges();            
    groupEntry.Invoke("Remove", new object[] { userDn });
    groupEntry.CommitChanges();            
    groupEntry.Close();
}

协议

public static void InsertGroupProtocols(string samAccountName)
{
    LdapDirectoryIdentifier ldapDirectoryIdentifier = new LdapDirectoryIdentifier("server.address");
    NetworkCredential credentials = new NetworkCredential("USERNAME", "PASSWORD");
    LdapConnection ldapConnection = new LdapConnection(ldapDirectoryIdentifier, credentials);
    ldapConnection.SessionOptions.ProtocolVersion = 3;
    ldapConnection.SessionOptions.Signing = true;
    ldapConnection.SessionOptions.Sealing = true;
    ldapConnection.AuthType = AuthType.Negotiate;
    ldapConnection.Bind();

    // Add
    DirectoryAttributeModification addDirectoryModification = new DirectoryAttributeModification();
    addDirectoryModification.Name = "member";
    addDirectoryModification.Add(String.Concat("CN=", samAccountName, ",OU=Users,OU=_PRODUCT,DC=address,DC=server"));
    addDirectoryModification.Operation = DirectoryAttributeOperation.Add;

    ModifyRequest addRequest = new ModifyRequest("CN=PSO_PRODUCT_USER,OU=PSO_,OU=Groups,OU=_PRODUCT,DC=address,DC=server", addDirectoryModification);
    ModifyResponse addResponse = ldapConnection.SendRequest(addRequest) as ModifyResponse;

    // Remoove
    DirectoryAttributeModification deleteDirectoryModification = new DirectoryAttributeModification();
    deleteDirectoryModification.Name = "member";
    deleteDirectoryModification.Add(String.Concat("CN=", samAccountName, ",OU=Users,OU=_PRODUCT,DC=address,DC=server"));
    deleteDirectoryModification.Operation = DirectoryAttributeOperation.Delete;

    ModifyRequest deleteRequest = new ModifyRequest("CN=PSO_PRODUCT_USER,OU=PSO_,OU=Groups,OU=_PRODUCT,DC=address,DC=server", deleteDirectoryModification);
    ModifyResponse deleteResponse = ldapConnection.SendRequest(deleteRequest) as ModifyResponse;
}

结果 table 以毫秒为单位

运行连续10次测试

所以在我的特殊情况下,通过 DirectoryServices / DirectoryEntry 的解决方案是最快的。