Prometheus -> InfluxDB でダウンサンプリングしてみたメモ

半年くらい前に社内の勉強会っぽい何かで話すために書いていたけど結局やらずにお蔵入りしたメモ。

半年くらい前なのでわりと古いです


Prometheus は単体だとダウンサンプリングができないので長期のデータ保持には向いていません。デフォルトだと 15 日しか保持しないし、もっと長期に渡って保持するように設定すると凄まじい量のストレージを喰います。

他のリソース監視・可視化ツールであれば、5 分毎のデータを 1 日保持、30 分毎のデータを 1 週間保持、2 時間毎のデータを 1 ヶ月保持、1 日ごとのデータを 1 年間保持、のように段階的に粒度を荒くしたデータを長期に保持できるようになっていたりします。

Prometheus の場合、データの粒度と保持期間が異なる別の Prometheus を用意して Prometheus -> Prometheus とデータを流したり(Federation)、Prometheus から別の時系列データベースにデータを書き込んだりすることで(Remote write)、ダウンサンプリングっぽいことができます。

そこで、試しに社内のいくつかのサーバを監視している Prometheus のメトリクスを InfluxDB に流してダウンサンプリングしてみました。

remote_storage_adapter (remote_storage_bridge) のインストール

以前セットアップしたときは remote_storage_bridge というものが必要だったんですが、今は remote_storage_adapter という名前に変わってました。たぶん次のような感じでビルドできます(別にビルドに Docker を使う必要は無いですが)。

docker run --rm -v /usr/local/bin:/go/bin:rw golang \
  go get github.com/prometheus/prometheus/documentation/examples/remote_storage/remote_storage_adapter

remote_storage_bridge の起動やら Prometheus の設定とか InfluxDB のセットアップとかは別記事に簡単に書いています。

グラフ

1 時間毎のデータを 1 ヶ月保持したものです。

f:id:ngyuki:20171114204706p:plain

ダウンサンプリングの前後の比較です。上のグラフがダウンサンプリング前の 1 分毎のデータ、下のグラフがダウンサンプリング後の 1 時間毎のグラフです。ダウンサンプリングには平均値を使っています。メトリクスの種類によっては最大値とかのが良いかも?

f:id:ngyuki:20171114204639p:plain

さいごに

大抵の場合は Prometheus のデフォルトの 15 日で十分な気がするのですが、たまに長期の傾向が知りたいことあります(1 年間を通して見て、あーこの時期はあれがあったわー、とか)。

今は、短期グラフのために Prometheus を、長期グラフのために Cacti を併用していたりするのですが・・・

Prometheus -> InfluxDB でダウンサンプリングする場合、InfluxDB の保持期間ごとに Grafana で別々にグラフを作る必要があってちょっと面倒な感じもしました(Cacti・・というか RRDTool ならデータを取得する期間に応じて自動的に適切な粒度を拾ってきてくれる)。が、そもそも短期と長期でグラフをみるきっかけ違うし、そんなには困らないかな?

長期グラフを見ているときに特定の期間にドリルダウンしても短期グラフに変わらないのが不便かも・・

テンプレート変数でどうにかするものなのだろうか。

Prometheus 使ってみたメモ

9ヶ月ぐらい前?に社内の勉強会っぽいなにかで話そうと思ってたけど結局やらなくてお蔵入りになっていたメモ。

このとき試した Prometheus はだいぶ古いです・・・たぶん 1.5 ぐらいです。今が 2.0 とかなので色々変わっていると思います


メモ

  • 監視対象でエージェントを実行してマネージャーが HTTP でメトリクスを取得するいわゆる PULL 型
    • エージェントは Exporter と呼ばれる
  • ワンバイナリで他に依存もない
    • インストールが超楽
  • 標準的な Exporter がデフォでめちゃくちゃいろんなメトリクスを取ってくる
    • とりあえず収集して、必要に応じでアラートとか可視化とかする、というメンタルで使える
    • あらかじめ監視設計を難しく考えなくてもとりあえず始めることができる
  • 外形監視や SNMP 監視もできることはできる
    • 外形監視や SNMP 監視のための Exporter がある
    • その Exporter を Prometheus に監視対象として設定する
    • Prometheus から PULL されたときに Exporter が実際の監視をする
  • Prometheus ではまともな可視化はできない
    • Grahana で可視化が基本
  • アラートや可視化には PromQL というクエリ式が使える
    • そこそこフレキシブルに書ける
  • デフォで 3G ぐらいメモリを使うらしい
  • ダウンサイジングができずデフォでは 15 日で消える
    • 期間は増やせるが 1 年とかのデータを保持するのは辛そう
    • InfluxDB などの別の時系列データベースにデータを送れる
    • ダウンサイジングしたければそっちでやる

参考

Prometheus セットアップ

下記のようにバイナリをダウンロードして実行してもいいし、

docker でも簡単に実行できます。

docker run \
  -v ~/data/prometheus:/prometheus:rw \
  -v ./prometheus.yml:/etc/prometheus/prometheus.yml:ro \
  -p 9090:9090 \
  prom/prometheus

設定ファイル prometheus.yml は次のような感じです。

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: prometheus
    scrape_interval: 5s
    static_configs:
      - targets: [localhost:9090]

デフォルトだと 9090 ポートでリッスンするのでブラウザで下記の URL を開くと画面が閲覧できます。

この画面は Expression browser と呼ばれるもので PromQL というクエリ式でグラフを表示したりできます。

本格的な可視化は Grafana でやるとして Grafana に設定するためのクエリ式を調べるのに使うと良いです。

監視対象 に Node exporter をセットアップ

↑の設定だと Prometheus 自身のメトリクスしか収拾されていないので、他のサーバのメトリクスも収集してみます。

監視するサーバには Node exporter をインストールします。

バイナリをダウンロードして実行します、設定ファイルとかはありません。

./node_exporter

試しに社内のサーバにインストールしたときの手順とかは下記の通り。

# CentOS 7 ... 再起動すると消えるのでちゃんとやるなら systemd のユニットファイルを作る
curl -L https://github.com/prometheus/node_exporter/releases/download/v0.14.0-rc.1/node_exporter-0.14.0-rc.1.linux-amd64.tar.gz |
  tar zxf -
cd node_exporter-0.14.0-rc.1.linux-amd64
systemd-run --unit=node_exporter ./node_exporter
systemctl status node_exporter

