This article was contributed by The MemCachier Add-on
MemCachier manages and scales clusters of memcache servers so you can focus on your app. Tell us how much memory you need and get started for free instantly. Add capacity later as you need it.
follow @MemCachier on Twitter
Memcache を使用した Gin アプリケーションのスケーリング
最終更新日 2020年06月12日(金)
Table of Contents
Memcache は、Web アプリとモバイルアプリバックエンドのパフォーマンスとスケーラビリティを改善する技術です。ページの読み込みが遅すぎる場合や、アプリにスケーラビリティの問題がある場合は、Memcache の使用を検討してください。小規模なサイトであっても、Memcache の導入によってページの読み込みを高速化し、将来の変化にアプリを対応させることができます。
このガイドでは、単純な Gin Gonic アプリケーションを 作成して Heroku にデプロイし、Memcache を追加してパフォーマンスのボトルネックを軽減する方法を示します。
前提条件
このガイドの手順を完了する前に、以下のすべての条件を満たしていることを確認してください。
- Go の知識がある (Gin の知識もあれば理想的です)
- Heroku ユーザーアカウント (無料ですぐにサインアップ)
- 「Heroku スターターガイド (Go)」の手順を理解している
- Go、govendor、Heroku CLI がコンピュータにインストールされている
GOPATH
環境変数が設定されていることの確認
Heroku への Gin アプリケーションのデプロイ
Gin は、最小限を指向し、アプリケーションスケルトンを必要としないフレームワークです。
次のように、Go アプリを作成し、依存関係として github.com/gin-gonic/gin
を追加するだけです。
$ cd $GOPATH/src
$ mkdir gin_memcache
$ cd gin_memcache
$ govendor init
$ govendor fetch github.com/gin-gonic/gin@v1.2
Gin フレームワークをインストールしたので、アプリのコードを追加できます。訪問者が送信した数よりも小さい最大の素数を計算するページを作成します。
main.go
を作成し、次のコードを貼り付けます。
package main
import (
"net/http"
"os"
"strconv"
"github.com/gin-gonic/gin"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
router := gin.New()
router.Use(gin.Logger())
router.LoadHTMLGlob("templates/*.tmpl.html")
router.Static("/static", "static")
router.GET("/", func(c *gin.Context) {
n := c.Query("n")
if n == "" {
// Render view
c.HTML(http.StatusOK, "index.tmpl.html", nil)
} else {
i, err := strconv.Atoi(n)
if err != nil || i < 1 || i > 10000 {
// Render view with error
c.HTML(http.StatusOK, "index.tmpl.html", gin.H{
"error": "Please submit a valid number between 1 and 10000.",
})
} else {
p := calculatePrime(i)
// Render view with prime
c.HTML(http.StatusOK, "index.tmpl.html", gin.H{"n": i, "prime": p})
}
}
})
router.Run(":" + port)
}
// Super simple algorithm to find largest prime <= n
func calculatePrime(n int) int {
prime := 1
for i := n; i > 1; i-- {
isPrime := true
for j := 2; j < i; j++ {
if i%j == 0 {
isPrime = false
break
}
}
if isPrime {
prime = i
break
}
}
return prime
}
次に、対応するビューを追加しましょう。templates/index.tmpl.html
ファイルを作成し、
次のコードを貼り付けます。
{{ define "index.tmpl.html" }}
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<title>Gin caching example</title>
</head>
<body>
<div class="container">
<h1>
Gin caching example
</h1>
<p class="lead">For any number N (max 10000), we'll find the largest prime number
less than or equal to N.
</p>
<!-- Form to submit a number -->
<form class="form-inline" action="/">
<input type="text" class="form-control" name="n" />
<input type="submit" class="btn btn-primary" value="Find Prime" />
</form>
<hr>
<!-- Show the result -->
{{ if .prime }}
<div class="alert alert-primary">
<p class="lead">Largest prime less or equal than {{ .n }} is {{ .prime }}</p>
</div>
{{ end }}
<!-- Error handling -->
{{ if .error }}
<div class="alert alert-danger">
<p class="lead">{{ .error }}</p>
</div>
{{ end }}
</div>
</body>
</html>
{{ end }}
動作するアプリが完成したので、go run main.go
を実行して起動できます。
Heroku 上でアプリを動作させるには、アプリの実行方法を指示する Procfile
を作成する必要があります。
$ echo web: gin_memcache > Procfile
アプリを Heroku にデプロイするには、Git リポジトリでアプリが管理されている必要があります。まず、.gitignore
ファイルを作成します。
$ echo 'vendor/*' > .gitignore
$ echo '!vendor/vendor.json' >> .gitignore
次に、リポジトリを作成してアプリの初期状態をコミットします。
$ git init
$ git add .
$ git commit -m 'Initial gin app'
最後に、Heroku アプリを作成し、そのアプリにコードをプッシュして、実行中のアプリを調べます。
$ heroku create
$ git push heroku master
$ heroku open
キャッシングを Gin に追加する
Memcache はインメモリの分散キャッシュです。そのプライマリ API は、SET(key, value)
と GET(key)
の 2 つの操作で構成されます。
Memcache は、複数のサーバーに分散していますが、操作は一定の時間に実行されるハッシュマップ (または辞書) のようなものです。
Memcache の最も一般的な用途は、コストの高いデータベースクエリや HTML レンダリングをキャッシュし、これらの高コスト操作を繰り返す必要をなくすことです。
Memcache のセットアップ
Gin で Memcache を使用するには、まず実際の Memcache キャッシュをプロビジョニングする必要があります。これは、MemCachier アドオンから無料で簡単に入手できます。
$ heroku addons:create memcachier:dev
これにより、MEMCACHIER_SERVERS
、MEMCACHIER_USERNAME
、MEMCACHIER_PASSWORD
の 3 つの環境設定が Heroku アプリケーションに 追加され、キャッシュに接続できるようなります。
Gin でキャッシュを使用するには、govendor
を使用して mc
をインストールする必要があります。
$ govendor fetch github.com/memcachier/mc
また、main.go
でその設定を行う必要もあります。
package main
import (
// ...
"github.com/memcachier/mc"
)
func main() {
username := os.Getenv("MEMCACHIER_USERNAME")
password := os.Getenv("MEMCACHIER_PASSWORD")
servers := os.Getenv("MEMCACHIER_SERVERS")
mcClient := mc.NewMC(servers, username, password)
defer mcClient.Quit()
// ...
}
// ...
コストの高い計算のキャッシュ
コストの高い計算の結果をキャッシュすることが良い考えであるのには、2 つの理由があります。
- キャッシュから結果を取得した方がずっと高速であり、ユーザーエクスペリエンスも向上します。
- コストの高い計算には多くの CPU リソースが使われ、アプリのその他の部分の動作も低速になる可能性があります。
今回の素数計算では、入力値を 10000 までに制限しているため、実際にはコストの高い計算は発生しません。ただし、チュートリアルの目的のために、素数の計算はコストが高い計算なのでキャッシュしたいと仮定します。
これを実現するために、main.go
で GET
ルートを変更して次の部分を
// ...
p = calculatePrime(i)
// ...
次のように置き換えます。
// ...
key := "prime." + strconv.Itoa(i)
p := 0
// Look in cache
val, _, _, err := mcClient.Get(key)
if err != nil {
// Prime not in cache (calculate and store)
p = calculatePrime(i)
val = strconv.Itoa(p)
mcClient.Set(key, val, 0, 0, 0)
} else {
// Found it!
p, _ = strconv.Atoi(val)
}
// ...
これらの変更を Heroku にデプロイし、いくつかの数値を送信して素数を見つけます。
$ git commit -am 'Add caching'
$ git push heroku master
ページは以前と同じように動作するはずです。ただし、内部では、すでに計算された素数がキャッシュされるようになりました。キャッシュ内で何が起きているかを見るために、MemCachier のダッシュボードを開きます。
$ heroku addons:open memcachier
ダッシュボードでは、素数をリクエストするたびに統計を更新できます。最初に数値を入力すると、get misses
が増加します。それ以降は、同じ数値をリクエストするたびに get hit
が増加するはずです。
レンダリングされたビューのキャッシュ
HTML ビューのレンダリングは概してコストの高い計算なので、可能であれば常に、レンダリングされたビューをキャッシュするべきです。Gin では、gin-contrib/cache
ライブラリを使用してこれを容易に実現できます。govendor
でライブラリを取得します。
$ govendor fetch github.com/gin-contrib/cache
ここで、main.go
で次のようにして、レンダリングされたビューをキャッシュできます。
package main
import (
// ...
"github.com/gin-contrib/cache"
"github.com/gin-contrib/cache/persistence"
// ...
)
func main() {
// ...
mcStore := persistence.NewMemcachedBinaryStore(servers, username, password, persistence.FOREVER)
router.GET("/", cache.CachePage(mcStore, persistence.DEFAULT, func(c *gin.Context) {
// ...
}))
// ...
}
// ...
これはとても簡単で、正しく動作します。しかし、ビューに変更がある場合は注意が必要です。ページに変化がある場合の例として、個々の数値と、それに対して計算される最大素数に 「Like」 (いいね) ボタンを追加してみましょう。index.tmpl.html
ファイルで、計算された素数のすぐ下にこのボタンを配置します。
<!-- ... -->
<!-- Show the result -->
{{ if .prime }}
<div class="alert alert-primary">
<p class="lead">Largest prime less or equal than {{ .n }} is {{ .prime }}</p>
<p>Likes: {{ .likes }}</p>
</div>
<form method='POST'>
<input type="hidden" name="n" value="{{ .n }}" />
<input type="submit" class="btn btn-primary" value="Like!" />
</form>
{{ end }}
<!-- ... -->
ここで、POST
ルートのコントローラーを main.go
に作成し、送信された “いいね” を変数に格納する必要があります。
“いいね” を変数に格納するのは適切ではありません。アプリが再起動するたびに、すべての “いいね” が 消去されてしまうからです。ここでは、便宜上そのようにしているだけです。本番アプリケーションでは、 このような情報はデータベースに保存してください。
// ...
func main() {
// ...
likes := make(map[string]int)
router.POST("/", func(c *gin.Context){
n := c.PostForm("n")
likes[n] += 1
c.Redirect(http.StatusMovedPermanently, "/?n=" + n)
})
router.GET("/", cache.CachePage(mcStore, persistence.DEFAULT, func(c *gin.Context) {
// ...
}))
//...
}
// ...
さらに、"いいね" が HTML
関数に渡されることを確認する必要もあります。
これは GET
コントローラーで行われます。
// ...
// Render view with prime
c.HTML(http.StatusOK, "index.tmpl.html", gin.H{"n": i, "prime": p, "likes": likes[n] })
// ...
ページが変化する場合の問題を実証するために、現在の実装をコミットしてテストしてみましょう。
$ git commit -am 'Add view caching'
$ git push heroku master
数値を送信すると、最大素数に加えてその下に [Like] ボタンが表示されるようになります。しかし、Like! をクリックしても “いいね” のカウントは増加しません。これは、ビューがキャッシュされているからです。
これを解決するには、キャッシュされたビューが更新されるたびにそのビューを無効化する必要があります。
// ...
router.POST("/", func(c *gin.Context){
n := c.PostForm("n")
likes[n] += 1
mcStore.Delete(cache.CreateKey("/?n=" + n))
c.Redirect(http.StatusMovedPermanently, "/?n=" + n)
})
// ...
もう一度 Heroku にデプロイします。
$ git commit -am 'Fix view caching'
$ git push heroku master
“いいね” の数が増えることを確認できるようになりました。
セッションのキャッシング
Heroku では、再起動時に内容が失われる一時的なファイルシステムが dyno に備わっているため、セッション情報をディスクに保存することは推奨されていません。
Memcache は、タイムアウトがある短命セッションの情報を保存するのには適しています。しかし、Memcache はあくまでキャッシュであって永続的ではないため、寿命の長いセッションについては、データベースなどの永続的なストレージオプションの方が適しています。
Gin でセッションを使用するには、gin-contrib/session
が必要です。
$ govendor fetch github.com/gin-contrib/sessions
$ govendor fetch github.com/gin-contrib/sessions/memcached
main.go
での設定は簡単です。
package main
import (
// ...
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memcached"
// ...
)
func main() {
// ...
// add below `router := gin.New()`
sessionStore := memcached.NewMemcacheStore(mcClient, "", []byte("secret"))
router.Use(sessions.Sessions("mysession", sessionStore))
// ...
}
// ...
セッションを自由に使用できるようになりました。Gin でのセッションの使用法についての詳細は、gin-contrib/sessions の README を参照してください。