介绍
LDAP(轻量级目录访问协议)是目录信息的常见来源,最早于 1993 年被正式定义。它通常用于广泛的应用场景,包括管理 Linux 实例的用户/组信息,以及控制 VPN 和传统应用程序的身份验证。
传统上,公司的 LDAP 服务器是在内部运行的。通常是 Microsoft Active Directory 的一部分,或是开源 OpenLDAP 项目的部署。如今,包括 Foxpass 在内的 SaaS 提供商都已支持 LDAP。Foxpass 是首家从零开始构建多租户、以云为中心的 LDAP 实现的云 LDAP 提供商。
LDAP 通常具有以下基本操作:bind、search、compare 和 add。创建 Transmission Control Protocol (TCP) 连接后,应用程序首先需要通过发送用户名和密码进行绑定。成功绑定 后,客户端会向 LDAP 服务器发出命令。通常,这是将 搜索命令与筛选器结合使用。TCP 连接会保持,直到客户端或服务器断开连接。
Foxpass 的 LDAP
在 Foxpass,我们的 LDAP 服务基于广受欢迎的 Python 事件驱动服务框架 Twisted 编写。该服务托管在 AWS 的 ECS 平台上,并运行在数十个容器(节点)上。由于 LDAP 连接是持久的,因此集群必须能够维持数十万个同时存在的 TCP 会话。
我们将客户的数据保存在 RAM 中。原因有多个:首先,数据集相对较小,即使对于大型客户 也是如此。其次,LDAP query language 支持搜索任意字段(这一点很重要,因为 Foxpass 允许自定义字段)。这意味着传统的 RDBMS 系统无法建立有效的索引,因此我们必须将数据转换为另一种内存中表示形式。第三,将数据保存在 RAM 中可实现尽可能快的响应时间:延迟通常约为 100ms(见图 a)。

这种方法的一个缺点是缓存失效。当客户的数据发生变化时(例如新增或删除了用户),LDAP 节点必须从我们的主 RDBMS 刷新其数据。在我们之前的架构中,这是一项相对昂贵的操作;当每个节点都刷新同一公司的数据时,可能会导致 LDAP 延迟明显上升,并增加 backing-store 的负载。
延迟敏感性挑战
如上所述,由于延迟要求,每个容器都会将客户的所有数据存储在内存中(见图 a)。当请求到达某个节点时,系统会按需获取该数据(如果尚不存在),之后只要该客户至少保留一个连接,数据就会一直保留。
由于传入的连接请求可能会通过负载均衡器落到任意一个容器上,因此接收查询的容器也会加载并存储该客户的所有数据。随后,容器会向 redis pubsub 服务注册,以接收失效消息。当公司的数据更新时,会向接收节点广播一条失效信号,这些节点会清除该公司的数据缓存,并前往数据库重新获取该公司的数据。
随着我们持续增长并将新客户加入系统,这给扩展我们的 LDAP 服务带来了以下挑战:
客户(M)在所有节点(N)上的全部数据在失效时都需要重新加载。尽管在实际情况中,并非每个节点都会承载来自每个客户的连接,但在最坏情况下,需要向数据库发起 MxN 次调用来刷新数据。随着添加的客户数量增多(M),DB 获取次数可能会按 N 倍增加。这也意味着,为了避免数据库机器负载过重,需要额外的 DB reader 实例来应对请求量的激增。
由于所有客户(M)的数据都存储在多个节点(N)的内存中,因此所有节点上的内存占用会随着客户数量(M)的增加而持续增长。这也意味着,容器的内存需求必须增加,以容纳内存中的所有数据,从而提高整体基础设施成本。
上述挑战非常明确,促使我们选择将客户数据分布到各个节点,而不是在每个节点上复制全部数据。这促使我们采用了分布式缓存管理解决方案。

分布式缓存管理
我们在 LDAP 服务中引入了一层智能路由:如果连接落到的容器并未托管客户数据,就会将请求转发到托管该客户数据的节点。为实现这一目标,我们将路由层的设计要求归纳为以下几点:
随着节点的增加,需要将客户动态分配到各个节点。
当节点收缩、扩展以及发生节点故障时,客户的数据需要进行动态分布。
能够为特定客户增加或减少分区数量,从而避免流量不均衡压垮任何单个节点。
我们引入了 Apache Helix,它可在各个实例之间分配资源。作为 helix 生态系统的大脑,Helix controller 会在节 点或客户新增或移除时,跨节点做出资源分配决策。我们将 Apache Helix 控制器和 Rest 服务器容器化,并将其部署到 ECS 上。Helix 控制器依赖 Zookeeper 来监听集群变更。我们实现了一种在 ECS 上可靠部署 Zookeeper 的方法,因此现在 Helix 和 Zookeeper 的整个基础架构都运行在 ECS 上。
每个 LDAP 节点都会与 Zookeeper 交互,注册自身以参与集群。每个 LDAP 实例都会作为 Helix 参与者启动,并通过 Helix 控制器参与客户到节点分配的集群。当 LDAP 节点动态创建新客户时,该客户的分区数量(每个分区代表一个托管数据的节点)将根据用户数量来确定。
借助此集成,我们的 LDAP 节点能够感知集群中发生的事件(即添加新客户时,或可用节点集合发生变化时)。
借助这种集群感知能力,我们 LDAP 服务中的路由层可在节点与客户之间提供映射关系。现在,每个传入连接都会通过路由层,以决定该连接应路由到哪个节点。这样一来,客户的数据会被分配到特定节点上(见图 b)。

路由层承载了一个缓存,其中包含客户和节点的路由信息。路由层会识别任何客户到节点分配的变更,并在检测到这些变更时立即更新。这样一来,每个 LDAP 节点都能在客户到节点的变更发生时立即感知。
通过上述缓存管理解决方案,延迟敏感性挑战部分中提到的可扩展性挑战已得到解决:
我们现在的客户已分布在各个节点上。每个节点只会承载一部分客户,并且在收到 pubsub 失效通知后,只负责获取其中一部分客户的数据。这可显著减少对数据库的读取次数,无需再为应对大量数据库读取而添加数据库读取节点。LDAP 集群现在的 DB 读取次数比之前减少了 75%。
我们还提升了内存效率,因为不需要在所有节点(N)的内存中存储全部客户(M)数据。此架构可实现水平扩展,而无需增加实例大小。现在,每个 LDAP 节点的内存占用比之前减少了 40%。


当前挑战
采用上述实现方式时,其中一个挑战是各个节点接收到的 TCP 连接数量并不均等。这种分布不均衡会导致某些节点(热点节点)的 CPU 利用率高于其他节点。但与之前相比,各节点的整体平均 CPU 利用率仍然保持不变。
致谢
感谢整个团队。特别感谢 Bryan Bojorque 协助我们将 Helix 和 Zookeeper 集群进行容器化与构建,并将这些集群的指标接入 Datadog。
升级安全防护
您的网络是否已防止未经授权的用户访问?点击此处,了解 Foxpass 如何帮助避免代价高昂的安全错误:





