从Word拷贝过来结果好多都变形了,附加下载地址:http://download.csdn.net/detail/wearenoth/5060429
Cloud Foundry Services源码分析之Node
在Cloud Foundry中Service的结构不是太复杂,由两个组件组成——Gateway、Node。如图1展示了Service相关的几个主要组件,每个组件有十分明确的分工:
图 1 Service相关主要组件
Gateway |
其它组件(Cloud Controller)访问Node的入口,它对外提供了对Node进行管理的一套“接口”。同时它对外隐藏内部Node的结构,这样外部的组件就可以忽略内部Node的情况,只需要关心Service实例的创建、绑定的动作。 |
Node |
负责管理Service,包括创建(provision)、注销(unprovision)、绑定(bind)、启用(enable)、禁用(disabled)等操作。Node不是Service的提供者,它是本地Service的管理者。 |
NATS |
最底层的消息总线,各大组件间的通信都由它进行转发。是一个P/S结构的消息总线,所以在源码中会经常见到调用subscribe、publish方法。 |
DEA |
APP运行的容器,Service就是为APP提供服务。在源码中会见到Node中提供bind方法,就是为了将创建的Service实例与APP绑定,APP能够访问Service实例。 |
Cloud Controller |
Cloud Foundry最核心的控制大脑。它接受各个组件和Client的请求进行处理,然后通过NATS向相应的组件发送指令,要求其执行。在源码中Node接收到如provision这样的请求,这些请求是由Cloud Controller发送给Gateway,Gateway经过负载均衡后再向Node发出。 |
本文主要介绍Cloud Foundry中Node的实现,图2绘制了CloudFoundry中Services中Node点的类图。
注意:Echo::XXX并不属于CloudFoundry中的内容,而是一个简单的Service的Node实现,也是本文中主要进行分析的Service实现。添加Echo Service只要将自定义的Echo::NodeBin通过配置到启动文件中,就可以在启动Cloud Foundry的时候自启动自定义Service。
整个Service Node分为2个层次结构,一部分是CloudFoundry提供的Base模板,如类图中的Base::XXX部分,这部分已经帮助Service开发人员完成了大部分Service的开发工作;另一部分是XXX Service部分,这部分就是Service开发人员所需要进行编写的代码,如类图中的Echo::XXX。
图中每个类都有其明确的职责,在下文其余部分会一点点进行分析。
图2 Echo Node类图
图2中类图分为两个层次。其中Base下的类都是系统提供的ServiceNode模板,也是下文中重点分析对象;自定义Service就是通过继承NodeBin和Node两个类,然后实现模板中预留的接口,在最后我们将会详细介绍如何去编写这部分的代码,这也是Service开发人员需要完成的部分。表1介绍了每个类实现的功能。
表 1 类功能说明
类名 |
功能 |
Base::Base |
提供了整个Service通信框架,主要是提供了NATS的连接与初始化。 |
Base::Node |
实现了Service Node功能模板,预留了大量的抽象方法便于Service开发者实现自定义的功能。 |
Base::NodeBin |
主要完成Service Node的初始化配置,然后将配置参数传入Base::Node。 |
Echo::NodeBin |
需要实现Base::NodeBin下的两个指明调用的Node点以及配置文件的方法即可。 |
Echo::Node |
需要实现Base::Node下的抽象方法,实现Service Node的实例创建、管理、注销、重启等操作。 |
第一部分:源码分析
源码之间的关系错综复杂,很多地方都有一种剪不断理还乱的感觉,所以在阅读该部分内容时,建议:
² 一边阅读源码,一边阅读本文。文中引用代码都给出了“路径名 #方法名”的格式。路径名是Cloud Foundry在GitHub上面的路径,源码直接到https://github.com/cloudfoundry上查阅。
² 文中引用代码都尽量截取了我感兴趣的部分,这部分会以灰色背景标识;此外一些地方代码被稍微修改。
² 在讲述某个方面内容时,为保证内容尽可能不跑题,所以相关的内容都会给出其可以参考的章节,这些地方都会以【XXX】的方式注明。如果看不懂了,可以跳跃到相应内容查看。
² 如果还看不懂,建议上网查阅相应资料。
² 文中不免有大量表述不清晰、谬误之处,欢迎发送邮件[email protected]。
文中主要以Echo Service为例,其他的Service也是一样的道理。
图2-1 Node启动流程
以Echo Service为例,图2-1描绘了Node的启动流程。Service中Node的启动过程其实不是太复杂,真正复杂的是配置参数与相应主题的功能。
注意:步骤中创建Echo::XXX实例不能算作启动流程的主要部分,它们仅仅只是调用了各自的new方法而已,描述它们是为了标识流程的执行方向才加入说明。
Service Node的启动入口位于自定义Service目录中bin目录下对应ServiceNode的可执行文件中,如代码2.1所示,启动Service Node代码看似简单,实际它后续的工作却是很复杂。
代码2.1:vcap-services / echo / bin / echo_node |
VCAP::Services::Echo::NodeBin.new.start |
因为Echo::NodeBin继承自Base::NodeBin,所以它也继承了Base::NodeBin所有的功能。创建Echo::NodeBin实例的过程就在代码2.1中——调用new方法创建Echo::NodeBin实例。创建后的实例调用的start方法是在Base::NodeBin方法中实现。start方法中就包含了启动流程中后续的所有过程。
注意:Service开发者不需要去重新实现该方法,启动过程中需要附加的一些功能,Cloud Foundry提供了2个Hook方法给Service开发者。分别为【pre_send_announcement】方法和【additional_config】方法。
Echo::NodeBin调用start方法定义在Base::NodeBin中,执行的第一个步骤就是进行所有参数的初始化。初始化参数配置代码过程很简单,就是确定配置文件->载入配置文件->初始化参数列表。
如代码2.2所示:首先需要确定默认的配置文件,对于default_config_file方法的说明可以查看【default_config_file】。OptionParser部分的内容一般不会得到执行,如果用户指定了启动过程中的配置文件,则会使用新的配置文件,否则使用默认配置文件。当然一般情况下这个opt参数是没有进行指定的,如果需要指定,可以对启动脚本进行修改。
代码2.2:vcap-services-base / lib / base / node_bin.rb #start |
def start config_file = default_config_file OptionParser.new do |opts| opts.banner = "Usage: #{$0.split(/\//)[-1]}[options]" opts.on("-c", "--config [ARG]", "Configuration File") do |opt| config_file = opt end …… |
然后载入配置文件,如代码2.3所示。配置文件采用YAML格式存储,最后得到的config中存储的是一个Hash表。
代码2.3:vcap-services-base / lib / base / node_bin.rb #start |
config = YAML.load_file(config_file) |
最后进行参数配置,如代码2.4所示。需要配置的参数内容非常复杂,参数的配置主要分为4个部分,除去Node需要的基本配置参数外,还包含有Warden的配置参数,日志文件的配置参数以及附加参数,详细部分参看【配置参数】。
代码2.4:vcap-services-base / lib / base / node_bin.rb #start |
options = { :index => parse_property(config, "index", Integer, :optional => true), …… # Wardenized service configuration :base_dir => parse_property(config, "base_dir", String, :optional => true), …… }
# Workaround for services that support running the service both inside and outside warden use_warden = parse_property(config, "use_warden", Boolean, :optional => true, :default => false) if use_warden warden_config = parse_property(config, "warden", Hash, :optional => true) …… options[:port_range] = parse_property(warden_config, "port_range", Range) …… end
VCAP::Logging.setup_from_config(config["logging"]) # Use the node id for logger identity name. options[:logger] = VCAP::Logging.logger(options[:node_id]) @logger = options[:logger]
|
注意:在代码2.4最后一行表示Service开发者也可以添加自定义的参数。在Base::NodeBin中提供了抽象方法additional_options来方便service开发者添加自己所需要的参数,详细部分参考【additional_config】
如代码2.5所示,在Base::NodeBin中start方法的结束部分开始创建一个Echo::Node实例。EM是指的是eventmachine,是一个异步事件处理机,与Node.js类似,更多内容参考【NATS与Event Machine】。
注意:其中node_class方法与default_config_file方法一样需要在Echo::NodeBin中进行重写,详细说明见【node_class】。
代码2.5:vcap-services-base / lib / base / node_bin.rb #start |
EM.run do node = node_class.new(options) …… end end |
在Echo Service中node_class方法返回的就是Echo::Node,所以调用new方法创建时调用的就是Echo::Node的initialize方法,如代码2.6所示。在Echo::Node的initialize方法中可以不做其它工作,直接调用父类中的initialize方法即可。
注意:该方法必须使用super调用父类(Base::Node)中的initialize方法。因为订阅主题的主题工作是在Base::Node的on_connect_node方法中进行实现,同时连接到NATS与工作也是在Base::Node的父类(Base::Base)中完成。
代码2.6:vcap-services / echo / lib / echo_service / echo_node.rb #initialize |
def initialize(options) super(options) end |
连接到NATS是在在Base::Base的initialize方法中完成。但是在调用Base::Base的initialize方法之前,事先调用的是Base::Node中的initialize方法,如代码2.7所示。而在Base::Node中最开始则会调用Base::Base中的initialize方法连接到NATS。
代码2.7:vcap-services-base / lib / base / node.rb #initialize |
def initialize(options) super(options) |
连接到NATS看似复杂,但是对于所有的连接到NATS的节点来说,调用流程是一样。如代码2.9所示,调用NATS的connect方法连接到NATS,在其代码块中:
² 向Component注册【--?--】,参考【周期任务】中关于update_varz的说明;
² 调用on_connect_node方法订阅主题,参考【订阅主题】。
代码2.9:vcap-services-base / lib / base / base.rb #initialize |
if options[:mbus] …… @node_nats = NATS.connect(:uri => options[:mbus]) do status_port = status_user = status_password = nil if not options[:status].nil? status_port = options[:status][:port] status_user = options[:status][:user] status_password = options[:status][:password] end
@logger.debug("Registering with NATS") VCAP::Component.register(:nats => @node_nats, :type => service_description, :host => @local_ip, :index => options[:index] || 0, :config => options, :port => status_port, :user => status_user,:password => status_password) on_connect_node end |
Base::Base提供了一个连接到NATS的通信框架。订阅主题是在Base::Base的initialize方法中调用on_connect_node方法进行订阅,参见代码2.9中倒数第二行。
注意,不要将on_connect_node理解成是创建与NATS的连接过程,它做的事情是ServiceNode向NATS订阅主题以及添加周期任务(参考【添加周期任务】)。
如代码2.10所示,根据对于该方法的注释我们知道:
² 该方法必须在Base::Node和Base::Provision中进行重写;
² 自定义Services中不能重写该方法。
代码2.10:vcap-services-base / lib / base / base.rb |
# Subclasses VCAP::Services::Base::{Node,Provisioner} implement the # following methods. (Note that actual service Provisioner or Node # implementations should NOT need to touch these!) # TODO on_connect_node should be on_connect_nats abstract :on_connect_node |
订阅主题过程在Base::Node的on_connect_node方法中进行实现,如代码2.11所示。这段代码读起来有些拗口,这部分内容将【主题】中展开讨论,这里只需要它订阅了主题即可。
代码2.11:vcap-services-base / lib / base / node.rb #on_connect_node |
def on_connect_node …… %w[provision unprovision bind unbind restore disable_instance enable_instance import_instance update_instance cleanupnfs_instance purge_orphan ].each do |op| eval%[@node_nats.subscribe("#{service_name}.#{op}.#{@node_id}") { |msg, reply| EM.defer{ on_#{op}(msg, reply) } }] end %w[discover check_orphan].each do |op| eval%[@node_nats.subscribe("#{service_name}.#{op}") { |msg, reply| EM.defer{ on_#{op}(msg, reply) } }] end |
在启动流程的最后一部分工作就是添加周期任务,就是Service Node在之后正常运行过程中定期执行的动作。添加的周期任务有两个:
² 向Gateway发送本地相关运行状态,Gateway可以根据这些信息可以更方便的管理多个Service Node,实现Service Node间的均衡。
² 向Component注册信息。
更多关于周期任务的内容参考【周期任务】。
如代码2.12所示,其中一个在on_connect_node方法的最后会设置周期任务,这个周期任务就是实现了向Gateway发送本地相关的运行状态信息。周期任务的执行体全部在send_node_announcement中完成。
代码2.12:vcap-services-base / lib / base / node.rb #on_connect_node |
pre_send_announcement send_node_announcement EM.add_periodic_timer(30) { send_node_announcement } end |
如代码2.13所示,另外一个则是在Base::Node的initialize方法最后,也就是连接到NATS之后,这个周期任务的执行体则是在update_varz中完成。
代码2.13:vcap-services-base / lib / base / node.rb #initialize |
@supported_versions = options[:supported_versions] || [] z_interval = options[:z_interval] || 30 EM.add_periodic_timer(z_interval) do EM.defer { update_varz } end if @node_nats |
该章节内容简要介绍了ServiceNode的启动流程。
² 配置参数的初始化配置时Node启动过程中最为复杂的部分,无论是Service开发人员还是Cloud Foundry管理人员都需要花费大量精力去学习这部分的内容。
² 订阅主题也是Node启动过程中较为复杂的一部分,而到了运行过程中,主题这部分就显得尤为重要。
² 周期任务看似复杂,但是需要Service开发人员注意的地方其实并不多,所以最后我们其实不需要太多的精力去学习它。
² 与NATS的连接也不需要花费太多的心思,它仅仅只是一个消息总线而已,只需要知道如何使用它发送和接收消息即可。
在后面的章节中,我们会分别展开这些部分的内容,一一分析其中的一些机制。
本章节内容主要讲解Service Node中的参数配置,中间会重点介绍几个在下文中经常见到的参数(个人感兴趣)。要弄明白一个参数的作用需要“大胆假设,小心求证”,我没办法顾及所有情况,所以无法保证我下文中所写都是正确的。如果觉得有什么地方有问题,欢迎指正。
在【初始化参数配置】一节中,ServiceNode启动最开始需要指定一个默认配置文件,在代码2.2中使用default_config_file方法来指定这个文件的路径。对于default_config_file方法的定义在Base::NodeBin中,如代码3.1所示。对于abstract的说明参考【abstract】,我们可以将它的作用类比做C++中的纯虚函数,在下文中会多次见到这样的声明。
代码3.1:vcap-services-base / lib / base / node_bin.rb |
classVCAP::Services::Base::NodeBin abstract :default_config_file |
对于default_config_file方法说明参考表3-1。
表3-1default_config_file方法说明
函数名:default_config_file |
参数名称 |
说明 |
输入参数 |
- |
|
返回值 |
file path |
默认配置文件的路径,例如:File.join(File.dirname(__FILE__), '..', 'config', 'XXX.yml') |
如代码3.2所示,在实现EchoService的时候,就需要Echo::NodeBin中进行重写default_config_file方法。重写内容很简单,只需要返回读入的默认文件的路径即可。
代码3.2:vcap-services / echo / bin / echo_node |
classVCAP::Services::Echo::NodeBin<VCAP::Services::Base::NodeBin …… def default_config_file File.join(File.dirname(__FILE__), '..', 'config', 'echo_node.yml') end end |
注意:Service开发者必须实现该方法,实现格式参考代码3.2即可。
在【初始化参数配置】一节中,Service开发者还可以按照自己的意愿添加参数,而对于这些新增参数的初始化过程则是在additional_config方法中进行处理,其定义如代码3.3所示。
代码3.3:vcap-services-base / lib / base / node_bin.rb |
classVCAP::Services::Base::NodeBin abstract:additional_config |
在代码中没有指出该方法需要导入的参数类型,返回查看代码2.4就可以知道这个方法需要导入两个参数——options和config。整理后的方法说明如表3-2所示:
表3-2 additional_config方法说明
函数名:additional_config |
参数名称 |
说明 |
输入参数 |
options |
是一个Hash表,读入的配置参数都会存储在该表中。 |
config |
就是载入的配置文件中的信息内容,它其实就是一个Hash数组,参考【代码2.3】。 |
|
返回值 |
options |
更新后的options表,与原来的options相比,新的options加入了在Service开发者需要的参数。 |
Service开发者只需要在实现该方法时候读入config中新加入的参数即可。例如在Echo Service中,Echo需要使用一个新的参数port表示Service的服务端口号,则如代码3.4所示从config中读入参数即可。
注意:所有的配置参数都在config中存储,最好使用【parse_property】方法从config中读取参数。
代码3.4:vcap-services / echo / bin / echo_node |
def additional_config(options, config) options[:port] = parse_property(config, "port", Integer) options end |
注意:Service开发者必须实现该方法,该方法存在两个参数options和config,而且要求返回值为更新后的options,实现格式参考代码3.4即可。
在Base::NodeBin中将所有经过parse_property方法处理后的参数结果存入在options(一个Hash表)中。这个处理过程的格式如代码3.5所示。
代码3.5:vcap-services-base / lib / base / node_bin.rb |
options[:capacity] = parse_property(config, "capacity", Integer, :optional => true, :default => 200) |
Service ServiceNode的配置参数很多,配置文件以YAML格式编写(Cloud Foundry中经常见到YAML与JSON两种数据交换格式,类似于XML),载入配置文件(参考【代码2.3】)后的参数值保存在config变量中。config其实就是一个Hash数组,此时读入的参数还为经过格式转换,在Base::NodeBin中提供了parse_property方法对对参数进行处理。对于parse_property方法说明如表3-3所示。
表3-3parse_property方法说明
函数名:parse_property |
参数名称 |
说明 |
|
|
|
输入参数 |
config |
就是载入的配置文件Hash数组,整个配置文件内容都在其内部保存。 |
|
|
|
key |
需要查找的参数键值,parse_property方法就是使用config[key]获取到相关参数的值。 |
|
|
|
|
type |
参数最后的类型,经过parse_property方法,该参数最后会转化为type类型返回。 |
|
|
|
|
options |
附加选项,包括了:optional与:default两个,详细情况参考图3-1。 |
|
|
|
|
返回值 |
value |
经过处理的参数值,类型为type类型。 |
|
|
|
对于parse_property方法的定义如代码3.6所示。
代码3.6:vcap-services-base / lib / base / node_bin.rb |
def parse_property(hash, key, type, options = {}) obj = hash[key] if obj.nil? raise "Missing required option: #{key}" unless options[:optional] options[:default] elsif type == Range raise "Invalid Range object: #{obj}" unless obj.kind_of?(Hash) first, last = obj["first"], obj["last"] raise "Invalid Range object: #{obj}" unless first.kind_of?(Integer) and last.kind_of?(Integer) Range.new(first, last) else raise "Invalid #{type}object: #{obj}" unless obj.kind_of?(type) obj end end |
代码虽然不长,但是分支情况比较多,对于config中的参数有如下几种处理情况,如图3-1所示。
图3-1 参数配置函数过程
² 如果在config中配置了该参数,并且不是一个Range类型的数据,就会读取该参数的配置,并转化为type类型
² 如果在config中配置了该参数,而且是一个Range类型的数据,返回类型就是一个Range类型。
² 如果在config中没有配置该参数,并且没有加入:optional=true选项,则会报错,表明该参数必须配置
² 如果在config中没有配置该参数,并且加入:optional=true选项,此时如果参数必须有一个默认值,则返回输入参数中的:default值。
² 如果在config中没有配置该参数,并且加入:optional=true选项,此时如果参数不必须有一个默认值,则返回的参数是一个nil值。
参数很多很复杂,但不是要求每个参数都需要配置,很多参数其实是可选的,对于一些可选参数还提供了默认值。对于Service Node使用的参数整理后如表3-4、表3-5、表3-6所示。
表3-4整理了配置参数值的情况。
表3-4 全局参数配置表(不完全)
编号 |
参数名称 |
options |
变量名 |
类型 |
可选 |
默认值 |
典型值 |
A01 |
index |
options[:index] |
|
Interger |
是 |
|
0 |
A02 |
plan |
options[:plan] |
@plan |
String |
是 |
free |
|
A03 |
capacity |
options[:capacity] |
@capacity @max_capacity |
Interger |
是 |
200 |
|
A04 |
ip_route |
options[:ip_route] |
|
String |
是 |
|
|
A05 |
node_id |
options[:node_id] |
@node_id options[:logger] |
String |
否 |
|
echo_node_1 |
A06 |
z_interval |
options[:z_interval] |
z_interval |
Interger |
是 |
30 |
|
A07 |
mbus |
options[:mbus] |
@node_nats |
String |
否 |
|
nats://localhost:4222 |
A08 |
local_db |
options[:local_db] |
|
String |
否 |
|
sqlite3:/var/vcap/services/echo/echo_node.db |
A09 |
migration_nfs |
options[:migration_nfs] |
@migration_nfs |
String |
是 |
|
|
A10 |
max_nats_payload |
options[:max_nats_payload] |
|
Interger |
是 |
|
|
A11 |
fqdn_hosts |
options[:fqdn_hosts] |
@fqdn_hosts |
Boolen |
是 |
FALSE |
|
A12 |
op_time_limit |
options[:op_time_limit] |
@op_time_limit |
Interger |
是 |
6 |
|
A13 |
supported_version |
options[:supported_version] |
@supported_versions |
Array |
否 |
|
["1.0"] |
A14 |
default_version |
options[:default_version] |
|
String |
否 |
|
"1.0" |
A15 |
max_clients |
options[:max_clients] |
|
Interger |
是 |
|
|
A16 |
database_lock_file |
options[:database_lock_file] |
|
String |
是 |
|
|
A17 |
disabled_file |
options[:disabled_file] |
@disabled_file |
String |
是 |
"/var/vcap/store/DISABLED" |
|
A18 |
|
options[:logger] |
@logger |
String |
- |
|
|
|
logging |
|
|
|
否 |
|
level: debug |
A19 |
pid |
|
pid_file |
String |
否 |
|
/var/vcap/sys/run/echo_node.pid |
A20 |
base_dir |
options[:base_dir] |
|
String |
是 |
|
|
A21 |
service_log_dir |
options[:service_log_dir] |
|
String |
是 |
|
|
A22 |
service_common_dir |
options[:service_common_dir] |
|
String |
是 |
"/var/vcap/store/common" |
|
A23 |
service_bin_dir |
options[:service_bin_dir] |
|
Hash |
是 |
|
|
A24 |
image_dir |
options[:image_dir] |
|
String |
是 |
|
|
A25 |
port_range |
options[:port_range] |
|
Range |
是 |
|
|
A26 |
filesystem_quota |
options[:filesystem_quota] |
|
Boolen |
是 |
FALSE |
|
A27 |
service_start_timeout |
options[:service_start_timeout] |
|
Interger |
是 |
3 |
|
A28 |
service_status_timeout |
options[:service_status_timeout] |
|
Interger |
是 |
3 |
|
A29 |
max_memory |
options[:max_memory] |
|
Numeric |
是 |
|
|
A30 |
memory_overhead |
options[:memory_overhead] |
|
Numeric |
是 |
0 |
|
A31 |
max_disk |
options[:max_disk] |
|
Numeric |
是 |
128 |
|
A32 |
disk_overhead |
options[:disk_overhead] |
|
Numeric |
是 |
0 |
|
A33 |
m_interval |
options[:m_interval] |
|
Interger |
是 |
10 |
|
A34 |
m_actions |
options[:m_actions] |
|
Array |
是 |
[] |
|
A35 |
m_failed_times |
options[:m_failed_times] |
|
Interger |
是 |
3 |
|
² 参数名称:配置文件中参数的名称。 ² options:经过类型转化后的配置参数保存。 ² 变量名:一些参数对应的实例变量。 ² 类型:参数类型 ² 可选:参数是否可选,是表示该参数可以不配置,否表示该参数必须配置。 ² 默认值:部分参数会提供默认值,这部分参数都是可选参数。 ² 典型值:配置文件中的典型配置参数。 |
大部分参数都可以不需要配置,其中参数A05、A07、A08、A13、A14、A19必须在配置文件中编写。
A20~A35的参数是warden的配置,关于warden内容可以参考【warden】。
A21~A28这组参数需要注意,如果在配置文件中将use_warden设置为true,则必须配置文件中加入warden元素,而A21~A28这组元素则可能被warden的中对应的子元素覆盖,如代码3.7所示。
代码3.7:vcap-services-base / lib / base / node_bin.rb #initialize |
use_warden = parse_property(config, "use_warden", Boolean, :optional => true, :default => false) if use_warden warden_config = parse_property(config, "warden", Hash, :optional => true) options[:service_log_dir] = parse_property(warden_config, "service_log_dir", String) …… end |
warden子元素的配置如表3-5所示:
表3-5 warden参数配置表(不完全)
编号 |
参数名称 |
options |
变量名 |
类型 |
可选 |
默认值 |
典型值 |
B01 |
use_warden |
|
use_warden |
Boolen |
是 |
FALSE |
|
B02 |
warden |
|
warden_config |
Hash |
是 |
|
|
B03 |
service_log_dir |
options[:service_log_dir] |
|
String |
是 |
|
|
B04 |
service_common_dir |
options[:service_common_dir] |
|
String |
是 |
"/var/vcap/store/common" |
|
B05 |
service_bin_dir |
options[:service_bin_dir] |
|
Hash |
是 |
|
|
B06 |
image_dir |
options[:image_dir] |
|
String |
否 |
|
|
B07 |
port_range |
options[:port_range] |
|
Range |
否 |
|
|
B08 |
filesystem_quota |
options[:filesystem_quota] |
|
Boolen |
是 |
FALSE |
|
B09 |
service_start_timeout |
options[:service_start_timeout] |
|
Interger |
是 |
3 |
|
B10 |
service_status_timeout |
options[:service_status_timeout] |
|
Interger |
是 |
3 |
|
注意:在启用warden的情况下B06、B07两个参数此时必须配置,所以A24、A25两个参数此时配置会被B06、B07给覆盖。
表3-6说明参数的作用。
表3-6 参数说明(不完全)
编号 |
参数名称 |
说明 |
A01 |
index |
|
A02 |
plan |
|
A03 |
capacity |
表示该节点可以创建多少个Node的服务实例,参考【capacity】 |
A04 |
ip_route |
|
A05 |
node_id |
表示当前主机的Node的名称,作为向NATS订阅主题时候使用的参数之一,参考【主题】 |
A06 |
z_interval |
定时任务的中间间隔,如果不设置,则默认使用时间为30秒 |
A07 |
mbus |
NATS总线,需要指明其IP地址和端口号,默认NATS使用4222 |
A08 |
local_db |
创建的Service实例需要存储,默认情况下使用sqlite3 |
A09 |
migration_nfs |
|
A10 |
max_nats_payload |
|
A11 |
fqdn_hosts |
|
A12 |
op_time_limit |
在timing_exec中使用,表示一个操作的超时时间。相关内容参考【timing_exec】 |
A13 |
supported_version |
该Service Node支持创建的实例的版本,一般情况下就1个版本,该参数必须在重写NodeBin时候设置,否则无法使用。 |
A14 |
default_version |
使用的默认版本号。 |
A15 |
max_clients |
|
A16 |
database_lock_file |
|
A17 |
disabled_file |
|
A18 |
logger |
由node_id生成,每个Service Node会根据其Service实例生成一个log文件。所以这个参数在配置文件中是不能配置的 |
|
logging |
|
A19 |
id |
|
A20 |
base_dir |
|
A21 |
service_log_dir |
|
A22 |
service_common_dir |
|
A23 |
service_bin_dir |
|
A24 |
image_dir |
|
A25 |
port_range |
|
A26 |
filesystem_quota |
|
A27 |
service_start_timeout |
|
A28 |
service_status_timeout |
|
A29 |
max_memory |
|
A30 |
memory_overhead |
|
A31 |
max_disk |
|
A32 |
disk_overhead |
|
A33 |
m_interval |
|
A34 |
m_actions |
|
A35 |
m_failed_times |
|
B01 |
use_warden |
|
B02 |
warden |
|
在Service Service Node的【配置参数】一节,我们见过一个参数叫做capacity,它表示了一个Service Node可以配置的容量,也就是可以创建的Service实例个数。引入capacity的原因如下【--?猜的--】:
² 为了实现Service实例的负载均衡,一个Gateway会对应多个ServiceNode,不能在同一个节点上创建太多实例,所以每个Service Node都会将capacity提供给Gateway,Gateway以此为计算参数进行计算,选举最适合的Service Node创建实例。
² 每个ServiceNode能够创建的Service实例不可能是无限大,引入capacity可以限制一个Node创建的Service实例个数。
capacity参数可选,默认值为200,如代码3.8所示。
代码3.8:vcap-services-base / lib / base / node_bin.rb #start |
:capacity => parse_property(config, "capacity", Integer, :optional=>true, :default=>200), |
如代码3.9所示,读取的capacity参数的值会传入到@capacity和@max_capacity中。其中:
² @capacity:表示剩余容量
² @max_capacity:表示最大容量
代码3.9:vcap-services-base / lib / base / node.rb #initialize |
@capacity = options[:capacity] @max_capacity = @capacity |
每次Service Node重启后会恢复之前创建的Service实例,对于已经创建的Service实例已经消耗了部分capacity,所以@capacity的值初始化还需要去除已经创建的Service实例消耗的容量值。而计算这个剩余容量的过程则需要Service开发人员自行编写。
对于计算剩余容量的时机可以选择在执行pre_send_announcement方法中进行计算。例如在EchoService中,如代码3.10所示,遍历所有创建的Service实例,然后依次去除每个实例消耗的容量,最后得到@capacity。其中:
² 【ProvisionedService】是用于保存Service实例的数据库接入对象(DAO);
² @ capacity_lock就是一个锁,定义就是@capacity_lock = Mutex.new,因为在计算剩余capacity的过程中可能还会出现销毁Service实例这样的同步问题,所以需要加锁保护;
² capacity_unit方法返回每个Service实例所消耗的capacity值。
代码3.10:vcap-services / echo / lib / echo_service / echo_node.rb #pre_send_announcement |
@capacity_lock.synchronize do ProvisionedService.all.each do |instance| @capacity -= capacity_unit end end |
注意:计算剩余capacity必须实现,最好在pre_send_announcement方法中计算,在计算剩余capacity的过程中必须对capacity进行加锁保护。
@capacity的值需要告知给Gateway,以便Gateway可以知道每个ServiceNode的剩余容量,并根据该值来实现负载均衡。
通告在【周期任务】中完成,使用send_node_announcement方法,不过对@capacity的操作不是在send_node_announcement,而是在announcement方法中,这个步骤还是需要Service开发人员完成,参考【announcement】。
如代码3.12所示,在发布给Gateway的announcement信息会带上capacity相关的信息,其中:
² :available_capacity:就是剩余容量——@capacity;
² :capacity_unit:创建一个Service实例需要消耗的代价。capacity_unit方法源代码如代码3.11所示,返回值默认设置为1。Service开发人员可以通过重写该方法改变返回值。
代码3.11:vcap-services-base / lib / base / node.rb |
def capacity_unit # subclasses could overwrite this method to re-define # the capacity unit decreased/increased by provision/unprovision 1 end |
² @capacity_lock.synchronize是一个锁操作。
代码3.12:vcap-services / echo / lib / echo_service / echo_node.rb #announcement |
def announcement @capacity_lock.synchronize do { :available_capacity => @capacity, :capacity_unit => capacity_unit } end end |
如代码3.13所示,Service Node每创建一个新Service实例,就会减少capacity_unit份额,更多详细内容可以参考【on_provision】。
代码3.13:vcap-services-base / lib / base / node.rb #on_provision |
def on_provision(msg, reply) …… @capacity_lock.synchronize{ @capacity -= capacity_unit } …… |
同理,Service Node每销毁一个Service实例,就会增加capacity_unit的容量,如代码3.14所示,更多内容可以参考【on_unprovison】。
代码3.14:vcap-services-base / lib / base / node.rb #on_unprovision |
def on_unprovision(msg, reply) …… @capacity_lock.synchronize{@capacity += capacity_unit } …… |
【--?--没怎么看懂它做什么用】
NATS作为整个CloudFoundry的消息总线,不作为本文介绍的重点。不过在下文运行过程介绍中会经常遇到它。所以还是需要知道一下它的一些简单使用。可以参考博文:http://blog.csdn.net/zdq0394/article/details/7860041
在连接到NATS后,每个ServiceNode都会向节点订阅它所关心的主题,相关内容参考【订阅主题】。
订阅主题的过程在Base::Node的on_connect_node方法中实现,实现过程如代码5.1所示。
代码5.1:vcap-services-base / lib / base / node.rb #on_connect_node |
%w[provision unprovision bind unbind restore disable_instance enable_instance import_instance update_instance cleanupnfs_instance purge_orphan].each do |op| eval%[@node_nats.subscribe("#{service_name}.#{op}.#{@node_id}") { |msg, reply| EM.defer{ on_#{op}(msg, reply) } }] end |
该段代码在句法上有些复杂,我们来慢慢剖析:
² %w[provision unprovision ……].each do |op| …… end :这个句法的意思就是一次遍历方括号中的每个元素,元素的类型为String,取出的元素至于op中,然后执行代码块中的内容。
² eval %[……] :这个句法的意思就是,把内部的文本内容翻译成Ruby可执行的语句,使其可以执行。
² @nodes_nats.subscribe(……) { |msg, reply| ……} :这个句法的意思是,向NATS订阅主题,每个主题对应的处理方法则是在其代码块中定义。
² "#{service_name}.#{op}.#{@node_id}":这个句法代表订阅的主题。一个主题由三者共同确定(注:部分主题只需要2个部分内容)。对于同一个Service,service_name其实是相同的;op是模板已经写好的,不能修改;@node_id则必须不同,目的是为了区分不同主机上的Service Node(注意:不是区分Service实例):
ü service_name:标识Service的名称,一般是”XXXaaS”格式,这个名字是由Service开发人员编写的,详细内容参考【service_name】。
ü op:订阅的主题操作,也就是%w[……]中的内容,Node中已经提供了大量的操作,Service开发人员在编写Node时最多的时间也就是对这些操作的开发。
ü @node_id:标识一个Service Node,这样Gateway才能准确将信息发送给指定Node。
注意:@node_id是在配置文件中进行配置的node_id参数,参考【配置参数】
² EM.defer{on_#{op}(msg, reply)} :这个部分的内容则是实现了相应主题的处理函数(在Event Machine中运行该处理方法)。对应主题的处理方法名称就是”on_XXX”。这些处理方法提供了处理主题的一份模板,大部分主题会预留Hook方法让Service开发人员完善,实现所需的Service Node开发。
表5-1整理了Node中的主题内容:
表5-1 Service Node订阅主题相关说明(不完全)
编号 |
主题名称 |
处理方法 |
Hook方法 |
说明 |
F01 |
provision |
on_provision |
provision |
创建一个Service实例 |
F02 |
unprovision |
on_unprovision |
unprovision |
销毁一个Service实例 |
F03 |
bind |
on_bind |
bind |
将Service实例与APP绑定 |
F04 |
unbind |
on_unbind |
unbind |
将Service实例与APP解除绑定 |
F05 |
restore |
on_restore |
|
|
F06 |
disable_instance |
on_disable_instance |
disable_instance |
让Service实例停止服务,但不销毁 |
F07 |
enable_instance |
on_enable_instance |
enable_instance |
让Service实例生效 |
F08 |
import_instance |
on_import_instance |
import_instance |
|
F09 |
update_instance |
on_update_instance |
update_instance |
|
F10 |
cleanupnfs_instance |
on_cleanupnfs_instance |
|
|
F11 |
purge_orphan |
on_purge_orphan |
|
|
F12 |
check_orphan |
on_check_orphan |
|
|
F13 |
discover |
on_discover |
|
|
² 主题名称:对应处理的主题。其中主题F01~F11主题内容是"#{service_name}.#{op}.#{@node_id}"格式;F12、F13主题内容是"#{service_name}.#{op}" 格式。另外,{F01~F09-F05}明确要求Service开发人员实现相关的处理方法。【--?--F05有些奇怪,我无法把它串联起来】 ² 处理方法:就是相应主题的处理方法。 ² Hook方法:处理方法中帮助Service开发人员完成了一些基本工作,真正的处理都需要在Hook方法中进行处理。 |
对于如何添加新的主题,Service的开发人员其实不用关心,因为Cloud Foundry已经为我们设计好了模板,而且这份模板已经足够使用,而且添加新的主题是一件非常麻烦的事情。我们所关心的事情是,对于相应主题的处理方法的调用过程以及Hook方法的实现,这些处理方法将在【运行过程】中一一展开讲述。
在Service Node中,一共需要执行2个周期任务。
周期任务底层依靠Event Machine实现,EventMachine中添加周期任务不难,只需要调用add_periodic_timer方法,语法格式如代码6.1所示.
代码6.1 |
EM.add_periodic_timer(10) do # do something end |
第一个周期任务在Base::Node#on_connect_node添加,如代码6.2所示。其中:
² pre_send_announcement方法可以参考【pre_send_announcement】,这个方法默认情况下为空,也就是指它是一个Hook函数,Service的开发人员可以通过重写它实现在进行周期任务前的一些预处理。
² EM.add_periodic_timer(30) {……}:是在EventMachine中添加一个30S的定时器,定期执行代码段中的内容。
² 周期任务的主要流程都在send_node_announcement方法中。该方法就是用于【--?--向Gateway进行注册以及保活】,让Gateway可以实时监测到Node的状态信息。
代码6.2:vcap-services-base / lib / base / node.rb #on_connect_node |
def on_connect_node …… pre_send_announcement send_node_announcement EM.add_periodic_timer(30) { send_node_announcement } end |
如代码6.3所示,send_node_announcement方法做的事情就是整理需要发送的announcement信息到发布主题#{service_name}.announce,这样就可以发送到对应的Gateway处理。
² 在发送announcement之前,需要确定节点是否是正常运行的,只有在正常运行的情况下才能发送announcement。
² 在发送的announcement信息中,有三个信息是必须存在,也就是@node_id、@plan、@supported_version。这三个参数也是在配置文件中必须指定的,参考【配置参数】。
² 还可以添加自定义的announcement信息,Service的开发人员通过announcement方法添加,参考【announcement】。
不过这个方法实现有些诡异,在函数定义的时候我们明显看到它带有2个参数,但是在代码6.2中我们可以看到并没有代入参数,还有就是msg为nil时候就可以发送announcement。
代码6.3:vcap-services-base / lib / base / node.rb #send_node_announcement |
def send_node_announcement(msg=nil, reply=nil) if disabled? …… return end unless node_ready? …… return end req = nil req = Yajl::Parser.parse(msg) if msg if !req || req["plan"] == @plan a = announcement a[:id] = @node_id a[:plan] = @plan a[:supported_versions] = @supported_versions publish(reply || "#{service_name}.announce", Yajl::Encoder.encode(a)) end …… end |
Service Node定期调用【send_node_announcement】方法向Gateway发送Node的声明,声明的内容除去三个必须含有的信息——@node_id、@plan、@supported_version——外,官方专门预留announcement方法提供给service开发人员进行扩展。
如代码6.4所示,官方要求必须实现这个方法。announcement方法需要处理的内容就是需要提供给Gateway的信息。
代码6.4:vcap-services-base / lib / base / node.rb |
# Service Node subclasses must implement the following methods # announcement() --> { any service-specific announcement details } abstract :announcement |
表6.1整理了announcement方法的参数说明,
表6.1announcement方法说明
函数名:announcement |
参数名称 |
说明 |
输入参数 |
- |
|
返回值 |
Hash |
返回需要通告信息的Hash表,最后该表会发送给Gateway |
如代码6.5所示,在EchoService中,announcement方法返回值就是一个Hash数组。
代码6.5:vcap-services / echo / lib / echo_service / echo_node.rb |
def announcement @capacity_lock.synchronize do { :available_capacity => @capacity, :capacity_unit => capacity_unit } end end |
Service Node定期发送的announcement最后由ServiceGateway接收。在代码6.3中,Service Node每次发送announcement周期任务的时候使用的主题是“#{service_name}.announce”。如代码6.5.1所示,在ServiceGateway中,对应于该主题的处理方法是on_announce方法。
代码6.5.1:vcap-services-base / lib / base / provisioner.rb #on_connect_node |
def on_connect_node @logger.debug("[#{service_description}] Connected to node mbus..") %w[announce node_handles handles update_service_handle].each do |op| eval%[@node_nats.subscribe("#{service_name}.#{op}") { |msg, reply| on_#{op}(msg, reply) }] end |
如代码6.5.2所示,【--?--】
代码6.5.2:vcap-services-base / lib / base / provisioner.rb #on_announce |
def on_announce(msg, reply=nil) announce_message = Yajl::Parser.parse(msg) if announce_message["id"] id = announce_message["id"] announce_message["time"] = Time.now.to_i if @provision_refs[id] > 0 announce_message['available_capacity'] = @nodes[id]['available_capacity'] end @nodes[id] = announce_message end end |
第二个周期任务任务在Base::Node#initialize方法中添加,如代码6.6所示。其中
² z_interval就是【配置参数】中配置,如果不配置,则会使用默认值30。
² 周期任务的主要流程都在update_varz中执行。
代码6.6:vcap-services-base / lib / base / node.rb #initialize |
z_interval = options[:z_interval] || 30 EM.add_periodic_timer(z_interval) do EM.defer { update_varz } end if @node_nats …… end |
update_varz定义如代码6.7所示
² 首先通过varz_details方法获取到需要想Component注册的信息表(Hash),关于varz_details的内容参考【varz_details】
² 然后向Component中依次注册每个参数。
代码6.7:vcap-services-base / lib / base / base.rb |
def update_varz() vz = varz_details vz.each { |k,v| VCAP::Component.varz[k] = v } if vz end |
varz是CloudFoundry用于监控组件状态,大致原理如下【--?--我还是不知道做什么的?】:
Cloud Foundry状态监控组件:VARZ原理 |
在代码中我们可以看到组件在启动时都会向Component注册自己。那么这个注册就会启动一个http server。启动的代码在vcap common的component.rb中。这个模块已经成为了一个gem,你不必装Cloud Foundry,而只需要gem install一个都可以使用了。在vcap common中,如果有组建来注册,他会为这个组件建立一个server。然后server的port以及帐号密码默认是cf自己生成的。但是按照上文的配置,这些参数就会被传入,我们就可以按照自己的参数来配置这个server了。 在组件向component注册完成之后,组建就可以通过以下方式向varz传数据了: [ruby]view plaincopy 1 #这是dea的状态更新 2 VCAP::Component.varz[:running_apps] = running_apps 3 VCAP::Component.varz[:frameworks] = metrics[:framework] 4 VCAP::Component.varz[:runtimes] = metrics[:runtime] |
在【update_varz】中介绍了CloudFoundry中每个组件都会向VCAP::Component注册自己,然后可以向varz传入参数。的传入的参数则是根据varz_details得到。代码6.8给出了varz_details的定义,默认情况下varz_details的返回值就是announcement的返回值。该方法可以重写,只需要返回内容是Hash表即可。
代码6.8:vcap-services-base / lib / base / node.rb |
def varz_details # Service Node subclasses may want to override this method to # provide service specific data beyond what is returned by their # "announcement" method. return announcement end |
注意:varz_details方法可以重写。如代码6.9所示,在MySqlService实现中就重写了该方法。
代码6.9:vcap-services / mysql / lib / mysql_service / node.rb |
def varz_details() acquired = @varz_lock.try_lock return unless acquired varz = {} # how many queries served since startup varz[:queries_since_startup] = get_queries_status # queries per second varz[:queries_per_second] = get_qps # disk usage per instance status = get_instance_status varz[:database_status] = status …… # how many long queries and long txs are killed. varz[:long_queries_killed] = @long_queries_killed …… # how many provision/binding operations since startup. @statistics_lock.synchronize do varz[:provision_served] = @provision_served varz[:binding_served] = @binding_served end # provisioned services status varz[:instances] = {} begin ProvisionedService.all.each do |instance| varz[:instances][instance.name.to_sym] = get_status(instance) end …… varz[:connection_pool] = @pool.inspect varz …… end |
Service Node的周期任务并不复杂。
相关的配置参数只有一个z_interval。该参数配置了每次执行update_varz的间隔时间。
需要实现的方法也只有一个:announcement,该方法规定了发布给Gateway的信息内容。该方法要求返回一个Hash表。
varz_details方法默认情况下使用announcement的返回值,Service开发人员可以根据自己的需求重新实现该方法。
Service Node在初始化完成后,在运行过程中的工作任务除了【周期任务】中的两个外,最主要职责就是负责Service实例的创建与维护。而Service Node如何知道自己在什么时刻应该执行什么功能?其实也简单,通过之前订阅的主题进行驱动(参考【订阅主题】、【主题】),当Service Node接收到其订阅的主题的请求后,就会调用相应的方法,而调用的方法的格式也就是【主题】一节中说的”on_xxx”格式。
Base::Node为Service的开发人员实现了这份模板,并且为其中的关键部分都留出了抽象方法让Service开发人员可以自定义在交互过程中提供的变化。相应的说明可以参考【主题】一节中的表格。
对于主题的处理过程其实是一样的,我们会在【创建Service instance】一节中细致的分析该过程,在之后的章节中略去这些重复说明,如果有不懂的地方可以类比【创建Service instance】中的过程。
创建Service实例使用的主题内容是provision。这点很容易理解,不过需要注意的一点就是,创建的Service实例此时还未与APP进行绑定,绑定动作是在bind主题中完成的。此外,为了提高运行效率,更推荐在创建Service实例时采用Lazy技术,也就是延迟向Service服务器程序申请创建Service实例的时机,直到要求将APP与Service实例进行绑定的时候才选择想Service服务器程序创建相应的实例。这点将会在下面的分析中体现出来。
在【主题】一章中我们知道Node在启动的时候会订阅主题“#{service_name}.provision.#{@node_id}”。当用户通知CloudController创建一个Service实例的时候,Cloud Controller让Gateway选出一个Service Node创建Service实例,此时Gateway就会发布一个这样的主题内容,NATS会将其正确的发送给对应的Service Node,Service Node就根据收到的主题内容使用对应的处理函数进行处理。
一个Service实例的创建过程大致如图7-1所示:
1) 用户使用命令vmc create 请求创建一个Service实例,这个请求被CloudController捕获。
2) CloudController根据vmc命令请求要求对应的Gateway处理该任务。
3) 这时候Gateway会调用provision_service方法从它管理的Service Node中选择一个最优的Service Node(best_node),整理好所有必需信息以后发布#{service_name}.provision.#{@node_id}这样的一份主题,NATS会负责把它发送给Base::Node。
4) 然后Base::Node中的on_provision方法接收到了这份主题请求,参考【on_provision】,在进行部分处理以后,
5) 调用provision方法创建Service实例——这个方法要求Service的开发人员进行编写,返回值要求是一个Hash数组,相关内容参见【provision】
6) 如果provision方法正确创建了一个Echo的实例,此时Base::Node会将相关的信息(哪些信息由Service的开发人员决定)通过encode_success方法编码后返回给Gateway,这样就创建了一个Service实例。
图 7-1 Service实例创建时序图(示意)
on_provision的源代码如代码7.1所示,其中:
² roolback是一个代码块,他的作用是当创建实例失败的时候调用unprovision方法将进行到一半的实例进行析构。
² timing_exec提供的功能就是在@op_time_limit时间内如果还无法成功创建Service实例,则调用roolback代码块,关于timing_exec方法参见【timing_exec】。
² 在timing_exec中间就会调用provision方法创建一个Service实例,关于provision方法参考【provision】。在这里会扣减capacity容量,相关内容可以参考【capacity】。如果创建成功,则讲成功的结果整理编码后反馈给Gateway。
代码7.1:vcap-services-base / lib / base / node.rb #on_provision |
def on_provision(msg, reply) response = ProvisionResponse.new rollback = lambda do |res| @capacity_lock.synchronize{ @capacity += capacity_unit } if unprovision(res.credentials["name"], []) end
timing_exec(@op_time_limit, rollback) do provision_req = ProvisionRequest.decode(msg) plan = provision_req.plan credentials = provision_req.credentials version = provision_req.version credential = provision(plan, credentials, version) credential['node_id'] = @node_id response.credentials = credential @capacity_lock.synchronize{ @capacity -= capacity_unit } response end publish(reply, encode_success(response)) …… end |
provision方法要求Service开发人员必须实现,其定义如代码7.2所示,它需要实现的功能就是实现在Local Node中创建一个Service 实例。
代码7.2:vcap-services-base / lib / base / node.rb |
# Service Node subclasses must 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.3:vcap-services-base / lib / base / node.rb |
def on_provision(msg, reply) provision_req = ProvisionRequest.decode(msg) plan = provision_req.plan credentials = provision_req.credentials version = provision_req.version credential = provision(plan, credentials, version) …… |
从代码7.4给出了Service Gateway整理发送给ServiceNode的这几个参数值的来源。
代码7.4:vcap-services-base / lib / base / provisioner.rb #provision_service |
def provision_service(request, prov_handle=nil, &blk) …… prov_req = ProvisionRequest.new prov_req.plan = plan prov_req.version = version # use old credentials to provision a service if provided. prov_req.credentials = prov_handle["credentials"] if prov_handle |
我们考察Echo Service中provision方法的实现,如代码7.5,其中credentials这个参数中包含的内容就是使用vmc create进行创建一个service的时候传入的参数【--?这个地方有待验证--】。所以credentials参数中最少会含有一个参数名称“name”。
注:这个name参数是用户希望创建的Service实例的名字。
代码7.5:vcap-services / echo / lib / echo_service / echo_node.rb #provision |
def provision(plan, credential = nil, version=nil) instance = ProvisionedService.new if credential instance.name = credential["name"] …… |
也可以包含“user”与“password”,例如在代码7.6中,MySql::Node的provision方法就就可以看到如下三个参数:
代码7.6:vcap-services / mysql / lib / mysql_service / node.rb #provision |
def provision(plan, credential=nil, version=nil) …… if credential name, user, password = %w(name user password).map{|key| credential[key]} …… |
如何创建一个Service实例,这个需要由Service开发人员实现。如代码7.7所示,我们考察Echo::Node中的provision方法,其中:
² ProvisionedService就是一个DAO(Database Access Object)。内部封装了一个Ruby数据库ORM——DataMaper。详细内容参考【ProvisionService】
² Echo中创建实例的过程相对简单,首先在数据库中创建相应Service实例,然后保存实例。
² 最后将用户访问Service实例所需要的信息(如主机号、端口号、用户名、登陆密码等)返回。
代码7.7:vcap-services / echo / lib / echo_service / echo_node.rb #provision |
def provision(plan, credential = nil, version=nil) #class ProvisionedService # include DataMapper::Resource # property :name, String, :key => true #end instance = ProvisionedService.new if credential instance.name = credential["name"] ……
begin save_instance(instance) ……
# gen_credential(instance) credential = { "host" => get_host, "port" => @port, "name" => instance.name } end |
图7-2整理了provision方法实现的时候的一个执行流程。如果没有特殊的要求,一般都可以按照这个步骤进行编写。Cloud Foundry中对于Service实例的序列化默认提供sqlite进行管理,参考【ProvisionService】。
图7-2provision执行流程
销毁一个Service实例,过程与provision类似,对应主题“#{service_name}.unprovision.#{@node_id}”。在收到unprovision主题后由on_unprovision方法处理。
on_unprovision是on_provision方法的一个逆过程,它的做法就是销毁一个已经存在的Service实例。其源码如代码7.8所示。
² 因为创建一个Service实例的主要动作是Service开发人员在provision方法中编写的,CloudFoundry无法得知如何去注销个Service实例,所以也提供了一个相应的方法——unprovision方法,该方法也需要Service开发人员进行编写。
² 方法中提供了unprovision使用的两个参数,name是需要销毁的service实例的名字;bindings是与该service实例绑定的APP信息。
代码7.8:vcap-services-base / lib / base / node.rb #on_provision |
def on_unprovision(msg, reply) …… unprovision_req = UnprovisionRequest.decode(msg) name = unprovision_req.name bindings = unprovision_req.bindings result = unprovision(name, bindings) if result publish(reply, encode_success(response)) …… |
unprovision定义如代码7.9所示,unprovision方法必须重写,它实现了销毁名字为name的Service实例。
代码7.9:vcap-services-base / lib / base / node.rb |
# Service Node subclasses must implement the following methods # unprovision(name) --> void abstract :unprovision |
源码中对于unprovision的注释存在一些问题,其需要带入两个参数,必须有1个返回值。整理后入表7-2所示:
表7-2unprovision方法说明
函数名:unprovision |
参数名称 |
说明 |
输入参数 |
name |
Service实例名字 |
bindings |
与该Service实例绑定的APP信息 |
|
返回值 |
bool |
是否成功 |
相应的,我们考察Echo::Node中unprovision方法的实现,如代码7.10所示。这个代码会出现一些不协调的地方,
² 首先,它将参数bindings的名字改成了credentials,这样的命名容易让人误解其含义;
² 然后,这个方法中并未使用到credentials这个参数,之后我们会考察MySql中是如何使用这个参数的。
² 最后会根据结果返回一个布尔值表明是否成功注销Service实例。
代码7.10:vcap-services / echo / lib / echo_service / echo_node.rb |
def unprovision(name, credentials = []) return if name.nil? instance = get_instance(name) raise EchoError.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.11:vcap-services / mysql / lib / mysql_service / node.rb #unprovision |
# TODO: validate that database files are not lingering # Delete all bindings, ignore not_found error since we are unprovision begin credentials.each{ |credential| unbind(credential)} if credentials |
注意:unprovision方法的执行流程与provision方法是一样的,可以参考图-2。不仅如此,bind、unbind等方法的执行流程也是一样的,下文中就列举了。
创建的Service实例对于APP而言是不可见的,甚至Service还不知道需要创建Service实例(如:在MySql的provision方法中使用Lazy技术延迟创建数据库),用户部署的APP如果希望能够见到Service实例,就需要将APP与Service实例进行绑定。APP与Service绑定过程类似于Service实例的创建过程,对应的主题是“#{service_name}.bind.#{@node_id}”。在收到unprovision主题后由on_bind方法处理。
如代码7.12所示,on_bind的源码格式和on_provision的格式基本相同,也是确定参数,调用bind方法,向Service Gateway返回结果这样一个流程。
代码7.12:vcap-services / mysql / lib / mysql_service / node.rb |
def on_bind(msg, reply) response = BindResponse.new rollback = lambda do |res| unbind(res.credentials) end
bind_message = BindRequest.decode(msg) name = bind_message.name bind_opts = bind_message.bind_opts credentials = bind_message.credentials response.credentials = bind(name, bind_opts, credentials) response end publish(reply, encode_success(response)) …… end |
bind方法也是预留给Service开发人员使用的接口,这个方法在源码中的定义如代码7.13所示。注释给出的参数也存在一些偏差,表7-3整理了bind方法的用法。
代码7.13:vcap-services-base / lib / base / node.rb |
# Service Node subclasses must implement the following methods # bind(name, bind_opts) --> {host, port, login, secret} abstract :bind |
表7-3 bind方法说明(不完整)
函数名:bind |
参数名称 |
说明 |
输入参数 |
name |
APP需要绑定的Service实例的名字 |
bind_opts |
绑定时候附加的参数 |
|
credentials |
|
|
返回值 |
credentials |
|
Echo::Node中的bind方法并没有太多的参考性,我们可以参考MySql::Node中的bind方法的实现,如代码7.14所示:
² 首先、从数据库中找到存储的Service实例(参考【provision】中介绍的Echo::Node的provision方法,用的数据库是一样的);
² 然后也是创建一个数据库存储绑定的APP的信息;
² 再之后调用enforce_instance_storage_quota方法在MySql中创建这个账户(注:不是创建了Service实例就会在MySql中创建这个账户,而是在绑定的时候才会创建,所以才会产生enforce_instance_storage_quota这个方法调用);
² 最后会将结果信息返回给Gateway。
代码7.14:vcap-services / mysql / lib / mysql_service / node.rb |
def bind(name, bind_opts, credential=nil) begin service = ProvisionedService.get(name) # create new credential for binding binding = Hash.new if credential binding[:user] = credential["user"] binding[:password] = credential["password"] …… binding[:bind_opts] = bind_opts
begin create_database_user(name, binding[:user], binding[:password]) enforce_instance_storage_quota(service) ……
response = gen_credential(name, binding[:user], binding[:password]) …… end |
将APP与Serviceinstance解除绑定,对应主题“#{service_name}.unbind.#{@node_id}”。在收到unbind主题后由on_unbind方法处理。
on_unbind是on_bind的逆过程,处理内容就是将APP与Service实例解除绑定。如代码7.15所示,真正执行解除绑定的任务是在unbind方法中调用。
代码7.15:vcap-services-base / lib / base / node.rb |
def on_unbind(msg, reply) response = SimpleResponse.new unbind_req = UnbindRequest.decode(msg) result = unbind(unbind_req.credentials) if result publish(reply, encode_success(response)) …… |
unbind方法定义如代码7.16所示,表7-4整理了unbind方法说明。
代码7.16:vcap-services-base / lib / base / node.rb |
# unbind(credentials) --> void abstract :unbind |
表7-4unbind方法说明(不完全)
函数名:unbind |
参数名称 |
说明 |
输入参数 |
ccredentials |
|
返回值 |
bool |
是否成功 |
【--?--】
Base::Node中提供了一组方法用于管理Service实例的主题与接口。包括了Service实例的启动、停止、导入、更新等常用操作。这些方法定义如代码7.17所示:表7-5整理了相应的主题与方法。
代码7.17:vcap-services-base / lib / base / node.rb |
# <action>_instance(prov_credential, binding_credentials) --> true for success and nil for fail abstract :disable_instance, :dump_instance, :import_instance, :enable_instance, :update_instance |
表7-5 Service实例管理方法
主题 |
处理方法 |
Hook方法 |
说明 |
disable_instance |
on_disable_instance |
disable_instance |
|
dump_instance |
|
||
enable_instance |
on_enable_instance |
enable_instance |
|
import_instance |
on_import_instance |
import_instance |
|
update_instance |
on_update_instance |
update_instance |
|
cleanupnfs_instance |
on_cleanup_instance |
|
|
何谓orphan,其实orphan就是在service节点上已经创建的一个service实例,但是它的存在还没有通知cloud_controller。比如说,在service节点创建完实例并通知完service gateway,而当gateway返回给cloud_controller时,发生了网络故障,从而cloud_controller通知终端用户,创建失败,当然也不会有信息更新在cloud_controller的数据库中。这样的话,就相当于在service_node上创建了一个没有用的实例,从而导致浪费了一些资源。orphan和没有绑定的instance是有区别的,在实际情况中经常会出现未经绑定的instance,但是他们在cloud_controller中都是有数据记录的,而orphan则没有。一般这种情况很罕见,但是源码中还是考虑了这一点。
orphan的几个管理函数
在分析源码的过程中还会遇到许多的辅助方法,这些方法因为与章节内容相关性很小,一直找不到地方放置。所以干脆就直接兜在一块说,这个章节的内容很零散,不需要专门阅读,只有在用到了或者看不懂的时候再看。
在前面的章节中经常可以看到“abstract:XXX”这样的语法格式。因为我没有专门学过Ruby,尤其对于Ruby元编程的内容更加不了解。以下内容都是个人的臆测。
首先,我们先找到abstract的定义(一开始我以为它是一个关键字,后来发现不是),如代码8.1所示,它其实也是一个方法,有自己的参数。
该方法就是调用了define_method方法检查是否实现了args中定义的方法。它所想要实现的就是类似于C++中的纯虚函数定义。所以使用了abstract :XXX表示的语句我们可以看做是定义了一个纯虚函数XXX,我叫它——抽象方法。
代码8.1:vcap-services-base / lib / base / abstract.rb |
classClass def abstract(*args) args.each do |method_name| define_method(method_name) do |*args| raise NotImplementedError.new("Unimplemented abstract method #{self.class.name}##{method_name}") end end end end |
在【创建Echo::Node实例】中遇到了node_class,该方法其实很容易看明白。特殊地方就在于node_class方法要求Echo::NodeBin中进行实现,重写内容很简单,只需要指向相应的XXX::Node即可。
表8-1node_class方法说明
函数名:node_class |
参数名称 |
说明 |
|
输入参数 |
- |
|
|
返回值 |
Node |
Service的Node类,例如VCAP::Service::Echo::Node |
|
例如在Echo Service实现的时候,参照代码8.2,它就返回了Echo中实现的Service Node。
代码8.2:vcap-services / echo / bin / echo_node |
classVCAP::Services::Echo::NodeBin<VCAP::Services::Base::NodeBin …… def node_class VCAP::Services::Echo::Node end …… end |
注意:该方法要求Service开发者必须实现。
在Base::Node中pre_send_announcement方法的定义为空,也就是说它是一个Hook方法,可以让Service的开发人员自定义实现该方法,当然也可以不实现。这个方法所在的上下文有:
² 已经完成了到NATS的连接建立过程,并且完成了主题的订阅。
² 在添加周期任务send_node_announcement之前执行
对于Node的初始化有两个时机,一个是Node的initialize方法中,另一个就是在pre_send_announcement方法。前者多用于参数的初始化,而在pre_send_announcement方法中更适合Node数据库的初始化(就是使用【配置参数】中local_db)以及剩余capacity的计算。
如代码8.3所示,在Echo Service实现中,pre_send_announcement的工作就是初始化了使用的数据库,以及对剩余capacity的计算。
代码8.3:vcap-services / echo / lib / echo_service / echo_node.rb |
def pre_send_announcement super FileUtils.mkdir_p(@base_dir) if @base_dir start_db @capacity_lock.synchronize do ProvisionedService.all.each do |instance| @capacity -= capacity_unit end end end |
Cloud Foundry实现了实现了一个日志文件系统,每个组件都会存在一个@logger实例,它就表示了该日志系统。我们并不关心该日志系统的实现,更关注于它的使用。
从代码8.4可以看出,与日志文件系统的相关配置参数是logging,每个节点的日志是根据node_id生成的。Logging参数配置可以参考【配置参数】。在开发期间还是选择debug模式。
代码8.4:vcap-services-base / lib / base / node_bin.rb #start |
VCAP::Logging.setup_from_config(config["logging"]) # Use the node id for logger identity name. options[:logger] = VCAP::Logging.logger(options[:node_id]) @logger = options[:logger] |
使用@logger也很简单,我们一般使用debug和info方法(我没去看日志系统,也不知道有没有其他高级特性)。例如如代码8.5,我们截取了两个使用日志系统的例子。
代码8.5:vcap-services-base / lib / base / node.rb |
@logger.info("#{service_description}: Not sending announcement because node is disabled") @logger.debug("#{service_description}: Not ready to send announcement") |
在【运行过程】一节中,会经常遇到timing_exec方法。这个方法其实很简单,就是要求在time_limit时间内完成代码块中的内容,如果超时,就调用roolback并抛出异常,如代码8.6所示。
代码8.6:vcap-services-base / lib / base / node.rb #timing_exec |
def timing_exec(time_limit, rollback=nil) return unless block_given?
start = Time.now response = yield if response && Time.now - start > time_limit rollback.call(response) if rollback raise ServiceError::new(ServiceError::NODE_OPERATION_TIMEOUT) end end |
在看Echo::Node和MySql::Node源码时候就会看到这个类。其实这个类名字是什么无所谓,关键的是知道它是做什么用的。我们都知道Service Node创建Service实例,但是这个实例不可能只存储内存中,否的一宕机Service实例的内容就没有了,所以就需要支持Service实例的可序列化。当然我们也可以使用XML这种格式存储,不过Cloud Foundry中则是使用了sqlite3进行保存,然后对数据库的中间层使用的是DataMapper。
如代码8.7所示,其中:
² ProvisionService其实就是表示了在数据库中存储的一个Service实例;
² include DataMapper::Resource 则是导入了对数据库操作的相关接口,它提供了包括创建、查询、保存等数据库常用操作;
² property XXX 就是一个数据库表,Service开发人员在这里定义希望在数据库中保存的Service信息。
代码8.7:vcap-services / echo / lib / echo_service / echo_node.rb |
class ProvisionedService includeDataMapper::Resource property :name, String, :key => true end |
在Node启动的时候,还需要调用代码8.8中的代码。该代码的调用时机一般就选择在【pre_send_announcement】中。其中@local_db参考【配置参数】中的说明。
代码8.8:: |
DataMapper.setup(:default, @local_db) DataMapper::auto_upgrade! |
如表8-2所示,我们截取了一些常用的操作的示例代码。
表8-2ProvisionService常用操作
操作 |
示例代码 |
创建一个新项 |
instance = ProvisionedService.new |
保存一个项 |
instance.save |
删除一个项 |
instance.destroy |
获取特定项 |
instance = ProvisionedService.get(name) |
遍历素有项 |
ProvisionedService.all.each do |instance| …… end |
每个Service需要一个名字(name),在【订阅主题】和【主题】两节中,我们知道service_name作为Node订阅主题中关键的一个数据结构。如代码8.9所示,源码中要求Service Node和Provisioner节点都必须实现该方法。
代码8.9:vcap-services-base / lib / base / base.rb |
# Service Provisioner and Node classes must implement the following # method abstract :service_name |
表8-2整理的service_name方法的定义
表8-2service_name方法说明
函数名: service_name |
参数名称 |
说明 |
输入参数 |
- |
|
返回值 |
name |
需要返回一个字符串,参考代码5.1即可知道。 |
按理来说,该方法在Node类中实现即可。不过按照CloudFoundry中Service实现的惯例来看,一般是将这个方法封装在Common模块中。这是因为这个方法是Node类和Provisioner类都需要的,为了避免两者重复实现,所以将其封装成了接口。
如代码8.10所示,在EchoService中,专门对service_name的实现封装,然后在代码8.11的Node类实现过程中,导入该接口,在Service的Provisioner类实现中也是类似的做法。
代码8.10:vcap-services / echo / lib / echo_service / common.rb |
moduleVCAP module Services module Echo module Common defservice_name "EchoaaS" end end end end end |
代码8.11:vcap-services / echo / lib / echo_service / echo_node.rb |
classVCAP::Services::Echo::Node includeVCAP::Services::Echo::Common |
注意:该方法要求Service开发人员必须实现
在【on_check_orphan】一节中,我们见到on_check_orphan方法调用了all_instance_list方法,该方法的定义如代码8.11所示。
² Service Node需要监测orphan的Service实例,关于orphan内容参考【orphan】,所以每次需要将所有需要检查的Service实例列表发送给Gateway,返回该列表的工作就由all_instance_list方法完成。
² 这个方法要求实现,默认情况下会返回一个空列表,意思是,如果Service开发者不实现该方法也不会报错,但是在运行过程中产生的orphan都不会被管理。
代码8.11:vcap-services-base / lib / base / node.rb |
# Subclass must overwrite this method to enable check orphan instance feature. # Otherwise it will not check orphan instance # The return value should be a list of instance name(handle["service_id"]). def all_instances_list [] end |
如果要实现该方法,其实也不复杂。
表8-3all_instance_list方法说明
函数名:all_instance_list |
参数名称 |
说明 |
输入参数 |
- |
|
返回值 |
list |
返回一个Service实例列表 |
如代码8.12所示,在MySql::Node中实现该方法,它返回的是数据库中所有保存的Service实例。
代码8.12:vcap-services / mysql / lib / mysql_service / node.rb |
def all_instances_list ProvisionedService.all.map{|s| s.name} end |
注意:该方法要求Service开发人员必须实现。
vcap-services-base / lib / base / node.rb |
# Subclass must overwrite this method to enable check 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_connection do |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').each do |entry| # Filter out the instances handles res << gen_credential(entry["db"], entry["user"], "fake-password") unless all_ins_users.include?(entry["user"]) end end res rescue Mysql2::Error => e @logger.error("MySQL connection failed: [#{e.errno}] #{e.error}") [] end |
vcap-services-base / lib / base / node.rb |
def node_ready?() # Service Node subclasses can override this method if they depend # on some external service in order to operate; for example, MySQL # and Postgresql require a connection to the underlying server. true end |
用于将需要发送的信息进行编码。
vcap-services-base / lib / base / node.rb |
# Helper def encode_success(response) response.success = true response.encode end
def encode_failure(response, error=nil) response.success = false if error.nil? || !error.is_a?(ServiceError) error = ServiceError.new(ServiceError::INTERNAL_ERROR) end response.error = error.to_hash response.encode end |
返回当前Node主机地址。
vcap-services-base / lib / base / node.rb |
def get_host @fqdn_hosts ? Socket.gethostname : @local_ip end |
第二部分 Service Node实现
假如我们要实现一个MyService。
对于Service Node的编写所需要实现那些内容基本上在之前的章节中已经拆散来讲了。可以使用Echo Service的源码为模板进行开发,重点是实现相应的方法。
查看【启动流程】章节说明,Node启动涉及了2个类——NodeBin和Node,所以这里需要两个源码文件分别实现者两个类。在【service_name】一节中提到,惯例上会将service_name方法单独封装,所以这里还需要一个源码文件;此外,在【default_config_file】中提到Node还需要导入配置文件,所以还需要编写一个配置文件。整理后如表9-1所示:
表9-1 Node源码实现所需文件列表
源码文件 |
说明 |
示例 |
NodeBin源码文件 |
作为Node的启动文件,放在bin目录下 |
vcap-services / echo / bin / echo_node |
Node源码文件 |
作为Node的核心功能文件,放在lib目录下 |
vcap-services / echo / lib / echo_service / echo_node.rb |
service_name封装 |
作为一个模块封装,放在lib目录下 |
vcap-services / echo / lib / echo_service / common.rb |
配置文件 |
放在config目录下 |
vcap-services / echo / config / echo_node.yml |
我们以Echo的源码为示例进行分析。
代码9.1:vcap-services / echo / bin / echo_node |
#!/usr/bin/env ruby #定义使用的解释器 # -*- mode: ruby -*- # # Copyright (c) 2009-2011 VMware, Inc.
ENV["BUNDLE_GEMFILE"]||=File.expand_path("../../Gemfile",__FILE__) require'bundler/setup' require'vcap_services_base' #需要加载service base库
$LOAD_PATH.unshift(File.expand_path("../../lib",__FILE__)) #将Echo目录下的lib库加入环境变量 require"echo_service/echo_node" #加载Echo的Node类
#必须实现:Echo Node类的命名空间。参考【创建一个Echo::Node实例】和【node_class】 VCAP::Services::Echo::Node end
#返回默认配置文件路径,参考【初始化参数配置】和【default_config_file】 def default_config_file File.join(File.dirname(__FILE__), '..', 'config', 'echo_node.yml') #一般会选择放在config目录下 end
#对于附加参数的处理,参考【addition_config_file】 def additional_config(options, config) options[:port] = parse_property(config, "port", Integer) options end
end
VCAP::Services::Echo::NodeBin.new.start |
代码9.2:vcap-services / echo / lib / echo_service / echo_node.rb |
# Copyright (c) 2009-2011 VMware, Inc. require"fileutils" require"logger" #导入日志文件系统,参考【@logger】 require"datamapper" #导入数据库中间件,参考【ProvisionService】 require"uuidtools"
#定义Echo ::Node module Services module Echo class Node < VCAP::Services::Base::Node #继承自Base::Node end end end end
require"echo_service/echo_error"
include VCAP::Services::Echo #导入Error处理方法
#定义Service实例表,参考【ProvisionService】 include DataMapper::Resource #导入数据库中间件 property :name, String, :key => true #定义数据库表项 end
def initialize(options) super(options) #调用父类中的初始化方法
#这三个参数其实不用这里初始化,在NodeBin的源码中已经实现了,参考【配置参数】 @port = options[:port] @base_dir = options[:base_dir] @supported_versions = ["1.0"] end
#参考【pre_send_announcement】 def pre_send_announcement Super #该语句没用 FileUtils.mkdir_p(@base_dir) if @base_dir start_db #启动数据库 @capacity_lock.synchronize do #初始化剩余容量 ProvisionedService.all.each do |instance| @capacity -= capacity_unit end end end
#参考【send_node_announcement】 def announcement @capacity_lock.synchronize do #返回需要通告的Hash表 { :available_capacity => @capacity, :capacity_unit => capacity_unit } end end
#创建Service实例,参考【主题】和【创建Service实例】 def provision(plan, credential = nil, version=nil) instance = ProvisionedService.new #创建一个新的数据库表项 if credential #初始化Service实例名字 instance.name = credential["name"] else instance.name = UUIDTools::UUID.random_create.to_s end
begin #保存Service实例 save_instance(instance) rescue => e1 #在出现错误的情况下,进行恢复,需要销毁创建一半的数据库项 @logger.error("Could not save instance: #{instance.name}, cleanning up") begin destroy_instance(instance) rescue => e2 @logger.error("Could not clean up instance: #{instance.name}") end raise e1 end
gen_credential(instance) #需要返回实例的具体认证信息 end
#销毁一个Service实例,参考【销毁Service instance】 def unprovision(name, credentials = []) return if name.nil? @logger.debug("Unprovision echo service: #{name}") instance = get_instance(name) #从数据库中找到该实例 destroy_instance(instance) #销毁该表项 true #返回成功标志 end
#将一个APP与Service实例绑定,参考【APP与Service instance绑定】,注意,在Echo::Node中其实并未实现该方法。 def bind(name, binding_options, credential = nil) instance = nil if credential instance = get_instance(credential["name"]) else instance = get_instance(name) end gen_credential(instance) end
#将一个APP与Service实例解除绑定,参考【APP与Service instance绑定】,注意,在Echo::Node中其实并未实现该方法。 def unbind(credential) @logger.debug("Unbind service: #{credential.inspect}") true end
#参考【ProvisionService】 def start_db DataMapper.setup(:default, @local_db) DataMapper::auto_upgrade! end
def save_instance(instance) raise EchoError.new(EchoError::ECHO_SAVE_INSTANCE_FAILED, instance.inspect) unless instance.save end
raise EchoError.new(EchoError::ECHO_DESTORY_INSTANCE_FAILED, instance.inspect) unless instance.destroy end
def get_instance(name) instance = ProvisionedService.get(name) raise EchoError.new(EchoError::ECHO_FIND_INSTANCE_FAILED, name) if instance.nil? instance end
def gen_credential(instance) credential = { "host" => get_host, "port" => @port, "name" => instance.name } end end |
在Echo Service Node的源码中并没有实现管理Serviceinstance的方法。
vcap-services / echo / lib / echo_service / common.rb |
# Copyright (c) 2009-2011 VMware, Inc. #按照此格式封装即可 moduleVCAP module Services module Echo module Common def service_name "EchoaaS" end end end end end |
vcap-services / echo / config / echo_node.yml |
--- plan:free capacity:100 local_db:sqlite3:/var/vcap/services/echo/echo_node.db mbus:nats://localhost:4222 base_dir:/var/vcap/services/echo/ index:0 logging: level: debug pid:/var/vcap/sys/run/echo_node.pid node_id:echo_node_1 port:5002 supported_versions:["1.0"] |
【--?--还未整理】