読者です 読者をやめる 読者になる 読者になる

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

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

GoとgRPCでKVS的なものを作ってみた

Go

正月で時間があったので、以前から触ってみたかったgRPCをGo言語から使い、キー・バリュー・ストアのようなものを作ってみた。

KVSといっても、GoのmapへのGet/Put/Delete/ScanをgRPC経由で叩けるようにしただけのもの。それだけだとあまり面白く無いので、gRPCらしく、Watch機能をつけてmapへの更新を監視できるようにした。

github.com

f:id:ono_matope:20160105002700g:plain

個人的には、HTTP/1.1 + JSON APIと比べた時のgRPC(HTTP/2 + ProtoBuf)のメリットや違いが気になっていたので、そのあたりを気をつけながら書いた。

開発の手順

サービス定義

まずはProtocol Buffers 3でKVSのサービスを定義する。サンプルを見ながら適当に書いた。

grpc-kvs/grpc-kvs.proto at master · matope/grpc-kvs · GitHub

syntax = "proto3";

package proto;

service Kvs {
  rpc Get(GetRequest) returns (GetResponse) {}
  rpc Put(PutRequest) returns (PutResponse) {}
  rpc Delete(DeleteRequest) returns (DeleteResponse) {}
  rpc Range(RangeRequest) returns (stream Entry) {}
  rpc Watch(WatchRequest) returns (stream Entry) {}
}

message Entry {
  string key = 1;
  string value = 2;
}

message GetRequest { string key = 1; }
message GetResponse { string value = 1; }

message PutRequest { string key = 1; string value = 2; }
message PutResponse {}

message DeleteRequest { string key = 1; }
message DeleteResponse {}

message RangeRequest { string startKey = 1; int32 maxKeys = 2; }

message WatchRequest { string prefix = 1; }

メソッドの引数と戻り値はmessageで定義した構造体である必要があるらしい。

RangeとWatchのreturnsにstreamとあるが、これはServer-side Streamingの宣言で、レスポンスをEntry構造体のストリームにすることができる。ストリームは非常に長いレスポンスの返却にも使えるし、Server-Sent Eventのようにサーバープッシュ用途にも使える。リクエストもストリーム化することができる(Client-side Streaming / Bidirectional Streaming)。

コード生成

上のkvs.protoから、protocコマンドでkvs.pb.goを生成する。

protoc  --go_out=plugins=grpc:. ./grpc-kvs.proto

ただし、事前にprotocol buffers 3.0 (未リリースなので、google/protobuf · GitHub からダウンロードしてインストールする必要がある)と github.com/golang/protobuf/protoc-gen-go をgo install しておく必要がある。

サービス実装

protocに成功すると、kvs.pb.goに下のようなサーバー用interfaceが定義されるので、これを実装してやれば良い。

grpc-kvs/grpc-kvs.pb.go at master · matope/grpc-kvs · GitHub

// Server API for Kvs service

type KvsServer interface {
    Get(context.Context, *GetRequest) (*GetResponse, error)
    Put(context.Context, *PutRequest) (*PutResponse, error)
    Delete(context.Context, *DeleteRequest) (*DeleteResponse, error)
    Range(*RangeRequest, Kvs_RangeServer) error
    Watch(*WatchRequest, Kvs_WatchServer) error
}

サーバー側実装はこのようになった。grpc-kvs/main.go at master · matope/grpc-kvs

type kvsServer struct {
    elements map[string]string
    mu       sync.RWMutex
    chans    map[chan pb.Entry]struct{}
}

func NewKvsServer() *kvsServer {
    return &kvsServer{
        elements: make(map[string]string),
        chans:    make(map[chan pb.Entry]struct{}),
    }
}

func (s *kvsServer) Get(ctx context.Context, r *pb.GetRequest) (*pb.GetResponse, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    if val, ok := s.elements[r.Key]; ok {
        return &pb.GetResponse{
            Value: val,
        }, nil
    }
    return &pb.GetResponse{}, grpc.Errorf(codes.NotFound, "element not found value=[%s]", r.Key)
}

func (s *kvsServer) Put(ctx context.Context, r *pb.PutRequest) (*pb.PutResponse, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.elements[r.Key] = r.Value

    // Notify updation
    for c := range s.chans {
        c <- pb.Entry{Key: r.Key, Value: r.Value}
    }
    return &pb.PutResponse{}, nil
}

