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とか)どういう基準で選定したかは・・・忘れた。

シンボリックリンクを用いたアトミックデプロイと opcache と realpath cache

これまで PHP のアプリケーションのデプロイは rsync でどべーとコードを撒いていました。が、それだと新旧のコードが混在するし Capistrano とかはデフォでシンボリックリンク切り替えでアトミックなデプロイになっているし、周回遅れな感じもしますが今後は似たような方法でデプロイしたいと思います。

releases/ ディレクトリの中にリリースタグでディレクトリを掘ってコードを配置して current を最新のリリースのディレクトリへのシンボリックリンクにします。そして Apache や Nginx でドキュメントルートを current の中の公開用のディレクトリに設定します(/path/to/app/current/public とか)。

/path/to/app/
  releases/
    20161213/
    20161224/
    20170101/
  current -> releases/20170101

なお、世間では(Capistrano では)リリース日時をディレクトリにしているようですけど、本番環境とか検証環境とかで同じバージョンなのに異なるディレクトリ名になるのもわかりにくいかな、と思うので、リリースタグ(Git のタグ)を使います。

新しいバージョンをデプロイする時は、新しいリリースタグでディレクトリを掘ってコードを rsync で撒いて、

/path/to/app/
  releases/
    20161213/
    20161224/
    20170101/
    20170112/ # new release
  current -> releases/20170101

current のシンボリックリンクの宛先を変更します。

/path/to/app/
  releases/
    20161213/
    20161224/
    20170101/
    20170112/
  current -> releases/20170112 # change symlink destination

シンボリックリンクのアトミックな入れ替えの ln -sfn vs mv -Tf で書いたように、アトミックにシンボリックリンクを切り替えれば新旧のコードが混在することなくデプロイが可能です。

が、しかし、PHP ではこのデプロイ方法の問題もよく知られています・・・

opcache と realpath cache が絡み合っていて、実際に試すと非常に不可解な動作になったりします。

いくつかの解決方法はあるのですが・・・とりあえず以下の選択肢は辛そうなので却下。

  • Blue Green Deployment
    • 構成からして考え直さなければならない
    • AWS の案件とかなら考えてもいいかも
  • mod_realdoc を使う (apache)
    • なんか辛そう

Apache を graceful restart

デプロイでシンボリックリンクを切り替えたあとに Apache を graceful restart します。

これなら opcache も realpath cache も間違いなくクリアされます。一番無難な方法です。

Nginx で $realpath_root

Nginx+PHP-FPM なら、Nginx の方で realpath に解決済のパスを渡すことで簡単に解決出来るらしいです。

fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;

これなら、シンボリックリンクを切り替えると PHP-FPM には PHP のコードは別のパスとして渡されるので、キャッシュが云々の問題はなくなります。

Apache でも mod_realdoc を使えば出来ますがね・・・

なお、この方法には後述の「キャッシュクリアしない場合の別の問題」の問題もあります。

opcache をクリアするページを呼ぶ

次のような opcache をクリアするページを作成しておいて、デプロイ後に curl でこのページを呼ぶ、という方法が考えられます。

<?php
opcache_reset();

なお、php の cli で opcache_reset() 実行しても意味がありません、opcache は sapi ごとの共有メモリにあるので cli と mod_php では別管理です。

で、一見この方法でうまくいきそうな気がしますが・・実際に試したところ realpath キャッシュが有効だとなんとも形容し難い動きになりました。

下記のように realpath キャッシュもクリアするようにしてもダメです。

<?php
opcache_reset();
clearstatcache(true);

opcache は共有メモリを用いて apache のプロセス間で共有されているのに対して、realpath キャッシュはプロセス単位なので、これだけだと複数の apache のワーカープロセスの realpath キャッシュをすべてクリアすることができないから・・なのかもしれない。

なので、この方法を用いる場合は realpath キャッシュは無効にしておくのが無難なようです。

realpath_cache_size = 0

opcache のタイムスタンプを毎回チェック

realpath キャッシュを無効にすれば、この問題は opcache だけの問題になります。その場合(mod_php なら)、opcache を明示的にクリアしなくても opcache によってファイルのタイムスタンプのチェックが行われる際に新しいファイルに置き換わります。

opcache のタイムスタンプのチェックは opcache.revalidate_freq に設定した秒数ごとに行われます。

この数秒の間、新旧のコードが混在することが妥協できるなら明示的にキャッシュをクリアする必要は無いし、妥協できないのであれば、0 を指定すればリクエストの都度チェックされるようになるので、下記のように設定すれば opcache をクリアしなくてもシンボリックリンクを切り替えたタイミングでアトミックにコードが差し替わります。

