1P by GN⁺ | ★ favorite | 댓글 1개
  • jumpcomedy.com에서 밤 10시쯤 RTK Query의 HTTP POST 호출이 모두 실패하며 사이트 기능이 깨졌고, 로컬에서는 정상이라 원인 추적이 어려웠음
  • 고객 불만이 쌓이는 동안 운영자는 프로덕션 지원, SRE, 시니어 엔지니어, 매니저 없이 혼자 장애를 감당해야 했음
  • 브라우저의 fetch 관련 TypeError는 GET·DELETE가 정상인 상황과 맞물려 직접적인 단서가 되지 못함
  • Sentry, 프로덕션 DB, Cloudflare, Chrome 업데이트, 과거 버전 롤백을 확인해도 변화가 없었고, 로컬에 비워둔 PostHog api_key를 넣자 문제가 재현됨
  • PostHog 제거 후 기능이 정상화됐으며, 이후 PostHog와 Redux Toolkit GitHub 이슈에서 같은 장애가 확인돼 외부 도구 영향으로 드러남

장애가 만든 압박

  • jumpcomedy.com은 밤 10시쯤부터 RTK Query 기반 HTTP POST 호출이 모두 실패하며 주요 기능이 제대로 동작하지 않았음
  • 최근 배포한 변경은 있었지만 원인으로 볼 만한 내용은 아니었고, 로컬 환경에서는 문제가 재현되지 않아 추적이 더 어려워짐
  • NextJS와 Vercel Discord에 도움을 요청했지만 응답을 받지 못했고, 장애 대응을 넘길 프로덕션 지원 조직도 없었음
  • 고객 이메일은 계속 쌓였음
    • 이벤트 가격을 변경할 수 없다는 문의
    • 프로모션 코드를 제거할 수 없다는 문의
  • 소규모 비즈니스 고객들이 서비스에 의존하고 있다는 점 때문에 운영자는 수치심, 슬픔, 무능감, 임포스터 증후군을 느꼈음

디버깅 과정과 원인 확인

  • 브라우저 오류는 fetch가 이미 사용된 request object와 함께 실행됐다는 TypeError였지만, 실제 원인을 가리키지는 못함
  • 여러 console.log()와 브레이크포인트를 추가해 헤더, API 토큰 길이, 호출 순서 등을 확인했지만 명확한 원인으로 이어지지 않았음
  • Chrome 업데이트 가능성을 의심했으나 Firefox와 Edge에서도 재현돼 브라우저 단독 문제는 아니었음
  • 과거 버전으로 되돌려도 실패가 계속됨
    • 한 달 전 버전에서도 실패
    • 세 달 전 버전에서도 실패
    • 1년 전 버전에서도 실패
  • 로컬과 프로덕션의 차이를 줄이기 위해 여러 후보를 확인했음
    • 프로덕션에서 Sentry 제거: 변화 없음
    • 로컬을 프로덕션 DB에 연결: 변화 없음
    • Cloudflare 비활성화: 변화 없음
  • 로컬에서는 비용 절감을 위해 PostHog api_key를 비워두고 있었고, 이를 추가하자 같은 문제가 재현됨
  • 다음 커밋에서 PostHog를 제거하자 모든 기능이 정상 동작함
  • 같은 문제는 이후 GitHub 이슈로도 확인됨

댓글과 토론

