前端領域中 Docker 與 Kubernetes的實踐姿勢

前端領域中 Docker 與 Kubernetes的實踐姿勢

看完本文希望讀者能夠了解到,Docker 的基本原理,Kubernetes 是怎麼工作的, 對於前端 Kubernetes 有哪些優勢與玩法。

Docker 和傳統部署方式最大的不同在於,它將不會限制我們使用任何工具,任何語言,任何版本的 runtime,Docker 將我們的應用看成一個只提供網絡服務的盒子(也即容器),Kubernetes 則是對這些盒子進行更多自動化的操作,自動創建,自動重啟,自動擴容,自動調度,這個過程稱之為容器編排。

在今天,容器編排技術給 Web 應用帶來了巨大的靈活性,讓我們輕鬆創建需要的程序對外提供服務。和傳統的 IaaS 相比,不需要去關心雲主機申請,雲主機配置等信息,也不需考慮雲主機故障導致的服務不可用,由 Kubernetes 的副本控制器幫我們完成雲主機故障發生後容器遷移。

本篇文章和大家一起,回顧一下從 Docker 到 Kubernetes 的一些相關內容,最後再看看 Kubernetes 在前端領域有哪些優勢和新玩法。

Docker 安裝

  • Linux Debian/Ubuntu, 安裝 社區版DockerCE
  • Windows 一鍵安裝

如果是 Windows10, Windows7 將會使用 VirtualBox 安裝 Linux 作為 Docker 的宿主機。
Windows10 Pro 會使用 Hyper-V 安裝 Linux 作為 Docker 的宿主機。

  • macOS 一鍵安裝

Docker 基本信息

默認 Docker 存儲位置為 /var/lib/docker,所有的鏡像,容器,卷都會在這裡,如果你使用了多硬盤,或者掛載了 SSD 不在 / 上,需要修改默認路徑(graph)到合適位置,配置文件為 /etc/docker/daemon.json, 例如

{
"bip": "192.168.0.1/16",
"graph": "/mnt/ssd/0/docker"
}

Docker 在安裝過程中會自動創建好 docker0 網卡,並分配 ip 給他。
上面指定的 bip 是指定了 docker0 網卡的 ip , 如果不指定那麼在創建 docker0 時會自動根據主機 ip 選取一個合適的 ip,不過由於網絡的複雜性,特別是機房網絡內很容易發現地址選取衝突,這時候就需要手動指定 bip 為一個合適的值。docker 的 ip 選取規則這篇文章分析的很好, 可以參考 https://blog.csdn.net/longxin... 。

安裝並啟動後可以通過 docker info 查看Docker的一些配置信息。

Docker hello world

Docker 檢查安裝是否正常的第一個測試命令很簡單。

docker run hello-world

首先他會去 Docker Hub 上下載 hello-world 這個鏡像,然後在本地運行這個鏡像,啟動後的這個 Docker 服務稱之為容器。容器創建後就會執行規定的入口程序,程序執行向流中輸出了一些信息後退出,容器也會隨著這個入口程序的結束而結束。

  • 查看所有容器
docker ps -a

輸出如下:

cf9a6bc212f9 hello-world "/hello" 28 hours ago Exited (0) 3 min

第一列為容器 id, 很多針對容器的操作都需要這個 id, 例如下面一些常用的操作。

docker rm container_id
docker stop container_id
docker start container_id
docker describe container_id

這裡有個docker start container_id, 啟動一個容器,說明容器即使退出後其資源依然存在,還可以使用docker start重啟這個容器。要想讓容器退出後自動刪除可以在docker run時指定--rm參數。

當我們運行這個命令時 Docker 會去下載 hello-world 這個鏡像緩存到本地,這樣當下次再運行這條命令時就不需要去源中下載。

  • 查看本地鏡像
docker images

運行 Nginx

Nginx 作為使用廣泛的 Web 服務器在 Docker 世界裡也是同樣流行, 常常用來啟動一個網絡服務驗證網絡配置情況, 使用下面這條命令啟動 Nginx 容器 docker run --rm -p 80:80 nginx。

