ES /docs

Kinesis put_records! SSL connection reset — no retry logic

RCA: Failed to create event: Connection reset by peer - SSL_connect

Overview#

What Happened#

2026-04-27 15:10~15:28 UTC 사이에 eu-central-1 리전의 AWS Kinesis 서비스가 일시적으로 장애를 겪으면서, cupixworks-api에서 Kinesis EventStream으로 이벤트를 publish하는 과정에서 SSL 연결이 원격 peer에 의해 리셋되었다. 이 클러스터에 기록된 SSL_connect 에러 11건은 약 17분간 지속된 Kinesis 장애의 마지막 단계에서 발생한 것으로, 전체 장애 동안 InternalFailure, Http503Error, execution expired 등 다양한 실패 모드가 함께 관측되었다.

Quick Facts#

Field Value
exception.message Connection reset by peer - SSL_connect
top_frame app/models/concerns/eventable/events/base.rb:20
runtime Ruby on Rails (tesla monolith)
deploy production-eu-central-1-20260427T1445Z0-a4578cc0-cupixworks
env production, eu-central-1

Timeline#

  1. 15:10:57Z — 최초 Aws::Kinesis::Errors::InternalFailure 에러 발생 (Kinesis 장애 시작)
  2. 15:11~15:26Z — InternalFailure, TCP execution expired 등 에러 지속
  3. 15:27:03Z — 최초 SSL_connect 에러 발생 (이 클러스터의 first_seen)
  4. 15:27:28Z — 마지막 SSL_connect 에러 (이 클러스터의 last_seen)
  5. 15:27:58Z — 전체 Kinesis 관련 에러 소멸, 서비스 자동 복구
  6. 2026-04-28 — Error Sweeper에 의해 RCA 수행

Error Log#

Datadog Logs

text
Failed to create event: Connection reset by peer - SSL_connect

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 11 (SSL_connect), ~100+ (전체 Kinesis 장애 포함)
  • 최초 발생: 2026-04-27T15:27:03.601Z
  • 최근 발생: 2026-04-27T15:27:28.169Z
  • 영향 범위: eu-central-1 리전의 3개 EC2 인스턴스, 12개 프로세스, ~90개 Pano/Reference 모델 리소스에 대한 이벤트 publish 실패. 이벤트가 Kinesis로 전달되지 못해 하류 EventStream 소비자(알림, 감사 로그 등)에 데이터 유실 가능성 있음. 단, DB에 Event 레코드는 이미 저장된 상태이므로 데이터 무결성에는 영향 없음.

Root Cause Summary#

eu-central-1 리전의 AWS Kinesis 서비스에서 약 17분간 일시적인 가용성 장애가 발생했다. cupixworks-api는 Cupix::Aws::Kinesis 클라이언트를 통해 put_records!로 이벤트를 publish하는데, memoized된 Kinesis 클라이언트(@kinesis_client)의 기존 TCP/SSL 연결이 원격 peer에 의해 리셋되면서 SSL_connect 에러가 발생했다. 애플리케이션 레벨에서는 Kinesis publish 실패에 대한 재시도 로직이 없으며(RestClient::Exception만 catch), StandardError를 catch한 뒤 로깅 후 re-raise하는 구조이므로, ActiveRecord 콜백 체인을 통해 원래 API 요청까지 에러가 전파된다.

Technical Analysis#

Code Path#

  1. Entry point — ActiveRecord 콜백: 모델이 생성/수정/삭제될 때 Eventable::Callbacksafter_create/after_update 콜백이 트리거된다.
app/models/concerns/eventable/callbacks.rb:30-36ruby
after_create do |model|
  Eventable::Events::Create.create_event(model) if model.event_creation_on_create?
end

after_update do |model|
  Eventable::Events::Update.create_event(model) if model.event_creation_on_update?
end
  1. 이벤트 생성 및 Kinesis publish: create_event는 DB에 Event 레코드 저장 후, Cupix::EventService.publish_event를 호출하여 Kinesis로 이벤트를 전송한다.
app/models/concerns/eventable/events/base.rb:7-21ruby
def create_event(model)
  return nil if invalid_event?(model)

  begin
    event = _create_event(model)
    reason = extract_reason(event, model)
    properties = build_properties(model)

    track_event(model, reason, properties)

    Cupix::EventService.publish_event([event])
    Cupix::Event.publish(event.serializable_hash(stringify_nested_fields: false))
  rescue StandardError => e
    Cupix::Logger.error("Failed to create event: #{e.message}", function: __method__, class: self.name, model: model.class.to_s, id: model.id, event_params: model.event_params, error: e)
    raise e
  end
