黑客新闻上最近CQRS的讨论和实践经验分享


自2017年以来,我一直在使用CQRS模式。它不是一个完整的系统模式,只是一个起点。您需要将系统设计为乐高积木式的。
那还需要什么?下面是清单:
1)内部设计指南/规则/法律制度,以便每个人都能理解和遵守。即“ C”命令服务不应与其他命令服务通讯,而只能与查询服务交互通讯。但是查询服务能与其他查询服务对话,您还需要服务网格和服务发现,有点复杂。
2)接收器或聚合器。如果让查询服务侦听来自消息总线的事件存储在DB中为查询请求提供服务,效率并不高。最好有专用的接收器服务,其中将数据以一定级别的流式传输到数据库。
3)规则引擎或工作流系统。由于命令服务之间无法相互通讯,因此您需要在它们之间协调动作,这就是工作流系统设计的目的。它们连接到查询和命令服务,并确保完成冗长或复杂的任务而无需做任何棘手的工作!
4)理智!使用CQRS + Sink + Workflows的那一刻,将会很容易感到不知所措。从一小部分基于MACRO的微服务开始,将所有命令打包到一个服务中,用于查询和接收,并且随着工作量的增长和您需要可伸缩性而逐渐分解为多个较小的微服务。这样,您需要管理约4个微服务,如果需要API网关,则只有5个。
5)使用所有服务以及可能的技术,您需要一种合同方式进行通讯。您需要一个以CODE为DOC的系统!您需要Protobuf或类似的东西来设计您的架构和api,由于采取probubuf,您需要使用GRPC ...
 
我认为最重要的“补充”是:实际上有多少公司受益于这种复杂性的业务模型和架构结构?实际上,确实有一些企业和大型团队从经过深思熟虑的架构(例如您所描述的架构)中受益匪浅。但是,大多数互联网企业只需要在两个或多个可用性区域中部署单个服务,即可与具有一个或多个热备份的SQL数据库进行通信。上面这种复杂基本体系结构容易出现问题停止工作,您需要解决很多问题。
 

我与他人共同创立了一家初创公司(https://batch.sh),专门致力于解决事件驱动空间中的问题-我们进行事件归档,搜索和重播。
您完全正确-仅CQRS模式是不够的。以我的经验,大多数走CQRS路径的架构也倾向于使用其他事件驱动模式,例如事件源/事件溯源/事后溯源。
与大多数情况一样,存在优缺点,最明显的缺点是复杂性增加,以换取更高的可靠性和规模。
您的建议很棒,尤其是决定采用结构化的消息格式(例如protobuf)时。我会100%避免使用JSON模式,因为人们可能会忘记填写字段。到目前为止,Protobuf拥有出色的支持,并且有大量的支持工具。此时,我很难选择Avro(除非您是Java或Kafka商店,已经在其生态系统中拥有了它)。
另外,如果您已经在使用CQRS,我建议您考虑完全接受事件驱动-通过使用某种事件总线(RabbitMQ,kafka,eventbridge),可以使消息传递完全异步(并避免使用gRPC) ,正如您提到的,这是另一层复杂性)。
我的一个好朋友曾经说过,为了成功地实现事件驱动,您必须保持良好的状态并保持最终的一致性,我认为这就是这一点的核心。如果您对最终的一致性表示满意,那么您将了解所带来的复杂性负担。
 
不幸的是,使消息总线成为真理的唯一来源并不是那么容易甚至是令人恐惧。Kafka是一个昂贵的家庭亲戚,您最好确保自己拥有的配置接近完美,否则仍然需要一些专家来帮助您正确处理流数据。
Nats Steaming是Kafka的不错替代品,我个人使用并推荐它,但在大规模事件上并不理想,例如规模有每天数十亿次事件。作为Nats Streaming的继任者,Jetstream即将面世,但它还处于预览阶段,需要再过一年左右的时间才能获得产品使用资格。
 
