Feedforce Developer Blog

フィードフォース開発者ブログ

Datadog で dd-agent に root 権限を与えずにプロセスがオープンしているファイルディスクリプタ数のメトリクスを取得する

こんにちは、エンジニアの id:tsub511 です。 ここ数日気温の寒暖差が凄いですね。昨日あまりにも寒すぎて一度しまった冬用の布団を引っ張りだしたら、また気温が上がってきたので片付けることになりそうです。

最近、Datadog でプロセスがオープンしているファイルディスクリプタ数のメトリクスを取る必要があり、色々と考えた結果良い方法を思いついたため、今回ご紹介します。

Datadog 標準の system.processes.open_file_descriptors メトリクスを取るには root 権限が必要

Datadog では標準で、Process Check という機能を使うことで system.processes.open_file_descriptors メトリクスを取ることができます。

f:id:tsub511:20180511150551p:plain

https://docs.datadoghq.com/integrations/process/#metrics

ただし、説明文にも書いてある通り dd-agent ユーザーが実行したプロセスしかこのメトリクスを取得することが出来ません。

そのため、例えば Rails アプリケーションを動かすために puma プロセスを dev ユーザーで動かしていた場合、以下のような設定を書いても system.processes.open_file_descriptors メトリクスを取得することが出来ません。

init_config:

instances:
  - name: puma_worker
    search_string: ["puma: cluster worker"]
    exact_match: False

f:id:tsub511:20180511151024p:plain

これは何故かというと、プロセスがオープンしているファイルディスクリプタ数を取得するためには /proc/<PID>/fd 以下にアクセスする必要があるためです。

/proc/<PID>/fd ディレクトリはそのプロセスを実行したユーザーにしか read 権限がありません。

$ ls -al /proc/1/fd
ls: cannot open directory /proc/1/fd: Permission denied

$ sudo ls -al /proc/1 | grep fd
dr-x------.   2 root root 0 Apr 18 06:58 fd
dr-x------.   2 root root 0 May 11 06:12 fdinfo

そのため、dd-agent はそのメトリクスを取得できないというわけです。

ただし、dd-agent に root 権限を与えることで、閲覧は可能になります。 公式ドキュメントではそのやり方が提示されていますが、セキュリティ的にリスクがあるため、推奨はされていません。

docs.datadoghq.com

さて、この記事の内容は dd-agent に root 権限を与えずに system.processes.open_file_descriptors メトリクスを取得するということでしたが、どうやれば良いのでしょう?

DogStatsD を使う

Datadog には DogStatsD という仕組みがあります。

DogStatsD は任意のカスタムメトリクスを Datadog に送る方法の一つです。

docs.datadoghq.com

通常は以下のような言語毎のライブラリを公式が提供してくれているため、こちらを使うことで任意のカスタムメトリクスを送ることができます。

github.com

DogStatsD を通してメトリクスを送る際は、その送り側のプロセスは任意のユーザーで実行できます。

そのため、上記の例にあったように dev ユーザーが puma プロセスを実行している場合は dev ユーザーで DogStatsD にメトリクスを送るプロセスを実行すれば、 同じ dev ユーザーのため /proc/<PID>/fd への read 権限があります。

思いついてみれば簡単なことでしたね。

でもプログラミング言語で実装するのは面倒じゃない?

少し本題とは反れますが、もう少しお手軽に DogStatsD にカスタムメトリクスを送りたいな、とも思います。

そこで、調べてみたところ「DogStatsD には単純に専用のフォーマットで UDP パケットを送るだけで良い」ということを知りました。

On Linux:

vagrant@vagrant-ubuntu-14-04:~$ echo -n "custom_metric:60|g|#shell" >/dev/udp/localhost/8125

or

vagrant@vagrant-ubuntu-14-04:~$ echo -n "custom_metric:60|g|#shell" | nc -4u -w0 127.0.0.1 8125

https://docs.datadoghq.com/developers/dogstatsd/#sending-metrics

上記のように、DogStatsD のエンドポイントである localhost:8125custom_metric:60|g|#shell のようなフォーマットで UDP パケットを送ってやれば良いです。

そのため、プロセスがオープンしているファイルディスクリプタ数のカスタムメトリクスを送るには、以下のコマンドを実行すれば良いです。

$ echo -n "open_file_descriptors.puma_worker:$(ls /proc/$(pgrep -f -u dev 'puma: cluster worker' | head -1)/fd/ | wc -l):g" | nc -u -4 localhost 8125

上記のコマンドを crontab などで毎分実行してやれば open_file_descriptors.puma_worker メトリクスを送ることができます。

ただし、実際に本番で利用しているコマンドはそこまで単純ではなく、以下のようなシェルスクリプトを書いて実行しています。

#!/bin/sh

if [ $# -ne 2 ]; then
  echo "Require 2 arguments" 1>&2
  exit 1
fi

PROCESS_NAME=$1
USER=$2

# pgrep でシェルスクリプト自身のプロセスがマッチしてしまうため `grep -v` で除外する
# CentOS 6 では pgrep に -a オプションがないため注意
#
# 複数のプロセスが見つかっても無視する
PROCESS=$(pgrep -f -a -u "${USER}" "${PROCESS_NAME}" | grep -v "$0" | head -1 | cut -f 1 -d ' ')

if [ -z "${PROCESS}" ]; then
  echo "${PROCESS_NAME} does not exists" 1>&2
  exit 1
fi

ls /proc/"${PROCESS}"/fd/ | wc -l
#!/bin/bash

if [ $# -ne 3 ]; then
  echo "Require 3 arguments" 1>&2
  exit 1
fi

METRIC_NAME=$1
VARUE=$2
METRIC_TYPE=$3

echo -n "${METRIC_NAME}:${VARUE}|${METRIC_TYPE}" | nc -u -4 localhost 8125
$ crontab -l
* * * * * /path/to/send-to-dogstatsd.sh open_file_descriptors.puma_cluster_worker $(/path/to/get-open-fd.sh "puma: cluster worker" dev) g > /dev/null