构建有状态应用,K8S 究竟准备好了吗?
技术
作者: 金戈
译者:
2018-03-21 03:30

大家好我叫金戈,目前就职于杭州沃趣科技,负责基于 Kubernetes 数据库私有云的研发工作。今天和大家分享的主题是《Is Kubernetes Ready for Statefulset Workloads》,为什么要分享这样一个主题呢?

一方面我们公司自身在基于 Kubernetes 的数据库私有云方面做了很多尝试,尝到了不少甜头,也踩了不少坑。其次,如果大家对 Kubernetes 圈子比较熟悉的话,可能也听说过这个大神,Kelsey Hightower,他以前是 CoreOS 的首席布道师,现在在谷歌也为 Kubernetes 做一些布道的工作。最近,他在 Twitter 上表示:“Kubernetes 虽然为构建有状态的应用,比如数据库和消息队列做了很多提升,但是我还是不想让它们在 Kubernetes 上面运行。”

这件事也引发了我的以下思考:

  • Kubernetes 为构建有状态的应用提供了哪些资源?
  • 基于这些资源我们到底能不能将有状态的应用,比如数据库运行到 Kubernetes 上面。

今天我将把自己的思考,结合我们在数据库私有云方面的一些实践给大家做一个分享。我的分享大概会分成以下三个部分: 

  • 以数据库为例,介绍 Kubernetes 为构建有状态的应用提供了哪些资源;
  • 如何结合这些资源来构建有状态的应用;
  • 分享我们在实现过程中遇到的问题以及思考。

K8S 能提供哪些资源

谈到有状态应用,我想大家可能首先想到的问题和我想的一样,那就是如何让数据存下来? 我们都知道,容器中的数据是易丢失的。Pod 宕掉,或者节点挂掉,Statefulset 或者 ReplicaSet 新创建一个 Pod,之前写到 Pod 中的数据不会保存下来。

Persistent Volume究竟应该怎样实现数据的持久化呢?Kubernetes 为我们提供了 PV 这个资源类型,基于 PV 能够将卷提供给容器。以创建一个数据库的 Pod 为例,通过 volumes 中的 mysql-data 卷,这个卷是由 Google Cloud 分配的云硬盘,然后用 volumeMounts,将这个卷挂载 Pod 的目录 /var/lib/mysql,MySQL 把数据写到 /var/lib/mysql 目录,从而写入了到云硬盘当中,就能完成数据的持久化了。

但是基于 PV 的方式创建有状态应用有一个很明显的问题就是,我们对应用的创建还需要感知具体的存储的细节,如何将存储细节与 PV 解耦?

Kubernetes 为我们提供了 PVC 这样的一个资源类型,基于 PVC 我们只需要从开发者的角度,描述对于卷的需求,比如需要要多大的卷,卷的读写方式是什么样的?然后Kubenetes 就会自动为我们找到满足需求的卷,从而屏蔽了应用对于存储细节关注。

然后基于 Storage Class 和 Provisioner,通过 Storage Class 定义不同类型,不同规格的存储类。然后通过 Provisioner 动态地提供存储资源,比如我们定义一个 production 类型的存储类,专门用来提供 Google Cloud 当中 SSD 类型的卷,然后再创建一个 development 的类型存储类,专门用来提供 Google Cloud 当中 HDD 类型的卷,从而实现了一个集群能够分配不同类型、不同规格存储的需求。 当然,有些时候我们想使用的存储类型 Kubernetes 并不支持,那么这个时候我们就可以通过 Provisioner 扩展 Kubernetes 的存储类型,从而满足我们自身对于特定存储的要求。

StatefulsetPV、PVC、Storage Class 和 Provisioner 等资源类型解决了数据持久化的问题,那么如何在 Kubernetes 中描述有状态的应用呢?

