重複排除スクリプト 2号

概要

重複ファイルの発見から重複の除去までを全自動で行うため

指定のファイルリストの重複チェックを行い、
見つけた重複ファイルは自動的に「ハードリンク」するスクリプトを書いた
半年以上動作しているのでそこそこ安定しているはず

「ハードリンク」の説明

例えば内容が同じ"./A/B/hoge.avi"と"./C/D/puga.avi"を「ハードリンク」すると
それぞれのパスはそのままで、ファイルの内容が共有される。
ショートカット機能との違いは「どっちも本物」な事。

スクリプト

以下のスクリプトを dedup.py 等として保存する

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sqlite3,sys,os,pickle,hashlib,base64

class HmDBO:
  con = None
  def __init__(self,dbpath):
    self.con = sqlite3.connect(dbpath)
    if not self.existFileInfoTable():
      self.createFileInfoTable()

  def commit(self):
    self.con.commit()

  def close(self):
    self.con.close()
    self.con = None

#
#fileinfo
#
  def existFileInfoTable(self):
    sql = u"""SELECT * FROM sqlite_master WHERE type='table' AND name='fileinfo';"""
    c = self.con.execute(sql)
    for o in c:
      return True
    return False
  
  def createFileInfoTable(self):
    sql = u"""
      create table fileinfo(
        hxpath varchar(64) primary key,
        b64path text,
        size integer,
        hash varchar(64),
        st_dev integer,
        st_ino integer
        );"""
    self.con.execute(sql)
    sql = u"""create index sizeindex on fileinfo(size);"""
    self.con.execute(sql)
    sql = u"""create index devindex on fileinfo(st_dev,st_ino);"""
    self.con.execute(sql)
    sql = u"""create index size2index on fileinfo(size,hash,st_dev);"""
    self.con.execute(sql)
    sql = u"""create index inoindex on fileinfo(st_ino);"""
    self.con.execute(sql)
    sql = u"""create index hashindex on fileinfo(hash,st_dev);"""
    self.con.execute(sql)

  def insertFileInfo(self,path,hash,st):
    sql = u"""insert into fileinfo values (?,?,?,?,?,?); """
    hxpath = hashlib.sha256(path).hexdigest()
    b64path = base64.b64encode(path)
    self.con.execute(sql,(hxpath,b64path,st.st_size,hash,st.st_dev,st.st_ino))
  
  def updateFileInfo(self,path,hash,st):
    sql = u"""update fileinfo set size = ?, hash = ?,st_dev = ?,st_ino = ? where hxpath = ?; """
    hxpath = hashlib.sha256(path).hexdigest()
    self.con.execute(sql,(st.st_size,hash,st.st_dev,st.st_ino,hxpath))
  
  def deleteFileInfo(self,path):
    sql = u"""delete from fileinfo where hxpath = ?;"""
    hxpath = hashlib.sha256(path).hexdigest()
    self.con.execute(sql,(hxpath,))
    
  def selectFileInfo(self,path):
    sql = u"""select size,hash,st_dev,st_ino from fileinfo where hxpath=?;"""
    hxpath = hashlib.sha256(path).hexdigest()
    c = self.con.execute(sql,(hxpath,))
    for o in c:
      return (o[0],o[1],o[2],o[3])
    return (None,None,None,None)
  
  def searchHashCode(self,st):
    sql = u"""
      select b64path,hash from fileinfo
      where st_dev = ?
        and st_ino = ?
        and hash is not null ;""";
    c = self.con.execute(sql,(st.st_dev,st.st_ino))
    ret = list()
    for o in c:
      path =  base64.b64decode(o[0])
      hash = o[1]
      if os.path.isfile(path):
        return hash
      else:
        self.deleteFileInfo(path)
    return None

  def listupFileSize(self):
    sql = u"""
      select size from fileinfo 
      group by size 
      having count(size) > 1 
         and count(st_ino) > 1 ;"""
    c = self.con.execute(sql)
    for size in c:
      yield size[0]

  def searchFileFromSize(self,size):
    sql = u""" select b64path from fileinfo where size = ? ; """
    c = self.con.execute(sql,(size,))
    for o in c:
      yield base64.b64decode(o[0])
  
  def searchFileDupHash(self):
    sql = u"""
      select hash,st_dev from fileinfo
      group by size,hash,st_dev
      having count(st_ino) > 1
    ;"""
    c = self.con.execute(sql)
    for o in c:
      yield (o[0],o[1])
  
  def searchFileFromHashOne(self,hash,st_dev):
    sql = u"""
      select b64path,st_ino from fileinfo
       where hash   = ?
         and st_dev = ?  ; """
    c = self.con.execute(sql,(hash,st_dev))
    for o in c:
      return (base64.b64decode(o[0]), o[1])
    return None
  
  def searchDedupFiles(self,hash,st_dev,st_ino):
    sql = u"""
      select b64path from fileinfo
        where hash  = ?
          and st_dev = ?
          and st_ino != ? ;"""
    c = self.con.execute(sql,(hash,st_dev,st_ino))
    for o in c:
      yield base64.b64decode(o[0])

