Twitter4JのSPDY対応について

投稿者: | 2014/01/05

この記事は主に開発者向けの内容です。

この記事は2014年1月に作成したもので、最新の情報ではありません。特にTwitPaneおよびTwitter4Jは2014年3月15日現在、SPDYだけでなくHTTP/2.0にも対応しています。


Twitter4J を次世代 HTTP の SPDY に対応させてみました。

拙作の Twitter クライアント TwitPane では2013年12月リリースの Ver.3.3.1 より SPDY に対応しました。

本ページでは TwitPane で利用してみた結果も踏まえて、Twitter4J の SPDY 対応方法についてまとめています(作者のブログ で随時書いていた内容のまとめ+αになります)。

Twitter4J-spdy-support

導入方法

Twitter4J を導入済みのアプリに対して、下記2つの jar をクラスパスに(Androidならlibsに)追加してください。

あとは twitter4j が自動的に(この jar に含まれる) “twitter4j.internal.http.alternative.HttpClientImpl” を見つけて使ってくれます。

対応環境

  • Twitter4J および OkHttp の対応環境に依存します
  • 但し、Android の場合 SPDY は Android 4.1 以降で対応します(Android 4.0 以前は HTTP/1.1 でアクセスします)
  • Java の場合は npn-boot が必要です(後述)

詳細な使い方とTips

SPDY通信の確認方法

Twitter4J の SPDY 拡張(twitter4j-spdy-support)を利用しても本当に SPDY 通信してるか不安ですよね。

一応下記のようなコードで Twitter オブジェクト(twitter) から SPDY のコネクションプール数を取得できます(リフレクションを利用しているので Twitter4J Ver.3.0.5 以降の版では動作しなくなる可能性があります)。

String message = "SPDY : ";
if (!twitter4j.internal.http.alternative.HttpClientImpl.sPreferSpdy) {
    message += "Disabled";
} else {
    message += "Enabled(";
    try {
        final Class<?> clazz = Class.forName("twitter4j.TwitterBaseImpl");
        final Field f1 = clazz.getDeclaredField("http");
        f1.setAccessible(true);

        // wrapper = twitter.http
        final HttpClientWrapper wrapper = (HttpClientWrapper) f1.get(twitter);
        final Field f2 = HttpClientWrapper.class.getDeclaredField("http");
        f2.setAccessible(true);

        // http = wrapper.http
        final twitter4j.internal.http.alternative.HttpClientImpl http = 
                (twitter4j.internal.http.alternative.HttpClientImpl) f2.get(wrapper);
        final Field f3 = http.getClass().getDeclaredField("client");
        f3.setAccessible(true);

        // client = http.client
        final OkHttpClient client = (OkHttpClient) f3.get(http);

        if (client != null) {
            final ConnectionPool p = client.getConnectionPool();
            message += "SPDY[" + p.getSpdyConnectionCount() + "], ";
            message += "HTTP[" + p.getHttpConnectionCount() + "])";
        } else {
            message += "no connections yet)";
        }

        final String lastRequestTransport = http.getLastRequestTransport();
        if (lastRequestTransport != null) {
            message += "\nOkHttp-Selected-Transport:[" + http.getLastRequestTransport() + "]";
        }

    } catch (Exception e) {
        Log.e(TAG, e.getMessage(), e);
    }
}
Log.d(TAG, message);

Twitter.getHomeTimeline() などを呼び出したあとに ConnectionPool.getSpdyConnectionCount() が 1 以上であれば正しくSPDY通信していると判定できます。

(2014/1/5追記) さらに、上記のように HttpClientImpl.getLastRequestTransport() で(後述の) OkHttp-Selected-Transport ヘッダーを取れるようにしました。”spdy/3″ のようなプロトコル名を取得できます。

ちなみに TwitterFactory.getInstance() で Twitter オブジェクトを作った単位でプールが共有されます。 通信のたびに TwitterFactory.getInstance() していると SPDY の意味がないので注意が必要です。

SPDYをオフにする設定

上の例のように twitter4j.internal.http.alternative.HttpClientImpl.sPreferSpdyfalse に設定することで SPDY 通信を強制的にオフにすることが出来ます。 これで、SPDY通信をオプション化することができます(TwitPaneでSPDY切替をするためにこの仕組みを入れてあります)。

OkHttp 単体を利用する場合の SPDY 通信の確認方法

Twitter4J ではなく OkHttp 自体を使う場合は (OkHttpClient.open() で取得した) HttpURLConnection からレスポンスヘッダーを見ると下記のようなカスタムヘッダーが取れます。

OkHttp-Received-Millis:[1388254484917]
OkHttp-Response-Source:[NETWORK 200]
OkHttp-Selected-Transport:[http/1.1]
OkHttp-Sent-Millis:[1388254484817]

SPDY 通信をしている場合は OkHttp-Selected-Transportspdy/3 などになります。

Java(非Android)で使うときの設定

Eclipseやコンソール上のjavaコマンドから実行するとどうにもSPDY通信してくれないので困っていました。

その後いろいろと調査した結果、どうやらJVMにnpn-bootというものが必要らしいです。

Jetty/Feature/NPN – Eclipsepedia

To enable NPN support, you need to start the JVM with:

java -Xbootclasspath/p:

肝心の npn_boot_jar も上記リンクからダウンロードできます(「もうjettyのバージョンに合わせてないのでファイル名のバージョンではなく日付に注目してね」って書いてあるので注意)。

NPN の仕組みは JVM が標準ではサポートしていないということですね。

むしろAndroid/Dalvik VM(4.1以降)が標準でサポートしているところがさすが Google 製といったところでしょうか。。

D:\(略)>java -version
java version "1.7.0_04"
Java(TM) SE Runtime Environment (build 1.7.0_04-b20)
Java HotSpot(TM) 64-Bit Server VM (build 23.0-b21, mixed mode)


D:\(略)>"C:\Program Files\Java\jdk1.7.0_04\bin\java" -Xms30m -Xmx30m -Xbootclasspath/p:..\npn-boot-8.1.2.v20120308.jar -classpath (中略) twitter4j.examples.tweets.ShowStatusBenchmark
start
 [1357ms]
 [160ms]
 [176ms]
average:[564ms]
rate limit:[159/180], [687sec]
SPDY : [1/1]

たぶん java で SPDY やってる人には常識なんでしょうけど、ググラビリティが低くて2時間も悩んでしまったのでどなたかの参考になれば。

簡易ベンチマーク(Androidの場合)

簡易ベンチマーク(並列実行)

拙作の Twitter クライアント TwitPane に入れてベンチマークを取ってみました。

単一のツイートを取得するメソッドである showStatus を 100ms ずつずらして11回1リクエストした結果です。SPDY版がいい感じで速いです。

GalaxyNexus SC-04D/Android 4.2

環境 試行1回目(平均所要時間) 2回目
no SPDY, 3G 2095ms 1980ms
SPDY, 3G 1088ms(48%高速) 1398ms(29%高速)
no SPDY, WiFi/B-Flets 654ms 606ms
SPDY, WiFi/B-Flets 527ms(19%高速) 505ms(17%高速)

この条件では概ね 20% 程度速くなりそうな感じです。

測定に使ったコード: https://gist.github.com/takke/8143276

簡易ベンチマーク(シーケンシャル実行)

先ほどの showStatus のコードを単純に11回繰り返し実行するパターンもやってみました。

環境 平均所要時間
no SPDY, 3G 1409ms
SPDY, 3G 780ms(45%高速)
no SPDY, WiFi/B-Flets 689ms
SPDY, WiFi/B-Flets 452ms(34%高速)

シーケンシャルに取得するパターンでも34%~45% ほど高速化されました。 これは使ってみたいと思える改善っぷりですね。素晴らしいです。

Android版Twitter4Jの注意

上記2件の測定結果は試行回数が少ないとはいえ、10%~40%程度の有意な改善結果になっています。 しかし実際にはこれは Android版Twitter4J 特有の事情によるものと思われます。

Android/Dalvik 環境で Twitter4J を利用する場合、とあるバグ の回避のために HTTP/1.1 の KeepAlive が無効になります。そのため、通信のたびに再接続するというオーバーヘッドがあります。

