Cloud Foundry中Stager组件的源码分析

        Cloud Foundry中有一个组件,名为Stager,它主要负责的工作就是将用户部署进Cloud Foundry的源代码打包成一个DEA可以解压执行的droplet。

        关于droplet的制作,Cloud Foundry v1中一个完整的流程为:

  1. 用户将应用源代码上传至Cloud Controller;
  2. Cloud Controller通过NATS发送请求至Stager,要求制作dropet;
  3. Stager从Cloud Controller下载压缩后的应用源码,并解压;
  4. Stager将解压后的应用源码,添加运行容器以及启动终止脚本;
  5. 压缩成droplet形式上传至Cloud Foundry。


Stager制作droplet总流程实现

        现在从制作一个droplet的流程将分析Cloud Foundry中Stager的源码。

        从Stager接收到请求,则开始制作droplet。而Stager能接收到Cloud Controller发送来的stage请求,是由于Stager订阅了主题为“staging”的消息,/stager/lib/vcap/server.rb中代码如下:

  def setup_subscriptions
    @config[:queues].each do |q|
      @sids << @nats_conn.subscribe(q, :queue => q) do |msg, reply_to|
        @thread_pool.enqueue { execute_request(msg, reply_to) }
        @logger.info("Enqueued request #{msg}")
      end
      @logger.info("Subscribed to #{q}")
    end
  end
        由以上代码可知,订阅了@config[:queues]中的主题后,关于该主题消息的发布,被Stager接收到后,Stager执行execute_request方法执行接收到的消息msg。在这里的实现,还借助了@thread_pool,该变量是创建了一个线程池,将执行结果加入线程池。

        以下进入/stager/lib/vcap/server.rb的execute_request方法:

  def execute_request(encoded_request, reply_to)
    begin
      request = Yajl::Parser.parse(encoded_request)
    rescue => e
      ……
      return
    end
    task = VCAP::Stager::Task.new(request, @task_config)
    result = nil
    begin
      task.perform
      ……
    end
    encoded_result = Yajl::Encoder.encode(result)
    EM.next_tick { @nats_conn.publish(reply_to, encoded_result) }
    nil
  end
        在该方法中首先对request请求进行解析,获得request对象,然后通过request对象和@task_config产生一个task对象,Task类为VCAP::Stager::Task。其中@task_config中有属性:ruby_path, :ruby_plugin_path, secure_user_manager。创建为task对象之后,执行task.perform。关于perform方法,是整个Stager执行流中最为重要的部分,以下便进入/stager/lib/vcap/stager/task.rb的perform方法中:

  def perform
    workspace = VCAP::Stager::Workspace.create
    app_path = File.join(workspace.root_dir, "app.zip")
    download_app(app_path)

    unpack_app(app_path, workspace.unstaged_dir)

    stage_app(workspace.unstaged_dir, workspace.staged_dir, @task_logger)

    droplet_path = File.join(workspace.root_dir, "droplet.tgz")
    create_droplet(workspace.staged_dir, droplet_path)

    upload_droplet(droplet_path)
    nil
  ensure
    workspace.destroy if workspace
  end
        该方法中的流程非常清晰,顺序一次是download_app, upack_app, stage_app, create_droplet, upload_app。

        首先先进入download_app方法:

  def download_app(app_path)
    cfg_file = Tempfile.new("curl_dl_config")
    write_curl_config(@request["download_uri"], cfg_file.path,"output" => app_path)
    # Show errors but not progress, fail on non-200
    res = @runner.run_logged("env -u http_proxy -u https_proxy curl -s -S -f -K #{cfg_file.path}")
    unless res[:status].success?
      raise VCAP::Stager::TaskError.new("Failed downloading app")
    end
    nil
  ensure
    cfg_file.unlink if cfg_file
  end
        该方法中较为重要的部分就是如何write_curl_config,以及如何执行脚本命令。在write_curl_config中有一个参数@request["download_uri"],该参数的意义是用户上传的应用源码存在Cloud Controller处的位置,也是Stager要去下载的应用源码的未知。该参数的产生的流程为:Cloud Controller中的app_controller.rb中,调用stage_app方法,该方法中调用了download_uri方法,并将其作为request的一部分,通过NATS发给了Stager。关于write_curl_config主要实现的是将curl的配置条件写入指定的路径,以便在执行curl命令的时候,可以通过配置文件的读入来方便实现,实现代码为:res = @runner.run_logged("env -u http_proxy -u https_proxy curl -s -S -f -K #{cfg_file.path}")。

        当将应用源码下载至指定路径之后,第二步需要做的是将其解压,unpack_app方法则实现了这一点,实现代码为:res = @runner.run_logged("unzip -q #{packed_app_path} -d #{dst_dir}")。

        第三步需要做的,也是整个stager最为重要的部分,就是stage_app方法。实现代码如下:

  def stage_app(src_dir, dst_dir, task_logger)
    plugin_config = {
      "source_dir"   => src_dir,
      "dest_dir"     => dst_dir,
      "environment"  => @request["properties"]
    }
    ……
    plugin_config_file = Tempfile.new("plugin_config")
    StagingPlugin::Config.to_file(plugin_config, plugin_config_file.path)

    cmd = [@ruby_path, @run_plugin_path,
           @request["properties"]["framework_info"]["name"],
           plugin_config_file.path].join(" ")
    res = @runner.run_logged(cmd,:max_staging_duration => @max_st./bin/catalina.sh runaging_duration)
    ……
  ensure
    plugin_config_file.unlink if plugin_config_file
    return_secure_user(secure_user) if secure_user
  end

        在该方法中,首先创建plugin_config这个Hash对象,随后创建plugin_config_file, 又创建了cmd对象,最后通过res = @runner.run_logged(cmd, :max_staging_duration => @max_staging_duration)实现了执行了cmd。现在分析cmd对象:

     cmd = [@ruby_path, @run_plugin_path,
           @request["properties"]["framework_info"]["name"],
           plugin_config_file.path].join(" ")
        该对象中@ruby_path为Stager组件所在节点出ruby的可执行文件路径;@ruby_plugin_path为运行plugin可执行文件的路径,具体为:/stager/bin/run_plugin;@request["properties"]["framework_info"]["name"]为所需要执行stage操作的应用源码的框架名称,比如,Java_web,spring,Play, Rails3等框架;而 plugin_config_file.path则是做plugin时所需配置文件的路径。

        关于cmd对象的执行,将作为本文的一个重要模块,稍后再讲,现在先将讲述一个完整dropet制作流程的实现。

        当昨晚stage_app工作之后,也就相当于一个将源码放入了一个server容器中,也做到了实现添加启动终止脚本等,但是这些都是一个完成的文件目录结构存在与文件系统中,为方便管理以及节省空间,Stager会将stage做完后的内容进行压缩,create_droplet则是实现了这一部分的内容,代码如下:

  def create_droplet(staged_dir, droplet_path)
    cmd = ["cd", staged_dir, "&&", "COPYFILE_DISABLE=true", "tar", "-czf", droplet_path, "*"].join(" ")
    res = @runner.run_logged(cmd)
    unless res[:status].success?
      raise VCAP::Stager::TaskError.new("Failed creating droplet")
    end
  end
        当创建完droplet这个压缩包之后,Stager还会将这个dropet上传至Cloud Controller的某个路径下,以便之后DEA在启动这个应用的时候,可以从Cloud Controller的文件系统中下载到droplet,并解压启动。以下是upload_app的代码实现:

  def upload_droplet(droplet_path)
    cfg_file = Tempfile.new("curl_ul_config")
    write_curl_config(@request["upload_uri"], cfg_file.path, "form" => "upload[droplet]=@#{droplet_path}")
    res = @runner.run_logged("env -u http_proxy -u https_proxy curl -s -S -f -K #{cfg_file.path}")
    ……
    nil
  ensure
    cfg_file.unlink if cfg_file
  end
        至此的话,Stager所做工作的大致流程,已经全部完成,只是在实现的同时,采用的详细技术,本文并没有一一讲述。

        

        虽然Stager的实现流程已经讲述完毕,但是关于Stager最重要的模块stage_app方法的具体实现,本文还没有进行讲述,本文以下内容会详细讲解这部分内容,并以Java_web和standalone这两种不同框架的应用为案例进行分析。

        上文中以及提到,实现stage的功能的代码部分为:

    cmd = [@ruby_path, @run_plugin_path,
           @request["properties"]["framework_info"]["name"],
           plugin_config_file.path].join(" ")./bin/catalina.sh run

    res = @runner.run_logged(cmd,
                             :max_staging_duration => @max_staging_duration)
        关于cmd中的各个参数,上文已经分析过,现在更深入的了解@run_plugin_path,该对象指向/stager/bin/run_plugin可执行文件,现在进入该文件中:

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'rubygems'
require 'bundler/setup'
$LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
require 'vcap/staging/plugin/common'
unless ARGV.length == 2
  puts "Usage: run_staging_plugin [plugin name] [plugin config file]"
  exit 1
end
plugin_name, config_path = ARGV
klass  = StagingPlugin.load_plugin_for(plugin_name)
plugin = klass.from_file(config_path)
plugin.stage_application
        该部分的源码主要实现了,提取出cmd命令中的后两个参数,一个为plugin_name,另一个为config_name,并通过这两个参数实现加载plugin以及真正的stage。

        首先进入load_plugin_for方法,源码位置为/vcap_staging/lib/vcap/staging/plugin/common.rb:

  def self.load_plugin_for(framework)./bin/catalina.sh run
    framework = framework.to_s
    plugin_path = File.join(staging_root, framework, 'plugin.rb')
    require plugin_path
    Object.const_get("#{camelize(framework)}Plugin")
  end

Java_web框架应用的stage流程       

        首先以Java_web为例在load_plugin_for方法中,首先提取出该应用源码所设置的框架,并通过框架来创建plugin_path对象,然后再实现require该框架目录下的plugin.rb文件,Java_web的应用则需要require的文件为:/vcap-staging/lib/vcap/staging/plugin/java_web/plugin.rb。

        通过require了子类的plugin.rb之后,当执行plugin.stage_application的时候,直接进入/vcap-staging/lib/vcap/staging/plugin/java_web/plugin.rb中的stage_application方法,该方法的源码实现为:

  def stage_application
    Dir.chdir(destination_directory) do
      create_app_directories
      webapp_root = Tomcat.prepare(destination_directory)
      copy_source_files(webapp_root)
      web_config_file = File.join(webapp_root, 'WEB-INF/web.xml')
      unless File.exist? web_config_file
        raise "Web application staging failed: web.xml not found"
      end
      services = environment[:services] if environment
      copy_service_drivers(File.join(webapp_root,'../../lib'), services)
      Tomcat.prepare_insight destination_directory, environment, insight_agent if Tomcat.insight_bound? services
      configure_webapp(webapp_root, self.autostaging_template, environment) unless self.skip_staging(webapp_root)
      create_startup_script
      create_stop_script
    end
  end
        在stage_application中,实现步骤为:1.创建相应的目录;2.拷贝源码和相应的所需文件;3.设置配置文件;4.添加启动和终止脚本。

        创建相应的目录,包括create_app_directory,在应用的的目标目录下创建log文件夹以及tmp文件夹。

        在实现代码 webapp_root = Tomcat.prepare(destination_directory) 时,进入/vcap-staging/lib/vcap/staging/plugin/java_web/tomcat.rb的prepare方法:

  def self.prepare(dir)
    FileUtils.cp_r(resource_dir, dir)
    output = %x[cd #{dir}; unzip -q resources/tomcat.zip]
    raise "Could not unpack Tomcat: #{output}" unless $? == 0
    webapp_path = File.join(dir, "tomcat", "webapps", "ROOT")
    server_xml = File.join(dir, "tomcat", "conf", "server.xml")
    FileUtils.rm_f(server_xml)
    FileUtils.rm(File.join(dir, "resources", "tomcat.zip"))
    FileUtils.mv(File.join(dir, "resources", "droplet.yaml"), dir)
    FileUtils.mkdir_p(webapp_path)
    webapp_path
  end
       该方法中,首先从resource文件夹拷贝至dir目录下,随即将resource文件夹中的tomcat.zip文件解压,随后在dir目录下进行一系列的文件操作。最后返回web_app路径。

       返回至stage_application方法中的,copy_source_fiiles从一些source文件全部拷贝至webapp_path下,代码为:copy_source_files(webapp_root);随后检查WEB-INF/web.xml配置文件是否存在,若不存在的话,抛出异常,代码为“

unless File.exist? web_config_file
        raise "Web application staging failed: web.xml not found"
      end

        接着将应用所需的一些服务驱动拷贝至指定的lib目录下,代码为:copy_service_drivers(File.join(webapp_root,'../../lib'), services);随即又实现了对Tomcat的配置,主要是代理的配置:      Tomcat.prepare_insight destination_directory, environment, insight_agent if Tomcat.insight_bound? services ;又然后对webapp路木进行了配置,代码为:configure_webapp(webapp_root, self.autostaging_template, environment) unless self.skip_staging(webapp_root);最后终于到了实现对启动脚本和终止脚本的生成,代码为:

      create_startup_script
      create_stop_script
         首先来看一下Java_web框架应用的的启动脚本创建,在/vcap-staging/lib/vcap/staging/plugin/common.rb中的create_startup_script方法:

  def create_startup_script
    path = File.join(destination_directory, 'startup')
    File.open(path, 'wb') do |f|
      f.puts startup_script
    end
    FileUtils.chmod(0500, path)
  end
        在该方法的时候,首先在目标文件中添加一个startup文件,然后再打开该文件,并通过startup_script产生的内容写入startup文件,最后对startup文件进行权限配置。现在进入startup_script方法中,位置为/vcap-staging/lib/vcap/staging/plugin/java_web/plugin.rb:

  def startup_script
    vars = {}
    vars['CATALINA_OPTS'] = configure_catalina_opts
    generate_startup_script(vars) do
      <<-JAVA
export CATALINA_OPTS="$CATALINA_OPTS `ruby resources/set_environm./bin/catalina.sh run./bin/catalina.sh run./bin/catalina.sh runent`"
env > env.log./bin/catalina.sh run
PORT=-1
while getopts ":p:" opt; do
  case $opt in
    p)
      PORT=$OPTARG
      ;;
  esac
done
if [ $PORT -lt 0 ] ; then
  echo "Missing or invalid port (-p)"
  exit 1
fi
ruby resources/generate_server_xml $PORT
      JAVA
    end
  end
         在generate_startup_script方法中回天夹start_command,位于/vcap-staging/lib/vcap/staging/plugin/common.rb,运行脚本的添加实现为:

<%= change_directory_for_start %>
<%= start_command %> > ../logs/stdout.log 2> ../logs/stderr.log &
        其中change_directory_for_start为: cd tomcat;而start_command为:./bin/catalina.sh run。

        至此的话,启动脚本的添加也就完成了,终止的脚本添加的话,也大致一样,主要还是获取应用进程的pid,然后强制杀死该进程,本文就不再具体讲解stop脚本的生成。

        讲解到这的话,一个Java_web框架的app应用以及完全stage完成了,在后续会被打包成一个droplet并上传。stage完成之后,自然是需要由DEA来使用的,而DEA正是获取droplet,解压到相应的目录下,最后通过DEA节点提供的运行环境,执行压缩后的droplet中的startup脚本,并最终完全在DEA中启动应用。


Standalone框架应用的stage流程

        在Cloud Foundry中,standalone应用被认为是不能被Cloud Foundry识别出框架的所有类型应用。在这种情况下,Cloud Foundry的stager还是会对其进行打包。

        在制作standalone应用的droplet时,总的制作流程与Java_web以及其他能被Cloud Foundry识别的框架相比,没有说明区别,但是在stage的时候,会由很大的区别。因为对于standalone的应用,Cloud Foundry的stager不会提供出源码以外的所有运行依赖,所以关于应用的执行的依赖,必须由用户在上传前将其与应用源码捆绑之后在上传。

        在识别到需要打包的源码的框架被定义为standalone之后,Stager使用子类StagingPlugin的子类StandalonePlugin来实现stage过程。流程与其类型框架的应用相同,但是具体操作会有一些区别,首先来阅读stage_application的方法:

  def stage_application
    Dir.chdir(destination_directory) do
      create_app_directories
      copy_source_files
      #Give everything executable perms, as start command may be a script
      FileUtils.chmod_R(0744, File.join(destination_directory, 'app'))
      runtime_specific_staging
      create_startup_script
      create_stop_script
    end
  end
        主要的区别在与runtime_specific_staging及之后的方法。runtime_specific_staging方法的功能主要是判断该应用程序的运行环境是否需要ruby,若不需要的话,staging_application继续往下执行;若需要的话,则为应用准备相应的gem包以及安装gem包。

        随后在create_startup_script的方法中,首先解析应用的运行时类型,如果是ruby,java,python的话,那就生成相应的启动脚本,如果是其他类型的话,那就直接进入generate_startup_script方法。现在以java运行时为例,分析该过程的实现,代码如下:

  def java_startup_script
    vars = {}
    java_sys_props = "-Djava.io.tmpdir=$PWD/tmp"
    vars['JAVA_OPTS'] = "$JAVA_OPTS -Xms#{application_memory}m -Xmx#{application_memory}m #{java_sys_props}"
    generate_startup_script(vars)
  end
         该方法的实现,创建了vars对象之后,通过将vars参数传递给方法generate_startup_script,来实现启动脚本的生成,代码如下:

  def generate_startup_script(env_vars = {})
    after_env_before_script = block_given? ? yield : "\n"
    template = <<-SCRIPT
#!/bin/bash
<%= environment_statements_for(env_vars) %>
<%= after_env_before_script %>
<%= change_directory_for_start %>
<%= start_command %> > ../logs/stdout.log 2> ../logs/stderr.log &
<%= get_launched_process_pid %>
echo "$STARTED" >> ../run.pid
<%= wait_for_launched_process %>
    SCRIPT
    # TODO - ERB is pretty irritating when it comes to blank lines, such as when 'after_env_before_script' is nil.
    # There is probably a better way that doesn't involve making the above Heredoc horrible.
    ERB.new(template).result(binding).lines.reject {|l| l =~ /^\s*$/}.join
  end
        到这里,便是stage过程中启动脚本的生成,终止脚本的生成的话,流程也一致,主要是获取应用进程的pid,然后通过kill pid来实现对应用的终止。


        以上便是笔者对于Cloud Foundry中Stager组件的简单源码分析。


关于作者:

孙宏亮,DAOCLOUD软件工程师。两年来在云计算方面主要研究PaaS领域的相关知识与技术。坚信轻量级虚拟化容器的技术,会给PaaS领域带来深度影响,甚至决定未来PaaS技术的走向。


转载清注明出处。

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

我的邮箱:[email protected]
新浪微博: @莲子弗如清


你可能感兴趣的:(源码,cloud,droplet,foundry,stager)