以创建 MySQL 的读写集群为例Kubernetes 为我们提供了 Statefulset,以创建一个 MySQL 的读写集群为例:

  • 对于 MySQL 的主库提供写的能力,我们基于一个独立的 Statefulset 来创建。
  • 对于 MySQL 的从库提供读的能力,我们基于一个 Statefulset 来创建一组规格相同的从库;创建 MySQL 从库的配置文件中,通过 volumeClaimTemplates 定义规格相同的数据卷;然后在 Statefulset 创建 Pod 的时候,创建对应的卷并且将卷和对应的 Pod 一一绑定,从而实现 Pod 数据的持久化。

Statefulset 的特性

•  Ordinal pod 是有序的Pod 的序号,在 Statefulset 当中是全局唯一的,结合到我们数据库的一个需求,server-id 在数据库集群当中,每个数据库也需要全局唯一。那么我们就可以将 Pod 的序号作为实例的 server-id,从而实现 server-id 的独立性。
•  Pod 具有固定名称并且和卷一一绑定结合监控系统,就能够非常方便地追踪监控和日志的信息,不会因为 Pod 宕掉名称发生改变,而造成前后标示不一致的问题。
•  稳定的网络 ID有了稳定的网络 ID,MySQL 的 Master 节点就可以很方便地找到任意一个 slave 节点。
•  正序创建 Pod,逆序终止 Pod这样在从库的扩展和收缩的时候,就会具有非常好的可预见性,方便 DBA 管理。 

基于 Statefulset,仅仅只是将数据库集群通过不同的角色划分,提供不同的 Statefulset,可以将其纳入到 Kubernetes 的管理当中。

 Service

如何实现数据库集群的读写分离功能?这个时候就要用到 Kubernetes 的 service 和数据库的中间件了,为集群的主库创建一个独立的 service。为 Statefulset 创建的一组从库创建一个 service,然后引入一个 MySQL 的中间件 Proxy。将写的请求发送给主库的 service,将读的请求发送给从库的 service,从库的 service 自身就具有 LoadBalance 的特性,从而将读请求发送给每个后端的 slave 节点,实现负载均衡。

当我的某一个从库出现故障的时候,基于 ReadinessProbe,service 会自动地将对应的从库从 ReadinessProbe 当中摘除,从而完成读故障的隔离。

创建数据库集群

基于 Kubernetes 为我们提供的基础资源,可以创建数据库集群。其实,整个集群创建的配置还是比较复杂的,创建主库 Statefulset、从库 Statefulset、Proxystatefulset,然后主库、从库和 Proxy 都需要对应的 service。

到目前为止,其实我们还没有讲到,PVC 如何管理、Proxy 如何配置、从库如何同步主库等问题。可见基于 Kubernetes 提供的基础资源,管理数据库集群还是比较复杂的。对于像数据库集群这样的复杂应用,最好以 CRD 的方式抽象出数据库集群的资源。抽象 MySQLCluster、Proxy,然后通过 Operator 来管理数据库集群。

可以看到有很多管理有状态应用的 Operator,并没有对应 MySQL 数据库集群管理的 Operator。当然,值得提到的一点的就是 CNCF 将 Youtube 开源的 MySQL 的中间件 Vitess 纳入到了其项目当中,也说明了云原生组织对于有状态应用的重视程度。

Operator 监听到 mysqlcluster 资源的创建,会创建 mysqldatabase 中 e2e-rw-mariadb10210-mycat0 作为主库,然后再基于 e2e-rw-mariadb10210-mycat0 主库,自动的创建 e2e-rw-mariadb10210-mycat1 从库。最终落到 Kubernetes 的资源,mysqldatabase 创建一个主库的 Statefulset,然后创建从库的一个 Statefulset,然后也会帮我们创建 Proxy 的 Statefulset。当然还有每个组件的 service,我们的 Operator 可以帮我们创建并且集成到整个数据库集群当中。

实践中遭遇问题

下面结合我们的实践分享,在有状态应用当中遇到的一些问题。

滚动升级问题在基于 Operator 管理数据库集群过程中,发现有一个比较常见的需求,需要对数据库集群资源做一个更新。比如对我们数据库集群中的主库、备库的 CPU 和内存做更新,比较稳妥的做法,先更新我们备库的 CPU 内存资源;等备库的 CPU 内存资源更新成功之后,再更新主库的 CPU、内存等资源,更新成功之后,再更新 Proxy 的 CPU、内存资源;这其实就是一种滚动升级的方式。

