Cloud Foundry Service Node源码分析及实现【附下载】

来源:互联网 发布:cn域名需要备案吗 编辑:程序博客网 时间:2024/06/05 04:46

从Word拷贝过来结果好多都变形了,附加下载地址:http://download.csdn.net/detail/wearenoth/5060429

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】的方式注明。如果看不懂了,可以跳跃到相应内容查看。

²  如果还看不懂,建议上网查阅相应资料。

²  文中不免有大量表述不清晰、谬误之处,欢迎发送邮件webothh@126.com。

文中主要以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传数据了:

[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

 

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 implement the 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)unless instance.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"]

 

完整配置文件

【--?--还未整理】