接收器或聚合器是一种有效地将聚合状态(将其存储在数据库中)与实际查询(仅查询数据库)分离的方式。这样,您可以随时停止聚合/接收器,并从上次中断的地方恢复。我们已经在PostgreSQL之上实现了事件存储。我不喜欢使用Kafka作为事件存储。
  
我对CQRS看到的最大缺点(尤其是在事件驱动的体系结构中)已增加了复杂性,通常是为了增加可伸缩性。我是“做可能可行的最简单的事情”的忠实拥护者,因此除某些特殊情况外,我大多反对CQRS。应用程序的CRUD设计可以很好地扩展,以使大多数企业获得可观的收入数字。当您开始遇到限制时,便可以开始重构。在此之前,CRUD应用程序的简单性可以让您更快地进行原型设计和交付,并显着增加新开发人员的入职时间。
  
一个经过微调后正常运行的数据库可以处理多少工作量。在我的上一份工作,是根据CQRSish设计的,引入了不必要的复杂性或低效率降低了许多事情的速度。人们通常会强调“我们需要扩展,这将使其更快”,而真正的答案通常是“我们为何专注于一些基础架构,只是使我们的代码更高效?”。
 
我看不到CQRS是如何增加复杂性的。毕竟,CQRS归结为将查询/读取/不可变操作和命令/写入/可变操作的接口分开。除非您不拘泥于诸如事件源之类的不相关概念,否则CQRS相比通常的CRUD应用程序并不是增加复杂性的重要来源。您能否阐明使用CQRS的哪些方面导致更高的复杂性?
 
我认为问题确实在于,当人们谈论CQRS时,他们几乎总是在谈论事件源+ CQRS,这就是复杂性所在。
我的意思是,请看一下GraphQL,从本质上来说,实现“普通” CQRS并不容易,因为对于状态和查询而言,总是存在明确分开的操作,并且为这些操作提供单独的对象定义很容易(这与许多REST形成直接对比架构,您的动词-GET,PUT,POST,DELETE等-基本上都作用于相同的对象)。
但是我想这些天人们谈论CQRS时,实际上是在谈论底层数据不同(例如,写操作日志与可查询对象的存储库),这通常意味着诸如事件源之类的东西,并且有了该模型涉及大量的复杂性,而且由于许多更新操作还涉及查询,您也会遇到事务实现的困难。
 
正确,这就是我的意思。即使它们在技术上是不同的概念,但在实践中我发现它们几乎总是耦合在一起的。这些天来,我通常在我的API中使用GraphQL而不是REST,GraphQL也能让我们在分离查询和状态上获得的一些优势。但是正如您所说,除非绝对不可避免,否则我最终希望将信息最终以一种一致的格式存储在数据库中。有时这是不可避免的,但是就像文章中提到的那样,将“报告数据库”与“主数据”数据库分开可以经常摆脱。
一旦您开始将数据分散到多个数据库中并将其存储在由具有不同API的不同应用程序管理的不同结构中,突然达到最终的一致性就会迅速成为一个不小的问题,通常可以通过增加更多的复杂性来解决或缓解该问题。
有时候,确实需要这种复杂性,因为一个项目上的规模庞大、庞大/庞大的工程团队或独特的业务需求-但我的原始观点是,在大多数情况下,尤其是新产品/项目不会拥有1000万+每天发布的用户都应该保持简单,直到您知道它需要完整的CQRS +事件源系统的复杂性为止。
 
CQRS一个明显的缺点是样板代码和多个文件的数量。尽管本质可能不再复杂,但是文件之间的“杂音”和碎片数量会增加精神负担。
 
