PolarDB-X 私有協議2.0

本文主要介紹私有協議2.0,也即XRPC的背景、總體設計、相關技術實現細節和性能測試結果。

私有協議作爲解決 PolarDB-X 中計算節點和存儲節點複雜通信需求的技術手段,在 PolarDB-X 2.0 公共雲版上線初期就作爲重要的功能一起發佈了。同時在PolarDB-X開源版中,也作爲唯一的和後端存儲節點的通信鏈路,在數據庫請求主鏈路中起着至關重要的作用。

然而隨着 PolarDB-X 的發展,存儲節點 5.7 & 8.0 的兼容問題,國產化 ARM 平臺的適配等需求接踵而至,私有協議基於 MySQL X plugin 的網絡框架設計逐漸變得力不從心,因此對存儲節點上私有協議服務端的代碼重構就勢在必行,XRPC即私有協議2.0應運而生。

難以解決的侷限性

私有協議最初的設計在前文已有說明,其旨在解決計算節點和存儲節點連接數爆炸的問題。通過連接會話解耦,將傳統的 MySQL 會話機制優化爲類 RPC 機制,通過會話 ID 實現在同一個通信信道上傳輸多個會話。由於當時對於快速上線的需求,開發相對困難的存儲節點端,使用了相對成熟完整的 MySQL X plugin 進行擴展改造,基於其網絡處理調度框架進行消息擴展,完成了私有協議 server 端的開發。其架構如下圖所示:

 

該網絡執行架構成功協助解決了 PolarDB-X 所遇到的後端連接爆炸問題,同時基於 protobuf 消息的擴展,也實現了很多計算節點和存儲節點的高級交互功能,幫助 PolarDB-X 從傳統的中間件模式邁入了完整分佈式數據庫的行列。

誠然,這個框架也是存在一定的歷史侷限性的,由於 MySQL X plugin 是以 one thread per connection 爲理念設計的,每個處理session都綁定了一個執行線程,同時請求消息接收是由額外的線程處理,並分發到對應的工作會話線程上。這種處理模式也帶來一定的性能損耗,特別是在高併發小請求的 TP 場景下,大量線程消息傳遞和調度本身對系統的壓力也是很大的。如下圖所示,task queue pop 佔據了大量的 CPU 時間:

其次 MySQL 中的 socket 處理模型比較簡單,基本上是採用 non-block socket + ppoll 的方式進行等待控制,且單個線程只等待一個 socket,這種設計在超大規模集羣中,性能下降和資源佔用都非常可觀,亟需多路複用的 IO 模型來解決這種超大規模連接及請求的處理。

全新設計的網絡框架

爲了解決上面遇到的問題,我們決定對網絡處理框架進行全部重新設計,並引入線程池模型,一步到位完成連接、會話、執行線程的全部解耦。首先我們調研了現有的一些高性能網絡和異步執行框架。

高性能網絡&執行框架調研

gRPC

vvv

grpc-client-server-polling-engine-usage

gRPC是個標準的多個 epoll complete queue 模型。對 listen socket,如果支持 SO_REUSEPORT,開多個分別綁定到每個 epoll 上,如果不支持,開一個,掛在所有 epoll 上。gRPC不建內部線程,用戶線程上去等(實際上間接監聽,見後文 epoll 模型中描述),accept新連接後,socket fd 隨機掛到一個 complete queue 上。

一個客戶端的請求會選擇一個 channel 中的 socket,作爲請求的 TCP,然後綁定到一個 complete queue 上進行處理。

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,額外代價低)。

libuv

nodejs 的事件框架,和libevenet和libev類似,單線程的 epoll 模型,所有非阻塞任務都由回調函數完成,阻塞的都會註冊到 epoll 中。對於網絡 server 服務,一般是在這個線程裏面處理返回寫入數據,或者分發出來給任務隊列做,但數據寫回還是要依賴那個處理事件的單線程寫。

常見的多線程的使用方法是開多個 instance,類似 gRPC 方式,用 SO_REUSEPORT 開多個 listen socket,在每個 epoll 上監聽,每個 epoll 上單線程處理,也可以單個 epoll 專門 listen,accept 的 socket 分發到其他 epoll 上處理。

由於一個 epoll 只有一個線程,數據結構線程不安全,事件隊列直接交互信息需要額外同步措施。而且如果存在連接熱點(某個連接上請求特別多且計算特別重),對應的事件隊列處理線程就會在同樣 epoll 上的其他消息響應變慢。而其他隊列上的線程也不能分擔任務。

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 本來就不存在這種線程。

多線程事件驅動框架(XRPC)

基於上述的調研結果,我們針對 PolarDB-X 私有協議的需求,設計了全新的基於epoll的多線程事件驅動網絡執行框架,內部命名爲 XRPC。整體架構如下圖所示:

