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

あらゆる日付文字列をよしなに扱う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」を予選から参加者に味わってもらえるようにするという気概を感じました。問題内容も素晴らしく、しかも競技環境自体もベンチマーカー含めてストレス無く快適でした。

本戦が楽しみです。

Goツールのビルドをおこない、GitHub Releasesにアップロードする

今回で最終回。Goのツールをビルドして成果物を生成し、それをGitHub Releasesにアップロードする。

ビルドする

CGOを使っていない限り、最近のGoは簡単にマルチプラットフォーム対応の実行バイナリをそれぞれビルドすることができる。

僕の場合、OSはlinuxとdarwin(Mac)用は必ず作っている。windows用も余力があれば作るようにはしている。アーキテクチャーはamd64(64bit)決め打ちにしてしまうことも多い。

また、成果物を配布する場合は、実行バイナリの他にLICENSEやREADMEなどを含め、zipやtar玉等に固めるのが定番。

これらのビルドとアーカイブに固める作業のために特別なツールは必要なく、シェルスクリプトでもいけるレベルだが、僕は goxc を使っている。

https://github.com/laher/goxc/

goxc は結構ゴツくてGitHub Releasesとかbintrayに上げる機能とかも持ってたりするんだけど、それの、成果物を生成する部分だけを利用しているという具合。

成果物のネーミングルールや、成果物に含めるファイルのルールを設定できるところが便利ではある。ただ、更新が滞っていることもあるし、そういう成果物生成だけに絞ったもう少しシンプルなツールが欲しいと思っている。

goxcの設定

goxc.goxc.json をリポジトリにおいておけば、 % goxc と打つだけで設定通りにビルドしてくれる。ただ、それを丁寧に書くのを以前はやってたんだけど、それらは大体コマンドライン引数で指定できるし、goxc のためにファイル増やすのも馬鹿らしいので、最近は、Makefileにcrossbuildっていうターゲットを作ってをそこにベタっと書くようにしている。

ghchのMakefile、より抜粋すると以下のような具合。

crossbuild: devel-deps
    goxc -pv=v$(shell gobump show -r) -build-ldflags=$(BUILD_LDFLAGS) \
      -d=./dist -arch=amd64 -os=linux,darwin,windows \
      -tasks=clean-destination,xc,archive,rmbin

これで、 make crossbuild を実行すれば、 dist/ 配下に成果物を配置してくれる。

$ tree dist/
dist/
└── v0.1.2
    ├── ghch_v0.1.2_darwin_amd64.zip
    ├── ghch_v0.1.2_linux_amd64.tar.gz
    └── ghch_v0.1.2_windows_amd64.zip

make crossbuild とすれば、手元でもCI環境であっても同様に成果物が作られるのがポイント。

CGOを使っている場合

CGOを使っている場合は、そのプラットフォーム用のCI/CD環境を用意するなどしてビルドする必要がある。Windowsの場合は、AppVeyorが定番です。Macの場合は、TravisのOS X環境を使うなどすれば良さそうですが、あまり知見はない。

mackerel-agentの場合、AppVeyorを利用してビルドの他にインストーラーの作成やインストーラーへの署名などもおこなっています。

それらの環境でビルドした成果物をアップロードする手順を作るのは少し複雑にはなってはしまう。

成果物をGitHub Releasesにアップロードする

成果物のアップロードには、 ghr を使っている。これはGitHub Releasesにアップロードすることに特化したシンプルで良いツールです。

参考: 高速に自作パッケージをGithubにリリースするghrというツールをつくった

環境変数 $GITHUB_TOKEN を設定した状態で、

% ghr v0.1.2 dist/v0.1.2

などとすれば、第1引数に指定したバージョンタグのGitHub Releasesに対して、第2引数に指定したディレクトリ以下の成果物を一括アップロードしてくれる。

クロスコンパイルして、アップロードするところまでは実際には以下のようなシェルスクリプトを用いている。

#!/bin/sh
set -e
make crossbuild
ver=v$(gobump show -r)
ghr $ver dist/$ver

これで、Goで書いたツールを、GitHub Releaseにアップロードできるようになりました。

手元ではなくTravis上でビルドとアップロードをおこなう

