PipenvとDocker Compose
最近Djangoのプロジェクトをつくっているのですが、docker-compose up
で立ち上がるようにしておこうとググったところ、なんかイマイチじゃない?ってのが多く出てきたので、自分なりのやつを書いておきます。
Dockerfile
FROM python:3.7-slim RUN apt-get update -qq ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 ENV PIPENV_VENV_IN_PROJECT 1 WORKDIR /app RUN pip install pipenv
docker-compose.yml
version: '3.7' services: db: image: postgres:11.1-alpine volumes: - postgresql:/var/lib/postgresql/data app: build: . command: pipenv run server volumes: - .:/app:cached ports: - 7000:7000 depends_on: - db volumes: postgresql:
Pipfile
[[source]] name = "pypi" url = "https://pypi.org/simple" verify_ssl = true [dev-packages] [packages] [requires] python_version = "3.7" [scripts] server = "python manage.py runserver 0.0.0.0:7000" test = "python manage.py test"
ポイントとしてはPIPENV_VENV_IN_PROJECT=1
にして、.venv
をDocker Composeのvolumesに入るようにするとこ。ググって出てきたイマイチっぽいやつは、Dockerfileの中でPipfileとPipfile.lockをCOPY
してpip install
をやっちゃうやつ。これだと新しい依存を追加するたびにbuildしないといけないんじゃないのって感じでお見送り。
ハチャメチャに速いサイトをつくりたい
ということを思って、もうだいぶ前だけどhttps://surisuri.ninjaっていうのをつくった。半端になってるページもあるけど、やりたいことはざっとできたので趣味としては満足。Herokuで動いてます。ソースはこちら。いま仕事で開発してるサービスの公開APIで本家をマネする企画がことの始まり。
やったこと
CDNのエッジキャッシュを使う
まず前提として、HTMLは全部CDNにキャッシュする。後述するように動的に変わるコンテンツはキャッシュに載せなかったので、そういうコンテンツは表示されなかったとしても、ヘッダとか静的な部分まで表示されず画面真っ白というのはハチャメチャに速いとは言えない。なのでオリジンのサーバはなるたけコンテンツを載せたtext/htmlを返すようにする。ほぼ空っぽのHTMLに<script>
がぽつんとあるSPAみたいにはしない。
APIとフロントを分割する
いわゆるモノリシックなWebアプリにはしなかった。JSON APIとフロントが分かれていたほうが個人的に好みだし、チームで開発する場合も適していると思うのでそうした。ただそうなるとtext/htmlをつくるのはフロント側になるので、SSRする仕組みが必要になる。SSRをサポートしてくれるフレームワークはNuxtとかNextとかこの頃興味を持っていたSvelteのSapperなどがあるけど、Nextはpathパラメータの扱いが苦手そう?に見えたのでやめて、Sapperは後述するaタグでの遷移ができなかったのでやめてNuxtを選んだ。仕事でもVueを使っていて手に馴染んでるのでちょうどよかった。
普通のaタグを使う
普通じゃないaタグって何だよって感じだけど、SPA的な画面遷移をしないということ。リンクをタップしてからAPI叩いて必要な情報が揃ってから画面書き換え、ということをするよりも、CDNのエッジキャッシュに載ってるHTMLを取ってくる方が速いんじゃねーのと考えた。もちろん代替案はいくつも考えられて、
など。前者はちょっと記憶が曖昧だけど、Vueのmounted
を使えば遷移してからAPIを叩けるけど、そのページをSSRしたときにコンテンツが載ってこない。だからと言ってNuxtのasyncData
やfetch
を使うと、こいつらは遷移前に呼ばれるので、リンクをタップしてから遷移するまで時間がかかってしまう。そんな具合にサーバサイドとクライアントサイド両方でいい感じに叩く手段がなかったので、サーバサイドに振り切ったということ。後者は無駄な通信が発生する可能性がありギガに優しくないなと思ったのでやめた。
その他
CSSはheadタグの中にstyleタグで書いてCSSOMの準備が早くできるようにするとか、assetなどURLと内容が対応してて変化しないものはService WorkerでNetwork Firstなキャッシュをしてオフライン対応するとか、いくつか考えていたけど、この辺はNuxtがやってくれちゃったので頑張った感じはない。詳しくはhttps://github.com/nuxt-community/pwa-moduleなど参照のこと。
やらなかったこと
動的なコンテンツのキャッシュはしなかった。CDNによってはクッキーの値をキャッシュキーに含めることができるらしいけど、こちらが意図していない、また把握できないキャッシュをつくられる可能性があるのでやめた。なのでユーザごとのコンテンツやユーザによって作成・更新されるものはブラウザからAPIを叩いて取得する。dev.toなんかはそういうコンテンツもキャッシュに載せていて、ユーザによって更新されたタイミングでCDNのキャッシュを消している。なので絶対無理ってわけでは無いのだけど、これはこれで大変なのでやらなかった。
まとめ
そんなところで、趣味レベルだけど一度キャッシュに載ってくれればそこそこ速いものはできた。ただ例えばトップは時間によって内容が変わるので3時間でキャッシュ消すようにしてるので、キャッシュ切れててHerokuのdynoが寝てるときの描画はかなり遅いし、やっぱりAPI叩くところはどうしてもモッサリするので、もうひと押しって感じですね。
Ectoのカスタムロガー
3.0-rc.1がもっぱら話題のEctoですが、ロガーを追加する方法をたまたま見つけたので書いておきます。Ecto.Repo
をuse
するときにloggers
っていうオプションを渡します。
defmodule MyApp.Repo do use Ecto.Repo, otp_app: :my_app, loggers: [ {Ecto.LogEntry, :log, []}, {MyApp.CustomLogger, :log, []} ] end
loggers
のデフォルト値は[{Ecto.LogEntry, :log, []}]
なので、今の挙動を変えたくないときはこんな風に追加するとよさそ。試しにこんなlog/1
で動かしてみると、
defmodule MyApp.CustomLogger do def log(entry) do IO.inspect entry, structs: false end end
[debug] QUERY OK source="nippoes" db=8.3ms SELECT n0."id", n0."date", n0."content", n0."user_id", n0."inserted_at", n0."updated_at" FROM "nippoes" AS n0 WHERE (n0."date" = $1) ORDER BY n0."inserted_at" DESC [{2018, 10, 17}] %{ __struct__: Ecto.LogEntry, ansi_color: :cyan, caller_pid: #PID<0.599.0>, connection_pid: nil, decode_time: 11000, params: [{2018, 10, 17}], query: "SELECT n0.\"id\", n0.\"date\", n0.\"content\", n0.\"user_id\", n0.\"inserted_at\", n0.\"updated_at\" FROM \"nippoes\" AS n0 WHERE (n0.\"date\" = $1) ORDER BY n0.\"inserted_at\" DESC", query_time: 8328995, queue_time: 68000, result: {:ok, %{ __struct__: Postgrex.Result, columns: ["id", "date", "content", "user_id", "inserted_at", "updated_at"], command: :select, connection_id: 15165, num_rows: 0, rows: [] }}, source: "nippoes" }
こんなログが出ました。引数はEcto.LogEntry
の構造体ですね。query_time
(ナノ秒)を見て、時間かかってたらLogger.warn
したりとか良いんじゃないでしょうか。本番環境だとNewRelicだなんだありますけど、開発環境とかテストとかで。