如何把應用程序遷移到k8s
- 2019 年 10 月 27 日
- 筆記
程序部署環境的容器化已經是大勢所趨,微服務為容器化提供了廣闊的應用舞台,k8s已經把Docker納入為它的底層支撐容器引擎,一統江湖,成為了容器技術事實上的標準。一般的應用程序是不能直接拿來部署到容器上的,需要經過一些修改才能移植到k8s上。那麼這些改動包括哪些內容呢?
它主要有兩個部分:
- 第一部分是服務調用。不論是微服務之間的調用,還是微服務調用數據庫或前端調用後端,調用的方式都是一樣的。都需要知道IP地址,端口和協議,例如「http://127.0.0.1:80」, 其中「http」是協議,「127.0.0.1」是IP地址,「80」是端口。它的關鍵是讓k8s的配置文件和應用程序都共享相同的調用地址。
- 第二部分是數據的持久存儲。在程序運行時,經常要訪問持久存儲(硬盤)上的數據,例如日誌,配置文件或臨時共享數據。程序在容器中運行,一旦出現問題,容器會被摧毀,k8s會自動重新生成一個與原來一模一樣的容器,並在上面重新部署應用程序。在集群環境下,用戶感覺不到容器故障,因為系統已經自動修復了。但當容器被摧毀時,容器上的數據也一起被摧毀了,因此要保證程序運行的連續性,就要讓持久存儲不受容器故障的影響。
程序實例:
我們通過一個Go(別的語言也大同小異)微服務程序做例子來展示要做的修改。它本身的功能非常簡單,只是用SQL語句訪問數據庫中的數據,並寫入日誌。你可以簡單地把它分成兩層,後端數據訪問層和數據庫層。在k8s中它被分成兩個服務。一個是後端服務程序,另一個是數據庫(用MySQL)服務。後端程序要調用數據庫服務,然後會把一些數據寫入日誌,而且這個日誌不能因為容器故障而丟失。數據庫對數據的保存要求更高,即使k8s集群或虛擬機出了問題或斷電也要保證數據的存在。
上面是程序的目錄結構。我們重點講一下與k8s相關的。「config」目錄包含與程序配置有關的代碼,「logs」目錄是用來存儲日誌文件的,沒有代碼。「script」目錄是重點,裏面包含了所有與部署程序相關的文件。其中「database」子目錄裏面是數據庫腳本,「kubernetes」子目錄存有k8s的所有配置文件,一回兒還會詳細講解。
服務調用:
服務調用涉及到兩個不同的部分。一部分是k8s的配置文件,它負責服務的註冊和發現。所有部署在k8s上的應用都通過k8s的服務來進行互相調用。另一部分是應用程序,它需要通過k8s的服務來訪問其他程序。在沒有k8s時,後端要想訪問數據庫,代碼是這樣的:
db, err := sql.Open("mysql", "dbuser:dbuser@tcp(localhost:3306)/service_config?charset=utf8")
其中,「dbuser:dbuser」是數據庫用戶名和口令,「localhost:3306」是數據庫主機名和端口地址,「service-config」是數據庫名,共有五個數據需要讀取。遷移到k8s之後,我們要把這些參數從程序中提取出來,轉化成從k8s中讀取相關數據。
k8s配置:
先來看一下k8s的配置文件。
上面就是k8s的配置文件目錄結構,最外層(kubernetes目錄下)有兩個「yaml」文件「k8sdemo-config.yaml」和"k8sdemo-secret.yaml",它們是被不同服務共享的,因此放在最外層。另外還有一個"k8sdemo.sh"文件是k8s命令文件,用來創建k8s對象。「kubernetes」目錄下有兩個子目錄「backend」和「database」分別存放後端程序和數據庫的配置文件。它們內部的結構是類似的,都有三個「yaml」文件:
- 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的配置和應用程序配合才能成功。
源碼:
備註:
本文中的Go程序只是示例程序,只有k8s配置文件部分是認真寫的,可以直接拷貝或引用。其他部分都是臨時拼湊來的,主要是為了作為例子,因此沒有花時間完善它們,總的來說它們寫得比較粗糙,千萬不要直接拷貝。
索引:
- 通過實例快速掌握k8s(Kubernetes)核心概念
- 通過搭建MySQL掌握k8s(Kubernetes)重要概念(上):網絡與持久卷
- 通過搭建MySQL掌握k8s(Kubernetes)重要概念(下):參數配置
- helm/helm
- Alpine: A simple Helm chart
- A First Look at the Helm 3 Plan
本文由博客一文多發平台 OpenWrite 發佈!