Cloud Foundry中有一個組件,名爲Stager,它主要負責的工作就是將用戶部署進Cloud Foundry的源代碼打包成一個DEA可以解壓執行的droplet。
關於droplet的製作,Cloud Foundry v1中一個完整的流程爲:
- 用戶將應用源代碼上傳至Cloud Controller;
- Cloud Controller通過NATS發送請求至Stager,要求製作dropet;
- Stager從Cloud Controller下載壓縮後的應用源碼,並解壓;
- Stager將解壓後的應用源碼,添加運行容器以及啓動終止腳本;
- 壓縮成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]新浪微博:@蓮子弗如清