SPDY 版ではこのロスを回避できるため、比較的大きな改善になっているのだと思います。 (つまり、AndroidでSPDYを利用すれば(GoogleやFacebookなど他の)あらゆるサービスに対してこれだけの改善効果が出るというわけではないと思います)

(2014/3/24追記) Android2.3以降はKeepAlive有効なのかも?

↑のように、Android2.3以降ではKeepAliveは有効かもしれないという情報がありました。


で解説されてるようにAndroid2.3以降は有効とのことでした。

ですが、実際にNexus5/Android4.4.2で確認してみるとhttp.keepAliveはfalseでした。

http://gyazo.com/148f8f0ca91ff17fc654a52db2c348f4

Android2.3 以降では確かに dalvik.system.VMRuntime が存在しないのですが、Class.forName は通るようです。つまりそういうことなんでしょう。

というわけで、Android 2.3 以降も Twitter4J を使うと KeepAlive は無効になっていて、その分、spdy-support を導入すると速くなるのではないかと考えています。

※このペナルティがけっこう大きいと思うので(元のバグはAndroid2.2で解消されているので)Android 2.1以前でのみkeepAliveを無効にする修正を行ってプルリク送っています。

そのうちマージされるでしょう⇒ https://github.com/yusuke/twitter4j/pull/137

簡易ベンチマーク(Java/Windowsの場合)

シーケンシャルに実行するパターンではSPDYを使うと3%~5%ほど速いっぽい

Java/Windows で測定してみたところ、平均値ではSPDYを使わないほうが速いのですが、よく見ると1発目が遅いだけで2回目以降はSPDYのほうが3%~5%ほど速いことが分かりました。

NPNの負荷について

上記の結果で「1発目はSPDYが遅い」結果になっていましたが、これはSPDYのNPNによるプロトコル選択が影響していると推測できます。

Adopting SPDY in Line – Part 1: An Overview で解説されているように、LINEではNPNなしでSPDYを利用しているようですし、サーバ側のプロトコルサポート状況が既知である場合はNPNは不要ですよね。

そこで okhttp でも NPN をスキップできないかソースを読んでみましたが、どうやら現状の実装ではできないようです。

(2014/1/7追記) NPNを不要にするOkHttpパッチとベンチマーク結果など

OkHttp で特定のホストに対して NPN を不要にするパッチを作ってみました。 性能はほとんど変わらなかったので前述の「NPNの負荷について」に書いた推測は間違っていました。SSL/TLS自体の接続が遅いという理解でいいかと思います。

というわけで https://github.com/square/okhttp/pull/402#issuecomment-31606818 のプルリクを送りましたが OkHttp の開発方針と合わないといった理由で却下されました。まぁ、OkHttp自体も利用するソフトウェアも保守しにくくなるんで妥当だと思います。

まとめ:Twitter4J で SPDY を有効活用できるパターンはあるのかな?

TwitterのAPIはいずれも重く、最短でも300ms~500ms程度はかかるので、SPDY による高速性は相対的に低く感じられます。

ただ、Twitter、CocoaSPDYをオープンソースに の記事にもあるように、iOS向けのTwitter公式アプリでは SPDY がデフォルトで有効になっています。

Twitterはこれにより通信遅延を最大30%削減でき、「ユーザのネットワーク状況が悪いとき」ほど改善が顕著に見られると説明している。

Twitterに限らず、2014年はSPDY対応が当然になっていくのかもしれませんね。

また、SPDYの一般的な利点として、「リクエストの多重化」があります。単一の TCP セッションで複数・バラバラのレスポンスを受信できるという機能ですが、Twitter4J の使い方でもこのパターンはあり得るかと思います。

拙作のTwitPaneはタブベースのTwitterクライアントですが、タブAのロード中にタブBを開くとタブAとBが同時にロードすることになります。さらに、バックグラウンドで新着の返信やDMを取得するタスクもあるため、複数のリクエストを同時に投げるシチュエーションがわずかながら存在します。 このような状況下では1本の TCP セッションを共有できるため、SPDYの恩恵は多少なりとも得られるのかと思います。特にモバイル環境下では顕著かもしれません。


  1. 10回のつもりだったけどベンチマーク用に10回繰り返したあとに1回本来の処理が走ってた…