DIコンテナ使ってみて思った雑文

アプリケーションの流れとして、

  1. コンテナを構築
  2. アプリを実行するなにか(パイプラインとかディスパッチャとか)をコンテナから取り出す
  3. リクエストオブジェクトを作成してアプリを実行

みたいな流れかなと思う。順番的にDIコンテナが先に作られるのでリクエストオブジェクトやリクエストに含まれる情報はDIコンテナには入らない。そもそもDIコンテナはグローバルなコンテキストのものなので、リクエストのコンテキストに属するオブジェクトを入れるべきではない。

(PHPでは出来ないけれども)もし可能なのならDIコンテナはリクエストをまたがって共有されても良い。なのでコンテナに入るオブジェクトはステートレスでなければならないし、再入可能でスレッドセーフであるべき(PHPでは関係ないけれども)。

・・・みたいなイメージ。


例えば Logger にリクエストに基づく情報が自動的に付加されるようにしてみる(IPアドレスとか)。

$logger = $logger->withContext(['ipaddr' => $ipaddr]);

// $context に 'ipaddr' が追加される
$logger->info('ほげほげ');

こうして作られた Logger はリクエストのコンテキストに属するので、グローバルなコンテキストであるところのコンテナには入らない。

サービスクラスがコンテナに入っているのだとすると、サービスクラスのコンストラクタにインジェクションできるものはコンテナに入っているものだけなので、↑のよう部分的に適用された Logger をインジェクションすることはできない。

大本の Logger がインジェクションされて、そしてサービスのメソッドにリクエストオブジェクトが渡されるようにする?(コードはイメージです)

class HogeService
{
    public function __construct($logger)
    {
        $this->logger = $logger
    }

    public function func($request)
    {
        $logger = $this->logger->withContext(['ipaddr' => $request->getRemoteAddr()]);

        // $logger を使ってログを出す
    }
}

ちがう、サービスがリクエストに依存しているわけではないので ipaddr が適用された Logger を渡すべき。

class HogeService
{
    public function func($logger)
    {
        // $logger には 'ipaddr' が適用されているけどサービスはそんなこと知らねえ
    }
}

ただ $logger をずっと引数として持ち回さなければならない。引数が Logger しかないなら良いけれども、他にもリクエストのコンテキストに属するオブジェクトをたくさん持ち回したくなったときにツラミが増してくる(Logger 以外なにがあるかぱっと思いつかないけど)。

ので、それを見越して、リクエストのコンテキスト、というオブジェクトが渡されるようにしてみる。

class HogeService
{
    public function func($requestContext)
    {
        $logger = $requestContext->getLogger();
    }
}

うーん悪手? サービスが RequestContext というよくわからないものに依存している。


Logger がコンストラクタからシュッと入ってきてそれをそのまま使えられれば良いのだけど、コンテナにリクエストに基づくモノが入っていないとすると引数で持ち回すしか無い?

あるいはコンテナにリクエストオブジェクトをぶっこんでも良いことにする?

// bootstrap.php っぽいなにか
$builder = new ContainerBuilder();
$builder->addDefinitions($definitions + [
    ServerRequestInterface::class => ServerRequestFactory::fromGlobals(),
]);

$container = $builder->build();

// コンテナにリクエストオブジェクトが入っているので logger に ipaddr が適用されている
$logger = $container->get(Logger::class);

でも PSR-7 だとミドルウェアパイプラインの中でリクエストオブジェクトがクローンされて次々に生成されるわけで、↑だとコンテナに入ったリクエストオブジェクトとコントローラーのアクションに渡ってくるリクエストオブジェクトが異なるものになってしまって違和感。

例えば X-Forwarded-For を見てリクエストが持つ RemoteAddr を本来のものに書き換えるようなミドルウェアを考えると問題がわかりやすい。

それか Logger をミュータブルにしてそういうミドルウェアが Logger に IP アドレスをセットする?

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $this->logger->addContext(['ipaddr' => $request->getRemoteAddr()]);
        return $delegate->process($request);
    }

うーん、どんどんゆるふわになっていく・・


コントローラーをディスパッチするときに元のコンテナを拡張した新たなコンテナを作成して、コントローラーの依存はその新たなコンテナによって解決する?

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $container = $this->extend($this->container, [
            LoggerInterface::class => function () use ($request) {
                return (new Logger())->withContext([
                    'ipaddr' => $request->getRemoteAddr(),
                ]);
            }
        ]);

        // コントローラーとコントローラーが依存するオブジェクトは新たなコンテナで依存解決される
        $result = $request->getAttribute(RouteResult::class);
        $instance = $container->get($result->getController());
    }

リクエストのアトリビュートを全部ぶっこむとかでも。

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $request = $request->withAttribute(
            LoggerInterface::class, (new Logger())->withContext([
                'ipaddr' => $request->getRemoteAddr(),
            ])
        );

        $container = $this->extend($this->container, $request->getAttributes());

        // コントローラーとコントローラーが依存するオブジェクトは新たなコンテナで依存解決される
        $result = $request->getAttribute(RouteResult::class);
        $instance = $container->get($result->getController());
    }

とくに結論はない。

PHPUnit の Functions.php を自動でロードするやつ

下記↓の記事のコメントの通り、

PHPUnit には Functions.php というファイルが含まれていて、これを require するとアサーションなどがグローバル関数に登録されるので、次のようにテストを書くことができます。

<?php
// 普通こう書くけど、
$this->assertThat($actual, $this->equalTo($expect));

// Functions.php を読んどけばこうも書ける。
assertThat($actual, equalTo($expect));

とても便利なんですけど Functions.php は明示的に読む必要があります。

<?php
require_once __DIR__ . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';

しかもこれ、PHPUnit のバージョンによって違う位置にあります。

さらに、最近なら PHPUnit を Phar で実行することもあると思います。その場合は Phar ファイル名を元に読む必要があります。

<?php
require_once 'phar://phpunit-6.2.phar/phpunit/Framework/Assert/Functions.php';

こういうのが辛かったので、次のようにリフレクションでファイル名を特定するようにしていたのですが・・・PHPUnit 6 でクラス名が PEAR から PSR-4 のスタイルに変わったので、これも動かなくなりました。

<?php
require __DIR__ . '/../vendor/autoload.php';
$reflection = new ReflectionClass('PHPUnit_Framework_Assert');
require_once dirname($reflection->getFileName()) . '/Assert/Functions.php';

そもそも↑のようなスニペットをいちいちコピペするのも面倒臭くなってきたので Functions.php を自動的に読み込むやつを作りました。

次のように composer で追加しておくと、テストの実行時に自動的に Functions.php が読まれます。

composer require --dev ngyuki/phpunit-functions

これは便利。


MySQL で date_add/sub で年が 0000 になると 0000-00-00 が返る

MySQL の sql_modeNO_ZERO_IN_DATE,NO_ZERO_DATE とかを指定すると 0000-00-00 や、月や日が 00 などの日時の挿入を禁止できます。

がしかし、年が 0000 の場合の動きが謎いです。

MySQL のバージョンは次の通り。

select version();
/*-----------+
| version()  |
+------------+
| 5.7.19-log |
+-----------*/

SQL モードを指定します。

set session sql_mode = 'STRICT_ALL_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO';
select @@sql_mode;
/*--------------------------------------------------------------------------+
| @@sql_mode                                                                |
+---------------------------------------------------------------------------+
| STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO |
+--------------------------------------------------------------------------*/

データを入れます。想定通りの動きだと思います。月や日の 00 は無効ですが年の 0000 は有効です。

create table t (d date);
insert into t values ('0000-00-00'); /* ERROR 1292 (22007): Incorrect date value */
insert into t values ('0001-01-00'); /* ERROR 1292 (22007): Incorrect date value */
insert into t values ('0001-00-01'); /* ERROR 1292 (22007): Incorrect date value: */
insert into t values ('0000-01-01'); /* Query OK */
insert into t values ('0000-01-02'); /* Query OK */
insert into t values ('9999-12-30'); /* Query OK */
insert into t values ('9999-12-31'); /* Query OK */

なんということでしょう。

select d, date_add(d, interval 1 day) as `d+1` from t;
/*-----------+------------+
| d          | d+1        |
+------------+------------+
| 0000-01-01 | 0000-00-00 |
| 0000-01-02 | 0000-00-00 |
| 9999-12-30 | 9999-12-31 |
| 9999-12-31 | NULL       |
+------------+-----------*/

select d, date_sub(d, interval 1 day) as `d-1` from t;
/*-----------+------------+
| d          | d-1        |
+------------+------------+
| 0000-01-01 | 0000-00-00 |
| 0000-01-02 | 0000-00-00 |
| 9999-12-30 | 9999-12-29 |
| 9999-12-31 | 9999-12-30 |
+------------+-----------*/

9999-12-31 + 1 day が NULL なのは良いとして 0000-01-010000-01-02date_adddate_sub すると 0000-00-00 なりました。

下記の結果を見るに、比較はうまく動いているっぽい。

select d, d < '0000-01-02', d > '0000-01-01' from t;
/*-----------+------------------+------------------+
| d          | d < '0000-01-02' | d > '0000-01-01' |
+------------+------------------+------------------+
| 0000-01-01 |                1 |                0 |
| 0000-01-02 |                0 |                1 |
| 9999-12-30 |                0 |                1 |
| 9999-12-31 |                0 |                1 |
+------------+------------------+-----------------*/

どうやら date_adddate_sub による演算の結果、年が 0000 になると 0000-00-00 になってしまうっぽい。

select date_add('0000-12-30', interval 1 day) as `0000-12-30 + 1`,
       date_add('0000-12-31', interval 1 day) as `0000-12-31 + 1`,
       date_sub('0001-01-01', interval 1 day) as `0001-01-01 - 1`,
       date_sub('0001-01-02', interval 1 day) as `0001-01-02 - 1`;
/*---------------+----------------+----------------+----------------+
| 0000-12-30 + 1 | 0000-12-31 + 1 | 0001-01-01 - 1 | 0001-01-02 - 1 |
+----------------+----------------+----------------+----------------+
| 0000-00-00     | 0001-01-01     | 0000-00-00     | 0001-01-01     |
+----------------+----------------+----------------+---------------*/

西暦1年の前年は0年ではなく紀元前1年?なのなら 0000 という年は無効な日付になるので結果が 0000-00-00 とかになるのも判らなくもないけど・・それなら date_adddate_sub の入力に与えられたときも結果は 0000-00-00 になるべきな気がするし NO_ZERO_IN_DATE で弾かれてほしい気もする。

あとこれ、テーブルへの格納ではなくて date_adddate_sub による演算に対して発生しているので sql_mode は関係なく発生するようです。

KeepAlive On な Apache+mod_php で HTTP/1.0 クライアントに HTTP/1.1 を返すとタイムアウトを待ってしまう

  • PHP 7.1.7
  • Apache 2.4.10
  • ApacheBench 2.3
  • zend-expressive 2.0.3
  • zend-expressive-skeleton 2.0.3
  • zend-diactoros 1.4.0
  • zend-stratigility 2.0.1

Docker で Apache+mod_php を実行して、

docker run --rm -p 8888:80 -v "$PWD:/var/www/html" php:apache

header 関数で HTTP/1.1 を指定するコードを配置して、

<?php
header('HTTP/1.1 200 OK');

次のように ab します。

ab -c 1 -n 1 http://localhost:8888/

すると、なんか異様なスコアになります(1回のリクエストに5秒もかかってる)。

Requests per second:    0.20 [#/sec] (mean)
Time per request:       5011.751 [ms] (mean)
Time per request:       5011.751 [ms] (mean, across all concurrent requests)

header 関数をコメントアウトしたり、

<?php
//header('HTTP/1.1 200 OK');

HTTP/1.0 を指定したり、

<?php
header('HTTP/1.0 200 OK');

Connection: close を指定したり、

<?php
header('HTTP/1.1 200 OK');
header('Connection: close');

Aapche Bench で KeepAlive を有効にすれば、

ab -k -c 1 -n 1 http://localhost:8888/

それっぽい結果になります。

Requests per second:    560.85 [#/sec] (mean)
Time per request:       1.783 [ms] (mean)
Time per request:       1.783 [ms] (mean, across all concurrent requests)

原因

Apache Bench はデフォで HTTP/1.0 なリクエストを送ります。なのでレスポンスの最後はサーバの方からクローズされることを期待します。

一方、php が header 関数で HTTP/1.1 を指定すると、Apache は元のリクエストが 1.0 だったとしても 1.1 を応答し、しかも KeepAlive On なら KeepAlive も有効になります。そのため KeepAliveTimeout で指定された秒数まで接続しっぱなしになります。

Apache Bench はサーバからのアクティブクローズを待つので KeepAliveTimeout の秒数が経過するまでリクエストが終わらなくなります。

Docker Hub の php:apache のイメージは KeepAlive ONKeepAliveTimeout 5 です。

curl だと発生しない

curl で HTTP/1.0 を指定してもこの現象は発生しません。

time curl -i -0 http://localhost:8888/
HTTP/1.1 200 OK
Date: Sun, 30 Jul 2017 08:54:12 GMT
Server: Apache/2.4.10 (Debian)
X-Powered-By: PHP/7.1.7
Content-Length: 0
Content-Type: text/html; charset=UTF-8


real    0m0.012s
user    0m0.003s
sys     0m0.004s

curl はリクエストを 1.0 で送ってもレスポンスが 1.1 なら 1.1 のクライアントとして振る舞うからだと思います。ので、Content-Length を元にレスポンスの終わりを判断できます。

なお、php のコードを次のようにすると、

<?php
header('HTTP/1.1 200 OK');
flush();

curl の応答は次のようになります。

time curl -i -0 http://localhost:8888/
HTTP/1.1 200 OK
Date: Sun, 30 Jul 2017 08:57:05 GMT
Server: Apache/2.4.10 (Debian)
X-Powered-By: PHP/7.1.7
Vary: Accept-Encoding
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8

real    0m0.014s
user    0m0.002s
sys     0m0.005s

この場合は Transfer-Encoding: chunked なので、チャンクサイズ 0 でレスポンスの終端が判断できます。

zend-expressive

この現象、zend-expressive を Docker Hub の php:apache で動かして ab したときのスコアがおかしなことになって気づきました。

zend-expressive で使われている zend-diactoros のレスポンスオブジェクトは普通に作るとリクエストの内容に依らず 1.1 で固定のためです。

パイプラインの最初のほうでリクエストのバージョンを元にレスポンスのバージョンを変更するミドルウェアを登録しておけば解決します。

// pipeline.php

use Psr\Http\Message\ServerRequestInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;

$app->pipe(function (ServerRequestInterface $request, DelegateInterface $delegate) {
    return $delegate->process($request)->withProtocolVersion($request->getProtocolVersion());
});

nginx+php-fpm では発生しない

ちなみに nginx+php-fpm の構成では発生しませんでした。

nginx の場合は php-fpm が 1.1 を返してきたとしても、リクエストのバージョンが 1.0 なら Connection: close を付与したうえでサーバからアクティブクローズするようです。

結局どこが悪いの?

現代の PHP は http_response_code があるのでレスポンスコードを指定するためだけにプロトコルバージョンまで付けて header 関数を使うべきでは無いと思いますけれども、zend-diactoros は header 関数でステータスコードを指定しています。ただ、PSR-7 の ResponseInterfacegetProtocolVersion/withProtocolVersion を持っているので、レスポンスの送信時にプロトコルバージョンも指定するのは至って自然な気がします。

なので、zend-expressive でレスポンスのプロトコルバージョンがリクエストのプロトコルバージョンに基いていないのがダメなのだろうか・・・?

でも、HTTP/1.0 のリクエストに対して HTTP/1.1 のレスポンスを返すこと自体は合法なようです。

ただ、サーバが「おれ HTTP/1.1 でも大丈夫だよ」とクライアントに伝えるためであって、KeepAlive などの HTTP/1.1 の機能は有効にするべきではない、気がする、ので、Apache が悪いような気もする。

あるいは zend-expressive が(あるいは mod_php が) HTTP/1.0 なリクエストに対しては Connection :close でレスポンスを返すべき?

SNMP v3 の engineID とかのメモ

man snmpcmd とか man snmpd.conf とか man snmptrapd.conf とか tcpdump とかで試行錯誤で推測した内容からのメモ。全然見当違いの可能性もある。


SNMP v3 のエージェントはエージェントを一意に識別するための engineID を設定する必要がある。engineID は不変で別のエージェントの engineID と競合してはならない。

Net-SNMP では engineID は未設定だとマシンのホスト名に対して見つかる最初の IP アドレスが使用される、と日本語の man にはるけど、英語の man では最初の起動時にシステム時刻と乱数に基づいて生成されるとなっている。

生成済の engineID を削除して再生成を何度か試したところ毎回異なる値になったので、英語の man のが正しいと思われる。engineIDType の設定で日本語の man と同じように生成することもできると思う。

生成された値は /var/lib/net-snmp/snmpd.conf に記録される。

SNMPv3 で snmpwalk すると、最初はユーザーや engineID は空でリクエストが送信され、エージェントからは .1.3.6.1.6.3.15.1.1.4.0=225 のような値が応答される。この OID は SNMP-USER-BASED-SM-MIB::usmStatsUnknownEngineIDs.0 という名前で、225 という値は engineID が不明なためにドロップされたパケットの数、らしい、MIB ファイルの記述によると。確かに実行するたびに値がインクリメントされている。

この応答が返ってくるとき、一緒に engineID も付いて返ってくる。snmpwalk は(つまりマネージャーは)この engineID を使って目的の OID の値を取得するためにもう一度リクエストする。

つまり、SNMPv3 を使う場合は engineID を取得するために1往復分余分にパケットがやり取りされている。もちろん engineID は不変という前提なので、一度取得した engineID をキャッシュしておけば必要ない、ただし snmpwalk はそんなのキャッシュしてないので毎回1往復余分にパケットをやり取りする。

一方、snmptrap コマンドでトラップを送る場合、snmpd の初回起動時に生成された engineID のとは別の engineID が生成されて、その engineID を付けてトラップが送信される。

ここで生成された値は /var/lib/net-snmp/snmpapp.conf に記録される。

トラップの場合、送信元がエージェントで送信先がマネージャーなので、エージェントはマネージャーが自分の engineID を知っているという前提でトラップを投げる。つまり、snmpwalk(で送られる GetRequest)のときのように1往復余分にやりとりしたりせず engineID が違ってればパケットが無視されておしまい。

snmptrapd で v3 のトラップを受けるためには、送信元の snmptrapengineID を調べて、その値を元に送信先の snmptrapd でユーザーを作成する必要がある。

たぶん、ネットワーク機器とかの組み込みの SNMP なら GetRequest に応答する engineID とトラップの engineID は同じ?なのかな??なのでマネージャーは GetRequest したときの engineID を記憶しておいてトラップを受信したときに照合することができる?のだろうか??マネージャーが Net-SNMP だと無理そう?だけど??

Net-SNMP の snmpdsnmptrap だと別々の engineID が生成されることがあるのでそういうのはできない。

v3 snmptrap とかでググると、適当な engineID をベタ書きで snmptrap コマンドの引数にしている例が多いのだけど、本当ならエージェントの /var/lib/net-snmp/snmpapp.conf に記録された engineID を使ってマネージャーの snmptrapd を設定するのが正しい?ような気がする??


2ヶ月ぐらい前に社内に書いてたメモからのコピペ。

うーん、結局のところよく判っていない。いろいろあって実案件でこの知識使うこともなくなったので、詳しくは調べていない。

というかなんでこんなニッチなことを調べる必要に迫られたのか・・なにエンジニアなのかわからなくなるわ。

Windows の SNMP サービスから取れる情報

Windows 10 で SNMP サービスを有効にして取ってみました。概ね下記のような情報が取れます。

  • SNMPv2-MIB::system
    • sysDescr とか sysUpTime とか
  • IF-MIB::interfaces
    • ネットワークインタフェース
  • RFC1213-MIB::ip
  • RFC1213-MIB::icmp
  • TCP-MIB::tcp
  • UDP-MIB::udp
    • ip とか icmp とか tcp とか udp とか
  • SNMPv2-MIB::snmp
    • SNMP パケット関係のメトリクスとか
  • HOST-RESOURCES-MIB::host
    • ホストのいろいろな情報(代表的なものを抜粋)
    • hrSystemUptime
    • hrSystemProcesses
    • hrStorageTable
      • hrStorageType
      • hrStorageDescr
      • hrStorageSize
      • hrStorageUsed
    • hrProcessorTable
      • hrProcessorLoad
  • IF-MIB::ifMIB
    • interfaces と似たような情報?
  • IPV6-MIB::ipv6MIB
    • ipv6 とか
  • SNMPv2-SMI::enterprises.77.1

トラフィックは IF-MIB::interfaces から普通に取れます。ただ、コントロールパネルのアダプタの設定には表示されないものがたくさんあがってきてるので、目的のインタフェースを特定するのが難しそう。

メモリ使用量とディスク使用量は HOST-RESOURCES-MIB::hrStorageTable から取れます。メモリもディスクも混在しているので hrStorageType を見て区別する必要があります。

CPU使用率は HOST-RESOURCES-MIB::hrProcessorLoad で CPU ごとに取れます。Load とあるけど MIB を読むと下記の通りだったので、ロードアベレージではなく CPU 使用率のはずです。ググるとロードアベレージだと思っている人が多そうでした。

The average, over the last minute, of the percentage of time that this processor was not idle. Implementations may approximate this one minute smoothing period if necessary.

このプロセッサがアイドル状態でなかった時間のパーセンテージの平均値。 実装は、必要であれば、この1分間のスムージング期間に近似することがあります。 Power by Google Translate

ロードアベレージは素ではとれなさそう・・ググると snmp-informant をインストールして取れるようにする例を見かけます。


2ヶ月ぐらい前に社内に書いてたメモからのコピペ。

いろいろなフレームワークのリクエスト/レスポンスクラスの実装

今日日の PHP のフレームワークでリクエスト/レスポンスクラスがどのように実装されているか調べたメモ。

PSR-7 の実装は zend-diactoros がほぼデファクトかと思ってたけど独自に実装されていることもあった(slim とか cake とか)。


社内に書いてたメモからのコピペ。PSR-7/PSR-15 で開発するときに PSR-7 の実装をどうしようかと思って調べたもの。

わりとメジャーなフレームワークで抜けてるものあるけど(Fuelとか)どういう基準で選定したかは・・・忘れた。