就該這樣理解 OSI 七層參考模型、淺談不同局域網之間的通信

簡介

說到OSI參考模型,理解網絡與網絡之間的關係,不說太深入難以理解的東西,只求能最大程度上理解與使用。

參考模型是國際標準化組織(ISO)制定的一個用於計算機或通信系統間互聯的標準體系,一般稱為OSI參考模型或七層模型。

概念性的東西,先知道這些就夠了,我們先來聊一聊一個常見的一個模型。

 

局域網與互聯網

互聯網就是許許多多個局域網組成的,從我們最簡單的一個局域網入手,開始理解

這裏舉例兩個不同的局域網,計算機用網線接入交換機、交換機連接網關路由器,另一處

也是通過相同的方式進行連接。先來了解一下OSI參考模型是如何定義這七層的

OSI 參考模型

參考:

這裏定義的七層只是為了方便我們去理解,實際上是不存在的。

簡單的了解一下這七層是如何定義的,具體的功能還是得舉例子來理解說明。

從最上層的應用層開始說起:如何一步步的封裝數據,到最後進行發送。

應用層

應用層是直接面向用戶的最高層,但它卻不是應用程序,它只是為引用程序提供服務的

就好比,我們用的電腦版微信吧!它就是一個實實在在的應用程序,假設要與一個遠方的小姐姐進行聊天會話,這個時候呢,發送一個Hello給遠方的小姐姐。

當你點擊發送的時候,其實做了很多事情,我們就來梳理一下。

需要發送的數據就是:Hello ,當然,應用層首先給這個數據拼接一個AH,這裏就是應用層的報頭,就好比是微信的一個特有的數據,就這樣先理解。

 

表示層

當然,我們總不能發送明文吧,將發送的文本數據進行編碼,平常我們計算機使用的萬國碼UTF-8,肯定要進行一下加密吧

表示層更關心的是所傳送數據的語法和語義,主要包括數據格式變化、與解密、與解壓等

 

會話層

字面意思,就可以理解出這一層表示的意思,建立一個會話,就好比使用Http訪問web的時候,都會存在一個Session 作為標識

讓服務器來區別訪問的計算機。主要功能是負責維護兩個節點之間的傳輸聯接,確保點到點傳輸不中斷,以及管理數據交換等功能。

會話層在應用進程中建立、管理和終止會話。會話層還可以通過對話控制來決定使用何種通信方式,通信或通信。會話層通過自身協議對請求與應答進行協調

 

傳輸層 端到端

傳輸層作為OSI參考模型中,最重要的一層,這裏主要是以端口到端口來區分。這裏涉及到兩個特別重要的協議TCP 以及UDP

一太計算機上同時運行着QQ、微信、以及瀏覽器等。發送數據報,這個數據包到底是哪個程序發出去的呢?當然要從指定的一個端口發出去

計算機的端口範圍 0-65535

0-1023 就是1024個端口為系統佔用端口

了解到這些,就該先來說說UDP協議

UDP 協議

UDP協議定義了端口,同一個主機上的每個應用程序都需要指定唯一的端口號,並且規定網絡中傳輸的數據包必須加上端口信息,當數據包到達主機以後,就可以根據端口號找到對應的應用程序了。UDP協議比較簡單,實現容易,但它沒有確認機制,數據包一旦發出,無法知道對方是否收到,因此可靠性較差,為了解決這個問題,提高網絡可靠性,TCP協議就誕生了。

 

參考:

一個UDP報文包含首部與數據部分,UDP首部佔用8個字節,數據部分最長長度為65535B(字節) 即 64KB

UDP協議是無連接,不保證穩定傳輸的協議,但處理速度較快,通常的音頻、視頻在傳送時候使用UDP較多。

我們這裏的例子是微信,微信能保證數據百分百到達,所以我們採用TCP來具體說明數據的封裝

 

TCP 協議

TCP即傳輸控制協議,是一種面向連接的、可靠的、基於字節流的通信協議。簡單來說TCP就是有確認機制的UDP協議,每發出一個數據包都要求確認,如果有一個數據包丟失,就收不到確認,發送方就必須重發這個數據包。為了保證傳輸的可靠性,TCP協議在UDP基礎之上建立了三次對話的確認機制,即在正式收發數據前,必須和對方建立可靠的連接。TCP數據包和UDP一樣,都是由首部和數據兩部分組成,唯一不同的是,TCP數據包沒有長度限制,理論上可以無限長,但是為了保證網絡的效率,通常TCP數據包的長度不會超過IP數據包的長度,以確保單個TCP數據包不必再分割

 

參考:

 這裏只需要了解的是TCP的基本封裝過程,這裏只涉及到源端口以及目的端口,還未涉及到IP相關的內容。它和UDP協議一樣。就好像是一個改進版的

UDP協議,它能保證數據的可靠傳輸,這個特點記住即可。這裏模擬一下,我們數據的封裝過程。假設微信使用的端口是6666,目標端口就是遠方小姐姐微信

的端口,當然也是一樣的。這裏我為了理解只做簡寫

 

網絡層

從上面幾層來看,我們已經將微信的數據封裝成來一個TCP數據報,裡面包含來微信的端口 假設是6666,當然,就好比寫信一樣,我的信封

已經準備好勒,裏面要發送的內容我也已經準備好了,接下來就是地址了。肯定要指定這個報文我要發送到哪裡去。所以呢IP 網際協議,就誕生了。

IP 網際協議

網絡層引入了IP協議,制定了一套新地址,使得我們能夠區分兩台主機是否同屬一個網絡,這套地址就是網絡地址,也就是所謂的IP地址。IP協議將這個32位的地址分為兩部分,前面部分代表網絡地址,後面部分表示該主機在局域網中的地址。如果兩個IP地址在同一個子網內,則網絡地址一定相同。為了判斷IP地址中的網絡地址,IP協議還引入了子網掩碼,IP地址和子網掩碼通過按位與運算后就可以得到網絡地址

IP地址在這裏我們就比較好理解了。我們平時的生活中都會涉及到。一個IP指向的就是互聯網當中的一台機器或者就是一台路由器了。

我們來封裝數據。再把上面的圖拿下來,說明一下,我們要給E電腦的小姐姐發送消息。比如我是A電腦,小姐姐在另外一個網關下的E電腦

 

 

比較重要的兩個參數:

源地址:192.168.0.120

目標地址:192.168.1.135

進行封裝后的數據,這裏將源地址,告訴路由器(郵局) 發件人 就是源地址,以及收件人 也就是目標地址

 

ARP 協議

這裏暫時不細說這個ARP協議的內容。我們只需要知道 ARP協議是用來拿IP換MAC地址的,上面的IP協議也已經提過了,通過子網掩碼和IP地址的換算,可以得到

網絡號,網絡號就可以區別這兩個IP是否在同一個局域網內。 參考這個秒懂:

數據鏈路層

到這一層,就已經到網卡、網絡設備(交換機)的範疇了。數據鏈路層最重要的協議是以太網協議,數據鏈路層最重要的一點就是數據成幀。

以太網協議

接入以太網的設備必須包含一塊以太網網卡,也就是我們常用的網卡,一組電信號稱作是一個數據幀 、或者叫做一個數據包

網卡都包含一個全球唯一的MAC地址,發送端的和接收端的地址便是指網卡的地址,即Mac地址。 

每塊網卡出廠時都被燒錄上一個實際上唯一的Mac地址,長度為48位2進制,通常由12位16進制數表示,(前六位是廠商編碼,后六位是流水線號)

 進行數據鏈路層的封裝,將本機的MAC(源MAC地址) 和目標MAC地址封裝在頭部,在尾部加入DT報尾,這樣 一個數據幀算是封裝完成了!

等等。我們好像還不知道小姐姐那邊的目標MAC地址,這時候就需要用ARP協議了。我們知道ARP協議就是用來用IP來換MAC地址的。

 

ARP協議

上面已經簡單的了解過了,我們要和在B局域網下的E電腦進行通信,但是我們不知道它的MAC地址,於是我們發送一個ARP請求,來獲取目標的MAC地址

目標MAC 為FF:FF:FF:FF:FF:FF 表示的是廣播地址,這個數據包發出去后,所有的子網機器都會收到,收到的機器判斷目標MAC是否是自己,若不是,則直接丟棄

若是,收到報文的主機會通過單播的形式,將MAC地址回傳給我們。

通過路由協議我們可以得知,若不在一個一個子網內,則會交給路由器

 路由器返回的包裏面,目標MAC就會變成路由器的MAC地址,我們拿路由器的MAC地址組裝數據鏈路層報文即可。

物理層 

經過以上的每一層的層層包裝,這時候,我們已經包裝好了一個以太網數據幀,包含源MAC,目標MAC,源IP,目標IP等等一系列數據。

物理層就是將這個數據通過電信號、光信號的方式傳遞過去的,物理層一般都是我么所說的光纜以及網線這些硬件設備。

 

 

 不同子網間的通信

通過上面的知識,我們已經了解到如何封裝成一個數據幀,以及一些協議的相關內容。那麼這裏就會有一個問題,同一子網、

不同子網、以及相隔很遠的兩個子網是如何進行通信的呢?以及我們撥號上網后,公網IP與內網IP是怎麼一回事呢?

 

同一子網通信

我們先來看一個圖,計算機A要與計算機B進行通信,這時候他們是同處於一個子網內的,這個時候就很簡單了。

按照上面的七層進行封裝數據,這裏的具體參數需要說明一下:

源IP: 0.120(簡寫)

目標IP:0.113

源MAC : A電腦的MAC

目標MAC:B電腦MAC(這裏若不知道就先發送ARP請求)

 

 

A將數據報發送出去后,交換機直接查詢目標MAC所轉發的端口,將這個數據報準確的推送到B電腦連接的那個端口即可。

不同子網通信

A電腦需要與E電腦進行通信,這時候發現A與E不在一個子網內,這時候呢,就需要路由器來協助了

源IP: A的IP

目標IP: E的IP

源MAC:A的 MAC

目標MAC: 路由器C的MAC

 

 

因為不在一個子網內,需要路由器來進行路由這個數據包,送至D路由器后,D路由器拿出數據報中目標的IP,發送ARP請求,

請求E的MAC地址,知道后,將數據報裏面的目標MAC進行替換,然後發送給E即可。

 

公網IP與內網IP通信的方式理解

我們在使用路由器上網后,運營商就會給我們分配一個公網IP,按照圖上的指示,C路由器在進行撥號后,就會給C路由器分配一個公網IP

我這裏假設有這樣兩個。這時候需要封裝數據,該如何封裝呢,還是以A電腦與E電腦進行通信,大家肯定會很迷惑。

這裏就需要了解一個協議:網路地址轉換協議

以下簡稱NAT,NAT 在IPV4 之前起到很大的作用,我們現在也在用,因為IPV4 IP數量的限制,但接入互聯網的電腦又那麼多

該怎麼辦呢。就是給一個路由下分配一個公網IP,路由器下面的IP與公網IP進行一個轉換,這裏面說的轉換就是:NAT

圖中黑色的就是轉換部分,通過端口的轉換,將多個子網IP映射到公網的一個IP上面

 

網絡地址端口轉換(NAPT)

這種方式支持端口的映射,並允許多台主機共享一個公網IP地址。 支持端口轉換的NAT又可以分為兩類:源地址轉換和目的地址轉換。前一種情形下發起連接的計算機的IP地址將會被重寫,使得內網主機發出的數據包能夠到達外網主機。后一種情況下被連接計算機的IP地址將被重寫,使得外網主機發出的數據包能夠到達內網主機。實際上,以上兩種方式通常會一起被使用以支持雙向通信。 還是舉例,這時候,我們的A電腦需要與E電腦進行通信,E電腦在廣東省,他們撥號后,都會分配一個公網IP,並且已經在路由器裏面完成了NAT映射,
源IP: A電腦IP
目標IP: E電腦映射后的公網IP
源MAC :A電腦MAC
目標MAC :  本地路由器MAC地址   封裝完成后,將數據報送到C路由器,路由器通過映射表,將源IP進行一個替換

 

替換后,交給互聯網上的路由器進行數據報的轉發,這就好像發快遞時候一樣,經過一系列的中轉站,到達目的路由。

到達D路由后,D路由將數據報中的目標地址也進行一個轉換,這個地址是可以相互轉的。現在就是公網映射轉到本機IP

 

轉換后就輕鬆了。按照ARP請求到E機器的MAC地址,然後發報即可。

 

小結

以上內容皆是自己查看一些博主的總結,通過學習后,能夠加深自己對OIS模型、以及TCP、IP、ARP

這些非常重要的協議的一個認識。以及了解到不同層級下面。兩台電腦如何完成一個通行。這裏講的比較淺,

互聯網的奧妙不是那麼容易就可以理解透的。還是那句,不要停止學習的腳步。就好

 

參考:

  • ARP請求 
  • 不同子網內兩台機器的通信方式 
  • OSI 參考模型 
  • 內網端口與外網端口的理解 
  • 網絡地址轉換協議:

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

JS三座大山再學習(二、作用域和閉包)

作用域

JS中有兩種作用域:全局作用域|局部作用域

栗子1

console.log(name);      //undefined
var name = '波妞';
var like = '宗介'
console.log(name);      //波妞
function fun(){
    console.log(name);  //波妞
    console.log(eat)    //ReferenceError: eat is not defined
    (function(){
        console.log(like)   //宗介
        var eat = '肉'
    })()
}
fun();
  1. name定義在全局,在全局可以訪問到,所以 (2) 打印能夠正確打印;
  2. 在函數fun中,如果沒有定義name屬性,那麼會到它的父作用域去找,所以 (3) 也能正確打印。
  3. 內部環境可以通過作用域鏈訪問所有外部環境,但外部環境不能訪問內部環境的任何變量和函數。類似單向透明,這就是作用域鏈,所以 (4) 不行而 (5) 可以。

那麼問題來了,為什麼第一個打印是”undefined”,而不是”ReferenceError: name is not defined”。原理簡單的說就是JS的變量提升

變量提升:JS在解析代碼時,會將所有的聲明提前到所在作用域的最前面

栗子2

console.log(name);      //undefined
var name = '波妞';
console.log(name);      //波妞
function fun(){
    console.log(name)   //undefined
    console.log(like)   //undefined
    var name = '大西瓜';
    var like = '宗介'
}
fun();

相當於

var name;
console.log(name);      //undefined
name = '波妞';
console.log(name);      //波妞
function fun(){
    var name;
    var like;
    console.log(name)   //undefined
    console.log(like)   //undefined
    name = '大西瓜';
    like = '宗介'
    console.log(name)   //大西瓜
    console.log(like)   //宗介
}
fun();

注意:是提前到當前作用域的最前面

栗子3

printName();     //printName is not a function
var printName = function(){
    console.log('波妞')
}
printName();       //波妞

相當於

var printName;
printName();     //printName is not a function
printName = function(){
    console.log('波妞')
}
printName();       //波妞

這樣一來就好理解了,函數表達式在聲明的時候還只是個變量

栗子4

{
    var name = '波妞';
}
console.log(name)   //波妞

(function(){
    var name = '波妞';
})()
console.log(name)   //ReferenceError: name is not defined

{
    let name = '波妞';
}
console.log(name)   //ReferenceError: name is not defined

從上面的栗子可以看出,不可以草率的認為JS中var聲明的變量的作用範圍就是大括號的起止範圍,ES5並沒有塊級作用域,實質是函數作用域;ES6中有了let、const定義后,才有了塊級作用域。

栗子5

function p1() { 
    console.log(1);
}
function p2() { 
    console.log(2);
}
(function () { 
    if (false) {
        function p1() {
            console.log(3);
        }
    }else{
        function p2(){
            console.log(4)
        }
    }
    p2();
    p1()
})();       
//4
//TypeError: print is not a function

這是一個非常經典的栗子,聲明提前了,但是因為判斷條件為否,所以沒有執行函數體。所以會出現”TypeError: print is not a function”。while,switch,for同理

閉包

函數與對其狀態即詞法環境(lexical environment)的引用共同構成閉包(closure)。也就是說,閉包可以讓你從內部函數訪問外部函數作用域。在JavaScript中,函數在每次創建時生成閉包。

上面的定義來自,簡單講,閉包就是指有權訪問另一個函數作用域中變量的函數。

  • 閉包的關鍵在於:外部函數調用之後其變量對象本應該被銷毀,但閉包的存在使我們仍然可以訪問外部函數的變量對象.,
