PolarDB-X 私有协议2.0

简介: 本文主要介绍私有协议2.0,也即XRPC的背景、总体设计、相关技术实现细节和性能测试结果。

本文主要介绍私有协议2.0,也即XRPC的背景、总体设计、相关技术实现细节和性能测试结果。

私有协议作为解决 PolarDB-X 中计算节点和存储节点复杂通信需求的技术手段,在 PolarDB-X 2.0 公共云版上线初期就作为重要的功能一起发布了。同时在PolarDB-X开源版中,也作为唯一的和后端存储节点的通信链路,在数据库请求主链路中起着至关重要的作用。
然而随着 PolarDB-X 的发展,存储节点 5.7 & 8.0 的兼容问题,国产化 ARM 平台的适配等需求接踵而至,私有协议基于 MySQL X plugin 的网络框架设计逐渐变得力不从心,因此对存储节点上私有协议服务端的代码重构就势在必行,XRPC即私有协议2.0应运而生。

1. 难以解决的局限性

私有协议最初的设计在前文已有说明,其旨在解决计算节点和存储节点连接数爆炸的问题。通过连接会话解耦,将传统的 MySQL 会话机制优化为类 RPC 机制,通过会话 ID 实现在同一个通信信道上传输多个会话。由于当时对于快速上线的需求,开发相对困难的存储节点端,使用了相对成熟完整的 MySQL X plugin 进行扩展改造,基于其网络处理调度框架进行消息扩展,完成了私有协议 server 端的开发。其架构如下图所示:
01.jpg
02.jpg

该网络执行架构成功协助解决了 PolarDB-X 所遇到的后端连接爆炸问题,同时基于 protobuf 消息的扩展,也实现了很多计算节点和存储节点的高级交互功能,帮助 PolarDB-X 从传统的中间件模式迈入了完整分布式数据库的行列。
诚然,这个框架也是存在一定的历史局限性的,由于 MySQL X plugin 是以 one thread per connection 为理念设计的,每个处理session都绑定了一个执行线程,同时请求消息接收是由额外的线程处理,并分发到对应的工作会话线程上。这种处理模式也带来一定的性能损耗,特别是在高并发小请求的 TP 场景下,大量线程消息传递和调度本身对系统的压力也是很大的。如下图所示,task queue pop 占据了大量的 CPU 时间:
03.jpg

其次 MySQL 中的 socket 处理模型比较简单,基本上是采用 non-block socket + ppoll 的方式进行等待控制,且单个线程只等待一个 socket,这种设计在超大规模集群中,性能下降和资源占用都非常可观,亟需多路复用的 IO 模型来解决这种超大规模连接及请求的处理。

2. 全新设计的网络框架

为了解决上面遇到的问题,我们决定对网络处理框架进行全部重新设计,并引入线程池模型,一步到位完成连接、会话、执行线程的全部解耦。首先我们调研了现有的一些高性能网络和异步执行框架。

2.1 高性能网络&执行框架调研

2.1.1 gRPC

04.jpg

grpc-client-server-polling-engine-usage

gRPC是个标准的多个 epoll complete queue 模型。对 listen socket,如果支持 SO_REUSEPORT,开多个分别绑定到每个 epoll 上,如果不支持,开一个,挂在所有 epoll 上。gRPC不建内部线程,用户线程上去等(实际上间接监听,见后文 epoll 模型中描述),accept新连接后,socket fd 随机挂到一个 complete queue 上。
05.jpg

一个客户端的请求会选择一个 channel 中的 socket,作为请求的 TCP,然后绑定到一个 complete queue 上进行处理。
06.jpg

