當前位置:
首頁 > 最新 > 逍遙-《Go實現的高性能http緩存伺服器Jaguar》

逍遙-《Go實現的高性能http緩存伺服器Jaguar》

逍遙 / 美麗聯合集團技術專家

開發維護過 Winzip 等大型軟體。2014年加入美麗聯合集團,從無到有構建基礎平台商品體系。2015年開始在蘑菇街落地基於 ATS 的靜態化方案。2017年開始和小夥伴用 go 語言實現 ATS 的替代方案 Jaguar,目前已在集團內完成對 ATS的替換。熱愛 Golang 和 Python ,目前正專註於 Jaguar 優化和流式任務平台 Hulk 的開發工作。

前言

我是來自美麗聯合集團電商基礎的逍遙,2014年開始承擔優化詳情頁鏈路的工作。2015年開始落地基於 ATS 的頁面靜態化方案,到2017年,詳情頁、店鋪頁、會場頁等展示性系統鏈路都已經被此套方案覆蓋。然而,在使用的過程中我們也發現了很多 ATS 的不便之處,於是從去年開始,我們成立了一個虛擬團隊,用 golang 重寫實現了一個 http 緩存伺服器 Jaguar,今天的演講就來向大家介紹一下這個伺服器。

今天的分享分為四個模塊,第一是介紹一下業界的緩存方案,第二是 Jaguar 相關介紹,第三是分享一些實現過程中性能優化的經驗,第四是介紹下具體的業務使用場景。

緩存方案

問題描述

首先,我相信在座的各位所在的公司應該都用過 http 緩存伺服器,在整個用戶訪問鏈路中很多節點都有緩存,客戶端會有客戶端緩存、服務端會有集中式緩存類似 redis,也會有本機的緩存庫例如 guava。我今天要講的 http 緩存伺服器則是相對比較獨立的一個緩存節點,雖然主要實現的一些優化效果跟其它緩存類似,但是它的優勢在於不僅可以降低整個訪問鏈路的 RT,更會對它的下游伺服器起到一定的保護作用,也會節省一些帶寬。

業界方案

在業界,http緩存伺服器有一些前輩產品,有些公司內部也會自己實現 http 緩存伺服器。我們在2015年落地詳情頁靜態化方案的時候,主要參考了三個業界比較著名的 http 緩存伺服器:VARNISH CACHE,SQUID 和 apache traffic server,但由於varnish 和 squid 對動靜態數據合併支持比較弱,且當時ATS在業界有不少成功的業界場景並且性能卓越,所以最後選擇了 ATS 來實現。而 NGIMX+LUA的方式由於可能會需要用lua實現一些比較複雜的功能以及性能上不及ATS,最後沒有被採用。

ATS存在的問題

一、ATS 本身部署起來比較麻煩,與各個公司自己的運維體系集成不太方便,我們之前在應用過程中曾把它和nginx搭配起來用,結果就導致日常PE維護起來比較費勁。

二、我們會存在一些需要全量失效的場景,比如大促的時候,可能過了零點之後有些數據需要失效,但是 ATS的緩存類似一個大的hashmap, 根據某個pattern來失效需要掃全表,這樣的話效率非常低,例如我們站內有幾千萬個K,失效功能約等於不可用。

三、功能擴展、定製化方面存在一定的制約,ATS支持用c寫plugin和用lua寫擴展腳本,前者實現起來難度比較大,後者提供的介面不是很全面的。

四、ATS本身實現起來比較複雜,不方便debug等定位問題,有很多代碼邏輯藏得比較深,導致排查問題很困難。

Jaguar介紹

Jaguar的優勢

一、我們在業務層面支持更多數據合併的標準,比如以詳情頁為例,有很多數據是跟當前登陸的用戶有關,比如有沒有喜歡/收藏某個商品,這是動態的數據,而有些數據是不經常變動的,比如商品標題等,這些是靜態的數據。前端拿到的數據,應該保證每個數據都是正確實時的。我們為了支持動靜態數據合併提供了很多數據合併標準的支持。

二、Jaguar提供了更靈活的 lua 擴展功能, 支持更多的擴展。

三、metrics 等數據方便上報,我們開放了很多介面,可以直接嵌入進去。

四、在一些特點的業務場景下,Jaguar的性能好於 ATS。

五、ATS 如果要升級協議不太方便,Jaguar 比較方便網路升級協議, 目前支持h1/h2等。

系統架構

接下來說一下 Jaguar 的架構。上層有做請求轉發的一個Router,Jaguar 主體有一個 pipeline,由cachekey(緩存key生成)、static(緩存)、amerger(動靜態合併)等valve

串聯,完成正常處理流程。最下游會有一個 adapter,會有兩個作用,一個是會跟底層網路框架連在一起,還有一個是方便調試。

執行流程

這是整個請求進來的流程,對請求處理的大流程處理和底層網路一塊出於實現成本考慮我們借鑒了業界著名的caddy框架,之後的開源版本中我們會將這塊自己實現掉。而caddy本身所有一個大的 pipeline,我們會在其中插入一個jaguar自己的pipeline,來做緩存相關的操作。