其具備以下特性:

  • 主體實現在plugin/polarx_rpc中,網絡、調度框架與mysql基本獨立,提供最大兼容性,便於移植(5.7 or 8.0, X86 or ARM)
  • 全新的基於多線程epoll異步事件驅動框架,包含網絡、任務隊列、timer等基本組件
  • 統一工作線程邏輯設計,所有線程對等、無狀態,自動根據任務分配負載
  • 動態線程池設計
    • 輕載下,線程完成 epoll 事件觸發,收包解碼,請求處理返回的全流程,減少上下文切換,提供最佳響應;
    • 重載下,線程池通過任務隊列處理請求,減少線程調度,提供最大化吞吐量。
  • 多 epoll instance 設計,最大化發揮多核 numa 架構性能

主路徑設計

事件驅動框架

事件驅動框架爲線程安全的多線程 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 流程

自適應綁核

綁核作爲處理 CPU 密集型任務的通用優化手段,XRPC 中也加入了自動的自適應綁核策略:

  • 獲取當前可運行的 processor 集合(適配全局的核限制,例如 k8s 的調度綁核)
  • 根據可運行的核,按 physical id,core id,processor 排序
  • 根據配置的 mt epoll 線程數,按序分配
  • epoll 主線程嚴格綁到一個核心上(也可配置爲綁到 group 內所有核上),互不重疊,提供最優簡單 TP 類請求處理和調度性能,動態擴容的線程綁定到 group 內的所有核上,提供 AP 類請求重載下更好的 CPU 負載均衡
  • 該策略適應包括 numa 在內的大部分情況,結合上述設計可以將 tcp 連接、session 和執行上下文按 epoll group 分配綁定到不同 CPU 核心及 numa 節點上

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 被提前釋放。

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)));

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比例

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;
}

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 問題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 } }; }
    XPLAN 及 chunk encoder 重構
    針對私有協議的2項重要功能:
  • 執行計劃傳輸執行
  • 列式數據傳輸

XRPC 對其進行了移植和部分編碼精簡優化,提高了其兼容性和修復了一些潛藏已久的bug。

可調參數&內部狀態可觀測設計

爲了在不同平臺、不同規格、不同負載下達到最優的性能,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 中的執行耗時

數據樣例如圖所示,採用指數分段直方圖,有利於分析各種響應分佈及長尾等情況。

通過在調用存儲過程中指定不同的統計項,可以顯示單獨的直方圖,all 會顯示全部5項直方圖。call xrpc.perf_hist('reset');可以重置直方圖,便於在壓測穩定後,觀察穩態的耗時分佈。

其他優化

XRPC 在開發過程中,也借鑑了不同高性能數據結構的實現,力求在網絡、調度部分提供最優的性能體驗:

  • 參考 rust crossbeam 中的 backoff 實現了指數級退讓機制
  • 參考 mcs spin lock 等 spin lock 思路,優化內部 spin lock 和 RW spin lock
  • 參考 rust crossbeam 中的 array queue 實現無鎖任務隊列
  • 大量的 likely 和 unlikely 分支預測優化
  • 大量的無鎖算法實現性能測試
    DN 上定性評估
    針對我們需要優化的線程調度問題,我們通過火焰圖進行評估確認。

從上圖的XRPC的點查壓測火焰圖中可以看到,請求執行佔用CPU提升到71.79%,CPU資源有較好的應用。

對比文章最開頭的舊私有協議的火焰圖,可見CPU的利用有了非常可觀的提升。

下圖展示了 MySQL sql 協議在 MySQL connecter 在 JDBC 下點查壓測的火焰圖,有效執行 CPU 利用率爲64.94%,也低於 XRPC 下的利用率。

DN 上定量評估

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

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

點查

  • 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

PolarDB-X 開箱性能

  • XRPC 在公共雲 5.4.17 默認打開(老版本升級上來的,可以在存儲節點參數中,將 new_rpc = 'ON' 打開)
  • XRPC 在開源版中,rpc_version=2 時默認打開
  • XRPC 對 OLTP 類請求性能有較大提升,對 OLAP 類請求性能持平

下列圖片爲,阿里雲官網購買 4*8c64g 的實例,按官網測試文檔配置後,新老私有協議的性能對比。

sysbench DRDS 模式性能對比

sysbench 單表打散模式性能對比

TPCC DRDS 模式性能對比

TPCC auto 模式性能對比

各種不同實例規格下的性能可以參考下列性能測試文檔中,版本5.4.17的性能數據:

總結

PolarDB-X 的私有協議2.0,內部也稱之爲 XRPC,完全重構了存儲節點上的私有協議網絡、調度、執行框架,實現了連接、會話、線程的解綁,並完全擺脫了 MySQL X plugin 的依賴,成爲完全獨立的 plugin。同時實現了在 MySQL 5.7 8.0 下同一套代碼,支持 ARM 等國產化平臺,極大提升了代碼的可維護性和自主可控性。同時針對原始設計中的侷限進行了優化改進,對 OLTP 類請求有着普適的性能提升,也爲未來添加新功能提供了更多的可能。

作者:辰宇

點擊立即免費試用雲產品 開啓雲上實踐之旅!

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章