Cloud Foundry中collector組件的源碼分析

        在Cloud Foundry中有一個叫collector的組件,該組件的功能是通過消息總線發現在Cloud Foundry中註冊過的各個組件的信息,然後通過varz和healthz接口來查詢它們的信息併發送到指定的存儲位置。

        本文從collector的功能出發,主要講述以上兩個功能的源碼實現。

發現註冊組件

        在Cloud Foundry中,每個組件在啓動的時候後會以一個component的形式向Cloud Foundry註冊,同時也會作爲一個組件,向NATS發佈一些啓動信息。

        首先以DEA爲例,講述該組件register與向NATS publish信息的實現。首先看以下/dea/lib/dea/agent.rb中register的代碼:

        VCAP::Component.register(:type => 'DEA',
                           :host => @local_ip,
                           :index => @config['index'],
                           :config => @config,
                           :port => status_config['port'],
                           :user => status_config['user'],
                           :password => status_config['password'])

        這段代碼表示,DEA通過VCAP::Component對象中的register方法,實現註冊。以下進入vcap-common/lib/vcap/component.rb中的register方法:

      def register(opts)
        uuid = VCAP.secure_uuid
        ……
        auth = [opts[:user] || VCAP.secure_uuid, opts[:password] || VCAP.secure_uuid]
        @discover = {
          :type => type,
          ……
          :credentials => auth,
          :start => Time.now
        }
        ……
        @healthz = "ok\n".freeze
        start_http_server(host, port, auth, logger)
        nats.subscribe('vcap.component.discover') do |msg, reply|
          update_discover_uptime
          nats.publish(reply, @discover.to_json)
        end
        nats.publish('vcap.component.announce', @discover.to_json)
        @discover
      end

        可見,在實現register方法的時候,首先通過傳遞進來的opts參數,構建一個@discover實例變量,訂閱了一個vcap.component.discover的消息,又發佈了一個vcap.component.announce的消息。關於collector的發現註冊組件的功能中,直接關聯的register方法中發佈的主題,因爲collector通過訂閱這個主題的消息,將json化的@discover變量取到,然後做相應的處理。

    def send_data(data)
      @adapters.each do |adapter|
        begin
          adapter.send_data(data)
        rescue => e
          Config.logger.warn("collector.historian-adapter.sending-data-error", adapter: adapter.class.name, error: e, backtrace: e.backtrace)
        end
      end
    end

        之前已經涉及了collector組件訂閱消息的話題,現在進入collector/lib/collector.rb中,實現消息的訂閱還有其他的消息請求:

      @nats = NATS.connect(:uri => Config.nats_uri) do
        Config.logger.info("collector.nats.connected")
        # Send initially to discover what's already running
        @nats.subscribe(ANNOUNCE_SUBJECT) { |message| process_component_discovery(message) }
        @inbox = NATS.create_inbox
        @nats.subscribe(@inbox) { |message| process_component_discovery(message) }
        @nats.publish(DISCOVER_SUBJECT, "", @inbox)
        @nats.subscribe(COLLECTOR_PING) { |message| process_nats_ping(message.to_f) }
        setup_timers
      end
        當collector接收到由ANNOUNCE_SUBJECT主題發佈過來的message(也就是json化的@discover變量)後,將該內容傳遞後方法process_component_discovery。以下是process_component_discovery方法的代碼實現:

    def process_component_discovery(message)
      message = Yajl::Parser.parse(message)
      if message["index"]
        Config.logger.debug1("collector.component.discovered", type: message["type"], index: message["index"], host: message["host"])
        instances = (@components[message["type"]] ||= {})
        instances[message["host"].split(":").first] = {
          :host => message["host"],
          :index => message["index"],
          :credentials => message["credentials"],
          :timestamp => Time.now.to_i
        }
      end
    rescue => e
      Config.logger.warn("collector.component.discovery-failure", error: e.message, backtrace: e.backtrace)
    end
        在該方法中,首先對message對象進行解析,若新產生的message對象中index鍵,則繼續往下的操作:在@components對象中,視情況添加一個instance。如貼出的代碼,其中兩行標註爲紅色的代碼需要理解:首先如果@components[message["type"]]不爲空,則將@components[message["type"]]賦值給instances;若爲空的話,那就把@components[message["type"]]賦爲空,如果之前不存在message["type"]這個鍵的話,那就先創建一個這樣的鍵,然後再賦爲空,最後還是將@components[message["type"]]賦值給instances。在這裏需要注意的是,instances與@components[message["type"]]是的首地址是相同的,所以之後給instances添加鍵值對的時候也是向@components[message["type"]]中添加鍵值對。需要提一下的是:一個instances代表Cloud Foundry中同一種類型的組件,instances中的每一個instance代表該類型組件的一個實際節點,而且可以發現instance是通過IP來設立鍵的,因此可見,在Cloud Foundry中相同類型的組件是不能或者不建議共存在同一個節點上或者共享一張網卡的。

        一般情況下,當Cloud Foundry的組件啓動是發佈vcap.component.announce消息後,很快在@components中就會有相應的信息,這樣的話,也就是實現了“發現註冊組件”的功能。


