gorilla/csrf で安全なWebフォームを作る

こんにちは。GoでWeb開発していますか?私はしていません。Goに限らず、既成のWebアプリケーションフレームワークを使わずに自前でWebフォームを作る場合、なにも考えずに書くと CSRF (Cross Site Request Forgery) 脆弱性を作りこみ、不正なユーザー操作を実行されてしまう可能性があります。

ダメな例

例えば以下のGoコードで作成されるフォームにはCSRF脆弱性があります。SubmitSignupForm ハンドラは、受け取ったリクエストが自分のサイト上のフォームからサブミットされたものかチェックしていないので、攻撃者が他のサイト上のフォームを使い、第三者のユーザーのブラウザで任意の操作を実行させることができてしまいます。

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/signup", ShowSignupForm)
    r.HandleFunc("/signup/post", SubmitSignupForm).Methods("POST")
    http.ListenAndServe(":8080", r)
}

func ShowSignupForm(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, `
<html>
  <body>
      <form method="POST" action="/signup/post" accept-charset="UTF-8">
          <input type="text" name="name">
          <input type="text" name="email">
          <input type="submit" value="Sign up!">
      </form>
  </body>
</html>`)
}

func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
    _ = r.ParseForm()
    fmt.Fprintf(w, "%v\n", r.PostForm)
    // TODO: ユーザー登録
}

CSRFについては「はまちちゃん事件」が有名です。mixiにログイン済みのユーザーにCSRF脆弱性のあったmixiの発言投稿用URLリンクを踏ませることで、ユーザーの意図しない発言を投稿させたというものです。CSRF対策にはいくつかの手法がありますが、権威あるセキュリティ団体であるOWASPが各手法の解説とメリット・デメリットを網羅しています。詳しく知りたい方はそちらを参照してください。

github.com

gorilla/csrf

信頼できるWebアプリケーションフレームワークではCSRFへの対策実装され、デフォルトで有効になっていますが、Goの net/http はWAFではないので、別途WAFを導入しないのであれば、自分でセキュリティ対策を導入する必要があります。そこで、今回は gorilla/csrf を使ってCSRF対策を実装します。

github.com

gorilla/csrf は net/http だけでなく、Echo や Gin といった人気のWAFと協調して動作することを目的としたCSRF対策パッケージです。

Cookieの二重送信(gorillaの場合)

gorilla/csrf が採用する対策手法は、OWASP の分類で言えば "Double Submit Cookie" (Cookieの二重送信) に分類されます。 Double Submit Cookieは、要点だけ言えばセッション開始時に暗号的に強い乱数でトークンを生成しておき、それをブラウザのCookieとフォームのhidden input要素の両方に送信するというものです。もちろん、サブミット時にはサーバーでCookieとフォームのトークンの一致を検証し、一致しなければエラーとします。この手法は、トークン情報をステートとして保持する必要がないため、実装・運用コストが低いのが特徴です。

一方、Cookieの二重送信には問題点も指摘されています。

要約すると、一定条件のもとではCookieを不正に書き換えることは可能であり、その場合に Double Submit Cookie の安全性は成立しないというものです。これについて、gorilla/csrfCookie に署名つき暗号化を施すことで回避しています。OWASPチートシートにも、Cookieの暗号化はうまく動作すると書かれています。

Including the token in an encrypted cookie - often within the authentication cookie - and then at the server side matching it (after decrypting authentication cookie) with the token in hidden form field or parameter/header for ajax calls mitigates both the issues mentioned above. This works because a sub domain has no way to over-write an properly crafted encrypted cookie without the necessary information such as encryption key.

また、gorilla/csrf では、リクエスト時に Unique-per-Request な one time padを生成し、CookieのtokenをXORでマスクしたものをフォームに送信する工夫を加えることで、BREACH攻撃を緩和しています。

プレーンな Double Submit Cookie の弱点を回避して、概ね安全なように思います。強いていえばユーザー自身が外部のWebページにフォームを送信してしまえば偽のフォームを作成可能ですが、そのケースはほぼ意図的な漏洩ですし、そこまで守るかどうかはポリシー次第(それも防ぎたかったらパスワード入力を求めるとか)じゃないでしょうか。

gorilla/csrfCSRF対策の流れを紹介します。

初回訪問時

最初にユーザーがWebサイトに訪れたとき、サーバーは暗号的に強い乱数生成器から basetoken を乱数生成します。そして basetoken を auth_key で署名つき暗号化したものを、レスポンスの _gorilla_csrf Cookie に格納します。この暗号は署名つき暗号なので、改竄に対する耐性があります。

f:id:ono_matope:20190605011412p:plain
初回訪問時

フォーム表示時

次に、サーバーが _gorilla_csrf Cookie を持ったクライアントにフォームを表示する際、サーバーはまず OTP (one-time-pad) を乱数生成します。この OTPと、basetokenをOTPでマスクした結果を結合し、CSRFトークンを生成します( csrfToken = OTP + XOR(basetoken, OTP) )。このCSRFトークンは、フォームのhidden inputとしてユーザーにレスポンスされます。

f:id:ono_matope:20190605092057p:plain
フォーム表示時

サブミット時

ユーザーがフォームをサブミットしてきたとき、サーバーはフォームのCSRFトークンからOTPを取り出し、Cookieのbasetokenを使って先ほどと同じ操作( OTP + XOR(basetoken, OTP)) をおこない、その結果がフォームのCSRFトークンと一致するか確かめることで、リクエストが正しく要求されたものか判別します。

f:id:ono_matope:20190605102640p:plain
サブミット時

How to use gorilla/csrf

さて、原理はわかったので使ってみましょう。

package main

import (
    "fmt"
    "html/template"
    "net/http"

    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/signup", ShowSignupForm)
    r.HandleFunc("/signup/post", SubmitSignupForm).Methods("POST")

    // Protect ミドルウェアは、非冪等なHTTPメソッドの場合にサブミットされたフォーム
    // をチェックし、CSRFトークンが一致しているか検証する。
    //
    // auth-key はコードに書き込まず、/dev/urandom したものを外部から与えること。
    // キーが変わるとそれまでに発行したトークンの検証に失敗する。
    //
    // HTTP な開発環境では opt に csrf.Secure(false) 指定が必要。本番では外すこと。
    h := csrf.Protect([]byte("32-byte-long-auth-key"), csrf.Secure(false))(r)
    http.ListenAndServe(":8080", h)
}

func ShowSignupForm(w http.ResponseWriter, r *http.Request) {

    // {{.CSRFField}} にCSRFトークンの隠しinputを埋め込む。
    t, _ := template.New("form").Parse(`
<html>
  <body>
      <form method="POST" action="/signup/post" accept-charset="UTF-8">
          <input type="text" name="name">
          <input type="text" name="email">
          {{.CSRFField}}
          <input type="submit" value="Sign up!">
      </form>
  </body>
</html> `)
    t.Execute(w, map[string]interface{}{
        "CSRFField": csrf.TemplateField(r),
    })
}

func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
    _ = r.ParseForm()
    fmt.Fprintf(w, "%v\n", r.PostForm)
    // TODO: ユーザー登録
}

csrf.ProtectCSRF保護機能を提供するHTTPミドルウェアを作成します。このミドルウェアは、POSTなどの非安全リクエストを受けた時にリクエストボディのフォームを自動的にチェックし、CSRFトークンが適格であれば次のハンドラを実行し、適格でなければエラー画面を表示して終了します。そのため、アプリケーション側のHTTPハンドラは(サブミットについては)CSRF検証済みであることを前提に開発することができます。OWASPチートシートにも、セキュリティ対策で怖いのはうっかりミスなので自動的な対策をせよと書かれているので、この仕様は歓迎できます。

一方、csrf.TemplateField(r *http.Request) template.HTML は、リクエストのCSRFトークンのhidden inputタグをリターンします。これをtemplateで置換してHTMLフォームを表示すると、gorilla.csrf.Token という名前の 隠し inputタグが埋め込まれます。

f:id:ono_matope:20190605144134p:plain
出力されるHTMLフォーム

同時に、Cookieにも _gorilla_csrf が記録されているのがわかります(内容は暗号化されているので確認も改竄もできません)。

f:id:ono_matope:20190605142544p:plain
Cookie

そこで、わざとフォームの値を書き換えてからSubmitすると、ちゃんとエラーになります。

f:id:ono_matope:20190605141731p:plain
シンプルなエラー画面

ところで、csrf.Protectはトークン不一致に簡単なエラー画面を出力するのですが、オプションでカスタムのエラーハンドラを指定させることができます。以下のように、何らかのセキュリティ対応を取れるようなログを落とすと良いと思います。

   h := csrf.Protect([]byte("32-byte-long-auth-key"),
        csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log.Println("CSRF攻撃の疑いのあるリクエストが発行されました")
            fmt.Fprintf(w, "ニャーン (%s)", csrf.FailureReason(r).Error())
        })),
        csrf.Secure(false),
    )(r)

f:id:ono_matope:20190605142904p:plain
にゃーん

以上です。

外付け(として使う)SSDって今何買えばいいのか調べたメモ

昨日の記事で言ったようにうっかりSSD+ケースを買ってしまったのだが、最近はUSB3.1 Gen2もThunderbolt3も普及してきてデータ転送帯域がどんどん広がっているし、m.2?NVMe?とかもなんかちっちゃくてかっこいいし、SSDも価格爆下がり中って聞くし、「最近の外付けSSDはどうなの?」みたいなところをちょっと調べてみた。買ってから調べるという。容量は1TBを前提に調べた。

外付けSSDについての調査だが、単品のSSDにケースを調達して外付けSSDに自作する、ということを選択肢に入れて調べたのでほとんど内蔵SSDについてです。あとNANDとかTLCとかには触れません。

まず、SSDの製品を分類するには、サイズ規格インターフェイスに注目する必要がある。

サイズ規格は以下のふたつがある。

覚えておく必要があるインターフェイスも二つだけだ。

  • SATA3.0
  • PCI-Express (PCIe 3.0)
    • 1レーンで転送速度8Gbps (1GB/s). 最大4レーン32Gbps (4GB/s)での転送が可能
    • コントローラのプロトコルはNVMe。NVMeとはPCIe上でブロックデバイスを効率よく動作させるために導入されたプロトコルである。

つまり、SSD市場には、以下の3カテゴリが存在していることになる。

外付け化のためのケーブルインターフェイス

PCIeはSATAの6倍以上でるのかーすごいなという感じだが、私は私のMacに外付けして使いたいので、つなげるやつをさがす。

2.5インチ + SATA3.0用のUSB 3.1 Gen2変換ケース

Amazonで大量に見つかる。どれも2,000円から3,000円程度と、安価。

Amazon.co.jp: USB3.1 gen2 ssd 2.5 sata ケース: パソコン・周辺機器ストア

m.2 + SATA3.0用のUSB 3.1 Gen2 変換ケース

m.2の変換ケースは、「Bキー、Mキー」などサポートするカードに相性があるので注意。

m.2 + PCIe3.0x4変換ケース

Thunderbolt3-PCIe3.0変換ケース

変わり種で、Thunderbolt3-PCIe3.0変換ケースというのもあった。ただ、書き込み性能の表記がないがレビューによると900MB/sしかでないらしい。

外付けSSD製品

USB接続の外付けSSD製品は、基本的にSATA3.0構成のようで、USB 3.1のGen1を搭載した安価なものとGen2を搭載した高価なものに二極化している。 Gen1の製品はリード性能300MB/s〜400MB/sでGB単価が14円/GBから、Gen2の製品はリード性能が560MB/sでGB単価が23円から、となっているようだ。

kakaku.com

フォームファクタに関しては、実はいまは外付けSSDは2.5インチ製品は少なくなっており、ほとんどがコンパクトなm.2仕様のようだ。2.5インチ: 価格.com - インターフェイス:USBのSSD 人気売れ筋ランキング (規格サイズ:2.5インチ)

考察・あなたが何を買うべきか

関連するパフォーマンス指標を書き起こしてみた。まず、PCにUSBで接続したい場合、データ転送速度はUSBの転送速度である500MB/s(USB 3.1 Gen1)や1GB/s(USB 3.1 Gen2)で律速する。この場合、ドライブにUSBの転送速度を超えたあまり高性能なものを買っても活かせないことに注意。

f:id:ono_matope:20190503055433p:plain

その上で、獲得したい転送速度に応じて最適な購入ルートを考えたみた。

転送速度を気にしない人向け (Gen1コース)

外付けSSDのGen1のものなら1,3000円/1TBくらいで買えるので、転送速度が300MB/s〜400MB/sでいいよという人はわざわざ内蔵ドライブとケースで自作せず、普通に外付けを買うといいと思います。

転送速度は560MB/sくらいはほしいという人(SATA+Gen2コース)

単価の安いSATA SSDに、安いUSB 3.1 Gen2-SATA変換ケースをつけると手頃に560MB/s帯のポータブルSSDになる。Gen2の製品は外付けドライブだと23円/GB程度するが、以下の構成は15GB/円で作れて嬉しい。

代表構成: 12,339 + 2,999 = 15,338円 / 1TB。WDのSSDTranscendのケースとのセットバージョンもある。

パッケージ品で良さそうなのはこれとか。でも23,763円/1TB。丈夫にできてそうってメリットはある。

転送速度は1000MB/sほしいよという人向け(PCIe+Gen2コース)

わかる。MacBook Pro 2018の内蔵SSDの転送速度が3,200MB/sに対して、300MB/sや500MB/sではなにか物足りない。もうちょっとハイスペックを楽しみたい。そんなあなたは m.2/PCIe3x4のSSDに、USB 3.1 Gen2変換ケースをの組み合わせで1000MB/sなポータブルストレージを作りましょう。既製品の外付けドライブではこの選択肢はそもそも存在しないのかな。ちょっと見つかんなかったです。

代表構成:¥15,980 + ¥5,680 = 21,660円 / 1TB

Crucial SSD M.2 1000GB P1シリーズ Type2280 PCIe3.0x4 NVMe 5年保証 CT1000P1SSD8JP

Crucial SSD M.2 1000GB P1シリーズ Type2280 PCIe3.0x4 NVMe 5年保証 CT1000P1SSD8JP

USB-C付属バージョン

akiba-pc.watch.impress.co.jp

金に糸目をつけない人向け (Thunderbolt 3プロ向け機材コース)

Thunderbolt3 SSD変換ケースとか、

そもそもThunderbolt3 SSDとか買うといいんじゃないでしょうか。

blog.livedoor.jp

聞いたことないメーカーだけど3万円で 1,600MB/s でる謎の Thunderbolt3 1TB SSD とかある

容量が十分あるにも関わらずAfter Effects がディスクキャッシュを保存する容量がないと言い張るときはTimeMachineローカルスナップショットを消せ

TL;DR: TimeMachineローカルスナップショットを消せ。

macOS Mojave / APFS 環境で、たまにAfter Effectsを起動すると「ディスクキャッシュフォルダーが存在するドライブには、環境設定で指定されている容量を全て保存できるだけの空き容量がありません。空き容量を増やすか、メディア&キャッシュ環境設定でフォルダーまたは最大ディスクキャッシュサイズを変更してください。」と警告ダイアログが出現する。

f:id:ono_matope:20190502224711p:plain

しかし、私はディスクキャッシュサイズをMacintosh HDに最大40GB程度に設定しており、Finderによるとそのドライブには十分な容量がある。

f:id:ono_matope:20190502225117p:plain

f:id:ono_matope:20190502225040p:plain

何の気なしに df してみたところ、'/' のファイルシステムに空きが18GBしかないと出てきた。Finderの空き容量表示と全く違うではないか。

f:id:ono_matope:20190502225318p:plain

ところで、High Sierra 以降のAPFSが有効なシステムでは、TimeMachineが自動的にMacintosh HDにローカルスナップショットを作成する。

ローカルスナップショットが消費しているストレージ容量について心配する必要はありません。ファイルのダウンロード、ファイルのコピー、新しいソフトウェアのインストールなどのタスクに必要な容量が使われてしまうことはないからです。

スナップショットが消費している容量は、Mac では空き容量に算入されます。その上さらに、Time Machine は空き容量の多いディスクにしかローカルスナップショットを保存しないようになっているほか、スナップショットが古くなったり、ほかのことに容量が必要になったりした場合は、自動的にスナップショットを削除してくれます。

Time Machine のローカルスナップショットについて - Apple サポート

作成されるローカルスナップショットは、OSのディスクキャッシュのように「ファイルシステムの向こう側で」動的に管理されるように設計されているようだが、怪しい。ためしにTimeMachineローカルスナップショットを削除した。

blog.h-wd.info

f:id:ono_matope:20190502230318p:plain

f:id:ono_matope:20190502230342p:plain

すると、df のAvailが129GBまで回復し、AEのディスク容量警告も出なくなった。めでたし。

f:id:ono_matope:20190502230501p:plain

容量が本当に足りないのかと思ってSSD買っちゃったんだけど、返品しようかな…。ちょっと考える。

MacBook Pro 2018 13" + Intel NUC Bean Canyon + eGPU で Mac/Win グラボ共有環境を構築した話

今年もよろしくお願いします、小野マトペです。去年の10月、MacBook Pro 2018 13インチを購入しました。

今まで使っていた2013モデルでは性能重視で15インチを使用していましたが、今年のモデルはCPUのコア数が増え、13インチでも去年の15インチよりも高いCPUパフォーマンスが出るということだったので、モビリティのために13インチをチョイス。やっぱりノートパソコンは持ち運んでなんぼですね。軽くて最高。

japanese.engadget.com

13インチを購入するにあたって最後まで躊躇した要素はグラフィック性能でした。15インチには専用のdGPUが搭載されていますが、13インチは性能の低いiGPUしか搭載されていません。僕はたまに After Effects での動画制作作業をするので、この点はちょっと都合が悪かったです。

そこで目をつけたのがeGPU(外付けGPU)です。macOSではHigh Sierraから Thunderbolt 2/3 での eGPU 接続に対応したことで、Mac環境でのeGPUカテゴリが昨今盛り上がりを見せています。ともあれこれを使えば一台のMacを、外出時には軽量ラップトップ、自宅ではデスクトップレベルのハイパワーグラフィックマシンとして運用することができるため、今回検討しました。

GIGABYTE GAMING BOXを試すつもりだったのだが…

そういうわけで、調査の末に去年の年末に amzon.com で GIGABYTE GAMING BOX 580 を注文しました。これはRadeon RX 580 グラフィックカード組み込み済みで販売されているeGPUエンクロージャで、コンパクトさが際立つ一品です(eGPUはどれもけっこうなサイズなのです)。持ち運び用のバッグが付属しているなどモビリティが高いのもポイントです。一方、ファンが壊れてノイズが鳴るようになったとか、不快なコイル鳴きがあるとか、工作精度の点で不安なレビューも見られました。しかし、Amazon.comでの価格の安さ(本体$419に送料+Import Fee Depositで$56)とコンパクトさに惹かれ、注文することにしたのですが・・・

が、開封した製品がバッキバキに破損しており、そのまま返品となりました。残念・・・

GAMING BOXを返品し、もう一度同じ製品を注文する気にもならなかったので、改めてグラボとエンクロージャを選び直すことにしました。

グラボ

GAMING BOXのような一体型でない eGPU を使うためには、eGPUエンクロージャとグラフィックボードを用意する必要があります。エンクロージャごとに供給電力やカードサイズがあり、適合する組み合わせを選ぶ必要があります。 https://egpu.io というサイトは発売中のエンクロージャとグラボの詳細な情報やレビュー記事が掲載されており、製品選びの助けになりました。

egpu.io

Macで eGPU を使用するにあたって最も大きい制約は、GPUのメーカーです。ゲーム用のGPUとしてはnVidia製品の方がメジャーですが、Macは現在AMDGPUしかサポートしていないため、AMD製品から選択することになります。nVidiaは記事執筆時点でMojave用のドライバーをリリースしておらず、High Sierra以前の環境でしか動作しません。

僕は Windows PC に詳しくないので今回初めてグラフィックカードを調べたのですが、現在AMDグラフィックカードは、主に以下のラインナップのようです。ミドルレンジ〜ハイエンドのゲーミングPC相当の性能が欲しいのであれば、RX580, RX590, Vega56あたりから選ぶことになるみたいです。

  • Radeon RX 570 (GeForce GTX970 と同じくらいの性能)
  • Radeon RX 580 (GeForce GTX1060 と同じくらいの性能)
  • Radeon RX 590 (GeForde GTX1060 より速いくらいの性能)
  • Radeon Vega 56 (GeForce GTX1070Ti と同じくらいの性能)
  • Radeon Vega 64 (GeForce GTX1080 と同じくらいの性能)

現在グラボは暗号通貨のマイニング需要や代理店のマージンにより国内での価格が高騰しているため、私は今回は Vega56 を実質35,000円程度( サンデーくじで入手したクーポン 3000円を使用)でネットオークションで入手しました。グラフィックカードは経年劣化しにくいパーツですし、中古市場で手に入れても良いと思います。amazon.com をのぞいてもいいかもしれませんね。

MSI Radeon RX Vega 56 Air Boost 8G OC グラフィックスボード VD6516

MSI Radeon RX Vega 56 Air Boost 8G OC グラフィックスボード VD6516

androgamer.net

Razer Core X

次にeGPUエンクロージャですが、これは Razer Core Xを選びました。これは安価で、Vega56への供給電力も十分、USB-PDでMac本体への電力供給も可能、動作実績も豊富と堅実な機種ですが、とにかくデカいという製品です。

左端の一番でかいやつです。

www2.razer.com

現物を見てあまりのデカさに笑う私。

狭小住宅の住人としては設置性は重視したかったのですが、年末に大容量の本棚を導入したことでデスク周りの収納スペースに余裕ができたために導入を決意し、TSUKUMO eX.の地下のRAZERストアで税込35,424円から2,000円のTSUKUMOポイント割引を適用して購入しました。空間は正義。

なんとかデスク下のメタルラックに押し込めました。デカいeGPUも、どこか隠せる場所に設置できればなんとかなります。ただし Thunderbolt 3 アクティブケーブルは高価なので気をつけてください。

eGPUゲーミングに最適な Intel NUC Bean Canyon

さて、ここまででeGPUはセットアップできたのですが、ぶっちゃけGPUを酷使する作業は年に数回あるかどうかという感じなので、そのためだけにeGPUを導入するのは正直もったいないです。せっかくつよつよグラボを入手したので、これを使ってどうにかWindowsゲームでもできないか考えました。久しぶりにマウスでFPSがやりたい。PUBGとか俺もやりたい!

最初に考えたのは、eGPU を Mac の BootCamp から利用するアプローチですが、

という問題があるため BootCamp 案は却下し、ゲーム用にWindows PCを生やすことにしました。1台くらい WIndows マシンを持っておくと何かと潰しが効くだろうし。

PCの選定ですが、

  • 内臓グラフィックカードは必要なく
  • 代わりにeGPU接続のためのThunderbolt3コネクタを搭載し、
  • CPUはミドルレンジ以上。
  • 部屋が狭いのでコンパクトPCと言える筐体サイズ、
  • コンソールゲーム機が4,5万のこのご時世、ゲームのためだけにそんなにお金をかけたくない…

というわがまま条件で探したところ、ひとつだけベストマッチな製品が見つかりました。それが Intel NUC Bean Canyon NUC8i5BEHです。

INTEL インテル i5-8259U M.2 SSDに対応 2.5" (9.5mm厚) HDD/SSDも搭載可能 ハイパフォーマンス小型ベアボーンキット BOXNUC8I5BEH

akiba-pc.watch.impress.co.jp

NUC とは Intel が出している小型・安価なPCベアボーンキットで、本体の他に電源ケーブル、メモリ、ストレージを別途購入する必要がありますが、現行世代(Beran Canyon)の NUC8i5BEH は MacBook Pro 2018 13インチと同じ第8世代i5 CPU i5-8259U と Thunderbolt 3コネクタを搭載しています。しかもサイズは超コンパクト。これ、まさに eGPU ゲーミングマシンに最適なPCじゃないですか。

この製品を狙っていたところ、ちょうどPayPayの20%ポイント還元祭りの期間中に池袋のTSUKUMOで4.8万円で販売しているのを見つけたため、その場で購入しました。250GBのm.2 SSD(6千円)、16GBメモリ(1.6万円)、Windows10 Home Edition(1.7万円)、電源ケーブル(1千円) で合計9万6千円のところ、PayPayの20%ポイント還元で実質7.7万円となりました。

Tweetのツリーに写っていますが、ついでに隣のビックカメラでLG 27UK850-Wも20%ポイント還元で買いました。これも大変良い4Kモニタです。ありがとうPayPay)

かわいい!こんなに小さいのにCPUもメモリもSSDもほとんどMBP 2018 13と同じスペック。みんなが欲しかった Mac mini ってこういうやつでは?

セットアップには少し手こずって、Windowsをインストールしたところ有線LANも無線LANもドライバがないためWebからドライバがダウンロードできないという事態になりました。MacIntelのサイトからドライバ一式をダウンロードしてUSBでコピーしましたが、他にPCがない人は注意が必要です。

完成!+ベンチマーク

f:id:ono_matope:20190120211948j:plain

f:id:ono_matope:20190120215914j:plain

というわけで、MacBook Pro, Intel NUC, Razer Core X, Vega 56 による軽量ラップトップ+ハイパワークリエイティブPC+WindowsゲーミングデスクトップPCシステムが完成しました。コンパクトさがすごくないですか(Razer Core Xをうまく机の下に隠せた場合に限る)。

ケーブルの差し替えはどうしても多くなるので、ケーブルマネジメントはだいじ。

f:id:ono_matope:20190120220252j:plain

さっそくベンチマークをとりましょう。まずは Mac で GeekBench 4 の Compute スコアを計測します。

MBP内臓GPU (Iris Plus 655) eGPU (Razer Core X + Vega 56)
Metal 35,000 146,234
OpenCL 35,238 140,855

4倍以上のグラフィックパフォーマンスが得られました。

次は NUC の Windows3DMark の Time Spy と Fire Strike を実行します。

f:id:ono_matope:20190119205711p:plain
Time Spy

f:id:ono_matope:20190119210459p:plain
Fire Strike

スコア 結果URL
3DMark Time Spy 5351 https://www.3dmark.com/3dm/32565573?
3DMark Fire Strike 13214 https://www.3dmark.com/3dm/32570334?

結果ページによると 4KゲーミングPC と ゲーミングラップトップの中間程度のパフォーマンスと評価されました。安価な小型PCをベースに構成したシステムの性能としては上出来ではないでしょうか。実際に CoD:BO4 をプレイしていますが、HD解像度の最高画質で滑らかにプレイができています。BO4 たのしい!

静音性については、ゲームプレイ中はRazer Core Xのファンが強めに回転しますが、筐体ファン半径が大きいため音が低く、耳障りな音ではないです。

そういうわけで、

  • Intel NUC + Windows10Home 7.7万円
  • Razer Core X 33,424円
  • Vega56 Air Boost(中古) 35,000円

PayPayやクーポンを併用しましたが、eGPUセット(7万弱円)とNUCセット(7.7万円)をあわせて14万円で、eGPUをMac+Winで共有可能なゲーミングPC環境が構築できました。超コンパクトな設置サイズを考慮すると、なかなかコスパよく組めたのではないでしょうか。私のように、eGPU構成を検討しているMacユーザーの方は、Intel NUCを使ったサブマシン構成を検討してみるのもおすすめです。

部屋の本棚をIKEAのBillyにした話

今の部屋には、引っ越す前から育てている無印のパルプボードボックスを利用した本棚があるのだけど、じわじわと紙の本が増えて、そろそろ本が溢れるようになってきた。このままパルプボードを増設していってもよかったが、この製品は仕様上は5段までしか重ねられず、壁の上部が有効利用できない問題があったため、ここはひとつ背の高い本棚を導入し、本棚の設置面積を圧縮して部屋をより広く使うことにした。ゆくゆくはソファを置くなどもしたい。

f:id:ono_matope:20181219193137j:plain
before: 無印のパルプボードボックス

本棚選び

我が家の本棚としての要件は、大判の美術書から文庫版の漫画まで、効率よく収納するために可動棚をもつことと、スッキリしたデザインであることだったが、デザインについて納得できるものがなかなか見つからず、本棚選びは難航していた。

無印のパルプボードボックスがそうであったように、ぼんやりした曲線を排したソリッドなかたちの本棚が欲しかったし、色に関しては、部屋がドアや木枠を含めて白で統一されていたので、本棚も白というのは心に決めていた。その点で、入手しやすい定番商品である 大洋のエースラック も、白井産業のタナリオも選択できなかった。エースラックはなんかちょっとカーブが入っていたのがどうしても許せなかったのと、タナリオのホワイトはうっすら木目が入っており、展示店舗が近くにないために実際の見栄えを確認することができなかったためだ。

このあたりでKEAの定番書棚Billyに行き着いた。

www.ikea.com

Billyはまさに上に挙げた要件を満たす理想の本棚だったが、購入に踏み切れない問題として、「たわみ」問題があった。どうも調べてみると、買おうと思っていた幅80cmのBillyは本を目一杯に収納していると、かなりの高確率で棚がたわんでくるらしかった。

たわむのはかなり見栄えが悪いので躊躇したが、結局80cm幅のBillyを諦め、40cm幅のBillyを連結することで回避することにした。40cmのBillyでは、さすがにたわみの報告は見られなかったためである。結果的に、40cm*3のマス目デザインは見た目にも良いものであった。

もう一つの問題として、転倒対策があった。Billyはかなりの重量があるので、倒れてくるとマジで危ない。IKEAの本棚は日本の家具と違い、重心が手前側に来るので、L字金具による壁固定など、転倒防止をしないと危険だとIKEAの店員に教えてもらった(どの家具でも何らかの対策は必要なのだが)。

しかし部屋は賃貸なので壁への釘打ちは躊躇われる。どうしたものかとネットを検索していたら、家具転倒防止おじさんこと篠原進さんが考案した対向くさびという転倒対策が目に入った。書棚と天井の間に楔状に組み合わせたスタイロフォームを詰め、震災時に転倒する方向への力を抑えるというものだった。これと、一般的な転倒防止シートを組み合わせて転倒防止策とすることに決めた。

家具転倒防止―経験交流サイト

auient.hatenablog.com

設置

というわけでIKEAオンラインストアで40cm幅のBillyを3台と追加棚を注文。工作精度への期待と横連結のため、組み立てサービスを依頼した。連結は別料金3000円でやってくれる。配送と組み立ては異なる業者により行われるので、配送から組み立てまでの間、届いた部材を部屋に置いておく必要がある。

設置されたBilly。連結されているため、この時点で一人では持ち上げるのが難しいほど重い。しかしなんとか頑張って足元にふんばる君120をはさんである。Billyは両側面に出っ張りがあるため、適宜カットして挟んだ。

f:id:ono_matope:20181222111715j:plain
組み立てが完了したBilly

ニトムズ 家具転倒防止安定板 ふんばる君 120 M6090

ニトムズ 家具転倒防止安定板 ふんばる君 120 M6090

連結は、使わない棚穴にビスを通して行われる。IKEAの家具は一度分解すると再組み立てが難しいため引っ越し業者に敬遠されるといわれるが、ビスは簡単に外して再連結できるそうなので、引っ越しの際は横連結を解除すれば問題なさそうである。

f:id:ono_matope:20181222171308j:plain
連結ビス

対向くさびを作ろう

さて、篠原式のスタイロフォーム対向くさびを作成する。東急ハンズで21cmx10cmx6cmの白のスタイロフォームレンガ、ノンスリップシート、発泡スチロールカッターを購入。レンガ一つで2つのくさびを作れる。(6つ作ろうと思ってレンガを3つ購入したが、強度的に2つで充分ですよだったので、レンガは余った。)

f:id:ono_matope:20181223222849j:plain
素材

棚から天井の梁まで高さ7.5cm。奥行き10cmのスタイロフォームに勾配1/10の斜面を作るため、1cmの高低差で線を引く。

f:id:ono_matope:20181223154440j:plain
罫書き

スタイロフォームの上下にノンスリップシートをあわせ、(霧吹きがなかったため)クッキングペーパーで水を含ませ…

f:id:ono_matope:20181223162940j:plain
ノンスリップシートに挟まれたスタイロフォーム

ハンマーで押し込む。なんかちょっと角の壁紙が裂けてしまった気がするが、気にしない。これで完成である。ふんばる君を敷いて本を収納した時点で、重みからそこそこ安定はしていたが、対向くさびを追加することで本当に押しても引いてもビクともしない、まるでL字金具で壁に固定されているかのような頑丈さになった。これはすごい。

f:id:ono_matope:20181223223845j:plain
対向くさび完成

パルプボードボックスから本を移して完成(写真は対向くさび設置とパルプボードボックスの撤去の前)。初期よりだいぶ設置面積がおさえられたし、部屋にもよく馴染んでおり満足。ただ、新しい本棚の容量をすでにほぼ使い切っており、写真にもあるように本などが少し入りきっていない。これとは別に映画のパンフレットの束も本当は本棚にしまいたかったし、会社に置いてあるダンボール一箱分の技術書もいつかは持ち帰ってこないといけないので、Billyがもう一台必要かもしれない。しかし既に述べたように3台連結済みのBillyは絶望的に重く、場所の微調整などできればしたくないので迷うところだ。しばらくは完全移行せず、パルプボードボックスを一部残して運用するかもしれない。本棚沼はこわい。

f:id:ono_matope:20181222142319j:plain
After: IKEA Billy 40cm x 3

ざっくり理解するPaxos

ゴールデンウィーク中、Appleオープンソースとしてリリースした分散データベース FoundationDB のドキュメントを読んでいました。なかなか面白いデータベースだと思うのでこれについては別途書きたいですが、それはそれとしてFoundationDBでは、分散環境下でACIDトランザクションを実現するために、分散合意プロトコルとして有名なPaxosを採用しているようでした。

PaxosはGoogleのChubbyやCassandraのLight Weight Transactionなどで使われていますが、僕はいまだにPaxosがどのように動作するのかあまりよく分かっていませんでした。良い機会だと思い、FoundationDBの技術を理解するためにも連休の後半でLeslie LamportによるPaxosの論文の一つ Paxos Made Simple を読み、何となくわかった気持ちになったので備忘録としてここに書き残しておきます。(間違いがあったらマサカリ頂きたいです)

Paxosが達成したいこと

Paxos を使って達成できるのは、簡単にいうと以下のようなセマンティックです。

  • ネットワーク上の複数ノードが、「提案」された値の中からひとつの値だけを 「選択」したい。
  • 一度値が「選択」されたら、その値はあとから未選択状態に戻ったり、値が変わったりしない。
  • 多少の参加ノードが途中で故障したり復帰したりしても、残ったメンバーで破綻なくプロトコルが続行され、選択に至ることができる。

一度選択された決定が、後からなかったことにされたり、ひっくり返されたりしない、というのは日常生活からも有用性さが納得できる話です。ここでは、例として「今日のお昼ご飯」を決定する例に挙げます。ここでは複数のメンバーが「ラーメン」「カレー」「ピザ」などの値を「提案」し、そのなかからひとつのメニューを「選択」し、そのメニューのお店にメンバー全員で一緒にご飯を食べに行きたいとします。(例題設定は @nobu_k さんのスライドからお借りしました)

メンバーからの提案を受けてメニューを決定するリーダーが一人だけいれば、「ラーメンにしよう」などとすぐに「選択」できて話は簡単です。しかし、このワンマンリーダーはプロジェクトの単一障害点なので、プロトコル中に突然の交通事故で病院に搬送されてしまうと、残されたメンバーは昼飯のメニューを選択できなくなります。そのような場合、他のメンバーをリーダーに昇格させ、新リーダーに「カレーにしよう」などと選択してもらうことができます。しかし、旧リーダーの傷が浅く、すぐに病院から戻ってきて、「ピザにしよう」などと選択を続行する可能性があります(Crash-Recovery故障モデル)。これは一度決まった決定がひっくり返り、値が一つより多く選択されてしまった状態と言えます。こうなると、メンバーのうち一部は新リーダーとカレーを食べに、一部は旧リーダーとピザを食べに行ってしまうなどの分裂状態に陥る可能性があり、全員で一緒にご飯を食べるという目的は達成されません(Split-Brain障害)。

Crash-RecoveryによるSplit-Brainを避ける手法として一般的なのは、リーダーに心拍計を取り付け、確実にリーダーが死亡してから新リーダーを昇格させるというものです。しかし、この死亡確認までの所要時間は、長すぎるとメンバーはいつまでも昼飯に出発できない(可用性の低下)し、かといって短すぎるとやはりSplit-Brainを起こします。(SRE本では他にもSTONITH(Shoot The Other Node in the Head = リーダーの頭を撃ち抜く)コマンドというより積極的な手法も紹介されていますが、こちらもネットワーク障害などでリーダーの機能を確実に停止できない可能性あり完全ではありません)

このように、従来一般的な単一の「マスター」ノードに値の「選択」をさせるアーキテクチャは、Crash-Recovery故障モデルでの耐障害性がありません。そこで、単一のリーダーノードによる独裁的な決定ではなく、分散された複数のノードによる「合意」に基づいた選択をすることによって、ノードが多少故障したりNWが一時的に分断しても値を選択し続けられるような耐障害性を獲得したいという動機があり、そのための分散合意アルゴリズムのひとつがPaxosです。

…というのが前提についての自分のおおざっぱな理解です。

Paxos

ここからPaxosの解説を始めます。

Paxos では、参加ノードを、値を提案する Proposer, 提案を受け入れる Acceptor, 提案の合意状態を観測して「選択」を判定する Learner の3種類のロールに分けます(Fig.1)。それぞれ何台いても良いし、同じサーバーが複数のロールを兼務してもよいです。 Proposerにより提案された値が過半数のAcceptorに受理(accept)されたとき、Learnerはその値を「選択」することができます(そのため、Acceptorノードは3つや5つなど、奇数が好ましい)。そのため、Paxosは、Acceptorの過半数が生き残っている限り、合意に至るプロトコルのどの段階でProposerが全滅したり復活したりしても破綻しません。

Fig.1: f:id:ono_matope:20180513201645p:plain

通常、Acceptorが値をAcceptするとLearnerに通知されるのでLearnerは素早く合意を検知できますが、パケットロスなどのため、Learnerが常に形成された合意を知りえるとは限らないので、そういう場合はLearnerからAcceptorにaccept状況を確認しにいく必要があります。

Paxosプロトコル

さて、この各々のAcceptorノードに、どのようなプロトコルで値をAcceptさせるかが肝心な点です。ナイーヴなプロトコルではPaxosが求める正しさや耐障害性は得られません。たとえば、単純な「各Acceptorは最初に自分に提案された値をAcceptする」プロトコルでは、同時に3つ以上の提案がなされたり(Fig.2)、合意形成前にAcceptorがFailしたりすると(Fig.3)、いずれの提案も過半数を獲得できずにプロトコルが終了しません。反対に、「最後に提案された値をAcceptする」プロトコルの場合は、いちど値が選択されたあとも、選択値が変化してしまい、目的を達成できません。

Fig.2 単純なFirst-Write-Winでは3つ以上のproposeで誰も過半数を握れなくなる f:id:ono_matope:20180513201841p:plain

Fig.3 単純なFirst-Write-Winではacceptorのfailで誰も過半数を握れなくなる f:id:ono_matope:20180513201902p:plain

Paxos では、Proposerが提案するそれぞれの提案に各提案にユニークな番号(便宜上、ここでは提案番号と呼ぶ)を付与し、フェーズ1 (prepare) / フェーズ2 (accept) のふたつのフェーズからなるプロトコルで提案をやりとりします。提案番号は、提案どうしで重複してはいけないので、時刻とノードIDの組み合わせなどで生成します。

フェーズ1: Prepare / Promise

Paxosのフェーズ1は、Proposerが値を提案するところから始まります。フェーズ1のルールは以下の通りです。

  • Phase 1. (a): Proposerは過半数のAcceptorに、生成した提案番号nとともにprepare(n)要求を送信する。
  • Phase 1. (b): Acceptorが既に応答したどのprepare要求の提案番号よりも大きな提案番号nとともにprepare要求を受信したら、nより小さな番号の提案は今後acceptしないという約束(promise)と、もしあれば現在までにacceptした最大の番号の提案とともに要求に応答する。そうでなければ、Acceptorはこのprepare要求を無視する。
    • 無視とあるが、これはタイムアウトと無視エラーレスポンスを同一視してよく、レスポンスを返さなくてもプロトコルは破綻しないという意味で、パフォーマンス最適化のために即時に無視レスポンスを返してもよい。

Proposerが過半数のAcceptorからpromiseレスポンスを受け取れたらフェーズ2に進みます。できなければ、もっと大きな提案番号を用いて最初からやり直します。少し複雑なので、以下に図解します。(Fig.4, Fig.5, Fig.6)

Prepare要求した提案番号がAcceptorの最大のpromise済み提案番号よりも大きかった場合

この場合、AcceptorはProposerにpromise応答を返します(Fig.4)。Acceptorがすでに何らかの提案をacceptしていた場合、その最大の提案をpromise応答に付与します(Fig.5)。

Fig.4 f:id:ono_matope:20180513202326p:plain Fig.5 f:id:ono_matope:20180513202334p:plain

Prepare要求した提案番号がAcceptorの最大のpromise済み提案番号よりも小さかった場合

AcceptorはPrepare要求を無視します。(Fig.6)

Fig.6 f:id:ono_matope:20180513183720p:plain

フェーズ2: Accept

  • Phase 2(a): proposerがprepare(n)要求に過半数のacceptorからレスポンスを受信したら、それらのAcceptorに、提案番号n、値Vのaccept要求を送信する。Vはレスポンスされた中で最大番号の提案の値か、もし何の提案も受け取っていなければ任意の値。
  • Phase 2 (b): accept(n,V)要求を受け取ったAcceptorは、もしnが今までpromiseした最大の提案番号よりも大きければ、その提案をacceptする。そうでなければ無視する。
    • Phase 1 (b) と同様、無視ではなくIgnoreレスポンスを返すことで素早くリトライできる。

過半数のAcceptorが同じ値をacceptすると、合意が成立したとみなされ、Learnerはその値を「選択」することができます。Phase 2(a) での値Vの選択がやや複雑ですが、ここがPaxosのキモです。

accept要求の提案番号が、Acceptorがpromiseした最大の提案番号以上だった場合

問題なくacceptされる。

Fig.7 f:id:ono_matope:20180513202431p:plain

accept要求の提案番号が、Acceptorがpromiseした最大の提案番号より小さかった場合

accept要求は無視される。

Fig.8 f:id:ono_matope:20180513202517p:plain

Paxosプロトコル

Paxos Made Smple で定義されているプロトコルはこれだけです。本当にこれだけのプロトコルで、「値がただ一つだけ選択される」が実現できるのか、実際に試してみよう。ここではProposer2ノード、Acceptor5ノード、Learner1ノードの集合を想定します。本当はラーメン・カレー・ピザの例を続けたかったですが、作図上の都合により、値V1,V2,V3と記して説明します。

一番かんたんなパターン

まずはすんなり選択に至るパターンを試してみましょう。Proposer P1が提案番号n1, 値V1 の提案(n1,V1)を提案したいとします。

フェーズ1

Fig.9: P1が過半数のAcceptor(ここではA1,A2,A3の3ノード)に提案番号n1をprepare要求する。全てのAcceptorがn1をpromiseしてくれたのでフェーズ2に進む。 f:id:ono_matope:20180513203218p:plain

フェーズ2

Fig.10: P1がpromise応答したAcceptorにaccept(n1,V1)要求をする。いずれのAcceptorもこれをacceptする。 f:id:ono_matope:20180513203234p:plain

過半数のAcceptorが同一の値をacceptしたので、Learner L1は値V1を「選択」できるようになりました。よかったですね。

選択後に他の値が提案されるパターン

ところで、Paxosの合意においては、いちど値が選択されたら、それ以外の値が選択されてはいけません。たとえば、この状態からProposer P2が、大きな提案番号n2で値V2を提案した場合、はたして値V2が選択されずにいられるでしょうか。ためしてみましょう。(n1より小さな提案番号を使った場合については、フェーズ1を満たせず棄却されることが自明なので省略する)

フェーズ1

Fig.11: P2が過半数のAcceptor A3,A4,A5に提案番号n2をprepare要求する。全てのAcceptorはpromise応答するが、(n1,V1)提案をaccept済みのA3はこの提案(n1,V1)をpromise応答に付与する。 f:id:ono_matope:20180513203253p:plain

提案(n1,V1)はすでに過半数のAcceptorによりacceptされているので、このとき、P2がどの過半数のノードの組み合わせにprepare要求しようとも、少なくともひとつのノードからは提案(n1,V1)つきのpromiseが応答されることになります。

フェーズ2

Fig.12: ルールPhase2(a)により、P2は、元々の提案(n2, V2)を、acceptorから受け取った値V1で書き換えた提案(n2,V1)をノードA3,A4,A5にaccept要求する。このaccept要求は、すべてacceptされるが、結局すでに選択済みの値と同じ値がacceptされるに過ぎず、Learnerが選択する値はV1のまま変わらない。 f:id:ono_matope:20180513203302p:plain

このように、各ProposerがPaxosプロトコルを守る限りにおいて、いちど過半数にAcceptされた値(V1)はくつがえされることはありません。

これで、Paxosがどういう挙動をするものなのかなんとなくイメージできたと思います。

衝突するパターン

もう少し複雑な、同時に提案が走ったり、途中でProposerが落ちたりするパターンを試してみましょう。

P1がA1,A2,A3に(n1,V1)をprepare要求します。 f:id:ono_matope:20180513162536p:plain

P2がA3,A4,A5に(n2,V2)をprepare要求します。 f:id:ono_matope:20180513162626p:plain

P1がA1,A2,A3に(n1,V1)をaccept要求しますが、A3が受理しなかったのでやり直しになります。 f:id:ono_matope:20180513162739p:plain

P2がA4,A5に(n2,V2)をaccept要求しますが、A3に要求する前にプロセスが停止し、離脱してしまったとします。 f:id:ono_matope:20180513162855p:plain

そこに新たなP3が登場し、(n3,V3)を提案しようと、A2,A3,A4にprepare要求します。すでにP1,P2による提案をacceptしているノードからは、それらの提案がpromise応答されます。 f:id:ono_matope:20180513163040p:plain

P3は、提案(n3,V3)の値をpromise応答中最大の値に書き換えた提案(n3,V2)をA2,A3,A4にaccept要求(n3,V2)します。これで過半数のノードが同じ値V2をacceptしたので、選択値はV2に確定します。よかったですね。 f:id:ono_matope:20180513163212p:plain

ざっくりと雰囲気でいうと、Paxosは

  • Acceptorは、promiseした番号以上の提案番号だけをacceptする
  • Proposerは、Acceptorがacceptしている最大番号の提案で自分の提案を上書きする

のルールの組み合わせによって、後から提案するProposerほど、すでに場に出ている提案を追認せざるを得ない仕組みで選択値の変動を防いでいるようです。

Multi-Paxosと複製ステートマシン

ここまでで、Paxosがいかにお昼ご飯を選択するのに便利なプロトコルか理解してもらえたと思いますが、実際のところ、Paxosそのものでは大したものは作れません。Paxosのうまみは、単一の値についての合意というよりも、これを順序つきのコマンド列についての合意に拡張し、複製ステートマシン(RSM)の保持に応用できる点にあります。(Paxos Made Simpleの最後の章はこの複製ステートマシンの実装についての記述になっています。この論文にはMulti-Paxosという名前は一度も出てきませんが、他の論文や資料ではこの手法をMulti-Paxosと呼んでいるので、たぶんこれがMulti Paxosなんだと思います)

ステートマシンとは、ある状態にコマンドを入力して出力と新しい状態を生成する概念上の機械です。例えば、銀行システムは口座の全ての集合を一つのステートマシンと考えることできて、預金の引き出しは、口座に引き出し金額以上の預金がある場合に引き出し金額を出力し、金額を差し引いた新しい状態に遷移するコマンドと表現できます。複製ステートマシンでは、初期状態から全く同じ操作を、同じ順番で適用することで、複数のレプリカの状態を同一に保つことができます。RDBのbin-logみたいですね。

こういったシステムでは、一般的に単一のマスターサーバー(マスターDB)が全ての更新順序を決定する役割を担いますが、冒頭で議論したように、単一マスターのシステムは可用性とSplit-Brainの問題に悩まされることになるので、やはり分散合意による高い耐障害性を獲得したいわけです。そのためにMulti-Paxosを使います。

Multi-Paxos の仕組み

Multi-Paxosは、「連続したコマンド入力を受け取り、コマンドが同じ順序で適用されるように合意していく」ことで複製ステートマシンを構築します。複製ステートマシンは、順番つけられた、個別のPaxosインスタンスの列を持ちます。i番目のPaxosインスタンスで選択された値は、i番目のステートマシンコマンドになります。クライアントがProposerにコマンドを要求すると、そのProposerはそのコマンドが何番目に実行されるべきコマンドかを決定し、その番号の値についてAcceptorに提案を行います。

f:id:ono_matope:20180513145042p:plain

ところで、Paxosである値を選択するには、Prepare/Acceptの2フェーズが必要で、アクセプターが5台の場合は最低6ラウンドトリップ、さらに他の提案と衝突した場合はリトライのため追加のラウンドトリップが必要です。これではステートマシンとして使うにはあまりにスループットが低いです。

そこで、Multi-Paxosでは特定のProposerをLeader Proposerに選出し、そのノードにコマンド順序の決定を行わせる最適化を行います。さらにPaxosプロトコルを拡張し、Proposerは「n番目以降の全てのPaxosインスタンスに対して、同じ提案番号でAcceptorにPrepare要求」ができるようになります。これらの拡張により、リーダープロポーザは、リーダー選出直後などのレアなイベント時をのぞいてフェーズ1を省略でき、さらに提案同士の衝突による性能低下も回避できます。 

f:id:ono_matope:20180513203859p:plain

リーダーを選出と聞いて、Sprit-Brainが起きないか心配してしまうかもしれませんが、あくまで性能最適化のためのリーダーに過ぎず、もしリーダー以外のノードがコマンドを提案したとしても(性能は低下しますが)、個々のPaxosにより正しさは守られているので、選択値が壊れたりはしません。

Multi-Paxos リーダー交代時の挙動

リーダープロポーザの故障などで他のプロポーザにリーダーシップを切り替える場合を考えてましょう。まず、新任リーダーは、コマンドを受け入れる仕事を始める前に、決定済みの全てのコマンドの決定を知る必要があります。Multi-Paxosでは基本的にProposerはLearnerも兼ねるので、リーダーを任された時点でほとんどの選択はLearnしているはずですが、単一Paxosの項で少し触れたように、Acceptor側の全ての合意が即座に全てのLearnerに通知されるわけではないので、認識に欠けがある可能性があります。

