作者 / 以码为梯
排版 / 以码为梯
文章字数 / 2972
本文是一篇个人觉得比较好的英文博客的译文,希望大家有所收获
事件驱动架构通过引入消息中间件,使其具备了低耦合(decouple),易扩展(scalability)、高弹性(resiliency)的架构特性,这些特性非常适合分布式微服务。
但是在实际情况中,相对于请求-响应这种客户端-服务端架构,想将事件驱动架构用好还是有一定难度的。
在过去的几年里,我们(Wix)已经渐渐地把我们的微服务从请求-响应模式迁移到事件驱动模式。
在迁移的过程中我们遇到了各种各样的问题,其中包括生产事故,代码重写、陡峭的学习曲线等等。针对这些种种问题,我们总结出事件架构中的五大陷阱,本文以一个简单的场景(电商中付款和扣库存流程)为例,介绍这五个陷阱以及每种陷阱的应对方案。
在电商业务中,一旦消费者完成了付款,那么商品库存就需要进行更新,以表示商品已经被某个消费者购买。
但在实际情况中,将支付完成的状态写入数据库和向kafka(其他消息队列)发出“支付完成”事件这两个操作不是原子的,可能出现只有一个操作完成的场景。数据库的不可用或者kafka的不可用可能会导致分布式系统中的不一致,在此例子中,可能造成库存和实际订单的不一致。针对原子性的问题有如下两种解决方案:
弹性生产者(resilient producer)
利用弹性生产者,确保消息最终会被写入kafka。这种方案的缺点是不能保证消息的顺序性,也就是说下游应用接收到的消息不是有序的,中间会有重试的消息。作者采用的是自研的弹性生产者平台(Greyhound)。
Kafka连接器Debezium
采用Debezium连接器确保数据库更新和kafka的生产操作都发生并且保证两者数据一致。Debezium可以捕获到数据库的所有变化(比如通过mysql的binlog)并发出事件消息。
kafka连接器和Debezium数据库连接器保证事件最终被发送到kafka,并保证了消息的顺序性。
事件溯源(Event sourcing)模式在业务操作时将业务操作当成一个事件进行存储,之后通过回放事件可以重构实体的当前状态,事件也可以通过消息队列发送给其他业务系统,生成物化视图用来获取比事件回放更高的查询效率。
事件溯源模式有它本身的好处,包括可靠的审计日志,在任何时间点获取实体状态的能力,一份数据可以构造出多种视图、它的缺点就是相对于更新数据库中一个实体的状态来说事件溯源更为复杂。
事件溯源的缺点
事件溯源的替代方案-CURD+CDC(Change data capture)
将CURD和发送数据变化事件相结合可以降低复杂度,提升灵活性并且在特定场景下依然可以使用CQRS(Command-Query Responsibility Segregation)。
对于大多数场景,服务可以暴露一个简单的接口用于从数据库中获取实体当前的状态。随着规模的增加,并且需要更复杂的查询时,额外发布的数据变更事件可用于创建专门为复杂查询量身定制的自定义物化视图。
为了避免将数据库变化暴露给其他服务,需要在他们之间创建一个耦合层服务,该服务消费CDC(Change data capture) 事件消息并暴露一个变化事件的API。
切换到事件驱动架构后,定位线上问题和跟踪最终用户的请求会变得困难。跟请求-响应模型不一样的是,事件驱动模式没有类似于HTTP和RPC这样特定的链去遵循。另外,调试代码也变得更加困难,因为事件处理代码分布在服务代码中,而不是通过在同一对象/模块中找到的函数定义来顺序跟踪。
在上面的例子中,Orders 服务必须使用来自 3 个不同主题的多个事件,所有这些事件都与同一用户操作相关。
其他的服务也需要从一个或多个主题中消费多个事件。假设我们发现库存不正确,能够调查所有相关的订单处理事件至关重要。否则,将花费很长时间去查看各个服务日志并尝试手动将不同的服务日志拼凑成一个连贯的业务流程。
自动的上下文传递
如果自动为所有的事件加上请求上下文标识,这样就会让过滤出所有用户相关的事件变得容易。在我们的例子中,可以添加requestId和userId两个事件头,这两个属性对调查很有帮助。
当尝试向kafka发送大负载的数据(图片识别场景或者视频分析场景)时,会带来高延时、降低吞吐量和提升内存压力的风险。针对这种场景可以通过压缩(compression)、将负载拆分为块以及将负载存储在对象存储中并只发送引用这三种方案来解决。
压缩(Compression)
Kafka和Pulsar都支持压缩,你可以根据你的消息类型来选择最适合你的压缩类型。在负载比较大的情况(大于5M),压缩50%对保证集群的性能会有帮助。
在kafka层级压缩会比在应用层级压缩更好,因为在kafka层级,消息可以被批量压缩以提升压缩率。
分块(Chunking)
另一个减低节点的压力并应对消息大小限制的方法是将消息拆分为块。Pulsar已经内嵌了分块的功能,kafka只能在应用层进行分块。
在使用分块时,为了让消息消费者可以还原消息,消息发送者需要给消息添加额外的元数据。
对象存储的引用
这种方法将消息村存储到对象存储中(比如S3)并将一个对象引用(通常是一个URL)发给kafka。
在使用对象存储时,需要确保在链接生成之前消息已经完全上传到对象存储中,否则消费者就需要不停地尝试下载。
大多数消息中间件保证了至少发送一次,这就意味着一些消息可能会被发送多次或者被处理多次,这种情况下只有幂等性解决方案才能应对这样的问题。
在上面的例子中,假设因为处理流程错误出现了库存减少的重复消息,那么库存的下降速度就要比真实情况的快。
重复消息的另一个副作用就是可能会造成对第三方系统的重复调用。
幂等性补救策略-修订号(版本)
在对消息进行幂等性处理时可以借用乐观锁的思想。借用乐观锁思想,在做更新之前都要先获取实体的当前修订号(版本号),如果多方试图同时更新实体,那么只有第一个更改的可以成功并且更新修订号,后续的更新操作会因为修订号不匹配导致更新失败。
以上面的这种方式实现幂等性,修订号必须唯一并且跟事件本身有关系,这样才能保证两个不同事件的修订号不会相同,并且保证即使不是并发的情况对同一个修订号的第二次更新也会失败。
虽然kafka提供了仅仅一次(exactly once)消息的配置,但是数据库操作却可能因为某些原因变成重复更新,所以采用幂等性相关解决方案还是必须的。
为了降低风险,迁移到事件驱动架构需要循序渐进地进行。微服务架构支持灵活的选择不同的模式,针对不同的服务,可以采用不同的处理方式,在事件处理流程中也可以包含HTTP/RPC的交互方式,反之亦然。
对于循序渐进的迁移,我强烈推荐CDC模式(Database changes streamed as events),因为这种方式既保证了数据的一致性(陷阱一)也避免了完全切换到事件溯源模式(陷阱二)后带来的复杂性和风险性。CDC 模式仍然允许将请求-回复模式与事件处理模式一起使用。
对于陷阱三的处理(通过事件传递上下文)大大提升了快速寻找问题根本原因的能力。
陷阱四和五的补救措施是针对非常大的有效负载和非幂等这样的更具体的用例,如果不需要,则无需执行建议的更改。
大家有什么看法欢迎评论留言一起探讨[666]
更新时间:2024-09-08
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号