dRuby 的機制與實現
dRuby 的機制與實現
面向對象的腳本語言 Ruby 由于它代碼容易編寫并且靈活,動態的特性被眾多程序員喜愛。過去的幾年里,Ruby onRails 的 web 開發框架非常流行,這是得益于它的開發效率,而且再次引起了企業對 Ruby 的關注。Ruby 的焦點已經開始從小的腳本工具向大型應用程序轉移,針對于 Ruby 的分布式系統的需求和 Ruby 一般用法的例子與特性的相關教程也正在與日俱增。一本由日本作者寫的關于 dRuby 的書,其英譯版正在由 Pragmatic Bookshelf 計劃出版。
作者開發 dRuby 和 Rinda 是為 Ruby 語言提供分布式對象系統和共享元組空間功能, 并且也作為 Ruby 標準庫的一部分。dRuby 通過網絡擴展了 Ruby 同時還保留了其優點。Rinda 在 dRuby 的基礎上構建,并且帶有 Linda 的功能,是作為 Ruby 與分布式協作系統通信的中間語言,這篇文章討論了這兩個系統的設計原則和實現要點,并且用示例代碼和實際應用程序的例子來展示它們的簡單易用性。dRuby 和 Rinda 除了可以做分布式系統,這篇文章還會給你展示它們可以作為現實中應用程序的底層組件。
1. 介紹
這篇文章的目標是給你一個關于 dRuby 和 Rinda 的細節。dRuby 為 Ruby 提供分布的對象環境。Rinda 是一個運行在 dRuby 上的協調機制。首先,我來介紹 Ruby。
Ruby 是一個面向對象的腳本語言,它是由松本行弘(Yukihiro Matsumoto)創造。
直到現在,Ruby 的流行被限制在早期采用的程序員社區中。但是,Ruby 因為其潛在的生產力,現在正在快速引得商業用戶的注意。
Ruby 有如下特性:
-
標準的面向對象特性,比如類與方法
-
一切皆為對象
-
弱類型變量
-
簡捷方便的使用庫
-
簡單易學的語法
-
垃圾回收器
-
豐富的反射功能
-
用戶級線程
Ruby 號稱是動態的面向對象語言。一切都由對象構成,并且無強類型變量。方法統一在運行時創建。此外,Ruby 還有豐富的反射功能,并且可以使用元編程。
Ruby 是一門難解的語言,好像是老天對 Ruby 開了個玩笑。就像 Ruby 在主導著我們進行編程一樣,在我們沒有得到明示的情況下就覺得已經觸及了面向對象的本質。
dRuby 是一個使 Ruby 運行于其上的分布式系統環境。dRuby 通過網絡擴展了 Ruby 的方法調用,并且使得其他進程或者機器也可以調用當前對象的方法。另一個就是 Rinda 在它自己內部加入了 Linda 的實現,所以它提供了公共的元組空間。這篇文章不但介紹了 dRuby 的基本概念和設計原則,而且還包括 dRuby 的實現和實際用法。這篇文章會討論如下主題:
-
在 Ruby 之路 - dRuby 概述與其設計原則
-
實現 - dRuby 的實現
-
性能 - dRuby 的使用開銷
-
應用程序 - 一個真正運行在 dRuby 上的系統應用,Rinda 的概述與運行在 Rinda 上的一個系統應用。
Blaine Cook,推ter(一個微博服務)的主要開發者負責人,在他的“"Scaling 推ter”發布會中提到過dRuby。
-
簡單至極,速度夠快
-
些許古怪,無冗余,高耦合
他的觀點是合理的看法。
在這章里,我會說一下 dRuby 的設計原則和特性。
2.1 dRuby 的特性
dRuby 是用于 Ruby 的 RMI(遠程方法調用)庫的其中一個,我的最終目標不是讓 dRuby 成為另一個用于 Ruby 的傳統的分布式系統。準確來說,我打算擴展 Ruby,使其可以調用其他進程或者別的機器上的 Ruby 方法。
最終,dRuby 擴展了 Ruby 解釋器,使其無論在物理環境還是虛擬環境都可以跨進程,跨機器。
dRuby 有如下特性:
-
專門用于 Ruby
-
完全純 Ruby 編寫
-
不需要像 IDL一樣的規范
從常規的分布式對象系統的角度來看,dRuby 還有這些特性:
-
便于安裝
-
易于學習
-
能自動選擇對象的傳遞策略(傳值或者傳引用)
-
足夠快速
-
服務器端與客戶端無差別
dRuby 是專門用于 Ruby 上的分布式對象系統,dRuby 運行的平臺上可以交換對象,也可以在對象之間調用方法。dRuby 可以不受操作系統影響,運行在任何可以跑 Ruby 的機器上。
dRuby 完全由 Ruby 編寫,沒有特殊由 C 編寫的擴展。感謝 Ruby 優秀的 thread,socket 和 marshaling 類庫,早期的 dRuby 實現僅有 200 行代碼,我相信這足以展示了 Ruby 類庫的強大和優雅。現在 dRuby 包含了 Ruby 的標準發布版作為其標準庫之一,所以 dRuby 在任何裝了 Ruby 環境的機器上都可以跑起來。
dRuby 特別注意保持與 Ruby 腳本的兼容性。dRuby 向 Ruby 加入分布式對象系統的同時也盡可能多的保留了 Ruby 的原汁原味。Ruby 程序員會發現 dRuby 是 Ruby 的無縫集成擴展。程序員可能習慣于其他的分布式對象系統,而且可能會感覺到 dRuby 有點奇怪。
Ruby 里的變量都是弱類型的, 而且對象賦值不受繼承體系限制,不像一些靜態檢查變量的語言,如java,在代碼執行之前不會檢查對象的正確性,并且方法定位也只在運行時(方法被調用的時候),這是 Ruby 語言一個重要的特性。
dRuby 也以相同的方式運行。dRuby 中,客戶端靜態方式 (DRbObject,在 dRuby 也稱之為 "reference" ) 也同樣是弱類型的,而且方法定位也只在運行時進行。這就不需要提前暴露方法列表或者知道繼承信息。 因此,不需要定義一個接口(e.g. by IDL)。
除了允許跨網絡方法調用,dRuby 也一直在用心的開發,遵守盡可能接近常規 Ruby 行為的準則。因此,開發者可以盡情的享受大量 Ruby 的獨特優勢。
例如,方法使用 blocks(原始叫法是迭代器)和異常是可以被處理的,如果是在本地的話。互斥,隊列和其他線程同步機制都可以同時被用來進行進程間同步,無需任何特殊考慮。
2.3. 對象傳遞
只要不是原生存在于 Ruby 里專有的概念,這里都會盡量自然的在 dRuby 的環境下介紹。
對象數據傳輸是個不錯的例子。當方法被調用時,各種對象數據,如方法參數,方法返回值異常對象等數據就會被傳輸。方法參數從客戶端傳遞到服務器,同時方法返回值和異常對象從服務器返回客戶端。在這篇文章里,我把這兩種對象傳輸的方式歸類為對象交換。
在 Ruby 中,賦值(或者綁定)給變量總是通過引用的方式。對象克隆絕不不能用賦值。但是,這一切在 dRuby 都不同于 Ruby。在分布式對象的世界里,分辨“傳值”與“傳引用”是日常不可避免的。對于 dRuby 也是這樣的。
可以想象在一個應用程序中,當一個處于引用中的計算模型長時間不斷進行對象數據交換(直到它們都變為 nil)。而這時候,就需要“值類型”了。
dRuby 提供的機制會讓程序員最小化關注對不同類型對象之間數據交換,同時又能如愿獲取到想要的結果。程序員不需要顯式指定傳值還是傳引用。系統會自己決定使用哪一種方式。決定的規則很簡單,可序列化的對象就是通過傳值,不可序列化的對象就是傳引用。
即使這個規則不總是對的,但大多數情況都是正常的。這里我簡要的說一下這個規則。首先要明白一點,對象不經過序列化是不能使用傳值這種方式的。問題是有的序列化的對象傳引用比傳值更合適。為解決這個問題,dRuby提供了一種機制,使用這種機制可以使序列化的對象顯式的指定以傳引用的方式傳遞。之后會在這篇文章中詳述。
dRuby自動選擇對象傳輸方式的意義所在就是最小化手寫處理對象傳輸的代碼規模數量。
dRuby幾乎不需要接口定義和對象傳輸形式定義,這并非只是dRuby不同于其他分布式系統的一個方面。這是因為dRuby旨在成為一個“類Ruby的分布式對象系統”,可能也是被當做“kinda flaky”的原因。
2.4. 不支持的特性
接下來我將介紹一些 dRuby 中不支持的特性,叫做垃圾回收和安全機制。
dRuby 沒有實現分布式垃圾回收的原因是,我沒找到一種合適的廉價解決方案。
現在,由應用程序自己負責防止導出的對象被回收。防止對象被垃圾回收的一個辦法是使用已經提供的 ping 機制,但是這會使對象被循環引用導致對象永遠不會被回收,針對這個問題可能的解決方案,包括修改 Ruby 解釋器,現在正在探索中。
dRuby目前沒有提供任何安全機制,最多就是想Ruby那樣強制限制方法的可見性。但是這對于惡意攻擊于事無補。所以盡可能使用SSL來保證網絡通信的安全。
這節我會介紹一下dRuby的設計原則。
總起來說,dRuby擴展了Ruby的方法調用,dRuby不僅僅只是一個類Ruby的RMI接口,dRuby也可以與XML-RPC、SOAP、CORBA等共存。實際上,有些地方使用http作為外網接口而使用dRuby作為其內網的后臺。
3. 實現
這個章節我會介紹幾個dRuby有趣的特性和其實現。
我會使用dRuby最初版本的代碼與其他示例的代碼解釋基本的RMI和對象傳輸機制。
3.1. 基本RMI
首先我會用實例代碼解釋基本的方法調用。下面的代碼是經典的共享隊列實現的消費者生產者例子。
# shared queue server require 'thread' require 'drb/drb' # (1) queue = SizedQueue.new(10) # (2) DRb.start_service('druby://localhost:9999', queue) # (3) sleep # (4)
首先,我來解釋共享隊列服務器。
使用dRuby的應用必須通過加載'drb/drb'來啟動(1),接下來,一個帶有一定數量緩沖區元素的SizedQueue(2)被實例化。這時DRB服務就已經啟動了(3)。給定DRb.start_service的對象和服務的URI通過dRuby來公開。在這個例子中,SizedQueue對象在URI"druby://localhost:9999"這里公開。
任何通過dRuby創建的系統總要有一個指定系統入口的對象。這個對象被稱做前端對象。
最后服務通過調用sleep停止,不是退出。 即使主線程已經停止,服務也仍然可用,因為它一直在后臺線程中運行。
# producer require 'drb/drb' DRb.start_service #(1) queue = DRbObject.new_with_uri('druby://localhost:9999') #(2) 100.times do |n| sleep(rand) queue.push(n) #(3) end
# consumer require 'drb/drb' DRb.start_service queue = DRbObject.new_with_uri('druby://localhost:9999') 100.times do |n| sleep(rand) puts queue.pop #(4) end
類似客戶端的生產者與消費者應用程序也需要調用DRb.start_service (1),一個沒有參數的DRb.start_service方法調用說明應用程序沒有前端對象。注意,如果應用程序從不導出對象,那就沒有必要調用DRb.start_service方法。
接下來,分布式隊列的引用對象進行實例化(2)。DRbObjects 是一個遠程對象的代理引用。DRbObject 通過一個指定了另一個URI對象引用的URI來實例化。
消息就在這個時候被發送到遠程對象中(3)。
這個示例需要準備三個終端才能執行,并且要按順序在各個終端中運行腳本。不需要其他額外的設置。
% ruby queue.rb
% ruby consumer.rb
% ruby producer.rb
這個例子簡單的展示了如何只用幾行代碼在不同進程之間共享Ruby對象。
用dRuby來寫分布式系統很容易,就像使用Ruby寫應用程序很容易一樣兒。dRuby不僅適于寫原型或者學習體系結構的分布式系統,而且這些系統也可以用在產品實現中。
這節里使用第一版的dRuby代碼來解釋dRuby的RMI實現。第一版的dRuby代碼易于理解,不像高版本dRuby那樣復雜。
當消息被發送至 DRbObject(遠程對象引用)時,消息與接收器標識一起最先到達 dRuby 服務器。dRuby 服務器使用接收器標識查找對象并調用方法。下面一起看一下 dRuby 第一個版本的 DRbObject 類實現。
class DRbObject def initialize(obj, uri=nil) @uri = uri || DRb.uri @ref = obj.id if obj end def method_missing(msg_id, *a) succ, result = DRbConn.new(@uri).send_message(self, msg_id, *a) raise result if ! succ result end attr :ref end
DRbObject 類中只定義了一個名為 method_missing 的方法。在 Ruby 中,當接受器對象收到一個未知方法的時候就會調用 method_missing 方法。也就是“一個未知的丟了的方法被調用了”的意思。DRbObject 中的 method_missing 方法連接服務器,并發送相應的接收器的 ID 和調用的方法名,同時返回結果。
換言之,任何沒有在 DRbObject 中定義的方法都會被轉發到遠程對象上去。
就像之前的腳本中的"pop"和"push"方法的例子。這些方法都不會在 DRbObject 中定義, 所以method_missin 就會被調用,同時消息被轉發到由初始化時 URI 指定的 dRuby 服務器上。
class DRbServer ... def proc ns = @soc.accept Thread.start(ns) do |s| begin begin ro, msg, argv = recv_request(s) if ro and ro.ref obj = ObjectSpace._id2ref(ro.ref) else obj = DRb.front end result = obj.__send__(msg.intern, *argv) succ = true rescue result = $! succ = false end send_reply(s, succ, result) ensure s.close if s end end end ... end
上面的偽代碼描述了 dRuby 服務器的主要的行為。
在"accpet"一個 socket (套接字)之后,創建一個新的線程接收對象標識符,消息還有參數。通過標識符找到對應的對象之后,所有接收到的消息都會被發送給這個對象。
在大多數分布式對象系統,通用的是為每個接收類代理定義可轉發方法做準備。在 dRuby 中,Ruby 中的 method_missing 用法和運行時方法回調機制允許一個單一的代理作為所有的類的根客戶端。
當遠程接收器(例如:面向對象)接收到一個消息沒有處理,它會拋出一個 NameError 異常——這樣的行為是作為一個常規的 Ruby 對象的。
注意,要為每一個單獨的消息創建一個線程。創建線程有很多優點,這樣可以通過提供獨立的代碼執行上下文并避免網絡 IO 阻塞來減少異常的發生。但是對用戶最大好處就是允許多個 RMI (遠程方法調用)。
這就是生產者-消費者問題導致死鎖的原因。當由消費者調用 pop 而鎖定 DRbServer 時,這時生產者仍然可以進行 push 操作。甚至在 dRuby 正在進行 RMI 的時候,它還可以再進行 RMI。因此,回調執行方式就是迭代器和遞歸調用在 dRuby 中進行,同時線程同步機制的使用可以跨進程。
3.2 消息格式
這節會描述一下消息格式和對象交換機制。
dRuby使用TCP/IP協議來處理網絡通信,使用Marshal類庫來處理編碼。
Marshal庫是一個唯一對象序列化庫,是Ruby核心類庫的一部分,從提供的對象開始,Marshal跟蹤所有相關聯的引用,并最終序列化整個在這個關聯圖中的對象。
dRuby對Marshal重度依賴,在一些系統中,只有對象有“可序列化”屬性才會被認為可以序列化。但是在dRuby中,所有的對象初始化時都被認為可以被序列化。如果dRuby遇到沒有必要進行序列化的對象或者無法序列化的對象(如文件,線程,進程對象等)就會拋出一個異常。
前面請求的遠程對象的內容就是由下面的信息集合構成。每個組件都是通過 Marshal 序列化過的對象。
-
接收器標識
-
消息字符串
-
參數表
下面我們看一下第一版的實現。最后一版實現還會發送內容長度,但是與現在沒有什么太大區別。
def dump(obj, soc) begin str = Marshal::dump(obj) # (1) rescue ro = DRbObject.new(obj) # (2) str = Marshal::dump(ro) # (3) end soc.write(str) if soc return str end
這一小段代碼是不同于 dRuby 實現的一部分。
首先,作為參數傳遞過來的經過Marshal.dump序列化的對象,這是會拋出一個異常,這個對象的引用被Marshal.dump序列化了。
如果Marshal.dump失敗,說明目標對象是不可序列化的,或者是目標對象引用了一個不可序列化的對象。
這個例子中dRuby不允許RMI失敗。不可序列化的對象,或者不可以通過傳值來傳遞的對象,都需要傳遞對象的引用。
這種行為是 dRuby 用來縮小 dRuby 和 vanilla Ruby 之間的差異的一種技巧。
讓我們來看看實際應用。
在這個例子當中,我們將準備一個共享的字典。在字典里一個 service 注冊 service,而另一個 service 使用字典里的 service。
首先我們先來解決字典 service。按照分布式系統的說法,我們可以稱它為命名服務器。
由于我們只做了一個 Hash public,所以他看起來非常短。
# dict.rb require 'drb/drb' DRb.start_service('druby://localhost:23456', Hash.new) sleep
接下來是日志服務。這是一個非常簡單的只記錄時間和字符串的服務。
SimpleLogger 類定義在主邏輯里。
運行 logger.rb 就會注冊一個 SimpleLogger 對象和一個對應的描述到字典服務里,同時進入 sleep。
由于 SimpleLogger 會引用一個文件對象(在下面這個腳本里是標準錯誤輸出),所以不能通過Marshal.dump 來進行序列化。因此,不能通過傳值,只能通過傳引用來傳遞。
# logger.rb require 'drb/drb' require 'monitor' DRb.start_service dict = DRbObject.new_with_uri('druby://localhost:23456') class SimpleLogger include MonitorMixin def initialize(stream=$stderr) super() @stream = stream end def log(str) s = "#{Time.now}: #{str}" synchronize do @stream.puts(s) end end end logger = SimpleLogger.new dict['logger'] = logger dict['logger info'] = "SimpleLogger is here." sleep
最后我解釋一下服務用戶。
app.rb 腳本創建了一個字典服務的引用,并通過日志描述字符串索引到日志服務。在用 p 方法檢查每個對象之后可以發現 Info 對象就是一個簡單的內容是“SimpleLogger is her”的字符串對象,同時還看得出日志對象就是一個 DRbObject。
# app.rb require 'drb/drb' DRb.start_service dict = DRbObject.new_with_uri('druby://localhost:23456') info = dict['logger info'] logger = dict['logger'] p info #=> "SimpleLogger is here." p logger #=> #<DRb::DRbObject:0x....> logger.log("Hello, World.") logger.log("Hello, Again.")
logger.log() 是一個生成輸出日志的 RMI。我們可以通過在終端運行 logger.rb 腳本來查看日志輸出。
這章里我們用一個實用的例子介紹一下 dRuby 兩個不同特性的實現:方法調用的實現,還有對象傳輸方法選擇策略和消息格式的實現。
摘錄 dRuby 的第一個版本的實現來解釋,但是讀者應該知道這個道理,第一個版本與現在的版本實現原理是完全相同的。dRuby 的第一個版本最適合來講述代碼實現。
4.性能
這章我們通過測試結果討論一下dRuby的RMI性能。
下面的試驗標示了在同一個機器進程之間最大可能數量的RMI。這個試驗的結果應該是理想情況下的,并非是常規使用情況下的結果。這個結果也不失為dRuby中的使用開銷的一個很好的參考。
require 'drb/drb' class Test def count(n) n.times do |x| yield(x) end end end DRb.start_service('druby://yourhost:32100', Test.new) sleep
require 'drb/drb' DRb.start_service(nil, nil) ro = DRbObject.new_with_uri('druby://yourhost:32100') ro.count(10000) {|x| x}
我在二個不同的環境中測試10000次遠程調用所需要的時間。
第一個例子是宿主系統與運行于宿主系統虛擬機里的訪客系統之間的傳輸測試。宿主系統在同一個機器上(奔騰4主頻3.0GHz)。二種測試組合分別是,Ruby在Windowx XP中,Ruby在coLinux中分別做為訪客系統運行在Windows XP的宿主系統上。
% time ruby count.rb real 0m11.250s user 0m0.810s sys 0m0.260s
下面是沒有使用RMI,只是在同一個進程中測試常規方法調用的結果,
% time ruby count.rb real 0m0.044s user 0m0.040s sys 0m0.010s
下面二個是一臺iMac G5運行測試結果,第一個是在同一個系統中的不同進程的測試結果,第二個是在同一個進程中常規方法調用的測試結果
real 0m13.858s user 0m6.517s sys 0m1.032s
real 0m0.079s user 0m0.031s sys 0m0.012s
每秒700-900個遠程調用還是沒問題的。是否滿足你的應用程序的需要由你來決定。注意如果只是單純的在一個進程里進行常規方法的調用(沒有遠程方法調用)大約會比帶RMI的快200倍。RMI(遠程方法調用)的頻率會對應用程序性能產生很大的影響,所以在構建真正的產品應用程序時要著重考慮這點。
5. 應用
只有很少的幾個應用是通過dRuby來實現的。
眾所周知,Ruby on Rails的調試器和Web應用的異步進程模型通常是通過dRuby來實現的。
在這個章節里,我會介紹幾個由dRuby來實現的應用,在介紹應用之前,我還是會先說一下基于dRuby的分布式系統,Rinda和一些實際的例子。
5.1. 大型程序的后端服務
現在,我先通過 Hatena 截圖服務介紹一下大型應用程序的后端服務。Hatena 截圖服務是由 Tateno 在上 Ruby Kaigi 2006 發布的。這個截圖服務是用來顯示類似 Hatena 博客服務的注冊鏈接的截圖縮略圖服務。
Web 前端配置在 Linux 上,截圖功能是基于 IE 的組件來實現的。因為據說在較高的速度下 Windows 下的截圖性能更好一些。
這個截圖服務是做為異步進程執行在前端的。
運行在Windows上的進程接收來自通過dRuby處理過的Linux進程的URL和的方法,并執行截圖方法。
根據2006年RubyKaigi提供的數據,這個由二臺機器組成的并行系統可以處理的數據吞吐量已經接近120SS/分鐘,170,000SS/天。
5.2. 內存數據庫還是持久進程
接下來,我將要介紹 dRuby 持久內存或者叫持久進程的用法。Web 應用里的很多問題都與需要處理短生命周期的請求回應循環和語義進程有關系。
一個很具體的例子就是 CGI。CGI 程序在接收到一個請求之后被調用,調用完成之后返回一個響應。在用戶從他們的瀏覽器角度看起來,應用程序響應了很長時間。由于是一系列的小而短的請求回應循環,所以看起來像是一個長駐應用,每個 CGI 程序在終結之前必須給下一次調用留下一些“有用的信息”。
這些"wills"(會話管理)的管理是 web 應用編程的痛點之一。很多因素需要被考慮--狀態串行化,處理文件或者關系型數據庫相互排斥,處理多重同時請求引起的矛盾。
有一個方法就是最小化甚至消除,“will”通過合并短暫的前端和長久的持久應用來留給下一個進程。
RWiki 是一個有趣的 WikiWikiWeb 實現,它應用了這樣的架構。
元數據比如 Wiki 頁面來源,HTML 輸出,連接和更新時間戳的 cache,都維持在一個長久服務器的進程內存中。短暫的 CGI 進程通過 dRuby 訪問找個服務器,檢索用戶請求的 Wiki 頁面。一個私有的 RWiki 服務器 host 大概在內存中有 20000 頁。為了能夠重啟后重新建立站點,服務器要不斷的記錄充足的數據到硬盤里邊。然而這些日志在正常的執行中從不被引用。
下面的例是一個特別簡單的 CGI 腳本(四步),還有一個“計數”服務器。我們先來快速的過一遍 CGI的運行機制。CGI 程序接受一個通過標準輸入和環境變量產生的 CGI 環境下的 http 請求,并通過標準輸出返回一個對此請求的響應。
CGI 腳本調用相對長時的計數服務器,并將環境變量和輸入輸出引用傳遞過去。CGI 應用不會使用暫存模式,特別容易編寫,可以用其他例子代替這個簡單的計數的例子。
#!/usr/local/bin/ruby require 'drb/'drb' DRb.start_service('druby://localhost:0') ro = DRbObject.new_with_uri('druby://localhost:12321') ro.start(ENV.to_hash, $stdin, $stdout)
require 'webrick/cgi' require 'drb/drb' require 'thread' class SimpleCountCGI < WEBrick::CGI def initialize super @count = Queue.new @count.push(1) end def count value = @count.pop ensure @count.push(value + 1) end def do_GET(req, res) res['content-type'] = 'text/plain' res.body = count.to_s end end DRb.start_service('druby://localhost:12321', SimpleCountCGI.new) sleep
6. Rinda 和 Linda
最后我要介紹一下基于 dRuby 的 Linda 的實現,還有 Rinda 和它的實際使用例子。
Linda 在分布式的協作系統就是一個膠水語言的概念。一個簡單的元組和元組空間模型能夠協調多個任務。也就是說,即使這是一個簡單的模型,也可以管理由并行編程引發的各種情況。因此,許多語言都納入自己的元組空間。
接下來從下面的實現就可以看出 C-Linda、 JavaSpace 和 Rinda 都是典型的例子。
C-Linda 增強了一門基本語言(C 語言)并且加入了 Linda 的一些操作。它們都是通過執行預處理器來實現的。說起 JavaSpace 的實現,它的元組空間是由 java 來實現的。dRuby 的元組空間是由 Rinda 實現的。也就是說,Rinda 用 Ruby 實現了 dRuby 的元組和元組空間模型。
在 C-Linda 中,操作元組是被元組空間隱含限制的,所以元組空間不能手動指定。而在 Rinda 里,由于元組和對象都是通過消息傳遞來通信,所以必須要決定應用使用哪一種元組空間。
在 Rinda 里,除了 Linda 的 eval 操作之外,其他基本操作如 out,in,inp,rd,rdp 都是可用的。但是可以取代 Ruby 的線程。
最新版本的 Rinda 修改成了類似 JavaSpace 的方法名。
-
write
-
向元組空間加入元組(out)
-
take
-
在元組空間刪除對應的元組,并返回對應的元組,如果元組不存在,鎖定它們。(in)
-
read
-
返回元組拷貝,如果對應元組不存在,鎖定它們。(rd)
take 和 read 操作可以設置超時。如果設置超時為 0,這兩個的操作類似 inp 和 rdp。
除了這些基本的操作之外,read_all 也可以讀取所有匹配模式的 tuple。read_all 似乎對調試有用處。
tuple 和 pattern 通過 Ruby 數組來表示。匹配 tuple pattern 的規則被擴大到和 Ruby 相似的規則,以致于不僅是通配符(Rinda 中的通配符表示 nil),而且有類,此外 Range 和 Regexp 也可以被指定。Rinda 可以像一組查詢語言一樣被處理。
此外,“時間限制”可以在 tuple 中設置,盡管這個功能還處于試驗性。還有,表示秒數和時間線的更新對象(Rinda 里邊稱為重建者)的數字也可以在時間限制中被指定。無論更新時間線與否都要被對象所詢問。Rinda 作為重建者也能夠給 dRuby 提供引用參考。打比方,tuple 創建者被不正常關閉。過了一段時間后,超過時間限制的 tuple 會被提供。
6.1. 哲學家就餐問題
在 Linda 角度我用實例代碼來為你解釋這個問題。
require 'rinda/tuplespace' # (1) class Phil def initialize(ts, num, size) # (2) @ts = ts @left = num @right = (@left + 1) % size @status = ' ' end attr_reader :status def think @status = 'T' sleep(rand) @status = '_' end def eat @status = 'E' sleep(rand) @status = '.' end def main_loop # (3) while true think @ts.take([:room_ticket]) @ts.take([:chopstick, @left]) @ts.take([:chopstick, @right]) eat @ts.write([:chopstick, @left]) @ts.write([:chopstick, @right]) @ts.write([:room_ticket]) end end end ts = Rinda::TupleSpace.new # (4) size = 10 phil = [] size.times do |n| phil[n] = Phil.new(ts, n, size) Thread.start(n) do |x| # (5) phil[x].main_loop end ts.write([:chopstick, n]) end (size - 1).times do ts.write([:room_ticket]) end while true # (6) sleep 0.3 puts phil.collect {|x| x.status}.join(" ") end
這里開始介紹眾所周知的"哲學家就餐"問題。
這個程序使用了二種元組類型。一個是 chopstick,另一個是 room_ticket。
chopstick 是一個含有二個元素的元組。第一個元素是":chopstick"標記,第二個元素是一個表示chopstick 編號的整型數字。room_ticket 是一個限制房間里哲學家人數的標簽元組,里面只包含一個":room_ticket"標記。
Phil 類表示一個哲學家。
Phil 對象與元組空間,各種數字,桌子的座位數一起生成。這個對象有一個實例變量表示其狀態。這些變量用來監聽哲學家們的狀態。
與 C-Linda 相比,Rinda 必須傳遞目標元組空間。
Phil 類中的 main_loop 方法是用來標識哲學家動作的無限循環。在執行 think() 方法后,main_loop 方法獲得用餐的 room_ticket,同時還獲得了左右兩邊的筷子。所有的都準備好之后,開始執行 eat() 方法。用餐結束之后,返還左右兩邊的筷子與 room_ticket 至元組空間,同時再次開始執行 think() 方法。
在主程序中,首先創建元組空間和 philosopher,同時由子線程調用 main_loop 方法。這些操作與 C-Linda 中的 eval() 操作非常類似。與人數相應的筷子數量和 room_ticket 元組中的小寫數字被寫入元組空間中。
最后的無限循環每 0.3 秒為一組來監聽哲學家。在循環中指出哲學家在某一刻是在思考,就餐還是在拿著筷子等行為。
6.2. Tuple and Pattern
這里,我解釋一下 Rinda 中的 tuple,pattern 已經模式匹配。之前提到的,那些 tuple 和 pattern 用數組表示。元素中有很多特征。關于這些元素,所有的 Ruby 對象包括 dRuby 的引用都可以被指定。
[:chopstick, 2] [:room_ticket] ['abc', 2, 5] [:matrix, 1.6, 3.14] ['family', 'is-sister', 'Carolyn', 'Elinor']
同樣的 pattern,所有 Ruby 對象可以被作為元素。在模式匹配方面,它的規則有點奇怪。nil 被解釋為可以匹配任何對象的通配符,并且每個元素通過"===(實例等于)"來對比。
Ruby 有一個 case 表達式,這種表達式是一個分支,像 C 語言的 switch。"===(實例等于)"是一個特殊的等式比較。然而,“===”和”==“在某一類中基本相似,它表現”像 pattern“。
例如,Regexp 只不過是字符串的模式匹配,Range 識別值是否在限定的 range 里邊。當一個類被指定為 pattern 元素,使用 kind_of() 也是合理的,以致于帶有指定類的 pattern 也可以被描述。
下面給出 pattern 的例子。
[/^A/, nil, nil] (1) [:matrix, Numeric, Numeric] (2) ['family', 'is-sister', 'Carolyn', nil] (3) [nil, 'age', (0..18)] (4)
-
一個由三個元素組成的 tuple,第一個元素使用”A“開頭的字符串。
-
一個由第一個元素是 matrix 標志,第二,第三個元素都是 Numveric 類排列而成的 tuple 。
-
Tuple of Carolyn sister。
-
年齡從0到18的 tuple。
讓我們看一下通過共享 TupleSpace 和交互環境irb匹配的模式。
require 'rinda/tuplespace' ts = Rinda::TupleSpace.new DRb.start_service('druby://localhost:12121', ts) sleep
這4行代碼就是共享 TupleSpace 的腳本。
% irb --simple-prompt -r rinda/rinda >> DRb.start_service >> ro = DRbObject.new_with_uri('druby://localhost:12121') >> ts = Rinda::TupleSpaceProxy.new(ro) >> ts.write(['seki', 'age', 20]) >> ts.write(['sougo', 'age', 18]) >> ts.write(['leonard', 'age', 18]) >> ts.read_all([nil, 'age', 0..19]) => [["sougo", "age", 18], ["leonard", "age", 18]] >> ts.read_all([/^s/, 'age', nil]) => [["seki", "age", 20], ["sougo", "age", 18]] >> exit
你可以看到類 Ruby 的靈活的模式是可用的。
你可能會考慮元組空間可以當簡單的數據庫來用。在這點上,數量巨大的元組必須要謹慎的處理,根據 API 和靈活模式的特性,Rinda 會進行線性的搜索。
6.3. 非均衡優化
僅僅當第一個元素是一個符號、存儲元組的集合時,為了以高速通過通用的應用程序,最新版本的 Rinda 已經實施了非均衡優化。
根據我的經驗,以下類型是應用程序經常使用的元組。
[:screenshot, 12345, "http://www.druby.org"] [:result_screenshot, 12345, true] [:prime, 10]
也就是說,它是由一個消息類型和一些參數組成的元組。在接受或讀取時,下列模式適用。
[:screenshot, nil, String] [:result_screenshot, 12345, nil] [:prime, Numeric]
這只是一個模式,以任何一個從一個特定消息類型的元組。
考慮到這種情況,你可以通過將第一個元素作為自己的集合,并集中在搜索目標上,這樣就可以期望高性能。還有另外一個問題是,是否有任意對象成為實現高性能的關鍵點。在 Rinda 情況下,使用“= = =(情況匹配)”不適用于 String 和 Integer 作為鍵。
然而,符號(Symbol)是仍然適合作為一個關鍵點的,因為“===(情況匹配)”的符號已經有一個符號(Symbol)類和它的值。此外,符號(Symbol)比字符串更易于閱讀。
讓我們來總結一下這種非均衡的優化。在最新版本的 Rinda 里,當在元組第一個元素是一個符號(Symbol)時,Rinda 執行這種非均衡的優化,并且在取/讀的性能上是有改進的。類似取/讀,當一個模式搜索的第一個元素是一個符號(Symbol)時,搜索的性能會變得比通常要快。
6.4. Rinda 的應用
這里,我將介紹一個Rinda的實際應用。Buzztter是一個翻譯推ter句子的網絡服務。推ter是一個微博社交網站。Buzztter收集發到推ter上的微博,將其翻譯,并找出比平時更常用的單詞。通過這些,Buzztter得到了當前推ter中單詞的總趨勢。
Buzztter由數個子系統組成,分別是一個分布式爬蟲子系統;一個通過推ter API(HTTP)收集微博的子系統;這個子系統中使用了Rinda。爬蟲子系統是由多個從推ter中抓取信息的的抓取器和導入器組成,導入器可以使信息持久保存。Rinda和dRuby是抓取器和導入器之間的中介器。
作為參考,Buzztte 在 2007 年十一月三日處理的數據量如下:
-
每天 125000 條
-
每天 72MB
6.5. Rinda 更新
最后,我再說一下 Rinda 的最新更新趨勢。去年的 RubyKaigi 2007 中,發布了持續性元組空間。一旦過程完成,關于 Rinda::TupleSpace 的信息就消失了。持續性 TupleSpace 在過程調用時開辟了一個連續的元組空間。在運行過程中,為了為再次調用這個過程時做準備,持續性 TupleSpace 將保持記錄。再次調用時,查詢日志信息,TupleSpace 將重建過程。運行中,TupleSpace 只需要保持記錄,這意味著不需要從存儲器中讀取內容。
7. 結論
我論述了 dRuby 的設計策略,其概念的實現,以及它的實際使用。除此之外,我還論述了基于 dRuby 和 Rinda 開發的實現。dRuby 和 Rinda 都是設計成一個簡單的系統從而讓 Ruby 程序員輕松的了解更多。所以,它們最適合簡單的構建分布式系統。然而,在這篇文章中討論的實際例子不是像玩具一樣討論其簡單為目的,而是證明了 dRuby 和 Rinda 幾乎可用于構建實際應用程序的基礎設施。