Api::V1::IssuesController#index (avg 1615ms, max 2280ms)
RCA: Api::V1::IssuesController#index Latency (avg 1615ms, max 2280ms)
Overview#
What Happened#
2026-05-26 03:28~12:38 UTC 사이에 cupixworks-api 서비스의 Api::V1::IssuesController#index 엔드포인트에서 평균 1615ms, 최대 2280ms의 응답 지연이 5개 리전(us-west-2, ap-southeast-2, eu-central-1, ap-southeast-1, ap-northeast-1)에서 43회 발생했다. 모든 요청은 HTTP 200으로 성공했으나 SLA 기준(500ms)을 초과했다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::IssuesController#index |
| top_frame | app/controllers/api/v1/issues_controller.rb:6 |
| runtime | Ruby (Rails) |
| env | production (us-west-2, ap-southeast-2, eu-central-1, ap-southeast-1, ap-northeast-1) |
Affected Teams#
| Team / Domain | Avg Latency | Impact |
|---|---|---|
| crcc-sama | 1093ms (max 2280ms) | 이슈 목록 로딩 지연으로 UX 저하 |
| shinryo | 1899ms | 응답 시간 SLA 초과 |
| fgip-pkg1 | 1394ms | 이슈 목록 로딩 지연 |
| endeavourgroup | 2217ms | 최대 latency 수준 |
Timeline#
- 2026-05-26T03:28:12Z — 최초 감지 (eu-central-1)
- 2026-05-26T05:48:57Z — 최대 latency 2280ms 기록 (eu-central-1, crcc-sama)
- 2026-05-26T12:38:05Z — 마지막 발생
- 2026-05-27 — RCA 분석 완료
Error Log#
{
"resource_name": "Api::V1::IssuesController#index",
"service": "cupixworks-api",
"occurrences": 20,
"avg_ms": 1615,
"max_ms": 2280,
"sample_trace_id": "2259982292470400049"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 43
- 최초 발생: 2026-05-26T03:28:12.778Z
- 최근 발생: 2026-05-26T12:38:05.439Z
Root Cause Summary#
IssuesController#index는 로컬 DB를 조회하지 않고 외부 issue microservice(apig.{host}/v1/issues)로 HTTP GET 요청을 프록시한다. 이 외부 서비스 호출에 timeout 설정이 없으며, 응답 시간이 tenant별 이슈 데이터 볼륨에 비례하여 증가한다. DB 시간은 평균 5ms(전체의 0.3%)에 불과하고, 렌더링은 0.3ms 미만이므로, 전체 latency의 99.7%는 외부 issue service의 응답 대기에서 발생한다. 이슈가 많은 tenant(crcc-sama, shinryo 등)에서 latency가 크게 증가하는 패턴은 issue service가 pagination 없이 대량 데이터를 반환하거나, 서버 측 처리 시간이 데이터 볼륨에 비례함을 시사한다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/api/v1/issues_controller.rb:4 - External call:
app/services/cupix/issue_service.rb:89 - HTTP client:
lib/cupix/http_client.rb:15 - Failure point: HTTP 응답 대기 시간 (no timeout configured)
def index
url = "#{$CUPIX_ISSUE_SERVICE_URL}/issues"
resp = Cupix::IssueService.list(url, params)
raise Cupix::Errors::System.new(code: 'SYS20000', reason: 'Temporarly failed to fetch issues') if resp.nil?
render_json resp.code, JSON.parse(resp.body)['result']
end
컨트롤러는 params 전체를 그대로 외부 서비스에 전달한다. 로컬 DB 조회 없이 완전한 프록시 패턴.
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
params.as_json은 Rails params 전체(controller, action, format 키 포함)를 전달한다. 외부 서비스에 불필요한 파라미터가 전송됨.
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은 무제한(nil)이므로, 외부 서비스가 느리면 Rails worker가 무기한 블로킹된다.
기대 동작: 외부 issue service가 200300ms 내에 응답하여 전체 요청이 500ms 이내에 완료.
실제 동작: issue service가 tenant별 이슈 볼륨에 따라 5002280ms 소요. timeout이 없어 Rails worker가 응답 대기 중 블로킹.
Log Evidence#
Datadog 쿼리:
service:cupixworks-api resource_name:"Api::V1::IssuesController#index" @duration:>500ms env:production
주요 로그 (모두 HTTP 200, error/warn 없음):
2026-05-26T05:48:57.891Z | duration=2278.5ms | db=5.31ms | view=0.15ms | region=eu-central-1 | team=crcc-sama
2026-05-26T08:03:52.139Z | duration=2216.8ms | db=3.99ms | view=0.12ms | region=ap-southeast-2 | team=endeavourgroup
2026-05-26T04:38:24.033Z | duration=2201.6ms | db=5.35ms | view=0.09ms | region=eu-central-1 | team=crcc-sama
2026-05-26T09:14:44.369Z | duration=2071.1ms | db=5.61ms | view=0.29ms | region=us-west-2 | team=cupix
DB 시간 분석:
avg_db_ms: 5.0ms (max 17.0ms)
avg_total_ms: 693.9ms (for all sampled requests)
DB percentage of total: 0.3% (for requests >1000ms)
리전별 분포:
ap-southeast-2: 64 requests, avg 523ms, max 1665ms
eu-central-1: 21 requests, avg 882ms, max 2118ms
us-west-2: 14 requests, avg 1106ms, max 1895ms
ap-northeast-1: 1 request, 1899ms
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 외부 issue service 응답 지연 (데이터 볼륨 비례) | DB=5ms, view=0.15ms로 99.7%가 app-level 대기. tenant별 latency 차이 존재 (crcc-sama max 2280ms vs 소규모 tenant 300ms). 로그에 Cupix::IssueService#list 호출만 기록됨. |
— | Confirmed |
| H2 | 로컬 DB 쿼리 병목 (N+1, 인덱스 누락) | — | DB 시간 평균 5ms, max 17ms. 이 컨트롤러는 로컬 DB를 전혀 조회하지 않음 (코드 확인). | Rejected |
| H3 | 네트워크 지연 (리전 간 호출) | eu-central-1 평균 882ms로 ap-southeast-2(523ms)보다 높음 | issue service가 모든 리전에서 동일 endpoint(apig.cupix.works) 사용. ap-southeast-2가 가장 많은 요청(64건)인데 가장 낮은 latency. 리전보다 tenant 데이터 볼륨과의 상관관계가 더 강함. | Rejected |
| H4 | RestClient timeout 미설정으로 worker 블로킹 | http_client.rb:15에 timeout 파라미터 없음. RestClient 기본값은 nil(무제한). 느린 응답 시 worker가 무기한 점유됨. |
현재까지 max 2280ms로 catastrophic하지 않으나, timeout 없으면 향후 worker pool 고갈 위험. | Confirmed (contributing factor) |
Fix Recommendation#
즉시 조치 (Critical)#
lib/cupix/http_client.rb:15—RestClient.get호출에:timeout(read timeout)과:open_timeout(connection timeout) 파라미터 추가. 권장값:open_timeout: 5,timeout: 10(초).- 이는 외부 서비스 장애 시 Rails worker pool 고갈을 방지하는 안전장치.
단기 개선 (1주 이내)#
- 외부 issue service 측 pagination 최적화: issue service가 대량 이슈를 가진 tenant에 대해 적절한 pagination을 제공하는지 확인.
params에per_page/page파라미터가 전달되는지, issue service가 이를 활용하는지 검증 필요. - 불필요한 params 제거:
params.as_json이 Rails 내부 키(controller,action,format)까지 전달하므로,params.permit(...)으로 필요한 파라미터만 전달하도록 수정. - Response caching: 동일 tenant/params 조합에 대해 짧은 TTL(30~60초)의 캐시 적용 고려.
장기 개선 (재발 방지)#
Cupix::HttpClient모듈에 기본 timeout 정책을 글로벌로 적용 (모든 GET/POST/PUT/DELETE에 일관된 timeout 설정).- Issue service 성능 프로파일링: 대량 이슈 tenant에서의 쿼리 최적화, DB 인덱스 점검.
- Circuit breaker 패턴 도입: 외부 서비스가 일정 시간 이상 응답하지 않을 때 빠르게 실패하여 worker 보호.
Monitoring#
- APM p95/p99 latency alert for
Api::V1::IssuesController#index:
avg(last_5m):trace.rack.request.duration{service:cupixworks-api, resource_name:Api::V1::IssuesController#index} > 1000
- Timeout 발생 빈도 모니터링:
service:cupixworks-api "fail to fetch list from issue service" status:error
- Issue service 자체의 응답 시간 메트릭 추가 (현재 issue service 측 모니터링 여부 확인 필요)
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard (timeout 추가는 trivial, issue service 측 최적화는 별도 조사 필요)