Using gPRC for long-lived and Streaming RPCs
발표자: Eric Anderson - gRPC Java 팀 tech lead. Google에서 근무
발표자료: https://kccna18.sched.com/event/GrWo
발표영상: https://youtu.be/Naonb2XD_2Q
Long-Lived gRPC란?
- RPCs that last minutes / hours/ days.
- Long polling (Hanging Get). 예컨대 서버에서 특정 이벤트가 발생할 때까지 대기하는 로직.
- 강연에서는 Notification / Watches를 예시로 들었음.
- Hanging Get : 서버에서 요청에 응답할 데이터가 없는 경우, 해당 데이터를 사용할 수 있게 될 때까지 요청을 유지하는 것
Issues
- Load Balancing
- 기본적으로 LB는 개별 RPC 기준으로 동작함.
- RPC를 호출하는 시점에서 load balancing이 동작하는데, 그 RPC가 Long-lived인 경우 - Load balancing Decision도 지속된다.
- long-lived RPC가 누적될 경우, 서버에서 new RPC Connection을 더 이상 생성할 수 없어서 통신이 안 될 수 있음. (TCP Connection fails)
- MAX_CONNECTION_AGE 설정으로 시간초과된 connection을 shut down할 수는 있으나, 이 설정 자체는 kill connection이 아니라 connection에 wait time limit을 설정하는 것.
- Network Failure.
- 네트워크 이슈가 생기면 Long-lived RPC 요청은 실패함. 서버에 문제가 있어도 통신 실패할 수 있음.
- TCP Connection은 문제가 생기면 그냥 끊어짐. Migration 기능 같은 거 없음
- Network 쪽 이슈는 감지될 때까지 시간이 필요함.
- 네트워크 이슈로 latency가 길어질 수 있다 보니, Unary Deadline을 실제 기댓값보다 더 크게 잡아야 함.
Load balancing에서의 Improvement 방법
- server에서 RPC close call을 실행한다. new connection을 생성하면 load balancer가 다시 동작할 수 있음.
- MAX_CONNECTION_AGE_GRACE 옵션을 사용해서 how long you let this RPCs last를 설정할 수 있음.
- MAX_CONNECTION_AGE 값이 초과되면, MAX_CONNECTION_AGE_GRACE 에서 설정한 만큼의 시간이 추가로 부여된 후 connection이 종료됨. 예컨대 5 minutes로 설정하면, MAX_CONNECTION_AGE 시간이 지난 뒤 5분의 추가시간이 주어지는 것.
- 다른 RPC Call 종료 / 취소 방법이 있지만, 일종의 backup system으로 적용할 수 있음.
- Network Failure : client side의 keep-alive 옵션을 사용한다. RPC connection call 했을 때 응답이 없으면 자동으로 connection kill + notification을 제공함
- Wait-for-ready 옵션
- RPC 통신을 시도했을 때 no connectivity -> fail 발생할 경우, retrying the connect until the RPC connection re-established.
Streaming RPC
- unary는 응답받는 메시지 개수가 1개이지만, Streaming은 0-to-Many Messages.
- 응답으로 오는 메시지 순서는 정렬되어 있음. n번째 메시지를 받았다면, 직전의 메시지는 n-1번째 메시지.
유의점: streaming으로 메시지를 보낼 때 Metadata는 headers / trailers에만 존재한다. 중간에 들어오는 Message에서는 metadata를 확인할 수 없음.
양쪽이 전부 Stream으로 데이터를 전송할 경우 (Bi-directional)
- Half duplex: 한 번에 하나의 Client만 서버와 bi-directional streaming 통신이 가능함. client Send -> server Respond.
- Full duplex
- TCP와 개념적으로 유사하지만 bytes 대신 messages를 전달함. close 방식은 tcp와 다소 다르지만, 여기서는 다루지 않음
- client send -> server respond 방식이 아니기 때문에, 'ack' 개념이 없음. 메시지를 수신했다 != 요청에 응답했다.
half-duplex
- API latency / memory reduction. ex) STT
- 문장 단위, 혹은 더 작은 chunk 단위의 message로 나눠서 streaming 방식으로 요청 / 응답
- 서버 입장에서는 모든 문장을 input으로 받을 때까지 대기하는 게 아니므로 latency / memory 단축.
- Response와 End of call의 구분 가능. unary의 경우 '요청 - 응답' 프로세스가 끝나면 더 이상 뭔가를 전달할 수 없지만, streaming은 계속 데이터를 전달할 수 있음.
- Flow Control (push-back)
- 'sender의 데이터 전송속도 > server의 데이터 처리속도' 인 경우, server가 처리할 수 있는 정도로 sender의 전송속도 조절이 가능함.
- bulk
uploaddownload의 경우 유용함.- 기존의 chunk 단위로 데이터 전송 -> 업로드 완료 -> 다음 데이터 chunk 전송... 하는 방식에서는 Latency 발생 / bandwidth 낭비 가능성이 높음.
- streaming 방식으로 message를 전달할 경우 server / network에서 처리 가능한 속도로 전달 가능 + chunk 단위를 세세하게 고려할 필요가 없어짐.
streaming으로 전달되는 데이터에서 '순서'에 의미가 있으므로, state 정보를 담은 데이터 전송이 가능함.
- 한 번 streaming 방식으로 연결 -> 해당 데이터는 전부 동일한 backend Server로 전송.
- call의 lifecycle이 길어짐 (Request -> response 단위보다 확장됨. 예컨대 transaction). 여러 query를 하나의 Streaming 단위로 보낼 경우 가능함. kill the call == kill the transaction.
- 서버에서 계속 데이터를 전달해야 할 경우... setup cost 감소
- state change를 stream message에 포함하는 것도 가능. (watches again...)
- stateless 방식의 경우 '이전 상태가 무엇인지'를 recovery token에 포함하거나 하는 등 부가작업이 필요함
- streaming으로 연결된 경우 message에 정보 포함하기만 하면 됨 -> client - server 간 sync 비용이 적게 든다.
Issues
단순히 gRPC에서 streaming 기능을 제공하고 있고, 그게 좋아보여서 사용할 거면 반대. 고려해야 할 Complexity는 증가한다. (unary의 경우는 괜찮음)
- streaming에서 flow control에 사용하는 buffer의 크기는 64KB - 4MB. 작은 데이터를 전송할 거라면 buffer 크기 낭비
- gRPC의 Flow control 단위는 Point to point. 만약 gRPC 통신 중간에 new proxy가 들어오거나 하면, proxy에서 필요로 하는 buffer가 또 추가될 수 있음.
- buffer 크기만 MB 단위로 커질 수 있음. (물론 network 세팅과 상태에 따라 실제 buffer 크기는 다를 수 있으나, buffer 단위가 커질 가능성이 높다는 건 염두해야 함)
- API Implementation에서의 Complexity 증가. (이건 구현에 사용한 언어에 따라 정도가 다를 수 있음)
- streaming이 framework-Level에서 정의된 retry level로는 제대로 동작하지 않을 수 있음. application-level로 retry 설정을 해줘야 함
- specialized logic이 추가로 필요할 수 있음.
- Tracing / Stats 기능이 streaming을 완벽히 지원한다고 보장할 수 없음. muddled / missing 가능성 있음.
half-duplex에서 Flow Control 관련 이슈 발생 시
- full duplex 사용 권장 + application-level flow control 추가해야 함
tracing / stats 이슈 발생: streaming으로 발생하는 메시지 전체를 하나의 unary처럼 간주해도 괜찮음
- streaming으로 발생한 5 messages -> 전부 합쳐서 하나의 big unary message처럼 취급하는 것
- watches처럼 streaming message 전체를 Unary 취급할 수 없는 경우도 있긴 한데.. 대부분의 경우 approximation으로는 나쁘지 않다고 함
full duplex
- 본질적으로 TCP with Message... Custom protocol 생성 가능.
- Application-level Flow Control 정의 가능
- Transaction 처리에 용이함.
- request로 Query를 계속 전달 -> response로 계속 응답. 이런 flow 전체가 하나의 stream에서 이루어지므로.
- client가 죽거나 connection이 끊기거나 cancel요청할 경우 요청 전체를 취소할 수 있음 (rollback처럼)
- Live Reconfiguration 가능. 사용자의 요청을 on-the-fly로 처리 가능 (서버에서 요청을 즉각적으로 반영해서 전달)
- bulk upload에서 유용함.
- large bulk upload의 경우 resumption이 필요함.
- 업로드할 때, Resumption key를 클라이언트가 응답으로 받아서 다음 업로드에 사용해야 함
- 이 키값을 가지고 있다면, upload RPC가 종료되거나 중단되었을 때 resumption Key를 가지고 new RPC connection을 생성하면 이어서 업로드 가능
- 읿반적으로 resumption key는 constant한 값으로 쓰이는데, full duplex를 적용할 경우 constant하지 않은 key값을 사용할 수 있음.
Issues
- half duplex와 마찬가지로 tracing / stats system이 완전하지 않음.
- infinite data transfer가 가능한 두 개의 streaming -> API 복잡도 증가.
- Application-Level Retry 로직이 반드시 있어야 함. (custom logic 대응을 위해서는 프레임워크에서 pre-defined된 걸 쓰기도 어려움. 직접 로직을 정의해야 함)
- Flow Control 과정에서 Deadlock 발생할 수 있음.
- Full duplex Streaming은 REST 철학에 마땅히 대응되는 게 없음.
자료화면에서 * 표시가 붙은 건 뾰족한 해결방법이 없다는 뜻임. Deal With it.
- 그래도 Deadlock 이슈는 '반드시, server / client 최소 한 쪽은 receiving 상태여야 한다'는 원칙을 지키면 해결할 수 있다. 이 원칙만 확실하다면 Flow system이 동작하면서 종래에는 괜찮아진다.
- deadlock이 발생하는 요인 중 하나는 '양쪽 다 데이터 sending만 하고, receiving을 어느 쪽도 하고 있지 않을 때' 이기 때문.
Long-lived Streaming Issues
- transaction case, all messages are implying a lot of load. 이 경우 load balancing에 cpu / memory 리소스 사용량이 많으므로 조심해야 함
- 단순히 long-lived RPC의 경우 load balancing에 리소스 사용량이 많진 않음
Long-lived RPC의 해결방안과 크게 다르진 않다고 함
- RPC가 너무 길어지지 않도록 finish + clean, make another load balancing decision...
Q&A
Max Concurrenct streams 옵션이 구글 api에서는 기본 100으로 설정되어 있는 걸로 알고 있다. 꽤 작은 값인데, multiple stream with lots of clients / one server Application 에서 적정한 값은 얼마쯤이라 생각하는지 궁금하다.
- max concurrent stream은 http/2 spec으로, 기본적으로는 infinite임. 하지만 proxy에서 보통 infinite보다는 작은 값으로 설정한다.
- Long-lived concurrenct RPC 세팅할 때는 order of hundreds / thousands (대충 몇백 / 몇천 정도) 적용하면 대략 된다.
Flow Control의 back pressure handling이 자동인지 또는 관리해줘야 하는지
- 언어마다 다름. API feature. 찾아보는 것을 권장
gRPC wire debugging tool로는 뭘 쓰는지? long-lived streaming의 complex case인 경우
- wire 레벨에서 보는 거 같진 않고, events 단위로 보는 듯 - which messages are received. http2 레벨에서 해결되는 걸 디버깅하기 위해 http2 전문가가 되어야 할 필요까지는 없는 듯함
- deadlock의 경우 window size 체크하고 비교하는 등의 작업이 필요할 수는 있어보인다.
- http2 frames을 확인할 수 있는 debug flag이 있다. 지원하는 언어가 있고 아닌 언어가 있긴 한데... gRPC의 Logging이나 flag 추가해서 frame logging하는 정도. 필요하다면 앞서 언급한 Tracing / stats도 괜찮더라.
'학습일지 > 네트워크' 카테고리의 다른 글
우아콘 2024 - 배달의민족 API Gateway (2) | 2024.12.18 |
---|---|
KubeCon 2019 - The story of Why we migrate to gRPC and How we go about it (Spoti (0) | 2022.10.12 |
Deview 2021 - NAVER 암호화 트래픽을 책임지는 HTTPS 플랫폼 기술 - 2. 활용 (0) | 2022.06.13 |
Deview 2021 - NAVER 암호화 트래픽을 책임지는 HTTPS 플랫폼 기술 - 1. 기술 (0) | 2022.06.11 |
gRPC (4) - Client Streaming 개념 및 예제코드 (0) | 2021.08.16 |