概述
Kubernetes 中有很多的控制器, 比如常用的 ReplicaSet,Deployment,DaemonSet,StatefulSet 等. 以这四种为例, 我们可以将它们分为两种, 一种为有状态控制器, 一种为无状态控制器, StatefulSet 则为有状态控制器, 通常用于管理有状态的服务, 如: MySQL,Redis,MongoDB 等.
有状态控制器通常有以下几个特点:
稳定的, 唯一的网络标识
稳定的, 持久的存储
有序的, 优雅的部署和伸缩
有序的, 优雅的删除和停止
有序的, 自动的滚更新
也就是说如果我们的应用如有以上的任何一个特点我们就可以使用 statefulset 控制器来完成应用的部署.
网络标识:
Pod 名称: 唯一且不会发生变化, 由 $(statefulset name)-(0-N)组成, N 为一个无限大的正整数, 从 0 开始.
容器内计算机名: 与 Pod 名称一至且不会发生变化.
DNS A 记录: 每一个副本都拥有唯一且不变的一条 A 记录指定自己, 格式组成:$(pod name).$(service name).$(namespace name).svc.
唯一的 Pod 标签, 通过 StatefulSet 创建的每个副本都拥有一个唯一的标签, 格式为: statefulset.kubernetes.io/pod-name=$(pod name), 通常可以将新的 service 单独关联到此标签, 来解决某个 Pod 的问题等.
以上的标识只要配置不发生改变, 发生重启, 升级, 删除后再创建这些标识都不会发生变化.
备注: Pod 的 IP 地址会发生变化, 但我们也可以使用另外的方案来结合 StatefulSet 实现固定 IP 的方案, 通常不推荐这么做.
持久存储:
每个 Pod 都对应一个 PVC,PVC 的名称组成:$(volumeClaimTemplates name)-$(pod name)-(0-N),N 为一个无限大的正整数, 从 0 开始.
当 Pod 被删除的时候不会自动删除对应的 PVC, 需要手动删除.
每次 Pod 升级, 重启, 删除重新创建后都呆以保证使用的是首次使用的 PVC, 保证数据的唯一性与持久化.
有序:
当部署有 N 个副本的 StatefulSet 时候, 严格按照从 0 到 N 的顺序创建, 并且下一个 Pod 创建的前提是上一个 Pod 已经 Running 状态.
当删除有 N 个副本的 StatefulSet 时候, 严格按照从 N 到 0 的顺序删除, 并且下一个 Pod 删除的前提是上一个 Pod 已经完全 Delete.
当扩容 StatefulSet 副本的时候, 每增加一个 pod 前提是上一个 Pod 已经 Running 状态.
当减少 StatefulSet 副本的时候, 每删除一个 pod 前提是上一个 Pod 已经完全 Delete.
当升级 StatefulSet 的时候, 严格按照从 N 到 1 的顺序升级, 并且下一个 Pod 升级的前提是上一个 Pod 已经 Running 状态.
无头服务(headless service)
无头服务是 Kubernetes 中 service 类型的一种, 为什么需要无头服务? 通常我们在使用 Deployment 部署 Pod 的时候名称是随机的字符串, Pod 也是无序的. 但 StatefulSet 是有序且名称唯一, 每一个 Pod 不可以被取代, 所以这时候就需要使用无头服务来实现.
无头服务特点: 当使用 Kubernetes 内 DNS 解析去访问 Service 的时候, 会将解析的 IP 指向 Service 后端的 Pod. 当无头服务与 StatefulSet 控制器使用的时候, 因为每个 Pod 的数据可能是不一样的, 所以在 Service 名前面再加了一个 Pod 名, 这样我们的每个 StatefulSet 控制器的 Pod 就拥有了唯一的网络标识(A 记录).
无头服务案例:
- apiVersion: v1
- kind: Service
- metadata:
- namespace: test
- name: MySQL-test
- spec:
- selector:
- App: MySQL-test
- clusterIP: None
- ports:
- - port: 3306
- targetPort: 3306
ClusterIP: 当此项的值为 "None" 的时候, 此服务就为无头服务, Kubernetes 不会给此服务分配 IP 地址. 当使用 type 为 NodePort 的时候不可以使用无头服务.
volumeClaimTemplates:
此为 Kubernetes 中的一个对象, 对象的路径为 "statefulset.spec.volumeClaimTemplates", 用于给 StatefulSet 的 Pod 申请 PVC, 称为卷申请模板, 它会为每个 Pod 生成不同的 PVC, 关绑定到 PV, 从而实现 Pod 的专有存储.
通常我们在使用 Deployment 中使用 "deployment.spec.template.spec.volumes" 与 PVC 建立关联的时候, 所有的副本使用的是相同的 PV 与 PVC, 他们的数据是相同的. 而 StatefulSet 为实现各 Pod 的专有存储, 所以才有了 "statefulset.spec.volumeClaimTemplates" 此方法.
实际应用场景
以 MySQL 主从为例, 我们将实现通过同一个 StatefulSet 控制器创建出主库和从库, 主库和从库的 my.cnf 配置自定义, 和从库的自动扩容(目前实现半自动)
思路:
使用 ConfigMap 实现主库和从库的自定义配置
使用 initContainers 实现主容器在启动之前初始化配置
https://www.qikqiak.com/post/pod-init-container/
关于 initContainers 简单介绍
从上图我们可以看出一个 Pod 的生命周期包含: initContainer,PostStart,PreStop,Liveness 和 Readiness. 其中容器 (Container) 的生命周期只包含 PostStart,PreStop,Liveness 和 Readiness, 而 initContainer 是属于在容器启动之前的.
initContainer 应用场景:
可以解决服务之间的依赖问题, 如我们有一个 web 服务和 MySQL, 如果我们将两个服务同时启动有可能会出现数据库连接异常等问题, 那么我们可以通过 initContainer 去监测当只有 MySQL 启动成功的时候再启动 Web.
做初始化配置, 如我们现在做的 MySQL 主从案例, 主库与从库 my.cnf 的 server_id 不一样, 我们无法使用同一配置, 为减少管理上的麻烦也不推荐使用两个 StatefulSet 控制器, 那么这时候我们就可以使用 initContainer 来完成我们的需求. 我们可以将数据卷挂载到 initContainer, 在 initContainer 内产生的数据是可以被主容器所使用的, 所以我们 server_id 通过 initContainer 内生成并写入 my.cnf.
在此不对初始化容器做过多介绍, 详情可参考: 初始化容器 https://www.qikqiak.com/post/pod-init-container/ , 或者查看官方文档.
首先创建名称空间
- apiVersion: v1
- kind: Namespace
- metadata:
- name: test
定义 ConfigMap 配置文件
- apiVersion: v1
- kind: ConfigMap
- metadata:
- namespace: test
- name: MySQL-cnf
- data:
- master.cnf: |
- [mysqld]
- log-bin=MySQL-bin
- binlog-ignore-db=information_schema,performance_schema,MySQL,sys
- expire_logs_days=7
- slave.cnf: |
- [mysqld]
- slave_parallel_workers=16
- slave_parallel_type=logical_clock
- master_info_repository=TABLE
- relay_log_info_repository=TABLE
- relay_log_recovery=ON
根据自身情况, 可自定义自己的选项. 其中 server_id 不需要指定, 将在初始化容器的时候写入.
定义 StatefulSet 配置文件
- apiVersion: apps/v1
- kind: StatefulSet
- metadata:
- namespace: test
- name: MySQL-test
- spec:
- serviceName: MySQL-test
- replicas: 2
- selector:
- matchLabels:
- App: MySQL-test
- template:
- metadata:
- namespace: test
- name: MySQL-test
- labels:
- App: MySQL-test
- spec:
- initContainers:
- - name: init-MySQL
- image: MySQL:5.7
- command:
- - bash
- - "-c"
- - |
- hostname=`echo $HOSTNAME | awk -F '-' '{print $NF}'`
- server_id=$[hostname+100]
- if [[ $hostname -eq 0 ]]; then
- cp /mnt/my.cnf.d/master.cnf /mnt/conf.d/my.cnf
- else
- cp /mnt/my.cnf.d/slave.cnf /mnt/conf.d/my.cnf
- fi
- echo "server_id=${server_id}" >> /mnt/conf.d/my.cnf
- volumeMounts:
- - name: MySQL-cnf
- mountPath: /mnt/my.cnf.d/
- - name: conf
- mountPath: /mnt/conf.d/
- containers:
- - name: MySQL-test
- image: MySQL:5.7
- args: ["--character_set_server=utf8","--collation-server=utf8_unicode_ci"]
- env:
- - name: MYSQL_ROOT_PASSWORD
- value: "!gogen123"
- volumeMounts:
- - name: conf
- mountPath: /etc/MySQL/conf.d/
- - name: MySQL-data
- mountPath: /var/lib/MySQL/
- volumes:
- - name: MySQL-cnf
- configMap:
- name: MySQL-cnf
- - name: conf
- emptyDir: {
- }
- volumeClaimTemplates:
- - metadata:
- namespace: test
- name: MySQL-data
- spec:
- accessModes: ["ReadWriteOnce"]
- resources:
- requests:
- storage: 2Gi
- storageClassName: nas
- ---
- apiVersion: v1
- kind: Service
- metadata:
- namespace: test
- name: MySQL-test
- spec:
- selector:
- App: MySQL-test
- clusterIP: None
- ports:
- - port: 3306
- targetPort: 3306
initContainers: 该配置段主要实现主容器启动时的配置初始化, 初始化容器. 因为 StatefulSet 控制器拥有唯一的网络标识, 如计算机机名是唯一, 所以我们根据计算机名做判断 , 让一个副本做 Master, 其余的做 Slave, 从而将不同的配置复制到共享数据卷 "conf". 此卷由我们自己定义, 并采用 emptyDir 方式, 前面我们讲了在 initContainers 中产生的数据可以在主容器中使用, 那么同样主容器也挂载此数据卷, 那么就可以使用我们自定义的配置, 我们通过 volumes 直接将 conf 挂载到 / etc/MySQL/conf.d / 目录即可使用.
volumeClaimTemplates: 关于此块配置用于自动申请 pvc 并于 pv 进行关联, 因为我们使用的是阿里云的 NAS, 默认情况下无法自动申请成功, 需要定义一个存储类(storageClassName), 具体此参数的值可以通过 pv 的配置获取, 可以将 pv 输出为 YAML 查看.
最后:
我们的应用已经启动成功, 还剩下最后一步. 进入从库的 MySQL Shell 内执行以下命令.
- change master to master_host='mysql-test-0.mysql-test.test.svc',master_port=3306,master_user='root',master_password='!gogen123',master_log_file='mysql-bin.000003',master_log_pos=154;
- start slave;
正常情况下这时候查看我们的从库状态就已经成功, 如果需要扩容, 扩容后的从库同样也只需要执行上面两条 SQL 命令即可.
总结: 最后一步还需要手动完成, 不是特别完善, 也大大提高了我们部署时的麻烦, 扩容也相对方便, 后续考虑将最后一步也实现自动化.
来源: http://blog.51cto.com/270142877/2366067