zshuangyan / blog

我的个人博客
2 stars 0 forks source link

Mongo HA (基于kubernetes) #1

Open zshuangyan opened 6 years ago

zshuangyan commented 6 years ago

Mongo复制集是由多个网络互通的运行mongod的节点组成的一个集群,在这个集群中,有一个主节点,负责处理业务,其他的节点通过定时从主节点中获取oplog来更新自身的数据形成备份;在检测到主节点发生故障后,辅节点可以继续响应客户端的读请求,并且执行选举操作,选举出一个新的主节点来处理业务,在新的主节点选举出来后,就可以正常处理读写业务了。

搭建思路:

  1. mongo复制集的每个节点由k8s的一个服务(service)和服务所关联的容器组(pod)构成,如果mongo复制集有三个节点,那么将需要有三个k8s服务(service)。使用服务而不是直接使用容器组的原因是:mongo复制集的每个节点间要保持通信,当容器组被重新调度后,它的地址很可能发生改变;而k8s的服务(service)通过k8s域名服务可以提供恒定不变的地址。

  2. k8s的服务通过label绑定关联到容器组,容器组执行实际的业务操作,容器组运行在k8s集群的物理机或虚拟机上。这里三个k8s服务分别关联到三个不同的容器组上,我们需要保证这三个容器组运行在k8s的不同节点上,否则如果k8s集群中的这个节点发生宕机或者重启,整个mongo服务都会down掉。而k8s在设计上会把不同的服务都尽量分布到同个节点上,我们可以创建一个额外的空服务绑定到这三个pod上,以使k8s把它们分布到k8s集群的不同节点上。

  3. 另外增加一个k8s服务(service)和容器组(pod)负责完成mongo复制集的初始化操作。需要专门制作一个初始化的镜像,这个镜像的运行脚本需要获取到mongo成员的nodePort和mongo复制集的名字,生成mongo复制集的配置config并执行初始化命令。在复制初始化的节点的container配置中,传入另外几个节点的nodePort,域名以及mongo复制集的名字作为container的环境变量。

mongo集群在k8s上的可靠性

场景一:从节点所在pod挂掉,k8s的ReplicationController负责重新调度一个pod来替代这个pod,默认情况下原来pod的所有数据都会丢失。虽然pod可能运行到其他宿主机上了,但是与它关联的svc的地址没有发生改变,主节点仍旧能通过这个地址和这个节点发送心跳,从节点收到主节点的心跳请求后,会根据心跳请求携带的复制集配置信息更新自身的复制集配置,进入STARTUP2状态,执行initial sync,选取sync source,将源节点中的所有db导入到本地后,再持续同步源的oplog。客户端对整个复制集的读写操作都不会受到影响。数据库原本的数据不会丢失。

场景二:主节点所在pod挂掉,k8s的ReplicationController负责重新调度一个pod来替代这个pod,和从节点down掉的情况一样,默认情况下新pod的数据也为空。在主节点down掉后,另外两个从节点通过心跳会检测到主节点不可达,在达到ElectionTimeout的时间后,从节点就会发起新的选举成为主节点。从节点升为主节点后,会向新起来的pod发送心跳,然后这个新pod会执行数据同步的操作,成为从节点。默认情况下,ElectionTimeout为10s,所以在10s内主备切换完成,客户端可以正常的支持写操作了,在主备切换的过程中,客户端只能连接到从节点并使能db.getMongo().setSlaveOk(),才能进行读操作。需要注意的是,主节点挂掉时可能会有少量的oplog日志没有来得及同步到从节点上,如果要保证没有写丢失,用户需要配置cfg.settings.getLastErrorDefaults = { w: "majority"},这样主节点在检测对应的oplog更新到从节点上后,才会向客户端返回写入成功的确认。

场景三:两个从节点所在pod先后挂掉,k8s的ReplicationController负责重新调度两个新的pod来替代,如果先挂掉的从节点的pod在另一个从节点挂掉前就调度并成功运行,那么和场景一类似;否则,主节点会检测到复制集中两个从节点不可达,超过总数的一半节点不可达的情况下,主节点会自动降级为从节点,直到任意一个从节点startup,被降级为从节点的那个节点连接到新up的节点后,发现超过ElectionTimeout时间集群中都没有主节点,发起选举,由于这个节点拥有最新的committed OpTime,因此它又被选举为主节点,新起来的从节点会从主节点上同步数据。这个过程中客户端无法执行写操作,可以选择连接到降级为从节点的那个节点,使能db.getMongo().setSlaveOk(),执行读操作,直到新的主节点被选举出来,才可以执行写操作。这个过程中不会有数据丢失。

