본문 바로가기

컨테이너/쿠버네티스 네트워크

[KANS] 쿠버네티스 네트워크 (17) AWS EKS: VPC CNI

목표:

1. VPC CNI이란? 

2. Prefix Delegation ? 

3. NLB vs ALB ( Hyperplane 아키텍처 ? )

4. CoreDNS와 NodeLocalDNS

5. 토스 SLASH 24 - 커널 단 Observability 강화 하기 테스트 

 - Flamegraph

 - page fault 찾기 , OOM 과정 찾아보기 

VPC CNI 

 

☑️  CNI 정의

  • K8s에서는 POD간의 통신을 확장하는 규약을 CNI(Container Network Interface)로 정의 하고 있다. 번역이라 말이 어려운데 쉽게 말해서 CNI  → 네트워크 환경을 구성해준다고 생각하자
  • 네트워크 환경을 구성한다 = IP 할당, 라우팅 테이블 관리,
  • AWS VPC CNI 도 마찬가지로 CNI 인데, 차이점이라고 하면 파드의 IP 네트워크 대역과 노드(워커)의 IP 대역이 같아서 직접 통신이 가능한 점이다.

☑️ Amazon VPC CNI 특징

  • VPC 와 통합 : VPC Flow logs , VPC 라우팅 정책, 보안 그룹(Security group) 을 사용 가능함
  • 기본적으로 CNI의 가장 큰 역할은 pod의 ip를 할당 하는것이라고 할수 있다. 물론 ip도 할당하고 내부에 ip link도 맺고, 각 pod와 노드에 라우팅 테이블도 고치고 많은 역할을 하지만..
  •  ENI 에 미리 할당된 IP(=Local-IPAM Warm IP Pool)를 파드에서 사용할 수 있음 -> Secondary IP
  • 인스턴스당 최대 할당 IP가 인스턴스 성능에 따라 정해짐

 AWS 인스턴스 당 최대 IP 할당 

 

1. 워커 노드 한개당 몇개의 보조 IP를 받을수 있을까? ( Prefix Delegation)_

  • ec2 인스턴스 유형에 따라 pod가 가져갈수 있는 IP가 굉장히 한정적인데, 그부분을 prefix 위임해서 훨씬 많이 쓸수 있다

 

1-1 . 버전 확인 및 VPC CNI install 상태 확인

 

EKS Max pod 개수 증가 - Prefix Delegation + WARM & MIN IP/Prefix Targets : EKS에 직접 설정 후 파드 150대 생성

  • nitro EC2 인스턴스 유형만 적용 가능함 ( c5,m5,r5,a1 등 )
  • /28(IP 주소 16개) IPv4 주소 접두사를 할당하도록 Amazon VPC CNI를 구성할 수 있습니다
  • 버전 1.9.0 이상부터 사용 가능함
  •  WARM_PREFIX_TARGET= 1은 기존 접두사가 단 하나의 파드에 사용되었을지라도 하나의 완전한 (/28) 접두사를 할당한다.
  • 하지만, WARM & MIN IP/Prefix Target 기능은  포드 실행 시간에 약간의 성능 저하가 허용될 경우 사용하면 chunk prefix를 최소화 할수 있다. 
  • 인스턴스에 pod가 25개 올라간다면 접두사 위임은 하나의 ENI를 사용하면 28비트로 총 16개의 ip 청크를 할당 받는다. 25개의 파드를 배포해야 하기 때문에 2개의 청크 즉 32개의 IP를 받아야 한다.이 때  WARM_IP_TARGET을 5로 설정하면 WARM_IP_TARGET의 요구사항인 5를 만족하는 7개의 미사용 IP 주소가 존재하기 때문에 IPAMD는 해당 설정에 만족 할 것이다. 해당 설정에 트래픽이 증가하여 만약 12개의 pod를 더 배포해야 한다면 37개의 IP는 2개의 청크로 부족하기 때문에 ENI에 할당된 접두사를 1개 더 요청

ENI 및 warm-pool 관리

노드가 프로비저닝되면, CNI 플러그인은 자동으로 기본 ENI에 노드의 서브넷에서 슬롯(IP 또는 프리픽스)을 할당하여 pool을 생성(크기는 인스턴스 유형에 따라 결정)합니다. 또한, ENI의 슬롯(IP 주소나 prefix)이 할당되면, CNI는 노드에 추가 ENI를 부착하여 warm-pool을 확장할 수 있습니다. CNI는 필요한 슬롯 수(주로 Pod 수)에 따라 인스턴스에 더 많은 ENI를 붙이게 되고, 더 이상 ENI를 붙일 수 없을 때까지 계속됩니다. 각 인스턴스 유형에는 부착할 수 있는 최대 ENI 수가 있습니다.

 

Warm-Pool이란?
Amazon VPC CNI 플러그인이 Kubernetes 노드에 할당하는 IP 주소 또는 prefix slot을 의미합니다. 웜 풀의 역할은 Kubernetes 클러스터에서 새롭게 생성될 Pod들이 즉시 사용할 수 있는 IP 주소나 네트워크 자원을 사전에 확보하여, Pod의 시작 시간을 단축하는 것입니다. warm-pool의 크기와 slot 수는 인스턴스 유형에 따라 달라지며, 자동으로 확장될 수 있습니다.

 

# 노드 IP 확인 및 PrivateIP 변수 지정
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
N1=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2a -o jsonpath={.items[0].status.addresses[0].address})
N2=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2b -o jsonpath={.items[0].status.addresses[0].address})
N3=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2c -o jsonpath={.items[0].status.addresses[0].address})
echo "export N1=$N1" >> /etc/profile
echo "export N2=$N2" >> /etc/profile
echo "export N3=$N3" >> /etc/profile
echo $N1, $N2, $N3

# 보안그룹 ID와 보안그룹 이름(Name아님을 주의!) 확인
aws ec2 describe-security-groups --query 'SecurityGroups[*].[GroupId, GroupName]' --output text

# 노드 보안그룹 ID 확인
aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text
NGSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text)
echo $NGSGID
echo "export NGSGID=$NGSGID" >> /etc/profile

# CNI 확인
kubectl get pods --selector=k8s-app=aws-node -n kube-system

# aws-node는 VPC CNI를 뜻한다.
# 각 노드마다 1개 씩 배포가 되있음을 확인 할수 있다.

kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2

# Confirm the CNI version. The CNI version must be 1.9.0 or later.
# CNI 버전이 1.9.0보다 높아야 prefix 기능을 이용할수 있다.

kubectl get ds aws-node -o yaml -n kube-system | yq '.spec.template.spec.containers[].env'

# 현재 Prefix Delegation 옵션이 무엇으로 되어 있는지 확인
# 확인 결과 false 
- name: ENABLE_PREFIX_DELEGATION
  value: "false"
#


aws ec2 describe-instances --filters "Name=tag-key,Values=eks:cluster-name" \
  "Name=tag-value,Values=myeks" \
  --query 'Reservations[*].Instances[].{InstanceId: InstanceId, Prefixes: NetworkInterfaces[].Ipv4Prefixes[]}'

# prefix 설정이 되어있는지 확인
[
    {
        "InstanceId": "i-0c6182e3f21515b54",
        "Prefixes": []
    },
    {
        "InstanceId": "i-01623a8bda355f3c9",
        "Prefixes": []
    },
    {
        "InstanceId": "i-0742989209715218e",
        "Prefixes": []
    }
]

kubectl describe node
...
Allocatable:
  cpu:                1930m
  ephemeral-storage:  27905944324
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             7220152Ki
  pods:               29
...

 

 

1-2. set Prefix Delegation

  • aws-node의 데몬셋의 환경변수가 변경 되었지만 node의 Allocate 가능한 pod수는 변경이 되지 않는다.
  • t3.medium이 nitro instance가 아니어서 그런가 싶어서 m5.large로 초기 구성을 다시 했지만 동일하다.
