내 Ghost blog는 왜 느릴까? - (1)

Ghost 블로그로 옮기고 이상한 문제를 발견했다. 블로그가 너무 느리다는 것. 내 블로그는 대체 왜 느린걸까? 하나 하나 짚어가며 해결하는 과정을 기록해보았다.

문제 상황

일단 속도 저하를 크게 두 가지 경우로 나눌 수 있었다.

문제상황 1 - 첫 로드 상황

특정 페이지에 완전히 첫 접근하는 경우에 문제가 발생했다. 한 포스팅 혹은 메인 페이지에 접속하는데 무려 8.21 sec 이 걸렸다. (아마 다른 유저가 들어왔다면 그냥 나가버릴 시간이다...)

inspection

문제상황 2 - 일반 로드 상황

또한 "첫 로딩" 의 경우가 아니더라도, 짧게는 400ms 에서 길게는 1000ms 까지 응답이 느린 문제도 발견하였다. (혹시나 해서 다른 self-host 고스트 블로그를 찾아보았으나, 나만큼 느리지는 않았다.)

Timing 탭을 통해서 확인해본 결과, "Waiting for server response" 부분에서 한참을 기다리고 있었다.

또한 https://pagespeed.web.dev/ 을 통해서 외부에서 측정되는 속도는 어떻게 되는지 확인해보았다.

딱 봐도 상당히 느리다. 특히 루트 문서의 경우 750ms 가량 소요가 됐다. 이는 개발자 도구를 통해서 살펴본 결과에서도 같았다.

가정 1. Host PC 성능이 문제다.

Host PC 자원 확인해보기

대체 내 블로그는 왜 느린 것인가....?? 아무래도 컴퓨팅 성능이 좋지 않은 PC 에 있다보니 host PC 의 성능을 먼저 의심했다. 다행히 proxmox 에 대해서 influxDB + Grafana 로 모니터링할 수 있도록 구비를 해둔 상황이라서 사용량에 대한 조회를 쉽게 할 수 있었다.

모니터링을 해본 결과, 해당 VM의 메모리가 90% 이상 소모되고 있었다. 그러나 이로 인해서 I/O 가 너무 많이 일어나고 있다거나, 같은 VM 에서 docker를 통해 호스팅되고 있는 다른 서비스들의 속도가 느려지지는 않았다. 그래도 메모리가 부족한 것을 알았으니 host에서 해당 VM에 2GB 정도 더 할당을 했다. (VM 재부팅까지 하니 메모리가 상당히 많이 남았다. 어디선가 많이 먹고있었다... docker container 에 대한 monitoring 도 추가할 필요가 있다...) CPU 사용량의 경우 딱히 영향을 미칠만큼은 아니었다.

이 과정에서 겸사겸사 siege 테스트를 통해 128 connection 에 대해서 테스트를 했는데, 해당 경우에는 정말 처절하게 느려지는 모습이 나타났다.

Lifting the server siege...
Transactions:		        1274 hits
Availability:		      100.00 %
Elapsed time:		       58.23 secs
Data transferred:	        2.73 MB
Response time:		        5.57 secs
Transaction rate:	       21.88 trans/sec
Throughput:		        0.05 MB/sec
Concurrency:		      121.80
Successful transactions:        1400
Failed transactions:	           0
Longest transaction:	       12.88
Shortest transaction:	        0.05

일단 Host PC 를 자원 사용량을 보면서 내린 결론은 다음과 같았다.

  • Host PC 의 관련이 없다. 다만, 나중에 동시접속이 많아지면 곤란해질듯...
    • 주기적으로 접속 속도에 대한 모니터링이 가능하면 좋을 것 같다.
    • 블로그 접속자 수에 대한 모니터링을 할 수 있어야 한다.
  • 성능이 문제가 아니라면 어디선가 병목현상이 일어나고 있다.
    • 우선, ghost 에서 페이지를 렌더링하고 건내는데 얼마가 걸리는지 알아야한다.
    • 블로그에 접속하는 과정을 살펴보며 각각에서 얼마나 소요되고 있는지 파악해야한다.

Ghost 의 렌더링 시간

Host PC 의 자원 사용량과 관련해서는 살펴보았지만, 정작 Ghost 에서 자체적으로 렌더링을 하고 내보내는데 얼마나 걸리지는지는 측정하지 않았다. 다행히 서버 로그를 통해 Ghost 에서 한 페이지를 렌더링하고 보내는데 얼마나 소요되는 지 볼 수 있었다.

서버에서 Request를 받고 Response 까지 얼마나 걸리는지 확인해보았다.

