沃趣科技—容器化RDS(四)—计算存储分离架构下的_Split-Brain-熊中哲

  • 格式:pdf
  • 大小:2.77 MB
  • 文档页数:13

如果采用本地存储作为数据库实例的存储介质, 试想一下, 一个 Storage Qos 要求是 Flash
的数据库实例无法调度到离线计算集群, 哪怕离线计算集群 CPU, Memory 有大量空闲.
计算存储分离为实现离线(ODPS)/在线集群的混合部署提供了可能.
结合 Kubernetes, Docker 和 MySQL, 进一步细化架构图, 如下图所示 :
访问架构图如下所示:
补充一句, 借助 ETCD 集群的高可用强一致, 得以保证 Kubernetes 集群元信息的一致性,
● ● ● ●
ETCD 基于 Raft 算法实现 Raft 算法是一种基于消息传递(state machine replicated)且具有高度容错(fault
tolerance)特性的一致性算法(consensus algorithm).
发生”Split-Brain”, 导致 Data Corruption. 数据丢失, 损失无法弥补.
下面是枯燥的故障复现, 通过日志和代码分析驱逐的工作机制, 总结”Split-Brain”整个过程. 测试过程 :
● ● ● ● ● ● ● ●
使用 Statefulset 创建 MySQL 单实例 gxr-oracle-statefulset (这是一个 Oracle DBA 取 的名字,请原谅他) Scheduler 将 MySQL 单实例调度到集群中的节点 “k8s-node3” 通过 sysbench 对该实例制造极高的负载, “k8s-node3” load 飙升, 导致“k8s-node3” 上 的 Kubelet 无法跟 API Server 通讯, 并开始报错 Node Controller 启动驱逐 Statefulset 发起重建 Scheduler 将 MySQL 实例调度到 “k8s-node1” 上 新旧 MySQL 实例访问同一个 Volume 数据文件被写坏, 新旧 MySQL 实例都报错, 并无法启动

通过 kubectl 可以获得节点的信息

时间点 December 1st 2017, 10:18:1ntroller 发现 k8s-node3 的状态有 32s 没有发生更新. ○ ready / outofdisk / diskpressure / memorypressue condition
一切多么的美好的. 是不是可以得到这个结论 :
借助 Kubernetes 的原生组件 Node Controller, Scheduler 和原生 API Statefulset, 加上 计算存储分离架构, 并将成熟的分布式文件系统集成到 Kubernetes 存储系统, 就能提 供私有 RDS 服务.
同时, 这套架构也带给我们更加简单, 通用, 高效的 High Availability 方案. 当集群中某个 Node
不可用后, 借助 Kubernetes 的原生组件 Node Controller, Scheduler 和原生 API Statefulset 即
可将数据库实例调度到其他可用节点, 以实现数据库实例的高可用.
kubelet 日志报错, 无法通过 API Server 更新 k8s-node3 状态.
Kubelet 细节如下: ● 通过 API Server 更新集群信息
if kl.kubeClient != nil { // Start syncing node status immediately, this may set up things the runtime needs to run. go wait.Until(kl.syncNodeStatus, kl.nodeStatusUpdateFrequency, wait.NeverStop) }
withoutError(kl.setNodeOODCondition), withoutError(kl.setNodeMemoryPressureCondition), withoutError(kl.setNodeDiskPressureCondition), withoutError(kl.setNodeReadyCondition), withoutError(kl.setNodeVolumesInUseStatus), withoutError(kl.recordNodeSchedulableEvent), } }
Raft 是大名鼎鼎的 Paxos 的简化版本 如果对于 Raft 算法的实现有兴趣,可以看看 https:///goraft/raft
所有感兴趣一致性算法的同学, 都值得花精力学习. 基于 goraft/raft, 我实现了 Network Partition Failures/Recovery TestCase, 收获不小.
测试参数:

kube-controller-manager 启动参数

