読者です 読者をやめる 読者になる 読者になる

まったり技術ブログ

Technology is power.

golang製ベンチマークツール『hey』を使ってみた

使ってみた Webサーバ Go言語

f:id:motikan2010:20170117010150p:plain Web界隈では「Apache Bench」「JMeter」などのベンチマークツールが有名ですが、ここではGo言語で開発されたベンチマークツール『hey』を紹介します。

github.com

百聞は一見に如かず「hey」!!

結果は下記ような形式で出力されます。abと比べて非常にシンプルな結果表示になっています。
このツールの実装自体もシンプルで可読性が高く(さすがはGo)、カスタマイズしても良し、このツールを参考に自分自身でベンチマークツールを作成するのにも参考になるかと思います。

# ./hey -n 200 -c 50 http://127.0.0.1:3000/login
2 requests done.
10 requests done.
15 requests done.
21 requests done.
(省略)
192 requests done.
197 requests done.
All requests done.

Summary:
  Total:    19.2161 secs
  Slowest:  18.6380 secs
  Fastest:  0.1297 secs
  Average:  2.6002 secs
  Requests/sec: 10.4080
  Total data:   180532 bytes
  Size/request: 902 bytes

Status code distribution:
  [200] 199 responses
  [500] 1 responses

Response time histogram:
  0.130 [1]    |
  1.981 [155]  |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  3.831 [5]    |∎
  5.682 [5]    |∎
  7.533 [5]    |∎
  9.384 [5]    |∎
  11.235 [4]   |∎
  13.086 [5]   |∎
  14.936 [6]   |∎∎
  16.787 [4]   |∎
  18.638 [5]   |∎

Latency distribution:
  10% in 0.3156 secs
  25% in 0.3908 secs
  50% in 0.4547 secs
  75% in 0.6077 secs
  90% in 11.2754 secs
  95% in 14.9333 secs
  99% in 18.3849 secs

インストール

Go言語が使える状態から始めます。動作するバージョンは1.7以上です。

$ go version
go version go1.7.4 linux/amd64

インストールは非常に簡単です。下記のコマンドを実行するだけです。

$ go install github.com/rakyll/hey

これでエラーが表示されなければheyコマンドが使用できるようになります。
どのようなオプションが用意されているのかを確認してみます。

$ hey
Usage: hey [options...] <url>

Options:
  -n  Number of requests to run. Default is 200.
  -c  Number of requests to run concurrently. Total number of requests cannot
      be smaller than the concurrency level. Default is 50.
  -q  Rate limit, in seconds (QPS).
  -o  Output type. If none provided, a summary is printed.
      "csv" is the only supported alternative. Dumps the response
      metrics in comma-separated values format.

  -m  HTTP method, one of GET, POST, PUT, DELETE, HEAD, OPTIONS.
  -H  Custom HTTP header. You can specify as many as needed by repeating the flag.
      For example, -H "Accept: text/html" -H "Content-Type: application/xml" .
  -t  Timeout for each request in seconds. Default is 20, use 0 for infinite.
  -A  HTTP Accept header.
  -d  HTTP request body.
  -D  HTTP request body from file. For example, /home/user/file.txt or ./file.txt.
  -T  Content-type, defaults to "text/html".
  -a  Basic authentication, username:password.
  -x  HTTP Proxy address as host:port.
  -h2 Enable HTTP/2.

  -host HTTP Host header.

  -disable-compression  Disable compression.
  -disable-keepalive    Disable keep-alive, prevents re-use of TCP
                        connections between different HTTP requests.
  -cpus                 Number of used cpu cores.
                        (default for current machine is 2 cores)
  -more                 Provides information on DNS lookup, dialup, request and
                        response timings.

動作確認

GETリクエスト

もっとも基本なGETリクエスト。下記のように実行します。

$ hey http://127.0.0.1:3000/

POSTリクエスト

  • HTTPメソッドをPOSTに指定。
  • リクエストヘッダに「Content-Type: application/x-www-form-urlencoded」を付与
  • もちろんPOSTパラメータも送ることができます。
    ちなみにURLエンコードをする必要はありませんでした。
$ hey -m POST -H "Content-Type: application/x-www-form-urlencoded" -d "email=example@example.com&password=foobar" http://127.0.0.1:3000/login

Basic認証

「-a [ユーザ名]:[パスワード]」をオプションに指定します。
ダブルクォーテーションはあってもなくても動作します。

$ hey -a "hogeuser:hogepassword" http://127.0.0.1/

使ってみて

