高可用实践——Keepalived踩坑记录【转】
项目中需要对调度系统做高可用,以确保系统的稳定性;经过分析,决定采用Keepalive这一经典的高可用解决方案实现,踩坑记录如下。
1. 技术调研
1.1 高可用解决方案
本人之前在一些小模块写过基于 ZooKeeper分布式锁 实现的高可用,但调度系统的模块较多,并没有什么 较好的、第三方的 手段去往zk注册信息,除非在写框架底层的时候就往zk注册。因此,我们只能对进程本身做一些监控,实现“整个系统”的高可用,而不是某个模块的高可用。
之前对市面上的三个调度框架——Azkaban、Airflow、DolphinScheduler,做了较为深入的调研,之后有时间再发出来。他们对于高可用的实现,原生只有ds是有这个机制的,以下做一些简单介绍:
- Azkaban直接就没有管高可用,本人也没有使用过其他开源的项目去实现高可用,但按照我的使用经验,他的WebServer还是很稳健的。
- Airflow原生也不支持高可用,之前关注他们的邮件列表,有人提到了这个issue,但这块好像并不是社区关心的重点,也就没有再提。github有一个 开源的小项目 ,通过监控进程并依据结果,往mysql或者zk里面发送心跳包,来决定是否切换主备。这个项目的年龄有点大,在1.10.x版本之前比较好用,现在airflow的scheduler进程自带一个supervisor,有可能会对这个主备切换过程造成一些困扰,例如造成 脑裂 等。
- DolphinScheduler原生支持高可用,在 配置 时就要指定好主备,但他这个强依赖于zk,看起来有点重,这里我截取ds的心跳线程 HeartBeatTask的run方法 。我不能像ds一样去往zk发心跳包,只能采取一些外置的方法。
StringBuilder builder = new StringBuilder(100);
builder.append(OSUtils.cpuUsage()).append(COMMA);
builder.append(OSUtils.memoryUsage()).append(COMMA);
builder.append(OSUtils.loadAverage()).append(COMMA);
builder.append(OSUtils.availablePhysicalMemorySize()).append(Constants.COMMA);
builder.append(maxCpuloadAvg).append(Constants.COMMA);
builder.append(reservedMemory).append(Constants.COMMA);
builder.append(startTime).append(Constants.COMMA);
builder.append(DateUtils.dateToString(new Date())).append(Constants.COMMA);
builder.append(status).append(COMMA);
//save process id
builder.append(OSUtils.getProcessID());
zookeeperRegistryCenter.getZookeeperCachedOperator().update(heartBeatPath, builder.toString())
1.2 Keepalived原理
这部分原理我也是一知半解,由于我不做os层面的开发,所以也没有太足的动力刨根问底。Keepalived大概是依据一个VRRP协议,利用VRRP维持主备节点的心跳[1],部署方式比较简单,一般用于Web应用的高可用,最常见和Nginx一起使用。
还有一个高可用解决方案Heartbeat,这两者有一些差别[2]。根据运维同学的经验,用keepalived基本上能解决大部分问题(它们也会用keepalived做mysql的高可用),遂采用该方式。
2. 配置Keepalived
先对调度系统的各进程做一个简介:两台机器记为A和B,A上起一个master一个slave,B上起一个slave,并且B作为A上master的备机,两个slave都只会和活着的那个master通信;master和slave都只有一个进程,不像airflow那样,webserver和scheduler是分开的两个进程。以下是踩过的巨坑。。
2.1 主备是否都活?
网上有很多参考资料,附上之前主要参考的两篇文章[3][4]。但单纯Web服务器的高可用,和本人这次做的调度系统的高可用有些不一样。以两台为例,会把两台机器都起起来,再通过抢vip实现只有一台对外提供服务。
但我的调度机,WebServer和Scheduler是在一个进程里面,如果我分别在主备起两个服务,那它们都会触发调度任务,但此时备机没有分配slave,备机的调度任务都会被阻塞,一旦主备切换,slave连上备机,备机会先执行之前阻塞的任务,这显然是不合理的。
因此,我在部署时应该只让主节点的master活着,备节点不应该有这个进程存在,否则会造成脑裂。所以我在参考博客的时候,之前没有考虑到这个问题,按照他们的配置,我始终都在和脑裂做斗争。如果有朋友看到这篇文章,“主备是否都活”是非常需要考虑的问题。
2.2 主备如何通信?
由于网上的博客几乎都只有一个安装和配置过程,尤其是涉及到“vrrp_script”,只放一个脚本,并没有人去解释这个脚本的使用场景,以及主机和备机的check脚本是否一样,完全没有人解释这些问题,我踩了太多的坑。
刚上手的时候我一直理解为,主备通过心跳包沟通,只有获取vip的主节点才会做这个“track_script”的动作,这个认知也是和我之前使用分布式锁的通信方式是一致的,但实际使用时主备永远连不上,备节点总是先进入BACKUP又马上进入MASTER,导致我不停地发生脑裂,非常痛苦。
通过查看日志和对进程的分析,我之前的认知是错误的(也可能是我对其原理的不熟悉导致,非科班同学需要系统学习一下网络,TODO),主备都会周期地做“track_script”动作,keepalived只负责这个vip的归属。
2.3 vip从哪里来?
网上资料还有一个问题没有交代清楚,设置的这个vip是哪里来的?在虚拟机上,似乎是可以随便指定一个没用过的ip作为vip(没有验证过,TODO)。但在现在流行的云服务的生产环境,这个vip是要申请的!这个问题可以通过telnet判断。
telnet $vip $host
2.4 配置文件踩坑
安装很简单,如果没有外网只能手动编译安装。
yum install keepalived -y
配置主要参考云服务文档 CentOS 6.X下配置Keepalived VIP 。这里面还有几个问题:
- 主备的“router_id”是否一致?博客上是随便配,文档上是必须一致,我尝试了随便配没有成功,现在先保持一致(没有进一步验证,TODO);
- “script_user“是keepalived执行脚本的用户,一般情况这个用户需要sudo权限;
- 按照文档,必须加“unicast_peer”,我还加上了“unicast_src_ip”,这个和云服务厂商有关,如果允许组播那就组播,不允许就只能单播[5];
- 关于“state”,也有两种配置方式[6],抢占式和非抢占式,我配置的是抢占式的,通过实践,在配置“state”分别为MASTER和BACKUP的情况下,“nopreempt”参数没有用;
- 关于“vrrp_script”,这个是keepalived周期执行的脚本,参考[7][8],由于我是“抢占式”的配置,“weight”随便给个2就行。
2.5 具体配置
主节点keepalived配置:
global_defs {
router_id LVS_DEVEL
script_user $script_user
}
vrrp_script check_xxx {
script "$script_path"
interval 60
weight 2
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 150
advert_int 1
unicast_src_ip $node1
unicast_peer {
$node2
}
authentication {
auth_type PASS
auth_pass 1111
}
track_script {
check_xxx
}
virtual_ipaddress {
$vip dev eth0
}
}
备节点keepalived配置:
global_defs {
router_id LVS_DEVEL
script_user $script_user
}
vrrp_script check_xxx {
script "$script_path"
interval 60
weight 2
}
vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 51
priority 100
advert_int 1
unicast_src_ip $node2
unicast_peer {
$node1
}
authentication {
auth_type PASS
auth_pass 1111
}
track_script {
check_xxx
}
virtual_ipaddress {
$vip dev eth0
}
}
check脚本,涉及到公司隐私,不放出来了,简单描述下:
#!/bin/bash
# 检查tomcat进程
A=$(ps aux | grep -v grep | grep org.apache.catalina.startup.Bootstrap | wc -l)
# 检查vip的状态
ip a | grep $vip
# 如果发现vip且服务宕机, 则尝试拉起 -> 如果主备都活的情况不需要检查vip
if [ $? -eq 0 ] && [ $A -lt 1 ]; then
# 杀掉另一个服务, 即使不一定存在, 防止脑裂
...
# 启动服务并等待3秒
...
# 再检查tomcat进程
B=$(ps aux | grep -v grep | grep org.apache.catalina.startup.Bootstrap | wc -l)
# 如果没拉起, 则把vip漂到备机
if [ $B -lt 1 ]; then
sudo service keepalived stop
fi
fi
2.6 其他
至于测试我就不测了,引的资料里面有很多,大概的步骤就是:先把主节点keepalived起起来,然后起从节点keepalived,然后关掉主节点keepalived,如果配置正确那vip应该会漂到从节点,服务也会转移到从节点。
需要注意的是,一般情况下认为调度系统还是很稳定的,不会发生宕机;次一级的情况,把keepalived当成supervisor用,拉起主服务;再次一级的情况,切换到备机并通知;如果备机都故障了,此时应该人为介入了,而不是再切回主服务;调度系统扛着一大堆SLA指标,如果出问题了是要背事故的,把vip漂来漂去没意义,只要做个临时方案保证服务基本可用就行。
3. 后续的问题
尽管做了主备,但调度系统仍然会在主备切换的时候有短暂的不可用状态,造成那个时间段正在执行的任务直接失败。首先考虑下开源的框架:
- 如果我们对azkaban的webserver做高可用,也会存在这个问题;
- 对airflow的scheduler做高可用,由于airflow会周期检查mysql数据库,失败的任务也会拉起,但这个周期很短大约在秒级,有时候反而会造成一些性能问题;
- ds不清楚,我用的不多(抽空补上,TODO)。。
因此在这个系统里,还需要开发一个周期拉起失败任务的进程,这个周期达到小时级即可。
参考
- ^Keepalived原理 https://blog.csdn.net/qq_24336773/article/details/82143367
- ^高可用开源方案 Keepalived VS Heartbeat对比 https://blog.csdn.net/yunhua_lee/article/details/9788433
- ^Keepalived之——Keepalived + Nginx 实现高可用 Web 负载均衡 https://blog.csdn.net/l1028386804/article/details/72801492?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-4.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-4.nonecase
- ^keepalived实现Tomcat服务双机热备 https://blog.csdn.net/u013256816/article/details/49127995
- ^【keepalived】关于keepalived配置中的 mcast_src_ip 和 unicast_src_ip https://www.jianshu.com/p/7c709c3be4a9
- ^Keepalived https://www.jianshu.com/p/a6b5ab36292a
- ^keepalived之vrrp_script详解 https://www.cnblogs.com/arjenlee/p/9258188.html
- ^keepalived 关于vrrp_script中的weight这个值的使用 https://blog.csdn.net/qq_41814635/article/details/84166700
转自
高可用实践——Keepalived踩坑记录 - 知乎
https://zhuanlan.zhihu.com/p/148136167