Cloud Foundry Service Node源碼分析及實現

Cloud Foundry Services源碼分析之Node

引言

Service結構

在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結構的消息總線,所以在源碼中會經常見到調用subscribepublish方法。

DEA

APP運行的容器,Service就是爲APP提供服務。在源碼中會見到Node中提供bind方法,就是爲了將創建的Service實例與APP綁定,APP能夠訪問Service實例。

Cloud Controller

Cloud Foundry最核心的控制大腦。它接受各個組件和Client的請求進行處理,然後通過NATS向相應的組件發送指令,要求其執行。在源碼中Node接收到如provision這樣的請求,這些請求是由Cloud Controller發送給GatewayGateway經過負載均衡後再向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.1vcap-services / echo /bin /echo_node

VCAP::Services::Echo::NodeBin.new.start

 

創建Echo::NodeBin實例

因爲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.2vcap-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.3vcap-services-base / lib /base /node_bin.rb  #start

      config = YAML.load_file(config_file)

最後進行參數配置,如代碼2.4所示。需要配置的參數內容非常複雜,參數的配置主要分爲4個部分,除去Node需要的基本配置參數外,還包含有Warden的配置參數,日誌文件的配置參數以及附加參數,詳細部分參看【配置參數】。

代碼2.4vcap-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]


    options = additional_config(options, config)

注意:在代碼2.4最後一行表示Service開發者也可以添加自定義的參數。在Base::NodeBin中提供了抽象方法additional_options來方便service開發者添加自己所需要的參數,詳細部分參考【additional_config】

創建Echo::Node實例

代碼2.5所示,在Base::NodeBin中start方法的結束部分開始創建一個Echo::Node實例。EM是指的是eventmachine,是一個異步事件處理機,與Node.js類似,更多內容參考【NATS與Event Machine】。

注意:其中node_class方法與default_config_file方法一樣需要在Echo::NodeBin中進行重寫,詳細說明見【node_class】。

代碼2.5vcap-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.6vcap-services / echo /lib /echo_service /echo_node.rb  #initialize

  def initialize(options)

    super(options)

  end

連接到NATS

連接到NATS是在在Base::Base的initialize方法中完成。但是在調用Base::Base的initialize方法之前,事先調用的是Base::Node中的initialize方法,如代碼2.7所示。而在Base::Node中最開始則會調用Base::Base中的initialize方法連接到NATS。

代碼2.7vcap-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.9vcap-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.10vcap-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.11vcap-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.12vcap-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.13vcap-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中的參數配置,中間會重點介紹幾個在下文中經常見到的參數(個人感興趣)。要弄明白一個參數的作用需要“大膽假設,小心求證”,我沒辦法顧及所有情況,所以無法保證我下文中所寫都是正確的。如果覺得有什麼地方有問題,歡迎指正。

default_config_file

在【初始化參數配置】一節中,ServiceNode啓動最開始需要指定一個默認配置文件,在代碼2.2中使用default_config_file方法來指定這個文件的路徑。對於default_config_file方法的定義在Base::NodeBin中,如代碼3.1所示。對於abstract的說明參考【abstract】,我們可以將它的作用類比做C++中的純虛函數,在下文中會多次見到這樣的聲明。

代碼3.1vcap-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.2vcap-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即可。

additional_config

在【初始化參數配置】一節中,Service開發者還可以按照自己的意願添加參數,而對於這些新增參數的初始化過程則是在additional_config方法中進行處理,其定義如代碼3.3所示。

代碼3.3vcap-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.4vcap-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即可。

parse_property

在Base::NodeBin中將所有經過parse_property方法處理後的參數結果存入在options(一個Hash表)中。這個處理過程的格式如代碼3.5所示。

代碼3.5vcap-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.6vcap-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.7vcap-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

 

capacity

在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.8vcap-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.9vcap-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.10vcap-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.11vcap-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.12vcap-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.13vcap-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.14vcap-services-base / lib /base /node.rb  #on_unprovision

  def on_unprovision(msg, reply)

      ……

      @capacity_lock.synchronize{@capacity+= capacity_unit }

      ……

warden

【--?--沒怎麼看懂它做什麼用】

 


NATS與Event Machine

NATS作爲整個CloudFoundry的消息總線,不作爲本文介紹的重點。不過在下文運行過程介紹中會經常遇到它。所以還是需要知道一下它的一些簡單使用。可以參考博文:http://blog.csdn.net/zdq0394/article/details/7860041

 

 

 


主題

在連接到NATS後,每個ServiceNode都會向節點訂閱它所關心的主題,相關內容參考【訂閱主題】。

