최근 본 면접에서 새로 알게 된 Exponential Back-Off 라는 개념과 Jitter를 사용한 개선 방법을 공부하고 정리한다. 그리고, Flutter 앱을 개발할 때 이 전략을 활용하기 위해 Dart code로 구현해 본다.
Retry Strategy
- 어떤 system에서 다른 system을 call하는 상황에서 failure는 언제든지 발생할 수 있다.
- 앱을 개발할 때는 server 부하 또는 일시적인 network 오류 등에 의해 http 요청이 오랜 시간 동안 완료되지 않거나 실패하는 상황을 떠올릴 수 있다.
- 이러한 일시적인 문제 때문에 client에서 server로 보내는 data가 유실될 수 있는데, 이것을 막기 위해 여러 가지 방법으로 retry 전략을 세운다.
- Request timeout을 설정하여 일정 시간 동안 응답이 없으면 작업을 취소하고 동일한 request를 retry하는 것이 대표적인 예시이다.
Exponential Back-Off
- 요청을 재시도할 때 가능한 또 다른 문제 중 하나는 연결이 정상화 되었을 때 server가 N개의 client로부터 동시다발적으로 요청을 수신하는 경우이다.
- Server가 한 번에 처리할 수 있는 양보다 훨씬 많은 수의 요청이 들어오면 server 과부하에 의해 data 유실 등의 문제가 여전히 발생할 수 있다.
- 이 문제를 해결하기 위해 요청을 분산시키는 방법으로 Exponential Back-Off(지수 백오프) 라는 방법을 사용한다.
- 말 그대로 backoff를 지수적으로 늘려가는 방법인데, retry 사이 시간 간격을 지수적으로 증가시키면서 더 많이 재시도 할 수록 더 나중에 요청을 보내도록 만든다.
- 이렇게 하면 server에 한 번에 들어오는 요청 수를 점진적으로 줄여나갈 수 있다.
Exponential Back-Off with Jitter
- 하지만 이 방법도 문제가 있는데, 여전히 특정 시점에 server로 요청이 몰리게 된다. Retry 간격이 상수이기 때문에 근본적인 원인을 해결하긴 어렵다.
- 특정 시점에 몰리는 call 횟수를 가능 한 고르게 분산시켜야 하는데, 그 방법 중 하나가 Jitter를 추가하는 것이다.
- Jitter란 ‘지연 변이’라는 뜻으로, 지수 백오프에 randomness를 추가하여 0에서 지수 back-off 값 사이에 랜덤한 값을 delay로 사용한다.
- 이렇게 하면 지수 back-off만 적용했을 때 보다 요청이 고르게 분포될 수 있다.
Implementation in Dart
Implementation of Exponential Back-Off
-
Exponential back-off는 Dart code로 구현해 보자. 아래는 exponential back-off delay를 계산하는 함수이다. Retry 횟수에 따라 delay가 2^n씩 증가한다.
int sleep({ required int base, // delay 초깃값 required int attempts, // retry 시도 횟수 }) { final exp = min(attempts, 31); // prevent overflow final sleep = base * pow(2, exp); return sleep.toInt(); }
-
이 값을 request에 실패했을 때 다음 retry까지의 delay 시간으로 사용한다. 아래는 위 함수가 반환하는 값 만큼 delay를 주며 지정한 횟수 만큼 retry를 하는 함수이다.
Future<T> request<T>({ required int maxAttempts, required int baseDelay, required FutureOr<T> Function() taskBuilder, FutureOr<void> Function(int retryCount)? willRetry, }) async { var attempts = 0; while (true) { attempts += 1; try { return await taskBuilder(); } on Exception { // 최대 시도 횟수를 초과하면 retry 종료 if (attempts >= maxAttempts) { rethrow; } await willRetry?.call(); final delay = min( _maxTimeout, sleep(attempts: attempts, base: baseDelay), ); final duration = Duration(milliseconds: delay); await Future.delayed(duration); } } }
Implementation of Exponential Back-Off with Jitter
-
아래는 exponential back-off를 구할 때 randomness를 추가하는 Dart code 이다.
int sleep({ required int base, required int attempts, }) { final exp = min(attempts, 31); final sleep = base * pow(2, exp); return _random.nextInt(sleep.toInt()); // 0부터 sleep 사이의 random 값 }
Packages
- Dart에서 exponential back-off를 통한 retry를 구현하는 package는 두 가지 정도 찾아볼 수 있었다. Randomization 구현 방식이 각각 다르기 때문에 상황에 맞게 선택해서 사용해도 좋을 것 같다.
- retry : Google에서 관리하는 package로,
randomizationFactor
값을 설정하여 exponential back-off 값을 중심으로 -n% ~ +n% 사이의 값을 사용하도록 유연하게 설정할 수 있다는 장점이 있다. - exponential_back_off : 개인 개발자가 관리하는 package로,
maxRandomizationFactor
값을 설정하여 exponential back-off 값을 중심으로 0% ~ n% 사이의 값을 사용하도록 설정할 수 있다.
Conclusion
- 실패가 없는 system을 만들 수는 없다. 실패했을 때 빠르게 잘 회복할 수 있는 system을 만드는 것이 중요하다.
- 지금까지 개발했던 앱에서는 retry 전략을 고려할 만큼 문제가 됐던 적이 없었지만, 사용자가 많아지고 server에 보내는 event들이 많아지면 이런 전략이 꼭 필요할 것 같다. 클라우드 서비스를 사용하는 경우 client에서 무분별하게 보내는 과도한 요청은 서비스의 안정성을 떨어뜨리고 비용 문제까지 갈 수도 있기 때문이다.
- Exponential backoff는 retry 전략을 위한 방법들 중 하나이지, 가장 좋은 방법이 아니다. Exponential backoff는 jitter를 추가하더라도 latency가 증가하는 등의 문제가 여전히 발생할 수 있다. 상황에 맞는 적절한 retry 전략을 구현해서 사용할 수 있어야 하겠다.