re:Invent 2019に行ってきました
昨年の12月に縁あってAWSの年次カンファレンス re:Invent 2019 (@ラスベガス) に行ってきました!
ブログは会社のページにも書いたので。
www.insight-tec.com
会社のブログに書いてない所感としては超たくさんの人が集まっている巨大カンファレンスの割には、そこまでラスベガスがre:Invent一色になっていないのが、ラスベガスの町の大きさを実感しました。
そして会場が分散していて本当にたくさんたくさん歩くので、健脚でないとつらいかも。。
Azure Functions (Python)での並列実行について気にしておいた方がよさそうなこと (従量課金)
Azure Functions (Python, 従量課金)を実行していて、いまいち並列に実行してくれない、など感じたことはないでしょうか?私はありました。で簡単なテストコードで実行してみたりすると、直列に実行されているように見えたり、並列に実行されているように見えたり・・・。いまいち再現性もよくわからず、とりあえず放置してました。作ったFunctionsがそんなに頻繁に実行されるものでなければ放置しておいても問題ないのですが、ある程度の頻度で実行されるようなものになるとそうもいきません。と思い、最近調べてわかった(と思っている)ことをまとめておきます。
Azure Functions (Python, 従量課金)使用時に気を付けること
1VMあたり(基本的に)1プロセスしか実行されない
コンカレンシー
既定では、Functions Python ランタイムで一度に処理できる関数の呼び出しは 1 つだけです。
既定ではPythonは1VMあたりに1関数のみの実行が行われます。複数の関数呼び出しが直列に処理されるように見えるときは、同じVMでの実行がスケジュールされていると思われます。たくさんのリクエストが存在し、VMが新たに起動できるような状態になると、並列に稼働することが可能となりますが、後述の通り、このVM生成を制御する術はありません。
VMは最大で200までスケールアウト(VM追加)される
スケーリングは以下の仕様となっており従量課金プランでは200VMまでスケールアウトされることになっています。
スケーリングの動作について
スケーリングはさまざまな要因によって異なる可能性があり、選択したトリガーと言語に基づいて異なる方法でスケールします。 スケーリング動作には、注意が必要な複雑な作業がいくつかあります。
1 つの関数アプリは、最大 200 インスタンスまでしかスケールアップできません。 1 つのインスタンスで一度に複数のメッセージや要求を処理できるので、同時実行の数に上限は設定されていません。
HTTP トリガーの場合、新しいインスタンスは、1 秒ごとに最大 1 回しか割り当てられません。
非 HTTP トリガーの場合、新しいインスタンスは、30 秒ごとに最大 1 回しか割り当てられません。
この記述には「 1 つのインスタンスで一度に複数のメッセージや要求を処理できる」とありますが、前述の通り、pythonでは1VMあたり1プロセスしか処理されない仕様になっているため、最大でも200並列しか実現できないということになります。なお、これは1関数アプリの制限(1関数アプリ内の複数の関数はこれらを共有)のようなので、なるべく並列に動かすには、作成する関数は、それぞれ別々の関数アプリを作成してデプロイした方がよさそうです。
同じ関数アプリにテスト コードと運用環境のコードを混在させない
Function App 内の関数はリソースを共有します。 たとえば、メモリは共有されます。 運用環境で Function App を使用している場合は、テストに関連する関数およびリソースを追加しないでください。 これが原因で、運用環境のコードの実行中に予期しないオーバーヘッドが発生する可能性があります。
VMは自動で追加されるが追加され具合を制御する手段がない
従量課金プランを使用する場合、Azure Functions ホストのインスタンスは、受信イベントの数に基づいて動的に追加および削除されます。 このサーバーレス プランではスケーリングが自動的に行われ、関数の実行中にのみコンピューティング リソースに対して料金が発生します。 従量課金プランでは、構成可能な期間が経過すると関数の実行はタイムアウトします。
実行時のスケーリング
Azure Functions は "スケール コントローラー" と呼ばれるコンポーネントを使用して、イベント レートを監視し、スケールアウトとスケールインのどちらを実行するかを決定します。 スケール コントローラーは、トリガーの種類ごとにヒューリスティックを使用します。 たとえば、Azure Queue Storage トリガーを使用した場合、拡大縮小はキューの長さや最も古いキュー メッセージの経過時間に基づいて実施されます。
自動でスケールしてくれるのは確かにお手軽なのですが、このスケールについて制御する手段がありません。Azure FunctionisではVMの実行ホストやツールなどを含め数多くがオープンソースで提供されているが一つの特徴でもあるのですが、このスケールコントローラーのソースコードは公開されておらず、どのくらいリクエストやキューがたまったらVMが追加されるのかの制御ができないようです。
従量課金プランの Functions ホストの各インスタンスは、1.5 GB のメモリと 1 個の CPU に制限されています。
VMのサイズはさほど大きくないので、処理実行時には注意が必要です。
関数は入力を処理して出力を生成する Python スクリプト内のステートレスなメソッドであること
以下に記載の通りです。別の投稿にも書きましたが、同じVM内で実行される際には、グローバル変数やクラス変数が共有されるため、それらの値を前提にしたり、Singletonな何かを作ったり使ったりすると予期せぬ挙動となる可能性があります。
プログラミング モデル
Azure Functions では、関数は入力を処理して出力を生成する Python スクリプト内のステートレスなメソッドであることが求められます。
キュートリガー使用時にはbatchSizeを1にした方がよさそう
batchSizeが規定(16)のままだと、普通に複数のキューを処理しようとしてしまうのですが、上述のように、1VMでは1 Functionsずつしか実行されないという制限があるため、処理を待たされることになってしまいます。
batchSizeを1にしておくと、1VMで複数を処理しようとはせずに、新たなVMを起動する方向に処理が働きます。が、前述の通り、httpTrigger以外でのVM生成速度は30秒に1つという仕様なので、急なキュー増加には対応できないという制限があります。これに対する効果的な対処は私は今のところはまだわかっていません・・・。キューストレージを使って、1000メッセージくらい追加してみましたが、平均処理速度は、25並列くらいでした。(実際に何VMまでスケールされたかはわからなかったのですが、確認する手段があるのかどうかも不明です・・・)
ちなにに、このqueueTriggerのオプションであるvisibilityTimeoutは、この投稿作成時点ではバグがあり、30秒などと設定しても、ハードコードされた10分で動作するようです。キューの取り出しに失敗すると10分待たされるという・・・・。ことが、制御できません。githubでは修正のpull requestが出ているようですが、何かの問題からか、取り込みが放置されています。。
Azure FunctionsでPythonを使うときのグローバル変数は要注意 (従量課金)
Azure FunctionsでPythonがGAになって久しいですが、実際に使う上で注意しなければならないことがいくつかありそうです。なお、これは従量課金プランでの挙動を確認しています。
グローバル変数やクラス変数は使わない方がいい
一つは実行のされ方に由来するのですがグローバル変数の取り扱い。「Azure Functions の Python 開発者向けガイド」には、以下のような記載があります。
main() 関数が同期 (async 修飾子が付いていない) の場合、関数は asyncio スレッド プール内で自動的に実行されます。
このことが意味するところは、そもそもAzure Functions作成する"アプリ"は、独立した"アプリ"のようにはふるまわず、どちらかというといわゆる"関数"のように実行されます。(だからAzure Functionsという?) 関数をきちんと"べき等"に書いていれば問題にはなりませんが、グローバル変数やクラス変数などを使ってコードを書くと問題になります。
Functions側コード例)
import logging import azure.functions as func X = None def main(req: func.HttpRequest) -> func.HttpResponse: logging.info('Python HTTP trigger function processed a request.') global X if X is None: X = 0 X = X + 1 return func.HttpResponse(f"X is {X}.\n")
これをデプロイしたい後に実行すると以下のように表示されます。
$ curl https://xxxxxxxxx.azurewebsites.net/api/testhttptrigger?code=xxxxxxxxx X is 1. $ curl https://xxxxxxxxx.azurewebsites.net/api/testhttptrigger?code=xxxxxxxxx X is 2. $ curl https://xxxxxxxxx.azurewebsites.net/api/testhttptrigger?code=xxxxxxxxx X is 3. $
また、これを非同期呼び出しでたくさん呼び出すとまた面白いことが起こります。(function側のコードは戻り値から改行コードをなくすように修正)
呼び出し側コード例)
import requests import urllib import asyncio END_POINT_URL='https://xxxxxxxxx.azurewebsites.net/api/testhttptrigger' API_KEY='xxxxxxxxx' HEADERS = { 'x-functions-key': API_KEY } def req(i): r = requests.post(url=END_POINT_URL, headers=HEADERS) print(r.text) async def run(loop): async def run_req(i): return await loop.run_in_executor(None, req, i) tasks = [run_req(i) for i in range(20)] return await asyncio.gather(*tasks) loop = asyncio.get_event_loop() print(loop.run_until_complete(run(loop)))
出力例)
$ python test_api_call.py X is 1. X is 1. X is 2. X is 2. X is 3. X is 3. X is 4. X is 4. X is 5. X is 5. X is 6. X is 6. X is 7. X is 7. X is 8. X is 9. X is 10. X is 8. X is 9. X is 1. [None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None] $
面白いですね。"X is 1."と複数回表示されています。
動的に新しいVMに割り当てられると挙動が異なる。
以下のドキュメントにあるように、従量課金プランではVMが動的に生成されます。
従量課金プランを使用する場合、Azure Functions ホストのインスタンスは、受信イベントの数に基づいて動的に追加および削除されます。 このサーバーレス プランではスケーリングが自動的に行われ、関数の実行中にのみコンピューティング リソースに対して料金が発生します。
つまり、グローバル変数やクラス変数は、同じVM内に割り当てられる関数では同じものを参照し、異なるVMに割り当てられた場合は異なるものを参照するという挙動のようです。
同じVMではデフォルトでは同時に1つしか実行されない
また、前述の「Azure Functions の Python 開発者向けガイド」には以下のような記載もあります。
既定では、Functions Python ランタイムで一度に処理できる関数の呼び出しは 1 つだけです。
つまり、基本的には、1VM内では同時に1ファンクションしか実行されません。したがって、先の例にあるような同じVM内で同じグローバル変数を参照するケースは、直列に実行されたことで発生したケースになります。ちなみに、従量課金プランだと最大200VMまでスケールすることができるようです。
では、先ほどの例では、処理時間が短い関数での実験でしたが、少し処理時間が長い関数を想定してsleepを入れて実行してみます。
Functions側コード例)
import logging import datetime import time import azure.functions as func X = None def main(req: func.HttpRequest) -> func.HttpResponse: logging.info('Python HTTP trigger function processed a request.') dt_start = datetime.datetime.now() global X if X is None: X = 0 X = X + 1 time.sleep(30) dt_finish = datetime.datetime.now() return func.HttpResponse(f"X is {X}. start:{dt_start} end:{dt_finish}")
先ほどの呼び出し側コードで実行してみます。
$ python test_api_call.py X is 1. start:2019-10-20 06:50:13.226000 end:2019-10-20 06:50:43.264680 X is 1. start:2019-10-20 06:50:19.663393 end:2019-10-20 06:50:49.673589 X is 2. start:2019-10-20 06:50:43.265475 end:2019-10-20 06:51:13.274649 X is 2. start:2019-10-20 06:50:49.674135 end:2019-10-20 06:51:19.707538 X is 3. start:2019-10-20 06:51:13.278349 end:2019-10-20 06:51:43.294640 X is 4. start:2019-10-20 06:51:43.296729 end:2019-10-20 06:52:13.324173 X is 5. start:2019-10-20 06:52:13.324622 end:2019-10-20 06:52:43.354648 X is 6. start:2019-10-20 06:52:43.355110 end:2019-10-20 06:53:13.384191 X is 7. start:2019-10-20 06:53:13.384767 end:2019-10-20 06:53:43.414497 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"/> <title>502 - Web server received an invalid response while acting as a gateway or proxy server.</title> <style type="text/css"> <!-- body{margin:0;font-size:.7em;font-family:Verdana, Arial, Helvetica, sans-serif;background:#EEEEEE;} fieldset{padding:0 15px 10px 15px;} h1{font-size:2.4em;margin:0;color:#FFF;} h2{font-size:1.7em;margin:0;color:#CC0000;} h3{font-size:1.2em;margin:10px 0 0 0;color:#000000;} #header{width:96%;margin:0 0 0 0;padding:6px 2% 6px 2%;font-family:"trebuchet MS", Verdana, sans-serif;color:#FFF; .....
VMの立ち上がり具合により異なった結果となりますが、実行途中でエラーとなりました。これは、実行に時間がかかりすぎていることに起因するエラーです。
まとめ
つまるところ、httpTriggerで、同時に大量のリクエストをされる可能性のある時間の長い処理をするのは、問題がありそうですので気を付けましょう。
db tech showcase Tokyo 2019への参加
今年は初めて発表
db tech showcase Tokyo 2019に3日間参加してきました。昨年は運営する側だったので全く聞くことができなかったのですが、今年は存分に聞くことができました。
そしてなんと今年ははじめての発表というおまけつき。短い時間でしたが、きちんと準備して無事発表を終えました。普通のIT会社で働いていると、こんな発表する機会はなかなかないと思うので、これはいい経験をさせてもらっていると思います。
今回はキーノートが3日間に!
今回はキーノートが3日間ともあるという大盤振る舞い。3日間とも面白い講演ばかりで、自分の過去・現在・将来について非常に考えさせられました。20年間もIT業界にいるわけですが、ちょっと最近やはり守りに入ってしまってますね。。。。反省しなければ。
もちろんキーノート以外の講演も、全部聞けたわけではないものの、いろんな面で参考になる講演が多かったです。来年も自分で、または自分のチームで発表できるよう、業務に励んでいきたいと思いますね。
LG gram 17 (2019)を購入♪&ssd換装&メモリ増設
LG gram 17購入
このたび主に仕事用として使用するpcを購入しました!約4年ぶり。会社ではBYODが許可されており、かつ会社支給pcの選択肢が狭いのが主な理由で、スペックに寄せてDELL XPS 15にするか結構悩んだ末、軽さと画面の大きさから、LG gram 17を選択することにしました。この時期に韓国製を買うのはリスク回避的にどうなのか、というのも若干悩みましたがね。ちなみに私の要件は以下でした。
ssd換装とメモリ増設
たくさん記事が出ているので手順は省きますが、ssdは以下を選びました。これも韓国メーカーですねー!
- SAMSUNG 970 EVO Plus 1TB (nvme m2)
ssdは増設でなく換装しました。つまり既存のm2 sataのディスクは外しました。遅いssdがついてるのに違和感を感じるため。体感は増設でも変わらないかもしれない?
メモリは以下です。
- crucial 16GB DDR4-2400 SODIMM
SSDは30000円くらい、メモリは8000円くらいでしょうか。
メモリは合計24GBです。デュアルチャネルのバランスがよくないですが、体感ほとんど差がないという記述が各所に多かったのでメモリの多さを取りました。
手順
SSDを換装することにしたのでOSの再インストールなどが必要です。私は以下の手順を採用しています。
次メモリが足りなくなるころには、32GBのモジュールが安くなってることを期待。
2020/02/27追記
新しいモデルが出ましたね!CPUが第10世代のCore i7に変わっていること以外は、基本は大きく変わってなさそうなので、拡張性もきっと維持されていることでしょう。
欲しい~