본문 바로가기

Log,Monitorings

[Log] Grafana Loki 성능 최적화 방법 정리

반응형

Loki 3.0에서는 Bloom Filter가 도입될 예정이라고 한다.
(이미 Loki 3.0은 release 되었다. Loki를 갓 도입하는 분들는 3.x 버전으로 도입하는게 좋아보인다.)
 
아마 이게 도입되면 필터링 검색 시 굉장히 빨라질 것으로 생각한다.
 
하지만 현재 2.x 버전에서는 Bloom Filter란 개념이 없기 때문에 Log 사이즈가 많을 경우 검색 속도에 이슈를 겪는 분들이 상당할 것이다.
당연히 Querier OOM 이슈 또한 그렇다.
 
이 글은 이런 문제를 해결할 수 있도록 도움을 줄 수 있을것이다.

3.x 버전이 아닌 2.x 버전 기준으로 작성되었으니 참고하셔서 볼 것.


 

Write

1. Timestamp Oedering
아래 글에 정리해두었으니 참고할 것.
 
https://nyyang.tistory.com/192

[Log] Grafana Loki 로그 Write 시 참고 사항

해당 글에서는 운영 시 참고하면 좋을 내용들만 정리합니다. 쓰기와 읽기로 나누어 각각 어떤 부분을 고려하며 Loki를 운영하면 좋은지에 대해 정리합니다. 정석적인 내용은 아니기에 참고용으

nyyang.tistory.com

 
도입 목적 : 1대 혹은 2대의 파드에 구성된 DaemonSet 파드로부터 Loki로 전송될 경우 큰 이슈를 겪지 않지만 수십, 수백대의 DaemonSet 파드로부터 Loki로 전송될 경우 Timestamp 순서가 보장되지 않는다.
 
이게 무슨 의미인지 모르겠으면 다음 공식 문서와 Loki 소스 코드를 확인하면 도움이 될 수 있다.
 
[1] Loki에 로그 쓰기 API
https://grafana.com/docs/loki/latest/reference/api/#push-log-entries-to-loki

Grafana Loki HTTP API |  Grafana Loki documentation

Open source Grafana Loki HTTP API Grafana Loki exposes an HTTP API for pushing, querying, and tailing log data. Note that authenticating against the API is out of scope for Loki. Microservices mode When deploying Loki in microservices mode, the set of endp

grafana.com

 
 
[2] hb(headblock) metadata 중 mint(최소 nanotimstamp, 최대 nonatimestamp)를 정의하는 부분

 
 
즉 수십개의 Client(fluentBit, Promtail, ..)로부터 Loki HTTP API로 로그를 쓸 때, 특정 Client의 이슈(Network, Queue, ..)로 인해 로그 전송이 늦어졌다면?
 
-> Loki HTTP API를 쏠 때 Timestamp보다 Loki에 도착한 Timestamp가 늦게 도착한다.
 
그럼 Loki Chunk의 메타데이터를 저장할 때 mint(시작 시간)은 로그 전송이 늦춰진 로그 때문에 좀 더 넓은 범위로 저장될 것이다.
 
 
필자는 이를 Kafka queue를 도입하고 파티셔닝을 통해 순서 보장을 함으로써 해결하였으며 실제로 약 10~15대가 뜨는 파드의 경우 로그를 읽는 사이즈가 2~3배 이상 줄어들었음을 확인했다.
 
 
2. 중복 제거 (RF=3일 경우)
이 또한 위 글에 정리를 해놓았다.
 
이 문제는 다음의 방식으로 해결하였다. 물론 이 방식은 완전한 방식이 아니지만 어느정도 중복된 데이터가 s3에 flush되는 이슈는 해결하였다.
 
[1] Memcached - Chunk Cache 도입
[2] writeback 설정
        background:
          writeback_goroutines: xxx
          writeback_buffer: xxxx
          writeback_size_limit: 500000MB
 
[3] ingester.sync_period, sync_min_utilization 설정
      sync_period: 30m
      sync_min_utilization: 0.2
 
 
writeback은 ingester가 s3에 flush 한 뒤 cache layer로 writeback(다시쓰기)하는 설정인데 기본 writeback_size_limit 값이 매우 낮다.
 
따라서 이를 높은 값으로 유지해줘야 ingester에서 writeback을 수행한다.
 
만약 이 값이 꽉 찼을 경우 중복제거가 되지 않는 것을 확인할 수 있을 것이다.
 
 


 

Read

위에 Write를 개선함으로써 Read도 개선된 것을 확인할 수 있었지만 Read path도 개선해줘야 한다.
 
 
1. LogQL 검색 과정 분해하여 병목 원인 파악
 
아래 Grafana Blog 글을 자세히 읽어보자.
 
https://grafana.com/blog/2023/12/28/the-concise-guide-to-loki-how-to-get-the-most-out-of-your-query-performance/

How to improve query performance in Grafana Loki

Michelle Tan   ·   4 Dec 2023   ·   5 min read Log management with Grafana Cloud: 4 observability experts share their move from OSS to Grafana...

grafana.com

 
 
필자는 위 글을 읽고 단일 LogQL이 Query Frontend, Querier에서 어떻게 처리되는지를 분해한 뒤 어디가 병목이 발생하는지를 확인한 뒤 해결하였다.
 
필자의 경우 원인이 다음과 같았다.
 
- time_ordering이 보장되지 않아 Log를 읽어들이는 양이 매우 많았다.
- SubQuery(split_queries_by_interval, sharding 처리 후 파싱된 최종 쿼리)가 높을 경우 8G까지 긁었음
- max_concurrent(querier 동시 처리 개수)가 6개였다.
- 이론상 8 * 6 = 최대 48G까지 querier 내에서 처리될 수 있음을 의미한다. 하지만 querier는 그정도의 memory 공간이 보장되지 않아 oom이 발생했고 실패한 subquery를 재처리하기 위해 계속 loop를 돌았음
 
 
이런 이슈들을 로그를 통해 확인했고 write path를 최적화하여 1차로 완화하였다.
 
 
2. memcached 도입 (frontend cache, chunk cache)
chunk cache는 일반적으로 사이즈가 크기에 memcached를 도입 시 고려해야 할 점이 다소 많다.
 
일반적인 상황이라면 frontend cache만 도입할 것을 권장하지만 필요로 할 경우 chunk cache도 도입해보자.
chunk cache는 ingester에서 중복 제거를 도와주지만 그 외에 read 입장에서는 s3로 읽기 전 memcached를 경유하여 key가 있는지 확인하고 key가 있다면 fetch 해오기 때문에 '이론상' 더 빠르게 검색할 수 있다. 물론 '이론상'이다.
 
 
chunk memcached를 도입 시 network bandwidth를 유심히 살펴봐야 한다.
 
만약 querier에서 memcached request time이 오래 걸린다면 이는 network queue에 막혀있을 가능성이 있다.
 
일반적으로 network bandwidth에서 막혀있을 경우 해결방법은 2가지이다.
 
1) 수평 확장 (인스턴스 스펙을 낮추고 여러대로 분할한다.)
2) n 타입처럼 네트워크 대역폭을 더 사용할 수 있는 인스턴스 타입을 활용 (ex. c5n)
 
필자는 1, 2 방법을 모두 사용했고 꽤나 만족스러운 결과를 얻었다. 추가로 비용이 꽤나 발생하기 때문에 spot instance를 활용했다.
 
 
 
3. Querier 인스턴스 타입 정하기
필자의 경우 m7g, c7g, c7i 타입을 테스트해봤다.
m7g은 처리량이 낮았고 c7g가 일관된 처리량을 보장했다. c7i는 처리량이 들쑥날쑥했다.
 
query frontend 입장에서는 처리량이 느린 subQuery도 기다려야 하기 때문에 가능하면 일관된 처리량을 보장해주는 c7g 같은 인스턴스 타입이 좀 더 권장되는게 아닐까 생각한다.
 
하지만 여기에서도 network 대역폭이 순간적으로 막힘을 경험했고 이로 인해 어쩔수없이 c6in 타입으로 querier 워크로드를 운영중이다.
 
추후 c6gn이나 c7gn 인스턴스 타입이 seoul region에 지원된다면 그 쪽으로 갈아탈 생각을 하고 있다.
 
 
 
4. Office hour에 Querier, Query Frontend 수평 확장 + outstanding request 기반 HPA
 
주말 및 야간 시간대에는 5대 정도로 해두고 개발자들이 주로 보는 Office hour에는 20대 혹은 30대로 미리 스케일링을 해놓는다.
 
추가로 야간 시간대에 확인할 때 5대면 느릴 수도 있기에 outstanding request 기반으로 HPA를 처리했다.
 
이는 KEDA를 활용하였고 크게 어려운 부분은 없으니까 설명은 생략한다.
 
 
 
 
5. (일반적으로 많이 알려진 튜닝 방식) 쿼리 분할 처리 및 동시성
이부분은 대부분 아는 내용이라 담지 않으려 했지만 그래도 혹시 모르니 담아본다.
 
(( 쪼개는 과정))
[1] split_queries_by_interval : 쿼리요청이 들어오면 시간 단위로 맨 처음에 분할한다.

[2] parallelise_shardable_queries(default : true) : 시간 단위로 분할된 뒤에 샤딩이 가능한 쿼리들을 샤딩하여 더 쪼갠다.
 
→ 이렇게 subQuery로 최종 분할된다.
 
(( 쪼개진 서브쿼리들을 처리하는 과정))
[3] tsdb_max_query_parallelism : SubQuery들을 담을 Queue에서 최대로 수행할 수 있는 갯수이 값 만큼 Queue에 들어온 뒤 max_concurrent * 쿼리서 개수 만큼 각 쿼리어들에게 subQuery들을 뿌린다.

[4] max_concurrent : 쿼리어 당 동시에 수행할 수 있는 subQuery 개수

 
위 값들이 무조건 높다고 절대 좋은게 아니며 쿼리를 날릴 시 쿼리어의 Memory가 어떻게 되는지 잘 확인하면서 조정해주면 된다.
 
 
 
 
여기까지 Loki 성능을 개선하는 방법에 대해 정리해보는데 위 작업들을 진행하며 상당한 성능 개선을 경험했다. 물론 생략한 내용들도 있겠지만 이정도만 해도 상당한 개선이 될 것이라 생각한다.

반응형