億級網關 janus 性能優化與 JVM 調優實踐

本文根據蔡磊於唯品會“唯享·技”分享會 2018年6月9日上海場 分享內容整理而成。

億級網關 janus 性能優化與 JVM 調優實踐

“唯享·技” 是唯品會對外分享的平臺。 希望以此平臺分享唯品會的技術研究案例與實踐的結果,也能聽到更多技術方向上的乾貨分享。

唯有分享可以讓技術的腳步更快, 享受技術鑽研的樂趣,在此讓我們聽見技術流~

本文講師

億級網關 janus 性能優化與 JVM 調優實踐

蔡磊

任職於唯品會基礎架構服務化團隊,負責API Gateway/Reverse Proxy的設計與開發,性能調優與疑難雜症。在營銷系統與微服務架構方面技術經驗積累深厚。

目錄

  1. janus背景介紹與技術選型
  2. 線程模型與優化
  3. netty深度調優與實踐
  4. 性能瓶頸快速定位工具與方法論
  5. 通用計數器實現
  6. 一個有意思的gc問題
  7. 踩坑後的代碼規範總結

janus背景介紹與技術選型

先說標題中的億級網關,我的角度是用戶量億級,tps按小時算是百億級別以上,主要是Api網關。

我們定位為:高性能、高可用、可擴展、可管理、可治理、安全的網關產品,作為唯品會所有流量的入口。

網關價值:所有中心化控制點都可以統一控制。

我們底層技術選型從Servlet到Akka到netty。Servlet就跳過,底層性能模型差太多;用Akka 這種純異步的方式是比較好的,但是作為網關的話,可能不算適合,它在線程模型上是有一些問題的,這塊我們後面會講到;所以現在基本上主流的是用netty來做這一塊。

我們現在這塊主要做一個ApiGateway,但是我們考慮到一個問題,我們唯品會這邊有部分Php程序,即一些老的程序,它們現在也要容器化,並且不止是Api接口有頁面存在,容器化了之後就涉及到一個問題,動態的一些發現,包括要做一個對外的統一控制,所以我們也把我們的Api網關進行了一部分裁剪,然後做了整個的一個反向代理的網關 。

Api網關需要一個一個配對對外暴露的,亞馬遜的、阿里的這塊都是這樣一個方式,但是我們這裡就說反向代理,通常反向代理是會支持一個正側匹配,還有反向代理其實不是Api,因為有頁面,可能是動態頁面有一些靜態化的東西,所以需要做一個反向代理的網關。我們去年就說這塊定義上,只要有頁面就走反向代理。

線程模型與優化

前面是一個介紹。然後是線程模型優化,這塊我們遇到一些問題,也涉及到一些調優,包括在後面講netty的時候也會說到這一塊。

我們現在的一個性能數據,因為跟機器很強相關,單純的談數據,拋開機器與壓測場景是沒有什麼意義的。我們在常規的一個普通12核機器上跑,在payload大小是1K的情況下,這種請求轉換為後端的http,大概性能是會在7萬多到8萬之間的一個數據。如果後端是一個RPC的話會到9萬左右的一個數據。如果payload大的話,這塊性能是會下降的,比如說100K的,測下來大概後端http會有大概4萬多5萬,因為後端超過100K我們會進行壓縮,壓縮這塊對CPU的消耗很大。但我們最後測下來的一個相關的瓶頸,沒有在IO方面反而存在CPU方面,只要我們CPU增強,我們的tps可以繼續漲。

線程模型這一塊,用到netty經典的boss worker模型。這個線程池其實是有一些可做的點。請求來的時候,怎麼樣去讓這個worker線程更加均衡一點?比如說一個請求發過來之後,從接收它會轉到worker線程上去,boss線程接收到這個時候去讀的話,會在worker線程轉給它去讀,下面問題就來了,那後端可能是一個RPC 的調用,還要發一個RPC到後端,那怎麼讓這個性能更好?

先共用線程池,共用worker線程池比較簡單,在操作上比較好做。但是基於這個做了之後,後面的優化點出來可能是比較難做的一點。接受請求之後最好是不切換上下文,雖然共用worker線程池,但是不知道大家看到了netty的源碼沒有,有個切換過程,就是我們怎麼減少這個切換。比如說現在worker線程通常是和CPU數量是一樣的,可能是24核的,那就24個線程,當一個請求來的時候,再去調後端的一個服務的時候,那麼調後端服務怎麼不切換這個?

因為去調後面的一個服務也是個異步的,整個網關是全異步化的一個過程,這個時候的請求會去看netty做一個判斷,是否在當前的線程裡面,如果沒有就提交一個隊列,放到隊列裡面,然後去觸發,這時候線程會不停的從這個裡面拉東西過去執行的,所以最好是不要切換。我們測完切換和切換成本大概是10%的一個性能數據,如果切換了性能數據下降10%,不切換10%的一個性能數據會有提升。

我們這塊做的就是有一些複雜點,在這個切換上需要通常後端連,比如說是http到http, 後端http就是一個連接池,拿連接的時候,怎麼判斷連接就是當前線程所在的連接?因為有很多連接,可能分在不同的worker上,需要一個標誌位,這個標誌位可以選擇線程ID或者是線程特點的一個東西去做,優先拿這個,如果沒有再去拿其他的。也看你的模型,http這種,可能連接用一百條連接,均勻的分在這個24個模塊線程上,這種不是完全的一個異步通信。

我們做RPC,最後的連接不會太多,因為RPC中我們自己做的是全異步化的一個方式,全雙工的方式,RPC可能一條鏈接就會跑很高的pps,可能不是等於CPU數量,所以可能這個worker線程上並沒有這樣一條連接,就需要去其他地方去,但是可以優先判斷一下,如果這裡面有就從這裡面去,這樣性能大概有10%的提升,但是這塊複雜度比較高一點。這是我們在線程池上的一些優化變化。

再下來一點就是插件化的問題。網關如果做插件化的話,我們可以減少一些發佈的問題,插件做成一個類似於配置中心數據下發的概念,現在網關是每個域來做一個接入,一個業務來接入之後,可能有些相關的配置,但是這個域可能有些特殊的權限認證,或者有些特殊的線流,或者有很特殊的一個安全處理,所以這部分做一個插件形式的下發之後,怎麼讓它去運行呢?

這塊線程池的一個模型,我們現在是想做一個共享線程池。首先大家先共享,共享之後有個卡的問題。大家可能做這種都會遇到線程池隔離,線程池隔離之後,就會遇到線程池隔離導致到底怎麼分配的問題,線程池隔離是快慢的一個隔離,還是怎麼樣的隔離?我們現在是先用共享一個線程池,如果不夠給這個域再單獨分,就不會導致卡的一個問題。

netty深度調優與實踐

netty來調優實踐。前面講的是worker線程池的共享。但是我們通常在寫代碼、在做微服務的時候,會經常提到儘量無狀態去處理這些東西,我們的代碼無狀態化,但是有些東西確實是很難無狀態化的,你可能要有一些有狀態的東西,或者是代碼裡面可能涉及線程安全的一個問題。一些是非線程安全怎麼去做?怎麼保證它的性能?這塊我們也遇到這些問題。但是我們結合參考點netty本身的一個經驗,多讀netty的源碼,多去了解它的一個實現機制,就會找到一些方式去做。

我們這塊參考的比較多的是用netty去做。舉個例子,我們有一些加密的類在使用JDK,或者是用一些其他的,這些都是非線程安全的,那其實我們的模型是跑在worker線程上了。剛才講的這個模型,其實網關通常做netty都會遇到幾種選型的問題,netty本身運行在worker上,但是業務線程怎麼處理?業務到底怎麼做,到底是跑在在netty 的worker上,還是單獨開新線程池,這裡單線程池的切換成本性能損耗較大,但帶來的好處就是你可以對這個線程池有很多獨特的管理,可以做定製化的內容。

現在由於常規的輕量級 CPU運算,處理速度特別快,然後都是耗CPU的,所有全部是運行在worker上。包括說獨立的線程池,都會遇到的問題,怎麼去做這種非線程安全的代碼?線程內共享,包括netty會做很多種優化,在單獨的線程內享,對線程內是無鎖的,這個很像AKKA的一個方式。用AKKA的時候會發現AKKA其實就是不太去考慮線程安全性的方式,因為它本身是一個線程去運行一個actor,它並不會有併發的問題,那我們參考這個netty也是類似的,其實是一個代碼運行在一個worker線程上,也不會有這相關的問題,只需要在裡面做一個判斷是否在當前線程,如果不在就提交到這個隊列裡面去做處理。

