K8S實戰基礎篇:一文帶你深入瞭解K8S實戰部署SpringBoot項目
1.前言
雲原生可以說是當下互聯網行業最火爆的概念和技術,雲原生從字面意思上來看可以分成雲和原生兩個部分。
雲是和本地相對的,傳統的應用必須跑在本地服務器上,現在流行的應用都跑在雲端,雲包含了IaaS,、PaaS和SaaS。
原生就是土生土長的意思,我們在開始設計應用的時候就考慮到應用將來是運行雲環境裏面的,要充分利用雲資源的優點,比如️雲服務的彈性和分佈式優勢。
聊到雲原生,避不開的就是容器技術,而docker作爲最流行的容器技術,已經經過很多年的線上實戰。今天我們不深入聊雲原生,docker這些技術概念,今天我們聊一聊時下最火的容器編排技術:K8S-實戰部署SpringBoot項目。
2.簡介
2.1.爲什麼寫這篇文章
前言中提到雲原生、docker、K8S,我是18年第一次docker,也是在18年接觸K8S,相對這門技術來說,我接觸的時候已經有些晚了,因爲在之後的面試中,已經感受到這些技術在大廠已經用的很成熟了,之前都在小公司,並不瞭解這些技術是什麼,幹什麼用,加上國內這方面的資料又比較少,學起來是相當喫力。而到大廠之後,發現這些技術無處不在,並且基礎設施建設已經很完備,一鍵部署雲端的騷操作,讓開發只需要關心業務而無需關心安裝部署等繁瑣的工作。禍兮福之所倚;福兮禍之所伏,大廠的技術設施完備的同時,另一方面也消弱了你去了解基礎設施背後的技術原理能力。正是認識到這一點,今天才寫這篇文章,爲迷途中的孩子找到回家的路。廢話不多,擼起袖子,幹就完了!
這裏沒有任何馬後炮套話,只有粗暴的乾貨。寫大家看得懂、用得着、賺得到的文章是唯一宗旨!
2.2.需求描述
我有一個簡單的Springboot項目,想部署在K8S集羣中,能夠實現擴縮容,負載均衡,同時我有一個互聯網域名,我想把這個域名綁定在這個服務上,能夠在有網絡的地方訪問。
2.3.需求分析
這個需求我想在很多剛開始接觸docker,k8s等技術的老鐵身上都會遇到過,真正實現起來,並不是那麼容易,聽我一一道來:
- image—Springboot項目一般是以jar包的形式跑在像centos等服務器上,運行
nohup java -jar xxx.jar &
命令就能啓動起來。但是在k8s中,運行起來的的並不是jar,而是image
,因此我們需要把jar打包成image; - 自動擴縮—最基礎的image有了,接下來就要考慮的是自動擴縮:顧名思義,比如說就是在服務訪問量大的時候,我可以添加實例,來減少每個服務實例的壓力,在訪問量小的時候,我可以刪除一部分實例,來實現資源的高效利用。
- 負載均衡—當我們的實例越來越多,我並不希望把所有的請求都落在一個實例上,如若不然,我們自動擴縮也就沒了意義,傳統方法我們可以用Nginx等實現負載均衡,待會來看看K8S能做些什麼
- 域名綁定—這個就沒什麼好說的了。
3. 部署實戰
3.1 環境準備
工欲善其事,必先利其器:
- Springboot jar包
- K8S集羣環境
K8S集羣環境部署我就不在這裏展開講了,我們準備一個最簡單的Springboot項目,裏面只有一個接口,訪問localhost:8088
,返回服務器的hostname
,當整個部署工作完成之後,我們通過域名訪問這個接口,返回的應該是不同的container
的hostname
,那我們的任務就完成了。
@GetMapping("/")
public String sayHello() throws UnknownHostException {
String hostname = "Unknown";
InetAddress address = InetAddress.getLocalHost();
hostname = address.getHostName();
return hostname;
}
3.2 image準備
我們都知道,所有image的生成都離不開Dockerfile
技術,我們有了一個jar包,要利用Dockerfile技術生成一個image。廢話不多,上代碼:
#使用jdk8作爲基礎鏡像
FROM java:8
#指定作者
MAINTAINER ***
#暴漏容器的8088端口
#EXPOSE 8088
#將複製指定的docker-demo-0.0.1-SNAPSHOT.jar爲容器中的job.jar,相當於拷貝到容器中取了個別名
ADD docker-demo-0.0.1-SNAPSHOT.jar /job.jar
#創建一個新的容器並在新的容器中運行命令
RUN bash -c 'touch /job.jar'
#設置時區
ENV TZ=PRC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
#相當於在容器中用cmd命令執行jar包 指定外部配置文件
ENTRYPOINT ["java","-jar","/job.jar"]
Dockerfile文件裏面有註釋,具體的每一行代碼什麼意思我就不展開多講了,這不是今天的重點,接下來,我們把docker-demo-0.0.1-SNAPSHOT.jar
,Dockerfile
文件放在同一個目錄,上傳到K8S的master 節點上,在目錄內執行如下命令生成images
$ docker build .
我們可以看到生成image的過程,通過docker images
查看鏡像
生成一個 docker-demo:latest的image鏡像。
注意:我們部署的是集羣,要想K8S集羣中都能拉到這個鏡像,那我們有以下兩種方式:
- 方法一:我們把這個docker-demo:latest上傳到遠端倉庫,這個倉庫可以是我們自己的,或者是像我一樣註冊一個阿里雲的賬號,上傳到阿里雲自己的容器鏡像服務倉庫,如下圖:
具體步驟
:
1.1 docker登陸阿里雲容器鏡像服務,需要輸入密碼
$ docker login --username=24k不怕(寫自己的用戶名) registry.cn-hangzhou.aliyuncs.com
1.2 在阿里雲上創建命名空間:例:cuixhao-docker-demo
1.3 鏡像打標籤
$ docker tag docker-demo:latest registry.cn-hangzhou.aliyuncs.com/cuixhao-docker-demo/docker-demo:latest
1.4 push到阿里雲
$ docker push registry.cn-hangzhou.aliyuncs.com/cuixhao-docker-demo/docker-demo:latest
1.5 刪除掉docker-demo:latest
$ docker rmi docker-demo:latest
- 方法二 把剛纔創建image的過程,在集羣中每一臺節點上都執行一遍,保證集羣中每一臺都有這個鏡像。我採用的是二者的結合:先在master上把鏡像生成,上傳到阿里雲,然後在另外的節點上,通過
docker pull registry.cn-hangzhou.aliyuncs.com/cuixhao-docker-demo/docker-demo:latest
命令,從阿里雲上拉到本地,然後在通過docer tag registry.cn-hangzhou.aliyuncs.com/cuixhao-docker-demo/docker-demo:latest docker-demo:latest
命令打標籤,然後刪掉拉取到的鏡像:docker rmi registry.cn-hangzhou.aliyuncs.com/cuixhao-docker-demo/docker-demo:latest
,
爲什麼這麼做?因爲我阿里雲建的命名空間中的image都是私有,K8S拉取image的時候是需要集羣中都配置ca證書的,如果設置爲公開則不存在這個問題。所以我用docker-demo:latest這個鏡像,直接用本地的,部署的時候不用再去阿里雲拉取。
3.3 部署2個實例
3.3.1 編寫yaml文件
基礎鏡像準備好了,那我們就開始部署吧。我們知道,k8s有deployment
,service
等概念,這裏不詳細講,簡單描述一下:deployment(命名空間),管理pod集羣,service
,管理pod中的服務。我們在master 節點編輯一個 ingress-docker-docker-deployment.yaml
文件
$ vi ingress-docker-docker-deployment.yaml
鍵入以下內容
apiVersion: apps/v1
kind: Deployment
metadata:
name: ingress-docker-demo-deployment
labels:
app: ingress-docker-demo
spec:
replicas: 2
selector:
matchLabels:
app: ingress-docker-demo
template:
metadata:
labels:
app: ingress-docker-demo
spec:
containers:
- name: docker-demo
image: docker-demo
imagePullPolicy: Never
ports:
- containerPort: 8088
---
apiVersion: v1
kind: Service
metadata:
name: ingress-docker-demo-service
spec:
ports:
- port: 80
protocol: TCP
targetPort: 8088
selector:
app: ingress-docker-demo
由yaml文件內容我們可以讀出:我們創建了一個 名字我爲ingress-docker-demo-deployment的deployment,裏面有2個 docker-demo 的pod (replicas: 2
),和一個名字爲ingress-docker-demo-service的service,管理名字爲ingress-docker-demo的pod服務,目標端口8088,即我們的springboot服務所用端口,對外提供80端口。
3.3.2 啓動
在master執行如下命令啓動deployment 和service:
$ kubectl apply -f ingress-docker-docker-deployment.yaml
查看啓動結果:
$ kubectl get pod -o wide
我們可以看到啓動結果,兩個container
分別在 worker01,worker02這兩個節點上,可以看到,K8S集羣給我們分配了兩個IP:192.168.14.11
,192.168.221.73
。我們思考以下三個問題:
a. 在集羣中,我們通過以上訪問這兩個服務,能訪問通嗎,是通過8088端口還是80端口?
我們不妨嘗試一下,在集羣中任何一個節點執行如下命令:
$ curl 192.168.14.11:8088
$ curl 192.168.221.73:8088
我們可以看到,接口返回了各自container的hostname,說明我們的服務是部署啓動成功了,訪問80端口是不通的,有興趣的老鐵可以試一下,因爲80是我們對外的端口,所以用container ip是訪問不通的。
b. 在集羣內部訪問,我們如何做到負載均衡?
有老鐵可能會考慮一個問題,K8S集羣中的pod有可能銷燬或者重啓,每次重啓之後的ip不能保證一致,那以上訪問方式肯定是不可採用的。想法很對,我們想訪問一個固定的地址,不管pod如何重啓,ip如何變化,我只訪問這一個ip,這豈不美哉?那我們能不能做到呢?且看如下騷操作:
$ kubectl get svc
通過以上命令,我們找到了ingress-docker-docker-deployment.yaml
中定義的名字爲 ingress-docker-demo-service
的service,它有一個 CLUSTER-IP
,PORT
爲80,那我們根據K8S中的service的作用,做一個大膽的猜測:我們是不是可以固定的通過 10.103.19.71
(省略默認80端口)或者 10.103.19.71:80
來永久訪問這兩個服務呢?
$ curl 10.103.19.71
$ curl 10.103.19.71:80
答案是肯定的!,它給我們做了負載均衡!
c. 在集羣外部我們如何訪問這兩個服務並且負載均衡?
集羣內訪問服務,負載均衡都已經做好了。有的老鐵會問:集羣外服想訪問集羣內的服務,該如何做呢?別急,還沒完!
3.3.3 引入Ingress
3.3.3.1 Ingress簡介
我們傳統的集羣負載均衡做法是在一臺機器上安裝Nginx,把我們的服務配置在Nginx上,外部直接訪問你Nginx,它幫我們做負載,做限流,但是今天我們玩了K8S,就不能在用這種方法了,我們大膽的想一下,我們把所有的東西都在K8S做了,豈不美哉!想法很好,"好事者"已經替我們想到了,並且替我們做到了。
kubernetes ingress 文檔
我來簡單介紹一下:
如圖:在K8S中,Ingress 提供 controller接口,由各個負載均衡廠家實現,傳統Nginx是配置在nginx.conf 中,在K8S中,我們只需要配置Ingress 資源yaml就可以,聽起來是不是方便多了,我們可以像管理deployment
,service
,pod
一樣管理Ingress
3.3.3.2 Ingress 安裝
我們使用 Nginx Ingress Controller
來一波騷操作:
編寫 ingress-nginx.yaml
$ vi ingress-nginx.yaml
鍵入以下內容:
apiVersion: v1
kind: Namespace
metadata:
name: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: nginx-configuration
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: tcp-services
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: udp-services
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: nginx-ingress-serviceaccount
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: nginx-ingress-clusterrole
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
rules:
- apiGroups:
- ""
resources:
- configmaps
- endpoints
- nodes
- pods
- secrets
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- "extensions"
- "networking.k8s.io"
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- "extensions"
- "networking.k8s.io"
resources:
- ingresses/status
verbs:
- update
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: nginx-ingress-role
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
rules:
- apiGroups:
- ""
resources:
- configmaps
- pods
- secrets
- namespaces
verbs:
- get
- apiGroups:
- ""
resources:
- configmaps
resourceNames:
# Defaults to "<election-id>-<ingress-class>"
# Here: "<ingress-controller-leader>-<nginx>"
# This has to be adapted if you change either parameter
# when launching the nginx-ingress-controller.
- "ingress-controller-leader-nginx"
verbs:
- get
- update
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
- apiGroups:
- ""
resources:
- endpoints
verbs:
- get
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: nginx-ingress-role-nisa-binding
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: nginx-ingress-role
subjects:
- kind: ServiceAccount
name: nginx-ingress-serviceaccount
namespace: ingress-nginx
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: nginx-ingress-clusterrole-nisa-binding
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: nginx-ingress-clusterrole
subjects:
- kind: ServiceAccount
name: nginx-ingress-serviceaccount
namespace: ingress-nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-ingress-controller
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
template:
metadata:
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
annotations:
prometheus.io/port: "10254"
prometheus.io/scrape: "true"
spec:
# wait up to five minutes for the drain of connections
terminationGracePeriodSeconds: 300
serviceAccountName: nginx-ingress-serviceaccount
hostNetwork: true
nodeSelector:
name: ingress
kubernetes.io/os: linux
containers:
- name: nginx-ingress-controller
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1
args:
- /nginx-ingress-controller
- --configmap=$(POD_NAMESPACE)/nginx-configuration
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
- --publish-service=$(POD_NAMESPACE)/ingress-nginx
- --annotations-prefix=nginx.ingress.kubernetes.io
securityContext:
allowPrivilegeEscalation: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
# www-data -> 33
runAsUser: 33
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 10
readinessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 10
lifecycle:
preStop:
exec:
command:
- /wait-shutdown
---
這個文件並不是我胡編亂造自己寫的,是"好事者"幫我們做好了,我只是稍作修改:設置網絡模式爲hostNetwork:true
,我希望我在集羣中一臺機器上開一個80端口,用這臺機器作爲負載均衡入口,因此:nodeSelector: name: ingress
。這是節點選擇器配置參數,設置這個,ingress服務會在節點名字爲ingress的機器上部署。
接下來我們在集羣中的除master節點之外的一個機器上執行下個命令:給這臺hostname爲worker01-kubeadm-k8s的機器取個別名ingress
$ kubectl label node worker01-kubeadm-k8s name=ingress
接下來,我們在master節點執行安裝ingress操作
$ kubectl apply -f ingress-nginx.yaml
安裝過程有點兒慢,因爲有個鏡像比較難拉取:quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1
,建議執行 docker pull 先拉取到本地,上傳到阿里雲,然後在各個節點從阿里雲拉取,然後在打tag的騷操作,都是有經驗的程序員,你們知道我在說什麼!
3.3.3.3 Ingress 配置啓動
編寫Ingress yaml資源:
$ vi nginx-ingress.yaml
鍵入以下內容:
#ingress
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nginx-ingress
spec:
rules:
- host: test.test.com
http:
paths:
- path: /
backend:
serviceName: ingress-docker-demo-service
servicePort: 80
文件定義了一種Ingress的資源,配置host爲:test.test.com
,代理K8S中名字爲ingress-docker-demo-service
的 Service, Service端口爲80.看起來是不是和Nginx配置有點兒類似?
最後一步:啓動
$ kubectl apply -f nginx-ingress.yaml
3.3.3.4 驗證
因爲test.test.com是不存在的域名,我們都是有經驗的開發人員,很自然的想到去修改本地host : 添加 ip test.test.com
,ip爲K8S節點中設置的別名爲ingress的ip地址
瀏覽器訪問:
完美!
3.3.3.4 自動擴縮
使用如下命令,可以然服務實現自動擴縮:
$ kubectl autoscale ingress-docker-docker-deployment.yaml --min=2 --max=5 --cpu-percent=80
還有很多自動擴縮的規則,老鐵們自己探討!
4. 總結
K8S實戰還有很多玩法,我今天只是講了最簡單的服務部署,在不同的生產環境中,需求也是不一樣的,比如說:一個簡單的web應用,有mysql數據庫,有redis,有springboot應用,都要在K8S中實踐,這又是另一種部署方法,但萬變不離其宗,核心都是要深入瞭解K8S網絡
,只有網絡打通了,各個組件纔會暢通無阻的運行。有興趣的老鐵可以關注一波,一起Hello World!