一开始Kubernetes只是被设计用来运行无状态应用,直到在1.5版本中才添加了StatefulSet控制器用于支持有状态应用,但它直到1.9版本才正式可用。本文将介绍有状态和无状态应用,一个通过K8S StatefulSet来编排有状态应用的示例,以及当前有状态应用容器化现状及将来的发展趋势。
无状态应用(Stateless Application)是指应用不会在会话中保存下次会话所需要的客户端数据。每一个会话都像首次执行一样,不会依赖之前的数据进行响应。有状态的应用(Stateful Application)是指应用会在会话中保存客户端的数据,并在客户端下一次的请求中来使用那些数据。
以服务器端组件为例,判断它是有状态的还是无状态的,其依据是两个来自相同发起者的请求在服务器端是否具备上下文关系。如果是有状态的,那么服务器端一般都要保存请求的相关信息,每个请求可以使用以前的请求信息。而如果是无状态的,其处理的过程必须全部来自于请求所携带的信息,以及其他服务器端自身所保存的、并且可以被所有请求所使用的公共信息。最著名的无状态的服务器应用是WEB服务器。每次HTTP请求和以前都没有啥关系,只是获取目标URI。得到目标内容之后,这次连接就被杀死,没有任何痕迹。有状态的服务器应用有更广阔的应用范围,比如网络游戏等服务器。它在服务端维护每个连接的状态信息,服务端在接收到每个连接的发送的请求时,可以从本地存储的信息来重现上下文关系。这样,客户端可以很容易使用缺省的信息,服务端也可以很容易地进行状态管理。比如说,当一个用户登录后,服务端可以根据用户名获取他的生日等先前的注册信息;而且在后续的处理中,服务端也很容易找到这个用户的历史信息。
一个大型应用往往具有许多功能模块,很难简单地将其整体性地设计为有状态或无状态的,而往往将其整个架构分成两个部分,即无状态部分和有状态部分。业务逻辑部分往往作为无状态的部分,而将状态保存在有状态的中间件中,如缓存、数据库、对象存储、大数据平台、消息队列等。这样无状态的部分可以很容易的横向扩展,而状态保存到后端。而后端的中间件是有状态的,这些中间件设计之初,就考虑了扩容的时候状态的迁移、复制、同步等机制,不用业务层关心。
(来源:刘超博文)
通常应用会有如下几种状态数据:
(来源:刘超博文)
常见的Kubernetes控制器不合适处理有状态应用:
Kubernetes在1.9版本中正式发布的StatefulSet控制器能支持:
以在K8S中部署高可用的PostgreSQL集群为例,下面是其架构示意图:
该架构中包含一个主节点和两个副本节点共3个Pod,这三个Pod在一个StatefulSet中。Master Service是一个Headless服务,指向主Pod,用于数据写入;Replica Service也是一个Headless服务,指向两个副本Pod,用于数据读取。这三个Pod都有唯一名称,这样StatefulSet让用户可以用稳定、可重复的方式来部署PostgreSQL集群。StatefulSet不会创建具有重复ID的Pod,Pod之间可以通过稳定的网络地址互相通信。
当前命名空间为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
(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-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卷中。
(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
StatefulSet 无法解决有状态应用的所有问题,它只是一个抽象层,负责给每个Pod打上不同的ID,并支持每个Pod使用自己的PVC卷。但有状态应用的维护非常复杂,否则每个公司也不用有一个独立的DBA团队来负责管理数据库。从上文也能看出,通过StatefulSet实例的操作,也只能做到创建集群、删除集群、扩缩容等基础操作,但比如备份、恢复等数据库常用操作,则无法实现。
基于此,CoreOS团队提出了K8SOperator概念。Operator是一个自动化的软件管理程序,负责处理部署在K8S和OpenShift上的软件的安装和生命周期管理。它包含一个Controller和CRD(Custom Resource Definition),CRD扩展了K8S API。其基本模式如下图所示:
OpenShift 在V4中发布了全新的OperatorHub,集成了原厂商的或第三方的或RedHat开发的各种Operator,用来部署和维护相应的服务。
Operator可以很简单,比如只负责软件安装,也可以很复杂,比如软件更新、完整生命周期管理、监控告警甚至自动伸缩等等。
一年以前,Oracle在github上开源了K8S MySQL Operator,它能在K8S上创建、配置和管理MySQL InnoDB 集群,其地址是https://github.com/oracle/mysql-operator。其主要功能包括:
其基本架构如下图所示:
定义一个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博文。可惜的是,已经快有一年该项目没什么更新了。
通过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时代的到来吧!
参考链接: