现在的位置: 首页 > 综合 > 正文

Cloud Foundry Service Gateway源码分析

2013年08月24日 ⁄ 综合 ⁄ 共 13560字 ⁄ 字号 评论关闭

        Cloud Foundry是一个开源的平台即服务产品,它提供开发者自由度去选择云平台,开发框架和应用服务。而Cloud Foundry中,服务则是体现了应用程序的高级功能,正是由于服务Service的存在,用户得以加速应用部署和简化应用管理。首先,还是简要的介绍一下Cloud Foundry的Service。

        目前Cloud Foundry的Service主要包括三方面:1.数据库服务(nosql以及relation),比如:Redis,MondoDB,Neo4j,CouchDB,Mysql,Postgres等;2.存储类服务,比如:Vblob,Filesystem等;3.其他类型服务,比如:RabbitMQ消息队列系统,Memcached分布式内存对象缓存服务等。在Cloud Foundry中,从组件的角度来看,service主要包括两部分:service gateway与service node。从工作流程来看,service有以下两个最为主要的功能:创建service实例和为运行的app绑定一个或若干个service实例,当然还有一些其他重要的功能,比如为service创建snapshot等,但这些不在本文源码探讨的范畴之内。

        个人觉得研究分析Cloud Foundry的某个组件,应该从两个方面入手:第一,研究组件启动的过程;第二,研究在与用户交互时的运作流程。关于Cloud Foundry的service gateway,源码也是如此。

        关于源码,在github上的Cloud Foundry板块都有,主要有:vcap-service-base,vcap-services以及cloud-controller,其中cloud-controller作为一个控制组件,很多关于service的信息都会通过http的形式转发给service gateway,所以cloud-controller和gateway在通信上戚戚相关。

        前期的准备工作准备好了,那就开始来分析一下源码吧。还是从启动和运行两个方面来理解。

1.gateway的启动

          

        关于gateway启动的代码,在cloudfoundry-vcap-services-base\lib\base目录下的asynchronous_service_gateway.rb中,这也是研究gateway的最主要ruby文件之一。其实gateway启动的代码很清晰,如下:

def initialize(opts)
    super
    setup(opts)
  end

          大家都知道initialize方法会在每次生成该类的一个新实例时被执行。所以在启动gatweway的时候,首先执行initialize方法,然后再去调用setup方法。setup方法顾名思义,主要是gateway启动设置的事情。可以看到在同一ruby文件中,setup方法就在initialize方法之下,该函方法以下几部分组成:实例变量的赋值,设置heartbeat以及终止处理方法,添加未知handles,注册反馈handles。


1.1设置heartbeat以及终止处理方法

          setup方法的代码很清晰,关于实例变量的赋值,这里不作赘述。所谓heartbeat,即是心跳,意味着人的心脏每过不到一秒,它就会跳一下,如果不跳的话,那说人出现假死状态或者已经死亡。这样的概念对于一个gateway节点来说,同样成立,主要功能告诉他人本节点活着,而且发给那个人可以找到自己的url等。而gateway的heartbeat则是向cloud controller发送的,如下:

EM.add_periodic_timer(@hb_interval) { send_heartbeat }
EM.next_tick { send_heartbeat }

         可以看到gateway是循环向cloud controller发送heartbeat的。而EM和add_periodic_timer是EventMachine的相关内容,具体可以参考实验室大牛的博文Research
on EventMachine
。以下是send_heartbeat方法的定义:

def send_heartbeat
    @logger.info("Sending info to cloud controller: #{@offering_uri}")
    req = create_http_request(
      :head => @cc_req_hdrs,
      :body => @svc_json
    )
    http = EM::HttpRequest.new(@offering_uri).post(req)
    http.callback do
      if http.response_header.status == 200
        @logger.info("Successfully registered with cloud controller")
      else
        @logger.error("Failed registering with cloud controller, status=#{http.response_header.status}")
      end
    end
    http.errback do
      @logger.error("Failed registering with cloud controller: #{http.error}")
    end
  end

      这些源码很清晰,首先是打出Sending info to cloud controller:#{@offering_uri}的的log,然后是创建一个http请求的req,然后通过新建一个HttpRequest类的实例,做post一个req的操作,一旦回复信息中status为200,则表示成功注册。这里关键部分就在http=EM::HttpRequest.new(@offering_uri).post(req)这行代码处。关于HttpRequest的代码实现是通过Gem包的形式被下载到安装gateway节点上的,主要就是通过http的方式向cloud
controller发送Rest请求。

        cloud controller收到的所有Rest请求,都会被转发到cloud_controller/cloud_controller/config/route.rb文件下,而send_heartbeat方法发来的Rest请求会被映射到该文件中的post'services/v1/offerings'=>'services#create',:as=>:service_create这行代码,从而将映射到service_controller.rb文件中的create方法。以下是create方法的具体实现(已省去部分代码):

def create
    ……
    req = VCAP::Services::Api::ServiceOfferingRequest.decode(request_body)
    ……
    success = nil
    svc = Service.find_by_label_and_provider(req.label, req.provider == "core" ? nil : req.provider)
    if svc
      CloudController.logger.debug("Found svc = #{svc.inspect}")
      ……
      svc.update_attributes!(attrs)
    else
      svc = Service.new(req.extract)
      ……
      svc.save!
    end

    render :json => {}
  end

        其中将发送来的请求解析一下,保存在req中,然后运行find_by_label_and_provider的函数。一开始对ruby和rails框架不是很熟,然后研究代码的时候通过grep查找这个方法,老是找不到这个方法,后来才发现方法的实现是被封装在ActiveRecord中的。

        ActiveRecord是Rails提供的一个对象关系映射(ORM)层。Active Record使用基本的ORM模式:表映射成类,行映射成为对象,列映射成对象的属性。与很多大量使用配置的ORM库不同,Active Record最小化了配置。由于ActiveRecord的存在,编程对于数据库的读写等不需要编写sql语句,只需通过指定格式来编写。比如find_by_label_and_provider方法只是调用了在数据库查询方面的某方法,查询的属性为label和provider。所以find_by_label_and_provider(req.label,
req.provider == "core" ? nil : req.provider)的逻辑可以等同于find(label=req.label, provider=((req.provider == "core" ? nil : req.provider))。执行该方法后,返回值赋给svc,一旦svc存在,则对数据库中的该条记录进行update操作;如果不存在的话,则将svc这条记录save到数据库中。send_heartbeat方法可以看作是向cloud_controller注册的一个过程,则注册的信息在cloud_controller处就是按照这个create方法来实现向数据库的存的。

        说到cloud_controller的数据库存储的话,这里有必要简单阐述一下。cloud_controller将postgres作为其存储信息数据库。其中,信息包括不同的service gateway向cloud_controller注册的url信息,service的绑定信息,service的credential等。如果需要了解这些信息的存储模式的话,那必须进入数据库进行查看,在实验过程中,我们使用了PG Admin的软件,建立与postgres的连接,从而查看数据库的模式以及记录的变化。

        关于heartbeat的信息,可以通过查看gateway以及cloud_controller的log来查看,以便有更深的理解。

1.2发送停用通知

          当gateway需要注销的时候,它就会向cloud_controller发送send_deactication_notice的通知,其主要代码如下:

Kernel.at_exit do
      if EM.reactor_running?
        # :/ We can't stop others from killing the event-loop here. Let's hope that they play nice
        send_deactivation_notice(false)
      else
        EM.run { send_deactivation_notice }
      end
    end

# Lets the cloud controller know that we're going away
  def send_deactivation_notice(stop_event_loop=true)
    @logger.info("Sending deactivation notice to cloud controller: #{@offering_uri}")

    req = create_http_request(
      :head => @cc_req_hdrs,
      :body => @deact_json
    )

    http = EM::HttpRequest.new(@offering_uri).post(req)

    http.callback do
      if http.response_header.status == 200
        @logger.info("Successfully deactivated with cloud controller")
      else
        @logger.error("Failed deactivation with cloud controller, status=#{http.response_header.status}")
      end
      EM.stop if stop_event_loop
    end

    http.errback do
      @logger.error("Failed deactivation with cloud controller: #{http.error}")
      EM.stop if stop_event_loop
    end
  end

        主要的实现方式的话,和send_heartbeat一致,通过http方式向cloud_controller发送REST请求,cloud_controller响应并反馈,gateway收到反馈后验证,并终止EventMachine的实例。

1.3启动gateway时添加必需的handles

        由于gateway在工作过程中会用到一些service配置和绑定的信息,而gateway存储的信息都是通过在该节点上开辟内存来实现的,所以每次在gateway启动的时候,都需要向cloud_controller发送一个获取handles的命令,从而在cloud_controller中找到关于该gateway负责服务的service信息,返回至自己。

        关于通过在本节点开辟内存来实现数据存储的方法,我认为是有利也有弊的。由于gateway为单节点,且可以有多个service_node节点,所以内存的存储会不安全。本人觉得这里可以做一下改进,gateway的handles信息可以通过数据库来存储。这样在gateway启动阶段,就不需要向cloud_controller获取handles的操作。这样的话,service的信息会在gateway和cloud_controller两个地方同步存储。

       以下是添加handles的代码实现。首先通过EM添加一个周期时钟,不停的调用fetch_handles。而每次调用fetch_handles方法,都会将向cloud_controller发送http请求,一旦收到合适状态,则显示成功获取handles,将返回的resp块进行update_handles操作,一旦成功则取消定时器fetch_handles_timer。这里的update_handles方法则是在provision.rb文件中实现。

# Add any necessary handles we don't know about
    update_callback = Proc.new do |resp|
      @provisioner.update_handles(resp.handles)
      @handle_fetched = true
      EM.cancel_timer(@fetch_handle_timer)

      # TODO remove it when we finish the migration
      current_version = @version_aliases && @version_aliases[:current]
      if current_version
        @provisioner.update_version_info(current_version)
      else
        @logger.info("No current version alias is supplied, skip update version in CCDB.")
      end
    end
    @fetch_handle_timer = EM.add_periodic_timer(@handle_fetch_interval) { fetch_handles(&update_callback) }
    EM.next_tick { fetch_handles(&update_callback) }
# Fetches canonical state (handles) from the Cloud Controller
  def fetch_handles(&cb)
    return if @fetching_handles

    @logger.info("Fetching handles from cloud controller @ #{@handles_uri}")
    @fetching_handles = true

    req = create_http_request :head => @cc_req_hdrs
    http = EM::HttpRequest.new(@handles_uri).get(req)

    http.callback do
      @fetching_handles = false
      if http.response_header.status == 200
        @logger.info("Successfully fetched handles")
        begin
          resp = VCAP::Services::Api::ListHandlesResponse.decode(http.response)
        rescue => e
          @logger.error("Error decoding reply from gateway:")
          @logger.error("#{e}")
          next
        end
        cb.call(resp)
      else
        @logger.error("Failed fetching handles, status=#{http.response_header.status}")
      end
    end

    http.errback do
      @fetching_handles = false
      @logger.error("Failed fetching handles: #{http.error}")
    end

        以上谈到的update_handles方法则是在asynchronous_service_gateway.rb同目录下的provision.rb文件中实现。由代码可知,从cloud_controller返回的handles信息则会传入update_handles方法,主要操作为验证handle的格式是否合法,另外将handles中的configuration、credentials以及service_id保存起来。存储地址就是上文提及的gateway开辟的内存prov_svcs中。具体代码如下:

def update_handles(handles)
    @logger.info("[#{service_description}] Updating #{handles.size} handles")
    handles.each do |handle|
      unless verify_handle_format(handle)
        @logger.warn("Skip not well-formed handle:#{handle}.")
        next
      end

      h = handle.deep_dup
      @prov_svcs[h['service_id']] = {
        :configuration => h['configuration'],
        :credentials => h['credentials'],
        :service_id => h['service_id']
      }
    end
    @logger.info("[#{service_description}] Handles updated")
  end


1.4 启动gateway时查看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则没有。一般这种情况很罕见,但是源码中还是考虑了这一点。

2. provision a service

           

        首先,要从代码的角度,讲述一下provision在service中与create的区别,provision为提供、创建一个service的实例,而create的实际实现为gateway向cloud_controller注册的过程。

        在通过研究gateway代码后,发现provision一个服务实例的主要步骤如下图:

       说了这么多,其实很多的组件间通信方式都是相似的。比如说VMC给cloud_controller发送http请求后,经过router.rb文件,转发到service_controller.rb文件中的provision方法。在该方法中,cloud_controller在数据库中查找相应service的url以及port,然后将request发送给指定gateway。

       gateway收到request信息后,首先对request进行一系列的验证。如果解析到request的信息中有post'/gateway/v1/configurations',经过解码等,会执行以下代码:

@provisioner.provision_service(req) do |msg|
      if msg['success']
        async_reply(VCAP::Services::Api::GatewayHandleResponse.new(msg['response']).encode)
      else
        async_reply_error(msg['response'])
      end
    end

        其实真正provision操作的方法在@provision.provison_service中,而该方法的实现在provison.rb文件中。由于代码篇幅较长,只能挑典型的进行概述。

        gateway首先根据cloud_controller发来请求中的plan,来选择合适的node。这里举个例子解释一下什么是node的plan。一般在生产环境下,同一个service会有多个node存在,node_0,node_1,…,并且每个node都对与自己的容量或者功能等有一定的描述,并发送给geteway。假设node_0的plan就是provision小于20MB的数据库,而node_1的plan是provison大于30MB的数据库,而cloud_controller发送来的request中,paln为大于30MB,则gateway通过以下代码会选中node_1加入plan_nodes中。

plan_nodes = @nodes.select{ |_, node| node["plan"] == plan}.values

        选出plan_nodes自然还是不够的,接下来是version的选择。version的话,主要指的是serivice的版本问题。从plan_nodes中选出version_nodes,主要代码如下:

version_nodes = plan_nodes.select{ |node|
        node["supported_versions"] != nil && node["supported_versions"].include?(version)

        选出version_nodes应该就差不多了,其实不然,gateway还要考虑这些符合条件的version_nodes中的使用情况怎么样,Cloud Foundry在这里关注的一点是service node的capacity的大小。所以会在version_nodes中选出best_node,这里用到的方法max_by就是选出version_nodes中的capacity最大的node,主要代码如下:

best_node = version_nodes.max_by { |node| node_score(node) }

        做完以上操作以后,gateway就开始准备合适的数据向NATS发送信息了,当收到response时,将收到的credential信息存入prov_svcs中。紧接着程序回到asynchronous_service_gateway.rb,向cloud_controller回复。

        由于unprovision的流程大致与provision一致,所以这里不在讲述。

3. bind a service

        

        bind一个服务,其实是一个缺少主语的说法,自然是已经存在了一个app,然后app需要bind一个服务。其实,每次bind的操作都是更新app本身的manifest中,然后发送一条update_app的指令。

        以下为bind一个服务的主要流程:

        主要代码的流程还是与provision非常相似,首先vmc发出命令,cloud_controller经过route.rb分发任务。在service_controller.rb中,bind方法实现app与provisioned instance的绑定。

def bind
    req = VCAP::Services::Api::CloudControllerBindRequest.decode(request_body)

    app = ::App.find_by_collaborator_and_id(user, req.app_id)
    raise CloudError.new(CloudError::APP_NOT_FOUND) unless app

    cfg = ServiceConfig.find_by_name(req.service_id)
    raise CloudError.new(CloudError::SERVICE_NOT_FOUND) unless cfg
    raise CloudError.new(CloudError::FORBIDDEN) unless cfg.provisioned_by?(user)

    binding = app.bind_to_config(cfg)

    resp = {
      :binding_token => binding.binding_token.uuid,
      :label => cfg.service.label
    }
    render :json => resp
  end

        首先,解析request_body,记者在cloud_controller的数据库中查询到相应user的相应app,随后找到service_id的配置信息,随即binding = app.bind_to_config(cfg),该方法的实现在cloud_controller/app/models/app.rb中,该方法首先创建一个binding_token,然后创建一个bind request,随后通过以下代码发送bind request:

client = VCAP::Services::Api::ServiceGatewayClient.new(svc.url, svc.token, svc.timeout)
      handle = client.bind(req.extract)

        该方法的实现文件,是通过gemfile的形式被下载到cloud_controller节点。,名为service_gateway_client.rb 。

        就这样bind命令就从cloud_controller发送出来了,接受者自然是gateway,以下来gateway的处理代码:

# Binds a previously provisioned instance of the service to an application
  post '/gateway/v1/configurations/:service_id/handles' do
    @logger.info("Binding request for service=#{params['service_id']}")

    req = VCAP::Services::Api::GatewayBindRequest.decode(request_body)
    @logger.debug("Binding options: #{req.binding_options.inspect}")

    @provisioner.bind_instance(req.service_id, req.binding_options) do |msg|
      if msg['success']
        async_reply(VCAP::Services::Api::GatewayHandleResponse.new(msg['response']).encode)
      else
        async_reply_error(msg['response'])
      end
    end
    async_mode
  end

       可见其主要代码在于bind_instance一行。该方法的实现在provision.rb文件中。该方法的实现代码比较长,但是细看以后也能分割成几个模块:选找service node_id,创建request,通过NATS发送bind请求,等待response。

       一旦bind成功,则回到以上贴出的代码段。更新prov_svcs的同时,向cloud_controller回复。cloud_controller收到resp后,将其保存在service_binding的数据表中。

       这样,从原理上讲bind直接成功实现了,当update app的时候,cloud_controller中由于已经存在bind信息,所以app的atage的过程中,会带有bind信息,当start app的时候,app即通过访问service所必需的credential去访问service,而此时是不经过gateway的。

4. 对gateway的一些看法

       

        最后分析完源码,对gateway组件有一些自己的看法。       

        对于gateway的看法,主要的从两个方面来看,一个是性能,一个是安全。

        首先是性能。在了解gateway实现机制的时候,我对它使用内存来存储信息的方式,感到很诧异。gateway随着使用时间的增长,provision和bind信息越来越多,很难保证内存不会出现性能问题。当存储信息很大时,其查询带来的时间消耗自然是不必要考虑的,因为在内存中数组的线性查询几乎可以不考虑时间消耗。但是gateway一旦挂掉的话,由于使用内存存储信息的缘故,存储信息会全部遗失,只有当重启gateway的时候,gateway会从cloud_controller重新获得遗失的信息,但是如果信息量很大的话,这必定也会消耗大量的时间,而且如果gateway又比较不稳定的话,这方面的时间开销几乎是不可容忍的。

        既然性能上存在缺陷,如果在gateway上使用轻量级的数据库来存储信息的话,这样的问题就有了改观,首先由于数据存储的信息是持久化的,所以不存在gateway挂掉向cloud_controller索取的时间开销问题,但是数据库的IO读写会占不少的时间。

        因此,如果gateway能稳定工作的话,内存自然是首选;如果gateway在大信息量以及不稳定的环境下,使用数据库存储信息不失为一个好的方法。

       另外一个方面是安全。了解Cloud Foundry可以知道,其中的每项服务都只有一个gateway(单节点)。那么单节点的话,需要考虑负载的问题,需要考虑单节点挂掉的问题嘛?

       关于负载的问题,也就是有很多的用户需要provision服务,有很多的app都需要bind服务,众多的访问量会不会使得gateway 崩溃?首先,我觉得应该出现这种情况,需要一个前提,那就是有众多的用户来provision服务,有众多的app需要要bind服务。比如说bind的情况的,bind只是一个一次性操作,一旦band完,就和gateway无关了,对于一个app的生命周期来讲,是极其小的一个时间段。众多的app都要来bind服务的情况本来就不大可能,而且就算有这样的可能性的话,那众多app的运行环境DEA肯定是已经处于超负荷的情况,所以相对与整个Cloud
Foundry来讲,gateway这边的多并发显得意义不是很大,因为瓶颈不在这里。

       单节点挂掉的安全问题,肯定是一个大问题。因为一旦挂掉的话,对于已经bind完服务的app来说,没有任何影响,但是对于之后需要bind或者provision服务的app,会是一个致命的问题。通过这一点,可以知道设计multi-gateway或者主从gateway就显得非常有必要。这也是Cloud Foundry爱好者或者Cloud Foundry的工作人员以后会涉及的部分。

转载清注明出处。

这篇文档更多出于我本人的理解,肯定在一些地方存在不足和错误。希望本文能够对开始接触Cloud Foundry中service的人有些帮助,如果你对这方面感兴趣,并有更好的想法和建议,也请联系我。

我的邮箱:shlallen@zju.edu.cn
新浪微博:@莲子弗如清

      

抱歉!评论已关闭.