一個非線程安全,只要線程內共享,每次從當前線程獲取,比如說我們用的線程內Map這種安全方式,netty提供了一些比JDK更好更快的FastThreadlocal,我們也是在裡面來做模塊線程的共享,整個的這種非安全性的代碼就可以進行處理掉。整個網關就不會出現任何一處有鎖或者有卡的問題,如果一定有鎖,比如說涉及到一些多個線程做共享狀態改變,那就有準確性的問題,要求必須準確,儘量通過cas的方式來搞定,而不是說強行加鎖,通常用自旋方式來把這一塊解決掉,整個性能是比較好的。說到日誌這個點,後面也會說到日誌的優化,我們日誌這個點,也是全異步。

第二點就是netty這塊的一個模型,這點可能在各個網關的優化點上說的比較多,就會聊到epoll底層的一個網絡的這個模型,包括之前有同事問這個不是用epoll ,只看到選擇器,好像沒有發現什麼代碼。其實這個epoll模型全是JDK底層幫我們自動選擇了。比如說跑到MAC上,它可能是用另外一種網絡模型,不同的版本的內核上,會選擇不同的模型,如果覺得這塊你不太確定的話,比如說用2.6以上它就會自動epoll,不太確定可以加參數,在啟動參數的時候,強行指定它必須用那個模型。

這塊對比netty覺得JDK實現這個C代碼是有些可能沒有自己做性能好吧,參考tomcat基於epoll來寫了一些C的代碼,對比JDK自帶的,底層epoll細節使用上還稍微有區別。netty號稱是比JDK自帶的性能好,這塊其實不需要安裝什麼東西,只需要引入一個native就可以了,然後在裡面寫法上就是有改變,比如說在寫netty的時候,在裡面構建的時候幾個東西稍微需要改幾個參數,名字改一下就行了。這塊性能話我們測下來是有一個提升的,CPU會稍微低一點點,性能沒有太明顯,就CPU稍微比平時負載會低一點。

第三點就是個bytebuf的問題。netty在官方文檔有幾種對比。一個是池化的內存和非池化的內存的一個對比,還有一個是直接堆外內存和堆內的一個使用對比,組合下來就是四種。池化的堆外這個對外的性能是比較高的,但這一塊也是根據業務來。通常拋開業務場景來談這個性能都會有一些問題。比如說後端如果是一個RPC調用,因為前端是http這種APP端,請求都是http,發過來之後,需要把它轉成RPC,所以這就涉及到必須要去拆包,拆了之後去轉,那其實就意味著必須把它堆內,可能堆外性能並沒有明顯優勢。跑一下確實性能上數據是沒有太大差距。

但有另一個場景——有後面的http的,http很多時候我們不去解析,直接把它寫出去,不解包的話性能也會很好,那就用堆外可能性能是非常好的。所以這塊,大家結合自己的場景做壓測,通過壓測來得出這個東西,而並不是直接看官方數據,按官方數據說哪方面性能好。

再下來一點,就是說這個boss的線程,通常我們demo上都會有一個問題,包括大家寫一些代碼copy的一些例子都會有這個問題。就是IT在啟動的時候都會把worker默認設置成CPU核數,這個是根據最佳經驗得出來是沒什麼問題,但是就是這個boss線程再去接收的話,線程通常都會是CPU的一半,寫這樣的一個代碼是有一些問題的,boss線程其實沒有用到CPU的一半,它其實根據端口決定了。如果去看它的源碼,就會發現這樣一個問題:它完全是根據端口來決定,就是有多少端口,就用多少線程。當然如果端口特別多,比如現在有20個端口,可能啟動的時候又在http上監聽,又在https做了什麼其他的一個內網RPC,可能有多個端口,端口可能有24個,那boss線程設置12個還是比較合理,但是其實只有一個網關,對外只有http請求,所以我們只有一個端口,寫多了也沒有用。配多少其實也沒有影響,只不過這個會給別人疑惑,因為在真正啟動過程中,其實不會啟動多個,所以倒也沒有這個損耗。

