欢迎光临汤雪华的博客

一个人一辈子能坚持做好一件事情就够了!坚持是一种刻意的练习,不断寻找缺点突破缺点的过程,而不是重复做某件事情。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

ENode 1.0 - 框架的总体目标

Posted on 2013-06-27 00:31  netfocus  阅读(4346)  评论(21编辑  收藏  举报

开源地址:https://github.com/tangxuehua/enode

本文想介绍一下enode框架要实现的目标以及部分实现分析思路剖析。总体来说enode框架是一个基于cqrs架构和消息驱动的应用开发框架。在说实现思路之前,我们先看一下enode框架希望实现的一些目标吧!

框架总体目标

  1. 高吞吐量(High Throughput)、低延迟(Low Latency)、高可用性(High Availability);
  2. 需要能充分利用CPU,即要允许方便配置需要使用的并行处理线程数,以提高单台机器的command处理能力;
  3. 支持command的同步和异步处理,同步处理时要允许客户端捕获异常,异步处理时要允许客户端设置回调函数;
  4. 应用编程模型要统一,框架api要简单、好用、一致、好理解;
  5. 能让开发人员只关注业务,不用关心数据哪里来,以及如何保存,也不用关心并发、重试、超时等技术相关的问题;
  6. 基于消息驱动的架构,那消息投递方面,要能做到:至少投递一次(即如果宕机了消息也不能丢)、且能做到最多投递一次,因为有时我们无法做到消息的等幂处理;
  7. 要足够可扩展,框架中每个组件都要允许用户自定义并替换掉,包括IOC容器;
  8. 因为是CQRS架构,那必须要确保单个聚合根的事件的持久化顺序与分发给查询端的顺序要完全一致,否则会出现严重的数据不一致的问题;

实现高吞吐量、低延迟、高可用的思路分析

关于性能的几个重要概念

吞吐量是指系统每秒可以处理的请求数;延迟是指系统在处理一个请求时的延迟;一般来说,一个系统的性能受到这两个条件的约束,缺一不可。比如,我的系统可以顶得住一百万的并发,但是系统的延迟是2分钟以上,那么,这个一百万的负载毫无意义。系统延迟很短,但是吞吐量很低,同样没有意义。所以,一个好的系统的性能测试必然受到这两个条件的同时作用。有经验的朋友一定知道,这两个东西的一些关系:Throughput越大,Latency会越差。因为请求量过大,系统太繁忙,所以响应速度自然会低。Latency越好,能支持的Throughput就会越高。因为Latency短说明处理速度快,性能高,于是就可以处理更多的请求。所以,可以看出,最根本的,我们是要尽量缩短单次请求处理的时间。另外,可用性是指系统的平均无故障时间,系统的可用性越高,平均无故障时间越长。如果你的系统能保持一年365天都能7*24全天候正常运行,那说明你的系统可用性非常高。

思路分析

要实现高可用,要怎么办?简单的办法就是主备模式,即一份站点同时运行在主备服务器上,主服务器如果正常,那所有请求都由主服务器处理,当主服务器挂了,那自动切换到备服务器;这种方式能确保高可用;甚至我们还能设置多台备的服务器增加可用性;但是主备模式解决不了高吞吐量的问题,因为一台机器能处理的请求数总是有限的,那怎么办呢?我觉得就需要让我们的系统支持集群部署了,也就是说,不是只有一台机器在服务,而是同时有很多台机器在服务,这些同时服务的机器称为一个集群。而且为了能让集群中的服务器的负载能平衡,为了尽量避免某台服务器很忙,其他服务器很空的情况,我们还需要负载均衡技术。当然,真正的高可用同样意味着不能有单点故障问题,就是不能因为集群中的一个点挂了导致整个集群挂掉,所以我们要杜绝所有的数据都要经过某个点的设计;相反,要做到每个点都能横向扩展,web应用站点(enode框架支持)、内存缓存(memcached,redis都支持)、持久化(mongodb支持),都要能支持集群与负载均衡。好,整个系统所有层次都支持集群+负载均衡解决了高吞吐高可用无单点的问题,但并没有解决低延迟的问题,那怎么办呢?如何才能尽量快的处理一个用户请求呢?我觉得关键是三个方面:In Memory+尽量快的IO+无阻赛,也就是内存模式加很快的数据持久化加无阻塞的编程模型。

In Memory

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不断串联的无阻塞的方式来实现整个过程。这一点在我之前的博文中应该已经做了比较详细讨论了。

目前只能想到这么多分析思路吧,希望对大家有帮助。为了篇幅不要太长的原因,框架的其他一些目标的分析思路只能在后续的文章中慢慢讨论了。希望我能坚持下去。我个人能思考到的问题毕竟有限,希望大家看了后能多多提一些问题,然后大家讨论解决,这样才能让框架不断完善起来。