前言:
如何使用golang构建一个分布式的任务系统 ? 该任务系统搞了几个月,有一些经验心得分享给大家。
在公司内部已做过一次分享,评价还不错。已经把ppt推到github了,有兴趣可看下完整的ppt。求在github上点个star。😁
https://github.com/rfyiamcool/share_ppt/blob/master/shark.pdf
shark
shark?对的,shark是我们项目名,为什么叫鲨鱼? 同事们的项目好多都是海产品,比如starfish ? 那么我的任务系统理所当然也是一个海产品了。
shark可以做很多东西,可以把他理解为一个简单版的ansible和saltstack,架构设计上更趋于salstack。起初单纯要做kubernetes集群部署工具,但随着后面的需求越来越多,所以抽象为面向于服务和主机的任务系统。相比ansible和satlstack来说,更好的分布式设计,对多机房友好、grpc接口更友好、扩展功能更适配我们自己。
架构设计
从下往上说,minion是客户端的意思,每个主机都要部署该服务,master为控制端,管理下发所有的请求。gateway是代理层,由于有多机房,所以需要gateway来转发请求。
协议是使用了grpc,这么没的说,grpc简单高效且适合stream流传输。
gatway的逻辑前面有阐述,主要是做机房的请求转发。对于客户端来说,只需连接gateway就可以了。
主备的选举方案很简单,直接采用redis的分布式锁,谁拿到锁谁就是主,锁操作使用redis lua封装避免误操作。由于redis本身无强一致的同步协议,而是用异步传输日志,所以当redis采用主从模式,redis主挂时,锁可能不太可靠。
这里没有采用etcd、zk这类强一致性db做分布式锁,也没有扩展redis的redlock方案。我们评估了业务需求,对于短时间内引起的锁冲突是可以容忍的。
master的主备之间会做同步的,采用的方案是参考了redis的fullsync和psync。
实现了服务的自升级,尽量避免运维去操作升级。
首先minion拿着本地的version询问master是否有更新。如有更新在通过grpc通道下载本地,然后对比md5及运行版本,通过校验后则创建一个使用新文件运行的子进程,并传递listen fd。主进程会监听winch信号,当子进程运行30s后,无问题则向主进程发送winch信号,主进程收到winch后执行优雅退出。
如果子进程在规定的时间内没有给主进程发送winch信号,主进程会强制把子进程干掉,并且做好状态位的回滚。我们在升级时参照了nginx在各阶段改变进程名的思路,这样有利于排查问题。子进程启动时调用syscall procname改名为 shark-minion (new),主进程则会变更为 shark-minion (old)。
功能设计
shark里完成了对k8s的管理,基于rancher rke二次开发的,原rke是一个基于ssh的工具,我们将其改造成基于minion的k8s管理模块。
任务异步化,通过各个state来标记任务当前的状态。异步化带来的好处在于任务进度可控,并发可控,协程数也可控,可取消任务。任务系统里的任务绝大数较耗时,如果使用同步请求来实现任务系统,不太好实现进度的上报,而异步就简单的多。
这里的watch跟etcd watch道理差不多,由于任务都是异步出路,如不实现watch监听则需要不断轮询结果。原理的实现很简单,在每条数据上加入revision版本,当客户端发起订阅时需要携带revison,每当minion产生数据都会用grpc stream推送给客户端。
使用golang plugin动态库来扩展自定义逻辑,目的在于方便的在线热升级,还有一个在于减少代码污染,因为shark作为任务系统总线,会有其他人对他需求的定制开发,但代码残次不齐,索性直接搞到plugin动态库里,依赖shark的load机制来加载其他人的逻辑。😅
在shark里也可自定义功能模块,为了避免模块出问题影响到minion,使用子进程方式去加载运行,子进程绑定unix domain socket,主进程和子进程之间通过grpc stream over unix domain socket通信。
可注册自定义方法,且每个方法需要接收eventHandler接口,该方法可实现数据的收集,进度发送及其他资源收敛。
高性能定时器的实现,这里没有再使用时间轮,而是采用类似rocketmq定时器的实现。根据不同时间间隔的定时器放到不同的队列里。由于定时器的需求在于任务超时控制及数据清理,所以定时器的精度可粗化。
minion需要本地存储,这里采用go badger来实现类似redis的数据结构。
安全控制,所有的链路都需要证书来保证安全,minion不能直接对外服务,只能master才能来访问,minion使用master的公钥加密token上报给master,master使用私钥解密token才可访问minion。
兜底主要是依赖systemd
防护处理,backoff退避算法,避免频繁的重试某操作。failfast 快速失败,避免无意义请求,failvoer 失效转义,当master主挂时会重置连接到master备。
调优
logrus日志库本就性能不咋地,但考虑更好排查问题,所以在日志里加入了行号、函数名、文件名的解析。这类操作加大了开销,所以改用zap来消减日志开销。
cpu off的开销也显示主要在打日志上。
遇到的问题
多个协程并发读写string时会出现数据混乱的情况,string不是线程安全的,因为string的底层数据结构为stringHeader,一个数据指针,一个length。解决方法可用atomic,也可采用创建新结构复制。
golang os exec里没有提供stop方法,如要实现关闭已启动的shell,可获取pid,然后进行kill。但问题来了,你获取的pid是bash的,kill的也是bash的,bash启动的命令不会被kill。如何处理该问题? 干掉进程组。
go-shell对shell命令做了封装,不仅解决了上面的stop问题,加入更易用的api。
https://github.com/rfyiamcool/go-shell
grpcx是对grpc的封装,实现了拦截器、balancer、grpc error的处理。
https://github.com/rfyiamcool/grpcx
wmanager对golang协程的封装,实现了start、stop、join、pname、status的接口。