blog-ghost-ghost-1  | [2024-02-03 10:42:38] INFO "GET /what-is-pointer/" 200 103ms
blog-ghost-ghost-1  | [2024-02-03 10:42:53] INFO "GET /understanding-otp/" 200 123ms
blog-ghost-ghost-1  | [2024-02-03 10:43:03] INFO "GET /what-is-cpp-allocator-easy/" 200 118ms
blog-ghost-ghost-1  | [2024-02-03 10:43:07] INFO "GET /floating-number-in-computer/" 200 116ms

빠르지는 않지만, 100ms 내외로 납득할만한 수치였다. 한 페이지를 선정해서 서버에서의 response 시간과 한번 비교해보자.

"GET /understanding-otp/" 200 103ms

서버의 응답을 받는데까지 664.14 ms 가 소요된 반면, 실제로 서버에서는 고작 103ms 의 시간동안 처리를 완료했다. 서버는 문제가 아니다. 우리가 요청을 하고 응답을 받는 그 중간 어디인가 속도를 저하시키고 있다.

서버 구조 살펴보기

우선 어디에서 문제가 발생하는지 확인하기 위해서 내 블로그 서버에 대해서 이해할 필요가 있다.

먼저 내 서버의 구조이다. 저 오른쪽 아래 ghost docker container 가 위치하고 있다.

그리고 내 서비스로 찾아올 때는 이렇게 들어온다.

차근차근 life.photogrammer.me 로 진입하는 경로를 살펴보자.

  1. life.photogrammer.me DNS 쿼리
  2. Cloudflare 에서 DNS resolve / proxy (*기본값이라 활성화 해뒀음)
  3. CNAME - DDNS 주소로 레코드가 기록되어있고, 내 서버의 주소를 찾음
  4. 인터넷을 타고, 내 집으로 도착 - 스위치에서 내 두 라우터로 패킷이 들어옴
  5. Router 02 로 결국 도착 80 / 443 (HTTP / HTTPS) 은 reverse proxy (NPM) 으로 포워딩됨.
  6. nginx 는 reverse proxy 수행 (layer 7에서 확인)
  7. Ghost 컨테이너로 도착

경로를 따져보았을 때 우선 다음과 같이 나눠볼 수 있었다.

외부 문제

  • Cloudflare proxy
    • DNS Resolve 시간은 오래 걸리지 않았음.
  • Client -> network -> server 에서의 망 지연 시간

내부 문제

  • router 문제
  • nginx proxy 를 하는 과정의 문제

문제 원인 찾기

내부망의 문제인가 외부망의 문제인가?

만약 내부망의 nginx 혹은 router 문제라면, 내부망을 통한 요청에서도 비슷한 시간이 소요될 것이다. 따라서, 내부 주소를 통해서 직접 접근해보기로 했다.

[2024-02-03 10:58:02] INFO "GET /what-is-cpp-allocator-easy/" 200 137ms

우선, 요청 시간은 크게 다르지 않다. 크롬 개발자 탭에서 한번 살펴보자.

!!! Waiting for server response 에서 600ms 나오던 페이지가 142ms 로 감소했다. 게다가, 이는 실제 응답시간과 5ms 밖에 차이나지 않는다. 일반적으로 내가 예상한 응답시간이다.

그러면 일단 내부적으로 발생한 문제는 아니다. 외부 문제를 살펴보자.

가정 2 - Cloudflare proxy 의 문제

나는 DNS 네임서버로 Cloudflare 를 사용하고 있다. cloudflare 의 경우에는 DNS 서비스에서 DNS Proxy 기능을 함께 제공한​다.

  • Cloudflare proxy 기능

https://developers.cloudflare.com/learning-paths/prevent-ddos-attacks/baseline/proxy-dns-records/

Cloudflare 에서 제공하는 proxy 기능을 사용하면, Cloudflare 가 일종의 CDN 역할을 수행하게된다. 사용자는 Cloudflare 서버에 먼저 방문을 한다. "proxy" 의 이름대로 중간에서 대리자의 역할을 하게 되고, 그 과정에서 캐싱 / 엣지 서비스까지 수행한다. client 입장에서도 Cloudflare 의 서버에서 결과를 얻게되니, IP 또한 cloudflare 의 IP가 노출된다.

다만, 여기서 눈여겨볼 것이 하나 있었는데, Cloudflare의 경우 한국에 Edge 서버가 별도로 존재하지 않는다...? 라는 이야기가 있다. (망사용료 때문..?) 따라서, 의미없이 더 먼 지역의 서버를 통해 받아야한다. (그럼에도 600ms 나 더 추가되는 것은 이해되지 않지만...)

