yasutomogのブログ

Software Engineerの雑記

PHPでGuzzleHttpを使用して並列処理(同時実行数の制御)の確認

概要

  • PHPでHTTP通信の並列処理をする
  • curl_multiではなくGuzzleHttpを使って試す
  • GuzzleHttpは並列実行が元々用意されている

知りたかったこと

  • GuzzleHttpでPoolの使用時に設定するconcurrency(並列処理数)の制御
  • 具体的には、全部で10リクエスト投げる処理でconcurrencyに2を設定して実行した場合、以下のどちらになるか調査
    • 片一方のリクエストが先に返ってきても、もう片方のリクエストが返るまで待ち、常に2リクエストずつ処理する
    • レスポンスが返ってきて空いたところから次のリクエストを投げて常時2リクエストを保とうとする

試した環境

  • lumen: v5.5.2
  • GuzzleHttp: 6.3.3(lumen導入時に一緒に入ったもの)

試したサンプルコード

routes/web.php

  • リクエストパラメータの秒数分Sleep後にレスポンスを返す関数とURLのマッピング
$router->get('/wait', 'SampleController@wait');

app/Http/Controllers/SampleController.php

  • リクエストパラメータの秒数分Sleep後にレスポンスを返す関数
public function wait(Request $request)
{

    $wait = $request->input("sec");
    sleep((int)$wait);
    return "ok" . $wait;

}

app/Console/Kernel.phpの$commandsに追加

  • API呼び出しをartisanを呼び出すため、実行クラスを追加
    protected $commands = [
        \App\Console\Commands\Sample\ApiCaller::class,
    ];

app/Console/Commands/Sample/ApiCaller.php

  • APIを呼び出す実行クラス
  • 全部で10リクエスト呼び出す
  • 10リクエストの内、2つめ(indexが1)のリクエストのみ10秒でそれ以外は1秒で返されるようにしている
<?php

namespace App\Console\Commands\Sample;

use Illuminate\Console\Command;
use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;

class ApiCaller extends Command
{

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'apicall';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'API呼び出し処理';

    /**
     * API呼び出し処理
     */
    public function handle()
    {

        $this->multiReq();

    }

    private function multiReq()
    {

        $uri = [
            'http://localhost/api/wait?sec=1',
            'http://localhost/api/wait?sec=10',
            'http://localhost/api/wait?sec=1',
            'http://localhost/api/wait?sec=1',
            'http://localhost/api/wait?sec=1',
            'http://localhost/api/wait?sec=1',
            'http://localhost/api/wait?sec=1',
            'http://localhost/api/wait?sec=1',
            'http://localhost/api/wait?sec=1',
            'http://localhost/api/wait?sec=1',
        ];

        $client = new Client();
        $requests = function () use ($uri) {
            foreach ($uri as $u) {
                yield new Request('GET', $u);
            }
        };

        $pool = new Pool($client, $requests(), [
            'concurrency' => 2,
            'fulfilled' => function ($response, $index) {
                // this is delivered each successful response
                \Log::info("成功::index::" . $index);
            },
            'rejected' => function ($reason, $index) {
                // this is delivered each failed request
                \Log::info("失敗::index::" . $index);
            },
        ]);

        // Initiate the transfers and create a promise
        $promise = $pool->promise();

        // Force the pool of requests to complete.
        $promise->wait();

    }

}

実行コマンド

php artisan apicall

結果

  • 以下のようなログが出力される
  • 2つ目(indexが1)の10秒掛かるリクエストが返されるまでに3つ目のリクエストが投げられなか確認したところ、レスポンス待たずにどんどんリクエストしているので並列処理としては常時2つを保つように動いていることがわかった
[2018-12-05 17:17:34] lumen.INFO: 成功::index::0 [] []
[2018-12-05 17:17:36] lumen.INFO: 成功::index::2 [] []
[2018-12-05 17:17:38] lumen.INFO: 成功::index::3 [] []
[2018-12-05 17:17:40] lumen.INFO: 成功::index::4 [] []
[2018-12-05 17:17:42] lumen.INFO: 成功::index::5 [] []
[2018-12-05 17:17:43] lumen.INFO: 成功::index::1 [] []
[2018-12-05 17:17:44] lumen.INFO: 成功::index::7 [] []
[2018-12-05 17:17:44] lumen.INFO: 成功::index::6 [] []
[2018-12-05 17:17:45] lumen.INFO: 成功::index::8 [] []
[2018-12-05 17:17:45] lumen.INFO: 成功::index::9 [] []

Azure Data Studioの設定方法(SQLの実行計画)

概要

  • Azure SQL Databaseを使っていて、Azure Data Studio(MSが提供しているクライアントツール)を試してみた
  • 実行計画のところだけ、ハマったのでメモ

バージョン

  • OS:10.12.6
  • Azure Data Stduio:1.1.3

インストール

以下のサイトからダウンロードしてインストールするだけ!

docs.microsoft.com

DB接続

既にAzure SQL Databaseが用意されていれば、以下のサイトを参考にして接続情報を入力するだけ!

docs.microsoft.com

Widget追加(ダッシュボード)

  • これまで、Azure Portal上で確認していた遅いクエリなどをクライアントツールからも確認可能となる
  • 設定しておいて損はないと思うので、以下のサイトを参考にして設定するだけ!

docs.microsoft.com

実行計画

  • 参考にするサイトは、Widget追加と同じ
  • 参考サイトだと、実行計画の確認するときに「Explain」ボタンがクリックするようになっているが、デフォルトだと「Explain」ボタンが表示されていない
  • 「Explain」ボタンはデフォルト非表示になっているで以下の流れで表示するように設定する

    • Azure Data Studioを起動しDBに接続する
    • 「Command + Shift + P」でコマンドパレットを開く
    • コマンドパレットから「Preferences: Open User Settings」を開く
    • ユーザー設定画面で、「workbench.enablePreviewFeatures」を検索
    • 左側の鉛筆アイコンをクリックし、「false」から「true」へ変更
    • trueを設定すると、以下のスクショのように右側に反映される

    f:id:yasug:20181031103225p:plain

  • Command + Nとかでクエリのページを新規で開くと、「Explain」ボタンが表示されている

Nuxt.jsの開発サーバでHot Module Reloading (HMR)のエラー対策

概要

  • Nuxt.jsのテンプレートを使ったプロジェクト
  • 「yarn run dev」コマンドで開発サーバ起動(デフォルトの3000ポート)
  • コンソールログに「hot-update.json timed out」というエラー出力
  • ホットリローディングができない

バージョン

  • node:10.12.0
  • nuxt.js:2.0.0
  • nuxtjs/pwa:2.6.0
  • vue-cli:2.9.6
  • yarn:1.10.1

対応方法

  • 開発サーバのポートを変更
  • package.jsonのscriptsを変更
"scripts": {
  "dev": "PORT=3333 nuxt",
},
  • 特に3000番ポートは使用していなかったので、なぜこれでうまくいくようになったかは不明。。。

参考サイト(開発サーバのポート変更)

ja.nuxtjs.org

Nuxt.jsでPWAをGitHub Pagesにコマンドでデプロイする流れ

概要

  • Nuxt.jsのテンプレートとPWAモジュールを使用して作成したアプリをGitHub Pagesにコマンドラインでデプロイする方法のまとめ

バージョン

  • node:10.12.0
  • nuxt.js:2.0.0
  • nuxtjs/pwa:2.6.0
  • push-dir:0.4.1
  • vue-cli:2.9.6
  • yarn:1.10.1

参考記事

https://pwa.nuxtjs.org/setup

ja.nuxtjs.org

方法

Nuxt.jsでプロジェクト生成

vue init nuxt-community/starter-template nuxt-pwa

PWAモジュールを追加

  • プロジェクトのルートディレクトリでpwaモジュールを追加
yarn add @nuxtjs/pwa
  • nuxt.config.jsに「modules」と「manifest」を追記
modules: [
  '@nuxtjs/pwa',
],
manifest: {
  name: 'yasutomog nuxt pwa',
  lang: 'ja'
},
  • .gitignoreに追記
sw.*

GitHubの準備

  • 本プロジェクト用のリポジトリ作成
  • masterブランチに生成したファイルとディレクトリをaddしてPush
  • gh-pagesブランチを作成

push-dirを追加

yarn add push-dir
  • nuxt.config.jsに「routerBase」を追記
