前言 在 2019年第五屆 Gopher China 大會上,小米科技基礎(chǔ)服務(wù)高級研發(fā)工程師徐成選做了題為《用 Go 構(gòu)建高性能數(shù)據(jù)庫中間件》的技術(shù)演講,詳細(xì)介紹了小米開源的數(shù)據(jù)庫中間件 Gaea 的整體架構(gòu)、內(nèi)部模塊和一些具體實踐。以下為演講實錄。 No.0 自我介紹 大家下午好,很榮幸能有這個機(jī)會跟大家分享一個用 Go 開發(fā)的數(shù)據(jù)庫中間件項目。在之前先做一下自我介紹,我是2015 年年初開始用 Go,最早是用 Python 和 C 比較多,接觸 Go 后就立刻喜歡上了這門語言。后來一直用 Go 做了一些項目,包括微服務(wù)、數(shù)據(jù)庫和緩存中間件等,還有一些偏業(yè)務(wù)的基礎(chǔ)服務(wù),比如說庫存中間層、ID生成器之類的。 No.1 Go in XiaoMi 首先簡單介紹下 Go 在小米的一個使用情況,雖然部門分散,缺乏具體的數(shù)字,但是可以看到覆蓋面還是非常廣的。 小米 2014 年引入 Go,最早解決商城日志收集問題。后面感覺表現(xiàn)挺不錯,開始大力推廣,像現(xiàn)在商城、云平臺、金融、IoT 都是用 Go 都是比較多的。用 Go 做的項目有中間件、微服務(wù)(比如說我們商城有微服務(wù)框架是 koala,這個2016年投產(chǎn)了,目前有幾百個微服務(wù)),還有更多的業(yè)務(wù)系統(tǒng)包括訂單、活動、運營、API 接入層等都是用 Go 開發(fā)的。 中間件很多,NewSQL 發(fā)展迅速,為什么還需要 Gaea 這樣一款中間件?個人感覺,作為一個單機(jī)時代的解決方案,DB proxy 還是有一定的存在價值,再就是也跟我們內(nèi)部一個現(xiàn)狀有關(guān)系。 第一,我們內(nèi)部其實是在用 MyCAT,但是了解很少,使用過程中出問題得不到及時解決,比如說連接過多、連接超時、load 過高和內(nèi)存溢出等。 第二,這個配置比較麻煩,我們維護(hù)了 MyCAT 的一個內(nèi)部版本,配置有一些寫在表里,還有一些寫在其他地方,這個很痛苦,易出錯、難管理。 第三,還有一些 haproxy、kingshard 等充當(dāng)了代理角色,不夠統(tǒng)一。 基于以上三個原因,并且我們還要做 DB 服務(wù)化,所以誕生了 Gaea 這一整套系統(tǒng)。 1. Gaea 特性 Gaea 現(xiàn)在已經(jīng)上線,已經(jīng)有的特性也跟大家簡單介紹一下。首先分布發(fā)表,兼容了 MyCAT、kingshard 兩種方案。Prepared statements支持分庫分表。再就是讀寫分離,多個從實例負(fù)載均衡。還有多租戶,針對某一個業(yè)務(wù)系統(tǒng)特別重要可以單獨部署機(jī)群,整個運維比較靈活。再就是像 SQL 追蹤,慢 SQL 指紋,使用方可以通過 Gaea 平臺查找自己的 SQL 信息。還有配置熱加載、連接池。我們使用 TiDB 的 Parser 作為 SQL 解析器這也是目前 Go 中最完備、優(yōu)秀的 SQL 解析器。 2. Gaea 架構(gòu) 這里是Gaea一個大致架構(gòu)情況。分四個模塊: 第一是應(yīng)用層,各種MySQL Client。 第二是代理層,主要是在線服務(wù)處理。 第三是存儲層,這一層主要是部署、啟停、還有插件系統(tǒng),DBA的很多插件通過agent來執(zhí)行。 側(cè)邊是管理模塊,這個地方多出一個中控,這個中控是Web和Proxy之間進(jìn)行交互的管控層。我們還打通了微服務(wù)koala和gaea,應(yīng)用方可以通過koala直接連接gaea proxy,無需lvs進(jìn)行轉(zhuǎn)發(fā)。代理層是我本次分享的一個重點,上面是會話管理,下面是連接池,中間是解析、計算、路由、聚合等。整個架構(gòu)大概如下: 這個項目去年11月份上線,兩套機(jī)群,16個業(yè)務(wù),有兩個是分庫分表業(yè)務(wù)。收益上: 綜合性能比之前提升了25%,其實好多場景還要高一些。并發(fā)優(yōu)秀,之前被連接打爆的情況沒有再次發(fā)生。平臺化方案,方便DBA分配、配置業(yè)務(wù)租戶。最后打點數(shù)據(jù)統(tǒng)一匯總到Prometheus,方便在線監(jiān)控。以上是Gaea目前線上的的情況。 3. Why Go? 為什么選擇Go? 第一,并發(fā)友好。one connection per goroutine,區(qū)別多線程像java、C 還要自己處理callback。 第二,開發(fā)效率非常高。 第三,工具比較豐富,Go內(nèi)建工具以及第三方工具。 最后是 Go 目前在db這塊的優(yōu)秀項目非常多,團(tuán)隊本身經(jīng)驗也非常豐富。 No.2 實現(xiàn)Gaea過程中有關(guān)的幾個技術(shù)點 1. 配置熱加載 第一件事情就是動靜分離,把動態(tài)配置和靜態(tài)配置區(qū)別對待。靜態(tài)配置比如端口、etcd的配置信息、Log位置信息等。動態(tài)配置是我們關(guān)注的重點,比如說各個分庫分表的規(guī)則,后端實例信息、讀寫分離開關(guān)等。 方案一,這也是我們在微服務(wù)框架里的使用方式。首先構(gòu)建一個atomic.value,配置加載和構(gòu)建可以根據(jù)框架相關(guān)的邏輯提前準(zhǔn)備好,最后做一個Store,存到配置面,這是reload階段。這個配置怎么使用呢?通過atomic.load獲取后進(jìn)行斷言,也就是自定義的config,這種方式也可以根據(jù)需要包裝成多實例的兩階段提交方案。 方案二,是Gaea應(yīng)用到的,滾動數(shù)組方案。首先定義切換標(biāo)識和兩份數(shù)據(jù);然后,prepare階段通過深拷貝當(dāng)前配置并將要變更的配置準(zhǔn)備好;最后,在commit階段,把上一個準(zhǔn)備好的一份配置切換過來,通過cow實現(xiàn)無鎖配置熱加載。 2. 資源抽象 大家可以看到,每個namespace的配置有幾十項,幾十項的配置都需要有對應(yīng)的管理接口,很難去編排接口順序,具體接口開發(fā)上也很復(fù)雜、繁瑣、易出錯。我們的方案是把資源直接抽象為新建和關(guān)閉,對于關(guān)閉,可以做到立刻關(guān)閉和延遲關(guān)閉。而延遲關(guān)閉就是配置熱加載階段需要用到,目的是讓既有消息得到安全的處理。這種方案其實是比較省接口,你也不需要在某一個地方考慮接口的順序之類,簡單可依賴。 3. 連接池設(shè)計 接下來是跟大家分享連接池的設(shè)計實現(xiàn)。我理想中的連接池是這樣。第一能夠在一定范圍內(nèi)做到自動調(diào)整容量;第二獲取的連接是活的;第三具備一定的超時獲取機(jī)制,連接池連接不夠用,不能一直等著,這樣會把代理層打垮,需要fail fast的機(jī)制。 自動調(diào)整,自動調(diào)整包含兩個方向,增加和減少。這里的具體實現(xiàn)是基于vitess的resource庫封裝的連接池。當(dāng)前連接池可以設(shè)置的最大值為MAX,初始連接wrapper為cap。新增連接通過獲取時延遲新建連接對象,關(guān)閉是通過一個定時器,定時去根據(jù)連接上的時間戳判斷當(dāng)前連接空閑時間,超過一定的時間就進(jìn)行回收,理論上連接數(shù)可以回收到0。 ?;?,大家最常見就是Ping方案,針對多租戶下每一個分片的每一個主從實例都需要進(jìn)行ping,資源消耗可能會非??捎^,所以我們采用了另一種方案,簡述為獲取連接-失敗-新建連接-復(fù)用連接對象的策略。區(qū)別于普通連接池獲取連接池失敗后、報錯、然后重新嘗試獲取、并通過旁路ping剔除的方案。我們的方案的好處就是,你不需要太多的ping,你的連接對象永遠(yuǎn)有效,同時你很容易驗證在什么位置進(jìn)行連接創(chuàng)建、回收等操作。這個方案的缺點是: 在網(wǎng)絡(luò)環(huán)境比較差或者是你的MySQL wait timeout比較多會增加重試次數(shù),所以建議idleTimeout小于MySQL wait_timeout值。 超時獲取,通過Context實現(xiàn)。我大致總結(jié)了一個Context的使用范式。在最底層的goroutine一定有一個單獨發(fā)送請求、接受應(yīng)答并通過Select去判斷resp、Context的狀態(tài)。 Context本質(zhì)是什么?Context是實現(xiàn)Context接口的非導(dǎo)出struct。每一個Context會存儲Parent Context或者Chidren的Context。對于取消的情況也有兩種方式,一種通過after function或者通過單獨的Goroutine執(zhí)行。最后是value context只有Parent Context,所以自頂向下傳值。 會話管理。一般是用SetReadDeadine、SetWriteDeadling兩個函數(shù)。但是存在一定的問題,一是分散設(shè)置,二是每一次讀寫都需要設(shè)置是一種高頻操作,會帶來一定的性能問題,所以考慮優(yōu)化。 方案一,在某一個應(yīng)用里,設(shè)置一個SetReadDeadine、SetWriteDeadling做定時設(shè)置,不是每次請求都設(shè)置,可能每五分鐘設(shè)置一次,需要定義一些標(biāo)志位,標(biāo)志位來的時候設(shè)置一次。這個可以緩解性能問題,但也有一些缺點,一是可能有誤差,原來狀態(tài)立刻可以知道,現(xiàn)在需要等幾分鐘,取決于設(shè)置,當(dāng)然還是有分散管理的問題。 方案二,Gaea使用第二種方案,就是時間片輪轉(zhuǎn)的方式。當(dāng)我建立一個連接的時候,首先會把會話加入時間輪,給定回調(diào)函數(shù),到期執(zhí)行關(guān)閉操作。當(dāng)客戶端主動做Close操作,可以把對應(yīng)的對象移除。這種管理方案非常清晰,也是一種集中式管理方式,同時 CPU 消耗非常低。 4. 內(nèi)存踩坑優(yōu)化 分享一個我們之前線上遇到的一個問題?,F(xiàn)在go gc已經(jīng)非常強(qiáng)大,基本不會遇到什么問題。但是我們遇到一個內(nèi)存一直增長的情況,這個讓人很發(fā)慌,你會懷疑是不是程序有泄露,我們啟動是30兆,跑著一段時間一直是20GB,并且一直會維持在這個內(nèi)存,我們考慮這是什么問題導(dǎo)致的呢? 我們在梳理我們的請求、處理、應(yīng)答的過程中發(fā)現(xiàn)有很多的append操作。 我們的優(yōu)化方案是將之前的append操作,包括數(shù)據(jù)包協(xié)議頭、返回字段、字段內(nèi)容等都改為Write操作。 因為上述操作采用了池化技術(shù),并且使用的位置比較多,帶來的一個問題就是你需要控制對象生命周期的位置比較多,很容易出現(xiàn)對象未回收到池子內(nèi)的bug,所以我們將整理流程做了一個改造,將資源回收進(jìn)行集中,同時,對于能在一個函數(shù)完成申請、回收的情況,我們盡量在一個函數(shù)內(nèi)完成。 當(dāng)然在這個過程中我們也修復(fù)掉了幾個可能有內(nèi)存泄露的點,整體改造之后,效果還是比較明顯的,內(nèi)存基本穩(wěn)定的3GB,未再出現(xiàn)竄高到20GB的情況。 No.4 Impressive runtime go runtime非常方便,我們的使用方式如下: 第一,將pprof包裹到admin http服務(wù)內(nèi),并增加鑒權(quán)機(jī)制,然后中間加一層shellproxy,它可以調(diào)用服務(wù)里面的一些接口或者指令,把相應(yīng)數(shù)據(jù)拿到傳給一個WEB; 第二,把一些數(shù)據(jù)通過打點統(tǒng)計到prometheus,然后通過grafana進(jìn)行展示; 第三,通過go-torch進(jìn)行火焰圖繪制。 尤其第一種方案,給我們在線排查問題帶來了非常大的助力,比如進(jìn)行全鏈路壓測或者線上有問題時,可以立馬做一次pprof,保留現(xiàn)場,觀察現(xiàn)場。 No.5 Go toolchains 這里其實是一個流水的頁面,介紹一下我們用到了哪一些 Go 工具。版本控制目前用 glide,感覺非常不錯。另外 gofmt、golint 還有 goimports 等,我們把一些工具集成到 gitlab ci 和 git hooks 對于提高代碼和工程質(zhì)量都非常有幫助。 No.5 Tests 對于一個基礎(chǔ)服務(wù)項目,監(jiān)控先行、注重單測。我們通過不斷拆分有狀態(tài)和無狀態(tài)模塊,在不影響系統(tǒng)模塊劃分情況下,努力提升單測覆蓋面。我們也通過 gitlab ci 進(jìn)行單測覆蓋統(tǒng)計,并針對每一次 commit,都會進(jìn)行一次 unit tests 驗證,保證功能符合預(yù)期。 基礎(chǔ)服務(wù)很少有 QA 進(jìn)行直接的跟蹤測試,所以我們通過 docker,構(gòu)建了一個簡版的集成測試套件,其中包含了 MySQL 官方測試語句、分庫分表 python 腳本等。每當(dāng)發(fā)一個新的版本的時候,我們就通過這個工程進(jìn)行集成測試,投入產(chǎn)出比非常理想。 Q&A 提問:我想請問一下 3GB 的內(nèi)存大概什么情況下產(chǎn)生的?這個量級是不是有優(yōu)化的空間? 徐成選:3GB 是目前線上跑到的一個數(shù)字,并沒有其他理論計算。優(yōu)化空間還是有的,比如減少中間 ResultSet 結(jié)果的構(gòu)建,做到 session、backend 復(fù)用統(tǒng)一 ResultSet,再就是根據(jù)是否需要做聚合,減少一部分冗余數(shù)據(jù)的存儲等。 提問:具體的情況是在多少個連接情況下,mysql qps是多少? 徐成選:qps幾千,連接幾百個左右。因為現(xiàn)在接入的量確實不多。但是這個隨著量的增長,這塊內(nèi)存不會增長。這里我們一是通過優(yōu)化協(xié)議層減少一部分內(nèi)存,二是修復(fù)掉了幾個可疑的泄漏點,在上述ppt中也有講到。 重磅活動預(yù)告 Gopher Meetup 廣州站即將開啟。來自小鵬汽車、騰訊、早安科技、PingCAP的大咖講師帶來 Go 開發(fā)領(lǐng)域的一線實踐經(jīng)驗分享,盡在10月26日,小鵬汽車總部銷售展廳! |
|