파일이 있는지 확인하는 코드가 항상 있다고 답하고 있었다. 없는 경로를 넣어도 참이 나왔다.
Table of contents
Open Table of contents
원인 — 확인이 만들어 낸 것
확인 로직은 해당 경로를 HEAD로 요청해서 200이 오면 있는 것으로 보고 있었다. 상대 서비스를 보니 없는 경로를 요청하면 원본에서 즉석으로 만들어 저장한 뒤 200으로 돌려주는 구조였다.
없어도 200이 오고 그 HEAD 요청 때문에 실제로 생겼다. 확인하는 행위 자체가 대상을 만들고 있었으므로 그 확인은 아무것도 확인하지 못했다.
증상 — 비용과 잘못된 결론
문제의 크기가 둘이었다. 확인만 하려던 4만여 건의 썸네일이 전부 생성되면서 저장 용량이 늘고 생성 비용이 나갔다.
더 나쁜 것은 이미 있다는 결과가 나오면서 정작 만들어야 할 것을 안 만들었다는 점이다. 확인이 자기가 찾던 것을 만들어 놓고 있다고 답하는 순환이었다.
조치 — 다른 경로로 하는 확인
두 가지 방법이 있었는데 상대가 제공하는 생성 건너뛰기 헤더를 붙여 요청하는 것과 S3 HeadObject로 저장소를 직접 보는 것이다. 앞쪽을 골랐다. 이유는 저장소 직접 조회는 상대 버킷 권한을 받아야 해서다. 대신 상대가 그 헤더를 없애면 다시 생성 경로로 돌아가는 위험을 감수했고 그래서 HeadObject 경로도 준비해 뒀다.
핵심은 생성 로직을 안 거치는 경로로 확인한다는 것이다. 같은 대상을 보더라도 어느 길로 보느냐에 따라 확인이 되기도 하고 생성이 되기도 한다.
검증 — 부작용 있는 조회의 전수
같은 성격의 것이 더 있는지 찾았다. 조회인데 상태를 바꾸는 것이 셋이었다.
썸네일 경로는 없으면 만들고 세션 확인은 호출하면 TTL이 연장되며 토큰 확인은 use_count를 센다. 두 번째가 특히 나빴는데 세션 만료를 확인하려고 부르면 만료가 미뤄져서 영영 만료되지 않는다.
우리 API에도 같은 것이 있었다. GET /orders/{id}가 view_count를 올리고 있어서 확인하려고 부르면 숫자가 변했고 조회와 POST /orders/{id}/views 두 엔드포인트로 나눴다. 상대 쪽에는 확인용 엔드포인트를 요청했는데 하나는 생겼고 둘은 안 돼서 우리 쪽에서 다른 경로를 찾았다.
대응 — 부작용 명시와 사후 정리
전부 나눌 수는 없어서 남는 것은 문서에 적었다. 부작용이 있으면 무엇인지와 확인 목적일 때 어떻게 부르는지를 적고 없으면 없다고 적었다. 없다고 적혀 있어야 안심하고 부를 수 있으므로 없다는 기록도 정보다.
확인 배치 때문에 만들어진 4만여 건도 정리했다. S3 액세스 로그로 보니 90일 안에 조회된 것이 열에 하나꼴이었고 지우기 전에 원본이 있어 다시 만들 수 있는지 확인했다. 원본이 없어진 십여 건만 남기고 나머지는 지운 뒤 Expiration 수명 주기 규칙을 걸었다.
정리
- 조회했더니 있었다는 것이 원래 있었다는 뜻은 아니다
- 확인하는 행위가 대상을 만들 수 있다
- 생성을 건너뛰는 방법을 찾거나
HeadObject같은 다른 경로로 확인한다 - 부작용이 있는 조회를 찾으면 세션
TTL연장이나view_count증가도 같이 나온다 - 조회와 변경을 나눠 달라고 요청한다
- 우리 API에도 같은 것이 있는지 본다
- 부작용을 문서에 적고 없으면 없다고 적는다
- 지우기 전에 다시 만들 수 있는지 확인한다