概述

对于基于 Raft 实现数据高可用的分布式 IoTDB,客户端缓存 leader 地址能够使得客户端在写数据时直接找到对应的正确节点,从而显著减少内部数据转发的次数,同时也能够省去多次序列化反序列化的代价,是一个十分有必要的性能优化。

前期评估测试

理论转发代价评估

理论上将客户端请求在内部转发的代价是不转发的 1.67 倍,如果加上日志序列化反序列化的代价可能更多。

实际转发代价测试

因此,需要实现客户端的缓存 leader 功能。

设计思路

概述

尽管 session 客户端和 jdbc 客户端实现是相对分离的,但是在分布式服务端中其最终都会走相同的逻辑(executeNonQueryPlan)。因此一旦在服务端实现关闭内部转发就必须同时实现 session 客户端和 jdbc 客户端的缓存 leader 功能才能够使得两个客户端都能正常工作。


设计的几点要点:

  • 此功能需要对用户完全透明,使用户体验与之前一致。
  • 考虑到具有不同语言的 session 客户端(如 c++,python),因此需要保证没有实现 leader 缓存的客户端也能够正常写入。

服务端

不论是 jdbc 客户端还是 session 客户端的写请求,在服务端都会进入 executeNonQueryPlan 函数中,而此函数的返回值是一个 TSStatus。为了防止逻辑上需要转发的请求得通过多次 RPC 重定向才能被最终执行,因此服务端可以默认执行请求,只不过对于需要转发的请求除了执行结果外还会回复给客户端此请求应该转发的 leader 节点地址,这样客户端就可以在下一次发送时直接切换到新的地址了。服务端这样的设计相比一旦发现需要转发就返回给客户端重定向的方式可以节约所有需要转发请求的第一次发送。具体的实现方式就是在 TSStatus 结构体中塞入一个 redirectNode 字段即可,然后客户端监控此变量是否有被设置过来做相应的处理即可。


更具体的:

  • 对于可以本地执行的 plan,协调者节点直接执行即可。
  • 对于 meta 组请求,如果协调者节点是 meta 组的 leader,则直接执行返回结果即可;如果协调者节点是 meta 组的 follower,则将此请求发送给 leader 执行后再将 response 的 TSStatus 中的 redirectNode 字段设置为 meta 组的 leader (比如 "192.168.130.31:55560") 即可,这样就可以让客户端去解析重定向了,之后所有meta 组的增删改查都可以直接发送到不需要转发的 meta 组 leader 节点了;如果此时 meta 组无 leader,则阻塞该请求直至新 leader 产生再按照前述规则执行返回即可。
  • 对于全部 data 组请求,协调者节点直接执行不需要重定向即可。
  • 对于 data 组请求,针对写请求首先执行该请求,然后再根据条件来判断是否需要设置 redirectNode。针对只向一个 data 组写数据的请求,如果协调者节点是此 data 组的 leader,则不用设置;如果协调者节点是此 data 组的 follower,则设置 redirectNode 为 leader 即可;如果协调者节点不在此 data 组内,则对转发的 response 监控,如果其设置过 redirectNode 则不再修改(保证返回给客户端的是多次转发后的最终执行节点),否则设置 redirectNode 为转发的节点;这期间不论哪个过程出现无 leader 的现象,都阻塞请求直至新 leader 产生再按照前述规则执行返回即可。针对向多个 data 组写数据的请求,如果协调者节点不在任何一个 data 组或者协调者节点在大于等于一个 data 组中但不是任何一个 data 组的 leader,则设置 redirectNode 为随机一个 data 组的 leader 即可;否则不设置直接执行返回即可。

客户端

session 缓存 leader 设计

方案 1

监听任何写请求的返回值 TSStatus,一旦检测发现 redirectNode 被设置过则 close 掉此 session 和 transport ,接着解析更新 host 和 port,然后重新构建 transport,client, sessionId,接着继续执行上次请求即可。

  • 优点:实现容易。
  • 缺点:如果用户利用 session 交替写两个存储组的数据,且恰好这两个存储组不在一个 data 组存储,则可能会出现客户端频繁切换节点创建 session 的行为。

方案 2(方案 1 改进)

既然方案 1 的实现有可能出现客户端频繁切换节点创建 session 的行为(虽然我们可以建议用户不要这么使用 session ,但是底层做好保障还是更好一些),那么我们可以在 session 客户端花一些内存保存一些信息并池化连接即可。具体实现是:在 session 类中维护两个 map,一个 map 记录 deviceId 和集群节点的映射(其实存储组就够用了,但是客户端不能自己从 deviceId 直接推导出存储组,所以只能退而求其次用 deviceId 了,考虑到目前单客户端 deivceId 的量级最大就几万左右,带来的内存消耗代价应该不高。),另一个 map 记录集群节点和对应客户端的映射(池化连接,如果只有 deviceId -> client 的 map 也会造成 session 的频繁创建)。每次写数据时,session 会自动根据其 deviceId 来从 deviceId -> node 的 map 中寻找是否存在对应的 node,若有则再根据此 node 从 node -> client 的 map 中得到 client 来发送请求;若无则在 node -> client 的 map 中随机挑选一个发送请求;期间检测每一条写请求,一旦发现 redirectNode 被设置就更新维护一下这两个 map。

  • 优点:可以复用所有连接。即使用户交替写不同存储组的数据,也不会出现多次和服务端创建连接的操作,最多创建集群节点个数个客户端就可以了。
  • 问题: 如果集群规模变大(比如几十个甚至几百个),一个带有缓存 leader 功能的客户端可能最多开集群节点数量个 transport ,此代价是否高昂?在目前阶段应该是可行的。

后续

若干 PR 已合并

  • No labels