Knowlbo 開発者ブログ

株式会社Knowlboの開発者ブログです。

ファイル一覧取得のパフォーマンス (C#)

開発部の本橋です。

C# において、あるディレクトリに格納されているファイルの一覧を取得するには通常 Directory.GetFiles メソッド*1を使います。 ですが、ファイルが大量に格納されているディレクトリの場合は非常に時間がかかるというデメリットがあります。

.NET 4.0 から Directory.EnumerateFiles メソッド*2が追加されており、これを使うと早い(というか要素が遅延評価される)のですが、では実際どのぐらい違うのか?というのを計測してみることにします。

準備

対象のディレクトリを準備

まずは大量のファイルが格納されたディレクトリを作るため、こんな PowerShell スクリプトを用意しました。

# _gen.ps1
# 10,000個のファイルと1,000個のディレクトリを作る
# 各ディレクトリには1,000個のファイルを作る

for ($i = 0; $i -lt 10000; $i++) {
    New-Item "$i.txt" -Force
}
for ($i = 0; $i -lt 1000; $i++) {
    New-Item "$i" -Force -ItemType Directory
    for ($j = 0; $j -lt 1000; $j++) {
        New-Item "$i\$j.txt" -Force > $null
    }
}

これを実行します。

PS> cd C:\too_many_files
PS> . _gen.ps1

これで C:\too_many_files には(生成用スクリプトも合わせて)1,010,001個のファイルと1000個のディレクトリができました。

計測用の関数

計測用関数のソースはこんな感じです。ファイル一覧を取得するコールバックを受け取り、取得にかかる時間とメモリ増加量を標準出力に出力します。

private static void Measure(Func<IEnumerable<string>> getFiles)
{
  Stopwatch sw = new Stopwatch();
  long ws = Environment.WorkingSet;

  // ファイル一覧を取得
  sw.Start();
  IEnumerable<string> files = getFiles();
  sw.Stop();
  Console.WriteLine("getFiles: {0} [seconds]", sw.Elapsed.TotalSeconds);
  Console.WriteLine("  => workingset usage: {0:N0} [bytes]", Environment.WorkingSet - ws);

  ws = Environment.WorkingSet;
  sw.Reset();
  Console.WriteLine();

  // ファイル数を取得
  sw.Start();
  int count = files.Count();
  sw.Stop();
  Console.WriteLine("Count(): {0:N0} [count], {1} [seconds]", count, sw.Elapsed.TotalSeconds);
  Console.WriteLine("  => workingset usage: {0:N0} [bytes]", Environment.WorkingSet - ws);
}

計測

Directory.GetFiles を使用

まずは Directory.GetFiles メソッドを使って計測します。

Measure(() => Directory.GetFiles(
  @"C:\too_many_files",
  "*",
  SearchOption.AllDirectories));

// getFiles: 3.7320308 [seconds]
//   => workingset usage: 87,420,928 [bytes]
//
// Count(): 1,010,001 [count], 4.77E-05 [seconds]
//   => workingset usage: 81,920 [bytes]

ファイル一覧の取得に 3.7 秒もかかっていますね。Directory.GetFiles メソッドは string 型の配列を返すので、ファイル数の取得は一瞬で終わっています。

Directory.EnumerateFiles を使用

次に Directory.EnumerateFiles メソッドを使って計測します。

Measure(() => Directory.EnumerateFiles(
  @"C:\too_many_files",
  "*",
  SearchOption.AllDirectories));

// getFiles: 0.0006461 [seconds]
//   => workingset usage: 548,864 [bytes]
//
// Count(): 1,010,001 [count], 3.3133729 [seconds]
//   => workingset usage: 4,624,384 [bytes]

こちらはファイル一覧の取得は一瞬で終わりますが、ファイル数の取得に 3.3 秒かかりました。Directory.EnumerateFiles メソッドで返されるコレクションは要素が遅延評価されるためですね。

使い分け

取得した要素をすべて利用するような場合はどちらでも大差ないと言えますが、GetFiles の方は配列で返ってきてしまうので扱いにくいケースがあるかもしれません。 要素の一部だけ使いたい場合は EnumerateFiles のほうが良さそうですので、どちらにしても EnumerateFiles の方を使うのが無難でしょうか。

おまけ : P/Invoke

以前、弊社に在籍していたベテランエンジニアが

「Directory.GetFiles は遅いからネイティブコードを呼び出すんですよ」

と言っていました。先人の教えに従ってネイティブコード呼び出しのケースも計測してみることにします。

準備

FindFirstFile 関数、FindNextFile 関数、FindClose 関数を使えるように DllImport します。*3

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern IntPtr FindFirstFileEx(
  string lpFileName,
  FINDEX_INFO_LEVELS fInfoLevelId,
  out WIN32_FIND_DATA lpFindFileData,
  FINDEX_SEARCH_OPS fSearchOp,
  IntPtr lpSearchFilter,
  int dwAdditionalFlags);

[DllImport("kernel32.dll", CharSet=CharSet.Auto)]
static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA
   lpFindFileData);