const routerBase = process.env.DEPLOY_ENV === 'GH_PAGES' ? {
  router: {
    base: '/nuxt-pwa/'
  }
} : {}

module.exports = {
  ...routerBase,
  • package.jsonのscriptsにGitHub Pages用のdeployとgenerateとbuildコマンドを追記
"scripts": {
  "deploy": "push-dir --dir=dist --branch=gh-pages --cleanup",
  "build:gh-pages": "DEPLOY_ENV=GH_PAGES nuxt build",
  "generate:gh-pages": "DEPLOY_ENV=GH_PAGES nuxt generate"
},

PWAの生成とデプロイ

yarn run generate:gh-pages
yarn run deploy

GitHub Pagesを確認

GitHub Pagesでアンダーバーのディレクトリ対応

概要

  • Nuxt.jsで構築したサイト(yarn run generateで生成したdist直下)をGitHub Pagesに上げると、_nuxtディレクトリ内のjsファイルなどが404になる
  • GitHub PagesではJekyllで処理されるため、アンダーバー付きディレクトリなどが正常に読み込まれない

対応方法

  • .nojekyllファイルをルートディレクトリに作成
    • touch .nojekyll

AjaxのCSVダウンロード(Excelでの文字化け対応)

概要

  • 既に色々なところであがっているトピックなので今更だが、Excelの特定バージョンでのみ文字化けするということがあったので、対策をメモ
  • 一般的にCSVExcelで開く時の文字化け対策としては以下2つ。
  • 今回はUTF-8(BOM付き)で対応していたところ、以下のExcelバージョンでのみ日本語が文字化け(同じ2007でも違うバージョンだと問題なく表示できていた、、)
    • Excel 2007(12.0.4518.1014)
  • ダウンロード処理のざっくりとした流れは以下。
    • ajaxでPOST通信
    • サーバ側からファイル内容を返す
    • JavaScriptでBlobを使いファイルとそのリンクを作成
    • aタグを作ってリンクを紐づけ、clickイベント実行

対応

  • JavaScriptShift_JISに変換してファイルを作成するように変更することで、文字化けしていたExcelでも正しく表示できました。
  • ファイル内容の文字コードを変更するために、encoding.js(1.0.29)を追加しました。

    github.com

// ダウンロード用Ajax通信のコールバック(サンプルコード)

let rt = response.responseText,
  strRt = Encoding.stringToCode(rt),
  arrRt = Encoding.convert(strRt, "SJIS", "UNICODE"),
  u8a = new Uint8Array(arrRt),
  blob = new Blob([u8a], { 'type' : 'text/csv;charset=sjis;' }),
  blobUrl = window.URL.createObjectURL(blob),
  a = document.createElement('a');

a.href = blobUrl;
a.download = 'hoge.csv';
a.click();

Lumen(Laravel)のQueue管理クラス(DB)の拡張方法

概要

  • Lumenを使用したWebアプリを運用している中で、Queue(DB)を使用
  • DBは、Azure SQL Databaseを使用
  • Azure SQL Databaseでは一時的に負荷が上がると、強制的に接続を切断しDB側の再構成が走る
    DB接続をキャッシュしたり、コネクションプーリングなどを使用している場合、古いDB接続が利用されるとDB接続エラーが発生する docs.microsoft.com
  • LumenのQueue(DB)では、DB接続をキャッシュして使用しているため、デフォルトのまま使用しているとDB負荷が上がったときにDB接続エラーが連続して発生するようになる yasutomo.hatenablog.com

前提

  • lumen:5.5.7
  • PHP:7.1
  • php artisan queue:work のコマンド実行時

詳細

Illuminate\Queue\DatabaseQueueを継承したクラスを作成

<?php

namespace App\Libs;

use Illuminate\Queue\DatabaseQueue;

class CustomDatabaseQueue extends DatabaseQueue
{

    // DB再接続時のwait秒
    const RETRY_WAIT_SEC = 10;

    // DB再接続用のSQL STATEコード
    const RETRY_SQL_STATE = ["08S01", "HY000"];

    /**
     * DBへの再接続処理を加えたoverride関数
     *
     * @see DatabaseQueue::pop()
     * @param  string  $queue
     * @return \Illuminate\Contracts\Queue\Job|null
     * @throws \Exception|\Throwable
     */
    public function pop($queue = null)
    {
        $queue = $this->getQueue($queue);

        try {

            try {

                // DB負荷が高くなるとAzure SQL Databaseは、強制的に切断しDB再構築する
                // Lumenの標準実装では、DB接続インスタンスを保持して使いまわしているため
                // DB再構築後も、切断されたDB接続インスタンスを使用しようとするため
                // キュー管理テーブルを参照するタイミングでDB接続エラーが発生し続ける
                $this->database->beginTransaction();

            } catch (\Throwable $e) {

                \Log::info($e);

                if (property_exists($e, 'errorInfo')) {

                    $sqlstate = $e->errorInfo[0];
                    $sqlerrcd = $e->errorInfo[1];

                    \Log::info("SQLSTATE::" . $sqlstate);
                    \Log::info("SQLERRCD::" . $sqlerrcd);

                    // DBのコネクションプーリングを使用していない場合でも
                    // 連続して接続エラーが発生するケースがある(Azureサポート)
                    // MSサイトから再接続までには最低でも5秒は間隔を空けることを推奨しているので
                    // 余裕をもって10秒後に再接続する処理
                    // https://docs.microsoft.com/ja-jp/azure/sql-database/sql-database-develop-error-messages

                    if (in_array($sqlstate, self::RETRY_SQL_STATE)) {

                        \Log::info('キュー管理テーブル参照時にDB接続エラーが発生したので、10秒後にDB再接続します。');
                        sleep(10);
                        $this->database->reconnect();
                        $this->database->beginTransaction();
                        \Log::info('キュー管理テーブルの再接続に成功しました。');

                    } else {

                        throw $e;

                    }

                } else {

                    throw $e;

                }

            }

            if ($job = $this->getNextAvailableJob($queue)) {
                return $this->marshalJob($queue, $job);
            }

            $this->database->commit();
        } catch (\Throwable $e) {
            $this->database->rollBack();

            throw $e;
        }
    }
}

Illuminate\Queue\Connectors\DatabaseConnectorを継承したクラスを作成

<?php

namespace App\Libs;

use Illuminate\Queue\Connectors\DatabaseConnector;

/**
 * キュー管理用のDBコネクタクラス
 *
 * Class CustomDatabaseConnector
 * @package App\Libs
 */
class CustomDatabaseConnector extends DatabaseConnector
{
    /**
     * returnするクラスを変更したoverride関数
     *
     * @see DatabaseConnector::connect()
     * @param  array  $config
     * @return \Illuminate\Contracts\Queue\Queue
     */
    public function connect(array $config)
    {
        return new CustomDatabaseQueue(
            $this->connections->connection($config['connection'] ?? null),
            $config['table'],
            $config['queue'],
            $config['retry_after'] ?? 60
        );
    }
}

App\Providers\AppServiceProviderの改修

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Libs\CustomDatabaseConnector;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // キュー管理のDBコネクタ登録
        $queueManager = $this->app['queue'];
        $queueManager->addConnector('customdatabase', function () {
            return new CustomDatabaseConnector($this->app['db']);
        });
    }
}

api/config/queue.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default Queue Driver
    |--------------------------------------------------------------------------
    |
    | The Laravel queue API supports a variety of back-ends via an unified
    | API, giving you convenient access to each back-end using the same
    | syntax for each one. Here you may set the default queue driver.
    |
    | Supported: "null", "sync", "database", "beanstalkd", "sqs", "redis"
    |
    */

    'default' => env('QUEUE_DRIVER', 'customdatabase'),

    /*
    |--------------------------------------------------------------------------
    | Queue Connections
    |--------------------------------------------------------------------------
    |
    | Here you may configure the connection information for each server that
    | is used by your application. A default configuration has been added
    | for each back-end shipped with Laravel. You are free to add more.
    |
    */

    'connections' => [

        'customdatabase' => [
            'connection' => 'hoge',
            'driver' => 'customdatabase',
            'table' => 'QueueJobs',
            'queue' => 'default',
            'retry_after' => 60,
        ]

    ],

];

artisanコマンドを変更

  • php artisan queue:work customdatabase