본문 바로가기

Log,Monitorings

[EKS] Data Transfer 비용 절감 작업 (istio destination rule, topology aware hints)

반응형

EKS 상에서 Data Transfer 비용을 줄이기 위해 했던 노력들과 그 후기를 정리해본다.
 
결론을 말하자면 서비스 안정성이 더 우선이기에 Data Transfer 줄이기 위한 작업은 포기했다.
 
 

1. Data Transfer 발생 원인

Data Transfer 비용은 크게 2가지에서 발생되었다.
 
1. NAT Gateway 비용
 
2. AZ 간 통신 비용
 
여기서 이제 Bytes 단위로 과금이 되는데 AZ 간 통신할 때 Network Bytes가 많으면 그만큼 과금이 된다고 보면 된다.
 
본 글에서는 AZ 간 통신 Data Transfer 비용에 대해서 다룬다.


2. EKS에서 Data Transfer 절감 방안

요즘 인터넷에 EKS에서 Data Transfer를 절감할 수 있는 다양한 방안들에 대해서 많이 소개되고 있다.
 
1. Topology Aware Hints 사용
 
2. (Istio 사용 시) Destination Rule - Outlier Detection 사용
 
위 2가지를 통해 동일 Zone 간 통신할 수 있도록 설정할 수 있다.
 
Topology Aware Hints의 경우 Istio-Proxy 사이드카 컨테이너가 없는 Pod의 목적지가 Topology Aware Hints 활성화가 되어 있을 경우 동일 Zone으로 가급적 통신하게 되고,
 
Istio Destination Rule의 경우 Istio-Proxy 사이드카 컨테이너가 있은 Pod의 목적지가 Destination Rule의 Outlier Detection이 활성화되어 있고 Locality LB Settings도 활성화 되어 있을 경우 동일 Zone으로 통신하게 된다.
 
 
사실 내용은 별거 없어서 더이상 설명할 것도 없고 인터넷 찾으면 많이 나오니까 자세한 설명은 생략한다.
 


 

3.  Topology Aware Hints , Destination Rule 사용 시 고려해야 할 사항

가장 중요한 내용인데 Zone 간 Pod 개수가 불균등할 경우에 트래픽의 불균형이 발생할 수 있음을 꼭 인지해야 한다.

 필자가 Topology Aware Hints와 Destination Rule 적용을 포기한 이유는 AZ Skew가 달라지는 것으로 인한 트래픽 불균형 때문이었다.
 
그림으로 간단히 알아보자.

 
Destination Rule을 적용했고 Client Pod가 Server Pod로의 요청 시 Server Pod의 AZ Skew가 2대 / 1대일 경우 어떻게 될까?
 
당연히 33%, 33%, 50%의 비율로 트래픽이 전달될 것이다.
 
당연히 Topology Aware Hints는 true 설정만 하면 k8s 내 알고리즘에 의해 자동으로 Zone 간 통신 설정을 해주기에 Destination Rule과는 메커니즘이 다르지만 Destinatoin Rule 뿐만 아니라 Topology Aware Hints으로 인해서도 Pod가 Scaling 되는 과정에서 트래픽 불균형으로 인해 이슈가 발생했었다.
 


 

4. 필자가 했던 설정 적용 방식

필자는 다음의 방식으로 Destination Rule과 Topology Aware Hints를 적용했다.
 
 

4.1. Destination Rule

Destination Rule은 CronJob Pod를 5분마다 한번씩 돌려서 파드 현 상태를 체크하고 그에 따라서 동적으로 주입 / 제거 해주었다.
 
파이썬으로 Pod의 상태를 확인 후 파드 개수 / Zone 간 균등 배포 유무를 확인 해주는게 전부다.
 

from kubernetes import config, client
import os
from kubernetes.client.rest import ApiException

if 'KUBERNETES_PORT' in os.environ:
    config.load_incluster_config()
else:
    config.load_kube_config()


ISTIO_API = 'networking.istio.io'
ISTIO_API_VERSION = 'v1alpha3'
ISTIO_DESTINATIONRULE_PLURAL = 'destinationrules'


# TARGET NAMESPACES
NAMESPACES = []
EXCLUDE_TARGETS = []

crds = client.CustomObjectsApi()
core_api = client.CoreV1Api()