//舉個例子
function makeFunc() {
    var name = "波妞";
    function displayName() {
        console.log(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc();

JavaScript中的函數會形成閉包。 閉包是由函數以及創建該函數的詞法環境組合而成。這個環境包含了這個閉包創建時所能訪問的所有局部變量

在例子中,myFunc 是執行 makeFunc 時創建的 displayName 函數實例的引用,而 displayName 實例仍可訪問其詞法作用域中的變量,即可以訪問到 name 。由此,當 myFunc 被調用時,name 仍可被訪問,其值 ‘波妞’ 就被傳遞到console.log中。創建閉包最常見方式,就是在一個函數內部創建另一個函數

  • 通常,函數的作用域及其所有變量都會在函數執行結束后被銷毀。但是,在創建了一個閉包以後,這個函數的作用域就會一直保存到閉包不存在為止
//例二
function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

//釋放對閉包的引用
add5 = null;
add10 = null;

從本質上講,makeAdder 是一個函數工廠 — 他創建了將指定的值和它的參數相加求和的函數。在上面的示例中,我們使用函數工廠創建了兩個新函數 — 一個將其參數和 5 求和,另一個和 10 求和。

add5 和 add10 都是閉包。它們共享相同的函數定義,但是保存了不同的詞法環境。在 add5 的環境中,x 為 5。而在 add10 中,x 則為 10。

閉包的作用域鏈包含着它自己的作用域,以及包含它的函數的作用域和全局作用域。

  • 閉包只能取得包含函數中的任何變量的最後一個值
//栗子1
function arrFun1(){
    var arr = [];
    for(var i = 0 ; i < 10 ; i++){
        arr[i] = function(){
            return i
        }
    }
    return arr
}
console.log(arrFun1()[9]());     //10
console.log(arrFun1()[1]());     //10

//栗子2
function arrFun2(){
    var arr = [];
    for(var i = 0 ; i < 10 ; i++){
        arr[i] = function(num){
            return function(){
                return num
            };
        }(i)
    }
    return arr
}
console.log(arrFun2()[9]());     //9
console.log(arrFun2()[1]());     //1

栗子 1 中,arr數組中包含10個匿名函數,每個函數都可以訪問外部的變量 i , arrFun1 執行后,其作用域被銷毀,但它的變量依然存在內存中,能被循環中的匿名函數訪問,這是的 i 為 10;

栗子 2 中,arr數組中有是個匿名函數,其匿名函數內還有匿名函數,最內層匿名函數訪問的 num 被 上一級匿名函數保存在了內存中,所以可以訪問到每次的 i 的值。

如有錯誤,請斧正

以上

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

中高級前端面試秘籍,助你直通大廠(一)

引言

又是一年寒冬季,只身前往沿海工作,也是我第一次感受到沿海城市冬天的寒冷。剛過完金九銀十,經過一場慘烈的江湖廝殺后,相信有很多小夥伴兒已經找到了自己心儀的工作,也有的正在找工作的途中。考慮到年後必定又是一場不可避免的廝殺,這裏提前記錄一下自己平時遇到和總結的一些知識點,自己鞏固複習加強基礎的同時也希望能在你的江湖路上對你有所幫助。筆者在入職最近這家公司之前也曾有過長達3個月的閉關修鍊期,期間查閱資料無數,閱讀過很多文章,但總結下來真正讓你印象深刻的,不是那些前沿充滿神秘感的新技術,也不是為了提升代碼逼格的奇淫巧技,而是那些我們經常由於項目周期緊而容易忽略的基礎知識。所謂萬丈高樓平地起,只有你的地基打得足夠牢固,你才有搭建萬丈高樓的底氣,你才能在你的前端人生路上越走越遠

這篇主要是先總結一下CSS相關的知識點,可能某些部分不會涉及到太多具體的細節,主要是對知識點做一下匯總,如果有興趣或者有疑惑的話可以自行百度查閱下相關資料或者在下方評論區留言討論,後續文章再繼續總結JS和其他方面相關的知識點,如有不對的地方還請指出。

1. CSS盒模型

CSS盒模型就是在網頁設計中經常用到的CSS技術所使用的一種思維模型。CSS 假定所有的HTML 文檔元素都生成了一個描述該元素在HTML文檔布局中所佔空間的矩形元素框,可以形象地將其看作是一個盒子。CSS 圍繞這些盒子產生了一種“盒子模型”概念,通過定義一系列與盒子相關的屬性,可以極大地豐富和促進各個盒子乃至整個HTML文檔的表現效果和布局結構。

CSS盒模型可以看成是由從內到外的四個部分構成,即內容區(content)、內邊距(padding)、邊框(border)和外邊距(margin)。內容區是盒子模型的中心,呈現盒子的主要信息內容;內邊距是內容區和邊框之間的空間;邊框是環繞內容區和內邊距的邊界;外邊距位於盒子的最外圍,是添加在邊框外周圍的空間。

根據計算寬高的區域我們可以將其分為IE盒模型W3C標準盒模型,可以通過box-sizing來進行設置:

  • content-box:W3C標準盒模型
  • border-box:IE盒模型

區別:
W3C標準盒模型:width(寬度) = content(內容寬度)
IE盒模型:width(寬度) = content(內容寬度) + padding(內邊距) + border(邊框)

2. BFC

BFC即Block Fromatting Context(塊級格式化上下文),它是頁面中的一塊獨立的渲染區域,並且有一套渲染規則,它決定了其子元素將如何定位,以及和其他元素的關係和相互作用。具有BFC特性的元素可以看成是一個隔離的獨立容器,讓處於BFC內部的元素與外部的元素相互隔離,使內外元素的定位不會相互影響。

IE瀏覽器下為hasLayout,一般可以通過zoom:(除normal外任意值)來觸發,hasLayout是IE瀏覽器渲染引擎的一個內部組成部分。在IE瀏覽器中,一個元素要麼自己對自身的內容進行計算大小和組織,要麼依賴於父元素來計算尺寸和和組織內容。為了調節這兩個不同的概念,渲染引擎採用了hasLayout的屬性,屬性值可以為true或false。當一個元素的hasLayout屬性為true時,我們就說這個元素有一個布局(Layout)。當擁有布局后,它會負責對自己和可能的子孫元素進行尺寸計算和定位,而不是依賴於祖先元素來完成這些工作。

2.1 觸發條件

  • 根元素(<html>)
  • 浮動元素(元素的float不是none)
  • 絕對定位元素(元素的positionabsolutefixed)
  • 行內塊元素(元素的displayinline-block)
  • 表格單元格(元素的displaytable-cell,HTML表格單元格默認為該值)
  • 表格標題(元素的displaytable-caption,HTML表格標題默認為該值)
  • display值為flow-root的元素
  • overflow屬性的值不為visible
  • 彈性元素(displayflexinline-flex元素的直接子元素)
  • 網格元素(displaygrid或者inline-grid元素的直接子元素)

    2.2 布局規則

    普通文檔流布局規則

  • 浮動的元素是不會被父級計算高度的
  • 非浮動元素會覆蓋浮動元素的位置
  • margin會傳遞給父級
  • 兩個相鄰元素上下margin會發生重疊

BFC布局規則

  • 浮動的元素會被父級計算高度(父級觸發了BFC)
  • 非浮動元素不會覆蓋浮動元素的位置(非浮動元素觸發了BFC)
  • margin不會傳遞給父級(父級觸發了BFC)
  • 兩個相鄰元素上下margin不會發生重疊(給其中一個元素增加一個父級,並讓它的父級觸發BFC)

    2.3 應用

  • 防止margin重疊
  • 清除內部浮動(原理是父級計算高度時,浮動的子元素也會參与計算)
  • 自適應兩欄布局
  • 防止元素被浮動元素所覆蓋

    3. 層疊上下文

    層疊上下文(stacking context),是HTML中一個三維的概念。在CSS2.1規範中,每個盒模型的位置都是三維的,分別是平面畫布上的X軸Y軸以及表示層疊的Z軸。一般情況下,元素在頁面上沿X軸Y軸平鋪,我們察覺不到它們在Z軸上的層疊關係。而一旦元素髮生堆疊,這時就能發現某個元素可能覆蓋了另一個元素或者被另一個元素覆蓋。

如果一個元素含有層疊上下文,我們就可以理解為這個元素在Z軸上就”高人一等”,最終表現就是它離屏幕觀察者更近。

你可以把層疊上下文理解為該元素當了官,而其他非層疊上下文元素則可以理解為普通群眾。凡是”當了官的元素”就比普通元素等級要高,也就是說元素在Z軸上更靠上,更靠近觀察者。

3.1 觸發條件

  • 根層疊上下文(<html>)
  • position屬性為非static值並設置z-index為具體數值
  • CSS3中的屬性也可以產生層疊上下文
    • flex
    • transform
    • opacity
    • filter
    • will-change
    • -webkit-overflow-scrolling

      3.2 層疊等級

      層疊等級(stacking level),又叫”層疊級別”或者”層疊水平”。

  • 在同一個層疊上下文中,它描述定義的是該層疊上下文中的層疊上下文元素在Z軸上的上下順序
  • 在其他普通元素中,它描述定義的是這些普通元素在Z軸上的上下順序

    注意:

    1. 普通元素的層疊等級優先由其所在的層疊上下文決定。
    2. 層疊等級的比較只有在當前層疊上下文元素中才有意義,不同層疊上下文中比較層疊等級是沒有意義的。

根據以上的層疊等級圖,我們在比較層疊等級時可以按照以下的思路來順序比較:

  • 首先判定兩個要比較的元素是否處於同一個層疊上下文中
  • 如果處於同一個層疊上下文中,則誰的層疊等級大,誰最靠上
  • 如果處於不同的層疊上下文中,則先比較他們所處的層疊上下文的層疊等級
  • 當兩個元素層疊等級相同,層疊順序相同時,在DOM結構中後面的元素層疊等級在前面元素之上

4. CSS3中新增的選擇器以及屬性

  • 屬性選擇器:
屬性選擇器 含義描述
E[attr^=”val”] 屬性attr的值以”val”開頭的元素
E[attr$=”val”] 屬性attr的值以”val”結尾的元素
E[attr*=”val”] 屬性attr的值包含“val”子字符串的元素
  • 結構偽類選擇器
選擇器 含義描述
E:root 匹配元素所在文檔的根元素,對於HTML文檔,根元素始終是<html>
E:nth-child(n) 匹配其父元素的第n個子元素,第一個編號為1
E:nth-last-child(n) 匹配其父元素的倒數第n個子元素,第一個編號為1
E:nth-of-type(n) 與:nth-child()作用類似,但是僅匹配使用同種標籤的元素
E:nth-last-of-type(n) 與:nth-last-child() 作用類似,但是僅匹配使用同種標籤的元素
E:last-child 匹配父元素的最後一個子元素,等同於:nth-last-child(1)
E:first-of-type 匹配父元素下使用同種標籤的第一個子元素,等同於:nth-of-type(1)
E:last-of-type 匹配父元素下使用同種標籤的最後一個子元素,等同於:nth-last-of-type(1)
E:only-child 匹配父元素下僅有的一個子元素,等同於:first-child:last-child或 :nth-child(1):nth-last-child(1)
E:only-of-type 匹配父元素下使用同種標籤的唯一一個子元素,等同於:first-of-type:last-of-type或 :nth-of-type(1):nth-last-of-type(1)
E:empty 匹配一個不包含任何子元素的元素,文本節點也被看作子元素
E:not(selector) 匹配不符合當前選擇器的任何元素
  • CSS3新增屬性
屬性 含義描述
transition 過渡效果
transform 變換效果(移動(translate)、縮放(scale)、旋轉(rotate)、傾斜(skew))
transform-origin 設置旋轉元素的基點位置
animation 動畫效果
border-color 為邊框設置多種顏色
border-radius 圓角邊框
box-shadow 邊框陰影
border-image 邊框圖片
background-size 規定背景圖片的尺寸
background-origin 規定背景圖片的定位區域
background-clip 規定背景圖片從什麼位置開始裁切
text-shadow 文本陰影
text-overflow 文本截斷
word-wrap 對長單詞進行拆分,並換行到下一行
opacity 不透明度
box-sizing 控制盒模型的組成模式
rgba 基於r,g,b三個顏色通道來設置顏色值,通過a來設置透明度

5. CSS3中transition和animation的屬性

1) transition(過渡動畫)

用法:transition: property duration timing-function delay
| 屬性 | 含義描述 |
| —- | —- |
| transition-property | 指定哪個CSS屬性需要應用到transition效果 |
| transition-duration | 指定transition效果的持續時間 |
| transition-timing-function | 指定transition效果的速度曲線 |
| transition-delay | 指定transition效果的延遲時間 |

2) animation(關鍵幀動畫)

用法:animation: name duration timing-function delay iteration-count direction fill-mode play-state
| 屬性 | 含義描述 |
| —- | —- |
| animation-name | 指定要綁定到選擇器的關鍵幀的名稱 |
| animation-duration | 指定動畫的持續時間 |
| animation-timing-function | 指定動畫的速度曲線 |
| animation-delay | 指定動畫的延遲時間 |
| animation-iteration-count | 指定動畫的播放次數 |
| animation-direction | 指定是否應該輪流反向播放動畫 |
| animation-fill-mode | 規定當動畫不播放時(當動畫完成時,或當動畫有一個延遲未開始播放時),要應用到元素的樣式 |
| animation-play-state | 指定動畫是否正在運行或已暫停 |

6. 清除浮動的方式以及各自的優缺點

  • 額外標籤法(在最後一個浮動元素的後面新加一個標籤如<div class="clear"></div>,並在其CSS樣式中設置clear: both;)

    優點:簡單,通俗易懂,寫少量代碼,兼容性好
    缺點:額外增加無語義html元素,代碼語義化差,後期維護成本大

  • 給父級設置高度

    優點:簡單,寫少量代碼,容易掌握
    缺點:不夠靈活,只適用於高度固定的布局

  • 觸發父級BFC(如給父元素設置overflow:hidden,特別注意的是:在IE6中還需要觸發hasLayout,例如給父元素設置zoom:1。原理是觸發父級BFC后,父元素在計算高度時,浮動的子元素也會參与計算)

    優點:簡單,代碼簡潔
    缺點:設置overflow:hidden容易造成不會自動換行導致超出的尺寸被隱藏掉,無法显示要溢出的元素

  • 使用after偽元素,常見的寫法如下:
 .clearfix::after {
    content: ".";
    display: block;
    height: 0;
    line-height: 0;
    clear: both;
    visibility:hidden;
    font-size: 0;
 }
 
 .clearfix {
    // 注意此處是為了兼容IE6和IE7瀏覽器,即觸發hasLayout
    zoom: 1;
 }

優點:符合閉合浮動思想,結構語義化正確
缺點:代碼量多,因為IE6-7下不支持after偽元素,需要額外寫zoom:1來觸發hasLayout

7. 居中布局的方式

水平居中

  • 若是行內元素,則直接給其父元素設置text-align: center即可
  • 若是塊級元素,則直接給該元素設置margin: 0 auto即可
  • 若子元素包含浮動元素,則給父元素設置width:fit-content並且配合margin
.parent {
    width: -webkit-fit-content;
    width: -moz-fit-content;
    width: fit-content;
    margin: 0 auto;
}
  • 使用flex布局的方式,可以輕鬆實現水平居中,即使子元素中存在浮動元素也同樣適用
// flex 2012年版本寫法
.parent {
    display: flex;
    flex-direction: row;
    justify-content: center;
}

// flex 2009年版本寫法
.parent {
    display: box;
    box-orient: horizontal;
    box-pack: center;
}
  • 使用絕對定位的方式,再配合CSS3新增的transform屬性
.child {
    position: absolute;
    left: 50%;
    transform: translate(-50%, 0);
}
  • 使用絕對定位的方式,再配合負值的margin-left(此方法需要固定寬度)
.child {
    position: absolute;
    left: 50%;
    width: 200px; // 假定寬度為200px
    margin-left: -100px; // 負值的絕對值為寬度的一半
}
  • 使用絕對定位的方式,再配合left:0;right:0;margin:0 auto;(此方法需要固定寬度)
.child {
    position: absolute;
    left: 0;
    right: 0;
    margin: 0 auto;
    width: 200px; // 假定寬度為200px
}

垂直居中

  • 若元素是單行文本,則直接給該元素設置line-height等於其父元素的高度
  • 若元素是行內塊級元素,可以配合使用display:inline-block;vertical-align:middle和一個偽元素來讓內容塊居中
.parent::after, .child {
    display: inline-block;
    vertical-align: middle;
}

.parent::after {
    content: "";
    height: 100%;
}
  • 使用vertical-align屬性並且配合使用display:tabledisplay:table-cell來讓內容塊居中
.parent {
    display: table;
}

.child {
    display: table-cell;
    vertical-align: middle;
}
  • 使用flex布局的方式,可以輕鬆實現垂直居中,即使子元素中存在浮動元素也同樣適用
// flex 2012年版本寫法
.parent {
    display: flex;
    align-items: center;
}

// flex 2009年版本寫法
.parent {
    display: box;
    box-orient: vertical;
    box-pack: center;
}
  • 使用絕對定位的方式,再配合CSS3新增的transform屬性
.child {
    position: absolute;
    top: 50%;
    transform: translate(0, -50%);
}
  • 使用絕對定位的方式,再配合負值的margin-top(此方法需要固定高度)
.child {
    position: absolute;
    top: 50%;
    height: 200px; // 假定高度為200px
    margin-top: -100px; // 負值的絕對值為高度的一半
}
  • 使用絕對定位的方式,再配合top:0;bottom:0;margin:auto 0;(此方法需要固定高度)
.child {
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto 0;
    height: 200px; // 假定高度為200px
}

水平垂直居中

  • 使用flex布局的方式同樣可以輕鬆實現水平垂直居中
// flex 2012年版本寫法
.parent {
    display: flex;
    justify-content: center;
    align-items: center;
}

// flex 2009年版本寫法
.parent {
    display: box;
    box-pack: center;
    box-align: center;
}
  • 使用絕對定位的方式,再配合CSS3新增的transform屬性
.child {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
}
  • 使用絕對定位的方式,再配合使用負值的margin-top和負值的margin-left(此方法需要同時固定寬度和高度)
.child {
    position: absolute;
    left: 50%;
    top: 50%;
    margin-top: -50px; // 負值的絕對值為高度的一半
    margin-left: -100px; // 負值的絕對值為寬度的一半
    width: 200px; // 假定寬度為200px
    height: 100px; // 假定高度為100px
}

8. CSS的優先級和權重

選擇器(優先級從高到低) 示例 特殊性值
!important(重要性標識) div { color: #fff !important; } 無,但為了方便記憶,可將其表示為1,0,0,0,0
行內樣式 <div style="color: #fff;"></div> 1,0,0,0
id選擇器 #id 0,1,0,0
類,偽類和屬性選擇器 .content, :first-child, [type="text"] 0,0,1,0
標籤和偽元素選擇器 h1, ::after 0,0,0,1
通配符、子選擇器、相鄰選擇器 *, div > p, p + p 0,0,0,0
繼承 span { color: inherit; }
瀏覽器默認值 瀏覽器開發者工具右側的Styles面板中會显示user agent stylesheet字樣

9. 移動端1px物理像素邊框

我們知道,在移動端存在物理像素(physical pixel)設備獨立像素(density-independent pixel)的概念。物理像素也稱為設備像素,它是显示設備中一個最微小的物理部件,每個像素可以根據操作系統設置自己的顏色和亮度。設備獨立像素也稱為密度無關像素,可以認為是計算機坐標系統中的一個點,這個點代表一個可以由程序使用的虛擬像素(比如CSS像素),然後由相關係統轉換為物理像素。根據物理像素和設備獨立像素也衍生出了設備像素比(device pixel ratio)的概念,簡稱為dpr,其定義了物理像素和設備獨立像素的對應關係,其計算公式為設備像素比 = 物理像素 / 設備獨立像素。因為視網膜(Retina)屏幕的出現,使得一個物理像素並不能和一個設備獨立像素完全對等,如下圖所示:

在上圖中,在普通屏幕下1個CSS像素對應1個物理像素,而在Retina屏幕下,1個CSS像素卻對應4個物理像素,即在Retina屏幕下會有不同的dpr值。為了追求在移動端網頁中更好的显示質量,因此我們需要做各種各樣的適配處理,最經典的莫過於1px物理像素邊框問題,我們需要根據移動端不同的dpr值來對邊框進行處理。在JavaScript中,可以通過window.devicePixelRatio來獲取當前設備的dpr,在CSS中,可以通過-webkit-device-pixel-ratio,-webkit-min-device-pixel-ratio和-webkit-max-device-pixel-ratio來進行媒體查詢,從而針對不同的設備,來做一些樣式適配。這裏對於1px像素的邊框問題,給出一種最常見的寫法:

.border-1px {
    position: relative;
}

.border-1px::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 1px;
    background-color: #000;
    -webkit-transform: scaleY(.5);
    transform: scaleY(.5);
}

@media only screen and (-webkit-min-device-pixel-ratio: 2.0), (min-device-pixel-ratio: 2.0) {
    .border-1px::after {
        -webkit-transform: scaleY(.5);
        transform: scaleY(.5);
    }
}

@media only screen and (-webkit-min-device-pixel-ratio: 3.0), (min-device-pixel-ratio: 3.0) {
    .border-1px::after {
        -webkit-transform: scaleY(.33);
        transform: scaleY(.33);
    }
}

10. 實現三欄布局的方式有哪些

三欄布局,顧名思義就是分為左中右三個模塊進行布局,並且左右兩邊固定,中間模塊根據瀏覽器的窗口變化進行自適應,效果圖如下:

這裏給出四種實現三欄布局的方式:

  • 使用絕對定位的方式
.container {
    position: relative;
    height: 200px;
    line-height: 200px;
    text-align: center;
    font-size: 20px;
    color: #fff;
}

.left {
    position: absolute;
    left: 0;
    top: 0;
    width: 150px;
    background: red;
}

.main {
    margin-left: 160px;
    margin-right: 110px;
    background: green;
}

.right {
    position: absolute;
    right: 0;
    top: 0;
    width: 100px;
    background: blue;
}

<div class="container">
    <div class="left">左</div>
    <div class="main">中</div>
    <div class="right">右</div>
</div>

優點:方便快捷,簡單實用,不容易出現問題,而且還可以將<div class="main"></div>元素放到最前面,使得主要內容被優先加載。
缺點:元素脫離了文檔流,可能會造成元素的重疊。

  • 使用flex布局的方式
.container {
    display: flex;      
    height: 200px;
    line-height: 200px;
    text-align: center;
    font-size: 20px;
    color: #fff;
}

.left {
    width: 150px;
    background: red;
}

.main {
    margin: 0 10px;
    flex: 1;
    background: green;
}

.right {
    width: 100px;
    background: blue;
}

<div class="container">
    <div class="left">左</div>
    <div class="main">中</div>
    <div class="right">右</div>
</div>

優點:簡單實用,是現在比較流行的方案,特別是在移動端,大多數布局都採用的這種方式,是目前比較完美的一個。
缺點:需要考慮到瀏覽器的兼容性,根據不同的瀏覽器廠商需要添加相應的前綴。

  • 雙飛翼布局
.content {
    float: left;
    width: 100%;
}

.main,
.left,
.right {
    height: 200px;
    line-height: 200px;
    text-align: center;
    font-size: 20px;
    color: #fff;
}

.main {
    margin-left: 160px;
    margin-right: 110px;
    background: green;
}

.left {
    float: left;
    margin-left: -100%;
    width: 150px;
    background: red;
}

.right {
    float: right;
    margin-left: -100px;
    width: 100px;
    background: blue;
}

<div class="content">
    <div class="main">中</div>
</div>
<div class="left">左</div>
<div class="right">右</div>

優點:比較經典的一種方式,通用性強,沒有兼容性問題,而且支持主要內容優先加載。
缺點:元素脫離了文檔流,要注意清除浮動,防止高度塌陷,同時額外增加了一層DOM結構,即增加了渲染樹生成的計算量。

  • 聖杯布局
.container {
    margin-left: 160px;
    margin-right: 110px;
}

.left,
.main,
.right {
    height: 200px;
    line-height: 200px;
    text-align: center;
    font-size: 20px;
    color: #fff;    
}

.main {
    float: left;
    width: 100%;
    background: green;      
}

.left {
    position: relative;
    left: -160px;
    margin-left:  -100%;
    float: left;
    width: 150px;
    background: red;
}

.right {
    position: relative;
    right: -110px;
    margin-left:  -100px;
    float: left;
    width: 100px;
    background: blue;
}

<div class="container">
    <div class="main">中</div>
    <div class="left">左</div>
    <div class="right">右</div>
</div>

優點:相比於雙飛翼布局,結構更加簡單,沒有多餘的DOM結構層,同樣支持主要內容優先加載。
缺點:元素同樣脫離了文檔流,要注意清除浮動,防止高度塌陷。

11. 實現等高布局的方式有哪些

等高布局,顧名思義就是在同一個父容器中,子元素高度相等的布局。從等高布局的實現方式來說,可以分為兩種,分別是偽等高真等高偽等高是指子元素的高度差依然存在,只是視覺上給人的感覺就是等高,真等高是指子元素的高度真實相等。效果圖如下:

這裏給出五種實現等高布局的方式:

偽等高

  • 使用padding-bottom和負的margin-bottom來實現
.container {
    position: relative;
    overflow: hidden;
}
    
.left,
.main,
.right {
    padding-bottom: 100%;
    margin-bottom: -100%;
    float: left;
    color: #fff;
}

.left {
    width: 20%;
    background: red;
}

.main {
    width: 60%;
    background: green;
}

.right {
    width: 20%;
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>

真等高

  • 使用flex布局的方式
.container {
    display: flex;
}

.left,
.main,
.right {
    flex: 1;
    color: #fff;
}

.left {
    background: red;
}

.main {
    background: green;
}

.right {
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>
  • 使用絕對定位的方式
.container {
  position: relative;
  height: 200px;
}

.left,
.main,
.right {
    position: absolute;
    top: 0;
    bottom: 0;
    color: #fff;
}

.left {
    left: 0;
    width: 20%;
    background: red;
}

.main {
    left: 20%;
    right: 20%;
    background: green;
}

.right {
    right: 0;
    width: 20%;
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>
  • 使用table布局的方式
.container {
    width: 100%;
    display: table;
}

.left,
.main,
.right {
    display: table-cell;
    color: #fff;
}

.left {
    width: 20%;
    background: red;
}

.main {
    width: 60%;
    background: green;
}

.right {
    width: 20%;
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>
  • 使用grid網格布局的方式
.container {
    display: grid;
    width: 100%;
    grid-template-columns: 1fr 1fr 1fr;
    color: #fff;
}

.left {
    background: red;
}

.main {
    background: green;
}

.right {
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>

12. CSS實現三角形的原理

工作中我們經常會遇到需要三角形圖標的應用場景,例如內容展開收起、左右箭頭點擊切換輪播,點擊某條列表數據查看詳情等。三角形圖標的應用範圍之廣,使得我們有必要了解一下它的實現原理。
1) 首先我們來實現一個最基礎的邊框效果

.content {
    width: 50px;
    height: 50px;
    border: 2px solid;
    border-color:#ff9600 #3366ff #12ad2a #f0eb7a;
}

效果如下:

2) 然後我們嘗試將border值放大10倍

.content {
    width: 50px;
    height: 50px;
    border: 20px solid;
    border-color: #ff9600 #3366ff #12ad2a #f0eb7a;
}

效果如下:

上圖中我們可以很清楚地看到,在繪製border的時候並不是矩形區域,而是梯形區域,那麼此時如果我們將widthheight值設置為0,看會發生什麼:

.content {
    width: 0;
    height: 0;
    border: 20px solid;
    border-color: #ff9600 #3366ff #12ad2a #f0eb7a;
}

效果如下:

此時會看到一個由四個三角形拼裝而成的矩形區域,即由上下左右四個邊框組合而成。因此不難想象,如果我們想得到某一個方向的三角形,我們只需要讓其他方向的邊框不可見就行了,例如我們想得到一個朝左的三角形:

.content {
    width: 0;
    height: 0;
    border: 20px solid;
    border-color: transparent #3366ff transparent transparent;
}

效果如下:

這樣就得到了一個很完美的三角形圖標,是不是很簡單?

13. link與@import的區別

  • 從屬關係區別

    @import是CSS提供的語法規則,只有導入樣式表的作用;link是HTML提供的標籤,不僅可以加載 CSS 文件,還可以定義 RSS,Rel連接屬性,設置瀏覽器資源提示符preload、prefetch等。

  • 加載順序區別

    HTML文檔在解析的過程當中,如果遇到link標籤,則會立即發起獲取CSS文件資源的請求;@import引入的CSS將在頁面加載完畢后才會被加載。

  • 兼容性區別

    @import是CSS2.1才有的語法,因此需要IE5以上才能識別;link標籤作為HTML元素,不存在兼容性問題。

  • DOM可控性區別

    link標籤可以通過JS來動態引入,而@import無法通過JS來插入樣式

const loadStyle = (url) => {
    const link = document.createElement('link');
    link.setAttribute('type', 'text/css');
    link.setAttribute('rel', 'stylesheet');
    link.setAttribute('href', url);
    
    document.head.appendChild(link);
}

14. 瀏覽器是怎樣解析CSS選擇器的

CSS選擇器的解析是從右向左解析的。若從左向右地匹配,發現不符合規則,需要進行回溯,會損失很多性能。若從右向左匹配,先找到所有的最右節點,對於每一個節點,向上尋找其父節點直到找到根元素或滿足條件的匹配規則,則結束這個分支的遍歷。兩種匹配規則的性能差別很大,是因為從右向左的匹配在第一步就篩選掉了大量的不符合條件的最右節點(恭弘=叶 恭弘子節點),而從左向右的匹配規則的性能都浪費在了失敗的查找上面。而在CSS解析完畢后,需要將解析的結果與DOM Tree的內容一起進行分析建立一棵 Render Tree,最終用來進行繪圖。在建立Render Tree時瀏覽器就要為每個DOM Tree中的元素根據CSS的解析結果(Style Rules)來確定生成怎樣的Render Tree。

15. CSS的性能優化方案

  • 層級盡量扁平,避免嵌套過多層級的選擇器;
  • 使用特定的選擇器,避免解析器過多層級的查找;
  • 減少使用通配符與屬性選擇器;
  • 減少不必要的多餘屬性;
  • 避免使用!important標識,可以選擇其他選擇器;
  • 實現動畫時優先使用CSS3的動畫屬性,動畫時脫離文檔流,開啟硬件加速;
  • 使用link標籤代替@import;
  • 將渲染首屏內容所需的關鍵CSS內聯到HTML中;
  • 使用資源預加載指令preload讓瀏覽器提前加載CSS資源並緩存;
  • 使用Gulp,Webpack等構建工具對CSS文件進行壓縮處理;

推薦閱讀

交流

終於接近尾聲了,居然花費掉了我一整個周末的時間,不過這篇主要是先總結一下CSS相關的知識點,當然還有很多地方沒有總結到,只是列出了個人覺得比較容易考察的點,如果你有其他補充的,歡迎在下方留言區討論哦,也歡迎關注我的公眾號[前端之境],關注后我可以拉你加入微信前端交流群,我們一起互相交流學習,共同進步。
後續會陸續總結出JS方面、瀏覽器視角、算法基礎和框架方面的內容,希望你能夠喜歡!

文章已同步更新至,若覺文章尚可,歡迎前往star!

你的一個點贊,值得讓我付出更多的努力!

逆境中成長,只有不斷地學習,才能成為更好的自己,與君共勉!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

[springboot 開發單體web shop] 8. 商品詳情&評價展示

上文回顧

我們實現了根據搜索關鍵詞查詢商品列表和根據商品分類查詢,並且使用到了mybatis-pagehelper插件,講解了如何使用插件來幫助我們快速實現分頁數據查詢。本文我們將繼續開發商品詳情頁面和商品留言功能的開發。

需求分析

關於商品詳情頁,和往常一樣,我們先來看一看jd的示例:

從上面2張圖,我們可以看出來,大體上需要展示給用戶的信息。比如:商品圖片,名稱,價格,等等。在第二張圖中,我們還可以看到有一個商品評價頁簽,這些都是我們本節要實現的內容。

商品詳情

開發梳理

我們根據上圖(權當是需求文檔,很多需求文檔寫的比這個可能還差勁很多…)分析一下,我們的開發大致都要關注哪些points:

  • 商品標題
  • 商品圖片集合
  • 商品價格(原價以及優惠價)
  • 配送地址(我們的實現不在此,我們後續直接實現在下單邏輯中)
  • 商品規格
  • 商品分類
  • 商品銷量
  • 商品詳情
  • 商品參數(生產場地,日期等等)

根據我們梳理出來的信息,接下來開始編碼就會很簡單了,大家可以根據之前課程講解的,先自行實現一波,請開始你們的表演~

編碼實現

DTO實現

因為我們在實際的數據傳輸過程中,不可能直接把我們的數據庫entity之間暴露到前端,而且我們商品相關的數據是存儲在不同的數據表中,我們必須要封裝一個ResponseDTO來對數據進行傳遞。

  • ProductDetailResponseDTO包含了商品主表信息,以及圖片列表、商品規格(不同SKU)以及商品具體參數(產地,生產日期等信息)
@Data
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProductDetailResponseDTO {
    private Products products;
    private List<ProductsImg> productsImgList;
    private List<ProductsSpec> productsSpecList;
    private ProductsParam productsParam;
}

Custom Mapper實現

根據我們之前表的設計,這裏使用生成的通用mapper就可以滿足我們的需求。

Service實現

從我們封裝的要傳遞到前端的ProductDetailResponseDTO就可以看出,我們可以根據商品id分別查詢出商品的相關信息,在controller進行數據封裝就可以了,來實現我們的查詢接口。

  • 查詢商品主表信息(名稱,內容等)

    com.liferunner.service.IProductService中添加接口方法:

        /**
         * 根據商品id查詢商品
         *
         * @param pid 商品id
         * @return 商品主信息
         */
        Products findProductByPid(String pid);

    接着,在com.liferunner.service.impl.ProductServiceImpl中添加實現方法:

        @Override
        @Transactional(propagation = Propagation.SUPPORTS)
        public Products findProductByPid(String pid) {
            return this.productsMapper.selectByPrimaryKey(pid);
        }

    直接使用通用mapper根據主鍵查詢就可以了。

    同上,我們依次來實現圖片、規格、以及商品參數相關的編碼工作

  • 查詢商品圖片信息列表

        /**
         * 根據商品id查詢商品規格
         *
         * @param pid 商品id
         * @return 規格list
         */
        List<ProductsSpec> getProductSpecsByPid(String pid);
    
    ----------------------------------------------------------------
    
        @Override
        public List<ProductsSpec> getProductSpecsByPid(String pid) {
            Example example = new Example(ProductsSpec.class);
            val condition = example.createCriteria();
            condition.andEqualTo("productId", pid);
            return this.productsSpecMapper.selectByExample(example);
        }
  • 查詢商品規格列表

        /**
         * 根據商品id查詢商品規格
         *
         * @param pid 商品id
         * @return 規格list
         */
        List<ProductsSpec> getProductSpecsByPid(String pid);
    
    ------------------------------------------------------------------
    
        @Override
        public List<ProductsSpec> getProductSpecsByPid(String pid) {
            Example example = new Example(ProductsSpec.class);
            val condition = example.createCriteria();
            condition.andEqualTo("productId", pid);
            return this.productsSpecMapper.selectByExample(example);
        }
  • 查詢商品參數信息

        /**
         * 根據商品id查詢商品參數
         *
         * @param pid 商品id
         * @return 參數
         */
        ProductsParam findProductParamByPid(String pid);
    
    ------------------------------------------------------------------
    
        @Override
        public ProductsParam findProductParamByPid(String pid) {
            Example example = new Example(ProductsParam.class);
            val condition = example.createCriteria();
            condition.andEqualTo("productId", pid);
            return this.productsParamMapper.selectOneByExample(example);
        }

Controller實現

在上面將我們需要的信息查詢實現之後,然後我們需要在controller對數據進行包裝,之後再返回到前端,供用戶來進行查看,在com.liferunner.api.controller.ProductController中添加對外接口/detail/{pid},實現如下:

    @GetMapping("/detail/{pid}")
    @ApiOperation(value = "根據商品id查詢詳情", notes = "根據商品id查詢詳情")
    public JsonResponse findProductDetailByPid(
        @ApiParam(name = "pid", value = "商品id", required = true)
        @PathVariable String pid) {
        if (StringUtils.isBlank(pid)) {
            return JsonResponse.errorMsg("商品id不能為空!");
        }
        val product = this.productService.findProductByPid(pid);
        val productImgList = this.productService.getProductImgsByPid(pid);
        val productSpecList = this.productService.getProductSpecsByPid(pid);
        val productParam = this.productService.findProductParamByPid(pid);
        val productDetailResponseDTO = ProductDetailResponseDTO
            .builder()
            .products(product)
            .productsImgList(productImgList)
            .productsSpecList(productSpecList)
            .productsParam(productParam)
            .build();
        log.info("============查詢到商品詳情:{}==============", productDetailResponseDTO);

        return JsonResponse.ok(productDetailResponseDTO);
    }

從上述代碼中可以看到,我們分別查詢了商品、圖片、規格以及參數信息,使用ProductDetailResponseDTO.builder().build()封裝成返回到前端的對象。

Test API

按照慣例,寫完代碼我們需要進行測試。

{
  "status": 200,
  "message": "OK",
  "data": {
    "products": {
      "id": "smoke-100021",
      "productName": "(奔跑的人生) - 中華",
      "catId": 37,
      "rootCatId": 1,
      "sellCounts": 1003,
      "onOffStatus": 1,
      "createdTime": "2019-09-09T06:45:34.000+0000",
      "updatedTime": "2019-09-09T06:45:38.000+0000",
      "content": "吸煙有害健康“
    },
    "productsImgList": [
      {
        "id": "1",
        "productId": "smoke-100021",
        "url": "http://www.life-runner.com/product/smoke/img1.png",
        "sort": 0,
        "isMain": 1,
        "createdTime": "2019-07-01T06:46:55.000+0000",
        "updatedTime": "2019-07-01T06:47:02.000+0000"
      },
      {
        "id": "2",
        "productId": "smoke-100021",
        "url": "http://www.life-runner.com/product/smoke/img2.png",
        "sort": 1,
        "isMain": 0,
        "createdTime": "2019-07-01T06:46:55.000+0000",
        "updatedTime": "2019-07-01T06:47:02.000+0000"
      },
      {
        "id": "3",
        "productId": "smoke-100021",
        "url": "http://www.life-runner.com/product/smoke/img3.png",
        "sort": 2,
        "isMain": 0,
        "createdTime": "2019-07-01T06:46:55.000+0000",
        "updatedTime": "2019-07-01T06:47:02.000+0000"
      }
    ],
    "productsSpecList": [
      {
        "id": "1",
        "productId": "smoke-100021",
        "name": "中華",
        "stock": 2276,
        "discounts": 1.00,
        "priceDiscount": 7000,
        "priceNormal": 7000,
        "createdTime": "2019-07-01T06:54:20.000+0000",
        "updatedTime": "2019-07-01T06:54:28.000+0000"
      },
    ],
    "productsParam": {
      "id": "1",
      "productId": "smoke-100021",
      "producPlace": "中國",
      "footPeriod": "760天",
      "brand": "中華",
      "factoryName": "中華",
      "factoryAddress": "陝西",
      "packagingMethod": "盒裝",
      "weight": "100g",
      "storageMethod": "常溫",
      "eatMethod": "",
      "createdTime": "2019-05-01T09:38:30.000+0000",
      "updatedTime": "2019-05-01T09:38:34.000+0000"
    }
  },
  "ok": true
}

商品評價

在文章一開始我們就看過jd詳情頁面,有一個詳情頁簽,我們來看一下:

它這個實現比較複雜,我們只實現相對重要的幾個就可以了。

開發梳理

針對上圖中紅色方框圈住的內容,分別有:

  • 評價總數
  • 好評度(根據好評總數,中評總數,差評總數計算得出)
  • 評價等級
  • 以及用戶信息加密展示
  • 評價內容

我們來實現上述分析的相對必要的一些內容。

編碼實現

查詢評價

根據我們需要的信息,我們需要從用戶表、商品表以及評價表中來聯合查詢數據,很明顯單表通用mapper無法實現,因此我們先來實現自定義查詢mapper,當然數據的傳輸對象是我們需要先來定義的。

Response DTO實現

創建com.liferunner.dto.ProductCommentDTO.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProductCommentDTO {
    //評價等級
    private Integer commentLevel;
    //規格名稱
    private String specName;
    //評價內容
    private String content;
    //評價時間
    private Date createdTime;
    //用戶頭像
    private String userFace;
    //用戶昵稱
    private String nickname;
}

Custom Mapper實現

com.liferunner.custom.ProductCustomMapper中添加查詢接口方法:

    /***
     * 根據商品id 和 評價等級查詢評價信息
     * <code>
     *         Map<String, Object> paramMap = new HashMap<>();
     *         paramMap.put("productId", pid);
     *         paramMap.put("commentLevel", level);
     *</code>
     * @param paramMap
     * @return java.util.List<com.liferunner.dto.ProductCommentDTO>
     * @throws
     */
    List<ProductCommentDTO> getProductCommentList(@Param("paramMap") Map<String, Object> paramMap);

mapper/custom/ProductCustomMapper.xml中實現該接口方法的SQL:

    <select id="getProductCommentList" resultType="com.liferunner.dto.ProductCommentDTO" parameterType="Map">
        SELECT
        pc.comment_level as commentLevel,
        pc.spec_name as specName,
        pc.content as content,
        pc.created_time as createdTime,
        u.face as userFace,
        u.nickname as nickname
        FROM items_comments pc
        LEFT JOIN users u
        ON pc.user_id = u.id
        WHERE pc.item_id = #{paramMap.productId}
        <if test="paramMap.commentLevel != null and paramMap.commentLevel != ''">
            AND pc.comment_level = #{paramMap.commentLevel}
        </if>
    </select>

如果沒有傳遞評價級別的話,默認查詢全部評價信息。

Service 實現

com.liferunner.service.IProductService中添加查詢接口方法:

    /**
     * 查詢商品評價
     *
     * @param pid        商品id
     * @param level      評價級別
     * @param pageNumber 當前頁碼
     * @param pageSize   每頁展示多少條數據
     * @return 通用分頁結果視圖
     */
    CommonPagedResult getProductComments(String pid, Integer level, Integer pageNumber, Integer pageSize);

com.liferunner.service.impl.ProductServiceImpl實現該方法:

    @Override
    public CommonPagedResult getProductComments(String pid, Integer level, Integer pageNumber, Integer pageSize) {
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("productId", pid);
        paramMap.put("commentLevel", level);
        // mybatis-pagehelper
        PageHelper.startPage(pageNumber, pageSize);
        val productCommentList = this.productCustomMapper.getProductCommentList(paramMap);
        for (ProductCommentDTO item : productCommentList) {
            item.setNickname(SecurityTools.HiddenPartString4SecurityDisplay(item.getNickname()));
        }
        // 獲取mybatis插件中獲取到信息
        PageInfo<?> pageInfo = new PageInfo<>(productCommentList);
        // 封裝為返回到前端分頁組件可識別的視圖
        val commonPagedResult = CommonPagedResult.builder()
                .pageNumber(pageNumber)
                .rows(productCommentList)
                .totalPage(pageInfo.getPages())
                .records(pageInfo.getTotal())
                .build();
        return commonPagedResult;
    }

因為評價過多會使用到分頁,這裏使用通用分頁返回結果,關於分頁,可查看。

Controller實現

com.liferunner.api.controller.ProductController中添加對外查詢接口:

    @GetMapping("/comments")
    @ApiOperation(value = "查詢商品評價", notes = "根據商品id查詢商品評價")
    public JsonResponse getProductComment(
        @ApiParam(name = "pid", value = "商品id", required = true)
        @RequestParam String pid,
        @ApiParam(name = "level", value = "評價級別", required = false, example = "0")
        @RequestParam Integer level,
        @ApiParam(name = "pageNumber", value = "當前頁碼", required = false, example = "1")
        @RequestParam Integer pageNumber,
        @ApiParam(name = "pageSize", value = "每頁展示記錄數", required = false, example = "10")
        @RequestParam Integer pageSize
    ) {
        if (StringUtils.isBlank(pid)) {
            return JsonResponse.errorMsg("商品id不能為空!");
        }
        if (null == pageNumber || 0 == pageNumber) {
            pageNumber = DEFAULT_PAGE_NUMBER;
        }
        if (null == pageSize || 0 == pageSize) {
            pageSize = DEFAULT_PAGE_SIZE;
        }
        log.info("============查詢商品評價:{}==============", pid);

        val productComments = this.productService.getProductComments(pid, level, pageNumber, pageSize);
        return JsonResponse.ok(productComments);
    }

FBI WARNING:

@ApiParam(name = “level”, value = “評價級別”, required = false, example = “0”)
@RequestParam Integer level
關於ApiParam參數,如果接收參數為非字符串類型,一定要定義example為對應類型的示例值,否則Swagger在訪問過程中會報example轉換錯誤,因為example缺省為””空字符串,會轉換失敗。例如我們刪除掉level這個字段中的example=”0“,如下為錯誤信息(但是並不影響程序使用。)

2019-11-23 15:51:45 WARN  AbstractSerializableParameter:421 - Illegal DefaultValue null for parameter type integer
java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Long.parseLong(Long.java:601)
    at java.lang.Long.valueOf(Long.java:803)
    at io.swagger.models.parameters.AbstractSerializableParameter.getExample(AbstractSerializableParameter.java:412)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:688)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:721)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:166)
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)