kubectl set env daemonset aws-node -n kube-system ENABLE_PREFIX_DELEGATION=true
kubectl set env ds aws-node -n kube-system WARM_PREFIX_TARGET=1

kubectl describe node
...
Allocatable:
  cpu:                1930m
  ephemeral-storage:  27905944324
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             7220152Ki
  pods:               29
...

 

1-3. 노드 그룹 배포

# Nitro 인스턴스 타입의 Managed 노드 그룹 생성 : 워커 노드 1대
SSHKEYNAME=koo-network
eksctl create nodegroup --cluster $CLUSTER_NAME --region $AWS_DEFAULT_REGION --name nitro-ng --node-type c5.large --nodes 1 --ssh-access --ssh-public-key $SSHKEYNAME

 

1-4. 접두사 위임 확인

# 1-3. 노드 그룹 배포 이후

kubectl describe node

...
Capacity:
  cpu:                2
  ephemeral-storage:  83873772Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             3787668Ki
  pods:               110
Allocatable:
  cpu:                1930m
  ephemeral-storage:  76224326324
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             3097492Ki
  pods:               110
...

NLB vs ALB 어떤 차이가 있을까? 

  •  EKS에서 AWS 책임영역에서 (유저는 만질수 없는 )사용되는 로드벨런서들에 NLB를 사용하는 이유에 대해서 정리해보자
    • 가장 큰 차이점 (ALB vs NLB ) : HTTP를 패킷 처리할수 있는가? L4 vs L7
    • 그렇다면, L4 단계의 패킷만 처리하면 된다면? 성능차이 없는가? 
    • etcd 앞단의 LB는 왜 ELB라 명칭되있을까?  ( NLB+ CLB + ALB )
      • TCP 기반 트래픽을 주로 사용하는 etcd와 ALB는 적합하지 않을것 같은데 ..
      • 그렇담 NLB라고 표기 되면 될거같은데 ..
      • ETCD는 CLB를 쓸 이유가 없을것 같은데..

 

결론 : 성능 및 가용성 차원에서도 NLB가 효율적이다 -> Hyperplane 아키텍처

 

NLB(Network Load Balancer)가 ALB(Application Load Balancer)보다 성능이 더 좋은 이유는 AWS Hyperplane 아키텍처에 의해 더 효율적으로 설계된 네트워크 처리가 가능하기 때문입니다. Hyperplane은 AWS의 고성능 네트워크 가상화 기술로, 이를 기반으로 하는 NLB는 더 낮은 지연 시간과 높은 처리량을 제공할 수 있습니다.

Hyperplane 아키텍처와 NLB의 성능 우위

  1. 고성능 패킷 처리:
    • Hyperplane 아키텍처는 패킷을 물리적 네트워크 장치에서 처리하는 대신 소프트웨어 기반의 분산 시스템으로 처리합니다. 이를 통해 AWS는 확장 가능한 고성능 네트워크를 구축할 수 있습니다.
    • NLB는 Hyperplane의 네트워크 패킷 분산 시스템을 활용하여 L4(전송 계층) 수준에서 직접적으로 패킷을 전달하므로, 네트워크 트래픽을 더 빠르게 전달할 수 있습니다. 이는 L7 계층에서 동작하는 ALB에 비해 훨씬 효율적이며 지연이 적습니다.
  2. 네트워크 기반 부하 분산:
    • Hyperplane은 고속 패킷 처리를 가능하게 하여 NLB가 높은 수준의 네트워크 처리량을 유지할 수 있도록 합니다. NLB는 직접 IP 주소로 라우팅을 수행하는데, Hyperplane은 이를 고도화하여 효율적으로 처리합니다.
    • 반면, ALB는 L7 계층에서 애플리케이션의 상태, 세션 쿠키 등을 검사하므로 네트워크 오버헤드가 발생하게 됩니다. Hyperplane이 이러한 부하를 최적화하고 처리하는 데는 한계가 있어 상대적으로 NLB의 성능이 더 높게 유지됩니다.
  3. 고가용성과 확장성:
    • Hyperplane은 수평적 확장이 가능한 구조로 설계되어, 요청이 증가할수록 더 많은 노드가 추가되어 부하를 분산합니다. 이를 통해 NLB는 수백만 개의 요청을 동시에 처리할 수 있으며, 대규모 네트워크 요청도 빠르게 처리 가능합니다.
    • ALB 역시 Hyperplane을 기반으로 하지만, 애플리케이션 계층 로드 밸런싱의 특성상 세션 상태 관리, 라우팅, SSL 처리 등이 추가되어 더 높은 레이턴시가 발생할 수 있습니다.
  4. 연결 유지와 빠른 응답:
    • Hyperplane은 NLB에서 TCP 및 UDP 트래픽을 직접 처리하도록 하여 연결 유지와 데이터 전송의 효율성을 극대화합니다. ALB는 HTTP/S 트래픽을 처리하기 때문에 애플리케이션 레벨에서의 처리 시간이 추가적으로 요구됩니다.

