2015 年 11 月 28 日,架构与运维的年度大趴——又拍云架构与运维大会·北京站圆满落幕了。近 1500 名来自全国各地的 IT 小伙伴们让现场热火朝天,而 23 位享誉互联网技术业界的大神们的登场,当当网架构师张亮在会上作了题为《当当分布式作业elastic-job解密》,以下是分享实录:
目前我在当当所做的工作,主要分为四点,一是主导当当一个应用框架的开发,目前当当是一个电商公司,很多业务比较复杂,也比较重。主要从两个方向去做,二是用 ddfame 去处理一些逻辑,三是技术白皮书的撰写,根据市面的公司整合的技术去整合白皮书。当当之前也做过技术委员会的调研,分成四块,我主要负责消息中间组。四是 elastic-job 的直接提炼。
应用框架的开发
先介绍一下 ddfame , ddfame 的框架核心主要在中间这一块,核心启动一个容器,包括快速地做一些搭建,和市面上的 dubboox 比较像,我们制定一些规则,快速配置文件,或者放一些供应的内库,还有单元测试,整合测试和 mark 测试,我们保证正确性。
其中有一个 log 的模块, log 打到什么地方,以什么样的方式去展现 log 。服务有两块,一块是数据服务,包括搜索、缓存都会在数据服务里面做一些提供。一块是无状态的服务,不一定只有服务,可能有数据,把 mq 的消息直接存储到数据, mq 外围用应用框架去挂接。应用框架外围常用客户端的分布式数据库,分布式作业,还有外围的组建,跟应用框架去做整合。最后做一些平台。
应用框架的核心组建是应用开发的两端,两端可以快速整合框架,成为一个快速启动的框架,把其它技术粘连进来。外边三个小块是开源或者即将开源的应用, dubbox 是基于阿里做的,是原来应用框架里面 job 抽离出来的。
定时任务不一定在消息中间件场景中出现,往一个表写很多数据,通过定时任务,把数据抓取出来,这个时候有消息中间件的话,完全把数据写到消息中件,直接把数据发布出来,实时性会高一些。作业和消息中间件的区别是,作业是时间驱动的产品,消息中间件是事件驱动。
举个例子,在有些场景下,消息中间件是没法用的,比如我们想从外部系统抓取价格,外部系统不能给内部提供事件,我们一个小时抓一次还是半个小时抓一次,这是需要作业的。其次作业属于批量的概念,消息中间件需要逐条去处理,举一个场景,如果电商公司和快递公司做结算,电商公司一个月在快递公司生产了一万单,电商公司给快递公司 5% 的提成,在月底批量处理计算时,如果消息中间件去处理是没法打赏的。作业的实时性没有那么重要,一个 VIP 用户长期不买东西降级,可以在一天的零点去做,这个实时性对资源是巨大浪费,用作业的比较合适。
一般各系统解耦,系统之间的顺序梳理很顺,作业内部来实现,它的内部有几点区别。因为当当是电商系统, cos 用的定时任务的标准,大家都会用的。
Cos 有几个问题,一是虽然支持高可用,可是它的高可用基于数据库,而且高可用并没有数据分片的功能,支持一主一备,主机挂掉的话,备机就上,如果主机没有挂掉,备机是浪费的。二是作业的时候不一定用数据库,这不太合适。三是前一段时间阿里开源一个问题,也有分布式的功能,它的代码稍微有点旧,核心代码去做作业的启动,这个有很多的缺点,较为重要的缺点是如果异常的话,手边的业务受到影响,正常的作业时间去做中间任务,导致后边的作业挂死。后边的作业类型比较单一,把数据分成取数据和执行数据,但是执行业务的时候不一定面向数据。最后是文档问题,有的时候出问题不是程序问题,是文档没有写清楚,用错一些东西。
Crontab 是系统级别的,它没有分布式功能,也缺乏集中管理的机制。做一些小任务用 crontab 很合适,大规模的系统不适合。以前当当去做任务,是遗留的系统,当当的系统逐渐严禁成先进的 pst 的系统,用 perl 不符合公司的规范,会渐渐的下线。
分布式作业的系统
所需要功能的列表分成三块,主要功能、易用性和非功能性需求。主要功能的第一个是分布式,分布式的作用是,运行这个作业,它可以在两台或者多台机器上去执行,这些机器在分布式执行的作业是一个整体的作业;第二个是并行调度,这个作用是 cos 所不具备的,假如有两台机器执行业务,每台去执行作业的二分之一,有三台去执行业务的话,每台执行三分之一,而不是之前一台执行百分之百,挂掉的备机扩容。还有弹性扩容,两台机器挂掉一台,剩下一台承担 100% 的作业,而不是 50% 。
如果作业堆积很多,快速加机器,把作业快速地给分散开,原来有两台、三台机器,加到十台,原来执行 50% 的作业,现在执行十分之一的作业,可以做到弹性的扩容和缩容。需要集中管理的,有分布式的作业,还有调度,这需要一个注册中心做集中管理。最后定制任务流,任务有取数据和看数据的两个状态,消息中间件出来,有时候处理出去,有时候提供一个定时调度的说法和处理文件的东西,需要定制任务流程,包括有一些前置任务和定制任务。
易用性并不是基于 spring 开发的作业,有的同学用 spring 比较多,基于 spring 的空间搭建作业系统,用 spring 定义好的边。运维平台提供集中管理的注册中心,所有的运维去操作这个注册中心就可以。
几点非功能性需求,一个是稳定性,如果某一台挂掉,不敢去作业,或者丢掉一些作业项不敢去做,在分布式的场景可以作业。分片根据之前的定制分到应该分到的机器。第二个是高性能,在分片的场景下,用分布式的机器去作业就是高性能,并行调度是高性能的一部分,两台各执行 50% ,如果十台机器就执行 10% 。
通过多线并行执行作业的分片项,很有可能一台机器执行 N 个。最后一点就是灵活性,在分布式的场景下,我们有一个很重要的概念是 CAP ,实现密等性的功能,给大家开放了足够灵活可配置的选项。要实现性能,要失效转移,很多这样的功能会根据一些配置直接去修改。
中心化和去中心化
实现作业的时候,我们分布式的场景有两种,一种是中心化,一种是去中心化。中心化的作业是这样的,现在有一个 elastic-job 的集群,有一台这个集群里面的主机,也就是调度中心。它通过调度中心分发作业,如果调度中心是一个单独的角色, cos 在调度中心存在,通过调度中心告诉作业服务器分发,别的作业服务器去执行,最后进行通信。
我们可以对比一下中心去和去中心化有什么样的区别,在实现难度上中心化非常高,只能实现一个调度中心,而且调度中心和作业执行的服务器之间要进行通信。而去中心化的话,实现难度比较低,没有一个中心,各个作业节点是自治的,不需要分布式的调度。第二个,部署难度,中心化稍微高一些,有一个调度中心,服务器还有一个注册中心。
去中心化有两种,一个是作业执行服务器,还有一个注册中心。触发时间统一控制,中心化是可以的,去中心化差一些,作业服务器执行的时间本身不一样的怎么办,用一个 NTP 去做同步,有的公司没有问题,作业乱掉时用中心化,用各个服务器去分发调度。去中心化,每个中心都是根据自己的时钟进行作业,这是很难控制的。
触发延迟,中心化分发请求各个作业节点,不管是长还是短一定有的,去中心化是没有延迟的。最后一个异构语言支持,中心化支持比较容易,中心化去控制作业分布式的调度,公开这些接口,其它的服务器去调度。去中心比较困难,每个作业都是客户端,调度需要在客户端,需要做大量的开发。
elastic-job 的中心化的运作。这是一个 elastic-job 的服务器,每个 QUARTZ 去控制它,假设两台机器连接 CK ,进行主节点选取,一台获取主节点。黄色的表示这个主节点,剩下的两台渐渐连上 CK ,进行作业的注册,把作业相关的状态放在 CK 里面。
在注册完成之后,作业启动之前进行分片,可以看到,如果我们分成十片,四台作业服务器是平均分开的。一旦作业到了触发时间,他们各自去执行各自的定时任务,每一台作业服务器根据自己的时钟去执行作业。这个时候如果配置最后一台机器挂掉了,触发重新分片的。可以看到,首先要进行重新分片,这是主节点的作用。主节点重新分片,实际上少了一台机器,三台机器平均去做分片。分片完成之后,主动通知变化,这个时候再去运行。如果说主节点要挂掉了,情况就不一样。因为要重新选取,产生新的节点,会重新分片,十个分片分各 50% ,在每个机器上去做。最后说一下最佳实践,分片项怎么和数据连接起来,分片项连一个数据,而不是拿分成项写很无聊的代码,从第一个到第十个写东西。如果分片项本身的分布规则去配合的话,就很自然根据这个数据中间层去做数据的分片。
elastic-job 的实现细节
第一是分片的实现,分片实现通过三个阶段完成,这是三个演化的阶段。通过一个无中心化的场景去实现这个业务。主节点不需要,都是各自去计算分片,发生网络上很明显。举个例子,第一台机器进行分片的时候,如果分成十片,拿第一片和第二片,新加了一台机器,由于机器总数的变化拿到一片,在分配过程中没法感知到别的机器变化。
这种场景下,新的机器加入或者新的机器下线会分配不准。过滤到第二种分片方式,在作业触发的时候选取主节点分片。这样分片次数很多,有的作业很短, 5 秒执行一次,这样分片的结果也是一样。最后一个场景,在主节点前选取,一旦有某个机器注册,这个时候选取主节点,分片按需触发节点。就是说机器一旦挂了马上进行分片,在运行的时候,作业重新分片了不好停止,分片一定在保持运行中稳定。
这个是分布式协调的实现,饼图是主节点分析,会进行一个分布式缩容,选取主节点。谁拿到了,谁就创建主节点。
第二,服务器的扩容和缩容。如果有服务器上线或者挂掉,标记一个分片项,在一个结点标记需要去分片了,而不是说在这个时候真正去分片。下一次作业触发的时候,由主节点负责分片,分片过程是一个阻塞的状态。
最后一个是失效转移,是一个补充的功能,分片过程中可能会挂掉,在下次作业执行的时候去触发。如果想在这次不落掉这个分片就是失效转移,通过两种方式通知其它存在的机器去执行这个分片。一种是崩溃的时候通知,某一台机器挂掉了,在 QUARTZ 里面有一个临时节点,在临时节点消失的时候感知哪台机器挂掉,去执行这个分片。另一种是拉取机制,通知是推送的机制,推送到各个服务器去执行,在某一台作业服务器完成但是没有执行,通过这两种方式的配合,进行失效转
幂等性的实现,如果分片项在两台不一样的机器执行的话,会造成数据不一致,这是很大的风险。所以 elastic-job 实现了一个幂等性,分成两块。这边是一个单机幂等性,是持续控制的。一个作业在执行过程中,假设间隔 5 分钟执行一次,执行时间会很长, 6 分钟。会不会继续执行,这个由 elastic-job 控制住。第二个,收集运行状态,在分布式的场景下单机幂等性是没有用的,随着分级的波动,同时,第一台机器执行的时候,有可能分布在第二台机器。目前的作业是什么,需要做一个监控。所以在分布式的场景下去监控,通过单机两个分布式的方向去实现幂等性。
定制化的任务流程,目前有 elastic-job 分成作业的流程,第一种 simple 作业,还有弹性、扩容、缩容,提供的接口调度提供分布式相关的功能。后两个作业是基于第一个作业的,它们每个有一个关系的层级,有两个大的类型专门去处理数据这一块,是一个高吞吐量的数据流作业。
抓取数据和处理数据用到流式处理,这个作业一直不停,直到抓取到数据,这是一个框架的灵活性配置。这个处理数据流任何现场的去处理,我想开一个线条去处理抓取的数据这是可以的。第三个作业的存在是有顺序的,这个在保证同一个分片有数据的,这个会有感觉。这个作业的顺序性也是一样的,在同一个分片下保持有序,这样的话,我分了十片,那最多起十个现场去启动,为了保证顺序牺牲一些东西,这是目前三种作业类型。
上图是注册中心的数据结构,从作业注册到注册中心,注册中心怎么管理作业,目前数据结构有 4 种 config ,作业的名称是什么,什么时候触发,一共需要分成多少分片,灵活性,幂等性,失效转移都需要配置。第二个 servers ,读取一些服务器的信息,将信息注册在里边,哪个服务器去执行,每一个服务器分配到分片项目是多少,在 servers 里面去控制。第三个 execution ,这就是幂等性,去详细记录每一个分片在这个时间的状态。最后一个是 leader ,这是一个主节点,这个非中心化,需要一个节点去调度,这是主节点去做得事,主节点放在里面,到底哪个 IP 去做,去清理或者操作。或者有什么时候挂掉了,这个放在主节点里面了。
这是运维界面两张截图,第一个根据作业的名称,第二个根据不同的作业服务器,这是注册中心的管理页面,可以去放置不同的注册中心,不同权限的配置以及作业的维度。第三个是服务器的维度,哪些作业通过哪个服务器查看。
运维平台和真正 elastic-job 的作业两者之间没有任何关联,他们唯一关联就是对注册中心数据的读取。 elastic-job 对注册中心有危险, elastic-job 平台会展现出来。通过修改配置作业去生效,所以注册中心天然分开,有的在开源了之后,注册中心不好看,运维平台不好看,大家可以自己去写,原理非常简单,把它设置成多语言,支持英语等都不难。
当当线上使用情况
这个作业是 9 月份开源的,之前在当当推广,不少系统使用了。目前列出来几个系统,像支付系统、订单系统、发票、促销、快递、仓储、全站配置或多或少都用,电商的系统比较复杂,不是所有的系统、订单直接用,而是渐渐去演化的。
先介绍一下我们的开发理念,我们对开发和代码要求很明确。
第一,用心去写代码。大家做技术的,看到别人写的代码是不是用心,一眼能够看出来。我们应该用代码去把这个故事说清楚。因为语言很完善的代码,可以用代码把这个故事说清楚。
第二,整洁的代码。代码不整洁,很多人看起来会别扭,我们团队有些代码洁癖,希望把代码写的很干净,高度重复用,不管是配置也好,代码也好提炼出来,把模块分的更合理。
第三,扩展性。并不是所有的场景都需要考虑扩展性,如果只有一个需求的话,不用考虑扩展性。只有需求扩展到两个以上,才需要提炼出来。没有必要只是为了追逐代码的扩展性,而让设计变得更复杂,耽误宝贵的时间。
第四,模块划分。这个不太好衡量,到底模块怎么划分才比较合理的,我只想举一个例子,在 elastic-job 模块,大的模块分成四块,一块是核心代码,是不同的项目,另外一块是配合一些命名空间的东西。第三块是 elastic-job 的外部控制台,最后一块是一些公用测试的东西。核心代码放在几块,第一块 API ,第二块代表项,第三块是因特,凡是内部放在因特,不管监控还是幂等性的相关实现会在因特分成一个包,最后一块会提供几种开放的东西,比如作业的分片规则,希望用开源社区的人对它进行扩展。
第五,单元测试。这个我们要求挺高的,没有特殊理由的话,单元测试率要求百分之百,但是几乎不可能的。有各种各样的理由,但是我们还是尽量地去提升单元测试的覆盖率。在分布式的场景,单元测试不一定说明问题。但是如果单元测试没有过的话,没有人自信这个东西没有低级的错误。
第六,文档。代码有一种声音,代码写的好实际上不需要文档的。但是很多人可能只是用这个代码,没有去看这个代码。所以通过文档把东西说清楚还是很有必要的。
目前这个项目的开源放在当当的 elastic-job 下面,大家感兴趣去看一些代码,之前当当开源过 dubbox ,可以让 PHP 去做调用。 sharding-jbdc 是当当的解决方案,没有中间件而且支持分片的,基于 jdbc 驱动的改写,有可能在春节前开源。