では、この新Leader Proposerがどうやって完全な決定を知るかというと、旧リーダーのものより大きな提案番号で「n番目以降の全てのPaxosインスタンスに同じ番号でPromise要求」コマンドを使います。このコマンドを受けたAcceptorは、promiseとともに「n番目以降の全てのAccept値」をレスポンスするので、これで新Leader Proposerは全ての未認知コマンドの選択値を入手できます。ちなみに、この方法でも選択値が確定できなかった虫食いコマンドのインデックスに対しては、なんと「No-opコマンドを提案」してスキップします。歴史改変じゃないかと心配になりますが、上記プロトコルで選択値が得られなかったインデックスのコマンドは、過去においても一度も選択されていなかったコマンドという保証があるので大丈夫ということのようです(つまり、プロトコルが正しく動いている限り虫食いは現れないはず)。

Multi-Paxos その他

そのほか、Multi-Paxosについてはメンバーシップ変更時は、メンバーシップの変更そのものをひとつのコマンドとして記録すればいいよとか、Leader Proposerは、提案が成功すれば、Learnを待たずに次の提案に移って良いけど、未Learnのコマンドの個数はα個までに制限すると良いとか、細かい議論がいくつかあります。

PaxosとRaft

ここまでPaxosとMulti-Paxosの動作を見てきました。しかし、Paxosは現在は以前ほど流行っていないようです。機能と性能がほぼ同じだがシンプルで理解しやすいとする新しい分散合意アルゴリズムのRaftが2013年に発表されました。また、Paxos Made Liveでは、Paxosは現実の分散システムに実装するためには論文に書かれていないことが多く、最終的なプロダクトは「未証明のプロトコル」になってしまうと指摘されています。実際、定評のあるオープンソース製品でPaxosを実装したものはあまり聞きません。それこそCassandraのLWTやFoundationDBくらいでしょうか。

一方、Raftは(まだ読んでいないのですが)、リーダーエレクションなど、実装に必要な詳細が論文化されているため多様なオープンソース実装が存在し、特にGo言語向けの github.com/coreos/etcd/raft は、etcdだけではなくCockroachDB、TiDBにも採用されるなど、新時代の分散DBの興隆を支えている感があります。そういうわけで、次はRaftを勉強したいと思います。

Raft Consensus Algorithm

参考文献

SRE サイトリライアビリティエンジニアリング ―Googleの信頼性を支えるエンジニアリングチーム

SRE サイトリライアビリティエンジニアリング ―Googleの信頼性を支えるエンジニアリングチーム

Googleを支える技術 ?巨大システムの内側の世界 (WEB+DB PRESSプラスシリーズ)

Googleを支える技術 ?巨大システムの内側の世界 (WEB+DB PRESSプラスシリーズ)

2016年を振り返って

あけましておめでとうございます。

2016年を振り返ってみます。

アジェンダ

  • 仕事:自分で立ち上げたプロダクトをローンチできた
  • 英語:海外出張がたて続き、英語能力の不足を痛感した
  • OSS:GoとCassandraにぼちぼちパッチを送っている
  • 抱負

仕事

取り組んでいたGoのプロダクトを1月にきちんとローンチ出来た。以前参加していたプロジェクトではいろいろあって挫折を味わったが、これはその反省から自分で立ち上げて、技術的には完全に統括し、自分がこうあるべきだと信じるやり方で開発したプロダクトだったので、実際に動くコードとなり価値を生み出すことができたというのは何より嬉しい。遅すぎるかもしれないが、やっとソフトウェアエンジニアとして一人前になれたと感じた。社内プロダクトだが、どこかで成果を外に出したい。

(そういえば自分が8年前に今の会社に入った時、配属先決定面談で上司に「技術力を身につけて、一人前になりたい」と言った気がする。では一人前のエンジニアとは何なのか自省してみると、「多くの人に価値を認められるソフトウェアを生み出すことができる人間」だろうか。自分はそうなりたいんだったなということを思い出したのでここに書いておく。元旦だし)

今年の後半はいろいろあって海外出張が立て続いた。9月はサンノゼのCassandra Summit、11月はラスベガスのAWS re:Inventに参加した。予備日にシリコンバレー巡りをしたりヨセミテ公園まで足を伸ばしたり、re:Inventではグローバル企業のパワーを間に当たりするなど、楽しみもしたし得るものが多かった。

英語

2015年から英会話レッスンを始めたのはこういった機会があると予期してのことだったので、その自己投資は正しかったわけだけども、一方でその投資が足りなかったのか、ネイティブのスピーチやミーティングについていくにはもっと勉強が必要だと痛感する機会ともなった(ちなみに英会話レッスンは5月まで続け、それ以降は社内で始まった英会話レッスンを受けていた)。

最近は HiNative Trek と TEDict をぼちぼちやっている。今年は英文法の基礎を固めなおすのとディクテーションを強化したい。

trek.hinative.com

OSS、その他活動

Go

2015年はGoの net/http にコントリビュートできたのが自分の中で大きかったが、2016年は「go getをGithub Enterpriseネイティブに対応させるパッチ」を送って Go1.9 Milestoneに入れてもらった。 2月にGo1.8がリリースされたらレビューが開始され、うまくいけば1.9に入るので、もうドメイン末尾に.gitをつけたりgo getを改造したりせずに、普通にGHEでGoが使えるようになる。「社内GHEでGoパッケージの管理どうするの?」という、悩ましい問題に直面した時、問題そのものにアタックして問題を消し去る、というアプローチが推奨され、実際にそれを行う敷居が低いのがGoのいいところだな、と思う。

github.com

OSSではないが、deeeetさんに呼んでもらい、Go 1.7 Release PartyでContextの話をできたのも楽しかった。実はContextパッケージが出てきた時点では「これ対応する必要あるのかなー」と懐疑的だったので、同じように懐疑的な人たちにContextの良さを伝えられたら良かったと思う。仕事でもその後Contextをヘビーに使うことになったので、そのリファレンスとしてとても役に立った。

Cassandra

Goの他に、2016年はCassandraにもいくつかのパッチをコントリビュートできた。ひとつは、system_tracesで取得可能なクエリのトレース記録に、プリペアドステートメントのCQL文を含めるようにしたというもの。Cassandraパフォーマンスチューニング中にカッとなって実装した。

もう一つは、Cassandra 3.7ではBig Partitionの扱いに大きな最適化が導入されたが、その最適化後のコードを読んでいたら明らかに無駄な配列のアロケーションが存在したので、そのアロケーションを削除して30%程度の高速化(Big Partition条件下において)をしたというもの。やったことは数行のコードを削除して幾つかの代替実装のベンチマークとともに提示しただけで、ほとんどこぼれ玉を拾ったみたいな話だけど、データベースを速くする仕事というのは実は昔から憧れるもののひとつだったので、少し夢がかなったような気持ちがある。

[CASSANDRA-12731] Remove IndexInfo cache from FileIndexInfoRetriever. - ASF JIRA

抱負

2017年の抱負はこんな感じです。仕事は仕事でやるとして

  • 一人暮らしを始めて今年で4年になり少し手狭になってきたこともあり、今年の契約更新までに引越しをしたい。そしてドラム式洗濯乾燥機を導入したい。
  • プレゼンと質疑応答ができる程度に英語力を上げたい。一方的にしゃべるだけなら練習すればなんとかなるけど、質疑応答の壁が高い。

本年もよろしくお願いします。