Heroku では通常 Web サーバと Worker プロセスでそれぞれ異なる Dyno を起動する必要があり、それぞれで課金されます。
個人用途レベルのツールであれば Dyno を 2 つも立ち上げるのはもったいないと感じることでしょう。
Ruby では EventMachine を使うことによって 1 Dyno で済ませるやり方が知られています。
HerokuのSinatraにバックグラウンドワーカーを詰め込んで節約
Golang においては goroutine をつかうことによって、より自然に実現することができます。
(まぁ Golang においては当たり前の話なので、Golang を使ったことない人に、Golang だとこういうことができる!という感じに読んでもらえればと思います。)
コード例
以下は Worker では 10 秒おきに数値をカウントアップし、HTTP サーバではその数値を出力するだけの例です。
package main
import (
"fmt"
"log"
"net/http"
"os"
"sync/atomic"
"time"
)
var count uint64
// 10 秒おきに数値をカウントアップし続けるだけの関数
func countUpdater() {
for {
atomic.AddUint64(&count, 1)
time.Sleep(10 * time.Second)
}
}
func main() {
port := os.Getenv("PORT")
if port == "" {
log.Fatal("$PORT must be set")
}
// カウントアップ用の goroutine (Worker) を起動
go countUpdater()
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "Count: %d\n", count)
})
// HTTP サーバを起動
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
実行例
数値が 10 秒置きに増えていくことが確認できると思います。
活用例
睡眠時間を避けて Keep-Alive する
Dyno の種類にもよりますが、Heroku では一定時間リクエストがないと Dyno がスリープ状態に入ります。
ですが、Worker (goroutine) 内で自信を一定時間おきに GET するようにすればスリープしないようになります。
(ここではそれを Keep-Alive と表現しています)
ですが、Heroku は 2015 年から料金プランが改定され、無料プランの場合は 1 日に Dyno が 6 時間はスリープ状態となるようになりました。
なので、例えば睡眠時間を避けた時間帯のみ Keep-Alive するようにすれば、自分がおきている時間は快適にアプリを利用できるし、Worker も動き続けることになります。
もちろん、アプリの用途によっては寝ている時間に動いていて欲しいものもあるとは思いますが、その辺はうまく調整しましょう。
重い処理を Worker で行う
まぁ Worker の使い方としては普通ですね。
クローラを Worker として動かし、そのフロントエンドとして HTTP サーバを動かす、といった感じがよくあるんじゃないでしょうか。
注意点
Dyno は再起動されることを念頭におく
デプロイ時もそうですし、スリープ後の起動時もそうです。
(それとらとは関係なしに一定時間の再起動も行われていたかも)
なので、インメモリの変数は再起動時にクリアされてしまいます。
上記のコード例がまさにそうですね。
永続化は Heroku Postgres や Heroku Redis などの永続化層を用意しましょう。
柔軟なスケールアップは難しい
例えば Dyno を 2 つにした時点で HTTP サーバも Worker も 2 つになります。
HTTP だけ 2 つにして Worker は 1 つでいい、といったことを実現するのであれば、普通にプロセスごとに Dyno を割り当てていくのが良いでしょう。
あくまでも個人的なレベルのツールを節約して使いたい時用ということで。