訪問 localhost:80 端口即可看到 Nginx 服務啟動, 控制檯中可以看到 Nginx 服務的日誌輸出。

因為 Docker 內的網絡與外部世界是隔離的,所以我們需要手動指定端口轉發 -p 80:80 來顯式將宿主機的80(前)轉發到容器的80端口, 暴露端口是我們提供服務最常用的使用方式之一。 也有一些其他類型的服務,例如日誌處理,數據收集需要共享數據卷才能提供服務,所有這些都需要我們在啟動容器時顯式指定。

前端領域中 Docker 與 Kubernetes的實踐姿勢

一些常見的啟動參數:

  • -p 本機端口:容器端口 映射本地端口到容器
  • -P 將容器端口映射為本機隨機端口
  • -v 本地路徑或卷名:容器路徑 將本地路徑或者數據卷掛載到容器的指定位置
  • -it 作為交互式命令啟動
  • -d 將容器放在後臺運行
  • --rm 容器退出後清除資源

Docker是如何工作的

Docker 的底層核心原理是利用了 Linux 內核的 namespace 以及 cgroup 特性,其中 namespace 進行資源隔離,cgroup 進行資源配額, 其中 Linux 內核中一共有 6 種 namespace,分別對應如下。

Namespace系統調用函數隔離內容UTSCLONE_NEWUTS主機與域名IPCCLONE_NEWIPC信號量、消息隊列和共享內存PIDCLONE_NEWPID進程編號NetworkCLONE_NEWNET網絡設備、網絡棧、端口等MountCLONE_NEWNS掛載點(文件系統)UserCLONE_NEWUSER用戶和用戶組

在系統調用中有三個與namespace有關的函數:

  1. clone http://man7.org/linux/man-pag...

如果我想讓子進程擁有獨立的網絡地址,TCP/IP 協議棧,可以下面這樣指定。

clone(cb, *stack , CLONE_NEWNET, 0)
  1. unshare http://man7.org/linux/man-pag...

將當前進程轉移到新的 namespace 中, 例如使用 fork 或 vfork 創建的進程將默認共享父級資源,使用 unshare 將子進程從父級取消共享。

  1. setns http://man7.org/linux/man-pag...

給指定的PID指定 namespace, 通常用於共享 namespace。

Linux 在內核層支持了在系統調用中隔離 namespace, 通過給一個進程分配單獨的 namespace 從而讓其在各個資源維度進行隔離,每個進程都能獲取到自己的主機名,IPC, PID, IP, 根文件系統,用戶組等信息,就像在一個獨佔系統中,不過雖然資源進行了隔離,但是內核還是共享同一個,這也是比傳統虛擬機輕量的原因之一。

另外只有資源進行隔離還不夠,要想保證真正的故障隔離,互不影響, 還需要對針對 CPU, 內存,GPU 等進行限制,因為如果一個程序出現死循環或者內存洩露也會導致別的程序無法運行。 資源配額是使用內核的 cgroup 特性來完成,想了解細節的同學可以參考: https://www.cnblogs.com/sammy... 。


(另外強烈推薦在 Linux 4.9 以上的內核跑容器,Linux 3.x 中有已知內核不穩定導致主機重啟的問題)

Docker 網絡

一個容器要想提供服務,就需要將自身的網絡暴露出去。Docker 是與宿主機上的環境是隔離的,要想暴露服務就需要顯示告訴 Docker 哪些端口允許外部訪問,在運行 docker run -p 80:80 nginx 時這裡就是將容器內部的 80 端口暴露到宿主機的 80 端口上,具體的端口轉發下面會具體分析一下。容器的網絡部分是容器中最重要的部分,也是構建大型集群的基石,在我們部署 Docker 的應用時,需要要對網絡有個基本的瞭解。

Docker 提供了四種網絡模式,分別為 Host, Container, None, Bridge 使用 --net 進行指定

