4 minutes
로드발란서로 ActivityPub 요청을 분리해 처리하기
큐돈의 서버는 평소 사용량에 맞게 할당되어 있어 사용자가 갑자기 증가한다면 서비스가 느려지게 됩니다. 하지만 사용자들이 갑자기 폭주하지 않아도 서버 사용량이 피크를 찍는 경우가 있는데, 사용자가 아닌 다른 서버들이 와르르 접속하는 것입니다. 이 때 큐돈의 사용자들에게도 영향이 가게 되는데 이를 방지하기 위해 로드발란서를 설정한 방법을 알아봅시다.
원인
왜 다른 서버들이 갑자기 큐돈을 와르르 방문할까요? 이유는 두 가지 중 하나입니다.
- 외부에서 큐돈을 링크한 경우
- 누군가가 큐돈에 있는 글에 부스트/답글 등의 반응을 한 경우
첫 번째 경우는 그렇게 큰 일은 아닙니다. 페이스북에 공유했다면 페이스북의 크롤러가, 트위터에 공유했다면 트위터의 크롤러가 글을 한 번만 방문하면 되기 때문에 서버에 부하가 크게 오지 않습니다. 다만 연합우주에 공유한 경우에는 그 글을 받아보는 모든 서버들이 큐돈을 방문하기 때문에 서버에 부하가 크게 올 수 있습니다. 다만 마스토돈의 경우 그런 피해를 줄이고자 1분 미만의 랜덤한 딜레이를 두고 방문하여 다른 서버에 의도치 않은 DoS 공격이 가지 않도록 하고 있습니다만 다른 모든 서버가 마스토돈은 아니기에 이 경우엔 부하가 커질 수 있습니다.
두 번째 경우는 연합우주의 누군가가 연합우주 본연의 기능인 답글이나 부스트 등의 반응을 했을 때입니다. 이 때는 그 글을 받아보는 (예: 부스트한 사람의 팔로워가 있는 서버) 서버들이 큐돈을 방문하게 됩니다. 요청에 대한 캐시를 이용할 수도 있겠지만 큐돈의 경우 프라이버시를 최대한 보호하기 위해 AUTHORIZED_FETCH 기능을 켜놓아 글을 요청하는 서버를 일일히 검증하기 때문에1 캐시를 이용할 수 없습니다. 이 경우에는 딜레이가 딱히 없기 때문에 아주 유명한 사람이 큐돈에 있는 글을 부스트한 경우 서버에 동시에 수천 개의 요청이 들어올 수 있습니다.
큐돈에 사용자가 몰려서 느려진 경우라면 억울하지도 않고 장기적으로 사용자가 많아진다면 서버 사양을 늘리면 되는 일입니다. 하지만 앞서 말한 두번째 경우는 한 순간에 몰리는 피크일 뿐입니다. 이것 때문에 서버 사양을 늘리는 것은 낭비입니다.
해결 방법
결론부터 말하자면 액티비티펍의 요청을 따로 처리하면 됩니다. 큐돈을 직접 사용하는 사람들의 경우 ActivityPub 요청을 전혀 사용하지 않고 웹페이지의 HTML/CSS/JS 삼종신기와 더불어 API만 사용하고 외부 서버에서는 ActivityPub 요청만 사용하기 때문에 구분이 쉽습니다2
그럼 로드발란서에서 AP 요청을 따로 처리하는 방법을 알아봅시다. 큐돈은 CloudFlare의 로드발란서를 사용하고 있습니다. 기본적인 로드발란서 설정은 생략합니다.
설정하다보면 Traffic Steering이라는 항목이 있습니다. 요청이 각각의 서버로 가는 방법을 조절하는 부분인데 여기서 방식은 아무 거나 상관이 없지만 저는 Least outstanding request steering을 택했습니다. 가장 요청을 적게 처리하고 있는 서버로 요청을 보내는 합리적인 방식입니다. 여기서 중요한 것은 그 아래에 있는 weight입니다. 기본적으로 모든 서버가 같은 비율로 요청을 처리하도록 되어 있는데 여기서 비율을 1:0.01 등으로 설정해 한쪽 서버가 더 많은 요청을 처리하도록 만듧니다. 더 많은 요청을 처리하는 서버를 A, 그렇지 않은 서버를 B라고 하겠습니다. 이렇게만 설정하면 그냥 B 서버가 한적할 뿐입니다.
Traffic Steering 다음으로 Custom Rules 항목이 있습니다. 여기에서는 특정 요청에 대해 어떻게 처리할지 설정을 할 수 있습니다. 여기에서 AP 요청을 걸러내보겠습니다.
(any(http.request.headers["accept"][*] contains "application/activity+json"))
or (any(http.request.headers["accept"][*] contains "application/ld+json"))
or (http.request.method eq "POST" and any(http.request.headers["content-type"][*] eq "application/activity+json"))
or (http.request.uri.path matches "^/.well-known/(webfinger|host-meta).*$")
or (http.request.uri.path eq "/api/v2/media")
위와 같은 표현식을 사용하면 되는데 저는 AP 요청과 함께 더불어 미디어 요청도 걸러내도록 하였습니다. 간단히 설명하면 다음과 같습니다.
Accept헤더에application/activity+json또는application/ld+json이 포함된 요청- 혹은
Content-Type헤더에application/activity+json이 포함된 POST 요청 - 혹은
/.well-known/webfinger또는/.well-known/host-meta로 시작하는 URI에 대한 요청 - 혹은
/api/v2/media에 대한 요청
해당 조건에 대해 처리 방법은 Override로 하여 B 서버가 처리하도록 합니다. 이렇게 설정하면 AP 요청은 B 서버가 처리하고 나머지 요청의 대부분은 A 서버가 처리하게 됩니다.
하지만 여기서 문제가 있습니다. B 서버가 다운되면 AP 요청은 B에서 처리되지 못하고 에러를 내뿜게 됩니다. 이래서야 로드발란서를 사용하는 의미가 없겠죠. 그래서 Fallback Pool이라는 항목을 추가해 B 서버가 다운된 경우 다른 서버에서 처리할 수 있도록 설정해줍니다. 이렇게 하면 AP 요청은 기본적으로 B 서버로 가지만 B 서버가 다운된 경우에는 다른 서버로 가게 됩니다.
결론
사실 로드발란서를 사용해도 뒷단에서 실제로 요청을 처리하는 마스토돈 프로세스를 공유하고 있기 때문에 앞단에서 갈려봤자 요청이 몰려온다는 점은 똑같아서 효과가 있을까 싶었습니다. 하지만 실제로 2024년부터 적용해보니 리버스프록시도 요청 풀을 따로 관리하고 있기 때문에 요청이 심하게 몰리는 경우 먼저 온 요청부터 순차적으로 처리하고 풀에 빈 공간이 생길 때까지 다른 요청은 대기하게 되어 효과가 있었습니다. 예전엔 다른 서버의 누군가가 큐돈에 있는 글을 부스트한 경우 사용자에게 체감이 될 정도로 1-2분 정도 느려지는 경우가 있었는데 지금은 그런 경우를 거의 못 느끼고 있지만 실제로 로그를 보면 AP를 처리하는 서버에 요청이 몰려있는 기록을 발견할 수 있었습니다.
이를 통해 추가적인 비용을 사용하지 않고 (CloudFlare 로드발란서는 유료지만 원래부터 사용하고 있었고 설정만 바꿨습니다) AP 요청에 대한 처리만 분리하여 사용자들에게 쾌적한 서비스를 제공할 수 있게 되었습니다