Operator 在升级过程中重启

基于我们创建的 Operator 来做滚动升级,有可能由于主机的重启,或者节点故障,或者人为的故障 Operator 重启,这会出现一个什么问题呢? 比如我们在做滚动升级的时候,已经完成从库资源的升级。此时 Operator 正好重启了,重启之后发现主从两个库全部都正常运行。 Operator 会认为我们这个数据库集群是 OK 的,但是此时,主库的资源还没有完成更新。

如何确定哪些资源已经完成更新?

虽然 Kubernetes 提供了 CRD 作为扩展方式,但并没有提供一个 RollingUpdate 的机制。通过学习 Kubernetes 源码,了解 Kuberenetes 内部滚动升级机制,其实就是通过 Kubernetes 中的 ControllerRevision 记录资源对象所有的历史版本。 然后再结合到资源类型自身的状态字段,将资源类型的状态维护到资源状态信息中,当前的 revision 是哪个版本,历史的 revision 是哪个版本,当前版本和历史版本是不是一致,资源哪些是 ready 的,哪些没有 ready,从而在滚动升级时帮助我们确认哪些子资源没有升级。

性能的问题我们采用计算和存储分离的架构,它的好处有很多。CPU 和内存等资源由 Kubernetes 管理,而存储资源是我们自定义的一个分布式存储,那么整体来讲的话,计算和存储分离的架构能提高我们数据库实例的部署密度,而且架构更清晰,扩展也会比较方便。但是带来的问题也是有的。

IO 路径更长

第一个问题,计算与存储分离的架构会导致整个的 IO 路径会更长。比如说以前,数据库可能只需要写到本地的磁盘上面,现在我们数据库的 IO 请求会发送到分布式存储,然后由于分布式存储可能又需要实现多副本这样的一个需求,所以分布式存储的 IO 可能会需要落到多台节点,然后再返回,这样的话整个 IO 开销就比较明显了。 

无法对 Pod 限流

第二个问题,因为我们的 Kubernetes 是一个完全的基于裸机的部署,原生 Kubernetes 又无法针对 Pod 进行限流。而数据库本身就是一个延迟敏感型的应用。当我的网络出现问题的时候,严重影响数据库的 QPS 和 TPS。那么刚才我们也讨论了一下,就发现这个原生 Kubernetes 确实无法对我们的 Pod 进行限流,但是可以基于 OS 的 netns 机制做一些改造。

计算和存储之间网络的延迟

第三个问题,对于计算与存储分离的性能优化并结合到我们数据库的特性,由于计算和存储之间网络的延迟是无法避免的,思路转为减少 IO 发生的次数。再结合到 MySQL  数据库的特性,MySQL 数据库中有个 DoubleWrite,简单来讲就是通过多写冗余数据保证写到错误页的时候,能够将写的数据恢复过来。但是如果我们的文件系统支持原子写的话,也就是说你写的 IO 发给我,我一定保证的数据是正确的落盘,那么数据库层就不需要开启 DoubleWrite 这个参数了。

上图是我们测试的一个最终对比结果,当 DoubleWrite 关闭的时候,我们的 TPS 和 QPS 都上升了接近 30%,IOPS 也提升了 20%,整体的延迟也是下降了 39% 左右。 今天,结合 Kubernetes 在有状态的应用当中所提供的一些资源,然后给大家分享一下如何构建一个数据库的集群,那么希望对大家能够有所帮助,谢谢。

                                                    金戈/沃趣科技高级技术专家

沃趣科技高级技术专家,负责沃趣科技所有产品线监控系统的研发工作,负责沃趣科技 RDS 的原型开发工作,精通监控系统架构设计与开发,致力于自动化智能化 IT 基础设施平台建设,有多年系统运维和监控系统开发经验。

813 comCount 0