Test API

福利講解

添加Propagation.SUPPORTS和不加的區別

有心的小夥伴肯定又注意到了,在Service中處理查詢時,我一部分使用了@Transactional(propagation = Propagation.SUPPORTS),一部分查詢又沒有添加事務,那麼這兩種方式有什麼不一樣呢?接下來,我們來揭開神秘的面紗。

  • Propagation.SUPPORTS

      /**
       * Support a current transaction, execute non-transactionally if none exists.
       * Analogous to EJB transaction attribute of the same name.
       * <p>Note: For transaction managers with transaction synchronization,
       * {@code SUPPORTS} is slightly different from no transaction at all,
       * as it defines a transaction scope that synchronization will apply for.
       * As a consequence, the same resources (JDBC Connection, Hibernate Session, etc)
       * will be shared for the entire specified scope. Note that this depends on
       * the actual synchronization configuration of the transaction manager.
       * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
       */
      SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

    主要關注Support a current transaction, execute non-transactionally if none exists.從字面意思來看,就是如果當前環境有事務,我就加入到當前事務;如果沒有事務,我就以非事務的方式執行。從這方面來看,貌似我們加不加這一行其實都沒啥差別。

    划重點:NOTE,對於一個帶有事務同步的管理器來說,這裡有一丟丟的小區別啦。(所以大家在讀註釋的時候,一定要看這個Note.往往這裏面會有好東西給我們,就相當於我們的大喇叭!)

    這個同步事務管理器定義了一個事務同步的一個範圍,如果加了這個註解,那麼就等同於我讓你來管我啦,你裏面的資源我想用就可以用(JDBC Connection, Hibernate Session).

