ISUCON6で準優勝でした

ISUCON 6 にチーム「morimoto組」で参加して、最終スコア 36,067 で準優勝しました。

morimoto組は自分と、会社の新卒1,2年目( kasari , id:moshisora ) という歳の差チームです。

お題

  • 匿名お絵かきサービス
    • ログイン、セッション管理などはない
  • SSE (Server Sent Events) で他のユーザの書き込みがストリーミングで流れてくる
  • 一番前に React のサーバサイドレンダリングをする node のプロセスがいる
  • react, 各言語実装のアプリケーションサーバ, MySQL はすべて Docker で起動している

やー、盛りだくさんでしたね…

スコア推移とやったこと

f:id:sfujiwara:20161024114926p:plain

11:02:53   PASS  942
11:21:28   PASS  1159
11:58:31   PASS  1364
12:31:15   FAIL  253
12:36:34   PASS  1616
12:39:18   PASS  2051  nginxをいれて /api 以下を直接 Perl に (fujiwara)
12:43:40   PASS  3175  並列度が高いのでPerlを200プロセスに (fujiwara)
12:46:41   PASS  3543
12:49:26   FAIL  12
12:53:18   PASS  2816
13:03:36   PASS  3314
13:35:14   FAIL  251
13:47:46   FAIL  251
14:07:14   PASS  3211  pointsをstrokesのカラムにJSONで保持 (kasari, moshisora)
14:09:41   FAIL  0  壊れたSVGを返してベンチマーカーがクラッシュしたらしい
14:10:56   FAIL  0
14:11:25   FAIL  0
14:14:25   PASS  3470
14:15:21   FAIL  0
14:16:38   FAIL  0
14:20:25   FAIL  0
14:24:03   PASS  4697  /img/* で返すSVGをPerlでレンダリング (fujiwara)
14:27:05   FAIL  252
14:29:53   PASS  4781
15:17:25   FAIL  106
15:21:32   PASS  7412  /img/* をRedisにCache(Perlで処理) (kasari)
15:31:52   PASS  6040
16:09:04   FAIL  253
16:10:24   FAIL  253
16:12:32   PASS  8020
16:54:56   FAIL  253
16:58:52   FAIL  253
17:10:31   PASS  9339  /api/stream/rooms SSEをGo + Redis PubSub実装に置き換え (fujiwara)
17:41:33   PASS  15241  React, Perl, Goをisu02で動かして2台構成
17:43:36   PASS  30714  React, Perl, Goを全台で動かす5台構成
17:45:12   PASS  29301
17:48:35   PASS  36067  React, Perl, Goを isu02〜05、isu01にnginx, MySQL, Redisの5台構成

全体の方針

全体の方針としては、以下のようなざっくりとしたスケジュールを組みました。

  • まず 1台でチューニングを進める
  • 複数台構成に1時間、再起動確認などに30分を用意する
  • つまりコードフリーズは 16:30 がデッドライン

チームメイト的には Perl のほうが書きやすい、ということでまず Perl 実装を選択。Perlの初期実装は各言語の中でもだいぶスコアが出にくかったようで、ベンチを回して1000前後。

  • MySQLはDockerから剥がす
  • nginx をいれてログを取りつつ /api は react ではなく直接Perl
  • 並列数がだいぶ高いので Perl を200プロセス起動する (無理矢理感ある)

なかなか立ち上がりが早くできず、スコアをあまり上げられずにお昼へ。

アプリケーションの改善

1 Strokeに対して複数保存されるPointsが1点毎に1レコードになっていたので、これはそのままJSONシリアライズしてStorkeのカラムに保存してしまう(kasari)。既存データもマイグレーションするスクリプトで処理(moshisora)。この時点ではスコアはほとんど変わらないが、その後の改善をしたら効いてくるはず。

/img/SVGを返している部分は、初期実装が

という作りになっていたので、ここを直接 PerlSVG を吐き出すように修正(fujiwara)。

my $svg = qq{<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" baseProfile="full" width="${width}" height="${height}" style="width:${width}px;height:${height}px;background-color:white;" viewBox="0 0 ${width} ${height}">};
for my $s (@{ $room->{strokes} }) {
    my ($r, $g, $b, $a) = ($s->{red}, $s->{green}, $s->{blue}, $s->{alpha});
    my ($id, $w) = ($s->{id}, $s->{width});
    $svg .= qq{<polyline id="$id" stroke="rgba($r,$g,$b,$a)" stroke-width="$w" stroke-linecap="round" stroke-linejoin="round" fill="none" points="};
    $svg .= join(" ", map { $_->{x} . "," . $_->{y} } @{$s->{points}});
    $svg .= qq{"></polyline>};
}
$svg .= "</svg>";

ひどいコードだが、動く (4697)。そのあと SVG を Redis に cache するようにして 7412。ここは nginx (OpenResty) から直接 Redis を参照するようにすればもっと伸びたはず、だけど手が回らず未着手。

SSEを処理している部分は初期実装が 500ms sleep してDBに新しいStrokeがあるかどうかをポーリングをくりかえす、という作りで、これは Perl の Prefork server (Starlet) ではプロセスが専有されてしまうので並列度が上がらず不利。

PerlとGoのハイブリッド構成へ

既に15:30なので、手を打つとしたらここが最後の判断ポイント。Goを混ぜることを決断。

  • /api/stream/rooms の SSE だけ Go 実装にする
    • nginxで振り分け
  • 500msのポーリングをやめて、Redis PubSub を使う
    • Perlで保存した新しいStrokeをPublish
    • GoはSubscribeして降ってくるJSONをそのままイベントとして流す

実装としては、以下のようなコード。

  • Redisはgoroutineを起動してそこでsubscribe、イベントが来たらchannelに流す
  • contextを使って3秒で全体をタイムアウトさせる
    • これは出題者にあとで聞いたところ、もっと長くても問題なかったそうです
  • 特にイベントが来なくても 500ms ごとに watcher_count は送信する
    • 最後の case <-time.After(500 * time.Millisecond): のところ
// func getAPIStreamRoomsID(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// ...
    ch := make(chan *redis.Message, 10)
    nctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
    go func(ctx context.Context) {
        redisClient := redis.NewClient(&redis.Options{
            Addr: os.Getenv("REDIS_HOST") + ":6379",
            DB:   0, // use default DB
        })
        defer redisClient.Close()
        log.Println("starting psubscribe")
        pubsub, err := redisClient.PSubscribe(fmt.Sprintf("%d", room.ID))
        defer pubsub.Close()
        if err != nil {
            log.Println("psubscrie err", err)
            return
        }
        for {
            msg, err := pubsub.ReceiveMessage()
            if err != nil {
                log.Println("recv err", err)
                return
            }
            select {
            case <-ctx.Done():
                return
            default:
                ch <- msg
            }
        }
    }(nctx)
    for {
        select {
        case <-nctx.Done():
            return
        case msg := <-ch:
            var s Stroke
            err := json.Unmarshal([]byte(msg.Payload), &s)
            if err != nil {
                log.Println(err)
                return
            }
            d, _ := json.Marshal(s)
            printAndFlush(w, "id:"+strconv.FormatInt(s.ID, 10)+"\n\n"+"event:stroke\n"+"data:"+string(d)+"\n\n")
        case <-time.After(500 * time.Millisecond):
        }
// ...

たいした変更でもないし1時間あればいけるだろう、と思って一人で書いたものの(fujiwara) 、やはり焦りからかなかなかベンチを通過できず。

17時をまわってしまったので、これで通らなければ Perl のまま複数台構成にするしかないか…というところで最後、kasari に「イベントがなくても 500ms ごとに watcherを送る必要があるのでは?」という指摘をもらってなんとか 17:10 にベンチを通すことに成功!(9339)。

複数台構成へ

ここまでチューニングしてきたホストには netdata をいれていたので、ベンチを回しつつまだまだアプリケーション (React, Perl, Go) がCPUを使っていることは確認済み。

そこで、Dockerで動いていて分散が容易な React, Perl, Go を 2〜5台目のホストで動作させ、1台目は nginx, Redis, MySQL という構成へ。1台目のnetwork転送量がちょっと心配だったけど、そこを詰めている時間的余裕がなかった…

5台構成にしたところ、MySQL が too many connections のエラーを吐いて接続できない現象を確認。

  • my.cnf では max-connections = 10000 になっている
  • 実際に値を見ると 214

...??? と一瞬パニクりかけたが、あこれは systemd から起動して limit が掛かってるな、という見当が付いたので /etc/systemd/system/mysql.service.d/limits.conf を適当に設定して再起動で解決。

[Service]
LimitNOFILE=1000000
LimitNPROC=1000000

1台ずつ再起動テストをして、02〜05 は 01 にあった docker-compose でのOS起動時のアプリケーション起動設定がない、というのも発見したので /etc/systemd/system/multi-user.target.wants/isu.service /etc/systemd/system/isu.service を01からコピーして反映。

結果

最終的には 17:48:35 に記録した 36,067 が提出スコアとなり、運営の再起動テストも通過。2位で準優勝しました。

他チームにスコアが見られる17時までは1万も行かない状態だったので、最後の1時間でだいぶ跳ねた感じでしたが、ここまで書いたとおり決して余裕があって潜行してたわけではありません…

予選も本選も、素晴らしい問題とイベントを提供していただいた出題者の皆様、運営の皆様に感謝します。ありがとうございました!

API Gateway + Lambdaでcatch allした処理をApex + Goでnet/httpで扱える ridge を書いた

タイトル長い。 LambdaでGoが正式サポートされるのを首を長くして待ちつつ、Apex で Go を実行しています。

先日、API Gatewayで受けたすべてのリクエストをLambdaに丸投げすることができるようになりました。

これまではAPI Gatewayでいちいちマッピング定義を作るのが面倒で、いまいち普通のWebAPI的なものをLambdaで作る気がしなかったわけですが、これで行けるのでは…? と思って、catch all されたリクエストとレスポンスを net/http.Request と net/http.ResponseWriter で扱えるようにする ridge というライブラリを書いてみました。

github.com

Readmeそのままですが、以下のような Go のコードが API Gateway (プロキシ統合)+ Lambda + Apex で動きます。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/apex/go-apex"
    "github.com/fujiwara/ridge"
)

var mux = http.NewServeMux()

func init() {
    mux.HandleFunc("/", handleRoot)
    mux.HandleFunc("/hello", handleHello)
}

func main() {
    if os.Getenv("APEX") == "" {
        // ローカルで動かしたい場合はこっち
        log.Println("starting up with local httpd")
        log.Fatal(http.ListenAndServe(":8080", mux))
    }
    // Lambdaで動く場合はこっち
    apex.HandleFunc(func(event json.RawMessage, ctx *apex.Context) (interface{}, error) {
        r, err := ridge.NewRequest(event)
        if err != nil {
            log.Println(err)
            return nil, err
        }
        w := ridge.NewResponseWriter()
        mux.ServeHTTP(w, r)
        return w.Response(), nil
    })
}

func handleHello(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    fmt.Fprintf(w, "Hello %s\n", r.FormValue("name"))
}

func handleRoot(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    fmt.Fprintln(w, "Hello World")
    fmt.Fprintln(w, r.URL)
}
  • 入力の JSON*net/http.Request に変換してアプリケーションに渡す
  • ridge.ResponseWriternet/http.ResponseWriter interfaceを持っているのでそれに対してレスポンスを書き込む

とすると、普通に Lambda で Go の webapp らしきものが動きます。

ユーザの書くコードは普通の Go の webapp と同様 func(http.ResponseWriter, *http.Request) で行けるので、ローカルでは http を直接 listen するサーバとして動かしつつ、API Gateway + Lambda でも同じコードを動かせる……これはもしかして凄く便利なのでは!

制限とか

  • URL引数に同名の値 (foo=1&foo=2 みたいなの) があると、入力Eventの時点でひとつになってしまう
  • HTTPリクエスト/レスポンスヘッダで複数同じものがあっても同様
  • バイナリの入出力ができるのかどうか良く分からない

という現時点の制約があります。ちゃんとバイナリが扱えれば、画像変換サーバみたいなものにも使えて夢が広がる感じなのですが…

ともあれ現時点でも、普通にHTMLやJSONを返すAPIを実装するのには使えるはずですので、是非お試しください。

ISUCON 6 予選通過しました

ISUCON 6 にチーム「morimoto組」で参加して、予選を通過して決勝進出することになりました。

ISUCONは過去5回のうち優勝3回、3位1回、出題1回、ということでもう引退(勝ち逃げ)しようかな…とも思ったのですが、今年は出題にも関わっていないので参加しないと完全に縁が切れてしまうし、それも寂しい。ということで。

チームメンバーは直前まで決まらなかったのですが、結局会社の新卒1,2年目( id:amusan , id:moshisora ) と組むことにしました。若いとはいえ去年と今年の社内ISUCON優勝メンバーです。(歳の差何歳だろう)

当日やったこと

天気は悪いが見晴らしはいい会議室を確保して万全の体制 (まぶしいのですぐブラインドは下ろされました)。

言語は Perl です。

  • インスタンス起動、事前に用意していたChefを実行して環境整備
  • isutar を isuda 内に取り込み
  • 正規表現の生成を Regexp::Trie に変更
    • 各プロセスで生成すると重いのと整合性を確保しづらいので、POST /(initialize|keyword) を処理するアプリケーションを1プロセスだけ起動するようにして分離。生成された正規表現を Redis に保存し、htmlify を処理するプロセスはその正規表現を使う
    • 1プロセスのみにしたので、keywordがpostされてきた順番に $trie->add($keyword) していくだけで整合性が維持できる
  • 静的ファイルは nginx で static_gzip、expires max を付与する
    • 304が返せるようになる

nginx.confはこんなかんじ。

 http {
    upstream app {
       server unix:/var/tmp/app.sock;
    }
    upstream app_post {
       server unix:/var/tmp/app_post.sock;
    }
    include /etc/nginx/mime.types;
    server {
        location ~ ^/(css|img|js|favicon\.ico) {
           gzip_static on;
           expires max;
           access_log off;
           root /home/isucon/webapp/public;
        }
        location /initialize {
            proxy_pass http://app_post;
        }
        location /keyword {
            set $app "app";
            if ($request_method = "POST") {
              set $app "app_post";
            }
            proxy_pass http://$app;
        }
        location / {
            proxy_pass http://app;
        }
  }
}

ここまでやった状態で14:40すぎに8万点ほどでトップに躍り出る。

ここで、アプリケーションのプロセスを再起動せずにベンチを回していくと、どんどんスコアが上がっていくという現象を確認。2回目だと12万点ぐらい。 特にメモリ内に cache は作っていない (つもりだった) ので、何が原因なのかさっぱり分からず。

ここから、他のチームがスコアを上げてきたのになかなか上げられず苦しむ時間帯。

  • / でoffsetが大きいクエリをentry using(id) の自己結合に
  • /stars を Resis にキャッシュ (get_multiで一気に取る)
  • uri_forを消す
  • user_nameもセッションに入れてクエリを消す
  • isupam は何とかできないかと考えたが結局手を入れられず

これでだいたい9万ぐらいだったので、このままだと予選通過はボーダー付近だった模様。

17:30ごろから2度目のベンチでスコアが上がる現象を解明するべく、個別にプロセスを再起動して切り分けしたところ、POSTを担当するプロセスを再起動しなければ速度が出る、ということを確認。こいつは Regexp::Trie のオブジェクトを永続化して持っているので、二回目は既に初回のベンチでキーワードが追加済みの状態になっていた (ので速かった)。

つまりこの状態では本来リンクになってはいないはずのキーワードがリンクになる状態。アプリケーションとしてはまずいんだけども、ベンチマーカーがそれを検出しても結果がPASSとされていればスコアも有効である、ということを運営とのやりとりで確認していたので、(多少良心が痛むものの) 突っ込むことに。

1回ベンチが走って追加された状態のキーワードをテキストファイルに書き出しておいて、initialize でそれを Regexp::Trie にaddしておくというコードを追加して、終了1分前にベンチqueue投入。18時の終了3秒前ぐらいにベンチが始まり、その状態で dstat をみていた限りでは14万点だしたときとおなじぐらい捌けてそう、というところで(最終的にスコアは知れずに) 競技終了。

結果、143,366で通過できました。

予選を終えて

チームメイト二人ともなぜか Perl-5.24 を手元で動かせずに、手元でコードの確認ができない状態だったのが完全に想定外 (環境構築で30分ぐらいハマってたので「もうそれは諦めて、こっちで動かすので」という判断を早めにできたのがよかった…)。それ以外は、結構ちゃんと戦えたのかな、という感想です。

聞くところによると、htmlfy()の結果cacheは結構雑でも (リンク関係を検出はされるものの) スコアが出せたようなので、ここがもっと厳密だったら展開も変わってきたのかなあ、という印象はあります。(とはいえ、一発アウトにするのは出題側も誤検知が怖いんですよね…)

幸いにも、これで ISUCON 本選は6年連続で出場できるので、精一杯戦いたいと思います。運営の皆様、楽しいイベントを本当にありがとうございます!

「みんなのGo言語」の執筆に参加しました

技術評論社から発行される「みんなのGo言語」という書籍の執筆に参加しました。本日、9月9日発売です!

gihyo.jp

みんなのGo言語【現場で使える実践テクニック】

みんなのGo言語【現場で使える実践テクニック】

Goと自分

Goは2009年に一番最初にリリースされたときにちょっと触っていて、このblogにもいくつかエントリを書いていました。 golang カテゴリーの記事一覧 - 酒日記 はてな支店

その後、実際にプロダクションで使うことがなかったのでしばらく離れていたのですが、 2013年頃からGoが盛り上がってるなという雰囲気もあり、

で、ここ数年はもっぱら Go をメインで書くようになっていました。公開している OSS としては

をはじめ、いろいろあります。Perlをメインで書いていたときも CPANに上げていたもの があるのですが、ほとんどがライブラリで、いわゆるアプリケーションは少数でした。

Goではライブラリよりもアプリケーションを多く公開するようになったのが面白いなと、今振り返って思います。これはGoが、デプロイのしやすいシングルバイナリで動作するアプリケーションを書くのに向いている、ということ無関係ではないでしょう。

執筆について

Go製の OSS をいくつか公開していたことからか suzuken (id:suzu_v) さん、id:mattn さん経由で声を掛けていただいて、共著で執筆することになりました。雑誌記事は WEB+DB PRESS に書いたことがあるのですが、書籍は初めての経験です。

自分がどういうことについて書けるかを考えた結果 (言語自体には特に詳しくもないですし)、Goでこれまで書いて、本番で運用してきた実用アプリケーションを作り上げることについてのTips的なことならば書けそう。ということで、本書では第3章の「実用的なアプリケーションを作るために」という内容で執筆させていただきました。

内容は本当にTips的で、最初に一通り書いたところではあまりに断片的すぎた感があったのですが、編集のかたや共著者のレビューでお力添えをいただいて、なんとかまとめることができました。

本書について

本書は、最初からGo自体の入門書ではなく応用のための実用書、という位置づけで書かれていて、どの章も現場でGoをつかって仕事をする人に役に立つ内容になっていると思います。A Tour of Go や プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES) で言語自体を学んだ上で、実際になにかまとまったものを書き始める時に、よいお供になると思います。144ページと気軽に読める分量で、お値段も1980円(本体)と比較的安価ですので、是非お手にとっていただければと思います。

最後に

connpass.com

リリースパーティーを開いていただけるそうで、自分もビールを美味しく飲みにお邪魔する予定です。まだ定員に余裕があるようなので、こちらも是非よろしくお願いします。

WEB+DB PRESS vol.94 特集「実践スケーラブルAWS」を執筆しました

機会をいただいて、技術評論社 WEB+DB PRESS vol.94 の特集1「実践スケーラブルAWS」を同僚の id:tkuchiki と執筆しました。本日8/24発売です。

WEB+DB PRESS Vol.94

WEB+DB PRESS Vol.94

どなたかが編集のかたに自分を推薦していただいたようで、インフラの特集で、というお話をいただいて、ここ数年はほぼAWSをメインで使っているのでAWS前提の内容でよければ…ということで寄稿することになりました。

[鍵は監視にあり!]実践スケーラブルAWS

  • 第1章:AWSにおけるスケーリングの基本戦略
    • 成長段階に合わせた適切なインフラ構成……藤原 俊一郎
  • 第2章:規模が拡大しても破綻しない監視
    • 監視対象を動的に増減,値を集約して適切に通知……藤原 俊一郎
  • 第3章:Webサーバ/アプリケーションサーバのスケール
    • ELBでロードバランシング,SQSで非同期処理……朽木 拓
  • 第4章:キャッシュサーバのスケール
    • ElastiCacheとtwemproxyを組み合わせ負荷を分散……朽木 拓
  • 第5章:データベースサーバのスケール
    • RDSでリードレプリカを利用,フェイルオーバーへの対応……朽木 拓
  • 第6章:オートスケールを無駄なく活用
    • 自律的なプロビジョニング,余裕を持った発動ポリシー……藤原 俊一郎

1,2,6章を自分が、3,4,5章を id:tkuchiki で分担しています。

内容は、1台からはじめた小規模なサービスを AWS で100台規模までスケールさせて行くにはどうすればいいのか、という想定で、それに必要な要素や構築の際に注意すべき事をまとめてあります。

最近はクラウドで「スケーラブル」というとコンテナやサーバーレスやら、というかっこいい話になりそうなものですがそうではなく(そもそもそこまで仕事で突っ込んでないので)、普通にEC2上にアプリケーションサーバを立てるような構成でどう徐々に拡張していけばいいのか、という、どちらかというと泥臭い話です。

全体を貫く軸としては「監視」をメインにしていて、どのような項目をモニタリングして判断すればいいのか、という指針で各章に記述があるのと、監視だけで独立して章を設けてZabbixとMackerel、ログ集約(分量は少ないですが…)には Fluentd と Norikra などを取り上げています。

また、AWSらしいところということでオートスケールについても章を設けました。これは先日 YAP(achimon)C::Asia Hachioji 2016 mid in Shinagawa で発表した内容がベースになっていたりします。

他の特集も Kotlin、Electron と今話題の技術が取り上げられていて大変面白いと思いますので、是非お買い求めください!

Fluent::Logger(Perl)をFluentd 0.14のSub-second timeに対応した

Fluentd 0.14 がリリースされましたね。

Fluentd v0.14.0 has been released | Fluentd

新機能が盛りだくさんですが、そのうちの一つ Sub-second time (秒未満の解像度のtimestamp) に Fluent::Logger 0.18で対応しました。

<source>
  type forward
</source>

<match **>
  @type file
  path  ./test
  time_format %Y-%m-%dT%H:%M:%S.%N
</match>

このような fluentd.conf で fluentd 0.14 で起動して、以下のように event_time オプションを真にした Fluent::Logger から送信すると、

#!/usr/bin/env perl
use 5.12.0;
use Fluent::Logger;
use Time::HiRes;
my $logger = Fluent::Logger->new( event_time => 1 );
$logger->post(
    test => { foo => "bar" },
);
$logger->post_with_time(
    test => { foo => "baz" },
    1464845619.12345, # floatで指定
);

以下のように秒以下の精度を持った時刻でログを送信できます。

2016-06-02T14:40:52.251250982    test    {"foo":"bar"}
2016-06-02T14:33:39.123450040    test    {"foo":"baz"}

Perl側のinterfaceは浮動小数点数なので、正確にはnanosecond単位の精度はありません。もし精度が必要なら、floatではない値で指定できるような方法を検討するのでお知らせください。時刻指定なしの post() では内部で Time::HiRes::time() を呼び出してその時刻を送信しています。

ちなみに送信先が fluentd 0.12 の場合に event_time => 1 で送信するとエラーが発生しますのでご注意ください。(クラッシュはしないようですが)

[error]: forward error error=#<MessagePack::MalformedFormatError: invalid byte>
error_class=MessagePack::MalformedFormatError

Enjoy!

#shibuyago #2 で Stretcher の実装について話した

Shibuya.go#2 という勉強会で、自分が開発している Pull型デプロイツール Stretcher について発表してきました。 shibuyago.connpass.com

発表資料はこちらです。 speakerdeck.com

Goの勉強会なので、Stretcherの実装で使われているTips的なコードの紹介を多めにしてみました。

ちなみに資料中で紹介している copyAndCalcHash の実装は、既にPRをいただいて io.MultiWriter によってすっきりきれいになっています。

copyAndCalcHash is simplified by io.MultiWriter by shogo82148 · Pull Request #13 · fujiwara/stretcher · GitHub

会場とピザ、ビールを提供していただいた VOYAGE GROUP 様、主催の @suzu_v 様、ありがとうございました!