Repository navigation
[FB-252] 로그 MQ가 멈추면 internal·marketplace의 로그인·저장·거래 요청까지 멈춤(LogService가 프로세스 락 안에서 발행 확인을 무기한 대기) #673
Copy link
Copy link
Closed
Labels
bugSomething isn't workingSomething isn't working
Description
심각도
medium (가용성: 로그 파이프라인 장애가 로그인·저장·거래 같은 핵심 요청 장애로 번짐)
근거 강도
보통. 코드 경로와 클라이언트 라이브러리 동작(RabbitMQ.Client 7.2.1 소스)은 확실하다. 발생에는 로그 브로커 쪽 정지(아래 트리거)가 필요하다.
대상 커밋
857683622 (구조는 819867748 feat: async RabbitMQ and LogService 이후 그대로)
위치
server/http/Service/LogService.cs:17— 프로세스 전체에 하나인SemaphoreSlim _mqLock(싱글턴 등록:server/internal/Program.cs:44,server/admin-tool/Program.cs:36, marketplace도 같은 서비스).server/http/Service/LogService.cs:90-136WriteAsync— 락을 쥔 채EnsureConnectedAsync(연결 생성, 기본RequestedConnectionTimeout30초)와BasicPublishAsync를 기다린다. 호출처 전부가cancellationToken을 넘기지 않는다.server/http/Service/LogService.cs:87— 채널을RabbitMqService.PublishChannelOptions로 연다(server/http/Service/RabbitMqService.cs:9-13:publisherConfirmationsEnabled: true,publisherConfirmationTrackingEnabled: true). 그래서BasicPublishAsync가 브로커 ack까지 기다린다. RabbitMQ.Client 7.2.1Impl/Channel.PublisherConfirms.cs:72는_publisherConfirmationTcs.Task.WaitAsync(cancellationToken)이라 토큰이 없으면 시간 제한이 없다.- 로그 소비 큐는 quorum 큐다(
server/log/Worker/LogConsumerService.cs:248-256,x-queue-type: quorum). 로그 브로커는 3노드(infra/pulumi/develop.jsonunified-infra.rabbitmq.log.replicas 3,cluster_partition_handling = autoheal, 자원 제한 없음·hostPath 저장:infra/pulumi/rabbitmq.js:72-79,194). - 핵심 요청 경로의 대기 지점:
server/internal/Controllers/InGameController.cs:227— 로그인 서버→게임 서버 이동(Response.Transfer) 응답 전에game_server_entry로그를 기다린다.server/internal/Controllers/InGameController.cs:583,620—save·save-batch가 저장 뒤 응답 전에 로그를 기다린다.server/internal/Controllers/AccountController.cs:226,324,SystemMailController.cs:83,server/http/Service/StorageService.cs:104,143— 커밋 뒤 응답 전에 기다린다.server/marketplace/Services/MarketplaceService.cs:470,481등 — 구매·등록·취소·abort 요청이 트랜잭션 앞뒤로 기다린다.
- 게임 쪽 HTTP 제한 시간은 5초다(
include/fb/http_client.hPOST).
문제
로그는 보조 기능인데, 로그 한 줄을 쓰는 일이 프로세스 전체 락 아래에서 브로커 확인을 기다리는 동기 단계로 핵심 요청 안에 들어 있다. 로그 브로커가 확인을 못 주는 동안 첫 요청이 락을 쥔 채 멈추고, 같은 파드의 다른 모든 요청은 그 락 앞에 줄을 선다.
확인이 오지 않는 트리거(어느 것도 서버 크래시를 전제하지 않음):
- 로그 브로커 3노드 중 2노드가 내려가거나 분할돼 quorum 큐가 과반을 잃음 → 발행 확인이 오지 않는다.
- 로그 소비자(server/log)가 멈춘 동안 게임 서버 C++ log_collector까지 같은 브로커에 계속 쌓여 hostPath 디스크·메모리 경보 → 브로커가 발행 연결을 block하고 확인을 보내지 않는다.
- 브로커 VM이 응답 없이 멈춤(FIN 없음) → 하트비트로 끊김을 알아챌 때까지(기본 수십 초) 확인을 기다린다. 끊긴 뒤에도 재연결이 락 안에서 요청마다 최대 30초씩 이어진다.
정지가 없어도 한 파드의 로그 처리량은 1 / (quorum fsync 포함 확인 지연)으로 묶인다. 점검 직후 대량 로그인처럼 요청이 몰리면 transfer가 로그 락 순서를 기다리다 게임·로그인 쪽 5초 제한을 넘길 수 있다(근거 약함, 측정 없음).
영향
- 로그 브로커 정지 동안 internal 파드 전부에서 로그인 이동(
transfer)이 응답하지 않아 새 로그인이 막힌다. 저장 요청도 타임아웃으로 실패해 서버 이동이 실패한다. - marketplace 구매·등록·abort가 모두 멈춰 게임은 결과 모름(unsettled)으로 처리한다. FB-159 수정(묘비)이 있어 복제는 막히지만, 거래소 기능이 로그 장애 동안 통째로 멈춘다.
- 대기 요청이 락 앞에 계속 쌓여(ASP.NET 기본 요청 제한 시간 없음) 메모리·연결이 늘어난다. 브로커가 돌아오면 밀린 요청이 한꺼번에 실행된다(이미 게임 쪽은 실패로 처리한 요청).
수정 방향
- 핵심 요청이 로그 발행을 기다리지 않게 한다.
LogService.WriteAsync는 항목을 상한 있는Channel<T>(예: 1~5만 건, 가득 차면 가장 오래된 것 버리고 버린 수를 경고 로그·지표로 남김)에 넣고 바로 반환한다. 연결·발행·확인은BackgroundService하나가 그 채널을 읽어 묶음으로 처리한다. 그러면_mqLock과 요청마다 재연결하는 코드가 사라지고, 로그 장애는 로그 유실(상한 초과분)로만 끝난다. C++log_collector가 이미 이 구조(버퍼·재연결·묶음 발행)이므로 같은 원칙이다. - 1을 당장 못 하면 최소한
WriteAsync전체에 짧은 제한 시간(예: 1~2초)CancellationTokenSource를 걸어 락 대기·연결·발행 확인에 넘기고, 시간 초과는 경고 로그 뒤 무시한다.ConnectionFactory.RequestedConnectionTimeout도 짧게 둔다. - 감사성이 꼭 필요한 이벤트(거래 등)는 로그 MQ가 아니라 이미 같은 트랜잭션에 남는 DB 행(구매 기록·보관함 행)을 근거로 삼는다는 점을 확인한다. 로그 확인 대기가 감사 보장을 더해 주지 않는다(요청 성공과 로그 성공이 원자적이지 않음).
확인한 것
- 같은 패턴의 업무 메시지 발행(
RabbitMqService, internal 브로커)은 핵심 브로커라 대기하는 것이 맞으므로 이 이슈 범위에서 뺐다. - 로그 타임스탬프는 C#(UtcNow)과 C++(
microsec_clock::local_time, 컨테이너 TZ 미설정 = UTC) 모두 UTC로 일치한다. C# 서비스는World가 없어fb.log로 보내고, 첫 월드 소비자가CrossLog로 받는다(infra/pulumi/log.js:14-15) — 누락 없음.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working
Activity
fix: local time, logs, hash #659 #673 #674 #679
boyism80 commentedon Oct 16, 2026
[FB 리뷰어] 1c2ca9bfa에서 해결을 확인했어요.
LogService.WriteAsync가 상한 20000의Channel(DropOldest)에 넣고 바로 반환하고, 발행은PublishLoop백그라운드에서 건당 2초 제한으로 해요. 요청 경로에서 브로커 confirm 대기와 프로세스 락이 사라졌으니 닫을게요.