GKE에 Memcached 배포


이 튜토리얼에서는 Kubernetes, Helm, Mcrouter로 Google Kubernetes Engine(GKE)에 분산된 Memcached 서버 클러스터를 배포하는 방법에 대해 알아봅니다. Memcached는 널리 사용되는 오픈소스 다목적 캐싱 시스템 중 하나로, 주로 웹 애플리케이션의 속도를 높이고 데이터베이스 로드를 줄이기 위해 자주 사용하는 데이터의 임시 저장소로 사용됩니다.

Memcached의 특성

Memcached의 두 가지 주요 설계 목표는 다음과 같습니다.

  • 단순성: Memcached는 대규모 해시 테이블처럼 작동하면서 비정형 객체를 키별로 저장하고 검색하는 간편한 API를 제공합니다.
  • 속도: Memcached는 RAM(Random Access Memory)에만 캐시 데이터를 유지하므로 데이터 액세스 속도가 매우 빠릅니다.

Memcached는 해시 테이블의 용량을 서버 풀 전체에 수평적으로 확장하는 분산 시스템입니다. 각 Memcached 서버는 풀의 다른 서버와 완전히 분리되어 작동하므로 서버 간의 라우팅과 부하 분산은 클라이언트 수준에서 수행해야 합니다. Memcached 클라이언트는 일관된 해싱 전략을 적용하여 타겟 서버를 적절하게 선택합니다. 이 방법으로 다음 조건을 보장할 수 있습니다.

  • 동일한 키에 항상 동일한 서버가 선택됩니다.
  • 서버 간에 메모리 사용량이 균등하게 유지됩니다.
  • 서버 풀이 축소 또는 확장될 때 재배치되는 키의 수가 최소화됩니다.

Memcached 클라이언트와 Memcached 서버의 분산된 풀 간의 상호작용을 대략적으로 나타낸 다이어그램은 다음과 같습니다.

Memcached와 Memcached 서버 풀 간의 상호작용
그림 1: Memcached 클라이언트와 Memcached 서버의 분산된 풀 간의 대략적인 상호작용

목표

  • Memcached의 분산 아키텍처의 일부 특성에 대해 알아봅니다.
  • Kubernetes 및 Helm을 사용하여 GKE에 Memcached 서비스를 배포합니다.
  • 오픈소스 Memcached 프록시인 Mcrouter를 배포하여 시스템의 성능을 높입니다.

비용

이 문서에서는 비용이 청구될 수 있는 다음과 같은 Google Cloud 구성요소를 사용합니다.

  • Compute Engine

프로젝트 사용량을 기준으로 예상 비용을 산출하려면 가격 계산기를 사용하세요. Google Cloud를 처음 사용하는 사용자는 무료 체험판을 사용할 수 있습니다.

시작하기 전에

  1. Google Cloud 계정에 로그인합니다. Google Cloud를 처음 사용하는 경우 계정을 만들고 Google 제품의 실제 성능을 평가해 보세요. 신규 고객에게는 워크로드를 실행, 테스트, 배포하는 데 사용할 수 있는 $300의 무료 크레딧이 제공됩니다.
  2. Google Cloud Console의 프로젝트 선택기 페이지에서 Google Cloud 프로젝트를 선택하거나 만듭니다.

    프로젝트 선택기로 이동

  3. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다.

  4. API Compute Engine and GKE 사용 설정

    API 사용 설정

  5. Google Cloud Console의 프로젝트 선택기 페이지에서 Google Cloud 프로젝트를 선택하거나 만듭니다.

    프로젝트 선택기로 이동

  6. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다.

  7. API Compute Engine and GKE 사용 설정

    API 사용 설정

  8. Cloud Shell 인스턴스를 시작합니다.
    Cloud Shell 열기

Memcached 서비스 배포

GKE에 Memcached 서비스를 간편하게 배포하는 방법 중 하나는 Helm 차트를 사용하는 것입니다. 배포를 진행하려면 Cloud Shell에서 다음 단계를 수행하세요.

  1. 노드가 3개인 새 GKE 클러스터를 만듭니다.

    gcloud container clusters create demo-cluster --num-nodes 3 --zone us-central1-f
    
  2. helm 바이너리 아카이브를 다운로드합니다.

    HELM_VERSION=3.7.1
    cd ~
    wget https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz
    
  3. 로컬 시스템에 아카이브 파일의 압축을 풉니다.

    mkdir helm-v${HELM_VERSION}
    tar zxfv helm-v${HELM_VERSION}-linux-amd64.tar.gz -C helm-v${HELM_VERSION}
    
  4. PATH 환경 변수에 helm 바이너리의 디렉터리를 추가합니다.

    export PATH="$(echo ~)/helm-v${HELM_VERSION}/linux-amd64:$PATH"
    

    이 명령어는 현재 Cloud Shell 세션 중 모든 디렉터리에서 helm 바이너리를 검색할 수 있도록 설정합니다. 이 구성이 여러 세션 간에 지속되도록 하려면 명령어를 Cloud Shell 사용자의 ~/.bashrc 파일에 추가합니다.

  5. 고가용성 아키텍처에서 새로운 Memcached Helm 차트 릴리스를 설치합니다.

    helm repo add bitnami https://charts.bitnami.com/bitnami
    helm install mycache bitnami/memcached --set architecture="high-availability" --set autoscaling.enabled="true"
    

    Memcached Helm 차트는 StatefulSet 컨트롤러를 사용합니다. StatefulSet 컨트롤러를 사용하는 이점 중 하나는 pod 이름이 순서대로 정렬되며 예측할 수 있다는 점입니다. 이 경우 이름은 mycache-memcached-{0..2}입니다. 이러한 정렬을 사용하면 Memcached 클라이언트가 서버를 쉽게 참조할 수 있습니다.

  6. 실행 중인 pod를 확인하려면 다음 명령어를 실행합니다.

    kubectl get pods
    

    Google Cloud Console 출력은 다음과 같습니다.

    NAME                  READY     STATUS    RESTARTS   AGE
    mycache-memcached-0   1/1       Running   0          45s
    mycache-memcached-1   1/1       Running   0          35s
    mycache-memcached-2   1/1       Running   0          25s

Memcached 서비스 엔드포인트 검색

Memcached Helm 차트는 Headless 서비스를 사용합니다. Headless 서비스는 개별 pod를 검색할 수 있도록 모든 pod의 IP 주소를 노출합니다.

  1. 배포된 서비스가 Headless인지 확인합니다.

    kubectl get service mycache-memcached -o jsonpath="{.spec.clusterIP}"
    

    None 출력은 서비스에 clusterIP가 없음을 확인하며 따라서 Headless입니다.

    서비스는 호스트 이름의 DNS 레코드를 다음 형식으로 생성합니다.

    [SERVICE_NAME].[NAMESPACE].svc.cluster.local
    

    이 가이드에서 서비스 이름은 mycache-memcached입니다. 네임스페이스가 명시적으로 정의되지 않았으므로 기본 네임스페이스가 사용되며 따라서 전체 호스트 이름은 mycache-memcached.default.svc.cluster.local입니다. 이 호스트 이름은 서비스에서 노출된 세 개의 pod 모두의 IP 주소 및 도메인 집합으로 결정됩니다. 향후 풀에 다른 pod가 추가되거나 기존의 pod가 제거될 경우 kube-dns는 DNS 레코드를 자동으로 업데이트합니다.

    다음 단계에서 설명하는 바와 같이 Memcached 서비스 엔드포인트를 검색하는 것은 클라이언트의 책임입니다.

  2. 엔드포인트의 IP 주소를 검색합니다.

    kubectl get endpoints mycache-memcached
    

    출력은 다음과 비슷합니다.

    NAME                ENDPOINTS                                            AGE
    mycache-memcached   10.36.0.32:11211,10.36.0.33:11211,10.36.1.25:11211   3m
    

    각 Memcached pod에는 각각 10.36.0.32, 10.36.0.33, 10.36.1.25의 개별 IP 주소가 있습니다. 이 IP 주소는 사용자의 서버 인스턴스마다 다를 수 있습니다. 각 pod는 Memcached의 기본 포트인 11211 포트를 수신합니다.

  3. 2단계의 대안으로 Python과 같은 프로그래밍 언어를 사용하여 DNS를 검사할 수 있습니다.

    1. 클러스터 내에서 Python 대화형 콘솔을 시작합니다.

      kubectl run -it --rm python --image=python:3.10-alpine --restart=Never python
      
    2. Python 콘솔에서 다음 명령어를 실행합니다.

      import socket
      print(socket.gethostbyname_ex('mycache-memcached.default.svc.cluster.local'))
      exit()
      

      출력은 다음과 비슷합니다.

      ('mycache-memcached.default.svc.cluster.local', ['mycache-memcached.default.svc.cluster.local'], ['10.36.0.32', '10.36.0.33', '10.36.1.25'])
  4. 11211 포트에서 실행 중인 Memcached 서버 중 하나로 telnet 세션을 열어 배포를 테스트합니다.

    kubectl run -it --rm busybox --image=busybox:1.33 --restart=Never telnet mycache-memcached-0.mycache-memcached.default.svc.cluster.local 11211
    

    telnet 프롬프트에서 Memcached ASCII 프로토콜을 사용하여 다음 명령어를 실행합니다.

    set mykey 0 0 5
    hello
    get mykey
    quit

    출력 결과가 다음과 같이 굵게 표시됩니다.

    set mykey 0 0 5
    hello
    STORED
    get mykey
    VALUE mykey 0 5
    hello
    END
    quit

