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#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.