結論1

SUPPORTS 標註的方法可以獲取和當前事務環境一致的 Connection 或 Session,不使用的話一定是一個新的連接;
再注意下面又一個NOTE,即便上面的配置加入了,但是事務管理器的實際同步配置會影響到真實的執行到底是否會用你。看它的說明:@see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization.

  /**
   * Set when this transaction manager should activate the thread-bound
   * transaction synchronization support. Default is "always".
   * <p>Note that transaction synchronization isn't supported for
   * multiple concurrent transactions by different transaction managers.
   * Only one transaction manager is allowed to activate it at any time.
   * @see #SYNCHRONIZATION_ALWAYS
   * @see #SYNCHRONIZATION_ON_ACTUAL_TRANSACTION
   * @see #SYNCHRONIZATION_NEVER
   * @see TransactionSynchronizationManager
   * @see TransactionSynchronization
   */
  public final void setTransactionSynchronization(int transactionSynchronization) {
      this.transactionSynchronization = transactionSynchronization;
  }

描述信息只是說在同一個事務管理器才能起作用,並沒有什麼實際意義,我們來看一下TransactionSynchronization具體的內容:

package org.springframework.transaction.support;

import java.io.Flushable;

public interface TransactionSynchronization extends Flushable {

  /** Completion status in case of proper commit. */
  int STATUS_COMMITTED = 0;

  /** Completion status in case of proper rollback. */
  int STATUS_ROLLED_BACK = 1;

  /** Completion status in case of heuristic mixed completion or system errors. */
  int STATUS_UNKNOWN = 2;

  /**
   * Suspend this synchronization.
   * Supposed to unbind resources from TransactionSynchronizationManager if managing any.
   * @see TransactionSynchronizationManager#unbindResource
   */
  default void suspend() {
  }

  /**
   * Resume this synchronization.
   * Supposed to rebind resources to TransactionSynchronizationManager if managing any.
   * @see TransactionSynchronizationManager#bindResource
   */
  default void resume() {
  }

  /**
   * Flush the underlying session to the datastore, if applicable:
   * for example, a Hibernate/JPA session.
   * @see org.springframework.transaction.TransactionStatus#flush()
   */
  @Override
  default void flush() {
  }

  /**
   * ...
   */
  default void beforeCommit(boolean readOnly) {
  }

  /**
   * ...
   */
  default void beforeCompletion() {
  }

  /**
   * ...
   */
  default void afterCommit() {
  }

  /**
   * ...
   */
  default void afterCompletion(int status) {
  }
}

事務管理器可以通過org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization(int)來對當前事務進行行為干預,比如將它設置為1,可以執行事務回調,設置為2,表示出錯了,但是如果沒有加入PROPAGATION.SUPPORTS註解的話,即便你在當前事務中,你也不能對我進行操作和變更。

結論2

添加PROPAGATION.SUPPORTS之後,當前查詢中可以對當前的事務進行設置回調動作,不添加就不行。

源碼下載

下節預告

下一節我們將繼續開發商品詳情展示以及商品評價業務,在過程中使用到的任何開發組件,我都會通過專門的一節來進行介紹的,兄弟們末慌!

gogogo!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Docker從入門到掉坑(三):容器太多,操作好麻煩

前邊的兩篇文章裏面,我們講解了基於docker來部署基礎的SpringBoot容器,如果閱讀本文之前沒有相關基礎的話,可以回看之前的教程。

不知道大家在初次使用docker的時候是否有遇到這種場景,每次部署微服務都是需要執行docker run xxx,docker kill xxx 等命令來操作容器。假設說一個系統中依賴了多個docker容器,那麼對於每個docker容器的部署豈不是都需要手動編寫命令來啟動和關閉,這樣做就會增加運維人員的開發工作量,同時也容易出錯。

Docker Compose 編排技術

在前邊的文章中,我們講解了Docker容器化技術的發展,但是隨着我們的Docker越來越多的時候,對於容器的管理也是特別麻煩,因此Docker Compose技術也就誕生了。

Docker Compose技術是通過一份文件來定義和運行一系列複雜應用的Docker工具,通過Docker-compose文件來啟動多個容器,網上有很多關於Docker-compose的實戰案例,但是都會有些細節地方有所遺漏,所以下邊我將通過一個簡單的案例一步步地帶各位從淺入深地對Docker-compose進行學習。

基於Docker Compose來進行對SpringBoot微服務應用的打包集成

我們還是按照老樣子來構建一套基礎的SpringBoot微服務項目,首先我們來看看基礎版本的項目結構:

首先是我們pom文件的配置內容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.sise.idea</groupId>
    <artifactId>springboot-docker</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-boot-docker</name>
    <url>http://maven.apache.org</url>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>springboot-docker</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

 

然後是java程序的內容代碼,這裏面有常規的controller,application類,代碼如下所示:

啟動類Application

package com.sise.docker;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author idea
 * @data 2019/11/20
 */
@SpringBootApplication
public class Application {

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

 

控制器 DockerController

package com.sise.docker.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author idea
 * @data 2019/11/20
 */
@RestController
@RequestMapping(value = "/docker")
public class DockerController {


    @GetMapping(value = "/test")
    public String test(){
        System.out.println("=========docker test=========");
        return "this is docker test";
    }
}

 

yml配置文件:

server:
  port: 7089

 

接下來便是docker-compose打包時候要用到的配置文件了。這裏採用的方式通常都是針對必要的docker容器編寫一份dockerfile,然後統一由Docker Compose進行打包管理,假設我們的微服務中需要引用到了MySQL,MongoDB等應用,那麼整體架構如下圖所示:

那麼我們先從簡單的單個容器入手,看看該如何對SpringBoot做Docker Compose的管理,下邊是一份打包SpringBoot進入Docker容器的Dockerfile文件:

#需要依賴的其他鏡像
FROM openjdk:8-jdk-alpine
# Spring Boot應用程序為Tomcat創建的默認工作目錄。作用是在你的主機”/var/lib/docker”目錄下創建一個臨時的文件,並且鏈接到容器中#的”/tmp”目錄。
VOLUME /tmp

#是指將原先的src文件 添加到我們需要打包的鏡像裏面
ADD target/springboot-docker.jar app.jar

#設置鏡像的時區,避免出現8小時的誤差
ENV TZ=Asia/Shanghai

#容器暴露的端口號 和SpringBoot的yml文件暴露的端口號要一致
EXPOSE 7089

#輸入的啟動參數內容 下邊這段內容相當於運行了java -Xms256m -Xmx512m -jar app.jar 
ENTRYPOINT ["java","-Xms256m","-Xmx512m","-jar","app.jar"]

 

接着便是加入docker-compose.yml文件的環節了,下邊是腳本的內容:

#docker引擎對應所支持的docker-compose文本格式
version: '3'
services:

  #服務的名稱
  springboot-docker:
    build:
      context: .
      # 構建這個容器時所需要使用的dockerfile文件
      dockerfile: springboot-dockerfile
    ports:
      # docker容器和宿主機之間的端口映射
      - "7089:7089"

 

docker-compose.ym配置文件有着特殊的規則,通常我們都是先定義version版本號,然後便是列舉一系列與容器相關的services內容。

接下來將這份docker服務進行打包,部署到相關的linux服務器上邊,這裏我採用的是一台阿里雲上邊購買的服務器來演示。

目前該文件還沒有進行打包處理,所以沒有target目錄,因此dockerfile文件構建的時候是不會成功的,因此需要先進行mvn的打包:

mvn package

 

接着便是進行Docker-Compose命令的輸入了:

[root@izwz9ic9ggky8kub9x1ptuz springboot-docker]# docker-compose up -d
Starting springboot-docker_springboot-docker_1 ... done
[root@izwz9ic9ggky8kub9x1ptuz springboot-docker]# 

 

你會發現這次輸入的命令和之前教程中提及的docker指令有些出入,變成了docker-compose 指令,這條指令是專門針對Docker compose文件所設計的,加入了一個-d的參數用於表示後台運行該容器。由於我們的docker-compose文件中知識編寫了對於SpringBoot容器的打包,因此啟動的時候只會显示一個docker容器。

為了驗證docker-compose指令是否生效,我們可以通過docker–compose ps命令來進行驗證。

這裏邊我們使用 docker logs [容器id] 指令可以進入容器查看日誌的打印情況:

 

docker logs ad83c82b014d

 

最後我們通過請求之前寫好的接口便會看到相關的響應:

 

基礎版本的SpringBoot+Docker compose案例已經搭建好了,還記得我在開頭畫的那張圖片嗎:

通常在實際開發中,我們所面對的docker容器並不是那麼的簡單,還有可能會依賴到多個容器,那麼這個時候該如何來編寫docker compose文件呢?

下邊我們對原先的SpringBoot項目增加對於MySQLMongoDB的依賴,為了方便下邊的場景模擬,這裏我們增加兩個實體類:

用戶類

package com.sise.docker.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author idea
 * @data 2019/11/23
 */
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {

    private Integer id;

    private String username;
}

 

汽車類:

package com.sise.docker.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;

/**
 * @author idea
 * @data 2019/11/23
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Car {

    @Id
    private Integer id;

    private String number;
}

 

增加對於mongodb,mysql的pom依賴內容

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.21</version>
        </dependency>

 

編寫相關的dao層:

package com.sise.docker.dao;

import com.sise.docker.domain.Car;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

/**
 * @author idea
 * @data 2019/11/23
 */
@Repository
public interface CarDao extends MongoRepository<Car, Integer> {
}
 

package com.sise.docker.dao;

import com.sise.docker.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * @author idea
 * @data 2019/11/23
 */
@Repository
public class UserDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;


    public void insert() {
        String time = String.valueOf(System.currentTimeMillis());
        String sql = "insert into t_user (username) values ('idea-" + time + "')";
        jdbcTemplate.update(sql);
        System.out.println("==========執行插入語句==========");
    }

    class UserMapper implements RowMapper<User> {

        @Override
        public User mapRow(ResultSet resultSet, int i) throws SQLException {
            User unitPO = new User();
            unitPO.setId(resultSet.getInt("id"));
            unitPO.setUsername(resultSet.getString("username"));
            return unitPO;
        }
    }
}

 

在控制器中添加相關的函數入口:

package com.sise.docker.controller;

import com.sise.docker.dao.CarDao;
import com.sise.docker.dao.UserDao;
import com.sise.docker.domain.Car;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Random;

/**
 * @author idea
 * @data 2019/11/20
 */
@RestController
@RequestMapping(value = "/docker")
public class DockerController {

    @Autowired
    private UserDao userDao;
    @Autowired
    private CarDao carDao;

    @GetMapping(value = "/insert-mongodb")
    public String insertMongoDB() {
        Car car = new Car();
        car.setId(new Random().nextInt(15000000));
        String number = String.valueOf(System.currentTimeMillis());
        car.setNumber(number);
        carDao.save(car);
        return "this is insert-mongodb";
    }

    @GetMapping(value = "/insert-mysql")
    public String insertMySQL() {
        userDao.insert();
        return "this is insert-mysql";
    }

    @GetMapping(value = "/test2")
    public String test() {
        System.out.println("=========docker test222=========");
        return "this is docker test";
    }
}

 

對原先的docker-compose.yml文件添加相應的內容,主要是增加對於mongodb和mysql的依賴模塊,

#docker引擎對應所支持的docker-compose文本格式
version: '3'
services:

  #服務的名稱
  springboot-docker:
    container_name: docker-springboot
    build:
      context: .
      dockerfile: springboot-dockerfile
    ports:
      - "7089:7089"

    depends_on:
      - mongodb


  mongodb:
    #容器的名稱
    container_name: docker-mongodb
    image: daocloud.io/library/mongo:latest
    ports:
      - "27017:27017"

  mysql:
    #鏡像的版本
    image: mysql:5.7
    container_name: docker-mysql
    ports:
      - 3309:3306
    environment:
       MYSQL_DATABASE: test
       MYSQL_ROOT_PASSWORD: root
       MYSQL_ROOT_USER: root
       MYSQL_ROOT_HOST: '%'

 

這裏頭我嘗試將application.yml文件通過不同的profile來進行區分:

應上篇文章中有讀者問到,不同環境不同配置的指定問題,這裡有一種思路,springboot依舊保持原有的按照profile來識別不同環境的配置,具體打包之後讀取的配置,可以通過springboot-dockerfile這份文件的ENTRYPOINT 參數來指定,例如下邊這種格式:

FROM openjdk:8-jdk-alpine

VOLUME /tmp

ADD target/springboot-docker.jar springboot-docker.jar

#設置鏡像的時區,避免出現8小時的誤差
ENV TZ=Asia/Shanghai

EXPOSE 7089
#這裏可以通過-D參數在對jar打包運行的時候指定需要讀取的配置問題
ENTRYPOINT ["java","-Xms256m","-Xmx512m","-Dspring.profiles.active=prod","-jar","springboot-docker.jar"]

 

最後便是我們的yml配置文件內容,由於配置類docker容器的依賴,所以這裏面對於yml的寫法不再是通過ip來訪問相應的數據庫了,而是需要通過service-name的映射來達成目標。

application-prod.yml

server:
  port: 7089

spring:
    data:
      mongodb:
        uri: mongodb://mongodb:27017
        database: test

    datasource:
             driver-class-name: com.mysql.jdbc.Driver
             url: jdbc:mysql://mysql:3306/test?useUnicode=true&amp;characterEncoding=UTF-8
             username: root
             password: root

 

當相關的代碼和文件都整理好了之後,將這份代碼發送到服務器上進行打包。

mvn package

 

接着我們便可以進行docker-compose的啟動了。

這裡有個小坑需要注意一下,由於之前我們已經對單獨的springboot容器進行過打包了,所以在執行docker-compose up指令的時候會優先使用已有的容器,而不是重新創建容器。

這個時候需要先將原先的image鏡像進行手動刪除,再打包操作:

[root@izwz9ic9ggky8kub9x1ptuz springboot-docker]# docker images
REPOSITORY                                           TAG                 IMAGE ID            CREATED             SIZE
springboot-docker                  latest              86f32bd9257f        4 hours ago         128MB
<none>                                               <none>              411616c3d7f7        2 days ago          679MB
<none>                                               <none>              77044e3ad9c2        2 days ago          679MB
<none>                                               <none>              5d9328dd1aca        2 days ago          679MB
springbootmongodocker_springappserver                latest              36237acf08e1        3 days ago          695MB

 

刪除鏡像的命令:

docker rmi 【鏡像id】

 

此時再重新進行docker-compose指令的打包操作即可:

 

docker-compose up

 

啟動之後,可以通過docker-compose自帶的一些指令來進行操作,常用的一些指令我都歸納在了下邊:

 

docker-compose [Command]

Commands:
  build              構建或重建服務
  bundle             從compose配置文件中產生一個docker綁定
  config             驗證並查看compose配置文件
  create             創建服務
  down               停止並移除容器、網絡、鏡像和數據卷
  events             從容器中接收實時的事件
  exec               在一個運行中的容器上執行一個命令
  help               獲取命令的幫助信息
  images             列出所有鏡像
  kill               通過發送SIGKILL信號來停止指定服務的容器
  logs               從容器中查看服務日誌輸出
  pause              暫停服務
  port               打印綁定的公共端口
  ps                 列出所有運行中的容器
  pull               拉取並下載指定服務鏡像
  push               Push service images
  restart            重啟YAML文件中定義的服務
  rm                 刪除指定已經停止服務的容器
  run                在一個服務上執行一條命令
  scale              設置指定服務運行容器的個數
  start              在容器中啟動指定服務
  stop               停止已運行的服務
  top                显示各個服務容器內運行的進程
  unpause            恢復容器服務
  up                 創建並啟動容器
  version            显示Docker-Compose版本信息

 

最後對相應的接口做檢測:

 

相關的完整代碼我已經上傳到了gitee地址,如果有需要的朋友可以前往進行下載。

代碼地址:https://gitee.com/IdeaHome_admin/wfw

 

實踐完畢之後,你可能會覺得有了docker-compose之後,對於多個docker容器來進行管理顯得就特別輕鬆了。

 

但是往往現實中並沒有這麼簡單,docker-compose存在着一個弊端,那就是不能做跨機器之間的docker容器進行管理

 

因此隨者技術的發展,後邊也慢慢出現了一種叫做Kubernetes的技術。Kubernetes(俗稱k8s)是一個開源的,用於管理雲平台中多個主機上的容器化的應用,Kubernetes的目標是讓部署容器化的應用簡單並且高效(powerful),Kubernetes提供了應用部署,規劃,更新,維護的一種機制。

 

Kubernetes這類技術對於小白來說入門的難度較高,後邊可能會抽空專門來寫一篇適合小白閱讀的k8s入門文章。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

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

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

高德服務單元化方案和架構實踐

導讀:本文主要介紹了高德在服務單元化建設方面的一些實踐經驗,服務單元化建設面臨很多共性問題,如請求路由、單元封閉、數據同步,有的有成熟方案可以借鑒和使用,但不同公司的業務不盡相同,要盡可能的結合業務特點,做相應的設計和處理。

一、為什麼要做單元化

  • 單機房資源瓶頸

隨着業務體量和服務用戶群體的增長,單機房或同城雙機房無法支持服務的持續擴容。

  • 服務異地容災

異地容災已經成為核心服務的標配,有的服務雖然進行了多地多機房部署,但數據還是只在中心機房,實現真正意義上的異地多活,就需要對服務進行單元化改造。

二、高德單元化的特點

在做高德單元化項目時,我們首先要考慮的是結合高德的業務特點,看高德的單元化有什麼不一樣的訴求,這樣就清楚哪些經驗和方案是可以直接拿來用的,哪些又是需要我們去解決的。

高德業務和傳統的在線交易業務還是不太一樣,高德為用戶提供以導航為代表的出行服務,很多業務場景對服務的RT要求會很高,所以在做單元化方案時,盡可能減少對整體服務RT的影響就是我們需要重點考慮的問題,盡量做到數據離用戶近一些。轉換到單元化技術層面需要解決兩個問題:

1.用戶設備的單元接入需要盡可能的做到就近接入,用戶真實地理位置接近哪個單元就接入哪個單元,如華北用戶接入到張北,華南接入到深圳。

2.用戶的單元劃分最好能與就近接入的單元保持一致,減少單元間的跨單元路由。如用戶請求從深圳進來,用戶的單元劃分最好就在深圳單元,如果劃到張北單元就會造成跨單元路由。

另外一個區別就是高德很多業務是無須登錄的,所以我們的單元化方案除了用戶ID也要支持基於設備ID。

三、高德單元化實踐

服務的單元化架構改造需要一個至上而下的系統性設計,核心要解決請求路由、單元封閉、數據同步三方面問題。

請求路由:根據高德業務的特點,我們提供了取模路由和路由表路由兩種策略,目前上線應用使用較多的是路由表路由策略。

單元封閉:得益於集團的基礎設施建設,我們使用vipserver、hsf等服務治理能力保證服務同機房調用,從而實現單元封閉(hsf unit模式也是一種可行的方案,但個人認為同機房調用的架構和模式更簡潔且易於維護)。

數據同步:數據部分使用的是集團DB產品提供的DRC數據同步。

單元路由服務採用什麼樣的部署方案是我們另一個要面臨的問題,考慮過以下三種方案:

第一種SDK的方式因為對業務的強侵入性是首先被排除的,統一接入層進行代理和去中心化插件集成兩種方案各有利弊,但當時首批要接入單元化架構的服務很多都還沒有統一接入到gateway,所以基於現狀的考慮使用了去中心化插件集成的方式,通過在應用的nginx集成UnitRouter。

服務單元化架構

目前高德賬號,雲同步、用戶評論系統都完成了單元化改造,採用三地四機房部署,寫入量較高的雲同步服務,單元寫高峰能達到數w+QPS (存儲是mongodb集群)。

以賬號系統為例介紹下高德單元化應用的整體架構。

賬號系統服務是三地四機房部署,數據分別存儲在tair為代表的緩存和XDB里,數據存儲三地集群部署、全量同步。賬號系統服務器的Tengine上安裝UntiRouter,它請求的負責單元識別和路由,用戶單元劃分是通過記錄用戶與單元關係的路由表來控制。

PS:因歷史原因緩存使用了tair和自建的uredis(在redis基礎上添加了基於log的數據同步功能),目前已經在逐步統一到tair。數據同步依賴tair和alisql的數據同步方案,以及自建的uredis數據同步能力。

就近接入實現方案

為滿足高德業務低延時要求,就要想辦法做到數據(單元)離用戶更近,其中有兩個關鍵鏈路,一個是通過aserver接入的外網連接,另一個是服務內部路由(盡可能不產生跨單元路由)。

