×

【中美技术专家分享实录】跨境茶话会——响应式编程的应用

96
魔窗magicwindow
2017.11.28 10:30* 字数 16369

大师兄说

许多场景下为了更迅速的响应客户端的请求,将问题转化为实时反映业务状态的变化,能更好地提升用户体验以及支撑更大量的用户请求,于是催生了响应式编程,本期跨境茶话会仍旧邀请了中美两地的相关专家来谈谈响应式编程的应用。

全网首发·技术干货·大咖对话

整理:魔窗丨 7728字丨15分钟阅读

来源:本文整理自跨境茶话会线上分享,为魔窗(magic-window)原创首发,转载需获得授权

【主持丨大师兄】:这期为什么要选响应式编程这样一个主题?因为前两期我们做了一个性能优化方面的主题和微服务方面的主题。性能优化本质上会解决高并发情况下同步延时的一些问题,但是可能另外有一些业务和架构上的模式,采取一些消息驱动的方式可以解决后端即使不能实时处理相应,也可以低延迟的保证客户端体验的场景。这样的场景就是响应式编程的系统架构。首先介绍一下这期的嘉宾,首先是徐鹏程,他目前任职于谷歌,是负责广告技术方面的专家。


【嘉宾丨徐鹏程】:大家好,我是徐鹏程,现在在上海谷歌。我在谷歌上海做了大概七年的时间,前六年我都是在做广告优化的一些工作。最近这一年我开始做安卓的开发。在谷歌上海之前我还在谷歌北京本部也待过一段时间,中间也出来创过业。


【主持丨大师兄】:接下来我介绍一下童奎松,他现在在Snapchat,负责Snapchat数据方面的工作。

【嘉宾丨童奎松】:我叫童奎松,2002年浙江大学毕业之后,我在国内写了差不多十年的代码,在2012年加入LinkedIn,主要负责和数据分析相关工具的开发。今年年初加入到Snapchat,做数据分析相关工具的开发。


【主持丨大师兄】:接下来我介绍一下徐焱飞,他现在是磐石科技的CEO,磐石科技也是一款投顾金融平台的产品,他本身在一些CQRS,Eveint Sourcing等相关技术方面非常有经验。

【嘉宾丨徐焱飞】:大家好,我是徐焱飞。我最早在Wipro做零售的一些跨国项目。后来加入爱立信上海研究院,一直做通信方面后台的一些工作。之后加入普兰金融在做分布式后台的开发。今年现在开始出来创业,是一个投顾平台。


【主持丨大师兄】:接下来我们进入这期茶话会第一个环节,首先想请各位嘉宾谈一下关于他们在工作中关于响应式编程的实践。

【嘉宾丨徐鹏程】:我在谷歌内部假如现在从事的安卓开发,我们在响应式编程上会有更多的实践,在之前的广告优化的地方,因为我长期是在做后台的工作,我们反而在这方面做的实践相对少一点。但是安卓这边因为涉及到很多前端的交互,所以我们在编程上的实践更多一些,因为应用的场景会更多一些。

【主持人丨大师兄】:能不能举一些你们在具体工作中用到的响应式编程的业务的场景和技术方面实践的经验呢?

【嘉宾丨徐鹏程】:比如我最近在做APP的时候,里面有一个常见的webview控件,他是一个事件驱动型的方式,比如你可以给它加载和定制化一些事件client,比如这个webviewclient上你可以做一些事件处理。但是,这个有一个比较不好的地方就是事件处理client是很难被重用的,因为两个不同的webview上面可能有不同的事件处理方式。但是它的业务逻辑可能80%是类似的,20%是不一样的,但是你没有办法重用这80%的代码,除非你做一个级层的关系,那这样两个完全不相关的webview可能就会存在一个依赖关系。我们并不想要这样的耦合方式,所以我们把wenview的事件完全改造成了observer模式的这种方式。所以每个事件我们都可以把事件相对应的observer提出来,这样的话最终在不同的webview的情况下我们就可以把80%类似的observer组装在一起,通过这样的方式来解耦,使得这个代码能够得到重用,也比较易于管理。

【主持人丨大师兄】:谢谢鹏程,鹏程刚刚从一个客户端的角度帮我们介绍了怎么样通过一些响应式编程,就是事件驱动的方式把真正的事件处理的逻辑给解耦。接下来奎松帮我们介绍一下吧。

【嘉宾丨童奎松】:我最近在做一个项目,Snapchat对user ID其实管得很严的,当时在我们所有处理的数据里面,所有的雇员都可以看到User ID的,所以我们想把它转化成全部用一个叫自动生成的Ghost ID来替代所有的user ID,在我们分析的数据库里面。然后这里我们就要做一个系统,让所有其他第三方系统能通过User ID查到对应的Ghost ID,然后把User ID换掉。它其实就是一个User ID和Ghost ID的对应关系,已经在数据库里面有了。但是因为它的吞吐量非常大,基本上所有的因为是事件都会有user ID,每天的事件基本上在T的量级。所以,为了应付这么大量级的查询,我们在前面做了一个cache。但这就是一个典型的高吞吐,它基本也没有什么CPU预算,基本是高吞吐、高IO的运用,所以我们就想到用响应式编程来解决它。其实我们在这里用响应式编程主要是解决的因为它大部分都是IO,所以我们不想把IO等待来占用CPU的时间。这样的话,同时我们就采用了全译步非阻塞的方式来响应请求,这样基本在单台机器可以达到100K以上的吞吐率。

