MoTLab -GO Inc. Engineering Blog-MoTLab -GO Inc. Engineering Blog-

HAProxyサイドカーでCloud SQL Replicaの参照クエリ負荷分散

SREK8s
June 10, 2022

SREグループの古越です。

MoTのサービスは多くがAWSのAurora MySQLまたはPostgreSQLを利用しリーダーエンドポイントを利用し参照クエリを発行しています。一部のサービスはGoogle Cloud上で構成されており、Cloud SQLの参照クエリの負荷分散が課題としてありました。

EKSからCloud SQLに対する負荷分散アプローチを検討する機会があり検証したところ、HAProxyサイドカーとして横付けする方法が有用と分かりましたので本記事で紹介いたします。


はじめに

本記事執筆時点では実験的な内容となります。近日中にプロダクションに投入予定ですので、何かトラブルがあれば別途共有できればと思います。

構成イメージ

本記事で紹介する構成のイメージ図です。

前提として、EKSなどのGoogle Cloud以外のKubernetesクラスターから接続するケースを想定しています。

An image from Notion

Kubernetes上で構成したアプリケーションからCloud SQL Replicaへの経路のみ抜粋します。

アプリケーションの通信流入経路やCloud SQLのMaster接続経路については割愛します。

構成要素について

簡単に構成要素について解説します。

Cloud SQL

https://cloud.google.com/sql/docs/introduction?hl=ja

Google CloudマネージドのMySQLサービスです。

マネージドMySQLとしてはAmazon RDSが比較対象になりますが、RDSの場合は選択肢としてAurora MySQLが有ります。Aurora MySQLを基準に考えると機能が幾つか不足しています。

An image from Notion

管理面に課題が多いのですが、Google CloudのマネージドMySQLは選択肢が少ないため、仕方なくCloud SQLを選択するケースが多いかと思います。負荷分散出来る参照用エンドポイントが無い事が今回の構成を作る動機となっています。

Google CloudのマネージドDBとしてAuroraに対抗する形で2022年5月にAlloyDB for PostgreSQLがリリースされていますが、今後MySQL版が出て不足部分が解消される事を期待しています。

Cloud SQL Auth Proxy (Cloud SQL Proxy)

https://cloud.google.com/sql/docs/mysql/sql-proxy?hl=ja

コンテナで起動し、Cloud SQLに接続可能にするための認証プロキシです。Google Cloudサービスアカウントさえ有れば、Google Cloudの外部からCloud SQLに接続可能になります。

ポイント

  • ネットワーク上の境界を無視しつつも、セキュアな接続を実現する
  • k8s上で使う場合、サイドカー方式で横付けする方法が公式で推奨されている
  • 1コンテナで複数Replicaに対する接続ポイントを作る事が出来る
  • distrolessを使ったコンテナイメージがデフォルト提供 10MB未満に収まる
  • 認証プロキシに過ぎないため、複数レプリカに対して負荷分散する機能は持たない

HAProxy

http://www.haproxy.org/

初版リリース 2001/12/16 の古くから存在するOSSのTCPロードバランサです。

ポイント

  • Cで実装され、高い信頼性があり、高速で、多数の実績がある
  • 古くから有り成熟している
  • コンテナ化されている
    • イメージはDockerHubで提供
    • alpine版のコンテナイメージは10MB未満と軽量

2015年頃まではMySQL参照クエリの負荷分散アプローチとしてはReplica(MySQL Slave)を作成し、HAProxyなどのTCPロードバランサを利用して分散するアプローチが比較的有名でした。

2016年頃にAurora リーダーエンドポイントが登場した後はAuroraがマネージドMySQLとして決定的な地位を確立したことも有り、MySQL負荷分散としてHAProxyを活用する事例を殆ど見なくなりました。

今回Cloud SQLの負荷分散アプローチとして使えるか改めて調査した所、コンテナ化したアプリケーション上でも使える事が分かりました。

HAProxy導入動機

動機は負荷分散装置を入れて以下のメリットを得るためです。

  • アプリケーションのMySQL参照エンドポイントを一つにして実装コストを下げる
  • ヘルスチェック機能を使ってmysql接続の信頼性を高める

HAProxyを採用した決め手

HAProxy以外にも負荷分散装置としてはProxySQLやEnvoyを検討していました。

An image from Notion

当初ProxySQLが使えると考えていたのですが、小規模かつサイドカー構成するという前提にマッチしなかったため候補から外す事になりました。シンプルなTCPロードバランサで事が足りるためEnvoyも良さそうでしたが、L7ヘルスチェックの有無が決め手で今回はHAProxyを採用しています。

ロードバランサからのヘルスチェックについて

今回想定していたサービスではGoogle Cloud外部から接続するため、Cloud SQL Auth Proxyの利用が必須でした。

App → [loadbalancer] → Cloud SQL Auth Proxy →Cloud SQL

という多段の接続経路になります。

この多段経路の場合、ロードバランサからのTCPヘルスチェックだけではCloud SQL Auth Proxyの裏に有るCloud SQL障害を十分に検知出来ないと想定しています。

構成方法

  1. Docker Composeを使い、localマシンからCloud SQLへ接続する方法
  2. KubernetesクラスターからCloud SQLに接続する方法

の2パターンを紹介いたします。

準備

以下は既に準備出来ている前提で記載します。

  • Cloud SQL Auth Proxy用 IAMサービスアカウント
  • アプリケーション用 Cloud SQL MySQLユーザ

今回は詳しく触れませんが、実際にお試し頂く場合は別途準備いただければと思います。

共通: HAProxy ヘルスチェックユーザの準備

準備として、HAProxyからMySQLに対するヘルスチェックを行うユーザを作成します。

今回はDocker Compose, Kubernetes共用のヘルスチェックユーザの例を記載します。

ユーザ作成コマンド

CREATE USER 'healthcheck'@'cloudsqlproxy~%' WITH MAX_QUERIES_PER_HOUR 1 MAX_UPDATES_PER_HOUR 0;

ヘルスチェックユーザからはクエリを発行する必要が無いため、GRANTなどの設定は不要です。念の為、 MAX_QUERIES_PER_HOUR, MAX_UPDATES_PER_HOURなどのパラメータでクエリ発行出来ないよう絞っておくと良いかも知れません。

ヘルスチェックユーザについての細かい説明はHAProxyのドキュメントが参考になります。

参考: https://www.haproxy.com/documentation/hapee/latest/onepage/#4.2-option%20mysql-check

Docker Compose版

An image from Notion

開発用マシンと開発用のCloud SQL Replicaが存在する前提になりますが、Kubernetesに乗せる前の確認として利用出来るので紹介いたします。

アプリケーションコンテナの内容は割愛しますが、アプリケーションから localhost:3306 として参照出来るようになります。

docker-compose.yml 例

version: '3'
services:
  # ----------
  # CloudSQLAuthProxy
  # ----------
  database:
    image: "gcr.io/cloudsql-docker/gce-proxy:1.30.0"
    environment:
      - GOOGLE_APPLICATION_CREDENTIALS=/var/run/secret/cloud.google.com/service-account-key.json
    command:
      ["/cloud_sql_proxy", "-instances=your-project:asia-northeast1:db-replica-001=tcp:0.0.0.0:13301,your-project:asia-northeast1:db-replica-002=tcp:0.0.0.0:13302,your-project:asia-northeast1:db-replica-003=tcp:0.0.0.0:13303"]
    volumes:
      - "./secrets:/var/run/secret/cloud.google.com"
    ports:
      - "13301-13303:13301-13303"
  # ----------
  # HAProxy
  # ----------
  haproxy:
    image: "haproxy:2.5-alpine"
    ports:
      - "8404:8404"
      - "3306:3306"
    volumes:
      - "./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg"

Cloud SQL Auth Proxyのサービスアカウントキー

docker-compose.ymlファイルの相対pathとして 以下に配置する想定です。

./secrets/service-account-key.json

複数レプリカの登録

cloud_sql_proxy の -instances 引数にCloud SQLインスタンスを書きますが、カンマ区切りで繋げて書くと複数個のCloudSQL Replicaを登録する事が出来ます。

登録したレプリカの数だけ待受ポートが作られるのでHAProxyで繋げていきます。

haproxy.cfg 例

global
    log stdout format raw local0

defaults
    log global
    retries 3
    timeout connect 10s
    timeout client 1m
    timeout server 1m
    timeout check 10s
    maxconn 3000

listen mysql
    bind *:3306
    mode tcp
    option mysql-check user healthcheck
    balance roundrobin
    server replica-001 database:13301 check port 13301
    server replica-002 database:13302 check port 13302
    server replica-003 database:13303 check port 13303

listen stats
   bind *:8404
   mode http
   maxconn 10
   timeout queue 100s
   stats enable
   stats refresh 30s
   stats show-node
   stats uri /stats

コンテナ間通信の補足

docker-composeの場合、コンテナ間通信に少し注意が必要でdocker-composeの名前解決に委ねる形が好ましいです。

上記の database:13301 と記載されている所は、HAProxy → Cloud SQL Auth Proxyの通信部分になります。Kubernetesの場合は loopback IPで 127.0.0.1:13301 と記載しますが、docker-composeの名前解決を利用するため docker-compose.ymlに記載したservice名 database を指定して設定しています。

stats 閲覧

docker-compose版は開発や動作確認を目的としているので、ヘルスチェックの挙動確認するためにstatsを有効化しています。

docker-compose起動後にブラウザで http://localhost:8404/stats を開くと以下の統計情報を見ることが出来ます。

An image from Notion

Docker Compose版の紹介としては以上です。

Kubernetes版

An image from Notion

Docker Composeと大きく変わらないものの、不要なものは削っています。

アプリケーションコンテナから 127.0.0.1:3306 で参照エンドポイントを構成出来るよう構成します。設定方法のサンプルとして、deploymentsとHAProxyのconfigmapを記載します。

deployments例

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: app
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        # アプリケーション設定は割愛
      - name: cloudsqlproxy
        args:
        - /cloud_sql_proxy
        - -instances=your-project:asia-northeast1:db-replica-001=tcp:13301,your-project:asia-northeast1:db-replica-002=tcp:13302,your-project:asia-northeast1:db-replica-003=tcp:13303
        envFrom:
        - secretRef:
            name: cloudsql-env
        image: gcr.io/cloudsql-docker/gce-proxy:1.30.0
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - mountPath: /var/run/secret/cloud.google.com
          name: gcp-secrets
      - name: haproxy
        image: haproxy:2.5-alpine
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - mountPath: /usr/local/etc/haproxy
          name: haproxy-config
      volumes:
      - name: gcp-secrets
        secret:
          defaultMode: 420
          secretName: myapp-gcp-secrets
      - configMap:
          defaultMode: 420
          name: myapp-haproxy-config
        name: haproxy-config

Cloud SQLAuth ProxyのサービスアカウントはsecretからvolumeMountで設定します。

HAProxyの設定は後述しますが、configmapからvolumeMountで読み込みます。

コンテナ間通信、リッスン範囲

cloud_sql_proxy コマンド引数の -instances で指定している箇所ですが、docker composeではコンテナ間通信のために以下の設定をしていました

=tcp:0.0.0.0:13301

cloud_sql_proxyの起動オプションについて細かい挙動はドキュメントに記載がありますが、TCPポートを利用する場合のデフォルトでは localhost としてリッスンします。Kubernetesでサイドカー構成する場合はpod内に閉じている方が好ましいため以下のように記載しています。

=tcp:13301

configmap例

HAProxyの設定はconfigmapで設定します。

apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app: myapp
  name: myapp-haproxy-config
data:
  haproxy.cfg: |
    global
        log stdout format raw local0

    defaults
        log global
        retries 3
        timeout connect 10s
        timeout client 1m
        timeout server 1m
        timeout check 10s
        maxconn 3000

    listen mysql
        bind 127.0.0.1:3306
        mode tcp
        option mysql-check user healthcheck
        balance roundrobin
        server replica-001 127.0.0.1:13301 check port 13301
        server replica-002 127.0.0.1:13302 check port 13302
        server replica-003 127.0.0.1:13303 check port 13303

ポイントとしては以下3点です

  • ログを標準出力に出す
  • listen範囲を最小限にする
  • stats無効化

docker-composeでも標準出力に出す設定を入れていますが、Kubernetesの場合も同様です。fluentbitなど任意のログコレクターでログを拾わせる事が出来るので、標準出力に出しています。

サイドカー構成の場合はpod外からアクセスする必要が無いため、listen範囲は最小限に削る事が出来ます。Cloud SQL Auth Proxyもpod内部通信のみlistenする設定を入れているので、loopback ip(localhost)を指定して接続する形になります。

ログ以外にモニタリングする目的が現時点では無いためstatsは無効化する事にしています。

おわりに

HAProxyとCloud SQL Auth Proxyをサイドカーとして構成する例を紹介いたしました。厳密な性能評価は実施していませんが、pod内通信は1台の物理マシンで完結しているため、実質的なネットワークオーバーヘッドは極小さいものと考えています。

古くから存在するHAProxyを掘り起こす形で検証しましたが、枯れきったプロダクトではなく時代に合わせて変化している事が分かり、テクノロジーの温故知新と言える一例と感じました。

DB参照アクセスの負荷分散手法の一例として、参考になれば幸いです。


We're Hiring!

📢
Mobility Technologies ではともに働くエンジニアを募集しています。

興味のある方は 採用ページ も見ていただけると嬉しいです。

Twitter @mot_techtalk のフォローもよろしくお願いします!