部署架構

這是我們用 Jaguar之前基於ATS的架構,看起來比較複雜的圓圈裡是 ATS+Nginx的架構,需要部署相關的東西,比較複雜。

現在替換成 Jaguar 之後鏈路比較清晰,服務發現框架SLB替代了內網DNS,緩存未命中情況下回源也是通過 SLB,然後Amerger+ATS+Nginx三個部件的進程間通信變成了Jaguar內部的函數調用。

性能比較

這是ATS和Jaguar性能和功能的比較。首先100%場景下 ATS的確會比 Jaguar 出色一點,因為它是由C++實現的,相比golang的gc在內存等處理上會更好一些,但是在80%命中率的esi場景下,我們的性能是 ATS 的1.5 倍,在80%命中率json場景下我們的性能是 ATS 的2倍。而網路協議升級,jesi標準支持,緩存全量失效等功能支持也是Jaguar優於ATS的點。ESI merge、JSON merge和JESI merge大家可以理解成為這是一個動靜態數據合併的標準。

業務功能

Jaguar 業務功能有:

一、esi/jesi/json 多種數據合併規範。

二、Lua 擴展可定製化。

三、多域名多 pattern支持,配置按 nginx 配置風格。

四、預留服務發現&metric上報介面,方便擴展。

在esi 合併標準下,當頁面裡面出現多個分散動態數據場景下,就會發多個請求,jesi 會把多個請求合併,然後在後端進行重組。

下面說明下lua擴展是如何插入到處理鏈路中去的,在請求進來的時候,會通過 remap節點,可以通過lua擴展去修改請求的header和cookie等數據,在cachekey生成之後,可以通過lua擴展修改緩存key,在獲取到緩存之後和回寫response等節點也可以通過lua擴展修改上下文數據。

為什麼要用 golang 來實現?

一、之後可能會調整站內網路協議棧,比如升級到h2,QUIC等,golang寫相關代碼比c和java來得方便

二、站內網關選擇了caddy做二次開發,插件化的架構更加合理,而且本身實現了h2,https 相關的功能

三、簡單,語法上簡單直白,沒有 java 廢話多,也沒有 C++的過分複雜的指針操作

四、性能卓越,用nodejs, java,lua, golang實現的json merge核心代碼golang 性能更突出

五、test, profile等工具簡單夠用,部署非常簡單就一個可執行文件

六、golang程序多核系統下有一定優化

性能優化

Json Merge 優化

接下來說一下之前一直提到的動靜態數據合併。這是一個 Esi Merge場景,頁面靜態數據中有一個esi標籤,當請求進來時,會從緩存中要取這部分數據,Esi 引擎會解析esi標籤中帶有的動態數據回源的url,然後到後端取到對應的數據,這個數據取到之後會進行簡單的純文本替換,最後出來這樣一個數據返回給前端。這個標準在早期html頁面用起來還是比較方便的,頁面比較簡單。但在當今很多SPA等複雜頁面場景出現的情況下,就顯得比較死板,性能上也不甚理想。

所以我們現在提出 Json Merge 標準,因為現在很多h5或者客戶端頁面都是通過後端的json數據來進行頁面渲染的。如果json格式的動靜態數據合併由前端和客戶端同學來做,會涉及到不同語言實現的細節上有差異,而且這個邏輯交給前端同學做邏輯上也不太合適,對他們來說這個步驟應該是透明的。

這邊對演算法做一下描述:

一、有靜態數據StaticData,動態數據DynData兩塊數據,都為string類型。

二、反序列化StaticData和DynData。

三、廣度優先遍歷StaticData節點,獲取某個節點對應的DynData中的數據,如果存在並且為array或者基礎數據類型(int, string...)則新值覆蓋。

四、對於節點為Object的對象採用遞歸的方式重複3步驟。

五、序列化merge後的數據結構為字元串返回。

最開始用golang的一些json庫寫了演算法實現,壓測下來性能都不太理想。跑profile發現主要的損耗在反射這一塊。

然後我們就開始轉變問題的解決思路,其實我們並不需要把它當做一個普通的 Json 問題來處理,這其實就是一個有限狀態機處理的問題,可以轉化為字元串處理的問題,這樣演算法複雜度頁降低到了O(n)。經過試驗之後,我們發現每次合併處理的時間會降到原來的四分之一。吞吐量都上去了。

簡單的總結一下這塊的優化經驗:

一、 基本去除了反射,在解析dynData時還是用了gjson來讀,因為需要把它轉化為一個類似hashmap的東西,但其中也幾乎沒有反射代碼。

二、 盡量減少value的展開,比如某個value預判下來是object的情況,如果dynData是中該位置的value為空,則可以直接寫入buf,反之如果dynData中的key在staticData中不存在,則可直接將dynData中的value寫入buf。

三、 字元串盡量用slice的方式提取,可以在遍歷的時候記錄下索引,因為用buf的WriteByte有很大的性能開銷。

四、用bufio將bytes.Buffer包一下,後者的Write方法中都會有一個試圖增長buf的函數,開銷比較大,而bufio包裝過後,最終flush一把性能提升比較明顯。

Lua優化

