대규모 시스템 설계 기초 1

규모 확장성이 있는 시스템은 어떻게 설계해야 하는 걸까요?

sjoonb 2024. 7. 6. 17:19

시작하며

먼저 규모 확장성이 있는 시스템이 무엇일지 고민해 보았습니다.

  1. 트래픽이 많이 늘어나더라도 안정적으로 동작하는 시스템.
  2. 트래픽 변화에 맞게 진화 하는 시스템. 반복적으로, 수평적으로 확장할 수 있어야 함.

수직적으로 확장할 수 있어도 규모 확장성 아니냐고 의문을 던질 수 있습니다. 수직적 확장이 좋지 않은 이유는 크게 2가지로, 하드웨어의 수직적 확장에 분명 한계가 존재한다는점, SPOF 문제가 발생한다는 점을 들 수 있을 것 같습니다.

규모 확장성이 있는 시스템의 설계는 초기단계부터 고려되어야 합니다. 설계가 잘못되었으나, 지속적으로 서비스가 성장하고 데이터가 쌓여가는 경우에, 레거시로 인해 아예 시스템을 새로 만드는게 나을 것이라는 이야기가, 실제 사례들이 괜히 있는 것이 아니겠죠.

어떻게 설계해야 규모 확장성이 있는 시스템인 걸까요?

우선, 시스템에 각 계층을 분리할 수 있어야 합니다. 그리고 각 계층이 독립된 형태로 확장될 수 있어야 하는 것이죠. 구체적인 예를 들면, 웹 서버 따로, API 서버 따로, 데이터베이스 계층 따로 각각 독립적으로 발전할 수 있어야 하는 것입니다. 더 자세하게 들어가면, API 서버 내부에서도 특정 서비스들 마다 별도로 분리가 잘 이루어져 있어야 합니다.

웹 계층의 다중화 (로드 밸런서의 필요성)

과거에는, 웹서버 하나로도 모든 요청을 처리하는 시절이 있었습니다. 하지만 서비스가 커지면서 웹서버에 많은 요청이 몰리게 되면 서버에 문제가 발생할 것입니다. 가장 쉽게 떠올릴 수 있는 방법은 서버를 수직적으로 확장하는 것이지만, 그 동안의 다운타임은 어떻게 처리할 것이며, 그 외에도 앞서 말한 수직적 확장의 한계 문제에 다다르게 됩니다.

그렇다면, 웹서버를 몇개 더 띄우는 방식은 어떨까요? 이렇게 되면 SPOF 문제도 없어질거고, 필요에 따라 웹서버를 늘리고 줄이는 것이 가능해(클라우드 서비스를 활용해야 하긴 하지만) 리소스 절약에도 큰 도움이 될  것입니다.

하지만, 웹 서버 하나마다 각각 다른 도메인을 가져야 하나요? 클라이언트가 어떻게 이를 구분해서 요청을 보내죠? 이 때 필요한게 로드 밸런서인 것이죠. 클라이언트는 고민할 필요없이 로드밸런서로 요청을 보내면 되고, 로드 밸런서는 본인이 알고 있는 웹 서버 각각의 고르게 부하를 분산해 보내주면 됩니다. 이 사례가 계층을 분리한 사례, 로드밸런서 계층과 웹서버 계층을 분리하여 규모 확장성을 보장한 사례인 것이죠.

웹 계층은 무상태여야 합니다.

웹 서버가 사용자의 세션을 관리하고 있었으면 어떻게 될까요? 웹 서버가 2개가 되는 순간 복잡해지기 시작합니다. A유저의 정보를 A서버가 관리하고 있는데, 다음 요청이 B서버로 전달된다면?

클라이언트가 로드밸런서의 동작을 예측할 수 없기 때문에, 이는 충분히 고려해야할 상황입니다.

이를 웹 계층에 상태가 존재한다고 표현하고, 해결방법은 상태를 없애는 것이죠. 가장 단순한 방법으로, 모든 서버들이 (서버가 몇개든) 직접 세션을 관리하지 않고, 별도의 데이터 스토어를 찌르며 세션을 유지하는 방식을 도입할 수 있습니다. 실제로 가장 흔하게 쓰이는 방법이죠.

데이터베이스는 어떻게 다중화 할까요? 

데이터베이스 다중화는 API 서버보다 많이 까다롭습니다. 기본적으로, 상태를 가지고 있기 때문이죠. 데이터베이스 계층이 무상태일 수는 없으니까요 ㅎㅎ 

