tokuhirom's Blog

Server::Starter を Java で利用する方法。または、System.inheritedChannel() の挙動について

Server::Starter を Java でも使いたいなあ、そうすれば LL の場合と同じような運用ができるなあ、という要望をたまに稀によく聞きます。 そんなときに決まって返される答えは、fdopen できないから無理じゃないすかね。。 ということである。

SO_REUSEPORT しよう

SO_REUSEPORT なら、java でもちょっとの工夫で使えるんじゃないの? っていう説が出てくる。

で、頑張れば jetty で SO_REUSEPORT 使っていい感じに実装できそうだな、ということはわかって、サンプルコードも書いてみた。

しかし、実装してから指摘されたのだが、弊社では CentOS 6 が標準となっており、CentOS 7 が来るのはいつになるのかさっぱりわからない。 つまり、とりあえずしばらくの間は実践で使える可能性がほぼないということだ。

とりあえず、出来たものは出来たので、置いておく。意外とリフレクションにまみれていて微妙である。

https://github.com/tokuhirom/jetty-so_reuseport-sample

System.inheritedChannel() の存在に気づく。

しかし、なんとかして出来ないものかと探していたら System.inheritedChannel() なる怪しいメソッドを jetty が利用していることに気づいた。

これは以下のようなものである。

Returns the channel inherited from the entity that created this Java virtual machine.
This method returns the channel obtained by invoking the inheritedChannel method of the system-wide default SelectorProvider object.

In addition to the network-oriented channels described in inheritedChannel, this method may return other kinds of channels in the future.

http://docs.oracle.com/javase/7/docs/api/java/lang/System.html#inheritedChannel%28%29

正味、この説明を読んでいてもまったく意味がわからない。

http://docs.oracle.com/javase/6/docs/technotes/guides/rmi/inetd/launch-service.html inetd で利用する方法が記載されているが、RMI とか関係なさげなコードも多くてあまり参考にならない。

詳細を追うには Open JDK のコードを見るのがよさそうだ。 Open JDK のコードを追っていくと、sun.nio.ch.InheritedChannel にたどり着く。

http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/sun/nio/ch/InheritedChannel.java

以下の部分が該当のコードとなる。

/*
 * If standard inherited channel is connected to a socket then return a Channel
 * of the appropriate type based standard input.
 */
private static Channel createChannel() throws IOException {

    // dup the file descriptor - we do this so that for two reasons :-
    // 1. Avoids any timing issues with FileDescriptor.in being closed
    //    or redirected while we create the channel.
    // 2. Allows streams based on file descriptor 0 to co-exist with
    //    the channel (closing one doesn't impact the other)

    int fdVal = dup(0);

    // Examine the file descriptor - if it's not a socket then we don't
    // create a channel so we release the file descriptor.

    int st;
    st = soType0(fdVal);
    if (st != SOCK_STREAM && st != SOCK_DGRAM) {
        close0(fdVal);
        return null;
    }


    // Next we create a FileDescriptor for the dup'ed file descriptor
    // Have to use reflection and also make assumption on how FD
    // is implemented.

    Class paramTypes[] = { int.class };
    Constructor<?> ctr = Reflect.lookupConstructor("java.io.FileDescriptor",
                                                   paramTypes);
    Object args[] = { new Integer(fdVal) };
    FileDescriptor fd = (FileDescriptor)Reflect.invoke(ctr, args);


    // Now create the channel. If the socket is a streams socket then
    // we see if tthere is a peer (ie: connected). If so, then we
    // create a SocketChannel, otherwise a ServerSocketChannel.
    // If the socket is a datagram socket then create a DatagramChannel

    SelectorProvider provider = SelectorProvider.provider();
    assert provider instanceof sun.nio.ch.SelectorProviderImpl;

    Channel c;
    if (st == SOCK_STREAM) {
        InetAddress ia = peerAddress0(fdVal);
        if (ia == null) {
           c = new InheritedServerSocketChannelImpl(provider, fd);
        } else {
           int port = peerPort0(fdVal);
           assert port > 0;
           InetSocketAddress isa = new InetSocketAddress(ia, port);
           c = new InheritedSocketChannelImpl(provider, fd, isa);
        }
    } else {
        c = new InheritedDatagramChannelImpl(provider, fd);
    }
    return c;
}

これならわかるぞ! ッて感じのコードである。 fd 0 、つまり STDIN がソケットであるときには、System.InheritedChannel() でソケットが取れる。

この挙動はつまり、JVM は fdopen(0) ならサポートしているということになる。 そして、このあたりのコードをリフレクションにまみれながら引きずり出せば dup2 や fdopen も可能そうだということもわかる。

あれこれ Server::Starter 使えるんじゃね?

リフレクションで頑張って引きずり出してもいいのだが、可搬性に乏しくなってしまうし、将来的に動かなくなるリスクも多きそうだというところで、よく考えてみると。。。。

実は Server::Starter が渡してきた fd を fdopen してから dup2 で STDIN に設定するラッパーをかませば Server::Starter 配下で実行できる気がしてくる。

その挙動をするスクリプトが以下になる。

use strict;
use warnings;

my $ssp = $ENV{SERVER_STARTER_PORT}
    or die "Missing SERVER_STARTER_PORT";
my ($port, $fd) = split /=/, $ssp;

open my $fh, ">&=${fd}"
    or die "Cannot dup ${fd}: $!";
open STDIN, '<&', $fh
    or die "Cannot dup: $!";

exec @ARGV;
die "could not exec(@ARGV): $!";

さて、このスクリプトをかませて、jetty を起動するには以下のように setInheritChannel を呼ぶようにコードを変更するだけでよい。

import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Slf4jRequestLog;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.server.ServerConnector;

public class Httpd {
        public static void main(String[] args) throws Exception {
                int port = 18080;
                Server server = new Server();

                ServerConnector connector = new ServerConnector(server);
                connector.setInheritChannel(true); // ←←←
                connector.setPort(port);
                server.setConnectors(new Connector[]{connector});

                HandlerCollection handlers = new HandlerCollection();
                server.setHandler(handlers);

                Slf4jRequestLog requestLog = new Slf4jRequestLog();
                requestLog.setExtended(true);
                requestLog.setLogCookies(false);
                requestLog.setLogTimeZone("GMT");
                RequestLogHandler requestLogHandler = new RequestLogHandler();
                requestLogHandler.setRequestLog(requestLog);
                handlers.addHandler(requestLogHandler);

                server.start();
                server.join();
        }

}

まとめ

Server::Starter から渡ってきた fd を 0 に dup2 することにより、JVM 言語でも Server::Starter を利用可能であることを示した。 Server::Starter 側に fd 0 に dup2 する機能がつけば、もっと気軽に使えるようになるのだが。。