1. 基于B/S架構的HTTP協議三級緩存設計
1.1. 騷話連篇
程序設計無非就是時間問題和空間問題,但是現在本質已經變了現在的軟件工程師已經不在乎軟件質量以及代碼質量,以增加系統復雜度的來解決并發問題,軟件變成變成了PPT編程。
其實大家都知道很多緩存方案 Redis讀寫緩存呀,MongoDB文檔緩存呀,網頁靜態化呀,,其實玩來玩去也就那樣子這么多年了也沒有什么創新的點子出現。既然大家都知道緩存方案為啥我還要專門寫一篇文章去解釋這個緩存方案設計哪,主要原因就是因為我沒有找到這個方案的輪子,我是真的沒有輪子,Spring和其他Java社區都沒有提供。
難道服務端就不能去控制緩存嘛?我在思考這個問題,服務端控制代理服務器和瀏覽器的緩存,并且做好緩存協商和緩存過期,利用好http緩存可以極大的減小服務端壓力,無論是網絡傳輸還是servlet容器對http的解析。
秉承著百度和Google都不能解決解決你的問題甚至沒有答案,那么恭喜你了,你要自己去解決問題并且發布解決方案了,所以我要造輪子以及寫博客了。
1.2. 設計初衷
HTTP協議作為B/S架構最通用的協議,但是大家都沒有很好的利用HTTP協議。(中國開發工程師就那點水平吧,紙上談兵之士,我也是紙上談兵,哈哈哈)
提升網站并發首先就是要縮短距離和減小體積以加快響應(我稱之為“短小快”)
HTTP協議的緩存策略可以縮短網頁請求資源的距離,減少延遲,節省網絡流量,并且由于緩存文件可以重復利用,降低網絡負荷,加快客戶端響應。
其實一個高并發網站的流量95%都是讀流量,寫流量甚至都達不到5%,Java工程師引用Redis將大量數據放入Redis來解決讀流量其實是錯誤甚至是致命的,在重后端輕前端的軟件開發環境下大家都是優先從后端入手去解決讀流量很少從網絡傳輸協議入手去解決問題。運維工程師對于HTT議緩存也僅僅是Nginx對靜態資源設置一個Cache-Control: max-age=31536000。
其實運維工程師也很難判斷緩存什么時候過期,過期時間設置多大,畢竟誰知道后端工程師什么時候更新數據吶。(這里就沒有前端什么事情了,瀏覽器都已經維護好HTTP緩存了)
1.3. 解決思路
哼,感覺Spring對HTTP協議支持太少了(有點郁悶),以至于我覺得他們是不是故意不開源部分代碼用于商業支持。
以后端方式來控制HTTP緩存(當然不是后端返回數據加一個Cache-Control: max-age=31536000),后端來決定HTTP緩存的時間,其中涉及到 HTTP協議緩存失效機制、反向代理緩存、后端緩存控制。
HTTP協議大家很熟悉吧,廢話少說寫文章打字很累,直接上鏈接吧
https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
https://web.dev/i18n/en/http-cache/
這兩篇文章看完大概心里對HTTP緩存就心里有數了,其目的是盡可能的把數據緩存在瀏覽器中減少網絡請求(用戶的電腦性能不會那么拉胯的啦)。
畫個時序圖來看看吧
點擊放大
通過流程圖就可以發現只要ETag一直有效,緩存協商返回304充分利用HTTP緩存就可實現, 至于如何高效的維護ETag的過期這里放到第三章手撕框架說吧
2. 短小精悍
短小精悍對標膀大腰圓,大家都是玩編程的靠提升系統復雜度來體現自己的技術的人真的沒有勁(PPT編程那群人啦)。
有點不想寫了,有點累了,邊抽煙邊寫這些東西很傷身體,寫出來有沒有人用都不知道。
睡了一夜,起床喝了一杯速溶咖啡接著碼字吧。
說一個微服務小故事臥槽!Nginx崩了。臥槽!系統崩了。領導:在這樣子搞下去不行呀,臉上掛不住呀,把系統拆成微服務吧,整點高大上的東西進去應該就不會崩潰了吧
一個系統拆成微服務中。改造中...注冊中心來一發,PRC來一發,gateway來一發。。。臥槽!系統還是老樣子崩潰了。
運維仔憋出大招喊出了:Nginx雙活來一發,Spring Cloud Gateway集群搞起來,給我活,活起來。。。運維仔:臥槽!版本升級好麻煩呀,來一發docker順帶一發k8s。后端仔:臥槽!配置不能同步呀,來一發配置中心吧。后端仔:臥槽!這個接口涉及到多個服務,來一發seata吧。后端仔:臥槽!!!出bug了看日志好麻煩呀 整一發ELK吧。
客戶:臥槽!!!升級微服務了好牛逼plus呀。臥槽!!!微服務化了穩定上上來了,這性能怎么這么拉胯呀。后端仔:我給你來一發zipkin做性能追蹤看看。
懶得寫了,故事就是這樣子啦,現在的性能優化就是做加法而不是做減法
一套組合拳下來,發現什么都沒有解決甚至加速熵增,軟件開發真有趣
2.1. 網關之短
來看看微服務最流行的網關設計吧
網關設計
其實LB在中小企業很少會用到, 基本上都是傳統網關模式,進階版本看到這種部署方式的就更少了。
一個請求在進入實際的微服務之前需要經過兩層網關,Nginx作為流量網關,Spring Cloud Gateway作為業務網關,經過兩層網關之后性能網絡延遲會增加5%-10%,這些損耗還沒有加上一些人會往Spring Cloud Gateway之中增加一些奇特的功能(報文校驗,報文加解密,token校驗。。),以至于Spring Cloud Gateway的性能下降的更厲害以至于要擴充Spring Cloud Gateway集群。
心中其實一直有一個疑問,有Spring Cloud Gateway還需要Nginx嘛,雖然Java的性能比不上C但是也沒有必要用多層網關設計吧。就算上了LB,阿里云那邊其實也是一臺Nginx在哪里跑著,進階版本和集群版本其實沒有多大差別。
19年的時候就想著Spring Cloud Gateway的性能比不上Nginx,Java天生劣勢,但是Nginx又沒有微服務生態,經過一段時間的苦思冥想后已經詢問眾多好友后,給我了一個OpenResty讓我去玩,但是OpenResty并沒有接入Spring Cloud Alibaba體系之中,而且OpenResty需要寫Lua,我不會Lua呀,最終22年的時候在和朋友瞎扯淡后拿到了 Apache APISIX。
Apache APISIX官網:
https://apisix.apache.org/docs/apisix/getting-started/
性能對比:
https://baijiahao.baidu.com/s?id=1673615010327758104&wfr=spider&for=pc
經過一段時間研究Apache APISIX后,發現Apache APISIX 可以完美替代 Spring Cloud Gateway和Nginx這種多層網關模式。
Apache APISIX基于Nginx 無論如何封裝 性能上都可吊打Spring Cloud Gateway。
列舉一下目前用到的幾個有趣的特性, 更多特性請看官網
動態routes
動態Upstream
服務發現
那么網關升級一下吧。
流量網關+業務網關的模式再也不存在了,現在只有一層流量網關,因為消除了Spring Cloud Gateway的存在 性能上略有提升,性能上面大概會提升2%-8%吧。
盡量不要讓網關 去和業務扯上關系,讓網關做好反向代理和負載均衡將可 (Spring Cloud Gateway上面一大堆業務,能不慢嘛)
別小看這種小小的性能提升,失之毫厘差之千里,流量上來了效果就不一樣了。
2.2. 數據之小
上面已經把網關的距離縮短了,如何讓數據變得小一點。
其實從架構較多來說,JSON和XML這些傳統的通訊格式很通用,換成其他的Kryo,dubbo這些之類的也只是適合服務和服務之間傳遞,并不適合前后端之間交互,就是JSON解析框架從Jackson換成了FastJSON(溫少出了FastJSON2)也只是從在解析速度上的提升
后前端其實沒有多少可以說的,HTTP協議很通用,JSON通訊格式也方便解析稍微主意一下幾個問題就好了
后端不需要把null值給前端,前端仔自行處理undefined,null,NaN的問題
后端仔別一股腦 select * 返回全部數據了,和前端確定好字段后定制化的返回所需要的字段
JSON的key其實壓縮一下,apple可以用a替代,user這個可以用u來替代,@JsonProperty這些注解之類的壓縮一下key
數據庫字段盡可能使用小字段,少用寬表
微服務,微服務嘛看PRC框架了,Spring Cloud Open Feign或者dubbo這些都有自己的壓縮算法的
Spring Boot其實有gzip的開關,可以考慮開起來壓縮一下response body,但是開啟后CPU會上來,沒有辦法時間和空間問題。
TCP協議有三次握手能快到那里吶,UDP可靠性問題也是一個很大的問題,其實有打算使用http2.0的,但是http2.0似乎只是提升了client的性能沒有提升server性能, 還不如用http緩存方案來解決這個問題好一點。
http緩存上面介紹過了,各個節點之間的緩存做好緩存和緩存協商接本上就能解決這些個問題了。
呃呃呃, 上面都是沒有營養的東西,寫偏題了(理科生啦沒有辦法就是文筆差勁)
來看看如何讓數據在變的小一點吧
那么網關升級一下吧。
oss是干啥的存儲文件的,前端工程都是靜態文件,把它扔到oss上面讓流量都走oss哪里好了呀。
剝離了前端工程靜態文件的流量,你就發現一件奇特的事情,按照一個前端工程流量5M來計算,網關的壓力又降低了呀。
2.3. 架構之精
每單你引入一個中間件,在與中間件交互的時候必定會帶來性能損耗
其實不難發現現在的Java架構生態,架構師都是往大了做了引入各種花哨的框架進來。
商品庫數量都沒有達到百萬級別想著上ES來提升一下商品檢索,就20臺服務器就想著上docker和k8s,為了防止服務崩潰而防止服務奔潰。
不談金錢預算的架構師,架構的東西真的合理嘛。
架構不往大的做就體現不了自己的技術,更有甚者拿公司成本試錯,加入一些自己不了解的技術已方便自己吹得高大上。
就按老東家cnool說吧(寧波人叫他東方熱線),領導叫我上微服務,我是瘋了么(是的我當時為了工作的確上了微服務哈哈哈),Nginx,gateway,服務,數據庫雙備等等,10臺服務器能干啥呀,20多個微服務都不夠分的。
架構真正的精髓在于如何設計出一個適合業務場景下最優的方案(最討厭面試的時候讓我去哈牛逼的面試官了,完全就不知道他的業務場景是什么,上來就說高并發如何設計那種。。。)
架構的精髓在于如何用最小的技術成本做出實現困難的東西,憑什么說只有上了Elasticsearch才能加速商品檢索,難道dbms就不能實現搜索引擎了嘛,當年Google都已經用dbsm實踐出了dbms可以作為搜索引擎的的可能性(人家當年都上線用了好幾年吶,只是數據量到了PB級別撐不住了),簡單的技術+數據結構即可實現。
電商系統開發呵呵,并發上來就阻塞了撐不住了,后端仔也許可以做一次精神小妹呀,來來下面來玩一下
GET?github.com/goods/info 這個接口很常見吧,獲取商品詳情,網站一半以上的流量都是這個接口在作妖,下面來看看如何優化啦。 goods/info就是獲取商品詳情嘛,里面的數據除了商品數量,其他的數據一年半載基本上都是不會變化的,所以?數據動靜分離一下 第一步吶:我們保留原有接口,但是 goods/info 這個加緩存 GET?github.com/goods/info 第二步吶:增加一個接口,這個接口獲取商品數量,加不加緩存都可以,加了緩存就做好緩存協商 GET?github.com/goods/quantity 搞定,這樣子info接口都是走緩存了,都是大字段現在都已經放在了緩存中基本上都不需要走服務端了,瀏覽器緩存和網關緩存基本上就攔截住了。 quantity接口因為變化速度太快了,也不知道什么時候變化,但是都是小字段,傳輸和數據庫檢索基本上沒有什么壓力。 對了,數據庫也可以這樣子拆分一下,?goods表弄成兩張,加一張quantity表和goods表做關聯,完事了世界就是你的了
當然上面是對API進行優化當然還有一個大殺器,那就是網頁靜態化,這個沒有專門研究過,針對app和小程序無效只能用在web網頁上面。
2.4. 性能之悍
物理計算機性能如何強悍,其實沒有沒有以上的短小精悍都不可能做到真正的強悍的。為什么想要強悍在設計及編碼之時必須摳門,是的摳門。
做過一個很有趣的功能,Java對300M的csv文件進行關鍵字檢索,開發環境硬件是 MacMini2018頂配,生產環境是 2C8G的服務器,開發環境各種測試都沒有問題,但是到了生成環境性能未達標,差了20%。
硬盤IO,CPU頻率,內存帶寬等等因素造成性能未達標,在無法改變硬件的情況下,只能在low逼硬件上面進行設計,經過多天的設計終于彌補那20%,可以在low逼硬件上面性能達標。把數據和程序放在開發環境上面運行性能居然提升了50%。
用最拉胯的硬件寫出高性能的軟件,這才是性能強悍。
2.5. 短板效應
軟件開發的性能問題永遠是短板效應,IO阻塞,cpu線程競爭,網卡帶寬等等,性能取決于最low的那個,但是為什么現在硬件如此強度的情況下為什么還是不能提升性能吶,這是一個很有趣的問題。
短板效應漸漸的不是出現在硬件上面了,而是代碼,短板效應居然是代碼,其次是架構。再優秀的架構都抵不垃圾代碼帶來的性能損耗。
不注重代碼管理的何須談性能吶,
3. 手撕http緩存框架
感覺說沒有意思,框架請自取,目前沒有發布到中央倉庫,應該會等到22年6月7日發布吧
項目源碼:
https://github.com/galaxy-sea/heifer/tree/main/heifer/heifer-common/heifer-common-http
http-example:
https://github.com/galaxy-sea/heifer/tree/main/heifer/heifer-examples/heifer-common-http-example
在線預覽,開啟F12, 注意看
@GetMapping("cache/{id}") @HttpCacheControl(key?=?"#id",?maxAge?=?10) public?String?getCache(@PathVariable?String?id)?{ ????return?ResponseEntity.ok() ?????????????????????????.body(?new?SimpleDateFormat("yyyy-MM-dd?HHss").format(new?Date()) ?????????????????????????}}) ????????????; }
@HttpCacheControl 會在 Response Header 上返回 Cache-Control: max-age=10 和 Etag: "403710060904730625"
@PostMapping("cache/{id}") @HttpETag(key?=?"#id") public?ResponseEntity
@HttpETag主要用于刷新Etag標簽
呃呃呃,懶得寫了,要去修復http模塊的bug了,還有好多功能和配置沒有加上去吶。
考慮要不要把模塊獨立出來,作為一個單獨的項目發展吶
編輯:黃飛
?
評論
查看更多