質量硬,性價比無敵,懂車的都說這3款車比BBA更值得買

點評:放在以前,皇冠就是身份的象徵,也算是款足以媲美百萬級別車輛的車子,隨着諸多豪華品牌的入駐以及價格的降低,市場份額確實也受到了一定的衝擊。但秉承豪華穩重路線的18款皇冠在外觀和內飾上進行了年輕化,這一性格上的變化致使皇冠既穩住了老用戶也吸引了不少年輕的消費者。

在中國,汽車市場有個很奇妙的現象:有時候沒有競爭優勢的車型,卻成為銷量榜單上前幾名的常客;而有些產品競爭力很強的車型,卻好似不怎麼如意。但今天要為大家揭曉的是那些銷量看似不怎樣,在細分市場卻有不錯表現的特殊車型。

在進行分析點評之前,我們首先來看看本次分析的那些車子在今年前3個月的銷量指數。

點評:金牛座是福特品牌旗下的一款高端轎車車型,雖非屬豪華品牌但售價快30萬的它,素質可謂非常的高,大氣又低調的外觀配以變態級別的配置,以絕對性的優勢足以和豪華品牌扳手腕。但面對豪華品牌和同級車大眾的帕薩特和邁騰在20幾萬區間所帶來的巨大壓力,在今年1-3月的總銷量中金牛座還是頂住了壓力,賣出了4778輛。就性價比而言,金牛座還是很高的,就是售價有點偏高,如若售價能稍微降低一些,並配以一些購車優惠,相信在接下來的日子里,金牛的銷售表現會更令人滿意。

點評:放在以前,皇冠就是身份的象徵,也算是款足以媲美百萬級別車輛的車子,隨着諸多豪華品牌的入駐以及價格的降低,市場份額確實也受到了一定的衝擊。但秉承豪華穩重路線的18款皇冠在外觀和內飾上進行了年輕化,這一性格上的變化致使皇冠既穩住了老用戶也吸引了不少年輕的消費者。況且,皇冠本身就擁有不錯的綜合性價比和情懷感,並不會比BBA車型差,如果30萬讓消費者選擇購車,相信不少消費者都會選擇這些開起來更有格調又有品味的車子。

點評:非豪華品牌的輝昂和奧迪A7、A8等車型屬同平台打造,帶點奧迪影子,並帶有眾多賣點的輝昂在實力上絲毫也不比豪華品牌同級別車型遜色,在國內市場大眾也有不少專屬中國的車子,藉助大眾的品牌影響,輝昂這款車的知名度也有相應的提升。另外從市場行情來說,雖說中大型車的市場仍舊是豪華品牌車的天下,但是大街上千篇一律的豪華品牌車也給了輝昂一定的機遇,憑藉過硬的實力在這个中大型車的市場中還是分得一杯羹。

總結

金牛座、皇冠、輝昂三款車都很好,性價比也很不錯,在整個細分市場上表現還算是差強人意!其實吧,覺得金牛座這些怎麼也是比3系和C級車要高上一個等級,轉而有不少的消費者放棄BBA車型,選擇這些二線豪華品牌也不足為奇。畢竟與其選擇開着分分鐘與人撞車的BBA上街,不如選擇這些二線豪華品牌車,開起來更有格調和氣質。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

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

教授試了一款不到10萬的智能SUV?

1英寸的中控大屏清晰度和靈敏度都表現不錯,並且還加入了車聯網功能,通過語音可以控制車輛大多數的功能,例如開關天窗,開關音樂等,方便車主日常用車生活,同時增加行車安全。整車的配置豐富,前排座椅加熱,車道偏離預警,盲區監測,全景攝像頭,ESp車身穩定系統應有盡有,這也是自主品牌的優勢之一。

五月份的青島是夏至未至的時分,海風輕拂過臉頰,聞着少許帶有魚腥味的海風,來到青島試駕2018款凱翼X3,雖然說凱翼是新興品牌,但並不代表車輛的競爭力低,到底細節如何?產品力怎麼樣?為你一一解讀。

外觀上,2018款凱翼X3與2017款區別不大,U型鍍鉻條圍繞着進去格柵,並與前大燈相結合,配合上半圓形行車燈圍繞的透鏡大燈,雖說是鹵素光源,但透鏡的加入讓整車的精神氣更上一層樓。

雙色的輪圈時尚感很強,215/60 R17的輪胎尺寸,有足夠厚的胎壁來面對各種複雜路況,符合它SUV的定位,高配車型為18寸輪圈,視覺效果更好,但濾震效果不及17寸輪胎。

車尾的造型是凱翼X3的亮點之一,小書包造型的外掛備胎,讓整輛車的野性十足,側開的開門方式也個性十足;尾燈的造型與頭燈呼應,同是半圓形,點亮后視覺效果滿分。

內飾有兩種配色:全黑/黑棕,中控台總體以對稱設計為主,主要突出年輕,簡約化,中控台上方使用的是軟質材料,銀色的飾條讓整个中控台的層次感增加了不少,圓形的空調出風口加上周圍的銀色裝飾,增添了內飾的一絲活力。10.1英寸的中控大屏清晰度和靈敏度都表現不錯,並且還加入了車聯網功能,通過語音可以控制車輛大多數的功能,例如開關天窗,開關音樂等,方便車主日常用車生活,同時增加行車安全。

整車的配置豐富,前排座椅加熱,車道偏離預警,盲區監測,全景攝像頭,ESp車身穩定系統應有盡有,這也是自主品牌的優勢之一。

主駕的座椅支持6向手動調節,座椅皮質的手感雖然不算特別好,比上不足比下有餘。第二排座椅支持4/6放倒,不過放倒以後上下地台差別大,不利於大物件裝載,側開門也帶來了一個缺點,往右開的側開門方式不太適合中國國情使用,因為我國道路是靠右行駛,一般情況下是靠右停車,向右開門的話會阻礙物品的裝載以及影響安全。

2018款凱翼X3全系標配1.6L自然吸氣發動機,智享版以上配備CVT變速箱(智享版一下為手動),發動機最大馬力126ps,最大扭矩160N*m,從數據上看,整體的功率不算大,由於CVT變速箱的特性,整體動力輸出以平順為主調,2000rpm一下車輛表現慵懶,2000rpm以上稍有起色,如果想要在高速上快速超車,需要增加多一點提前量。3000rpm以上發動機的噪音又會相對明顯,總體來說動力總成在夠用的範圍。

方向盤轉向力度有三擋可調,轉向有三種重量,日常使用個人比較喜歡最輕模式,減輕勞動強度。

凱翼X3使用前麥弗遜后雙連桿結構,整體調校毫無意外地偏舒適,在車輛經過減速帶等快速的凸起時,會比較积極地過濾震動,但遇到波浪型或較大拋跳的路面時,懸挂就顯得有點無所適從,且懸挂偏向支撐,所以傳到車內的震動會相對大一點,給人一絲運動感。

總結

凱翼X3的外形和內飾都是為了迎接年輕人圍目的,2018款凱翼X3將該理念貫徹得更加徹底,並且增加了車聯網系統,進入如今時髦的互聯網汽車行列,即使將價格控制在10萬以下,ESp、盲區監測、車道偏離預警系統等實用的配置一樣不落,動力和操控不是凱翼的長處,但好在空間濾震都不錯,總體中規中矩,沒有什麼短板,是年輕人第一輛車,又需要一定實用性不錯的選擇。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

曾經的銷量強者,三十而立能否再度發力?

整個內飾最吸睛的地方是方向盤,兩輻式設計的方向盤尺寸不算小,但是會感覺有點奇怪,喇叭蓋板是從下往上包,而不是一般的從上往下,而且整個設計形狀有點彆扭,需要一點時間去適應,具介紹稱整個形狀調轉一百八十度就是跟中控面板的形狀一致的,好像是這樣,但是打滿一百八十度的感覺會更怪,這算是個彩蛋。

說起海馬這個品牌,是需要花一點時間去回憶起來,然後感覺到有點惋惜,近幾年自主品牌突飛猛進,很多都發展到令人佩服的地步,眾多車型充斥着各大主流市場,而海馬則沒能成為其中之一,對於第一代的福美來記憶猶新,年銷量可達10萬輛,而現在已經不堪回首。

海馬從1988建立至今已經30年了,福美來再推新車型,那麼在而立之年,這款新車有着怎樣的表現呢?它對比自身有什麼突破,對比對手競爭力又在哪裡呢?前往海南三亞,一探究竟。

外觀:這大概是最好看的福美來了

這裏需要提一下福美來家族的其他車型,雖然臉和顏值這個是比較主觀的東西,但客觀地講,福美來家族的外觀設計比起同級競品,是存在一定差距的,譬如福美來MpV和福美來F7,兩款車其實是兄弟車型,只是兩款車型採用了不同的設計。

福美來MpV的設計是比較年輕、張揚一點,外觀最奪目的就是前臉的進氣格柵和大燈的設計,大量的豎立鍍鉻飾條,對於顏值高低保留意見,而F7採用了橫向的鍍鉻飾條,看起來更居家一點,而大燈的造型是一樣的,相比福美來MpV,我更容易接受福美來F7。

福美來F5定位緊湊型家轎,車身尺寸和福美來大體一致,可以看做是兄弟車型,長寬高分別是4698*1805*1477(mm),軸距2685mm,只是寬度差了1mm。

尺寸相同,但是前臉比起福美來,就如福美來F7之於福美來MpV,採用差異化的設計風格,更居家的設計讓更容易接受一些,最大的變化在於前進氣格柵,相比起福美來那粗大的橫飾條,F5採用了滿天星前臉格柵,據介紹星星的形狀是用上下翻轉重合的兩個海馬車標合成的這麼一個設計理念,一眼看上去的話是比較有辨識度。

大燈的形狀其實是跟福美來一致的,試駕的是高配車型,全系採用鹵素光源,有加透鏡。

LED日行燈手動擋和自動擋的頂配才有配備,布置在最外側,霧燈靠裡邊。

對比起福美來也是有一點區別的,福美來沒有LED日行燈,兩側進氣口的裝飾都不一樣,比起它比較犀利的兩個尖角裝飾,F5的這個設計明顯更大氣,有橫向拉伸感。

個人覺得側面是F5最大的看點,上腰線斷開若隱若現,下腰線一直延伸至尾部,非常有動感,特別是寶藍色的車色,特別騷,配合三亞藍天、藍海、藍車,完美。

換一個角度看着車身側面的腰線,簡直賞心悅目。

高配車型配備無鑰匙進入和無鑰匙啟動,這個價位的車型來說,應該是值得肯定的表現。

電動調節后視鏡為全系標配,不過後視鏡加熱/自動摺疊這些就沒有了。

前後都採用盤剎制動,標配205 /55 R16的輪胎,輪胎是韓泰的,輪轂形狀也是比較運動年輕的設計。

車尾也有可圈可點之處,其實可以看到尾部是想奔馳C級的那樣翹起一個小尾巴,營造出不錯的動感。

個人覺得尾燈的設計比前大燈更具協調感,雙邊共雙出的排氣是裝飾的,視覺效果還想;試駕車型是頂配車型,配備后駐車雷達和倒車影像,除了最低配以外,其餘車型均為標配項,比較良心。

內飾設計有點“皮”&配置基本夠用

內飾和福美來相比完全是兩個設計風格,但是有幾個地方讓人覺得設計師有點“調皮”,整體的設計風格比較居家簡約,把空調按鍵布置在空調下方,是跟奔馳相似的做法,中控用料基本都是硬塑料,相信這個價位是無功無過吧。

整個內飾最吸睛的地方是方向盤,兩輻式設計的方向盤尺寸不算小,但是會感覺有點奇怪,喇叭蓋板是從下往上包,而不是一般的從上往下,而且整個設計形狀有點彆扭,需要一點時間去適應,具介紹稱整個形狀調轉一百八十度就是跟中控面板的形狀一致的,好像是這樣,但是打滿一百八十度的感覺會更怪,這算是個彩蛋?設計師很皮。

方向盤用的是液壓助力,尺寸比較大,但是形狀不粗,反而比較細,握感不飽滿,8點、4點鐘方向其實是設計有手指凹紋的,方便這樣操作

只有一側有功能按鍵,右側是空的,這樣給人感覺欠妥,或許日後會有功能加上?

另外是一鍵啟動的位置,把它布置在空調按鍵下方,說不上市奇特,但是不常見,另外更不常見的是居然有配備無線充電,不過是頂配車型才有。

