22. 서비스 디스커버리 등록과 헬스체크의 준비 상태·해제 순서 (C#)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// [시나리오]
// 존(Zone) 서버 클러스터의 서비스 디스커버리 등록과 헬스체크.
//
// - 인스턴스가 기동하면 디스커버리(Consul/etcd 모사)에 등록하고 하트비트를 보낸다.
// - 게이트웨이는 인스턴스 목록을 주기적으로 받아 라우팅 캐시에 넣고, 신규 접속을
// "현재 연결 수가 가장 적은" 인스턴스로 보낸다.
// - 존 서버는 기동 시 맵 데이터 로드 / 스폰 테이블 캐시 워밍업 / DB 커넥션 풀 예열을
// 마쳐야 실제 플레이 트래픽을 처리할 수 있다.
// - 종료 시에는 신규 접속을 끊고 기존 세션을 정리한 뒤 프로세스를 내린다.
//
// [요구사항] 처리 가능한 인스턴스에만 신규 접속이 가고, 내려가는 인스턴스로는 가지 않으며,
// 디스커버리의 일시적 조회 실패가 서비스 전체 중단으로 번지지 않아야 한다.
//
// [과제]
// 1) 표시된 두 지점 (A), (B) 에서 무엇이 어떻게 잘못되었는지 설명하라.
// 2) 각각이 운영 환경에서 어떤 장애로 드러나는지 서술하라.
// 3) 수정안과 더 나은 설계를 제시하라.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
namespace ZoneCluster
{
public sealed class InstanceInfo
{
public string InstanceId; public int Port; public int ActiveConnections;
}
public sealed class DiscoveryClient
{
private readonly string _baseUrl;
public DiscoveryClient(string baseUrl) { _baseUrl = baseUrl; }
public async Task<bool> RegisterAsync(InstanceInfo info)
{
using var http = new HttpClient();
var body = new StringContent($"{{\"ID\":\"{info.InstanceId}\",\"Port\":{info.Port}}}");
return (await http.PutAsync($"{_baseUrl}/v1/agent/service/register", body)).IsSuccessStatusCode;
}
public async Task<bool> DeregisterAsync(string id)
{
using var http = new HttpClient();
return (await http.PutAsync($"{_baseUrl}/v1/agent/service/deregister/{id}", null)).IsSuccessStatusCode;
}
public async Task HeartbeatAsync(string id)
{
using var http = new HttpClient();
await http.PutAsync($"{_baseUrl}/v1/agent/check/pass/{id}", null);
}
public async Task<List<InstanceInfo>> ListAsync(string serviceName)
{
using var http = new HttpClient();
var res = await http.GetAsync($"{_baseUrl}/v1/health/service/{serviceName}");
if (!res.IsSuccessStatusCode) return new List<InstanceInfo>();
return Parse(await res.Content.ReadAsStringAsync());
}
private static List<InstanceInfo> Parse(string json) => new();
}
public sealed class ZoneServerHost : IHostedService
{
private readonly DiscoveryClient _discovery;
private readonly InstanceInfo _self;
private readonly TcpListenerLike _listener;
private readonly SessionManager _sessions;
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
public ZoneServerHost(DiscoveryClient discovery, InstanceInfo self, TcpListenerLike listener,
SessionManager sessions, IHostApplicationLifetime lifetime)
{
_discovery = discovery; _self = self; _listener = listener; _sessions = sessions;
lifetime.ApplicationStopping.Register(() => ShutdownAsync().GetAwaiter().GetResult());
}
public async Task StartAsync(CancellationToken ct)
{
_listener.Start(_self.Port);
await _discovery.RegisterAsync(_self); // (A)
Task.Run(() => HeartbeatLoop(_cts.Token)); // (A)
await MapDataLoader.LoadAsync(_self.InstanceId);
await SpawnTableCache.WarmUpAsync();
await DbPool.PrewarmAsync(minConnections: 32);
}
// 디스커버리와 게이트웨이가 호출하는 /healthz 핸들러
public int HealthCheck() => 200; // (A)
private async Task HeartbeatLoop(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await _discovery.HeartbeatAsync(_self.InstanceId);
await Task.Delay(TimeSpan.FromSeconds(5), ct);
}
}
public Task StopAsync(CancellationToken ct) => ShutdownAsync();
private async Task ShutdownAsync()
{
_listener.Stop(); // (B)
await _sessions.DrainAsync(TimeSpan.FromSeconds(30));
_cts.Cancel();
await _discovery.DeregisterAsync(_self.InstanceId); // (B)
}
}
public sealed class GatewayRouter
{
private readonly DiscoveryClient _discovery;
private readonly ConcurrentDictionary<string, InstanceInfo> _routeCache = new();
private DateTime _cachedAtUtc = DateTime.MinValue;
private static readonly TimeSpan Ttl = TimeSpan.FromSeconds(20);
public GatewayRouter(DiscoveryClient discovery) { _discovery = discovery; }
public async Task RefreshLoop(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var list = await _discovery.ListAsync("zone-server");
_routeCache.Clear(); // (B)
foreach (var i in list) _routeCache[i.InstanceId] = i; // (B)
_cachedAtUtc = DateTime.UtcNow;
await Task.Delay(TimeSpan.FromSeconds(10), ct);
}
}
public InstanceInfo PickForNewConnection()
{
if (DateTime.UtcNow - _cachedAtUtc > Ttl) Console.WriteLine("[gw] cache stale");
return _routeCache.Values.OrderBy(i => i.ActiveConnections).FirstOrDefault();
}
}
public sealed class TcpListenerLike { public void Start(int p) { } public void Stop() { } }
public sealed class SessionManager { public Task DrainAsync(TimeSpan t) => Task.CompletedTask; }
public static class MapDataLoader { public static Task LoadAsync(string id) => Task.CompletedTask; }
public static class SpawnTableCache { public static Task WarmUpAsync() => Task.CompletedTask; }
public static class DbPool { public static Task PrewarmAsync(int n) => Task.CompletedTask; }
} 결함 코드 · C++
// [시나리오]
// 존(Zone) 서버 클러스터의 서비스 디스커버리 등록과 헬스체크.
//
// - 인스턴스가 기동하면 디스커버리(Consul/etcd 모사)에 등록하고 하트비트를 보낸다.
// - 게이트웨이는 인스턴스 목록을 주기적으로 받아 라우팅 캐시에 넣고, 신규 접속을
// "현재 연결 수가 가장 적은" 인스턴스로 보낸다.
// - 존 서버는 기동 시 맵 데이터 로드 / 스폰 테이블 캐시 워밍업 / DB 커넥션 풀 예열을
// 마쳐야 실제 플레이 트래픽을 처리할 수 있다.
// - 종료 시에는 신규 접속을 끊고 기존 세션을 정리한 뒤 프로세스를 내린다.
//
// [요구사항] 처리 가능한 인스턴스에만 신규 접속이 가고, 내려가는 인스턴스로는 가지 않으며,
// 디스커버리의 일시적 조회 실패가 서비스 전체 중단으로 번지지 않아야 한다.
//
// [과제]
// 1) 표시된 두 지점 (A), (B) 에서 무엇이 어떻게 잘못되었는지 설명하라.
// 2) 각각이 운영 환경에서 어떤 장애로 드러나는지 서술하라.
// 3) 수정안과 더 나은 설계를 제시하라.
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
namespace zone {
struct InstanceInfo {
std::string instanceId;
int port = 0;
int activeConnections = 0;
};
// 디스커버리 백엔드(Consul/etcd)에 대한 얇은 추상화.
struct DiscoveryClient {
virtual ~DiscoveryClient() = default;
virtual bool Register(const InstanceInfo& info) = 0;
virtual bool Deregister(const std::string& instanceId) = 0;
virtual void Heartbeat(const std::string& instanceId) = 0;
// 조회에 실패하면 빈 벡터를 돌려준다.
virtual std::vector<InstanceInfo> List(const std::string& serviceName) = 0;
};
struct Listener {
void Start(int) {}
void Stop() {}
};
struct SessionManager {
void Drain(std::chrono::seconds) {}
};
void LoadMapData(const std::string&) {}
void WarmUpSpawnTable() {}
void PrewarmDbPool(int) {}
class ZoneServerHost {
public:
ZoneServerHost(DiscoveryClient* discovery, InstanceInfo self)
: discovery_(discovery), self_(std::move(self)) {}
~ZoneServerHost() { // (B)
running_.store(false);
}
void Start() {
listener_.Start(self_.port);
discovery_->Register(self_); // (A)
running_.store(true);
std::thread([this] { HeartbeatLoop(); }).detach(); // (A)
LoadMapData(self_.instanceId);
WarmUpSpawnTable();
PrewarmDbPool(32);
}
// 디스커버리와 게이트웨이가 호출하는 /healthz 핸들러
int HealthCheck() const { return 200; } // (A)
void Shutdown() {
listener_.Stop(); // (B)
sessions_.Drain(std::chrono::seconds(30));
running_.store(false);
discovery_->Deregister(self_.instanceId); // (B)
}
private:
void HeartbeatLoop() {
while (running_.load()) {
discovery_->Heartbeat(self_.instanceId);
std::this_thread::sleep_for(std::chrono::seconds(5));
}
}
DiscoveryClient* discovery_;
InstanceInfo self_;
Listener listener_;
SessionManager sessions_;
std::atomic<bool> running_{false};
};
class GatewayRouter {
public:
explicit GatewayRouter(DiscoveryClient* discovery) : discovery_(discovery) {}
void RefreshLoop(const std::atomic<bool>& stop) {
while (!stop.load()) {
std::vector<InstanceInfo> list = discovery_->List("zone-server");
routeCache_.clear(); // (B)
for (const auto& i : list) routeCache_.push_back(i); // (B)
cachedAtMs_.store(NowMs());
std::this_thread::sleep_for(std::chrono::seconds(10));
}
}
// 워커 스레드가 신규 접속마다 호출한다.
const InstanceInfo* PickForNewConnection() const {
if (NowMs() - cachedAtMs_.load() > kTtlMs) std::cout << "[gw] cache stale\n";
const InstanceInfo* best = nullptr;
for (const auto& i : routeCache_) { // (B)
if (best == nullptr || i.activeConnections < best->activeConnections) best = &i;
}
return best;
}
private:
static std::int64_t NowMs() {
using namespace std::chrono;
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
static constexpr std::int64_t kTtlMs = 20000;
DiscoveryClient* discovery_;
std::vector<InstanceInfo> routeCache_;
std::atomic<std::int64_t> cachedAtMs_{0};
};
} // namespace zone 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.