# CentOS 6
wget https://github.com/prometheus/node_exporter/releases/download/v0.14.0-rc.1/node_exporter-0.14.0-rc.1.linux-amd64.tar.gz
tar zxf node_exporter-0.14.0-rc.1.linux-amd64.tar.gz
cd node_exporter-0.14.0-rc.1.linux-amd64
cat <<EOS> /etc/init/node_exporter.conf
start on runlevel [2345]
stop on runlevel [!2345]
chdir $PWD
exec ./node_exporter
respawn
EOS
initctl start node_exporter

Node exporter を起動したサーバの http://localhost:9100/metrics を叩くとメトリクスがだらーっと出ていることがわかります。

これを prometheus.yml で次のような感じで設定します。

scrape_configs:
  - job_name: node
    static_configs:
      - targets:
          - vm01
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
      - source_labels: [__address__]
        replacement: ${0}:9100
        target_label: __address__

クエリ

Prometheus の画面でクエリを入力するとグラフが見えます。

instant-vector

メトリクス名をそのまま書くと instant-vector となります。これは時刻がインデックスな配列みたいなものなのです。

コードで表現すると下記のような感じ?

[
  "12:00": 100,
  "12:01": 101,
  "12:02": 103,
  "12:03": 104,
  "12:04": 109,
]

ロードアベレージのような GAUGE 値は instant-vector をそのままグラフに表示できます。

# ロードアベレージ
node_load1

range-vector

node_cpu とかは COUNTER 値なので、そのまま表示してもあまり意味がありません。

node_cpu[5m] のように角括弧で時間を指定すると range-vector になります。これは要素が5分の範囲の配列である配列のようなものです。

コードで表現すると下記のような感じ?(実際にはこんな非効率な持ち方はしていない)

[
  "12:09": [
    "12:04": 10000,
    "12:05": 10000,
    "12:06": 10100,
    "12:07": 10300,
    "12:08": 10400,
    "12:09": 10900,
  ],
  "12:10": [
    "12:05": 10000,
    "12:06": 10100,
    "12:07": 10300,
    "12:08": 10400,
    "12:09": 10900,
    "12:10": 11100,
  ],
]

これはそのままだとグラフにできませんが range-vector を instant-vector に変換する関数を使えばグラフにできます。

例えば rate は range-vector範囲の最初と最後のポイントから導かれる1秒間の増加分です。なので、下記のようにすると CPU 使用率が求められます。

# CPU 使用率 ... 元が 100ミリ秒単位の CPU カウンタなので秒間の増加値がパーセンテージとして使える
rate(node_cpu[5m])

フィルタ

複数の監視対象がある場合、メトリクスのラベルでフィルタできます。

例えば特定のインスタンスのロードアベレージだけ表示してみたり。

node_load1{instance="sv01"}

否定の条件や正規表現も使えます。

node_load1{instance!="sv01"}
node_load1{instance=~"vm.*"}
node_load1{instance!~"vm.*"}

正規表現は文字列全体にマッチする必要があります(前後に ^$ が付く感じ)。また、わりと貧弱で言明とか使えません。

フィルタは監視対象を表す instance ラベルだけでなく、例えば node_cpu なら CPU の状態とかごとにラベルがあるので、例えば特定の状態をフィルタしたりできます。

rate(node_cpu{mode!="idle",instance="sv01"}[5m])

集計

CPU 使用率の場合、CPU コアごとにラベル付けされて記録されているため、複数コアだとコアごとに表示されます。

rate(node_cpu{mode!="idle",instance="vm01"}[5m])

集計関数を使えば合計した結果にできます。

sum(rate(node_cpu{mode!="idle",instance="vm01"}[5m]))

SQL での GROUP BY もできます。例えば下記の様にすれば mode ごとに集計できます。

sum(rate(node_cpu{mode!="idle",instance="vm01"}[5m])) by (mode)

こう書いても一緒です。

sum by (mode)(rate(node_cpu{mode!="idle",instance="vm01"}[5m]))

どのラベルで集計するか、ではなく、どのラベルを除いて集計するか、も出来ます。

例えば下記のようにすれば cpu ラベルを除いたラベルで集計されます。

sum(rate(node_cpu{mode!="idle",instance="vm01"}[5m])) without (cpu)

演算子

instant-vector は四則演算もできます。例えば下記のように CPU コア数に対しての CPU 使用率が計算できます。

sum(rate(node_cpu{mode!="idle"}[5m]) * 100) by (instance) /
  count(node_cpu{mode="system"}) by (instance)

メトリクス名をフィルタに使う

__name__ でメトリクス名をフィルタにできます。この方法を使えば1つのクエリで複数のメトリクスが同時に表示できます。

# ロードアベレージを 1s 5s 15s 全部表示
{__name__=~"node_load.*"}

# メモリ(Buffers + Cached + MemFree)
{__name__=~"node_memory_(Buffers|Cached|MemFree)"}

Grafana を使う場合はクエリ式を縦に並べればいいだけなのであんまり使うことはないです。

アラート

Prometheus のクエリ式の結果を元にメールとか Slack とかでアラートできます。

例えば下記のようなクエリ式でアラートできます。

ALERT cpu_usage
    IF sum(rate(node_cpu{mode!="idle"}[2m]) * 100) by (job, instance) /
       count(node_cpu{mode="system"}) by (job, instance) > 80
    FOR 30s
    LABELS { severity = "warning" }
    ANNOTATIONS {
        summary = "[{{ $labels.job }} {{ $labels.instance }}] cpu usage",
        description = "cpu usage now:{{$value}}",
    }

Grafana で複数の監視対象をずらーっと表示

Templating と Repeat for を使えば、同じ種類の複数の監視対象のグラフをずらーっと並べたりできます。

Cacti のように検索条件を指定してアドホックに表示とかはできなさそうだけど。

Grafana のダッシュボードのインポートとエクスポート

Grafana はダッシュボードの作成は基本的に WebUI ぽちぽちなのですが、Grafana の API を使えばダッシュボードの定義のインポートやエクスポートがサクッとできます。

Grafana の API は下記のように呼べます。

# /api/org の例
curl \
  -H "Authorization: Bearer $GRAFANA_API_KEY" \
  -H "Content-type: application/json" \
  "http://localhost:3000/api/org"

下記のスクリプトを用意してサクッと呼べるようにします。

#!/bin/bash

set -eu