これまでは手元でビルドしてアップロードする方法を書きましたが、git tagが打たれたのを契機に、CI/CD環境でビルドやデリバリーをおこなうのも定番スタイルです。CI/CD環境であればクリーンな環境でビルドを作れるのもメリットです。反面複雑さが増す部分もあります。

とは言え、上に挙げたシェルスクリプトを単にTravisなどの環境で実行させればよいだけの話ではあります。その際 $GITHUB_TOKEN 環境変数をセットする必要はあります。

また、Travisで実施する場合、ビルドは make crossbuild でおこなうが、GitHub ReleasesへのアップロードはTravisの機能を使う手もあります。ghchではそのようにしています。

その場合、

% travis setup releases

とすれば、 .travis.yml を書き換えてくれるのでそれを調整する手順になります。調整した、 .travis.yml を抜粋すると以下のようになります。

before_deploy:
- make crossbuild
deploy:
  provider: releases
  skip_cleanup: true
  api_key:
    secure: ...
  file_glob: true
  file: "dist/**/*.{tar.gz,zip}"
  on:
    tags: true
    branch: master
    condition: "$TRAVIS_TAG =~ ^v[0-9].*$"

まあ、それなりに便利ではあるんですが、若干煩雑になる感はある。また、 travis setup の度に、GitHubの個人Tokenがぽこぽこ作られる仕組みになっていて、ツールを提供しているリポジトリ毎にtokenが作られてしまうのは気持ち悪いので、あまりやらないようにしている。

以上です。

Goツールのリリースエンジニアリング

前回: Goツールのリリースにおけるバージョニングについて

前回挙げた以下のリリース5段階の中で、バージョニングだけで1エントリになりましたが、今回は、2,3について。

  1. versionをbumpする
  2. CHANGELOGを更新する
  3. 1,2での変更をgitに反映してタグを打つ
  4. ビルドする
  5. ビルドをアップロードする

具体的には、リリースに纏わるファイル更新をgitに反映さえてタグを打つところまで。ビルドする直前までとも言えます。

CHANGELOG.mdを自動更新する

CHANGELOGは ghch で自動生成させている。規定の CHANGELOG.md をリポジトリに配置して、

% ghch -w -N $next_tag

とすれば、魔法のように CHANGELOG.md を更新してくれる。生成された CHANGELOG.md はこんな感じ。

https://github.com/Songmu/ghg/blob/master/CHANGELOG.md

CHANGELOG、あったほうが良いと思ってる派だけど、丁寧に書くのもメンドイし、とは言えコミットログをそのままCHANGELOGとするのも乱暴なので、 ghch を使ってpull requestの粒度でCHANGELOGとするのはまあ悪くないと思っている。

ghch についてはこちら。→ Gitのtagとpull requestのマージ履歴からChangelogを自動生成する ghch

gitの更新とそのためのリリーススクリプト

これで、 version.goCHANGELOG.md が更新されているので、これらはもちろんgit repositoryに反映させる必要がある。そしてタグを打つ。これでリリース一段落である。

前回のエントリで書いたバージョンの更新作業から、git repository反映までを一撃でやるシェルスクリプトが以下。

#!/bin/sh
set -e

echo current version: $(gobump show -r)
read -p "input next version: " next_version

gobump set $next_version -w
ghch -w -N v$next_version

git commit -am "Checking in changes prior to tagging of version v$next_version"
git tag v$next_version
git push && git push --tags

これを、 _tools/releng とかに配置 している。Goのプロジェクトでこういうツール類を置くディレクトリは、個人的にはこの _tools/ のようにアンダースコアで始まるディレクトリに配置して、Goのソースコードが入ってないことをわかりやすくしているけど、そこまでやっている人あまり見ないので、やらなくても良いのかもしれない。 scripts/ とかに配置しているのもまま見ます。

ちなみに、たまに聞かれますが、 relengRelease engineering の略らしいです。一昔前にPerlハッカーの人たちが、CPANモジュールを上げるときに「relengする」ってよく言ってたのに影響されてます。

また、 Makefilerelease みたいなターゲットを定義しておくと、 make release とかやれば、リリースが走ってくれるので便利。

release:
    _tools/releng

残りはビルド

今回で git tag するところまで終了しました。後は、打たれたタグに対して成果物のビルドをおこない、適切なアップロードをすることを残すのみである。待て、次回。

Goツールのリリースにおけるバージョニングについて

Goのツールをリリースする時、個人的には以下のような手順を踏んでいる。もちろんスクリプトで一撃でできるようにはしている。今回は1.の話。セマンティックバージョニングの話は出てきません。

  1. versionをbumpする
  2. CHANGELOGを更新する
  3. 1,2での変更をgitに反映してタグを打つ
  4. ビルドする
  5. ビルドをアップロードする

versionは -ldflags を使って動的に埋め込む方法があるが、最近は明示的にソースコードに書いた方が良いと思うようになってそうしている。

理由としては、ユーザーが go get/build で実行ファイルを取得した場合でもバージョンは表示されて欲しいというのが一つ。 -ldflags で実行ファイルに色々な値を埋めることはできますが、基本原則として、それらを埋めてない状態でもちゃんと実行ファイルが正常に動くようにすることを意識した方が良い。

もう一つの理由として、バージョン埋め込み手法として git describe --abbrev=0 --tags で最新のタグを取得する方法がよく取られるが、これだと「最新のタグ」なので、意図しないタグ(テスト用途で雑に付けたタグとか)で埋められてしまう可能性があるのも困る。

で、最近は実際にどうやっているかというと、 version.go というファイルを用意して、そこにバージョン関連の情報が記載されるようにしている。ghg だと以下のような具合。

package ghg

const version = "0.1.1"

var revision = "Devel"

version 定数の更新は、 gobump をリリーススクリプトの中で使っている。例えば、 0.2.0 に更新したい場合は以下のようなコマンドを実行すれば良い。そうすれば自動的に version.go ファイルが書き換えられる。

% gobump set 0.2.0 -w

gobump に関しては、作者であるmotemenの gobump で Go プロジェクトのバージョニングをおこなう を参照のこと。

revision 変数はビルド時点のgitのコミットハッシュを埋めるためのものだが、これをきちんと埋めたい場合は -ldflags で埋める形になる。具体的には以下のようになる。

% go build -ldflags="-X github.com/Songmu/ghg.revision=$(git rev-parse --short HEAD)" ./cmd/ghg

もちろん、これは make build で実行できるように、 Makefile に定義してある。

これで、最新の ghg のバージョンを表示させてみると、以下のようになる。

% ghg version
ghg version: 0.1.1 (rev: 63eb454)

go get で実行ファイルを取得したり、 -ldflags を指定せずに go build した場合は以下のように、"Devel" が表示される。程よい挙動といえるのではないでしょうか。

% ghg version
ghg version: 0.1.1 (rev: Devel)

versionをどのように表示させるのが良いのか

コマンドラインツール全般の話として、バージョンをどのように表示させるのが良いのか最近少し悩んでいる。

-v とか --version でバージョン番号出すのあんま良くないんじゃないかと思うことがある。特に -v--verbose と紛らわしいので絶対ダメ。

コマンドラインオプションはあくまで標準の振る舞いを調整するためのものであって、バージョン表示のように全然違う振る舞いをするものはオプションではなくて、サブコマンドでやるべきなのではないか。

確かに、 -h/--help 同様に慣習ではある。GNU Coding Standardのコマンドラインの項目にも以下のように書かれている。

All programs should support two standard options: ‘--version’ and ‘--help’.

ただ、最近のツールだと --version オプションが用意されているツールも減ってきているようにも感じる。

また、バージョン表記がサブコマンドであれば、オプションで挙動の調整もやりやすい。例えば、 --format オプションを与えれば、表示方法を切り替えられる、など。バージョン文字列は、人が読みやすい形、機械が読みやすい形、両方で提供できると嬉しいので、このような挙動は嬉しいはず。

% ore-tool version --format=json
{"version":"0.0.1"}

ただ、本当に単機能しか無いコマンドラインツールに、わざわざ version サブコマンドだけを追加するとなるとやり過ぎ感もありますね。

ちなみに、最近個人で作るツールだと --version オプションも version サブコマンドどちらも用意せず、 --help で表示される内容の中に、バージョン情報を記載するに留めることもあります。

結論として、 version をサブコマンドで用意するのがやはり良いのではないかという気持ちなのが最近です。 dep コマンドとかもそうなってますね。

今回はバージョンの話でしたが、Goツールのリリースに関するその他徒然について気が向けば続きを書きます。

