Quarkus 的长期运行活动扩展
简介
Quarkus LRA 扩展对于构建希望明确同意交互何时完成的 JAX-RS 服务非常有用,无论是成功还是失败。在成功的情况下,所有参与者都可以清理资源,并确信所有其他服务也会这样做。反之,在失败的情况下,参与者知道彼此会补偿在交互过程中执行的任何操作。此功能意味着参与服务的可以达成共识并实现原子结果。
我们将服务交互称为 LRA,即 Long Running Action(长期运行操作)的缩写。LRA 具有特定的属性和保证,有助于构建可靠的服务交互。每个操作都会被分配一个唯一的标识符(称为 LRA 上下文),以便它可以与其他 LRA 区分开来。
服务通过使用 @LRA 注解标记 JAX-RS 方法来启动 LRA(或加入现有 LRA)。当调用此类方法时,系统将启动该操作,并将其标识符作为名为“Long-Running-Action”的 JAX-RS 标头提供。如果方法体执行了任何 JAX-RS 调用,该标头将自动添加到出站请求中。通过这种方式,目标服务可以加入交互(如果它们也使用 @LRA
注解进行标记)。
以这种方式注解的任何方法所执行的工作都应该是“可补偿的”,这意味着如果某个服务“取消”了 LRA,那么该服务将可靠地收到通知,它应该补偿所做的任何工作。每个服务负责解释补偿的含义,唯一的要求是当它收到应补偿的通知时,它能够 适当地响应。服务通过使用 @Compensate 注解标记其某个方法来指示如何接收通知。有关如何控制 LRA 结果的详细信息,请参阅 @LRA
注解的 javadoc。
该扩展提供了 MicroProfile LRA 规范及其相关 注解的实现。 |
LRA 协调器
narayana-lra
扩展要求环境中存在一个正在运行的协调器。协调器可以从 https://quay.io/repository/jbosstm/lra-coordinator
获取,或者您可以使用包含适当依赖项的 maven pom 构建自己的协调器。在本博客中,我们将展示如何使用 quarkus-maven-plugin
从头开始创建一个。在其中一个 narayana 博客中有一些关于配置协调器的额外信息。
由于协调器只是一个 JAX-RS 资源,我们可以使用 quarkus 来构建一个,并添加 resteasy-jackson
和 rest-client
扩展。
$ mvn io.quarkus:quarkus-maven-plugin:2.2.1.Final:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=narayana-lra-coordinator \
-Dextensions="resteasy-jackson,rest-client"
$ cd narayana-lra-coordinator/
$ rm -rf src
请注意,我们删除了生成的 src 目录,因为我们只需要 quarkus 框架来运行协调器。
更新 pom.xml 文件以添加对 narayana 协调器实现的依赖。
<dependency>
<groupId>org.jboss.narayana.rts</groupId>
<artifactId>lra-coordinator-jar</artifactId>
<version>5.12.0.Final</version>
</dependency>
现在构建它并在后台运行它。
$ ./mvnw clean package
$ java -Dquarkus.http.port=50000 -jar target/quarkus-app/quarkus-run.jar &
这里我们将在 narayana-lra
quarkus 扩展使用的默认端口 50000
上运行协调器。您可以通过列出当前的 LRAs 来验证协调器是否正常运行。
$ curl https://:50000/lra-coordinator
[]
此代码片段显示请求返回了一个空的 json 数组。
我们将保持协调器运行(在默认端口上),同时开发和测试使用 LRA 的服务。在文章的最后,我们将展示如何将协调器嵌入服务中(注意:您不能使用此方法在原生模式下运行协调器,将提供一个未来的扩展来支持此需求)。
JVM 模式
要构建具有 LRA 支持的 JAX-RS 应用程序,请将 io.quarkus:quarkus-narayana-lra
依赖项添加到应用程序的 pom 中。这将添加 JAX-RS 支持,并在开发服务时提供 LRA 注解,它还会注册一个 JAX-RS 过滤器,自动管理您的服务在 LRA 中的参与。
如上所述,LRA 规范要求的保证(最终一致性)依赖于协调参与 LRA 的服务。此组件必须在开始交互、加入交互和结束交互时存在。如果协调器变得不可用,应重新启动它。同样,参与 LRA 的服务在结束阶段必须可用;系统将继续重试服务,直到它指示已完成 LRA,一旦服务指示补偿(或完成)成功,它就不再参与交互(尽管它可以注册以获得所有服务已完成补偿或完成的可靠通知)。虽然可以有多个协调器,但在撰写本文时,一个 LRA 只能由一个协调器管理。
步骤 1:创建应用程序
$ mvn io.quarkus:quarkus-maven-plugin:2.2.1.Final:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=narayana-lra-quickstart \
-Dextensions="narayana-lra"
$ cd lra-quickstart
请注意,如果协调器运行的端口与默认端口 50000
不同,那么您需要更新应用程序配置文件(src/main/resources/application.properties
)并指定主机和端口。
quarkus.lra.coordinator-url=https://:<port>/lra-coordinator
验证在这些更改后生成的应用程序仍然有效。
$ ./mvnw clean package
步骤 2:添加 LRA 支持
现在更新生成的应用程序,使 hello 方法在长期运行操作的上下文中执行。
在编辑器中打开 src/main/java/org/acme/GreetingResource.java
文件,并使用 @LRA 注解 hello 方法(同时使用 JAX-RS javax.ws.rs.HeaderParam
注解将 LRA 上下文注入方法)。此外,添加两个回调方法,它们将在 LRA 关闭或取消时被调用。
最终结果,包括导入,应如下所示。
package org.acme;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
// Step 2a: Add imports for reading the LRA context and for using LRA annotations
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.core.Response;
import java.net.URI;
import org.eclipse.microprofile.lra.annotation.ws.rs.LRA;
import org.eclipse.microprofile.lra.annotation.Compensate;
import org.eclipse.microprofile.lra.annotation.Complete;
import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER;
@Path("/hello")
public class GreetingResource {
@GET
@LRA // Step 2b: The method should run within an LRA
@Produces(MediaType.TEXT_PLAIN)
public String hello(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId /* Step 2c the context is useful for associating compensation logic */) {
System.out.printf("hello with context %s%n", lraId);
return "Hello RESTEasy";
}
// Step 2d: There must be a method to compensate for the action if it's cancelled
@PUT
@Path("compensate")
@Compensate
public Response compensateWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) {
System.out.printf("compensating %s%n", lraId);
return Response.ok(lraId.toASCIIString()).build();
}
// Step 2e: An optional callback notifying that the LRA is closing
@PUT
@Path("complete")
@Complete
public Response completeWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) {
System.out.printf("completing %s%n", lraId);
return Response.ok(lraId.toASCIIString()).build();
}
}
通过这些更改,如果您构建应用程序然后调用 hello 方法,则将在方法进入前启动 LRA,并在方法完成后结束 LRA。
$ ./mvnw clean package
$ java -jar target/quarkus-app/quarkus-run.jar &
[1] 2389948
$ curl https://:8080/hello
hello with context https://:50000/lra-coordinator/0_ffffc0a8000e_8c1f_612a6e9b_a
completing https://:50000/lra-coordinator/0_ffffc0a8000e_8c1f_612a6e9b_a
Hello RESTEasy
确保协调器仍在运行,否则您将看到类似以下的错误消息。
2021-08-11 14:27:45,779 WARN [io.nar.lra] (executor-thread-0) LRA025025: Unable to process LRA annotations: -3: StartFailed (start LRA client request timed out, try again later) ()'
注意指示 @Complete
回调已被调用的 System.out
消息。现在为了准备下一步而终止 Java 进程(进程 id 已在控制台中打印,在我举例的 pid 是 2389948,所以我输入了 kill 2389948
)。
步骤 3:将 LRA 扩展到两个服务方法
在此步骤中,我们将启动一个 LRA,但不在方法完成时结束它,而是使用 LRA 注解的 end 元素。
定义第二个业务方法来完成此操作。
@GET
@Path("/start")
@LRA(end = false) // Step 3a: The method should run within an LRA
@Produces(MediaType.TEXT_PLAIN)
public String start(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) {
System.out.printf("hello with context %s%n", lraId);
return lraId.toASCIIString();
}
与 hello 方法唯一的区别在于 @Path
和 @LRA
注解,并且它将 LRA id 返回给调用者。我们需要它来设置当我们向 hello 方法发送请求以完成 LRA 时的标头(请注意,此标头也可在 JAX-RS 响应标头之一中获得)。
终止现有实例(kill 2389948
)并重建应用程序(./mvnw package -DskipTests
)并在后台运行。
$ java -jar target/quarkus-app/quarkus-run.jar &
[1] 2495275
使用 curl
向我们刚刚添加的新方法发送请求来启动一个 LRA。
$ LRA_URL=$(curl https://:8080/hello/start)
hello with context https://:50000/lra-coordinator/0_ffffc0a8000e_a909_611a92ea_2
start 方法被编码为返回 LRA id,并且我使用了 bash
将其保存在一个名为 LRA_URL
的环境变量中。原始的 hello 方法使用了 @LRA
注解的 end 元素的默认值,因此如果我们使用 LRA 上下文调用该方法,LRA 将在方法完成后自动关闭。
$ curl --header "Long-Running-Action: $LRA_URL" https://:8080/hello
hello with context https://:50000/lra-coordinator/0_ffffc0a8000e_a909_611a92ea_2
completing https://:50000/lra-coordinator/0_ffffc0a8000e_a909_611a92ea_2
Hello RESTEasy
请注意,completeWork
方法已被调用。
步骤 4:在一个微服务中启动 LRA,并在另一个微服务中结束 LRA
此步骤展示了两个不同的微服务如何在没有耦合的情况下协调它们的活动。在不同的端口上启动 hello 应用程序的第二个实例。
$ java -Dquarkus.http.port=8081 -jar target/quarkus-app/quarkus-run.jar &
[2] 2495369
由于我们仍在使用相同的应用程序资源文件和外部协调器,因此无需更新配置。
再次,使用 curl
向第一个服务的 start 方法发送请求来启动一个 LRA。
$ LRA_URL=$(curl https://:8080/hello/start)
hello with context https://:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_11
然后在第二个服务(运行在 8081 端口上的那个)中结束它。
$ curl --header "Long-Running-Action: $LRA_URL" https://:8081/hello
hello with context https://:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_11
completing https://:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_11
completing https://:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_11
Hello RESTEasy
请注意,两个微服务都表明它们收到了完成回调。
终止两个 Java 进程(kill 2495275 2495369
)。
可选步骤:使用 MANDATORY 元素
您可能更喜欢编写一个需要上下文的方法来关闭 LRA,而不是使用现有方法。在这种情况下,您需要设置 LRA.Type
元素。
@GET
@Path("/end")
@LRA(value = LRA.Type.MANDATORY) // Step 3a: The method MUST be invoked with an LRA
@Produces(MediaType.TEXT_PLAIN)
public String end(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) {
return lraId.toASCIIString();
}
由于 end 方法使用 @LRA(value = LRA.Type.MANDATORY)
进行注解,因此必须存在上下文标头,否则方法将返回错误响应代码。
$ ./mvnw clean package -DskipTests
$ java -Dquarkus.http.port=8081 -jar target/quarkus-app/quarkus-run.jar &
[1] 300189
$ LRA_URL=$(curl https://:8081/hello/start)
$ curl -v https://:8081/hello/end
...
HTTP/1.1 412 Precondition Failed
...
而提供 LRA 上下文标头将起作用。
$ curl --header "Long-Running-Action: $LRA_URL" -I https://:8081/hello/end
HTTP/1.1 200 OK
Content-Type: application/octet-stream
connection: keep-alive
$ kill 300189
其他 LRA.Type 元素值根据您的应用程序想要实现的目标可能会很有用。对于熟悉 JTA 的读者来说,值得注意的是,它在很大程度上借鉴了 Java Transactional TxType 注解。
原生模式
在原生模式下,仅支持外部协调器(即未嵌入应用程序中的协调器)(我们将在后续版本中提供协调器扩展来解决此缺陷)。 |
首先构建一个原生可执行文件。
$ ./mvnw package -DskipTests -Pnative
检查协调器部分 中启动的外部协调器 是否仍在 50000 端口上运行,然后在后台以原生可执行文件的形式启动服务。请注意,如果协调器未在默认端口上运行,您需要将正在运行的协调器位置作为 Java 系统属性(-Dquarkus.lra.coordinator-url=https://:50000/lra-coordinator
)传入,或者您可以更新应用程序配置并重新构建原生可执行文件。
启动原生服务的实例。
$ ./target/narayana-lra-quickstart-1.0.0-SNAPSHOT-runner &
[1] 2434426
记下进程 ID(例如 2434426),因为我们稍后需要终止该进程。
开始一个新的 LRA。
$ LRA_URL=$(curl https://:8080/hello/start)
并在另一个方法中结束它。
$ curl --header "Long-Running-Action: $LRA_URL" https://:8080/hello
hello with context https://:50000/lra-coordinator/0_ffffc0a8000e_8479_612e13fa_2
completing https://:50000/lra-coordinator/0_ffffc0a8000e_8479_612e13fa_2
Hello RESTEasy
在准备下一步之前终止服务(kill 2434426
),或者让它保持运行。
故障处理
在此步骤中,我们将启动一个在一个服务中运行的 LRA,然后在 LRA 完成之前终止该服务。然后,我们将使用第二个服务来结束 LRA,并注意到第二个服务已完成,但 LRA 仍处于 Closing
状态,因为第一个失败的服务中的参与者仍需要完成。如果要使 LRA 进入 Closed
状态,则必须重新启动失败的服务,以便它可以响应 Complete
请求。
在默认端口 8080 上重新启动第一个服务(并记下其进程 ID)。
$ ./target/narayana-lra-quickstart-1.0.0-SNAPSHOT-runner &
[1] 2434936
并启动第二个服务实例(在 8082 端口上)。
$ ./target/narayana-lra-quickstart-1.0.0-SNAPSHOT-runner -Dquarkus.http.port=8082 &
[2] 2434984
在第一个服务处启动一个 LRA。
$ LRA_URL=$(curl https://:8080/hello/start)
hello with context https://:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_34
终止第一个服务。
$ kill 2434936
2021-08-11 16:02:24,542 INFO [io.quarkus] (Shutdown thread) narayana-lra-quickstart stopped in 0.003s
现在,只运行第二个服务,尝试结束 LRA。
$ curl --header "Long-Running-Action: $LRA_URL" https://:8082/hello
hello with context https://:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_34
completing https://:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_34
Hello RESTEasy
LRA 仍将运行,您可以通过查询协调器(curl https://:50000/lra-coordinator
)来验证。
要完成 LRA,请重新启动失败的服务(该服务监听在 8080 端口上)。
$ ./target/narayana-lra-quickstart-1.0.0-SNAPSHOT-runner &
[3] 2435130
恢复处理是周期性的(默认恢复间隔为 2 分钟)。如果您无法等待 2 分钟,可以通过协调器的恢复端点手动触发恢复周期,如下所示。
$ curl https://:50000/lra-coordinator/recovery
completing https://:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_34
[]
这里需要注意的是,重新启动的服务收到了完成通知(completing …
)。运行恢复周期的请求结果是正在恢复的 LRA 的 json 数组(在本例中列表为空,因为最后一个 LRA 现已完成,如空 json 数组 []
所示)。
通过停止两个服务来清理(kill 2434984 2435130
)。
附录 1
嵌入式协调器
由于协调器只是 JAX-RS 应用程序,因此可以通过将 LRA 协调器依赖项添加到应用程序 pom.xml 文件中,轻松地将其嵌入 JAX-RS 服务中。
<dependency>
<groupId>org.jboss.narayana.rts</groupId>
<artifactId>lra-coordinator-jar</artifactId>
<version>5.12.0.Final</version>
</dependency>
并且由于 quarkus 默认只允许每个部署一个应用程序,因此您需要将以下属性添加到应用程序配置文件(src/main/resources/application.properties
)中。
quarkus.resteasy.ignore-application-classes=true
如 协调器部分中所述,相同的注意事项仍然适用。
-
不支持原生可执行文件。
-
每个实例都需要协调器的事务日志专用存储(因为目前不支持共享事务存储)。
嵌入式协调器将可通过应用程序的同一端口(路径为 lra-coordinator
)访问,但请注意,默认协调器端口为 50000
,因此您需要在应用程序配置中配置其位置,以告知应用程序使用它。
quarkus.http.port=8080
quarkus.lra.coordinator-url=https://:8080/lra-coordinator
事务日志的位置无法以此方式配置,必须通过系统属性(ObjectStoreEnvironmentBean.objectStoreDir
)进行配置。
$ java -DObjectStoreEnvironmentBean.objectStoreDir=target/lra-logs -jar target/quarkus-app/quarkus-run.jar &
[1] 2443349
$ LRA_URL=$(curl https://:8080/hello/start)
02021-08-11 17:42:30,464 INFO [com.arj.ats.arjuna] (executor-thread-1) ARJUNA012170: TransactionStatusManager started on port 35827 and host 127.0.0.1 with service com.arjuna.ats.arjuna.recovery.ActionStatusService
hello with context https://:8080/lra-coordinator/0_ffffc0a8000e_a985_6113fdf6_2
$ curl https://:8080/lra-coordinator
[{"lraId":"https://:8080/lra-coordinator/0_ffffc0a8000e_a985_6113fdf6_2","clientId":"org.acme.GreetingResource#start","status":"Active","startTime":1628700150466,"finishTime":0,"httpStatus":204,"topLevel":true,"recovering":false}]
现在在另一个方法中结束 LRA。
$ curl --header "Long-Running-Action: $LRA_URL" https://:8080/hello
hello with context https://:8080/lra-coordinator/0_ffffc0a8000e_a985_6113fdf6_2
completing https://:8080/lra-coordinator/0_ffffc0a8000e_a985_6113fdf6_2
Hello RESTEasy
后续的扩展将为嵌入式协调器提供更好的支持(包括使用标准 quarkus 机制来配置它们)。