男なら潔くC言語書けよと言われた話。〜mod_db,mod_dbdの実装〜
恩師に言われた言葉
Geek女優の池澤あやかさんに会いたいと思っている@yutakikuchi_です。
池澤さんはRubyが出来てSFCで女優さんなんて羨ましいですね〜。僕なんてRubyは得意じゃないし東京とは言えないような都心から離れた場所の地味な国立大だし、何よりお金も無いパンピーだしね〜。
僕の学生時代にもRubyはあったんですけどRailsはまだ出始めでそんなに流行っている雰囲気は無かったし、Webを書くには面倒くさいJSP/ServletかPerlかって感じでした。ApacheのModuleでWebを書ける事も学生ながら知っていたんですが、ポインタ、メモリの動的確保/解放の間違いが頻発して開発効率が落ちるから極力Javaで、どうしてもCを書かなければ行けない時はC++で逃げてました。
でも学生時代に恩師に言われたんですよね、JavaやPerlを書く奴はチャラいやつと。男なら潔くC言語書けよと。 (因に恩師はJavaとJavascriptの違いも良く理解していなかったと思いますけど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 */ };