過年回家好多人買了哈弗和眾泰,到底該不該也買一輛?

再舉個生動栗子,的朋友陳二狗是一個下水道維修人員,經過幾年的艱苦工作,終於存夠錢買車了,但是不知道買什麼車。然鵝看到了隔壁的老王買了一台新車,外觀挺好看的,坐進去看起來配置也不錯,問一下價格竟然還不貴。

春節回家過年,是我們中國人傳統的習俗。過年除了回家探望親人,最大的目的還是在“炫耀”上,中國人都存在一種攀比的心理,比誰混得更好是春節回家的一個必備項目。汽車是一件“奢飾品”,作為身份的象徵,不少人都會在過年前買一台小汽車,開回老家彰顯身份。

也正是有這樣的“攀比”心理,導致中國人買車變得極度不理性。說實話“沒主見”是最主要的因素之一。因為購車所需要考慮的因素很多,不管你對車輛熟不熟悉,到了真正購車時,購車前所有的準備都煙消雲散,到了4S店你也只會 一味地跟着銷售的套路走下去。

為什麼消費者的購車思想都不理性?

影響消費者買車的因素有很多,像車型的品牌、車型的顏值、車型的配置、汽車的售價/優惠幅度、提車時間等,另外消費者們還有一個最為致命的共通點:就是跟風消費。

舉個栗子,就像哈弗H6那樣,每個月都有幾萬的銷量,一年能賣上幾十萬台的車型,難道就真的這麼多人買?那是肯定的,就好比國家在發展,人們的生活質量日益提高,所以不少家庭都開始將購車的計劃提上了議程。相對於一線大城市,在汽車保有量方面已經趨向於飽和甚至是過剩的狀態,所以目前銷售主力主要集中在二三線甚至是更偏的城市,而其中絕大部分的人都是首次購車的消費者。

其實大部分人都是和一樣的普通人,如果不是專業做汽車,也不知道買什麼車才是最好的選擇。對於自己本身來說,大多都是靠自己的努力辛辛苦苦存錢買車,很重視汽車的價格以及優惠幅度。也可以這樣說,如果是比較極端的消費者,在15萬以內的購車預算,兩個相差不大的競品車型,也許是因為某個車型有更多的優惠、價格更低。於是選擇了這台車。

再舉個生動栗子,的朋友陳二狗是一個下水道維修人員,經過幾年的艱苦工作,終於存夠錢買車了,但是不知道買什麼車。然鵝看到了隔壁的老王買了一台新車,外觀挺好看的,坐進去看起來配置也不錯,問一下價格竟然還不貴。再過了两天同村的老李又買了輛一模一樣的車,哇!同村這麼多人都買這車,質量肯定可以了,於是二狗高高興興地又買了一輛,於是他們愉快地創辦了二貨村殺馬特車友會。

其實大家都一樣,往往容易被表面迷惑,買車大多是看外觀數配置,這也是目前眾多車型品牌在追求的一個東西,外觀越來越大氣、配置越來越高,價格也越來越低了。這也是為什麼能夠火了部分喜歡“山寨”的品牌,譬如最近新出的保時泰SR9,10多萬的售價竟然有百萬級SUV的外觀,不僅如此連內飾的設計都有極高的相似度。當然消費者對於保時泰SR9的熱情是非常高的,關鍵是逼格夠高。還得說一下,能不能開上蘭博基尼就得看眾泰了。

只不過買這些車的人在選擇的時候壓根就沒有想過這車的質量到底行不行,至於眾泰這個品牌的質量能不能過關?還是建議去看一下車主的口碑,不是說質量不行,只是車型整體故障率還是比較高的。不過這類山寨車型還有一個好處呀,我還能剩下一大筆的設計費呀!

看得見的地方給到你逼格十足的感覺,但是在看不到不知道的地方就給你各方面的減配,你覺得這樣真的好嗎?為了降低成本,同一個部位的零件,哪個供應商便宜選哪個,就算質量差一點也是沒所謂,這樣的行為見得太多了!當然,這樣的行為不僅出現在國產品牌中,更有出現在合資品牌上。不過厚道的汽車廠家不少,只是消費者們不懂得識別,這也是目前我國消費者在購車方面最大的囧況。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

[Computer Vision]Harris角點檢測的詳細推導

Harris角點檢測

思想

為什麼要檢測角點呢?因為角點的特徵比較明顯。進行角點檢測的樸素思想是利用圖像梯度,也就是根據圖像強度的變化來尋找角點。如圖所示

這裏舉了個例子,給定一個小的區域(Patch),當這個小區域在不同位置滑動的時候,所呈現出來的一些特性是不同的,根據圖示,有三個方面。

  • Flat,平的地方,在任何方向,梯度都沒什麼變化。
  • Edge,邊的地方,當沿着邊方向的時候,梯度沒什麼變化。
  • Corner,角的地方,沿着任何方向,梯度都有變化。

Error Function

\[E(u,v)=\sum_{x,y}{w(x,y)[I(x+u,y+v)-I(x,y)]^2} \]

  • \(x,y\)是相對於一個小patch來說的,例如一個5*5的區域
  • \((u,v)\)是一個很小的移動量
  • \(w(x,y)\)是windows function,也就是對於每個點的權重,例如想讓中心的點權重高,可以用高斯核,一般就是全1或者高斯。
  • \(I(x,y)\)就代表圖像在\((x,y)\)的強度值。
  • 後面做差其實就是類似求梯度一樣

根據之前的討論,在一個patch里,如果有角點的存在,各個方向的梯度值都很大,於是乎,我們的目標是讓\(E(u,v)\)盡可能的大。
因為\((u,v)\)的值很小,所以我們可以利用二元函數的泰勒展開,來近似計算。
二元函數的泰勒展開,當然扔掉了一些項。

\[f(x+u,y+v) \approx f(x,y)+uf_x(x,y)+vf_y(x,y) \]

那麼我們對Error function中的關鍵部分進行展開

\[\begin{aligned} [I(x+u,y+v)-I(x,y)]^2 &\approx [I(x,y)+uI_x+vI_y-I(x,y)]\\ &=(uI_x+vI_y)^2\\ &=[u,v] \begin{bmatrix} I_x^2 &I_xI_y\\ I_xI_y&I_y^2 \end{bmatrix} \begin{bmatrix} u\\v \end{bmatrix} \end{aligned} \]

所以Error Function可以近似為

\[E(u,v)\approx [u,v]M\begin{bmatrix} u\\v \end{bmatrix} \]

\[M= \sum_{x,y}{w(x,y) \begin{bmatrix} I_x^2 &I_xI_y\\ I_xI_y&I_y^2 \end{bmatrix} } \]

這就涉及到線性代數里的二次型問題了。

簡單的二次型

例如 \(f(x,y) = x^2+y^2\)的可以寫作矩陣的形式

\[[x,y]\begin{bmatrix} 1 & 0\\ 0 & 1 \end{bmatrix} \begin{bmatrix} x\\y \end{bmatrix} \]

由中間這個矩陣來決定這個二次型的形狀,因為我們研究的二次型只有兩個變量,所以可以可視化來理解如下圖所示。對形狀矩陣可以進行特徵分解,分為中間的對角陣(對角線都是特徵值)兩邊是特徵向量。特徵向量代表了橢圓切片的長短軸的方向,而特徵值平方根的倒數代表了軸的長短。至於為什麼分解完會和橢圓對應,線性代數書上會有。

這樣就把Error Function給可視化了,有了幾何含義,更加直觀了。

  • Flat的時候,\((u,v)\)往哪個方向變化都不大,反應在幾何上,應該是一個較為平坦的面
  • Edge的時候,\((u,v)\)往某個方向變化大,反應在幾何上,應該是某個方向翹起。
  • Corner的時候,\((u,v)\)往大部分方向變化都大,反應在幾何上,應該是大部分方向都翹起。

如圖所示

我們可以通過兩個特徵值之間的大小關係,以及他們自身的關係來作為評估的依據。

當兩個特徵值都很大,且差不多時,意味着角點。

角點響應的度量

以上分析了,要兩個特徵值都很大,且同時大,那怎麼來度量?於是乎在最原始的論文里,這樣定義了響應函數,並且對不同的\(\lambda\)有以下的響應圖

\[R = det(M)-k(trace(M))^2\\ det(M) = \lambda_1\lambda_2\\ trace(M) = \lambda_1+\lambda_2 \]

\(k\)一般在是0.04-0.06

如圖所示,黃色的線是等值線,代表\(R\)的值都相同,左上角是\((0,0)\)點,往右下角去\(R\)的值越大,代表角點的響應越高,圖中畫了個綠線,右側的R值基本可以判斷為是角點了。另外還有一些別的響應函數,基本大同小異吧。

算法

所以現在經過以上的分析,總結一下角點檢測的算法步驟。

  1. 計算整個圖像的梯度值\(I_x,I_y\)
  2. 對於每個像素的\(I_{x^2}=I_xI_x,I_{y^2}=I_yI_y,I_{xy}=I_xI_y\)
  3. 計算每一個像素窗口的和,意思就是對於一個像素,定義一個領域例如5*5,就和之前提及的那樣,然後計算這個鄰域裏面所有第二步計算出來的值的和。\(S_{x^2}=G_{\sigma}*I_{x^2},S_{y^2}=G_{\sigma}*I_{y^2},S_{xy}=G_{\sigma}*I_{xy}\)
  4. 對於每個點\((x,y)\),定義矩陣\(\begin{bmatrix}S_{x^2}&S_{xy}\\S_{xy}&S_{y^2}\end{bmatrix}\)
  5. 對於每個點,計算響應值\(R=Det(H)-k(Trace(H))^2\)
  6. 對\(R\)設定閾值,並且計算非極大值抑制(nonmax suppression, NMS),這個的意思應該就是比如5*5的鄰域內有好幾個點通過了閾值的篩選,那麼選擇最大的那個,抑制其他的點。

一些特性

  • Harris角點響應具有旋轉不變性,因為旋轉不會改變特徵值的大小。
  • Harris角點響應對強度變化具有一定的不變性,縮放或者平移。因為經過縮放或者平移,最大值還是最大值,但是閾值可能要改改。
  • Harris角點響應不對尺度有不變性,改變尺度可能會改變檢測的結果。可能在某一尺度下檢測出為角點,而另一尺度檢測出為邊緣。