我已经在小型企业中开发了类似CQRS的系统已有一段时间了。我还一直在借鉴域驱动设计(DDD)原理。需要花更多的时间进行实验才能找到如何最好地将其应用于较小规模的软件,但是结果却非常令人满意。
我首先草拟了一个计划,该计划将我如何严格地将DDD和CQRS应用于问题领域,但知道这可能会过于复杂。然后我放弃了我认为对我的特定情况不必要的组件。
结果是重量相当轻,没有太多样板,并且非常可靠。有关客户肯定已经发现它是核心业务流程中不可或缺的一部分。
对此的需求并非来自任何性能要求,而是因为我认为必须有一种明智的方式来构建SME商业软件。尤其是在业务需求和流程经常变化的创业起步环境中。我觉得DDD允许我实际在软件中对业务流程进行建模,而不是要求业务需要适应我正在开发的软件。
对于企业规模的开发人员来说,这可能是个老新闻,但是作为自由职业者,来自较小企业的一族,这是一件大事。
作为此过程的一部分,我开发了一个Python库来简化编写事件/ RPCed系统(事件源或其他事件),并编写了一些非常粗糙的体系结构技巧作为文档的一部分。它们的水平很高,但可能是一个有趣的(有点自以为是)起点。
[1]: https://lightbus.org
[2]: https://lightbus.org/dev/explanation/architecture-tips/

 
一直在研究CQRS,人们的想法/示例/实现都不相同,权威答案来自Greg Young的文章
原来的服务:

CustomerService

void MakeCustomerPreferred(CustomerId)

Customer GetCustomer(CustomerId)

CustomerSet GetCustomersWithName(Name)

CustomerSet GetPreferredCustomers()

void ChangeCustomerLocale(CustomerId, NewLocale)

void CreateCustomer(Customer)

void EditCustomerDetails(CustomerDetails)

使用CQRS 读写分离以后:

---------

CustomerWriteService

void MakeCustomerPreferred(CustomerId)

void ChangeCustomerLocale(CustomerId, NewLocale)

void CreateCustomer(Customer)

void EditCustomerDetails(CustomerDetails)

---------

CustomerReadService

Customer GetCustomer(CustomerId)

CustomerSet GetCustomersWithName(Name)

CustomerSet GetPreferredCustomers()

---------

 就是这样,没有任务/中介架构,没有事件源,什么也没有。
 
