도쿠위키 로드밸런싱

로드밸런싱은 주로 어느 정도 규모 있는 서비스가 더 많은 요청을 처리하기 위해 하는 것으로 알고 있었습니다. 오랫동안 사용해오던 위키의 규모가 처음 예상한 것보다 훨씬 커지고 이에 따라 의존성이 점점 높아지면서 몇 가지 요구사항이 추가되기 시작했습니다. 가령 데이터를 유실할 가능성을 줄여야 했고 서비스가 항상 유지되어야 했습니다. 데이터 유실 가능성은 인스턴스와 분리할 수 있는 디스크를 사용하고 이 디스크를 포함한 인스턴스 전체의 스냅샷을 매일 생성하는 정도로 줄이고 있습니다. 하지만 서비스가 항상 유지되어야 하는 요구사항은 달성하기 쉽지 않았습니다. 지난번의 장애 경험으로 라이트세일 인스턴스에 생각보다 더 자주 장애가 발생할 수 있다는 점을 깨닫게 되었고 저 스스로 위키와 웹서버에 이것 저것 실험해보면서 서비스를 망가뜨리곤 했습니다. 제 스스로 뭔가 시도하다가 위키를 망가뜨린 다음 이 상황을 벗어날 방법을 위키에서 검색할 수 없는 상황에 좌절하곤 했습니다.

그러다가 이 서비스가 항상 유지되어야 하는 두 번째 요구사항을 만족하려면 서버를 추가하고 로드밸런싱 하도록 구성해야 한다는 결론에 다다랐습니다. 지난번에 장애를 겪은 다음 서버를 추가하고 로드밸런싱을 설정한 이야기를 한 적이 있습니다만1) 도쿠위키 로드밸런싱을 주제로 직접 이야기한 적은 없는 것 같고 또 이 설정을 하면서 아무리 구글링을 해도 저와 같은 시도를 한 글을 찾기 어려워서 정확히 이 주제를 다룬 글을 남겨놓기로 했습니다. 이 글은 '도쿠위키를 로드밸런싱을 통해 서비스'하는 내용입니다.

배경

도쿠위키는 파일시스템 기반으로 동작합니다. 이 자체가 도쿠위키의 장점이기도 하고 단점이기도 합니다. 설치와 사용 모두 단순하지만 플랫폼에 영향을 받고 또 지금처럼 로드밸런싱을 통해 확장하려고 할 때 데이터베이스처럼 안정적인 확장 방법을 선택하기 어렵게 만듭니다. 도쿠위키 공식 웹사이트에는 도쿠위키가 파일시스템 기반으로 동작하기 때문에 생기는 확장성에 대한 걱정에 답하는 문서2)가 있기는 하지만 이 문서 자체가 실제로 도쿠위키를 더 큰 서비스에 학장해서 사용하는 사례를 설명하고 있지는 않습니다. 또 이 문서는 단지 세상에 얼마나 큰 도쿠위키가 존재하는지, 또 이 위키가 그런 큰 규모로 동작 가능함을 이야기할 뿐 사용자가 늘어나거나 장애상황에도 지속적으로 동작해야 하는 상황을 설명하지는 않고 있었습니다.

규모

이 위키는 공개된 부분과 공개되지 않은 부분으로 나뉩니다. 도쿠위키는 스크립트 한 세트를 설치해 놓고 이를 기반으로 서로 다른 데이터 디렉토리를 설정해 여러 위키를 구동하는 이라는 기능을 제공하지만 그렇게 사용하지는 않고 있습니다. 네임스페이스를 기준으로 공개된 영역과 그렇지 않은 영역이 있고 이들을 도쿠위키에서 지원하는 ACL3)을 통해 구분하고 있습니다. 이 위에 보안 수준을 올리기 위해 클라우드플레어 액세스를 사용해 공개되지 않은 부분에 접근할 때 추가로 인증을 요구하고 로그인에는 특정 VPN을 사용하도록 설정4)했습니다. 공개된 영역에는 이들 중 어느 것도 요구하지 않고 있습니다. 위키는 캐시파일을 제외하고 텍스트와 이미지를 모두 합쳐 약 20기가 정도이고 라이트세일에서 32기가짜리 디스크를 사용해 서비스하고 있습니다. 등록된 사용자는 10명 이하, 하루 평균 편집 횟수는 50회 정도입니다.

주제

로드밸런싱

단순히 위키 규모가 커지거나 사용자가 늘어나는 상황이라면 더 큰 라이트세일 번들5)을 사용해 문제를 간단히 해결할 수 있습니다. 지금은 2기가 메모리 번들을 사용해 서비스하고 있는데 어직까지는 도쿠위키에 제가 요구하는 기능을 소화하는데 특별히 부족함을 느끼고 있지는 않습니다. 특히 클라우드플레어가 웹서버 앞에서 굳이 웹서버가 직접 받을 필요 없는 요청을 처리해주고 있어6) 웹서버 사양을 더 올릴 필요를 느끼고 있지는 않습니다. 하지만 위에서 이야기한 대로 서비스 중단을 줄이고 제 실험에 의해 서비스가 중단되는 상황에도 위키를 계속해서 사용할 수 있기를 바랬고 이 요구사항을 만족하려면 더 큰 인스턴스 대신 인스턴스 개수를 늘려야 한다는 결론에 이르렀습니다. 로드밸런싱을 해야 했습니다.

당장 쉽게 찾을 수 있는 로드밸런싱 서비스에는 라이트세일과 클라우드플레어에서 제공하는 서비스가 있었습니다. 라이트세일에서는 월 $18을 고정비용7)을 내면 서버 수, 요청 횟수, 트래픽에 관계 없이 로드밸런싱을 구축할 수 있었습니다. 클라우드플레어는 월 $58)를 내면 서버 두 대로 로드밸런싱을 시작할 수 있지만 서버 수를 늘리거나 요청 횟수가 늘어나면 비용이 늘어나는 구조입니다. 제 경우에는 규모가 작기 때문에 오히려 클라우드플레어의 종량제 요금이 더 저렴해서 이쪽을 선택했습니다. 만약 언젠가 규모가 커진다면 라이트세일에서 제공하는 고정 요금 로드밸런싱으로 이전할 여지는 있습니다.

클라우드플레어의 로드밸런싱은 API를 사용하지 않고, 또 라이트세일 쪽에서 별도로 DNS를 설정하지 않은 채 사용하려면 서버 개수만큼 아이피 주소가 필요합니다. 지금은 라이트세일 인스턴스 두 개로 로드밸런싱을 구축하고 있는데 인스턴스 두 개가 별도의 아이피 주소를 가지고 있고 클라우드플레어의 DNS 설정을 통해 접근하게 됩니다. 각 서버는 기존과 동일하게 A레코드설정을 해 서브도메인을 통해 접근할 수 있는 상태여야 하고 이를 기반으로 로드밸런싱 설정을 하게 됩니다. 로드밸런싱 설정은 먼저 서버 풀을 생성하고 그 풀 안에 속할 서버 각각을 추가하게 되는데 월 $5로 할 수 있는 것은 풀 한 개와 그 안에 들어갈 서버 두 대 까지입니다. 정확히 지금 제가 하고 있는 서비스까지만 이 금액으로 처리할 수 있습니다. 여기서 풀 개수를 늘리거나 서버 수를 늘리려면 비용이 약 $5 단위로 증가합니다.

일단 설정을 마치면 몇 초 안에 배포되어 도메인 네임을 입력하면 서버 두 대 중 한 대가 응답하기 시작합니다. 세션 어피니티9) 설정이 있어 서버가 여러대이더라도 정해진 시간 동안은 처음에 응답한 서버가 계속해서 응답해 로그인 유지, 동기화를 편하게 해줍니다.

동기화

도쿠위키가 파일시스템 기반으로 동작하기 때문에 이런 식으로 확장할 때 여러 가지 고민거리를 만듭니다. 구글링해보면 도쿠위키를 확장하고 싶은데 파일시스템 기반이라 어떻게 해야 할지 잘 모르겠다는 글10)들이 나타납니다. 아마도 상황에 맞는 방법을 알고 있는 전문가들은 이미 도쿠위키를 떠났거나 네트워크 파일시스템을 통해 이미 문제를 해결했을 것 같지만 이런 글은 구글에 나타나지 않았습니다. 저는 이런 설정을 할만큼 전문가는 아니었고 또 한동안은 도쿠위키를 떠날 생각도 없으므로 문제를 해결할 방법을 찾아야 했습니다. 정확히는 서버 두 대 사이에 파일시스템을 어떻게 동기화할지 정하고 실행해야 했습니다.

이전에 동기화에서 이야기한 대로 unison11)을 크론탭에 넣고 짧은 주기로 실행하고 있습니다. 근본적으로는 파일시스템이 변경될 시점마다 동기화를 수행해야 가장 안전하고 또 낭비를 줄일 수 있는 방법일 거라고 생각합니다. 하지만 어째서인지 구글에 나타난 파일시스템을 동기화하려는 요구사항을 가진 사람들은 최대 1분 단위로만 수행할 수 있는 크론탭에 꼼수를 써서 더 짧은 주기로 동기화를 수행하는 방법12)을 더 많이 사용하고 있었습니다. 일단 제가 당장 실행 하기에도 이쪽이 훨씬 쉬워 보였기 때문에 저도 똑같이 따라했고 아직까지는 별다른 사고는 없었습니다. 하지만 잠재적으로 동기화 작업 한 번이 동기화가 일어나는 최소 시간단위보다 더 길 경우 문제가 생길 수 있습니다.

unison은 서버 두 대 중 한 대에서만 실행합니다. 나머지 한 대는 동기화를 받기만 할 뿐 스스로 동기화를 실행하지 않습니다. 만약 미래에 인스턴스 수를 늘려야 하면 '동기화를 받는 쪽' 인스턴스를 기반으로 증설하고 동기화를 실행하는 인스턴스에는 동기화 설정을 추가해 대응할 계획입니다.

인증서

서비스 전체의 보안을 유지하려면 모두 세 구간에 인증서가 필요합니다. 하나는 클라이언트와 클라우드플레어 구간입니다. 여기에는 클라우드플레어가 제공하는 무료 인증서를 사용합니다. 데스크탑 브라우저에서는 주소 옆에 있는 자물쇠 아이콘을 클릭해 인증서 정보를 볼 수 있는데 여기에 제 도메인네임 대신 클라우드플레어 도메인네임이 나타나기는 합니다만 모바일 기계에서는 대부분 인증서를 직접 볼 일이 없어 인증서에 내 도메인네임이 나타나지 않는다는 점이 문제는 아니라고 생각했습니다. 그리고 이 구간의 트래픽을 암호화하려는 원래 목적을 충분히 달성하고요.

다른 둘은 서버 두 대와 클라우드플레어 서버 구간입니다. 처음에는 Let's Encrypt인증서를 발급받아 사용했는데 서브도메인 각각마다 인증서를 발급받고 유지하는 일은 자동화할 수는 있지만 근본적으로 관리부담을 늘린다고 생각했습니다. 그래서 클라우드플레어에서 제공하는 오리진 인증서13)를 사용하도록 바꿨습니다. 이 오리진 인증서는 클라우드플레어가 사인해 인증서 자체는 유효합니다만 루트인증서로부터 모든 인증 경로에는 유효하지 않습니다. 그래서 이 인증서를 사용한 채로 클라우드플레어를 거치지 않고 서비스하면 인증이 깨진 상태로 표시됩니다. 하지만 모든 요청은 항상 클라우드플레어를 통하므로 클라이언트에 요청이 깨진 상태로 표시될 일은 없었고 인증서의 유효기간이 길어 관리부담이 적을 뿐 아니라 클라우드플레어와 오리진 서버 두 대 사이를 암호화하는 목적을 달성할 수도 있었습니다.

코드 배포

데이터와 설정은 짧은 주기로 동기화됩니다. 하지만 도쿠위키 스크립트는 그렇게 하면 안될 것 같았습니다. 웹서버와 위키에 자주 실험을 했고 만약 실험이 실패하면 서비스가 일시적으로 중단되곤 했습니다. 이 때 도쿠위키 스크립트도 동기화된다면 한 서버에서 생긴 문제가 다른 서버로 빠르게 퍼질 수 있었습니다. 하지만 코드를 수정할 일은 있었고 이를 편안하게 배포할 방법이 필요했습니다. 아마도 전문가들은 다른 방법이 있을 것 같습니다만 저는 깃헙에 리파지토리14)를 생성해 코드를 배포하고 있습니다. 먼저 스냅샷으로 인스턴스를 생성해 거기서 코드를 수정하고 테스트합니다. 여기서 별 문제가 없는 것 같으면 깃헙에 푸시하고 서버 두 대 중 어느 한 대에만 배포한 다음 한동안 놔둬봅니다. 그래도 별 일 없는 것 같으면 나머지 한 대에도 배포합니다.

이런 방법은 얼마 전에 도쿠위키의 새 RC15)를 적용16)할 때 사용했습니다. 먼저 브랜치를 만들어 새 코드를 적용한 다음 검토해보고 새로 만든 인스턴스에만 반영해 얼마 동안 테스트했습니다. 큰 문제를 겪지 않았고 또 세 번째 RC에서는 모든 변경사항이 코드 정리에 해당한다고 판단해 이를 각 서버에 하루 간격을 두고 배포했습니다. 물론 이렇게 배포할 수 있는 이유는 이번 메이저 버전 업데이트는 파일시스템을 마이그레이션 하지 않기 때문입니다. 만약 미래의 새로운 버전이 파일시스템을 마이그레이션 할 경우에는 이런 식으로 한대씩 배포할 수는 없을 겁니다.

문제

동기화 간격

파일시스템 동기화는 시간 간격에 따라 일어납니다. 지금은 10초에 한번 일어납니다. 처음 1분 간격으로 설정할 때는 한 쪽에는 글을 썼지만 다른 쪽에는 아직 반영되지 않아 주소를 공유할 때 없는 페이지가 나타나는 경우가 있었습니다. 그래서 얼마 동안은 주소를 공유할 때 모든 서버의 서브도메인을 열어 양쪽 모두에 글이 똑같이 표시되는지 확인한 다음 주소를 공유했습니다. 지금은 동기화 간격을 줄였고 어느 정도 신뢰할 수 있게 동작해 더이상 주소를 공유하기 전에 모든 서버를 확인하지는 않습니다.

또 사용자가 늘어나 같은 페이지를 동기화 간격보다 짧은 간격에 서로 다른 서버에서 수정할 경우 일찍 수정한 쪽의 데이터가 유실됩니다. 양쪽에서 동시에 수정이 일어날 때 이를 자동으로 머지하는 건 아주 어려운 일이었습니다. 간단히 더 나중에 수정된 쪽으로 덮어쓰고 있는데 제 규모에서는 아직까지 문제가 일어나지는 않았습니다만 문제가 없는 것은 아닙니다. 잠재적으로 수정이 잦은 페이지에서는 수정사항을 유실할 수 있습니다.

바깥 편집

도쿠위키는 파일시스템의 타임스탬프를 통해 도쿠위키가 모르는 편집을 감지하는 기능이 있습니다. 만약 누군가, 또는 어떤 다른 앱이 도쿠위키 데이터파일을 직접 열어 수정하면 파일 내부에 기록된 타임스탬프와 파일 자체의 타임스탬프에 차이가 생깁니다. 이 상태에서 다음번에 이 페이지를 수정하려고 하면 먼저 이 도쿠위키가 모르는 변경사항을 새로운 리비전으로 만든 다음 이 리비전을 열어 수정을 시작하게 합니다. 그런데 파일을 동기화하는 환경에서는 수정사항을 동기화 받는 쪽이 항상 타임스탬프가 틀리게 됩니다. 한쪽 서버에서는 파일 내부의 타임스탬프와 파일 자체의 타임스탬프가 일치하겠지만 다른쪽 서버에서는 파일 자체의 타임스탬프가 더 미래의 시각을 가리키게 되어 항상 '바깥 편집' 기록이 남게 됩니다. 동기화되는 서버 두 대는 항상 같은 리비전 기록을 보여주되 동기화가 일어난 기록들마저 리비전으로 등록되기를 원하지는 않았기 때문에 detectExternalEdit함수를 부르지 않도록 설정17)했습니다. 다만 이 설정으로 인해 실제로 바깥 편집이 발생할 때 이 리비전을 기록하지 못하게 됩니다.

다른 한 가지 바깥편집 문제는 페이지의 최신 리비전을 다른 서버로부터 동기화 받은 경우 이 페이지를 수정한 사용자가 '바깥 편집'으로 나타나는 점입니다. 이건 리비전에 기록되지는 않고 다음 수정이 일어나면 리비전 상에는 정상적으로 기록되지만 최신 페이지가 한쪽 서버에서는 수정한 사용자 정보를 정상적으로 표시하는 반면 다른쪽 서버에서는 수정한 사용자 정보 대신 '바깥 편집'으로 표시됩니다. 이건 템플릿 출력 코드에서 수정할 수 있을 것 같아 보이지만 페이지를 편집한 사용자 정보가 없을 때 바깥 편집으로 표시하게 되어 있어 사용자 정보가 없는 이유를 이해하지 못했기 때문에 수정하지 않았습니다.

세션 어피니티

이 바깥 편집 문제를 해결하면서 세션 어피니티 기능이 동작하는 상황을 알게 되었습니다. 세션 어피니티는 로드밸런싱을 통해 어느 한 쪽 서버에 접근한 사용자를 계속해서 그 서버로만 라우팅하는 기능입니다. 이 기능이 없다면 방금 한쪽 서버에 로그인한 사용자가 로그인 직후 다른 서버로 라우팅 되어 다시 로그인을 요구받게 될 수도 있습니다. 위에 바깥 편집 문제와 연결되어 페이지를 수정하고 저장한 시점에는 페이지 히스토리에 사용자 정보가 표시되지만 잠시 후 다시 그 페이지에 접근해보면 '바깥 편집'으로 바뀌어 있을 때가 있습니다. 이는 제가 새로고침을 하며 위키를 사용하는 동안에 한 서버에서 다른 서버로 라우팅되었다는 것을 의미합니다.

결론

만약 도쿠위키가 데이터베이스를 사용했다면 로드밸런싱 대부분에 데이터베이스 리플리케이션 이야기만 했을 겁니다. 또 한편으로는 unison같은 또 다른 도구를 사용하는 대신 그냥 데이터베이스 자체의 기능을 사용해 위에 이야기한 잠재적인 문제 없이 더 안정적으로 로드밸런싱을 구축했을 수도 있습니다. 하지만 제 규모에서는 잠재적인 문제를 포함해 어쨌든 동작하는 도쿠위키 로드밸런싱을 구축했고 한동안 문제 없이 동작하고 있습니다. 서버를 증설하려면 동기화 받는 쪽 서버를 증설하면 되고 데이터 유실 가능성을 줄이려면 동기화 하는 쪽 서버를 백업하면 됩니다. 만약 미래에 어느쪽 서버가 유실되더라도 다른쪽 서버의 데이터나 다른 가용 영역에 있는 데이터를 통해 복구할 수 있습니다. 맨 위에 이야기한 두 가지 목적을 어느 정도 달성했다고 판단합니다.