epoll-polling-engine
gRPC 的 epoll 模型中,每个 complete queue 对应实现一个 epoll 的 fd set,同一个 fd 可能会注册到不同的 epoll set 中(complete queue),用户的线程通过调用特定的函数,会对 epoll 进行 poll 等待,在实现中使用 poll 去监听自身的一个 fd(上图的 ev_fd)和 epoll 的 fd(上图的 epoll fd1),因为多个线程去监听 epoll fd,不确定哪个线程会完成 epoll 中注册的事件处理,当实际处理的事务完成后,通过 signal ev_fd 来唤醒真正想等待对应事件的线程(个人理解为 client 模式中,等待请求处理的线程被其他线程把任务完成后,被唤醒得知等待的事件完成)。
gRPC 的这种设计也存在一定缺陷即惊群。标准 epoll_wait() 在多线程等待时,如果有一个事件触发,只会唤醒一个线程,而 gRPC 模型中,由于线程等待在 [ev_fd,epoll_fd] 上,同时是拿 poll 去监听的,一旦任意在 epoll 中的 fd 事件唤醒,会导致所有 poll 在这个 complete queue 上的线程都被唤醒。而且 fd 可能被绑在多个 complete queue 上,影响会更大。这种情况主要出现在 server 模式,因为 listen fd 会绑到每个 complete queue 上,accept 时候会触发,此外多个线程处理一个 complete queue 也会在一个 fd 变成 readable 时候引起惊群。
gRPC 给出的解决方案是,搞个新的 epoll set,命名为 polling island,保证等待的 fd 只存在于一个 polling island,避免因为 fd 存在于多个 complete queue 而导致的多个 queue 上等待的惊群(这里 polling island 的聚合算法细节忽略,本质上会生成一个大的 epoll set,有相同 fd 的 complete queue 实质上会等到一个 polling island 上)。其次为了避免 poll 在 [ev_fd,epoll_fd] 上带来的惊群,改进为 psi_wait 到 epoll 上,同 signal 唤醒对应的等待线程。
由于 gRPC 需要考虑指定等待事件线程的唤醒,以及多线程可能 poll 在同一个 complete queue 上的情况,采用了这种分2层 poll 的模型(改进后的 psi_wait 变成单层模型)。在 server 模型下由于服务线程对等,不存在等待特定客户端返回,可以直接退化成多线程 epoll_wait 形式,效率会更高。在 client 模型下,psi_wait 的模型值得借鉴(低并发下,指定等待线程被唤醒处理事件的概率高,少一层 notify,额外代价低)。

2.1.2 libuv

nodejs 的事件框架,和libevenet和libev类似,单线程的 epoll 模型,所有非阻塞任务都由回调函数完成,阻塞的都会注册到 epoll 中。对于网络 server 服务,一般是在这个线程里面处理返回写入数据,或者分发出来给任务队列做,但数据写回还是要依赖那个处理事件的单线程写。
常见的多线程的使用方法是开多个 instance,类似 gRPC 方式,用 SO_REUSEPORT 开多个 listen socket,在每个 epoll 上监听,每个 epoll 上单线程处理,也可以单个 epoll 专门 listen,accept 的 socket 分发到其他 epoll 上处理。
由于一个 epoll 只有一个线程,数据结构线程不安全,事件队列直接交互信息需要额外同步措施。而且如果存在连接热点(某个连接上请求特别多且计算特别重),对应的事件队列处理线程就会在同样 epoll 上的其他消息响应变慢。而其他队列上的线程也不能分担任务。

2.1.3 Percona

Percona 中实现了 Thread_pool_connection_handler,替代原生 MySQL 网络处理模型。
具体实现为多个线程池 instance,每个池子里面单独调度。每个线程池一个 epoll,在 connection handler 收到请求的时候,将连接注册到 epoll 中。epoll 使用 edge triggered,one shot 模式,仅在连接建立或者 idle 状态再注册进来。线程池第一次工作时候,会选出一个线程作为 listener,该 listener 负责 epoll_wait,在收到请求后,如果高优先级队列为空,会自己也参与到请求处理中,然后让出 listener 角色。
综上,还是比较标准的多实例、单个线程进行 epoll_wait 再进行任务队列分发的模型,针对 listener 线程是否参与数据读取和处理进行了特殊优化(在高优先级队列为空时,参与请求处理,减少低并发下调度导致的 rt)。队列非空情况下,仅对 epoll 事件压任务队列,并不实际 recv,提升网络响应效率。
虽然这个线程池模型设计考虑非常多,也对各种情况进行针对性优化。但在高并发且较差网络环境下,由于 listener 并没有实际处理收包就将任务委派给 worker,worker 需要在对应 socket 上做 recv 到一个完整请求,大部分情况没有问题,但如果请求比较大或者网络比较差,这里会有较长的 io wait,同时线程池的等待扩张机制,也会导致线程池快速扩张。
单线程做 epoll_wait 再分发是比较标准的模型,正如代码注释中写的,listener 线程不干活只分发是个比较糟糕的思路,因为多了一层数据和唤醒 worker 流程,既然 gRPC 中就采用多线程等待 epoll 的方法,这种纯 server 场景更加适合无状态的多 server thread 做,只要保证至少有一个线程等在 epoll_wait 上即可,同时也不要 signal 唤醒对应等待线程,因为 server 本来就不存在这种线程。

