15. 오래된 라우팅 캐시와 IP:포트 재사용으로 인한 인스턴스 오인
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 서비스 디스커버리 라우팅 캐시와 인스턴스 재사용
// ----------------------------------------------------------------------------
// 시나리오 (서버-서버):
// - 매치메이킹 서버는 각 게임 인스턴스 서버(파드/컨테이너)의 "논리 ID → 주소
// (IP:포트)" 매핑을 서비스 디스커버리로부터 받아 로컬 캐시(RoutingCache)에
// 보관한다.
// - 캐시는 TTL 동안 재조회를 생략해 디스커버리 조회 부하를 줄인다(주기적
// 갱신, 강제 무효화 없음).
// - 인스턴스 서버는 배포/재시작 시 오케스트레이터에 의해 같은 IP:포트를
// 재할당받는 경우가 흔하다(파드 재스케줄링, 컨테이너 재생성).
// - 재시작된 인스턴스는 이전 인스턴스와 논리적으로 무관하다(다른 인스턴스
// 식별자, 초기화된 상태 — 이전에 진행 중이던 매치는 없다).
//
// 상황:
// - 인스턴스 서버 하나가 재시작됐다(크래시 복구 또는 배포). 재시작 직후
// 같은 IP:포트로 새 인스턴스가 떴는데, 매치메이킹 서버가 캐시된 라우팅
// 정보로 그 주소에 "예전 매치의 후속 요청"을 보내 새 인스턴스가 모르는
// 매치 ID에 대한 오류가 발생하거나, 최악의 경우 새 인스턴스가 우연히
// 같은 매치 ID를 다른 목적으로 재사용해 엉뚱한 요청을 처리해버린다.
//
// 과제:
// RoutingCache.Resolve(A)와 인스턴스 등록/해제 흐름(B)을 검토하라. 인스턴스가
// 재시작되어 같은 주소를 재사용할 때 캐시가 왜 그것을 감지하지 못하는지
// 설명하고 수정안을 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Generic;
namespace GameServer.Discovery
{
public sealed class InstanceEndpoint
{
public string InstanceId; // 논리적 인스턴스 ID (예: "match-07")
public string Address; // "IP:포트"
public DateTime CachedAt;
}
public interface IDiscoveryClient
{
// 디스커버리 서버(예: etcd/Consul)에서 최신 등록 정보를 가져온다.
InstanceEndpoint Lookup(string instanceId);
}
public sealed class RoutingCache
{
private static readonly TimeSpan Ttl = TimeSpan.FromSeconds(30);
private readonly Dictionary<string, InstanceEndpoint> _cache = new Dictionary<string, InstanceEndpoint>();
private readonly IDiscoveryClient _discovery;
public RoutingCache(IDiscoveryClient discovery)
{
_discovery = discovery;
}
// 매치메이킹 서버가 요청을 보낼 주소를 얻을 때 호출
public string Resolve(string instanceId)
{
if (_cache.TryGetValue(instanceId, out var cached))
{
bool fresh = (DateTime.UtcNow - cached.CachedAt) < Ttl;
if (fresh)
{
return cached.Address; // (A) TTL 이내면 캐시 그대로 반환
}
}
InstanceEndpoint fresh2 = _discovery.Lookup(instanceId);
_cache[instanceId] = fresh2; // (B) 최신 정보로 교체
return fresh2.Address;
}
// 이 서버가 특정 인스턴스로 요청을 보낼 때, 응답에 실려온 인스턴스
// 식별자로 자신이 기대한 대상과 같은지 확인하지 않고 그대로 신뢰한다.
public void SendMatchRequest(string instanceId, string matchId, byte[] payload)
{
string address = Resolve(instanceId);
TransportSend(address, matchId, payload); // 응답의 인스턴스 식별자 검증 없음
}
private void TransportSend(string address, string matchId, byte[] payload)
{
// 실제 네트워크 전송 (생략)
}
}
} 결함 코드 · C++
// ─────────────────────────────────────────────────────────────
// 시나리오 (서버-서버): 서비스 디스커버리 라우팅 캐시와 인스턴스 재사용
// - 매치메이킹 서버는 각 게임 인스턴스 서버(파드/컨테이너)의 "논리 ID → 주소
// (IP:포트)" 매핑을 서비스 디스커버리로부터 받아 로컬 캐시(RoutingCache)에
// 보관한다. 캐시는 연결 핸들(TCP 커넥션)도 주소별로 재사용한다.
// - 캐시는 TTL 동안 재조회를 생략해 디스커버리 조회 부하를 줄인다.
// - 인스턴스 서버는 배포/재시작 시 오케스트레이터에 의해 같은 IP:포트를
// 재할당받는 경우가 흔하다. 재시작된 인스턴스는 이전 인스턴스와 논리적으로
// 무관하다(다른 인스턴스 식별자, 초기화된 상태).
//
// 상황: 인스턴스 서버 하나가 재시작됐다. 재시작 직후 같은 IP:포트로 새
// 인스턴스가 떴는데, 매치메이킹 서버가 캐시된 라우팅 정보(와 예전 커넥션
// 핸들)로 그 주소에 "예전 매치의 후속 요청"을 보내 새 인스턴스가 모르는
// 매치 ID에 대한 오류가 발생하거나, 최악의 경우 새 인스턴스가 우연히 같은
// 매치 ID를 다른 목적으로 재사용해 엉뚱한 요청을 처리해버린다.
//
// 과제: RoutingCache::resolve(A)의 TTL 판단과, 커넥션 핸들을 그대로 재사용하는
// 부분(B)을 검토하라. 인스턴스가 재시작되어 같은 주소를 재사용할 때 캐시가
// 왜 그것을 감지하지 못하는지 설명하고 수정안을 제시하라.
// ─────────────────────────────────────────────────────────────
#include <chrono>
#include <cstdint>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace discovery {
using Clock = std::chrono::steady_clock;
struct InstanceEndpoint {
std::string instanceId; // 논리적 인스턴스 ID (예: "match-07")
std::string address; // "IP:포트"
Clock::time_point cachedAt;
};
// 실제로는 TCP 소켓을 감싼 래퍼. 재시작으로 상대 프로세스가 바뀌어도
// 로컬에서 유지 중인 연결 핸들 자체는 (재연결 전까지는) 유효해 보인다.
class Connection {
public:
explicit Connection(std::string address) : address_(std::move(address)) {}
void send(const std::string& matchId, const std::vector<uint8_t>& payload) {
// 실제 네트워크 전송 (생략) — 상대가 같은 논리적 인스턴스인지 확인하지 않는다.
}
private:
std::string address_;
};
class IDiscoveryClient {
public:
virtual ~IDiscoveryClient() = default;
virtual InstanceEndpoint lookup(const std::string& instanceId) = 0;
};
class RoutingCache {
public:
explicit RoutingCache(std::shared_ptr<IDiscoveryClient> discovery)
: discovery_(std::move(discovery)) {}
// 매치메이킹 서버가 요청을 보낼 주소/커넥션을 얻을 때 호출
std::shared_ptr<Connection> resolve(const std::string& instanceId) {
auto it = cache_.find(instanceId);
if (it != cache_.end()) {
auto age = Clock::now() - it->second.endpoint.cachedAt;
if (age < kTtl) {
return it->second.conn; // (A) TTL 이내면 캐시된 커넥션 그대로 반환
}
}
InstanceEndpoint fresh = discovery_->lookup(instanceId);
auto conn = std::make_shared<Connection>(fresh.address);
cache_[instanceId] = Entry{fresh, conn}; // (B) 커넥션 핸들을 새로 만들어 교체
return conn;
}
void sendMatchRequest(const std::string& instanceId, const std::string& matchId,
const std::vector<uint8_t>& payload) {
auto conn = resolve(instanceId);
conn->send(matchId, payload); // 응답의 인스턴스 식별자 검증 없음
}
private:
static constexpr std::chrono::seconds kTtl{30};
struct Entry {
InstanceEndpoint endpoint;
std::shared_ptr<Connection> conn;
};
std::shared_ptr<IDiscoveryClient> discovery_;
std::unordered_map<std::string, Entry> cache_;
};
} // namespace discovery 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.