如何在Laravel中无超时/内存问题地从AD LDAP获取全部用户
Laravel 10 + AdLdap2 批量获取AD用户遇到的问题
我正在开发一个Laravel 10应用,用AdLdap2包和PHP 8的LDAP函数对接Active Directory(AD),但获取2万+用户时遇到了瓶颈。
已尝试的两种方法
方法1:使用AdLdap2包
private function handleAdLdapUsersEx($provider): void { try { $pageSize = 200; if ($this->searchBase->filter) { $provider->where($this->searchBase->filter); } $pagedUsers = $provider->select('distinguishedname', 'givenname', 'name', 'objectguid', 'samaccountname', 'userprincipalname', 'mail') ->whereEnabled() ->limit($pageSize) ->paginate($pageSize, $this->currentPage); dump($pagedUsers->count()); if ($pagedUsers->count() > 0) { collect($pagedUsers->getResults())->chunk(100)->each(function ($chunkedUsers) { $this->handleBulk($chunkedUsers); unset($chunkedUsers); gc_collect_cycles(); }); if ($pagedUsers->count() === $pageSize) { ImportUsersJob::dispatch($this->searchBase, $this->ldapConnector, $this->ldapConfig, $this->currentPage + 1); } } } catch (\Exception $e) { dd($e->getMessage()); } }
设置了分页限制,但实际还是一次性拉取所有记录导致超时。虽然把public $timeout = 600;可以解决问题,但不想硬编码超时时间。
方法2:使用PHP原生LDAP函数
private function handleAdLdapUsers($provider, $ldapConnector): void { try { $usersFetched = 0; $lastUserEntry = null; $ldapConnection = ldap_connect($provider->getConnection()->getHost()); ldap_set_option($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_bind($ldapConnection, $ldapConnector->getUsernames(), $ldapConnector->getPassword()); $filter = !empty($this->searchBase->filter) ? $this->searchBase->filter : "(objectClass=*)"; $result = @ldap_search($ldapConnection, $provider->getDn(), $filter, [], 0, 1); $firstEntry = ldap_first_entry($ldapConnection, $result); while ($firstEntry) { $attributes = ldap_get_attributes($ldapConnection, $firstEntry); $users = $provider->select('distinguishedname', 'givenname', 'name', 'objectguid', 'samaccountname', 'userprincipalname', 'mail', 'usncreated') ->whereEnabled() ->get($attributes); if ($users->count() > 0) { $this->handleBulk($users); $usersFetched = $users->count(); } else { break; } $lastUserEntry = ldap_next_entry($ldapConnection, $firstEntry); $firstEntry = $lastUserEntry; } ldap_unbind($ldapConnection); } catch (\Exception $e) { dd($e->getMessage()); } }
出现了“Sizelimit exceeded”警告,只能获取1000条记录。虽然用@ldap_search屏蔽了警告,但需要完整获取甚至超过5万的用户,同时避免大小限制和内存问题。
待解决的问题
- 如何修改查询或分页逻辑,高效获取全部用户?
- Laravel中查询LDAP大数据集有哪些最佳实践?
- 能否通过配置LDAP服务器放宽默认的大小限制?
解决方案
1. 修复分页逻辑,实现真正的LDAP分页
AdLdap2的paginate方法依赖LDAP的分页控制扩展(Paged Results Control),当前代码可能未正确启用该功能,以下是修复方案:
修改AdLdap2配置
在config/ldap.php的连接配置中开启分页支持:
'connections' => [ 'default' => [ // ...其他配置 'settings' => [ // ...其他设置 'enable_paging' => true, // 开启分页支持 'page_size' => 200, // 默认分页大小 ], ], ],
调整AdLdap2代码逻辑
利用AdLdap2自带的分页迭代器,自动遍历所有分页:
private function handleAdLdapUsersEx($provider): void { try { $query = $provider->select('distinguishedname', 'givenname', 'name', 'objectguid', 'samaccountname', 'userprincipalname', 'mail') ->whereEnabled(); if ($this->searchBase->filter) { $query->where($this->searchBase->filter); } // 自动处理所有分页,逐页拉取数据 $pagedUsers = $query->paginate(200); foreach ($pagedUsers as $page) { collect($page)->chunk(100)->each(function ($chunk) { $this->handleBulk($chunk); gc_collect_cycles(); // 及时释放内存 }); } } catch (\Exception $e) { report($e); } }
这种方式不会一次性加载所有记录,从根源避免超时和内存溢出。
原生LDAP函数实现分页
如果用原生PHP LDAP,需手动启用分页控制绕过1000条限制:
private function handleAdLdapUsers($provider, $ldapConnector): void { try { $ldapConnection = ldap_connect($provider->getConnection()->getHost()); ldap_set_option($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_set_option($ldapConnection, LDAP_OPT_REFERRALS, 0); // 禁用引用,减少无效数据 ldap_bind($ldapConnection, $ldapConnector->getUsernames(), $ldapConnector->getPassword()); $pageSize = 200; $cookie = ''; // 直接过滤启用的用户,替代whereEnabled() $filter = !empty($this->searchBase->filter) ? $this->searchBase->filter : "(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"; do { ldap_control_paged_result($ldapConnection, $pageSize, true, $cookie); $result = ldap_search($ldapConnection, $provider->getDn(), $filter, [ 'distinguishedname', 'givenname', 'name', 'objectguid', 'samaccountname', 'userprincipalname', 'mail' ]); $entries = ldap_get_entries($ldapConnection, $result); if ($entries['count'] > 0) { $this->handleBulk(collect(array_slice($entries, 1))); // 跳过count字段 } // 获取下一页的cookie ldap_control_paged_result_response($ldapConnection, $result, $cookie); } while ($cookie !== null && $cookie !== ''); ldap_unbind($ldapConnection); } catch (\Exception $e) { report($e); } }
2. Laravel查询LDAP大数据集的最佳实践
- 拆分队列任务:把导入拆成单页任务,用Laravel队列异步处理,避免阻塞主进程,完成一页后自动调度下一页任务。
- 精准选择属性:不要用
select('*'),明确指定需要的字段,减少数据传输量和内存占用。 - 主动释放内存:每批数据处理完后,用
unset()销毁变量,调用gc_collect_cycles()强制垃圾回收。 - 动态设置超时:在队列任务中根据每页数据量动态计算超时,而非硬编码:
public $timeout; public function __construct() { $this->timeout = ceil(200 / 50) * 60; // 每50条用户分配60秒超时 } - 禁用LDAP引用:设置
LDAP_OPT_REFERRALS为0,避免AD返回额外的引用数据,减少网络开销。
3. 配置LDAP服务器放宽大小限制
可以修改AD服务器的默认查询限制,但需要AD管理员权限:
- 全局查询限制:在AD服务器的
Active Directory Users and Computers中,右键域名→属性→“组策略”→编辑→计算机配置→管理模板→系统→LDAP客户端→“LDAP服务器查询大小限制”,设置目标数值(如50000)。 - 单个账号限制:给服务账号单独设置查询限制,在AD用户属性→“账户”→“高级”→“查询限制”中调整数值。
注意:放宽服务器端限制会增加AD负载,建议优先用分页方案解决,仅在必要时调整服务器配置。
内容的提问来源于stack exchange,提问作者Fawwash Khan
相关产品推荐
相关产品推荐

