kafka-log日誌程式碼解析

Unplug發表於2020-04-02

本文是針對kong-plugin-kafka-log的程式碼進行簡要解析,由於kong-plugin-kafka-log適配的kong的版本較老,我對原始碼做了更新,適配kong的2.0.x版本,參考(github.com/tzssangglas…)

handler.lua

  1. 本地變數
local mt_cache = { __mode = "k" }
local producers_cache = setmetatable({}, mt_cache)
複製程式碼

setmetatable背景解析:

元表(metatable)是 Lua 中獨有的概念,表現行為類似於操作符過載,比如我們可以過載 add,來計算兩個 Lua 陣列的並集;或者過載 tostring,來定義轉換為字串的函式。 而 Lua 提供了兩個處理元表的函式:

  • 第一個是setmetatable(table, metatable), 用於為一個 table 設定元表;
  • 第二個是getmetatable(table),用於獲取 table 的元表。

弱表(weak table),它是 Lua 中很獨特的一個概念,和垃圾回收相關。 當一個 table 的元表中存在 mode 欄位時,這個 table 就是弱表(weak table)了。

  • 如果 mode 的值是 k,那就意味著這個 table 的 鍵 是弱引用。
  • 如果 __mode 的值是 v,那就意味著這個 table 的 值 是弱引用。
  • 當然,你也可以設定為 kv,表明這個表的鍵和值都是弱引用。

這三者中的任意一種弱表,只要它的 鍵 或者 值 被回收了,那麼對應的整個鍵值 物件都會被回收。 綜上,這兩行程式碼的意思是:mt_cache過載了producers_cache的物件回收策略,key是弱引用,只要key被回收了,producers_cache中key和對應的value整個物件都會被回收。

  1. local function log函式
if premature then
    return
end
複製程式碼

這段程式碼是計時器的回撥。premature是個標記,指示計時器是否過早執行,僅在Nginx worker退出時才會發生(基本上說“我不執行此計時器,但由於關機/重灌而取消了它”)

kafka-log日誌程式碼解析
參考(github.com/Kong/kong/i…)

  1. local function log函式 原來的程式碼,本次改動主要是針對cache_key的
--- Computes a cache key for a given configuration.
local function cache_key(conf)
  -- here we rely on validation logic in schema that automatically assigns a unique id
  -- on every configuartion update
  return conf.uuid
end

--- Publishes a message to Kafka.
-- Must run in the context of `ngx.timer.at`.
local function log(premature, conf, message)
  if premature then
    return
  end

  local cache_key = cache_key(conf)
  if not cache_key then
    ngx.log(ngx.ERR, "[kafka-log] cannot log a given request because configuration has no uuid")
    return
  end

  local producer = producers_cache[cache_key]
  if not producer then
    kong.log.notice("creating a new Kafka Producer for cache key: ", cache_key)

    local err
    producer, err = producers.new(conf)
    if not producer then
      ngx.log(ngx.ERR, "[kafka-log] failed to create a Kafka Producer for a given configuration: ", err)
      return
    end

    producers_cache[cache_key] = producer
  end

  local ok, err = producer:send(conf.topic, nil, cjson_encode(message))
  if not ok then
    ngx.log(ngx.ERR, "[kafka-log] failed to send a message on topic ", conf.topic, ": ", err)
    return
  end
end
複製程式碼

kafka-log外掛原來的邏輯是:

  1. 新增/更新外掛時,隨機生成uuid,用uuid作為producers_cache的key,value是根據當前配置新建的producer
  2. 由於producers_cache是一個key為弱引用的表,因此每次更新外掛後,uuid更新,producers_cache中舊producer會被GC,然後用更新後的uuid作為key,根據更新後的配置新建producer作為value,放入producers_cache中;
  3. 這樣做的用處是把外掛的最新配置同步到producers_cache中,並進行日誌推送,保證了配置與producers_cache同步,避免了每次推送日誌都生成producer的開銷;

理順了這層邏輯之後,進行改造就比較好下手了,同樣應該在新增或者更新外掛的時機來更新uuid;kafka-log剛出來的時候,kong的版本還是0.1.x版本,那時候可能是可以手動配置外掛中未宣告的屬性(uuid)的,所以作者這樣寫:

--- (Re)assigns a unique id on every configuration update.
-- since `uuid` is not a part of the `fields`, clients won't be able to change it
local function regenerate_uuid(schema, plugin_t, dao, is_updating)
  plugin_t.uuid = utils.uuid()
  return true
end
複製程式碼

即uuid不屬於外掛配置中已宣告的屬性,所以不需要使用者關心,在self_check的時機去給外掛配置新增一個uuid屬性。這樣使用者無感知,但是每次更新的時候,執行self_check悄悄更新uuid屬性,完成producers_cache更新。 在kong的2.0.x版本中,self_check被刪除,但是有entity_check屬性,我修改如下:

entity_checks = {
        { custom_entity_check = {
            field_sources = { "config" },
            fn = function(entity)
                local config = entity.config
                 ……
                --更新配置的時候同時更新uuid屬性
                config.uuid = utils.uuid()
                return true
            end
        } },
    },
複製程式碼

剩下的程式碼比較好理解 producerproducers.new(conf)構建出來的物件 producers來自於外掛中另一個程式碼 local producers = require "kong.plugins.kafka-log.producers" 拿到快取中的producer,執行send函式,執行失敗則記錄本地日誌。

  1. function KafkaLogHandler:log
function KafkaLogHandler:log(conf, other)
    KafkaLogHandler.super.log(self)
    local message = basic_serializer.serialize(ngx)
    local ok, err = ngx.timer.at(0, log, conf, message)
    if not ok then
        ngx.log(ngx.ERR, "[kafka-log] failed to create timer: ", err)
    end
end
複製程式碼

這是kong提供的外掛執行宣告週期中的一個階段,log階段,在請求接收到來自upstream響應之後,返回給下游客戶端之前,這個階段執行。 其中,kong推送出去的日誌來源是 basic_serializer.serialize(ngx),即序列化後的當前請求在nginx中的上下文,後面會轉成json格式。 觸發local function log函式執行的程式碼是

ngx.timer.at(0, log, conf, message)
複製程式碼

ngx.timer.at背景解析:

在 OpenResty 中,我們有時候需要在後臺定期地執行某些任務,比如同步資料、清理日誌等。如果讓你來設計,你會怎麼做呢?最容易想到的方法,便是對外提供一個 API 介面,在介面中完成這些任務;然後用系統的 crontab 定時呼叫 curl,來訪問這個介面,進而曲線地實現這個需求。不過,這樣一來不僅會有割裂感,也會給運維帶來更高的複雜度。所以, OpenResty 提供了 ngx.timer 來解決這類需求。你可以把ngx.timer ,看作是 OpenResty 模擬的客戶端請求,用以觸發對應的回撥函式。其實,OpenResty 的定時任務可以分為下面兩種:

  • ngx.timer.at,用來執行一次性的定時任務;
  • ngx.time.every,用來執行固定週期的定時任務。 (以上引用來自溫銘)

為什麼要用ngx.timer.at來執行local function log函式呢? 因為 cosocket API 在 set_by_lua, log_by_lua, header_filter_by_lua* 和 body_filter_by_lua* 中是無法使用的。而在 init_by_lua*init_worker_by_lua* 中暫時也不能用。 而local function log函式就是log_by_lua*階段,無法直接使用cosocket API。而用ngx.timer.at(0, log, conf, message)的方式可以繞過這種限制。這種繞過方式也是OpenResty應用開發中類似case的主流方式。

producers.lua

  1. create_producer函式
--- Creates a new Kafka Producer.
local function create_producer(conf)
    ……
end
return { new = create_producer }
複製程式碼

先從最下面看 return { new = create_producer } 這行程式碼呼應了handler.lua中的 producers.new(conf),相當於呼叫producers程式碼中的create_producer()函式;

  1. create_producer函式
local function create_producer(conf)
  local broker_list = {}
  for idx, value in ipairs(conf.bootstrap_servers) do
    local server = types.bootstrap_server(value)
    if not server then
      return nil, "invalid bootstrap server value: " .. value
    end
    broker_list[idx] = server
  end
複製程式碼

