Lambda＋Pythonでマルチプロセス化による高速化検証(キューイング処理を自前で作る)

Lambdaの処理を高速化しようと思ったのでマルチプロセス化を試した結果を残しておきます。
pythonのmultiprocessingを使う場合、[LambdaではPool／Queueが使えないという問題](https://aws.amazon.com/jp/blogs/compute/parallel-processing-in-python-with-aws-lambda/#:~:text=Python-,Parallel%20Execution,-The%20multiprocessing%20module)を回避しているので、困っている方の参考になればと思います。

# 前提

高速化対象の処理はブロッキングが発生する処理
⇒asyncio等でマルチスレッディングしても効果がない処理
⇒検証ではシンプルに1000万回ループして変数にインデックスをセットするだけという処理を10回実行

# 比較結果一覧

Lambdaはメモリに応じてvCPUの数が増減し、メモリ10GBで6つまで拡張可能となっているため、各メモリに応じた実行時間を比較してみたいと思います。

https://aws.amazon.com/jp/about-aws/whats-new/2020/12/aws-lambda-supports-10gb-memory-6-vcpu-cores-lambda-functions/

|メモリ　⇒|128MB<br>(CPUx2)|512MB<br>(CPUx2)|1GB<br>(CPUx2)|2GB<br>(CPUx2)|3GB<br>(CPUx3)|6GB<br>(CPUx4)|7GB<br>(CPUx5)|10GB<br>(CPUx6)
|--|--|--|--|--|--|--|--|--|
|シーケンシャル実行|49.405|11.891|6.135|3.422|3.410|3.503|3.601|3.470|
|マルチプロセス実行|56.760|12.692|6.502|3.364|2.456|1.391|1.069|0.869|

※単位は秒です。

### メモリ増やしただけで早くなってる？

（2022/01/13追記）
検証用のコードではメモリを大量消費するようなことをしてないはずなのにメモリ増やしただけで処理速度が速くなってるのが気になって調べてみたところ、どうやらCPUパワーもメモリに応じて上がるみたいです。
検証結果を見る限りでは2GB以降は変わらない感じですね。

> Q: コンピューティングリソースはどのように AWS Lambda 関数に割り当てられるのですか?
>
> AWS Lambda のリソースモデルでは、お客様が関数に必要なメモリ量を指定すると、それに比例した CPU パワーとその他のリソースが割り当てられま す。例えば、256 MB のメモリを指定した場合に Lambda 関数に割り当てられる CPU パワーは、128 MB のメモリを指定した場合の約 2 倍となり、 512 MB のメモリを指定した場合の約半分となります。詳細については、[関数の設定に関するドキュメント](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-function-common.html)をご覧ください。

引用：https://aws.amazon.com/jp/lambda/faqs/

### 検証詳細

<details>
<summary>検証コード</summary>
<div>

```lambda_function.py
import time
import lambda_multiprocessing as mp

def lambda_handler(event, context):
    print('params', event)
    
    is_multiprocessing = event['multiprocess']
    start_time = time.time()
    print('start time: ', start_time)
    
    if event['multiprocess']:
        print('run multiprocess')
        events = []
        for num in range(10):
            events.append({'function': _calc, 'args': []})
            
        m = mp.Multiprocessing()
        m.run(events)
        
    else:
        print('run sequential')
        for num in range(10):
            _calc()
    
    end_time = time.time()
    print('end time: ', start_time)

    print('run time: ', end_time - start_time)
    return 'complete'

def _calc():
    print('calc start')
    for num in range(10000000):
        a = num
    print('calc end')
```

マルチプロセス用のクラスです。
multiprocessing.Queue/Poolが使えないので、自前でキューイング処理を実装しています。
また、物理CPUではなくvCPUなのでオーバーコミットを防ぐために使用可能なCPUの数を並列実行可能なプロセス数として制御しています。

```lambda_multiprocessing.py
import os
import time
import multiprocessing as mp
import threading as th

class Multiprocessing:
    def __init__(self):
        self.queue = []
        # 使用可能なCPU数を並列実行可能上限とする
        # multiprocessing.cpu_count()だと使用可能なCPUではないため注意
        # 参考　https://docs.python.org/ja/3/library/multiprocessing.html#multiprocessing.cpu_count
        self.limit = len(os.sched_getaffinity(0))
        
    def run(self, events):
        print('available CPU: ', self.limit)
        threads = []
        for event in events:
            t = th.Thread(target=self._proc, args=(event,))
            t.start()
            threads.append(t)
            
        # 全スレッド終了を待つ
        for thread in threads:
            thread.join()
                
    def _proc(self, event):
        if len(self.queue) == self.limit:
            time.sleep(0.01)
            self._proc(event)
            return
        
        self.queue.append(event)
        p = mp.Process(target=event['function'], args=(event['args']))
        p.start()
        p.join()
        self.queue.remove(event)
```

</div>
</details>

<details>
<summary>比較詳細</summary>
<div>

# Lambdaの作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/0772ee85-144b-43b9-e7ea-32b34485bc8b.png)

## メモリ:128MB

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/95dfa609-0477-32c6-67cf-d1c5c91532ed.png)

</details>

### 通常実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/9796d37e-3b67-7ada-aa29-8c8a763eacda.png)

### マルチプロセス実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/855ec672-1335-0b84-e2d9-2ddf9a6d4877.png)


## メモリ:2048MB

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/d29fa004-6f73-69f2-1669-458750576d4b.png)


### 通常実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/67facbb5-5bb1-6f54-d82a-bcc189d702da.png)


### マルチプロセス実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/61ed82d3-692b-8d72-21d5-01fa646086aa.png)


## メモリ:3072MB

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/fb9d11cc-33e4-4b94-2a50-c0d4f835ad76.png)


### 通常実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/cc6ec664-356b-5aad-274a-df5e6bd624b6.png)


### マルチプロセス実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/1554f387-12dc-3688-7d8e-b7c519d239ee.png)


## メモリ:6144MB

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/b3293c3c-1a41-bdbf-49f1-09daef093ced.png)

### 通常実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/5ecf17fc-39d3-8144-3062-ddfabfb78df7.png)


### マルチプロセス実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/513255ea-52be-7626-f059-6d4412844bdc.png)


## メモリ:7168MB

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/50d3b6a2-a26f-1369-d4f5-681cc5086e97.png)


### 通常実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/29c00ff5-3d41-6bf1-fbe1-4e53c3ed2844.png)


### マルチプロセス実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/045efa96-3e41-ffb0-c0ed-6959e23370bb.png)


## メモリ:10240MB

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/39e98b25-30ca-79ac-a857-3d14f52e48be.png)


### 通常実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/6b03b1f3-174b-f1df-b902-955762d39ffd.png)


### マルチプロセス実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/d7748e72-ff97-9f98-73b9-7841c4e4d6d1.png)
</div>
</details>


# まとめ

ちゃんとCPUの数に応じて処理が高速化されました。
最大1/4程度まで処理時間が短くなっているので、コストと相談して並列化するのはありだと思います。
ただ、ある程度メモリを確保しないと逆に遅くなるという結果になったので注意が必要ですね。


検証で使用したコードは↓にも置いてあります
https://github.com/teracy164/lambda_multiprocessing_test


# 参考

https://aws.amazon.com/jp/about-aws/whats-new/2020/12/aws-lambda-supports-10gb-memory-6-vcpu-cores-lambda-functions/
https://aws.amazon.com/jp/blogs/compute/parallel-processing-in-python-with-aws-lambda/
https://dev.classmethod.jp/articles/remote-py-2-lambda-tuning/