Ruby(Rails) VS Go(echo)
RailsとGo製サーバーの耐アクセス性能を比べてみようと思います。
それぞれDockerコンテナでサーバーを建てて、どれほどのアクセス量まで耐えられるのか検証します。
理想はEC2上などでやりたかったですが、本検証は手元のMacで行っております。
Rails Serverを建てる
apiモードでrails new
rails new rails_app --api
コントローラーだけ作ります。
rails g controller posts
class PostsController < ApplicationController
def index
render json: {"status": "ok"}
end
end
ルーティング
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
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"]
ビルド
docker build -t rails_server:latest .
起動
docker run -p 3000:3000 rails_server:latest
アクセスしてみます。
$ curl localhost:3000
{"status":"ok"}
echoサーバーを建てる
お次はechoサーバーです。
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"]
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)
}
ビルド
docker build -t echo_server_demo:latest .
起動
docker run -p3002:1323 echo_server_demo:latest
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.11.3
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
O\
検証用プログラム
並行処理を用いてリクエストを送るプログラムです。
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から。
$ 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です。
$ 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
$ 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
$ 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
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
$ 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がこの環境の限界のようですね)