如何把應用程序遷移到k8s

程序部署環境的容器化已是大勢所趨,微服務爲容器化提供了廣闊的應用舞臺,k8s已經把Docker歸入爲它的底層支撐容器引擎,一統江湖,成爲了容器技術事實上的標準。通常的應用程序是不能直接拿來部署到容器上的,須要通過一些修改才能移植到k8s上。那麼這些改動包括哪些內容呢?前端

它主要有兩個部分:node

  • 第一部分是服務調用。不管是微服務之間的調用,仍是微服務調用數據庫或前端調用後端,調用的方式都是同樣的。都須要知道IP地址,端口和協議,例如「http://127.0.0.1:80」, 其中「http」是協議,「127.0.0.1」是IP地址,「80」是端口。它的關鍵是讓k8s的配置文件和應用程序都共享相同的調用地址。
  • 第二部分是數據的持久存儲。在程序運行時,常常要訪問持久存儲(硬盤)上的數據,例如日誌,配置文件或臨時共享數據。程序在容器中運行,一旦出現問題,容器會被摧毀,k8s會自動從新生成一個與原來如出一轍的容器,並在上面從新部署應用程序。在集羣環境下,用戶感受不到容器故障,由於系統已經自動修復了。但當容器被摧毀時,容器上的數據也一塊兒被摧毀了,所以要保證程序運行的連續性,就要讓持久存儲不受容器故障的影響。

程序實例:

咱們經過一個Go(別的語言也大同小異)微服務程序作例子來展現要作的修改。它自己的功能很是簡單,只是用SQL語句訪問數據庫中的數據,並寫入日誌。你能夠簡單地把它分紅兩層,後端數據訪問層和數據庫層。在k8s中它被分紅兩個服務。一個是後端服務程序,另外一個是數據庫(用MySQL)服務。後端程序要調用數據庫服務,而後會把一些數據寫入日誌,並且這個日誌不能由於容器故障而丟失。數據庫對數據的保存要求更高,即便k8s集羣或虛擬機出了問題或斷電也要保證數據的存在。mysql

file

上面是程序的目錄結構。咱們重點講一下與k8s相關的。「config」目錄包含與程序配置有關的代碼,「logs」目錄是用來存儲日誌文件的,沒有代碼。「script」目錄是重點,裏面包含了全部與部署程序相關的文件。其中「database」子目錄裏面是數據庫腳本,「kubernetes」子目錄存有k8s的全部配置文件,一回兒還會詳細講解。git

服務調用:

服務調用涉及到兩個不一樣的部分。一部分是k8s的配置文件,它負責服務的註冊和發現。全部部署在k8s上的應用都經過k8s的服務來進行互相調用。另外一部分是應用程序,它須要經過k8s的服務來訪問其餘程序。在沒有k8s時,後端要想訪問數據庫,代碼是這樣的:github

db, err := sql.Open("mysql", "dbuser:dbuser@tcp(localhost:3306)/service_config?charset=utf8")

其中,「dbuser:dbuser」是數據庫用戶名和口令,「localhost:3306」是數據庫主機名和端口地址,「service-config」是數據庫名,共有五個數據須要讀取。遷移到k8s以後,咱們要把這些參數從程序中提取出來,轉化成從k8s中讀取相關數據。sql

k8s配置:

先來看一下k8s的配置文件。docker

file

上面就是k8s的配置文件目錄結構,最外層(kubernetes目錄下)有兩個「yaml」文件「k8sdemo-config.yaml」和"k8sdemo-secret.yaml",它們是被不一樣服務共享的,所以放在最外層。另外還有一個"k8sdemo.sh"文件是k8s命令文件,用來建立k8s對象。「kubernetes」目錄下有兩個子目錄「backend」和「database」分別存放後端程序和數據庫的配置文件。它們內部的結構是相似的,都有三個「yaml」文件:shell

  • backend-deployment.yaml:部署配置文件,
  • backend-service.yaml:服務配置文件
  • backend-volume.yaml:持久卷配置文件.

關於k8s的核心概念,請參閱「經過實例快速掌握k8s(Kubernetes)核心概念」. 「backend」目錄還多了一個「docker」子目錄用來存儲backend應用的Docker鏡像,database的鏡像文件直接從Docker的庫中取得,所以不須要另外生成鏡像文件。數據庫

k8s參數配置:

要想集成應用程序和k8s須要兩個層面的參數共享,一個是應用程序和k8s之間的參數共享,另外一個是不一樣k8s服務之間的參數共享。編程

k8s共享參數定義:

共享參數能夠經過兩種方式實現,一個是環境變量,另外一個是持久卷。這兩種方式大同小異,咱們這裏用環境變量的方式。這其中最關鍵的是「k8sdemo-config.yaml」和"k8sdemo-secret.yaml"這兩個文件,它們分別存儲了普通參數和保密參數。這些參數是屬於整個應用程序的,被各個服務共享。