最近Go言語にハマってしまいGo言語製のツールを探してみてこのツールにたどり着きましたが、やはりベンチーマークツールは楽しいですね。なによりズラズラと流れるアプリケーションログを眺めるのが非常に快感を覚えます。
まだまだ『hey』のオプションは全て使え切れていない。(今回は2つしか使っていないし...)
キープアライブやHTTP/2の知識を入れて是非使いこなしてみたいツールですね。

Webサーバベンチマークツール『weighttp』をさわってみる

Webサーバ

Webサーバのベンチマークツールである"weighttp"のインストール・動作確認までを行っていきます。
私の環境ではPythonのバージョン3がデフォルトして動作しており、それが原因でつまずいた点がありましたので、記載しておきます。

github.com

インストール

$ yum install -y --enablerepo=epel libev libev-devel
$ wget http://github.com/lighttpd/weighttp/zipball/master
$ unzip master
$ cd lighttpd-weighttp-f680bec/

configure実行(失敗)

$ ./waf configure
Checking for program gcc,cc              : ok /usr/bin/gcc
Checking for program cpp                 : ok /usr/bin/cpp
Checking for program ar                  : ok /usr/bin/ar
Checking for program ranlib              : ok /usr/bin/ranlib
Checking for gcc                         : ok
Checking for library ev                  : ok
Traceback (most recent call last):
  File "/tmp/weighttp-master/.waf3-1.5.9-d1e0349fc8937631a656fb8ea7e99063/wafadmin/Utils.py", line 414, in recurse
    txt=readf(base+'_'+name,m='rU')
  File "/tmp/weighttp-master/.waf3-1.5.9-d1e0349fc8937631a656fb8ea7e99063/wafadmin/Utils.py", line 379, in readf
    f=open(fname,m)
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/weighttp-master/wscript_configure'

"FileNotFoundError"が発生している。下のサイトで解決することができた。 バージョン2のPythonを使用すればいいらしい。

[solved] Can't Compile Hamster (Python app) / Applications & Desktop Environments / Arch Linux Forums

configure実行(失敗2度目・・・)

$ python2 ./waf configure

Checking for program gcc,cc              : ok /usr/bin/gcc
Checking for program cpp                 : ok /usr/bin/cpp
Checking for program ar                  : ok /usr/bin/ar
Checking for program ranlib              : ok /usr/bin/ranlib
Checking for gcc                         : ok
Checking for library ev                  : ok
Checking for header ev.h                 : not found
 error: the configuration failed (see '/tmp/weighttp-master/build/config.log')

"Checking for header ev.h : not found"と出力されているので、ヘッダファイルを移動させます。

ヘッダファイル"ev.h"を移動

$ cp /usr/include/libev/ev.h /usr/include/

configure実行(成功!!)

$ python2 ./waf configure

Checking for program gcc,cc              : ok /usr/bin/gcc
Checking for program cpp                 : ok /usr/bin/cpp
Checking for program ar                  : ok /usr/bin/ar
Checking for program ranlib              : ok /usr/bin/ranlib
Checking for gcc                         : ok
Checking for library ev                  : ok
Checking for header ev.h                 : ok
Checking for library pthread             : ok
Checking for header pthread.h            : ok
Checking for header unistd.h             : ok
Checking for header stdint.h             : ok
Checking for header fcntl.h              : ok
Checking for header inttypes.h           : ok
'configure' finished successfully (0.481s)

ビルド

$ python2 ./waf build

Waf: Entering directory `/tmp/weighttp-master/build'
[1/4] cc: src/client.c -> build/default/src/client_1.o
[2/4] cc: src/weighttp.c -> build/default/src/weighttp_1.o
[3/4] cc: src/worker.c -> build/default/src/worker_1.o
[4/4] cc_link: build/default/src/client_1.o build/default/src/weighttp_1.o build/default/src/worker_1.o -> build/default/weighttp
Waf: Leaving directory `/tmp/weighttp-master/build'
'build' finished successfully (0.754s)

インストール

$ python2 ./waf install

Waf: Entering directory `/tmp/weighttp-master/build'
* installing build/default/weighttp as /usr/local/bin/weighttp
Waf: Leaving directory `/tmp/weighttp-master/build'
'install' finished successfully (0.007s)

"weighttp"コマンドを実行

これで"weighttp"を利用することができるようになりました。 試しにヘルプコマンドを実行してみます。

$ weighttp --help
weighttp 0.4 - a lightweight and simple webserver benchmarking tool

error: unkown option: --

weighttp <options> <url>
  -n num   number of requests    (mandatory)
  -t num   threadcount           (default: 1)
  -c num   concurrent clients    (default: 1)
  -k       keep alive            (default: no)
  -6       use ipv6              (default: no)
  -H str   add header to request
  -h       show help and exit
  -v       show version and exit

example: weighttpd -n 10000 -c 10 -t 2 -k -H "User-Agent: foo" localhost/index.html

試してみる

helpコマンドで出力されたコマンド例通りに動かしてみます。

$ weighttp -n 10000 -c 10 -t 2 -k -H "User-Agent: foo" localhost/index.php

weighttp 0.4 - a lightweight and simple webserver benchmarking tool

starting benchmark...
spawning thread #1: 5 concurrent requests, 5000 total requests
spawning thread #2: 5 concurrent requests, 5000 total requests
progress:  10% done
progress:  20% done
progress:  30% done
progress:  40% done
progress:  50% done
progress:  60% done
progress:  70% done
progress:  80% done
progress:  90% done
progress: 100% done

finished in 5 sec, 34 millisec and 237 microsec, 1986 req/s, 387 kbyte/s
requests: 10000 total, 10000 started, 10000 done, 10000 succeeded, 0 failed, 0 errored
status codes: 10000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 2000000 bytes total, 1920000 bytes http, 80000 bytes data

Webサーバログにも通信ログが残っておりしっかりとベンチマークを取得できていそう。
今までのは"Apache Bench"を使っていましたが、"weighttp"が代替ツールになるかこれからいろいろ検証していく予定。

キャプチャ認証『SimpleCaptcha2』を使ってみる

Ruby Rails

f:id:motikan2010:20170112012951p:plain
今回はBot等に自動認証を許さないために用いられる 「CAPTCHA」をRailsアプリケーションに実装してみます。
ImageMagickあたりでつまずいたので、備忘録感覚にメモ。

環境確認とアプリの作成

まずは動作環境の確認とサンプルサプリケーションの作成を行っていきます。
普及している4系を使っていきます。

$ cat /etc/redhat-release
CentOS release 6.8 (Final)
$ rails _4.2.0_ new app

「simple-captcha」の追加(失敗)

メインとなるキャプチャのライブラリですが、上位の人気を誇っている「simple-captcha」を入れてみました。

$ vim Gemfile

(追加)
gem 'simple_captcha', :git => 'git://github.com/galetahub/simple-captcha.git'

そして、いつも通りにインストールします。

# bundle install

simple-captchaのセットアップ

ここで問題が発生しました。

$ rails generate simple_captcha
Running via Spring preloader in process 11946
   identical  app/views/simple_captcha/_simple_captcha.erb
/opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/simple-captcha-2602bf19a63d/lib/generators/simple_captcha_generator.rb:18:in `create_migration': wrong number of arguments (given 3, expected 0) (ArgumentError)
  from /opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/railties-4.2.0/lib/rails/generators/migration.rb:63:in `migration_template'
  from /opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/simple-captcha-2602bf19a63d/lib/generators/simple_captcha_generator.rb:19:in `create_migration'

https://github.com/pludoni/simple-captcha/issues/18 上記URL先を見てみると、新バージョンである『SimpleCaptcha2』を使えば解決できそうな予感。

『SimpleCaptcha2』のセットアップ

$ vim Gemfile
(追加)
gem 'simple_captcha2', git: 'https://github.com/pludoni/simple-captcha.git', require: true

そして再度インストール

$ bundle install

改めて「rails generate simple_captcha」コマンド

$ rails generate simple_captcha
Running via Spring preloader in process 1067
Expected string default value for '--helper'; got true (boolean)
Expected string default value for '--jbuilder'; got true (boolean)
      create  app/views/simple_captcha/_simple_captcha.erb
      create  db/migrate/20170112000239_create_simple_captcha_data.rb

様々なファイルが作成されたということで今回は成功したらしい。

db/migrate/に下記のようなマイグレーションファイルが作成されたことが確認できます。

$ cat db/migrate/20170112000239_create_simple_captcha_data.rb

class CreateSimpleCaptchaData < ActiveRecord::Migration
  def self.up
    create_table :simple_captcha_data do |t|
      t.string :key, :limit => 40
      t.string :value, :limit => 6
      t.timestamps
    end

    add_index :simple_captcha_data, :key, :name => "idx_key"
  end

  def self.down
    drop_table :simple_captcha_data
  end
end

下記コマンドでテーブルを作成します。

$ rake db:migrate
== 20170112000239 CreateSimpleCaptchaData: migrating ==========================
-- create_table(:simple_captcha_data)
   -> 0.0026s
-- add_index(:simple_captcha_data, :key, {:name=>"idx_key"})
   -> 0.0012s
== 20170112000239 CreateSimpleCaptchaData: migrated (0.0040s) =================

テーブルが正常に作成されたことを確認して、下記のようにapplication.rbコントローラに「include SimpleCaptcha::ControllerHelpers」の一行を追記します。

$ vim app/controllers/application.rb
 
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  include SimpleCaptcha::ControllerHelpers
end

テストアプリを作成

ここでは簡単にRESTアプリを作成します。

# rails g scaffold Memo title:string description:text
# rake db:migrate

ビューを変更

下記のようにキャプチャ入力項目を追記します。

$ vim app/views/memos/_form.html.erb

(省略)
<div class="field">
    <%= f.label :description %><br>
    <%= f.text_area :description %>
  </div>
  <div class="field">
    <%= f.label "Simple-Captcha" %><br>
    <%= f.simple_captcha :label => "上の文字を記入してくだい", :placeholder => "ここに入力" %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
(省略)

ブラウザでキャプチャの確認

ここまで進めますと「http://localhost:3000/memos/new」にアクセスするとキャプチャ入力項目が表示されるはずですので、確認してみます。 f:id:motikan2010:20170112001609p:plain:w200
表示されていません・・・。
サーバ側では以下のようなエラーが表示されています。

StandardError (Error while running convert: convert: not authorized `ACTWH' @ error/constitute.c/ReadImage/453.
convert: missing an image filename `jpeg:-' @ error/convert.c/ConvertImageCommand/3015.
):
  /opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/simple-captcha-b145495ab9e5/lib/simple_captcha/utils.rb:17:in `run'
  /opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/simple-captcha-b145495ab9e5/lib/simple_captcha/image.rb:83:in `generate_simple_captcha_image'
  /opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/simple-captcha-b145495ab9e5/lib/simple_captcha/middleware.rb:42:in `make_image'
  /opt/rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/simple-captcha-b145495ab9e5/lib/simple_captcha/middleware.rb:21:in `call'
(以下省略)

キャプチャ画像を表示

画像の加工処理がうまくいっていないようですので、その処理に必要なパッケーシをインストールします。

# yum -y install ghostscript
# yum -y install ImageMagick-devel

ここでブラウザで確認してみても画像が表示されていません。 http://www.srcw.net/wiki/index.php?SimpleCaptcha 調べてみるとImageMagick脆弱性があり、設定ポリシーが厳しくなりデフォルト設定だと表示されていないと推測。 その脆弱性に関する情報はこちらを参照 ImageMagickの脆弱性(CVE-2016-3714他)についてまとめてみた - piyolog

その設定ファイルである「ポリシーファイル」を以下のように編集してみます。

$ vim /etc/ImageMagick/policy.xml

(省略)
<policymap>
  <!-- <policy domain="system" name="precision" value="6"/> -->
  <!-- <policy domain="resource" name="temporary-path" value="/tmp"/> -->
  <!-- <policy domain="resource" name="memory" value="2GiB"/> -->
  <!-- <policy domain="resource" name="map" value="4GiB"/> -->
  <!-- <policy domain="resource" name="area" value="1gb"/> -->
  <!-- <policy domain="resource" name="disk" value="16eb"/> -->
  <!-- <policy domain="resource" name="file" value="768"/> -->
  <!-- <policy domain="resource" name="thread" value="4"/> -->
  <!-- <policy domain="resource" name="throttle" value="0"/> -->
  <!-- <policy domain="resource" name="time" value="3600"/> -->
<!--
  <policy domain="coder" rights="none" pattern="EPHEMERAL" />
  <policy domain="coder" rights="none" pattern="HTTPS" />
  <policy domain="coder" rights="none" pattern="HTTP" />
  <policy domain="coder" rights="none" pattern="URL" />
  <policy domain="coder" rights="none" pattern="FTP" />
  <policy domain="coder" rights="none" pattern="MVG" />
  <policy domain="coder" rights="none" pattern="MSL" />
  <policy domain="coder" rights="none" pattern="TEXT" />
  <policy domain="coder" rights="none" pattern="LABEL" />
  <policy domain="path" rights="none" pattern="@*" />
-->
</policymap>

編集を終えたら再度ブラウザでアクセスし確認します。 f:id:motikan2010:20170112001211p:plain:w200
今度はうまく表示できているようです。 ですが肝心なキャプチャをデタラメな値を入力しても作成できるようになっています。
正常に動作させるためにモデルとコントローラの編集を行います。

正常に動作させるためモデルとコントローラを編集

モデル

「attr_accessor :captcha_key, :captcha」の一行を追記します。

$ vim app/models/memo.rb

class Memo < ActiveRecord::Base
  attr_accessor :captcha_key, :captcha
end

コントローラ

末尾にあるmemo_params関数の中身を以下のように編集します。
(, :captcha_key, :captchaを追加)

$ vim app/controllers/memos_controller.rb

(省略)
    def memo_params
        params.require(:memo).permit(:title, :description, :captcha_key, :captcha)
    end
end

同じコントローラファイルに入力されたキャプチャ文字列が正しいかの検証処理をcreateメソッド内に記述します。 以下のように編集します。

$ vim app/controllers/memos_controller.rb

  # POST /memos
  # POST /memos.json
  def create
    @memo = Memo.new(memo_params)

    if simple_captcha_valid?
      respond_to do |format|
        if @memo.save
          format.html { redirect_to @memo, notice: 'Memo was successfully created.' }
          format.json { render :show, status: :created, location: @memo }
        else
          format.html { render :new }
          format.json { render json: @memo.errors, status: :unprocessable_entity }
        end
      end
    else
        redirect_to :action => "new"
    end
  end

最後に入力された値の引き渡しがうまくいくように下記のコードを記述します。

$ vim config/environment.rb

module SimpleCaptcha
  module ControllerHelpers
    def simple_captcha_valid?
      return true if Rails.env.test?
      if params[:memo][:captcha]
        data = SimpleCaptcha::Utils::simple_captcha_value(params[:memo][:captcha_key] || session[:captcha])
        result = data == params[:memo][:captcha].delete(" ").upcase
        SimpleCaptcha::Utils::simple_captcha_passed!(session[:captcha]) if result
        return result
      else
        return false
      end
    end
  end
end

これで入力された文字列と画像に表示されている文字列の比較が行われ、等しい場合に正常な処理が行われるようになります。

参考サイト

joppot.info

PythonだけでHTTPSサーバ

Python

Secure属性を付与したCookieの取り扱いなど簡単な検証で重宝しています。

HTTPS通信にしようするサーバ証明書の作成
# openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes
Webサーバ側のコード

ここではWebフレームワークのbottleを使用します。

import sys, os, datetime
from bottle import route, run, request, HTTPResponse, ServerAdapter

class SSLWebServer(ServerAdapter):

def run(self, handler):
from gevent.pywsgi import WSGIServer
srv = WSGIServer((self.host, self.port), handler,
certfile='./server.pem',
keyfile='./server.pem')
srv.serve_forever()

@route('/', method='GET')
@route('/index', method='GET')
def index():
body = "0"
if request.get_cookie("counter"):
body = str(int(request.get_cookie("counter")) + 1)
res = HTTPResponse(status=200, body=body)
res.set_cookie('counter', body, secure=1)
return res

run(host='127.0.0.1', port=8080, server=SSLWebServer)
ブラウザで確認

f:id:motikan2010:20161225194304p:plain

curlコマンドで確認

-k オプションでHTTPS通信を行います。

# curl -v -k https://127.0.0.1:8080/
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
* Server certificate: Internet Widgits Pty Ltd
> GET / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 1
< Content-Type: text/html; charset=UTF-8
< Set-Cookie: counter=0; Secure
< Date: Sun, 25 Dec 2016 10:31:46 GMT
<
* Connection #0 to host 127.0.0.1 left intact
0

参考:dgtool: SSL encryption in python bottle

キーボード入力文字列をバイナリデータへ変換

Python

関数inputで入力された値は文字列型として扱われる。
16進数→10進数→バイナリデータ

import struct

def main():
  str = input()

  str_list = str.split(" ")

  f = open('test.dat', 'wb')
  for hex in str_list:
    f.write(struct.pack('B', int(hex, 16)))

  f.close()

if __name__ == "__main__":
    main()

ここでは「hex_sample.py」で保存

# python hex_sample.py
68 65 6c 6c 6f 20 77 6f 72 6c 64 21 21

# hexdump -C test.dat
00000000  68 65 6c 6c 6f 20 77 6f  72 6c 64 21 21           |hello world!!|
0000000d

バイナリデータで保存されている。
実用には例外処理なども記述するように。