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

bauer's diary

凡人の凡人による凡人のための備忘録

GitHubの2段階認証におけるプロトコル最適解を考える

なに?

cloneしたときに選択したプロトコルが、ローカルGit設定の remote url にそのまま設定されるのですが、
そもそもプロトコルは何を選べばいいのか、ベストプラクティスは何かを検討します。

前提

現場では2段階認証が必須となっているので、前提項目とします。
2段階認証にしておけば、パスワードが盗まれても安全なので推奨します。

プロトコルの種類と速さ

まずプロトコルの種類は下記の3つです。

  1. git
  2. https
  3. ssh

そしてcloneの速さは下記の順番です。
git > https >> ssh

これだけ見れば "gitプロトコルでいいのでは?" となるのですが、そうもいかない事情があります。

git プロトコル

gitプロトコルは read-only のため、そのままではpushできません。
また、社内proxyを経由していて gitプロトコルで使う9418ポートがFWなどで制限されている場合は使えないので、その場合は他のプロトコルに変更する必要が出てきます。

尚、gitプロトコルは現在GitHubのヘルプ上では言及されていませんが、利用可能です。

https プロトコル

f:id:kitakitabauer:20170219015533p:plain
httpsプロトコルの場合、通常はpushのたびにユーザ名/PWによる認証が必要になりますが、2段階認証の場合、PWの代わりにGiHubの設定画面から発行するPersonal Access Tokenが必要となります。

Personal Access Tokenの発行手順は公式ヘルプにあるとおりです。
尚、credential helperを導入すればキャッシュしてくれますが、キャッシュが切れたら再度入力する必要があります。

ssh プロトコル

f:id:kitakitabauer:20170219015440p:plain
ssh は鍵認証です。
clone 後のメリットとして、push のときに ssh-agent や pageant が、パスフレーズの入力を代行してくれるので便利ですが、前述したとおり最も遅いプロトコルとなります。


開発上、sshプロトコルが楽ですが、速さも求めたい。
そんなときどうすればよいのでしょうか。

結論

下記手順を踏むことによって、
cloneは最速のgitプロトコルで clone し、
pushではsshプロトコルによってパスフレーズを求められることなくpushできます。


指定したurlに対して他のプロトコルを使うようにgitconfigに定義します。

[url "git@github.com:"]
  pushInsteadOf = git://github.com/
  pushInsteadOf = https://github.com/
[url "git://github.com/"]
  insteadOf = https://github.com/

この設定によって、"https:" や "git:" を使っていても git push のときには ssh 経由になります。

git fetch や git pull の時は "https:" の代わりに "git:" を使用します。
尚、git configでは"pushInsteadOf"の部分が一つずつしか設定できないため、直接ファイルに追記しました。

また、ssh/configに下記設定を追加すれば、sshのcloneが高速化するので、
間違ってsshでcloneしたときのために設定しておくとよいかと思います。

Host github.com
  Compression yes
  Ciphers arcfour128,arcfour,aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,aes192-cbc,aes256-cbc

Ciphersで利用する暗号方式を指定します。左から順に利用を試みます。

検証

それでは設定を検証してみます。

まずgitプロトコルでcloneします。

% git clone git://github.com/kitakitabauer/clone-sample.git
Cloning into 'clone-sample'...
remote: Counting objects: 51, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 51 (delta 0), reused 0 (delta 0), pack-reused 48
Receiving objects: 100% (51/51), 10.82 KiB | 0 bytes/s, done.
Resolving deltas: 100% (6/6), done.

リモートURLを確認すると、"fetch" ではgitプロトコルが、
"push" ではsshプロトコルが設定されていることがわかります。

% git remote -v
origin	git://github.com/kitakitabauer/clone-sample.git (fetch)
origin	git@github.com:kitakitabauer/clone-sample.git (push)

問題なくpull/pushできます。

% git pull -v
Looking up github.com ... done.
Connecting to github.com (port 9418) ... 192.XX.XXX.XXX done.
From git://github.com/kitakitabauer/clone-sample
 = [up to date]      master     -> origin/master
Already up-to-date.

% git push -v
Pushing to git@github.com:kitakitabauer/clone-sample.git
To github.com:kitakitabauer/clone-sample.git
 = [up to date]      master -> master
updating local tracking ref 'refs/remotes/origin/master'
Everything up-to-date

リポジトリ内の .git/config にも反映されています。

[remote "origin"]
	url = https://kitahara-yuki@github.com/kitakitabauer/go-sample.git
	fetch = +refs/heads/*:refs/remotes/origin/*



おしまい

golang製のREPL内でUnixタイムスタンプから日付フォーマットを取得する

忘れがちだけど、よくある動作なので書き留めておきます。

準備

goreをインストールしておきます。
github.com

$ go get -u github.com/motemen/gore

"gore" とは、コード補完もしてくれるREPL(Read-eval-print loop)で、文字通り読んで評価して表示して繰り返すツールのことです。
仕組み的には、裏ではそれまでの入力を順番に実行するソースコードを生成して "go run" しています。

goreをより使いこなしたい場合は下記もインストールしておくとよいです。

# 入力補完
$ go get -u github.com/nsf/gocode
# プリティプリント
$ go get -u github.com/k0kubun/pp
# ドキュメント
$ go get -u golang.org/x/tools/cmd/godoc


実践

$ gore
gore > :import time

# 第一引数に日付フォーマット変換したいUnixタイムスタンプ
gore > t := time.Unix(1488697427, 0)
time.Time{sec:63624294227, nsec:0, loc:(*time.Location)(0x1070a0)}

gore > t.Format(time.RFC3339)
"2017-03-05T16:03:47+09:00"

フォーマットの種類は公式のformat.goのdocを参照してください。
https://golang.org/src/time/format.go

逆に、指定した日付のタイムスタンプを取得する場合

UTCでの、2016年1月1日0時00分の定点がほしい場合

$ gore
gore > :import time

gore > day := time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)
time.Time{sec:63587203200, nsec:0, loc:(*time.Location)(nil)}

gore> day.String()
"2016-01-01 00:00:00 +0000 UTC"

gore> day.Unix()
1451606400

東京での、2016年1月1日0時00分の定点がほしい場合

# ロケーション名とUTCとの時差で、ロケーション構造体を作成
gore> jst := time.FixedZone("Asia/Tokyo", 9 * 60 * 60)
&time.Location{name:"Asia/Tokyo", zone:[]time.zone{time.zone{name:"Asia/Tokyo", offset:32400, isDST:false}}, tx:[]time.zoneTrans{time.zoneTrans{when:-9223372036854775808, index:0x0, isstd:false, isutc:false}}, cacheStart:-9223372036854775808, cacheEnd:9223372036854775807, cacheZone:(*time.zone)(0xc42000a3e0)}

gore> day := time.Date(2016, 1, 1, 0, 0, 0, 0, jst)
time.Time{sec:63587170800, nsec:0, loc:(*time.Location)(0xc42006e120)}

gore> day.String()
"2016-01-01 00:00:00 +0900 Asia/Tokyo"

gore> day.Unix()
1451574000


おしまい

「学力の経済学」は、今の日本の教育を考えさせられる良書だった

この本に出会った経緯

最近、テレビでも話題になっている慶應義塾大学の教育経済学者である、中室牧子さん著書の本を読みました。
投稿時点のAmazonの"経済学・経済事情"カテゴリで、ベストセラー1位になっている人気図書です。

「学力」の経済学

「学力」の経済学

以前、"今でしょ!"の林先生のTV番組に出演されていた方なのですが、
そこでは

  • 勉強をしたか確認している
  • 勉強を横について見ている
  • 勉強をする時間を決めて守らせている
  • 勉強するように言っている

のうち、どの方法が効果が一番あったかという中室さん自身が行った実験について説明していました。

母親・父親で結果が異なるのですが、
母親の場合「勉強をする時間を決めて守らせている」が一番効果的で、
父親の場合「勉強をしたか確認している」が一番効果的との結果が出ていました。

データは個人の経験に勝る

この本では、そういった子育てに携わる親なら非常に気になる話題についてメスを入れており、他にも

  • ご褒美で釣っては「いけない」
  • ほめ育てはしたほうが「よい」
  • ゲームをすると「暴力的になる」

といった世論が正しいかどうか、中室さんは様々なデータ・前例を元に解説していました。
結論と詳細は本に書かれているのですが、一貫して

経済学がデータを用いて明らかにしている教育や子育てに関する発見は、教育評論家や子育て専門家の指南やノウハウよりも、よっぽど価値がある

ことを定説しています。

因果関係と相関関係の違い

また、本書の中で何度も「因果関係」と「相対関係」を見極める事例をいくつも解説しており、非常に納得感が強い内容になっています。

  • 因果関係:Aという原因によってBという結果が生じた
  • 相関関係:AとBが同時に起こっている

例えば、読書をしている子どもは学力が高い、というメディアの報道に対して、
読書をしているから子どもの学力が高い(因果関係)のではなく、
学力の高い子どもが読書をしているのにすぎない(相関関係)可能性について言及していました。
他の要因も考えられる中で因果関係がはっきりしたデータがない限り、報道側の主観にすぎないということです。


子育てにおいて少しでも戸惑いを感じている方にとって、考え直すきっかけを与えてくれる良書になっているので、是非読んでみてください。

Jenkinsの全ジョブに対して条件でフィルタリングするワンライナー

掲題の通りです。
業務で棚卸しするときに必要だったので手順をまとめておきます。

コマンド中の "/var/lib/jenkins/jobs" は、
それぞれの環境に合わせてJenkinsのジョブ直下を指定してください。

disabled一覧

不要なジョブを整理するためにdisabledのフラグと共に出力しました。
そのままExcelスプレッドシートに貼り付けるために、ジョブ名とdisabledの間はタブで区切っています。

find /var/lib/jenkins/jobs/*/config.xml -print0 | xargs -0 grep -o "\<disabled\>.*" | sed -e 's/\/var\/lib\/jenkins\/jobs\///g' -e 's/\/config.xml:\<disabled>/\t/g' -e 's/<\/disabled>//g'

findに "-print0" オプション、xargsに "-0" オプションをつけることで、
ジョブ名にスペースが含まれていると別々なものとして認識してしまう問題を回避するため、
区切り文字をスペースから"\0"に変更しています。

※ disabled=trueだけ出力したい場合はこちら。

find /var/lib/jenkins/jobs/*/config.xml -print0 | xargs -0 grep "\<disabled>true" | sed -e 's/\/var\/lib\/jenkins\/jobs\///g' -e 's/\/config.xml:.*\<disabled>/\t/g'

displayName一覧

ジョブの表示名称も一緒に出力したいときに使いました。

find /var/lib/jenkins/jobs/*/config.xml -print0 | xargs -0 grep -o \<displayName\>.* | sed -e 's/\/var\/lib\/jenkins\/jobs\///g' -e 's/\/config.xml:<displayName>/\t/g' -e 's/<\/displayName>//g'

Linuxでpbcopy的な動きを実現する

ここからは少し余談になりますが、上記のような結果をクリップボードに入れたくなるのが常で、
Mac OS ではクリップボードにコピーするためにpbcopyができたのですが、Linuxではどのようにやるのでしょうか。

$ cat hoge.txt | pbcopy

Ubuntuの場合、"xsel" というものを使ってこの動きを模倣できます。
標準インストールされてないので、インストールします。

sudo apt-get install xsel

下記の通り実行すれば、クリップボードにcatの結果が保存されます。

cat hoge.txt | xsel --clipboard --input

毎回オプション付きで実行するのは辛いので、
.bashrcにエイリアスを定義しておきます。

alias pbcopy='xsel --clipboard --input'


以上です。素敵なJenkinsライフを!

本家の最新を取り込みながら安全に自分のリポジトリで修正したいとき

はじめに

