Jekyll2022-12-31T04:49:04+00:00https://genkami.github.io/feed.xmlTOKYO OYASUMI CLUBプログラミング関係のメモ等年末なので、今まで書いてきた”ほぼ全て”のコードを眺めてみる2022-12-31T00:00:00+00:002022-12-31T00:00:00+00:00https://genkami.github.io/2022/12/31/review-all-code<p>かなり久しぶりのブログです。</p>
<p>4~5年ほど前から書き捨てのコードや勉強用のコードなどを一つのリポジトリに雑に放り込んでいたのですが、それなりのサイズになってきたので今まで書いてきたコードを振り返ってみることにしてみました。</p>
<h2 id="対象となるコード">対象となるコード</h2>
<p>対象となるコードは、前述した通りのプライベートリポジトリの中にあるもの全てになります。このリポジトリ自体は(本の写経など権利的に公開してよいか微妙なものもあるので)非公開ですが、だいたい以下のような目的で書かれたコードが放り込まれています:</p>
<ul>
<li>本やドキュメントなどのサンプルコードの写経やそれをもとに書いてみたコード</li>
<li>特定のライブラリ or API or 言語機能等をとりあえず使ってみるだけのコード</li>
<li>書き捨ての、おそらく二度と使わないコード</li>
<li>一旦GitHub等で公開してみたものの、そんなにメンテする気も使う気もないので消してしまったものを墓場がわりにsubtreeでぶち込んでいるもの</li>
<li>等々……</li>
</ul>
<p>その他にも、この前実家に帰ったタイミングで発掘された過去のコード(おそらく2009~2017頃に書かれたもの)も一部含まれています。さすがにこの時代のコードは大部分が消失してしまっていますが、それでもある程度主なものは奇跡的に残っていました。</p>
<h2 id="集計">集計</h2>
<p>こんな感じのRubyスクリプトを書いて、どの言語をどれくらい書いたのか集計してみます:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env ruby</span>
<span class="nb">require</span> <span class="s1">'rugged'</span>
<span class="nb">require</span> <span class="s1">'linguist'</span>
<span class="c1"># YAML が統計にカウントされないのを無理やり直す (Kubernetes マニフェストとかもカウントされてほしい)</span>
<span class="no">Linguist</span><span class="o">::</span><span class="no">Language</span><span class="p">.</span><span class="nf">find_by_name</span><span class="p">(</span><span class="s1">'YAML'</span><span class="p">).</span><span class="nf">instance_eval</span> <span class="p">{</span> <span class="vi">@type</span> <span class="o">=</span> <span class="ss">:markup</span> <span class="p">}</span>
<span class="n">repo</span> <span class="o">=</span> <span class="no">Rugged</span><span class="o">::</span><span class="no">Repository</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'.'</span><span class="p">)</span>
<span class="n">project</span> <span class="o">=</span> <span class="no">Linguist</span><span class="o">::</span><span class="no">Repository</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">repo</span><span class="p">,</span> <span class="n">repo</span><span class="p">.</span><span class="nf">head</span><span class="p">.</span><span class="nf">target_id</span><span class="p">)</span>
<span class="n">lines</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">total_lines</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">pwd</span> <span class="o">=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span>
<span class="n">project</span><span class="p">.</span><span class="nf">breakdown_by_file</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">lang</span><span class="p">,</span> <span class="n">files</span><span class="o">|</span>
<span class="n">lines</span><span class="p">[</span><span class="n">lang</span><span class="p">]</span> <span class="o">||=</span> <span class="mi">0</span>
<span class="n">files</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span>
<span class="n">blob</span> <span class="o">=</span> <span class="no">Linguist</span><span class="o">::</span><span class="no">FileBlob</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="n">pwd</span><span class="p">)</span>
<span class="n">lines</span><span class="p">[</span><span class="n">lang</span><span class="p">]</span> <span class="o">+=</span> <span class="n">blob</span><span class="p">.</span><span class="nf">loc</span>
<span class="n">total_lines</span> <span class="o">+=</span> <span class="n">blob</span><span class="p">.</span><span class="nf">loc</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">langs</span> <span class="o">=</span> <span class="n">project</span><span class="p">.</span><span class="nf">languages</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">sort</span> <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="o">|</span> <span class="n">lines</span><span class="p">[</span><span class="n">b</span><span class="p">]</span> <span class="o"><=></span> <span class="n">lines</span><span class="p">[</span><span class="n">a</span><span class="p">]</span> <span class="p">}</span>
<span class="nb">printf</span> <span class="s2">"%15s</span><span class="se">\t</span><span class="s2">%8s</span><span class="se">\t</span><span class="s2">%6s</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"LANG"</span><span class="p">,</span> <span class="s2">"LOC"</span><span class="p">,</span> <span class="s2">"LOC%"</span>
<span class="n">langs</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">lang</span><span class="o">|</span>
<span class="n">loc</span> <span class="o">=</span> <span class="n">lines</span><span class="p">[</span><span class="n">lang</span><span class="p">]</span>
<span class="nb">printf</span> <span class="s2">"%15s</span><span class="se">\t</span><span class="s2">%8d</span><span class="se">\t</span><span class="s2">%6.2f</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">lang</span><span class="p">,</span> <span class="n">loc</span><span class="p">,</span> <span class="p">(</span><span class="mi">100</span> <span class="o">*</span> <span class="n">loc</span><span class="p">.</span><span class="nf">to_f</span> <span class="o">/</span> <span class="n">total_lines</span><span class="p">)</span>
<span class="k">end</span>
<span class="nb">printf</span> <span class="s2">"%15s</span><span class="se">\t</span><span class="s2">%8d</span><span class="se">\t</span><span class="s2">%6.2f</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"TOTAL"</span><span class="p">,</span> <span class="n">total_lines</span><span class="p">,</span> <span class="mf">100.0</span>
</code></pre></div></div>
<p>リポジトリのサイズがそれなりにでかいのでやや時間がかかりますが、「並列に集計しておけばよかったな~」と言っている間に集計が終わる程度には時間がかからなかったので今回はそのままにしました。</p>
<p>これを上述したリポジトリ直下で実行すると、以下のようにそのリポジトリに含まれているコードの行数が言語ごとに集計されます:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> LANG LOC LOC%
Rust 49649 19.27
Haskell 30464 11.82
Go 30441 11.81
C 24654 9.57
YAML 16135 6.26
Python 15563 6.04
Scheme 10625 4.12
Java 9937 3.86
Assembly 8187 3.18
Scala 6221 2.41
Elixir 5133 1.99
Ruby 5067 1.97
Starlark 4573 1.77
OCaml 3787 1.47
C# 3539 1.37
Elm 3444 1.34
JavaScript 3378 1.31
HTML 3240 1.26
HCL 3048 1.18
C++ 3003 1.17
SystemVerilog 2396 0.93
Coq 2201 0.85
Cuda 2042 0.79
CSS 1480 0.57
Shell 1324 0.51
Makefile 1038 0.40
Lua 902 0.35
WebAssembly 891 0.35
Racket 887 0.34
Processing 699 0.27
Verilog 629 0.24
SuperCollider 523 0.20
CUE 520 0.20
Dockerfile 462 0.18
Procfile 457 0.18
Prolog 274 0.11
ShaderLab 205 0.08
PHP 189 0.07
Perl 111 0.04
CoffeeScript 105 0.04
M4 71 0.03
Batchfile 62 0.02
COBOL 59 0.02
Smarty 42 0.02
Dhall 23 0.01
Jsonnet 10 0.00
SCSS 9 0.00
Brainfuck 1 0.00
TOTAL 257700 100.00
</code></pre></div></div>
<p>思っていたより言語の数が多かったので、コード量上位10件分くらいの言語について何を書いたのか等を振り返っていきたいと思います。</p>
<h2 id="第1位-rust-49649行">第1位: Rust (49649行)</h2>
<p>1位はRustでした。とはいえ、体感としては行数ほど習熟している感じはあまりしません。
書いたコードはおそらくほとんど今年のものだと思います。また、実用的なコードをちゃんと書いたこともありません。</p>
<p>行数が膨れ上がっている理由は、おそらく今年の夏ごろに転職しようと思ってLeetCodeやAtCoderの問題をちょこちょこ解いていたからだと思います。雑に数えてみると、この2つだけで合計255問も解いていたようです(キリがいいのは偶然です)。この手のコード(特にAtCoderのもの)はテンプレ関数のコピペによって行数が膨れ上がる傾向にあるので、そのあたりがこの結果につながったのではないかなと思います。</p>
<p>その他にも最近は <a href="https://github.com/pixix4/ev3dev-lang-rust">ev3dev-lang-rust</a> でマインドストームのロボットを動かしたりするのにもRustを使っています。</p>
<h2 id="第2位-haskell-30464行">第2位: Haskell (30464行)</h2>
<p>2位はHaskellでした。Haskellはここ数年ほとんど書いていないので、おそらく大昔に書かれたコードが大半を占めているのではないかと思います。
内容としては、主に高校~大学(2012~2016くらい)の頃に書かれた中規模程度のアプリケーションのコードが多いように見えました(逆に書き捨てのコード系は古いのでほとんど消失してしまっているようです)。</p>
<p>具体的なコードとしては、Yesodで作られたWebサイト、大学の講義で作ったCのサブセットのコンパイラ、Git(のかなり限られたサブセット)のクローン実装、諸々のesolangの自作処理系等が眠っていました。</p>
<p>Haskellは最近は全然触っていませんが、並行処理を低コストかつ気軽に行うことができる結構いいランタイムを持った言語なので現代においてこそ役に立つ場面がまだまだあるんじゃないかという気がしています。またどこかの機会で触っておきたいです。</p>
<h2 id="第3位-go-30441行">第3位: Go (30441行)</h2>
<p>3位はGoでした。Goはここ3年程度メインで使っていたので、これだけ行数が増えるのも納得かなという感じです。</p>
<p>内容についてですが、先ほどのRustと違ってこちらは何かのライブラリなどをとりあえず試しに使ってみただけみたいなコードが大半を占めているように見えました。先ほどと違って言語の習熟度に比例して順当に行数が増えている感じもあり、納得感があります。</p>
<p>Goの行数が多いその他の理由として、最近は何の言語でやってもいいことはとりあえずGoでやることが多かった点、コード生成を多用する文化なので生成されたコードがある程度混ざってそうという点も挙げられます。</p>
<h2 id="第4位-c-24654行">第4位: C (24654行)</h2>
<p>4位はCでした。CのコードもGoと同様に勉強用っぽいコードがほとんどを占めていますが、Goとは違って特定のライブラリというよりはPOSIXのAPIを叩いてみるだけなどのようなコードが多いように見えました。</p>
<p>中学生の頃(2009頃?)、自分が一番最初にプログラミングを勉強し始めた時期のコードもごく一部残っていました(おそらくこの時代としては珍しく、なぜかCでプログラミング入門しました)。この頃のコードはSVNで管理されていたり、そもそもVCSを使わずに日付でディレクトリを切って†バージョン管理†しているコード等もあって懐かしい気持ちになりました。</p>
<p>最近はなぜか一生使うことのなさそうなWindows APIを触り始めていたりするので、Cの行数はこれからも増えていきそうです。</p>
<h2 id="第5位-yaml-16135行">第5位: YAML (16135行)</h2>
<p>5位はYAMLでした。これはおそらくほとんどKubernetesのせいだと思います。それ以外にも諸々のツールやフレームワークなどの設定ファイルなども若干含まれているようです。</p>
<p>Kubernetesは2020年ごろにちゃんと触り始めましたが、2021年の途中くらいから仕事が忙しくなりすぎてキャッチアップする暇がなくなってきてしまいました。そのため、最近の事情などはあんまりわかっていません。また、今のところユーザーとしての立場でKubernetesを触ることしかしていなかったので、今後は自宅にクラスタを立てて管理してみるというのも面白いかもしれません。</p>
<h2 id="第6位-python-15563行">第6位: Python (15563行)</h2>
<p>6位はPythonでした。Pythonは最近はあまり触っていませんが、昔(中高生くらい。2009~2012頃?)はメインで使っていたためそれなりの量のコードが残っていました。現在残されているコードも多くはこの時代に書かれたもののようです。</p>
<p>そのほかにも、研究室(2017頃)でちょっとだけディープラーニングをかじっていた頃のコードも少し残っていました。</p>
<p>最近はごく稀に <a href="https://github.com/kitao/pyxel">Pyxel</a> でちょっとしたミニゲームを作るのにPythonを使ったりしてます。</p>
<h2 id="第7位-scheme-10625行">第7位: Scheme (10625行)</h2>
<p>7位はSchemeでした。なぜか2016~2017ごろにGaucheばかり書いていた時期があったので、この頃のコードが大半なのではないかと思います。それ以外にも授業で <a href="http://www.yuasa.kuis.kyoto-u.ac.jp/~yuasa/jakld/index-j.html">JAKLD</a> というマイナーScheme処理系を使って課題を解いていた頃のコードなどもごくごく一部だけ残っていました。Linguist gemの判定では別言語扱いになっていますが、Racketのコードも少しあります。</p>
<h2 id="第8位-java-9937行">第8位: Java (9937行)</h2>
<p>8位はJavaでした。見事ランクインしたJavaですが、実態としては2016年頃に大学で出されたHTTPサーバーを作ろうみたいな課題を提出するためにたった一度書いたっきり、二度と触れることのないまま今に至っています…。</p>
<p>本来はおそらく200行程度で済む簡易的なHTTPサーバーが一個あれば課題としては合格だったはずなのですが、気合を入れすぎて機能モリモリのWebアプリケーションフレームワークもどきをフルスクラッチで作ってしまったので行数が大幅に増えてしまった上に提出期限もだいぶ過ぎてしまった記憶が残っています。今思うと、採点する側も大変だっただろうな……。</p>
<p>とはいえ、まあ使える程度のルーティング、テンプレート、ORM(もどき)がぜんぶ入ったWebアプリケーションフレームワークであっても意外とこの程度の規模に収まるんだなということに気づけたのは結構いい経験だったのではないかなと思います。</p>
<h2 id="第9位-アセンブリ-8187行">第9位: アセンブリ (8187行)</h2>
<p>9位はアセンブリでした。内訳としてはほとんどがx64のものになります。それ以外にも、少数ARMやRISC-V, MIPS, 大学で作った謎の自作アーキテクチャなどのものも混じっています。</p>
<p>アセンブリについてはあまり具体的に何かを作ったという感じではなく、CやGoと同様に勉強目的で何か動かしてみただけみたいなコードが大半を占めています。
そのうちアセンブリがちゃんと必要になってくる物も作りたいですね。</p>
<p>アセンブリの行数が多い理由は、たくさん書いたからというよりは単純にアセンブリだからだと思います。</p>
<h2 id="第10位-scala-6221行">第10位: Scala (6221行)</h2>
<p>10位はScalaでした。内訳としては自作Schemeインタプリタが半分、 <a href="https://gihyo.jp/book/2021/978-4-297-12305-5">RISC-V本</a> を参考に書いたコードが残り半分といった感じです。</p>
<p>Scalaは2014年ごろバイトで使っていたのですが、バイトを辞めてからはほとんど自主的に書かなくなってしまいました。</p>
<h1 id="まとめ">まとめ</h1>
<p>書き捨てのコードだけで25万行以上も溜まっていたのにはびっくりしましたが、よく考えると13年分のコードが(一部とは言え)積み重なっているわけなので一日に54行書けば今の行数に達してしまいます。
実際は直近4年くらいのコードが多くを占めているのでこんなに単純な計算にはならないのですが、それでも塵も積もれば結構な行数になるということが実感できました。</p>
<p>また、単純なコードの行数と主観的な習熟度に対する自信がそんなに釣り合っていないというのも新しい気づきでした。</p>
<p>来年は仕事ではGo, 趣味ではRust, C, アセンブリみたいな年になると予想しているので、今後はこのあたりの言語の行数がまた伸びていくんじゃないかと思っています。</p>
<p>それでは、よいお年を。</p>かなり久しぶりのブログです。gRPC でメソッドが副作用を持つ場合冪等性キーを持つことを保証する2021-07-17T00:00:00+00:002021-07-17T00:00:00+00:00https://genkami.github.io/2021/07/17/protobuf-ensure-idempotent<p>gRPC に限らず、一般的に副作用のある API 呼び出しを行う際にはリクエストに一意な ID (いわゆる冪等性キー)を付け、同じ ID のリクエストが複数回飛んできた場合後続のものは無視することによって冪等性を保つ手法が取られることがあります。</p>
<p>このような冪等性キーを用いる方法は gRPC の仕様として標準化されてはいないものの、特定のサービス内や組織内で統一した方法を強制したい場合は多いかと思います。この記事では、 gRPC の Go 実装においてそのような冪等性キーに関するルールを強制する方法を紹介します。</p>
<h2 id="idempotencylevel-methodoption">IdempotencyLevel MethodOption</h2>
<p>gRPC には <a href="https://pkg.go.dev/google.golang.org/protobuf@v1.27.1/types/descriptorpb#MethodOptions_IdempotencyLevel">IdempotencyLevel</a> という <code class="language-plaintext highlighter-rouge">MethodOption</code> が存在します。 <code class="language-plaintext highlighter-rouge">IdempotencyLevel</code> の値は <code class="language-plaintext highlighter-rouge">IDEMPOTENCY_UNKNOWN</code>, <code class="language-plaintext highlighter-rouge">NO_SIDE_EFFECTS</code>, <code class="language-plaintext highlighter-rouge">IDEMPOTENT</code> の3種類があり、これらを指定することでメソッドの冪等性のレベルを注釈することができます:</p>
<div class="language-protobuf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">service</span> <span class="n">Example</span> <span class="p">{</span>
<span class="k">rpc</span> <span class="n">ReadOnlyMethod</span><span class="p">(</span><span class="n">DummyReadOnlyRequest</span><span class="p">)</span> <span class="k">returns</span> <span class="p">(</span><span class="n">DummyReadOnlyResponse</span><span class="p">)</span> <span class="p">{</span>
<span class="k">option</span> <span class="na">idempotency_level</span> <span class="o">=</span> <span class="n">NO_SIDE_EFFECTS</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">rpc</span> <span class="n">ReadWriteMethod</span><span class="p">(</span><span class="n">DummyReadWriteRequest</span><span class="p">)</span> <span class="k">returns</span> <span class="p">(</span><span class="n">DummyReadWriteResponse</span><span class="p">)</span> <span class="p">{</span>
<span class="k">option</span> <span class="na">idempotency_level</span> <span class="o">=</span> <span class="n">IDEMPOTENT</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>ただし、これらはあくまで注釈を行うことができるというだけで、これらのオプションの具体的な使い方は規定されていないようです。実際、 <a href="https://github.com/golang/protobuf">golang/protobuf</a> や <a href="https://github.com/grpc/grpc-go">grpc/grpc-go</a> のコードを見ても、これらのオプションについては型が定義されているのみで、実際になにかに使われているわけではありません。</p>
<h2 id="設定された-methodoption-を読む">設定された MethodOption を読む</h2>
<p>少なくとも grpc-go では、 <a href="https://pkg.go.dev/google.golang.org/protobuf@v1.27.1/reflect/protoreflect#FileDescriptor"><code class="language-plaintext highlighter-rouge">FileDescriptor</code></a> を通して <code class="language-plaintext highlighter-rouge">MethodOption</code> のようなメタデータを簡単に取得することができます。
例えば、上記の protobuf から生成された <code class="language-plaintext highlighter-rouge">.pb.go</code> のパッケージ名を <code class="language-plaintext highlighter-rouge">pb</code> として、サービス <code class="language-plaintext highlighter-rouge">Example</code> のメソッド <code class="language-plaintext highlighter-rouge">ReadOnlyMethod</code> のオプション <code class="language-plaintext highlighter-rouge">idempotency_level</code> を取得する方法は次のようになります:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="s">"google.golang.org/protobuf/types/descriptorpb"</span>
<span class="o">...</span>
<span class="n">fileDesc</span> <span class="o">:=</span> <span class="n">pb</span><span class="o">.</span><span class="n">File_api_api_proto</span> <span class="c">// 具体的な名前はファイル名やパッケージ名によって異なります</span>
<span class="n">methodDesc</span> <span class="o">:=</span> <span class="n">fileDesc</span><span class="o">.</span><span class="n">Services</span><span class="p">()</span><span class="o">.</span><span class="n">ByName</span><span class="p">(</span><span class="s">"Example"</span><span class="p">)</span><span class="o">.</span><span class="n">Methods</span><span class="p">()</span><span class="o">.</span><span class="n">ByName</span><span class="p">(</span><span class="s">"ReadOnlyMethod"</span><span class="p">)</span>
<span class="n">methodOpts</span> <span class="o">:=</span> <span class="n">methodDesc</span><span class="o">.</span><span class="n">Options</span><span class="p">()</span><span class="o">.</span><span class="p">(</span><span class="o">*</span><span class="n">descriptorpb</span><span class="o">.</span><span class="n">MethodOptions</span><span class="p">)</span>
<span class="c">// nil チェック等は省略しています</span>
<span class="k">switch</span> <span class="o">*</span><span class="n">methodOpts</span><span class="o">.</span><span class="n">IdempotencyLevel</span> <span class="p">{</span>
<span class="k">case</span> <span class="n">descriptorpb</span><span class="o">.</span><span class="n">MethodOptions_IDEMPOTENCY_UNKNOWN</span><span class="o">:</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"UNKNOWN"</span><span class="p">)</span>
<span class="k">case</span> <span class="n">descriptorpb</span><span class="o">.</span><span class="n">MethodOptions_NO_SIDE_EFFECTS</span><span class="o">:</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"NO_SIDE_EFFECTS"</span><span class="p">)</span>
<span class="k">case</span> <span class="n">descriptorpb</span><span class="o">.</span><span class="n">MethodOptions_IDEMPOTENT</span><span class="o">:</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"IDEMPOTENT"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>これを利用することで、 <code class="language-plaintext highlighter-rouge">IdempotencyLevel</code> が <code class="language-plaintext highlighter-rouge">NO_SIDE_EFFECTS</code> でないメソッドに対して冪等性キーを持たせることをテストで強制することができます。</p>
<h2 id="リクエストに冪等性キーが含まれることをテストする">リクエストに冪等性キーが含まれることをテストする</h2>
<p>先程のオプションと同様に、メソッドのリクエストに含まれるフィールドなどの情報についても <code class="language-plaintext highlighter-rouge">Descriptor</code> から取得することができます。
例えば、以下の例ではサービス <code class="language-plaintext highlighter-rouge">Example</code> のメソッド <code class="language-plaintext highlighter-rouge">ReadOnlyMethod</code> がリクエストに <code class="language-plaintext highlighter-rouge">idempotency_key</code> というフィールドを持っているかどうかを調べています:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fileDesc</span> <span class="o">:=</span> <span class="n">pb</span><span class="o">.</span><span class="n">File_api_api_proto</span>
<span class="n">methodDesc</span> <span class="o">:=</span> <span class="n">fileDesc</span><span class="o">.</span><span class="n">Services</span><span class="p">()</span><span class="o">.</span><span class="n">ByName</span><span class="p">(</span><span class="s">"Example"</span><span class="p">)</span><span class="o">.</span><span class="n">Methods</span><span class="p">()</span><span class="o">.</span><span class="n">ByName</span><span class="p">(</span><span class="s">"ReadOnlyMethod"</span><span class="p">)</span>
<span class="n">fieldDesc</span> <span class="o">:=</span> <span class="n">methodDesc</span><span class="o">.</span><span class="n">Input</span><span class="p">()</span><span class="o">.</span><span class="n">Fields</span><span class="p">()</span><span class="o">.</span><span class="n">ByName</span><span class="p">(</span><span class="s">"idempotency_key"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">fieldDesc</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"method %s has idempotency_key</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">methodDesc</span><span class="o">.</span><span class="n">Name</span><span class="p">())</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"method %s does not have idempotency_key</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">methodDesc</span><span class="o">.</span><span class="n">Name</span><span class="p">())</span>
<span class="p">}</span>
</code></pre></div></div>
<p>これら2つの例を組み合わせることで、最終的に <code class="language-plaintext highlighter-rouge">NO_SIDE_EFFECTS</code> でないメソッドが <code class="language-plaintext highlighter-rouge">idempotency_key</code> を持つことをテストによって強制することができます:</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">TestApi</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
<span class="n">fileDesc</span> <span class="o">:=</span> <span class="n">pb</span><span class="o">.</span><span class="n">File_api_api_proto</span>
<span class="c">// 今回は簡単のために Examples サービスのみを対象としています。</span>
<span class="n">methodDescs</span> <span class="o">:=</span> <span class="n">fileDesc</span><span class="o">.</span><span class="n">Services</span><span class="p">()</span><span class="o">.</span><span class="n">ByName</span><span class="p">(</span><span class="s">"Example"</span><span class="p">)</span><span class="o">.</span><span class="n">Methods</span><span class="p">()</span>
<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o"><</span> <span class="n">methodDescs</span><span class="o">.</span><span class="n">Len</span><span class="p">();</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
<span class="n">methodDesc</span> <span class="o">:=</span> <span class="n">methodDescs</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
<span class="n">opts</span> <span class="o">:=</span> <span class="n">methodDesc</span><span class="o">.</span><span class="n">Options</span><span class="p">()</span><span class="o">.</span><span class="p">(</span><span class="o">*</span><span class="n">descriptorpb</span><span class="o">.</span><span class="n">MethodOptions</span><span class="p">)</span>
<span class="k">var</span> <span class="n">hasSideEffects</span> <span class="kt">bool</span>
<span class="k">if</span> <span class="n">opts</span><span class="o">.</span><span class="n">IdempotencyLevel</span> <span class="o">==</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">hasSideEffects</span> <span class="o">=</span> <span class="no">true</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">hasSideEffects</span> <span class="o">=</span> <span class="o">*</span><span class="n">opts</span><span class="o">.</span><span class="n">IdempotencyLevel</span> <span class="o">!=</span> <span class="n">descriptorpb</span><span class="o">.</span><span class="n">MethodOptions_NO_SIDE_EFFECTS</span>
<span class="p">}</span>
<span class="k">if</span> <span class="n">hasSideEffects</span> <span class="p">{</span>
<span class="n">idempotencyKeyDesc</span> <span class="o">:=</span> <span class="n">methodDesc</span><span class="o">.</span><span class="n">Input</span><span class="p">()</span><span class="o">.</span><span class="n">Fields</span><span class="p">()</span><span class="o">.</span><span class="n">ByName</span><span class="p">(</span><span class="s">"idempotency_key"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">idempotencyKeyDesc</span> <span class="o">==</span> <span class="no">nil</span> <span class="p">{</span>
<span class="n">t</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"%s: non-readonly method must have idempotency_key"</span><span class="p">,</span> <span class="n">methodDesc</span><span class="o">.</span><span class="n">Name</span><span class="p">())</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>完全なコードの例は <a href="https://github.com/genkami/examples/tree/main/go/protobuf/ensure-idempotent/">こちら</a> に置いてあります。</p>gRPC に限らず、一般的に副作用のある API 呼び出しを行う際にはリクエストに一意な ID (いわゆる冪等性キー)を付け、同じ ID のリクエストが複数回飛んできた場合後続のものは無視することによって冪等性を保つ手法が取られることがあります。ISUCON10に参加しました!2020-09-12T00:00:00+00:002020-09-12T00:00:00+00:00https://genkami.github.io/2020/09/12/01-isucon10<h2 id="はじめに">はじめに</h2>
<p>ISUCON10に「イキリ社会人」というチームで参加しました。</p>
<p>結果は惜しくも予選突破ならずでしたが、今回の大会でやったことを時系列順に書き留めておこうと思います。</p>
<h2 id="メンバー">メンバー</h2>
<ul>
<li><a href="https://twitter.com/cloudear8">自分</a></li>
<li><a href="https://twitter.com/soiya1919">@soiya1919</a></li>
<li><a href="https://twitter.com/neglect_yp">@neglect_yp</a></li>
</ul>
<h2 id="やったこと">やったこと</h2>
<h3 id="716">7/16</h3>
<p><a href="https://twitter.com/soiya1919">@soiya1919</a> がチームの登録をしてくれたものの、自分だけ完全に参加登録を忘れていたことに気づく。</p>
<h3 id="81">8/1</h3>
<p>練習しないと…</p>
<h3 id="818">8/18</h3>
<p>意外とみんなの予定が合わず、練習日が決まらない</p>
<h3 id="831">8/31</h3>
<p>やっと練習日が決定。9/6にリモートで集まって過去問を解くことになる。</p>
<h3 id="96">9/6</h3>
<p>早朝に会社のサーバーがぶっ壊れ、緊急メンテ。早起きしたので終わったら爆睡してしまって結局何も練習できなかった。</p>
<p>※自分以外の2人はちゃんと練習していました。えらい!</p>
<h3 id="910">9/10</h3>
<p>そろそろ練習なりなんなりするぞ……という気分を高める</p>
<h3 id="912-0900">9/12 09:00</h3>
<p>気づいたら本番当日</p>
<h3 id="912-0940">9/12 09:40</h3>
<p>開始が遅くなるらしいことがわかり、二度寝</p>
<h3 id="912-1150">9/12 11:50</h3>
<p>起きて昼飯を食べ始める</p>
<h3 id="912-1220">9/12 12:20</h3>
<p>ISUCON10 予選開始。
予選マニュアルが公開されたので、みんなで眺め始める。</p>
<h3 id="912-1225">9/12 12:25</h3>
<p>予選マニュアルにわざわざbotは弾いてもいいと明記されていたので、 <a href="https://twitter.com/soiya1919">@soiya1919</a> がbotを弾くためのnginxの設定を書き始める。</p>
<h3 id="912-1230">9/12 12:30</h3>
<p>みんな一通りのサーバーにsshで入れるのを確認。</p>
<h3 id="912-1250">9/12 12:50</h3>
<p>今更になって後々使いそうな便利コマンドの一覧を作り始める。事前にやっとけ。</p>
<h3 id="912-1300">9/12 13:00</h3>
<p>用意していたISUCON10用のリポジトリにアプリケーションコードやら設定ファイルやらをぶちこむのがこのへんでだいたい完了する。</p>
<p><a href="https://twitter.com/soiya1919">@soiya1919</a> がNew Relicを入れる。</p>
<p>記念すべき第一回ベンチを回す。</p>
<p>この時点でのスコア: 447</p>
<h3 id="912-1330">9/12 13:30</h3>
<p>突然initializeが失敗するようになる。自分が調査した結果、なぜかdefault charsetがlatin1になっていたことが発覚。初期化用のsqlをいじって <code class="language-plaintext highlighter-rouge">CHARSET utf8mb4</code> とかを色々な箇所に入れる。
(実は自分がミスってこの時点で <code class="language-plaintext highlighter-rouge">my.cnf</code> が読み込まれなくなっていたのだが、この時点では3人ともそれを知るよしも無いのであった…)</p>
<h3 id="912-1350">9/12 13:50</h3>
<p>New Relicでフレームグラフを見る方法がよくわからなかった(そもそも機能があるのかもよく分からなかった)ので、自分がpprofも入れる。</p>
<h3 id="912-1400">9/12 14:00</h3>
<p><code class="language-plaintext highlighter-rouge">go tool pprof -seconds 120</code> とかやったら余裕でタイムアウトしたので、 <code class="language-plaintext highlighter-rouge">proxy_read_timeout 600</code> あたりを自分が追加。</p>
<h3 id="912-1430">9/12 14:30</h3>
<p><a href="https://twitter.com/soiya1919">@soiya1919</a> が書いたbotを弾くやつをデプロイしたが、この時点では効果を実感できなかった。とはいえ、わざわざマニュアルに書いてるということは入れておいて損はないだろうということで入れたままにしておく。</p>
<h3 id="912-1500">9/12 15:00</h3>
<p>自分がpt-query-digestとかを叩き始める。プロファイラとあわせてやっと最適化の準備が整ってくる。</p>
<h3 id="912-1540">9/12 15:40</h3>
<p><a href="https://twitter.com/neglect_yp">@neglect_yp</a> がデプロイスクリプトを用意する。</p>
<h3 id="912-1550">9/12 15:50</h3>
<p>せっかくサーバー3台あるしってことで、 <a href="https://twitter.com/soiya1919">@soiya1919</a> がリクエストを3台全部に振り分けるようにする。
MySQLでユーザー作ったり <code class="language-plaintext highlighter-rouge">GRANT ALL</code> したりとかの作業を普段あんまりしてないのでちょっと詰まるが、無事分散完了。
ただし、DBで詰まってたのでそんなにスコアは変わらなかったはず。</p>
<p>このあたりから運用方法が固まってきて、変更は一つずつPRにだし、マージしてデプロイ→ベンチマーカーを回してスコアをメモするという流れができる。</p>
<p>この時点でのスコア: 476</p>
<h3 id="912-1600">9/12 16:00</h3>
<p>initializeでよくコケるので、自分がinitializeでMySQLに食わせるスクリプトを実行するだけのやつを用意して確認を簡略化。</p>
<h3 id="912-1625">9/12 16:25</h3>
<p>なんか突然bashrcが壊れてデプロイスクリプトが動かなくなったので、自分が修正。</p>
<h3 id="912-1635">9/12 16:35</h3>
<p>とりあえずfilesort全部メモリ上にしてくれないかな、という話になり、<a href="https://twitter.com/soiya1919">@soiya1919</a> が試しに <code class="language-plaintext highlighter-rouge">sort_buffer_size</code> を上げてみる。しかし、 <code class="language-plaintext highlighter-rouge">my.cnf</code> は1時間前に自分がぶち壊してしまっていたため、なぜか設定が反映されずに苦戦する。</p>
<h3 id="912-1700">9/12 17:00</h3>
<p><a href="https://twitter.com/neglect_yp">@neglect_yp</a> が <code class="language-plaintext highlighter-rouge">SELECT * FROM chair WHERE stock != 0 ORDER BY price ASC, id ASC LIMIT ?</code> を速くするため、chairに <code class="language-plaintext highlighter-rouge">(price, id, stock)</code> のインデックスを貼って、スコアが少し上がる。</p>
<p>この時点でのスコア: 554</p>
<h3 id="912-1705">9/12 17:05</h3>
<p>ボトルネックになっていたNazotteを高速化するために、自分が <code class="language-plaintext highlighter-rouge">ST_Contains</code> をしていた部分を <a href="https://github.com/akavel/polyclip-go">polyclip</a> に置き換える。 N+1が消滅。</p>
<p>この時点でのスコア: 719</p>
<h3 id="912-1715">9/12 17:15</h3>
<p><a href="https://twitter.com/neglect_yp">@neglect_yp</a> が <code class="language-plaintext highlighter-rouge">SELECT * FROM estate ORDER BY rent ASC, id ASC LIMIT ?;</code> を速くするため、estateに <code class="language-plaintext highlighter-rouge">(rent, id)</code> のインデックスを貼る。</p>
<p>この時点でのスコア: 913</p>
<h3 id="912-1720">9/12 17:20</h3>
<p>chairとestateの <code class="language-plaintext highlighter-rouge">ORDER BY popularity DESC, id ASC</code> がうざかったので、自分が <code class="language-plaintext highlighter-rouge">UPDATE chair SET popularity = -popularity</code> して、人気度が低い椅子ほどいい椅子ということにしてしまう。これによって降順インデックスが無くても普通に <code class="language-plaintext highlighter-rouge">(popularity, id)</code> のインデックスが使えるようになる。</p>
<p>この時点でのスコア: 1312</p>
<h3 id="912-1735">9/12 17:35</h3>
<p>自分がestateに <code class="language-plaintext highlighter-rouge">(latitude, longitude)</code> のインデックスを貼る。</p>
<p>この時点でのスコア: 1565</p>
<h3 id="912-1805">9/12 18:05</h3>
<p>nginxがリクエストをファイルにバッファしまくっていたので <a href="https://twitter.com/soiya1919">@soiya1919</a> が <code class="language-plaintext highlighter-rouge">client_max_body_size</code> を上げてメモリ上に持たせてくれるように頑張る。</p>
<p>この頃から <code class="language-plaintext highlighter-rouge">POST /api/chair</code> や <code class="language-plaintext highlighter-rouge">POST /api/estate</code> がちょくちょくタイムアウトするようになる。ちなみにISUCON10のルールとしてはこれらがタイムアウトすると失格。</p>
<p>この時点でのスコア: 1565</p>
<h3 id="912-1835">9/12 18:35</h3>
<p><a href="https://twitter.com/neglect_yp">@neglect_yp</a> が、search系を多少でもマシにするためインデックスを貼りまくる。なんかよくわからないけどそこそこスコアが上がった。今回は更新系のクエリはそこまで厳しくないので、多く貼っておいてもそんなに困らない。</p>
<p>先ほどに引き続きPOST系がタイムアウトして失格になることがちょくちょくあるので、ベンチマークガチャが始まる。</p>
<p>この時点でのスコア: 2164</p>
<h3 id="912-1845">9/12 18:45</h3>
<p>自分がPOST系を全部非同期でやるように変更。しかし、ベンチマーカー的には変更は即時反映されていないといけないらしいので一瞬で怒られて0点になってしまった。</p>
<p>この時点でのスコア: 2164</p>
<h3 id="912-1940">9/12 19:40</h3>
<p>POST系を全部同期かつbulk insertでやるようにして、POST系タイムアウト問題を解決する。</p>
<p>sqlxのバージョンが絶妙に古く、 <code class="language-plaintext highlighter-rouge">db.NamedExec("INSERT INTO hoge (a, b) VALUES (:a, :b)", []hoge{a, b, c, ...})</code> みたいな形でのbulk insertが動かずにハマる。しかもコンパイルが通らないとかの類ではなくリフレクションをやりまくった結果どっかで <code class="language-plaintext highlighter-rouge">panic</code> するみたいな落ち方をするのでたちが悪い。結局sqlxのコード読んだりissue漁ったりしているうちに1時間弱ほどこの罠で溶けてしまった。</p>
<p>この時点でのスコア: 2164</p>
<h3 id="912-2000-2059">9/12 20:00-20:59</h3>
<p>インスタンスを再起動して必要なプロセスが立ち上がるのを確認したり、ベンチマークガチャをひたすら回し続けたりする。しかし、なぜか先ほどよりも点数が出ず、若干点が落ちる。</p>
<p>この時点でのスコア: 2088</p>
<h3 id="912-2100">9/12 21:00</h3>
<p>終了!!</p>
<h2 id="感想">感想</h2>
<p>中盤までは順調だったのですが、最後の2時間くらいでスコアを稼ぐことができず、最終的には追い抜かれて予選敗退となってしまいました。</p>
<p>悔しいですが、去年のISUCONと比べると成長を感じられたのでよかったです。</p>
<p>来年は本選出場します!!!</p>はじめに ISUCON10に「イキリ社会人」というチームで参加しました。BIT VALLEY 2020 プレイベントで登壇しました2020-08-05T00:00:00+00:002020-08-05T00:00:00+00:00https://genkami.github.io/2020/08/05/01-bit-valley-pre<p><a href="https://sbv.connpass.com/event/183549/">BIT VALLEY 2020 プレイベント#2 20代で圧倒的成長を目指すエンジニア~これが私のグロース戦略~</a> というイベントで、ガチャのリファクタリングについての話をしました。</p>
<p>スライドはこちらです:</p>
<script async="" class="speakerdeck-embed" data-id="6dabb4caf97c4084aebf793f67c16cfe" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>
<p>スライドには載せませんでしたが、新ロジックの設計や移行にあたっては以下の書籍の内容も参考にしました:</p>
<ul>
<li><a href="https://www.amazon.co.jp/dp/B07FSBHS2V/ref=cm_sw_em_r_mt_dp_L.MkFbXAP3Q3F">Clean Architecture 達人に学ぶソフトウェアの構造と設計</a></li>
<li><a href="https://www.amazon.co.jp/dp/B077D2L69C/ref=cm_sw_em_r_mt_dp_QaNkFb1GH5H7C">テスト駆動開発</a></li>
</ul>
<p>補足:</p>
<p>サービス内で大規模な再設計を行おうという試み自体今まであまり行われていなかったので、開発や移行のフローに関してはまだまだ改善の余地はあると思います。そのあたりに関しても、例えばデザインドキュメントを最初に公開して全社的に公開してみる等のような取り組みを行ってみたりしているので、今後はもしかするとそのへんの話についてなにか喋れるかもしれません。</p>BIT VALLEY 2020 プレイベント#2 20代で圧倒的成長を目指すエンジニア~これが私のグロース戦略~ というイベントで、ガチャのリファクタリングについての話をしました。キャッシュについての雑多な話2019-12-20T00:00:00+00:002019-12-20T00:00:00+00:00https://genkami.github.io/2019/12/20/01-cache-misc<p>この記事は、 <a href="https://qiita.com/advent-calendar/2019/mixi">ミクシィグループ Advent Calendar 2019</a> の20日目の記事です。</p>
<p>本当は全然別な話を書こうかと思っていたのですが、今朝同期が分報で↓の記事について言及していたのをみて触発され、急遽パフォーマンスに関する話をすることにしました。素敵な記事を書いてくださったリクルートの濱田さん、ありがとうございます!</p>
<p><a href="https://tech.recruit-mp.co.jp/server-side/post-19614/">Railsアプリの処理を100倍以上に高速化して得られた知見 – PSYENCE:MEDIA</a></p>
<hr />
<p>この記事では、「キャッシュをしないといけない」と思った時点で考えなければならないことを雑多にまとめました。だいたい上の項目ほど一般的な話で、下に行けばいくほど特定の状況におけるtipsみたいになってるはずです。</p>
<h2 id="そもそもなぜキャッシュしたいか">そもそもなぜキャッシュしたいか</h2>
<p>「負荷が高いからキャッシュをしよう」みたいな話で一口にまとめられてしまいがちですが、キャッシュはただの手段であり、それが解決しようとしている問題は様々です。
今自分が扱っているデータがどのような性質を持っていて、何が問題になっていて、それがどの程度にまで緩和されれば解決したとみなせるのかを意識することで、より効果的なキャッシュの方法を考えることができます。(もしくは、キャッシュ以外の選択肢が見つかるかもしれません。キャッシュ以外で解決できる問題ならばキャッシュを使わないに越したことは無いでしょう。)</p>
<h2 id="どこにキャッシュするか">どこにキャッシュするか</h2>
<p>これは何を解決したいかによって大きくかわってきます。
例えば、より高いレイテンシを求めているなら、キャッシュはキャッシュされる側より物理的に近い位置にいなければ効果は薄いでしょう。しかし、これもどの程度のレイテンシが必要なのかによって解決策は異なってきます。極端に高いレイテンシが必要な場合、L1キャッシュに乗り切るかどうかが重要になってくるかもしれません。逆に今問題となっているのがマイクロサービス間のレイテンシのような場合、呼び出し側のメモリ上やディスク上に結果をキャッシュすることで十分なレイテンシを得ることができるかもしれません。また、インターネットの向こうにあるサーバーとのレイテンシが問題になっている場合は、エッジロケーション程度の距離があっても十分満足のいくレイテンシを得られるかもしれません。</p>
<p>なお、下の方のtipsでは、基本的にはKey-Value型のキャッシュサーバーを用いてRDBのデータをキャッシュすることを想定していることが多いです。</p>
<h2 id="キャッシュを一箇所に置くか複数箇所に置くか">キャッシュを一箇所に置くか、複数箇所に置くか</h2>
<p>負荷の集中が問題になっている場合、多くの場合は同じデータを複数の場所に置く必要が発生します。しかし、同一のデータが複数の場所に置かれれば置かれるほど扱いが大変になってくるので、直面している問題を解決できる範囲でできる限りシンプルな方法を取るのがベストだと思います。</p>
<h2 id="キャッシュ上のデータとキャッシュされる側の整合性">キャッシュ上のデータとキャッシュされる側の整合性</h2>
<p>ここでいう整合性とは、キャッシュされている側のデータが新しい値に更新された場合、それと同じタイミングでキャッシュ上の古い値は無効化されているか、新しい値に更新されていることが保証されていることを言います。</p>
<p>多くの場合、キャッシュの整合性を完璧に取るのは非常に難しいか、不可能でしょう。キャッシュ上のデータが完璧に整合性が保たれていると信じるのは諦めて、整合性が保たれていなくてもいいようなコードを書くほうが楽なことが多いです。
楽観的ロックやCASを利用するとこれを比較的手軽に実現できますが、予めこれらを使うことを想定してそもそものデータ構造やキャッシュの手段を考えておく必要があります。これらの機能の利用を全く想定してしなかったところに後付けで入れるのは、場合によってはかなり大変になるかもしれません。</p>
<h2 id="キャッシュの原子性と独立性">キャッシュの原子性と独立性</h2>
<p>キャッシュされる側とキャッシュする側の間にまたがるトランザクションを張れるばあいはそれがベストですが、普通はそんなことはできません。
これが問題になってくる場合、キャッシュされる側の操作に対しても原子性や独立性が保たれていないか、もしくはコミットされていない変更を用いてキャッシュを更新していることが多いでしょう。このような状況は避けるべきです。</p>
<h2 id="キャッシュの期限をどれくらいに設定すべきか">キャッシュの期限をどれくらいに設定すべきか</h2>
<p>明示的に無効化できないようなキャッシュ機構の場合は、データの更新頻度に合わせて適切なttlを設定する必要があります。キャッシュされる側の負荷の許す限りで、できる限り短く設定したほうが万が一の場合にも対処しやすいかもしれません。</p>
<p>明示的に無効化できるようなキャッシュ機構でも、ほとんど使われないデータがいつまでもキャッシュに残ってしまうような状況は避けるべきです。LRUのような方法で使われていないキャッシュを掃除できるようにしておくか、それが無理であるなら適当なタイミングでキャッシュが無効化されるようにしておくとよいでしょう。</p>
<h2 id="キャッシュの更新による整合性の破壊">キャッシュの更新による整合性の破壊</h2>
<p>同一のキャッシュキーに対応する値を複数のプロセスが並行して行う場合、値の不整合が生じる可能性があります。
トランザクションが使えるのであれば、書き込みが必要なケースではキャッシュを見ないようにし、値の取得と更新はトランザクション内部で行えばよいでしょう。</p>
<p>キャッシュの値を使って値を更新するような場合は、CASや最低でも楽観的ロックあたりができないと悲惨なことになるかもしれません。</p>
<h2 id="キャッシュの無効化による整合性の破壊">キャッシュの無効化による整合性の破壊</h2>
<p>キャッシュの更新で整合性が保てなくなるんなら更新じゃなくて無効化だけすればいいじゃないかと思うかも知れませんが、無効化しかしない場合でも不整合は起こり得ます。</p>
<p>キャッシュされる側のデータが更新される直前にキャッシュが切れると、キャッシュ上には古いデータが残ってしまう場合があります。</p>
<p>例えばキャッシュキーkに対応する値を2つのプロセスP, Qが取得・更新しようとしている場合、以下のような問題が発生します:</p>
<ol>
<li>Pがkに対応する値を取得する(このときの値をXとする)</li>
<li>kのキャッシュが(ttlを過ぎるか、この2つ以外の他のプロセス等によって)無効化される</li>
<li>Qがkに対応する値Xを取得する(キャッシュミス)</li>
<li>Pがkに対応する値をX’に更新する(このときkに対応するキャッシュがあればexpireする)</li>
<li>QはキャッシュミスしたのでXをキャッシュに積んでおく</li>
</ol>
<p>この手の問題はそれ自体を解決するよりも、そもそも古いデータがキャッシュに載っていることを考慮してCASなりするほうが楽なことが多いです。</p>
<h2 id="書き込みに対して読み込みの頻度が極端に高いデータ">書き込みに対して読み込みの頻度が極端に高いデータ</h2>
<p>楽観的ロックやCASのような仕組みでは、基本的に書き込み時にしかキャッシュが古くなっていることを検知できません。ほとんど読み取りにしか使われないようなデータの場合は、適切にTTLを設定するか、書き込みのタイミングで確実にキャッシュが無効化できるようにしておくとよいでしょう。</p>
<p>書き込みの頻度が十分に低いのであれば、書き込み用のモードの場合だけキャッシュを経由せずにデータを取りに行くようにすることによって、CAS等を使わずに書き込み時の不整合を防ぐことができるかもしれません。</p>
<h2 id="キャッシュのn1問題">キャッシュのN+1問題</h2>
<p>キャッシュのレイテンシが相対的に高い場合、これが問題になってくることがあります。</p>
<p>この問題は、キャッシュの構造を工夫することによって解決できるかもしれません。</p>
<p>複数のキーに対応する値を一括で取ってくることができるようなキャッシュ機構であれば、工夫すれば定数回のキャッシュとのやりとりだけで済ませることができます。</p>
<p>例えば特定条件で検索した結果の一覧をキャッシュしたい場合、「検索条件→検索結果のidのリスト」と「id→レコードの値」の二種類のキャッシュを持っておけば、高い空間効率でキャッシュを行うことができます。</p>
<h2 id="キャッシュの保存先の変更">キャッシュの保存先の変更</h2>
<p>キャッシュサーバーを変える、キャッシュサーバーの台数を増減させる、キャッシュキーを変える、もしくはそもそもキャッシュ機構そのものを入れ替えるといった場合、気をつけないと不整合が発生してしまいます。</p>
<p>キーkに対応するキャッシュの保存先がAからA’に変わる場合、変更の途中で以下のようなことが発生する可能性があります:</p>
<ol>
<li>プロセスPに変更が反映される</li>
<li>Pはkの値を書き換え、同時にA’にあるkの値を無効化する</li>
<li>まだ変更が反映されていないプロセスQが、kに対応する値を取得しようとする</li>
<li>このとき、QはAを見に行くため、無効化されていない古い値が帰ってくる</li>
</ol>
<p>あらゆるプロセスに対して変更をを完全に同時に反映させるのは基本的に無理なので、ほとんどの場合はこの問題に対して何かしらの対処をする必要があります。
これは例えば、次のような方法で解決することができます</p>
<ol>
<li>先にA’はキャッシュの書き込み・無効化だけ行えるようにしておく</li>
<li>キャッシュにアクセスする全プロセスがA, A’の存在を認知するようになる</li>
<li>A’からキャッシュを読み取り、Aは書き込みと無効化だけするようにする</li>
<li>全プロセスがAからの読み取りをやめたら、Aへは参照しないようにする</li>
</ol>
<h2 id="ネガティブキャッシュ">ネガティブキャッシュ</h2>
<p>ネガティブキャッシュとは、「無い」ことをキャッシュすることです。</p>
<p>あるキャッシュキーに対するキャッシュが存在せず、キャッシュされる側にも対応する値がなかった場合、「そのような値は存在しない」という事実をキャッシュしておくのは、キャッシュされる側の負荷が問題になっている場合は有益であることが多いです。</p>
<h2 id="キャッシュサーバーの障害">キャッシュサーバーの障害</h2>
<p>キャッシュサーバーに対して障害が発生したときに、キャッシュされている側への負荷の影響を最小限にする必要があります。複数台のキャッシュサーバーに対して分散してキャッシュを保存している場合、分散のアルゴリズムが悪ければ一台のサーバーの障害だけですべてのキャッシュが無効になってしまう可能性もあります。</p>
<p>また、キャッシュサーバーの台数を十分多くしておくことで、一台の故障に対する影響を最小限に留めることができるかもしれません。</p>
<h2 id="極端な時間的局所性があるデータ">極端な時間的局所性があるデータ</h2>
<p>サービスによっては、ある時刻までは全く使われないが、ある時刻になった瞬間からかなり頻繁に参照されるようになるデータがあるかもしれません。このようはデータはキャッシュの観点から見ると厄介です。ある時刻になるまでは全く使われないためキャッシュにも乗らず、それが必要な時刻になった瞬間に一気にキャッシュされる側への負荷が跳ね上がってしまう可能性ががあります。</p>
<p>どのような値がいつから使われるのか予め分かっているのであれば、前もってキャッシュにデータを積んでおくことで負荷を軽減できるかもしれません。</p>
<h2 id="おわりに">おわりに</h2>
<p>全くまとまりの無い記事でしたが、より高いパフォーマンスが求められるようなサービスを作る際の参考になれば幸いです。</p>
<p>明日は弊サービスのスーパー新卒エンジニア、ずーみんの担当です。何を書くつもりなのかは全く知りませんが、たぶん面白いことを書いてくれると思うので乞うご期待!!</p>この記事は、 ミクシィグループ Advent Calendar 2019 の20日目の記事です。社内LTで紹介したリアルタイムpt-query-digestの話を共有します2019-11-26T00:00:00+00:002019-11-26T00:00:00+00:00https://genkami.github.io/2019/11/26/01-fluent-plugin-query-fingerprint<p><a href="https://github.com/genkami/fluent-plugin-query-fingerprint">fluent-plugin-query-fingerprint</a> というリアルタイムpt-query-digestをやるためのFluentd Pluginを社内LTで紹介したときの資料を発掘したので、こちらにも公開します。</p>
<script async="" class="speakerdeck-embed" data-id="e946e3b5c8164c7bbaee2fc6670490cd" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>fluent-plugin-query-fingerprint というリアルタイムpt-query-digestをやるためのFluentd Pluginを社内LTで紹介したときの資料を発掘したので、こちらにも公開します。よく忘れるinclude, prepend, extend2019-07-07T00:00:00+00:002019-07-07T00:00:00+00:00https://genkami.github.io/2019/07/07/01-include-extend-prepend<p>Rubyの <code class="language-plaintext highlighter-rouge">include</code>, <code class="language-plaintext highlighter-rouge">prepend</code>, <code class="language-plaintext highlighter-rouge">extend</code> のどれがどれだかすぐに忘れるのでまとめました。</p>
<h2 id="include">include</h2>
<p><code class="language-plaintext highlighter-rouge">Module#include</code> は、自身の継承チェーンの「自身より後、自身のスーパークラスより前」に指定したモジュールを挿入するメソッドです。</p>
<p>結果として、 <code class="language-plaintext highlighter-rouge">include</code> したモジュールに定義されているメソッドをそのままインスタンスメソッドとして取り込むことができます。</p>
<p>モジュールを継承?と思うかもしれませんが、Rubyの場合クラスはただのインスタンス化できるモジュールであることを思い出してください。</p>
<p>例:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Parent</span>
<span class="k">def</span> <span class="nf">hello</span>
<span class="nb">puts</span> <span class="s2">"Parent#hello"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">TheClass</span> <span class="o"><</span> <span class="no">Parent</span>
<span class="k">def</span> <span class="nf">hello</span>
<span class="nb">puts</span> <span class="s2">"TheClass#hello"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">module</span> <span class="nn">Included</span>
<span class="k">def</span> <span class="nf">hello</span>
<span class="nb">puts</span> <span class="s2">"Included#hello"</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">goodbye</span>
<span class="nb">puts</span> <span class="s2">"Included#goodbye"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">TheClass</span><span class="p">.</span><span class="nf">include</span> <span class="no">Included</span>
<span class="nb">puts</span> <span class="no">TheClass</span><span class="p">.</span><span class="nf">ancestors</span><span class="p">.</span><span class="nf">to_s</span> <span class="c1">#=> [TheClass, Included, Parent, Object, Kernel, BasicObject]</span>
<span class="n">instance</span> <span class="o">=</span> <span class="no">TheClass</span><span class="p">.</span><span class="nf">new</span>
<span class="n">instance</span><span class="p">.</span><span class="nf">hello</span> <span class="c1">#=> TheClass#hello</span>
<span class="n">instance</span><span class="p">.</span><span class="nf">goodbye</span> <span class="c1">#=> Included#goodbye</span>
</code></pre></div></div>
<h2 id="prepend">prepend</h2>
<p><code class="language-plaintext highlighter-rouge">Module#prepend</code> は、自身の継承チェーンの「自身より前」に指定したモジュールを挿入するメソッドです。</p>
<p>結果として、元々のクラスが持っているインスタンスメソッドを <code class="language-plaintext highlighter-rouge">prepend</code> したモジュールのメソッドで上書きすることができます。</p>
<p>例:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Parent</span>
<span class="k">def</span> <span class="nf">hello</span>
<span class="nb">puts</span> <span class="s2">"Parent#hello"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">TheClass</span> <span class="o"><</span> <span class="no">Parent</span>
<span class="k">def</span> <span class="nf">hello</span>
<span class="nb">puts</span> <span class="s2">"TheClass#hello"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">module</span> <span class="nn">Included</span>
<span class="k">def</span> <span class="nf">hello</span>
<span class="nb">puts</span> <span class="s2">"Included#hello"</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">goodbye</span>
<span class="nb">puts</span> <span class="s2">"Included#goodbye"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">module</span> <span class="nn">Prepended</span>
<span class="k">def</span> <span class="nf">hello</span>
<span class="nb">puts</span> <span class="s2">"Prepended#hello"</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">goodbye</span>
<span class="nb">puts</span> <span class="s2">"Prepended#goodbye"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">TheClass</span><span class="p">.</span><span class="nf">include</span> <span class="no">Included</span>
<span class="no">TheClass</span><span class="p">.</span><span class="nf">prepend</span> <span class="no">Prepended</span>
<span class="nb">puts</span> <span class="no">TheClass</span><span class="p">.</span><span class="nf">ancestors</span><span class="p">.</span><span class="nf">to_s</span> <span class="c1">#=> [Prepended, TheClass, Included, Parent, Object, Kernel, BasicObject]</span>
<span class="n">instance</span> <span class="o">=</span> <span class="no">TheClass</span><span class="p">.</span><span class="nf">new</span>
<span class="n">instance</span><span class="p">.</span><span class="nf">hello</span> <span class="c1">#=> Prepended#hello</span>
<span class="n">instance</span><span class="p">.</span><span class="nf">goodbye</span> <span class="c1">#=> Prepended#goodbye</span>
</code></pre></div></div>
<h2 id="extend">extend</h2>
<p><code class="language-plaintext highlighter-rouge">Object#extend</code> は、そのオブジェクトの特異クラスに対する <code class="language-plaintext highlighter-rouge">include</code> です。</p>
<p>結果として、特定のインスタンスだけにメソッドを追加したり、クラスメソッドを追加するようなことができます。</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Parent</span>
<span class="k">def</span> <span class="nf">hello</span>
<span class="nb">puts</span> <span class="s2">"Parent#hello"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">TheClass</span> <span class="o"><</span> <span class="no">Parent</span>
<span class="k">def</span> <span class="nf">hello</span>
<span class="nb">puts</span> <span class="s2">"TheClass#hello"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">module</span> <span class="nn">Included</span>
<span class="k">def</span> <span class="nf">hello</span>
<span class="nb">puts</span> <span class="s2">"Included#hello"</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">goodbye</span>
<span class="nb">puts</span> <span class="s2">"Included#goodbye"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">module</span> <span class="nn">Prepended</span>
<span class="k">def</span> <span class="nf">hello</span>
<span class="nb">puts</span> <span class="s2">"Prepended#hello"</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">goodbye</span>
<span class="nb">puts</span> <span class="s2">"Prepended#goodbye"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">module</span> <span class="nn">Extended</span>
<span class="k">def</span> <span class="nf">hello</span>
<span class="nb">puts</span> <span class="s2">"Extended#hello"</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">goodbye</span>
<span class="nb">puts</span> <span class="s2">"Extended#goodbye"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">TheClass</span><span class="p">.</span><span class="nf">include</span> <span class="no">Included</span>
<span class="no">TheClass</span><span class="p">.</span><span class="nf">prepend</span> <span class="no">Prepended</span>
<span class="no">TheClass</span><span class="p">.</span><span class="nf">extend</span> <span class="no">Extended</span>
<span class="nb">puts</span> <span class="no">TheClass</span><span class="p">.</span><span class="nf">ancestors</span><span class="p">.</span><span class="nf">to_s</span> <span class="c1">#=> [Prepended, TheClass, Included, Parent, Object, Kernel, BasicObject]</span>
<span class="c1"># 注意: #<Class:hoge> となっているのは hoge の特異クラス</span>
<span class="nb">puts</span> <span class="no">TheClass</span><span class="p">.</span><span class="nf">singleton_class</span><span class="p">.</span><span class="nf">ancestors</span><span class="p">.</span><span class="nf">to_s</span> <span class="c1">#=> [#<Class:TheClass>, Extended, #<Class:Parent>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]</span>
<span class="n">instance</span> <span class="o">=</span> <span class="no">TheClass</span><span class="p">.</span><span class="nf">new</span>
<span class="n">instance</span><span class="p">.</span><span class="nf">hello</span> <span class="c1">#=> Prepended#hello</span>
<span class="n">instance</span><span class="p">.</span><span class="nf">goodbye</span> <span class="c1">#=> Prepended#goodbye</span>
<span class="no">TheClass</span><span class="p">.</span><span class="nf">hello</span> <span class="c1">#=> Extended#hello</span>
<span class="no">TheClass</span><span class="p">.</span><span class="nf">goodbye</span> <span class="c1">#=> Extended#goodbye</span>
<span class="n">instance</span><span class="p">.</span><span class="nf">extend</span> <span class="no">Extended</span>
<span class="nb">puts</span> <span class="n">instance</span><span class="p">.</span><span class="nf">singleton_class</span><span class="p">.</span><span class="nf">ancestors</span><span class="p">.</span><span class="nf">to_s</span> <span class="c1">#=> [#<Class:#<TheClass:0x00007ff45d039888>>, Extended, Prepended, TheClass, Included, Parent, Object, Kernel, BasicObject]</span>
<span class="n">instance</span><span class="p">.</span><span class="nf">hello</span> <span class="c1">#=> Extended#hello</span>
<span class="n">instance</span><span class="p">.</span><span class="nf">goodbye</span> <span class="c1">#=> Extended#goodbye</span>
</code></pre></div></div>
<p>注意すべき点は、 <code class="language-plaintext highlighter-rouge">instance.extend Extended</code> は <code class="language-plaintext highlighter-rouge">TheClass</code> に対する動作ではなく <code class="language-plaintext highlighter-rouge">instance</code> の特異クラスに対する動作なので、 <code class="language-plaintext highlighter-rouge">Extended</code> が挿入される位置は「<code class="language-plaintext highlighter-rouge">TheClass</code> の後、 <code class="language-plaintext highlighter-rouge">Parent</code> の前」ではなく、「<code class="language-plaintext highlighter-rouge">instance</code> の特異クラスの後、 <code class="language-plaintext highlighter-rouge">TheClass</code> の前」になります。</p>Rubyの include, prepend, extend のどれがどれだかすぐに忘れるのでまとめました。defでメソッドが定義される対象2019-07-07T00:00:00+00:002019-07-07T00:00:00+00:00https://genkami.github.io/2019/07/07/02-def-and-ruby-class<p><code class="language-plaintext highlighter-rouge">def</code> キーワードを使うとメソッドを定義できますが、実は文脈によって挙動が結構違います。</p>
<h2 id="特異メソッド">特異メソッド</h2>
<p>誰にメソッドが追加されるかを明示している特異メソッド定義の場合は挙動がわかりやすいです。この場合は、単純に対象のオブジェクトの特異クラスにメソッドが定義されます。</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">hoge</span> <span class="o">=</span> <span class="s2">"hoge"</span>
<span class="k">def</span> <span class="nc">hoge</span><span class="o">.</span><span class="nf">puts_self</span>
<span class="nb">puts</span> <span class="nb">self</span>
<span class="k">end</span>
<span class="n">hoge</span><span class="p">.</span><span class="nf">puts_self</span> <span class="c1">#=> hoge</span>
</code></pre></div></div>
<h2 id="トップレベル">トップレベル</h2>
<p>トップレベルでの <code class="language-plaintext highlighter-rouge">def</code> では、 <code class="language-plaintext highlighter-rouge">Object</code> のプライベートメソッドとして定義されます。</p>
<p>また、トップレベルでは <code class="language-plaintext highlighter-rouge">self</code> が <code class="language-plaintext highlighter-rouge">main</code> オブジェクトという特別な <code class="language-plaintext highlighter-rouge">Object</code> のインスタンスになるので、定義したメソッドをそのまま関数のように呼ぶことができます。</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">puts_self</span>
<span class="nb">puts</span> <span class="nb">self</span>
<span class="k">end</span>
<span class="n">puts_self</span> <span class="c1">#=> main</span>
<span class="no">Object</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="ss">:puts_self</span><span class="p">)</span> <span class="c1">#=> #<Object:0x00007fc336858d00></span>
</code></pre></div></div>
<h2 id="クラス定義の内部">クラス定義の内部</h2>
<p>クラス定義の内部では、現在定義しようとしているクラスにメソッドが定義されます。</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">TheClass</span>
<span class="k">def</span> <span class="nf">puts_self</span>
<span class="nb">puts</span> <span class="nb">self</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">TheClass</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">puts_self</span> <span class="c1">#=> #<TheClass:0x00007ff2d8868440></span>
</code></pre></div></div>
<h2 id="class_eval">class_eval</h2>
<p><code class="language-plaintext highlighter-rouge">class_eval</code> では、クラス定義の内部と同じように、レシーバのクラスにメソッドが定義されます。</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AnotherClass</span>
<span class="k">end</span>
<span class="no">AnotherClass</span><span class="p">.</span><span class="nf">class_eval</span> <span class="k">do</span>
<span class="nb">puts</span> <span class="nb">self</span> <span class="c1">#=> AnotherClass</span>
<span class="k">def</span> <span class="nf">puts_self</span>
<span class="nb">puts</span> <span class="nb">self</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">AnotherClass</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">puts_self</span> <span class="c1">#=> #<AnotherClass:0x00007fb538113740></span>
</code></pre></div></div>
<h2 id="instance_eval">instance_eval</h2>
<p><code class="language-plaintext highlighter-rouge">instance_eval</code> は <code class="language-plaintext highlighter-rouge">class_eval</code> と違い、レシーバの特異クラスに対してメソッドが定義されます。</p>
<p>注意点として、 <code class="language-plaintext highlighter-rouge">class_eval</code> と <code class="language-plaintext highlighter-rouge">instance_eval</code> ではともにそのコンテキストでの <code class="language-plaintext highlighter-rouge">self</code> はレシーバ自身になります。</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">AnotherClass</span><span class="p">.</span><span class="nf">instance_eval</span> <span class="k">do</span>
<span class="nb">puts</span> <span class="nb">self</span> <span class="c1">#=> AnotherClass</span>
<span class="k">def</span> <span class="nf">puts_self</span>
<span class="nb">puts</span> <span class="nb">self</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">AnotherClass</span><span class="p">.</span><span class="nf">puts_self</span> <span class="c1">#=> AnotherClass</span>
</code></pre></div></div>def キーワードを使うとメソッドを定義できますが、実は文脈によって挙動が結構違います。git commitを使わずにコミットを作ってみる2019-07-07T00:00:00+00:002019-07-07T00:00:00+00:00https://genkami.github.io/2019/07/07/03-making-git-commit<p><code class="language-plaintext highlighter-rouge">git commit</code> や <code class="language-plaintext highlighter-rouge">git commit-tree</code> を使わずにコミットを作ってみます。</p>
<h2 id="完成後のイメージ">完成後のイメージ</h2>
<p>こんな感じのコミット履歴になれるようにコミットオブジェクトを手作りしていきます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ pwd
/path/to/my-first-repository
$ ls
hello.txt
$ cat hello.txt
HELLO WORLD
$ git log --oneline
XXXXXXX (HEAD -> master) fix typo (s/HALLO/HELLO/)
YYYYYYY add hello.txt
</code></pre></div></div>
<p>最初のコミットで <code class="language-plaintext highlighter-rouge">hello.txt</code> というファイルを作り、次のコミットでそのtypoを直していくという想定になっています。</p>
<h2 id="初期化">初期化</h2>
<p>コミットを手作りする前に、先に <code class="language-plaintext highlighter-rouge">git init</code> だけやっておきます。</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">mkdir </span>my-first-repository
<span class="nv">$ </span><span class="nb">cd </span>my-first-repository
<span class="nv">$ </span>git init
Initialized empty Git repository <span class="k">in</span> /path/to/my-first-repository/.git/
</code></pre></div></div>
<h2 id="blobを作る">blobを作る</h2>
<p>まずはhello.txtの最初の中身となるblobを作っていきます。</p>
<p>Gitは内部的にはSHA1をキーとするKVSみたいなものを持っており、すべてのファイルの中身はこのKVSのようなものにblobオブジェクトという種類で登録されます。</p>
<p>オブジェクトを作るには、 <a href="https://git-scm.com/docs/git-hash-object">git hash-object</a>というコマンドを使います。</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">echo</span> <span class="s1">'HALLO WORLD'</span> | git hash-object <span class="nt">-w</span> <span class="nt">--stdin</span>
a479c095720051370f567216bb5d89b300edc3cd
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">a479c095720051370f567216bb5d89b300edc3cd</code> がこのオブジェクトのSHA1です。</p>
<h2 id="treeを作る">treeを作る</h2>
<p>ファイルの中身ができたら、次はファイルを収めるディレクトリに対応するオブジェクトを作ります。</p>
<p>ディレクトリはGitではtreeオブジェクトという種類のオブジェクトとして表されます。</p>
<p>treeオブジェクトは、各行に <code class="language-plaintext highlighter-rouge">permission type SHA1\tpath</code> という形でファイルのメタデータが並んだ形式で表現されます。</p>
<p>また、treeオブジェクトは <code class="language-plaintext highlighter-rouge">git hash-object -t tree</code> で作ることができます。</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>hex2oct <span class="o">()</span> <span class="o">{</span>
perl <span class="nt">-ne</span> <span class="s1">'printf "\\%03o", hex for /../g'</span>
<span class="o">}</span>
<span class="nv">$ </span><span class="nb">printf</span> <span class="s2">"100644 hello.txt</span><span class="se">\0</span><span class="si">$(</span><span class="nb">echo </span>a479c095720051370f567216bb5d89b300edc3cd | hex2oct<span class="si">)</span><span class="s2">"</span> | git hash-object <span class="nt">-t</span> tree <span class="nt">-w</span> <span class="nt">--stdin</span>
cc5eabd265414052d23c2a355c7fbb9ecd7d2203
</code></pre></div></div>
<p>ここではSHA1のバイナリ表現を簡単に生成するために、hex2octというコマンドを<a href="https://github.com/git/git/blob/6a6c0f10a70a6eb101c213b09ae82a9cad252743/t/test-lib-functions.sh#L1244">Gitのspec</a>より引用しています。</p>
<h2 id="commitを作る">commitを作る</h2>
<p>ファイルとそれを収めるディレクトリができたら、最後にコミットを作成します。コミットオブジェクトは以下のような形式になっています。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>parent (親コミットのSHA1、ルートのコミットの場合は無し)
tree (コミットのルートディレクトリのtreeオブジェクトのSHA1)
author (authorの名前 <email> タイムスタンプ)
committer (committerの名前 <email> タイムスタンプ)
コミットメッセージ
</code></pre></div></div>
<p>また、コミットおぶは <code class="language-plaintext highlighter-rouge">git hash-object -t commit</code> で作ることができます。</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"tree cc5eabd265414052d23c2a355c7fbb9ecd7d2203
author Hoge Taro <hogefuga@example.com> 1557821403 +0900
committer Hoge Taro <hogefuga@example.com> 1557821403 +0900
add hello.txt
"</span> | git hash-object <span class="nt">-t</span> commit <span class="nt">-w</span> <span class="nt">--stdin</span>
0d9a4e89e7a9cda92bee74c8d2055006ab8c39dc
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">tree</code> で指定されるtreeの中身を内容として持つコミットオブジェクトを作成しました。
ちなみに <code class="language-plaintext highlighter-rouge">1557821403</code> はこれを書いている時点での現在時刻です。</p>
<h2 id="子コミットを作る">子コミットを作る</h2>
<p>次に、今作ったコミット <code class="language-plaintext highlighter-rouge">0d9a4</code> を親として持つコミットを作ります。
<code class="language-plaintext highlighter-rouge">0d9a4</code> は <code class="language-plaintext highlighter-rouge">hello.txt</code> の中身を <code class="language-plaintext highlighter-rouge">HALLO WORLD</code> とtypoしてるので、これを <code class="language-plaintext highlighter-rouge">HELLO WORLD</code> に修正したコミットを作ります。</p>
<p>まずは先程と同様にblobから</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">echo</span> <span class="s1">'HELLO WORLD'</span> | git hash-object <span class="nt">-w</span> <span class="nt">--stdin</span>
4e3dffe834ac70600a7cb71fbc1f6a694c9d041f
</code></pre></div></div>
<p>次にtree</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">printf</span> <span class="s2">"100644 hello.txt</span><span class="se">\0</span><span class="si">$(</span><span class="nb">echo </span>4e3dffe834ac70600a7cb71fbc1f6a694c9d041f | hex2oct<span class="si">)</span><span class="s2">"</span> | git hash-object <span class="nt">-t</span> tree <span class="nt">-w</span> <span class="nt">--stdin</span>
77808726e703c5f4d7394d735ad02032e2f43202
</code></pre></div></div>
<p>最後にcommit。今回は <code class="language-plaintext highlighter-rouge">0d9a4</code> に対する修正という設定なので、この <code class="language-plaintext highlighter-rouge">0d9a4</code> を親commitとして指定します。</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"tree 77808726e703c5f4d7394d735ad02032e2f43202
parent 0d9a4e89e7a9cda92bee74c8d2055006ab8c39dc
author Hoge Taro <hogefuga@example.com> 1557821600 +0900
committer Hoge Taro <hogefuga@example.com> 1557821600 +0900
fix typo (s/HALLO/HELLO/)
"</span> | git hash-object <span class="nt">-t</span> commit <span class="nt">-w</span> <span class="nt">--stdin</span>
39cc518115462d6b8ea5e2ba30e92890170de705
</code></pre></div></div>
<h2 id="完成">完成</h2>
<p>最後にmasterの位置を今できた <code class="language-plaintext highlighter-rouge">39cc51</code> に移動させれば完成です。</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git reset <span class="nt">--hard</span> 39cc518115462d6b8ea5e2ba30e92890170de705
HEAD is now at 39cc518 fix typo <span class="o">(</span>s/HALLO/HELLO/<span class="o">)</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">git log</code> を見てみるとちゃんとコミットが作成されていることがわかります。</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git log <span class="nt">-p</span>
commit 39cc518115462d6b8ea5e2ba30e92890170de705 <span class="o">(</span>HEAD -> master<span class="o">)</span>
Author: Hoge Taro <hogefuga@example.com>
Date: Tue May 14 17:13:20 2019 +0900
fix typo <span class="o">(</span>s/HALLO/HELLO/<span class="o">)</span>
diff <span class="nt">--git</span> a/hello.txt b/hello.txt
index a479c09..4e3dffe 100644
<span class="nt">---</span> a/hello.txt
+++ b/hello.txt
@@ <span class="nt">-1</span> +1 @@
<span class="nt">-HALLO</span> WORLD
+HELLO WORLD
commit 0d9a4e89e7a9cda92bee74c8d2055006ab8c39dc
Author: Hoge Taro <hogefuga@example.com>
Date: Tue May 14 17:10:03 2019 +0900
add hello.txt
diff <span class="nt">--git</span> a/hello.txt b/hello.txt
new file mode 100644
index 0000000..a479c09
<span class="nt">---</span> /dev/null
+++ b/hello.txt
@@ <span class="nt">-0</span>,0 +1 @@
+HALLO WORLD
</code></pre></div></div>git commit や git commit-tree を使わずにコミットを作ってみます。Battle Conference Under30に登壇し(て)ました2019-07-06T00:00:00+00:002019-07-06T00:00:00+00:00https://genkami.github.io/2019/07/06/01-battle-conference<p>去年のやつです…(この記事を投稿したのは2020年夏)。</p>
<p>ブログに書こうと思ってたはずが永遠に下書きになってたので、スライドだけ貼っつけておきます。</p>
<p>特に僕が何かした話ではないんですが、実はモンストってTURNめちゃくちゃ使ってるんだよっていう話をしたくなったので話してみた、という感じの経緯だったはずです。</p>
<script async="" class="speakerdeck-embed" data-id="5bd78c062319483e966714823cfd7da6" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>去年のやつです…(この記事を投稿したのは2020年夏)。