Lua 優化是在我們開發過程中第二個比較重要的優化點,在請求處理的各個節點Jaguar都會預埋一些Hook點,lua腳本會在hook點執行一些操作,但第一個版本上去之後發現性能差不多下降一半。後來發現,GC在某個節點會變得特別頻繁,最終導致到某個點之後系統會崩掉。經過分析之後,找到主要原因是因為最開始每次請求進來會構造一個LState對象,那個對象是比較大的一個對象,但是它較長時間都不會被回收掉,慢慢就會導致 GC頻繁。最後我們想到了一個辦法,利用池化技術,在整個程序啟動的時候會構建一個LState的Pool,每次請求進來我們會從這裡面去取,用完之後歸還給pool。採用這種方式後,性能上面的影響就比較少了。

應用場景

詳情頁

接下來說一下我們的使用場景。這是一個很典型的蘑菇街詳情頁,頁面中會包含一些動態的數據,比如像 SKU 數據,商品的庫存、商品的喜歡狀態,這些數據要實時的。而另一些比如標題和圖文詳情描述這些數據變化是不太頻繁的,我們會定義為靜態數據。這個場景就會用到json動靜態數據合併。

這是整個詳情頁從用戶訪問的鏈路圖,我們曾經嘗試過一個二級 CDN 的方案,比如在華南、華北區域部署一些 CDN,來加速用戶的訪問,但因為最終機房還是在江浙滬這一帶,就會導致動態數據回源成為一個瓶頸點,導致二級 CDN 效果並不好,所以我們現在暫時把二級 CDN 去掉了。但是站內像大促會場場景,我們還是在用二級 CDN 方案。還有,緩存會對應一個失效邏輯,現在失效模式有兩種,一是主動發purge請求到jaguar上,也可以配一個緩存失效時間。我們有一個失效中心,它會監聽一些關鍵事件的消息,比如商品表、店鋪表的變更的binlog消息,會觸發靜態化伺服器某些緩存失效(精確的url or pattern)。

這是一份典型的詳情頁的Jaguar緩存配置,我們可以配置緩存的時間,提取哪些參數作為緩存key的一部分等。

魔方系統

第二個要介紹的場景是和魔方系統的配合,大多數電商網站在每次大促或者日常活動期間都會有一些活動頁。我們有一個魔方系統,可以用它來把頁面和數據綁定起來,渲染完之後會生成一個活動頁面。這個生成的頁面會最終push到jaguar上緩存起來,這個場景就不存在動態數據了。

魔方系統用到了lua擴展的功能,主要要實現的功能是根據請求的User-Agent對客戶端瀏覽器進行歸類,以便返回不通的頁面數據。因為我們的瀏覽器很多,有些瀏覽器其實是android或ios中的容器等,需要進行歸類。

手頭事

一、針對小公司並沒有特別好的前後端分離的場景,所以之後我們會支持簡單的模板,和前端模板引擎相結合,把簡單的頁面渲染,渲染完之後直接放到jaguar上去。

二、進行 H2 協議和QUIC協議的內測。

三、是集中式緩存(eg. redis)等對接。

四、開源版本會有兩個,一個是作為caddy 插件,另一個是一個all-in-one 版本。

五、控制台相關功能優化。

這是我們站內在做的接入層相關的規劃,會和waf等插件做一些結合,再做接入層相關的一些改造。

今年規劃

這是今年的一些規劃,從去年年底開始,我們預計今年3月份主體代碼完成,7月份站內替換完成,9月份會出一個社區版本。

Q&A

提問:我之前做移動開發,有一個問題是想問關於動態數據和靜態數據,為什麼會有這個需求?你說的靜態數據是文件形式的,還是說這個數據是在兩個不同的庫裡面?

逍遙:在規模比較小的情況下可以完全不區分,也可以完全不接入,請求直接打到後端,每次去調內部的緩存或者直接db去取數據,這是在請求量比較小的時候。但是在請求量比較大的時候,尤其是有些熱點數據,如果每次都要穿透到後端伺服器,會導致後端伺服器或DB等資源的浪費,http緩存伺服器完全可以把一些數據直接緩存起來直接返回。

提問:靜態數據存在哪裡

逍遙:目前默認的數據內存中的,我們根據boltdb這個lib做了一些改造,增加了一些容量控制和lru等演算法的實現。

主持人:你現在在做的這個東西,ATS 裡面都是支持的?

逍遙:有相當一部分功能ATS是不支持的,比如json merge, jesi等,ATS更像是一個通用的框架,業務上的定製會少很多。

主持人:另外一個問題,你說你們現在這個也支持lualua是通過什麼方式調用的?

逍遙:其實是用了golua這個庫,但是我們現在對這個庫進行了改造,因為原生來講這個庫的性能並不理想。

主持人:因為我了解下來的golua都是基於cgo的模式去調用,但我特別討厭cgo

逍遙:對,所以如果是一些通用的業務場景,我們會把它沉澱在核心代碼裡面用golang來實現,但是我們也在考慮用golang原生去寫lua的解釋器,來提升性能。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Go中國 的精彩文章:

TAG:Go中國 |