獲取組件varz和healthz併發送

        在以上功能中,collector只是實現了發現組件,只包括組件的ip:port信息,index,crdentials等信息,並不帶有其他關於組件運行時產生的數據信息。而通過varz和healthz訪問那些註冊的組件,正好可以做到這些收集varz和healthz信息。

        實現的過程中,首先是使用添加週期性定時器:EM.add_periodic_timer(Config.varz_interval) { fetch_varz },在一定的週期內,執行fetch_varz方法,以下進入fetch_varz方法:

    def fetch_varz
      fetch(:varz) do |http, job, index|
        ……
      end
    end
        進入fetch_varz方法後,首先是調用fetch方法,參數爲:varz,可見通過fetch方法會返回三個值,並作爲之後的代碼塊的參數傳入。現在進入fetch方法:

    def fetch(type)
      @components.each do |job, instances|
        instances.each do |index, instance|
          next unless credentials_ok?(job, instance)
          host = instance[:host]
          uri = "http://#{host}/#{type}"
          http = EventMachine::HttpRequest.new(uri).get(
            :head => authorization_headers(instance))
         ……
          http.callback do
            begin
              yield http, job, instance[:index]
            rescue => e
              ……
            end
          end
        end
      end
    end
        在發現組件該模塊中,以及講解過@components變量的含義,該方法中首先遍歷@components中的每一類組件,再遍歷每一類組件中的每一個組件實例,對並該組件實例發起獲取varz的請求。實現請求的過程中,首先查閱instance的credentails是否具有,然後組件請求的uri,然後通過EventMachine發送http請求,當請求響應返回的時候,通過yield關鍵字,將http,job以及instance[:index]返回給fetch_varz方法的代碼塊,fetch_varz代碼塊的實現如下:

        varz = Yajl::Parser.parse(http.response)
        now = Time.now.to_i

        handler = Handler.handler(@historian, job)
        Config.logger.debug("collector.job.process", job: job, handler: handler)
        ctx = HandlerContext.new(index, now, varz)
        handler.do_process(ctx)
        首先對http.response進行解析,然後通過Handler類的handler方法創建一個handler對象,其中需要注意的是調用了一個@historian對象,在Collector對象的初始化中,有代碼:@historian = ::Collector::Historian.build。

        @historian對象的功能是創建了網絡連接,具體代碼實現在/collector/lib/collector/historian.rb中:

class Historian
    def self.build
      historian = new
      if Config.tsdb
        historian.add_adapter(Historian::Tsdb.new(Config.tsdb_host, Config.tsdb_port))
        Config.logger.info("collector.historian-adapter.added-opentsdb", host: Config.tsdb_host)
      end
      if Config.aws_cloud_watch
        historian.add_adapter(Historian::CloudWatch.new(Config.aws_access_key_id, Config.aws_secret_access_key))
        Config.logger.info("collector.historian-adapter.added-cloudwatch")
      end
      if Config.datadog
        historian.add_adapter(Historian::DataDog.new(Config.datadog_api_key, HTTParty))
        Config.logger.info("collector.historian-adapter.added-datadog")
      end
      historian
    end
    ……
