Skip to content

[FB-252] 로그 MQ가 멈추면 internal·marketplace의 로그인·저장·거래 요청까지 멈춤(LogService가 프로세스 락 안에서 발행 확인을 무기한 대기) #673

Closed

Description

@boyism80

심각도

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-136 WriteAsync — 락을 쥔 채 EnsureConnectedAsync(연결 생성, 기본 RequestedConnectionTimeout 30초)와 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.1 Impl/Channel.PublisherConfirms.cs:72는 _publisherConfirmationTcs.Task.WaitAsync(cancellationToken)이라 토큰이 없으면 시간 제한이 없다.
  • 로그 소비 큐는 quorum 큐다(server/log/Worker/LogConsumerService.cs:248-256, x-queue-type: quorum). 로그 브로커는 3노드(infra/pulumi/develop.json unified-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.h POST).

문제

로그는 보조 기능인데, 로그 한 줄을 쓰는 일이 프로세스 전체 락 아래에서 브로커 확인을 기다리는 동기 단계로 핵심 요청 안에 들어 있다. 로그 브로커가 확인을 못 주는 동안 첫 요청이 락을 쥔 채 멈추고, 같은 파드의 다른 모든 요청은 그 락 앞에 줄을 선다.

확인이 오지 않는 트리거(어느 것도 서버 크래시를 전제하지 않음):

  1. 로그 브로커 3노드 중 2노드가 내려가거나 분할돼 quorum 큐가 과반을 잃음 → 발행 확인이 오지 않는다.
  2. 로그 소비자(server/log)가 멈춘 동안 게임 서버 C++ log_collector까지 같은 브로커에 계속 쌓여 hostPath 디스크·메모리 경보 → 브로커가 발행 연결을 block하고 확인을 보내지 않는다.
  3. 브로커 VM이 응답 없이 멈춤(FIN 없음) → 하트비트로 끊김을 알아챌 때까지(기본 수십 초) 확인을 기다린다. 끊긴 뒤에도 재연결이 락 안에서 요청마다 최대 30초씩 이어진다.

정지가 없어도 한 파드의 로그 처리량은 1 / (quorum fsync 포함 확인 지연)으로 묶인다. 점검 직후 대량 로그인처럼 요청이 몰리면 transfer가 로그 락 순서를 기다리다 게임·로그인 쪽 5초 제한을 넘길 수 있다(근거 약함, 측정 없음).

영향

  • 로그 브로커 정지 동안 internal 파드 전부에서 로그인 이동(transfer)이 응답하지 않아 새 로그인이 막힌다. 저장 요청도 타임아웃으로 실패해 서버 이동이 실패한다.
  • marketplace 구매·등록·abort가 모두 멈춰 게임은 결과 모름(unsettled)으로 처리한다. FB-159 수정(묘비)이 있어 복제는 막히지만, 거래소 기능이 로그 장애 동안 통째로 멈춘다.
  • 대기 요청이 락 앞에 계속 쌓여(ASP.NET 기본 요청 제한 시간 없음) 메모리·연결이 늘어난다. 브로커가 돌아오면 밀린 요청이 한꺼번에 실행된다(이미 게임 쪽은 실패로 처리한 요청).

수정 방향

  1. 핵심 요청이 로그 발행을 기다리지 않게 한다. LogService.WriteAsync는 항목을 상한 있는 Channel<T>(예: 1~5만 건, 가득 차면 가장 오래된 것 버리고 버린 수를 경고 로그·지표로 남김)에 넣고 바로 반환한다. 연결·발행·확인은 BackgroundService 하나가 그 채널을 읽어 묶음으로 처리한다. 그러면 _mqLock과 요청마다 재연결하는 코드가 사라지고, 로그 장애는 로그 유실(상한 초과분)로만 끝난다. C++ log_collector가 이미 이 구조(버퍼·재연결·묶음 발행)이므로 같은 원칙이다.
  2. 1을 당장 못 하면 최소한 WriteAsync 전체에 짧은 제한 시간(예: 1~2초) CancellationTokenSource를 걸어 락 대기·연결·발행 확인에 넘기고, 시간 초과는 경고 로그 뒤 무시한다. ConnectionFactory.RequestedConnectionTimeout도 짧게 둔다.
  3. 감사성이 꼭 필요한 이벤트(거래 등)는 로그 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) — 누락 없음.

Activity

  1. boyism80 commented on Oct 16, 2026

    @boyism80
    OwnerAuthor

    [FB 리뷰어] 1c2ca9bfa에서 해결을 확인했어요. LogService.WriteAsync가 상한 20000의 Channel(DropOldest)에 넣고 바로 반환하고, 발행은 PublishLoop 백그라운드에서 건당 2초 제한으로 해요. 요청 경로에서 브로커 confirm 대기와 프로세스 락이 사라졌으니 닫을게요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions