oinume journal

Scratchpad of what I learned

OpenTelemetryとhttptrace.ClientTraceを使ってHTTPリクエストのlatencyを可視化する

この記事は OpenCensusとhttptrace.ClientTraceを使ってHTTPリクエストのlatencyを可視化する - oinume journal のOpenTelemetry版。OpenTelemetryについては OpenTelemetryとは何か、そしてなぜそれが計装器の未来なのか? | New Relic を見てもらうのが手っ取り早くて、httptrace.ClientTraceについては先のブログを見てもらえればと。

OpenTelemetry Tracing + httptrace.ClientTrace

早速本題に入ってしまうと、以下のようなコードを書くことでhttp.ClientでHTTPリクエストを送った際のlatencyを可視化することが可能になる。

以下のように otelhttptrace.NewClientTraceで生成したClientTraceをctxにセットし、そのctxをhttp.Requestにセットするだけで良い。

clientTrace := otelhttptrace.NewClientTrace(ctx)
ctx = httptrace.WithClientTrace(ctx, clientTrace)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
...

Jaeger UIでの確認

https://github.com/oinume/go-http-client-trace-sample/ のリポジトリをcloneしてdocker-compose up -dするとJaegerが立ち上げることができる。

$ git clone https://github.com/oinume/go-http-client-trace-sample
$ docker-compose up -d
$ go run ./examples/otelhttp_client_trace/main.go

のように、Jaegerを立ち上げてサンプルコードを実行するとJaegerのexporterにトレースの情報が送られる。そのトレースの情報は http://localhost:16686/search にブラウザでアクセスすると見ることができる。以下のスクショを見てもらうとわかるように、http.getconnhttp.dnshttp.tlsなど、httptrace.ClientTraceのコールバックメソッドごとにSpanが作成され、それがどのぐらい時間がかかっているのかがわかるようになっている。

OpenCensusとOpenTelemetryの違い

httptrace.ClientTraceに関わるところで、OpenCensusとOpenTelemetryで微妙に挙動が違うところがあったので、メモしておく。

OpenCensusの場合はデフォルトではhttp.getconnhttp.dnsなどの情報がSpanにならないが、OpenTelemetryの場合はそれぞれSpanになっている。この挙動で注意した方が良いのは、例えばGCPのCloud Traceだとスパン単位で従量課金されるので、場合によっては金額が大きくなってしまうかもしれない。しかし、以下のようにWithoutSubSpansを指定することでSpanを生成しないようにすることができる。

clientTrace := otelhttptrace.NewClientTrace(ctx, otelhttptrace.WithoutSubSpans())

最後に

OpenTelemetryのTracingについては各言語のSDKも揃ってきて、クラウドベンダーの対応もされてきているので、そろそろ実戦投入していこうと思った。

Chapter 5 - Information Hiding (and Leakage) / A Philosophy of Software Design

第5章はInformation Hiding (and Leakage)というタイトル。いいモジュールを作るには情報の隠蔽(カプセル化)を行うことが大事だよ、という話。

5.1 Information hiding

  • Deep Moduleを作る上で最も大事なのは情報の隠蔽
  • モジュールを実装する上で必要なメカニズムをinterfaceとして表には出さないこと
    • ex)
      • B-treeにどういう情報を保存するか
      • ファイルの中身が物理的なディスクのブロックのどこに存在するかを認識する方法
      • TCPプロトコルの実装の詳細
      • マルチコアCPUでのスレッドの実装
      • JSONをどうやってパースしているか
  • 情報の隠蔽は2つの方法で複雑性を減らす
    • 情報の隠蔽はモジュールのインターフェースをシンプルにする
      • 具体例) B-tree classを開発者が使うときに、理想的なノードの広がりやツリー内部の均衡化については、利用者は気にしなくて良い
    • 情報の隠蔽はシステムの発展を容易にする
      • 情報がモジュール外には隠蔽されている=(モジュール外からみて)その情報への依存性がなくなっているので、その情報に関する内部的な設計の変更の影響はモジュール内部のみである
        • 例えば、TCPでネットワーク混雑時の制御についてプロトコルの仕様が新しく追加されても、TCPのデータの送受信のコードには影響がないはずである

5.2 Information leakage

  • 直訳すると情報の漏洩。情報の隠蔽の反対の概念
  • これはモジュール間の依存を作り出してしまう
  • 情報の漏洩はインターフェースだけで起こるものではない
    • 例えば特定のファイルフォーマットを扱う2つのクラスがあり、片方がファイルを読み込み、もう片方が書き込みをするような場合。ファイルのフォーマットが変更された時は両方のクラスを修正する必要がある。
  • 情報の漏洩もまたred flag(危険信号)
  • 情報の漏洩をどうやって防ぐか?
    • 先ほどの2つのクラスの例であれば、クラスが小さくて密接に関連しているのであればマージして1つにしてしまうのが良い
    • 情報をpull(取得)してカプセル化するクラスを新しく作る、という方法もある。
      • Another possible approach is to pull the information out of all of the affected classes and create a new class that encapsulates just that information. However, this approach will be effective only if you can find a simple interface that abstracts away from the details; if the new class exposes most of the knowledge through its interface, then it won’t provide much value (you’ve simply replaced back-door leakage with leakage through an interface).

5.3 Temporal decomposition

  • 時間による分解
  • Temporal decompositionの考え方で「ファイルを読み込んで、その内容を変更して、新しいバージョンを書き出す」というアプリケーションを考えると、3つのクラスに分解される。
    • ファイルを読むクラスと新しいバージョンを書き出すクラスはそれぞれファイルのフォーマットを知っている必要があるので、ここで情報の漏洩が発生する
    • 情報の漏洩を防ぐには、ファイルを読むクラスと書き込むクラスは同じものにする必要がある
  • 時間による処理の順番でクラスを分解するのではなく、そのクラスが行う処理が知識として何を持っているべきか(先程の例だとファイルのフォーマット)に着目してクラスを分解するべき

5.4  Example: HTTP server

  • 生徒がHTTPプロトコルの実装をした際のデザインの意思決定について、実際の例を交えて考察
  • HTTPプロトコル説明の図

5.5 Example: too many classes

  • 生徒がよくやってしまう間違いとしては、たくさんのShallow Classに分割してそのクラス間での情報漏洩をもたらしてしまうこと
    • 例) ネットワークコネクションからHTTPリクエストを読み込んでstringに変換するクラスを作り、別のクラスでそのstringをパースする
      • Temporal Decompositionのダメな例
      • 例えばContent-Length headerでbodyの長さが指定されているので、Bodyの長さを計算するにはまずContent-Lengthをパースしないといけない
        • stringになったリクエストをパースする時にBodyの長さがわからないとパースできない
        • コードの具体例がなかったのでちょっとここはよくわからなかった
      • 結果として、2つのクラスに分割してもそれぞれがHTTPリクエストの構造を知っていないとダメな実装になっている
      • さらに呼び出し側も2つのクラスの使い方を知っている必要が出てくるので、呼び出し側に余計な認知負荷がかかる
    • よって、上の例ではリクエストを読み込むクラスとパースするクラスは一つにマージした方が良い
      • HTTPリクエストをパースという処理を1クラスに集約することで情報の漏洩が発生しない
      • クラスの数を減らすことで利用者から見たインターフェースはシンプルになる

5.6 Example: HTTP parameter handling

  • HTTP requestがサーバーに来ると、サーバーはリクエストの情報を参照する必要がある
  • 例えば、以前のFigure 5.1で言うとphoto_idというリクエストパラメーター
    • パラメーターはURLのQuery Stringとして送られてくることもあれば、ボディで送られてくることもある
    • Query Stringの場合はURLEncodeされているが、処理する時にはこれはデコードされている必要がある
  • 著者の生徒の大半は以下の良い情報の隠蔽を行なっていた
    • パラメータはQuery StringまたはBody経由で送信されてくるが、それを利用する人にはどちらから来ているかは重要ではないのでマージしている
    • 2つ目の良いところは、パラメーターのデコードについて利用者が意識しなくて済むようにしていること
  • ただし、生徒が作るモジュールは以下の点でShallow Moduleだった
    • HTTPRequestというクラスを作ってその中に Map<String, String> getParams() というメソッドを定義していた
    public Map<String, String> getParams() {
      return this.params;
    }
  • Map<String, String> という内部のデータ型を利用者に公開するのではなく、String getParam(String name) という形にした方が、仮に内部のデータ構造を変えたいときに利用者に影響を与えずに済む
  • パフォーマンスチューニングのために内部のデータ構造を変更することもあるので、なるべく内部のデータ構造は利用者からは見えないようにした方が良い
  • あとはこのMapを利用者側で変更できないようにImmutableにする必要があるなどの問題もある

5.7  Example: defaults in HTTP responses

  • HTTP Responseに関する、生徒たちがよくやる不適切なデフォルト値について説明
  • HTTP ResponseのHTTPのバージョンに関して
    • (生徒の)あるチームはメソッドの呼び出し元にHTTPのバージョンを指定させるようにしていた
    • しかし、HTTPのバージョンはリクエストで指定されているものと一致しているべき
    • なので、リクエストのHTTPバージョンをそのままレスポンスのバージョンとすることが望ましい
    • 呼び出し元がHTTPのバージョンを指定すると、このHTTPのライブラリと呼び出し元の間で情報の漏洩が発生してしまう
  • HTTPレスポンスに必要なものとしてDateヘッダーもある
    • これもHTTPライブラリが気の利いたデフォルト値を提供すると良い
    • もし呼び出し元が何かしらの理由でDateヘッダーをオーバーライドしたい場合は、専用のメソッドを用意する
    • クラス or モジュールはできる限りいい感じのデフォルト値を用意する
    • P.26のJavaのIOバッファリングは、この点でよくない例となっている
      • IOライブラリのほとんどのユーザーはバッファリングするのでそれがデフォルトになっているべきだが、Javaの標準ライブラリではバッファリングのために別のクラスを使ってラップする必要がある
    FileInputStream fileStream = new FileInputStream(fileName);
    BufferedInputStream bufferedStream = new BuffferedInputStream(fileStream);
  • Red Flag: Overexposure(過度の露出)
    • よく使われるAPIが、たまにしか使われない機能もユーザーに覚えさせるようにしてしまうと、認知負荷を増大させてしまう(意味がよくわからなかった)

5.10 Conclusion

  • 情報の隠蔽とDeep Modulesは密接に関連している
  • モジュールがたくさんの情報を隠蔽することで、インタフェースをシンプルにしつつ多くの機能が提供される
  • システムをモジュールに分解するときに、実行時に行われる処理の順番を意識しないようにすること
    • 以前に説明したTemporal decompositionが発生し情報の漏洩やShallow modulesの問題を引き起こす
    • モジュールが知っているべき知識を考慮してそれを各モジュールがカプセル化することで、Deep modulesを作ることができる

2022年の抱負

もうすっかり年が明けてしまって1月末ですが、今年の目標というか抱負を書いておこうかなと思います。

健康第一

最近は風邪をひいたりすると仕事でもプライベートでも明らかに悪影響が出るので、健康第一で生きたい。その意味で以下を引き続き習慣として頑張る。

  • 筋トレ
  • 有酸素運動

有酸素運動はジョギングや早歩きの散歩だと膝が痛くなってしまい、色々と試した結果、マンションの階段昇降が一番コスパ良いかつ膝へのダメージが少ないということが昨年分かったので、1日20分 / 週4を目安に頑張る。

GraphQL+Next.jsでWebアプリを作れるようになる

昨年は趣味プロダクトのReact Hooks + React Query化が終わったので、今年はGraphQL+Next.jsを導入しつつ学びたい。具体的には以下を使っていきたい。

  • Next.js
  • GraphQL
    • gqlgen
    • apollo-client

英語

  • iKnowを週5
  • Camblyが良さそうなのでトライ
  • TOEICを年に2回受ける

をやる。DMM英会話はやめてしまったけど、値段的にはCamblyが良さそうなので試してみる。

アウトプット

1ヶ月に1つはちゃんとした記事を書く。昨年に引き続き継続は力なり、ということで。

というわけで

2022年もやっていきだ!

f:id:oinume:20220130100849p:plain

2021年の振り返り

2021年の抱負 - oinume journalで年初に考えていた抱負がどうだったのか?を振り返ってみる。

習慣化

Habitifyというアプリを1年使い続けてみたけど、習慣化するものとそうじゃないものがあった。ある程度習慣化したものはiKnow(英単語)だけで、その他は習慣化しなかった。Habitify、通知してくれるのはいいんだけど習慣化するにはモチベーションも維持する必要があるので、その点ではあまり役に立たなかったように思う。何かいいアプリがあったら乗り換えたい。

というわけでこれは達成度20%ぐらい。

ReactでSPA作れるようになる

趣味で開発しているプロダクトで、ログイン後の動的なページはReact化が終わった。まだSPAにはできていないけど、React + React Queryを使って動的なページを作るのはできるようになったので、来年はGraphQL + Next.jsを導入してみたい。

英語

今年は6月にTOEICを受けて自己ベストを更新 & 800点超えしたのが地味に嬉しかった(この時の問題が異常に簡単だったという説もある)。来年も自己ベストを更新できるように頑張りたい。

英語のレッスン自体は週1でやっていたのだけど、DMM英会話時代からお世話になっていた先生が育休に入ってしまうということで3ヶ月ぐらい中断している。来年は再開してこれも習慣化したい。

アウトプット

月に1エントリは書くという目標だったけど、2,3,4,5,10月が達成できず。いつものように仕事が忙しくなってしまうとアウトプットが疎かになってしまうので、月初からちゃんとネタを仕込みつつアウトプットできるようにする。

ライブ

海外アーティストが来れないので、そっちは諦めて邦楽のライブに3回ほど参戦。本当はGWのビバラも行くはずだったんだけどコロナが怖くて断念。これがずっと心残りだったので、デルタ株がピークだったけど散々悩んだ挙句、死ぬ気で8/6にZeppまで足を運んだ。もはや生き甲斐がライブぐらいしかないので、不謹慎で危険な行為だとは思ったけど行って良かったと思う。神経すり減らしながらも爆音が鳴っている中で「生きてる」って感じがして、なんとも言えない感覚だった。

  • 8/6 Straightener & Dragon Ash @ Zepp Tokyo
  • 10/14 ROTTENGRAFFTY @ Zepp DiverCity
  • 12/28 Creepy Nuts & Dragon Ash @ Zepp Tokyo

まとめ

2021年もパンデミックであまり旅行も行けなかったけど、ライブにも映画にも行けたし2020年よりはパンデミックに慣れてきた気がする。2022年はライブハウスでモッシュとかダイブができる世の中になるといいなぁと思っている。海外旅行も行きたいですね。

Chapter 4 - Modules Should Be Deep / A Philosophy of Software Design

第4章はModules Should Be Deepというタイトル。

4.1 Modular Design

  • ソフトウェアの複雑性を管理するためのもっとも大事なテクニックの一つとして、全体の複雑性の一部分だけに直面するようにシステムを設計するということが挙げられる。このアプローチは modular design と呼ばれている。
  • この手法を用いると、ソフトウェアシステムは複数のモジュール(クラス, サブシステム, サービス)に分解される。
  • それぞれのモジュールは相互に依存せず独立しているため、開発者はあるモジュールの開発をしている時に他のモジュールのことを考えなくて良い
  • しかし、上記の独立性の話はあくまで理想で実際にはそんなことはない
    • 例えば、あるメソッドに引数を追加した場合はその呼び出し元のコードも変更しないといけない
  • 依存の認識と管理をするためには、モジュールをinterfaceとimplementationの2つに分けることが大事である
  • Typically, the interface describes what the module does but not how it does it.
    • interfaceはそのモジュールが「何をするか」を表明し、それを「どうやるか」については表明しない
  • あるモジュールの開発者は、そのモジュールのinterfaceと実装、およびそのモジュールが依存している他のモジュールのinterfaceを知っている必要があるが、他のモジュールの実装については知らなくても大丈夫である
  • interfaceが実装に比べてとてもシンプルに表現されているモジュールがベスト
  • 上記のようなモジュールは2つのメリットがある
    • シンプルなインタフェースを提供するモジュールは、他のモジュールにもたらす複雑性を少なくする
    • インターフェースが変更されない限り、他のモジュールには影響がない。

4.2 What's in an Interface?

  • interfaceにはformal, informalな情報がある
  • formal: メソッドのシグネチャ(引数およびその型、戻り値およびその型)
    • プログラミング言語の機能によってもたらされる
  • informal: そのメソッドを呼び出した時にもたらされる結果など
    • eg) 引数で与えられたファイル名のファイルを削除するなど
    • informalな情報はメソッドのコメントで説明されることが多い
    • Method Bを呼ぶ前にMethod Aを呼ぶ必要がある、みたいなものもinformalな情報である。

4.3 Abstraction

  • Abstractionとは、実体から重要ではない詳細を除外したもの
  • 間違った抽象化には以下の2つがある
    • 抽象化したが、重要ではないものを含んでしまう
      • 結果的に他の開発者の認知負荷をあげてしまう
    • 抽象化して重要なものを隠してしまう(false abstraction)
      • そのモジュールがシンプルなものだと誤解させてしまう
  • 良い抽象化の例として、ファイルシステムが挙げられる
    • ファイルにデータを書き込む時に、ユーザーはそのデータがストレージデバイスのどのブロックに書き込まれるかなどは意識しない
    • これはファイルシステムがうまく抽象化されていて、ユーザーにとっての不必要な情報を除外しているから
    • 一方で、書き込んだデータのフラッシュについての情報は除外されていない
      • 例えばデータベースのようなソフトウェアだと、「システムがクラッシュしてもファイルに必ず書き込まれているか」を保証するために、「実際のストレージにいつデータが書き込まれるか(=フラッシュされるか)」は知っておく必要がある。そのためこの情報は抽象化されても利用者からわかるようになっている。

4.4 Deep Modules

  • モジュールの深さはコストとベネフィットで考える
    • モジュールのコスト:システムの複雑性。interfaceによって表現される
    • 小さくてシンプルなインターフェースだと複雑性は少なくなる
  • 以下がコストとベネフィットの説明の図
  • UnixのFile I/O はdeep interfaceの良い例
    • 以下の図のようにinterfaceはシンプルだが、実装は以下のような複雑である
      • ディスク上での効率的なアクセスのためのファイルの実装
      • ディレクトリ構造とその階層構造
      • ファイルのパーミッション
      • interruption, background code、およびこれらのやりとり
      • 同時アクセスが発生した場合のスケジューリング
      • アクセスしたファイルのメモリ上のキャッシュ
      • セカンダリのストレージデバイス(ディスク、Flashドライブなど)を一つのファイルシステムに統合する
    • 上記のような複雑なものは利用者からは見えない一方で、何年もの時間を経て大幅に進化している
    • 別の例だと、ガベージコレクションもdeep interfaceである
      • 利用者がほとんど意識しなくていいものだが、実装はとても複雑という意味で。

4.5 Shallow modules

  • interfaceが(提供する機能と比較して)複雑であること

      private void addNullValueForAttribute(String attirubte) {
          data.put(attribute, null);
      }
    
  • 上のコードは何も抽象化していない

  • もしこのメソッドのドキュメントを適切に書いたとしたら、コードより長いドキュメントになる
  • Shallow moduleはRed Flag
    • A shallow module is one whose interface is complicated relative to the functionality it provides. Shallow modules don’t help much in the battle against complexity, because the benefit they provide (not having to learn about how they work internally) is negated by the cost of learning and using their interfaces. Small modules tend to be shallow.
      • Shallow moduleは提供するベネフィットをラーニングコストで打ち消してしまっている
        • 小さいモジュールがshallow moduleになりやすい
  • (個人的に思ったこと)Clean Architectureって割とshallow moduleになりやすいのでは?と思った。例えばinfrastructure層とか、クライアントライブラリのメソッド呼ぶだけみたいなのが多い
    • クライアントライブラリ自体をinfrastructure層に見立てるというやり方もあるっぽい

4.6 Classitis

  • 昨今だとクラスは小さくする方が好まれている。メソッドも同様で「N行超えたら分割するべき」みたいな風潮がある
  • しかし、Deep Classを目指すのであれば、クラスは大きくなる傾向にある
  • Classitis
    • The extreme of the “classes should be small” approach is a syndrome
  • Classitisはたくさんのクラスを生み出し、それぞれのクラスはシンプルになるが全体のシステムとして見ると複雑性が高まる
    • 小さいクラスはそれ単体では十分な機能を提供できない

4.7 Examples: Java and Unix I/O

  • Classitisの典型的な例としてJavaのIOまわりのクラスの話がある。例えばJavaでオブジェクトをファイルから読むときのコードは以下のようになっている。
FileInputStream fileStream = new FileInputStream(fileName);
BufferedInputStream bufferedStream = new BuffferedInputStream(fileStream);
ObjectInputStream objectStream = new ObjetInputStream(bufferedStream);
....
  • バッファリングは基本的にみんな使うので、上記のように専用のクラスを使わないと有効にならないデザインよりかは、よくあるケースに対してシンプルなデザインになっていた方が良い
    • この例だとデフォルトでバッファリングを有効にして、オプションで無効にできるとか。
  • 対照的な例として、Unix Filesystemはシンプルになっている。
    • シーケンシャルIOが最も一般的に使われるので、それをデフォルトの挙動にしている
      • readはシーケンシャルアクセスで、 lseek でランダムアクセスもできるようになっている

4.8 Conclusion

  • interfaceを抽象化してシンプルなものにして、複雑な実装はinterfaceから除外することが大事