def check_az_balance(namespace_name, app_name):
    az_count = {
        "ap-northeast-2a": 0,
        "ap-northeast-2c": 0
    }

    pods = core_api.list_namespaced_pod(namespace=namespace_name, label_selector=f'app={app_name}').to_dict()['items']

    for pod in pods:
        node_name = pod['spec']['node_name']

        node = core_api.read_node(name=node_name)
        pod_az = node.to_dict()['metadata']['labels']['topology.kubernetes.io/zone']

        az_count[pod_az] += 1

    # 짝수개이면서 a존과 c존의 개수가 다를 경우
    if az_count['ap-northeast-2a'] != az_count['ap-northeast-2c']:
        return False
    return True


########################################
# Namespace 반복
########################################
for namespace in NAMESPACES:
    # Rollouts 조회
    rollout_response = crds.list_namespaced_custom_object(
        group="argoproj.io",
        version="v1alpha1",
        namespace=namespace,
        plural="rollouts"
    )['items']

    # DestinationRules 조회
    destination_rule_response = crds.list_namespaced_custom_object(
        group=ISTIO_API,
        version=ISTIO_API_VERSION,
        namespace=namespace,
        plural=ISTIO_DESTINATIONRULE_PLURAL,
    )['items']

    # Destination rule 이름을 array로 저장
    destination_rules = [dest_rule['metadata']['name'] for dest_rule in destination_rule_response]

    # namespace에 존재하는 rollout 반복
    for rollout in rollout_response:
        replicas = rollout['status']['replicas']
        appName = rollout['status']['selector'].split("app=")[1].split(",")[0]

        ##############################
        # 예외 대상은 Skip
        ##############################
        if appName in EXCLUDE_TARGETS:
            print(f"[Skip] {appName} is in EXCLUDE_TARGET.")
            continue

        ##############################
        # 짝수
        ##############################
        if int(replicas % 2) == 0:
            ############################################################
            # AZ 밸런스가 맞을 경우 DestinationRule을 생성해준다.
            ############################################################
            if check_az_balance(namespace_name=namespace, app_name=appName):
                print(f"[GOOD] {appName} has {replicas} replicas and balance az skew.")

                body = {
                    "apiVersion": "networking.istio.io/v1alpha3",
                    "kind": "DestinationRule",
                    "metadata": {},
                    "spec": {
                        "trafficPolicy": {
                            "outlierDetection": {
                                "consecutive5xxErrors": 100,
                                "interval": "30s",
                                "baseEjectionTime": "30s"
                            }
                        }
                    }
                }

                body['metadata']['name'] = f"{appName}-outlier-detection-rule"
                body['metadata']['namespace'] = namespace
                body['spec']['host'] = f"{appName}.{namespace}.svc.cluster.local"

                ############################################################
                # DestinationRule이 존재하지 않으면 생성해준다.
                ############################################################
                if f"{appName}-outlier-detection-rule" not in destination_rules:
                    print(f"[New] {appName}-outlier-detection-rule not in destination rules. create a new destRule.")

                    try:
                        response = crds.create_namespaced_custom_object(
                            group=ISTIO_API,
                            version=ISTIO_API_VERSION,
                            namespace=namespace,
                            plural="destinationrules",
                            body=body
                        )

                    except ApiException as e:
                        print(f"[Error] Exception when calling CustomObjectsApi->create_namespaced_custom_object: {e}")

                # DestinationRule이 존재하면 생성하지 않는다.
                else:
                    print(f"[Skip] {appName}-outlier-detection-rule already in destination rules.")

            ############################################################
            # AZ 밸런스가 맞지 않을 경우에는 DestinationRule이 있다면 제거해준다.
            ############################################################
            else:
                print(f"[BAD] {appName} has {replicas} replicas and do not balance az skew.")

                ############################################################
                # DestinationRule이 있을 경우 제거한다.
                ############################################################
                if f"{appName}-outlier-detection-rule" in destination_rules:
                    print(f"[Remove] {appName}-outlier-detection-rule is in destination rules. remove Destination Rule.")

                    try:
                        response = crds.delete_namespaced_custom_object(
                            group=ISTIO_API,
                            version=ISTIO_API_VERSION,
                            namespace=namespace,
                            plural=ISTIO_DESTINATIONRULE_PLURAL,
                            name=f"{appName}-outlier-detection-rule"
                        )

                    except ApiException as e:
                        print(f"[Error] Exception when calling CustomObjectsApi->delete_namespaced_custom_object: {e}")

        ############################################################
        # 홀수
        ############################################################
        else:
            ############################################################
            # DestinationRule이 있을 경우 제거한다.
            ############################################################
            if f"{appName}-outlier-detection-rule" in destination_rules:
                print(f"[Remove] {appName}-outlier-detection-rule is in destination rules. remove Destination Rule.")

                try:
                    response = crds.delete_namespaced_custom_object(
                        group=ISTIO_API,
                        version=ISTIO_API_VERSION,
                        namespace=namespace,
                        plural=ISTIO_DESTINATIONRULE_PLURAL,
                        name=f"{appName}-outlier-detection-rule"
                    )

                except ApiException as e:
                    print(f"[Error] Exception when calling CustomObjectsApi->delete_namespaced_custom_object: {e}")

 

