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

ちょこっとcode reading

memcached

ここまでのあらすじ稼働中のmemcachedでときどき反応がなくなることがあったので、ちょっとstatsの結果を見てみたところ、そのmemcachedだけconnection_structuresの値が異常に多かった。で、connection_structuresってどういう風に増えていくのか、Web上に情報が見つからなかったため、ざざっとコードを見てみることに。

まずは概要をとりあえず、良い機会なのでconnection_structuresにこだわらず、全体の構成をみてみる。とはいえTCP/IPの接続管理周り以外はあまり見ないかも。

  • 目についたデータ(構造体)
    • conn
      • 接続ごとの状態や読み書き用バッファ、などなどを管理
    • struct stats
      • 統計情報。お目当てのconn_structsもここに
    • struct settings
      • よくある設定情報
    • item
      • データ管理用(多分)の双方向リスト
      • まだ全然見ていない(あまり興味なし)けどslabsと絡んできそう
  • 大まかな処理
    • int main (int argc, char **argv)
      • libeventのAPI:event_init()でグローバル変数main_baseを生成(以降至る所でこの構造体にアクセス)
      • いろんな初期化をしたり
      • 引数オプションをさばいたりして
      • server_socket()でサーバー側のソケットを生成しlistenまで頑張ったり、イベントハンドラーを設定。こいつが重要
      • 最後にevent_base_loop(main_base)でイベントループに入る
    • int server_socket(const int port, const bool is_udp)
      • socket(), bind(), listen()でサーバー側のソケットを作成
      • ちなみに作成したソケットはfcntl(O_NONBLOCK)
      • そいつをconn_new(EV_READ | EV_PERSIST)に渡している
    • conn *conn_new(const int sfd, const int init_state, const int event_flags, const int read_buffer_size, const bool is_udp, struct event_base *base)
      • connのポインタ配列であるfreeconns(後で調べる)に、一つも接続情報が入っていない場合…
        • 新しくconnをアロケートして初期値設定, 読み書き用バッファも作成
        • お目当ての統計情報stats.conn_structs++
      • libeventのAPI:event_set(), event_base_set(), event_add()を使い、イベントハンドラーとしてevent_handler()をセット(何だか不毛な記述な気が…). ちなみにこいつはdrive_machine()を呼んでいるだけ
    • void drive_machine(conn *c)
      • 接続状態:conn->state(初っ端はconn_listening)に応じていろいろやる
      • 接続状態==conn_listening
        • accept()実施. もしerrno == EAGAIN || errno == EWOULDBLOCKだったらdrive_machine()を抜ける(O_NONBLOCK済みなのでそのように判定)
        • acceptしたソケットはconn_new(接続状態==conn_read)に渡される
      • 接続状態==conn_read
        • memcacheクライアントからのコマンドをread
        • update_event(c, EV_READ | EV_PERSIST)が呼ばれイベントフラグを更新(後で調べる)
      • 接続状態==conn_nread
        • conn_readの場合と大体おなじような雰囲気なので後回し
      • 接続状態==conn_swallow
        • 何となく後回し
      • 接続状態==conn_write
    /*
     * We want to write out a simple response. If we haven't already,
     * assemble it into a msgbuf list (this will be a single-entry
     * list for TCP or a two-entry list for UDP).
     */
    if (c->iovused == 0 || (c->udp && c->iovused == 1)) {
        if (add_iov(c, c->wcurr, c->wbytes) != 0 ||
            (c->udp && build_udp_headers(c) != 0)) {
           :
    }
    /* fall through... */
    case conn_mwrite:
        • いつこの状態になるのかも含め、後で調べる
      • 接続状態==conn_mwriteの場合
        • transmit()呼び出し. その中では…
          • memcacheクライアントにsendmsg()
          • 送信情報(我ながら胡散臭い名称…)の更新
          • errno == EAGAIN || errno == EWOULDBLOCKの場合はupdate_event(c, EV_WRITE | EV_PERSIST)
        • transmit()戻り値 == TRANSMIT_COMPLETE
          • 接続状態==conn_mwriteの場合、送信情報を更新し接続状態をconn_readに
          • 接続状態==conn_writeの場合、接続状態をconn->write_and_goの設定値に(ここに何が入りうるかは後で調べる)
        • transmit()戻り値 == TRANSMIT_INCOMPLETE || TRANSMIT_HARD_ERRORの場合、リトライ
        • transmit()戻り値 == TRANSMIT_SOFT_ERRORの場合、一旦drive_machine()を抜ける
      • 接続状態==conn_close
  • freeconnsについて

空き接続状態を管理している入れ物(スタック)で関連する定義は以下

static conn **freeconns;
static int freetotal;
static int freecurr;
    • 各変数についてまとめ
      • freeconns
        • 説明: connポインタ用スタック(拡張できるようにメモリはヒープ上)
        • 初期値: sizeof(conn *) * freetotal分のmallocされたメモリ
        • 変化する要因: do_conn_from_freelist()でconnをpop, do_conn_add_to_freelist()で新しいconnをpush. その際、freecurr >= freetotalの場合はreallocして倍のサイズに拡張.
      • freetotal
        • 説明: freeconnsに格納できるconnポインタの一応の上限値
        • 初期値: 200
        • 変化する要因: do_conn_add_to_freelist()でfreecurr >= freetotalの場合、倍加
      • freecurr
        • 説明: 現在freeconnsに格納されているconnポインタの数
        • 初期値: 0
        • 変化する要因: do_conn_from_freelist()の中でデクリメント、do_conn_add_to_freelist()の中でインクリメント
    • do_conn_*_freelist()
      • do_conn_from_freelist()
        • conn_new()で関数の最初に呼ばれている
      • do_conn_add_to_freelist()
        • conn_close()でc->rsize <= READ_BUFFER_HIGHWATの場合に呼ばれている
        • conn_new()でevent_add()呼び出しに失敗した場合に呼ばれている(異常時の後始末)
      • c->rsize <= READ_BUFFER_HIGHWATとは?
        • 定義は以下
typedef struct conn conn;
struct conn {
      :
    int    rsize;   /** total allocated size of rbuf */
      :

/** High water marks for buffer shrinking */
#define READ_BUFFER_HIGHWAT 8192
        • conn->rsizeへのアクセス(後で調べる)
  • conn_new()の呼び出しについて
    • main()->server_socket()の流れでlisten()したソケットに対してconn_new(sfd, conn_listening, EV_READ | EV_PERSIST, 1, false, main_base)
    • drive_machine()でクライアントからの接続をaccept()した後、そのソケットに対してdispatch_conn_new(sfd, conn_read, EV_READ | EV_PERSIST, DATA_BUFFER_SIZE, false)
  • TCP接続とfreeconns
    • 起動直後のTCPサーバー構築時
      • freecurrが0のまま、conn_new()内でconn_from_freelist()が呼ばれるが空なので、connが生成されstats.conn_structs++
    • memcacheクライアントからの接続(初回)
      • freecurrが0のまま、conn_new()内でconn_from_freelist()が呼ばれるが空なので、connが生成されstats.conn_structs++
    • memcacheクライアントからの接続(二回目)
      • freecurrが0のまま、conn_new()内でconn_from_freelist()が呼ばれるがが空なので、connが生成されstats.conn_structs++
    • memcacheクライアントからの接続(N回目)←上記同様
    • memcacheクライアント切断(初回分)
      • freecurrは0、conn_close()内でdo_conn_add_to_freelist()が呼ばれるので、使用していたconnがfreeconnsに積み上げられfreecurrインクリメント
    • memcacheクライアント切断(初回分)
      • freecurrは1、conn_close()内でdo_conn_add_to_freelist()が呼ばれるので、使用していたconnがfreeconnsに積み上げられfreecurrインクリメント
  • TCP接続関連のあっさりとしたまとめ
    • memcacheクライアントから接続がある度に"connを生成&関連するバッファを確保"するのは無駄なので、使い終わったconnはfreeconnに積み上げておく
    • memcacheクライアントから接続を受けた際、freeconnsにconnが積み上がっていたらそれをpopして使う(使い終わったらまたpush)
    • connを生成した数がstats.conn_structsなので、これが増加したということは、何らかの理由により「再利用されるよりも早いスピードでクライアントからの接続を受けた」のでは?