end
        可以看到@Historian對象的創建是添加了三個網絡適配器,或者說是三條連接分別是Tsdb,aws_cloud_watch以及DataDog。當之後需要@historian對象發送數據的時候,也就是通過者三條連接,將數據發送出去,本文馬上將涉及這一塊。

        現在回到fetch_varz中,創建爲handler實例對象之後,還創建了一個ctx對象,最後通過handler的do_process方法處理了ctx對象,現在進入collector/lib/collector/handler.rb中,查看do_process方法:

    def do_process(context)
      varz = context.varz
      send_metric("mem_free_bytes", varz["mem_free_bytes"], context) if varz["mem_free_bytes"]
      ……
      varz.fetch("log_counts", {}).each do |level, count|
        next unless %w(fatal error warn).include?(level)
        send_metric("log_count", count, context, {"level" => level})
      end
      process(context)
    end
        首先解析出varz對象,並通過send_metric方法將數據發送出去,Handler的子類在執行process方法時,都會調用自己覆寫的process方法,以DEA爲例,collector/lib/collector/handlers/dea.rb中的process方法爲:

      def process(context)
        send_metric("can_stage", context.varz["can_stage"], context)
        ……
        state_counts(context).each do |state, count|
          send_metric("dea_registry_#{state.downcase}", count, context)
        end

        metrics = registry_usage(context)
        send_metric("dea_registry_mem_reserved", metrics[:mem], context)
        send_metric("dea_registry_disk_reserved", metrics[:disk], context)
      end
        可以看到其實在process方法也僅僅是將不同組件各自對應的屬性值,通過send_metirc方法發送出去。以下進入collector/lib/collector/handler.rb中的send_metric方法中:

    def send_metric(name, value, context, tags = {})
      tags.merge!(additional_tags(context))
      ……
      @historian.send_data({
                               key: name,
                               timestamp: context.now,
                               value: value,
                               tags: tags
                           })
    end
       在send_metric代碼中,最後通過@historian對象中的send_data實現數據的發送,也就是之前說到的,@historian向@adapter的三條連接中,發送相應的數據,在collector/lib/collector/handler.rb中:

    def send_data(data)
      @adapters.each do |adapter|
        begin
          adapter.send_data(data)
        rescue => e
          Config.logger.warn("collector.historian-adapter.sending-data-error", adapter: adapter.class.name, error: e, backtrace: e.backtrace)
        end
      end
    end

        以上講述從獲取varz到發送給TSDB等數據存儲模塊的流程,關於發送的是什麼信息,還沒有具體深入。這裏以router爲例,講述發送的信息的類型。源碼於collector/lib/collector/handlers/router.rb中:

      def process(context)
        varz = context.varz
        send_metric("router.total_requests", varz["requests"], context)
        send_metric("router.total_routes", varz["urls"], context)
        send_metric("router.ms_since_last_registry_update", varz["ms_since_last_registry_update"], context)
        send_metric("router.bad_requests", varz["bad_requests"], context)
        send_metric("router.bad_gateways", varz["bad_gateways"], context)
        return unless varz["tags"]
        varz["tags"].each do |key, values|
          values.each do |value, metrics|
            if key == "component" && value.start_with?("dea-")
              # dea_id looks like "dea-1", "dea-2", etc
              dea_id = value.split("-")[1]
              # These are app requests, not requests to the dea. So we change the component to "app".
              tags = {:component => "app", :dea_index => dea_id }
            else
              tags = {key => value}
            end
            send_metric("router.requests", metrics["requests"], context, tags)
            send_latency_metric("router.latency.1m", metrics["latency"], context, tags)
            ["2xx", "3xx", "4xx", "5xx", "xxx"].each do |status_code|
              send_metric("router.responses", metrics["responses_#{status_code}"], context, tags.merge("status" => status_code))
            end
          end
        end
      end
        可見在接收到router的varz信息之後,collector會從中取出過個鍵值,並進行發送,比如說router的“total_requests”,"total_routes","bad_requests","bad_gateways","router.latency.1m","response_2xxx"等。也正式collector通過分析varz並王TSDB發送這樣的信息,所以TSDB中可以存有這些信息,最終DashBoard可以通過獲取TSDB中的信息,並顯示給用戶,當然用戶可以看到router組件的請求數,請求延遲,請求的響應時間等,也就不足爲怪了。
        

        綜上代碼分析,可以得到框架圖如下:


        以上便是Cloud Foundry中collector組件的功能分析。


關於作者:

孫宏亮,DAOCLOUD軟件工程師。兩年來在雲計算方面主要研究PaaS領域的相關知識與技術。堅信輕量級虛擬化容器的技術,會給PaaS領域帶來深度影響,甚至決定未來PaaS技術的走向。


轉載請註明出處。

這篇文檔更多出於我本人的理解,肯定在一些地方存在不足和錯誤。希望本文能夠對接觸Cloud Foundry中collector組件的人有些幫助,如果你對這方面感興趣,並有更好的想法和建議,也請聯繫我。

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


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