API-шлюзы в целом, и Apache APISIX в частности, обеспечивают единую точку входа в информационную систему. Такая архитектура позволяет управлять распределением нагрузки и отказоустойчивостью на аналогичных узлах. Например, вот как можно создать маршрут, сбалансированный на двух узлах Apache APISIX:
curl http://localhost:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '{
"uri": "/*",
"upstream": {
"type": "roundrobin",
"nodes": {
"192.168.0.1:80": 1, # 1
"192.168.0.2:80": 1 # 1
}
}
}'
- Каждый запрос имеет шанс 50/50 быть отправленным на любой из узлов.
Долгое время это работало, но в наше время узлы, вероятно, не домашние животные, а скот: они приходят, и они уходят. Следовательно, необходимо динамически обновлять список узлов, когда это происходит.
В этой статье я хотел бы объяснить, как это сделать.
Существующие реестры обнаружения сервисов
Пожалуйста, не изобретайте колесо! Apache APISIX поставляется с кучей существующих реестров обнаружения сервисов «из коробки».
Реестр | Провайдер | Описание | Интеграция | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
DNS | Ссылка | ||||||||||||||||||
Consul | HashiCorp |
|
Прежде чем писать свою собственную, убедитесь, что ваша платформа не входит в список выше.
Настройка окружения
Чтобы облегчить себе жизнь и проще воспроизводить шаги, я решил использовать Docker и Docker Compose. Вот образец, который вы можете использовать в целях разработки:
version: "3"
services:
apisix:
image: apache/apisix:2.14.1-alpine # 1
volumes:
- ./config/config.yaml:/usr/local/apisix/conf/config.yaml:ro # 2
- ./yaml:/usr/local/apisix/apisix/discovery/yaml:ro # 3
- ./sample:/var/apisix:ro # 3
ports:
- "9080:9080"
- "9090:9090"
restart: always # 4
depends_on:
- etcd
etcd:
image: bitnami/etcd:3.5.2
environment:
ETCD_ENABLE_V2: "true"
ALLOW_NONE_AUTHENTICATION: "yes" # 5
ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2397"
ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2397"
ports:
- "2397:2397" # 6
- Используется последний образ на момент написания статьи.
- Минимальная конфигурация, подробнее смотрите полный файл.
- Потерпите; я объясню позже
- Apache APISIX запускается быстрее, чем etcd. Он будет искать etcd, сделает вывод, что он недоступен, и остановится. После этого мы хотим запустить его снова.
- Не делайте этого в продакшене!
- При запуске на Docker Desktop с включенным Kubernetes возникает конфликт портов с портом по умолчанию. Нам нужно его изменить.
Пример использования
Представим себе YAML-файл, который ссылается на доступные узлы. Специальный процесс прослушивает изменения в топологии: он заново генерирует файл с новыми узлами. Наш клиент регулярно читает этот файл и обновляет свой внутренний список узлов.
Вот предлагаемая структура:
nodes:
"192.168.1.62:81": 1
#END
Разработка клиента службы обнаружения
Для создания клиента службы обнаружения необходима следующая структура:
yaml # 1
|_ schema.lua # 2
|_ init.lua # 3
- Дайте ему имя;
yaml
подходит как никакое другое. - Перечислите параметры конфигурации — имя, тип, требуются ли они и т.д.
- Для самого кода
Чтобы Apache APISIX мог использовать клиент, необходимо установить папку yaml
как дочернюю папку /usr/local/apisix/apisix/discovery
; отсюда и монтирование в файле Docker Compose выше.
Клиент должен следовать определенной структуре.
local _M = {}
-- Initialize the client
function _M.init_worker()
end
-- Get available nodes.
--
-- @param service_name Not used
-- @treturn table
-- @return Available nodes, e.g., { [1] = { ["port"] = 81, ["host"] = 127.0.0.1, ["weight"] = 1 }}
function _M.nodes(service_name)
end
-- Dump existing nodes.
--
-- @return Debugging information
function dump_data()
end
Начнем с самого простого:
local nodes
function _M.nodes(service_name)
return nodes -- 1
end
- Верните таблицу
nodes
. Мы заполняем узлы в функции_M.init()
.
Мы хотим, чтобы клиент регулярно читал YAML-файл. Для этого мы можем использовать возможности модуля Lua Nginx. Он является частью OpenResty, на котором построен Apache APISIX. Модуль предлагает дополнительные API, и два из них особенно полезны:
local ngx_timer_at = ngx.timer.at
local ngx_timer_every = ngx.timer.every
function _M.init_worker()
ngx_timer_at(0, read_file) -- 1
ngx_timer_every(20, read_file) -- 2
end
- Вызвать функцию
read_file
немедленно - Вызывать функцию
read_file
каждые 20 секунд
Настало время написать функцию read_file
.
local util = require("apisix.cli.util") -- 1
local yaml = require("tinyyaml") -- 2
local function read_file()
local content, err = util.read_file("/var/apisix/nodes.yaml") -- 3
if not content then
return
end
local nodes_conf, err = yaml.parse(content) -- 4
if not nodes_conf then
return
end
if not nodes then
nodes = {}
end
for uri, weight in pairs(nodes_conf.nodes) do -- 5
local host_port = {}
for str in string.gmatch(uri, "[^:]+") do
table.insert(host_port, str) -- 6
end
local node = {
host = host_port[1],
port = tonumber(host_port[2]),
weight = weight,
} -- 7
table.insert(nodes, node) -- 8
end
end
- Импортируйте библиотеку для чтения файла
- Импортируйте библиотеку для преобразования содержимого YAML в таблицы Lua
- Прочитать файл
- Разберите его содержимое
- Итерация по строкам, которые должны быть отформатированы как
"<ip>:<port>":<weight>
. Я слишком ленив, чтобы обработать все угловые случаи, будьте моим гостем. - Разберите каждый ключ — строку
"<ip>:<port>"
. - Создайте таблицу Lua для каждого узла
- Вставьте ее в локальную переменную файла
nodes
.
Тестирование кода
Для проверки кода я использовал веб-сервер Apache, установленный по умолчанию на моем Mac.
- Я изменил порт с 80 на 81, чтобы избежать конфликтов.
- Я запустил его с помощью
sudo apachectl start
. - Я отметил IP своей машины, который доступен из контейнеров Docker
-
Я обновил конфигурационный файл:
nodes: "192.168.1.62:81": 1 #END
-
Я запустил контейнеры Docker Compose —
docker compose up
.
На этом этапе я использовал API администратора для создания маршрута с новым клиентом обнаружения сервисов YAML:
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '{
"uri": "/",
"upstream": {
"service_name": "MY-YAML", # 1
"type": "roundrobin",
"discovery_type": "yaml" # 2-3
}
}'
- Соответствует параметру
service_name
в функции_M.nodes(service_name)
. Это потенциально позволяет возвращать различные узлы на его основе. Мы не использовали его здесь, поэтому подойдет любой. - Магия происходит здесь. Метка должна совпадать с именем папки обнаружения в
/usr/local/apisix/apisix/discovery/
. - Узлы не задаются; клиент обнаружения сервисов будет динамически возвращать их.
Давайте протестируем:
curl localhost:9080
Возвращается корневая страница, обслуживаемая сервером Apache, как и ожидалось:
<html><body><h1>It works!</h1></body></html>
Нитпикинг
Хотя приведенный выше код работает так, как ожидалось, мы можем его улучшить.
Ведение журнала
Релевантное протоколирование может помочь вашему будущему «я» решить неприятные ошибки на производстве.
local core = require("apisix.core")
local function read_file(premature)
local content, err = util.read_file("/var/apisix/nodes.yaml")
if not content then
log.error("Unable to open YAML discovery configuration file: ", err) -- 1
return
end
- Отследить ошибку
Параметризация
До сих пор мы не использовали никаких параметров. Путь к файлу конфигурации и интервал выборки жестко закодированы. Мы можем сделать лучше, сделав их настраиваемыми.
return {
type = "object",
properties = {
path = { type = "string", default = "/var/apisix/nodes.yaml" }, -- 1
fetch_interval = { type = "integer", minimum = 1, default = 30 }, -- 1
},
}
- Параметры с их типом и значением по умолчанию. Ни один из них не является обязательным.
На стороне кода мы можем использовать их соответствующим образом.
local core = require("apisix.core")
local local_conf = require("apisix.core.config_local").local_conf()
function _M.init_worker()
local fetch_interval = local_conf.discovery and
local_conf.discovery.yaml and
local_conf.discovery.yaml.fetch_interval
ngx_timer_every(fetch_interval, read_file)
Преждевременный
Наконец, API ngx.timer.every
вызывает нашу функцию со специальным параметром premature
:
Преждевременное истечение таймера происходит, когда рабочий процесс Nginx пытается завершить работу, например, при перезагрузке конфигурации Nginx, вызванной сигналом
HUP
или при выключении сервера Nginx. Когда рабочий процесс Nginx пытается завершить работу, больше нельзя вызыватьngx.timer.at
для создания новых таймеров с ненулевой задержкой, и в этом случаеngx.timer.at
вернет значение «conditional false» и строку, описывающую ошибку, то есть «процесс завершается».— ngx.timer.at
Давайте будем хорошими гражданами-разработчиками и обработаем параметр соответствующим образом:
local function read_file(premature)
if premature then
return
end
end
Заключение
Большинство современных инфраструктур динамичны — серверы это скот, а не домашние животные. В этом случае нет особого смысла настраивать узлы восходящего потока статически.
По этой причине Apache APISIX предоставляет клиентов обнаружения сервисов. Хотя он поставляется с пакетом из коробки, можно написать свой собственный, выполнив несколько шагов. В этом посте я описал эти шаги для реализации реестра узлов на основе YAML-файла.
Полный исходный код этого поста можно найти на Github:
ajavageek / apisix-yaml-service-discovery
Идти дальше:
- Реестр обнаружения интеграционных сервисов
- Модуль Lua Nginx
Первоначально опубликовано на A Java Geek 17 июляth, 2022