【實拍】能賣得過凱美瑞邁騰?20萬級最熱門中型車到了!

而車尾造型簡潔,還帶有小鴨尾式的隆起。比較讓人費解的是它那雙C字型尾燈,雖然個性,但放在一款大尺寸的B級車身上顯得小氣,與簡潔明了的頭燈也沒太大呼應。內裝部分,它的設計沒有我們想象中那麼動感,造型四平八穩,反倒是具有幾分商務氣息。

近日,虎哥收到一則重磅消息,那就是在廣州地區的一家廣汽本田4S店已經有全新一代雅閣到店!聽到這一則消息的虎哥馬上放開手中的糯米雞,帶上小夥伴飛奔到這家廣汽本田4S店!

視頻看完還沒過癮?咱們繼續往下看。

其實雅閣對於國內朋友來說是一款相當熟悉的車型,而對於廣東地區的朋友來說,它更加是屬於具有特殊情懷的車型!早在1999年3月份,第6代雅閣就正式在廣州實現國產,作為廣汽本田成立后推出的第一款車型它被寄予厚望。

而截止到2002年第6代車型停產,它銷量一直很火爆,耐用、駕駛舒適成為它的最大標籤!在1999年-2002年間它銷量累計達到了13.8萬

輛!

緊接着推出的7代雅閣也一度造成一車難求的現象,也是經過兩代車型的努力,雅閣車型在家用中型車市場確立了標杆地位。

而8代雅閣以前衛、富有動感的外觀,實用的大空間亦獲得優秀的市場表現。

然而,從9代雅閣開始,“鍍鉻狂魔式”的前臉造型,和略顯臃腫的車身線條讓雅閣車型的油膩指數直線飆升!也是在這一代開始人們更多的認為它其實是一輛大叔座駕。

當然,廣汽本田也意識到這代車型設計有些用力過猛,所以在後來改款的9.5代設計向運動感回歸,銷量得以上升!

時至今日,第10代雅閣也即將來到我們身邊,“油膩”這個標籤也徹底與它脫離了關係!而關於它的具體細節我們就來細細分析。

10代雅閣的前臉營造出強烈的視覺衝擊力,看實車的時候這種感覺尤為明顯。中網上粗壯的一條鍍鉻飾條立體感強,而頭燈內部一字排開的LED燈組,十分不低調!

(注:本文圖片拍攝場地為廣汽本田第一店)

而設計師為側面造型畫下了濃墨重彩的一筆,Coupe式的車身設計,具有溜背式的車尾造型,車頂弧線平滑。而A柱的位置比上一代車型延後了100mm,發動機艙更修長、造型也被壓得更低,運動感已相當明顯。

輪轂的造型比較動感,但從目前的信息來看,雅閣的1.5T頂配車型也只配備17英寸的輪圈,它放在10代雅閣身上只是顯得剛剛夠用。想營造出讓人熱血噴張的運動感,它遠遠不行。或者廠家是出於對減低油耗的考慮,才限制了輪圈規格。畢竟它採用的是米其林primacy 3st浩悅系列 225/50 R17 規格的輪胎,它是一款注重降低油耗同時具備一定的操控性的輪胎型號。

尾部帶有真雙出排氣,這點是比較厚道的。而車尾造型簡潔,還帶有小鴨尾式的隆起。比較讓人費解的是它那雙C字型尾燈,雖然個性,但放在一款大尺寸的B級車身上顯得小氣,與簡潔明了的頭燈也沒太大呼應。

內裝部分,它的設計沒有我們想象中那麼動感,造型四平八穩,反倒是具有幾分商務氣息。

而新車型的內飾在細節處理方面有了較明顯進步。它加入了深色的木紋飾板和金屬拉絲材質進行點綴,比以前採用的鋼琴烤漆更顯檔次感。而且做工水準也更符合B級車定位。

儀錶盤左側設置了液晶显示屏,它能显示的多種信息,實用性不錯。值得一提的是,中控台上獨立式的8英寸多媒體觸控屏操作流暢,系統反應很快,而且显示界面也簡潔而細膩!比上一代車型好用不少,而兩側設置有實體按鍵,在開車過程中盲操作也便利,這提高了駕駛安全性。

而中控屏支撐Honda CONNECT功能,這是一套本田開發的智能互聯繫統。頂配車型帶有前排座椅通風、加熱功能,雙區自動空調還帶有空氣凈化功能,而詳細的配置信息會在後期公布。

這個內飾,並不能給人驚艷感,但它實用性很不錯,我想用一個“功能產生美”來形容它最貼切!

本田的乘坐空間最大化理念在新車型上依然有很好的體現,它溜背式的尾部設計沒有對後排頭部空間帶來過多影響。而對於身高177cm的虎哥來說,它營造出接近3拳的腿部空間,翹個二郎腿很合適。

而座椅柔軟程度和日產天籟有得一拼,坐墊長度也足夠,對於大腿的承托到位,中央扶手的長度和高度也到位,舒適性高!

只是稍有遺憾的是後排乘客能享受到的配置基本上只有後排出風口和中央扶手上帶有的兩個杯架。

關於新車預定:

銷售人員表示,第10代雅閣現在已經接受預定,喜歡它的朋友可以多加留意,因為車型還沒上市,所以提車時間現在還不能確定。

來到4S店看全新雅閣的人真的很多,可見它對於消費者吸引力還是比較大,而與這些潛在客戶交談時,他們也表示新車型內飾實用性強、做工有了進步!

寫在最後:第10代雅閣即將到來,它的造型無疑有了脫胎換骨般的變化,其造型確實富有運動感、衝擊力!而內飾的質感得到進步,但是整體水準仍然只屬於B級車應該有的水平,乘坐空間表現出色,座椅很舒適!1.5T+CVT的動力總成雖值得期待,但2.0T+10AT車型的缺失確實有些遺憾,不過作為主力銷售的1.5T車型到底具備多大的競爭力,那就要等待後期它的配置和價格信息公布!

特別鳴謝以下經銷商提供拍攝車輛:廣汽本田第一店;

電話:020-36312608;地址:廣東省廣州市白雲區黃石東路448號。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

12萬買1.6T合資緊湊型SUV?看完恍然大悟!

7-2。9米C級=中大型車長度4。8米-5米,軸距2。8-3米,排量超過2。4LD級=大型車長度超過5米,軸距超過3米,排量超過3L呃,根據這個分類方法,不同的車型基本都可以找到各自對應的分類,而且通過看車型的分類級別,就能一目瞭然找到自己要的大小的車子。

昨晚,現代ENCINO上市

除了分體式大燈以及標配的1.6T發動機十分好看之外

還注意到一個有意思的地方

有媒體竟然叫它緊湊型SUV!

緊湊型SUV?

就是和CR-V、RAV4榮放一個級別?

只要12萬而且還標配1.6T?

這也太划算了吧

然而,事情沒那麼簡單,這貨的尺寸為4195*1800*1575mm,軸距為2600mm

長度和軸距甚至還不如本田XR-V這種小型SUV

比起中國車企的東南DX3、傳祺GS3等小型SUV也差了不少

這樣的尺寸也敢叫緊湊型SUV,是梁靜茹給它的勇氣嗎?

吐槽的同時

也必須弄清一個概念

汽車級別怎麼劃分的?

如今多數媒體車企給車型劃分分類的標準多是歐洲標準

也就是大眾汽車的分級辦法,綜合排量、車型大小等因素分為

A00級、A0級、A級、B級、C級、D級。

A00級=微型車

長度4米內,軸距2-2.3米

A0級=小型車

長度4-4.3米,軸距2.3-2.5米

A級=緊湊型車

長度4.2-4.6米,軸距2.5-2.7米

B級=中型車

長度4.5-4.9米,軸距2.7-2.9米

C級=中大型車

長度4.8米-5米,軸距2.8-3米,排量超過2.4L

D級=大型車

長度超過5米,軸距超過3米,排量超過3L

呃,根據這個分類方法,不同的車型基本都可以找到各自對應的分類,而且通過看車型的分類級別,就能一目瞭然找到自己要的大小的車子。因此這一套分類方法十分流行。

然而,這套方法的分級卻經常被車企混用,比如這一次,尺寸4195*1800*1575mm,軸距為2600mm的ENCINO也敢叫自己緊湊型SUV。

除了ENCINO之外,還有不少這類型的例子:

咱們熟悉的緊湊型轎車科魯茲,在官網pDF上把自己叫做“新銳性能中級車”,這,莫非科魯茲是中型轎車?

非也,中型車與中級車一字之差,但是差距可不是一星半點,前文中說到中型車是歐洲分類標準,而中級車則是咱們中國的標準了,依照中國汽車分類標準(GB9417-89)的分級方法,中級車屬於排量1.6-2.5L的車型,因此依照這個標準來看,科魯茲還確實是中級車,而且大多數緊湊型車也確實可以叫自己中級車,不過在咱們大多數人的理解中,中級車=中型車啊!因此科魯茲也確實有鑽這個空子的嫌疑。

為了產品賣得好一點,吹出一點牛皮也是合情合理的,不過相比上面兩款車型的手法,下面這些才是真大佬!

奔馳S級:再次發明汽車

奔馳S級在上市之初打出了許多十分誇張的口號,比如:“汽車發明者,再次發明汽車”“再見愛迪生”等等,雖然S級從設計的角度來說確實達到了一個新高度,但是再次發明汽車的口號也有些太狂了。

昂科威:百萬級最好的隔音

昂科威是別克旗下的中型SUV,售價21.99-31.99萬,這個價位的SUV老老實實賣車才是王道,然而昂科威並不安分,在上市之初昂科威便把百萬內最好的隔音作為賣點,要知道不同價位車型之間的差別可是十分大的,昂科威這口號也是夠大膽的,不過經實測,昂科威隔音確實比百萬級的卡宴更好。

君越:圖書館級靜音

同樣宣傳隔音的還有君越,這一次君越使用了圖書館級靜音水準這個詞彙,而根據《圖書館、博物館、美術館、展覽館衛生標準》(GB9669-1996)規定,圖書館的噪聲標準為≤50dB(A),這樣的噪音數值恐怕君越只有怠速工況下能夠達到吧~

攬勝:越野車中的勞斯萊斯

嚴格來說,這個稱號是廣大粉絲送的,不過也是非常霸氣的一個稱號了,除了越野車中的勞斯萊斯之外,路虎還有英國皇室狩獵專用車等頭銜,不過勞斯萊斯的越野車馬上就要上市了…

總結:

汽車廣告與宣傳中往往用到許多誇張的詞彙,越級、澎湃、奢華等詞語的出鏡率十分高,這樣的宣傳往往能讓人印象深刻,不過如果真的太相信這些宣傳詞彙,到頭來往往會讓人失望,汽車說到底也只是普通商品,既然是商品那麼一分錢一分貨這個道理還是適用的,用10萬元買到20萬的品質這種事情往往不會存在的,作為消費者,在看車企宣傳的同時一定要自己辨別,這樣才能避免被騙哦~本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

空間大做工好!這是6萬元區間最好的7座車?

相比較7座SUV來說,歐尚A800的後備箱也小有優勢,主要體現在A800的後備箱高度和進深上,在這兩個參數上A800十分有優勢,超過1米的後備箱高度十分誇張。A800搭載了一台1。5T渦輪增壓發動機,型號為JL476ZQCD,這台全鋁發動機帶有DVVT技術,最大功率156馬力,最大扭矩225牛米,參數並不是很高。

看過了非常適合家用的SUV奇駿、夠大夠霸氣的銳界、大氣實用的奧德賽、精緻好用的途安L之後,你是覺得SUV好還是MpV好呢?有興趣的朋友可以點擊鏈接查看往期文章:

奧德賽:67.9分

途安L:64.6分

銳界:66分

奇駿:65.4分

說起8萬左右的MpV車型,就不得不提歐尚A800了!它最大的特點當然是空間、動力以及配置,這些方面它絲毫不遜於對手寶駿,而且由於這台車還是我們的工作車的原因,長期使用下來我們對它也是非常熟悉,歐尚A800外表雖然不算出色,但是論及內在絕對是一名出色的选手!

在測試中歐尚A800也表現出了強大的實力,無論是在外觀品質、動力表現以及車內空間上都可圈可點。

相比較長安以往的車型,歐尚A800在設計上盡量營造出時尚感與精緻感,從外觀很多細節上都能看到它的設計思路,這樣的造型設計顯然是成功的,A800雖然尺寸龐大,但是看上去卻並不顯臃腫,而且較大的車窗也能夠提供非常不錯的採光。

內飾也是如此,我們這台高配車型中控台非常簡潔,碩大的屏幕與空調操作區的按鈕擺放都很有檔次感,全液晶儀錶盤在這個價格區間的車型里也十分少見,加上內飾的材質比較考究,整體營造的氛圍還是不錯的。

A800的外觀工藝相比較更高價位的車型也毫不遜色,無論是外觀的鈑金縫隙,還是車漆的噴漆均勻度都很不錯,不過車漆的厚度平均不足100微米則有點太薄了。

雖然內飾看上去不錯,但是受限於價格,A800在內飾材質上大面積使用了硬塑料,如果真的談及觸感的話還是顯得有一些廉價,不過好在內飾的拼裝工藝還是不錯的,塑料件也沒有毛刺。

有了龐大的尺寸以及方正的設計,A800的內部空間可以說十分寬裕,無論是前排後排還是第三排空間都可以用寬敞來形容,而且A800的第二排還是採用獨立座椅設計,相比較大多數轎車來說都要更加舒適,不過受限於第三排地板以及空間,第三排的座椅規格比前兩排要小一些,硬度上也更硬一點。

相比較7座SUV來說,歐尚A800的後備箱也小有優勢,主要體現在A800的後備箱高度和進深上,在這兩個參數上A800十分有優勢,超過1米的後備箱高度十分誇張。

A800搭載了一台1.5T渦輪增壓發動機,型號為JL476ZQCD,這台全鋁發動機帶有DVVT技術,最大功率156馬力,最大扭矩225牛米,參數並不是很高。

與之匹配的是6擋手動變速箱,這台變速箱齒比比較綿密,尤其是前兩個擋位可以說是為拉貨設計的,非常大的齒比對於載重來說是一件好事。

不過由於齒比比較綿密,因此在加速上A800就有些吃虧了,2擋僅能跑到70km/h的速度來,再升上3擋之後才能破百,而3擋的加速度就遠不如1/2擋了,因此最終A800的破百成績為12.5秒,這樣的成績對於這台大傢伙來說倒也還算可以。

作為一台MpV車型,A800顯然和運動扯不上關係,對於這類車型來說我們的要求也就是好開,從這個角度考慮A800確實算得上不錯,首先A800的離合點十分清晰,變速箱的換擋手感也不錯!加上發動機的低扭還算不錯,開起來比較得心應手。

不過由於尺寸龐大且車身較高,懸挂也偏軟,因此A800在高速行駛的穩定性上和轎車以及多數SUV比還是不佔優勢,尤其是面對橫風的時候需要更加集中精力駕駛。

雖然加速成績是橫評車型里最慢的,不過在實際動力感受上還是不錯,尤其是低速駕駛的時候會感覺車子很有力,再加上不錯的變速箱,A800是一台很能輕鬆駕馭的手動擋車型。

對於這類型的MpV,其實最讓人擔心的就是隔音了,由於車內空間比較大,車子的迎風面積也大,所以容易在第二/三排產生較大的共鳴聲和風聲,不過在實際體驗中A800這個問題倒也不算嚴重,當然相比較轎車那肯定是差一些了。

在售價上歐尚A800的指導價算是自主入門MpV中比較低的了,性價比還是不錯的。

A800在諸多方面的表現都堪稱出色,優異的配置、不錯的駕駛感受和寬敞的空間都是它的優勢所在,對於這個價位買車的消費者來說這恰恰也是它們最關心的,再加上較低的售價使得這款車有了不錯的性價比,所以在6-9萬的MpV市場中A800確實算得上一個稱心的好選擇!

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

ThreadLocal源碼解析-Java8,ThreadLocal的使用場景分析,利用線性探測法解決hash衝突,Java-強引用、軟引用、弱引用、虛引用,利用線性探測法解決hash衝突,分析ThreadLocal的弱引用與內存泄漏問題-Java8

目錄

一.ThreadLocal介紹

  1.1 ThreadLocal的功能

  1.2 ThreadLocal使用示例

二.源碼分析-ThreadLocal

  2.1 ThreadLocal的類層級關係

  2.2 ThreadLocal的屬性字段

  2.3 創建ThreadLocal對象

  2.4 ThreadLocal-set操作

  2.5 ThreadLocal-get操作

  2.6 ThreadLocal-remove操作

三.ThreadLocalMap類

  3.0 線性探測算法解決hash衝突

  3.1 Entry內部類

  3.2 ThreadLocalMap的常量介紹

  3.3 實例化ThreadLocalMap

  3.4 ThreadLocalMap的set操作

  3.5 清理陳舊Entry和rehash

四.總結 

 

一.介紹ThreadLocal

1.1ThreadLocal的功能

  我們知道,變量從作用域範圍進行分類,可以分為“全局變量”、“局部變量”兩種:

  1.全局變量(global variable),比如類的靜態屬性(加static關鍵字),在類的整個生命周期都有效;

  2.局部變量(local variable),比如在一個方法中定義的變量,作用域只是在當前方法內,方法執行完畢后,變量就銷毀(釋放)了;

  使用全局變量,當多個線程同時修改靜態屬性,就容易出現併發問題,導致臟數據;而局部變量一般來說不會出現併發問題(在方法中開啟多線程併發修改局部變量,仍可能引起併發問題);

  再看ThreadLocal,可以用來保存局部變量,只不過這個“局部”是指“線程”作用域,也就是說,該變量在該線程的整個生命周期中有效。

  關於ThreadLocal的使用場景,可以查看ThreadLocal的使用場景分析。

 

1.2ThreadLocal的使用示例

  ThreadLocal使用非常簡單。

package cn.ganlixin;

import org.junit.Test;

import java.util.Arrays;
import java.util.List;

public class TestThreadLocal {

    private static class Goods {
        public Integer id;
        public List<String> tags;
    }

    @Test
    public void testReference() {
        Goods goods1 = new Goods();
        goods1.id = 10;
        goods1.tags = Arrays.asList("healthy", "cheap");

        ThreadLocal<Goods> threadLocal = new ThreadLocal<>();
        threadLocal.set(goods1);

        Goods goods2 = threadLocal.get();
        System.out.println(goods1); // cn.ganlixin.TestThreadLocal$Goods@1c655221
        System.out.println(goods2); // cn.ganlixin.TestThreadLocal$Goods@1c655221

        goods2.id = 100;
        System.out.println(goods1.id);  // 100
        System.out.println(goods2.id);  // 100

        threadLocal.remove();
        System.out.println(threadLocal.get()); // null
    }

    @Test
    public void test2() {
        // 一個線程中,可以創建多個ThreadLocal對象,多個ThreadLoca對象互不影響
        ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
        ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
        // ThreadLocal存的值默認為null

        System.out.println(threadLocal1.get()); // null

        threadLocal1.set("this is value1");
        threadLocal2.set("this is value2");
        System.out.println(threadLocal1.get()); // this is value1
        System.out.println(threadLocal2.get());  // this is value2

        // 可以重寫initialValue進行設置初始值
        ThreadLocal<String> threadLocal3 = new ThreadLocal<String>() {
            @Override
            protected String initialValue() {
                return "this is initial value";
            }
        };
        System.out.println(threadLocal3.get()); // this is initial value
    }
}

  

二.源碼分析-ThreadLocal

2.1ThreadLocal類層級關係

  

  ThreadLocal類中有一個內部類ThreadLocalMap,這個類特別重要,ThreadLocal的各種操作基本都是圍繞ThreadLocalMap進行的

  對於ThreadLocalMap有來說,它內部定義了一個Entry內部類,有一個table屬性,是一個Entry數組,和HashMap有一些相似的地方,但是ThreadLocalMap和HashMap並沒有什麼關係。

  先大概看一下內存關係圖,不理解也沒關係,看了後面的代碼應該就能理解了:

   

  大概解釋一下,棧中的Thread ref(引用)堆中的Thread對象,Thread對象有一個屬性threadlocals(ThreadLocalMap類型),這個Map中每一項(Entry)的value是ThreadLocal.set()的值,而Map的key則是ThreadLocal對象。

  下面在介紹源碼的時候,會從兩部分進行介紹,先介紹ThreadLocal的常用api,然後再介紹ThreadLocalMap,因為ThreadLocal的api內部其實都是在操作ThreadLocalMap,所以看源碼時一定要知道他們倆之間的關係

 

2.2ThreadLocal的屬性

  ThreadLocal有3個屬性,主要的功能就是生成ThreadLocal的hash值。

// threadLocalHashCode用來表示當前ThreadLocal對象的hashCode,通過計算獲得
private final int threadLocalHashCode = nextHashCode();

// 一個AtomicInteger類型的屬性,功能就是計數,各種操作都是原子性的,在併發時不會出現問題
private static AtomicInteger nextHashCode = new AtomicInteger();

// hash值的增量,不是隨便指定的,被稱為“黃金分割數”,能讓hash結果均衡分佈
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * 通過計算,為當前ThreadLocal對象生成一個HashCode
 */
private static int nextHashCode() {
    // 獲取當前nextHashCode,然後遞增HASH_INCREMENT
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

  

2.3創建ThreadLocal對象

  ThreadLocal類,只有一個無參構造器,如果需要是指默認值,則可以重寫initialValue方法:

public ThreadLocal() {}

/**
 * 初始值默認為null,要設置初始值,只需要設置為方法返回值即可
 *
 * @return ThreadLocal的初始值
 */
protected T initialValue() {
    return null;
}

  需要注意的是initialValue方法並不會在創建ThreadLocal對象的時候設置初始值,而是延遲執行:當ThreadLocal直接調用get時才會觸發initialValue執行(get之前沒有調用set來設置過值),initialValue方法在後面還會介紹。 

 

2.4ThreadLocal-set操作

  下面這段代碼只給出了ThreadLocal的set代碼:

public void set(T value) {
    // 獲取當前線程
    Thread t = Thread.currentThread();

    // 獲取當前線程的ThreadLocalMap屬性,ThreadLocal有一個threadLocals屬性(ThreadLocalMap類型)
    ThreadLocalMap map = getMap(t);

    if (map != null) {
        // 如果當前線程有關聯的ThreadLocalMap對象,則調用ThreadLocalMap的set方法進行設置
        map.set(this, value);
    } else {
        // 創建一個與當前線程關聯的ThreadLocalMap對象,並設置對應的value
        createMap(t, value);
    }
}

/**
 * 獲取線程關聯的ThreadLocalMap對象
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

/**
 * 創建ThreadLocalMap
 * @param t          key為當前線程
 * @param firstValue value為ThreadLocal.set的值
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

  如果想立即了解ThreadLocalMap的set方法,則可點此跳轉!

 

2.5ThreadLocal-get操作

  前面說過“重寫ThreadLocal的initialValue方法來設置ThreadLocal的默認值,並不是在創建ThreadLocal的時候執行的,而是在直接get的時候執行的”,看了下面的代碼,就知道這句話的具體含義了,感覺設計很巧妙:

public T get() {
    // 獲取當前線程
    Thread t = Thread.currentThread();

    // 獲取當前線程對象的threadLocals屬性
    ThreadLocalMap map = getMap(t);

    // 若當前線程對象的threadLocals屬性不為空(map不為空)
    if (map != null) {
        // 當前ThreadLocal對象作為key,獲取ThreadLocalMap中對應的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);

        // 如果找到對應的Entry,則證明該線程的該ThreadLocal有值,返回值即可
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
            return result;
        }
    }

    // 1.當前線程對象的threadLocals屬性為空(map為空)
    // 2.或者map不為空,但是未在map中查詢到以該ThreadLocal對象為key對應的entry
    // 這兩種情況,都會進行設置初始值,並將初始值返回
    return setInitialValue();
}

/**
 * 設置ThreadLocal初始值
 *
 * @return 初始值
 */
private T setInitialValue() {
    // 調用initialValue方法,該方法可以在創建ThreadLocal的時候重寫
    T value = initialValue();
    Thread t = Thread.currentThread();

    // 獲取當前線程的threadLocals屬性(map)
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // threadLocals屬性值不為空,則進行調用ThreadLocalMap的set方法
        map.set(this, value);
    } else {
        // 沒有關聯的threadLocals,則創建ThreadLocalMap,並在map中新增一個Entry
        createMap(t, value);
    }

    // 返回初始值
    return value;
}

/**
 * 初始值默認為null,要設置初始值,只需要設置為方法返回值即可
 * 創建ThreadLocal設置默認值,可以覆蓋initialValue方法,initialValue方法不是在創建ThreadLocal時執行,而是這個時候執行
 *
 * @return ThreadLocal的初始值
 */
protected T initialValue() {
    return null;
}

     

2.6ThreadLocal-remove操作

  一般是在ThreadLocal對象使用完后,調用ThreadLocal的remove方法,在一定程度上,可以避免內存泄露;

 

/**
 * 刪除當前線程中threadLocals屬性(map)中的Entry(以當前ThreadLocal為key的)
 */
public void remove() {
    // 獲取當前線程的threadLocals屬性(ThreadLocalMap)
    ThreadLocalMap m = getMap(Thread.currentThread());

    if (m != null) {
        // 調用ThreadLocalMap的remove方法,刪除map中以當前ThreadLocal為key的entry
        m.remove(this);
    }
}

 

三.ThreadLocalMap內部類

3.0 線性探測算法解決hash衝突

  在介紹ThreadLocalMap的之前,強烈建議先了解一下線性探測算法,這是一種解決Hash衝突的方案,如果不了解這個算法就去看ThreadLocalMap的源碼就會非常吃力,會感到莫名其妙。

  鏈接在此:利用線性探測法解決hash衝突

 

3.1Entry內部類

  ThreadLocalMap是ThreadLocal的內部類,ThreadLocalMap底層使用數組實現,每一個數組的元素都是Entry類型(在ThreadLocalMap中定義的),源碼如下:

/**
 * ThreadLocalMap中存放的元素類型,繼承了弱引用類
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    // key對應的value,注意key是ThreadLocal類型
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

  ThreadLocalMap和HashMap類似,比較一下:

  a:底層都是使用數組實現,數組元素類型都是內部定義,Java8中,HashMap的元素是Node類型(或者TreeNode類型),ThreadLocalMap中的元素類型是Entry類型;

  b.都是通過計算得到一個值,將這個值與數組的長度(容量)進行與操作,確定Entry應該放到哪個位置;

  c.都有初始容量、負載因子,超過擴容閾值將會觸發擴容;但是HashMap的初始容量、負載因子是可以更改的,而ThreadLocalMap的初始容量和負載因子不可修改;

  注意Entry繼承自WeakReference類,在實例化Entry時,將接收的key傳給父類構造器(也就是WeakReference的構造器),WeakReference構造器又將key傳給它的父類構造器(Reference):

// 創建Reference對象,接受一個引用
Reference(T referent) {
    this(referent, null);
}

// 設置引用
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

  關於Java的各種引用,可以參考:Java-強引用、軟引用、弱引用、虛引用

 

3.2ThreadLocalMap的常量介紹

// ThreadLocalMap的初始容量
private static final int INITIAL_CAPACITY = 16;

// ThreadLocalMap底層存數據的數組
private Entry[] table;

// ThreadLocalMap中元素的個數
private int size = 0;

// 擴容閾值,當size達到閾值時會觸發擴容(loadFactor=2/3;newCapacity=2*oldCapacity)
private int threshold; // Default to 0

  

3.3創建ThreadLocalMap對象

  創建ThreadLocalMap,是在第一次調用ThreadLocal的set或者get方法時執行,其中第一次未set值,直接調用get時,就會利用ThreadLocal的初始值來創建ThreadLocalMap。

  ThreadLocalMap內部類的源碼如下:

/**
 * 初始化一個ThreadLocalMap對象(第一次調用ThreadLocal的set方法時創建),傳入ThreadLocal對象和對應的value
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 創建一個Entry數組,容量為16(默認)
    table = new Entry[INITIAL_CAPACITY];

    // 計算新增的元素,應該放到數組的哪個位置,根據ThreadLocal的hash值與初始容量進行"與"操作
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

    // 創建一個Entry,設置key和value,注意Entry中沒有key屬性,key屬性是傳給Entry的父類WeakReference
    table[i] = new Entry(firstKey, firstValue);

    // 初始容量為1
    size = 1;

    // 設置擴容閾值
    setThreshold(INITIAL_CAPACITY);
}

/**
 * 設置擴容閾值,接收容量值,負載因子固定為2/3
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

 

3.4 ThreadLocalMap的set操作

  ThreadLocal的set方法,其實核心就是調用ThreadLocalMap的set方法,set方法的流程比較長

/**
 * 為當前ThreadLocal對象設置value
 */
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;

    // 計算新元素應該放到哪個位置(這個位置不一定是最終存放的位置,因為可能會出現hash衝突)
    int i = key.threadLocalHashCode & (len - 1);

    // 判斷計算出來的位置是否被佔用,如果被佔用,則需要找出應該存放的位置
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 獲取Entry中key,也就是弱引用的對象
        ThreadLocal<?> k = e.get();

        // 判斷key是否相等(判斷弱引用的是否為同一個ThreadLocal對象)如果是,則進行覆蓋
        if (k == key) {
            e.value = value;
            return;
        }

        // k為null,也就是Entry的key已經被回收了,當前的Entry是一個陳舊的元素(stale entry)
        if (k == null) {
            // 用新元素替換掉陳舊元素,同時也會清理其他陳舊元素,防止內存泄露
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // map中沒有ThreadLocal對應的key,或者說沒有找到陳舊的Entry,則創建一個新的Entry,放入數組中
    tab[i] = new Entry(key, value);
    // ThreadLocalMap的元素數量加1
    int sz = ++size;

    // 先清理map中key為null的Entry元素,該Entry也應該被回收掉,防止內存泄露
    // 如果清理出陳舊的Entry,那麼就判斷是否需要擴容,如果需要的話,則進行rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        rehash();
    }
}

  上面最後幾行代碼涉及到清理陳舊Entry和rehash,這兩塊的代碼在下面。

 

3.5清理陳舊Entry和rehash

  陳舊的Entry,是指Entry的key為null,這種情況下,該Entry是不可訪問的,但是卻不會被回收,為了避免出現內存泄漏,所以需要在每次get、set、replace時,進行清理陳舊的Entry,下面只給出一部分代碼:

/**
 * 清理map中key為null的Entry元素,該Entry也應該被回收掉,防止內存泄露
 *
 * @param i 新Entry插入的位置
 * @param n 數組中元素的數量
 * @return 是否有陳舊的entry的清除
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ((n >>>= 1) != 0);
    return removed;
}

private void rehash() {
    // 清除底層數組中所有陳舊的(stale)的Entry,也就是key為null的Entry
    // 同時每清除一個Entry,就對其後面的Entry重新計算hash,獲取新位置,使用線性探測法,重新確定最終位置
    expungeStaleEntries();

    // 清理完陳舊Entry后,判斷是否需要擴容
    if (size >= threshold - threshold / 4) {
        // 擴容時,容量變為舊容量的2倍,再進行rehash,並使用線性探測發確定Entry的新位置
        resize();
    }
}

  在rehash的時候,涉及到“線性探測法”,是一種用來解決hash衝突的方案,可以查看利用線性探測法解決hash衝突了解詳情。

 

3.6ThreadLocalMap-remove操作

  remove操作,是調用ThreadLocal.remove()方法時,刪除當前線程的ThreadLocalMap中該ThreadLocal為key的Entry。

/**
 * 移除當前線程的threadLocals屬性中key為ThreadLocal的Entry
 *
 * @param key 要移除的Entry的key(ThreadLocal對象)
 */
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;

    // 計算出該ThreadLocal對應的key應該存放的位置
    int i = key.threadLocalHashCode & (len - 1);

    // 找到指定位置,開始按照線性探測算法進行查找到該Thread的Entry
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {

        // 如果Entry的key相同
        if (e.get() == key) {
            // 調用WeakReference的clear方法,Entry的key是弱引用,指向ThreadLocal,現在將key指向null
            // 則該ThreadLocal對象在會在下一次gc時,被垃圾收集器回收
            e.clear();

            // 將該位置的Entry中的value置為null,於是value引用的對象也會被垃圾收集器回收(不會造成內存泄漏)
            // 同時內部會調整Entry的順序(開放探測算法的特點,刪除元素後會重新調整順序)
            expungeStaleEntry(i);

            return;
        }
    }
}

 

四.總結

  在學習ThreadLocal類源碼的過程還是受益頗多的:

  1.ThreadLocal的使用場景;

  2.initialValue的延遲執行;

  3.HashMap使用鏈表+紅黑樹解決hash衝突,ThreadLocalMap使用線性探測算法(開放尋址)解決hash衝突

  另外,ThreadLocal還有一部分內容,是關於弱引用和內存泄漏的問題,可以參考:分析ThreadLocal的弱引用與內存泄漏問題-Java8。

 

  原文地址:https://www.cnblogs.com/-beyond/p/13093032.html

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

這 10 行比較字符串相等的代碼給我整懵逼了,不信你也來看看

抱歉用這種標題吸引你點進來了,不過你不妨看完,看看能否讓你有所收穫。​(有收穫,請評論區留個言,沒收穫,下周末我直播吃**,哈哈,這你也信)

補充說明:微信公眾號改版,對各個號主影響還挺大的。目前從後台數據來看,對我影響不大,因為我這反正都是小號,閱讀量本身就少的可憐,真相了,狗頭(剛從交流群學會的表情)。

先直接上代碼:

boolean safeEqual(String a, String b) {
   if (a.length() != b.length()) {
       return false;
   }
   int equal = 0;
   for (int i = 0; i < a.length(); i++) {
       equal |= a.charAt(i) ^ b.charAt(i);
   }
   return equal == 0;
}

上面的代碼是我根據原版(Scala)翻譯成 Java的,Scala 版本(最開始吸引程序猿石頭注意力的代碼)如下:

def safeEqual(a: String, b: String) = {
  if (a.length != b.length) {
    false
  } else {
    var equal = 0
    for (i <- Array.range(0, a.length)) {
      equal |= a(i) ^ b(i)
    }
    equal == 0
  }
}

剛開始看到這段源碼感覺挺奇怪的,這個函數的功能是比較兩個字符串是否相等,首先“長度不等結果肯定不等,立即返回”這個很好理解。

再看看後面的,稍微動下腦筋,轉彎下也能明白這其中的門道:通過異或操作1^1=0, 1^0=1, 0^0=0,來比較每一位,如果每一位都相等的話,兩個字符串肯定相等,最後存儲累計異或值的變量equal必定為 0,否則為 1。

再細想一下呢?

for (i <- Array.range(0, a.length)) {
  if (a(i) ^ b(i) != 0// or a(i) != b[i]
    return false
}

我們常常講性能優化,從效率角度上講,難道不是應該只要中途發現某一位的結果不同了(即為1)就可以立即返回兩個字符串不相等了嗎?(如上所示)

這其中肯定有……

再再細想一下呢?

結合方法名稱 safeEquals 可能知道些眉目,與安全有關。

本文開篇的代碼來自playframewok 里用來驗證cookie(session)中的數據是否合法(包含簽名的驗證),也是石頭寫這篇文章的由來。

以前知道通過延遲計算等手段來提高效率的手段,但這種已經算出結果卻延遲返回的,還是頭一回!

我們來看看,JDK 中也有類似的方法,如下代碼摘自 java.security.MessageDigest

public static boolean isEqual(byte[] digesta, byte[] digestb) {
   if (digesta == digestb) return true;
   if (digesta == null || digestb == null) {
       return false;
   }
   if (digesta.length != digestb.length) {
       return false;
   }

   int result = 0;
   // time-constant comparison
   for (int i = 0; i < digesta.length; i++) {
       result |= digesta[i] ^ digestb[i];
   }
   return result == 0;
}

看註釋知道了,目的是為了用常量時間複雜度進行比較。

但這個計算過程耗費的時間不是常量有啥風險? (腦海里響起了背景音樂:“小朋友,你是否有很多問號?”)

真相大白

再深入探索和了解了一下,原來這麼做是為了防止計時攻擊(Timing Attack)。(也有人翻譯成時序攻擊​)​

計時攻擊(Timing Attack)

計時攻擊是邊信道攻擊(或稱”側信道攻擊”, Side Channel Attack, 簡稱SCA) 的一種,邊信道攻擊是一種針對軟件或硬件設計缺陷,走“歪門邪道”的一種攻擊方式。

這種攻擊方式是通過功耗、時序、電磁泄漏等方式達到破解目的。在很多物理隔絕的環境中,往往也能出奇制勝,這類新型攻擊的有效性遠高於傳統的密碼分析的數學方法(某百科上說的)。

這種手段可以讓調用 safeEquals("abcdefghijklmn", "xbcdefghijklmn") (只有首位不相同)和調用 safeEquals("abcdefghijklmn", "abcdefghijklmn") (兩個完全相同的字符串)的所耗費的時間一樣。防止通過大量的改變輸入並通過統計運行時間來暴力破解出要比較的字符串。

舉個,如果用之前說的“高效”的方式來實現的話。假設某個用戶設置了密碼為 password,通過從a到z(實際範圍可能更廣)不斷枚舉第一位,最終統計發現 p0000000 的運行時間比其他從任意a~z的都長(因為要到第二位才能發現不同,其他非 p 開頭的字符串第一位不同就直接返回了),這樣就能猜測出用戶密碼的第一位很可能是p了,然後再不斷一位一位迭代下去最終破解出用戶的密碼。

當然,以上是從理論角度分析,確實容易理解。但實際上好像通過統計運行時間總感覺不太靠譜,這個運行時間對環境太敏感了,比如網絡,內存,CPU負載等等都會影響。

但安全問題感覺更像是 “寧可信其有,不可信其無”。為了防止(特別是與簽名/密碼驗證等相關的操作)被 timing attack,目前各大語言都提供了相應的安全比較函數。各種軟件系統(例如 OpenSSL)、框架(例如 Play)的實現也都採用了這種方式。

例如 “世界上最好的編程語言”(粉絲較少,評論區應該打不起架來)—— php中的:

// Compares two strings using the same time whether they're equal or not.
// This function should be used to mitigate timing attacks; 
// for instance, when testing crypt() password hashes.
bool hash_equals ( string $known_string , string $user_string )

//This function is safe against timing attacks.
boolean password_verify ( string $password , string $hash )

其實各種語言版本的實現方式都與上面的版本差不多,將兩個字符串每一位取出來異或(^)並用或(|)保存,最後通過判斷結果是否為 0 來確定兩個字符串是否相等。

如果剛開始沒有用 safeEquals 去實現,後續的版本還會通過打補丁的方式去修復這樣的安全隱患。

例如 JDK 1.6.0_17 中的Release Notes[1]中就提到了MessageDigest.isEqual 中的bug的修復,如下圖所示:

MessageDigest timing attack vulnerabilities

大家可以看看這次變更的的詳細信息openjdk中的 bug fix diff[2]為:

MessageDigest.isEqual計時攻擊

Timing Attack 真的可行嗎?

我覺得各大語言的 API 都用這種實現,肯定還是有道理的,理論上應該可以被利用的。 這不,學術界的這篇論文就宣稱用這種計時攻擊的方法破解了 OpenSSL 0.9.7 的RSA加密算法了。關於 RSA 算法的介紹可以看看之前本人寫的這篇文章。

這篇Remote Timing Attacks are Practical[3] 論文中指出(我大致翻譯下摘要,感興趣的同學可以通過文末鏈接去看原文):

計時攻擊往往用於攻擊一些性能較弱的計算設備,例如一些智能卡。我們通過實驗發現,也能用於攻擊普通的軟件系統。本文通過實驗證明,通過這種計時攻擊方式能夠攻破一個基於 OpenSSL 的 web 服務器的私鑰。結果證明計時攻擊用於進行網絡攻擊在實踐中可行的,因此各大安全系統需要抵禦這種風險。

最後,本人畢竟不是專研完全方向,以上描述是基於本人的理解,如果有不對的地方,還請大家留言指出來。感謝。

補充說明2:感謝正在閱讀文章的你,讓我還有動力繼續堅持更新原創。

本人發文不多,但希望寫的文章能達到的目的是:佔用你的閱讀時間,就盡量能夠讓你有所收穫。

如果你覺得我的文章有所幫助,還請你幫忙轉發分享,另外請別忘了點擊公眾號右上角加個星標,好讓你別錯過後續的精彩文章(微信改版了,或許我發的文章都不能推送到你那了)。

​原創真心不易,希望你能幫我個小忙唄,如果本文內容你覺得有所啟發,有所收穫,請幫忙點個“在看”唄,或者轉發分享讓更多的小夥伴看到。 ​ 參考資料:

  • Timing Attacks on RSA: Revealing Your Secrets through the Fourth Dimension
  • Remote Timing Attacks are Practical

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

SpringColud Eureka的服務註冊與發現

一、Eureka簡介

本文中所有代碼都會上傳到git上,請放心瀏覽
項目git地址:https://github.com/839022478/Spring-Cloud

在傳統應用中,組件之間的調用,通過有規範的約束的接口來實現,從而實現不同模塊間良好的協作。但是被拆分成微服務后,每個微服務實例的網絡地址都可能動態變化,數量也會變化,使得原來硬編碼的地址失去了作用。需要一个中心化的組件來進行服務的登記和管理,為了解決上面的問題,於是出現了服務治理,就是管理所有的服務信息和狀態,也就是我們所說的註冊中心

1.1 註冊中心

比如我們去做火車或者汽車,需要去買票乘車,只看我們有沒有票(有沒有服務),有就去買票(獲取註冊列表),然後乘車(調用),不用關心到底有多少車在運行

流程圖:

使用註冊中心,我們不需要關心有多少提供方,只管去調用就可以了,那麼註冊中心有哪些呢?

註冊中心:Eureka,Nacos,Consul,Zookeeper

本文中講解的是比較火熱的Spring Cloud微服務下的Eureka,Eureka是Netflix開發的服務發現框架,是一個RESTful風格的服務,是一個用於服務發現和註冊的基礎組件,是搭建Spring Cloud微服務的前提之一,它屏蔽了Server和client的交互細節,使得開發者將精力放到業務上。

服務註冊與發現主要包括兩個部分:服務端(Eureka Server)和客戶端(Eureka Client)

  • 服務端(Eureka Server): 一個公共服務,為Client提供服務註冊和發現的功能,維護註冊到自身的Client的相關信息,同時提供接口給Client獲取註冊表中其他服務的信息,使得動態變化的Client能夠進行服務間的相互調用。

  • 客戶端(Eureka Client): Client將自己的服務信息通過一定的方式登記到Server上,並在正常範圍內維護自己信息一致性,方便其他服務發現自己,同時可以通過Server獲取到自己依賴的其他服務信息,完成服務調用,還內置了負載均衡器,用來進行基本的負載均衡

Eureka GIt官網:https://github.com/Netflix/Eureka

1.3 服務註冊與發現

服務註冊與發現關係圖:

1.2 client功能和server功能

1.2.1 client功能

  1. 註冊:每個微服務啟動時,將自己的網絡地址等信息註冊到註冊中心,註冊中心會存儲(內存中)這些信息。
  2. 獲取服務註冊表:服務消費者從註冊中心,查詢服務提供者的網絡地址,並使用該地址調用服務提供者,為了避免每次都查註冊表信息,所以client會定時去server拉取註冊表信息到緩存到client本地。
  3. 心跳:各個微服務與註冊中心通過某種機制(心跳)通信,若註冊中心長時間和服務間沒有通信,就會註銷該實例。
  4. 調用:實際的服務調用,通過註冊表,解析服務名和具體地址的對應關係,找到具體服務的地址,進行實際調用。

1.2.2 server註冊中心功能

  1. 服務註冊表:記錄各個微服務信息,例如服務名稱,ip,端口等。
    註冊表提供 查詢API(查詢可用的微服務實例)和管理API(用於服務的註冊和註銷)。
  2. 服務註冊與發現:註冊:將微服務信息註冊到註冊中心。發現:查詢可用微服務列表及其網絡地址。
  3. 服務檢查:定時檢測已註冊的服務,如發現某實例長時間無法訪問,就從註冊表中移除。

二、Eureka單節點搭建

2.1 pom.xml

在有的教程中,會引入spring-boot-starter-web,這個依賴其實不用,因為spring-cloud-starter-netflix-eureka-server的依賴已經包含了它,在pom依賴進去,就可以了

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

2.2 application.yml

server:
  port: 8500
eureka:
  client:
    #是否將自己註冊到Eureka Server,默認為true,由於當前就是server,故而設置成false,表明該服務不會向eureka註冊自己的信息
    register-with-eureka: false
    #是否從eureka server獲取註冊信息,由於單節點,不需要同步其他節點數據,用false
    fetch-registry: false
    #設置服務註冊中心的URL,用於client和server端交流
    service-url:
      defaultZone: http://localhost:8080/eureka/

2.3 服務端啟動類

啟動類上添加此註解標識該服務為配置中心
@EnableEurekaServer

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }

}

2.4 啟動

我們啟動EurekaDemoApplication ,然後在瀏覽器中輸入地址 http://localhost:8500/,就可以啟動我們的 Eureka 了,我們來看下效果,出現了這個畫面,就說明我們已經成功啟動~,只是此時我們的服務中是還沒有客戶端進行註冊

三、服務註冊

注意:在客戶端pom裏面我們需要加上spring-boot-starter-web,否則服務是無法正常啟動的

3.1 pom.xml

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>     

3.2 application.yml

#註冊中心
eureka:
  client:
    #設置服務註冊中心的URL
    service-url:
      defaultZone: http://localhost:8500/eureka/
  #服務名
  instance:
    appname: mxn

3.3 客戶端啟動類

在客戶端啟動類中我們需要加上 @EnableDiscoveryClient註解

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class EurekaClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaClientApplication.class, args);
    }
}

3.4 查看效果

工程啟動后,刷新http://localhost:8500/頁面,我們可以發現服務註冊成功了

並且我們可以在idea日誌打印中看到DiscoveryClient_MXN/DESKTOP-5BQ3UK8 - registration status: 204,說明就是註冊成功了
Eureka Server與Eureka Client之間的聯繫主要通過心跳的方式實現。心跳(Heartbeat)即Eureka Client定時向Eureka Server彙報本服務實例當前的狀態,維護本服務實例在註冊表中租約的有效性。

Eureka Client將定時從Eureka Server中拉取註冊表中的信息,並將這些信息緩存到本地,用於服務發現

四、Eureka 端點

官網地址:https://github.com/Netflix/eureka/wiki/Eureka-REST-operations

Eureka服務器還提供了一個端點(eureka/apps/{applicaitonName})可以查看所註冊的服務詳細信息 。applicaitonName就是微服務的名稱,比如這裏我們訪問 http://localhost:8500/eureka/apps/mxn

五、Eureka 原理

5.1 本質

存儲了每個客戶端的註冊信息。EurekaClient從EurekaServer同步獲取服務註冊列表。通過一定的規則選擇一個服務進行調用

5.2 Eureka架構圖

  • 服務提供者: 是一個eureka client,向Eureka Server註冊和更新自己的信息,同時能從Eureka Server註冊表中獲取到其他服務的信息。
  • 服務註冊中心: 提供服務註冊和發現的功能。每個Eureka Cient向Eureka Server註冊自己的信息,也可以通過Eureka Server獲取到其他服務的信息達到發現和調用其他服務的目的。
  • 服務消費者: 是一個eureka client,通過Eureka Server獲取註冊到其上其他服務的信息,從而根據信息找到所需的服務發起遠程調用。
  • 同步複製: Eureka Server之間註冊表信息的同步複製,使Eureka Server集群中不同註冊表中服務實例信息保持一致。
  • 遠程調用: 服務客戶端之間的遠程調用。
  • 註冊: Client端向Server端註冊自身的元數據以供服務發現。
  • 續約: 通過發送心跳到Server以維持和更新註冊表中服務實例元數據的有效性。當在一定時長內,Server沒有收到Client的心跳信息,將默認服務下線,會把服務實例的信息從註冊表中刪除。
  • 下線: Client在關閉時主動向Server註銷服務實例元數據,這時Client的服務實例數據將從Server的註冊表中刪除。
  • 獲取註冊表: Client向Server請求註冊表信息,用於服務發現,從而發起服務間遠程調用。

5.3 Eureka自我保護

有時候我們會看到這樣的提示信息:EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.,這是因為默認情況下,Eureka Server在一定時間內,沒有接收到某個微服務心跳,會將某個微服務註銷(90S)。但是當網絡故障時,微服務與Server之間無法正常通信,上述行為就非常危險,因為微服務正常,不應該註銷,它的指導思想就是 寧可保留健康的和不健康的,也不盲目註銷任何健康的服務
我們也可以通過命令去關閉自我保護的功能:

eureka:
  server: 
    enable-self-preservation: false

那麼自我保護是如何觸發的呢?
自我保護機制的觸發條件是,當每分鐘心跳次數( renewsLastMin) 小於 numberOfRenewsPerMinThreshold時,並且開啟自動保護模式開關( eureka.server.enable-self-preservation = true) 時,觸發自我保護機制,不再自動過期租約
上面我們所有的小於 numberOfRenewsPerMinThreshold,到底是怎麼計算的呢,我們在eureka源碼中可以得知

numberOfRenewsPerMinThreshold = expectedNumberOfRenewsPerMin * 續租百分比(默認為0.85)
expectedNumberOfRenewsPerMin = 當前註冊的應用實例數 x 2
當前註冊的應用實例數 x 2 是因為,在默認情況下,註冊的應用實例每半分鐘續租一次,那麼一分鐘心跳兩次,因此 x 2

例如:我們有10個服務,期望每分鐘續約數:10 * 2=20,期望閾值:20*0.85=17,當少於17時,就會觸發自我保護機制

5.4 健康檢查

由於server和client通過心跳保持 服務狀態,而只有狀態為UP的服務才能被訪問。看eureka界面中的status。

比如心跳一直正常,服務一直UP,但是此服務DB(數據庫)連不上了,無法正常提供服務。

此時,我們需要將 微服務的健康狀態也同步到server。只需要啟動eureka的健康檢查就行。這樣微服務就會將自己的健康狀態同步到eureka。配置如下即可。

在client端配置:將自己的健康狀態傳播到server。

eureka:
  client:
    healthcheck:
      enabled: true

5.5 Eureka監聽事件

import com.netflix.appinfo.InstanceInfo;
import org.springframework.cloud.netflix.eureka.server.event.*;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
public class CustomEvent {

    @EventListener
    public void listen(EurekaInstanceCanceledEvent event ) {
        System.out.println(LocalDateTime.now()+"服務下線事件:"+event.getAppName()+"---"+event.getServerId());
//發釘釘
    }

    @EventListener
    public void listen(EurekaInstanceRegisteredEvent event) {
        InstanceInfo instanceInfo = event.getInstanceInfo();
        System.out.println(LocalDateTime.now()+"服務上線事件:"+instanceInfo.getAppName()+"---"+instanceInfo.getInstanceId());
    }

    @EventListener
    public void listen(EurekaInstanceRenewedEvent event) {
        System.out.println(LocalDateTime.now()+"服務續約/心跳上報事件:"+event.getAppName()+"---"+event.getServerId());

    }

    @EventListener
    public void listen(EurekaRegistryAvailableEvent event) {
        System.out.println(LocalDateTime.now()+"註冊中心可用事件");
    }

    @EventListener
    public void listen(EurekaServerStartedEvent event) {
        System.out.println(LocalDateTime.now()+"註冊中心啟動事件");

    }
}

5.6 Renew: 服務續約

Eureka Client 會每隔 30 秒發送一次心跳來續約。 通過續約來告知 Eureka Server 該 Eureka Client 運行正常,沒有出現問題。 默認情況下,如果 Eureka Server 在 90 秒內沒有收到 Eureka Client 的續約,Server 端會將實例從其註冊表中刪除,此時間可配置,一般情況不建議更改。

5.6 服務剔除

如果Eureka Client在註冊后,既沒有續約,也沒有下線(服務崩潰或者網絡異常等原因),那麼服務的狀態就處於不可知的狀態,不能保證能夠從該服務實例中獲取到回饋,所以需要服務剔除此方法定時清理這些不穩定的服務,該方法會批量將註冊表中所有過期租約剔除,剔除是定時任務,默認60秒執行一次。延時60秒,間隔60秒

剔除的限制:
1.自我保護期間不清除。
2.分批次清除。

六、Eureka缺陷

由於集群間的同步複製是通過HTTP的方式進行,基於網絡的不可靠性,集群中的Eureka Server間的註冊表信息難免存在不同步的時間節點,不滿足CAP中的C(數據一致性)

七、總結

中間我們講解了eureka的節點搭建,以及原理,對於現在很火熱的微服務,我們對Eureka是非常有必要進行了解的,如果覺得文章對你有幫助,來個點贊支持吧,如果對文章有疑問或建議,歡迎討論留言,謝謝大家~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

Express4.x之中間件與路由詳解及源碼分析,Express4.x之API:express,Express4.x之API:express

  • Application.use()
  • Application.router()
  • express核心源碼模擬

 一、express.use()

1.1app.use([path,] callback [, callback …])

通過語法結構可以看到Application.use()參數分別有以下幾種情形:

app.use(function(){...}); //給全局添加一个中間件
app.use(path,function(){...}); //給指定路由path添加一个中間件
app.use(function(){...}, function(){...}, ...); //給全局添加n个中間件
app.use(path,function(){...},function(){...}, ...); //給指定路由path添加n个中間件

關於path最簡單也是最常用的就是字符串類型(例:‘/abcd’);除了字符串Express還提供了模板和正則格式(例:’/abc?d‘, ‘/ab+cd‘, ‘/ab\*cd‘, ‘/a(bc)?d‘, ‘/\/abc|\/xyz/‘);除了單個的字符串和模板還可以將多個path作為一個數組的元素,然後將這個數組作為use的path,這樣就可以同時給多個路由添加中間件,詳細內容可以參考官方文檔:https://www.expressjs.com.cn/4x/api.html#path-examples。

關於callbakc多個或單个中間件程序這已經再語法結構中直觀的體現出來了,這裏重點來看看回調函數的參數:

app.use(function(req,res,next){...}); //必須提供的參數 
app.use(function(err,req,res,next){...}); //錯誤中間件需要在最前面添加一個錯誤參數

關於中間件的簡單應用:

let express = require('./express');
let app = express();
app.use('/',function(req,res,next){
    console.log("我是一個全局中間件");
    next(); //每个中間件的最末尾必須調用next
});

app.use('/',function(err,req,res,next){
    console.log("我是一個全局錯誤中間,當發生錯誤是調用")    
    console.error(err.stack);
    res.status(500).send('服務出錯誤了!');
    //由於這個錯誤處理直接響應了客戶端,可以不再調用next,當然後面還需要處理一些業務的話也是可以調用next的
});

1.2簡單的模擬Express源碼實現Appliction.use()以及各個請求方法的響應註冊方法(這裏個源碼模擬路由概念還比較模糊,所以使用請求方法的響應註冊API,而沒有使用路由描述):

 1 //文件結構
 2 express
 3     index.js
 4 //源碼模擬實現
 5 let http = require("http");
 6 let url = require('url');
 7 function createApplication(){
 8     //app是一個監聽函數
 9     let app = (req,res) =>{
10         //取出每一個層
11         //1.獲取請求的方法
12         let m = req.method.toLowerCase();
13         let {pathname} = url.parse(req.url,true);
14 
15         //通過next方法進行迭代
16         let index = 0;
17         function next(err){
18             //如果routes迭代完成還沒有找到,說明路徑不存在
19             if(index === app.routes.length) return res.end(`Cannot ${m} ${pathname}`);
20             let {method, path, handler} = app.routes[index++];//每次調用next就應該取下一個layer
21             if(err){
22                 if(handler.length === 4){
23                     handler(err,req,res,next);
24                 }else{
25                     next(err);
26                 }
27             }else{
28                 if(method === 'middle'){ //處理中間件
29                     if(path === '/' || path === pathname || pathname.startsWith(path+'/')){
30                         handler(req,res,next);
31                     }else{
32                         next();//如果這个中間件沒有匹配到,繼續通過next迭代路由容器routes
33                     }
34                 }else{ //處理路由
35                     if( (method === m || method ==='all') && (path === pathname || path === '*')){ //匹配請求方法和請求路徑(接口)
36                         handler(req,res);//匹配成功后執行的Callback
37                     }else{
38                         next();
39                     }
40                 }
41             }
42         }
43         next();
44     }
45     app.routes = [];//路由容器
46     app.use = function(path,handler){
47         if(typeof handler !== 'function'){
48             handler = path;
49             path = '/';
50         }
51         let layer = {
52             method:'middle', //method是middle就表示它是一个中間件
53             path,
54             handler
55         }
56         app.routes.push(layer);
57     }
58     app.all = function(path,handler){
59         let layer = {
60             method:'all',
61             path,
62             handler
63         }
64         app.routes.push(layer);
65     }
66     console.log(http.METHODS);
67     http.METHODS.forEach(method =>{
68         method = method.toLocaleLowerCase();
69         app[method] = function (path,handler){//批量生成各個請求方法的路由註冊方法
70             let layer = {
71                 method,
72                 path,
73                 handler
74             }
75             app.routes.push(layer);
76         }
77     });
78     //內置中間件,給req擴展path、qury屬性
79     app.use(function(req,res,next){
80         let {pathname,query} = url.parse(req.url,true);
81         let hostname = req.headers['host'].split(':')[0];
82         req.path = pathname;
83         req.query = query;
84         req.hostname = hostname;
85         next();
86     });
87     //通過app.listen調用http.createServer()掛在app(),啟動express服務
88     app.listen = function(){
89         let server = http.createServer(app);
90         server.listen(...arguments);
91     }
92     return app;
93 }
94 module.exports = createApplication;

View Code

測試模擬實現的Express:

 1 let express = require('./express');
 2 
 3 let app = express();
 4 
 5 app.use('/',function(req,res,next){
 6     console.log("我是一個全局中間件");
 7     next();
 8 });
 9 app.use('/user',function(req,res,next){
10     console.log("我是user接口的中間件");
11     next();
12 });
13 app.get('/name',function(req,res){
14     console.log(req.path,req.query,req.hostname);
15     res.end('zfpx');
16 });
17 app.post('/name',function(req,res){
18     res.end('post name');
19 });
20 
21 
22 app.all("*",function(req,res){
23     res.end('all');
24 });
25 
26 app.use(function(err,req,res,next){
27     console.log(err);
28     next();
29 });
30 
31 app.listen(12306);

View Code

在windows系統下測試請求:

 

 

關於源碼的構建詳細內容可以參考這個視頻教程:app.use()模擬構建視頻教程,前面就已經說明過這個模式實現僅僅是從表面的業務邏輯,雖然有一點底層的雛形,但與源碼還是相差甚遠,這一部分也僅僅只是想幫助理解Express採用最簡單的方式表現出來。

1.3如果你看過上面的源碼或自己也實現過,就會發現Express關於中間件的添加方式除了app.use()還有app.all()及app.METHOD()。在模擬源碼中我並未就use和all的差異做處理,都是採用了請求路徑絕對等於path,這種方式是all的特性,use的path實際表示為請求路徑的開頭:

app.use(path,callback):path表示請求路徑的開頭部分。

app.all(path,callback):paht表示完全等於請求路徑。

app.METHOD(path,callback):並不是真的有METHOD這個方法,而是指HTTP請求方法,實際上表示的是app.get()、app.post()、app.put()等方法,而有時候我們會將這些方法說成用來註冊路由,這是因為路由註冊的確使用這些方法,但同時這些方法也是可以用作中間的添加,這在前一篇博客中的功能解析中就有說明(Express4.x之API:express),詳細見過後面的路由解析就會更加明了。

 二、express.router()

2.1在實例化一個Application時會實例化一個express.router()實例並被添加到app._router屬性上,實際上這個app使用的use、all、METHOD時都是在底層調用了該Router實例上對應的方法,比如看下面這些示例:

 1 let express = require("express");
 2 let app = express();
 3 
 4 app._router.use(function(req,res,next){
 5     console.log("--app.router--");
 6     next();
 7 });
 8 
 9 app._router.post("/csJSON",function(req,res,next){
10     res.writeHead(200);
11     res.write(JSON.stringify(req.body));
12     res.end();
13 });
14 
15 app.listen(12306);

上面示例中的app._router.use、app._router.post分別同等與app.use、app.post,這裏到這裏也就說明了上一篇博客中的路由與Application的關係Express4.x之API:express。

2.2Express中的Router除了為Express.Application提供路由功能以外,Express也將它作為一個獨立的路由工具分離了出來,也就是說Router自身可以獨立作為一個應用,如果我們在實際應用中有相關業務有類似Express.Application的路由需求,可以直接實例化一個Router使用,應用的方式如下:

let express = require('/express');
let router = express.Router();
//這部分可以詳細參考官方文檔有詳細的介紹

2.3由於這篇博客主要是分析Express的中間件及路由的底層邏輯,所以就不在這裏詳細介紹某個模塊的應用,如果有時間我再寫一篇關於Router模塊的應用,這裏我直接上一份模擬Express路由的代碼以供參考:

文件結構:

express //根路徑
    index.js //express主入口文件
    application.js //express應用構造模塊
    router //路由路徑
        index.js //路由主入口文件
        layer.js //構造層的模塊
        route.js //子路由模塊

Express路由系統的邏輯結構圖:

模擬代碼(express核心源碼模擬):

1 //express主入口文件
2 let Application = require('./application.js');
3 
4 function createApplication(){
5     return new Application();
6 }
7 
8 module.exports = createApplication;

index.js //express主入口文件

 1 //用來創建應用app
 2 let http = require('http');
 3 let url = require('url');
 4 
 5 //導入路由系統模塊
 6 let Router = require('./router');
 7 
 8 const methods = http.METHODS;
 9 
10 //Application ---- express的應用系統
11 function Application(){
12     //創建一個內置的路由系統
13     this._router = new Router();
14 }
15 
16 //app.get ---- 實現路由註冊業務
17 // Application.prototype.get = function(path,...handlers){
18 //     this._router.get(path,'use',handlers);
19 // }
20 
21 methods.forEach(method => {
22     method = method.toLocaleLowerCase();
23     Application.prototype[method] = function(path,...handlers){
24         this._router[method](path,handlers);
25     }
26 });
27 
28 //app.use ---- 實現中間件註冊業務
29 //這裏模擬處理三種參數模式:
30 // -- 1個回調函數:callback
31 // -- 多個回調函數:[callback,] callback [,callback...]
32 // -- 指定路由的中間件:[path,] callback [,callback...]
33 // -- 注意源碼中可以處理這三種參數形式還可以處理上面數據的數組形式,以及其他Application(直接將其他app上的中間件添加到當前應用上)
34 Application.prototype.use = function(fn){
35     let path = '/';
36     let fns = [];
37     let arg = [].slice.call(arguments);
38     if(typeof fn !== 'function' && arg.length >= 2){
39         if(typeof arg[0] !== 'string'){
40             fns = arg;
41         }else{
42             path = arg[0];
43             fns = arg.slice(1);
44         }
45     }else{
46         fns = arg;
47     }
48     this._router.use(path,'use',fns);
49 }
50 
51 Application.prototype.all = function(fn){
52     let path = '/';
53     let fns = [];
54     let arg = [].slice.call(arguments);
55     if(typeof fn !== 'function' && arg.length >= 2){
56         if(typeof arg[0] !== 'string'){
57             fns = arg;
58         }else{
59             path = arg[0];
60             fns = arg.slice(1);
61         }
62       }else{
63         fns = arg;
64     }
65     this._router.use(path,'all',fns);
66 }
67 
68 //將http的listen方法封裝到Application的原型上
69 Application.prototype.listen = function(){
70     let server = http.createServer((req,res)=>{
71         //done 用於當路由無任何可匹配項時調用的處理函數
72         function done(){
73             res.end(`Cannot ${req.url} ${req.method}`);
74         }
75         this._router.handle(req,res,done); //調用路由系統的handle方法處理請求
76     });
77     server.listen(...arguments);
78 };
79 
80 module.exports = Application;

application.js //express應用構造模塊

 1 //express路由系統
 2 const Layer = require('./layer.js');
 3 const Route = require('./route.js');
 4 
 5 const http = require('http');
 6 const methods = http.METHODS;
 7 
 8 const url = require('url');
 9 
10 
11 //路由對象構造函數
12 function Router(){
13     this.stack = [];
14 }
15 
16 //router.route ---- 用於創建子路由對象route與主路由上層(layer)的關係
17 //並將主路由上的層緩存到路由對象的stack容器中,該層建立路徑與子路由處理請求的關係
18 Router.prototype.route = function(path){
19     let route = new Route();
20     let layer = new Layer(path,route.dispatch.bind(route));
21     this.stack.push(layer);
22     return route;
23 }
24 
25 //router.get ---- 實現路由註冊
26 //實際上這個方法調用router.route方法分別創建一個主路由系統層、一個子路由系統,並建立兩者之間的關係,詳細見Router.prototype.route
27 //然後獲取子路由系統對象,並將回調函數和請求方法註冊在這個子路由系統上
28 
29 
30 // Router.prototype.get = function(path,handlers){
31 //     let route = this.route(path);
32 //     route.get(handlers);
33 // }
34 
35 methods.forEach(method =>{ 
36     method = method.toLocaleLowerCase();
37     //注意下面這個方法會出現內存泄漏問題,有待改進
38     Router.prototype[method] = function(path, handlers){
39         let route = this.route(path);
40         route[method](handlers);
41     }
42 });
43 
44 //router.use ---- 實現中間件註冊(按照路由開頭的路徑匹配,即相對路由匹配)
45 Router.prototype.use = function(path,routerType,fns){
46     let router = this;
47     fns.forEach(function(fn){
48         let layer = new Layer(path,fn);
49         layer.middle = true; //標記這個層為相對路由中間件
50         layer.routerType = routerType;
51         router.stack.push(layer);
52     });
53 }
54 
55 //調用路由處理請求
56 Router.prototype.handle = function(req,res,out){
57     let {pathname} = url.parse(req.url);
58     let index = 0;
59     let next = () => {
60         if(index >= this.stack.length) return out();
61         let layer = this.stack[index++];
62         if(layer.middle && (layer.path === '/' || pathname === layer.path || pathname.startsWith(layer.path + '/'))){
63             //處理中間件
64             if(layer.routerType === 'use'){
65                 layer.handle_request(req,res,next);
66             }else if(layer.routerType === 'all' && layer.path === pathname){
67                 layer.handle_request(req,res,next);
68             }else{
69                 next();
70             }
71         }else if(layer.match(pathname)){
72             //處理響應--更準確的說是處理具體請求方法上的中間件或響應
73             layer.handle_request(req,res,next);
74         }else{
75             next();
76         }
77     }
78     next();
79 }
80 
81 module.exports = Router;

index.js //路由主入口文件

 1 //Layer的構造函數
 2 function Layer(path,handler){
 3     this.path = path; //當前層的路徑
 4     this.handler = handler;  //當前層的回調函數
 5 }
 6 
 7 //判斷請求方法與當前層的方法是否一致
 8 Layer.prototype.match = function(pathname){
 9     return this.path === pathname;
10 }
11 
12 //調用當前層的回調函數handler
13 Layer.prototype.handle_request = function(req,res,next){
14     this.handler(req,res,next);
15 }
16 
17 module.exports = Layer;

layer.js //構造層的模塊

 1 //Layer的構造函數
 2 function Layer(path,handler){
 3     this.path = path; //當前層的路徑
 4     this.handler = handler;  //當前層的回調函數
 5 }
 6 
 7 //判斷請求方法與當前層的方法是否一致
 8 Layer.prototype.match = function(pathname){
 9     return this.path === pathname;
10 }
11 
12 //調用當前層的回調函數handler
13 Layer.prototype.handle_request = function(req,res,next){
14     this.handler(req,res,next);
15 }
16 
17 module.exports = Layer;

route.js //子路由模塊

測試代碼:

 1 let express = require('./express');
 2 let app = express();
 3 
 4 
 5 app.use('/name',function(req,res,next){
 6     console.log('use1');
 7     next();
 8 });
 9 app.use(function(req,res,next){
10     console.log('use2-1');
11     next();
12 },function(req,res,next){
13     console.log('use2-2');
14     next();
15 });
16 
17 app.all('/name',function(req,res,next){
18     console.log('all-1');
19     next();
20 });
21 app.all('/name/app',function(req,res,next){
22     console.log('all-2');
23     next();
24 });
25 
26 app.get('/name/app',function(req,res){
27     res.end(req.url);
28 });
29 // console.log(app._router.stack);
30 app.listen(12306);

View Code

測試結果:

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

Kubernetes-subpath的使用

一、什麼是subpath

為了支持單一個pod多次使用同一個volume而設計,subpath翻譯過來是子路徑的意思,如果是數據卷掛載在容器,指的是存儲卷目錄的子路徑,如果是配置項configMap/Secret,則指的是掛載在容器的子路徑。

 

二、subpath的使用場景

1、 1個pod中可以拉起多個容器,有時候希望將不同容器的路徑掛載在存儲卷volume的子路徑,這個時候需要用到subpath

2、volume支持將configMap/Secret掛載在容器的路徑,但是會覆蓋掉容器路徑下原有的文件,如何支持選定configMap/Secret的每個key-value掛載在容器中,且不會覆蓋掉原目錄下的文件,這個時候也可以用到subpath

 

三、subpath的使用

1、存儲卷

    採用hostpath的方式創建PV,宿主機的映射目錄為/data/pod/volume5

[root@k8s-master zhanglei]# cat pv-subpath.yaml 
kind: PersistentVolume
apiVersion: v1
metadata:
  name: pv-subpath-05
  labels:
    release: stable
spec:
  capacity:
    storage: 0.1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  hostPath:
    path: /data/pod/volume5                 # 宿主機的目錄

[root@k8s-master zhanglei]# kubectl create -f pv-subpath.yaml  

PV創建成功后,再創建PVC

[root@k8s-master zhanglei]# cat pvc-subpath.yaml 
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-subpath
  namespace: default
spec:
 accessModes: ["ReadWriteOnce"]
 resources:
   requests: 
     storage: 0.05Gi
[root@k8s-master zhanglei]# kubectl create -f pvc-subpath.yaml

在pod中聲明並使用subpath

[root@k8s-master zhanglei]# cat pod-subpath.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: pod-subpath-zltest
spec:
    containers:
    - name: ubuntu-subpath-container
      image: ubuntu
      volumeMounts:
      - mountPath: /var/lib/ubuntu            # 容器1的掛載目錄
        name: subpath-vol
        subPath: ubuntutest                   # 宿主機volume5的子目錄1
    - name: nginx-subpath-container
      image: nginx
      volumeMounts:
      - mountPath: /var/www/nginx             # 容器2的掛載目錄
        name: subpath-vol
        subPath: nginxtest                   # 宿主機volume5的子目錄2 
    volumes:
    - name: subpath-vol
      persistentVolumeClaim:
        claimName: pvc-subpath               # PVC的名字

  [root@k8s-master zhanglei]# kubectl create -f pod-subpath.yaml

[root@k8s-master zhanglei]# kubectl describe pod  pod-subpath-zltest 
Name:         pod-subpath-zltest
Namespace:    default
Priority:     0
Node:         k8s-master/192.168.126.129
Start Time:   Fri, 29 May 2020 16:45:49 +0800
Labels:       <none>
Annotations:  cni.projectcalico.org/podIP: 10.122.235.235/32
              cni.projectcalico.org/podIPs: 10.122.235.235/32
Status:       Running
IP:           10.122.235.235
IPs:
  IP:  10.122.235.235
Containers:
  ubuntu-subpath-container:
    Container ID:   docker://6e5cb30ee7e03b77d2ca22e4cd818ff326fa40836427fe17b1584646b4388dce
    Image:          ubuntu
    Image ID:       docker-pullable://ubuntu@sha256:747d2dbbaaee995098c9792d99bd333c6783ce56150d1b11e333bbceed5c54d7
    Port:           <none>
    Host Port:      <none>
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Sun, 14 Jun 2020 22:38:11 +0800
      Finished:     Sun, 14 Jun 2020 22:38:11 +0800
    Ready:          False
    Restart Count:  558
    Environment:    <none>
    Mounts:
      /var/lib/ubuntu from subpath-vol (rw,path="ubuntutest")
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-74s86 (ro)
  nginx-subpath-container:
    Container ID:   docker://95101741eb1b6aa4c1e53d8fc4ab8006e74fd2eb923eca211ca20a01edcd7630
    Image:          nginx
    Image ID:       docker-pullable://nginx@sha256:30dfa439718a17baafefadf16c5e7c9d0a1cde97b4fd84f63b69e13513be7097
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Fri, 29 May 2020 16:47:14 +0800
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-74s86 (ro)
      /var/www/nginx from subpath-vol (rw,path="nginxtest")
Conditions:
  Type              Status
  Initialized       True 
  Ready             False 
  ContainersReady   False 
  PodScheduled      True 
Volumes:
  subpath-vol:
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  pvc-subpath
    ReadOnly:   false
  default-token-74s86:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-74s86
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type     Reason   Age                    From                 Message
  ----     ------   ----                   ----                 -------
  Normal   Pulled   21m (x555 over 16d)    kubelet, k8s-master  Successfully pulled image "ubuntu"
  Normal   Created  21m (x555 over 16d)    kubelet, k8s-master  Created container ubuntu-subpath-container
  Normal   Started  21m (x555 over 16d)    kubelet, k8s-master  Started container ubuntu-subpath-container
  Normal   Pulling  6m10s (x562 over 16d)  kubelet, k8s-master  Pulling image "ubuntu"
  Warning  BackOff  71s (x11744 over 16d)  kubelet, k8s-master  Back-off restarting failed container

現在來驗證下在宿主機存儲卷的目錄下是否有2個子目錄,1個是ubuntutest用來掛載容器1的,另外1個是nginxtest用來掛載容器2的

[root@k8s-master /]# cd data/pod/volume5
[root@k8s-master volume5]# ls
nginxtest ubuntutest
[root@k8s-master volume5]# cd nginxtest/     # 可以看到是1個目錄,非文件
[root@k8s-master nginxtest]#

進入到容器中,掛載一個文件,驗證是否可以同步到存儲卷

[root@k8s-master nginxtest]# kubectl exec -it pod-subpath-zltest -c nginx-subpath-container -- bash
root@pod-subpath-zltest:/# cd /var/www/nginx
root@pod-subpath-zltest:/var/www/nginx# ls
nginx-test-subpath.txt
[root@k8s-master volume5]# cd nginxtest/
[root@k8s-master nginxtest]# ls
nginx-test-subpath.txt

可以看到容器1的目錄/var/www/nginx 和存儲卷的子目錄 nginxtest完成了映射,容器2類似,這裏不再贅述。

2、配置項-configMap

1)創建configMap

[root@k8s-master consecret]# cat conf-subpath.yaml 
apiVersion: v1
kind: ConfigMap
metadata:
  name: conf-subpath-zltest
  namespace: default
data:
  example.property.1: hello      # key-value鍵值對
  example.property.2: world
  example.property.file: |-
    property.1=value-1
    property.2=value-2
    property.3=value-3

2)在Pod中使用configMap

[root@k8s-master consecret]# cat pod-conf-subpath.yaml 
apiVersion: v1
kind: Pod
metadata:
  labels:
    purpose: test-configmap-volume
  name: pod-conf-testvolume
spec:
  containers:
    - name: test-configmap-volume
      image: nginx
      volumeMounts:
        - name: config-volume
          mountPath: /etc/nginx/example.property.1       # 容器掛載目錄
          subPath: example.property.1                    # 將key名稱作為文件名,hello作為文件內容
  volumes:
    - name: config-volume
      configMap:
         name: conf-subpath-zltest      # 指定使用哪個CM
        
[root@k8s-master consecret]# kubectl create -f pod-conf-subpath.yaml 
[root@k8s-master consecret]# kubectl describe pod  pod-conf-testvolume 
Name:         pod-conf-testvolume
Namespace:    default
Priority:     0
Node:         k8s-master/192.168.126.129
Start Time:   Wed, 03 Jun 2020 11:46:36 +0800
Labels:       purpose=test-configmap-volume
Annotations:  cni.projectcalico.org/podIP: 10.122.235.249/32
              cni.projectcalico.org/podIPs: 10.122.235.249/32
Status:       Running
IP:           10.122.235.249
IPs:
  IP:  10.122.235.249
Containers:
  test-configmap-volume:
    Container ID:   docker://e2cf37cb24af32023eb5d22389545c3468104a4344c47363b5330addc40cb914
    Image:          nginx
    Image ID:       docker-pullable://nginx@sha256:883874c218a6c71640579ae54e6952398757ec65702f4c8ba7675655156fcca6
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Wed, 03 Jun 2020 11:46:53 +0800
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /etc/nginx/example.property.1 from config-volume (rw,path="example.property.1")  
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-74s86 (ro)
Conditions:
  Type              Status
  Initialized       True 
  Ready             True 
  ContainersReady   True 
  PodScheduled      True 
Volumes:
  config-volume:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      conf-subpath-zltest
    Optional:  false
  default-token-74s86:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-74s86
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:          <none>

在容器掛載路徑驗證下是否將configMap中example.property.1掛載在容器中,且是否會覆蓋掉原有的目錄

root@pod-conf-testvolume:/# cd  /etc/nginx 
root@pod-conf-testvolume:/etc/nginx# ls
conf.d            fastcgi_params  koi-win    modules     scgi_params   win-utf
example.property.1  koi-utf        mime.types    nginx.conf  uwsgi_params

從上可以看到example.property.1已經掛載到容器中,且未對目錄原有的文件進行覆蓋

root@pod-conf-testvolume:/etc/nginx# cd example.property.1 
bash: cd: example.property.1: Not a directory
root@pod-conf-testvolume:/etc/nginx# cat example.property.1 helloroot@pod-conf-testvolume:/etc/nginx# 

從上可以驗證configMap的subpath用法支持將configMap中的每對key-value以key名稱作為文件名,value作為文件內容掛載到容器的目錄中。

四、總結

本文介紹了subpath分別在持久化存儲卷和配置項configMap中的使用,豐富了volume在pod中的使用場景。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

線上服務的FGC問題排查,看這篇就夠了!

線上服務的GC問題,是Java程序非常典型的一類問題,非常考驗工程師排查問題的能力。同時,幾乎是面試必考題,但是能真正答好此題的人並不多,要麼原理沒吃透,要麼缺乏實戰經驗。

過去半年時間里,我們的廣告系統出現了多次和GC相關的線上問題,有Full GC過於頻繁的,有Young GC耗時過長的,這些問題帶來的影響是:GC過程中的程序卡頓,進一步導致服務超時從而影響到廣告收入。

這篇文章,我將以一個FGC頻繁的線上案例作為引子,詳細介紹下GC的排查過程,另外會結合GC的運行原理給出一份實踐指南,希望對你有所幫助。內容分成以下3個部分:

1、從一次FGC頻繁的線上案例說起

2、GC的運行原理介紹

3、排查FGC問題的實踐指南

01 從一次FGC頻繁的線上案例說起

去年10月份,我們的廣告召回系統在程序上線后收到了FGC頻繁的系統告警,通過下面的監控圖可以看到:平均每35分鐘就進行了一次FGC。而程序上線前,我們的FGC頻次大概是2天一次。下面,詳細介紹下該問題的排查過程。

1. 檢查JVM配置

通過以下命令查看JVM的啟動參數:
ps aux | grep “applicationName=adsearch”

-Xms4g -Xmx4g -Xmn2g -Xss1024K
-XX:ParallelGCThreads=5
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=80

可以看到堆內存為4G,新生代為2G,老年代也為2G,新生代採用ParNew收集器,老年代採用併發標記清除的CMS收集器,當老年代的內存佔用率達到80%時會進行FGC。

進一步通過 jmap -heap 7276 | head -n20 可以得知新生代的Eden區為1.6G,S0和S1區均為0.2G。

2. 觀察老年代的內存變化

通過觀察老年代的使用情況,可以看到:每次FGC后,內存都能回到500M左右,因此我們排除了內存泄漏的情況。

3. 通過jmap命令查看堆內存中的對象

通過命令 jmap -histo 7276 | head -n20

上圖中,按照對象所佔內存大小排序,显示了存活對象的實例數、所佔內存、類名。可以看到排名第一的是:int[],而且所佔內存大小遠遠超過其他存活對象。至此,我們將懷疑目標鎖定在了 int[] .

4. 進一步dump堆內存文件進行分析

鎖定 int[] 后,我們打算dump堆內存文件,通過可視化工具進一步跟蹤對象的來源。考慮堆轉儲過程中會暫停程序,因此我們先從服務管理平台摘掉了此節點,然後通過以下命令dump堆內存:

jmap -dump:format=b,file=heap 7276

通過JVisualVM工具導入dump出來的堆內存文件,同樣可以看到各個對象所佔空間,其中int[]佔到了50%以上的內存,進一步往下便可以找到 int[] 所屬的業務對象,發現它來自於架構團隊提供的codis基礎組件。

5. 通過代碼分析可疑對象

通過代碼分析,codis基礎組件每分鐘會生成約40M大小的int數組,用於統計TP99 和 TP90,數組的生命周期是一分鐘。而根據第2步觀察老年代的內存變化時,發現老年代的內存基本上也是每分鐘增加40多M,因此推斷:這40M的int數組應該是從新生代晉陞到老年代。

我們進一步查看了YGC的頻次監控,通過下圖可以看到大概1分鐘有8次左右的YGC,這樣基本驗證了我們的推斷:因為CMS收集器默認的分代年齡是6次,即YGC 6次后還存活的對象就會晉陞到老年代,而codis組件中的大數組生命周期是1分鐘,剛好滿足這個要求。

至此,整個排查過程基本結束了,那為什麼程序上線前沒出現此問題呢?通過上圖可以看到:程序上線前YGC的頻次在5次左右,此次上線后YGC頻次變成了8次左右,從而引發了此問題。

6. 解決方案

為了快速解決問題,我們將CMS收集器的分代年齡改成了15次,改完后FGC頻次恢復到了2天一次,後續如果YGC的頻次超過每分鐘15次還會再次觸發此問題。當然,我們最根本的解決方案是:優化程序以降低YGC的頻率,同時縮短codis組件中int數組的生命周期,這裏就不做展開了。

02 GC的運行原理介紹

上面整個案例的分析過程中,其實涉及到很多GC的原理知識,如果不懂得這些原理就着手處理,其實整個排查過程是很抓瞎的。

這裏,我選擇幾個最核心的知識點,展開介紹下GC的運行原理,最後再給出一份實踐指南。

1. 堆內存結構

大家都知道: GC分為YGC和FGC,它們均發生在JVM的堆內存上。先來看下JDK8的堆內存結構:

可以看到,堆內存採用了分代結構,包括新生代和老年代。新生代又分為:Eden區,From Survivor區(簡稱S0),To Survivor區(簡稱S1區),三者的默認比例為8:1:1。另外,新生代和老年代的默認比例為1:2。

堆內存之所以採用分代結構,是考慮到絕大部分對象都是短生命周期的,這樣不同生命周期的對象可放在不同的區域中,然後針對新生代和老年代採用不同的垃圾回收算法,從而使得GC效率最高。

2. YGC是什麼時候觸發的?

大多數情況下,對象直接在年輕代中的Eden區進行分配,如果Eden區域沒有足夠的空間,那麼就會觸發YGC(Minor GC),YGC處理的區域只有新生代。因為大部分對象在短時間內都是可收回掉的,因此YGC后只有極少數的對象能存活下來,而被移動到S0區(採用的是複製算法)。

當觸發下一次YGC時,會將Eden區和S0區的存活對象移動到S1區,同時清空Eden區和S0區。當再次觸發YGC時,這時候處理的區域就變成了Eden區和S1區(即S0和S1進行角色交換)。每經過一次YGC,存活對象的年齡就會加1。

3. FGC又是什麼時候觸發的?

下面4種情況,對象會進入到老年代中:

1、YGC時,To Survivor區不足以存放存活的對象,對象會直接進入到老年代。

2、經過多次YGC后,如果存活對象的年齡達到了設定閾值,則會晉陞到老年代中。

3、動態年齡判定規則,To Survivor區中相同年齡的對象,如果其大小之和佔到了 To Survivor區一半以上的空間,那麼大於此年齡的對象會直接進入老年代,而不需要達到默認的分代年齡。

4、大對象:由-XX:PretenureSizeThreshold啟動參數控制,若對象大小大於此值,就會繞過新生代, 直接在老年代中分配。

當晉陞到老年代的對象大於了老年代的剩餘空間時,就會觸發FGC(Major GC),FGC處理的區域同時包括新生代和老年代。除此之外,還有以下4種情況也會觸發FGC:

1、老年代的內存使用率達到了一定閾值(可通過參數調整),直接觸發FGC。

2、空間分配擔保:在YGC之前,會先檢查老年代最大可用的連續空間是否大於新生代所有對象的總空間。如果小於,說明YGC是不安全的,則會查看參數 HandlePromotionFailure 是否被設置成了允許擔保失敗,如果不允許則直接觸發Full GC;如果允許,那麼會進一步檢查老年代最大可用的連續空間是否大於歷次晉陞到老年代對象的平均大小,如果小於也會觸發 Full GC。

3、Metaspace(元空間)在空間不足時會進行擴容,當擴容到了-XX:MetaspaceSize 參數的指定值時,也會觸發FGC。

4、System.gc() 或者Runtime.gc() 被顯式調用時,觸發FGC。

4. 在什麼情況下,GC會對程序產生影響?

不管YGC還是FGC,都會造成一定程度的程序卡頓(即Stop The World問題:GC線程開始工作,其他工作線程被掛起),即使採用ParNew、CMS或者G1這些更先進的垃圾回收算法,也只是在減少卡頓時間,而並不能完全消除卡頓。

那到底什麼情況下,GC會對程序產生影響呢?根據嚴重程度從高到底,我認為包括以下4種情況:

1、FGC過於頻繁:FGC通常是比較慢的,少則幾百毫秒,多則幾秒,正常情況FGC每隔幾個小時甚至幾天才執行一次,對系統的影響還能接受。但是,一旦出現FGC頻繁(比如幾十分鐘就會執行一次),這種肯定是存在問題的,它會導致工作線程頻繁被停止,讓系統看起來一直有卡頓現象,也會使得程序的整體性能變差。

2、YGC耗時過長:一般來說,YGC的總耗時在幾十或者上百毫秒是比較正常的,雖然會引起系統卡頓幾毫秒或者幾十毫秒,這種情況幾乎對用戶無感知,對程序的影響可以忽略不計。但是如果YGC耗時達到了1秒甚至幾秒(都快趕上FGC的耗時了),那卡頓時間就會增大,加上YGC本身比較頻繁,就會導致比較多的服務超時問題。

3、FGC耗時過長:FGC耗時增加,卡頓時間也會隨之增加,尤其對於高併發服務,可能導致FGC期間比較多的超時問題,可用性降低,這種也需要關注。

4、YGC過於頻繁:即使YGC不會引起服務超時,但是YGC過於頻繁也會降低服務的整體性能,對於高併發服務也是需要關注的。

其中,「FGC過於頻繁」和「YGC耗時過長」,這兩種情況屬於比較典型的GC問題,大概率會對程序的服務質量產生影響。剩餘兩種情況的嚴重程度低一些,但是對於高併發或者高可用的程序也需要關注。

03 排查FGC問題的實踐指南

通過上面的案例分析以及理論介紹,再總結下FGC問題的排查思路,作為一份實踐指南供大家參考。

1. 清楚從程序角度,有哪些原因導致FGC?

1、大對象:系統一次性加載了過多數據到內存中(比如SQL查詢未做分頁),導致大對象進入了老年代。

2、內存泄漏:頻繁創建了大量對象,但是無法被回收(比如IO對象使用完后未調用close方法釋放資源),先引發FGC,最後導致OOM.

3、程序頻繁生成一些長生命周期的對象,當這些對象的存活年齡超過分代年齡時便會進入老年代,最後引發FGC. (即本文中的案例)

4、程序BUG導致動態生成了很多新類,使得 Metaspace 不斷被佔用,先引發FGC,最後導致OOM.

5、代碼中顯式調用了gc方法,包括自己的代碼甚至框架中的代碼。

6、JVM參數設置問題:包括總內存大小、新生代和老年代的大小、Eden區和S區的大小、元空間大小、垃圾回收算法等等。

2. 清楚排查問題時能使用哪些工具

1、公司的監控系統:大部分公司都會有,可全方位監控JVM的各項指標。

2、JDK的自帶工具,包括jmap、jstat等常用命令:

查看堆內存各區域的使用率以及GC情況
jstat -gcutil -h20 pid 1000

查看堆內存中的存活對象,並按空間排序
jmap -histo pid | head -n20

dump堆內存文件
jmap -dump:format=b,file=heap pid

3、可視化的堆內存分析工具:JVisualVM、MAT等

3. 排查指南

1、查看監控,以了解出現問題的時間點以及當前FGC的頻率(可對比正常情況看頻率是否正常)

2、了解該時間點之前有沒有程序上線、基礎組件升級等情況。

3、了解JVM的參數設置,包括:堆空間各個區域的大小設置,新生代和老年代分別採用了哪些垃圾收集器,然後分析JVM參數設置是否合理。

4、再對步驟1中列出的可能原因做排除法,其中元空間被打滿、內存泄漏、代碼顯式調用gc方法比較容易排查。

5、針對大對象或者長生命周期對象導致的FGC,可通過 jmap -histo 命令並結合dump堆內存文件作進一步分析,需要先定位到可疑對象。

6、通過可疑對象定位到具體代碼再次分析,這時候要結合GC原理和JVM參數設置,弄清楚可疑對象是否滿足了進入到老年代的條件才能下結論。

04 最後的話

這篇文章通過線上案例並結合GC原理詳細介紹了FGC的排查過程,同時給出了一份實踐指南。

後續會以類似的方式,再分享一個YGC耗時過長的案例,希望能幫助大家吃透GC問題排查,如果覺得本文對你有幫助,請大家關注我的個人公眾號!

– End –

作者簡介:程序員,985碩士,前亞馬遜Java工程師,現58轉轉技術總監。持續分享技術和管理方向的文章。如果感興趣,可微信掃描下面的二維碼關注我的公眾號:『IT人的職場進階』

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

3dTiles 數據規範詳解[1] 介紹

版權:轉載請帶原地址。https://www.cnblogs.com/onsummer/p/12799366.html @秋意正寒

Web中的三維

html5和webgl技術使得瀏覽器三維變成了可能。

巧婦難為無米之炊,三維數據(三維模型)是三維可視化重要的一環,事實上就是:三維數據眾多,行業跨界廣。

參考資料:http://www.bgteach.com/article/132

three.js的各種加載器實現了大部分通用三維格式的加載,屏蔽了格式不同的數據結構差異。

然而,這樣還是不能滿足日益增長的效果需求,比如場景一大,模型文件體積變大,解析所耗費的時間越來越長。

webgl,包括所有gpu有關的圖形渲染編程,幾乎只認這樣的三維數據:頂點、頂點顏色、頂點法線、着色語言…

所以,三維圖形界的通用格式:glTF應運而生,它面向終點,它按照圖形編程所需的格式來存儲數據,藉以二進制編碼提高傳輸速度。

它不再使用面向對象的思維存儲三維模型、貼圖紋理,而是按顯卡的思維存儲,存的是頂點、法線、頂點顏色等最基礎的信息,只不過組織結構上進行了精心的設計。

它面向終點,就意味着可編輯性差,因為渲染性能的提高犧牲了可編輯性,它不再像3ds、dae甚至是max、skp一樣容易編輯和轉換。

事實上,大多數三維軟件提供了glTF格式的轉換,或多一步,或一步到位。

地理真三維

早年,地理的三維還處於地形三維上,即数字高程模型(DEM)提供地表的高度拉伸。柵格高程數據、等高線、不規則三角網等均是数字高程模型的具體案例。
下圖是不規則三角網,也即所謂的三角面片(圖形渲染中很常見):

隨着學科的融合、計算機技術和硬件的更新換代,使得有模型、有細節的真三維融入到GIS中成為了可能,或者說,計算機技術和硬件的升級,給GIS以更廣闊的視角觀察世界。

cesium.js 號稱是 webgl 封裝的三維地理庫,是支持 gltf 模型的加載的。

面對大規模精細三維數據的加載,還要照顧到GIS的各種坐標系統、分析計算,gltf這種單個模型的方案顯得力不從心。

2016年,Cesium 團隊借鑒傳統2DGIS的地圖規範——WMTS,借鑒圖形學中的層次細節模型,打造出大規模的三維數據標準—— 3d-Tiles,中文譯名:三維瓦片。

它在模型上利用了 gltf 渲染快的特點,對大規模的三維數據進行組織,包括層次細節模型、模型的屬性數據、模型的層級數據等。

3dTiles的設計思想

3dTiles 繼承了 gltf 的優點:貼合圖形渲染 API 的邏輯,討 GPU 喜愛,webgl 對其內部組織起來的三維模型數據,不需要轉換,可以直接渲染(glTF 的功勞)。

關於 glTF 是如何嵌入到 3dTiles 中的,開篇不談,後續精講。

我們區分一組概念:規範和實現。

3dTiles 是一種規範,在規範的指導下,各種資源文件可以是獨立存在於硬盤中的目錄、文件,也可以以二進制形式寫入數據庫中。目前,3dTiles 的官方實現只有 “散列文件”,也就是文件、文件夾的形式存儲在硬盤中,有關如何存儲到數據庫中的討論,官方仍在進行中(截至發博客)。

glTF 也是一種規範,它的數據文件不一定就是後綴名為 .gltf 的文件,也不一定只有一個文件(glTF 的文件還可以是二進制文件、紋理貼圖文件等,扯遠了哈)。
在本文,會嚴格指明是數據還是數據標準,如果我說的是 “XXX文件(例如 Bird.glb 文件)” ,那就是在指特定的文件。

3dTiles還有一個特點:那就是不記錄模型數據,只記錄各級“Tile”的邏輯關係,以及“Tile”自己的屬性信息。所謂的模型數據,是指三維模型的頂點、貼圖材質、法線、顏色等信息。邏輯關係是指,各級Tile是如何在空間中保持連續的,LOD是如何組織的。屬性信息就很簡單啦,門有門的生產商,窗戶有窗戶的使用年限等,往大了說,建築還有它自己的壽命、法人、施工單位等屬性信息。

3dTiles的特點總結如下:

  • 三維模型使用了 glTF 規範,繼承它的渲染高性能
  • 除了嵌入的 glTF,3dTiles 自己 只記錄各級Tile的空間邏輯關係(如何構成整個3dtiles)和屬性信息,以及模型與屬性如何掛接在一起的信息

我覺得你還是雲里霧裡的,下一節將展示3dTiles具體數據,說說3dTiles的組織結構,說說3dTiles中的”Tile”,也就是“三維瓦片數據”中的“瓦片”是什麼。

3dTiles系列博客最終目錄:

01 引入與博客目錄:3dTiles 數據規範詳解

02 Tileset與Tile

03 內嵌在瓦片文件中的兩大數據表

04.1 B3dm 類型

04.2 I3dm 類型

04.3 Pnts 類型

04.4 Cmpt 類型

04.5 未發布的瓦片規範

05 3dTiles強大的擴展能力

06 優缺點

07 與I3S比較

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心