Написание собственного клиента обнаружения сервисов для Apache APISIX

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
    }
  }
}'
Войти в полноэкранный режим Выйти из полноэкранного режима
  1. Каждый запрос имеет шанс 50/50 быть отправленным на любой из узлов.

Долгое время это работало, но в наше время узлы, вероятно, не домашние животные, а скот: они приходят, и они уходят. Следовательно, необходимо динамически обновлять список узлов, когда это происходит.

В этой статье я хотел бы объяснить, как это сделать.

Существующие реестры обнаружения сервисов

Пожалуйста, не изобретайте колесо! Apache APISIX поставляется с кучей существующих реестров обнаружения сервисов «из коробки».

Реестр Провайдер Описание Интеграция
DNS Ссылка
Consul HashiCorp

Consul использует идентификацию услуг и традиционные сетевые методы, чтобы помочь организациям безопасно соединить приложения, работающие в любой среде.

Ссылка
nacos Alibaba

Простая в использовании платформа динамического обнаружения, конфигурации и управления сервисами для создания «облачных» приложений.

Ссылка
Эврика Netflix

Eureka — это RESTful (Representational State Transfer) сервис, который в основном используется в облаке AWS для обнаружения, балансировки нагрузки и обхода отказа серверов среднего уровня. Он играет важную роль в инфраструктуре среднего уровня Netflix.

Ссылка
Zookeeper Apache

ZooKeeper — это централизованная служба для поддержания информации о конфигурации, именования, обеспечения распределенной синхронизации и предоставления групповых услуг. Все эти виды услуг в той или иной форме используются распределенными приложениями. Каждый раз, когда они реализуются, много работы уходит на исправление ошибок и условий гонки, которые неизбежны. Из-за сложности реализации этих видов услуг, приложения изначально обычно экономят на них, что делает их хрупкими в условиях изменений и сложными в управлении. Даже когда все сделано правильно, различные реализации этих услуг приводят к сложности управления при развертывании приложений.

Ссылка
Kubernetes CNCF

Kubernetes, также известный как K8s, — это система с открытым исходным кодом для автоматизации развертывания, масштабирования и управления контейнерными приложениями.

Ссылка

Прежде чем писать свою собственную, убедитесь, что ваша платформа не входит в список выше.

Настройка окружения

Чтобы облегчить себе жизнь и проще воспроизводить шаги, я решил использовать 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
Вход в полноэкранный режим Выход из полноэкранного режима
  1. Используется последний образ на момент написания статьи.
  2. Минимальная конфигурация, подробнее смотрите полный файл.
  3. Потерпите; я объясню позже
  4. Apache APISIX запускается быстрее, чем etcd. Он будет искать etcd, сделает вывод, что он недоступен, и остановится. После этого мы хотим запустить его снова.
  5. Не делайте этого в продакшене!
  6. При запуске на Docker Desktop с включенным Kubernetes возникает конфликт портов с портом по умолчанию. Нам нужно его изменить.

Пример использования

Представим себе YAML-файл, который ссылается на доступные узлы. Специальный процесс прослушивает изменения в топологии: он заново генерирует файл с новыми узлами. Наш клиент регулярно читает этот файл и обновляет свой внутренний список узлов.

Вот предлагаемая структура:

nodes:
  "192.168.1.62:81": 1
#END
Вход в полноэкранный режим Выход из полноэкранного режима

Разработка клиента службы обнаружения

Для создания клиента службы обнаружения необходима следующая структура:

yaml                             # 1
  |_ schema.lua                  # 2
  |_ init.lua                    # 3
Вход в полноэкранный режим Выход из полноэкранного режима
  1. Дайте ему имя; yaml подходит как никакое другое.
  2. Перечислите параметры конфигурации — имя, тип, требуются ли они и т.д.
  3. Для самого кода

Чтобы 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
Войти в полноэкранный режим Выйти из полноэкранного режима
  1. Верните таблицу 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
Вход в полноэкранный режим Выход из полноэкранного режима
  1. Вызвать функцию read_file немедленно
  2. Вызывать функцию 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
Войдите в полноэкранный режим Выход из полноэкранного режима
  1. Импортируйте библиотеку для чтения файла
  2. Импортируйте библиотеку для преобразования содержимого YAML в таблицы Lua
  3. Прочитать файл
  4. Разберите его содержимое
  5. Итерация по строкам, которые должны быть отформатированы как "<ip>:<port>":<weight>. Я слишком ленив, чтобы обработать все угловые случаи, будьте моим гостем.
  6. Разберите каждый ключ — строку "<ip>:<port>".
  7. Создайте таблицу Lua для каждого узла
  8. Вставьте ее в локальную переменную файла 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
  }
}'
Войти в полноэкранный режим Выйти из полноэкранного режима
  1. Соответствует параметру service_name в функции _M.nodes(service_name). Это потенциально позволяет возвращать различные узлы на его основе. Мы не использовали его здесь, поэтому подойдет любой.
  2. Магия происходит здесь. Метка должна совпадать с именем папки обнаружения в /usr/local/apisix/apisix/discovery/.
  3. Узлы не задаются; клиент обнаружения сервисов будет динамически возвращать их.

Давайте протестируем:

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
Вход в полноэкранный режим Выход из полноэкранного режима
  1. Отследить ошибку

Параметризация

До сих пор мы не использовали никаких параметров. Путь к файлу конфигурации и интервал выборки жестко закодированы. Мы можем сделать лучше, сделав их настраиваемыми.

return {
    type = "object",
    properties = {
        path = { type = "string", default = "/var/apisix/nodes.yaml" },    -- 1
        fetch_interval = { type = "integer", minimum = 1, default = 30 },  -- 1
    },
}
Вход в полноэкранный режим Выход из полноэкранного режима
  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

Оцените статью
devanswers.ru
Добавить комментарий