Puppetが遅いときはRubyバージョンアップ ？【実測編】

Puppet Advent Calendar 2015の12日目です。
Puppetの性能比較です。9日目に投稿した、[OSのRubyと違うバージョンでPuppetを動かす方法](http://qiita.com/kijibato/items/9a3bd4c184780ff2f4b9)でRubyを変えた場合の性能比較。

# リソース定義が増えると遅くなる
マニフェストのリソース定義が多すぎると、実行に時間がかかります。なんとなくCPUネックな気がしますが、サーバリソースに余裕がないとかなり遅い場合があります。設定差分チェック、設定変更に時間がかかると使いづらいのでその対策です。

# マスター側の対策
マスター/エージェント構成の場合、Puppet3の場合[Configuring a Puppet Master Server with Passenger and Apache](https://docs.puppetlabs.com/guides/passenger.html)を試す、もしくはPuppet4にしてpuppetserver（高速らしい）に乗り換えるなどです。今回こっちは対象外です。

# エージェント側の対策
Puppet4のマスター側が速くなったなら、エージェント側も速くなったのか？ということでベンチマークやりました。結果はPuppet4だから速いではなく、エージェント側のRubyをバージョンアップすると速かったとなりました。特に元がRuby1.8.7の場合、2.1.7にすることで実行時間が1/3程度の大幅改善です。(※マニフェスト構成、実行環境によっては違う結果になる可能性もあります。おそらく、インストール処理が大半の場合は短縮しないです)

# ベンチマーク
## ベンチマーク用マニフェスト
ベンチマークとして、/tmpに10000ファイル作るマニフェストをpuppet applyで適用するテストをしました。ループは使わず、べた書きでリソース定義数は10000です。

## ベンチマーク対象

ベンチマーク対象として、次のようなバージョンの組み合わせのDockerイメージをビルドして、ベンチマークやりました。

|  | イメージ | OS | Puppet | Ruby |
|:----------:|:-----------|:------------|:------------|:------------|
| 1 | CentOS6 (Puppet 3.8.4, Ruby 1.8.7) | CentOS6 | 3.8.4(rpm) | 1.8.7(rpm) |
| 2 | CentOS6 (Puppet 3.8.4, Ruby 2.1.7) | CentOS6 | 3.8.4(**gem**) | 2.1.7(**rbenv**) |
| 3 | CentOS6 (Puppet 4.3.1, Ruby 2.1.7) | CentOS6 | 4.3.1(rpm) | 2.1.7(**puppet-agent**) |
| 4 | CentOS7 (Puppet 3.8.4, Ruby 2.0.0) | CentOS7 | 3.8.4(rpm) | 2.0.0(rpm) |
| 5 | CentOS7 (Puppet 3.8.4, Ruby 2.1.7) | CentOS7 | 3.8.4(**gem**) | 2.1.7(**rbenv**) |
| 6 | CentOS7 (Puppet 4.3.1, Ruby 2.1.7) | CentOS7 | 4.3.1(rpm) | 2.1.7(**puppet-agent**) |

補足ですが、
1,4はPuppet3でのrpm最新版の組み合わせです。
3,6はPuppet4のrpm最新版でRubyはpuppet-agentに同梱されているものです。（※Puppet4のrpmで入れるpuppet-agentは、OS側のRubyに依存しない）
2,5はRuby2.1.7をrbenvを使ってバイナリからビルドして、puppetはRuby Gemsで入れています。**rpmで入れたPuppetだと、rbenvのRubyを使ってくれないので、rbenvの下のgemパッケージとしてpuppetを入れないとだめ**でした。最初気づかなくて、Ruby変えても全然速くならないとがっかりしたのは過去のこと。
※最新版は2015/12/5時点。また、Ruby2.2.xはまだ、推奨ではないみたいな記載があったためRuby2.1.7です。

## ベンチマーク環境
MacにDocker ToolboxでDocker入れた環境でベンチマークしました。
Mac,docker-machine,dockerと環境表記が難しいので、後の詳細のdockerコンテナのfacterの結果を参考にしてください。

## ベンチマーク方法
ベンチマークはDockerコンテナを立てて、１回目のpuppet applyでファイル作成(以降１回目)、２回目のpuppet applyで環境変更なしの場合(以降２回目)、コンテナ停止、コンテナ削除をそれぞれのイメージでやりました。１回目、２回目の実行時間はtimeコマンドのrealの結果です（実行時間は、puppet applyのみの計測）。
なお、ホスト側のバックグランド処理の影響を極力なくすため、各イメージで14回やって、上位２つと下位２つを除いた10回分の平均値をとりました。

## ベンチマーク結果
説明が長くなってしまいましたが、ベンチマーク結果です。

![puppetベンチ.png](https://qiita-image-store.s3.amazonaws.com/0/95528/5c9d9461-777a-9453-a6bd-ab81603acd43.png)

横軸が実行時間（秒）で、1st applyが１回目（ファイル作成あり）、2nd applyが２回目（ファイル作成なし）です。**結果的には、Rubyのバージョンが新しいほど速い**ようです。特に今回の結果ではRuby1.8.7はかなり遅い結果でした。
Puppetのバージョンについては、Puppet3.8.4の方が今回わずかに速いですが、この辺はやり方によるかもです。Puppet3.8.4の方は、Ruby2.1.7を実行環境でビルドしているのでその辺の最適化がされている？とか、Puppet4の方がマスター側が高速であれば、トータルではPuppet4の方が速い可能性もあります。その辺は、今回未検証です。あと、OSバージョンについてはほぼ差はないようです。

# まとめ
**Puppetが遅いと思ったら、Rubyのバージョンアップを検討した方がよいかも**。特にRuby1.8系を使っている場合です。

# 余談
今回は単純リソースを大量につくるだけだったので、moduleが多かったり、Hieraを使ったりすると傾向が違うかもしれません。ちなみに、１ファイルだけ作るマニフェストだと逆にRuby1.8.7の方が速かったりします、この辺はRuby1.9以降の実行時オーバヘッドに絡んでいる？？？
あと、まとめてから気づきましたが、性能比較としては全部rbenv + Gemsで比較した方がよかったかも。。。

# 参考

- [Puppet Open Source » Supported Platforms and System Requirements](https://docs.puppetlabs.com/guides/platforms.html)

# 以降、ベンチマークに使ったものとか
詳細を書くと結構大変なので、参考程度にひたすら貼っただけです。

## ベンチマーク用マニフェスト作成スクリプト

/tmpの下にファイルをたくさん作る定義をつくる。

```rb:makepp.rb
#! /usr/bin/evn ruby

def file(no)
  index = "%05d"%no
  content = "0123456789\\n" * 10
  ret = <<EOS
file { '/tmp/benchmark_file_#{index}.txt':
  ensure  => 'file',
  content => "#{content}",
  owner   => 'root',
  group   => 'root',
  mode    => '666',
}

EOS
  return ret
end

testcase = [1, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]
testcase.each do |test|
  output = "file_%05d.pp"%test
  File.open(output, 'w') do |io|
    test.times do |no|
      io.puts file(no+1)
    end
  end
end
```

## ベンチマーク環境
facterの抜粋

### ホスト依存

```text:facter
memoryfree => 1.03 GB
memorysize => 1.96 GB
processors => {"models"=>["Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz", "Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz", "Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz", "Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz"], "physicalcount"=>1, "count"=>4}
```

### コンテナ側

#### イメージ名

上記のDockerイメージは、今回は次のようなイメージ名でビルドしました。

|  | イメージ | イメージ名 |
|:----------:|:-----------|:------------|
| 1 | CentOS6 (Puppet 3.8.4, Ruby 1.8.7) | local/centos6-puppet |
| 2 | CentOS6 (Puppet 3.8.4, Ruby 2.1.7) | local/centos6-puppet-ruby-2-1-7  |
| 3 | CentOS6 (Puppet 4.3.1, Ruby 2.1.7) | local/centos6-puppet-puppet-4-3-1 |
| 4 | CentOS7 (Puppet 3.8.4, Ruby 2.0.0) | local/centos7-puppet |
| 5 | CentOS7 (Puppet 3.8.4, Ruby 2.1.7) | local/centos7-puppet-ruby-2-1-7 |
| 6 | CentOS7 (Puppet 4.3.1, Ruby 2.1.7) | local/centos7-puppet-puppet-4-3-1 |

#### 各コンテナ関連

※facterの表示形式が違うのは、バージョンが違うためです。

local/centos6-puppet

```text:facter
facterversion => 2.4.4
os => {"release"=>{"full"=>"6.7", "minor"=>"7", "major"=>"6"}, "name"=>"CentOS", "family"=>"RedHat"}
osfamily => RedHat
puppetversion => 3.8.4
rubyplatform => x86_64-linux
rubysitedir => /usr/lib/ruby/site_ruby/1.8
rubyversion => 1.8.7
```

local/centos6-puppet-ruby-2-1-7 

```text:facter
facterversion => 2.4.4
os => {"name"=>"CentOS", "family"=>"RedHat", "release"=>{"major"=>"6", "minor"=>"7", "full"=>"6.7"}}
puppetversion => 3.8.4
rubyplatform => x86_64-linux
rubysitedir => /opt/rbenv/versions/2.1.7/lib/ruby/site_ruby/2.1.0
rubyversion => 2.1.7
```

local/centos6-puppet-puppet-4-3-1

```text:facter
facterversion => 3.1.3
os => {
  architecture => "x86_64",
  family => "RedHat",
  hardware => "x86_64",
  name => "CentOS",
  release => {
    full => "6.7",
    major => "6",
    minor => "7"
  },
  selinux => {
    enabled => false
  }
}
ruby => {
  platform => "x86_64-linux",
  sitedir => "/opt/puppetlabs/puppet/lib/ruby/site_ruby/2.1.0",
  version => "2.1.7"
}
```

local/centos7-puppet

```text:facter
facterversion => 2.4.4
os => {"name"=>"CentOS", "family"=>"RedHat", "release"=>{"major"=>"7", "minor"=>"1", "full"=>"7.1.1503"}}
puppetversion => 3.8.4
rubyplatform => x86_64-linux
rubysitedir => /usr/local/share/ruby/site_ruby/
rubyversion => 2.0.0
```

local/centos7-puppet-ruby-2-1-7

```text:facter
facterversion => 2.4.4
os => {"name"=>"CentOS", "family"=>"RedHat", "release"=>{"major"=>"7", "minor"=>"1", "full"=>"7.1.1503"}}
puppetversion => 3.8.4
rubyplatform => x86_64-linux
rubysitedir => /opt/rbenv/versions/2.1.7/lib/ruby/site_ruby/2.1.0
rubyversion => 2.1.7
```

local/centos7-puppet-puppet-4-3-1

```
facterversion => 3.1.3
os => {
  architecture => "x86_64",
  family => "RedHat",
  hardware => "x86_64",
  name => "CentOS",
  release => {
    full => "7.1.1503",
    major => "7",
    minor => "1"
  },
  selinux => {
    enabled => false
  }
}
ruby => {
  platform => "x86_64-linux",
  sitedir => "/opt/puppetlabs/puppet/lib/ruby/site_ruby/2.1.0",
  version => "2.1.7"
}
```

## Dockerfile
各イメージビルドしたときに使ったDockerfile

### local/centos6-puppet

```docker:Dockerfile
FROM centos:6
MAINTAINER kijibato

# puppet install
RUN rpm -ivh http://yum.puppetlabs.com/puppetlabs-release-el-6.noarch.rpm
RUN yum clean all && \
    yum install -y puppet && \
    yum install -y puppet-server
```

### local/centos6-puppet-ruby-2-1-7

evalの実行があやしいかも？

```dockerfile:Dockerfile
FROM centos:6
MAINTAINER kijibato

# ----- init
RUN yum clean all

# ----- ruby(2.1.7) install
RUN yum install -y git
RUN yum install -y tar
RUN yum install -y gcc make openssl-devel libffi-devel readline-devel

RUN cd /opt; git clone git://github.com/sstephenson/rbenv.git
RUN mkdir /opt/rbenv/plugins
RUN cd /opt/rbenv/plugins; git clone git://github.com/sstephenson/ruby-build.git

ENV RBENV_ROOT="/opt/rbenv"
ENV PATH="${RBENV_ROOT}/bin:${PATH}"
RUN eval "$(rbenv init -)" && \
    rbenv install 2.1.7 && \
    rbenv global 2.1.7

# ----- puppet(3.8.4) install from Gems
RUN eval "$(rbenv init -)" && \
    rbenv exec gem install puppet -v 3.8.4

```

### local/centos6-puppet-puppet-4-3-1

```docker:Dockerfile
FROM centos:6
MAINTAINER kijibato

# puppet install
RUN rpm -ivh https://yum.puppetlabs.com/puppetlabs-release-pc1-el-6.noarch.rpm
RUN yum clean all && \
    yum install -y puppet-agent && \
    yum install -y puppetserver
```

### local/centos7-puppet

```dockerfile:Dockerfile
FROM centos:7
MAINTAINER kijibato

# puppet install
RUN rpm -ivh http://yum.puppetlabs.com/puppetlabs-release-el-7.noarch.rpm
RUN yum clean all && \
    yum install -y puppet && \
    yum install -y puppet-server
```

### local/centos7-puppet-ruby-2-1-7

evalの実行があやしいかも？

```dockerfile:Docker
FROM centos:7
MAINTAINER kijibato

# ----- init
RUN yum clean all

# ----- ruby(2.1.7) install
RUN yum install -y git
RUN yum install -y tar
RUN yum install -y gcc make openssl-devel libffi-devel readline-devel

RUN cd /opt; git clone git://github.com/sstephenson/rbenv.git
RUN mkdir /opt/rbenv/plugins
RUN cd /opt/rbenv/plugins; git clone git://github.com/sstephenson/ruby-build.git

ENV RBENV_ROOT="/opt/rbenv"
ENV PATH="${RBENV_ROOT}/bin:${PATH}"
RUN eval "$(rbenv init -)" && \
    rbenv install 2.1.7 && \
    rbenv global 2.1.7

# ----- puppet(3.8.4) install from Gems
RUN eval "$(rbenv init -)" && \
    rbenv exec gem install puppet -v 3.8.4
```

### local/centos7-puppet-puppet-4-3-1

```dockerfile:Dockerfile
FROM centos:7
MAINTAINER kijibato

# puppet install
RUN rpm -ivh https://yum.puppetlabs.com/puppetlabs-release-pc1-el-7.noarch.rpm
RUN yum clean all && \
    yum install -y puppet-agent && \
    yum install -y puppetserver
```

## ベンチマークスクリプト

上記のDockerfileでビルドした後に実行しました。
全部流すと結構長いです。もし、使う人がいましたら、利用は自己責任でお願いします。ただ、evalがちょっとあやしいかも？

```rb:benchmark.rb
#! /usr/bin/evn ruby
require 'open3'
require 'pp'
require 'yaml'

test_n = 14
ignore_faster = 2
ignore_later = 2
work = File.expand_path(File.dirname(__FILE__))

ppfile = '/share/file_10000.pp'

images = [ 
           'local/centos6-puppet',
           'local/centos6-puppet-ruby-2-1-7',
           'local/centos6-puppet-puppet-4-3-1',
           'local/centos7-puppet',
           'local/centos7-puppet-ruby-2-1-7',
           'local/centos7-puppet-puppet-4-3-1'
]
results = {}
images.each do |image|
  results[image] = []
end

def get_time(lines)
  real = 0
  user = 0 
  sys = 0  
  lines.each_line do |line|
    if /^real (\d+\.\d+)$/ =~ line
      real = $1.to_f
    end
    if /^user (\d+\.\d+)$/ =~ line
      user = $1.to_f
    end
    if /^sys (\d+\.\d+)$/ =~ line
      sys = $1.to_f
    end
  end
  return real, user, sys
end

test_n.times do |no|
  images.each do |image|
    result = {}
    puts "----- #{image} "+"-"*25
    name = image.gsub(/[\/\-]/, '_')
    puts `docker run -d -v #{work}:/share --name #{name} #{image} /sbin/init`
    case image
    when 'local/centos6-puppet', 'local/centos7-puppet' then
      puppet = 'puppet'
    when 'local/centos6-puppet-ruby-2-1-7','local/centos7-puppet-ruby-2-1-7' then
      puts `docker exec #{name} /bin/bash -c 'eval "$(rbenv init -)"'`
      puppet = 'rbenv exec puppet'
    when 'local/centos6-puppet-puppet-4-3-1', 'local/centos7-puppet-puppet-4-3-1' then
      puppet = '/opt/puppetlabs/bin/puppet'
    end
    puts '--- 1st apply'
    out, err, status = Open3.capture3("docker exec #{name} /bin/bash -c 'time -p #{puppet} apply #{ppfile}'")
    puts out
    puts err
    real, user, sys = get_time(err)
    result['1st'] = {'real' => real, 'user' => user, 'sys' => sys}
    puts '--- ls'
    puts `docker exec #{name} ls -l /tmp`
    puts '--- 2nd apply'
    out, err, status = Open3.capture3("docker exec #{name} /bin/bash -c 'time -p #{puppet} apply #{ppfile}'")
    puts out
    puts err
    real, user, sys = get_time(err)
    result['2nd'] = {'real' => real, 'user' => user, 'sys' => sys}
    puts `docker stop #{name}`
    puts `docker rm #{name}`

    results[image].push(result)
  end
end

puts YAML.dump(results)

results.each do |image, result|
  puts image + ':'
  sort_ret = result.sort{ |a, b| a["1st"]["real"] <=> b["1st"]["real"] }
  sort_ret.shift(ignore_faster)
  sort_ret.pop(ignore_later)
  sum = 0
  sort_ret.each do |time|
    sum += time['1st']['real']
  end
  ave = sum / sort_ret.size
  puts '+'*10 + "1st(#{sort_ret.size}): #{ave}"

  sort_ret = result.sort{ |a, b| a["2nd"]["real"] <=> b["2nd"]["real"] }
  sort_ret.shift(ignore_faster)
  sort_ret.pop(ignore_later)
  sum = 0
  sort_ret.each do |time|
    sum += time['2nd']['real']
  end
  ave = sum / sort_ret.size
  puts '+'*10 + "2nd(#{sort_ret.size}): #{ave}"
end

```

おわり。