2.2 多线程事件驱动框架(XRPC)

基于上述的调研结果,我们针对 PolarDB-X 私有协议的需求,设计了全新的基于epoll的多线程事件驱动网络执行框架,内部命名为 XRPC。整体架构如下图所示:
07.jpg

其具备以下特性:

  • 主体实现在plugin/polarx_rpc中,网络、调度框架与mysql基本独立,提供最大兼容性,便于移植(5.7 or 8.0, X86 or ARM)
  • 全新的基于多线程epoll异步事件驱动框架,包含网络、任务队列、timer等基本组件
  • 统一工作线程逻辑设计,所有线程对等、无状态,自动根据任务分配负载
  • 动态线程池设计

    • 轻载下,线程完成 epoll 事件触发,收包解码,请求处理返回的全流程,减少上下文切换,提供最佳响应;
    • 重载下,线程池通过任务队列处理请求,减少线程调度,提供最大化吞吐量。
  • 多 epoll instance 设计,最大化发挥多核 numa 架构性能

3. 主路径设计

3.1 事件驱动框架

事件驱动框架为线程安全的多线程 epoll 模型,代码主要在epoll.h文件中。
epoll loop 处理逻辑和大多数事件驱动的异步框架类似,为一个大循环。作为一个多线程模型,为提升 epoll 中的等待唤醒性能(避免多线程调用 epoll_wait 中的锁),同时保证多线程事件驱动模型的任务本地执行通用性,默认使用4线程作为基础 epoll 线程,在 epoll 上等待,处理网络事件,其他新增 worker 线程可以在 eventfd 上等待,eventfd 用于在任务队列中新增任务时唤醒线程,eventfd 同时也会注册到 epoll 上作为唤醒条件之一。
和一般的异步事件框架不同的是,由线程池同时承担数据库中的请求执行,不能使用传统的任务队列模型,因为存在数据库请求间的依赖关系(事务之间的锁等待),需要具备动态增加线程,以应对大于线程池数的 wait 以打破事务 wait 的特殊情况,同时这些线程可以优雅退出完成线程池收缩。
针对多线程事件驱动框架,还设计了以下特性以提升性能:

  • timer 使用小顶堆,一次性消费 timer 设计,重复 timer 可以重复插入,不可删除(可以回调中支持逻辑删除)
  • timer 和 work queue 使用 lock free array queue,timer 获取小顶堆的最小超时时间采用 try lock,只有一个线程去处理 timer(都是轻量级任务)
  • eventfd 唤醒后,优先重置,在大量任务堆积时尽可能唤醒更多线程
  • 统计 wait 状态计数,减少不必要的 eventfd notify 流程

08.jpg

3.2 自适应绑核

绑核作为处理 CPU 密集型任务的通用优化手段,XRPC 中也加入了自动的自适应绑核策略:

  • 获取当前可运行的 processor 集合(适配全局的核限制,例如 k8s 的调度绑核)
  • 根据可运行的核,按 physical id,core id,processor 排序
  • 根据配置的 mt epoll 线程数,按序分配
  • epoll 主线程严格绑到一个核心上(也可配置为绑到 group 内所有核上),互不重叠,提供最优简单 TP 类请求处理和调度性能,动态扩容的线程绑定到 group 内的所有核上,提供 AP 类请求重载下更好的 CPU 负载均衡
  • 该策略适应包括 numa 在内的大部分情况,结合上述设计可以将 tcp 连接、session 和执行上下文按 epoll group 分配绑定到不同 CPU 核心及 numa 节点上

09.jpg

3.3 TCP context 生命周期控制

在多线程事件驱动框架下,可销毁对象的生命周期控制是个比较麻烦的事情,目前大部分资源以 TCP 连接为单位进行管理,即 TCP context 为基本的生命周期管理单元。由于多线程 epoll,可能存在摘除 fd 后,其他线程因为线程交换出去仍可以看到 TCP context 情况,造成 dangling ptr 情况,这时就需要一些手段来对 TCP context 进行保护。考虑到 EBR(epoch based reclamation) 代码复杂度,这里采用了 ref cnt + 延迟 reclaim 方式对 TCP context 进行保护,延迟采用 timer 调度(超时为2倍最大 epoll 超时时间),epoll 触发后会先通过 pre_event 加 ref,避免网络包请求处理时间过长而导致 context 被提前释放。

3.4 TCP context 无锁收包设计

