enode框架系列step by step文章系列索引:
本文想介绍一下enode框架要实现的目标以及部分实现分析思路剖析。总体来说enode框架是一个基于cqrs架构和消息驱动的应用开发框架。在说实现思路之前,我们先看一下enode框架希望实现的一些目标吧!
吞吐量是指系统每秒可以处理的请求数;延迟是指系统在处理一个请求时的延迟;一般来说,一个系统的性能受到这两个条件的约束,缺一不可。比如,我的系统可以顶得住一百万的并发,但是系统的延迟是2分钟以上,那么,这个一百万的负载毫无意义。系统延迟很短,但是吞吐量很低,同样没有意义。所以,一个好的系统的性能测试必然受到这两个条件的同时作用。有经验的朋友一定知道,这两个东西的一些关系:Throughput越大,Latency会越差。因为请求量过大,系统太繁忙,所以响应速度自然会低。Latency越好,能支持的Throughput就会越高。因为Latency短说明处理速度快,性能高,于是就可以处理更多的请求。所以,可以看出,最根本的,我们是要尽量缩短单次请求处理的时间。另外,可用性是指系统的平均无故障时间,系统的可用性越高,平均无故障时间越长。如果你的系统能保持一年365天都能7*24全天候正常运行,那说明你的系统可用性非常高。
要实现高可用,要怎么办?简单的办法就是主备模式,即一份站点同时运行在主备服务器上,主服务器如果正常,那所有请求都由主服务器处理,当主服务器挂了,那自动切换到备服务器;这种方式能确保高可用;甚至我们还能设置多台备的服务器增加可用性;但是主备模式解决不了高吞吐量的问题,因为一台机器能处理的请求数总是有限的,那怎么办呢?我觉得就需要让我们的系统支持集群部署了,也就是说,不是只有一台机器在服务,而是同时有很多台机器在服务,这些同时服务的机器称为一个集群。而且为了能让集群中的服务器的负载能平衡,为了尽量避免某台服务器很忙,其他服务器很空的情况,我们还需要负载均衡技术。好,集群+负载均衡解决了高吞吐量的问题,但并没有解决低延迟的问题,那怎么办呢?如何才能尽量快的处理一个用户请求呢?我觉得关键是三个方面:In Memory+尽量快的IO+无阻赛,也就是内存模式加很快的数据持久化加无阻塞的编程模型。
in memory是什么意思呢?在enode框架中,主要的体现是,当我们要获取领域聚合根对象然后进行一些业务逻辑操作时,是从内存获取,而不是从数据库。这样的好处就是快。那这样做要面临的一些问题,如内存不够怎么办?用分布式缓存,如memcached, redis这样的成熟基于key-value模式的nosql产品。redis服务器挂了怎么办?没关系,我们可以让框架自动处理,即当发现内存缓存中不存在时,自动在从eventstore取,就是取出当前聚合根的所有事件,然后使用事件溯源(event sourcing,简称ES)的机制还原聚合根,然后尝试更新到缓存,然后返回给用户。这样就解决了缓存挂了的问题,当redis缓存服务器重启后,又能继续从缓存中取聚合根了;实际上,我们也要根据情况进行分布式集群部署redis服务器,这样一方面是为了能将数据sharding,另一方面能提高缓存的可用性,因为不会因为一台redis缓存服务器挂了导致整个系统所有的缓存数据都丢失了。另外,你可能会奇怪,redis缓存服务器里的数据哪里来呢?同样利用ES模式,因为我们在eventstore中存储了所有聚合根的所有的事件,所以我们就能在redis缓存服务器启动时,对所有需要放在缓存中的聚合根根据ES模式来得到。
怎样才能尽量快的持久化呢?我们先分析下enode框架需要持久化的关键数据是什么,就是事件。因为enode框架是一个基于event sourcing架构模式的,我们不会存储对象的最终状态,而是存储对象每次发生的事件;并且,每次事件都是append的方式追加到eventstore。我们唯一需要确保的是eventstore中的事件表中的聚合根ID+事件版本号唯一即可;通过这个唯一索引,我们能检测同一个聚合根是否有并发冲突产生。除了这个唯一性索引的要求外,我们不需要事务的支持,因为我们每次总是只插入一条记录;好了,那这样的话,我们要选择传统的关系型数据库来持久化事件吗?显然不太合适,因为慢!更明智的选择是用性能更高的NoSQL DB。如MongoDB,MongoDB默认的持久化是先放入内存,然后每隔100毫秒写入日志,然后可能60秒写入一次磁盘。这样的特性使得我们可以非常快速的持久化事件,因为持久化事件实际上只是写到mongodb server的内存中而已。另外,当数据被写入到日志后,我们就可以认为数据已经被安全的持久化了,因为即使断电了,mongodb也能将数据从日志恢复。当然你的疑问是,那如果断电了,那理论上这100毫秒的数据不是就丢了,没关系,我们还可以同时把数据写入到多台mongodb server,也就是我们可以部署一个MongoDB server的集群,一般整个集群的所有机器都同时挂掉的可能性是很低的,所以我们可以认为这样的思路是可行的。当然,这里所说的一切要能实现,还需要很过重要的细节问题要考虑。本文主要是给出思路。我一直觉得解决问题的思路最重要,是吗?另外,mongodb是介于key-value结构的NoSQL产品和关系型DB之间,它是一个文档型数据库,最主要的是它也支持像数据库一样的关系查询、更新、删除等操作,再加上高性能以及支持集群分布式等特性;所以我觉得非常适合用来作为eventstore。
另外,还有一个问题很重要,那就是序列化。数据存储到mongodb时,要被序列化,而.net自带的二进制序列化类(BinaryFormatter)不是太快,所以会成为持久化的瓶颈,那怎么办呢?呵呵,当然也是去找一个更高效的二进制序列化类库了。目前为止,我找到的是一个开源的NetSerializer,测试下来发现是.net自带的10倍左右,这样的性能完全可以满足我们的要求了;再简单谈一下为什么NetSerializer能这么快呢?很简单,.net自带的BinaryFormatter每次都需要反射,而NetSerializer在程序启动时已经将所有要序列化的类型的元数据都一次性生成了,所以系列化或反序列化的时候就不用再做这一步耗时的操作,所以当然就快了。当然像google protocol buffer也性能非常高,也很成熟,对,总之序列化方面我们还有很多解决方案来优化。
接下来我们来看看如何实现无阻塞。先想一下为什么要无阻赛?举个例子:比如电商网站通过信用卡来订购商品。一般的做法就很直接,就是先获取订单信息,通过银联的外部服务来验证信用卡信息是否有效(这意味着信用卡号如果有问题,根本就不会生成订单),然后生成订单信息入库,这两步放在一个操作里。这样做的问题是,由于信用卡验证服务是一个外部服务,因此操作往往会被阻塞较长的一段时间。这样就导致整个系统无法高效的运行。
无阻赛的方式是:把整个操作分为两个,第一个操作是获取用户填写的订单。这个操作的结果是产生一个“信用卡验证请求”的事件。第二个操作是当它接受一个“信用卡验证成功响应”的事件,生成订单入库。我们的系统在完成第一个操作之后会接下来执行另外其他的事件,也就是不会依赖于信用卡验证的结果了,直到“信用卡验证成功响应”事件产生了,我们的系统才会继续处理后续的创建订单的事情。
可以看出,这样的设计实际上就是一种事件驱动(event-driven)的思想。基于这样的思想,我们的系统一直在不停的运转,不会因为和外部系统的交互而要同步等待外部系统的处理结果。同样,对于一个用户操作如果涉及多个聚合根的修改的情况,也是采用这样的事件驱动的思想,采用我常提到的saga的思想。我们不会在一个command中把所有事情都做完,而是会通过command+event不断串联的无阻塞的方式来实现整个过程。这一点在我之前的博文中应该已经做了比较详细讨论了。
目前只能想到这么多分析思路吧,希望对大家有帮助。为了篇幅不要太长的原因,框架的其他一些目标的分析思路只能在后续的文章中慢慢讨论了。希望我能坚持下去。我个人能思考到的问题毕竟有限,希望大家看了后能多多提一些问题,然后大家讨论解决,这样才能让框架不断完善起来。