參考

  • [1]CSE486 PSU http://www.cse.psu.edu/~rtc12/CSE486/
  • [2]16-385 CMU 5http://www.cs.cmu.edu/~16385/

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

【其他文章推薦】

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

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

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

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

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

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

解Bug之路-記一次存儲故障的排查過程

解Bug之路-記一次存儲故障的排查過程

高可用真是一絲細節都不得馬虎。平時跑的好好的系統,在相應硬件出現故障時就會引發出潛在的Bug。偏偏這些故障在應用層的表現稀奇古怪,很難讓人聯想到是硬件出了問題,特別是偶發性出現的問題更難排查。今天,筆者就給大家帶來一個存儲偶發性故障的排查過程。

Bug現場

我們的積分應用由於量非常大,所以需要進行分庫分表,所以接入了我們的中間件。一直穩定運行,但應用最近確經常偶發連接建立不上的報錯。報錯如下:

GetConnectionTimeOutException

而筆者中間件這邊收到的確是:

NIOReactor - register err java.nio.channels.CloasedChannelException 

這樣的告警。整個Bug現場如下圖所示:

偶發性錯誤

之前出過類似register err這樣的零星報警,最後原因是安全掃描,並沒有對業務造成任何影響。而這一次,類似的報錯造成了業務的大量連接超時。由於封網,線上中間件和應用已經穩定在線上跑了一個多月,代碼層面沒有任何改動!突然出現的這個錯誤感覺是環境出現了某些問題。而且由於線上的應用和中間件都是集群,出問題時候都不是孤立的機器報錯,沒道理所有機器都正好有問題。如下圖所示:

開始排查是否網絡問題

遇到這種連接超時,筆者最自然的想法當然是網絡出了問題。於是找網工進行排查,
在監控裏面發現網絡一直很穩定。而且如果是網絡出現問題,同一網段的應用應該也都會報錯
才對。事實上只有對應的應用和中間件才報錯,其它的應用依舊穩穩噹噹。

又發生了兩次

就在筆者覺得這個偶發性問題可能不會再出現的時候,又開始抖了。而且是一個下午連抖了兩次。臉被打的啪啪的,算了算了,先重啟吧。重啟中間件后,以為能消停一會,沒想到半個小時之內又報了。看來今天不幹掉這個Bug是下不了班了!

開始排查日誌

事實上,筆者一開始就發現中間件有調用後端數據庫慢SQL的現象,由於比較偶發,所以將這個現象發給DBA之後就沒有繼續跟進,DBA也反饋SQL執行沒有任何異常。筆者開始認真分析日誌之後,發現一旦有 中間件的register err 必定會出現中間件調用後端數據庫的sql read timeout的報錯。
但這兩個報錯完全不是在一個線程裏面的,一個是處理前端的Reactor線程,一個是處理後端SQL的Worker線程,如下圖所示:

這兩個線程是互相獨立的,代碼中並沒有發現任何機制能讓這兩個線程互相影響。難道真是這些機器本身網絡出了問題?前端APP失敗,後端調用DB超時,怎麼看都像網絡的問題!

進一步進行排查

既然有DB(數據庫)超時,筆者就先看看調用哪個DB超時吧,畢竟後面有一堆DB。筆者突然發現,和之前的慢SQL一樣,都是調用第二個數據庫超時,而DBA那邊卻說SQL執行沒有任何異常,

筆者感覺明顯SQL執行有問題,只不過DBA是採樣而且將採樣耗時平均的,偶爾的幾筆耗時並不會在整體SQL的耗時裏面有所體現。

只能靠日誌分析了

既然找不到什麼頭緒,那麼只能從日誌入手,好好分析推理了。REACTOR線程和Worker線程同時報錯,但兩者並無特殊的關聯,說明可能是同一個原因引起的兩種不同現象。筆者在線上報錯日誌裏面進行細細搜索,發現在大量的

NIOReactor-1-RW register err java.nio.channels.CloasedChannelException

日誌中會摻雜着這個報錯:

NIOReactor-1-RW Socket Read timed out
	at XXXXXX . doCommit
	at XXXXXX Socket read timedout

這一看就發現了端倪,Reactor作為一個IO線程,怎麼會有數據庫調用呢?於是翻了翻源碼,原來,我們的中間件在處理commit/rollback這樣的操作時候還是在Reactor線程進行的!很明顯Reactor線程卡主是由於commit慢了!筆者立馬反應過來,而這個commit慢也正是導致了regsiter err以及客戶端無法創建連接的元兇。如下面所示:

由於app1的commit特別慢而卡住了reactor1線程,從而落在reactor1線程上的握手操作都會超時!如下圖所示:

為什麼之前的模擬宕機測試發現不了這一點

因為模擬宕機的時候,在事務開始的第一條SQL就會報錯,而執行SQL都是在Worker線程裏面,
所以並不會觸發reactor線程中commit超時這種現象,所以測試的時候就遺漏了這一點。

為什麼commit會變慢?

系統一直跑的好好的,為什麼突然commit就變慢了呢,而且筆者發現,這個commit變慢所關聯的DB正好也是出現慢SQL的那個DB。於是筆者立馬就去找了DBA,由於我們應用層和數據庫層都沒有commit時間的監控(因為一般都很快,很少出現慢的現象)。DBA在數據庫打的日誌裏面進行了統計,發現確實變慢了,而且變慢的時間和我們應用報錯的時間相符合!
順藤摸瓜,我們又聯繫了SA,發現其中和存儲相關的HBA卡有報錯!如下圖所示:

報錯時間都是一致的!

緊急修復方案

由於是HBA卡報錯了,屬於硬件故障,而硬件故障並不是很快就能進行修復的。所以DBA做了一次緊急的主從切換,進而避免這一問題。

一身冷汗

之前就有慢sql慢慢變多,而後突然數據庫存儲hba卡宕機導致業務不可用的情況。
而這一次到最後主從切換前為止,報錯越來越頻繁,感覺再過一段時間,HBA卡過段時間就完全不可用,重蹈之前的覆轍了!

中間件修復

我們在中間件層面將commit和rollback操作挪到Worker裏面。這樣,commit如果卡住就不再會引起創建連接失敗這種應用報錯了。

總結

由於軟件層面其實是比較信任硬件的,所以在硬件出問題時,就會產生很多詭異的現象,而且和硬件最終的原因在表面上完全產生不了關聯。只有通過抽絲剝繭,慢慢的去探尋現象的本質才會解決最終的問題。要做到高可用真的是要小心評估各種細節,才能讓系統更加健壯!

公眾號

關注筆者公眾號,獲取更多乾貨文章:

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

用費曼技巧學編程,香不香?

引子

 有一本講諾貝爾獎獲得者,物理學家費曼的書,叫做《發現的樂趣》,書中寫到一個費曼小時候的故事:

 “我們家有《大不列顛百科全書》,我還是小孩子的時候,父親就常常讓我坐在他腿上,給我讀些《大不列顛百科全書》。比如說,我們讀關於恐龍的部分,書上可能講雷龍或其他什麼龍,書上會說:“這傢伙有 25 英尺高,腦袋寬 6 英尺。” 

這時父親就停下來,說:“我們來看看這句話什麼意思。這句話的意思是:假如它站在我們家的前院里,它是那麼高,高到足以把頭從窗戶伸進來。不過呢,它也可能遇到點麻煩,因為它的腦袋比窗戶稍微寬了些,要是它伸進頭來,會擠破窗戶。 

費曼說:凡是我們讀到的東西,我們都盡量把它轉化成某種現實,從這裏我學到一個本領——凡我所讀的內容,我總設法通過某種轉換,弄明白它究竟什麼意思,它到底在說什麼。

 

費曼技巧

 

費曼技巧,或者說費曼學習法是一種以教促學的方法,一共有四步(已經知道的可以無視,直接跳過): 

(1) 選擇新概念/新知識, 自己先去學習它。 

(2) 假裝當一個老師,去教授別人 

想象你面對一群小白,怎麼把這個概念講給他們聽,讓他們理解呢? 

把你講解的思路也寫到紙上,如果實在不想寫,可以說出來。 

非常重要!!!不要讓你的思路停留在大腦中,因為大腦中對於知識點之間的關聯會有些想當然的、錯誤的假設,說出來或者寫出來能找到這些“盲點”!!

 

(3) 如果你在教授的過程中遇到了麻煩,卡了殼,返回去學習。 

重新去看書,搜相關資料,問別人,倒逼自己把這個概念搞清楚, 然後回到第二步,繼續給小白講授。

 

(4) 簡化你的語言。 

目標是用你自己的語言,非專業的詞彙去解釋這個概念。盡量做到簡單直白,或者找到比喻來表達。 

非常簡單的過程,對吧? 

 

實戰演練

我們來用個例子來演練一下,有請碼農翻身頭號主人公張大胖出場。 

張大胖正在學習Java,這一天他遇到了一個新的概念:“動態代理”  (注意是學習這個概念,不是具體實現), 非常抽象,在日常編程中幾乎不會直接使用,理解起來有難度。

 

第一步,自學

 張大胖看了動態代理的介紹,書上列舉出一堆煩人的代碼來展示這個東西是怎麼使用的,比如有個接口(IHelloWorld)及其實現類(HelloWorld), 然後有個InvocationHandler的實現,最後用Proxy.newProxyInstance(….)創建一個新的類出來,這些都是什麼鬼?啰里啰唆的。

 

第二步,張大胖嘗試教一下小白(當然這裏的小白至少得懂點兒Java)

 

張大胖:動態代理嘛,很簡單,就是給定一個接口和實現類,再加上一個InvocationHandler , 動態代理這個技術可以在運行時創建一個新的代理類出來。 

小白:張老師, 新的代理類有什麼用? 

張大胖:舉個例子,有個叫IHelloWorld接口及其實現類HelloWorld,它有一個叫sayHello()的方法。可以在sayHello()之前和之後,額外加一些日誌的輸出。 

(在講解一個概念的時候,舉例和類比很重要,人類習慣於通過例子來學習,從具體走向抽象) 

小白:那我直接寫一個新的類,比如HelloWorldEx,把日誌輸出添加到其中不就行了,為什麼還要用Proxy.newProxyInstance(……)這麼麻煩的方法?

public class HelloWorldEx implements IHelloWorld{
    IHelloWorld hw;
    public HelloWorldEx(IHelloWorld hw){
        this.hw = hw;
    }    
    public void sayHello(){        
        Logger.startLog();
        hw.sayHello();
        Logger.endLog();
    }
}

  