func (s *kvsServer) Delete(ctx context.Context, r *pb.DeleteRequest) (*pb.DeleteResponse, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    delete(s.elements, r.Key)

    // Notify deletion
    for c := range s.chans {
        c <- pb.Entry{Key: r.Key}
    }

    return &pb.DeleteResponse{}, nil
}

func (s *kvsServer) Range(r *pb.RangeRequest, rs pb.Kvs_RangeServer) error {
    s.mu.RLock()
    defer s.mu.RUnlock()

    // sort and filter  keys of elements
    keys := make([]string, 0, len(s.elements))
    for k := range s.elements {
        if k < r.StartKey {
            continue
        }
        keys = append(keys, k)
    }
    sort.Strings(keys)

    for _, k := range keys {
        if err := rs.Send(&pb.Entry{Key: k, Value: s.elements[k]}); err != nil {
            return err
        }
    }
    return nil
}

func (s *kvsServer) Watch(r *pb.WatchRequest, ws pb.Kvs_WatchServer) error {
    ech := make(chan pb.Entry)
    s.mu.Lock()
    s.chans[ech] = struct{}{}
    s.mu.Unlock()
    fmt.Println("Added New Watcher", ech)

    defer func() {
        s.mu.Lock()
        delete(s.chans, ech)
        s.mu.Unlock()
        close(ech)
        fmt.Println("Deleted Watcher", ech)
    }()

    for e := range ech {
        if !strings.HasPrefix(e.Key, r.Prefix) {
            continue
        }
        err := ws.Send(&e)
        if err != nil {
            return err
        }
    }
    return nil
}

通常のRPCは、フレームワーク経由でnet/httpのハンドラを書くの感覚と大して変わらない。

ただしコード生成によりリクエストフィールドが全て静的にアクセスできるので、ボイラープレートコードが不要で良い。

REST APIでいうところの404などのアプリケーションエラーコードは、grpc.Errorfを使い、 https://godoc.org/google.golang.org/grpc/codes に定義されているエラーコードを渡してやるのが流儀らしい。

Sever-side Streaming RPCは、ハンドラに専用のサーバーが渡されるので、そのサーバーにSendしてやることでストリームの要素を送出する(Range, Watch参照)。

サーバー起動部分はこれだけ。

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterKvsServer(s, NewKvsServer())
    s.Serve(lis)
}

クライアント実装

grpc-kvs/main.go at master · matope/grpc-kvs · GitHub

protocにより完全なクライアントコードが生成されているので、クライアントの接続部分はこれだけでよい。

