容器化RDS—计算存储分离架构下的“Split-Brain”
2018-03-04 16:59
846 查看
容器化RDS—计算存储分离架构下的“Split-Brain” 2018-02-26 09:28:26分类: MySQL沃趣科技 熊中哲
不管是架构选型还是生活,绝大多数时候都是在做 trade off,收获了计算存储分离带来的好处,也意味着要忍受它带来的一些棘手问题。本文尝试结合 Kubernetes, Docker, MySQL和计算存储分离架构, 分享我们遇到的“Split-Brain”问题。
[align=center][/align]2018年1月19号参加了阿里巴巴双十一数据库技术峰会,见到了好多老同事(各位研究员,资深专家),同时也了解到业界最新的数据库技术发展趋势:● 数据库容器化作为下一代数据库基础架构● 基于编排架构管理容器化数据库● 采用计算存储分离架构这和我们在私有 RDS 上的技术选型不谋而合. 尤其是计算存储分离架构.
在我们看来,其最大优势在于:● 计算资源 / 存储资源独立扩展,架构更清晰,部署更容易。● 将有状态的数据下沉到存储层,Scheduler 调度时,无需感知计算节点的存储介质,只需调度到满足计算资源要求的Node,数据库实例启动时,只需在分布式文件系统挂载mapping volume 即可,可以显著的提高数据库实例的部署密度和计算资源利用率。
看上去合理的机制会给我们带来两个问题,
问题一 : 无法判定节点真实状态
心跳更新是判断节点是否可用的依据,但是,心跳更新丢失是无法判定节点真实状态的(Kubernetes 中将节点标记为 ConditionUnknown 也说明了这点)。
Node 可能仅仅是网络问题,CPU 繁忙,”假死”,Kubelet bug 等原因导致心跳更新丢失,但节点上的数据库实例还在运行中。
问题二 : 缺乏有效的 fence 机制在这个情况下,借助 Kubernetes 的原生组件Node Controller,Scheduler和原生API Statefulset实现的 Failover,将数据库实例从Unknown 节点驱逐到可用节点,但对原Unknown 节点不做任何操作。
这种”软驱逐”,将会导致新旧两个数据库实例同时访问同一份数据文件。
发生”Split-Brain”,导致 Data Corruption,数据丢失,损失无法弥补。所以,必须借助 WOQU RDS Operator 提供的 fence 机制,才能保障数据文件的安全。[align=center][/align] 下面是枯燥的故障复现,通过日志和代码分析驱逐的工作机制,总结”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实例都报错, 并无法启动
测试参数:● kube-controller-manager 启动参数
● kubelet 启动参数
基于日志, 整个事件流如下:● 时间点 December 1st 2017,10:18:05.000 (最后一次更新成功应该是 10:17:42.000):节点(k8s-node3)启动数据库压力测试, 以模拟该节点”假死”, kubelet 跟 API Server 出现心跳丢失。
kubelet 日志报错,无法通过 API Server 更新 k8s-node3 状态。
● 时间点 December 1st 2017, 10:19:42.000: statefulset controller 发现 default/gxr1-oracle-statefulset 状态异常
● 时间点 December 1st 2017, 10:19:42.000:scheduler 将 pod 调度到 k8s-node1
这样旧的MySQL 实例在 k8s-node3 上,kubernetes 又将新的实例调度到 k8s-node1。两个数据库实例写同一份数据文件,导致 data corruption,两个节点都无法启动。老实例启动报错, 日志如下:
原文:http://blog.itpub.net/28218939/viewspace-2151253/
不管是架构选型还是生活,绝大多数时候都是在做 trade off,收获了计算存储分离带来的好处,也意味着要忍受它带来的一些棘手问题。本文尝试结合 Kubernetes, Docker, MySQL和计算存储分离架构, 分享我们遇到的“Split-Brain”问题。
[align=center][/align]2018年1月19号参加了阿里巴巴双十一数据库技术峰会,见到了好多老同事(各位研究员,资深专家),同时也了解到业界最新的数据库技术发展趋势:● 数据库容器化作为下一代数据库基础架构● 基于编排架构管理容器化数据库● 采用计算存储分离架构这和我们在私有 RDS 上的技术选型不谋而合. 尤其是计算存储分离架构.
在我们看来,其最大优势在于:● 计算资源 / 存储资源独立扩展,架构更清晰,部署更容易。● 将有状态的数据下沉到存储层,Scheduler 调度时,无需感知计算节点的存储介质,只需调度到满足计算资源要求的Node,数据库实例启动时,只需在分布式文件系统挂载mapping volume 即可,可以显著的提高数据库实例的部署密度和计算资源利用率。
离线(ODPS)以机械磁盘为主● 无法计算存储分离为实现离线(ODPS)/在线集群的混合部署提供了可能。当集群中某个 Node 不可用后, 借助 Kubernetes 的原生组件Node Controller, Scheduler和原生API Statefulset即可将数据库实例调度到其他可用节点, 以实现数据库实例的高可用.● 基于 Raft 算法实现。● 算法是一种Raft 如果对于 Raft 算法的实现有兴趣,可以看看 https://github.com/goraft/raft |
问题一 : 无法判定节点真实状态
心跳更新是判断节点是否可用的依据,但是,心跳更新丢失是无法判定节点真实状态的(Kubernetes 中将节点标记为 ConditionUnknown 也说明了这点)。
Node 可能仅仅是网络问题,CPU 繁忙,”假死”,Kubelet bug 等原因导致心跳更新丢失,但节点上的数据库实例还在运行中。
问题二 : 缺乏有效的 fence 机制在这个情况下,借助 Kubernetes 的原生组件Node Controller,Scheduler和原生API Statefulset实现的 Failover,将数据库实例从Unknown 节点驱逐到可用节点,但对原Unknown 节点不做任何操作。
这种”软驱逐”,将会导致新旧两个数据库实例同时访问同一份数据文件。
发生”Split-Brain”,导致 Data Corruption,数据丢失,损失无法弥补。所以,必须借助 WOQU RDS Operator 提供的 fence 机制,才能保障数据文件的安全。[align=center][/align] 下面是枯燥的故障复现,通过日志和代码分析驱逐的工作机制,总结”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实例都报错, 并无法启动
测试参数:● kube-controller-manager 启动参数
● kubelet 启动参数
基于日志, 整个事件流如下:● 时间点 December 1st 2017,10:18:05.000 (最后一次更新成功应该是 10:17:42.000):节点(k8s-node3)启动数据库压力测试, 以模拟该节点”假死”, kubelet 跟 API Server 出现心跳丢失。
kubelet 日志报错,无法通过 API Server 更新 k8s-node3 状态。
细节如下:● // Start syncing node status immediately, this may set up things the runtime needs to run.}● 默认时间为 10 秒, 测试时设置的是8sobj.NodeStatusUpdateFrequency = metav1.Duration{Duration: 10 * time.Second}更新如下信息:func (kl *Kubelet) defaultNodeStatusFuncs() []func(*v1.Node) error { withoutError := func(f func(*v1.Node)) func(*v1.Node) error { f(n) } return []func(*v1.Node) error{ withoutError(kl.setNodeStatusInfo), withoutError(kl.setNodeMemoryPressureCondition), withoutError(kl.setNodeReadyCondition), withoutError(kl.recordNodeSchedulableEvent),}通过 kubectl 可以获得节点的信息 ● go wait.Until(func() { glog.Errorf("Error monitoring node status: %v", err)}, nc.nodeMonitorPeriod, wait.NeverStop)● 默认 5秒,测试时4sNodeMonitorPeriod: metav1.Duration{Duration: 5 * time.Second},当超过 NodeMonitorGracePeriod 时间后,节点状态没有更新将节点状态设置成 unknownif nc.now().After(savedNodeStatus.probeTimestamp.Add(gracePeriod)) { // (regardless of its current value) in the master. glog.V(2).Infof("node %v is never updated by kubelet", node.Name) Type: v1.NodeReady, Reason: "NodeStatusNeverUpdated", LastHeartbeatTime: node.CreationTimestamp, }) glog.V(4).Infof("node %v hasn't been updated for %+v. Last ready condition is: %+v", if observedReadyCondition.Status != v1.ConditionUnknown { currentReadyCondition.Reason = "NodeStatusUnknown" // LastProbeTime is the last time we heard from kubelet. currentReadyCondition.LastTransitionTime = nc.now() }● if observedReadyCondition.Status == v1.ConditionUnknown { // We want to update the taint straight away if Node is already tainted with the UnreachableTaint taintToAdd := *UnreachableTaintTemplate glog.Errorf("Failed to instantly swap UnreachableTaint to NotReadyTaint. Will try again in the next cycle.") } else if nc.markNodeForTainting(node) { node.Name, ) } else { if nc.evictPods(node) { node.Name, nc.nodeStatusMap[node.Name].readyTransitionTimestamp, ) }}● // evictPods queues an eviction for the provided node name, and returns false if the node is alreadyfunc (nc *Controller) evictPods(node *v1.Node) bool { defer nc.evictorLock.Unlock()}● if nc.useTaintBasedEvictions { // taints and we normally don't rate limit evictions caused by taints, we need to rate limit adding taints.} else { // When we delete pods off a node, if the node was not empty at the time we then go wait.Until(nc.doEvictionPass, scheduler.NodeEvictionPeriod, wait.NeverStop)通过删除 pods 的方式驱逐 nc.evictorLock.Lock() for k := range nc.zonePodEvictor { nc.zonePodEvictor[k].Try(func(value scheduler.TimedValue) (bool, time.Duration) { if apierrors.IsNotFound(err) { } else if err != nil { } else { evictionsNumber.WithLabelValues(zone).Inc() nodeUID, _ := value.UID.(string) if err != nil { return false, 0 if remaining { } })} |
● 时间点 December 1st 2017, 10:19:42.000:scheduler 将 pod 调度到 k8s-node1
这样旧的MySQL 实例在 k8s-node3 上,kubernetes 又将新的实例调度到 k8s-node1。两个数据库实例写同一份数据文件,导致 data corruption,两个节点都无法启动。老实例启动报错, 日志如下:
2017-12-01 10:19:47 5628 [Note] InnoDB: PUNCH HOLE support available2017-12-01 10:19:47 5628 [Note] InnoDB: Uses event mutexes2017-12-01 10:19:47 5628 [Note] InnoDB: Compressed tables use zlib 1.2.32017-12-01 10:19:47 5628 [Note] InnoDB: Number of pools: 12017-12-01 10:19:47 5628 [Note] InnoDB: Initializing buffer pool, total size = 3.25G, instances = 2, chunk size = 128M2017-12-01 10:19:47 5628 [Note] InnoDB: If the mysqld execution user is authorized, page cleaner thread priority can be changed. See the man page of setpriority().2017-12-01 10:19:47 5628 [Note] InnoDB: Log scan progressed past the checkpoint lsn 4068223232017-12-01 10:19:47 5628 [Note] InnoDB: Database was not shutdown normally!2017-12-01 10:19:47 5669 [Note] InnoDB: Starting an apply batch of log records to the database...2017-12-01 10:19:47 5669 [Note] InnoDB: Apply batch completed2017-12-01 10:19:47 5669 [Note] InnoDB: Removed temporary tablespace data file: "ibtmp1"2017-12-01 10:19:47 5669 [Note] InnoDB: Setting file './ibtmp1' size to 12 MB. Physically writing the file full; Please wait ...2017-12-01 10:19:47 5669 [Note] InnoDB: 96 redo rollback segment(s) found. 96 redo rollback segment(s) are active.2017-12-01 10:19:47 5669 [Note] InnoDB: Waiting for purge to startInnoDB: Failing assertion: purge_sys->iter.trx_no <= purge_sys->rseg->last_trx_noInnoDB: Submit a detailed bug report to http://bugs.mysql.com.InnoDB: immediately after the mysqld startup, there may beInnoDB: http://dev.mysql.com/doc/refman/5.7/en/forcing-innodb-recovery.html10:19:47 5669 - mysqld got signal 6 ; |
相关文章推荐
- 容器化RDS——计算存储分离架构下的“Split-Brain”
- [置顶] 容器化RDS|计算存储分离架构下的“Split-Brain”
- 容器化RDS|计算存储分离架构下的 IO 优化
- 容器化RDS|计算存储分离架构下的 IO 优化
- 容器化RDS|计算存储分离架构下的 IO 优化
- 容器化RDS:计算存储分离还是本地存储?
- 容器化RDS|计算存储分离架构下的IO优化
- 容器化RDS|计算存储分离架构下的IO优化
- [置顶] 容器化RDS:计算存储分离还是本地存储?
- 容器化RDS|计算存储分离架构下的 IO 优化
- 按照计算与存储的关系,计算机架构的四种分类
- 2017双11技术揭秘—阿里数据库计算存储分离与离在线混布
- 存储与计算的分离
- 2017双11技术揭秘—阿里数据库计算存储分离与离在线混布
- 计算密集型分布式内存存储和运算平台架构
- 谢源:计算存储一体化,在存储里做深度学习,架构创新实现下一代AI芯片
- 计算与存储分离实践—swift消息系统
- 计算密集型分布式内存存储和运算平台架构_0
- 利用C#反射实现存储视图和呈现视图分离的软件架构