張大胖無法回答這個問題,卡殼了! 

第三步,回過頭去看書,學習。

書中也沒有解釋,唉! 

仔細想一想,手動寫一個類HelloWorldEx和用Proxy.newProxyInstance來創建,區別到底是什麼? 

實現的功能是相同的,但是HelloWorldEx需要事先寫好,編譯后不能改了,相當於寫死了!如果我想對Order類,Employee類,Department類,也想加點兒日誌,還得寫個OrderEx,EmployeeEx,DepartmentEx的類,太麻煩了! 

而Proxy.newProxyInstance這種方法,可以在程序運行的時候為任意類動態地創建增強的類。 

事先寫死的叫做靜態代理,Proxy.newProxyInstance這種方式叫做動態代理,更加靈活。 

張大胖覺得這麼解釋就通了。 

小白:為什麼要創建新的代理類,那個Proxy.newProxyInstance不能直接修改老的HelloWorld類嗎? 

張大胖再度卡殼,上網搜索,找到了答案,和Python,Ruby等方法不同,Java本質是一個靜態類型的語言,class一旦被裝入JVM,是不能修改,添加,刪除方法的,既然老的class不能修改,只能通過代理的方式來創建新的類了。 

小白:懂了,這個技術主要用在什麼地方啊? 難道只是加個日誌? 

張大胖第三次卡殼,只好再次搜索。 

原來動態代理使用得最多的是AOP,AOP中經常會以聲明的方式提出這樣的要求: 

某個包下所有add開頭的方法,在執行之前都要調用Logger.startLog()方法,在執行之後都要調用Logger.endLog()方法。 

或者對於所有以Service結尾的類,所有的方法執行之前都要調用tx.begin(),執行之後都要調用tx.commit(), 如果拋出異常的話調用tx.rollback()。

 

到此為止,張大胖可以這樣來給小白講述了: 

你不是用過Spring AOP嗎?AOP中經常有這樣的需求……  ,Spring想添加這些日誌和事務的功能,但是卻沒有辦法去修改用戶的類,它是框架啊,一是不知道用戶類的源碼,二是Java不允許再修改裝載入JVM的class。 

沒辦法,Spring只好在運行時找到用戶的類,然後操作字節碼動態創建一個新類,新類會對原有的類進行增強,添加日誌,事務這些功能,注意啊,這些都是在內存中動態創建的。 

這個技術就是Java的動態代理,不過它有個前提要求,就是用戶的類需要實現接口才行。我用一個簡單的例子給你說下,你就明白細節了……

 

第四步,簡化,比喻

上面的講解從文字上來說還是非常啰嗦的,用了很大篇幅來講解“為什麼”,因為理解了why ,剩下的就是細節了。  

如果你徹底理解了以後,動態代理的技術細節會在大腦中會建立這麼一幅圖景:

 

$HelloWorld100就是那個代理類,它和HelloWorld都實現了IHelloWorld這個接口。 

如果一定要用個比喻來說,它們倆就是“兄弟關係”,CgLib提供了另外一種對現有類增強的辦法,動態生成的類繼承了現有的類,兩者是“父子關係”。

  

小結

 怎麼樣?用這種(假裝)教授別人,層層遞進、自我逼問的方法是不是很有效果?收益很大?  

用這種辦法,實際上就是逼着你把大腦中的盲點和一些想當然的假設給暴露出來,效果要比單純地閱讀和記憶好得多,趕緊在學習中試一下吧!

  

更多精彩文章,盡在碼農翻身

 

我是一個線程

TCP/IP之大明郵差

一個故事講完Https

CPU 阿甘

Javascript: 一個屌絲的逆襲

微服務把我坑了

如何降低程序員的工資?

程序員,你得選准跑路的時間!

兩年,我學會了所有的編程語言!

一直CRUD,一直996,我煩透了,我要轉型

字節碼萬歲!

上帝託夢給我說:一切皆文件

Node.js :我只需要一個店小二

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

【其他文章推薦】

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

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

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

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

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

Netty 中的內存分配淺析

Netty 出發點作為一款高性能的 RPC 框架必然涉及到頻繁的內存分配銷毀操作,如果是在堆上分配內存空間將會觸發頻繁的GC,JDK 在1.4之後提供的 NIO 也已經提供了直接直接分配堆外內存空間的能力,但是也僅僅是提供了基本的能力,創建、回收相關的功能和效率都很簡陋。基於此,在堆外內存使用方面,Netty 自己實現了一套創建、回收堆外內存池的相關功能。基於此我們一起來看一下 Netty 是如何實現內存分配的。

1. Netty 中的數據容器分類

談到數據保存肯定要說到內存分配,按照存儲空間來劃分,可以分為 堆內存 和 堆外內存;按照內存區域連貫性來劃分可以分為池化內存和非池化內存。這些劃分在 Netty 中的實現接口分別是:

按照底層存儲空間劃分:

  • 堆緩衝區:HeapBuffer;
  • 直接緩衝區:DirectBuffer。

按照是否池化劃分:

  • 池化:PooledBuffer;
  • 非池化:UnPooledBuffer。

默認使用 PoolDireBuf 類型的內存, 這些內存主要由 PoolArea 管理。另外 Netty 並不是直接對外暴露這些 API,提供了 Unsafe 類作為出口暴露數據分配的相關操作。

小知識:

什麼是池化?

一般申請內存是檢查當前內存哪裡有適合當前數據塊大小的空閑內存塊,如果有就將數據保存在當前內存塊中。

那麼池化想做的事情是:既然每次來數據都要去找內存地址來存,我就先申請一塊內存地址,這一塊就是我的專用空間,內存分配、回收我全權管理。

池化解決的問題:

內存碎片:

內碎片

內碎片就是申請的地址空間大於真正數據使用的內存空間。比如固定申請1M的空間作為某個線程的使用內存,但是該線程每次最多只佔用0.5M,那麼每次都有0.5M的碎片。如果該空間不被有效回收時間一長必然存在內存空洞。

外碎片

外碎片是指多個內存空間合併的時候發現不夠分配給待使用的空間大小。比如有一個 20byte,13byte 的連續內存空間可以被回收,現在有一個 48byte 的數據塊需要存儲,而這兩個加起來也只有 33byte 的空間,必然不會被使用到。

如何實現內存池?

  1. 鏈表維護空閑內存地址

    最簡單的就是弄一個鏈表來維護當前空閑的內存空間地址。如果有使用就從鏈表刪除,有釋放就加入鏈表對應位置。這種方式實現簡單,但是搜索和釋放內存維護的難度還是比較大,不太適合。

  2. 定長內存空間分配

    維護兩個列表,一個是未分配內存列表,一個是已分配內存列表。每個內存塊都是一樣大小,分配時如果不夠就將多個塊合併到一起。這種方式的缺點就是會浪費一定的內存空間,如果有特定的場景還是沒有問題。

  3. 多段定長池分配

    在上面的定長分配基礎上,由原來的固定一個長度分配空間變為按照不同對象大小(8,16,32,64,128,256,512,1k…64K),的方式分配多個固定大小的內存池。每次要申請內存的時候按照當前對象大小去對應的池中查找是否有剩餘空間。

    Linux 本身支持動態內存分配和釋放,對應的命令為:malloc/free。malloc 的全稱是 memory allocation,中文叫動態內存分配,用於申請一塊連續的指定大小的內存塊區域以void*類型返回分配的內存區域地址。

    malloc / free的實現過程:

    1. 空閑存儲空間以空閑鏈表的方式組織(地址遞增),每個塊包含一個長度、一個指向下一塊的指針以及一個指向自身存儲空間的指針。( 因為程序中的某些地方可能不通過 malloc 調用申請,因此 malloc 管理的空間不一定連續)
    2. 當有申請請求時,malloc 會掃描空閑鏈表,直到找到一個足夠大的塊為止(首次適應)(因此每次調用malloc 時並不是花費了完全相同的時間)
    3. 如果該塊恰好與請求的大小相符,則將其從鏈表中移走並返回給用戶。如果該塊太大,則將其分為兩部分,尾部的部分分給用戶,剩下的部分留在空閑鏈表中(更改頭部信息)。因此 malloc 分配的是一塊連續的內存。
    4. 釋放時首先搜索空閑鏈表,找到可以插入被釋放塊的合適位置。如果與被釋放塊相鄰的任一邊是一個空閑塊,則將這兩個塊合為一個更大的塊,以減少內存碎片。

2. Netty 中的內存分配

Netty 採用了 jemalloc 的思想,這是 FreeBSD 實現的一種併發 malloc 的算法。jemalloc 依賴多個 Arena(分配器) 來分配內存,運行中的應用都有固定數量的多個 Arena,默認的數量與處理器的個數有關。系統中有多個 Arena 的原因是由於各個線程進行內存分配時競爭不可避免,這可能會極大的影響內存分配的效率,為了緩解高併發時的線程競爭,Netty 允許使用者創建多個分配器(Arena)來分離鎖,提高內存分配效率。

線程首次分配/回收內存時,首先會為其分配一個固定的 Arena。線程選擇 Arena 時使用 round-robin 的方式,也就是順序輪流選取。

每個線程各種保存 Arena 和緩存池信息,這樣可以減少競爭並提高訪問效率。Arena 將內存分為很多 Chunk 進行管理,Chunk 內部保存 Page,以頁為單位申請。申請內存分配時,會將分配的規格分為幾類:TINY,SAMLL,NORMAL 和 HUGE,分別對應不同的範圍,處理過程也不相同。

tiny 代表了大小在 0-512B 的內存塊;

small 代表了大小在 512B-8K 的內存塊;

normal 代表了大小在 8K-16M 的內存塊;

huge 代表了大於 16M 的內存塊。

每個塊裏面又定義了更細粒度的單位來分配數據:

  • Chunk:一個 Chunk 的大小是 16M,Chunk 是 Netty 對操作系統進行內存申請的單位,後續所有的內存分配都是在 Chunk 裏面進行操作。
  • Page:Chunk 內部以 Page 為單位分配內存,一個 Page 大小為 8K。當我們需要 16K 的空間時,Netty 就會從一個 Chunk 中找到兩個 Page 進行分配。
  • Subpage 和 element:element 是比 Page 更小的單位,當我們申請小於 8K 的內存時,Netty 會以 element 為單位進行內存分配。element 沒有固定大小,具體由用戶的需求決定。Netty 通過 Subpage 管理 element,Subpage 是由 Page 轉變過來的。當我們需要 1K 的空間時,Netty 會把一個 Page 變成 Subpage,然後把 Subpage 分成 8 個 1K 的 element 進行分配。

