使用大量记录有效地更新 Active Directory 属性

Efficiently updating Active Directory attribute with large amount of records

关于在 Active Directory 中处理大约 15K 条记录的有效方法,我需要一些建议。在我的 List<string> employeeCN 变量中,我存储了大约 15K 的 employee id 以在 Active Directory 中处理。目标是更新活动目录中我列表中每个员工 ID 的一个属性 a-excludelock

目前,我有以下代码。我 运行 它作为我的基线,它花了大约 35 分钟来完成并更新我数组中的所有员工 ID。

using (DirectoryEntry directoryEntry = new DirectoryEntry("my_ldap", "my_username", "my_password"))
{
    DirectorySearcher directorySearcher = new DirectorySearcher(directoryEntry);
    
    // employeeCN is a list of string with 15K+ of data
    foreach (string empId in employeeCN)
    {
        try
        {
            string filter = $"(&(objectClass=user)(&(cn={empId})))";
            directorySearcher.Filter = filter;
            var searchResult = directorySearcher.FindOne();

            if (searchResult != null)
            {
                var de = searchResult.GetDirectoryEntry();
                de.Properties["a-excludelock"].Value = "Y";
                de.CommitChanges();
            }
        }
        catch(Exception ex)
        {
            // TODO: Log an error here why there's an error on the said employee id
            continue;
        }
    }
}

如果我使用 Parallel.Foreach 这会给我一些性能优势吗?

TIA!

前段时间写了一篇关于获得better performance when programming with AD的文章。简短的版本是您需要尽量减少网络请求的数量 and/or 来回发送的数据量。有几种方法可以做到这一点。

我看到你在搜索cn。如果所有用户都在同一个 OU 中,您可以完全消除搜索并直接绑定到每个对象。比如用户都在OU=Users,DC=example,DC=com这个OU里,那么你可以这样做:

foreach (string empId in employeeCN)
{
    try
    {
        using(var de = new DirectoryEntry($"LDAP://{empId},OU=Users,DC=example,DC=com")) {
            de.Properties["a-excludelock"].Value = "Y";
            de.CommitChanges();
        }
    }
    catch(Exception ex)
    {
        // TODO: Log an error here why there's an error on the said appid
        continue;
    }
}

如果用户在不同的 OU 中,则您将不得不进行搜索,但您可以做几件事。首先,将 something 放入 directorySearcher.PropertiesToLoad。如果您不这样做,AD 将 return 每个具有您不需要的值的属性。这只是浪费带宽。因此,只需添加任何属性,这样只有该属性会被 return 从搜索中编辑。

directorySearcher.PropertiesToLoad.Add("cn");

您还可以批量搜索,这样它会一次搜索 50 个。我在我的文章中写了一个例子,但这里它适用于你的用例(我实际上没有 运行 这个,所以你可能会发现一些错误):

using (var directoryEntry = new DirectoryEntry("my_ldap", "my_username", "my_password"))
{
    var directorySearcher = new DirectorySearcher(directoryEntry) {
        PropertiesToLoad = { "cn" }
    };
    
    var filter = new StringBuilder();
    var numUsernames = 0;
    
    var e = employeeCN.GetEnumerator();
    var hasMore = e.MoveNext();
    
    while (hasMore) {
        var cn = e.Current;
        filter.Append($"(cn={cn})");
        numUsernames++;
        
        hasMore = e.MoveNext();
        if (numUsernames == 50 || !hasMore) {
            directorySearcher.Filter = $"(&(objectClass=user)(objectCategory=person)(|{filter}))";
            
            using (var results = directorySearcher.FindAll()) {
                foreach (SearchResult searchResult in results) {
                    using (var de = searchResult.GetDirectoryEntry()) {
                        de.Properties["a-excludelock"].Value = "Y";
                        de.CommitChanges();
                    }
                }
            }
            filter.Clear();
            numUsernames = 0;
        }
    }
}

另一件重要的事情是将任何一次性物品包裹在 using 块中。通常你不需要担心处理 DirectoryEntry 对象,因为垃圾收集会很好地清理它们。但是当你 运行 进行一个长循环时,垃圾收集器没有时间 运行,所以你会发现你的应用程序会随着循环的进行而占用越来越多的内存.如果您正在查看 15k+ 个对象,那么在完成时很容易占用几 GB。所有这些内存分配都在浪费时间。因此,如果您在处理完每个对象后将其丢弃,将节省内存和时间。

就是说,你应该 总是 处理一个 SearchResultCollection(什么 FindAll() returns)——即使它不是 运行 循环 - 因为,正如文档所说:

Due to implementation restrictions, the SearchResultCollection class cannot release all of its unmanaged resources when it is garbage collected. To prevent a memory leak, you must call the Dispose method when the SearchResultCollection object is no longer needed.