_registercount = 0

def calcSHA256(dbo,path,st):
  global _registercount
  uhash = dbo.searchHashCode(st)
  if uhash != None :
    return uhash
  sys.stderr.write('.')
  _registercount += 1
  sha = hashlib.sha256()
  fp = open(path,'r')
  while True:
    cache = fp.read(65536)
    if not cache: break
    sha.update(cache)
  fp.close()
  return sha.hexdigest()

def updateCache(dbo,path,calcHash):
  size,hash,st_dev,st_ino = dbo.selectFileInfo(path)
  st = os.stat(path)
  if size == None:
    if calcHash and hash == None:
      hash = calcSHA256(dbo,path,st)
      dbo.insertFileInfo(path,hash,st)
    else:
      dbo.insertFileInfo(path,None,st)
  else:
    if (calcHash and hash == None) or hash != None and (size != st.st_size or st_dev != st.st_dev or st_ino != st.st_ino):
      hash = calcSHA256(dbo,path,st)
      dbo.updateFileInfo(path,hash,st)
  return (st.st_size, hash, st.st_dev, st.st_ino)

def releaseVer():
  return True

def hashmarge(dbpath):
  margecount=0
  filecount=0
  global _registercount
  _registercount=0
  
  dbo = HmDBO(dbpath)
  #register size
  for rwln in iter(sys.stdin.readline,""):
    path = rwln.rstrip('\n')
    if os.path.isfile(path):
      sys.stderr.write('stat ' + path + '...')
      filecount += 1
      #fileinfo
      updateCache(dbo,path,False)
      sys.stderr.write(' done\n')
  dbo.commit()
  #update hash
  updates = list()
  for size in dbo.listupFileSize():
    for path in dbo.searchFileFromSize(size):
      updates.append(path)
  updates.sort()
  for path in updates:
    sys.stderr.write('read ' + path + '...')
    if os.path.isfile(path):
      updateCache(dbo,path,True)
      sys.stderr.write(' done\n')
    else:
      dbo.deleteFileInfo(path)
      sys.stderr.write(' not found\n')
  dbo.commit()
  #linking
  for hash,st_dev in dbo.searchFileDupHash():
    (centerPath,c_ino) = dbo.searchFileFromHashOne(hash,st_dev)
    for tpath in dbo.searchDedupFiles(hash,st_dev,c_ino):
      sys.stderr.write('link ' + tpath + '...')
      if releaseVer():
        os.remove(tpath)
        os.link(centerPath,tpath)
        updateCache(dbo,tpath,True)
      margecount += 1
      sys.stderr.write(' done\n')
 
  dbo.commit()
  dbo.close()

  sys.stderr.write('files  = ' + str(filecount) + '\n')
  sys.stderr.write('register = ' + str(_registercount) + '\n')
  sys.stderr.write('marges = ' + str(margecount) + '\n')


def main():
  if len(sys.argv) < 2:
    exit()
  elif len(sys.argv) == 2:
    hashmarge(sys.argv[1])

if __name__ == '__main__':
  main()
使い方

ファイルリストはパイプ経由で渡してやり、
DBファイルのパスをパラメータで指定してやる形で利用する

$ find <ターゲットディレクトリ> | dedup.py <dbファイルのパス>
動作概要
  1. 受け取ったファイルリストのファイルサイズをdbに格納
  2. db中の同一サイズのファイルを読み重複チェック
  3. 重複していたファイルをハードリンク
注意点

ファイル数が5万以下ならそこそこ使えますが、
ファイル数があまりにも多くなると
内容が異なるファイル同士でファイルサイズの被りが発生するせいで
処理が遅くなってしまいます。

os.stat()が未対応のため windowsでは動作しませんでした

nilfs のテスト gc編

nilfs2でのGCについて調査をしてみました。

今回試した手順1

  1. 4並列でファイル作成/書き込み/削除の繰り返し
  2. 全履歴を保持する特性から、ファイル数 0 でも書き込み領域が満杯になる
  3. 自動でGCが起動
  4. GC中に4並列のランダムR/W
  5. 繰り返し実行してもエラーにならないか調べる

手順2

  1. 書き込み領域がほとんどない状況で、手動でGC
  2. 開放可能な全領域が処理される時間を図る

分かった事をまとめると。

  1. GC中はファイル読み込みでも数分間プチフリする場合がある
  2. GCで開放される領域は 70Mbyte/s 程度でシーケンシャル書き込みの半分程度の性能は出た
  3. GC中に並列で読み書きしても意外と落ちない

nilfs2 + raid5 を高速化する

raid5+xfs+nilfs2

raid5なパーティションをnilfs2でそのまま利用するより、
パーティションはxfsでフォーマットして、
ループバックファイルとしてnilfs2を利用したほうがいいみたいです。

ベンチマーク

ddでの書き込み速度を比較

nilfs2 xfs+nilfs2 xfs
write(MByte/s) 33 121 169

nilfs2のシーケンシャル書き込みの速度はxfsを経由したほうがずっと速いようです。

ESXi と KVM でベンチマーク比較してみた

KVMwindowsの仮想化がすごく苦手というイメージがあります、
そこでESXiに乗り換えればどの程度改善するかベンチマークしてみました。

テスト環境

  • ホストPC NEC Express5800/S70
  • ゲストOS windows server 2008r2 sp1 試用版
  • ゲスト割当HDD 40GB
  • ゲスト割当CPU 1core
  • ゲスト割当MEM 4GB
  • ゲストOSにはリモートデスクトップで接続

以上の条件で、CrystalMark 2004R3 を実行しました。

結果一覧

KVM(kernel 2.6.32-pve)

ESXi ゲストアドインなし

ESXi ゲストアドインあり

グラフで比較すると僅差でESXiが優秀です

まとめ

劇的な性能向上を期待したのですが、
元々の性能ロス(ベアメタルとの性能差)が少ないこともあり、
人が体感できるほどのの差は出ていません。

シビアな要求が無いならKVMでも十分かもしれません。

HDD 性能について

どちらもハイパーバイザ側でのディスクキャッシュが存在するため
正確な測定は難しいです。
※追記参照

参照

実機での数値はこちらのエントリを参考にしました。
http://damegamer.com/034

追記

上記のテストではKVMのHDDイメージ格納先にext3上のファイルを利用しましたが、
追加テストでHDDイメージ格納先にLVMを利用したベンチマークを行ないました。

LVMは以下のような特徴があります。

  • ext3によるディスクキャッシュがなくなる(スコア低下要素)
  • ext3ファイルシステムのオーバーヘッドがなくなる(スコア向上要素)

結果として、ベンチマークスコアは下がりました。
実性能ではLVMの方が上なハズなので、
ベンチマークスコアにキャッシュの有無はかなり影響するようです。

ubuntu 12.04 で lxc を使ってみた(natモード)

lxc は 12.04でびっくりするほど導入しやすくなってます、
今回はnatモードで動作するコンテナを作ってみました。

lxc インストール
$ sudo apt-get install lxc
ゲスト(コンテナ) インストール

以下形式です
sudo lxc-create -n <コンテナ名> -t <テンプレート名>

$ sudo lxc-create -n lxtest -t ubuntu
ネットワークの設定

lxcインストール時点で lxcbr0(例:10.0.3.0/24) が構築されています
ここに繋ぐ時のIPアドレス(例:10.0.3.10)を以下のファイルに追加してやります

/var/lib/lxc/<コンテナ名>/config

$ sudo vim /var/lib/lxc/lxtest/config
lxc.network.ipv4 = 10.0.3.10 #この行を追加
起動する

以下形式です
sudo lxc-start -n <コンテナ名> -d

$ sudo lxc-start -n lxtest -d
ssh で繋ぐ

デフォルトアカウントに user=ubuntu pass=ubuntu でログインできます

$ ssh ubuntu@10.0.3.10
本体起動時に自動起動させる

autostart させるために、シンボリックリンクを作成します

/var/lxc/auto/<コンテナ名>.conf 形式のリンクを作成します

$ sudo ln -s /var/lib/lxc/lxtest/config /var/lxc/auto/lxtest.conf
まとめ

仮想natの構築が自動で行われるため、非常に手軽に導入できるようになりました、
レンタルvps内での仮想化やノートPCにテスト環境作る時に便利そうです。

turnkey linuxをlxcで利用する

lxcでturnkey linuxを利用する方法が載ってました、
turnkey linuxのisoファイルをスクリプトでlxcコンテナに変換するみたいですね。

http://www.turnkeylinux.org/forum/general/20111016/script-create-turnkeylinux-lxc-containers

lxcなら最新カーネルでも利用できますし、いろいろ便利に使えそうです。

proxmox VE について その2

proxmox VE について引き続き調査してみました。

OpenVZのテンプレートの利用方法

proxmox VEではアプライアンス(アプリ組み込み済みOS)として
turnkey linux テンプレートが利用できます。

このテンプレート、管理者パスワードがすごく判りずらいのですが
初期値は"turnkey"で統一されているようです。

たとえばredmineテンプレートの場合管理者パスワードは初期状態で以下のように設定されています


user: admin
pass: turnkey