标签: LMDB

  • 深入 OpenLDAP 陷阱排查:syncrepl 脑裂引发的 contextCSN 紊乱与 SSSD 认证雪崩实战

    某次处理线上核心业务主机鉴权大面积超时,根因是 OpenLDAP(2.4.59 版本)Consumer 节点网络抖动重连后,触发 syncrepl 机制追平数据。因核心属性缺失索引,导致 Provider 节点发生全目录扫描(Full DIT Scan),slapd CPU 瞬间飙升至 400%。叠加 SSSD 客户端超时重试机制,演变成对 LDAP 集群的 DDoS。解法:在线补充 entryCSN 等底层索引,收紧 SSSD 探测与退避策略。

    现场:Load 飙升与 SSH 登录卡死

    排查过程中,监控系统报出大量机器 SSH 登录卡顿,甚至直接超时(连接耗时 > 30s)。查看 LDAP 监控面板,发现 Provider 节点(Master)的 slapd 进程 CPU 使用率打满,Load Average 飙升至 45,QPS 从日常的 200 突增至 8000+。

    抓取 Provider 节点的 slapd 日志(olcLogLevel: 256):

    slapd[11302]: conn=102435 op=1 SRCH base="dc=example,dc=com" scope=2 deref=0 filter="(objectClass=*)"
    slapd[11302]: conn=102435 op=1 SRCH attr=* +
    slapd[11302]: conn=102435 op=1 SEARCH RESULT tag=101 err=0 nentries=125430 text=
    

    同时,客户端侧的 SSSD (/var/log/sssd/sssd_example.com.log) 疯狂刷屏:

    [sdap_get_users_done] (0x0040): Recoverable error from LDAP server: 115 (Operations error)
    [sssd[be[example.com]]] [be_ptask_execute] (0x0040): Task [SUDO Rules Refresh]: execution failed [143]: Connection timed out
    [sssd[be[example.com]]] [fo_resolve_service_send] (0x0020): No available servers for service 'LDAP'
    

    直观现象是:Consumer 节点由于网络短暂断开,尝试通过 syncrepl 协议向 Provider 节点同步数据,但由于查询极为缓慢,不仅同步失败,还把 Provider 节点拖垮,进而导致全网依托 SSSD 的 Linux 机器鉴权请求堆积,最终雪崩。

    根因定位:缺失的暗坑索引

    通过 ldapsearch 提取两端节点的 contextCSN(上下文提交序列号),发现已经发生断层(Split-brain 雏形):

    # Provider 端
    ldapsearch -x -LLL -H ldap://provider.example.com -b "dc=example,dc=com" -s base contextCSN
    # contextCSN: 20231025143021.123456Z#000000#000#000000
    
    # Consumer 端
    ldapsearch -x -LLL -H ldap://consumer.example.com -b "dc=example,dc=com" -s base contextCSN
    # contextCSN: 20231025142810.654321Z#000000#000#000000
    

    Consumer 落后约 2 分钟。当它发起 Sync Request 时,会带上自己的 contextCSN,Provider 需要找出所有 entryCSN 大于该值的条目进行增量推送。

    此时检查 Provider 端的 olcDbIndex 配置:

    ldapsearch -Y EXTERNAL -H ldapi:/// -b "olcDatabase={2}mdb,cn=config" olcDbIndex
    

    输出结果中,仅有 uid, cn, memberUid, objectClass 等常规业务索引,唯独没有 entryCSNentryUUID

    为什么 syncrepl 追逃会导致 Provider 节点 CPU 被打满?

    在 RFC 4533(LDAP Content Synchronization Operation)中,syncrepl 的增量同步(Refresh phase)高度依赖 entryCSNentryUUID 这两个内部操作属性。

    当 Consumer 携带过期的 contextCSN 请求增量数据时,Provider 内部执行的等效查询过滤条件实际上包含: (&(objectClass=*)(entryCSN>=20231025142810.654321Z#000000#000#000000))

    在 LMDB 存储引擎中,如果一个属性没有配置等值(eq)索引,LDAP 就会退化为全目录树遍历(Full DIT Scan)。对于包含 10 万+ 条目(User/Group/Sudoers)的目录树,每次重连都会导致 Provider 将 10 万个条目从内存/磁盘中全量载入,逐一比较 entryCSN 字符串。

    单次扫描可能耗时 3-5 秒。而此时 SSSD 客户端设置的 ldap_search_timeout 通常是 3 秒。 这产生了一个致命的死循环:

    1. syncrepl 触发全表扫描,阻塞 Provider 线程池(olcThreads 默认 16)。

    2. SSSD 在 Consumer/Provider 上的常规鉴权查询排队。

    3. SSSD 查询超时,断开连接,立即向下一个备用节点重试。

    4. 重试流量像海啸一样涌来,彻底击穿所有 LDAP 节点的可用连接数。

    防御性调优实战

    要彻底解决该问题,必须从服务端底层索引和客户端退避策略两端同时下手。

    1. 在线补充核心索引 (LDAP 端)

    切记不要直接修改底层文件,必须通过 OLC (cn=config) 动态生效。编写 LDIF 文件 add_index.ldif

    dn: olcDatabase={2}mdb,cn=config
    changetype: modify
    add: olcDbIndex
    olcDbIndex: entryCSN eq
    -
    add: olcDbIndex
    olcDbIndex: entryUUID eq
    -
    add: olcDbIndex
    olcDbIndex: memberOf eq
    

    注:如果你开启了 memberof overlay,也必须为其建立 eq 索引,否则组查询同样会导致慢查询。

    应用配置并等待后台构建索引完成(LMDB 支持在线建索引,期间会有短暂的 IO 升高,但不阻塞读写):

    ldapmodify -Y EXTERNAL -H ldapi:/// -f add_index.ldif
    

    2. 加固 syncrepl 同步参数 (LDAP 端)

    原生 syncrepl 配置往往忽略了重试退避和保活机制,网络微小抖动极易引发连接挂起。修改 Consumer 上的 olcSyncrepl

    olcSyncrepl: {0}rid=101 provider=ldap://provider.example.com 
      bindmethod=simple binddn="cn=replicator,dc=example,dc=com" credentials="xxx" 
      searchbase="dc=example,dc=com" 
      type=refreshAndPersist 
      retry="60 10 300 3"    # 核心退避策略:每60秒重试10次,若失败则每300秒重试3次(或者无限次 +)
      keepalive="240:10:30"  # 开启 TCP Keepalive:空闲240秒后,每10秒探测一次,30次失败断开
      timeout=3              # 控制连接超时时间,防止同步线程被死锁
    

    3. SSSD 容灾与熔断配置调优 (Client 端)

    SSSD 的默认配置过于激进,在面对服务端降级时缺乏足够的容忍度。修改 /etc/sssd/sssd.conf 中的关键参数:

    [domain/example.com]
    id_provider = ldap
    auth_provider = ldap
    chpass_provider = ldap
    ldap_uri = _srv_, ldap://provider.example.com, ldap://consumer.example.com
    
    # 【熔断与退避】
    # 当服务器标记为离线后,多久后重新尝试连接(避免无脑重试打死服务端)
    ldap_connection_expire_timeout = 60
    ldap_network_timeout = 3
    ldap_opt_timeout = 3
    ldap_search_timeout = 6
    
    # 【本地缓存加固】
    # 允许在 LDAP 不可用时,使用本地缓存的凭据进行登录
    cache_credentials = True
    # 延长离线缓存的有效期(默认可能太短)
    offline_credentials_expiration = 7
    
    # 【查询优化】禁用冗余枚举,减少服务端压力
    enumerate = False
    # 优化 Sudo 规则拉取频率(如果使用了 sudo provider)
    ldap_sudo_smart_refresh_interval = 900
    ldap_sudo_full_refresh_interval = 10800
    

    重启 SSSD 后生效:systemctl restart sssd; sss_cache -E

    常见问题 (FAQ)

    Q1:如何判断我的 OpenLDAP 集群是否发生了脑裂(Split-Brain)? 首先在集群所有节点上执行 ldapsearch 获取根部的 contextCSN。如果发现某些节点的 contextCSN 已经停止更新,且时间差超过了你的同步延迟容忍度,再查看 slapd 日志中是否有 CSN too old 或重复的 op=1 SRCH base="dc=example,dc=com"err=0 nentries=全量条目数,这表明增量同步已失效,正在退化为全量拉取或已经卡死。

    Q2:如果 syncrepl 彻底断层,无法自动追平,如何手动介入恢复?

    1. 停止落后节点(Consumer)的 slapd 服务。

    2. 备份当前数据(可选):slapcat -n 2 -l backup.ldif

    3. 清空落后节点的 LMDB 数据目录(如 /var/lib/ldap/*,保留 DB_CONFIG 如果有的话)。

    4. 在 Provider 节点使用 slapcat 导出全量数据,并拷贝至 Consumer。

    5. 在 Consumer 节点使用 slapadd -q -l export.ldif 导入数据(这一步会自动包含 Provider 的 contextCSN)。

    6. 启动 Consumer 的 slapd,此时同步会从最新的 contextCSN 瞬间无缝衔接。

    Q3:高可用架构下,推荐使用 N-Way Multi-Master 还是 MirrorMode? 作为踩过无数坑的老鸟,强烈建议使用 MirrorMode(Active-Standby 模式)。OpenLDAP 的 N-Way Multi-Master 在面对并发写入更新同一条目(特别是组成员更新)时,极易产生难以自动解决的冲突(Conflict entries)。MirrorMode 结合 Keepalived/HAProxy 提供单一写入入口,同时保持双向的 syncrepl 复制,既保证了写操作的数据一致性,又实现了无缝的故障转移,是目前生产环境最为稳妥的落地架构。

    Q4:为什么通过 SSSD 查组下的用户很慢,但直接 ldapsearch 很快? 这通常是因为 SSSD 默认开启了复杂的嵌套组解析(RFC 2307bis)。如果你的 DIT 设计中没有使用嵌套组,应该在 SSSD 中配置 ldap_group_member = memberUid(针对 posixGroup)或确保 OpenLDAP 端加载了 memberof 模块。若是后者,务必检查是否为 membermemberOf 属性建立了 eq 索引,否则 SSSD 在展开每个用户组时,依然会触发服务端的高级范围搜索惩罚。

  • 深入 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 的并发查询会引发洪峰。