为了避免惊群问题,epoll 中对 socket 的触发采用 edge trigger 模式,即收到包后仅仅会唤醒一个线程进行处理,但由于 TCP 包处理以及多线程 epoll 框架的特性,可能多个包才能组成一个完整的请求,而这多个 edge trigger 可能会唤醒不同的线程处理,则需要一个机制去保证只有一个线程去处理同一个 TCP 下的同一个请求的多个网络包,这里我们采用 spin lock try_lock 抢占保证只有一个线程收包,使用 recheck flag+retry 实现第一个收包线程继续收包,避免处理过程中新到达的包漏包,完整流程参考下面的伪代码。采用无锁无等待的设计保证不会浪费任何一个线程资源让其处于锁等待状态(可以立刻去处理 epoll 上的其他 socket 消息)。

/// 伪代码
do {
  if (UNLIKELY(!read_lock_.try_lock())) {
    recheck_.store(true, std::memory_order_release);
    std::atomic_thread_fence(std::memory_order_seq_cst);
    if (LIKELY(!read_lock_.try_lock()))
      break; /// do any possible notify task
  }

  do {
    /// clear flag before read
    recheck_.store(false, std::memory_order_relaxed);

    RECV_ROUTINE;
    
    if (RECV_SUCCESS) {
      recheck_.store(true, std::memory_order_relaxed);

      DEAL_PACKET_ROUTINE;
    }

  } while (recheck_.load(std::memory_order_acquire));

  read_lock_.unlock();
  std::atomic_thread_fence(std::memory_order_seq_cst);
} while (UNLIKELY(recheck_.load(std::memory_order_acquire)));

3.5 TCP context 本地执行设计

前文也提到,设计的这个多线程事件驱动框架,需要在不同任务负载下,以最优的方式进行执行调度。因此需要针对数据库请求做一系列针对性处理策略和优化,主要包含以下几点:

  • 在 recv 线程上下文做解包,充分利用 cache
  • 复用 recv buffer 连续进行解包,较少 malloc 和 memcpy/memmove 代价
  • 大包自动动态扩容 recv buffer 到 dynamic buffer,10s内无大包,完成接收后收缩 buffer,节省内存占用
  • 解包完成后,push 到 session 指令队列时,记录需要 notify 的 session(push前队列为空),尽可能较少 event notify 调用
  • 完成全部消息处理和 push 指令队列后,根据 event 目前处理情况,如果是最后 event 且是 notify set 的最后一个 session,直接当前上下文开始请求处理(本地线程执行,最大化利用 cache),其他的都推到框架的任务队列中处理,代码逻辑如下所示
/// dealing notify or direct run outside the read lock.
if (!notify_set.empty()) {
  /// last one in event queue and this last one in notify set,
  /// can run directly
  auto cnt = notify_set.size();
  for (const auto &sid : notify_set) {
    if (0 == --cnt && index + 1 == total) {
      /// last one in set and run directly
      DBG_LOG(("tcp_conn run session %lu directly", sid));
      auto s = sessions_.get_session(sid);
      if (s)
        s->run();
    } else {
      /// schedule in work task
      DBG_LOG(("tcp_conn schedule task session %lu", sid));
      epoll_.push_work((new CdelayedTask(sid, this))->gen_task());
    }
  }
}
  • 当前上下文执行和任务队列执行比例可以通过 epoll_wait 的 event 数量控制(高压力情况下,events 基本上都是满的),执行比例满足 slos_cnt-1 : 1 的比例,slots_cnt 越大,任务队列执行比例越大,如下图,epoll_events_per_thread(slos_cnt) = 4,基本上是3:1比例

10.jpg

3.6 TCP context 发包设计

考虑到有流程同时包正常情况下不会很大,采用阻塞模型(大部分查询结果集 TCP sndbuf 就能 hold 住,大结果集的情况,会有外置流控和缓冲机制,保证即使阻塞也不会有太大影响)。同时通过外置 mutext 避免跨 session 串包,确保 session 解耦后的数据正确性。同时每个 session 内置 encoder 自带 buffer 池,自己满了或需要 flush 时候再拿 tcp 的锁 send,保证 encoder 性能的同时减少锁 TCP 通道的时间。如下代码展示了 send 的各种报错处理。

inline int wait_send() {
  auto timeout = net_write_timeout;
  if (UNLIKELY(timeout > MAX_NET_WRITE_TIMEOUT))
    timeout = MAX_NET_WRITE_TIMEOUT;
  ::pollfd pfd{fd_, POLLOUT | POLLERR, 0};
  return ::poll(&pfd, 1, static_cast<int>(timeout));
}

