Datadog Agentが、Dockerのメトリクスをどのように集めているかを追ってみる。


[今回のポストは、AWS-UG アドベントカレンダーの12/22日ポストです。](http://www.adventar.org/calendars/587)

ECSのプレビューは無事に入手できたのですが、[@spesnova](https://twitter.com/spesnova)氏により[「Datadog で Docker コンテナをモニタリングする」](http://qiita.com/spesnova/items/b1f2cc4e33ddf7cea285)で紹介されている方法を使ってメトリクスが収集できるかどうかを検証できるほど時間が取れなかった。なので、お題を変更してDatadog AgentがDcokerのメトリクスをどのように集めているかを探ってみることにしたいと思います。(ごめんなさい。)そして先のポストを読む際のサプリメントとして、目を通してもらえると幸いです。

## Dcokerのメトリクス

Dockerのメトリクスに関して詳しく書かれているドキィメントは、[「Runtime Metrics」](http://docs.docker.com/v1.4/articles/runmetrics/)ではないかと思います。要は、「Linuxコンテナのメトリクスは、cgroupを通して公開される」ということだど思います。従って正しいマウントポイントで、疑似（Pseudo）ファイルを開くことで、コンテナのメトリクスが集められるようになります。

![runtime_metrics.png](https://qiita-image-store.s3.amazonaws.com/0/53101/f421b235-c27c-5024-a76f-d5c0d29e10c7.png "runtime_metrics.png")


## Datadog Agentに内包されているDocker Integrationの実態

Datadogがメトリクスの収集に使っているAgentソフトウェア(Datadog Agent)は、オープンソースなのでgithubの[DataDog/dd-agent](https://github.com/DataDog/dd-agent)でソースを見ることが出来ます。リポジトリの中で、[`checks.d` -> `docker.py`](https://github.com/DataDog/dd-agent/blob/master/checks.d/docker.py)と移動するとDokcer Integration のコアな部分のソースコードを見ることができます。

### メトリクスの転送は、AgentCheckを使って

Datadog Agentからメトリクスを送信するには2つの方法があります。それらは、[AgentCheck](http://docs.datadoghq.com/guides/agent_checks/)と[DogStatsd](http://docs.datadoghq.com/guides/dogstatsd/)です。
どちらの仕組みを使ってもメトリクスとイベントの転送はできます。

ソースコードの14行目で次のようにAgentCheckのライブラリーの読み込みをしていることから、Dockerのインテグレーションでは、DogStatsD経由ではなくAgentCheck経由でメトリクスを転送していることがわかります。AgentCheckでの、メトリクス転送の詳細は、[「Writing an Agent Check」](http://docs.datadoghq.com/guides/agent_checks/)を参照してください。[(日本語訳)](http://221.249.136.53/ja/guides/agent_checks/)

```python
# project
from checks import AgentCheck
```

### Dockerに関する情報を3タイプに分けて転送

先にも述べたように、今回はAgentCheckを使ってメトリクスを転送しているので、AgentCheckを承継したクラスの中で、check関数をさがしてみる。このソースコードをざっと眺めてみると、3つのタイプの情報を転送していることがわかる。

```python
def check(self, instance):
  # Report image metrics
  self._count_images(instance)

  # Get the list of containers and the index of their names
  containers, ids_to_names = self._get_and_count_containers(instance)

  # Report container metrics from cgroups
  self._report_containers_metrics(containers, instance)

  # Send events from Docker API
  if instance.get('collect_events', True):
    self._process_events(instance, ids_to_names)
```

クラス内の関数を追っていかないと、詳細については分かりづらいと思うが、大まかには次のような情報を送信している。

1. コンテナイメージに関する情報
2. コンテナに関連したメトリクス情報
    - memory
    - cpu
    - コンテナのディスクサイズ (基本Falseで停止しています)
    - コンテナの状態(起動 or 停止)
3. コンテナに関連したイベント情報 (状態の変化)

### メトリクスの収集項目はsub-cgroupsの情報は含まない

ソースコードの18行目~48行目あたりと収集しているメトリクスのリストが定義されています。先に紹介したDockerのドキュメントを見ていると、tatal_でsub-cgroupsの情報もcgroupで収取できるようになっているのです。しかしながらDatadogのインテグレーションでは、それらをあえて取り扱わないようになっています。(初期のソースコードでは収集し転送していた形跡がありましたが、バージョン改定の過程でなくなっていったようです。)個人的な見解ですが、Datadog自体が、タグによるメトリクスの集計(統計)機能を持っているので、total_ 項目を取り扱わないと判断したのだと思います。

```python
CGROUP_METRICS = [
    {
        "cgroup": "memory",
        "file": "memory.stat",
        "metrics": {
            # Default metrics
            "cache": ("docker.mem.cache", "gauge", True),
            "rss": ("docker.mem.rss", "gauge", True),
            "swap": ("docker.mem.swap", "gauge", True),
            # Optional metrics
            "active_anon": ("docker.mem.active_anon", "gauge", False),
            "active_file": ("docker.mem.active_file", "gauge", False),
            "inactive_anon": ("docker.mem.inactive_anon", "gauge", False),
            "inactive_file": ("docker.mem.inactive_file", "gauge", False),
            "mapped_file": ("docker.mem.mapped_file", "gauge", False),
            "pgfault": ("docker.mem.pgfault", "rate", False),
            "pgmajfault": ("docker.mem.pgmajfault", "rate", False),
            "pgpgin": ("docker.mem.pgpgin", "rate", False),
            "pgpgout": ("docker.mem.pgpgout", "rate", False),
            "unevictable": ("docker.mem.unevictable", "gauge", False),
        }
    },
    {
        "cgroup": "cpuacct",
        "file": "cpuacct.stat",
        "metrics": {
            "user": ("docker.cpu.user", "rate", True),
            "system": ("docker.cpu.system", "rate", True),
        },
    },
]
```

尚、Dockerのドキュメントによると、cgroupを介してメモリーに関するメトリクスを収取する際には、次のカーネルコマンドラインパラメーターが設定されているか確認してください。オーバーヘッドの問題で、多くのディストロでデフォルトで無効化されているとのことです。

```
cgroup_enable=memory swapaccount=1
```

> Note that the memory control group adds a little overhead, because it does very fine-grained accounting of the memory usage on your host. Therefore, many distros chose to not enable it by default. Generally, to enable it, all you have to do is to add some kernel command-line parameters: `cgroup_enable=memory swapaccount=1`.

#### memory関連メトリクス

各メトリクスの詳細に関しては、Dcokerのrunning metricsの内容を抜粋を参照してもらいたい。

**cache: Datadog内 [docker.mem.cache]**
> the amount of memory used by the processes of this control group that can be associated precisely with a block on a block device. When you read from and write to files on disk, this amount will increase. This will be the case if you use "conventional" I/O (open, read, write syscalls) as well as mapped files (with mmap). It also accounts for the memory used by tmpfs mounts, though the reasons are unclear.

**rss: Datadog内 [docker.mem.cache]**
> the amount of memory that doesn't correspond to anything on disk: stacks, heaps, and anonymous memory maps.

**mapped_file: Datadog内 [docker.mem.mapped_file]**
> indicates the amount of memory mapped by the processes in the control group. It doesn't give you information about how much memory is used; it rather tells you how it is used.

**pgfault and pgmajfault: Datadog内 [docker.mem.pgfault] and [docker.mem.pgmajfault]**
> indicate the number of times that a process of the cgroup triggered a "page fault" and a "major fault", respectively. A page fault happens when a process accesses a part of its virtual memory space which is nonexistent or protected. The former can happen if the process is buggy and tries to access an invalid address (it will then be sent a SIGSEGV signal, typically killing it with the famous Segmentation fault message). The latter can happen when the process reads from a memory zone which has been swapped out, or which corresponds to a mapped file: in that case, the kernel will load the page from disk, and let the CPU complete the memory access. It can also happen when the process writes to a copy-on-write memory zone: likewise, the kernel will preempt the process, duplicate the memory page, and resume the write operation on the process` own copy of the page. "Major" faults happen when the kernel actually has to read the data from disk. When it just has to duplicate an existing page, or allocate an empty page, it's a regular (or "minor") fault.

**swap: Datadog内 [docker.mem.swap]**
> the amount of swap currently used by the processes in this cgroup.

**active_anon and inactive_anon: Datadog内 [docker.mem.active_anon] and [docker.mem.inactive_anon]**
> the amount of anonymous memory that has been identified has respectively active and inactive by the kernel. "Anonymous" memory is the memory that is not linked to disk pages. In other words, that's the equivalent of the rss counter described above. In fact, the very definition of the rss counter is active_anon + inactive_anon - tmpfs (where tmpfs is the amount of memory used up by tmpfs filesystems mounted by this control group). Now, what's the difference between "active" and "inactive"? Pages are initially "active"; and at regular intervals, the kernel sweeps over the memory, and tags some pages as "inactive". Whenever they are accessed again, they are immediately retagged "active". When the kernel is almost out of memory, and time comes to swap out to disk, the kernel will swap "inactive" pages.

**active_file and inactive_file: Datadog内 [docker.mem.active_file] and [docker.mem.inactive_file]**
> cache memory, with active and inactive similar to the anon memory above. The exact formula is cache = active_file + inactive_file + tmpfs. The exact rules used by the kernel to move memory pages between active and inactive sets are different from the ones used for anonymous memory, but the general principle is the same. Note that when the kernel needs to reclaim memory, it is cheaper to reclaim a clean (=non modified) page from this pool, since it can be reclaimed immediately (while anonymous pages and dirty/modified pages have to be written to disk first).

**unevictable: Datadog内 [docker.mem.unevictable]**
> the amount of memory that cannot be reclaimed; generally, it will account for memory that has been "locked" with mlock. It is often used by crypto frameworks to make sure that secret keys and other sensitive material never gets swapped out to disk.

**memory and memsw limits: Datadog内 [docker.mem.memory] and [docker.mem.memsw]**
> These are not really metrics, but a reminder of the limits applied to this cgroup. The first one indicates the maximum amount of physical memory that can be used by the processes of this control group; the second one indicates the maximum amount of RAM+swap.

#### cpu関連メトリクス

**user: Datadog内 [docker.cpu.user]**
> the time during which the processes were in direct control of the CPU (i.e. executing process code)

    CPUがプロセスのコードを実行している時間。

**system: Datadog内 [docker.cpu.system]**
> the time during which the CPU was executing system calls on behalf of those processes

    プレセスがsystem callsを実行している時間。

unit of time:
> Those times are expressed in ticks of 1/100th of a second.

    時間はticksで表現しされていて、1tick=1/100秒。

#### コンテナのディスクサイズ

**Datadog内 [docker.disk.size]**

コンテナが使用しているディスクサイズもコンテナに関連したメトリクスとして転送されている。50-53行目で定義されて、

```python
DOCKER_METRICS = {
"SizeRw": ("docker.disk.size", "gauge"),
}
```
221-223行目で処理できるようになっているものの、

```python
for key, (dd_key, metric_type) in DOCKER_METRICS.iteritems():
  if key in container:
    getattr(self, metric_type)(dd_key, int(container[key]), tags=container_tags)
```

159-163行目で、基本的の収集しない仕様になっている。Dockerインテグレーションの設定ファイルの記述によると、Docker自体にbugがあり、ディスクサイズを収集を有効にすると問題が発生することがあるらしい。(特にDocker1.2の場合は注意してほしい！)

```python
with_size = instance.get('collect_container_size', False)

service_check_name = 'docker.service_up'
try:
  running_containers = self._get_containers(instance, with_size=with_size)
```

#### コンテナの状態

**Datadog内 [docker.containers.running] and [docker.containers.stopped]**

更に、メトリクスを仕分けしている関数内(179-183行目)で、コンテナの状態のタグも付与しています。Datadogのタグによる集計機能を使って、インフラ内の起動インスタンスの数をカウントしたり、スクリーンボード設定に活用するのだと思われる。

```python
if container['Id'] in running_containers_ids:
  self.set("docker.containers.running", container['Id'], tags=container_tags)
else:
  self.set("docker.containers.stopped", container['Id'], tags=container_tags)
  ```

### event に関して

**コンテナの状態遷移のイベント**に関しても、ディフォルトで転送されるようになっているようです。状態遷移情報に関しては、不要なケースもあるので、conf.yamlファイルで無効にすることもできるようです。

このイベント情報に関しては、メッセージ構成(294-302行目)を見てみると、どのような内容を持ってメッセージがDatadogに登録されているか分かります。

```python
events.append({
  'timestamp': max_timestamp,
  'host': self.hostname,
  'event_type': EVENT_TYPE,
  'msg_title': msg_title,
  'msg_text': msg_body,
  'source_type_name': EVENT_TYPE,
  'event_object': 'docker:%s' % image_name,
  })
```
16行明で`EVENT_TYPE`は設定されている。

```python
EVENT_TYPE = SOURCE_TYPE_NAME = 'docker'
```
メッセージの構成の詳細をみてみるとなんとなんとなく想像ができるのだが、Datadogのダッシュボードでは"source:docker"と"msg_textの文字列"(イメージ名, host名, 状態)を組みわせて、目的のイベントを検索することになるのではないかと思われます。

```python
msg_title = "%s %s on %s" % (image_name, status_text, self.hostname)
msg_body = ("%%%\n"
    "{image_name} {status} on {hostname}\n"
    "```\n{status_changes}\n```\n"
    "%%%").format(
        image_name=image_name,
        status=status_text,
        hostname=self.hostname,
        status_changes="\n".join(
            ["%s \t%s" % (change[1].upper(), change[0]) for change in status_change])
)
```

## まとめ。

ざっと、Datadog Agent 内のdocker.py眺めて、どんな情報が収集できるか見てきました。ディフォルトの状態でも動的に動き回るDockerコンテナの状態をタグを使って集約しながら把握できる情報の内容が分かった気がします。

この先、更に高度なメトリクスが必要な場合は、各コンテナーから、Datadog Agentが起動しているコンテーナー内のDogStasD経由で、メトリクスやイベントを転送する必要があるのも分かってきました。

ここから先は、インフラの全体の構成やAWS EC2上でDockerを使っている目的にもよると思うので、ケースバイケースでニーズが発生した際に再検討してみたいと思います。

この情報を元に、EC2や、ECS上で使っているDcokerの監視が少しでも、楽になれば幸いです。