본문 바로가기
Mobile/Android

[Android] gRPC keepAlive 적용기

by WooHey 2024. 5. 25.

문제상황

서비스 운영 중, 앱에서 실시간 데이터가 갱신이 되어야 하는 상황임에도 불구하고 실시간 정보가 제대로 갱신되지 않는 것 같다는 요구사항이 들어왔다.

해결과정

현재 우리는

Android 에서 gRPC Java 를 통해 server 와 통신 중에 있으며, StreamObserver 로 응답 값을 받아오고 있다.

당시 문제가 되는 rpc 는 Server Stream 이었다.

원인 분석

Q) 서버에서는 응답 값을 정상적으로 전달하였는가?

⇒ YES.

server 측 로그 분석 결과, client 로 정상적으로 응답 값을 전달을 시도한 것으로 확인했다. (하지만 client 에서 메시지를 받지 못하였음)

============근데 전송 당시에 실제 서버에 남은 로그가 존재했던가? 아니면 아무런 이상이 없었던가?

Q) 네트워크 불안정으로 인한 stream 연결이 단절된 것인가?

⇒ 네트워크 문제는 아닌 것 같음

그렇게 생각한 이유

  1. ConnectivityManager 를 통해 네트워크 상태를 바꿔가며 체크했을 때 특이사항을 발견할 수 없었다.
    1. wifi, LTE, 등 모두 확인
  2. 만약, 네트워크 이상으로 인해 stream 연결이 단절되었다면, StreamObserveronError() 가 트리거 되었어야 한다.

Q) 클라이언트에서 channel 생성 후 stream 을 연결한 시점으로부터 일정시간 아무런 message 를 송수신하지 않으면 channel 이 유휴상태로 들어간다?

postman 으로 했을 땐 10분 지나도 유휴상태로 안들어갔었음

proxy 또는 로드 밸런서에 의해 유후상태로 간주되었나? ⇒ 사실 잘 모르겠음..

증상 재현

우선, 증상 재현에 들어갔다.

실시간 데이터가 갱신되어야 하는 상황을 만들어서 데이터가 갱신이 되는지 확인했다.

하지만, 내 로컬 환경에서는 데이터 갱신이 정상적으로 잘 되었다. (재현 실패)

인입된 쪽에게 다시 물어보니, 데이터가 갱신되지 않는 현상이 아주 간헐적으로 발현된다고 전달 받았다.

그러던 중, server stream 을 열어 둔 채로 회의를 갔다 와서 다시 확인해보니 운영팀에서 말했던 현상과 동일한 현상이 재현되었었다. 즉, (1) 네트워크 연결이 끊겨있지 않고, (2) gRPC onError 가 발생하지 않았음에도 불구하고 server 로부터의 응답을 받을 수 없는 상황인 것이다.

최초에 재현한 조건과 현재 조건의 차이점에는 딱 한 가지가 존재했다.

stream 을 열어 둔 채 일정 시간이 지난 상태인 것이다.

본격적으로 증상 재현을 하기 위해 이것 저것 테스트를 진행했다.

클라이언트(Android) 에서 stream 을 열어둔 채 일정 시간(약 10~30분) 방치(여기서 방치라는 것은 stream 을 열어둔 채로 어떠한 메시지도 주고 받지 않은 상태를 말함) 한 후, 메시지를 전달했다.

그런데 동일 현상이 발생하지 않았다..?

이상하다 싶어서 다시 시도 했는데 위에서 말했던 동일한 현상(서버로부터 응답을 받을 수 없는 상황)이 발생했다.

요구사항이 인입된 쪽의 말처럼 정말 간헐적으로 발생하는 것 같다.

(이거 확실하지 않음, 지워야할 듯)여러 번 시도한 끝에 약 10분 정도 stream 을 열어둔 채로 방치했을 경우, 해당 현상이 발생하는 것으로 추측했다.

(이거도 확실하지 않음) (여담) Postman 의 gRPC client 로 테스트했을 때도 동일하게 재현되었다.

현재까지 확인된 사항

