三小時攻克 Kubernetes!


我保證本文是最詳盡的 Kubernetes 技術文檔,從我在后台排版了這么漫長的時間就能看出來。廢話不多說——牢牢占據容器技術統治地位的 Kubernetes,其重要性想必不言而喻。

以下為譯文:

為什么銀行肯花大價錢雇我做 Kubernetes 如此簡單的工作?——我一直很奇怪這一點,因為我覺得任何人都可以在 3 個小時內學會這項技術。

如果你懷疑我說的,就來挑戰一下吧!讀完本文,你絕對可以學會如何在 Kubernetes 集群上運行基於微服務的應用程序。我保證你可以做到,因為我就是這樣向我的客戶介紹 Kubernetes 的。

本文的教程與其他資源有何不同?有很大不同。大多數的教程都從最簡單的內容講起:Kubernetes 的概念和 kubectl 的命令。本文則是基於讀者了解應用程序的開發、微服務和 Docker 容器等基礎之上。

本文中,我們會涉及的內容包括:

在計算機上運行基於微服務的應用程序;

為微服務應用程序的每個服務建立容器映像;

Kubernetes 的基本介紹;

在 Kubernetes 管理的集群內部署基於微服務的應用程序。

通過一步步深入學習,讓大家能夠領會 Kubernetes 的簡單易用。只有你了解它的使用環境時,才能輕松掌握 Kubernetes。廢話不多說,我們開始吧。

應用程序示范

如下應用程序有一個功能:每次輸入接受一個句子;使用文本分析,計算句子所表達的情緒。

圖1:情感分析網絡應用

從技術的角度看來,這個應用程序包含 3 個微服務,每個都包含特定功能:

  • SA-Frontend:前端, Nginx 網絡服務器,提供 ReactJS 靜態文件;

  • SA-WebAp:網絡應用, Java 網絡應用程序,處理來自前端的請求;

  • SA-Logic:邏輯處理, Python 應用程序,執行情感分析。

你需要知道微服務是無法獨立工作的,它們引入了“關注點分離”(separation of concerns),但是它們之間依然需要交互。

圖2:情感分析網絡應用中的數據流

我們可以通過微服務之間的數據流來描述這種交互:

  • 客戶端應用程序請求初始頁面 index.html(index.html 頁面會反過來加載 ReactJS 應用程序的腳本);

  • 用戶與應用程序的交互觸發到 Spring 網絡應用的請求;

  • Spring 網絡應用將請求發送給 Python 應用做情感分析;

  • Python 應用計算情感值,並返回結果;

  • Spring 網絡應用將結果返回給 React 應用(然后由 React 應用將結果顯示給用戶)。

點擊此處下載這些應用程序的代碼:https://github.com/rinormaloku/k8s-mastery。現在就可以克隆這個代碼倉庫,接下來我們要做更加精彩的東西。

在計算機上運行基於微服務的應用程序

我們需要啟動所需的 3 個服務。讓我們從最有意思的部分——前端應用程序開始。

設置 React 的本地部署

為了運行 React 應用程序,首先你需要在計算機上安裝 NodeJS 和 NPM。安裝好這些后,在終端中進入目錄 sa-frontend。然后運行如下命令:

 

npm install

 

該命令會將 React 應用程序的所有 Javascript 依賴都下載到文件夾 node_modules 中(package.json 文件中定義了所有依賴)。在所有依賴都解決后,運行如下命令:

 

npm start

 

這樣就可以了!我們運行了 React 應用程序,現在可以通過默認端口 localhost:3000 訪問該應用程序了。你可以自由修改代碼,並從瀏覽器中觀察即時效果。這里用到了熱模塊替換(即在運行時用替換模塊來減少頁面刷新次數)技術,可以減輕前端開發的工作。

准備好 React 應用的產品環境

為了搭建產品環境,我們需要建立應用程序的靜態網頁,並通過網絡服務器提供服務。

為了搭建 React 應用程序,首先在終端中進入目錄 sa-frontend。然后運行如下命令:

 

npm run build

 

該命令會在項目的文件目錄中生成一個名叫“build”的文件夾。該文件夾內包含了 ReactJS 應用程序所需的所有靜態文件。

利用 Nginx 提供靜態文件

首先安裝並啟動 Nginx 網絡服務器。然后將 sa-frontend/build 目錄內的文件移動到 [nginx安裝目錄]/html。

如此一來,我們就可以通過 [nginx安裝目錄]/html/index.html 來訪問 index.html 文件了,而它是 Nginx 服務的默認文件。

默認情況下,Nginx 網絡服務器會監聽端口 80。你可以通過修改 [nginx安裝目錄]/conf/nginx.conf 文件中的 server.listen 字段來指定不同的端口。

打開瀏覽器,並訪問端口 80,可以看到 ReactJS 應用程序加載成功。

圖3:Nginx 提供的 React 應用程序服務

在輸入框“Type your sentence”(輸入句子)中輸入句子,然后點擊 SEND(發送)按鈕,但是頁面會返回錯誤 404(你可以檢查瀏覽器的控制台)。為什么?讓我們檢查一下代碼。

檢查代碼