func main() {
    conn, err := grpc.Dial(addr, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewKvsClient(conn)
 

あとはKvsClientに実装されたメソッドを使えば良い。

所感

コンポーネント間通信をREST APIで設計する場合、そこそこ綺麗に書いたとしても、どうしても認証・ハンドラルーティング、リクエスト・レスポンスのエンコーディングなど、ボイラープレートコードは発生してしまう。

また、メソッドを増やすたびにURLやBody, ステータスコードなどREST表現の全体としての整合性を考慮しなければならず、しかも使用しているURLルータによっては変更を余儀なくされたりとつらさを感じていた。そんな理由でRPCに魅力を感じてはいたが、できること・できないことがいまひとつ分からず、乗り換えられずにいた。

gRPCを試してみて、サービスの定義さえしてしまえばサーバ・クライアント実装が自動生成されるため、ボイラープレートコードも発生せず、本質的なロジックに集中して書き進めていけるのは快適だった。コードの質もよくなっていると思う。そのうえで、HTTPハンドラに出来てgRPCに出来ないことはあまりなさそうということも分かった。例えば大きなファイルのやり取りはチャンク化してStreaming APIで流せば大丈夫そうとか。

RPCの使用にはコネクティビティの心配もあるが、gRPCは主要な多くのプログラミング言語へのコード生成に対応しているので、実際に問題なることはなさそう。むしろC++あたりだと普通のREST APIよりクライアント/サーバーの実装が楽になって結果的にRESTより相互接続性が上がりそう。

性能

似たようなHTTP/1.1 + JSON版実装を作り、id:lestrrat さんのベンチマーク実装 http://lestrrat.ldblog.jp/archives/43568967.html を参考に、Getのパフォーマンスを比較してみた。

go バージョン 並列数 gRPC HTTP/1.1+JSON
go1.5.2 100 16161.13 jobs/sec 37361.61 jobs/sec
go1.5.2 1 6705.33 jobs/sec 10151.33 jobs/sec
gotip 100 21528.42 jobs/sec 37931.31 jobs/sec

(サーバ・クライアント同居での雑な計測です)

遅い…。便利であるとはいえ、スループットが生REST時の半分以下になるのはちょっと厳しい。gotipでビルドしたところかなりパフォーマンスが改善しているので、今後のgoの実装の改善を期待しつつ、パフォーマンスが重要でないところから導入したほうがいいかもしれない。

参考

2015年を振り返って

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

なんかみんな振り返っているので、少し出遅れましたが2015年を振り返りたいと思います。

  • アジェンダ
    • 英会話教室に通い始めた
    • Goにコントリビュートをした
    • 仕事で作っているプロダクトがリリースできなかった

英会話教室に通い始めた

2014年はGo言語に全てをつぎ込んだ年だったので、2015年は英語を頑張ろうと思っていました。5月のTOEIC試験の結果が700点ラインを上回ったので、会社の教育補助を受けて英会話教室のGabaに通い始めました。

Gabaではほぼ毎週、40分のマンツーマンのレッスンを受けています。レッスンは雑談とテキストを主体に進みますが、雑談で話題を無茶振りされたりはしないので、無理なく進められます。趣味の話で盛り上がったりもあり、アニメ好きの女性講師とシン・ゴジラの話題で盛り上がるのはこう…いいよね…みたいなのもあります。

英会話教室は高価でなかなか踏ん切りがつかないので、こういう機会を持ててよかったです。補助で行けるのは10回までなので、その後は自費で継続しています。

実際にどれくらい英語力が向上しているのかは分かりませんが、ミーティングで突然外国人エンジニアへの説明を振られた時も慌てずに英語で対応できたし、「ああこれくらい喋れれば海外でも生活できるかもなー」みたいな肌感を得られたのは大きな収穫です。相変わらず映画の英語は全く聞きとれないけど対面なら案外大丈夫

Goにコントリビュートをした

技術面で2015年一番よかったのは、Goの標準ライブラリにコントリビュートをしたことです。

net/http: Client support for Expect: 100-continue · Issue #3665 · golang/go

仕事でGoのHTTPクライアントを使っていて、net/http.ClientがExpect: continue ヘッダに対応していないことに気づきました。この仕様はPUT/POSTリクエストのリトライ処理を実装するは重要なものですが、リクエストとレスポンスの例外的なやりとりが含まれ、実装は面倒なものでした。そのせいか、2012年にissueがopenされて以降、実装に至る動きが見られませんでした。

そこで、HTTPのRFCを調べ、net/httpの実装を読み込み、この機能を実装してパッチを送りました。ちょうどGo1.5リリースのためのコードフリーズ期間中だったのでやや期間はあきましたが、Bradfitzに(!)丁寧にコードをレビューしてもらい、無事本体にマージしてもらえました。レビューをしてもらえた時は「Bradfitzが俺のコードを見てくれた!What a lovely day!!」とウォーボーイズみたいな気持ちになっていました。

仕事で使っていて大好きな言語にコードを取り込んでもらえて、しかも自分にとってヒーローのようなプログラマにコードを見てもらえるというのは最高に嬉しい経験でした。

仕事

2015年は、以前から準備していた(Goを使った)プロダクトの開発が本格化し、メンバーも増えて自分の役割も変化していった一年でした。

相変わらずマネジメントはしませんが、技術的には責任を持っているので、どうやってGo経験の浅いメンバーを短時間で訓練して仕事を任せられるようにするか、というのが大きなテーマでした。

序盤では鬼コーチよろしく徹底的にコードをレビューして、主にGoのイディオム面からの指摘や代替実装を教えるという方針を取りました。この方法は大量の駄目出しが必要になりメンバーへの心理的な負荷が大きいのですが、終盤はレビューもそれほど必要なくなったので、結果的には良かったのかなと思います(どのみち性格上変なコードにツッコミを入れずには済ませられないし)。

そのプロジェクトは本当は昨年末リリース予定だったのですが諸事情により2016年1月に持ち越しになってしまったのが心残りです。なので2016年の抱負はまずリリースと、成果を使って色々と展開していきたいです。

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

RFC的に、HTTPヘッダってどんな値を使えるんでしたっけ?のメモ

Web APIを開発していると、HTTPのヘッダについてRFCにおける規約を確認しなきゃいけない場面がたまにあるので、今回調べたことをまとめた。

HTTP/1.1のRFC

HTTP/1.1のRFCといえば、長らくRFC2616であったが、2014年にRFC7230〜7239が発行され、2616は廃止された。

両者の変更点については、RFC 723xの付録に記述されているので参照のこと。Content-MD5が廃止されたり、ちょいちょい面白い。文章としても723xの方が分かりやすくなっているので、一度目を通しておくことをお勧めする。

RFC的に、ヘッダの名前と値って何が使えるんでしたっけ?

RFC 7230のABNFによると

     header-field   = field-name ":" OWS field-value OWS

     field-name     = token
     field-value    = *( field-content / obs-fold )
     field-content  = field-vchar [ 1*( SP / HTAB / field-vchar )
                      field-vchar ]
     field-vchar    = VCHAR / obs-text

     obs-fold       = OWS CRLF 1*( SP / HTAB )
                    ; obsolete line folding
                    ; see Section 3.2.4

かつ、

  • token
    • "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
    • (VCHARからセパレータを抜いたもの)
  • VCHAR
    • %x21-7E (任意のUS-ASCII 任意の印字可能なUS-ASCII)
  • obs-text
    • %x80-FF (US-ASCII以外のレンジ)
    • ただし、obs-text(及びobs-fold)は廃止された文法である

なので、

箇所 利用可能文字種
ヘッダ名(field-name) token (アルファベット、数字、一部の記号)
ヘッダ値(firld-value) US-ASCII文字 印字可能なUS-ASCII

となる。念のためUS-ASCII 印字可能なUS-ASCII(%x21-7E)は以下の表の21-7Eだ。 (man asciiより)

     00 nul   01 soh   02 stx   03 etx   04 eot   05 enq   06 ack   07 bel
     08 bs    09 ht    0a nl    0b vt    0c np    0d cr    0e so    0f si
     10 dle   11 dc1   12 dc2   13 dc3   14 dc4   15 nak   16 syn   17 etb
     18 can   19 em    1a sub   1b esc   1c fs    1d gs    1e rs    1f us
     20 sp    21  !    22  "    23  #    24  $    25  %    26  &    27  '
     28  (    29  )    2a  *    2b  +    2c  ,    2d  -    2e  .    2f  /
     30  0    31  1    32  2    33  3    34  4    35  5    36  6    37  7
     38  8    39  9    3a  :    3b  ;    3c  <    3d  =    3e  >    3f  ?
     40  @    41  A    42  B    43  C    44  D    45  E    46  F    47  G
     48  H    49  I    4a  J    4b  K    4c  L    4d  M    4e  N    4f  O
     50  P    51  Q    52  R    53  S    54  T    55  U    56  V    57  W
     58  X    59  Y    5a  Z    5b  [    5c  \    5d  ]    5e  ^    5f  _
     60  `    61  a    62  b    63  c    64  d    65  e    66  f    67  g
     68  h    69  i    6a  j    6b  k    6c  l    6d  m    6e  n    6f  o
     70  p    71  q    72  r    73  s    74  t    75  u    76  v    77  w
     78  x    79  y    7a  z    7b  {    7c  |    7d  }    7e  ~    7f del

ヘッダに日本語を突っ込みたいんですけどどうにかならないですか?

RFC2616時代の見解はStudying HTTPさんが詳しい。

RFC2616では、field-valueはTEXT(制御文字以外のOCTET)が使えるが、TEXTでUS-ASCII外の文字を使っていいのは、RFC2047形式でエンコードされた時のみである。2047形式というのはこういうやつ。 要するに生のUTF-8なんかはヘッダ値に載せてはいけないのだ。

でもそれだと Content-Disposition でのファイル名指定とかでこまるよね、ということでRFC5987でUTF-8エンコードするための拡張が定義されている。だけどこれは、それこそContent-Dispositionヘッダなどのみで使える、=つなぎのパラメータに対して適用するものなので、いつでも使えるわけではない。

Rails - Content-Disposition の日本語問題 - Qiita より

 Content-Disposition: attachment; filename*=UTF-8''foo-%c3%a4-%e2%82%ac.html

一方、RFC7230ではどうなのかというと

新たなヘッダ値の構文は(中略)通例的に,US-ASCII 文字の範囲に拘束される。 より広範囲の文字を必要とするヘッダは、[RFC5987]にて定義されるものなどの,符号化方式を利用できる。 RFC 7231 — HTTP/1.1: Semantics and Content (日本語訳)

他には、

歴史的に,HTTP は、 ISO-8859-1 charset [ISO-8859-1] のテキストによるヘッダ内容を許容し、他の charset のサポートは, [RFC2047] 符号化方式の利用を通してのみ許容してきた。 実施においては、大部分の HTTP ヘッダ値は[ US-ASCII charset [USASCII] のサブセット ]のみを利用している。 新たに定義されるヘッダは、そのヘッダ値を,US-ASCII オクテットに制限するべきである。 受信者は、[ ヘッダ内容 内の他のオクテット( obs-text ) ]を,不透明なデータとして扱うべきである。 RFC 7230 — HTTP/1.1: Message Syntax and Routing (日本語訳)

などとある。RFC2616との違いは、利用可能な符号化方式がRFC2047に限定されず、RFC5987など、他のエンコードが認められていることくらいで、基本的にUS-ASCIIオクテットに制限「すべき」とのことだ。とはいえ既存の符号化手法は利用箇所が限られているし、どうしても(独自の)ヘッダでマルチバイトを送りたいなら、適当にパーセントエンコーディングなどで送ればいいという感じだろうか。

RFC的に、複数の同名ヘッダってどう扱うのが正解?

こういうやつ。

X-Foo: Bar
X-Foo: Baz

RFC7230 Section 3.2.2 によると、

  • 受信者は、複数の同名ヘッダがある時、ヘッダ値を現れた順にカンマで結合した一つのヘッダとして扱ってよい。
  • 送信者は、カンマ区切りで結合してもよい時以外、複数の同名ヘッダを送信してはならない。
  • ただしSet-Cookieヘッダは同じレスポンスに複数回現れることが多く、カンマ結合すると意味論が変わってしまうので例外である。

RFC的に、ヘッダの区切りの改行ってCRLF(\r\n)でしたよね

そうとも限らない。実際にはCRやLF単体がヘッダ区切り文字として認識されている。

  • RFCはLF単体をヘッダ区切り文字として認めている
  • 多くのブラウザはRF単体を区切り文字として認めている

参考 LWSとHTTPヘッダインジェクション | MBSD Blog

RFC的に、まさかヘッダの値って改行できないですよね?

実は、RFC2616ではLWSという仕様を使うことで、複数行に渡るヘッダ値を表現できた。 以下の例は、「LWS(改行+スペースまたはタブ)」により、2行に渡るX-Fooヘッダ値を表現できる。

X-Foo: Foo
[sp] X-Bar: Bar

だが、〜IE11などのブラウザはLWSをそのように扱わず、2行目を別のヘッダとして扱うため、レスポンス分割攻撃などが可能であった。そのような問題があったため、RFC7230では、LWSの仕様はobs-foldに改められ、(message/httpメディアタイプを除き)非推奨になった。

  • 送信者は、obs-foldに合致するfield-valueを生成してはならない
  • 受信者は、400 Bad Requestを応答しリクエストを却下するか、obs-foldを一個以上のSP(空白文字)で置換しなくてはならない
  • UAは、obs-foldを一個以上のSP(空白文字)で置換しなくてはならない

結論:できなくなりました

ちなみにGoのnet/httpはLWSにどう対応しているか?

HTTPリクエストの送信、HTTPレスポンスの送信において、ヘッダのfield-valueに含まれる改行文字(CR, LF)が各々スペースに置換されている。(http.Header.WriteSubset() at net/http/header.go) HTTPリクエストの受信時は?→LWSを処理する実装は見当たらないので単に無視されるようだ。(RFC7230の指示とは違うが) 行頭文字がSPであるヘッダがやってきた時にどうなるか?気になるので後で調べる

RFC的に、クライアントから変なヘッダを受け取ったサーバーはどうすればいいの?

PUTについては記述がある

[ PUT 要請内に受信された,認識できないヘッダ ]は、無視する(すなわち,リソース状態の一部として保存しない)べきである。 RFC 7231 — HTTP/1.1: Semantics and Content (日本語訳)

2015/8/4 追記

ヘッダ名に利用可能な文字種について修正しました。

Goのflagでダブルダッシュ(--hoge)なフラグを許可する

Go

それヘルパー関数書くだけで出来ますよ!

と言う訳で書いた

double-dash hyphen

この実装が実現するのは

  • "-hoge" と "--hoge"を変数hoge *stringに受け取れる
  • "-str"と"--str"を変数str stringに受け取れる
  • UsageはflagのデフォルトFlagSetのものを使っているので、ダブルダッシュ版のFlagはUsageに出てこない
./flag2 -h
Usage of ./flag2:
  -hoge="default-value": First string option
  -str="default-value": Second string option

GoのHTTPサーバーを80番や443番ポートでListenする方法を調べた

1024以下の番号のポートでサーバーをListenするには、rootで実行する必要がある。それはもちろん嫌なので、GoのWebサーバーを80番ポートでサービスするためにどういう方法があるのか調べた。

root権限で起動してListenしてからSetuidで権限降格

最初はroot権限で起動して、80番ポートをListenしてからsetuidで権限降格するやりかた。 Node.jsでもそんな感じだったし、まあそういう感じだろうと当たりをつけて調べたら、どうもLinuxではうまくいかないらしい。

コメント欄でのid:methaneさんの指摘によると、Linuxのsetuidシステムコールは、実行したスレッドにしか効力が無い。Goは自動的に複数スレッドに分散されるので、権限降格されないまま実行されるGoroutineが出てくる事になり、望ましくない。さらにGo 1.4ではLinuxでのSetuid/Setgidが禁止されるらしい。Oh...

という訳で他の方法を探す。

rootで起動した起動スクリプトからポートのfdをもらってExec

Go で 1024 以下のポートを Listen するアプリを作る - methaneのブログ

id:methaneさんの実装。外部スクリプトで80番をListenして、そのポートのfdをコマンドラインオプションで与えてGoのサーバーをexecする。サーバの方は、ポートで起動する場合はgoprogram -port 8080、fd受け渡しの場合はgoprogram -fd 4みたいな感じで起動できるように実装を変更する必要がある。外部スクリプトの方はCircusのようなツールを利用することもできるようだ

トリッキーな起動オプションの実装が追加されるのは若干気持ち悪い気がするが、それさえ気にしなければ実装コストも少ないしパフォーマンスペナルティも多分ない。ソケット管理ツールを書くなり、既存のツールを学習するコストはそこそこ面倒くさそうで気になる。

iptablesでポートフォワーディングする

この方法であればサーバー側実装には全く手を入れる必要は無い。起動用のツールをrootで立ち上げる必要が無いのでクリーンな感じもする。ひとつのサーバーを動かすためにシステム設定に手を入れるのは気が引けるとか、運用コストが上がりそうとか思うが、Chefでiptablesの書き換えすればOKみたいな環境であれば手軽なのでは。

Deploying Go servers with Docker - The Go Blogとかを読んでも、Dockerを使うならDockerにポートフォワーディングを任せればいいみたいな感じがするので、環境側でなんとかする方向はありっぽい。

ローカルにHTTP層のReverse Proxyを使うという手もある。この場合プロキシを挟むのでパフォーマンスペナルティや、プロキシに使うサーバとの相性などが気になる。

実際、Nginxを挟む事によるパフォーマンスペナルティはちょっと無視できない。

あるいは今気づいたけど、もともとReverse Proxy用のサーバを別に立てるつもりなら、そもそも気にしなくてよい問題ではある。

GoバイナリにウェルノウンポートをListenするケーパビリティを設定する

ケーパビリティ で権限を少しだけ与える - いますぐ実践! Linuxシステム管理 / Vol.183

ケーパビリティとは、ファイルまたはスレッドに対して、root権限の一部を付与したり剥奪したりできるもの。実行ファイルに対してsetcapコマンドで任意の権限を与えられる。GoWebの人たちはこの方法を使っているらしい。この場合は以下のようになる。

setcap 'cap_net_bind_service=+ep' /path/to/program

Why not use Goweb? · Issue #49 · stretchr/goweb · GitHub

簡単にcapabilityを試してみた。通常のlsでは/root配下が覗けないが、読み込み時パーミッションチェックをスキップするCAP_DAC_READ_SEARCHケーパビリティをlsのコピーに付与したところ、sudoもなしに一般ユーザーで/root配下が参照できる。

[vagrant@localhost ~]$ ls /root
ls: ディレクトリ /root を開くことが出来ません: 許可がありません
[vagrant@localhost ~]$ cp /usr/bin/ls ./ls.copy
[vagrant@localhost ~]$ getcap ./ls.copy
[vagrant@localhost ~]$ sudo setcap 'CAP_DAC_READ_SEARCH=+ep' ./ls.copy
[vagrant@localhost ~]$ getcap ./ls.copy
./ls.copy = cap_dac_read_search+ep
[vagrant@localhost ~]$ ./ls.copy /root
anaconda-ks.cfg
[vagrant@localhost ~]$ 

ただ、このcapability、手元で検証した限りではcpなどで複製すると権限は失われてしまうようなので、デプロイ先の環境でsudo setcapしてやる必要があるようだ。

まとめ

今回候補に挙げた手法は以下の通り。

  • Listen後にsetuidで権限降格する
    • Linuxでは問題がある
  • 起動スクリプト(カスタムメイド or Circus)からfdをもらう
    • ソケット管理ソフトウェアを用意する必要がある
    • サーバー側にもfdによるListenに対応するコードを追加する必要がある
  • iptablesなど、OSやコンテナ側でポートフォワーディングする
    • システム側に手を加える必要がある
  • ローカルでProxyを立てる
    • パフォーマンスペナルティがある
  • ケーパビリティを設定する
    • デプロイ先ホストにバイナリを配置後、sudo setcapする必要がある。

この中では、ポートフォワーディングかケーパビリティが筋が良さそうな気がしている。せっかくシングルバイナリデプロイができるGoなのだし、できればバイナリで完結する手法でなんとかしたいが、そういう方法は今は無いようだ。

Golangで関数をグローバル変数に代入してテスト時にスタブする

Go

こんにちは、小野マトペです。タイトル全部です。

昨日、現在時刻によって条件分岐するロジックを含むGoプログラムを書いていて、どうテストするか困ったのですが、うまい(と思う)やり方を思いついたのでここに書いておきます。

書いていたのはまあだいたいこういうソースコードです。

// main.go
package main

import (
        "fmt"
        "time"
)

func main() {
        fmt.Println(Greet("マトペ"))
}

func Greet(n string) string {
        t := time.Now()
        if 6 <= t.Hour() && t.Hour() <= 18 {
                return fmt.Sprintf("こんにちは%sさん。今は%d時ですよ!", n, t.Hour())
        } else {
                return fmt.Sprintf("こんばんは%sさん。今は%d時ですよ!", n, t.Hour())
        }
}
go run main.go
こんにちはマトペさん。今は12時ですよ!

で、このGreet関数をテストしたいんですが、このままでは夜の挨拶を出すのに18時まで待つ必要があります。それでは困るのでtime.Nowの部分をなんとかちょろまかしていつでも全ての条件をテスト出来るようしないといけません。いわゆるスタブ化ですね。

すぐに思い浮かぶのは、Greet関数を下のようにDIっぽく依存性である現在時刻を引数から受け取るように変更する方法だと思います。

// main.go
func Greet(n string, t time.Time) string { // 引数に現在時刻を追加
        if 6 <= t.Hour() && t.Hour() <= 18 {
                return fmt.Sprintf("こんにちは%sさん。今は%d時ですよ!", n, t.Hour())
        } else {
                return fmt.Sprintf("こんばんは%sさん。今は%d時ですよ!", n, t.Hour())
        }
}

これなら任意の時間の場合のGreet関数のユニットテストが書けますが、現在時刻のテストのために本来不要な引数が増えるのは大げさで好ましくありません(好みの問題です)。それに、結局コールスタックのどこかでtime.Now()を呼ぶ必要があり、その呼び出しポイントを含むテストはスタブ化できません。

ここはやはりtime.Now関数をテスト実行時だけ動的に上書きしたいところですが、Golangはあまりスタブ化に優しい言語ではなく、例えばtimeパッケージをテスト時に動的に他のパッケージに差し替えるとかはできません。これが自作パッケージで提供している構造体かなにかであれば、interfaceでもなんでも使ってスタバブルにすればいいと思いますが…。

(参考:go - Is there an easy way to stub out time.Now() globally in golang during test? - Stack Overflow

で、どうしたかというと、タイトル通りですがmain.goでtime.Nowの関数ポインタをグローバル変数に代入しておき、テスト時にmain_test.goで任意の時刻を返す関数にさしかえるという方法を使いました。

// main.go
package main

import (
        "fmt"
        "time"
)

var timeNowFunc = time.Now // 関数をグローバル変数に代入

func main() {
        fmt.Println(Greet("マトペ"))
}

func Greet(n string) string {
        t := timeNowFunc() // グローバル変数でtime.Now関数を呼び出し
        if 6 <= t.Hour() && t.Hour() <= 18 {
                return fmt.Sprintf("こんにちは%sさん。今は%d時ですよ!", n, t.Hour())
        } else {
                return fmt.Sprintf("こんばんは%sさん。今は%d時ですよ!", n, t.Hour())
        }
}

Goは関数が一級市民なのでシグネチャさえ合致すれば変数に代入できます。この場合、timeNowFunc変数は型推論func() time.Time型として宣言され、time.Now関数で初期化されます。で、これを次のようにテストします。

// main_test.go
package main

import (
        "testing"
        "time"
)

const timeformat = "2006-01-02 15:04:06" // timeのフォーマット指定文字列

// timeNowFuncグローバル変数の関数を入れ替える関数
func setNow(t time.Time) {
        timeNowFunc = func() time.Time { return t }
}

func TestMain(t *testing.T) {

        evening, _ := time.Parse(timeformat, "2014-08-14 14:10:00")
        night, _ := time.Parse(timeformat, "2014-08-14 22:30:00")

        // 昼のテスト
        setNow(evening)  // スタブ!timeNowFuncに昼時刻を返す関数をセット
        if ret := Greet("まとぺ"); ret != "こんにちはまとぺさん。今は14時ですよ!" {
                t.Errorf("Greet Fails. Got [%s]\n", ret)
        }

        // 夜のテスト
        setNow(night)  // スタブ!timeNowFuncに昼時刻を返す関数をセット
        if ret := Greet("まとぺ"); ret != "こんばんはまとぺさん。今は22時ですよ!" {
                t.Errorf("Greet Fails. Got [%s]\n", ret)
        }
}
$ go test
PASS
ok      _/Users/matope/dev/go/mock     0.006s

テスト内で宣言しているsetNow(time.Time)関数は、グローバル変数のtimeNowFuncに、任意の時刻を返す関数を代入する関数です。これでテストごとにtimeNowFuncの返す時刻をコントロールできるので、任意の時刻のテストが書けます。しかも面倒なスタブ用クラス実装や不要な引数の引き回しなしで、テスト時にのみ作用するスタブです。

グローバル変数と聞いてつらくなる人もいるかもしれませんが、Goのグローバル変数は実際にはパッケージスコープで、頭文字を小文字にしておけばパッケージの外からアクセスできないので、比較的クリーンです。Goはテストがコードと同じパッケージ空間で実行できるので、テストコードに対してのみExportされたパラメータとして扱えます。

この、テスト時にスタブしたい関数の呼び出しをグローバル変数でプロキシしておいてtest側の初期化で差し替える方法は、同一パッケージ内であればモック化のための追加の記述量が少なく、データベースのテストなど、いろいろ応用が利きそうだなと思いました。複数パッケージにまたがるtime.Now()を一括で差し替えたいような場合は、スタブプロキシ用のパッケージを作ってそこのExported(大文字から始まる)グローバル変数にセットしてやればよさそうですね。

ちなみに豆ですが、Grouped Globalを使えばこんな感じでちょっとかっこよくグローバル変数を呼べます。もはや紛らわしいような気もしますが。

// main.go
package main

import (
        "fmt"
        "time"
)

var _time = struct {
        Now   func() time.Time
        Since func(time.Time) time.Duration
}{
        time.Now,
        time.Since,
}

func main() {
        fmt.Println(_time.Now()) // ← ちょっとかっこいい
}

追記:同じやり方を解説してる記事 があったのでこっち読んだ方がよさそう