大家好,我是阿呆,一个不务正业的程序员,不愿躺平的年轻人。
上一篇文章我们介绍了一下Temporal的一些基础概念和简单的架构设计。今天我们来说一说,为什么要用Temporal。
因为对复杂的分布式系统进行抽象,是Temporal很重要的一部分。我们先来想想为什么要用分布式。分布式系统是为了实现我们服务的扩展性,在系统负载发生变化时,随时扩展我们的服务能力。也就是说分布式系统实现了我们应用程序的高可靠、高性能和可扩展性。
但是使用分布式系统就要面临一个问题:下游应用程序随时可能会发生故障,尤其是在规模比较大的时候,发生故障是很常见的事情。
在传统的系统中,通常会投入大量的资源到组件之间的健康检查、健康状态的可视化、设计执行的超时约束、执行失败的重试以及保证状态一致性上。这种系统一般都是无状态服务、数据库、cron作业和任务队列的一个组合体。随着系统的扩展,如果想要响应异步事件、与外部资源进行通信或者监听一个复杂的事物状态时候,会给系统带来比较大的挑战。
那Temporal是怎么做的呢?Temporal直接把服务端、数据库、cron作业、任务队列、主机进程和SDK组合封装在了Temporal Platform里,这样就能直接解决故障。
我们来看一张对比图:
乍一看上去是不是差不多?但其实有几个方面存在着重大的差异。
对于传统系统,如果一个函数执行失败,就无法再恢复了,因为所有执行状态都丢失了。函数执行等待的时间越长,失败的可能性就越大。另外通常函数的执行具有有限的生命周期,通常以分钟为单位。
而对于Temporal,Workflow Execution在失败后是完全可以恢复的,同时Temporal对工作流的执行没有最后的期限,可以执行无限长时间。
对于传统系统,一个函数执行失败或者停止,意味着所有的执行状态就丢失了。我们的应用程序必须监听服务的响应来重启服务并执行重试。这个重试是从初始状态开始的。
而Temporal失败恢复时是从最新的失败状态恢复的,也就是说可以保留所有的执行进度。
使用传统系统,是无法与函数执行进行通信的。
使用Temporal的Signals和Queries,可以将数据发送到 Workflow Execution 或从中查询一些数据。
说了这么多,也不是很清楚?我们来看一个例子。订阅在我们生活中是非常常见的,例如我们订阅每个月的报纸,每个月续费的会员也是订阅,我们就以订阅为例,看一下传统系统和Temporal分别是怎么设计的。
先来梳理一下订阅的业务逻辑:
我们先来看第一种设计方案:以数据库为中心的设计
客户订阅的状态存在数据库,然后应用程序定期去扫描数据库表查找特定客户的订阅状态,然后执行操作例如扣费或者取消订阅,同时更新数据库状态。
这么做看上去没什么问题,但是会存在一些缺点:
另一种常用的设计是基于队列系统,使用定时服务和队列,订阅状态变更时发送到队列,然后服务消费并更新数据库。定时服务可以安排队列的轮询或者数据库操作。
虽然这种方法显示出更好的扩展性,但编程模型可能变得非常复杂且容易出错,因为排队系统、定时服务和数据库之间通常没有事务更新。
我们最后来看看Temporal是怎么做的。
Temporal的核心思想是把我们的业务逻辑封装成一个Workflow,我们来看一下:
package io.temporal.sample.workflow;
import io.temporal.activity.ActivityOptions;
import io.temporal.sample.activities.SubscriptionActivities;
import io.temporal.sample.model.Customer;
import io.temporal.workflow.Workflow;
import java.time.Duration;
/** 定义一个Workflow的实现. 注意这就是一个POJO对象. */
public class SubscriptionWorkflowImpl implements SubscriptionWorkflow {
private int billingPeriodNum;
private boolean subscriptionCancelled;
private Customer customer;
/*
* 定义Activity的一个配置项,不需要关心
*/
private final ActivityOptions activityOptions =
ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(5)).build();
// 定义一个Activity,暂时也不需要关心
private final SubscriptionActivities activities =
Workflow.newActivityStub(SubscriptionActivities.class, activityOptions);
@Override
public void startSubscription(Customer customer) {
// 设置工作流变量——订阅客户
this.customer = customer;
// 给客户发送欢迎邮件
activities.sendWelcomeEmail(customer);
// 开始试用期,用户也可以在这个过程中取消订阅
Workflow.await(customer.getSubscription().getTrialPeriod(), () -> subscriptionCancelled);
// 如果在试用期就取消了订阅,就发送一个取消订阅邮件
if (subscriptionCancelled) {
activities.sendCancellationEmailDuringTrialPeriod(customer);
// 这个用户的订阅就结束了,直接结束掉Workflow
return;
}
// 试用期已经结束, 开始收费直到订阅到期或者取消订阅
while (billingPeriodNum < customer.getSubscription().getMaxBillingPeriods()) {
// 向用户收取使用费
activities.chargeCustomerForBillingPeriod(customer, billingPeriodNum);
// 等待用户支付,或者取消订阅
Workflow.await(customer.getSubscription().getBillingPeriod(), () -> subscriptionCancelled);
// 如果取消订阅,发送邮件
if (subscriptionCancelled) {
activities.sendCancellationEmailDuringActiveSubscription(customer);
// 订结束
break;
}
// 订阅周期加1
billingPeriodNum++;
}
// 整个订阅已经结束,通知用户购买新的订阅
if (!subscriptionCancelled) {
activities.sendSubscriptionOverEmail(customer);
}
}
@Override
public void cancelSubscription() {
subscriptionCancelled = true;
}
@Override
public void updateBillingPeriodChargeAmount(int billingPeriodChargeAmount) {
customer.getSubscription().setBillingPeriodCharge(billingPeriodChargeAmount);
}
@Override
public String queryCustomerId() {
return customer.getId();
}
@Override
public int queryBillingPeriodNumber() {
return billingPeriodNum;
}
@Override
public int queryBillingPeriodChargeAmount() {
return customer.getSubscription().getBillingPeriodCharge();
}
}
如果下游处理服务宕机或没有响应,chargeCustomerForBillingPeriod被阻塞一天或更长时间是完全可以的。同理,直接在 Workflow 代码内部休眠 30 天是完全正常的操作。这是非常有可能的,因为基础设施故障不会影响工作流状态——包括线程、阻塞调用和任何变量。
Temporal Platform实际上对开放工作流执行的数量没有可伸缩性限制,因此即使您的应用程序有数亿客户,也可以反复使用此代码。
也许你看这段代码还是有一些不理解,里边的Workflow和Activity的概念又是什么。没关系,从下篇文章开始,我们就正式进入核心概念的介绍,到那个时候再回过头来看我想就能看懂了。
我是阿呆,我们明天见。