flush_geo_coordinate - error - message: Failed to open TCP connection to s3.me-south-1.amazonaws.com
RCA: flush_geo_coordinate - Failed to open TCP connection to s3.me-south-1.amazonaws.com
Overview#
What Happened#
us-west-2 리전에서 실행되는 cupixworks-worker(Sidekiq)의 FlushRecordGeoCoordinateWorker가 me-south-1(Bahrain) 리전에 위치한 S3 버킷으로 geo_coordinate JSON을 업로드하는 도중 TCP connect timeout (execution expired)이 반복 발생했다. 2026-07-13 10:11 KST부터 12:28 KST 사이 동일 fingerprint 로그가 59회 관측되었고, production 뿐 아니라 qa 환경에서도 같은 시간대에 재현된다. me-south-1 스토리지 옵션을 사용하는 Record는 사실상 100% 실패하고 있으며, 동일 워커에서 다른 리전 대상 flush 는 정상 완료된다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Seahorse::Client::NetworkingError (원인 Net::OpenTimeout) |
| exception.message | Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired) |
| top_frame | app/models/concerns/record_geo_coordinate.rb:45 (geo_coordinate_s3_object.upload_stream) |
| runtime | Ruby / AWS SDK for Ruby v3 |
| deploy | qa-us-west-2-20260713T0407Z0-32e73b4c-cupixworks (qa 샘플), production 은 별도 릴리스 |
| env | production, qa (모두 host region: us-west-2) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| me-south-1 스토리지에 매핑된 team(들) | 59 (cluster window) | geo_coordinate JSON 이 CloudFront 로 갱신되지 않아 해당 Record 의 좌표/지도 표시가 stale 상태로 유지 |
구체 team domain 은 record.team.domain 이 이 로그 라인에 포함되어 있지 않아 특정하지 못함 — uncertain, 필요 시 Record.find(134500).team.domain 등으로 확인 필요.
Timeline#
- 2026-07-13 10:11 KST — 클러스터 첫 발생 (cluster
first_seen). - 2026-07-13 12:28 KST — 클러스터
last_seen시점, production 로그에서 마지막으로 관측된 실패 (record id 134500). - 2026-07-13 13:37 KST — qa 환경에서도 동일 에러가 계속 발생 중 (record id 8768). 이슈 미해결.
- 2026-07-13 14:00 KST — 같은 워커에서 me-south-1 이 아닌 다른 Record 의
flush_geo_coordinate - finished로그 관측 → 워커 프로세스 자체는 정상.
Error Log#
flush_geo_coordinate - error - message: Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired)
Impact#
- Service:
cupixworks-worker - 발생 횟수: 59
- 최초 발생: 2026-07-13 10:11 KST
- 최근 발생: 2026-07-13 12:28 KST (production, cluster window 기준. qa 는 13:37 KST 까지 계속 발생)
영향받는 Record 는 여러 id 에 걸쳐 있으며(prod: 134500, 134120, 132296, 132290, 131819 등 / qa: 8768) 재시도해도 성공 로그가 남지 않음(flush_geo_coordinate - finished 로그 0건). Sidekiq retry: 1 설정으로 인해 재시도 1회 후 dead queue 로 이동, geo_coordinate JSON 이 갱신되지 않는다. 결과적으로 me-south-1 팀의 사용자가 Record 지도/좌표 뷰에서 실시간 pano 위치 반영이 지연된다.
Root Cause Summary#
us-west-2 워커 노드에서 s3.me-south-1.amazonaws.com:443 로의 TCP 3-way handshake 가 AWS SDK 의 기본 http_open_timeout (15초) 내에 완료되지 않아 Net::OpenTimeout 이 발생하고, Seahorse 가 이를 execution expired 메시지로 노출한다. flush_geo_coordinate 는 upload_stream 블록 내부에서 예외가 나면 rescue StandardError 로 잡혀 lock 만 해제하고 false 를 반환하므로 (record_geo_coordinate.rb:90-97), 문제 자체는 조용히 감춰지고 로그만 남는다. 다른 지역 대상 flush 는 같은 워커에서 정상 완료되므로 (14:00 KST flush_geo_coordinate - finished 로그 존재) 워커의 네트워크 스택 전반 문제가 아니라 us-west-2 → me-south-1(Bahrain) 경로 특유의 지속적 connect 지연/차단 이 원인이다. 애플리케이션 관점의 근본 원인은 (1) cross-region S3 endpoint 로 초기 TCP 연결을 매 요청마다 새로 수립하며 warm-up/재시도 여유가 없고, (2) SDK 기본 http_open_timeout 이 me-south-1 왕복 지연에 비해 지나치게 타이트하며, (3) 인프라(VPC endpoint / NAT 경로) 가 me-south-1 방향으로만 성능 저하되어 있어도 애플리케이션이 이를 흡수할 재시도/타임아웃 튜닝을 갖고 있지 않다는 점이다.
Technical Analysis#
Code Path#
- Entry point:
app/workers/flush_record_geo_coordinate_worker.rb:7(FlushRecordGeoCoordinateWorker#perform) - 워커는 record 를 조회한 뒤
record.flush_geo_coordinate호출 flush_geo_coordinate내부에서Cupix::StorageService.object(...)로 me-south-1 S3 object 를 만들고upload_stream호출 — 여기서 첫 TCP 연결 시도- Failure point:
app/models/concerns/record_geo_coordinate.rb:45—geo_coordinate_s3_object.upload_stream(...)내부에서Net::OpenTimeout발생 →rescue StandardError(line 90) 에서 삼켜지고 error 로그만 출력
class FlushRecordGeoCoordinateWorker
include Sidekiq::Worker
sidekiq_options queue: :fresh, retry: 1
HEAVY_FLUSH_RECORD_GEO_COORDINATE_TEAM_DOMAINS = %w[aifa].freeze
def perform(record_id = nil, accept_delay = false)
return if record_id.nil?
record = Record.find_by_id(record_id)
Cupix::Logger.error("Record not found: #{record_id}") and return if record.nil?
def flush_geo_coordinate(accept_delay: false)
lock_flushing_geo_coordinate
_delayed_flush_geo_coordinate and return if accept_delay
Cupix::Logger.info('flush_geo_coordinate - begin', ...)
start_time = DateTime.now.to_i
geo_coordinate_s3_object.upload_stream(
content_type: 'application/json',
cache_control: "max-age=#{1.year.to_i}",
acl: 'bucket-owner-full-control'
) do |write_stream|
write_stream << '{ "panos": ['
# ... panos batch 반복
end
update(geo_coordinate_url_updated_at: DateTime.now)
Cupix::Logger.info("flush_geo_coordinate - finished - duration: ...")
_flush_delayed_geo_coordinate
fresh_fresh_state!
rescue StandardError => e
unlock_flushing_geo_coordinate
Cupix::Logger.error("flush_geo_coordinate - error - message: #{e.message}", ...)
false
else
unlock_flushing_geo_coordinate
end
def object(storage_option: nil, **kwargs)
opts = parse_storage_option(storage_option).merge(kwargs)
opts[:force_path_style] = true
check_required_params(opts, %i[region bucket_name key])
Aws::S3::Object.new(opts)
end
parse_storage_option 은 storage_option.s3_bucket_region (여기서는 me-south-1) 을 그대로 SDK 옵션 region 으로 전달하고, http_open_timeout / http_read_timeout 을 지정하지 않는다. config/initializers/** 및 app/** 어디에도 http_open_timeout 설정이 없다(Grep 결과: sorbet RBI 시그니처 외 매치 0건). 따라서 AWS SDK 기본값(연결 15초, 응답 60초) 이 적용되며, us-west-2 ↔ me-south-1 왕복이 지연되면 첫 TCP handshake 에서 즉시 소진된다.
기대 동작: upload_stream 이 me-south-1 endpoint 로 TCP 를 열고 multipart part 를 업로드 → JSON 커밋.
실제 동작: 15초 이내에 SYN/ACK 이 오지 않아 Net::OpenTimeout → 예외 발생 → rescue 에서 error 로그만 출력하고 false 반환. Sidekiq retry: 1 이라 1회 재시도 후 dead. geo_coordinate_url_updated_at 미갱신, CloudFront invalidation 도 수행되지 않는다.
Log Evidence#
Query (Datadog, cluster 파일에서 복사):
service:cupixworks-worker status:error @environment:production "flush_geo_coordinate - error - message: Failed to open TCP connection to s3.me-south-1.amazonaws.com"
대표 로그 raw payload (production, 2026-07-13 12:28 KST):
{
"status": "error",
"message": "flush_geo_coordinate - error - message: Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired)",
"module": "RecordGeoCoordinate",
"class": "Record",
"function": "flush_geo_coordinate",
"record": { "id": 134500 },
"environment": "production",
"service": "cupixworks-worker",
"host": { "name": "ip-10-128-18-154.us-west-2.compute.internal" },
"tenant": "cupix",
"@timestamp": "2026-07-13T03:28:11.561Z"
}
qa 환경 동일 시간대 로그 (record id 8768):
{
"status": "error",
"message": "flush_geo_coordinate - error - message: Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired)",
"environment": "qa",
"record": { "id": 8768 },
"@timestamp": "2026-07-13T04:37:22.555Z"
}
성공 로그 부재 확인:
service:cupixworks-worker @environment:production "flush_geo_coordinate - finished" @record.id:(134500 OR 134120 OR 132296)
→ Found 0 logs (last 24h)
동일 워커의 me-south-1 이 아닌 flush 는 정상 완료:
service:cupixworks-worker "flush_geo_coordinate - begin" @environment:production
→ 14:00:37 KST begin / 관련 finished 로그 존재
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | us-west-2 → me-south-1 경로 지연/차단으로 TCP handshake 가 SDK 기본 http_open_timeout (15s) 을 초과 |
에러 문구가 정확히 Failed to open TCP connection ... (execution expired) (Ruby Net::OpenTimeout). 같은 워커에서 다른 리전 flush 는 성공. qa/production 양 환경 모두 재현 (같은 us-west-2 워커 노드). me-south-1 대상 다른 API 로그는 관측 안 됨 → 해당 리전 접근 자체가 드물어 이 워커 경로에서 처음 실패 |
config/initializers/** 에 timeout override 없음 확인. 애플리케이션 side 에서 재시도/타임아웃 튜닝 부재 |
Confirmed |
| H2 | Sidekiq 워커 프로세스 전체 네트워크 스택 장애 | 다수의 record 가 연속 실패 | 동시 시간대 다른 flush 는 finished 로 완료 (14:00 KST), 다른 status:error 로그에 me-south-1 이 아닌 것은 없음 |
Rejected |
| H3 | storage_option.s3_bucket_region 설정 오류로 잘못된 me-south-1 endpoint 사용 |
에러가 me-south-1 지역에 국한 | config/storage-us-west-2.yaml:203 에 me-south-1 정상 정의됨. bucket/region 매핑 자체는 유효 |
Rejected |
| H4 | S3 me-south-1 리전 자체의 AWS 장애 | cross-region timeout 은 벤더 이슈로 나타날 수 있음 | 다른 서비스/워커 함수에서 me-south-1 관련 에러가 관측되지 않아 판단 불가. status board 에는 dep:s3-me-south-1 인시던트가 열려있지 않음 | Inconclusive — 벤더 상태 페이지 확인 필요 |
| H5 | Sidekiq worker 가 heavy team (aifa) 스킵 로직에 의해 실패 | 워커에 HEAVY_FLUSH_RECORD_GEO_COORDINATE_TEAM_DOMAINS skip 로직 존재 |
실패는 upload 단계에서 발생하고 로그 문구가 skip 이 아닌 TCP timeout | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- us-west-2 → me-south-1 네트워크 경로 상태 확인 (인프라 팀 협조 필요)
- us-west-2 워커 노드(
ip-10-128-18-154.us-west-2.compute.internal) 에서curl -v --connect-timeout 20 https://s3.me-south-1.amazonaws.com및traceroute로 SYN/ACK RTT 측정. - NAT Gateway / VPC endpoint 상태, S3 me-south-1 리전 AWS Health Dashboard 확인.
- 벤더/네트워크 이슈로 판정되면 인프라 fix, 아니라면 아래 애플리케이션 fix 병행.
- us-west-2 워커 노드(
단기 개선 (1주 이내)#
Cupix::StorageService.client/object에 cross-region 대응 timeout & retry 옵션 주입- 대상:
app/services/cupix/storage_service.rb:29-36,parse_storage_option(line 290-307). http_open_timeout을 30-45초,http_read_timeout60-120초,retry_limit4-5 로 상향(SDK 기본은 3). Bahrain/UAE 등 원거리 리전은 first-hop RTT 가 300ms+ 로 SDK 기본이 빠듯함.Aws::S3::Client.new(opts)옵션에 명시적으로 넣고, minio(bucket_type == 'minio') 는 기존 값 유지.
- 대상:
flush_geo_coordinate예외 재분류- 대상:
app/models/concerns/record_geo_coordinate.rb:90-97. Seahorse::Client::NetworkingError,Aws::S3::Errors::*transient 계열은warn레벨로 강등하고 Sidekiq 재큐잉을 통해 백오프 재시도(현retry: 1을 3-5 로 상향 또는 명시적perform_in) 하도록 조정. 그 외 진짜 버그는 error 유지.
- 대상:
- Sidekiq retry 재검토
- 대상:
app/workers/flush_record_geo_coordinate_worker.rb:3. retry: 1은 네트워크 blip 흡수에 부족. 최소 3-4, exponential backoff 적용.
- 대상:
장기 개선 (재발 방지)#
- CloudFront + presigned upload 로 전환 고려: 현재 워커가 me-south-1 S3 로 직접 stream upload 하는 구조. 원거리 리전 대상 스트리밍 대신 asset service / 로컬 리전 프록시 도입 검토.
- 리전별 워커 큐 분리:
s3_bucket_region별로 큐를 나눠 각 리전에 가까운 워커 노드가 처리하도록 라우팅. - S3 endpoint 별 성공률 SLO 모니터링: 특정 리전 실패 급증을 status-board 가 자동 감지하도록 dep-scope (
dep:s3-me-south-1) fingerprint 향상.
Monitoring#
flush_geo_coordinate 오류율 (widget-friendly):
sum:trace.rails.request.errors{service:cupixworks-worker,resource_name:flushrecordgeocoordinateworker}.as_count()
리전별 실패 count (custom log-based metric 필요 — 없으면 도입):
sum:cupixworks_worker.flush_geo_coordinate.errors{env:production} by {s3_region}.as_count()
Datadog 로그 검색으로 즉시 재현 가능한 쿼리:
service:cupixworks-worker status:error "flush_geo_coordinate" "me-south-1"
Sidekiq dead queue 사이즈:
avg:sidekiq.dead{env:production}
권장 알림 임계: 위 두 번째/네 번째 쿼리 15분 창에서 10건 이상.
Risk Assessment#
- Risk level: medium — production 사용자가 볼 수 있는 UI 데이터(geo_coordinate JSON) 갱신 지연이나 서비스 다운은 아님. 다만 me-south-1 팀의 pano 지도가 stale 상태로 남는다.
- 예상 복잡도: standard — timeout/retry 옵션 주입은 소규모 코드 변경이지만, 네트워크 root cause 판정을 위해 인프라 팀 협조가 필요하며 spec 갱신(외부 SDK mocking) 이 뒤따른다.