先迴圈校驗外掛配置中的bootstrap_servers引數合法性,即kafka的broker的ip+port,這是kafka的推送訊息的埠。

  1. producer_config
local producer_config = {
    -- settings affecting all Kafka APIs (including Metadata API, Produce API, etc)
    socket_timeout = conf.timeout,
    keepalive_timeout = conf.keepalive,
    -- settings specific to Kafka Produce API
    required_acks = conf.producer_request_acks,
    request_timeout = conf.producer_request_timeout,
    batch_num = conf.producer_request_limits_messages_per_request,
    batch_size = conf.producer_request_limits_bytes_per_request,
    max_retry = conf.producer_request_retries_max_attempts,
    retry_backoff = conf.producer_request_retries_backoff_timeout,
    producer_type = conf.producer_async and "async" or "sync",
    flush_time = conf.producer_async_flush_timeout,
    max_buffering = conf.producer_async_buffering_limits_messages_in_memory,
  }
  local cluster_name = conf.uuid
  return kafka_producer:new(broker_list, producer_config, cluster_name)
複製程式碼

producer_config是一個table,在這個table中設定外掛配置裡面的各種tcp連線和kafka相關的配置。

local cluster_name = conf.uuid
return kafka_producer:new(broker_list, producer_config, cluster_name)
複製程式碼

配置cluster_name叢集名,這裡又使用了conf.uuid,後續可能需要優化,找到提取conf的唯一標識的方法,代替uuid。 呼叫lua-resty-kafka庫中的resty.kafka.producer物件,執行真正的推送訊息到kafka的底層方法。

schema.lua

local types = require "kong.plugins.kafka-log.types"
local utils = require "kong.tools.utils"
--- Validates value of `bootstrap_servers` field.
local function check_bootstrap_servers(values)
  if values and 0 < #values then
    for _, value in ipairs(values) do
      local server = types.bootstrap_server(value)
      if not server then
        return false, "invalid bootstrap server value: " .. value
      end
    end
    return true
  end
  return false, "bootstrap_servers is required"
end
--- (Re)assigns a unique id on every configuration update.
-- since `uuid` is not a part of the `fields`, clients won't be able to change it
local function regenerate_uuid(schema, plugin_t, dao, is_updating)
  plugin_t.uuid = utils.uuid()
  return true
end
return {
  fields = {
    bootstrap_servers = { type = "array", required = true, func = check_bootstrap_servers },
    topic = { type = "string", required = true },
    timeout = { type = "number", default = 10000 },
    keepalive = { type = "number", default = 60000 },
    producer_request_acks = { type = "number", default = 1, enum = { -1, 0, 1 } },
    producer_request_timeout = { type = "number", default = 2000 },
    producer_request_limits_messages_per_request = { type = "number", default = 200 },
    producer_request_limits_bytes_per_request = { type = "number", default = 1048576 },
    producer_request_retries_max_attempts = { type = "number", default = 10 },
    producer_request_retries_backoff_timeout = { type = "number", default = 100 },
    producer_async = { type = "boolean", default = true },
    producer_async_flush_timeout = { type = "number", default = 1000 },
    producer_async_buffering_limits_messages_in_memory = { type = "number", default = 50000 },
  },
  self_check = regenerate_uuid,
}
複製程式碼

這是外掛的配置頁面相關的程式碼,其中

local function regenerate_uuid(schema, plugin_t, dao, is_updating)
  plugin_t.uuid = utils.uuid()
  return true
end
複製程式碼

即是上面conf.uuid相關問題的程式碼,self_check屬性在kong的新版本中被刪除了,替換為entity_checks,因此將不會執行regenerate_uuid函式,所以解決conf.uuid的問題最好是在這裡入手。

types.lua

--- Parses `host:port` string into a `{host: ..., port: ...}` table.
function _M.bootstrap_server(string)
  local m = re_match(string, bootstrap_server_regex, "jo")
  if not m then
    return nil, "invalid bootstrap server value: " .. string
  end
  return { host = m[1], port = m[2] }
end
return _M
複製程式碼

這個就是前面producers.lua中呼叫的types.bootstrap_server(value),用於校驗引數配置的bootstrap_server是否是合法的ip+port屬性。

相關文章