作者: 粟含
全量SQL(所有訪問數據庫的SQL)可以有效地幫助安全進行數據庫審計,幫助業務快速排查性能問題。一般可通過開啟genlog日志或者啟動MySQL審計插件方式來進行獲取,而美團選用了一種非侵入式的旁路抓包方案,使用Go語言實現。無論采用哪種方案,都需要重點關注它對數據庫的性能損耗。
本文介紹了美團基礎研發平臺抓包方案在數據庫審計實踐中遇到的性能問題以及優化實踐,希望能對大家有所幫助或啟發。
1 背景
數據庫安全一直是美團信息安全團隊和數據庫團隊非常注重的領域,但由于歷史原因,對數據庫的訪問只具備采樣審計能力,導致對于一些攻擊事件無法快速地發現、定損和優化。安全團隊根據歷史經驗,發現攻擊訪問數據庫基本上都存在著某些特征,經常會使用一些特定SQL,我們希望通過對MySQL訪問流量進行全量分析,識別出慣用SQL,在數據庫安全性上做到有的放矢。
2 現狀及挑戰
下圖是采樣MySQL審計系統的架構圖,數據采集端基于pcap抓包方式實現,數據處理端選用美團大數據中心的日志接入方案。所有MySQL實例都部署了用于采集MySQL相關數據的rds-agent、日志收集的log-agent。rds-agent抓取到MySQL訪問數據,通過log-agent上報到日志接收端,為了減少延時,上報端與接收端間做了同機房調度優化。日志接收端把數據寫入到約定的Kafka中,安全團隊通過Storm實時消費Kafka分析出攻擊事件,并定期拉數據持久化到Hive中。
我們發現,通常被攻擊的都是一些核心MySQL集群。經統計發現,這些集群單機最大QPS的9995線約5萬次左右。rds-agent作為MySQL機器上的一個寄生進程,為了宿主穩定性,資源控制也極為重要。為了評估rds-agent在高QPS下的表現,我們用Sysbench對MySQL進行壓測,觀察在不同QPS下rds-agent抓取的數據丟失率和CPU消耗情況,從下面的壓測數據來看結果比較糟糕:
如何在高QPS下保證較低的丟失率與CPU消耗?已經成為當前系統的一個亟待解決的難題與挑戰。
3 分析及優化
下面主要介紹圍繞丟失率與CPU消耗這一問題,我們對數據采集端在流程、調度、垃圾回收和協議方面做的分析與改進。
3.1 數據采集端介紹
首先,簡要介紹一下數據采集端rds-agent,它是一個MySQL實例上的進程,采用Go語言編寫,基于開源的MysqlProbe的Agent改造。通過監聽網卡上MySQL端口的流量,分析出客戶端的訪問時間、來源IP、用戶名、SQL、目標數據庫和目標IP等審計信息。下面是其架構圖,主要分為5大功能模塊:
1. probe
probe意為探針,采用了gopacket作為抓包方案,它是谷歌開源的一個Go抓包庫,封裝了pcap。probe把抓取到原始的數據鏈路層幀封裝成TCP層的數據包。通過變種的Fowler-Noll-Vo算法哈希源和目的IP port字段,快速實現把數據庫連接打散到不同的worker中,該算法保證了同一連接的來包與回包的哈希值一樣。
2. watcher
登錄用戶名對于審計來說極其重要,客戶端往往都是通過長連接訪問MySQL,而登錄信息僅出現在MySQL通信協議的認證握手階段,僅通過抓包容易錯過。
watcher通過定時執行show processlist獲取當前數據庫的所有連接數據,通過對比Host字段與當前包的客戶端ip port,補償錯過的用戶名信息。
3. worker
不同的worker負責管理不同數據庫連接的生命周期,一個worker管理多個連接。通過定期比對worker的當前連接列表與watcher中的連接列表,及時發現過期的連接,關閉并釋放相關資源,防止內存泄漏。
4. connStream
整個數據采集端的核心邏輯,負責根據MySQL協議解析TCP數據包并識別出特定SQL,一個連接對應一個connStream Goroutine。因為SQL中可能包含敏感數據,connStream還負責對SQL進行脫敏,具體的特定SQL識別策略,由于安全方面原因,這里不再進行展開。
5. sender
負責數據上報邏輯,通過thrift協議將connStream解析出的審計數據上報給log-agent。
3.2 基礎性能測試
抓包庫gopacket的性能直接決定了系統性能上限,為了探究問題是否出在gopacket上,我們編寫了簡易的tcp-client和tcp-server,單獨對gopacket在數據流向圖中涉及到的前三個步驟(如下圖所示)進行了性能測試,從下面的測試結果數據上看,性能瓶頸點不在gopacket。
3.3 CPU畫像分析
丟失率與CPU消耗二者密不可分,為了探究如此高CPU消耗的原因,我們用Go自帶的pprof工具對進程的CPU消耗進行了畫像分析,從下面火焰圖的調用函數可以歸納出幾個大頭:SQL脫敏、解包、GC和Goroutine調度。下面主要介紹一下圍繞它們做的優化工作。
3.4 脫敏分析及改進
因為SQL中可能包含敏感信息,出于安全考慮,rds-agent會對每一條SQL進行脫敏處理。
脫敏操作使用了pingcap的SQL解析器對SQL進行模板化:即把SQL中的值全部替換成“?”來達到目的,該操作需要解析出SQL的抽象語法樹,代價較高。當前只有采樣和抓取特定SQL的需求,沒有必要在解析階段對每條SQL進行脫敏。這里在流程上進行了優化,把脫敏下沉到上報模塊,只對最終發送出去的樣本脫敏。
?
這個優化取得的效果如下:
3.5 調度分析及改進
從下面的數據流向圖可以看出整個鏈路比較長,容易出現性能瓶頸點。同時存在眾多高頻運行的Goroutine(紅色部分),由于數量多,Go需要經常在這些Goroutine間進行調度切換,切換對于我們這種CPU密集型的程序來說無疑是一種負擔。
針對該問題,我們做了如下優化:
- 縮短鏈路:分流、worker、解析SQL等模塊合并成一個Goroutine解析器。
- 降低切換頻率:解析器每5ms從網絡協議包的隊列中取一次,相當于手動觸發切換。(5ms也是一個多次測試后的折中數據,太小會消耗更多的CPU,太大會引起數據丟失)
這個優化取得的效果如下:
3.6 垃圾回收壓力分析及改進
下圖為rds-agent抓包30秒,已分配指針對象的火焰圖。可以看出已經分配了4千多萬個對象,GC壓力可想而知。關于GC,我們了解到如下兩種優化方案:
- 池化:Go的標準庫中提供了一個sync.Pool對象池,可通過復用對象來減少對象分配,從而降低GC壓力。
- 手動管理內存:通過系統調用mmap直接向OS申請內存,繞過GC,實現內存的手動管理。
但是,方案2容易出現內存泄漏。從穩定性的角度考慮,我們最終選擇了方案1來管理高頻調用函數里創建的指針對象,這個優化取得的效果如下:
3.7 解包分析及改進
MySQL是基于TCP協議之上的,在功能調試過程中,我們發現了很多空包。從下面的MySQL客戶端-服務端數據的交互圖可以看出:當客戶端發送一條SQL命令,服務端響應結果,由于TCP的消息確認機制,客戶端會發送一個空的ack包來確認消息,而且空包在整個流程中的比例較大,它們會穿透到解析環節,在高QPS下對于Goroutine調度和GC來說無疑是一個負擔。
下圖是MySQL數據包的唯一格式,通過分析,我們觀察到以下特點:
- 一個完整的MySQL數據包長度>=4Byte
- 客戶端新發送命令的sequence id都是為0或者1
而pcap支持設置過濾規則,讓我們可以在內核層將空包排除掉,下面是上述特點對應的兩條過濾規則:
特點1:ip[2:2] - ((ip[0] & 0x0f) << 2) - ((tcp[12:1] & 0xf0) >> 2) >= 4
特點2: (dst host {localIP} and dst port 3306 and (tcp[(((tcp[12:1] & 0xf0) >> 2) + 3)] <= 0x01))
這個優化取得的效果如下:
基于上述經驗,我們對數據采集端進行功能代碼重構,同時還進行一些其它優化。
4 最終成果
下面是優化前后的數據對比,丟失率從最高60%下降到了0%, CPU消耗從最高占用6個核下降到了1個核。
為了探究抓包功能對MySQL性能損耗,我們用Sysbench做了一個性能對比測試。從下面的結果數據可以看出功能對MySQL的TPS、QPS和響應時間99線指標最高大約有6%的損耗。
5 未來規劃
雖然我們對抓包方案進行了各種優化,但對于一些延遲敏感的業務來說性能損耗還是偏大,而且該方案對一些特殊場景支持較差:如TCP協議層發生丟包、重傳、亂序時,MySQL協議層使用壓縮、傳輸大SQL時。而業界普遍采用了直接改造MySQL內核的方式來輸出全量SQL,同時也支持輸出更多的指標數據。
目前,數據庫內核團隊也完成了該方案開發,正在線上灰度替換抓包方案中。另外,對于線上全量SQL端到端丟失率指標的缺失,我們也將陸續進行補齊。
本文作者
粟含,來自于美團基礎研發平臺/基礎技術部/數據庫技術中心。