iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0
Cloud Native

與雲原生精靈共舞:APISIX使用者的兩年旅程系列 第 25

Ch18 - 用Lua擴充APISIX「服務發現」機制:機制參與三成員是誰?

  • 分享至 

  • xImage
  •  

服務發現

服務發現是一個古老的話題,當應用開始脫離單機運行和訪問時,服務發現就誕生了。目前的網絡架構是每個主機都有一個獨立的 IP 地址,那麼服務發現基本上都是通過某種方式獲取到服務所部署的 IP 地址。DNS 協議是最早將一個網絡名稱翻譯為網絡 IP 的協議,在最初的架構選型中,DNS+LVS+Nginx 基本可以滿足所有的 RESTful 服務的發現,此時服務的 IP 列表通常配置在 nginx或者 LVS。後來出現了 RPC 服務,服務的上下線更加頻繁,人們開始尋求一種能夠支持動態上下線並且推送 IP 列表變化的註冊中心產品。
----《Nacos架構&原理》

我對於「服務發現」實際運作方式,是從《Nacos架構&原理》一書才有比較清楚的了解。服務發現運作機制上有三個成員:

  • 服務註冊中心/服務發現中心
  • 提供服務者
  • 消費服務者

服務發現運作機制
(參考《Nacos架構&原理》內容後重劃)

DNS伺服器可以視作一個最早、最廣泛使用的服務註冊中心。透過nslookup工具查看tw.yahoo.com.服務,這個動作就像是在服務查詢一樣。可能會得到以下結果:

> nslookup tw.yahoo.com
Server:		127.0.0.53
Address:	127.0.0.53#53

Non-authoritative answer:
tw.yahoo.com	canonical name = fp-ycpi.g03.yahoodns.net.
Name:	fp-ycpi.g03.yahoodns.net
Address: 180.222.106.11
Name:	fp-ycpi.g03.yahoodns.net
Address: 180.222.109.252
Name:	fp-ycpi.g03.yahoodns.net
Address: 180.222.106.12
Name:	fp-ycpi.g03.yahoodns.net
Address: 180.222.109.251
Name:	fp-ycpi.g03.yahoodns.net
Address: 2406:2000:a0:807::1
Name:	fp-ycpi.g03.yahoodns.net
Address: 2406:2000:9c:800::11
Name:	fp-ycpi.g03.yahoodns.net
Address: 2406:2000:9c:800::12
Name:	fp-ycpi.g03.yahoodns.net
Address: 2406:2000:a0:807::2

如果服務提供者使用的是固定IP,或許做這麼一次手動設定就好。但通常固定IP使用費用比動態IP昂貴,因此有可能用上動態DNS(DDNS)的機制:

中華電信費用

動態DNS(DDNS)的提供者,可能會提供一個程式或腳本,這個程式會定期的向DDNS伺服器回報最新的IP位置。其實這個動作,映射到服務發現的概念,就是「服務註冊」。

Apache APISIX在這個結構中,也可以作爲「消費服務者」。為了知道上游服務代的節點,除了直接以Domain方式設定上游以外,APISIX也支援多種「服務發現」的機制:除了前述提到的DNS外,還有Nacos、Eureka、Consul。

ETCD也是常見於用來提供服務發現的工具之一,但APISIX並不能直接使用,需要多做一些工作才行。

APISIX支援的服務發現機制

除此之外,也可以自行為APISIX擴充服務發現的能力。譬如近期接觸到的一組軟體架構,其自行實現了服務發現中心,並將資訊儲存與SQL Server之中。前端會透過服務發現知道後端API節點,並實現客戶端的負載平衡。

service discovery

其服務發現回傳的結構類似下面JSON結果:

[
  {
    "Id": "02198043-1888-41e4-b055-493fae277f4a",
    "Name": "新竹巨城",
    "Description": "新竹巨城開始營業",
    "Host": "127.0.0.1",
    "Port": 8081,
    "IsEnabled": true,
    "Weight": 10,
    "CreatedOn": "2025-01-16T11:02:55.943+08:00",
    "CreatedBy": "System",
    "ModifiedOn": "2025-08-12T14:54:42.52+08:00",
    "ModifiedBy": "System",
    "LastServiceHistoryId": "02198043-1888-41e4-b055-493fae277f4a"
  },
  {
    "Id": "5ef3f698-51dd-4a4e-b25c-bd81e2f96e24",
    "Name": "新竹SOGO",
    "Description": "新竹巨城開始營業",
    "Host": "127.0.0.1",
    "Port": 8082,
    "IsEnabled": true,
    "Weight": 10,
    "CreatedOn": "2025-01-16T11:02:55.943+08:00",
    "CreatedBy": "System",
    "ModifiedOn": "2025-08-12T14:54:42.52+08:00",
    "ModifiedBy": "System",
    "LastServiceHistoryId": "5ef3f698-51dd-4a4e-b25c-bd81e2f96e24"
  }
]

可以將其格式轉換成APISIX使用的格式:

[
  {
    "host": "127.0.0.1",
    "port": 8081,
    "weight": 100
  },
  {
    "host": "127.0.0.1",
    "port": 8082,
    "weight": 100
  },
]

擴充APISIX服務發現能力

https://ithelp.ithome.com.tw/upload/images/20251009/20112470BRzUGy7hgA.png
(圖片來自APISIX文檔)

要擴充APISIX服務發現能力,可以建立discovery/customer01建立資料夾與相關檔案。當然,因爲使用docker建立服務,也需要將其掛載進容器位置/apisix/discovery/customer01/

services:
  apisix:
    container_name: apisix
    volumes:
      - ./apisix_config/config.yaml:/usr/local/apisix/conf/config.yaml:ro
      - ./discovery/customer01/:/usr/local/apisix/apisix/discovery/customer01/:ro

discovery/customer01/init.lua中的nodes(servcie_name)函式,其結果需要回傳前述APISIX需求格式:

function _M.nodes(service_name)
  local mock = {
    {
      host = "127.0.0.1",
      port = 8081,
      weight = 100,
      metadata = {
        ["management.port"] = "8081"
      }
    }
  }

  return mock
end

並且可以在init_worker()建立模組初始化的邏輯:

local _M = {
    version = 0.1,
}

function _M.init_worker()
    ...
end

function _M.dump_data()
  return {services = services_dict or  {}}
end

return _M

這次設計的邏輯,會在init_worker(),透過ngx_timer_at()ngx_timer_every()註冊週期性檢查機制:

function _M.init_worker()
  default_weight = local_conf.discovery.customer01.weight
  services_dict = {}
  log.info('default_weight:', default_weight)
  local fetch_interval = local_conf.discovery.customer01.fetch_interval
  log.info('fetch_interval:', fetch_interval)
  ngx_timer_at(0, fetch_full_registry)
  ngx_timer_every(fetch_interval, fetch_full_registry)
end

services_dict是個模組內部變數,其保存了服務發現的結果,譬如:

{
  "新竹百貨公司": [
    {
      "host": "127.0.0.1",
      "port": 8081,
      "weight": 100
    },
    {
      "host": "127.0.0.1",
      "port": 8082,
      "weight": 100
    },
  ]
}

並且會透過fetch_full_registry()更新服務發現的結果:

local function fetch_full_registry(premature)
  if premature then
      return
  end

  local up_app = {}
  for service_name in pairs(services_dict) do
    up_app[service_name] = get_service_nodes(service_name)
  end
  services_dict = up_app
  return up_app
end

get_service_nodes()是實際取得服務資訊的方法:

local function get_service_nodes(service_name)
  local url = get_base_url()
  url = url .. ngx.escape_uri(service_name .. '.json')
  local res, err = request_get(url) -- 透過「GET /新竹百貨公司.json」方式取得資訊
  if err then
    log.error(string.format('get servcie nodes failed, %q', err))
    return
  end
  if not res.body or res.status ~= 200 then  
    log.error(string.format('response is empty, status %d', res.status))
    return
  end
  
  local json_str = res.body
  local data, err = core.json.decode(json_str) -- 將結果從JSON字串轉換成內部物件
  if err then
    log.error('parse data failed, %q', err)
    return
  end
  ----------- section: 將結果轉換成APISIX需求格式 -------------
  local targets = {}
  services_dict[service_name] = targets
  for _i, node in ipairs(data) do
    if node.IsEnabled then  -- 僅保留啓用的節點
      table.insert(targets, {
        host = node.Host,
        port = node.Port,
        weight = node.Weight or default_weight,
	-- metadata = {
        --   ["management.port"] = node.Port
	-- }
      })
    end
    log.info(string.format('[%s]%s({%d}): %s : %s -> %s',  node.IsEnabled, service_name, _i, node.Name, node.Description, node.Endpoint))
  end
  ----------- endsection: 將結果轉換成APISIX需求格式 -------------
  return targets
end

設計上,會透過GET /<service_name>.json取得服務資訊,譬如GET /新竹百貨公司.json。取得的資訊會再轉換成APISIX的需求格式,而這個結果會儲存在services_dict[service_name]中。

現在可以調整nodes(service_name)的內容。調整的邏輯很簡單,如果services_dict[servicd_name]已經有結果了,直接回傳結果;否則註冊服務,並嘗試取得服務資訊:

function _M.nodes(service_name)
  if services_dict[servicd_name] then
    return services_dict[service_name]
  else
    return get_service_nodes(service_name)
  end
end

最後要提供schema.lua申明設定格式。其結果是一個符合JSON Type Definition的內容:

local host_pattern = [[^http(s)?:\/\/([a-zA-Z0-9-_.]+:.+\@)?[a-zA-Z0-9-_.:]+$]]
local prefix_pattern = [[^[\/a-zA-Z0-9-_.]+$]]


return {
    type = 'object',
    properties = {
        host = {
            type = 'string',
	    pattern = host_pattern,
            maxLength = 500,
        },
        prefix = {
            type = 'string',
            pattern = prefix_pattern,
            maxLength = 100,
            default = '/'
        },
        fetch_interval = {type = 'integer', minimum = 1, default = 30},
        weight = {type = 'integer', minimum = 1, default = 100},
        timeout = {
            type = 'object',
            properties = {
                connect = {type = 'integer', minimum = 1, default = 2000},
                send = {type = 'integer', minimum = 1, default = 2000},
                read = {type = 'integer', minimum = 1, default = 5000},
            },
            default = {
                connect = 2000,
                send = 2000,
                read = 5000,
            }
        }
    },
    required = {'host'}
}

使用擴充的服務發現機制

要使用擴充的服務發現機制,需要調整apisix_config/config.yaml內容:

discovery:
  customer01:
    host: http://127.0.0.1:8090
    prefix: '/'
    fetch_interval: 30
    weight: 100
    timeout:
      connect: 2000
      send: 2000
      read: 5000

discovery.customer01所設定的屬性,就是在discovery/customer01/schema.lua所申明的結構。

然後調整Upstream設定:

{
  "timeout": {
    "connect": 6,
    "send": 6,
    "read": 6
  },
  "type": "roundrobin",
  "scheme": "http",
  "discovery_type": "customer01",
  "pass_host": "pass",
  "name": "新竹百貨公司",
  "service_name": "新竹百貨公司",
  "keepalive_pool": {
    "idle_timeout": 60,
    "requests": 1000,
    "size": 320
  }
}

特別留意discovery_typeservice_name。分別申明了使用的服務發現機制和服務名稱:

discovery_type: customer01
service_name: 新竹百貨公司

最後要實現服務發現中心:同樣透過http-server ./ --port 8090簡單實現,並提供新竹百貨公司.json檔案為服務資訊。現在同樣瀏覽「新竹百貨公司」頁面,會發現沒什麼變化。但是僅透過調整新竹百貨公司.json檔案內容,就可以增加或減少上游的節點。

當然,正常的服務發現中心,一般不會簡單使用檔案儲存資訊XD。

參考資料

完整程式碼可以在GitHub Gist找到。


上一篇
Ch17 - APISIX x Keycloak:為 K8S Dashboard 部署「特勤門禁」—— Edge Gateway SSO 實戰
下一篇
Ch19 - APISIX 雙重鎖:OIDC 認證門神與 JWT-AUTH 身份驗證官的強強聯手
系列文
與雲原生精靈共舞:APISIX使用者的兩年旅程26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言