서비스 검색 로직 구현

이제 다음 다이어그램과 같이 기본 서비스 검색 로직을 구현할 준비가 되었습니다.

서비스 검색 로직
그림 2: 서비스 검색 로직

대략적으로 서비스 검색 로직은 다음 단계로 구성됩니다.

  1. 애플리케이션은 mycache-memcached.default.svc.cluster.local의 DNS 레코드에 대해 kube-dns를 쿼리합니다.
  2. 애플리케이션은 해당 레코드에 연결된 IP 주소를 검색합니다.
  3. 애플리케이션은 새로운 Memcached 클라이언트를 인스턴스화하고 검색된 IP 주소를 제공합니다.
  4. Memcached 클라이언트의 통합 부하 분산기는 주어진 IP 주소의 Memcached 서버에 연결됩니다.

이제 Python을 사용하여 이 서비스 검색 로직을 구현하겠습니다.

  1. 클러스터에 새로운 Python 지원 pod를 배포하고 pod 내에서 셸 세션을 시작합니다.

    kubectl run -it --rm python --image=python:3.10-alpine --restart=Never sh
    
  2. pymemcache 라이브러리를 설치합니다.

    pip install pymemcache
    
  3. python 명령어를 실행하여 Python 대화형 콘솔을 시작합니다.

  4. Python 콘솔에서 다음 명령어를 실행합니다.

    import socket
    from pymemcache.client.hash import HashClient
    _, _, ips = socket.gethostbyname_ex('mycache-memcached.default.svc.cluster.local')
    servers = [(ip, 11211) for ip in ips]
    client = HashClient(servers, use_pooling=True)
    client.set('mykey', 'hello')
    client.get('mykey')
    

    출력은 다음과 같습니다.

    b'hello'

    b 프리픽스는 Memcached가 데이터를 저장하는 형식인 바이트 리터럴을 나타냅니다.

  5. Python 콘솔을 종료합니다.

    exit()
    
  6. Control+D를 눌러 pod의 셸 세션을 종료합니다.

연결 풀링 사용 설정

캐싱의 필요성이 증가하고 수십, 수백, 수천 개의 Memcached 서버로 풀이 확장되면 몇 가지 제한이 발생할 수 있습니다. 특히 Memcached 클라이언트의 개방형 연결의 수가 많으면 다음 다이어그램과 같이 서버에 부하가 많아질 수 있습니다.

모든 Memcached 클라이언트가 모든 Memcached 서버에 직접 액세스할 경우 많은 수의 개방형 연결 발생
그림 3: 모든 Memcached 클라이언트가 모든 Memcached 서버에 직접 액세스할 경우 많은 수의 개방형 연결 발생

개방형 연결의 수를 줄이려면 다음 다이어그램처럼 프록시를 도입하여 연결 풀링을 사용해야 합니다.

프록시로 연결 풀링 구현
그림 4: 프록시를 사용하여 개방형 연결 수 축소

강력한 오픈소스 Memcached 프록시인 Mcrouter('믹 라우터'라고 발음)를 사용하면 연결 풀링을 구현할 수 있습니다. Mcrouter는 표준 Memcached ASCII 프로토콜을 사용하므로 원활하게 통합됩니다. Memcached 클라이언트에 대해 Mcrouter는 일반적인 Memcached 서버처럼 작동하고 Memcached 서버에 대해 Mcrouter는 일반적인 Memcached 클라이언트처럼 작동합니다.

Mcrouter를 배포하려면 Cloud Shell에서 다음 명령어를 실행합니다.

  1. 이전에 설치한 mycache Helm 차트 출시 버전을 삭제합니다.

    helm delete mycache
    
  2. 새로운 Mcrouter Helm 차트 릴리스를 설치하여 새로운 Memcached pod와 Mcrouter pod를 배포합니다.

    helm repo add stable https://charts.helm.sh/stable
    helm install mycache stable/mcrouter --set memcached.replicaCount=3
    

    이제 프록시 pod가 클라이언트 애플리케이션의 요청을 수락할 준비가 되었습니다.

  3. 프록시 pod 중 하나에 연결하여 이 설정을 테스트합니다. Mcrouter의 기본 포트인 포트 5000에서 telnet 명령어를 사용합니다.

    MCROUTER_POD_IP=$(kubectl get pods -l app=mycache-mcrouter -o jsonpath="{.items[0].status.podIP}")
    
    kubectl run -it --rm busybox --image=busybox:1.33 --restart=Never telnet $MCROUTER_POD_IP 5000
    

    telnet 프롬프트에서 다음 명령어를 실행합니다.

    set anotherkey 0 0 15
    Mcrouter is fun
    get anotherkey
    quit

    명령어가 키 값을 설정하고 표시합니다.

이제 연결 풀링을 구현하는 프록시가 배포되었습니다.

지연 시간 단축

복원력을 향상시키기 위해 여러 개의 노드가 포함된 클러스터를 사용하는 것이 일반적입니다. 이 가이드에서는 3개의 노드가 포함된 클러스터를 사용합니다. 하지만 여러 개의 노드를 사용하면 노드 간 네트워크 트래픽의 부하가 커져 지연 시간이 늘어날 위험이 있습니다.

동일 노드에 프록시 pod 배치

클라이언트 애플리케이션 pod를 동일한 노드 내의 Memcached 프록시 pod에만 연결하여 이 위험을 줄일 수 있습니다. 이 구성을 다이어그램으로 나타내면 다음과 같습니다.

pod 간의 상호작용을 나타내는 토폴로지
그림 5: 3개의 노드가 포함된 클러스터에서 애플리케이션 pod, Mcrouter pod, Memcached pod 간의 상호작용을 나타내는 토폴로지

다음 단계를 따라 이 구성을 수행하세요.

  1. 각 노드에 실행 중인 프록시 노드가 하나씩 포함되어 있는지 확인합니다. 일반적인 방법은 DaemonSet 컨트롤러를 사용하여 프록시 pod를 배포하는 것입니다. 노드가 클러스터에 추가되면 여기에 새로운 프록시 pod가 자동으로 추가됩니다. 클러스터에서 노드가 제거되면 이 pod가 가비지로 수집됩니다. 이 가이드에서는 앞서 배포한 Mcrouter Helm 차트가 기본적으로 DaemonSet 컨트롤러를 사용하므로 이 단계는 이미 완료되었습니다.
  2. 프록시 컨테이너의 Kubernetes 매개변수에 hostPort 값을 설정하여 노드가 해당 포트를 리슨하고 트래픽을 프록시로 리디렉션하도록 합니다. 이 가이드에서는 Mcrouter Helm 차트가 기본적으로 5000 포트에 대해 이 매개변수를 사용하므로 이 단계도 이미 완료되었습니다.
  3. spec.env 항목을 사용하고 spec.nodeName fieldRef 값을 선택하여 애플리케이션 pod 내에 노드 이름을 환경 변수로 노출합니다. 이 방법에 대한 자세한 내용은 Kubernetes 문서를 참조하세요.

    1. 샘플 애플리케이션 pod를 배포합니다.

      cat <<EOF | kubectl create -f -
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: sample-application
      spec:
        selector:
          matchLabels:
            app: sample-application
        replicas: 9
        template:
          metadata:
            labels:
              app: sample-application
          spec:
            containers:
              - name: busybox
                image: busybox:1.33
                command: [ "sh", "-c"]
                args:
                - while true; do sleep 10; done;
                env:
                  - name: NODE_NAME
                    valueFrom:
                      fieldRef:
                        fieldPath: spec.nodeName
      EOF
      
  4. 샘플 애플리케이션 pod 중 하나를 확인하여 노드 이름이 노출되었는지 확인합니다.

    POD=$(kubectl get pods -l app=sample-application -o jsonpath="{.items[0].metadata.name}")
    
    kubectl exec -it $POD -- sh -c 'echo $NODE_NAME'
    

    이 명령어는 노드 이름을 다음 형식으로 출력합니다.

    gke-demo-cluster-default-pool-XXXXXXXX-XXXX

pod 연결

이제 샘플 애플리케이션 pod를 Mcrouter의 기본 포트인 5000 포트의 각 상호 노드에서 실행되는 Mcrouter pod에 연결할 준비가 되었습니다.

  1. telnet 세션을 열어 pod 중 하나에 대한 연결을 시작합니다.

    POD=$(kubectl get pods -l app=sample-application -o jsonpath="{.items[0].metadata.name}")
    
    kubectl exec -it $POD -- sh -c 'telnet $NODE_NAME 5000'
    
  2. telnet 프롬프트에서 다음 명령어를 실행합니다.

    get anotherkey
    quit
    

    출력 결과는 다음과 같습니다.

    Mcrouter is fun

마지막으로 예시를 들면 다음 Python 코드는 환경에서 NODE_NAME 변수를 검색하고 pymemcache 라이브러리를 사용하여 이 연결을 수행하는 샘플 프로그램입니다.

import os
from pymemcache.client.base import Client

NODE_NAME = os.environ['NODE_NAME']
client = Client((NODE_NAME, 5000))
client.set('some_key', 'some_value')
result = client.get('some_key')

삭제

이 가이드에서 사용된 리소스 비용이 Google Cloud 계정에 청구되지 않도록 하려면 리소스가 포함된 프로젝트를 삭제하거나 프로젝트를 유지하고 개별 리소스를 삭제하세요.

  1. 다음 명령어를 실행하여 GKE 클러스터를 삭제합니다.

    gcloud container clusters delete demo-cluster --zone us-central1-f
    
  2. 필요한 경우 Helm 바이너리를 삭제합니다.

    cd ~
    rm -rf helm-v3.7.1
    rm helm-v3.7.1-linux-amd64.tar.gz
    

다음 단계

  • 단순한 연결 풀링 이외에 장애 조치 복제본, 안정적인 삭제 스트림, 콜드 캐시 준비, 다중 클러스터 브로드캐스트 등 Mcrouter가 제공하는 다양한 기능 살펴보기
  • Memcached 차트Mcrouter 차트의 소스 파일을 살펴보고 해당 Kubernetes 구성 자세히 알아보기
  • App Engine에서 Memcached를 사용하는 효과적인 기법 알아보기. 이 중 일부는 GKE 등의 기타 플랫폼에도 적용됩니다.