realpath_cache_size = 0
opcache.revalidate_freq = 0

ただし、この設定だとリクエストの都度 require/include で読まれるすべてのファイルの realpath が解決されて、かつ、タイムスタンプのチェックが行われるようになります。

それが問題にならない場合は、この設定が一番なにも考えなくても良い無難な設定となるでしょう。

ただ、この方法には後述の「キャッシュクリアしない場合の別の問題」の問題もあります。

キャッシュクリアしない場合の別の問題

opcache にしても realpath cache にしても、キャッシュのサイズには上限があります。

realpath_cache_size = 16K
opcache.memory_consumption = 64M
opcache.max_accelerated_files = 2000

この上限に達したとき、古いキャッシュをパージして空き容量を確保する・・・なんてことはなく、単純に新しくキャッシュされなくなる、だけです。

つまり、上限に達するとそれ以降のファイルはキャッシュされなくなります。

realpath cache のデフォルトは現代のフレームワークだと少なすぎる気もするので適当に増やしておくと良いかもしれません。

一方で opcache はデフォルト値で十分なような気もします。

が、シンボリックリンク切り替えによるアトミックなデプロイしていると、新旧のコードが両方ともキャッシュとして残るので、キャッシュを明示的にクリアしない限り、キャッシュがどんどん肥大化します。

いずれは共有メモリのサイズかファイル数のどちらかの上限に達して、新しくデプロイしたコードが一切キャッシュされなくなってしまうことがあります。

そのため、opcache_get_status() を監視して一定値を超えたら手動でキャッシュをクリアするように運用しているところもあるそうです。

もっとも、Apache なら大抵の場合は logrotate のために毎日 restart なり graceful restart なりしていると思うので、あんまり関係ないと思いますが。

まとめ

Apache を graceful restart するのが一番簡単で問題も少ないと思います。

この場合は graceful restart したときにコードが切り替われば良いので opcache.revalidate_freq とか realpath_cache_ttl とかはかなり大きめでも良いと思います。

むしろ opcache.validate_timestamps = 0 にして、opcache によるタイムスタンプのチェックを行わないようにしても良いかもしれません。その場合、Apache を graceful restart しない限り、いつまでたってもコードは新しいものに反映されなくなります。

また、php ファイルの require/include に限って言えば opcache は realpath cache の前段のキャッシュのように振る舞います。つまり、require/include したときに opcache にヒットすれば realpath の解決は行われません。

realpath cache が利くのはほとんど php コードの require/include だと思うので(PHPUnit は死ぬほど realpath() を呼んでた気がするけど)、opcache が有効なら realpath cache を無効にしてもあんまり性能は劣化しません(むしろ性能がよくなることもある?という話も聞いた)。


昨年の終わりぐらいに社内用に書いていたメモからのコピペ。

今のところ、Apache をおもむろに graceful restart しても大丈夫な感じのシステムなので、opcache.revalidate_freqrealpath_cache_ttl は大きめに設定して、デプロイ後は graceful restart してます。

シンボリックリンクのアトミックな入れ替えの ln -sfn vs mv -Tf

WEB+DB Press vol.84 読んでて知った、シンボリックリンクのアトミックな入れ替え。

$ mkdir 1
$ touch 1/1.txt
$ mkdir 2
$ touch 2/2.txt
$ ln -s 1 cur
$ strace ln -sfn 2 cur
lstat("cur", {st_mode=S_IFLNK|0777, st_size=1, ...}) = 0
lstat("cur", {st_mode=S_IFLNK|0777, st_size=1, ...}) = 0
stat("2", {st_mode=S_IFDIR|0755, st_size=60, ...}) = 0
symlink("2", "cur")                     = -1 EEXIST (File exists)
unlink("cur")                           = 0
symlink("2", "cur")                     = 0
$ mkdir 3
$ touch 3/3.txt
$ ln -s 3 cur.tmp
$ strace mv -Tf cur.tmp cur
lstat("cur.tmp", {st_mode=S_IFLNK|0777, st_size=1, ...}) = 0
lstat("cur", {st_mode=S_IFLNK|0777, st_size=1, ...}) = 0
rename("cur.tmp", "cur")                = 0
:

前者は unlink -> symlink なのに対して後者は rename だけなので、つまり ln -sfn よりも mv -Tf の方がよりアトミック。


だいぶ前(1年半ぐらい?)社内用に書いてたメモからのコピペ。

監視とかで定期的に SSH するなら ControlMaster を設定するとスッキリするけど常用はちょっと

Cacti の Percona のテンプレートを使う場合、Cacti から監視対象のサーバに SSH で接続してメトリクスを取ってくることになるのですが・・・普通にやると都度サーバにSSH接続する事になりますし、対象サーバの /var/log/secure にログが無駄に記録されて辛いです。

