公司物联网项目集成Orleans以支持高并发的分布式业务,对于Orleans也是第一次接触,本文就分享下个人对Orleans的理解。
这里先抛出自己的观点:Orleans 是一个支持有状态云生应用/服务水平伸缩的基于Virtual Actor 模型的.NET分布式框架。
下面我会从以下几个关键点,进行阐述:
云生应用的挑战
何为有状态/无状态
什么是 Actor 模型
什么是 Virtual Actor 模型
在讲云生应用之前,我们来先讲讲传统应用,对于传统应用常用的三层结构如下图所示。
随着业务的发展,数据库层通常存在瓶颈,为了缓解数据库的压力,一般会优先考虑增加一层缓存层。
而随着业务的继续发展,高并发、大数据量的应用场景逐渐凸显。如果继续在单体应用的基础上进行扩展,能做的无非是增加消息队列、异步、读写分离等机制进行性能优化。总体而言,优化空间不大,但应用的整体复杂度却随着引入的新的技术框架而迅速增加,对于应用的维护,是一个潜在的定时炸弹。
这个时候你可能会想,既然单体应用单机部署不能满足需求,我可以做集群啊。通过将单体应用按照分层结构进行纵向分离,将数据库从应用服务器分离,将缓存从应用服务器分离。这样就可以对分离的各个部分进行分别部署,再借助负载均衡完成集群效应。到这一步,你的应用应该能撑一段时间了。
这个时候,如果回到业务本身去分析,对于一个复杂应用来说,通常的性能瓶颈就是几个核心服务上。如果能够对存在性能瓶颈的服务进行伸缩,既能大大提高应用的整体可用性又能提高资源的利用率。那怎么做呢?服务拆分。
云生应用就是服务拆分的结果,云生应用最大的特点就是:
并行:是指同一时刻能够处理多个任务。这无可厚非,云生应用以多个服务形式提供服务,自然是支持并行的。
分布式:是指一个应用/服务多次部署,以应对高并发,提升应用/服务的整体性能。
或者简单来说,云生应用通过服务拆分支持服务并行,同时各个服务能够快速伸缩以提升系统吞吐量来应对高并发的业务场景。
虽然通过服务拆分简化了整个应用的业务复杂度,但是实现的技术复杂度却只增不减。
转向云生应用我们面对的第一个难题就是:如何进行服务拆分,才能确保其能分布式部署,或者说是水平伸缩?!
有经验的同学,可能会立马想到,要将应用/服务设计为无状态的。但是这里我要向你讨教几个问题:
这个状态是指什么?
何为有状态?
何为无状态?
大家不妨先停下来思考一下。(欢迎大家在评论中阐述不同观点。)
这里,我尝试从以下两个角度来谈下自己的看法:
1. 对象
面向对象编程强调的是对现实事物的抽象和封装。通过对事物状态和行为进行抽象然后封装为对象(类),其中状态封装为类的属性、字段,将行为封装为类的方法。这个时候得到的对象是没有生命力的,因为它本质是一个抽象的结果。只有在程序运行中对类进行实例化得到一个对象的实例时,才可以说这个实例对象是有状态和行为的,因为这个状态和行为是其独自持有的,这是一个非常核心的条件。独自持有,换句话说,就是非共享成员。独自持有非共享的成员就可以说这个对象实例是有状态的吗?这里面你就要看清状态和有状态的区别!举个简单例子,大街上你看到一大叔开着豪车,你觉得他很富有。“开着豪车”是你即时看到的状态属性。“富有”是你的状态断言。但这个状态断言是一个假设,毕竟可能是借的嘛。怎样才能断定“富有”就是这位大叔拥有的状态呢?很简单,假设一年365天你天天见到他开豪车,那基本八九不离十了。
所以,如果认定一个对象是否有状态,还要看其状态属性是否持久化!
如果你同意这个观点,那么哪天你看我骑个共享单车,气喘吁吁从你面前经过,就不要简单认为我是苦逼工薪族。毕竟我也是身价上千万,只是偶尔骑个车锻炼锻炼。(身价上千万,昨晚梦到的。)
所以,从对象角度看,一个对象是否有状态的充分必要条件是:
对象已实例化(处于运行时)
拥有非共享的状态属性
状态持久化
那问题来了,我们经常写的类创建的实例,是有状态的吗?
2. 应用
基于上面的总结,我们再来从应用的角度来看分析这个问题。
那应用的状态和行为是什么?首先,只有运行中的应用才有状态和行为。基于这个前提,个人理解运行时应用的状态是应用持有的数据,行为是应用提供的功能。那应用的有无/无状态界定就要看运行时应用持有的数据能否持久化。
以简单的Web分层应用举例 。从逻辑架构上来讲应用一般分为三层,表示层、业务层和数据访问层。上层进行状态行为的封装,数据层提供数据的持久化。所以从整体的角度来看,其是一个有状态的应用。但单独来看,我们不能对每一层进行有/无状态的界定。第一,每一层不能单独运行;第二,分层的目的是为了职责的隔离,每一层负责相应职责的抽象和封装,其输出的是类文件,是对象的集合,没有生命力。
那从物理架构上来讲,Web应用可以分开两个部分进行部署:Web实例和MySQL实例。也就是说应用和数据库是可以分开部署的。这个时候Web实例就是无状态的。那我们一般常说的无状态服务其实是就是从这个拆分的角度来说的。
理清完服务拆分的核心问题后,我们不得不来处理第二个棘手的问题:如何解决云生应用高并发的应用场景呢?那首先我们需要明确处理高并发的难点在哪?第一个是高性能,第二个就是:资源竞争导致的数据一致性问题。对于第一个难点,通过水平扩展服务可以化解;对于第二个难点,一般就是采用锁机制,而对于云生分布式的应用场景下,处理手段就更加复杂,可能需要使用分布式锁,而这种做法,大大降低了应用的整体响应性能。那有没有更好的解决方案,既兼顾性能又可以确保数据一致性呢?
有,借助Actor模型。
简单来讲:Actor模型 = 状态 + 行为 + 消息。一个应用/服务由多个Actor组成,每个Actor都是一个独立的运行单元,拥有隔离的运行空间,在隔离的空间内,其有独立的状态和行为,不被外界干预,Actor之间通过消息进行交互,而同一时刻,每个Actor只能被单个线程执行,这样既有效避免了数据共享和并发问题,又确保了应用的伸缩性。
另外Actor基于事件驱动模型进行异步通信,性能良好。且位置透明,无论Actor是在本机亦或是在集群中的其他机器,都可以直接进行透明调用。
因此Actor模型赋予了应用/服务的生命力(有状态)、高并发的处理能力和弹性伸缩能力。
对于Actor模型,业界已经有系列的实现框架,比如Erlang、Akka。然而Actor模型作为一个偏底层的技术框架,对于开发者来说,需要有一定分布式应用的开发经验,才能用好Actor(包括Actor的生命周期管理,状态管理等等)。为了进一步简化分布式编程,微软的研究人员引入了 Virtual Actor 模型概念,简单来讲Virtual Actor模型是对Actor模型的进一步封装和抽象。其与Actor模型的最大的区别在于,Actor的物理实例完全被抽象出来,并由Virtual Actor所在的运行时自动管理。
Orleans 就是作为一款面向.NET的Virtual Actor模型的实现框架,提供了开发者友好的编程方式,简化了分布式应用的开发成本。在Orleans中Virtual Actor由Grain来体现。
Orleans中核心优势:开发效率高、透明可伸缩。
开发效率高具体表现为:
面向对象的编程范式去实现Grain
Grain单线程执行
Grain透明实例化:换句话说,应用无需关注Actor实例的创建、销毁,可以直接调用Actor提供的方法。Actor的生命周期由Virtual Actor 运行时进行管理,类似GC,可以把Actor理解为完全托管的状态。
Grain位置透明:Actor之间通过持有彼此的逻辑引用(非实例引用)进行相互调用,而不需要知道Actor所处的实际位置。
Grain状态透明存储
异常的自动传播
透明可伸缩体现为:
应用状态的隐式细粒度划分
自适应的资源管理:Grain的生命周期完全由Orleans 运行时托管。
多路通信:Grain的位置透明,Grain之间通过一组固定的TCP链接进行多路复用来进行消息传递。
高效调度
显式异步
这篇文章,就简单写到这里,对于Orleans的详细介绍后续会结合实际项目输出更系统的应用细节,下次再见。