PHP の Guzzle で直列 vs 並列

前やった node.js の Promise で直列 vs 並列を php でやってみた。 素の php だと非同期処理はめんどくさいので 今回は Guzzle を使ってみた。

Guzzle は非同期もできる http クライアントだけど、 Guzzle Promises を使えば 一般的な処理も非同期で書けるみたい。

<?php
/*
 * PHP の Guzzle で直列 vs 並列
 */

require './vendor/autoload.php';

function createUrl(int $sec) {
    return 'http://httpbin.org/delay/' . $sec;
}

function serial(array $urls) {
    $client = new \GuzzleHttp\Client();

    $bodies = [];
    foreach ($urls as $url) {
        $response = $client->request('GET', $url);
        $bodies[] = $response->getBody()->getContents();
    }
    return $bodies;
}

function parallel(array $urls) {
    $client = new \GuzzleHttp\Client();

    $promises = [];
    foreach ($urls as $url) {
        $promises[] = $client->requestAsync('GET', $url);
    }
    $responses = \GuzzleHttp\Promise\all($promises)->wait();

    $bodies = [];
    foreach ($responses as $response) {
        $bodies[] = $response->getBody()->getContents();
    }
    return $bodies;
}

function main() {
    $urls = [];
    foreach (range(1, 4) as $sec) {
        $urls[] = createUrl($sec);
    }

    $startTime = new \DateTime();
    serial($urls);
    $diff = $startTime->diff(new \DateTime());
    var_dump('serial: '. $diff->format("%s sec"));

    $startTime = new \DateTime();
    parallel($urls);
    $diff = $startTime->diff(new \DateTime());
    var_dump('parallel: '. $diff->format("%s sec"));
}
main();

動かしてみる。

$ composer require guzzlehttp/guzzle
$ php main.php
/path/to/main.php:50:
string(14) "serial: 11 sec"
/path/to/main.php:55:
string(15) "parallel: 4 sec"

ちゃんと並列で動いてるみたい。

php 7.2.4 で確認

AWS の ECS & Fargate でゆるいバッチ環境を作る

仕事で以下のようなバッチを作る事になったのでその記録をメモしとく。

  • 月 1 回実行
  • 内容は手動でやってた作業を自動でやって結果をメールするだけ
  • 実行には 30 分ほどかかる
  • 全然クリティカルじゃない
    • エラーが発生しても連絡があってから再実行すればよい
    • 再実行もすぐでなくてもよい

で、考えた事がこちら

  • 運用工数かけたくない
    • cron 等で定期実行
    • サーバレス
  • そんなに変更ないだろうからデプロイは自動でなくてよい
  • なるべく安くすませたい

AWS でいくつかやり方があるけど、どれがいいか検討してみる。

  • Lambda + ClowdWatch Events
    • サーバレス
    • デプロイは zip 上げるだけ
    • cron 風の記述で定期実行可能
    • 実行時間の上限が 5 分なので今回は使えず
  • Elastic Beanstalk の worker 環境
    • デプロイ、切り戻しが楽なのはよい
    • docker も使える
    • cron 風定期実行可
    • ただし、安くもないし、サーバレスでもない
      • t2.micro でも一ヶ月で 1000 円強かかる
    • 一旦保留、他の方法でダメだった時はこれで
  • AWS Batch
    • サーバレス
    • 裏は docker なので使える技術はわりと自由
    • 定期実行はあまり得意ではないみたい
  • ECS + EC2
    • docker
    • ScheduleTask でやれば cron 記法で定期実行できる
    • サーバレスでないし安くもない
  • ECS + Fargate
    • docker
    • Fargate なので使った分だけ課金
    • 東京リージョンにない
      • 注: これやった当時はまだでした
    • Fargate はまだ ScheduleTask で使えないみたい

で、安さと運用を考えて ECS + Fargate で行くことにした。

SES の設定

今回は送信制限を解除してない SES を使ったので、 予め送信先のメールアドレスを登録しておいた。

プログラム作成 & docker 化

バッチとして実行するプログラムを作る。 docker なので言語は自由に選べばよいが、今回は node.js で。 メールは aws-sdk 経由で SES で送る。

プログラムができたら Docker 化する。 Fargate は pull が遅いらしいんで、軽量化のため ベースイメージは node の alpine にした。

ECS の設定

Fargate & SES が使える us-east-1 に構築する。

  • ECR にバッチのリポジトリを作って Docker イメージを push する。
  • クラスタの作成
    • クラスタとは ECS の実行環境みたいなもんらしい。
    • ECS のコンソールから
    • クラスターの作成」を選んで
    • 「ネットワーキングのみAWS Fargate を使用)」の
    • クラスタテンプレートを選んで
    • 後はよしなに作る。
  • タスク定義
    • 実行するイメージとかロール、メモリ、CPU を設定する
    • ECS のコンソールから
    • 「新しいタスク定義の作成」を選んで後はよしなに
    • コンテナは先程追加したやつを選択。
  • スケジュール設定

まとめ

という事で、ECS & Fargate を使って いい感じのバッチ環境を作ることができた。 cron もサーバレスにしたければ Lambda & ClowdWatch Events で いけるかもだけど、力尽きたので今回はここまで。

補足

今回はバッチ 1 つだけだったんで Fargate を選択したけど、 バッチサーバ立てるくらいの規模なら クラスタを EC2 で組んで ScheduleTask にした方がよいかも。

ただし、pull してから実行までに少しタイムラグがあるので、 起動時刻に厳密なケースでは他の方法の方がよいと思われる。

追記

現在は

  • 東京リージョン
  • 時間指定実行

に対応したるので、さらに使いやすくなった。

typescript の async/await で直列 vs 並列

typescript でちょっとやってみました。

http://httpbin.org/delay/{delay} というのは delay 秒待ってからレスポンス返してくれる URL です。

/*
 * serial vs parallel
 */
import request from 'request-promise-native';

function createUrl(sec: number): string {
    const secStr: string = String(sec);
    return `http://httpbin.org/delay/${secStr}`;
}

async function serial(urls: string[]): Promise<string[]> {
    let results = [];
    for (const url of urls) {
        const ret = await request(url);
        results.push(ret);
    }
    return results;
}

async function parallel(urls: string[]): Promise<string[]> {
    const promises = [];
    for (const url of urls) {
        const promise = request(url);
        promises.push(promise);
    }
    const results = await Promise.all(promises);
    return results;
}

async function main() {
    const range = [1, 2, 3, 4];
    const urls = range.map(createUrl);

    let startTime = Date.now();
    let results = await serial(urls);
    console.log('serial');
    // 1 + 2 + 3 + 4 = 10 秒かかる
    console.log((Date.now() - startTime) / 1000);

    startTime = Date.now();
    results = await parallel(urls);
    console.log('parallel');

    // max(1, 2, 3, 4) = 4 秒かかる
    console.log((Date.now() - startTime) / 1000);
}
main();

動かしてみる。

$ mkdir test; cd $_
$ pbpaste > index.ts
$ npm init -y
$ npm i -S typescript @types/node
$ npm i -S request @types/request
$ npm i -S request-promise-native @types/request-promise-native
$ npx tsc --init
$ vim .tsconfig # target を es2017 に
$ npx tsc
$ node -v
v8.4.0
$ node ./index.js
serial
11.58
parallel
4.373

ちゃんと並列で動いてますね。

コマンドラインで json を作る

json をパースするツールとして jq というのがありますが、 その反対、json を作るツールとして jo というのがあります。

使い方

引数でキーと値を指定する

$ jo key=value
{"key":"value"}

配列を作るときは -a オプションをつける

$ jo -a 1 2 3
[1,2,3]

ネストしたオブジェクトを作る場合は $() を使う

$ jo key=falue object=$(jo name=test value=1)
{"key":"falue","object":{"name":"test","value":1}}

-p オプションをつければ見やすくしてくれる

$ jo -p key=falue object=$(jo name=test value=1)
{
   "key": "falue",
   "object": {
      "name": "test",
      "value": 1
   }
}

標準入力からのデータも使える

$ seq 1 10 | jo -a
[1,2,3,4,5,6,7,8,9,10]

インストール

brew からでも入るみたいだけど、 ソースからコンパイルした。

git clone git://github.com/jpmens/jo.git
cd jo
autoreconf -i
./configure
make check
make install

fselect で SQL ライクにファイルを検索する

fselect という ツールを見つけて、面白そうだったんで触ってみた。

主な機能はこんな感じ

  • SQL like な検索
  • 正規表現も使える
  • 出力フォーマットの変更可能(json, csv...)
  • mp3 対応

早速インストールして fselect のリポジトリで試してみる。

$ fselect name, from ./ where name = '*.rs'
lexer.rs
main.rs
mode.rs
parser.rs
searcher.rs
util.rs
$ fselect name, from ./ where name = '*.rs' limit 2
lexer.rs
main.rs
$ fselect size, name from ./ where name = '*.rs' limit 2 into csv
8890,lexer.rs
1598,main.rs

結構複雑なクエリも使えるみたいだし、 毎回 find の使い方を検索するよりいいかもしれん。

惜しいのは order by が使えない事だけど issue には上がってるんで、そのうち実装されるんではないかと期待。

Elixir がよかった件

昨年は いま学ぶべき第二のプログラミング言語はコレだ! 未来のために挑戦したい9つの言語とその理由 の記事がきっかけで Rust, Haskell をやって非常によかったので この流れで残りの Elixir もやってみた。

テキストはもちろんこれ 最速で知る! ElixirプログラミングとErlang/OTPの始め方【第二言語としてのElixir】

で、やってみた結果、

  • ゆるい(非純粋)関数型
    • なので末尾最適化できる
    • 関数定義にパターンマッチ、ガードが使える
    • ゆるいので変なコト書いても怒られない
  • 動的型なので気楽に書ける
  • map のリテラルがある
  • return, セミコロン書かなくていい

な点が非常によかった。 さらに新しい言語っぽく

  • repl
  • パッケージマネージャ

がちゃんとあるんで、 普段使いの言語としていい感じに使えそう。

もちろん並列処理が得意というのもいいんだけど、 それなしでもよさげ。

なので、しばらくこれ使ってみる事にする。

色々な言語で偶数自乗和 その2

TL;DR

Haskell ならパイプライン演算子を自分で定義できるよ

java8

import java.util.stream.IntStream;

public class Main {
    public static Integer evenSquareSum(Integer n) {
        return IntStream.range(1, n)
            .filter(n1 -> n1 % 2 == 0) // variable n is already defined in method evenSquareSum(Integer) と出るので変数名を変更。引数と同じ変数名は使えないみたい
            .map(n1 -> n1 * n1)
            .reduce(0, (acc, n1) -> acc + n1)
            ;
    }

    public static void main(String[] args) throws Exception {
        Integer result = evenSquareSum(10);
        System.out.println(result);
    }
}

ちょっと見ない間にだいぶマシになった印象。 でも SIer とかだとバージョンアップできない所も多いんだろうな…

kotlin

fun main(args: Array<String>) {
    val result = evenSquareSum(10)
    println(result)
}

fun evenSquareSum(n: Int): Int {
    return (1..n)
        .filter{
            n -> n % 2 == 0
        }.map{
            n -> n * n
        }.fold(0) {
            acc, n -> acc + n
        }
}

よいです。

swift

func evenSquareSum(n: Int) -> Int {
    return (1...n)
        .filter { $0 % 2 == 0 }
        .map { $0 * $0 }
        .reduce(0) { $0 + $1 }
}

let result = evenSquareSum(n: 10)
print(result)

クロージャの省略記法いいね。

haskell

evenSquareSum n =
  sum . map (\n -> n * n) . filter even $ [1..n]

{- これでも OK
evenSquareSum n =
  foldl (+) 0 $ map (^2) $ filter even [1..n]
-}

main = do
    let result = evenSquareSum 10
    print result

やっぱり右からになるのね と思ったら、自分でパイプライン演算子が定義できるみたい via * HaskellでElixirのようなパイプラインで記述したいとき - Qiita * Hakellでパイプライン演算子

(|>) :: a -> (a -> b) -> b
(|>) a f = f a
-- a |> f = f a

evenSquareSum n =
  [1..n]
    |> filter even
    |> map (\n -> n * n)
    |> foldl (+) 0
main = do
    let result = evenSquareSum 10
    print result

すごい