Tech-Verse 2022에서 변일수 님이 발표한 하이퍼 스케일 프로젝트, Ceph의 한계를 극복하고 구축한 방법 세션 내용을 옮긴 글입니다.
안녕하세요. LINE Plus 클라우드 스토리지 팀에서 Ceph 스토리지 개발 및 운영 업무를 맡고 있는 변일수입니다. 이번 글에서는 Ceph으로 구축한 하이퍼 스케일 오브젝트 스토리지를 소개하려고 합니다. 하이퍼 스케일 오브젝트 스토리지를 구상한 배경을 말씀드리고, 왜 Ceph 단일 클러스터로 문제를 해결할 수 없었는지 살펴본 뒤, 적용한 해결책과 몇 가지 최적화 방법을 소개하는 순서로 진행하겠습니다.
본격적으로 들어가기 전에 아래 슬라이드를 보겠습니다. 무슨 사진일까 궁금하실 텐데요. Ceph 클러스터를 운영하면서 오픈소스 커뮤니티에 기여할 수 있는 기회가 운 좋게 몇 번 있었습니다. Ceph은 새로운 버전이 출시될 때마다 기여자들에게 이름이 적힌 티셔츠를 보내주는데요. 아래 사진은 제가 받았던 티셔츠 사진들로 Octopus 버전부터 최근에 받은 Quincy 버전까지의 티셔츠입니다. 오픈소스 프로젝트에 참가하면서 얻은 뜻깊은 경험을 단적으로 잘 보여주는 것 같아서 도입부에 넣어봤습니다.
하이퍼 스케일 오브젝트 스토리지 구축 배경
LINE에서는 다양한 형태의 데이터를 저장합니다. 이미지와 영상 데이터가 상당히 많고, 크기는 수 KB에서 수 GB까지 다양합니다. 이와 관련된 메타 데이터도 저장하는데요. 이 파일들은 수십 바이트 정도 됩니다. 이와 같이 저장하는 데이터의 크기가 다양하다는 것은 스토리지 측면에서 관리하기가 쉽지 않다는 뜻입니다.
또한 하이퍼 스케일 오브젝트 스토리지를 구상하면서 예상한 저장 총량은 수백 PB 정도, 오브젝트 수는 3000억 개 이상인데요. 이 정도면 상당히 큰 규모로, 서버를 가득 채운 랙을 80개 정도 사용하는 규모입니다.
데이터 저장 솔루션으로 Ceph을 선택한 이유
저희는 이와 같은 데이터를 저장하기 위한 솔루션으로 Ceph을 선택했습니다. Ceph은 오랜 기간 확장성과 안정성이 검증된 솔루션이며 오픈소스이기 때문에 상대적으로 비용도 저렴합니다. 물론 오픈소스라고 인터넷에서 다운로드해서 설치하기만 하면 끝나는 것은 아닙니다. 오픈소스를 운영하기 위해서는 Ceph 내부를 잘 이해하고 다룰 수 있는 내부 인력이 필요한데요. 규모가 작을 때는 경제성이 없지만 규모가 커지면 경제성을 확보할 수 있습니다.
또한 Ceph은 NAS와 스케일 아웃 NAS, 블록 스토리지, 공유 파일 시스템, 오브젝트 스토리지 등 다양한 형태의 스토리지 서비스를 제공합니다. 따라서 다른 형태의 스토리지를 제공하고 싶을 때 다시 새로운 솔루션을 도입하고 관련 인력을 충원할 필요가 없습니다. Ceph 담당 인력이 여러 형태를 모두 커버할 수 있습니다.
저희는 이와 같은 장점을 고려해 Ceph을 선택했습니다.
Ceph 클러스터 운영 현황
아래는 현재 LINE에서 운영하고 있는 Ceph 클러스터 현황입니다. 30개 이상의 독립된 클러스터와 2500대 이상의 서버로 구성돼 있습니다. OSD(object storage daemon) 수, 다시 말해 데이터를 저장하는 디스크 개수는 7만 개 이상이며, 로우(raw) 크기(물리적 총량)는 700 PB 이상입니다. 아마 아시아에서 이 정도 규모로 Ceph을 설치해서 운영하고 있는 회사는 많지 않을 것이라고 생각합니다.
그런데 이는 용도별로 독립적으로 구성해 운영하고 있는 클러스터의 현황이고요. 이번 글에서는 하나의 용도, 하나의 목적으로 구성한 클러스터가 대규모인 경우를 말씀드리려고 합니다. 여기서 대규모라는 것은 어느 정도로 큰 규모일까요?
현재 하이퍼 스케일 오브젝트 스토리지라는 이름으로 구성된 클러스터의 규모는 약 3000개의 디스크와 150대 이상의 서버, 총 크기가 20 PB인 클러스터 10개의 규모입니다.
다시 말씀드리지만 클러스터 하나가 아니라 클러스터 '10개'의 규모입니다. 왜 하나가 아니라 10개로 구성했을까요?
Ceph 확장성의 한계
Ceph은 확장성이 대단히 좋긴 했지만 한계가 있었습니다. 특정 규모 이상으로 커지면 일부 Ceph 컴포넌트가 불안정해지는 것을 경험했습니다. 아래는 이와 관련해서 Ceph 기술 블로그에 올라온 글입니다.
- https://ceph.io/en/news/blog/2022/scaletesting-with-pawsey/
- https://ceph.io/en/news/blog/2022/mgr-ttlcache/
그럼 각 문제를 조금 더 자세히 살펴보겠습니다.
Ceph 매니저 컴포넌트 문제
Ceph에는 Ceph 매니저라는 컴포넌트가 있습니다. Ceph 클러스터의 상태를 보관하고 지표를 수집하며 플러그인 관리 역할을 수행하는 컴포넌트인데요. 이 매니저 컴포넌트에 구조적 한계가 있었습니다.
매니저 컴포넌트는 데몬이 여러 개 있더라도 그중 하나만 활성화돼 작동하고 나머지는 대기합니다. 활성화된 데몬이 멈추거나 종료되면 대기하던 데몬이 활성화되는 방식인데요. 이는 클러스터 규모가 아무리 크더라도 결국 모든 OSD의 상태를 보관하고 수집하는 역할은 단 하나의 데몬이 수행한다는 뜻입니다. 그 때문에 규모가 특정 임계치를 넘어서면 Ceph 매니저가 계속 멈추거나 잘못된 정보를 반환하는 상황이 발생했습니다.
아래는 OSD 개수를 약 4천 개로 확장하면서 수집한 지표를 그래프로 나타낸 것으로, 그래프가 주기적으로 끊기는 모습을 확인할 수 있습니다. Ceph 매니저가 주기적으로 다운됐다는 뜻입니다. 4천 개 정도되는 클러스터에서 플레이스먼트 그룹에 변화가 발생하면 Ceph 매니저가 버티지 못하고 계속해서 다운됐고 Ceph 클러스터 상태도 제대로 보여주지 못했습니다.
물론 Ceph 매니저가 다운돼도 Ceph 클러스터 기능에 문제가 생기는 것은 아니지만, 이 정도 규모를 관리하려면 안정성이 필요했기에 이 문제를 해결할 필요가 있었습니다.
Ceph 모니터 문제
드물지만 Ceph 모니터에서도 문제가 발생했습니다. 잘못된 클라이언트가 잘못된 요청을 보내서 모니터 메모리가 계속 증가하다가 메모리 부족(out of memory)으로 종료당하는 경우가 있었는데요. 다른 데몬이 그 역할을 잘 이어 받으면 별문제가 되지 않았지만 그렇지 못한 경우도 있었습니다.
아래는 Ceph 모니터 데몬 세 개가 동시에 응답 불능 상태가 되면서 전체 클러스터에 문제가 발생했던 상황입니다. 왼쪽을 보면 마치 전체 OSD가 다운된 것처럼 표현돼 있고, 오른쪽 그래프를 보면 중간에 그래프가 끊겨 있는 것을 확인할 수 있습니다. 이 시간 동안 Ceph 모니터에 문제가 발생했던 것입니다.
이는 실제로 겪었던 상황으로 LINE 메시지와 관련된 스토리지는 아니었고 내부에서 사용하던 스토리지에서 대략 3~40분 가량 장애가 발생했던 상황입니다.
원인은 Ceph 모니터의 버그였습니다. Ceph은 전체 클러스터 상태를 OSD 맵이라는 곳에 보관합니다. OSD 맵에는 변경을 추적할 수 있는 epoch
라는 값이 있는데요. 클러스터에 노드가 추가 혹은 삭제되면 이 값을 변경합니다. 예를 들어 OSD가 3개일 때 OSD 맵의 epoch
가 1이었다면, OSD를 하나 더 추가하면 epoch
가 2가 됩니다. 이런 방식으로 OSD 맵 히스토리를 보관하는데 모든 히스토리를 보관할 수는 없기에 보통 500에서 700 사이의 값을 설정해 놓고, epoch
가 이 값 이상이 되면 삭제합니다.
그런데 이 삭제 처리가 제대로 되지 않는 상황이 간혹 발생했습니다. 아래 그래프를 보면 정상적으로 삭제 처리되지 않아 주황색 선이 계속 올라가는 것을 볼 수 있습니다.
이 상황에서 뒤늦게 Paxos 커밋이 발생하면 Ceph 모니터가 응답을 못 하는 상황이 발생할 수 있습니다. 앞서 말씀드렸듯 Ceph 모니터 데몬은 세 개이고 하나가 응답하지 못하면 다른 데몬이 그 역할을 이어받는 방식으로 안정성을 유지하는데 이때는 그조차 작동하지 못했습니다. Ceph 모니터는 Paxos라는 프로토콜을 이용해 동기화하는데요. 모니터 데몬 세 개의 히스토리가 동일했고, 이때 데몬 응답에 영향을 주는 커밋이 동일하게 발생하면서 세 데몬 모두 동시에 문제가 발생했던 것으로 추정하고 있습니다.
이 문제도 규모가 커지면서 발생한 문제라고 볼 수 있습니다. 현재 문제가 됐던 부분은 오픈소스 커뮤니티의 패치를 적용해 대응했고, 위 값을 계속 모니터링하면서 적절한 수준으로 관리하고 있습니다.
Ceph 확장성의 한계를 극복하기 위해
이와 같이 Ceph의 한계를 살펴봤는데요. 그렇다고 Ceph이 불안정하다는 뜻은 절대 아닙니다. 그동안 적절한 규모에서는 아주 잘 작동했고, 안정성이나 확장성 면에서 탁월한 성능을 보여줬습니다. 게다가 여기서 말하는 적절한 규모라는 것도 절대 작은 규모는 아니었습니다. 그렇기에 계속 사용하고 있는 것입니다.
다만 규모가 그 이상으로 커지면 예상치 못한 문제가 발생할 수 있기에 계획하고 있던 대규모 스토리지를 하나의 클러스터로 구성하기에는 위험 부담이 있었습니다.
하이퍼 스케일 오브젝트 스토리지 소개
저희는 Ceph의 한계를 극복하려면 클러스터를 관리 가능한 단위로 나눠 관리하는 것이 필요하다고 생각했습니다. '계란을 한 바구니에 담지 말라'는 유명한 격언을 따라서 여러 바구니를 사용하는 것입니다. 이에 다음과 같은 사항을 고려해서 하이퍼 스케일 오브젝트 스토리지를 설계했습니다.
- 수백 PB를 저장할 수 있어야 합니다.
- 지속해서 확장할 수 있어야 합니다. 이는 Ceph 한계를 넘어 계속 확장할 수 있어야 한다는 뜻입니다.
- 결함에 내성이 있어야 합니다.
- S3와 호환돼야 합니다.
- 클러스터 규모가 상당히 크기 때문에 데이터를 효율적으로 저장하고 관리할 수 있어야 합니다.
- 위 모든 조건을 만족하면서 사용자에게는 여러 클러스터가 아니라 마치 하나의 클러스터처럼 작동해야 합니다.
클러스터 연합 구성 방법
그럼 클러스터 연합을 어떻게 구성했는지 살펴보겠습니다.
클러스터 연합 구조
클러스터 연합을 구상한 방법은 클러스터를 여러 클러스터로 분리하되, 마치 하나의 클러스터처럼 작동하도록 추상 레이어를 제공하는 것이었습니다. 한마디로 클러스터의 클러스터를 구성하는 것입니다. 이때 다음과 같은 제약사항을 둘 필요가 있었습니다.
- 클러스터를 구성하는 클러스터의 수는 증가할 수 있어야 합니다.
- 기존 클러스터는 제거하지 않습니다.
- 각 클러스터 간 트래픽을 가중치 기반으로 제어할 수 있어야 합니다.
- 데이터 리셔플링 혹은 데이터 리밸런싱은 지원하지 않습니다.
아래 그림은 클러스터의 클러스터를 어떻게 구성했는지 개략적으로 표현한 그림입니다.
맨 밑에는 Ceph 클러스터와 로드 밸런서가 위치합니다. 각각 독립적으로 작동하는 하나의 클러스터로 현재 10개가 있습니다.
그 위에 NGINX로 라우팅 레이어를 구현했습니다. 이 라우팅 레이어는 단순 리버스 프락시는 아닙니다. 추가로 담당해야 할 일이 있기 때문인데요. 먼저 특정 버킷의 트래픽을 어떤 클러스터가 처리할 것인지를 여기서 결정합니다. 앞서 제약사항에서 트래픽을 가중치 기반으로 제어할 수 있어야 한다고 했는데요. 하부의 각 클러스터로 향하는 트래픽이 항상 균등하다고 볼 수 없고, 저장 용량에 차이가 있는 경우도 있기 때문에 상부에서 가중치 기반으로 트래픽을 제어할 수 있어야 했습니다. 또한 대규모 트래픽을 감당할 수 있어야 하기에 다수의 로드 밸런서와 라운드 로빈 DNS를 이용해서 확장성도 확보했습니다.
클러스터 맵 도입
앞서 보여드린 것처럼 데이터를 저장하는 곳은 결국 독립 클러스터입니다. 데이터 내구성(durability)을 위한 데이터 레플리케이션은 각 클러스터 내에서 수행되고, 클러스터 간 레플리케이션은 별도로 수행하지 않습니다. 그렇다면 사용자 트래픽이 어떤 클러스터에서 처리될지를 어떻게 결정할 수 있을까요? 앞서 제약 조건에서 말씀드린 것처럼 클러스터를 구성하는 멤버 클러스터 구성은 시간에 따라 계속 변하는데요. 이 변화를 추적하기 위해 라우팅 레이어에서 클러스터 맵이라는 것을 관리하고 있습니다.
클러스터 맵에는 클러스터를 구성하는 멤버 클러스터와, 각 멤버 클러스터에 대한 가중치 값, 클러스터 변화에 따라 증가하는 epoch
값을 저장합니다. 아래 그림을 보면 클러스터 두 개로 구성됐던 클러스터(왼쪽)가 클러스터 네 개로 구성된 클러스터(오른쪽)로 확장되면서 epoch
값이 증가한 것을 볼 수 있습니다.
또한 각 버킷이 어떤 클러스터 맵에 속하는지도 관리합니다. 이 정보를 이용해 특정 버킷에 대한 트래픽이 들어오면 버킷이 속한 클러스터 맵을 결정하고, 클러스터 맵 내에서 버킷 이름 해시 값을 기반으로 클러스터를 결정합니다. 위 그림을 보면 버킷 A, B가 클러스터 1, 2에 분산되고, 확장 후에는 버킷 C, D가 클러스터 1, 2, 3, 4에 분산되는 모습을 볼 수 있습니다.
클러스터 맵에 변화가 발생하면 epoch
값이 증가한다고 말씀드렸는데요. 이때 두 가지 경우가 있습니다. 첫 번째는 새로운 멤버 클러스터가 추가됐을 때, 두 번째는 멤버 클러스터 간 가중치 값이 변경됐을 때입니다. 두 가지 경우에 모두 epoch 값이 증가하며, 요청을 보내는 모든 객체는 epoch 값을 비교하면서 최신 맵을 보고 있는지 계속 확인하기 때문에 클러스터 구성이 변하더라도 사용자 트래픽이 어떤 클러스터로 향할지 결정할 수 있습니다. 아래 그림은 가중치를 조정해서 클러스터 1번으로 더 많은 트래픽을 보내도록 구성한 모습입니다.
S3 호환을 유지하면서 클러스터 간 복사 요청 처리
하이퍼 스케일 오브젝트 스토리지는 S3 프로토콜을 사용합니다. 이때 대부분의 요청은 적절한 클러스터로 잘 포워딩하는 것으로 처리할 수 있지만 복사 요청 같은 경우는 포워딩만으로는 작동하지 않습니다. 복사를 처리하는 Ceph 내부 로직 때문입니다.
아래 그림과 같이 Ceph은 오브젝트를 여러 개의 오브젝트로 나눠서 저장합니다. 나뉜 오브젝트 중 첫 번째 오브젝트는 헤드 역할을 담당하며, 해당 오브젝트의 테일(tail) 개수와 이름을 내부 메타 데이터에 저장합니다.
이때 이 오브젝트를 복사해 달라는 요청이 들어오면 Ceph의 RADOS 게이트웨이는 새로운 헤드를 만들어서 기존 오브젝트의 테일을 공유하는 방식으로 진행합니다. 이 방식은 데이터 복사를 하지 않아도 된다는 게 장점인데요. 이 장점이 저희 시나리오에서는 문제가 됐습니다. 아래 오른쪽과 같이 클러스터 바운더리가 바뀔 수 있기 때문입니다.
저희 클러스터는 클러스터의 클러스터이기 때문에 다른 클러스터에 있는 오브젝트 복사 요청이 들어올 수 있습니다. 그런데 이 요청을 Ceph의 기본적인 RADOS 게이트웨이는 처리할 수 없습니다. 소스 오브젝트가 해당 클러스터에 없기 때문에 그냥 에러를 반환합니다.
이런 상황이 멀티 파트 요청이 되면 더욱 복잡해집니다. S3 프로토콜에서 멀티 파트 오브젝트를 업로드할 때는 더 복잡한 과정을 거치기 때문입니다. 위 왼쪽과 같이 먼저 init 요청을 보내고 업로드 ID를 받은 뒤, 받은 ID를 이용해 파트를 전송하고, 마지막으로 완료(complete) 요청을 보내서 오브젝트 업로드를 마무리하는 방식입니다. 여기서 전송하는 파트가 다른 오브젝트의 복사본이라면 더 복잡해질 수도 있습니다.
이와 같이 복잡한 일련의 프로세스를 준수하면서 클러스터 간 복사를 수행할 수 있어야 했고, 라우팅 레이어에서 이 역할을 담당하고 있습니다.
데이터 리셔플링 배제
앞서 제약사항에서 말씀드린 것처럼 하이퍼 스케일 오브젝트 스토리지를 구축하면서 데이터 리셔플링을 배제했습니다. 그 이유를 말씀드리기 전에 먼저 리셔플링이 무엇인지 간략히 설명하겠습니다.
한 클러스터에 노드 하나가 추가됐을 때 Ceph에서는 기존 노드의 오브젝트들을 새로운 노드로 이전하는 작업을 수행합니다. 스토리지 용량이나 트래픽 측면에서 균형을 유지하기 위한 작업인데요. 이를 리셔플링이라고 부릅니다.
이 리셔플링이 저희가 설계하는 하이퍼 스케일 오브젝트 스토리지의 규모에서는 현실성이 없다고 판단했습니다. 이 정도 크기의 클러스터에 새로운 멤버 클러스터를 추가하는 것은 한 클러스터에 노드 하나를 추가하는 것과는 차원이 다르기 때문입니다. 새로운 큰 클러스터를 추가하는 것이기 때문에 오브젝트를 재배치하려면 너무 오랜 시간이 걸릴 것으로 판단했습니다.
또한 추가 단위가 노드가 아니라 클러스터이기 때문에 용량이나 트래픽이 한쪽으로 조금 쏠리더라도 사용자 트래픽을 충분히 처리할 수 있는 상황이었고, 추가된 클러스터를 개별로 확장할 수도 있었습니다. 이런 특징을 고려해 리셔플링은 배제하기로 결정했습니다.
스토리지 저장 효율 향상 방법
지금까지 여러 클러스터를 묶는 방법을 소개했습니다. 이어서 스토리지 저장 효율을 높이기 위해 적용한 몇 가지 방법을 소개하겠습니다. 규모가 너무 크다 보니 비용이 어마어마하게 들어갈 수밖에 없는데요. 이 비용을 최대한 줄이기 위해서 데이터를 효율적으로 저장해야 했습니다.
하이브리드 형태의 스토리지 도입
저희는 SSD와 하드 디스크, 레플리케이션과 이레이저 코딩 방식을 적절하게 혼합해서 아래와 같이 하이브리드 형태로 스토리지를 구성했습니다. 계층 구조가 아니고 오브젝트 크기에 따라 저장 미디어를 선택하는 방식입니다. Ceph 기본 기능은 아니고 필요해서 추가로 구현했습니다.
보통 SSD와 하드 디스크를 섞어서 쓸 때 계층 구조를 채택합니다. 핫 데이터는 SSD에 저장하고 웜 혹은 콜드 데이터는 하드 디스크에 저장하는 방식인데요. 이런 방식을 선택하지 않은 이유는 트래픽의 특성 때문이었습니다.
저희는 하드 디스크에 이레이저 코딩을 적용했는데요. 이레이저 코딩은 복제본 세 개를 유지하는 것보다 비용 효율 측면에서 유리하다고 알려져 있지만, 실제로 클러스터에 적용해 보니 작은 오브젝트를 저장했을 때에는 오버헤드가 상당히 커진다는 것을 확인할 수 있었습니다. 더군다나 저희가 받고 있는 트래픽은 오브젝트 하나에 메타 데이터가 포함된 작은 오브젝트 하나가 반드시 딸려오기 때문에 단순히 핫이냐 콜드냐를 따져서 데이터 저장 효율을 높일 수는 없었습니다.
대신 오브젝트 크기에 따라서 미디어를 선택하는 방식을 채택했습니다. 크기가 작고 요청이 빈번히 들어와서 지연 시간에 민감한 메타 데이터 오브젝트는 SSD에 저장하고, 크기가 크고 지연 시간보다는 대역폭에 더 민감한 데이터 오브젝트는 하드 디스크에 저장했는데요. 결과적으로 데이터 특성과 잘 맞았습니다.
데이터 라이프 사이클 최적화
마지막으로 데이터 라이프 사이클을 최적화했습니다. 클러스터 규모가 커지고 데이터가 많아지면 지워야 하는 데이터의 양도 많아지기 때문에 기한이 만료된 오브젝트를 제때 처리할 수 있는 능력이 대단히 중요해집니다. 그런데 RADOS 게이트웨이 기본 구현에 약간 문제가 있었습니다. RADOS 게이트웨이 기본 구현은 오브젝트를 지울 때 처리해야 하는 버킷 목록을 각 샤드별로 분류하고 스레드 하나가 투입돼 샤드를 처리하는 방식이었는데요. 만약 저장된 오브젝트가 많은 버킷이 있으면 다음 버킷 처리가 지연될 수밖에 없는 구조였습니다.
아래 예시를 보겠습니다. 1번 샤드의 버킷 B에 지워야 할 오브젝트가 많으면 버킷 C는 처리가 지연될 수밖에 없습니다. 이때 단순히 몇 시간 더 걸리는 정도가 아니라, 클러스터 규모 때문에 며칠이 더 걸릴 수도 있습니다. 데이터가 며칠 간이나 제때 지워지지 못하고 남아 있는 것입니다.
이 문제를 해결하기 위해 여러 개의 워크 스레드가 한 샤드에 진입하도록, 또한 하나의 스레드가 하나의 버킷만 처리하도록 RADOS 게이트웨이를 수정했습니다.
마치며
이번 글을 간략히 정리해 보겠습니다. 그동안 운영한 경험에 비춰봤을 때 Ceph은 대단히 안정적이면서 확장성도 갖춘 좋은 오픈소스 스토리지였습니다. 다만 무한히 확장 가능한 것은 아니어서 특정 규모 이상에서는 문제가 발생할 가능성이 있었습니다.
이를 해결하기 위해 여러 개의 클러스터를 하나로 묶는 방식을 선택했는데, 그런 여러 클러스터가 마치 하나의 클러스터처럼 작동하게 만들면서 S3 호환성도 확보했습니다. 여기에 더해 데이터 저장 방식이나 삭제 방법을 최적화해서 비용 측면에서도 효율적으로 작동할 수 있도록 구성했습니다.
이 작업을 통해 Ceph의 한계에 구애받지 않고 지속적으로 확장 가능한 하이퍼 스케일 오브젝트 스토리지를 구성할 수 있었습니다. 저희와 같이 Ceph을 대규모로 운영하시려는 분들께 도움이 되길 바라며 이만 마치겠습니다. 긴 글 읽어주셔서 감사합니다.