ハイパーマッスルエンジニア

Vim、ShellScriptについてよく書く

phpで書いたスクリプトをGoで書いたら7倍になった(phpとgoではビルトインサーバーが違うからだった)

Instagramぶっこ抜きを作った

instagramのページURLを与えると投稿画像、投稿文、ユーザー名を取得するシステム(?)を作った。
当初はphpで作っていたが、勉強がてらGoで書き直してみたら57秒→8秒と約7倍になった出来事を話したい。

結論から言うと、GoとPHPで立てるサーバーの設定が違ったからだった。
勘違いワロタ、、、

デモ

まずはどんなものを作ったか紹介。

github.com

https://user-images.githubusercontent.com/17779386/59974954-5bb6b880-95ed-11e9-9eb3-56627862e1c8.gif
URLを画面に入力して、「ぶっこ抜く」ボタンを押すと画面下部に結果を表示する。

実際に使ってみたいという方はこちら

Instagramぶっこ抜き

構成

f:id:rasukarusan:20190623204910p:plain

サーバー側でやっていることは単純で、

  1. cURLする
  2. 正規表現で情報を抜き出す
  3. JSONレスポンスとして返す

これだけ。

PHPのソース

エラー処理とか載せると長くなるので割愛。クラス分けとかしていない完全にペラ1のスクリプト。
これをGoで書き直した。

<?php
$request = json_decode(file_get_contents('php://input'), true);
$urls = $request['URLs'];

foreach ($urls as $url) {
    $json      = getJsonByUrl($url);
    $username  = getUsername($json);
    $post_text = getPostText($json);
    $image_url = getImageUrl($json);

    $result = [[
        'Username' => $username,
        'ImageURL' => $image_url,
        'PostText' => $post_text,
        'OrgURL'   => $url,
    ]];
    echo json_encode($result);
}
return;

/**
 * InstagramにcurlしてJSONを取得する
 */
function getJsonByUrl($url) {
    $url = preg_replace('/\n|\r|\r\n/', '', $url);
    $curl_result = fetch($url);
    preg_match_all('/window._sharedData.*{.*}/', $curl_result, $match);
    $json_str = str_replace('window._sharedData = ','',$match[0][0]);
    return json_decode($json_str);
}

function getImageUrl($json) : string {
    return $json->entry_data->PostPage[0]->graphql->shortcode_media->display_url;
}

function getPostText($json) : string {
    return $json->entry_data->PostPage[0]->graphql->shortcode_media->edge_media_to_caption->edges[0]->node->text;
}

function getUsername($json) : string {
    return $json->entry_data->PostPage[0]->graphql->shortcode_media->owner->username;
}

function fetch(string $url) : string {
    $ch = curl_init();
    $options = [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_CUSTOMREQUEST => 'GET',
    ];
    curl_setopt_array($ch, $options);
    $resp = curl_exec($ch);
    curl_close($ch);
    return $resp;
}

Goで書き直してみたら早くなった

phpとgoの比較動画。

https://rasukarusan.github.io/blog-assets/go-instagram-php/php_vs_go.gif
左がPHP、右がGoで動いているサーバーに向けてPOSTしたもの

ざっくり計算だとPHPが完了までにかかる時間は57秒、Goは8秒で終わった。
なんでこんなに違うのか考察してみる。

やってることはPHPもGoもほぼ同じ。

・サーバー立てる
・httpリクエスト投げる
・正規表現で抜く
・JSON形式に変換して吐き出す

これのどこかでパフォーマンスに大きく差が出ているはず。。

立てたサーバーの設定が違うのが大きな原因

ここで冒頭の結論に戻るのだがlocalhostで立てるサーバーがPHPとGoでは違う。
サーバーを立てるのはそれぞれ下記の方法で立てた。

・phpの場合

$ php -S localhost:9000

・Goの場合

net/httpを使用。main.goに下記を記載。

http.ListenAndServe(":9000", api.MakeHandler())

Chromeのデベロッパーツールで5リクエスト出した時のパフォーマンスを計測してみると以下の結果に。

f:id:rasukarusan:20190623202210p:plain:w500
5リクエストしたときのPHPとGoのパフォーマンス

明らかにレスポンスの挙動が違う。。。
Goでは一気に5レスポンス返ってきているのに対し、phpでは1レスポンス待ってから次の処理が開始されているように見える。

PHPのビルトインサーバーについて

PHPManualに全ての答えが書いてあった。

ビルトインウェブサーバー このウェブサーバーは単一のシングルスレッドプロセスしか実行しないので、 リクエストがブロックされると、PHP アプリケーションはストールします

https://php.net/manual/ja/features.commandline.webserver.php

つまり今回の場合、phpで5リクエスト投げた時2つ目以降の処理はWAITになっていた、ということだった。

Herokuにデプロイして試してみた

ならばと思い、apache等でちゃんとマルチスレッドで動いているサーバーなら実行は早くなるのか試してみた。 下記はHerokuにデプロイして検証したもの。

https://rasukarusan.github.io/blog-assets/go-instagram-php/heroku-php.gif
herokuでphpスクリプトを動かしてみた

おお、、、ちゃんと早くなっている。
何回か計測してみると、やはりGoの方が速いがビルトインサーバーでやっていたときよりは段違いに速くなった。

ちなみにherokuのapache2の同時実行数は4でした。(phpinfo()で見れるEVN['WEB_CONCURRENCY']がそれらしい)

f:id:rasukarusan:20190623202254p:plain:w500
herokuのパフォーマンス結果

じゃあGoの同時実行数はどうなの?

Goでnet/httpでサーバー立てたときの同時実行数がわからない、、、。
ググってもどうも調べ方が悪いのか出てこない。そもそも俺の考え方が違うような気がする。

一応Chromeのデベロッパーツールで見てみたところ、30リクエストまでは同時に実行できていた。

f:id:rasukarusan:20190623202330p:plain:w500
Goの同時実行数

終わり

Goのhttpサーバーについてもうちょっと調べたい。
Goにしただけでめっちゃ速度上がった!と思っていたので少し残念だったが、ワンレスポンスだけ見たら3倍以上Goのほうが早かったので、言語の処理速度もやはり関係はしている。
なによりGoは書いていて楽しいからしばらくハマっていたい。
今の所やりたいことは

  • Herokuじゃなくてlambdaで動かす
  • 画面のリクエスト分割(1URL1リクエストではなくある程度URLをまとめて1リクエスト)
  • テストを書く

ぐらい。割とすぐいけそうな予感はしてる。