이러한 이유로 Hyperplane 아키텍처는 NLB와 결합하여 더 낮은 지연 시간, 높은 처리량, 그리고 성능 효율성을 제공할 수 있게 하며, 이로 인해 NLB가 ALB보다 고성능을 유지할 수 있습니다.

 

CoreDNS and NodeLocalDNS 

https://themapisto.tistory.com/240?category=904331

 

Core DNS 성능개선 [ nodelocaldns / ndots & FQDN ]

먼저 CoreDNS 성능 개선을 위해 여러가지 테스트를 하기전에 CoreDNS와 NodeLocalDNS가 무엇인지, 그리고 그 동작과정에 대해 먼저 알아보겠습니다.이를 이해하기 위해서는 ndots과 search 도메인 , 그리고

themapisto.tistory.com

 

# 썰 

운영환경에서 적용 하게 된 이야기

API GW 담당자에게 티켓이 발행됨

Search Domain Exception  이 계속 뜨면서 UDP 패킷들이 드랍된다. (아주 미미하게 0.02% 트래픽 ) - 서비스 영향도 x 

특정 인입에 대해서만 + 트래픽이 몰리는 시간에서 발생하는것으로 보아 DNS Searching을 하는 과정에서 CPU 로드를 확인 함 

하지만 해당 CPU 로드는 호스트 neighbor issue로 밝혀짐 

이참에 FQDN 적용을 하자는 의견이 나옴 

검증계 -> 운영계 전개하는 방식으로 전 인입에 대해서 FQDN 적용 테스트중 

현재 11% 정도의 Coredns 호출 트래픽을 감소시킴 - 끝 - 

  • A. search 도메인이란? 
    • search도메인에 대해 알기 위해 먼저 상대 도메인과 절대 도메인을 알아야 합니다.
    • 예를 들어서, 제가 nslookup에 kubernetes라고 조회를 하면 해당 search 도메인에 맨 앞에 있는 default.svc.cluster.local을 붙여서 조회를 합니다.
    • nslookup에서 리눅스 리졸버는 search도메인을 만들어 DNS요청을 합니다.
    • 하지만 리눅스 리졸버는 모든 search도메인을 요청하지 않습니다. DNS응답결과가 있을 때까지 즉, NOERROR응답을 받을 때까지 search도메인 DNS요청을 합니다. NXDOAMIN은 DNS응답이 없다는 뜻입니다.
  • B. nameserver IP (10.200.1.10) 는 kube-dns 서비스의 IP이다.
  • C. ndots은 뭘까?
    • ndots는 "dot"이라는 DNS에서 사용되는 구분자인 점(.)을 의미하며, 이 숫자는 도메인 이름에 포함된 점(.)의 개수를 나타냅니다. 즉, options ndots:5는 DNS 질의에서 도메인 이름에 점(.)이 5개 포함된 경우까지 검색을 시도한다는 것을 의미합니다.
    •  "foo.bar.svc.cluster.local" 다음과 같이 전체 도메인에 (.) 4개 포함된 경우는 nodts:4 인것이죠.
    • 아니 그럼? 5개 점을 포함한 경우도 있을까요?  이걸 FQDN 이라고 부릅니다. 
      • 절대 도메인과 상대 도메인의 차이를 알아야 합니다.
      • google.com은 상대도메인 - 상대 도메인인 경우엔 ndots의 점 개수를 보고 설정값보다 점 개수가 적으면 search 도메인으로 일일이 정해진 순서대로 모든 search 도메인을 붙여서 조회합니다. 그래서 불필요한 DNS 쿼리 요청이 생김
      • google.com.은 절대 도메인 이기 때문에 search 도메인을 붙이지 않고 한번에 바로 조회합니다. 그래서 불필요한 DNS 요청이 없는것이죠.

