etcd deep dive - Client Model
etcd를 사용하는 개발자들은 etcd가 공식적으로 제공해주는 클라이언트를 사용해 서버에 접근하는 것이 일반적이다. Go로 만들어진 클라이언트를 잘 관리해주고 있어서 보통은 이 클라이언트를 쓰는 것 같다. 지금 진행하는 프로젝트 역시 Go 클라이언트를 통해 etcd에 접근한다. 이번 글은 클라이언트가 어떻게 발전해 왔는지를 알려주는 Learning의 글을 번역하고 공부했던 내용을 간단히 정리했다.
Requirements
클라이언트의 구현체는 다음과 같은 요구사항을 가지고 있다
- Correctness: 서버 실패와 상관없이 일관성 보장을 훼손하면 안 된다. 예를 들어 글로벌 순서, 손상된 데이터를 쓰지 않는 것, 최대 한 번(At Most Once) 동작하는 Mutable Operation, 부분적인 데이터를 Watch 하지 않는 등의 동작을 의미한다.
- Liveness: 서버는 간단히 실패하거나 연결이 끊어질 수 있는데, 클라이언트는 특별한 설정으로 통제하고 있는 게 없다면 문제 상황에서 서버가 다시 회복될 때를 기다리며 DeadLock이 발생하지 않도록 해야 한다.
- Effectiveness: 최소의 리소스를 사용해야 한다. 예를 들어 TCP 컨넥션은 엔드포인트가 교체되면 안전하게 정리되어야 한다.
- Portability: 공식적인 클라이언트의 형태가 문서화 되어있고, 여러 다른 언어에서 이를 구현할 수 있어야 한다. 또한 다른 언어들 사이의 에러 핸들링 방식이 일관되어야 한다.
요구사항의 많은 부분이 gRPC 클라이언트의 동작으로 인해 해결될 수도 있겠다고 생각했다.
Client Overview
클라이언트는 다음 세 가지 컴포넌트로 구성되어 있다.
- Balancer: etcd 클러스터와 gRPC 컨넥션을 만드는 컴포넌트
- API Client: RPC를 etcd 서버로 보내는 컴포넌트
- Error Handler: gRPC 에러를 처리하며 엔드포인트를 변경할 것인지, 재시도할 것인지를 결정하는 컴포넌트
API Client와 Error Handler의 경우는 v3로 오면서 큰 변화가 없었는지, 이하 내용에서는 밸런서의 변화에 관해서만 이야기한다. 밸런서의 변화는 클러스터 구성과 연결을 어떻게 할 것 인지를 결정할 때 도움을 줄 것 같다.
clientv3-grpc1.0
Overview
여러 엔드포인트 컨넥션을 유지하지만 하나의 엔드 포인트와 Pinned Connection
을 가정하고 통신한다. 즉, 모든 클라이언트의 요청을 하나의 클라이언트에 먼저 보내는 구조다. 이 상황에서 장애가 발생하면 밸런서는 다른 엔드포인트를 선택해서 연결하거나 재시도하게 된다.
Limitation
이렇게 구성하게 되면 여러 엔드포인트에 대해 연결을 유지하므로 더 빠른 FailOver(페일오버)가 가능하지만, 더 많은 리소스를 사용하고 있게 된다. 또한 밸런서가 노드의 상태나 클러스터 멤버십 상태에 대해 이해할 수 없기 때문에 Network Partition이 발생한 노드에 갇힐 수 있다.
clientv3-grpc1.7
Overview
여러 엔드포인트 중 하나의 엔드포인트에만 TCP 컨넥션을 구성한다. 처음 연결할 때 클라이언트는 주어진 모든 엔드포인트에 컨넥션을 시도하는데, 가장 빠르게 연결된 컨넥션이 나오면 해당 주소를 Pinned Connection
으로써 사용하고, 나머지 연결을 종료한다. Pinned
주소는 연결이 에러에 의해 닫힐 때까지 유지된다. 에러가 발생한다면 클라이언트의 에러 핸들러가 이를 받아 재시도할 수 있는 경우인지 아닌지 판단해서 다음 동작을 적절히 수행한다.
Stream RPC인 Watch
, KeepAlive
는 타임아웃 설정 없이 보내지는 경우가 많은데, 클라이언트는 HTTP/2 PING
을 통해 서버의 상태를 체크한다. 서버의 응답이 없으면 새로운 서버에 연결한다.
장애로 인해 Pinned Connection
상태에서 내려갔든, 모종의 이유로 새로운 연결을 시도하든 건강하지 않다고 판단된 엔드포인트는 Unhealthy List
에 등록된다. 만약 해당 리스트에 올라가게 되면 기본값으로 5초 동안은 해당 엔드포인트를 사용하지 않게 된다.
위 동작은
grpc-1.0
버전 당시에도 마찬가지로 있었다고 한다.
Limitation
grpc-1.0
버전처럼 여전히 멤버십 정보는 알지 못하므로 동일하게 네트워크 파티션을 판단 못 하는 문제가 발생할 수 있다. 그리고 밸런서가 잘못된 Unhealthy List
를 관리하는 문제가 있을 수 있다. 예를 들어 Unhealthy 마킹을 하고 나서 바로 서버가 정상화되었다면 5초 동안 건강한 서버를 사용하지 못하는 문제가 생긴다. 또한 Unhealthy로 관리되는 Recovery 과정이 gRPC의 Dial
을 사용하고 하드 코딩된 부분이 있어 굉장히 복잡한 구현체였다고 한다.
위 문제는 사실 이번 버전의 문제는 아니고 이전 버전과 동일한 구현으로 인해 생기는 문제이다. 이번 버전은 컨넥션을 위한 리소스 소비를 줄였지만, 페일오버의 속도를 느리게 만든다는 단점이 생긴다.
하위 버전과 비교했을 때 페일오버 문제는 트레이드 오프라고 생각된다.
clientv3-grpc1.23
Overview
grpc1.7
버전은 gRPC 인터페이스와 강하게 결합하여 gRPC 버전을 올릴 때마다 클라이언트의 동작이 망가지기 일쑤였고, 개발 과정의 많은 부분이 이를 호환되게 하는 수정이었다고 한다. 결과적으로 구현체는 복잡해지는 문제가 지속되었다.
개발 과정에서도 gRPC 메인테이너가 과거 버전의 인터페이스를 유지하지 말 것을 권장했다고 한다.
grpc1.23
으로 오면서 가장 주안점으로 둔 점은 밸런서의 페일오버 로직을 단순화하는 것이었다. Unhealthy List
를 관리하지 않고 현재 사용 중인 엔드포인트에 문제가 생기면 다른 엔드포인트로 라운드로빈 하도록 바꿨다. 이에 따라 복잡한 상태 체크가 필요 없어졌다.
다른 포인트는 gRPC의 인터페이스와 강결합하지 않도록 바꾸는 것이었다. 관련된 내용은 자세히 다루지 않는데, 내부적인 변화가 많이 있었다고 한다. 이로써 Backward Compatibility(하위 호환성)를 유지하면서 gRPC 업그레이드에 쉽게 깨지지 않도록 구성했다.
컨넥션 방법에도 변경이 생겼는데, 여러 엔드포인트가 주어졌을 때 클라이언트는 다시 여러 sub-connection
을 만드는 방법으로 바뀌었다. 한 서브 컨넥션은 각 엔드포인트를 의미하는 gRPC의 인터페이스(gRPC SubConn)이다. 5개의 노드가 있다면 5개의 TCP 컨넥션 풀을 만든다. 초기 버전에서 설명한 것처럼 TCP 컨넥션은 자원을 더 소모하지만, 더 유연한 페일오버가 가능해진다.
대신 grpc1.0
과는 다르게 Pinned Connection
을 사용하지 않고 모든 연결 포인트에 요청을 분산했다. 이로써 더 공평하게 부하를 분산할 수 있게 되었다. 기본적으로는 라운드 로빈을 사용하고 있는데 gRPC처럼 갈아 끼울 수 있다.
grpc1.7
과 High Level은 유사하지만, 내부적으로 gRPC의 기능을 많이 활용하고 있다. 밸런서는 gRPC의 resolver group
을 사용하고 Balancer Picker Policy를 구현해서 복잡한 동작을 gRPC가 수행하도록 위임했다. Retry의 경우도 gRPC의 인터셉터를 통해 처리함으로써 체인 안에서 자동으로, 그리고 보다 정교한 방법으로 처리되도록 했다.
과거 버전에서는 Retry 로직이 직접 구현되어 복잡한 부분이 있었다고 한다.
Limitation
이 버전은 현재 구현 상태이기 때문에, 앞으로 어떻게 발전시킬 것인지를 중점적으로 설명했다. 우선 현재 각 엔드포인트 상태를 캐싱함으로써 성능적 향상이 가능하다고 한다. 예를 들어 TCP 연결을 유지하기 때문에 PING
을 보내고 Health가 보장되는 앤드포인트를 우선하도록 만들 수 있다. 하지만 Unhealthy List
를 관리하는 것처럼 복잡도 증가 우려가 있기 때문에 논의가 더 필요한 주제라고 한다.
그리고 클라이언트 사이드의 KeepAlive PING
을 여전히 사용 중인데, 네트워크 파티션 등 클러스터 멤버십을 고려한 Health Check가 필요하다. (관련 논의)
마지막으로 Retry를 인터셉터에 의해 처리되도록 하고 있는데, 이후 공식적인 gRPC 스펙으로서 처리할 수도 있다.
아직 관련된 gRPC의 Retry는 Proposal 상태인 것 같다.
Reference
etcd deep dive - Client Model