Docker&K8s學習
Docker主要包括三部分
**Image **鏡像
一種特殊的文件系統,它除了提供容器運行時所需的程序、庫、資源、配置等文件外,還包含了一些為運行時準備的一些配置參數(例如環境變量)。鏡像不包含任何動態數據,其內容在構建之后也不會被改變。
Dockerfile制作說明
https://docs.docker.com/engine/reference/builder/
Container 容器
運行鏡像服務
Repository 倉庫
托管公共鏡像
Docker和虛擬機的區別
K8S介紹
Kubernetes,就是基于容器的集群管理平臺
就在Docker容器技術被炒得熱火朝天之時,大家發現,如果想要將Docker應用于具體的業務實現,是存在困難的——編排、管理和調度等各個方面,都不容易。于是,人們迫切需要一套管理系統,對Docker及容器進行更高級更靈活的管理。
K8s架構
Kubernetes 中部署的最小單位是 pod,而不是 Docker 容器。實現上 Kubernetes 是不依賴于 Docker 的,完全可以使用其他的容器引擎在 Kubernetes 管理的集群中替代 Docker。在與 Docker 結合使用時,一個 pod 中可以包含一個或多個 Docker 容器。但除了有緊密耦合的情況下,通常一個 pod 中只有一個容器,這樣方便不同的服務各自獨立地擴展。
使用Docker&K8S
Docker安裝
首先快速地介紹一下 Docker:作為示例,我們在本地啟動 Docker 的守護進程,并在一個容器里運行簡單的 HTTP 服務。先完成安裝:
$ brew cask install docker
上面的命令會從 Homebrew 安裝 Docker for Mac,它包含 Docker 的后臺進程和命令行工具。Docker 的后臺進程以一個 Mac App 的形式安裝在 /Applications 里,需要手動啟動。啟動 Docker 應用后,可以在 Terminal 里確認一下命令行工具的版本:
$ docker --version
Docker version 18.03.1-ce, build 9ee9f40
上面顯示的 Docker 版本可能和我的不一樣,但只要不是太老就好。
編寫Dockerfile
我們建一個單獨的目錄來存放示例所需的文件。為了盡量簡化例子,我們要部署的服務是用 Nginx 來 serve 一個簡單的 HTML 文件 html/index.html
。
$ mkdir docker-demo
$ cd docker-demo
$ mkdir html
$ echo '<h1>Hello Docker!</h1>' > html/index.html
接下來在當前目錄創建一個叫 Dockerfile 的新文件,包含下面的內容:
FROM nginx
COPY html/* /usr/share/nginx/html
每個 Dockerfile 都以 FROM ...
開頭。FROM nginx
的意思是以 Nginx 官方提供的鏡像為基礎來構建我們的鏡像。在構建時,Docker 會從 Docker Hub 查找和下載需要的鏡像。Docker Hub 對于 Docker 鏡像的作用就像 GitHub 對于代碼的作用一樣,它是一個托管和共享鏡像的服務。使用過和構建的鏡像都會被緩存在本地。第二行把我們的靜態文件復制到鏡像的 /usr/share/nginx/html
目錄下。也就是 Nginx 尋找靜態文件的目錄。Dockerfile 包含構建鏡像的指令,更詳細的信息可以參考這里。
Dockerfile編寫可以參考https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
構建Docker鏡像
$ docker build -t docker-demo:0.1 .
請確保你按照上面的步驟為這個實驗新建了目錄,并且在這個目錄中運行 docker build
。如果你在其它有很多文件的目錄(比如你的用戶目錄或者 /tmp
)運行,docker 會把當前目錄的所有文件作為上下文發送給負責構建的后臺進程。
這行命令中的名稱 docker-demo
可以理解為這個鏡像對應的應用名或服務名,0.1
是標簽。Docker 通過名稱和標簽的組合來標識鏡像。
查看Docker鏡像
可以用下面的命令來看到剛剛創建的鏡像:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-demo 0.1 efb8ca048d5a 5 minutes ago 109MB
運行Docker鏡像
下面我們把這個鏡像運行起來。Nginx 默認監聽在 80 端口,所以我們把宿主機的 8080 端口映射到容器的 80 端口:
$ docker run --name docker-demo -d -p 8080:80 docker-demo:0.1
查看容器運行狀態
用下面的命令可以看到正在運行中的容器:
$ docker container ps
CONTAINER ID IMAGE ... PORTS NAMES
c495a7ccf1c7 docker-demo:0.1 ... 0.0.0.0:8080->80/tcp docker-demo
這時如果你用瀏覽器訪問 http://localhost:8080,就能看到我們剛才創建的「Hello Docker!」頁面。
在現實的生產環境中 Docker 本身是一個相對底層的容器引擎,在有很多服務器的集群中,不太可能以上面的方式來管理任務和資源。所以我們需要 Kubernetes 這樣的系統來進行任務的編排和調度。在進入下一步前,別忘了把實驗用的容器清理掉:
$ docker container stop docker-demo
$ docker container rm docker-demo
==============
安裝 Kubernetes
介紹完 Docker,終于可以開始試試 Kubernetes 了。我們需要安裝三樣東西:Kubernetes 的命令行客戶端 kubctl、一個可以在本地跑起來的 Kubernetes 環境 Minikube、以及給 Minikube 使用的虛擬化引擎 xhyve。
$ brew install kubectl
$ brew cask install minikube
$ brew install docker-machine-driver-xhyve
Minikube 默認的虛擬化引擎是 VirtualBox,而 xhyve 是一個更輕量、性能更好的替代。它需要以 root 權限運行,所以安裝完要把所有者改為 root:wheel
,并把 setuid 權限打開:
$ sudo chown root:wheel /usr/local/opt/docker-machine-driver-xhyve/bin/docker-machine-driver-xhyve
$ sudo chmod u+s /usr/local/opt/docker-machine-driver-xhyve/bin/docker-machine-driver-xhyve
然后就可以啟動 Minikube 了:
$ minikube start --vm-driver xhyve
你多半會看到一個警告說 xhyve 會在未來的版本被 hyperkit 替代,推薦使用 hyperkit。不過在我寫這個教程的時候 docker-machine-driver-hyperkit 還沒有進入 Homebrew, 需要手動編譯和安裝,我就偷個懶,仍然用 xhyve。以后只要在安裝和運行的命令中把 xhyve 改為 hyperkit 就可以。
如果你在第一次啟動 Minikube 時遇到錯誤或被中斷,后面重試仍然失敗時,可以嘗試運行 minikube delete
把集群刪除,重新來過。
Minikube 啟動時會自動配置 kubectl,把它指向 Minikube 提供的 Kubernetes API 服務。可以用下面的命令確認:
$ kubectl config current-context
minikube
Kubernetes 架構簡介
典型的 Kubernetes 集群包含一個 master 和很多 node。Master 是控制集群的中心,node 是提供 CPU、內存和存儲資源的節點。Master 上運行著多個進程,包括面向用戶的 API 服務、負責維護集群狀態的 Controller Manager、負責調度任務的 Scheduler 等。每個 node 上運行著維護 node 狀態并和 master 通信的 kubelet,以及實現集群網絡服務的 kube-proxy。
作為一個開發和測試的環境,Minikube 會建立一個有一個 node 的集群,用下面的命令可以看到:
$ kubectl get nodes
NAME STATUS AGE VERSION
minikube Ready 1h v1.10.0
部署一個單實例服務
我們先嘗試像文章開始介紹 Docker 時一樣,部署一個簡單的服務。Kubernetes 中部署的最小單位是 pod,而不是 Docker 容器。實時上 Kubernetes 是不依賴于 Docker 的,完全可以使用其他的容器引擎在 Kubernetes 管理的集群中替代 Docker。在與 Docker 結合使用時,一個 pod 中可以包含一個或多個 Docker 容器。但除了有緊密耦合的情況下,通常一個 pod 中只有一個容器,這樣方便不同的服務各自獨立地擴展。
Minikube 自帶了 Docker 引擎,所以我們需要重新配置客戶端,讓 docker 命令行與 Minikube 中的 Docker 進程通訊:
$ eval $(minikube docker-env)
在運行上面的命令后,再運行 docker image ls
時只能看到一些 Minikube 自帶的鏡像,就看不到我們剛才構建的 docker-demo:0.1 鏡像了。所以在繼續之前,要重新構建一遍我們的鏡像,這里順便改一下名字,叫它 k8s-demo:0.1。
$ docker build -t k8s-demo:0.1 .
創建Pod定義文件
然后創建一個叫 pod.yml 的定義文件:
apiVersion: v1
kind: Pod
metadata:
name: k8s-demo
spec:
containers:
- name: k8s-demo
image: k8s-demo:0.1
ports:
- containerPort: 80
這里定義了一個叫 k8s-demo 的 Pod,使用我們剛才構建的 k8s-demo:0.1 鏡像。這個文件也告訴 Kubernetes 容器內的進程會監聽 80 端口。然后把它跑起來:
$ kubectl create -f pod.yml
pod "k8s-demo" created
kubectl 把這個文件提交給 Kubernetes API 服務,然后 Kubernetes Master 會按照要求把 Pod 分配到 node 上。用下面的命令可以看到這個新建的 Pod:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
k8s-demo 1/1 Running 0 5s
因為我們的鏡像在本地,并且這個服務也很簡單,所以運行 kubectl get pods
的時候 STATUS 已經是 running。要是使用遠程鏡像(比如 Docker Hub 上的鏡像),你看到的狀態可能不是 Running,就需要再等待一下。
雖然這個 pod 在運行,但是我們是無法像之前測試 Docker 時一樣用瀏覽器訪問它運行的服務的。可以理解為 pod 都運行在一個內網,我們無法從外部直接訪問。要把服務暴露出來,我們需要創建一個 Service。Service 的作用有點像建立了一個反向代理和負載均衡器,負責把請求分發給后面的 pod。
創建Service定義文件(類似于反向代理)
創建一個 Service 的定義文件 svc.yml:
apiVersion: v1
kind: Service
metadata:
name: k8s-demo-svc
labels:
app: k8s-demo
spec:
type: NodePort
ports:
- port: 80
nodePort: 30050
selector:
app: k8s-demo
這個 service 會把容器的 80 端口從 node 的 30050 端口暴露出來。注意文件最后兩行的 selector 部分,這里決定了請求會被發送給集群里的哪些 pod。這里的定義是所有包含「app: k8s-demo」這個標簽的 pod。然而我們之前部署的 pod 并沒有設置標簽:
$ kubectl describe pods | grep Labels
Labels: <none>
所以要先更新一下 pod.yml,把標簽加上(注意在 metadata:
下增加了 labels
部分):
apiVersion: v1
kind: Pod
metadata:
name: k8s-demo
labels:
app: k8s-demo
spec:
containers:
- name: k8s-demo
image: k8s-demo:0.1
ports:
- containerPort: 80
然后更新 pod 并確認成功新增了標簽:
$ kubectl apply -f pod.yml
pod "k8s-demo" configured
$ kubectl describe pods | grep Labels
Labels: app=k8s-demo
然后就可以創建這個 service 了:
$ kubectl create -f svc.yml
service "k8s-demo-svc" created
用下面的命令可以得到暴露出來的 URL,在瀏覽器里訪問,就能看到我們之前創建的網頁了。
$ minikube service k8s-demo-svc --url
http://192.168.64.4:30050
橫向擴展、滾動更新、版本回滾
在這一節,我們來實驗一下在一個高可用服務的生產環境會常用到的一些操作。在繼續之前,先把剛才部署的 pod 刪除(但是保留 service,下面還會用到):
$ kubectl delete pod k8s-demo
pod "k8s-demo" deleted
在正式環境中我們需要讓一個服務不受單個節點故障的影響,并且還要根據負載變化動態調整節點數量,所以不可能像上面一樣逐個管理 pod。Kubernetes 的用戶通常是用 Deployment 來管理服務的。一個 deployment 可以創建指定數量的 pod 部署到各個 node 上,并可完成更新、回滾等操作。
創建Deployment定義文件
首先我們創建一個定義文件 deployment.yml:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: k8s-demo-deployment
spec:
replicas: 10
template:
metadata:
labels:
app: k8s-demo
spec:
containers:
- name: k8s-demo-pod
image: k8s-demo:0.1
ports:
- containerPort: 80
注意開始的 apiVersion
和之前不一樣,因為 Deployment API 沒有包含在 v1 里,replicas: 10
指定了這個 deployment 要有 10 個 pod,后面的部分和之前的 pod 定義類似。提交這個文件,創建一個 deployment:
$ kubectl create -f deployment.yml
deployment "k8s-demo-deployment" created
用下面的命令可以看到這個 deployment 的副本集(replica set),有 10 個 pod 在運行。
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
k8s-demo-deployment-774878f86f 10 10 10 19s
假設我們對項目做了一些改動,要發布一個新版本。這里作為示例,我們只把 HTML 文件的內容改一下, 然后構建一個新版鏡像 k8s-demo:0.2:
$ echo '<h1>Hello Kubernetes!</h1>' > html/index.html
$ docker build -t k8s-demo:0.2 .
然后更新 deployment.yml:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: k8s-demo-deployment
spec:
replicas: 10
minReadySeconds: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
app: k8s-demo
spec:
containers:
- name: k8s-demo-pod
image: k8s-demo:0.2
ports:
- containerPort: 80
這里有兩個改動,第一個是更新了鏡像版本號 image: k8s-demo:0.2
,第二是增加了 minReadySeconds: 10
和 strategy
部分。新增的部分定義了更新策略:minReadySeconds: 10
指在更新了一個 pod 后,需要在它進入正常狀態后 10 秒再更新下一個 pod;maxUnavailable: 1
指同時處于不可用狀態的 pod 不能超過一個;maxSurge: 1
指多余的 pod 不能超過一個。這樣 Kubernetes 就會逐個替換 service 后面的 pod。運行下面的命令開始更新:
$ kubectl apply -f deployment.yml --record=true
deployment "k8s-demo-deployment" configured
這里的 --record=true
讓 Kubernetes 把這行命令記到發布歷史中備查。這時可以馬上運行下面的命令查看各個 pod 的狀態:
$ kubectl get pods
NAME READY STATUS ... AGE
k8s-demo-deployment-774878f86f-5wnf4 1/1 Running ... 7m
k8s-demo-deployment-774878f86f-6kgjp 0/1 Terminating ... 7m
k8s-demo-deployment-774878f86f-8wpd8 1/1 Running ... 7m
k8s-demo-deployment-774878f86f-hpmc5 1/1 Running ... 7m
k8s-demo-deployment-774878f86f-rd5xw 1/1 Running ... 7m
k8s-demo-deployment-774878f86f-wsztw 1/1 Running ... 7m
k8s-demo-deployment-86dbd79ff6-7xcxg 1/1 Running ... 14s
k8s-demo-deployment-86dbd79ff6-bmvd7 1/1 Running ... 1s
k8s-demo-deployment-86dbd79ff6-hsjx5 1/1 Running ... 26s
k8s-demo-deployment-86dbd79ff6-mkn27 1/1 Running ... 14s
k8s-demo-deployment-86dbd79ff6-pkmlt 1/1 Running ... 1s
k8s-demo-deployment-86dbd79ff6-thh66 1/1 Running ... 26s
從 AGE 列就能看到有一部分 pod 是剛剛新建的,有的 pod 則還是老的。下面的命令可以顯示發布的實時狀態:
$ kubectl rollout status deployment k8s-demo-deployment
Waiting for rollout to finish: 1 old replicas are pending termination...
Waiting for rollout to finish: 1 old replicas are pending termination...
deployment "k8s-demo-deployment" successfully rolled out
由于我輸入得比較晚,發布已經快要結束,所以只有三行輸出。下面的命令可以查看發布歷史,因為第二次發布使用了 --record=true
所以可以看到用于發布的命令。
$ kubectl rollout history deployment k8s-demo-deployment
deployments "k8s-demo-deployment"
REVISION CHANGE-CAUSE
1 <none>
2 kubectl apply --filename=deploy.yml --record=true
這時如果刷新瀏覽器,就可以看到更新的內容「Hello Kubernetes!」。假設新版發布后,我們發現有嚴重的 bug,需要馬上回滾到上個版本,可以用這個很簡單的操作:
$ kubectl rollout undo deployment k8s-demo-deployment --to-revision=1
deployment "k8s-demo-deployment" rolled back
Kubernetes 會按照既定的策略替換各個 pod,與發布新版本類似,只是這次是用老版本替換新版本:
$ kubectl rollout status deployment k8s-demo-deployment
Waiting for rollout to finish: 4 out of 10 new replicas have been updated...
Waiting for rollout to finish: 6 out of 10 new replicas have been updated...
Waiting for rollout to finish: 8 out of 10 new replicas have been updated...
Waiting for rollout to finish: 1 old replicas are pending termination...
deployment "k8s-demo-deployment" successfully rolled out
在回滾結束之后,刷新瀏覽器就可以確認網頁內容又改回了「Hello Docker!」。
參考資料