CoreDNS Recent Changes 

(심화) Recent changes to the CoreDNS add-on - Link

kubectl get cm -n kube-system coredns -o yaml | kubectl neat

apiVersion: v1
data:
  Corefile: |
    .:53 {
        errors
        health {
            lameduck 5s
          }
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
          pods insecure
          fallthrough in-addr.arpa ip6.arpa
        }
        prometheus :9153
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
    }
kind: ConfigMap
metadata:
  labels:
    eks.amazonaws.com/component: coredns
    k8s-app: kube-dns
  name: coredns
  namespace: kube-system

 

1. topology Spread Constraints 추가 

  • v.10.1-eksbuild.2에서 추가됬음

 CoreDNS 파드가 클러스터 내의 다양한 물리적 위치(예: 다른 가용 영역, 노드 그룹)로 분산됩니다.

# JSON 구성 스키마에 topologySpreadConstraints 매개변수를 추가
aws eks describe-addon-configuration --addon-name coredns --addon-version v1.10.1-eksbuild.2 --query 'configurationSchema' --output text | jq .
aws eks describe-addon-configuration --addon-name coredns --addon-version v1.10.1-eksbuild.2 --query 'configurationSchema' --output text | jq . | grep -A3 topologySpreadConstraints

# check coredns deployment - it returns an empty output because it is not set by default 
kubectl get deploy -n kube-system coredns -o yaml | grep topologySpreadConstraints -A8

# 결과값
      topologySpreadConstraints:
      - labelSelector:
          matchLabels:
            k8s-app: kube-dns
        maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: ScheduleAnyway
      volumes:
      - configMap:
  • 아래는 적용이 안되어있는 경우 업데이트 하는 방법이다.
aws eks describe-addon --cluster-name $CLUSTER_NAME --addon-name coredns | jq
{
  "addon": {
    "addonName": "coredns",
    "clusterName": "myeks",
    "status": "ACTIVE",
    "addonVersion": "v1.10.1-eksbuild.7",
    "health": {
      "issues": []
    },
    "addonArn": "arn:aws:eks:ap-northeast-2:911283464785:addon/myeks/coredns/4ec712ea-54eb-05df-5d57-7b52ef1efc6f",
    "createdAt": "2024-03-10T09:47:58.100000+09:00",
    "modifiedAt": "2024-03-10T09:48:06.073000+09:00",
    "tags": {}
  }
}

# add-on configuration YAML blob
cat << EOT > topologySpreadConstraints.yaml
"topologySpreadConstraints":
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: ScheduleAnyway
    labelSelector:
      matchLabels:
        k8s-app: kube-dns
EOT

# apply change to add-on
aws eks update-addon --cluster-name $CLUSTER_NAME --addon-name coredns --configuration-values 'file://topologySpreadConstraints.yaml'


# check add-on configuration to see if it is in ACTIVE status
aws eks describe-addon --cluster-name $CLUSTER_NAME --addon-name coredns | jq
kubectl get deploy coredns -n kube-system -o yaml | kubectl neat
...
    topologySpreadConstraints: 
    - labelSelector: 
        matchLabels: 
          k8s-app: kube-dns
      maxSkew: 1
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: ScheduleAnyway
...

 

2. coredns 애드온에 PDB(Pod Disruption Budget) 추가

  • PDB: Kubernetes에서 파드의 중단을 제어하기 위한 정책입니다.
    • 유지보수 작업이나 장애 발생 시에도 일부 파드는 항상 클러스터에 남아있게 할 수 있습니다.
    • 노드 종료 중에 동시에 작동이 중지되는 애플리케이션의 Pod 수를 보장
# "coredns" add-on v1.9.3-eksbuid.5 and v1.9.3-eksbuid.6
kubectl get pdb -n kube-system coredns
NAME    MIN AVAILABLE   MAX UNAVAILABLE  ALLOWED DISRUPTIONS   AGE
coredns 1               N/A              1                    13h

# "coredns" add-on v1.10.1-eksbuild.2 and v1.10.1-eksbuild.3
kubectl get pdb -n kube-system coredns
NAME    MIN AVAILABLE   MAX UNAVAILABLE  ALLOWED DISRUPTIONS   AGE
coredns N/A             1                1                     27h

3. coredns DNS 확인 실패를 최소화하기 위해 기본적으로 coreDNS 플러그인에 lameduck 옵션을 추가

  • 플러그인에 lameduck을 추가하면 CoreDNS 포드 다시 시작(예: 상태 문제, 노드 종료 등으로 인해) 또는 배포 롤아웃 중에 DNS 확인 실패가 최소화됨
# CoreDNS 기본 정보 확인
kubectl get cm -n kube-system coredns -o yaml | kubectl neat
data: 
  Corefile: |
    .:53 {
        errors
        health {
            lameduck 5s
          }
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
          pods insecure
          fallthrough in-addr.arpa ip6.arpa
        }
        prometheus :9153
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
    }

 

3. coredns DNS 의 readinessProbe에서 /health 대신 /ready를 사용

  • readinessProbe 목적: 파드가 트래픽을 받을 준비가 되었는지를 확인합니다.
  •  /health 대신 /ready를 사용 ? 
#
kubectl get deploy coredns -n kube-system -o yaml | kubectl neat 
...
      readinessProbe: 
        failureThreshold: 3
        httpGet: 
          path: /ready
          port: 8181
          scheme: HTTP
        periodSeconds: 10
        successThreshold: 1
        timeoutSeconds: 1
...

# The “ready” plugin is already part of the “coredns” ConfigMap:
kubectl get cm -n kube-system coredns -o yaml | kubectl neat
data: 
  Corefile: |
    .:53 {
        errors
        health {
            lameduck 5s
          }
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
          pods insecure
          fallthrough in-addr.arpa ip6.arpa
        }
        prometheus :9153
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
    }

 

stress를 여러 인스턴스로 실행 하고 Flamegraph로 벤치 마킹하기

목표:

# 썰 

운영환경에서 적용 하게 된 이야기

 

CPU Load가 올라가면서 경합이 일어나거나 

갑자기 트래픽이 몰리면서 CPU 사용률이 급격히 오르는경우 ..

메모리 부족의 경우.. OOM 발생 및 발생 후 장애로 이어져서 흔적을 뒤지면서 RCA를 찾을때..

  • 시스템의 물리적 메모리가 부족할 경우, 운영체제는 메모리 페이지를 디스크(스왑 영역)로 사용한다. 이로 인해, 필요할 때마다 페이지가 디스크에서 다시 로드되면서 페이지 폴트가 자주 발생하게 됩니다.
  • 여러 애플리케이션이 메모리를 많이 소비하고, 그 요구량이 물리적 메모리 한도를 초과하면 페이지 폴트가 증가합니다.

최근 ebpf가 흥행하면서 커널에 대한 관심도 올라가고 

다음과 같이 커널을 모니터링 하는 많은 툴들도 사용됨에 따라 나의 관심도 증가하고 있었는데.. 

토스 SLASH 24 의 이재성님 영상에 다음과 같은 그림이 

잠시 나온적이 있는데.. 궁금해서 찾와봤다 Flamegraph.. 

 

 

1. stress를 실행 

stress를 여러 개의 개별 프로세스로 실행하여 메모리 사용량을 높이는 방법입니다.

