Kubernetes CronJobを使ったクラウドSQL Databaseの監視と運用

f:id:vasilyjp:20190612190746p:plain

こんにちは。ZOZOテクノロジーズ リプレイスチームの杉山です。
本記事では、ZOZOTOWNリプレースで行っている「マルチクラウド環境への移行」を目指したデータベースの監視システムを「Kubernetes CronJob」と、監視SaaS「Datadog」を使用して構築した事例をご紹介したいと思います。

マルチクラウドを見据えた設計と監視システムの構築


弊社のリプレースプロジェクトでは、マルチクラウド環境構築での運用を目標としているため、データベースの監視システムも各クラウドベンダー(Azure・AWS、GCPの3クラウドベンダーということで、以下、3Cといいます)のリソースを横断的に監視できる必要があります。 各ベンダーでは、そのクラウドサービスのリソースを監視する機能が提供されています。 しかし、各々のツールを使用して監視運用をすると運用の手間がかかるため、これらを一元管理する必要があります。
設計の段階からマルチクラウドでの使用を見据えて、監視システムを設計しました。

マルチクラウド監視を実現できるサービスを選択


標準ツールでは難しいマルチクラウド監視の実現には、共通で提供しているサービスの選択や、外部の監視SaaS・Slackなどとの連携が不可欠です。 また、システムのメンテナンスコストも考慮して設計する必要があります。 そこで、共通で提供しているサービスリソースから、以下のようなサービスを使用することを選択しました。

<プラットフォーム>
Kubernetes(AKS、EKS、GKE)

<コンテナエンジン>
Docker(Kubernetesがサポートしている)

<ジョブスケジューラー>
Kubernetes CronJob(Kubernetesリソース)(以下、本文中はCronJobという)

<データベース>
SQL Server(Azure SQL Server、Amazon RDS for SQL Server、GCPのSQL Server)
※GCPではまだGAされていないが将来的にGAされることを見据えて。
※SQL Serverを選んだ理由には、会社的な理由もありました。

<監視SaaS>
Datadog
- 3C各社のIntegrationsに対応
- カスタムメトリクスの送信にも対応

<通知>
Slack

<オンコール>
PagerDuty

f:id:vasilyjp:20190611101236p:plain

監視システムの要件


監視システムの要件としては、以下の内容があげられます。

  • 安定した定期実行と定期的なメトリクス取得
  • コンテナイメージを3Cそれぞれに最適化せず汎用的に使う
  • SQL Serverのジョブやレプリケーションの遅延状態などのカスタムメトリクス取得
  • 台数増減に合わせた稼働しているデータベースのカスタムメトリクスを動的に取得

これらを考慮したAzureでの構成は以下のようになります。

f:id:vasilyjp:20190611100724p:plain

具体的な設計やポイントを、各フェーズに分けてご説明します。

「Kubernetes CronJob」を選択した理由


以前は、Kubernetes上ではありますが、Linuxのcronを利用して運用していました。 当初は特に問題なく稼働していましたが、監視対象の数が数十台になって来た時に「Can not allocate memory」のエラーがぽつぽつと出るようになりました。 APIのチューニング、並列化、drop_cache設定、flockでの多重実行の制御、ログ回りの最適化などの対策などを講じましたが、根本的に監視システムのパフォーマンスを向上させるためには、新たな定期実行のシステムを設計する必要があると考えるようになりました。

弊社では、アプリケーションの運用にすでにKubernetesを使用していました。 運用コストも考えるとKubernetesでの運用が良いと判断し、Kubernetesの定期実行リソースである「CronJob」を選択しました。

定期実行を実現する「Kubernetes CronJob」の機能


CronJobは、cronと同じようにスケジュール設定に基づいて、定期実行を実現するリソースです。 cronにはない様々な便利な機能が使用できます。

「Kubernetes CronJob」の特長

CronJobには、以下のような特長があります。

  • スケジュール設定に基づいて、一度だけ実行される処理を実装したコンテナを定期的に起動する。
  • 処理が完了したコンテナは破棄される。
  • 毎回、新規コンテナが起動するため不要なキャッシュなどがコンテナ内に溜まらない。
  • 実行失敗時のリトライ制御・生存可能な時間設定・実行開始の遅延許容の設定・多重実行時の制御などの有用な機能がある。

以下は、CronJobの各種機能のマニフェストサンプルです。

apiVersion: batch/v1beta1
kind: CronJob
metadata:
 name: my-cronjob
spec:
 concurrencyPolicy: Allow      # Allow(同時実行許可) / Forbid(スキップ) / Replace(キャンセルして入れ替え)
 schedule: "*/1 * * * *"       #1分毎 cronのスケジュールと同じ設定方法 
 startingDeadlineSeconds: 30   #30秒 Jobのスケジュール遅延許容
 failedJobsHistoryLimit: 1     #異常終了したJobの履歴保有数
 successfulJobsHistoryLimit: 3 #正常終了したJobの履歴保有数
 suspend: false                # スケジューリングの対象とするかどうか trueでスケジュール対象外
 jobTemplate:
  spec:
        completions: 1     #正常終了とするJOBの完了回数
        parallelism: 1     #Jobで同時にPodを実行できる並列数
        backoffLimit: 2    #何らかの理由で失敗したJobのリトライ回数
        activeDeadlineSeconds: 300     #300秒 Jobの生存可能な制限時間を秒数で指定する
        template:
    以下にはJobのテンプレート設定を記述。

コンテナイメージを汎用的に使う


作成する各コンテナイメージを3Cで汎用的に使えるようにするためには、環境変数を利用します。

Kubernetesでは、マニフェストと呼ばれる設定ファイルに「ConfigMap(環境変数)」や「Secret(秘匿情報)」を指定して起動することで、コンテナに環境変数を注入できます。 コンテナ内のアプリケーションは、環境変数を参照して稼働させることで、同一イメージから稼働設定を変えたコンテナが起動できます。

f:id:vasilyjp:20190611093530p:plain

例えば、使用するクラウドが変わったとしても、対象クラウドに合わせたマニフェストを作成しapplyすることで3Cそれぞれに対応することが可能です。
(もちろん、適切なネットワーク設定やアクセス権限が必要です)。

以下は、ConfigMap(環境変数)とSecret(秘匿情報)のマニフェストファイルの例です。

apiVersion: v1
kind: ConfigMap
metadata:
    name: my-configmap-common
data:
   YOUR_ENV: <YOUR_ENV>
---
apiVersion: v1
kind: Secret
metadata:
    name: my-secret-common
data:
   YOUR_ENV_SECRET: <YOUR_ENV_SECRET_BASE64_ENCORDING>
type: Opaque

「Kubernetes CronJob」で利用する各コンテナ設計のポイント


Kubernetesでは、適切にコンテナの役割を分けた、コンテナ・デザイン・パターンを考える必要があります。
例えば、サイドカーパターン、アンバサダパターン、アダプタパターンなどのパターンがあります。

今回構築した監視システムのコンテナは、次の2つとなるため、アンバサダパターンで進めました。

Jobコンテナ:
APIにメトリクス情報取得をリクエストし、Datadogにメトリクスを送信する。

APIコンテナ:
対象クラスタとデータベースを特定し、必要な処理を実行し各種メトリクス情報を生成する。

役割と目的から考えると、アンバサダパターンで問題無いように思われます。 しかし、この構成では以下のような挙動となり、Jobが完了(Completed)しません。

f:id:vasilyjp:20190611093525p:plain

CronJobで使用する「Job」は先述した通り「1度だけ実行し、終了したら破棄される」という仕様となっています。 そのため、同一Pod内にJobとしてAPIのような「常時待ち受け」をするコンテナがいると、Jobが完了しません。

NAME                               READY   STATUS      RESTARTS   AGE
pod/my-monitor-cronjob-xxxxxx      2/2     Running     0          32s
 ↓
NAME                               READY   STATUS      RESTARTS   AGE
pod/my-monitor-cronjob-xxxxxx      1/2     Running     0          52s

このため、APIコンテナはdeploymentに変更します。 この場合、JobとAPIの通信ではPodが違うため、ポート番号での通信ができません。
そのため「my-monitor-service」として「ClusterIP」を作成し、名前解決で通信できるようにします。

f:id:vasilyjp:20190611093528p:plain

これにより、Jobコンテナは「my-monitor-service:<port番号>」でAPIへアクセスできるようになり、 Jobが完了します。

NAME                               READY   STATUS      RESTARTS   AGE
pod/my-monitor-cronjob-xxxxxx      1/1     Running     0          14s
pod/my-monitor-deployment-xxx      1/1     Running     0          21h
 ↓
NAME                               READY   STATUS      RESTARTS   AGE
pod/my-monitor-cronjob-xxxxxx      0/1     Completed   0          37s
pod/my-monitor-deployment-xxx      1/1     Running     0          21h

動的に対象クラスター内に存在するデータベースを探す


APIコンテナでは、クラスター内に存在しているDBのタグ情報などを動的に取得するため、クラウドが提供しているREST APIを使用しています。 これにより、対象クラスター内のデータベースの台数が変わった場合にも動的にDMV情報の取得が可能になります。

f:id:vasilyjp:20190611093520p:plain

【ポイント】
ターゲットの台数と監視間隔によっては、このREST APIのリクエスト制限を超過することがありました。
この制限は、Azureであれば「30分に700回のリクエスト」という制限で、ベンダーへ緩和対応を要求しましたが制限解除や緩和ができませんでした。
データベースのタグ情報取得の部分は頻繁に変わるものではありませんが、負荷に応じたスケールアウト時などは可能な限りリアルタイムで情報を取得する必要があります。

対策として、許容できるTTLでキャッシュを使用して制限内に収めています。
この点は、1実行で破棄されてしまうJobコンテナではできず、deploymentにしたことで可能になった対応です。
(別でキャッシュサーバーを用意すればJobコンテナからもキャッシュを使用する事は可能です)

各種コンテナのOSイメージ


本システムでは、使用するコンテナのOSにミドルウェアなどをインストールしたイメージを別途作成し、コンテナリポジトリに保管して使っています。
設計時の必要なミドルウェアがインストールされた状態のimmutableなOSイメージを保持しておくことは、コンテナでのシステム運用においては「immutable inflastructure」の観点からも、とても重要です。 理由としては、次のような事があげられます。

  • ミドルウェアやドライバーが予期せずバージョンアップされてしまうことを防ぐ。
  • 必須のドライバーなどが、何らかの理由で公開が停止になった場合の対策。

実際に、メンテナンス時にSQL Serverのドライバーのインストール方法が変わったことによる影響で、ビルドができなくなってしまったことがありました。
急遽対応を入れて事なきを得ましたが、ミドルウェア構成の変更は緊急対応ではなく検証の時間を取って行いたいものです。
オフィシャルのイメージだったとしても、突然削除される可能性がないとは言えません。

具体的なDockerfileのコードは割愛しますが、今回は、Jobコンテナ・APIコンテナの両方で使用するミドルウェアを全てインストールしたコンテナイメージを作成します。

Jobコンテナ


CronJobで使用するJobは「1回だけ実行される処理」をコンテナ化します。
Jobコンテナは、次の2つの処理を1度だけ実行するように設計します。

  • メトリクス取得をAPIにリクエスト。
  • 取得したメトリクスをDatadogに送信する。

稼働設定を変更する情報は、環境変数で注入します。

f:id:vasilyjp:20190611100729p:plain

Datadogへのメトリクス送信は、公式のDatadog APIを使用しています。
API Referenceページにもサンプルコードがありますので、Pythonで実装します。

以下は、Datadog APIでTagsのカスタムメトリクスを送信するコードサンプルです。

# -*- coding:utf-8 -*-
import os
import threading
import requests
import json
from datadog import initialize
from datadog import api


print("monitoring start")

# env datadog
# 環境変数から秘匿情報を読み込み適用します。
dd_api_key = os.environ.get('DATADOG_API_KEY')
dd_app_key = os.environ.get('DATADOG_APP_KEY')
options = {
    'api_key':dd_api_key,
    'app_key':dd_app_key
}
initialize(**options)


def main():

    #APIへ渡すパラメーターを環境変数から読み込む
    target = os.environ.get("TARGET_CLOUD")
    param = os.environ.get("DATABASE_CLUSTER")

    #API情報
    host = 'my-monitor-service'
    version = '/api/v1'
    url = 'http://my-monitor-service:80/monitor/<target>?param=<param>'
    domain = 'my-monitor.jp'

    #JSONパース
    result = requests.get(url)
    jsonDic = json.loads(result.text)

    #整形と送信
    if result.status_code == 200:
        for object in jsonDic

            #----
            #APIからのレスポンスJSONをもとにリクエストを組み立てる
            #    ~
            #例:
            #metricName = 'メトリクス名'
            #value = '値'
            #domain = 'ドメイン'
            #tags = 'タグ情報の配列' 
            #など
            #----

            #スレッドでDatadogへ並列送信
            process = threading.Thread(target=metricToDatadog,args=(metricName, float(value), domain, tags))
            process.start()

#メトリクス送信ファンクション
def metricToDatadog(metricName, value, host, tags):
    api.Metric.send(metric=metricName, points=value, host=host, tags=tags)

# monitor run
main()

print("monitoring end")

【Jobコンテナのポイント】
APIのURLは、Kubernetesのservice(ClusterIP)で使用する名前解決の名称と同じ「my-monitor-service」にします。 作成したOSイメージを使用して、モニタリング実行用のコードと、コマンドのシェルを実装したイメージを作成します。

【Jobコンテナのログについて】
ログの出力先は、必要に応じて設定してください。
Jobコンテナは1実行で破棄されるので、ログの出力を残したい場合には標準出力をノードロギングエージェントを使うなどして、外部サービスなどに保管してもよいでしょう。

APIコンテナ


APIコンテナは、ご自身の好きな言語で開発していただければと思います。
今回は、Linux・Apache・PHP・Lumen・Swaggerで作成しています。

APIコンテナは、Jobからのリクエストを待ち受けるため、deploymentとしてコンテナ化します。

次の3つの処理を行うように設計します。

  • クラウドのREST APIを使用しデータベース情報を取得。
  • ターゲットDBにクエリを発行。
  • クエリ実行結果からタグ情報を生成しJSONを返却。

秘匿情報は必要に応じて環境変数で注入します。

Lumenフレームワークや、API仕様を定義するSwagger(OpenAPI Specification)については割愛します。

f:id:vasilyjp:20190611100727p:plain

このAPIコンテナには、必要な情報を取得するSQLファイルを、yamlファイルでファイル別に格納しています。 APIは、このSQLファイルの格納されているディレクトリ内にあるクエリを条件に応じて実行するように設計しています。 クエリを追加したい場合は、指定フォルダ内にルールに従ってファイルを追加することで自動的に実行クエリが追加されます。

以下は、yamlで作成したSQLファイルの例です。 メトリクス名、対象データベース、SQLなど、必要な情報を適宜記載しています。

metrics_name: sql_server.custom.database_job
database: 'xxxxx'
run_type: sqlserver
monitor_sql: |

   ここに情報取得のクエリを記載する

こちらも作成したOSイメージを使用して、APIコンテナのイメージを作成します。

なぜ、Jobにデータベースのメトリクス取得の処理をさせずAPIを使うの?


大きな理由として以下の2点があげられます。

1:コンテナ最適化(コンテナの役割は、あまり大きくせず小さくする方がよい)
Jobコンテナは可能な限り軽快に立ち上がり、素早く破棄されるように軽く動くような設計にする必要があります。
そのため、比較的処理の重い「メトリクス情報の生成」部分はAPIとして、コンテナ化しています。

2:REST APIリクエスト回数制限
「動的に対象クラスター内に存在するデータベースを探す」でポイントとしてあげたとおり、外部のAPIを使用する場合は、制限も考慮に入れる必要があります。
今回はAPIを別にdeploymentとしたことで、解決していることも1つの要因です。

以上のように、稼働設定などの必要な情報をパラメータや環境変数として注入するように設計・実装することで、どのクラウドでも対応できるようにしています。

クラウドリソース情報を取得するAPIは、各クラウドのAPI仕様に合わせてAPIエンドポイントを作る必要はありますが、イメージが1つで汎用的であることで運用管理コストは格段に下がります。

ローカル確認でのコンテナの連携方法


CronJobで定期実行させるという点を除いて、JobコンテナとAPIコンテナの連携とメトリクスが送信できていることを確認できれば、コンテナテストの目的は達成できます。
ここでは、本番Kubernetesでの設定と、それに近い形での動作確認を「minikube」などを使わずに確認するdocker-composeのテクニックのお話です。

今回はdocker-composeを利用しています。

テスト環境は以下のような構成でコンテナを起動します。

f:id:vasilyjp:20190611093514p:plain

これを実現するdocker-compose設定のサンプルを以下に記載します。
なお、APIの仕様作成やAPIのテストを簡単にする「swagger-editor」と「swagger-ui」も一緒に起動しています。
後述するポイントで軽くお話しします。  

version: "2"
services:
  my-monitor:
    build: ./job
    links:
      - my-monitor-service
    environment:
      - 実行に必要なパラメーターなどの環境変数
   #1回実行を確認する場合
   #command: ["/bin/bash", "-c", "/bin/docker-entrypoint.sh"] #※1
    #ローカルでdocker-composeでコンテナに入って確認する場合
    command: ["/usr/sbin/httpd","-DFOREGROUND"] #※2

  my-monitor-service:
    build: ./api
    ports:
      - “8888:80” #swagger-ui用
    environment:
      - 実行に必要なパラメーターなどの環境変数
    command: ["/usr/sbin/httpd","-DFOREGROUND"]

  swagger-editor:
    image: swaggerapi/swagger-editor
    container_name: "swagger-editor"
    ports:
      - "8881:8080"

  swagger-ui:
    image: swaggerapi/swagger-ui
    container_name: "swagger-ui"
    ports:
      - "8882:8080"
    environment:
      API_URL: "http://localhost:8888/api-docs"

以下のコマンドで、コンテナを起動します。

docker-compose up -d

コンテナの起動状態を確認します。

CONTAINER ID     IMAGE                        COMMAND                  CREATED          STATUS         PORTS                            NAMES
15d8baa91469     my-monitor                   "/usr/sbin/httpd -DF…"   4 hours ago      Up 4 hours     80/tcp                           my-monitor_1
ff2ac4b06899     my-monitor-service           "/usr/sbin/httpd -DF…"   4 hours ago      Up 4 hours     0.0.0.0:8888->80/tcp             my-monitor-service_1
70178c24d238     swaggerapi/swagger-editor    "sh /usr/share/nginx…"   4 hours ago      Up 4 hours     0.0.0.0:8881->8080/tcp           swagger-editor
3ebbecf86616     swaggerapi/swagger-ui        "sh /usr/share/nginx…"   4 hours ago      Up 4 hours     80/tcp, 0.0.0.0:8882->8080/tcp   swagger-ui

