圧縮ファイルのハッシュ値がタイムスタンプによって変わってしまう
どんな問題が起こったのか
中身が同じファイルを、それぞれ圧縮した。
だが、圧縮ファイルのハッシュ値が同じにならなかった。
再現してみる
注意
PCの性能によっては、圧縮処理が一瞬で終わるかもしれません。
そうなると事象の再現ができないため、 sleep 1
のような処理を挟む必要があります。タイムスタンプによる影響を確かめるだけであり、ファイルの中身を変える処理ではないためご容赦ください。
中身が同じファイルを用意します。
➜ ls
foo.txt hoge.txt
➜ diff hoge.txt foo.txt
➜
念の為、ハッシュ値も同じか確かめる。
➜ irb >> require 'digest/md5' => true >> Digest::MD5.file('hoge.txt') => #<Digest::MD5: 7d54bd30550c55950ec0e7f65d1d53c7> >> Digest::MD5.file('foo.txt') => #<Digest::MD5: 7d54bd30550c55950ec0e7f65d1d53c7>
gzip圧縮します。
require 'zlib' require 'pathname' MAC_ONLY_FILE = '.DS_Store' SKIP_FILE = [MAC_ONLY_FILE, 'test.rb'] Dir.open('.') do |d| d.children.each do |child| next if SKIP_FILE.include?(child) sleep 1 # 事象の再現のために挿入 path = Pathname.new(child) gzfile_path = Pathname.new(File.join(path.dirname, "#{path.basename}.gz")) File.open(path, 'r') do |f| Zlib::GzipWriter.open(gzfile_path) do |gz| f.each_line do |line| gz.puts line end end end end end
圧縮ファイルのハッシュ値を確かめてみる。
>> Digest::MD5.file('hoge.txt.gz') => #<Digest::MD5: 4c537db81e4240fdeec3638319d6be60> >> Digest::MD5.file('foo.txt.gz') => #<Digest::MD5: 0e83011362f4781e60e7813d3a78a0e3>
合わないぞ。。。
なにが原因だったのか
圧縮時のタイムスタンプが、異なっているのが原因。また圧縮ファイルには、ファイル名とコメントも、gzipファイルのヘッダーに記録されます。
まとめると、圧縮時には以下の情報が含まれることになります。
- タイムスタンプ情報(atime, ctime, mtime)
- 圧縮元ファイル名
- コメント(デフォルトはnull)
ちなみに、atime, ctime, mtimeは、ファイルが持つタイムスタンプの情報です。詳しくはこちらを参考にしてください。
どうやって解決したか
設計と実装に分けて整理します。
設計
以下の仕様を満たす必要があった。
- 圧縮時のmtimeを、デフォルト値ではなくこちら側で制御する。
Zlib::GzipWriter#=mtimeを呼び出せば大丈夫です。
またmtimeは、UNIX TIMEで設定されています。そのため、実際に圧縮した時刻をUNIX TIMEで与える必要があります。
よって、最終的に仕様は以下のようになります。
- 圧縮時のmtimeを、デフォルト値ではなくこちら側で制御する。
- mtimeの設定を、現在時刻のUNIX TIMEにすること。
実装
time
モジュールからUNIX TIMEを作り出し、mtimeに代入するだけです。
require 'zlib' require 'pathname' require 'time' MAC_ONLY_FILE = '.DS_Store' SKIP_FILE = [MAC_ONLY_FILE, 'test.rb'] UNIX_TIME_NOW = Time.now.to_i # UNIX TIME Dir.open('.') do |d| d.children.each do |child| next if SKIP_FILE.include?(child) sleep 1 path = Pathname.new(child) gzfile_path = Pathname.new(File.join(path.dirname, "#{path.basename}.gz")) File.open(path, 'r') do |f| Zlib::GzipWriter.open(gzfile_path) do |gz| gz.mtime = UNIX_TIME_NOW # mtimeを上書きする f.each_line do |line| gz.puts line end end end end end
ハッシュ値が同じになっているか確認。
>> Digest::MD5.file('hoge.txt.gz') => #<Digest::MD5: 4cf682fe3c756d9225d01d1f74567c6d> >> Digest::MD5.file('foo.txt.gz') => #<Digest::MD5: 4cf682fe3c756d9225d01d1f74567c6d>
大丈夫そうです👍
おわりに
年内最後の記事でした!良いお年を〜!
Datadogのカスタムメトリクス収集方法を整理する
はじめに
Datadogで独自のメトリクスを取る方法が2つあり、どちらを使うべきか迷いました。そのため調べて分かったことを、ここに整理しておきたいと思います。
結論だけ知りたい方は、こちらからどうぞ。
カスタムメトリクスについて
ユーザが独自に設定したメトリクスを収集することができます。
例えばセッション情報を取得し、そこから細かに宛先IPや接続状況によって、アラートを鳴らしたいといった要望があるとします。取得するには netstat
なり ss
なり打って、その結果をDatadog側に送る必要があります。
カスタムメトリクスは、こういった独自の指標・メトリクスを取りたいときに有効です。
実現する方法として、Datadogでは主に
- カスタムチェック
- DogStatsD
があります。どちらを使うべきかは、状況によって変える必要があります。
結論
それぞれのユースケースまとめです。どちらを選んでも、最終的にやりたいことはできます。
カスタムチェックのユースケース
- サーバのメトリクスは基本的にこちらを使うべき。
- Pythonでしか使えない(agent v7からはPython3のみサポート)。
- チームがPythonを採用しているかで保守コストが変わる。
DogStatsDのユースケース
- Appのメトリクスは基本的にこちらを使うべき。
- クライアント呼び出しなので言語選択が広い。
- サーバのメトリクスを取ろうとすると、構成で考えることが多い。
Pros/Cons
カスタムチェック
pros
cons
- 今使える言語はPythonだけ。
- AgentCheckクラスを継承する必要がある。
- Pythonを採用していない場合、このために保守コストを払うか考える必要がある。
DogStasD
pros
- クライアントが豊富なので、言語の縛りがない。
cons
- サーバのメトリクスを取ろうとすると苦労が多い。
- 自前でcronのような、定期実行の仕組みを用意する必要がある。
- スクリプトの管理場所を考える必要がある。
- 取るメトリクスがサーバかAppで、置き場所を変えるのか。それともまとめて管理するのか。
おわりに
どちらの方法でも運用可能だとは思いますが、監視にかかる保守コストはできるだけ減らしたいですよね。
この記事がその助力になれば幸いです。
参考
MItamaeでdotfilesを管理してみた
MItamaeを使ってdotfilesを管理してみたら、お手軽に良い感じになったので紹介します。
そもそもMItamaeって?
プロビジョニングツール(Infrastructure as Code)であるitamaeを、mrubyで実装したものになります。
itamaeの文法が使えるため、シェルスクリプトやMakefileと違った、Ruby DSLライクな構成管理が可能になります。 そのため、RubyやChefを触ったことがある人は特に導入がしやすいかと思います(itamaeはChefの簡易版)。
itamaeじゃダメなの?
itamaeでは、実行時にRubyの環境が必要です。MacのようにデフォルトでRubyがインストールされていれば問題ありませんが、そうではない場合、Rubyの環境を自前で用意する必要があります。
MItamaeは環境に左右されないの?
mruby実装を生かして、コンパイルされた実行用のシングルバイナリが配布されています。カーネルによって対象のバイナリを変える必要があるため、そこだけはローカル環境に合ったものを、 wget
や curl
で落とす必要があります。
MItamaeを用いたdotfiles
私のdotfilesを参考にしながら、ディレクトリ構成と処理の流れを整理していきます。
ディレクトリの構成
以下のようになりました。
➜ tree -aL 2 . ├── .gitignore ├── LICENSE ├── README.md ├── bin │ ├── mitamae -> mitamae-1.9.5 │ ├── mitamae-1.9.5 │ └── setup ├── cookbooks │ ├── fish │ ├── functions │ ├── git │ ├── homebrew │ ├── nvim │ ├── python2 │ ├── python3 │ ├── rbenv │ ├── tfenv │ └── tig ├── install.sh ├── lib │ ├── recipe.rb │ └── recipe_helper.rb └── roles ├── base └── darwin
上から順に
bin/
... シングルバイナリの管理ディレクトリ。cookbooks/
... パッケージごとの設定ディレクトリ。install.sh
... dotfilesの起点スクリプト。lib/
... MItamaeの起点ディレクトリ。roles/
... カーネルごとの設定ディレクトリ。
処理の流れ
install.sh
を実行する。bin/setup
がキックされる。install.sh
に戻り、bin/mitamae local lib/recipe.rb
を実行する。bin/mitamae
がitamae
コマンドの起点になる。
- ローカルのカーネルと同じ
roles
設定ファイルを実行する。例えばMacなら、roles/darwin/
の設定を読み込むといったもの。 - パッケージごとの設定を記した、
cookbooks
を実行する。
dotfilesをMItamaeにして感じたメリット
Ruby DSLで書ける
経験不足もありますが、シェルスクリプトやMakefileのみのコードを保守し続けるのは少し敷居が高いです。その点MItamaeは、慣れているRuby DSLを使って保守し続けることができます。導入しようと思ったきっかけのひとつです。
itamaeのレールに乗れる
dotfilesの管理にプロビジョニングツールを使えるのは、メリットのひとつだと思います。例えば、itamaeの文法そのままにディレクトリを掘ったり、シンボリックリンクを貼る処理が書けます。公式が提供している構成のベストプラクティスも利用できます。
MItamaeのdotfiles実践プラクティス
ここでは開発中に、「こうすると良いのでは」と感じたことを整理したいと思います。
開発はitamaeで
開発環境では、itamaeコマンドを使えるようにしておくと良いです。例えばディレクトリを掘るために、 mkdir cookbooks/fish
を打つといった手作業がなくなります。主に使うコマンドは
- itamae generate [cookbook | role]
- itamae destroy [cookbook | role]
のふたつだと思います。 詳しくは itamae help
で確認しましょう。
シンボリックリンク処理を再定義しない
シンボリックリンクを貼る処理を、 dotfile
や ln
といった名前で再定義しているケースがあります。私も最初は以下のように処理をまとめていました。
define :ln do name = params[:name] link node[:xdg_config_home] do to File.expand_path("../../#{name}/files/#{name}", __FILE__) user node[:user] force true end end
しかし、これだと以下のような問題が発生します。
結局ブロック内で再定義する状況が生まれるなら、 わざわざ ln
関数を作る必要はないと思います。また ln
内を変更するとき、処理が破壊されていないか気を揉む必要があります。
こうなると例外として、 link
リソースを使いたくなるケースもあるかもしれません。ですが、シンボリックリンクを貼る処理が複数あるのは混乱の元です。どちらかにまとめたほうが保守はしやすいでしょう。
おわりに
MItamaeを使ったdotfiles管理は初めてだったのですが、今のところ不満はありません。
俗に言う「学習コスト」も
が主になるので、取り立てて難しくないでしょう。
時期も時期ですので、新しいPC移行用のdotfilesをMItamaeで作ってみてはどうでしょうか。