kubelet 启动参数
基于日志, 整个事件流如下: ●
时间点 December 1st 2017, 10:18:05.000 (最后一次更新成功应该是 10:17:42.000):
节点(k8s-node3)启动数据库压力测试, 以模拟该节点”假死”, kubelet 跟 API Server 出 现心跳丢失
这和我们在私有 RDS 上的技术选型不谋而合. 尤其是计算存储分离架构.
在我们看来, 其最大优势在于: ● ● 计算资源 / 存储资源独立扩展, 架构更清晰, 部署更容易. 将有状态的数据下沉到存储层, Scheduler 调度时, 无需感知计算节点的存储介质, 只需 调度到满足计算资源要求的 Node, 数据库实例启动时, 只需在分布式文件系统挂载
mapping volume 即可. 可以显著的提高数据库实例的部署密度和计算资源利用率
以阿里巴巴为例, 考虑到今时今日它的规模, 如果能够实现数据库服务的离线(ODPS)/在线 集群的混合部署, 意义极其重大. 关键问题在于, 离线(ODPS)计算和在线计算对实时性要求 不同, 硬件配置也不同, 尤其是本地存储介质, ● ● 离线(ODPS)以机械磁盘为主 在线以 SSD / Flash 为主.
不管是架构选型还是生活, 绝大多数时候都是在做 trade off, 收获了计算存储分离带来的好处, 也意味着要忍受它带来的一些棘手问题. 本文尝试结合 Kubernetes, Docker, MySQL 和计算存 储分离架构, 分享我们遇到的诸多问题之一 “Split-Brain”.
2018 年 1 月 19 号参加了阿里巴巴双十一数据库技术峰会, 见到了好多老同事. 也了解到业界 最新的数据库技术发展趋势 : ● ● ● 数据库容器化作为下一代数据库基础架构 基于编排架构管理容器化数据库 采用计算存储分离架构
问题二 : 缺乏有效的 Fence 机制
在这个情况下, 借助 Kubernetes 的原生组件 Node Controller, Scheduler 和原生 API Statefulset 实现的 Failover, 将数据库实例从 Unknown 节点驱逐到可用节点, 但对原 Unknown 节点不做任何操作. 这种”软驱逐”, 将会导致新旧两个数据库实例同时访问同一份数据文件.

将该节点状态更新为 UNKNOWN

每隔 NodeMonitorPeriod 继续节点状态是否有更新

定期(nodeMonitorPeriod)查看一次节点状态
// Incorporate the results of node status pushed from kubelet to master. go wait.Until(func() { if err := nc.monitorNodeStatus(); err != nil { glog.Errorf("Error monitoring node status: %v", err) } }, nc.nodeMonitorPeriod, wait.NeverStop)

nodeMonitorPeriod 默认 5 秒, 测试时 4s
NodeMonitorPeriod: metav1.Duration{Duration: 5 * time.Second},

当超过 NodeMonitorGracePeriod 时间后, 节点状态没有更新将节点状态设置成 unknown

定期(nodeStatusUpdateFrequency)更新对应节点状态 nodeStatusUpdateFrequency 默认时间为 10 秒, 测试时设置的是 8s
obj.NodeStatusUpdateFrequency = metav1.Duration{Duration: 10 * time.Second}

更新如下信息:
func (kl *Kubelet) defaultNodeStatusFuncs() []func(*v1.Node) error { // initial set of node status update handlers, can be modified by Option's withoutError := func(f func(*v1.Node)) func(*v1.Node) error { return func(n *v1.Node) error { f(n) return nil } } return []func(*v1.Node) error{ kl.setNodeAddress, withoutError(kl.setNodeStatusInfo),
if nc.now().After(savedNodeStatus.probeTimestamp.Add(gracePeriod)) { // NodeReady condition was last set longer ago than gracePeriod, so update it to Unknown // (regardless of its current value) in the master. if currentReadyCondition == nil { glog.V(2).Infof("node %v is never updated by kubelet", ) node.Status.Conditions = append(node.Status.Conditions, v1.NodeCondition{ Type: v1.NodeReady, Status: v1.ConditionUnknown, Reason: "NodeStatusNeverUpdated", Message: fmt.Sprintf("Kubelet never posted node status."), LastHeartbeatTime: node.CreationTimestamp, LastTransitionTime: nc.now(), }) } else { glog.V(4).Infof("node %v hasn't been updated for %+v. Last ready condition is: %+v", , nc.now().Time.Sub(savedNodeStatus.probeTimestamp.Time), observedReadyCondition) if observedReadyCondition.Status != v1.ConditionUnknown { currentReadyCondition.Status = v1.ConditionUnknown currentReadyCondition.Reason = "NodeStatusUnknown" currentReadyCondition.Message = "Kubelet stopped posting node status." // LastProbeTime is the last time we heard from kubelet. stHeartbeatTime = stHeartbeatTime stTransitionTime = nc.now() }