在这之前,我在LinkedIn的时候,整个系统都是基于事件驱动的,它是基于Kafka架构上的事件驱动的响应式系统,这个对系统来说是一样的,就是所有的编程全是异步。我先不说这个异步编程在整个应约上带来的高吞吐率、低延迟,其实延迟不会低,就是容错率有一个好处,另外带来一个好处,从我们数据分析来说,因为所有业务中的事件,所以你在做业务分析的时候只要基于事件来做,其实你能解决大部分数据分析的问题。这是我在这两个公司用响应式编程的一些经验。

【主持人丨大师兄】:刚刚谈到在LinkedIn上你们所有的数据分析都是用事件,用一些事件无非就是一些状态的改变,按照这样的模式去做,你能举个具体的例子吗?

【嘉宾丨童奎松】:我举个例子,比如最典型的PageView的事件,比如我的任何一个LinkedIn的用户查看我的profile页面,就有一个profile的PageView事件。但是这个Page view 的事件发到Kafka,它会返回我的相关信息,然后生成Profile页面。同时这个事件因为它是发的Kafka的,所以Kafka可以把它存到HDFS上,然后我们做数据分析的时候只要从HDFS把这个PageView的事件拉出来,我们就能算出当天的每个页面的PageView是多少。

【主持人丨大师兄】:你们如何对这种事件做一些抽象的?因为可能你们定义了一个具体的状态,但是真正处理这个状态变化的不仅仅一个业务模块,还会有一些其他业务上的模块会针对这个状态变化做处理。

【嘉宾丨童奎松】:我们在整体设计的时候专门有一个committee来统一地审查这个事件,也就是一个团队你可以说我要这个事件那个事件,committee会去审核你这个事件是不是已经存在了,和以前的事件是可以重用的,如果不可以单独的话,那你的事件里面需要哪些属性。你要兼顾到业务需要和数据分析需要。因为事件进了Kafka之后,Kafka可以分发到到处,每个系统只要监听同样的事件,它能做不同的业务。

【主持人丨大师兄】:你还有什么要分享的吗?

【嘉宾丨童奎松】:这里有一个事情我要澄清的,从我自己的理解来说,我们这期的分享题目叫响应式编程,但是配图其实从我的角度来说是响应式系统的图,其实它是整个系统。所以我会发一个链接:https://www.oreilly.com/ideas/reactive-programming-vs-reactive-systems,这个链接里面有详细的人家写的响应式编程和响应式系统的区别,在我的理解来说,响应式编程基本上属于你在一个process里面怎么用异步来高效地使用你的线程,也就是让你的线程在你的任务之间切换会很少,你的CPU切换很少。响应式系统就涉及到你怎么去用事件驱动或者消息驱动来构造你的系统,你的系统需要很好的用户响应,还要容错,要有弹性,在高负载情况下也要有弹性。所以,从我的理解上响应式编程和响应式系统其实是两个概念。

【主持人丨大师兄】:其实响应式编程是包含在响应式系统里面的?

【嘉宾丨童奎松】:对。

【主持人丨大师兄】:我这边也说一下,这次我们整个的主题其实应该是响应式系统,而不是响应式编程,可能名字上会有有点误导大家。接下来徐焱飞你这边介绍一下吧。

【嘉宾丨徐焱飞】:好的。我非常同意刚才奎松说的关于响应式编程和响应式系统的区别。就在本次交流前,刚和申竣线下沟通了一下,因为这个地方确实是要澄清一下。我之前的工作经历,有一个项目其实和奎松有点接近,也是用全异步式的用Kafka将所有的请求聚合起来,然后用storm对流数据进行处理,一方面是做一些大数据的分析,一方面是把这些数据进行落地。我介绍一下基于事件驱动的CQRS架构一般所适合的场景。比如我们做传统的系统当中,拿电商举例,如果我现在发了一个订单出来,这个订单落地,如果我们只关心订单的变化,比如订单的数量发生变化,那是一个非常简单的系统,常规的我们用CURD的系统就可以解决。但是现在这个电子商务的时代,可能会遇到两个问题,第一个问题是高并发,且读大于写。如果我们这个时候往数据库做简单的CURD的时候可能就承受不了。这个时候就得引入各种缓存、读写分离、分库分表之类的技术,CQRS其实算是从应用级开始的一种读写分离。第二个问题,是需求变化的太迅速以至于我们难以推测系统在后面会如何演进。比如我们现在最开始只是订单,那么后面可能对这个订单数据有一个延伸,比如我去减库存,或者从这个订单产生的后面有支付,红包等等,可能会延伸出更多的业务流程。在这种时候按照传统的CURD的系统,我们函数式的往上去拼接各种代码,无意就显得效率比较低。除了采用微服务外,使用CQRS用类似事件流处理的方式去实现,可以避免业务系统间相互影响,相互隔离的同时,也能为未来的各种扩展做好准备。所以我现在最新的项目就是采用基于CQRS,加上EventSourcing,对整个的系统一个是满足读大于写的高并发,第二个是满足我们未来对系统的快速调整和演进。

【主持人丨大师兄】:感谢各位嘉宾介绍了在工作当中的一些响应式系统的经验。接下来第二个环节我们会讨论一些问题,这些问题都是在做响应式系统当中会出现的比较细节的业务或者是技术方面的具体的问题。为什么会出现问题呢?可能你在做一些同步的方式当中是不会出现,做一些事件驱动,包括像高并发,异步处理就会衍生出这些问题。我的问题是在一个响应式系统中,可能你整个的端到端的业务场景中间会经过很多的事件处理的环节,如果某一个环节出了问题,我想问一下各位嘉宾,如何去做一些事务的回滚,不知道各位嘉宾在这方面有没有相关的一些经验。

