CPUが律速となる処理を行うのに適したサーバーレス環境を選定するために,ベンチマークなどを行いました.候補はGCPのCloud FunctionsとCloud Runです.AWSやAzureなど他のプラットフォームは検証しません.
注意事項
今回の検証結果は,みーが2021/01/10に実行した結果であり,実行する時刻やリージョンなどの影響を受ける可能性があります.Cloud Functionsのドキュメントには,「 CPU 割り当てはおおよその値です。CPU クロック サイクルの実際の割り当ては、関数呼び出しによって少し変わることがあります。*1」と記載されているなど,GCPで提供されるリソースは同じサービスでも差が出る可能性があることを考慮してください.また,将来に提供されるリソースが変更される可能性があるため,この記事の結果の永続性は保証できません.
Cloud Functions
Cloud FunctionsはCPUとメモリを決まった組み合わせから選択することができます.2021/01/10現在,以下の選択肢が存在します.
メモリ | CPU | 料金/100 ms(Tier 1 料金) |
---|---|---|
128 MB | 200 MHz | $0.000000231 |
256 MB | 400 MHz | $0.000000463 |
512 MB | 800 MHz | $0.000000925 |
1,024 MB | 1.4 GHz | $0.000001650 |
2,048 MB | 2.4 GHz | $0.000002900 |
4,096 MB | 4.8 GHz | $0.000005800 |
Pricing | Cloud Functions Documentation | Google Cloud
そもそも,4.8 GHzのCPUが選べるのはとても魅力的です.調べたところベースクロックが4.8 GHzのCPUは見つかりませんでしたが,Core i7-10700Fのターボブーストの最大周波数に一致しました.こんな優秀なCPUが使えるのかと夢が膨らみます.
実際のパフォーマンスをPythonのBenchmarker.pyを用いた以下のソースコードを実行して測定してみます.
# main.py import json from cpuinfo import get_cpu_info from benchmarker import Benchmarker def run(request): with Benchmarker(2000*1000, width=20) as bench: s1, s2, s3, s4, s5 = "Haruhi", "Mikuru", "Yuki", "Itsuki", "Kyon" @bench("join") def _(bm): for i in bm: # instead of xrange(N) sos = ''.join((s1, s2, s3, s4, s5)) @bench("concat") def _(bm): for i in bm: sos = s1 + s2 + s3 + s4 + s5 @bench("format") def _(bm): for i in bm: sos = '%s%s%s%s%s' % (s1, s2, s3, s4, s5) # info = get_cpu_info() return f"CPU = {json.dumps(info, indent=2, sort_keys=True)}" # requirements.txt py-cpuinfo==7.0.0 Benchmarker==4.0.1
これをCloud Functionsにデプロイして,HTTPリクエストを送信して実行し,ログを確認します.リージョンは全てasia-northeast1にしました.
結果は以下のようになりました.なお,見やすさのため一部省略した部分があります.また,手間がかかるので一部の設定の検証を省略しました.
256 MB 400 MHz
CPU = { "arch": "X86_64", "arch_string_raw": "x86_64", "bits": 64, "brand_raw": "unknown", "count": 2, "cpuinfo_version_string": "7.0.0", "family": 6, "flags": [... ], "hz_actual": [ 2700250000, 0 ], "hz_actual_friendly": "2.7003 GHz", "hz_advertised": [ 2700250000, 0 ], "hz_advertised_friendly": "2.7003 GHz", "model": 85, "python_version": "3.8.5.final.0 (64 bit)", "vendor_id_raw": "GenuineIntel" } ## benchmarker: release 4.0.1 (for python) ## python version: 3.8.5 ## python compiler: GCC 7.5.0 ## python platform: Linux-4.4.0-x86_64-with-glibc2.27 ## python executable: /layers/google.python.pip/pip/bin/python3 ## cpu model: unknown # 2700.250 MHz ## parameters: loop=2000000, cycle=1, extra=0 ## real (total = user + sys) join 2.4157 2.1900 2.1600 0.0300 concat 2.9958 2.7100 2.7100 0.0000 format 2.9025 2.5900 2.5900 0.0000 ## Ranking real join 2.4157 (100.0) ******************** format 2.9025 ( 83.2) ***************** concat 2.9958 ( 80.6) **************** ## Matrix real [01] [02] [03] [01] join 2.4157 100.0 120.2 124.0 [02] format 2.9025 83.2 100.0 103.2 [03] concat 2.9958 80.6 96.9 100.0
1,024 MB 1.4 GHz
CPU = { "arch": "X86_64", "arch_string_raw": "x86_64", "bits": 64, "brand_raw": "unknown", "count": 2, "cpuinfo_version_string": "7.0.0", "family": 6, "flags": [... ], "hz_actual": [ 2700211000, 0 ], "hz_actual_friendly": "2.7002 GHz", "hz_advertised": [ 2700211000, 0 ], "hz_advertised_friendly": "2.7002 GHz", "model": 85, "python_version": "3.8.5.final.0 (64 bit)", "vendor_id_raw": "GenuineIntel" } ## benchmarker: release 4.0.1 (for python) ## python version: 3.8.5 ## python compiler: GCC 7.5.0 ## python platform: Linux-4.4.0-x86_64-with-glibc2.27 ## python executable: /layers/google.python.pip/pip/bin/python3 ## cpu model: unknown # 2700.211 MHz ## parameters: loop=2000000, cycle=1, extra=0 ## real (total = user + sys) join 0.6989 0.7000 0.7000 0.0000 concat 0.8304 0.8000 0.8000 0.0000 format 0.7980 0.7900 0.7900 0.0000 ## Ranking real join 0.6989 (100.0) ******************** format 0.7980 ( 87.6) ****************** concat 0.8304 ( 84.2) ***************** ## Matrix real [01] [02] [03] [01] join 0.6989 100.0 114.2 118.8 [02] format 0.7980 87.6 100.0 104.1 [03] concat 0.8304 84.2 96.1 100.0
2,048 MB 2.4 GHz
CPU = { "arch": "X86_64", "arch_string_raw": "x86_64", "bits": 64, "brand_raw": "unknown", "count": 2, "cpuinfo_version": [ 7, 0, 0 ], "cpuinfo_version_string": "7.0.0", "family": 6, "flags": [... ], "hz_actual": [ 2700397000, 0 ], "hz_actual_friendly": "2.7004 GHz", "hz_advertised": [ 2700397000, 0 ], "hz_advertised_friendly": "2.7004 GHz", "model": 85, "python_version": "3.8.5.final.0 (64 bit)", "vendor_id_raw": "GenuineIntel" } ## benchmarker: release 4.0.1 (for python) ## python version: 3.8.5 ## python compiler: GCC 7.5.0 ## python platform: Linux-4.4.0-x86_64-with-glibc2.27 ## python executable: /layers/google.python.pip/pip/bin/python3 ## cpu model: unknown # 2700.397 MHz ## parameters: loop=2000000, cycle=1, extra=0 ## real (total = user + sys) join 0.5032 0.5000 0.5000 0.0000 concat 0.5893 0.5800 0.5800 0.0000 format 0.5761 0.5800 0.5800 0.0000 ## Ranking real join 0.5032 (100.0) ******************** format 0.5761 ( 87.3) ***************** concat 0.5893 ( 85.4) ***************** ## Matrix real [01] [02] [03] [01] join 0.5032 100.0 114.5 117.1 [02] format 0.5761 87.3 100.0 102.3 [03] concat 0.5893 85.4 97.8 100.0
4,096 MB 4.8 GHz
CPU = { "arch": "X86_64", "arch_string_raw": "x86_64", "bits": 64, "brand_raw": "unknown", "count": 2, "cpuinfo_version_string": "7.0.0", "family": 6, "flags": [ ... ], "hz_actual": [ 2799983000, 0 ], "hz_actual_friendly": "2.8000 GHz", "hz_advertised": [ 2799983000, 0 ], "hz_advertised_friendly": "2.8000 GHz", "model": 79, "python_version": "3.8.5.final.0 (64 bit)", "vendor_id_raw": "GenuineIntel" } ## benchmarker: release 4.0.1 (for python) ## python version: 3.8.5 ## python compiler: GCC 7.5.0 ## python platform: Linux-4.4.0-x86_64-with-glibc2.27 ## python executable: /layers/google.python.pip/pip/bin/python3 ## cpu model: unknown # 2799.983 MHz ## parameters: loop=2000000, cycle=1, extra=0 ## real (total = user + sys) join 0.4763 0.4800 0.4800 0.0000 concat 0.6557 0.6500 0.6500 0.0000 format 0.6132 0.6200 0.6200 0.0000 ## Ranking real join 0.4763 (100.0) ******************** format 0.6132 ( 77.7) **************** concat 0.6557 ( 72.6) *************** ## Matrix real [01] [02] [03] [01] join 0.4763 100.0 128.7 137.7 [02] format 0.6132 77.7 100.0 106.9 [03] concat 0.6557 72.6 93.5 100.0
結果と考察
CPUの情報については,CPUの設定を変更しても,概ね2.7-2.8 GHzの周波数という情報が取得されました.おそらく,400 MHzの設定を行うと,2.8 GHzのCPUを使って1/7の時間を割り当てられるのだと思われます.つまり,どの設定を選択しても使用されるCPUには大きな差はないことが分かりました.
Pythonのベンチマークの結果を整理すると以下のようになりました.T*F
は,実行にかかった時間とCPUの設定の周波数を掛けて正規化した値です.値が小さい程周波数あたりの計算量が多いことを示します.
CPU | 400 MHz | 1.4 GHz | 2.4 GHz | 4.8 GHz |
---|---|---|---|---|
Memory | 256 MB | 1024 MB | 2048 MB | 4096 MB |
join | 2.4157 | 0.6989 | 0.5032 | 0.4763 |
concat | 2.9958 | 0.8304 | 0.5893 | 0.6557 |
format | 2.9025 | 0.7980 | 0.5761 | 0.6132 |
T*F join | 0.96628 | 0.97846 | 1.20768 | 2.28624 |
T*F concat | 1.19832 | 1.16256 | 1.41432 | 3.14736 |
T*F format | 1.161 | 1.1172 | 1.38264 | 2.94336 |
1.4 GHzまでは周波数と処理量が比例しますが,2.4 GHz以上の周波数では,処理量が期待されるほど増えませんでした.
CPUのモデルについて,ほとんどの設定ではFamily 6 Model 85であり,Skylake (server) と思われました.しかし,4.8 GHzの設定のみFamily 6 Model 79で,Broadwell (Server)と1世代古いCPUで実行されたことが示唆されました.concatとformatで2.4 GHzよりも4.8 GHzの方が遅くなっているのはCPUの世代の差が原因かもしれません.
4.8 GHzのFamily 6 Model 79 CPUの正体は分かりませんが,おそらくはBroadwellのインテル Xeon プロセッサーなのだと思われます.
Cloud Run
先程のmain.pyを,以下のDockerfileで関数にして,Cloud Runに Docker Imageをデプロイしました.
FROM python:3.8-slim-buster WORKDIR /app COPY . . RUN pip install functions-framework RUN pip install -r requirements.txt CMD exec functions-framework --target=run
PythonはAlpineと相性が悪いので,slim-busterを使いました.Memoryは1024Miに,リージョンはasia-northeast1にしました.
vCPU 1
CPU = { "arch": "X86_64", "arch_string_raw": "x86_64", "bits": 64, "brand_raw": "unknown", "count": 2, "cpuinfo_version_string": "7.0.0", "family": 6, "flags": [... ], "hz_actual": [ 2789450000, 0 ], "hz_actual_friendly": "2.7894 GHz", "hz_advertised": [ 2789450000, 0 ], "hz_advertised_friendly": "2.7894 GHz", "model": 79, "python_version": "3.8.7.final.0 (64 bit)", "vendor_id_raw": "GenuineIntel" } ## benchmarker: release 4.0.1 (for python) ## python version: 3.8.7 ## python compiler: GCC 8.3.0 ## python platform: Linux-4.4.0-x86_64-with-glibc2.2.5 ## python executable: /usr/local/bin/python ## cpu model: unknown # 2789.450 MHz ## parameters: loop=2000000, cycle=1, extra=0 ## real (total = user + sys) join 0.4616 0.4600 0.4600 0.0000 concat 0.5031 0.5100 0.5100 0.0000 format 0.5861 0.5800 0.5800 0.0000 ## Ranking real join 0.4616 (100.0) ******************** concat 0.5031 ( 91.7) ****************** format 0.5861 ( 78.8) **************** ## Matrix real [01] [02] [03] [01] join 0.4616 100.0 109.0 127.0 [02] concat 0.5031 91.7 100.0 116.5 [03] format 0.5861 78.8 85.8 100.0
vCPU 1で,Cloud Functionsの最高設定よりも計算速度が早いという驚きの結果が出ました.参考のため,vCPU 2でも実行してみました.
vCPU 2
CPU = { "arch": "X86_64", "arch_string_raw": "x86_64", "bits": 64, "brand_raw": "unknown", "count": 2, "cpuinfo_version_string": "7.0.0", "family": 6, "flags": [... ], "hz_actual": [ 2799990000, 0 ], "hz_actual_friendly": "2.8000 GHz", "hz_advertised": [ 2799990000, 0 ], "hz_advertised_friendly": "2.8000 GHz", "model": 63, "python_version": "3.8.7.final.0 (64 bit)", "vendor_id_raw": "GenuineIntel" } ## benchmarker: release 4.0.1 (for python) ## python version: 3.8.7 ## python compiler: GCC 8.3.0 ## python platform: Linux-4.4.0-x86_64-with-glibc2.2.5 ## python executable: /usr/local/bin/python ## cpu model: unknown # 2799.990 MHz ## parameters: loop=2000000, cycle=1, extra=0 ## real (total = user + sys) join 0.4632 0.4700 0.4700 0.0000 concat 0.4823 0.4800 0.4800 0.0000 format 0.6046 0.6000 0.6000 0.0000 ## Ranking real join 0.4632 (100.0) ******************** concat 0.4823 ( 96.1) ******************* format 0.6046 ( 76.6) *************** ## Matrix real [01] [02] [03] [01] join 0.4632 100.0 104.1 130.5 [02] concat 0.4823 96.1 100.0 125.4 [03] format 0.6046 76.6 79.8 100.0
結果と考察
vCPU毎にベンチマークの実行時間をまとめると以下のようになりました.
vCPU | 1 | 2 |
---|---|---|
join | 0.4616 | 0.4632 |
concat | 0.5031 | 0.4823 |
format | 0.5861 | 0.6046 |
1CPUしか使わないベンチマークなので,当たり前ですがvCPUは1,2でほとんど差がありませんでした.
vCPU 1の時はFamily 6 Model 79であり,Broadwell (Server)だと思われます.vCPU 2の時はFamily 6 Model 63であり,Haswell (Server)だと思われます.vCPU 2でformatが遅くなっているのはCPUの世代の差が原因かもしれません.
Cloud FunctionsとCloud Runを比較すると以下のようになりました.
CPU | Cloud Functions 1.4 GHz | Cloud Functions 4.8 GHz | Cloud Run vCPU 1 |
---|---|---|---|
join | 0.6989 | 0.4763 | 0.4616 |
concat | 0.8304 | 0.6557 | 0.5031 |
format | 0.7980 | 0.6132 | 0.5861 |
Cloud Runは最高設定のCloud FunctionsよりもCPUが高速でした.とても驚きました.気軽に作ったDockerfileですが,Cloud Functionsよりも高速なのは,Cloud Runが使っているKnativeが凄いのかもしれません.
料金の検討
CPUの速度において,Cloud Functionsの最高設定よりもvCPU 1のCloud Runの方が早いので,4,096 MB 4.8 GHzのCloud FunctionsとvCPU 1のCloud Runで,1リクエストに1000msかかるCPUが律速となる関数を1,000,000回実行するための費用を計算しました.3,107円と2,981円となり,少しだけCloud Runの方が安いものの,ほとんど同じ金額でした.
CPUが律速となるため,Cloud RunのConcurrencyは2以上に上げても意味がない点を考慮すると,費用に対する計算量のコストパフォーマンスはCloud Run = Cloud Functions 4.8GHz < Cloud Functions 1.4 GHzと考えられます.
まとめ
CPUが律速となる処理において,
Cloud Functionsは,CPUの計算量のコストパフォーマンスは1.4 GHzまで同じ.2.4 GHz以上はコストパフォーマンスが悪い.
Cloud Runの計算速度は早いが,費用のコストパフォーマンスは悪い.
という結論になりました.