그래도 최대한 수평적 규모로 확장할 수 있어야합니다. 데이터가 많이 쌓이고 트래픽이 늘어나면 아무리 좋은 스펙에 데이터베이스를 쓴다고 하더라도 속도가 느려지고 문제가 발생하겠죠. 그럴 때 활용할 수 있는 전략이 크게 2가지 일 것 같습니다. 하나는 레플리케이션, 다른 하나는 데이터베이스 샤딩입니다.

데이터베이스 레플리케이션

레플리케이션은 동일한 데이터베이스를 복제본으로 여러개 만들어두는 전략으로, 가용성, 재해 복구성에도 좋습니다. 복제된 데이터베이스들 중 한 데이터베이스는 쓰기가 가능한 마스터 용도로, 나머지는 읽기 연산만 가능하도록 처리하면 동시 처리량이 늘어나겠죠.

다만, 그만큼 리소스를 활용해야 한다는 점, 마스터 데이터베이스에 쓰인 데이터가 아직 복제되지 않았는데 읽기 연산이 들어온 경우에 어떻게 처리할 것인지에 대한 고려등이 추가로 필요합니다.

데이터베이스 샤딩

샤딩은 쉽게말해 테이블을 수평으로 쪼개서 여러군데에 저장하는 전략입니다. 예를 들어 4개의 샤드가 존재한다고하면, user_id 와 같은 샤딩키 (파티션 키) 를 활용하여, user_id % 4 모듈러 연산을 통해 각 해당하는 샤드에 데이터를 저장하는 것이죠.

동시 처리량이 늘어날 것입니다. 다만, 조인 등의 연산이 매우 까다로워 진다는 점을 고려해야 합니다. 또한, 샤드 개수를 동적으로 수정해야 하는 경우도 복잡해집니다. 이 때 안정 해시 기법을 활용할텐데, 이는 나중에 다루도록 하겠습니다.

여러 데이터 센터를 지원하기

데이터 센터가 무엇일까요? 로드밸런서 다음단계에 해당하는 계층들이 모두 하나의 데이터센터에 속하는 요소들로 볼 수 있을 것 같습니다. 웹 서버, 데이터베이스 등이 한 데이터센터에 속할 수 있는 거죠. 그러한 데이터센터를 여러개 가질 수 있습니다. 규모 확장성을 위해서요. 전세계에 서비스를 한다고 생각했을 때, 한국에서 들어오는 요청은 한국 데이터센터에서, 미국에서 들어오는 요청은 미국 데이터센터에서 처리하는게 훨씬 효율적이겠죠.

이 또한, 로드밸런서가 결정할 것입니다. IP를 확인하여 어디에 보내야 가장 효율적인지를 판단할 수 있겠죠. 이 절차를 지리적 라우팅, geo-routing 이라고 부릅니다.

여러 데이터 센터를 지원하려면 데이터 동기화 어떻게 할 것인지에 대하여 잘 고려해야 할 것입니다.

넷플릭스가 이를 어떻게 다중화 하였는지 한번 공부하여 포스트로 다뤄볼 수 있으면 좋을 것 같네요.

가능한 한 많은 데이터를 캐시할 것

운영체제든, 네트워크든, 어디서든 뭔가 최적화, 속도를 높인다고 했을 때 캐싱을 떠올려 볼 수 있습니다. 그 만큼 강력한 방법이기 때문이죠. 규모 확장성 있는 시스템 설계에선 어떤 캐싱을 적용할 수 있을까요?

우선, 지역별로 CDN을 활용하는 것도 방법이고, 각 데이터베이스 앞에 자주 쓰이는 데이터는 캐시 서버를 활용하는 것도 방법이죠. 클라이언트에서 로컬에 캐시를 저장하는 방법도 있습니다. 

캐시를 사용할 때는 데이터의 최신성에 대하여 신경써야 합니다. 이를 위한 다양한 캐시 전략이 존재하고, 캐시를 무효화 하는 방법들도 존재하죠. 또한, 캐시 서버를 한대만 두는 경우엔 이 역시 SPOF가 될 수도 있습니다. 캐시 서버 그룹을 두거나, 캐시 서버가 동작하지 않더라도 시스템이 작동하게끔 로직을 구성해야겠죠. 여러지역에 캐시 서버를 분산시키는 것도 방법입입니다.