/// blocking send
bool send(const void *data, size_t length) final {
  if (UNLIKELY(fd_ < 0))
    return false;
  auto ptr = reinterpret_cast<const uint8_t *>(data);
  auto retry = 0;
  while (length > 0) {
    auto iret = ::send(fd_, ptr, length, 0);
    if (UNLIKELY(iret <= 0)) {
      auto err = errno;
      if (LIKELY(EAGAIN == err || EWOULDBLOCK == err)) {
        /// need wait
        auto wait = wait_send();
        if (UNLIKELY(wait <= 0)) {
          if (wait < 0)
            tcp_warn(errno, "send poll error");
          else
            tcp_warn(0, "send net write timeout");
          fin();
          return false;
        }
        /// wait done and retry
      } else if (EINTR == err) {
        if (++retry >= 10) {
          tcp_warn(EINTR, "send error with EINTR after retry 10");
          fin();
          return false;
        }
        /// simply retry
      } else {
        /// fatal error
        tcp_err(err, "send error");
        fin();
        return false;
      }
    } else {
      retry = 0; /// clear retry
      ptr += iret;
      length -= iret;
    }
  }
  return true;
}

3.7 session 设计

MySQL 中,提供了一个供外部使用的 session 对象,即 MYSQL_SESSION,XRPC 中的会话即为对MYSQL_SESSION包装。除此之外,XRPC 还做了以下优化,以适配其和计算节点通信:

  • 自己实现结果集 encoder,send buffer
  • 自带指令队列用于流水线请求
  • 类似 TCP context 的无锁单线程执行机制,实现 session 内单线程顺序执行,快速释放其他线程资源到其他请求上
  • 优化 MYSQL_SESSION 的 valid 机制,干掉全局 session 锁
  • 优化 srv_session 中 thread local 里面 THD 等生命周期控制,消除 dangling ptr 问题

3.8 encoder 重构

结果集 encoder 参考了最新 MySQL X plugin 的设计思路,重构同时做了以下优化:

  • 使用 protobuf-lite,减少了 binary 体积
  • 完全脱离 MySQL X plugin 依赖,砍掉了很多用不到的过度抽象设计
  • 基于 protobuf 消息,底层 api 直接在 buffer 上生成消息,primitives 采用参数模板的直接对应 msg 生成 hardcode 的编码
  • 大小端机器下之间指针强转编码,优化 int16,int32,int64 编码效率,如下所示
template <> struct Fixint_length<8> {
  template <uint64_t value> static void encode(uint8_t *&out) { // NOLINT
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
    *reinterpret_cast<uint64_t *>(out) = __builtin_bswap64(value);
    out += 8;
#else
    *reinterpret_cast<uint64_t *>(out) = value;
    out += 8;
#endif
  }

  static void encode_value(uint8_t *&out, const uint64_t value) { // NOLINT
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
    *reinterpret_cast<uint64_t *>(out) = __builtin_bswap64(value);
    out += 8;
#else
    *reinterpret_cast<uint64_t *>(out) = value;
    out += 8;
#endif
  }
};
}

3.9 XPLAN 及 chunk encoder 重构

针对私有协议的2项重要功能:

  • 执行计划传输执行
  • 列式数据传输

XRPC 对其进行了移植和部分编码精简优化,提高了其兼容性和修复了一些潜藏已久的bug。

3.10 可调参数&内部状态可观测设计

为了在不同平台、不同规格、不同负载下达到最优的性能,XRPC 提供了大量的可调参数:

