Перейти к содержанию

Конструкция клиента etcd

Архитектурные решения клиента и подробности их реализации

Конструкция клиента etcd

Gyuho Lee (github.com/gyuho, Amazon Web Services, Inc.), Joe Betz (github.com/jpbetz, Google Inc.)

Введение

Сервер etcd доказал свою надёжность многолетним тестированием с внедрением отказов. Большая часть сложной логики приложений уже обрабатывается сервером etcd и его хранилищами данных (например, состав кластера прозрачен для клиентов, а уровень Raft пересылает предложения лидеру). Хотя серверные компоненты корректны, их взаимодействие с клиентом требует иного набора сложных протоколов, обеспечивающих корректность и высокую доступность в условиях отказов. В идеале сервер etcd представляет множество физических машин как единый логический кластер, а клиент реализует автоматическое переключение между репликами при отказе. В этом документе описаны архитектурные решения клиента и подробности их реализации.

Глоссарий

clientv3: официальный клиент etcd на Go для API etcd v3.

clientv3-grpc1.0: официальная реализация клиента с grpc-go v1.0.x , используемая в последней версии etcd v3.1.

clientv3-grpc1.7: официальная реализация клиента с grpc-go v1.7.x , используемая в последних версиях etcd v3.2 и v3.3.

clientv3-grpc1.23: официальная реализация клиента с grpc-go v1.23.x , используемая в последней версии etcd v3.4.

Балансировщик: балансировщик нагрузки клиента etcd, реализующий механизм повторных попыток и переключения при отказе. Клиент etcd должен автоматически распределять нагрузку между несколькими конечными точками.

Конечные точки: список конечных точек сервера etcd, к которым могут подключаться клиенты. Обычно это 3 или 5 клиентских URL кластера etcd.

Закреплённая конечная точка: при настройке нескольких конечных точек балансировщик клиента <= v3.3 выбирает только одну для установления TCP-соединения, чтобы сократить общее количество открытых соединений с кластером etcd. В v3.4 балансировщик циклически выбирает закреплённые конечные точки для каждого запроса, распределяя нагрузку равномернее.

Клиентское соединение: TCP-соединение с сервером etcd, установленное посредством gRPC Dial.

Подсоединение: интерфейс gRPC SubConn. Каждое подсоединение содержит список адресов. Балансировщик создаёт SubConn из списка разрешённых адресов. gRPC ClientConn может соответствовать нескольким SubConn (например, example.com разрешается в 10.10.10.1 и 10.10.10.2, относящиеся к двум подсоединениям). Балансировщик etcd v3.4 использует внутренний разрешитель, чтобы создать по одному подсоединению для каждой конечной точки.

Кратковременный разрыв соединения: сервер gRPC возвращает ошибку состояния code Unavailable .

Требования к клиенту

Корректность. При отказах сервера запросы могут завершаться неудачно. Однако гарантии согласованности никогда не нарушаются: свойства глобального порядка, недопустимость записи повреждённых данных, семантика «не более одного раза» для изменяющих операций, наблюдение никогда не видит частичные события и так далее.

Живучесть. Серверы могут кратковременно отказывать или отключаться. Клиенты должны в любом случае продолжать работу. Если не настроено иное, клиенты никогда не должны попадать во взаимоблокировку , ожидая возвращения сервера в сеть. В идеале клиенты обнаруживают недоступные серверы с помощью ping HTTP/2, переключаются на другие узлы и выдают понятные сообщения об ошибках.

Эффективность. Клиенты должны эффективно работать с минимальными ресурсами: после переключения конечной точки прежние TCP-соединения следует плавно закрывать . Механизм переключения при отказе должен эффективно выбирать следующую реплику для подключения, не тратя ресурсы на повторные попытки к отказавшим узлам.

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

Обзор клиента

Клиент etcd реализует следующие компоненты:

  • балансировщик, устанавливающий соединения gRPC с кластером etcd;
  • клиент API, отправляющий RPC серверу etcd; и
  • обработчик ошибок, решающий, следует ли повторить неудачный запрос или переключить конечную точку.

