쿠버네티스의 네트워크 프록시는 각 노드에서 실행되며, 쿠버네티스 API에 정의 된 서비스를 반영하는 역할을 수행한다.
⇒ netfilter는 rule-based 패킷 처리 엔진으로, kernel space에 위치하여 모든 오고가는 패킷의 생명주기를 관찰한다. 그리고 규칭에 매칭되는 패킷을 발견하면 미리 정의된 action을 수행한다. iptables는 이러한 netfilter를 사용해서 chain rule이라는 규칙을 지정하여 패킷을 포워딩하게 한다.
kube-proxy가 위 작업을 직접하게 되면 user space에서 kernel space로 변환이 요구되어 비용이 많이 발생하게 된다. 그래서 iptables 프록시 모드에서는 이러한 역할을 netfilter에게 맡기고 kube-proxy는 netfilter의 규칙을 수정하는 역할만 담당한다.
#클러스터 리스트 확인
kops get cluster --name $NAME
#클러스터의 설정 및 수정
kops edit cluster --name $NAME
#클러스터 내 인스턴스그룹(ig) 리스트 확인
kops get ig --name $NAME
#클러스터 내 인스턴스그룹(ig) 설정 및 수정
kops edit ig --name $NAME [인스턴스그룹명]
각각의 디렉토리에서 git config user.name을 했을 때 지정한 이름이 잘 나오는지를 통해 잘 적용되었는지 확인할 수 있습니다.
Step 3: SSH Key 생성
company 용 ssh key(company.pri)와 personal 용 ssh key(personal.pri)를 각각 생성해준 다음, github에 등록해두고, ~/.ssh/config에서 아래와 같이 수정해줍니다.
Host company-github.com
Hostname github.com
User git
Port 22
IdentityFile ~/.ssh/company.pri
Host personal-github.com
Hostname github.com
User git
Port 22
IdentityFile ~/.ssh/personal.pri
이제 회사 원격 저장소를 쓸 때는 git@company-github.com:~ 과 같은 형태로, 개인 원격 저장소를 쓸 때는 git@personal-github.com:~ 과 같은 형태로 사용해주시면 됩니다.
끝.
Outro.
역시 필요한 기능은 대부분 이미 존재합니다...
gitconfig 파일의 존재를 알게 되었고, 회사 맥북의 좋은 툴(?)을 사용해서 개인 개발이 가능해져서 너무 행복합니다. 🎉
컨트롤러가 뭐냐고 물어보면 뭐라고 할까요? Pod를 관리하는 컴포넌트? 의도한 상태를 유지할 수 있도록 도와주는 컨트롤 루프? 막상 제대로 답하려니 너무 막연하게 표현하게 되는 컨트롤러... 이제는 제대로 답할 수 있도록 제대로 한번 파보도록 하겠습니다.
Controller, 넌 도대체 뭐냐?
불신의 이모티콘
Background
쿠버네티스 설계 원칙 : 선언적 API
쿠버네티스의 동작을 이해하기 위해서는 선언적 API의 의미를 이해해야합니다. 선언적 API라는 개념과 대비되는 개념으로 명령적 API이 있는데, 명령적 API는 말 그대로 어떠한 행위를 수행하도록 명령을 하는 것입니다. 그리고 선언적 API는 특정 상태이길 원하는 상태를 정의함으로써, 원하는 상태가 되도록 지속적으로 맞춰가게 유도합니다. 쿠버네티스는 이러한 선언적 API를 사용함으로써 관리자가 직접 모니터링하며 원하는 상태를 맞춰주기 위해 명령을 내릴 필요 없다는 장점을 얻을 수 있습니다.
Mimir 2.6.x 버전이 나옴에 따라 회사에서 2.5.0으로 버전을 올리는 작업을 진행하고 있습니다.
Mimir 2.5.0은 Helm Chart 4.0.x버전에서 기본으로 사용되며, Helm Chart도 겸사 겸사 4.0.1으로 올리는 작업을 진행하였습니다.
Mimir 2.4.x 버전으로 올리는 작업부터 mimir 관리는 제가 ownership을 가지고 진행하고 이었습니다. (전사 메트릭을 aggregate하는 모니터링 시스템 장애내본 사람 손…! ㅎㅎ)
중요 변경사항
Lower memory usage in store-gateway, ingester and alertmanager
alertmanager는 사용하지 않기 때문에 store-gateway와 ingester의 메모리 사용량을 줄이는 것에서 큰 효과를 볼 수 있을 것으로 예상되었고, 실제로 아주 조금 메모리 사용량이 줄어드는 것을 확인할 수 있었습니다.
Zone aware replication
zone-aware replication 기능은 원래 존재하였지만, 이를 helm chart에서 자체적으로 지원하는 것은 이번에 새로 추가되었습니다.
What I did?
Mimir Helm Chart 버전이 올라가면 주로 mimir.config 값에 변경이 생깁니다. 말그대로 각 컴포넌트에 주입되는 설정 값들이 들어가며, 기본 설정이 변경된 것을 맞춰줍니다.
버전업에서 변경되는 추가적인 설정이 있는지 체크합니다. (이번에는 zone-awareness replication 외에 특별히 달라지는 것은 없었습니다)
새로 추가된 zone-awareness in helm chart를 도입하기 위해 테스트를 진행하고 있습니다. (ing)
Challenge
원래 mimir는 zone-awareness replication 기능을 지원하고 있었습니다. Ingester를 예시로 설명을 하면, ingester를 여러 AZ에 고르게 분배되게 배포하여 HA를 얻어내는 설정입니다.
즉, 하나의 AZ에 장애가 발생해서 해당 AZ에 있는 Ingester가 모두 죽어도, 다른 AZ에 있는 Ingester가 살아있기 때문에 HA를 보장할 수 있는 것입니다. 물론 Ingester의 replication Factor를 잘 설정하는 것도 중요합니다.
ex) 3개의 AZ에 대해 Ingester를 최소 3개 이상 zone-aware하게 배포하고, replication factor를 3으로 두면,
각 AZ에서 최소 한개의 Ingester는 series를 받게 되고,
하나의 AZ가 비가용 상태가 되어도 최소 두 개의 ingester는 series를 받아서 2개의 동의를 바탕으로 합의가 이루어져 정상적으로 series를 저장할 수 있게 되어 series 손실이 발생하지 않습니다.
그래서 무엇이 문제였는가… 하면, helm chart에서 자체적으로 zone-awareness replication 기능을 제공해주지 않아서, 별도의 스크립트를 사용해서 ingester manifest를 만들고 각 az 별로 ingester-a, ingester-b, ingester-c와 같은 형태로 3개의 sts manifest를 만들어서 개별적으로 배포를 해왔습니다. 하지만 이제 helm chart 자체적으로 이를 가능하게 해주면서 충돌이 나게 되었습니다.
즉, zoneAwareReplication.enabled: true 로 설정하면 이제는 알아서 3개의 sts를 생성해주게 되었는데, 이부분이 회사에서 별도로 처리해주던 스크립트와 충돌이 나기 때문에 스크립트를 수정하는 작업을 진행할 예정입니다(글을 작성하는 시점에서는 아직 진행 중입니다)
그리고 3개의 sts에 동시에 kubectl apply가 이루어지면 문제가 발생할 수 있습니다. 3개의 AZ에 있는 ingester가 동시에 rolling되면서 죽어버려서 다운타임이 발생할 수 있습니다. 그래서 3개의 sts에서 하나의 파드만 동시에 rolling되게 보장하는 기능이 필요해지며, 이를 위해서 mimir helm chart에서는 rollout-operator를 사용합니다. rollout-operator는 group을 만들고, group 내에서 하나의 파드만 rolling되는 것을 보장시켜줍니다.
그래도 다행이었던 점은 이미 zone-awareness replication 기능을 사용하고 있었던 점입니다. 이게 아니었다면 zone-awareness replication을 사용하기 위해 별도의 migration 작업을 해야했을 것이고 이는 많게는 12시간 이상 걸리는 작업이 되었을 것입니다. 이미 이 기능을 적용해둔 앞선 작업자분들에게 진심으로 감사합니다. ㅎㅎ
내가 다니는 회사는 mimir를 사용하고 있습니다. Grafana Labs는 mimir에 집중해서 지원하려고 하고 있으며, mimir는 cortex를 기반으로 만들었기 때문에 cortex에 대한 이해가 있다면 손쉽게 mimir로 넘어갈 수 있습니다. 회사에서 mimir를 테스트하고 전환하던 당시에는 남 일이라 생각하고 신경을 안썼지만(애초에 모든게 새로웠던 시기라서 다른 것을 보고 있었다)… 이제는 그런 생각을 버리고 뭐든 내 일이라고 생각하고 mimir를 제대로 파보고 있습니다.
생각보다 mimir는 분산 시스템을 고려해서 잘 설계된 프로젝트였고, 분산 시스템을 공부했던 내 입장에서는 옛날 생각이 나면서 재밌게 공부를 할 수 있었습니다.
Why Mimir?
Github Readme에서는 아래와 같은 점들을 장점으로 소개하고 있습니다.
Easy to install and maintain
Massive scalability
Global view of metrics
Cheap, durable metric storage
High availability
Natively multi-tenant
하지만, 좀더 느낌이 확 와닿는 장점을 추려서 소개하려고 합니다.
Prometheus HA
Mimir의 궁극적인 목표는 Prometheus의 HA를 지원하는 것 입니다. Prometheus는 자체적으로 HA가 되지 않습니다. 그래서 업그레이드할 때 다운타임이 무조건 발생하게 되고, 어떠한 이유로든 Prometheus가 죽게 되면 메트릭 수집이 완전히 멈춰버리는 문제가 발생하게 됩니다. 그러면 모니터링 시스템이 완전히 멈춰버리고, 시스템에 장애가 발생해도 알 수 없는 문제가 발생하게 됩니다. 이는 회사 전체에 큰 영향을 미칠 수 있는 사항이고, 그렇기 때문에 Prometheus의 HA는 선택이 아닌 필수 사항이라고 볼 수 있습니다.
How HA? (Advanced)
HA를 지원하는 방법은 간단합니다. 하나의 클러스터에 prometheus를 여러 개를 두는 것입니다. 그리고 각각의 prometheus는 동일한 metric을 수집하여 mimir distributor라는 component로 전송합니다. (prometheus의 config에서 remote_write의 url에 distributor url을 넣어주면 prometheus의 remote_write 기능을 사용해서 metric을 distributor로 보냅니다). 그런데 각각의 prometheus가 metric을 전송하기 때문에 distributor는 중복되는 metric을 받게 되며(정확히는 완전히 똑같은 metric data는 아니지만 중복되는 기간에 대한 metric을 포함할 수 있습니다), 이를 모두 저장하는 것은 불필요한 작업이며 혼란을 발생시킬 수 있습니다. 그래서 leader replica에서 보낸 metric인지를 식별해서 해당하는 metric만 수용하고, 그 외의 metric은 drop시킵니다.
Prometheus는 로컬 TSDB에 metric을 저장합니다. 하지만 운영 시간이 길어질수록 metric이 계속 쌓이고 용량이 부족해지면 매번 추가적으로 PVC를 수정해서 용량을 늘려줘야하는 번거로움이 발생하게 됩니다. 사실 이렇게 지속적으로 용량이 증가하는 문제를 해결하기 위해서 prometheus에서는 retention을 설정해서 주기적으로 storage를 비워주는 기능을 제공하고 있기는 하지만, 오랜 기간 동안 metric을 저장해야하는 경우에는 결국 prometheus이 storage 사이즈를 필요에 따라 올려줘야하는 상황이 필요하게 됩니다. 하지만 mimir는 최근 metric은 in-memory에 저장하고, 주기적으로 s3로 flush하여 저장하며, 쿼리를 수행할 때는 이 둘을 동시에 사용할 수 있게 하여 Long-Term Storage를 지원합니다.
Metric Aggregation
또한, Mimir는 여러 클러스터에서 발생하는 metric을 하나로 모으는 역할도 수행합니다. 이를 통해 grafana 같은 대시보드에서 매번 data source를 바꿔가며 쿼리를 수행할 필요 없고, 여러 클러스터에 대한 쿼리를 수행할 수 있습니다. 많은 클러스터를 관리하는 기업에서는 이 기능이 매우 유용합니다(매우 매우…).
distributor는 전달받은 metric이 정상적인 metric인지에 대한 vaildation을 수행합니다.
distributor는 metric을 sample 단위로 나눠서 모든 ingester에 전송합니다.
ingester는 sample을 in-memory와 Write-Ahead Log(WAL)에 저장합니다. (WAL을 통해서 ingester가 restart해도 WAL을 통해서 빠르게 복구할 수 있도록 합니다)
ingester는 주기적으로 in-memory에 있는 sample을 block 단위로 local tsdb와 s3에 저장하고, 해당 block에 해당하는 WAL은 제거합니다.
local tsdb에는 default로 24시간(blocks-storage.tsdb.retention-period) 동안 block이 저장되며, s3로 flush하는 과정에서 문제가 생겨도 복구할 수 있도록 하기 위한 용도로 사용됩니다.
ingester는 각각 개별적인 block을 생성하여 저장하며, compactor가 주기적으로 s3에 있는 중복되는 block을 합쳐주어, duplicate samples를 제거합니다.
The Read Path
query-frontend가 query를 분석해서 작은 단위의 query로 나눕니다.
query-frontend는 캐시를 확인하고, 캐시가 없으면 in-memory 상의 큐에 query 작업을 넣어둡니다. 만약 query-scheduler를 사용한다면 query-scheduler의 큐에 query 작업을 넣어둡니다. (appVersion 2.3.0 부터는 default로 query-scheduler를 사용합니다)
캐시가 존재하면 캐시에 있는 query 결과를 client에 리턴합니다.
query-scheduler를 사용함으로써 query-frontend에서 큐를 제거할 수 있으며, 큐를 제거함으로써 큐에 대응하는 worker가 생성되어야하는 문제가 해결되어 query-frontend의 스케일링이 효율적이게 됩니다. (즉, query-frontend 수를 늘리면 강제적으로 worker 수가 폭발적으로 늘어나기 때문에 비효율적이었습니다)
querier는 worker 역할을 수행하며, 큐에서 query를 받아와서 실제로 query 요청을 수행시킵니다.
sample은 s3 (long-term storage)와 ingester에 존재하며, 최신 데이터는 ingester를 통해서, 오래된 데이터는 store-gateway를 통해서 받아옵니다.