变量名 默认 说明
polarx_rpc_enable_perf_hist [ON\ OFF] OFF 是否开启XRPC性能统计直方图(性能调优时候用)
polarx_rpc_enable_tasker [ON\ OFF] ON 是否允许扩展线程池
polarx_rpc_enable_thread_pool_log [ON\ OFF] ON 是否打开线程池log
polarx_rpc_epoll_events_per_thread [1-16] 4 每个epoll线程处理的epoll事件数
polarx_rpc_epoll_extra_groups [0-32] 0 额外的epoll线程池组数,一般不配置
polarx_rpc_epoll_group_ctx_refresh_time [1000-60000] 10000 每个epoll线程池组共享session刷新时间,用于释放超时的session,单位ms,默认10s
polarx_rpc_epoll_group_dynamic_threads [0-16] 0 每个epoll线程池组中期待的非基础(动态扩展)线程数,一般设置为0
polarx_rpc_epoll_group_dynamic_threads_shrink_time [1000-600000] 10000 epoll线程池组中非基础(动态扩展)线程收缩的延迟时间,用于在高并发负载下来后,扩展了的线程持续存活时间,单位ms,默认为10s
polarx_rpc_epoll_group_tasker_extend_step [1-50] 2 在并发上来后,基于排队任务扩展线程池,线程池扩展的步长(一次扩展多少线程)
polarx_rpc_epoll_group_tasker_multiply [1-50] 3 在并发上来后,基于排队任务扩展线程池,线程池扩展的阈值因数,即当 排队任务>该因子*工作线程数 时,线程池会扩展
polarx_rpc_epoll_group_thread_deadlock_check_interval [1-10000] 500 检查因为内部事务或者其他外部等待依赖导致死锁的检测时间,单位ms,默认500ms
polarx_rpc_epoll_group_thread_scale_thresh [0-100] 2 基于线程等待原因分析的线程池扩容机制,该参数用于指定至少有多少线程等待后才去扩容,实际最大允许为线程池中基础线程数-1,最小为0,默认给2
polarx_rpc_epoll_groups [0-128] 0 默认epoll组个数,以为epoll存在大锁,还是多组打散,默认是0,自动根据核数和每组的基础线程数计算
polarx_rpc_epoll_threads_per_group [1-128] 4 每个epoll组的线程数,越小锁冲突越小,但越难发挥线程池自动调度能力,默认4
polarx_rpc_epoll_timeout [1-60000] 10000 每次调用epoll的超时时间,单位ms,默认10s
polarx_rpc_epoll_work_queue_capacity [128-4096] 256 每个epoll组的任务队列深度
polarx_rpc_force_all_cores [ON\ OFF] OFF 是否突破执行核限制绑到所有CPU核上,默认不允许
polarx_rpc_galaxy_protocol [ON\ OFF] OFF 是否开启galaxy protocol协议,默认不开
polarx_rpc_galaxy_version [0-127] 0 galaxy protocol协议版本
polarx_rpc_max_allowed_packet [4096-1073741824] 67108864 XRPC的最大包限制,默认64MB
polarx_rpc_max_cached_output_buffer_pages [1-256] 10 每个session的默认输出缓冲大小,单位是页,每个页4K,默认10个
polarx_rpc_max_epoll_wait_total_threads [0-128] 0 最多允许等待epoll的线程数,默认是0,自动计算的,为 epoll组数*每个epoll的基础线程数
polarx_rpc_max_queued_messages [16-4096] 128 每个session允许的最大排队流水线请求深度
polarx_rpc_mcs_spin_cnt [1-10000] 2000 内部用到的mcs自旋锁自旋次数,默认2000,超过后yield
polarx_rpc_min_auto_epoll_groups [1-128] 5.7 16 8.0 32 自动计算的最少的epoll组数
polarx_rpc_multi_affinity_in_group [ON\ OFF] OFF 公共云通过参数默认打开 是否允许epoll组内线程绑定到多个核上,开启后tpch多个大任务倾斜长尾现象会缓解
polarx_rpc_net_write_timeout [1-7200000] 10000 网咯写超时时间,单位ms,默认10s
polarx_rpc_request_cache_instances [1-128] 16 Sql/Xplan cache的分组数,减少锁冲突,默认16
polarx_rpc_request_cache_max_length [128-1073741824] 1048576 允许缓存到cache的请求大小,单位字节,默认值缓存小于1MB的sql
polarx_rpc_request_cache_number [128-16384] 1024 Sql/Xplan cache缓存slot数,sql和xplan是单独的空间,每个都有默认1024个slot
polarx_rpc_session_poll_rwlock_spin_cnt [1-10000] 1 RW自旋锁自旋数,默认1,超过后yield
polarx_rpc_shared_session_lifetime [1000-3600000] 60000 每个epoll组*享session的最长生存时间
polarx_rpc_tcp_fixed_dealing_buf [4096-65536] 4096 每个tcp的解析缓冲大小,单位字节,默认4K
polarx_rpc_tcp_keep_alive [1-7200] 30 tcp的keep alive参数,单位s,默认30s
polarx_rpc_tcp_listen_queue [128-4096] 128 tcp accept队列深度,默认128
polarx_rpc_tcp_recv_buf [0-2097152] 0 tcp recv buffer,默认0用系统默认值
polarx_rpc_tcp_send_buf [0-2097152] 0 tcp send buffer,默认0用系统默认值
rpc_port [0-65536] 33660 XRPC端口号
rpc_use_legacy_port [ON\ OFF] ON 是否兼容模式使用polarx_port的值作为端口号