儀錶盤是中規中矩,雙圓設計比較常見,然後加入了石英錶設計元素。

配備8英寸中控屏,Carplay/CarLife手機互聯、藍牙連接,都是除最低配以外其餘車型配備,高配車型還有車載導航。

ESp和电子手剎也是除最低配以外其餘車型配備,另外,這裏的後備廂開啟鍵目前找到的唯一一個打開方式,似乎找不到其他方式打開後備廂,這多少會有點不便,比如買完東西準備放後備廂,只能先打開車門然後再打開後備廂最後把東西拿到後備廂放進去。

空間方面,體驗者身高為1.75米,在調整好駕駛坐姿后,在後排有接近2拳腿部空間,頭部空間一拳由於,表現還是不錯的。

有點不足的是作為一輛前驅車,中央地板的凸起高度比較高,而且只有兩個頭枕,這樣的話後排坐3個人中間那個就會有點難受。

後備廂官方容積為415L,不算大,但縱深比較深,有點不足是第二排座椅不能放到,中間也不能把扶手拉下來從後備廂拿東西,這個有待改進,另外再提一下,後備廂真的應該加上一個打開按鈕或者拉扣。

好開舒服,底盤基本功到位是最大的優勢

F5搭載和福美來一樣的1.6L發動機,最大馬力125匹,在6000轉才能爆發十成功力,匹配5擋手動和6擋手自一體變速箱,發動機採用全鋁發動機,最大扭矩151N·m,峰值扭矩從四千轉后開始輸出,作為一款自吸發動機來說,賬面數據也就這樣了。

在習慣了渦輪浪潮下,開着這裏僅有125匹馬力的轎車真的感覺到不少差距,在市區或者說中低速路段行駛其實還算夠用,超個車提提速什麼的都沒什麼大問題,但是上了高速的話真的顯得有點佛系了,即使地板油猛催它提速依然是比較慢的,讓一度以為是不是忘記鬆手剎?不對呀,电子的。

還需要挑刺的是深踩油門發動機提速起來噪音抑制的一般般,轟鳴聲也會傳到車內,不過在6AT變速箱兢兢業業的工作下,時刻都會抓住油門的動作,只要有較大的動力需求就會立馬提速,總的來說平順性已經響應性都是不用擔心的,只不過提速真的急不來,畢竟一輛家轎,滿足平順的駕駛體驗以及夠用的動力就已經很高分了。

接下來都是F5值得肯定的點,首先是油門和剎車,這是首先感覺到的有點,兩者調教的很線性,而且很均勻,不會像日系或者其他車型前半段或者前三分之一要麼一下子用力過猛要麼就基本沒反應,特別是剎車,做得很線性,加上前後盤剎,踩得越深是越有勁,感覺怎麼樣都能剎得住,這給日常駕駛帶來了足夠信心。

而且這個油門配上自吸天生的特性,真的是想要多少踩多少,不像渦輪發動機那樣猛的沖一下讓人覺得措手不及,就這兩個方面就能給比較好的影像。

前懸架是麥弗遜獨立懸架,后懸架是雙E型多連桿獨立懸挂,后懸都是獨立的再這個級別當中可以說是相當厚道了,很多十來萬的都是板車呢,所以F5開起來路過一些顛簸路面,它可以處理得游刃有餘,完全沒有廉價感,濾震是做的很到位,是一款稱職的家用車。

另外底盤的紮實感以及對轉彎、變道的側傾做的是比較不錯的,整個底盤很整、很穩,跑了半天高速深有體會,時速接近120km/h,整個車身一點都不飄,甚至有點大眾那樣的感覺,而且在一些中高速的變道或者是超車,就完全不像一款幾萬塊的車的表現,應該像十來萬或者以上,側傾小,車身姿態平穩,底盤基本功沒丟,有祖傳的功力。

總的來說,F5最大的優勢在於底盤的質感以及整個偏向紮實的調教,但是不足依然很多,小缺點依然是存在一部分的,另外最大的爭議點或許是外形設計和內飾,這個跟同級競品相比確實有待提高,海馬或許是不想隨波逐流,保留自己的特色,但是覺得,有很多東西是可以借鑒一下的,即便順應大部分消費者的審美口味也可以做出自己的特色,最重要的好事賣得好。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

8萬就夠,近期最多人關注的3款6座車都這麼給力,買誰好?

而寶駿360第三排的空間表現不錯,美中不足的是其坐墊對於大腿的承托稍顯不足。比亞迪宋MAX 6座版表現要比7座版本更為靈活和更為寬鬆,不管前排、中排還是後排都能夠找到舒適輕鬆的坐姿。扒一扒內飾寶駿360:中控檯布局與寶駿510相似,整體內飾層次感較為豐富,配合懸浮式中控屏以及液晶儀錶盤,整個內飾顯得科技感十足。

什麼車廠最了解國內消費者的需要?答案必定是本土車廠。想必大家也知道2018車輛年檢時間有新規定,非營運轎車享有6年內免上線檢測政策,可尷尬的是現在許多MpV車型是7座的布局,因此許多MpV被迫要一年一檢。對此本土車廠紛紛為消費者排憂解難,相繼推出了6座MpV車型,讓新車可以滿足新規從而6年內免上線檢測。接下來我們看看最近新上市的3款6座MpV吧。

寶駿作為一個善於打造爆款車型的品牌,不僅能打造出高性價比的車型,更重要的是它清晰當今消費者需求,上周剛推出了旗下第二款MpV車型——寶駿360,該車瞄準的就是6座MpV這一細分市場。

全新的歐尚A600是長安繼歐尚A800后又一力作,從命名來看,其定位是在A800之下。從外觀跟內飾看,歐尚A600可作為歐尚的升級改款。相比於原本的歐尚,多了一種動力組成,也多了自動擋車型的選擇,這樣的變化讓歐尚A600更加的親民。

作為爆款車型的比亞迪宋MAX也推出了六座車型,可見比亞迪對於六座MpV這一細分市場的重視程度。新車在尺寸上保持與7座車型一致,減少了一個座位,換來的是更寬敞的中排空間以及能更方便進出第三排的中央通道。

比一比尺寸

通過三車尺寸數據的對比可以看出比亞迪宋MAX佔據一定的優勢,尤其是在軸距以及寬度方面。由此可以推斷出比亞迪宋MAX的車內空間表現應為三車之最。寶駿360在尺寸上比歐尚A600稍微大一些,可是軸距上的差距是明顯的,其軸距已貼近宋MAX,換句話說寶駿360在車廂內的空間不比宋MAX差太多。

經過實測也驗證了前文的觀點,可以看到,三台車前排空間是足夠寬敞。受限於軸距,長安歐尚A600在第二排以及第三排在腿部空間上稍顯局促,應付日常短途出行問題不大。而寶駿360第三排的空間表現不錯,美中不足的是其坐墊對於大腿的承托稍顯不足。比亞迪宋MAX 6座版表現要比7座版本更為靈活和更為寬鬆,不管前排、中排還是後排都能夠找到舒適輕鬆的坐姿。

扒一扒內飾

寶駿360:中控檯布局與寶駿510相似,整體內飾層次感較為豐富,配合懸浮式中控屏以及液晶儀錶盤,整個內飾顯得科技感十足。

長安歐尚A600:內飾與歐尚相似,連貫式儀錶和中控台是一討喜的設計,細節部分歐尚A600與歐尚有着細微的差別,比如空調出風口飾板的微調以及空調控制區域按鈕的布局,這些微調使歐尚A600較於歐尚更為精緻。

比亞迪宋MAX:6座版本內飾布局與7座完全一致,在內飾的配色上新增了黑+棕色的版本,使整個內飾看上去不再沉悶。

瞧一瞧動力

動力方面,寶駿360搭載與寶駿310系列相同的1.5L自然吸氣發動機,最大輸出功率82kW,和發動機相匹配是6速手動和自動擋。長安歐尚A600動力配置上較為豐富,提供了1.5L和1.6L兩種動力選擇,此外,新車在1.6L車型上均匹配了六速自動變速箱,從而提高了車型的競爭力。比亞迪宋MAX則是熟悉的1.5T渦輪增壓發動機匹配6速手動變速箱或6速雙離合變速箱。

總結:以上三台車都是本土車廠為滿足國內消費者需要而交出的答卷。可以看出歐尚A600與寶駿360均走高性價比路線,歐尚A600自動變速箱是其一個優勢,同樣的價格區間內寶駿360有空間的優勢。相比之下,比亞迪宋MAX6座版整體配置是跟7座版本看齊,更多的可以將其看作為順應新政策而在座椅數上作出的妥協,不過也因為少了一個座,車內的空間表現將比7座版本更好。以上就是最近新上市的三款六座MpV至於要怎麼選,相信各位心中自有答案。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※超省錢租車方案

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

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

【Flutter實戰】圖片組件及四大案例

老孟導讀:大家好,這是【Flutter實戰】系列文章的第三篇,這一篇講解圖片組件,Image有很多高級用法,希望對您有所幫助。

圖片組件是Flutter基礎組件之一,和文本組件一樣必不可少。圖片組件包含Image和Icon兩個組件,本質上Icon不屬於圖片組件,但其外形效果上類似於圖片。

在項目中建議優先使用Icon組件,Icon本質上是一種字體,只不過显示的不是文字,而是圖標,而Image組件先通過圖片解碼器將圖片解碼,所以Icon有如下優點:

  • 通常情況下,圖標比圖片體積更小,顯著的減少App包體積。
  • 圖標不會出現失真或者模糊的現象,例如將20×20的圖片,渲染在200×200的屏幕上,圖片會失真或模糊,而圖標是矢量圖,不會失真,就像字體一樣。
  • 多個圖標可以存放在一個文件中,方便管理。
  • 全平台通用。

Image

Image組件用於显示圖片,圖片的來源可以是網絡、項目中圖片或者設備上的圖片。

加載網絡圖片:

Image.network(
  'http://pic1.win4000.com/pic/c/cf/cdc983699c.jpg',
)

加載項目中圖片:

首先將圖片拷貝到項目中,通常情況下,拷貝到assets/images/目錄下,assets/images/目錄為手動創建,新建的項目默認是沒有此目錄的。

設置pubspec.yaml配置文件:

assets:
  - assets/images/

或者指定具體圖片的名稱:

assets:
  - assets/images/aa.jpg

通常情況下,使用第一種方式,因為圖片會有很多張,增加一張就這裏配置一個太麻煩。

注意:assets前面的空格問題,極容易引發編譯異常,正確格式如下:

加載圖片:

Image.asset('assets/images/aa.jpg')

加載設備上的圖片:

要加載設備(手機)上的圖片首先需要獲取設備圖片的路徑,由於不同平台的路徑不同,因此路徑的獲取必須依靠原生支持,如果了解原生(Android和iOS)開發,可以直接使用MethodChannel獲取路徑,如果不懂原生(Android和iOS)開發,可以使用第三方插件獲取路徑,這裏推薦官方的path_provider

加載設備上的圖片:

Image.file(File('path'))

設置圖片的大小:

Image.asset('assets/images/aa.jpg',width: 100,height: 200,),

當Image的大小和圖片大小不匹配時,需要設置填充模式fit,設置組件大小為150×150,

Container(
  color: Colors.red.withOpacity(.3),
  child: Image.asset('assets/images/aa.jpg',width: 150,height: 150),
)

看到,圖片左右兩邊有空白區域(淺紅色填充的區域),如果想要圖片充滿整個區域,設置如下:

Container(
  color: Colors.red.withOpacity(.3),
  child: Image.asset('assets/images/aa.jpg',width: 150,height: 150,fit: BoxFit.fill,),
)

雖然圖片充滿整個區域,但圖片變形了,使圖片等比拉伸,直到兩邊都充滿區域:

Container(
  color: Colors.red.withOpacity(.3),
  child: Image.asset('assets/images/aa.jpg',width: 150,height: 150,fit: BoxFit.cover,),
)

此時,圖片未變形且兩邊都充滿區域,不過圖片被裁減了一部分。

fit參數就是設置填充方式,其值介紹如下:

  • fill:完全填充,寬高比可能會變。
  • contain:等比拉伸,直到一邊填充滿。
  • cover:等比拉伸,直到2邊都填充滿,此時一邊可能超出範圍。
  • fitWidth:等比拉伸,寬填充滿。
  • fitHeight:等比拉伸,高填充滿。
  • none:當組件比圖片小時,不拉伸,超出範圍截取。
  • scaleDown:當組件比圖片小時,圖片等比縮小,效果和contain一樣。

BoxFit.none的裁減和alignment相關,默認居中,

Image.asset(
  'assets/images/aa.jpg',
  width: 150,
  height: 150,
  fit: BoxFit.none,
  alignment: Alignment.centerRight,
),

左邊為原圖。

設置對齊方式:

Container(
  color: Colors.red.withOpacity(.3),
  child: Image.asset(
    'assets/images/aa.jpg',
    width: 150,
    height: 150,
    alignment: Alignment.centerLeft,
  ),
),

colorcolorBlendMode用於將顏色和圖片進行顏色混合,colorBlendMode表示混合模式,下面介紹的混合模式比較多,瀏覽一遍即可,此屬性可以用於簡單的濾鏡效果。

  • clear:清楚源圖像和目標圖像。
  • color:獲取源圖像的色相和飽和度以及目標圖像的光度。
  • colorBurn:將目標的倒數除以源,然後將結果倒數。
  • colorDodge:將目標除以源的倒數。
  • darken:通過從每個顏色通道中選擇最小值來合成源圖像和目標圖像。
  • difference:從每個通道的較大值中減去較小的值。合成黑色沒有效果。合成白色會使另一張圖像的顏色反轉。
  • dst:僅繪製目標圖像。
  • dstATop:將目標圖像合成到源圖像上,但僅在與源圖像重疊的位置合成。
  • dstIn:显示目標圖像,但僅显示兩個圖像重疊的位置。不渲染源圖像,僅將其視為蒙版。源的顏色通道將被忽略,只有不透明度才起作用。
  • dstOut:显示目標圖像,但僅显示兩個圖像不重疊的位置。不渲染源圖像,僅將其視為蒙版。源的顏色通道將被忽略,只有不透明度才起作用。
  • dstOver:將源圖像合成到目標圖像下。
  • exclusion:從兩個圖像的總和中減去兩個圖像的乘積的兩倍。
  • hardLight:調整源圖像和目標圖像的成分以使其適合源圖像之後,將它們相乘。
  • hue:獲取源圖像的色相,以及目標圖像的飽和度和光度。
  • lighten:通過從每個顏色通道中選擇最大值來合成源圖像和目標圖像。
  • luminosity:獲取源圖像的亮度,以及目標圖像的色相和飽和度。
  • modulate:將源圖像和目標圖像的顏色分量相乘。
  • multiply:將源圖像和目標圖像的分量相乘,包括alpha通道。
  • overlay:調整源圖像和目標圖像的分量以使其適合目標后,將它們相乘。
  • plus:對源圖像和目標圖像的組成部分求和。
  • saturation:獲取源圖像的飽和度以及目標圖像的色相和亮度。
  • screen:將源圖像和目標圖像的分量的逆值相乘,然後對結果求逆。
  • softLight:對於低於0.5的源值使用colorDodge,對於高於0.5的源值使用colorBurn。
  • src:放置目標圖像,僅繪製源圖像。
  • srcATop:將源圖像合成到目標圖像上,但僅在與目標圖像重疊的位置合成。
  • srcIn:显示源圖像,但僅显示兩個圖像重疊的位置。目標圖像未渲染,僅被視為蒙版。目標的顏色通道將被忽略,只有不透明度才起作用。
  • srcOut:显示源圖像,但僅显示兩個圖像不重疊的位置。
  • srcOver:將源圖像合成到目標圖像上。
  • xor:將按位異或運算符應用於源圖像和目標圖像。

是不是感覺看了和沒看差不多,看了也看不懂。正常,估計只有學過視覺算法的才能看懂吧,直接看下各個屬性的效果吧:

repeat表示當組件有空餘位置時,將會重複显示圖片

Image.asset(
  'assets/images/aa.jpg',
  width: double.infinity,
  height: 150,
  repeat: ImageRepeat.repeatX,
)

重複的模式有:

  • repeat:x,y方向都充滿。
  • repeatX:x方向充滿。
  • repeatY:y方向充滿。
  • noRepeat:不重複。

matchTextDirection設置為true時,圖片的繪製方向為TextDirection設置的方向,其父組件必須為Directionality

Directionality(
    textDirection: TextDirection.rtl,
    child: Image.asset(
      'assets/images/logo.png',
      height: 150,
      matchTextDirection: true,
    )),

左邊為原圖,效果是左右鏡像。

filterQuality表示繪製圖像的質量,從高到低為:high->medium->low->none。越高效果越好,越平滑,當然性能損耗越大,默認是low,如果發現圖片有鋸齒,可以設置此參數。

當加載圖片的時候回調frameBuilder,當此參數為null時,此控件將會在圖片加載完成后显示,未加載完成時显示空白,尤其在加載網絡圖片時會更明顯。因此此參數可以用於處理圖片加載時显示佔位圖片和加載圖片的過渡效果,比如淡入淡出效果。

下面的案例是淡入淡出效果:

Image.network(
  'https://flutter.github.io/assets-for-api-docs/assets/widgets/puffin.jpg',
  frameBuilder: (BuildContext context, Widget child, int frame,
      bool wasSynchronouslyLoaded) {
    if (wasSynchronouslyLoaded) {
      return child;
    }
    return AnimatedOpacity(
      child: child,
      opacity: frame == null ? 0 : 1,
      duration: const Duration(seconds: 2),
      curve: Curves.easeOut,
    );
  },
)

loadingBuilder參數比frameBuilder控制的力度更細,可以獲取圖片加載的進度,下面的案例显示了加載進度條:

Image.network(
    'https://flutter.github.io/assets-for-api-docs/assets/widgets/puffin.jpg',
    loadingBuilder: (BuildContext context, Widget child,
        ImageChunkEvent loadingProgress) {
  if (loadingProgress == null) {
    return child;
  }
  return Center(
    child: CircularProgressIndicator(
      value: loadingProgress.expectedTotalBytes != null
          ? loadingProgress.cumulativeBytesLoaded /
              loadingProgress.expectedTotalBytes
          : null,
    ),
  );
})

centerSlice用於.9圖,.9圖用於拉伸圖片的特定區域,centerSlice設置的區域(Rect)就是拉伸的區域。.9圖通常用於控件大小、寬高比不固定的場景,比如聊天背景圖片等。

Container(
    width: 250,
    height: 300,
    decoration: BoxDecoration(
        image: DecorationImage(
            centerSlice: Rect.fromLTWH(20, 20, 10, 10),
            image: AssetImage(
              'assets/images/abc.jpg',
            ),
            fit: BoxFit.fill))),

上面為原圖,下面為拉伸的圖片。

在使用時大概率會出現如下異常:

這是由於圖片比組件的尺寸大,如果使用centerSlice屬性,圖片必須比組件的尺寸小,一般情況下,.9圖的尺寸都非常小。

Icon

Icon是圖標組件,Icon不具有交互屬性,如果想要交互,可以使用IconButton。

Icon(Icons.add),

設置圖標的大小和顏色:

Icon(
  Icons.add,
  size: 40,
  color: Colors.red,
)

上面的黑色為默認大小和顏色。

Icons.add是系統提供的圖標,創建Flutter項目的時候,pubspec.yaml中默認有如下配置:

所有的圖標在Icons中已經定義,可以直接在源代碼中查看,也可以到官網查看所有圖標。

所有圖標效果如下:

案例

聊天背景(.9圖實現)

Container(
  width: 200,
  padding: EdgeInsets.only(left: 8,top: 8,right: 20,bottom: 8),
  decoration: BoxDecoration(
      image: DecorationImage(
          centerSlice: Rect.fromLTWH(20, 20, 1, 1),
          image: AssetImage(
            'assets/images/chat.png',
          ),
          fit: BoxFit.fill)),
  child: Text('老孟,專註分享Flutter技術和應用實戰。'
      '老孟,專註分享Flutter技術和應用實戰。'
      '老孟,專註分享Flutter技術和應用實戰。',),
)

背景圖片大小是57×80:

右側三角已經不在中間了,如果想讓其一直保持居中,修改拉伸區域:

centerSlice: Rect.fromLTWH(20, 10, 1, 60),

圓形帶邊框的頭像

Container(
  width: 100,
  height: 100,
  padding: EdgeInsets.all(3),
  decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.blue),
  child: Container(
    decoration: BoxDecoration(
        shape: BoxShape.circle,
        image: DecorationImage(
            image: AssetImage('assets/images/aa.jpg'), fit: BoxFit.cover)),
  ),
)

圖片佔位符:

Image.network(
  'https://flutter.github.io/assets-for-api-docs/assets/widgets/puffin.jpg',
  height: 150,
  width: 150,
  fit: BoxFit.cover,
  frameBuilder: (
    BuildContext context,
    Widget child,
    int frame,
    bool wasSynchronouslyLoaded,
  ) {
    if (frame == null) {
      return Image.asset(
        'assets/images/place.png',
        height: 150,
        width: 150,
        fit: BoxFit.cover,
      );
    }
    return child;
  },
)

添加自己的圖標庫

如果系統提供的圖標沒有我們想要的圖標,這時需要引入第三方庫的圖標,下面以阿里巴巴的圖標庫為例。

打開阿里巴巴的圖標官網,找到自己想要的圖標后,將鼠標放置到圖標上,加入購物車:

點擊右上角的購物車,然後點擊添加至項目:

如果沒有添加過項目,需要創建一個新項目:

創建好后加入此項目,跳轉到我的項目頁面,點擊下載:

解壓下載的文件,解壓出來的文件有好幾個,如下圖:

選擇iconfont.ttf文件拷貝到 Flutter 項目的assets/fonts目錄下,assets/fonts目錄默認是沒有的,需要手動創建,在pubspec.yaml設置如下:

千萬注意紅框內開頭的空格問題,否則編譯不通過,family後面跟的字符串最好有意義,後面用圖標的時候需要用到。

用法如下:

Icon(IconData(0xe613,fontFamily: 'appIconFonts')

0xe613在下載圖標時已經標註,將&#替換為0,如下圖:

fontFamily是在pubspec.yaml中設置的family屬性,第三方的圖標和系統圖標一樣,可以設置其顏色和大小。

交流

老孟Flutter博客地址(330個控件用法):http://laomengit.com

歡迎加入Flutter交流群(微信:laomengit)、關注公眾號【老孟Flutter】:

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

【其他文章推薦】

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

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

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

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

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

[源碼解析] GroupReduce,GroupCombine 和 Flink SQL group by

[源碼解析] GroupReduce,GroupCombine和Flink SQL group by

目錄

  • [源碼解析] GroupReduce,GroupCombine和Flink SQL group by
    • 0x00 摘要
    • 0x01 緣由
    • 0x02 概念
      • 2.1 GroupReduce
      • 2.2 GroupCombine
      • 2.3 例子
    • 0x03 代碼
    • 0x04 Flink SQL內部翻譯
    • 0x05 JobGraph
    • 0x06 Runtime
      • 6.1 ChainedFlatMapDriver
      • 6.2 GroupReduceCombineDriver
      • 6.3 GroupReduceDriver & ChainedFlatMapDriver
    • 0x07 總結
    • 0x08 參考

0x00 摘要

本文從源碼和實例入手,為大家解析 Flink 中 GroupReduce 和 GroupCombine 的用途。也涉及到了 Flink SQL group by 的內部實現。

0x01 緣由

在前文[源碼解析] Flink的Groupby和reduce究竟做了什麼中,我們剖析了Group和reduce都做了些什麼,也對combine有了一些了解。但是總感覺意猶未盡,因為在Flink還提出了若干新算子,比如GroupReduce和GroupCombine。這幾個算子不搞定,總覺得如鯁在喉,但沒有找到一個良好的例子來進行剖析說明。

本文是筆者在探究Flink SQL UDF問題的一個副產品。起初是為了調試一段sql代碼,結果發現Flink本身給出了一個GroupReduce和GroupCombine使用的完美例子。於是就拿出來和大家共享,一起分析看看究竟如何使用這兩個算子。

請注意:這個例子是Flink SQL,所以本文中將涉及Flink SQL goup by內部實現的知識。

0x02 概念

Flink官方對於這兩個算子的使用說明如下:

2.1 GroupReduce

GroupReduce算子應用在一個已經分組了的DataSet上,其會對每個分組都調用到用戶定義的group-reduce函數。它與Reduce的區別在於用戶定義的函數會立即獲得整個組。

Flink將在組的所有元素上使用Iterable調用用戶自定義函數,並且可以返回任意數量的結果元素。

2.2 GroupCombine

GroupCombine轉換是可組合GroupReduceFunction中組合步驟的通用形式。它在某種意義上被概括為允許將輸入類型 I 組合到任意輸出類型O。與此相對的是,GroupReduce中的組合步驟僅允許從輸入類型 I 到輸出類型 I 的組合。這是因為GroupReduceFunction的 “reduce步驟” 期望自己的輸入類型為 I。

在一些應用中,我們期望在執行附加變換(例如,減小數據大小)之前將DataSet組合成中間格式。這可以通過CombineGroup轉換能以非常低的成本實現。

注意:分組數據集上的GroupCombine在內存中使用貪婪策略執行,該策略可能不會一次處理所有數據,而是以多個步驟處理。它也可以在各個分區上執行,而無需像GroupReduce轉換那樣進行數據交換。這可能會導致輸出的是部分結果,所以GroupCombine是不能替代GroupReduce操作的,儘管它們的操作內容可能看起來都一樣。

2.3 例子

是不是有點暈?還是直接讓代碼來說話吧。以下官方示例演示了如何將CombineGroup和GroupReduce轉換用於WordCount實現。即通過combine操作先對單詞數目進行初步排序,然後通過reduceGroup對combine產生的結果進行最終排序。因為combine進行了初步排序,所以在算子之間傳輸的數據量就少多了

DataSet<String> input = [..] // The words received as input

// 這裏通過combine操作先對單詞數目進行初步排序,其優勢在於用戶定義的combine函數只調用一次,因為runtime已經把輸入數據一次性都提供給了自定義函數。  
DataSet<Tuple2<String, Integer>> combinedWords = input
  .groupBy(0) // group identical words
  .combineGroup(new GroupCombineFunction<String, Tuple2<String, Integer>() {

    public void combine(Iterable<String> words, Collector<Tuple2<String, Integer>>) { // combine
        String key = null;
        int count = 0;

        for (String word : words) {
            key = word;
            count++;
        }
        // emit tuple with word and count
        out.collect(new Tuple2(key, count));
    }
});

// 這裏對combine的結果進行第二次排序,其優勢在於用戶定義的reduce函數只調用一次,因為runtime已經把輸入數據一次性都提供給了自定義函數。  
DataSet<Tuple2<String, Integer>> output = combinedWords
  .groupBy(0)                              // group by words again
  .reduceGroup(new GroupReduceFunction() { // group reduce with full data exchange

    public void reduce(Iterable<Tuple2<String, Integer>>, Collector<Tuple2<String, Integer>>) {
        String key = null;
        int count = 0;

        for (Tuple2<String, Integer> word : words) {
            key = word;
            count++;
        }
        // emit tuple with word and count
        out.collect(new Tuple2(key, count));
    }
});

看到這裏,有的兄弟已經明白了,這和mapPartition很類似啊,都是runtime做了大量工作。為了讓大家這兩個算子的使用情形有深刻的認識,我們再通過一個sql的例子,向大家展示Flink內部是怎麼應用這兩個算子的,也能看出來他們的強大之處

0x03 代碼

下面代碼主要參考自 flink 使用問題匯總。我們可以看到這裏通過groupby進行了聚合操作。其中collect方法,類似於mysql的group_concat。

public class UdfExample {
    public static class MapToString extends ScalarFunction {

        public String eval(Map<String, Integer> map) {
            if(map==null || map.size()==0) {
                return "";
            }
            StringBuffer sb=new StringBuffer();
            for(Map.Entry<String, Integer> entity : map.entrySet()) {
                sb.append(entity.getKey()+",");
            }
            String result=sb.toString();
            return result.substring(0, result.length()-1);
        }
    }

    public static void main(String[] args) throws Exception {
        MemSourceBatchOp src = new MemSourceBatchOp(new Object[][]{
                new Object[]{"1", "a", 1L},
                new Object[]{"2", "b33", 2L},
                new Object[]{"2", "CCC", 2L},
                new Object[]{"2", "xyz", 2L},
                new Object[]{"1", "u", 1L}
        }, new String[]{"f0", "f1", "f2"});

        BatchTableEnvironment environment = MLEnvironmentFactory.getDefault().getBatchTableEnvironment();
        Table table = environment.fromDataSet(src.getDataSet());
        environment.registerTable("myTable", table);
        BatchOperator.registerFunction("MapToString",  new MapToString());
        BatchOperator.sqlQuery("select f0, mapToString(collect(f1)) as type from myTable group by f0").print();
    }
}

程序輸出是

f0|type
--|----
1|a,u
2|CCC,b33,xyz

0x04 Flink SQL內部翻譯

這個SQL語句的重點是group by。這個是程序猿經常使用的操作。但是大家有沒有想過這個group by在真實運行起來時候是怎麼操作的呢?針對大數據環境有沒有做了什麼優化呢?其實,Flink正是使用了GroupReduce和GroupCombine來實現並且優化了group by的功能。優化之處在於:

  • GroupReduce和GroupCombine的函數調用次數要遠低於正常的reduce算子,如果reduce操作中涉及到頻繁創建額外的對象或者外部資源操作,則會相當省時間。
  • 因為combine進行了初步排序,所以在算子之間傳輸的數據量就少多了。

SQL生成Flink的過程十分錯綜複雜,所以我們只能找最關鍵處。其是在 DataSetAggregate.translateToPlan 完成的。我們可以看到,對於SQL語句 “select f0, mapToString(collect(f1)) as type from myTable group by f0”,Flink系統把它翻譯成如下階段,即

  • pre-aggregation :排序 + combine;
  • final aggregation :排序 + reduce;

從之前的文章我們可以知道,groupBy這個其實不是一個算子,它只是排序過程中的一個輔助步驟而已,所以我們重點還是要看combineGroup和reduceGroup。這恰恰是我們想要的完美例子。

input ----> (groupBy + combineGroup) ----> (groupBy + reduceGroup) ----> output

SQL生成的Scala代碼如下,其中 combineGroup在後續中將生成GroupCombineOperator,reduceGroup將生成GroupReduceOperator。

  override def translateToPlan(
      tableEnv: BatchTableEnvImpl,
      queryConfig: BatchQueryConfig): DataSet[Row] = {

    if (grouping.length > 0) {
      // grouped aggregation
      ...... 
      if (preAgg.isDefined) { // 我們的例子是在這裏
        inputDS          
          // pre-aggregation
          .groupBy(grouping: _*)
          .combineGroup(preAgg.get) // 將生成GroupCombineOperator算子
          .returns(preAggType.get)
          .name(aggOpName)
          // final aggregation
          .groupBy(grouping.indices: _*) //將生成GroupReduceOperator算子。
          .reduceGroup(finalAgg.right.get)
          .returns(rowTypeInfo)
          .name(aggOpName)
      } else {
        ......
      }
    }
    else {
      ......
    }
  }
}

// 程序變量打印如下
this = {DataSetAggregate@5207} "Aggregate(groupBy: (f0), select: (f0, COLLECT(f1) AS $f1))"
 cluster = {RelOptCluster@5220} 

0x05 JobGraph

LocalExecutor.execute中會生成JobGraph。JobGraph是提交給 JobManager 的數據結構,是唯一被Flink的數據流引擎所識別的表述作業的數據結構,也正是這一共同的抽象體現了流處理和批處理在運行時的統一。

在生成JobGraph時候,系統得到如下JobVertex。

jobGraph = {JobGraph@5652} "JobGraph(jobId: 6aae8b5e5ad32f588136bef26f8b65f6)"
 taskVertices = {LinkedHashMap@5655}  size = 4

{JobVertexID@5677} "c625209bb7fb9a098807551840aeaa99" -> {InputOutputFormatVertex@5678} "CHAIN DataSource (at initializeDataSource(MemSourceBatchOp.java:98) (org.apache.flink.api.java.io.CollectionInputFormat)) -> FlatMap (select: (f0, f1)) (org.apache.flink.runtime.operators.DataSourceTask)"

{JobVertexID@5679} "b56ace4acd7a2f69ea110a9f262ff80a" -> {JobVertex@5680} "CHAIN GroupReduce (groupBy: (f0), select: (f0, COLLECT(f1) AS $f1)) -> FlatMap (select: (f0, mapToString($f1) AS type)) -> Map (Map at linkFrom(MapBatchOp.java:35)) (org.apache.flink.runtime.operators.BatchTask)"
 
{JobVertexID@5681} "3f5e2a0f700421d80ce85e02a6d9db73" -> {InputOutputFormatVertex@5682} "DataSink (collect()) (org.apache.flink.runtime.operators.DataSinkTask)"
 
{JobVertexID@5683} "ad29dc5b2e0a39ad2cd1d164b6f859f7" -> {JobVertex@5684} "GroupCombine (groupBy: (f0), select: (f0, COLLECT(f1) AS $f1)) (org.apache.flink.runtime.operators.BatchTask)"

我們可以看到,在JobGraph中就生成了對應的兩個算子。其中這裏的FlatMap就是用戶的UDF函數MapToString的映射生成。

GroupCombine (groupBy: (f0), select: (f0, COLLECT(f1) AS $f1))  
  
CHAIN GroupReduce (groupBy: (f0), select: (f0, COLLECT(f1) AS $f1)) -> FlatMap (select: (f0, mapToString($f1) AS type)) -> Map 

0x06 Runtime

最後,讓我們看看runtime會如何處理這兩個算子。

6.1 ChainedFlatMapDriver

首先,Flink會在ChainedFlatMapDriver.collect中對record進行處理,這是從Table中提取數據所必須經歷的,與後續的group by關係不大。

@Override
public void collect(IT record) {
   try {
      this.numRecordsIn.inc();
      this.mapper.flatMap(record, this.outputCollector);
   } catch (Exception ex) {
      throw new ExceptionInChainedStubException(this.taskName, ex);
   }
}

// 這裡能夠看出來,我們獲取了第一列記錄
record = {Row@9317} "1,a,1"
 fields = {Object[3]@9330} 
this.taskName = "FlatMap (select: (f0, f1))"

// 程序堆棧打印如下
collect:80, ChainedFlatMapDriver (org.apache.flink.runtime.operators.chaining)
collect:35, CountingCollector (org.apache.flink.runtime.operators.util.metrics)
invoke:196, DataSourceTask (org.apache.flink.runtime.operators)
doRun:707, Task (org.apache.flink.runtime.taskmanager)
run:532, Task (org.apache.flink.runtime.taskmanager)
run:748, Thread (java.lang)

6.2 GroupReduceCombineDriver

其次,GroupReduceCombineDriver.run()中會進行combine操作。

  1. 會通過this.sorter.write(value)把數據寫到排序緩衝區。
  2. 會通過sortAndCombineAndRetryWrite(value)進行實際的排序,合併。

因為是系統實現,所以Combine的用戶自定義函數就是由Table API提供的,比如org.apache.flink.table.functions.aggfunctions.CollectAccumulator.accumulate

@Override
public void run() throws Exception {
   final MutableObjectIterator<IN> in = this.taskContext.getInput(0);
   final TypeSerializer<IN> serializer = this.serializer;

   if (objectReuseEnabled) {
    .....
   }
   else {
      IN value;
      while (running && (value = in.next()) != null) {
         // try writing to the sorter first
         if (this.sorter.write(value)) {
            continue;
         }

         // do the actual sorting, combining, and data writing
         sortAndCombineAndRetryWrite(value);
      }
   }

   // sort, combine, and send the final batch
   if (running) {
      sortAndCombine();
   }
}

// 程序變量如下
value = {Row@9494} "1,a"
 fields = {Object[2]@9503} 

sortAndCombine是具體排序/合併的過程。

  1. 排序是通過 org.apache.flink.runtime.operators.sort.QuickSort 完成的。
  2. 合併是通過 org.apache.flink.table.functions.aggfunctions.CollectAccumulator.accumulate 完成的。
  3. 給下游是由 org.apache.flink.table.runtime.aggregate.DataSetPreAggFunction.combine 調用 out.collect(output) 完成的。