api=${1#/}
shift
exec curl -H "Authorization: Bearer $GRAFANA_API_KEY" -H "Content-type: application/json" http://localhost:3000/$api "$@"

ダッシュボードをエクスポートします。

./grafana-api.sh /api/dashboards/db/servers | jq . > grafana/servers.json

トップレベルのキーに metadashboard がありますが、必要なのは dashboard だけです。

{
  "meta": {
    "...snip..."
  },
  "dashboard": {
    "...snip..."
  }
}

インポートするときは、新規追加なのか上書きなのかで微妙に異なります。

新規追加の場合は .dashboard.idnull にする必要があります。

cat grafana/servers.json |
  jq '. * {dashboard:{id:null}}' |
  ./grafana-api.sh /api/dashboards/db -X POST -d @-

上書きする場合はさらに .overwritetrue にする必要があります(同じ名前のダッシュボードが上書きされます)。

cat grafana/servers.json |
  jq '. * {dashboard:{id:null},overwrite:true}' |
  ./grafana-api.sh /api/dashboards/db -X POST -d @-

外形監視

外形監視も Blackbox exporter を使えばできます。

  • Prometheus に Blackbox exporter を監視対象として追加
    • このとき外形監視したいサーバをパラメータとして設定
  • Prometheus から PULL されるときパラメータも渡される
    • そのパラメータに基づいて Blackbox exporter が PING や HTTP で監視する
    • 結果を Blackbox exporter のメトリクスとして Prometheus に返す

Blackbox exporter 自体は外形監視先の情報を持っていないので、Blackbox exporter の設定は下記のように、どのように監視するか、しかありません。

modules:
  http_2xx:
    prober: http
    timeout: 5s
    http:
      valid_status_codes: [] # Defaults to 2xx
      method: GET
      headers:
        Host: example.com
  ssh_banner:
    prober: tcp
    timeout: 5s
    tcp:
      query_response:- expect: "^SSH-2.0-"
  icmp:
    prober: icmp
    timeout: 5s

このように設定された Blackbox exporter に対して Prometheus が下記のようにメトリクスを PULL することで実際の監視が実行されます。

SNMP

SNMP での監視も Snmp Exporter で外形監視と同じような仕組みで動きます。

ただ、監視する OID を Snmp Exporter でだばーっと設定する必要があります。例えば次のように。

default:
  version: 2
  auth:
    community: hoge
  walk:
    - 1.3.6.1.4.1.2021.4
    - 1.3.6.1.4.1.2021.11
  metrics:
    - { name: snmp_memTotalSwap      , oid: 1.3.6.1.4.1.2021.4.3.0   , type: gauge }
    - { name: snmp_memAvailSwap      , oid: 1.3.6.1.4.1.2021.4.4.0   , type: gauge }
    - { name: snmp_memTotalReal      , oid: 1.3.6.1.4.1.2021.4.5.0   , type: gauge }
    - { name: snmp_memAvailReal      , oid: 1.3.6.1.4.1.2021.4.6.0   , type: gauge }
    - { name: snmp_memTotalFree      , oid: 1.3.6.1.4.1.2021.4.11.0  , type: gauge }
    - { name: snmp_memMinimumSwap    , oid: 1.3.6.1.4.1.2021.4.12.0  , type: gauge }
    - { name: snmp_memBuffer         , oid: 1.3.6.1.4.1.2021.4.14.0  , type: gauge }
    - { name: snmp_memCached         , oid: 1.3.6.1.4.1.2021.4.15.0  , type: gauge }
    - { name: snmp_ssCpuRawUser      , oid: 1.3.6.1.4.1.2021.11.50.0 , type: counter }
    - { name: snmp_ssCpuRawNice      , oid: 1.3.6.1.4.1.2021.11.51.0 , type: counter }
    - { name: snmp_ssCpuRawSystem    , oid: 1.3.6.1.4.1.2021.11.52.0 , type: counter }
    - { name: snmp_ssCpuRawIdle      , oid: 1.3.6.1.4.1.2021.11.53.0 , type: counter }
    - { name: snmp_ssCpuRawWait      , oid: 1.3.6.1.4.1.2021.11.54.0 , type: counter }
    - { name: snmp_ssCpuRawKernel    , oid: 1.3.6.1.4.1.2021.11.55.0 , type: counter }
    - { name: snmp_ssCpuRawInterrupt , oid: 1.3.6.1.4.1.2021.11.56.0 , type: counter }
    - { name: snmp_ssIORawSent       , oid: 1.3.6.1.4.1.2021.11.57.0 , type: counter }
    - { name: snmp_ssIORawReceived   , oid: 1.3.6.1.4.1.2021.11.58.0 , type: counter }
    - { name: snmp_ssRawInterrupts   , oid: 1.3.6.1.4.1.2021.11.59.0 , type: counter }
    - { name: snmp_ssRawContexts     , oid: 1.3.6.1.4.1.2021.11.60.0 , type: counter }
    - { name: snmp_ssCpuRawSoftIRQ   , oid: 1.3.6.1.4.1.2021.11.61.0 , type: counter }
    - { name: snmp_ssRawSwapIn       , oid: 1.3.6.1.4.1.2021.11.62.0 , type: counter }
    - { name: snmp_ssRawSwapOut      , oid: 1.3.6.1.4.1.2021.11.63.0 , type: counter }

こまかいメモ

ここから下はこまかいことのメモ。

ストレージについて

https://prometheus.io/docs/operating/storage/

Memory usage

  • インデックスの場合は LevelDB を使用する
  • バルクサンプルデータの場合は独自のストレージ?
    • 1024 バイトのチャンクごとに整理される
    • チャンクは time series ごとにファイルに保存される
  • 現在使用されているすべてのチャンクをメモリに保持する
  • 最近使用されたチャンクは storage.local.memory-chunks までメモリに保持する
    • デフォルトは 1048576 で増やしたり減らしたりの調整をしても良い
  • サーバのメモリ使用量は storage.local.memory-chunks * 1024 よりも遥かに大きくなる
    • オーバーヘッドがあるので。また、単にサンプルを保存する以外のこともしているので
    • どの程度のオーバーヘッドがあるかは使用方法による
    • 設定値よりも多くのチャンクをメモリに乗せることもある
      • 使用するすべてのチャンクをメモリに乗せる必要があるので
    • 少なくとも 3 倍ぐらいのメモリを使用する
    • 下記のメトリクスでどれぐらい使われているか見ることができる
      • prometheus_local_storage_memory_chunks
      • process_resident_memory_bytes
  • 大量の time series を含む PromQL クエリは LevelDB バックエンドインデックスを大量に使用する
    • その種のクエリを実行するならインデックスキャッシュサイズを調整する必要がある

Settings for high numbers of time series

  • 100,000 を超える time series を扱うならストレージ設定の調整が必要
  • 本質的には、各 time series の特定の数のチャンクをメモリの保持する必要がある
    • storage.local.memory-chunks のデフォルトは 1048576 です
    • 300,000 series までは、series ごとに平均 3 のチャンクがある
    • もっと多くの series を扱うなら storage.local.memory-chunks を増やすべき
    • とりあえず最初は series の 3 倍にしておくと良い
  • 設定されているメモリチャンクよりも多くの series がアクティブになった場合・・・
    • 設定値よりも多くのチャンクをメモリに読む必要があるのだが・・・
    • 設定値を 10% 以上上回ると、設定値が 5% 以下になるまでサンプルの取得を抑止する
      • スクラップやルールの評価をスキップすることで
    • これはとても良くないことです
  • spinning disk に書き込むときは storage.local.max-chunks-to-persist の値を上げる
    • とりあえず最初は storage.local.memory-chunks の 50% ぐらいにしておくと良い
    • storage.local.max-chunks-to-persist はディスクに書き込まれるのを待つチャンクの数
    • 待機チャンクがこの値を超えると設定値の 95% に下がるまでサンプルの取り込みを調整する
    • series が 1M なら storage.local.memory-chunks は 3M ぐらい必要
    • このうち 2M が永続化可能なので・・・
    • storage.local.max-chunks-to-persist を 2M 以上にすると・・・
    • storage.local.memory-chunks の設定にかかわらず・・・
    • メモリ内に 3M 以上のチャンクが簡単に生成される

Helpful metrics

  • prometheus_local_storage_max_memory_chunks
    • storage.local.memory-chunks の設定値
  • prometheus_local_storage_memory_series
    • メモリに保持されているシリーズ数
  • prometheus_local_storage_memory_chunks
    • メモリに保持されているチャンク数
  • prometheus_local_storage_chunks_to_persist
    • ディスクに永続化する必要のあるメモリチャンク数
  • prometheus_local_storage_persistence_urgency_score
    • 緊急度スコア (0...1)
  • prometheus_local_storage_rushed_mode
    • 緊急モードのフラグ (0 or 1)

うーん? 下記のように設定しておけば OK かな。

  • storage.local.memory-chunks
    • 総メトリクス数の 3 倍
    • 総メトリクス数ってどうやって取れば良い?
    • prometheus_local_storage_memory_series で良い?
  • storage.local.max-chunks-to-persist
    • ↑の半分

Node exporter の調整

デフォだと dm-X みたいな LVM のディスクとか tmpfs とか nfs とかのファイルシステムとか vnat とかのインタフェースの情報まで取ってきてしまうので、除外パターンを調整する。

./node_exporter \
  -collector.diskstats.ignored-devices '^(dm-|[sv]d[a-z]|sr|drbd)\d+$' \
  -collector.filesystem.ignored-fs-types '^(sys|proc|root|rpc_pipe|tmp|n)fs$' \
  -collector.filesystem.ignored-mount-points '^/(sys|proc|dev|run)($|/)' \
  -collector.netdev.ignored-devices '^(vnet\d+|p5p\d+|br\d+|br\d+-nic|lo)$'

設定後は下記のようにコマンドでサッと確認する。

curl -s http://127.0.0.1:9100/metrics | grep node_filesystem_avail
curl -s http://127.0.0.1:9100/metrics | grep node_disk_bytes_read
curl -s http://127.0.0.1:9100/metrics | grep node_network_receive_bytes

PromQL でも下記のように確認する。

count(node_disk_bytes_read) without (instance)
count(node_filesystem_avail) without (instance)
count(node_network_receive_bytes) without (instance)

メモリ使用量

メモリ使用量は Node exporter の node_memory_* なメトリクスで詳細に取れるけど、下記でもリアルなメモリ使用量が取れる。

process_resident_memory_bytes
process_virtual_memory_bytes

この値は Prometheus でも Node exporter でも取れるっぽい(Prometheus 自身に Node exporter をセットアップしなくてもメモリ使用量は取れる、という意味)。

予測監視

あと何秒でディスクが枯渇するか、のような予測監視を deriv とか使ってできる。

(
  node_filesystem_size{instance='sv01'} - node_filesystem_free{instance='sv01'}
) / deriv(node_filesystem_free{instance='sv01'}[3d])

irate と rate

irate のが細かい変化が見れる、大まかな変化が知りたい時やアラートで FOR しているときは rate のが良い。

rate[5m] とかの範囲の最初と最後が、irate は範囲の最後の2点、irate での [5m] とかの範囲は「最後の2点」をどこまで遡るかの指定(無限に遡るわけにはいかないので)。

つまり、rate[5m] は 5 分間の平均を意味するので範囲の広さによって明らかにグラフが変わるが、irate[5m] はこの範囲の最後の 2 点という意味なので、範囲を広くしてもグラフは変わらない。ただし、短くしすぎて範囲内に観測点が 1 つしかなくなるとグラフが表示できなくなるので、それなりの広さにしておく必要がある。

アラートで FOR しているときは、閾値を超えた時間がそれだけ続いたらアラートにする、という意味になる。irate だとグラフが激しく振れるので、FOR が設定されているととアラートされにくくなる。

memory_chunks の監視

下記のように max_memory_chunks に対する memory_chunks の割合を監視したりしてみたけど、 たぶんあるだけ使うだろうのであんまりが意味ない(全データがメモリに乗せる前提とかじゃない限り)。

prometheus_local_storage_memory_chunks / prometheus_local_storage_max_memory_chunks * 100 > 70

chunks_to_persist とか、

prometheus_local_storage_chunks_to_persist / prometheus_local_storage_max_chunks_to_persist * 100 > 70

persistence_urgency_score とかで監視するのが良いだろう。

prometheus_local_storage_persistence_urgency_score * 100 > 60

InfluxDB にデータを渡す

Prometheus で remote_write を使えば取得したメトリクスを HTTP で外に投げることができる。この機能を用いて InfluxDB などの他の時系列データベースへデータを送ることができる。

InfluxDB を起動します。

docker run -p 8086:8086 -p 8083:8083 -e INFLUXDB_ADMIN_ENABLED=true \
  -v ~/data/influxdb:/var/lib/influxdb:rw influxdb

http://localhost:8083/ で管理画面を開いて、データベースとユーザーを作成します。

DROP DATABASE prometheus;
CREATE DATABASE prometheus;
CREATE USER prometheus WITH PASSWORD 'password';
GRANT ALL ON prometheus TO prometheus;

長期保存のためにデータを間引くための2つの RP を作成します。

CREATE RETENTION POLICY "tmp" ON prometheus DURATION 1d REPLICATION 1;
CREATE RETENTION POLICY "monthly" ON prometheus DURATION 30d REPLICATION 1 DEFAULT;

さらに Continuous Query を作成します。

CREATE CONTINUOUS QUERY monthly ON prometheus
BEGIN
  SELECT mean(value) as value
  INTO prometheus."monthly".:MEASUREMENT
  FROM prometheus."tmp"./.*/
  GROUP BY time(1h), job, virt, instance
END

Prometheus と InfluxDB を繋ぐためのコマンドをインストールします。

docker run -v /usr/local/bin:/go/bin:rw golang \
  go get github.com/prometheus/prometheus/documentation/examples/remote_storage/remote_storage_bridge

起動します。

INFLUXDB_PW=password remote_storage_bridge \
  -influxdb-url http://influxdb:8086 \
  -influxdb.database prometheus \
  -influxdb.username prometheus \
  -influxdb.retention-policy tmp

Prometheus で下記のように設定します。

remote_write:
  url: "http://remote_storage_bridge:9201/receive"
  write_relabel_configs:
    - source_labels: [__name__]
      regex: node_load.*
      action: keep

InfluxDB でクエリを実行すると書き込まれていることがわかります。

SHOW RETENTION POLICIES;
SHOW CONTINUOUS QUERIES
SHOW MEASUREMENTS;
SELECT * FROM tmp.node_load1;
SELECT * FROM monthly.node_load1;
SELECT * FROM node_load1;

"tmp" には Prometheus で取得したのと同じ間隔で記録されています。一方、"monthly" には Continuous Query により1時間毎の平均になった値が記録されます。

Grafana で InfluxDB のデータを見る

次のようなクエリで InfluxDB のデータを取得できます。

SELECT mean(value) FROM tmp.node_load1 WHERE time > now() - 1h GROUP BY time(5m), instance

Grafana に設定するときは time > now() - 1h$timeFilter に、5m$interval に置き換えます。

SELECT mean(value) FROM tmp.node_load1 WHERE $timeFilter GROUP BY time($interval), instance fill(null)

SELECT mean(value) FROM tmp./^node_load/ WHERE time > now() - 1h GROUP BY time(5m) fill(null)

他の Prometheus からデータを取ってくる(Federation)

他の Prometheus サーバからデータを取ってきたりできる。

データを取る元の prometheus.yml で下記のように設定するとできる。データを取る先(データ元)で特に設定は必要ないっぽい。

scrape_configs:
  - job_name: federate
    scrape_interval: 2m
    honor_labels: true
    metrics_path: /federate
    params:
      match[]:
        - instance_mode:cpu:rate
    static_configs:
      - targets:
        - other-prometheus:9090

保持期間やスクレイプの時間を変更した Prometheus を別に設けてそいつに Federate することでダウンサイジングしたりできる。

jQuery の Deferred と Promise/A+

jQuery の Deferred といわゆる Promise/A+ について、ごっちゃになってたのでメモ。

then/catch で発生した例外は reject された Promise になる

jQuery 3 から? Promise/A+ 互換になったため then/catch で発生した例外は reject された Promise として次のチェインに渡る。

$.Deferred().resolve(1).promise()
    .then((v) => {
        console.log('then', v); // then 1
        throw 2;
    })
    .catch((v) => {
        console.log('catch', v); // catch 2
    })

なので catch で処理しない例外は静かに無視される。

$.Deferred().resolve(1).promise()
    .then((v) => {
        console.log('then', v); // then 1
        throw 2;
    })
    .then((v) => {
        console.log('then', v); // never
    })

Node.js の Promise ならそういう状況では以下のような警告が表示される。

(node:87126) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): 2
(node:87126) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