同时为了确保运行时观察工作状态,XRPC开放了一部分全局变量用于观察内部线程数和会话数量:

全局状态变量 说明 样例
polarx_rpc_inited XRPC是否启动成功 ON
polarx_rpc_plan_evict xplan cache LRU中淘汰数 123
polarx_rpc_plan_hit xplan cache LRU中命中数 4234244
polarx_rpc_plan_miss xplan cache LRU中未命中数 42424
polarx_rpc_sql_evict sql cache LRU中淘汰数 123
polarx_rpc_sql_hit sql cache LRU中命中数 4234244
polarx_rpc_sql_miss sql cache LRU中未命中数 42424
polarx_rpc_tcp_closing 正在关闭的TCP数 0
polarx_rpc_tcp_connections 当前TCP数 32
polarx_rpc_threads XRPC中的总线程数 64
polarx_rpc_total_sessions XRPC中的总session数(包含共享session) 38
polarx_rpc_worker_sessions XRPC中的工作session数(CN的后端session) 32

由于内部调度的复杂性,XRPC也自带了内部高精度时钟统计各阶段的耗时直方图,便于定位性能问题和调优。

mysql> show variables like '%perf_hist%';
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| polarx_rpc_enable_perf_hist | OFF   |
+-----------------------------+-------+
1 row in set (0.00 sec)

mysql> set global polarx_rpc_enable_perf_hist = 'ON';
Query OK, 0 rows affected (0.01 sec)

mysql> show variables like '%perf_hist%';
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| polarx_rpc_enable_perf_hist | ON    |
+-----------------------------+-------+
1 row in set (0.00 sec)

mysql> call xrpc.perf_hist('all')\G

上述命令会开启运行时的各网络、调度、执行阶段的耗时直方图,主要有:

  • work queue,工作队列获取任务的耗时
  • recv first,收第一个网络包并处理的耗时
  • recv all,收到一个完整请求网络包并处理解码的耗时
  • schedule,一个请求从接收到调度开始执行的延迟
  • run,一个请求在 mysql 中的执行耗时

数据样例如图所示,采用指数分段直方图,有利于分析各种响应分布及长尾等情况。
11.png

通过在调用存储过程中指定不同的统计项,可以显示单独的直方图,all 会显示全部5项直方图。call xrpc.perf_hist('reset');可以重置直方图,便于在压测稳定后,观察稳态的耗时分布。

3.11 其他优化

XRPC 在开发过程中,也借鉴了不同高性能数据结构的实现,力求在网络、调度部分提供最优的性能体验:

  • 参考 rust crossbeam 中的 backoff 实现了指数级退让机制
  • 参考 mcs spin lock 等 spin lock 思路,优化内部 spin lock 和 RW spin lock
  • 参考 rust crossbeam 中的 array queue 实现无锁任务队列
  • 大量的 likely 和 unlikely 分支预测优化
  • 大量的无锁算法实现

4. 性能测试

4.1 DN 上定性评估

针对我们需要优化的线程调度问题,我们通过火焰图进行评估确认。
12.png

从上图的XRPC的点查压测火焰图中可以看到,请求执行占用CPU提升到71.79%,CPU资源有较好的应用。
对比文章最开头的旧私有协议的火焰图,可见CPU的利用有了非常可观的提升。
13.png

下图展示了 MySQL sql 协议在 MySQL connecter 在 JDBC 下点查压测的火焰图,有效执行 CPU 利用率为64.94%,也低于 XRPC 下的利用率。
14.jpg

4.2 DN 上定量评估

4.2.1 echo server

评估一个网络框架收发包能力的最直接的方式就是写个echo server进行压测,这里我们将 XRPC 的网络执行框架和阿里内部常用的 libeasy 进行对比,libeasy 的使用代码在 libeasy_bench 目录下,结果如下,测试环境为 64 core 物理机,XRPC 略高于 libeasy 64线程同步模式的性能。

并发 XRPC libeasy async 16 listen 64 worker libeasy async 64 listen 64 worker libeasy sync 64 threads
2 55414.457 37486.25 37564.242 52956.703
4 107255.27 73971.2 74943.016 106999.3
8 203521.3 145596.88 146340.73 208922.2
16 392131.56 274835.03 276866.94 390191.97
32 703287.0 480919.72 481255.5 715153.44
64 1175622.9 799120.2 757774.44 1221337.8
128 1837832.9 1047939.56 1157251.1 1844174.2
256 2649329.2 1345222.0 1550693.4 2556187.2
512 3291273.0 1397924.2 1342323.6 3182367.2
1024 3612264.8 1360113.9 1440107.8 3415289.2