for i in {1..10}; do     stress --vm 1 --vm-bytes 1G --timeout 60s & done
  • for i in {1..10}: 10개의 stress 프로세스를 실행합니다.
  • --vm 1: 메모리 할당 작업을 수행하는 VM 워커를 1개 생성합니다.
  • --vm-bytes 1G: 각 stress 프로세스가 1GB의 메모리를 할당하도록 설정합니다.
  • --timeout 60s: 각 프로세스가 60초 동안 실행됩니다. 필요에 따라 시간을 조정할 수 있습니다.
  • &: 각 프로세스를 백그라운드에서 실행하여 동시에 여러 개가 실행되도록 합니다.
  • wait: 모든 프로세스가 완료될 때까지 대기합니다.

이 명령어는 동시에 10개의 stress 프로세스를 실행하여 총 10GB의 메모리를 사용하게 됩니다.

 

2.  Flamegraph 벤치마킹 툴 설치

  • perf Tool
$ sudo perf record -F 99 -a -g -- sleep inf

 

perf record는 스택 트레이스 뿐만 아니라 다양한 성능 지표들을 perf.data라는 파일에 기록한다.

 

-F:  초당 몇번씩 샘플을 기록할지 주기를 나타낸다. 보통 49Hz나 99Hz를 많이쓴다. 50이나 100과 같은 값을 사용할 경우에는 lockstep sampling으로 인해 결과가 왜곡될 수 있다고 한다.

-a: 모든 CPU에 대해서 샘플을 기록하라는 의미이다. 이걸 명시하지 않으면 'sleep inf'라는 명령어에 대해서만 샘플을 기록한다.

-g --: perf.data에 호출 스택을 표시하라는 뜻이다. 이때 -- 대신 다른것을 적으면 stack unwinding하는 방법을 선택할 수 있다.

sleep inf: 실행할 명령어이다. perf는 이 명령어가 끝날 때까지 기록한다.

이렇게 perf record를 해서 기록된 perf.data 파일을 분석하면 함수 호출 스택을 분석할 수 있다. 가장 간단한 방법은 perf report 명령어로 perf.data 파일을 분석하는 것이다.

 

  • FlameGraph 모니터링 도구 

perf report는 너무 출력이 많아서 한 번에 보기가 쉽지 않다.

Brendan Gregg가 개발한 FlameGraph라는 도구는 함수 호출 스택을 시각적으로 잘 보여준다.

 

# 플레임 그래프 설치 
git clone https://github.com/brendangregg/FlameGraph.git
cd FlameGraph

# 실제 커널 측정
sudo perf record -F 99 -a -g -- sleep inf

# 가공하는 과정이 조금 필요함 
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > pagefault-flamegraph.svg
# s3로 업로드해서 파일을 노트북 ui에서 확인 하려고 함 
aws s3 cp ./pagefault-flamegraph.svg s3://levin-file/

 

이 Flamegraph는 stress 툴에 의해 메모리와 CPU 부하가 걸린 시스템에서 발생한 page faultOOM(Out of Memory) 상황을 보여줍니다. 주요 스택과 함수들을 분석하여, stress 툴로 인한 메모리 소모가 어떻게 page fault를 유발하고, 최종적으로 OOM(메모리 부족) 상황을 초래하는지 설명하겠습니다.

참고로 가로 공간이 길 수록 메모리를 점유하고 있는 시간이 긴것 이기 때문에 중간지점에 stress 발생 이후 위로 올라가는 순서대로 설명 됩니다.