Языки могут различаться способом установления исходного соединения (например, настройкой TLS), кодирования и отправки серверу сообщений Protocol Buffer, обработки потоковых RPC и так далее. Однако ошибки, возвращаемые сервером etcd, одинаковы. Такими же должны быть обработка ошибок и политика повторных попыток.

Например, сервер etcd может вернуть "rpc error: code = Unavailable desc = etcdserver: request timed out" — временную ошибку, предполагающую повторные попытки. Либо он может вернуть rpc error: code = InvalidArgument desc = etcdserver: key is not provided, что означает недопустимый запрос, который повторять не следует. Клиент Go может разбирать ошибки с помощью google.golang.org/grpc/status.FromError, а клиент Java — с помощью io.grpc.Status.fromThrowable.

clientv3-grpc1.0: обзор балансировщика

При настройке нескольких конечных точек etcd clientv3-grpc1.0 поддерживает несколько TCP-соединений. Затем он выбирает один адрес и использует его для всех клиентских запросов. Закреплённый адрес сохраняется до закрытия объекта клиента (см. рисунок 1). Получив ошибку, клиент случайным образом выбирает другой адрес и повторяет запрос.

client-balancer-figure-01.png

clientv3-grpc1.0: ограничение балансировщика

Несколько TCP-соединений, открываемых clientv3-grpc1.0, могут ускорить переключение балансировщика при отказе, но требуют больше ресурсов. Балансировщик не знает ни состояние узлов, ни состав кластера. Поэтому он может застрять на одном отказавшем или изолированном узле.

clientv3-grpc1.7: обзор балансировщика

clientv3-grpc1.7 поддерживает только одно TCP-соединение с выбранным сервером etcd. Получив несколько конечных точек кластера, клиент сначала пытается подключиться ко всем. Как только одно соединение устанавливается, балансировщик закрепляет адрес и закрывает остальные (см. рисунок 2). Закреплённый адрес сохраняется до закрытия объекта клиента. Ошибка сервера или клиентской сети передаётся клиентскому обработчику ошибок (см. рисунок 3).

client-balancer-figure-02.pngclient-balancer-figure-03.png

Клиентский обработчик получает ошибку от сервера gRPC и по её коду и сообщению решает, повторить ли запрос к той же конечной точке или переключиться на другие адреса (см. рисунок 4 и рисунок 5).

client-balancer-figure-04.pngclient-balancer-figure-05.png

Потоковые RPC, такие как Watch и KeepAlive, часто запрашиваются без тайм-аутов. Вместо этого клиент может периодически отправлять ping HTTP/2 для проверки состояния закреплённой конечной точки; если сервер не отвечает, балансировщик переключается на другие конечные точки (см. рисунок 6).

client-balancer-figure-06.png

clientv3-grpc1.7: ограничение балансировщика

