微进程:微服务中后台作业的一种新架构设计模式
实现微服务时,后台进程是最容易被忽略的元素,而绝大多数应用程序都需要后台进程。
微服务领域的大多数参考书目都着重于如何拆分单体、领域驱动设计、编排与同步、如何拆分数据库等。但人们往往不会提到后台进程,以及如何在微服务架构环境中实现它们。
关于这一点,我会推荐 Sam Newman 的《构建微服务》和《从单体到微服务》两本书,其中涵盖了上面的几乎所有内容,当然只有后台作业除外。
介绍下我们的背景概况。在 CreditorWatch,我们目前实现了几个微服务,每个微服务负责一个数据(域)。
我们的后台进程不仅限于用户交互触发的操作(在这种情况下,我们将引发一个事件,该事件将通过我们的事件驱动架构等部分,相当标准的设计)。实际上很多(大多数)后台任务都是计划任务,并负责数据提取、数据更新、电子邮件等事项。
就规模而言,在 CreditorWatch,我们每个月大约有 4000 万个微进程。
其中许多进程非常冗长且笨重(其中一些进程可能需要长达一周的时间才能完成),例如在我们的 OLTP 系统中计算澳大利亚所有公司的所有信用评分的进程。在 CreditorWatch,我们有一个非常高效的 CI/CD 管道,每天可以触发多次部署,并且我们为微服务使用了 Docker 容器。
这基本上就是我们拥有的基础架构以及需要解决的问题。
理想情况下,我们不希望有哪个盒子,就因为它正在运行一个长期进程就无法更新,因此解决方案需要考虑到这一点,并将尝试解决这个问题。
在这篇短文中,我们将尝试解释微进程模式(我们根据微服务和后台进程这两个词创造了这个术语),以及如何使用 AWS 服务成功实现微进程模式。我们把它叫做一种设计模式,是因为它是针对一个常见问题(在微服务架构中实现较长的后台进程)的可靠解决方案(我们已经成功实现了多次)。
微进程处理过程主要是将非常大的任务(1 个进程)划分为一些较小的任务(微进程),然后使用我们的微服务逻辑和架构处理它们。
这个概念并不是什么新鲜事物,并已在其他领域广泛使用(BigData 集群中的 MapReduce,或分治算法),但这种方法将相同的技术应用于微服务架构,给我们带来了很多好处,而缺点却很少。
要实现这种方法,我们有 1 个进程(可以是计划或手动触发),其唯一的工作就是收集并触发所有需要处理的作业。请注意,此进程实际上不会处理任何需要实现的最终结果(在我们之前的示例中,最终结果指的是计算所有澳大利亚公司的所有信用评分)。只需排队一个作业就可以更新每家公司的信用评分
这明显要比计算所有信用分数要快,因为分成多个的微进程只需要花费几分钟就能算出分数,而计算所有信用分数则需要几天时间。在我们的案例中,计算一个信用评分平均需要半秒(我们如何做到以如此快的速度来处理我们这么大规模的数据量,可以另开一篇文章详细讲解了),因此考虑到我们的数据库中有近 1900 万家公司,单个进程完成整个计算大约需要 100 天。鉴于我们每个月都要这样做,因此一个单独的进程是不可行的。
很多时候,划分任务的进程非常轻巧,我们可以在一个 lambda 函数中实现它(请注意 lambda 函数的处理时间限制为 15 分钟),这样我们就不必担心服务器或虚拟机中的 crontab 配置。
此时,我们的队列中有很多(也许是数百万个)小任务等待处理,因此“真正的工作”尚未完成。
当然,一旦你将所有作业都排在队列中,就有许多方法可以并行执行作业。
传统上,我们可能会有一个带有监督者(或类似对象)的盒子,让多个进程从队列中提取消息,但这意味着我们会有一个盒子不断地运行代码以提取消息和代码等待处理,这就属于微服务了。
即使这种方法(和其他使用相同微服务代码的方法,以及在同一环境中从队列中提取消息的代码)是有效且可行的,我们还是发现有两种不同的环境(具有后台进程和用于实时流量的 docker 容器的虚拟或物理服务器)会带来很多开销。
在某些配置中(例如一个虚拟盒子),如果我们要部署,将需要停止监督并等待进程完成,然后再用新代码启动一个新的并销毁前一个,这将大大增加部署的复杂程度,因为我们需要跟踪所有后台进程。
另外,我们不得不想出两种不同的方式来监视我们的应用程序(后台进程和活动端点),确保我们的日志记录器能够正确跟踪两个不同环境中的所有日志,并确保两处的依赖都正确无误,等等。
请注意,我甚至没有提到有两个不同的代码库负责计算信用评分,一个代码库用于后台进程,另一个代码库用于微服务,所以还得考虑那些不能出现代码复制的禁区。
理想情况下,我们希望:
不要重复代码
没有多个(需要测试)的系统配置
能够监控我们后台进程的健康状况和进度
缩放(例如,在工作时间以外更快地处理)
能够快速部署并尽快使用最新版本的代码
部署简单且维护成本低廉
我们提出的用于处理微进程的解决方案是微服务架构的原生方案。我们利用 SQS+Lambda 创建了一个推送队列,并调用一个微服务端点来执行微进程的任务。
我们在这里更具体地讨论了 SQS+lambda 方法。
微进程模式架构
这里仅包含以下三个元素:
一个进程将大进程分成多个很小的微进程
推送队列(在我们的示例中使用 SQS+Lambda 函数实现)
嵌入微服务的端点
我们实现了我们想要的大部分目标。
我们实现了:
不要重复代码(所有代码都驻留在微服务代码库中)
没有多个需要我们测试的系统配置(我们只有微服务基础架构)
能够监视我们后台进程的健康状况和进度(我们可以全程看到队列中有多少待处理消息)
缩放(在实现 lambda 函数时,我们可以按需缩放,更多信息请参见这里)
能够快速部署并(通过当前的部署)尽快使用最新版本的代码
部署简单且维护成本低(我们像往常一样部署,不需要额外的开销)
但是,这一解决方案也有其缺点:
微进程限制为 15 分钟(如果使用 Lambda 的话)
实时流量和来自后台作业,到同一基础架构的流量会混淆监视并影响实时流量(后文会列出解决方案)
也许进程无法分割,所以这种方法无济于事
微进程的进程可能比实时流量慢,并且我们要确保可以正确监控两种进程的健康状态。
为了避免混淆监控,并避免微进程可能对实时流量产生的影响(它会消耗实时流量所需的资源,例如内存、每个容器的最大进程等),我们在另一个子域下构建了一个克隆基础架构(相同的 docker 容器映像)。
在我们的案例中,对于信用评分示例,我们有:
scores-live.domain.com
score-queue.domain.com.au
后台作业将指向 creditscore-queue-service.creditorwatch.com.au,而实时流量将指向 creditscore-service.creditorwatch.com.au。
通过这一小小的调整,我们可以按需只缩放实时流量(或后台进程)的容量,而又不影响另一方,并且可以更有效地进行监控,因为我们可以轻松地按主机过滤。
先前的进程涵盖了我们大进程中的所有小部分,但是如何将它们粘合在一起呢?
继续看前文的示例,后台进程的目标是获取包含我们所有公司的所有信用评分的报告,并将其通过电子邮件发送给数据科学团队,以便他们进行统计。
当处理并发进程时,这是软件工程中一个非常著名的问题,并且它有很多解决方案(囚徒问题是并发的经典问题,如果你想编写一个监视器模式,则它是一个很好的练习)。我们会只使用已经讨论过的解决方案提出一个简单的方案。
启动所有进程时,我们将在数据库中创建一条记录。该进程将有一个进程 ID。这将是父 ID 进程。对于其余的部分,我们还将创建一条记录,并使用其自己的进程 ID 和对父记录的引用。该记录将具有该进程的结果(在本例中为信用评分)。请注意,你可能需要存储大量信息(实际上,我们有一个进程存储一个文本文件,该文件需要合并到其他文件中以完成整个任务)。在这种情况下,你可以放入一个文件管理器(已挂载的卷、S3 文件夹等),并存储对它的引用。
现在,当子进程运行并完成时,它需要通知父进程,后者将检查所有其他进程是否已完成。如果完成,它将运行任务将所有信用评分存储在文件中,然后发送电子邮件。
当然,有不同的方法来通知父进程。在上面的示例中,使用现有的架构似乎是合理的,该架构是将作业排队,然后使用一个推送队列在微服务中执行代码以评估一切是否完成,如果完成,则收集结果并发送电子邮件。
提醒一下:在处理并发进程时,请确保锁定正在使用的表,以确保进程互斥。否则,你会遇到一些麻烦事。
长时间运行的后台进程可能很难在微服务架构中实现,并且会带来一些挑战,因此,为了克服这些挑战,我们创建了一种称为微进程的新设计模式。
微进程模式包括:
创建一个将长时间运行的进程划分为很多较小的微进程的进程
将所有微进程排入推送队列
将消息转发到你的微服务进行处理
使用现有的 APM 工具和日志进行监视
推送队列和 lambda 函数可能会让人头疼,因此这里推荐大家阅读一些相关文章,这样你就能事先了解所有所需的信息。
希望你满意,有任何问题或建议都可以联系我们!我会很高兴与大家交流的!