【ポイント】
docker-composeでのポイントを記載します。

<Links>
Jobコンテナ「my-monitor」を、APIコンテナのサービス名「my-monitor-service」にLinksで連携させる。これにより、Jobコンテナから「my-monitor-service」の名前解決でAPIを呼び出しできるようになります。
本番のKubernetesでも、Jobは同様の名前解決でAPIを呼び出します。

<swagger-editor>
「localhost:8881」swagger-editorでswaggerファイルを編集できます。
「Conver and save as JSON」swagger-ui用のJSONを書き出しできます。
「Generate Server」「Generate Client」様々な言語の実装コードに書き出しできます。

<swagger-ui>
API仕様書としてSwaggerを使用しておりますので、モックサーバー(localhost:8882)を利用してAPIをテストできます。
簡単にAPIテストができるので大変便利です。

<APIの確認方法>
「localhost:8882」swagger-uiでAPIをテストできます。

<Jobの確認>
こちらの確認方法は2つあります。

1.docker-composeのコードの※1を有効化する場合
Jobコンテナは、起動すると「/bin/docker-entrypoint.sh」を実行し、完了すると終了します。
成功している場合は、DatadogのMetricsExplorerでメトリクスを確認できます。

2.docker-composeのコードの※2を有効化する場合
コンテナにbashで入り、実行予定の「/bin/docker-entrypoint.sh」を手動で実行します。
実行後、ログを確認します。

以下は、テストのコマンドの例です。

#コンテナに入る
docker container exec -ti my-monitor /bin/bash

#実行される予定のスクリプトを実行
./bin/docker-entrypoint.sh

#ログを確認する

Kubernetesへのapply


ここまでで作成したイメージをKubernetesにapplyします。

Kubernetesのマニフェストファイルは肥大化しがちなので、kustomizeを使用しています。
kustomizeは「kustomization.yaml」に記載した複数のマニフェストファイルを1つにマージしてくれる機能です。
「kustomize build」コマンドを使用すると、「kubectl apply」可能なマニフェストファイルを作成してくれます。
(Kubernetes1.14で統合されました)

baseとoverlayのマニフェストを用意することで、両者をマージしてくれるため、より効率的に管理できます。

それぞれのマニュフェストのサンプルは、コードが長くなるため割愛しますが、以下のようなイメージです。

f:id:vasilyjp:20190611093536p:plain

メトリクスの可視化を監視SaaSで集約する


【Datadogのサービス内容】
Datadogの主要サービスとしては、次のようなことがあげられます。

  • ダッシュボード作成
  • メトリクスの可視化
  • 閾値などを使用したアラート
  • 各種コミュニケーションツールへの通知
  • ログアナリティクス
  • 外形監視

私のチームでは、使い勝手が良いためDatadogを使用して送信したメトリクスを可視化したり、アラートを設定しています。

【Datadogのクラウド連携機能】
Datadogでは、基本的なクラウドリソースのメトリクスを取得し可視化する「Integrations」という機能があります。 Integrations機能には、次のようなものがあり、3Cのクラウドリソースのメトリクス可視化に対応しています。

  • Azure Integrations
  • AWS Integrations
  • Google Cloud Platform Integrations

これらのIntegrationsで取得できるメトリクスと、本システムで取得したカスタムメトリクスを組み合わせることで、3Cリソースを包括的に監視できます。

まとめ


いかがでしたでしょうか?
「Kubernetes CronJobを使ったクラウドSQL Databaseの監視と運用」のお話をさせていただきました。 マルチクラウド化の推進のほか、アプリケーションの分散トレーシングなどにも力を入れていきたいと思っています。

ZOZOテクノロジーズでは、一緒にデータ基盤を作ってくれる方を募集しています。 ご興味がある方は以下のリンクから是非ご応募ください!

www.wantedly.com

カテゴリー