措施1:客戶端的外網接入通過aserver上的配置,將不同地理區域(七個大區)的設備劃分到對應近的單元,如華北用戶接入張北單元。

措施2:通過記錄用戶和單元關係的路由表來劃分用戶所屬單元,這個關係是通過系統日誌分析出來的,用戶經常從哪個單元入口進來,就會把用戶劃分到哪個單元,從而保證請求入口和單元劃分的相對一致,從而減少跨單元路由。

所以,在最終的單元路由實現上我們提供了傳統的取模路由,和為降延時而設計的基於路由表路由兩種策略。同時,為解無須登錄的業務場景問題,上述兩種策略除了支持用戶ID,我們同時也支持設備ID。

路由表設計

路由表分為兩部分,一個是用戶-分組的關係映射表,另一個是分組-單元的關係映射表。在使用時,通過路由表查對應的分組,再通過分組看用戶所屬單元。分組對應中國大陸的七個大區。

先看“用戶-(大區)分組”:

路由表是定期通過系統日誌分析出來的,看用戶最近IP屬於哪個大區就劃分進哪個分組,同時也對應上了具體單元。當一個北京的用戶長期去了深圳,因IP的變化路由表更新后將划進新大區分組,從而完成用戶從張北單元到深圳單元的遷移。

再看“分組-單元”:

分組與單元的映射有一個默認關係,這是按地理就近來配置的,比如華南對應深圳。除了默認的映射關係,還有幾個用於切流預案的關係映射。

老用戶可以通過路由表來查找單元,新用戶怎麼辦?對於新用戶的處理我們會降級成取模的策略進行單元路由,直至下次路由表的更新。所以整體上看新用戶跨單元路由比例肯定是比老用戶大的多,但因為新用戶是一個相對穩定的增量,所以整體比例在可接受範圍內。

路由計算

有了路由表,接下來就要解工程化應用的問題,性能、空間、靈活性和準確率,以及對服務穩定性的影響這幾個方面是要進行綜合考慮的,首先考慮外部存儲會增加服務的穩定性風險,後面我們在BloomFilter 、BitMap和MapDB多種方案中選擇BloomFilter,萬分之幾的誤命中率導致的跨單元路由在業務可接受範圍內。

通過日誌分析出用戶所屬大區后,我們將不同分組做成多個布隆過濾器,計算時逐層過濾。這個計算有兩種特殊情況:

1) 因為BloomFilter存在誤算率,有可能存在一種情況,華南分組的用戶被計算到華北了,這種情況比例在萬分之3 (生成BloomFilter時可調整),它對業務上沒有什麼影響,這類用戶相當於被劃分到一個非所在大區的分組裡,但這個關係是穩定的,不會影響到業務,只是存在跨單元路由,是可接受的。

2) 新用戶不在分組信息里,所以經過逐層的計算也沒有匹配到對應大區分組,此時會使用取模進行模除分組的計算。

如果業務使用的是取模路由而非路由表路由策略,則直接根據tid或uid計算對應的模除分組,原理簡單不詳表了。

單元切流

在發生單元故障進行切流時,主要分為四步驟

打開單元禁寫 (跨單元寫不敏感業務可以不配置)

檢查業務延時

切換預案

解除單元禁寫

PS:更新路由表時,也需要上述操作,只是第3步的切換預案變成切換新版本路由表;單元禁寫主要是了等待數據同步,避免數據不一致導致的業務問題。

核心指標

單元計算耗時1~2ms

跨單元路由比例底於5%

除了性能外,因就近接入的訴求,跨單元路由比例也是我們比較關心的重要指標。從線上觀察看,路由表策略單元計算基本上在1、2ms內完成,跨單元路由比例3%左右,整體底於5%。

四、後續優化

統一接入集成單元化能力

目前大部分服務都接入了統一接入網關服務,在網關集成單元化能力將大大減少服務單元化部署的成本,通過簡單的配置就可以實現單元路由,服務可將更多的精力放在業務的單元封閉和數據同步上。

分組機制的優化

按大區分組存在三個問題:

通過IP計算大區有一定的誤算率,會導致部分用戶劃分錯誤分組。

分組粒度太大,單元切流時流量不好分配。舉例,假如華東是我們用戶集中的大區,切流時把這個分組切到任意一個指定單元,都會造成單元服務壓力過大。

計算次數多,分多少個大區,理論最大計算次數是有多少次,最後採取取模策略。

針對上述幾個問題我們計劃對分組機製做如下改進

通過用戶進入單元的記錄來確認用戶所屬單元,而非根據用戶IP所在大區來判斷,解上述問題1。

每個單元劃分4個虛擬分組,支持更細粒度單元切流,解上述問題2。

用戶確實單元后,通過取模來劃分到不同的虛擬分組。每個單元只要一次計算就能完成,新用戶只需經過3次計算,解上述問題3。

熱更時的雙表計算

與取模路由策略不同,路由表策略為了把跨單元路由控制在一個較好的水平需要定期更新,目前更新時需要一個短暫的單元禁寫,這對於很多業務來說是不太能接受的。

為優化這個問題,系統將在路由表更新時做雙(路由)表計算,即將新老路由表同時加載進內存,更新時不再對業務做完全的禁寫,我們會分別計算當前用戶(或設備)在新老路由表的單元結果,如果單元一致,則說明路由表的更新沒有導致該用戶(或設備)變更單元,所以請求會被放行,相反如果計算結果是不同單元,說明發生了單元變更,該請求會被攔截,直至到達新路由表的一個完全起用時間。

優化前服務會完全禁寫比如10秒(時間取決於數據同步時間),優化後會變成觸髮禁寫的是這10秒內路由發生變更的用戶,這將大大減少對業務的影響。

服務端數據驅動的單元化場景

前面提到高德在路由策略上結合業務的特別設計,但整體單元劃分還是以用戶(或設備)為維度來進行的,但高德業務還有一個大的場景是我們未來要面對和解決的,就是以數據維度驅動的單元設計,基於終端的服務路由會變成基於數據域的服務路由。

高德很多服務是以服務數據為核心的,像地圖數據等它並非由用戶直接產生。業務的發展數據存儲也將不斷增加,包括5G和自動駕駛,對應數據的爆髮式增長單點全量存儲並不實現,以服務端數據驅動的服務單元化設計,是我們接下來要考慮的重要應用場景。

寫在最後

不同的業務場景對單元化會有不同的訴求,我們提供不同的策略和能力供業務進行選擇,對於多數據服務我們建議使用業務取模路由,簡單且易於維護;對於RT敏感的服務使用路由表的策略來盡可能的降低服務響應時長的影響。另外,要注意的是強依賴性的服務要採用相同的路由策略。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

程序員修神之路–kubernetes是微服務發展的必然產物



菜菜哥,我昨天又請假出去面試了


戰況如何呀?


多數面試題回答的還行,但是最後讓我介紹微服務和kubernetes的時候,掛了


話說微服務和kubernetes內容確實挺多的


那你給我大體介紹一下唄


可以呀,不過要請和coffee哦


◆◆
kubernetes介紹
◆◆

在很多項目的發展初期,都是小型或者大型的單體項目,部署在單台或者多台服務器上,以單個進程的方式來運行。這些項目隨着需求的遞增,發布周期逐漸增長,迭代速度明顯下降。傳統的發布方式是:開發人員將項目打包發給運維人員,運維人員進行部署、資源分配等操作。

隨着軟件行業架構方式的改變,這些大型的單體應用按照業務或者其他維度逐漸被分解為可獨立運行的組件,我們稱之為微服務。微服務彼此之間被獨立開發、部署、升級、擴容,真正實現了大型應用的解耦工作。關於微服務的介紹,大家可以去擼一下菜菜之前的文章:

https://mp.weixin.qq.com/s/b7Bd8giwWVNF1CtkaDaVpw

https://mp.weixin.qq.com/s/BixgyGFrlwZ7wpgDdrmU_g

軟件開發行業就是這樣奇葩,每一個問題被解決之後總是伴隨着另外的問題出現,就像程序員改bug,為什麼總有改不完的bug,真的很令人頭大!!!

微服務雖然解決了一些問題,但是隨着微服務數量的增多,配置、管理、擴容、高可用等要求的實現變的越來越困難,包括運維團隊如何更好的利用硬件資源並降低服務器成本,以及部署的自動化和故障處理等問題變得原來越棘手。

以上問題正是kubernetes要解決並且擅長的領域,它可以讓開發者自主部署應用,自主控制迭代的頻率,完全解放運維團隊。而運維團隊的工作重心從以往的服務器資源管理轉移到了kubernetes的資源管理。kubernetes最厲害之處是對硬件基礎設施進行了封裝和抽象,使得開發人員完全不用去了解硬件的基礎原理,不用去關注底層服務器。kubernetes內部把設置的服務器抽象為資源池,在部署應用的時候,它會自動給應用分配合適合理的服務器資源,並且能夠保證這些應用能正常的和其他應用進行通信。一個kubernetes集群的大體結構如下:

那kubernetes有哪些具體優勢呢?能說下不?


再加一杯coffee?


◆◆
kubernetes優勢
◆◆

微服務雖好,但是數量多了就會有量帶來的問題。隨着系統組件的不斷增長,這些組件的管理問題逐漸浮出水面。首先我們要明白kubernetes是一個軟件系統,它依賴於linux容器的特性來管理組件(kubernetes和容器並非一個概念,請不要混淆)。通過kubernetes部署應用程序時候,你的集群無論包含多少個節點,對於kubernetes來說不會有什麼差異,這完全得益於它對底層基礎設置的抽象,使得數個節點運行的時候表現的好像一個節點一樣。

自動擴容

在kubernetes系統中,它可以對每個應用進行實時的監控,並能根據策略來應對突發的流量做出反應。例如:在流量高峰期間,kubernetes可以根據各個節點的資源利用情況,進行自動的增加節點或者減少節點操作,這在以前的傳統應用部署方式中是不容易做到的。

簡化部署流程

以往的傳統應用發布的時候,需要開發人員把項目打包,並檢查項目的配置文件是否正確,然後發給運維人員,運維人員然後把線上的應用版本備份,然後停止服務進行更新。在kubernetes中,我們多數情況下只需要一條指令或者點擊一個按鈕,就可以把應用升級到最新版本,而且升級的過程中還可做做到不間斷服務。當然整個的流程還涉及到容器的操作,本次這裏不再做過多介紹。

但是這裡有一個意外情況,如果kubernetes集群中存在不同架構CPU的服務器,而你的應用程序是針對特定CPU架構的軟件,可能需要在kubernetes中指定節點去運行你的應用程

提高服務器資源的利用率

傳統應用部署的時候,多數情況下總會把資源留有一定的比例來作為資源的緩衝,來應對流量的峰值,很少有人把單個服務器資源利用率提高到90%以上,從服務器故障的概率來說,服務器資源使用率在90%要比50%高很多,而且服務器一旦出現故障,都是運維人員來解決問題和背鍋,所以傳統的物理機或者虛擬機部署應用的方式,硬件的資源利用率相比較來說是比較低的。

而kubernetes對集群的管理由於抽象了底層硬件設施,所以已經將應用程序和基礎設施分離開來。當你告訴kubernetes運行你 應用程序時,它會根據程序的資源需求和集群內每隔節點的可用資源情況選擇合適的節點來運行。而且通過容器的技術,可以讓應用程序在任何時間遷移到集群中的任何機器上。而對於服務器選擇的最優的組合,kubernetes比人工做的更好,它會根據集群中每台服務器的負載情況來把硬件利用率提高到最高。

自動修復

在傳統的應用架構中,如果一台服務器發生故障,那麼這台服務器上的應用將會全部down掉,多數情況下需要運維人員去處理,這也是為什麼運維人員需要7*24小時隨時待命的一個重要原因。相信你也曾看到過因為半夜故障運維人員罵娘的情景。在kubernetes中,它監視並管理着所有的節點和應用,在節點出現故障的時候,kubernetes可以自動將該節點上的應用遷移到其他健康節點,並將故障節點在資源池中排除。如果你的kubernetes集群基礎設施有足夠的備用資源來支撐系統的正常運行,運維人員完全可以拖延到正常的工作時間再處理故障,讓程序員和運維人員過一下965的工作節奏。

這點有點像Actor模型的設計理論,提倡的是任其崩潰原理。

一致的運行環境

無論你是開發還是運維人員,在傳統的部署方案中,總會有運行環境差異性的煩惱,這樣的差異性大到每個服務器的差異,小到開發環境、仿真環境、生產環境,而且每個環境的服務器都會隨着時間的推移而變化。我相信你一定遇到過開發環境程序運行正常,生產環境卻異常的情況。這種差異性不僅僅是因為生產環境由運維團隊管理,開發環境由開發者管理,更重要的這兩組人對系統的要求是不同的,運維團隊會對線上生產環境定時的打補丁,做安全監測等操作,而開發者可能根本就不會弔這些問題。除此之外,應用系統依賴的第三方庫可能在開發、仿真、生產環境中版本不同,這樣的問題反正我是遇到過。

而kubernetes採用的容器技術,在把應用打包的時候,運行環境也一起被打入包中,這就保證了相同版本的容器包(鏡像)在任何服務器上都有相同的運行環境

kubernetes原來有這麼優勢,那我得好好學學了


雖然kubernetes優勢很多,但是入門門檻比較高,而且在個別情況下反而不合適


kubernetes要求開發人員對容器技術和網絡知識有一定了解,所以是否採用kubernetes要根據團隊的綜合技能和項目斟酌使用,並不是所有項目採用kubernetes都有利

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

Rio手把手教學:如何打造容器化應用程序的一站式部署體驗

11月19日,業界應用最為廣泛的Kubernetes管理平台創建者Rancher Labs(以下簡稱Rancher)宣布Rio發布了beta版本,這是基於Kubernetes的應用程序部署引擎。它於今年5月份推出,現在最新的版本是v0.6.0。Rio結合了多種雲原生技術,從而簡化了將代碼從測試環境發布到生產環境的流程,同時保證了強大而安全的代碼體驗。

什麼是Rio?

下圖是Rio的架構:

Rio採用了諸如Kubernetes、knative、linkerd、cert-manager、buildkit以及gloo等技術,並將它們結合起來為用戶提供一個完整的應用程序部署環境。

Rio具有以下功能:

  1. 從源代碼構建代碼,並將其部署到Kubernetes集群

  2. 自動為應用程序創建DNS記錄,並使用Let’s Encrypt的TLS證書保護這些端點

  3. 基於QPS以及工作負載的指標自動擴縮容

  4. 支持金絲雀發布、藍綠髮布以及A/B部署

  5. 支持通過服務網格路由流量

  6. 支持縮容至零的serverless工作負載

  7. Git觸發的部署

Rancher的產品生態

Rio屬於Rancher整套產品生態的一部分,這些產品支持從操作系統到應用程序的應用程序部署和容器運維。當Rio和諸如Rancher 2.3、k3s和RKE等產品結合使用時,企業可以獲得完整的部署和管理應用程序及容器的體驗。

深入了解Rio

要了解Rio如何實現上述功能,我們來深入了解一些概念以及工作原理。

安裝Rio

前期準備

  • Kubernetes版本在1.15以上的Kubernetes集群

  • 為集群配置的kubeconfig(即上下文是你希望將Rio安裝到的集群)

  • 在$PATH中安裝的Rio CLI工具,可參閱以下鏈接,了解如何安裝CLI:
    https://github.com/rancher/rio/blob/master/README.md

安裝

使用安裝好的Rio CLI工具,調用rio install。你可能需要考慮以下情況:

ip-address:節點的IP地址的逗號分隔列表。你可以在以下情況使用:

  • 你不使用(或不能使用)layer-4的負載均衡器

  • 你的節點IP不是你希望流量到達的IP地址(例如,你使用有公共IP的EC2實例)

服 務

在Rio中,service是一個基本的執行單位。從Git倉庫或容器鏡像實例化之後,一個service由單個容器以及服務網格的關聯sidecar組成(默認啟用)。例如,運行使用Golang構建的一個簡單的“hello world”應用程序。

rio run https://github.com/ebauman/rio-demo

或者運行容器鏡像版本:

rio run ebauman/demo-rio:v1

還有其他選項也可以傳遞給rio run,如需要公開的任意端口(-p 80:8080/http),或者自動擴縮的配置(--scale 1-10)。你可以通過這一命令rio help run,查看所有可傳遞的選項。

想要查看你正在運行的服務,請執行rio ps

$ rio ps
NAME            IMAGE                               ENDPOINT
demo-service    default-demo-service-4dqdw:61825    https://demo-service...

每次你運行一個新的服務,Rio將會為這一服務生成一個全局性的端點:

$ rio endpoints
NAME           ENDPOINTS
demo-service   https://demo-service-default.op0kj0.on-rio.io:30282

請注意,此端點不包括版本——它指向由一個common name標識的服務,並且流量根據服務的權重進行路由。

自動DNS&TLS

默認情況下,所有Rio集群都將為自己創建一個on-rio.io主機名,並以隨機字符串開頭(如lkjsdf.on-rio.io)。該域名成為通配符域名,它的記錄解析到集群的網關。如果使用NodePort服務,則該網關可以是layer-4負載均衡器,或者是節點本身。

除了創建這個通配符域名,Rio還會使用Let’s Encrypt為這個域名生成一個通配符證書。這會允許自動加密任何HTTP工作負載,而無需用戶進行配置。要啟動此功能,請傳遞-p參數,將http指定為協議,例如:

rio run -p 80:8080/http ...

自動擴縮容

Rio可以根據每秒所查詢到的指標自動擴縮服務。為了啟用這一特性,傳遞--scale 1-10作為參數到rio run,例如:

rio run -p 80:8080/http -n demo-service --scale 1-10 ebauman/rio-demo:v1

執行這個命令將會構建ebauman/rio-demo並且部署它。如果我們使用一個工具來添加負載到端點,我們就能夠觀察到自動擴縮容。為了證明這一點,我們需要使用HTTP端點(而不是HTTPS),因為我們使用的工具不支持TLS:

$ rio inspect demo-service
<snipped>
endpoints:
- https://demo-service-v0-default.op0kj0.on-rio.io:30282
- http://demo-service-v0-default.op0kj0.on-rio.io:31976
<snipped>

rio inspect除了端點之外還會显示其他信息,但我們目前所需要的是端點信息。使用HTTP端點以及HTTP基準測試工具rakyll / hey,我們可以添加綜合負載:

hey -n 10000 http://demo-service-v0-default.op0kj0.on-rio.io:31976

這將會發送10000個請求到HTTP端點,Rio將會提高QPS並適當擴大規模,執行另一個rio ps將會展示已經擴大的規模:

$ rio ps
NAME            ...     SCALE       WEIGHT
demo-service    ...     2/5 (40%)   100%

分階段發布、金絲雀部署以及權重

注意

對於每個服務,都會創建一個全局端點,該端點將根據基礎服務的權重路由流量。

Rio可以先交付新的服務版本,然後再推廣到生產環境。分階段發布一個新的版本十分簡單:

rio stage --image ebauman/rio-demo:v2 demo-service v2

這一命令使用版本v2,分階段發布demo-service的新版本,並且使用容器鏡像ebauman/rio-demo:v2。我們通過執行rio ps這一命令,可以看到新階段的發布:

$ rio ps
NAME                IMAGE                   ENDPOINT                    WEIGHT
demo-service@v2     ebauman/rio-demo:v2     https://demo-service-v2...  0%
demo-service        ebauman/rio-demo:v1     https://demo-service-v0...  100%

請注意,新服務的端點具有v2的新增功能,因此即使權重設置為0%,訪問此端點仍將帶你進入服務的v2。這可以讓你能夠在向其發送流量之前驗證服務的運行情況。

說到發送流量:

$ rio weight demo-service@v2=5%
$ rio ps
NAME                IMAGE                   ENDPOINT                    WEIGHT
demo-service@v2     ebauman/rio-demo:v2     https://demo-service-v2...  5%
demo-service        ebauman/rio-demo:v1     https://demo-service-v0...  95%

使用rio weight命令,我們現在將發送我們5%的流量(從全局的服務端點)到新版本。當我們覺得demo-service的v2性能感到滿意之後,我們可以將其提升到100%:

$ rio promote --duration 60s demo-service@v2
demo-service@v2 promoted

超過60秒之後,我們的demo-service@v2服務將會逐漸提升到接收100%的流量。在這一過程中任意端點上,我們可以執行rio ps,並且查看進程:

$ rio ps
NAME                IMAGE                   ENDPOINT                    WEIGHT
demo-service@v2     ebauman/rio-demo:v2     https://demo-service-v2...  34%
demo-service        ebauman/rio-demo:v1     https://demo-service-v0...  66%

路由(Routing)

Rio可以根據主機名、路徑、方法、標頭和cookie的任意組合將流量路由到端點。Rio還支持鏡像流量、注入故障,配置retry邏輯和超時。

創建一個路由器

為了開始制定路由決策,我們必須首先創建一個路由器。路由器代表一個主機名和一組規則,這些規則確定發送到主機名的流量如何在Rio集群內進行路由。你想要要定義路由器,需要執行rio router add。例如,要創建一個在默認測試時接收流量並將其發送到demo-service的路由器,請使用以下命令:

rio route add testing to demo-service

這將創建以下路由器:

$ rio routers
NAME             URL                            OPTS    ACTION      TARGET
router/testing   https://testing-default.0pjk...        to          demo-service,port=80

發送到https://testing-default…的流量將通過端口80轉發到demo-service。

請注意,此處創建的路由為testing-default. 。Rio將始終使用命名空間資源,因此在這種情況下,主機名測試已在默認命名空間中進行了命名。要在其他命名空間中創建路由器,請將 -n <namespace>傳遞給rio命令:

rio -n <namespace> route add ...

基於路徑的路由

為了定義一個基於路徑的路由,當調用rio route add時,指定一個主機名加上一個路徑。這可以是新路由器,也可以是現有路由器。

$ rio route add testing/old to demo-service@v1

以上命令可以創建一個基於路徑的路由,它會在https://testing-default. /old接收流量,並且轉發流量到 demo-service@v1服務。

標頭和基於方法的路由

Rio支持基於HTTP標頭和HTTP verbs的值做出的路由策略。如果你想要創建基於特定標頭路由的規則,請在rio route add命令中指定標頭:

$ rio route add --header X-Header=SomeValue testing to demo-service

以上命令將創建一個路由規則,它可以使用一個X-Header的HTTP標頭和SomeValue的值將流量轉發到demo-service。類似地,你可以為HTTP方法定義規則:

$ rio route add --method POST testing to demo-service

故障注入

Rio路由有一項有趣的功能是能夠將故障注入響應中。通過定義故障路由規則,你可以設置具有指定延遲和HTTP代碼的失敗流量百分比:

$ rio route add --fault-httpcode 502 --fault-delay-milli-seconds 1000 --fault-percentage 75 testing to demo-service

其他路由選項

Rio支持按照權重分配流量、為失敗的請求重試邏輯、重定向到其他服務、定義超時以及添加重寫規則。要查看這些選項,請參閱以下鏈接:

https://github.com/rancher/rio

自動構建

將git倉庫傳遞給rio run將指示Rio在提交到受監控的branch(默認值:master)之後構建代碼。對於Github倉庫,你可以通過Github webhooks啟動此功能。對於任何其他git repo,或者你不想使用webhooks,Rio都會提供一項“gitwatcher”服務,該服務會定期檢查您的倉庫中是否有更改。

Rio還可以根據受監控的branch的拉取請求構建代碼。如果你想要進行配置,請將--build-pr傳遞到rio run。還有其他配置這一功能的選項,包括傳遞Dockerfile的名稱、自定義構建的鏡像名稱以及將鏡像推送到指定的鏡像倉庫。

堆棧和Riofile

Rio使用稱為Riofile的docker-compose-style manifest定義資源

configs:
  conf:
    index.html: |-
      <!DOCTYPE html>
      <html>
      <body>

      <h1>Hello World</h1>

      </body>
      </html>
services:
  nginx:
    image: nginx
    ports:
    - 80/http
    configs:
    - conf/index.html:/usr/share/nginx/html/index.html

Riofile定義了一個簡單的nginx Hello World網頁所有必要的組件。通過rio up部署它,會創建一個Stack(堆棧),它是Riofile定義的資源的集合。

Riofile具有許多功能,例如觀察Git庫中的更改以及使用Golang模板進行模板化。

其他Rio組件

Rio還有許多功能,例如configs、secrets以及基於角色訪問控制(RBAC)。詳情可參閱:

https://rio.io/

Rio可視化

Rio Dashboard

Rio的beta版本包括了一個全新的儀錶盤,使得Rio組件可視化。要訪問此儀錶盤,請執行命令:rio dashboard。在有GUI和默認瀏覽器的操作系統上,Rio將自動打開瀏覽器並加載儀錶盤。

你可以使用儀錶盤來創建和編輯堆棧、服務、路由等。此外,可以直接查看和編輯用於各種組件技術(Linkerd、gloo等)的對象,儘管不建議這樣做。儀錶盤目前處於開發的早期階段,因此某些功能的可視化(如自動縮放和服務網格)尚不可用。

Linkerd

作為Rio的默認服務網格,Linked附帶了一個儀錶盤作為產品的一部分。該儀錶盤可以通過執行rio linkerd來使用,它將代理本地本地主機流量到linkerd儀錶盤(不會在外部公開)。與Rio儀錶盤類似,有GUI和默認瀏覽器的操作系統上,Rio將自動打開瀏覽器並加載儀錶盤:

Linkerd儀錶盤显示了Rio集群的網格配置、流量和網格組件。Linkerd提供了Rio路由的某些功能組件,因此這些配置可能會显示在此儀錶盤上。還有一些工具可用於測試和調試網格配置和流量。

結 論

Rio為用戶提供許多功能,是一款強大的應用程序部署引擎。這些組件可以在部署應用程序時為開發人員提供強大的功能,使流程穩定而安全,同時輕鬆又有趣。在Rancher產品生態中,Rio提供了企業部署和管理應用程序和容器的強大功能。

如果你想了解Rio的更多信息,歡迎訪問Rio主頁或Github主頁:

https://rio.io

https://github.com/rancher/rio

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

024.掌握Pod-部署MongoDB

一 前期準備

1.1 前置條件


  • 集群部署:Kubernetes集群部署參考003——019。
  • glusterfs-Kubernetes部署:參考《附010.Kubernetes永久存儲之GlusterFS超融合部署》。

1.2 部署規劃


本實驗使用StatefulSet部署MongoDB集群,同時每個MongoDB實例使用glusterfs實現永久存儲。從而部署無單點故障、高可用、可動態擴展的MongoDB集群。

部署架構如下:

二 創建StatefulSet

2.1 創建storageclass存儲類型

  1 [root@k8smaster01 ~]# vi heketi-secret.yaml			#創建用於保存密碼的secret
  2 apiVersion: v1
  3 kind: Secret
  4 metadata:
  5   name: heketi-secret
  6   namespace: heketi
  7 data:
  8   # base64 encoded password. E.g.: echo -n "mypassword" | base64
  9   key: YWRtaW4xMjM=
 10 type: kubernetes.io/glusterfs


  1 [root@k8smaster01 heketi]# kubectl create -f heketi-secret.yaml	#創建heketi
  2 [root@k8smaster01 heketi]# kubectl get secrets -n heketi
  3 NAME                                 TYPE                                  DATA   AGE
  4 default-token-6n746                  kubernetes.io/service-account-token   3      144m
  5 heketi-config-secret                 Opaque                                3      142m
  6 heketi-secret                        kubernetes.io/glusterfs               1      3m1s
  7 heketi-service-account-token-ljlkb   kubernetes.io/service-account-token   3      143m
  8 [root@k8smaster01 ~]# mkdir mongo
  9 [root@k8smaster01 ~]# cd mongo


  1 [root@k8smaster01 heketi]# vi storageclass-fast.yaml
  2 apiVersion: storage.k8s.io/v1
  3 kind: StorageClass
  4 metadata:
  5   name: fast
  6 parameters:
  7   resturl: "http://10.254.82.26:8080"
  8   clusterid: "d96022e907f82045dcc426a752adc47c"
  9   restauthenabled: "true"
 10   restuser: "admin"
 11   secretName: "heketi-secret"
 12   secretNamespace: "default"
 13   volumetype: "replicate:3"
 14 provisioner: kubernetes.io/glusterfs
 15 reclaimPolicy: Delete
  1 [root@k8smaster01 heketi]# kubectl create -f storageclass-fast.yaml
  2 [root@k8smaster01 heketi]# kubectl get storageclasses/fast



2.2 授權ServiceAccount


本實驗2.4步驟需要使用mongo-sidecar的pod來配置管理mongo pod。

由於默認的service account僅僅只能獲取當前Pod自身的相關屬性,無法觀察到其他名稱空間Pod的相關屬性信息。如果想要擴展Pod,或者一個Pod需要用於管理其他Pod或者是其他資源對象,是無法通過自身的名稱空間的serviceaccount進行獲取其他Pod的相關屬性信息的,因此需要進行手動創建一個serviceaccount,並在創建Pod時進行定義。或者直接將默認的serviceaccount進行授權。

  1 [root@uk8s-m-01 mongo]# vi defaultaccout.yaml
  2 ---
  3 apiVersion: rbac.authorization.k8s.io/v1beta1
  4 kind: ClusterRoleBinding
  5 metadata:
  6   name: DDefault-Cluster-Admin
  7 subjects:
  8   - kind: ServiceAccount
  9     # Reference to upper's `metadata.name`
 10     name: default
 11     # Reference to upper's `metadata.namespace`
 12     namespace: default
 13 roleRef:
 14   kind: ClusterRole
 15   name: cluster-admin
 16   apiGroup: rbac.authorization.k8s.io
 17 
 18 [root@uk8s-m-01 mongo]# kubectl apply -f defaultaccout.yaml


2.3 創建headless Service

  1 [root@k8smaster01 mongo]# vi mongo-headless-service.yaml




提示:本實驗直接將headless結合在StatefulSet同一個yaml文件中,參考2.4。

2.4 創建StatefulSet

  1 [root@k8smaster01 mongo]# vi statefulset-mongo.yaml
  2 ---
  3 apiVersion: v1
  4 kind: Service
  5 metadata:
  6   name: mongo
  7   labels:
  8     name: mongo
  9 spec:
 10   ports:
 11   - port: 27017
 12     targetPort: 27017
 13   clusterIP: None
 14   selector:
 15     role: mongo
 16 ---                                  #以上為headless-service
 17 apiVersion: apps/v1beta1
 18 kind: StatefulSet
 19 metadata:
 20   name: mongo
 21 spec:
 22   serviceName: "mongo"
 23   replicas: 3
 24   template:
 25     metadata:
 26       labels:
 27         role: mongo
 28         environment: test
 29     spec:
 30       terminationGracePeriodSeconds: 10
 31       containers:
 32         - name: mongo
 33           image: mongo:3.4             #新版可能不支持smallfiles參數,因此指定為3.4版本
 34           command:
 35             - mongod
 36             - "--replSet"
 37             - rs0
 38             - "--bind_ip"
 39             - 0.0.0.0
 40             - "--smallfiles"           #使用較小的默認文件
 41             - "--noprealloc"           #禁用數據文件預分配
 42           ports:
 43             - containerPort: 27017
 44           volumeMounts:
 45             - name: mongo-persistent-storage
 46               mountPath: /data/db
 47         - name: mongo-sidecar
 48           image: cvallance/mongo-k8s-sidecar
 49           env:
 50             - name: MONGO_SIDECAR_POD_LABELS
 51               value: "role=mongo,environment=test"
 52             - name: KUBERNETES_MONGO_SERVICE_NAME
 53               value: "mongo"
 54   volumeClaimTemplates:
 55   - metadata:
 56       name: mongo-persistent-storage
 57       annotations:
 58         volume.beta.kubernetes.io/storage-class: "fast"
 59     spec:
 60       accessModes: [ "ReadWriteOnce" ]
 61       resources:
 62         requests:
 63           storage: 2Gi



釋義:

  1. 該StatefulSet定義了兩個容器:mingo和mongo-sidecar。mongo是主服務程序,mongo-sidecar是將多個mongo實例進行集群設置的工具。同時mongo-sidecar中設置了如下環境變量:


    • MONGO_SIDECAR_POD_LABELS:設置為mongo容器的標籤,用於sidecar查詢它所要管理的MongoDB集群實例。
    • KUBERNETES_MONGO_SERVICE_NAME:它的值為mongo,表示sidecar將使用mongo這個服務名來完成MongoDB集群的設置。


  1. replicas=3表示MongoDB集群由3個mongo實例組成。
  2. volumeClaimTemplates是StatefulSet最重要的存儲設置。在annotations段設置volume.beta.kubernetes.io/storage-class=”fast”表示使用名為fast的StorageClass自動為每個mongo Pod實例分配後端存儲。
  3. resources.requests.storage=2Gi表示為每個mongo實例都分配2GiB的磁盤空間。




  1 [root@k8smaster01 mongo]# kubectl create -f statefulset-mongo.yaml	#創建mongo


提示:由於國內mongo鏡像可能無法pull,建議通過VPN等方式提前pull鏡像,然後上傳至所有node節點。

  1 [root@VPN ~]# docker pull cvallance/mongo-k8s-sidecar:latest
  2 [root@VPN ~]# docker pull mongo:3.4.4
  3 [root@VPN ~]# docker save -o mongo-k8s-sidecar.tar cvallance/mongo-k8s-sidecar:latest
  4 [root@VPN ~]# docker save -o mongo_3_4_4.tar mongo:3.4.4
  5 [root@k8snode01 ~]# docker load -i mongo-k8s-sidecar.tar
  6 [root@k8snode01 ~]# docker load -i mongo.tar
  7 [root@k8snode01 ~]# docker images



創建異常可通過如下方式刪除,重新創建:

  1 kubectl delete -f statefulset-mongo.yaml
  2 kubectl delete -f mongo-headless-service.yaml
  3 kubectl delete pvc -l role=mongo


三 確認驗證

3.1 查看資源

  1 [root@k8smaster01 mongo]# kubectl get pod -l role=mongo			#查看集群pod
  2 NAME      READY   STATUS    RESTARTS   AGE
  3 mongo-0   2/2     Running   0          9m44s
  4 mongo-1   2/2     Running   0          7m51s
  5 mongo-2   2/2     Running   0          6m1s



StatefulSet會用volumeClaimTemplates中的定義為每個Pod副本都創建一個PVC實例,每個PVC的名稱由StatefulSet定義中volumeClaimTemplates的名稱和Pod副本的名稱組合而成。

  1 [root@k8smaster01 mongo]# kubectl get pvc



  1 [root@k8smaster01 mongo]# kubectl get pods mongo-0 -o yaml | grep -A 3 volumes	#查看掛載


3.2 查看mongo集群


登錄任意一個mongo Pod,在mongo命令行界面用rs.status()命令查看MongoDB集群的狀態,該mongodb集群已由sidecar完成了創建。在集群中包含3個節點 每個節點的名稱都是StatefulSet設置的DNS域名格式的網絡標識名稱:

mongo-0.mongo.default.svc.cluster.local

mongo-1.mongo.default.svc.cluster.local

mongo-2.mongo.default.svc.cluster.local

同時,可以查看每個mongo實例各自的角色(PRIMARY或SECONDARY)。

  1 [root@k8smaster01 mongo]# kubectl exec -ti mongo-0 -- mongo
  2 ……
  3 rs0:PRIMARY> rs.status()




四 集群常見管理

4.1 MongoDB擴容


運行環境過程中,若3個mongo實例不足以滿足業務的要求,可對mongo集群進行擴容。僅需要通過對StatefulSet進行scale操作,從而實現在mongo集群中自動添加新的mongo節點。

  1 [root@k8smaster01 ~]# kubectl scale statefulset mongo --replicas=4	#擴容為4個
  2 [root@k8smaster01 ~]# kubectl get pod -l role=mongo
  3 NAME      READY   STATUS    RESTARTS   AGE
  4 mongo-0   2/2     Running   0          105m
  5 mongo-1   2/2     Running   0          103m
  6 mongo-2   2/2     Running   0          101m
  7 mongo-3   2/2     Running   0          50m


4.2 查看集群成員

  1 [root@k8smaster01 mongo]# kubectl exec -ti mongo-0 -- mongo
  2 ……
  3 rs0:PRIMARY> rs.status()
  4 ……



4.3 故障自動恢復


若在系統運行過程中,某個mongo實例或其所在主機發生故障,則StatefulSet將會自動重建該mongo實例,並保證其身份(ID)和使用的數據(PVC) 不變。以下為mongo-0實例發生故障進行模擬,StatefulSet將會自動重建mongo-0實例,併為其掛載之前分配的PVC“mongo-persistent-storage-mongo-0”。新的服務“mongo-0”在重新啟動后,原數據庫中的數據不會丟失,可繼續使用。

  1 [root@k8smaster01 ~]# kubectl get pvc
  2 [root@k8smaster01 ~]# kubectl delete pod mongo-0
  3 [root@k8smaster01 mongo]# kubectl exec -ti mongo-0 -- mongo
  4 ……
  5 rs0:PRIMARY> rs.status()
  6 ……





提示:進入某個實例查看mongo集群的狀態,mongo-0發生故障前在集群中的角色為PRIMARY,在其脫離集群后,mongo集群會自動選出一個SECONDARY節點提升為PRIMARY節點(本例中為mongo-2)。重啟后的mongo-0則會成為一個新的SECONDARY節點。本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

從壹開始 [ Design Pattern ] 之二 ║ 單例模式 與 Singleton

前言

這一篇來源我的公眾號,如果你沒看過,正好直接看看,如果看過了也可以再看看,我稍微修改了一些內容,今天講解的內容如下

 

 

 

 

 

 

 

一、什麼是單例模式

 

【單例模式】,英文名稱:Singleton Pattern,這個模式很簡單,一個類型只需要一個實例,他是屬於創建類型的一種常用的軟件設計模式。通過單例模式的方法創建的類在當前進程中只有一個實例(根據需要,也有可能一個線程中屬於單例,如:僅線程上下文內使用同一個實例)。

1、單例類只能有一個實例。

2、單例類必須自己創建自己的唯一實例。

3、單例類必須給所有其他對象提供這一實例。

 

那咱們大概知道了,其實說白了,就是我們整個項目周期內,只會有一個實例,當項目停止的時候,實例銷毀,當重新啟動的時候,我們的實例又會產品。

上文中說到了一個名詞【創建類型】的設計模式,那什麼是創建類型的設計模式呢?

創建型(Creational)模式:負責對象創建,我們使用這個模式,就是為了創建我們需要的對象實例的。

 

那除了創建型還有其他兩種類型的模式:

結構型(Structural)模式:處理類與對象間的組合

行為型(Behavioral)模式:類與對象交互中的職責分

這兩種設計模式,以後會慢慢說到,這裏先按下不表。

咱們就重點從0開始分析分析如何創建一個單例模式的對象實例。

 

二、如何創建單例模式

 

實現單例模式有很多方法:從“懶漢式”到“餓漢式”,最後“雙檢鎖”模式,這裏咱們就慢慢的,從一步一步的開始講解如何創建單例。

 

1、正常的思考邏輯順序

 

既然要創建單一的實例,那我們首先需要學會如何去創建一個實例,這個很簡單,相信每個人都會創建實例,就比如說這樣的:

/// <summary>
/// 定義一個天氣類
/// </summary>
public class WeatherForecast
{
    public WeatherForecast()
    {
        Date = DateTime.Now;
    }
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string Summary { get; set; }
}


 [HttpGet]
 public WeatherForecast Get()
 {
     // 實例化一個對象實例
     WeatherForecast weather = new WeatherForecast();
     return weather;
 }

 

我們每次訪問的時候,時間都是會變化,所以我們的實例也是一直在創建,在變化:

 

 

相信每個人都能看到這個代碼是什麼意思,不多說,直接往下走,我們知道,單例模式的核心目的就是:

必須保證這個實例在整個系統的運行周期內是唯一的,這樣可以保證中間不會出現問題。

 

那好,我們改進改進,不是說要唯一一個么,好說!我直接返回不就行了:

 

 /// <summary>
 /// 定義一個天氣類
 /// </summary>
 public class WeatherForecast
 {
     // 定義一個靜態變量來保存類的唯一實例
     private static WeatherForecast uniqueInstance;

     // 定義私有構造函數,使外界不能創建該類實例
     private WeatherForecast()
     {
         Date = DateTime.Now;
     }
     /// <summary>
     /// 靜態方法,來返回唯一實例
     /// 如果存在,則返回
     /// </summary>
     /// <returns></returns>
     public static WeatherForecast GetInstance()
     {
         // 如果類的實例不存在則創建,否則直接返回
         // 其實嚴格意義上來說,這個不屬於【單例】
         if (uniqueInstance == null)
         {
             uniqueInstance = new WeatherForecast();
         }
         return uniqueInstance;
     }
     public DateTime Date { get; set; }public int TemperatureC { get; set; }
     public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
     public string Summary { get; set; }
 }

 

 

然後我們修改一下調用方法,因為我們的默認構造函數已經私有化了,不允許再創建實例了,所以我們直接這麼調用:

[HttpGet]
 public WeatherForecast Get()
 {
     // 實例化一個對象實例
     WeatherForecast weather = WeatherForecast.GetInstance();
     return weather;
 }

 

最後來看看效果:

 

 

這個時候,我們可以看到,時間已經不發生變化了,也就是說我們的實例是唯一的了,大功告成!是不是很開心!

 

但是,別著急,問題來了,我們目前是單線程的,所以只有一個,那如果多線程呢,如果多個線程同時訪問,會不會也會正常呢?

這裏我們做一個測試,我們在項目啟動的時候,用多線程去調用:

 

 [HttpGet]
 public WeatherForecast Get()
 {
     // 實例化一個對象實例
     //WeatherForecast weather = WeatherForecast.GetInstance();

     // 多線程去調用
     for (int i = 0; i < 3; i++)
     {
         var th = new Thread(
         new ParameterizedThreadStart((state) =>
         {
             WeatherForecast.GetInstance();
         })
         );
         th.Start(i);
     }
     return null;
 }

 

然後我們看看效果是怎樣的,按照我們的思路,應該是只會走一遍構造函數,其實不是:

 

 

 

 

 

 

3個線程在第一次訪問GetInstance方法時,同時判斷(uniqueInstance ==null)這個條件時都返回真,然後都去創建了實例,這個肯定是不對的。那怎麼辦呢,只要讓GetInstance方法只運行一個線程運行就好了,我們可以加一個鎖來控制他,代碼如下:

public class WeatherForecast
{
    // 定義一個靜態變量來保存類的唯一實例
    private static WeatherForecast uniqueInstance;
    // 定義一個鎖,防止多線程
    private static readonly object locker = new object();

    // 定義私有構造函數,使外界不能創建該類實例
    private WeatherForecast()
    {
        Date = DateTime.Now;
    }
    /// <summary>
    /// 靜態方法,來返回唯一實例
    /// 如果存在,則返回
    /// </summary>
    /// <returns></returns>
    public static WeatherForecast GetInstance()
    {
        // 當第一個線程執行的時候,會對locker對象 "加鎖",
        // 當其他線程執行的時候,會等待 locker 執行完解鎖
        lock (locker)
        {
            // 如果類的實例不存在則創建,否則直接返回
            if (uniqueInstance == null)
            {
                uniqueInstance = new WeatherForecast();
            }
        }

        return uniqueInstance;
    }
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string Summary { get; set; }
}

 

這個時候,我們再併發測試,發現已經都一樣了,這樣就達到了我們想要的效果,但是這樣真的是最完美的么,其實不是的,因為我們加鎖,只是第一次判斷是否為空,如果創建好了以後,以後就不用去管這個 lock 鎖了,我們只關心的是 uniqueInstance 是否為空,那我們再完善一下:

 

/// <summary>
/// 定義一個天氣類
/// </summary>
public class WeatherForecast
{
    // 定義一個靜態變量來保存類的唯一實例
    private static WeatherForecast uniqueInstance;
    // 定義一個鎖,防止多線程
    private static readonly object locker = new object();

    // 定義私有構造函數,使外界不能創建該類實例
    private WeatherForecast()
    {
        Date = DateTime.Now;
    }
    /// <summary>
    /// 靜態方法,來返回唯一實例
    /// 如果存在,則返回
    /// </summary>
    /// <returns></returns>
    public static WeatherForecast GetInstance()
    {
        // 當第一個線程執行的時候,會對locker對象 "加鎖",
        // 當其他線程執行的時候,會等待 locker 執行完解鎖
        if (uniqueInstance == null)
        {
            lock (locker)
            {
                // 如果類的實例不存在則創建,否則直接返回
                if (uniqueInstance == null)
                {
                    uniqueInstance = new WeatherForecast();
                }
            }
        }

        return uniqueInstance;
    }
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string Summary { get; set; }
}

 

這樣才最終的完美實現我們的單例模式!搞定。

 

2、幽靈事件:指令重排

當然,如果你看完了上邊的那四步已經可以出師了,平時我們就是這麼使用的,也是這麼想的,但是真的就是萬無一失么,有一個 JAVA 的朋友提出了這個問題,C# 中我沒有聽說過,是我孤陋寡聞了么:

單例模式的幽靈事件,時令重排會偶爾導致單例模式失效。

 

是不是聽起來感覺很高大上,而不知所云,沒關係,咱們平時用不到,但是可以了解了解:

為何要指令重排?       

指令重排是指的 volatile,現在的CPU一般採用流水線來執行指令。一個指令的執行被分成:取指、譯碼、訪存、執行、寫回、等若干個階段。然後,多條指令可以同時存在於流水線中,同時被執行。
指令流水線並不是串行的,並不會因為一個耗時很長的指令在“執行”階段呆很長時間,而導致後續的指令都卡在“執行”之前的階段上。
相反,流水線是并行的,多個指令可以同時處於同一個階段,只要CPU內部相應的處理部件未被佔滿即可。比如說CPU有一個加法器和一個除法器,那麼一條加法指令和一條除法指令就可能同時處於“執行”階段, 而兩條加法指令在“執行”階段就只能串行工作。
相比於串行+阻塞的方式,流水線像這樣并行的工作,效率是非常高的。

然而,這樣一來,亂序可能就產生了。比如一條加法指令原本出現在一條除法指令的後面,但是由於除法的執行時間很長,在它執行完之前,加法可能先執行完了。再比如兩條訪存指令,可能由於第二條指令命中了cache而導致它先於第一條指令完成。
一般情況下,指令亂序並不是CPU在執行指令之前刻意去調整順序。CPU總是順序的去內存裏面取指令,然後將其順序的放入指令流水線。但是指令執行時的各種條件,指令與指令之間的相互影響,可能導致順序放入流水線的指令,最終亂序執行完成。這就是所謂的“順序流入,亂序流出”。

 

這個是從網上摘錄的,大概意思看看就行,理解雙檢鎖失效原因有兩個重點

1、編譯器的寫操作重排問題.
例 : B b = new B();

上面這一句並不是原子性的操作,一部分是new一個B對象,一部分是將new出來的對象賦值給b.

直覺來說我們可能認為是先構造對象再賦值.但是很遺憾,這個順序並不是固定的.再編譯器的重排作用下,可能會出現先賦值再構造對象的情況.

2、結合上下文,結合使用情景.

理解了1中的寫操作重排以後,我卡住了一下.因為我真不知道這種重排到底會帶來什麼影響.實際上是因為我看代碼看的不夠仔細,沒有意識到使用場景.雙檢鎖的一種常見使用場景就是在單例模式下初始化一個單例並返回,然後調用初始化方法的方法體內使用初始化完成的單例對象.

 

三、Singleton = 單例 ?

 上邊我們說了很多,也介紹了很多單例的原理和步驟,那這裏問題來了,我們在學習依賴注入的時候,用到的 Singleton 的單例注入,是不是和上邊說的一回事兒呢,這裏咱們直接多多線程測試一下就行:

 

/// <summary>
/// 定義一個心情類
/// </summary>
public class Feeling
{
    public Feeling()
    {
        Date = DateTime.Now;
    }
    public DateTime Date { get; set; }
}


 // 單例注入
 services.AddSingleton<Feeling>();


[HttpGet]
public WeatherForecast Get()
{

    // 多線程去調用
    for (int i = 0; i < 3; i++)
    {
        var th = new Thread(
        new ParameterizedThreadStart((state) =>
        {
            //WeatherForecast.GetInstance();
            
            // 此刻的心情
            Feeling feeling = new Feeling();
            Console.WriteLine(feeling.Date);
        })
        );
        th.Start(i);
    }
    return null;
}

 

測試的結果,情理之中,也是意料之外:

 

 

竟然和我們上邊說的是一樣的, 
Singleton是一種懶漢模式 的單例, 因為結論可以看出,有時候我們使用單例模式,並不是寫一個 Sigleton 就能滿足的。    

四、單例模式的優缺點

 

        【優】、單例模式的優點:

             (1)、保證唯一性:防止其他對象實例化,保證實例的唯一性;

             (2)、全局性:定義好數據后,可以再整個項目種的任何地方使用當前實例,以及數據;

        【劣】、單例模式的缺點: 

             (1)、內存常駐:因為單例的生命周期最長,存在整個開發系統內,如果一直添加數據,或者是常駐的話,會造成一定的內存消耗。

 

以下內容來自百度百科:

優點

一、實例控制 單例模式會阻止其他對象實例化其自己的單例對象的副本,從而確保所有對象都訪問唯一實例。
二、靈活性 因為類控制了實例化過程,所以類可以靈活更改實例化過程。  

缺點

一、開銷 雖然數量很少,但如果每次對象請求引用時都要檢查是否存在類的實例,將仍然需要一些開銷。可以通過使用靜態初始化解決此問題。
二、可能的開發混淆 使用單例對象(尤其在類庫中定義的對象)時,開發人員必須記住自己不能使用
new關鍵字實例化對象。因為可能無法訪問庫源代碼,因此應用程序開發人員可能會意外發現自己無法直接實例化此類。
三、對象生存期 不能解決刪除單個對象的問題。在提供內存管理的語言中(例如基於.NET Framework的語言),只有單例類能夠導致實例被取消分配,因為它包含對該實例的私有引用。在某些語言中(如 C++),其他類可以刪除對象實例,但這樣會導致單例類中出現懸浮引用。

 

五、示例代碼

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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