そうではない処理系のために Promise のライブラリによっては Promise#done というメソッドが設けられていることがあるらしい。

要するにチェインの最後で .done() しておけばキャッチされない例外は Promise の外まで飛んで行くようになる。

一方で jQuery の done/fail/always で例外が発生した場合はそのまま Promise の外まで飛んで行く。done の動きは↑の説明とぜんぜん違うので注意(done は Promise/A+ の仕様ではないので jQuery が特別おかしいわけではない)。

$.Deferred().resolve(1).promise()
    .done((v) => {
        console.log('done', v); // done 1
        throw 2;                // uncaught exception: 2
    })
    .fail((v) => {
        console.log('fail', v); // never
    })
    .always((v) => {
        console.log('always', v); // never
    })

jQuery で then しつつ catch されない例外を Promise の外に持っていきたければチェインの最後で次のようにすると良いだろう。

$.Deferred().resolve(1).promise()
    .then((v) => {
        console.log('then', v); // then 1
        throw 2;
    })
    .then((v) => {
        console.log('then', v); // never
    })
    .fail((err) => {
        throw err;              // uncaught exception: 2
    })

jQuery の done/fail/always の戻り値は Promise の状態を変えない

thencatch はコールバック関数が返した値で解決された新たな Promise を返す(コールバックが Promise を返したならその Promise の状態と値を持つ新たな Promise が返る)。

