読者です 読者をやめる 読者になる 読者になる

カメニッキ

カメとインコと釣りの人です

初めてOSS(Apacheモジュール)にPRだした

OSS

今回PRをだした対象


github.com

ApacheのVirtualHost単位でMaxClientsを設定することが可能な、Apacheモジュールです。 詳細は↓

ApacheのVirtualHost単位でMaxClientsを設定するApacheモジュールをOSS化 - 人間とウェブの未来

どんな機能を実装したか


MaxClientsの設定を有効にする時間帯を指定可能にする機能

本モジュールの利用を予定しているサーバの負荷は、常に一定ではなく高い時もあれば低い時もあります。 負荷の低い時間帯はMaxClientsを設定せず、自由に利用してもらうため、本機能の追加を行いました。

やったこと


途中色々アドバイスを受けながらあれこれ手戻ったりしてますが、簡略化するため一部改変してます。関数とか用語とか勘違いしてたら教えてあげてください やったこと、と言っても別に特別なことをしたわけではない

1. ソースを読む

mod_vhost_maxclients/mod_vhost_maxclients.c at master · matsumoto-r/mod_vhost_maxclients · GitHub

行数が短く、比較的読みやすいコードでした。
以下参考

上から構造体や関数定義などが並んでますが、とりあえず一番したから読む。
apxsを使用してApacheのモジュールを作成する場合、同じ形式になっているもよう。

#ifdef __APACHE24__
AP_DECLARE_MODULE(vhost_maxclients) = {
#else
module AP_MODULE_DECLARE_DATA vhost_maxclients_module = {
#endif
    STANDARD20_MODULE_STUFF, NULL,             /* create per-dir config structures     */
    NULL,                                      /* merge  per-dir    config structures  */
    # サーバ設定作成関数
    vhost_maxclients_create_server_config,     /* create per-server config
                                                  structures  */
    # サーバ設定関数
    vhost_maxclients_create_server_merge_conf, /* merge  per-server config structures  */
    # directiveの値を取得する関数
    vhost_maxclients_cmds,                     /* table of config file commands        */
    # フックするポイントを登録する関数
    vhost_maxclients_register_hooks};

それぞれ、登録された関数が何をやっているか見ました。

  • vhost_maxclients_create_server_config

    サーバ設定作成関数の名の通り、本モジュールで使用する設定値がデフォルト値とともに定義されています。

static void *vhost_maxclients_create_server_config(apr_pool_t *p, server_rec *s)
{
  vhost_maxclients_config *scfg = (vhost_maxclients_config *)apr_pcalloc(p, sizeof(*scfg));

  scfg->dryrun = -1;
  scfg->log_path = NULL;
  scfg->vhost_maxclients = 0;
  scfg->vhost_maxclients_log = 0;
  scfg->vhost_maxclients_per_ip = 0;
  scfg->ignore_extensions = apr_array_make(p, VHOST_MAXEXTENSIONS, sizeof(char *));

  return scfg;
}
  • vhost_maxclients_create_server_merge_conf
static void *vhost_maxclients_create_server_merge_conf(apr_pool_t *p, void *b, void *n)
{
  vhost_maxclients_config *base = (vhost_maxclients_config *)b;
  vhost_maxclients_config *new = (vhost_maxclients_config *)n;
  vhost_maxclients_config *scfg = (vhost_maxclients_config *)apr_pcalloc(p, sizeof(*scfg));

  if (new->dryrun > -1) {
    scfg->dryrun = new->dryrun;
  } else {
    scfg->dryrun = base->dryrun;
  }
  scfg->log_path = base->log_path;
  scfg->vhost_maxclients = new->vhost_maxclients;
  scfg->vhost_maxclients_log = new->vhost_maxclients_log;
  scfg->vhost_maxclients_per_ip = new->vhost_maxclients_per_ip;
  scfg->ignore_extensions = new->ignore_extensions;

  return scfg;
}
  • vhost_maxclients_cmds
    ディレクティブの定義をおこなっています。
    AP_INIT_TAKE1 を例にすると、directive指定は hoge 100 のように directive名 値 と名前の通り一個だけ取得する定義です。
    指定しているのは directive名 , directiveの値を変数にセットするための関数 mconfig(何かよく分かってない) , 定義されている場所, Help になります。
static command_rec vhost_maxclients_cmds[] = {
    AP_INIT_FLAG("VhostMaxClientsDryRun", set_vhost_maxclients_dryrun, NULL, ACCESS_CONF | RSRC_CONF,
                 "Enable dry-run which don't return 503, logging only: On / Off (default Off)"),
    AP_INIT_TAKE1("VhostMaxClients", set_vhost_maxclientsvhost, NULL, RSRC_CONF | ACCESS_CONF,
                  "maximum connections per Vhost"),
    AP_INIT_TAKE1("VhostMaxClientsLogOnly", set_vhost_maxclientsvhost_log, NULL, RSRC_CONF | ACCESS_CONF,
                  "loggign only: maximum connections per Vhost"),
    AP_INIT_TAKE1("VhostMaxClientsLogPath", set_vhost_maxclientsvhost_log_path, NULL, RSRC_CONF | ACCESS_CONF,
                  "logging file path instead of error_log"),
    AP_INIT_TAKE1("VhostMaxClientsPerIP", set_vhost_maxclientsvhost_perip, NULL, RSRC_CONF | ACCESS_CONF,
                  "maximum connections per IP of Vhost"),
    AP_INIT_ITERATE("IgnoreVhostMaxClientsExt", set_vhost_ignore_extensions, NULL, ACCESS_CONF | RSRC_CONF,
                    "Set Ignore Extensions."),
    {NULL},
};

directiveの値を変数にセットするための関数 を一個例に見ると set_vhost_ignore_extensions をちょっと見てみると、
バリデーションと値の取得をやってました。

static const char *set_vhost_ignore_extensions(cmd_parms *parms, void *mconfig, const char *arg)
{
  vhost_maxclients_config *scfg =
      (vhost_maxclients_config *)ap_get_module_config(parms->server->module_config, &vhost_maxclients_module);

  if (VHOST_MAXEXTENSIONS < scfg->ignore_extensions->nelts) {
    return "the number of ignore extensions exceeded";
  }

  *(const char **)apr_array_push(scfg->ignore_extensions) = arg;

  return NULL;
}
  • vhost_maxclients_register_hooks
    ハンドラをフックポイントに登録する関数
    人間とウェブの未来 - Apache hook関数 が参考になる。
    initルーチンとかを呼ぶ時にap_hook_post_config
    クライアントが認証受ける前に追加でアクセスチェックをしたい時にap_hook_access_checker
    をフックさせてます。本モジュールの役割的に ap_hook_access_checker(vhost_maxclients_handler, NULL, NULL, APR_HOOK_MIDDLE); が実際に制限させる処理をやってるぽいです。
static void vhost_maxclients_register_hooks(apr_pool_t *p)
{
  ap_hook_post_config(vhost_maxclients_init, NULL, NULL, APR_HOOK_MIDDLE);
  ap_hook_access_checker(vhost_maxclients_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

2. 書いてみる

  • vhost_maxclients_configに追加が必要なものを考える

    今回は制限する時間帯を指定したかったので、 制限開始する時間制限終了する時間 があればよさそうなので、vhost_maxclients_config構造体に2つ変数を追加

  unsigned int vhost_maxclients_time_from;
  unsigned int vhost_maxclients_time_to;
  • directiveから値を取得する

    2つ数字を渡せればいいので、1directive追加します。これで VhostMaxClientsTimeSlot 1100 2300 のようにconfに指定できます。 AP_INIT_TAKE2AP_INIT_TAKE1 の引数が一個増えた版。

AP_INIT_TAKE2("VhostMaxClientsTimeSlot", set_vhost_maxclients_time, NULL, RSRC_CONF | ACCESS_CONF,
                  "Time to enable the VhostMaxClients. (default 0:00 ~ 23:59)"),

TAKE2なのでarg1とarg2に一個目二個目の設定値が入ってくるので、時間のバリデーションをし、取得します。

static const char *set_vhost_maxclients_time(cmd_parms *parms, void *mconfig, const char *arg1, const char *arg2)
{
  vhost_maxclients_config *scfg =
      (vhost_maxclients_config *)ap_get_module_config(parms->server->module_config, &vhost_maxclients_module);

  scfg->vhost_maxclients_time_from = atoi(arg1);
  scfg->vhost_maxclients_time_to = atoi(arg2);

  if(scfg->vhost_maxclients_time_from < 0 || scfg->vhost_maxclients_time_from > 2359){
    return "VhostMaxClientsTimeSlot_From is invalid. should be set range 0 < VhostMaxClientsTimeSlot_From < 2359";
  }

  if (scfg->vhost_maxclients_time_to < 0 || scfg->vhost_maxclients_time_to > 2359){
    return "VhostMaxClientsTimeSlot_To is invalid. should be set range 0 < VhostMaxClientsTimeSlot_To < 2359";
  }

  return NULL;
}  
  • vhost_maxclients_handlerの処理中で時間を判定する処理の追加

    今回の機能は実際に制限をおこなうロジックの手前でやる必要があります。拡張子の判定処理の下あたりに入れます

  /* check time */
  if (check_time_slot(r->pool, scfg->vhost_maxclients_time_from, scfg->vhost_maxclients_time_to)) {
    return DECLINED;
  }

インラインでずらずら処理を書いてもいいのですが、汚いので関数に切り出します。
判定のやりかたは当初request_rec構造体のrequest_timeの値を利用しようとしましたが、apr_time_nowの値を使用することにしました。
apr_time_nowは現在時刻をUTC+マイクロ秒で返すので扱いにくいため、apr_time_exp_ltで人間向けに変換し、 をとってくっつけてintにしてます。
日をまたいだ指定( 23:00 ~ 2:00 など)のため、toよりfromが小さければ24時間プラスしてます。あとは現在時刻が指定した範囲内か判定します。

static int check_time_slot(apr_pool_t *p, unsigned int from, unsigned int to)
{
  unsigned int cur;

  apr_time_exp_t tm;
  apr_time_exp_lt(&tm, apr_time_now());
  cur = atoi(apr_psprintf(p, "%02d%02d", tm.tm_hour, tm.tm_min));

  if (from > to){
    to += 2400;
  }

  if ((from < cur) && (to > cur)){
    return 0;
  }

  return 1;
}

だいたい、上記のようなことをやりました。
あとは、以下のようにdirective指定し、想定通りの動作をするか確認します。

<VirtualHost *>
  DocumentRoot /var/www/html
  ServerName localhost
  VhostMaxClients 1
  VhostMaxClientsTimeSlot 2130 2300
</VirtualHost>

3. 検証

  • 正常動作 既存の同時接続数を制限する機能と時間の指定がどちらも動作するか確認します。
  • パフォーマンス 判定を一個入れたので、少なからず遅くなってるはずです。問題ないレベルかチェックします。見た感じ問題なさそう…
# 改修前のバージョン
[vagrant@localhost ~]$ ab -n100000 -c100  http://127.0.0.1/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 10000 requests
Completed 20000 requests
Completed 30000 requests
Completed 40000 requests
Completed 50000 requests
Completed 60000 requests
Completed 70000 requests
Completed 80000 requests
Completed 90000 requests
Completed 100000 requests
Finished 100000 requests


Server Software:        Apache/2.2.15
Server Hostname:        127.0.0.1
Server Port:            80

Document Path:          /
Document Length:        17 bytes

Concurrency Level:      100
Time taken for tests:   16.434 seconds
Complete requests:      100000
Failed requests:        0
Write errors:           0
Total transferred:      28507125 bytes
HTML transferred:       1700425 bytes
Requests per second:    6084.97 [#/sec] (mean)
Time per request:       16.434 [ms] (mean)
Time per request:       0.164 [ms] (mean, across all concurrent requests)
Transfer rate:          1693.99 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    8   3.7      8      43
Processing:     1    9   3.6      8      45
Waiting:        0    7   3.5      7      43
Total:          1   16   4.2     15      57

Percentage of the requests served within a certain time (ms)
  50%     15
  66%     16
  75%     17
  80%     18
  90%     19
  95%     21
  98%     33
  99%     38
 100%     57 (longest request)

# 改修後のバージョン_1 制限を行う時間外
[vagrant@localhost ~]$ ab -n100000 -c100  http://127.0.0.1/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 10000 requests
Completed 20000 requests
Completed 30000 requests
Completed 40000 requests
Completed 50000 requests
Completed 60000 requests
Completed 70000 requests
Completed 80000 requests
Completed 90000 requests
Completed 100000 requests
Finished 100000 requests


Server Software:        Apache/2.2.15
Server Hostname:        127.0.0.1
Server Port:            80

Document Path:          /
Document Length:        17 bytes

Concurrency Level:      100
Time taken for tests:   13.657 seconds
Complete requests:      100000
Failed requests:        0
Write errors:           0
Total transferred:      28508550 bytes
HTML transferred:       1700510 bytes
Requests per second:    7322.41 [#/sec] (mean)
Time per request:       13.657 [ms] (mean)
Time per request:       0.137 [ms] (mean, across all concurrent requests)
Transfer rate:          2038.59 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    4   3.2      5      15
Processing:     2    9   2.7      9      30
Waiting:        0    8   2.8      8      27
Total:          2   14   2.5     13      34

Percentage of the requests served within a certain time (ms)
  50%     13
  66%     14
  75%     15
  80%     15
  90%     17
  95%     18
  98%     20
  99%     22
 100%     34 (longest request)

# 改修後のバージョン_2 制限を行う時間内
[vagrant@localhost ~]$ ab -n100000 -c100  http://127.0.0.1/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 10000 requests
Completed 20000 requests
Completed 30000 requests
Completed 40000 requests
Completed 50000 requests
Completed 60000 requests
Completed 70000 requests
Completed 80000 requests
Completed 90000 requests
Completed 100000 requests
Finished 100000 requests


Server Software:        Apache/2.2.15
Server Hostname:        127.0.0.1
Server Port:            80

Document Path:          /
Document Length:        17 bytes

Concurrency Level:      100
Time taken for tests:   15.214 seconds
Complete requests:      100000
Failed requests:        0
Write errors:           0
Total transferred:      28514250 bytes
HTML transferred:       1700850 bytes
Requests per second:    6572.97 [#/sec] (mean)
Time per request:       15.214 [ms] (mean)
Time per request:       0.152 [ms] (mean, across all concurrent requests)
Transfer rate:          1830.30 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    5   3.6      6      18
Processing:     2   10   3.0      9      39
Waiting:        0    9   2.9      9      35
Total:          2   15   3.1     15      43

Percentage of the requests served within a certain time (ms)
  50%     15
  66%     16
  75%     17
  80%     17
  90%     19
  95%     21
  98%     22
  99%     24
 100%     43 (longest request)

振り返り


  • apr_xxxxxが便利 Apache Portable Runtimeというサポートライブラリがあって、これが非常に便利なやつでした。
    たとえば cur = atoi(apr_psprintf(p, "%02d%02d", tm.tm_hour, tm.tm_min)); の部分
    当初は以下のように書いてました。

    c char hoge[5]; sprintf(hoge,"%02d%02d", tm.tm_hour, tm.tm_min));

    apr_psprintfを使用すると、hogeのようなメモリ割り当てが不要になり、request_rec構造体のプールを使わせることができます。
    戻り値をそのまま別の関数にくわせて、欲しい型でとれるから便利
    人間とウェブの未来 - apr_psprintfはかなり楽(apache2.x系)

  • 自分の英語雑過ぎる コメント無し

  • 普段コードを書いていないと基本的なことを忘れていく 関数名・変数名が長い、短くするとき意味がわからない。エラーメッセージに必要な内容が載ってない。 if(!((a!=b)&&(c<b)) みたいなこと書いちゃう。関係性の無い判定をelseifしちゃう。 酷い感じでした

  • githubOSSへPR出す際のお作法がわかってない コミットログ汚してしまって反省

  • httpdのソースを眺めながら書くとよい 変数の型や、利用する関数でソース中調べると色々助かる。コード量がおおいので、GNU Globalを使いました 人間とウェブの未来 - GNU GLOBALとvimで巨大なコードでも快適にコードリーディング - Forkwell

初めてcを書き、初めてOSSへPRを出すことができました。
cの入門書を眺めてただけだとしれなかったことがたくさんあり、やれてよかったと思います。
びびりまくってましたが、いざやってみるとみんな優しくて色々教えてくださったので、やってみたほうがいいです。

apacheのモジュール!? + cで書かれている... + OSSなんか怖い... = 不安 を克服していきたい