使用CQRS读写分离,这在GraphQL中非常容易实现,因为GraphQL明确地将查询与状态分开(这是我最喜欢的主题博客, https://www.apollographql.com/blog/designing-graphql-mutatio ...),但是由于某种原因,当他们像这样实现时,没有人将其称为CQRS。实际上,在过去的5年中,我只听说过在也使用事件源的体系结构中使用CQRS的情况。
 
在工作中(咨询/开发店),几年前我们走的很远。结论:
1.如果认真执行CQRS + ES,效果很好,这需要纪律。我们最终创建了自己的框架(ugh),以便轻松地走上正确的道路。
2.当然,需要权衡取舍。
3.您只是一个精明的敏捷思考者,可以远离规程并创建真正棘手的复杂性和更多的负面权衡。
4.在我们正在研究的典型企业业务解决方案中,对这种方法的市场需求很小。尽管据我了解,某些金融服务领域出现了一些需求高峰。
(奇怪的是……我们有金融服务客户,但不在需要CQRS的特定领域内。)
 
几年前,在我的老东家公司中,我们也走了这条路(CQRS + ES)。实现它很有趣,但是确实增加了一些复杂性。当然,它具有很好的可扩展性。
有趣的是,我们在企业消息传递应用程序中使用了它,尽管在发布它之前我已经离开,我不知道今天的代码看起来如何。
 
以我的经验,很多银行业务,一些交易。在这两种情况下,它都用于历史记录,审计和精度。
这个想法是,对于银行业来说,仅获得当前状态是不够的-更重要的是某人如何达到该状态。
将历史记录添加到事务中并不是什么新鲜事物-因此,您不必花很多时间在历史记录/审计机制上,而是将两者都淘汰了-更高的弹性,分布式系统+内置的审计/历史记录机制。
 
尽管有这些好处,但您在使用CQRS时应非常谨慎。许多信息系统都非常适合以读取信息的方式进行更新的信息库概念,将CQRS添加到这样的系统可能会增加相当大的复杂性。我当然已经看到过这样的情况,它极大地拖累了生产力,甚至在一个有能力的团队的手中,也给项目增加了不必要的风险。因此,尽管CQRS是在工具箱中很好的一种模式,但是请注意,很难很好地使用它。
 
CQRS是一个很好的模式。但是,就像任何模式主义者一样,不应将其应用于整个系统。
imo,它不适合作为系统体系结构。它更多的是子系统设计,您可以将其应用于具有许多写操作和最终一致的读存储的对象,这些存储当然可以从写存储中构建。
无需添加事件源就可以轻松完成此操作,并且两者本身并不是需要并存。
关键是您有时无法基于不断写入系统的数据来构建“足够快”的视图。因此,在CQRS设计中具有处理查询的系统对于基于此设计非常重要。
我将其视为一种模式,因为几乎不可能使查询执行大量的命令/写入操作,因此它确实可以帮助您执行读取/查询负载
 
最近在F#和postgres中实现了CQRS + ES设计模式。查询端由于业务需要而不断变化,而命令端则保持不变。
在我们的例子中,查询状态又称为投影是异步的,这使得它非常灵活和快速地使用。投影可以存在于Web服务器,Redis或数据库中的任何位置。
 
我目前是使用CQRS + ES的系统的贡献者。我们是一家金融科技公司,采用它所带来的积极影响要大于负面影响。
我们使用Lagom框架在Akka Actor和相关技术上实施CQRS + ES。Lagom框架和文档中有足够的护栏/指南,我认为不同的部分非常好地结合在一起!
总体而言,系统复杂性与可伸缩性之间的权衡并不太简单。
 
CQRS + ES更多地与事件源有关,而与CQRS无关。该方案最困难的部分是CAP定理,而不是如何将操作分离为命令和查询。
 
为了实现可伸缩性,我已经实现了几次。
实际上,在我不得不使用它的地方,我们最终通过一个物化视图来满足查询部分,该物化视图对数据进行了组织和聚合,从而使查询在几毫秒内而不是数分钟或数十分钟就变得可行。
对于云中的大量数据源而言,这种模式可能非常重要,但对于“本地”应用程序中的较小数据集甚至可能很有用。
就像任何东西一样,如果您尝试将其应用于所有地方,那只是糟糕的体系结构。
 
我喜欢GraphQL如何通过显式为查询和状态提供不同的DTO来为您的公共API普及CQRS(无事件源!)方法的优势。这是执行此操作的一种非常简洁的方法,而不是像Axon + Spring Boot这样的超级笨拙的框架。
 
REST将资源放在首位,并且通常在同一资源上使用不同的动词:通过PUT / POST创建新资源,然后可以获取该资源以读取它,然后通过PUT / PATCH对其进行修改,删除资源以将其删除。
CQRS通常意味着命令(修改系统)的处理方式与查询(检查系统的当前状态)的处理方式不同。
当然,您绝对可以创建一个REST API,其中有一组您可以修改但不能读取的资源(命令资源),以及一组可以读取但不能修改的资源(查询资源)。但这绝对不符合人们对REST API期望的一般想法。
而且,对于那些关心“最终” /“真实” REST的人来说,要使HATEOAS兼容,这样的API将相对困难。
 
举一个更具体的例子,我们有一个Angular应用程序。该应用程序有两个区域,一个区域用户可以使用自由文本搜索记录,而另一个区域用户可以创建或更新记录。我们的真相来源是Postgres数据库,其中所有更新和插入都使用微服务发送。数据使用AWS中的SQS从Postgres流向Elasticsearch。然后,Angular应用程序使用其他微服务查询Elasticsearch。因此,本质上我们的命令域模型与我们的查询域模型是分开的。
 
如何处理分布式事务中的丢失/失败事件?这实际上不是CQRS问题。这更多是围绕您希望如何处理重播事件的系统功能。如果围绕幂等性设计系统,则应该能够重播事件并获得相同的结果。