$.Deferred().reject(1).promise()
    .catch((v) => {
        console.log('catch', v); // catch 1
        return 2;
    })
    .then((v) => {
        console.log('then', v); // then 2
        return 3;
    })
    .then((v) => {
        console.log('then', v); // then 3
        return $.Deferred().reject(4).promise();
    })
    .catch((v) => {
        console.log('catch', v); // catch 4
    })

一方で done/fail/always の戻り値は元の Promise のまま。

var promise = $.Deferred().resolve(1).promise();
console.log(promise === promise.done(() => {})); // true
console.log(promise === promise.then(() => {})); // false

そのためコールバックが何を返してもチェインされる Promise の状態や値は変わらない。

$.Deferred().reject(1).promise()
    .always((v) => {
        console.log('always', v); // always 1
        return 0;
    })
    .fail((v) => {
        console.log('fail', v); // fail 1
        return 2;
    })
    .done((v) => {
        console.log('done', v); // never
        return 3;
    })
    .done((v) => {
        console.log('done', v); // never
        return $.Deferred().reject(4).promise();
    })
    .fail((v) => {
        console.log('fail', v); // fail 1
    })

jQuery の Promise は複数の値を持てる

本来 Promise/A+ では1つの値しか持てない。

Promise.resolve(1, 2, 3)
    .then((a, b, c) => {
        console.log('then', a, b, c); // then 1 undefined undefined
    })

jQuery の Promise は複数の値を持てる。

$.Deferred().resolve(1, 2, 3).promise()
    .then((a, b, c)=>{
        console.log('then', a, b, c); // then 1 2 3
    })

thencatch をチェインするときに次の Promise に複数の値を持たせたいときは複数の値を持つ jQuery の Promise を返せば良い。

$.Deferred().resolve(1, 2).promise()
    .then((a, b)=>{
        console.log(a, b); // 1 2
    })
    .then((a, b)=>{
        console.log(a, b); // undefined undefined
        return $.Deferred().resolve(3, 4)
    })
    .then((a, b)=>{
        console.log(a, b); // [3, 4]
    })

さいごに

昨今はバベれば async/await が非常に書きやすいので、jQuery 独特の仕様に依存しないよう $.ajax をラップした関数で Promise が1つの値だけを持つようにして、async/await 前提で書くのが良いと思う。

function ajaxGet(url, params) {

    return $.ajax(/* ... */)
        .then(
            (data, textStatus, jqXHR) => {

                // data を見て(必要なら jqXHR とかも)リクエストの成否を判断する

                if (err) {
                    // リクエストが失敗しているなら失敗の理由を示すなにかを例外として投げる
                    throw err;

                    // こっちのほうが良いかも
                    return Promise.reject(err);
                }

                // リクエストが成功したならその結果を返す
                return data;
            },
            (jqXHR, textStatus, errorThrown) => {
                
                // リクエストの失敗を示すなにかを例外として投げる
                throw err;
            }
        )
}

async function handler() {

    try {
        const data = await ajaxGet('/path/to/api', {});

        // .done() の処理

    } catch (err) {

        // .fail() の処理

        // 握りつぶさないように再送
        throw err;

    } finally {

        // .always() の処理 
    }
}

バベれないときは thencatch で発生した例外が闇の彼方に葬られないように気をつける必要がある。 ↑の方で書いたように thencatch の後で fail で例外を throw するか、catchsetTimeout(function () { throw err }, 0) とかだろうか。

Node.js で jQuery を使う

jQuery の DOM とは関係のない Deferred の動きを確認するために jQuery を Node.js で動かしたかったのだけど、下記によるとそういう場合でも window オブジェクトが必要とのこと。

ただ、この通りにやってもなんか動きませんでした。

下記のどちらかで動かせられました。

jsdom

yarn add jquery jsdom
const { JSDOM } = require('jsdom')
const $ = require('jquery')(new JSDOM().window)

$.Deferred().resolve(1).promise()
    .then((v) => { console.log(v) })

jsdom-no-contextify

yarn add jquery jsdom-no-contextify
const $ = require('jquery')(require('jsdom-no-contextify').jsdom().parentWindow)

$.Deferred().resolve(1).promise()
    .then((v) => { console.log(v) })

jQuery で data アトリビュートから文字列を取り出すときは .attr() を使うべき

PHP から JS に値を渡したいとき、PHP から適当な要素の data アトリビュートに書き出して jQuery の .data() メソッドで取り出していたんですけど・・・

http://api.jquery.com/data/ ... When the data attribute is an object (starts with '{') or array (starts with '[') then jQuery.parseJSON is used to parse the string; it must follow valid JSON syntax including quoted property names. If the value isn't parseable as a JavaScript value, it is left as a string. ...

なんと .data() メソッドで data アトリビュートから値が取られるとき、値が JSON としてパースできるならパースした結果が返り、できなければ属性値がそのまま文字列として返る仕様でした。

ので、PHP から JS に文字列を渡したいときに jQuery の .data() メソッドで取っていると、文字列がたまたま JSON っぽかったときに文字列ではなく配列やオブジェクトになってしまうことがあります。

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
    <script src="//code.jquery.com/jquery-3.2.1.min.js"></script>
</head>
<body>
    <script id="data" data-aaa="[1, 2, 3]" data-bbb="[1, 2, 3]x">
        console.log($('#data').data('aaa'));      // Array [ 1, 2, 3 ]
        console.log($('#data').data('bbb'));      // "[1, 2, 3]x"
        console.log($('#data').attr('data-aaa')); // "[1, 2, 3]"
        console.log($('#data').attr('data-bbb')); // "[1, 2, 3]x"
    </script>
</body>
</html>

jQuery の .data() は元々は HTML の data アトリビュートから値を取るためのものではなく jQuery で独自に要素とデータを関連付けるためのもので(要素のプロパティとして直接データを持たせることによる DOM と JS オブジェクトを跨る循環参照よるメモリリークの問題を解消するため?)、data アトリビュートからも値が取れるのは後付けの機能だったと思うので、単に data アトリビュートから文字列を取りたいときは .data() ではなく .attr() を使うべき、ということですね。

ちなみにこんな感じに判定されているようです。

配列やオブジェクトなら PHP から json_encode した値を書き出して jQuery で .data() を使えば受け渡しが簡単にできて便利?なのかな??

超長い文字列や配列でも実体が同じなら中身の大きさには依存しない

手元にあった PHP 7.1.10 で試してます。


PHP の配列で、連想配列なのかいわゆる添字が順序通りのただの配列なのかを調べるには、下記の方法が一番手っ取り早いと思います。

array_values($arr) === $arr;

がしかし、配列の === は配列の要素の値の比較も行われるため、これだと配列の中に超でかい文字列や配列が入っているときに超遅くなります。

比較する必要があるのはキーだけなはずなのに配列の要素の値に依存して実行時間が変わるのはイケていないので array_keys でキーだけ取り出して比較したり愚直に foreach で回す方が良いですね・・・そんなふうに考えていた時期が俺にもありました。

いろんなパターンでベンチして最適な方法さがすぞー!と意気込んだところ↑のパターンが概ね良好でした(要素数が少ないときや早期にミスマッチが判断できるときは foreach のが早いこともある)。

<?php
namespace array_values {
    function isSequence($arr)
    {
        return $arr === array_values($arr);
    }
    $namespaces[] = __NAMESPACE__;
}

namespace array_keys {
    function isSequence($arr)
    {
        return array_keys($arr) === array_keys(array_keys($arr));
    }
    $namespaces[] = __NAMESPACE__;
}

namespace range {
    function isSequence($arr)
    {
        if (count($arr) === 0) {
            return true;
        } else {
            return array_keys($arr) === range(0, count($arr) - 1);
        }
    }
    $namespaces[] = __NAMESPACE__;
}

namespace foreach_ {
    function isSequence($arr)
    {
        $j = 0;
        foreach ($arr as $i => $v) {
            if ($i !== $j++) {
                return false;
            }
        }
        return true;
    }
    $namespaces[] = __NAMESPACE__;
}

namespace {
    function test($ns)
    {
        $func = "$ns\\isSequence";

        assert($func([]) === true, "$func([]) === true");
        assert($func([2, 4, 6]) === true, "$func([2, 4, 6]) === true");
        assert($func([2, 4, 'x' => 6]) === false, "$func([2, 4, 'x' => 6]) === false");
        assert($func([2 => 2, 1 => 4, 0 => 6]) === false, "$func([2 => 2, 1 => 4, 0 => 6]) === false");
    }

    $data = [
        '[]' => [],
        '[2,4,6]' => [2, 4, 6],
        '[x:9]' => ['x' => 9],
        '[1:1,0:0]' => [1 => 1, 0 => 0],
        '[0..10000]' => range(0, 10000),
        '{0..10000, x:x}' => range(0, 10000) + ['x' => 'x'],
        '[0, 0..100000]' => [0, range(0, 100000)],
        '{x:x, z: 0..100000}' => ['x' => 'x', 'z' => range(0, 100000)],
        '[0, x..x]' => [0, str_repeat("x", 100000)],
        '{x:x, z: x..x}' => ['x' => 'x', 'z' => str_repeat("x", 100000)],
    ];

    foreach ($namespaces as $ns) {
        test($ns);
    }

    foreach ($namespaces as $ns) {
        $results = [];
        foreach ($data as $name => $arr) {
            $func = "$ns\\isSequence";
            for ($i=0, $t=microtime(true)+1; microtime(true)<$t; $i++) {
                $func($arr);
            }
            printf("%-16s%-24s%d\n", $ns, $name, $i);
            $results[] = $i;
        }
        printf("%-16s%-24s%d ... %d\n", $ns, "", min($results), max($results));
    }
}
array_values    []                      3947641
array_values    [2,4,6]                 3383804
array_values    [x:9]                   3956602
array_values    [1:1,0:0]               3893512
array_values    [0..10000]              5912
array_values    {0..10000, x:x}         6079
array_values    [0, 0..100000]          3246093
array_values    {x:x, z: 0..100000}     3820617
array_values    [0, x..x]               3392908
array_values    {x:x, z: x..x}          3945126
array_values                            5912 ... 3956602
array_keys      []                      3104513
array_keys      [2,4,6]                 2405964
array_keys      [x:9]                   2596304
array_keys      [1:1,0:0]               2524770
array_keys      [0..10000]              3455
array_keys      {0..10000, x:x}         3429
array_keys      [0, 0..100000]          2456517
array_keys      {x:x, z: 0..100000}     2541887
array_keys      [0, x..x]               2523583
array_keys      {x:x, z: x..x}          2530141
array_keys                              3429 ... 3104513
range           []                      5123887
range           [2,4,6]                 1966400
range           [x:9]                   2065069
range           [1:1,0:0]               2093647
range           [0..10000]              4480
range           {0..10000, x:x}         4243
range           [0, 0..100000]          1963215
range           {x:x, z: 0..100000}     2149248
range           [0, x..x]               2077303
range           {x:x, z: x..x}          2039283
range                                   4243 ... 5123887
foreach_        []                      5173692
foreach_        [2,4,6]                 3569323
foreach_        [x:9]                   4653941
foreach_        [1:1,0:0]               4681362
foreach_        [0..10000]              4177
foreach_        {0..10000, x:x}         4102
foreach_        [0, 0..100000]          4032628
foreach_        {x:x, z: 0..100000}     4661640
foreach_        [0, x..x]               4007409
foreach_        {x:x, z: x..x}          4776680

実体が同じなら文字列や配列の中身は比較されない

ということのようです。

<?php
function func($a, $b, $name)
{
    for ($i=0, $t=microtime(true)+1; microtime(true)<$t; $i++) {
        $a === $b;
    }
    printf("%10d ... %s\n", $i, $name);
}

$a = str_repeat('x', 1024*1024*10);
$b = $a;
func($a, $b, 'same huge str');

$a = str_repeat('x', 1024*1024*10);
$b = str_repeat('x', 1024*1024*10);
func($a, $b, 'diff huge str');

$a = 'x';
$b = 'x';
func($a, $b, 'diff tiny str');
  11970864 ... same huge str
       782 ... diff huge str
  11892552 ... diff tiny str

最初のパターンはちょうでかい文字列ですけど実体が同じ変数の比較なので1文字の文字列の比較と差がありません。 一方で2番目のパターンは同じ文字列ではあるものの実体が異なるので超遅いです。

配列でも同様です。

<?php
function func($a, $b, $name)
{
    for ($i=0, $t=microtime(true)+1; microtime(true)<$t; $i++) {
        $a === $b;
    }
    printf("%10d ... %s\n", $i, $name);
}

$a = range(1, 1000000);
$b = $a;
func($a, $b, 'same huge arr');

$a = range(1, 1000000);
$b = range(1, 1000000);
func($a, $b, 'diff huge arr');

$a = [];
$b = [];
func($a, $b, 'diff tiny arr');
  11500321 ... same huge arr
        82 ... diff huge arr
  11197598 ... diff tiny arr

PHP の変数は Copy on Write になっており単に = でコピーしただけなら実際に確保されている文字列や配列のためのメモリ領域は共有されており、なんらかの変更を行ったときに実際の領域がコピーされます。

そして、2つの変数を比較するとき、変数の中身が同じ実体で同じ領域を指しているのであれば、中身を比較するまでもなく「一致」と判断することができるのでしょう(ソースは見ていないけどたぶん)。

配列のキーも文字列のサイズには依存しない

PHP の配列はハッシュテーブルなので文字列をキーにする場合は文字列を元にハッシュ値を計算する必要があります。なので、あまりに巨大な文字列をキーにすると計算のコストが増大して性能が劣化する・・・そんなふうに考えていた時期が俺にもありました。

<?php
function func($arr, $key, $name)
{
    $arr[$key] = 1;
    for ($i=0, $t=microtime(true)+1; microtime(true)<$t; $i++) {
        $v = $arr[$key];
    }
    printf("%10d ... %s\n", $i, $name);
}