【嘉宾丨童奎松】:我先回答吧,当你做整个反应式系统的设计的时候,其实大部分情况下你应该结合DDD,领域设计驱动来做你的设计。你的数据其实在某种情况下是有边界的,就刚刚你说的,如果有一致性要求的话,事务应该是在某一个模块或者某一个服务的范围之内,它不会跨多个渠道,就是分布式事务,这是在反应式编程里面忌讳的东西,是不应该采用的。所以在单个叫application也好,或者叫服务也好,在单个服务里面你可以用你的事务来保证你的数据的一致性。因为不管是任何数据你总会有一个拥有者,比如说订单系统,所有和订单相关的信息都会属于这个订单系统。所以当你对订单做任何变更的时候,它的一致性是由订单系统处理的。当订单下单之后他要去预留库存的时候,这时候他会去发个消息给库存系统,然后由库存系统去预留这个库存。所以刚刚你说的,从我的理解来说,它的跨系统之间的事务失败,这种情况是不存在的。

【主持人丨大师兄】:你的意思是说其实从一些业务设计的角度去讲,没有一个端到端的事务失败的概念?

【嘉宾丨童奎松】:当你做领域驱动设计来说,每个系统就是独立性的,是孤立的,没有端对端。比如端对端只是你整个订单系统或者单独库存系统,但没有说我一个订单下去到保留库存,到保留库存,到付款,到交货,这是整个端对端的失败。

【主持人丨徐鹏程】:我能请教一个问题吗?刚刚您说的订单系统里面,比如我在生成订单的过程中需要减少库存,对于库存系统来说那就是一个完整的事务,因为这个库存的事务是发生在订单操作的过程中的,如果最终这个订单失败了,那么库存那边的事务是不是要回滚呢?

【嘉宾丨童奎松】:这看你怎么设计,我的设计方法是这样的,下订单的时候其实你不需要预留库存,为什么呢?因为从某种角度上来说,整个反应式系统里面最重要的一个概念是最终一致,而不是你要实时一致。也就是说即使我下定单的时候没有库存又会怎么样呢?最多我下了订单之后我再去发个消息给库存系统说你给我留一个库存,这时候库存系统如果没有库存的话他会返回一个比如说我没有库存,那订单系统读到这个没有库存的事件之后就会把自己状态改成backordered或者别的状态。也就是说下单的过程中我是不需要和库存有这种分布式事务的交互的。

【嘉宾丨徐鹏程】:如果我这个订单除了库存之外还依赖其他的角色,比如依赖于付款,比方库存说我有,然后付款说最终失败了,那么你还是需要再去让那个库存恢复过来是吗?中间这个状态怎么处理?

【嘉宾丨童奎松】:状态表示一致是最终的一致性,它是最终的一次性,不是我这样一个时刻下全部表现持续。比如你刚刚说的,我库存已经留了,我现在去付款,然后付款失败了,然后就会有一个付款失败的消息发出来。这个时候库存和订单都会监听这个付款失败的消息,库存监听到付款失败的消息,库存回滚,订单监听到付款失败的消息订单的状态变成付款失败。

【嘉宾丨徐焱飞】:这里我来说几句吧。关于这个地方,首先需要明白一个问题,为什么会有分布式事务这个问题。如果说我们没有分布式,传统的比如下订单,到保留库存,到支付的流程,常规的理解肯定是一个事务。后面一个环节必须等到前面一个环节成功,那才能进行到下一步。比如说如果下订单的时候这个商品本来就没有库存了,那这时不可能就支付它,买家不可能去买不存在的东西。但是因为压力的东西,我们要到分布式上,一旦产生了分布式,状态就会落在不同的节点上,这个时候问题就变得复杂了。

这就是我们业界内通常来说的CAP理论,所谓的CAP理论这里稍微普及一下,它是指分布式系统的一致性、分布性和容错性,这三方面是不可能满足三者的,最多只能满足其中两者。所以一般业界内在分布式事务上有两种做法,第一种是TPC,两段式提交,就是淘宝之前最常用的做法,比如这三个阶段每个阶段都尝试着向数据库做一次写入,先各自尝试把当前资源给预留下来。当三个都OK了,当然这中间是有另外一个第三方的协调者,比如像JBoss他们做JTA这样的事务实现的时候就会有一个第三方的监听,保证在三个阶段的提交都肯定成功了,他再做第二次的提交,把三个对应的资源真正的commit掉,从而最终同步更新状态,这就是两阶段提交大致的过程。

还有一个做法就是刚刚奎松说的最终一次性,最终一次性在DDD领域设计当中,通常来讲我们是这样做的。整个大的流程这一个环节,我们常规的理解是叫做一个事务。但是,因为我们把它分成比如这三个服务的话可能会落在三个独立的领域、独立的服务当中去处理,就像现在微服务可能就变成了三个服务的节点,每个服务节点各自的处理过程是一个事务,比如下一个订单,到数据库保存成功,那这个事务就算成功了。

那整个流程怎么保证呢?在领域当中我们有一个概念叫Saga,Saga可以用来保证整个流程是按照设计好的流程走,比如下订单是成功的,但是去保存库存的时候可能失败了,这个时候就可以发出来一个库存失败的消息,其他的几个节点就可以监听到这个消息以做出不同反应,例如订单服务就可以将订单状态改为下单失败。

