引言
为指导大家更好地使用Akka框架,Lightbend官方放出了一份使用Akka框架搭建购物车微服务的指南—— Akka Platform Guide。本文对应Akka Platform Guide中的相应章节,厘出了用Akka搭建微服务的主要步骤,给出了相应的顺序图,并对数据的封装传递以及部件之间的集成进行了说明。
-
在微服务内部,该指南使用了最新推出的Akka Typed作为实现购物车微服务的核心框架,示范了如何为聚合实现CQRS与Event Sourcing模式。同时,指南还引入了Akka Cluster与Akka Management作为部署和管理节点的工具。
-
在微服务之间,该指南使用了gRPC作为微服务的统一接口,并示范了如何使用Kafka在微服务之间传递消息。
-
在微服务底层,该指南使用PostgreSQL作为数据库支持,并使用Docker配合Kubernetes进行了部署。
除了Akka框架,Lightbend还提供了更便捷的微服务框架Lagom。但由于其封装的内容更多,内部细节更趋复杂,因此使得Lagom的推广相较更差一些。一方面,从Lightbend官方论坛目前传出的声音来看,大家似乎也更喜欢用原生的Akka搭建微服务,而不是Lagom。另一方面,由于Lagom内部同样是使用Akka作为实现CQRS与ES的框架,因此学习这份指南对于上手Lagom也大有裨益。
Create the gRPC Cart service
微服务的数据封装与传递
gRPC默认使用Google开发的Protocol Buffers
作为接口定义语言,来描述服务接口和有效载荷消息结构,指定服务中可以被远程调用的API方法及其参数和返回类型。这些类型均使用ProtoBuf IDL定义在一系列POJO当中。这些POJO的字段,都有一个顺序号,以方便序列化/反序列化。
客户端发出的数据经其内部的Stub(相当于一个代理,gRPC命令行工具也是客户端的一种情形)打包为HTTP请求(严格讲叫ProtoBuf Request),经ProtoBuf序列化为二进制流后发出。Service收到请求后,使用ProtoBuf将二进制流反序列化为POJO,再作为参数in
提交给ServiceImplementation中的API方法。API方法处理完成后,返回Future[out]
,同样经过序列化后以HTTP应答(严格讲叫ProtoBuf Response)的方式返回给Stub,再由Stub解析后交给客户端。
使用gRPC SDK包中的protoc
编译器可以生成客户端需要的Stub类和服务端需要的API模板。
- 在服务端:首先根据
protoc
生成的gRPC类的API接口定义,实现具体的服务类(本例中为ShoppingCartServiceImpl
),然后在Server里进行服务绑定,最后再绑定到特定的地址和端口开启HTTP侦听。 - 在客户端:首先创建一个Stub实例,之后便可象本地调用一般,使用该Stub实例直接调用服务的API方法。
Create the Event Sourced Cart entity
聚合的对外协议
EventSourcedBehavior
是Akka实现CQRS+ES的核心基础,而与之相关的Command与Event构成了聚合与外部打交道的协议本身。有了EventSourcedBehavior,开发者无需自己动手实现传统的Repository、Snapshot和Replay,它们均由框架直接提供支持。开发者需要留心的,是为EventSourcedBehavior提供一个恰当的EntityKey,然后就可以把精力全部放在对命令的响应和对聚合状态的维护上了。
需要注意的是,指南给
ShoppingCart
聚合提供了一个可以返回当前购物车状态的命令Get
,这与CQRS是相悖的。后经Akka团队反馈,这样做的目的一是为了演示并非所有的Command都会产生Event,二是演示在聚合内部如何维护一致性。而按照CQRS模式的要求,聚合的所有状态均应该通过下一步将要提及的Query获得。
Projection for queries
实现最终一致性
Projection是实现CQRS+ES模式下最终一致性的关键,其职责是负责根据Write-Side推送来的事件更新Read-Side的Repository(这是读端的库,不是DDD里的聚合仓库),为之后服务直接查询该Repository提供最新的数据。所以在这一侧的工作主要分为两部分:一是从ReadJournal获取用Envolope包裹好的领域事件,再借助Projection更新Repository,确保数据与Write-Side保持一致。二是从服务获取传递来的Query并直接提交给Repository进行查询,在返回查询结果后即完成对一次Query的响应。
Projection publishing to Kafka
与Kafka的集成
Kafka中的数据交换单位为Record
,主要包括topic
、key
和value
三个字段。负责发送消息的SendProducer
由Akka Kafka定义并提供工厂方法。
首先是从ReadJournal中取出被Envolope
封装好的领域事件,然后据此创建ProducerRecord
。在这个过程中,会先用领域事件的信息创建一个ProtoBuf消息,然后使用bytes = ScalaPBAny.pack(protoMessage, "shopping-cart-service").toByteArray
将消息序列化(pack的第一个参数是消息本身,第二个参数是URL前缀,这样可以使序列化信息的类名称被包含在序列化后的串当中,方便解析),最后交给使用上述工厂方法创建的sendProducer.send()
发出即可。发送是否成功的情况,可以从该方法返回的RecordMetadata
中获取。
而消费端从Kafka获得消息时,数据将被封装在ConsumerRecord
里。它与ProducerRecord
类似,也包括topic
、key
和value
三个字段,还包括partition
、offset
、timestamp
等其他一些字段。要从ConsumerRecord里取出关注的信息,首先需要使用ProtoBuf的ScalaPBAny.parseFrom(record.value)
提取一个Any
对象x
。该对象有三个主要字段:typeUrl
对应事件的名称(本例中为shopping-cart-service/shoppingcart.ItemAdded
),value
对应ProtoBuf序列化的二进制串,unknownFields
对应未能被识别的其他内容。随后,根据typeUrl
匹配的结果,使用对应的析取方法获得定义在ProtoBuf中的结构(本例中为event = proto.ItemAdded.parseFrom(x.value.newCodedInput())
),即可得到最终需要的数据。
Projection calling gRPC service
与其他gRPC服务的集成
由于集成的两端都使用gRPC协议,所以数据使用ProtoBuf IDL定义的结构即可。而调用方则需要在其本地先创建一个被调用端的代理对象(本例中为ShoppingOrderServiceClient
,它实际只有一个apply
方法),就可以作为客户端直接调用其API了。
而调用时的参数ProtoBuf Request则来源于聚合。首先使用eventsByTag()
从ReadJournal中获得被写端推送而来的领域事件。该领域事件将被封装进EventEnvelope[ShoppingCart.Event]
,在具体处理时再使用envelope.event
根据match匹配取出(本例中为ShoppingCart.CheckedOut
)。最后再根据ProtoBuf IDL的定义,创建ProtoBuf Request对应的Request POJO(本例中为proto.OrderRequest
),交给代理对象发出即可。
被调用方收到代理传递来的Request后,在其API内部完成处理,并将结果封装在ProtoBuf IDL定义的ProtoBuf Response里返回(本例中为Future[proto.OrderResponse]
)。
与HTTP后端框架的集成
Spring MVC等常见的HTTP后端框架都采用类似Restul的方式、采取MVC的架构,使用URI定义资源,使用HTTP的谓词GET、PUT、POST、DELETE定义对资源的操作,再由Controller根据路由和HTTP映射指向具体的处理方法。
由于gRPC使用基于Protocol Buffers协议的二进制串传递消息,因此常见的HTTP框架无法直接提交gRPC请求或解析gRPC应答,所以最简单的方法是参考上述与其他gRPC服务的集成中的内容,通过创建一个本地的代理对象来进行集成。
需要在HTTP连接上启用TLS时,首先需要确保JDK为8u252以上版本,以提供ALPN支持(Application-Layer Protocol Negotiation)。然后使用安全证书创建一个HttpConnectionContext,再以该上下文为参数来创建Akka Http实例。而在客户端,则相应在创建Client对象时向GrpcClientSettings中加入TLS支持即可。
.NET平台的微服务生态
最新ASP.NET Core已经支持ProtoBuf的自动转换器,ProtoBuf对应的MIME类型为application/x-protobuf
,这又为我们省了不少事。
参考:.NET上的gRPC
此外,Microsoft官网也推荐了一个微服务框架Dapr,其中的许多思想与Akka是共通的。