億級網關 janus 性能優化與 JVM 調優實踐

然後再下來一點是我們最近正好在討論的方案,netty這塊解析,handle我們準備做一個優化。就netty目前大部分都是拿到完整的http做業務處理,但是我們其實有很多可以前置,不需要解析完整的http。比如說,我們通常是一次請求,或者是我們的一些非法請求,包括限流防刷這種非法的請求,我們其實只需要根據ITP的一個請求函和請求頭,我們就可以做一個提前判斷,並不需要把它完整地拆包。這塊就需要我們把netty的http的解析重寫,然後再結合我們future做一些優化。netty本身對http是一個狀態機的方式解析。

代碼參考netty的一些東西,比如說我們重度使用netty的InternalThreadLocalMap,包括前面說的fastthreadlocal。線程安全的,我們就可以在這個線程內共享,而不需要頻繁的去創建,包括我們用了一些數組或者是其他相關東西,大家可以看看這塊的源碼,這塊也是用的比較多的。

目前是整個唯品會這邊都是在跑的JDK7,我們用JDK7的話,JDK8的一些東西——比如說這個concurrenthashmap這種優化——我們可以用PlatformDependent享受到這些方面的東西,但是用netty的時候去看一下,4.1的代碼就把這塊移除掉,因為不支持jdk7了,沒必要了。4.0的代碼是有了,netty會把JDK8裡面的一些點明顯的優化點挪過來。

然後最後一點就是worker線程的一個問題。前面也講到了,就是說這個線程儘量不去切換上下文,怎麼去做到這個切換的問題,儘量保持在一個線程上整合請求來做,比如說現在可能要兩次外部請求,就要常規的一個請求來做,首先要進行一次外部調用認證,過了之後再去調後面的服務,然後整個這個流程都不切換的話,性能會數據會跑得非常好。

性能瓶頸工具與快速定位

再下面一點就是性能瓶頸工具快速定位。這塊我司正好最近開源一個工具。大家可以關注下,相關這幾點都有。這塊我覺得是排查問題的利器。通常我們在做優化的時候,到底優化哪個代碼?是去一段段扣代碼還是怎麼弄?我覺得還是不要扣代碼,扣代碼這個價值點很難找,你可能覺得這塊可能是不是怎麼樣更好,是利用一個string拼,如果換一個方式是不是性能更好?可能效果並不是那麼明顯。

還是儘量用工具,用工具從系統底層到整個jvm部分最好是能貫通起來,貫通起來也方便於排查問題。如果把這塊從系統底層到jvm層整個通了之後,排查問題的速度也比較快,然後定位問題比較快。

比如說火焰圖,我們跑其實是會跑到火焰圖數據的。我們性能測試是會看一下跑出來的是不是符合我們的預期。第一個系統函數底層的調動是否符合我們的預期,系統上的一個開銷,然後到jvm方法棧上的一個消耗,這塊比如說比較簡單的作用,谷歌的火焰圖這個工具生成netfilx火焰圖,jvm上採樣你可以設置採樣頻率,採樣下來之後會拿會拿到一個詳細的一個圖。

億級網關 janus 性能優化與 JVM 調優實踐

這兒有一個圖,這是我們整個jvm裁剪的一個圖,這一個火焰圖,我們跑出來一個數據,它的底層是比較大的,比如說這塊是一個read,會看到上面的一個調用到底是什麼樣一個情況。就會看到我們整個網關跑出來一個結果,當然這裡面又有細節,這就是怎麼看火焰圖的問題了,快速識別一些性能問題。

下面還有java 自帶,就是jmc這塊自帶的一個分析工具,它其實也會看到一個消耗,是整個的一個方法調用,可以看到哪一塊消耗CPU比較多,但jmc的話是覺得沒有火焰圖這麼直觀,具體看分析哪些問題,各有特點。火焰圖可能就更多的在系統函數上,jvm可以做到就是整個都可以看到。

剩下就是在寫代碼的時候,比如說一個選擇,舉個上週的例子,我們在做這種base64的一個轉碼的過程當中,可能一些特殊需求需要轉,那現在有這麼多工具類,到底用哪個工具的性能更好呢?通常做法是寫一個單元測試來跑一下、測一下,但是這種單元測試很不可靠,因為這種單元測試跟最終你去jvm運行跑代碼其實區別還是很大,它會沒有去做編譯優化,有很多問題。