本家リポジトリをforkして、それを上流に設定し、最新を取り込みながらfork先のリポジトリに修正したいときに今回の手順を踏みます。
本家に直接pushができないときに、こういった形を取ることがあります。
この流れを"Fork & Pull モデル"と呼びます。

本家リポジトリをfork

取り込みたいリポジトリを、自身のアカウントなりでforkします。
GitHubからでもいいですし、hubコマンドがある方は"hub fork"でもいいです。

hubコマンドとは、CLIGitHubの操作ができるツールです。
github.com

リモート一覧にupstream追加

取り込みたいリポジトリ"upstream"としてリモート一覧に追加します。

% git remote add upstream git@github.com:[organization or user]/[repository].git

間違って本家からリポジトリを取ってきてしまったときは、originのリモートURLを変更すればよいです。

% git remote set-url origin git@github.com:kitakitabauer/[repository].git

リモート一覧を確認すると、"upstream" が追加されていることがわかります。

% git remote -v
origin	git@github.com:kitakitabauer/[repository].git (fetch)
origin	git@github.com:kitakitabauer/[repository].git (push)
upstream	git@github.com:[organization or user]/[repository].git (fetch)
upstream	git@github.com:[organization or user]/[repository].git (push)
上流の更新を取り込む

上流の更新を取得して反映させる場合は下記の通りです。

% git fetch upstream
% git merge upstream/master

または一度にpullしてしまいます。

% git pull upstream master
ブランチ作成

ローカルリポジトリにブランチを作成してcheckoutします。

% git checkout -b [開発ブランチ名]
Switched to a new branch '開発ブランチ名'

新しいブランチに切り替わっていることを確認します。

% git branch
* [開発ブランチ名]
  master
originにpush

originに対して、ローカルで作成したブランチ名を指定して、空ブランチをpushします。

% git push origin [開発ブランチ名]
Total 0 (delta 0), reused 0 (delta 0)
To git@github.com:kitakitabauer/[開発repository].git
 * [new branch]      開発ブランチ名 -> 開発ブランチ名
差分確認

開発用ブランチとmasterブランチとの差分を表示するGitHubページを開きます。

% hub compare
CI の結果を確認する場合
% hub ci-status
上流のmasterブランチへPR作成
  • iでissueに紐付けて作成できます。
hub pull-request [branch name] -i [issue id]


おしまい。

Goのことはじめ その3:IntelliJ IDEAでGoを書くために

はじめに

これまでNode.jsやPythonではVimを使ってきましたが、WebStormなど優秀なIDEが登場してきたこともあり、Goをお仕事で書くタイミングでそろそろIDEにも慣れておきたく、IntelliJ IDEAを使うことにしました。

IntelliJ IDEA(インテリジェイ アイディア)とは

チェコに本社を置くJetBrains社が開発した、Java言語など多言語対応の統合開発環境です。
EclipseNetBeans の競合に当たりますが、Javaに関していえば、周りのエンジニアはこちらを使っている人が多いです。

Community(無償版) or Ultimate(有償版)

Golangはデフォルトではサポートされていませんが、IntelliJのための下記プラグインを使うことでGo言語に対応させることができます。
github.com

Goを書くだけなら事足りるので、迷わず無償版を選択します。もちろんライセンスを購入できるなら有償版でもかまわないかと思います。
詳しい違いはこちらにあります。

インストール

下記からダウンロードして展開してもいいですが、今回はbrew-caskでインストールします。
www.jetbrains.com

もしbrew caskがない場合は下記コマンドで準備します。

% brew tap phinze/homebrew-cask
% brew install brew-cask
% brew tap caskroom/versions

brew caskの準備が整ったら"intellij"で検索します。

% brew cask search intellij
==> Partial matches
caskroom/cask/intellij-idea  intellij-idea-ce             intellij-idea-ce-eap         intellij-idea-eap            intellij-idea-next-ce-eap    intellij-idea-next-eap       caskroom/cask/intellij-idea

"intellij-idea-ce"がCommunity Edition(無償版)なので、こちらの詳細を確認し、インストールします。ほかは有償版やRC版です。