client 에서..

  • 네트워크는 정상
  • stream observer 에서 오류가 발생하지 않았고 rpc 가 종료되지 않았음
  • channel 이 닫히지 않았음
  • 문제가 발생한 시점, 모든 rpc(server stream rpc 를 포함한 현재 사용 중인 다른 모든 rpc)도 동일하게 메시지를 송수신 할 수 없는 상태였음. (채널이 유휴상태로 바뀐 듯함)
// client 측 로그

channel.isShutdown: false,
channel.isTerminated: false,
Context.current().isCancelled: false,
Context.current().deadline null,

server 에서..

  • client 로부터 cancel context 가 전달되지 않음
  • deadline 이 만료되지 않음
  • (이거 확실한가?) 해당 현상이 발생한 시점에, 클라이언트로 메시지를 보내니 client 와의 연결이 단절되었다는 로그를 확인
// server 측 로그

System.IO.IOException: The request stream was aborted.
 ---> Microsoft.AspNetCore.Connections.ConnectionAbortedException: The HTTP/2 connection faulted.
 ---> Microsoft.AspNetCore.Connections.ConnectionResetException: 현재 연결은 원격 호스트에 의해 강제로 끊겼습니다.
 ---> System.Net.Sockets.SocketException (10054): 현재 연결은 원격 호스트에 의해 강제로 끊겼습니다.

최종 조치

HTTP/2 연결에서 ping 기반의 keepAlive 를 구성하기로 했다. (https://grpc.io/docs/guides/keepalive/)

keepAlive 에 대한 잠깐 설명?

  • TCP keepalive 와는 다르며, 이것을 설정헀을 경우 중복 패킷을 보낼 수도 있음
  • server 에서는 keepalive 를 구성할 필요x
  • client 는 server 와 keepalive 를 구성할 지 협의해야 함
  • keepalive 는 주로 deadline 이 긴 rpc 에 대해 구성됨
  • keepalive 를 1분 미만으로 구성하는 것은 권장하지 않음
  • keepalive 서버 기본 값은 2시간

keepalive 가 유용한 상황

  • proxy 또는 로드 밸런서에 의해 유휴 상태로 간주될 수 있는 장기 연결을 통해 데이터를 전송하는 경우
  • 네트워크의 신뢰성이 떨어지는 경우(mobile application)
  • 장기간 사용하지 않은 상태에서 연결을 사용할 때

문제를 해결하기 위해 client(Android) 에서 Channel 생성 시, 아래 3가지 옵션을 추가하여 채널을 빌드했다.

  • keepAliveTime → 새로운 keepalive Ping을 보내기 전에 새 메시지를 기다리는 시간
  • keepAliveTimeout → 이 시간 내에 keepalive 응답이 수신되지 않으면 연결을 종료
  • keepAliveWithoutCalls → 아직 처리되지 않은 RPC가 없을 때 연결 유지를 수행할지 여부

결론

keepalive 를 설정하여 connection 을 유지시켰고, 실시간 데이터가 정상적으로 응답될 수 있도록 조치하였다.

더 이상 동일한 현상이 발생하지 않았다.

마무리

현재 시스템에서는 grpc stream 이 연결되면 client 측에서 의도적으로 rpc 를 cancel 하지 않는 이상, 계속해서 연결을 유지시키고 있다.

그 이유는, grpc server 에서는 redis 채널을 구독하고 있고, 구독 중인 채널의 상태가 변경되면 grpc server 에서 client 로 응답 메시지를 전달하는 형태인데, 그 상태가 언제 변경될 지 몰라서 계속 connection 을 유지시키고 있었다.

이에 따른 부하는 없는지, gRPC 기술이 우리 제품에 적합한 지, 제대로 검토를 해야 할 것 같다.

느낀점

로그를 남기는 중요성모니터링의 중요성에 대해 알게 되었다.

로그를 체계적으로 남겼고 모니터링을 확실하게 할 수 있었다면 이번 상황이 발생하더라도 발빠르게 대처할 수 있었지 않았을까 라는 생각이 들었다.

같은 상황이 발생하는 것을 줄이기 위해 ‘로그를 언제 어떤 방식으로 남기는 것이 좋을 것인가’ 에 대해서 고민을 해야겠다는 생각이 들었다.

참고