Cobar SQL審計的設計與實現

捉蟲大師發表於2021-11-04

背景介紹

Cobar簡介

Cobar 是阿里開源的一款資料庫中介軟體產品。

在業務高速增長的情況下,資料庫往往成為整個業務系統的瓶頸,資料庫中介軟體的出現就是為了解決資料庫瓶頸而產生的一種中間層產品。

在軟體工程中,沒有什麼問題是加一層中間層解決不了的,如果有,再加一層。

一款proxy型別(本文不討論client SDK型別的資料庫中介軟體)的資料庫中介軟體具備以下能力:

  • 支援資料庫的透明代理,做到使用者無感知
  • 能夠水平、垂直拆分資料庫和表,橫向擴充套件資料庫的容量和效能
  • 讀和寫的分離,降低主庫壓力
  • 複用資料庫連線,降低資料庫的連線消耗
  • 能夠檢測資料庫叢集的各種故障,做到快速failover
  • 足夠穩定可靠,效能足夠好

而本文的主角Cobar除了讀寫分離外其他特性都支援的很好,而且基於Cobar開發讀寫分離的特性並不是一件很難的事。

SQL審計

筆者有幸也曾在公司內的Cobar上做過定製開發,開發的功能是SQL審計。

從資料庫產品的運營角度看,統計分析執行過的SQL是一個必要的功能;從安全形度看,資訊洩露、異常SQL也需要被審計。

SQl審計需要審計哪些資訊?通過調研,大致確定要採集執行的SQL、執行時間、來源host、返回行數等幾個維度。

SQL審計的需求很簡單,但就算是一個很簡單的需求放在資料庫中介軟體的高併發、低延遲,單機QPS可達幾萬到十幾萬的場景下都需要謹慎考慮,嚴格測試。

舉個例子,獲取作業系統時間,在Java中直接呼叫 System.currentTimeMillis(); 就可以,但在Cobar中如果這麼獲取時間,就會導致效能損耗非常嚴重(怎麼解決?去Cobar的github倉庫上看看程式碼吧)。

技術方案

大方向

經調研,SQL審計實現的方向大致有兩種

  • 一種是比較容易想到的直接修改Cobar程式碼,在需要收集資訊的地方埋點
  • 另一種是阿里雲資料庫提供的方案,通過抓取資料庫的通訊流量進行分析。

考慮到技術的複雜度,我們選擇了較為簡單的第一種實現方式。

SQL審計在Cobar中屬於“錦上添花”的需求,不能因為這個功能導致Cobar效能下降,更不能導致Cobar不可用,所以必須遵循以下兩點:

  • 效能儘可能接近無SQL審計版本
  • 無論如何不能造成Cobar不可用

對於效能的損耗,沒有度量就沒法優化,於是使用sysbench(一種資料庫基準測試工具)來對現在版本的Cobar進行壓測。

Cobar部署在4C8G的機器上,mysql部署在效能足夠好的物理機上,壓出了5.5w/s的基準,後續的版本都和這個數值進行比對。

由於採取了侵入Cobar程式碼的方式,想對Cobar造成影響最小,就需要保持程式碼最小的修改,於是採取了agent的方案。

這樣可以保持程式碼的最小修改,只需要打點採集並傳輸給agent,向遠端傳輸審計資訊的邏輯就只需要在agent中處理即可,向遠端傳輸資訊幾乎在一開始就確定了用kafka,這樣也能保持Cobar不引入新的第三方依賴,保持程式碼的乾淨(要知道Cobar的第三方依賴只有log4j),讓kafka和Cobar保持在兩個JVM中,更是一種隔離。於是有了下圖的架構初稿

image

通過上圖梳理出了兩個關鍵技術點:執行緒通訊和程式通訊。

程式通訊容易理解,為什麼這裡還涉及執行緒通訊?

首先Cobar的execute執行緒是執行SQL的主執行緒,如果在這個執行緒中去進行程式通訊,那效能肯定被消耗的體無完膚。於是只能丟給審計執行緒去做,這樣對Cobar的效能影響最小。

程式間通訊

先說程式間的通訊,這塊稍微簡單點,我們只需要羅列出可用的程式間通訊方式,然後對比優缺點,選擇一個合適的使用即可

image

首先Cobar是Java編寫,於是我們框定了範圍:TCP、UDP、UnixDomainSocket、檔案。

經過調研,UnixDomainSocket與平臺相關性太強,且沒有官方的實現,只有第三方的實現(如junixsocket),測試下來,不同linux的版本支援都不一致,所以這裡直接排除。

寫檔案會導致高IO,甚至有寫滿磁碟的風險,畢竟在如此高的併發之下,遂排除。

最終在TCP和UDP中選擇,考慮效能UDP比TCP好,且TCP還得自己解決粘包問題,於是我們選擇了UDP。其實想想,SQL審計需求類似日誌收集、metric上報,許多日誌收集、metric上報都是採取UDP的方式。

執行緒間通訊

如果說程式間通訊拍拍腦袋就能決定,是因為他並不直接影響Cobar,他是審計執行緒與agent程式間的通訊。然而執行緒間的通訊則直接決定了對Cobar的效能影響,必須謹慎。

執行緒間通訊必須通過一個中間的緩衝buffer來中轉,我們對這個buffer有如下要求

  • 有界,無界就可能會導致記憶體溢位
  • 投遞不能阻塞,阻塞會導致夯住主執行緒,極大影響Cobar效能
  • 可以無序,為了保證Cobar可用性,甚至可以在極端情況下丟失一些資料
  • 執行緒安全,高併發下如果執行緒不安全,資料就會錯亂
  • 高效能

Java內建佇列

Java中內建的佇列可以充當這個buffer

image

有界的只有ArrayBlockingQueue和LinkedBlockingQueue,然而他們都是加鎖的,直覺告訴我,他的效能不會太好。

想到Java中CurrentHashMap和LongAdder都是通過分段來解決鎖衝突的,於是打算使用多個ArrayBlockingQueue來構造這個buffer

image

實測下來,只達到了4.7w/s,效能損失約10%

Disruptor

Java內建的佇列屬於有鎖佇列,那麼有沒有不加鎖且有界的佇列呢?搜尋後發現了一款開源的無鎖佇列實現Disruptor,大量的產品如Log4j2等都使用了Disruptor。它是一種環形的資料結構,使用了Java中的CAS代替了鎖,且有許多細節上的效能優化,導致他的效能非常強悍。

image

但很可惜的是,在測試時發現當Disruptor的buffer寫滿之後,再寫就會阻塞,這和我們的需求不符合,如果主執行緒發生阻塞將是災難性的,於是放棄。

SkyWalking的RingBuffer

剛好當時組內同學在研究SkyWalking,SkyWalking是一款開源的應用效能監控系統,包括指標監控,分散式追蹤,分散式系統效能診斷。

他的原理是利用Java的位元組碼修改技術在呼叫處插入埋點,採集資訊上報。和Cobar的採集上報過程類似。

那麼他的RingBuffer是如何實現的呢?其實非常簡單,緩衝區就是一個陣列,每次投遞時獲取一個沒有寫入資料的陣列下標即可,在多執行緒下只要保證獲取的下標不會被兩個執行緒同時獲取即可。資料的寫入速度快慢就看這個下標獲取是否高效即可,如下圖:

image

獲取陣列下標和Disruptor類似也是使用了CAS,但他實現非常簡單,甚至有點粗糙,但他可以在寫滿時選擇是阻塞、覆蓋或是忽略,我們選擇覆蓋這個策略,在極端情況下丟掉老資料來換取Cobar的可用性。我們測試了一下使用多個SkyWalking的RingBuffer的場景,結果只有3w/s,損失45%效能。

於是我們對這個Ringbuffer進行了一些優化

image

這個優化主要是將CAS換成incrementAndGet,這樣就能利用到JDK8對incrementAndGet的優化,在JDK8之前,incrementAndGet底層也是CAS,但在JDK8之後,incrementAndGet使用了fetch-and-add(CPU指令),效能要強勁很多。這塊具體的介紹和程式碼可以參考《一種極致效能的緩衝佇列》

除了這個主要的優化外,還參考Disruptor進行對SkyWalking進行了快取行填充優化,最後達到了5.4w/s,效能損失僅僅1.8%,非常給力,於是使用了這個版本的Ringbuffer作為Cobar SQL審計的快取區。

優化後的Ringbuffer也回饋給了SkyWalking社群,SkyWalking作者讚賞這是一個“intersting contribution”。

image

總結

Cobar的SQL審計在上線後穩定支撐了公司所有Cobar叢集,是承載最高QPS的系統之一。

回頭來看對效能的極致追求可能或許過於"偏執",創造的收益在旁人眼裡看來並沒有那麼大,加一臺機器就能搞定的事情非要搞這麼複雜。但這份“偏執”卻是我們對技術最初的追求,生活不止眼前的苟且,還有詩和遠方。


關於作者:專注後端的中介軟體開發,公眾號"捉蟲大師"作者,關注我,給你最純粹的技術乾貨

image

相關文章