Chunk 中的內存分配

線程分配內存主要從兩個地方分配: PoolThreadCache 和 Arena。其中 PoolThreadCache 線程獨享, Arena 為幾個線程共享。

初次申請內存的時候,Netty 會從一整塊內存(Chunk)中分出一部分來給用戶使用,這部分工作是由 Arena 來完成。而當用戶使用完畢釋放內存的時候,這些被分出來的內存會按不同規格大小放在 PoolThreadCache 中緩存起來。當下次要申請內存的時候,就會先從 PoolThreadCache 中找。

Chunk、Page、Subpage 和 element 都是 Arena 中的概念,Arena 的工作就是從一整塊內存中分出合適大小的內存塊。Arena 中最大的內存單位是 Chunk,這是 Netty 向操作系統申請內存的單位。而一塊 Chunk(16M) 申請下來之後,內部會被分成 2048 個 Page(8K),當用戶向 Netty 申請超過 8K 內存的時候,Netty 會以 Page 的形式分配內存。

Chunk 內部通過夥伴算法管理 Page,具體實現為一棵完全平衡二叉樹:

二叉樹中所有子節點管理的內存也屬於其父節點。當我們要申請大小為 16K 的內存時,我們會從根節點開始不斷尋找可用的節點,一直到第 10 層。那麼如何判斷一個節點是否可用呢?Netty 會在每個節點內部保存一個值,這個值代表這個節點之下的第幾層還存在未分配的節點。比如第 9 層的節點的值如果為 9,就代表這個節點本身到下面所有的子節點都未分配;如果第 9 層的節點的值為 10,代表它本身不可被分配,但第 10 層有子節點可以被分配;如果第 9 層的節點的值為 12,此時可分配節點的深度大於了總深度,代表這個節點及其下面的所有子節點都不可被分配。下圖描述了分配的過程:

對於小內存(小於4096)的分配還會將 Page 細化成更小的單位 Subpage。Subpage 按大小分有兩大類:

  1. Tiny:小於 512 的情況,最小空間為 16,對齊大小為 16,區間為[16,512),所以共有 32 種情況。
  2. Small:大於等於 512 的情況,總共有四種,512,1024,2048,4096。

PoolSubpage 中直接採用位圖管理空閑空間(因為不存在申請 k 個連續的空間),所以申請釋放非常簡單。

第一次申請小內存空間的時候,需要先申請一個空閑頁,然後將該頁轉成 PoolSubpage,再將該頁設為已被佔用,最後再把這個 PoolSubpage 存到 PoolSubpage 池中。這樣下次就不需要再去申請空閑頁了,直接去池中找就好了。Netty 中有 36 種 PoolSubpage,所以用 36 個 PoolSubpage 鏈表表示 PoolSubpage 池。

因為單個 PoolChunk 只有 16M,這遠遠不夠用,所以會很很多很多 PoolChunk,這些 PoolChunk 組成一個鏈表,然後用 PoolChunkList 持有這個鏈表。

我們先從內存分配器 PoolArena 來分析 Netty 中的內存是如何分配的,Area 的工作就是從一整塊內存中協調如何分配合適大小的內存給當前數據使用。PoolArena 是 Netty 的內存池實現抽象類,其內部子類為 HeapArena 和 DirectArena,HeapArena 對應堆內存(heap buffer),DirectArena 對應堆外直接內存(direct buffer),兩者除了操作的內存(byte[]和ByteBuffer)不同外其餘完全一致。

從結構上來看,PoolArena 中主要包含三部分子內存池:

tinySubpagePools;

smallSubpagePools;

一系列的 PoolChunkList。

tinySubpagePools 和 smallSubpagePools 都是 PoolSubpage 的數組,數組長度分別為 32 和 4。

PoolChunkList 則主要是一個容器,其內部可以保存一系列的 PoolChunk 對象,並且,Netty 會根據內存使用率的不同,將 PoolChunkList 分為不同等級的容器。

abstract class PoolArena<T> implements PoolArenaMetric {

   enum SizeClass {
        Tiny,
        Small,
        Normal
    }
  // 該參數指定了tinySubpagePools數組的長度,由於tinySubpagePools每一個元素的內存塊差值為16,
	// 因而數組長度是512/16,也即這裏的512 >>> 4
  static final int numTinySubpagePools = 512 >>> 4;
	//表示該PoolArena的allocator
  final PooledByteBufAllocator parent;
  //表示PoolChunk中由Page節點構成的二叉樹的最大高度,默認11
  private final int maxOrder;
  //page的大小,默認8K
  final int pageSize;
  // 指定了恭弘=叶 恭弘節點大小8KB是2的多少次冪,默認為13,該字段的主要作用是,在計算目標內存屬於二叉樹的
	// 第幾層的時候,可以藉助於其內存大小相對於pageShifts的差值,從而快速計算其所在層數
  final int pageShifts;
  //默認16MB
  final int chunkSize;
  // 由於PoolSubpage的大小為8KB=8196,因而該字段的值為
	// -8192=>=> 1111 1111 1111 1111 1110 0000 0000 0000
	// 這樣在判斷目標內存是否小於8KB時,只需要將目標內存與該数字進行與操作,只要操作結果等於0,
	// 就說明目標內存是小於8KB的,這樣就可以判斷其是應該首先在tinySubpagePools或smallSubpagePools
	// 中進行內存申請
  final int subpageOverflowMask;
  // 該參數指定了smallSubpagePools數組的長度,默認為4
  final int numSmallSubpagePools;
  //tinySubpagePools用來分配小於512 byte的Page
  private final PoolSubpage<T>[] tinySubpagePools;
  //smallSubpagePools用來分配大於等於512 byte且小於pageSize內存的Page
  private final PoolSubpage<T>[] smallSubpagePools;
  //用來存儲用來分配給大於等於pageSize大小內存的PoolChunk
  //存儲內存利用率50-100%的chunk
  private final PoolChunkList<T> q050;
  //存儲內存利用率25-75%的chunk
  private final PoolChunkList<T> q025;
  //存儲內存利用率1-50%的chunk
  private final PoolChunkList<T> q000;
  //存儲內存利用率0-25%的chunk
  private final PoolChunkList<T> qInit;
  //存儲內存利用率75-100%的chunk
  private final PoolChunkList<T> q075;
  //存儲內存利用率100%的chunk
  private final PoolChunkList<T> q100;
	//堆內存(heap buffer)
  static final class HeapArena extends PoolArena<byte[]> {
  
  }
   //堆外直接內存(direct buffer)
  static final class DirectArena extends PoolArena<ByteBuffer> {
    
  }
  
  
}

如上所示,PoolArena 是由多個 PoolChunk 組成的大塊內存區域,而每個 PoolChun k則由多個 Page 組成。當需要分配的內存小於 Page 的時候,為了節約內存採用 PoolSubpage 實現小於 Page 大小內存的分配。在PoolArena 中為了保證 PoolChunk 空間的最大利用化,按照 PoolArena 中各 個PoolChunk 已使用的空間大小將其劃分為 6 類:

  1. qInit:存儲內存利用率 0-25% 的 chunk;
  2. q000:存儲內存利用率 1-50% 的 chunk;
  3. q025:存儲內存利用率 25-75% 的 chunk;
  4. q050:存儲內存利用率 50-100% 的 chunk;
  5. q075:存儲內存利用率 75-100%的 chunk;
  6. q100:存儲內存利用率 100%的 chunk。

PoolArena 維護了一個 PoolChunkList 組成的雙向鏈表,每個 PoolChunkList 內部維護了一個 PoolChunk 雙向鏈表。分配內存時,PoolArena 通過在 PoolChunkList 找到一個合適的 PoolChunk,然後從 PoolChunk 中分配一塊內存。

下面來看 PoolArena 是如何分配內存的:

private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
  // 將需要申請的容量格式為 2^N
  final int normCapacity = normalizeCapacity(reqCapacity);
  // 判斷目標容量是否小於8KB,小於8KB則使用tiny或small的方式申請內存
  if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
    int tableIdx;
    PoolSubpage<T>[] table;
    boolean tiny = isTiny(normCapacity);
    // 判斷目標容量是否小於512字節,小於512字節的為tiny類型的
    if (tiny) { // < 512
      // 將分配區域轉移到 tinySubpagePools 中
      if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
        // was able to allocate out of the cache so move on
        return;
      }
      // 如果無法從當前線程緩存中申請到內存,則嘗試從tinySubpagePools中申請,這裏tinyIdx()方法
      // 就是計算目標內存是在tinySubpagePools數組中的第幾號元素中的
      tableIdx = tinyIdx(normCapacity);
      table = tinySubpagePools;
    } else {
      // 如果目標內存在512byte~8KB之間,則嘗試從smallSubpagePools中申請內存。這裏首先從
      // 當前線程的緩存中申請small級別的內存,如果申請到了,則直接返回
      if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
        // was able to allocate out of the cache so move on
        return;
      }
      tableIdx = smallIdx(normCapacity);
      table = smallSubpagePools;
    }
		// 獲取目標元素的頭結點
    final PoolSubpage<T> head = table[tableIdx];

    // 這裏需要注意的是,由於對head進行了加鎖,而在同步代碼塊中判斷了s != head,
    // 也就是說PoolSubpage鏈表中是存在未使用的PoolSubpage的,因為如果該節點已經用完了,
    // 其是會被移除當前鏈表的。也就是說只要s != head,那麼這裏的allocate()方法
    // 就一定能夠申請到所需要的內存塊
    synchronized (head) {
      // s != head就證明當前PoolSubpage鏈表中存在可用的PoolSubpage,並且一定能夠申請到內存,
      // 因為已經耗盡的PoolSubpage是會從鏈表中移除的
      final PoolSubpage<T> s = head.next;
      // 如果此時 subpage 已經被分配過內存了執行下文,如果只是初始化過,則跳過該分支
      if (s != head) {
        // 從PoolSubpage中申請內存
        assert s.doNotDestroy && s.elemSize == normCapacity;
        // 通過申請的內存對ByteBuf進行初始化
        long handle = s.allocate();
        assert handle >= 0;
        // 初始化 PoolByteBuf 說明其位置被分配到該區域,但此時尚未分配內存
        s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
				// 對tiny類型的申請數進行更新
        if (tiny) {
          allocationsTiny.increment();
        } else {
          allocationsSmall.increment();
        }
        return;
      }
    }
    // 走到這裏,說明目標PoolSubpage鏈表中無法申請到目標內存塊,因而就嘗試從PoolChunk中申請
    allocateNormal(buf, reqCapacity, normCapacity);
    return;
  }
   // 走到這裏說明目標內存是大於8KB的,那麼就判斷目標內存是否大於16M,如果大於16M,
  // 則不使用內存池對其進行管理,如果小於16M,則到PoolChunkList中進行內存申請
  if (normCapacity <= chunkSize) {
    // 小於16M,首先到當前線程的緩存中申請,如果申請到了則直接返回,如果沒有申請到,
    // 則到PoolChunkList中進行申請
    if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
      // was able to allocate out of the cache so move on
      return;
    }
    allocateNormal(buf, reqCapacity, normCapacity);
  } else {
    // 對於大於16M的內存,Netty不會對其進行維護,而是直接申請,然後返回給用戶使用
    allocateHuge(buf, reqCapacity);
  }
}

