A Day In The Life

とあるプログラマの備忘録

Time型をJSONに変換するとtime.Locationの情報が欠落する場合の対処法

Time 型のフィールドを含む構造体を JSONエンコードJSON からデコードすると特定条件下で Time 型オブジェクトの time.Location の情報が失われることがあります。 以下のプログラムを見てください。

package main

import (
    "fmt"
    "time"
    "encoding/json"
)

type Hoge struct {
    Name string
    CreatedAt time.Time
}

func main() {
    time.Local = time.UTC
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        CreatedAt: time.Unix(1593077427, 0),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    fmt.Printf("h1の出力結果\n%#v\n", h1)
    fmt.Printf("h2の出力結果\n%#v\n", h2)
}

出力結果を確認すると、JSON 変換前の Hoge オブジェクト(変数名 h1)の CreatedAt フィールドは loc に (*time.Location)(0x5c7a40) がセットされているのに対し、JSON 変換後に復元した Hoge オブジェクト(変数名 h2)の CreatedAt フィールドは loc が nil になっています。

h1の出力結果
main.Hoge{Name:"aaa", CreatedAt:time.Time{wall:0x0, ext:63728674227, loc:(*time.Location)(0x5c7a40)}}
h2の出力結果
main.Hoge{Name:"aaa", CreatedAt:time.Time{wall:0x0, ext:63728674227, loc:(*time.Location)(nil)}}

これ %+v で出力しても違いがわからないので注意が必要です。%#v で出力してみないとわからないです。

time.Localにtime.UTCをセットした場合のみ起きる現象

JSON に変換すると Time 型フィールドの loc 情報が nil になってしまう現象は time.Localにtime.UTCをセットした場合のみ発生します。

func main() {
    time.Local = time.UTC
    ...
    ... その他、処理
    ...
}

以下のように UTC 以外のローカルタイムを設定している場合は発生しません。

func main() {
    loc, _ := time.LoadLocation("Asia/Tokyo")
    time.Local = loc
    ...
    ... その他、処理
    ...
}

ローカル時間をUTCにセットするとreflect.DeepEqualが常にfalse判定されてしまう

time.Location の情報が失われると、Time 型フィールドを含む構造体を reflect.DeepEqual で比較した時に意図通り判定されなくなります。以下のプログラムを見てください。

package main

import (
    "fmt"
    "time"
    "encoding/json"
    "reflect"
)

// Hoge構造体の定義は省略

func main() {
    time.Local = time.UTC
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        CreatedAt: time.Unix(1593077427, 0),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    result := reflect.DeepEqual(h1, h2)
    fmt.Printf("reflect.DeepEqual: %v", result)
}

result の出力結果は true ではなく false になります。

reflect.DeepEqual: false

対処方法

これに対処する方法はいくつか存在します。

  • Time.Equal メソッドを使って判定する
  • UTC に変換する
  • go-cmp を使って判定する
  • time.Local に nil をセットする

個別に対処方法を見ていきましょう

Time.Equal メソッドを使って判定する

こちらは time パッケージの Godoc に記載されている方法です。

result := h1.CreatedAt.Equal(h2.CreatedAt)
fmt.Printf("Time.Eqaul: %v", result)

出力結果は true になります。

Time.Eqaul: true

ただこの方法だと Time 型のフィールドを含む構造体同士を比較する場合はフィールドを一つずつ比較しないといけなくなります。

UTC に変換する

Time オブジェクトを生成するタイミングで UTC メソッドを使って UTC 変換に変換します。

func main() {
    time.Local = time.UTC
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        // UTCに変換する
        CreatedAt: time.Unix(1593077427, 0).UTC(),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    result := reflect.DeepEqual(h1, h2)
    fmt.Printf("reflect.DeepEqual: %v", result)
}

Hoge 構造体を生成するときに UTC メソッドを使って変換する必要がありますが結果はちゃんと true になります。

reflect.DeepEqual: true

Equal メソッドを使うよりも良さそうではありますが UTC メソッド呼び出しを忘れるとバグの温床になります。個人的にはあまりおすすめしません。

go-cmp を使って判定する

reflect.DeepEqual を使う代わりに、ユニットテストで構造体を比較する時の定番パッケージ go-cmp を使って比較します。

import (
    "fmt"
    "time"
    "encoding/json"
    "github.com/google/go-cmp/cmp"
    "reflect"
)

// Hoge構造体の定義は省略

func main() {
    time.Local = time.UTC
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        CreatedAt: time.Unix(1593077427, 0),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    result1 := reflect.DeepEqual(h1, h2)
    fmt.Printf("reflect.DeepEqual: %v\n", result1)
 
    result2 := cmp.Equal(h1, h2)
    fmt.Printf("cmp.Equal: %v", result2)
}

出力結果は以下のようになります。

reflect.DeepEqual: false
cmp.Equal: true

go-cmp は Time 型フィールドを比較する時、内部で Time.Equal メソッドを使って判定してくれるので reflect.DeepEqual が false 判定してしまう場面でも意図通り判定してくれます。

time.Local に nil をセットする

go-cmp も良いのですが Go の標準パッケージだけでなんとかするなら、main 関数で time.Local に nil をセットする方法があります。

package main

import (
    "fmt"
    "time"
    "encoding/json"
    "reflect"
)

// Hoge構造体の定義は省略

func main() {
    // nilをセットする
    time.Local = nil
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        CreatedAt: time.Unix(1593077427, 0),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    result := reflect.DeepEqual(h1, h2)
    fmt.Printf("reflect.DeepEqual: %v\n", result)
}

出力結果は以下のようになります。意図通り reflect.DeepEqual の結果が true になります。

reflect.DeepEqual: true

参考記事

Prometheusで収集したメトリクスを使ってCloud Monitoringのダッシュボードを作成する

Prometheus で収集した GKE アプリケーションのメトリクスを GCP の Cloud Monitoring(旧Stackdriver Monitoring) にエクスポートして Cloud Monitoring のダッシュボードを作成する方法を紹介します。Prometheus といえば Grafana を使ってダッシュボードを作成することが多いですが GCP 環境では Cloud Monitoring に一元化して監視できたほうが何かと便利です。あえて Grafana を使わず Cloud Monitoring でメトリクスを可視化します。

以下の記事で GKE 上に構築した Redis のメトリクスを Prometheus を使って収集しました。この環境を利用して Prometheus から Cloud Monitoring にメトリクスをエクスポートする方法を紹介します。

glassonion.hatenablog.com

メトリクスを Cloud Monitoring にエクスポートする方法

Prometheus のメトリクスを Cloud Monitoring にエクスポートする方法は以下の2つがあります。

  • prometheus-to-sd
  • stackdriver-prometheus-sidecar

prometheus-to-sd は prometheus text format (Prometheusのメトリクスデータの形式)を Cloud Monitoring 形式に変換してエクスポートすることができるツールです。ただし prometheus-to-sd は GoogleKubernetes チームが必要とする指標のみをサポートするように開発されたもので使用用途が限定されているのとドキュメントが少ないのであまりおすすめしません(使ったことがないので実際のところどうなのかわかりませんが)。

一方の stackdriver-prometheus-sidecar は Stackdriver が開発した Prometheus のメトリクスを Cloud Monitoring にエクスポートするためのツールです。GCP の公式ドキュメントでも stackdriver-prometheus-sidecar を使って Cloud Monitoring に指標をエクスポートする方法が紹介されています。

この記事ではドキュメントも多く情報が充実している stackdriver-prometheus-sidecar を使います。

メトリクスを Cloud Monitoring にエクスポートするためのポイント

ポイントは3つあります。

  • Prometheus のメトリクス収集は static_config ではなくサービスディスカバリを使って行う
  • Prometheus のメトリクスデータを共有ボリュームに書き込むように設定する
  • stackdriver-prometheus-sidecar のコンテナを Prometheus のサイドカーとして配置する

Prometheus のメトリクス収集は static_config ではなくサービスディスカバリを使って行う

static_config だと stackdriver-prometheus-sidecar が Prometheus から情報を読み出せないので Prometheus のメトリクス収集は static_config ではなく サービスディスカバリで行ってください。

Prometheus のメトリクスデータを共有ボリュームに書き込むように設定する

stackdriver-prometheus-sidecar は共有ボリュームにある WAL(write-ahead-log) ファイルにアクセスしてメトリクスをエクスポートするので Prometheus が共有ボリュームに対してメトリクスデータを書き出すように設定する必要があります。WAL については以下のページに詳細があるので気になる方は参照してみてください。

stackdriver-prometheus-sidecar のコンテナを Prometheus のサイドカーとして配置する

stackdriver-prometheus-sidecar は Prometheus の収集したメトリクスを読み出して Cloud Monitoring にエクスポートするので、stackdriver-prometheus-sidecar を Prometheus のサイドカーとして配置します。

構成

今回の Kubernetes の構成は以下のようになります。

sequence dialog

Prometheus の Deployment の設定

それでは実際に Prometheus のメトリクスを Cloud Monitoring にエクスポートする環境を構築してみます。Prometheus と stackdriver-prometheus-sidecar のコンテナを配置する Deployment ファイルを以下のように作成してください。

Deployment ファイルの重要な設定をみていきましょう。

共有ボリュームにデータを書き出すための設定

以下はPrometheus のデータ書き込みを共有ボリュームにするための設定です。23行目部分の storage.tsdb.path=/data で指定しています。

args:
  - '--config.file=/etc/prometheus/config/prometheus.yaml'
  - '--storage.tsdb.path=/data' #共有ボリュームに書き込むための設定

stackdriver-prometheus-sidecarコンテナの配置

以下の31行目から44行目が stackdriver-prometheus-sidecar コンテナの配置設定になります。 {{GCPプロジェクトID}}{{クラスタ名}} はお使いの GCP プロジェクトの環境に合わせて変更してください。

- name: sidecar
  image: gcr.io/stackdriver-prometheus/stackdriver-prometheus-sidecar:0.7.3
  args: [
    "--stackdriver.project-id", "{{GCPプロジェクトID}}",
    "--stackdriver.kubernetes.cluster-name", "{{クラスタ名}}",
    "--stackdriver.kubernetes.location", "asia-northeast1",
    "--prometheus.wal-directory", "/data/wal",
    "--prometheus.api-address", "http://127.0.0.1:9090",
  ]
  ports:
  - containerPort: 9091
  volumeMounts: # 共有ボリュームのマウント
  - name: data-volume
     mountPath: /data

こちらの記事で作成した prom-deployment.yaml ファイルを今回作成した yaml ファイルに置き換えて Kubernetes にデプロイすれば構築は完了です。

Metrics explorerでグラフを作成する

ここまでの設定で Prometheus のメトリクスを Cloud Monitoring の外部指標として取得できるようになりました。Cloud Monitoring で外部指標をグラフ化する手順は以下のようになります。

  1. GCP の Cloud Console で、[Monitoring] を選択します
  2. [Monitoring] のナビゲーションパネルで、[Metrics explorer] をクリックします
  3. [Find resource type and metric] メニューで、次(4と5)の手順を行います
  4. [Resource type] として [Kubernetes Container](k8s_container)を選択します(gke_containerではないので注意)
  5. [Metric] フィールドには、接頭辞 external.googleapis.com/prometheus/ を含む指標を選択します

以下は外部指標 external.googleapis.com/prometheus/redis_cpu_user_seconds_total をグラフ化したイメージです。

