k8s之有状态应用的一个demo--mysql主从模式
在K8S运行的服务,从简单到复杂可以分成三类:无状态服务、普通有状态服务和有状态集群服务。下面分别来看K8S是如何运行这三类服务的。
- 无状态服务,K8S使用RC(或更新的Replica Set)来保证一个服务的实例数量,如果说某个Pod实例由于某种原因Crash了,RC会立刻用这个Pod的模版新启一个Pod来替代它,由于是无状态的服务,新启的Pod与原来健康状态下的Pod一模一样。在Pod被重建后它的IP地址可能发生变化,为了对外提供一个稳定的访问接口,K8S引入了Service的概念。一个Service后面可以挂多个Pod,实现服务的高可用。
- 普通有状态服务,和无状态服务相比,它多了状态保存的需求。Kubernetes提供了以Volume和Persistent Volume为基础的存储系统,可以实现服务的状态保存。
- 有状态集群服务,与普通有状态服务相比,它多了集群管理的需求。K8S为此开发了一套以Pet Set为核心的全新特性,方便了有状态集群服务在K8S上的部署和管理。具体来说是通过Init Container来做集群的初始化工作,用 Headless Service 来维持集群成员的稳定关系,用动态存储供给来方便集群扩容,最后用Pet Set来综合管理整个集群。
有状态应用的特点:
- 唯一性 - 对于包含 N 个副本的 StatefulSet,每个 pod 会被分配一个 [0,N)范围内的唯一序号。
- 顺序性 - StatefulSet 中 pod 的启动、更新、销毁默认都是按顺序进行的。
- 稳定的网络身份标识 - pod 的主机名、DNS 地址不会随着 pod 被重新调度而发生变化。
- 稳定的持久化存储 - 当 pod 被重新调度后,仍然能挂载原有的 PersistentVolume,保证了数据的完整性和一致性。
要运行有状态集群服务要解决的问题有两个,一个是状态保存,另一个是集群管理。 我们先来看如何解决第一个问题:状态保存。Kubernetes 有一套以Volume插件为基础的存储系统,通过这套存储系统可以实现应用和服务的状态保存。
K8S的存储系统从基础到高级又大致分为三个层次:普通Volume,Persistent Volume 和动态存储供应。
1.普通Volume
最简单的普通Volume是单节点Volume。它和Docker的存储卷类似,使用的是Pod所在K8S节点的本地目录。
第二种类型是跨节点存储卷,这种存储卷不和某个具体的K8S节点绑定,而是独立于K8S节点存在的,整个存储集群和K8S集群是两个集群,相互独立。
跨节点的存储卷在Kubernetes上用的比较多,如果已有的存储不能满足要求,还可以开发自己的Volume插件,只需要实现Volume.go 里定义的接口。如果你是一个存储厂商,想要自己的存储支持Kubernetes 上运行的容器,就可以去开发一个自己的Volume插件。
2.persistent volume
它和普通Volume的区别是什么呢?
普通Volume和使用它的Pod之间是一种静态绑定关系,在定义Pod的文件里,同时定义了它使用的Volume。Volume 是Pod的附属品,我们无法单独创建一个Volume,因为它不是一个独立的K8S资源对象。
而Persistent Volume 简称PV是一个K8S资源对象,所以我们可以单独创建一个PV。它不和Pod直接发生关系,而是通过Persistent Volume Claim,简称PVC来实现动态绑定。Pod定义里指定的是PVC,然后PVC会根据Pod的要求去自动绑定合适的PV给Pod使用。
PV的访问模式有三种:
第一种,ReadWriteOnce:是最基本的方式,可读可写,但只支持被单个Pod挂载。
第二种,ReadOnlyMany:可以以只读的方式被多个Pod挂载。
第三种,ReadWriteMany:这种存储可以以读写的方式被多个Pod共享。不是每一种存储都支持这三种方式,像共享方式,目前支持的还比较少,比较常用的是NFS。在PVC绑定PV时通常根据两个条件来绑定,一个是存储的大小,另一个就是访问模式。
刚才提到说PV与普通Volume的区别是动态绑定,我们来看一下这个过程是怎样的。
这是PV的生命周期,首先是Provision,即创建PV,这里创建PV有两种方式,静态和动态。所谓静态,是管理员手动创建一堆PV,组成一个PV池,供PVC来绑定。动态方式是通过一个叫 Storage Class的对象由存储系统根据PVC的要求自动创建。
一个PV创建完后状态会变成Available,等待被PVC绑定。
一旦被PVC邦定,PV的状态会变成Bound,就可以被定义了相应PVC的Pod使用。
Pod使用完后会释放PV,PV的状态变成Released。
变成Released的PV会根据定义的回收策略做相应的回收工作。有三种回收策略,Retain、Delete 和 Recycle。Retain就是保留现场,K8S什么也不做,等待用户手动去处理PV里的数据,处理完后,再手动删除PV。Delete 策略,K8S会自动删除该PV及里面的数据。Recycle方式,K8S会将PV里的数据删除,然后把PV的状态变成Available,又可以被新的PVC绑定使用。
在实际使用场景里,PV的创建和使用通常不是同一个人。这里有一个典型的应用场景:管理员创建一个PV池,开发人员创建Pod和PVC,PVC里定义了Pod所需存储的大小和访问模式,然后PVC会到PV池里自动匹配最合适的PV给Pod使用。
前面在介绍PV的生命周期时,提到PV的供给有两种方式,静态和动态。其中动态方式是通过StorageClass来完成的,这是一种新的存储供应方式。
使用StorageClass有什么好处呢?除了由存储系统动态创建,节省了管理员的时间,还有一个好处是可以封装不同类型的存储供PVC选用。在StorageClass出现以前,PVC绑定一个PV只能根据两个条件,一个是存储的大小,另一个是访问模式。在StorageClass出现后,等于增加了一个绑定维度。
比如这里就有两个StorageClass,它们都是用谷歌的存储系统,但是一个使用的是普通磁盘,我们把这个StorageClass命名为slow。另一个使用的是SSD,我们把它命名为fast。
在PVC里除了常规的大小、访问模式的要求外,还通过annotation指定了Storage Class的名字为fast,这样这个PVC就会绑定一个SSD,而不会绑定一个普通的磁盘。
到这里Kubernetes的整个存储系统就都介绍完了。总结一下,两种存储卷:普通Volume 和Persistent Volume。普通Volume在定义Pod的时候直接定义,Persistent Volume通过Persistent Volume Claim来动态绑定。PV可以手动创建,也可以通过StorageClass来动态创建。
下面重介绍Kubernetes与有状态集群服务相关的两个新特性:Init Container 和 Pet Set 。
什么是Init Container?
从名字来看就是做初始化工作的容器。可以有一个或多个,如果有多个,这些 Init Container 按照定义的顺序依次执行,只有所有的Init Container 执行完后,主容器才启动。由于一个Pod里的存储卷是共享的,所以 Init Container 里产生的数据可以被主容器使用到。
Init Container可以在多种 K8S 资源里被使用到如 Deployment、Daemon Set, Pet Set, Job等,但归根结底都是在Pod启动时,在主容器启动前执行,做初始化工作。
我们在什么地方会用到 Init Container呢?
第一种场景是等待其它模块Ready,比如我们有一个应用里面有两个容器化的服务,一个是Web Server,另一个是数据库。其中Web Server需要访问数据库。但是当我们启动这个应用的时候,并不能保证数据库服务先启动起来,所以可能出现在一段时间内Web Server有数据库连接错误。为了解决这个问题,我们可以在运行Web Server服务的Pod里使用一个Init Container,去检查数据库是否准备好,直到数据库可以连接,Init Container才结束退出,然后Web Server容器被启动,发起正式的数据库连接请求。
第二种场景是做初始化配置,比如集群里检测所有已经存在的成员节点,为主容器准备好集群的配置信息,这样主容器起来后就能用这个配置信息加入集群。
还有其它使用场景,如将pod注册到一个中央数据库、下载应用依赖等。
这些东西能够放到主容器里吗?从技术上来说能,但从设计上来说,可能不是一个好的设计。首先不符合单一职责原则,其次这些操作是只执行一次的,如果放到主容器里,还需要特殊的检查来避免被执行多次。
这是Init Container的一个使用样例, 这个例子创建一个Pod,这个Pod里跑的是一个nginx容器,Pod里有一个叫workdir的存储卷,访问nginx容器服务的时候,就会显示这个存储卷里的index.html 文件。
而这个index.html 文件是如何获得的呢?是由一个Init Container从网络上下载的。这个Init Container 使用一个busybox镜像,起来后,执行一条wget命令,获取index.html文件,然后结束退出。
由于Init Container和nginx容器共享一个存储卷(这里这个存储卷的名字叫workdir),所以在Init container里下载的index.html文件可以在nginx容器里被访问到。
可以看到 Init Container 是在 annotation里定义的。Annotation 是K8S新特性的实验场,通常一个新的Feature出来一般会先在Annotation 里指定,等成熟稳定了,再给它一个正式的属性名或资源对象名。
介绍完Init Container,千呼万唤始出来,主角Pet Set该出场了。
什么是Pet Set?
在数据结构里Set是集合的意思,所以顾名思义Pet Set就是Pet的集合,那什么是Pet呢?我们提到过Cattle和Pet的概念,Cattle代表无状态服务,而Pet代表有状态服务。具体在K8S资源对象里,Pet是一种需要特殊照顾的Pod。它有状态、有身份、当然也比普通的Pod要复杂一些。
具体来说,一个Pet有三个特征。
一是有稳定的存储,这是通过我们前面介绍的PV/PVC 来实现的。
二是稳定的网络身份,这是通过一种叫 Headless Service 的特殊Service来实现的。要理解Headless Service是如何工作的,需要先了解Service是如何工作。我们提到过Service可以为多个Pod实例提供一个稳定的对外访问接口。这个稳定的接口是如何实现的的呢,是通过Cluster IP来实现的,Cluster IP是一个虚拟IP,不是真正的IP,所以稳定。K8S会在每个节点上创建一系列的IPTables规则,实现从Cluster IP到实际Pod IP的转发。同时还会监控这些Pod的IP地址变化,如果变了,会更新IP Tables规则,使转发路径保持正确。所以即使Pod IP有变化,外部照样能通过Service的ClusterIP访问到后面的Pod。
普通Service的Cluster IP 是对外的,用于外部访问多个Pod实例。而Headless Service的作用是对内的,用于为一个集群内部的每个成员提供一个唯一的DNS名字,这样集群成员之间就能相互通信了。所以Headless Service没有Cluster IP,这是它和普通Service的区别。
Headless Service为每个集群成员创建的DNS名字是什么样的呢?右下角是一个例子,第一个部分是每个Pet自己的名字,后面foo是Headless Service的名字,default是PetSet所在命名空间的名字,cluser.local是K8S集群的域名。对于同一个Pet Set里的每个Pet,除了Pet自己的名字,后面几部分都是一样的。所以要有一个稳定且唯一的DNS名字,就要求每个Pet的名字是稳定且唯一的。
三是序号命名规则。Pet是一种特殊的Pod,那么Pet能不能用Pod的命名规则呢?答案是不能,因为Pod的名字是不稳定的。Pod的命名规则是,如果一个Pod是由一个RC创建的,那么Pod的名字是RC的名字加上一个随机字符串。为什么要加一个随机字符串,是因为RC里指定的是Pod的模版,为了实现高可用,通常会从这个模版里创建多个一模一样的Pod实例,如果没有这个随机字符串,同一个RC创建的Pod之间就会由名字冲突。
如果说某个Pod由于某种原因死掉了,RC会新建一个来代替它,但是这个新建里的Pod名字里的随机字符串与原来死掉的Pod是不一样的。所以Pod的名字跟它的IP一样是不稳定的。
为了解决名字不稳定的问题,K8S对Pet的名字不再使用随机字符串,而是为每个Pet分配一个唯一不变的序号,比如 Pet Set 的名字叫 mysql,那么第一个启起来的Pet就叫 mysql-0,第二个叫 mysql-1,如此下去。
当一个Pet down 掉后,新创建的Pet 会被赋予跟原来Pet一样的名字。由于Pet名字不变所以DNS名字也跟以前一样,同时通过名字还能匹配到原来Pet用到的存储,实现状态保存。
这些是Pet Set 相关的一些操作:
- Peer discovery,这和我们上面的Headless Service有密切关系。通过Pet Set的 Headless Service,可以查到该Service下所有的Pet 的 DNS 名字。这样就能发现一个Pet Set 里所有的Pet。当一个新的Pet起来后,就可以通过Peer Discovery来找到集群里已经存在的所有节点的DNS名字,然后用它们来加入集群。
- 更新Replicas的数目、实现扩容和缩容。
- 更新Pet Set里Pet的镜像版本,实现升级。
- 删除 Pet Set。删除一个Pet Set 会先把这个Pet Set的Replicas数目缩减为0,等到所有的Pet都被删除了,再删除 Pet Set本身。注意Pet用到的存储不会被自动删除。这样用户可以把数据拷贝走了,再手动删除。
mysql主从搭建的一个demo
一、创建ConfigMap
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
在pod初始化时, pod会根据自身标识(master或slave)从configmap中应用指定配置. 被应用配置将覆盖my.cnf中的配置. 目的是使master向slave提供复制日志服务,并设置slave拒绝非复制写入.
二、创建headless service
# Headless service for stable DNS entries of StatefulSet members.
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
ports:
- name: mysql
port: 3306
clusterIP: None
selector:
app: mysql
三、创建StatefulSet
apiVersion: apps/v1beta1
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_ROOT_PASSWORD
value: "123",
- name: MYSQL_REPLICATION_USER
value: "sync",
- name: MYSQL_REPLICATION_PASSWORD
value: "sync",
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", "-uroot", "-p123", "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", "-uroot", "-p123", "-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 ]]; then
# XtraBackup already generated a partial "CHANGE MASTER TO" query
# because we're cloning from an existing slave.
mv xtrabackup_slave_info change_master_to.sql.in
# Ignore xtrabackup_binlog_info in this case (it's useless).
rm -f 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 xtrabackup_binlog_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 -uroot -p123 -e "SELECT 1"; do sleep 1; done
echo "Initializing replication from clone position"
# In case of container restart, attempt this at-most-once.
mv change_master_to.sql.in change_master_to.sql.orig
mysql -h 127.0.0.1 -uroot -p123 <<EOF
$(<change_master_to.sql.orig),
MASTER_HOST='mysql-0.mysql',
MASTER_USER='root',
MASTER_PASSWORD='123',
MASTER_CONNECT_RETRY=10;
START SLAVE;
EOF
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 --password=123"
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"]
resources:
requests:
storage: 10Gi
这里使用了两个初始化容器.
第一个初始化容器init-mysql作用包括根据pod名称后缀生成server_id文件, 从configmap中获取指定配置. 这里默认StatefulSet控制的第一个pod即mysql-0为master, 其余为slave.
第二个clone-mysql使用开源工具xtrabackup, 用于当前slave从前一个slave复制数据, 当有新的作为slave的pod加入集群时, 这个pod会从上一个pod(序号小1)的非master的pod复制数据, 因为新pod加入时, 我们必须假定master中的数据是不为空的, 需要进行一次复制操作, 这样在扩容时会避免数据丢失,
而从前一个pod复制数据是因为StatefulSet按顺序启动, 当前一个pod为ready时才启动后一个pod的机制而来.
当init容器执行完成后, 常规容器开始启动并运行. mysql Pods由运行mysqld服务的mysql容器和充当SideCar的xtrabackup容器组成
四、创建用于访问Service
# Client service for connecting to any MySQL instance for reads.
# For writes, you must instead connect to the master: mysql-0.mysql.
apiVersion: v1
kind: Service
metadata:
name: mysql-access
labels:
app: mysql
spec:
ports:
- name: mysql
port: 3306
selector:
app: mysql
五、查看
root@node4:~# kubectl -n admin-d2069c get pvc,pv,statefulset,pod,service,configmap |grep mysql
pvc/storage-mysql-0 Bound pvc-bb591208-c2f4-11e8-b599-0050568eef9f 512M RWX managed-nfs-storage 12m
pvc/storage-mysql-1 Bound pvc-c4ada169-c2f4-11e8-b599-0050568eef9f 512M RWX managed-nfs-storage 12m
pvc/storage-mysql-2 Bound pvc-ce20ad71-c2f4-11e8-b599-0050568eef9f 512M RWX managed-nfs-storage 11m
pv/pvc-bb591208-c2f4-11e8-b599-0050568eef9f 512M RWX Delete Bound admin-d2069c/storage-mysql-0 managed-nfs-storage 12m
pv/pvc-c4ada169-c2f4-11e8-b599-0050568eef9f 512M RWX Delete Bound admin-d2069c/storage-mysql-1 managed-nfs-storage 12m
pv/pvc-ce20ad71-c2f4-11e8-b599-0050568eef9f 512M RWX Delete Bound admin-d2069c/storage-mysql-2 managed-nfs-storage 11m
statefulsets/mysql 3 3 12m
po/mysql-0 2/2 Running 0 12m
po/mysql-1 2/2 Running 0 12m
po/mysql-2 2/2 Running 0 11m
svc/mysql ClusterIP None <none> 3306/TCP 12m
svc/mysql-access ClusterIP 10.68.8.223 <none> 3306/TCP 12m
cm/mysql 2 12m