APISIX: Миграция операций etcd с HTTP на gRPC

Zexuan Luo

Zexuan Luo

February 10, 2023

Products

Ограничения HTTP-операций Apache APISIX с etcd

Когда etcd находился в версии 2.x, его API-интерфейс использовал HTTP 1 (далее будем называть его просто HTTP). После обновления etcd до версии 3.x протокол был изменён с HTTP на gRPC. Для пользователей, которые не поддерживают gRPC, etcd предоставляет gRPC-Gateway, который проксирует HTTP-запросы как gRPC для доступа к новым gRPC API.

Когда APISIX начал использовать etcd, он использовал API etcd v2. В APISIX 2.0 (2020) мы обновили требование к версии etcd с 2.x до 3.x. Совместимость etcd с HTTP сэкономила нам усилия при обновлении версии. Нам нужно было лишь изменить код, связанный с вызовом методов и обработкой ответов. Однако за эти годы мы также столкнулись с некоторыми проблемами, связанными с HTTP API etcd. Существуют определённые тонкие различия. Мы поняли, что наличие gRPC-gateway не означает, что он может идеально поддерживать HTTP-доступ.

Вот список проблем, с которыми мы столкнулись при работе с etcd за последние годы:

  1. gRPC-gateway отключён по умолчанию. Из-за невнимательности разработчиков в некоторых проектах gRPC-gateway не включён в конфигурации по умолчанию. Поэтому нам пришлось добавить инструкции в документацию, чтобы проверить, включён ли gRPC-gateway в текущей версии etcd. См. https://github.com/apache/apisix/pull/2940.
  2. По умолчанию gRPC ограничивает размер ответа до 4 МБ. etcd снимает это ограничение в предоставляемом SDK, но не в gRPC-gateway. Оказалось, что официальный etcdctl (построенный на предоставляемом SDK) работает нормально, но APISIX — нет. См. https://github.com/etcd-io/etcd/issues/12576.
  3. Та же проблема — на этот раз с максимальным количеством запросов для одного соединения. Реализация HTTP2 в Go имеет конфигурацию MaxConcurrentStreams, которая контролирует количество запросов, которые один клиент может отправить одновременно, по умолчанию это 250. Какой клиент обычно отправляет более 250 запросов одновременно? Поэтому etcd всегда использовал эту конфигурацию. Однако gRPC-gateway, который проксирует все HTTP-запросы к локальному gRPC-интерфейсу, может превысить этот лимит. См. https://github.com/etcd-io/etcd/issues/14185.
  4. После включения mTLS в etcd используется один и тот же сертификат как для серверного, так и для клиентского сертификата: серверный сертификат для gRPC-gateway и клиентский сертификат, когда gRPC-gateway обращается к gRPC-интерфейсу. Если на сертификате включено расширение серверной аутентификации, но не включено расширение клиентской аутентификации, это приведёт к ошибке проверки сертификата. Опять же, доступ с помощью etcdctl работает нормально (так как в этом случае сертификат не используется как клиентский), но APISIX — нет. См. https://github.com/etcd-io/etcd/issues/9785.
  5. После включения mTLS etcd позволяет настраивать политики безопасности для информации о пользователях в сертификатах. Как упоминалось выше, gRPC-gateway использует фиксированный клиентский сертификат при доступе к gRPC-интерфейсу, а не информацию о сертификате, используемом для доступа к HTTP-интерфейсу в начале. Таким образом, эта функция не будет работать, так как клиентский сертификат фиксирован и не изменяется. См. https://github.com/apache/apisix/issues/5608.

Мы можем обобщить проблемы в двух пунктах:

  1. gRPC-gateway (и, возможно, другие попытки преобразования HTTP в gRPC) не является универсальным решением, которое устраняет все проблемы.
  2. Разработчики etcd не уделяют достаточного внимания методу преобразования HTTP в gRPC. И их крупнейший пользователь, Kubernetes, не использует эту функцию.

Чтобы решить эту проблему, нам нужно использовать etcd напрямую через gRPC, чтобы не проходить через путь HTTP, зарезервированный для совместимости через gRPC-Gateway.

Преодоление трудностей миграции на gRPC

Ошибка в lua-protobuf

Нашей первой проблемой в процессе миграции стала неожиданная ошибка в сторонней библиотеке. Как и большинство приложений OpenResty, мы используем lua-protobuf для декодирования/кодирования protobuf.

После интеграции proto-файла etcd мы обнаружили, что в Lua-коде иногда происходят сбои с ошибкой "table overflow". Поскольку этот сбой нельзя воспроизвести надёжно, нашим первым инстинктом было найти минимальный воспроизводимый пример. Интересно, что если использовать proto-файл etcd отдельно, проблему вообще нельзя воспроизвести. Этот сбой, похоже, происходит только при работе APISIX.

После некоторого отладки я обнаружил проблему в lua-protobuf при разборе поля oneof в proto-файле. lua-protobuf пытался предварительно выделить размер таблицы при разборе, и выделенный размер рассчитывался на основе определённого значения. Была вероятность, что это значение могло быть отрицательным числом. Затем LuaJIT преобразовывал это число в большое положительное число при выделении, что приводило к ошибке "table overflow". Я сообщил об этой проблеме автору, и мы поддерживали форк с временным решением внутри компании.

Автор lua-protobuf отреагировал очень оперативно, предоставив исправление на следующий день и выпустив новую версию через несколько дней. Оказалось, что когда lua-protobuf очищал proto-файлы, которые больше не использовались, он пропускал очистку некоторых полей, что приводило к неразумному отрицательному числу при последующей обработке oneof. Проблема возникала лишь изредка, и её нельзя было воспроизвести при использовании proto-файла etcd отдельно, так как пропускались шаги очистки этих полей.

