このチュートリアルでは、Kubernetes、Helm、Mcrouter を使用して Google Kubernetes Engine(GKE)に分散型 Memcached サーバー クラスタをデプロイする方法について学習します。Memcached は、よく使われているオープンソースの多目的キャッシュ システムです。通常は、ウェブ アプリケーションの処理速度を高めてデータベースの負荷を軽減するために、使用頻度の高いデータの一時的な保管場所として機能します。
Memcached の特長
Memcached には、2 つの主な設計目標があります。
- シンプル: Memcached は大きなハッシュ テーブルのように機能し、キーを使って任意の形式のオブジェクトを格納して取り出すシンプルな API を提供します。
- 速度: Memcached はキャッシュ データをランダム アクセス メモリ(RAM)に独占的に保存し、きわめて高速なデータアクセスを実現します。
Memcached は、ハッシュ テーブルの容量をサーバープール全体で水平スケーリングできるようにする分散型システムです。Memcached の各サーバーは、プール内の他のサーバーとは完全に独立して動作します。したがって、クライアント レベルでサーバー間のルーティングとロード バランシングを行う必要があります。Memcached クライアントではコンシステント ハッシュ スキームが適用され、ターゲット サーバーが適切に選択されます。このスキームでは、次の条件が保証されます。
- 同じキーに対して常に同じサーバーが選択されます。
- サーバー間のメモリ使用量が均等に分散されます。
- サーバープールが縮小または拡大されるとき、最小数のキーが再配置されます。
次の図は、Memcached クライアントと Memcached サーバーの分散されたプールとの間のやり取りの概要を示しています。
Memcached サービスのデプロイ
Memcached サービスを GKE にデプロイする簡単な方法の 1 つは、Helm チャートを使用することです。デプロイを進めるには、Cloud Shell で次の手順を行います。
- 3 つのノードからなる新しい GKE クラスタを作成します。 - gcloud container clusters create demo-cluster --num-nodes 3 --location us-central1-f
- helmバイナリ アーカイブをダウンロードします。- HELM_VERSION=3.7.1 cd ~ wget https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz
- アーカイブ ファイルをローカル システムに解凍します。 - mkdir helm-v${HELM_VERSION} tar zxfv helm-v${HELM_VERSION}-linux-amd64.tar.gz -C helm-v${HELM_VERSION}
- helmバイナリのディレクトリを- PATH環境変数に追加します。- export PATH="$(echo ~)/helm-v${HELM_VERSION}/linux-amd64:$PATH"- このコマンドは、現在の Cloud Shell セッション中に任意のディレクトリから - helmバイナリを検出可能にします。この構成を複数のセッションにわたって保持するには、このコマンドを Cloud Shell ユーザーの- ~/.bashrcファイルに追加します。
- 高可用性アーキテクチャで新しい 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 クライアントはサーバーを参照しやすくなります。
- 実行中の 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 アドレスが公開されるため、個別に検出できるようになります。
- デプロイされたサービスがヘッドレス サービスであることを確認します。 - 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 サービス エンドポイントの検出はクライアントが担います。 
- エンドポイントの 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.32、- 10.36.0.33、- 10.36.1.25を持っています。これらの IP アドレスは、実際のサーバー インスタンスごとに異なる場合があります。各 Pod は Memcached のデフォルトのポート- 11211をリッスンします。
- さらにステップ 2 の代わりに、Python などのプログラミング言語を使用して DNS 検査を行うこともできます。 - クラスタ内で Python のインタラクティブ コンソールを起動します。 - kubectl run -it --rm python --image=python:3.10-alpine --restart=Never python
- 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'])
 
- 実行中のいずれかの 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 
サービス ディスカバリ ロジックの実装
これで、次の図に示す基本的なサービス ディスカバリ ロジックを実装する準備が整いました。
大まかに言えば、サービス ディスカバリ ロジックは次の手順で構成されます。
- アプリケーションは kube-dnsに対してmycache-memcached.default.svc.cluster.localの DNS レコードのクエリを実行します。
- アプリケーションは、そのレコードに関連付けられている IP アドレスを取得します。
- アプリケーションは新しい Memcached クライアントをインスタンス化して、取得した IP アドレスで提供します。
- Memcached クライアントの統合ロードバランサは、指定された IP アドレスの Memcached サーバーに接続します。
このサービス ディスカバリ ロジックを Python を使用して実装します。
- 新しい Python 対応 Pod をクラスタにデプロイし、Pod 内で Shell セッションを開始します。 - kubectl run -it --rm python --image=python:3.10-alpine --restart=Never sh
- pymemcacheライブラリをインストールします。- pip install pymemcache
- pythonコマンドを実行して、Python のインタラクティブ コンソールを起動します。
- 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 のデータ保存形式がバイトリテラルであることを示しています。
- Python のコンソールを終了します。 - exit()
- Pod のシェル セッションを終了するには、 - Control+- Dを押します。
接続プールの有効化
キャッシングの需要が高まり、プールが数十、数百、数千もの Memcached サーバーまで拡大されると、いくつかの制限が発生する可能性があります。特に、次の図のように Memcached クライアントからの多数のオープン接続がサーバーに負荷をかける可能性があります。
オープン接続の数を減らすには、次の図のように接続プールを有効にするためにプロキシを導入する必要があります。
高度なオープンソースの Memcached プロキシである Mcrouter(「ミックルーター」と発音)は、接続プールを有効にします。標準の Memcached ASCII プロトコルが使用されているため、Mcrouter はシームレスに統合できます。Memcached クライアントに対して、Mcrouter は通常の Memcached サーバーのように動作します。Memcached サーバーに対して、Mcrouter は通常の Memcached クライアントのように動作します。
Mcrouter をデプロイするには、Cloud Shell で次のコマンドを実行します。
- 前にインストールした - mycacheHelm チャートのリリースを削除します。- helm delete mycache
- 新しい 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 に接続してこの設定をテストします。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 が 1 つ含まれていることを確認します。一般的な方法は、DaemonSet コントローラを使用してプロキシ Pod をデプロイすることです。ノードがクラスタに追加されると、そのノードに新しいプロキシ Pod が自動的に追加されます。ノードがクラスタから削除されると、Pod はガベージ コレクションされます。このチュートリアルで以前にデプロイした Mcrouter Helm チャートでは、デフォルトで DaemonSet コントローラを使用します。このステップはすでに完了しています。
- プロキシ コンテナの Kubernetes パラメータで hostPort値を設定することにより、ノードがそのポートをリッスンしてトラフィックをプロキシにリダイレクトするようにします。このチュートリアルの Mcrouter Helm チャートでは、デフォルトでこのパラメータをポート5000で使用しています。そのため、このステップもすでに完了しています。
- spec.envエントリを使用し- spec.nodeName- fieldRef値を選択し、アプリケーションの Pod 内の環境変数としてノード名を公開します。この方法の詳細については、Kubernetes のドキュメントをご覧ください。- サンプル アプリケーションの Pod をデプロイします。次のコマンドを実行すると、Kubernetes Deployment が適用されます。Deployment は、クラスタ内のノードに分散された Pod の複数のレプリカを実行できる Kubernetes API オブジェクトです。 - 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
 
- いずれかのサンプル アプリケーション 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 Pod に Mcrouter のデフォルト ポート 5000 で接続する準備ができました。
- いずれかの 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'
- 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')