java官方也是推薦直接基準測試,用基準測試來測它到底性能怎麼樣,但是這種基準測試寫的話,最好去把官方的demo讀一遍,它裡面其實有很多坑,比如說很容易寫一個for循環在裡面,for循環還很容易被優化掉。還有就是解決測試的時候,其實最後值並不返回,它去跑的時候就會把優化掉,系統去跑一塊代碼,就認為最後這個值根本沒有用到,沒用到優化就全部蓋掉,全部蓋掉之後,跑出來你會發現系統數據很好,但是實際並不是這個樣子。所以這塊有很多坑,寫它之前最好去把官方的demo通讀一遍,會避免很多東西,它的demo也比較詳盡,避免很多錯誤的一個寫法。我附了一個日常的demo,這就是上週我們跑64的一個demo寫的。比如剛才的坑,說明這樣去運行的時候,沒有返回就不會處理,它其實提供一種方式上處理,就是你把它消費掉,這塊的話會跑一個性能數據出來,最終結果會是這樣的一個情況。這樣話就會看到那到底選擇哪一個比較好,比如說我們每秒的調動次數,那可能下來發現確實JDK8自帶的這個性能比較好一點。

億級網關 janus 性能優化與 JVM 調優實踐

日常jmh的demo

高併發下的gc優化與排查

下面聊到這個gc優化與排查。其實對網關來說很難容忍這個gc問題,特別是高併發的情況下,比如說在上次做促銷的時候,流量很大的情況下,由於網關發生gc導致整個超時了是很大一個問題,所以我們在gc上儘量控制頻率,大概現在的情況下是我們能控制到一個月一次cms gc,但是要想網關流量很大,每天都有流量過來,這種大促、更別說這種搶購之類導致的一些流量。所以我們在這塊做了很多優化,整個控制它的一個頻率,後面會講到。

然後的話是我們一些常見的踩坑的問題,比如說我們會遇到熔斷、指標統計上的一個踩坑。比如說熔斷分方法的和服務器的,大家都知道壟斷要寫這個計數器,我們可能要統計容納多少秒內失敗次數等於多少,比如說我定義是十秒內失敗次數——請求數(就有幾個維度)請求十次,十秒內連續失敗,或者按幾個維度來,失敗率是60%,我就認為這個服務不好,應該熔斷。

這就會導致我們會去統計很多這種指標,就會帶來一個問題,我們的統計量太大。比如說我們當時是Api網關,是一個一個Api沒有什麼問題,但我們現在反向代理就會帶來這個問題,因為反向代理涉及一個通配,後面的這個方法很多,所以這個帶來問題就是熔斷上統計會導致我們內存爆掉。指標統計容量上,也是反向代理的時候也會遇到一個問題,因為我們請求的url是很多的,無數的url過來,我們去統計的話,最後遇到這個問題,我們通常會統計1秒鐘、10秒鐘這種各種的一個指標,或者說1分鐘5分鐘,這種都會帶來一個問題。

包括這塊順帶再說一下kafka,我們用到kafka這個坑是跳不過去的,原生代碼裡面自帶的採樣會統計1分鐘5分鐘15分鐘,但是我們網關流量很大的情況下,根據jvm的一個原理,肯定首先對象進入這個年輕帶,在年輕帶裡面倒騰幾次,就老年代。流量很大,我們這塊這gc就會特別頻繁,它就很快進入老年代,就會帶來這個問題。相當於它的統計指標全部在老年代堆積。堆積之後,關鍵是它採樣採樣指標是1分鐘5分鐘15分鐘,old區直線增長,就會觸發我們的cms gc,這塊我們最後的一個就直接把這塊代碼幹掉,因為我們沒有辦法統計資料,就不用統計指標,我們不想它這個給我們帶來的問題。

億級網關 janus 性能優化與 JVM 調優實踐

億級網關 janus 性能優化與 JVM 調優實踐

這塊也是old區的一個問題,就是說我們當時之前是15天左右發生一次,上線之後發現每天穩定增長400兆,就去排查這個問題,反查發現kafka的一個問題,之後發現數據結構裡面有個跳錶,我們反查了一下代碼發現只有kafka在用,然後這塊就跟蹤到kafka,因為已經找到這個到底是誰關聯它,這個反查到了之後發現是kafka的問題。我們做了一個空實驗之後,發現這個問題就解決掉了,就回到我們原先那個頻率上。

說到這個日誌,我們剛才說的日誌其實是權益部的一個輸出日誌,這塊我們是按log4j2的一個方式處理,就是二點幾的版本,但我們在用的時候2.6還沒出來,但是後面2.6出來是很推薦大家試一下,因為這塊還做了什麼gc優化,它號稱是減少了很多gc問題,我們測下來也是基本上gc耗時也會有明顯的一個降低。

億級網關 janus 性能優化與 JVM 調優實踐

這一塊的話是說我們當時上線的時候,其實沒有設cms gc到底什麼時候觸發,比如說都是區,可能通常大家會設置到75%就觸發,然後不設置,我們覺得這塊讓jvm去做可能更好一點。我們不知道什麼時候觸發是合理值,既然我們不知道就讓jvm自己來做動態調整。但發現一個問題,就是現唉網上的資料都說,包括我們去讀這個幾本jvm的書,他們也都會說,這個JDK你不設的話,它默認是68%觸發這個cms,我們線上上線之後發現並不是,我們92%才觸發,我們就看了一下我們jdk的一個版本,我們版本是7,就拿了一個open JDK的原代碼算了一下,算出來確實是92%出發。

這塊舉這個例子意思是看到這種問題,其實通過代碼是很容易找的,我們直接可以將JDK版本拉下來之後去找,並不需要去相信查各種資料,發現沒有,到底是多少觸發?這塊代碼裡面很明顯,其實也不復雜,雖然說C++這種代碼,其實我們只是去找一些這種配置的話,很容易快速識別的。代碼裡面是沒有秘密的。

還有一個問題就是高IO導致jvm停頓的問題,這種測試會發現一個問題,在連續壓測大概四五個小時,就會發現我們的有些四個9的數據可能會出現有一秒增加,我們大部分時間都是在一毫秒的一個範圍,但是發現有四個9會有一種一秒的特別長的一個時間,覺得這可能是一個問題,我們線上會這樣是不太能接受的。我們線上基本上時間都是經過網絡的調動後端返回這個時間,請求發起進來出去的時間大概都在一兩毫秒,所以覺得這塊我們是不能接受這一點。然後就去排查問題,就發現高IO導致jvm的停頓這樣一個問題。

一個是把gc日誌並不直接放磁盤上,放內存文件系統上必須加上這個參數,因為jvm在運行,它會輸出、關聯操作好幾個文件,有一個文件是用一些jvm的命令做一些分析用的。

通用計數器實現方案

後面我們就舉例子來講做網關的時候,因為做這種對質量要求比較高一點。我們這個承載的是一線流量,其實影響是非常大的,對性能也是要求很高,而且又涉及到改動很頻繁。各種業務方可能有些需求點要去為他們做一些定製化的一些開發。當然可以用插件化來做,但是這塊其實有很多改動,所以就舉一些場景,到底應該怎麼做?

比如讓你來寫網關的代碼你應該考慮哪些點?我們通常做一些抽象,會用到一些通用技計數器,我們以這個場景來講,要去做一些考慮。這也是拿我們平時遇到一個點來發散的講,就說網關到底哪些問題?

我們通常會用一個通用計數器這種東西,因為我們會做很多計數,比如限流、後端的服務:我們要保護後端服務我們來做限流;再比如說我們要做防刷,我們可能要根據ip或者根據session各個維度來做防刷,做成保護安全上的一個東西;或者有一些技術上的一些需求,比如說是熔斷,它其實也是一個技術上的需求,這種技術怎麼來做的?

想比較簡單,就Java自帶的這個原子類的這種cas,我就直接上面加就可以。其實也會有問題,原子類它的維度比較單一。來看這樣一個實現,它這裡面就有一個計數的概念,還有一個時間的概念,這兩個概念我們就需要用時輪的方式來實現。那時間輪的方式就會帶來一個問題,用時間輪要有一個清晰的概念在裡面,包括時間輪怎麼快速去查找(比如說你看時間輪列表的話,性能上一個問題),包括考慮用時間輪怎麼去避免產生GC或者一系列的問題。

我們這塊要實現一個滑動窗口的一個時間輪。那首先第一個就是選擇,是提前清理的話,就使用時清理。我們的最後選擇是使用時清理。

這樣一個統計,實現上首先用數組去實現,做對應上的一個映射數組裡面的對象,但是這就涉及到我們要怎麼去清理它?我們並不是直接加一把鎖去清理,每次去鎖住,它每個線程都會遇到這個鎖,性能會降很低。所以我們是用一個自旋鎖的方式,因為我們其實是不停的請求進來,我們去自旋一下就可以了,這塊時間的話性能是比較好的。

然後這塊還有一個問題,拿JDK8在後面供應對象是明顯的比前面這個cas一個原生對象性能高很多,但為什麼不用後面這個?這就是一個內存上的問題。比如說前面用度量工具測,測完之後發現前面完全滿足我們的需求,後面的話確實性能會好,但是後面的內存又會高很多。大概在60萬的對象,後面是45兆,前面是一點幾兆,就這種內存上的一個對比,在內存上我們接受不了這個。

包括這種對象,用完之後要清理掉,對象清理掉之後,它就會帶來gc問題,這個對象已經在新生代裡面倒騰了幾次,因為流量很大,容量很大的時候,就不停的ygc。ygc幾次就進老年帶,那又會帶來老年代這個對象增加,清理掉,解除這個關聯關係之後,老一代又會直線增加,我們想控制1月一次的cms這個目標就很難達成了。所以這塊我們的一個需求就有很多點需要考慮,有相關的一些點考慮到之後,我們還要把它做成一個抽象工具,要用的話直接調就可以做,這些考慮點全部在裡面,而並不是說我們每個人去實現一套。

一個有意思的gc場景

再說一個非常有意思的一個gc場景,其實我們有些gc場景會用到這種lru的方式去統計,因為不夠用。比如說我們要統計一些東西,我們只能用最近最常使用的一個算法去做,但是這就會帶來幾個問題。我們如果把它設置很大,準確性倒是挺高的,準確性提高了之後,gc問題就來了,如果設小了,但是又準確性不夠,那我多小合適了,這個需要根據流量來。

這個問題呢可以進一步抽象,其實會帶來很多問題,這個問題進一步抽象到說所有能熬過ygc到了老年帶,並且把它反覆創建問題解除,其實都會有這個老年帶增長的一個問題。

像廣泛的一些日常使用,在開發日常代碼的時候肯定會涉及到連接池,這是一個比較常見的例子。你的連接池會設有一個共享連接,幾次ygc後也會進入老年帶。會有配置一些參數去把這些共享連接給關掉動態變化數量,檢查閒置連接之類的會清理掉,這個對象就在老年帶裡待著了。但是在老年帶裡的連接數發現不夠又會去創建它,創建之後就會隨著流量不停的在老年帶裡面創建對象,就會帶來這個問題。

唯品會AR系統介紹

最後一點,我們寫了這麼多代碼之後,發現一些規律性的東西,儘可能複用對象、儘快釋放對象。 如我們在創建一個對象的話,能複用儘量複用,比如說我們用了一些計數器,移除的時候,其實放在一個隊列上,要創建什麼首先從隊列上去取。隊列沒有,我們再用一個減少這種垃圾對象地方創建。

netty還是很有參考價值的,如果把源碼過一遍的話,可能會有很多想象不到的點,會對你有些價值。

儘量狀態無鎖,如果一定要有狀態,先走線程內共享,就是模塊內的共享,之後如果發現還是解決不了問題,那沒辦法,只能cas,儘量選擇小的一把鎖,怎麼小怎麼來,而並不是加一把大的讀寫鎖或者怎麼樣,在這可能做一個循環、一個自選鎖來做。

最後一點,這裡比較重要,在寫作代碼,因為網關的代碼這塊入口流量如果有問題會影響很嚴重。當然我們通過一些發佈的方式可以避免,但這塊儘量去了解你寫的每一行代碼背後的邏輯。內部到底是怎麼運行,再用這種基準測試去度量,度量你寫的代碼到底有多少的性能損耗,到底是怎樣的。

我的這塊分享就完了,謝謝大家!


分享到:


相關文章: