各種 Web Application Framework メモリ使用量比較調査 (Go, Ruby, Python, Node.js, Java)

## 概要

- 「Hello World な HTML を返す程度の Web アプリケーション」または「それぞれの公式ドキュメントに載っているチュートリアルの初期状態に近いもの」を作成してメモリ使用量を比較する
- Web アプリケーションサーバは現時点でよく使われていると思われるものを使用する
- テンプレートエンジンはフレームワークのデフォルト設定またはフレームワークが推奨しているものを使用する
- macOS Catalina 上でメモリ使用量を計測
- メモリ使用量の計測には ps コマンドの RSS 値を使う (複数のプロセスを使うものはRSS値を単純に足したものをメモリ使用量とした。メモリ共有部分については考慮していないため、もう少しメモリ使用量が少ないかもしれない)

## 調査結果

メモリ使用量が少ない順に並べる。

- 7MB: Go 1.13 + Gin 1.5
- 10MB: Go 1.13 + Revel 0.21
- 24MB: Ruby 2.7 + Sinatra 2.0 + Puma 4.3
- 40MB: Python 3.8 + Flask 1.1 + Gunicorn 20.0
- 42MB: Node.js 12.14 + Express 4.16
- 51MB: Python 3.8 + Django 3.0 + Gunicorn 20.0
- 80MB: Ruby 2.7 + Ruby on Rails 6.0 + Puma 4.3
- 167MB: Java 11.0 + Spring Boot 2.2 + Tomcat Embed Core 9.0

## 計測環境の構築と計測

### Go 1.13 + Gin 1.5

- [Gin Web Framework](https://gin-gonic.com/)
- 構成: Go 1.13.5 + Gin 1.5.0

```main.go
package main

import (
  "net/http"
  "github.com/gin-gonic/gin"
)

func main() {
  router := gin.Default()
  router.LoadHTMLGlob("templates/*")
  router.GET("/hello", func(c *gin.Context) {
    c.HTML(http.StatusOK, "hello.tmpl", gin.H{})
  })
  router.Run(":8080")
}
```

```templates/hello.tmpl
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hello, World!</title>
</head>
<body>
Hello, World!
</body>
</html>
```

```
$ go build main.go
```

```
$ GIN_MODE=release ./main
```

```
$ curl http://localhost:8080/hello
```

```
$ ps au
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
hoge 30713   0.0  0.1  4405168   7220 s000  S+   12:04AM   0:00.03 ./main
```

### Go 1.13 + Revel 0.21

- [Welcome to Revel, the Web Framework for Go\!](https://revel.github.io/)
- 構成: Go 1.13.5 + Revel 0.21.0

[Creating a new Revel application \| Revel \- A Web Application Framework for Go\!](https://revel.github.io/tutorial/createapp.html)

```
$ revel new myapp
```

[Revel tool \| Revel \- A Web Application Framework for Go\!](https://revel.github.io/manual/tool.html)

```
$ revel package -a myapp -m prod
```

[Deployment \| Revel \- A Web Application Framework for Go\!](https://revel.github.io/manual/deployment.html)

```
$ tar zxvf myapp.tar.gz
```

```run.sh
#!/bin/sh

SCRIPTPATH=$(cd "$(dirname "$0")"; pwd)
"$SCRIPTPATH/myapp" -importPath myapp -srcPath "$SCRIPTPATH/src" -runMode prod
```

```
$ ./run.sh 
Revel engine is listening on.. localhost:9000
```

```
$ curl http://localhost:9000/
```

```
$ ps au
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
hoge 32777   0.0  0.1  4405468   8928 s000  S+   12:32AM   0:00.04 /Users/hoge/go/myapp
hoge 32771   0.0  0.0  4288312   1048 s000  S+   12:32AM   0:00.01 /bin/sh ./run.sh
```

### Ruby 2.7 + Sinatra 2.0 + Puma 4.3

- [Sinatra](http://sinatrarb.com/)
- 構成: Ruby 2.7.0 + Sinatra 2.0.8.1 + Erb + Puma 4.3.1

```myapp.rb
require 'sinatra'

get '/hello' do
  erb :hello
end
```

```hello.erb
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hello, World!</title>
</head>
<body>
Hello, World!
</body>
</html>
```

```
$ APP_ENV=production RACK_ENV=production ruby myapp.rb
== Sinatra (v2.0.8.1) has taken the stage on 4567 for production with backup from Puma
Puma starting in single mode...
* Version 4.3.1 (ruby 2.7.0-p0), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: production
* Listening on tcp://0.0.0.0:4567
```

```
$ curl http://localhost:4567/hello
```

```
$ ps au
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
hoge 99121   0.0  0.3  4378332  24540 s002  S+   10:18PM   0:00.43 puma 4.3.1 (tcp://0.0.0.0:4567)
```

### Python 3.8 + Flask 1.1 + Gunicorn 20.0

- [Flask \| The Pallets Projects](https://palletsprojects.com/p/flask/)
- 構成: Python 3.8.0 + Flask 1.1.1 + Jinja2 2.10.3 + Gunicorn 20.0.4

```myapp.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route("/hello")
def hello():
  return render_template("hello.html")
```

```templates/hello.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hello, World!</title>
</head>
<body>
Hello, World!
</body>
</html>
```

```
$ gunicorn myapp:app
[2020-01-04 23:55:18 +0900] [29978] [INFO] Starting gunicorn 20.0.4
[2020-01-04 23:55:18 +0900] [29978] [INFO] Listening at: http://127.0.0.1:8000 (29978)
[2020-01-04 23:55:18 +0900] [29978] [INFO] Using worker: sync
[2020-01-04 23:55:18 +0900] [29995] [INFO] Booting worker with pid: 29995
```

```
$ curl http://localhost:8000/hello
```

```
$ ps au
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
hoge 29995   0.0  0.3  4273780  22632 s000  S+   11:55PM   0:00.37 /Users/hoge/.pyenv
hoge 29978   0.0  0.2  4265996  18532 s000  S+   11:55PM   0:00.46 /Users/hoge/.pyenv
```

### Node.js 12.14 + Express 4.16

- [Express \- Node\.js web application framework](https://expressjs.com/)
- 構成: Node.js 12.14.0 + Express 4.16.4 + Pug 2.0.4

[Express application generator](https://expressjs.com/en/starter/generator.html)

```
$ express --view=pug myapp
```

```
$ cd myapp/

$ npm install

$ npm audit fix
```

```
$ node ./bin/www
```

```
$ curl http://localhost:3000/
```

```
$ ps au
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
hoge 19537   0.0  0.5  4565284  43228 s000  S+    9:59PM   0:00.83 node ./bin/www
```

### Python 3.8 + Django 3.0 + Gunicorn 20.0

- [The Web framework for perfectionists with deadlines \| Django](https://www.djangoproject.com/)
- 構成: Python 3.8.0 + Django 3.0.2 + Gunicorn 20.0.4

[はじめての Django アプリ作成、その 1 \| Django ドキュメント \| Django](https://docs.djangoproject.com/ja/3.0/intro/tutorial01/)

```
$ django-admin startproject mysite

$ python manage.py startapp polls
```

mysite/settings.py の設定値の一部を修正。

```
DEBUG = False

ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]']

# Application definition

INSTALLED_APPS = [
    'polls',
```

[はじめての Django アプリ作成、その 3 \| Django ドキュメント \| Django](https://docs.djangoproject.com/ja/3.0/intro/tutorial03/)

```polls/templates/polls/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hello, World!</title>
</head>
<body>
Hello, World!
</body>
</html>
```

```polls/views.py
from django.shortcuts import render

def index(request):
  return render(request, 'polls/index.html')
```

```
$ gunicorn mysite.wsgi:application
[2020-01-04 23:44:32 +0900] [29172] [INFO] Starting gunicorn 20.0.4
[2020-01-04 23:44:32 +0900] [29172] [INFO] Listening at: http://127.0.0.1:8000 (29172)
[2020-01-04 23:44:32 +0900] [29172] [INFO] Using worker: sync
[2020-01-04 23:44:32 +0900] [29191] [INFO] Booting worker with pid: 29191
```

```
$ curl http://localhost:8000/polls/
```

```
$ ps au
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
hoge 29191   0.0  0.4  4283728  34016 s000  S+   11:44PM   0:00.55 /Users/hoge/.pyenv
hoge 29172   0.0  0.2  4265996  18552 s000  S+   11:44PM   0:00.43 /Users/hoge/.pyenv
```

### Ruby 2.7 + Ruby on Rails 6.0+ Puma 4.3

- [Ruby on Rails \| A web\-application framework that includes everything needed to create database\-backed web applications according to the Model\-View\-Controller \(MVC\) pattern\.](https://rubyonrails.org/)
- 構成: Ruby 2.7.0 + Ruby on Rails 6.0.2.1 + Erb + Puma 4.3.1

[Getting Started with Rails — Ruby on Rails Guides](https://guides.rubyonrails.org/getting_started.html)

```
$ rails new blog
$ cd blog
$ rails generate controller Welcome index
```

```app/views/welcome/index.html.erb
Hello, World
```

```config/routes.rb
Rails.application.routes.draw do
  get 'welcome/index'
  root 'welcome#index'
end
```

```
$ RAILS_ENV=production rails assets:precompile
```

```
$ rails server -e production
=> Booting Puma
=> Rails 6.0.2.1 application starting in production 
=> Run `rails server --help` for more startup options
Puma starting in single mode...
* Version 4.3.1 (ruby 2.7.0-p0), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: production
* Listening on tcp://0.0.0.0:3000
```

```
$ curl http://localhost:3000/
```

```
$ ps au
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
hoge 66416   0.0  1.0  4445976  81964 s000  S+    9:42AM   0:02.97 puma 4.3.1 (tcp://0.0.0.0:3000) [blog]
```

### Java 11.0 + Spring Boot 2.2 + Tomcat Embed Core 9.0

- [Spring Boot](https://spring.io/projects/spring-boot)
- 構成: OpenJDK 11.0.2 + Spring Boot 2.2.2 + Spring Web + Thymeleaf 3.0.11 + Tomcat Embed Core 9.0.29

```build.gradle 
plugins {
  id 'org.springframework.boot' version '2.2.2.RELEASE'
  id 'io.spring.dependency-management' version '1.0.8.RELEASE'
  id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
  mavenCentral()
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
  implementation 'org.springframework.boot:spring-boot-starter-web'
}
```

```DemoApplication.java 
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@SpringBootApplication
@Controller
public class DemoApplication {

  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }

  @GetMapping("/hello")
  public String hello() {
    return "hello";
  }
}
```

```hello.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Hello, World!</title>
</head>
<body>
Hello, World!
</body>
</html>
```

```
$ gradle bootJar
```

Java は起動直後にメモリを大きく確保してしまうので、最小ヒープサイズに32MBを指定する。

```
$ java -Xms32m -jar build/libs/demo-0.0.1-SNAPSHOT.jar
```

```
$ curl http://localhost:8080/hello
```

```
$ ps au
USER   PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
hoge 20294   0.0  2.0  8094612 171024 s000  S+   10:09PM   0:17.35 java -Xms32m -jar build/libs/demo-0.0.1-SNAPSHOT.jar
```