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]
新浪微博:@蓮子弗如清


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