前回の記事に書いたように最近 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 メトリクスに送るとかは厳しそうな感じでした。
どんな構成にするかを考える
それを考えている時 CloudWatch Agent の次のドキュメントを見つけました。
CloudWatch Agent が collectd プロトコロルにそったデータを受け取ってそれを CloudWatch になげてくれそうです。
ここで一度自分の頭の中で考えていることをぼんやり絵に書いてみます。
今回の作業の大まかな流れとしては次のようになります。
- cloudwatch agentが動作する pod を起動して Service を作って他の namespace からリクエストできるようにする
- Rails に ActionCable の connection を取得するAPIを生やす
- collectd をサイドカーで起動する
それぞれ見ていきます。
cloudwatch agentが動作する pod を起動して Service を作って他の namespace からリクエストできるようにする
まず CloudWatch Agent が起動する pod を作成する必要があるのですが、これは Container Insight をセットアップする次のドキュメントに沿って導入します。
私の場合はもともと Container Insight を利用していたので、これを使いました。
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
うまくいけば
うまくいけばこんな感じにメトリクスがとれて可視化できます。