くんすとの備忘録

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

AnsibleのDocker Connection Pluginを使って空ファイルをコピーするとジョブがたまにハングする問題

ためした環境

  • 実行元
    • macOS High Sierra 10.13.6
    • Ansible 2.4.3.0
    • Python 2.7.10
  • 対象サーバー
    • Docker for Mac上のコンテナ
      • Centos 6.10
      • AmazonLinux

再現手順

1. 空ファイルを作成

$ touch empty

2. site.yml を以下の内容で作成

- hosts: all
  connection: docker
  tasks:
    - name: copy empty file
      copy:
        src: empty
        dest: /empty

    - name: remove empty file
      file:
        state: absent
        path: /empty

3. 検証スクリプトを以下の内容で作成

test.sh

#!/bin/bash
set -eu

NAME=$(docker ps -l --format '{{.Names}}')

cat <<++EOS>hosts
[all]
$NAME
++EOS

for ((i=0; i<100; i++)); do
    ansible-playbook -i hosts site.yml
done

4 対象サーバーのdockerコンテナを起動

$ docker run -it --rm centos:6.10 bash

5. 4.とは別のターミナルで検証スクリプトを起動

$ ./test.sh

_人人人人人人人人人_
> そのうち止まる <
 ̄YYYYYYYY

止まったときの状態

コンテナのプロセス

# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  11500  2652 pts/0    Ss   15:31   0:00 bash
root      2268  0.0  0.0   8396   556 ?        Ss   15:33   0:00 dd of=/root/.ansible/tmp/ansible-tmp-1536593616.23-267547494832017/source bs=65536
root      2279  0.0  0.0  13380  1872 pts/0    R+   15:34   0:00 ps aux

こんな感じで、dd コマンドで停止しているように見えます。

ansible-playbookの詳細ログ

-vvvオプションを付けて再実行してみます。

$ ansible-playbook -vvv -i hosts site.yml

ログを全部貼ると長いので、停止した箇所付近だけ貼り付けます。

TASK [copy empty file] **************************************************************************************************************************************************
task path: /Users/kunst/work2/ansible-test/site.yml:4
<goofy_goldstine> ESTABLISH DOCKER CONNECTION FOR USER: root
<goofy_goldstine> EXEC ['/usr/local/bin/docker', 'exec', '-i', u'goofy_goldstine', u'/bin/sh', '-c', u"/bin/sh -c 'echo ~ && sleep 0'"]
<goofy_goldstine> EXEC ['/usr/local/bin/docker', 'exec', '-i', u'goofy_goldstine', u'/bin/sh', '-c', u'/bin/sh -c \'( umask 77 && mkdir -p "` echo /root/.ansible/tmp/ansible-tmp-1536593766.1-91378099171844 `" && echo ansible-tmp-1536593766.1-91378099171844="` echo /root/.ansible/tmp/ansible-tmp-1536593766.1-91378099171844 `" ) && sleep 0\'']
Using module file /Users/kunst/Library/Python/2.7/lib/python/site-packages/ansible/modules/files/stat.py
<goofy_goldstine> PUT /var/folders/ys/535_fggj5fb3k98v2xz06b0h0000gn/T/tmpEojaf5 TO /root/.ansible/tmp/ansible-tmp-1536593766.1-91378099171844/stat.py
<goofy_goldstine> EXEC ['/usr/local/bin/docker', 'exec', '-i', u'goofy_goldstine', u'/bin/sh', '-c', u"/bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1536593766.1-91378099171844/ /root/.ansible/tmp/ansible-tmp-1536593766.1-91378099171844/stat.py && sleep 0'"]
<goofy_goldstine> EXEC ['/usr/local/bin/docker', 'exec', '-i', u'goofy_goldstine', u'/bin/sh', '-c', u"/bin/sh -c '/usr/bin/python /root/.ansible/tmp/ansible-tmp-1536593766.1-91378099171844/stat.py && sleep 0'"]
<goofy_goldstine> PUT /Users/kunst/work2/ansible-test/empty TO /root/.ansible/tmp/ansible-tmp-1536593766.1-91378099171844/source

原因調査

最初は copy モジュールが怪しいなぁと思って site-packages/ansible/modules/files/copy.py あたりをデバッグしていたのですが、どうやらその前に止まっている模様……

もしやと思い、Docker Connection Pluginのソースを見てみると、そのもの dd コマンドを投げている箇所がありました。 github.com

そのままissueを検索してみると、bugでissueが立っているのを発見。 github.com

あー、これですね。ログの出方も同じ。

応急処置

  • 空ファイルをコピーせず、 touch コマンドなどで作成する
  • 空ファイルじゃなければいいので、適当に半角スペースや改行などを入れて1バイトにする

開発環境をセットアップする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回目くらいなのでひどいコードなのは見逃して頂きたく…

広告