ES /docs

Api::V1::IssueTypesController#index (avg 1888ms, max 2020ms)

RCA: Api::V1::IssueTypesController#index Latency (avg 1888ms)

Overview#

What Happened#

2026-05-26 04:31~05:41 UTC 사이에 cupixworks-apiApi::V1::IssueTypesController#index 엔드포인트에서 평균 1888ms, 최대 2020ms의 응답 지연이 발생했다. us-west-2, ap-southeast-2, ap-southeast-1 리전에서 5건이 감지되었으며, 동일 시간대에 더 넓은 범위(16건, eu-central-1 포함)로 발생했다.

Quick Facts#

Field Value
resource_name Api::V1::IssueTypesController#index
top_frame app/controllers/api/v1/issue_types_controller.rb:6
avg_duration 1888ms
max_duration 2020ms
db_time 1.5~5.7ms
env production (us-west-2, ap-southeast-2, ap-southeast-1, eu-central-1)

Timeline#

  1. 2026-05-26T04:31:27Z — 최초 slow trace 감지 (ap-southeast-1)
  2. 2026-05-26T05:41:50Z — 마지막 slow trace (클러스터 종료 시점)
  3. 2026-05-26T05:54:50Z — 추가 slow trace 발견 (Datadog 확장 조회)

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::IssueTypesController#index",
  "service": "cupixworks-api",
  "occurrences": 5,
  "avg_ms": 1888,
  "max_ms": 2020,
  "sample_trace_id": "4441130473776059076"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 5건 (확장 조회 시 16건)
  • 최초 발생: 2026-05-26T04:31:27.330Z
  • 최근 발생: 2026-05-26T05:41:50.975Z
  • 영향 범위: 다수 tenant (sinsw, built, varcomac, multiplex-global, futsu, as2-katris, bv-th, shape, fortescue, crcc-sama, mbjv) — 특정 tenant에 국한되지 않음

Root Cause Summary#

IssueTypesController#index는 내부 DB 조회 없이 외부 Issue Service ($CUPIX_ISSUE_SERVICE_URL/issue_types)에 동기식 HTTP GET 요청을 수행한다. Datadog APM 데이터에서 DB 시간이 1.55.7ms에 불과한 반면 총 응답 시간이 17002020ms인 것으로 확인되었으며, 이는 외부 Issue Service의 응답 지연이 전체 latency의 99.7%를 차지함을 의미한다. RestClient.get 호출에 명시적인 timeout 설정이 없어 외부 서비스 지연이 그대로 클라이언트 응답 시간에 전파된다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/issue_types_controller.rb:4 (#index action)
  • 외부 서비스 호출: app/services/cupix/issue_service.rb:89 (Cupix::HttpClient.get)
  • HTTP 클라이언트: lib/cupix/http_client.rb:15 (RestClient.get — timeout 미설정)
  • 응답 렌더링: app/controllers/concerns/renderable_controller.rb:31 (render_json)

1. Controller — 외부 서비스 호출

app/controllers/api/v1/issue_types_controller.rb:4-11ruby
def index
  url = "#{$CUPIX_ISSUE_SERVICE_URL}/issue_types"
  resp = Cupix::IssueService.list(url, params)

  raise Cupix::Errors::System.new(code: 'SYS20000', reason: 'Temporarly failed to fetch issue types') if resp.nil?

  render_json resp.code, JSON.parse(resp.body)['result']
end

컨트롤러는 자체 DB 조회 없이 Cupix::IssueService.list를 통해 외부 서비스에 위임한다.

2. Service Layer — HTTP 요청 실행

app/services/cupix/issue_service.rb:82-107ruby
def list(url, params)
  unless url.start_with?('http')
    Cupix::Logger.info('Bypass posting issue because ISSUE_SERVICE_URL is not set', class: self.name, function: __method__)
    return
  end

  begin
    resp = Cupix::HttpClient.get(
      url,
      {
        params: params.as_json,
        content_type: :json,
        accept: :json,
        authorization: "Bearer #{_access_token}"
      }
    )
  rescue RestClient::Exception => e
    Cupix::Logger.error("fail to fetch list from issue service. reason: 'RestClient error' message: #{e.response}", class: self.name, function: __method__)
  rescue StandardError => e
    Cupix::Logger.error("fail to fetch list from issue service. reason: 'Standard error' message: #{e.message}", class: self.name, function: __method__)
  else
    Cupix::Logger.info('fetching list from issue service successful.', class: self.name, function: __method__)
  end

  resp
end

3. HTTP Client — timeout 없는 RestClient 호출

lib/cupix/http_client.rb:11-24ruby
sig { params(url: String, headers: T::Hash[T.untyped, T.untyped], retries: Integer).returns(RestClient::Response) }
def self.get(url, headers = {}, retries: MAX_RETRIES)
  attempt = 0
  begin
    RestClient.get(url, headers)
  rescue RestClient::Exception => e
    if RETRIABLE_STATUS_CODES.include?(e.http_code) && attempt < retries
      attempt += 1
      sleep((2**(attempt - 1)) + rand(0.0..0.5))
      retry
    end
    raise
  end
end

RestClient.get 호출 시 timeout 또는 open_timeout 파라미터가 전혀 설정되지 않았다. RestClient의 기본 timeout은 무한대이므로, 외부 서비스가 느리게 응답하면 Puma worker 스레드가 무한정 블로킹된다.

4. 응답 렌더링 — session 직렬화