所有內存分配的 size 都會經過 normalizeCapacity() 進行處理,申請的容量總是會被格式為 2^N。主要規則如下:

  1. 如果目標容量小於 16 字節,則返回 16;
  2. 如果目標容量大於 16 字節,小於 512 字節,則以 16 字節為單位,返回大於目標字節數的第一個 16 字節的倍數。比如申請的 100 字節,那麼大於 100 的 16 整數倍最低為: 16 * 7 = 112,因而返回 112;
  3. 如果目標容量大於 512 字節,則返回大於目標容量的第一個 2 的指數冪。比如申請的 1000 字節,那麼返回的將是:2^10 = 1024。

PoolArena 提供了兩種方式進行內存分配:

  1. PoolSubpage 用於分配小於 8k 的內存

    • tinySubpagePools:用於分配小於 512 字節的內存,默認長度為 32,因為內存分配最小為 16,每次增加16,直到512,區間[16,512)一共有 32 個不同值;
    • smallSubpagePools:用於分配大於等於 512 字節的內存,默認長度為 4;
  • tinySubpagePools 和 smallSubpagePools 中的元素默認都是 subpage。
  1. poolChunkList 用於分配大於 8k 的內存

    上面已經解釋了 q 開頭的幾個變量用於保存大於 8k 的數據。

默認先嘗試從 poolThreadCache 中分配內存,PoolThreadCache 利用 ThreadLocal 的特性,消除了多線程競爭,提高內存分配效率;

首次分配時,poolThreadCache 中並沒有可用內存進行分配,當上一次分配的內存使用完並釋放時,會將其加入到 poolThreadCache 中,提供該線程下次申請時使用。

如果是分配小內存,則嘗試從 tinySubpagePools 或 smallSubpagePools 中分配內存,如果沒有合適 subpage,則採用方法 allocateNormal 分配內存。

如果分配一個 page 以上的內存,直接採用方法 allocateNormal() 分配內存,allocateNormal()則會將申請動作交由 PoolChunkList 進行。

private synchronized void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
  //如果在對應的PoolChunkList能申請到內存,則返回
  if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
      q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
      q075.allocate(buf, reqCapacity, normCapacity)) {
    ++allocationsNormal;
    return;
  }

  // Add a new chunk.
  PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
  long handle = c.allocate(normCapacity);
  ++allocationsNormal;
  assert handle > 0;
  c.initBuf(buf, handle, reqCapacity);
  qInit.add(c);
}

首先將申請動作按照 q050->q025->q000->qInit->q075 的順序依次交由各個 PoolChunkList 進行處理,如果在對應的 PoolChunkList 中申請到了內存,則直接返回。

如果申請不到,那麼直接創建一個新的 PoolChunk,然後在該 PoolChunk 中申請目標內存,最後將該 PoolChunk 添加到 qInit 中。

上面說過 Chunk 是 Netty 向操作系統申請內存塊的最大單位,每個 Chunk 是16M,PoolChunk 內部通過 memoryMap 數組維護了一顆完全平衡二叉樹作為管理底層內存分佈及回收的標記位,所有的子節點管理的內存也屬於其父節點。

關於 PoolChunk 內部如何維護完全平衡二叉樹就不在這裏展開,大家有興趣可以自行看源碼。

對於內存的釋放,PoolArena 主要是分為兩種情況,即池化和非池化,如果是非池化,則會直接銷毀目標內存塊,如果是池化的,則會將其添加到當前線程的緩存中。如下是 free()方法的源碼:

public void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity,
     PoolThreadCache cache) {
  // 如果是非池化的,則直接銷毀目標內存塊,並且更新相關的數據
  if (chunk.unpooled) {
    int size = chunk.chunkSize();
    destroyChunk(chunk);
    activeBytesHuge.add(-size);
    deallocationsHuge.increment();
  } else {
    // 如果是池化的,首先判斷其是哪種類型的,即tiny,small或者normal,
    // 然後將其交由當前線程的緩存進行處理,如果添加成功,則直接返回
    SizeClass sizeClass = sizeClass(normCapacity);
    if (cache != null && cache.add(this, chunk, nioBuffer, handle,
          normCapacity, sizeClass)) {
      return;
    }

    // 如果當前線程的緩存已滿,則將目標內存塊返還給公共內存塊進行處理
    freeChunk(chunk, handle, sizeClass, nioBuffer);
  }
}

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

分享我在前後端分離項目中Gitlab-CI的經驗

長話短說,今天分享我為前後端分離項目搭建Gitlab CI/CD流程的一些額外經驗。

Before

Gitlab-ci是Gitlab提供的CI/CD特性,結合Gitlab簡單友好的配置界面,能愉悅的在Gitlab界面查看管道執行流程,並自然流暢的推動敏捷開發流程。
Gitlab-CI/CD的核心是搭建Gitlab Runner、編寫.gitlab-ci.yaml文件。
詳細示例請參考:Gitlab CI/CD+ASP.NETCore.

本次前後端兩個項目使用同一個Gitlab Runner(shell模式),前端項目的gitlab-ci.yaml構建Job如圖:

Round 1

單個Gitlab Runner可為多個項目提供構建服務,

gitlab-Runner register命令只能接受一個註冊token,當時為支持多個項目,花了不少冤枉心思倒騰Gitlab Runner.

你可以為註冊的項目解鎖Runner,這樣Girlab Runner就可以為其他項目提供構建:

Round 2

使用Runner緩存加快前端構建過程
大家都知道npm_module被前端開發者詬病為毒瘤, 而Gitlab runner執行每次構建job之前都會清場,pull/fetch指定的代碼再執行job, 這就導致每次build job會耗時很久(要拉取毒瘤)。

#!/bin/bash

cd   packages/event-analysis
yarn config set registry http://registry.npm.gridsum.com &&  yarn --prefer-offline --frozen-lockfile
npm run build

以上是build任務的腳本frontend.sh,總耗時3m33s,其中yarn命令拉取npm_modules耗時172.52s

gitlab runner支持緩存
在.gitlab-ci.yaml 文件中定義cache指令:
cache被用來在job之間緩存文件,更強大的是可以定義文件依賴緩存:

build:
  stage: build
  cache:
    key:
      files:
        - packages/event-analysis/package.json
    paths:
      - node_modules
  script: 
    - ./frontend.sh
  tags:
    - my-tag

緩存key是yarn命令要用到的package.json,緩存內容是npm_modules;
只要這個package.json文件未變更,後續任務就會使用緩存的npm_modules,而不用重建npm_modules依賴。

使用runner緩存優化后build任務總耗時1m18s,其中yarn命令耗時22.83s:

以上針對Gitlab-CI的使用經驗點到為止,足夠應對我當前項目,更多請關注:

Reference

  1. https://docs.gitlab.com/ee/ci/runners/#prevent-a-specific-runner-from-being-enabled-for-other-projects
  2. https://docs.gitlab.com/ee/ci/caching/

Devops的圈子很大,上面的Gitlab-ci也只是點到為止,應付我當前的前後端分離項目.. 歡迎大家來捶我。

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

博客園自用主題美化 – Light

事件背景

 

之前做過幾個主題,但是都有些看膩了,然後就想着啥時候重構一下,於是就有了現在的新樣式:

https://github.com/KU4NG/CNBLOG-Theme-Light

當然,如果你對之前的感興趣,也可以查看之前的分享:

https://github.com/KU4NG/CNBlog-Theme

裡面包含幾個主題,但是我是有些懶得維護了,太忙了,感興趣的自己去改改 CSS 就行了!

 

 

新主題配置方法

 

1. 基礎配置:

前往博客園設置頁面,就行配置,需要注意的是:

a. 博客標題就是最終你博客的 logo,所以選你喜歡的。

b. 我博客標題中的來自 windows 10 系統輸入法旁邊的表情。

c. 基礎主題選:SimpleMemory,這一步很重要,因為我得樣式都是根據該主題的 HTML 結構寫的。

 

2. 配置樣式:

將本文中提供的 CSS 代碼或者 github 上面 css 目錄下 style.css 文件中的內容粘貼到此處。