下面就是「k8sdemo-config.yaml」,它裏面(在「data:」下面)定義了三個數據庫參數,分別是數據庫主機(MYSQL_HOST),數據庫端口(MYSQL_PORT),數據庫名(MYSQL_DATABASE)。

apiVersion: v1
kind: ConfigMap
metadata:
  name: k8sdemo-config  # ConfigMap的名字, 在引用數據時須要
  labels:
    app: k8sdemo
data:
  MYSQL_HOST: k8sdemo-database-service   # 數據庫主機
  MYSQL_PORT: "3306" # 數據庫端口
  MYSQL_DATABASE: service_config # 數據庫名

下面就是「k8sdemo-secret.yaml」,它裏面(在「data:」下面)也定義了三個數據庫參數,根用戶口令(MYSQL_ROOT_PASSWORD),普通用戶名(MYSQL_USER_NAME),普通用戶口令(MYSQL_USER_PQSSWORD)

apiVersion: v1
kind: Secret
metadata:
  name: k8sdemo-secret
  labels:
    app: k8sdemo
data:
  MYSQL_ROOT_PASSWORD: cm9vdA== # 根用戶口令("root")
  MYSQL_USER_NAME: ZGJ1c2Vy # 普通用戶名("dbuser")
  MYSQL_USER_PASSWORD: ZGJ1c2Vy # 普通用戶口令("dbuser")

有關k8s的參數配置詳細信息,請參閱「經過搭建MySQL掌握k8s(Kubernetes)重要概念(下):參數配置」.

引用k8s共享參數:

下面就是「backend-deployment.yaml」,它定義了「backend「服務的部署(Deployment)配置。它的「containers:」部分定義了容器,「env:」部分定義了環境變量,也就是咱們所熟悉的操做系統的環境變量,通常是由系統來定義。不一樣的系統例如Linux和Windows都有本身的方法來定義環境變量。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8sdemo-backend-deployment
  labels:
    app: k8sdemo-backend
spec:
  selector:
    matchLabels:
      app: k8sdemo-backend
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: k8sdemo-backend
    spec:
      containers: # 定義容器
        - image: k8sdemo-backend-full:latest
          name: k8sdemo-backend-container
          imagePullPolicy: Never
          env: # 定義環境變量
            - name: MYSQL_USER_NAME
              valueFrom:
                secretKeyRef:
                  name: k8sdemo-secret
                  key: MYSQL_USER_NAME
            - name: MYSQL_USER_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: k8sdemo-secret
                  key: MYSQL_USER_PASSWORD
            - name: MYSQL_HOST
              valueFrom:
               configMapKeyRef:
                 name: k8sdemo-config
                 key: MYSQL_HOST
            - name: MYSQL_PORT
              valueFrom:
                configMapKeyRef:
                  name: k8sdemo-config
                  key: MYSQL_PORT
            - name: MYSQL_DATABASE
              valueFrom:
                configMapKeyRef:
                  name: k8sdemo-config
                  key: MYSQL_DATABASE
          ports:
            - containerPort: 80
              name: portname
          volumeMounts:
            - name: k8sdemo-backend-persistentstorage
              mountPath: /app/logs
      volumes:
        - name: k8sdemo-backend-persistentstorage
          persistentVolumeClaim:
            claimName: k8sdemo-backend-pvclaim

k8s的環境變量主要是用來向容器傳遞參數的。環境變量引用了「k8sdemo-config.yaml」和"k8sdemo-secret.yaml"文件裏的參數,這樣就在k8s內部用過共享參數定義和參數引用實現了k8s層的參數共享。

下面是部署配置文件裏的環境變量的片斷。「 - name: MYSQL_USER_PASSWORD」是環境變量名,「secretKeyRef」說明它的值來自於secret,「name: k8sdemo-secret」是secret的名字,「key: MYSQL_USER_PASSWORD」是secret裏的鍵名,它的最終含義就是環境變量「MYSQL_USER_PASSWORD」的值是由secret裏的量「MYSQL_USER_PASSWORD」來定義。

env:
    - name: MYSQL_USER_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: k8sdemo-secret
                  key: MYSQL_USER_PASSWORD

下面是另外一個定義環境變量的片斷,與上面的相似,只不過它的鍵值來自於configMap,而不是secret。

env:
     - name: MYSQL_DATABASE
              valueFrom:
                configMapKeyRef:
                  name: k8sdemo-config
                  key: MYSQL_DATABASE

關於k8s的部署配置細節,請參閱「經過搭建MySQL掌握k8s(Kubernetes)重要概念(上):網絡與持久卷」. "

程序和k8s的參數共享:

