一開始Kubernetes只是被設計用來運行無狀態應用,直到在1.5版本中才添加了StatefulSet控制器用於支持有狀態應用,但它直到1.9版本才正式可用。本文將介紹有狀態和無狀態應用,一個通過K8S StatefulSet來編排有狀態應用的示例,以及當前有狀態應用容器化現狀及將來的發展趨勢。
1. 有狀態應用和無狀態應用
無狀態應用(Stateless Application)是指應用不會在會話中保存下次會話所需要的客戶端數據。每一個會話都像首次執行一樣,不會依賴之前的數據進行響應。有狀態的應用(Stateful Application)是指應用會在會話中保存客戶端的數據,並在客戶端下一次的請求中來使用那些數據。
以服務器端組件為例,判斷它是有狀態的還是無狀態的,其依據是兩個來自相同發起者的請求在服務器端是否具備上下文關系。如果是有狀態的,那么服務器端一般都要保存請求的相關信息,每個請求可以使用以前的請求信息。而如果是無狀態的,其處理的過程必須全部來自於請求所攜帶的信息,以及其他服務器端自身所保存的、並且可以被所有請求所使用的公共信息。最著名的無狀態的服務器應用是WEB服務器。每次HTTP請求和以前都沒有啥關系,只是獲取目標URI。得到目標內容之后,這次連接就被殺死,沒有任何痕跡。有狀態的服務器應用有更廣闊的應用范圍,比如網絡游戲等服務器。它在服務端維護每個連接的狀態信息,服務端在接收到每個連接的發送的請求時,可以從本地存儲的信息來重現上下文關系。這樣,客戶端可以很容易使用缺省的信息,服務端也可以很容易地進行狀態管理。比如說,當一個用戶登錄后,服務端可以根據用戶名獲取他的生日等先前的注冊信息;而且在后續的處理中,服務端也很容易找到這個用戶的歷史信息。
一個大型應用往往具有許多功能模塊,很難簡單地將其整體性地設計為有狀態或無狀態的,而往往將其整個架構分成兩個部分,即無狀態部分和有狀態部分。業務邏輯部分往往作為無狀態的部分,而將狀態保存在有狀態的中間件中,如緩存、數據庫、對象存儲、大數據平台、消息隊列等。這樣無狀態的部分可以很容易的橫向擴展,而狀態保存到后端。而后端的中間件是有狀態的,這些中間件設計之初,就考慮了擴容的時候狀態的遷移、復制、同步等機制,不用業務層關心。
(來源:劉超博文)
通常應用會有如下幾種狀態數據:
-
持久性狀態數據:這種狀態數據在應用重啟或宕機時需要能被保存下來。典型地,這種狀態會被保存到一個冗余的數據庫層,而且數據會被周期性地備份。建議將應用組件和數據庫分開,以便能使得應用組件變成無狀態的。
-
配置狀態數據:應用總是會用各種配置數據,比如數據庫連接字符串等,過去往往保存在配置文件中。進行容器化時,配置文件應該外部化,或環境變量,或配置中心管理。
-
會話狀態數據:每當用戶登錄進應用后,應用都會為它產生會話數據。在現代應用中,會話數據都會保存在分布式緩存中,因此可以被所有服務實例訪問到。但是在傳統web應用中,會話數據會被保存在服務器本地,因此,登錄后的該用戶的所有請求都必須在這台服務器上才能被處理,這就是所謂的粘滯會話(sticky session)。
-
連接狀態:一些應用使用有狀態通信協議,比如Websocket。另外一些協議比如HTTP被認為是無狀態的。對於使用有狀態協議的應用,客戶端的訪問必須被路由到指定的容器內。
-
集群狀態:某些應用以集群形式運行多個實例,以滿足可用性和規模性。在這種應用中,集群內每個成員需要了解其他成員的狀態和角色,比如MySQL集群。現在,Kubernetes提供了StatefulSet控制器來支持這種應用。
-
日志數據:傳統應用的日志通過保存在日志文件中。進行容器化時,要對日志輸出格式進行改造,適配集中式日志系統規范,和容器運行時的日志組件對接,使得日志能通過標准輸出被收集到再保存到統一容器存儲中。
(來源:劉超博文)
2. Kubernetes StatefulSet控制器
常見的Kubernetes控制器不合適處理有狀態應用:
2.1 Kubernetes StatefulSet概述
Kubernetes在1.9版本中正式發布的StatefulSet控制器能支持:
-
Pod會被順序部署和順序終結:StatefulSet中的各個 Pod會被順序地創建出來,每個Pod都有一個唯一的ID,在創建后續 Pod 之前,首先要等前面的 Pod 運行成功並進入到就緒狀態。刪除會銷毀StatefulSet 中的每個 Pod,並且按照創建順序的反序來執行,只有在成功終結后面一個之后,才會繼續下一個刪除操作。
-
Pod具有唯一網絡名稱:Pod具有唯一的名稱,而且在重啟后會保持不變。通過Headless服務,基於主機名,每個 Pod 都有獨立的網絡地址,這個網域由一個Headless 服務所控制。這樣每個Pod會保持穩定的唯一的域名,使得集群就不會將重新創建出的Pod作為新成員。
-
Pod能有穩定的持久存儲:StatefulSet中的每個Pod可以有其自己獨立的PersistentVolumeClaim對象。即使Pod被重新調度到其它節點上以后,原有的持久磁盤也會被掛載到該Pod。
-
Pod能被通過Headless服務訪問到:客戶端可以通過服務的域名連接到任意Pod。
以在K8S中部署高可用的PostgreSQL集群為例,下面是其架構示意圖:
該架構中包含一個主節點和兩個副本節點共3個Pod,這三個Pod在一個StatefulSet中。Master Service是一個Headless服務,指向主Pod,用於數據寫入;Replica Service也是一個Headless服務,指向兩個副本Pod,用於數據讀取。這三個Pod都有唯一名稱,這樣StatefulSet讓用戶可以用穩定、可重復的方式來部署PostgreSQL集群。StatefulSet不會創建具有重復ID的Pod,Pod之間可以通過穩定的網絡地址互相通信。
2.2 使用Kubernetes StatefulSet部署高可用MySQL
當前命名空間為testmysql。
(1)創建ConfigMap,用於向mysql傳遞配置文件。
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql
labels:
app: mysql
data:
master.cnf: |
#Apply this config only on the master.
[mysqld]
log-bin
slave.cnf: |
#Apply this config only on slaves.
[mysqld]
super-read-only
(2)創建StatefulSet對象,它會負責創建Pod。
apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql spec: selector: matchLabels: app: mysql serviceName: mysql replicas: 3 template: metadata: labels: app: mysql spec: initContainers: - name: init-mysql image: mysql:5.7 command: - bash - "-c" - | set -ex # Generate mysql server-id from pod ordinal index. [[ `hostname` =~ -([0-9]+)$ ]] || exit 1 ordinal=${BASH_REMATCH[1]} echo [mysqld] > /mnt/conf.d/server-id.cnf # Add an offset to avoid reserved server-id=0 value. echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf # Copy appropriate conf.d files from config-map to emptyDir. if [[ $ordinal -eq 0 ]]; then cp /mnt/config-map/master.cnf /mnt/conf.d/ else cp /mnt/config-map/slave.cnf /mnt/conf.d/ fi volumeMounts: - name: conf mountPath: /mnt/conf.d - name: config-map mountPath: /mnt/config-map - name: clone-mysql image: gcr.io/google-samples/xtrabackup:1.0 command: - bash - "-c" - | set -ex # Skip the clone if data already exists. [[ -d /var/lib/mysql/mysql ]] && exit 0 # Skip the clone on master (ordinal index 0). [[ `hostname` =~ -([0-9]+)$ ]] || exit 1 ordinal=${BASH_REMATCH[1]} [[ $ordinal -eq 0 ]] && exit 0 # Clone data from previous peer. ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C/var/lib/mysql # Prepare the backup. xtrabackup --prepare --target-dir=/var/lib/mysql volumeMounts: - name: data mountPath: /var/lib/mysql subPath: mysql - name: conf mountPath: /etc/mysql/conf.d containers: - name: mysql image: mysql:5.7 env: - name: MYSQL_ALLOW_EMPTY_PASSWORD value: "1" ports: - name: mysql containerPort: 3306 volumeMounts: - name: data mountPath: /var/lib/mysql subPath: mysql - name: conf mountPath: /etc/mysql/conf.d resources: requests: cpu: 500m memory: 1Gi livenessProbe: exec: command: ["mysqladmin", "ping"] initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 readinessProbe: exec: # Check we can execute queries over TCP (skip-networking is off). command: ["mysql", "-h", "127.0.0.1","-u", "root", "-e", "SELECT 1"] initialDelaySeconds: 5 periodSeconds: 2 timeoutSeconds: 1 - name: xtrabackup image: gcr.io/google-samples/xtrabackup:1.0 ports: - name: xtrabackup containerPort: 3307 command: - bash - "-c" - | set -ex cd /var/lib/mysql # Determine binlog position of cloned data, if any. if [[ -f xtrabackup_slave_info &&"x$(<xtrabackup_slave_info)" != "x" ]]; then # XtraBackup already generated a partial "CHANGE MASTER TO"query # because we're cloning from an existing slave. (Need to remove thetailing semicolon!) cat xtrabackup_slave_info | sed -E 's/;$//g' >change_master_to.sql.in # Ignore xtrabackup_binlog_info in this case (it's useless). rm -f xtrabackup_slave_info xtrabackup_binlog_info elif [[ -f xtrabackup_binlog_info ]]; then # We're cloning directly from master. Parse binlog position. [[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1 rm -f xtrabackup_binlog_info xtrabackup_slave_info echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\ MASTER_LOG_POS=${BASH_REMATCH[2]}"> change_master_to.sql.in fi # Check if we need to complete a clone by starting replication. if [[ -f change_master_to.sql.in ]]; then echo "Waiting for mysqld to be ready (accepting connections)" until mysql -h 127.0.0.1 -u root-e "SELECT 1"; do sleep 1; done echo "Initializing replication from clone position" mysql -h 127.0.0.1 -u root \ -e"$(<change_master_to.sql.in), \ MASTER_HOST='mysql-0.mysql',\ MASTER_USER='root', \ MASTER_PASSWORD='', \ MASTER_CONNECT_RETRY=10; \ START SLAVE;" ||exit 1 # In case of container restart, attempt this at-most-once. mv change_master_to.sql.in change_master_to.sql.orig fi # Start a server to send backups when requested by peers. exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \ "xtrabackup --backup --slave-info --stream=xbstream--host=127.0.0.1 --user=root" volumeMounts: - name: data mountPath: /var/lib/mysql subPath: mysql - name: conf mountPath: /etc/mysql/conf.d resources: requests: cpu: 100m memory: 100Mi volumes: - name: conf emptyDir: {} - name: config-map configMap: name: mysql volumeClaimTemplates: -metadata: name: data spec: accessModes: ["ReadWriteOnce"] storageClassName: "nfs" resources: requests: storage: 2Gi
(3)創建服務,用於訪問mysql集群。
# Headless service for stable DNS entriesof StatefulSet members.
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
ports:
-name: mysql
port: 3306
clusterIP: None
selector:
app: mysql
---
# Client service for connecting to anyMySQL instance for reads.
# For writes, you must instead connect tothe master: mysql-0.mysql.
apiVersion: v1
kind: Service
metadata:
name: mysql-read
labels:
app: mysql
spec:
ports:
-name: mysql
port: 3306
selector:
app: mysql
2.3 MySQL StatefulSet實例
(1)一個StatefulSet對象
NAME DESIRED CURRENT AGE
statefulset.apps/mysql 2 2 2d
(2)三個Pod
[root@master1 ~]# oc get pod
NAME READY STATUS RESTARTS AGE
mysql-0 2/2 Running 0 2d
mysql-1 2/2 Running 0 2d
mysql-2 2/2 Running 0 2d
StatefulSet 控制器創建出三個Pod,每個Pod使用數字后綴來區分順序。創建時,首先mysql-0 Pod被創建出來,然后創建mysql-1 Pod,再創建mysql-2 Pod。
(3)兩個服務
[root@master1 ~]# oc get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE mysql ClusterIP None <none> 3306/TCP 2d mysql-read ClusterIP 172.30.169.48 <none> 3306/TCP 2d
mysql服務是一個Headless服務,它沒有ClusterIP,只是為每個Pod提供一個域名,三個Pod的域名分別是:
-
mysql-0.mysql.testmysql.svc.cluster.local
-
mysql-1.mysql.testmysql.svc.cluster.local
-
mysql-2.mysql.testmysql.svc.cluster.local
mysql-read 服務則是一個ClusterIP服務,作為集群內部的負載均衡,將數據庫讀請求分發到后端的兩個Pod。
(4)三個PVC
[root@master1 ~]# oc get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-mysql-0 Bound pvc-98a6f5c9-11a9-11ea-b651-fa163e71648a 2Gi RWO nfs 2d
data-mysql-1 Bound pvc-845c0eae-11bb-11ea-b651-fa163e71648a 2Gi RWO nfs 2d
data-mysql-2 Bound pvc-018762f6-11bc-11ea-b651-fa163e71648a 2Gi RWO nfs 2d
每個pvc和一個pod相對應,從名字上也能看出來其對應關系。mysql Pod的 /var/lib/mysql 文件夾保存在PVC卷中。
2.4 MySQL 集群操作
(1)集群訪問
客戶端通過 mysql-0.mysql.testmysql.svc.cluster.local 域名來向數據庫寫入數據:
[root@master1 ~]# mysql -h mysql-0.mysql.testmysql.svc.cluster.local -P 3306 -u root
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 142230
Server version: 5.7.28-log MySQL Community Server (GPL)
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MySQL [(none)]> show databases;
客戶端通過 mysql-read.testmysql.svc.cluster.local 域名來從數據庫讀取數據:
[root@master1 ~]# mysql -h mysql-read.testmysql.svc.cluster.local -P 3306 -u root
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 142318
Server version: 5.7.28-log MySQL Community Server (GPL)
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MySQL [(none)]> show databases;
(2)集群擴容
當前的MySQL集群,具有一個寫節點(mysql-0)和兩個讀節點(mysql-1和mysql-2)。如果要提升讀能力,可以對StatefulSet對象擴容,以增加讀節點。比如以下命令將總Pod數目擴大到4,讀Pod數目擴大到3.
oc scale statefulset mysql --replicas=4
(3)集群縮容
運行以下命令,將集群節點數目縮容到3:
oc scale statefulset mysql --replicas=3
然后mysql-3 Pod會被刪除:
[root@master1 ~]# oc get pod
NAME READY STATUS RESTARTS AGE
mysql-0 2/2 Running 0 2d
mysql-1 2/2 Running 0 2d
mysql-2 2/2 Running 0 2d
mysql-3 2/2 Terminating 0 2m
3. Kubernetes Operator
StatefulSet 無法解決有狀態應用的所有問題,它只是一個抽象層,負責給每個Pod打上不同的ID,並支持每個Pod使用自己的PVC卷。但有狀態應用的維護非常復雜,否則每個公司也不用有一個獨立的DBA團隊來負責管理數據庫。從上文也能看出,通過StatefulSet實例的操作,也只能做到創建集群、刪除集群、擴縮容等基礎操作,但比如備份、恢復等數據庫常用操作,則無法實現。
3.1 Kubernetes Operator概述
基於此,CoreOS團隊提出了K8SOperator概念。Operator是一個自動化的軟件管理程序,負責處理部署在K8S和OpenShift上的軟件的安裝和生命周期管理。它包含一個Controller和CRD(Custom Resource Definition),CRD擴展了K8S API。其基本模式如下圖所示:
OpenShift 在V4中發布了全新的OperatorHub,集成了原廠商的或第三方的或RedHat開發的各種Operator,用來部署和維護相應的服務。
Operator可以很簡單,比如只負責軟件安裝,也可以很復雜,比如軟件更新、完整生命周期管理、監控告警甚至自動伸縮等等。
3.2 MySQL Operator
一年以前,Oracle在github上開源了K8S MySQL Operator,它能在K8S上創建、配置和管理MySQL InnoDB 集群,其地址是https://github.com/oracle/mysql-operator。其主要功能包括:
-
在K8S上創建和刪除高可用的MySQL InnoDB集群
-
自動化數據庫的備份、故障檢測和恢復操作
-
自動化定時備份和按需備份
-
通過備份恢復數據庫
其基本架構如下圖所示:
定義一個1主2備MySQL集群:
apiVersion: mysql.oracle.com/v1alpha1
kind: Cluster
metadata:
name: mysql-test-cluster
spec:
members: 3
定義一個3主集群:
apiVersion: mysql.oracle.com/v1alpha1
kind: Cluster
metadata:
name: mysql-multimaster-cluster
spec:
multiMaster: true
members: 3
創建一個到S3的備份:
apiVersion: "mysql.oracle.com/v1"
kind: MySQLBackup
metadata:
name: mysql-backup
spec:
executor:
provider: mysqldump
databases:
- test
storage:
provider: s3
secretRef:
name: s3-credentials
config:
endpoint: x.compat.objectstorage.y.oraclecloud.com
region: ociregion
bucket: mybucket
clusterRef:
name: mysql-cluster
詳細信息,請閱讀 github項目文檔以及https://blogs.oracle.com/developers/introducing-the-oracle-mysql-operator-for-kubernetes博文。可惜的是,已經快有一年該項目沒什么更新了。
4. 展望未來
通過K8S Operator實現常見運維操作是容易的,但對於復雜問題,Operator要么會做得非常復雜,但也可能無法面面俱到,對某些復雜場景甚至會無能為力。以etcd Operator為例,其開源項目地址是 https://github.com/coreos/etcd-operator。etcd本身應該不算特別復雜的有狀態應用,etcd Operator的功能看起來也很基礎,主要包括創建和刪除集群、擴縮容、切換、滾動升級、備份和回復等基礎功能,但其代碼超過了9000行。
因此,Operator要解決“有“的問題還相對容易,但要解決”好“的問題,確實非常困難。這是因為管理有狀態應用本來就是非常困難的,更何況在容器雲平台上進行管理。從技術上講,維護有狀態數據非常困難。大量研究和方式都被提了出來,比如冗余、高可用等等,但問題並沒徹底解決。從商務上講,所有雲供應商都提供了托管數據庫服務。因此,他們沒有太大興趣去提供另一個會跟他們直接競爭的方案,也許Oracle沒繼續更新K8S MySQL Operator項目也有這方面的考慮。從實際情況來看,在傳統企業中,數據庫的架構變遷一直就很緩慢,很多企業的數據庫還部署在小機上,部分數據庫部署在x86物理機上,部分數據庫部署在虛擬機上。
因此,短期內,對於生產環境,需要有穩定性,因此如果你用公有雲,那就使用公有雲的各種托管服務,將你的精力更多用到業務應用自身上吧;如果你用私有雲,對生產環境來說,短期內有狀態應用還是放在虛擬化環境甚至物理機環境上,然后安排專業運維團隊來維護吧。對於開發測試環境,可以自己通過K8S StatefulSet來做編排或者使用Operator,來利用其便捷性。
但是,有狀態應用要想在K8S上生產就緒地運行,目前來看,Operator也許是最可行的路徑,這也是為什么RedHat在上面大量投入的原因。可以想象,在將來所有要發布在K8S上的應用,廠商在發布軟件時都會發布對應的Operator。其實現在已經有廠商這么做了,比如PingCAP公司已經發布了TiDB K8S Operator,其開源項目地址在https://github.com/pingcap/tidb-operator。在某種意義上,Operator也符合DevOps理念,因為開發人員通過編寫代碼做了本該是運維團隊干的事情。
讓我們一起期待Operator時代的到來吧!
參考鏈接:
-
Run a Replicated Stateful Application,https://kubernetes.io/docs/tasks/run-application/run-replicated-stateful-application/
-
Containerizing Stateful Applications,https://dzone.com/articles/containerizing-stateful-applications
-
The sad state of stateful Pods in Kubernetes, https://elastisys.com/2018/09/18/sad-state-stateful-pods-kubernetes/
-
劉超,微服務化之無狀態化與容器化,https://myopsblog.wordpress.com/2017/02/06/why-databases-is-not-for-containers/
感謝您的閱讀,歡迎關注我的微信公眾號: