本文要点
CQRS 和事件溯源需要特定基础设施的支持,比如事件存储以及命令、查询和事件的传输(消息传递中心)。
我们可以通过结合使用现有中间件工具(例如 Kafka 和 AMQP)来实现 CQRS 和事件溯源的多种消息传递模式,但像 Axon 服务器这样的多合一解决方案是一种更强有力的选择。
最好以一种“无状态”的方式来安装和运行 Axon 服务器,将配置分为固定的和特定于环境的两部分。
Axon 服务器企业版具备群集能力,不过个体节点也可以具有特定的身份,可以提供不同的服务。这会影响到部署策略。
向 Axon 服务器添加访问控制(令牌和帐户)和 TLS 非常容易。
Java 中的 CQRS 和事件溯源
基于消息传递和事件驱动的应用程序与传统企业应用程序有着明显不同的需求。其中一个明确的标志是将重点从保证传递单个消息(消息传递职责主要在中间件)转移到了“智能端点和管道”上,由应用程序自己负责监控消息的传递。于是,消息传递基础设施的 QoS 更多的是与吞吐量而不是传递保证有关。如果消息未到达预期目的地,这是发送者和接收者要解决的问题,因为它们需要负责受此类故障影响的业务需求,并做出合适的响应。
另一个正在发生的变化是存储和算力价格的稳步下降以及为应用程序组件分配资源的灵活性的提升:命令查询责任隔离(简称为 CQRS)和事件溯源。在应用程序环境中,我们不再使用单个组件来管理更新和查询操作,而是将这两种职责分开,并提供多个查询源。命令组件(通常是低频的)可以针对验证和存储进行优化,然后通过事件向企业的其他部分发布变更。(多个)查询组件使用事件来构建优化的模型。缓存和批处理使用率的增加是一个预警信号,表明系统迫切需要这种架构模式和具有可重播事件存储的查询模型。事件溯源通过事件序列来定义实体的当前状态。这意味着我们不再使用可更新的记录存储,而是使用仅追加的事件存储,这样我们就可以使用 Write-Once(写入一次)语义,同时实现不中断的审计跟踪。
为了支持这些变化,传统组件(即数据库)和面向消息的中间件经过扩展,加入了所需的功能和新的专用基础设施组件。在 Java 和 Kotlin 领域,开源的 Axon 框架提供了 CQRS 和事件溯源范例实现,不过它只是为应用程序的个体模块提供了解决方案。如果将应用程序作为一个单体(这无疑是启动、运行以及进行后续开发工作最快的方式),就无法利用分布式架构的优势,那将是一种浪费。基于 Axon 的应用程序很容易拆分,问题是我们如何支持消息传递和事件存储。
CQRS 应用程序架构
典型的 CQRS 应用程序包含用于交换命令和事件的组件,这些组件为聚合处理结果和优化的查询模型提供持久化存储。聚合持久化层可以建立在 RDBMS 存储层或 NoSQL 文档组件之上,框架的核心部分包含了基于标准 JPA/JDBC 的存储库。查询模型的存储也是一样。
消息交换可以通过大多数标准的消息传递组件来实现,不过模式对于一些特定的应用场景来说还是有用的。对于发布和订阅模型,我们几乎可以使用任意一种现代消息传递解决方案,只要能够确保消息不丢失,因为我们希望查询模型能够忠实地反映出聚合的状态。对于命令组件,我们需要将单向消息传递扩展为请求和应答模式,只要确保能够知道命令处理程序是否可用。其他应答可能是聚合的结果状态,或者是更新失败时的验证信息。在查询方面,简单的请求和应答模式对于分布式微服务架构来说是不够的,我们还需要看看分散和聚集(scatter-gather)模式和先入(first-in)模式,以及持续更新的流式结果。
对于事件溯源,可以将聚合持久层替换成一写多读(Write-Once-Read-Many)的层,该层会捕获由命令产生的所有事件,并为特定聚合提供重播支持。这些事件重播也可以用在查询模型中,我们可以使用内存存储来重新实现它们,或者在怀疑数据出现不一致时进行重新同步。一种有用的改进措施是使用快照,这样就可以避免重放很长的历史事件。Axon 框架为聚合提供了一个标准的快照实现。
现在,我们可以看到,应用程序需要的基础设施组件如下:
用于状态聚合存储和查询模型的“标准”持久化实现。
事件源聚合的一写多读持久化解决方案。
一个消息传递解决方案:
请求和应答;
发布和订阅,至少传递一次;
带有重播的发布和订阅;
分散和聚集;
请求流式应答。
Axon 框架提供了额外的模块来集成一系列开源产品,如基于 Kafka 和 AMQP 的事件分发解决方案。不过,AxonIQ 自己的 Axon 服务器也可以作为一个完整的解决方案。本文将介绍如何安装和运行它,首先是简单的本地安装,然后是基于 Docker 的安装(包括 docker-compose 和 Kubernetes)和“云端”虚拟机安装。
编写测试代码
首先,我们使用一个小程序来演示架构中的组件。为了方便配置,我们将使用 Spring Boot。Axon 有一个 Spring Boot Starter,它会自动扫描我们使用到的注解。在第一次迭代中,我们尽量保持应用程序的简单。它会发送一个命令,并生成一个事件。我们需要处理这个命令:
这里的命令和事件都是简单的值对象,命令包含了来源和消息,事件则只包含消息。这个类还定义了事件处理器:
最后,我们需要通过应用程序入口来发送命令:
另外,我们还需要一些支持代码。Axon 框架希望每一个命令都带有某种身份识别,让同一个接收者可以收到后续相关的命令。完整代码可以从GitHub上下载,除了上述的代码外,下载的代码中还包含了 TestCommand 和 TestEvent 类,并配置了一个路由策略。这些是在创建 CommandBus 时配置的。现在,我们来看一下架构中用到的四个组件。
如果应用程序没有指定命令总线和事件总线,Axon 运行时会假定使用默认设置(即 Axon 服务器提供的实现),并尝试连接。Axon 服务器标准版基于 AxonIQ 开源协议,官方网站提供了预编译的下载包。将下载的包解压,并用 Java 11 运行,它就会用默认设置启动。注意,下面显示使用的是“4.3”版本,具体取决于你下载的是哪个版本。
我们的测试应用程序运行起来了,并连接到 Axon 服务器,然后执行之前的测试代码。
多运行几次,如果幸运的话,你可能会看到多个事件被处理。如果没有,在发送命令和“SpringApplication.exit()”方法调用之间加入“Thread.sleep(10000)”,然后再运行。这个小程序相当于一个客户端应用,它连接到 Axon 服务器,并向处理程序发送命令,然后处理程序将事件传回给客户端。处理程序发送的是一个事件,沿着同样的路径,只是走的是 EventBus,而不是 CommandBus。事件被保存在 Axon 服务器的事件存储中,事件处理器在初次连接时会重播所有的事件。如果你在消息里追加当前的日期和时间,比如直接加个“new Date()”,就会看到事件按照它们进入的顺序排好序。
Axon 框架有两种事件处理器:订阅和跟踪。订阅处理器会订阅事件流,并从订阅那一刻开始处理事件。跟踪处理器会跟踪自己的处理进度,默认从一开始重放事件存储里的所有事件。你可以把它们看成是获取已发布的事件(订阅处理器)和自己拉取事件(跟踪处理器)。在实际应用中,构建查询模型和事件溯源聚合很好地体现了这种区别,因为这个时候你需要完整的事件历史。不过本文不打算继续深入细节,更多内容可以参考 Axon 指南https://docs.axoniq.io。
在我们的例子中,我们可以增加一个配置,用来选择事件处理器类型:
如果按照这个配置 profile 来运行,你会看到只有一个事件被处理。如果不使用这个 profile,就会进入默认的模式,也就是跟踪模式,那么之前所有的事件(包括在订阅模式下发送的事件)都会被处理。
运行和配置 Axon 服务器
现在,我们有了一个可以用的客户端,接下来让我们看一下 Axon 服务器端与消息处理和事件存储相关的东西。在之前的小节中,我们创建了一个叫作“axonserver-se”的目录,并在里面放了两个 JAR 包。现在,在服务器运行的同时,你会看到它生成了一个“PID”文件,这个文件里包含了进程 ID,还有一个“data”目录(这个目录里有一个数据库文件)和一个叫作“default”的目录(这里是事件存储)。到目前,所有的事件存储都包含了一个用于保存事件的文件和一个用于保存快照的文件。这些文件看起来很大,不过其实它们都很“稀疏”,因为为了确保有足够的可用空间,空间是预先分配的,虽然可能里面只有几个事件。到现在我们还没看到有日志文件,也就是保存 Axon 服务器标准输出内容的文件,我们首先要做的就是让它生成日志文件。
Axon 服务器是一个基于 Spring Boot 的应用程序,所以添加日志配置是一件很容易的事情。默认的属性文件叫作“axonserver.properties”,所以如果我们创建一个这样的文件,并把它放到 Axon 服务器运行目录,配置就会生效。Spring Boot 还会检查当前工作目录下的一个叫作“config”的文件夹,我们可以在这个目录里放置公共配置信息,然后使用“config/axonserver.properties”文件来进行定制化配置。Spring Boot 可识别的最简单的日志配置是这样的:
使用以上的配置,日志会被发送到“axonserver.log”文件,并最多保留 10 个 10MB(最大)的文件,其他的会被自动清除。接下来,我们一起看看其他常见的配置属性:
axoniq.axonserver.event.storage=./events
这个属性为事件存储配置自己的目录,因为事件存储会持续增长,我们不希望其他应用程序受到它的影响,出现“磁盘爆满”的情况。我们可以使用挂载的磁盘或链接符号,把事件存储放在我们指定的磁盘空间。
axoniq.axonserver.snapshot.storage=./events
为了减少重播事件的数量,我们可以使用快照。默认情况下,快照被保存在事件的同一个目录,不过我们也可以把它们分开放置。不过,毕竟它们与事件是紧密相关的,所以我们仍然把它们放在同一个位置。
axoniq.axonserver.controldb-path=./data
我们可以保持 ControlDB 在默认位置不动,也可以使用挂载磁盘或符号链接把它放在其他的数据卷上。不过,ControlDB 不会占用太大的空间,所以可以给它分配一个目录,不用担心会使用太大的磁盘。
axoniq.axonserver.pid-file-location=./events
正如我们看到的那样,Axon 服务器会在当前的工作目录生成 PID 文件。我们把生成文件的位置改到和 ControlD 一样,这样就把相关的小文件都放在了同一个目录下,让当前的工作目录变成“只读”的。
logging.file=./data/axonserver.log
这个属性取决于你想要怎样管理日志文件,你可以把目录设置到/var/log,还可以添加日志轮换方式,甚至可以使用“logging.config=logback-spring.xml”来配置更多的日志选项。
axoniq.axonserver.replication.log-storage-folder=./log
这是 Axon 服务器企业版的一个配置属性,用于指定副本日志目录,也就是分布在集群其他节点上的数据变更日志。你可以设置清理时间间隔,所有已提交的日志变更会从日志文件中移除。
在做好这些配置之后,Axon 服务器就可以按照配置的方式使用磁盘,我们可以使用网络存储或云存储,为部署到 CI/CD 管道做好了准备。我在 GitHub 代码库里添加了启动和关闭 Axon 服务器的脚本。
安全配置
出于安全性方面的考虑,我们需要配置服务器的访问控制策略和 TLS。访问控制需要一个令牌,在向 REST 和 gRPC 端点发送请求时需要用到,同时在访问 UI 时需要输入账户信息。另外,一些功能需要特定的角色才能使用,企业版提供了更多的角色,允许为不同的上下文指定不同的角色。在标准版中,我们可以在属性配置文件里使用一个开关,并提供令牌:
你可以使用命令行工具(比如 uuidgen)来生成随机的令牌,并用它来进行身份验证。如果现在启动 Axon 服务器,你需要在命令行工具中指定令牌,在访问 UI 时也需要使用账户登录,尽管这个时候我们还没有创建账号。我们可以通过命令行工具来解决这个问题:
这个时候你可以登录。另外,为了方便起见,你可以创建一个叫作“security”的目录,并把令牌写到一个叫作“.token”的文件里,然后把这个文件放到这个目录。命令行工具会检查这个目录和文件:
在客户端,我们也需要指定令牌:
下一步是添加 TLS 配置。如果是在本地运行,我们可以使用自签名的认证文件。我们可以使用“openssl”工具套件来生成 PEM 格式的 X509 证书文件,用它来保护 gRPC 连接,然后再生成 PKCS12 格式的秘钥和证书文件,用来保护 HTTP 端口:
生成 INI 格式的证书签名请求(CSR),同时也会生成一个未受保护的 2048 位的 RSA 私钥。
用这个文件来生成和签名证书,有效期为 365 天。
把秘钥和证书保存在 PKCS12 秘钥存储里,使用别名“axonserver”。因为这个秘钥存储需要是受保护的,所以我们使用了密码“axonserver”。
现在我们有了如下这些文件:
tls.csr:证书签名请求,这个文件已经没有用了;
tls.key:PEM 格式的私钥;
tls.crt:PEM 格式的证书;
tls.p12:PKCS12 格式的秘钥存储。
我们可以在 Axon 服务器中配置这些文件:
这两种配置方式不一样是因为运行时的不同:HTTP 端口是由 Spring Boot 提供的,使用了“server”的属性前缀,需要使用 PKCS12 秘钥存储。gRPC 端口使用了谷歌的库,需要使用 PEM 格式的证书。把这些添加到“axonserver.properties”,然后重启 Axon 服务器,就可以在控制台看到“Configuration initialized with SSL ENABLED and access control ENABLED”。我们要告诉客户端现在需要使用 SSL 和自签名证书:
需要注意的是,我已经在“hosts”文件中添加了“axonserver.megacorp.com”主机名,其他应用程序也可以找到它,并且与证书中的主机名是匹配的。现在,我们的测试应用程序可以使用 TLS 连接服务器了(下面的内容移除了时间戳等信息):
Axon Sever 企业版
从运维的角度来看,运行 Axon 服务器企业版与标准版区别并不是很大,最主要的区别在于:
你可以运行包含多个实例的集群;
集群支持多个上下文(在标准版中只有“default”);
访问控制有更多角色;
应用程序可以有自己的令牌和认证。
在连接方面,我们有一个额外的 gRPC 端口,用于集群节点之间的通信,默认是 8224。
Axon 服务器节点集群将为(基于 Axon 框架的)客户端应用程序提供多个连接点,分摊管理消息传递和事件存储的工作负载。所有为特定上下文提供服务的节点都维护一个完整的副本,使用了一个控制分布式事务的“上下文首领”。首领由选举产生,遵循RAFT协议。在本文中,我们不会深入讨论 RAFT 的细节及其工作原理,但我们要知道选举会导致一个很重要的结果:节点需要能够赢得选举,或者至少能够获得多数节点的支持。因此,尽管 Axon 服务器集群不一定要有奇数个节点,但每个单独的上下文需要,以便排除在选举中出现平局的可能性。这也适用于内部上下文“_admin”,管理节点使用这个上下文来存储集群结构数据。因此,大多数集群的节点数都是奇数,只要大多数节点(对于某个特定上下文)能够做出响应和存储事件,集群就会一直是可用的。
Axon 服务器集群
Axon 服务器群集中的节点在一个上下文中可以有不同的角色:
“PRIMARY”节点功能齐全(且具有投票权)。上下文要对客户端应用程序可用,需要大多数这样的节点。
“MESSAGING_ONLY”节点不提供事件存储,并且(因为它不参与事务)没有投票权。
“ACTIVE_BACKUP”节点提供事件存储,并且有投票权,但它不提供消息传递服务,所以客户端连接不到它。请注意,如果要保证拥有最新的备份,必须至少启动一个活动的备份节点。
“PASSIVE_BACKUP”提供事件存储,但不参与事务或选举,也不提供消息传递服务。不管它是在线还是离线,都不会影响上下文的可用性。如果因为维护要离线,在重新上线后首领会将累积的事件重新发送给它。
从备份策略的角度来看,活动备份可用于保留异地副本,该副本始终是最新的。如果你有两个活动的备份节点,就可以在其中一个节点上停止 Axon 服务器,对事件存储文件进行备份,而另一个节点继续接收更新。被动备份节点提供了另一种策略,上下文首领可用异步发送更新。虽然这不能保证始终保持最新状态,但事件最终会到达,即使是使用单个备份实例,也可以关闭 Axon 服务器进行文件备份,而不会影响群集的可用性。当它重新在线时,首领就会立即开始发送新数据。
支持多个上下文和不同角色(每个节点可单独设置角色)的结果是这些个体节点提供给客户端应用程序的服务可能有很大的不同。在这种情况下,增加节点数不会对所有的上下文产生相同的影响:尽管消息传递负载由支持上下文的所有节点分摊,但事件存储必须将数据分发到其他节点,并且大多数节点需要在客户端继续发送事件之前对事件进行确认。需要注意的是,“ACTIVE_BACKUP”和“PASSIVE_BACKUP”角色具有(RAFT)特定的含义,即使它们的名字看起来与高可用性不太有关系。通常,修改 Axon 服务器节点的角色不仅仅是为了解决可用性问题。只要上下文中有大多数节点可用,群集就可以继续运行,但如果“_admin”上下文丢失了大多数节点,那么群集配置的变更也无法被提交。
如果是在本地运行集群,我们需要补充一些“公共”属性,其中最重要的是与集群初始化有关的属性:当节点启动时,它并不知道自己是否将成为新集群的核心或者自己是否会加入到现有的集群。因此,如果在启动 Axon 服务器企业版后立即连接客户端应用程序,将会收到一条错误消息,说群集还未初始化。如果你只想将所有节点注册为“ PRIMARY”,则可以添加 autocluster 属性:
添加完这些属性后,主机名和群集内部端口与“first”匹配的节点(当然未指定默认端口 8224)将在必要时初始化“default”和“_admin”上下文。其他节点将使用指定的主机名和端口向群集注册自己,并请求将自己添加到给定的上下文中。在单主机上启动多节点群集的典型解决方案是使用端口属性,第二个节点将使用:
第三个节点可以使用 8026、8126 和 8226。在以后的文章中,我们将介绍 Docker 部署,并自定义主机名,用于集群内部通信。
UI 和客户端的访问控制
关于启用访问控制,可能有一些东西需要解释一下,尤其是从客户端的角度来看。之前提到过,客户端应用程序在连接到 Axon 服务器时必须提供令牌。令牌用于 HTTP 和 gRPC 连接,并且 Axon 服务器为此使用了一个叫作“AxonIQ-Access-Token”的自定义 HTTP 标头。在标准版中,两种连接类型都有一个令牌,而企业版则维护了一个应用程序列表,并为每个连接生成 UUID 作为令牌。群集内部端口使用的是另一个令牌,需要在属性文件中配置“axoniq.axonserver.internal-token”属性。
另一种身份验证是使用用户名和密码(适用于 HTTP 端口)。对于 UI,它会显示登录界面。它也可以用于基于 BASIC 身份验证的 REST 调用:
CLI 也是一种客户端应用程序,只不过它只能调用 REST API。如前所述,启用访问控制后,你可以使用令牌连接服务器,但如果连接的是企业版,你会发现这条通路被关闭了。原因是单个系统范围的令牌被替换成了应用程序专用的令牌。实际上,CLI 也需要一个令牌,只是它现在变成节点本地的令牌,由 Axon 服务器生成,保存在“security/.token”文件(相对于节点当前工作目录)中。在系列文章的第二部分,我们将介绍 Docker 和 Kubernetes 部署,并为它添加秘钥。
结论
至此,本系列关于如何运行 Axon 服务器的部分结束了。在第二部分中,我们将转向 Docker、docker-compose 和 Kubernetes,并一起体验卷管理的差异所带来的乐趣。下次见!
作者简介:
Bert Laverman 是 AxonIQ 的高级软件架构师和开发者,拥有超过 25 年的经验,最近几年主要专注 Java。他是互联网访问基金会(Internet Access Foundation)的联合创始人,该基金会是一个非营利性组织,旨在促进荷兰北部和东部访问互联网。他在 90 年代成为开发者,后从事软件架构工作,并担任过保险公司的战略顾问和企业架构师。现在,他在 AxonIQ(Axon 框架和 Axon 服务器背后的初创公司)工作,致力于产品开发,重点是软件架构和 DevOps,并为客户提供帮助。
原文链接:
Running Axon Server - CQRS and Event Sourcing in Java
评论