Cloud Foundry中应用实例生命周期过程中的文件目录分析

在Cloud Foundry中,应用在DEA上运行,而应用在自身的生命周期中,自身的文件目录也会随着不同的周期,做出不同的变化。

        本文将从创建一个应用(start an app),停止一个应用(stop an app),删除一个应用(delete an app),重启一个应用(restart an app),应用crash,关闭dea,启动dea,dea异常退出后重启,这几个方面入手,进行分析应用实例目录的变化。

        本文所讲述的Cloud Foundry仅限于v1版本,v2版本会后续跟进。

start an app

        start an app主要是指应用用户发出请求,让Cloud Foundry创建一个应用,或者启动一个应用。需要注意的是,在start an app之前,Cloud Foundry的每一个DEA中都不会存有该app的文件。在某一个DEA接受到start an app的请求后,该DEA必须从存放droplet的地方,下载droplet,并在DEA所在节点的某个文件路径下解压改droplet,最终启动解压后的droplet的应用启动脚本。这样的话,该DEA的文件系统中就会有一个该应用相应的文件目录存在。

        以上操作的代码实现,在/dea/lib/dea/agent.rb的process_dea_start方法中:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. tgz_file = File.join(@staged_dir"#{sha1}.tgz")  
  2. instance_dir = File.join(@apps_dir"#{name}-#{instance_index}-#{instance_id}")  
        该部分的代码产生应用在所在DEA上的压缩包文件目录以及具体执行的文件目录,并在后续的success = stage_app_dir(bits_file, bits_uri, sha1, tgz_file, instance_dir, runtime)中实现下载应用源码至instance_dir。启动完成之后,以上的instance_dir,就是该应用的文件路径。

        总结:start an app创建应用在某一个DEA上的文件目录并启动该应用。

stop an app

        stop an app主要是指应用用户发出请求,让Cloud Foundry停止一个应用的运行。需要注意的是,在stop an app之前,肯定是必须要在运行的该应用,该应用的文件目录以及源码已经存在于某一个DEA的文件系统中。Cloud Controller收到用户的stop an app请求后,首先会找到该应用所在运行的DEA节点,并对该DEA发送stop该应用的请求。当DEA接收到该请求后,执行process_dea_stop方法,如下:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1.       NATS.subscribe('dea.stop') { |msg| process_dea_stop(msg) }  
        在process_dea_stop中,主要执行的便是该应用的停止,包括该应用的所有实例,代码实现如下:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. return unless instances = @droplets[droplet_id]  
  2. instances.each_value do |instance|  
  3.   version_matched  = version.nil? || instance[:version] == version  
  4.   instance_matched = instance_ids.nil? || instance_ids.include?(instance[:instance_id])  
  5.   index_matched    = indices.nil? || indices.include?(instance[:instance_index])  
  6.   state_matched    = states.nil? || states.include?(instance[:state].to_s)  
  7.   if (version_matched && instance_matched && index_matched && state_matched)  
  8.     instance[:exit_reason] = :STOPPED if [:STARTING, :RUNNING].include?(instance[:state])  
  9.     if instance[:state] == :CRASHED  
  10.       instance[:state] = :DELETED  
  11.       instance[:stop_processed] = false  
  12.     end  
  13.     stop_droplet(instance)  
  14.   end  
  15. end  
         首先现在@droplets这个hash对象中找到所在停止的应用id,然后再遍历该应用的所有实例,在对应用实例进行状态处理之后,随即执行stop_droplet方法。也就是说真正实现停止应用实例的操作在stop_droplet方法,以下进入该方法的代码实现:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1.     def stop_droplet(instance)  
  2.       return if (instance[:stop_processed])  
  3.       send_exited_message(instance)  
  4.       username = instance[:secure_user]  
  5.   
  6.       # if system thinks this process is running, make sure to execute stop script  
  7.       if instance[:pid] || [:STARTING, :RUNNING].include?(instance[:state])  
  8.         instance[:state] = :STOPPED unless instance[:state] == :CRASHED  
  9.         instance[:state_timestamp] = Time.now.to_i  
  10.         stop_script = File.join(instance[:dir], 'stop')  
  11.         insecure_stop_cmd = "#{stop_script} #{instance[:pid]} 2> /dev/null"  
  12.         stop_cmd =  
  13.           if @secure  
  14.             "su -c \"#{insecure_stop_cmd}\" #{username}"  
  15.           else  
  16.             insecure_stop_cmd  
  17.           end  
  18.         unless (RUBY_PLATFORM =~ /darwin/ and @secure)  
  19.           Bundler.with_clean_env { system(stop_cmd) }  
  20.         end  
  21.       end  
  22.       ………………  
  23.       cleanup_droplet(instance)  
  24.     end  
        可以看到在该方法中,主要是通过执行该应用的停止脚本来实现stop an app请求。其中,stop_script = File.join(instance[:dir], 'stop')为找到停止脚本所在的位置,insecure_stop_cmd = "#{stop_script} #{instance[:pid]} 2> /dev/null"未生成脚本命令,然后通过@secure变量重生成stop_cmd,最后执行Bundler.with_clean_env { system(stop_cmd) },实现为启动一个全新环境来让操作系统执行脚本stop_cmd。

        其实本文最关心的是DEA接下来的操作cleanup_droplet操作,因为该操作才是真正于应用在DEA文件系统目录相关的部分。以下进入cleanup_droplet方法:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. def cleanup_droplet(instance)  
  2.   remove_instance_resources(instance)  
  3.   @usage.delete(instance[:pid]) if instance[:pid]  
  4.   if instance[:state] != :CRASHED || instance[:flapping]  
  5.     if droplet = @droplets[instance[:droplet_id].to_s]  
  6.       droplet.delete(instance[:instance_id])  
  7.       @droplets.delete(instance[:droplet_id].to_s) if droplet.empty?  
  8.       schedule_snapshot  
  9.     end  
  10.     unless @disable_dir_cleanup  
  11.       @logger.debug("#{instance[:name]}: Cleaning up dir #{instance[:dir]}#{instance[:flapping]?' (flapping)':''}")  
  12.       EM.system("rm -rf #{instance[:dir]}")  
  13.     endFileUtils.mv(tmp.path, @app_state_file)  
  14.   else  
  15.     @logger.debug("#{instance[:name]}: Chowning crashed dir #{instance[:dir]}")  
  16.     EM.system("chown -R #{Process.euid}:#{Process.egid} #{instance[:dir]}")  
  17.   end  
  18. end  
        在该方法中,检查应用实例状态后,如果应用的状态不为:CRASHED或者instance[:flapping]不为真时,在@droplets这个hash对象中删除所要停止的应用实例ID,随后进行schedule_snapshot操作,该方法的实现于作用稍后会进行分析。然后通过以下代码实现应用实例文件目录删除:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. unless @disable_dir_cleanup  
  2.    @logger.debug("#{instance[:name]}: Cleaning up dir #{instance[:dir]}#{instance[:flapping]?' (flapping)':''}")  
  3.    EM.system("rm -rf #{instance[:dir]}")  
  4. end  
        也就在是说@disable_dir_cleanup变量为真话,不会执行脚本命令 rm -rf #{instance[:dir]} ,如果为假,则执行脚本命令 rm -rf #{instance[:dir]} ,换句话说会将应用实例的文件目录全部删除。在默认情况下,Cloud Foundry关于@disable_dir_cleanup变量的初始化,在agent类的intialize()方法中,初始化读取配置config['disable_dir_cleanup'],而该配置默认为空,即为假。

        现在分析刚才涉及的方法schedule_snapshot方法,在stop_droplet方法中,删除了@droplets中关于要删除应用实例的信息后,随即调用该schedule_snapshot方法。该方法的实现如下:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. def schedule_snapshot  
  2.   return if @snapshot_scheduled  
  3.   @snapshot_scheduled = true  
  4.   EM.next_tick { snapshot_app_state }  
  5. end  
        可以看到主要是实现了snapshot_app_state方法,现在进入该方法:

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. def snapshot_app_state  
  2.   start = Time.now  
  3.   tmp = File.new("#{@db_dir}/snap_#{Time.now.to_i}", 'w')  
  4.   tmp.puts(JSON.pretty_generate(@droplets))  
  5.   tmp.close  
  6.   FileUtils.mv(tmp.path, @app_state_file)  
  7.   @logger.debug("Took #{Time.now - start} to snapshot application state.")  
  8.   @snapshot_scheduled = false  
  9. end  
        首先,该方法获取了当前时间,并以tmp = File.new("#{@db_dir}/snap_#{Time.now.to_i}", 'w')创建了一个文件,通过将@droplets变量json化,随后将json信息写入tmp文件;关闭该文件后,通过命令FileUtils.mv(tmp.path, @app_state_file)实现将该tmp文件重命名为@app_state_file,该变量为@app_state_file = File.join(@db_dir, APP_STATE_FILE),其中APP_STATE_FILE = 'applications.json':。

        总结,当stop an app时,DEA的操作流程如下:

  1. 删除该app的所有实例在@droplets中的信息;
  2. 对该app的所有实例执行stop脚本;
  3. 将删除指定记录后的@droplets对象中的所有记录写入@app_state_file;
  4. 对该app的所有实例的文件目录,进行删除处理。

delete an app

        delete an app主要是指应用用户发起一个删除应用的请求,该请求由Cloud Controller捕获,Cloud Controller首先 将该应用的所有实例停止,然后再将该应用的droplet删除掉。因此,在操作该请求的时候,有相关该应用的所有信息都会被删除,自然包括该应用实例在DEA上的文件目录。


restart an app

        restart an app主要是指应用用户发起一个重启应用的请求,该请求在vmc处的实现就是分解为两个请求,一个stop请求,一个start请求。因此,stop请求在一个DEA上停止该应用的运行,并且删除该应用的文件目录;而start请求在一个DEA上现下载该应用的源码,也就是创建一个文件目录,最后将该应用启动起来。需要特别注意的是,执行stop请求的DEA和执行start请求的DEA不一定是同一个DEA。执行stop请求的DEA为当前需要停止的应用所在的DEA,而执行start请求的DEA,需要由Cloud Controller决策而出。


app crashes

        app crashes主要是指应用在运行过程中出现了崩溃的请求。换句话说,应用崩溃,DEA是事先不知晓的,这和stop an app有很大的区别,在具体集群中可以通过强制杀死应用进程来模拟应用的崩溃。

        首先由于应用的崩溃不经过DEA,所以DEA不会执行stop_droplet方法以及cleanup_droplet方法,理论上该应用的文件目录依然会存在于DEA的文件系统中,据许占据DEA文件系统的磁盘空间。可以想象,如果应用长此以往的话,对系统磁盘空间的浪费是很明显的。而关于这个话题,Cloud Foundry中DEA会采取定期执行清除crashed应用的操作,将已经崩溃的应用文件目录删除。

        具体来讲,由于应用崩溃,那么关于之前该应用的pid也就不会存在了(理论上是这样),在DEA定期执行monitor_app方法的时候,将所有进程的信息保存起来,随后执行monitor_apps_helper方法,对于@droplets中的每一个应用的每一个实例,将其的pid信息于实际在DEA节点处的进程pid进行对比,如果失败,则说明@droplets中的该应用实例已经不在运行,可以认为是不正常的退出执行。实现代码如下:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. def monitor_apps_helper(startup_check, ma_start, du_start, du_all_out, pid_info, user_info)  
  2.       …………  
  3.   
  4.       @droplets.each_value do |instances|  
  5.         instances.each_value do |instance|  
  6.           if instance[:pid] && pid_info[instance[:pid]]  
  7.             …………  
  8.           else  
  9.             # App *should* no longer be running if we are here  
  10.             instance.delete(:pid)  
  11.             # Check to see if this is an orphan that is no longer running, clean up here if needed  
  12.             # since there will not be a cleanup proc or stop call associated with the instance..  
  13.             stop_droplet(instance) if (instance[:orphaned] && !instance[:stop_processed])  
  14.           end  
  15.         end  
  16.       end  
  17.       …………  
  18.     end  

        当发现该应用实例实际情况下已经不再运行的话,DEA就会执行代码 instance.delete(:pid) 以及 stop_droplet(instance) if (instance[:orphaned] && !instance[:stop_processed]) ,可以如果(instance[:orphaned] && !instance[:stop_processed]) 为真的话,那就执行stop_droplet方法,在执行stop_droplet方法的时候,由于先执行send_exited_message方法,如下:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. def stop_droplet(instance)  
  2.       # On stop from cloud controller, this can get called twice. Just make sure we are re-entrant..  
  3.       return if (instance[:stop_processed])  
  4.   
  5.       # Unplug us from the system immediately, both the routers and health managers.  
  6.       send_exited_message(instance)  
  7.   
  8.       ……  
  9.       cleanup_droplet(instance)  
  10.     end  
        而send_exited_message方法中的代码实现如下:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. def send_exited_message(instance)  
  2.   return if instance[:notified]  
  3.   
  4.   unregister_instance_from_router(instance)  
  5.   
  6.   unless instance[:exit_reason]  
  7.     instance[:exit_reason] = :CRASHED  
  8.     instance[:state] = :CRASHED  
  9.     instance[:state_timestamp] = Time.now.to_i  
  10.     instance.delete(:pidunless instance_running? instance  
  11.   end  
  12.   
  13.   send_exited_notification(instance)  
  14.   
  15.   instance[:notified] = true  
  16. end  
       首先先在router中注销该应用实例的url,由于对于一个异常终止的应用实例来说,肯定不会有instance[:exit_reason]值,所以正如正常逻辑,应该将该应用实例的:exit_reason以及:state设置为:CRASHED。

        stop_droplet方法中执行完send_exit_message方法之后,最后会执行cleanup_droplet方法。进入cleanup_droplet方法中,由于该应用实例的:state已经被设定为:CRASHED,所以该应用实例不会进入删除文件没有的命令中,而是执行chown命令,代码如下:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. def cleanup_droplet(instance)  
  2.   ……  
  3.   if instance[:state] != :CRASHED || instance[:flapping]  
  4.    ……  
  5.   else  
  6.     @logger.debug("#{instance[:name]}: Chowning crashed dir #{instance[:dir]}")  
  7.     EM.system("chown -R #{Process.euid}:#{Process.egid} #{instance[:dir]}")  
  8.   end  
  9. end  
        到目前为止,crashed应用的状态只是被标记为:CRASHED,而其文件目录还是存在于DEA的文件系统中,并没有删除。

        但是可以想象的是,对于一个崩溃的应用实例,没有将其删除的情况是不合理的,当时Cloud Foundry的设计者肯定会考虑这一点。实际情况中,DEA的执行时,会添加一个周期性任务crashes_reaper,实现代码如下:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. EM.add_periodic_timer(CRASHES_REAPER_INTERVAL) { crashes_reaper }  
        而CRASHES_REAPER_INTERNAL的数值设定为3600,也就是每隔一小时都是执行一次crashes_reaper操作,现在进入crashes_reaper方法的代码实现:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. def crashes_reaper  
  2.   @droplets.each_value do |instances|  
  3.     # delete all crashed instances that are older than an hour  
  4.     instances.delete_if do |_, instance|  
  5.       delete_instance = instance[:state] == :CRASHED && Time.now.to_i - instance[:state_timestamp] > CRASHES_REAPER_TIMEOUT  
  6.       if delete_instance  
  7.         @logger.debug("Crashes reaper deleted: #{instance[:instance_id]}")  
  8.         EM.system("rm -rf #{instance[:dir]}"unless @disable_dir_cleanup  
  9.       end  
  10.       delete_instance  
  11.     end  
  12.   end  
  13.   
  14.   @droplets.delete_if do |_, droplet|  
  15.     droplet.empty?  
  16.   end  
  17. end  
        该代码的实现很简单,也就是如果一个应用实例的状态为:CRASHED,那就删除该应用实例的文件目录。

        总结,当一个应用实例crash的时候,应用实例将不能被访问,而且其文件目录依然会存在与DEA所在节点的文件系统中 ,DEA会将应用实例的状态标记为:CRASHED,随后通过周期为1小时的任务crashes_reaper将其文件目录删除。

stop DEA

        stop DEA主要是指,Cloud Foundry的开发者用户通过Cloud Foundry中指定的脚本命令,停止DEA组件的运行。当开发者用户发起该请求时,DEA组件会捕获这个请求:
[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. ['TERM''INT''QUIT'].each { |s| trap(s) { shutdown() } }  
        捕获到这个请求时,DEA会执行shutdown方法,现在进入该方法的代码实现:
[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. def shutdown()  
  2.   @shutting_down = true  
  3.   @logger.info('Shutting down..')  
  4.   @droplets.each_pair do |id, instances|  
  5.     @logger.debug("Stopping app #{id}")  
  6.     instances.each_value do |instance|  
  7.       # skip any crashed instances  
  8.       instance[:exit_reason] = :DEA_SHUTDOWN unless instance[:state] == :CRASHED  
  9.       stop_droplet(instance)  
  10.     end  
  11.   end  
  12.   
  13.   # Allows messages to get out.  
  14.   EM.add_timer(0.25) do  
  15.     snapshot_app_state  
  16.     @file_viewer_server.stop!  
  17.     NATS.stop { EM.stop }  
  18.     @logger.info('Bye..')  
  19.     @pid_file.unlink()  
  20.   end  
  21. end  
        看以上代码可知,执行shutdown方法的时候,对于@droplets中的每一个应用的每一个非CRASHED状态的实例,将:exit_reason设置为:DEA_SHUTDOWN之后,随后执行stop_droplet方法以及cleanup_droplet方法,也就是说会将这些应用实例的文件目录全部删除。删除完之后,DEA会选择结束进程。当然关于这些进程信息的application.json文件中,也会删除那些正常运行的应用实例信息。

        总结:stop一个DEA的时候,会先停止所有正常应用实例的运行,随后这些正应用实例的文件目录会被删除。

start DEA

        start DEA主要是指,Cloud Foundry的开发者用户通过Cloud Foundry指定的脚本命令,启动DEA组件的运行。当开发者发起该请求时,DEA组件启动,重要的部分为agent对象的创建与运行,现在进入agent实例对象的运行代码,主要关注与应用实例文件目录的部分:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. # Recover existing application state.  
  2. recover_existing_droplets  
  3. delete_untracked_instance_dirs  

        可以看到的是首先进行recover_existing_droplets方法,代码实现如下:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. def recover_existing_droplets  
  2.   …………  
  3.   File.open(@app_state_file'r') { |f| recovered = Yajl::Parser.parse(f) }  
  4.   # Whip through and reconstruct droplet_ids and instance symbols correctly for droplets, state, etc..  
  5.   recovered.each_pair do |app_id, instances|  
  6.     @droplets[app_id.to_s] = instances  
  7.     instances.each_pair do |instance_id, instance|  
  8.       …………  
  9.     end  
  10.   end  
  11.   @recovered_droplets = true  
  12.   # Go ahead and do a monitoring pass here to detect app state  
  13.   monitor_apps(true)  
  14.   send_heartbeat  
  15.   schedule_snapshot  
  16. end  

        该方法主要根据@app_state_file文件中的信息,还原@droplets信息,随后执行monitor_apps,send_heartbeat以及schedule_snapshot方法。

        随后会执行delete_untracked_instance_dirs方法,主要是删除与@droplets不相符的应用实例文件目录。

        总结,如果之前DEA为正常退出的话,且正常退出前已经清除所有crashed应用实例的话,aplication_json文件中不会有任何信息,而存放应用文件目录的路径下不会有任何应用实例,因此该方法不会文件目录删除;如果DEA正常退出之前,还有crashed应用实例还没有删除的话,启动的时候该应用实例还是会存在,等待crashes_reaper操作将其删除;如果DEA崩溃退出时,存在应用实例文件目录的路径下与DEA崩溃前出现不一致,而application.json也与实际的应用实例不一致时,会将不匹配的应用实例的文件目录进行删除。

        实现如下:

[ruby]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. # Removes any instance dirs without a corresponding instance entry in @droplets  
  2. # NB: This is run once at startup, so not using EM.system to perform the rm is fine.  
  3. def delete_untracked_instance_dirs  
  4.   tracked_instance_dirs = Set.new  
  5.   for droplet_id, instances in @droplets  
  6.     for instance_id, instance in instances  
  7.       tracked_instance_dirs << instance[:dir]  
  8.     end  
  9.   end  
  10.   
  11.   all_instance_dirs = Set.new(Dir.glob(File.join(@apps_dir'*')))  
  12.   to_remove = all_instance_dirs - tracked_instance_dirs  
  13.   for dir in to_remove  
  14.     @logger.warn("Removing instance dir '#{dir}', doesn't correspond to any instance entry.")  
  15.     FileUtils.rm_rf(dir)  
  16.   end  
  17. end  

DEA crashes

        DEA crashes主要是指,DEA在运行过程崩溃,非正常终止,可以是用强制结束DEA进程来模拟DEA crashes。

        由于DEA进程退出后,并不会直接影响到应用实例的运行,所以应用的文件目录还是会存在的,应用还是可以访问。当重新正常启动DEA进程的时候,由于和start DEA操作完全一致。需要注意的是,假如重启的时候,之前运行的应用都正常运行的话,那么通过recover_existing_droplets方法可以做到监控所有应用实例,通过monitor_apps方法。随后又可以通过send_heartbeat以及schedule_snapshot方法,实现与外部组件的通信。假如DEA重启的时候,之前运行的应用实例有部分已经crashes掉了,那在monitor_apps方法的后续执行中会将其文件目录删除。


        以上便是我对Cloud Foundry中应用实例生命周期中文件目录的变化分析。



关于作者:

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

转载请注明出处。

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

我的邮箱:allen.sun@daocloud.io
新浪微博: @莲子弗如清
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值