以NATS爲主線的CloudFoundry原理

       本文將試圖以CloudFoundry中的消息組件NATS爲主要線索,以在CF中廣泛使用的併發和網絡編程框架EventMachine爲側重,來串聯整個CF主線功能的工作原理,力求能用簡單直接的方式描述出較多的架構細節和系統設計。


需要準備的知識:

EventMachine(EM)的基礎知識和使用方法,可以參考不久前的介紹Research on EventMachine

關於NATS源碼級別的介紹,可以參考我們之前的這篇文章:Research on NATS


一、以NATS爲線索部署CloudFoundry的更多細節


我們之前曾寫過一系列的基於dev_setup的安裝與部署文章:

Part 1、單節點安裝:

http://blog.csdn.net/resouer/article/details/7939952

Part 2、基於Iaas進行多節點部署:
http://blog.csdn.net/resouer/article/details/8010756


在上述文章的描述中,我們其實已經可以看到NATS在我們部署CF時所扮演的關鍵角色。沒錯,是否跟NATS溝通順暢,也是我們檢驗各個組件正常工作的重要標準之一。所以,我們在這裏着重解決兩個問題:

1、如何以模板爲基礎安裝CF集羣

2、如何爲這個集羣實現LB和Custom Domain?


回憶一下我們之前的工作步驟:

  1. 先按照Step A安裝單節點CF的VM
  2. 使用該VM做模板,克隆出所需數目的VM
  3. 用NATS把每一個安裝了完整CF的VM連接起來
  4. 進行一些其他配置
  5. 分別啓動所需的組件

好了,在上面文章的基礎上,我們這次提供一個更加清晰的部署策略:(後面的版本組件會不斷增加,但是這裏的思想是確定的)

Nginx Load Balancer:使用Nginx爲Router做負載均衡,綁定LB到*yourdomain.com

Router:作爲LB的server,3個節點

Cloud Controller:2個節點,共享文件系統和配置數據庫

Health Manager:與CC共用2節點

DEA:3個節點,數量根據應用不同而不同,一般根據資源需求動態添加

Service Gateway:1個節點,不支持集羣,一種服務需要一個Gateway

Service Node:2個節點,根據需求可動態添加,一般一種服務1~2個節點

NATS:輕量級不支持集羣,只能在單節點上

其他:服務工具類組件,打包組件,用戶控制組件各1-2節點(圖中未畫出,具體類似於service_lifecycle的各個節點)





接下我們需要到這些節點中做下面的簡單工作:

1、login到每個VM中,比如CloudController

2、找到./devbox/config/cloud_controller.yml中nats://nats:[email protected]:4222

3、修改該IP爲NATS的IP,

4、對其它的node做這項工作,然後啓動該節點上需要的那幾個組件即可(../vcap_dev start xxx xxx ...)


然後是一些額外的配置工作,包括:

1、配置CC的external_url,以及CC和HM的共享文件和數據庫(參見Part 2裏的說明以及 Step 5. Other things TODO部分)

2、多個service node的編號

3、單獨啓動nats節點上的nats-server服務

4、Custom Domain、Muti-router與Nginx LB的配置


需要重點補充下4 的操作。在Part 2 裏我們提到過:在你的IaaS層的網絡功能裏把*.yourdomain.com綁定到這個LB上。這樣所有對該URL的訪問會首先經過該LB(當然該LB也可以是個Nginx集羣)。

而在前面的額外配置中,api.yourdomain.com已經分配給CC了。其實CloudFoundry之所以能解析api.yourdomain.com到你的CC,靠的是Router的路由功能,這在後面的原理部分會詳細說明。

所以,當你執行vmc targert api.yourdomain.com時,你的request實際上是這樣轉發的:

vmc target api.yourdomain.com -> LB -> LB選擇某一個Router -> Router選擇某一個CloudController



二、以NATS和EM爲主線的CloudFoundry源碼導讀


1、NATS



這裏我們關注的問題有兩個:
1、NATS客戶端的生命週期與組件的運行關係如何?
2、NATS是否負責處理CF中所有管理類消息的中轉?


首先強烈學習官方的README:https://github.com/derekcollison/nats

閱讀源碼的話,請重點關注server,connection,sublist這幾個部分,動手實驗的時候使用nats-server -c "your_config_file"來用自己新寫的配置文件啓動nats server。具體的參數表在這裏:

https://github.com/derekcollison/nats/blob/master/lib/nats/server/options.rb#L10


NATS作爲CF的神經網絡,負責者組件之間的通訊和交互工作:

NATS基於Topic
發佈者以Topic發送消息
訂閱者訂閱特定Topic並收到
這種策略下,發佈者與訂閱者不需要相互知道,只要按照訂閱的主題進行發佈,訂閱者就能收到消息。



每個CF組件的啓動,很多都需要啓動EventMachine和NAST,並在NATS啓動過程中做下面幾個事情

  EM.epoll # EM默認使用select系統調用,所以這裏往往使用處理能力更高的epoll調用

  EM.run do 
    ...
    NATS.start(:uri => @config['mbus']) do
      configure_timers # 設置基於EM的定時器

      register_as_component # 向VCAP::Component註冊本組件的信息以便監控本組件信息
      
      subscribe_to_messages # 設定訂閱列表
    end
    ...
  end


其實,並不是所有的消息傳遞都是有NATS來做的,NATS在CF中起作用的場景應該是這樣描述:

Publisher並不知道也沒有必要關心Subscriber的存在和數量,同樣後者對前者的存在也無需關心,更重要的是Pub和Sub的工作機制應該是基於“事件“和”響應“的。

所以,對於有一些需要在知曉對方信息的基礎上建立通信的場合,CloudFoundry中會採用HttpSever的方式來響應request,比如用戶經由Router訪問到應用instance,以及Service Gateway與CloudController之間的關係。下面的圖示向您展示了這種不同的信息傳遞方式在CF中的使用場合:


在上圖中我們可以清晰地看到,只有藍色線畫出的場景(當然圖中給出的只是比較典型的幾個場景)纔是NATS的主要用武之地。不過,NATS以及EM爲我們提供的並不只只是消息的傳遞,而是基於消息和事件驅動的編程方式以及鬆耦合和自治式的組件結構。

NATS的通信機制基於EM所提供的TCP連接功能。每次會話起始於NATS客戶端發起請求與服務器端建立連接,然後NATS服務器端回覆一條自己的INFO信息作爲響應,這樣簡單的過程之後NATS就已經可以工作了。

NATS的消息協議非常簡單:所有的消息都由一個操作指令開頭,然後各個參數以空格分開跟在操作指令之後。比如,NATS發佈消息的一條完整指令爲:PUB <TOPIC> <REPLY_TO><MSG_SIZE>,當服務器端收到這條指令之後它會轉到“等待數據”的狀態,並等待客戶發出一條包含消息內容的指令:PUB <PAYLOAD>,然後服務器端收到客戶端發來的消息內容:payload。這樣publish的工作就完成了。同理,NATS訂閱消息的過程也是類似的。我們在這裏給出一次訂閱和發佈交互中TCP數據流的順序圖:


我們可以看到這次sub-pub的交互過程如下:

1.   雙方的連接成功建立之後(CONNECT操作成功得到響應之後),客戶端首先訂閱了主題爲foo的消息,SID爲1。

2.   服務器端會記錄下這主題和SID並響應+OK。

3.   客戶端發佈了一個主題爲foo的消息,長度爲12,然後緊接着發來了消息數據“Hello World!”。

4.   服務器端通過主題匹配找到該主題訂閱者的SID是1,於是服務器端把這個消息的主題foo,SID值1,還有消息本身攜帶的數據“Hello World!”一起返回給客戶端。

客戶端根據SID =1從自己維護的訂閱者列表裏找到對應的訂閱者,然後把服務器端返回來的數據交給訂閱者去使用,一次對PUB操作的響應也就完成了。
NATS服務器端負責進行主題匹配的數據結構被稱作Sublist,關於這部分數據結構的存儲可以參考前面有關NATS原理的文章。

2、Router

Router作爲CF的請求訪問分配與轉發門戶,主要承擔着以下四種任務:

  • 處理所有來訪的HTTP流量
  • 將對URL的訪問路由至具體的實例或CF組件
  • 應用實例之間分發流量實現均衡負載
  • DEAs獲得信息並實時更新路由表

我們這裏重點關注的問題是:

1、Router究竟是如何實現了某個域名與IP的綁定功能?

2、Router選擇instance的策略是怎樣的?

                                                                                              


 上圖展示了Router的工作流程,它的原理其實很容易描述:

  1. 組件和應用實例均被註冊到某個ULR
  2. Nginx通過lua腳本把lookup請求發送給一個由ruby代碼建立的http server
  3. Server根據URL查詢註冊信息,選擇某一具體的ip:port,轉發請求
  4. Session Sticky:將被轉發給上次訪問的應用實例

所以這裏Router訂閱的消息無外乎兩種:register和unregister

    def setup_listeners
      NATS.subscribe('router.register') { |msg|
        msg_hash = Yajl::Parser.parse(msg, :symbolize_keys => true)
        return unless uris = msg_hash[:uris]
        uris.each { |uri| register_droplet(uri, msg_hash[:host], msg_hash[:port],
                                           msg_hash[:tags], msg_hash[:app]) }
      }
      NATS.subscribe('router.unregister') { |msg|
        msg_hash = Yajl::Parser.parse(msg, :symbolize_keys => true)
        return unless uris = msg_hash[:uris]
        uris.each { |uri| unregister_droplet(uri, msg_hash[:host], msg_hash[:port]) }
      }
    end

而它們對應的回調函數在這裏:https://github.com/cloudfoundry/router/blob/master/lib/router/router.rb#L171 代碼段中可以看到:

log.info "Registering #{url} at#{host}:#{port}"

log.info"#{droplets.size} servers available for#{url}"

這就是我們很熟悉的Router啓動時打印出的一系列的register log

Router中app的信息是需要不斷輪詢着的,所以會有一個check_registered_urls定時被執行。值得注意的是,在此期間會有這樣的判斷:


 to_drop << droplet if ((start - droplet[:timestamp]) > MAX_AGE_STALE)


然後符合該條件的instance會被unregister掉。這裏的“陳舊instance“是通過時間戳來判斷的,默認2min內沒有被更新時間戳的instance會被拋棄,而負責更新instance時間戳的工作由DEA負責。

在Router的這個部分中有一處EM與NATS的非常典型的用法:

   def setup_sweepers
      @rps_timestamp = Time.now
      @current_num_requests = 0
      EM.add_periodic_timer(RPS_SWEEPER) { calc_rps }
      EM.add_periodic_timer(CHECK_SWEEPER) {
        check_registered_urls
      }
      if @enable_nonprod_apps
        EM.add_periodic_timer(@flush_apps_interval) do
          flush_active_apps
        end
      end
    end

而在flush_active_apps這個方法中,我們看到了EM的一種使用模式:先使用defer將任務放到線程池中進行處理,但是在執行期間,又需要在主線程中通過NATS發佈消息,於是使用到了next_tick回到Reactor週期中來執行:

def flush_active_apps
      ... ...

      EM.defer do
        msg = Yajl::Encoder.encode(@flushing_apps.to_a)
        zmsg = Zlib::Deflate.deflate(msg)

        log.info("Flushing active apps, app size: #{@flushing_apps.size}, msg size: #{zmsg.size}")
        EM.next_tick { NATS.publish('router.active_apps', zmsg) }

        @flushing = false
      end



Router的另一個核心部分就是router/ lib/router/router_uls_server.rb,這個文件爲Router建立起了負責處理來訪URL的一個基於sinatra的HTTPserver (圖中的upstream locator svc)。

見:https://github.com/cloudfoundry/router/blob/master/lib/router/router_uls_server.rb#L10

這部分的可讀性非常強:

Router首先解析該request body

# Parse request body
    uls_req = JSON.parse(body, :symbolize_keys => true)
    raise ParserError if uls_req.nil? || !uls_req.is_a?(Hash)
    stats, url = uls_req[ULS_STATS_UPDATE], uls_req[ULS_HOST_QUERY]
    sticky = uls_req[ULS_STICKY_SESSION]

如果訪問類型是URL的話,直接在這個server上查詢URL的註冊信息:

      # Lookup a droplet
      unless droplets = Router.lookup_droplet(url)
        Router.log.debug "No droplet registered for #{url}"
        raise Sinatra::NotFound
      end


然後做判斷:如果來訪的request是帶session的,那麼直接路由到上一次訪問的instance中:

droplet = check_original_droplet(droplets, host, port)

否則的話,從剛剛lookup到的droplet中隨機選擇一個。最後組裝一個response以便client端獲取正確的響應:


    uls_response = {
        ULS_STICKY_SESSION => new_sticky,
        ULS_BACKEND_ADDR => "#{droplet[:host]}:#{droplet[:port]}",
        ULS_REQUEST_TAGS => uls_req_tags,
        ULS_ROUTER_IP => Router.inet,
        ULS_APP_ID => droplet[:app] || 0,
      }

這樣,你的request 就被轉發到目的地了。

需要注意的是:對於Router而言,組件和instance都是一樣的,所以在register時,CloudController,uaa,service_broker等組件都會被註冊到Router中。比如api.vcap.me其實就是註冊到了CC的ip:port上。這樣,你的管理類型指令也是由Router進行轉發的。

最後有一些問題要說明,這種結構下Router本身需要啓動一個HTTPserver :client -> [ nginx -> lua ->http server ] -> CF。這其實是兩次轉發,更好的設計是不必再次經過一次server就能夠被路由出去。


3、CloudController

  • 用戶控制
  • 與stager模塊一起對應用進行打包上傳和預處理
  • 應用和服務的生命週期管理
  • 應用運行資源管理
  • 通過RESTfulAPI來進行交互

CC就是api.vcap.me對應的節點,是整個集羣的管理中樞。CC是一個典型的ROR項目,所以熟悉下ROR的目錄結構對於這部分的研究是很有幫助的,這裏是篇極好的guide: http://ihower.tw/rails3/firststep.html

在../config/routes.rb裏定義了客戶端(比如vmc)與CF進行交互的API。熟悉ROR的話,我們就可以在/app目錄下很快的定位到對於的controller。

與應有有關具體的業務邏輯由app_manager類負責,這一部分也是最值得鑽研的部分,比如啓動應用的start_instance(message, index),尋找DEA的find_dea_for(message)方法等等。而從NATS的角度來看,CC 功能可以這麼描述:

根據用戶發來的指令,組裝所需的信息(MSG),然後使用NATS.publish廣播出去,這樣訂閱了對應主題的組件就能夠按照指令的意圖完成後續操作。這個例子在一開始NATS的部分就已經提到了。由於在接下來我們會不斷涉及到CC的實際工作,所以這裏不做單獨分析。


4、Stager


CC的一個重要作用就是與Stager合作,製作droplets,並它們部署到合適的DEA運行起來。Stager就是用來接管制作droplets的組件。這裏我們關注問題是:

1、打包的過程具體是在所什麼?

2、CF到底如何爲我們的APP提供運行容器呢?


應用之所以能夠運行起來,以java web爲例子,用戶端上傳的只是可執行文件以及外部依賴(war包)而已,而這些可執行文件需要放在容器中才能運行的。所以在staging的過程中,很重要的一點就是製作一個bits+server組成的“可運行起來的Droplet”。CloudFoundry提供的embeded server 是Tomcat。

Stager其實只是一個入口,它通過圖示的方式調用staging plugin來執行打包操作,這樣的設計方便開發者對CF進行擴展以支持其他的runtime,所以staging plugin也被單獨抽象成了gem包。

java_web的plugin主要做兩個事情:

1、將war包解壓出來後放到Tomcat的ROOT目錄下,這樣將來直接執行./bin/catalina.sh run就能運行起來這個server並使應用能被訪問到

2、配置Tomcat的catalina_opts,使得該Tomcat能夠使用到DEA中的runtime。

另外,如果我們看下這個Tomcat的配置文件模板:https://github.com/cloudfoundry/vcap-staging/blob/master/lib/vcap/staging/plugin/java_web/resources/generate_server_xml,我們會發現shutdown的端口被設爲dsable:

<Server port="-1">

這是因爲CF中的應用訪問端口是分配出來的,指定shutdown端口反而會kill掉正常的CF進程。


關於CF中server到底要做哪些修改才能正常運行,推薦閱讀 http://cnblog.cloudfoundry.com/?p=382

這篇文章使用standalone方式支持Tomcat7就是模擬staging的工作,把tomcat7+app作爲一個整體部署到CF上運行起來的實例。所以在CF中如何支持jetty,weblogic等容器的方法,相信大家也略知一二了。


5、DEA

NOTE:我們這裏的DEA略有過時,這個版本還沒有warden,不過新版的stable的dea_ng應該已經在github上了。另外,DEA中使用fiber(ruby的一種非搶佔式多線程模型)來處理比如下載打包之類的耗時操作,這與其他組件有所不同。

這裏我們關注的問題包括:

1、應用到底是如何啓動的?

2、執行push的時候,CF如何從幾個DEA之間做出選擇?

3、DEA怎樣獲得droplets文件來運行?

4、應用的監控是怎樣的?


大家都知道DEA是應用運行的主場,也是整個PaaS中與應用關係最密切的部分,所以我們不妨先通過一個場景來描述其工作方式:


  1. 當我們啓動一app instance時候DEA節點會從指定位置下載一個Droplet的副本啓動起來
  2. 如果我們擴展該app10instances,那這個Droplet就被會複製十
  3. CF通過NATS來“發現”DEADEA根據自己的“能力”來立即或推遲響應請求,instance會被下載到最先響應的DEA上啓動
  4. 啓動後的instance會被分配PID和響應端口,它會將自己的IP+Port信息註冊到Router中對應的URL
  5. DEA負責把應用實例的運行狀態定時報告給HealthManager

整個過程如下圖所示:


前面已經提到過,DEA start的時候,在../lib/dea/agent.rb除了初始化各個變量外,還會訂閱一系列的消息,最後在向其他組件廣播自己啓動的消息:

        # Setup our listeners..
        NATS.subscribe('dea.status') { |msg, reply| process_dea_status(msg, reply) }
        NATS.subscribe('droplet.status') { |msg, reply| process_droplet_status(msg, reply) }
        NATS.subscribe('dea.discover') { |msg, reply| process_dea_discover(msg, reply) }
        NATS.subscribe('dea.find.droplet') { |msg, reply| process_dea_find_droplet(msg, reply) }
        NATS.subscribe('dea.update') { |msg| process_dea_update(msg) }
        NATS.subscribe('dea.stop') { |msg| process_dea_stop(msg) }
        NATS.subscribe("dea.#{uuid}.start") { |msg| process_dea_start(msg) }
        NATS.subscribe('router.start') {  |msg| process_router_start(msg) }
        NATS.subscribe('healthmanager.start') { |msg| process_healthmanager_start(msg) }
        NATS.subscribe('dea.locate') { |msg|  process_dea_locate(msg) }

        # Recover existing application state.
        recover_existing_droplets
        delete_untracked_instance_dirs

        EM.add_periodic_timer(@heartbeat_interval) { send_heartbeat }
        EM.add_periodic_timer(@advertise_interval) { send_advertise }
        EM.add_timer(MONITOR_INTERVAL) { monitor_apps }
        EM.add_periodic_timer(CRASHES_REAPER_INTERVAL) { crashes_reaper }
        EM.add_periodic_timer(VARZ_UPDATE_INTERVAL) { snapshot_varz }
        EM.add_periodic_timer(DROPLET_FS_PERCENT_USED_UPDATE_INTERVAL) { update_droplet_fs_usage }

        NATS.publish('dea.start', @hello_message_json)
        send_advertise

這裏很多的訂閱是帶有reply的,這意味着回調方法執行結束後需要使用 NATS.publish(reply, response.to_json) 來返回處理結果。
所以,我們在這一部分按照NATS爲主線進行研究是再適合不過的了。

send_heartbeat方法是DEA向HM發送心跳的部分,這個heartbeat是HM監視DEA中instance狀態的重要部分,至於HM收到這個心跳之後做什麼我們在HM的部分說。


大多數方法都能直接從名字和邏輯中判斷個差不多,這裏我們單獨看幾個有意思的地方:

1、process_dea_discover(message, reply)

     ......
        # Pull resource limits and make sure we can accomodate
        limits = message_json['limits']
        mem_needed = limits['mem']
        droplet_id = message_json['droplet'].to_i
        if (@reserved_mem + mem_needed > @max_memory)
          @logger.debug('Ignoring request, not enough resources.')
          return
        end
        delay = calculate_help_taint(droplet_id)
        delay = ([delay, TAINT_MAX_DELAY].min)/1000.0
        EM.add_timer(delay) { NATS.publish(reply, @hello_message_json) }
    ... ...

我們可以看到DEA是如何響應“發現DEA”的:在前面check過空間,runtime等支持後,DEA首先判斷DEA的內存是否足夠,然後調用calculate_help_taint來計算一個延遲,最後使用根據這個延遲時間來做出響應。而這個計算延遲的部分就更清晰了:

  def calculate_help_taint(droplet_id)
      # Calculate taint based on droplet already running here, then memory and cpu usage, etc.
      taint_ms = 0
      already_running = @droplets[droplet_id]
      taint_ms += (already_running.size * TAINT_MS_PER_APP) if already_running
      mem_percent = @reserved_mem / @max_memory.to_f
      taint_ms += (mem_percent * TAINT_MS_FOR_MEM)
      # TODO, add in CPU as a component..
      taint_ms
    end

計算延遲考慮了兩個因素:

1、該DEA上對應droplet已經啓動的instance數量 2、該DEA上的資源使用情況。然後兩者求和作爲延時值。

這裏還沒有warden,所以這個mem百分比可能會出現超出limit的情況。


由於訂閱了xxx.start這樣的消息Router啓動後DEA向Router註冊自己持有的instance,給HM發送heartbeat的過程也是類似的:

    def process_router_start(message)
      return if @shutting_down
      @logger.debug("DEA received router start message: #{message}")
      @droplets.each_value do |instances|
        instances.each_value do |instance|
          register_instance_with_router(instance) if instance[:state] == :RUNNING
        end
      end
    end


在register_instance_with_router,DEA把instance的信息封裝成msg_json,然後NATS.publish('router.register', msg),由Router負責處理。


2、從CC處下載droplet並在DEA建立本地可執行目錄的方法:def stage_app_dir(bits_file, bits_uri, sha1, tgz_file, instance_dir, runtime)

在這個方法的一開始就對這個過程的思路做了說明:

      ... ...
      # See if we have bits first..
      # What we do here, in order of preference..
      # 1. Check our own staged directory.
      # 2. Check shared directory from CloudController that could be mounted (bits_file)
      # 3. Pull from http if needed.
      ... ... 

DEA首先判斷本地是否已經有了應用的可執行目錄和所需的文件,如果已經存在,直接使用就好了

    if File.exist?(tgz_file)
        @logger.debug('Found staged bits in local cache.')


如果不存在,首先判斷DEA與CC之間建立了共享文件系統的話,我們直接使用文件操作從CC的/var/vcap/shared/下把這些文件cp過來(需支持FUSE)


    else
        # If we have a shared volume from the CloudController we can see the bits
        # directly, just link into our staged version.
        if File.exist?(bits_file) and not @force_http_sharing
          @logger.debug("Sharing cloud controller's staging directories")
          start = Time.now
          FileUtils.cp(bits_file, tgz_file)
          @logger.debug("Took #{Time.now - start} to copy from shared directory")

DEA和CC這部分文件共享在通常情況下是需要手動配置的。簡單地說,就是建立一個NFS server和共享目錄,然後把CC和DEA都mount這個目錄。當然,我們可以使用其他支持FUSE的文件系統來實現HP/HA,畢竟這部分用戶應用的存儲是十分重要的。

如果共享文件系統沒有建立,那我們還有最後一種方式:直接通過HTTP方式下載droplet。

download_app_bits(bits_uri, sha1, tgz_file)

這個方法會通過EM向下載URL發送HttpRequest,並以流的方式把文件解壓寫入到DEA本地的目錄中。


droplet下載並解壓後,刪除原來的壓縮文件,然後還要綁定runtime才能運行:

    def bind_local_runtime(instance_dir, runtime_name)
      ... ...

      startup_contents = File.read(startup)
      new_startup = startup_contents.gsub!('%VCAP_LOCAL_RUNTIME%', runtime['executable'])
      return unless new_startup

      FileUtils.chmod(0600, startup)
      File.open(startup, 'w') { |f| f.write(new_startup) }
      FileUtils.chmod(0500, startup)
    end

上面方法會將VCAP_LOCAL_RUNTIME變量被替換成當前DEA runtime的可執行文件路徑(比如這種:../cloudfoundry/.deployments/devbox/deploy/rubies/ruby-1.9.2-p180/bin/ruby)。從某種意義上來說,應用之間共享runtime在CF中是不可避免的;但從另一方面講,這種對運行環境輕量級的封裝不需要用戶調用特定的API或導入外部依賴,其實是最大的優點。


上述stage_app_dir的執行過程實際上交給一個fiber(協程)完成的。當stage_app_dir方法發現使用bits_uri下載droplet的工作是在進行中的(說明有其它DEA在download同一個droplet),它會通過Fiber.yield就可以掛起當前的下載直到被resume。同樣,在stage_app_dir裏負責下載方法download_app_bits中也是如此:

      ... ...

      f = Fiber.current
      @downloads_pending[sha1] = []
      http = EventMachine::HttpRequest.new(bits_uri).get

      ... ...
      
      http.callback {
        file.close
        FileUtils.mv(pending_tgz_file, tgz_file)
        f.resume
      }
      Fiber.yield

      ... ..
當下載的請求發出後,先掛起當前調用自己的Fiber,當請求獲得響應後在回調方法中完成剩餘的文件操作並resume這個Fiber。


3、最後一個提到的方法是monitor_apps,儘管沒有warden的情況下資源監控的作用並不大,但鑑於這一點是我們必然會涉及的部分,還是稍作說明。

實際上負責蒐集instance資源信息的是這個方法:monitor_apps_helper,而每個instance對應的進程資源則直接使用`ps axo pid=,ppid=,pcpu=,rss=,user=`來獲得。

          metrics.each do |key, value|
              metric = value[instance[key]] ||= {:used_memory => 0, :reserved_memory => 0,
                                                 :used_disk => 0, :used_cpu => 0}
              metric[:used_memory] += mem
              metric[:reserved_memory] += instance[:mem_quota] / 1024
              metric[:used_disk] += disk
              metric[:used_cpu] += cpu
            end
         ... ...
         VCAP::Component.varz[:running_apps] = running_apps
         VCAP::Component.varz[:frameworks] = metrics[:framework]
         VCAP::Component.varz[:runtimes] = metrics[:runtime]

而資源的使用會被保存到metric這個數據結構中,最後所有的監控信息都被註冊到VCAP::Component.varz下。至於如何在客戶端訪問這個../varz變量,cherry_sun之前已經有文章做出了說明。
DEA這一部分的解讀其實略過了一個很重要的內容:droplet和instance的狀態轉化——這對監控來說是一個非常重要的部分,今後補上。


6、HealthManager

HM的功能和作用比較單一,而我們繼續以NATS作爲線索可以看到HM訂閱的消息如下:

    NATS.subscribe('dea.heartbeat') do |message|
      @logger.debug("heartbeat: #{message}")
      process_heartbeat_message(message) # 處理DEA發來的心跳
    end

    NATS.subscribe('droplet.exited') do |message|
      @logger.debug("droplet.exited: #{message}")
      process_exited_message(message) # 處理DEA關閉instance後的消息
    end

    NATS.subscribe('droplet.updated') do |message|
      @logger.debug("droplet.updated: #{message}")
      process_updated_message(message) # 處理更新DEA instance的消息
    end

    NATS.subscribe('healthmanager.status') do |message, reply|
      @logger.debug("healthmanager.status: #{message}")
      process_status_message(message, reply) # 處理查詢HM status的消息
    end

    NATS.subscribe('healthmanager.health') do |message, reply|
      @logger.debug("healthmanager.health: #{message}")
      process_health_message(message, reply) # 處理查詢HM health的消息
    end