场景四:主从节点所在pod先后挂掉,k8s的ReplicationController负责重新调度两个新的pod来替代,整个集群只有一个从节点,直到一个新的pod调度成功,由于没有down掉的从节点上拥有最新的committed OpTime,它会被选举为主节点,新起来的从节点会从主节点上同步数据。没有主节点的过程中,无法提供写操作,客户端可以选择连接到没有down掉的那个节点,使能db.getMongo().setSlaveOk(),执行读操作。和场景二类似,如果主节点down掉时,没来得及同步部分oplog数据到这个节点上,也可能会发生数据丢失的情况,如果要保证数据完全不丢失,那么需要设置cfg.settings.getLastErrorDefaults = { w: 3},这样主节点在检查对应的oplog更新到两个从节点上,才会向客户端返回写入成功的确认。

场景五:从主节点所在pod先后挂掉,k8s的ReplicationController负责重新调度两个新的pod来替代,在主节点未挂掉之前,对复制集的读写都可以正常进行,主节点挂掉后,集群中只剩一个从节点,客户端只能连接到这个从节点上进行读操作,直到一个新的节点起来,这个没有down掉过的节点由于拥有最新的committed OpTime,被选举为主节点,才可以对复制集进行写操作。和场景二相同的原因,也可能会导致部分数据丢失。

场景六:三个节点都先后挂掉。k8s的ReplicationController负责重新调度三个新的pod来替代,如果在第三个节点挂掉前,新调度的pod就成功运行,那么这个新调度的pod会同步第三个节点的数据,如果能同步完所有数据,那么大多数数据也不会丢失。如果在第三个节点挂掉前,没有新的pod成功调度,那么会导致所有的数据丢失,所有节点都重新调度成功后,将得到一个空的数据库,支持读写操作。

数据持久化:

当三个节点都同时挂掉的情况下,我们就要面临所有的数据都丢失的后果了,这是无法容忍的。和同事沟通后,得到下面两种方案: 方案一:把容器组(pod)的数据库目录挂载到ceph上 方案二:把mongo集群的部分节点放到公司内部的某个服务器上

第一种方案的缺点在于公司目前部署的kubernetes系统对ceph使用还不成熟,使用的过程中偶尔会出现旧的pod故障被删除后,它所关联的ceph image的锁未被释放,导致新调度的pod无法关联上这个ceph image,因此无法正常运行。另一方面,目前公司部署的分布式存储系统ceph的读写性能和稳定性还没有可靠的数据统计,因此老大建议只把部分从节点的目录挂载到ceph上。

第二种方案的缺点就在于还要部署,运维和监控在服务器上的mongo实例。

需要特别展示是,一个同事提出的建议(非常感谢他),原文如下:

===================================================================

专门用一个节点用作备份,数据存放在 ceph 或干脆在 k8s 外的虚拟机上起一个节点,有如下问题:

备份节点挂了->主节点接受用户写操作->k8s 上所有节点挂了(包括主节点),备份节点挂了之后的数据会丢失

如果以不丢失用户数据为前提,有下面两种方案:

主节点数据挂载 ceph,从节点数据挂载本地 docker 文件系统,且优先级设为 0(不能选为主节点);当主节点挂了的时候,repliset 无法选取主节点,只能提供读功能;该 repliset 的稳定性完全取决于 pod 的重启概率,算是优化版的主从备份; 设 repliset 节点数 为 n, n / 2 + 1 个节点的数据挂载 ceph (节点集设为 A),其它节点挂载本地 docker 文件系统(节点集群设为 B),如果 A 中的节点全挂了,repliset 无法选取主节点,只能提供读功能;只要 A 中的节点有一个存活,用户写入的数据就不会丢失;如果考虑性能,可以禁止 A 中的节点竞选主节点