4.2. Topology Aware Hints

설정 방법은 그냥 Service Annotation에 넣어주면 알아서 EndpointSlices 정보를 기반으로 Zone 간 라우팅하게 해준다.
 
  annotations:
    service.kubernetes.io/topology-aware-hints: true
 
 
 


 

5. Zone 간 균등 배분을 하기 위한 설정

 
Zone 간 균등 배분을 하기 위해 다음의 설정들을 수행했다.
 
1. Topology Spread Constraints
 
Topology Spread Constraints는 DoNotSchedule (충족되지 않을 경우 Pod를 Pending state로 만들기) , zone 설정을 추가해줬다.

 
topology spread constraints를 할 때 labels에 App의 Version도 추가해줘야 한다. 그래야 디플로이먼트 배포 시 굳이 새로운 Node를 띄우지 않기 때문이다.
 
여기까지는 잘 해줬다.
 
 
2. Scale Down 시에 Topology Spread Constraint 설정을 무시하며 Random하게 Pod를 제거하기에 이를 다시 맞춰주기 위한 Descheduler
 
Descheduler는 문제가 터지고나서 도입한건데, Scheduling의 반대인 De-Scheduling을  수행해주는 오픈소스이다.
 
Descheduler를 써야 하는 이유는 Pod가 스케일 다운 될 때 AZ간 균등 배분 설정을 무시하기 때문이다. (중요)
 
따라서 A 존에 3대, C 존에 2대 였던 파드가 Scale down 되어서 1대가 줄어든다면 A존의 Pod가 줄어들지, C 존의 Pod가 줄어들지 모른다는 의미이다.
 
따라서 A 존의 Pod가 3대, C 존의 Pod가 1대라면 Descheduler가 이를 인지하고 A 존의 Pod 1대를 C 존으로 옮겨줘야 Zone 간 균등 배포 설정이 맞춰질것이다.

 


 

6. 결론

결국 위의 내용들을 적용했음에도 불구하고 어쩃든 Pod 간 트래픽 불균형은 어떻게서든 발생할 수 있으며 AZ Data Transfer 비용을 줄이려고 서비스 안정성을 해치는 작업을 하기보다 그냥 해당 설정들을 다 포기하였다.
 
물론 위 설정에서 좀 더 최적화를 하고, 일부 트래픽 불균형을 감수할 수 있도록 애플리케이션이 설계되었다면 적용해도 무방하다.
 
하지만 꼭 다음의 2가지를 인지하고 설정하여 AZ Tansfer 비용을 줄이는게 중요하다고 말씀드리고 싶다.
 
1. AZ 간 파드를 균등하게 하기 위해서는 Topology Spread Constraint를 잘 설정하자. APP, Version 2가지로 구분해야 잘 동작된다.
 
2. Pod Scale Down 간에는 Topology Spread Constraint를 무시하기에 한쪽 존에 Pod가 몰릴 수 있다. 이를 해결하기 위해 Descheduler를 도입하는 것이 중요하다.
 
3. Topology Aware Hints를 auto로 설정한다고 해서 k8s Cluster가 Pod 불균형을 최대한 우회하여 Zone 간 통신하도록 해줄 것이라 생각할 수 있는데 그렇지 않다. 꼭 다양한 케이스와 가설을 두고 테스트해보는 것이 중요하다.
 
4. Active Active Single Zone EKS 클러스터를 구축하는게 더 좋을수도 있다.
 
 
 
(만약 Pod Scale Up, Down을 짝수개 기준으로 할 수 있다면.. 좋을 것 같다. 방법을 아신다면 알려주세요.)

반응형