LCL Engineers' Blog

夜行バス比較なび(高速バス比較)・格安移動・バスとりっぷを運営する LCLの開発者ブログ

Varnish Grace Modeで非同期にキャッシュを更新する

Webエンジニアの森脇です。

先日、Inside Frontend というフロントエンドのイベントがあり、チームメンバーが参加しました。参加メンバーから日経新聞社様の「日経電子版を速くする」について共有をしてもらい、弊社でも活用できそうな点は、取り入れさせて頂くことにしました。

日経電子版を速くする / nikkei-inside-frontend // Speaker Deck

資料の中で、Fastlyの「キャッシュの非同期更新」について紹介されています。弊社では、FastlyではなくVarnishを利用していますが、VarnishのGrace Modeでも同等のことが実現可能のため、今回は簡単な検証を兼ねて紹介します。

失効済みコンテンツの配信 - パフォーマンス・チューニング | Fastly Help Guides

Grace Modeとは

キャッシュ有効期間(TTLで設定された期間)が過ぎた場合に置いても、 grace期間はキャッシュからレスポンスを返し、バックグラウンドでフェッチし、キャッシュオブジェクトを更新します。

これにより、クライアントにはなるべくキャッシュからレスポンスを返すとともに、キャッシュのオブジェクトを最新化できます。

図にするとこのような動きになると認識しています。

f:id:lcl-engineer:20180220112240p:plain

検証

graceの動きを簡単に検証します。

Varnishバージョン

varnish 4.1.9

シナリオ

  • backendサーバ側では、現在時刻を表示するHTMLを返す
  • ttl、graceは30sとする

vcl

sub vcl_backend_response {
  set beresp.ttl = 30s;
  set beresp.grace = 30s;
}

初回アクセス

初回アクセスのため、backendからHTMLを取得し現在時刻が表示されます。

f:id:lcl-engineer:20180220115013p:plain:w200

約25秒後に再アクセス

初回アクセスから30s以内のため、キャッシュされた時点での時刻が表示されます。

f:id:lcl-engineer:20180220115013p:plain:w200

ttl: 4.556

約35秒後に再アクセス

初回アクセスから30sを過ぎていますが、grace期間内のためキャッシュされた時点での時刻が表示さます。

f:id:lcl-engineer:20180220115013p:plain:w200

// 30sを過ぎているので、ttlはマイナスとなる
ttl: -5.069

この時、バックグラウンドでフェッチされ、キャッシュが更新されます。

約36秒後にアクセス

前回のアクセスによって、キャッシュが更新されているため、前回アクセス日時が表示されます。

f:id:lcl-engineer:20180220115711p:plain:w200

以上、簡単ですがgraceの動きを検証しました。

他ユースケースの紹介

次に、いくつかのユースケースに応じたVCLの定義を紹介します。

backendがエラーの場合のみ、graceを利用する

backendの状態によって、graceの利用有無を制御することも可能です。

まず、grace期間を1Hと長めに設定しておきます。

sub vcl_backend_response {
  set beresp.grace = 1h;
}

vcl_hitで、backendの状態を見て、graceの利用有無を制御します。

sub vcl_hit {
    if (obj.ttl >= 0s) {
        // キャッシュを利用する
        return (deliver);
    }
    if (!std.healthy(req.backend_hint) && (obj.ttl + obj.grace > 0s)) {
      // backendがエラーの場合、grace期間内はキャッシュを利用する
      return (deliver);
    } else {
      return (fetch);
    }
}

リクエストURLに応じて、grace期間を分ける

リクエストURLに応じて、graceの設定を変更することも可能です。

以下の例では、デフォルトは30sでトップページのみ300sにしています。

sub vcl_recv {
  if (req.url == "/") {
    set req.http.X-set-grace = "300s";
  }
}

sub vcl_backend_response {
  set beresp.grace = std.duration(bereq.http.X-set-grace,30s);
}

まとめ

graceをうまく利用することで、以下の要望をある程度は満たすことができます。

  • キャッシュからレスポンスを返したい
  • 新しいオブジェクトを返したい

ですが、適切なTTL/graceを設定するのはやはり難しく、弊社でも必要に応じて随時調整しています。今後も、継続的にパフォーマンス向上に取り組んでいきます。

Railsアプリケーションで採用しているDBスキーマ設計ガイドライン

Webエンジニアの森脇です。 Railsアプリケーションで採用しているDB設計(スキーマ定義)について紹介します。

※ Railsでは当たり前もの、Railsに依存していない内容も含んでいます。

前提

環境違えば、採用するルールも異なると思いますので、まずは弊社で利用している環境を記載します。

  • DBはPostgreSQL 9.x
  • 開発言語は、Rails 4.x,5.xを利用

命名ルール

Railsの規約に沿う命名を採用しています。DB設計にこだわりがあるメンバーにとっては、異論があるルールもあるのですが、アプリケーション開発の生産性も考慮しRailsの規約に寄り添うのがよいと判断をしました。

テーブル

  • 動詞は使用せず、名詞とする
  • 複数形とする
  • 1:n のテーブル名は「単数形_複数形」 とする
  • n:n のテーブル名は「複数形_複数形」とする

カラム

  • カラム名にテーブル名のprefixは付与しない
    • usersテーブルのユーザ名は「user_name」ではなく「name」とする
  • 主キーは、idとする
  • 外部キーは、「JOIN先のテーブル名(単数系)_id」とする
  • boolean型は、true/falseが自明な名称にする
    • xxxx_flagなど、true/falseの意味が自明ではないため利用しない
    • is_xxxxやcan_xxxxなどの明確な名称にする
  • 時間を記録するカラムは 受動態on、受動態atとする
    • DATE型は「受動態_on」 とする
    • TIMESTAMP型は「受動態_at」とする

インデックス

  • 原則、Railsのmigrationでの自動生成ルールに任せる
  • 自動生成で桁数オーバーする場合は、idx_<テーブル名>_on_<カラム名>をベースとして収まるように適宜調整

型もRailsのMigrationから生成される標準の型を利用しています。

数値

  • 整数
    • 特別な理由がない場合は、integerを利用する
  • 浮動小数
    • 特別な理由がない場合は、numericを利用する
    • numeric とdecimalは同一のため使い分け不要

文字列

日付

  • dateを利用する

時刻

  • timeを利用する

日時

  • timestampを利用する

制約など

データ整合性を担保するための制約は、極力利用する方針としています。

外部キー制約

使用する

  • テストデータ作成が若干手間なのは否めないが、不整合があるデータを作ってしまい結局ハマることもあるので、DB側でガードしたい
  • SQLで直接データ修正する場合も考慮すると、アプリケーションだけでのデータチェックでは不十分

ユニーク制約

使用する

  • DB側で、データ整合性を担保するため

NOT NULL制約

付与可能なカラムには、必ず付与する

  • DB側で、データ整合性を担保するため
  • 既存テーブルへのカラム追加の場合は、以下の手順をとる
    • NOT NULL 制約なしでカラム追加
    • 該当カラムに値を設定
    • NOT NULL 制約を追加

デフォルト値

利用しない

  • デフォルト値に頼らず、アプリケション側で明確な意図を持って設定したい

Check制約

利用しない

  • 複雑なチェック条件は、Check制約では対応できないため、利用しても中途半端になると判断

ON DELETE CASCADE

利用しない

  • データの削除は、アプリケーション側で明確にコントロールしたい

最後に

以前はレビューの都度、制約などのあるべき論を議論をしてしまい、レビューに時間がかかってしまっていました。ガイドラインを整備することでチーム内の意識統一が容易になり、より本質的なレビューができるようになりました。

Dangerの指摘をChatWorkに流してリリースのミスを防ぐ

モバイルアプリエンジニアの山下です。

LCLでは作業中のPull Reqeustが誤マージされるのを防ぐため、Pull Requestのステータスをラベルで管理しています。
ラベルは「WIP」と「ready for release」の2つあり、マージするためには「ready for release」を付ける必要があります。
「ready for release」を付けられたPull RequestはChatWorkに通知され、開発部のメンバーへ周知されるようになっています。

f:id:lcl-engineer:20180207134621p:plain

今回はこの通知に先日導入したDangerの指摘を一緒に流し、指摘事項を全員が確認できるようにしてみました。
それでは実装例とGitHub Webhookの効率的なデバッグ方法を紹介していきます。

実装

GitHub Webhookで送られたJSONからDangerのコメントを取得して特定のルームに通知します。
今回は以下のGemを使っています。 ※これらの詳細な使い方については当記事では取り扱いません

  • octokit
  • nokogiri
  • chatwork

Dangerのコメントを取得

Dangerのコメントには「generated_by_danger」の文字列が含まれるため、この条件を利用します。

class GithubController < ApplicationController
  def fetch_danger_comments
    client.issue_comments(repo_full_name, pr_number).each do |comment|
      return comment['body'].gsub(/[\r\n]/, '') if comment['body'].include?('generated_by_danger')
    end
  end

  def client
    @client ||= Github::Base.new.client
  end

  def pr
    @pr ||= params['pull_request']
  end

  def repo_full_name
    pr['base']['repo']['full_name']
  end

  def pr_number
    pr['number']
  end
end

Dangerのコメントを解析

DangerのコメントはHTMLで構成されているため、nokogiriを使って要素を取得します。
今回はテーブルで表現される Error、Warnin、Message に対応しました。

class GithubController < ApplicationController
  ...
    
  def parse_danger_comments(comments)
    doc = Nokogiri::HTML(comments)
    contents = doc.search('//th[@data-kind]')

    notice = "\n"
    contents.each_with_index do |content, index|
      title_text = content.inner_text.gsub(/[\r\n]|\s{2,}/, '')
      notice += "\n" if index > 0
      notice += "[title]#{title_text}[/title]\n"
      doc.search("//table[#{index + 1}]//tr/td[2]").each_with_index do |item|
        item_text = item.inner_text
        notice += "#{item_text}\n"
      end
    end
    notice += "\n"
  end
end

ChatWorkへメッセージを送る

asonas/chatwork-ruby を使うことで簡単にChatWorkへメッセージを送信することができます。

require 'chatwork'

class GithubController < ApplicationController
  ...
    
  def post_on_chatwork
    body = "[info]#{repo_name} にPull Requestされました。\n"
    body += "[hr]#{title}\n"
    body += html_url
    danger_comments = fetch_danger_comments
    body += parse_danger_comments(danger_comments) if danger_comments.present?
    body += '[/info]'
    
    ChatWork.api_key = "API_KEY"
    ChatWork::Message.create(room_id: room_id, body: messages)
  end
  
  ...

  def title
    pr['title']
  end

  def html_url
    pr['html_url']
  end

  def repo_name
    params['repository']['name']
  end
end

以上でWebhookからpost_on_chatworkを呼ぶことで通知されます。

f:id:lcl-engineer:20180207134638p:plain

GitHub Webhookの効率的なデバッグ方法

Webhookのデバッグでよくやりがちなのは、本番と同じ操作を繰り返して動作確認をすることです。
今回の例で言えば、Pull Requestのラベルを何度も切り替えて確認を繰り返してしまうことです。

ラベルの切り替えのみならまだラクですが、確認したいイベントがコメント追加やPull Request作成、Pushなどの場合、それらを実際に何度も操作するのは手間がかかります。
さらに、対象のリポジトリが組織の管理するもので不要な操作をし辛かったり、アクセス権限がありwebhookやリポジトリの設定画面を見れる環境でない場合、Webhookテスト用のリポジトリを自前で用意する必要があります。

実装する前から用意することを考えるとやる気もなくなります。
そこで、ローカルからcurlコマンドを打ってWebhookを再現するようにしたいと思います。

手順

curlで送るためのテスト用JSONデータを用意します。
実際のデータとダミーのデータを用意する2つの方法があります。

リポジトリの設定画面へのアクセス可能な場合は、実際のJSONデータを使うといいと思います。

実際のJSONデータを用意

リポジトリの設定画面のWebhooks & services > Recent Deliveries から過去の送信履歴を確認することができます。
履歴の一覧からテストしたいPayloadをコピーすることで実際のデータで実装を進められます。

ダミーのJSONデータを用意

dummy.payload.json というJSONファイルを作成し、以下から再現したい対象のWebhook payload exampleをコピペします。

Event Types & Payloads | GitHub Developer Guide

ローカルからWebhookを再現

ターミナルから以下のコマンドを叩いてPOSTします。 URLやX-GitHub-Eventはそれぞれ適当な値に変更してください。

$ cat dummy.payload.json | curl http://localhost:8080/XXXX -H 'Content-Type:application/json' -H "X-GitHub-Event: pull_request" -d @-

以上で実際の流れを何度も操作する必要なく、ターミナルから直ぐに何回でも処理を動かすことができます。

効果

今回の取り組みで以下のメリットがありました。

  • Dangerの指摘の確認漏れが減った
  • フロントエンド・バックエンドなど担当分野の違う警告が発生した際に別途確認する手間が無くなった
  • 明るみに出ることによってリーダーや互いの不安が減り、精神衛生的によい環境になった
  • コードに関係ない警告(PR説明の項目漏れや名前の決まりなど)も直そうという意識が持たざるを得なくなった

まとめ

既に取り入れている仕組みを組み合わせるだけで開発効率や安全性の向上に繋げることができました。
最初はコメントの解析を正規表現でやろうとしていましたが、自分の分報へ流した際に他のメンバーから助言をもらい簡潔に処理することができました。

f:id:lcl-engineer:20180207134653p:plain

LCLでは社内のモブプロ勉強会でもこのような改善系ツールの開発を題材にして、各メンバーが環境づくりをしやすくしています。 チームでの活動や使っているツールを組み合わせることで日々環境が改善されていくのは面白いです。