说到这里不得不再啰嗦几句,关于领域设计当中我们一般说到聚合状态的更新,其实已经牵扯到了Actor模型了。在领域设计当中,我们把具有相关联关系的一些数据单元用Aggregate聚合来表示,如果说要找一个类比便于理解的话,就类似于我们OO编程中的一个大对象。比如订单,常规我们去更新订单的时候,在传统的系统当中我们可以前面发起一个函数调用去更新这个订单的状态。但是在领域当中,比如Actor模型中,它其实是通过一个对外的事件接收器,外面的人不能够直接更改聚合内部的状态,必须是通过事件,你发一个事件告诉我你要干什么,然后至于怎么干是整个聚合根内部自己来做的一件事,这就是一个Actor模型。

所以刚才提到了在整个过程当中的异常处理体系,这当中的整个异常处理体系其实分两部分的,一部分是聚合根内部的体系,这个时候它的异常我们可以用常规的手段来解决,比如如果发生了异常也好,或者写入失败也好,这个时候我们可以发出一些事件发给外部,交给外部一些对这个事件有兴趣的订阅者。

还有一种是我们整个事务的,整个体系的,像刚刚那个流程从创建订单到保留库存,到支付,整个环节其实是靠聚合之外的,比如Saga的这种方式来进行保证的。

【主持人丨大师兄】:我这边总结一下,徐焱飞和奎松说的,我如果在领域和领域之间其实没有所谓事务的概念,所有的东西都是基于状态变化的处理,整个领域只是负责接收外部领域的消息,做一个相应的处理。

【嘉宾丨徐鹏程】:对的。其实我这边还有一种情况就是我之前做系统的时候我们有一个索引系统,就是我们对存储的数据做了一个索引,那个东西也是事件触发的,但是那个东西我们认为是一个比较弱的辅助系统,所以我们不强行保证它的最终一致性。如果更新失败了那也就随他去了,因为对于我们整个系统来讲不是必须要求。所以这种情况下我们通常的做法,如果事件处理的时候失败了那就不去管它。但是我们另外还做了一个offline的re-indexer,也就是每天它会重新把所有的数据重新索引一遍,保证它在一定时间之后数据还是正确的。

【嘉宾丨童奎松】:对,这个就涉及到当你的领域系统里面处理一个消息的时候如果失败了怎么处理,失败了其实有各种方式,一种就像你说的我直接把消息丢掉,因为我失败就失败了,我不在意这个。比如说去写log,我如果log写不进去,我失败了就把它丢掉。另外一种就是我重试,我会定期重试,或者各种算法定期重试,或者我有多台机器的话我换一台重试。第三种就是刚才你提到的,我可能不重试,我也不把它丢掉,我把它放到一个错误消息的队列里面,定时比如每天或者每小时,定时对错误队列里的消息再重试。

【主持人丨大师兄】:刚刚的问题是关于事务的,我看大家把异常处理的问题也讨论了,和事务是相关联的。所以接下来的问题是响应式系统是建立在事件驱动之上的,那这个问题就是当事件越来越多, 公司的系统越来越大的时候,由于公司的组织架构,可能我研发一个工程师或者一个小工程的团队只是负责两三个事件的处理,就是两三个领域,两三个事件的处理。但是可能作为我整体的系统架构或者是大的产品经理,很可能面对的是整个事件处理的流程有成百上千个状态。其实,我作为更大的一个架构层面或者产品层面的角度,可能有架构师需要了解所有的成千上百的事件处理的状态过程到底是怎么样的。我在设计的时候,或者编写代码的时候,整个在存在成千上百个事件的过程中,我怎么样了解到整个事件处理的全貌,使我当中要做一些修改的时候不至于发生任何的问题。这个我想请各位嘉宾发表一下自己的意见,怎么样管理这么多的事件?

【嘉宾丨徐鹏程】:首先,你刚才提到了,如果你是一个复杂系统的话,不是说一定,但是百分之八九十一定推荐你要用领域驱动设计。当你做领域驱动设计的时候你会把整个大的系统分成不同的领域,比如分成十个领域或者八个领域,简单一点来说就是我们刚才说的网上订单你会分成订单、库存、付款三个领域。你的业务是有流程的,比如下订单你是先下订单再去保留库存,再付款,再发货。

但是我们刚刚也提到另外一个事,你的领域之间的交互只能通过消息来处理。你把这个结合起来,你的流程就是你的消息,这样的话当你把你的领域画个图,然后它们之间的连接就是消息,这个消息是决定你的流程的。比如我作为订单的流程第一步是下单,在订单系统里面发生,然后订单会发一个事件说下单成功,这个时候库存会监听到下单成功这个消息,然后再做保留库存的动作,然后保留库存成功的时候会发一个消息说库存保留成功,然后再是付款系统。然后付款会有成功和失败两个不同消息。如果成功了之后会进入发货系统,回到库存之后发货。

所以,刚才你说怎么去确定我有哪些消息,我哪些消息是必要的,哪些消息是不必要的,哪些消息是重复的。这其实就由设计的时候你的业务流程决定,因为消息就是你的业务流程当中的一步,你没有这个消息,你业务流程走不通。当你的业务流程不需要步骤的话你也应该不存在有单独的消息。这是我的理解。

【主持人丨大师兄】:我这边延伸到了一个问题,刚刚讲到可能从业务上来讲我是需要做这样一个设计,可能就是画一个比较大的流程图,或者是用文档的方式把他们画下来。但是有一个问题是在于,在你真正做一个开发的时候,可能分好几个阶段,你在开发的时候需不需要我这边全局地去维护一张比如XML描述文件也好,这样一个存在我所有的一个消息处理,所有一个工作流的文件,让我整个工程师能够去到单一的文件上去找相应的流程步骤,第一个是开发过程当中。第二个是我整个系统上来以后有没有一些比较好的监控的手段,能够让我清晰地看到现在每一个事件处理,每一个领域每一个事件处理的状态会有没有什么异常,我是不是需要进行一些人工的干预?

