Cloud Foundry Services源碼分析之Node
在Cloud Foundry中Service的結構不是太複雜,由兩個組件組成——Gateway、Node。如圖1展示了Service相關的幾個主要組件,每個組件有十分明確的分工:
圖 1 Service相關主要組件
Gateway |
其它組件(Cloud Controller)訪問Node的入口,它對外提供了對Node進行管理的一套“接口”。同時它對外隱藏內部Node的結構,這樣外部的組件就可以忽略內部Node的情況,只需要關心Service實例的創建、綁定的動作。 |
Node |
負責管理Service,包括創建(provision)、註銷(unprovision)、綁定(bind)、啓用(enable)、禁用(disabled)等操作。Node不是Service的提供者,它是本地Service的管理者。 |
NATS |
最底層的消息總線,各大組件間的通信都由它進行轉發。是一個P/S結構的消息總線,所以在源碼中會經常見到調用subscribe、publish方法。 |
DEA |
APP運行的容器,Service就是爲APP提供服務。在源碼中會見到Node中提供bind方法,就是爲了將創建的Service實例與APP綁定,APP能夠訪問Service實例。 |
Cloud Controller |
Cloud Foundry最核心的控制大腦。它接受各個組件和Client的請求進行處理,然後通過NATS向相應的組件發送指令,要求其執行。在源碼中Node接收到如provision這樣的請求,這些請求是由Cloud Controller發送給Gateway,Gateway經過負載均衡後再向Node發出。 |
本文主要介紹Cloud Foundry中Node的實現,圖2繪製了CloudFoundry中Services中Node點的類圖。
注意:Echo::XXX並不屬於CloudFoundry中的內容,而是一個簡單的Service的Node實現,也是本文中主要進行分析的Service實現。添加Echo Service只要將自定義的Echo::NodeBin通過配置到啓動文件中,就可以在啓動Cloud Foundry的時候自啓動自定義Service。
整個Service Node分爲2個層次結構,一部分是CloudFoundry提供的Base模板,如類圖中的Base::XXX部分,這部分已經幫助Service開發人員完成了大部分Service的開發工作;另一部分是XXX Service部分,這部分就是Service開發人員所需要進行編寫的代碼,如類圖中的Echo::XXX。
圖中每個類都有其明確的職責,在下文其餘部分會一點點進行分析。
圖2 Echo Node類圖
圖2中類圖分爲兩個層次。其中Base下的類都是系統提供的ServiceNode模板,也是下文中重點分析對象;自定義Service就是通過繼承NodeBin和Node兩個類,然後實現模板中預留的接口,在最後我們將會詳細介紹如何去編寫這部分的代碼,這也是Service開發人員需要完成的部分。表1介紹了每個類實現的功能。
表 1 類功能說明
類名 |
功能 |
Base::Base |
提供了整個Service通信框架,主要是提供了NATS的連接與初始化。 |
Base::Node |
實現了Service Node功能模板,預留了大量的抽象方法便於Service開發者實現自定義的功能。 |
Base::NodeBin |
主要完成Service Node的初始化配置,然後將配置參數傳入Base::Node。 |
Echo::NodeBin |
需要實現Base::NodeBin下的兩個指明調用的Node點以及配置文件的方法即可。 |
Echo::Node |
需要實現Base::Node下的抽象方法,實現Service Node的實例創建、管理、註銷、重啓等操作。 |
第一部分:源碼分析
源碼之間的關係錯綜複雜,很多地方都有一種剪不斷理還亂的感覺,所以在閱讀該部分內容時,建議:
² 一邊閱讀源碼,一邊閱讀本文。文中引用代碼都給出了“路徑名 #方法名”的格式。路徑名是Cloud Foundry在GitHub上面的路徑,源碼直接到https://github.com/cloudfoundry上查閱。
² 文中引用代碼都儘量截取了我感興趣的部分,這部分會以灰色背景標識;此外一些地方代碼被稍微修改。
² 在講述某個方面內容時,爲保證內容儘可能不跑題,所以相關的內容都會給出其可以參考的章節,這些地方都會以【XXX】的方式註明。如果看不懂了,可以跳躍到相應內容查看。
² 如果還看不懂,建議上網查閱相應資料。
² 文中不免有大量表述不清晰、謬誤之處,歡迎發送郵件[email protected]。
文中主要以Echo Service爲例,其他的Service也是一樣的道理。
圖2-1 Node啓動流程
以Echo Service爲例,圖2-1描繪了Node的啓動流程。Service中Node的啓動過程其實不是太複雜,真正複雜的是配置參數與相應主題的功能。
注意:步驟中創建Echo::XXX實例不能算作啓動流程的主要部分,它們僅僅只是調用了各自的new方法而已,描述它們是爲了標識流程的執行方向才加入說明。
Service Node的啓動入口位於自定義Service目錄中bin目錄下對應ServiceNode的可執行文件中,如代碼2.1所示,啓動Service Node代碼看似簡單,實際它後續的工作卻是很複雜。
代碼2.1:vcap-services / echo /bin /echo_node |
VCAP::Services::Echo::NodeBin.new.start |
因爲Echo::NodeBin繼承自Base::NodeBin,所以它也繼承了Base::NodeBin所有的功能。創建Echo::NodeBin實例的過程就在代碼2.1中——調用new方法創建Echo::NodeBin實例。創建後的實例調用的start方法是在Base::NodeBin方法中實現。start方法中就包含了啓動流程中後續的所有過程。
注意:Service開發者不需要去重新實現該方法,啓動過程中需要附加的一些功能,Cloud Foundry提供了2個Hook方法給Service開發者。分別爲【pre_send_announcement】方法和【additional_config】方法。
Echo::NodeBin調用start方法定義在Base::NodeBin中,執行的第一個步驟就是進行所有參數的初始化。初始化參數配置代碼過程很簡單,就是確定配置文件->載入配置文件->初始化參數列表。
如代碼2.2所示:首先需要確定默認的配置文件,對於default_config_file方法的說明可以查看【default_config_file】。OptionParser部分的內容一般不會得到執行,如果用戶指定了啓動過程中的配置文件,則會使用新的配置文件,否則使用默認配置文件。當然一般情況下這個opt參數是沒有進行指定的,如果需要指定,可以對啓動腳本進行修改。
代碼2.2:vcap-services-base / lib /base /node_bin.rb #start |
def start config_file = default_config_file OptionParser.newdo |opts| opts.banner ="Usage: #{$0.split(/\//)[-1]}[options]" opts.on("-c","--config [ARG]","Configuration File")do |opt| config_file= opt end …… |
然後載入配置文件,如代碼2.3所示。配置文件採用YAML格式存儲,最後得到的config中存儲的是一個Hash表。
代碼2.3:vcap-services-base / lib /base /node_bin.rb #start |
config = YAML.load_file(config_file) |
最後進行參數配置,如代碼2.4所示。需要配置的參數內容非常複雜,參數的配置主要分爲4個部分,除去Node需要的基本配置參數外,還包含有Warden的配置參數,日誌文件的配置參數以及附加參數,詳細部分參看【配置參數】。
代碼2.4:vcap-services-base / lib /base /node_bin.rb #start |
options = { :index=> parse_property(config,"index",Integer,:optional => true), …… # Wardenized service configuration :base_dir=> parse_property(config,"base_dir",String,:optional => true), …… }
# Workaround for services that support running the service both inside and outside warden use_warden = parse_property(config,"use_warden",Boolean,:optional => true, :default=> false) if use_warden warden_config = parse_property(config,"warden",Hash, :optional=> true) …… options[:port_range]= parse_property(warden_config,"port_range",Range) …… end
VCAP::Logging.setup_from_config(config["logging"]) # Use the node id for logger identity name. options[:logger]=VCAP::Logging.logger(options[:node_id]) @logger= options[:logger]
|
注意:在代碼2.4最後一行表示Service開發者也可以添加自定義的參數。在Base::NodeBin中提供了抽象方法additional_options來方便service開發者添加自己所需要的參數,詳細部分參考【additional_config】
如代碼2.5所示,在Base::NodeBin中start方法的結束部分開始創建一個Echo::Node實例。EM是指的是eventmachine,是一個異步事件處理機,與Node.js類似,更多內容參考【NATS與Event Machine】。
注意:其中node_class方法與default_config_file方法一樣需要在Echo::NodeBin中進行重寫,詳細說明見【node_class】。
代碼2.5:vcap-services-base / lib /base /node_bin.rb #start |
EM.rundo node = node_class.new(options) …… end end |
在Echo Service中node_class方法返回的就是Echo::Node,所以調用new方法創建時調用的就是Echo::Node的initialize方法,如代碼2.6所示。在Echo::Node的initialize方法中可以不做其它工作,直接調用父類中的initialize方法即可。
注意:該方法必須使用super調用父類(Base::Node)中的initialize方法。因爲訂閱主題的主題工作是在Base::Node的on_connect_node方法中進行實現,同時連接到NATS與工作也是在Base::Node的父類(Base::Base)中完成。
代碼2.6:vcap-services / echo /lib /echo_service /echo_node.rb #initialize |
def initialize(options) super(options) end |
連接到NATS是在在Base::Base的initialize方法中完成。但是在調用Base::Base的initialize方法之前,事先調用的是Base::Node中的initialize方法,如代碼2.7所示。而在Base::Node中最開始則會調用Base::Base中的initialize方法連接到NATS。
代碼2.7:vcap-services-base / lib /base /node.rb #initialize |
def initialize(options) super(options) |
連接到NATS看似複雜,但是對於所有的連接到NATS的節點來說,調用流程是一樣。如代碼2.9所示,調用NATS的connect方法連接到NATS,在其代碼塊中:
² 向Component註冊【--?--】,參考【週期任務】中關於update_varz的說明;
² 調用on_connect_node方法訂閱主題,參考【訂閱主題】。
代碼2.9:vcap-services-base / lib /base /base.rb #initialize |
if options[:mbus] …… @node_nats=NATS.connect(:uri=> options[:mbus])do status_port = status_user= status_password= nil if not options[:status].nil? status_port = options[:status][:port] status_user = options[:status][:user] status_password = options[:status][:password] end
@logger.debug("Registering with NATS") VCAP::Component.register(:nats=>@node_nats,:type=> service_description, :host=>@local_ip,:index=> options[:index]||0,:config=> options, :port=> status_port,:user=> status_user,:password=> status_password) on_connect_node end |
Base::Base提供了一個連接到NATS的通信框架。訂閱主題是在Base::Base的initialize方法中調用on_connect_node方法進行訂閱,參見代碼2.9中倒數第二行。
注意,不要將on_connect_node理解成是創建與NATS的連接過程,它做的事情是ServiceNode向NATS訂閱主題以及添加週期任務(參考【添加週期任務】)。
如代碼2.10所示,根據對於該方法的註釋我們知道:
² 該方法必須在Base::Node和Base::Provision中進行重寫;
² 自定義Services中不能重寫該方法。
代碼2.10:vcap-services-base / lib /base /base.rb |
# Subclasses VCAP::Services::Base::{Node,Provisioner} implement the # following methods. (Note that actual service Provisioner or Node # implementations should NOT need to touch these!) # TODO on_connect_node should be on_connect_nats abstract :on_connect_node |
訂閱主題過程在Base::Node的on_connect_node方法中進行實現,如代碼2.11所示。這段代碼讀起來有些拗口,這部分內容將【主題】中展開討論,這裏只需要它訂閱了主題即可。
代碼2.11:vcap-services-base / lib /base /node.rb #on_connect_node |
def on_connect_node …… %w[provision unprovision bind unbind restore disable_instance enable_instance import_instance update_instance cleanupnfs_instance purge_orphan ].eachdo |op| eval%[@node_nats.subscribe("#{service_name}.#{op}.#{@node_id}") { |msg, reply| EM.defer{ on_#{op}(msg, reply) } }] end %w[discover check_orphan].eachdo |op| eval%[@node_nats.subscribe("#{service_name}.#{op}") { |msg, reply| EM.defer{ on_#{op}(msg, reply) } }] end |
在啓動流程的最後一部分工作就是添加週期任務,就是Service Node在之後正常運行過程中定期執行的動作。添加的週期任務有兩個:
² 向Gateway發送本地相關運行狀態,Gateway可以根據這些信息可以更方便的管理多個Service Node,實現Service Node間的均衡。
² 向Component註冊信息。
更多關於週期任務的內容參考【週期任務】。
如代碼2.12所示,其中一個在on_connect_node方法的最後會設置週期任務,這個週期任務就是實現了向Gateway發送本地相關的運行狀態信息。週期任務的執行體全部在send_node_announcement中完成。
代碼2.12:vcap-services-base / lib /base /node.rb #on_connect_node |
pre_send_announcement send_node_announcement EM.add_periodic_timer(30) { send_node_announcement } end |
如代碼2.13所示,另外一個則是在Base::Node的initialize方法最後,也就是連接到NATS之後,這個週期任務的執行體則是在update_varz中完成。
代碼2.13:vcap-services-base / lib /base /node.rb #initialize |
@supported_versions= options[:supported_versions]|| [] z_interval = options[:z_interval]||30 EM.add_periodic_timer(z_interval)do EM.defer { update_varz } end if @node_nats |
該章節內容簡要介紹了ServiceNode的啓動流程。
² 配置參數的初始化配置時Node啓動過程中最爲複雜的部分,無論是Service開發人員還是Cloud Foundry管理人員都需要花費大量精力去學習這部分的內容。
² 訂閱主題也是Node啓動過程中較爲複雜的一部分,而到了運行過程中,主題這部分就顯得尤爲重要。
² 週期任務看似複雜,但是需要Service開發人員注意的地方其實並不多,所以最後我們其實不需要太多的精力去學習它。
² 與NATS的連接也不需要花費太多的心思,它僅僅只是一個消息總線而已,只需要知道如何使用它發送和接收消息即可。
在後面的章節中,我們會分別展開這些部分的內容,一一分析其中的一些機制。
本章節內容主要講解Service Node中的參數配置,中間會重點介紹幾個在下文中經常見到的參數(個人感興趣)。要弄明白一個參數的作用需要“大膽假設,小心求證”,我沒辦法顧及所有情況,所以無法保證我下文中所寫都是正確的。如果覺得有什麼地方有問題,歡迎指正。
在【初始化參數配置】一節中,ServiceNode啓動最開始需要指定一個默認配置文件,在代碼2.2中使用default_config_file方法來指定這個文件的路徑。對於default_config_file方法的定義在Base::NodeBin中,如代碼3.1所示。對於abstract的說明參考【abstract】,我們可以將它的作用類比做C++中的純虛函數,在下文中會多次見到這樣的聲明。
代碼3.1:vcap-services-base / lib /base /node_bin.rb |
classVCAP::Services::Base::NodeBin abstract :default_config_file |
對於default_config_file方法說明參考表3-1。
表3-1default_config_file方法說明
函數名:default_config_file |
參數名稱 |
說明 |
輸入參數 |
- |
|
返回值 |
file path |
默認配置文件的路徑,例如:File.join(File.dirname(__FILE__), '..', 'config', 'XXX.yml') |
如代碼3.2所示,在實現EchoService的時候,就需要Echo::NodeBin中進行重寫default_config_file方法。重寫內容很簡單,只需要返回讀入的默認文件的路徑即可。
代碼3.2:vcap-services / echo /bin /echo_node |
classVCAP::Services::Echo::NodeBin<VCAP::Services::Base::NodeBin …… def default_config_file File.join(File.dirname(__FILE__),'..','config','echo_node.yml') end end |
注意:Service開發者必須實現該方法,實現格式參考代碼3.2即可。
在【初始化參數配置】一節中,Service開發者還可以按照自己的意願添加參數,而對於這些新增參數的初始化過程則是在additional_config方法中進行處理,其定義如代碼3.3所示。
代碼3.3:vcap-services-base / lib /base /node_bin.rb |
classVCAP::Services::Base::NodeBin abstract:additional_config |
在代碼中沒有指出該方法需要導入的參數類型,返回查看代碼2.4就可以知道這個方法需要導入兩個參數——options和config。整理後的方法說明如表3-2所示:
表3-2 additional_config方法說明
函數名:additional_config |
參數名稱 |
說明 |
輸入參數 |
options |
是一個Hash表,讀入的配置參數都會存儲在該表中。 |
config |
就是載入的配置文件中的信息內容,它其實就是一個Hash數組,參考【代碼2.3】。 |
|
返回值 |
options |
更新後的options表,與原來的options相比,新的options加入了在Service開發者需要的參數。 |
Service開發者只需要在實現該方法時候讀入config中新加入的參數即可。例如在Echo Service中,Echo需要使用一個新的參數port表示Service的服務端口號,則如代碼3.4所示從config中讀入參數即可。
注意:所有的配置參數都在config中存儲,最好使用【parse_property】方法從config中讀取參數。
代碼3.4:vcap-services / echo /bin /echo_node |
def additional_config(options, config) options[:port]= parse_property(config, "port",Integer) options end |
注意:Service開發者必須實現該方法,該方法存在兩個參數options和config,而且要求返回值爲更新後的options,實現格式參考代碼3.4即可。
在Base::NodeBin中將所有經過parse_property方法處理後的參數結果存入在options(一個Hash表)中。這個處理過程的格式如代碼3.5所示。
代碼3.5:vcap-services-base / lib /base /node_bin.rb |
options[:capacity] =parse_property(config,"capacity",Integer,:optional=>true,:default=>200) |
Service ServiceNode的配置參數很多,配置文件以YAML格式編寫(Cloud Foundry中經常見到YAML與JSON兩種數據交換格式,類似於XML),載入配置文件(參考【代碼2.3】)後的參數值保存在config變量中。config其實就是一個Hash數組,此時讀入的參數還爲經過格式轉換,在Base::NodeBin中提供了parse_property方法對對參數進行處理。對於parse_property方法說明如表3-3所示。
表3-3parse_property方法說明
函數名:parse_property |
參數名稱 |
說明 |
|
|
|
輸入參數 |
config |
就是載入的配置文件Hash數組,整個配置文件內容都在其內部保存。 |
|
|
|
key |
需要查找的參數鍵值,parse_property方法就是使用config[key]獲取到相關參數的值。 |
|
|
|
|
type |
參數最後的類型,經過parse_property方法,該參數最後會轉化爲type類型返回。 |
|
|
|
|
options |
附加選項,包括了:optional與:default兩個,詳細情況參考圖3-1。 |
|
|
|
|
返回值 |
value |
經過處理的參數值,類型爲type類型。 |
|
|
|
對於parse_property方法的定義如代碼3.6所示。
代碼3.6:vcap-services-base / lib /base /node_bin.rb |
def parse_property(hash, key, type, options= {}) obj = hash[key] if obj.nil? raise "Missing required option: #{key}"unless options[:optional] options[:default] elsif type ==Range raise "Invalid Range object: #{obj}"unless obj.kind_of?(Hash) first, last = obj["first"], obj["last"] raise "Invalid Range object: #{obj}"unless first.kind_of?(Integer)and last.kind_of?(Integer) Range.new(first, last) else raise "Invalid #{type}object: #{obj}"unless obj.kind_of?(type) obj end end |
代碼雖然不長,但是分支情況比較多,對於config中的參數有如下幾種處理情況,如圖3-1所示。
圖3-1 參數配置函數過程
² 如果在config中配置了該參數,並且不是一個Range類型的數據,就會讀取該參數的配置,並轉化爲type類型
² 如果在config中配置了該參數,而且是一個Range類型的數據,返回類型就是一個Range類型。
² 如果在config中沒有配置該參數,並且沒有加入:optional=true選項,則會報錯,表明該參數必須配置
² 如果在config中沒有配置該參數,並且加入:optional=true選項,此時如果參數必須有一個默認值,則返回輸入參數中的:default值。
² 如果在config中沒有配置該參數,並且加入:optional=true選項,此時如果參數不必須有一個默認值,則返回的參數是一個nil值。
參數很多很複雜,但不是要求每個參數都需要配置,很多參數其實是可選的,對於一些可選參數還提供了默認值。對於Service Node使用的參數整理後如表3-4、表3-5、表3-6所示。
表3-4整理了配置參數值的情況。
表3-4 全局參數配置表(不完全)
編號 |
參數名稱 |
options |
變量名 |
類型 |
可選 |
默認值 |
典型值 |
A01 |
index |
options[:index] |
|
Interger |
是 |
|
0 |
A02 |
plan |
options[:plan] |
@plan |
String |
是 |
free |
|
A03 |
capacity |
options[:capacity] |
@capacity @max_capacity |
Interger |
是 |
200 |
|
A04 |
ip_route |
options[:ip_route] |
|
String |
是 |
|
|
A05 |
node_id |
options[:node_id] |
@node_id options[:logger] |
String |
否 |
|
echo_node_1 |
A06 |
z_interval |
options[:z_interval] |
z_interval |
Interger |
是 |
30 |
|
A07 |
mbus |
options[:mbus] |
@node_nats |
String |
否 |
|
nats://localhost:4222 |
A08 |
local_db |
options[:local_db] |
|
String |
否 |
|
sqlite3:/var/vcap/services/echo/echo_node.db |
A09 |
migration_nfs |
options[:migration_nfs] |
@migration_nfs |
String |
是 |
|
|
A10 |
max_nats_payload |
options[:max_nats_payload] |
|
Interger |
是 |
|
|
A11 |
fqdn_hosts |
options[:fqdn_hosts] |
@fqdn_hosts |
Boolen |
是 |
FALSE |
|
A12 |
op_time_limit |
options[:op_time_limit] |
@op_time_limit |
Interger |
是 |
6 |
|
A13 |
supported_version |
options[:supported_version] |
@supported_versions |
Array |
否 |
|
["1.0"] |
A14 |
default_version |
options[:default_version] |
|
String |
否 |
|
"1.0" |
A15 |
max_clients |
options[:max_clients] |
|
Interger |
是 |
|
|
A16 |
database_lock_file |
options[:database_lock_file] |
|
String |
是 |
|
|
A17 |
disabled_file |
options[:disabled_file] |
@disabled_file |
String |
是 |
"/var/vcap/store/DISABLED" |
|
A18 |
|
options[:logger] |
@logger |
String |
- |
|
|
|
logging |
|
|
|
否 |
|
level: debug |
A19 |
pid |
|
pid_file |
String |
否 |
|
/var/vcap/sys/run/echo_node.pid |
A20 |
base_dir |
options[:base_dir] |
|
String |
是 |
|
|
A21 |
service_log_dir |
options[:service_log_dir] |
|
String |
是 |
|
|
A22 |
service_common_dir |
options[:service_common_dir] |
|
String |
是 |
"/var/vcap/store/common" |
|
A23 |
service_bin_dir |
options[:service_bin_dir] |
|
Hash |
是 |
|
|
A24 |
image_dir |
options[:image_dir] |
|
String |
是 |
|
|
A25 |
port_range |
options[:port_range] |
|
Range |
是 |
|
|
A26 |
filesystem_quota |
options[:filesystem_quota] |
|
Boolen |
是 |
FALSE |
|
A27 |
service_start_timeout |
options[:service_start_timeout] |
|
Interger |
是 |
3 |
|
A28 |
service_status_timeout |
options[:service_status_timeout] |
|
Interger |
是 |
3 |
|
A29 |
max_memory |
options[:max_memory] |
|
Numeric |
是 |
|
|
A30 |
memory_overhead |
options[:memory_overhead] |
|
Numeric |
是 |
0 |
|
A31 |
max_disk |
options[:max_disk] |
|
Numeric |
是 |
128 |
|
A32 |
disk_overhead |
options[:disk_overhead] |
|
Numeric |
是 |
0 |
|
A33 |
m_interval |
options[:m_interval] |
|
Interger |
是 |
10 |
|
A34 |
m_actions |
options[:m_actions] |
|
Array |
是 |
[] |
|
A35 |
m_failed_times |
options[:m_failed_times] |
|
Interger |
是 |
3 |
|
² 參數名稱:配置文件中參數的名稱。 ² options:經過類型轉化後的配置參數保存。 ² 變量名:一些參數對應的實例變量。 ² 類型:參數類型 ² 可選:參數是否可選,是表示該參數可以不配置,否表示該參數必須配置。 ² 默認值:部分參數會提供默認值,這部分參數都是可選參數。 ² 典型值:配置文件中的典型配置參數。 |
大部分參數都可以不需要配置,其中參數A05、A07、A08、A13、A14、A19必須在配置文件中編寫。
A20~A35的參數是warden的配置,關於warden內容可以參考【warden】。
A21~A28這組參數需要注意,如果在配置文件中將use_warden設置爲true,則必須配置文件中加入warden元素,而A21~A28這組元素則可能被warden的中對應的子元素覆蓋,如代碼3.7所示。
代碼3.7:vcap-services-base / lib /base /node_bin.rb #initialize |
use_warden = parse_property(config,"use_warden",Boolean,:optional=> true, :default=> false) if use_warden warden_config = parse_property(config,"warden",Hash,:optional=> true) options[:service_log_dir]= parse_property(warden_config,"service_log_dir",String) …… end |
warden子元素的配置如表3-5所示:
表3-5 warden參數配置表(不完全)
編號 |
參數名稱 |
options |
變量名 |
類型 |
可選 |
默認值 |
典型值 |
B01 |
use_warden |
|
use_warden |
Boolen |
是 |
FALSE |
|
B02 |
warden |
|
warden_config |
Hash |
是 |
|
|
B03 |
service_log_dir |
options[:service_log_dir] |
|
String |
是 |
|
|
B04 |
service_common_dir |
options[:service_common_dir] |
|
String |
是 |
"/var/vcap/store/common" |
|
B05 |
service_bin_dir |
options[:service_bin_dir] |
|
Hash |
是 |
|
|
B06 |
image_dir |
options[:image_dir] |
|
String |
否 |
|
|
B07 |
port_range |
options[:port_range] |
|
Range |
否 |
|
|
B08 |
filesystem_quota |
options[:filesystem_quota] |
|
Boolen |
是 |
FALSE |
|
B09 |
service_start_timeout |
options[:service_start_timeout] |
|
Interger |
是 |
3 |
|
B10 |
service_status_timeout |
options[:service_status_timeout] |
|
Interger |
是 |
3 |
|
注意:在啓用warden的情況下B06、B07兩個參數此時必須配置,所以A24、A25兩個參數此時配置會被B06、B07給覆蓋。
表3-6說明參數的作用。
表3-6 參數說明(不完全)
編號 |
參數名稱 |
說明 |
A01 |
index |
|
A02 |
plan |
|
A03 |
capacity |
表示該節點可以創建多少個Node的服務實例,參考【capacity】 |
A04 |
ip_route |
|
A05 |
node_id |
表示當前主機的Node的名稱,作爲向NATS訂閱主題時候使用的參數之一,參考【主題】 |
A06 |
z_interval |
定時任務的中間間隔,如果不設置,則默認使用時間爲30秒 |
A07 |
mbus |
NATS總線,需要指明其IP地址和端口號,默認NATS使用4222 |
A08 |
local_db |
創建的Service實例需要存儲,默認情況下使用sqlite3 |
A09 |
migration_nfs |
|
A10 |
max_nats_payload |
|
A11 |
fqdn_hosts |
|
A12 |
op_time_limit |
在timing_exec中使用,表示一個操作的超時時間。相關內容參考【timing_exec】 |
A13 |
supported_version |
該Service Node支持創建的實例的版本,一般情況下就1個版本,該參數必須在重寫NodeBin時候設置,否則無法使用。 |
A14 |
default_version |
使用的默認版本號。 |
A15 |
max_clients |
|
A16 |
database_lock_file |
|
A17 |
disabled_file |
|
A18 |
logger |
由node_id生成,每個Service Node會根據其Service實例生成一個log文件。所以這個參數在配置文件中是不能配置的 |
|
logging |
|
A19 |
id |
|
A20 |
base_dir |
|
A21 |
service_log_dir |
|
A22 |
service_common_dir |
|
A23 |
service_bin_dir |
|
A24 |
image_dir |
|
A25 |
port_range |
|
A26 |
filesystem_quota |
|
A27 |
service_start_timeout |
|
A28 |
service_status_timeout |
|
A29 |
max_memory |
|
A30 |
memory_overhead |
|
A31 |
max_disk |
|
A32 |
disk_overhead |
|
A33 |
m_interval |
|
A34 |
m_actions |
|
A35 |
m_failed_times |
|
B01 |
use_warden |
|
B02 |
warden |
|
在Service Service Node的【配置參數】一節,我們見過一個參數叫做capacity,它表示了一個Service Node可以配置的容量,也就是可以創建的Service實例個數。引入capacity的原因如下【--?猜的--】:
² 爲了實現Service實例的負載均衡,一個Gateway會對應多個ServiceNode,不能在同一個節點上創建太多實例,所以每個Service Node都會將capacity提供給Gateway,Gateway以此爲計算參數進行計算,選舉最適合的Service Node創建實例。
² 每個ServiceNode能夠創建的Service實例不可能是無限大,引入capacity可以限制一個Node創建的Service實例個數。
capacity參數可選,默認值爲200,如代碼3.8所示。
代碼3.8:vcap-services-base / lib /base /node_bin.rb #start |
:capacity =>parse_property(config,"capacity",Integer,:optional=>true,:default=>200), |
如代碼3.9所示,讀取的capacity參數的值會傳入到@capacity和@max_capacity中。其中:
² @capacity:表示剩餘容量
² @max_capacity:表示最大容量
代碼3.9:vcap-services-base / lib /base /node.rb #initialize |
@capacity= options[:capacity] @max_capacity=@capacity |
每次Service Node重啓後會恢復之前創建的Service實例,對於已經創建的Service實例已經消耗了部分capacity,所以@capacity的值初始化還需要去除已經創建的Service實例消耗的容量值。而計算這個剩餘容量的過程則需要Service開發人員自行編寫。
對於計算剩餘容量的時機可以選擇在執行pre_send_announcement方法中進行計算。例如在EchoService中,如代碼3.10所示,遍歷所有創建的Service實例,然後依次去除每個實例消耗的容量,最後得到@capacity。其中:
² 【ProvisionedService】是用於保存Service實例的數據庫接入對象(DAO);
² @ capacity_lock就是一個鎖,定義就是@capacity_lock = Mutex.new,因爲在計算剩餘capacity的過程中可能還會出現銷燬Service實例這樣的同步問題,所以需要加鎖保護;
² capacity_unit方法返回每個Service實例所消耗的capacity值。
代碼3.10:vcap-services / echo /lib /echo_service /echo_node.rb #pre_send_announcement |
@capacity_lock.synchronizedo ProvisionedService.all.eachdo |instance| @capacity-= capacity_unit end end |
注意:計算剩餘capacity必須實現,最好在pre_send_announcement方法中計算,在計算剩餘capacity的過程中必須對capacity進行加鎖保護。
@capacity的值需要告知給Gateway,以便Gateway可以知道每個ServiceNode的剩餘容量,並根據該值來實現負載均衡。
通告在【週期任務】中完成,使用send_node_announcement方法,不過對@capacity的操作不是在send_node_announcement,而是在announcement方法中,這個步驟還是需要Service開發人員完成,參考【announcement】。
如代碼3.12所示,在發佈給Gateway的announcement信息會帶上capacity相關的信息,其中:
² :available_capacity:就是剩餘容量——@capacity;
² :capacity_unit:創建一個Service實例需要消耗的代價。capacity_unit方法源代碼如代碼3.11所示,返回值默認設置爲1。Service開發人員可以通過重寫該方法改變返回值。
代碼3.11:vcap-services-base / lib /base /node.rb |
def capacity_unit # subclasses could overwrite this method to re-define # the capacity unit decreased/increased by provision/unprovision 1 end |
² @capacity_lock.synchronize是一個鎖操作。
代碼3.12:vcap-services / echo /lib /echo_service /echo_node.rb #announcement |
def announcement @capacity_lock.synchronizedo { :available_capacity=>@capacity, :capacity_unit=> capacity_unit } end end |
如代碼3.13所示,Service Node每創建一個新Service實例,就會減少capacity_unit份額,更多詳細內容可以參考【on_provision】。
代碼3.13:vcap-services-base / lib /base /node.rb #on_provision |
def on_provision(msg, reply) …… @capacity_lock.synchronize{@capacity-= capacity_unit } …… |
同理,Service Node每銷燬一個Service實例,就會增加capacity_unit的容量,如代碼3.14所示,更多內容可以參考【on_unprovison】。
代碼3.14:vcap-services-base / lib /base /node.rb #on_unprovision |
def on_unprovision(msg, reply) …… @capacity_lock.synchronize{@capacity+= capacity_unit } …… |
【--?--沒怎麼看懂它做什麼用】
NATS作爲整個CloudFoundry的消息總線,不作爲本文介紹的重點。不過在下文運行過程介紹中會經常遇到它。所以還是需要知道一下它的一些簡單使用。可以參考博文:http://blog.csdn.net/zdq0394/article/details/7860041
在連接到NATS後,每個ServiceNode都會向節點訂閱它所關心的主題,相關內容參考【訂閱主題】。
訂閱主題的過程在Base::Node的on_connect_node方法中實現,實現過程如代碼5.1所示。
代碼5.1:vcap-services-base / lib /base /node.rb #on_connect_node |
%w[provision unprovision bind unbind restore disable_instance enable_instance import_instance update_instance cleanupnfs_instance purge_orphan].eachdo |op| eval%[@node_nats.subscribe("#{service_name}.#{op}.#{@node_id}") { |msg, reply| EM.defer{ on_#{op}(msg, reply) } }] end |
該段代碼在句法上有些複雜,我們來慢慢剖析:
² %w[provision unprovision ……].each do |op| …… end :這個句法的意思就是一次遍歷方括號中的每個元素,元素的類型爲String,取出的元素至於op中,然後執行代碼塊中的內容。
² eval %[……] :這個句法的意思就是,把內部的文本內容翻譯成Ruby可執行的語句,使其可以執行。
² @nodes_nats.subscribe(……) { |msg, reply| ……} :這個句法的意思是,向NATS訂閱主題,每個主題對應的處理方法則是在其代碼塊中定義。
² "#{service_name}.#{op}.#{@node_id}":這個句法代表訂閱的主題。一個主題由三者共同確定(注:部分主題只需要2個部分內容)。對於同一個Service,service_name其實是相同的;op是模板已經寫好的,不能修改;@node_id則必須不同,目的是爲了區分不同主機上的Service Node(注意:不是區分Service實例):
ü service_name:標識Service的名稱,一般是”XXXaaS”格式,這個名字是由Service開發人員編寫的,詳細內容參考【service_name】。
ü op:訂閱的主題操作,也就是%w[……]中的內容,Node中已經提供了大量的操作,Service開發人員在編寫Node時最多的時間也就是對這些操作的開發。
ü @node_id:標識一個Service Node,這樣Gateway才能準確將信息發送給指定Node。
注意:@node_id是在配置文件中進行配置的node_id參數,參考【配置參數】
² EM.defer{on_#{op}(msg, reply)} :這個部分的內容則是實現了相應主題的處理函數(在Event Machine中運行該處理方法)。對應主題的處理方法名稱就是”on_XXX”。這些處理方法提供了處理主題的一份模板,大部分主題會預留Hook方法讓Service開發人員完善,實現所需的Service Node開發。
表5-1整理了Node中的主題內容:
表5-1 Service Node訂閱主題相關說明(不完全)
編號 |
主題名稱 |
處理方法 |
Hook方法 |
說明 |
F01 |
provision |
on_provision |
provision |
創建一個Service實例 |
F02 |
unprovision |
on_unprovision |
unprovision |
銷燬一個Service實例 |
F03 |
bind |
on_bind |
bind |
將Service實例與APP綁定 |
F04 |
unbind |
on_unbind |
unbind |
將Service實例與APP解除綁定 |
F05 |
restore |
on_restore |
|
|
F06 |
disable_instance |
on_disable_instance |
disable_instance |
讓Service實例停止服務,但不銷燬 |
F07 |
enable_instance |
on_enable_instance |
enable_instance |
讓Service實例生效 |
F08 |
import_instance |
on_import_instance |
import_instance |
|
F09 |
update_instance |
on_update_instance |
update_instance |
|
F10 |
cleanupnfs_instance |
on_cleanupnfs_instance |
|
|
F11 |
purge_orphan |
on_purge_orphan |
|
|
F12 |
check_orphan |
on_check_orphan |
|
|
F13 |
discover |
on_discover |
|
|
² 主題名稱:對應處理的主題。其中主題F01~F11主題內容是"#{service_name}.#{op}.#{@node_id}"格式;F12、F13主題內容是"#{service_name}.#{op}" 格式。另外,{F01~F09-F05}明確要求Service開發人員實現相關的處理方法。【--?--F05有些奇怪,我無法把它串聯起來】 ² 處理方法:就是相應主題的處理方法。 ² Hook方法:處理方法中幫助Service開發人員完成了一些基本工作,真正的處理都需要在Hook方法中進行處理。 |
對於如何添加新的主題,Service的開發人員其實不用關心,因爲Cloud Foundry已經爲我們設計好了模板,而且這份模板已經足夠使用,而且添加新的主題是一件非常麻煩的事情。我們所關心的事情是,對於相應主題的處理方法的調用過程以及Hook方法的實現,這些處理方法將在【運行過程】中一一展開講述。
在Service Node中,一共需要執行2個週期任務。
週期任務底層依靠Event Machine實現,EventMachine中添加週期任務不難,只需要調用add_periodic_timer方法,語法格式如代碼6.1所示.
代碼6.1 |
EM.add_periodic_timer(10) do # do something end |
第一個週期任務在Base::Node#on_connect_node添加,如代碼6.2所示。其中:
² pre_send_announcement方法可以參考【pre_send_announcement】,這個方法默認情況下爲空,也就是指它是一個Hook函數,Service的開發人員可以通過重寫它實現在進行週期任務前的一些預處理。
² EM.add_periodic_timer(30) {……}:是在EventMachine中添加一個30S的定時器,定期執行代碼段中的內容。
² 週期任務的主要流程都在send_node_announcement方法中。該方法就是用於【--?--向Gateway進行註冊以及保活】,讓Gateway可以實時監測到Node的狀態信息。
代碼6.2:vcap-services-base / lib /base /node.rb #on_connect_node |
def on_connect_node …… pre_send_announcement send_node_announcement EM.add_periodic_timer(30) { send_node_announcement } end |
如代碼6.3所示,send_node_announcement方法做的事情就是整理需要發送的announcement信息到發佈主題#{service_name}.announce,這樣就可以發送到對應的Gateway處理。
² 在發送announcement之前,需要確定節點是否是正常運行的,只有在正常運行的情況下才能發送announcement。
² 在發送的announcement信息中,有三個信息是必須存在,也就是@node_id、@plan、@supported_version。這三個參數也是在配置文件中必須指定的,參考【配置參數】。
² 還可以添加自定義的announcement信息,Service的開發人員通過announcement方法添加,參考【announcement】。
不過這個方法實現有些詭異,在函數定義的時候我們明顯看到它帶有2個參數,但是在代碼6.2中我們可以看到並沒有代入參數,還有就是msg爲nil時候就可以發送announcement。
代碼6.3:vcap-services-base / lib /base /node.rb #send_node_announcement |
def send_node_announcement(msg=nil, reply=nil) if disabled? ……return end unless node_ready? ……return end req = nil req = Yajl::Parser.parse(msg)if msg if !req || req["plan"]==@plan a = announcement a[:id]=@node_id a[:plan]=@plan a[:supported_versions]=@supported_versions publish(reply||"#{service_name}.announce",Yajl::Encoder.encode(a)) end …… end |
Service Node定期調用【send_node_announcement】方法向Gateway發送Node的聲明,聲明的內容除去三個必須含有的信息——@node_id、@plan、@supported_version——外,官方專門預留announcement方法提供給service開發人員進行擴展。
如代碼6.4所示,官方要求必須實現這個方法。announcement方法需要處理的內容就是需要提供給Gateway的信息。
代碼6.4:vcap-services-base / lib /base /node.rb |
# Service Node subclassesmust implement the following methods # announcement() --> {any service-specific announcement details} abstract :announcement |
表6.1整理了announcement方法的參數說明,
表6.1announcement方法說明
函數名:announcement |
參數名稱 |
說明 |
輸入參數 |
- |
|
返回值 |
Hash |
返回需要通告信息的Hash表,最後該表會發送給Gateway |
如代碼6.5所示,在EchoService中,announcement方法返回值就是一個Hash數組。
代碼6.5:vcap-services / echo /lib /echo_service /echo_node.rb |
def announcement @capacity_lock.synchronizedo { :available_capacity=>@capacity, :capacity_unit=> capacity_unit } end end |
Service Node定期發送的announcement最後由ServiceGateway接收。在代碼6.3中,Service Node每次發送announcement週期任務的時候使用的主題是“#{service_name}.announce”。如代碼6.5.1所示,在ServiceGateway中,對應於該主題的處理方法是on_announce方法。
代碼6.5.1:vcap-services-base / lib / base / provisioner.rb #on_connect_node |
def on_connect_node @logger.debug("[#{service_description}] Connected to node mbus..") %w[announce node_handles handles update_service_handle].eachdo |op| eval%[@node_nats.subscribe("#{service_name}.#{op}") { |msg, reply| on_#{op}(msg, reply) }] end |
如代碼6.5.2所示,【--?--】
代碼6.5.2:vcap-services-base / lib / base / provisioner.rb #on_announce |
def on_announce(msg, reply=nil) announce_message =Yajl::Parser.parse(msg) if announce_message["id"] id= announce_message["id"] announce_message["time"]=Time.now.to_i if @provision_refs[id]>0 announce_message['available_capacity']=@nodes[id]['available_capacity'] end @nodes[id]= announce_message end end |
第二個週期任務任務在Base::Node#initialize方法中添加,如代碼6.6所示。其中
² z_interval就是【配置參數】中配置,如果不配置,則會使用默認值30。
² 週期任務的主要流程都在update_varz中執行。
代碼6.6:vcap-services-base / lib /base /node.rb #initialize |
z_interval = options[:z_interval]||30 EM.add_periodic_timer(z_interval)do EM.defer {update_varz } end if @node_nats …… end |
update_varz定義如代碼6.7所示
² 首先通過varz_details方法獲取到需要想Component註冊的信息表(Hash),關於varz_details的內容參考【varz_details】
² 然後向Component中依次註冊每個參數。
代碼6.7:vcap-services-base / lib /base /base.rb |
def update_varz() vz = varz_details vz.each { |k,v| VCAP::Component.varz[k] = v } if vz end |
varz是CloudFoundry用於監控組件狀態,大致原理如下【--?--我還是不知道做什麼的?】:
Cloud Foundry狀態監控組件:VARZ原理 |
在代碼中我們可以看到組件在啓動時都會向Component註冊自己。那麼這個註冊就會啓動一個http server。啓動的代碼在vcap common的component.rb中。這個模塊已經成爲了一個gem,你不必裝Cloud Foundry,而只需要gem install一個都可以使用了。在vcap common中,如果有組建來註冊,他會爲這個組件建立一個server。然後server的port以及帳號密碼默認是cf自己生成的。但是按照上文的配置,這些參數就會被傳入,我們就可以按照自己的參數來配置這個server了。 在組件向component註冊完成之後,組建就可以通過以下方式向varz傳數據了: [ruby]view plaincopy 1 #這是dea的狀態更新 2 VCAP::Component.varz[:running_apps] = running_apps 3 VCAP::Component.varz[:frameworks] = metrics[:framework] 4 VCAP::Component.varz[:runtimes] = metrics[:runtime] |
在【update_varz】中介紹了CloudFoundry中每個組件都會向VCAP::Component註冊自己,然後可以向varz傳入參數。的傳入的參數則是根據varz_details得到。代碼6.8給出了varz_details的定義,默認情況下varz_details的返回值就是announcement的返回值。該方法可以重寫,只需要返回內容是Hash表即可。
代碼6.8:vcap-services-base / lib /base /node.rb |
def varz_details # Service Node subclassesmay want to override this method to # provide service specific data beyond what is returned by their # "announcement" method. return announcement end |
注意:varz_details方法可以重寫。如代碼6.9所示,在MySqlService實現中就重寫了該方法。
代碼6.9:vcap-services / mysql /lib /mysql_service /node.rb |
def varz_details() acquired = @varz_lock.try_lock return unless acquired varz = {} # how many queries served since startup varz[:queries_since_startup]= get_queries_status # queries per second varz[:queries_per_second]= get_qps # disk usage per instance status = get_instance_status varz[:database_status]= status …… # how many long queries and long txs are killed. varz[:long_queries_killed]=@long_queries_killed …… # how many provision/binding operations since startup. @statistics_lock.synchronizedo varz[:provision_served]=@provision_served varz[:binding_served]=@binding_served end # provisioned services status varz[:instances]= {} begin ProvisionedService.all.eachdo |instance| varz[:instances][instance.name.to_sym] = get_status(instance) end …… varz[:connection_pool]=@pool.inspect varz …… end |
Service Node的週期任務並不複雜。
相關的配置參數只有一個z_interval。該參數配置了每次執行update_varz的間隔時間。
需要實現的方法也只有一個:announcement,該方法規定了發佈給Gateway的信息內容。該方法要求返回一個Hash表。
varz_details方法默認情況下使用announcement的返回值,Service開發人員可以根據自己的需求重新實現該方法。
Service Node在初始化完成後,在運行過程中的工作任務除了【週期任務】中的兩個外,最主要職責就是負責Service實例的創建與維護。而Service Node如何知道自己在什麼時刻應該執行什麼功能?其實也簡單,通過之前訂閱的主題進行驅動(參考【訂閱主題】、【主題】),當Service Node接收到其訂閱的主題的請求後,就會調用相應的方法,而調用的方法的格式也就是【主題】一節中說的”on_xxx”格式。
Base::Node爲Service的開發人員實現了這份模板,並且爲其中的關鍵部分都留出了抽象方法讓Service開發人員可以自定義在交互過程中提供的變化。相應的說明可以參考【主題】一節中的表格。
對於主題的處理過程其實是一樣的,我們會在【創建Service instance】一節中細緻的分析該過程,在之後的章節中略去這些重複說明,如果有不懂的地方可以類比【創建Service instance】中的過程。
創建Service實例使用的主題內容是provision。這點很容易理解,不過需要注意的一點就是,創建的Service實例此時還未與APP進行綁定,綁定動作是在bind主題中完成的。此外,爲了提高運行效率,更推薦在創建Service實例時採用Lazy技術,也就是延遲向Service服務器程序申請創建Service實例的時機,直到要求將APP與Service實例進行綁定的時候才選擇想Service服務器程序創建相應的實例。這點將會在下面的分析中體現出來。
在【主題】一章中我們知道Node在啓動的時候會訂閱主題“#{service_name}.provision.#{@node_id}”。當用戶通知CloudController創建一個Service實例的時候,Cloud Controller讓Gateway選出一個Service Node創建Service實例,此時Gateway就會發佈一個這樣的主題內容,NATS會將其正確的發送給對應的Service Node,Service Node就根據收到的主題內容使用對應的處理函數進行處理。
一個Service實例的創建過程大致如圖7-1所示:
1) 用戶使用命令vmc create 請求創建一個Service實例,這個請求被CloudController捕獲。
2) CloudController根據vmc命令請求要求對應的Gateway處理該任務。
3) 這時候Gateway會調用provision_service方法從它管理的Service Node中選擇一個最優的Service Node(best_node),整理好所有必需信息以後發佈#{service_name}.provision.#{@node_id}這樣的一份主題,NATS會負責把它發送給Base::Node。
4) 然後Base::Node中的on_provision方法接收到了這份主題請求,參考【on_provision】,在進行部分處理以後,
5) 調用provision方法創建Service實例——這個方法要求Service的開發人員進行編寫,返回值要求是一個Hash數組,相關內容參見【provision】
6) 如果provision方法正確創建了一個Echo的實例,此時Base::Node會將相關的信息(哪些信息由Service的開發人員決定)通過encode_success方法編碼後返回給Gateway,這樣就創建了一個Service實例。
圖 7-1 Service實例創建時序圖(示意)
on_provision的源代碼如代碼7.1所示,其中:
² roolback是一個代碼塊,他的作用是當創建實例失敗的時候調用unprovision方法將進行到一半的實例進行析構。
² timing_exec提供的功能就是在@op_time_limit時間內如果還無法成功創建Service實例,則調用roolback代碼塊,關於timing_exec方法參見【timing_exec】。
² 在timing_exec中間就會調用provision方法創建一個Service實例,關於provision方法參考【provision】。在這裏會扣減capacity容量,相關內容可以參考【capacity】。如果創建成功,則講成功的結果整理編碼後反饋給Gateway。
代碼7.1:vcap-services-base / lib /base /node.rb #on_provision |
def on_provision(msg, reply) response = ProvisionResponse.new rollback=lambdado |res| @capacity_lock.synchronize{@capacity+= capacity_unit } if unprovision(res.credentials["name"],[]) end
timing_exec(@op_time_limit, rollback)do provision_req = ProvisionRequest.decode(msg) plan = provision_req.plan credentials = provision_req.credentials version = provision_req.version credential = provision(plan, credentials, version) credential['node_id']=@node_id response.credentials = credential @capacity_lock.synchronize{@capacity-= capacity_unit } response end publish(reply, encode_success(response)) …… end |
provision方法要求Service開發人員必須實現,其定義如代碼7.2所示,它需要實現的功能就是實現在Local Node中創建一個Service 實例。
代碼7.2:vcap-services-base / lib /base /node.rb |
# Service Node subclassesmust implementthe following methods abstract :provision |
注意:註釋中給出了相應的參數與返回值,但是在使用的時候卻又出現了差異,這應該是Cloud Foundry的開發人員忘記修改註釋引起。在使用provision的時候實際傳入的是三個參數(注意:Ruby中沒有函數重載),返回類型也存在差異,所以註釋中給定的信息並不可信。
表7-1整理了該方法的參數說明。
表7-1provision方法說明
函數名:provision |
參數名稱 |
說明 |
輸入參數 |
plan |
Gateway發送過來的plan值。 |
credentials |
爲一個Hash數組,其中包含了用戶創建的Service實例的名字。 |
|
version |
使用的Service版本號。 |
|
返回值 |
credentials |
credentials是一個Hash表,推薦含有name,host,port,user,password幾個鍵值。 |
如代碼7.3所示,其顯示了provision帶入參數的來源以及provision方法的調用過程。provision使用的參數從通過ServiceGateway發送的請求信息中提取。需要注意credentials參數的來源。
注意:除credentials外還有一個參數是credential,少個s。
代碼7.3:vcap-services-base / lib /base /node.rb |
def on_provision(msg, reply) provision_req=ProvisionRequest.decode(msg) plan = provision_req.plan credentials= provision_req.credentials version = provision_req.version credential = provision(plan, credentials, version) …… |
從代碼7.4給出了Service Gateway整理髮送給ServiceNode的這幾個參數值的來源。
代碼7.4:vcap-services-base / lib /base /provisioner.rb #provision_service |
def provision_service(request,prov_handle=nil,&blk) …… prov_req = ProvisionRequest.new prov_req.plan = plan prov_req.version = version # use old credentials to provision a service if provided. prov_req.credentials= prov_handle["credentials"]if prov_handle |
我們考察Echo Service中provision方法的實現,如代碼7.5,其中credentials這個參數中包含的內容就是使用vmc create進行創建一個service的時候傳入的參數【--?這個地方有待驗證--】。所以credentials參數中最少會含有一個參數名稱“name”。
注:這個name參數是用戶希望創建的Service實例的名字。
代碼7.5:vcap-services / echo /lib /echo_service /echo_node.rb #provision |
def provision(plan, credential= nil, version=nil) instance = ProvisionedService.new if credential instance.name =credential["name"] …… |
也可以包含“user”與“password”,例如在代碼7.6中,MySql::Node的provision方法就就可以看到如下三個參數:
代碼7.6:vcap-services / mysql /lib /mysql_service /node.rb #provision |
def provision(plan, credential=nil, version=nil) …… if credential name, user, password=%w(name user password).map{|key| credential[key]} …… |
如何創建一個Service實例,這個需要由Service開發人員實現。如代碼7.7所示,我們考察Echo::Node中的provision方法,其中:
² ProvisionedService就是一個DAO(Database Access Object)。內部封裝了一個Ruby數據庫ORM——DataMaper。詳細內容參考【ProvisionService】
² Echo中創建實例的過程相對簡單,首先在數據庫中創建相應Service實例,然後保存實例。
² 最後將用戶訪問Service實例所需要的信息(如主機號、端口號、用戶名、登陸密碼等)返回。
代碼7.7:vcap-services / echo /lib /echo_service /echo_node.rb #provision |
def provision(plan, credential= nil, version=nil) #class ProvisionedService # include DataMapper::Resource # property :name, String, :key => true #end instance = ProvisionedService.new if credential instance.name = credential["name"] ……
begin save_instance(instance) ……
# gen_credential(instance) credential = { "host"=> get_host, "port"=>@port, "name"=> instance.name } end |
圖7-2整理了provision方法實現的時候的一個執行流程。如果沒有特殊的要求,一般都可以按照這個步驟進行編寫。Cloud Foundry中對於Service實例的序列化默認提供sqlite進行管理,參考【ProvisionService】。
圖7-2provision執行流程
銷燬一個Service實例,過程與provision類似,對應主題“#{service_name}.unprovision.#{@node_id}”。在收到unprovision主題後由on_unprovision方法處理。
on_unprovision是on_provision方法的一個逆過程,它的做法就是銷燬一個已經存在的Service實例。其源碼如代碼7.8所示。
² 因爲創建一個Service實例的主要動作是Service開發人員在provision方法中編寫的,CloudFoundry無法得知如何去註銷個Service實例,所以也提供了一個相應的方法——unprovision方法,該方法也需要Service開發人員進行編寫。
² 方法中提供了unprovision使用的兩個參數,name是需要銷燬的service實例的名字;bindings是與該service實例綁定的APP信息。
代碼7.8:vcap-services-base / lib /base /node.rb #on_provision |
def on_unprovision(msg, reply) …… unprovision_req = UnprovisionRequest.decode(msg) name= unprovision_req.name bindings = unprovision_req.bindings result = unprovision(name, bindings) if result publish(reply, encode_success(response)) …… |
unprovision定義如代碼7.9所示,unprovision方法必須重寫,它實現了銷燬名字爲name的Service實例。
代碼7.9:vcap-services-base / lib /base /node.rb |
# Service Node subclasses must implement the following methods abstract :unprovision |
源碼中對於unprovision的註釋存在一些問題,其需要帶入兩個參數,必須有1個返回值。整理後入表7-2所示:
表7-2unprovision方法說明
函數名:unprovision |
參數名稱 |
說明 |
輸入參數 |
name |
Service實例名字 |
bindings |
與該Service實例綁定的APP信息 |
|
返回值 |
bool |
是否成功 |
相應的,我們考察Echo::Node中unprovision方法的實現,如代碼7.10所示。這個代碼會出現一些不協調的地方,
² 首先,它將參數bindings的名字改成了credentials,這樣的命名容易讓人誤解其含義;
² 然後,這個方法中並未使用到credentials這個參數,之後我們會考察MySql中是如何使用這個參數的。
² 最後會根據結果返回一個布爾值表明是否成功註銷Service實例。
代碼7.10:vcap-services / echo /lib /echo_service /echo_node.rb |
def unprovision(name,credentials= []) return if name.nil? instance = get_instance(name) raiseEchoError.new(EchoError::ECHO_DESTORY_INSTANCE_FAILED,instance.inspect)unlessinstance.destroy true end |
爲了更好的理解unprovision方法,我們考察了MySql下該方法中對於credentials參數的使用,如代碼7.11所示。如果看過【on_bind】,就知道在對一個Service實例進行註銷的時候,必須對bind操作進行一個逆向操作,也就是unbind。所以在MySql::Node中就會調用unbind方法將Service實例和與之綁定的APP解除綁定。
代碼7.11:vcap-services / mysql /lib /mysql_service /node.rb #unprovision |
# TODO: validate that database files are not lingering # Delete all bindings, ignore not_found error since we are unprovision begin credentials.each{ |credential|unbind(credential)}if credentials |
注意:unprovision方法的執行流程與provision方法是一樣的,可以參考圖-2。不僅如此,bind、unbind等方法的執行流程也是一樣的,下文中就列舉了。
創建的Service實例對於APP而言是不可見的,甚至Service還不知道需要創建Service實例(如:在MySql的provision方法中使用Lazy技術延遲創建數據庫),用戶部署的APP如果希望能夠見到Service實例,就需要將APP與Service實例進行綁定。APP與Service綁定過程類似於Service實例的創建過程,對應的主題是“#{service_name}.bind.#{@node_id}”。在收到unprovision主題後由on_bind方法處理。
如代碼7.12所示,on_bind的源碼格式和on_provision的格式基本相同,也是確定參數,調用bind方法,向Service Gateway返回結果這樣一個流程。
代碼7.12:vcap-services / mysql /lib /mysql_service /node.rb |
def on_bind(msg, reply) response = BindResponse.new rollback = lambdado |res| unbind(res.credentials) end
bind_message = BindRequest.decode(msg) name= bind_message.name bind_opts = bind_message.bind_opts credentials = bind_message.credentials response.credentials= bind(name, bind_opts, credentials) response end publish(reply, encode_success(response)) …… end |
bind方法也是預留給Service開發人員使用的接口,這個方法在源碼中的定義如代碼7.13所示。註釋給出的參數也存在一些偏差,表7-3整理了bind方法的用法。
代碼7.13:vcap-services-base / lib /base /node.rb |
# Service Node subclasses must implement the following methods abstract :bind |
表7-3 bind方法說明(不完整)
函數名:bind |
參數名稱 |
說明 |
輸入參數 |
name |
APP需要綁定的Service實例的名字 |
bind_opts |
綁定時候附加的參數 |
|
credentials |
|
|
返回值 |
credentials |
|
Echo::Node中的bind方法並沒有太多的參考性,我們可以參考MySql::Node中的bind方法的實現,如代碼7.14所示:
² 首先、從數據庫中找到存儲的Service實例(參考【provision】中介紹的Echo::Node的provision方法,用的數據庫是一樣的);
² 然後也是創建一個數據庫存儲綁定的APP的信息;
² 再之後調用enforce_instance_storage_quota方法在MySql中創建這個賬戶(注:不是創建了Service實例就會在MySql中創建這個賬戶,而是在綁定的時候纔會創建,所以纔會產生enforce_instance_storage_quota這個方法調用);
² 最後會將結果信息返回給Gateway。
代碼7.14:vcap-services / mysql /lib /mysql_service /node.rb |
def bind(name, bind_opts, credential=nil) begin service = ProvisionedService.get(name) # create new credential for binding binding=Hash.new if credential binding[:user]= credential["user"] binding[:password]= credential["password"] …… binding[:bind_opts]= bind_opts
begin create_database_user(name,binding[:user],binding[:password]) enforce_instance_storage_quota(service) ……
response = gen_credential(name,binding[:user],binding[:password]) …… end |
將APP與Serviceinstance解除綁定,對應主題“#{service_name}.unbind.#{@node_id}”。在收到unbind主題後由on_unbind方法處理。
on_unbind是on_bind的逆過程,處理內容就是將APP與Service實例解除綁定。如代碼7.15所示,真正執行解除綁定的任務是在unbind方法中調用。
代碼7.15:vcap-services-base / lib /base /node.rb |
def on_unbind(msg, reply) response = SimpleResponse.new unbind_req = UnbindRequest.decode(msg) result = unbind(unbind_req.credentials) if result publish(reply, encode_success(response)) …… |
unbind方法定義如代碼7.16所示,表7-4整理了unbind方法說明。
代碼7.16:vcap-services-base / lib /base /node.rb |
# unbind(credentials) --> void abstract :unbind |
表7-4unbind方法說明(不完全)
函數名:unbind |
參數名稱 |
說明 |
輸入參數 |
ccredentials |
|
返回值 |
bool |
是否成功 |
【--?--】
Base::Node中提供了一組方法用於管理Service實例的主題與接口。包括了Service實例的啓動、停止、導入、更新等常用操作。這些方法定義如代碼7.17所示:表7-5整理了相應的主題與方法。
代碼7.17:vcap-services-base / lib /base /node.rb |
# <action>_instance(prov_credential, binding_credentials) --> true for success and nil for fail abstract :disable_instance,:dump_instance,:import_instance,:enable_instance,:update_instance |
表7-5 Service實例管理方法
主題 |
處理方法 |
Hook方法 |
說明 |
disable_instance |
on_disable_instance |
disable_instance |
|
dump_instance |
|
||
enable_instance |
on_enable_instance |
enable_instance |
|
import_instance |
on_import_instance |
import_instance |
|
update_instance |
on_update_instance |
update_instance |
|
cleanupnfs_instance |
on_cleanup_instance |
|
|
何謂orphan,其實orphan就是在service節點上已經創建的一個service實例,但是它的存在還沒有通知cloud_controller。比如說,在service節點創建完實例並通知完service gateway,而當gateway返回給cloud_controller時,發生了網絡故障,從而cloud_controller通知終端用戶,創建失敗,當然也不會有信息更新在cloud_controller的數據庫中。這樣的話,就相當於在service_node上創建了一個沒有用的實例,從而導致浪費了一些資源。orphan和沒有綁定的instance是有區別的,在實際情況中經常會出現未經綁定的instance,但是他們在cloud_controller中都是有數據記錄的,而orphan則沒有。一般這種情況很罕見,但是源碼中還是考慮了這一點。
orphan的幾個管理函數
在分析源碼的過程中還會遇到許多的輔助方法,這些方法因爲與章節內容相關性很小,一直找不到地方放置。所以乾脆就直接兜在一塊說,這個章節的內容很零散,不需要專門閱讀,只有在用到了或者看不懂的時候再看。
在前面的章節中經常可以看到“abstract:XXX”這樣的語法格式。因爲我沒有專門學過Ruby,尤其對於Ruby元編程的內容更加不瞭解。以下內容都是個人的臆測。
首先,我們先找到abstract的定義(一開始我以爲它是一個關鍵字,後來發現不是),如代碼8.1所示,它其實也是一個方法,有自己的參數。
該方法就是調用了define_method方法檢查是否實現了args中定義的方法。它所想要實現的就是類似於C++中的純虛函數定義。所以使用了abstract :XXX表示的語句我們可以看做是定義了一個純虛函數XXX,我叫它——抽象方法。
代碼8.1:vcap-services-base / lib /base /abstract.rb |
classClass def abstract(*args) args.each do |method_name| define_method(method_name)do |*args| raise NotImplementedError.new("Unimplemented abstract method #{self.class.name}##{method_name}") end end end end |
在【創建Echo::Node實例】中遇到了node_class,該方法其實很容易看明白。特殊地方就在於node_class方法要求Echo::NodeBin中進行實現,重寫內容很簡單,只需要指向相應的XXX::Node即可。
表8-1node_class方法說明
函數名:node_class |
參數名稱 |
說明 |
|
輸入參數 |
- |
|
|
返回值 |
Node |
Service的Node類,例如VCAP::Service::Echo::Node |
|
例如在Echo Service實現的時候,參照代碼8.2,它就返回了Echo中實現的Service Node。
代碼8.2:vcap-services / echo /bin /echo_node |
classVCAP::Services::Echo::NodeBin<VCAP::Services::Base::NodeBin …… def node_class VCAP::Services::Echo::Node end …… end |
注意:該方法要求Service開發者必須實現。
在Base::Node中pre_send_announcement方法的定義爲空,也就是說它是一個Hook方法,可以讓Service的開發人員自定義實現該方法,當然也可以不實現。這個方法所在的上下文有:
² 已經完成了到NATS的連接建立過程,並且完成了主題的訂閱。
² 在添加週期任務send_node_announcement之前執行
對於Node的初始化有兩個時機,一個是Node的initialize方法中,另一個就是在pre_send_announcement方法。前者多用於參數的初始化,而在pre_send_announcement方法中更適合Node數據庫的初始化(就是使用【配置參數】中local_db)以及剩餘capacity的計算。
如代碼8.3所示,在Echo Service實現中,pre_send_announcement的工作就是初始化了使用的數據庫,以及對剩餘capacity的計算。
代碼8.3:vcap-services / echo /lib /echo_service /echo_node.rb |
def pre_send_announcement super FileUtils.mkdir_p(@base_dir)if@base_dir start_db @capacity_lock.synchronizedo ProvisionedService.all.eachdo |instance| @capacity-= capacity_unit end end end |
Cloud Foundry實現了實現了一個日誌文件系統,每個組件都會存在一個@logger實例,它就表示了該日誌系統。我們並不關心該日誌系統的實現,更關注於它的使用。
從代碼8.4可以看出,與日誌文件系統的相關配置參數是logging,每個節點的日誌是根據node_id生成的。Logging參數配置可以參考【配置參數】。在開發期間還是選擇debug模式。
代碼8.4:vcap-services-base / lib /base /node_bin.rb #start |
VCAP::Logging.setup_from_config(config["logging"]) # Use the node id for logger identity name. options[:logger]=VCAP::Logging.logger(options[:node_id]) @logger= options[:logger] |
使用@logger也很簡單,我們一般使用debug和info方法(我沒去看日誌系統,也不知道有沒有其他高級特性)。例如如代碼8.5,我們截取了兩個使用日誌系統的例子。
代碼8.5:vcap-services-base / lib /base /node.rb |
@logger.info("#{service_description}: Not sending announcement because node is disabled") @logger.debug("#{service_description}: Not ready to send announcement") |
在【運行過程】一節中,會經常遇到timing_exec方法。這個方法其實很簡單,就是要求在time_limit時間內完成代碼塊中的內容,如果超時,就調用roolback並拋出異常,如代碼8.6所示。
代碼8.6:vcap-services-base / lib /base /node.rb #timing_exec |
def timing_exec(time_limit, rollback=nil) return unless block_given?
start = Time.now response = yield if response&&Time.now- start> time_limit rollback.call(response)if rollback raise ServiceError::new(ServiceError::NODE_OPERATION_TIMEOUT) end end |
在看Echo::Node和MySql::Node源碼時候就會看到這個類。其實這個類名字是什麼無所謂,關鍵的是知道它是做什麼用的。我們都知道Service Node創建Service實例,但是這個實例不可能只存儲內存中,否的一宕機Service實例的內容就沒有了,所以就需要支持Service實例的可序列化。當然我們也可以使用XML這種格式存儲,不過Cloud Foundry中則是使用了sqlite3進行保存,然後對數據庫的中間層使用的是DataMapper。
如代碼8.7所示,其中:
² ProvisionService其實就是表示了在數據庫中存儲的一個Service實例;
² include DataMapper::Resource 則是導入了對數據庫操作的相關接口,它提供了包括創建、查詢、保存等數據庫常用操作;
² property XXX 就是一個數據庫表,Service開發人員在這裏定義希望在數據庫中保存的Service信息。
代碼8.7:vcap-services / echo /lib /echo_service /echo_node.rb |
class ProvisionedService includeDataMapper::Resource property :name,String,:key => true end |
在Node啓動的時候,還需要調用代碼8.8中的代碼。該代碼的調用時機一般就選擇在【pre_send_announcement】中。其中@local_db參考【配置參數】中的說明。
代碼8.8:: |
DataMapper.setup(:default,@local_db) DataMapper::auto_upgrade! |
如表8-2所示,我們截取了一些常用的操作的示例代碼。
表8-2ProvisionService常用操作
操作 |
示例代碼 |
創建一個新項 |
instance =ProvisionedService.new |
保存一個項 |
instance.save |
刪除一個項 |
instance.destroy |
獲取特定項 |
instance =ProvisionedService.get(name) |
遍歷素有項 |
ProvisionedService.all.eachdo |instance|…… end |
每個Service需要一個名字(name),在【訂閱主題】和【主題】兩節中,我們知道service_name作爲Node訂閱主題中關鍵的一個數據結構。如代碼8.9所示,源碼中要求Service Node和Provisioner節點都必須實現該方法。
代碼8.9:vcap-services-base / lib /base /base.rb |
# Service Provisioner and Node classes must implement the following # method abstract :service_name |
表8-2整理的service_name方法的定義
表8-2service_name方法說明
函數名: service_name |
參數名稱 |
說明 |
輸入參數 |
- |
|
返回值 |
name |
需要返回一個字符串,參考代碼5.1即可知道。 |
按理來說,該方法在Node類中實現即可。不過按照CloudFoundry中Service實現的慣例來看,一般是將這個方法封裝在Common模塊中。這是因爲這個方法是Node類和Provisioner類都需要的,爲了避免兩者重複實現,所以將其封裝成了接口。
如代碼8.10所示,在EchoService中,專門對service_name的實現封裝,然後在代碼8.11的Node類實現過程中,導入該接口,在Service的Provisioner類實現中也是類似的做法。
代碼8.10:vcap-services / echo /lib /echo_service /common.rb |
moduleVCAP module Services module Echo module Common defservice_name "EchoaaS" end end end end end |
代碼8.11:vcap-services / echo /lib /echo_service /echo_node.rb |
classVCAP::Services::Echo::Node includeVCAP::Services::Echo::Common |
注意:該方法要求Service開發人員必須實現
在【on_check_orphan】一節中,我們見到on_check_orphan方法調用了all_instance_list方法,該方法的定義如代碼8.11所示。
² Service Node需要監測orphan的Service實例,關於orphan內容參考【orphan】,所以每次需要將所有需要檢查的Service實例列表發送給Gateway,返回該列表的工作就由all_instance_list方法完成。
² 這個方法要求實現,默認情況下會返回一個空列表,意思是,如果Service開發者不實現該方法也不會報錯,但是在運行過程中產生的orphan都不會被管理。
代碼8.11:vcap-services-base / lib /base /node.rb |
# Subclass must overwrite this method to enablecheck orphan instance feature. # Otherwise it will not check orphan instance # The return value should be a list of instance name(handle["service_id"]). def all_instances_list [] end |
如果要實現該方法,其實也不復雜。
表8-3all_instance_list方法說明
函數名:all_instance_list |
參數名稱 |
說明 |
輸入參數 |
- |
|
返回值 |
list |
返回一個Service實例列表 |
如代碼8.12所示,在MySql::Node中實現該方法,它返回的是數據庫中所有保存的Service實例。
代碼8.12:vcap-services / mysql /lib /mysql_service /node.rb |
def all_instances_list ProvisionedService.all.map{|s| s.name} end |
注意:該方法要求Service開發人員必須實現。
vcap-services-base /lib /base /node.rb |
# Subclass must overwrite this method to enablecheck orphan binding feature. # Otherwise it will not check orphan bindings # The return value should be a list of binding credentials # Binding credential will be the argument for unbind method # And it should have at least username & name property for base code # to find the orphans def all_bindings_list [] end |
vcap-services /mysql /lib /mysql_service /node.rb |
def all_bindings_list res = [] all_ins_users = ProvisionedService.all.map{|s| s.user} @pool.with_connectiondo |connection| # we can't query plaintext password from mysql since it's encrypted. connection.query('select DISTINCT user.user,db from user, db where user.user = db.user and length(user.user) > 0').eachdo |entry| # Filter out the instances handles res << gen_credential(entry["db"], entry["user"],"fake-password")unless all_ins_users.include?(entry["user"]) end end res rescue Mysql2::Error=> e @logger.error("MySQL connection failed: [#{e.errno}] #{e.error}") [] end |
vcap-services-base /lib /base /node.rb |
def node_ready?() # Service Node subclasses can override this method if they depend # on some external service in order to operate; for example, MySQL # and Postgresql require a connection to the underlying server. true end |
用於將需要發送的信息進行編碼。
vcap-services-base /lib /base /node.rb |
# Helper def encode_success(response) response.success = true response.encode end
def encode_failure(response, error=nil) response.success = false if error.nil?|| !error.is_a?(ServiceError) error = ServiceError.new(ServiceError::INTERNAL_ERROR) end response.error = error.to_hash response.encode end |
返回當前Node主機地址。
vcap-services-base /lib /base /node.rb |
def get_host @fqdn_hosts ?Socket.gethostname :@local_ip end |
第二部分 Service Node實現
假如我們要實現一個MyService。
對於Service Node的編寫所需要實現那些內容基本上在之前的章節中已經拆散來講了。可以使用Echo Service的源碼爲模板進行開發,重點是實現相應的方法。
查看【啓動流程】章節說明,Node啓動涉及了2個類——NodeBin和Node,所以這裏需要兩個源碼文件分別實現者兩個類。在【service_name】一節中提到,慣例上會將service_name方法單獨封裝,所以這裏還需要一個源碼文件;此外,在【default_config_file】中提到Node還需要導入配置文件,所以還需要編寫一個配置文件。整理後如表9-1所示:
表9-1 Node源碼實現所需文件列表
源碼文件 |
說明 |
示例 |
NodeBin源碼文件 |
作爲Node的啓動文件,放在bin目錄下 |
vcap-services /echo /bin /echo_node |
Node源碼文件 |
作爲Node的核心功能文件,放在lib目錄下 |
vcap-services /echo /lib /echo_service /echo_node.rb |
service_name封裝 |
作爲一個模塊封裝,放在lib目錄下 |
vcap-services /echo /lib /echo_service /common.rb |
配置文件 |
放在config目錄下 |
vcap-services /echo /config /echo_node.yml |
我們以Echo的源碼爲示例進行分析。
代碼9.1:vcap-services / echo /bin /echo_node |
#!/usr/bin/env ruby #定義使用的解釋器 # -*- mode: ruby -*- # # Copyright (c) 2009-2011 VMware, Inc.
ENV["BUNDLE_GEMFILE"]||=File.expand_path("../../Gemfile",__FILE__) require'bundler/setup' require'vcap_services_base' #需要加載service base庫
$LOAD_PATH.unshift(File.expand_path("../../lib",__FILE__)) #將Echo目錄下的lib庫加入環境變量 require"echo_service/echo_node" #加載Echo的Node類
#必須實現:Echo Node類的命名空間。參考【創建一個Echo::Node實例】和【node_class】 VCAP::Services::Echo::Node end
#返回默認配置文件路徑,參考【初始化參數配置】和【default_config_file】 def default_config_file File.join(File.dirname(__FILE__),'..','config', 'echo_node.yml')#一般會選擇放在config目錄下 end
#對於附加參數的處理,參考【addition_config_file】 def additional_config(options, config) options[:port]= parse_property(config,"port",Integer) options end
end
VCAP::Services::Echo::NodeBin.new.start |
代碼9.2:vcap-services / echo /lib /echo_service /echo_node.rb |
# Copyright (c) 2009-2011 VMware, Inc. require"fileutils" require"logger" #導入日誌文件系統,參考【@logger】 require"datamapper"#導入數據庫中間件,參考【ProvisionService】 require"uuidtools"
#定義Echo::Node module Services module Echo class Node<VCAP::Services::Base::Node#繼承自Base::Node end end end end
require"echo_service/echo_error"
include VCAP::Services::Echo#導入Error處理方法
#定義Service實例表,參考【ProvisionService】 include DataMapper::Resource#導入數據庫中間件 property :name,String,:key => true#定義數據庫表項 end
def initialize(options) super(options) #調用父類中的初始化方法
#這三個參數其實不用這裏初始化,在NodeBin的源碼中已經實現了,參考【配置參數】 @port= options[:port] @base_dir= options[:base_dir] @supported_versions= ["1.0"] end
#參考【pre_send_announcement】 def pre_send_announcement Super #該語句沒用 FileUtils.mkdir_p(@base_dir)if@base_dir start_db #啓動數據庫 @capacity_lock.synchronizedo#初始化剩餘容量 ProvisionedService.all.eachdo |instance| @capacity-= capacity_unit end end end
#參考【send_node_announcement】 def announcement @capacity_lock.synchronizedo#返回需要通告的Hash表 { :available_capacity=>@capacity, :capacity_unit=> capacity_unit } end end
#創建Service實例,參考【主題】和【創建Service實例】 def provision(plan, credential= nil, version=nil) instance = ProvisionedService.new#創建一個新的數據庫表項 if credential #初始化Service實例名字 instance.name = credential["name"] else instance.name =UUIDTools::UUID.random_create.to_s end
begin #保存Service實例 save_instance(instance) rescue => e1 #在出現錯誤的情況下,進行恢復,需要銷燬創建一半的數據庫項 @logger.error("Could not save instance: #{instance.name}, cleanning up") begin destroy_instance(instance) rescue => e2 @logger.error("Could not clean up instance: #{instance.name}") end raise e1 end
gen_credential(instance) #需要返回實例的具體認證信息 end
#銷燬一個Service實例,參考【銷燬Service instance】 def unprovision(name, credentials= []) return if name.nil? @logger.debug("Unprovision echo service: #{name}") instance = get_instance(name)#從數據庫中找到該實例 destroy_instance(instance) #銷燬該表項 true #返回成功標誌 end
#將一個APP與Service實例綁定,參考【APP與Service instance綁定】,注意,在Echo::Node中其實並未實現該方法。 def bind(name, binding_options, credential= nil) instance = nil if credential instance = get_instance(credential["name"]) else instance = get_instance(name) end gen_credential(instance) end
#將一個APP與Service實例解除綁定,參考【APP與Service instance綁定】,注意,在Echo::Node中其實並未實現該方法。 def unbind(credential) @logger.debug("Unbind service: #{credential.inspect}") true end
#參考【ProvisionService】 def start_db DataMapper.setup(:default,@local_db) DataMapper::auto_upgrade! end
def save_instance(instance) raise EchoError.new(EchoError::ECHO_SAVE_INSTANCE_FAILED, instance.inspect) unless instance.save end
raise EchoError.new(EchoError::ECHO_DESTORY_INSTANCE_FAILED, instance.inspect) unless instance.destroy end
def get_instance(name) instance = ProvisionedService.get(name) raise EchoError.new(EchoError::ECHO_FIND_INSTANCE_FAILED,name)if instance.nil? instance end
def gen_credential(instance) credential = { "host"=> get_host, "port"=>@port, "name"=> instance.name } end end |
在Echo Service Node的源碼中並沒有實現管理Serviceinstance的方法。
vcap-services /echo /lib /echo_service /common.rb |
# Copyright (c) 2009-2011 VMware, Inc. #按照此格式封裝即可 moduleVCAP module Services module Echo module Common def service_name "EchoaaS" end end end end end |
vcap-services /echo /config /echo_node.yml |
--- plan:free capacity:100 local_db:sqlite3:/var/vcap/services/echo/echo_node.db mbus:nats://localhost:4222 base_dir:/var/vcap/services/echo/ index:0 logging: level: debug pid:/var/vcap/sys/run/echo_node.pid node_id:echo_node_1 port:5002 supported_versions:["1.0"] |
【--?--還未整理】