Совместимость с поведением HTTP

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

Если ответы были в gRPC, их нужно было обернуть в оболочку HTTP-ответа, чтобы соответствовать логике обработки. В противном случае вызывающей стороне пришлось бы изменять код в нескольких местах, чтобы адаптироваться к новому (gRPC) формату ответа. Особенно учитывая, что старые HTTP-операции с etcd также должны поддерживаться одновременно.

Хотя добавление дополнительного слоя для совместимости с HTTP-ответом нежелательно, нам пришлось с этим работать. Кроме того, нам также нужно было обработать gRPC-ответ. Например, когда нет соответствующих данных, HTTP не возвращает никаких данных, но gRPC возвращает пустую таблицу. Это также нужно было адаптировать, чтобы соответствовать поведению HTTP.

От коротких соединений к длительным

В HTTP-операциях с etcd APISIX использует короткие соединения, поэтому нет необходимости управлять соединениями. Всё, что нам нужно сделать, — это установить новое соединение, когда это необходимо, и закрыть его по завершении.

Но gRPC не может этого сделать. Одной из основных целей миграции на gRPC является достижение мультиплексирования, что невозможно, если для каждой операции создаётся новое gRPC-соединение. Здесь нам нужно поблагодарить gRPC-go за встроенную возможность управления соединениями, которая автоматически восстанавливает соединение после его разрыва. Таким образом, мы можем использовать gRPC-go для повторного использования соединения. И только бизнес-требования нужно учитывать на уровне APISIX.

Операции с etcd в APISIX можно разделить на две категории: операции CRUD (создание, удаление, изменение, запрос) с данными etcd и синхронизация конфигурации с плоскости управления. Хотя теоретически эти две операции с etcd могут использовать одно и то же gRPC-соединение, мы решили разделить их на два соединения для разделения ответственности. Для соединения операций CRUD, поскольку APISIX нужно обрабатывать отдельно при запуске и после запуска, добавлено условие при получении нового соединения. Если есть несоответствие (т.е. текущее соединение создано при запуске, а нам нужно соединение после запуска), то мы закрываем текущее соединение и создаём новое. Я разработал новый метод синхронизации для синхронизации конфигурации, чтобы каждый ресурс использовал поток под существующим соединением для наблюдения за etcd.

Преимущества миграции на gRPC

Одним из очевидных преимуществ после миграции на gRPC является значительное сокращение количества соединений, необходимых для работы с etcd. При работе с etcd через HTTP APISIX мог использовать только короткие соединения. А при синхронизации конфигурации каждый ресурс имел отдельное соединение.

После перехода на gRPC мы можем использовать функцию мультиплексирования gRPC, и каждый ресурс использует только один поток вместо полного соединения. Таким образом, количество соединений больше не увеличивается с количеством ресурсов. Учитывая, что в последующих версиях APISIX будет добавлено больше типов ресурсов, например, в последней версии 3.1 добавлены secrets, сокращение количества соединений при использовании gRPC будет ещё более значительным.

При использовании gRPC для синхронизации каждый процесс имеет только одно (два, если включена подсистема потоков) соединение для синхронизации конфигурации. На рисунке ниже мы видим, что два процесса имеют четыре соединения, два из которых используются для синхронизации конфигурации, одно соединение используется для Admin API, а оставшееся соединение используется для привилегированного агента для отчёта о состоянии сервера.

gRPC использует гораздо меньше соединений

Для сравнения, на следующем рисунке показаны 22 соединения, необходимые для использования оригинального метода синхронизации конфигурации при неизменных параметрах. Кроме того, эти соединения являются короткими.

слишком много соединений

Единственное различие между этими двумя конфигурациями заключается в том, включён ли gRPC для операций с etcd:

etcd: use_grpc: true host: - "http://127.0.0.1:2379" prefix: "/apisix" ...

Помимо сокращения количества соединений, использование gRPC для прямого доступа к etcd вместо gRPC-gateway позволяет решить ряд архитектурно ограниченных проблем, таких как аутентификация mTLS, упомянутая в начале статьи. Также будет меньше проблем при использовании gRPC, так как Kubernetes использует gRPC для работы с etcd. Если возникнет проблема, она будет обнаружена сообществом Kubernetes.

Конечно, поскольку метод gRPC всё ещё относительно новый, APISIX неизбежно столкнётся с новыми проблемами при работе с etcd через gRPC. В настоящее время по умолчанию всё ещё используется оригинальный метод на основе HTTP для работы с etcd. Пользователи могут самостоятельно настроить use_grpc в config.yaml под etcd на true. Вы можете попробовать, если метод gRPC лучше. Мы также будем постоянно собирать отзывы из различных источников, чтобы улучшить работу с etcd на основе gRPC. Когда мы убедимся, что подход gRPC достаточно зрелый, мы сделаем его подходом по умолчанию.

Чтобы максимизировать APISIX, вам нужен API7

Вам нравится производительность Apache APISIX, но не накладные расходы на его управление. Вы можете сосредоточиться на своём основном бизнесе, не беспокоясь о настройке, обслуживании и обновлении.

Наша команда состоит из создателей и участников Apache APISIX, основных разработчиков OpenResty и NGINX, участников Kubernetes и экспертов по облачной инфраструктуре. Вы получаете лучших специалистов за кулисами.

Хотите ускорить разработку с уверенностью? Чтобы максимизировать поддержку APISIX, вам нужен API7. Мы предоставляем глубокую поддержку APISIX и решения для управления API, основанные на ваших потребностях!

Свяжитесь с нами сейчас: https://api7.ai/contact.

Tags: