標籤: 網頁設計公司

  • 有德系有美系,這幾款大7座SUV比漢蘭達還牛得多

    有德系有美系,這幾款大7座SUV比漢蘭達還牛得多

    98-63。98萬細心的朋友可能會發現,探險者的外觀設計有幾分“路虎車”的影子,如果你真的有這樣的直覺,那就對了,探險者的確和“路虎”之間有點不為人知的故事,感興趣的話可以上網找找哦。超大的尺寸仍然是這個級別車型的亮點,外觀大氣是無可厚非的,探險者車身上的設計即帶有都市的時尚感,亦有硬派的越野風,這也是路虎車的風格。

    要說大七座SUV,我想不少人第一時間就想到漢蘭達,但是對於大眾粉來說,第一時間想到的或許是大眾的途昂。途昂作為一款比途觀L 定位更高的SUV,售價區間為30.89-51.89萬,比起漢蘭達23.98-42.28萬的售價,雖然重疊度很高,但也能明顯看出途昂的定位要高於漢蘭達。

    如果宏觀市面上的所有合資中大型SUV,途昂可是說是定價非常低的一款,以至於它沒有多少直接的競爭對手,這也是他能在最近一年時間里月銷量能到達1萬輛左右的原因之一,那實際上到底在這個價位還有誰能與之匹敵?我們把終端優惠的價格也考慮進去,發現豐田的普拉多和福特的探險者似乎也很吸引人,對比一波看看。

    上汽大眾-途昂

    指導價:30.89-51.89萬

    很多人第一眼看到途昂的時候可能都會以為是途觀L再加長后的SUV,因為他們套用的是同一套設計語言,呈現的效果很一致。途昂除了尺寸更大了,其實也有其他明顯的區別,就是途昂前後翼子板的腰線呈隆起的形狀,使其整體看起來更霸氣。當時話說回來,其實大眾車最終還是講究由簡潔的設計營造高級感,所以其實這款途昂的外觀也就一般般吧,不太高級,只是比較霸氣而已。

    一汽豐田-普拉多

    指導價:46.48-63.68萬

    普拉多的外觀造型可是說是整個SUV界最獨居一格的,在如今的SUV設計風格都以精緻化、年輕化的方向靠攏的環境下,普拉多依舊以“粗曠”的設計營造出硬派的風格,最獨特的當然是“兇悍”的前臉,還有就是滿身肌肉感的車身,雖然沒有什麼平直硬朗的線條,但整體車身外觀也彰顯出很強的力量感。

    福特(進口)-探險者

    指導價:44.98-63.98萬

    細心的朋友可能會發現,探險者的外觀設計有幾分“路虎車”的影子,如果你真的有這樣的直覺,那就對了,探險者的確和“路虎”之間有點不為人知的故事,感興趣的話可以上網找找哦。超大的尺寸仍然是這個級別車型的亮點,外觀大氣是無可厚非的,探險者車身上的設計即帶有都市的時尚感,亦有硬派的越野風,這也是路虎車的風格。

    尺寸對比

    尺寸方面,三車的長度很接近,探險者的車寬最寬;普拉多的車高最高;而途昂的軸距是最長的,而且比另外兩部車都要長不少,長軸距的車型更利於車內空間的布置,不過由於三輛車都已經達到了中大型SUV級別,而且都是7座車型,所以空間方面其實不必多慮。

    內飾對比

    雖然三輛車都不是豪華品牌,但是從售價來看,三輛車都超越了普通品牌的水平,所以內飾方面也絕對不能落伍。途昂的內飾還是很“大眾”,滿滿的家族氣息,只不過比起其他大眾車型,橫向面積更舒展,而且用料也更高級。普拉多的內飾一直都飽受好評,坐進車內后很容易就被其舒適性所征服,軟材質應用到位。但探險者的內飾設計就比較中庸,這也是福特車的特點,設計一向以來都比較平淡,不過用料方面還是很厚道。

    動力對比

    途昂的動力配備,由於選擇比較多,而且還應用了2.0T低功率發動機,以此將其門檻拉得很低,186匹馬力,這麼大的車,其實還是有點勉強吧。普拉多則全系搭載3.5L自然吸氣發動機,配備6AT的變速箱,這套動力總成無論是在鋪裝路面的日常駕駛,還是在一般的越野道路都可以說游刃有餘吧。而探險者的最低動力是2.3T的渦輪增壓發動機,276匹,是肯定夠用的,這個無需擔心。

    總結

    從以上對比就可以看出,其實途昂的起步價能這麼低,主要還是入門車型搭載了一款功率比較一般的發動機。但是我們綜合各方面表現,特別是配置方面(由於途昂配置領先很多,所以這次沒作對比),途昂還是很有優勢的,所以結論是,大七座SUV,途昂非常值得推薦。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

  • 最低5.48萬起,這些車代表了國產車最高水準?

    最低5.48萬起,這些車代表了國產車最高水準?

    相比較其它車企來說,寶駿就簡單粗暴多了,價格就是寶駿最大的優勢,因此寶駿也有價格屠夫的稱號,早期寶駿推出寶駿樂馳和寶駿630的時候其實價格優勢並不明顯,而從寶駿730和560開始,寶駿的高性價比稱號便越打越響,而310和510的上市也直接讓其坐穩了家轎市場的頭把交椅。

    俗話說,沒有兩把刷子,怎麼能出來混社會;做人如此,造車也是這樣,對於各家車企來說,它們的產品雖然都各有優缺,但是那些賣的好混的不錯的車企卻往往有它們的看家本領。中國汽車品牌的繁榮發展雖然不過短短20年,但是卻也有不少的車企做得越來越大,在中國汽車市場上呼聲較高,除了自主車向來的高性價比之外,這些自主品牌都有什麼看家本領呢?

    長安這些年來混得是順風順水,相當不錯;說起長安,可能很多人想到的是長安車型漂亮的外觀內飾,長安最新的睿騁CC和逸動就十分帥氣。

    不過長安在自主車型中優勢最大的其實還是底盤,長安的車型底盤的厚實感和懸挂的濾震處理、側傾抑制都是相當出色的。長安的車型在底盤的質感上有一種歐系車的感覺,這是很多自主車型望塵莫及的。

    代表車型:長安CS95

    作為長安目前產品序列中最高級也是最碩大的車型,CS95代表了長安的產品高度,而實際上這款車的表現也是可圈可點,厚實的底盤不錯的隔音以及較高的性價比都是CS95的競爭力所在。

    在自主車型中如果要問誰的發動機最好的話,那麼一定是榮威和MG的車型,目前榮威和MG產品序列中有兩款發動機堪稱強悍,那就是1.5T和2.0T的兩款渦輪增壓發動機,前者擁有169馬力/250牛米的動力,後者輸出更是高達220馬力/350牛米。

    這樣的兩台發動機加持使得榮威MG的車型動力十分強悍,我們實測的1.5T名爵6百公里加速時間僅需7.09秒,可以說相當厲害了。除此之外搭載這兩台發動機的車型油耗也普遍比同規格的自主車型更低,實力可見一斑。

    代表車型:名爵6

    作為名爵榮威最有代表的車型,名爵6的動力十分強悍,1.5T的發動機加持下實測百公里加速僅需7.09秒,而且混合動力版本的名爵6加速還更加恐怖;除了強大的動力之外,名爵6在科技感上也相當出色,加上不錯的外觀,名爵6銷量也比較不錯了。

    相比較其它車企來說,寶駿就簡單粗暴多了,價格就是寶駿最大的優勢,因此寶駿也有價格屠夫的稱號,早期寶駿推出寶駿樂馳和寶駿630的時候其實價格優勢並不明顯,而從寶駿730和560開始,寶駿的高性價比稱號便越打越響,而310和510的上市也直接讓其坐穩了家轎市場的頭把交椅。

    寶駿的車型在保證品質的基礎上,價格卻總是比同規格的哈弗等車型便宜不少,因此也獲得了市場的熱捧,我們老闆就因為看中了寶駿的低價高質,買了台寶駿730給我們做公務用車呢~

    代表車型:寶駿510

    自從哈弗H6誕生以來,連續幾年的時間霸佔銷量榜首位,不過510發力之後完成了對哈弗H6的超越,憑藉超高的性價比以及靚麗的外觀,寶駿510為寶駿品牌的形象以及銷量立下了汗馬功勞,可以說510成就了如今的寶駿。

    寶馬7系的遙控泊車可以說是一個創舉,但是第一個做遙控泊車的卻並非寶馬,而是咱們的比亞迪,並且比亞迪的遙控泊車不僅可以控制前後行駛,還能轉向,這就是比亞迪在科技配置上創新的體現,而諸如此類的电子配置很多,比如說全景影像、綠凈系統、可以自動切換內外循環的空調以及自帶記錄儀等功能都是比亞迪率先涉足的,在科技配置方面比亞迪說第二,傳統車企里估計沒人敢認第一。

    代表車型:宋MAX

    雖說宋MAX並非是比亞迪最高端的車型,但確實是比亞迪目前最受市場歡迎的車型了,除了那個漂亮的外觀和內飾之外,宋MAX的配置也是非常誇張,前後雷達、全景影像、電動尾門、前排座椅通風加熱、12.8英寸大屏、全LED大燈等配置出現在這麼一台十萬級的MpV也是十分誇張了。

    從前有這麼一個段子,長城的老闆公布年度預算:“研發部門500萬,設計部門10億”,雖然是車友編出來的一個段子,但是也能看得出公眾對於長城車型的印象。而這一個設計花十億的印象也是直接來自於長城現有的車型。

    在自主車型中長城的外觀內飾精緻度確實非常高,除了最近哈弗車系大面積推廣的电子手剎之外,在車輛的鈑金噴漆水平、內飾的用料做工甚至按鍵的手感質感上哈弗都在不遺餘力地做好,而這樣帶來的好處就是哈弗的車型與廉價感這一詞已經完全脫離關係了,無論是視覺感受還是觸感都十分出色,讓人愛不釋手。

    代表車型:哈弗H7

    目前哈弗產品中定位最高的並不是哈弗H7,但是在內飾外觀精緻度上認為哈弗H7是最出色的,尤其是擋桿後方的按鈕分佈,看起來用起來都有幾分奧迪的精緻感,加上車內不錯的做工用料,這個內飾的品質感絕對不輸30萬的合資車。

    雖然長安車型的底盤質感不錯,但是長安為了迎合年輕人而做了很多運動化調校,因此多數長安車型的底盤濾振不算徹底,要說自主車型中哪個品牌的車濾振最能給人好感,虎哥覺得一定是東南。

    東南車型雖然外觀足夠靚麗,但是並非是普通貨色,在濾振水平上東南可謂相當出色,比如說東南DX3和東南DX7的濾振就足夠徹底,路面上大大小小的振動都能被過濾得十分到位,即使是快速軋過井蓋也不會有單薄感和突兀感,感覺有一種貼地飛行的質感,濾振水平完全不遜色於別克。

    代表車型:東南DX3

    在試駕DX3就對DX3的濾震水平十分震驚,激烈駕駛時底盤的安穩感很好、經過大大小小的坑窪時車內感覺都相當淡定,懸挂的濾振表現值得表揚,底盤的厚實感也是比較到位了,超過同價位的多數自主車。

    2010年前後,中國市場掀起了一股合資自主品牌的風潮,當時許多諸如理念、啟辰、朗世之類的合資自主品牌誕生,但是大潮褪去才知道誰在裸泳,如今完整活下來並且混得還不錯的也就剩下啟辰了。

    而說起啟辰最厲害的地方,那就是啟辰的車型基本就是換殼的日產,比如說T70實際上就是海外的7座版老款逍客、D60就是軒逸、D50/R50也就是老款的日產騏達,因此在三大件和造車水準上啟辰也原封不動地繼承了日產的基因,但是在價格上啟辰比哈弗之類的自主車有過之而無不及,因此賣得好也不足為奇了。

    代表車型:啟辰T70

    啟辰T70實際上就是海外7座版逍客的底盤,加上日產那套成熟的2.0L+CVT動力總成,雖然動力不算強悍,但是勝在可靠性和燃油經濟性好,這也是選擇啟辰的一大重要原因,在性價比上T70也超過了大多數的自主車,比如說11.78萬的T70 2.0L CVT睿享版就有定速巡航、胎壓監測、一鍵啟動無鑰匙進入、主駕駛電動座椅等高端裝備。

    有神似奧迪Q3的SR7、跟保時捷macan傻傻分不清的SR9、撞臉大眾概念SUV的大邁X7、有奧迪A6L內涵的眾泰Z700以及神似奧迪Q5的眾泰T600,花五分之一的錢買一輛豪車,誰能不心動?

    代表車型:幾乎全系車型~本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

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

    質量硬,性價比無敵,懂車的都說這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?

    教授試了一款不到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/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

  • mingw32 exception在sjlj與dwarf差別-反彙編分析

    mingw32 exception在sjlj與dwarf差別-反彙編分析

    sjlj (setjump/longjump)與dwarf-2為mingw32兩種異常處理模型的實現。sjlj有着開銷,而隨linux發行的mingw32開發庫包都是用sjlj版編譯的,而Qt卻採用dwarf-2版,那麼兩者之間有多少差異,本文就這問題對兩版的異常代碼的反彙編進行分析比較。

    我使用mingw-w65-i686-810的sjlj與dwarf-2兩個版本對下面異常代碼編譯。

    __attribute__((dllimport)) int dllfunc();
    int main()
    {
        dllfunc();
        //_create_locale(LC_ALL, "C");
        printf("abc");
        //return 0;
    
        try
        {
            try
            {
                throw std::exception();
            }
            catch(std::exception&)
          {
                std::rethrow_exception(std::current_exception());
          }
            
        }
        catch(int)
        {
            
        }
        catch(std::exception& e)
        {
            std::cout << e.what() << std::endl;
        }
        catch(...)
        {
            std::cout << "unknown" << std::endl;
        }
        return 0;
    }

    代碼邏輯:

    兩層 try/catch,

    1. 裡層 try/catch

    1.1 try塊, throw 異常

    1.2 catch塊, rethrow

    2. 外層 try/catch

    2.1 有三catch分支。

     

    開刷前,先定義一下。

    如果將 try/catch 去除 c++語言特性后,基本就是一種由c++庫還有c++編譯器共同管理的 goto。

    throw相當於goto, catch相當於label(一種以類型區分的)。

    那麼c++編譯器與c++庫為我們提供了什麼樣的管理呢?

    c++編譯器

    0. 利用c++支持對象析構進行try塊保護。

    1. 將 throw 關鍵字生成彙編 call __cxa_throw,調用 c++庫的函數。

    2. 為每個catch塊生成代碼片斷,只能通過jmp跳轉進來。

    2.1 開頭 call __cxa_begin_catch。

    2.2 結尾 call __cxa_end_catch。

    2.3 最後跳出到 try/catch塊邏輯代碼的下條執行指令。

    3. 為同一try/catch塊的所有catch塊產生分支控制代碼。

    4. 為try塊的析構代碼產生跳轉入口。

    5. 為每一層try/catch塊生成 uncaught 代碼塊,調用 _Unwind_Resume。

    c++庫:

    1. __cxa_throw,馬上_Unwind_RaiseException。跳轉到當前最裏面一層 try/catch的支路控制代碼片斷。

    2. _Unwind_Resume,向上繼續展開。

    3. std::rethrow_exception,調用 __gcclibcxx_demangle_callback,

    3.1 要麼有 catch可達跳回到原來代碼的控制流,直接離開std::rethrow_exception的調用上下文。

    3.2 要麼從__gcclibcxx_demangle_callback返回,執行terminate結束進程。

     

    sjlj 版的反彙編代碼比 dwarf-2 版的多了50行。

    先來看dwarf-2的反彙編代碼 

      1  <+0>:    lea    0x4(%esp),%ecx
      2  <+4>:    and    $0xfffffff0,%esp
      3  <+7>:    pushl  -0x4(%ecx)
      4  <+10>:    push   %ebp
      5  <+11>:    mov    %esp,%ebp
      6  <+13>:    push   %esi
      7  <+14>:    push   %ebx
      8  <+15>:    push   %ecx
      9  <+16>:    sub    $0x2c,%esp
     10  <+19>:    call   0x401890 <__main>
     11  <+24>:    mov    0x4071a4,%eax
     12  <+29>:    call   *%eax
     13  <+31>:    movl   $0x404045,(%esp)
     14  <+38>:    call   0x4027c4 <printf>
     15  <+43>:    movl   $0x4,(%esp)
     16  <+50>:    call   0x4017ac <__cxa_allocate_exception>
     17  <+55>:    mov    %eax,%ebx
     18  <+57>:    mov    %ebx,%ecx
     19  <+59>:    call   0x402890 <std::exception::exception()>
     20  <+64>:    movl   $0x4017d4,0x8(%esp)
     21  <+72>:    movl   $0x4042a8,0x4(%esp)
     22  <+80>:    mov    %ebx,(%esp)
     23  <+83>:    call   0x401794 <__cxa_throw>
     24  <+88>:    mov    $0x0,%eax
     25  <+93>:    jmp    0x401723 <main()+355>
     26  <+98>:    mov    %edx,%ecx
     27  <+100>:    cmp    $0x2,%ecx
     28  <+103>:    je     0x40162b <main()+107>
     29  <+105>:    jmp    0x401663 <main()+163>
     30  <+107>:    mov    %eax,(%esp)
     31  <+110>:    call   0x4017a4 <__cxa_begin_catch>
     32  <+115>:    mov    %eax,-0x1c(%ebp)
     33  <+118>:    lea    -0x28(%ebp),%eax
     34  <+121>:    mov    %eax,(%esp)
     35  <+124>:    call   0x4017cc <_ZSt17current_exceptionv>
     36  <+129>:    lea    -0x28(%ebp),%eax
     37  <+132>:    mov    %eax,(%esp)
     38  <+135>:    call   0x4017c4 <_ZSt17rethrow_exceptionNSt15__exception_ptr13exception_ptrE>
     39  <+140>:    mov    %eax,%esi
     40  <+142>:    mov    %edx,%ebx
     41  <+144>:    lea    -0x28(%ebp),%eax
     42  <+147>:    mov    %eax,%ecx
     43  <+149>:    call   0x4017ec <_ZNSt15__exception_ptr13exception_ptrD1Ev>
     44  <+154>:    call   0x40179c <__cxa_end_catch>
     45  <+159>:    mov    %esi,%eax
     46  <+161>:    mov    %ebx,%edx
     47  <+163>:    cmp    $0x1,%edx
     48  <+166>:    je     0x40166f <main()+175>
     49  <+168>:    cmp    $0x2,%edx
     50  <+171>:    je     0x401683 <main()+195>
     51  <+173>:    jmp    0x4016ca <main()+266>
     52  <+175>:    mov    %eax,(%esp)
     53  <+178>:    call   0x4017a4 <__cxa_begin_catch>
     54  <+183>:    mov    (%eax),%eax
     55  <+185>:    mov    %eax,-0x24(%ebp)
     56  <+188>:    call   0x40179c <__cxa_end_catch>
     57  <+193>:    jmp    0x401618 <main()+88>
     58  <+195>:    mov    %eax,(%esp)
     59  <+198>:    call   0x4017a4 <__cxa_begin_catch>
     60  <+203>:    mov    %eax,-0x20(%ebp)
     61  <+206>:    mov    -0x20(%ebp),%eax
     62  <+209>:    mov    (%eax),%eax
     63  <+211>:    add    $0x8,%eax
     64  <+214>:    mov    (%eax),%eax
     65  <+216>:    mov    -0x20(%ebp),%edx
     66  <+219>:    mov    %edx,%ecx
     67  <+221>:    call   *%eax
     68  <+223>:    mov    %eax,0x4(%esp)
     69  <+227>:    movl   $0x6ff07a00,(%esp)
     70  <+234>:    call   0x4017b4 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
     71  <+239>:    movl   $0x4017bc,(%esp)
     72  <+246>:    mov    %eax,%ecx
     73  <+248>:    call   0x4017f4 <_ZNSolsEPFRSoS_E>
     74  <+253>:    sub    $0x4,%esp
     75  <+256>:    call   0x40179c <__cxa_end_catch>
     76  <+261>:    jmp    0x401618 <main()+88>
     77  <+266>:    mov    %eax,(%esp)
     78  <+269>:    call   0x4017a4 <__cxa_begin_catch>
     79  <+274>:    movl   $0x404049,0x4(%esp)
     80  <+282>:    movl   $0x6ff07a00,(%esp)
     81  <+289>:    call   0x4017b4 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
     82  <+294>:    movl   $0x4017bc,(%esp)
     83  <+301>:    mov    %eax,%ecx
     84  <+303>:    call   0x4017f4 <_ZNSolsEPFRSoS_E>
     85  <+308>:    sub    $0x4,%esp
     86  <+311>:    call   0x40179c <__cxa_end_catch>
     87  <+316>:    jmp    0x401618 <main()+88>
     88  <+321>:    mov    %eax,%ebx
     89  <+323>:    call   0x40179c <__cxa_end_catch>
     90  <+328>:    mov    %ebx,%eax
     91  <+330>:    mov    %eax,(%esp)
     92  <+333>:    call   0x402770 <_Unwind_Resume>
     93  <+338>:    mov    %eax,%ebx
     94  <+340>:    call   0x40179c <__cxa_end_catch>
     95  <+345>:    mov    %ebx,%eax
     96  <+347>:    mov    %eax,(%esp)
     97  <+350>:    call   0x402770 <_Unwind_Resume>
     98  <+355>:    lea    -0xc(%ebp),%esp
     99  <+358>:    pop    %ecx
    100  <+359>:    pop    %ebx
    101  <+360>:    pop    %esi
    102  <+361>:    pop    %ebp
    103  <+362>:    lea    -0x4(%ecx),%esp
    104  <+365>:    ret    

    我們的主要代碼邏輯只有20-30條指令

     

     當 throw時,__cxa_throw函數是不會返回的, 如同goto最後是跳轉到他處,若被本層catch處理完才會跳轉回來<+88>。

    然後看c++編譯器為我們生成的異常代碼 。

     

     

     

     

     

     對於沒有發生異常時,代碼執行路徑基本不會去涉及到異常代碼支路,開銷幾近為0,只是代碼量增大。

    下面來看 sjlj 版的彙編代碼,

      1 function main():
      2  <+0>:    lea    0x4(%esp),%ecx
      3  <+4>:    and    $0xfffffff0,%esp
      4  <+7>:    pushl  -0x4(%ecx)
      5  <+10>:    push   %ebp
      6  <+11>:    mov    %esp,%ebp
      7  <+13>:    push   %edi
      8  <+14>:    push   %esi
      9  <+15>:    push   %ebx
     10  <+16>:    push   %ecx
     11  <+17>:    sub    $0x68,%esp
     12  <+20>:    movl   $0x4017ac,-0x44(%ebp)
     13  <+27>:    movl   $0x402958,-0x40(%ebp)
     14  <+34>:    lea    -0x3c(%ebp),%eax
     15  <+37>:    lea    -0x18(%ebp),%ebx
     16  <+40>:    mov    %ebx,(%eax)
     17  <+42>:    mov    $0x4015b4,%edx
     18  <+47>:    mov    %edx,0x4(%eax)
     19  <+50>:    mov    %esp,0x8(%eax)
     20  <+53>:    lea    -0x5c(%ebp),%eax
     21  <+56>:    mov    %eax,(%esp)
     22  <+59>:    call   0x402790 <_Unwind_SjLj_Register>
     23  <+64>:    call   0x4018b0 <__main>
     24  <+69>:    mov    0x406194,%eax
     25  <+74>:    movl   $0xffffffff,-0x58(%ebp)
     26  <+81>:    call   *%eax
     27  <+83>:    movl   $0x404001,(%esp)
     28  <+90>:    call   0x4027e4 <printf>
     29  <+95>:    movl   $0x4,(%esp)
     30  <+102>:    call   0x4017cc <__cxa_allocate_exception>
     31  <+107>:    mov    %eax,-0x60(%ebp)
     32  <+110>:    mov    %eax,%ecx
     33  <+112>:    call   0x4028b0 <std::exception::exception()>
     34  <+117>:    movl   $0x4017f4,0x8(%esp)
     35  <+125>:    movl   $0x404264,0x4(%esp)
     36  <+133>:    mov    -0x60(%ebp),%eax
     37  <+136>:    mov    %eax,(%esp)
     38  <+139>:    movl   $0x1,-0x58(%ebp)
     39  <+146>:    call   0x4017b4 <__cxa_throw>
     40  <+151>:    mov    $0x0,%eax
     41  <+156>:    mov    %eax,-0x60(%ebp)
     42  <+159>:    jmp    0x401733 <main()+547>
     43  <+164>:    lea    0x18(%ebp),%ebp
     44  <+167>:    mov    -0x54(%ebp),%edx
     45  <+170>:    mov    -0x50(%ebp),%ecx
     46  <+173>:    mov    -0x58(%ebp),%eax
     47  <+176>:    test   %eax,%eax
     48  <+178>:    je     0x4015e6 <main()+214>
     49  <+180>:    sub    $0x1,%eax
     50  <+183>:    test   %eax,%eax
     51  <+185>:    je     0x40161b <main()+267>
     52  <+187>:    sub    $0x1,%eax
     53  <+190>:    test   %eax,%eax
     54  <+192>:    je     0x4016f8 <main()+488>
     55  <+198>:    sub    $0x1,%eax
     56  <+201>:    test   %eax,%eax
     57  <+203>:    je     0x401712 <main()+514>
     58  <+209>:    sub    $0x1,%eax
     59  <+212>:    ud2    
     60  <+214>:    mov    %edx,%eax
     61  <+216>:    mov    %ecx,%edx
     62  <+218>:    mov    %edx,%ecx
     63  <+220>:    cmp    $0x2,%ecx
     64  <+223>:    je     0x4015f3 <main()+227>
     65  <+225>:    jmp    0x401642 <main()+306>
     66  <+227>:    mov    %eax,(%esp)
     67  <+230>:    call   0x4017c4 <__cxa_begin_catch>
     68  <+235>:    mov    %eax,-0x1c(%ebp)
     69  <+238>:    lea    -0x28(%ebp),%eax
     70  <+241>:    mov    %eax,(%esp)
     71  <+244>:    call   0x4017ec <_ZSt17current_exceptionv>
     72  <+249>:    lea    -0x28(%ebp),%eax
     73  <+252>:    mov    %eax,(%esp)
     74  <+255>:    movl   $0x2,-0x58(%ebp)
     75  <+262>:    call   0x4017e4 <_ZSt17rethrow_exceptionNSt15__exception_ptr13exception_ptrE>
     76  <+267>:    mov    %edx,-0x60(%ebp)
     77  <+270>:    mov    %ecx,-0x64(%ebp)
     78  <+273>:    lea    -0x28(%ebp),%eax
     79  <+276>:    mov    %eax,%ecx
     80  <+278>:    call   0x40180c <_ZNSt15__exception_ptr13exception_ptrD1Ev>
     81  <+283>:    mov    -0x60(%ebp),%eax
     82  <+286>:    mov    %eax,-0x60(%ebp)
     83  <+289>:    mov    -0x64(%ebp),%esi
     84  <+292>:    mov    %esi,-0x64(%ebp)
     85  <+295>:    call   0x4017bc <__cxa_end_catch>
     86  <+300>:    mov    -0x60(%ebp),%eax
     87  <+303>:    mov    -0x64(%ebp),%edx
     88  <+306>:    cmp    $0x1,%edx
     89  <+309>:    je     0x40164e <main()+318>
     90  <+311>:    cmp    $0x2,%edx
     91  <+314>:    je     0x401665 <main()+341>
     92  <+316>:    jmp    0x4016b3 <main()+419>
     93  <+318>:    mov    %eax,(%esp)
     94  <+321>:    call   0x4017c4 <__cxa_begin_catch>
     95  <+326>:    mov    (%eax),%eax
     96  <+328>:    mov    %eax,-0x20(%ebp)
     97  <+331>:    call   0x4017bc <__cxa_end_catch>
     98  <+336>:    jmp    0x4015a7 <main()+151>
     99  <+341>:    mov    %eax,(%esp)
    100  <+344>:    call   0x4017c4 <__cxa_begin_catch>
    101  <+349>:    mov    %eax,-0x24(%ebp)
    102  <+352>:    mov    -0x24(%ebp),%eax
    103  <+355>:    mov    (%eax),%eax
    104  <+357>:    add    $0x8,%eax
    105  <+360>:    mov    (%eax),%eax
    106  <+362>:    mov    -0x24(%ebp),%edx
    107  <+365>:    mov    %edx,%ecx
    108  <+367>:    call   *%eax
    109  <+369>:    mov    %eax,0x4(%esp)
    110  <+373>:    movl   $0x6ff29a00,(%esp)
    111  <+380>:    movl   $0x3,-0x58(%ebp)
    112  <+387>:    call   0x4017d4 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
    113  <+392>:    movl   $0x4017dc,(%esp)
    114  <+399>:    mov    %eax,%ecx
    115  <+401>:    call   0x401814 <_ZNSolsEPFRSoS_E>
    116  <+406>:    sub    $0x4,%esp
    117  <+409>:    call   0x4017bc <__cxa_end_catch>
    118  <+414>:    jmp    0x4015a7 <main()+151>
    119  <+419>:    mov    %eax,(%esp)
    120  <+422>:    call   0x4017c4 <__cxa_begin_catch>
    121  <+427>:    movl   $0x404005,0x4(%esp)
    122  <+435>:    movl   $0x6ff29a00,(%esp)
    123  <+442>:    movl   $0x4,-0x58(%ebp)
    124  <+449>:    call   0x4017d4 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
    125  <+454>:    movl   $0x4017dc,(%esp)
    126  <+461>:    mov    %eax,%ecx
    127  <+463>:    call   0x401814 <_ZNSolsEPFRSoS_E>
    128  <+468>:    sub    $0x4,%esp
    129  <+471>:    movl   $0xffffffff,-0x58(%ebp)
    130  <+478>:    call   0x4017bc <__cxa_end_catch>
    131  <+483>:    jmp    0x4015a7 <main()+151>
    132  <+488>:    mov    %edx,-0x60(%ebp)
    133  <+491>:    call   0x4017bc <__cxa_end_catch>
    134  <+496>:    mov    -0x60(%ebp),%eax
    135  <+499>:    mov    %eax,(%esp)
    136  <+502>:    movl   $0xffffffff,-0x58(%ebp)
    137  <+509>:    call   0x402788 <_Unwind_SjLj_Resume>
    138  <+514>:    mov    %edx,-0x60(%ebp)
    139  <+517>:    movl   $0x0,-0x58(%ebp)
    140  <+524>:    call   0x4017bc <__cxa_end_catch>
    141  <+529>:    mov    -0x60(%ebp),%eax
    142  <+532>:    mov    %eax,(%esp)
    143  <+535>:    movl   $0xffffffff,-0x58(%ebp)
    144  <+542>:    call   0x402788 <_Unwind_SjLj_Resume>
    145  <+547>:    lea    -0x5c(%ebp),%eax
    146  <+550>:    mov    %eax,(%esp)
    147  <+553>:    call   0x402780 <_Unwind_SjLj_Unregister>
    148  <+558>:    mov    -0x60(%ebp),%eax
    149  <+561>:    lea    -0x10(%ebp),%esp
    150  <+564>:    pop    %ecx
    151  <+565>:    pop    %ebx
    152  <+566>:    pop    %esi
    153  <+567>:    pop    %edi
    154  <+568>:    pop    %ebp
    155  <+569>:    lea    -0x4(%ecx),%esp
    156  <+572>:    ret    

    下面的分析只列出不同的地方 

     上圖的註釋有誤沒有勘誤過,lea是不訪問內存,通常代替add指令做加法,應該是6條指令要訪問內存。

    支路控制代碼:

     

     

     

     

     可以看出,支路選路控制指令多而且複雜,還有就是跳轉多。

    最後是函數結束前。

     

     

     

     可以看出在 sjlj 版本中,即使代碼不發生異常,函數在進入與離開時都要為登記維護付出一此成本,當涉及異常代碼時,支路選路控制更加複雜更多跳轉。這裡有一個成本比例,你的函數邏輯簡單,上面的開銷比重就越大,如果是頻繁調用的輕量函數就要考慮不用exception這樣的error handle。

    還有就是當發生異常時,需要交給c++庫去管理,不同異常處理模型的實現,有着不同的開銷,本文並沒有涉及到。只是單純從c++庫以外的代碼進行分析,也足夠看出他們之間有着一定的差別。

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

    【其他文章推薦】

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

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

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

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

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

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

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

    【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消費位移的那些事,包括:

    概念剖析

    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 + 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
    

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

    【其他文章推薦】

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

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

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

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

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