おそらくはそれさえも平凡な日々

ghr 0.9.0をリリースしました

https://github.com/tcnksm/ghr/releases/tag/v0.9.0

GitHub Releasesに手軽に成果物をアップロードできる、みんな大好きghrですが、最近deeeetさんが忙しく、メンテが滞っていたこともあり、僕もメンテに加わらせてもらうことにしました。

ghrは僕個人として常用させてもらっている他にも、Mackerelのプラグイン作成時にも利用を推奨しています。今回取り込んで欲しいpull requestがあったため、メンテ権をいただくことにしました。

そのあたりのpull requestを諸々マージして、ghr v0.9.0をリリースしました。0.5.4からジャンプアップ気味ですが、そろそろ1.0.0を見越してもいいのかなと言う気持ちです。

今回一番大きな変更点は、リリースがアトミックになったことです。これが今回入れてほしかった変更です。

これまでのghrでは

  1. リリースを作成する
  2. そのリリースに対して成果物をアップロードする

という手順になっており、1と2の実行間でlatest releaseを取得しようとした場合、そのリリース中のバージョンを取得してしまい、正しく成果物を取得できない可能性がありました。これを以下のように変更しました。

  1. リリースをDraftとして作成する
  2. そのリリースに対して成果物をアップロードする
  3. リリースのDraftを解除する

これで、最新のリリースを取得した際に、必ず必要な成果物が含まれている状態を保証できるようになりました。

また、成果物のアップロード失敗時に2回までリトライするようになったことも目玉の変更点です。これでより安定的にリリースをおこなえるようになります。

その他、リリースフローや依存関係のアップデートをおこないました。より安定性と信頼性の増したghrを是非ご利用ください。

homebrew formulaをどうするかはちょっと保留中です。ghtoolsというorgを取得したのでそちらに移すことも考えています。1つのツールに1つのformula repoを作るのもどうなのかという気持ちもある。ご意見ください。

2017年振り返りと2018年の所信表明

2018年になって暫く経った。はてなは半期末が1月なので、年明けに半期末がやってくる感じになっててマネージャーにとっては慌ただしい。その後体制変更などがありあっという間に4月になった。

2017年は2016年にも増して慌ただしい1年だった。3月に育休が取れたのは良かったけど、それ以外な必死になってしまって余裕のない一年だったように思う。

対外的な発表とかOSS的な活動とかが少なかったのは反省点。

書籍「Mackerel実践入門」が出せたのは良かった。はてなに入ったときに意識していたことは「技術を作る側に回る」ということだったので、それがある程度体現できたのは良かった。「ある程度」って言ったのは、まだまだやりたいことがあるということ。今年は単著が書けると良いんだけど、ちょっと無理かもしれない。

Mackerelが新たな時系列データベースを開発できてプラットフォーム移行できたのもめでたかった。これで次の成長の可能性が大きく増したことになる。

その他、良かった点としては、YAPCとかMackerel関係で、地方に行って色々なエンジニアと交流できたこと。福岡では、YAPCでもMackerel関連のイベントでも、 @pyama さんにはお世話になりました。ありがとうございました。

あとは、採用が結構順調なことも良い点。エンジニアもいい人が採用できている。エンジニアの他にもハイクラスのマネージャー層の人が何人か入ってきているのも良くて、違う畑の人に学ばせてもらう部分も多く、そういう優秀な人と働けることにワクワクしている。

この4月で、はてなに入って3年7ヶ月が経った。自分のキャリアの中で一番長く務めた会社ということになる。最近何のために、はてなに入ったのか今、改めて考え直している。そうすると、やはり、Mackerelを成功させることと、はてなという会社を技術ブランディングの点で盛りたてることにあると思う。

Mackerelという技術は面白い、監視やインフラ管理は面白い。これは多くのエンジニアが思っている以上に面白いものだ。これは僕自身もMackerelに関わってわかったことで、もっと周りに伝えていきたいと思っている。

技術ブランディング的には、僕自身は、技術力がそれほど高くない割には、インターネット上ではプレゼンスがあることに常々課題意識があって、プレゼンスに溺れないように、もっとストイックに技術を追求したい気持ちがもともとある。ただ、その意識が強くなって勢いが弱くなってしまっていたことが反省点。

そこは開き直って、僕自身が、はてなの技術ブランディングの上で必要な人間という自覚を持って、プレゼンスに偏ってるのは自分の特性としてなりふり構わず活かしていこうと思っている。それを通して、社内の隠れた技術力が高いエンジニアを社外にアピールしていく役割も果たしたい。

去年良かった本

ピクサー流 創造するちから
反応しない練習
Real World HTTP
ふつうのLinuxプログラミング 第2版
詳解システム・パフォーマンス

Goツールのクロスビルドとパッケージングのためのgoxzというツールを作った

Goツールのクロスビルドと成果物生成には個人的に長らく、goxcを愛用していましたが、その乗り換えとして、goxzというのを作った。go + x(cross) + z(zip)でgoxz。便利です。

https://github.com/Songmu/goxz

goxcは非常に高機能なのですが、僕がその機能の一部しか必要ないことや、goxcのメンテ自体も止まっている(とオフィシャルでも案内されている)ことが気になったので作りました。

具体的には「Goツールのクロスビルドと成果物のアーカイブ生成をパラレルにおこなう」ことしかしない。アーカイブ生成時に、リポジトリからLICENSEやREADMEを自動的にかき集めるのはやってくれます。

基本的には「設定より規約」という感じで、良い感じのデフォルトを決め打ちにして、あまり細かい設定項目などは作らない想定です。

インストール

https://github.com/Songmu/goxz/releases に生成物があります。homebrewのFormulaも用意しました。お好きな方法でインストールしてください。

% brew install Songmu/tap/goxz
% ghg get Songmu/goxz
% go get github.com/Songmu/goxz/cmd/goxz

使いかた

Goプロジェクトのリポジトリ直下で以下のようにすれば、./dist ディレクトリ以下に生成物が配置されます。

% goxz -pv=1.0.1 -os=darwin,linux -arch=amd64 -d=dist ./cmd/mytool
% tree ./dist
dist/
├─ mytool_1.0.1_darwin_amd64.zip
└─ mytool_1.0.1_linux_amd64.tar.gz

後は、みんな大好きghr を使えば、GitHub Releasesに成果物をアップロードできます。

% ghr 1.0.1 ./dist

補足など

成果物配置ディレクトリ内の事前クリーンナップ処理はおこないませんので、ビルド前に整理するか、新規ディレクトリを指定するなどしてください。配置ディレクトリが存在しない場合の新規作成はgoxzはおこないます。

成果物の名前に、バージョン情報を含みたくない場合は、-pvオプションを指定しなければそうなります。

圧縮方式は、windowsとdarwinでzip、それ以外がtar.gzが標準となっていますが、-z オプションを使うことで圧縮方式をzipに統一することができます。

Mackerelプラグインパッケージの作成に利用する

goxz はMackerelプラグインパッケージ作成にも利用可能です。Mackerelプラグインパッケージは、ファイル名にバージョン情報を含まず、また、zipであることが期待されているため、以下のように指定すると良いでしょう。

# mackerel-plugin-hoge 配下で
% goxz -os=darwin,linux,windows -arch=amd64 -z
% tree ./goxz
goxz/
├─ mackerel-plugin-hoge_darwin_amd64.zip
├─ mackerel-plugin-hoge_linux_amd64.zip
└─ mackerel-plugin-hoge_windows_amd64.zip

後は、先にも述べたとおり、ghrを使って、GitHub Releasesにアップロードするのが簡単でしょう。

ぜひご利用ください

単純に比較するのはフェアではないですが、goxcのコードベースが8000行を超えているのに対し、goxzはテスト込みで600行程度ですので、見通しも良いと思います。

goxzを作るにあたって、goxcを参考にした部分もあります。特にコマンドラインオプション設計などは参考になり、改めてgoxcは良くできているな、と思ったりしました。

goxz は既に goxz 自体のビルドに使われています。僕の身の回りのツールも徐々に乗り換えていく予定です。

Goツールの成果物をGitHub Releasesに上げるときのこれからの定番は、goxz + ghr ってことで一つ。

ということで、ぜひご利用ください。機能要望やpull requestもお待ちしています。

horenso v0.1.0 をリリースしました

https://github.com/Songmu/horenso/releases/tag/v0.1.0

変更点は以下の通りです。

Result JSONへの signaled フィールドの追加

以前のバージョンでは、対象のプロセスがシグナルで停止したかどうかを、終了コードが128以上かどうかで判別していましたが、本バージョンでは、プロセスがシグナルで停止したかどうかを正確に判断するようにしました。それに伴い、 signaled フィールドをResult JSONへ追加し、シグナルで停止された場合にこのフィールドに true がセットされるようになりました。signaledtrue の場合、そのシグナルナンバーは exitCode - 128 で求めることができます。

ちなみに、Goでコマンドの実行エラーから終了コードを取得するためのサンプルコードがネット上に散見されますが、殆どが、意図してのことかもしれませんが、シグナルが考慮されていません。そのあたりの話はそのうち別途書きます。

アーカイブの命名規則の変更

これは実はミスで、リリーススクリプトを整備したら以前と非互換な命名規則になってしまいました。実際社内のプロビジョニングもコカしてしまう羽目になってしまった…。今後は現状の命名ルールにそろえていく予定です。

以上ご活用ください。

えるしっているか、Lは便利

Perl Advent calendar 2017の6日目です。

LというCPANモジュールがありまして、これはPerlのコマンドラインワンライナーを書きやすくするためのモジュールです。

% perl -MString::Random -E 'say String::Random->new->randregex("[0-9a-z-A-Z]{12}")'

あるモジュールを読み込んでクラスメソッドを呼び出したい時には、上記のように、モジュール名を2回打ち込まないといけないのが難点ですが、それを以下のように書くことができる。

% perl -ML -E 'say "String::Random"->new->randregex("[0-9a-zA-Z]{12}")'

便利。

このモジュールは僕が5年前にCPANに上げたモジュールなのですが、Perl 5.27.1で失敗するテストケースがあり、今年それを直すとともに、バージョンv1.0.0としてリリースしました。

それが補足されて、少し話題になったようで、今年のYAPC::Fukuokaの際に、 @nqounet さんに「最近Lを知ったんですけど、すごく便利ですね」と言っていただきました。ということで、ここで改めて紹介してみることにしました。

L というトップレベルかつ1文字モジュールということで、2012年当時、これをCPANに上げるかどうかはかなり躊躇したのですが、周りのPerl Hackerの皆様に後押ししてもらって上げました。

反響を心配していたのですが、好意的なレビューもついて、一安心したことを覚えています。思い出深いモジュールです。

go-memcached-toolを書いていた

はてなエンジニア Advent Calendar 2017の3日目です。 昨日は、id:y_uuki さんによるウェブシステムの運用自律化に向けた構想 - 第3回ウェブサイエンス研究会でした。

さて、memcachedにはmemcached-toolというツールが同梱されています。

https://github.com/memcached/memcached/blob/master/scripts/memcached-tool

これを、今回(実は大分前に書いていたのですが)、Goに移植しました。

https://github.com/Songmu/go-memcached-tool

以下のようにインストール可能ですが、GitHub Releasesにも置いてあります

% go get github.com/Songmu/go-memcached-tool/cmd/go-memcached-tool

memcached-toolは、memcachedの作者であり、今はGoのメインコミッターの一人であるBradfitz氏の書いたPerlスクリプトなのですが、これを僕がGoに移植したというのがちょっと面白ポイントです。

memcached-toolにはいくつか機能がありますが、今回は、よく使われる、memcachedのslabの状態とかをいい感じに表示してくれる display モードと、memcachedのデータをダンプする dump モードを移植しました。

memcachedのslabとは何ぞやとか、memcached-toolの使い方は以下の記事によくまとまっています。

参考: 第2回 memcachedのメモリストレージを理解する:memcachedを知り尽くす

memcached-toolと同様ではありますが、軽く使い方の説明を。

displayモード

memcachedの各slabの状態を良い感じに表示してくれます。

% go-memcached-tool 127.0.0.1:11211
  #  Item_Size  Max_age   Pages   Count   Full?  Evicted Evict_Time OOM
  1      96B  17550349s       1   10922     yes  1121115        0    0
  2     120B  17529950s       3   26214     yes  2693486        0    0
  3     152B  17656340s       2   13793     yes  1262170        0    0
  4     192B  17652024s       2   10917     yes   139583        0    0
  ...

dumpモード

memcachedの内容を書き出すことができます。

% go-memcached-tool 127.0.0.1:11211 dump > memd.dump

以下のようにすれば、あるサーバーのキャッシュをゴソッと別サーバーに移すことができるのが面白いですね。実際にはこんな乱暴なことはしないとは思いますが。

% go-memcached-tool 127.0.0.1:11211 dump | nc cache2.example.com:11211

その他のモード(stats/settings)について

オリジナルのmemcached-toolには statssettings と言ったモードがありますがこの辺は以下のようにすれば同じようなことができるので特に移植予定はありません。

% echo "stats"          | nc 127.0.0.1 11211
% echo "stats settings" | nc 127.0.0.1 11211

ライブラリとして使う

以下のようにライブラリとしても利用可能です。Goでmemcachedをモニタリングするようなツールを書く際に有用でしょう。

import (
    "net"
    "github.com/Songmu/go-memcached-tool"
)
func main() {
    conn, _ := net.Dial("tcp", "127.0.0.1:11211")
    items, err := memdtool.GetSlabStats(conn)
    ...
}

コマンドラインツールを書く時に、その言語のライブラリとしても再利用可能にすることは個人的に心がけている点です。実は、このgo-memcached-tool自体、mackerel-plugin-memcachedの拡張用のライブラリとして利用するために書き始めた背景があります。

ぜひご利用ください。

ISUCON7本戦、3位でした

運営の皆様お疲れ様でした。今年も本当に楽しませてもらいました。

最終スコアは27,816。2位と僅差(301点差)の3位でした。惜しかったとも言えるけど、1位は6万点超えなので惨敗です。

問題

WebSocketを使った複数人同時接続のクッキークリッカー的なゲームでした。ゲームとWebSocketは来そうな予感はしていました。ちなみに、チームメンバーは誰もクッキークリッカーやってなかった。

最初にレギューレーションを読みながら以下のような会話と作戦立てをした。

シャーディング

とりあえず、シャーディングしましょう、ということで手を動かし始める。言語はGoを選択。

僕は、サーバー割り当てのロジックを書くことにした。とりあえず状態持つのもメンドイので最初は雑にルーム名のハッシュ値で分散させることに。すぐ終わる予定だったが、ハッシュ値から数値を取り出すのに案外苦労した。

その間にmotemenは、 m_items (マスタデータ)を変数化したりしていた。それが素早く終わっていたので、僕がやる予定だった初期化の広報処理(どこかアクセスが来たら、全台の initialize 処理を叩きに行く)のをお願いした。それも一瞬で実装してて流石だった。

masayoshiにはサーバーの設定とか、deployの設定とか、サーバー全台にRedisを立てたりしてもらっていた。

12時くらいにお弁当が配られ始める頃にちょうどこのあたりの実装が終わった。一足飛びにRedis実装まで行くか、みたいな機運もあったんだけど、お弁当を食べながら一旦、全台にMySQL置いてベンチ回してみるか、という話になった。

その辺の設定をを終えて、飯を食いながらベンチを回す。この辺で、スコアは初期実装の5500程度から15000位まで伸び、暫定トップになったんだけど、このへんで暫定トップになるの過去を鑑みても死亡フラグな気がする。

プロファイル

これで各サーバーがシャードできたのでRedis化していくぞ!ってなりかけたんだけど、ここでmasayoshiが「でも、ベンチ中にGoのアプリが150%以上使ってて、MySQLは20%しか使ってませんよ」と指摘。言われてみれば確かにその通り。

先にやるべきはアプリの改善ということで、motemenがプロファイルを取ってくれた。pprofgo-torch を使って、flame graphを出力。

めっちゃ便利で、とにかく、どこがボトルネックか一目瞭然である。 calcStatus が全体の処理の67%を占めており圧倒的に支配的なのであった。Redis化とかオンメモリ以前にここをなんとかしないといけないぞ、となる。

この時点で開始3時間経過した13:20。暫定トップでボトルネックも見えていて、今から思うと、この時点では、かなり良い戦いをしていたように思う。

スコア26という大火傷

プロファイルを読み解いて、ボトルネックになってそうな big2exp の改善にmotemenが取り組んだ。しかし、修正を入れたら大幅にスコアが急落し、脅威のスコア26を叩き出す。暫定トップからの急落である。あとから聞いたら、これだけのスコアで逆にfailしなかったことは、運営部屋でも話題になっていたらしい。

初手でこのような手痛い火傷を負ってしまい、我々は「calcStatus怖い」となってしまった。なので、この関数自体の最適化に取り組むよりかは、キャッシュしたり呼び出しを減らす方向に方針転換した。

今から思うと、これは大きな誤りだった。ユニットテストが設けられていることから分かるように、明らかに calcStatus は最適化ポイントなのだから、もうちょっと真剣に読み解く時間を作るべきだったと競技後にチームで反省した。

細かいチューニング

てことで、以下のようなちまちました改善をおこなった。テストしやすさのためにサーバーは1台に固定しつつ開発。

あまり目覚ましいスコア改善は見られず、この時点で、一台辺りのスコアは7000強。終了時間が近づいてきていて、終戦ムードに。

シャーディングの改善とGCチューニング

ハッシュでシャードするのがどうも安定しないので、素朴にラウンドロビンで分ける実装を作ることに。その間、motemenとmasayoshiは、GCのパラメーターや getStatus の間隔調整して悪あがきをしていた。そう、先のflame graphを見ると、GCが結構ボトルネックになってはいそうではあったので、ここを調整しにいくのは悪あがきとしては悪くなかったのかも知れない。

ラウンドロビンにしたらスコアは大分安定するようになった。ルーム名とサーバーの対応を記録するためにRedisを用いた。今回結局ここでしかRedisは使わなかった。

パラメーターチューニングは案外効果を発揮し、この時点で4台構成にして何度かベンチを回しても、failもせずスコアは2万は割らなくなって安定するようになった。

終戦

ここで残り30分くらいだったので、残念だけど、ここらあたりが潮時かなーというところで、各種ログなどを切るなどの小細工をして、再起動試験。再起動後、無事にアプリケーションも動いたので、後は何度かベンチを回して、27,816が出たところで打ち止めとした。

あとは、 calcStatus の実装読みながら「ここの1000回ループとかいかにも修正できそうだよなぁ」とかそういう話をしたりするなどしていた。

結果としては3位で、途中経過からすると良い順位を取れたなぁという感じではあるけど、これで満足してはいかんな、という気持ちです。

感想

運営の皆様、本当にお疲れ様でした。予選・本戦ともに過去最大級のボリュームだったと思うのですごいと思います。レギュレーションもしっかり書かれていて見事でした。本戦に関しては、結局単に一台あたりの処理能力を上げる戦いになってるなーとか思ったりはしましたが、それをうちのチーム含め、殆どの参加者はチューニングしきれなかったわけで、そういう問題だったんだなと思ってます。

来年の開催を願っています。はてなメンバーだけで優勝したい。

ちなみに、去年は予選・本戦で別の会社が作問しており、予選がはてなで決勝がpixivでした。僕は予選側の作問担当だったのですが、その形式はお互いの負担軽減以上の効果があったように感じています。お互いの問題を知らない状態で、相互にリハーサルができたことが一番良かったことで、それが難易度調整だったり、作問者が気付かない盲点の発見になりました。来年は可能であれば、その形式に戻した方が良いんじゃないかと思っています。

あらゆる日付文字列をよしなに扱うgo-httpdate を書いた

https://github.com/Songmu/go-httpdate

Perl界には HTTP::Date という便利モジュールがあります。これは、あらゆる日付文字列を特にフォーマットの指定無しによしなにパースしてくれるもので、クイックハックに非常に有用です。ISUCONでは毎回使っている気がします。

このモジュールは異常な正規表現によって成り立っています。おそらく元々はその名の通り、単にHTTPのための日付フォーマットを扱うモジュールだったのでしょうが、徐々に拡張が継ぎ足されてこのようなモジュールになったのだと想像されます。

で、これをGoに移植しました。以下のように使います。

import "github.com/Songmu/go-httpdate"

t1, _ := httpdate.Str2Time("2017-11-11", nil)
t2, _ := httpdate.Str2Time("Thu, 03 Feb 1994 12:33:44 GMT", nil)
t3, _ := httpdate.Str2Time("Thu Nov  9 18:20:31 GMT 2017", nil)
t4, _ := httpdate.Str2Time("08-Feb-94 14:15:29 GMT", nil)

文字列をよしなにパースして、 time.Time を返してくれます。非常に便利。対応フォーマットは以下のようになっています。HTTP::Dateがそうなっているので仕方ないのですが、もはやHTTPとは何なのか…、という気持ちになりますね。

ソースコードをご覧いただければわかりますが、愚直に移植したので、正規表現祭りとなっておりますので、パフォーマンスはお察しください。ちなみに計測すらしていません。とは言え実用上そこまで困ることもないはずです。

移植をしていて、Goは時刻を正確に扱うパッケージが標準ライブラリに存在するのは地味に大きな優位性だなーとか思ったりしました。

結構便利だと思うのでお試しください。

GoでSingletonぽいことを実現する、とある方法

ちなみに今回のコードはそれほど実用性はありません。ここまで頑張って、シングルトンぽいことを実現する必要性は感じられないからです。サンプルコードはこちら。

https://www.github.com/Songmu/go-sandbox/

Goでシングルトンを実現する方法として以下の様なコードが良く見られます。

package singleton

import "sync"

type singleton struct{
}

var (
    instance *singleton
    once     sync.Once
)

func GetInstance() *singleton{
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

このコードのグッドポイントとしては、 sync.Once を使っていること。以下のように素朴に nil チェックをする形だと、マルチスレッドで競合が発生するのでアウトです。

if instance == nil {
    instance = &singleton{}
}

とは言え、初めて評価されるときに代入されることにこだわりが無いのであれば、わざわざ sync.Once を使わず、トップレベルで初期化時に代入してしまって良いとは思います。

var instance = &singleton{}

さて、これで、シングルトンは実現できるのですが、個人的に気になっていることがありました。それは、 golint で怒られるということです。

% golint .
singleton/singleton.go:14:20: exported func GetInstance returns unexported type *singleton.singleton, which can be annoying to use

つまり「GetInstance というパブリックな関数が、 *singleton.singleton というプライベートな型を返すのは紛らわしい」ということです。実際、 *singleton.singleton にパブリックなメソッドが生えていれば、それを呼び出すことはできるのですが、それは、godocなどで抽出されないので困りものです。

dummyメソッドを持ったinterfaceを使うという解法

それをdummyメソッドを持ったinterfaceを使う手でこれを解決してみました。dummyメソッドを持ったinterfaceと言うのは、go/ast パッケージなどで見られますが、パブリックなinterfaceの中に、プライベートなメソッドを埋め込むことで、そのinterface自体はパブリックですが、外のパッケージではそのinterfaceを満たす変数を作れなくするというものです。

go/ast パッケージでの様子などは以下の @haya14busa さんの記事に詳しいです。

Sum/Union/Variant Type in Go and Static Check Tool of switch-case handling

今回のシングルトンの場合、パッケージ自体はこのようになります。

package singleton

import (
    "fmt"
    "strings"
    "sync"
    "sync/atomic"
)

// Deeeeter implements Deeeet() method
type Deeeeter interface {
    Deeeet()
    getAge() // as a dmmuy method
}

type deeeet struct {
    age int64 // accessed atomically
}

var (
    d Deeeeter
    o sync.Once
)

// GetDeeeter gets the Deeeeter
func GetDeeeter() Deeeeter {
    o.Do(func() {
        d = &deeeet{}
    })
    return d
}

// Deeeet desu...
func (de *deeeet) Deeeet() {
    age := int(atomic.AddInt64(&de.age, 1))
    fmt.Printf("d%stです…\n", strings.Repeat("e", age))
}

func (de *deeeet) getAge() {
    fmt.Println(de.age)
}

これを以下のようなコードで実行してみましょう。

package main

import (
    "fmt"

    "./singleton"
)

func main() {
    deeeet := singleton.GetDeeeter()

    deeeet.Deeeet()
    deeeet.Deeeet()
    singleton.GetDeeeter().Deeeet()
    deeeet.Deeeet()
}

これを実行してみると以下のようになります。

% go run singleton.go
detです…
deetです…
deeetです…
deeeetです…

ちゃんと GetDeeeeter が同じ変数を返しており、シングルトンの様な挙動が実現されています。Deeeeter 自体はパブリックなinterfaceですが、それを満たす変数をパッケージ外から作ることができないため、この deeeet は唯一無二の存在となります。

https://godoc.org/github.com/Songmu/go-sandbox/singleton にもちゃんとドキュメントが生成されており、ニッコリ。

本当に、パッケージ外から Deeeeter を作ることはできないのか?

それを試すために、 https://github.com/Songmu/go-sandbox/blob/master/singleton.go の末尾に以下のようなコードがあります。Deeeet メソッドと、 getAge メソッドを実装してそれっぽくなっています。

type imitateDeeeet struct {
}

func (ide *imitateDeeeet) Deeeet() {
    fmt.Println("deeeet(偽)です…")
}

func (ide *imitateDeeeet) getAge() {
    fmt.Println("17歳です♥")
}

// var _ singleton.Deeeeter = &imitateDeeeet{}

末尾の代入がコメントアウトされていますが、ここをアンコメントして imitateDeeeetDeeeeter を満たしているか確認してみましょう。すると以下のように怒られます。残念でした。

% go run singleton.go
# command-line-arguments
./singleton.go:29:5: cannot use imitateDeeeet literal (type *imitateDeeeet) as type singleton.Deeeeter in assignment:
        *imitateDeeeet does not implement singleton.Deeeeter (missing singleton.getAge method)
                have getAge()
                want singleton.getAge()

この _ に代入するというテクは、あるtypeが狙ったinterfaceを満たすかどうかを担保するために実コードでも使われるパターンなので、覚えておくと良いでしょう。

追記: @shogo82148 さんに、 de.age の保護が甘いと指摘を受けたので、エントリ内のサンプルコードを直しました。合わせて、GitHub上のサンプルコードも修正しました。

ISUCON7の予選をはてなメンバーで通過してきました

ISUCONは過去3回優勝させてもらっているわけですが、はてなのメンバーだけで優勝したい気持ちがあります。前回はてなのメンバーだけで出たISUCON4の時は本戦は出られたものの惨敗。今回はそのリベンジも兼ねて挑みました。

チーム名は、id:Songmu(ソンムー)・id:motemen(モテメン)・id:masayoshi(マサヨシ)の3人で出たので、「ソン・モテメン・マサヨシ」。役割分担は、僕が一応リーダーで、motemenがアプリケーションメイン、masayoshiがインフラメイン。

予選は、リモート体制で、はてなの東京オフィスと京都オフィスの会議室を繋いで参加しました。僕だけが東京オフィス。

予選参加は土曜参加にした。土曜参加のほうが、翌日の日曜日休めるし、その日はISUCONのことを気にしないで過ごせるので例年そうしている。また、日曜のほうが強豪が集まる気配がしたので、土曜の方が3位以内通過を決めてしまうのも楽かもしれないという打算もあった。

言語はGoで行くことに事前に決めていた。Perlも捨てがたかったのだが、問題の特性上Perlだと辛いこともあるだろうということでGoにした。ただ、Perlの方が明らかにスラスラ書けるし、GoでそこまでゴリゴリWebアプリケーションの表側を書いたこともないので不安もあった。

競技開始直後

事前のレギュレーションに「複数台」という記述があって、こりゃ、予選で複数台来そうだなーと思ってた。しかし、予選で複数台構成でやるのは出題チームの負担がヤバイので本気なのかな、とは思ってた。しかし、予選開始時間が遅れたこともあって「ああこりゃ絶対複数台構成だわ」ってなった。果たして3台構成であった。

当日マニュアルを読むと、帯域について丁寧に説明があり、そのうえ静的ファイルの304についての得点も事細かに書いてあったので「こりゃ帯域ネックになる要素があるな」「304を適切に返させたいんだろうな」みたいなところは読み解けた。

それで、ベンチマーク画面を見ると、複数台に対してリクエスト飛ばせるようになってたので「ああ、これは、1台だけで配信しようとすると最終的に帯域足りなくなるやつで、最後は2台か3台で配信できるようにしないとダメだろうな」と予想。

複数台で静的ファイルを配信させつつ、適切に304を返したいとなると「ああ、これは『ハイパフォーマンスWebサイト』で読んだやつだ」ってピンときた。つまり、複数台でそれぞれちゃんと静的ファイルの更新日時を揃えておく必要があるし、ETagの生成ルールも合わせておく必要がある。

ハイパフォーマンスWebサイト

なんか「CDNがどうこう」という話がでてますが、個人的には、CDN経由かどうかみたいなところはあまり考えず、ただ単に、その辺りのセオリー通りに従って、そのための設定をおこなったというところだった。

初期準備と初手 13:00-15:00

masayoshiにインフラ設定してもらっている間にレギュレーションとアプリも読む。そうすると以下の辺りが見えてきた。

ベンチを回しても、大体その辺がネックになってるっぽかったので最初はそのあたりから手を付けることに。

icons画像はとりあえず、初期画像はDBから抜き出して、各サーバーに静的ファイルを配置してNginxから配信することに。後から追加で飛んでくる更新画像は各サーバーに配置するのは難しいので、WebDAVで配信するとか、Nginxで別サーバーにプロキシさせるとか、何らかのKVSに突っ込む形になるだろうなーとは思ったけど、それはそこがネックになってから考えようという話をした。

なので「これは一旦 try_files だなー」とか言ってたら、motemenが秒速でスクリプトを作ってくれてNginxに設定入れて、各サーバーにファイルを撒くところまでやってくれた。ここのスクリプトはPerlを使っていた。Perl実装で、kazeburoさんのDBIx::Sunnyが使われていたのでそれを使うことができて便利。

masayoshiがこの段階で、AppArmorによってMySQLが起動しなくなるというトラブルに見舞われていて、皆でそれぞれサーバー設定を触る流れになった。僕も make deploy で一撃でビルドしたアプリケーションを3台全部に撒いて起動できるようにしたりしていた。単に go build したやつを scp で撒いて、アプリケーションを再起動するだけの簡単なスクリプトです。全く、makeは最高だぜ。

この辺の、最初の読みは大体当たってたんだけど、この辺でわちゃわちゃと皆でサーバーを触っていたせいで混乱があった。特に、ここでiconsの画像のETagも揃えていると思ってたんだけど、実はそうなってなかった。これが後々足を引っ張ることになる。

/fetch の未読カウントの改善等 15:00-16:30

motemenがicons画像の対応やっている間に、 /fetch の未読カウントの改善方法を考えてたんだけど、 motemenがあっという間にiconsの対応終わらせてたので、ここも実装方針を話し合った後、motemenにお願いすることにした。方針としてはカウントテーブルを作る形。Redisを使うこともちょっと頭によぎったけどここでは温存することにした。

この実装を入れてベンチを回すと、35,000点くらい。なんか思ったよりスコアが伸びないし、Nginxで返しているはずのiconsでタイムアウトが出まくっている。つまり、帯域があたっている状況で何かがおかしい。

帯域の調整 16:30-18:30

iftopで見ても帯域上限当たってるし、304も全然返せてないし何かがおかしい。アプリケーション側でもgzip入れてみたり、Nginxのcacheやgzipの設定を見直して、2台配信にしたり、3台配信にしたりして試行錯誤しても、5万点どまり。この辺で、 COUNT(*) はコードから撲滅はさせた。

チームも混乱状態で、他のメンバーも帯域ネックなのにワーカー数とかコネクション数とか調整し始めてたので、ちょっとマズイな、と思い「とにかく帯域をなんとかしよう。304の割合を増やさないとどうにもならないぞ」とメンバーを落ち着かせた。

これは何かがおかしいぞ、ということで、icons画像を各サーバーにcurlでアクセスして目視で見比べてみると、果たしてETagがずれていたのであった。

「ちょっと!ETagずれてんじゃん!」ということになり、以下のようにして各サーバーの静的ファイルの更新時刻を揃えた。

find /home/isucon/isubata/webapp/public -type f -exec touch -t 10200000 {} \;

これで、Last-Modifiedも揃うし、Nginxはデフォルトではファイルの更新時刻とファイルサイズを元にETagを生成するのでずれなくなった。(昔のApacheはinodeを元に生成したりしていましたね)

この対策でスコアは12万点まで上昇。しかし折り返し(17時)くらいまでにここまでくるイメージだったので、このタイムロスはかなり痛い。残り2時間ちょい。

この辺は、ちゃんとイメージを揃えていたつもりだったんだけど、リモートでやっていた弊害がでた所であった。

DBネック解消 18:30-19:45

この辺でやっと、帯域ネックだったところからボトルネックが移り、MySQLネックになった。ベンチかけている間にtopを眺めていると、MySQLが一番CPUを食っている状況。ここで打った手は以下。

スロークエリにはプロフィール画像の更新クエリが溜まっている状況だった。更新画像投稿はまだMySQLに行っている状況だったので、ここをなんとかしないといけない。競技当初からそこがそのうちネックになるだろうなと思っていたのでやっとそこまでたどり着いた感があった。

WebDAVとか使うのが正攻法なんだろうけど、使ったことが無いし残り時間も少なかったので、ここは雑にRedisに突っ込むことにした。ここのコードが僕が今回一番バリューを出したところだと思う。Redisはmasayoshiが2台目のサーバーに用意してくれていたのでそれを使うことに。これはナイスプレイであった。

このあたりの改善を入れたら、スコアも18万まで上昇。また、MySQLのCPU利用率も40%程度までに落ち着いた。

最終調整と再起動試験 19:45-20:40

このあたりでまた帯域が当たり始めた。この時点では、01と02の2台でしかリクエストを受けていなかったのだけど、帯域が足りない以上DBが同居している03でもリクエストを受けたほうが良いかもしれないと考え始める。MySQLは40%程度しかCPUを使ってないので、ギリギリNginxとAppの同居もいけるのではないかという判断をして、3台にベンチをかけてみることに。

すると20:27にベストスコアの246,625が出た。もう少し調整できそうだったが、もう時間が無いのでここでスコアを固めに行くことに。設定を見直して、再起動試験をかけていった。最終的な構成は以下。

再起動後のアプリケーションの動作確認後、ベストスコアを狙って、何度かベンチをかけてみるがあまり良い点数が出ない。20:54に221,823が出たのでそれで妥協して打ち止めとした。

感想戦とか

競技が終わるまではメンバーと以下の様な話をしていた。

しかし、出題内容は大体読み切っていたのに、やりたいことができなかったことに相当悔いが残った。当初のイメージではこの辺りまでは上位陣は確実にやってくるだろうな、と予想をしていた。そこから pprof とか回してアプリケーション自体の改善に手を入れてスコアを伸ばさないと、予選通過は厳しいと思っていた。

予選通過 :tada:

なんとか予選通過はできたものの、最終結果を見ると、やはり予想通りで20万点強がボーダーラインであった。危うかった。

とは言え予選通過は嬉しい。これで、ISUCONは7大会連続で本戦会場に行けることになる(出題含む)。多分もうISUCON本戦皆勤なのは、941さんを除くと、fujiwaraさんと僕くらいしかいないのではないか。

本戦はチームメンバーがちゃんと3人顔を合わせて作業ができるのでもっとパフォーマンスが上がるはずである。レギュレーションの読み合わせや認識合わせをしっかりして本戦に臨もうと思う。

運営の皆様へ

予選からフルスペックのISUCON問題を出してくる辺りおみそれいたしました。「本物のISUCON」を予選から参加者に味わってもらえるようにするという気概を感じました。問題内容も素晴らしく、しかも競技環境自体もベンチマーカー含めてストレス無く快適でした。

本戦が楽しみです。