日向夏特殊応援部隊

俺様向けメモ

Note of MogileFS #09 - Inside MogileFS architecture (2)

はじめに

前回のエントリ(d:id:ZIGOROu:20061018:1161155472)の続きです。
前回は主にdaemonとしての起動からclientからのリクエスト受信、コマンドのパース辺りまでだったんで、
実際に発行されるcreate_openコマンドなどを見て行こうと思います。

MogileFS::Worker::Query->cmd_create_open

ここのソースは相当長いです。気合入れて読まないと迷子になります。

sub cmd_create_open {
    my MogileFS::Worker::Query $self = shift;
    my $args = shift;

    # has to be filled out for some plugins
    $args->{dmid} = $self->check_domain($args)
        or return $self->err_line('domain_not_found');

    # first, pass this to a hook to do any manipulations needed
    MogileFS::run_global_hook('cmd_create_open', $args);

ドメインのチェックはスキップ。
面白いのはMogileFS::run_global_hookですかね。


きちんとhookを掛けれる仕様のようです。前回のエントリでも触れましたがMogileFSってpackageの実体はmogilefsdの中にあります。

MogileFS::run_global_hook
sub register_global_hook {
    $hooks{$_[0]} = $_[1];
    return 1;
}

sub unregister_global_hook {
    delete $hooks{$_[0]};
    return 1;
}

sub run_global_hook {
    my $hookname = shift;
    my $ref = $hooks{$hookname};
    return $ref->(@_) if defined $ref;
    return undef;
}

と言う訳でいずれの関数も%hooksと言うハッシュへのアクセサですね。
実体はもっと別の場所にあると。


serverのtrunkでackで検索。

$ ack --perl register_global_hook
mogilefsd
783:sub register_global_hook {
788:sub unregister_global_hook {

lib/MogileFS/Plugin/FilePaths.pm
23:    MogileFS::register_global_hook( 'cmd_create_open', sub {
30:    MogileFS::register_global_hook( 'cmd_create_close', sub {
41:    MogileFS::register_global_hook( 'file_stored', sub {
74:    MogileFS::register_global_hook( 'cmd_get_paths', \&_path_to_key );
75:    MogileFS::register_global_hook( 'cmd_delete', \&_path_to_key );
140:    MogileFS::unregister_global_hook( 'cmd_create_open' );
141:    MogileFS::unregister_global_hook( 'cmd_create_close' );
142:    MogileFS::unregister_global_hook( 'file_stored' );

と言う訳でMogileFS::Plugin::FilePathsで設定されてるみたいですね。
但し再びack使って検索してみると、

$ ack --perl "Plugin"
lib/MogileFS/Config.pm
169:        my $rv = eval "use MogileFS::Plugin::$plugin; MogileFS::Plugin::$plugin->load; 1;";

lib/MogileFS/Plugin/FilePaths.pm
8:package MogileFS::Plugin::FilePaths;
53:        my $parentnodeid = MogileFS::Plugin::FilePaths::vivify_path( $args->{dmid}, $path );
57:        my $oldfid = MogileFS::Plugin::FilePaths::get_file_mapping( $args->{dmid}, $parentnodeid, $filename );
65:        my $nodeid = MogileFS::Plugin::FilePaths::set_file_mapping( $args->{dmid}, $parentnodeid, $filename, $args->{fid} );
94:        my $nodeid = MogileFS::Plugin::FilePaths::load_path( $dmid, $path );
101:        my @files = MogileFS::Plugin::FilePaths::list_directory( $nodeid );
256:    my $parentnodeid = MogileFS::Plugin::FilePaths::load_path( $args->{dmid}, $path );
260:    my $fid = MogileFS::Plugin::FilePaths::get_file_mapping( $args->{dmid}, $parentnodeid, $filename );

と言う訳でMogileFS::Configで呼び出してますね。
従ってPluginで設定されているhookは全て設定されてます。*1


ちなみにloadメソッドで設定できますし、自分で好きなhookを記述する事も出来るでしょう。

MogileFS::Worker::Query->cmd_create_open (2)

でまたcmd_create_openの続き。

    # validate parameters
    my $dmid = $args->{dmid};
    my $key = $args->{key} || "";
    my $multi = $args->{multi_dest} ? 1 : 0;
    my $fid = ($args->{fid} + 0) || undef; # we want it to be undef if they didn't give one
                                           # and we want to force it numeric...

    # get DB handle
    my $dbh = Mgd::get_dbh() or
        return $self->err_line("nodb");

ここで重要なのはmulti_destオプションくらいか?
create_openだと強制的にtrueです。

    # figure out what classid this file is for
    my $class = $args->{class} || "";
    my $classid = 0;
    if (length($class)) {
        # TODO: cache this
        $classid = $dbh->selectrow_array("SELECT classid FROM class ".
                                         "WHERE dmid=? AND classname=?",
                                         undef, $dmid, $class)
            or return $self->err_line("unreg_class");
    }

classの存在チェックですね。

    # if we haven't heard from the monitoring job yet, we need to chill a bit
    # to prevent a race where we tell a user that we can't create a file when
    # in fact we've just not heard from the monitor
    while (! $self->monitor_has_run) {
        $self->read_from_parent;
        $self->still_alive;
        sleep 1;
    }

monitorのworkerの生存チェック。*2


ここからが重要だと思われる。

    # find a device to put this file on that has 100Mb free.
    my (@dests, @hosts);
    my $devs = Mgd::get_device_summary();

    while (scalar(@dests) < ($multi ? 3 : 1)) {
        my $devid = Mgd::find_deviceid(
                                       random           => 1,
                                       must_be_writeable => 1,
                                       weight_by_free   => 1,
                                       not_on_hosts     => \@hosts,
                                       );
        last unless defined $devid;

        push @dests, $devid;
        push @hosts, $devs->{$devid}->{hostid};
    }
    return $self->err_line("no_devices") unless @dests;

trackerの数及びmultiがtrueかどうかでそもそもwhileループの条件が異なります。
$devidが取れる間はlastで抜けない事から、ランダムでdeviceを取ってくるって事ですね。
さらに$devidを@destsに追加している事から、ループの条件に関係してきます。


$multiがfalseな場合最初のループが実行された後にwhileのstatementでfalseになるので処理が抜けますね。
よって@dests, @hostsは要素数1の配列。


$multiがtrueの場合は、デバイスを最大3つ取り出すまでランダムで抽出します。

    my $explicit_fid_used = $fid ? 1 : 0;
    # setup the new mapping.  we store the devices that we picked for
    # this file in here, knowing that they might not be used.  create_close
    # is responsible for actually mapping in file_on.  NOTE: fid is being
    # passed in, it's either some number they gave us, or it's going to be
    # undef which translates into NULL which means to automatically create
    # one.  that should be fine.
    my $ins_tempfile = sub {
        $dbh->do("INSERT INTO tempfile SET ".
                 " fid=?, dmid=?, dkey=?, classid=?, createtime=UNIX_TIMESTAMP(), devids=?",
                 undef, $fid, $dmid, $key, $classid, join(',', @dests));
        return undef if $dbh->err;
        unless (defined $fid) {
            # if they did not give us a fid, then we want to grab the one that was
            # theoretically automatically generated
            $fid = $dbh->{mysql_insertid};  # FIXME: mysql-ism
        }
        return undef unless defined $fid && $fid > 0;
        return 1;
    };

    return undef unless $ins_tempfile->();

テンポラリファイルと言う物をDB上にセットします。*3

    my $fid_in_use = sub {
        my $exists = $dbh->selectrow_array("SELECT COUNT(*) FROM file WHERE fid=?", undef, $fid);
        die if $dbh->err;
        return $exists ? 1 : 0;
    };

    # if the fid is in use, do something
    while ($fid_in_use->($fid)) {
        return $self->err_line("fid_in_use") if $explicit_fid_used;

        # mysql could have been restarted with an empty tempfile table, causing
        # innodb to reuse a fid number.  so we need to seed the tempfile table...

        # get the highest fid from the filetable and insert a dummy row
        $fid = $dbh->selectrow_array("SELECT MAX(fid) FROM file");
        $ins_tempfile->();

        # then do a normal auto-increment
        $fid = undef;
        return undef unless $ins_tempfile->();
    }

fidが被らないようにテンポラリファイルを作るってとこですね。

    # make sure directories exist for client to be able to PUT into
    foreach my $devid (@dests) {
        my $path = Mgd::make_path($devid, $fid);
        Mgd::vivify_directories($path);
    }

ここが凄い重要!


Mgdも前回のエントリで述べた通り、mogilefsdに含まれます。

Mgd::make_path
sub make_path {
    return Mgd::make_full_url(@_);
}

はい、次。

Mgd::make_full_url
sub make_full_url {
    # set use_get_port to be true to specify to use the get port
    my ($devid, $fid, $use_get_port) = @_;

    # get some information we'll need
    my $devs = Mgd::get_device_summary();
    my $dev = $devs->{$devid} or return undef;
    my $path = Mgd::make_http_path($devid, $fid) or return undef;
    my $host = Mgd::hostid_ip($dev->{hostid}) or return undef;
    my $port = $use_get_port ? Mgd::hostid_http_get_port($dev->{hostid}) : undef;
    $port ||= Mgd::hostid_http_port($dev->{hostid}) or return undef;
    return "http://$host:$port$path";
}

細かい事は抜きにして、理屈上配置しうるWebDAVリソースの場所、
ぶっちゃけURLが返ってきます。


このloopではさらにMgd::vivify_directoriesってメソッドを呼び出してます。

Mgd::vivify_directories()

長いけど、コメントが分かりやすいので、読みやすい。

sub vivify_directories {
    my $path = shift;
    # $path is something like:
    #    http://10.0.0.26:7500/dev2/0/000/148/0000148056.fid

    # three directories we'll want to make:
    #    http://10.0.0.26:7500/dev2/0
    #    http://10.0.0.26:7500/dev2/0/000
    #    http://10.0.0.26:7500/dev2/0/000/148

つまり再帰的に指定したデバイスディレクトリ作ってくれるって事ですね。

    foreach my $path (@need) {
        $depth++;
        next if $dir_made{$path};
        my $sock = IO::Socket::INET->new(PeerAddr => $peer, Timeout => 1)
            or next;
        print $sock "MKCOL $path HTTP/1.0\r\n".
            "Content-Length: 0\r\n\r\n";
        my $ans = <$sock>;
        $dir_made{$path} = [$depth, $now];
    }

ちなみにMKCOLってメソッドで作る模様。*4



と言う訳で先のforeachは1〜3個の@destsに対してdeviceのID、
WebDAVのURLを突っ込んであげて、

    # original single path support
    return $self->ok_line({
        fid => $fid,
        devid => $dests[0],
        path => Mgd::make_path($dests[0], $fid),
    }) unless $multi;

最後にレスポンスをclientに返します。*5
但しこれは単一指定がある場合、multi_destが0だとこのようになります。*6

    # multiple path support
    my $ct = 0;
    my $res = {};
    foreach my $devid (@dests) {
        $ct++;
        $res->{"devid_$ct"} = $devid;
        $res->{"path_$ct"} = Mgd::make_path($devid, $fid);
    }
    $res->{fid} = $fid;
    $res->{dev_count} = $ct;
    return $self->ok_line($res);
}


ここまでで言えばcreate_openコマンドはこれからファイルを突っ込むよって言う宣言と、その下準備で実はstorage nodeにはディレクトリを作成するのみで、他には何もやっていないようですね。
ここではmultiがtrueで多くて3つのデバイスがランダムで抽出されて出てきますけど、この際にdeviceにはstrageのhostが当然含まれますので、mogadmで設定したhostのdocroot/device以下にディレクトリが出来ている事になります。

MogileFS::Client->new_file

create_openコマンド後です。

    my $dests = [];  # [ [devid,path], [devid,path], ... ]

    # determine old vs. new format to populate destinations
    unless (exists $res->{dev_count}) {
        push @$dests, [ $res->{devid}, $res->{path} ];
    } else {
        for my $i (1..$res->{dev_count}) {
            push @$dests, [ $res->{"devid_$i"}, $res->{"path_$i"} ];
        }
    }

multiがtrueの時にquery workerでは複数のdeviceを恐らく選択できているはずでそのカウンタもレスポンスに含まれるので、恐らくelse, forと続きます。


いずれにせよ保存先を$destsに登録します。

    my $main_dest = shift @$dests;
    my ($main_devid, $main_path) = ($main_dest->[0], $main_dest->[1]);

    # create a MogileFS::NewHTTPFile object, based off of IO::File
    unless ($main_path =~ m!^http://!) {
        Carp::croak("This version of MogileFS::Client no longer supports non-http storage URLs.\n");
    }

    return IO::WrapTie::wraptie('MogileFS::NewHTTPFile',
                                mg    => $self,
                                fid   => $res->{fid},
                                path  => $main_path,
                                devid => $main_devid,
                                backup_dests => $dests,
                                class => $class,
                                key   => $key,
                                content_length => $bytes+0,
                                );

保存先のうち抽出した先頭を代表としてMogileFS::NewHTTPFileモジュールをIO::WrapTieでtieしてIOハンドルを返します。
つまりIOハンドルとして使えるんだけど、中でどんなマジック使ってるかはMogileFS::NewHTTPFileに依存って訳です。


続きはまた次回!

*1:ここ、見落としてたんで訂正しました。

*2:多分死んでたら復帰させると思われる、未確認

*3:ファイルの実体ではなくてこれからファイル突っ込むぞって宣言みたいなもんだと思われる。

*4:まさか、httpの一般的なメソッド?

*5:ok_lineメソッド

*6:ちなみにcreate_openコマンドは強制的にtrue