Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

類似度計算と転置Indexとb-Bit Minwise Hashing

Recommend Engineでの類似度計算

RecommendEngineを作る時の話。アイテム間の相関を計算する為にユーザーの購買データからJaccard係数やCos類似度を求める手法が一般的です(アイテム×ユーザーTableと、アイテム×アイテム相関Tableが必要)。しかしアイテムの個数(N)×ユーザー数(M)の行列を作り、Nの中から2つのアイテムを取り出してそれぞれの係数や類似度を求め、それを個数分繰り返していたら行列が大きくなる程計算が大変になります。特にアイテムの購買という行為がほとんど発生しないので、購買のベクトルがほとんど0となる疎ベクトルが作られて効率が悪く感じられます。一時期はこれを回避する為にベクトル数を減らす(購買データが多いユーザーに超超限定する)事で回避していたんですが、ユーザーが偏るしデータも少なくなってしまう事を問題として認識していました。そこでデータ数を減らすよりもっと色んな方法あるっしょって事で調べてみました。


レコメンドにおける類似度計算その傾向と対策 #DSIRNLP 第4回 2013.9.1 // Speaker Deck はてなブックマーク - レコメンドにおける類似度計算その傾向と対策 #DSIRNLP 第4回 2013.9.1 // Speaker Deck
転置Indexを使う手法。特定のアイテムAを買ったUser一覧をIndexから引き、User一覧が買った商品一覧を引いて来てアイテムA以外の共起回数を計算する。この方法では共起回数の計算はそこまで大変ではなく、アイテム数とユーザー数の両方が増えても処理時間への影響が小さい(らしい)です。


b-Bit Minwise Hashing はてなブックマーク -
b-bit miniwise Hashingという手法。ハッシュ関数(MurmurHash3はてなブックマーク - MurmurHash3 - smhasher - MurmurHash3 information and brief performance results - Test your hash functions. - Google Project Hosting)を使って2つのアイテムの全ベクトル要素に対して適用し、それぞれの最小の値が一致する確率はJaccard係数と等しいという理論から導きだされます。ハッシュ関数だけ共有すれば分散処理も行ける優れもの。b-bitというのは保存するbit数の事でMurmurHash3の下位1bitで良いようです。ただしハッシュ値の衝突が生じるので衝突確率を補正した値をJaccard係数とするようです。


自分が詳しく把握していなかったのは上の2つなんですが、他に調べていて手法が見つかったらここに纏めて行こうと思います。

ブラウザ識別用Cookieを生成する「mod_oreore(仮)」を作ったった

mod_oreore(仮)

ネーミングセンスが糞すぎる@yutakikuchi_です。
アクセス履歴をLogに落として行動履歴を追いたい時はCookieに識別子を設定するのが一般的かと思います。一般的にあるCookie識別子の設定のタイミングはFWやアプリケーションのでやるというように様々パターンを見かけますが、今回はApacheのレイヤーで自動的に付与してくれるModuleを作ってみました。因に同じようなApacheModuleは幾つか存在しますが、完全なる一意性が保証されていないことやApacheのVersionで使えなかったり等、ちょっとイケテナイ感じがしたので自作してみました。※mod_oreoreとはユーザー視点で「俺だよ!俺!」っというLogに自ら足跡を残す意味で、決してオレオレ詐欺とは関係ありません。
mod_usertrack ※ 一意性に問題あり
mod_session_cookie ※ apache2.3以降で利用可能


github : mod_oreore(仮)
識別子の値にはRequestを受け付けたサーバーのIPアドレス、リクエスト時刻(タイムスタンプ:マイクロ秒)、ApacheのプロセスID、コネクションIDを重ね合わせ、最終的な出力はbase64のURLSafeな形でencodeしています。base64する前に生成した識別子を暗号化しようと思ったのですが、処理が冗長的な気がして辞めました(ソースには暗号化をそのまま残しております)。またDOS攻撃を防ぐ処理は入れていません。

設定と確認

$ sudo yum install httpd httpd-devel openssl openssl-devel
$ git clone https://github.com/yutakikuchi/apache_module.git
$ cd apache_module/mod_oreore
$ sudo apxs -i -a -c -I/usr/include/openssl -L/usr/lib64 -lcrypto mod_oreore.c
$ sudo cp conf/oreore.conf /etc/httpd/conf.d/
$ sudo vim /etc/httpd/conf.d/oreore.conf

<IfModule mod_oreore.c>
    #NameパラメータでCookie名を指定する(ここは自由に書き換える)
    Name OREORE
    #Domainパラメータの先頭は.で始める(ここは自由に書き換える)
    Domain .cookie-test.com
    #Expiresパラメータで有効期間を指定する(指定可能なのはyears,months,weeks,days,hours,minutes,seconds)
    Expires "2 years"
</IfModule>

$ sudo vim /etc/httpd/conf/httpd.conf
#以下をcombinedログに指定
LogFormat "domain:%V\thost:%h\tserver:%A\tident:%l\tuser:%u\ttime:%{%d/%b/%Y:%H:%M:%S %z}t\tmethod:%m\tpath:%U%q\tprotocol:%H\tstatus:%>s\tsize:%b\treferer:%{Referer}i\tagent:%{User-Agent}i\tresponse_time:%D\toreore_cookie:%{OREORE}C" combined

$ sudo /etc/init.d/httpd restart
$ #Webブラウザで2回アクセスしてみる
$ tail -f /var/log/httpd/access_log

domain:cookie-test.com	host:XXXXXXX	server:YYYYYYY	ident:-	user:-	time:07/Aug/2014:16:37:09 +090method:GET	path:/index.html	protocol:HTTP/1.1	status:304	size:-	referer:-	agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36	response_time:5588	oreore_cookie:id=<ここに設定された識別子が入る>&v=1

$ sudo vim /etc/httpd/conf/httpd.conf 
#消したい場合は以下を削除orコメントアウト
LoadModule oreore_module      /usr/lib64/httpd/modules/mod_oreore.so

Apache側で全て自動的に処理をしてくれるので楽ですね。(^o^)/
後は識別子毎にLog行をsortしたりするとブラウザ毎にどんな行動があったかが纏められて把握できますね。(^o^)/(^o^)/

常駐型受託開発の経験から

常駐型受託開発

久しぶりにブログを更新します。「常駐型受託開発」という言葉が正確かどうかも分からないけど、とりあえず取引先のシステムを作ってました@yutakikuchi_です。とある会社様(クライント)の新規事業立ち上げが目的で、そのクライアント様のOfficeにお邪魔しながら約1年程携わらせて頂きました。結論から言うとこの経験が凄く身になってとても良かったと感じました。(※優秀なスタッフさんが沢山いらっしゃる環境で、とても親切にして頂いた事は厚くお礼を申し上げます。)
受託やらないほうが良いぜ!論を当然否定することはしませんし、逆に強く薦めもしませんが、新規事業立ち上げに必要な事を凄く近いところで経験できたのは今後自分が事業を立ち上げる事にもプラスの材料になったと思います。今日はこの開発現場で感じた受託開発とマネジメントについて記録するだけなので、内容的には糞つまらないことかもしれませんが、どなたかのお役に立てればと思います。因に僕の会社には社員がいないので、一人で出向いていました。

常駐型受託で良かった点、残念だった点

良かった点
  • 自分と同じような受託社長さん達と知り合う事が出来た(ここ凄く大事)。また受託開発現場というのを肌で感じれた。
  • 自分の技術力の立ち位置を把握できた。また技術力よりも人間力/信頼力が仕事を得る材料である事も理解できた。
  • 常駐する事で社員さんとのコミュニケーションが密にできたし、認識の大きな間違いも少なかった。
  • 事業立ち上げフェーズから入れてもらえた事で社員さんと同等のStartUPの進め方のノウハウを身につける事ができた。
  • 0からのスタートなので、幅広く必要なシステムの根幹を作り込んだ。またその仕組みをいち早く作れるように考える頭も身につけた。
  • 注文請書、請求書等のやり取り作業を全部自分で経験できた。契約面で質問したい内容があれば直ぐに現場でも解消する事が出来た。
残念だった点
  • 常駐型+時間契約だったので労働時間の対価を得るような内容になってしまった事。またそれによって時間的な拘束が多く発生してしまった事。これにより他社からの開発を受けられなくなってしまった。
  • 受託に重きを置きすぎて自社開発に手がつけられなくなってしまった事。(僕はここの比率を途中で変更してしまったのだが、最初に決めた信念を貫く必要があると感じた。)
  • 決められた方針に反論しづらい、また自分が思った改善策や良いアイディアを率先して現場に浸透させづらい。どうしても所詮受託の意見という考えが浮かんでしまう。
  • イベントや勉強会で直接的な成果を発表しづらい。発表する場合はコアな話は出来なく、概要レベルもしくは一般論に置き換えて話す必要がある。
  • 頑張った事に対する評価についてあれこれ考えてしまう。

受託エンジニアの種別

受託エンジニアと言っても様々な背景を持った人と知り合う事ができたのでそれもプラスの材料になりました。今回の現場では受託エンジニアご本人が別の会社に所属しているか否(独立している)か、採用に紹介会社がいるのか否かという2×2の4パターンありました。凄く失礼な話かもしれませんが、この区分けによりなんとなーくのエンジニア特性が見れて取れたように思います。ここでも書けるような内容としては個人を見た場合、会社所属の人たちは要件の抽出とそれを具体化する力、独立している人達は何かやったるぜー!という野心と個性が強く見れた気がします。マネジメントをする人はご自身で特性を見つけ出して知っておくと良いかもしれません。ここはもっと詳しく書きたいんですがこの辺まで(笑)

受託の人数

現場で社員を雇えない状況で今直ぐ対応する人が欲しい場合は受託の人数を増やすのが手っ取り早いですが、その人数が増えすぎるとマネジメントが確実に行き届かなくなります。これはどんなに優秀な社員マネージャーがいたとしても受託に対する指示を一人ずつに細かく伝えるのは不可能であり、結局のところ一番コミュニケーションが発生するのは社員マネージャと受託では無く、機能担当の社員エンジニアと受託の直接になります。この社員さんのコミュニケーションコストが半端無い。仕様の伝達、ソースコードレビュー、改修依頼等、ただでさえ忙しい社員エンジニアさんもその辺のサポートに相当な工数が掛かってしまいます。今回の経験での一つの指標として、どんなに苦しい状況でも社員エンジニアの数より受託の数を増やすことはNGであり、更に言うと理想的には受託人数が社員数の半分より小さくなるように採用すべきかと思いました。もしそれでも人数対工数の折り合いが付かないのであれば、常駐型だけではなくマネジメントを含めて外部委託会社に依頼する、そもそものリリーススケジュールを見直すか機能の削ぎ落としや簡略化を図るのが得策と思います。大事なので2度言いますが、単純に常駐型受託の人数を増やすのはマネジメントのリスクが大きくなります。あと、受託に重要な機能を任せると後々いなくなってしまった時に大変な事になるので、機能の重要度で委託するかどうかを考える事が必要だと思います。

紹介業者の戦略

僕も最初は受託を1人採用するのは紹介会社1社を経由するケースしか無いのかと思ったのですが、実際には間に紹介会社を2,3社挟むケースもあってその分受託サラリーのマージンが引かれているようです。このケースで美味しい思いをしているのは間にいる紹介業者だけであり、発注元は金額が高くなるし現場で頑張っているエンジニアの労は報われません。人材だけでなく仲介業全般での当然の話なんでしょうけど、このシステムってなんとかならないんでしょうか。受託の士気を上げる為にも常識的なマージンであって欲しいと強く希望します。


紹介会社は既に送り出している受託の人から都度現場状況をヒアリングして、更に人数を増やせるか、そしてどれぐらいのレベルのエンジニアなら長期採用されるのかというのを把握しています。紹介会社としても当然長く現場にいて欲しいという希望があるので、今求められているスキルと新たに送り出せる人のマッチング具合が気になるのだと思います。ある程度の採用単価が保証される事も当然前提の一つですが、ちゃんとした技術基盤があり高い目標を常に持ち続けられるいい環境という情報がうまく紹介会社側に伝われば優秀なエンジニアが来てくれる可能性は高まります。逆に炎上状態やスキル面での低レベル感が伝わると当然紹介会社もハイエンドな人材を送り出したいとは思わなくなってしまう。後者に嵌ってしまうと現場としては完全なる悪循環ですよね。

男なら潔くC言語書けよと言われた話。〜mod_db,mod_dbdの実装〜

恩師に言われた言葉

Geek女優の池澤あやかさんに会いたいと思っている@yutakikuchi_です。
池澤さんはRubyが出来てSFCで女優さんなんて羨ましいですね〜。僕なんてRubyは得意じゃないし東京とは言えないような都心から離れた場所の地味な国立大だし、何よりお金も無いパンピーだしね〜。


僕の学生時代にもRubyはあったんですけどRailsはまだ出始めでそんなに流行っている雰囲気は無かったし、Webを書くには面倒くさいJSP/ServletPerlかって感じでした。ApacheのModuleでWebを書ける事も学生ながら知っていたんですが、ポインタ、メモリの動的確保/解放の間違いが頻発して開発効率が落ちるから極力Javaで、どうしてもCを書かなければ行けない時はC++で逃げてました。


でも学生時代に恩師に言われたんですよね、JavaPerlを書く奴はチャラいやつと。男なら潔くC言語書けよと。 (因に恩師はJavaJavascriptの違いも良く理解していなかったと思いますけどww) おそらく恩師が言いたいのは学生の時から高級言語に頼る事無く、まずはプログラミングの仕組みを理解するためにC言語で苦労してエキスパートを目指せっていう意図だったと解釈しています。