そこで ControlMaster を使います。

ControlMaster とは

適当なディレクトリに ssh_config を下記のように作成します。

ControlMaster auto
ControlPath ~/.ssh/mux-%r@%h:%p
ControlPersist 10m

ssh -F ssh_config ... のようにこのファイルを指定して対象サーバに接続します。すると・・・

  • 最初に接続しようとしたときにバックグラウンドでマスター接続を張るプロセスが起動する
  • このマスター接続は ~/.ssh/mux-%r@%h:%p というファイル名でソケットファイルを作る
  • このソケットファイル上のマスター接続を用いて実際に SSH セッションは執り行われる
  • 次回以降の接続もソケットファイルが存在するなら自動的に使用される
  • 10m 以上マスター接続が使われなかったら切断されてソケットファイルも消える

つまり、1つの TCP のセッション上で複数の SSH のセッションが張られるようになる、みたいな感じです。

# バックグラウンドでマスター接続を張るプロセスが起動してその接続上で SSH セッションが張られる
ssh 192.0.2.123 uname -n

# ↑で作られたマスター接続のソケットファイルの接続上で SSH セッションが張られる
ssh 192.0.2.123 uname -n

# 同上
ssh 192.0.2.123 uname -n

:
:

# しばらく(↑の設定だと 10 分)放置するとマスター接続が切断される

# 新たにマスター接続を張るプロセスが起動する
ssh 192.0.2.123 uname -n

詳しくは・・・ControlMaster とか参照。

ControlMaster が有効な場合は /var/log/secure には最初のマスター接続のときだけログが記録されるので、ログもスッキリします。

常用はしないほうが良い

とても便利な ControlMaster ですが、普段から有効にしておけば同じサーバに何度も接続するときに接続のオーバーヘッドが小さくなるので、ユーザーの ~/.ssh/config で下記のようにしたくなりますが・・・

ControlMaster auto
ControlPath ~/.ssh/mux-%r@%h:%p
ControlPersist 10m

正直オススメしません。。。次のようなケースで大事故の元になります。

  • 本番系と検証系で同じ IP アドレスになっている
  • 直接接続することはできず中継サーバを挟む必要がある
  • どちらの系に接続するかは経由する中継サーバによって変わる
  • 例えば中継サーバは下記のアドレスだとすると・・・
    • 本番系に接続するときは 192.0.2.100 を経由
    • 検証系に接続するときは 192.0.2.200 を経由

ProxyCommand を使って下記のように本番系に接続したとします。

ssh -o "ProxyCommand ssh hj@192.0.2.100 -W %h:%p 2>/dev/null" ore@10.21.10.4

このとき、下記のようなソケットファイルが作成されます。

  • ~/.ssh/mux-ore@192.0.2.100:22
  • ~/.ssh/mux-ore@10.21.10.4:22

次に、検証系に次のように接続しようとします。サーバのアドレスは同じ(10.21.10.4)ですが、中継サーバのアドレスが異なるので検証系に接続されるはずです。

ssh -o "ProxyCommand ssh hj@192.0.2.200 -W %h:%p 2>/dev/null" ore@10.21.10.4

がしかし、実際には ControlMaster の機能により ~/.ssh/mux-ore@10.21.10.4:22 というソケットファイルが発見され、このソケットファイルを使用して SSH セッションが張られます。その接続先は本番系です。

つまり、検証系に接続するつもりが、本番系に接続されます。

さいごに

Nagios の check_ssh プラグインは ssh コマンドではなくプラグインが直接 TCP で接続しにいくので ControlMaster とか全然関係ないですしそもそも ControlMaster が有効では監視の意味がないです。

ので、Cacti からのアクセスで ControlMaster を有効にして /var/log/secure への出力を抑止したとしても、Nagios のせいでわりと /var/log/secure が汚れました。

ssh なんてサービスに必要なものではないし、Nagios の SSH 監視は頻度を下げるとかで良いかと。


社内用に書いていたメモからのコピペ、1年ぐらい前に書いたメモでした。そもそも Cacti はもうほとんど使ってないです(概ね Prometheus にした)。

「本番と検証でIPアドレスが同じで中継サーバが違う」という状況はよくあるように思うので(あんまり無い?)、常用はしないようにしてます。検証系のつもりで本番系に接続したことはないですけど、本番系のつもりで検証系に接続しててヒヤリしたことがあるので。

Jenkins でビルドのパラメータ化でブランチをフィルタしつつ自動でもビルドするメモ

