GKE での Memcached のデプロイ


このチュートリアルでは、KubernetesHelmMcrouter を使用して Google Kubernetes Engine(GKE)に分散型 Memcached サーバー クラスタをデプロイする方法について学習します。Memcached は、よく使われているオープンソースの多目的キャッシュ システムです。通常は、ウェブ アプリケーションの処理速度を高めてデータベースの負荷を軽減するために、使用頻度の高いデータの一時的な保管場所として機能します。

Memcached の特長

Memcached には、2 つの主な設計目標があります。

  • シンプル: Memcached は大きなハッシュ テーブルのように機能し、キーを使用して任意の形式のオブジェクトを格納して取り出すシンプルな API を提供する。
  • 速度: Memcached はキャッシュ データをランダム アクセスメモリ(RAM)にのみ保持して、きわめて高速なデータアクセスを実現する。

Memcached は、ハッシュ テーブルの容量をサーバープール全体で水平スケーリングできるようにする分散型システムです。Memcached の各サーバーは、プール内の他のサーバーとは完全に独立して動作します。したがって、クライアント レベルでサーバー間のルーティングと負荷分散を行う必要があります。Memcached クライアントには、ターゲット サーバーを適切に選択するためにコンシステント ハッシュ スキームが適用されています。このスキームでは、次の条件が保証されます。

  • 同じキーに対して常に同じサーバーが選択されます。
  • サーバー間のメモリ使用量が均等に分散されます。
  • サーバープールが縮小または拡大されるとき、最小数のキーが再配置されます。

次の図は、Memcached クライアントと Memcached サーバーの分散されたプールとの間のやり取りの概要を示しています。

Memcached と Memcached サーバープール間のやり取り
図 1: Memcached クライアントと Memcached サーバーの分散されたプールとの間のやり取りの概要

目標

  • Memcached の分散型アーキテクチャの特徴について学習します。
  • Kubernetes と Helm を使用して、Memcached サービスを GKE にデプロイします。
  • オープンソースの 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. Compute Engine and GKE API を有効にします。

    API を有効にする

  5. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタに移動

  6. Google Cloud プロジェクトで課金が有効になっていることを確認します

  7. Compute Engine and GKE API を有効にします。

    API を有効にする

  8. Cloud Shell インスタンスを起動します。
    Cloud Shell を開く

Memcached サービスのデプロイ

Memcached サービスを GKE にデプロイする簡単な方法の 1 つは、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. helm バイナリのディレクトリを PATH 環境変数に追加します。

    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 コントローラを使用するメリットの 1 つは、Pod の名前が順序付けられて予測可能になることです。この場合、名前は mycache-memcached-{0..2} になります。この順序付けによって、Memcached クライアントはサーバーを参照しやすくなります。

  6. 実行中の Pod を表示するには、次のコマンドを実行します。

    kubectl get pods
    

    Google Cloud コンソールの出力は次のようになります。

    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 チャートでは、ヘッドレス サービスが使用されています。ヘッドレス サービスでは、すべての Pod の IP アドレスが公開されるため、個別に検出できるようになります。

  1. デプロイされたサービスがヘッドレス サービスであることを確認します。

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

    None という出力により、サービスに clusterIP がないこと、したがってヘッドレスであることを確認できます。

    このサービスでは、次の形式でホスト名の DNS レコードが作成されます。

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

    このチュートリアルでは、サービス名は mycache-memcached になります。Namespace が明示的に定義されていないため、デフォルトの Namespace が使用され、ホスト名全体は mycache-memcached.default.svc.cluster.local になります。このホスト名は、サービスによって公開された 3 つの 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 はそれぞれ、個別の IP アドレス 10.36.0.3210.36.0.3310.36.1.25 を持っています。これらの 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. 実行中のいずれかの Memcached サーバーとの telnet セッションをポート 11211 で開き、デプロイのテストを行います。

    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. アプリケーションは kube-dns に対して mycache-memcached.default.svc.cluster.local の DNS レコードのクエリを実行します。
  2. アプリケーションは、そのレコードに関連付けられている IP アドレスを取得します。
  3. アプリケーションは新しい Memcached クライアントをインスタンス化して、取得した IP アドレスで提供します。
  4. Memcached クライアントの統合ロードバランサは、指定された IP アドレスの Memcached サーバーに接続します。