Host 模式:

docker run --net host nginx

Host 模式不會單獨為容器創建 network namespace, 容器內部直接使用宿主機網卡,此時容器內獲取 ip 為宿主機 ip,端口綁定直接綁在宿主機網卡上,優點是網絡傳輸時不用經過 NAT 轉換,效率更高速度更快。

Container 模式:

docker run --net container:xxx_containerid nginx

和指定的 container 共享 network namespace, 共享網絡配置,ip 地址和端口,其中無法共享網絡模式為 Host 的容器。

None 模式:

docker run --net none busybox ifconfig

指定為 None 模式的容器內將不會分配網卡設備,僅有內部 lo 網絡。

前端領域中 Docker 與 Kubernetes的實踐姿勢

Bridge 模式

docekr run --net bridge busybox ifconfig
前端領域中 Docker 與 Kubernetes的實踐姿勢

該模式為默認模式,容器啟動時會被分配一個單獨的 network namespace,同時 Docker 在安裝/初始化時會在宿主機上創建一個名為 docker0 的網橋,該網橋也作為容器的默認網關,容器網絡會在該網關網段內進行 ip 的分配。

當我執行 docker run -p 3000:80 nginx 時,Docker 會在宿主機上創建下面一條 iptable 轉發規則。

前端領域中 Docker 與 Kubernetes的實踐姿勢

最底下的規則顯示當外部請求主機網卡 3000 端口時將它進行目的地址轉換(DNAT), 目的地址修改為 172.18.0.2,端口修改為 80,修改好目的地址後流量會從本機默認網卡經過 docker0 轉發到對應的容器,這樣當外部請求宿主機的 3000 端口,內部會將流量轉發給內部容器服務,從而實現服務的暴露。

前端領域中 Docker 與 Kubernetes的實踐姿勢

同樣 Docker 內部訪問外部接口也會進行源地址轉換(SNAT), 容器內部請求 google.com, 服務器上收到的將是主機網卡的 ip。

前端領域中 Docker 與 Kubernetes的實踐姿勢

Bridge 模式由於多了一層 NAT 轉換所以效率會比 Host 模式差一些,但是能夠很好的隔離外部網絡環境,讓容器獨享 ip 且具有完整的端口空間。

上面四種網絡模式是 Docker 自帶的幾種工作方式,但是部署 Kubernetes 需要所有的容器都工作在一個局域網中,所以在部署集群時需要多主機網絡插件的支持。

Flannel

多主機網絡解決方案有 CNCF 推出的 CNI 規範以及 Docker 自帶的 CNM 方案,但是目前大家用的最多的還是 CNI 規範,其中一種實現就是 Flannel。

Flannel 使用了報文嵌套技術來解決多主機網絡互通問題,將原始報文進行封包,指定包ip為目的主機地址,等包到達主機後再進行拆包傳送到對應的容器。下圖顯示 flannel 使用效率更高的 UDP 協議來在主機間傳輸報文。

前端領域中 Docker 與 Kubernetes的實踐姿勢

目前主流跨主機通信目前常用的有三種,各有優缺,視場景選擇:

  • overlay, 即上面的報文嵌套。
  • hostgw 通過修改主機路由表實現轉發,不需要拆包和封包,效率更高,但同樣限制比較多,只適合在相同局域網中的主機使用。
  • 使用軟件實現的 BGP(邊界網關協議)以此向網絡中的路由器廣播路由規則。和 hostgw 一樣不需要拆包,但是實現成本較高。

有了CNI才能在此基礎上構建 Kubernetes 集群。

Kubernetes 介紹

在小規模場景下使用 Docker 可以一鍵部署應用確實很方便,達到了一鍵部署的目的,但是當出現需要在幾百臺主機上進行多副本部署,需要管理這麼多主機的運行狀態以及服務的故障時需要在其他主機重啟服務,想象一下就知道手動的方式不是一種可取的方案,這時候就需要利用 Kubernetes 這種更高維度的編排工具來管理了。Kubernetes 簡稱 K8S, 簡單說 K8S 就是抽象了硬件資源,將 N 臺物理機或雲主機抽象成一個資源池,容器的調度交給 K8S 就像親媽一樣照顧我們的容器,CPU 不夠用就調度到一臺足夠使用的機器上,內存不滿足要求就會尋找一臺有足夠內存的機器在上面創建對應的容器,服務因為某些原因掛了, K8S 還會幫我們自動遷移重啟, 簡直無微不至,至尊享受。我們作為開發者只關心自己的代碼,應用的健康由 K8S 保證。

這裡就不介紹具體的安裝方式了,如果使用 Windows 或者 MacOS 可以直接使用 Docker Desktop 下的 Kubernetes 選項一鍵安裝單主機集群,也可以使用 kind 工具 在本地模擬多集群 K8S。

K8S 調度的基本單位為 pod, 一個 pod 表示一個或多個容器。引用一本書裡所說

之所以沒有使用容器作為調度單位,是因為單一的容器沒有構成服務的概念;例如 Web 應用做了前後端分例,需要一個 NodeJS 與 Tomcat 才能組成一個完整的服務,這樣就需要部署兩個容器來實現一個完整的服務,雖然也可以把他們都放到一個容器裡,但這顯然違反了一個容器即一個進程的核心思想 --《Service Mesh實戰 - 用 istio軟負載實現服務網格》

K8S 與傳統 IaaS 系統的不同:

IaaS 就是 Infrastructure as a service, 所謂基礎設施即服務,開發者想要上線一個新應用需要申請主機,ip, 域名等一系列資源,然後登錄主機自行搭建所需環境,部署應用上線,這樣不僅不利於大規模操作,而且還增加了出錯的可能,運維或開發這常常自己寫腳本自動化完成,遇到一些差異再手動修改腳本,非常痛苦。

K8S 則是將基礎設施可編程化,由原來的人工申請改為一個清單文件自動創建,開發者只需要提交一份文件,K8S 將會自動為你分配創建所需的資源。對這些設施的 CRUD 都可以通過程序的方式自動化操作。

為了瞭解 K8S 的基礎概念,下面來部署一個 Node SSR 應用:

初始化應用模板

npm install create-next-app
npx create-next-app next-app
cd next-app

創建好工程後給添加一個 Dockerfile 用來構建服務的鏡像

Dockerfile

FROM node:8.16.1-slim as build

COPY ./ /app

WORKDIR /app
RUN npm install
RUN npm run build
RUN rm -rf .git


FROM node:8.16.1-slim

COPY --from=build /app /

EXPOSE 3000
WORKDIR /app

CMD ["npm", "start"]

這個 Dockerfile 做了兩部分優化

  1. 使用精簡版的 node 基礎鏡像, 大大減少鏡像體積
  2. 使用分步構建的方式, 能夠減少鏡像層數以及移除臨時文件從而減少了鏡像體積。

構建鏡像

docker build . --tag next-app

之後我們就可以向 Kubernetes 提出我們應用的要求了。為了保證高可用,服務至少創建兩個副本,我們還需要一個應用的域名當這個域名請求到我們集群上時自動轉發到我們的服務上。那麼我們對應的配置文件就可以這麼寫

Deployment.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: app-ingress
spec:
rules:
- host: next-app-server
http:
paths:
- backend:
serviceName: app-service
servicePort: 80

---
kind: Service
apiVersion: v1
metadata:
name: app-service
spec:
selector:
app: web
ports:
- port: 80
targetPort: 3000

---

apiVersion: apps/v1
kind: Deployment
metadata:
name: app-deployment
spec:

replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- image: next-app
name: next-app
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000

上面這個清單告訴 K8S:

  • 首先需要一個 Deployment 控制器,鏡像為 next-app, 服務端口為 3000,給我創建兩個副本。
  • 還需要創建一個 Service, 這個 Service 指向由副本控制器創建的幾個 next-app。
  • 申請一個 Ingress 入口, 域名為 next-app-server, 其指向剛剛的 Service。

提交這份申請給 K8S。

kubectl apply -f ./Deployment.yaml
前端領域中 Docker 與 Kubernetes的實踐姿勢

接著就可以看到已經部署的 pod。

sh-4.4$ kubectl get pod
NAME READY STATUS RESTARTS AGE
app-deployment-594c48dbdb-4f4cg 1/1 Running 0 1m
app-deployment-594c48dbdb-snj54 1/1 Running 0 1m

然後瀏覽器打開 Ingress 裡配置的域名即可訪問對應的應用(前提是這個域名能夠打到你的 K8S 集群節點上)。

前端領域中 Docker 與 Kubernetes的實踐姿勢

上面的清單主要創建了三種最常見資源來保證服務的運行, 這也是 Kubernetes 的最主要的三類資源。

  • IngressL7層負載均衡配置, 可以根據不同的域名或者路徑等信息指向不同的 Service, Ingress 和 Nginx 很像,實際上 Ingress 的一種實現就是 Nginx, 所以可以將 Ingress 來當成 Nginx 來用,只不過我們不需要手動修改 nginx.conf,也不用手動重啟 Nginx 服務。
  • Service一組 pod 的抽象,用來選擇提供同一服務的 pod。 由於 pod 是不穩定的,銷燬重建經常發生,pod 的 ip 經常發生變化,所以需要一種抽象的資源 Service 來表示 pod 的位置。 Service 也是K8S內部服務發現機制,會自動將 Service 名稱寫入內部 DNS 記錄中。
  • Deployment副本控制器,用來管理維護 pod 的一種機制。通過 Deployment 可以指定副本數量,發佈策略, 記錄發佈日誌並支持回滾。

應用發佈系統

K8S 僅僅負責容器的編排,實際上如果部署應用還需要外部 Pipeline 的支持,代碼的構建,靜態檢查,鏡像的打包由 Pipeline 完成.

前端領域中 Docker 與 Kubernetes的實踐姿勢

;

目前國內用的比較多的發佈系統常常由下面幾個服務組成: GitLab/GitHub, Jenkins, Sonar, Harbor。

K8S 在前端的優勢

  1. 首先前端應用和 Java 不同,一個小型 NodeJS 服務佔用內存僅 40M 左右,這意味著如果我們有很多 NodeJS 應用,使用 K8S 將節省大量的硬件資源。
前端領域中 Docker 與 Kubernetes的實踐姿勢

  1. 使用容器的思想進行非侵入式日誌,性能指標收集。由於容器即是一個進程,所以對容器的監控可以看作對我們 NodeJS 進程的監控,K8S 生態裡已經有很多成熟的容器監控方案,例如 Prometheus + Grafana, 使用此可以達到應用的非侵入式性能指標的收集包括: 網絡IO / 磁盤IO / CPU / MEM。
前端領域中 Docker 與 Kubernetes的實踐姿勢

同樣對於日誌收集,我們在代碼中可以直接使用console的方式輸出, 在容器維度再使用日誌收集服務進行日誌收集,同樣的非侵入式, 代碼層無感知,對開發者更加友好,將日誌和服務解耦。

  1. 前端微服務架構基礎設施層。

微服務架構是近兩年越來越流行的一種前端架構組織方式,微服務架構需要有一種更加彈性靈活的部署方式。 使用 Docker 讓我們在複雜架構中抽象服務的最小單元,K8S 給自動維護大規模集群提供了可能。可以說微服務架構天然適合使用 K8S。

K8S 新玩法, 流量分配

K8S 中使用 Service 來抽象一組 pod,而 Service 的選擇器可以動態變更,所以了我們很多可能的玩法, 比如藍綠髮布系統。

藍綠髮布是指發佈過程中新應用發佈測試通過後,通過切換網關流量, 一鍵升級應用的發佈方式, 在 K8S 中通過動態更新 Service 的選擇器實現不同版本的一鍵切換