우선, nslookup life.photogrammer.me 를 통해 주소가 뭘로 나오는지 확인해보자.

Server:    168.126.63.1
Address 1: 168.126.63.1 kns.kornet.net

Name:      life.photogrammer.me
Address 1: 2606:4700:3031::ac43:d1c6
Address 2: 172.67.209.198
Address 3: 104.21.16.17

Cloudflare Proxy 가 켜져있으니 역시 cloudflare 의 주소를 반환하고 있다. 지역 확인을 해보면 다음과 같다.

  • 104.21.16.17
  • 172.67.209.198

내 요청이... 미국을 다녀오고 있다...!

그러면 Cloudflare의 Proxy 가 문제라고 가정하고, 문제를 한번 해결해보자. 문제 해결은 간단하다. 단순히 Cloudflare DNS Record 에서 Proxy 기능을 끄면 된다.

이제 다시 한번 nslookup 을 통해서 내 서버를 어디서 받아오는지 확인해보자.

Server:    168.126.63.1
Address 1: 168.126.63.1 kns.kornet.net

Name:      life.photogrammer.me
Address 1: 220.88.92.79

이제는 DNS 에서 주소를 내 서버 IP 로 안내해주고 있다.

💡
이 과정에서 하나 발견한게 있는데, 내부 DNS 서버에서 DNS 캐싱을 해두는 통에, 실제로 내 맥에서 받아오는 address 와 불일치한 경우가 있었다. 또한, 맥에서도 DNS 를 캐싱을 해두기 때문에 해당 과정에서 맥 / DNS 서버의 캐시를 비워주는 과정이 필요했다.

이제 proxy 를 해제했으니 다시 한번 상태를 확인해보자.

눈물이 절로 난다. 내부 네트워크와 비교했을 때, 얼마 차이나지 않는다. 몇 번 시도하면 더 빠른(?) 경우도 있다.

현재 서버가 위치한 망과 맥이 위치한 망이 같이 때문에 정확한 측정이 불가능할 수도 있다는생각이 들어, 한번 LTE 를 통해서 접속한 경우에도 측정해보았다.

테더링을 통해서 LTE 로 접속하고 다시 시도..

이동통신망임을 감안하면, 크게 달라지지 않았다. 성공이다!

pagespeed 에서도 한 번 확인해보자.

확실히 개선됐다! 루트 문서 대상으로도 160ms 으로 750ms 대폭 개선되었다.

남은 문제

문제 상황 2의 케이스는 확실하게 해결되었다. 그러나, 문제 1의 경우 외부의 문제보다는 Ghost 에서의 렌더링 시간 자체에 문제가 있는 것으로 추측된다. 확인을 위해 서버를 다시 시작해서 한번 확인해보자.

  • 서버의 로그
INFO "GET /" 200 9217ms
  • 실제 응답 소요 시간

여전히 첫 로드시에는 굉장히 느리다. 이후 해당 페이지에 대해서 다시 접근하면 정상 속도가 나오는데, 아무래도 Ghost 에서 첫 응답 이후 해당 페이지를 캐싱해두는 것으로 보인다.

그런데 Ghost documentation 이나 포럼에서 관련한 정보를 찾아봐도 도움이 될 만한 정보가 보이지 않는다. 해당 문제에 대한 해결은 다음 글에서...


Read more

대량의 더미 데이터 DB에 주입하기

대량의 더미 데이터 DB에 주입하기

대량의 더미 데이터 DB에 주입하기 현재 만들고 있는 토이 프로젝트에서는 대량의 데이터가 존재하는 DB 를 대상으로 여러가지 테스트를 수행한다. 테스트를 수행하기 위해서 더미 데이터를 주입할 방법에 대해서 모색해야했다. SQL Procedure * SQL 문으로 직접 테이블에 데이터를 주입한다. * 직접 Insert into () values (); 와 같은 구문을 작성할 수도 있지만, Procedure 혹은 function 을

By Sungjun Park
Lombok 에 대한 짧은 생각

Lombok 에 대한 짧은 생각

최근 Java / Spring 으로 스택을 변경하면서 작은 프로젝트를 하고 있다. 팀원의 추천으로 lombok 이라는 라이브러리를 사용하게 되었다. Lombok 을 쓰면 정말 간단하다. Getter, Setter, Constructor, equals, hashCode 과 같이 작성해야하지만 작성에 피로를 느끼는 코드들을 annotation을 통해서 너무 쉽게 만들 수 있다. 그런데, 이 라이브러리 사용할수록 필요성에 대한 의문이 생긴다. 생각을

By Sungjun Park