주요 함수와 스택 분석

  1. stress 프로세스
    • Flamegraph에서 stress 프로세스가 하단에 표시되어 있으며, 메모리 소모 작업을 수행하고 있습니다.
    • stress는 설정에 따라 큰 메모리 블록을 지속적으로 할당하여 메모리 부족 상황을 만들고 있으며, 이는 page fault와 OOM을 유발하는 주요 원인입니다.
  2. do_page_fault 및 async_page_fault
    • do_page_fault와 async_page_fault 함수는 Flamegraph의 중앙 부분에서 나타나며, page fault 처리 루틴입니다.
    • 메모리 부족 상황에서, 필요한 메모리 페이지가 물리적 메모리에 없는 경우 page fault가 발생하며, 이로 인해 do_page_fault와 async_page_fault 함수가 호출됩니다.
    • 이러한 page fault가 발생할 때마다 운영체제는 필요한 메모리 페이지를 확보하려고 시도하고, 이는 메모리 할당 루틴을 촉발합니다.
  3. 메모리 할당 및 관리 함수 (alloc_pages_vma, alloc_pages_nodemask, get_page_from_freelist)
    • alloc_pages_vma, alloc_pages_nodemask, get_page_from_freelist 등은 메모리 할당과 관련된 함수로, page fault 발생 시 운영체제가 새로운 페이지를 할당하려 할 때 호출됩니다.
    • 이 함수들은 물리적 메모리나 스왑 영역에서 페이지를 가져와 필요한 메모리 블록을 할당하려고 시도합니다. 하지만 stress에 의해 메모리가 대부분 사용된 상태이기 때문에 페이지 할당이 원활하게 되지 않으며, page fault가 반복적으로 발생하게 됩니다.
  4. OOM 관련 함수 (out_of_memory, oom_kill)
    • Flamegraph에서 out_of_memory와 oom_kill 함수는 메모리 부족이 심각한 상황에서 호출되는 함수입니다.
    • 시스템이 더 이상 메모리를 할당할 수 없는 경우, 커널은 out_of_memory 함수를 통해 메모리 해제를 시도하고, 특정 프로세스를 종료하여 메모리를 확보하려고 합니다. 이 과정에서 oom_kill이 호출되어 stress 프로세스가 종료될 수 있습니다.
    • oom_kill은 메모리 소모가 심한 프로세스를 대상으로 하므로, 이 예에서는 stress 프로세스가 강제로 종료될 가능성이 큽니다.
  5. 메모리 축소 및 정리 루틴 (shrink, do_try_to_free_pages)
    • shrink, do_try_to_free_pages 등의 함수는 운영체제가 메모리를 확보하기 위해 페이지 캐시를 줄이거나, 사용하지 않는 페이지를 스왑 아웃하려고 시도하는 과정에서 호출됩니다.
    • 이는 메모리를 확보하기 위한 시도로, 메모리 부족 상황을 완화하려는 운영체제의 노력이지만, stress에 의해 메모리가 빠르게 소모되고 있어 큰 효과를 발휘하지 못하고 있습니다.

전체 과정 요약

  1. stress 툴에 의한 메모리 부하: stress 툴이 메모리를 할당하면서 시스템 전체 메모리가 소모됩니다.
  2. page fault 발생: 필요한 페이지가 메모리에 없는 상황이 자주 발생하며, do_page_fault와 async_page_fault 함수가 호출됩니다. 운영체제는 필요한 페이지를 메모리에 로드하려 시도하지만, 메모리가 부족해 page fault가 빈번히 발생합니다.
  3. 메모리 할당 실패 및 OOM: 메모리를 할당할 수 없는 심각한 상황에 도달하면, 커널은 out_of_memory와 oom_kill 함수를 호출하여 stress 프로세스를 종료하는 등의 조치를 취합니다.
  4. 프로세스 종료: 최종적으로 oom_kill에 의해 stress 프로세스가 종료될 가능성이 높으며, 시스템은 메모리 부족 상태를 해소하려 합니다.

결론

Flamegraph를 통해 stress 툴이 메모리를 빠르게 소모하면서 page fault를 유발하고, 최종적으로 시스템이 메모리를 확보할 수 없는 상태에 도달해 OOM 프로세스가 실행되는 과정을 시각적으로 확인할 수 있습니다.

이 그래프는 메모리 소모와 페이지 폴트 증가가 어떻게 OOM과 프로세스 종료를 유도하는지 잘 보여주며, 메모리 집약적인 애플리케이션이 시스템 성능에 어떤 영향을 미치는지 분석하는 데 유용합니다.