GitHub Releases用インストーラーghgのhomebrew tapを作った

Goで作ったツールは、GitHub Releasesに上げるのがよく行われますが、それらを統一的にインストールする方法が無いのが困りものでした。

そこで開発されたのが、ghgですが、ではそのghgを最初にどうやってインストールするのか、という問題がありました。

ということで、Macのみではありますが、homebrew用のtapを作りました。これで以下のようにghgがインストール可能になりました。

% brew install Songmu/tap/ghg

tapリポジトリは以下のように適当に作った。なんかhomwbrew公式だと、最近はbottleっていうフォーマットがあるみたいなんだけど、それをどう作るかよくわからなかったので、アーカイブURLを指定して配置するような感じでお茶を濁している。

https://github.com/Songmu/homebrew-tap

Goのテスト内で静的ファイル配信サーバーを起動する

実質一行。異常に簡単だった。

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestDownload(t *testing.T) {
    ts := httptest.NewServer(http.FileServer(http.Dir("testdata")))
    defer ts.Close()

    resp, err := http.Get(ts.URL + "/test.txt")
    ...
}

ちなみに、テスト用のファイルを testdata という名前のディレクトリに置くのは、公式のドキュメントにも書かれている慣習ですね。

https://golang.org/cmd/go/

The go tool will ignore a directory named "testdata", making it available to hold ancillary data needed by the tests.

書籍「Mackerel サーバ監視[実践]入門」を執筆しました

Mackerel サーバ監視[実践]入門

やっと出せました。この本は、僕がはてなに入社してからずっと携わっている、Mackerelというサーバー管理・監視サービスに関する本です。Software Design誌の2015年3月号から2016年11月号まで掲載していた「Mackerelではじめるサーバ管理」という連載の内容をベースに再構成し、加筆、修正をおこなったものです。

連載時は、主に id:stanaka, id:y_uuki と僕の3人で執筆をローテーションし、書籍化にあたっては、id:sugiyama88, id:daiksy, id:a-know と僕の4人で、加筆、修正作業をおこないました。この6人が著者陣となります。連載と書籍化両方に携わっているのは僕だけで、中心的に携わらせてもらいました。

この本を通して、Mackerelの使い方を学ぶとともに、サーバー管理・監視についても学べる内容となっております。Mackerelには無料で使えるTrial/Freeプランがあるので、個人でもお試しできる内容となっています。是非お手にとってご購入いただけると幸いです。

僕にはエンジニアとして「世の中に使われる技術を作りたい」という継続的な目標があります。プログラミング言語開発とかがわかり易い例ではありますが、実際それができる人は一握りです。今回、SaaSであるとは言え、自分が開発に携わったサービスが「書籍化できるようなソフトウェア」になったことは感慨深く思っています。

今回、連載記事から書籍への再構成が一番大変な作業だったように思います。ここではかなり、技術評論社の中田瑛人様に助けていただきました。編集のプロの技を見ました。

去年は「みんなのGo言語」、今年はこの本を共著で書いたので、来年は単著がなんか出せると嬉しいなぁとかおぼろげに思っております。

サブコマンドはUNIX哲学と相反していないのか

「UNIXという考え方」に書かれているUNIX哲学に「各プログラムが一つのことを上手くやる」というのがある。それとサブコマンドは矛盾するんじゃないかと感じていた。一つのプログラムが複数のことを実行できるじゃん、という。

最近は以下のように思うようにあった。

汎用的なコマンドラインツールはグローバルな名前になるので、名前の衝突には気をつける必要がある。今や多くの開発者がコマンドラインツールを書くようになった。

また、コンテキストを同じくした複雑なツール群を提供する場合、名前空間的なものはあったほうが良いのは確かでしょう。例えば git のサブコマンドが全部バラバラのコマンド名だったら発狂してしまう。

最近、僕はGoでツールを書く事が多いが、サブコマンドを採用せずに各々のコマンドでビルドバイナリを作ってしまうと、スクリプト言語と違い、容量も大きくなってしまうという現実問題もある。

なので、サブコマンド自体は許容できるが、サブコマンドを持つツールは、サブコマンド同士の実装が疎になるように設計しないといけない、と思うようになったのが最近の心境です。

UNIXという考え方