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

Gorを試してみたmemo

github.com

Gorとはlog replayを自動化するためのツールです。go製。

log replayとは、log(httpのトラフィック)をreplay(再生)することで、よりリアルなデータ(プロダクションのトラフィック)をうまいこと使い、より効果時なテスト・確認をしようという考えからなるものです。ざっくりと。

たとえば、プロダクションのトラフィックをステージング環境に転送することでステージング環境にデプロイした実装のテストをしたり、転送するトラフィックの割合を制限しつつ負荷テストをしたり、あるいはtcpdumpのようにトラフィックをファイルに書き出し、tcpreplayのようにそれを再生したりと。

テストがないコードで、テストの回数が制限されているようなiscon5予選のときに使えたのではとgorが気になってたので試してみたメモです。(gorの作者の方がfluetndのアーキテクチャが好きでそれを参考にしているということもあり気になってもいました*1

f:id:karahiyo:20151128174658p:plain

!

Gorでできること

提供しているのはlog replayのための機能です。指定したポートでのtcpトラフィックを取得し、そのトラフィックを指定した環境に転送することができます。その転送については、転送するトラフィックの割合を制限したり、フィルタをかけたり、rewriteしたり。あるいは転送先にファイルを指定することでtcpdumpのようにトラフィックをファイルに書き出し、入力にトラフィックを書き出したファイルを指定することで、tcpreplayのようにトラフィックを再生することもできます。 そこには、さまざまなオプションがあり、ほかのlog replayのツールと比較しかなり細かいところまで手が届くものになっているように見えます。

gor/README.md at master · buger/gor · GitHub

$ gor -h
Version: 0.10.1
Gor is a simple http traffic replication tool written in Go. Its main goal is to replay traffic from production servers to staging and dev environments.
Project page: https://github.com/buger/gor
Author: <Leonid Bugaev> leonsbox@gmail.com
Current Version: 0.10.1

  -cpuprofile="": write cpu profile to file
  -debug=false: Turn on debug output, shows all itercepted traffic. Works only when with `verbose` flag
  -http-allow-header=[]: A regexp to match a specific header against. Requests with non-matching headers will be dropped:
         gor --input-raw :8080 --output-http staging.com --http-allow-header api-version:^v1
  -http-allow-method=[]: Whitelist of HTTP methods to replay. Anything else will be dropped:
        gor --input-raw :8080 --output-http staging.com --http-allow-method GET --http-allow-method OPTIONS
  -http-allow-url=[]: A regexp to match requests against. Filter get matched against full url with domain. Anything else will be dropped:
         gor --input-raw :8080 --output-http staging.com --http-allow-url ^www.
  -http-disallow-header=[]: A regexp to match a specific header against. Requests with matching headers will be dropped:
         gor --input-raw :8080 --output-http staging.com --http-disallow-header "User-Agent: Replayed by Gor"
  -http-disallow-url=[]: A regexp to match requests against. Filter get matched against full url with domain. Anything else will be forwarded:
         gor --input-raw :8080 --output-http staging.com --http-disallow-url ^www.
  -http-header-limiter=[]: Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific header:
         gor --input-raw :8080 --output-http staging.com --http-header-imiter user-id:25%
  -http-original-host=false: Normally gor replaces the Host http header with the host supplied with --output-http.  This option disables that behavior, preserving the original Host header.
  -http-param-limiter=[]: Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific GET param:
         gor --input-raw :8080 --output-http staging.com --http-param-limiter user_id:25%
  -http-rewrite-url=[]: Rewrite the request url based on a mapping:
        gor --input-raw :8080 --output-http staging.com --http-rewrite-url /v1/user/([^\/]+)/ping:/v2/user/$1/ping
  -http-set-header=[]: Inject additional headers to http reqest:
        gor --input-raw :8080 --output-http staging.com --http-set-header 'User-Agent: Gor'
  -http-set-param=[]: Set request url param, if param already exists it will be overwritten:
        gor --input-raw :8080 --output-http staging.com --http-set-param api_key=1
  -input-dummy=[]: Used for testing outputs. Emits 'Get /' request every 1s
  -input-file=[]: Read requests from file:
        gor --input-file ./requests.gor --output-http staging.com
  -input-http=[]: Read requests from HTTP, should be explicitly sent from your application:
        # Listen for http on 9000
        gor --input-http :9000 --output-http staging.com
  -input-raw=[]: Capture traffic from given port (use RAW sockets and require *sudo* access):
        # Capture traffic from 8080 port
        gor --input-raw :8080 --output-http staging.com
  -input-tcp=[]: Used for internal communication between Gor instances. Example:
        # Receive requests from other Gor instances on 28020 port, and redirect output to staging
        gor --input-tcp :28020 --output-http staging.com
  -memprofile="": write memory profile to this file
  -middleware="": Used for modifying traffic using external command
  -output-dummy=[]: Used for testing inputs. Just prints data coming from inputs.
  -output-file=[]: Write incoming requests to file:
        gor --input-raw :80 --output-file ./requests.gor
  -output-http=[]: Forwards incoming requests to given http address.
        # Redirect all incoming requests to staging.com address
        gor --input-raw :80 --output-http http://staging.com
  -output-http-elasticsearch="": Send request and response stats to ElasticSearch:
        gor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'
  -output-http-header=[]: WARNING: `--output-http-header` DEPRECATED, use `--http-set-header` instead
  -output-http-header-filter=[]: WARNING: `--output-http-header-filter` DEPRECATED, use `--http-allow-header` instead
  -output-http-header-hash-filter=[]: WARNING: `output-http-header-hash-filter` DEPRECATED, use `--http-header-hash-limiter` instead
  -output-http-method=[]: WARNING: `--output-http-method` DEPRECATED, use `--http-allow-method` instead
  -output-http-redirects=0: Enable how often redirects should be followed.
  -output-http-rewrite-url=[]: WARNING: `--output-http-rewrite-url` DEPRECATED, use `--http-rewrite-url` instead
  -output-http-stats=false: Report http output queue stats to console every 5 seconds.
  -output-http-timeout=0: Specify HTTP request/response timeout. By default 5s. Example: --output-http-timeout 30s
  -output-http-url-regexp=[]: WARNING: `--output-http-url-regexp` DEPRECATED, use `--http-allow-url` instead
  -output-http-workers=0: Gor uses dynamic worker scaling by default.  Enter a number to run a set number of workers.
  -output-tcp=[]: Used for internal communication between Gor instances. Example:
        # Listen for requests on 80 port and forward them to other Gor instance on 28020 port
        gor --input-raw :80 --output-tcp replay.local:28020
  -output-tcp-stats=false: Report TCP output queue stats to console every 5 seconds.
  -split-output=false: By default each output gets same traffic. If set to `true` it splits traffic equally among all outputs.
  -stats=false: Turn on queue stats output
  -verbose=false: Turn on more verbose output

install

$ wget https://github.com/buger/gor/releases/download/v0.10.1/gor_0.10.1_x64.tar.gz
$ tar zxvf gor_0.10.1_x64.tar.gz
$ mv ./gor /usr/local/bin/

run

サンプルのアプリケーションのコードはgistにあげてあります。

$ sudo setcap CAP_NET_RAW=ep gor # 必要なら
$ go run server1.go
$ go run server2.go
$ gor --input-raw :8081 -output-http :8082
$ http localhost:8081
HTTP/1.1 200 OK
Content-Length: 7
Content-Type: text/plain; charset=utf-8
Date: Wed, 25 Nov 2015 13:37:42 GMT

server1

ngrepでみてみます

$ sudo ngrep -d lo -W byline port 8081
interface: lo (127.0.0.0/255.0.0.0)
filter: ( port 8081 ) and (ip or ip6)
######
T 127.0.0.1:45925 -> 127.0.0.1:8081 [AP]
GET / HTTP/1.1.
Host: localhost:8081.
Connection: keep-alive.
Accept-Encoding: gzip, deflate.
Accept: */*.
User-Agent: HTTPie/0.9.2.
.

##
T 127.0.0.1:8081 -> 127.0.0.1:45925 [AP]
HTTP/1.1 200 OK.
Date: Wed, 25 Nov 2015 13:37:42 GMT.
Content-Length: 7.
Content-Type: text/plain; charset=utf-8.
.
server1
####

転送されています🙌

$ sudo ngrep -d lo -W byline port 8082
interface: lo (127.0.0.0/255.0.0.0)
filter: ( port 8082 ) and (ip or ip6)
#
T 127.0.0.1:51706 -> 127.0.0.1:8082 [AP]
GET / HTTP/1.1.
Host: :8082.
Connection: keep-alive.
Accept-Encoding: gzip, deflate.
Accept: */*.
User-Agent: HTTPie/0.9.2.
.

#
T 127.0.0.1:8082 -> 127.0.0.1:51706 [AP]
HTTP/1.1 200 OK.
Date: Wed, 25 Nov 2015 13:40:49 GMT.
Content-Length: 7.
Content-Type: text/plain; charset=utf-8.
.
server2
#

header情報を指定してみます

User-Agent、Cookie、リファラを指定してみます

$ http localhost:8081 User-Agent:"Mozilla/5.0 (Linux; Android 4.3; Ne
xus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Safari/537.36" '
Cookie:uuid=gor-test-12345' Referer:http://full.com.bo
HTTP/1.1 200 OK
Content-Length: 7
Content-Type: text/plain; charset=utf-8
Date: Sat, 28 Nov 2015 08:27:37 GMT

server1

UA、Cookie、リファラも問題なく転送できてます

$ sudo ngrep -d lo -W byline port 8082
interface: lo (127.0.0.0/255.0.0.0)
filter: ( port 8082 ) and (ip or ip6)
#
T 127.0.0.1:47130 -> 127.0.0.1:8082 [AP]
GET / HTTP/1.1.
Host: :8082.
Accept-Encoding: gzip, deflate.
Accept: */*.
User-Agent: Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Safari/537.36.
Connection: keep-alive.
Cookie: uuid=gor-test-12345.
Referer: http://full.com.bo.
.

##
T 127.0.0.1:8082 -> 127.0.0.1:47130 [AP]
HTTP/1.1 200 OK.
Date: Sat, 28 Nov 2015 08:27:37 GMT.
Content-Length: 7.
Content-Type: text/plain; charset=utf-8.
.
server2
#

複数のoutput先を指定

ここらへんの転送先の設定をレゴのように組み合わせるのはfluentd由来ぽい

$ gor --input-raw :8081 --output-http :8082 --output-http :8083 --output-http :8084

リクエストをファイルに書き出し再送する

$ gor --input-raw :8081 --output-file gor-request.log
$ gor --input-file gor-request.log -output-http :8082

gor-request.logには次のような文字列が書き出されます 区切り文字のセンス🐵🙈🙉

1 f07d3261aafa4a8e7056bd812fb58c6e0ea3e978 1448459937338780583
GET / HTTP/1.1
Host: localhost:8081
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: HTTPie/0.9.2

🐵🙈🙉
2 f07d3261aafa4a8e7056bd812fb58c6e0ea3e978 395185
HTTP/1.1 200 OK
Date: Wed, 25 Nov 2015 13:58:57 GMT
Content-Length: 7
Content-Type: text/plain; charset=utf-8

server1
🐵🙈🙉

ファイルに書き出したリクエストのログからリクエストを再生する

$ gor --input-file "gor-request2.log|200%" -output-http :8082

mysqlのポートを指定してcatch???

package main

import (
        "database/sql"
        _ "github.com/go-sql-driver/mysql"
)

func main() {
        db, err := sql.Open("mysql", "karahiyo@/test")
        if err != nil {
                panic(err.Error())
        }
        defer db.Close()

        db.QueryRow("SELECT 1").Scan(&one)
}

とれては...いますね

1 12aaebef061677e56df3e983c639fd437c9c2808 1448466432628463498
SELECT @@max_allowed_packet
🐵🙈🙉
2 12aaebef061677e56df3e983c639fd437c9c2808 175777
*def@@max_allowed_packet
                        �1048576�
🐵🙈🙉
1 7cebd7122df7e346ebcb48bc3236b0e6e38bdc63 1448466432628711332
        SELECT 1
🐵🙈🙉
2 7cebd7122df7e346ebcb48bc3236b0e6e38bdc63 101965
def1
    ��1�
🐵🙈🙉

!

他にも、fluetndのout_execみたいな機能がmiddlewareという形であるようです。

f:id:karahiyo:20151128181359p:plain


!

log replayができるほかのツール

!

感想

ローカルでのテストやステージング環境で確認できることと、本番系で確認できることではたしかに得られるものに大きな差があると思います。その差を少しでも小さくするためにgorがつくられたようです。

実際にはサービスでは予期していない様々なことが起きます。どれだけ事前にテストをしたとしてもカバーするのは難しく、事は起こるときには起こるものです。 できそうなことは例えば、起き得る事を考え、その影響範囲を確認し、必要ならその影響範囲をより少なくするための方法を考え、対応し、そしてリカバリ手順を確認したうえで、えいやと本番系で確認するという方法があります。1リリース1機能の方針など。ほかには、auto scalingな構成のサービスでならリリース先のサーバを数台に限定してデプロイするなどの手段もあるかと思います。 サービスに直接利益があるわけではないけど安定したサービスのために無視できないそれら心配事が、gorなどを使った仕組みによって意識しなければいけないことが減るのならそれはいい話だなと思いました。

© karahiyo