% brew cask info intellij-idea-ce
intellij-idea-ce: 2016.2.4
https://www.jetbrains.com/idea/
/usr/local/Caskroom/intellij-idea-ce/2016.2.4 (68B)
From: https://github.com/caskroom/homebrew-versions/blob/master/Casks/intellij-idea-ce.rb
==> Names
IntelliJ IDEA Community Edition
IntelliJ IDEA CE
==> Artifacts
IntelliJ IDEA CE.app (app)

% brew cask install intellij-idea-ce


IntelliJ Goプラグインダウンロード・適用

IntelliJ IDEAを起動して、Configure→Preferencesをクリックします。
f:id:kitakitabauer:20160922235021p:plain
Plugins→"Search in repositories"リンクをクリックします。
f:id:kitakitabauer:20160922235034p:plain
検索窓に"Go"を入力して、先程のプラグインをインストールします。
f:id:kitakitabauer:20160922235053p:plain
その後intelliJの再起動を促されるので、再起動します。

GitHubのプロジェクトをインポート

ここでは、GitHubのgoプロジェクトをインポートする手順をまとめます。

再び起動したIntelliJにて、configure→Version Control→GitHubから、アカウントを入力し、テスト認証してみます。
f:id:kitakitabauer:20160923002833p:plain

問題なければ"OK"でトップに戻った後、"Check out from Version Control"→GitHubをクリックします。
f:id:kitakitabauer:20160923000003p:plain

インポートしたいGitHubリポジトリURLを入力します。
"Parent Directory"は、ローカルのGOPATHが通っているパスを指定します。
f:id:kitakitabauer:20160924013208p:plain
Clone後、再びトップに戻って、"Create New Project"を選択します。
その後、"Create project from existing sources"をクリックします。
Project Name/Project locationはそのままとします。

Go SDKの設定

Project SDKを設定します。"Configure"をクリックします。
f:id:kitakitabauer:20160923000348p:plain
brewなどでインストールしたgoのGOROOTパスを入力します。
f:id:kitakitabauer:20160923000401p:plain
表示されたGo SDKで問題なければ"Next"をクリックします。
f:id:kitakitabauer:20160923000411p:plain
その後"Finish"をクリックすれば、先程cloneしたGoのプロジェクトが表示されます!
f:id:kitakitabauer:20160923000438p:plain


おしまい。

ISUCON6で屈辱的に負けてきた

はじめに

去る2016年9月18日(日)に、ISUCON6の予選に参加しました。
結果は最高スコア12023で屈辱的に敗退しました・・・
その一部始終をまとめておきたいと思います。

ISUCONとは?

お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトル、それがISUCON

isucon.net

予選当日までにやってきたこと

主に下記のことを行ってきました。

1. チームメンバーとISUCON4(2014年開催)の過去問で復習をしてきました。
kitakitabauer.hatenablog.com
kitakitabauer.hatenablog.com
kitakitabauer.hatenablog.com
kitakitabauer.hatenablog.com
2. 今回予選で利用するサーバはMicrosoft Azureなので、アカウントを用意した後、リーダーがリソースグループ作成と、サブスクリプションへのメンバーID登録をしてくれました。
3. bitbucketに作成された共有リポジトリに、作業端末の公開鍵を入れたauthorized_keysなどをコミットしていました。作成されたインスタンスにこれらを突っ込む予定です。
4. ローカルにNode, Goの最新バージョンをインストールしました。言語選定の理由は後述します。
5. アンチョコとして、my.cnfとnginx.confを用意しておきました。
6. 予選会場は自社の会議室を間借りさせてもらうことになったので、その手配と人数分のモニターの確保をしてもらいました。

チームメンバー

Aragamiというチーム名で参加しました。(なぜか自社の障害チケット管理システムの名前を拝借)
構成メンバーは、自社でカジュアルソーシャルコーディングという、カジュアルな勉強会を4年以上続けてきたチームメンバーで参加しました。

  1. id:nakimura:チームリーダー。主夫。アプリ担当。
  2. @wataru420:HipHopper。ミドルウェア周りのチューニング担当。
  3. id:kitakitabauer:私。モグリエンジニア。アプリ担当。

という作業割り振りで行こうと決まりました。
見づらくなるので、誰が何をやったとかは基本的に省略して書いていきます。

言語選定ですが、

高速化対象のソフトウェアとして主催者から Perl, Ruby, Python, PHP, Node.js, Go, Scala によるWebアプリケーションが与えられる。 ただし各々の性能が一致することを主催者は保証しない。どれをベースに用いてもよいし、独自で実装したものを用いてもよい。

ということで、メンバー中2人が Node.js での実務経験が豊富だったことから、
Node > Go >>>>>>>>> Python >>>>>>>>>>>>>>>>> PHP
の順で、当日に実装内容を見てから決めようということになりました。
*1

いきなり詰まったクソ野郎

10:00
予選が開始し、運営から事前に共有されていたポータルサイトが参照・ログイン可能となりました。
と同時に予選用イメージURLが閲覧可能となったので、Azureのリソースグループにインスタンス起動・公開鍵登録後、そこに表示されたPublic IP addressに対してSSHログインしようとしたら…
SSHログインできない・・・!」
もう出だしから一人つまづいて死にたいと思っていたけど、死ぬのをやめて生きて原因を探しました。
こんな感じの~/.ssh/configで、自分はいつもユーザ名を指定せずに"ssh XX.XX.XX.XXX"していたのですが、

user kitakitabauer
…
Host XX.XX.XX.XXX
 User isucon
 IdentityFile ~/.ssh/id_rsa_github

userをHost毎に上書きしてくれている(つまり、Host XX.XX.XX.XXXではisuconユーザでsshしてくれる)と勘違いしていたため、ログインできなかったという恥ずかしいオチでまた死にたくなりました。
生きてユーザ指定して、無事sshできました。

Webアプリの構成を確認

11:00
ベンチマークサイトは、簡易的な辞書ページのようなものでした。
ベンチマークはキューイング方式で、それぞれのチームがベンチマークのキューに登録して実行を待つという感じです。

Node.jsの構成はざっくりこんな感じでした。

./js
├── bin
│   ├── isuda   # isudaプロセス起動
│   ├── isutar   # isutarプロセス起動
│   …
│   └── www
├── route
│   ├── isuda.js   # ルーティングされたリクエストパスの処理詳細/isudaデータベースへのセットアップなど
│   └── isutar.js   # ルーティングされたリクエストパスの処理詳細/isutarデータベースへのセットアップなど
├── views   # テンプレートエンジンによる画面描画
│   …
│   └── index.ejs
├── .gitignore
├── isuda.js   # isudaプロセスのセットアップ/リクエストのルーティング等
├── isutar.js   # isutarプロセスのセットアップ/リクエストのルーティング等
├── npm-shrinkwrap.json
└── package.json


Node実装の確認

Nodeの実装を確認してびっくり!過去問から見る例年のものと比べて想像以上にしっかり実装されていました。
Webフレームワークはkoa。
しかもES7のAsync Awaitで書かれている。これはまずい。koaやAsync Awaitは勉強不足だ…。
それでもGoよりかはましということで、systemctrlで、実行言語をNodeに変更しました。

ベンチマーク初回実行

とりあえずベンチマークが動くことを確認したかったので、キューに入れていざ実行したところ、0点。
スコア計算ですが、エラーよりも、レスポンス遅延のほうが大きく減点されるようで、
実行後のメッセージ欄にはそれほどFailが確認できなかったことから、レスポンス遅延による減点が大きいのかもと推測。

マシンリソースやアプリ以外のボトルネックを確認

その後、アプリをじっくり確認する前に行ったことです。

  • 現時点のwebappをバックアップとしてbitbucketにpush
  • restart.shを作って、slackにWebhook設定することで、restartを1オペでかつslack上からも再起動を確認できるように
  • CPUコアは2コア(これはAzureポータルからも確認できた)やメモリは7GBで潤沢なことを確認
  • 余計なミドルウェアやツールが起動してリソースを逼迫していないか確認
  • ベンチマークを流したあと、データ登録後のMySQLの容量が肥大化しすぎていないか確認
  • アンチョコのmy.cnfを置いて、MySQLのslowqueryをONにしてスロークエリを確認

特段おかしなところはなさそう。
これはまさか、今回アプリをどれだけ改修できるかが勝負となる・・?

kataribeでボトルネック調査

13:00
kataribeを入れて時間がかかっているHTTPリクエストをサマリすることでボトルネックを確認したところ、静的ファイルのリクエストに時間がかかっていると出ていたので、nginxでキャッシングして返す・クライアントでもキャッシュできるならするようにしました。

すると、"/"、"/keyword"、"/login"へのアクセスが異様に遅いことが明白になっていきました。

君の名は。

いろんなリクエストから呼ばれている処理の中で、ユーザの名前を毎回DBから取得していたので、メモリにマップを持たせて初回のみ取得するように。

 const setName = async (ctx) => {
   ctx.state = {};
   const db = await dbh(ctx);
   const userId = ctx.session.userId;
   if (userId != null) {
-    const users = await db.query('SELECT name FROM user WHERE id = ?', [userId.toString()]);
-    if (users.length > 0) {
-      ctx.state.user_name = users[0].name;
+    let name = userNameMap[userId];
+    if (name === undefined) {
+        const users = await db.query('SELECT name FROM user WHERE id = ?', [userId.toString()]);
+        name = users[0] && users[0].name;
+    }
+    if (name) {
+      userNameMap[userId] = name; 
+      ctx.state.user_name = name;

スコアは0。これぐらいじゃあまだまだ。

Nodeで一つ一つのIO処理待ってる意味 is 何?

最も遅い"/"へのリクエストの中で、for文内でAsync Awaitで1つ1つ直列に処理している部分がひどいので、Promiseに詰めて並列実行できるように(リーダーが)改善しました。

-  for (let entry of entries) {
Add a comment to this line
-    entry.html = await htmlify(ctx, entry.description);
-    entry.stars = await loadStars(ctx, entry.keyword);
-  }
+
+  const tasks = entries.map(entry => {
+    return Promise.all([
+      htmlify(ctx, entry.description),
+      loadStars(ctx, entry.keyword),
+    ]).then(result => {
+      entry.html = result[0];
+      entry.stars = result[1];
+    });
+  });
+  await Promise.all(tasks);

スコアはまだ0のままうんともすんとも。

キーワード長を毎回取得するなんて

MySQLクエリ解析にかけるほどでもなく明らかだったのですが、下記は"keyword"を長さ順に全件取得しているので重いです。

SELECT * FROM entry ORDER BY CHARACTER_LENGTH(keyword) DESC

キーワード長は不変なので、キーワードをDBに登録する処理の時に、長さも合わせて保存することで、HTMLページのキーワードリンク生成処理時に毎回レングスを取ってこなくてもいいように変更。

ALTER TABLE entry ADD COLUMN `keyword_length` int(11) after `keyword`
   await db.query(
-    'INSERT INTO entry (author_id, keyword, description, created_at, updated_at) ' +
-    'VALUES (?, ?, ?, NOW(), NOW()) ' +
+    'INSERT INTO entry (author_id, keyword, keyword_length, description, created_at, updated_at) ' +
+    'VALUES (?, ?, ?, CHARACTER_LENGTH(?), NOW(), NOW()) ' +
     'ON DUPLICATE KEY UPDATE ' +
-    'author_id = ?, keyword = ?, description = ?, updated_at = NOW()',
+    'author_id = ?, keyword = ?, keyword_length = CHARACTER_LENGTH(?), description = ?, updated_at = NOW()',
     [
-      userId, keyword, description, userId, keyword, description
+      userId, keyword, keyword, description, userId, keyword, keyword, description
     ]);

うーん、いまだスコアは0のまま。

initializeでイニシアチブとってこ

HTMLページのキーワードリンクを生成するための処理の中で、for文で全ての登録キーワードをDBから毎回取得して正規表現でゴニョゴニョしている部分を、まずinitializeで一度行うように(これもリーダーが)変更して、その後新しいキーワードが登録されたときだけ、その正規表現を更新するように。
あと、entryテーブルの全フィールドをSELECTしていたのを"keyword"だけにするのも同時に。

これはかなりききそうだけど、まだスコアは0のまま。。ほんとに上がるのか・・?
(後々、他の箇所の実装で書かれたSQL構文が間違っていたことでスコアが上がらなかったことに気づきました。なので、ここだけの伸びはわかりかねますが、多分今回対応できた中ではここが一番効いたのかと!)

パスワード=名前説

14:00
/login へのリクエストのときに、SHA-1でメッセージダイジェストを生成して、userテーブルのパスワードと比較していますが、この生成されたメッセージダイジェストを保存するようにしたら、同じユーザのログインが高速化されるかもという話が上がり、改修していきました。
すると、そもそもリクエストされたパスワードがユーザのnameと一緒だという衝撃の事実が明らかになりました。

これは、ちゃんとHTTPステータス4xxのものは弾いて、それ以外はすんなりログインさせれば、かなりの高速化が見込めそうだということで修正した結果、ようやくスコアが 0 → 約6000 まで上がりました。

こんなマイクロサービスは嫌だ

15:00
コードをじっくり読んでいくと、isudaとisutarが、互いにHTTPアクセスして、アクセス先のプロセスでisuda/isutarデータベース要求している部分が完全に無駄なので、それぞれの実装にDBアクセス設定をゴリゴリ書いてアクセスするようにしました。

これによって 約6000 → 9970 まで上がりました!
もっと余裕があれば、isudaとisutarの完全統合までやりたかった。

Nodeプロセス数が微妙に多い、そう微妙に。

17:00
起動するisudaとisutarのNodeプロセス数を、CPUコア数に合わせて3→2ずつに変更
9970 → これまでのベストスコア 12023 に!

と、ここまででタイムリミット。最後の最後に1万超えは嬉しかった…

ISUCONに参加してみて

個人的な反省点は下記の通り。

  • 最後までサーバローカルで直接ソースコードを編集していたので、同時に修正できなかった。
    • 結構思い込みでコーディングしてた中でミスも多々あったので、レビューもし易いことを考えても効率は上がるはず
    • 声を掛け合っていたので、デグレードが起きなかったのはせめてもの救い
  • 次はチェックリストを作っておいて、作業の経過が見えたり、よりバッティングしないようにしたほうがいいかも
    • id:foostanさんのように、GitHubのProjectsを使ってみるのもいいなぁと
  • Async Awaitとかkoaとか、もっとEcmaScriptやNodeの新しめの仕様を勉強しておけばよかった
  • ページングのために、キーワードの総数を毎回SELECT COUNTしていたけど、キーワード登録のときにメモリに回数を持って、それを使うようにすればそこそこ速くなったはず。途中まで実装していたけど間に合わず。
  • html生成の部分がもっとも重いことは明白だったので、トライ木の構築をしてキャッシュすればかなり速くなるということで、id:nakimuraさんが最後の最後までライブラリを検証しながらテストしてたけど結局スコアが落ちてしまい、導入を断念orz
    • トライ木の構築はメモリは食うのだけど、だからこそメモリが潤沢だったと思われるだけに残念…


ISUCON、前々から興味はあったけど、自分の現在地を知るのが恐くて参加できずにいました。
そしてこんな優秀なメンバーと参加できた以上、もっと貢献できたらなぁと思う場面ばかりで、悔やまれるばかりです。

でもめっちゃ楽しかった!プライベートな時間は結構費やしたけど、とても勉強になったので、もっと周りのISUCON人口を増やして切磋琢磨したいです。


本選に出場されたチームの皆様、当日は激しい闘いを期待しています!
最後に、ISUCON運営の皆様、本当にありがとうございました!
まだ本選は控えていますが、来年も激しく楽しい大会の開催を期待しています!

*1:ちなみに、C++で独自実装したチームもいたそうです。しかも予選通過。すごすぎる…!