4.2.2 select 1

对比新老私有协议及 JDBC 在 select 1 下的性能。新架构性能更高,且在高并发下也能稳定运行。

  • 64 core 物理机
并发 JDBC 老架构 新架构
2 29719.084 26994.299 29986.0
4 63485.3 59082.09 66999.305
8 126720.66 115059.984 126951.61
16 242323.53 217389.78 232871.14
32 448065.38 366213.53 423825.47
64 753734.6 588699.25 733777.9
128 1038840.2 821294.5 1150645.6
256 1182257.2 966579.4 1473572.4
512 1177471.2 843260.1 1555356.1
1024 1147890.2 825537.44 1514292.5
2048 - - 1455882.8
4096 - - 1200290.2
  • 104 core 物理机
并发 JDBC 老架构 新架构
2 36907.62 33711.63 36453.35
4 80340.96 67205.28 79440.055
8 159827.02 137136.58 156556.69
16 299065.2 264378.7 298600.28
32 582958.06 506158.16 538147.75
64 987595.2 854529.56 917313.56
128 1383830.9 1195628.9 1348939.5
256 1622596.8 1554815.1 1685460.8
512 1799647.1 1470166.8 1941278.5
1024 1815061.2 916179.2 2084961.6
2048 1673776.8 - 2008663.9
4096 - - 1820561.0

4.2.3 点查

  • sysbench表

    • --tables='1' --table-size='100000'
    • oltp_point_select
  • JDBC 走 SQL 查询
  • XRPC 测试了走 SQL 查询和走 XPLAN 查询
并发 64c JDBC 64c xrpc+xplan 64c xrpc+sql 104c JDBC 104c xrpc+xplan 104c xrpc+sql
2 16578.027 23809.62 17772.223 25471.36 32103.791 25454.455
4 36202.38 47754.45 37122.574 54391.56 62056.797 54073.594
8 71760.65 97431.516 73274.34 106510.695 127509.5 106510.695
16 137715.45 176151.16 137329.8 195314.94 245143.45 196580.03
32 254749.1 311442.44 239416.25 367031.2 415063.97 356066.56
64 413138.38 526345.1 407636.72 640735.9 721447.75 604598.06
128 502932.12 720127.94 570637.7 919598.2 1052270.2 939035.44
256 539180.5 843516.9 628808.2 1084268.9 1281496.0 1163551.5
512 534332.7 854824.5 610362.25 1100764.5 1340563.2 1220010.0
1204 510401.28 843499.75 623204.1 1040283.5 1320433.1 1187091.4
2048 - 835596.94 597368.94 - 1241896.4 1102568.6
4096 - 771388.9 527704.0 - 1131214.1 987188.8

4.3 PolarDB-X 开箱性能

  • XRPC 在公共云 5.4.17 默认打开(老版本升级上来的,可以在存储节点参数中,将 new_rpc = 'ON' 打开)
  • XRPC 在开源版中,rpc_version=2 时默认打开
  • XRPC 对 OLTP 类请求性能有较大提升,对 OLAP 类请求性能持平

下列图片为,阿里云官网购买 4*8c64g 的实例,按官网测试文档配置后,新老私有协议的性能对比。

sysbench DRDS 模式性能对比
15.jpg

sysbench 单表打散模式性能对比
16.jpg

TPCC DRDS 模式性能对比
17.jpg

TPCC auto 模式性能对比
18.jpg

各种不同实例规格下的性能可以参考下列性能测试文档中,版本5.4.17的性能数据:

5. 总结

PolarDB-X 的私有协议2.0,内部也称之为 XRPC,完全重构了存储节点上的私有协议网络、调度、执行框架,实现了连接、会话、线程的解绑,并完全摆脱了 MySQL X plugin 的依赖,成为完全独立的 plugin。同时实现了在 MySQL 5.7 8.0 下同一套代码,支持 ARM 等国产化平台,极大提升了代码的可维护性和自主可控性。同时针对原始设计中的局限进行了优化改进,对 OLTP 类请求有着普适的性能提升,也为未来添加新功能提供了更多的可能。

本文来源:PolarDB-X知乎号
作者:辰宇

作者介绍
目录

相关产品

  • 云原生分布式数据库 PolarDB-X