【嘉宾丨童奎松】:第一个是不是要一个中心,一个地方放所有的事件,这个我个人是不建议的。因为每个模块是独立的,每个模块负责处理自己的事件,它发哪个事件,比如库存、订单,订单成功之后库存系统会关心,这些事是由每个人自己决定的,而不是我由一个中心的地方来放所有的事件。怎么确定我有哪些事件呢?你要有一个流程图,每个领域之间的交互关系是怎么样的,在你做领域设计的时候,做系统设计的时候你的这个文档是需要的。

第二个就是刚刚说的监控,因为所有的事件其实你会有一个地方去存储的。比如说Kafka,这时候你事件处理的成功与失败完全可以通过监控kafka来确定。如果你的流程设计得好的时候,当某个事件处理失败的时候你会把它放到一个错误队列里面,或者你会有一定的标准说这是执行失败了或者执行成功了,你可以监控kafka里面队列的状态,或者监控你的metric系统来决定。比如我一个事件成功了,我会往我的metric系统里面发一个+1,成功+1或者失败+1,或者尝试+1,这取决于整个系统怎么设计。

【主持人丨大师兄】:我这边理解了,其他两位嘉宾有什么想法?

【嘉宾丨徐鹏程】:首先我觉得你刚刚说的我们可能有成千上万个不同的事务,不同的状态,但是通常来讲是可以分模块的,实际上未必可能真的有那么多状态。因为比方我有一千个状态,但是可能这一千个状态分别属于十个不同的领域,可能有80%是内部的状态,并不一定暴露到外面去,所以你可能最终画的那个图我觉得最终不应该是我这个状态互相的转换关系,而是我这十个领域之间有哪些依赖关系,可能80%是在某个领域之内的。这是第一点。

第二点,其实在我自己看来,因为之前我负责那个项目我们也是可以认为是一个领域,我们其实并不是太关心调用者或者使用者之间的状态,因为这也是他们的事情,我们只关心第一我们内部的状态,第二我们暴露出去服务的状态。所有的东西我们都会有log,我们通常是会知道谁在使用我们某一个接口,最终比方我们需要在技术上改进这个接口,或者需要改接口的参数的时候我们通常会看谁用了这些东西,它是否会跟我们新的接口有问题,我会通知我们的调用者你的这个东西可能需要进行怎么样的修改才能符合我们调用的正确性,可能从什么时候开始我们会改变调用的接口。只有在这个情况下,我们才会对我们上下游的状态有更多的沟通,否则我们只要保证对外的接口不变就可以了,对内的状态和事件我们不认为我们的上下游接口是应该知道这些东西的。

【嘉宾丨童奎松】:这里我补充一点,刚刚谈到的是调用接口,其实这还是原来的http或者rest调用,或者RPC调用这种同步方式,或者说它不是事件驱动的。真正当你事件驱动、消息驱动的时候,其实你消息的内容和消息的类型已经决定了它是要做什么事情。当你做升级的时候其实你要做到一个上下兼容,大部分情况下只是你的消息里面的内容只增不减或者不改,其实你就能做到向下兼容。

【嘉宾丨徐鹏程】:对,但是有具体情况,比如我们碰到的情况这个消息你不会再支持了,你发过来也没有用,我们没有这样的功能了。

【嘉宾丨童奎松】:这个是这样的,你接不接消息取决于你,但是发不发消息取决于我们,我在反应系统里面发消息那一方面为主的,我把我的消息发出去,你收不收,你做什么事情我根本管不了,这可能调用方式不一样。

【嘉宾丨徐鹏程】:实际情况下你通常发一个消息过来,你可能是希望我处理完之后生成一个新的消息出去,但是像这种情况下我们之前会反馈回去,但是现在因为某些原因我们不再有这样的反馈了,其实会影响到你的一些东西。

【嘉宾丨童奎松】:这个就看你怎么设计,从我的角度来说我发消息的时候我根本不关心谁来处理不处理。我关心的是比如下订单那一块,我订单生成之后我有一个订单成功的消息,我根本不在乎处理不处理这个消息,我只等下一个库存预留成功这个消息,我只要等这个消息,我不管你听不听。所以,我决定需要当我做订单系统的时候,我需要关心你付款成功不成功这个消息,你库存成不成功这个消息,我不关心我的订单生成成功这个消息发出去之后到底有多少人去处理,或者说你今天处理,明天不处理。

【嘉宾丨徐鹏程】:但是通常情况是这样的,比如你是订单系统,我是库存系统,你发一个消息给我说要求我预留这个库存,但是比如我从现在开始当机了,我既不发出去一个预留成功的,也不发出去一个预留失败的,那在你那边的流程不就work了吗?

【嘉宾丨童奎松】:首先我的订单成功之后我不是发一个告诉库存你要预留这个事件,我发的是订单成功事件。库存那方是监听我订单成功事件来决定我保留库存成功不成功。就是订单系统本身不知道你库存是要做保留库存还是直接去发货,订单系统不在乎。库存系统拿到订单创建成功这个消息之后他自己来决定我是要保留库存还是直接发货。这个理解我觉得反应式编程要反过来的。

【主持人丨大师兄】:奎松的观点是说,我所有发的消息都是基于状态的变化,不是我告诉你做什么事情,是我告诉你我这边状态发生了哪些变化?

【嘉宾丨童奎松】:对的,我告诉你我做出了哪些变化,或者说我告诉你,这个其实就像他们会计记账一样,会计记账你告诉我的时候你就来报销,你告诉我今天吃饭一百块钱,我不管你吃了什么,不管你怎么吃,我只记录一点,吃饭一百块。

【主持人丨大师兄】:焱飞对这个问题有什么看法吗?

【嘉宾丨徐焱飞】:我非常同意奎松刚刚说的,因为在领域驱动里面其实每个聚合根自己是通过事件驱动的,通常它不会去管其他领域的情况,只管自己。说白了,有什么样的事件进来,然后聚合根内部逻辑根据这个事件进行处理,然后对外发出相应的事件,就是事件传递驱动的过程。但是从系统设计的角度来说,但是像鹏程刚刚讲的,有的时候会遇到宕机,这其实是属于运维级别的东西,我们要对这个信息了解。通常的做法除了做自启外,刚刚奎松也讲了,一个是记log,一个是metics,如果用记log的方式我们一般会在每一个事务的发起点,产生一个全局的ID,这个ID会带在每一个消息中。比如,上面的例子,从创建订单开始的时候就可以产生一个事务级的ID,那这个ID在订单创建成功后随着创建成功的事件发出去,那库存系统在收到这个事件之后依然会把着这个ID带着往下游或者其他的地方发出去。这个时候无论是日志系统也好,还是metrics也好,我们去查找这个事件的时候可以很容易地通过这个唯一的ID,把它上下游所有的事件串起来,这样后台的管理员可以看的到,比如像刚刚说的支付系统宕机了,那这个时候可以很明显的看到,同一ID的事件流会存在大量的更新库存之后,后面就没有支付了。像在SpringCloud全家桶里面有一个SpringMetrics的集成或者SpringBootAdmin里面就有类似的图形界面,可以很清晰地在里面看到每一个流程走到每一个节点的状态,我觉得这个情况下可以快速地发现问题。

【嘉宾丨童奎松】:这个完全同意,原来在Linkin系统的时候也是,在你的业务流程的最开始的步骤会生成一个UUID,这个UUID会带到你整个业务流程的每一步。

【主持人丨大师兄】:各位是谈了一些监测方面的时候可能要有一个像奎松说的UUID,这个UUID负责端到端的业务链过程的监控,这是上线以后的监控过程。但是我顺便引入下一个问题,因为刚刚鹏程说的,实际上你即使保证你每一个领域,每一个事件处理的模块,在自己的范围内都测试通过的情况下,但是作为我整个的测试环节,针对我整个测试工程来讲我也不能保证你有这么大的事件处理业务是完全正确的。所以整个开发管理之后你还是要经过端到端的测试过程的,但是这样的测试过程肯定和传统的方式有很大的区别的,我想问一下各位,在最后整个系统开发完之后你的上线测试期,我想让各位分享一下在测试过程中有什么经验,有什么和传统的方式不同的地方是需要注意或者是需要花时间的。因为即使你各个模块测试充分了,但是从工程上来讲我还是要把整个连在一起的集成做测试的。

【嘉宾丨徐焱飞】:我觉得这个是不冲突的,这个东西已经跟我们说的领域系统也好,或者是事件驱动的系统也好,关系不大。包括我们现在最流行的微服务,也要处理这样的问题。首先把每个节点系统自己的测试做掉,因为这个非常简单,其实只要花时间进去就好了,可以用穷举的方式把所有的事件传进去,看看它的output是不是我们想要的。把这个做掉,然后整个大环节的集成测试其实就跟传统的测试没有什么区别,只要在源头发出一个请求,比如像CQRS当中先发出一个command,然后去query出一个结果就好了。如果这个结果不是我们想要的,那就说明这个环节过程当中是出了问题,并且对一些异常的情况我们也要去做相应的测试和处理。

【主持人丨大师兄】:在用传统的方式我这边一个负责测试的,出了一个问题,我马上知道去找哪个负责人,像这种情况的话我的测试人员找谁,他实际上是不知道的。

【嘉宾丨童奎松】:其实你是知道的,我们刚刚说了,你的业务流程有个UUID,你用这个UUID就可以查到你事件的列表,其实你就知道你应该在哪一步事件没有发出去,或者哪一步事件出错了,你就应该知道找谁。

【主持人丨大师兄】:所以是整个的监测系统必须是事先就上掉的,而不是我事后再去补,如果为了系统的健壮性的话我必须是要在整个的系统上线之前把整套的监测系统也做一下部署。

【嘉宾丨童奎松】:对,这个监测系统和各种错误处理其实是你程序的一部分。反应式编程四个东西,一个是说你的用户响应,你的反应不管任何情况下你的反应时间都要相对在一定范围之内的。第二,你高负载的时候,照样可以撑得住,你不能出错。第三,当你的系统错的时候,你也能容错的,不管是硬件错还是你的程序本身有错,还是各种网络错,你都是要处理一下。这三个是它的要求,因为反应式系统这个要求就是这样的。所以,为了达到这三个要求,你刚才说的监测,刚才说的自动恢复机制,比如你部署了三个实例,有个实例当掉了,你的系统要负责把重启新的实例然后补回三个实例。就是你的整个库存是三个实例,这些东西都是一个自动或者自愈的过程,不能说我今天三个实例当掉一个我通过人去手工启,这个在现在的高负载的要求下你来不及的。或者刚刚说的,在akka里面的actor模式,所有的actor会有supervisor,supervisor要负责把不工作的,比如说异常,或者状态有错,或者硬件失败,或者网络失败这种失败的actor全部给杀掉,然后再创建新的actor来替换掉它。

