짧은 링크 하나 만드는데 1초? infl.ly 응답속도 개선기

D1을 두 번 읽던 요청을 KV로 바꿨더니 서버 처리 시간이 약 400ms에서 20~39ms로 줄었습니다.

team-loggy4분 분량

안녕하세요. 로기에요!

최근 loggy.page에 글 공유 기능을 붙이면서 공유 주소를 infl.ly 짧은 링크로 만들도록 했습니다.

기능 자체는 잘 동작했는데, 막상 써보니 문제가 하나 있었습니다.

"공유 버튼을 눌렀는데 왜 이렇게 늦게 뜨지?"

X나 스레드, 페이스북 공유 버튼을 누르면 짧은 링크를 만드는 데 0.8~1.4초 정도가 걸렸습니다.

1초 남짓한 시간이 별것 아닌 것 같지만, 버튼을 누른 직후 기다리는 1초는 생각보다 길어요.

그래서 이번에는 링크 생성 구조 자체를 한번 뜯어봤습니다.

처음에는 어디가 느린지부터 확인했습니다

한국에서 curl로 첫 바이트까지 걸리는 시간을 측정해봤습니다.

요청

응답 시간

정적 화면

약 0.45초

API 키 확인

약 0.5초

기존 링크 재사용

약 0.8~1.0초

흥미로운 건 새 링크를 만드는 상황이 아니라 이미 존재하는 링크를 다시 가져오는 요청도 느리다는 점이었습니다.

같은 링크가 있다면 ID만 찾아서 반환하면 될 것 같은데, 왜 1초 가까이 걸릴까요?

첫 번째 원인은 생각보다 멀리 있었습니다

Cloudflare trace를 확인해봤습니다.

colo=SJC
loc=KR

한국에서 요청했는데 실제 Worker는 미국 산호세(SJC)에서 처리되고 있었습니다.

그래서 아무 작업도 하지 않는 정적 페이지조차 첫 응답까지 약 0.45초가 걸렸어요.

하지만 이것만으로는 설명이 부족했습니다.

정적 요청이 0.45초라면 링크 재사용 요청은 왜 0.8~1초까지 늘어나는 걸까요?

진짜 문제는 D1을 두 번 읽는 구조였습니다

기존 POST /api/v1/links의 재사용 경로를 따라가 보니 원인이 꽤 명확했습니다.

API Key 확인        → KV
앱 설정 확인 → D1
기존 링크 검색 → D1
링크 응답 생성 → KV

문제는 D1을 순차적으로 두 번 읽고 있었다는 것입니다.

Worker는 SJC에서 실행되고 D1 데이터는 APAC 쪽에 있었기 때문에, D1을 읽을 때마다 네트워크 지연이 추가됐습니다.

실제로 apps/me와 POST /links의 차이만 약 0.3~0.9초였습니다.

이미 만들어진 링크 하나를 찾기 위해 너무 먼 길을 두 번 왕복하고 있었던 셈입니다.

그럼 D1을 읽지 않으면 되지 않을까?

여기서 방향은 단순해졌습니다.

기존 링크를 재사용하는 상황이라면 굳이 매번 D1까지 갈 필요가 없었습니다.

그래서 (appId, contentHash)를 기준으로 링크 ID를 KV에 저장하도록 바꿨습니다.

(appId, contentHash)
↓
KV
↓
linkId

링크를 만들 때 KV에도 함께 기록하고, 같은 내용의 링크를 다시 요청하면 KV에서 바로 찾습니다.

링크가 수정되거나 비활성화되거나 만료되면 해당 캐시도 함께 지웁니다.

KV에 값이 없는 경우에만 기존처럼 D1에서 찾고 다시 KV를 채우도록 했습니다.

앱 설정도 KV 캐시를 사용했습니다

기존에는 allowedHosts 같은 앱 설정이 변경됐을 때 즉시 반영하기 위해 링크를 만들 때마다 D1을 읽고 있었습니다.

이번에는 이 부분도 KV 캐시를 사용하도록 바꿨습니다.

물론 트레이드오프는 있습니다.

Cloudflare KV 특성상 다른 지역까지 변경 사항이 전달되는 데 약간의 시간이 걸릴 수 있습니다.

하지만 allowedHosts를 줄이는 작업 자체가 자주 발생하는 것도 아니고, 모든 링크 생성 요청마다 D1을 읽는 비용보다 캐시를 활용하는 편이 낫다고 판단했습니다.

결과는 꽤 확실했습니다

배포 후 같은 조건으로 다시 측정했습니다.

구분

서버 처리

전체 응답

개선 전

약 400ms

중앙값 약 0.82초

개선 후

20~39ms

중앙값 약 0.51초

서버 처리 시간만 보면 약 15배 줄었습니다.

특히 기존 링크를 재사용하는 요청은 이제 D1을 읽지 않고 KV만으로 끝납니다.

기존

Worker → D1 → D1 → KV


개선 후

Worker → KV

다만 전체 응답은 여전히 약 0.5초가 걸립니다.

서버가 느려서가 아니라 한국에서 SJC까지 연결하고 TLS 핸드셰이크를 거치는 네트워크 비용이 남아 있기 때문입니다.

그래서 loggy.page에서도 한 번 더 줄였습니다

서버를 20ms대로 만들어도 네트워크 왕복 시간이 남는다면, 이번에는 연결 자체를 미리 준비하면 됩니다.

글 화면에 다음 코드를 추가했습니다.

<link rel="preconnect" href="https://infl.ly">

링크를 미리 만드는 것은 아닙니다.

infl.ly와의 연결만 미리 열어두는 방식이에요.

그리고 공유 창을 열었다고 모든 채널의 링크를 만드는 것도 그만뒀습니다.

X를 눌렀다면 X 링크만,
스레드를 눌렀다면 스레드 링크만 만듭니다.

실제로 사용하지 않을 링크가 infl.ly에 쌓이지 않으면서, 사용자가 눌렀을 때의 체감 속도도 줄일 수 있었습니다.

빠른 API보다 중요한 것

처음에는 단순히 "API가 1초나 걸린다"가 문제라고 생각했습니다.

그런데 뜯어보니 이야기가 조금 달랐어요.

서버 처리 시간도 있었고, D1을 읽는 네트워크 비용도 있었고, 한국에서 SJC까지 가는 연결 비용도 있었습니다.

결국 성능을 개선하려면 어디에서 시간이 쓰이고 있는지부터 분리해서 봐야 했습니다.

그래서 이번 작업에서는 Server-Timing도 추가했습니다.

이제 연결이 느린 건지, API Key 확인이 느린 건지, 링크를 찾는 과정이 느린 건지 브라우저에서도 구분해서 볼 수 있습니다.

그리고 아직 끝난 건 아닙니다.

새 링크를 처음 생성하는 201 요청은 여전히 D1을 두 번 거치기 때문에 약 1.1초가 걸립니다.

다만 새 링크 생성은 글과 공유 채널 조합당 한 번뿐이고, 이후에는 KV에서 재사용할 수 있습니다.

그래서 이번에는 여기까지만 줄였습니다.

모든 요청을 무조건 빠르게 만드는 것보다, 사용자가 반복해서 경험하는 경로부터 빠르게 만드는 게 더 중요하다고 생각해요.

댓글

0

첫 댓글을 남겨보세요

로그인하면 댓글을 남길 수 있어요