详细分析了Akka、AMQP、函数编程的特点及趋势
本期我们邀请到了15年大型软件系统工程师张天虎(ID:Sky-Tiger)与我们一起分享自己的研发之路以及对于开发领域新技术的观点看法,详细分析了Akka、AMQP、函数编程的特点及趋势等。欢迎大家跟贴分享更多相关的经验与讨论。
童馨:
虽然社区的很多人已经对您不陌生了,但是大家对现实中的您还是不太了解。麻烦您先自我介绍一下吧?分享一下您的职业经历?以及您目前从事的工作?方便大家更深入的了解您。
Sky-Tiger:
98年浙大硕士毕业后就进入华为工作至今,一直从事软件的研发工作。先后经历过十几个大型软件系统的开发和设计工作。最初开发使用BC++,在NT下,多进程系统,后来系统全部切换到Solaris上,开发语言依旧是C++。2000年左右,公司引入ACE/TAO平台(开源的跨平台的C++通讯中间件),系统变成多进程+多线程开发,并且要求跨平台运行,系统CORBA化。2003年开始转入JAVA的开发,主要是WEB系统,当时都是JSP+TAG+JS+WEBWORK,没有AJAX和JQUERY,后台是EJB2,现在想起来当时真是痛苦。由于EJB的问题, 后来项目转成使用HIBERNATE。2005年开始从事系统对接的工作,主要是北向系统,使用WEBSERVICE来实施MTOSI标准,当时是使用了EQUINOX+CXF的方案设计,实现动态WS的发布,后来切换成EQUINOX+SPRINGDM+CXF的方案。2007年-2009年转到公司印度所工作,主要是协调和国内的需求和交付。2010后转到瑞典研究所,从事分布式计算和大数据的实时分析的预研工作。
童馨:
在分布式计算领域,AKKA平台成为最新关注的焦点之一,能否详细介绍一下AKKA平台的技术特点? 基于actor编程模型来开发并发计算程序相对于原来基于JAVA线程方式来开发,关键的不同在哪里,为啥说actor是更轻量化的并发处理模型?
Sky-Tiger:
JAVA在JDK5之前写并发程序是非常麻烦的,你要么继承Thread类,要么实现Runnable接口,同步机制的力粒度也很粗。JDK5之后,引入了Concurrent包,增加了很多并发特性的支持,如Callable<T>接口,可以使用Future<T>来获取每个任务返回的结果,而原来的Runnable是没有这个能力的。还有就是更细粒度的锁,如Lock接口。
Actor Model最早是在上世纪70-80年代就被提出来了,是用来编写并行计算或分布式系统的高层次抽象,让程序员不必为多线程模式下共享锁而烦恼, Erlang 最先实现Actor Model,SCALA参考了这个实现。
Actors将状态和行为封装在一个轻量的进程/线程中,但是不和其他Actors分享状态,每个Actors有自己的封闭环境,当需要和其他Actors交互时,通过发送事件和消息,发送是异步的,非堵塞的(fire-and-forget),发送消息后不必等另外Actors回复,也不必暂停,每个Actors有自己的消息队列,进来的消息按先来后到排列,这就有很好的并发策略和可伸缩性,可以建立性能很好的事件驱动系统。
SCALA实现Actor Model有两种实现方式:
基于线程的实现:类似JAVA中的thread方式,通过定义一个thread-specific方法(Scala中的Actor类的actor方法/JAVA中的Thread类的run方法),将一个线程和actor绑定;
基于事件的实现:通过定义一个线程池和一系列的事件处理函数,将事件处理函数作为actor方法,实现多个actor共享一个线程池;
通常情况下两种实现模型各有自己的优缺点,线程模型简单易用,但效率不高;而事件模型高效但难于设计。为了适应这两种模型,Scala的Actor库实现了两个不同操作:receive和react,两个操作都是试图从actor的邮箱中提取消息,然后通过模式匹配的方式调用处理函数,但是receive是基于线程模型的,而react是基于消息模型的。
AKKA平台是TypeSafe发布并维护的轻量级Actor框架,相对于SCALA原生定义的Actor,其具有如下的特点:
1.更加简单的并发策略Simpler Concurrency,通过Actors STM & Transactors能够简化编写可靠的并行计算。
2.EDA架构Event-driven Architecture,完美的异步事件驱动架构,不会堵塞。
3.真正的可伸缩性,使用异步消息在多核以及多个节点之间扩展。
4.容错性,重视失败。Let it crash!
5.远程透明性,底层可以使用NETTY或是THRIFT来通讯
6.集群管理能力,Cluster机制是AKKA20后提供的新功能
AKKA系统特别适合在云端或是基于多刀片系统的分布式计算,具有很强的Scale out的能力。TypeSafe同时也是SCALA语言的发布者,它目前正在致力于实现SCALA全生态链,包括Play为基础的WEB前端框架,和后面的数据库访问技术。
童馨:
JMS标准已经发布进10年,而最近另一种消息标准AMQP逐渐展露头角,能否给大家介绍一样两者的不同和各自的特点?
Sky-Tiger:
AMQP是Advanced Message Queuing Protocol的简称,最初被应用于金融领域,解决异构金融系统之间的互联互通问题。现在AMQP已经成为一个开放的标准,有数十个来自各个领域的企业参与其中。目前AMQP的实现有很多,比如APACHE的QPID,RabbitMQ等。JMS作为JAVA领域的标准已经存在十余年,但JMS仅仅是定义了JAVA系统之间的互动标准,而不是JAVA系统之外的。如果我们有一个JAVA的系统要和一个用RUBY实现系统来对接,那么JMS就不能解决。你需要找一个既支持JMS又支持STOMP的消息中间件。如果对接方是一个C#系统呢?虽然现在有很多中间件如ACTIVEMQ, HORNETQ等都提供了一定层次的跨平台互动的能力,但这些能力都不是标准化。这也正是为什么AMQP被大家接受的主要原因之一。JSM定义了JAVA平台之间的标准消息协议,AMQP定义了跨所有平台的标准消息协议。AMQP同时还定义了消息传送的二进制标准。两者还是有很多不同:
1、 消息路由:JSM的路由模型相当简单,每个消息在发送的时候都要指定QUEUE;而AMQP,这个QUEUE是事先不知道的(PRODUCER不知道的),每个消息都要带一个路由KEY,并由EXCHANGE来决定消息被路由给哪个QUQUE,这样AMQP能够提供更灵活的路由机制。
AMQP: PRODUCER->EXCHANGE->BINDING->QUEUE->COSUMER
JMS: PRODUCER->QUEUE->CONSUMER
2、 消息模型:JMS定义了两种消息模型, Point2Point和订阅发布;
AMQP定义了五种消息模型,Direct Exchange和Fanout Exchange是两个必须支持
模型,另外三个Topic Exchange,Headers Exchange,System Exchange是可选实现的。Direct Exchange模型与JMS中Point2Point模式类似,唯一不同是Direct Exchange的接受QUEUE可以是多个,而不是Point2Point中的只能有一个!Fanout Exchange,Topic Exchange,Headers Exchange类似于JMS的订阅发布模型,但提供了更细粒度的控制机制。
3、 消息结构:AMQP消息的结构非常类似JMS,JMS中的消息包含三个主要的部分:头部分,属性部分和消息体。但两者在每个部分所包含内容的定义上有很大的不同,如AMQP的头部分可以定义不变的应用属性,而JMS只能定义不变的JMS定义的属性。同时AMQP的消息体只能是二进制格式的,而JMS可以定义几种不同的格式。
AMQP是消息互动机制的一个重要的进步,但这并不意味着你一定要使用AMQP而放弃原来的JMS,除非你的系统确实需要通过消息机制对接一些非JAVA系统。
童馨:
OSGI标准最近刚刚增加了blueprint的新DI标准,相对于原来的DS,他们的不同和适用的前景如何?
Sky-Tiger:
OSGI平台定义了很多API和机制,方便用于Bundle的开发。比如BundleActivitor接口可以帮助做一些Bundle启动/终止时的资源管理工作,BundleContext可以让你通过它和OSGI的运行时打交道,包括注册服务和查找服务等。直接使用这些接口所带来的问题也是显而易见的,就是你的业务逻辑跟OSGI绑定了,它们不在是PURE的了,你的单元测试不能脱离OSGI而单独进行,这是我们不愿意看到的。同时OSGI特别强调服务的动态性,于是在服务的管理上又增加了开发的难度,虽然OSGI提供了几种机制来应对服务的动态性,如事件通知机制和ServiceTracker,虽然缓解了问题,但没有根本解决。OSGI R4发布了DS规范,以声明的方式来定义Bundle 的服务发布和查找,支持DI,同时增加回调通知机制,帮助解决Bundle启停是的资源管理和动态服务问题和Bundle延迟加载问题。整个声明的过程看上去类似Spring中Bean的声明。这无疑是一个巨大的进步,你的业务逻辑可以是PURE的了,你不需要了解OSGI环境API就可以发布自己的Bundle了。但是它遗留了一个重要的问题给开发人员自己解决,就是如何处理服务的动态性。DS支持在声明Reference的时候指定bind/unbind的回调方法,但在这个方法中如何处理,需要开发人员自己解决。所以开发人员需要自己解决线程并发等诸如此类的问题。由于缺少统一的管理机制,每个开发人员在处理服务的动态性方面都有随意性,这导致了DS不适合于大型系统的开发。
Spring组织发布了自己的DM系统,它将Spring Bean的管理模式应用到Bundle内和Bundle间,使得OSGI开发和Spring系统无缝结合,同时它定义一种统一的服务的动态性行为:每个服务的Reference都仅仅是一个proxy,而不是真正的服务。那么服务的动态性完全被proxy挡住,对Bundle是缺省不可见的,当然可以定义监听接口来获取这些通知。当服务消失时,proxy依旧存在,所有对这个proxy的调用都被阻塞知道服务恢复或是超时。这是一个统一的行为,开发人员也不必考虑服务消失是回调所带来的多线程并发的问题。同时由于DM和Spring无缝结合,使得原来用Spring开发的系统很容易移植到OSGI环境中。
鉴于SpringDM的成功,OSGI标准组采取了开放接纳的态度,将SpringDM的精髓吸纳进来,定义成Blueprint规范,成为R4标准的一部分。OSGI组织同时也将继续保留DS标准。如果你熟悉SpringDM的开发,那么使用Bluepring将非常容易,它们几乎是都是一样的。
目前Blueprint的开源实现并没有在原来老的OSGI框架中如FELIX/EQUINOX继续进行,而是另外成立单独的项目如APCHE的ARIES项目和ECLIPSE的GEMINI项目。APACHE的ARIES项目是一个企业级OSGI实现,封装很多企业级别的特性,如事务、持久化等。Blueprint的实现是其中很小的一部分。
大家如果想使用Blueprint的特性,可以考虑使用APACHE的KARAF系统,它集成了ARIES项目中的Blueprint特性(feature),同时提供了针对OSGI的发布(Bundle Repository),部署和管理机制,是一个非常高效的OSGI管理和运行平台。内部KARAF支持FELIX/EQUINOX。KARAF内部预部署了很多Features,如SPRING/SPRINDDM(如果你不喜欢Blueprint话),还有D-OSGI能力(CXF+ZOOKEEPER),你可以根据自己的需要启动安装不同的Feature。
所以从长远看,Blueprint更适合企业级,大系统的开发。
童馨:
JAVA7没有引入大家期望的闭包等函数编程的特性,SCALA/CLOJURE这些基于JVM的面向函数编程语言逐渐引起大家的重视,函数编程的真正价值在哪里?为什么在当今大数据和云时代,函数编程又能够重放异彩?
Sky-Tiger:
函数编程之所以又被重视起来,很大原因是来自于多核系统的发布,云计算以及大数据处理的大背景驱动。我们熟悉的MapReduce算法就是来自于函数编程的思想。
当编写一个程序,我们需要使用计算机理解的词汇来解释我们的目的。在命令式语言中如C++/JAVA,它包含很多的命令。我们可以添加诸如”查询客户资料”的新命令词汇,但整个过程是一个说明要计算机完成的总体任务的步骤描述。随着程序的增长,我们的命令的数量也随之增加,使得它很难使用。而面向对象编程改变了这些,因为它使我们能够更好地组织我们的命令。我们可以将与客户相互关联的所有命令封装到一个客户实体(类)中,这使得能够更清晰的描述客户。然而,该方案仍然是一个指定计算机应该如何处理的命令序列。函数编程提供了一种与原来简单扩展的命令词汇完全不同的方式。它使我们不局限于仅仅增加新的简单的命令,同时也可以添加新控制结构和原语来的命令词汇一起共同创造一个程序。
在命令式程序或是面向对象程序中,往往包含很多对象,这些对象有自己的内部状态,这些状态可以间接或是直接的通过调用对象的方法来改变,特别是在多线程环境下,很难判断某一个时间点上对象的状态到底是什么样子的。而在函数编程的程序中,所有的对象或是数据结构都是不变的,所以这里的方法唯一能做的事情就是返回一个值,而不能改变对象或是结构的状态。当使用命令式语言或是面向对象语言编写多线程程序时,要面临两个问题:(1)如何把原来串行的代码转换成并行运行的代码;(2)对于共享状态的加锁过程相当复杂,你必须小心的设计以防止竞争条件或是死锁。函数编程却能很容易解决这两个问题:(1)它是基于Declarative编程风格,易于实现并发机制。编程语言支持声明性风格可以让我们使用新的方法来构建基本逻辑构造。当使用这种风格时,我们不再局限于基本的语句序列或内置的循环,因此产生的代码描述了更多的”做什么”,而不是”如何做”;(2)它使用不变对象,不存在数据状态的共享,不需要加锁,同步过程。
什么是基于Declarative编程风格?来看个例子,这是一段JAVA代码
- List<String> ret = new ArrayList<String>();
- for(person p :persons){
- if(p.getPrice()> 10.4){
- ret.add(String.format("{%1$s} - {%2$s}", p.getName(), p.getPrice()));
- }
- }
你可能需要仔细的阅读才能够了解这段代码到底要干啥,这段代码用了三个命令式语法:(1)创建一个存放结果的List;(2)遍历所有元素;(3)向结果集中添加信息;如果用基于声明样式的方法来实现上述过程,Scala实现:
- val ret = persons filter { kv => kv.age >20 } map{kv=> String.format("{%1$s} - {%2$s}",kv.name ,kv.age.toString())}
这个表达式的计算出的结果来自基本的操作filter和map,这些操作使用其他表达式作为参数。其语义简单明了,值得注意的是整个计算是作为一个单一的表达来表述结果,而不是一个语句序列。另一个有趣的方面是,许多实现的技术细节现在转移到操作的基本实现(如for循环,本身就在filter和map的基本实现中实施了)。这使得代码更简单,也更灵活,因为我们可以很容易地变更,无需另行作出更大的改变。
再看一个Monads的例子,先看一个普通的程序:
- File f = open("keys.txt");
- if (f == null)
- return null;
- String key = readLine(f);
- if (keys == null)
- return null;
- String value = ourDatabase.get(key);
- if (value == null)
- return null;
- return "The value is: " + value;
每一句都要检测是否为空,这样的代码写起来让人心烦。如果没有异常呢?或是异常可以被以某种方式处理掉,代码就会变成:
- File f = open("keys.txt") ;?
- String key = readLine(f) ;?
- String value = ourDatabase.get(key) ;?
- return "The value is: " + value;
代码是不是就简洁很多?SCALA中定义了Option类型的Monad,使用Option[T],上面的代码就会变成这样:
- open("keys.txt") flatMap { f =>
- readLine(f) flatMap { key =>
- ourDatabase.get(key) flatMap { value =>
- "The value is: " + value
- }
- }
- }
如果任何一个过程为空,就返回空,Option内部会帮你处理这些。
我这里讲这些不是推崇函数编程就完全可以替代传统的OO编程,两者应该是一种互补的关系,在某些场景下使用函数编程会达到意想不到的简洁的效果。
童馨:
最后问您一个比较宽泛的话题,有些人认为:由于很多的IT企业和程序员都比较浮躁,在IT技术领域很难有创新的技术出现,这些都是由于整个IT大局造成的,您是否认同这样的观点?为什么?
Sky-Tiger:
浮躁是中国的普遍现象,不仅仅是在IT领域,作为IT技术基础研究的学校更是如此。在国内静下心来做技术真是不容易!国内的企业招人更多是抓人做事情,尽快赚钱,而忽略了企业还有为社会培养人才的作用。即使是华为这样的企业,也难免有这样的情况。国内的IT领域缺少学术基础,特别是来自大学的研究成果支持。我曾经将今年数据库大户的资料上传到公司内部的网上,也收到了很多来自公司内部的评论,很多都是指出我们的技术更多是跟随,模仿和使用,谈不上创新。我的一个同事参加了美国TDWI大会,是美国一年一次的数据仓库大会,参加的都是来自各个大学的教授或是知名的企业,他们讨论的很多东西都是很前沿的,如“Big Data Analytics for Real-Time Business Advantage”、“ Advanced Analytics versus Online Analytic Processing”、“ Preparing Analytic Data Differs from ETL for Data Warehousing”。国内相对来说还要有很长的路走。但问题是:这样路(跟随,模仿和适用)还要走多久?