我們可以在 App.js 文件中看到,點擊“SEND”按鈕會觸發 analyzeSentence。該方法的代碼如下所示(我們對每一段代碼進行了編號“#號碼”,具體解釋如下所示):

 

analyzeSentence() {
   fetch('http://localhost:8080/sentiment', {  // #1
       method: 'POST',
       headers: {
           'Content-Type': 'application/json'
       },
       body: JSON.stringify({
                      sentence: this.textField.getValue()})// #2
   })
       .then(response => response.json())
       .then(data => this.setState(data));  // #3
}

 

#1:POST 方法調用的 URL(應用程序應該在該 URL 上監聽訪問);

#2:發送到應用程序的請求主體如下所示:

 

{
   sentence: “I like yogobella!”
}

 

#3:返回值將更新該組件的狀態,而狀態的變更將重新渲染這個組件。如果我們收到數據(即包含用戶輸入的句子和極性的 JSON 對象),那么我們會顯示組件 polarityComponent,因為滿足條件而且我們可以如下定義該組件:

 

const polarityComponent = this.state.polarity !== undefined ?
   <Polarity sentence={this.state.sentence}
             polarity={this.state.polarity}/> :
   null;

 

一切看起來都很好。但是我們還差什么呢?你可能已經發現我們沒有在 localhost:8080 上設置任何東西!我們必須啟動 Spring 網絡應用程序監聽這個端口!

圖4:缺失的 Spring 網絡應用程序微服務

建立 Spring 網絡應用程序

為了設置 Spring 網絡應用程序,你必須安裝 JDK8 和 Maven,並設置它們的環境變量。設置好后,我們繼續下個部分。

將應用程序打包成 Jar 文件

在終端中進入 sa-webapp 目錄,並運行如下命令:

 

mvn install

 

該命令會在目錄 sa-webapp 中生成一個名叫 target 的文件夾。target 文件夾內有打包好的 Java 應用程序包:’sentiment-analysis-web-0.0.1-SNAPSHOT.jar’。

啟動應用程序

進入 target 目錄,並通過如下命令啟動應用程序:

 

java -jar sentiment-analysis-web-0.0.1-SNAPSHOT.jar

 

等等……出錯了。應用程序啟動失敗,我們可以看到如下異常信息:

 

Error creating bean with name 'sentimentController': Injection of autowired dependencies failed; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'sa.logic.api.url' in value "${sa.logic.api.url}"

 

這里顯示的重要信息是 SentimentController 中的 sa.logic.api.url。我們檢查一下這段代碼。

檢查代碼

 

@CrossOrigin(origins = "*")
@RestController
public class SentimentController {
@Value("${sa.logic.api.url}")    // #1
   private String saLogicApiUrl;
@PostMapping("/sentiment")
   public SentimentDto sentimentAnalysis(
                           @RequestBody SentenceDto sentenceDto) {
       RestTemplate restTemplate = new RestTemplate();
return restTemplate.postForEntity(
               saLogicApiUrl + "/analyse/sentiment",    // #2
               sentenceDto, SentimentDto.class)
               .getBody();
   }
}

 

#1:SentimentController 有一個名叫 saLogicApiUrl 的字段。這個字段的賦值是由 sa.logic.api.url 屬性定義的。

#2:saLogicApiUrl 與值“/analyse/sentiment”連接在一起,共同構成了 Sentiment Analysis 請求的 URL。

定義屬性

在 Spring 中默認的屬性資源是 application.properties(具體位置在 sa-webapp/src/main/resources 中)。但是這不是定義屬性的唯一方式,我們可以通過之前的命令完成屬性定義:

 

java -jar sentiment-analysis-web-0.0.1-SNAPSHOT.jar 
    --sa.logic.api.url=WHAT.IS.THE.SA.LOGIC.API.URL

 

應該由 Python 應用程序運行時定義的值初始化該屬性,如此一來 Spring 網絡應用程序就可以知道在運行時把信息傳遞到哪里了。

為了簡單起見,我們假設在 localhost:5000 上運行 Python 應用程序。請記得哦!

運行如下命令,然后我們來看看最后一個服務:Python 應用程序。

 

java -jar sentiment-analysis-web-0.0.1-SNAPSHOT.jar 
    --sa.logic.api.url=http://localhost:5000

 

建立 Python 應用程序

為了啟動 Python 應用程序,首先我們需要安裝 Python3 和 Pip,以及設置它們的環境變量。

安裝依賴

在終端中進入 sa-logic/sa (代碼庫),然后運行如下命令:

 

python -m pip install -r requirements.txt
python -m textblob.download_corpora

 

啟動應用

在利用 Pip 安裝好依賴后,我們就可以通過運行如下命令啟動應用程序了:

 

python sentiment_analysis.py
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

 

這意味着應用程序已經啟動,並在 localhost 的端口 5000 上監聽 HTTP 請求了。

檢查代碼

讓我們檢查代碼,看看處理邏輯部分的 Python 應用程序在干什么:

 

from textblob import TextBlob
from flask import Flask, request, jsonify
app = Flask(__name__)                                   #1
@app.route("/analyse/sentiment", methods=['POST'])      #2
def analyse_sentiment():
   sentence = request.get_json()['sentence']           #3
   polarity = TextBlob(sentence).sentences[0].polarity #4
   return jsonify(                                     #5
       sentence=sentence,
       polarity=polarity
   )
if __name__ == '__main__':
   app.run(host='0.0.0.0', port=5000)                #6

 

#1:實例化一個 Flask 對象;

#2:定義 POST 請求訪問的路徑;

#3:從請求主體內抽出“sentence”屬性;

#4:初始化匿名 TextBlob 對象,並從第一個句子(我們只有一個)中獲取極性;

#5:在相應體內返回句子和極性;

#6:運行 flask 對象應用來監聽 localhost:5000 上的請求。

所有的服務都設置好,可以互相交流了。試試看重開前端的 localhost:80。

圖6:微服務架構完成

在下面一節中,我們將介紹如何在 Docker 容器內啟動這些服務,因為這是在 Kubernetes 集群內運行這些服務的前提條件。

為每個服務創建容器映像

Kubernetes 是容器管理平台。可想而知我們需要容器去管理它們。但是容器是什么?Docker 官方文檔的最佳答案如下:

容器映像是輕量級的、獨立的、可執行軟件包,包含所有可運行的東西:代碼、運行時、系統工具、系統庫、設置。對於基於 Linux 和 Windows 的應用,不論環境如何,容器化的軟件都可以照常運行。

這意味着容器可以在任何計算機上運行,甚至是在產品服務器上,都沒有任何差別。

為了更形象地描述,讓我們來對比一下 React 應用程序在虛擬機上和容器內運行的情況。

通過虛擬機提供 React 靜態文件

使用虛擬機的缺點包括:

  • 資源效率低下,每個虛擬機都需要一個完全成熟的操作系統;

  • 對平台有依賴性。本地機器上運行得很好的功能未必能在產品服務器上正常工作;

  • 與容器相比,更重而且規模伸縮較慢。

圖7:虛擬機上提供靜態文件的 Nginx 網絡服務器

通過容器提供 React 靜態文件

使用容器的優點包括:

  • 資源效率很高,在 Docker 的幫助下使用主機操作系統;

  • 對平台沒有依賴性。可以在本地機器上運行的容器可以在任何機器上正常工作;

  • 通過映像層提供輕量級服務。

圖8:在容器內提供靜態文件的 Nginx 網絡服務器

以上是使用容器最突出的特色和優勢。更多信息,請參閱 Docker 官方文檔:https://www.docker.com/what-container。

為 React 應用建立容器映像(Docker 簡介)

Docker 容器最基本的組件是 .dockerfile。該 Dockerfile 文件最基本的組成是容器鏡像,我們將通過下列一系列說明,介紹如何創建一個符合應用程序需求的容器鏡像。

在開始定義 Dockerfile 之前,讓我們先回想一下使用 Nginx 服務 React 靜態文件的步驟:

  • 創建靜態文件(npm run build);

  • 啟動 Nginx 服務器;

  • 將前端項目的 build 文件夾的內容復制到 nginx/html 目錄中。

在下一節中,你會注意到創建容器與建立本地 React 的過程非常相似。

為前端定義 Dockerfile

前端 Dockerfile 的建立只有兩個步驟。這是因為 Nginx 團隊為我們提供了基本的 Nginx 映像,我們可以直接利用。這兩個步驟如下:

  • 啟動基本的 Nginx 映像;

  • 將 sa-frontend/build 目錄復制到容器的 nginx/html 中。

轉換成的Dockerfile如下所示:

 

FROM nginx
COPY build /usr/share/nginx/html

 

很驚訝吧?這個文件是可讀的,我們可以概括為:

從 Nginx 映像開始(不管里面是什么)。將 build 目錄復制到映像的 nginx/html 目錄中。然后就好了!

你可能在想,我該從哪兒復制 build 文件呢?例如:/usr/share/nginx/html。非常簡單:在 Docker Hub 的 Nginx 映像文檔中有記載。

建立並推送容器

在推送映像之前,我們需要一個容器注冊來托管映像。Docker Hub 是一個免費的雲容器服務,我們將使用它來做演示。接下來有 3 個任務需要完成:

  • 安裝 Docker CE;

  • 注冊 Docker Hub;

  • 在終端中運行如下命令登錄:

 

docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"

 

在完成上述任何后,請進入目錄 sa-frontend。然后運行如下命令(請用你的 docker hub 用戶名替換 $DOCKER 用戶名,例如:rinormaloku/sentiment-analysis-frontend)。

 

docker build -f Dockerfile -t $DOCKER_USER_ID/sentiment-analysis-frontend .

 

現在我們可以刪掉 -f Dockerfile 了,因為我們已經在包含 Dockerfile 的目錄中了。

我們可以使用 docker push 命令來推送映像:

 

docker push $DOCKER_USER_ID/sentiment-analysis-frontend

 

請確認映像已成功地被推送到 docker hub 代碼庫。

運行容器

現在任何人都可以獲取 $DOCKER_USER_ID/sentiment-analysis-frontend 中的映像並運行:

 

docker pull $DOCKER_USER_ID/sentiment-analysis-frontend
docker run -d -p 80:80 $DOCKER_USER_ID/sentiment-analysis-frontend

 

Docker 容器已經處於運行狀態了!

在進行下一步之前,讓我們先來講解一下 80:80,很多人對此比較不解:

  • 第一個 80 是主機的端口號(例如:我的計算機);

  • 第二個 80 是容器的端口號,請求都會被轉送到這里。

圖9:從主機到容器的端口匹配

這是從<主機端口>匹配到<容器端口>的。也就是說每個發往主機 80 端口的請求都會被匹配到容器的 80 端口,如圖 9 所示。

因為在主機上(你的計算機)80 端口上運行的端口可以訪問 localhost:80。如果本地不支持 Docker,那么你可以在 <docker機器ip>:80 上打開應用程序。運行 docker-machine ip 命令可以找到 Docker 機器的 IP。

試試看!你現在應該可以訪問 React 應用程序了。

Dockerignore 文件

剛才我們看到建立 SA-Frontend 的映像非常慢,不好意思,應該是超級慢。這是因為我們必須將建立過程中的環境文件發送給 Docker 服務。更具體地來說,建立過程中的環境文件指的是在建立映像的時候,所有會用到的 Dockerfile 目錄中的數據。

以我們的例子來說,SA-Frontend 文件包括如下文件夾:

 

sa-frontend:
|   .dockerignore
|   Dockerfile
|   package.json
|   README.md
+---build
+---node_modules
+---public
\---src

 

但是我們只需要 build 文件夾。上傳其他的文件會浪費時間。我們可以通過刪除其他目錄來節約時間。這就需要用到 .dockerignore。你可能覺得這與 .gitignore 很相似,例如你可以所有想要忽略的目錄都添加到 .dockerignore,如下所示:

 

node_modules
src
public

 

這個 .dockerignore 文件應該與 Dockerfile 在同一文件夾中。現在建立映像文件只需要幾秒鍾了。

讓我們繼續看看 Java 應用程序。

為 Java 應用程序建立容器映像

你知道嗎?你已經差不多學習了所有關於創建容器映像的知識!這就是為什么這一小節這么短的原因。

在 sa-webapp 中打開 Dockerfile,你會看到只有兩個新的關鍵字:

 

ENV SA_LOGIC_API_URL http://localhost:5000

EXPOSE 8080

 

關鍵字 ENV 在 Docker 容器內聲明了環境變量。這可以讓我們在啟動容器的時候為情感分析 API 提供 URL。

另外,關鍵字 EXPOSE 提供了一個端口,供我們以后訪問。但是等等,我們在 SA-Frontend 的時候沒有做這一步,說得很對!這個端口僅用於文檔,換句話說就是這個端口是用來向閱讀 Dockerfile 的人提供信息的。

你應該已經掌握了創建和推送容器映像。如果遇到任何困難,可以閱讀 sa-webapp中的README.md 文件。

為 Python 應用程序創建容器映像

sa-logic 的 Dockerfile 中沒有新的關鍵字。現在你已經是 Docker 達人了。

關於如何建立和推送容器映像,請閱讀 sa-logic 目錄中的 README.md 文件。

測試容器化的應用程序

你能相信沒有測試過的東西嗎?我也不信。所以我們來測試一下這些容器吧。

1.運行 sa-logic 容器,並配置監聽端口 5050:

 

docker run -d -p 5050:5000 $DOCKER_USER_ID/sentiment-analysis-logic

 

2.運行 sa-webapp 容器,並配置監聽端口 8080(因為我們改變了 Python 應用監聽的端口,所以我們需要重寫環境變量 SA_LOGIC_API_URL):

 

$ docker run -d -p 8080:8080 -e SA_LOGIC_API_URL='http://<container_ip or docker machine ip>:5000' $DOCKER_USER_ID/sentiment-analysis-web-app

 

3.運行 sa-frontend 容器:

 

docker run -d -p 80:80 $DOCKER_USER_ID/sentiment-analysis-frontend

 

然后就可以了。在瀏覽器中打開 localhost:80。

請注意:如果你改變 sa-webapp 的端口,或使用 Docker 機器的 IP,那么你需要更新 sa-frontend 中的 App.js,讓 analyzeSentence 從新的 IP 或端口獲取 URL。然后你需要建立並使用更新后的映像。

圖10:在容器內運行微服務

智力問答題——為什么使用 Kubernetes?

本節中,我們學習了 Dockerfile,如何使用它創建映像,以及推送映像到 Docker注冊目錄的命令。另外,我們探討了如何通過忽略沒用的文件,減少需要發送的建立過程中的環境文件。最后我們從容器上運行了應用程序。

接下來,我們介紹為什么要使用 Kubernetes?我們將在下面深入介紹 Kubernetes,這里我想給你留個智力問答題。

如果我們的情感分析網絡應用完成得很好,突然間訪問量暴漲到每分鍾數百萬的請求,那么我們的 sa-webapp 和 sa-logic 將面臨巨大的負荷壓力。請問,我們如何才能擴大容器的規模?

Kubernetes 簡介

我向你保證我沒有誇大其詞,讀完本文你會問“為什么我們不稱它為 Supernetes?”

圖11:Supernetes

Kubernetes 是什么?

從容器啟動微服務后,我們有一個問題,讓我們通過如下問答的形式具體描述這個問題:

問:我們怎么擴大或縮小容器?

答:我們啟動另外一個容器。

問:我們如何在容器間分攤負荷?如果當前服務器的負荷達到最大,那我們是否需要另外一個服務器?我們如何最大化硬件使用率?

答:唔......呃......(讓我搜一下)

問:如果在打更新補丁的時候,不影響到所有的服務?如果服務出了問題,如何才能返回之前能正常工作的版本?

Kubernetes 可以解決以上所有問題(以及更多問題!)。我可以用一句話總結 Kubernetes:“Kubernetes 是容器控制平台,可以抽象所有的底層基礎設施(容器運行用到的基礎設施)。”

我們對容器控制平台有個模糊的概念。在本文后續部分,我們將看看它的實際應用,但是這是第一次我們提到“底層基礎設施的抽象”,所以我們來詳細看看這個概念。

底層基礎設施的抽象

Kubernetes 通過一個簡單的 API 提供底層基礎設施的抽象,我們可以向該 API 發送請求。這些請求可以讓 Kubernetes 盡最大能力應對。例如,可以簡單地要求“Kubernetes 添加映像 x 的 4 個容器。”然后 Kubernetes 會找出使用中的節點,並在內添加新的容器(如圖 12 所示)。

圖12:向 API 發送請求

這對開發人員來說意味着什么?意味着開發人員不需要在意節點的數目,也不需要在意從哪里運行容器以及如何與它們交流。開發人員不需要管理硬件優化,或擔心節點關閉(它們將遵循墨菲法則),因為新的節點會添加到 Kubernetes 集群。同時 Kubernetes 會在其他運行的節點中添加容器。Kubernetes 會發揮最大的作用。

在圖 2 中我們看到了一些新東西:

  • API服務器:與集群交互的唯一方式。負責啟動或停止另外一個容器,或檢查當前狀態,日志等;

  • Kubelet:監視節點內的容器,並與主節點交流;

  • Pod:初始階段我們可以把 pod 當成容器。

就介紹這么多,跟深入的介紹會導致我們分心,我們可以等到后面一點再介紹,有一些有用的資源,比如官方文檔,或者閱讀 Marko Lukša 的著作《Kubernetes in Action》,以及 Sébastien Goasguen & Michael Hausenblas 的《Kubernetes Cookbook》。

標准化的雲服務提供商

Kubernetes 另外一個深入人心的點是:它標准化了雲服務提供商。這是一個很大膽的宣言,我們通過如下例子來具體看一看:

比如,有一個 Azure、Google 雲平台或其他雲服務提供商的專家,他擔任了一個搭建在全新的雲服務提供商的項目。這可能引起很多后果,比如說:他可能無法在截止期限內完成;公司可能需要招聘更多相關的人員,等等。

相對的,Kubernetes 就沒有這個問題。因為不論是哪家雲服務提供商,你都可以在上面運行相同的命令。你可以以既定的方式向 API 服務器發送請求。Kubernetes 會負責抽象,並實裝這家雲服務商。

停一秒鍾仔細想一下,這是極其強有力的功能。對公司來說,這意味着他們不需要綁定到一家雲服務商。他們可以計算別家雲服務商的開銷,然后轉移到別家。他們依舊可以保留原來的專家,保留原來的人員,他們還可以花更少的錢。

說了這么多,在下一節中讓我們來實際使用 Kubernetes。

Kubernetes 實踐——Pod

我們建立了微服務在容器上運行,雖然頗為坎坷,但還是可以工作的。我們還提到這種解決方案不具有伸縮性和彈性,而 Kubernetes 可以解決這些問題。在本文的后續章節,我們會將各個服務轉移到由 Kubernetes 管理的容器中,如圖 13 所示。

圖13:在 Kubernetes 管理的集群中運行微服務

在本文中,我們將使用 Minikube 進行本地調試,盡管所有東西都是運行在 Azure 和 Google 雲平台中的。

安裝和啟動 Minikube 

請參閱安裝 Minikube 的官方文檔:

  • https://kubernetes.io/docs/tasks/tools/install-minikube/

在安裝 Minikube 的同時,你可以捎帶着安裝 Kubectl。Kubectl 是向 Kubernetes API 服務器發送請求的客戶端。

可以通過運行 minikube start 命令啟動 Minikube,在啟動后,運行 kubectl get nodes 命令可以得到如下結果:

 

kubectl get nodes
NAME       STATUS    ROLES     AGE       VERSION
minikube   Ready     <none>    11m       v1.9.0

 

Minikube 提供給我們的 Kubernetes 集群只有一個節點,但是記住我們並不在乎有多少個節點,Kubernetes 會負責抽象,對我們來說深入掌握 Kubernetes 並不重要。

在下一節中,我們將介紹 Kubernetes 的第一個資源:Pod。

Pod

我大愛容器,相信現在你也很喜歡容器。那為什么 Kubernetes 給我們最小的可部署計算單元 Pod 呢?Pod是干什么的?由一個或一組容器組成的 Pod 可以共享相同的運行環境。

但是我們真的需要在一個 Pod 內運行兩個容器嗎?呃……一般來說,只會運行一個容器,我們的例子中也是這樣的。但是有些情況下,比如兩個容器需要共享卷,或它們之間是通過跨進程的交流方式交流的,又或者它們被綁到一起,那么就可以使用 Pod。Pod 的另一個特征是:如果我們希望使用其他 Rke 等技術的話,我們可以做到不依賴 Docker 容器。

圖14:Pod 屬性

總的來說,Pod 的主要屬性包括(如圖 14 所示):

  • 每個 Pod 可以在 Kubernetes 集群內擁有唯一的 IP 地址;

  • Pod 可以擁有多個容器。這些容器共享同一個端口空間,所以他們可以通過 localhost 交流(可想而知它們無法使用相同的端口),與其他 Pod 內容器的交流可以通過結合 Pod 的 IP 完成;

  • 一個 Pod 內的容器共享同一個卷、同一個 IP、端口空間、IPC 命名空間。

注:容器有個自己獨立的文件系統,盡管他們可以通過 Kubernetes 的資源卷共享數據。

更多詳細內容,請參閱相關的官方文檔:

https://kubernetes.io/docs/concepts/workloads/pods/pod/

Pod 的定義

如下是我們的第一個 pod sa-frontend 的清單文件,我們會對文件內容進行逐一解釋。

 

apiVersion: v1
kind: Pod                                            # 1
metadata:
 name: sa-frontend                                  # 2
spec:                                                # 3
 containers:
   - image: rinormaloku/sentiment-analysis-frontend # 4
     name: sa-frontend                              # 5
     ports:
       - containerPort: 80                          # 6

 

#1 kind:指定我們想創建的 Kubernetes 資源的類型。這里是 Pod。

#2 name:定義該資源的名字。我們在這里命名為 sa-frontend。

#3 spec:該對象定義了資源應有的狀態。Pod Spec 中最重要的屬性是容器的數組。

#4 image:是指我們希望在本 Pod 中啟動的容器的映像。

#5 name:Pod 中容器中唯一的名字。

#6 containerPort:是指容器監聽的端口號。這只是為了提供文檔信息(即便沒有這個端口也不會影響訪問)。

創建 SA Frontend 的 Pod

你可以在 resource-manifests/sa-frontend-pod.yaml 中找到上述 Pod 的定義。你可以在終端中進入該文件夾,或在命令行輸入完整的路徑。然后執行如下命令:

 

kubectl create -f sa-frontend-pod.yaml
pod "sa-frontend" created

 

可以通過如下命令確認 Pod:

 

kubectl get pods
NAME                          READY     STATUS    RESTARTS   AGE
sa-frontend                   1/1       Running   0          7s

 

如果該 Pod 還處於容器生成中的狀態的話,你可以在運行命令的時候加入參數 --watch,當 Pod 進入運行狀態的時候,終端會顯示信息。

從外部訪問應用程序

為了從外部訪問應用程序,我們需要創建服務類型的Kubernetes資源,具體內容我們將在后續章節講解,雖然通過服務類型的資源支持外部訪問是更合適的做法,但是此處為了快速調試,我們還有另外一個辦法,即轉發端口:

 

kubectl port-forward sa-frontend-pod 88:80
Forwarding from 127.0.0.1:88 -> 80

 

在瀏覽器中訪問 127.0.0.1:88,即可打開 React 應用程序。

擴大規模的錯誤方法

我們說過 Kubernetes 的主要特色之一就是伸縮性,為了證明這一點,讓我們運行另外一個 Pod。我們通過如下定義創建另外一個 Pod 資源:

 

apiVersion: v1
kind: Pod                                            
metadata:
 name: sa-frontend2      # The only change
spec:                                                
 containers:
   - image: rinormaloku/sentiment-analysis-frontend
     name: sa-frontend                              
     ports:
       - containerPort: 80

 

然后,通過如下命令創建新的 Pod:

 

kubectl create -f sa-frontend-pod2.yaml
pod "sa-frontend2" created

 

可以通過如下命令確認第二個 Pod:

 

kubectl get pods
NAME                          READY     STATUS    RESTARTS   AGE
sa-frontend                   1/1       Running   0          7s
sa-frontend2                  1/1       Running   0          7s

 

現在我們有兩個運行中的 Pod。

請注意:這不是最終的解決方案,還有很多缺陷。我們將在另一個 Kubernetes 資源的部署一節中改善這個方案。

總結 Pod

提供靜態文件的 Nginx 網絡服務器在另個不同的 Pod 內運行。現在我們有兩個問題:

  • 怎樣對外開放這些服務,讓用戶通過 URL 來訪問它們?

  • 怎樣平衡 Pod 之間的負荷?

圖15:服務之間的負荷平衡

Kubernetes 提供了服務類型的資源。在下一節中我們將詳細介紹。

Kubernetes 實踐——服務

Kubernetes 服務資源可以作為一組提供相同服務的 Pod 的入口。這個資源肩負發現服務和平衡 Pod 之間負荷的重任,如圖 16 所示。

圖16:Kubernetes 服務維護 ID 地址

在 Kubernetes 集群內,我們擁有提供不同服務的 Pod(前端、Spring 網絡應用和 Flask Python 應用程序)。所以這里的問題是:服務如何知道該處理哪個 Pod?例如:它如何生成這些 Pod 的終端列表?

這個問題可以用標簽來解決,具體分兩個步驟:

  • 給所有服務處理的對象 Pod 貼上標簽;

  • 在服務中使用一個選擇器,該選擇器定義了所有貼有標簽的對象 Pod。

下列視圖看起來更清晰:

圖17:帶有標簽的 Pod 和它們的清單文件

我們可以看到 Pod 都貼着標簽“app: sa-frontend”,服務用這個標簽找到目標 Pod。

標簽

標簽提供了一種簡單的方法用於管理Kubernetes資源。它們有一對鍵值表示,且可以用於所有資源。按照圖17中的例子,修改清單文件。

在修改完畢后保存文件,並通過如下命令應用這些變更:

 

kubectl apply -f sa-frontend-pod.yaml
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
pod "sa-frontend" configured
kubectl apply -f sa-frontend-pod2.yaml
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
pod "sa-frontend2" configured

 

我們看到了一個警告(在應用的時候,而非創建,明白了)。在第二行我們看到部署了 pod “sa-frontend”和 “sa-frontend2”。我們可以過濾想要查看的 Pod:

 

kubectl get pod -l app=sa-frontend
NAME           READY     STATUS    RESTARTS   AGE
sa-frontend    1/1       Running   0          2h
sa-frontend2   1/1       Running   0          2h

 

驗證帶有標簽的 Pod 的另一種方法是在上述命令中加入標志符 --show-labels,那么結果中會顯示每個 Pod 的所有標簽。

很好!Pod 已經貼上了標簽,我們准備好通過服務找到它們了。讓我們定義 LoadBalancer 類型的服務,如圖 18 所示。

圖18:用 LoadBalancer 服務平衡負荷

服務的定義

LoadBalancer 服務的 YAML 定義如下所示:

 

apiVersion: v1
kind: Service              # 1
metadata:
 name: sa-frontend-lb
spec:
 type: LoadBalancer       # 2
 ports:
 - port: 80               # 3
   protocol: TCP          # 4
   targetPort: 80         # 5
 selector:                # 6
   app: sa-frontend       # 7

 

#1 kind:服務;

#2 type:指定類型,我們選擇 LoadBalancer,因為我們想平衡 Pod 之間的負荷;

#3 ports:指定服務獲取請求的端口;

#4 protocol:定義交流;

#5 targetPort:可以將來訪的請求轉發到這個端口;

#6 selector:包含選擇pod屬性的對象;

#7 app:sa-frontend定義了哪個是目標 Pod,只有擁有標簽“app: sa-frontend”的才是目標 Pod。 

通過運行如下命令創建服務:

 

kubectl create -f service-sa-frontend-lb.yaml
service "sa-frontend-lb" created

 

可以通過運行如下命令檢查的服務的狀態:

 

kubectl get svc
NAME             TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
sa-frontend-lb   LoadBalancer   10.101.244.40   <pending>     80:30708/TCP   7m

 

External-IP 處於 pending 狀態(不用再等了,這個狀態不會變的)。這是因為我們使用的是 Minikube。如果我們在 Azure 或 Google 雲服務上運行,那么我們可以得到一個公開的 IP,那么全世界都可以訪問我們的服務了。

盡管如此,Minikube 也不會置我們於不顧,它提供一個非常有用的本地調試命令,如下所示:

 

minikube service sa-frontend-lb
Opening kubernetes service default/sa-frontend-lb in default browser...

 

這可以在瀏覽器中打開指向該服務的 IP。服務受到請求后,會將請求轉發給其中一個 Pod(不用理會是哪個)。通過利用服務作為訪問入口,這種抽象可以讓我們看到並將多個 Pod 當成一個來交互。

服務的總結

在本節中,我們介紹了給資源貼標簽,在服務中使用標簽作為選擇器,我們還定義並創建了一個 LoadBalancer 的服務。這滿足了我們希望伸縮應用程序規模的需求(只需加入新的貼了標簽的 Pod),並通過將服務作為訪問入口在 Pod 之間做負載均衡。

Kubernetes 實踐——部署

Kubernetes 部署可以幫助每一個應用程序的生命都保持相同的一點:那就是變化。此外,只有掛掉的應用程序才會一塵不變,否則,新的需求會源源不斷地涌現,更多代碼會被開發出來、打包以及部署。這個過程中的每一步都有可能出錯。

部署資源可以自動化應用程序從一版本升遷到另一版本的過程,並保證服務不間斷,如果有意外發生,它可以讓我們迅速回滾到前一個版本。

部署實踐

現在我們有兩個 Pod 和一個服務開放,而且它們之間有負載均衡(如圖 19 所示)。我們提到過現有的 Pod 還遠遠不夠完美。需要分開管理每一個 Pod(創建、更新、刪除和監視他們的情況)。快速更新和迅速回滾根本不可能!這樣是不行的,部署 Kubernetes 資源可以解決這里的每個問題。

圖19:現狀

在繼續下面的內容之前,讓我們復述下我們的目標,通過概述可以讓我們更好的理解部署資源的清單文件的定義。我們想要的是:

  • 映像 rinormaloku/sentiment-analysis-frontend 的兩個 Pod;

  • 部署期間服務不間斷;

  • Pod 貼有標簽 app: sa-frontend,所以我們可以通過 sa-frontend-lb 服務找到各個服務。

在下一節中,我們可以將這些需求反映到部署的定義中。

部署的定義

如下資源定義的YAML文件可以達成以上所有提到的點:

 

apiVersion: extensions/v1beta1
kind: Deployment                                          # 1
metadata:
 name: sa-frontend
spec:
 replicas: 2                                             # 2
 minReadySeconds: 15
 strategy:
   type: RollingUpdate                                   # 3
   rollingUpdate:
     maxUnavailable: 1                                   # 4
     maxSurge: 1                                         # 5
 template:                                               # 6
   metadata:
     labels:
       app: sa-frontend                                  # 7
   spec:
     containers:
       - image: rinormaloku/sentiment-analysis-frontend
         imagePullPolicy: Always                         # 8
         name: sa-frontend
         ports:
           - containerPort: 80

 

#1 kind:部署;

#2 replicas:是部署 Spec 對象的一個屬性,定義了我們想運行多少的 Pod。所以是 2;

#3 type:指定從當前版本升遷到下個版本的時候,部署使用的策略。此處的策略 RollingUpdate 可以保證部署期間服務不間斷;

#4 maxUnavailable:是 RollingUpdate 對象的一個屬性,定義了在升級的時候,最大允許停止的 Pod 數量(與希望的狀態相比)。對我們的部署來說,我們有 2 個副本,這意味着在一個 Pod 停止后,我們還會有另外一個 Pod 運行,所以可以保證應用程序可訪問;

#5 maxSurge:是 RollingUpdate 對象的另一個屬性,定義了添加到部署的最大 Pod 數量(與希望的狀態相比)。對我們的部署來說,這意味着在向新版本遷移的時候,我們可以加一個 Pod,那么我們可以同時擁有個 3 個 Pod;

#6 template:指定 Pod 的模板,部署在創建新 Pod 的時候,會用到該模板。很可能這個非常相似的 Pod 會立即吸引你;

#7 app: sa-frontend:根據模板創建的 Pod 將被貼上該標簽;

#8 imagePullPolicy:當設置成 Always 的時候,每一次新部署都會重新獲取容器映像。

坦白來說,這一堆的文本讓我更糊塗了,所以還是讓我們來看個例子:

 

kubectl apply -f sa-frontend-deployment.yaml
deployment "sa-frontend" created

 

照例讓我們確認是否一切如約履行了:

 

kubectl get pods
NAME                           READY     STATUS    RESTARTS   AGE
sa-frontend                    1/1       Running   0          2d
sa-frontend-5d5987746c-ml6m4   1/1       Running   0          1m
sa-frontend-5d5987746c-mzsgg   1/1       Running   0          1m
sa-frontend2                   1/1       Running   0          2d

 

現在我們有 4 個運行中的 Pod,兩個是由部署創建的,而另外兩個是我們手動創建的。通過 kubectl delete pod <pod-name> 命令刪除其中一個手動創建的 Pod。

練習:刪除其中一個部署創建的 Pod,看看結果怎樣。在閱讀如下的解釋前,請先想想原因。

解釋:刪除一個 Pod 后,部署注意到當前的狀態(只有 1 個 Pod 在運行)與希望的狀態(2 個 Pod 處於運行狀態),所以它會再啟動一個 Pod。

那么,除了保持希望的狀態外,使用部署還有什么好處?讓我們先來看看好處。

好處 1:采用零停機時間部署(Zero-downtime)

產品經理帶着新的需求來找我們,說客戶想要在前端加一個綠色的按鈕。開發者寫好了代碼后,只需提供給我們一樣必須的東西,容器映像 rinormaloku/sentiment-analysis-frontend:green。然后就該我們了,我們需要采用零停機時間部署,這項工作很難嗎?讓我們試試看!

編輯 deploy-frontend-pods.yaml 文件,將容器映像改為新的映像:rinormaloku/sentiment-analysis-frontend:green。保存變更,並運行如下命令:

 

kubectl apply -f deploy-frontend-green-pods.yaml --record
deployment "sa-frontend" configured

 

讓我們通過如下命令檢查下上線的狀態:

 

kubectl rollout status deployment sa-frontend
Waiting for rollout to finish: 1 old replicas are pending termination...
Waiting for rollout to finish: 1 old replicas are pending termination...
Waiting for rollout to finish: 1 old replicas are pending termination...
Waiting for rollout to finish: 1 old replicas are pending termination...
Waiting for rollout to finish: 1 old replicas are pending termination...
Waiting for rollout to finish: 1 of 2 updated replicas are available...
deployment "sa-frontend" successfully rolled out

 

從部署上看來,上線已經成功。在這個過程中副本被逐個替換。意味着應用程序始終在線。在繼續下面的內容前,先讓我們來確認一下更新確實有效。

確認部署

讓我們在瀏覽器中確認更新的結果。運行我們之前用到過的命令 minikube service sa-frontend-lb,它會打開瀏覽器。我們可以看到按鈕 SEND 已更新了。

圖20:綠色按鈕

“RollingUpdate”背后的情況

在我們應用了新的部署后,Kubernetes 會將新狀態與舊的相比。在我們的例子中,新狀態需要兩個 rinormaloku/sentiment-analysis-frontend:green 映像的 Pod。這與當前的運行狀態不同,所以 Kubernetes 會執行 RollingUpdate。

圖21:RollingUpdate 替換 Pod

這里的 RollingUpdate 會根據我們指定的規格執行,也就是“maxUnavailable: 1″和“maxSurge: 1″。這意味着部署需要終止一個 Pod,並且僅可以運行一個新的 Pod。這個過程會不斷重復,一直到所有的 Pod被替換(如圖 21 所示)。

我們繼續介紹第二個好處。

聲明:出於娛樂的目的,下面的部分我按照小說的形式來書寫。

好處2:回滾到前一個狀態

產品經理跑進辦公室說,他遇到一個大麻煩!

產品經理大喊道:“產品環境中的應用程序有一個很關鍵的 bug!!需要馬上回滾到前一個版本”。

你冷靜地看着他,眼睛都沒有眨一下,就轉向了心愛的終端,然后開始敲:

 

kubectl rollout history deployment sa-frontend
deployments "sa-frontend"
REVISION  CHANGE-CAUSE
1         <none>        
2         kubectl.exe apply --filename=sa-frontend-deployment-green.yaml --record=true

 

你看了一眼前一個部署,然后問產品經理:“上個版本很多 bug,那前一個版本運行得很完美嗎?”

產品經理吼道:“是啊,你沒聽見我說嘛?!”

你沒理他,你知道該如何處理,於是你開始敲:

 

kubectl rollout undo deployment sa-frontend --to-revision=1
deployment "sa-frontend" rolled back

 

然后,你輕輕地刷新了頁面,之前的修改全都不見了!

產品經理瞠目結舌地看着你。

你拯救了大家!

我知道……這是個很無聊的故事。在 Kubernetes 出現之前,這個故事挺好的,更加戲劇化,讓人高度緊張,而且這種狀態持續了很長時間。那段舊時光還是很美好的!

大多數的命令都自帶說明,只是有一些細節你需要自己搞清楚。為什么第一個版本中字段 CHANGE-CAUSE 的值為 <none>,而同時第二次改版的時候,CHANGE-CAUSE 的值為“kubectl.exe apply –filename=sa-frontend-deployment-green.yaml –record=true”。

你應該可以發現這是因為在應用新的映像的時候,我們用到了標志符 --record。

在下一節中,我們將使用之前所有的概念,完成整個架構。

Kubernetes 和其他一切的實戰應用

現在我們學習了完成架構的所有必須的資源,因此這一節會非常快。圖 22 中灰色的部分是需要做的事情。讓我們從底部開始:部署 sa-logic 的部署。

圖 22:當前應用程序狀態

部署 SA-Logic

在終端中進入資源清單文件所在的目錄,然后運行如下命令:

 

kubectl apply -f sa-logic-deployment.yaml --record
deployment "sa-logic" created

 

SA-Logic 的部署會創建三個 Pod(Pod 上運行着我們的 Python 應用)。該命令還會給Pod 貼上 app: sa-logic 的標簽。有了這個標簽,我們就能從 SA-Logic 服務中利用選擇器來選擇這些 Pod。請花點時間打開 sa-logic-deployment.yaml,查看其內容。

這里的概念都是一樣的,因此我們可以直接講解下一個資源:SA-Logic 服務。

SA Logic 服務

首先來解釋下為什么需要該服務。我們的 Java 應用(在 SA-WebApp 部署的 Pod 中運行)依賴於 Python 應用提供的情感分析。但現在,與我們在本地運行一切服務時的狀況不同,我們並沒有一個單一的 Python 應用監聽着某個端口,我們只有兩個 Pod,如果需要,我們可以有更多的 Pod。

這就是為什么需要“服務”為一組提供相同功能的 Pod 提供訪問入口。這就是說,我們可以利用 SA-Logic 服務作為所有 SA-Logic Pod 的訪問入口。

運行如下命令:

 

kubectl apply -f service-sa-logic.yaml
service "sa-logic" created

 

更新后的應用程序狀態:現在我們有兩個 Pod 在運行(包含 Python 應用程序),並且 SA-Logic 服務提供了訪問入口,該訪問入口將在 SA-WebApp 的 Pod 中使用。

圖23:更新后的應用程序狀態

現在需要部署 SA-WebApp Pod,我們需要用到部署資源。

SA-WebApp 部署

我們已經學過了部署,盡管這個部署會用到更多的特性。打開 sa-web-app-deployment.yaml 文件,會發現以下的新內容:

 

- image: rinormaloku/sentiment-analysis-web-app
 imagePullPolicy: Always
 name: sa-web-app
 env:
   - name: SA_LOGIC_API_URL
     value: "http://sa-logic"
 ports:
   - containerPort: 8080

 

我們感興趣的第一件事就是 env 屬性。我們猜測它定義了環境變量 SA_LOGIC_API_URl,值為在 Pod 內的值為 http://sa-logic。但為什么要初始化成 http://sa-logic,sa-logic 究竟是什么?

我們先來介紹下 kube-dns。

KUBE-DNS

Kubernetes 有個特殊的 Pod 叫做 kube-dns。默認情況下,所有 Pod 都用它作為 DNS 服務器。kube-dns 的一個重要屬性就是它為每個建立的訪問都創建一條 DNS 記錄。

這就是說當我們創建 sa-logic 服務時,它會獲得一個 IP 地址。它的名字會加入到 kube-dns 中(和它的 IP 地址一起)。這樣所有 Pod 都能夠把 sa-logic 翻譯成 SA-Logic 服務的 IP 地址。

好,現在可以繼續了:

SA WebApp 部署(續)

運行以下命令:

 

kubectl apply -f sa-web-app-deployment.yaml --record
deployment "sa-web-app" created

 

完了。剩下的工作就是通過 LoadBalancer 服務將 SA-WebApp Pod 暴露到外部。LoadBalancer 服務提供了 SA-WebApp Pod 的訪問入口,這樣 React 應用程序就能發送 HTTP 請求了。

SA-WebApp 服務

打開 service-sa-web-app-lb.yaml 文件,可以看到內容還是挺熟悉的。

所以我們可以運行如下命令:

 

kubectl apply -f service-sa-web-app-lb.yaml
service "sa-web-app-lb" created

 

這樣架構就完成了。但還有一點不完美的地方。在部署 SA-Frontend Pod 之后,容器映像指向了 http://localhost:8080/sentiment 處的 SA-WebApp。但現在我們需要將其更新為 SA-WebApp LoadBalancer 的 IP 地址(其作用是 SA-WebApp Pod 的訪問入口)。

修補該不完美是個快速復習一切的絕佳機會(如果能不參照以下的指南獨立完成更好)。下面我們開始:

  • 執行下列命令獲取 SA-WebApp LoadBalancer 的 IP:

 

minikube service list
|-------------|----------------------|-----------------------------|
|  NAMESPACE  |         NAME         |             URL             |
|-------------|----------------------|-----------------------------|
| default     | kubernetes           | No node port                |
| default     | sa-frontend-lb       | http://192.168.99.100:30708 |
| default     | sa-logic             | No node port                |
| default     | sa-web-app-lb        | http://192.168.99.100:31691 |
| kube-system | kube-dns             | No node port                |
| kube-system | kubernetes-dashboard | http://192.168.99.100:30000 |
|-------------|----------------------|-----------------------------|

 

  • 在 sa-frontend/src/App.js 中使用 SA-WebApp LoadBalancer 的 IP,如下:

 

analyzeSentence() {
       fetch('http://192.168.99.100:31691/sentiment', { /* shortened for brevity */})
           .then(response => response.json())
           .then(data => this.setState(data));
   }

 

 構建靜態文件 npm build (需要先切換到 sa-front-end 目錄);

構建容器映像:

 

docker build -f Dockerfile -t $DOCKER_USER_ID/sentiment-analysis-frontend:minikube .

 

  • 將映像推送到 Docker hub:

 

docker push $DOCKER_USER_ID/sentiment-analysis-frontend:minikube

 

  • 編輯 sa-frontend-deployment.yaml 並使用新的映像;

  • 執行 kubectl apply -f sa-frontend-deployment.yaml 命令。

刷新瀏覽器(如果你關閉了瀏覽器,則執行 minikube service sa-frontend-lb)。敲個句子試試看!

全文總結

Kubernetes 對團隊、項目都很有好處,它能簡化部署,提供伸縮性、靈活性,可以讓我們使用任何底層基礎設施。以后我們叫它 Supernetes 吧!

本文中覆蓋的內容:

  • 構建/打包/運行 ReactJS、Java 和 Python 應用程序;

  • Docker容器,如何利用 Dockerfile 定義並構建容器;

  • 容器注冊目錄,我們采用 Docker Hub 作為容器的代碼庫;

  • 介紹了 Kubernetes 的最重要的內容;

  • Pod;

  • 服務;

  • 部署;

  • 新概念,如零停機時間部署;

  • 創建可伸縮的應用;

  • 流程上,我們將整個微服務應用程序轉成了 Kubernetes 集群。

本文為你提供了堅實的基礎供你在實際的項目中使用,並且幫你更容易地學習更多新概念。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM