实践经验|分布式协调 Kubernetes

2016-06-03

Parkster 这个项目正在从单体应用转化到微服务的过程中,已经使用 Kubernetes 有一段时间了,尚未被移动到 Kubernetes 应用程序的就是单体应用剩下的部分。将单体应用完全分裂成微服务是一个愿景,在这个过程中,我们从 Kubernetes 提供的调度、服务发现、高可用性、日志收集等服务中受益诸多。要达到在这种程度还需要做很多工作。在这篇文章中,我们要探索的是周期性单个开启调度工作的问题。


处理调度 Jobs

单一的应用程序,就是之前在单个节点上运行的单个实例,包括了很多在数据库更新状态的调度 jobs(现在也同样发布商务events)。单体程序创建在java中,并且大量使用 Spring,所以 job 看起来是这个样子的:

Spring 之后会确认提到过的 doSomethingEveryMinute 方法每分钟执行一次。问题是,如果我们目前不在 Kubernetes 上主持单体程序,并且跟多个实例一起运行,这个 job 就每分钟会在每个实例上被执行一次,而不仅仅只是每分钟执行了一次而已。如果 job 有类似发送通知邮件或更新数据库这样的副作用的话,这就是一个问题了。所以我们要怎样避免这个?当然,解决方案还是很多的,显而易见的选择就是利用 Kubernetes Jobs,让 Kubernetes 自己周期性调度 jobs。问题就是这个作用只在 Kubernetes1.3 版本及以上版本中可用,但是 1.3 还没有发布。但是即使我们能够使用这样一个功能,从技术角度来说,这也是不太可行的。我们的jobs被高度耦合到已经存在的代码库,并且提取每个 job 到它自己的应用程序,程序可能会有错误,而且如果一次性完成的话会非常耗费时间。所以我们最初的计划是提取所有的调度 jobs 到一个应用程序,这个应用程序在 Kubernetes 中只能作为一个实例来运行。但由于现有代码的本质,和高耦合性,即使是这样也很难实现。那么,有没有一种很轻松的办法允许我们目前在单体应用中保持jobs,并且当我们从这个应用中提取功能到独立的服务的时候,逐渐替代他们呢?其实还是有的。


Kubernetes 中的 Leader 选举

要解决这个,我们需要做一些分布式协调,比如,当 jobs 被 Spring 执行的时候,如果这个节点不是“leader节点”,为运行调度jobs负责,我们就只需要传回信息(而且,不要和job一起运行代码)。有一些项目能够帮助我们来处理诸如 zookeeper 和 hazelcast 之类的东西。但是仅仅只是为了决定哪个节点执行调度jobs,以此来设置、保留zookeeper集群就太劳师动众了。我们需要一些易于管理的东西,假如我们能够利用Kubernetes会怎么样呢?Kubernetes已经在cover下(使用 RAFT consensus algorithm)处理了leader选举。结果证明,这个功能通过使用gcr.io/google_containers/leader-elector Docker镜像已经被暴露给了终端用户。之前已经有一个很棒的博客帖子很细节地描述过这个是如何运行的了,点击这个网址查看:http://blog.kubernetes.io/2016/01/simple-leader-election-with-Kubernetes.html。所以在这里我就不多加赘述了,但是我会讲一讲我们是如何利用镜像来解决我们的问题的。


解决问题

我们做的就是带来gcr.io/google_containers/leader-elector容器到我们的pod,这样就可以让单体应用的实例都运行一个leader选举的实例。这是证明Kubernetes pod有用的典型例子。

以下是一个在我们的配置 resource 中定义好的 pod 的摘录:

我们开启leader选举以及我们的单体应用程序。注意,我们将 --election=monolith-jobs 当作第一个参数。这就意味着 leader 选举知道容器属于哪一个组。所以指定这个组的容器会是leader选举进程中的一部分,这个组之中只有一个容器会被选举为 leader。 --http=localhost:4040的第二个参数同样是非常重要的。它在 4040 端口打开了一个网页服务器,在这个端口,我们可以查询到目前leader的pod名字,然后以这个格式返回:

这是我们决定要不要运行我们的job的一个小把戏。我们要做的事情就是检查即将执行调度 pod 的名字是否跟选举出来的 leader 一致,如果一致,我们就应该继续执行,并且执行 job,或者其它的我们应该返回的东西。比如:

所以我们来看看 ClusterLeaderService 是如何被实施的。首先,我们必须从应用程序获得pod的名字。Kubernetes将pod名字存储在/etc/hostname ,Java 将这个 /etc/hostname 暴露在 HOSTNAME 环境变量,这就是我们将在这个例子中引用的。另一个方法就是使用 Downward API 将pod名字暴露到环境变量选择。比如:

从这里,我们可以看到 metadata.name (也就是pod的名字)会与MY_POD_NAME环境变量联系在一起。但是现在让我们来看看 ClusterLeaderService的实施是看起来是怎么样的:

在这里例子中,我们正在从 RESTAssured 项目使用 JsonPath 来查询选举者网页服务,并且从回应中提取 pod 的名字。然后我们简单地将本地容器的名字跟 leader 相比较,如果他们是相同的,那么我们就知道这个实例就是leader!就是这样!


结论

事实证明,上述工作运行地很不错!如果leader节点要挂掉了,那么另一个就会自动选举上。但这个过程会花上一点时间,要一分钟左右。所以这个还是要权衡一下的。假如你的工作中不允许错过任意一个 job 执行,那么这个选择对你来说是不合适的。但在我们上述的例子中,中间有那么一分钟 job 没有被准确执行,对我们来说无伤大雅。所以我觉得这个方法十分简单,而且当移植一个现有的包含调度jobs的应用程序的时候很有价值,因为调度 jobs 总有各种各样很难提取的原因。

点击体验,开启谷歌级数字化之旅
立即体验
立即咨询