f:id:glass-_-onion:20200529192144p:plain

参考

Prometheus のサービスディスカバリを使って Kubernetes 上に構築した Redis を監視する

こちらの記事で Prometheus の static_config を設定して Redis のメトリクスを取得する方法について紹介しました。

glassonion.hatenablog.com

Prometheus で監視対象のサーバの設定をする方法は static_config の他に Service Discovery があります。static_config はわかりやすく簡単に設定できるので入門にはとても良いのですが、実運用を考えると static_config よりも Service Discovery を使ってメトリクスを取得するのがおすすめです。この記事では Service Discovery を使って監視対象を設定する方法を紹介します。

前回のおさらい

前回の記事では Kubernetes 上に構築したマスター/スレーブ構成の Redis を Prometheus の static_config を使って監視するまでの流れを紹介しました。

前回作成したファイルは以下です。

  • 監視対象のファイル
    • redis-service.yaml(Redisのサービス)
    • redis-ss.yaml(RedisのStatefulSet)
    • redis.conf(Redisの設定ファイル)
    • launch.sh(Redisの起動ファイル)
  • Prometheus関連のファイル
    • prom-service.yaml(Prometheusのサービス)
    • prom-deployment.yaml(Prometheusのデプロイメント)
    • prometheus.yaml(Prometheusのコンフィグファイル)

今回は主に prometheus.yaml に関する内容になります。それ以外は権限回りの設定で RBAC ファイル追加や prom-deployment.yaml の修正をします。

サービスディスカバリを使う場合のPrometheusコンフィグの設定

サービスディスカバリの説明やコンフィグの内容の説明は後回しにして Prometheus のコンフィグファイルの設定をしてみます。

前回は static_configs を使って以下のように設定しました。

scrape_configs:
- job_name: redis_exporter
  static_configs:
  - targets:
    - redis-0.redis.default.svc:9121
    - redis-1.redis.default.svc:9121
    - redis-2.redis.default.svc:9121

static_configs の場合は監視する Pod を一つづつ targets に設定する必要があります。直感的でわかりやすいものの Pod を増やしたり減らしたりした場合に毎回修正が必要になるのでコンフィグファイルのメンテナンスが大変です。サービスディスカバリを使う場合は Pod をひとつひとつ指定するのではなく、「サービスを介してポート 9121 を公開している Pod を検知すること」というような指定をします。具体的に以下のようになります。

static_configs に比べると設定行数も増えて複雑になりましたが、こうすることで監視対象の Pod が増減しても設定ファイルの修正なしで済みます。

PrometheusのService Discovery

Prometheus の Service Discovery はコンフィグファイルに設定された情報から動的に監視対象を検知してくれる仕組みです。Prometheus の Service Discovery には以下の種類があります。

  • azure_sd_config
  • consul_sd_config
  • dns_sd_config
  • ec2_sd_config
  • openstack_sd_config
  • file_sd_config
  • gce_sd_config
  • kubernetes_sd_config
  • marathon_sd_config
  • nerve_sd_config
  • serverset_sd_config
  • triton_sd_config

Configuration | Prometheus から引用

Kubernetes のノードや Kubernetes 上に構築したサービスまたは Pod を監視する場合は kubernetes_sd_config を使います。kubernetes_sd_config は Kubernetes 向けの Service Discovery です。Prometheus のコンフィグに kubernetes_sd_config を設定することによって Prometheus が kube-apiserver(Kubernetes' REST API)を使って Pod や Service のメトリクスを収集することができるようになります。

static_config と kubernetes_sd_config の監視対象に対するアクセス方法の違いは以下のようになります。

種類 監視対象に対するアクセス方法
static_config HTTP アドレスを直指定してアクセスする
kubernetes_sd_config kube-apiserver を使って監視対象を探してから HTTP アクセスする

kubernetes_sd_configのロールの種類

kubernetes_sd_config は以下の5種類のロールを指定してメトリクスを取得します。種類によってそれぞれ検知できる監視対象が異なります。

Role 内容
node nodeを検出する
endpoints サービスに紐づいたPodを検出する
service サービスを検出する(Podは検出できない)
pod サービスに紐づいていないPodも含めて検出する
ingress ingressを検出する

Pod の監視設定をする場合は endpoints または pod を使います。Pod はサービス経由で公開していることが多いので endpoints を使うことが多いです。

設定したprometheus.yamlの説明

Prometheus のサービスディスカバリがどのようなものか理解できたと思うので、先ほど設定した prometheus.yaml ファイルの内容について説明していきます。

まずはロールの設定から見ていきます。以下はロールに endpoints を指定してサービス経由の Pod を検出できるようにしています。

kubernetes_sd_configs:
- role: endpoints

次は source_labels の action の部分です。こちらを指定することによりどのような条件の Pod を検知するか設定することができます。以下はネームスペース名 default かつポート番号が9121の Pod を検出するように指定しています。action には keep 以外に drop を指定することができます。drop を指定すると監視対象から外れます(明示的に監視対象から外したいときに使います)。

- source_labels:
  - __meta_kubernetes_namespace
  - __meta_kubernetes_pod_container_port_number
  regex: default;9121
  action: keep

最後に Prometheus の監視画面に表示される job 名と pod 名を設定します。こちらの設定は表示の設定なのでこだわりがなければ無くても問題ありません。

- source_labels:
  - __meta_kubernetes_service_name
  target_label: job
- source_labels:
  - __meta_kubernetes_pod_name
  target_label: pod

以上で Kubernetes 上に構築した Pod の監視設定は終わりです。

Kubernetes 1.8以上はRBACの設定が必要

Prometheus の static_config は直接 HTTP アドレスを指定してメトリクスを取得するので Kubernetes の権限まわりのことを考慮する必要はありませんでした。一方サービスディスカバリを使う場合は kube-apiserver から Kubernetes の情報を取得する必要があります。kube-apiserver から情報を取得するためには RBAC を設定する必要があります。GKE Kubernetes 1.7までは RBAC がまだ stable になっていなかったので設定は不要でした。しかし GKE Kubernetes 1.8から RBAC が stable になり、Prometheus の Pod に対して RBAC を設定しないと、サービスディスカバリを使った監視対象の検出ができなくなりました。RBAC のカスタムロールを作成して Prometheus の Pod に権限を付与する必要があります(監視対象のリソースに対して RBAC を設定することもできますが、それをすると Redis 側に修正を入れないといけなくなるため今回はその方法はとりません)。

RBAC を設定する

RBAC を設定するときにポイントは以下です。

  • Prometheus と監視対象(今回はRedis)の Namespace を分ける
    • Prometheus 側は stats、Redis 側は default ネームスペースを設定しています
  • カスタムロールを作成する
    • all-readerという名前のカスタムロールを作成します
  • Prometheus 側の Namespace にサービスアカウントを作成する
    • prometheusという名前のサービスアカウントを作成します
  • 作成したサービスアカウントにカスタムロールを紐づけて権限を付与する
    • prometheusサービスアカウントとall-readerロールを紐付けます
  • Prometheus の Pod にサービスアカウントを設定して監視対象を検出できるようにする
    • 別途 Prometheus の Deployment の yaml を修正します

上記ポイントを加味した RBAC の設定は以下のようになります。

Prometheus の Pod にサービスアカウントを設定して監視対象を検出できるようにする

RBAC の設定ができたので Prometheus の Pod に対して先ほど作成したサービスアカウントを以下のように紐付けます。

前回の記事の Deployment 設定に1行追加(17行目)しただけです。

... 省略 ...
    spec:
      serviceAccount: prometheus # 追加した設定
      containers:
... 省略 ...

デプロイして Prometheus の管理画面から Redis の状態を確認する

それでは設定したファイルをデプロイして Prometheus が監視対象を検知できるか確認してみます。Kustomize ファイルの内容は前回の記事から rbac.yaml が追加になった以外は同じです。

resources:
- redis-service.yaml
- redis-ss.yaml
- prom-service.yaml
- prom-deployment.yaml # 今回修正したファイル
- brac.yaml # 新規追加したファイル
configMapGenerator:
- name: redis-config
  files:
    - launch.sh
    - redis.conf
- name: prometheus-config
  namespace: stats
  files:
    - prometheus.yaml # 今回修正したファイル

今回修正したのは prom-deployment.yaml と prometheus.yaml の2ファイルです。あと新たに brac.yaml が追加になりました。 kustomize.yaml を base ディレクトリに保存して以下のコマンドを実行します。

kubectl apply -k base

デプロイが終わったら、ブラウザから Prometheus の管理画面(http://35.1x9.1x0.XXX:9090)にアクセスして Targets 画面に移動します。以下のように Redis Exporter が認識されて入れば成功です。

f:id:glass-_-onion:20200509194108p:plain
targets画面

まとめ

Prometheus のサービスディスカバリを使って Kubernetes のメトリクスを取得するには kubernetes_sd_config を使います。Kubernetes 1.8以上で Prometheus のサービスディスカバリを使うには RBAC の設定が必要です。

サンプルコード

今回のサンプルコードはこちらに置いています。

github.com

参考書籍

Prometheus について一番詳しく書かれている書籍です。

参考記事

関連記事

Prometheus を使って Kubernetes 上に構築した Redis を監視する

PrometheusからRedisの状態を取得する

私は普段 Redis を GKE 上に構築して使用していますが、GCP コンソール画面から得られる情報が Pod の CPU 使用率、メモリ使用率、ディスク使用率くらいしかなくもう少し細かい情報を得たいと考えていました。そこで Prometheus を使って Redis の状態を監視してしてみたところとても良い感じだったのでその方法を紹介してみたいと思います。 Prometheus を使うと Redis の INFO コマンドで得られる情報を時系列に見ることができます。Prometheus は機能が豊富でいろいろできるのですが、この記事では必要最低限の設定でサクッとPrometheusを使った監視環境を構築をしてみたいと思います。

Prometheusって何?

Prometheus は SoundCloud が中心になって開発しているプル型の監視ツールです。既存のプログラムコードを変えることなくサクッと導入できるのでおすすめです。詳細は本家サイト Prometheus - Monitoring system & time series database か、 最近は専門の書籍も出ていますのでその辺りをチェックしてみてください 。

構成

以下の3つを Kubernetes 上に構築します。

  • Master/Slave構成のRedis
    • 監視対象のコンテナ
  • Exporter
    • Redisのメトリクス情報をPrometheusが読み込める形式に変換するコンテナ
  • Prometheus
    • 収集したRedisのメトリクス情報を時系列DBに保存して公開してくれるコンテナ

PrometheusはPull型の監視ツールのため、被監視対象のサーバからメトリクスを取得する必要があります。Prometheus は Redis から直接メトリクス情報を取得することができません。代わりに Exporterと呼ばれる Redis のメトリクス情報取得するためのアダプタ的なコンテナを Reids と同じ Pod に配置してそこから監視に必要な情報を取得します。

sequence dialog

Exporter はただの HTTP サーバです。Redis の各種メトリクス情報を Prometheus が解釈できるフォーマットに変換してくれます。Prometheus は Exporter が公開している /metrics にアクセスしてメトリクスを収集します。

構築の手順

構築の流れは以下のようになります。

  1. Prometheus を単体で構築する
  2. マスタースレーブ構成の Redis を構築する
  3. Redis の配置された Pod に Exporter を追加する
  4. Prometheus が Exporter からデータを収集できるように設定を修正する

前準備

本記事では Kubernetes 関連の yaml ファイルのデプロイに Kustomize を使います。以下のように k8s 関連の yaml を base という名前のディレクトリに格納するようにしてください。

base/
  ├ deployment.yaml
  ├ service.yaml
  └ ...etc

Prometheusを単体でk8s上に構築してみる

Prometheus を Kubernetes 上に構築するには Deployment と Service に加えて専用のコンフィグファイルをマウントする必要があります。

Deploymentの作成

はじめに Prometheus 本体の Pod リソースの設定をするため Deployment を作成します。Prometheus のコンテナは Docker Hub の prom/prometheus を使用します。 コンテナのポートは 9090 に設定します。stats という名前の Namespace 上に構築します(Namespaceの設定はあとでサービスと一緒に実施します)。

収集したメトリクス情報を保存するためのボリューム(data-volume)とConfigMap を読み込むためのボリューム(config-volume)をマウントしています。レプリカ数は1に設定します。

サービスの作成

Deployment が作成できたので Prometheus にアクセスするためのサービスを作成します。

今回は Service と一緒に Prometheus 専用の stats という名前の Namespace の定義もします。Service のタイプが LoadBalancer になっていますが、実際に運用する場合はロードバランサを internal にするか NodePort あたりを使ってください。

prometheus.yamlの作成

最後に Prometheus の設定をするためのコンフィグファイル(prometheus.yaml)を作成します。10秒間隔で Prometheus コンテナ自体の情報を収集するように設定してみます。この yaml ファイルは ConfigMap として読み込みます。

global:
  scrape_interval:     10s
  evaluation_interval: 10s
scrape_configs:
- job_name: 'prometheus'
  static_configs:
  - targets: ['localhost:9090']

global に Prometheus が情報を収集する時間間隔を定義し、scrape_configs に実施するジョブを定義しています。Prometheus は指定された間隔でジョブを実行してメトリクスデータを収集してくれます。

Prometheusをデプロイする

ここまで作成できるとファイル構成は以下のようになっています。

base/
  ├ prom-deployment.yaml
  ├ prom-service.yaml
  └ prometheus.yaml

base ディレクトリ以下に kustomize用のファイル kustomization.yaml を作成します。

resources:
- prom-service.yaml
- prom-deployment.yaml
configMapGenerator:
- name: prometheus-config
  files:
    - prometheus.yaml

kubectl コマンドを使って Prometheus をデプロイします。

kubectl apply -k base

ブラウザにURLhttp://35.1x9.1x0.XXX:9090/graphを入力して以下の画面が表示されればデプロイ成功です(35.1x9.1x0.XXXはサービスのIPアドレスです)。

f:id:glass-_-onion:20200421153349p:plain
Prometheus管理画面

Master/Slave構成のRedisを構築する

監視対象の Redis を Kubernetes 上に構築します。Headless Service を使ったシンプルなマスタースレーブ構成にしています。以前このブログで紹介した Redis Sentinel 構成から Sentinel 部分を取り払った構成になっています。Headless Service の説明もこちらの記事に書いてあります。

redis.confとRedisの起動ファイルを作成する

Redisの設定ファイル redis.conf ファイルを作成します。

bind 0.0.0.0
port 6379
maxclients 50000
dir /redis-data

Redisの起動時に実行されるスクリプトファイル launch.sh を作成します。

#!/bin/bash

SERVICE=redis
MASTER=${SERVICE}-0

if [ ${HOSTNAME} == ${MASTER} ]; then
    redis-server /config/redis.conf
else
    redis-server /config/redis.conf --slaveof ${MASTER}.${SERVICE} 6379
fi

StatefulSet の作成

Redis の Pod リソースの設定をするため StatefulSet を作成します。マスター1台、スレーブ2台構成にするのでレプリカ数は3に設定します。

StatefulSetを使うことで redis-0, redis-1, redis-2 という名前の Pod が生成されます。

Serviceの作成

Redis にアクセスするための Service を作成します。clusterIP に None を指定して Headless Service にします。

Headless Service にすることでクラスタ内から Pod名.サービス名 でアクセスすることが可能になります(今回の構成だとredis-0.redisのようになります)。

Redis をデプロイしてみる

以下の Kustomize ファイルを作成してデプロイしてみます。

resources:
- redis-service.yaml
- redis-ss.yaml
configMapGenerator:
- name: redis-config
  files:
    - launch.sh
    - redis.conf

kustomize.yaml を base ディレクトリに保存して以下のコマンドを実行します。

kubectl apply -k base

Redis にアクセスしてみる

redis-2 にアクセスして IP アドレスが返ってくるか確認します。

$ kubectl exec redis-2 -c redis \
-- redis-cli -p 26379 sentinel get-master-addr-by-name master

10.0.9.6
6379

Redis Exporterを追加する

ここまでで Prometheus 単体と Redis のマスタ/スレーブ がそれぞれ構築できました。ただこのままでは Prometheus は Redis のメトリクス情報を取得できません。そこで Redis の StatefulSet に Prometheus 用の Exporter コンテナを追加して Prometheus が Redis のメトリクスを取得できるようにします。頑張れば Redis の Exporter を自前で開発することもできますが割と大変なのでオープンソースの Redis Exporter を使います。Redis の Exporter は redis_exporter が有名なのでそちらを使ってみます。

先ほど作成した Redis の StatefulSet のファイル redis-ss.yaml を以下のように修正します。Redis の Pod に Redis Exporter をサイドカーとして配置します。

...省略...
      containers:
      - name: redis
        command: [sh, -c, source /config/launch.sh]
        image: redis:5-alpine
        ports:
        - containerPort: 6379
        volumeMounts:
        - mountPath: /config
          name: config
        - mountPath: /redis-data
          name: data
      - name: redis-exporter
        image: oliver006/redis_exporter:latest
        ports:
        - containerPort: 9121
...省略...

Exporter のポートは9121にします。Redis Exporter のコンテナの配置ができたので Prometheus が Exporter アクセスできるように Service にポートの設定を追加します。redis-service.yaml に以下の設定を追加します。

apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  type: ClusterIP
  clusterIP: None
  ports:
  - port: 6379
    name: port-redis
  - port: 9121
    name: port-redis-exporter
  selector:
    redis-app: redis

PrometheusからExporterの情報を収集する

ここまでで Redis 側の準備は整いました。あとは Prometheus が Redis Exporter にアクセスしてメトリクスを収集できるように設定するだけです。 Prometheus が Exporter からデータを取得するための設定を prometheus.yaml ファイルに書きます。Redis Exporter には Pod名.サービス名.ネームスペース名.svc:ポート番号 でアクセスすることができるようになります。prometheus.yaml の static_configs の targets を以下のように修正してください。

global:
  scrape_interval:     10s
  evaluation_interval: 10s
scrape_configs:
- job_name: redis_exporter
  static_configs:
  - targets:
    - redis-0.redis.default.svc:9121
    - redis-1.redis.default.svc:9121
    - redis-2.redis.default.svc:9121

Prometheus に Exporter を認識させる方法は static_configs とサービスディスカバリがありますが、今回は static_configs を使いました。サービスディスカバリの方が実用的ではあるものの RBAC の設定やサービスアカウントの作成が必要になり設定が複雑になるので static_configs にしました。

デプロイして Prometheus の管理画面から Redis の状態を確認する

Kustomize のファイルを以下のように修正して

resources:
- redis-service.yaml
- redis-ss.yaml
- prom-service.yaml
- prom-deployment.yaml
configMapGenerator:
- name: redis-config
  files:
    - launch.sh
    - redis.conf
- name: prometheus-config
  namespace: stats
  files:
    - prometheus.yaml

kustomize.yaml を base ディレクトリに保存して以下のコマンドを実行します。

kubectl apply -k base

デプロイが終わったら、ブラウザから Prometheus の管理画面(http://35.1x9.1x0.XXX:9090)にアクセスして Targets 画面に移動します。

f:id:glass-_-onion:20200421195319p:plain
Targets画面に移動する

以下のように Redis Exporter が認識されて入れば成功です。

f:id:glass-_-onion:20200421195357p:plain
Targets画面

試しに Redis の connected clients の情報を取得してみます。テキストボックスに redis_connected_clients と入力してから(またはドロップダウンから選択) Execute ボタンを押します。

f:id:glass-_-onion:20200421195714p:plain
redis_connected_clients

Redis の クライアント接続数が表示されました。取得したデータをグラフで見たい時は Graph タブを押すとグラフをみることができます。

f:id:glass-_-onion:20200421195859p:plain
グラフ

まとめ

Redis のコンテナと一緒に Redis Exporter のコンテナを配置すると Prometheus から Redis のメトリクスを取得することができます。Prometheusの コンフィグ設定は static_config を使うとお手軽です。

サンプルコード

今回のサンプルコードはこちらに置いています。

github.com

参考書籍

Prometheus について一番詳しく書かれている書籍です。

アダプタパターンとして Redis Exporter を使った事例が紹介されていました。

参考記事

関連記事

go-mode + lps-modeを使ってEmacsのGolang開発環境を整える

以前の記事 go-mode + eglotを使ってEmacsのGolang開発環境を整える - A Day In The Life で eglot を使った Go の開発環境構築方法を紹介しましたが、今回は lsp-mode を使って環境を構築してみたいと思います。前回と同じく Go の Language Server は gopls を使います。

動作確認バージョン

Emacsは25以上、Golangは1.11以上なら問題なく動作するはずです。

gopls のインストール

Go の Language Server は3種類あります(bingo 最近は開発を中止したみたいです)。

前回同様 gopls をインストールします。

$ go get -u golang.org/x/tools/cmd/gopls 

goimportsのインストール

こちらも前回同様、Go の import を整理してくれるコマンドもついでにインストールしておきます。

$ go get golang.org/x/tools/cmd/goimports

init.el の設定

入力補完は company-mode を使います。あと GOPATH や GOROOT など環境変数設定のために exec-path-from-shell を使います。

;; パッケージ管理サーバ
(require 'package)
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t)
(add-to-list 'package-archives '("marmalade" . "http://marmalade-repo.org/packages/"))
(package-initialize)
;; パッケージ情報の更新
(package-refresh-contents)

;; インストールするパッケージのリスト
(defvar my/favorite-packages
  '(
    use-package
    exec-path-from-shell
    ))
;; my/favorite-packagesからインストールしていないパッケージをインストール
(dolist (package my/favorite-packages)
  (unless (package-installed-p package)
    (package-install package)))

;; シェルに設定されている環境変数を引き継ぐ
(exec-path-from-shell-initialize)

;; lsp-mode
;; プロジェクトルートで M-x lsp-workspace-folder-add を実行すること
(use-package lsp-mode
  :ensure t ;自動インストール
  :custom ((lsp-inhibit-message t)
         (lsp-message-project-root-warning t)
         (create-lockfiles nil))
  :hook
  (prog-major-mode . lsp-prog-major-mode-enable)
  :config
  (setq lsp-response-timeout 5))
(add-hook 'hack-local-variables-hook
          (lambda () (when (derived-mode-p 'go-mode) (lsp))))

;; company-lsp integrates company mode completion with lsp-mode.
;; completion-at-point also works out of the box but doesn't support snippets.
(use-package company-lsp
  :ensure t
  :commands company-lsp)

;; Company mode is a standard completion package that works well with lsp-mode.
(use-package company
  :ensure t
  :config
  (global-company-mode)
  ;; Optionally enable completion-as-you-type behavior.
  (setq company-idle-delay 0)
  (setq company-minimum-prefix-length 1)
  (setq completion-ignore-case t)
  (setq company-dabbrev-downcase nil)
  (setq company-selection-wrap-around t))

;; go-mode
(use-package go-mode
  :ensure t
  :commands go-mode
  :config
  (setq gofmt-command "goimports")
  (add-hook 'before-save-hook 'gofmt-before-save))

emacs-lispのコードはこちらに置いてます。

プロジェクトルートが見つからない時の対策

Monorepo 構成で Go プロジェクトが複数存在する場合に lsp-mode がプロジェクトルートが判別できずローカルのパッケージの補完ができないことがあります。その場合は Emacs を起動後、以下のコマンドで追加します。

M-x lsp-workspace-folder-add

関連記事

参考

Kubernetes 上に Redis Sentinel を構築する

GCP環境でRedisを使う

GCP には Cloud Memorystore という Redis 互換のマネージドサービスがあるのですが、対応している Redis のバージョンが古い(4系)ので自前で GKE 上に Redis を立てることにしました。どうせ立てるなら master/slave 構成のほうが良いだろうということで Redis Sentinel にしました。

環境

  • GKE 1.13.6
  • Redis 5

Redis Sentinelって何?

Redis Sentinel は master/slave 構成の Redis の監視を行ってくれるサーバソフトウェアです。自動フェイルオーバー機能を持っています。

Redis Sentinelの構成

構成は以下のようになります。

sequence dialog

上の図では省略しましたが Sentinel は Sentinel 同士で繋がっていてすべての Redis を監視しています。

sequence dialog

Redisの設定

Redis の Config ファイルとコンテナ起動時に必要なスクリプトを作成します。

RedisのConfigファイルを作成する

Redis の master/slave 用の Config ファイルと Sentinel 用の Config ファイルを作成します。 はじめにマスター用の Config ファイル master.conf を作成します。

bind 0.0.0.0
port 6379

dir /redis-data

次にスレーブ用の Config ファイル slave.conf を作成します。

bind 0.0.0.0
port 6379

dir /redis-data

slaveof redis-0.redis 6379

最後に Sentinel 用の Config ファイル sentinel.conf を作成します。

bind 0.0.0.0
port 26379

sentinel monitor master redis-0.redis 6379 2
sentinel parallel-syncs master 1
sentinel down-after-milliseconds master 10000
sentinel failover-timeout master 20000

redis-0.redis をマスターに設定し sentinel でマスターを監視するように config に指定しています。 sentinel.confの設定内容については以下の記事が参考になります。

コンテナ起動時に実行するスクリプトを作成します

Redis コンテナ起動時に実行するスクリプト init.sh を作成します。どのコンテナをマスターにするか決定するためのものです。

#!/bin/bash
if [[ ${HOSTNAME} == 'redis-0' ]]; then
  redis-server /redis-config/master.conf
else
  redis-server /redis-config/slave.conf
fi

Sentinel 用のスクリプト sentinel.sh を作成します。マスターが立ち上がるのを待ってからredis-sentinelコマンドを実行します。

#!/bin/bash
while ! ping -c 1 redis-0.redis; do
    echo 'Waiting for server'
    sleep 1
done

redis-sentinel /redis-config/sentinel.conf

Redis 関連のファイルは以上で作成完了です。ファイル構成は以下のようになります。

|-- master.conf
|-- slave.conf
|-- sentinel.conf
|-- init.sh
`-- sentinel.sh

Redisの設定をConfigMapにまとめる

RedisのConfigファイルとコンテナ起動時に使うスクリプトをまとめて redis-config という名前の ConfigMap を作成します。

$ kubectl create configmap \
> --from-file=slave.conf=./slave.conf \
> --from-file=master.conf=./master.conf \
> --from-file=sentinel.conf=./sentinel.conf \
> --from-file=init.sh=./init.sh \
> --from-file=sentinel.sh=./sentinel.sh \
> redis-config

ワークロードの作成

次に Redis コンテナを定義する redis.yaml を作成します。デプロイメント(Deployment)ではなくステーフルセットを使うとポッドに ID が採番されポッド名-インデックスの形式でポッドにアクセスできます。replicas に3を指定しているので redis-0, redis-1, redis-2 という名前のポッド(Pod)が生成されます。Redis Sentinel 構成にする場合は Redis のコンテナが最低でも3つ以上必要なのとPodのIPアドレスをPod名から取得したかったのでこのような構成にしています。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  selector:
    matchLabels:
      app: redis
  replicas: 3
  serviceName: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      initContainers:
      - image: busybox:latest
        name: redis-init
        command: [sh, -c, cp -L /tmp/* /config]
        volumeMounts:
        - mountPath: /tmp
          name: tmp
        - mountPath: /config
          name: config
      containers:
      - command: [sh, -c, source /config/init.sh]
        image: redis:5-alpine
        name: redis
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - mountPath: /config
          name: config
        - mountPath: /redis-data
          name: data
      - command: [sh, -c, source /config/sentinel.sh]
        image: redis:5-alpine
        name: sentinel
        ports:
        - containerPort: 26379
          name: sentinel
        volumeMounts:
        - mountPath: /config
          name: config
      volumes:
      - configMap:
          name: redis-config
        name: tmp
      - emptyDir:
        name: config
      - emptyDir:
        name: data

サービスの作成

Redis にアクセスするためのサービスを作成します。clusterIP に None を設定することで Pod の IPアドレスを直接取得することができます。

apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  ports:
  - port: 6379
    name: port-redis
  - port: 26379
    name: port-sentinel
  clusterIP: None
  selector:
    app: redis

IP アドレスを設定しないサービスのことを Headless サービスと呼びます。今回のように Headless Service + StatefulSet の組み合わせで使うことが多いようです。Headless サービスの説明は以下を参照してください。

k8s 1.13以降はConfigMapが読み取り専用になったので注意が必要です

Redis Sentinel は Config ファイルにアクセスして内容を更新します。ConfigMap のままでは正常に動作しません。なのでボリュームに ConfigMap を読み込んでから書き込み可能な emptyDir なボリュームにファイルをコピーする必要があります。

# ファイルコピー部分の抜粋
initContainers:
- image: busybox:latest
  name: redis-init
  command: [sh, -c, cp -L /tmp/* /config]
  volumeMounts:
  - mountPath: /tmp #読み取り専用
    name: tmp
  - mountPath: /config #書き込み可能
    name: config

ConfigMap はボリューム作成時にシンボリックリンクとして作成されリンク先が読み取り専用に設定されます。なのでdefaultModeで書き込み権限を付与しても書き込みできるようにはなりません。

Redisのデプロイ

サービスをデプロイします。

$ kubectl apply -f service.yaml

ステートフルセットをデプロイします。

$ kubectl apply -f redis.yaml

確認

redis-2 にアクセスして IP アドレスが返ってくるか確認します。

$ kubectl exec redis-2 -c redis \
-- redis-cli -p 26379 sentinel get-master-addr-by-name master

10.0.9.6
6379

参考書籍

こちらの「入門 Kubernetes」の14章を参考にしました。

参考

ConfigMap の読み取り専用を回避する方法

sentinel.conf の設定

go-mode + eglotを使ってEmacsのGolang開発環境を整える

普段 Golang でサーバコードを書くときはもっぱら Emacs を使っています。Go Modules に移行してから gocode が動作しなくなったので最近はやりの LSP(Language Server Protocol) を導入することにしました。Go の Language Server は gopls 、Emacs のLSPクライアントは eglot を使います

動作確認バージョン

Emacsは25以上、Golangは1.11以上なら問題なく動作するはずです。

gopls のインストール

Go の Language Server は3種類あります。一番下の gopls が Golang の公式 Language Server です。

一長一短あるようですが公式が安心そうなので gopls をインストールします。

$ GO111MODULE=on go get -u golang.org/x/tools/cmd/gopls 

goimportsのインストール

Go の import を整理してくれるコマンドもついでにインストールしておきます。

$ go get golang.org/x/tools/cmd/goimports

init.el の設定

Emacs の LSP クライアントは lsp-mode が有名ですが、動作がめちゃくちゃ重かったのと余計な表示がいっぱい出てきたのでシンプルな eglot を使うことにしました。入力補完は company-mode を使います。あと GOPATH や GOROOT など環境変数設定のために exec-path-from-shell を使います。

;; パッケージ管理サーバ
(require 'package)
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t)
(add-to-list 'package-archives '("marmalade" . "http://marmalade-repo.org/packages/"))
(package-initialize)
;; パッケージ情報の更新
(package-refresh-contents)

;; インストールするパッケージのリスト
(defvar my/favorite-packages
  '(
    use-package
    exec-path-from-shell
    ))
;; my/favorite-packagesからインストールしていないパッケージをインストール
(dolist (package my/favorite-packages)
  (unless (package-installed-p package)
    (package-install package)))

;; シェルに設定されている環境変数を引き継ぐ
(exec-path-from-shell-initialize)

;; eglot
;; M-.で定義ジャンプ、M-,でジャンプ先からもどる
;; eglot はデフォルトの Language Server として go-langserver を使うので golsp に変更する
;; 事前に go get -u golang.org/x/tools/cmd/gopls しておくこと
(use-package eglot
  :ensure t ;自動インストール
  :config
  (define-key eglot-mode-map (kbd "M-.") 'xref-find-definitions)
  (define-key eglot-mode-map (kbd "M-,") 'pop-tag-mark)
  (add-to-list 'eglot-server-programs '(go-mode . ("gopls")))
  (add-hook 'go-mode-hook #'eglot-ensure))

;; company-mode
(use-package company
  :ensure t
  :config
  (global-company-mode)
  (setq company-idle-delay 0)
  (setq company-minimum-prefix-length 1)
  (setq company-dabbrev-downcase nil)
  (setq company-selection-wrap-around t))

(use-package go-mode
  :ensure t
  :commands go-mode
  :config
  (setq gofmt-command "goimports")
  (add-hook 'before-save-hook 'gofmt-before-save))

emacs-lispのコードはこちらに置いてます。

プロジェクトルートが見つからない時の対策

Monorepo 構成で Go プロジェクトが複数存在する場合に eglot がプロジェクトルートが判別できずローカルのパッケージの補完ができないことがあります(eglotはproject.elをつかってgitのファイル構成からルートを判別するようです)。その場合は projectile-mode を使ってプロジェクトルートを判別させます。init.elに以下のスクリプトを追加してから

;; Bridge projectile and project together so packages that depend on project
;; like eglot work
(use-package projectile
  :ensure t)
(defun my-projectile-project-find-function (dir)
  (let ((root (projectile-project-root dir)))
    (and root (cons 'transient root))))
(projectile-mode t)
(with-eval-after-load 'project
  (add-to-list 'project-find-functions 'my-projectile-project-find-function))

こんな感じでプロジェクトルートのディレクトリに空の . projectile ファイルを配置してください。

├ .git
├ foo_service
    ├ main.go
    └ .projectile
├ bar_service
    ├ main.go
    └ .projectile
└ baz_service
    ├ main.go
    └ .projectile

保管で Keyword argument のエラーが出た時の対処方法

error in process filter: Keyword argument :version not one of (:uri :diagnostics) のエラーが出るときは eglot のバージョンアップをすると解消します。

参考