先說標(biāo)題中的億級網(wǎng)關(guān),我的角度時用戶量是億級,訪問量按小時算是時百億級別以上,主要是api網(wǎng)關(guān),也做了老程序php反向代理,區(qū)別點(diǎn)為是否存在頁面,是否需要通配。
我們定位為:高性能,高可用,可擴(kuò)展,可管理,可治理,安全的網(wǎng)關(guān)產(chǎn)品,作為唯品會所有流量的入口
網(wǎng)關(guān)價值:所有中心化控制點(diǎn)都可以統(tǒng)一控制
我們底層技術(shù)選型從servlet到akka到netty,servlet就跳過,底層性能模型差太多
線程模型使用netty的boss worker,目前所有業(yè)務(wù)的處理由于不會阻塞住worker線程,都跑在worker上。
目前性能數(shù)據(jù)為在12核的機(jī)器上,1k payload大小的請求http請求,后端服務(wù)為rpc為9萬 http為7萬多。瓶頸在cpu上
線程模型,我們進(jìn)一步合并了后端請求調(diào)用的woker線程池跟接受請求線程池,考慮連接池可以讓一個請求的worker線程處理,跟發(fā)送到后端的請求線程處理在一個worker上,這樣性能會提高8%-10%,因?yàn)樯倭艘淮吻袚Q。
比如說是http到http, 后端http的話就是一個連接池,拿連接的時候,怎么判斷拿連接就是當(dāng)前線程所在的連接,很多連接就可能分在不同的worker上,需要一個標(biāo)志位,這個標(biāo)志位可能可以選擇線程ID或者是線程特點(diǎn)的一個東西去做一個標(biāo)志位,優(yōu)先拿這個,如果沒有我再去拿其他的。也看你的模型,http這種非雙工到后端會需要連接數(shù),可以均勻的分在這個24個模塊線程上. 如果是rpc,可能一兩條連接打天下,就需要盡量不切換或者多創(chuàng)建連接的代價達(dá)到不切換。
插件化方式需要不在worker線程上運(yùn)行,做到隔離問題不影響,包含類的隔離與線程池隔離,目前想采用共享線程池+獨(dú)立線程池分配模式,先使用共享,沒有可用則獨(dú)立使用一個小的線程池。
netty這塊來調(diào)優(yōu)實(shí)踐
除了worker線程切換的優(yōu)化,線程安全與共享的方式做了一些事,可以讓非安全的代碼在線程內(nèi)共享,類似threadlocal,但我們用的netty的fastthreadlocal,更快更好,底層用的是數(shù)組,而且減少了對象的創(chuàng)建,服用了對象對gc也更友好
netty里比如stringbuild等都這么共享使用,我們代碼用到也是直接參考netty的用法。
netty有一個平臺類,可以直接使用,由于我們是jdk7,用這個netty copy了jdk8采有的一些類,比如hashmap的優(yōu)化,可以直接用,包括包含mpsc這類
整個網(wǎng)關(guān)不會出現(xiàn)任何一處有鎖或者有卡的問題,日志也是全異步,如果一定有鎖,比如說我涉及到一些多個線程去做共享狀態(tài)改變,那我們就有準(zhǔn)確性的問題,要求必須準(zhǔn)確,盡量通cas的方式來搞定,而不是說強(qiáng)行加鎖,是不會做一個強(qiáng)行加鎖,通常用自旋方式來把這一塊解決掉,整個性能是比較好的。
使用netty的native epoll 性能更優(yōu),只需要很簡單的更改幾個類名就可以。
這塊性能話我們測下來其實(shí)是有一個提升的,就是CPU會稍微低一點(diǎn)點(diǎn),性能沒有太明顯,就CPU稍微比平時負(fù)載會低一點(diǎn)。
bytebuf的問題。netty在就是官方文檔當(dāng)時有幾種對比,但根據(jù)你使用場景可以自己跑一下,比如你需要解包并且轉(zhuǎn)換所有包內(nèi)容,那么堆外不一定有優(yōu)勢。我們是混合場景,選擇堆外。
boss線程數(shù)根據(jù)端口數(shù)決定,我們只監(jiān)聽一個,不需要設(shè)置cpu的一半,走這個到處都有的最佳實(shí)踐。當(dāng)然如果你設(shè)置一半,其實(shí)也不會用到,這個可以看netty源碼邏輯。
就netty我我們目前的話大部分都是說 拿到完整的http做業(yè)務(wù)處理,但是我們其實(shí)有很多可以前置,不需要解析完整的http。比如說,我們的通常是一次請求,或者是我們的一些非法請求,包括限流防刷這種非法的教練,我們其實(shí)只需要根據(jù)ITP的一個請求函和請求頭,我們就可以做一個判斷,并不需要把它完整地拆包,我們就可以提前判斷。這塊呢就需要我們把netty的這塊http的解析重寫,netty本身對http是一個狀態(tài)機(jī)的方式解析。
就說我們通常在做優(yōu)化的時候,你到底優(yōu)化哪個代碼,是去一段段扣代碼還是怎么弄,我覺得還是不要扣代碼,扣代碼這個價值點(diǎn)很難找,就是說你可能覺得這塊可能是不是怎么樣更好,我是利用一個string拼,然后我如果換一個方式是不是性能更好?可能效果并不是那么明顯。
還是盡量用工具,用工具從系統(tǒng)底層到整個jvm部分最好是能貫通起來,貫通起來也方便于排查問題。整個如果把這塊是從系統(tǒng)底層到j(luò)vm層,整個通了之后,你排查問題的速度也比較快,然后定位問題比較快。
比如說火焰圖,我們跑其實(shí)是會跑到火焰圖數(shù)據(jù)的。我們性能測試是會看一下跑出來的是不是符合我們的預(yù)期。第一個就是說系統(tǒng)函數(shù)底層的調(diào)動是否符合我們的預(yù)期,系統(tǒng)上的一個開銷,然后到j(luò)vm方法棧上的一個消耗,這塊比如說比較簡單的作用,谷歌的火焰圖這個工具,netfilx火焰圖的生成,jvm上就得采樣,就你可以設(shè)置采樣頻率,,采樣下來之后會拿會拿到一個詳細(xì)的一個圖。
就會看到我們整個網(wǎng)關(guān)跑出來一個結(jié)果,當(dāng)然這里面又有細(xì)節(jié),怎么看火焰圖的問題了,快速識別一些性能問題。
下面還有java 自帶。最近不是這個jmc比較火嘛,就說這jmc開源之后把團(tuán)隊給裁了。就是jmc這塊自帶的一個分析工具,他其實(shí)也會到一個消耗,是整個的一個方法調(diào)用,你會看到哪一塊消耗CPU比較多,但JMC的話是覺得沒有火焰圖這么比較直觀點(diǎn),看你分析哪些問題,各有特點(diǎn)。火焰圖可能就更多的在系統(tǒng)函數(shù)上,jvm都可以做到就是整個都可以看到整個。
剩下就說我在寫代碼的時候,比如說一個選擇,比如說上周的一個例子,就是我們在做這種base64的一個轉(zhuǎn)碼的過程當(dāng)中,我們可能一些特殊需求需要轉(zhuǎn),那我現(xiàn)在有這么多工具類,那我到底用哪個工具的性能更好呢?我們通常做法是寫一個單元測試來跑一下、測一下,但是這種單元測試很不可靠,因?yàn)檫@種單元測試跟最終你去jvm運(yùn)行跑代碼其實(shí)區(qū)別還是很大,首先他會沒有去做編譯優(yōu)化,有很多問題。
java官方也是推薦直接基準(zhǔn)測試,用基準(zhǔn)測試來測它到底性能怎么樣,但是這種基準(zhǔn)測試寫的話,最好去把官方的demo讀一遍,它里面其實(shí)有很多坑,比如說你很容易寫一個for循環(huán)在里面,for循環(huán)還很容易被優(yōu)化掉。 還有就是你解決測試的時候,其實(shí)最后值并不返回,他去跑的時候就會把優(yōu)化掉,
系統(tǒng)就會去跑一塊代碼,就認(rèn)為最后這個值根本沒有用到,沒用到優(yōu)化就全部蓋掉,全部蓋掉之后,跑出來你會發(fā)現(xiàn)系統(tǒng)數(shù)據(jù)很好,但是實(shí)際并不是這個樣子。所以的話這塊有很多坑,寫他之前最好去把官方的demo通讀一遍,會避免很多東西,他的demo也比較詳盡,避免很多錯誤的一個寫法。我后面附了一個日常的demo吧,這可能就是上周我們跑64的一個demo寫的。比如剛才的坑,就說明這樣去運(yùn)行的時候,其實(shí)并沒有返回就不會處理,它其實(shí)提供一種方式上處理,就是你把它消費(fèi)掉,這塊的話會跑一個性能數(shù)據(jù)出來,最終結(jié)果會是這樣的一個情況。 這樣話就會看到那到底選擇哪一個比較好,比如說我們每秒的調(diào)動次數(shù),那可能下來發(fā)現(xiàn)確實(shí)JDK8自帶的這個性能比較好一點(diǎn)。
下面聊到這個GC優(yōu)化與排查。Gc優(yōu)化,其實(shí)對網(wǎng)關(guān)來說很難容忍這個gc問題,因?yàn)樘貏e是高并發(fā)的情況下,比如說在上做促銷的時候,流量很大的情況下,由于網(wǎng)關(guān)發(fā)生gc導(dǎo)致整個超時了是很大一個問題,所以我們在gc上盡量控制頻率,我們大概現(xiàn)在的情況下是我們能控制到是說一個月一次cms gc,但是要想網(wǎng)關(guān)其實(shí)流量很大,每天都有流量過來,這種大促、更別說這種搶購之類導(dǎo)致的一些流量。所以我們在這塊做了很多一些優(yōu)化,就是整個控制它的一個頻率,后面會講到。
然后的話是我們一些常見的踩坑的問題,比如說我們會遇到這種熔斷、指標(biāo)統(tǒng)計上的一個踩坑。這可能這一塊的話,比如說熔斷分方法的熔斷和服務(wù)器的。大家都知道壟斷會要寫這個計數(shù)器,計數(shù)器我們可能要統(tǒng)計容納多少秒內(nèi)失敗次數(shù)等于多少的話,比如說我定義是十秒內(nèi)失敗次數(shù)——請求數(shù)(就有幾個維度)請求十次,十秒內(nèi)連續(xù)失敗,或者按幾個維度來吧,失敗率是60%,我就認(rèn)為這個服務(wù)不好,應(yīng)該壟斷。
這樣的話就會導(dǎo)致我們其實(shí)會去統(tǒng)計很多這種指標(biāo),這種指標(biāo)就會帶來一個問題,就是說我們的統(tǒng)計量太大。比如說我們當(dāng)時是API網(wǎng)關(guān)其實(shí)就是一個一個API沒有什么問題,但我們現(xiàn)在反向代理就會帶來這個問題,因?yàn)榉聪虼砩婕耙粋€通配,后面的這個方法很多,所以這個帶來問題就是熔斷上統(tǒng)計會導(dǎo)致我們內(nèi)存的一個爆掉。指標(biāo)統(tǒng)計這個容量上,也是反向代理的時候也會遇到一個問題,因?yàn)槲覀兤鋵?shí)請求的url是很多的,無數(shù)的url過來,我們?nèi)ソy(tǒng)計的話,最后遇到這個問題,我們通常會統(tǒng)計1秒鐘、10秒鐘這種各種的一個指標(biāo),或者說1分鐘5分鐘,這種都會帶來一個問題。
包括這塊順帶再說一下kafka,包括我們用到kafka,這個坑是跳不過去的,原生代碼里面自帶的這個采樣,采樣的話他會統(tǒng)計1分鐘5分鐘15分鐘,但是我們網(wǎng)關(guān)流量很大的情況下,這個根據(jù)jvm的一個原理,就是說我肯定首先對象進(jìn)入這個年輕帶,他年輕帶里面就是倒騰幾次,就老年代。流量很大,我們這塊這ygc就會特別頻繁,他就很快進(jìn)入老年代,但很快進(jìn)入老年代,就會帶來這個問題。
那就就相當(dāng)于它的統(tǒng)計指標(biāo)全部在老年代堆積。堆積之后,關(guān)鍵是它采樣就說,可能我采樣指標(biāo)是1分鐘5分鐘15分鐘,old區(qū)直線增長,就會觸發(fā)我們的cms gc,這塊我們最后的一個時間我們就直接把這塊代碼干掉,因?yàn)槲覀兤鋵?shí)沒有辦法統(tǒng)計資料,不用統(tǒng)計指標(biāo),我們不想它這個給我們帶來的問題
這塊也是old區(qū)的一個問題,就是說我們當(dāng)時之前是15天左右發(fā)生一次,上線之后發(fā)現(xiàn)每天穩(wěn)定增長400兆,就去排查這個問題,反查發(fā)現(xiàn)kafka的一個問題,反查之后發(fā)現(xiàn)里面有數(shù)據(jù)結(jié)構(gòu)里面有個跳表,我們反查了一下代碼發(fā)現(xiàn)只有kafka在用,然后這塊就跟蹤到kafka,因?yàn)橐呀?jīng)找到這個到底是誰關(guān)聯(lián)它。這個反查到了之后發(fā)現(xiàn)是kafka的問題,我們做了一個空實(shí)驗(yàn)之后,發(fā)現(xiàn)這個其實(shí)問題就解決掉,就回到我們原先那個頻率上。
說到這個日志,我們剛才說的日志其實(shí)是權(quán)益部的一個輸出日志,這塊我們是按log4j2的一個方式處理,就是二點(diǎn)幾的版本,但我們在用的時候2.6還沒出來,但是后面2.6出來是很推薦大家試一下,因?yàn)檫@塊還做什么gc優(yōu)化,它號稱是減少了很多gc問題,我們測下來也是基本上gc耗時也會有明顯的一個降低。
這一塊的話是說我們當(dāng)時上線的時候,我們其實(shí)沒有設(shè)cms gc到底什么時候觸發(fā)的,比如說都是區(qū),可能通常大家會設(shè)置到說75%就觸發(fā)了,然后不設(shè)置,我們覺得這塊讓jvm去做可能更好一點(diǎn),就說我們不知道什么時候觸發(fā)是合理值,既然我們不知道就讓jvm來自己來做自己來做動態(tài)調(diào)整。但發(fā)現(xiàn)一個問題,就是現(xiàn)網(wǎng)上的資料都說,包括我們?nèi)プx這個幾本jvm的書,他們也都會說,這個JDK你不設(shè)的話,它默認(rèn)是68%處發(fā)這個cms到我線上上線之后發(fā)現(xiàn)并不是什么,我們92%才觸發(fā),我們就看一下我們jdk的一個版本,我們版本是7,我們就拿了一個open jdk的原代碼,就算了一下,算出來,確實(shí)是92%出發(fā)。
這塊我舉這個例子意思說,看到這種問題,其實(shí)通過這個代碼是很容易找的,我們直接可以對你的jdk版本拉下來之后去找,并不需要去相信說我去查各種資料,發(fā)現(xiàn)沒有,到底是多少觸發(fā),這塊直接代碼里面很明顯,其實(shí)也不復(fù)雜,就是雖然說C++這種代碼,那我們其實(shí)只是從去找一些這種配置的話,就很很容易快速識別的。代碼里面是沒有秘密的。
還有一個問題就是說高io導(dǎo)致jvm停頓的問題,這種測試會發(fā)現(xiàn)一個問題,說我們在連續(xù)壓測大概四五個小時,就會發(fā)現(xiàn)我們的有些四個9的數(shù)據(jù),可能不上網(wǎng)看,四個9的數(shù)據(jù),可能會出現(xiàn)有一秒增加,其實(shí)我們大部分時間都是在一毫秒的一個范圍,但是發(fā)現(xiàn)有四個9會有一種一秒的特別長的一個時間,覺得這可能是一個問題,我們線上會這種其實(shí)不太能接受的,我們線上基本上時間都是經(jīng)過網(wǎng)絡(luò)的一個調(diào)動后端返回這個時間,請求發(fā)起進(jìn)來出去的時間大概都在一兩毫秒。所以覺得這塊我們是不能接受這一點(diǎn),然后就去排查問題,就發(fā)現(xiàn)這樣一個問題說高io導(dǎo)致jvm的停頓
一個是把gc日志并不直接放磁盤上,放在這里面放內(nèi)存文件系統(tǒng)上,然后必須加上這個參數(shù),因?yàn)閖vm在運(yùn)行,它會輸出、關(guān)聯(lián)操作好幾個文件,它有一個文件是你用一些jvm的命令做一些分析用的
后面我們就舉例子來講就做網(wǎng)關(guān)的時候,因?yàn)樽鲞@種對質(zhì)量要求比較高一點(diǎn)。因?yàn)槲覀冞@個承載的是一線流量,其實(shí)影響是非常大的,對性能也是要求很高,而且又涉及到我們這個改動很頻繁。各種業(yè)務(wù)方可能有些需求點(diǎn)在我們這我們要去為他們做一些定制化的一些開發(fā)。當(dāng)然可以用插件化來做,但是這塊其實(shí)有很多改動,所以就舉一些場景,到底應(yīng)該怎么做?
比如說讓你來寫網(wǎng)關(guān)的代碼你應(yīng)該考慮哪些點(diǎn)?比如說我們通常做一些抽象,我們會用到一些通用技計數(shù)器,我們以這個場景來講,我們要去做一些考慮。這也是拿我們平時遇到一個點(diǎn)來發(fā)散的講,就說網(wǎng)關(guān)到底哪些問題?
我們通常會用一個通用計數(shù)器這種東西,因?yàn)槲覀儠龊芏嘤嫈?shù),比如說我們限流,后端的服務(wù):我們要保護(hù)后端服務(wù)我們來做限流,再比如說我們要做防刷,我們可能要根據(jù)ip或者根據(jù)session各個維度來做防刷,做成保護(hù)安全上的一個東西,或者有一些技術(shù)上的一些需求,比如說是熔斷,它其實(shí)也是一個技術(shù)上的需求,這種技術(shù)啊我怎么來做的?
可能想比較簡單,它就Java自帶的這個原子類的這種cs,我就直接上面加就可以。其實(shí)也會有問題,原子類它的維度比較單一。來看這樣一個實(shí)現(xiàn),其實(shí)它這里面就有一個是計數(shù)的一個概念,還有一個時間的概念,這兩個概念我們就需要用時輪的方式來實(shí)現(xiàn)。那時間輪的方式就會帶來一個問題,用時間輪的話我就要有一個清晰的概念在里面,包括時間輪怎么快速去查找,比如說你看時間輪列表的話,性能上一個問題,包括考慮我用時間輪怎么去避免產(chǎn)生GC或者一系列的問題。
我們這塊要實(shí)現(xiàn)一個滑動窗口的一個時間輪。那就首先第一個就是選擇,到底是提前清理的話,就使用時清理,提前清理怎么去清理這樣一個問題。我們的最后選擇是說我們使用時清理。
這樣一個統(tǒng)計的話,那么實(shí)現(xiàn)上首先用數(shù)組去實(shí)現(xiàn),做對應(yīng)上的一個映射,數(shù)組里面的對象,但是這就涉及到我們要怎么去清理它?我們并不我們?nèi)绻苯蛹右话焰i去清理,比如說我們每次去鎖住,它就是每個線程都會遇到這個鎖,這個性能會降很低。所以我們是用一個自旋鎖的方式,因?yàn)槲覀兤鋵?shí)是不停的請求進(jìn)來,我們?nèi)プ孕涂梢粤耍孕幌戮涂梢?,這塊時間的話性能是比較好的。
然后這塊還有一個問題,就是說那我們那JDK8在后面供應(yīng)對象是明顯的比前面這個cas一個原生對象性能高很多了,但為什么不用后面這個,這就是一個內(nèi)存上的問題。比如說用前面說用度量工具測,測完之后發(fā)現(xiàn)前面完全滿足我們的需求,因?yàn)楹竺娴脑挻_實(shí)性能會好,但是后面的內(nèi)存又會高很多。大概在60萬的對象,后面是45兆,前面是一點(diǎn)幾兆,就這種內(nèi)存上的一個對比,我們可能內(nèi)存上接受不了這個。
包括這種對象,我用完之后我要清理掉,對象清理掉之后,它就會帶來gc問題,我這個對象已經(jīng)在新生代里面倒騰了幾次,因?yàn)榱髁亢艽?,容量很大的時候,我就不停的ygc。ygc幾次我就進(jìn)老年帶,那去的時候又會帶來老年代這個對象增加,清理掉,解除這個關(guān)聯(lián)關(guān)系之后,老一代又會直線增加,我們想控制1月一次的cms這個目標(biāo)給它很難達(dá)成了。所以這塊就是我們的作用這樣一個需求就有很多點(diǎn)需要考慮。
有相關(guān)的一些點(diǎn)考慮到之后,我們還要把它做成一個抽象工具,就是說要用的話,我們直接調(diào)就可以做,這些考慮點(diǎn)全部在里面,而并不是說我們每個人去實(shí)現(xiàn)一套,
再說一個非常有意思的一個GC場景,其實(shí)我們有些技術(shù)場景,我們會用到這種lru的方式我們?nèi)ソy(tǒng)計,因?yàn)椴粔蛴?。比如說我們要統(tǒng)計一些東西,我們只能用最近最常使用的一個算法去做,但是這就會帶來幾個問題。我們?nèi)绻阉O(shè)置很大,準(zhǔn)確性倒是挺高的,準(zhǔn)確性提高了之后,GC問題就來了,如果我設(shè)小了,但是又準(zhǔn)確性不夠,那我多小合適了,這個需要根據(jù)流量來。
這個問題呢可以進(jìn)一步抽象,其實(shí)會帶來很多問題,就是說那這個問題進(jìn)一步抽象到說所有能熬過ygc到了老年帶,并且把它反復(fù)創(chuàng)建問題解除,其實(shí)都會有這個老年帶增長的一個問題。
比如說我們可能像廣泛的一些日常這種使用,如你在開發(fā)日常代碼的話,肯定會涉及到連接池,這是一個比較常見的例子。就說你的連接池會設(shè)有一個共享連接,共享連接的話,幾次ygc后也會進(jìn)入老年帶。進(jìn)入老年帶之后,會有配置一些參數(shù)去把這些共享連接給關(guān)掉動態(tài)變化數(shù)量,檢查閑置連接啥的,會清理掉,這個對象就在老年帶里待著了。但是在老年帶里的連接數(shù)發(fā)現(xiàn)不夠又會去創(chuàng)建它,創(chuàng)建之后就會,于是這個隨著流量不停的在老年帶里面創(chuàng)建對象,就會帶來這個問題。
最后一點(diǎn)呢就是說我們寫了這么多代碼之后,發(fā)現(xiàn)一些規(guī)律性的東西,盡可能復(fù)用對象,盡快釋放對象。
如我們在創(chuàng)建一個對象的話,能復(fù)用盡量復(fù)用,比如說我們用了一些計數(shù)器,我們移除的時候,其實(shí)放在一個隊列上,我們要創(chuàng)建什么首先從隊列上去取。隊列沒有,我們再用一個就是減少這種垃圾對象地方創(chuàng)建。
netty而且還是很有參考價值,如果你把源碼過一遍的話,可能很有很多想象不到的點(diǎn),會對你有些價值。
盡量狀態(tài)無鎖,如果一定狀態(tài),先走共享。先走線程內(nèi)共享,就是模塊內(nèi)的一個共享,模塊的共享之后,如果說發(fā)現(xiàn)還是解決不了問題,那你沒辦法,你只能cas,那cas,我盡量選擇小的一把鎖,怎么小怎么來,而并不是我加一把大的讀寫鎖或者怎么樣,我可能做一個循環(huán)做一個自選鎖來做。
最后一點(diǎn),這里比較重要,就是說在寫作代碼,因?yàn)榫W(wǎng)關(guān)的代碼這塊,入口流量,如果有問題會影響很嚴(yán)重。當(dāng)然我們通過一些發(fā)布的方式可以避免,但這塊的話就是說你在寫的話,盡量了解你代碼你寫的每一行代碼背后的邏輯。里面內(nèi)部到底是怎么運(yùn)行,再用這種基準(zhǔn)測試去度量,度量你的代碼到底有多少的性能損耗,到底是怎樣的
我的這塊分享就完了!