*{margin:0;padding:0;font-weight:normal;letter-spacing:1.5px;font-family:PingFangSC-Regular,HelveticaNeue-Light,'Helvetica Neue Light','Microsoft YaHei',sans-serif,Simsun}div{border-radius:0!important}a{text-decoration:none!important}a:link{text-decoration:none!important}a:visited{text-decoration:none!important}a:hover{text-decoration:none!important}a:active{text-decoration:none!important}body{background-color:#eee}#header{min-width:1200px;height:50px;width:100%;display:block;background-color:white;box-shadow:0 1px 2px 0 rgba(0,0,0,.05)}#header *{color:#333!important}#header h1{float:left;width:200px;padding-left:20px}#header h1 a{height:46px;line-height:46px;font-size:18px!important;font-weight:bolder!important;letter-spacing:3px!important}#header #lnkBlogLogo{display:none}#header h2{display:none}#header li{list-style:none;float:right;height:50px;line-height:50px;position:relative}#header li:hover{cursor:pointer}#header li a{font-size:12px;letter-spacing:2px;padding:0 20px}#header li a::before,#header li a::after{position:absolute;top:0;right:0;bottom:0;left:0;content:'';opacity:0;pointer-events:none;-webkit-transition:opacity .4s,-webkit-transform .4s;transition:opacity .4s,transform .4s}#header li a::before{border-top:2px solid #333;-webkit-transform:scale(0,1);transform:scale(0,1)}#header li:hover a::before,#header li:hover a::after{opacity:1;-webkit-transform:scale(1);transform:scale(1)}#header .blogStats{display:none}#header #navigator{margin-right:200px}#sidebar_search{display:block;width:200px;position:absolute;top:0;right:0;height:48px;line-height:48px}#sidebar_search h3{display:none}#sidebar_search .div_my_zzk{margin-top:0;margin-bottom:0}#sidebar_search .input_my_zzk{display:inline-block;vertical-align:middle;padding:0 10px;border:0;cursor:text;font-size:12px}#sidebar_search .btn_my_zzk{background-color:#009688;color:#fff!important;white-space:nowrap;text-align:center;cursor:pointer;padding-left:10px;padding-right:10px}#sidebar_search input:focus{outline:0}#sidebar_search input{outline:0;border:0}#main{width:1200px;margin:10px auto}#main #mainContent{width:960px;background-color:white;padding:20px;float:left}#main #mainContent .day{position:relative;border:1px dashed lightgrey;padding:15px;margin:0 0 15px 0}#main #mainContent .day .dayTitle{position:absolute;top:-1px;right:-1px;background-color:#eee;width:110px;text-align:right;padding:0 10px 3px 10px}#main #mainContent .day .dayTitle a{font-size:12px;color:#333;opacity:.6}#main #mainContent .day .postTitle a{position:relative;left:-18px;border-left:4px solid #dc3545;padding-left:20px;font-size:18px;font-weight:bolder!important;color:#333}#main .postTitle span{font-weight:bolder!important}#main #mainContent .day .postTitle a:hover{color:#036}#main #mainContent .day .postCon{padding:10px}#main #mainContent .day .postCon .c_b_p_desc{font-size:12px;letter-spacing:2px;opacity:.6;line-height:2;width:900px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}#main #mainContent .day .postDesc{font-size:12px;opacity:.6;text-align:right}#main #mainContent .day .postDesc a{font-size:12px;color:#dc3545}#main #mainContent .day .postSeparator{height:15px;border-top:1px dashed lightgrey;border-bottom:1px dashed lightgrey;border-left:2px solid white;border-right:2px solid white;margin:15px -17px}#main #mainContent #nav_next_page a{font-size:12px;color:#333}#main #mainContent .topicListFooter{margin-right:0}#main #mainContent .pager{font-size:12px;color:#dc3545;text-align:right}#main #mainContent .pager a{font-size:12px;color:gray;border:1px solid lightgray}#main #mainContent #homepage_top_pager{display:none}#sideBar{width:200px;float:right}#sideBar #sideBarMain{width:190px;float:right}#sideBar #sideBarMain *{color:#333;font-size:12px;letter-spacing:2px}#sideBar #sideBarMain #sidebar_news{background-color:white}#sideBar #sideBarMain #sidebar_news h3{background-color:#dc3545;color:white;border-bottom:1px solid #eee;padding:5px 15px;font-size:14px}#sideBar #sideBarMain #sidebar_news #blog-news{padding:15px 15px 0 15px}#sideBar #sideBarMain #sidebar_news #blog-news *{line-height:25px}#sideBar #sideBarMain #sidebar_news #blog-news #profile_block{margin-top:0}#sideBar #sideBarMain #sidebar_categories{background-color:white}#sideBar #sideBarMain #sidebar_categories h3{padding:20px 15px 5px 15px}#sideBar #sideBarMain #sidebar_categories ul{list-style:none;counter-reset:headings;padding:0 15px 15px 15px}#sideBar #sideBarMain #sidebar_categories ul li{line-height:30px;border-bottom:1px dashed #eee}#sideBar #sideBarMain #sidebar_categories ul li:before{counter-increment:headings;content:counter(headings,decimal) ".";font-family:"Bree Serif",serif}#sideBar #sideBarMain #sidebar_categories ul li a{letter-spacing:1px}#sideBar #sideBarMain #sidebar_categories ul li a:hover{color:#dc3545}#footer{font-size:12px;text-align:center;line-height:25px;margin-top:20px;margin-bottom:20px;color:#036}
#footer br{display:none}#main #mainContent .entrylist h1{font-size:18px;font-weight:bolder;text-align:center;margin-top:15px;margin-bottom:30px}#main #mainContent .entrylistItem{position:relative;border:1px dashed lightgrey;padding:15px;margin:0 0 15px 0}#main #mainContent .entrylistItem .entrylistPosttitle a{position:relative;left:-18px;border-left:4px solid #dc3545;padding-left:20px;font-size:18px;font-weight:bolder;color:#333}#main #mainContent .entrylistItem .entrylistPosttitle a:hover{color:#036}#main .entrylistPosttitle span{font-weight:bolder}#main #mainContent .entrylistItem .entrylistPostSummary{padding:10px}#main #mainContent .entrylistItem .entrylistPostSummary .c_b_p_desc{font-size:12px;letter-spacing:2px;opacity:.6;line-height:2;width:900px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}#main #mainContent .entrylistItem .entrylistItemPostDesc{font-size:12px;opacity:.6;text-align:right}#main #mainContent .entrylistItem .entrylistItemPostDesc a{font-size:12px;color:#dc3545}#main #post_detail{padding:30px;position:relative}#main #post_detail .postTitle{padding:0 0 50px 0;text-align:center;border-bottom:1px dotted #ccc}#main #post_detail .postTitle a{font-size:20px;color:#333;font-weight:bolder}#main #post_detail .postDesc{position:absolute;width:calc(100% - 60px);top:85px;text-align:center}#main #post_detail .postDesc{font-size:12px;line-height:25px;color:#333;opacity:.7}#main #post_detail .postDesc *{font-size:12px;color:#333;opacity:.7}#main #post_detail .postBody #cnblogs_post_body{margin-top:30px;padding:15px}#main #post_detail .postBody #cnblogs_post_body p,#main #post_detail .postBody #cnblogs_post_body span{letter-spacing:1.5px;line-height:30px;font-size:14px;font-family:SimHei;margin:0;opacity:.9}#main #post_detail .postBody #cnblogs_post_body img{margin:15px 0;max-width:880px}#main #post_detail .postBody #cnblogs_post_body table{width:100%;margin:15px 0}#main #post_detail .postBody #cnblogs_post_body table *{font-size:12px}#main #post_detail .postBody #cnblogs_post_body table th{padding:6px 10px!important;text-align:left;background-color:#1c2b36;color:white;border-color:#1c2b36}#main #post_detail .postBody #cnblogs_post_body table td{padding:6px 10px!important;text-align:left}#main #post_detail .postBody #cnblogs_post_body .cnblogs_code{padding:15px 20px;border:0}#main #post_detail .postBody #cnblogs_post_body .cnblogs_code_toolbar{display:none}#main #post_detail .postBody #cnblogs_post_body blockquote{border:0;background-color:#eee;border-left:5px solid #009688}#main #post_detail .postBody #cnblogs_post_body blockquote *{font-size:12px;color:#333}#main #post_detail .postBody #blog_post_info_block{padding:15px}#main #post_detail .postBody #blog_post_info_block *{font-size:12px;color:#333}#main #post_detail .postBody #blog_post_info_block #author_profile_info{display:none}#main #post_detail .postBody #blog_post_info_block #author_profile_detail{display:none}#main #post_detail .postBody #blog_post_info_block #blog_post_info #green_channel{float:left;border:0}#main #post_detail .postBody #blog_post_info_block #blog_post_info #div_digg{float:right}#main #comment_form{padding:45px!important}#main #comment_form *{font-size:12px}#main #comment_form .comment_textarea{width:100%}#main #blog-comments-placeholder{padding:0 45px!important}#main #blog-comments-placeholder *{font-size:12px;color:#333}#main #blog-comments-placeholder .feedbackItem{padding:15px 0;border-bottom:1px dashed #eee}#main #blog-comments-placeholder .feedbackListSubtitle{line-height:30px}#main #blog-comments-placeholder a{color:#009688}#main #blog-comments-placeholder div{line-height:30px}#main #blog-comments-placeholder .feedbackManage{float:right}#main #blog-comments-placeholder .feedbackListSubtitle .layer{color:#036;font-weight:bolder}#main #blog-comments-placeholder .feedbackListSubtitle .louzhu{color:white;background-color:#1c2b36;padding:0 10px;margin:0 -8px}#main #blog-comments-placeholder br{display:none;padding-left:20px}#main #commentbox_opt #btn_comment_submit{background-color:#009688;color:#fff!important;white-space:nowrap;text-align:center;cursor:pointer;padding-left:10px;padding-right:10px;border:0;width:auto}#main #commentbox_opt a{color:#dc3545}.my-title{font-size:18px!important;font-weight:bolder;font-family:simsun!important;border-left:5px solid #dc3545;padding-left:10px;line-height:20px!important;position:relative;left:-15px}#comment_nav{display:none}#ad_t2{display:none}.c_ad_block{display:none}#under_post_news{display:none}

注意,一定要選擇禁用默認的樣式,否則可能樣式衝突! 

 

3. 配置側邊欄:

代碼如下:

<div style="width: 100%;text-align: center;">
    <img style="border-radius: 50%;border: 1px solid #eee;" src="https://pic.cnblogs.com/face/979767/20180915094029.png" alt="">
</div>
<div>聯繫: Q-1214966109</div>

值得注意的是:

a. 頭像的連接可以在自己的博客中找到,如: 

b. 聯繫方式可以寫自己,如果你還需要其它項目,繼續添加 div 即可!

最終的效果就是我的效果:

 

4. 配置首頁按鈕和 GITHUB 鏈接:

代碼如下:

<script>
    // 菜單新加標籤
    var indexEle = '<li><a target="_blank" class="menu" href="https://www.cnblogs.com/Dy1an">首頁</a></li>';
    var githubEle = '<li><a target="_blank" class="menu" href="https://github.com/KU4NG">GITHUB</a></li><li><a target="_blank" class="menu" href="https://github.com/KU4NG">主題</a></li>';
    document.getElementById('navList').insertAdjacentHTML("beforeEnd", indexEle);
    document.getElementById('navList').insertAdjacentHTML("afterBegin", githubEle);
</script>

需要注意的是:

a. 首頁鏈接記得換成自己的,當然你要是直接用我的我也是不會介意的。

b. GITHUB 你可以選擇換成其它的自己的一些資源。

c. 主題鏈接建議不要去掉,這裏吸波粉,讓我看到到底有多少人是支持這個項目的!

最終效果如下:

 

5. 配置搜索框:

這裏沒有需要修改的,直接粘貼就行,代碼如下:

<script>
    window.onload =  function() {
        var ele = document.getElementById('q');
        console.log(ele);
        ele.setAttribute("placeholder","搜索相關博客...");
        ele.setAttribute("autocomplete","off");
    }
</script>

最終效果:

已知問題:

由於網絡原因可能導致沒有加載或者加載不完全,刷新頁面即可!

 

6. 配置菜單:

想要以我的博客效果显示,則需要配置菜單:

在選項配置中選擇配置!

 

 

特殊用法說明 

 

1. 段落標題:

文中出現段落標題需要瘦動修改 HTML:

增加樣式:

代碼:

class="my-title"

 

2. 表格:

表格同樣需要以 html 格式插入:

<table>
    <thead>
        <th>
            <td>標題</td>
            <td>標題</td>
            <td>標題</td>
        </th>
    </thead>
    <tbody>
        <tr>
            <td>數據行</td>
            <td>數據行</td>
            <td>數據行</td>
        </tr>
        <tr>
            <td>數據行</td>
            <td>數據行</td>
            <td>數據行</td>
        </tr>
    </tbody>
</table>

效果如下:

 

3. 圖片:

最後就是截圖,文中的截圖建議大小不超過 870px,865px最佳,否則圖片會壓縮,影響體驗!

 

 

意見和建議

 

最後就是幾點說明:

1. 如果遇到樣式問題,歡迎反饋,QQ,留言,GITHUB 都可以。

2. 如果覺得可以,可以去 GITHUB star 支持一下,也可以在本文中頂一下! 

3. 最後吹一波博客園!

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

docker registry 鏡像同步

docker registry 鏡像同步

Intro

之前我們的 docker 鏡像是保存在 Azure 的 Container Registry 里的,最近我們自己搭建了一個 docker registry,我們想把之前保存的 Azure 的 Container Registry 的 docker 鏡像同步到我們自己的 docker registry 里

實現思路

我們的做法比較簡單也比較LOW,但是基本可以滿足要求,

我們的做法是

  1. 首先獲取到源 Registry 里的所有鏡像列表
  2. 然後逐個獲取鏡像的 tags
  3. 然後依次遍歷將對應的鏡像拉到本地,然後 docker tag 一下,命名為新的 registry 鏡像名稱
  4. 然後 push docker 鏡像到新的 registry
  5. 刪除下載到本地的鏡像和推送到新的 registry 的鏡像

後來突然想起來阿里雲好像有一個鏡像同步工具,https://github.com/AliyunContainerService/image-syncer image-syncer 是一個docker鏡像同步工具,可用來進行多對多的鏡像倉庫同步,支持目前絕大多數主流的docker鏡像倉庫服務,看介紹還是很棒的,有需要 registry 之間同步鏡像的可以試試這個工具,看介紹這個工具不會拉取到本地磁盤,從源 registry 獲取鏡像數據之後直接就推送到新的 registry 里了,效率會高很多

Docker-Registry API

docker registry 有一套規範,可以查閱 https://docs.docker.com/registry/spec/api/ 了解更多

獲取所有鏡像

docker registry v2 新增了一個 _catalog 的 api 可以獲取所有的鏡像,v1 可以用 _search 來代替

語法如下:

GET /v2/_catalog

默認最多返回100條記錄,多餘 100 可以通過參數 n 指定返回數量,分頁的話可以指定另外一個參數 last指定完上一頁返回的最後一個鏡像,舉個栗子: http://example.com/v2/_catalog?n=20&last=b

獲取鏡像的 tag

獲取 docker 鏡像的 tag 列表可以使用 GET /v2/<repository-name>/tags/list 來獲取,也可以分頁,類似於上面獲取鏡像列表,可以通過 n 和 last 來實現分頁加載

操作示例

在本地部署了一個測試用的 docker registry 來做演示,我這裏用 httpie 來做測試

獲取鏡像列表:

調用 _catalog 接口來獲取鏡像列表

http :5000/v2/_catalog

獲取鏡像的 tag 列表

調用 tags/list 接口獲取鏡像的 tag

http :5000/v2/busybox/tags/list
http :5000/v2/redis/tags/list

PowerShell 腳本

一切不是自動化的運維都是耍流氓,很有可能以後會有類似的需求,不如寫個腳本自動化的跑吧

下面的腳本做了一些簡化,因為我們的 azure container registry 上的數量不多,只有五六十個鏡像,而且鏡像只有 latest 的 tag,沒有其他 tag ,所以把上面的步驟做了簡化,並沒有分頁獲取所有的鏡像,也沒有獲取所有的 tag,實際使用的話還請自行修改后使用

# variables
$srcRegUser = "xxx"
$srcRegPwd = "111111"
$srcRegHost = "xxx.azurecr.cn"
$destRegUser = "yyy"
$destRegPwd = "222"
$destRegHost = "registry.xxx.com"


# get repositories from source registry
# httpie
$response = (http -b -a "${srcRegUser}:${srcRegPwd}" "https://${srcRegHost}/v2/_catalog") | ConvertFrom-Json
# curl
#$response = (curl -u "${srcRegUser}:${srcRegPwd}" "https://${srcRegHost}/v2/_catalog") | ConvertFrom-Json
# repository
$repositories = $response.repositories

#
Write-Host $repositories

# login source registry
docker login $srcRegHost -u $srcRegUser -p $srcRegPwd
# login dest registry
docker login $destRegHost -u $destRegUser -p $destRegPwd

# sync
foreach($repo in $repositories)
{
    Write-Host "sync $repo begin"

    $srcTag = "${srcRegHost}/${repo}:latest"
    $destTag = "${destRegHost}/${repo}:latest"

    Write-Host "source image tag: $srcTag"
    Write-Host "dest image tag $destTag"

    Write-Host "docker pull $srcTag begin"

    docker pull $srcTag

    Write-Host "docker pull $srcTag completed"

    Write-Host "docker tag $srcTag $destTag ing"

    docker tag $srcTag $destTag

    Write-Host "docker push $destTag begin"

    docker push $destTag

    Write-Host "docker push $destTag completed"
    
    Write-Host "docker rmi $srcTag $destTag begin"

    docker rmi $srcTag $destTag

    Write-Host "docker rmi $srcTag $destTag end"

    Write-Host "sync $repo completed"
}

Write-Host "Completed..."

More

如果要同步的鏡像比較多,考慮使用阿里雲的鏡像同步工具去同步

Reference

  • https://stackoverflow.com/questions/31251356/how-to-get-a-list-of-images-on-docker-registry-v2
  • https://github.com/AliyunContainerService/image-syncer
  • https://docs.docker.com/registry/spec/api/

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

寫一個擴展性較強的搜索主頁

前置

  • 點擊按鈕切換搜索引擎
  • 搜索框跟隨切換改變樣式
  • 使用 vue 最快了

template

為了方便擴展,使用 v-for 循環渲染出按鈕,綁定切換搜索引擎的 method , 傳入不同名稱以區別搜索引擎。按鈕的樣式也動態綁定。

輸入框動態綁定樣式,在點擊按鈕切換搜索引擎時,搜索框綁定的樣式對應的 data 改變。

<template>
  <section id="search-wrapper">
    <el-row class="search-wrapper-row">
      <el-row style="margin-bottom: 10px">
        <el-button
          size="mini"
          type="primary"
          v-for="(item, index) in source"
          @click="changeSource(item.name)"
          :key="index"
          :style="
                        `background:${item.color};border-color:${item.color}`
                    "
          >{{ item.name }}</el-button
        >
      </el-row>
      <el-input :placeholder="searchbarStyle.placeholder" :class="searchbarStyle.className" v-model="searchValue" clearable>
        <el-button @click="submit" slot="append" icon="el-icon-search"></el-button>
      </el-input>
    </el-row>
  </section>
</template>

script

data

  • baseUrl 搜索引擎地址
  • searchValue input v-model 綁定的搜索內容
  • searchbarStyle 搜索框對應的樣式,值類型為 Object, 方便擴展不同搜索框樣式
  • source 按鈕的樣式即名稱,數組對象, 方便按鈕擴展

methods

changeSource 點擊按鈕時觸發,接收搜索引擎 name, 內部使用 Map,匹配對應的函數,在函數中更改 baseUrl 和 searchbarStyle,由於在 template 動態綁定了 searchbarStyle,這樣就能根據所選擇的搜索類型改變搜索框的樣式了。

代碼塊較長,我將它摺疊

export default {
  data() {
    return {
      baseUrl: 'https://www.baidu.com/s?ie=UTF-8&wd=',
      searchValue: '',
      searchbarStyle: {
        className: 'baidu',
        placeholder: '百度一下,你就知道',
      },
      source: [
        {
          name: '百度',
          color: '#2932E1',
        },
        {
          name: '必應',
          color: '#0c8484',
        },
        {
          name: '搜狗',
          color: '#FF6F17',
        },
        {
          name: '谷歌',
          color: '#4285F4',
        },
        {
          name: 'NPM',
          color: '#EA4335',
        },
      ],
    }
  },
  methods: {
    changeSource(name) {
      const actions = new Map([
        [
          '百度',
          () => {
            this.baseUrl = 'https://www.baidu.com/s?ie=UTF-8&wd='
            this.searchbarStyle = {
              className: 'baidu',
              placeholder: '百度一下,你就知道',
            }
          },
        ],
        [
          '必應',
          () => {
            this.baseUrl = 'https://cn.bing.com/search?FORM=BESBTB&q='
            this.searchbarStyle = {
              className: 'bing',
              placeholder: '必應搜索',
            }
          },
        ],
        [
          '搜狗',
          () => {
            this.baseUrl = 'https://www.sogou.com/web?query='
            this.searchbarStyle = {
              className: 'sougou',
              placeholder: '搜狗搜索',
            }
          },
        ],
        [
          '谷歌',
          () => {
            this.baseUrl = 'https://www.google.com/search?q='
            this.searchbarStyle = {
              className: 'google',
              placeholder: 'Google Search',
            }
          },
        ],
        [
          'NPM',
          () => {
            this.baseUrl = 'https://www.npmjs.com/search?q='
            this.searchbarStyle = {
              className: 'npm',
              placeholder: 'Search Packages',
            }
          },
        ],
      ])
      actions.get(name)()
    },
    submit() {
      const url = this.baseUrl + this.searchValue
      window.open(url)
    },
  },
}

style

在 searchbarStyle 對象中有個 className 字段,input 會動態綁定與之對應的 css class。比如選擇百度時對應 .baidu, 選擇必應時對應 .bing etc. 由於使用了 scss 預處理器,通過 @each 循環它們就好了。

$sources-color: (
  baidu: #2932e1,
  bing: #0c8484,
  sougou: #ff6f17,
  google: #4285f4,
  npm: #ea4335,
);

$source-list: baidu bing sougou google npm;

@each $source in $source-list {
  .#{$source} {
    .el-input-group__append,
    input {
      border-color: map-get($sources-color, $source);
      &:hover {
        border-color: map-get($sources-color, $source);
      }
    }
    .el-icon-search {
      color: map-get($sources-color, $source);
      &:hover {
        border-color: map-get($sources-color, $source);
      }
    }
  }
}

最後

搜索引擎在搜索時並不是簡單的 baseUrl + 搜索內容的形式,url 中還攜帶了其他參數。

數據可以單獨抽離, 使用 export 導出並引入, 這樣 .vue 看起來不會太長,易於維護。

可以綁定按下 enter 時發起搜索。

預覽地址

如果你有建議歡迎指教,謝謝

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

SpringBoot後端系統的基礎架構

前言

前段時間完成了畢業設計課題——《基於Spring Boot + Vue的直播後台管理系統》,項目名為LBMS,主要完成了對直播平台數據的可視化展示和分級的權限管理。雖然相當順利地通過了答辯,但是由於時間以及本人水平的不足,其實後端系統的代碼還僅僅停留在“能跑就行”。因此這篇文章主要也是為了反思一下項目中亟待完善的地方,我後續也會考慮在此基礎上編寫一個後端管理系統的通用架構模板。

2020/6/10 這個模板項目已經在做了:common-MS
2020/6/12 完成了日誌處理、異常處理、結果封裝、參數校驗模塊

日誌處理

日誌框架

Java中可用的日誌框架有很多,並且通常都有着抽象層+實現層的結構,在實際應用中,只需要考慮抽象層提供的功能接口而不用了解實現層的具體結構。Spring Boot默認的日誌框架為Slf4j + logback。在我的畢設項目中,雖然引入了日誌框架,但是卻很少使用。

Slf4j的輸出級別有5種:trace、debug、info、warn、error,可以通過在properties或yml文件中通過logging.level.root參數指定日誌輸出的級別,其中root代表配置對整個項目生效,可以修改為其他路徑進行自定義配置

日誌代碼的簡化

使用lombok可以簡化代碼的編寫:

Logger logger = LoggerFactory.getLogger(MyLog.class);
logger.info("logger info test");
@Slf4j
// ...
log.info("lombok info test")

對於日誌信息中的變量,建議使用佔位符形式而非字符串拼接

log.info(time + " " + methodName + "is invoked");
log.info("{} {} is invoked", time, methodName)

將日誌輸出到文件

這裏用了某位大牛寫的logback-spring.xml進行配置(可以訪問我的Github獲取具體文件),配置完成后可以將日誌按級別的不同輸出到指定目錄下的不同文件,並且對每天的日誌分開保存,日誌文件大小超過100MB時,還可以自動分塊。

基於AOP的日誌處理

之前用DRF做一個項目時,發現它很貼心地在控制台展示了每個請求的參數、返回狀態碼等信息,SpringBoot當然也可以實現類似的功能。

想要實現上述需求,毫無疑問要在Controller層使用AOP了。對每個請求,我想要輸出對應的URL、請求方法、參數、返回狀態碼等信息。

AOP的切點切面:

@Pointcut("execution(* priv.zzz.controller..*.*(..))")
public void controllerAspect() {}

@Before("controllerAspect()")
public void before(JoinPoint joinPoint){
    log.info(getRequestMessage(joinPoint));
}

@AfterReturning(pointcut = "controllerAspect()", returning = "returnValue")
public void after(JoinPoint joinPoint, Object returnValue){
    if (returnValue instanceof Result){
            log.info(getResponseMessage(joinPoint, ((Result) returnValue).getStatus()));
    }
    if (returnValue instanceof ResultSet){
        log.info(getResponseMessage(joinPoint, ((ResultSet) returnValue).getStatus()));
    }
}

URL、rquestMethod:

private String getBaseMessage(JoinPoint joinPoint) {

    HttpServletRequest request = ((ServletRequestAttributes)(Objects.requireNonNull(RequestContextHolder.getRequestAttributes()))).getRequest();
    String url = request.getRequestURI();
    String requestMethod = request.getMethod();
    String datetime = DateFormatter.format(new Date());

    return datetime + " " + url + " " + requestMethod;
}

請求參數:

private String getRequestMessage(JoinPoint joinPoint) {
    MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
    Object[] args = joinPoint.getArgs();
    String[] parameters = methodSignature.getParameterNames();

    StringBuilder stringBuilder = new StringBuilder();
    for (int i = 0; i < Math.min(args.length, parameters.length); i++){
        stringBuilder.append(parameters[i]).append(":").append(args[i]).append(" ");
    }
    String params = "{ "+stringBuilder.toString()+"}";

    return this.getBaseMessage(joinPoint) + " " + params;
}
private String getResponseMessage(JoinPoint joinPoint, int status) {
    return this.getBaseMessage(joinPoint) + " " + status;
}

最終效果:

2020-06-11 13:10:32 /log GET { name:test number:1 }
2020-06-11 13:10:32 /log GET 200

結果封裝

前後端分離的情況下前後端一般都是通過Json數據進行交互,使用@RestController註解可以將返回的對象轉為Json格式,在那之前,我們需要對返回的結果封裝為Result對象。Result中主要要包含的字段有status、message和data,對於status和message,我使用枚舉類型ResultCode進行封裝,其中包含SUCCESS、NOT_FOUND、UNAUTHORIZED等常見狀態碼。data要考慮返回的數據是否是一個列表,如果是列表,還需要實現分頁功能。

在LBMS中,我將這兩種結果集(單個對象和列表對象)封裝為同一個結果集,在新的模板項目中,我嘗試使用Result和ResultSet兩種結果集進行封裝。這樣做的好處是返回結果更加清晰,缺點是有些地方可能需要一些額外的處理,比如在日誌模塊獲取controller返回的狀態碼時,具體的優劣有待更加深入的使用。

Result示例:

{
  "timestamp": "2020-06-12T15:44:02.106+08:00",
  "status": 200,
  "message": "success",
  "data": 123,
  "path": "/result"
}

ResultSet示例:

{
  "timestamp": "2020-06-12T15:38:01.130+08:00",
  "total": 2,
  "status": 200,
  "message": "success",
  "list": [
    {
      "username": "Alice",
      "age": 20,
      "sex": 0,
      "email": "12345@qq.com"
    },
    {
      "username": "Eric",
      "age": 21,
      "sex": 1,
      "email": "12345@163.com"
    }
  ],
  "path": "/result/set"
}

結果封裝還要考慮的一個問題是對異常的處理,這個我在異常處理章節會談到。

參數校驗

上一個項目中的參數校驗做的相當有限,目前Spring Boot主流的參數校驗方式有hibernate-validator、Assert等。使用validator參數校驗的位置可以在實體類字段處,也可以在Controller傳參處。

網上大部分文章說spring-boot-starter-web已經包含了hibernate-validator,但我不知道為什麼無法直接使用@NotNull等註解,因此手動引入validator:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.5.Final</version>
</dependency>

一個簡單的例子:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TestUser {

    @NotNull(message = "用戶名不能為空")
    @NotBlank(message = "用戶名不能為空")
    @Length(max = 20, message = "用戶名過長")
    private String username;

    @Min(0)
    private Integer age;

    @Range(min = 0, max = 1)
    private Integer sex;

    @Email(message = "郵箱格式錯誤")
    private String email;

}

使用Assert進行校驗:

Assert.notNull(user.getUsername(), "用戶名不能為空");

validator校驗失敗時,會拋出MethodArgumentNotValidException異常。

Assert校驗失敗時會拋出IllegalArgumentException。

實際應用中我們可以靈活使用這兩種校驗方式,並且可以通過ExceptionHandler對這些異常進行捕獲和統一處理。

異常處理

LBMS中,我的異常處理採用的是自定義異常+@ResponseStatus註解的方式,在特定的地方拋出異常,交給ResponseStatusExceptionResolver去處理。

@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "無法識別的操作")
public class BadOperationException extends Exception {

    public BadOperationException(){
        super();
    }

    public BadOperationException(String msg){
        super(msg);
    }
}

在common-MS中,異常處理採用@ControllerAdvice+@ExceptionHandler實現,@ControllerAdvice將一個類標註為全局的異常處理類,@ExceptionHandler用於捕獲不同的異常進行對應處理。同理,對於異常的返回結果也與正常返回結果格式保持一致,使用Result封裝。

例如,捕獲上述validator拋出的MethodArgumentNotValidException異常並進行處理的代碼為:

@ExceptionHandler(value = { MethodArgumentNotValidException.class })
public Result<String> validatorException(HttpServletResponse response, MethodArgumentNotValidException e) {
    // validator設置了message時返回message,未設置則返回“非法參數”
    FieldError error = e.getBindingResult().getFieldError();
    String message = "非法參數";
    if(error != null){
        message = error.getField() + error.getDefaultMessage();
    }
    response.setStatus(400);
    return Result.failure(400, message);
}

當提交的郵箱格式錯誤時返回:

{
  "timestamp": "2020-06-12T15:45:07.874+08:00",
  "status": 400,
  "message": "email郵箱格式錯誤",
  "data": null,
  "path": "/user"
}

同理,還可以對自定義的異常進行處理:

public class ExampleException extends Exception{

    public ExampleException() {super();}

    public ExampleException(String message) {
        super(message);
    }
}

使用時直接拋出異常即可:

@RequestMapping(value = "exception", method = RequestMethod.GET)
public Result exampleException() throws ExampleException {
    throw new ExampleException("這是一個測試異常");
}

如果需要修改Response的狀態碼而不僅僅是使用自定義的status,可以在@ExceptionHandler註解的方法內引入並使用

response.setStatus(400);

待續~

todo:Shiro、分頁功能、Redis等。

完整代碼移步Github:common-MS

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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