另外HM會定時執行analyze_all_apps來分析應用和instance的狀態,該方法中使用了EM.next_tick來更有效率地執行這個分析過程,防止主進程阻塞在這裏(參見前面EM掃盲的EM#next_tick部分)。在分析完成之後,應用的信息和狀態會被註冊到VCAP::Component.varz中。

在分析APP的方法中,HM需要關注的是droplet的狀態和instance的狀態。

如果發現instance的狀態爲down,而對應droplet的狀態確是started,那HM會認爲此instance需要restart,這時該instance的id會被記錄到missing_indices中,然後HM調用start_instances(app_id, missing_indices)來啓動對應droplet的一個instance。

當然,啓動instance的工作是由CC來做的,所以HM只需要組裝好start_msg,然後使用NATS來publish一個專門的消息:

@logger.info("Requesting the start of missing instances: #{start_message}")
 NATS.publish('cloudcontrollers.hm.requests', start_message.to_json)

這樣,訂閱了改主題消息的CC就會根據傳來的msg啓動一個新的instance。


7、Service

NOTE:Service部分代碼更新很多,這裏不能全照顧到。Service部分作爲CloudFoundry裏一個具有相當規模且較爲獨立的組成部分,我們Lab會有單獨的篇幅來專門講述。所以我們這裏儘量High-level一些

這裏我們的問題有:

1、我們爲什麼能訪問到CF幫我們建立的數據庫等服務?

2、CF建立數據庫等服務的機制是怎樣的?

3、Gateway與Node的關係是什麼樣的?


Service部分在CF中涵蓋的種類非常多,所以CF把Gateway和Node中的公共代碼抽象成了一個gem,即vcap-service-base,然後各種service自己通過重寫指定的方法來實現具體的細節。這樣,不同種類的service可以有統一的接口來遵循,使得諸如添加自定義service這樣的工作纔有章可循。

在這一部分,Service Gateway - CC - Service Node這條線上NATS實際上並不是信息傳遞的最主要方式。下圖說明了Service部分組件間的聯繫:



我們只提一些重要的細節:

在ServiceGateway啓動之後,首先應該讓CloudController知曉自己的存在,所以在asynchronous_service_gateway.rb中需要向CC發送heartbeat

心跳的作用是向CC發送一個註冊請求,實際上是一個create(POST)請求,而這個請求的目的URL是:

@offering_uri = "#{@cld_ctrl_uri}/services/v1/offerings"
cld_ctrl_url就是CC的URL,即我們熟知的api.vcap.me。最後,gateway會查看CC的響應是不是200。

對照CC的routers.rb文件,我們可以知道在接收到上述請求後CC的工作實際上是向數據庫中插入一條(如果沒有的話)這個gateway的信息,這樣註冊就生效了。

好了,剩餘的CC與Service Gateway的交互工作也都是通過這條handler途徑來進行的,您可以參考我們的這篇文章深入學習:Cloud Foundry Service Gateway源碼分析


現在回到我們基於NATS的gateway與service node的交互過程上來。

以NATS爲主線,我們首先看一下Service Node的公共部分.../vcap-service-base/lib/base/node.rb的訂閱,非常簡單:

   %w[provision unprovision bind unbind restore disable_instance
      enable_instance import_instance update_instance cleanupnfs_instance purge_orphan
    ].each do |op|
      eval %[@node_nats.subscribe("#{service_name}.#{op}.#{@node_id}") { |msg, reply| EM.defer{ on_#{op}(msg, reply) } }]
    end
    %w[discover check_orphan].each do |op|
      eval %[@node_nats.subscribe("#{service_name}.#{op}") { |msg, reply| EM.defer{ on_#{op}(msg, reply) } }]
    end

第一個訂閱需要node_id參數,訂閱主題是在本node上進行的操作。而第二個訂閱則只針對discover操作和check_orphan操作,這兩個操作都是針對所有node的,所以沒有id的區別。

按照老規矩,每個Service Node節點在啓動後,都要使用NATS向外publish自己的信息。以MySQL爲例,它需要發佈的信息包括自己的id,支持的版本,service plan,還有這個node的capacity等。當然在Service Gateway中一定訂閱了它需要的消息(文件位置:.../vcap-service-base/lib/base/provisioner.rb):

    %w[announce node_handles handles update_service_handle].each do |op|
      eval %[@node_nats.subscribe("#{service_name}.#{op}") { |msg, reply| on_#{op}(msg, reply) }]
    end

好了,現在provisioner的作用應該能瞭解了:如果說前面asynchronous_service_gateway.rb是Gateway與CC進行交互的部分,那麼Provisioner就是Gateway與Service Node交互的部分了。這裏的設計分層很清楚。

這樣通過announce操作,ServiceGateway就能記錄下所有Service Node的有用信息了。


說完了sending announcement,我們現在簡單回顧下create service操作到底是怎麼執行的:

  1. CC把一個provsion request交給Service Gateway
  2. Gateway調用provisioner#provision_service(req) 來執行整個provision操作

所以provisioner#provision_service(request, prov_handle=nil, &blk)方法就是創建service實例的核心部分:

首先,我們要從目前維護的nodes列表裏挑選best_node

然後在這個node上執行provsion操作,這裏纔是我們關注的重點,請看這個方法:

subscription = 
 @node_nats.request("#{service_name}.provision.#{best_node}", prov_req.encode) do |msg|
   ... ... 
end

在這一段方法中,prov_req 是我們剛剛新建出來的 ProvisionRequest對象,它被通過NATS#request方法交給best_node,而當這個node完成了provision操作之後,返回的reply就傳遞給request代碼段的msg參數進行解析,並在最後把成功provsion生成的credentials等一些列服務相關的信息會打印出來。

這一定要注意的是NATS的通信是異步操作,我們千萬不能先NATS#request然後在接下來的代碼裏再使用返回來的msg。所有的工作都應該在do ... end這一部分回調的代碼段裏執行完。

那麼Service Node究竟做了哪些操作完成了provision呢?

在公共的.../vcap-service-base/lib/base/node.rb中,我們會看到on_provision(msg, reply)方法主要的工作,其實就是負責在service node上生成出credencial信息出來,包括數據庫名,用戶名,密碼等,同時修改該node的capacity等信息,最後把這些信息都返回給gateway的provisioner。

當然具體的操作會根據數據庫不同而不同,這也是除了上面公共部分之外各個Service Node需要自己實現的provision方法,以MySQL爲例,在../vcap/services/mysql/lib/mysql_service.rb中,我們可以看到其中的provision方法:


1、生成上述的credencial信息

2、按照生成的database name在這個service node上創建數據庫

3、返回上述信息


前面的create-service操作爲我們創建了數據庫,同時也生成了user,password信息,而bind-service操作實際上就是在之前創建的數據庫中爲我們之前生成的用戶分配權限:

"GRANT ALL ON #{name}.* to #{user}@'%' IDENTIFIED BY '#{password}' WITH MAX_USER_CONNECTIONS #{@max_user_conns}"

這樣子持有這些credencial信息的應用實例就可以像普通的應用那樣訪問這個數據庫了。



三、總結


基於NATS和EventMachine的CloudFoundry原理分析就寫到這裏,文章的作用一方面是補充之前的CF部署細節,另一方面也是爲了作爲CF源碼導讀供Lab使用。由於CF代碼更新非常快,我們這篇文章的內容實際上已經過時很多了。我們會在接下來的時間裏有計劃地開展新的工作,包括CC_ng,DEA_ng,warden,HMv2的研究,以及基於BOSH的大規模部署等等。






發佈了27 篇原創文章 · 獲贊 11 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章