$key = str_repeat('x', 1024*1024*10);
$arr[$key] = 1;
func($arr, $key, 'same huge str');

$key = str_repeat('x', 1024*1024*10);
$arr[$key . ''] = 1;
func($arr, $key, 'diff huge str');

$key = 'x';
$arr[$key . ''] = 1;
func($arr, $key, 'diff tiny str');
  11339929 ... same huge str
       781 ... diff huge str
  11241169 ... diff tiny str

最初のパターンは超でかい文字列をキーにしているのでハッシュ値の計算のために性能が劣化しそうなものですが、キーが短いときと代わりありません。

文字列自体が配列のキーとして使うときのためのハッシュ値を持っているとでもいうの・・・

zend_stringに関するメモ - Qiita

struct _zend_string {
        zend_refcounted_h gc;
        zend_ulong        h;                /* hash value */
        size_t            len;
        char              val[1];
};

あ、持ってるっぽい、なるほど。

PHP 5.4.16

まだまだ現役?? の PHP 5.4.16 で試してみました。

# str.php
      1801 ... same huge str
       816 ... diff huge str
   5962900 ... diff tiny str

# arr.php
   5819125 ... same huge arr
        47 ... diff huge arr
   5843144 ... diff tiny arr

# key.php
        85 ... same huge str
        84 ... diff huge str
   6210093 ... diff tiny str

配列の比較だけは PHP 7 と同じような傾向ですが、文字列の比較と配列のキーはご覧の有様でした。

さいごに

PHP 7 なめてた。

例えば PSR-7 を実装したリクエストオブジェクトのアトリビュートに何か入れるとき、名前の競合を避けるためにクラス名を使いたかったりすることあるんですけど、

$request = $request->withAttribute(HogeAttr::class, new HogeAttr($hoge));

今日日の PHP は名前空間が付いていてクラス名の完全修飾名は長くなりがちなので、リクエストオブジェクトの中でアトリビュート名が配列のキーとして持たれることを考えるとクラス名よりも短い文字列の定数とかを別に用意するのが良いだろうかと思ったのだけど・・・

$request = $request->withAttribute(HogeAttr::NAME, new HogeAttr($hoge));
// or
$request = $request->withAttribute('hoge', new HogeAttr($hoge));

そんなこと考える必要は無いということですね。むしろクラスがロードされた時点で存在していることが明らかであるクラス名の文字列の方が好ましいでしょう(::class が使えるならですけど)。

DI コンテナのオブジェクトの ID も同じです。PSR-11 で ID にはクラス名やインタフェース名を使うのを奨励、みたいなことが書かれてた気がしますが、PHP 7 ならクラス名やインタフェース名が超長くてもノーコストなのですね(コンテナの実装に依る)。

DIコンテナ使ってみて思った雑文:その2

アプリケーションにテーブルゲートウェイがあったとして、もちろんDB接続に依存している。

class UserTable
{
    private $connection;

    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    // select/insert/update/delete/etc...
}

テーブルゲートウェイはテーブルの数だけ存在するので、全部に同じ実装を書くのはDRYじゃないので抽象クラスを作る。

abstract class AbstractTableGateway
{
    private $connection;

    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    // select/insert/update/delete/etc...
}

class UserTable extends AbstractTableGateway
{
    // UserTable spesific method...
}

特定のテーブルで Connection 以外のなにかに依存する場合、コンストラクタを拡張して DI する。

class HogeTable extends AbstractTableGateway
{
    private $hoge;

    public function __construct(Connection $connection, Hoge $hoge)
    {
        parent::__construct($connection);
        $this->hoge = $hoge;
    }

    // HogeTable spesific method...
}

おっと、ここでテーブルのメタデータを保存するために AbstractTableGateway でキャッシュを使いたくなった。

abstract class AbstractTableGateway
{
    private $connection;
    private $cache;

    public function __construct(Connection $connection, Cache $cache)
    {
        $this->connection = $connection;
        $this->cache = $cache;
    }

    // select/insert/update/delete/etc...
}

すると HogeTable のコンストラクタも弄らなければならない。

class HogeTable extends AbstractTableGateway
{
    private $hoge;

    public function __construct(Connection $connection, Cache $cache, Hoge $hoge)
    {
        parent::__construct($connection, $cache);
        $this->hoge = $hoge;
    }

    // HogeTable spesific method...
}

HogeTable 的には「キャッシュとか知らんがなそっちで勝手にやってくれ」なので HogeTable のコンストラクタにまで影響するのは違和感がある。

これはまあ Connection と Cache を併せたなにかを作るか(ConnectionFacade とか)、あるいは、Connection に Cache のインスタンスを持たせればスッキリする。

あるいは AbstractTableGateway のような抽象クラスを作って継承するのはやめて、委譲とかトレイトだけでどうにかするべきだろうか。

trait TableGatewayTrait
{
    abstract protected function getTableGateway();

    // select/insert/update/delete/etc...
}

class UserTable
{
    use TableGatewayTrait;

    private $tableGateway;

    public function __construct(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    protected function getTableGateway()
    {
        return $this->tableGateway;
    }

    // UserTable spesific method...
}

class HogeTable
{
    use TableGatewayTrait;

    private $tableGateway;
    private $hoge;

    public function __construct(TableGateway $tableGateway, Hoge $hoge)
    {
        $this->tableGateway = $tableGateway;
        $this->hoge = $hoge;
    }

    protected function getTableGateway()
    {
        return $this->tableGateway;
    }

    // HogeTable spesific method...
}

コンストラクタとかで定形パターンをなんども書くのが辛ければそれらもトレイトに含めて、必要に応じてコンストラクタをオーバーライドするとか。

trait TableGatewayTrait
{
    private $tableGateway;

    public function __construct(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    protected function getTableGateway()
    {
        return $this->tableGateway;
    }

    // select/insert/update/delete/etc...
}

class UserTable
{
    use TableGatewayTrait;

    // UserTable spesific method...
}

class HogeTable
{
    use TableGatewayTrait { __construct as constructTableGateway; }

    private $hoge;

    public function __construct(TableGateway $tableGateway, Hoge $hoge)
    {
        $this->constructTableGateway($tableGateway);
        $this->hoge = $hoge;
    }

    // HogeTable spesific method...
}

コンストラクタを as で別名にするのはさすがに変か。。。


コンストラクタインジェクション前提で考えると、継承元のコンストラクタを弄ったときに派生先のすべてに影響してしまうので継承させにくいなーと思ったけどよく考えたら DI とか関係なく継承してれば当たり前のことだった。