标签: syncrepl

  • 深入 OpenLDAP 生产雪崩排查:SSSD 全表扫描引发的 syncrepl 同步阻塞与 PAM 认证超时

    SSSD 客户端缺乏精准过滤且 OpenLDAP 缺少核心字段索引,会导致 LMDB 后端触发全表扫描。这不仅会让 slapd 进程 CPU 长期打满,还会饿死 syncrepl 复制线程,最终引发多主集群 contextCSN 断层与全局 SSH/PAM 认证雪崩。破局点在于重建 olcDbIndex、收敛 SSSD 搜寻范围并启用 delta-syncrepl

    某次排查过程中,某环境数千台 Linux 服务器突然出现 SSH 无法登陆、sudo 命令卡死的问题。查看 K8S Worker 节点的 /var/log/secure,满屏的 pam_sss(sshd:auth): System error 与超时报错。

    登录核心认证集群,发现所有 OpenLDAP (版本 2.4.59) 节点的 slapd 进程 CPU 利用率飙升至 400%(4核跑满),Load Average 突破 80。

    通过 ldapsearch 提取各节点的 contextCSN,发现 Provider 与 Consumer 之间的数据已经严重割裂:

    # Provider 节点
    $ ldapsearch -x -LLL -H ldap://10.0.0.10 -s base -b "dc=corp,dc=com" contextCSN
    contextCSN: 20231018120001.123456Z#000000#000#000000
    
    # Consumer 节点 (同步延迟超过半小时)
    $ ldapsearch -x -LLL -H ldap://10.0.0.11 -s base -b "dc=corp,dc=com" contextCSN
    contextCSN: 20231018112500.654321Z#000000#000#000000
    

    syncrepl 同步几乎处于停滞状态。开启 slapdstats 日志级别后,我们抓到了导致血案的直接原因:大量无索引的 Group 遍历查询。

    为什么百万级 DIT 下,SSSD 组查询会演变成全表扫描?

    在标准的 PAM/SSSD 集成架构中(SSSD 2.2.3),当用户尝试 SSH 登录时,SSSD 会通过 LDAP 校验用户身份并拉取该用户所属的所有组(Group)信息。

    如果我们看当时的 slapd 日志,会频繁出现以下警告:

    slapd[1234]: <= mdb_equality_candidates: (memberUid) not indexed
    slapd[1234]: <= mdb_equality_candidates: (member) not indexed
    

    在默认的 SSSD 配置下,如果你开启了 enumerate = true,或者使用了极其宽泛的 LDAP Search Base(例如直接挂在 dc=corp,dc=com 而非 ou=Groups,dc=corp,dc=com),SSSD 客户端会定期向 LDAP 发起类似 (&(objectClass=posixGroup)(memberUid=username)) 的查询。

    OpenLDAP 的 LMDB (Lightning Memory-Mapped Database) 底层是基于 B+ 树的键值对存储。当查询条件中的属性(如 memberUid)在 olcDbIndex 中没有定义 eq (精确匹配) 索引时,slapd 只能回退到最原始的处理方式:全表遍历 (Full Table Scan)

    在拥有数十万 Entry 的 DIT (Directory Information Tree) 中,单次全表扫描就会产生巨量的内存分页换入换出(Page Fault)。当几千台机器的 SSSD 并发发起查询时,LMDB 的 PageCache 被迅速击穿,磁盘 IO Wait 暴增,slapd 的查询线程池被彻底耗尽。

    syncrepl 复制堆积与写饿死机制

    理解了读性能衰减,还需要解释为什么主从同步会断层。

    OpenLDAP 的 syncrepl (基于 refreshAndPersist 模式) 是单线程拉取机制。Consumer 节点通过一个持续的 LDAP Search 连接监听 Provider 的变动。

    当 Provider 的查询线程被全表扫描的 SSSD 客户端占满时:

    1. 底层 LMDB 引擎面临极高的读锁竞争。

    2. Provider 端尝试将新的写入(比如密码错误次数更新 pwdFailureTime)提交到磁盘,但写事务在等待读事务释放锁,或者 CPU 时间片被读事务耗尽。

    3. 即使写入成功,负责向 Consumer 推送更新的 Sync Provider 线程也拿不到资源去构建同步 Payload。

    4. Consumer 端的 syncrepl 线程长轮询超时,触发重连,重连后发送自己旧的 contextCSN 要求全量对比增量数据,进一步加重了 Provider 的负担。

    这就是经典的读风暴导致写饿死,进而引发复制雪崩

    防御性调优与落地实战

    面对这种架构脆弱性,仅仅重启是没用的,必须从索引层、服务端防刷层以及客户端检索边界三个维度进行彻底改造。

    1. 补齐核心字段索引 (olcDbIndex)

    生产环境的 OpenLDAP,绝不允许出现 not indexed 警告。必须通过 ldapmodify 动态注入索引配置,然后离线重建。

    构建 index.ldif

    dn: olcDatabase={2}mdb,cn=config
    changetype: modify
    add: olcDbIndex
    olcDbIndex: memberUid eq,pres,sub
    olcDbIndex: member eq,pres
    olcDbIndex: uidNumber eq,pres
    olcDbIndex: gidNumber eq,pres
    olcDbIndex: entryCSN eq
    olcDbIndex: entryUUID eq
    

    应用配置并重建索引(针对 2.4.x 大库,最安全的方式是停机重建):

    ldapmodify -Y EXTERNAL -H ldapi:/// -f index.ldif
    systemctl stop slapd
    # 使用 slapindex 重建底层 LMDB B+ 树,切换为 ldap 用户执行
    su - ldap -s /bin/bash -c "slapindex -b 'dc=corp,dc=com'"
    systemctl start slapd
    

    2. OpenLDAP 防刷限流 (Limits & Timeouts)

    为了防止单个烂 SQL (LDAP Query) 拖垮整库,必须在服务端设置防御性阈值。在 cn=config 中限制单次查询扫描的最大条目数和时间:

    dn: olcDatabase={2}mdb,cn=config
    changetype: modify
    replace: olcSizeLimit
    olcSizeLimit: size.soft=1000 size.hard=5000
    -
    replace: olcTimeLimit
    olcTimeLimit: time.soft=10 time.hard=30
    

    超过该限制的恶意查询将直接被掐断,返回 Size limit exceeded 异常,保证核心进程存活。

    3. SSSD 客户端瘦身配置 (sssd.conf)

    绝大部分运维配置 SSSD 时喜欢照抄网上的模板。正确的 sssd.conf 应当极度收敛搜索边界:

    [domain/corp.com]
    id_provider = ldap
    auth_provider = ldap
    # 严禁在几千台机器上开启 enumerate (这会拉取全量用户列表)
    enumerate = false
    
    # 强制限定 Search Base,不要在根路径捞针
    ldap_user_search_base = ou=People,dc=corp,dc=com
    ldap_group_search_base = ou=Groups,dc=corp,dc=com
    
    # 忽略不必要的组成员查询(如果不需要依赖组成员做 sudoers 细粒度控制)
    ignore_group_members = true
    
    # 开启离线凭证缓存,在 LDAP 抖动时保证老用户依然能登录
    cache_credentials = true
    entry_cache_timeout = 14400
    

    4. 优化复制模式 (delta-syncrepl)

    当涉及到超大 Group(例如拥有上万个 memberUid 的组)时,任何一人的增删都会导致整个 Group 的全量条目被 syncrepl 传输。 在架构改造层面,必须启用 accesslog Overlay,并切换到 delta-syncrepl。该模式下,Provider 将变更操作(Modify/Add/Delete)记录到独立的 LMDB 库中,Consumer 只拉取具体的变更动作(如 add: memberUid: newuser),而不是拉取包含1万个用户的整个 Group 对象,使得网络传输和 CPU 解析开销呈指数级下降。

    常见问题 (FAQ)

    Q1:如何准确监控 OpenLDAP 的 syncrepl 复制延迟? 不要依靠 ping 端口,必须采集 contextCSN。可通过编写 Exporter 或 Shell 脚本,分别从 Provider 和 Consumer 取出 contextCSN 的时间戳部分进行差值计算。如果有多个 Provider 写入,contextCSN 会包含多个 Server ID(如 #000001, #000002),必须分别对比每个 ID 的时间戳。

    Q2:slapd 日志大量报错 mdb_db_open: database "dc=xxx" cannot be opened, err 12. Cannot allocate memory,如何处理? 这是 LMDB 的 maxsize 达到了限制。LMDB 使用内存映射文件(mmap),其 maxsize 并不代表真实占用的磁盘空间,而是虚拟内存映射的上限。默认值通常太小(如 1GB),对于生产环境,应该在 cn=configolcDbMaxSize 修改为更大的值(例如 8589934592 即 8GB),并确保操作系统层面没有限制进程的 VIRT 内存。

    Q3:SSSD 缓存导致用户刚改了组权限却不生效,怎么清理最快? 执行 sss_cache -E 清理全量缓存,或者针对特定用户执行 sss_cache -u username,然后重启 sssd 服务(systemctl restart sssd)。在生产环境批量排查时,切忌盲目清空缓存,否则瞬间穿透到 OpenLDAP 的并发查询会引发洪峰。