What Is gRPC? How to Work With APISIX?
September 28, 2022
gRPC란 무엇인가
gRPC는 Google이 오픈소스로 공개한 RPC 프레임워크로, 서비스 간 통신 방식을 통일하는 것을 목표로 합니다. 이 프레임워크는 HTTP/2를 전송 프로토콜로, Protocol Buffers를 인터페이스 설명 언어로 사용합니다. 이를 통해 서비스 간 호출을 위한 코드를 자동으로 생성할 수 있습니다.
gRPC의 지배력
gRPC는 Google의 개발자 및 클라우드 네이티브 환경에 대한 탁월한 영향력 덕분에 RPC 프레임워크의 표준이 되었습니다.
etcd 기능을 호출하고 싶나요? gRPC!
OpenCensus 데이터를 전송하고 싶나요? gRPC!
Go로 구현된 마이크로서비스에서 RPC를 사용하고 싶나요? gRPC!
gRPC의 지배력은 매우 강력하여, gRPC를 RPC 프레임워크로 선택하지 않았다면 그 이유를 명확히 설명해야 할 정도입니다. 그렇지 않으면 누군가 항상 왜 주류인 gRPC를 선택하지 않았는지 물어볼 것입니다. 심지어 Alibaba도 자신들의 RPC 프레임워크인 Dubbo를 적극적으로 홍보했음에도 불구하고, 최신 버전인 Dubbo 3에서는 프로토콜 설계를 크게 수정하여 gRPC와 Dubbo 2 모두와 호환되는 gRPC 변형으로 변경했습니다. 사실, Dubbo 3이 Dubbo 2의 업그레이드라기보다는 gRPC의 우위를 인정한 것에 가깝습니다.
gRPC를 제공하는 많은 서비스들은 해당 HTTP 인터페이스도 제공하지만, 이러한 인터페이스는 주로 호환성을 위한 것입니다. gRPC 버전이 훨씬 더 나은 사용자 경험을 제공합니다. gRPC를 통해 접근할 수 있다면 해당 SDK를 직접 가져올 수 있습니다. 일반 HTTP API만 사용할 수 있다면 보통 문서 페이지로 안내되며, 해당 HTTP 작업을 직접 구현해야 합니다. HTTP 접근은 OpenAPI 사양을 통해 해당 SDK를 생성할 수 있지만, HTTP는 낮은 우선순위이기 때문에 gRPC만큼 HTTP 사용자를 진지하게 여기는 프로젝트는 거의 없습니다.
gRPC를 사용해야 할까요
APISIX는 etcd를 구성 중심으로 사용합니다. v3부터 etcd는 인터페이스를 gRPC로 마이그레이션했습니다. 그러나 OpenResty 생태계에서는 gRPC를 지원하는 프로젝트가 없기 때문에 APISIX는 etcd의 HTTP API만 호출할 수 있습니다. etcd의 HTTP API는 gRPC-gateway를 통해 제공됩니다. 본질적으로 etcd는 서버 측에서 HTTP를 gRPC로 변환하는 프록시를 실행하고, 외부 HTTP 요청은 gRPC-gateway를 통해 gRPC 요청으로 변환됩니다. 이러한 통신 방식을 몇 년간 배포한 결과, HTTP API와 gRPC API 간의 상호작용에서 몇 가지 문제를 발견했습니다. gRPC-gateway가 있다고 해서 HTTP 접근이 완벽하게 지원되는 것은 아닙니다. 여전히 미묘한 차이가 있습니다.
지난 몇 년간 etcd와 관련하여 겪은 문제 목록은 다음과 같습니다:
- gRPC-gateway가 기본적으로 비활성화됨. 유지보수자의 실수로 인해 etcd의 기본 구성이 일부 프로젝트에서 gRPC-gateway를 활성화하지 않았습니다. 그래서 문서에 현재 etcd가 gRPC-gateway를 활성화했는지 확인하는 지침을 추가해야 했습니다. https://github.com/apache/apisix/pull/2940 참조.
- 기본적으로 gRPC는 응답을 4MB로 제한합니다. etcd는 제공하는 SDK에서 이 제한을 제거했지만 gRPC-gateway에서는 제거하는 것을 잊었습니다. 공식 etcdctl(SDK 기반)은 잘 작동하지만 APISIX는 그렇지 않았습니다. https://github.com/etcd-io/etcd/issues/12576 참조.
- 동일한 문제 - 이번에는 동일한 연결에 대한 최대 요청 수입니다. Go의 HTTP2 구현에는 단일 클라이언트가 동시에 보낼 수 있는 요청 수를 제어하는
MaxConcurrentStreams
구성이 있으며, 기본값은 250입니다. 어떤 클라이언트가 일반적으로 동시에 250개 이상의 요청을 보낼까요? 그래서 etcd는 항상 이 구성을 사용했습니다. 그러나 모든 HTTP 요청을 로컬 gRPC 인터페이스로 프록시하는 "클라이언트"인 gRPC-gateway는 이 제한을 초과할 수 있습니다. https://github.com/etcd-io/etcd/issues/14185 참조. - etcd가 mTLS를 활성화한 후, etcd는 서버 인증서와 클라이언트 인증서로 동일한 인증서를 사용합니다. gRPC-gateway의 서버 인증서와 gRPC 인터페이스에 접근할 때의 클라이언트 인증서로 사용됩니다. 인증서에 서버 인증 확장이 활성화되어 있지만 클라이언트 인증 확장이 활성화되어 있지 않으면 인증서 검증에서 오류가 발생합니다. 다시 한번, etcdctl로 직접 접근하면 잘 작동하지만(이 경우 인증서가 클라이언트 인증서로 사용되지 않음) APISIX는 그렇지 않습니다. https://github.com/etcd-io/etcd/issues/9785 참조.
- mTLS를 활성화한 후, etcd는 인증서의 사용자 정보에 대한 보안 정책 구성을 허용합니다. 위에서 언급했듯이, gRPC-gateway는 gRPC 인터페이스에 접근할 때 고정된 클라이언트 인증서를 사용하며, 처음에 HTTP 인터페이스에 접근할 때 사용된 인증서 정보를 사용하지 않습니다. 따라서 클라이언트 인증서가 고정되어 변경되지 않기 때문에 이 기능은 자연스럽게 작동하지 않습니다. https://github.com/apache/apisix/issues/5608 참조.
이 문제들을 두 가지로 요약할 수 있습니다:
- gRPC-gateway(및 HTTP를 gRPC로 변환하려는 다른 시도)는 모든 문제를 해결하는 만능 해결책이 아닙니다.
- etcd 개발자들은 HTTP 방식에 충분히 중점을 두지 않습니다. 그리고 그들의 가장 큰 사용자인 Kubernetes는 이 기능을 사용하지 않습니다.
여기서 특정 소프트웨어의 문제를 논하는 것이 아니라, etcd는 gRPC를 사용하는 전형적인 예일 뿐입니다. gRPC를 주요 RPC 프레임워크로 사용하는 모든 서비스는 HTTP 지원에 있어서 유사한 제한이 있습니다.
APISIX 3.0은 이 문제를 어떻게 해결했나요
"산이 무함마드에게 오지 않으면 무함마드가 산으로 가야 한다"는 말이 있습니다. OpenResty에서 gRPC 클라이언트를 구현하면 gRPC 서비스와 직접 통신할 수 있습니다.
작업량과 안정성을 고려하여, 우리는 바퀴를 다시 발명하지 않고 일반적으로 사용되는 gRPC 라이브러리를 기반으로 개발하기로 결정했습니다. 우리는 다음과 같은 gRPC 라이브러리를 검토했습니다:
- NGINX의 gRPC 서비스. NGINX는 gRPC를 외부 사용자에게 노출하지 않으며, 고수준 API도 제공하지 않습니다. 사용하려면 몇 가지 저수준 함수를 복사한 후 고수준 인터페이스로 통합해야 합니다. 이를 통합하면 추가 작업량이 발생합니다.
- 공식 C++ gRPC 라이브러리. 우리 시스템은 NGINX 기반이기 때문에 C++ 라이브러리를 통합하는 것이 약간 복잡할 수 있습니다. 또한 이 라이브러리의 의존성은 약 2GB에 가까워 APISIX의 구축에 큰 도전이 될 것입니다.
- 공식 Go 구현의 gRPC. Go는 강력한 도구 체인을 가지고 있으며, 프로젝트를 빠르게 구축할 수 있습니다. 그러나 안타깝게도 이 구현의 성능은 C++ 버전에 비해 훨씬 떨어집니다. 그래서 우리는 또 다른 Go 구현을 살펴보았습니다: https://github.com/bufbuild/connect-go/. 불행히도 이 프로젝트의 성능도 공식 버전보다 나은 것은 아니었습니다.
- Rust 구현의 gRPC 라이브러리. 이 라이브러리는 의존성 관리와 성능을 결합하는 자연스러운 선택이 될 것입니다. 그러나 우리는 Rust에 익숙하지 않아 이를 내기에 걸 수 없었습니다.
gRPC 클라이언트의 작업은 기본적으로 모두 IO 바운드이기 때문에 성능 요구 사항은 주요하지 않습니다. 신중히 고려한 후, 우리는 Go-gRPC를 기반으로 구현하기로 결정했습니다.
Lua의 코루틴 스케줄러와 조율하기 위해, 우리는 NGINX C 모듈을 작성했습니다: https://github.com/api7/grpc-client-nginx-module. 처음에는 cgo를 통해 Go 코드를 정적으로 링크된 라이브러리로 컴파일하여 이 C 모듈에 통합하려고 했지만, Go는 다중 스레드 애플리케이션이기 때문에 포크 후 자식 프로세스가 부모 프로세스의 모든 스레드를 상속하지 않는다는 것을 발견했습니다. NGINX의 마스터-워커 다중 프로세스 아키텍처에 적응할 방법이 없었습니다. 그래서 우리는 Go 코드를 동적 링크 라이브러리(DLL)로 컴파일한 후 런타임에 워커 프로세스에 로드했습니다.
우리는 Go의 코루틴과 Lua의 코루틴을 조율하기 위해 작업 큐 메커니즘을 구현했습니다. Lua 코드가 gRPC IO 작업을 시작하면 Go 측에 작업을 제출하고 자신을 일시 중단합니다. Go 코루틴이 이 작업을 실행하고 실행 결과를 큐에 기록합니다. NGINX 측의 백그라운드 스레드가 작업 실행 결과를 소비하고 해당 Lua 코루틴을 재스케줄링하여 Lua 코드를 계속 실행합니다. 이렇게 하면 gRPC IO 작업은 Lua 코드의 관점에서 일반 소켓 작업과 다르지 않습니다.
이제 NGINX C 모듈의 대부분의 작업이 완료되었습니다. 우리가 해야 할 일은 etcd의 .proto
파일(gRPC 인터페이스를 정의하는)을 가져와 수정한 후 Lua에서 파일을 로드하여 다음과 같은 etcd 클라이언트를 얻는 것입니다:
local gcli = require("resty.grpc")
assert(gcli.load("t/testdata/rpc.proto"))
local conn = assert(gcli.connect("127.0.0.1:2379"))
local st, err = conn:new_server_stream("etcdserverpb.Watch", "Watch",
{create_request =
{key = ngx.var.arg_key}},
{timeout = 30000})
if not st then
ngx.status = 503
ngx.say(err)
return
end
for i = 1, (ngx.var.arg_count or 10) do
local res, err = st:recv()
ngx.log(ngx.WARN, "received ", cjson.encode(res))
if not res then
ngx.status = 503
ngx.say(err)
break
end
end
이 gRPC 기반 구현은 Lua만으로 1600줄의 코드를 가진 etcd-HTTP 클라이언트 프로젝트인 lua-resty-etcd보다 훨씬 낫습니다.
물론, lua-resty-etcd를 대체하기까지는 아직 갈 길이 멉니다. etcd와 완전히 연결하기 위해 grpc-client-nginx-module은 다음 기능도 완료해야 합니다:
- mTLS 지원
- gRPC 메타데이터 구성 지원
- 매개변수 구성 지원(예:
MaxConcurrentStreams
및MaxRecvMsgSize
) - L4 요청 지원
다행히 우리는 기반을 마련했고, 이러한 것들을 지원하는 것은 당연한 일입니다.
grpc-client-nginx-module은 APISIX 3.0에 통합될 예정이며, APISIX 사용자는 APISIX 플러그인에서 이 모듈의 메서드를 사용하여 gRPC 서비스와 직접 통신할 수 있습니다.
gRPC에 대한 네이티브 지원으로 APISIX는 더 나은 etcd 경험을 얻을 수 있으며, gRPC 건강 검사 및 gRPC 기반 오픈 텔레메트리 데이터 보고와 같은 기능의 가능성을 열어줍니다.
우리는 앞으로 APISIX의 더 많은 gRPC 기반 기능을 기대합니다!