Api::V1::IssuesController#create (avg 2481ms, max 2615ms)
RCA: Api::V1::IssuesController#create Latency (avg 2481ms)
Overview#
What Happened#
2026-05-28 03:5804:22 UTC 사이 cupixworks-api의 47ms로 정상이며, 외부 Issue Service(AWS Lambda + API Gateway)에 대한 동기 HTTP POST 호출이 전체 응답 시간의 80% 이상을 차지했다.Api::V1::IssuesController#create 엔드포인트에서 평균 2481ms, 최대 2615ms의 응답 지연이 ap-southeast-1과 ap-southeast-2 리전에서 2건 발생했다. DB 시간은 14
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::IssuesController#create |
| top_frame | app/services/cupix/issue_service.rb:117 |
| env | production, ap-southeast-1 / ap-southeast-2 |
| avg_duration | 2481ms |
| max_duration | 2615ms |
| issue-service infra | AWS Lambda (arm64, Node.js) + API Gateway + DynamoDB |
Timeline#
- 2026-05-28T03:58:31Z — 첫 번째 slow request 발생 (ap-southeast-2, 2344ms)
- 2026-05-28T04:09:16Z — 두 번째 slow request 발생 (ap-southeast-2, 2558ms)
- 2026-05-28T04:22:43Z — 세 번째 slow request 발생 (ap-southeast-1, 2612ms)
- 2026-05-28T04:22:36Z — error-sweeper가 latency cluster로 감지
Error Log#
{
"resource_name": "Api::V1::IssuesController#create",
"service": "cupixworks-api",
"occurrences": 2,
"avg_ms": 2481,
"max_ms": 2615,
"sample_trace_id": "2779944330653972320"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 2
- 최초 발생: 2026-05-28T03:58:31.404Z
- 최근 발생: 2026-05-28T04:22:36.577Z
이 엔드포인트는 캡처 처리 파이프라인에서 자동으로 호출되며 (에러 코드: CGT6016, CGT6003), 동기 호출 시 캡처 파이프라인의 전체 처리 시간이 2초 이상 지연된다. 사용자 대면 영향은 제한적이나, 대량 처리 시 worker thread 점유 시간이 누적될 수 있다.
Root Cause Summary#
IssuesController#create가 async: false 파라미터로 호출되면서, 외부 Issue Service에 대한 HTTP POST가 요청 lifecycle 내에서 동기적으로 실행된다. Issue Service는 AWS Lambda(arm64, Node.js) + API Gateway + DynamoDB 기반 서버리스 아키텍처로, POST /issues 요청 처리 시 다음이 순차적으로 실행된다:
- Lambda cold start (500~1500ms, 간헐적)
- DynamoDB IssueType 조회 (1회, ~30ms)
- DynamoDB BatchWrite (2 items, ~30ms)
getIssueWithFullInfo()— 중복 DynamoDB 조회 3회 (~90ms)sendEmailNotification()— Lambda Invoke APIawait(~100-500ms)sendMondayNotification()— Lambda Invoke APIawait(~100-500ms)
총 DynamoDB I/O만 4~5회(~150ms), Lambda Invoke API 2회(~200-1000ms), 여기에 cold start가 추가되면 일관되게 ~2000ms가 된다. cupixworks-api 측의 Cupix::HttpClient.post()는 이 전체 응답을 동기적으로 대기한다.
Technical Analysis#
Code Path#
Caller (cupixworks-api):
- Entry point:
app/controllers/api/v1/issues_controller.rb:18 - Controller에서
capture.create_issue(async: false)호출 - Issueable concern에서
Cupix::IssueService.create_issue()호출 - IssueService가 동기적으로 외부 서비스에 POST 수행
- Blocking point:
app/services/cupix/issue_service.rb:117— 외부 HTTP POST 응답 대기
def create
raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'capture_id is required') if params[:capture_id].blank?
raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'error_code is required') if params[:error_code].blank?
capture = Capture.find(params[:capture_id])
issue_data = capture.create_issue(error_code: params[:error_code], issued_by: @current_user.email, validate_reprocess: false, async: false)
raise Cupix::Errors::Entity.new(code: 'ENT10001', reason: 'issue already exists on this model') if issue_data.nil?
render_json 200, issue_data['result'].as_json
end
Issue Service (Lambda):
export const handler = async (event: APIGatewayEvent) => {
const data = JSON.parse(event.body || "{}");
// ... validation ...
// Step 1: IssueType lookup (DynamoDB GetItem or Query)
issueType = await findIssueType(data.issue_type_id, data.error_code);
// Step 2: BatchWrite (issue + issueType link)
const client = new DynamoDBClient(dynamoDBClientConfig);
await client.send(new BatchWriteItemCommand(params));
// Step 3: Redundant read-back (3 additional DynamoDB queries)
const issue = await getIssueWithFullInfo(client, issueId);
// Step 4-5: Sequential await on async Lambda invocations
await sendEmailNotification(event.headers?.Authorization, issueId);
if (!issue.issue_type.error_code?.startsWith("AGT")) {
await sendMondayNotification(issueId);
}
return success(issue);
};
주요 병목 — getIssueWithFullInfo() (write-then-read anti-pattern):
export const getIssueWithFullInfo = async (client: DynamoDBClient, issue_id: string) => {
// Query 1: 방금 쓴 issue를 다시 조회
const queryResult = await client.send(new QueryCommand(issueParams));
const issue = queryResult?.find(item => item.SK === SortKey.metadata);
// Query 2: IssueType by ID (이미 findIssueType에서 조회한 데이터)
const issueTypeById = await getIssueTypById(client, issueTypeId);
// Query 3: IssueType by errorCode (TODO: remove error_code search — 코드 주석 존재)
const issueTypeByErrorCode = await getIssueTypByErrorCode(client, errorCode);
return issue;
};
주요 병목 — Sequential Lambda Invoke await:
export const sendEmailNotification = async (authorization, issueId) => {
const lambdaClient = new LambdaClient();
const params = {
FunctionName: ENV.EMAIL_SEND_LAMBDA_NAME,
InvocationType: InvocationType.Event, // async, fire-and-forget
Payload: JSON.stringify({ authorization, issue_id: issueId })
};
await lambdaClient.send(new InvokeCommand(params)); // 그럼에도 await 대기
};
InvocationType.Event는 Lambda 실행 결과를 기다리지 않지만, Lambda Invoke API 자체의 HTTP 응답(202 Accepted)을 await하므로 Lambda 서비스 API 레이턴시(~100-500ms/회)가 추가된다. 이 호출이 순차적으로 2번 실행된다.
주의: await 제거 불가 — Node.js 20.x Lambda async handler는 handler가 return하면 런타임이 실행 환경을 즉시 freeze한다. await 없이 lambdaClient.send()를 호출하면 HTTP 요청이 wire에 전송되기 전에 환경이 frozen될 수 있어, 이메일/Monday 알림이 비결정적으로 누락된다. callbackWaitsForEmptyEventLoop은 callback-style handler에만 적용되며 async handler에서는 무효. 따라서 Promise.all()로 병렬화가 유일한 안전한 최적화 방법이다.
Lambda 인프라 설정:
timeout = 120
Lambda timeout은 120초로 충분하나, Provisioned Concurrency 미설정 — cold start 방지 설정 없음.
Log Evidence#
Datadog에서 확인한 요청별 타이밍 분석:
service:cupixworks-api resource_name:"Api::V1::IssuesController#create" env:production @duration:>500ms
요청 ID b459c7c5 (2558ms total)의 로그 타임라인:
04:09:16.288Z Cupix::IssueService.create_issue "create issue for capture"
04:09:18.290Z Cupix::IssueService._post "post message to issue service successful."
04:09:18.290Z Capture.reset_parent_cached_entity_updates
04:09:18.290Z Class._create_issue_in_worker "issue is created"
04:09:18.339Z [request complete] duration=2557.91ms
create_issue 시작(16.288Z)부터 _post 완료(18.290Z)까지 약 2002ms — 이 단일 HTTP 호출이 전체 응답 시간의 78%를 차지한다.
세 건의 요청 타이밍 비교:
| Timestamp | Duration | DB Time | View Time | Region |
|---------------------|----------|----------|-----------|------------------|
| 2026-05-28T04:22:43Z | 2612ms | 36.25ms | 0.2ms | ap-southeast-1 |
| 2026-05-28T04:09:18Z | 2558ms | 47.25ms | 0.14ms | ap-southeast-2 |
| 2026-05-28T03:58:34Z | 2344ms | 14.5ms | 0.15ms | ap-southeast-2 |
Issue Service 내부 추정 타이밍 (코드 분석 기반):
| 단계 | 예상 소요 | 비고 |
|---------------------------------|-------------|-------------------------------------------|
| API Gateway + Lambda cold start | 500~1500ms | Provisioned Concurrency 미설정 |
| findIssueType (DynamoDB) | 20~50ms | GSI Query 1회 |
| BatchWriteItem | 20~30ms | 2 items |
| getIssueWithFullInfo | 60~90ms | 3 DynamoDB queries (2회 중복) |
| sendEmailNotification (await) | 100~500ms | Lambda Invoke API HTTP 응답 대기 |
| sendMondayNotification (await) | 100~500ms | Lambda Invoke API HTTP 응답 대기 |
| **합계** | **800~2670ms** | Cold start 유무에 따라 변동 |
issue-service는 Datadog에 별도 로그를 전송하지 않아 내부 timing을 직접 확인할 수 없으나, 코드 분석 기반으로 cold start + redundant queries + sequential await가 ~2000ms의 원인으로 판단된다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 외부 Issue Service HTTP POST 동기 호출이 병목 | 로그에서 _post 호출 2002ms 소요 확인, DB는 14~47ms로 정상, 두 리전 모두 동일 패턴 |
— | Confirmed |
| H2 | Lambda cold start가 주요 지연 원인 | arm64 + Node.js Lambda, Provisioned Concurrency 미설정, 일관된 ~2000ms 패턴 | 매번 cold start라면 편차가 더 클 수 있으나, 호출 빈도가 낮아 cold start 가능성 높음 | Likely |
| H3 | getIssueWithFullInfo() write-then-read 중복 조회 |
코드에서 방금 쓴 데이터를 3번 재조회 확인 (dynamodb-helper.ts:68-98), TODO 주석도 존재 (line 89) |
DynamoDB 자체 latency는 ~90ms로 전체 대비 작음 | Contributing |
| H4 | sendEmailNotification/sendMondayNotification sequential await |
InvocationType.Event임에도 await로 Lambda Invoke API 응답 대기 (lambda-helper.ts:13,26). 순차 실행으로 ~200-1000ms 추가 |
await 자체는 제거 불가(async handler freeze 문제) — Promise.all()로 병렬화만 가능 |
Contributing |
| H5 | N+1 쿼리로 인한 DB 지연 (cupixworks-api 측) | 코드에서 capture.facility, capture.user 등 lazy load 존재 | DB 시간 14~47ms로 전체 대비 미미 | Rejected |
Fix Recommendation#
Issue Service 측 개선 (Priority 1 — 가장 큰 효과)#
1. getIssueWithFullInfo() 제거 — write-then-read anti-pattern 해소
src/lambda/issue/create.ts:101— 방금 write한 데이터를 다시 read할 필요 없음findIssueType()에서 이미 조회한issueType데이터를 in-memory로 조합하여 응답 구성- 예상 효과: DynamoDB 3회 조회 제거 (~60-90ms 절감)
2. sendEmailNotification/sendMondayNotification 병렬화 (Promise.all)
src/lambda/issue/create.ts:103-105— 두 호출을Promise.all()로 병렬 실행await제거(fire-and-forget)는 불가 — Node.js 20.x async handler에서await없이 반환하면 Lambda 런타임이 즉시 실행 환경을 freeze하여 in-flight HTTP 요청이 완료되지 않을 수 있음.InvocationType.Event라도 Lambda Invoke API에 HTTP 요청을 전송하는 단계는 반드시 완료되어야 대상 Lambda가 trigger됨.- Best practice:
Promise.all([sendEmailNotification(...), sendMondayNotification(...)])로 병렬 await — 두 API 호출이 동시에 진행되면서도 모두 완료를 보장 - 예상 효과: 순차 대기 ~200-1000ms → 병렬 시 ~100-500ms (약 50% 절감)
3. DynamoDBClient 재사용 (singleton)
src/lambda/issue/create.ts:99,dynamodb-helper.ts내 각 함수에서 매번new DynamoDBClient()생성- Lambda 모듈 스코프에 단일 client 선언하여 warm start 시 connection 재사용
- 예상 효과: warm start 시 ~5-10ms 절감
cupixworks-api 측 개선 (Priority 2)#
4. async: false → async: true 전환
app/controllers/api/v1/issues_controller.rb:18— Sidekiq worker로 위임- 응답에 issue 데이터가 필요하다면 202 Accepted + polling 또는 webhook 패턴으로 전환
- 예상 효과: API 응답 시간 2500ms → ~50ms
인프라 개선 (Priority 3)#
5. Lambda Provisioned Concurrency 설정
modules/lambda/main.tf—provisioned_concurrent_executions = 1추가- 호출 빈도가 낮아 cold start가 빈번한 상황에 효과적
- 예상 효과: cold start ~500-1500ms 제거
6. Issue Service에 Datadog 모니터링 추가
- Lambda에 Datadog Extension 또는 CloudWatch Logs → Datadog 연동
- 내부 timing을 직접 측정 가능하게 하여 병목 구간 정밀 진단
장기 개선 (재발 방지)#
- API endpoint에서 외부 서비스 동기 호출 패턴을 제거하는 가이드라인 수립
- Issue Service에 circuit breaker 패턴 적용
Monitoring#
- 추가할 메트릭:
IssuesController#createp95/p99 latency 알림 (임계값 1000ms) - Datadog 쿼리 예시:
service:cupixworks-api resource_name:"Api::V1::IssuesController#create" env:production @duration:>1000ms
- Issue Service Lambda 모니터링 (CloudWatch):
AWS/Lambda FunctionName=issue-service-*-issue_create Duration, ColdStart
Risk Assessment#
- Risk level: low
- 예상 복잡도:
- Issue Service 측 (Priority 1): moderate —
getIssueWithFullInfo제거 + Lambda invoke await 제거는 코드 3군데 수정 - cupixworks-api 측 (Priority 2): trivial —
async: false→true1줄 수정 (단, caller가 응답 데이터를 사용하는지 확인 필요) - 인프라 (Priority 3): trivial — Terraform 1줄 추가
- Issue Service 측 (Priority 1): moderate —
Revision History#
Revision 1#
Feedback: cupixworks/applications/issue-service 코드를 분석하여 개선여지를 판단하고, 로그에서 어디서 오래걸렸는지 확인
판정:
| 피드백 항목 | 판정 | 근거 |
|---|---|---|
| issue-service 코드 분석으로 개선여지 판단 | 수용 | src/lambda/issue/create.ts:99-106에서 write-then-read anti-pattern 확인 — getIssueWithFullInfo()가 방금 쓴 데이터를 DynamoDB에서 3회 재조회(dynamodb-helper.ts:77,87,91). lambda-helper.ts:13,26에서 InvocationType.Event임에도 await로 순차 대기. modules/lambda/main.tf:14에서 Provisioned Concurrency 미설정 확인. 총 3가지 개선 포인트 식별. |
| 로그에서 어디서 오래걸렸는지 확인 | 부분 수용 | cupixworks-api 측 Datadog 로그에서 _post 호출이 2002ms 소요 확인(04:09:16.288Z→04:09:18.290Z). 그러나 issue-service 자체는 Datadog에 로그를 전송하지 않아 내부 단계별 timing을 직접 측정할 수 없음. 코드 구조 분석 기반으로 cold start(500-1500ms) + DynamoDB 4-5회(~150ms) + Lambda Invoke API 2회(~200-1000ms)로 추정 타이밍 산출. |
변경 사항:
- Root Cause Summary: issue-service의 구체적 아키텍처(Lambda + API Gateway + DynamoDB)와 내부 처리 단계별 소요 시간 추정 추가
- Technical Analysis > Code Path: issue-service Lambda 코드 (
create.ts,dynamodb-helper.ts,lambda-helper.ts) 분석 추가, write-then-read anti-pattern과 sequential await 병목 식별 - Technical Analysis > Log Evidence: issue-service 내부 추정 타이밍 테이블 추가
- Hypotheses Considered: H2(Lambda cold start), H3(write-then-read), H4(sequential await) 가설 추가
- Fix Recommendation: issue-service 측 코드 개선(Priority 1) 3항목, 인프라 개선(Priority 3) 2항목 추가. 기존 권장사항을 Priority 2로 재배치
추가 조사 내용:
/home/ec2-user/repos/cupixworks/applications/issue-service/레포 전체 탐색src/lambda/issue/create.ts— POST /issues Lambda handler 분석src/common/dynamodb-helper.ts— DynamoDB 조회 패턴 분석 (getIssueWithFullInfo의 중복 조회, TODO 주석 확인)src/common/lambda-helper.ts— email/monday 알림 호출 패턴 분석modules/lambda/main.tf— Lambda timeout(120s) 및 Provisioned Concurrency 미설정 확인main.tf— DynamoDB PAY_PER_REQUEST 설정, Lambda 모듈 구성 확인- Datadog 로그 검색: issue-service는 별도 Datadog 서비스로 등록되어 있지 않음 확인
Revision 2#
Feedback: lambda 에서 await 을 빼면 fire 되기전에 lambda 가 먼저 끝나서 메일 전송이 안되는거 같은데 best practice 가 뭔지 검토
판정:
| 피드백 항목 | 판정 | 근거 |
|---|---|---|
| await 제거 시 Lambda 종료로 메일 미전송 우려 | 수용 | src/lambda/issue/create.ts:65의 handler는 async (event: APIGatewayEvent) 시그니처로, Node.js 20.x async handler 패턴 사용(variable.tf:34 — nodejs20.x). AWS Lambda async handler는 handler 함수가 return하면 런타임이 실행 환경을 즉시 freeze한다. lambda-helper.ts:13의 await lambdaClient.send(new InvokeCommand(params))에서 await를 제거하면, create.ts:108의 return success(issue)가 먼저 실행되어 handler가 종료되고, Lambda Invoke API로의 HTTP 요청이 wire에 전송되기 전에 환경이 frozen될 수 있다. callbackWaitsForEmptyEventLoop은 callback-style handler에만 적용되며, 이 코드에서는 context 파라미터를 사용하지 않음(handler 시그니처에 context 없음). 따라서 기존 RCA의 "await 제거" 권장은 잘못됨. |
| best practice 검토 | 수용 | AWS Lambda Node.js best practice: (1) Promise.all()로 독립적인 async 호출을 병렬 await — 모든 호출 완료를 보장하면서 순차 대기 제거. (2) InvocationType.Event + await는 대상 Lambda 실행을 기다리지 않으면서(202 즉시 반환) 호출이 Lambda 서비스에 안전하게 전달됨을 보장하는 올바른 패턴. 현재 코드의 문제는 await 존재가 아니라 순차 실행 — Promise.all([sendEmailNotification(...), sendMondayNotification(...)])로 변경하면 ~100-500ms 절감 가능. |
변경 사항:
- Technical Analysis > Code Path:
await제거 불가 사유에 대한 상세 설명 추가 (Node.js 20.x async handler freeze 동작,callbackWaitsForEmptyEventLoop미적용 설명) - Hypotheses Considered > H4: "Evidence against" 컬럼에
await제거 불가 사유 반영 - Fix Recommendation > Priority 1 항목 2: "
await제거 (fire-and-forget)" 옵션 삭제,Promise.all()병렬화만 권장으로 수정. 예상 효과도 "~0ms" 제거하고 "~100-500ms (약 50% 절감)"으로 변경
추가 조사 내용:
variable.tf:34— Lambda 런타임nodejs20.x확인src/lambda/issue/create.ts:65— handler 시그니처에context파라미터 없음 확인 (callbackWaitsForEmptyEventLoop미사용)src/common/lambda-helper.ts:1-28— 전체 코드 재검토,InvocationType.Event+await조합의 동작 분석- AWS Lambda Node.js async handler 동작: handler return 시 event loop drain 없이 즉시 freeze됨 확인