📌 이 내용은 책 내용 메모입니다.
개인적인 생각과 경험 그리고 여러 잡담이 있으니, 책을 읽으며 의견을 나누고싶은분이 봐주시면 좋겠습니다.
2.1 트래픽 관리
2.1.1 SPOF (single point of failure)
클라우드는 멀티테넌트 환경이기때문에 우리가 알지 못하는 사이에 장애가 발생하고, 원인분석도 쉽지 않다. (저번에 겪은 cloudfront장애처럼..)
특히 네트워크는 SPOF 특징을 가지고 있고 그 영향도가 매우 크리티컬하다.
그렇기 때문에 관측가능성 뿐만 아니라 네트워크 트래픽을 수집하고 관리하여 사전에 탐지하고 예측하는것이 중요하다.
2.1.2 로드벨런서
클라우드 + 쿠버네티스의 조합으로 네트워크의 복잡도가 매우 크게 증가했다.😭😭😭😭
많이 사용되는 3가지 로드벨런서 유형을 소개하고있다.
플랫폼 로드벨런싱
- AWS로 치면 ALB, NLB, nginx와 같은 4계층 7계층 로드벨런서를 말한다.
게이트웨이 로드벨런싱
- api gateway를 말한다.
- 로드벨런싱,라우팅, 인증,보안검사,SSL터미네이션과 같은 플랫폼 로드벨런싱의 기능도 수행하지만 플랫폼 로드벨런싱은 가용성과 확장성만을 위해 사용한다면, 게이트웨이 로드벨런싱은 애플리케이션 수준에서 세밀한 제어를 제공한다. (마이크로서비스 아키텍쳐일때 필요한것같다..)
- 게이트웨이 로드벨런싱 전략을 구축할때 레이턴시 지표와 리소스 사용률 지표를 모두 고려해야간다.
- 레이턴시(Latency): 사용자가 서비스에 요청을 보내고 받기까지 걸리는 시간. 네트워크지연, 서버 처리시간등에 기반한다.
로드벨런서는 클라이언트로부터 라운드로드 트립(RTT)를 측정하여 서버 응답시간을 파악할 수 있다. - 리소스 사용률(Resource Utilization): CPU/MEM/디스크IO/네트워크트래픽 등 부하를 확인할 수 있는 지표다.
- 레이턴시(Latency): 사용자가 서비스에 요청을 보내고 받기까지 걸리는 시간. 네트워크지연, 서버 처리시간등에 기반한다.
- 예시: 스프링클라우드 게이트웨이, 넷플릭스 줄(Zuul}
클라이언트측 부하 분산 (Client-side Load Balancing)
- 여기서 말하는 '클라이언트'는 서비스나 데이터를 요청하는 애플리케이션이다.
- 이 클라이언트에서 여러 서버중 하나로 트래픽을 전달하도록 하는것이 클라이언트측 부하 분산이다.
- 클라이언트측 로드벨런싱 작동방식
- 서비스 디스커버리: 클라이언트는 서비스 디스커버리를 이용하여 서버 목록을 알아낸다.
- 결정로직: 클라이언트 내장 로드밸런식 로직(예, 라운드로빈)을 사용하여 이 요청을 어디에 보낼지 결정한다.
- 요청전송: 클라이언트는 선택된 서버 인스턴스로 요청을 보낸다.
- 예시: istio, 넷플릭스 Ribbon
- 궁금해서 찾아본것 - 서비스 디스커버리를 통해 알아낸 서버가 마침 장애상황이면 어떻게 하나?
- HealthChecks: 당연히 주기적으로 서버가 정상인지 확인하는 로직이 필요하고, 정상이 아니라면 서버목록에서 제외한다.
- 회로 차단기 패턴 구현 (curcuit breaker): 특정 서버로의 요청이 지속되는경우, 해당 서버로의 요청을 차단(회로차단)하고 다른 서버로 요청을 전송한다. 실패한 서버가 복구할 시간을 벌고 또 전체적인 부하를 낮추는데 좋다.
- 재시도 매커니즘: 요청이 실패하면 클라이언트는 자동으로 다른 서버 인스턴스에 요청을 재시도 한다. 재시도는 제한된 횟수와 전략(예,지수백오프)를 사용한다. (네트워크 오버헤드 조심)
- 타임아웃 설정: 요청에 타임아웃을 설정해서 서버응답이 지정된 시간 내에 오지 않으면 실패하도록 하고, 재시도 매커니즘을 통해 다시 요청하도록 한다.
- 서비스 디스커버리 동기화: 서비스 디스커버리 시스템은 당연하게도 서버의 상태를 실시간으로 동기화 해야한다.
2.1.3 복원성 패턴
- 애플리케이션 관점에서 복원력을 높이는 방법은 소스 내 예외처리, 재시도, 타임아웃 등을 정의하는것이다.
- 비즈니스적인 에러와 시스템에러를 구분하고, 유형에 따라 후속처리를 해야한다.
- 인프라 관점에서 복원력을 높이는것은 서비스메시와 메세징을 사용하는것이다.
복원성 패턴1. 재시도
- 비즈니스에러인 경우, 호출자가 잘못된 데이터를 입력하거나 프로그램의 버그로 인해 발생했을때는 재시도 불필요하다.
- 시스템 에러인 경우, 네트워크의 일시적인 지연, 순간적인 사용률 이슈 등 인 경우 일정주기로 재시도 하면 정상적인 처리가 가능하다.
- 서비스에 대한 도메인 지식이 있어야 재시도 처리에 대해 판단할 수 있으며 처리시간이 초과되었을때 재시도를 허용할것인지, 재시도로 중복이 발생하지는 않을지도 고민해야한다.
- 단, 재시도가 누적되어 돌연 장애를 일으킬 수 있으므로 최대 재시도 횟수와 시간간격은 정책에 의해 정의해야 한다.
복원성 패턴2. 비율제한
- 특정시간동안에 애플리케이션에 대한 요청수를 제한하는것을 말한다.
- 시스템이 과부하 상태에 놓였을때 시스템 전체를 정지시켜버릴 수 있으므로 비율을 제한함으로써, 제한된 만큼 만이라도 사용할 수 있도록 한다.
- 대부분의 LoadBalancer에서 구현할 수 있을것으로 보인다. (특정시간동안 허용된 요청수를 초과하는경우 HTTP 429 반환 등)
복원성 패턴3. 벌크헤드
- 배의 구획을 나누는 방수벽(벌크헤드)에서 유래했으며, 시스템의 여러부분을 격리하는 기술이다.
- 한 구획에서 장애가 발생해도 나머지 구획에는 영향을 미치지 않도록 하는 방식이다.
- 목적: 시스템의 부분적인 실패로부터 자른 부분을 보호하기 위해 논리적/물리적으로 분리하는데 중점을 둔다.
- 예시: 스레드 풀 (ex.java ExcutorService), 데이터베이스 연결 풀..등등
복원성 패턴4. 서킷브레이커
- 전기회로의 서킷 브레이커에서 영감을 받은 소프트웨어 설계 패턴으로, 장애가 발생한 시스템에 연결을 자동으로 끊어 오류의 전파를 방지하는것을 말한다.
- 목적: 실패가 감지되었을때 추가적인 손상을 방지하기 위해 자동으로 연결은 끊는데 중점을 둔다.
- 서킷브레이커와 함께 Kafka같은 분산 메세지 시스템을 적용하는것도 좋다.
- Netflex의 Hystrix
2.1.4 가시성
- 이 책에서 말하는 가시성은 네트워크/인프라 부분이다.
- 관측가능성과 가시성은 상호 보완적이며 단일 시스템 내에 구축하는것을 권장한다.
2.1.5 서비스 매시
- 서비스 메시는 마이크로서비스 아키텍쳐에서 서비스간의 통신을 용이하게 하기 위한 솔루션이다.
- 마이크로서비스의 사이트카 패턴을 사용하는 경량 프록시로 구성되며, 서비스간의 모든 통신을 중재한다. (istio envoy proxy)
- 서비스 매시는 서비스간의 통신(로드벨런싱,장애복구,서비스디스커버리), 보안(SSL,접근제어), 관측가능성(로깅,모니터링,추적 등) 등의 기능을 제공한다.
- 다만, 이 책에서는 사이드카 패턴으로 구현되는 서비스 매시에 대한 단점을 정리했고, eBPF를 지원하는 Cilium을 추천했다
- 단점1. 이스티오는 벌크형태에 가까운 서킷브레이커만 지원한다.
- 단점2. 관리정책은 yaml로 구사할 수 있는 수준 이상으로 정교하게 구성할 수 없다.
- 사이트카는 비싼 리소스이며, 이스티오의 내부 네트워크 처리과정은 다소 비효율적이다.
- 예시: Istio, Linkerd, Consul Connect(콘술이거 Loki에서 본적이 있다!!)
2.2 쿠버네티스 오토스케일링
쿠버네티스는 HPA(Horizontal Auto Scaler)를 사용하여 아래 리소를 파드 스케일링에 사용할 수 있다.
HPA가 정상적으로 동작하려면 CPU (.spec.resources.requests) 제한을 선언해야하고, 메트릭 서버를 활성화 해야한다. (당연히 데몬셋은 불가하다)
- 디플로이먼트
- 레플리카셋
- 레플리케이션 컨트롤러
- 스테이트풀셋
파드 스케일링은 리소스 메트릭 (Cpu/Mem) 등의 리소스 사용량기준, 사용자 정의 메트릭을 사용하여 오토스케일링 할 수 있다.
사용자 정의 메트릭은 custom.metrics.k8s.io API경로의 Aggregated API서버로 수행되고, 프로메테우스 등 메트릭 솔루션 공급업체에서 제공하는 어뎁터 API서버에서 제공한다.
파드 레벨의 오토스케일링은 노드의 오토스케일링과 같이 동작해야한다.
2.2.1 오토스케일링 오픈소스
오토스케일링을 구현하는 방법과 이를 지원하는 오픈소스를 확인한다.
메트릭 서버
- kubelet은 컨테이너 리소스 관리를 위한 노드 에이전트이며, /stats/summary 엔드포인트를 통해 노드별 요약정보를 제공한다.
- 메트릭 서버는 각 kubelet으로부터 수집하고 집계하여 API 서버에 보낸다.
- API서버는 HPA, VPA(Vertical Pod Autoscaler), kubectl top 명령어를 사용할 수 있도록 메트릭 API를 제공한다.
메트릭 솔루션 공급업체에서 제공하는(예,프로메테우스) 어뎁터
- 프로메테우스 어뎁터를 사용한 사용자정의 메트릭으로의 HPA는 3장에서 다시 설명하므로 패스
KEDA (Event Driven AutoScaling)
- 이벤트 기반으로 리소스를 확장한다. (Event Driven Autoscaler)
- Apache kafka와 같은 queue서비스와 함께 사용하기 좋다.
- 예를들어, Kafka에 메세지 큐가 증가하면, KEDA가 감지하고 Pod를 자동으로 늘려주도록 할 수 있다.
- Apache Kafka 외에도 RabbitMQ, Azure Event Hubs, AWS SQS 등의 메세징 서비스와도 함께 사용할 수 있다.
2.2.2 메트릭 측정
위에서 메트릭을 이용해서 오토스케일링을 구현하는 방법 크게 3가지를 알아봤다.
CPU, Memory 사용량 외에도 초당처리개수(TPS), 처리지연시간(Latency) 등을 활용할 수 있고, 이런 메트릭을 조합한 값으로도 오토스케일링을 트리거 시킬 수 있다.
그렇다면, 어떤 메트릭을 오토스케일링에 이용할 수 있는지 알아본다.
특히, 구글 SRE이 제시하는 4가지 골든 시그널 (Latency, Traffic, Errors, Saturation)에서 좋은 예시를 볼 수 있다.
The Four Golden Signals
출처: https://sre.google/sre-book/monitoring-distributed-systems/
The four golden signals of monitoring are latency, traffic, errors, and saturation. If you can only measure four metrics of your user-facing system, focus on these four.
Latency
The time it takes to service a request. It’s important to distinguish between the latency of successful requests and the latency of failed requests. For example, an HTTP 500 error triggered due to loss of connection to a database or other critical backend might be served very quickly; however, as an HTTP 500 error indicates a failed request, factoring 500s into your overall latency might result in misleading calculations. On the other hand, a slow error is even worse than a fast error! Therefore, it’s important to track error latency, as opposed to just filtering out errors.
Traffic
A measure of how much demand is being placed on your system, measured in a high-level system-specific metric. For a web service, this measurement is usually HTTP requests per second, perhaps broken out by the nature of the requests (e.g., static versus dynamic content). For an audio streaming system, this measurement might focus on network I/O rate or concurrent sessions. For a key-value storage system, this measurement might be transactions and retrievals per second.
Errors
The rate of requests that fail, either explicitly (e.g., HTTP 500s), implicitly (for example, an HTTP 200 success response, but coupled with the wrong content), or by policy (for example, "If you committed to one-second response times, any request over one second is an error"). Where protocol response codes are insufficient to express all failure conditions, secondary (internal) protocols may be necessary to track partial failure modes. Monitoring these cases can be drastically different: catching HTTP 500s at your load balancer can do a decent job of catching all completely failed requests, while only end-to-end system tests can detect that you’re serving the wrong content.
Saturation
How "full" your service is. A measure of your system fraction, emphasizing the resources that are most constrained (e.g., in a memory-constrained system, show memory; in an I/O-constrained system, show I/O). Note that many systems degrade in performance before they achieve 100% utilization, so having a utilization target is essential.In complex systems, saturation can be supplemented with higher-level load measurement: can your service properly handle double the traffic, handle only 10% more traffic, or handle even less traffic than it currently receives? For very simple services that have no parameters that alter the complexity of the request (e.g., "Give me a nonce" or "I need a globally unique monotonic integer") that rarely change configuration, a static value from a load test might be adequate. As discussed in the previous paragraph, however, most services need to use indirect signals like CPU utilization or network bandwidth that have a known upper bound. Latency increases are often a leading indicator of saturation. Measuring your 99th percentile response time over some small window (e.g., one minute) can give a very early signal of saturation.Finally, saturation is also concerned with predictions of impending saturation, such as "It looks like your database will fill its hard drive in 4 hours."
If you measure all four golden signals and page a human when one signal is problematic (or, in the case of saturation, nearly problematic), your service will be at least decently covered by monitoring.
- 요청수 (number of request)
- 어플리케이션이 수신하는 요청 수를 기반으로 한다.
- 요청수는 어플리케이션의 트래픽 패턴, 시스템이 처리할 수 있는 요청수, 요청처리 성공률 등을 기반으로 판단할 수 있다.
- 요청기간 (duration of the request)
- 요청 기간이란, 특정 요청이 시작되어 완료될때까지 걸리는 시간을 의미한다.
- 요청이 얼마나 걸렸는지는 히스토그램에서 가장 잘 나타난다.
- 시스템(예를들어 ALB) 에서 URI별 (duration of the request)을 나타낼 수 있다면 엄청나게 유용한 그래프가 될것같다.
- 동시요청
- 동시요청 지표는, 특정 어플리케이션에서 병목현상이 발생하고있는지 등을 볼 수 있다.
2.2.3 메트릭 선정
측정하고자 하는 메트릭을 식별했다면 부하테스트를 통해 검증해야한다.
또한 더 선행되어야 하는것은 메트릭서버, 프로메테우스 어뎁터, KEDA등 AutoScaler들이 제대로 동작하는지 이 Pod들의 cpu/mem 사용량을 파악해야한다.
어플리케이션이 AutoScaling 되기 전 AutoScaler가 ☠️죽어버리면 말짱 도루묵이다.
- ⭐️중요⭐️ AutoScaling 설정할때 고려야해야할것
책에선 HPA 설정 주의사항이지만, 이건 K8s HPA 뿐 아니라 모든 상황에서 AutoScaling 설정시 반드시 고려해야 한다.
어플리케이션에 대한 이해와, AutoScaling 주의사항을 읽지 않고 마구 설정해버리면 매우 큰 장애로 이어질 수 있다.
- 메트릭 선택
- 어떤 메트릭을 기준으로 AutoScaling할것인지는 매우 중요하며, 부하를 반영하는 정확한 메트릭을 선정해야한다.
- 예를들어 위에 구글 SRE 문서(더보기)에 따르면 웹서버의 경우 초당 HTTP수(단, 정적컨텐츠냐 동적컨텐츠냐 분리 세분화 필요), 오디오 스트리밍 시스템의 경우 네트워크 I/O 속도 또는 동시세션, key-value저장소의 경우 초당 트랜젝션 및 검색 수 등을 기준으로 Traffic 을 측정해야한다고 나와있다.
- 스레싱(thrashing) 방지
- 스레싱이란, 자원이 빈번하게 추가되었다가 제거되는 현상을 말하며, 이런 증상은 오히려 비효율적인 자원 사용 및 성능저하로 이어질 수 있다.
- 예전에 겪었던 이슈로 CPU기준으로 AutoScaling동작하도록 설정해두었다가, 인스턴스 몇백개 떠있었던 적이 있다. 너무 예민하게 설정해놓았던 탓에 CPU 사용률 높아짐 -> 컨테이너 늘림 -> 부팅될때 CPU높게 침 -> CPU사용률 안낮아져서 더 늘림 -> 부팅될때 CPU높게침 😮💨의 무한 반복이었다.
정말 초보적인 실수지만, 어플리케이션의 특징을 잘 알지 못하고 설정해버리면 이런 실수를 겪을 수 밖에 없다. - 스레싱 방지를 위해 생각해야할건 아래와 같다.
- 적절한 지표 선택: 올바른 지표를 선택하는것이 가장 중요하다.
- 쿨다운 기간 지정: 너무 빠르게 스케일인/아웃 되지 않도록 쿨다운 기간(스케일 지표에 포함하지 않는기간)을 설정한다.
- 적절한 스케일링 기준 지정: 너무 민감하게 반응하지 않도록 주의한다. 일시적인 부하로 스케일 인/아웃되면서 더 큰 문제가 발생할 수 있다.
- 예측 스케일링 사용: 부하가 많은 시간이 예측 된다면, 스케일링 해두도록 한다. (예, 저녁 7-10시 사용자가 늘 몰린다면 그 시간동안엔 평소보다 많은 서버를 준비하도록 한다)
- 스케일 인/아웃 정책 분리: 스케일 인/아웃에 대해 다른 지표, 임계값, 쿨다운 기간을 설정하여 안정적으로 운영할 수 있다. 이는 어플리케이션 특성과 지표에 대해 잘 알고있어야 한다.
- 수동개입할 수 있도록 알람설정: 이름에 Auto가 들어갔다고 해서 완벽한 자동화는 아니다. 완벽한건 그 어디에도 존재할 수 없다.. 그러므로.. AutoScaling이 너무 자주 일어나면 운영자가 개입할 수 있도록 알람을 설정하자!
- Graceful Shutdown
- Scale-in/out 동작으로 서버(pod)가 갑자기 종료되면 문제가 발생할 수 있다.
- 어플리케이션이 안전하게 종료될 수 있도록, 모든 작업이 안전하게 완료될 수 있도록 보장해야한다.
- 이를 달성하기 위해 K8s의 Lifecycle Hooks 중 PreStop Hook을 사용할 수 있다.
예를들어, 컨테이너가 종료되기 전 PreStop훅이 호출되어, 정의된 명령이나 스크립트를 실행하고, 어플리케이션을 안전하게 종료시킨다.- [참고] 쿠버네티스(Kubernetes)에서 훅(Hook) 핸들러 종류
- Lifecycle Hooks:
- PostStart: 이 훅은 컨테이너가 생성되고 바로 실행된 직후에 호출한다. 이 훅이 실행되는 동안, 컨테이너의 메인 프로세스는 아직 시작되지 않을 수 있고, PostStart 훅이 성공적으로 실행되면, 컨테이너는 정상적으로 계속 실행된다.
- PreStop: 이 훅은 컨테이너가 종료되기 전에 호출된다. 이 훅을 사용하면 컨테이너가 종료되기 전에 필요한 정리 작업을 수행한다. PreStop 훅이 호출된 후, 시스템은 SIGTERM 신호를 컨테이너의 메인 프로세스에 보내어 종료 절차를 진행한다.
- Admission Hooks: (https://kyverno.io/docs/introduction/ 🔥 Kyverno가 이 훅을 사용한다.)
Admission Hooks는 API 서버에 들어오는 요청을 가로채어 검증하거나 수정할 수 있는 강력한 기능을 제공한다. 이 훅은 주로 리소스 생성, 수정, 삭제 등의 요청이 쿠버네티스 API 서버에 도달하기 전이나 후에 특정 로직을 실행하도록 설정된다.- Validating Admission Webhooks: 이 훅은 요청된 변경 사항이 특정 규칙이나 정책을 준수하는지 검증하고, 검증에 실패하면 요청은 거부된다.
- Mutating Admission Webhooks: 이 훅은 요청된 객체를 수정한다. 예를 들어, 리소스에 대한 기본값 설정, 변경 사항 적용 등을 수행할 수 있다.
- Lifecycle Hooks:
- [참고] 쿠버네티스(Kubernetes)에서 훅(Hook) 핸들러 종류
- 지연반응
- 위에 쿨다운과 같은 맥락인것 같다.
- 주의해야할건, 지연시간을 늘리면 AutoScaling동작이 둔해지고, 지연시간이 짧거나 없으면 스레싱이 발생할 가능성이 높아진다.
- 메트릭 선택
# 지연반응(Cooldown) 설정 예시
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: my-hpa
namespace: my-namespace
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-deployment
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # 스케일 다운 결정을 지연시키는 시간을 초 단위로 설정 (5분)
policies:
- type: Pods
value: 1
periodSeconds: 60
- type: Percent
value: 10
periodSeconds: 60
2.3 관측가능성 프로세스
2.3.1 관측가능성 운영 프로세스
[ 메트릭 분석 ] → [ 추적 분석 ] → [ 로그 분석 ]
↖────┘
이 부분에서 무엇을 설명하고싶은건지 정확하게 모르겠다.. 그래도 혹시모르니 적어놓자.
- 메트릭 분석: SLI (Service Lever Indicater(서비스 수준 지표)), SLO (Service Lever Object(서비스 수준 목표))등 다양한 신뢰성 지표를 응용해서 알람업무 규칙을 개발한다.
- 추적 및 로그는 비용을 제어하기 위해 샘플링 된다.
- 메트릭과 추적은 상호 연결될 수 있도록 태그를 중첩시켜야 한다. 분산추적은 특정 사용자나 상호작용의 요청을 관착할 수 있도록 카디널리티가 높은 태그를 추가하면 좋다 (*카디널리티: 전체 행에 대한 특정 컬럼의 중복 수치)
- 메트릭과 추정명을 일관되게 유지하는것이 텔레메트리의 상호 연관성을 유지하는 방법이다.
- 증가한 트래픽으로 인해서 지연이 발생하고 처리 시간이 증가한다면 문제가 발생한 트랜잭션을 상세히 분석해야한다. TraceID로 거래를 식별하고 지연이 발생한 애플리케이션을 확인한다. 추적의 로그 (스팬콘텍스트)와 태그를 상세히 분석한다.
- 추적은 로깅보다 우선되어야 한다. 수집하는정보는 같지만 콘텍스트가 더 풍부하기 때문이다. 시스템의 정상 작동 여부부터 판단해야하므로, 추적과 메트릭의 역할이 겹칠 때는 메트릭으로 시작한다.
- TraceID를 포함하는 로그파일을 검색하고 로그 파일에 있는 상세 정보를 확인한 후 디버깅을 시작한다. 상관관계에 따라 추적 로그 메트릭을 전환하면서 다양한 측면에서 내부 데이터를 관찰한다.
2.3.2 관측가능성 장애 프로세스
[ 장애 발생 ] → [ 알람 전송 ] → [ 추적분석 ] → [ 로그분석 ]
- 장애가 발생하면 알람이 올 수 있도록 해야한다. 알람을 받으면 메트릭/추적/로그를 활용해서 문제의 원인을 빠르게 확인해야 한다.
- 상세한분석에는 로그를 활용할 수 있다. 구체적인 에러베세지, 입출력메세지등의 정보를 확인하고 디버깅에 활용한다.
- 추적으로는 지연시간, 시스템별 처리시간등을 분석해서 장애가 발생한 시스템을 식별할 수 있다.
- 추적과 로그에 출력되지 않는, 보다 상세한 시스템 수준의 디버깅은 프로파일을 사용한다.
2.4 수평샤딩
수평 샤딩(Horizontal Sharding)은 데이터베이스 관리에서 데이터를 여러 서버에 분산시켜 저장하는 방식을 말한다.
그라파나패밀리 뿐 만 아니라 Kafka, Redis, Cassandra 등 많은 시스템에서 유사한 개념으로 샤딩을 구현한다.
- 샤딩의 특징
- 부하분산: 데이터를 여러 서버에 분산시켜 각 서버의 부하를 줄여 시스템 전체의 성능을 향상시킨다.
- 확장성: 시스템의 데이터 처리 요구가 증가하더라도, 추가 서버를 도입하여 샤딩을 확장함으로써 용이하게 대응할 수 있다.
- 고가용성: 단일 장애 지점(Single Point of Failure, SPOF)을 줄이며, 특정 서버에 문제가 발생해도 시스템 전체의 가용성에 큰 영향을 미치지 않는다.
- 수평샤딩 구현 고려사항
- 샤딩 키 선택: 데이터를 어떻게 분할할지 결정하는 샤딩 키(Sharding Key)를 신중하게 선택해야 한다. (데이터 분산 균형/ 검색 성능)
- 데이터 분산: 데이터 분산 방식을 결정해야한다. (데이터가 서버 간에 고르게 분산되어야 성능 이점을 최대화 할 수 있다.)
- 조인과 트랜잭션 처리: 수평 샤딩 환경에서는 다른 서버에 분산된 데이터 간의 조인이나 트랜잭션 처리가 복잡해질 수 있다.
- 데이터 무결성: 데이터가 여러 서버에 분산되어 있기 때문에, 데이터 무결성을 유지하는 것이 중요하다.
- 수평샤딩의 문제점
-
- 데이터의 재분배
- 샤드간 데이터의 재분배가 이루어질때(리벨런싱) 성능이 엄청나게 낮아질 수 있다.
- 데이터의 재분배가 필요할때란?
- 하나의 샤드로는 더이상 감당하기 어려울때, 샤드간 데이터 분포가 불균등하여 샤드소진(Shard Exhaustion)이 발생할 수 있어 샤드 키를 계산하는 함수를 변경하고 데이터를 재배치해야한다.
- 노드 수가 변경될때마다 (노드의 추가/삭제) 데이터를 재배치 해야한다.
- 유명인사 문제
- "유명인사 문제" 또는 "핫 스팟" 문제는 데이터베이스 또는 분산 시스템에서 특정 데이터에 대한 요청이 집중되어 과도한 트래픽이 발생하는 현상을 말한다.
- 예를 들어, 요리 레시피를 제공하는 온라인 서비스가 있으며, 이 서비스는 재료 이름을 기준으로 데이터를 샤드에 분류하여 저장한다고 가정해보자. 샤딩 키로는 재료의 알파벳 첫 글자를 사용한다. 이때 "Apple pie"와 "Avocado salad"가 유명한 방송에 소개되면서 두 레시피에 대한 검색 요청이 급증한다. 'A' 샤드에 해당하는 재료를 사용하는 레시피 데이터에 대한 검색 요청이 폭발적으로 증가하며, 해당 샤드가 처리해야 할 트래픽 양이 급격히 늘어나게 되며, 결과적으로, 'A' 샤드의 과부하가 전체 서비스의 응답 시간 증가, 사용자 경험 저하 등으로 이어질 수 있다.
- 유명인사 문제는 위의 예시와 같이 데이터 샤딩 전략에서 특정 키(여기서는 알파벳)에 따라 데이터를 분류할 때, 일부 키에 해당하는 데이터에 대한 접근 빈도가 예상보다 훨씬 높을 경우 이 문제가 발생한다.
- 조인과 비정규화
- 여러 샤드 서버를 쪼개고 데이터를 쪼개어 저장하면, 여러 샤드에 걸친 데이터를 조인하기가 힘들어진다. 특히 RDB에는 특히 도전과제가 된다.
- 데이터베이스를 비정규화 하여 조인문제를 해결할 수 있다. (하나의 테이블에서 질의가 수행될 수 있도록)
- 정규화(Normalization)가 데이터의 중복을 최소화하고 무결성을 유지하기 위해 데이터를 여러 테이블로 분할하는 과정이라면, 비정규화는 이러한 분할된 데이터를 다시 통합하거나 중복을 허용하는 것이다.
- 데이터의 재분배
-
2.5 마이크로서비스
견고하고 안정적인 분산시스템을 구성하기 위해서는 쿼럼(Quorum), 가십(Gossip), 해싱, 샤딩등이 필요하다.
이와같은 기법들이 실제 애플리케이션에선 어떻게 구현되어있는지 이 책에서는 아주 자세하게 설명하고 있다.
이 책에서 말하는 관측가능성의 구현체로 Grafana 패밀리 (Loki,Grafana,Tempo,Mimir)를 이야기하며, 아래 설명엔 Grafana 패밀리가 그 대상이다.
- 메트릭 기준으로 AutoScaling을 구현하는것 뿐만 아니라, 동적으로 변경된 리소스에 안정적으로 트래픽을 분배하고, 서비스 디스커버리를 통해 리소스를 모니터링 하는것도 매우 중요하다.
- 그라파나 패밀리(Loki,Grafana,Tempo,Mimir)는 마이크로서비스, 샤딩, 해싱을 아주 잘 구현했다.
2.5.1. 마이크로서비스 개발 흐름
[ 바이너리 ] → [ 도커이미지 ] → [ K8s Pod ]
- 스프링부트, Go로 개발된 소스를 Makefile로 빌드해서 바이너리를 생성한다. 운영환경에서는 바이너리로 실행될수도 있고 도커 컨테이너, 쿠버네티스 파드 등 다양한 *런타임환경에 빠르게 배포되고 운영될 수있다.
* 런타임: 실행환경, 단순히 프로그램이 실행되는 시간을 넘어 실행 환경의 특성과 그 환경에서 프로그램이 어떻게 동작하고 관리되는지까지 포함하는 포괄적인 개념 - 도커파일등을 사용하여 도커 이미지를 빌드한다.
- 도커 이미지를 참조해서 Helm차트를 만들고, 쿠버네티스에 배포한다.
- Grafana 패밀리 (Loki,Grafana,Tempo,Mimir)는 바이너리 파일뿐만 아니라 Docker, K8s등 여러 런타임에 운영될 수 있도록 다양한 형태로 제공된다.
- 이 책에서는 Grafana 패밀리를 쿠버네티스에 배포한다. 쿠버네티스에 배포함으로써 Istio Sidecar, Telemetry API 계측, AutoScaling 등이 유연하고 쉽게 통합된다.
2.5.2 관측가능성의 마이크로서비스
Grafana 패밀리 (Loki,Grafana,Tempo,Mimir)는 마이크로서비스 형태로 개발되어 쿠버네티스에 운영된다.
모듈화된 각 컴포넌트가 마이크로서비스로 구성되었고, 외부통신엔 REST, 내부통신엔 gRPC를 사용한다.
책에서는 Mimir를 예시로 들지만, Mimir보다는 Loki가 조금 더 나에겐 편함으로 Loki로 설명을 대체한다.
- Loki란?
Loki는 여러 clients에서 로그를 수집하고, 백엔드 스토리지에 저장하고, 저장된 로그를 검색할 수 있는 로그 집계시스템이다.- 수집: 여러 클라이언트 (e.g. promtail, fluentbit…)에서 보내주는 로그를 수집한다.
- 저장: 로컬 스토리지에 로그를 가지고있다가 일정 주기로 영구스토리지(e.g. s3, minio…)로 보낸다.
- 쿼리: 저장된 로그는 LogQL 로 쿼리할 수 있으며, Grafana와 바로 통합된다.

- 모듈화된 컴포넌트
- 각 컴포넌트는 각자의 역할을 수행하며, 함께 동작하며 로그 데이터를 수집/저장/조회하는 전체 시스템을 구성한다.
- Distributer
- 클라이언트로부터 로그 데이터를 받아, Ingester를 선택하고, Ingesterd에게 로그를 전달한다.
- 로그 데이터를 해싱하여 해당 데이터가 어떤 Ingester로 전송될지 결정하고, 이 과정은 로드벨런싱 역할로 데이터가 고르게 분산되도록 한다.
- 레이블이 올바른지, 전송률 제한에 걸리지는 않는지 등 들어오는 로그가 사양에 맞는지 확인한다.
- 레이블을 정규화 한다. e.g. {foo="bar", bazz="buzz"}를 {bazz="buzz", foo="bar"}와 동등하게 만든다.
- Ingester
- Ingester는 Distributor로부터 받은 로그 데이터를 메모리에 임시로 저장하고, 일정 시간이나 크기에 도달하면 그 데이터를 Chunk로 만들어 장기 저장소로 플러시(flush)한다.
- 데이터가 장기 저장소로 옮겨지기 전까지 Ingester에서 임시로 관리되므로, 고속의 데이터 쓰기와 질의 성능을 제공한다.
- 수집된 로그가 순서에 맞는지 확인한다. (Write ahead log 설정에 따라 동작이 달라진다)
- Ruler
- Ruler 컴포넌트는 로그 데이터에 대한 경고 규칙(alerting rules)을 평가하고, 경고를 발생시키는 역할을 한다.
- 로그 패턴이나 특정 조건을 기반으로 경고를 설정할 수 있다.
- Querier
- 실제 쿼리를 하는 컴포넌트이다. 저장된 로그 데이터를 검색하고 결과를 반환한다.
- Ingester 인메모리 쿼리와 영구저장소의 데이터를 쿼리하여 반환한다.
- Query frontend
- 옵션이다. 있어도 되고 없어도 된다. 하지만 사용하는것을 권장하는것 같다.
- 쿼리 Queue와 Caching을 담당한다. Querier의 부하를 줄이고, 사용자에게 더 빠른 질의 응답 시간을 제공한다.
- 큰 쿼리가 있을때 OOM을 방지하도록 작은 쿼리로 나누어 Querier에게 전달하고, 결과를 다시 연결한다.
- Chunk Store / Index Store
- 로그 데이터를 "Chunk" 형태로 저장하며, 이 데이터는 Chunk Store에 저장한다. 또한, 로그 데이터를 효율적으로 조회할 수 있도록 인덱스 정보를 Index Store에 저장한다.
- Amazon S3, Google Cloud Storage, Microsoft Azure Storage와 같은 클라우드 스토리지 또는 로컬 파일 시스템을 사용할 수 있다.
- 외부통신엔 REST, 내부통신엔 gRPC
- Loki 아키텍처에서 이러한 통신 방식을 조합하여 외부 사용자와의 상호작용을 위한 편리함과 내부 서비스 간의 효율적인 데이터 처리 및 통신의 필요성을 모두 충족한다.
- 외부 통신: 클라이언트와의 상호작용에서 REST API는 널리 사용되며, HTTP/HTTPS 프로토콜을 기반으로 하는 웹 서비스와의 통합이 용이하다. 사용자가 시스템과 상호작용할 때, 예를 들어 로그 데이터를 조회하거나 설정을 변경하는 등의 작업을 REST API를 통해 수행할 수 있다.
- 내부 통신: Loki의 내부 서비스 간 통신에는 gRPC가 사용된다. (gRPC는 고성능, 확장 가능한 API 개발을 위해 구글에 의해 개발된 오픈 소스 RPC 프레임워크) gRPC는 프로토콜 버퍼(Protocol Buffers)를 사용하여 데이터를 직렬화하므로, 효율적인 데이터 전송이 가능하며, 네트워크 오버헤드를 최소화할 수 있다. 이는 Loki의 분산된 구성 요소 간에 빠르고 효율적인 데이터 교환을 가능하게 한다
Grafana 패밀리 (Loki,Grafana,Tempo,Mimir)는 그 자체로 우수한 마이크로 서비스 구현체다.
2.5.3 읽기와 쓰기를 분리하기
데이터 저장소로부터 읽기와 쓰기를 분리하는 패턴을 CQRS(Command and Query Responsibility Segregation)라 한다.
위에 설명한 Loki뿐만 아니라 대중적으로 쓰이는 많은 시스템에서 이런 구조를 가지고 있다.
이런 CQRS 패턴은 여러 장점을 가지고 있다.
- CQRS 패턴의 이점
- 독립적인 크기 조정: 시스템의 부하가 명령 처리 또는 쿼리 처리 중 어느 한 쪽에 치우쳐 있을 때 독립적으로 확장할 수 있다.
- 최적화된 데이터 스키마: 쿼리 모델은 읽기 연산을 최적화하기 위해 설계되며, 명령 모델은 데이터의 무결성과 변경을 관리하기 위해 최적화한다. 이는 데이터의 저장과 조화 성능을 향상시킬 수 있다.
- 관심사의 분리: 독립적으로 개발, 최적화, 유지보수될 수 있으며 시스템의 복잡성을 낮춰 개발팀의 생산성을 향상시킬 수 있다.
- 보안강화: 민감한 데이터의 변경을 엄격히 통제하고, 필요한 정보만을 제공할 수 있다.
- 단순한 쿼리: 쿼리 모델은 읽기 연산에 특화되어 설계되므로, 애플리케이션에서 필요한 데이터를 조회하는 쿼리가 훨씬 단순해진다.
- CQRS 패턴의 단점 (장점만 말하길래 단점도 추가 😉)
- 복잡성 증가: CQRS는 시스템을 두 개의 별도 부분(명령 처리와 쿼리 처리)으로 분리하므로,전체 아키텍쳐가 복잡성이 증가할 수 있다.
- 데이터의 일관성 유지 어려움: 명령모델과 쿼리모델이 분리되어있을때 두 모델간의 데이터 일관선을 유지하기 어렵다. 특히 이벤트소싱(Event Sourcing)과 함께 사용할 경우, 데이터 동기화와 일관성 유지에 더 많은 주의가 필요하다.
- 개발 및 운영비용 증가/학습곡선/오버엔지니어링의 위험: 시스템의 복잡성이 증가함에 따라, 개발 초기단계에서 보다 많은 설계와 계획이 요구된다. 분리된 아키텍쳐를 운영하고 관리하기 위한 추가적인 리소스 비용이 발생할 수 있다.
Q. 이벤트소싱이 뭔가? 이벤트 소싱과 CQRS를 함께 사용할 때 데이터 동기화와 일관성 유지의 어려움이 발생할까? (아래더보기)
[기본개념 정리]
- 이벤트 소싱(Event Sourcing): 시스템에서 발생하는 모든 변경사항(이벤트)을 순차적으로 저장하는 방식이다.
예를 들어, 은행 계좌에서 돈을 입금하거나 출금하는 각각의 행동이 이벤트가 되어 기록되게 된다. - CQRS(Command Query Responsibility Segregation): 시스템을 두 부분으로 나누는 패턴이다.
하나는 명령(데이터 변경)을 처리하고, 다른 하나는 쿼리(데이터 조회)를 처리한다.
[이벤트 소싱(Event Sourcing)의 예시]
예를 들어, 은행 계좌 시스템을 이벤트 소싱으로 구현한다고 가정한다.
- 계좌 생성 이벤트: 사용자가 새로운 계좌를 생성하면 "계좌 생성" 이벤트가 발생하고 이벤트 스토어에 저장
- 금액 입금 이벤트: 사용자가 계좌에 금액을 입금하면 "금액 입금" 이벤트가 생성되어 이벤트 스토어에 순차적으로 추가
- 금액 출금 이벤트: 사용자가 계좌에서 금액을 출금하면 "금액 출금" 이벤트가 생성되어 저장
이벤트 스토어에는 계좌의 모든 변경 사항이 이벤트의 형태로 저장되며, 이를 통해 현재 계좌의 상태(잔액 등)를 언제든지 재구성할 수 있다. 또한, 이 시스템은 과거의 특정 시점으로 계좌의 상태를 되돌아볼 수 있으며, 시스템의 변경사항에 대한 완벽한 감사 로그(audit log)를 제공한다.
이때 이벤트 소싱과 CQRS(Command Query Responsibility Segregation) 패턴을 함께 사용할 때, 데이터 동기화와 일관성 유지의 어려움은 명령 모델과 쿼리 모델 간의 동기화 과정에서 발생할 수 있다.
- 비동기성: 이벤트가 발생하고 저장되는 과정(명령 처리)과 이 이벤트를 바탕으로 데이터를 조회하는 과정(쿼리 처리)은 비동기적으로 이루어진다. 즉, 이벤트가 저장되고 나서 즉시 쿼리 모델이 업데이트되지 않을 수 있다.
이로 인해 사용자가 최신 데이터를 조회하려 할 때, 아직 쿼리 모델이 최신 상태로 업데이트되지 않아 오래된 정보를 보게 되는 문제가 발생할 수 있다. - 이벤트 처리 순서: 여러 이벤트가 거의 동시에 발생하면, 이 이벤트들이 처리되는 순서가 중요하다.
예를 들어, 계좌에 돈을 입금한 후에 출금하는 이벤트가 발생했다면, 이 두 이벤트가 정확한 순서대로 처리되어야 한다. 만약 순서가 뒤바뀌면, 쿼리 모델이 잘못된 상태를 반영하게 된다. - 동기화 과정: 명령 모델에서 처리한 이벤트를 바탕으로 쿼리 모델을 업데이트하는 과정에서 동기화 문제가 발생할 수 있다. 이벤트 처리 시스템이 바쁘거나 오류가 발생하면, 쿼리 모델이 최신 상태를 반영하는 데 지연이 생길 수 있다.
다시 정리해서 쉽게 이야기 하자면,
쿠팡에서 사과를 구매하는것을 가정한다.
사과를 산다는 이벤트가 발생했다고 기록(이벤트 소싱)하고, 쿠팡의 재고를 감소시키는 작업(명령 처리)과 재고 상태를 보여주는 작업(쿼리 처리)이 있다. 사과를 사고 재고가 감소하는 이벤트를 기록했지만, 재고 상태를 보여주는 화면이 아직 그 변경사항을 반영하지 않았다면, 나는 여전히 오래된 재고 정보를 보게 된다. 여기서 동기화와 일관성을 유지하는 것이 어렵게 된다.
즉, CQRS 패턴이 아닌 단일모델접근 방식이라면 쓰기와 읽기가 동일한 모델을 사용하니까 이런 이슈가 안나오게 된다.
2.6 일관된 해시
위에서 알아봤던 샤딩(Sharding)과 함께 해싱(Hashing)은 데이터를 효과적으로 분산 처리하고, 시스템의 확장성과 성능을 개선하기 위해 사용되며, 상호 보완적인 관계에 있다.
- 해싱의 역할
- 해싱은 키값을 해시 함수라는 수식에 대입하여 계산한 후 나온 결과를 주소로 사용하여 바로 값에 접근시키는 방법이다.
- 데이터 분산: 해싱 함수는 입력값(예: 키 값)을 받아 고정된 크기의 해시값을 출력한다. 이 해시값은 데이터를 여러 샤드에 균등하게 분산시키는 데 사용된다. 해싱을 통해 각 데이터 항목이 어떤 샤드에 저장될지 결정하게 된다..
- 일관된 해싱(Consistent Hashing): 일관된 해싱은 샤드가 추가되거나 제거될 때 전체 데이터의 최소한의 재분배만을 유발하여, 시스템의 확장성을 개선한다.
- 해싱은 데이터저장 뿐만 아니라 검색(해시테이블), 암호화, 데이터무결성검사에 사용할 수 있다.
- 샤딩의 역할
- 데이터 분할 및 확장성: 샤딩은 데이터베이스 또는 데이터 스토어를 더 작은 파티션(샤드)으로 분할하는 프로세스입다. 이를 통해 데이터 관리의 복잡성을 줄이고, 시스템의 확장성을 향상시킬 수 있다.
- 성능 개선: 데이터를 여러 샤드에 분산시킴으로써, 단일 샤드에 대한 부하를 줄이고, 동시에 여러 샤드에서 병렬 처리를 할 수 있게 되어 전체 시스템의 성능이 개선된다.
- 데이터의 병렬처리를 가능하게 하는 기술이 바로 샤딩이다.
- 샤딩은 데이터베이스나 데이터 저장소를 여러 개의 파티션으로 분할하여 데이터를 분산 저장하는 방식이며, 해싱방식이 아니더라도 범위기반샤딩(Range-based Sharding), 디렉터리 기반 샤딩(Directory-based Sharding), 사용자정의 사딩(Custom Sharding)이 있다. 즉, 해싱은 샤딩을 구현하는 효율적인 방법중에 하나이며, 해싱없이 샤딩을 구현할 수 있다.
그렇다면, 해싱에 대해 조금더 확인해보자
- 단순(일반)해싱 (General/Standard/Simple Hashing)
- 작동 방식: 데이터의 키를 해시 함수에 입력하여, 결과값을 해시 테이블의 슬롯 번호로 사용한다. 이 방식은 데이터를 해시 테이블에 고르게 분산시키기 위해 사용된다.
- 문제점: 해시 테이블의 크기(예: 서버의 개수)가 변경될 때, 대부분의 데이터를 재배치해야 한다. 이는 해시 테이블의 크기가 변경될 때마다 새로운 해시 함수 결과값이 다른 슬롯 번호를 가리키기 때문이다.
따라서, 서버 추가나 제거 같은 시스템의 확장 및 축소 작업이 발생할 때 많은 양의 데이터 이동을 초래하며, 이는 시스템에 큰 부하를 주고 성능 저하를 일으킬 수 있다.
- 일관된 해싱(Consistent Hashing)
- 작동 방식: 일관된 해싱은 해시 테이블의 크기 변경이 일어날 때 오직 일부 데이터만 재배치되도록 하는 해싱 알고리즘이다. 해시 공간을 원형으로 가정하고, 데이터와 서버를 해시 공간 상의 포인트로 매핑한다. 데이터는 해시 값에 따라 이 원 위의 가장 가까운 서버 포인트에 할당된다.
- 장점: 서버가 추가되거나 제거될 때, 변경이 필요한 데이터의 양이 훨씬 적다. 오직 새 서버와 인접한 데이터만 다시 할당되며, 나머지 데이터는 영향을 받지 않는다. 이로 인해 시스템의 확장성과 성능이 크게 향상된다. 또한, 핫스폿 키 문제를 줄일 수 있다.
- 단순(일반)해싱 VS 일관된 해싱
- 기본해싱은 슬롯(서버)의 개수가 바뀔때 대부분 아이템을 재배치 해야한다.
- 일관된 해싱은 소수의 아이템만 재배치하게되어 시스템 전체의 안정성과 성능에 미치는 영향이 최소화 된다.
- 해시 테이블(Hash Table)과 해시 링(Hash Ring)은 개념적인 모델이며, 물리적인 테이블이나 저장소가 존재하는것이 아니다.
데이터를 저장하고 검색하는 매커니즘을 구현하기 위한 추상적인 구조(논리적인 모델)이며, 실제 데이터 저장소 위에 추상화된 계층을 제공한다. 이런 개념들은 데이터를 효율적으로 관리하고, 빠르게 접근하기 위한 알고리즘과 자료구조의 일부이다.
Grafana 패밀리 (Loki,Grafana,Tempo,Mimir)는 특정 스토리지에 종속되지 않는다.
이는 곧 스토리지와 데이터베이스가 제공하는 샤딩 기능을 사용하지 못하고, 스토리지 유형에 상관없이 어플리케이션 레벨에서 샤딩을 구현하고 있다는 뜻이다. 또한 디스크레벨이 아닌 메모리 상에 구현된다.
2.7 관측가능성 시각화
Grafana는 시각화 가능한 수많은 종류의 차트를 제공한다.
* 최신 시각화 패널: https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/
Visualizations | Grafana documentation
Path: docs/grafana/latest/panels-visualizations/visualizations/_index.md Copied!
grafana.com
문서에 가장 좋은 최신 자료를 참고할 수 있으니, 앞으로도 문서를 참고하는것이 좋을 것 같다.
좀 신기한건 Canvas이다.
Canvas 패널은 사용자가 자유롭게 커스텀 시각화를 생성하고, 대시보드에 다양한 시각적 요소와 인터랙티브한 그래픽을 추가할 수 있다.
2.8 키-값 저장소
키-값 저장소란, 데이터를 키(Key)와 값(Value)의 쌍으로 저장하는 단순하면서도 효율적인 데이터 저장 방식을 가진 NoSQL 데이터베이스 이며, 대표적으로 Redis, Memcached, AWS Dynamodb, Apache Cassandra, Google LevelDB 등이 있다.
일반적으로 가장 많이 사용하는 키값저장소는 Redis이나, Grafana 패밀리 (Loki,Grafana,Tempo,Mimir)는 Redis보다는 Memcached를 권장하고, 주로 읽기에 사용한다. 병목이 발생하는 부분과 지연없이 많은 트랜젝션을 처리해야하는 요구사항이 있다면 캐시를 사용해서 비약적으로 성능을 향상시킬 수 있다. (+비용절감) --> 우리 Loki는 Redis/Memcached 안써서 읽기가 좀만 커지면 힘든걸까 😭
키-값 쌍에서의 키는 유일해야 하며, 해당 키에 매달린 값은 키를 통해서만 접근할 수 있다.
- 키는 일반 텍스트일수도 있고, 해시값일수도 있다.키는 성능상의 이슈로 짧을수록 좋다.
- 값은 문자열, 리스트, 객체일 수 있다. (제한없다)
이 책에서 키-값 저장소는 아래의 값을 저장하는 용도로 쓰인다.
- 콘술(Consul), Etcd, 멤버리스트(Memberlist) 등을 사용해 구성정보를 관리한다.
- 로그 관리에 사용하는 인덱스 정보를 저장한다.
키-값 저장소 설계시 고려해야할 특성
- 키-값 쌍의 크기는 10KB 이하다.
- 사이즈가 큰 데이터를 저장할 수 있어야 한다.
- 시스템은 장애에도 빠르게 응답하는 높은 가용성을 제공해야 한다.
- 트래픽 양에 따라 자동적으로 서버증설,삭제가 이루어지는 높은 규모의 확장성을 제공해야한다.
- 데이터일관성 수준은 조정이 가능해야 한다. -> 데이터의 일관성과 가용성 사이의 고민은 항상 해야한다.
- 응답 지연 시간이 짧아야 한다.
Q. 데이터의 일관성(Consistency)과 가용성(Availability) 사이의 고민
[상황 설정]
온라인 서점 애플리케이션은 전 세계 여러 지역의 데이터 센터에 데이터를 분산하여 저장합니다. 사용자는 어느 지역에서든지 책을 검색하고, 구매하며, 리뷰를 남길 수 있다.
[일관성(Consistency)의 중요성]
일관성은 모든 사용자가 동일한 데이터를 보고 있다는 것을 보장하는 성질이다.
예를 들어, 한 사용자가 특정 책의 재고를 업데이트(예: 재고 +1)하면, 다른 모든 사용자도 즉시 업데이트된 재고 정보를 볼 수 있어야 한다.
만약 한 지역의 데이터 센터가 업데이트를 처리하고 다른 지역의 데이터 센터와 동기화하는 데 시간이 걸린다면, 일부 사용자는 구식 재고 정보를 보게 될 수 있으며, 이는 재고가 실제보다 적다고 잘못 보여, 사용자가 구매를 망설이게 만들 수 있다.
[가용성(Availability)의 중요성]
가용성은 시스템이 사용자의 요청에 대해 항상 응답을 제공하는 성질이다. 사용자가 언제든지 책을 검색하고 구매할 수 있어야 한다.
만약 데이터의 일관성을 유지하기 위해 모든 데이터 센터가 동기화될 때까지 사용자의 요청을 대기시킨다면, 일부 지역에서는 시스템의 응답이 지연될 수 있다. 예를 들어, 한 지역의 데이터 센터에 장애가 발생해 동기화가 지연된다면, 그 지역의 사용자는 서비스를 이용할 수 없게 된다.
[균형 찾기]
일관성과 가용성 사이의 균형을 찾는 것은 쉽지 않다.
- 일관성을 선택: 모든 사용자가 항상 최신의 데이터를 보는 것을 보장한다.
하지만, 이는 데이터 센터 간의 동기화로 인해 응답 속도가 느려질 수 있다. - 가용성을 선택: 사용자가 시스템에 항상 접근할 수 있도록 한다.
하지만, 이는 사용자가 항상 최신의 데이터를 보는 것을 보장하지 못할 수 있다.
--번외
예전에 S3공부할때 가용성, 일관성, 내구성에 대해 공부했었는데 그때 일관성에 대해 기억이 가물가물해서 한번 다시 찾아봤다.
- 2020년 말, AWS S3는 강력한 읽기 일관성(Strong Read-After-Write Consistency) 모델을 도입했다. 이전에는 최종 일관성(eventual consistency) 모델을 사용했었다.
- https://aws.amazon.com/ko/blogs/korea/amazon-s3-update-strong-read-after-write-consistency/
Amazon S3 업데이트 — 강력한 쓰기 후 읽기 일관성 | Amazon Web Services
2006년에 S3를 출시했을 때 사실상 무제한의 용량 (“…개수에 상관없이 블록을 쉽게 저장…”)과, 99.99%의 가용성을 제공하도록 설계되었으며 데이터가 여러 위치에 투명하게 저장되는 내구성 있
aws.amazon.com
2.9 객체스토리지
객체 스토리지 (Object Storage)란, 데이터를 객체(Object) 단위로 저장하는 데이터 스토리지이다.
객체의 데이터 파일자체 뿐 만 아니라, 데이터에 대한 메타데이터와 고유 식별자가 포함되며, 고유 식별자를 통해 직접 참조하고 접근할 수 있다. 객체 스토리지는 확장성이 뛰어나고, 대규모의 비정형 데이터를 효율적으로 저장하고 관리할 수 있다.
- 객체 스토리지 특징
- 확장성: 객체 스토리지는 거의 무한에 가까운 확장성을 제공한다. 데이터를 평면구조에 저장하기때문에 파일시스템이나 블록스토리지 시스템에서 발생할 수 있는 계층적 구조의 복잡성과 한계없이 데이터를 추가할 수 있다.
- 메타데이터 관리: 각 객체에는 파일 자체 외에도 상세한 정보를 담고있는 메타데이터가 포함된다. 메타데이터는 객체를 분류하고 검색하고 관리하는데 사용할 수 있다.
- 데이터 접근: 객체 스토리지는 HTTP/HTTPS 프로토콜을 이용해 접근한다. RESTful API를 통해 데이터를 저장하고 검색할 수 있다.
- 내구성과 가용성: 객체 스토리지는 데이터를 여러 위치에 자동으로 복제하고, 분산하여 저장함으로써 높은 내구성과 가용성을 보장한다. 데이터 손실의 위험을 최소화 하고 언제든 데이터에 접근할 수 있다.
객체 스토리지는 SSD에 비해 IOPS가 낮지만(이 책에서는 IOPS가 낮다고 이야기 했지만, 정확하게 이야기하면 객체 스토리지에는 IOPS 개념이 없다. 측정의 관점이 다르다.), 대용량 처리에 대한 높은 처리량(Throughput)을 제공한다. (또 완전히 무제한은 아니다. S3만 하더라도 api에 따라 prefix별 limit이 존재한다)
Q. 객체 스토리지는 평면구조에 저장되는 말 그대로의 Object 저장소인데, 왜 Prefix별 제한이 있지?
참고: https://docs.aws.amazon.com/AmazonS3/latest/userguide/optimizing-performance.html
Best practices design patterns: optimizing Amazon S3 performance - Amazon Simple Storage Service
Best practices design patterns: optimizing Amazon S3 performance Your applications can easily achieve thousands of transactions per second in request performance when uploading and retrieving storage from Amazon S3. Amazon S3 automatically scales to high r
docs.aws.amazon.com
S3는 Prefix당 Amazon S3 접두사당 초당 최소 3,500개의 PUT/COPY/POST/DELETE 또는 5,500개의 GET/HEAD 요청을 처리할 수 있다. (Prefix를 나누면 물론 x3500, x5500이 되지만..)
그런데 생각해보면 객체스토리지는 평면구조에 데이터를 저장하고, 또 Object는 말 그대로 객체 그대로를 저장하는거라 Prefix가 디렉토리가 아닌데 (예, test/object1, test/object2 에서 test/가 디렉토리의 개념이 아니고 각 오브젝트의 이름이 test/object1, test/object2 이다. test/는 prefix(접두사)다.) 어째서 Prefix당 Limit이 존재하는걸까?
- Prefix의 역할
- 객체 스토리지에서 Prefix는 객체를 논리적으로 그룹화하는 데 사용된다. 이는 사용자가 특정 패턴을 가진 키를 가진 객체들을 쉽게 조회하고 관리할 수 있게 한다.
- 예를 들어, 'test/' 접두사를 가진 모든 객체를 나열하는 것은 'test'라는 "가상의 폴더" 내의 모든 항목을 조회하는 것과 유사한 효과를 제공한다.
- Limit의 존재 이유
- Prefix 별 limit이 존재하는 이유는, 객체 스토리지 시스템의 성능 최적화 및 관리 효율성을 위함이다.
- 대규모의 객체 스토리지 시스템에서는 수백만에서 수십억 개의 객체를 저장할 수 있어서 특정 Prefix를 가진 객체의 수에 제한을 두지 않는다면, 한 "가상의 폴더" 안에 극도로 많은 객체가 존재할 수 있으며, 이는 조회 성능 저하로 이어질 수 있다. 따라서, Prefix 별로 일정 수의 객체를 넘지 않도록 제한함으로써 시스템의 성능을 유지하고, 사용자가 데이터를 더 효율적으로 관리할 수 있도록 한다.
즉, Prefix와 관련된 제한은 객체 스토리지 시스템의 성능 및 사용성을 최적화하기 위한 설계 선택이다!
시계열 데이터는 시간과 함께 생성되는 빅데이터 이며, 시계열 데이터가 수시로 객체 스토리지에 기록되면 작인 파일이 대량으로 생산된다.작은 데이터는 모아서 하나의 큰 파일로 만들어 효율을 높여야 하며, 지나치게 큰 파일도 네트워크 전송에 시간이 오래 걸리거나 예상치 못한 오류가 발생할 확률을 높인다. 데이터는 적당히 분할하여 저장되어야 한다.
CSV, JSON과 같은 데이터 형식을 컬럼러방식의 파케이(Parquet), ORC 등의 형식으로 변환하여 저장하여 읽고 쓰는 처리 속도를 향상시킬 수 있다.
Grafana 패밀리 (Loki,Grafana,Tempo,Mimir)에서는 객체 스토리지를 영구저장스토어로 사용하고 있다.
- Loki: 로그를 비용 효율적인 객체 스토리지에 저장
- Tempo: 추적 데이터(Trace Data)를 객체 스토리지에 저장
- Mimir: 시계열 데이터의 장기 저장 및 확장성 있는 데이터를 객체 스토리지에 저장
2.10 안정적인 데이터 관리
Grafana 패밀리 (Loki,Grafana,Tempo,Mimir)에서는 안정적인 데이터 관리를 위해 여러 기술을 적용하고 있다.
시스템을 튜닝하고, 트러블슈팅을 위해서는 꼭 알아야 할 몇가지를 소개한다.
- 데이터 파티션
- 데이터 다중화
- 데이터 일관성 (쿼럼)
- 장애감지
- 일시적 장애 처리
데이터 파티션
대규모 어플리케이션에서는 한대의 서버에서 모든 처리를 하는것은 불가능하다.
일관된 해시 (Consistent hash)를 사용해 데이터를 파티션 하면 시스템 부하에 따라 서버의 자동 추가/삭제가 가능해진다.
- 샤딩과 파티션
- 샤딩: 주로 메모리에 적용되며 고유한 파티션키를 통해 재분배가 이루어진다.
- 파티셔닝은 시간 순서에 따라 적재되며 주로 디스크에 적용된다.
파티셔닝을 사용하면 자원의 효율적인 사용을 통해 적은 비용으로 보다 빠르게 처리할 수 있다.
분단위로 파티셔닝을 하면 매 분마다 들어오는 데이터를 사용해서 블록을 만든다. (만약 파티셔닝이 업사면, 대용량 블록 하나를 생성하게 되고 어플리케이션은 대용량 파일을 메모리에 로딩하기 위한 처리시간이 길어지고, 필요한 데이터를 검색하는데도 긴 시간이 필요하다. 인덱스가 없다면 시간은 더 소요된다)
어플리케이션이 병렬처리와 병렬 쿼리를 지원한다면 파티션을 나누고 동시에 다수 프로세스를 실행하는것이 가능하다.
😮💨 이게 무슨소린지 이해하기 위해 Loki를 확인해봤다.
- Client(Promtail)에서 로키로 로그를 보낸다. (Gateway등 설명에 필요없는 컴포넌트는 생략)
- Distributor는 받은 로그 데이터를 일관된 해싱을 통해 해당 데이터가 어느 Ingester에 저장될지 결정하고 로그를 전달한다.
- Ingester는 로그 데이터를 받아서 일정 시간 동안 메모리에 보관한 후(데이터를 청크(chunk) 단위로 그룹화하기 위함), 청크(chunk) 단위로 디스크에 저장한다.
- Ingester는 최종적으로 객체스토리지에 데이터를 Flush 한다.
즉, 여기에서 말하는 샤딩과 디스크 레벨의 파티션이 실제로 이렇게 구현된 것이다. Loki 누가 만든건지 진짜너무너무 똑똑하다.👏👏👏👏
데이터 다중화
높은 가용성과 안정성을 확보하기 위해서 데이터를 N개 서버에 비동기적으로 다중화 해야한다.
N개는 지정 가능한 옵션으로, N개를 선정하는 방법또한 일관된 해시 매커니즘을 통해 결정된다.
😋 이것도 이전에 Loki에 설정한적이 있어서 설명에 추가한다.
Loki에서는 replication_factor 설정을 통해 로그 데이터의 복제본 수를 지정한다.
Distributor가 로그 데이터를 몇 개의 Ingester에게 복사하여 저장할지 결정한다.
예를들어 오른쪽 예시를 보면 (http://<loki>:3100/config) replication_factor를 3으로 설정했다. 이 설정에 따라 Distributor는 받은 로그 데이터를 3개의 다른 Ingester에게 전송하여, 각 Ingester가 데이터의 복사본을 보관하도록한다. (Ingester 간에 로그 데이터를 얼마나 많은 복제본으로 유지할지 설정)
즉 3개의 데이터 복사본이 Ingester에게 전달된다.
Q. Ingester에 3개의 복제본이 있더라도, 객체 스토리지에는 하나만 저장되어야 할텐데? 어떻게 보장하지?
replication_factor가 3일 때, Loki 시스템은 로그 데이터를 3개의 Ingester 인스턴스에 복제하여 저장한다.
이 복제 과정은 주로 데이터의 내구성과 가용성을 높이기 위해 메모리 내 및 임시 디스크 저장 단계에서 이루어지며, 최종적으로 객체 스토리지에 저장될 때는, 데이터의 중복 저장을 방지하기 위한 메커니즘이 작동해야한다.
- 객체 스토리지에 데이터 중복 방지 메커니즘 예시
- Quorum 기반 쓰기: Loki는 복제된 데이터 중에서 쿼럼(quorum)을 형성하는 일부만을 최종적으로 객체 스토리지에 쓰기 위한 결정을 내린다. 쿼럼은 복제본 중 다수의 합의(agreement)를 의미하며, 이는 모든 복제본이 객체 스토리지에 저장되는 것을 방지한다.
- Deduplication: Loki의 Ingester들은 청크(chunk)를 객체 스토리지에 쓰기 전에 청크의 고유성을 검사한다. 이미 동일한 청크가 객체 스토리지에 존재한다면, 중복 청크의 저장을 방지한다. 이 과정은 데이터의 중복 저장을 최소화한다.
- Flush 조건: Ingester는 특정 조건(예: 메모리 사용량, 청크의 시간 범위, 시스템의 부하 등)에 따라 로그 데이터를 청크로 구성하여 객체 스토리지에 Flush한다. 이 때, 여러 Ingester가 동일한 로그 데이터를 처리하더라도, 청크의 고유 식별자나 시간 범위 같은 메타데이터를 기반으로 중복을 방지하는 로직이 적용된다.
- 정족수 합의 (Quorum Consensus): 분산 시스템에서 일반적으로 사용되는 합의(consensus) 알고리즘을 통해, 어느 복제본이 최종적으로 객체 스토리지에 저장될지 결정할 수 있다. Loki 구현에서는 Ingester 간에 합의를 통해 어떤 데이터가 최종적으로 객체 스토리지에 저장될지 결정한다.
공식문서에는 이렇게 나와있다.
When a flush occurs to a persistent storage provider, the chunk is hashed based on its tenant, labels, and contents. This means that multiple ingesters with the same copy of data will not write the same data to the backing store twice, but if any write failed to one of the replicas, multiple differing chunk objects will be created in the backing store.
즉, 영구저장소로 flush가 발생하면, 청크는 테넌트,레이블,콘텐츠에 따라 해시되고 쓰기를 실행한다. 이는 ingester에 여러개의 데이터가 있지만 한번만 쓰기된다는것같다.. 😓(맞나..)
replication_factor 설정은 주로 데이터의 가용성과 내구성을 위한 내부 복제 메커니즘에 영향을 주며, 최종적으로 객체 스토리지에 저장되는 데이터는 중복 없이 관리된다.
데이터 일관성
여러 노드에 다중화된 데이터는 적절히 동기화 되어야 한다. 이때 정족수 합의(Quorum Consensus)를 사용하여 읽기/쓰기에 일관성을 보장한다.
- 정족수 합의(Quorum Consensus)란, 시스템 내의 여러 노드 사이에서 결정을 내리기 위해 필요한 최소한의 동의(또는 투표)의 수를 의미한다.
- 분산 시스템에서 모든 노드가 항상 동시에 동일한 정보를 가지고 있지 않을 때, 특정 작업(예: 데이터 업데이트, 시스템 설정 변경 등)에 대한 결정을 내려야 한다. 정족수 합의는 이러한 결정이 유효하려면 시스템 내에서 일정 수 이상의 노드가 동의해야 함을 의미한다.
- 이는 시스템의 일부 노드가 실패하거나 네트워크 분할로 인해 일시적으로 접근할 수 없는 상황에서도 시스템이 계속해서 안정적으로 운영될 수 있도록 보장한다.
- 정족수 개념은 다양한 합의 프로토콜에서 중요한 역할을 한다. 예를 들어, Paxos나 Raft와 같은 합의 알고리즘은 시스템의 상태를 변경하거나 새로운 데이터를 커밋하기 전에 정족수를 확보해야 한다. 이러한 프로토콜은 분산 시스템의 일관성과 내결함성(Fault Tolerance)을 보장하는 데 필수적이다.
그렇다면, 어떻게 동작하는지 확인해보자.
- 데이터 파티션
- N = 복제본 개수
- W = 쓰기 (쓰기연산에 대한 정족수, 쓰기가 성공한것으로 간주되려면 적어도 W개의 서버로 부터 쓰기가 성공했음을 응답받아야 함)
- R = 읽기 (읽기연산에 대한 정족수, 읽기가 성공한것으로 간주되려면 적어도 R개의 서버로부터 응답 받아야 함)
W, R, N의 값을 정하는 것은 응답 지연과 데이터 일관성 사이의 타협점을 찾는 전형적인 과정이다.
시스템마다 요구되는 일관성의 수준이 다르므로, 그 요구사항에 따라 R,W,N이 조정되어야 한다.
R=1 또는, W=1 구성의 경우, 중재자는 서버 한대의 응답만 받으면 되니 응답속도는 빠르다.
R,W가 1보다 큰 경우에는 시스템이 보여주는 일관성의 수준은 향상될테지만 중재자의 응답속도는 느려진다.
R+W > N 인 경우 강한 일관성이 보장된다.
R=1, W=N인 경우 빠른 읽기 연산에 최적화 되어있다.
W=1, R=N인 경우 빠른 쓰기 연산에 최적화 되어있다.
🤭 Loki의 경우 여기 문서에 보면 이렇게 나와있다.
Replication factor
In order to mitigate the chance of losing data on any single ingester, the distributor will forward writes to a replication_factor of them. Generally, this is 3. Replication allows for ingester restarts and rollouts without failing writes and adds additional protection from data loss for some scenarios. Loosely, for each label set (called a stream) that is pushed to a distributor, it will hash the labels and use the resulting value to look up replication_factor ingesters in the ring (which is a subcomponent that exposes a distributed hash table). It will then try to write the same data to all of them. This will error if less than a quorum of writes succeed. A quorum is defined as floor(replication_factor / 2) + 1. So, for our replication_factor of 3, we require that two writes succeed. If less than two writes succeed, the distributor returns an error and the write can be retried.
Caveat: If a write is acknowledged by 2 out of 3 ingesters, we can tolerate the loss of one ingester but not two, as this would result in data loss.
Replication factor isn’t the only thing that prevents data loss, though, and arguably these days its main purpose is to allow writes to continue uninterrupted during rollouts & restarts. The ingester component now includes a write ahead log which persists incoming writes to disk to ensure they’re not lost as long as the disk isn’t corrupted. The complementary nature of replication factor and WAL ensures data isn’t lost unless there are significant failures in both mechanisms (i.e. multiple ingesters die and lose/corrupt their disks).
floor(replication_factor / 2) + 1 식으로 쓰기 정족수가 결정되며, 과반수이상이어야 하는것으로 보인다.
장애감지
장애 감지(Failure Detection)란, 내트워크 내의 노드(서버, 컴퓨터 등)가 실패했는지 여부를 판단하는 과정이다.
분산시스템에서의 장애감지는 시스템의 가용성과 내구성을 유지하는데 매우 중요한 부분이다.
예전에는 multicast 를 이용했지만 요즘은 가십(Gossip)프로토콜을 사용한다.
- 가십(Gossip) 프로토콜이란?
- 이름에서 알 수 있듯이, 이 프로토콜은 사회적 가십(소문)이 퍼지는 방식을 모방한다.
- 각 노드는 정기적으로 또는 비정기적으로 몇몇 다른 노드와 정보를 교환한다. 이때 노드의 상태 정보(예: 살아있음, 실패함)가 포함된다.
- 노드는 받은 정보를 기반으로 로컬 상태를 업데이트하고, 다음 가십 교환 때 이 정보를 다른 노드와 공유한다.
- 이렇게 해서 노드는 네트워크 상의 장애상태에 대해 일관된 시각을 가지게 된다.
- 가십(Gossip) 프로토콜 장점
- 확장성: 가십 프로토콜은 네트워크의 크기가 커져도 잘 동작한다. 각 노드는 전체 네트워크가 아닌 몇몇 노드와만 정보를 교환하기 때문에, 네트워크 트래픽이 비교적 적게 증가한다.
- 내결함성: 하나 또는 소수의 노드에 장애가 발생해도 시스템 전체의 정보 전파에 큰 영향을 미치지 않는다. 가십 프로토콜은 네트워크 분할(partition) 상황에서도 일정 수준의 정보 전파를 보장한다.
- 자가 치유(Self-healing): 시스템이 자동으로 장애 노드를 감지하고, 필요한 경우 재구성할 수 있다.
- 가십(Gossip) 프로토콜 단점
- 최종 일관성(Eventual Consistency): 가십 프로토콜은 정보가 전체 시스템에 걸쳐 즉각적으로 전파되는 것을 보장하지 않는다. 따라서, 모든 노드가 동일한 시점에 동일한 정보를 가지고 있음을 보장할 수 없다.
Loki도 Memberlist라는 라이브러리(Gossip 프로토콜) 을 사용한다. (서비스 디스커버리 = 장애감지?)
아래 캡쳐는 minikube켜서 얼른 올려본거라 Members가 너무 없다. 그래도 중요한 부분은 확인할 수 있다.
일시적 장애처리
가십프로토콜은 노드의 상태정보를 전파하여 노드가 실패했는지 여부를 판단하게 된다.
이때 어떠한 노드의 장애를 감지했다면, 시스템 가용성을 보장하기 위해 필요한 조치를 해야한다.
만약, 엄격한 정복수 접근법을 쓴다면, 읽기와 쓰기 연산이 금지되게 되야할것이다.
느슨한 정족수 접근법을 사용한다면, 규모에 따라 다르겠지만 노드 한두개 쯤 장애가 나더라도 여전히 가용성을 보장할 수 있을것이다.
Grafana 패밀리 (Loki,Grafana,Tempo,Mimir)에서는 아마도, 정족수 요구사항을 강제하는것이 아니라 해시링에서 건강한 쓰기노드(W)와 읽기노드(R)를 골라 작업을 계속 진행하는것 같다. (아마 책에서 말하려고 하는것이 이 부분인것 같다)
2.11 시계열 데이터 집계
시계열 데이터는 오래된 과거의 데이터를 불러오거나, 정확한 시/분/초 데이터를 검색해야할 때가 있다.
객체 스토리지를 사용하여 시계열 데이터를 저장했을때, 잘못된 인덱스나 쿼리 방법으로 인해 종종 풀스캔(full-scan)이 발생할 수 있다.
풀스캔(full-scan)이란, 데이터베이스, 객체 스토리지, 또는 기타 저장 시스템에서 데이터를 검색할 때, 필터링 조건이나 인덱스를 효과적으로 사용하지 못하여 저장된 전체 데이터 세트를 처음부터 끝까지 검색하는 과정을 말한다.
이는, 시스템 성능 저하, 비용 증가, 자원 과소비를 야기시킨다.
- 풀 스캔(full-scan) 방지 방법
- 적절한 인덱싱: 시계열 데이터의 경우, 시간대별로 인덱싱하는게 좋을것같다.
- 쿼리최적화
- 데이터 파티셔닝
- 캐싱
이 책에서는 컬럼 기반의 열 지향 스토리지에 대해 설명한다.
풀스캔을 방지하기 위해 시계열 데이터 특성에 알맞은 스토리지를 처음부터 사용하는게 좋다는 이야기 인것으로 보인다.
특히 장기적인 분석(데이터 분석 필드에서도 열 지향 스토리지를 널리 사용하는것을 보면)에서는 집계 효율이 더 좋은 열지향 스토리지를 널리 사용한다.
- 열 지향 스토리지(Column-oriented storage)
- 데이터를 열 단위로 저장하여, 특정 열에 대한 연산이나 질의(query)를 빠르게 수행할 수 있다.
특히 데이터 분석 작업과 같이 대량의 데이터에서 특정 열에 대한 연산 특이 읽기 쿼리에 최적화 되어있다. - 컬럼 단위의 통계정보와 메타데이터가 읽기 쿼리 성능의 핵심이다.
- 데이터베이스: Apache Cassandra, Apache HBase, Google Bigtable, Redshift
- 데이터 구조: Apache Parquet, Apache ORC (Optimized Row Columnar).
- 컬럼단위의 통계정보 (이게 핵심인거같음)
- 컬럼에 대한 메타데이터와 통계 정보(예: 최솟값, 최댓값, 합, 평균, 데이터 분포 등)를 저장한다.
- 이를통해 최적화된 데이터 액세스가 가능하고, 효율적으로 데이터를 압축할 수 있다.
- 조건절 푸시다운(push-down)
- 쿼리 조건을 가능한 한 데이터에 가까운 곳으로 "내려보내는" 쿼리 최적화 기술이다.
- 조건절 푸시다운은 데이터 처리의 효율성을 높이기 위해, 실제 데이터를 메모리나 CPU로 불러오기 전에 적용된다.
- 데이터를 열 단위로 저장하여, 특정 열에 대한 연산이나 질의(query)를 빠르게 수행할 수 있다.
Q. 조건절 푸시다운(push-down)에 대해 조금 더 이해해보자.
[조건절 푸시다운 이해하기]
고객 데이터가 저장된 customers 테이블이 있고, 특정 도시(city)에 사는 고객들의 이름(name)을 조회하는 쿼리가 있다고 가정한다.
-----------------------------------------------------------
SELECT name
FROM customers
WHERE city = 'Seoul';
-----------------------------------------------------------
조건절 푸시다운이 적용되지 않는 경우의 쿼리 과정
- customers 테이블의 모든 레코드를 스캔한다.
- 각 레코드에 대해 city = 'Seoul' 조건을 평가한다.
- 조건에 맞는 레코드에서 name 필드를 추출한다.
조건절 푸시다운이 적용 된 경우의 쿼리 과정
- city 컬럼에 대한 인덱스를 사용하여 city = 'Seoul' 조건에 해당하는 레코드만 빠르게 식별한다.
- 이 단계에서, city 컬럼의 인덱스가 있는 경우, 해당 인덱스를 통해 필요한 레코드만 선택적으로 액세스 한다.
- 조건에 맞는 레코드에서 name 필드를 추출한다.
컬럼단위의 통계정보와 메타데이터가 있어 읽기 쿼리에 활용된다.
- 데이터 세그먼트 건너뛰기: 쿼리가 요청하는 값의 범위와 매치되지 않는 데이터 세그먼트(예: 특정 컬럼의 값이 쿼리 조건에 부합하지 않는 경우)는 스캔 대상에서 제외된다. 예를 들어, "날짜" 컬럼에 대한 쿼리가 특정 기간의 데이터만 요청하는 경우, 해당 기간 외의 데이터를 포함하는 세그먼트는 처음부터 스캔하지 않는다.
- 조건에 따른 빠른 필터링: 컬럼의 최소값과 최대값 같은 메타데이터를 사용하여 쿼리 조건과 일치하지 않는 레코드를 빠르게 걸러낼 수 있습니다.
또한 시계열 테이블(Time-Series Table)에 대해서도 언급한다.
아마 프로메테우스가 있어서 시계열 테이블을 언급했나보다.
- 시계열 테이블(Time-Series Table)
- 시간에 따라 변하는 데이터를 저장하고 관리하기 위해 최적화된 테이블 형태다.
- 각 데이터 포인트는 특정 시간에 발생한 이벤트나 측정값을 나타내며, 시간을 주요 차원으로 사용한다.
- 시계열 데이터는 연속적이며, 시간 순서대로 정렬되어 있다.
- 시계열 데이터의 특징
- 시간 기반 인덱싱: 데이터는 시간을 기준으로 인덱싱되고 정렬된다. 이는 시간 범위 쿼리와 순차적 데이터 접근을 효율적으로 만든다.
- 높은 쓰기 처리량: 대부분의 시계열 데이터는 새로운 데이터가 지속적으로 추가되는 형태로 발생한다. 시계열 데이터베이스는 이런 특성을 반영하여 높은 쓰기 처리량을 지원한다.
- 시간 기반 질의 최적화: 시간 범위 선택, 시간 단위로 데이터 집계, 시계열 패턴 탐색 등의 작업이 최적화되어 있다.
- 데이터 압축과 보존 정책: 방대한 양의 시계열 데이터를 효율적으로 관리하기 위해 데이터 압축과 오래된 데이터에 대한 보존 정책(자동 삭제, 다운샘플링 등)을 제공한다.
- 대표적인 시계열 테이블 데이터베이스/시스템
- Prometheus
- TimescaleDB
- InfluxDB
- OpenTSDB