ls /asapon/blog

基本tech、時々多趣味

圧縮ファイルのハッシュ値がタイムスタンプによって変わってしまう

どんな問題が起こったのか

中身が同じファイルを、それぞれ圧縮した。
だが、圧縮ファイルのハッシュ値が同じにならなかった。

再現してみる


注意
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

  • 定期実行の仕組みをはじめから持っている。
    • checks.d/ 以下のpyファイルが読み込まれる。
  • 収集間隔や閾値といった個別設定の管理ができる。
    • conf.d/ 以下にyamlファイルで管理する。

cons

  • 今使える言語はPythonだけ。
    • AgentCheckクラスを継承する必要がある。
  • Pythonを採用していない場合、このために保守コストを払うか考える必要がある。

DogStasD

pros

cons

  • サーバのメトリクスを取ろうとすると苦労が多い。
    • 自前でcronのような、定期実行の仕組みを用意する必要がある。
    • スクリプトの管理場所を考える必要がある。
      • 取るメトリクスがサーバかAppで、置き場所を変えるのか。それともまとめて管理するのか。

おわりに

どちらの方法でも運用可能だとは思いますが、監視にかかる保守コストはできるだけ減らしたいですよね。
この記事がその助力になれば幸いです。

参考

MItamaeでdotfilesを管理してみた

MItamaeを使ってdotfilesを管理してみたら、お手軽に良い感じになったので紹介します。

そもそもMItamaeって?

プロビジョニングツール(Infrastructure as Code)であるitamaeを、mrubyで実装したものになります。

github.com

itamaeの文法が使えるため、シェルスクリプトMakefileと違った、Ruby DSLライクな構成管理が可能になります。 そのため、RubyやChefを触ったことがある人は特に導入がしやすいかと思います(itamaeはChefの簡易版)。

itamaeじゃダメなの?

itamaeでは、実行時にRubyの環境が必要です。MacのようにデフォルトでRubyがインストールされていれば問題ありませんが、そうではない場合、Rubyの環境を自前で用意する必要があります。

MItamaeは環境に左右されないの?

mruby実装を生かして、コンパイルされた実行用のシングルバイナリが配布されています。カーネルによって対象のバイナリを変える必要があるため、そこだけはローカル環境に合ったものを、 wgetcurl で落とす必要があります。

MItamaeを用いたdotfiles

私のdotfilesを参考にしながら、ディレクトリ構成と処理の流れを整理していきます。

github.com

ディレクトリの構成

以下のようになりました。

➜  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

上から順に

処理の流れ

  1. install.sh を実行する。
  2. bin/setup がキックされる。
    1. カーネルに合ったMItamaeバイナリを落とす。
    2. バイナリに bin/mitame という名前でシンボリックリンクを貼る。
  3. install.sh に戻り、 bin/mitamae local lib/recipe.rb を実行する。
    1. bin/mitamaeitamae コマンドの起点になる。
  4. ローカルのカーネルと同じ roles 設定ファイルを実行する。例えばMacなら、 roles/darwin/ の設定を読み込むといったもの。
  5. パッケージごとの設定を記した、 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 で確認しましょう。

シンボリックリンク処理を再定義しない

シンボリックリンクを貼る処理を、 dotfileln といった名前で再定義しているケースがあります。私も最初は以下のように処理をまとめていました。

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 関数を作る必要はないと思います。また ln 内を変更するとき、処理が破壊されていないか気を揉む必要があります。
こうなると例外として、 link リソースを使いたくなるケースもあるかもしれません。ですが、シンボリックリンクを貼る処理が複数あるのは混乱の元です。どちらかにまとめたほうが保守はしやすいでしょう。

おわりに

MItamaeを使ったdotfiles管理は初めてだったのですが、今のところ不満はありません。
俗に言う「学習コスト」も

が主になるので、取り立てて難しくないでしょう。
時期も時期ですので、新しいPC移行用のdotfilesをMItamaeで作ってみてはどうでしょうか。