C言語をやると他の言語と違って問題の発生率が高い。 要は言語側であまり頑張ってくれないから自分で工夫する必要があります。特に他のコンポーネントに接続する時には接続数の制御/ポートの使い切り、ConnectionPoolの実装、使用メモリのオーバー、良く分からないSegmentation Fault等必ず経験します。こういった問題解決の経験値をつける事でエンジニアの実力が養われるのだと思います。


恩師の言葉を信じてCの勉強を重ねた結果、会社の開発チームに配属された時は凄く重宝されましたし、極力PHP以外を書く案件を回して貰えました。だからCを勉強して来て良かったと思ってます。しかし今後C言語はどうなるんでしょうか... 身の回りではゲーム系、広告配信エンジニア以外は触って無さそうです。その他の言語が強力で簡単WebアプリぐらいだとCで頑張って書くとか全く無いんですよね。学生の勉強の仕組み理解と違って会社は効率化や作業時間短縮を掲げてくるのでC推しも難しいですね。


前置きが長くなりましたが、それでも僕はCを触る事が多いのでこのエントリーではC言語からのDB接続について書きたいと思います。

DB接続

mysqlclientを使った自作ApacheModule mod_db.c

mysqlclient,prepared stmtを使ったApache Moduleのmod_dbは以下になります。完全なる自作なのであまり自信が無いですが、並列テストをしてもSegmentation Faultは発生しませんでした。下のソースではbindしたいparameterとresultをmemsetで設定するところが分かりづらいのと、Pointerの設定を間違えてmemory errorを起こし易いので注意が必要です。このプログラムの駄目な点はmysql_real_connectで都度Connectionしているので、接続のOverHeadが大きいところです。実際にabでbenchmarkを取ってみるとCommand Connectが多く発生していますし、netstatでも大量のTIME_WAITが出てます。都度接続の方式で頑張る場合は、TIME_WAITが残り続けると実行Port不足に陥って処理が進まなくなる可能性があるので、/etc/sysctlのnet.ipv4.tcp_tw_recycle=1に設定してリサイクルを速く回す設定を入れるか、net.ipv4.ip_local_port_range = 1024 65535のように使用可能なPortを増やすかのどちらかが有効になりそうです。ただしsysctlの設定をチューニングする場合は十分にテストすると良いと思います。

$ sudo yum install mysql-devel -y

$ sudo vim /etc/httpd/conf/httpd.conf
<IfModule mod_db.c>
DBHost localhost
DBPort 3306
DBUser root
DBPass root
DBName helloworld
DBTableName hello
</IfModule>

$ sudo apxs -i -a -c -I /usr/include/mysql -L /usr/lib64/mysql -lmysqlclient mod_db.c
$ sudo /etc/init.d/httpd restart 

$ ab -n 10000 -c 30 "http://localhost/" 
Server Software:        Apache/2.2.15
Server Hostname:        localhost
Server Port:            80

Document Path:          /
Document Length:        68 bytes

Concurrency Level:      30
Time taken for tests:   25.316 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      2352350 bytes
HTML transferred:       680680 bytes
Requests per second:    395.01 [#/sec] (mean)
Time per request:       75.948 [ms] (mean)
Time per request:       2.532 [ms] (mean, across all concurrent requests)
Transfer rate:          90.74 [Kbytes/sec] received

$ mysql> SHOW PROCESSLIST;
+------+----------------------+-----------------+------+---------+------+-------+------------------+
| Id   | User                 | Host            | db   | Command | Time | State | Info             |
+------+----------------------+-----------------+------+---------+------+-------+------------------+
|  443 | root                 | localhost       | NULL | Query   |    0 | NULL  | SHOW PROCESSLIST |
| 9824 | unauthenticated user | connecting host | NULL | Connect | NULL | login | NULL             |
| 9825 | unauthenticated user | connecting host | NULL | Connect | NULL | login | NULL             |
| 9826 | unauthenticated user | connecting host | NULL | Connect | NULL | login | NULL             |
| 9827 | unauthenticated user | connecting host | NULL | Connect | NULL | login | NULL             |
+------+----------------------+-----------------+------+---------+------+-------+------------------+

$ sudo netstat -antp
tcp        0      0 ::1:80                      ::1:50593                   TIME_WAIT   -                   
tcp        0      0 ::1:80                      ::1:52086                   TIME_WAIT   -                   
tcp        0      0 ::1:80                      ::1:52864                   TIME_WAIT   -                   
tcp        0      0 ::1:80                      ::1:51305                   TIME_WAIT   -                   
tcp        0      0 ::1:80                      ::1:52557                   TIME_WAIT   -                   
tcp        0      0 ::1:80                      ::1:53047                   TIME_WAIT   -                   
tcp        0      0 ::1:80                      ::1:49646                   TIME_WAIT   -                   
tcp        0      1 ::1:80                      ::1:53489                   FIN_WAIT1   -          

#設定を変える場合は十分にテストが必要
$ sudo vim /etc/sysctl.conf
net.ipv4.tcp_tw_recycle = 1
net.ipv4.ip_local_port_range = 1024 65535
$ sudo sysctl -p 
#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "apr_hash.h"
#include "apr_tables.h"
#include "apr_strings.h"
#include "ap_config.h"
#include "util_script.h"
#include "http_log.h"
#include "mysql/mysql.h"
#include "stdio.h"
#include "stdlib.h"

module AP_MODULE_DECLARE_DATA db_module;

typedef struct {
    int port;
    MYSQL *conn;
    MYSQL_STMT *stmt;
    char *host;
    char *user;
    char *pass;
    char *name;
    char *table_name;
} db_env;

/* The db handler */
static int db_handler(request_rec *r) {
    // connect
    db_env *db = ap_get_module_config(r->per_dir_config, &db_module);
    db->conn = mysql_init(NULL);
    if (!mysql_real_connect(db->conn, db->host, db->user, db->pass, db->name, db->port, NULL, 0)) {
        mysql_close(db->conn);
        return HTTP_INTERNAL_SERVER_ERROR;
    }
    
    char *query = (char *)apr_psprintf(r->pool,"SELECT id,title,created_at FROM %s.%s WHERE id = ?", db->name, db->table_name);
    db->stmt = mysql_stmt_init(db->conn);
    if (mysql_stmt_prepare(db->stmt, query, strlen(query)) != 0) {
        goto STMTError;
    }

    // bind
    MYSQL_BIND bind[1];
    int id = 1;
    memset(bind, 0, sizeof(id));
    bind[0].buffer = &id;
    bind[0].buffer_type = MYSQL_TYPE_LONG;
    bind[0].buffer_length = sizeof(id);
    bind[0].is_null = 0;

    if (mysql_stmt_bind_param(db->stmt,bind) != 0 || mysql_stmt_execute(db->stmt) != 0) {
        goto STMTError;
    }

    // bind_result
    MYSQL_BIND result[3];
    memset(result, 0, sizeof(result));
    int rid;
    result[0].buffer = &rid;
    result[0].buffer_type = MYSQL_TYPE_LONG;
    result[0].buffer_length = sizeof(rid);
    result[0].is_null = 0;
    char title[100];
    result[1].buffer = &title;
    result[1].buffer_type = MYSQL_TYPE_STRING;
    result[1].buffer_length = sizeof(title);
    result[1].is_null = 0; 
    MYSQL_TIME ts;
    result[2].buffer = &ts;
    result[2].buffer_type = MYSQL_TYPE_DATETIME;
    result[2].buffer_length = sizeof(ts);
    result[2].is_null = 0;

    // bind_result
    if (mysql_stmt_bind_result(db->stmt,result) != 0 || mysql_stmt_store_result(db->stmt) != 0) {
        goto STMTError;
    }
    // stmt_fetch
    while (!mysql_stmt_fetch(db->stmt)) {
        char *time_str = (char *)apr_psprintf(r->pool, "%04d-%02d-%02d %02d:%02d:%02d", ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second);
        ap_rprintf(r, "id: %d, title: %s, created_at: %s\n", rid, title, time_str);
    }
    // close
    mysql_stmt_close(db->stmt);
    mysql_close(db->conn);
    return OK;

STMTError:
    mysql_stmt_close(db->stmt);
    mysql_close(db->conn);
    return HTTP_INTERNAL_SERVER_ERROR;
}

/* make db dir */
static void *make_db_dir(apr_pool_t *p, char *d){
    db_env *db;
    db = (db_env *) apr_pcalloc(p, sizeof(db_env));
    return db;
}

/*
 * Set the value for the 'DBHost' attribute.
 */
static const char *set_db_host(cmd_parms *cmd, void *mconfig, const char *name){
    db_env *db;
    db = (db_env *) mconfig;
    db->host = ap_getword_conf(cmd->pool, &name);
    return NULL;
}

/*
 * Set the value for the 'DBUser' attribute.
 */
static const char *set_db_user(cmd_parms *cmd, void *mconfig, const char *user){
    db_env *db;
    db = (db_env *) mconfig;
    db->user = ap_getword_conf(cmd->pool, &user);
    return NULL;
}

/*
 * Set the value for the 'DBPass' attribute.
 */
static const char *set_db_pass(cmd_parms *cmd, void *mconfig, const char *pass){
    db_env *db;
    db = (db_env *) mconfig;
    db->pass = ap_getword_conf(cmd->pool, &pass);
    return NULL;
}

/*
 * Set the value for the 'DBPort' attribute.
 */
static const char *set_db_port(cmd_parms *cmd, void *mconfig, const char *port){
    db_env *db;
    db = (db_env *) mconfig;
    db->port = *(int *)ap_getword_conf(cmd->pool, &port);
    return NULL;
}

/*
 * Set the value for the 'DBName' attribute.
 */
static const char *set_db_name(cmd_parms *cmd, void *mconfig, const char *name){
    db_env *db;
    db = (db_env *) mconfig;
    db->name = ap_getword_conf(cmd->pool, &name);
    return NULL;
}

/*
 * Set the value for the 'DBTable' attribute.
 */
static const char *set_db_table(cmd_parms *cmd, void *mconfig, const char *table){
    db_env *db;
    db = (db_env *) mconfig;
    db->table_name = ap_getword_conf(cmd->pool, &table);
    return NULL;
}

static const command_rec db_conf_cmds[] = {
    AP_INIT_TAKE1("DBHost", set_db_host, NULL, OR_FILEINFO, "db hostname"),
    AP_INIT_TAKE1("DBPort", set_db_port, NULL, OR_FILEINFO, "db port"),
    AP_INIT_TAKE1("DBUser", set_db_user, NULL, OR_FILEINFO, "db username"),
    AP_INIT_TAKE1("DBPass", set_db_pass, NULL, OR_FILEINFO, "db password"),
    AP_INIT_TAKE1("DBName", set_db_name, NULL, OR_FILEINFO, "db name"),
    AP_INIT_TAKE1("DBTableName", set_db_table, NULL, OR_FILEINFO, "db tablename"),
    {NULL}
};

static void db_register_hooks(apr_pool_t *p){
    ap_hook_handler(db_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

/* Dispatch list for API hooks */
module AP_MODULE_DECLARE_DATA db_module = {
    STANDARD20_MODULE_STUFF, 
    make_db_dir,                  /* create per-dir    config structures */
    NULL,                  /* merge  per-dir    config structures */
    NULL,                  /* create per-server config structures */
    NULL,                  /* merge  per-server config structures */
    db_conf_cmds,          /* table of config file commands       */
    db_register_hooks      /* register hooks                  */
};
mod_dbdを使った自作ApacheModule mod_dbdsample.c

mod_dbの問題を回避する為にmod_dbdを利用します。利用する際はapr-util-mysqlというパッケージが必要なのでyumで取得します。mod_dbdはConnectionPoolを作成し接続を永続化しますし、PreparedStatementの機能も持ち合わせています。mod_dbdはぐぐってもさほど有益な情報が得られないので、注意してください。凄く分かりづらい箇所としてはPrepareする際のBind Parameterの指定が"?"ではなく"%s"となる事です。更にはStatementの結果となるresと取得行のrowを初期化としてNULLしておかないと子プロセスでSegmentation Faultを引き起こします。上のmod_dbに比べてPerformanceが若干改善していますが、期待していた以上の働きをしていないため何とも言えない状況でした。MysqlのCommandはSleepになって待機状態なのが分かります。またnetstatでも少ないPortがESTABLISHEDとなっています。

$ sudo yum install apr-util-mysql -y

$ sudo vim /etc/httpd/conf/httpd.conf
#有効化
LoadModule dbd_module modules/mod_dbd.so
#追記
<IfModule mod_dbdsample.c>
DBDriver    mysql
DBDParams   "host=localhost,user=root,pass=root,dbname=helloworld"
DBDPersist  ON
DBDKeep     5
DBDMax      10
DBDMin      3
DBDExptime  300
</IfModule>

$ sudo apxs -i -a -c mod_dbdsample.c
$ sudo /etc/init.d/httpd restart 

$ ab -n 10000 -c 30 "http://localhost/"
Server Software:        Apache/2.2.15
Server Hostname:        localhost
Server Port:            80

Document Path:          /
Document Length:        68 bytes

Concurrency Level:      30
Time taken for tests:   19.113 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      2352820 bytes
HTML transferred:       680816 bytes
Requests per second:    523.20 [#/sec] (mean)
Time per request:       57.340 [ms] (mean)
Time per request:       1.911 [ms] (mean, across all concurrent requests)
Transfer rate:          120.21 [Kbytes/sec] received

mysql> SHOW PROCESSLIST;
+-------+------+-----------+------------+---------+------+-------+------------------+
| Id    | User | Host      | db         | Command | Time | State | Info             |
+-------+------+-----------+------------+---------+------+-------+------------------+
|   443 | root | localhost | NULL       | Query   |    0 | NULL  | SHOW PROCESSLIST |
| 24215 | root | localhost | helloworld | Sleep   |  294 |       | NULL             |
| 24216 | root | localhost | helloworld | Sleep   |  294 |       | NULL             |
| 24217 | root | localhost | helloworld | Sleep   |  271 |       | NULL             |
| 24218 | root | localhost | helloworld | Sleep   |  294 |       | NULL             |
| 24220 | root | localhost | helloworld | Sleep   |  294 |       | NULL             |
| 24221 | root | localhost | helloworld | Sleep   |  271 |       | NULL             |

$ sudo netstat -antp
tcp        0      0 192.168.56.10:22            192.168.56.1:52664          ESTABLISHED 2161/sshd
tcp        0      0 192.168.56.10:22            192.168.56.1:52626          ESTABLISHED 2100/sshd
tcp        0      0 192.168.56.10:22            192.168.56.1:52333          ESTABLISHED 1511/sshd
tcp        0      0 192.168.56.10:22            192.168.56.1:52332          ESTABLISHED 1494/sshd
#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "http_log.h"
#include "ap_config.h"
#include "apr_dbd.h"
#include "mod_dbd.h"
#include "apr_strings.h"

typedef struct{
  apr_dbd_results_t *res;
  apr_dbd_row_t *row;
  apr_dbd_prepared_t *stmt;
  ap_dbd_t *dbd;
} struct_dbd;

static int dbdsample_handler(request_rec *r) {
  struct_dbd *db = (struct_dbd *)apr_palloc(r->pool, sizeof(struct_dbd));
  db->res  = NULL;
  db->row  = NULL;
  db->stmt = NULL;
  if((db->dbd = ap_dbd_acquire(r)) == NULL) {
    return HTTP_INTERNAL_SERVER_ERROR;
  }
  const char* query = "SELECT id,title,created_at FROM helloworld.hello WHERE id = %s";
  if(apr_dbd_prepare(db->dbd->driver, r->pool, db->dbd->handle, query, NULL, &db->stmt) != 0) {
    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error %s.", apr_dbd_error(db->dbd->driver, db->dbd->handle, errno));
    return HTTP_INTERNAL_SERVER_ERROR;
  }
  const char **params = (const char**)apr_palloc(r->pool,(1*sizeof(char *)));
  *params = "1";
  if(apr_dbd_pselect(db->dbd->driver, r->pool, db->dbd->handle, &db->res, db->stmt, 0, 1, params) != 0) {
    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error %s.", apr_dbd_error(db->dbd->driver, db->dbd->handle, errno));
    return HTTP_INTERNAL_SERVER_ERROR;
  }
  while(!apr_dbd_get_row(db->dbd->driver, r->pool, db->res, &db->row, -1)) {
    const char* id = apr_dbd_get_entry(db->dbd->driver, db->row, 0);
    const char* title = apr_dbd_get_entry(db->dbd->driver, db->row, 1);
    const char* created_at = apr_dbd_get_entry(db->dbd->driver, db->row, 2);
    ap_rprintf(r, "id: %s, title: %s, created_at: %s\n", id, title, created_at);
  }
  return OK;
}

static void dbdsample_register_hooks(apr_pool_t *p) {
  ap_hook_handler(dbdsample_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

/* Dispatch list for API hooks */
module AP_MODULE_DECLARE_DATA dbdsample_module = {
  STANDARD20_MODULE_STUFF,
  NULL,                  /* create per-dir    config structures */
  NULL,                  /* merge  per-dir    config structures */
  NULL,                  /* create per-server config structures */
  NULL,                  /* merge  per-server config structures */
  NULL,                  /* table of config file commands       */
  dbdsample_register_hooks  /* register hooks                      */
};

速いよ Java Play Framework

言語とFrameworkの選定

phpにはあまり魅力を感じていない@yutakikuchi_です。本題とは関係ありませんが4.25(金)@ヒカリエのイベントに登壇します。ネタは同窓会GrowthHackとログ集計/解析の2本立てです。興味のある方はどうぞ。【ヒカ☆ラボ】同窓会GrowthHack!×データログ集計、解析!をテーマに事例をまじえお話します! 16年ぶりの再会でも参加率6割の同窓会を開くには?Yahoo出身のエンジニアが語る、アクセスログ可視化、 ユーザ属性解析を行うためのシステム設計のコツとは? はてなブックマーク - 【ヒカ☆ラボ】同窓会GrowthHack!×データログ集計、解析!をテーマに事例をまじえお話します! 16年ぶりの再会でも参加率6割の同窓会を開くには?Yahoo出身のエンジニアが語る、アクセスログ可視化、 ユーザ属性解析を行うためのシステム設計のコツとは?


Round 8 results - TechEmpower Framework Benchmarks はてなブックマーク - Round 8 results - TechEmpower Framework Benchmarks
さて、本題に入ります。僕がphpを書き始めたのも前職のmain言語として指定されていたことがあり、あまり書いていて楽しく無いとは思っていながらも泣く泣く仕事としてやってい感じです。過去に何回かphpのエントリー書いてますけどそれも仕事で利用する為です。前職は本当にphperの集まりでWebだけじゃなくてバッチ処理スクリプト処理も全てphpで書こうとする姿勢を初めて見たときは驚愕しました。まぁ速くコードを書くならそれでもいいんでしょうけど。


言語とFrameworkの選定にはドキュメント量、言語のCommitterや精通者がいる、必要なライブラリや機能がある、書き易い、チームの多数決等が基準となり決定されるケースが多いと感じます。僕なら「処理速度が速い」を正義とし、それに掛け合わせる形で「書き易さ」で選ぶと思います。時間コストを考える場合、もし新しい言語とFrameworkの導入で2人月掛かったとしても(2人月掛かることが許される場合)、Daily100万PVのサイトで0.2secリクエストが速くなったとしたら1日の処理削減コストは100万*0.2/(3600*24)の2.3人日。ということは一か月で導入コストの2人月は巻き返せる事になります。更にはユーザーがアプリを使った時の満足感も上がるはずですし。


僕は今Frameworkを使わずにCを書いていてphpの3倍以上の速さが出ているので満足しているんですが、書き易さの点からは本当に最悪な状態。メモリの動的確保/解放や配列処理を本当に間違える...まぁそんな事で処理が速いとされ、C言語より書き易いJava,ScalaやGoのFrameworkを少しずつ勉強かつ紹介していけたらなと思い、今日はPlay Frameworkについて書きます。Play Frameworkはソース更新後の最初のアクセスで自動的にJavaをrebuildしてくれるようなのでコンパイルの手間が省けてとても便利です。

Play Framework

環境の確認と設定

CentOSは6.4、Javajava-1.7.0-openjdk、playは2.2.2を使っています。

$ cat /etc/system-release
CentOS release 6.4 (Final)

$ yum list installed | grep java
java-1.5.0-gcj.x86_64   1.5.0.0-29.1.el6
java-1.7.0-openjdk.x86_64
java-1.7.0-openjdk-devel.x86_64
java_cup.x86_64         1:0.10k-5.el6   @base                                   
tzdata-java.noarch      2014a-1.el6     @updates

$ java -version
java version "1.7.0_51"
OpenJDK Runtime Environment (rhel-2.4.4.1.el6_5-x86_64 u51-b02)
OpenJDK 64-Bit Server VM (build 24.45-b08, mixed mode)

$ alternatives --display java
/usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/java - 優先項目 170051
 スレーブ keytool: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/keytool
 スレーブ orbd: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/orbd
 スレーブ pack200: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/pack200
 スレーブ rmid: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/rmid
 スレーブ rmiregistry: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/rmiregistry
 スレーブ servertool: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/servertool
 スレーブ tnameserv: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/tnameserv
 スレーブ unpack200: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/unpack200
 スレーブ jre_exports: /usr/lib/jvm-exports/jre-1.7.0-openjdk.x86_64
 スレーブ jre: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64
 スレーブ java.1.gz: /usr/share/man/man1/java-java-1.7.0-openjdk.1.gz
 スレーブ keytool.1.gz: /usr/share/man/man1/keytool-java-1.7.0-openjdk.1.gz
 スレーブ orbd.1.gz: /usr/share/man/man1/orbd-java-1.7.0-openjdk.1.gz
 スレーブ pack200.1.gz: /usr/share/man/man1/pack200-java-1.7.0-openjdk.1.gz
 スレーブ rmid.1.gz: /usr/share/man/man1/rmid-java-1.7.0-openjdk.1.gz
 スレーブ rmiregistry.1.gz: /usr/share/man/man1/rmiregistry-java-1.7.0-openjdk.1.gz
 スレーブ servertool.1.gz: /usr/share/man/man1/servertool-java-1.7.0-openjdk.1.gz
 スレーブ tnameserv.1.gz: /usr/share/man/man1/tnameserv-java-1.7.0-openjdk.1.gz
 スレーブ unpack200.1.gz: /usr/share/man/man1/unpack200-java-1.7.0-openjdk.1.gz
現在の「最適」バージョンは /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/java です。

$ vi ~/.zshrc
#2行追記
JAVA_HOME=/usr/lib/jvm/java-1.7.0/
export JAVA_HOME
PATH=${JAVA_HOME}/bin:$PATH:/home/yuta/work/src/play/play-2.2.2/:/home/yuta/work/src/sbt/sbt/bin/
export CLASSPATH=$JAVA_HOME/jre/lib/ext:$JAVA_HOME/lib/tools.jar

$ type javac
javac is /usr/bin/javac
installと実行
$ wget "http://downloads.typesafe.com/play/2.2.2/play-2.2.2.zip"
$ unzip play-2.2.2.zip
$ cd play-2.2.2

# helloworldプロジェクトを作成
$./play new helloworld 
       _
 _ __ | | __ _ _  _
| '_ \| |/ _' | || |
|  __/|_|\____|\__ /
|_|            |__/

play 2.2.2 built with Scala 2.10.3 (running Java 1.7.0_51), http://www.playframework.com

The new application will be created in /home/yuta/work/src/play/play-2.2.2/helloworld

What is the application name? [helloworld]
> helloworld

Which template do you want to use for this new application? 

  1             - Create a simple Scala application
  2             - Create a simple Java application

# ここではJavaを選択
> 2
OK, application helloworld is created.

Have fun!

$ cd helloworld
$ ../play run

../play run
Getting org.scala-sbt sbt 0.13.0 ...
:: retrieving :: org.scala-sbt#boot-app
	confs: [default]
	43 artifacts copied, 0 already retrieved (12440kB/579ms)
[info] Loading project definition from /home/yuta/work/src/play/play-2.2.2/helloworld/project

[info] Set current project to helloworld (in build file:/home/yuta/work/src/play/play-2.2.2/helloworld/)
[info] Updating {file:/home/yuta/work/src/play/play-2.2.2/helloworld/}helloworld...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.

--- (Running the application from SBT, auto-reloading is enabled) ---

[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

(Server started, use Ctrl+D to stop and go back to the console...)

[info] Compiling 4 Scala sources and 2 Java sources to /home/yuta/work/src/play/play-2.2.2/helloworld/target/scala-2.10/classes...
[info] Error occurred during initialization of VM
[info] Could not reserve enough space for object heap
[error] Error: Could not create the Java Virtual Machine.
[error] Error: A fatal exception has occurred. Program will exit.

初回実行時はJavaのheapエラーで怒られてしまいました。play-2.2.2/framework/buildファイルのheap領域指定のオプションを修正します。僕は自分の環境に合せて-Xmx1536Mから-Xmxを768Mに修正しました。ぐぐると同じディレクトリにあるbuild.batを修正する内容も見かけるので、そちらの-Xmxも合せて修正しておくと良いと思います。修正後にplay runで問題なく起動します。9000ポートで見れるように解放も忘れないようにしましょう。

$ cd framework
$ vi build
# Xmxを768Mに修正
"$JAVA" ${DEBUG_PARAM} -Xms512M -Xmx768M -Xss1M -XX:ReservedCodeCacheSize=192m -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=512M ${JAVA_OPTS} -Dfile.encoding=UTF-8 -Dplay.version="${PLAY_VERSION}" -Dplay.home=`dirname $0` -Dsbt.boot.properties=`dirname $0`/sbt/sbt.boot.properties -Dsbt.scala.version=${SBT_SCALA_VERSION} ${PLAY_OPTS} -jar `dirname $0`/sbt/sbt-launch.jar "$@"

# playの再起動
$ cd helloworld
$ play run

--- (Running the application from SBT, auto-reloading is enabled) ---

[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

(Server started, use Ctrl+D to stop and go back to the console...)

[info] play - Application started (Dev)

$ sudo vim /etc/sysconfig/iptables
# 追加
-A INPUT -m state --state NEW -m tcp -p tcp --dport 9000 -j ACCEPT
$ sudo service iptables restart 

Practice

JavaHome(日本語) はてなブックマーク - JavaHome
JavaHome(English) はてなブックマーク - JavaHome

目的

以下ではDBからデータを引っ張ってViewに表示するまでの一連の処理をJava Play Frameworkで記載します。

File layout

appの下にMVCのロジックを書くようですが、本家のドキュメントにあるようなmodelsディレクトリが生成されません。ちなみにですが、日本語のplay framewokのドキュメントは2.1.5までのversionしかないので、新しいドキュメントを参照したい場合は本家を見た方が良いです。modelsディレクトリは自分でapp以下に付け足して以下のlayoutにします。また後で追加するjar系のlibraryを配置するlibも無いので追加します。

$ pwd
/home/yuta/work/src/play/play-2.2.2/helloworld
$ tree -L 2
├── README
├── app
│        ├── controllers
│        ├── models
│        └── views
├── build.sbt
├── conf
│         ├── application.conf
│         └── routes
├── lib
├── logs
│         └── application.log
├── project
│         ├── Build.scala
│         ├── build.properties
│         ├── plugins.sbt
│         ├── project
│         └── target
├── public
│         ├── images
│         ├── javascripts
│         └── stylesheets
├── target
│         ├── native_libraries
│         ├── resolution-cache
│         ├── scala-2.10
│         └── streams
└── test
    ├── ApplicationTest.java
    └── IntegrationTest.java
mysqlの設定

お試しなので簡単にid,titile,created_at,updated_atのカラムを持つテーブルを定義し、2つデータをINSERTします。

DROP TABLE helloworld.hello;
CREATE TABLE helloworld.hello(
    id INT(11) NOT NULL AUTO_INCREMENT,
    title VARCHAR(64) NOT NULL,
    created_at DATETIME NOT NULL, 
    updated_at DATETIME NOT NULL,
    PRIMARY KEY (id)
);
INSERT INTO helloworld.hello(title,created_at,updated_at) VALUES('This is Hello World!', NOW(), NOW());
INSERT INTO helloworld.hello(title,created_at,updated_at) VALUES('World is not Enough!', NOW(), NOW());
Modelの用意

上のTableと接続するDBModelをmodels/Hello.javaという名前で保存します。一般的なDBModelでpropertyにカラム名、メソッドにデータやり取りの関数を記載します。またここでは紹介しませんがJavaのコードで良く見かけるConnection connection = DB.getConnection();connection.createStatement().execute();のように手続き型でもDBからデータを取得する事は可能なようです。

package models;
import java.util.*;
import play.db.ebean.*;
import play.data.validation.Constraints.*;
import javax.persistence.*;

@Entity
public class Hello extends Model {
  @Id 
  public Integer id;
  @Column
  public String title;
  @Column
  public Date created_at;
  @Column
  public Date updated_at;
  public static Finder<Long,Hello> find = new Finder(Long.class, Hello.class);
  public static List<Hello> all() {
    return find.all();
  }
}
mysql driver

Play FrameworkのdefaultでのDBEngineはh2になっているのでmysqlに変更します。conf/application.confの設定を以下のように変更します。またproject/Build.scalaというファイルを作成しmysql-connector-javaを依存として記述するとplayの再起動時にdownloadしてくれます。ただしdownload先がProjectの一つ上のディレクトリのrepositoryに配置されるので、lib以下にコピーします。準備が整ったらplay runにて再起動をしてアクセスをしてみます。Database 'default' needs evolution!というerror画面が出てもApply This Script Now!というボタンを押下し、更に先の画面でMart it resolvedというボタンを押下すれば処理が先に進みます。

$ vi conf/application.conf

db.default.driver=com.mysql.jdbc.Driver
db.default.url="jdbc:mysql://localhost/helloworld?characterEncoding=UTF8"
db.default.user=root
db.default.password=""
ebean.default="models.*"

$ vi project/Build.scala 

import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {

  val appName         = "helloworld"
  val appVersion      = "1.0-SNAPSHOT"
  
  val appDependencies = Seq(
    // Add your project dependencies here,
    javaCore,
    javaJdbc,
    javaEbean,
    "mysql" % "mysql-connector-java" % "5.1.20"
  )
 
  val subProject = Project("subProject",file("subProject-dir"))
  val main = play.Project(appName, appVersion, appDependencies, path = file("playProject"))
    .dependsOn(subProject)
}

$ cp ../repository/cache/mysql/mysql-connector-java/jars/mysql-connector-java-5.1.20.jar lib/

$ play run
--- (Running the application from SBT, auto-reloading is enabled) ---

[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

(Server started, use Ctrl+D to stop and go back to the console...)

[info] Updating {file:/home/yuta/work/src/play/play-2.2.2/helloworld/}subProject...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Updating {file:/home/yuta/work/src/play/play-2.2.2/helloworld/}helloworld...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] downloading http://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.20/mysql-connector-java-5.1.20.jar ...
[info] 	[SUCCESSFUL ] mysql#mysql-connector-java;5.1.20!mysql-connector-java.jar (3416ms)
[info] Done updating.
[error] application - 
! @6hlo1ai71 - Internal server error, for (GET) [/] ->
play.api.db.evolutions.InvalidDatabaseRevision: Database 'default' needs evolution![An SQL script need to be run on your database.]

()

! @6hlo29f7h - Internal server error, for (GET) [/@evolutions/apply/default?redirect=http%3A%2F%2Flocalhost%3A9000%2F] ->

play.api.db.evolutions.InconsistentDatabase: Database 'default' is in an inconsistent state![An evolution has not been applied properly. Please check the problem and resolve it manually before marking it as resolved.]
Controller

Controllerのメソッド呼び出しはconf/routesで決定されます。GET / controllers.Application.index()という記述が/にアクセスした時にcontrollersのApplicationクラスのindexメソッドを呼び出すという定義です。Frameworkによくあるお決まりのやつですね。上で定義したHelloModelのallメソッドを呼び出してDBに格納されているデータの一覧を取得してViewに渡します。return ok();の記述でStatus:200OKを返すようです。okの中のhelloworld.render(hello)でhelloworldのviewに渡すデータを書きます。ここではList型を渡します。

package controllers;
import java.util.*;
import play.*;
import views.html.*;
import models.*;

public class Application extends Controller {
  public static Result index() {
    List<Hello> hello = Hello.all();
      return ok(helloworld.render(hello));
  }   
}
View

Play FrameworkはMVCのMCはJavaで書けてもViewだけはScala文法なのでそこだけは新しく勉強が必要ですが、記述がとても簡単なので難なくこなせると思います。htmlタグの中への記述もとてもSimpleに書けて楽しいです。下のファイルをviews/helloworld.scala.htmlという名前で保存します。これでDBに入れたデータが表示できるので一通りModel=>Controller=>Viewへのデータのやり取りを記載する事ができました。

@(data: List[Hello])
<!DOCTYPE html>
<html>
<head></head>
<body>
@for(node <- data) {
   <p>id:   @node.id</p> 
   <p>title:@node.title</p> 
   <p>created_at:@node.created_at</p> 
   <p>updated_at:@node.updated_at</p> 
}
</body>
</html>

Performance

php

Frameworkを使わずにmysqlに接続してデータを取得する処理をScriptとして記載したものをabスクリプトで実行した結果です。rpsは176.39となりました。

<!DOCTYPE html>
<html>
<head></head>
<body>
<?php

$link = mysqli_connect('localhost', 'root', '', 'helloworld');
$query = 'SELECT id,title,created_at,updated_at FROM hello';
$result = $link->query($query);
while($row = mysqli_fetch_array($result)) {
  echo '<p>' . $row['id'] . '</p><br/>';
  echo '<p>' . $row['title'] . '</p><br/>';
  echo '<p>' . $row['created_at'] . '</p><br/>';
  echo '<p>' . $row['updated_at'] . '</p><br/>';
}
mysqli_close($link);
?>
</body>
</html>
$ ab -n 50000 -c 100 "http://localhost/helloworld.php"
Server Software:        Apache/2.2.15
Server Hostname:        localhost
Server Port:            80

Document Path:          /helloworld.php
Document Length:        274 bytes

Concurrency Level:      100
Time taken for tests:   283.461 seconds
Complete requests:      50000
Failed requests:        0
Write errors:           0
Total transferred:      23414976 bytes
HTML transferred:       13708768 bytes
Requests per second:    176.39 [#/sec] (mean)
Time per request:       566.922 [ms] (mean)
Time per request:       5.669 [ms] (mean, across all concurrent requests)
Transfer rate:          80.67 [Kbytes/sec] received
Java Play Framework

上で設定したJava Play FrameworkのDB接続をそのまま利用してPerfomanceを測定。rpsは432となり、Java Play Frameworkの勝利です。2.5倍ほどの実力差がでましたね。(※Play Frameworkの設定を良く理解していないのでもしかしたら有利な条件になっているかもしれないです。)

$ ab -n 50000 -c 100 "http://localhost:9000/"
Server Software:        
Server Hostname:        localhost
Server Port:            9000

Document Path:          /
Document Length:        382 bytes

Concurrency Level:      100
Time taken for tests:   115.527 seconds
Complete requests:      50000
Failed requests:        0
Write errors:           0
Total transferred:      23100000 bytes
HTML transferred:       19100000 bytes
Requests per second:    432.80 [#/sec] (mean)
Time per request:       231.055 [ms] (mean)
Time per request:       2.311 [ms] (mean, across all concurrent requests)
Transfer rate:          195.27 [Kbytes/sec] received

SolrのSpatial Searchを試してみた


前書き

10代の頃は(ゴースト)ライターという職業に憧れていた時期もありました@yutakikuchi_です。
Geospatial Indexes and Queries ― MongoDB Manual 2.4.9 はてなブックマーク - Geospatial Indexes and Queries ― MongoDB Manual 2.4.9
MySQL :: MySQL 4.1 リファレンスマニュアル :: 10.6.1 空間インデックスの作成 はてなブックマーク - MySQL :: MySQL 4.1 リファレンスマニュアル :: 10.6.1 空間インデックスの作成
位置情報IndexをMongoDBで管理する手法については前に調査済みで、mysqlにもSpatialindexはあまり普及していない印象、ということで...今日は検索SolrのSpatial Searchについて調べてみます。最終的にはFessやNutchでWebPageをCrawlingして得た住所データをGeocodingでLat/Lngデータに変換して自前のServerにIndexingしていく事を考えており、その前段階の作業です。Solrを選ぶ理由ですがSpatial Search以外にもTermVectorでの類似度を算出してくれるMoreLikeThisという機能があり、Lat/Lngデータの掛け合わせでコンテンツを面白くSuggestすることを考えています。MoreLikeThisについても調査したら書きますね。

Solr設定

java, tomcat6, Solr

javatomcat、Solr本体が必要なので以下の手順でInstallです。Solrは2014.3.15現在で最新のV4.7.0を取ってきます。僕が3年程前にSolrを使っていた時はV1.*とかだったので、もう過去の記憶や記録は役立たなさそうですね...

$ sudo yum install java-1.7.0-openjdk tomcat6 --enablerepo=remi
$ wget "ftp://ftp.riken.jp/net/apache/lucene/solr/4.7.0/solr-4.7.0.tgz"
$ tar xf solr-4.7.0.tgz
Portfowarding

Solrのadminツールに接続する為の設定です。僕の場合はMacVirtualBoxを立ち上げ、HostOSからGuestOS(CentOS)に接続してSolrを使っているのでVirtualBox内のPortfowardingとGuestOS側のFireWallの設定をします。VirtualBoxでは設定=>ネットワーク=>ポートフォワーディングで以下の画面に辿れます。Solrのdefaultportである8983を指定しておきます。※GuestOS側のIPアドレスをifconfigで調べて設定してください。

Firewall

下はHostOS側のFirewall設定です。

$ sudo vi /etc/sysconfig/iptables

-A INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT
-A INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT
#追加
-A INPUT -m state --state NEW -m tcp -p tcp --dport 8983 -j ACCEPT

$ sudo service iptables restart
Solr-exampleの起動

Solrのexampleにある管理画面を表示してみます。tarで展開したディレクトリ以下にstart.jarファイルがあるのでそれを実行します。下のコマンドを実行してhttp://localhost:8983/solrにアクセスします。start.jarの実行時もそうですが、色々とエラーがでていますが取りあえずは画面を見る事ができます。

$ cd solr-4.7.0/example
$ java -jar start.jar
0    [main] INFO  org.eclipse.jetty.server.Server  &#8211; jetty-8.1.10.v20130312
41   [main] INFO  org.eclipse.jetty.deploy.providers.ScanningAppProvider  &#8211; Deployment monitor /home/yuta/work/src/solr/solr-4.7.0/example/contexts at interval 0
50   [main] INFO  org.eclipse.jetty.deploy.DeploymentManager  &#8211; Deployable added: /home/yuta/work/src/solr/solr-4.7.0/example/contexts/solr-jetty-context.xml
1872 [main] INFO  org.eclipse.jetty.webapp.StandardDescriptorProcessor  &#8211; NO JSP Support for /solr, did not find org.apache.jasper.servlet.JspServlet
()


exampledocsの追加

展開したSolrのディレクトリにexampledocsがあるのでSolrに追加してみます。xmlの中身を見てみるとPCパーツや電化製品の情報みたいです。post.shを実行するとcollection1という新しいコレクションが生成され、32個のドキュメントが追加されます。indexのデータはsolr-4.7.0/example/solr/collection1/data/indexに生成されます。

$ cd solr-4.7.0/example/exampledocs
$ cat vidcard.xml
<add>
<doc>
  <field name="id">EN7800GTX/2DHTV/256M</field>
  <field name="name">ASUS Extreme N7800GTX/2DHTV (256 MB)</field>
  <!-- Denormalized -->
  <field name="manu">ASUS Computer Inc.</field>
  <!-- Join -->
  <field name="manu_id_s">asus</field>
  <field name="cat">electronics</field>
  <field name="cat">graphics card</field>
  <field name="features">NVIDIA GeForce 7800 GTX GPU/VPU clocked at 486MHz</field>
  <field name="features">256MB GDDR3 Memory clocked at 1.35GHz</field>
  <field name="features">PCI Express x16</field>
  <field name="features">Dual DVI connectors, HDTV out, video input</field>
  <field name="features">OpenGL 2.0, DirectX 9.0</field>
  <field name="weight">16</field>
  <field name="price">479.95</field>
  <field name="popularity">7</field>
  <field name="store">40.7143,-74.006</field>
  <field name="inStock">false</field>
  <field name="manufacturedate_dt">2006-02-13T15:26:37Z/DAY</field>
</doc>

$ ./post.sh *.xml
Posting file gb18030-example.xml to http://localhost:8983/solr/update
<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader"><int name="status">0</int><int name="QTime">164</int></lst>
</response>
()

$ ls solr-4.7.0/example/solr/collection1/data/index
-rw-rw-r--. 1 yuta yuta  4928  316 00:55 2014 _0.fdt
-rw-rw-r--. 1 yuta yuta    45  316 00:55 2014 _0.fdx
-rw-rw-r--. 1 yuta yuta  2300  316 00:55 2014 _0.fnm
-rw-rw-r--. 1 yuta yuta   250  316 00:55 2014 _0.nvd
-rw-rw-r--. 1 yuta yuta   112  316 00:55 2014 _0.nvm
-rw-rw-r--. 1 yuta yuta   395  316 00:55 2014 _0.si
-rw-rw-r--. 1 yuta yuta   144  316 00:55 2014 _0.tvd
-rw-rw-r--. 1 yuta yuta    45  316 00:55 2014 _0.tvx
-rw-rw-r--. 1 yuta yuta  1047  316 00:55 2014 _0_Lucene41_0.doc
-rw-rw-r--. 1 yuta yuta    34  316 00:55 2014 _0_Lucene41_0.pay
-rw-rw-r--. 1 yuta yuta  2204  316 00:55 2014 _0_Lucene41_0.pos
-rw-rw-r--. 1 yuta yuta 14134  316 00:55 2014 _0_Lucene41_0.tim
-rw-rw-r--. 1 yuta yuta   712  316 00:55 2014 _0_Lucene41_0.tip
-rw-rw-r--. 1 yuta yuta    20  316 00:55 2014 segments.gen
-rw-rw-r--. 1 yuta yuta   110  316 00:55 2014 segments_2
-rw-rw-r--. 1 yuta yuta     0  316 00:25 2014 write.lock
query実行

SolrはREST形式なのでUPDATEやSELECTも基本的にはcurlにて行います。solr-4.7.0/example/exampledocsにはtest_utf8.shというテストスクリプトがあるのでこれを参考に実行してみます。test_utf8.shの実行ではERROR:と表示されなければ問題ないと判断できます。test_utf8.sh中に記載されているcurlスクリプトを真似てqueryをASUS、outputをjsonとしてSolrからのresponseを確認します。

$ ./test_utf8.sh 
Solr server is up.
HTTP GET is accepting UTF-8
HTTP POST is accepting UTF-8
HTTP POST defaults to UTF-8
HTTP GET is accepting UTF-8 beyond the basic multilingual plane
HTTP POST is accepting UTF-8 beyond the basic multilingual plane
HTTP POST + URL params is accepting UTF-8 beyond the basic multilingual plane
Response correctly returns UTF-8 beyond the basic multilingual plane

$ curl "http://localhost:8983/solr/select?q=ASUS&params=explicit&wt=json" | python -mjson.tool
{
    "response": {
        "docs": [
            {
                "_version_": 1462657512030339072, 
                "cat": [
                    "electronics", 
                    "graphics card"
                ], 
                "features": [
                    "NVIDIA GeForce 7800 GTX GPU/VPU clocked at 486MHz", 
                    "256MB GDDR3 Memory clocked at 1.35GHz", 
                    "PCI Express x16", 
                    "Dual DVI connectors, HDTV out, video input", 
                    "OpenGL 2.0, DirectX 9.0"
                ], 
                "id": "EN7800GTX/2DHTV/256M", 
                "inStock": false, 
                "manu": "ASUS Computer Inc.", 
                "manu_id_s": "asus", 
                "manufacturedate_dt": "2006-02-13T00:00:00Z", 
                "name": "ASUS Extreme N7800GTX/2DHTV (256 MB)", 
                "popularity": 7, 
                "price": 479.94999999999999, 
                "price_c": "479.95,USD", 
                "store": "40.7143,-74.006", 
                "weight": 16.0
            }
        ], 
        "numFound": 1, 
        "start": 0
    }, 
    "responseHeader": {
        "QTime": 1, 
        "params": {
            "params": "explicit", 
            "q": "ASUS", 
            "wt": "json"
        }, 
        "status": 0
    }
}
設定ファイル

solr-4.7.0/example/solr/collection1/conf/solrconfig.xmlに色々な記述が書かれているのでより深くexampleでのREST仕様を理解したい人は目を通してみると良いかもしれません。solr/selectにアクセスするとsolr.SearchHandlerClassを呼び出すなどの定義が書かれています。その他indexのschemaについてはsolr-4.7.0/example/solr/collection1/conf/schema.xmlで定義されています。bi-gram、tokenizer、filter等の指定がされています。

$ vi solr-4.7.0/example/solr/collection1/conf/solrconfig.xml

  <requestHandler name="/select" class="solr.SearchHandler">
    <!-- default values for query parameters can be specified, these
         will be overridden by parameters in the request
      -->
     <lst name="defaults">
       <str name="echoParams">explicit</str>
       <int name="rows">10</int>
       <str name="df">text</str>
     </lst>

()

$ vi solr-4.7.0/example/solr/collection1/conf/schema.xml

    <!-- CJK bigram (see text_ja for a Japanese configuration using morphological analysis) -->
    <fieldType name="text_cjk" class="solr.TextField" positionIncrementGap="100">
      <analyzer>
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <!-- normalize width before bigram, as e.g. half-width dakuten combine  -->
        <filter class="solr.CJKWidthFilterFactory"/>
        <!-- for any non-CJK -->
        <filter class="solr.LowerCaseFilterFactory"/>
        <filter class="solr.CJKBigramFilterFactory"/>
      </analyzer>
    </fieldType>
solrpy

solrpy - Python Solr Module - Google Project Hosting はてなブックマーク - solrpy - Python Solr Module - Google Project Hosting
Reference ― solrpy v0.9.2 documentation はてなブックマーク - Reference ― solrpy v0.9.2 documentation
SolrがREST形式ならclient言語は別に何だっていいんですが、僕はJavaよりPythonが好きなのでおしゃべりする言語に使用します。solrpyというライブラリがありますが日本語ドキュメントは無いので、英語版を見ましょう。solrpyには2種類のclassがあってclass solr.Solr(url)、class solr.SolrConnection(url)になります。SolrConnectionの方が記述が分かり易いのですが、Solrの方が新しいという事で以下ではclass solr.Solrを使っています。


Solr-exampleのschema.xmlを少し書き換えて配列形式のauthorを登録するような処理を書いてみます。複数のauthorが登録できるようにmultiValued="true"にします。設定しないとsolrpyのスクリプト実行時にsolr.core.SolrException: HTTP code=400, reason=Bad Requestとerrorが出ます。

$ sudo easy_install solrpy
()
Installed /usr/lib/python2.6/site-packages/solrpy-0.9.6-py2.6.egg
Processing dependencies for solrpy
Finished processing dependencies for solrpy

# 下のtest_solrpy.pyを実行
$ python test_solrpy.py
()
solr.core.SolrException: HTTP code=400, reason=Bad Requestとerror

# errorを回避する為に修正
$ vi solr-4.7.0/example/solr/collection1/conf/schema.xml

<!-- multiValued属性を"true"に設定 --> 
<field name="author" type="text_general" indexed="true" stored="true" multiValued="true"/>
<!-- author_sを追加 --> 
<field name="author_s" type="text_general" indexed="true" stored="true" multiValued="true"/>

# solrのrestart
$ java -jar start.jar

# 再度実行
$ python test_solrpy.py 
1 [u'Lucene in Action'] [u'Erik Hatcher', u'Otis Gospodneti\u0107']
#!/usr/bin/env python
#coding:utf-8
#solrpyのテスト

import solr
# add a document to the index
con = solr.Solr('http://localhost:8983/solr')
doc ={'id':1, 'title':u'Lucene in Action', 'author':[u'Erik Hatcher', u'Otis Gospodneti〓']}
con.add(doc,commit=True)
# do a search
response = con.select('title:lucene')
for hit in response.results:
    print hit['id'],hit['title'],hit['author']

Spatial Search

設定

SpatialSearch - Solr Wiki はてなブックマーク - SpatialSearch - Solr Wiki
SpatialSearchについては上のwikiに詳しく載っています。内容に書かれているschema.xmlのfield name="store"、fieldType name="location"は既にexampleでは定義済みなので追記は不要だと思います。

$ vi solr-4.7.0/example/solr/collection1/conf/schema.xml

# 以下はexampleのfieldTypeにdefaultで記載済み
<field name="store" type="location" indexed="true" stored="true"/>
<fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/>
テスト

post.shにてindex化したデータに対してSpatial Searchを掛けてみると問題なく対象となるデータが抽出出来ている事が確認できます。ちょいとSpatial Searchとして指定するパラメータが複雑ですが纏めると下の表のようになります。

parameter meaning
fl 結果として抽出するfield
q query (下では*:*を指定)
fq queryのfilter(下では{!geofilt}を指定)
sfield Spatial Searchの対象field
pt 検索位置のlatlng
d ptからの検索対象距離を指定
wt 出力形式

$ curl "http://localhost:8983/solr/select?fl=name,store&q=*%3A*&fq=%7B%21geofilt%7D&sfield=store&pt=45.15,-93.85&d=5&wt=json" | python -mjson.tool
{
    "response": {
        "docs": [
            {
                "name": "Maxtor DiamondMax 11 - hard drive - 500 GB - SATA-300", 
                "store": "45.17614,-93.87341"
            }, 
            {
                "name": "Belkin Mobile Power Cord for iPod w/ Dock", 
                "store": "45.18014,-93.87741"
            }, 
            {
                "name": "A-DATA V-Series 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - OEM", 
                "store": "45.18414,-93.88141"
            }
        ], 
        "numFound": 3, 
        "start": 0
    }, 
    "responseHeader": {
        "QTime": 0, 
        "params": {
            "d": "5", 
            "fl": "name,store", 
            "fq": "{!geofilt}", 
            "pt": "45.15,-93.85", 
            "q": "*:*", 
            "sfield": "store", 
            "wt": "json"
        }, 
        "status": 0
    }
}
実用データでテスト

ソニア - 新橋/お好み焼き [食べログ] はてなブックマーク - ソニア - 新橋/お好み焼き [食べログ]
島風お好み焼きがとても美味しい上のお店のデータでテストしてみます。冒頭の写真はお店のお好み焼き(シングルというメニュー)です。新橋/汐留近辺ではとても有名なお店だと思うので近い方は一度行ってみてください。さて、YahooのGeocoderAPIを使ってお店の住所からlatlngを出し、SolrのIndexに格納しSpatialSearchをやってみます。
食べログの案内に汐留駅から372mと書かれており、パラメータd=0.3では抽出不可、d=0.4=抽出可能という点から正確にSpatialSearchが出来たと思います。

#!/usr/bin/env python
#coding:utf-8
#住所データからSpatial Indexを作成 
#latlon.pyとして保存する
import urllib,urllib2,json,solr
opener = urllib2.build_opener()
ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.51.22 (KHTML, like Gecko) Version/5.1.1 Safari/    534.51.22'
referer = 'http://www.yahoo.co.jp/'
opener.addheaders = [('User-Agent', ua),('Referer', referer)]
appid = 'Yahoo!' # yahooapiのappidをここに定義
address = u'東京都港区新橋5-15-1'
try:
    url = 'http://geo.search.olp.yahooapis.jp/OpenLocalPlatform/V1/geoCoder?appid=%s&query=%s&output=%s' % (appid, urllib.quote_plus(address.encode('utf-8')), 'json') 
    res = json.loads(opener.open(url).read())
    if (res['ResultInfo']['Status'] == 200 and res['ResultInfo']['Count'] > 0 ):
        (lng,lat) = res['Feature'][0]['Geometry']['Coordinates'].split(',')
        latlng = lat + ',' + lng 
except urllib2.URLError:
    print "Error: API"

try:
    con = solr.Solr('http://localhost:8983/solr')
    doc ={'id':'sonia_latlon', 'name':u'ソニア', 'store':latlng}
    con.add(doc,commit=True)
#汐留駅からの0.5KMの距離で計算
    response = con.select('*:*', 'id,name,store', '', 'true', 'id', 'desc', fq='{!geofilt}', sfield='store', d='0.4', pt='35.662800,139.760000')
    for hit in response.results:
        print hit['id'], hit['name'], hit['store']
except solr.SolrException:
    print "Error: Solr"
$ python latlon.py
sonia_latlon ソニア 35.66223082,139.75596233

OpenSSLの暗号処理が爆速な件

目次

  1. OpenSSLによる暗号
  2. 実行環境
  3. OpenSSLによる暗号化速度
  4. ECBとCBCの違い
  5. PHP
    • OpenSSLとMcrypt関数のalgorithms比較
    • OpenSSLとmcrypt関数のDES,AESの速度比較
    • Mcryptのゼロpaddingの癖
  6. C
    • DES暗号
    • AES暗号
    • OpenSSLとMcryptのDES,AESの速度比較

OpenSSLによる暗号

OpenSSL日本語サイト: The Open Source toolkit for SSL/TLS はてなブックマーク - OpenSSL日本語サイト: The Open Source toolkit for SSL/TLS
あどてくやっている@yutakikuchi_です。
今日はOpenSSLの共通鍵暗号について調査した内容を纏めます。OpenSSLについて特にC言語での日本語ドキュメントが少なく、あったとしても内容が古くてあまり参考にならなかったりするので色々とサンプルを上げて行きます。PHPについて記載します。
結論を先に書いておくとC,PHPともにMcryptでは無くOpenSSLを使って暗号化した方が処理効率がはるかに向上します。以下がECBモードで100万回の暗号化/復号化処理時間(sec)の表になります。速度以外の点としてはMcryptはPadding(後述)の仕様が厄介です。みなさん、OpenSSLを使いましょう!※下で挙げているソースコードにはほとんどError処理が書いていないので、コピペする人はその辺に気をつけてくださいね。コードはgithubにも上げたのでご自由にどうぞ。
Crypto/openssl at master · yutakikuchi/Crypto はてなブックマーク - Crypto/openssl at master · yutakikuchi/Crypto

暗号ライブラリ 暗号メソッド 言語 実行回数 暗号化処理時間 復号化処理時間 備考
OpenSSL DES-ECB PHP 1000000 1.7591309 1.9514858 爆速
Mcrypt DES-ECB PHP 1000000 231.3662681 218.4248890 爆遅
OpenSSL AES-128-ECB PHP 1000000 1.1707689  1.51384592 爆速
Mcrypt AES-128-ECB PHP 1000000 93.1640918 86.3522748 爆遅
OpenSSL DES-ECB C 1000000 1.160 1.240 爆速
Mcrypt DES-ECB C 1000000 214.830 194.970 爆遅
OpenSSL AES-128-ECB C 1000000 0.620 0.720 爆速
Mcrypt AES-128-ECB C 1000000 62.850 62.430 爆遅

実行環境

このドキュメントは以下の環境およびパッケージで実行しています。

$ cat /etc/system-release 
CentOS release 6.4 (Final)

$ uname -a                                                   
Linux localhost.localdomain 2.6.32-358.el6.x86_64 #1 SMP Fri Feb 22 00:31:26 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux

$ rpm -qa | grep "openssl"
rpm -qa | grep -i "openssl"
openssl-1.0.1e-16.el6_5.4.x86_64
openssl-devel-1.0.1e-16.el6_5.4.x86_64
pyOpenSSL-0.10-2.el6.x86_64
openssl098e-0.9.8e-17.el6.centos.2.x86_64

$ rpm -qa | grep "php"
php-cli-5.4.25-2.el6.remi.x86_64
php-mcrypt-5.4.25-2.el6.remi.x86_64
php-common-5.4.25-2.el6.remi.x86_64
php-5.4.25-2.el6.remi.x86_64

$ rpm -qa | grep "python"
python-libs-2.6.6-52.el6.x86_64
python-iniparse-0.3.1-2.1.el6.noarch
python-pycurl-7.19.0-8.el6.x86_64
newt-python-0.52.11-3.el6.x86_64
python-2.6.6-52.el6.x86_64
python-devel-2.6.6-52.el6.x86_64
python-setuptools-0.6.10-3.el6.noarch
rpm-python-4.8.0-32.el6.x86_64
python-urlgrabber-3.9.1-8.el6.noarch

OpenSSLによる暗号化速度

DES,3DES,AESあたりがよく使われる共通鍵暗号方式のメソッドだと思いますが、それぞれどの程度の速度で処理されているかをopensslコマンドにより比較する事が出来ます。3秒間にそれぞれの固定サイズのデータを何回暗号化出来るかというspeed勝負です。DESの暗号化強度が不安なのでより強度の高いAESが開発されたという歴史の流れがありますが、処理速度はDESよりAES-128の方が速いという結果になります。(僕の環境ではAES-128とAES-192がDESより速いという結果。DESはkey長が8Byte(64bit)、AESは16Byte(128bit)/24Byte(192bit)/32Byte(258bit)なのでAESの方が遅いと予想されがちですが、AES-256だけがDESより遅くなりました。)3DESは名前の通りDESの3倍近く遅いですね。
一番下の表は1secあたりの処理キロバイト数を計算したものになります。

$ openssl speed des aes
Doing des cbc for 3s on 16 size blocks: 9642571 des cbc's in 2.98s
Doing des cbc for 3s on 64 size blocks: 2527420 des cbc's in 2.99s
Doing des cbc for 3s on 256 size blocks: 592201 des cbc's in 2.98s
Doing des cbc for 3s on 1024 size blocks: 122972 des cbc's in 2.93s
Doing des cbc for 3s on 8192 size blocks: 16906 des cbc's in 2.97s
Doing des ede3 for 3s on 16 size blocks: 3467602 des ede3's in 2.98s
Doing des ede3 for 3s on 64 size blocks: 910272 des ede3's in 2.98s
Doing des ede3 for 3s on 256 size blocks: 192319 des ede3's in 2.97s
Doing des ede3 for 3s on 1024 size blocks: 49983 des ede3's in 2.96s
Doing des ede3 for 3s on 8192 size blocks: 5577 des ede3's in 2.98s
Doing aes-128 cbc for 3s on 16 size blocks: 13118363 aes-128 cbc's in 2.96s
Doing aes-128 cbc for 3s on 64 size blocks: 3854938 aes-128 cbc's in 2.98s
Doing aes-128 cbc for 3s on 256 size blocks: 939551 aes-128 cbc's in 2.97s
Doing aes-128 cbc for 3s on 1024 size blocks: 509362 aes-128 cbc's in 2.97s
Doing aes-128 cbc for 3s on 8192 size blocks: 59653 aes-128 cbc's in 2.97s
Doing aes-192 cbc for 3s on 16 size blocks: 10306361 aes-192 cbc's in 2.83s
Doing aes-192 cbc for 3s on 64 size blocks: 3184489 aes-192 cbc's in 2.97s
Doing aes-192 cbc for 3s on 256 size blocks: 757061 aes-192 cbc's in 2.97s
Doing aes-192 cbc for 3s on 1024 size blocks: 428066 aes-192 cbc's in 2.97s
Doing aes-192 cbc for 3s on 8192 size blocks: 37443 aes-192 cbc's in 2.97s
Doing aes-256 cbc for 3s on 16 size blocks: 4759675 aes-256 cbc's in 3.02s
Doing aes-256 cbc for 3s on 64 size blocks: 2045784 aes-256 cbc's in 2.97s
Doing aes-256 cbc for 3s on 256 size blocks: 541610 aes-256 cbc's in 2.97s
Doing aes-256 cbc for 3s on 1024 size blocks: 315557 aes-256 cbc's in 2.98s
Doing aes-256 cbc for 3s on 8192 size blocks: 46577 aes-256 cbc's in 2.96s

(略)

The 'numbers' are in 1000s of bytes per second processed.
type                  16 bytes        64 bytes       256 bytes      1024 bytes    8192 bytes
des cbc             51772.19k    54098.62k    50873.64k    42977.25k    46630.96k
des ede3           18618.00k    19549.47k    16576.99k    17291.42k    15331.14k
aes-128 cbc      70910.07k    82790.61k    80984.87k   175618.41k   164537.84k
aes-192 cbc      58269.18k    68621.99k    65255.09k   147589.09k   103277.12k
aes-256 cbc      25216.82k    44084.23k    46684.23k   108433.01k   128904.99k

ECBとCBCの違い

ブロック暗号モード(block cipher mode) はてなブックマーク - ブロック暗号モード(block cipher mode)
上のサイトに詳しく書いてあります。ECBは平文を固定ブロックに分割して1ブロック毎に暗号化を行い連結して暗号文を作成する。CBCも平文を固定ブロックに分割し暗号化を行いますが、前回の暗号結果が次回の暗号結果にも連鎖して利用される方式になります。CBCは前回の結果を反映するため、初回の暗号結果としてIV(初期ベクトル)を必要とします。CBCモードを選択している時にプログラム上でIVが無いと怒られるのはこの為ですね。

PHP

OpenSSLとMcrypt関数のalgorithms比較

まずはサポートされている暗号化メソッドですが、圧倒的にOpenSSLの方が充実しています。以下のコマンドを叩いていただければ表示されます。※OpenSSLは重複している暗号メソッドも含まれます。

#openssl
$ php -r "print_r(openssl_get_cipher_methods());"
Array
(
    [0] => AES-128-CBC
    [1] => AES-128-CFB
    [2] => AES-128-CFB1
    [3] => AES-128-CFB8
    [4] => AES-128-CTR
    [5] => AES-128-ECB
    [6] => AES-128-OFB
    [7] => AES-128-XTS
    [8] => AES-192-CBC
    [9] => AES-192-CFB
    [10] => AES-192-CFB1
 ()
    [165] => rc4
    [166] => rc4-40
    [167] => rc4-hmac-md5
    [168] => seed-cbc
    [169] => seed-cfb
    [170] => seed-ecb
    [171] => seed-ofb

)

#mcrypt
$ php -r 'print_r(mcrypt_list_algorithms());'                          
Array
(
    [0] => cast-128
    [1] => gost
    [2] => rijndael-128
    [3] => twofish
    [4] => arcfour
    [5] => cast-256
    [6] => loki97
    [7] => rijndael-192
    [8] => saferplus
    [9] => wake
    [10] => blowfish-compat
    [11] => des
    [12] => rijndael-256
    [13] => serpent
    [14] => xtea
    [15] => blowfish
    [16] => enigma
    [17] => rc2
    [18] => tripledes
)
OpenSSLとMcrypt関数のDES,AESの速度比較

まずはDESの速度比較です。IVを必要としないECBモードで実行します。結果はOpenSSLの圧勝でした。Mcryptは遅い!encryptで131倍、decryptで111倍も時間が掛かって今います。今mcrypt使って頻繁にDES暗号化処理をしている人は乗り換えした方が良いレベルですね。

<?php

$n = 1000000;
$message = '魔法少女まどか☆マギカ';
$key = 'Soul Gem';
$method = 'DES-ECB';

//openssl
$start = microtime(true);
for($i=0; $i<$n; ++$i){
  $crypto = openssl_encrypt( $message, $method, $key );
}
echo "encrypt message = {$crypto} \n";
echo "openssl {$n} encrypt time = " . ( microtime(true) - $start ) . "\n";

$start = microtime(true);
for($i=0; $i<$n; ++$i){
  $message = openssl_decrypt( $crypto, $method, $key );
}
echo "decrypt message = {$message} \n";
echo "openssl {$n} decrypt time = " . ( microtime(true) - $start ) . "\n";

//mcrypto
$start = microtime(true);
for($i=0; $i<$n; ++$i){
  $crypto = base64_encode(mcrypt_encrypt(MCRYPT_DES, $key, $message, MCRYPT_MODE_ECB));
}
echo "encrypt message = {$crypto} \n";
echo "mcrypt {$n} encrypt time = " . ( microtime(true) - $start ) . "\n";

$start = microtime(true);
for($i=0; $i<$n; ++$i){
  $message = mcrypt_decrypt(MCRYPT_DES, $key, base64_decode($crypto), MCRYPT_MODE_ECB);
}

echo "decrypt message = {$message} \n";
echo "mcrypt {$n} decrypt time = " . ( microtime(true) - $start ) . "\n";
$ php des.php
encrypt message = s3TrEokf/1xTgoHIimEtUi8s5hNgultndusqMud+XGa0KXi6EbHF4w== 
openssl 1000000 encrypt time = 1.7591309547424
decrypt message = 魔法少女まどか☆マギカ 
openssl 1000000 decrypt time = 1.9514858722687
encrypt message = s3TrEokf/1xTgoHIimEtUi8s5hNgultndusqMud+XGZW+smvwY2mXw== 
mcrypt 1000000 encrypt time = 231.36626815796
decrypt message = 魔法少女まどか☆マギカ 
mcrypt 1000000 decrypt time = 218.42488908768

次にAES-128-ECBで比較します。結果はDES同様にOpenSSLの圧勝でした。encryptで79倍、decryptで57倍速度に差が出ています。Mcryptは何故そんなにも重いんでしょうか...前職でめっちゃMcryptに頼っていたコードを今から書き換えたい...

<?php

$n = 1000000;
$message = '魔法少女まどか☆マギカ';
$key = 'Soul GemSoul Gem';
$method = 'AES-128-ECB';

//openssl
$start = microtime(true);
for($i=0; $i<$n; ++$i){
  $crypto = openssl_encrypt( $message, $method, $key );
}
echo "encrypt message = {$crypto} \n";
echo "openssl {$n} encrypt time = " . ( microtime(true) - $start ) . "\n";

$start = microtime(true);
for($i=0; $i<$n; ++$i){
  $message = openssl_decrypt( $crypto, $method, $key );
}
echo "decrypt message = {$message} \n";
echo "openssl {$n} decrypt time = " . ( microtime(true) - $start ) . "\n";

//mcrypto
$start = microtime(true);
for($i=0; $i<$n; ++$i){
  $crypto = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $message, MCRYPT_MODE_ECB));
}
echo "encrypt message = {$crypto} \n";
echo "mcrypt {$n} encrypt time = " . ( microtime(true) - $start ) . "\n";

$start = microtime(true);
for($i=0; $i<$n; ++$i){
  $message = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, base64_decode($crypto), MCRYPT_MODE_ECB);
}
echo "decrypt message = {$message} \n";
echo "mcrypt {$n} decrypt time = " . ( microtime(true) - $start ) . "\n";
$ php aes.php 
encrypt message = jkSF8YDL4Bi/JDC6z/5aoosGSBRqcEeRri2ZH5Qs+PMb+HceqdCgV5iMH/DziDzB 
openssl 1000000 encrypt time = 1.1707689762115
decrypt message = 魔法少女まどか☆マギカ 
openssl 1000000 decrypt time = 1.5138459205627
encrypt message = jkSF8YDL4Bi/JDC6z/5aoosGSBRqcEeRri2ZH5Qs+PPPZdMXB4Zn5TSdDI1WVm86 
mcrypt 1000000 encrypt time = 93.164091825485
decrypt message = 魔法少女まどか☆マギカ 
mcrypt 1000000 decrypt time = 86.352274894714
mcryptのゼロpaddingの癖

mcrypt関数は処理速度が遅いだけではなく、暗号化をする際に文字列を固定ブロック長に分割するのですが、分割後に足りないByte数を埋め合せるPadding処理をNULLで行うという癖があります。上の例も実はopensslとmcryptで生成された暗号化文に差異が発生しています。暗号化には通常はpkcs#5というPaddingが使われる事が多いです。php-mcryptで暗号化、復号化を他言語で行おうとするとこのPaddingの違いによりエラーが出る場合があるので注意が必要です。通常はphp-mcryptに自前でpkcs5_pad、pkcs5_unpadの関数を用意してencrypt前にブロック長に対して不足している長さを特定の文字で埋めるpaddingを先に行います。以下はそのサンプルです。この実行でopensslとmcyrptで暗号化文の差異が発生しなくなります。

<?php

function pkcs5_pad($text, $blocksize) { 
    $pad = $blocksize - (strlen($text) % $blocksize); 
    return $text . str_repeat(chr($pad), $pad); 
}
function pkcs5_unpad($text) { 
    $pad = ord($text{strlen($text)-1}); 
    if ($pad > strlen($text)) return false; 
    if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) return false; 
    return substr($text, 0, -1 * $pad); 
}  

$message = '魔法少女まどか☆マギカ';
$key = 'Soul Gem';

$encrypt = base64_encode(mcrypt_encrypt(MCRYPT_DES, $key, pkcs5_pad($message, mcrypt_get_block_size(MCRYPT_DES, 'ecb')), MCRYPT_MODE_ECB));
echo "encrypt message = {$encrypt} \n";
$message = pkcs5_unpad(mcrypt_decrypt(MCRYPT_DES, $key, base64_decode($encrypt), MCRYPT_MODE_ECB));
echo "decrypt message = {$message} \n";
$ php mcrypt_des.php 
encrypt message = s3TrEokf/1xTgoHIimEtUi8s5hNgultndusqMud+XGa0KXi6EbHF4w== 
decrypt message = 魔法少女まどか☆マギカ

C

DES,AES暗号

OpenSSLの暗号化をCで実装した日本語ドキュメントがあまりにも少ないのでソースコードを残す事を目的として書きます。opensslのevp.hとbio.hを使って暗号化/復号化/Base64エンコード処理を行います。具体的な処理はint main関数の中に記載していますが、EVP_CIPHER_CTX_init:初期化、EVP_EncryptInit_ex:暗号化メソッドとkey指定、EVP_EncryptUpdate:暗号化、EVP_EncryptFinal_ex:ブロック長を超えたデータの調整、EVP_CIPHER_CTX_cleanup:お掃除のような処理になります。Decryptはその逆になります。他言語よりコード量が多いですが、まぁCなのでそこはしようがないです。
CBCモードでもIVが必要とされるぐらいで他の処理は基本的にECB暗号と同じ処理になります。下のコードでは復号時にIVが分かっている事を前提に記載していますが、一般的な利用の場合は別途IVの共有が必要になります。IVをRAND_bytes関数にてblocksizeと同じ長さのランダム文字列を生成しています。RAND_bytes関数を利用するにはopenssl/rand.hをincludeします。base64系の処理は上と同じなので省略します。
※現在原因を調査中ですがBIOを使ったbase64_encode,base64_decodeのコストがとてつもなく大きいので、下のサンプルは注意して使ってください。

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <openssl/evp.h>
#include <openssl/bio.h>
#include <openssl/rand.h>
#define ITERATION 1000000

int base64_encode(const char *message, char **buffer) {
    BIO *bio, *b64;
    FILE* stream;
    int encodedSize = 4*ceil((double)strlen(message)/3);
    *buffer = (char *)malloc(encodedSize+1);
    stream = fmemopen(*buffer, encodedSize+1, "w");
    b64 = BIO_new(BIO_f_base64());
    bio = BIO_new_fp(stream, BIO_NOCLOSE);
    bio = BIO_push(b64, bio);
    BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
    BIO_write(bio, message, strlen(message));
    BIO_flush(bio);
    BIO_free_all(bio);
    fclose(stream);
    return 0;
}

int calc_decode_length(const char *b64input) {
    int len = strlen(b64input);
    int padding = 0;
    if (b64input[len-1] == '=' && b64input[len-2] == '=') {
        padding = 2;
    } else if (b64input[len-1] == '=') { 
        padding = 1;
    }
    return (int)len*0.75 - padding;
}

int base64_decode(char *b64message, char **buffer) {
    BIO *bio, *b64;
    int decodeLen = calc_decode_length(b64message),len = 0;
    *buffer = (char*)malloc(decodeLen+1);
    FILE* stream = fmemopen(b64message, strlen(b64message), "r");
    b64 = BIO_new(BIO_f_base64());
    bio = BIO_new_fp(stream, BIO_NOCLOSE);
    bio = BIO_push(b64, bio);
    BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
    len = BIO_read(bio, *buffer, strlen(b64message));
    (*buffer)[len] = '\0';
    BIO_free_all(bio);
    fclose(stream);
    return 0;
}

int create_iv(unsigned char **iv, size_t blocksize) {
    *iv = calloc(blocksize+1, sizeof(char));
    RAND_bytes(*iv, blocksize);
}

int openssl_encrypt(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *method, unsigned char *key, unsigned char *iv, const char *message, unsigned char **cipher, unsigned int cipher_len) {
    unsigned int out_len=0;
    EVP_CIPHER_CTX_init(ctx);
    EVP_EncryptInit_ex(ctx,method,NULL,(unsigned char *)key, iv);
    EVP_EncryptUpdate(ctx,(unsigned char *)*cipher,&cipher_len,(unsigned char *)message,strlen(message));
    EVP_EncryptFinal_ex(ctx,(unsigned char *)(*cipher+cipher_len),&out_len);
    EVP_CIPHER_CTX_cleanup(ctx);
    return 0;
}

int openssl_decrypt(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *method, unsigned char *key, unsigned char *iv, char *dectext, unsigned char **plain, unsigned int plain_len) {
    unsigned int out_len=0;
    EVP_CIPHER_CTX_init(ctx);
    EVP_DecryptInit_ex(ctx,method,NULL,(unsigned char *)key,iv);
    EVP_DecryptUpdate(ctx,(unsigned char *)*plain,&plain_len,(unsigned char *)dectext,strlen(dectext));
    EVP_DecryptFinal_ex(ctx,(unsigned char *)(*plain+plain_len),&out_len);
    EVP_CIPHER_CTX_cleanup(ctx);
    return 0;
}

int main(int argc,char *argv[]){
    
    EVP_CIPHER_CTX ctx;
    unsigned char *iv, *cipher, *plain, *key="Soul Gem", *message="魔法少女まどか☆マギカ";
    unsigned int blocksize = 8, cipher_len, plain_len, i;
    char *enctext, *dectext;

    //dec-ecb-encrypt
    cipher_len=strlen(message)+EVP_MAX_BLOCK_LENGTH;
    cipher=(unsigned char *)calloc(cipher_len,sizeof(char));
    openssl_encrypt(&ctx, EVP_des_ecb(), key, NULL, message, &cipher, cipher_len);
    base64_encode( (const char *)cipher, &enctext ); 
    printf( "dec-ecb-encrypt message = %s\n", enctext );
    free(cipher);

    //des-ecb-decrypt
    base64_decode(enctext, &dectext);
    plain_len=strlen(dectext)+EVP_MAX_BLOCK_LENGTH;
    plain=(unsigned char *)calloc(plain_len,sizeof(char));
    openssl_decrypt(&ctx, EVP_des_ecb(), key, NULL, dectext, &plain, plain_len); 
    printf( "dec-ecb-decrypt message = %s\n", plain );
    free(plain);

    //iv生成
    create_iv(&iv, blocksize);

    //des-cbc-encrypt
    cipher_len=strlen(message)+EVP_MAX_BLOCK_LENGTH;
    cipher=(unsigned char *)calloc(cipher_len,sizeof(char));
    openssl_encrypt(&ctx, EVP_des_cbc(), key, iv, message, &cipher, cipher_len);
    base64_encode( (const char *)cipher, &enctext ); 
    printf( "dec-cbc-encrypt message = %s\n", enctext );
    free(cipher);
    cipher = '\0';

    //dec-cbc-decrypt
    base64_decode(enctext, &dectext);
    plain_len=strlen(dectext)+EVP_MAX_BLOCK_LENGTH;
    plain=(unsigned char *)calloc(plain_len,sizeof(char));
    openssl_decrypt(&ctx, EVP_des_cbc(), key, iv, dectext, &plain, plain_len); 
    printf( "dec-cbc-decrypt message = %s\n", plain );
    free(plain);
    free(iv);
    return 0;
}
$ gcc -lm -I/usr/include/openssl -L/usr/lib64 -lcrypto openssl_des.c -o openssl_des
$ ./openssl_des
dec-ecb-encrypt message = s3TrEokf/1xTgoHIimEtUi8s5hNgultndusqMud+XGa0KXi6EbHF4w==
dec-ecb-decrypt message = 魔法少女まどか☆マギカ
dec-cbc-encrypt message = C7ZA6Br7+b6Bv33cOkUSbpJA6SDtVOygKAQNzPDn0OSWzgZnnwzYYQ==
dec-cbc-decrypt message = 魔法少女まどか☆マギカ
AES暗号

基本的に上のDESコードと同じになります。DESからの変更は暗号化メソッド名と、必要なkey長に応じたByte数を確保します。ここではAES-ECBの128bitモード(16Byte)の例を書きます。256bitモード(32Byte)の場合もメソッド名の数値とblocksizeを書き換えるぐらいで対応できます。main関数以外の処理は上と同じなので省略します。

int main(int argc,char *argv[]){
    
    EVP_CIPHER_CTX ctx;
    unsigned char *iv, *cipher, *plain, *key="Soul GemSoul Gem", *message="魔法少女まどか☆マギカ";
    unsigned int blocksize = 16, cipher_len, plain_len, i;
    char *enctext, *dectext;

    //aes-ecb-encrypt
    cipher_len=strlen(message)+EVP_MAX_BLOCK_LENGTH;
    cipher=(unsigned char *)calloc(cipher_len,sizeof(char));
    openssl_encrypt(&ctx, EVP_aes_128_ecb(), key, NULL, message, &cipher, cipher_len);
    base64_encode( (const char *)cipher, &enctext ); 
    printf( "aes-ecb-encrypt message = %s\n", enctext );
    free(cipher);

    //aes-ecb-decrypt
    base64_decode(enctext, &dectext);
    plain_len=strlen(dectext)+EVP_MAX_BLOCK_LENGTH;
    plain=(unsigned char *)calloc(plain_len,sizeof(char));
    openssl_decrypt(&ctx, EVP_aes_128_ecb(), key, NULL, dectext, &plain, plain_len); 
    printf( "aes-ecb-decrypt message = %s\n", plain );
    free(plain);
    
    //iv生成
    create_iv(&iv, blocksize);

    //aes-ecb-encrypt
    cipher_len=strlen(message)+EVP_MAX_BLOCK_LENGTH;
    cipher=(unsigned char *)calloc(cipher_len,sizeof(char));
    openssl_encrypt(&ctx, EVP_aes_128_cbc(), key, iv, message, &cipher, cipher_len);
    base64_encode( (const char *)cipher, &enctext ); 
    printf( "aes-cbc-encrypt message = %s\n", enctext );
    free(cipher);

    //aes-ecb-decrypt
    base64_decode(enctext, &dectext);
    plain_len=strlen(dectext)+EVP_MAX_BLOCK_LENGTH;
    plain=(unsigned char *)calloc(plain_len,sizeof(char));
    openssl_decrypt(&ctx, EVP_aes_128_cbc(), key, iv, dectext, &plain, plain_len); 
    printf( "aes-cbc-decrypt message = %s\n", plain );
    free(plain);
    
    //free
    free(iv);
    return 0;
}
$ gcc -lm -I/usr/include/openssl -L/usr/lib64 -lcrypto openssl_aes.c -o openssl_aes
$ ./openssl_aes
aes-ecb-encrypt message = jkSF8YDL4Bi/JDC6z/5aoosGSBRqcEeRri2ZH5Qs+PMb+HceqdCgV5iMH/DziDzB
aes-ecb-decrypt message = 魔法少女まどか☆マギカ
aes-cbc-encrypt message = ruD0dtX9DBV3Agy+WMLRg5kpHxQJiLFEmxxKXyG26YC60xuTIqBPf7ta41oSUccc
aes-cbc-decrypt message = 魔法少女まどか☆マギカ
OpenSSLとMcryptのDES,AESの速度比較

PHP同様にCでもMcryptを使って暗号化をする事ができます。mcrypt関数の方がopensslよりも直感的に使い易く少ないコード量で実現できますが、例のごとくPaddingがイケてないので下ではPaddingをpkcs5にするようコードを加えています。速度の評価ですがDES-ECBの暗号化でMcryptの方が185倍、復号化で157倍、AES-ECBの暗号化で101倍、復号化で86倍も遅い事が分かりました。

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <mcrypt.h>
#include <time.h>
#include <openssl/evp.h>
#include <openssl/bio.h>
#include <openssl/rand.h>
#define ITERATION 1000000

int base64_encode(const char *message, char **buffer) {
    BIO *bio, *b64;
    FILE* stream;
    int encodedSize = 4*ceil((double)strlen(message)/3);
    *buffer = (char *)malloc(encodedSize+1);
    stream = fmemopen(*buffer, encodedSize+1, "w");
    b64 = BIO_new(BIO_f_base64());
    bio = BIO_new_fp(stream, BIO_NOCLOSE);
    bio = BIO_push(b64, bio);
    BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
    BIO_write(bio, message, strlen(message));
    BIO_flush(bio);
    BIO_free_all(bio);
    fclose(stream);
    return 0;
}

int calc_decode_length(const char *b64input) {
    int len = strlen(b64input);
    int padding = 0;
    if (b64input[len-1] == '=' && b64input[len-2] == '=') {
        padding = 2;
    } else if (b64input[len-1] == '=') { 
        padding = 1;
    }
    return (int)len*0.75 - padding;
}

int base64_decode(char *b64message, char **buffer) {
    BIO *bio, *b64;
    int decodeLen = calc_decode_length(b64message),len = 0;
    *buffer = (char*)malloc(decodeLen+1);
    FILE* stream = fmemopen(b64message, strlen(b64message), "r");
    b64 = BIO_new(BIO_f_base64());
    bio = BIO_new_fp(stream, BIO_NOCLOSE);
    bio = BIO_push(b64, bio);
    BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
    len = BIO_read(bio, *buffer, strlen(b64message));
    (*buffer)[len] = '\0';
    BIO_free_all(bio);
    fclose(stream);
    return 0;
}

int create_iv(unsigned char **iv, size_t blocksize) {
    *iv = calloc(blocksize+1, sizeof(char));
    RAND_bytes(*iv, blocksize);
    return 0;
}

int openssl_encrypt(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *method, unsigned char *key, unsigned char *iv, const char *message, unsigned char **cipher, unsigned int cipher_len) {
    unsigned int out_len=0;
    EVP_CIPHER_CTX_init(ctx);
    EVP_EncryptInit_ex(ctx,method,NULL,(unsigned char *)key, iv);
    EVP_EncryptUpdate(ctx,(unsigned char *)*cipher,&cipher_len,(unsigned char *)message,strlen(message));
    EVP_EncryptFinal_ex(ctx,(unsigned char *)(*cipher+cipher_len),&out_len);
    EVP_CIPHER_CTX_cleanup(ctx);
    return 0;
}

int openssl_decrypt(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *method, unsigned char *key, unsigned char *iv, char *dectext, unsigned char **plain, unsigned int plain_len) {
    unsigned int out_len=0;
    EVP_CIPHER_CTX_init(ctx);
    EVP_DecryptInit_ex(ctx,method,NULL,(unsigned char *)key,iv);
    EVP_DecryptUpdate(ctx,(unsigned char *)*plain,&plain_len,(unsigned char *)dectext,strlen(dectext));
    EVP_DecryptFinal_ex(ctx,(unsigned char *)(*plain+plain_len),&out_len);
    EVP_CIPHER_CTX_cleanup(ctx);
    return 0;
}

int *mcrypt_encrypt(unsigned char *method, unsigned char *mode, unsigned char *key, unsigned char *cipher, unsigned int cipher_len) {
    MCRYPT td = mcrypt_module_open(method, NULL, mode, NULL);
    mcrypt_generic_init(td, (void *)key, strlen(key), NULL);
    mcrypt_generic(td, cipher, cipher_len);
    mcrypt_generic_deinit(td);
    mcrypt_module_close(td);
    return 0;
}

int *mcrypt_decrypt(unsigned char *method, unsigned char *mode, unsigned char *key, unsigned char *plain, unsigned int plain_len) {
    MCRYPT td = mcrypt_module_open(method, NULL, mode, NULL);
    mcrypt_generic_init(td, (void *)key, strlen(key), NULL);
    mdecrypt_generic(td, plain, plain_len);
    mcrypt_generic_deinit(td);
    mcrypt_module_close(td);
    return 0;
}

int *pkcs5_padding(unsigned char **cipher, unsigned int mod, const char *message, unsigned int blocksize) {
    if( mod != 0 ) {
        unsigned int pad = blocksize - mod, i;
        char p[1];
        sprintf(p, "%c", pad);
        for(i=0; i<pad; i++) {
            strcat(*cipher, p);
        }
    }
    return 0;
}

int *pkcs5_unpadding(unsigned char **plain, unsigned int plain_len) {
    unsigned char dp[1], int_dp[1];
    sprintf(dp, "%c", *(*plain + plain_len -1));
    sprintf(int_dp, "%d", *dp);
    int j = atoi(int_dp);
    if(dp != NULL && j != 0) {
        if(j < plain_len) {
            int i;
            for(i=plain_len - j; i<plain_len; i++) {
                *(*plain + i) = '\0';
            }
        }
    }
    return 0;
}

int main(int argc,char *argv[]){

    EVP_CIPHER_CTX ctx;
    unsigned char *iv, *cipher, *plain, *key="Soul Gem", *message="魔法少女まどか☆マギカ";
    unsigned int blocksize = 8, cipher_len, plain_len, mod, i, j;
    char *enctext, *dectext;
    clock_t start, end;

    //dec-ecb-encrypt
    cipher_len=strlen(message)+EVP_MAX_BLOCK_LENGTH;
    cipher=(unsigned char *)calloc(cipher_len+1,sizeof(char));
    start = clock();
    for(i=0; i<ITERATION; ++i){
        openssl_encrypt(&ctx, EVP_des_ecb(), key, NULL, message, &cipher, cipher_len);
    }
    base64_encode((const char *)cipher, &enctext);
    end = clock();
    printf("dec-ecb-encrypt message = %s\n", enctext);
    printf("openssl %d encrypt time = %8.7f\n", ITERATION, (double)(end-start)/CLOCKS_PER_SEC);
    free(cipher);
    cipher = '\0';

    //des-ecb-decrypt
    base64_decode(enctext, &dectext);
    plain_len=strlen(dectext)+EVP_MAX_BLOCK_LENGTH;
    plain=(unsigned char *)calloc(plain_len+1,sizeof(char));
    start = clock();
    for(i=0; i<ITERATION; ++i){
        openssl_decrypt(&ctx, EVP_des_ecb(), key, NULL, dectext, &plain, plain_len);
    }
    end = clock();
    printf("dec-ecb-decrypt message = %s\n", plain );
    printf("openssl %d decrypt time = %8.7f\n", ITERATION, (double)(end-start)/CLOCKS_PER_SEC);
    free(plain);
    plain = '\0';

    blocksize = 16;
    key = "Soul GemSoul Gem";

    //aes-ecb-encrypt
    cipher_len=strlen(message)+EVP_MAX_BLOCK_LENGTH;
    cipher=(unsigned char *)calloc(cipher_len+1,sizeof(char));
    start = clock();
    for(i=0; i<ITERATION; ++i){
        openssl_encrypt(&ctx, EVP_aes_128_ecb(), key, NULL, message, &cipher, cipher_len);
    }
    base64_encode((const char *)cipher, &enctext);
    end = clock();
    printf("aes-ecb-encrypt message = %s\n", enctext);
    printf("openssl %d encrypt time = %8.7f\n", ITERATION, (double)(end-start)/CLOCKS_PER_SEC);
    free(cipher);
    cipher = '\0';

    //aes-ecb-decrypt
    base64_decode(enctext, &dectext);
    plain_len=strlen(dectext)+EVP_MAX_BLOCK_LENGTH;
    plain=(unsigned char *)calloc(plain_len+1,sizeof(char));
    start = clock();
    for(i=0; i<ITERATION; ++i){
        openssl_decrypt(&ctx, EVP_aes_128_ecb(), key, NULL, dectext, &plain, plain_len);
    }
    end = clock();
    printf("aes-ecb-decrypt message = %s\n", plain );
    printf("openssl %d decrypt time = %8.7f\n", ITERATION, (double)(end-start)/CLOCKS_PER_SEC);
    free(plain);
    plain = '\0';

    blocksize = 8;
    key = "Soul Gem";

    //des-cbc-encrypt
    mod = strlen(message) % blocksize;
    cipher_len = strlen(message) + ( blocksize - mod );
    cipher = (char *)calloc(cipher_len+1, sizeof(char));
    start = clock();
    for(i=0; i<ITERATION; ++i){
        strcpy(cipher, message);
        pkcs5_padding(&cipher, mod, message, blocksize);
        mcrypt_encrypt(MCRYPT_DES, MCRYPT_ECB, key, cipher, strlen(cipher));
    }
    end = clock();
    base64_encode( (const char *)cipher, &enctext );
    printf("des-ecb-encrypt message = %s\n", enctext);
    printf("mcrypt %d encrypt time = %8.7f\n", ITERATION, (double)(end-start)/CLOCKS_PER_SEC);

    free(cipher);
    cipher = '\0';

    //dec-cbc-decrypt
    base64_decode(enctext, &dectext);
    plain_len = strlen(dectext);
    plain = (char *)calloc(plain_len+1, sizeof(char));
    start = clock();
    for(i=0; i<ITERATION; ++i){
        strcpy(plain,dectext);
        mcrypt_decrypt(MCRYPT_DES, MCRYPT_ECB, key, plain, strlen(plain));
        pkcs5_unpadding(&plain, strlen(plain));
    }

    end = clock();
    printf("des-ecb-decrypt message = %s\n", plain);
    printf("mcrypt %d decrypt time = %8.7f\n", ITERATION, (double)(end-start)/CLOCKS_PER_SEC);

    free(plain);
    plain = '\0';

    blocksize = 16;
    key = "Soul GemSoul Gem";
    mod = strlen(message) % blocksize;
    cipher_len = strlen(message) + ( blocksize - mod );
    cipher = (char *)calloc(cipher_len+1, sizeof(char));

    //aes-ecb-encrypt
    start = clock();
    for(i=0; i<ITERATION; ++i){
        strcpy(cipher, message);
        pkcs5_padding(&cipher, mod, message, blocksize);
        mcrypt_encrypt(MCRYPT_RIJNDAEL_128, MCRYPT_ECB, key, cipher, strlen(cipher));
    }
    end = clock();
    base64_encode((const char *)cipher, &enctext);
    printf("aes-ecb-encrypt message = %s\n", enctext);
    printf("mcrypt %d encrypt time = %8.7f\n", ITERATION, (double)(end-start)/CLOCKS_PER_SEC);
    free(cipher);
    cipher = '\0';

    //aes-ecb-decrypt
    base64_decode(enctext, &dectext);
    plain_len = strlen(dectext);
    plain = (char *)calloc(plain_len+1, sizeof(char));
    start = clock();
    for(i=0; i<ITERATION; ++i){
        strcpy(plain,dectext);
        mcrypt_decrypt(MCRYPT_RIJNDAEL_128, MCRYPT_ECB, key, plain, strlen(plain));
        pkcs5_unpadding(&plain, strlen(plain));
    }

    end = clock();
    printf("aes-ecb-decrypt message = %s\n", plain);
    printf("mcrypt %d decrypt time = %8.7f\n", ITERATION, (double)(end-start)/CLOCKS_PER_SEC);
    free(plain);
    plain = '\0';
    return 0;
}
$ gcc -lm -I/usr/include/openssl -L/usr/lib64 -lcrypto -lmcrypt openssl_vs_mcrypt.c -o openssl_vs_mcrypt
$ ./openssl_vs_mcrypt
dec-ecb-encrypt message = s3TrEokf/1xTgoHIimEtUi8s5hNgultndusqMud+XGa0KXi6EbHF4w==
openssl 1000000 encrypt time = 1.1600000
dec-ecb-decrypt message = 魔法少女まどか☆マギカ
openssl 1000000 decrypt time = 1.2400000
aes-ecb-encrypt message = jkSF8YDL4Bi/JDC6z/5aoosGSBRqcEeRri2ZH5Qs+PMb+HceqdCgV5iMH/DziDzB
openssl 1000000 encrypt time = 0.6200000
aes-ecb-decrypt message = 魔法少女まどか☆マギカ
openssl 1000000 decrypt time = 0.7200000
des-ecb-encrypt message = s3TrEokf/1xTgoHIimEtUi8s5hNgultndusqMud+XGar
mcrypt 1000000 encrypt time = 214.8300000
des-ecb-decrypt message = 魔法少女まどか☆マギカ
mcrypt 1000000 decrypt time = 194.9700000
aes-ecb-encrypt message = jkSF8YDL4Bi/JDC6z/5aoosGSBRqcEeRri2ZH5Qs+POr
mcrypt 1000000 encrypt time = 62.8500000
aes-ecb-decrypt message = 魔法少女まどか☆マギカ
mcrypt 1000000 decrypt time = 62.4300000