Apache APISIX 擴充套件指南

語言: CN / TW / HK

Apache APISIX 提供了 50 多個外掛、常用的幾個負載均衡選擇器,以及對主流服務發現(如 Nacos 和 DNS)的支援。API 閘道器和企業內部的業務緊密相關,為了滿足企業的業務需求,使用者通常需要在 Apache APISIX 的基礎上新增一些程式碼,以實現業務所需的功能。如何拓展 Apache APISIX 成了許多使用者的共同痛點:在保證 Apache APISIX 平穩執行的前提下,如何新增業務程式碼,滿足實際需求?

本文提供了 Apache APISIX 的拓展指南,旨在為使用者提供拓展 Apache APISIX 的一些思路。由於 Apache APISIX 處於高速發展階段,版本迭代的頻率比較高,所以本文會以 Apache APISIX 的首個 LTS 版本 v2.10.0 為基礎進行說明。如果你使用的 Apache APISIX 版本低於 2.10.0,可能需要結合實際情況做一些變通。另外,雖然本文只講解了 HTTP 相關的邏輯,但是 TCP 相關的部分,大體上也較為相似。

擴充套件方向1:Rewrite 還是 Access?

我們從請求的生命週期說起:當一個請求進入到 Apache APISIX 時,首先會由 http_access_phase 這個方法進行處理。熟悉 OpenResty phases 概念的讀者可能會有些疑惑:OpenResty 一共有 6 個 phases,按照執行的先後順序排列,分別是: rewriteaccessbefore_proxyheader_filterbody_filterlog ,為什麼一上來就是 accessrewrite 怎麼不見了?

Apache APISIX 外掛的 phases 概念和 OpenResty 的 phases 概念略有不同。為了提升 Apache APISIX 的效能,APISIX 外掛的 rewrite 方法會在 OpenResty 的 access 階段中執行。使用者依然能夠在外掛層面自定義 rewrite 的邏輯,但是在程式碼層面, rewrite 實際上也是在 access 裡面執行。

雖然說 rewrite 的邏輯和 access 的邏輯都在 access phase 裡面執行,但是 rewrite 的邏輯還是會比 access 的邏輯先執行。為了避免後續外掛執行 rewrite 失敗後,沒有繼續執行 access ,導致 trace 遺漏的問題,必須在 rewrite 裡面新增 trace 的邏輯。

除了執行的先後順序外, rewriteaccess 之間還有一個差別,就是它們之間有一個處理 consumer 的邏輯:

plugin.run_plugin("rewrite", plugins, api_ctx)
if api_ctx.consumer then
...
end
plugin.run_plugin("access", plugins, api_ctx)

consumer 代表一種身份。 你可以針對不同的 consumer 進行許可權控制,比如使用 consumer-restriction 這個外掛實現基於角色的許可權控制,也就是大家所說的 RBAC。 另外,你也可以給不同的 consumer 設定對應的限流策略。

Apache APISIX 裡面的鑑權外掛(在外掛定義裡面有 type = "auth" ),需要在 rewrite 階段選好 consumer 。這裡我們用 key-auth 外掛舉個例子:

local _M = {
version = 0.1,
priority = 2500,
type = 'auth',
name = plugin_name,
schema = schema,
consumer_schema = consumer_schema,
}


...
function _M.rewrite(conf, ctx)
...
local consumer_conf = consumer_mod.plugin(plugin_name)
if not consumer_conf then
return 401, {message = "Missing related consumer"}
end


local consumers = lrucache("consumers_key", consumer_conf.conf_version,
create_consume_cache, consumer_conf)


local consumer = consumers[key]
if not consumer then
return 401, {message = "Invalid API key in request"}
end


consumer_mod.attach_consumer(ctx, consumer, consumer_conf)
end

鑑權外掛的執行邏輯都是相似的:首先從使用者輸入中獲取某組引數,然後根據引數查詢對應的 consumer ,最後連同該外掛對應的 consumer_conf 附加到 ctx 中。

綜上,對於無需在請求早期階段執行,且不需要查詢 consumer 的外掛,建議把邏輯寫到 access 裡面。

擴充套件方向2:配置服務發現

在執行完 access 之後,我們就要跟上游(Upstream)打交道了。通常情況下,上游節點是寫死在 Upstream 配置上的。不過也可以從服務發現上獲取節點來實現 discovery。

接下來我們會以 Nacos 為例,講講怎麼實現。

一個動態獲取 Nacos 管理的節點的 Upstream 配置如下:

{
"service_name": "APISIX-NACOS",
"type": "roundrobin",
"discovery_type": "nacos",
"discovery_args": {
"namespace_id": "test_ns",
"group_name": "test_group"
}
}

我們可以看到其中三個重要的變數:

  1. discovery_type : 服務發現的型別, "discovery_type": "nacos" 表示使用 Nacos 實現服務發現。

  2. service_name : 服務名稱。

  3. discovery_args : 不同的 discovery 特定的引數,Nacos 的特定引數包括: namespace_id 和   group_name

Nacos discovery 對應的 Lua 程式碼位於 discovery/nacos.lua 。開啟 nacos.lua 這個檔案,我們可以看到它裡面實現了幾個所需的方法。

一個 discovery 需要實現至少兩個方法: nodesinit_worker

function _M.nodes(service_name, discovery_args)
local namespace_id = discovery_args and
discovery_args.namespace_id or default_namespace_id
local group_name = discovery_args
and discovery_args.group_name or default_group_name


...
end




function _M.init_worker()
...
end

其中 nodes 的函式簽名已經明瞭地展示獲取新節點所用的查詢引數: service_namediscovery_args 。每次請求時,Apache APISIX 都會用這一組查詢最新的節點。該方法返回的是一個數組:

{
{host = "xxx", port = 12100, weight = 100, priority = 0, metadata = ...},
# priority 和 metadata 是可選的
...
}

init_worker 負責在後臺啟動一個 timer,確保本地的節點資料能跟服務發現的資料保持一致。

擴充套件方向3:配置負載均衡

獲取到一組節點後,我們要按照負載均衡的規則來決定接下來要先嚐試哪個節點。如果常用的幾種負載均衡演算法滿足不了需求,你也可以自己實現一個負載均衡。

讓我們以最少連線數負載均衡為例。對應的 Lua 程式碼位於 balancer/least_conn.lua 。開啟 least_conn.lua 這個檔案,我們可以看到它裡面實現了幾個所需的方法: newgetafter_balancebefore_retry_next_priority

  • new 負責做一些初始化工作。

  • get 負責執行選中節點的邏輯。

  • after_balance 在下面兩種情況下會執行:

  • 每次重試之前(此時 before_retry 為 true)

  • 最後一次嘗試之後

  • before_retry_next_priority 則是在每次嘗試完當前一組同 priority 的節點,準備嘗試下一組之前執行。

function _M.new(up_nodes, upstream)
...


return {
upstream = upstream,
get = function (ctx)
...
end,
after_balance = function (ctx, before_retry)
...
if not before_retry then
if ctx.balancer_tried_servers then
core.tablepool.release("balancer_tried_servers", ctx.balancer_tried_servers)
ctx.balancer_tried_servers = nil
end


return nil
end


if not ctx.balancer_tried_servers then
ctx.balancer_tried_servers = core.tablepool.fetch("balancer_tried_servers", 0, 2)
end


ctx.balancer_tried_servers[server] = true
end,
before_retry_next_priority = function (ctx)
if ctx.balancer_tried_servers then
core.tablepool.release("balancer_tried_servers", ctx.balancer_tried_servers)
ctx.balancer_tried_servers = nil
end
end,
}
end

如果沒有內部狀態需要維護,可以直接借用固定的模板程式碼(上述程式碼中,位於省略號以外的內容)來填充 after_balance before_retry_next_priority 這兩個方法。

選中節點之後,我們也可以通過外掛的形式新增額外的邏輯。外掛可以實現 before_proxy 方法。該方法會在選中節點之後呼叫,我們可以在該方法裡面記錄當前選中的節點資訊,這在 trace 裡面會有用。

擴充套件方向4:處理響應

我們可以通過 response-rewrite 外掛,在 header_filterbody_filter 處理上游返回的響應。前一個方法是修改響應頭,後一個方法修改響應體。注意 Apache APISIX 的響應處理是流式的,如果 header_filter 裡面沒有修改響應頭,響應頭就會被先發送出去,到了 body_filter 就沒辦法修改響應體了。

這意味著如果你後續想要修改body,但是 header 裡面又有 Content-Length 之類跟 body 相關的響應頭,那麼就要提前在 header_filter 裡面把這些頭改掉。我們提供了一個輔助方法: core.response.clear_header_as_body_modified ,只需要在 header_filter 呼叫它就行。

body_filter 也是流式的,而且還會被多次呼叫。所以如果你想要獲取完整的響應體,你需要把每次 body_filter 提供的部分響應體給拼起來。在 Apache APISIX master 分支上,我們提供了一個叫做 core.response.hold_body_chunk 的方法來簡化操作,感興趣的讀者可以看看程式碼。

擴充套件方向5:上報日誌和監控引數

在請求結束之後,我們還可以通過 log 方法來做一些清場工作。這一類工作可以分成兩類:

prometheus
http-logger

如果你感興趣的話,可以看看這兩個外掛的 log 方法是怎麼實現的:

  • prometheus 外掛文件:https://apisix.apache.org/zh/docs/apisix/plugins/prometheus/

  • http-logger 外掛文件:https://apisix.apache.org/zh/docs/apisix/plugins/http-logger/

關於 Apache APISIX

Apache APISIX 是一個動態、實時、高效能的開源 API 閘道器,提供負載均衡、動態上游、灰度釋出、服務熔斷、身份認證、可觀測性等豐富的流量管理功能。Apache APISIX 可以幫忙企業快速、安全的處理 API 和微服務流量,包括閘道器、Kubernetes Ingress 和服務網格等。               

Apache APISIX 落地使用者(僅部分)

  • Apache APISIX GitHub:https://github.com/apache/apisix

  • Apache APISIX 官網:https://apisix.apache.org/

  • Apache APISIX 文件:https://apisix.apache.org/zh/docs/apisix/getting-started

參考閱讀:

技術原創及架構實踐文章,歡迎通過公眾號選單「聯絡我們」進行投稿。

高可用架構

改變網際網路的構建方式