Балансировщик clientv3-grpc1.7 отправляет сигналы keepalive HTTP/2, чтобы обнаруживать разрывы потоковых запросов. Это простой механизм ping сервера gRPC, не учитывающий состав кластера и потому не способный обнаружить разделение сети. Поскольку изолированный сервер gRPC всё ещё может отвечать на клиентские ping, балансировщик способен застрять на нём. В идеале ping keepalive обнаруживает разделение и вызывает переключение конечной точки до истечения тайм-аута запроса (см. etcd#8673 и рисунок 7).

client-balancer-figure-07.png

Балансировщик clientv3-grpc1.7 поддерживает список неисправных конечных точек. Отключённые адреса добавляются в список «неисправных» и считаются недоступными до окончания периода ожидания, жёстко заданного как тайм-аут набора номера со значением по умолчанию 5 секунд. Балансировщик может ошибочно считать конечные точки неисправными. Например, конечная точка A может вернуться сразу после внесения в чёрный список, но останется недоступной следующие 5 секунд (см. рисунок 8).

clientv3-grpc1.0 испытывал те же описанные выше проблемы.

client-balancer-figure-08.png

Вышестоящий gRPC Go уже перешёл на новый интерфейс балансировщика. Например, внутренняя реализация балансировщика clientv3-grpc1.7 использует новый балансировщик gRPC и пытается сохранять поведение старого. Хотя совместимость поддерживалась достаточно хорошо, клиент etcd всё же страдал от малозаметных нарушающих совместимость изменений . Кроме того, сопровождающие gRPC рекомендуют не полагаться на старый интерфейс балансировщика . В целом для более качественной поддержки со стороны вышестоящего проекта лучше синхронизироваться с последними выпусками gRPC. Новые функции, например политика повторных попыток, могут не переноситься в ветвь gRPC 1.7. Поэтому и сервер, и клиент etcd должны перейти на последние версии gRPC.

clientv3-grpc1.23: обзор балансировщика

clientv3-grpc1.7 настолько тесно связан со старым интерфейсом gRPC, что каждое обновление зависимости gRPC нарушало поведение клиента. Большая часть усилий по разработке и отладке уходила на исправление этих изменений. В результате реализация стала чрезмерно сложной и опиралась на неверные предположения о соединениях с серверами.

Основная цель clientv3-grpc1.23 — упростить логику переключения балансировщика при отказе. Вместо поддержки потенциально устаревшего списка неисправных конечных точек он просто циклически переходит к следующей точке при каждом отключении клиента от текущей. Состояние конечных точек не предполагается, поэтому сложное отслеживание состояния больше не нужно (см. рисунок 8 и текст выше). Переход на clientv3-grpc1.23 не должен создавать проблем: все изменения внутренние, а обратная совместимость полностью сохранена.

Получив несколько конечных точек, clientv3-grpc1.23 внутри создаёт несколько подсоединений (по одному на каждую точку), тогда как clientv3-grpc1.7 создаёт только одно соединение с закреплённой точкой (см. рисунок 9). Например, в кластере из 5 узлов балансировщику clientv3-grpc1.23 потребуется 5 TCP-соединений, а clientv3-grpc1.7 — только одно. Сохраняя пул TCP-соединений, clientv3-grpc1.23 может потреблять больше ресурсов, но предоставляет более гибкую балансировку нагрузки и лучшее переключение при отказе. По умолчанию используется циклическая политика балансировки, которую легко расширить другими типами балансировщиков (например, выбором из двух, выбором лидера и т. п.). clientv3-grpc1.23 использует группу разрешителей gRPC и реализует политику выбора балансировщика, чтобы передать сложную балансировку вышестоящему gRPC. Напротив, clientv3-grpc1.7 вручную управляет каждым соединением gRPC и переключением балансировщика, что усложняет реализацию. clientv3-grpc1.23 реализует повторные попытки в цепочке перехватчиков gRPC, которая автоматически обрабатывает внутренние ошибки gRPC и поддерживает более сложные политики повторов, например отложенные повторы; clientv3-grpc1.7 вручную интерпретирует ошибки gRPC для повторных попыток.

client-balancer-figure-09.png

clientv3-grpc1.23: ограничение балансировщика

Работу можно улучшить, кэшируя состояние каждой конечной точки. Например, балансировщик может заранее проверять каждый сервер с помощью ping, поддерживая список исправных кандидатов, и использовать эти сведения при циклическом выборе. Либо при разрыве соединения отдавать приоритет исправным точкам. Это может усложнить реализацию балансировщика, поэтому улучшение можно оставить для последующих версий.

Клиентский ping keepalive всё ещё не учитывает разделения сети. Потоковый запрос может застрять на изолированном узле. Для понимания состава кластера необходимо реализовать расширенную службу проверки работоспособности (подробнее см. etcd#8673 ).

client-balancer-figure-07.png

Сейчас логика повторных попыток обрабатывается вручную перехватчиком. Её можно упростить с помощью официального механизма повторных попыток gRPC .