bauer's diary

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

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++で独自実装したチームもいたそうです。しかも予選通過。すごすぎる…!

ghq/peco/hubを組み合わせてローカル・リモートリポジトリに即座にたどり着く

はじめに

あまりにも有名な下記ツールを組み合わせて、ストレスなく、リポジトリにたどり着くような設定にしたのでまとめておきます。

あまりにも有名なので、ツールの説明は割愛します。が、ざっくりいうと、

Go製のリポジトリ管理ツール
github.com
同じくGo製の使い勝手のいいフィルタリングツール
github.com
GitHubCLIで操作するツール
github.com
ですw

ローカル編

ローカルにcloneしたリポジトリを一覧して選択したリポジトリに移動する 操作を素早く行えるようにしています。
zshだとこんな感じの設定になります。

function peco-src () {
  local selected_dir=$(ghq list -p | peco --query "$LBUFFER")
  if [ -n "$selected_dir" ]; then
    BUFFER="cd ${selected_dir}"
    # pecoで選択中, Enter を押した瞬間に実行する
    zle accept-line
  fi
  zle clear-screen
}
# 関数をウィジェットに登録
zle -N peco-src
bindkey '^]' peco-src

"ghq list -p" で、"ghq get"でローカルにクローンしたリポジトリ一覧を表示します。
パイプでつないだ'peco --query "$LBUFFER"'で、その一覧をpecoのコンソールに表示させます。
このようなやり方は、他のQiitaの記事でも紹介されている(というかほぼパクリ)ので、目新しいことはありません。

リモート編

リモートリポジトリを一覧し、選択したリポジトリをブラウザで表示します。
下記は、先程のローカル用の関数をカスタマイズして作成した関数です。

function peco-src-remote () {
  local selected_repo=$(ghq list -p | peco --query "$LBUFFER" | rev | cut -d "/" -f -2 | rev)
  echo $selected_repo
  if [ -n "$selected_repo" ]; then
    BUFFER="hub browse ${selected_repo}"
    zle accept-line
  fi
  zle clear-screen
}
zle -N peco-src-remote
bindkey '^^' peco-src-remote
解説

hubツールに"browse"というコマンドを使って、リモートのリポジトリページをブラウザで開くことができます。

$ hub browse [ユーザ名/リポジトリ名]

なので、下記のようなローカルリポジトリのパス文字列を分解して"hub browse"に渡すということをしたいとおもいます。

$ /Users/kitakitabauer/.ghq/github.com/ユーザ名/リポジトリ名

しかし、$GOPATHやghqのルートによって、パス階層がどのぐらいになるのかわからないので、渡す文字列を固定で切り出すことがめんどくさそうです。

  • /Users/kitakitabauer/.ghq/github.com/kitakitabauer/dotfiles
  • /Users/kitakitabauer/go/src/github.com/kitakitabauer/dotfiles
  • /Users/kitakitabauer/go/src/bitbucket.org/kitakitabauer/dotfiles

自分は以前の記事でご紹介しましたが、".ghq" や "go/src" のように、cloneするディレクトリを分けているので、単純に前者と後者で階層に1つ差が生まれます。

そこで、今回の記事のポイントなのですが、汎用的に"hub browse"に渡したい文字列を整形できるようにしました。
ポイントは下記の部分です。

rev | cut -d "/" -f -2 | rev

1. rev で文字列をひっくり返す

名リトジポリ/名ザーユ/moc.buhtig/qhg./reuabatikatik/sresU/

2. cut -d "/" -f -2 で、スラッシュ区切りで先頭から2フィールド目までを取り出す

名リトジポリ/名ザーユ

3. rev で再び逆にして、ユーザ名/リポジトリ名の順番に戻す

ユーザ名/リポジトリ名


最後に作成した関数を"bindkey"コマンドで、割当がないキーにあてることで一発呼び出しができます。

bindkey '^]' peco-src
bindkey '^^' peco-src-remote



今回のzshの設定を含めたdotfilesはこちらにコミットしています。
github.com

rev は使い道に迷いますが、cut と組み合わせることで他にも面白いことができそうです。


おしまい。

Goのことはじめ その1:GOPATHとghqのルート指定

はじめに

業務でGoを触ることになったので、Goに関して気になったことをまとめていこうと思います。

GOPATH

GOPATHはGoで使う環境変数なのですが、2つの役割があります。

  1. ビルド時のインポートパスのルート(正確には$GOPATH/src配下)
  2. go getコマンドで外部パッケージをインストールしたときの、ダウンロードルートディレクト

詳細は公式ドキュメントに記載されています。
How to Write Go Code - The Go Programming Language

ghq

id:motemen さん作の、Go製のリポジトリ管理ツールです。
github.com

$ ghq get (GitHubの)ユーザ名/リポジトリ名

とすると、ローカルの "~/.ghq" というディレクトリにデフォルトでインストールされます。
"ユーザ名/リポジトリ名"の部分ですが、GitHub以外、例えばbitbucketは"https://ユーザ名@bitbucket.org/…"のように一から指定する必要があります。

Goにはgetコマンドがあるので、事足りるような気もするのですが、他にもクローンされたリポジトリを一覧したり、指定したローカルのリポジトリにcdできるので、重宝しています。

GOPATHとghqのルートをどこに設定するか

「GOPATHは適当に決めて問題ない」という記事もありましたが、個人的には適当に決めてあとで地雷を踏みたくないのが性分です。
例えば".ghq"の下にgetしたけど、やっぱりこのライブラリをインポートしたいから"go/src"の下に入れたい…となったときにシンボリックリンクを貼るのか、同じものをgo getするのかなど、色々困りそうです。

それを踏まえて、自分はこのようにしようと思います。

$GOPATH
export GOPATH=~/go
.gitconfig
[ghq]
  root = ~/.ghq
  root = ~/go/src

Go 言語のソースだけは go get で 取得し "$GOPATH/src" 以下に
そうでないものは ghq get で "~/.ghq" 以下に取得するようにしています。

  • go getした結果は "$GOPATH/src" に配置される
  • ghq getしたものも同じディレクトリに配置させるため


今回はここまでです。
次回はGoのバージョン管理システム選定について書きたいと思います。

dotfiles整理 その2:zshrcから分割した設定ファイルが読み込まれない問題への対処

kitakitabauer.hatenablog.com
前回の記事はこちら。
この構成に変えたあと、.zprofileに記述した部分が読み込まれなくなりました。

ターミナルアプリで起動されるのは、ログインシェルではなく、インタラクティブシェル

という事実を下記記事にて知りました…!
qiita.com

自分はターミナルアプリにiTerm2を使っており、下記の通りbrewインストールしたzshを起動しています。
f:id:kitakitabauer:20160913180649p:plain

前回の記事で「zshが起動時に読み込むファイルとその順番」について記述しましたが、
ログインシェルでないと、$ZDOTDIR/.zprofile は読み込まれません。

かといって、"Send text as start: "に "source $HOME/.zprofile" を入れて毎回読み込むのも、インタラクティブシェルのときに毎回読み込むことになるので、zsh標準の仕様と外れるので気持ち悪いし、セッションを起動するたびにそのコマンドが毎回出力されるのも嫌です。

ログインシェル変更

ターミナルアプリ上ではなく、ローカル設定からzshを指定するようにして対応することとします。
(なので、iTerm上の設定は"Command"→"Login shell"に変更しておきます)

まずは現在のインストールシェル一覧を確認
$ cat /etc/shells
# List of acceptable shells for chpass(1).
# Ftpd will not allow users to connect who are not using
# one of these shells.

/bin/bash
/bin/csh
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh
/etc/shellsに、brewインストールされたzshのパスを追加

デフォルトインストールされたzshはバージョンが少し古いため、brewインストールしたzshのパスをroot権限で追加します。

$ sudo vi /etc/shells

…
/bin/zsh
/usr/local/bin/zsh   # 追加
ログインシェル変更

デフォルトのシェルを先ほど追加したzshに変更します。
下記コマンドによって、/etc/shellsの一番最後に記述されたzshに変更します。

$ chsh -s "$(command -v zsh)" "${USER}"
確認

読み込んだ順番を確認するために、それぞれの設定ファイルにechoを入れたあと、
ターミナルアプリの別セッションを立ち上げると、

~/.zshenv
~/.zsh/.zshenv
~/.zsh/.zprofile
~/.zsh/.zshrc

問題なし!


尚、ログインシェル変更の方法は色々あるらしいです。
rcmdnk.github.io

dotfiles整理 その1:zshの構成を見なおそう

はじめに

最近ちょっと時間ができたので、随分放置してきたdotfilesを整理をすることにしました。

まずはzsh
これまでは.zshrcに全て記述していたので、前々から役割毎に分けたいと思っていました。
大まかな方針は下記の通りです。

  1. ZDOTDIR を指定して、"~/.zsh" の下にzsh関連の設定ファイルを配置したい
  2. .zshenv、.zprofile に移譲したい

構成はこんな感じを想定しています。

/Users/kitakitabauer/.
├── .zshenv -> /Users/kitakitabauer/dotfiles/.zshenv
├── .zsh
│   ├──.zprofile -> /Users/kitakitabauer/dotfiles/.zsh/.zprofile
│   ├──.zshenv -> /Users/kitakitabauer/dotfiles/.zsh/.zshenv
│   └──.zshrc -> /Users/kitakitabauer/dotfiles/.zsh/.zshrc
└── …


環境変数 ZDOTDIR の設定

ホームディレクトリがzsh関連のファイルで埋まるのが嫌なので、ZDOTDIR を指定して、zsh関連の設定ファイルを別フォルダにて管理します。

ZDOTDIR は$HOME/.zshenv に記述します。

export ZDOTDIR=$HOME/.zsh

source $ZDOTDIR/.zshenv

これで、$HOMEに置く必要があるのは.zshenvだけになります。

zshが起動時に読み込むファイルとその順番

zshには様々な設定ファイルがあり、それらは決まった順番で読み込まれます。

ログインシェルは下記の順番で読み込まれます。

/etc/zshenv
$ZDOTDIR/.zshenv
/etc/zprofile
$ZDOTDIR/.zprofile
/etc/zshrc
$ZDOTDIR/.zshrc
/etc/zlogin
$ZDOTDIR/.zlogin

インタラクティブシェルは下記の順番で読み込まれます。

/etc/zshenv
$ZDOTDIR/.zshenv
/etc/zshrc
$ZDOTDIR/.zshrc

ログインシェルとしてzshを使うので、前述の通り.zshenv、.zprofile、.zshrcの構成でいきます。
.zloginは今のところ使用しないので作成しません。
尚、全てのユーザに影響するため、etc配下の設定ファイルは変更しません。

それぞれの設定ファイルに何を記述するか

$ZDOTDIR/.zshenv

環境変数系。ヒストリー系もここで記述します。

$ZDOTDIR/.zprofile

ログインシェルだけで使いたいaliasを記述します。

$ZDOTDIR/.zshrc

上述以外の全てを記述します。

dotfilesのシンボリックリンク生成

最後にsymlink.shにて、gitリポジトリのdotfilesに対して、$HOMEにシンボリックリンクを貼ります。

basepath=$(cd $(dirname $0);pwd)

# symlink dotfiles into ~files=.*
for file in $files
do
  if [ ! -d $file -a $file != "." -a $file != ".." -a $file != ".git" ] ; then
    ln -sf $basepath/$file ~
  fi
done

# symlink zsh configuration files into ~/.zsh
if [ ! -d ~/.zsh ] ; then
  mkdir ~/.zsh
fi
for file in .zsh/.*
do
  if [ $file != "." -a $file != ".." -a $file != ".git" ] ; then
    ln -sf $basepath/$file ~/.zsh/
  fi
done


完成

まだ工事中ですが、一旦こんな感じになりました。
github.com


次の記事はこちら
kitakitabauer.hatenablog.com

きたるISUCON6向け武者修行その4 -kataribeでHTTPリクエストのパフォーマンスを可視化しよう-

kitakitabauer.hatenablog.com

前回はNginxのチューニングを行いました。
今回はISUCONの過去問でも登場してきたプロファイリング用のツールを予習しておきます。

kataribeとは?

"Nginx/Apache/Varnishncsa Log Profiler"と書かれている通り、
アクセスログに出力されたリクエスト時間を元に、時間がかかっているアクセスをランキング化してわかりやすくしてくれるツールです。
github.com

インストール

このサイトから実行元OSのものをダウンロードします。
Releases · matsuu/kataribe · GitHub

$ wget https://github.com/matsuu/kataribe/releases/download/v0.3.0/linux_386.zip ~

unzipがなかったので、yumインストールして展開します。

$ sudo yum install unzip

$ unzip ~/linux_386.zip
Archive:  /home/vagrant/linux_386.zip
  inflating: LICENSE
  inflating: README.md
  inflating: kataribe
  inflating: kataribe.toml


ログフォーマット確認、プロファイル用ログの準備

nginx.confに定義するログフォーマットは前回設定しましたが、kataribeのREADMEに記載された通りなので問題ありません。
上記フォーマットでログを出力するために、ベンチマーカーを一度動かしておきます。

$ ./benchmarker b


アクセスログをkataribeでプロファイル

終わったら、nginxのアクセスログをkataribeに噛ませます。

$ cat /var/log/nginx/access.log | ./kataribe

Top 20 Sort By Total
Count   Total       Mean     Stddev     Min   P50.0   P90.0   P95.0   P99.0     Max   2xx  3xx  4xx  5xx  Request
  587  46.784   0.079700   0.027365   0.004   0.092   0.102   0.109   0.134   0.182     0  587    0    0  POST /login HTTP/1.1
    1  23.642  23.642000   0.000000  23.642  23.642  23.642  23.642  23.642  23.642     1    0    0    0  GET /report HTTP/1.1
 3519  14.101   0.004007   0.001516   0.001   0.004   0.006   0.007   0.009   0.013  3519    0    0    0  stylesheets
 1173   4.765   0.004062   0.001323   0.001   0.004   0.006   0.006   0.008   0.014  1173    0    0    0  images
  113   3.293   0.029142   0.012153   0.002   0.033   0.037   0.039   0.047   0.054   113    0    0    0  GET /mypage HTTP/1.1
 1060   1.645   0.001552   0.000827   0.001   0.001   0.002   0.002   0.005   0.009  1060    0    0    0  GET / HTTP/1.1

単純なリクエスト数や中央値など色々な指標で出力されますが、上記のTotalが、最も時間がかかっているリクエストパスのランキングになります。
なので、ここでは/loginを改善すればパフォーマンスが出そう!ということが考察できます。

おまけ

リクエスト時間には関係ないですが、最も多いHTTPリクエストを昇順に表示するワンライナーです。
kataribeでも"Top 20 Sort By Count"で出力されますが、ワンライナーでさくっと表示したい場合はこちらで。

$ cat /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -n
      1 /report
    113 /mypage
    587 /login
   1060 /
   1173 /images/isucon-bank.png
   1173 /stylesheets/bootflat.min.css
   1173 /stylesheets/bootstrap.min.css
   1173 /stylesheets/isucon-bank.css