詳細分析了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”。國內相對來說還要有很長的路走。但問題是:這樣路(跟隨,模仿和適用)還要走多久?