Lambda Layerを使ってAWS LambdaのCPU使用率を計測する

# はじめに

lambdaでは通常CPU使用率を気にする必要はありませんが、パフォーマンスチューニングをして実行時間を短くしようとした際に、処理のボトルネックがIOなのかCPUなのか知りたいと思うケースがあります。

その際に簡単なデコレータを用意してCPU使用率を計測できるようにして、その機能をLambda Layerで使えるようにしてみました。

なおpython3の話です。

# 概要

- psutilを使えばCPU時間を取得できる
- 関数の実行前後でCPU時間を取得するデコレータ関数を作る
- その関数をLambda Layerとしてデプロイする
- Lambda関数でLayerをインポートしてのエントリポイントにデコテータをつける

# 詳細

## psutilを使ったデコレータ関数を用意する

[psutl](https://pypi.org/project/psutil/)を使えばCPU時間が取れます。

```python
import psutil
psutil.cputimes()
```

```bash:結果
scputimes(user=344.21, nice=2.71, system=38.98, idle=39275.96, iowait=5.9, irq=0.0, softirq=7.84, steal=13.8, guest=0.0, guest_nice=0.0)
```

`psutil.cpu_times`を使って関数実行前後にCPU時間を取得し、その差分からCPU使用率を取得するデコレータ関数を作成します。

```python
from functools import wraps
from psutil import cpu_times

def cpu_trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        before = cpu_times()._asdict()
        result = func(*args, **kwargs)
        after = cpu_times()._asdict()

        increment = {}
        for key in after.keys():
            increment[key] = after[key] - before[key]
        total = sum(increment.values())
        rate = dict( (key, value / total ) for key, value in increment.items())
        print(rate)

        return result

    return wrapper
```

`cpu_times`の戻り値は`named_tuple`なので、そのままでは各要素の値を計算し辛いです。なので`_asdict()`を読んでdict型で保存しています。
後はafterの各状態からbeforeの各状態を引いて差分を計算すれば関数実行中にどれだけCPU時間が増加したかわかります。それを元に各状態(idle, nice, user, ...)の比率を計算します。

## 作ったデコレータをLambda Layerとしてデプロイする

https://github.com/kanga333/pylambda_cputrace

作ったデコレータをSAMでデプロイする設定を上のリポジトリにあげてます。
Lambda Layerで作るPython用パッケージは`python`という名前のディレクトリに配置されるようなディレクトリ構成にする必要があります。

```
workdir(ContentUri)
└── python
    ├── psutil
    ├── psutil-5.4.8.egg-info
    └── traceutil
        ├── __init__.py
        └── cpu_trace.py
```

Lambda LayerはCloud Formationのpackageとdeployでアップロード出来ます。

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Lambda Layers to measure cpu in Python3 applications.

Resources:
  CpuTrace:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: AWSLambda-Python3-TraceUtil
      Description: Lambda Layers to measure cpu in Python3 applications.
      ContentUri: workdir
      CompatibleRuntimes:
        - python3.6
        - python3.7
      RetentionPolicy: Retain
```

```bash
aws cloudformation package \
	--template-file sam.yml \
	--s3-bucket $S3_BACKET \
	--s3-prefix $S3_PREFIX \
	--output-template-file .template.yml

aws cloudformation deploy \
	--template-file .template.yml \
	--stack-name $STACK_NAME
```

## lambdaのエントリポイントにデコレータを追加する

あとはLambdaFunctionsのLayersに先程のLayerを追加した後にエントリポイントにデコレータを記述するだけです。

```python
import json
from traceutil import cpu_trace
import time 

@cpu_trace
def lambda_handler(event, context):
    time.sleep(1)

    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```
## 結果の確認

結果はCloudwatch Logsで見れます

```bash:結果
Function Logs:
START RequestId: eff78e17-e3ee-11e8-a1bb-97928c2e9342 Version: $LATEST
{'user': 0.0, 'nice': 0.0, 'system': 0.0, 'idle': 1.0, 'iowait': 0.0, 'irq': 0.0, 'softirq': 0.0, 'steal': 0.0, 'guest': 0.0, 'guest_nice': 0.0}
END RequestId: eff78e17-e3ee-11e8-a1bb-97928c2e9342
REPORT RequestId: eff78e17-e3ee-11e8-a1bb-97928c2e9342	Duration: 1001.90 ms	Billed Duration: 1100 ms 	Memory Size: 128 MB	Max Memory Used: 25 MB	
```

## 終わりに

LambdaのCPU使用率を計算したかったのでデコレータを使って計測できるLambda Layerを作ってみました。
こういうユーティリティっぽいものにも使えてLambda Layer便利ですね。