文章で説明しにくい・・・要するに次のようにしたいとき。

  • masterissue/* のみをビルドの対象にする
  • Gitlab とかの WebHook で Jenkins の /git/notifyCommit を呼んでプッシュから自動ビルドさせる
  • 手動でビルドするときも↑のパターンにマッチするブランチを選択してビルドできる

自動と手動を併用しない、または両方ですべてのブランチを対象にする例はググるとよく見かける気がしたのですが、↑みたいなパターンでフィルタしつつ自動と手動を共存している設定例をあんまり見ない気がしたので、試行錯誤しました。

環境は下記の通り。

  • Jenkins 2.68
  • Jenkins plugins
    • Job DSL 1.63
    • Git Parameter Plug-In 0.8.0
    • Git plugin 3.3.1
    • Parameterized Trigger plugin 2.34
    • SCM API Plugin 2.1.1
    • etc…

次のように設定します。

  • ビルドのパラメータ化で Git Parameter を追加して・・・
    • Parameter TypeBranch を選択
    • Branch Filter に対象とするブランチを正規表現で指定
    • Default Value に↑の正規表現の頭に : を付けたものを指定
  • ソースコード管理の Git の ビルドするブランチ で↑のパラメータ名を指定する
    • パラメータ名が PARAM_GIT_BRANCH なら ${PARAM_GIT_BRANCH} のように指定

DSL だと次のような感じ。

job('test') {
    parameters {
        gitParameterDefinition {
            name('PARAM_GIT_BRANCH')
            type('BRANCH')
            // ↓の正規表現に `:` を付けたもの
            defaultValue(':^origin/(master|issue/[^/]+)$')
            description(null)
            branch(null)
            // ビルドするブランチの正規表現
            branchFilter('^origin/(master|issue/[^/]+)$')
            tagFilter(null)
            sortMode('ASCENDING_SMART')
            selectedValue('NONE')
            useRepository(null)
            quickFilterEnabled(false)
        }
    }
    scm {
        git {
            remote {
                url('git@git.example.com:oreore/testing.git')
                credentials('jenkins')
            }
            // ブランチに↑のパラメータを指定
            branches ('${PARAM_GIT_BRANCH}')
        }
    }
    triggers {
        scm('@midnight')
    }
}

手動でビルドするときは、存在するブランチから branchFilter の正規表現でフィルタされた候補が表示されて、そこから選択してビルドを開始できます。選択したブランチはソースコード管理の ビルドするブランチ にそのまま当てはめられます。

Gitlab の WebHook などから Jenkins の /git/notifyCommit を呼んで自動でビルドされるときは、パラメータが指定されないので、ビルドのパラメータの defaultValue で指定した値がソースコード管理の ビルドするブランチ に入ります。

ソースコード管理の ビルドするブランチ は先頭に : がついていると正規表現として解釈されるので branchFilter に指定した正規表現の頭に : を付けて defaultValue に指定することで、手動でビルドするときと自動でビルドされるときで同じブランチを対象にできます。


要するに、Branch FilterbranchFilter)が手動でビルドするときに選択候補に表示されるブランチ、Default Value(defaultValue)が自動でビルドするときに対象となるブランチ、になります。

ビルドのパラメータ化をしない場合は masterissue/* をビルドしたければ ビルドするブランチ に2行ブランチを指定するだけで良いのですが、

job('test') {
    scm {
        git {
            remote {
                url('git@git.example.com:oreore/testing.git')
                credentials('jenkins')
            }
            branches (
                'origin/master',
                'origin/issue/*',
            )
        }
    }
    triggers {
        scm('@midnight')
    }
}

ビルドをパラメータ化する場合は ビルドするブランチ にはパラメータ化した値が1つしか無いので↑みたいな2行の設定はできず、Default Value(defaultValue) に正規表現で指定する必要があります。


ちなみにブランチをフィルタせずにすべてのブランチを対象にする場合は次のようにしておくと良さそうです。

job('test') {
    parameters {
        gitParameterDefinition {
            name('PARAM_GIT_BRANCH')
            type('BRANCH')
            defaultValue('**')
            description(null)
            branch(null)
            branchFilter(null)
            tagFilter(null)
            sortMode('ASCENDING_SMART')
            selectedValue('NONE')
            useRepository(null)
            quickFilterEnabled(false)
        }
    }
    scm {
        git {
            remote {
                url('git@git.example.com:oreore/testing.git')
                credentials('jenkins')
            }
            // ブランチに↑のパラメータを指定
            branches ('${PARAM_GIT_BRANCH}')
        }
    }
    triggers {
        scm('@midnight')
    }
}

defaultValue を null とかにしてしまうと自動でビルドされるときに Couldn't find any revision to build. Verify the repository and branch configuration for this job. みたいになると思います。