【主持人丨大师兄】:我这边再说最后一个问题,刚刚的问题都是和监测,测试响应式系统相关。最后一个问题是跟消息本身相关的,在某些场景下可能我的上游系统同时可能会发来许多消息,不止一个的消息,可能你在下游系统当中接收消息的时候会出现这种情况,其中我有一个消息要等另一个消息处理完我才能去做。其实有一个消息处理的先后顺序的保障,我不知道各位在这个方面有没有类似的一些实践呢?如果是我要去保证一个消息处理的顺序的情况下我应该怎么样去保证?

【嘉宾丨童奎松】:首先我觉得这是一个伪命题,因为你说的消息有前后的,同一个系统发出去以后我会发出去一个消息,这个消息一定会有系统来处理,然后发出另外一个消息,另外一个消息再触发下一步,他是一步步来进行的。没有说我同时收到两个消息,第二个消息效果等第一个消息处理完之后才可以处理。更多情况下是我先收到第一个消息,然后我再处理,发出第二个消息,第二个消息再处理发出第三个消息。

【嘉宾丨徐鹏程】:存不存在我这个系统发出去两个消息,但是我接收到这两个消息的下游消息之后再做下一步,其实相当于一个定型的状态。但是我又希望他可以做到,这两个都做完之后才能做到下一步。

【嘉宾丨童奎松】:这个是可以的,这个其实就是说你在你的系统本身你的状态,比如我们把刚刚的例子重新变一下。当我的订单成功之后我把客户付款,预留库存这两个是同时发生的,这时候库存系统和付款系统,两个系统会同时监听订单成功这个消息,分别做他的工作,分别发出保留成功和付款成功两个事件,然后在你的订单系统里面你会分别监听这两个事件,你不需要关心这两个事件谁先来谁后来,你只要关心当这两个事件都来了之后我再进行下一步的处理。

【嘉宾丨徐鹏程】:其实就是说我在接收这两个消息的时候我去检查一下另外的有没有成功?

【嘉宾丨童奎松】:你不需要检查你的另外消息成不成功,因为你不需要关心另外消息成不成功,你只关心你的系统的状态。因为当我接触到我的库存保留成功的状态的时候我可能会在我的订单更改一个状态的时候订单成功了,在我付款成功之后我可能会生成一个付款成功的交易号,然后填在订单里面,不管谁先来后来,只要我后来的检查到我的订单里面有这个状态了,我就可以往下一步走,而不是我检查到那个消息来没来。

【主持人丨大师兄】:但是之前因为在做最后一步处理的时候其实还是等消息,而不是去看订单的状态,因为订单的状态变化本身应该是会发一个消息吧。

【嘉宾丨童奎松】:没有,订单状态自己不发消息。

【主持人丨大师兄】:我今天一定要看两个状态的变化吗?

【嘉宾丨徐鹏程】:它相当于是在收到消息的时候触发那个状态检查吗?

【嘉宾丨童奎松】:对,它收到消息的时候你可以说是中间状态,这个取决于你的系统怎么写,你的程序怎么写。你收到两个消息之后你分别会在你的订单mark两个状态,你后收到的那个你只要检查前面的是不是收到,或者你的逻辑,你的订单,你都不需要担心我的哪个消息先到,我的逻辑是说,当我的订单满足了这个这个之后我走下一步,我不管你这个这个是事件改的,还是手工改的,还是通过rest改的,我不在乎。

【主持人丨大师兄】:其实遇到一些类似的可能需要有一些dependency的情况,那我整个把它设计也是归结为一个可能某个领域的状态的变化,可能就是有一些类似于可能我已经付款了,但是库存没拿货这样一个类似的中间状态你是需要考虑进去的,帮助你做下一步?

【嘉宾丨童奎松】:对的,只要你的业务设计的时候有存在这个可能,你的订单整个模块的设计就要考虑到这个情况。考虑到我付了款,但是没有库存,这个时候怎么办?是backorder还是退款?

【嘉宾丨徐焱飞】:我补充一下,在做分布式消息同步的情况下可能会有记录消息的顺序问题,做一些分析的时候,因为比如我们去查某一个特定的时间节点它的前后发生了什么顺序的时候,这个时候如果消息带有时间顺序那对我们分析这个问题是有帮助的。这种情况下我们一般会在消息自己的属性里面可能会增加一个比如类似于happenBefore或者happenAfter的关联关系,把这种关联关系放在消息本身里面去。比如一个消息A到一个聚合根的处理过程当中产生另外一个消息B,那就可以在消息B当中可能会加一个属性,就是happenBefore,就是A这个消息是在这个B消息前发生过的。当我们把所有的消息落地,落到持久层里面的时候,我们最终回溯找问题的时候,或者把整个回溯去获取最新状态的时候,那这个先后顺序是有帮助的。这是基于EventSourcing的方式。

【嘉宾丨童奎松】:对,每个消息一定会有它的timestamp,每个消息一定有它什么时候发生的。第二个,加上我们刚才说过的UUID,其实你就可以拎出一条线来。

【主持人丨大师兄】:对,但是UUID应该不会用到一个具体的业务场景中吧,应该也只是你外部的一个监测,从Devops的角度去加这个UUID。

【嘉宾丨童奎松】:因为我们刚刚说的,你的UUID是在你整个的业务流程都存在的。所以你整个业务完成之后你拿到你的事件的列表你就知道这个业务是怎么走的。你通过你的UUID能找出你所有的事件你就知道你的业务流程怎么走的,他到底是走到了订单创建失败还是订单成功,到底走到了付款成功还是付款失败。

【主持人丨大师兄】:对,这个UUID的目的还是在于背后监测你的系统的状态,但是本身应该是不会去取这样一个关系链吧。

【嘉宾丨童奎松】:会,这个UUID非常重要。

【嘉宾丨徐鹏程】:通常我们是用来做事件回溯的时候用。

