Y's note

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

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

男なら潔く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                      */
};