各podのActionCableのconnection数をCloudWatch Metricsに送る

前回の記事に書いたように最近 graphql-ruby を使って Subscription を実装しました。
blog.hatappi.me

そのタイミングで Rails の ActionCable を使うことになったのですが、ふと各 pod でどれくらい connection 数があるのかを見れるようにしてみたいなという気持ちになりました。

実現するために考える必要があるのが どうやって connection 数を集めるのかどうやって可視化するのかの二つです。
可視化に関しては現状k8sのpodのCPU使用率などのメトリクスを CloudWatch メトリクスで収集してダッシュボードを作っているので、 CloudWatch メトリクスとして収集できると良さそうです。
そのためどうやって CloudWatch メトリクスになげるかの方法を考える必要があります。

そもそもどうやってコネクション数をとるのか

どうも↓のように呼び出すと取れる?らしい。

> ActionCable.server.remote_connections.server.connections.size
3

さらに実行中のサーバープロセスで呼び出す必要があるっぽくてActionCableのコネクション数をとれる API を用意する必要がありそう。
そのため Rake task を作って定期実行して CloudWatch メトリクスに送るとかは厳しそうな感じでした。

どんな構成にするかを考える

RailsAPI を生やすとして誰がそれを叩くのか?

それを考えている時 CloudWatch Agent の次のドキュメントを見つけました。

docs.aws.amazon.com

CloudWatch Agent が collectd プロトコロルにそったデータを受け取ってそれを CloudWatch になげてくれそうです。

ここで一度自分の頭の中で考えていることをぼんやり絵に書いてみます。

f:id:hatappi1225:20191127012101j:plain

今回の作業の大まかな流れとしては次のようになります。

  • cloudwatch agentが動作する pod を起動して Service を作って他の namespace からリクエストできるようにする
  • Rails に ActionCable の connection を取得するAPIを生やす
  • collectd をサイドカーで起動する

それぞれ見ていきます。

cloudwatch agentが動作する pod を起動して Service を作って他の namespace からリクエストできるようにする

まず CloudWatch Agent が起動する pod を作成する必要があるのですが、これは Container Insight をセットアップする次のドキュメントに沿って導入します。
私の場合はもともと Container Insight を利用していたので、これを使いました。

docs.aws.amazon.com

CloudAgent の設定は ConfigMap 経由で設定します。
今回は collectd からの UDP リクエストを 25826 で受け付けるので次のような設定に変更しました。

apiVersion: v1
kind: ConfigMap
metadata:
  name: cwagentconfig
  namespace: cloudwatch
data:
  cwagentconfig.json: |
    {
      "logs": {
        "metrics_collected": {
          "kubernetes": {
            "cluster_name": "xxxx.example.com",
            "metrics_collection_interval": 60
          }
        },
        "force_flush_interval": 5,
        "endpoint_override": "logs.ap-northeast-1.amazonaws.com"
      },
      "metrics": {
        "metrics_collected": {
          "collectd": {
            "service_address": "udp://0.0.0.0:25826",
            "metrics_aggregation_interval": 60,
            "collectd_security_level": "none"
          }
        }
      }
    }

今回必要なのは metris 配下の設定です。

  • service_address: CloudWatch Agent が Listen するアドレスとポートを指定する。
    デフォルトは udp://127.0.0.1:25826 になっているので外から受けられるように upd://0.0.0.0:25826 に変更します。
  • metrics_aggregation_interval : きめ。
  • collectd_security_level : これはネットワーク通信のセキュリティレベルを指定します。本来は encrypt を指定すると良いと思いますが、今回はサボりました。。。。

あとは port に 25826 を指定して起動すればひとまず Agent の指定は完了です。

          ports:
            - containerPort: 25826
              hostPort: 25826
              protocol: UDP

最後に Service を作成します。

apiVersion: v1
kind: Service
metadata:
  name: collectd
  namespace: cloudwatch
spec:
  ports:
    - protocol: UDP
      port: 25826
      targetPort: 25826
  selector:
    name: cloudwatch-agent

これで他の namespace からは collectd.cloudwatch.svc.cluster.local で pod にアクセスできるようになるはずです。

Rails に ActionCable の connection を取得するAPIを生やす

ActionCable の connection 数をとる API は次のように実装します。
hostname をいれているのは CloudWatch メトリクスとして収集する時にどこで発生したメトリクスかを識別するためです。

class ActionCableStatusController < ApplicationController
  def index
    render json: {
      hostname: ENV['HOSTNAME'], # HOSTNAMEにはpod name が入る
      connection_count: ActionCable.server.remote_connections.server.connections.size
    }, status: :ok
  end
end

あとは route に追加します。

Rails.application.routes.draw do
  resources :action_cable_status, only: :index
end

これで /action_cable_status とアクセスすると次のようなレスポンスが返ってくるようになります。

{"hostname":"rails-xxxxxx-yyyyy","connection_count":2}

collectd をサイドカーで起動する

collectd にはメトリクスを収集するための便利なプラグインが提供されていて curl して受け取った JSON レスポンスから値を取り出してくれる cURL-JSON などがある。
最初はこの cURL-JSON を使用しようとしたのですが、ホスト情報を良い感じに送ることができなくて断念しました。
そこで任意のスクリプトを自分で定義してそれを実行してメトリクスを収集する Exec を使用することにしました。

今回スクリプトは次のようなものを用意しました。

#!/bin/bash

INTERVAL=${INTERVAL:-30}
PLUGIN_NAME=action_cable_connections
URL=${URL:-http://localhost:3000/action_cable_status}

while true; do
  BODY=$(curl -s ${URL})

  CONNECTION_COUNT=$(echo ${BODY} | jq -r '.connection_count')
  HOSTNAME=$(echo ${BODY} | jq -r '.hostname')
  UNIX_TIME=`date +%s`

  echo PUTVAL \"${HOSTNAME}/${PLUGIN_NAME}/count\" interval=${INTERVAL} ${UNIX_TIME}:${CONNECTION_COUNT}

  sleep ${INTERVAL}
done

標準出力を collectd が拾ってくれる。
ちょっと出力の仕方に癖があるのですが、 HOSTNAME が入っているところが CloudWatch メトリクス上の host になり、 PLUGIN_NAME が メトリクス名の一部になります。

次に collectd の設定ファイルを作成します。
真ん中らへんにある Plugin Exec がメトリクスを収集する設定で Exec "ユーザー名" "実行するスクリプト" というフォーマットになっています。
Plugin Network が Cloudwatch Agent にメトリクスを送る設定です。

Interval 10

LoadPlugin logfile
LoadPlugin exec
LoadPlugin network

<Plugin logfile>
  LogLevel info
  File STDOUT
  Timestamp true
  PrintSeverity false
</Plugin>

<Plugin exec>
  Exec "collectd-user" "/usr/local/bin/action_cable_connections.sh"
</Plugin>

<Plugin network>
  <Server "collectd.cloudwatch.svc.cluster.local" "25826">
    ResolveInterval 30
  </Server>
  TimeToLive 128
  ReportStats false
  CacheFlush 1800
</Plugin>

Include "/etc/collectd.d"

あとは Dokcer Image を作成します。

FROM centos:8

RUN yum update -y && \
  yum install -y epel-release
RUN yum install -y collectd jq curl

COPY collectd.conf /etc/
COPY action_cable_connections.sh /usr/local/bin/

RUN chmod +x /usr/local/bin/action_cable_connections.sh

RUN groupadd collectd-group && adduser -G collectd-group collectd-user

USER collectd-user

CMD collectd -f

あとはサイドカーで collectd を起動します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rails-app
spec:
~~~
    spec:
      containers:
        - name: rails-app
           image: xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hatappi/rails
        ~~~
        - name: collectd
          image: xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hatappi/collectd

うまくいけば

うまくいけばこんな感じにメトリクスがとれて可視化できます。

f:id:hatappi1225:20191128000121p:plain