【嘉宾丨童奎松】:包括我举个例子,比如你新版本上线你想测试一下,你怎么办?你一种是想做压力测试,一种是说你生成很多的数据来做压力测试。第二,你就把老的事件拿过来,UUID排序去做测试。其实就是事件另外一个好处,你可以重放,对于任何的场景你可以重放。

【主持人丨大师兄】:对,所以就像焱飞刚刚说的,可能就是用来回溯的一个场景,因为回溯可能大部分是做一个问题的查询或者帮你做测试。但是我在做真正的一个线上的业务逻辑里面应该不会再去拿这关系链去做这样一个查询吧。

【嘉宾丨童奎松】:对,你真正写你的程序里面是不会用UUID来决定做任何identity的,你肯定是拿订单ID或者业务ID来做的。但是这个UUID主要就是说的,一个是你监控,还有一个是你数据分析。比如你事后分析的时候你会把事件联起来。

【主持人丨大师兄】:我们今天所有的问题就到这里了,接下来我想看一下各位听众有什么问题吗?

【嘉宾丨童奎松】:我看到一个问题是状态机的问题,刚刚我们说到每个系统,每个领域都接收到消息来负责它自己的处理。但是当一些复杂的流程,比如有一些判断或者复杂的流程的时候每个系统自己本身已经处理不了的时候你可能会有一个状态机统一处理,就是我往你哪个系统去发,负责收到哪个系统事件,然后告诉哪个系统要做什么事情。但是这个状态机当然有不同的做法,一种是说你单独一个系统去做状态机来负责整个其实就像你把每个领域去耦合,第二种做法就是我某一个子系统,我一个子领域作为状态机,也就是刚刚说到的订单系统。可能订单系统里面有一个状态机来决定我所有的事情,针对这张订单,包括你的预留库存、付款都是针对这张订单的,所以我就会把状态机放在订单系统里面。不知道我这个问题解释得清楚吗?

【主持人丨大师兄】:所以状态机制我本身还是一个,我是不是可以理解为它其实不是一个正常的业务流程,而是一个处理日常运营情况的场景,就是发生了某些情况下我可以不从业务的角度,而是从中间去干预,直接往一个中间系统去发一个消息,去处理一些可能日常的例外情况。

【嘉宾丨童奎松】:状态机其实就是业务系统,状态机有一个好处是你可以很灵活地修改你的业务流程,可能是我通过一定的配置,不需要再修改我的代码,只是要修改我一定的配置我就可以重新调整我的业务流程。比如刚刚说的,开始可能我系统设计的时候,我没有库存我就不能收款,或者我不能发货。可能我这时候业务说了,像苹果来说,我可以预留三到四周以后发货,或者我先backorder,这个时候你就需要改流程。一种做法是你改代码,第二种是我功能都在,我只需要把我的状态机重新配置一下就可以了。

【主持人丨大师兄】:刚刚群里有人在说还是提出状态机的问题他在问状态机每个状态是否都是独立的?还是说一个全局的?

【嘉宾丨童奎松】:每个状态是独立的,相互不干扰的。但是从我的角度来说,状态机还有另外一个用处,解决冲突。比如你的系统是有状态的,你要把状态保存到某个地方,但是你又想分布式,比如你的数据库,你的分布式,这时候你要有两台数据库,以防我某台数据库当掉了,我能用另外的数据库来顶替,这有不同的模式,一种是主从模式,就是你是完全靠主服务。第二种是我主主模式,两个都是主,你的系统可以用任何一个数据库来写数据,这时候他们之间可能就会产生冲突。

但是冲突有几种做法,一种是我冲突之后我提醒数据库按照一定的规则,比如后来的肯定是先把前面的覆盖,这是一种规则。第二种规则,你通过设计状态机做一种叫conflict free,就不会产生冲突的一个状态机。当你两个系统,你的一个订单在两个数据库里面有不同的状态的时候它会合并进入到另外一个状态。举例说,当一个订单在一个地方付款成功,另外一个地方是取消的话,那你可能进入第三个状态,比如用户取消,这个时候你再根据这个状态去做相应的业务处理。

【主持人丨大师兄】:所以,你其实是在说我整个的消息处理的工作的流程中可以设置多套策略,然后根据不同的情况去运营一些不同的策略。

【嘉宾丨童奎松】:对,这是状态机的一个用法。另外一个用法我刚刚说了,当你要解决你分布式里面的冲突的时候这也是一个做法。

【主持人丨大师兄】:好的,这期就到这里,谢谢三位嘉宾。

跨境茶话会

“跨境茶话会”是由移动增长技术服务商“魔窗”联合国内外众多技术专家发起的在线技术交流活动,目前已邀请嘉宾来自Google、ebay、Snap、Uber、VISA、Pinterest、BranchMeteics、Splunk、小红书、华为等国际知名IT企业在职高级工程师,面向所有互联网从业技术人员分享交流先进理念和实战案例,同时为中美技术朋友提供跨境交友和深度学习的平台。

“跨境茶话会”结合前沿热点技术话题,精心策划每月一个线上技术主题交流,每期邀请来自国内外互联网界的三位实战嘉宾分享一线技术最佳实践,并针对核心技术问题对话答疑,为技术从业者拓宽思路、提升技术实力、驱动业务增长。

嘉宾报名:欢迎“实战派”、“硅谷牛”、“极客”碰撞交流,请联系Tina(微信:tina_1903)

听众报名:https://www.wenjuan.net/s/3iuuym/,免费参与每期线上分享讨论,与嘉宾一对一探讨,获取往期精彩干货(仅限研发与产品)

日记本
Web note ad 1