end
  1. Kinesis publish: EventService가 stream name을 결정하고 put_records!를 호출한다.
lib/cupix/event_service.rb:36-43ruby
case ::Cupix::Tesla.launch_mode
when 'CUPIXWORKS'
  stream_name = "#{::Cupix::Tesla.tenant}-cupixworks-#{Rails.env}-EventStream"
when 'CUPIXVISTA'
  stream_name = "#{::Cupix::Tesla.tenant}-cupixvista-#{Rails.env}-EventStream"
end

response = Cupix::Aws::Kinesis.put_records!({ stream_name: stream_name, records: records })
  1. Failure point — Kinesis 클라이언트: memoized 클라이언트가 put_records를 호출할 때 원격 Kinesis 엔드포인트와의 SSL 연결이 리셋된다.
lib/cupix/aws/kinesis.rb:5-21ruby
def put_records!(opts = {})
  raise Cupix::Errors::Argument.new(code: 'ARG10001', reason: 'stream_name is blank') if opts[:stream_name].blank?
  raise Cupix::Errors::Argument.new(code: 'ARG10001', reason: 'records is blank') if opts[:records].blank?

  kinesis_client.put_records({
    stream_name: opts[:stream_name],
    records: opts[:records]
  })
rescue => e
  Cupix::Logger.error("Failed to put records: #{e.message}", class: self.name, function: __method__, stream_name: opts[:stream_name], record_count: opts[:records]&.size, record_sizes: opts[:records]&.map { |r| r[:data]&.bytesize })
  raise e
end

def kinesis_client
  @kinesis_client ||= ::Aws::Kinesis::Client.new(region: ::Cupix::Tesla.region)
end
  1. 에러 전파 문제: EventService.publish_eventRestClient::Exception만 catch하고(line 50), OpenSSL::SSL::SSLErrorErrno::ECONNRESET은 catch하지 않는다. 따라서 SSL 에러는 base.rb:19StandardError rescue까지 올라가서 로깅 후 re-raise되고, 이것이 ActiveRecord 콜백 체인을 통해 원래 API 요청까지 전파된다.
lib/cupix/event_service.rb:50-53ruby
rescue RestClient::Exception => e
  Cupix::Logger.error("Failed to publish event: #{e.message}", class: self.name, function: __method__, event: event, model: { type: _event.eventable_type, id: _event.eventable_id })
  raise Cupix::Errors::System.new(code: 'SYS20000', reason: "Failed to publish event: #{e.message}")
end

Log Evidence#

Datadog에서 사용한 쿼리:

text
service:cupixworks-api status:error "Failed to create event" @environment:production

SSL_connect 에러 11건의 타임스탬프와 상세 내역:

text
2026-04-27T15:27:03.601Z  Eventable::Events::Update  create_event  Pano  ip-10-1-16-213
2026-04-27T15:27:03.601Z  Eventable::Events::Update  create_event  Pano  ip-10-1-83-162
2026-04-27T15:27:14.822Z  Eventable::Events::Update  create_event  Pano  ip-10-1-147-185
2026-04-27T15:27:15.611Z  Eventable::Events::Update  create_event  Pano  ip-10-1-16-213
2026-04-27T15:27:15.612Z  Eventable::Events::Update  create_event  Pano  ip-10-1-83-162
2026-04-27T15:27:16.151Z  Eventable::Events::Update  create_event  Pano  ip-10-1-147-185
2026-04-27T15:27:21.617Z  Eventable::Events::Update  create_event  Pano  ip-10-1-16-213
2026-04-27T15:27:24.162Z  Eventable::Events::Update  create_event  Pano  ip-10-1-83-162
2026-04-27T15:27:26.167Z  Eventable::Events::Update  create_event  Pano  ip-10-1-16-213
2026-04-27T15:27:28.169Z  Eventable::Events::Update  create_event  Pano  ip-10-1-83-162
2026-04-27T15:27:28.169Z  Eventable::Events::Update  create_event  Pano  ip-10-1-147-185

동시간대 Kinesis 에러 전체 분포 (더 넓은 시간 범위):

text
Aws::Kinesis::Errors::InternalFailure     — 84건 (15:10:57Z ~ 15:27:58Z)
Connection reset by peer - SSL_connect     — 11건 (15:27:03Z ~ 15:27:28Z)  ← 이 클러스터
Aws::Kinesis::Errors::Http503Error         —  4건 (15:15:xx ~ 15:27:xx)
Failed to open TCP connection (expired)    —  3건 (15:20:xx ~ 15:27:xx)

Cupix::Aws::Kinesis / put_records!의 스택 트레이스:

text
aws-sdk-kinesis-1.41.0/lib/aws-sdk-kinesis/client.rb:1926:in `put_records'
lib/cupix/aws/kinesis.rb:9:in `put_records!'
lib/cupix/event_service.rb:43:in `publish_event'
app/models/concerns/eventable/events/base.rb:17:in `create_event'
app/models/concerns/jobable.rb:67:in `create_running_state_changed_event'
app/models/concerns/jobable.rb:47:in `block (3 levels) in <module:Jobable>'

교차 서비스 검색 — 다른 서비스에서의 "Connection reset by peer" 에러:

text
Query: status:error "Connection reset by peer" @environment:production
Result: 22건 — 전부 cupixworks-api (다른 서비스 없음)

이는 Kinesis kinesis.eu-central-1.amazonaws.com 엔드포인트에 국한된 문제임을 확인.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 AWS Kinesis eu-central-1 일시적 가용성 장애 동시간대에 InternalFailure(84건), Http503Error(4건), SSL_connect(11건), TCP expired(3건) 등 4종의 서로 다른 실패 모드가 동일 리전에서 발생; 다른 서비스/리전에는 "Connection reset by peer" 없음; ~17분 후 자동 복구됨 Confirmed
H2 Memoized Kinesis 클라이언트의 stale TCP 연결 재사용 kinesis.rb:20에서 `@kinesis_client =`로 클라이언트 memoize; SSL_connect 에러는 연결 재설정 시 발생 가능
H3 애플리케이션 배포(rolling deploy)로 인한 SSL 연결 불안정 장애 시간대에 3개 deploy 버전이 동시 실행 중 모든 버전이 동일 앱 커밋 a4578cc0 사용; 배포 완료 후에도 에러 지속; InternalFailure가 주요 에러 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 에러 전파 차단: Eventable::Events::Base.create_event (base.rb:17)에서 Cupix::EventService.publish_event 호출이 실패하더라도 에러를 re-raise하지 않도록 변경. Kinesis publish는 사이드 이펙트이므로, 실패 시 원래 API 요청까지 실패시키면 안 된다. Event DB 레코드는 이미 저장된 상태(_create_event at line 11)이므로 publish 실패를 별도로 처리해야 한다.
  • EventService.publish_event의 rescue 범위 확대: event_service.rb:50에서 RestClient::Exception만 catch하고 있으나, 실제로는 Aws::Kinesis::Errors::ServiceError, OpenSSL::SSL::SSLError, Errno::ECONNRESET 등이 발생한다. StandardError로 확대하거나, Kinesis/네트워크 관련 에러를 명시적으로 catch해야 한다.

단기 개선 (1주 이내)#

  • Kinesis publish를 비동기로 전환: ActiveRecord 콜백 내에서 동기적으로 Kinesis에 publish하는 현재 구조는 외부 서비스 장애 시 API 응답에 직접 영향을 준다. ActiveJob 또는 Sidekiq 워커로 publish를 비동기 처리하면 장애 전파를 근본적으로 차단할 수 있다.
  • 재시도 로직 추가: kinesis.rbput_records!에 일시적 네트워크 에러(Errno::ECONNRESET, OpenSSL::SSL::SSLError, Aws::Kinesis::Errors::InternalFailure)에 대한 지수 백오프 재시도(최대 2-3회)를 추가.

장기 개선 (재발 방지)#

  • Circuit breaker 패턴 도입: Kinesis 엔드포인트에 대한 circuit breaker를 적용하여, 연속 실패 시 일정 시간 동안 publish 시도를 건너뛰고 fallback(예: 로컬 큐에 버퍼링)으로 전환.
  • Memoized 클라이언트 상태 관리: @kinesis_client의 stale 연결 문제를 방지하기 위해 주기적으로 클라이언트를 재생성하거나, AWS SDK의 http_open_timeout/http_read_timeout 설정을 명시적으로 구성.

Monitoring#

  • Kinesis publish 실패율 모니터링:
text
service:cupixworks-api status:error "Failed to create event" @environment:production
  • Kinesis 에러 유형별 추적:
text
service:cupixworks-api status:error ("InternalFailure" OR "SSL_connect" OR "Http503Error" OR "execution expired") @environment:production
  • eu-central-1 리전별 에러 비율 알림 추가 권장 (5분 내 10건 이상 시 알림)

Risk Assessment#

  • Risk level: low — AWS 일시적 장애로 자동 복구됨. 그러나 에러 전파로 인해 API 요청 실패가 발생하는 구조적 문제가 존재.
  • 예상 복잡도: standard — 즉시 조치(rescue 범위 확대)는 간단하나, 비동기 전환은 아키텍처 변경이 필요.