23. 요청 데드라인 전파 부재와 재시도 증폭 (C#)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// [시나리오]
// 게임 백엔드는 게이트웨이 → 랭킹 서비스 → (통계 서비스, 프로필 서비스, DB) 로 이어지는
// 다단계 호출 체인을 갖는다. 클라이언트는 랭킹 화면을 열 때 게이트웨이에 요청하고,
// 게이트웨이는 5초 안에 응답하지 못하면 클라에 타임아웃을 반환한다.
//
// 각 서비스는 자기 나름의 타임아웃과 재시도 정책을 갖고 있다.
// 게이트웨이 : 전체 5초, 재시도 2회
// 랭킹 서비스 : 하위 호출당 3초, 재시도 3회
// 통계 서비스 : DB 쿼리 10초
//
// 운영 중 보고된 현상:
// - 통계 서비스가 느려지면 게이트웨이는 이미 타임아웃으로 클라에 실패를 반환했는데,
// 하위 서비스들의 CPU/DB 부하는 오히려 계속 올라간다.
// - 부하가 걷히지 않고 오래 지속되며, 회복이 느리다.
// - 랭킹 서비스 로그에는 "완료"된 요청이 게이트웨이 로그의 요청 수보다 훨씬 많다.
//
// [요구사항]
// - 클라이언트가 더 이상 기다리지 않는 작업은 하위 계층에서도 중단되어야 한다.
// - 한 요청의 총 소요 시간은 체인 전체에 걸쳐 상한이 있어야 한다.
//
// [과제]
// (A), (B) 지점을 중심으로 위 현상이 발생하는 조건을 찾고, 재현 시나리오와 수정안을
// 제시하시오.
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace GameServer.Distributed
{
// ---- 게이트웨이 ----
public sealed class GatewayHandler
{
private readonly RankingClient _ranking = new();
public async Task<string> GetRankingAsync(long accountId)
{
// (A)
for (int attempt = 0; attempt < 3; attempt++)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
return await _ranking.FetchAsync(accountId, cts.Token);
}
catch (OperationCanceledException)
{
// 재시도
}
}
throw new TimeoutException("ranking unavailable");
}
}
public sealed class RankingClient
{
private static readonly HttpClient Http = new();
public async Task<string> FetchAsync(long accountId, CancellationToken ct)
{
var req = new HttpRequestMessage(HttpMethod.Get, $"http://ranking/v1/{accountId}");
var res = await Http.SendAsync(req, ct);
return await res.Content.ReadAsStringAsync(ct);
}
}
// ---- 랭킹 서비스 ----
public sealed class RankingService
{
private readonly StatsClient _stats = new();
private readonly ProfileClient _profile = new();
public async Task<string> HandleAsync(long accountId)
{
// (B)
var statsTask = CallWithRetryAsync(
ct => _stats.GetAsync(accountId, ct), retries: 3, timeout: TimeSpan.FromSeconds(3));
var profileTask = CallWithRetryAsync(
ct => _profile.GetAsync(accountId, ct), retries: 3, timeout: TimeSpan.FromSeconds(3));
var stats = await statsTask;
var profile = await profileTask;
return Combine(stats, profile);
}
private static async Task<string> CallWithRetryAsync(
Func<CancellationToken, Task<string>> call, int retries, TimeSpan timeout)
{
for (int i = 0; i < retries; i++)
{
using var cts = new CancellationTokenSource(timeout);
try { return await call(cts.Token); }
catch (OperationCanceledException) { }
catch (HttpRequestException) { }
}
throw new TimeoutException();
}
private static string Combine(string a, string b) => a + b;
}
public sealed class StatsClient
{
public Task<string> GetAsync(long id, CancellationToken ct) => Task.FromResult("");
}
public sealed class ProfileClient
{
public Task<string> GetAsync(long id, CancellationToken ct) => Task.FromResult("");
}
} 결함 코드 · C++
// [시나리오]
// C++ 게임 백엔드의 다단계 호출 체인: 게이트웨이 → 랭킹 서비스 → (통계 서비스, 프로필 서비스).
// 클라이언트는 랭킹 화면을 열 때 게이트웨이에 요청하고, 게이트웨이는 5초 안에 응답하지
// 못하면 클라에 타임아웃을 반환한다.
//
// 각 서비스의 정책:
// 게이트웨이 : 전체 5초, 재시도 2회
// 랭킹 서비스 : 하위 호출당 3초, 재시도 3회
// 통계 서비스 : DB 쿼리 10초
//
// 운영 중 보고된 현상:
// - 통계 서비스가 느려지면 게이트웨이는 이미 클라에 실패를 반환했는데도 하위 서비스의
// CPU/DB 부하가 계속 올라간다.
// - 부하가 걷히지 않고 회복이 느리다.
// - 랭킹 서비스가 처리한 요청 수가 게이트웨이 요청 수보다 훨씬 많다.
//
// [요구사항]
// - 클라이언트가 더 이상 기다리지 않는 작업은 하위 계층에서도 중단되어야 한다.
// - 한 요청의 총 소요 시간은 체인 전체에 걸쳐 상한이 있어야 한다.
//
// [과제]
// (A), (B) 지점을 중심으로 위 현상이 발생하는 조건을 찾고, 재현 시나리오와 수정안을
// 제시하시오.
#include <chrono>
#include <cstdint>
#include <future>
#include <stdexcept>
#include <string>
namespace gs {
using Clock = std::chrono::steady_clock;
class TimeoutError : public std::runtime_error {
public:
TimeoutError() : std::runtime_error("timeout") {}
};
// 지정한 타임아웃 안에 끝나지 않으면 TimeoutError 를 던진다.
template <class F>
auto callWithTimeout(F&& fn, std::chrono::milliseconds timeout) -> decltype(fn()) {
auto fut = std::async(std::launch::async, std::forward<F>(fn));
if (fut.wait_for(timeout) != std::future_status::ready) throw TimeoutError();
return fut.get();
}
class StatsClient { public: std::string get(std::int64_t id); };
class ProfileClient { public: std::string get(std::int64_t id); };
class RankingClient { public: std::string fetch(std::int64_t id); };
// ---- 게이트웨이 ----
class GatewayHandler {
public:
std::string getRanking(std::int64_t accountId) {
// (A)
for (int attempt = 0; attempt < 3; ++attempt) {
try {
return callWithTimeout([&] { return ranking_.fetch(accountId); },
std::chrono::seconds(5));
} catch (const TimeoutError&) {
// 재시도
}
}
throw TimeoutError();
}
private:
RankingClient ranking_;
};
// ---- 랭킹 서비스 ----
class RankingService {
public:
std::string handle(std::int64_t accountId) {
// (B)
std::string stats = callWithRetry([&] { return stats_.get(accountId); }, 3,
std::chrono::seconds(3));
std::string profile = callWithRetry([&] { return profile_.get(accountId); }, 3,
std::chrono::seconds(3));
return stats + profile;
}
private:
template <class F>
auto callWithRetry(F&& fn, int retries, std::chrono::milliseconds timeout) -> decltype(fn()) {
for (int i = 0; i < retries; ++i) {
try { return callWithTimeout(fn, timeout); }
catch (const TimeoutError&) {}
catch (const std::runtime_error&) {}
}
throw TimeoutError();
}
StatsClient stats_;
ProfileClient profile_;
};
} // namespace gs
int main() { return 0; } 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.