Hacker News 의견들
  • 대형 글로벌 회사에서 1년간 SRE로 일하면서, 글에서 말한 “패닉” 모드에서 벗어날 수 있었음
    비즈니스 입장에서는 모든 문제가 세상이 끝나는 사건처럼 보이고, 그 상황에서 패닉에 빠지기 쉽지만 실제로는 그렇게 나쁜 경우가 드물고, 설령 나쁘더라도 대개 무사히 살아남음
    이런 상황에서는 바로 고치려고 손대기 전에 5~10분 멈추고 최대한 명확하게 상황을 그려보는 게 핵심임. 공포는 이성적 판단을 방해하고, 패닉 상태에서 버튼을 마구 누르면 문제가 더 꼬일 수 있음. 얼굴과 손에 아주 찬물을 끼얹어 공포 회로를 끊는 게 내 요령임
    이런 일을 몇 번 겪고 나면 생각보다 괜찮고, 이전에도 나쁜 상황을 처리해봤다는 자신감이 생겨서, 도움을 청할 사람이 없어도 감당할 수 있다는 걸 알게 됨
    • 무언가 “고장” 나면 회사는 난리칠 수 있지만, 사실 그보다 더 중요할 수도 있는 문제들에는 전혀 당황하지 않는다는 점을 기억해야 함
      산 소프트웨어가 인력 배치나 설정 실패로 아무 일도 못 하거나, 직원들이 나쁜 사용자 경험과 무의미한 요구사항 때문에 매년 수천 시간을 잃거나, 아무 기능도 하지 않는 역량이 방치되거나, 쓸모없는 회의가 매일 시간을 낭비하거나, 감사 요건을 맞추기 위해 존재만 하는 기능이 있거나, 임원들이 계속 회사 돈을 낭비하는 일들 말임
      다운타임이 이런 문제들보다 더 나빠 보이지는 않지만 훨씬 더 많은 관심과 패닉을 끌어냄. 테러와 심장병의 대비처럼 느껴짐. 회사는 당신의 수면이나 정신 건강을 신경 쓰지 않고, 가능한 한 밀어붙임. 악의적이라는 뜻은 아니지만, 이 점에서는 괴롭히는 사람처럼 당신이 물러나는 만큼 더 밀어붙임
    • 실제 장애 중에 본 최악의 실수들은 대개 과잉 대응에서 나왔음
      내 프로그래밍 좌우명 중 하나는 “흑마법 금지”임. 왜 동작하는지 이해하지 못하면 끝난 게 아님
      장애 대응도 마찬가지로 봄. 누군가의 제안이 왜 영향을 줄지 일관되게 설명하지 못한다면 실행하면 안 된다고 생각함. 언젠가는 그냥 방아쇠를 당겨야 할 때가 올 수도 있지만, 돌이켜보면 결국 그런 경우는 없었던 것 같음
      평소에는 아주 침착한 고위 임원들이 장애 중에 임의의 수정 후보를 던지기 시작하는 걸 보는 건 꽤 충격적이었음
    • 반대로 jumpcomedy.com에서 새벽 2시, ±2개 시간대쯤에 이벤트 가격을 바꿨어야 했던 사람들은 크게 실망했을 것임. 그중 일부는 아마 죽었을지도 모름
      누군가 이 1인 개발자에게 “fetch를 유행시키려 하지 말라”고 멈춰 세웠다면 피해가 얼마나 더 컸을지 상상해보라
    • 내가 아는 가장 멋진 VP 중 한 명은 자주 “느린 것이 매끄럽고, 매끄러운 것이 빠르다”고 말했음
      공포가 이성적 판단을 방해한다는 말은 맞고, 여기에 공포는 전염성이 매우 강하다는 점도 덧붙이고 싶음. 실무자들이 리더, 관리자, 동료가 패닉에 빠지는 걸 보면 자주 같이 패닉에 빠짐. 다행히 내 VP는 늘 침착했고, 행동보다 명확성을 우선했음
    • 결국 당신이 그 위험을 지는 건 아님. 당신 회사가 아니고, 회사는 임의의 시점에 당신을 끊어낼 수 있고 실제로도 그럴 것임. 물론 당신 회사라면 예외임
  • 이게 정신적 붕괴인지는 잘 모르겠고, 기술 관련 스트레스로 실제 붕괴를 겪는 사람들에게 잘못된 인상을 줄 수도 있음
    내 경우 딱 한 번 있었고, 그건 불안 발작이었음. 아내가 옆에서 상황을 설명해주고 내가 무엇을 겪는지 이해하도록 도와줘서 정말 운이 좋았음. 아내는 여러 번 겪었지만 나는 처음이자 다행히 마지막이었음
    이런 일은 사람에게 일어날 수 있고, 그 자체로 잘못된 건 아님. 결함이 있거나 약하다는 뜻이 아니라는 점을 내면화하는 게 정말 중요함
    내 경우 결국 멈추게 해준 건 Xanax였고, 잠들 수 있었기 때문에 손에 닿는 곳에 두는 게 가치 있다고 봄
    말하고 싶은 건 침투적 사고와, 불안 발작이나 공황 발작처럼 실제로 통제할 수 없고 기능을 마비시키는 상태는 다르다는 것임. 그런 일이 생기면 일을 못 하게 되고, 그래도 괜찮음
    • https://www.webmd.com/mental-health/signs-nervous-breakdown
      모든 붕괴가 공황이나 불안 발작 형태로 오는 건 아님. 그런 식으로 드러날 수는 있지만 유일한 방식은 아님. 스트레스는 사람마다, 심지어 스트레스 요인마다 매우 다르게 나타남
      그 사람 머릿속에서 실제로 어떤 일을 겪었는지 알 수 없으니 밖에서 “진단”하는 건 거의 불가능함. 완전한 공황 발작이 아니었더라도, 몇 시간 동안 기능적으로 마비된 것처럼 들리긴 함
    • “Xanax를 가까이 두는 게 좋다”는 말은 조심해야 함
      온라인으로 찾아보면 Xanax는 중독성이 있을 수 있는 것처럼 보임
      https://www.drugs.com/xanax.html
      가볍게 복용할 만한 종류는 아닌 듯함
    • 늘 대충 릴리스되는 기능, 아무 생각 없이 밀어 넣은 변경, 그로 인한 새벽 3시 PagerDuty 알람을 감당하려고 내가 약까지 먹어야 해서는 안 됨
      2000년대 중반에 기술 업계에 들어온 큰 집단이 스트레스 관련 질환으로 죽어가는 걸 보게 될 가능성이 큼
    • 엔터프라이즈 기술 회사에서 일하며 예상 못 했던 것 중 하나는 Xanax를 정기적으로 먹는 동료가 얼마나 많은지였음
      평생 불안이 심했던 입장에서는, 중독성 있는 알약 하나가 그걸 모두 없애줄 수 있다는 생각 자체가 무서움. 나는 평생 매달리게 될 것 같음
    • 사실 평범한 의존성 디버깅 이야기라서 실망했음. 예전에도 붕괴 직전까지 갔다고 느낀 적이 몇 번 있어서, 더 관련 있는 글이길 기대했음
  • 이 사람의 스트레스는 PostHog의 코드 한 줄 때문에 생겼음. 되돌린 커밋은 이쪽임: https://github.com/PostHog/posthog-js/pull/1371/commits/7598...
    여기서 두 가지 교훈이 보임. 첫째, 배포했다면 소유한 것임. 그러니 덜 배포할수록 좋고, 의존성은 최소화해야 함. 둘째, 중요하지 않은 것을 중요 경로 밖에 둬야 함. 에어컨 압축기가 고장 났다고 엔진이 멈추면 안 됨. 브라우저에서는 달성하기 매우 어렵지만 시도할 가치는 있음
    • 더 나쁘게도 PostHog는 자기 코드 일부를 실행 시간에 동적으로 업데이트하는 것처럼 보이고, 빌드 시 묶지 않는 듯함
      문서에는 모든 의존성을 빌드에 포함하는 고급 옵션이 있음. 왜 그렇게 하는지는 이해하고, 내가 오해했을 수도 있지만, 사용자 입장에서는 실행 코드 지연 로딩이 기본값이 아니라 최적화 옵션이어야 한다고 기대함. 완전 번들이 심각한 전달 지연을 만들 때만 써야 한다고 봄
    • 이 교훈들은 분명 가치 있지만, 그다음에는 마케팅 쪽 누군가가 와서 사이트에 PostHog나 다른 추적 스크립트를 넣으라고 요구하고 거절을 받아들이지 않음
  • 버그는 monkey-patch된 window.fetch 안에 있었던 것 같음
    https://github.com/PostHog/posthog-js/blob/759829c67fcb8720f...
    여기서 가장 큰 교훈은, 인기 있는 라이브러리를 만들면서 전역 함수 monkey-patch를 한다면 테스트가 정말 잘 되어 있어야 한다는 것임
    “혹시 모르니 PostHog 호출을 try/catch에 넣자”와 “PostHog 때문에 말 그대로 fetch()로 POST 요청을 보낼 수 없다”는 완전히 다름
    • 이게 왜 테스트에서 잡히지 않았는지 보려고 살펴봤는데, 평범한 fetch 호출만으로도 오류가 날 수 있었음. fetch를 쓸 수 있는 여러 방식에 대한 테스트 범위가 부족한 데다, 과도한 목 처리도 한몫한 듯함: https://github.com/PostHog/posthog-js/blob/main/src/_tests...
      fetch와 XHR 함수 전체가 목 처리되어 아무 일도 안 하게 되니, 당연히 하위의 네이티브 또는 다른 라이브러리와 상호작용할 때 생기는 문제는 잡지 못함. Cypress도 설정되어 있는데 왜 브라우저 API를 목 처리하려는지 모르겠음
    • 이걸 짚어줘서 고마움. 글을 자세히 읽지는 않았지만, 모니터링 라이브러리가 어떻게 전체 애플리케이션을 다운시킬 수 있는지 궁금했음
      합리적으로 통합되었다면 최악의 경우에도 모니터링 이벤트 처리가 실패하는 정도여야 한다고 생각했음
      PostHog가 매우 중요한 전역 함수를 패치한다는 건 잘 문서화되어야 할 기능임. 그래야 사용하는 사람들이 이를 알고, 겉으로는 설명하기 어려운 문제를 디버깅할 때 합리적으로 염두에 둘 수 있음
    • 정의된 대로 동작한 것 같음. POST 요청을 hog한 거 아닌가?
    • 이런 분석 도구 모음에서는 흔한 일임. 이렇게 핵심적인 API를 건드리면서 모든 것을 실제로 어떻게 테스트할 수 있는지 모르겠음
      예를 들어 Heap Analytics는 아직도, 이번 달 기준으로, Hotwire 내부의 뭔가를 건드려서 Hotwire를 무작위로 완전히 깨뜨리고 모든 클릭이 전체 페이지 로드가 되게 함. 내 경험상 페이지 로드의 30~60%에 영향을 줌. 고칠 수는 있지만, Heap이 모든 Hotwire 자바스크립트 뒤에 로드되도록 만들기까지 50시간 넘게 디버깅해야 했음
  • 다른 사람들이 말했듯, 이 심야 스트레스로 이어진 버그는 PostHog 라이브러리의 한 줄 변경이었음[0]
    나는 이걸 변수에 정확한 이름을 붙이는 것의 중요성을 상기시키는 일로 봄
    res = await originalFetch(url, init) 코드는 충분히 무해해 보임. 하지만 TypeScript 선언이 보여주듯 url 매개변수는 반드시 URL이 아님: url: URL | RequestInfo
    URL이 아니라 RequestInfo 객체인 경우 문제가 생김. 함수 구현 앞부분에서 Request 객체를 만들며 이미 “소모”되었고, 여기서 다시 사용할 수 없기 때문임
    매개변수 이름이 urlOrRequestInfo처럼 더 정확했다면 이 변경에서 문제를 놓치기가 더 어려웠을 것임
    훨씬 더 추측적인 생각이지만, 선형 논리에서 나온 선형 타입으로 값이 “소모됨”을 형식화할 수 있으니, 적절한 타입 시스템이 이런 종류의 버그를 막을 수도 있을 듯함
    [0] https://github.com/PostHog/posthog-js/pull/1351/commits/2497...
    • 선형/아핀 타입 시스템의 문제는 진입 장벽이 엄청나게 높다는 것임
      Rust 같은 언어의 소유권 의미론만 봐도 됨. 뚫을 수 없는 수준은 아니고, 특히 경험이 쌓이면 나아지지만, 학습자들이 가장 많이 불평하는 지점일 만큼 부담이 큼
  • 스트레스가 컸지만 웃기기도 한 글이었음. 다만 자기비난 부분은 너무 익숙하게 다가옴
    꽤 성공적인 iOS/macOS 앱을 운영하는데, 릴리스를 밀었다가 35만 개 이상 설치본을 완전히 망가뜨린 적이 있음. 전적으로 내 잘못은 아니었지만, 내 제품이니 별 차이는 없었음
    그때 식은땀과 수치심은 정말 심했음. 게다가 App Store라 수정본도 심사 과정을 거쳐야 해서 시간이 늘어남. 다행히 제출 30분 뒤 심사에 들어갔고 몇 분 만에 승인됐음
    • 개발자로서 첫 직장은 똑똑해서가 아니라 무능해서, 내가 어린 나이에 고객들의 것을 망가뜨리도록 “허용”했음
      경력을 쌓고 리더십으로 넘어오며 그게 매우 가치 있는 경험이었다는 걸 깨달았음. 예전에는 스트레스를 받았을지 모르지만, 이제는 그 기억이 너무 멀어져 닿지도 않음. 지금은 확실히 스트레스받지 않음
      논란이 있을 수 있지만, 나는 가끔 초기 경력 팀원이 운영 환경을 깨뜨리도록 허용함. 미리 보이고, 우리가 빠르게 복구할 수 있다고 확신할 때 말임
      실패할 공간을 주는 게 중요하다는 건 상식이지만, 많은 리더는 실제 고객에게 영향을 주는 실패에서 선을 그음. 비행기를 착륙시키는 소프트웨어처럼 중요한 걸 만드는 게 아니라는 매우 흔하고 운 좋은 상황이라면, 워싱턴주 스포캔의 누군가가 몇 분간 제품을 못 쓰는 대가를 치르더라도 팀이 운영 장애를 경험하게 해야 함
  • 이런 글을 써줘서 고마움. 특히 압박 아래에서, 보통 밤새, 사람들이 이런 도전을 어떻게 넘기는지 읽는 걸 좋아함
    기술적인 사후 분석뿐 아니라 보통 이런 이야기에서 지워지는 인간적인 관점까지 들을 수 있어 더 나아졌다고 느낌. 이런 기술 서사는 소규모/1인 개발자나 창업자만 자유롭게 공유할 수 있는 종류임
  • 문제를 추적한 방식만 봐도 먼저 프로그래머라는 게 보임. 자기 코드로 갔고, 로그로 갔음. 둘 다 합리적이고 둘 다 원인일 수 있지만, 가진 가장 중요한 단서인 “localhost에서는 동작했다”는 점을 놓쳤음
    SRE, DevOps, 플랫폼 엔지니어, 그날그날 붙는 직함이 무엇이든 나는 동작하는 시스템과 동작하지 않는 시스템의 차이에 집중했을 것임. 차이를 하나씩 추가했다가 제거하거나, 제거했다가 다시 추가하면서 무언가 동작할 때까지 봤을 것임
    내가 보는 건 두 가지임. 1) 동작하는 환경이 있다. 2) 실패하는 환경도 원래는 동작하다가 실패하기 시작했다
    내 방법이 더 우월하다는 뜻은 아님. 문제를 보는 방식의 차이를 드러내려는 것뿐임. 둘 다 아는 것을 중심으로 좁혀감. 나는 시스템을 알고, 당신은 코드를 앎
    • 오래전 전자 기술자로 일할 때, 서비스에서 제거된 Perkin Elmer 7/32의 프로세서 보드 더미가 있었음. 고장 난 보드들이었고, 리비전도 여러 가지였으며, 각 보드마다 한 리비전의 회로도만 있었음
      나는 가망이 없다고 봤지만, 더 나이 많고 현명한 기술자가 방법을 가르쳐줬음
      좋은 보드를 익스텐더에 꽂고, 실패하는 진단 프로그램을 루프로 돌림. 오실로스코프로 커넥터의 모든 핀을 보고 기록함. 나쁜 보드로 바꿔서 반복함
      어떤 신호가 다른가? 그 신호를 거슬러 올라감. 회로도가 맞지 않으면 전압계와 눈으로 실제 배선을 반영하는 회로도를 그림
      그는 이걸 “좋은 카드 - 나쁜 카드”라고 불렀고, 실제로 통했음. 비용 효율이 좋았다고 주장하지는 않겠지만 모든 보드를 고쳤고, 디지털 전자 회로 문제 해결 능력은 크게 늘었음
      이건 일종의 “소방관” 같은 일이었음. 시스템이 고장 나길 기다리는 일이어서, 기술자 2명이 회로 보드 하나에 1주일을 써도 상관없었음
  • “한 달 전 버전으로 되돌려보자. 안 됨. 세 달 전은? 안 됨. 여전히 실패. 1년 전은? 전혀 안 됨.”
    자기 코드만 되돌리고, 같은 날 망가진 PostHog 업데이트는 계속 쓰고 있었던 건가? 내 교훈은 의존성까지 포함해 모든 것을 되돌릴 수 있게 해야 한다는 것임
  • 서비스 뒤에 있는 사람들을 다시 떠올리게 해주는 좋은 글이고, 디버깅 과정도 잘 보여줌
    현실적으로 압박은 문제를 더 빨리 디버깅하게 해주지 않음. 보통은 사고를 방해함. 결과를 최대한 무시하고 가능한 한 침착해야 함
    우리 대부분은 정도의 차이는 있어도 비슷한 상황을 겪어봤을 것임. 물론 자기 회사를 운영하는 스트레스는 유독 클 것임