2014年12月31日水曜日

Yesod AuthのHashDB認証をやってみた

YesodのHashDBプラグインを利用した認証処理をやってみた

いま、最も熱いWeb FrameworkのYesod...って言いたいけど、最近、西のほうのPython遣いたちに追いやられている感はあるものの、でもやっぱYesodは最高ということで、

Yesodの認証処理を調べていて分かったことを忘れないうちにまとめてみました



Yesodの認証処理


Yesodの認証処理には複数のプラグインがありましてyesod-authというパッケージにまとめられています、主なプラグインは

  • OpenID認証
    • 試していません Orz...
     
  • BrowserID認証
    • 試していません Orz...
     
  • email認証
    • メールアドレスでアカウント発行、ログインまでを実装してあるプラグイン、アカウント新規発行時は確認メールを送信するため、サーバーでpostfixなんかのMTAを動かさなければならないのでチョット面倒、外部のメールサーバーにも出来る
     
  • Hash認証
    • 通常のアカウントとパスワードによる認証機構、仕組みを理解するのに簡単なので今回はこちらでお勉強 と言う感じで外部の認証機構をそのまま利用できる今時のプラグインが実装されている。 
 このうちHash認証でサンプルを構築してみました。

認証のサンプルを作成


とりあえずScaffoldサイトを構築する、MySQLを利用するのでその辺も起動しておく


Scaffoldの作成

cuomo@karky7 ~/Code $ yesod init
Welcome to the Yesod scaffolder.
I'm going to be creating a skeleton Yesod project for you.

What do you want to call your project? We'll use this for the cabal name.

Project name: YesodAuthSample
Yesod uses Persistent for its (you guessed it) persistence layer.
This tool will build in either SQLite or PostgreSQL or MongoDB support for you.
We recommend starting with SQLite: it has no dependencies.

    s      = sqlite
    p      = postgresql
    pf     = postgresql + Fay (experimental)
    mongo  = mongodb
    mysql  = MySQL
    simple = no database, no auth
    url    = Let me specify URL containing a site (advanced)

So, what'll it be? mysql
That's it! I'm creating your files now...
...
...

HashDB認証を構築


まず必要なパッケージをインストール
karky7 ~ # emerge -pv dev-haskell/yesod-auth-hashdb

These are the packages that would be merged, in order:

Calculating dependencies... done!
[ebuild   R   ~] dev-haskell/yesod-auth-hashdb-1.4.1.1:0/1.4.1.1::karky7  USE="doc hscolour profile {-test}" 0 KiB

Total: 1 packages (1 reinstalls), Size of downloads: 0 KiB

 * IMPORTANT: 27 news items need reading for repository 'gentoo'.
 * Use eselect news to read news items.

karky7 ~ #
上のパッケージはgentoo-haskellへmergeしていただいたのでそちらでもインストール出きるはずです

データベースへの接続設定をする


Databaseへ接続するためsettings.ymlを修正
cuomo@karky7 ~/Code $ cd YesodAuthSample
~/Code/YesodAuthSample $ git diff
diff --git a/config/settings.yml b/config/settings.yml
index 4145c02..62a7dd4 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -16,9 +16,9 @@ ip-from-header: "_env:IP_FROM_HEADER:false"
 
 database:
   user:     "_env:MYSQL_USER:YesodAuthSample"
-  password: "_env:MYSQL_PASSWORD:YesodAuthSample"
+  password: "_env:MYSQL_PASSWORD:abc12345"
   host:     "_env:MYSQL_HOST:localhost"
-  port:     "_env:MYSQL_PORT:5432"
+  port:     "_env:MYSQL_PORT:3306"
   database: "_env:MYSQL_DATABASE:YesodAuthSample"
   poolsize: "_env:MYSQL_POOLSIZE:10"

~/Code/YesodAuthSample $ 

modelの作成


ログインアカウントを管理するUser型を作成、他のモデルは削除しても構いません
User
    email Text
    password Text
    UniqueUser email
    deriving Typeable

 -- By default this file is used in Model.hs (which is imported by Foundation.hs)

ルーティングを若干修正


HomeRのPOSTを利用しないので削除

/static StaticR Static appStatic
/auth   AuthR   Auth   getAuth

/favicon.ico FaviconR GET
/robots.txt RobotsR GET

/ HomeR GET

YesodAuthインスタンスを修正


ディフォルトだとBrowserIDの認証設定になっているので、ここをHashDBプラグインへ修正する、User型をHashDBUserクラスのインスタンスへすることも忘れずに。renderAuthMessageはログイン後に出力されるメッセージをカスタマイズするためにloginMessage関数へ差し替えています。詳しくはYesod.Auth.Messageモジュールを参照してみてください(japaneseMessageとかもあるよ)

instance YesodAuth App where
    type AuthId App = UserId

    -- ログイン後のリダイレクト先
    loginDest _ = HomeR
    -- ログアウト後のリダイレクト先
    logoutDest _ = HomeR
    -- Override the above two destinations when a Referer: header is present
    redirectToReferer _ = True

    -- UserIdを取得するための関数を設定、maybeAuthId系の関数が利用可能になる
    getAuthId creds = getAuthIdHashDB AuthR (Just . UniqueUser) creds

    -- プラグインをauthHashDBへ修正
    authPlugins _ = [authHashDB (Just . UniqueUser)]

    -- ログイン後のメッセージをディフォルトから修正
    renderAuthMessage _ _ = loginMessage

    -- authManagerはそのまま
    authHttpManager = getHttpManager

-- ログイン後に出力されるメッセージ
loginMessage :: AuthMessage -> Text
loginMessage _ = "乱入ではなくログインしました"

-- Userモデルを認証用としSha1のパスワードで認証するように HashDBUserのインスタンスに設定
instance HashDBUser User where
  userPasswordHash = Just . userPassword
  setPasswordHash h u = u { userPassword = h }

Home.hsを修正


ログイン、ログアウト後のリダイレクト先を作成、POSTは使わないので削除

module Handler.Home where

import Import

-- This is a handler function for the GET request method on the HomeR
-- resource pattern. All of your resource patterns are defined in
-- config/routes
--
-- The majority of the code you will write in Yesod lives in these handler
-- functions. You can spread them across multiple files if you are so
-- inclined, or create a single monolithic file.

getHomeR :: Handler Html
getHomeR = do
  maid <- maybeAuthId
  let handlerName = "getHomeR" :: Text
  defaultLayout $ do
    setTitle "HashDB認証サンプル"
    $(widgetFile "homepage")

テンプレート類の修正


色々削除してしまったので、不要な展開変数やタグを削除

homepage.hamlet

<h2>ログインサンプル
$maybe uid <- maid
    <p>#{show uid}
        <a href=@{AuthR LogoutR}>ログアウト
$nothing
    <p>
        <a href=@{AuthR LoginR}>ログイン画面へ
homepage.julius
全て削除
 
homepage.lucius
h1 {
    text-align: center
}
h2 {
    color: #990
}

cabalファイルに依存パッケージを追加


YesodAuthSample.cabalファイルを修正

~/Code/YesodAuthSample $ git diff YesodAuthSample.cabal
diff --git a/YesodAuthSample.cabal b/YesodAuthSample.cabal
index 0bc29ae..f196287 100644
--- a/YesodAuthSample.cabal
+++ b/YesodAuthSample.cabal
@@ -79,6 +79,7 @@ library
                  , containers
                  , vector
                  , time
+                 , yesod-auth-hashdb
 
 executable         YesodAuthSample
     if flag(library-only)
~/Code/YesodAuthSample $ 

スキーマを作成


yesod develを実行すればマイグレーションして自動でスキーマを作成してくれる

cuomo@karky7 ~/Code $ cd YesodAuthSample
cuomo@karky7 ~/Code/YesodAuthSample $ mysqladmin -u root create YesodAuthSample
cuomo@karky7 ~/Code/YesodAuthSample $ yesod devel
Yesod devel server. Press ENTER to quit
Warning: The package list for \'hackage.haskell.org\' does not exist. Run \'cabal
update\' to download it.
Resolving dependencies...
Configuring YesodAuthSample-0.0.0...
Rebuilding application... (using cabal)
Starting development server...
Starting devel application
Migrating: CREATe TABLE `user`(`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,`email` TEXT CHARACTER SET utf8 NOT NULL,`password` TEXT CHARACTER SET utf8 NOT NULL)
30/Dec/2014:12:00:06 +0900 [Debug#SQL] \"CREATe TABLE `user`(`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,`email` TEXT CHARACTER SET utf8 NOT NULL,`password` TEXT CHARACTER SET utf8 NOT NULL)\" [] @(persistent-2.1.1:Database.Persist.Sql.Raw ./Database/Persist/Sql/Raw.hs:55:18)
Migrating: ALTER TABLE `user` ADD CONSTRAINT `unique_user` UNIQUE(`email`(200))
Devel application launched: http://localhost:3000
30/Dec/2014:12:00:06 +0900 [Debug#SQL] \"ALTER TABLE `user` ADD CONSTRAINT `unique_user` UNIQUE(`email`(200))\" [] @(persistent-2.1.1:Database.Persist.Sql.Raw ./Database/Persist/Sql/Raw.hs:55:18)
cuomo@karky7 ~/Code/YesodAuthSample $

ログインアカウントデータを挿入

sha1でhash化する

cuomo@karky7 ~/Code/YesodAuthSample $ mysql -u root YesodAuthSample
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 5.6.21-log Source distribution
...
...
...
mysql> INSERT INTO user(email, password) VALUES('karky7@sample.jp', sha1('karky7'));
Query OK, 1 row affected (0.37 sec)

mysql> quit
Bye

動かしてみる

cuomo@karky7 ~/Code/YesodAuthSample $ yesod devel

初期画面

ログイン前はログイン画面へのリンクを表示する


ログイン画面

「http://localhost:3000/auth/login」というURLでログイン画面が出力される、先ほどINSERTした認証でログインを実行する


ログイン完了

見た目は悪いのですがログイン完了の画面、URLもHomeRへ戻り、getAuthIdで取得できた認証情報を表示している。User情報のIDを表示しているだけですが。


こんな感じで認証ができる、ログインフォームまで作ってくれるので楽です、細かい動作や出力の調整はクラスの関数をオリジナルのものへ修正すれば可能なので、基本的にディフォルトの動作は実装されています。

コードを書いているとほとんどの悪い箇所をコンパイラが拾ってくれるので凄く楽。Haskell+Yesodが今のところ、一番のお気に入りフレームワークかな。



2014年12月7日日曜日

PythonのGILについて簡単に調べてみました

PythonのGIL


Pythonで平行処理をやろうと思った時に、いろいろやり方があるのですがどの方法が一番効率的かどうか考えた時に、いつも
「GIL」様
が、お顔をお出しになって
「じゃぁ、どうやれって言うんだよ」
って感じになりますよね?

えっ?ならない...そうですか

世間ではあまりよく思われていない「GIL」だと思いますがでも私はPythonが好きなので頑張ってみました。 今回はサンプルコードとか作ってる時間がありませんでしたので、ほとんど説明だけですので、ごめんなさい。

GIL(Global Interpreter Lock)とは



全然違います、

Pythonインタープリターが内部で利用しているスレッドの同期プリミティブで、GILを取得できたスレッドがコードの実行を行うことができます、最初はスクリプトを実行したスレッドは1つ、mainスレッドだけなのでシングルスレッド動作させた場合はGILに対しての競合は起こらずスムーズに実行されます。

試してみる


以下のサンプルコードを実行した時間を計測してみる。シングルスレッド版は単純にcount関数を2回呼ぶだけの平凡なコード、2スレッド版はマルチコアなのでスレッドを2つ作成し平行に実行するように修正を加えたコード。
コードはCPUバンドなコードなので、I/Oの要素は含んでないことに注意。

def count(n):
    while n > 0:
        n -= 1

if __name__ == '__main__':
    count(100000000)
    count(100000000)
from threading import Thread

def count(n):
    while n > 0:
        n -= 1

if __name__ == '__main__':
    t1 = Thread(target=count, args=(100000000,))
    t1.start()
    t2 = Thread(target=count, args=(100000000,))
    t2.start()
    t1.join();
    t2.join();


* 実行結果

~/Code/lwl/Python/GIL $ time python sample1.py
18.181 secs
~/Code/lwl/Python/GIL $ time python sample2.py
30.090 secs
~/Code/lwl/Python/GIL $ 

Mac Book Air Core i5 1.6GHz 2Coreで実行しましたが、2スレッドの方が遅いではないですか。

どうなってるんだよ?!


私もGILを知るまで上のようなコードを書いていました、しっかり計測したことがなかったのですがこんな感じで遅くなっていたのでしょう、機械の無駄使いですね、すいませんでした。
同一プロセス内でスレッドを複数起動しても、GILの取得で内部競合を起こしてしまい遅くなってしまっている為です。

GILの取得方法


じゃぁ、GILがどういう風に取得されるか、これはそのスクリプトがCPUバンドなのかI/Oバンドなのかでちょっと違うようです。

* スクリプトがCPUバンドの場合

上のサンプルのようにCPU時間しか使わないようなプログラムの場合、一定期間にcheckが入ります、一定期間とはPythonインタープリターが内部で持っているticksという(単位時間ではない)カウンタを持っていてそのticksが100(適当...)を計測した際にGILの取得チェックが行われます。面白いのが、一定時間ではなく、一定のコードを実行したらticksが1カウントされるのです。さらに、その区間は割り込みさえ受け付けません。

poko-no-MacBook-Air:~ cuomo$ python
Python 2.7.8 (default, Oct  2 2014, 23:45:37)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.51)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> nums = xrange(1000000000)
>>> -1 in nums
^C^C^C^C^C^CTraceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyboardInterrupt
>>>

Ctrl+Cを押してもticks区間の場合帰ってきません。なるほど


* スクリプトがI/Oバンドの場合

ブロッキングI/Oを多く発行するようなスクリプトの場合、I/O要求を出す前に、スレッドがGILをリリースしI/Oから戻った時にGILを取得するような動きをするので、最終的に、I/Oとticksのせめぎ合いのような感じになりますね、長いI/Oばかりの処理が多いとするといくらスレッドをI/O用に生成したところで戻って来るまで待つことになりますし、I/Oが早いとしてもCPUのticksで性能が出ないので、どちらにしても悪いような気がします

* あともう一つGILとシグナルについて

厄介な話として、シグナルがペンディングされた場合、シグナルハンドラーを処理できるスレッドがmainスレッドに限られてしまうこと。もしその時に複数のスレッドが実行されていた場合、それぞれのスレッドが1tick実行するたたびにcheckが実行され、そこでGILの取得と解放が検査されるようになってしまうこと、最終的にmainスレッドに制御が移るまでシグナルはペンディングされたままになってしまいます。 ようするに、シグナルが届いたからといって、優先的にmainスレッドにコンテキストスイッチしないということです。

まとめると

  • Pythonインタープリターはスレッドのスケジューラーを持っていない
  • GILを取得したスレッドに実行権限がある
  • CPUバンドの場合、一定期間のticks(コードの塊)でスレッドの実行がスイッチされる
  •  I/Oバンドの場合、I/Oの発行でGILのリリース、I/Oからの戻りでGILの取得が実行される
  • シグナルはmainスレッドしか実行できない
ちょっと簡単すぎますけどこんな感じのPythonだと思いますが、そもそも、同一プロセス(同一インタープリターといったほうがいいかな)内でThreadを生成しても性能がでないという結果に落ち着くと思いますが、皆さんは如何でしょうか?(笑)

でもmultiprocessingがあるじゃないですか


なので世のPythonistaの方達がmultiprocessingを使いなさいと教えてくれました。早い話が、forkを使った方法でGILの競合を無くしてしまえという方法です。

from multiprocessing import Process

def count(n):
    while n > 0:
        n -= 1

if __name__ == '__main__':
    p1 = Process(target=count, args=(100000000,))
    p2 = Process(target=count, args=(100000000,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()


* 実行結果
~/Code/lwl/Python/GIL $ time python sample3.py
10.061 secs
~/Code/lwl/Python/GIL $ 

そこそこ性能が出てますね、1プロセス1スレッドにしてしまえばGILを取り放題ですね。 forkして、OSのスケジューラに任せた方が性能がでるってことです、スクリプトが遅い責任をカーネルのスケジューラのせいにできるのでなにかあった時に適当な言い訳を上司に言えますよ(笑)

複数プロセスにした場合、プロセス間でデータを共有するためにIPCを利用したりしなければならないので、ちょっとその辺りが面倒になりますが、ValueクラスとかArrayクラスなどを利用すれば共有メモリでデータの共有を簡単にしてくれるのでそんなに気にならないと感じます(自分は使ったことがありませんが...)。

デメリットとすればプロセステーブルを消費してしまうのと、プロセス生成のオーバーヘッド、メモリリソースの空間的な問題ですが、昨今のコンピュータなら気にすることないレベルですね。

いろいろ、グダグダ書いてしまいしたが、やり方を変えれば性能が出せることと、GILなんか怖くないよってはなし、しゃべりすぎました、間違ってたら指摘してください...

時間があったらもうちょっと突っ込んで調べます、お許しを...PHPやりたい