private void sortAndCombine() throws Exception {
   final InMemorySorter<IN> sorter = this.sorter;
   // 這裏進行實際的排序
   this.sortAlgo.sort(sorter);
   final GroupCombineFunction<IN, OUT> combiner = this.combiner;
   final Collector<OUT> output = this.output;

   // iterate over key groups
   if (objectReuseEnabled) {
			......		
   } else {
      final NonReusingKeyGroupedIterator<IN> keyIter = 
            new NonReusingKeyGroupedIterator<IN>(sorter.getIterator(), this.groupingComparator);
      // 這裡是歸併操作
      while (this.running && keyIter.nextKey()) {
         // combiner.combiner 是用戶定義操作,runtime把某key對應的數據一次性傳給它
         combiner.combine(keyIter.getValues(), output);
      }
   }
}

具體調用棧如下:

accumulate:57, CollectAggFunction (org.apache.flink.table.functions.aggfunctions)
accumulate:-1, DataSetAggregatePrepareMapHelper$5
combine:71, DataSetPreAggFunction (org.apache.flink.table.runtime.aggregate)
sortAndCombine:213, GroupReduceCombineDriver (org.apache.flink.runtime.operators)
run:188, GroupReduceCombineDriver (org.apache.flink.runtime.operators)
run:504, BatchTask (org.apache.flink.runtime.operators)
invoke:369, BatchTask (org.apache.flink.runtime.operators)
doRun:707, Task (org.apache.flink.runtime.taskmanager)
run:532, Task (org.apache.flink.runtime.taskmanager)
run:748, Thread (java.lang)

6.3 GroupReduceDriver & ChainedFlatMapDriver

這兩個放在一起,是因為他們組成了Operator Chain。

GroupReduceDriver.run中完成了reduce。具體reduce 操作是在 org.apache.flink.table.runtime.aggregate.DataSetFinalAggFunction.reduce 完成的,然後在其中直接發送給下游 out.collect(output)

@Override
public void run() throws Exception {
   // cache references on the stack
   final GroupReduceFunction<IT, OT> stub = this.taskContext.getStub();
 
   if (objectReuseEnabled) {
       ......	
   }
   else {
      final NonReusingKeyGroupedIterator<IT> iter = new NonReusingKeyGroupedIterator<IT>(this.input, this.comparator);
      // run stub implementation
      while (this.running && iter.nextKey()) {
         // stub.reduce 是用戶定義操作,runtime把某key對應的數據一次性傳給它
         stub.reduce(iter.getValues(), output);
      }
   }
}

從前文我們可以,這裏已經配置成了Operator Chain,所以out.collect(output)會調用到CountingCollector。CountingCollector的成員變量collector已經配置成了ChainedFlatMapDriver。

public void collect(OUT record) {
   this.numRecordsOut.inc();
   this.collector.collect(record);
}

this.collector = {ChainedFlatMapDriver@9643} 
 mapper = {FlatMapRunner@9610} 
 config = {TaskConfig@9655} 
 taskName = "FlatMap (select: (f0, mapToString($f1) AS type))"

於是程序就調用到了 ChainedFlatMapDriver.collect

public void collect(IT record) {
   try {
      this.numRecordsIn.inc();
      this.mapper.flatMap(record, this.outputCollector);
   } catch (Exception ex) {
      throw new ExceptionInChainedStubException(this.taskName, ex);
   }
}

最終調用棧如如下:

eval:21, UdfExample$MapToString (com.alibaba.alink)
flatMap:-1, DataSetCalcRule$14
flatMap:52, FlatMapRunner (org.apache.flink.table.runtime)
flatMap:31, FlatMapRunner (org.apache.flink.table.runtime)
collect:80, ChainedFlatMapDriver (org.apache.flink.runtime.operators.chaining)
collect:35, CountingCollector (org.apache.flink.runtime.operators.util.metrics)
reduce:80, DataSetFinalAggFunction (org.apache.flink.table.runtime.aggregate)
run:131, GroupReduceDriver (org.apache.flink.runtime.operators)
run:504, BatchTask (org.apache.flink.runtime.operators)
invoke:369, BatchTask (org.apache.flink.runtime.operators)
doRun:707, Task (org.apache.flink.runtime.taskmanager)
run:532, Task (org.apache.flink.runtime.taskmanager)
run:748, Thread (java.lang)

0x07 總結

由此我們可以看到:

  • GroupReduce,GroupCombine和mapPartition十分類似,都是從系統層面對算子進行優化,把循環操作放到用戶自定義函數來處理。
  • 對於group by這個SQL語句,Flink將其翻譯成 GroupReduce + GroupCombine,採用兩階段優化的方式來完成了對大數據下的處理。

0x08 參考

flink 使用問題匯總

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

漲姿勢了解一下Kafka消費位移可好?

摘要:Kafka中的位移是個極其重要的概念,因為數據一致性、準確性是一個很重要的語義,我們都不希望消息重複消費或者丟失。而位移就是控制消費進度的大佬。本文就詳細聊聊kafka消費位移的那些事,包括:

概念剖析

kafka的兩種位移

關於位移(Offset),其實在kafka的世界里有兩種位移:

  • 分區位移:生產者向分區寫入消息,每條消息在分區中的位置信息由一個叫offset的數據來表徵。假設一個生產者向一個空分區寫入了 10 條消息,那麼這 10 條消息的位移依次是 0、1、…、9;

  • 消費位移:消費者需要記錄消費進度,即消費到了哪個分區的哪個位置上,這是消費者位移(Consumer Offset)。

注意,這和上面所說的消息在分區上的位移完全不是一個概念。上面的“位移”表徵的是分區內的消息位置,它是不變的,即一旦消息被成功寫入到一個分區上,它的位移值就是固定的了。而消費者位移則不同,它可能是隨時變化的,畢竟它是消費者消費進度的指示器。

消費位移

消費位移,記錄的是 Consumer 要消費的下一條消息的位移,切記,是下一條消息的位移! 而不是目前最新消費消息的位移

假設一個分區中有 10 條消息,位移分別是 0 到 9。某個 Consumer 應用已消費了 5 條消息,這就說明該 Consumer 消費了位移為 0 到 4 的 5 條消息,此時 Consumer 的位移是 5,指向了下一條消息的位移。

至於為什麼要有消費位移,很好理解,當 Consumer 發生故障重啟之後,就能夠從 Kafka 中讀取之前提交的位移值,然後從相應的位移處繼續消費,從而避免整個消費過程重來一遍。就好像書籤一樣,需要書籤你才可以快速找到你上次讀書的位置。

那麼了解了位移是什麼以及它的重要性,我們自然而然會有一個疑問,kafka是怎麼記錄、怎麼保存、怎麼管理位移的呢?

位移的提交

Consumer 需要上報自己的位移數據,這個彙報過程被稱為位移提交。因為 Consumer 能夠同時消費多個分區的數據,所以位移的提交實際上是在分區粒度上進行的,即Consumer 需要為分配給它的每個分區提交各自的位移數據。

鑒於位移提交甚至是位移管理對 Consumer 端的巨大影響,KafkaConsumer API提供了多種提交位移的方法,每一種都有各自的用途,這些都是本文將要談到的方案。

void commitSync(Duration timeout);
void commitSync(Map<TopicPartition, OffsetAndMetadata> offsets);
void commitSync(final Map<TopicPartition, OffsetAndMetadata> offsets, final Duration timeout);
void commitAsync();
void commitAsync(OffsetCommitCallback callback);
void commitAsync(Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback);

先粗略的總結一下。位移提交分為自動提交和手動提交;而手動提交又分為同步提交和異步提交。

自動提交

當消費配置enable.auto.commit=true的時候代表自動提交位移。

自動提交位移是發生在什麼時候呢?auto.commit.interval.ms默認值是50000ms。即kafka每隔5s會幫你自動提交一次位移。自動位移提交的動作是在 poll()方法的邏輯里完成的,在每次真正向服務端發起拉取請求之前會檢查是否可以進行位移提交,如果可以,那麼就會提交上一次輪詢的位移。假如消費數據量特別大,可以設置的短一點。

越簡單的東西功能越不足,自動提交位移省事的同時肯定會帶來一些問題。自動提交帶來重複消費和消息丟失的問題:

  • 重複消費: 在默認情況下,Consumer 每 5 秒自動提交一次位移。現在,我們假設提交位移之後的 3 秒發生了 Rebalance 操作。在 Rebalance 之後,所有 Consumer 從上一次提交的位移處繼續消費,但該位移已經是 3 秒前的位移數據了,故在 Rebalance 發生前 3 秒消費的所有數據都要重新再消費一次。雖然你能夠通過減少 auto.commit.interval.ms 的值來提高提交頻率,但這麼做只能縮小重複消費的時間窗口,不可能完全消除它。這是自動提交機制的一個缺陷。

  • 消息丟失: 假設拉取了100條消息,正在處理第50條消息的時候,到達了自動提交窗口期,自動提交線程將拉取到的每個分區的最大消息位移進行提交,如果此時消費服務掛掉,消息並未處理結束,但卻提交了最大位移,下次重啟就從100條那消費,即發生了50-100條的消息丟失。

手動提交

當消費配置enable.auto.commit=false的時候代表手動提交位移。用戶必須在適當的時機(一般是處理完業務邏輯后),手動的調用相關api方法提交位移。比如在下面的案例中,我需要確認我的業務邏輯返回true之後再手動提交位移

 while (true) {
     try {
         ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
         if (!consumerRecords.isEmpty()) {
             for (ConsumerRecord<String, String> record : consumerRecords) {
                 KafkaMessage kafkaMessage = JSON.parseObject(record.value(), KafkaMessage.class);
                 // 處理業務
                 boolean handleResult = handle(kafkaMessage);
                 if (handleResult) {
                     log.info(" handle success, kafkaMessage={}" ,kafkaMessage);
                 } else {
                     log.info(" handle failed, kafkaMessage={}" ,kafkaMessage);
                 }
             }
             // 手動提交offset
             consumer.commitSync(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
        
         } 
     } catch (Exception e) {
         log.info("kafka consume error." ,e);
     }
 }

手動提交明顯能解決消息丟失的問題,因為你是處理完業務邏輯后再提交的,假如此時消費服務掛掉,消息並未處理結束,那麼重啟的時候還會重新消費。

但是對於業務層面的失敗導致消息未消費成功,是無法處理的。因為業務層的邏輯千變萬化、比如格式不正確,你叫Kafka消費端程序怎麼去處理?應該要業務層面自己處理,記錄失敗日誌做好監控等。

但是手動提交不能解決消息重複的問題,也很好理解,假如消費0-100條消息,50條時掛了,重啟後由於沒有提交這一批消息的offset,是會從0開始重新消費。至於如何避免重複消費的問題,在這篇文章有說。

手動提交又分為異步提交和同步提交。

同步提交

上面案例代碼使用的是commitSync() ,顧名思義,是同步提交位移的方法。同步提交位移Consumer 程序會處於阻塞狀態,等待 Broker 返回提交結果。同步模式下提交失敗的時候一直嘗試提交,直到遇到無法重試的情況下才會結束。在任何系統中,因為程序而非資源限制而導致的阻塞都可能是系統的瓶頸,會影響整個應用程序的 TPS。當然,你可以選擇拉長提交間隔,但這樣做的後果是 Consumer 的提交頻率下降,在下次 Consumer 重啟回來后,會有更多的消息被重新消費。因此,為了解決這些不足,kafka還提供了異步提交方法。

異步提交

異步提交會立即返回,不會阻塞,因此不會影響 Consumer 應用的 TPS。由於它是異步的,Kafka 提供了回調函數,供你實現提交之後的邏輯,比如記錄日誌或處理異常等。下面這段代碼展示了調用 commitAsync() 的方法

 consumer.commitAsync((offsets, exception) -> {
 if (exception != null)
     handleException(exception);
 });

但是異步提交會有一個問題,那就是它沒有重試機制,不過一般情況下,針對偶爾出現的提交失敗,不進行重試不會有太大問題,因為如果提交失敗是因為臨時問題導致的,那麼後續的提交總會有成功的。所以消息也是不會丟失和重複消費的。
但如果這是發生在關閉消費者或再均衡前的最後一次提交,就要確保能夠提交成功。因此,組合使用commitAsync()commitSync()是最佳的方式。

try {
    while (true) {
        ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
        if (!consumerRecords.isEmpty()) {
             for (ConsumerRecord<String, String> record : consumerRecords) {
                KafkaMessage kafkaMessage = JSON.parseObject(record.value(), KafkaMessage.class);
                boolean handleResult = handle(kafkaMessage);             
             }
             //異步提交位移               
             consumer.commitAsync((offsets, exception) -> {
             if (exception != null)
                 handleException(exception);
             });
           
        }
    }
} catch (Exception e) {
    System.out.println("kafka consumer error:" + e.toString());
} finally {
    try {
        //最後同步提交位移
        consumer.commitSync();
    } finally {
        consumer.close();
    }
}

讓位移提交更加靈活和可控

如果細心的閱讀了上面所有demo的代碼,那麼你會發現這樣幾個問題:

1、所有的提交,都是提交 poll 方法返回的所有消息的位移,poll 方法一次返回1000 條消息,則一次性地將這 1000 條消息的位移一併提交。可這樣一旦中間出現問題,位移沒有提交,下次會重新消費已經處理成功的數據。所以我想做到細粒度控制,比如每次提交100條,該怎麼辦?

答:可以通過commitSync(Map<TopicPartition, OffsetAndMetadata>)commitAsync(Map<TopicPartition, OffsetAndMetadata>)對位移進行精確控制。

2、poll和commit方法對於普通的開發人員而言是一個黑盒,無法精確地掌控其消費的具體位置。我都不知道這次的提交,是針對哪個partition,提交上去的offset是多少。

答:可以通過record.topic()獲取topic信息, record.partition()獲取分區信息,record.offset() + 1獲取消費位移,記住消費位移是指示下一條消費的位移,所以要加一。

3、我想自己管理offset怎麼辦?一方面更加保險,一方面下次重啟之後可以精準的從數據庫讀取最後的offset就不存在丟失和重複消費了。
答:可以將消費位移保存在數據庫中。消費端程序使用comsumer.seek方法指定從某個位移開始消費。

綜合以上幾個可優化點,並結合全文,可以給出一個比較完美且完整的demo:聯合異步提交和同步提交,對處理過程中所有的異常都進行了處理。細粒度的控制了消費位移的提交,並且保守的將消費位移記錄到了數據庫中,重新啟動消費端程序的時候會從數據庫讀取位移。這也是我們消費端程序位移提交的最佳實踐方案。你只要繼承這個抽象類,實現你具體的業務邏輯就可以了。

public abstract class PrefectCosumer {
    private Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
    int count = 0;
    public final void consume() {
        Properties properties = PropertiesConfig.getConsumerProperties();
        properties.put("group.id", getGroupId());
        Consumer<String, String> consumer = new KafkaConsumer<>(properties);
        consumer.subscribe(getTopics());
        consumer.poll(0);
        // 把offset記錄到數據庫中 從指定的offset處消費 
        consumer.partitionsFor(getTopics()).stream().map(info ->
        new TopicPartition(getTopics(), info.partition()))
        .forEach(tp -> {
               consumer.seek(tp, JdbcUtils.queryOffset().get(tp.partition()));   
         });
        try {
            while (true) {
                ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
                if (!consumerRecords.isEmpty()) {
                    for (ConsumerRecord<String, String> record : consumerRecords) {

                        KafkaMessage kafkaMessage = JSON.parseObject(record.value(), KafkaMessage.class);
                        boolean handleResult = handle(kafkaMessage);
                        if (handleResult) {
                            //注意:提交的是下一條消息的位移。所以OffsetAndMetadata 對象時,必須使用當前消息位移加 1。
                            offsets.put(new TopicPartition(record.topic(), record.partition()),
                                    new OffsetAndMetadata(record.offset() + 1));

                            // 細粒度控制提交 每10條提交一次offset
                            if (count % 10 == 0) {
                                // 異步提交offset
                                consumer.commitAsync(offsets, (offsets, exception) -> {
                                    if (exception != null) {
                                        handleException(exception);
                                    }
                                    // 將消費位移再記錄一份到數據庫中
                                    offsets.forEach((k, v) -> {
                                        String s = "insert into kafka_offset(`topic`,`group_id`,`partition_id`,`offset`) values" +
                                                " ('" + k.topic() + "','" + getGroupId() + "'," + k.partition() + "," + v.offset() + ")" +
                                                " on duplicate key update offset=values(offset);";
                                        JdbcUtils.insertTable(s);
                                    });


                                });
                            }
                            count++;
                        } else {         
                            System.out.println("消費消息失敗 kafkaMessage={}" + getTopics() + getGroupId() + kafkaMessage.toString());                         
                        }
                    }


                }
            }
        } catch (Exception e) {
            System.out.println("kafka consumer error:" + e.toString());
        } finally {
            try {
                // 最後一次提交 使用同步提交offset
                consumer.commitSync();
            } finally {
                consumer.close();
            }


        }
    }


    /**
     * 具體的業務邏輯
     *
     * @param kafkaMessage
     * @return
     */
    public abstract boolean handle(KafkaMessage kafkaMessage);

    public abstract List<String> getTopics();

    public abstract String getGroupId();

    void handleException(Exception e) {
        //異常處理
    }
}

控制位移提交的N種方式

剛剛我們說自己控制位移,使用seek方法可以指定offset消費。那到底怎麼控制位移?怎麼重設消費組位移?seek是什麼?現在就來仔細說說。

並不是所有的消息隊列都可以重設消費者組位移達到重新消費的目的。比如傳統的RabbitMq,它們處理消息是一次性的,即一旦消息被成功消費,就會被刪除。而Kafka消費消息是可以重演的,因為它是基於日誌結構(log-based)的消息引擎,消費者在消費消息時,僅僅是從磁盤文件上讀取數據而已,所以消費者不會刪除消息數據。同時,由於位移數據是由消費者控制的,因此它能夠很容易地修改位移的值,實現重複消費歷史數據的功能。

了解如何重設位移是很重要的。假設這麼一個場景,我已經消費了1000條消息后,我發現處理邏輯錯了,所以我需要重新消費一下,可是位移已經提交了,我到底該怎麼重新消費這1000條呢??假設我想從某個時間點開始消費,我又該如何處理呢?

首先說個誤區:auto.offset.reset=earliest/latest這個參數大家都很熟悉,但是初學者很容易誤會它。大部分朋友都覺得在任何情況下把這兩個值設置為earliest或者latest ,消費者就可以從最早或者最新的offset開始消費,但實際上並不是那麼回事,他們生效都有一個前提條件,那就是對於同一個groupid的消費者,如果這個topic某個分區有已經提交的offset,那麼無論是把auto.offset.reset=earliest還是latest,都將失效,消費者會從已經提交的offset開始消費。因此這個參數並不能解決用戶想重設消費位移的需求。

kafka有七種控制消費組消費offset的策略,主要分為位移維度和時間維度,包括:

  • 位移維度。這是指根據位移值來重設。也就是說,直接把消費者的位移值重設成我們給定的位移值。包括Earliest/Latest/Current/Specified-Offset/Shift-By-N策略

  • 時間維度。我們可以給定一個時間,讓消費者把位移調整成大於該時間的最小位移;也可以給出一段時間間隔,比如 30 分鐘前,然後讓消費者直接將位移調回 30 分鐘之前的位移值。包括DateTime和Duration策略

說完了重設策略,我們就來看一下具體應該如何實現,可以從兩個角度,API方式和命令行方式。

重設位移的方法之API方式

API方式只要記住用seek方法就可以了,包括seek,seekToBeginning 和 seekToEnd。

void seek(TopicPartition partition, long offset);    
void seek(TopicPartition partition, OffsetAndMetadata offsetAndMetadata);    
void seekToBeginning(Collection<TopicPartition> partitions);    
void seekToEnd(Collection<TopicPartition> partitions);    

從方法簽名我們可以看出seekToBeginningseekToEnd是可以一次性重設n個分區的位移,而seek 只允許重設指定分區的位移,即為每個分區都單獨設置位移,因為不難得出,如果要自定義每個分區的位移值則用seek,如果希望kafka幫你批量重設所有分區位移,比如從最新數據消費或者從最早數據消費,那麼用seekToEnd和seekToBeginning。

Earliest 策略:從最早的數據開始消費

從主題當前最早位移處開始消費,這個最早位移不一定就是 0 ,因為很久遠的消息會被 Kafka 自動刪除,主要取決於你的刪除配置。

代碼如下:

Properties properties = PropertiesConfig.getConsumerProperties();
properties.put("group.id", getGroupId());
Consumer<String, String> consumer = new KafkaConsumer<>(properties);
consumer.subscribe(getTopics());
consumer.poll(0);
consumer.seekToBeginning(
consumer.partitionsFor(getTopics()).stream().map(partitionInfo ->
   new TopicPartition(getTopics(), partitionInfo.partition()))
   .collect(Collectors.toList()));

首先是構造consumer對象,這樣我們可以通過partitionsFor獲取到分區的信息,然後我們就可以構造出TopicPartition集合,傳給seekToBegining方法。需要注意的一個地方是:需要用consumer.poll(0),而不能用consumer.poll(Duration.ofMillis(0))

在poll(0)中consumer會一直阻塞直到它成功獲取了所需的元數據信息,之後它才會發起fetch請求去獲取數據。而poll(Duration)會把元數據獲取也計入整個超時時間。由於本例中使用的是0,即瞬時超時,因此consumer根本無法在這麼短的時間內連接上coordinator,所以只能趕在超時前返回一個空集合。

Latest策略:從最新的數據開始消費

    consumer.seekToEnd(
        consumer.partitionsFor(getTopics().get(0)).stream().map(partitionInfo ->
            new TopicPartition(getTopics().get(0), partitionInfo.partition()))
              .collect(Collectors.toList()));

Current策略:從當前已經提交的offset處消費

consumer.partitionsFor(getTopics().get(0)).stream().map(info ->
        new TopicPartition(getTopics().get(0), info.partition()))
        .forEach(tp -> {
            long committedOffset = consumer.committed(tp).offset();
            consumer.seek(tp, committedOffset);
        });

**Special-offset策略:從指定的offset處消費 **

該策略使用的方法和current策略一樣,區別在於,current策略是直接從kafka元信息中讀取中已經提交的offset值,而special策略需要用戶自己為每一個分區指定offset值,我們一般是把offset記錄到數據庫中然後可以從數據庫去讀取這個值

    consumer.partitionsFor(getTopics().get(0)).stream().map(info ->
                new TopicPartition(getTopics().get(0), info.partition()))
                .forEach(tp -> {
                    try {
                        consumer.seek(tp, JdbcUtils.queryOffset().get(tp.partition()));
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                });

以上演示了用API方式重設位移,演示了四種常見策略的代碼,另外三種沒有演示,一方面是大同小異,另一方面在實際生產中,用API的方式不太可能去做時間維度的重設,而基本都是用命令行方式。

重設位移的方法之命令行方式

命令行方式重設位移是通過 kafka-consumer-groups 腳本。比起 API 的方式,用命令行重設位移要簡單得多。

Earliest 策略指定–to-earliest。

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-earliest –execute

Latest 策略指定–to-latest。

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-latest --execute

Current 策略指定–to-current。

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-current --execute

Specified-Offset 策略指定–to-offset。

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-offset <offset> --execute

Shift-By-N 策略指定–shift-by N。

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --shift-by <offset_N> --execute

DateTime 策略指定–to-datetime。

DateTime 允許你指定一個時間,然後將位移重置到該時間之後的最早位移處。常見的使用場景是,你想重新消費昨天的數據,那麼你可以使用該策略重設位移到昨天 0 點。

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --to-datetime 2019-06-20T20:00:00.000 --execute

Duration 策略指定–by-duration。
Duration 策略則是指給定相對的時間間隔,然後將位移調整到距離當前給定時間間隔的位移處,具體格式是 PnDTnHnMnS。如果你熟悉 Java 8 引入的 Duration 類的話,你應該不會對這個格式感到陌生。它就是一個符合 ISO-8601 規範的 Duration 格式,以字母 P 開頭,後面由 4 部分組成,即 D、H、M 和 S,分別表示天、小時、分鐘和秒。舉個例子,如果你想將位移調回到 15 分鐘前,那麼你就可以指定 PT0H15M0S

bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --by-duration PT0H30M0S --execute

提交的位移都去哪了?

通過上面那幾部分的內容,我們已經搞懂了位移提交的方方面面,那麼提交的位移它保存在哪裡呢?這就要去位移主題的的世界里一探究竟了。kafka把位移保存在一個叫做__consumer_offsets的內部主題中,叫做位移主題。

注意:老版本的kafka其實是把位移保存在zookeeper中的,但是zookeeper並不適合這種高頻寫的場景。所以新版本已經是改進了這個方案,直接保存到kafka。畢竟kafka本身就適合高頻寫的場景,並且kafka也可以保證高可用性和高持久性。

既然它也是主題,那麼離不開分區和副本這兩個機制。我們並沒有手動創建這個主題並且指定,所以是kafka自動創建的, 分區的數量取決於Broker 端參數 offsets.topic.num.partitions,默認是50個分區,而副本參數取決於offsets.topic.replication.factor,默認是3。

既然也是主題,肯定會有消息,那麼消息格式是什麼呢?參考前面我們手動設計將位移寫入數據庫的方案,我們保存了topic,group_id,partition,offset四個字段。topic,group_id,partition無疑是數據表中的聯合主鍵,而offset是不斷更新的。無疑kafka的位移主題消息也是類似這種設計。key也是那三個字段,而消息體其實很複雜,你可以先簡單理解為就是offset。

既然也是主題,肯定也會有刪除策略,否則消息會無限膨脹。但是位移主題的刪除策略和其他主題刪除策略又不太一樣。我們知道普通主題的刪除是可以通過配置刪除時間或者大小的。而位移主題的刪除,叫做 Compaction。Kafka 使用Compact 策略來刪除位移主題中的過期消息,對於同一個 Key 的兩條消息 M1 和 M2,如果 M1 的發送時間早於 M2,那麼 M1 就是過期消息。Compact 的過程就是掃描日誌的所有消息,剔除那些過期的消息,然後把剩下的消息整理在一起。

Kafka 提供了專門的後台線程定期地巡檢待 Compact 的主題,看看是否存在滿足條件的可刪除數據。這個後台線程叫 Log Cleaner。很多實際生產環境中都出現過位移主題無限膨脹佔用過多磁盤空間的問題,如果你的環境中也有這個問題,我建議你去檢查一下 Log Cleaner 線程的狀態,通常都是這個線程掛掉了導致的。

總結

kafka的位移是個極其重要的概念,控制着消費進度,也即控制着消費的準確性,完整性,為了保證消息不重複和不丟失。我們最好做到以下幾點:

  • 手動提交位移。

  • 手動提交有異步提交和同步提交兩種方式,既然兩者有利也有弊,那麼我們可以結合起來使用。

  • 細粒度的控制消費位移的提交,這樣可以避免重複消費的問題。

  • 保守的將消費位移再記錄到了數據庫中,重新啟動消費端程序的時候從數據庫讀取位移。

獲取Kafka全套原創學習資料及思維導圖,關注【胖滾豬學編程】公眾號,回復”kafka”。

本文來源於公眾號:【胖滾豬學編程】。一枚集顏值與才華於一身,不算聰明卻足夠努力的女程序媛。用漫畫形式讓編程so easy and interesting!求關注!

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

【其他文章推薦】

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

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

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

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

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

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

VSCode + WSL 2 + Ruby環境搭建詳解

vscode配置ruby開發環境

vscode近年來發展迅速,幾乎在3年之間就搶佔了原來vim、sublime text的很多份額,猶記得在2015-2016年的時候,ruby推薦的開發環境基本上都是vim和sublime text,然而,隨着vscode的發展,vscode下ruby的開發體驗已經非常不錯。現在基本上使用win 10 wsl2 + vscode + windows terminal的體驗已經不遜於mac + vim (sublime) + item 2的體驗了

總體步驟

使用win10專業版配置ruby開發環境大致分為以下幾步:

  1. 開啟win10 wsl功能
  2. 升級wsl2
  3. 安裝ubuntu
  4. 安裝ruby(rvm)
  5. 安裝vscode
  6. 安裝vscode wsl擴展
  7. 安裝vscode ruby相關擴展

經過以上7步就可以開始愉悅的ruby開發了,再開始之前,可以先看個效果圖。

1. 開啟win10 wsl功能

ruby對Linux和Mac比較友好,在windows下很多第三方庫要配合mingw或msys2才能安裝,不過好在windows 10提供了Linux子系統,在win10 2004版本中wsl也升級到了wsl2,速度更快,功能更完善。

要使用wsl2需要先在控制面板中開啟wsl功能:

  • 適用於Linux的Windows子系統
  • 虛擬機平台

2. 升級wsl2

目前wsl2還需要安裝一個內核升級包,具體可參考微軟說明:

  • wsl2安裝說明
  • wsl2 update包

更新包安裝完成后,輸入命令

wsl --set-default-version 2

3. 安裝Ubuntu

在微軟應用商店安裝Ubuntu,當前Ubuntu版本為20.04 LTS

安裝完成以後,配置Ubuntu默認為wsl2

# 查看
wsl --list --verbose

# 設置
wsl --set-version Ubuntu 2

4. 安裝ruby

在Linux下安裝ruby有多種方法,比較主流的方法是RVM,不過為了簡單起見,我直接通過ubuntu的apt工具進行了安裝。

關於RVM的安裝可參考如下網站:

  • RVM官網
  • RVM實用指南

通過APT安裝,輸入下列命令即可

sudo apt install ruby ruby-dev ri ruby-bundle

安裝完成以後需要配置gem國內鏡像,參考如下網址:

  • gem中文鏡像

輸入下列命令

# 設置gem source
gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/

# 查看gem source
gem sources -l

# 設置bundle
bundle config mirror.https://rubygems.org https://gems.ruby-china.com

5. 安裝vscode

vscode直接在官網下載安裝即可,這裏我選擇了System Installer

  • vscode官網下載頁面

6. 安裝vscode wsl擴展

vscode安裝完成以後,可以在plugin中找到Remote – WSL擴展,點擊安裝即可

7. 安裝vscode ruby相關擴展

直接在plugin中搜索ruby在wsl中安裝下列五個擴展即可

  • Peng Lv/Ruby
  • Castwide/Ruby Solargraph(Language Server)
  • misogi/ruby-rubocop(Lint)
  • Simple Ruby ERB
  • endwise

其中,ruby solargraphrubocop除了安裝擴展,還需要通過gem安裝第三方包

sudo gem install rubocop
sudo gem install solargraph

重新加載vscode-wsl就可以愉快的使用ruby language進行開發了

vscode使用

在使用上基本只要require了相應的庫,就solargraph就會對require的庫中涉及的類和模塊進行提示,非常方便。唯一有問題的地方就是require的時候沒有提示,這可能就需要自己記一下庫的名稱,不過相比於原來已經好太多了,應該說在可以接受的範圍內。

1. 如果安裝了新的第三方庫會提示嗎?

如果安裝了sinatra這樣的庫,vscode-ruby如何給出提示呢?只需要Ctrl + Shift + P,選擇solargraph: build new gem documention即可

2. rubocop如何使用?

rubocop是一個Ruby Lint工具,可以進行Ruby代碼風格檢查,並能夠自動修復,只需要Ctrl + Shift + P,選擇Ruby: autocorrect by rubocop即可

3. 常用類型註釋

ruby是動態強類型語言,由於不需要指定函數返回值類型,這導致IDE無法自動推斷一些變量的類型。目前Python、PHP、TypeScript都在不斷的強化類型以方便IDE進行靜態檢查。IDE只有在知道類型的情況下才能準確地進行智能提示。

在ruby 2當中,我們可以通過類型註釋的方式增強IDE推斷能力。常見的類型註釋可參考YARD項目

下面代碼給出了一些示例。

require 'socket'

server = TCPServer.new 2000
loop do
  # 代碼塊參數類型註釋
  # @param {TCPSocket} client
  Thread.start(server.accept) do |client|
    client.puts 'hello !'
    client.puts "Time is #{Time.now}"
    client.close
  end
end

server = TCPServer.new 2000
loop do
  # 變量註釋
  # @type {TCPSocket} client
  client = server.accept
end

# 函數參數和返回值註釋,數組類型
# @param {Array(Integer)} nums
# @param {Integer} target
# @return {Array(Integer)}
def two_sum(nums, target)
  hash_nums = {}
  result = []
  nums.each_with_index do |num, index|
    hash_nums[num] = index
  end

  nums.each_with_index do |num, index|
    another = target - num
    if hash_nums[another] && hash_nums[another] != index
      result.push(index, hash_nums[another])
      break
    end
  end

  result
end

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

【其他文章推薦】

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

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

SpringMVC-攔截器

概述

Java 里的攔截器是動態攔截 action 調用的對象。

可以在Controller 中的方法執行之前與執行之後,及頁面显示完畢后,執行指定的方法,自定義的攔截器必須實現HandlerInterceptor 接口。

方法介紹

preHandle

在業務處理器處理請求之前被調用

postHandle

在業務處理器處理完請求后

afterCompletion

在 DispatcherServlet 完全處理完請求后被調用

SpringMVC攔截器使用

攔截所有請求

創建一個類實現 HandlerInterceptor 接口

 

 配置文件當中添加攔截器

 

 內部源碼分析

 

 

 

 

攔截指定請求

 

 

配置多個攔截器執行順序

 

 

 

 

 

 

 

第 2 個返回 false

 

 

 

 

 

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

【其他文章推薦】

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

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

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

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

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

Nginx服務器的使用與反向代理負載均衡

Nginx服務器

一:什麼是Nginx?

我們生活的世界中,有的時候需要上網。我們可以瀏覽很多很多的網頁,這些網頁都是由一系列的程序組成,但是我們是否想過,這些程序存儲在什麼地方呢?沒錯,這些程序都是存儲在一種名叫服務器的硬件上,比如我們的電腦也是一種服務器,只不過我們的個人電腦作為服務器的話性能會比較低。我們的網頁程序存儲在服務器硬件上,是否可以隨意存儲呢?不是的,我們需要在服務器硬件的操作系統中搭建一個服務器軟件,那麼這樣,有服務器軟件跟服務器硬件配合,才形成一個完整的服務器。服務器軟件有非常多,比如Apache、tomcat等等都是服務器軟件,而我們今天要學習的Nginx也是一種服務器軟件之一。

Nginx是一種服務器軟件,故而其最主要、最基本的功能當然是可以與服務器硬件結合,讓程序員可以將程序放在Nginx服務器上,將程序發布出去,讓成千上萬的網民可以瀏覽。除此之外,Nginx是一種高性能的HTTP和反向代理服務器,同時也是一個代理郵件服務器。也就是說,我們Nginx上可以發布網站,也可以實現負載均衡的功能,還可以作為郵件服務器實現收發郵件等功能。所謂的負載均衡是指,當同時有N多用戶訪問我們服務器的時候,為了減少服務器壓力,我們需要將用戶分別引入各服務器,分擔服務器的壓力。

Nginx與其他服努器的性能比較

首先說IIS, IIS服務器只能在Windows上運行,Windows服務器性能不如Linux— 類服務器。其次說Tomcat,Tomcat服務器面向的是Java語言,是一種重量級的服 務器,而Nginx是輕量級服務器,Tomcat與Nginx不具備可比性。最後,我們講一 下Apache,Apache優點非常多,比如穩定、幵源、跨平台等等,但是Apache不支 持高併發。Nginx能支持處理百萬級的TCP連接,10萬以上的併發連接,並且是一 個很好的跨平台服務器。

Nginx的主要優點有可以實現高併發、部署簡單、內存消耗少、成本低等等。

Nginx的主要缺點有rewrite功能不夠強大,模塊沒有Apache的多。

本文版權歸微信公眾號”代碼藝術”(ID:onblog)所有,若是轉載請務必保留本段原創聲明,違者必究。若是文章有不足之處,歡迎關注微信公眾號私信與我進行交流!

二:Linux中搭建Nginx服務器

新建壓縮包下載位置(可選)

新建目錄
mkdir /usr/local/nginx_down
切換目錄
cd /usr/local/nginx_down

下載解壓 Nginx

下載
wget http://nginx.org/download/nginx-1.14.0.tar.gz
解壓
tar -zxvf nginx-1.14.0.tar.gz
切換目錄
cd nginx-1.14.0

配置 Nginx

./configure --with-http_ssl_module
  1. 這樣會默認安裝nginx在 /usr/local/nginx 目錄,可以使用--prefix=/usr/local/nginx指定安裝位置。

  2. 如果需要HTTPS(SSL)的支持,需要指定參數--with-http_ssl_module

如果提示錯誤,那麼需要其它環境,請參考下面

安裝 make

yum -y install gcc automake autoconf libtool make

安裝 g++

yum -y install gcc gcc-c++

安裝 PCRE庫

yum -y install pcre pcre-devel

安裝 Zlib

yum -y install zlib zlib-devel

安裝 GD library

yum -y install gd-devel

安裝 openssl

yum -y install openssl openssl-devel

-y:跳過所有手動確認輸入

如果./configure安裝成功,只需要再執行兩個命令:

make
make install

查看是否安裝成功

cd /usr/local/nginx

如果安裝成功,則會出現下列目錄:

conf  html  logs  sbin

切換到sbin目錄

cd /sbin

啟動程序

./nginx

追加參數 -c 可以指定配置文件。

常見的錯誤

在Linux操作系統下搭建Nginx服務器,很多時候會出現不同的錯誤,在此,我們對搭建過程中出現的錯誤進行一些總結。主要有這些類型:

防火牆問題,缺少gc++,缺少pcre、zlib等庫。

三:Nginx的反向代理和負載均衡

什麼是反向代理

我們有時候,用自己的計算機A想訪問國外的某個網站B,但是訪問不了,此時,有一台中間服務器C可以訪問國外的網站B,那麼,我們可以用自己的電腦訪問服務器C,通過C來訪問B這個網站。那麼這個時候,服務器C稱為代理服務器,這種訪問方式叫做正向代理。正向代理有一個特點,就是我們明確知道要訪問哪個網站。再如,當我們有一個服務器集中,並且服務器集群中的每台服務器的內容一樣的時候,同樣我們要直接從個人電腦訪問到服務器集中的服務器的時候無法訪問,且此時第三方服務器能訪問集群,這個時候,我捫通過第三方服務器訪問服務器集群的內容,但是此吋我們並不知道是哪一台服務器提供的內容,此時的代理方式稱為反向代理。

正向代理

反向代理

什麼是負載均衡

當一台服務器的單位時間內的訪問量越大的時候,服務器的壓力會越大。當一台服務器壓力大得超過自身的承受能力的時候,服務器會崩潰。為了避免服務器崩潰,讓用戶有更好地體驗,我們通常通過負載均衡的方式來分擔服務器的壓力。那麼什麼是負載均衡呢?是這樣,我們可以建立很多很多個服務器,這些服務器組成一個服務器集群,然後,當用戶訪問我們網站的時候,先訪問一个中間服務器,再讓這个中間服務器在服務器集群中選擇一個壓力較小的服務器,然後將該訪問請求引入該選擇的服務器。這樣,用戶的每次訪問,都會保證服務器集群中的每個服務器的壓力趨於平衡,分擔了服務器壓力,避免了服務器崩潰的情況。

基於反向代理的原理實現負載均衡

四:Nginx負載均衡的實現

Nginx 是一款可以通過反向代理實現負載均衡的服務器,使用 Nginx 服務實現負載均衡的時候,用戶的訪問首先會訪問到 Nginx 服務器,然後 Nginx 服務器再從服務器集群表中選擇壓力較小的服務器,然後將該訪問請求引向該服務器。若服務器集群中的某個服務器崩潰,那麼從待選服務器列表中將該服務器刪除,也就是說一個服務器假如崩潰了,那麼 Nginx 就肯定不會將訪問請求引入該服務器了。那麼下面,我們通過實例來講解一下 Nginx 負載均衡的實現。

負載均衡配置文件

默認配置文件nginx.conf 很重要,我們一般是新建一個配置文件,在啟動時指定加載。

[root@hostname conf]# ls  //查看目錄
fastcgi.conf            koi-win             scgi_params
fastcgi.conf.default    mime.types          scgi_params.default
fastcgi_params          mime.types.default  uwsgi_params
fastcgi_params.default  nginx.conf          uwsgi_params.default
koi-utf                 nginx.conf.default  win-utf
[root@hostname conf]# touch fzjh.conf  //新建負載均衡配置文件

編輯fzjh.conf配置文件

vi fzjh.conf
worker_processes  1;#工作進程的個數,一般與計算機的cpu核數一致  
  
events {  
    worker_connections  1024;#單個進程最大連接數(最大連接數=連接數*進程數) 併發 
}  
  
http {  
    include       mime.types; #文件擴展名與文件類型映射表  
    default_type  application/octet-stream;#默認文件類型  
  
    sendfile        on;#開啟高效文件傳輸模式,sendfile指令指定nginx是否調用sendfile函數來輸出文件,對於普通應用設為 on,如果用來進行下載等應用磁盤IO重負載應用,可設置為off,以平衡磁盤與網絡I/O處理速度,降低系統的負載。注意:如果圖片显示不正常把這個改成off。  
      
    keepalive_timeout  65; #長連接超時時間,單位是秒  
  
    gzip  on;#啟用Gizp壓縮  
      
    #服務器的集群  
    upstream  myproject {  #服務器集群名字   
        server    220.181.111.188:80  weight=1;#服務器配置   weight是權重的意思,權重越大,分配的概率越大。  
        server    42.121.252.58:80  weight=2;  
    }     
  
    #當前的Nginx的配置  
    server {  
        listen       80;  #監聽80端口,可以改成其他端口  
        server_name  localhost; ##############   當前服務的域名  
  
        location / {  ##配置路徑/下實現負載均衡
            proxy_pass http://myproject;  ##對應哪個服務器集群
            proxy_redirect default;  
        }  
          
        error_page   500 502 503 504  /50x.html;  
        location = /50x.html {  
           root   html;  #根目錄
        }  
    }  
}  

我們在nginx目錄下執行命令,啟動負載均衡

./sbin/nginx -c ./conf/fzjh.conf

然後嘗試訪問你的服務器,多訪問幾次,你會發現它會訪問2次csdn網站,1次百度。說明我們的負載均衡已經部署完畢。

如何重啟Nginx

【本文版權歸微信公眾號”代碼藝術”(ID:onblog)所有,若是轉載請務必保留本段原創聲明,違者必究。若是文章有不足之處,歡迎關注微信公眾號私信與我進行交流!】

./sbin/nginx -s reload

如何關閉Nginx服務器

1.查看nginx進程號

ps -ef|grep nginx

2.kill掉進程即可 (1709是第二列的進程號)

kill 1709

或者

killall -9 nginx

五:HTTP Upstream 模塊

HTTP Upstream 模塊

Upstream 模塊是 Nginx 服務器的一個重要模塊。 Upstream 模塊實現在輪詢和客戶端 iP 之間實現後端的負載均衡。常用的指令有 ip_hash指令、 server 指令和 upstream 指令等,下面我們分別來講一下。

Http Upstream模塊- ip_hash指令

在負載均衡系統中,假如用戶在某台服務器上登錄,那麼如果該用戶第二次請求的時候,因為我們是負載均衡系統,每次請求都會重新定位到服務器集群中的一個服務器,那麼此時如果將已經登錄服務器A的用戶再定位到其他服務器,顯然不妥。故而,我們可以採用 ip_hash指令解決這個問題,如果客戶端請求已經訪問了服務器A並登錄,那麼第二次請求的時候,會將該請求通過哈希算法自動定位到該後端服務器中。下面我們通過實例講解。

實例

此時不應該使用weight權重。


tp {  
   ....
  upstream  myproject {    
      ip_hash; #實現會話跟蹤
      server 140.205.140.234;
      server 61.135.169.125;

  }     
 ....

Http Upstream 模塊一 upstream 指令及相關變量

upstream 指令主要是用於設置一組可以在 proxy_pass 和 fastcgi_pass 指令中使用代理服務器,默認負載均衡方式為輪詢。

六:其他負載均衡實現方式

負載均衡的實現方法除了可以使用 Nginx服務器實現外,還可以通過很多種方法來實現。負載均衡的核心就是建立一個服務器集群,然後用戶首先訪問到第三方代理服務器,然後由代理服務器選擇一個集群中的服務器,然後將請求引入選定的服務器。那麼代理服務器可以使用多種方式來充當,故而實現負載均衡的方式也是多種。總的來說,負載均衡實現的方式分為軟件實現和硬件實現兩種,如果中間的代理機構是硬件,那麼就是通過硬件設備來實現負載均衡的方式,如果中間的代理機構為軟件,就是軟件實現負載均衡的方式。而其中,軟件又可以是服務器軟件、系統軟件以及應用軟件等充當。

負載均衡實現方式小結

下面我們簡單總結一下負載均衡不同實現方式的優缺點:

假如使用硬件的方式實現負載均衡,那麼中間的轉發機構就是硬件,這個時候運行的效率非常高,但是對應的成本也非常高。如果我們採用軟件的方式來實現負載均衡,那麼中間的轉發機構就是軟件,這個時候,運行效率不如硬件,但是成本相對來說低得多。而使用Nginx服務器實現負載均衡,那麼就是通過軟件的方式來實現負載均衡,並且 Nginx本身支持高併發等。故而使用 Nginx服務器實現負載均衡,能大大節約企業的成本,並且由於 Nginx是服務器軟件,其執行效率也是非常高。

七:location匹配順序

  1. “=”前綴指令匹配,如果匹配成功,則停止其他匹配
  2. 普通字符串指令匹配,順序是從長到短,匹配成功的location如果使用^~,則停止其他匹配(正則匹配)
  3. 正則表達式指令匹配,按照配置文件里的順序,成功就停止其他匹配
  4. 如果第三步中有匹配成功,則使用該結果,否則使用第二步結果

注意點

  1. 匹配的順序是先匹配普通字符串,然後再匹配正則表達式。另外普通字符串匹配順序是根據配置中字符長度從長到短,也就是說使用普通字符串配置的location順序是無關緊要的,反正最後nginx會根據配置的長短來進行匹配,但是需要注意的是正則表達式按照配置文件里的順序測試。找到第一個比配的正則表達式將停止搜索。
  2. 一般情況下,匹配成功了普通字符串location后還會進行正則表達式location匹配。有兩種方法改變這種行為,其一就是使用“=”前綴,這時執行的是嚴格匹配,並且匹配成功后立即停止其他匹配,同時處理這個請求;另外一種就是使用“^~”前綴,如果把這個前綴用於一個常規字符串那麼告訴nginx 如果路徑匹配那麼不測試正則表達式。

匹配模式及順序

  location = /uri    =開頭表示精確匹配,只有完全匹配上才能生效。

  location ^~ /uri   ^~ 開頭對URL路徑進行前綴匹配,並且在正則之前。

  location ~ pattern  ~開頭表示區分大小寫的正則匹配。

  location ~* pattern  ~*開頭表示不區分大小寫的正則匹配。

  location /uri     不帶任何修飾符,也表示前綴匹配,但是在正則匹配之後。

  location /      通用匹配,任何未匹配到其它location的請求都會匹配到,相當於switch中的default。

八:配置HTTPS

1.獲取證書

獲得SSL證書文件 1_www.domain.com_bundle.crt 和私鑰文件 2_www.domain.com.key

2.證書安裝

將域名 www.domain.com 的證書文件1_www.domain.com_bundle.crt 、私鑰文件2_www.domain.com.key保存到同一個目錄,例如/usr/local/nginx/conf目錄下。

3.配置conf

打開nginx.conf文件,找到nginx.conf的下段配置內容:

# HTTPS server
#
#server {
#    listen       443 ssl;
#    server_name  localhost;

#    ssl_certificate      cert.pem;
#    ssl_certificate_key  cert.key;

#    ssl_session_cache    shared:SSL:1m;
#    ssl_session_timeout  5m;

#    ssl_ciphers  HIGH:!aNULL:!MD5;
#    ssl_prefer_server_ciphers  on;

#    location / {
#        root   html;
#        index  index.html index.htm;
#    }
#}

打開註釋,修改server_name為綁定證書的域名(如:www.domain.com),修改ssl_certificate 為 1_www.domain.com_bundle.crt,修改 ssl_certificate_key 為 2_www.domain.com.key 即可。

4.HTTP自動跳轉HTTPS

對於用戶不知道網站可以進行https訪問的情況下,讓服務器自動把http的請求重定向到https。 在服務器這邊的話配置的話,可以在頁面里加js腳本,也可以在後端程序里寫重定向,當然也可以在web服務器來實現跳轉。

Nginx是支持rewrite的(只要在編譯的時候沒有去掉pcre) 在http的server里增加rewrite ^(.*) https://$host$1 permanent; 這樣就可以實現80進來的請求,重定向為https了。

還是在此配置文件中,加入下面一句:

server {
        listen       80;
        server_name  localhost;
        
		rewrite ^(.*) https://$host$1 permanent;
		...

版權聲明

本文版權歸微信公眾號”代碼藝術”(ID:onblog)所有,若是轉載請務必保留本段原創聲明,違者必究。若是文章有不足之處,歡迎關注微信公眾號私信與我進行交流!

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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