このサービス ディスカバリ ロジックを Python を使用して実装します。

  1. 新しい Python 対応 Pod をクラスタにデプロイし、Pod 内で Shell セッションを開始します。

    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. Pod のシェル セッションを終了するには、Control+D を押します。

接続プールの有効化

キャッシングの需要が高まり、プールが数十、数百、数千もの Memcached サーバーまで拡大されると、いくつかの制限が発生する可能性があります。特に、次の図のように Memcached クライアントからの多数のオープン接続がサーバーに負荷をかける可能性があります。

すべての Memcached クライアントがすべての Memcached サーバーに直接アクセスする場合の多数のオープン接続
図 3: すべての Memcached クライアントがすべての Memcached サーバーに直接アクセスする場合の多数のオープン接続

オープン接続の数を減らすには、次の図のように接続プールを有効にするためにプロキシを導入する必要があります。

接続プールを有効にするプロキシ
図 4: プロキシを使用してオープン接続の数を減らす

高度なオープンソースの Memcached プロキシである Mcrouter(「ミックルーター」と発音)は、接続プールを有効にします。標準の Memcached ASCII プロトコルが使用されているため、Mcrouter はシームレスに統合できます。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
    

    これで、プロキシポッドでクライアント アプリケーションからのリクエストを受け入れる準備が整いました。

  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 ノードのクラスタを使用しています。ただし、複数ノードの使用によってノード間のネットワーク トラフィックが増加するため、レイテンシが増加するリスクもあります。

プロキシポッドを同じ場所に配置する

クライアント アプリケーションのポッドを同じノード上にある Memcached プロキシポッドにのみ接続することで、このリスクを軽減できます。次の図は、この構成を示しています。

Pod 間のやり取りのトポロジ
図 5: 3 ノードのクラスタにまたがるアプリケーション Pod、Mcrouter Pod、Memcached Pod 間のやり取りのトポロジ

この構成は次のように行います。

  1. 各ノードに実行中のプロキシ Pod が 1 つ含まれていることを確認します。一般的な方法は、DaemonSet コントローラを使用してプロキシ Pod をデプロイすることです。ノードがクラスタに追加されると、そのノードに新しいプロキシ Pod が自動的に追加されます。ノードがクラスタから削除されると、Pod はガベージ コレクションされます。このチュートリアルで以前にデプロイした Mcrouter Helm チャートでは、デフォルトで DaemonSet コントローラが使用されているため、このステップはすでに完了しています。
  2. プロキシ コンテナの Kubernetes パラメータで hostPort 値を設定することにより、ノードがそのポートをリッスンしてトラフィックをプロキシにリダイレクトするようにします。このチュートリアルの Mcrouter Helm チャートでは、デフォルトでこのパラメータをポート 5000 で使用しています。そのため、このステップもすでに完了しています。
  3. spec.env エントリを使用し spec.nodeName fieldRef 値を選択し、アプリケーションの Pod 内の環境変数としてノード名を公開します。この方法の詳細については、Kubernetes のドキュメントをご覧ください。

    1. いくつかのアプリケーションのサンプルポッドをデプロイします。

      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=$(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 Pod に Mcrouter のデフォルト ポート 5000 で接続する準備ができました。

  1. いずれかの Pod の接続を開始するには、telnet セッションを開きます。

    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 が提供するその他の多くの機能を確認する。
  • それぞれの Kubernetes の構成について、Memcached チャートMcrouter チャートのソースファイルを確認する。
  • App Engine で Memcached を使用する効果的なテクニックについてのページを読む。このテクニックの一部は、GKE など他のプラットフォームに適用できます。