Api::V1::FacilitiesController#update (avg 1055ms, max 1055ms)
RCA: FacilitiesController#update Latency (1055ms)
Overview#
What Happened#
2026-05-27 07:11:39 UTC, cupixworks-api 서비스의 Api::V1::FacilitiesController#update 요청이 1055ms 소요되었다. 일반적인 facility update 응답 시간(80-135ms) 대비 약 8배 느린 응답이다. DB 시간은 24ms에 불과했으며, 나머지 ~1028ms는 before_commit 콜백에서 발생한 Google Maps Timezone API 동기 호출과 after_commit에서의 Google Static Map 다운로드에 소비되었다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::FacilitiesController#update |
| top_frame | app/models/concerns/geo_queriable/facility.rb:12 |
| env | production, us-west-2 |
| duration | 1055ms (DB: 24ms, external HTTP: ~1028ms) |
| facility | oqlhj2 |
Timeline#
- 07:09:49 — Facility
oqlhj2생성 (create) - 07:10:39 — 첫 번째 update (110ms, 정상 — name 변경만)
- 07:11:17 — 두 번째 update (474ms — siteinsights_version, DB heavy 395ms)
- 07:11:39 — 세 번째 update (1055ms — location 변경으로 timezone API + thumbnail 다운로드 발생)
- 07:11:39 —
update_timezone→set_location_based_timezone!콜백 실행 (Google Timezone API 호출)
Error Log#
{
"resource_name": "Api::V1::FacilitiesController#update",
"service": "cupixworks-api",
"occurrences": 1,
"avg_ms": 1055,
"max_ms": 1055,
"sample_trace_id": "149094867954276496"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 1
- 최초 발생: 2026-05-27T07:11:38.193Z
- 최근 발생: 2026-05-27T07:11:38.193Z
단일 사용자(yohan.kim@cupix.com)의 facility 생성 후 location 설정 과정에서 발생. 기능적 실패는 아니며 응답 지연만 해당. 그러나 location이 포함된 모든 facility update 요청에서 동일하게 발생할 수 있는 구조적 문제.
Root Cause Summary#
Facility의 location 필드가 변경될 때 before_commit :update_timezone 콜백이 동기적으로 Google Maps Timezone API를 호출하여 timezone을 조회한다. 이 외부 HTTP 호출이 요청 사이클 내에서 ~1초를 차지했다. 추가로 after_commit :change_thumbnail_job!이 Google Static Map 이미지를 동기 다운로드한 후 self.save를 호출하여 추가 지연을 유발한다. 두 콜백 모두 외부 API 호출을 request cycle 내에서 동기적으로 수행하는 것이 근본 원인이다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/api/v1/facilities_controller.rb:111 - Repository update:
app/repositories/facility_repository.rb:164(@model.save!) - Before commit 콜백:
app/models/concerns/geo_queriable/facility.rb:8 - Timezone lookup:
app/models/concerns/has_timezone.rb:10 - After commit 콜백:
app/models/concerns/thumbnailable/facility.rb:62
1. Controller → Repository → save!
def update
@model = repository_instance.update(params)
super
end
begin
@model.save!
rescue StandardError => e
raise Cupix::Errors::Parameter.new(
code: 'ARG10000',
reason: "update failed. #{e.message}"
)
end
2. before_commit 콜백에서 timezone API 동기 호출
save! 호출 시 location이 변경되었으면 before_commit :update_timezone이 트리거된다:
before_commit :update_timezone, on: :update
private
def update_timezone
return unless location_changed?
Cupix::Logger.info("update timezone on facility #{self.key}.", class: self.class.name, function: __method__, facility: { id: self.id, key: self.key, saved_changes: saved_changes })
self.set_location_based_timezone
end
set_location_based_timezone은 update!를 포함하며, 내부에서 set_location_based_timezone!를 호출:
def set_location_based_timezone!
raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'Location not set') if location.blank?
timezone = Timezone.lookup(latitude, longitude)
self.timezone_name = timezone.name
self.timezone_offset = timezone.utc_offset
Cupix::Logger.info("Set location based timezone on facility #{self.key}. timezone_offset: #{timezone.utc_offset} timezone_name: #{timezone.name}", class: self.class.name, function: __method__, facility: { id: self.id, key: self.key })
end
Timezone.lookup은 timezone gem (v1.0)을 사용하며, Google Maps Timezone API에 HTTP 요청을 보낸다:
GOOGLE_API_KEY = ENV['GOOGLE_API_KEY'] || 'AIzaSyACNIOJof_WCGjD8Ol4gNkyHnd27_7EKMI'
Timezone::Lookup.config(:google) do |c|
c.api_key = GOOGLE_API_KEY
end
이 API 호출은 request transaction 내에서 동기적으로 수행되며, 네트워크 왕복 시간(~500-1000ms)이 그대로 응답 시간에 반영된다.
3. after_commit에서 Google Static Map 동기 다운로드
location 변경 시 after_commit :change_thumbnail_job!도 트리거된다:
def change_thumbnail_job!
return if self.google_static_map_url.nil?
begin
Cupix::Logger.info("[Thumbnail][Facility] (#{id}) {change_thumbnail_job! - 1} #{Digest::SHA1.hexdigest(self.thumbnail.to_json)}")
self.thumbnail = download_to_file(self.google_static_map_url)
Cupix::Logger.info("[Thumbnail][Facility] (#{id}) {change_thumbnail_job! - 2} #{Digest::SHA1.hexdigest(self.thumbnail.to_json)}")
self.save
Cupix::Logger.info("[Thumbnail][Facility] (#{id}) {change_thumbnail_job! - 3} #{Digest::SHA1.hexdigest(self.thumbnail.to_json)}")
rescue StandardError => e
Cupix::Logger.error("[Thumbnail][Facility] (#{id}) {change_thumbnail_job! - failed} : #{e.message}")
raise e
end
end
download_to_file이 Google Static Map 이미지를 동기적으로 다운로드하고, self.save가 추가 save cycle(ES indexing 포함)을 유발한다. after_commit은 transaction이 commit된 후 실행되지만 여전히 request cycle 내에서 동기적으로 실행된다.
Log Evidence#
Datadog에서 확인된 로그:
service:cupixworks-api @function:update_timezone
Time range: 2026-05-27 06:00 - 08:00 UTC
{
"timestamp": "2026-05-27T07:11:39.946Z",
"message": "update timezone on facility oqlhj2.",
"class": "Facility",
"function": "update_timezone"
}
{
"timestamp": "2026-05-27T07:11:39.946Z",
"message": "Set location based timezone on facility oqlhj2. timezone_offset: 32400 timezone_name: Asia/Seoul",
"class": "Facility",
"function": "set_location_based_timezone!"
}
동일 시간대의 다른 facility update 요청과 비교:
service:cupixworks-api "FacilitiesController#update"
Time range: 2026-05-27 06:00 - 08:00 UTC
| Timestamp | Facility | Duration | DB Time | Notes |
|---|---|---|---|---|
| 07:11:39 | oqlhj2 | 1053ms | 24ms | timezone + thumbnail (location 변경) |
| 07:11:17 | oqlhj2 | 474ms | 395ms | siteinsights_version (DB heavy) |
| 07:31:12 | 4yreuj | 343ms | 139ms | flush_cached_facility_size |
| 07:10:39 | oqlhj2 | 110ms | 21ms | name만 변경 (정상) |
| 07:35:02 | jcvm02 | 123ms | 21ms | name만 변경 (정상) |
Location 변경이 없는 일반 update는 80-135ms로 정상적이며, location 변경이 포함된 요청에서만 ~1초 지연이 발생함을 확인.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Google Timezone API 동기 호출이 지연 원인 | before_commit :update_timezone 콜백에서 Timezone.lookup 호출 확인 (geo_queriable/facility.rb:8), DB 시간 24ms로 외부 호출이 대부분의 시간 차지, timezone 로그 동일 타임스탬프 확인 |
— | Confirmed |
| H2 | DB 쿼리 N+1 또는 slow query 문제 | — | DB 시간 24ms로 매우 짧음. 동일 facility의 이전 update(110ms)도 정상 | Rejected |
| H3 | Elasticsearch 동기 indexing 지연 | after_commit :_update_document가 동기적으로 실행됨 (searchable.rb:55) |
다른 facility update도 동일한 ES indexing을 거치지만 110-135ms로 정상. ES가 원인이라면 모든 요청에서 지연 발생해야 함 | Rejected |
| H4 | Google Static Map 다운로드 추가 지연 | after_commit :change_thumbnail_job!에서 download_to_file 동기 호출 (thumbnailable/facility.rb:67), location 변경 시에만 트리거 |
이 콜백은 after_commit이므로 APM trace duration에 포함되는지 불확실 — trace가 commit 이후에도 측정한다면 기여 |
Likely contributing |
Fix Recommendation#
즉시 조치 (Critical)#
app/models/concerns/geo_queriable/facility.rb:12-16:update_timezone콜백에서 동기 호출을 제거하고 background worker로 이동- 접근 방식:
before_commit에서 직접set_location_based_timezone을 호출하는 대신,after_commit에서TimezoneUpdateWorker.perform_async(self.id)를 호출하여 비동기로 처리
단기 개선 (1주 이내)#
app/models/concerns/thumbnailable/facility.rb:62-75:change_thumbnail_job!이 이미ThumbnailChangeWorker가 존재함에도 동기적으로 이미지를 다운로드하고 있음.change_thumbnail메서드(line 17-19)처럼ThumbnailChangeWorker::Facility.perform_async(self.id)로 대체하여 비동기 처리- timezone lookup 결과 캐싱: 동일 좌표에 대한 반복 API 호출 방지를 위해 Redis 캐시 추가 고려
장기 개선 (재발 방지)#
before_commit/after_commit콜백에서 외부 HTTP 호출을 금지하는 코드 컨벤션 수립- Location 변경 시 트리거되는 모든 동기 외부 호출을 audit: timezone lookup, static map download, geocoding 등을 모두 background job으로 이관
- 외부 API 호출에 대한 timeout 설정 확인 (
timezonegem의 기본 timeout은 무제한)
Monitoring#
- Facility update 응답 시간 모니터링:
service:cupixworks-api resource_name:"Api::V1::FacilitiesController#update" @duration:>500ms
- Timezone API 호출 지연 추적:
service:cupixworks-api @function:set_location_based_timezone!
- p95/p99 latency alert 설정: facility update endpoint에 500ms 초과 시 알림
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard
- 발생 빈도가 낮고(location 변경 시에만), 기능적 실패가 아닌 응답 지연이므로 위험도는 낮음. 다만 구조적으로 모든 location 변경 요청에 영향을 주므로 수정이 바람직함.