K8S實戰基礎篇:一文帶你深入瞭解K8S實戰部署SpringBoot項目

1.前言

雲原生可以說是當下互聯網行業最火爆的概念和技術,雲原生從字面意思上來看可以分成雲和原生兩個部分。
是和本地相對的,傳統的應用必須跑在本地服務器上,現在流行的應用都跑在雲端,雲包含了IaaS,、PaaS和SaaS。
原生就是土生土長的意思,我們在開始設計應用的時候就考慮到應用將來是運行雲環境裏面的,要充分利用雲資源的優點,比如️雲服務的彈性和分佈式優勢。
聊到雲原生,避不開的就是容器技術,而docker作爲最流行的容器技術,已經經過很多年的線上實戰。今天我們不深入聊雲原生,docker這些技術概念,今天我們聊一聊時下最火的容器編排技術:K8S-實戰部署SpringBoot項目。

2.簡介

2.1.爲什麼寫這篇文章

前言中提到雲原生dockerK8S,我是18年第一次docker,也是在18年接觸K8S,相對這門技術來說,我接觸的時候已經有些晚了,因爲在之後的面試中,已經感受到這些技術在大廠已經用的很成熟了,之前都在小公司,並不瞭解這些技術是什麼,幹什麼用,加上國內這方面的資料又比較少,學起來是相當喫力。而到大廠之後,發現這些技術無處不在,並且基礎設施建設已經很完備,一鍵部署雲端的騷操作,讓開發只需要關心業務而無需關心安裝部署等繁瑣的工作。禍兮福之所倚;福兮禍之所伏,大廠的技術設施完備的同時,另一方面也消弱了你去了解基礎設施背後的技術原理能力。正是認識到這一點,今天才寫這篇文章,爲迷途中的孩子找到回家的路。廢話不多,擼起袖子,幹就完了!

這裏沒有任何馬後炮套話,只有粗暴的乾貨。寫大家看得懂、用得着、賺得到的文章是唯一宗旨!

2.2.需求描述

我有一個簡單的Springboot項目,想部署在K8S集羣中,能夠實現擴縮容,負載均衡,同時我有一個互聯網域名,我想把這個域名綁定在這個服務上,能夠在有網絡的地方訪問。

2.3.需求分析

這個需求我想在很多剛開始接觸docker,k8s等技術的老鐵身上都會遇到過,真正實現起來,並不是那麼容易,聽我一一道來:

  1. image—Springboot項目一般是以jar包的形式跑在像centos等服務器上,運行nohup java -jar xxx.jar &命令就能啓動起來。但是在k8s中,運行起來的的並不是jar,而是image,因此我們需要把jar打包成image;
  2. 自動擴縮—最基礎的image有了,接下來就要考慮的是自動擴縮:顧名思義,比如說就是在服務訪問量大的時候,我可以添加實例,來減少每個服務實例的壓力,在訪問量小的時候,我可以刪除一部分實例,來實現資源的高效利用。
  3. 負載均衡—當我們的實例越來越多,我並不希望把所有的請求都落在一個實例上,如若不然,我們自動擴縮也就沒了意義,傳統方法我們可以用Nginx等實現負載均衡,待會來看看K8S能做些什麼
  4. 域名綁定—這個就沒什麼好說的了。

3. 部署實戰

3.1 環境準備

工欲善其事,必先利其器:

  1. Springboot jar包
  2. K8S集羣環境

K8S集羣環境部署我就不在這裏展開講了,我們準備一個最簡單的Springboot項目,裏面只有一個接口,訪問localhost:8088,返回服務器的hostname,當整個部署工作完成之後,我們通過域名訪問這個接口,返回的應該是不同的containerhostname,那我們的任務就完成了。

	@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.jarDockerfile文件放在同一個目錄,上傳到K8S的master 節點上,在目錄內執行如下命令生成images

$ docker build .

我們可以看到生成image的過程,通過docker images 查看鏡像
在這裏插入圖片描述
生成一個 docker-demo:latest的image鏡像。
注意:我們部署的是集羣,要想K8S集羣中都能拉到這個鏡像,那我們有以下兩種方式:

  1. 方法一:我們把這個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
  1. 方法二 把剛纔創建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,servicepod一樣管理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!

下一篇:K8S實戰進階篇:一文帶你深入瞭解K8S持久化存儲解決方案

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章