Lambdaが6vCPUまで拡張されたので性能検証してみました

こんにちは

某SIerでソリューションアーキテクトをしております、たくまです。
主にクラウドの導入についてお客様を支援させて頂いており、2020年にAWSさんよりAPN Ambassador選出頂きました。

日本のAPN Ambassadorによる[Japan APN Ambassador Advent Calendar 2020](https://qiita.com/advent-calendar/2020/japan-apn-ambassador)の15日目の記事となります。Ambassadorって何？！という方はこちらの記事を参考にして下さい。

[APN Ambassadorってなんだ？](https://qiita.com/keizoa/items/343af854447f53f7570b)

re:Invent 2020期間中という事もあり、大好きなLambdaについてもアップデートがあり色々触ってみましたのでそちらの内容を記事にしたいと思います。

尚、投稿内容は私個人の意見であり、所属企業・部門見解を代表するものではありません。
本記事は2020/12/15時点の情報を元に記載しており、最新の情報は公式ドキュメント等で確認お願いします。
#2020/11/30までのLambdaの割り当てメモリの考え方について
みなさんは、Lambdaのメモリの割り当てってどうされていますか？

1. Lambdaは実際に実行された時間にのみ課金される
2. メモリ増やすとCPU性能もあがるが料金は高くなる
3. 性能が上がると実行時間も短くなる
4. 性能を上げても実行時間が短くなるので料金的同じになる事もある。

という事を考えながら、メモリを調節して実行時間を見ながら決めていたという方は多いのではないでしょうか？

例えばメモリを調節て実行時間がこの様な感じになった場合

|メモリ|実行時間|
|---|---|
| 128MB|400ms|
| 256MB|200ms|
| 512MB|100ms|
|1024MB| 50ms|
|2048MB| 50ms|

課金単位が100msなので512MBまでは料金的には同じになります。また、速度的には、1024MBで頭打ちになっています。
大体こんな感じのメモリと実行時間の関係になる事が多いので、どちらかを選択されるケースが多かったのではないでしょか？

- 課金単位が100msなので料金と性能の事考えて512MBを選択
- 性能を求めるので速度が頭打ちになる1024MB選択

#Lambdaのアップデートについて

Lambda関係のアップデートが色々ありますが、この記事に関係して、以下2つのアップデートがありました。

[AWS Lambdaが1ミリ秒の課金単位に変更](https://aws.amazon.com/jp/blogs/aws/new-for-aws-lambda-1ms-billing-granularity-adds-cost-savings/)
[AWS Lambdagが最大10GBのメモリと6vCPUに拡張](https://aws.amazon.com/jp/blogs/aws/new-for-aws-lambda-functions-with-up-to-10-gb-of-memory-and-6-vcpus/)
これにより

- 100ms以下の応答速度にする為に、メモリ(cpu)を割り当ててもトータル費用は上がらなくなった
- cpuの性能アップが一定以上になるとvCPUの数が増えるという方向性になる

という形になり、性能チューニングやコストを考慮したメモリの割り当ての方法が今までと変わったのでは？と感じました。
具体的には、100ms以下のチューニングも費用的に意味があるという点とマルチコアになるのでマルチコアを活かすような処理についても考える必要が出てきた事などです。
今回その辺りを検証してみました。
#Lambdaの割り当てメモリとvCPUの関係
アップデートでは、最大6vCPUと記載がありましたが、具体的にメモリどれだけで何vCPUになるのでしょうか？公式ドキュメントを確認しましたが、以下記載しかありませんでした。
[[公式]Lambda関数メモリの設定](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-memory.html)
>1,769MBの場合、1vCPUに相当します

公式ドキュメントには無さそうだったのでPythonの「multiprocessing.cpu_count()」を利用してLambdaのメモリを変えてvCPUの数を出力してみました。
公式情報ではないので、仕様変更等あるかもしれませんが以下参考にして下さい。　（2020/12/12のオレゴンリージョンでの検証結果です。）

|割り当てメモリ|vCPU|
|---|---|
|128MB～1769MB|1vCPU*|
|1770MB～3008MB|2vCPU|
|3009MB～5307MB|3vCPU|
|5308MB～7076MB|4vCPU|
|7077MB～8845MB|5vCPU|
|8846MB～10240MB|6vPCU|
*cpu_countでは、2vCPUと出力されますが、ドキュメントの記載と性能検証結果から内部的に1vCPUしか使えてない動きに見えます。


#性能検証1(シングルタスク、マルチスレッド、マルチプロセス）
3009MB以上のCPUの性能アップはvCPUが増える形になるので、シングルタスクの様な処理は、そこで頭打ちになるのでは？それ以上性能を上げる場合は、マルチコアが有効活用できるような処理にしないと上がらないのでは？
という事で、検証してみました。
####処理概要：30番目のフィボナッチ数列を4回求める

それぞれ以下で実行してみました。（実際のコードは記事の最後に記載しています。）

- シングルタスク
- マルチスレッド処理
- マルチプロセス処理

###性能検証1　結果

メモリサイズ|vCPU数|シングルタスク|マルチスレッド|マルチプロセス
---|---|---|---|---
128|1*|22,357.25|24,526.17|22,601.75
256|1*|11,103.38|12,096.86|11,613.07
512|1*|5,554.15|5,783.73|5,675.32
1024|1*|2,737.14|2,913.90|2,792.90
1536|1*|1,859.68|1,909.14|1,880.79
1769|1*|<font color="Red">1,576.30|<font color="Red">1,691.91</font>|1,597.14
2048|2|1,574.19|1,626.24|1,370.34
3008|2|1,590.36|1,643.64|950.26
3009|3|1,621.39|1,639.41|940.40
4096|3|1,574.45|1,590.55|722.13
5120|3|1,578.06|1,633.17|637.16
6144|4|1,547.85|1,656.60|484.49
7076|4|1578.67|1653.11|<font color="Red">403.14</font>
7168|5|1,606.02|1,627.12|402.30
8192|5|1,602.95|1,654.36|402.57
9216|6|1,577.55|1,633.96|420.52
10240|6|1,591.83|1,640.31|407.27

<font color="Red">赤字部分</font>が性能が頭打ちになったポイントです。

当たり前の話ですがシングルタスクやマルチスレッド化を行っても内部的に1コアしか有効に使えていないようです。
vCPUが増える形で性能拡張になるポイントで性能が頭打ちになってると思われます。

cpu_count()では、128MB～3008MBまでは2vCPUに見えますが、結果を見る限り1769でシングル処理の性能限界がきているので、公式ドキュメントの「1,769MBの場合、1vCPUに相当」記載の通り、1769までが1vCPU相当、それ以上が2vCPU相当になっているように見えました。

逆にマルチプロセス処理化していれば、それ以上も性能アップしますが、プロセス数＝＜vCPUの数の所が限界でそれ以上は頭打ちになりました。

#検証2(プロセス数の増減)

検証1ではシングルタスク、マルチスレッド、マルチプロセスの比較でしたが、今度はマルチプロセス処理でプロセス数を変更して性能検証してみました。
####処理概要：30番目のフィボナッチ数列プロセスごとに求める。

それの処理で計算量を合わせた方がよかったのですが、上記で検証しました。（ですので絶対的な計算量は、プロセス数が多い方が多いです）

###性能検証2　結果


メモリサイズ|vCPU数|プロセス3|プロセス4|プロセス6|プロセス8|プロセス12
---|---|---|---|---|---|---
128|1*|17,170.78|22,601.75|34,307.67|45,027.37|67,933.81
256|1*|8,469.28|11,613.07|17,009.97|22,894.79|34,513.40
512|1*|4,237.92|5,675.32|8,498.68|11,360.69|17,194.66
1024|1*|2,138.52|2,792.90|4,218.83|5,620.41|8,468.93
2048|2|1,088.32|1,370.34|2,037.55|2,817.35|4,222.83
4096|3|964.51|722.13|1,064.67|1,423.73|2,099.09
5120|3|440.11|637.16|853.15|1,132.36|1,685.33
5307|3|<font color="Red">412.64</font>|607.87|-|-|-
6144|4|401.42|484.49|707.66|954.88|1,402.62
7076|4|-|<font color="Red">403.14</font>|-|-|-
7168|5|411.62|402.30|714.30|846.54|1,220.98
8192|5|398.72|402.57|649.03|767.90|1,089.49
9216|6|402.85|420.52|470.74|673.93|947.82
10240|6|400.56|407.27|<font color="Red">424.13</font>|<font color="Red">642.19</font>|<font color="Red">870.46</font>

こちらの結果からもプロセス数以上のvCUPを割り当てても早くなりませんでした。逆に7プロセス以上で処理させてもlambdaの現在の割り当てMAXが6vCPUなのでそこまで並列化する意味はあまり無いという事が分かりました。

#最後に
コストメリットが無いのでと今まで、100ms以上の性能アップを諦めていた方もいるかもしれませんが今回のアップデートを元に性能を突き詰めてみてはいかがでしょうか？

例えば、自分が関わったPJでS3にアップロードされたExcelファイルのデータ(複数シート)をDynamoDBに書き込むという様なLambdaがありました。
なかなか性能アップが難しかったのですが、シートごとマルチプロセスで分散処理する様なコーディングに変更する事で早くなるのかなと思います。


#検証したpythonコード
##シングルタスクのコード
```python:single-task.py
def lambda_handler(event, context):
    fibonacci_num=int(30)
    
    s0=fibonacci(fibonacci_num)
    s1=fibonacci(fibonacci_num)
    s2=fibonacci(fibonacci_num)
    s3=fibonacci(fibonacci_num)

    return 0

#計算処理(フィボナッチ取得)
def fibonacci(n):
    if n < 2 :
        return n
    else:
        return fibonacci(n-2) + fibonacci(n-1)
```
##マルチスレッドのコード
```python:multi-thread.py
import threading

def lambda_handler(event, context):
    fibonacci_num=int(30)
    
    # スレッド生成
    th0 = threading.Thread(target=fibonacci, args=(fibonacci_num,))
    th1 = threading.Thread(target=fibonacci, args=(fibonacci_num,))
    th2 = threading.Thread(target=fibonacci, args=(fibonacci_num,))
    th3 = threading.Thread(target=fibonacci, args=(fibonacci_num,))

    #スレッド開始
    th0.start()
    th1.start()
    th2.start()
    th3.start()
 
    #スレッド待ち合わせ   
    th0.join()
    th1.join()
    th2.join()
    th3.join()

    return 0

#計算処理(フィボナッチ取得)
def fibonacci(n):
    if n < 2 :
        return n
    else:
        return fibonacci(n-2) + fibonacci(n-1)
```

##マルチプロセスのコード
```python:multi-process.py
import multiprocessing

def lambda_handler(event, context):
    fibonacci_num=int(30)
    
    # プロセス生成
    p0 = multiprocessing.Process(target=fibonacci, args=(fibonacci_num,))
    p1 = multiprocessing.Process(target=fibonacci, args=(fibonacci_num,))
    p2 = multiprocessing.Process(target=fibonacci, args=(fibonacci_num,))
    p3 = multiprocessing.Process(target=fibonacci, args=(fibonacci_num,))
    
    # プロセス開始
    p0.start()
    p1.start()
    p2.start()
    p3.start()
    
    # プロセス終了待ち合わせ
    p0.join()
    p1.join()
    p2.join()
    p3.join()

    return 0

#計算処理(フィボナッチ取得)
def fibonacci(n):
    if n < 2 :
        return n
    else:
        return fibonacci(n-2) + fibonacci(n-1)
```