【負荷試験】Rails Sever VS Go Server

# Ruby(Rails) VS Go(echo)
RailsとGo製サーバーの耐アクセス性能を比べてみようと思います。

それぞれDockerコンテナでサーバーを建てて、どれほどのアクセス量まで耐えられるのか検証します。
理想はEC2上などでやりたかったですが、本検証は手元のMacで行っております。


## Rails Serverを建てる

apiモードで`rails new`
```bash
 rails new rails_app --api
 ```

コントローラーだけ作ります。
```bash
rails g controller posts
```

```ruby:app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    render json: {"status": "ok"}
  end
end
```

ルーティング
```ruby:config/routes.rb
Rails.application.routes.draw do
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
  root "posts#index"
end

```

```Dockerfile:Dockerfile
FROM ruby:3.2.1-slim-bullseye

RUN apt-get update -qq
RUN apt-get install -y nodejs postgresql-client npm
RUN npm install --global yarn

WORKDIR /myapp
COPY . /myapp
RUN bundle install

CMD ["./bin/rails", "s", "-b", "0.0.0.0"]

```
ビルド
```bash
docker build -t rails_server:latest .
```
起動
```bash
docker run -p 3000:3000  rails_server:latest
```

アクセスしてみます。
```bash
$ curl localhost:3000
{"status":"ok"}
```


## echoサーバーを建てる
お次はechoサーバーです。

```Dockerfile:Dockefile
FROM golang:1.21.5
WORKDIR /app
RUN go mod init echo_app

RUN go get github.com/labstack/echo/v4
RUN go get github.com/labstack/echo/v4/middleware

COPY . .
CMD ["go", "run", "main.go"]
```

```go:main.go
package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

type ResponseData struct {
	Status string `json:"status"`
}

func main() {
	e := echo.New()

	e.Use(middleware.Logger())

	e.GET("/", hello)

	e.Logger.Fatal(e.Start(":1323"))
}

func hello(c echo.Context) error {
	data := ResponseData{
		Status: "ok",
	}
	return c.JSON(http.StatusOK, data)
}
```

ビルド
```bash
 docker build -t echo_server_demo:latest .
```

起動
```bash
docker run -p3002:1323 echo_server_demo:latest

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.11.3
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
```

## 検証用プログラム

並行処理を用いてリクエストを送るプログラムです。
```go
package main

import (
	"fmt"
	"net/http"
	"sync"
)

// sendRequest関数は、指定されたURLにHTTPリクエストを送信し、
// 成功か失敗かをchannelを通して返します。
func sendRequest(url string, wg *sync.WaitGroup, ch chan<- bool) {
	defer wg.Done()

	_, err := http.Get(url)
	if err != nil {
		ch <- false // 失敗
		return
	}

	ch <- true // 成功
}

func main() {
	var wg sync.WaitGroup
	requestCount := 100            // 送信するリクエストの回数
	url := "http://localhost:3000" // リクエストを送信するURL

	// 成功と失敗をカウントするための変数
	var successCount, failureCount int

	// 結果を受け取るためのchannelを作成
	ch := make(chan bool, requestCount)

	for i := 0; i < requestCount; i++ {
		wg.Add(1)
		// 各リクエストをgoroutineで実行
		go sendRequest(url, &wg, ch)
	}

	// すべてのgoroutineの完了を待機
	wg.Wait()
	close(ch)

	// 成功と失敗をカウント
	for result := range ch {
		if result {
			successCount++
		} else {
			failureCount++
		}
	}

	// 結果を表示
	fmt.Printf("Success: %d, Failure: %d\n", successCount, failureCount)
}
```
リクエストの成功、失敗それぞれを最後に出力しています。

### 100回
まずは100回送ってみます。
Railsから。
```bash
$ time go run app/day18/main.go
Request Count: 100
URL: http://localhost:3000
Success: 100, Failure: 0

real	0m4.582s
user	0m0.315s
sys	0m0.301s
```

次にGoです。
```bash
$ time go run app/day18/main.go
Request Count: 100
URL: http://localhost:3002
Success: 100, Failure: 0

real	0m0.395s
user	0m0.285s
sys	0m0.290s
```
どちらもすべてのリクエストをさばけていますが、Goのほうが10倍ほど速いですね。

### 200回
Rails
```bash
$ time go run app/day18/main.go
Request Count: 200
URL: http://localhost:3000
Success: 128, Failure: 72

real	0m5.877s
user	0m0.329s
sys	0m0.302s
```

Go
```bash
$ time go run app/day18/main.go
Request Count: 200
URL: http://localhost:3002
Success: 128, Failure: 72

real	0m0.549s
user	0m0.333s
sys	0m0.349s
```

ともに半分弱のリクエストが失敗となりましたが、速度差は変わらず。

### 500回
Rails
```bash
time go run app/day18/main.go
Request Count: 500
URL: http://localhost:3000
Success: 135, Failure: 365

real	0m6.139s
user	0m0.373s
sys	0m0.358s
```
Go
```bash
$ time go run app/day18/main.go
Request Count: 500
URL: http://localhost:3002
Success: 133, Failure: 367

real	0m0.626s
user	0m0.367s
sys	0m0.351s
```
傾向は変わらず。
成功、失敗はどちらも大差がないので、どうやらメモリやCPUの性能によって決まりそうだということがわかりますね。(どちらもDockerで2GBのメモリを指定しています)


## まとめ
| Framework | Request Count | URL               | Success | Failure | Real Time | User Time | Sys Time |
|-----------|---------------|-------------------|---------|---------|-----------|-----------|----------|
| Rails     | 100           | http://localhost:3000 | 100     | 0       | 0m4.582s  | 0m0.315s  | 0m0.301s |
| Go        | 100           | http://localhost:3002 | 100     | 0       | 0m0.395s  | 0m0.285s  | 0m0.290s |
| Rails     | 200           | http://localhost:3000 | 128     | 72      | 0m5.877s  | 0m0.329s  | 0m0.302s |
| Go        | 200           | http://localhost:3002 | 128     | 72      | 0m0.549s  | 0m0.333s  | 0m0.349s |
| Rails     | 500           | http://localhost:3000 | 135     | 365     | 0m6.139s  | 0m0.373s  | 0m0.358s |
| Go        | 500           | http://localhost:3002 | 133     | 367     | 0m0.626s  | 0m0.367s  | 0m0.351s |

- echoの方がRailsより10倍速い
- 耐アクセス性能は言語による差分はなく、メモリやCPUの性能に依存すると思われる

速く捌ければそれだけたくさん処理できると思っていましたが、そういうわけでもないようです。

今回はDBへアクセスしていないので、純粋なhttpサーバーの性能比較となりました。
もしDBへアクセスするようにした場合、ORMの性能も影響してきます。
Activerecordは素晴らしいですが、実行速度はお世辞にも速いとは言えません。
なので、Webアプリケーションとしての性能差はもっと開くのではないかと思います。

### おまけ
Chat GPTに検証結果の標準出力を与えてグラフを作成してもらいました。
(こうしてみてみるとリクエスト数200がこの環境の限界のようですね)

![ea3ad4ab-3c68-4b55-9aea-baaaa18f2f8d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473706/52e1bd34-6ea6-cee1-efe7-1acdbb094280.png)