仕事やプライベートで調べたことのメモ書きなど(@札幌)

仕事やプライベートで調べたこと、興味ある事のメモ書きです。2016年4月から札幌で働いてます。※このブログは個人によるもので、団体を代表するものではありません。

Azure FunctionsでPythonを使うときのグローバル変数は要注意 (従量課金)

Azure FunctionsでPythonがGAになって久しいですが、実際に使う上で注意しなければならないことがいくつかありそうです。なお、これは従量課金プランでの挙動を確認しています。

グローバル変数やクラス変数は使わない方がいい

一つは実行のされ方に由来するのですがグローバル変数の取り扱い。「Azure Functions の Python 開発者向けガイド」には、以下のような記載があります。

main() 関数が同期 (async 修飾子が付いていない) の場合、関数は asyncio スレッド プール内で自動的に実行されます。

github.com

このことが意味するところは、そもそも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 ホストのインスタンスは、受信イベントの数に基づいて動的に追加および削除されます。 このサーバーレス プランではスケーリングが自動的に行われ、関数の実行中にのみコンピューティング リソースに対して料金が発生します。

docs.microsoft.com

つまり、グローバル変数やクラス変数は、同じ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日間参加してきました。昨年は運営する側だったので全く聞くことができなかったのですが、今年は存分に聞くことができました。

www.db-tech-showcase.com

そしてなんと今年ははじめての発表というおまけつき。短い時間でしたが、きちんと準備して無事発表を終えました。普通のIT会社で働いていると、こんな発表する機会はなかなかないと思うので、これはいい経験をさせてもらっていると思います。

今回はキーノートが3日間に!

今回はキーノートが3日間ともあるという大盤振る舞い。3日間とも面白い講演ばかりで、自分の過去・現在・将来について非常に考えさせられました。20年間もIT業界にいるわけですが、ちょっと最近やはり守りに入ってしまってますね。。。。反省しなければ。

もちろんキーノート以外の講演も、全部聞けたわけではないものの、いろんな面で参考になる講演が多かったです。来年も自分で、または自分のチームで発表できるよう、業務に励んでいきたいと思いますね。

LG gram 17 (2019)を購入♪&ssd換装&メモリ増設

LG gram 17購入

このたび主に仕事用として使用するpcを購入しました!約4年ぶり。会社ではBYODが許可されており、かつ会社支給pcの選択肢が狭いのが主な理由で、スペックに寄せてDELL XPS 15にするか結構悩んだ末、軽さと画面の大きさから、LG gram 17を選択することにしました。この時期に韓国製を買うのはリスク回避的にどうなのか、というのも若干悩みましたがね。ちなみに私の要件は以下でした。

  • 15インチ以上、解像度をフルhdより細かく
  • メモリを16gbより大きく
  • cpuはcore i7以上

ssd換装とメモリ増設

たくさん記事が出ているので手順は省きますが、ssdは以下を選びました。これも韓国メーカーですねー!

  • SAMSUNG 970 EVO Plus 1TB (nvme m2)

ssdは増設でなく換装しました。つまり既存のm2 sataのディスクは外しました。遅いssdがついてるのに違和感を感じるため。体感は増設でも変わらないかもしれない?

メモリは以下です。

  • crucial 16GB DDR4-2400 SODIMM

SSDは30000円くらい、メモリは8000円くらいでしょうか。 

メモリは合計24GBです。デュアルチャネルのバランスがよくないですが、体感ほとんど差がないという記述が各所に多かったのでメモリの多さを取りました。

手順

SSDを換装することにしたのでOSの再インストールなどが必要です。私は以下の手順を採用しています。

  • 開封の儀
  • 電源入れて回復ディスク作成(usbメモリ16gb使用)
  • 分解
  • メモリ増設
  • SSD交換(元のsata ssdが刺さっていたところに刺した)
  • 組立て
  • 回復ドライブからosインストール

f:id:takumats:20191001025732p:plain

無事インストール完了♪

f:id:takumats:20191001025844p:plain

SSDも認識

次メモリが足りなくなるころには、32GBのモジュールが安くなってることを期待。

2020/02/27追記

新しいモデルが出ましたね!CPUが第10世代のCore i7に変わっていること以外は、基本は大きく変わってなさそうなので、拡張性もきっと維持されていることでしょう。

欲しい~

はじめてのGARMIN


f:id:takumats:20190903233041j:image

北海道マラソンを、初めてのGARMINと走りきりました!245というモデルで、心拍計がついてるやつです。4時間切れなかったのが悔しいところ。

今までトレーニングでも心拍とかを意識したことはなかったけれど、測定できると気になるもんですね。

デフォルトの設定で1kmごとにラップを計測してくれるけれど、数キロでずれてするので、手動でもラップボタンを時々押しながら、という感じですね。

これ以来、トレッドミルでも腕時計つけながら走ってみたり。しばらく遊んでみます!

今度こそ頑張るぞっと。

北海道マラソン近し

北海道マラソンを今週末に控えてます。今年の東京マラソンで久々にサブ4を行けたので、今回の北海道マラソンも調子に乗って目指したいところ。ただ北海道マラソンは暑いのだけど。。

ちなみに今年の東京マラソンは以下の記事を参考にチャレンジしたのでした。

https://toyokeizai.net/articles/-/159771

多くの記事がイーブンペースとか後半のペースアップを推奨してるんですが、この記事は納得させられることが多かったので乗ってみました。結果的には後半足がつらなかったのもありいい記録。今回も頑張りたいです!

勾配ブースティング (Gradient Boosting) について勉強する

はじめに

機械学習での回帰や分類問題に適用されるアルゴリズムで、勾配ブースティング (Gradient Boosting) とやらがなかなかよいらしいというのをいろんなところで見かけたのでちょっとお勉強。基本的には同じっぽいので、以下では回帰問題を前提にして考える。

なお、以下の記事が非常にわかりやすかったので、以下の記事を読んで理解できる方は私の記事を見る必要はありません。
woodyzootopia.github.io

勾配ブースティングを理解する上でのキーワード

  • アンサンブル学習
  • 回帰木

の二つ。

アンサンブル学習

アンサンブル学習は、複数の(あまり精度のよくない)学習器を組み合わせることで、全体として高い精度の学習器を作り出す仕組みのことです。詳細は以下などをご覧ください。

www.codexa.net

アンサンブル学習には、その組み合わせ方によりいくつかの手法があるようです。勾配ブースティングはその名の通り、アンサンブル学習の中のブースティングという手法を用います。ブースティングではまずある学習器で学習をやった後、残った誤差に対して、新たな学習を行う、というのが基本的な流れ。
勾配ブースティングでは、誤差の勾配にフィットさせるような学習器を作った後に、勾配降下法的な流れで繰り返し学習させていくようです。

回帰木

勾配ブースティングの基本的な基本的な考え方では、使用する学習器は任意のものを使うことができるようですが、ほとんどのケースで回帰木を使うようです。
回帰木の説明も世の中にたくさんありますが、以下のようなPythonコードを参考にするとわかりやすいかもしれませんね。

https://fisproject.jp/2016/07/regression-tree-in-python/


で、上記2点を踏まえた上で、以下を読んでみると非常に面白いですヨ

woodyzootopia.github.io

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())

以上