某次处理线上核心业务主机鉴权大面积超时,根因是 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 等常规业务索引,唯独没有 entryCSN 和 entryUUID。
为什么 syncrepl 追逃会导致 Provider 节点 CPU 被打满?
在 RFC 4533(LDAP Content Synchronization Operation)中,syncrepl 的增量同步(Refresh phase)高度依赖 entryCSN 和 entryUUID 这两个内部操作属性。
当 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 秒。
这产生了一个致命的死循环:
-
syncrepl触发全表扫描,阻塞 Provider 线程池(olcThreads默认 16)。 -
SSSD 在 Consumer/Provider 上的常规鉴权查询排队。
-
SSSD 查询超时,断开连接,立即向下一个备用节点重试。
-
重试流量像海啸一样涌来,彻底击穿所有 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 彻底断层,无法自动追平,如何手动介入恢复?
-
停止落后节点(Consumer)的
slapd服务。 -
备份当前数据(可选):
slapcat -n 2 -l backup.ldif。 -
清空落后节点的 LMDB 数据目录(如
/var/lib/ldap/*,保留DB_CONFIG如果有的话)。 -
在 Provider 节点使用
slapcat导出全量数据,并拷贝至 Consumer。 -
在 Consumer 节点使用
slapadd -q -l export.ldif导入数据(这一步会自动包含 Provider 的contextCSN)。 -
启动 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 模块。若是后者,务必检查是否为 member 和 memberOf 属性建立了 eq 索引,否则 SSSD 在展开每个用户组时,依然会触发服务端的高级范围搜索惩罚。