訂閱主題的過程在Base::Node的on_connect_node方法中實現,實現過程如代碼5.1所示。

代碼5.1vcap-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
dump_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

send_node_announcement

第一個週期任務在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.2vcap-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.3vcap-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

announcement

Service Node定期調用【send_node_announcement】方法向Gateway發送Node的聲明,聲明的內容除去三個必須含有的信息——@node_id、@plan、@supported_version——外,官方專門預留announcement方法提供給service開發人員進行擴展。

代碼6.4所示,官方要求必須實現這個方法。announcement方法需要處理的內容就是需要提供給Gateway的信息。

代碼6.4vcap-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.5vcap-services / echo /lib /echo_service /echo_node.rb

  def announcement

    @capacity_lock.synchronizedo

      { :available_capacity=>@capacity,

        :capacity_unit=> capacity_unit }

    end

  end

Gateway on_announce

Service Node定期發送的announcement最後由ServiceGateway接收。在代碼6.3中,Service Node每次發送announcement週期任務的時候使用的主題是“#{service_name}.announce”。如代碼6.5.1所示,在ServiceGateway中,對應於該主題的處理方法是on_announce方法。

代碼6.5.1vcap-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.2vcap-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

 

update_varz

第二個週期任務任務在Base::Node#initialize方法中添加,如代碼6.6所示。其中

²  z_interval就是【配置參數】中配置,如果不配置,則會使用默認值30。

²  週期任務的主要流程都在update_varz中執行。

代碼6.6vcap-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.7vcap-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 commoncomponent.rb中。這個模塊已經成爲了一個gem,你不必裝Cloud Foundry,而只需要gem install一個都可以使用了。在vcap common中,如果有組建來註冊,他會爲這個組件建立一個server。然後serverport以及帳號密碼默認是cf自己生成的。但是按照上文的配置,這些參數就會被傳入,我們就可以按照自己的參數來配置這個server了。

     在組件向component註冊完成之後,組建就可以通過以下方式向varz傳數據了:

1         #這是dea的狀態更新      

2               VCAP::Component.varz[:running_apps] = running_apps 

3               VCAP::Component.varz[:frameworks] = metrics[:framework

4               VCAP::Component.varz[:runtimes] = metrics[:runtime

 

varz_details

在【update_varz】中介紹了CloudFoundry中每個組件都會向VCAP::Component註冊自己,然後可以向varz傳入參數。的傳入的參數則是根據varz_details得到。代碼6.8給出了varz_details的定義,默認情況下varz_details的返回值就是announcement的返回值。該方法可以重寫,只需要返回內容是Hash表即可。

代碼6.8vcap-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.9vcap-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 instance

provision主題

創建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就根據收到的主題內容使用對應的處理函數進行處理。

provision過程

一個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

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.1vcap-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

provision方法要求Service開發人員必須實現,其定義如代碼7.2所示,它需要實現的功能就是實現在Local Node中創建一個Service 實例。

代碼7.2vcap-services-base / lib /base /node.rb   

  # Service Node subclassesmust implementthe following methods

  # provision(plan) --> {name, host, port, user, password}, {version}

  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.3vcap-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.4vcap-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

實現provision

我們考察Echo Service中provision方法的實現,如代碼7.5,其中credentials這個參數中包含的內容就是使用vmc create進行創建一個service的時候傳入的參數【--?這個地方有待驗證--】。所以credentials參數中最少會含有一個參數名稱“name”。

注:這個name參數是用戶希望創建的Service實例的名字。

代碼7.5vcap-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.6vcap-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.7vcap-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 instance

銷燬一個Service實例,過程與provision類似,對應主題“#{service_name}.unprovision.#{@node_id}”。在收到unprovision主題後由on_unprovision方法處理。

on_unprovision

on_unprovision是on_provision方法的一個逆過程,它的做法就是銷燬一個已經存在的Service實例。其源碼如代碼7.8所示。

²  因爲創建一個Service實例的主要動作是Service開發人員在provision方法中編寫的,CloudFoundry無法得知如何去註銷個Service實例,所以也提供了一個相應的方法——unprovision方法,該方法也需要Service開發人員進行編寫。

²  方法中提供了unprovision使用的兩個參數,name是需要銷燬的service實例的名字;bindings是與該service實例綁定的APP信息。

代碼7.8vcap-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

unprovision定義如代碼7.9所示,unprovision方法必須重寫,它實現了銷燬名字爲name的Service實例。

代碼7.9vcap-services-base / lib /base /node.rb

  # Service Node subclasses must implement the following methods

  # unprovision(name) --> void

  abstract :unprovision

源碼中對於unprovision的註釋存在一些問題,其需要帶入兩個參數,必須有1個返回值。整理後入表7-2所示:

表7-2unprovision方法說明

函數名:unprovision

參數名稱

說明

輸入參數

name

Service實例名字

bindings

與該Service實例綁定的APP信息

返回值

bool

是否成功

實現unprovision

相應的,我們考察Echo::Node中unprovision方法的實現,如代碼7.10所示。這個代碼會出現一些不協調的地方,

²  首先,它將參數bindings的名字改成了credentials,這樣的命名容易讓人誤解其含義;

²  然後,這個方法中並未使用到credentials這個參數,之後我們會考察MySql中是如何使用這個參數的。

²  最後會根據結果返回一個布爾值表明是否成功註銷Service實例。

代碼7.10vcap-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.11vcap-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等方法的執行流程也是一樣的,下文中就列舉了。

綁定與解除綁定

APP與Service instance綁定

創建的Service實例對於APP而言是不可見的,甚至Service還不知道需要創建Service實例(如:在MySql的provision方法中使用Lazy技術延遲創建數據庫),用戶部署的APP如果希望能夠見到Service實例,就需要將APP與Service實例進行綁定。APP與Service綁定過程類似於Service實例的創建過程,對應的主題是“#{service_name}.bind.#{@node_id}”。在收到unprovision主題後由on_bind方法處理。

on_bind

代碼7.12所示,on_bind的源碼格式和on_provision的格式基本相同,也是確定參數,調用bind方法,向Service Gateway返回結果這樣一個流程。

代碼7.12vcap-services / mysql /lib /mysql_service /node.rb

  def on_bind(msg, reply)

    response = BindResponse.new

    rollback = lambdado |res|

      unbind(res.credentials)

end


    timing_exec(
@op_time_limit, rollback)do

      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

bind方法也是預留給Service開發人員使用的接口,這個方法在源碼中的定義如代碼7.13所示。註釋給出的參數也存在一些偏差,表7-3整理了bind方法的用法。

代碼7.13vcap-services-base / lib /base /node.rb 

  # Service Node subclasses must implement the following methods

  # bind(name, bind_opts) --> {host, port, login, secret}

  abstract :bind

表7-3 bind方法說明(不完整)

函數名:bind

參數名稱

說明

輸入參數

name

APP需要綁定的Service實例的名字

bind_opts

綁定時候附加的參數

credentials

 

返回值

credentials

 

實現bind

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.14vcap-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與Service instance解除綁定

將APP與Serviceinstance解除綁定,對應主題“#{service_name}.unbind.#{@node_id}”。在收到unbind主題後由on_unbind方法處理。

on_unbind

on_unbind是on_bind的逆過程,處理內容就是將APP與Service實例解除綁定。如代碼7.15所示,真正執行解除綁定的任務是在unbind方法中調用。

代碼7.15vcap-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

unbind方法定義如代碼7.16所示,表7-4整理了unbind方法說明。

代碼7.16vcap-services-base / lib /base /node.rb

  # unbind(credentials) --> void

  abstract :unbind

表7-4unbind方法說明(不完全)

函數名:unbind

參數名稱

說明

輸入參數

ccredentials

 

返回值

bool

是否成功

實現unbind

 

on_restore

【--?--】

管理Service instance方法

Base::Node中提供了一組方法用於管理Service實例的主題與接口。包括了Service實例的啓動、停止、導入、更新等常用操作。這些方法定義如代碼7.17所示:表7-5整理了相應的主題與方法。

代碼7.17vcap-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,其實orphan就是在service節點上已經創建的一個service實例,但是它的存在還沒有通知cloud_controller。比如說,在service節點創建完實例並通知完service gateway,而當gateway返回給cloud_controller時,發生了網絡故障,從而cloud_controller通知終端用戶,創建失敗,當然也不會有信息更新在cloud_controller的數據庫中。這樣的話,就相當於在service_node上創建了一個沒有用的實例,從而導致浪費了一些資源。orphan和沒有綁定的instance是有區別的,在實際情況中經常會出現未經綁定的instance,但是他們在cloud_controller中都是有數據記錄的,而orphan則沒有。一般這種情況很罕見,但是源碼中還是考慮了這一點。

on_check_orphan

 

 on_purge_orphan

orphan的幾個管理函數

小結

 


雜項

在分析源碼的過程中還會遇到許多的輔助方法,這些方法因爲與章節內容相關性很小,一直找不到地方放置。所以乾脆就直接兜在一塊說,這個章節的內容很零散,不需要專門閱讀,只有在用到了或者看不懂的時候再看。

abstract

在前面的章節中經常可以看到“abstract:XXX”這樣的語法格式。因爲我沒有專門學過Ruby,尤其對於Ruby元編程的內容更加不瞭解。以下內容都是個人的臆測。

首先,我們先找到abstract的定義(一開始我以爲它是一個關鍵字,後來發現不是),如代碼8.1所示,它其實也是一個方法,有自己的參數。

該方法就是調用了define_method方法檢查是否實現了args中定義的方法。它所想要實現的就是類似於C++中的純虛函數定義。所以使用了abstract :XXX表示的語句我們可以看做是定義了一個純虛函數XXX,我叫它——抽象方法。

代碼8.1vcap-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

node_class

在【創建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.2vcap-services / echo /bin /echo_node

classVCAP::Services::Echo::NodeBin<VCAP::Services::Base::NodeBin

  ……

  def node_class

    VCAP::Services::Echo::Node

  end

  ……

end

注意:該方法要求Service開發者必須實現。

pre_send_announcement

在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.3vcap-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

@logger

Cloud Foundry實現了實現了一個日誌文件系統,每個組件都會存在一個@logger實例,它就表示了該日誌系統。我們並不關心該日誌系統的實現,更關注於它的使用。

代碼8.4可以看出,與日誌文件系統的相關配置參數是logging,每個節點的日誌是根據node_id生成的。Logging參數配置可以參考【配置參數】。在開發期間還是選擇debug模式。

代碼8.4vcap-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.5vcap-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

在【運行過程】一節中,會經常遇到timing_exec方法。這個方法其實很簡單,就是要求在time_limit時間內完成代碼塊中的內容,如果超時,就調用roolback並拋出異常,如代碼8.6所示。

代碼8.6vcap-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

ProvisionedService

在看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.7vcap-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),在【訂閱主題】和【主題】兩節中,我們知道service_name作爲Node訂閱主題中關鍵的一個數據結構。如代碼8.9所示,源碼中要求Service Node和Provisioner節點都必須實現該方法。

代碼8.9vcap-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.10vcap-services / echo /lib /echo_service /common.rb 

moduleVCAP

  module Services

    module Echo

      module Common

        defservice_name

          "EchoaaS"

        end

      end

    end

  end

end

代碼8.11vcap-services / echo /lib /echo_service /echo_node.rb

classVCAP::Services::Echo::Node

  includeVCAP::Services::Echo::Common

注意:該方法要求Service開發人員必須實現

all_instance_list

在【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.11vcap-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.12vcap-services / mysql /lib /mysql_service /node.rb

  def all_instances_list

    ProvisionedService.all.map{|s| s.name}

  end

注意:該方法要求Service開發人員必須實現。

all_binding_list

 

 

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

 

node_ready?

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

 

encode_XXX

用於將需要發送的信息進行編碼。

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

 

get_host

返回當前Node主機地址。

vcap-services-base /lib /base /node.rb

  def get_host

    @fqdn_hosts ?Socket.gethostname :@local_ip

  end

 


第二部分 Service Node實現

1.       自定義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的源碼爲示例進行分析。

NodeBin類實現

方法彙總

 

代碼示例

代碼9.1vcap-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" #加載EchoNode


class
VCAP::Services::Echo::NodeBin<VCAP::Services::Base::NodeBin#繼承Base::NodeBin

 

  #必須實現:Echo Node類的命名空間。參考【創建一個Echo::Node實例】和【node_class
  def 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

Node類實現

方法彙總

 

代碼示例

代碼9.2vcap-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
VCAP

  module Services

    module Echo

      class Node<VCAP::Services::Base::Node#繼承自Base::Node

      end

    end

  end

end


require"echo_service/common"#加載service_name方法,參考【service_name

require"echo_service/echo_error"


class
VCAP::Services::Echo::Node


  include
VCAP::Services::Echo::Common#導入service_name方法

  include VCAP::Services::Echo#導入Error處理方法

 

  #定義Service實例表,參考【ProvisionService
  class ProvisionedService

    include DataMapper::Resource#導入數據庫中間件

    property :name,String,:key => true#定義數據庫表項

  end


 
#實現Service Node的初始化,參考【啓動流程】。

  def initialize(options)

    super(options) #調用父類中的初始化方法

 

    #這三個參數其實不用這裏初始化,在NodeBin的源碼中已經實現了,參考【配置參數】
    @local_db= options[:local_db]

    @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

 

  #將一個APPService實例綁定,參考【APPService 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

 

  #將一個APPService實例解除綁定,參考【APPService 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


  def
destroy_instance(instance)

    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的方法。

service_name的封裝

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"]

 

完整配置文件

【--?--還未整理】


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章