前端領域中 Docker 與 Kubernetes的實踐姿勢

下面使用上面的 Next.js 應用來演示一下藍綠髮布,倉庫地址

git clone https://github.com/Qquanwei/test-ab-deploy
cd test-ab-deploy
docker build . --tag next-app:stable
kubectl apply -f ./Deployment.yaml

這裡會將 next-app:stable 這個鏡像部署到集群中,並且給 pod 打上 version: stable 的tag。

前端領域中 Docker 與 Kubernetes的實踐姿勢

部署後打開顯示如下。

前端領域中 Docker 與 Kubernetes的實踐姿勢

接著,我們部署 test 分支, 這個分支我們會構建為 next-app:test 的鏡像,並且部署時給這個pod打上 version: test 的標籤。

git checkout test
docker build . --tag next-app:test
kubectl apply -f ./Deployment.yaml

這時候我們一共部署了兩個版本的應用,而且都已經就緒狀態。

前端領域中 Docker 與 Kubernetes的實踐姿勢

但是由於我們的 Service 為 version=stable, 所以所有的請求並不會打到 test 版本上,仍然都會請求 stable 的服務。

前端領域中 Docker 與 Kubernetes的實踐姿勢

當我們用其他的方式已經驗證 test 版本服務可用時, 例如配另外一個 Service 用來測試(Good), 這時候可以下面一條指令切換當前的 Service 到 test 應用上。

kubectl apply -f ./switch-to-test.yaml

執行完這條命令後,刷新頁面可以看到如下。

前端領域中 Docker 與 Kubernetes的實踐姿勢

通過切換 Service 的方式很輕鬆就實現了藍綠髮布的功能,而且是瞬間完成,因為 Service 是 K8S 裡比較輕量的資源,不會和隔壁 Nginx 一樣修改配置就要重啟服務影響整個線上服務。當然實際生產環境會比演示更加嚴謹,可能有專門的平臺以及審核人員進行每個操作的二次驗證。

對於藍綠, 灰度發佈方式,使用 K8S 可以較為輕鬆地實現,讓我們能夠有更多的方式去驗證想法。不過如果想實現更加高級的流量分配方案(例如A/B發佈),需要複雜的流量管理策略 (鑑權,認證),就需要用到服務網格了。

Istio 是目前比較流行的服務網格框架,相比於 K8S 注重運行容器的管理, Istio 則是更注重容器之間組成的服務網格的流量傳輸。

下圖是 Istio 捕獲的官方示例的 bookinfo 微服務中服務的拓撲結構和一些數據指標。

前端領域中 Docker 與 Kubernetes的實踐姿勢

使用 Istio 有兩個明顯的好處:

  1. Istio 能夠捕捉到服務間的調用鏈路,而且不入侵用戶代碼。
  2. Istio 能夠對每一條連接,進行單獨的管理。

例如,我們可以輕鬆對的不同版本的 review 應用的 v1, v2, v3 版本進行動態權重分配。

前端領域中 Docker 與 Kubernetes的實踐姿勢

不僅僅可以對流量權重分配,而且還可以制定一些A/B方案,例如根據 URL 是否匹配請求不同的版本應用,或者根據 Header 種下的 Cookie 進行用戶的區分,從而請求不同的應用。當然,面對行業場景不同,Istio 還會誕生很多有趣的玩法。

不過缺點同樣存在,Istio 實際上也是一個很複雜的系統,會對性能造成影響,而且會佔用不小的系統資源。

總結

K8S 是劃時代的,隨著未來的發展微服務化,雲原生化將會是我們的應用的主要形式,對於前端而言 K8S 無疑會改變現有前端的開發方式和前端架構,讓前端能夠更迅速地擴展,更穩定地交付,應用之間的聯繫也會愈加緊密。沉寂已久的前端下一個三年相信將會是微服務架構的天下,K8S 作為微服務架構基礎設施層也將會被越來越多的公司團隊所重視。



分享到:


相關文章: