程式部署環境的容器化已經是大勢所趨,微服務為容器化提供了廣闊的應用舞臺,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的配置和應用程式配合才能成功,
原始碼:
完整原始碼的github鏈接
備注:
本文中的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 發布!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/178325.html
標籤:其他
上一篇:圖論——差分約束