[DllImport("kernel32.dll")]
static extern bool FindClose(IntPtr hFindFile);

internal enum FINDEX_INFO_LEVELS
{
  FindExInfoStandard = 0,
  FindExInfoBasic = 1
}

internal enum FINDEX_SEARCH_OPS
{
   FindExSearchNameMatch = 0,
   FindExSearchLimitToDirectories = 1,
   FindExSearchLimitToDevices = 2
}

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
internal struct WIN32_FIND_DATA
{
  public uint dwFileAttributes;
  public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
  public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
  public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
  public uint nFileSizeHigh;
  public uint nFileSizeLow;
  public uint dwReserved0;
  public uint dwReserved1;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
  public string cFileName;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst=14)]
  public string cAlternateFileName;
}

これらを使ってファイル一覧を取得する関数を作ります。以下ページのサンプルコードを改造して、再帰的にファイル一覧を取得するようにします。

http://pinvoke.net/default.aspx/kernel32/FindFirstFile.html

private static void myGetFiles(string dir, string pattern, List<string> result)
{
  WIN32_FIND_DATA findData;
  FINDEX_INFO_LEVELS findInfoLevel = FINDEX_INFO_LEVELS.FindExInfoStandard;
  int additionalFlags = 0;
  if (Environment.OSVersion.Version.Major >= 6) {
    findInfoLevel = FINDEX_INFO_LEVELS.FindExInfoBasic;
    additionalFlags = 2; //FIND_FIRST_EX_LARGE_FETCH
  }

  IntPtr hFile = FindFirstFileEx(
    Path.Combine(dir, pattern),
    findInfoLevel,
    out findData,
    FINDEX_SEARCH_OPS.FindExSearchNameMatch,
    IntPtr.Zero,
    additionalFlags);
  int error = Marshal.GetLastWin32Error();

  if (hFile.ToInt32() != -1) {
    do {
      if ((findData.dwFileAttributes & (uint)FileAttributes.Directory) != (uint)FileAttributes.Directory) {
        //Console.WriteLine("Found file {0}", findData.cFileName);
        result.Add(findData.cFileName);
      }
      else if (findData.cFileName != "." && findData.cFileName != "..") {
        myGetFiles(Path.Combine(dir, findData.cFileName), pattern, result);
      }
    }
    while (FindNextFile(hFile, out findData));

    FindClose(hFile);
  }
}

計測

Measure(() => {
  List<string> result = new List<string>();
  myGetFiles(@"C:\too_many_files", "*", result);
  return result;
});

// getFiles: 1.4302673 [seconds]
//   => workingset usage: 38,064,128 [bytes]
//
// Count(): 1,010,001 [count], 3.14E-05 [seconds]
//   => workingset usage: 45,056 [bytes]

ファイル一覧の取得で 1.4 秒ほどかかりますが、すべての要素が評価済みであることを考えると非常に早いですね。先人の教えすごい。

ですがコードが非常に長くなってしまうので・・・これは最後の手段として秘密にしておきしましょう。

結論

というわけで、良さそうな方針としては以下のようになるかと思います。

  • .NET 4.0 以上が使えるなら Directory.EnumerateFiles を使う。
  • .NET 4.0 以上が使えないなら Directory.GetFiles で我慢する。
  • パフォーマンス要件が非常に厳しい場合は最後の手段として P/Invoke を使う。

Selenium WebDriver で Web アプリのテストを自動化中 (PhantomJS 編)

開発部の本橋です。

先日の記事 で Chrome を自動操作しました。今回は Headless な Web ブラウザである PhantomJS を使って同じことをやってみます。 Headless であることで CI に組み込みやすくなったりと、幅が広がりますね。

PhantomJS の今後が微妙

いきなりですが、Chrome のベータ版で Headless モードが利用可能であるため、 PhantomJS の開発は終了しそうな感じです。

Phantom.jsのメンテナー、プロジェクトの将来に疑問を呈し、その座を降りる

そのうち Chrome の安定版にも降りてくると思われるので、もう少ししたらそちらを使うようになるかもしれません。

ひとまず今回は使用例も多い PhantomJS を使います。

PhantomJS (とPython) をインストール

Headless なので今回は Docker イメージでいけます。PhantomJSの公式な Docker イメージはないようなので DockerHub にある良さそうなイメージを使うか、もしくは以下のような Dockerfile を使って自分でイメージをビルドしましょう。

今回も Selenium を Python3 で使いたいので、Python も一緒にインストールします。

FROM debian:jessie

ENV PAHNTOMJS_VERSION 2.1.1

WORKDIR /tmp

RUN apt-get update \
    && apt-get install -y \
           curl \
           bzip2 \
           # linux用のバイナリには fontconfig が必要
           # http://phantomjs.org/download.html#linux-64-bit
           libfontconfig \
           python3 \
           python3-pip \

    # PhantomJS をインストール
    && curl -L -O https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-${PAHNTOMJS_VERSION}-linux-x86_64.tar.bz2 \
    && tar xvf phantomjs-${PAHNTOMJS_VERSION}-linux-x86_64.tar.bz2 \
    && ln -s $(pwd)/phantomjs-${PAHNTOMJS_VERSION}-linux-x86_64/bin/phantomjs /usr/local/bin/phantomjs \

    # selenium の Python バインディングをインストール
    && pip3 install selenium \

    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

ENTRYPOINT ["python3"]
CMD ["/mnt/run.py"]

run したときに /mnt/run.py を実行するようにしています。

イメージのビルドはこんな感じです。

$ docker build . -t my/selenium-phantomjs

Selenium で操作する

WebDriver の使い方は前回と同じなので 先日の記事 を見ていただくとして、1点だけ変わるところがあります。

# run.py

# ...略

if __name__ == '__main__':
    # PhantomJS を使うようにする。
    driver = webdriver.PhantomJS()

    # ウィンドウサイズを設定。
    # レスポンシブ対応しているサイトなんかだと
    # ウィンドウ幅が狭いとリンクテキストで要素を取得できない場合がある。
    driver.set_window_size(width=1200, height=1000)

run.py をカレントディレクトリに置いたら以下のコマンドでイメージを run します。/mnt/run.py を実行するように Dockerfile を書いたので、カレントディレクトリを /mnt にマウントします。

$ docker run -v $(pwd):/mnt my/selenium-phantomjs
..
----------------------------------------------------------------------
Ran 2 tests in 8.846s

OK

これで Headless に Selenium を使えるようになりました!

冒頭に書いたように Chrome の Headless モードが安定版に降りてきたらそちらも試したいですね。

なぜクラウド化するのか?

開発部の醍醐です。

春なので・・・新入社員の方々も会社に慣れてきたり(5月病になりそうだったり)している事と思われます。
そんな方々、もしくは私のようなベテランの域に入った方々を含め、あらためて

「システムをクラウド化する意味」

について整理してみたいと思います。

というのも、最近のソフトウェアシステム開発案件では「クラウド」が基本の前提条件となることも多くなってきているように思うからです。
私のような古参エンジニアの場合、オンプレがすべてであった時代から、現在のクラウド化の時代までを経験しているので、そのインフラ環境の変遷については身を持って経験しています。
しかし、特にここ2~3年の間にこの業界にジョインしたエンジニアの方々は、「こういう理由によりクラウドでシステムを作ろう」というフェーズをスキップし、「前提としてのクラウド」というケースが多くなっているのではないでしょうか。

1.自社内オンプレ運用

今でもこのケースは多々見られますし、現役の構成ですが、昔々はこれがすべてでした。
自社内にサーバーを配置し、ハードウェア構成を組み、ネットワーク構成を組み、OS環境を整え、アプリケーションをデプロイする、というケースです。
そしてこの場合は勿論、これらのインフラを構築する、また、運用し続けるインフラエンジニアが張り付くことになります。

2.オンプレ運用からデータセンター運用へ

自社内オンプレ運用からデータセンター運用への移行、が次の段階としてあります。
自社内に配置していたサーバーをデータセンター(N○○さん)などに移行することになります。
ケースはいろいろあると思いますが、代表的には「サーバー機器の設置場所を自社からデータセンターに移行する」という事になります。
これにより以下のような作業から解放されることになります。

  • 空調や電源などの低レベルのインフラ管理からの解放
  • 死活管理等の基本インフラ運用管理からの解放

3.IaaSによるクラウド化

IaaS = Infrastructure as a sreviceです。
Azureであれば Virtual Machine 、Amazon AWSであれば EC2 がこれに該当します。
クラウドのIaaS環境にシステムをパブリッシュして運用することになります。
IaaS環境では、特定のスペックの物理ハードウェア機器を持ちません。
AzureなりAWSなりがバックエンドで運用しているサーバー機器の上に仮想環境を構築することになります。
IaaSはハードウェア機器の仮想化(抽象化)である為、利用者は提供される仮想環境上に Windows Server のようなOS環境を構築することになります。
IISを構成し、必要であればSQL Serverを構成します。

オンプレやデータセンター運用との大きな相違点は「ハードウェア機器を自ら購入(レンタル)しなくてよい」という事。更にCPU・メモリ・ストレージは必要に応じてスケールアップ・スケールアウト、逆にスケールダウン・スケールインすることができるという事です。

IaaS化で得られたメリットは以下のようになります。

  • ハードウェアインフラ管理の放棄
  • 必要に応じて柔軟にハードウェア性能を変動させることができる
  • 使った分だけのコスト支払い

既存システムのクラウド化が容易

既存システム(もしくは既存アーキテクチャ設計のシステム)をクラウド化する場合、この後で説明するPaaSと比べ、IaaSへの移行は比較的容易です。
それはハードウェアの抽象化を行ったのみで、OS環境は従来のWindows Serverのままだからです。
搔い摘んだ細かな対比でいうと、企業内ネットワークのサブネットは Azureの VNet になり、Load Balancerは Azure Load Balancer や Traffic Managerとなり、基本的にはオンプレ時と同じような役割を果たすサービスがAzureでも提供されています。
特別なハードウェアの制御を行う様なシステムでない限り、アプリケーションから見た場合「ファイルシステム」も「ネットワーク」も「データベース」もオンプレ環境と何ら変わることがありません。
つまり、既存システム(もしくは既存アーキテクチャ設計のシステム)は、そのままIaaSクラウド化することが可能です。

OS環境の運用からは逃れられない

IaaSではハードウェアインフラは仮想化されますが、OS環境は利用者の管理下にあります。
つまり セキュリティパッチ等の Windowsアップデート は、利用者側で管理する必要があります。
サービス停止のスケジューリング、停止が許されないシステムであれば 複数台の冗長化を行った上でのアップデートスケジュール等の運用管理は利用者側で行う必要があります。

4.PaaSによるクラウド化

この数年の間に主にIaaSによるクラウド化が進んだように思います。
それは前述のように「既存システム(もしくは既存アーキテクチャ設計のシステム)を乗せ換えるのが容易」であることが影響しているように思います。
ドラスティックなソフトウェア アーキテクチャの置き換えではなく、ソフトウェア アーキテクチャは変更せず、緩やかにインフラに近い部分の抽象化、またそれによるコストカットをIaaS化により実現しました。
次のフェーズとして PaaS = Platform as a service という言葉が最近多く聞かれるようになっています。
単純に言うと「PaaSは”OS”という物自体をも抽象化」してしまいます。
Azureにおける PaaS は以下のようなものがあります。

  • App Service
  • SQL Database
  • DocumentDB
  • Azure Functions
  • 等々…

App Service

App Serviceは、ASP.NETアプリケーションをそのまま乗せて動作させるイメージになります。
Windows ServerとかIISとかいう概念はありません(バックエンドではWindowsがホストしていますが・・・またContainerとしDockerでホストすることも可能です)。

SQL Database

SQL Databaseは、SQL ServerのPaaS版です。
こちらも、利用者側からは Windows Server という概念は存在せず、「ただ、SQL Databaseというデータベースがサービスとして利用可能」というものになります。
オンプレのSQL Serverに対して、PaaSとして提供されているSQL Databaseのメリットの1つとして分かりやすい点には「レプリケーション設定の容易性」などがあげられます。
オンプレのSQL Serverであれば「2台の構成を組み、各SQL Serverに対してレプリケーション構成を組む」というインフラエンジニア的作業が必要になります。これに対して PaaS の SQL Database であれば、ポータル上からの比較的簡易な作業によりレプリケーション構成を組んでしまうことが可能です。

DocumentDB

DocumentDBは、NoSQLで無制限にスケール可能な、まさにクラウド時代のデータベースになります。
ポータル画面上から世界地図をクリックするだけで、国を超えたGeoレプリケーションも行われてしまいます。

Azure Functions

Azure Functionsは、いわゆるサーバーレスアーキテクチャの技術です。こちらも勿論、OSという概念は利用者からは見えません。
こちらもエラスティックにスケールします。

OSの抽象化

OSが抽象化されたことにより「OSに対するセキュリティパッチ適用のための再起動停止」という概念がなくなっています。
Azure内の本当のバックエンドではWindows Serverというインフラが存在し、そこに対するパッチ適用や再起動が行われていますが、その上で動作するPaaSサービスには特定マシン(OS)の再起動が直接的に影響しない仕組みが構成されています。

PaaS化のメリット

その詳細に触れていない項目も含まれますが、PaaS化のメリットは以下のようなものがあります。

  • OSの運用管理からの解放
  • サービス運用を継続したまま、エラスティックに高速でスケール変更が可能なインフラを利用可能
  • インフラ管理ではなくシステムのドメイン領域に注力可能

そのままではPaaS化できない

上記に示したような各種PaaS技術から分かるに、アーキテクチャの変化が伴う為、「このシステムは明日からPaaS化しよう!」とはなかなかいきません。
ただし、ASP.NETアプリケーション(特定のファイルシステムやCOM、またはWindowsサービスとの連携等を行っていない)をApp Serviceで動かすという事であれば、比較的スムーズに移行可能なものもあるでしょう。
また SQL ServerベースのシステムのDBを SQL Database にする、という事もそれほど大きな変更なく移行可能でしょう。とはいえ、通貨単位(国設定)等で不具合が出る箇所、調整を要する箇所が出てくるかもしれませんので動作検証は必要となります。
新規の、完全に自由なアーキテクチャを選択可能な状況でなければ、IaaSで動作しているシステムのWindowsサービス部分をFunctionsに移行する・部分的にApp Service化する等の順次移行、並行移行等、計画的な移行が必要になるでしょう。

(おまけ)可用性(耐障害性)について

Azureは2017年3月に3度の大規模障害を起こしたため、可用性(耐障害性)で不安を抱く状況が生まれている部分があるかもしれません。
一般によく言われていることですが、「基本的に障害は起こる」ことを前提としておく必要があります。
ハードウェアは壊れるし、人為的ミスも発生しうるという前提です。
IaaSではリージョン内での可用性セットを組んだ冗長化構成、または複数リージョンにまたがる冗長化設定を行う。
Azure Storageに関しても、リージョン内3レプリケーションの LRS から、別リージョンへの3レプリケーションの GRS等の選択検討も必要です。
SQL DatabaseやDocumentDBでも同様のレプリケーション設定が容易に可能です。
ただし、コスト上の問題が発生することもあります。常に複数のサービスをアクティブ状態で起動しておけるほどコストをかけられないケースもあります。
この場合は、障害発生時の素早い復旧のためのデプロイインフラを用意していくという事が1つの対応策です。
Infrastructure as codeによる自動デプロイであったり、App Service Linux ContainerであればDockerイメージで即時デプロイを可能にしておくことであったり、AzureではなくAWSへのデプロイも可能にしておく事も1つの手段です。
フェイルオーバーを正しく行うための手順等も、Azureでのノウハウや知識が必要になる部分もある為、やはり、いくらかの学習と経験が必要なところではあるでしょう。

まとめ

IaaS / PaaS含めクラウド化は、従来のインフラ管理の自動化(抽象化)を実現することができます。
オンプレ導入とクラウドのコスト比較をした場合、必ずしもクラウドが特別に安い状態にはならないように見えるケースがあります。ただし、自前では用意不可能な程のエラスティックなインフラをAzureやAWSは提供し、また、(ハードウェアの定期的なメンテナンス等の)物理的・人的運用コストの低減も実現されることも含めてコスト検討する必要があるように思います。
「データを社外に置く事はできない」等の、企業方針からクラウド化がNGとなるシステムもまだまだ多く存在します。しかし、クラウドのメリットは大きく、そして IaaS さらには PaaS が今後の主流になる事と考えます。