app/controllers/concerns/renderable_controller.rb:31-38ruby
def render_json(status, body = nil, message = nil, items = nil)
  render status: status, json: {
    result: body,
    session: session,
    message: message,
    items: items
  }
end

render_json은 매 응답마다 session 메서드를 호출하지만, DB 시간이 1.5~5.7ms인 것으로 볼 때 session 직렬화는 이 케이스에서 주요 병목이 아니다.

Log Evidence#

Datadog APM 조회 — slow traces:

text
service:cupixworks-api resource_name:"Api::V1::IssueTypesController#index" @duration:>1000ms env:production

주요 trace 데이터 (16건 중 발췌):

text
05:41:54.551Z | duration: 1728.10ms | db: 1.53ms | view: 0.19ms | region: us-west-2 | team: varcomac
05:38:12.814Z | duration: 1922.58ms | db: 1.84ms | view: 0.42ms | region: ap-southeast-2 | team: multiplex-global
05:34:09.335Z | duration: 1934.12ms | db: 3.97ms | view: 0.31ms | region: ap-southeast-1 | team: futsu
04:32:45.929Z | duration: 2018.15ms | db: 4.28ms | view: 0.86ms | region: ap-southeast-1 | team: as2-katris

빠른 응답과의 비교 (동일 시간대):

text
service:cupixworks-api resource_name:"Api::V1::IssueTypesController#index" @duration:<500ms env:production
text
04:xx:xx.xxxZ | duration: 199.29ms | db: 1.96ms | region: ap-southeast-2 | team: cupix
04:xx:xx.xxxZ | duration: 278.84ms | db: 5.01ms | region: eu-central-1 | team: cupix
04:xx:xx.xxxZ | duration: 372.79ms | db: 2.99ms | region: us-west-2 | team: varcomac

핵심 관찰:

  • DB 시간은 빠른/느린 요청 모두 1.5~5.7ms로 동일
  • View 시간도 모두 <1ms
  • 총 응답 시간의 차이(200ms vs 1900ms)는 전적으로 application layer(= 외부 HTTP 호출)에서 발생
  • 같은 tenant(varcomac)에서도 빠른(372ms)과 느린(1728ms) 응답이 혼재 → tenant 데이터 크기와 무관

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 외부 Issue Service 응답 지연이 전체 latency의 원인 DB time 1.5~5.7ms로 DB 병목 아님. View <1ms. 1700ms 이상의 unaccounted time이 외부 HTTP 호출과 정확히 일치. timeout 미설정으로 지연 전파 구조 확인 (http_client.rb:15) Confirmed
H2 N+1 쿼리 또는 slow SQL로 인한 DB 병목 IssueType 관련 endpoint이므로 DB 조회 가능성 Datadog APM db 필드가 1.5~5.7ms로 일관되게 낮음. 코드상 controller에 DB 직접 조회 없음 Rejected
H3 Session 직렬화(SessionSerializer.reload)로 인한 지연 render_json이 매번 session 호출. SessionSerializer가 .reload 사용 (session_serializer.rb) DB time이 5.7ms를 넘지 않으므로 session DB 조회는 무시할 수준. 빠른 요청에서도 동일하게 session 직렬화 수행됨 Rejected
H4 Ruby GC pause 또는 Puma thread contention 간헐적 패턴, 균일한 ~1800ms duration 다수 호스트(ip-10-1-17-211, ip-10-1-17-214, ip-10-1-19-190 등)에서 동시 발생. GC는 개별 호스트에서 독립적이며 이렇게 일관된 duration을 만들지 않음 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • lib/cupix/http_client.rb:15: RestClient.get 호출 시 timeout 파라미터 추가. RestClient::Request.execute(method: :get, url: url, headers: headers, timeout: 5, open_timeout: 3) 형태로 변경하여 외부 서비스 지연이 5초를 초과하지 않도록 제한한다.
  • 대안으로 Cupix::HttpClient.getheaders 해시에 :timeout:open_timeout 키를 추가하여 RestClient가 인식하도록 한다.

단기 개선 (1주 이내)#

  • Issue Service 응답 캐싱: issue_types 목록은 자주 변경되지 않으므로, Redis 또는 Rails.cache에 TTL 기반 캐싱(5~10분)을 추가하여 외부 호출 빈도를 줄인다.
  • Circuit Breaker 도입: 외부 Issue Service가 연속으로 느린 응답을 보낼 경우 빠르게 실패(fallback)하도록 circuit breaker 패턴 적용.

장기 개선 (재발 방지)#

  • Issue Service 성능 조사: 외부 Issue Service 자체의 응답 시간이 왜 1700~2000ms인지 해당 서비스 팀과 협의하여 근본 원인을 해결해야 한다.
  • 비동기 패턴 검토: 프론트엔드에서 issue_types를 별도 비동기 호출로 로드하거나, BFF 레이어에서 캐시된 응답을 제공하는 구조로 전환.

Monitoring#

  • Datadog APM 알림: service:cupixworks-api resource_name:"Api::V1::IssueTypesController#index" P95 duration > 1000ms 시 알림
  • 외부 서비스 health:
text
service:cupixworks-api "fetching list from issue service" @duration:>1000ms
  • Timeout 발생 모니터링 (fix 적용 후):
text
service:cupixworks-api status:error "RestClient" "issue_service"

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — timeout 추가는 단순하나, 캐싱 및 circuit breaker는 설계 필요