k8s在建立容器時,會建立環境變量。應用程序在容器裏運行時能夠從環境變量裏讀取共享參數已達到應用程序和k8s共享參數的目的。下面就是Go程序訪問數據庫的代碼片斷。

type dbConfig struct {
   dbHost     string
   dbPort     string
   dbDatabase string
   dbUser string
   dbPassword string
}

func buildMysql() (dataservice.UserDataInterface, error) {
   tool.Log.Debug("connect to database ")
   dc :=  buildDbConfig ()
   dataSourceName := dc.dbUser + ":"+ dc.dbPassword + "@tcp(" +dc.dbHost +":" +dc.dbPort +")/" + dc.dbDatabase + "?charset=utf8";
   tool.Log.Debug("dataSourceName:", dataSourceName)
   //db, err := sql.Open("mysql", "dbuser:dbuser@tcp(localhost:3306)/service_config?charset=utf8")
   db, err := sql.Open("mysql", dataSourceName)
   checkErr(err)
   dataService := userdata.UserDataMysql{DB: db}
   return &dataService, err
}

func buildDbConfig () dbConfig{
   dc :=dbConfig{}
   dc.dbHost = os.Getenv("MYSQL_HOST")
   dc.dbPort = os.Getenv("MYSQL_PORT")
   dc.dbDatabase = os.Getenv("MYSQL_DATABASE")
   dc.dbUser = os.Getenv("MYSQL_USER_NAME")
   dc.dbPassword = os.Getenv("MYSQL_USER_PASSWORD")
   return dc
}

上面程序中,「buildDbConfig()」函數從環境變量中讀取k8s給容器設置好的參數,並上傳給「buildMysql()」函數,用來鏈接數據庫。上面是用Go程序讀取環境變量,但其它語言例如Java也有相似的功能。

持久存儲:

「backend」服務日誌:

持久存儲相對比較簡單,它不須要作額外的應用程序修改 ,但須要程序和k8s相互配合來完成。

Go代碼:

下面是日誌設置的Go代碼片斷,它把日誌的輸出設爲k8sdemo的logs目錄和Stdout。

func RegisterLogrusLog() error {
    //standard configuration
    log := logrus.New()
    log.SetFormatter(&logrus.TextFormatter{})
    log.SetReportCaller(true)
    file, err := os.OpenFile("../logs/demo.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err != nil {
        fmt.Println("Could Not Open Log File : ", err)
        return errors.Wrap(err, "")
    }
    mw := io.MultiWriter(os.Stdout,file)
    log.SetOutput(mw)
    ...
    return nil
}

掛載持久卷:

下一步要作的就是掛載本地目錄到容器的「logs」目錄,這樣日誌在寫入「logs」目錄的時候就寫入了本地目錄。下面是生成k8s持久卷的配置文件「backend-volume.yaml」,它內部分紅兩部分(用「---」隔開)。上半部分是持久卷,下半部分是持久卷申請。它由本地硬盤的「/home/vagrant/app/k8sdemo/logs」目錄生成k8s的持久卷。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: k8sdemo-backend-pv
  labels:
    app: k8sdemo-backend
spec:
  capacity:
    storage: 1Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  storageClassName: standard
  local:
    path: /home/vagrant/app/k8sdemo/logs
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - minikube
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: k8sdemo-backend-pvclaim
  labels:
    app: k8sdemo-backend
spec:
  accessModes:
    - ReadWriteOnce
  # storageClassName: local-storage
  resources:
    requests:
      storage: 1Gi #1 GB

下面是「backend-deployment.yaml」部署文件片斷,它把k8s的持久卷掛載到容器的「app/logs」上。

volumeMounts:
            - name: k8sdemo-backend-persistentstorage
              mountPath: /app/logs
      volumes:
        - name: k8sdemo-backend-persistentstorage
          persistentVolumeClaim:
            claimName: k8sdemo-backend-pvclaim

完成以後,就能夠在本地目錄上查看日誌文件,這樣即便容器或k8s集羣出現問題,日誌也不會丟失。

爲何目錄是「app/logs」呢?由於在生成「beckend」的鏡像時,設定的容器的運行程序根目錄是「app」。關於如何建立Go鏡像文件,請參閱「建立優化的Go鏡像文件以及踩過的坑」.

數據庫持久卷:

Mysql數據庫的持久卷設置與日誌相似,詳情請參閱「經過搭建MySQL掌握k8s(Kubernetes)重要概念(上):網絡與持久卷」.

存在的問題:

細心的讀者可能已經發現了,在定義的環境變量中,有兩個與其餘的有些不一樣,這兩個就是「MYSQL_HOST」和"MYSQL_PORT"。全部的環境變量都是在參數文件(k8sdemo-config.yaml)中定義,別的環境變量是在k8s配置文件(例如backend-deployment.yaml)中引用,但這兩個雖然在k8s的部署配置文件提到了,但只是用來定義環境變量,最終只是被應用程序引用了,但服務的配置文件並無真正引用它。

apiVersion: v1
kind: Service
metadata:
  name: k8sdemo-database-service # 這裏並無引用環境變量
  labels:
    app: k8sdemo-database
spec:
  type: NodePort
  selector:
    app: k8sdemo-database
  ports:
    - protocol : TCP
      nodePort: 30306
      port: 3306 # 這裏並無引用環境變量
      targetPort: 3306

上面是數據庫服務的配置文件「database-service.yaml」, 這裏並無引用「MYSQL_HOST」和"MYSQL_PORT",而是直接寫上「k8sdemo-database-service」和「3306」。爲何會是這樣呢?由於k8s的環境變量是有侷限性的,它只能定義在「containers:」裏面,也就是說只有容器才能定義環境變量,這從理論上也說得過去。由於若是沒有容器,那麼環境變量定義給誰呢?但這就致使了服務名不能引用配置參數,結果就是服務名要在兩處被定義,一個是參數文件,另外一個是服務配置文件。若是你要修改它,就要在兩處同時修改,加大了出錯的概率。有什麼辦法能夠解決呢?

Helm

這在k8s內部是無法解決的,但在k8s外是能夠解決的。有一個很流行的k8s的包管理工具,叫「helm」, 可以用來定義服務變量。

下面就是使用了Helm以後的Pod的配置文件。

alpine-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: {{ template "alpine.fullname" . }}
  labels:
    # The "app.kubernetes.io/managed-by" label is used to track which tool deployed a given chart.
    # It is useful for admins who want to see what releases a particular tool
    # is responsible for.
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    # The "app.kubernetes.io/instance" convention makes it easy to tie a release to all of the
    # Kubernetes resources that were created as part of that release.
    app.kubernetes.io/instance: {{ .Release.Name | quote }}
    app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
    # This makes it easy to audit chart usage.
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
    app.kubernetes.io/name: {{ template "alpine.name" . }}
spec:
  # This shows how to use a simple value. This will look for a passed-in value called restartPolicy.
  restartPolicy: {{ .Values.restartPolicy }}
  containers:
  - name: waiter
    image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
    imagePullPolicy: {{ .Values.image.pullPolicy }}
    command: ["/bin/sleep", "9000"]

下面是變量的定義文件values.yaml

image:
  repository: alpine
  tag: latest
  pullPolicy: IfNotPresent

restartPolicy: Never

程序來源

Helm使用了Go的模板(template)。模板是用數據驅動的文本生成器。它在文本模板裏用特殊符號(這裏是「{{ }}」)定義變量或數據,而後在執行模板時再將變量轉換成變量值,生成最終文本,通常在前端用的比較多。在Helm模板裏,「{{ }}」裏面的就是變量引用,變量是定義在「values.yaml」文件裏的。

上面的例子有兩個文件,一個是「alpine-pod.yaml」,另外一個是「values.yaml」。變量定義在「values.yaml」裏,再在「alpine-pod.yaml」文件裏引用,這樣就解決了k8s的環境變量的侷限性。

Helm是功能很是強大的k8s包管理工具,並且能夠簡化容器部署,是一款很是流行的工具。但它的問題是Helm增長了配置文件的複雜度,下降了可讀性。如今的版本是Helm2,但Helm3不久就要出爐了。Helm3有一個功能是支持Lua模板,能直接用對象編程(詳情請見A First Look at the Helm 3 Plan),新的模板比如今的看起來要強很多,若是你想使用新的還須要再等一等。

結論:

通常的應用程序是不能直接部署到k8s上的,須要通過一些改動才行。它主要有兩個部分。第一個是服務調用。第二個是數據的持久存儲。服務調用的關鍵是讓k8s和應用程序共享參數。k8s裏已經有這種機制,但它還有一點缺陷,只能用來定義容器的環境變量,須要引入其餘工具,例如Helm才能解決這個問題。持久存儲不須要修改程序,但須要k8s的配置和應用程序配合才能成功。

源碼:

完整源碼的github連接

備註:

本文中的Go程序只是示例程序,只有k8s配置文件部分是認真寫的,能夠直接拷貝或引用。其餘部分都是臨時拼湊來的,主要是爲了做爲例子,所以沒有花時間完善它們,總的來講它們寫得比較粗糙,千萬不要直接拷貝。

索引:

  1. 經過實例快速掌握k8s(Kubernetes)核心概念
  2. 經過搭建MySQL掌握k8s(Kubernetes)重要概念(上):網絡與持久卷
  3. 經過搭建MySQL掌握k8s(Kubernetes)重要概念(下):參數配置
  4. helm/helm
  5. Alpine: A simple Helm chart
  6. A First Look at the Helm 3 Plan

本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索