콘텐츠 전송 네트워크(CDN)

앞서 가볍게 언급했는데, CDN 도 중요한 캐시 전략이자 규모 확장성을 위한 요소중 하나입니다. 정적 콘텐츠를 빠르게 전달하는 것을 담당한다고 볼 수 있을 것 같ㅇ습니다.

CDN 자체적으로, 원본 서버와 통신하며 새로운 데이터를 가져와 캐시하고, 캐시된 데이터는 빠르게 돌려주고 등에 전략이 있을 것 같습니다.

저는 물음표 살인마이기 때문에 왜 CDN 이 필요한지에 대한 의문을 던져보겠습니다. 아니, 정적 콘텐츠 API 서버에서도 보내줄 수 있잖아?!

사실 해답은 간단한 것 같은데, 정적 콘텐츠에 특성상 따로 이를 가져오는 기능을 분리하기 쉽고, 그렇게 해야 API 서버에 부담이 많이 줄어들 수 있기 때문이겠죠. 

CDN은 데이터를 받아와서 내려준다 라는 단순한 로직 밖에 없을 겁니다. 복잡한 비지니스 로직이 필요 없죠. 동적으로 콘텐츠를 생성할 필요도 없구요. 단순히 데이터를 내려받는 기능을 위해 최적화된 요소이며, 이를 별도로 활용했을 때 속도는 물론 비용 측면에서 훨씬 효율적인 결과를 만들어 낼 수 있을 것입니다.

다만, CDN 은 주로 서드파티에 의해 운영되어 요금이 꽤 나갈 수도 있습니다. 자주 사용되지 않는 콘텐츠는 캐싱하지 않는게 좋겠죠. 그리고 적절한 만료 기한을 설정해야 하고, CDN 이 죽었을 경우에도 SPOF 가 되지 않도록 이를 처리할 수 있어야 합니다. 가령, 당분간 다른 CDN 에서 데이터를 가져오게 한다던가, 아니면 직접 원본 서버를 찌르게 한다던가 하는 식으로요.

메세지 큐의 역할은 무엇일까요?

메세지 큐는 규모 확장성 있는 시스템에서 중요한 역할을 합니다. 자세한 내용을 다루진 않겠지만, 비동기 적으로 요청을 처리할 수 있도록 큐를 제공하여 서비스 간의 결합도를 낮추는데 기여합니다.

심지어, 카프카와 같은 경우 Durability 도 보장되기 때문에 데이터가 유실될 걱정도 할 필요가 없습니다. 큐에 데이터를 넣는 전송부와 큐에서 데이터를 빼내는 수신부가 존재할텐데, 둘 중 하나가 죽어도 나머지 하나는 계속 일을 하고 있을 수 있다는게, 느슨한 결합이 의미하는 바를 잘 표현해 준다고 생각합니다.

만약 그렇지 않은 경우, 동기적으로 API 를 통해 통신하는 경우엔 수신부가 죽을 경우 처리부도 따라서 죽는 겁니다. 

또한, 수신부, 처리부의 서버 양도 독립적으로 늘리고 줄이고 할 수 있다는 것도 규모 확장성의 큰 기여를 할 수 있는 부분입니다.

대규모 시스템을 안정적으로 유지하려면, 로그와 메트릭 그리고 자동화가 필요합니다

에러 로그를 모니터링하는 것은 중요합니다. 서버 단위로 모니터링할 수도 있겠지만 단일 서비스로 로그를 모아주는 것도 필요하겠죠.

각 서버의 CPU 사용량, I/O, 메모리등을 실시간으로 파악하는 것도 중요합니다. 더 나아가 쿠버네티스와 같은 오케스트레이션 인프라를 통해 자원이 부족한 경우 자동으로 서버를 띄우고, 반대의 경우 서버를 내리는 비용 효율적인 자동화가 가능하겠죠.

서버 관련한 메트릭 외에도, 데이터베이스 계층 성능, 캐시 계층 성능도 수집할 필요가 있으며, 비지니스와 관련된 로직 (DAU, 수익, 재방문) 등도 수집해야 할 것입니다.

마무리

대규모 시스템 설계 1권의 1장 내용을 보고, 떠오르는대로 글을 정리해 보았습니다. 언젠가, 대규모 시스템 설계나 그 환경을 직접 경험을 해볼 수 있으면 정말 좋을 것 같네요.