日記

のみろぐ

主に競プロ日記です

Future Contestを無料運用にした

まえがき

  • Future Contestとは?→これ
  • 初めてWebサイト作ってみたで作ったサイトを無料運用にしたのでログを取っておく。
  • ただのメモなので技術的なことは詳しく書いてない
  • 時系列順に書いてく
  • たぶんめっちゃアホみたいなことやってる

使ってるもの

クラウド

  • GAE

言語

  • Go

アーキテクチャ

  • 最終的には以下のようなアーキテクチャになった。
  • 5分に一度コンテストサイトのAPIを叩いてクラウドストレージに保存する。
  • クライアントからリクエストが来たら、クラウドストレージからコンテストデータを読み出して整形し、HTMLとしてクライアントに返す。
  • 正確にはAtCoderのコンテストページをスクレイピングして別にAPIサーバーを立てているが、ココにはそれは書いてない。

f:id:nomikura:20181101214919p:plain

やったことを簡潔に

  • GAEが動く環境は、スタンダード環境とフレキシブル環境の2つがある。僕はそれをしらず、ずっとフレキシブル環境で動かしていた。
  • 無料枠が使えるのはスタンダード環境だけなので、スタンダード環境に乗り換えた。
  • 変更点は大体以下の通り

  • app.yamlをスタンダード環境用に書き換えた

  • スタンダード環境で動く用のコードに書き換えた
  • cron.yamlを使った。

お金がやばくなる

f:id:nomikura:20181101214940p:plain

  • ぎゃぁぁぁぁぁぁぁぁ!!!!!!
  • 44日で15000円くらい消費されてる
  • 無料で3万使えるとは言え、こうも早く消費されるとつらい。
  • GAEはこれからも使いたいので無料で運用する方法を知っておきたい

無料運用する方法をググってみる

  • ググると、こんな感じの記事が出てくる。なんか良さげなので、とりあえずこの設定をapp.yaml(設定ファイル)に書き込んでデプロイしてみる。ちゃんとは覚えてないけど、そのときのapp.yamlは以下のような感じ(上手く動きません)
runtime: go
env: flex

threadsafe: true

automatic_scaling:
  min_idle_instances: automatic
  max_idle_instances: 1
  min_pending_latency: 3000ms
  max_pending_latency: automatic
  • でも、デプロイできない。「min_idle_instancesとかいうパラメータは存在しないよ」って怒られる。他のパラメータも怒られてしまう。

自分でapp.yamlを書いてみる

  • デプロイしようとすると、パラメータの名前自体が怒られるので、app.yamlの書き方がおかしいんだろうなーと思った。なのでapp.yamlの書き方をググった。
  • すると、app.yamlによるアプリ構成という記事が見つかった。公式の記事はすごいわかりやすかったので、これを元に設定ファイルを書くことにした。
  • Google Cloud Platformの無料枠によると、とりあえず1日28インスタンス時間を守れば無料運用できそうだなーと思った(ストレージは5GBも使わないし、メールも使わないため)。ただ、「1 日あたり 1,000 回の検索オペレーション、10 MB の検索インデックス作成」というのはよく分からなかったので無視した。
  • インスタンス時間とは、全てのインスタンスが動いている合計時間のことである。1つのインスタンスを24時間動かすと、それは24インスタンス時間になる。なので、とりあえず1つのインスタンスだけ動かせば無料枠からはみ出ることは無い(弊害はありそうだが)
  • というわけで、app.yamlを以下のように書き換えた(正確には覚えてないけど確かこんな感じ)。この設定でインスタンスが勝手に1台以上立つことはない。完璧だと思ってた。
runtime: go
env: flex

automatic_scaling:
  min_num_instances: 1
  max_num_instances: 1

お金が減り続ける

  • 残り 320 日で、¥17,017です
  • 残り 319 日で、¥16,420です
  • 残り 317 日で、¥16,214です
  • 残り 316 日で、¥16,011です

  • 残り 314 日で、¥15,393です

  • 残り 313 日で、¥15,392です
  • ...
  • うわああああああああああああああああああ!!!!!
  • なぜかお金が減り続ける。
  • インスタンスの状況を確認しても、1つしか立っていない。なので、お金が消費されるはずが無い。
  • 「1 日あたり 1,000 回の検索オペレーション、10 MB の検索インデックス作成」という謎の項目もあんまり関係なさそう
  • なので、料金履歴を見てみることにした。最初からそうしていればいいじゃんって感じだけど、料金表みても何にお金使ったかが分かりづらい。
  • とりあえず、料金表を調べてみた。料金表の内容をコピペしてググると、Stack Overflowの記事が見つかった。
  • 記事によると、「App Engineのバージョンが複数あることに原因がある」みたいなことを言ってた。以下の画像では「11件のバージョン」と表示されている。これが無駄らしい。なので、最新バージョン以外を消してみた。はい、一件落着。

f:id:nomikura:20181101214958p:plain

お金が減り続ける2

  • 状況は以前と変わらず、お金が減り続ける。うーん、つらい
  • わけ分からん部分で8000円くらい吹っ飛んでたりして、笑えてくる。
  • 次は、料金表の怪しい部分に焦点を当てて調べてみた。
  • その結果、料金は以下のように使われていることがわかった。(英語版の料金一覧ページで「Core Hours」という単語が見つかったので、この辺が怪しそう)

f:id:nomikura:20181101215018p:plain

  • そこで、日本語版の料金ページの表を参考に、料金を計算してみた。

  • すると、 以下のように対応することがわかった。計算してこの3つが対応することを確認した。

料金表の文章 リソース
App Engine Flex Instance RAM Japan メモリ
App Engine Flex Instance Core Hours Japan vCPU
Compute Engine Storage PD Capacity in Japan 永続ディスク
  • 実際に見た料金表は、以下の表である。これを見て分かるんだけど、「フレキシブル環境で動く」みたいなことが書いてある。何も知らずに今まで動かしてきたため、フレキシブル環境とは???と思った。そして、色々調べた結果、フレキシブル環境は無料枠が無いことが判明した!!!(うーん、これはひどい

f:id:nomikura:20181101215047p:plain

GAEの動作環境

  • 料金表のページに普通に書いてあったんだけど、GAEの動作環境は「スタンダード環境」と「フレキシブル環境」があるみたい。
  • スタンダード環境では無料枠が使えて、フレキシブル環境では無料枠が使えない。
  • 僕は今までそれを知らずにフレキシブル環境で動かしていた。元々クイックスタートを参考にして、これをベースに進めていた。そして、このページのタイトルに大きく書かれているがこれは「フレキシブル環境でのGoのクイックスタート」だった。うーん、このミスはひどい。
  • というわけで、今フレキシブル環境で動かしている者をスタンダート環境で動かせば良いことがわかった。

スタンダード環境を試してみる

  • スタンダード環境で動かすために必要なことを調べた結果、app.yamlをスタンダード環境用に書き換えれば良いことがわかった。なーんだ、簡単じゃん!!
  • GAEでGo言語製ウェブサーバーのためのapp.yamlの基本的な書き方というブログを参考にした。この記事を見ると、app.yamlenv: standardと書き込めばいいっぽい。けど、これを書き込んでデプロイしても怒られる。このあと色々してもよくわかんなくなった。
  • スタンダード環境のクイックスタートがあったので、それを参考にした。このページに載ってるHelloWorldのコードをコピペしてデプロイしたらちゃんと動いた。
  • コンソールからバージョンを見ると、「標準」と表示されるようになった。いままでは「フレキシブル」だった。これはスタンダード環境で動いているということだろう。

Future Contestをスタンダード環境に切り替える

  • 先ほどのHello, worldのプロジェクトで使ったapp.yamlをそのままFuture Contestで使ってデプロイしてみた。
  • デプロイには成功するが、サイトを見ても「500 (何かしらのメッセージ)」が表示される。ステータスコード500は、サーバー側にエラーが起きたことを表す(このとき知った)。なので、GAEのエラーログを見た。
  • すると、該当箇所が見つかった。GETリクエストをしている部分だった。
  • 調べたところ、Issuing HTTP(S) Requests)という公式ドキュメントが見つかった。これによると、スタンダード環境では以下のようにGETリクエストを送る必要があることがわかった。
func handler(w http.ResponseWriter, r *http.Request) {
        ctx := appengine.NewContext(r) // これを作る必要がある
        client := urlfetch.Client(ctx)
        resp, err := client.Get("https://www.google.com/")
        if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
        }
        fmt.Fprintf(w, "HTTP GET returned status %v", resp.Status)
}
  • 該当部分を書き換えたが、まだエラーは消えなかった。次に出たエラーは、ファイル読み書きの部分だった。どうやらスタンダート環境では通常の方法でファイル読み書きをする事はできないようだ。
  • 調べたところ、Google Cloud Storageを使うと良いことがわかった。5GBが無料で使えるので、安心して使えそうだ。
  • 大体の使い方はGitHubに載っていた。これを参考に該当箇所を変更したところ、いつも通りサイトが動いてくれた(クラウドストレージで結構手こずったけど)

定期的に更新する

  • このサイトは、各コンテストサイトから5分に一度情報を取得する。
  • 今まではこの部分をゴルーチンを利用して関数呼び出していたが、スタンダード環境だとそれができなさそう。なぜなら、スタンダート環境では以下のようにctxを作らないと外部操作ができない(たぶん)。ctxを作るには*http.Requestの情報が必要であるが、この関数はユーザがこのサイトにアクセスしたときにしか呼び出されない。
func handler(w http.ResponseWriter, r *http.Request) {
        ctx := appengine.NewContext(r) // これを作る必要がある
}
  • 解決方法としては、5分に1回このサイトにアクセスすれば良いのでは?と思う。調べてみると、cron.yamlというファイルにそういった設定を書き込めるようだ。cron.yamlリファレンスがあるので、それを参考にして書いた。
  • これで大丈夫だろうと思ってデプロイしたが、5分に1回呼び出す処理が出来ていない。これは罠なんだけど、gcloud app deployではcron.yamlはデプロイされない。gcloud app deploy cron.yamlというコマンドを打ってようやくcron.yamlがデプロイできるようだ。
  • このコマンドを打ったらちゃんと動いてくれた。

その他

AtCoderAPIサーバーを立てる

  • Future ContestはAPIを叩くだけのシンプルな機能にしたかった。AtCoderスクレイピングしていたので、AtCoderコンテストのAPIを別に用意した。
  • コンテスト用のAPIkenkooooさんが製作したものがあるが、過去のコンテストのみを扱っている。僕は開催予定のコンテストを含めた全てのコンテスト情報が欲しかったため、自前で用意した。
  • フレームワークを使った方が楽そうだったので、Ginというフレームワークを使った。これもフレキシブル環境とスタンダート環境では書き方が少し違う。
  • Ginを用いたHello world(スタンダード環境)を参考にコードを書いた。
  • 通常のnet/httpで書くハンドラとGinで書くハンドラは少し違う。違いは以下のようである。スタンダード環境では*http.Requestの情報が必要なため、それを取得するコードを書く必要がある。w, rの取得方法はIssueに載ってた(もっとわかりやすいページがあるのかも知れないけど)
// 通常のハンドラの書き方
func handler(w http.ResponseWriter, r *http.Request) {
        ctx := appengine.NewContext(r)
}

// Ginでのハンドラの書き方
func handlerGin(c *gin.Context) {
  // 以下で通常net/httpで使っていた値を取得できる
  var w http.ResponseWriter = c.Writer
  var r *http.Request = c.Request
}

ページがプレーンテキストとして表示される

  • ページがHTMLとして表示されず、プレーンテキストとして表示されたしまった。他のサイトのレスポンスを見たところ、レスポンスヘッダにcontent-type: text/htmlが付いていることがわかった。このときのFuture Contrstはcontent-type: text/planeであった。なので、content-type: text/htmlに設定したらページがHTMLとして表示された。
  • GinでHTMLテンプレートを扱えるらしいんだが、僕の場合は上手く動かなかった。なので、通常のテンプレートエンジンを用いた。

ページが更新されない

  • キャッシュが消えず、更新しても同じページが表示される現象が起きた。ブラウザのキャッシュを消すスーパーリロードを試したがダメだった。原因はGAE側のキャッシュにあった。GAE側のキャッシュを消すことでこの問題は解決した。
  • キャッシュが追加されたのは、このブログを参考にして書いたcache-control: public, max-age=3600が原因だと思う。これをレスポンスヘッダにくっつけるとキャッシュが使われるそう。これを消したらキャッシュは使われなくなった。
  • キャッシュは高速な処理を可能にする技術ではあるが、ページが更新されないのは面倒なのでキャッシュは使わないことにした。上手い使い方があるのかも知れないが,よく分からない。

結果

  • 無料運用できたー!わーい!
  • それなりに時間をかけてしまったけど、これが今の実力ということで... f:id:nomikura:20181101220419p:plain

感想

  • 個人のブログは問題解決の足がかりにはなる。しかし、初めて読むにしては内容が不十分な場合がある。なので、結局公式ドキュメントを見た方が早いことがわかった。
  • ただ、公式ドキュメントは分量が多くて全てに目を通すのは大変である。なので、ググって出てきたブログが参考にしている公式ドキュメントを見るのが最適かなぁとか思ってる。調べ方に関してはよく分からん...
  • かなり遠回りをしてしまった気がする
  • 今回はパワポアーキテクチャかいた。draw.ioをちょっと触ってみたが、すごい使いやすかった。最初からこれ使えば良かった...

これから

  • GitHubのREADMEを書き直す。ライセンス(MITみたいな)と、使用方法。みんな以外と使いたがるらしいので書くつもり。
  • CodeforcesJSONがたまに取得できない。なので、その辺をなんとかしたい
  • GAEでは無料でDBが使えない。herokuだと無料でDBが使えるみたいなので、次はherokuを使ってみたい