くんすとの備忘録

プログラミングや環境設定の覚え書き。

開発環境をセットアップするAnsible Playbookを作成したよ

前回の続き。

VPS上で生活しよう!ということなので、開発用の各種パッケージをインストールしたり設定ファイルをセットアップしたりするPlaybookを作成しました。

今回は量が多く何度もやり直しをしたのと、今後もちょこちょこメンテして再実行をしたいので、冪等性を保つような作りにしています。

github.com

軽い解説

Roleについて

今回は Role の機能を使ってみました。 Roleを使ってセットアップ内容を以下のように分割しました。

  • common - 共通で使うパッケージのインストール
  • dotfiles - dotfilesのインストール
  • vim - Vimプラグインマネージャ(Dein)のインストール
  • linuxbrew - linuxbrewのインストールと、brewを使ったソフトウェアのインストール
  • nodejs - nodebrewのインストール
  • rbenv - rbenvのインストール
  • python - venvをPythonの環境設定

……なんというか、ほぼ docker-desktop でやっている内容の移植です。

処理の共通化

また、セットアップ処理には同じような作業が多いため、処理を共通化しました。 lib ディレクトリ以下にタスクをまとめたモジュールを作成し、include して使うようにしてみました。

これらのモジュールを使うことで、 tasks には処理の順番を、 vars には処理の中身やパッケージのリストを宣言するという作りで Playbook を作成できるようになりした。 ControllerとModelに分割したようなイメージです。

  • apt-install.yml - 変数 packages に宣言されているパッケージをaptでインストールする
  • execute-commands.yml - 変数 commands に宣言されているコマンドを、ログインシェルで実行する
  • get-url.yml - 変数 `remote_urls に宣言されている url からファイルをダウンロードする
  • git-clone.yml - 変数 remote_repos に宣言されているリポジトリを git clone する
  • make-directories.yml - 変数 dirs に宣言されているディレクトリを作成する
  • set-environments.yml - 変数 envs に宣言されている環境変数を ~/.bash_profile に追記する

ちなみにこのやり方はAnsible Galaxyをちょっと真似したような感じです。

例えばわかりやすい例だと、 rbenv の場合、

  • vars - roles/rbenv/vars/main.yml
dir: "{{ cache }}/rbenv"

remote_repos:
  - repo: https://github.com/sstephenson/rbenv.git
    dest: "{{ dir }}"
  - repo: https://github.com/sstephenson/ruby-build.git
    dest: "{{ dir }}/plugins/ruby-build"
packages:
  - libssl-dev
  - libreadline-dev
  - zlib1g-dev
commands:
  - "eval '$(rbenv init -)'"
  - rbenv install -v 2.4.1
  - rbenv rehash
  - rbenv global 2.4.1
envs:
  - "# RBENV"
  - "export RBENV_ROOT={{ dir }}"
  - export PATH=$RBENV_ROOT/bin:$PATH
  - 'eval "$(rbenv init -)"'
  • tasks - roles/rbenv/tasks/
- name: check rbenv
  shell: "bash -lc 'which rbenv'"
  register: exists
  failed_when: false

- block:
  - include: lib/apt-install.yml
  - include: lib/git-clone.yml
  - include: lib/set-environments.yml
  - include: lib/execute-commands.yml
  when: exists.rc != 0

と書けます。

軽いまとめ

こんな感じで環境を用意できたので、しばらくはブラウザの上で生きていきます。

Cloud9をインストールするAnsible Playbookを作成したよ

前回の続き。

なぜVPSをセットアップしているかというと、Cloud9の環境が欲しかったからなのです。 AWSで使うと高額なので、Conohaで動かそうということなのです。

というわけで、Cloud9のセットアップ手順を Playbook にまとめました。 今回のは短いし何度も使うようなものでもないので、冪等性はないです。

github.com

userport は適当に変えて〼

参考URL

Ansible入門しました

今まで食わず嫌いをしてしまっていたAnsibleに、今更ながら1入門しました。

使う前に抱いていた印象と実際に使ってみた感想を並べてみて、最後に書いてみたPlaybookを載せます。 使う前はあまりいいイメージではなかったのですが、使ってみるとなかなか便利では……という使用感でした。

使う前に抱いていた印象

箇条書きで。

  • 小規模利用にはオーバースペックなのでは?
  • リモートに処理を流し込むだけなら、シェルスクリプトを標準入力経由でsshに流せばいいだけでは……
  • Playbookがシェルスクリプトを難しくしただけのラッパーに見えた
    • 「実際のコマンドは~だから、Playbookにはこう書いて~」みたいなことを考えるくらいなら直接シェルスクリプトを書いた方がシンプルでわかりやすいのでは。

実際に使ってみた感想

今回は「新規作成したVPSに初期セットアップをする」という用途でPlaybookを作成し、実行しました。 基本的に、シェルスクリプトで同じことをする場合との比較だと思ってください。

使用感を箇条書きで。

  • 小規模向きかと言うと微妙だけど普通に使える
    • 対象のホスト名を書いたファイルを用意しなくてはいけないのは若干面倒
      • とはいえ1ファイル作るだけ
    • localhost向けに実行することもできる
  • 再実行しやすい
    • 同じ設定を上書きしない作りになっていたり、回避する手段がちゃんと用意されていたりする2
      • 例えば lineinfile という、ファイルに行を追加する命令(?)を再実行した場合、内容が既に追加されていれば追記は行われない
      • "ifだらけ"現象になりにくいため、見通しがよくなる
  • aptgitなどよく使う命令(?)が用意されており、書き方を自然に統一できる
    • shellcommandは極力使わないほうがよさそう
      • ほぼ使わずにできるようになっているし、必要になったら自分の設計が怪しいかもというサインになりそう

実際に書いたPlaybook

VPS(Conoha)に作成したUbuntuへ、毎度やっている初期セットアップ作業をするPlaybookを書きました。

※ユーザー名とsshd_portは適当に変えてます

※セットアップ直後なのでrootで実行します

- hosts: all
  vars:
    - username: user
    - sshd_port: 12345
  vars_prompt:
    - name: password
      prompt: "Input user password"
      encrypt: sha512_crypt
      private: yes
      confirm: yes

  tasks:
    - name: Add user
      user:
        name: "{{ username }}"
        password: "{{ password }}"
        groups: sudo
        shell: /bin/bash

    - name: Add authorized_keys
      authorized_key:
        user: "{{ username }}"
        key: "{{ lookup('file', '.ssh/authorized_keys') }}"

    - name: SSHD settings
      lineinfile:
        dest: /etc/ssh/sshd_config
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
      with_items:
        - regexp: "^PasswordAuthentication"
          line: "PasswordAuthentication no"
        - regexp: "^ChallengeResponseAuthentication"
          line: "ChallengeResponseAuthentication no"
        - regexp: "^Port"
          line: "Port {{ sshd_port }}"

    - name: Restart sshd
      service:
        name: sshd
        state: restarted

    - name: Configure ufw default rules
      ufw:
        direction: "{{ item.direction }}"
        policy: "{{ item.policy }}"
      with_items:
        - direction: "incoming"
          policy: deny
        - direction: "outgoing"
          policy: allow

    - name: Configure ufw rules
      ufw:
        rule: "{{ item.rule }}"
        port: "{{ item.port }}"
        proto: tcp
      with_items:
        - rule: "limit"
          port: "{{ sshd_port }}"

    - name: Enable ufw logging
      ufw:
        logging: on

    - name: Enable ufw
      ufw:
        state: enabled

    - name: Restart ufw
      service:
        name: ufw
        state: restarted

作ってから5回くらい使ってますが便利ですね。

まとめ的な

以前「メンテナンスのことを考えるとシェルスクリプトよりAnsibleの方がいい」という言説を見かけたんですが、確かにその通りだなと感じました。

サーバの初期セットアップをする用途で考えると、 型のあるシェルスクリプト と言えるかもしれません。


食わず嫌いはいけませんね!


  1. Ansibleがリリースされたのは2012年、今年は2018年。

  2. “ちゃんと用意されている"というのは、"自分でやり方を考えたり、工夫したりしなくてもいい"というニュアンスです。

~/.gitconfig を切り替えるCLIツールを作りました

github.com

解決したかったこと

gitconfigの切り替えが面倒だった

昼休みや空き時間にちょっとしたツールを作るようなことがあり、今までは毎回 ~/.gitconfig を書き換えたり、対象のリポジトリだけ .git/config を書き換えたりする運用をしていました。 が、いくつか問題がありました。

  • うっかり忘れて、コミットユーザー誤爆する
  • 書き換えが面倒くさい
  • ~/.gitconrfig の方はバージョン管理しているので、そもそもあまり書き換えたくない

こういった問題があったので、 ~/.gitconfig をシンボリックリンクにして、リンク先を切り替えるツールを作成しました。

設計

ざっくりこのようなつくりにしました。

  • ~/.gitenv/ 以下に環境の名前ごとのサブディレクトリを作成し、その中に .gitconfig を作成
  • ~/.gitconfig へシンボリックリンクを張る
  • リンク先をコマンドラインで切り替える

使い方の例

※既存の ~/.gitconfig は退避しておいてください

# ~/.gitconfig を Alice 用の設定にする
$ gitenv -c alice

# Alice のユーザー設定をする
$ git config --global user.name "Alice"
$ git config --global user.email alice@example.com
$ cat ~/.gitconfig
[user]
    name = Alice
    email = alice@example.com

# ~/.gitconfig を Bob 用の設定に切り替える
$ gitenv -c bob

# Bob のユーザー設定をする
$ git config --global user.name "Bob"
$ git config --global user.email bob@example.com
$ cat ~/.gitconfig
[user]
    name = Bob
    email = bob@example.com

$ ~/.gitconfig を Alice 用の設定に戻す
$ gitenv -c alice
$ cat ~/.gitconfig
[user]
    name = Alice
    email = alice@example.com

# 現在の環境の名前を表示する
$ gitenv
alice (Alice)

実運用

実際に使っている ~/.gitconfig の内容

こんな感じで Include だけを書いてます。

[Include]
    path = ~/.gitconfig.all
    path = ~/.gitaliases
    path = ~/.gitconfig.kunst1080  # この行を環境ごとに変えて〼

プロンプト

普段は zsh を使っているのですが、環境変数 PROMPT$(gitenv) を埋め込むようにしました。 これで今の設定がどれになっているのかわかります。

プロンプト:

hoge@MacBook-Pro.local ~  env2 (kunst1080)
$ 

まとめ

これで誤爆を防げるよ。やったね!

古いfind(1)と新しいfind(1)

ソース読むときのためのメモ。

https://www.gnu.org/software/findutils/manual/html_mono/find.html#fts

The findutils source distribution contains two different implementations of find. The older implementation descends the file system recursively, while the newer one uses fts. Both are normally installed.

If the option --without-fts was passed to configure, the recursive implementation is installed as find and the fts-based implementation is installed as ftsfind. Otherwise, the fts-based implementation is installed as find and the recursive implementation is installed as oldfind.

「findutilsソースディストリビューションには、findの2つの異なる実装が含まれています。古い実装はファイルシステムを再帰的に降下させ、新しいものはftsを使用します。どちらも通常インストールされています」

「configureに--without-ftsオプションが渡された場合、再帰的実装はfindとしてインストールされ、fts-based実装はftsfindとしてインストールされます。それ以外の場合、ftsベースの実装はfindとしてインストールされ、再帰実装はoldfindとしてインストールされます」

v4.6.0のソース

root/find/Makefile.amより

http://git.savannah.gnu.org/cgit/findutils.git/tree/find/Makefile.am?h=v4.6.0

# We always build two versions of find, one with fts (called "find"),
# one without (called "oldfind").  The oldfind binary is no longer
# installed.
bin_PROGRAMS     = find
check_PROGRAMS   = oldfind
find_SOURCES     = ftsfind.c
oldfind_SOURCES  = oldfind.c
man_MANS         = find.1

root/find/ftsfind.c より

http://git.savannah.gnu.org/cgit/findutils.git/tree/find/ftsfind.c?h=v4.6.0

L567

     while ( (errno=0, ent=fts_read (p)) != NULL )

root/find/oldfind.c より

http://git.savannah.gnu.org/cgit/findutils.git/tree/find/oldfind.c?h=v4.6.0

L1425

   dp = readdir (dirp);

コミットログ確認

2005-11-21 05:42:27 +0000

Findutils 4.3.x defaults to using the the FTS implementation of find.

http://git.savannah.gnu.org/cgit/findutils.git/commit/?id=f0759ab8db9cab16699fba45fa6117ef06620194

【謎】本当にあったfindコマンドの怖い話【質問編】

※質問受付は終了しました。(3/22)

先にまとめ

  • リネームではinode番号は変わらないけどエントリの位置が変わることがある。
    • これが一番知りたかった情報。でも文章では理解したけど、検証コードはどう書けばいいかわからん…
  • readdirはアトミックじゃない。読み込み中にエントリ情報が変われば次の読み込みに影響する。
    • man にも「readdir()は非スレッドセーフです」って書いてある。
  • fts_readは実行時にreaddirの結果を10万件(ずつ?)キャッシュしていて、途中(たぶん10万件)まではエントリの変更の影響を受けないっぽい。途中からreaddirと同じことが起こる。
    • ソースコードの斜め読みと挙動を観察した限りそんな感じっぽい。厳密に裏取りしたいけど疲れた。
  • findコマンドは readdir ではなくそのラッパーの fts_read を使っているので、 fts_read と同じことが起こる。はず。

※3/25追記

リネームでエントリ位置が変わる現象について、ファイルシステムごとにどんな挙動を示すのか比較検証した記事をいただきました。ありがとうございます。

手元で簡単に検証できるような準備もされており、とてもわかりやすかったです。私も手を動かして追確認しました。

hiboma.hatenadiary.jp

本編

昨日の記事 www.kunst1080.net

と今日の記事 www.kunst1080.net

のブコメを見て、詳しい人がたくさんいらっしゃるようだったので、せっかくなので質問コーナーやらせてください><

全然詳しくないので教えてやってください><

質問①

findはinode順に出力をする(予想)が、mvは同一ディスク内ではinode番号は変わらないと思っています。 なので、mvしたところでエントリには再度出てくるのは不思議…って思ってるんですが、どの辺の理解がおかしいですか?

もしかして: fts_readはinode順じゃなくてファイルシステム依存? だとしたら何順?

A1-1: id:xbs2r さんからのブコメより

これを読めばわかる、っていうことなのでちゃんと読みます・・・(スミマセン

readdir() nonatomicity (Theodore Ts'o)

ざっくり読んだ感じ、記事中の質問は

readdir()が、別のプロセスからrename()されたファイルを拾ってくれない。リネーム前の名前もリネーム後の名前も降ってこない

で、こちらの例では find-exec mv は別のプロセスなのでシチュエーションは同じ。

記事中の回答は

linked listで実装されているディレクトリでエントリが完全に密集しているとき(?)、その中のファイルをリネームするとディレクトリエントリの最後に追加される

readdir() がエントリをロックしてしまうと、readdir() を呼び出しまくるdos攻撃ができてしまうので、スレッドセーフにはあえてしていない。

ということなので、状態に寄っては readdir で同じファイルが複数回読まれるケースがある、ってことですね。。。

A1-2: あー (id:uva) さんからのコメント

ありがとうございます!

質問①について readdirが返すエントリの順序は不定のようですね

The order in which filenames are read by successive calls to readdir() depends on the filesystem implementation; it is unlikely that the names will be sorted in any fashion. http://man7.org/linux/man-pages/man3/readdir.3.html

SOに似た質問ありました https://stackoverflow.com/questions/8977441/does-readdir-guarantee-an-order

inode順じゃなくて不定なんですね。(ファイルシステムに依存。SOにはディスクに格納されてる順、とかっていうのもありますね)

これならもし mv コマンドでinode番号が変われば、二重読み込みは発生しそうです。

inodeは関係ないですね。

A1-3: 自分: readdirの動きを検証

A1-1を確認するために、readdir(3) でファイルを読みつつ system(3) で mv コマンドを実行、mv前後の inode を確認するCのコードを雑に書きました。1

gist.github.com

ファイルを1500ファイル読み込んだところ、inode番号は変わらず、でも同じファイルが複数回読まれまたというのが見えました。

$ seq 1 1500 | xargs touch
$ ~/a.out | tee ~/a.txt
()
1635: 1 (393237) -> 1a (393237)
1636: 316 (393553) -> 316a (393553)
1637: 1163a (394400) -> 1163aa (394400)
1638: 1002 (394239) -> 1002a (394239)
$ wc -l ~/a.txt
1638 /home/hoge/a.txt

同じファイルが複数回読み込みされています。 inodeは関係ないですね。

A1-4: 自分: fts_read の動きを検証

readdirについては確認できましたが、findコマンドで実際に使われているのは readdir ではなく fts_read です。 なので、そちらについても確認していきます。

readdirの検証コードをftsで書き直します。

gist.github.com

まずは10万ファイルの書き換えを実行してみます。

$ seq 1 100000 | xargs touch
$ ~/a.out.fts| tee ~/fts.100000.txt
()
99998: 98499 (491739) -> 98499a (491739)
99999: 56129 (449369) -> 56129a (449369)
100000: 52271 (445511) -> 52271a (445511)
$ wc -l ~/fts.100000.txt
100000 /root/fts.100000.txt

二重読みしてませんね。

次に20万ファイルで試してみます。

$ seq 1 200000 | xargs touch
$ ~/a.out.fts| tee ~/fts.200000.txt
()
199998: 56129 (449369) -> 56129a (449369)
199999: 52271 (445511) -> 52271a (445511)
200000: 171787 (565029) -> 171787a (565029)
$ wc -l  ~/fts.200000.txt
200000 /root/fts.200000.txt

これでも二重読みしませんね。(オイオイまじかよ……)

……fts.cの実装をソースからちゃんと理解してるわけでは全然なくって挙動を見てるだけなんだけど、やっぱり fts_read は途中までは保証されてるんじゃないかなぁ。


# 質問② 名前を変えただけでもう一度一覧に出てくるなら、対象となるファイル数を異常に増やしたり、処理中にsleepを噛ませたら無限にfindできると思うんですができるんでしょうか?

findの1件ずつsleepを挟むのは試してみたけど無理でした。(最初は無限にfindする企画でした) もしかして試してみたことのある方っています?


質問③

ファイル数が少ないときでもfindからmvしたときに二重読みしないのはたまたまなんでしょうか?

【解決編】で fts_read のタイミングで 最大100000件 までエントリをキャッシュしてるような風に見えたので、それ以下のエントリ数なら大丈夫だと思うんですが……

「不定だからやるな」って書いてあるのは理解したんですが、実際はどういう実装になっているんでしょうか。

A3-1: (id:siglite) さんからのブコメ

ありがとうございます。

"If a filename is renamed during a readdir() session of a directory, it is undefined where that neither, either, or both of the new and old filenames will be returned." / 質問3: exec前に最大10万件先読みするから(最初の10万件は)execの影響がない…という感じ?

A1-1のリンクからの抜粋ですね。reddirのセッション中のディレクトリ内でリネームをすると、新しいファイル・古いファイル・両方のどれが読まれるか不定っていうことですね。 んで、findコマンド側で10万件キャッシュしてるから(最初の10万件は)execの影響がない…という。

前半は私がちゃんと理解できていなかったところで、後半は予想と同じですよね。 これで理解が合っていてほしいです。

その他いろいろ見ていて面白かったこと

FreeBSD の fts.c

freebsd/fts.c at 82974662ad9f9ece5f8374d2c898e83bd03aece9 · freebsd/freebsd · GitHub

FTS_MAX_READDIR_ENTRIES などというものはない。

gnulib の fts.c のコミットログ

fts: do not exhaust memory when processing million-entry directories · coreutils/gnulib@47cb657 · GitHub

FTS_MAX_READDIR_ENTRIES は7年前(2011/8/17)に追加されてる。

最後に

もういい加減飽きてきたのでここまで。濃い3日間だった。

無限 find 出来たよ! って人がいればあとで教えて下さい。


  1. C言語を書いたのは人生で10回目くらいなのでひどいコードなのは見逃して頂きたく…

【謎】本当にあったfindコマンドの怖い話【検証編】

3/21 22時頃: 質問編へのリンクを撤去し、タイトルを変更しました。(元のタイトルは「【謎】本当にあったfindコマンドの怖い話【解決編】」)


昨日のエントリについて、実験にしてはケースが雑だったので再検証していきます。

www.kunst1080.net

ちなみにモチベーションは「問題を回避したい」ではなく「この現象の原因を知りたい」です1。 よろしくお願いします。

現象からしてfindコマンドが処理中に書き換えられたファイルを読み込んでいるのは明白です。

少しずつ仮設を立て見ていきましょう。 まぁまぁお付き合いください。

検証ケース

  • ケース1: 10万ファイルで実行
  • ケース2: パイプを使わずfindコマンド一発にし、100万ファイルで実行
  • ケース3: ケース2を10万ファイルで実行
  • ケース4: ケース2を15万ファイルで実行

ケース1: 10万ファイルで実行 → 発現しない

昨日の記事は100万ファイルで検証していました。書いてはいなかったんですが、実は10万ファイル程度であれば謎現象が発現しないことは確認済みでした。 とりあえずその様を御覧ください。

$  seq 100000 | xargs touch
$ find . | sed 's;./;;g' | fgrep -v . | awk '{print $1, "a"$1}' | xargs -n2 -I@ bash -c 'echo @;mv @' | nl

(中略)

99996    99996 a99996
99997    99997 a99997
99998    99998 a99998
99999    99999 a99999
100000    100000 a100000

パイプで繋いでいるからといって同じファイルを2回も読んだりしていません。直感的ですね。

ケース2: パイプを使わずfindコマンド一発にし、100万ファイルで実行 → 発現する

「間のパイプが怪しい」、オーケー、気持ちはわかります。それならfindコマンド一発にしてみましょう。

$ seq 1000000 | xargs touch
$ find . -type f -exec mv -v {} {}a \; | tee ~/case2.txt

(中略)

'./653325' -> './653325a'
'./653328a' -> './653328aa'
'./653345a' -> './653345aa'
'./653373aa' -> './653373aaa'
'./653392a' -> './653392aa'
'./653395aa' -> './653395aaa'
'./653416aa' -> './653416aaa'
'./653527aaa' -> './653527aaaa'

$ wc -l ~/case2.txt
1632595 /home/hoge/case2.txt

パイプを外しても二重読みは発生するようです。 パイプ関係なかったっすね。2

ケース3: ケース2を10万ファイルで実行 → 発現しない

ケース1と同様に、パイプなし版でも10万ファイルでの挙動を確認してみます。

$ seq 100000 | xargs touch
$ find . -type f -exec mv -v {} {}a \; | tee ~/case3-100000.txt
()
$ wc -l ~/case3-100000.txt
100000 /home/hoge/case3-100000.txt

10万ファイルではこっちも大丈夫みたいです。

ケース4: ケース2を15万ファイルで実行 → 発現する

さてここで、こんな有益な情報が……

gnulib の fts.c のソースを確認したところ、確かに定数の宣言がありました。

/* If possible (see max_entries, below), read no more than this many directory
   entries at a time.  Without this limit (i.e., when using non-NULL
   fts_compar), processing a directory with 4,000,000 entries requires ~1GiB
   of memory, and handling 64M entries would require 16GiB of memory.  */
#ifndef FTS_MAX_READDIR_ENTRIES
# define FTS_MAX_READDIR_ENTRIES 100000
#endif


/* If there are more than this many entries in a directory,
   and the conditions mentioned below are satisfied, then sort
   the entries on inode number before any further processing.  */
#ifndef FTS_INODE_SORT_DIR_ENTRIES_THRESHOLD
# define FTS_INODE_SORT_DIR_ENTRIES_THRESHOLD 10000
#endif

https://github.com/coreutils/gnulib/blob/66ae2f356a594c83ad690d0dfadbc9c9a4cec5f4/lib/fts.c#L135-L148

これはファイルを読み込むpublicな関数 fts_read で使われている、ファイルを読み込む内部関数の fts_build の中で、一度に読み込むファイル数を決めいている定数(だと思います多分。私はマジでC言語ワカリマセン)です。 1回に 100000 エントリ読むっぽい感じでしょうか。大事を取って 150000 ファイルで処理して、二重読みが発生するかを検証してみます。

$ seq 150000 | xargs touch
$ find . -type f -exec mv -v {} {}a \; | tee ~/case4-150000.txt
()
$ wc -l ~/case4-150000.txt
182581 /home/hoge/case4-150000.txt

どうやら 100000 と 150000 の間くらいから二重読みが発生し出すみたいです。 1回試すのに1~2時間くらいかかる3ので、これ以上探すのはやめておきます。

findコマンドを改造して検証

ここまでの検証によって、どうやら FTS_MAX_READDIR_ENTRIES の辺りが怪しいっぽいということがわかりました。 本当にそうなのでしょうか? findコマンドを改造して確認してみましょう。

findコマンドのソースは findutils というプロジェクトからダウンロードできます。 以前、個人的にfindutilsからソースをコピーしてビルドするdockerコンテナを作成していたので、それを利用します。

github.com

cloneし、FTS_MAX_READDIR_ENTRIES の値を10に書き換えてビルドします。

$ git clone --recursive https://github.com/kunst1080/docker-build-findutils
$ cd docker-build-findutils

# ソースの書き換え
$ sed -i".bak" 's/define FTS_MAX_READDIR_ENTRIES 100000/define FTS_MAX_READDIR_ENTRIES 10/g' findutils/gnulib/lib/fts.c

# ビルド
$ ./docker-build.sh
$ ./bootstrap.sh
$ ./configure.sh
$ ./make.sh
$ cp findutils/find/find ~/find10

はいできました。実行してみましょう。

1000ファイル → 発現しない

$ seq 1 1000 | xargs touch
$ ~/find10 . -type f -exec mv -v {} {}a \; | tee ~/new-find-1000.txt
$ wc -l ~/new-find-1000.txt 
1000 /home/hoge/new-find-1000.txt

1400ファイル → 発現する

$ seq 1 1400 | xargs touch
$ ~/find10 . -type f -exec mv -v {} {}a \; | tee ~/new-find-1400.txt
$ wc -l ~/new-find-1400.txt 
1432 /home/hoge/new-find-1400.txt

※1200、1300は発現するときとしないときがありました。

念の為、素の状態でビルドして1万ファイルで試してみる

$ git clone --recursive https://github.com/kunst1080/docker-build-findutils
$ cd docker-build-findutils

# ビルド
$ ./docker-build.sh
$ ./bootstrap.sh
$ ./configure.sh
$ ./make.sh
$ cp findutils/find/find ~/find-org

10万ファイルで実行

$ seq 1 100000 | xargs touch
$ ~/find-org . -type f -exec mv -v {} {}a \; | tee ~/new-find-org-100000.txt
$ wc -l ~/new-find-org-10000.txt
100000 /home/hoge/new-find-org-100000.txt

こっちは1万ファイルあっても大丈夫ですね。

まとめ

findコマンドについて、以下のことがわかりました。

  • find コマンドは、コンパイル時に使用した fts(3) に定義されている FTS_MAX_READDIR_ENTRIES の数だけエントリをキャッシュするっぽい。
  • FTS_MAX_READDIR_ENTRIES のデフォルト値は 100000 で、これ以下のファイル数であれば二重読み込みは発生しなさそう。
  • FTS_MAX_READDIR_ENTRIES 以上の数のファイルを対象に find すると、処理中に変更を加えた場合は影響が発生することがありそう。厳密な閾値は不定っぽい。

情報をご提供いただいいたり、いっしょに検証してくださったみなさまには感謝です。 ありがとうございました。

次でラスト

www.kunst1080.net


  1. 回避策なんてくらでもあるので面白みはないでそ。

  2. 前回の記事のブコメでパイプガーって言っていた人たちはちゃんと検証してなかったんですね。まぁブコメ書くのにわざわざ検証なんてしないですよね。

  3. 実際は 500000、300000、200000 も試したのでもうおなかいっぱい。件数はいずれも概ね1.5倍前後になりました。

広告