本文是針對kong-plugin-kafka-log的程式碼進行簡要解析,由於kong-plugin-kafka-log適配的kong的版本較老,我對原始碼做了更新,適配kong的2.0.x版本,參考(github.com/tzssangglas…)
handler.lua
- 本地變數
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整個物件都會被回收。
- local function log函式
if premature then
return
end
複製程式碼
這段程式碼是計時器的回撥。premature是個標記,指示計時器是否過早執行,僅在Nginx worker退出時才會發生(基本上說“我不執行此計時器,但由於關機/重灌而取消了它”)
參考(github.com/Kong/kong/i…)- 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外掛原來的邏輯是:
- 新增/更新外掛時,隨機生成uuid,用uuid作為
producers_cache
的key,value是根據當前配置新建的producer
; - 由於
producers_cache
是一個key為弱引用的表,因此每次更新外掛後,uuid更新,producers_cache
中舊producer
會被GC,然後用更新後的uuid作為key,根據更新後的配置新建producer
作為value,放入producers_cache
中; - 這樣做的用處是把外掛的最新配置同步到
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
} },
},
複製程式碼
剩下的程式碼比較好理解
producer
是producers.new(conf)
構建出來的物件
producers
來自於外掛中另一個程式碼
local producers = require "kong.plugins.kafka-log.producers"
拿到快取中的producer
,執行send函式,執行失敗則記錄本地日誌。
- 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
- 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()
函式;
- 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的推送訊息的埠。
- 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屬性。