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に変わっていること以外は、基本は大きく変わってなさそうなので、拡張性もきっと維持されていることでしょう。
欲しい~
勾配ブースティング (Gradient Boosting) について勉強する
はじめに
機械学習での回帰や分類問題に適用されるアルゴリズムで、勾配ブースティング (Gradient Boosting) とやらがなかなかよいらしいというのをいろんなところで見かけたのでちょっとお勉強。基本的には同じっぽいので、以下では回帰問題を前提にして考える。
なお、以下の記事が非常にわかりやすかったので、以下の記事を読んで理解できる方は私の記事を見る必要はありません。
woodyzootopia.github.io
勾配ブースティングを理解する上でのキーワード
- アンサンブル学習
- 回帰木
の二つ。
アンサンブル学習
アンサンブル学習は、複数の(あまり精度のよくない)学習器を組み合わせることで、全体として高い精度の学習器を作り出す仕組みのことです。詳細は以下などをご覧ください。
アンサンブル学習には、その組み合わせ方によりいくつかの手法があるようです。勾配ブースティングはその名の通り、アンサンブル学習の中のブースティングという手法を用います。ブースティングではまずある学習器で学習をやった後、残った誤差に対して、新たな学習を行う、というのが基本的な流れ。
勾配ブースティングでは、誤差の勾配にフィットさせるような学習器を作った後に、勾配降下法的な流れで繰り返し学習させていくようです。
回帰木
勾配ブースティングの基本的な基本的な考え方では、使用する学習器は任意のものを使うことができるようですが、ほとんどのケースで回帰木を使うようです。
回帰木の説明も世の中にたくさんありますが、以下のようなPythonコードを参考にするとわかりやすいかもしれませんね。
https://fisproject.jp/2016/07/regression-tree-in-python/
で、上記2点を踏まえた上で、以下を読んでみると非常に面白いですヨ
Pythonのloggingでログが重複して困った話 [fbprophet]
Pythonでログが重複する話は比較的よくある模様。
自分で作ったLoggerでログが重複してしまったのであれば以下のようなものも参考になるが・・・。
uyamazak.hatenablog.com
今回はいろいろ調査した挙句、インポートして使っているライブラリであるfbprophetに原因があった。
github.com
このライブラリは以下のようにLoggerを初期化していたのです。
github.com
basicConfigを見てみると、、、Root Loggerが生成されます。
docs.python.org
自分でロガーを別途生成して制御していたのにもかかわらず、ライブラリで勝手にRoot Loggerを作るため、自分で作ったLoggerへのログ出力も重複してしまうことになったのです。。。
そういう時は、先回りしてLoggerを殺して(もしくは自分の仕様にあったLoggerを生成して)あげましょう。
Pythonのloggingか階層構造を持っているから、とも言えます。
qiita.com
ちなみに、fbprophetでは、内部的にpystanも使っており、pystanも同様のコードでLoggerを初期化してるので要注意。
Root Loggerが勝手に作られる処理を殺したければ以下のようなコードでどうぞ♪(この場合は、当然、fbprophetからログが出なくなるので、出力したい場合は適切なHandlerを設定してください。)
logging.getLogger('fbprophet').addHandler(logging.NullHandler()) logging.getLogger('pystan').addHandler(logging.NullHandler())
以上