エンジニア的なネタを毎週書くブログ

東京でWebサービスの開発をしています 【英語版やってみました】http://taichiw-e.hatenablog.com/

バグチケットに各APIのリクエストとレスポンスが貼ってあったらバグの調査は早く終わるのか?

モヤモヤと考えたことを書き出しています。結論ありません。実践もまだしてません。

背景

私は今、こんな感じでInternalなWeb APIが相互作用するサービス*1に関わっています。
f:id:taichiw:20190107184003p:plain

また、こんな感じで組織が別れています*2
f:id:taichiw:20190107184619p:plain

さて、このサービスについて不具合が起票されることがあります。
リリース前のQAだったり、残念ながらリリース後、本番可動しているサービスに対してだったりします。
いずれにしても大事なこととして、不具合はここの視点、最も浅いレイヤである、UIの「挙動が期待通りでない」事象として発見されます。
f:id:taichiw:20190107185147p:plain

一方システム的には、不具合の原因は以下のいずれかになります(複数の組み合わせの場合もある)

  • UI componentにバグが有る
  • API 1にバグが有る
  • API 2にバグが有る
  • API 3にバグが有る
  • API 4にバグが有る
  • DB1のデータが正しくない
  • DB2のデータが正しくない

不具合を修正するためには、この中のどこに問題があるかを見つける必要があります。

どうすれば早く原因が見つかるのか

シンプルに、いずれか一つのプログラム(=UI component または API 1~4 のいずれか)にバグが有るケースを考えます。
バグが有る、ということは外から見たプログラムの挙動として、以下の2つのいずれかに分類することができます。

  • 正しいリクエストを受けているのに、レスポンスが正しくない
  • 正しいリクエストを受けているのに、次のAPIの呼び出しが正しくない あるいは データの更新が正しくない

たとえばAPI 3がバグっていて、「正しいリクエストを受けているのに、レスポンスが正しくない」場合
f:id:taichiw:20190107190453p:plain
API 3が正しくないレスポンスを返すので、それがAPI 1→UI と伝搬することになります。
この場合、API 3を修正する必要があります。

また、API 2がバグっていて、「正しいリクエストを受けているのに、次のAPIの呼び出しが正しくない」場合
f:id:taichiw:20190107191303p:plain
API 4そのものは正しく動作していますが、リクエストが誤っているため期待通りでないレスポンスを返し、最終的にそのレスポンスが原因となってUIの挙動が期待通りになりません。
この場合、API 2を修正する必要があります。

「赤い矢印」が見つかればどのプログラムがバグっているのかわかる

前項で見たとおり、期待通りでないリクエスト、またはレスポンスである、「赤い矢印」
f:id:taichiw:20190107191317p:plain
がどこから始まっているかがわかれば、バグのあるプログラムである、「赤い四角」
f:id:taichiw:20190107191417p:plain
の箇所が特定できそうです。

ということで、バグが起票された際に、全APIのリクエストとレスポンスがチケットに貼ってあれば、即座にどのAPIがバグっているのかわかるのでは? と考えました。
こんな表を自動生成することはできないでしょうか。

API リクエス レスポンス APIへのリクエス
API 1 f:id:taichiw:20190107191851p:plain f:id:taichiw:20190107191902p:plain to API 2 f:id:taichiw:20190107191929p:plain, to API 3 f:id:taichiw:20190107191851p:plain
API 2 f:id:taichiw:20190107191929p:plain f:id:taichiw:20190107192044p:plain to API 4 f:id:taichiw:20190107191317p:plain
API 3 f:id:taichiw:20190107191851p:plain f:id:taichiw:20190107192118p:plain
API 4 f:id:taichiw:20190107191317p:plain f:id:taichiw:20190107191902p:plain

※矢印はイメージです。実際はこの表の各セルに具体的なJsonが書かれます

Spring Cloud Sleuth でTraceIDを発行しているため、一つのUIアクセスから始まる各APIのリクエスト/レスポンスを集めてくるのは技術的には可能そうです。

「赤か青か」を判断するのが実は難しい?

上の表では、API 2は正しいリクエストを受けているにもかかわらず、API 2からAPI 4へのリクエストが間違っています。ここから、API 2にバグが有る、と推測することが可能です。
f:id:taichiw:20190107193039p:plain
では、API 2→API 4のリクエストが間違っている と気づくことができるのは誰でしょうか…。API 2はTeam C、API 4はTeam Dが担当しているので、それぞれのチームのエンジニアに気づくチャンスがありそうです。

API 2の担当エンジニアは気づけるか?

まず、API 2の担当エンジニアは、上の表から以下のことに比較的簡単に気づけそうです。

  • API 2のレスポンスが期待通りでない
  • API 4からのレスポンスが期待通りでない

ここから、API 4へのリクエストが間違っているか、API 4そのものにバグが有る という推測は立てられそうです。
しかし、API 4へのリクエストの検証についてはどうでしょうか。
シンプルな間違え*3ならばすぐ気がつけそうですが、もう少し複雑な間違えですとすぐには気づけないかもしれません。
また、API 2の担当エンジニアが、API 4のリクエスト使用を誤って理解しているかもしれません。

API 4の担当エンジニアは気づけるか?

API 2→API 4のリクエストが間違っている」ということにAPI 4の担当エンジニアが気づくためには、以下のことを理解している必要があります。

なぜならば、バグの起票時点では、UIレベルの情報しかないからです。
f:id:taichiw:20190107193948p:plain
これも、即座に正しいか誤っているか判断できる場合もあると思います。しかし、API 4の使われ方によっては、中間のAPIの仕様を知らないとリクエストの正誤の判断が難しいかもしれません。

リクエストの正しさの確認は狭間に落ちる?

…ということを想像していくと、API 2→API 4のリクエストの正しさの確認というのは、どちらのエンジニアにとっても難しいのでは…? という気がしてきました。

レスポンスの正しさの確認のほうが簡単かも

では一旦、「すべてのAPIのリクエストは正しい」と仮定して、レスポンスの正しさだけに注目したらどうでしょうか。

バグの起票時に、UIレベルで「本来Aという振る舞いをすべきところ、Bという振る舞いをしている」ということは記載されています。
この場合、「『B』の原因となるレスポンスを自分のAPIが返している」かどうかの確認は、リクエストの確認の正しさに比べると比較的難易度が低い …気がします。 自分の経験的には。

APIのレスポンスだけとりあえず並べてみる

レスポンスの正誤の判別は比較的簡単… という前提で、「バグ起票時に各APIのレスポンスが自動的に貼り付けられるツール」のようなものを想像してみます。

API レスポンス
API 1 f:id:taichiw:20190107191902p:plain
API 2 f:id:taichiw:20190107192044p:plain
API 3 f:id:taichiw:20190107192118p:plain
API 4 f:id:taichiw:20190107191902p:plain

※繰り返しになりますが、矢印のところには具体的なJSONが入るイメージです

システム構成が頭に入っていれば、以下のような図が見えるわけです。
f:id:taichiw:20190107195535p:plain
※データがおかしい可能性もありますが、今は除外して考えます

API 3には絶対バグがないことがわかった!バンザイ! といいたいところなのですが、
UI, API 1, API 2, API 4 のどれもバグ持ちの可能性が消えていません。
これだけでは、あまり何かが解決しているようには思えません。

いろいろ考えてみたけどこれだったら結局…

先程問題にあげた、「リクエストの正誤の方法」について掘り下げたほうが何か効果があるかもしれません。

今日はここまでです。

続きます…(多分)

*1:図は簡略化しています。現物はもっと大きいです。また、いわゆるマイクロサービスアーキテクチャに近いとは思いますが微妙に違う気もします。組織とか。

*2:この組織構成が良いのかどうかの話は、ここではしません。

*3:例えば、API 1 -> API 2 では Item A がリクエストされているのに、API 2 -> API 4 ではItem Bがリクエストされている というケースです。

『複雑なドメインに泥臭く立ち向かう』JJUG CCC 2018 Fall #jjug_ccc

JJUG CCC 2018 Fallに参加し、@su_kun_1899 さんの 『複雑なドメインに泥臭く立ち向かう』というセッションに参加してまいりました。

speakerdeck.com

twitter.com


法律という完全に外部要因で決まって変えることもできないものが要件に深く絡む世界で、
どのように立ち向かっているのか、というお話でした。

今一番読みたかった本が現れたかのように感じたセッション

どうやって複雑なドメインを理解し適切にコードに落とし込むか
というのは今現在の私の最大の関心事の一つでありながら、
もう一段自分のレベルを上げたいのだけれど何を勉強したら良いかわからないのが課題でした。

そんな中、今日のセッションは

  • 自分が聞きたかった話を
  • 理解できる言葉で
  • 体系立てて

話してもらえたセッションでした。

更にいうと、一部の内容は、普段から自分がぼんやりと「思っていた*1」ことを、明確に言語化してもらった内容でした。

以下、私が感じたことが中心です。
順序も元の発表と異なります。ご了承ください。

共感したポイント

一本道を見つける

「変化は受け入れ、一本道に対して複雑さを足していくアプローチをとる」

そうなんですよね。いかに幹を見極め、枝葉と分離できるか。
いまから作ろうとしているプロダクトの背景にある真の目的はなんなのか。
それを見つけ出せることが重要なんだと思います。

モデルを抽出するするために ひたすら write & talk

とにかくなんでも良いから書き始めてみること。
脳内でウンウン考えているだけだと、自分自身が認識するのもなかなか難しい。

サービスクラスがユースケースを表す

すごくわかります。
個人的に、サービスクラスが数行でかけたら価値だと思っています。(というよりはサービスクラスがごちゃごちゃしたら負け)
taichiw.hatenablog.com

コードに書いて、動かして、初めて理解したと言える

今年の私の実体験。
単なるAPIだろうがんなんだろうが、実際に動くものを見て、触れて、初めて理解できることや気づける疑問が人間どうしてもあります。
更にこの時、クソコードだろうがなんだろうが、
とりあえずでも「動いて」、そして一応でも「読める」プログラムがあることによって、それまでに比べて格段に理解がはかどるようになります。
皆さんエンジニアですから。
(クソコードでもよいのですが、「適切な枠で切られている」ことはとっても大事。その中がクソな分には最悪差し替えれば良いのです)

「枝葉」は課題管理表にきちんと残す

「一本道の骨格を洗い出す過程で、とりあえず置いておくことに決めたものはきちんとバックログに、すぐに残すこと。」

これは悪い方向での今年の私の実体験。
途中から管理が雑になって、結局漏れた要件や、
他のメンバーから「なにそれ聞いてない!追加要件だ!」って言われてしまった*2ものがいくらか。

気づかせてもらったこと

最低限のドメインナレッジの勉強は必要

「はじめての『介護保険』」のような、初心者向けの書籍は大抵あるので、それだけでも読んでおく

これだけでもドメインエキスパートと話すときに単語の理解が捗る。

実際の業務のロールプレイをしてみる

手書きで帳票を書く、という言うようなことをしているそうです

「ユーザーストーリーマッピング

まさかの、ここでこの本。
もともと本書がターゲットにしている話題とは少し異なるが、ドメインを知ることにも使える手法がたくさんだと。

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

ユーザーストーリーマッピング [ ジェフ・パットン ]
価格:3240円(税込、送料無料) (2018/12/15時点)


Talkの重要性

モデルを作り上げていく際、付箋とホワイトボードを使って「Write & Talk」を行うという話。

書くだけでなく、他のメンバーと話すことによって理解が深まっていくわけですね。
個人的にはここがうまくできていないことがあって、常に試行錯誤中…。
それぞれの箇所について1対1ではできているんだけど他のメンバーが巻き込めていなかったりとか。

*1:思うことと実践することは別です

*2:とはいえ、「枝葉」なので、後で足しても根底から設計を覆すものではなかった… はずなのですが

3歳の息子が私のサービスを使った日

このエントリは、子育てエンジニア Advent Calendar 2018の14日目のエントリです。
このブログではプライベートなことをあまり書いたことがないのですが、乗っからせていただきます。

About 私 & 息子 (と奥さん)

私には、現在3歳半の息子がおります。一日の生活リズムはこんな感じです。
f:id:taichiw:20181215011212p:plain
f:id:taichiw:20181215011256p:plain

私の「夢」

私には、ずっと以前、社会人になるよりももっと前から思い描いていた「夢」が一つありました。

「家のリビングにテレビが置いてあって、そのテレビを指差して、『このテレビ、お父さんが作ったんだよ』って自分の子供に向かって話すんです」

この話を唐突に、(今も働いている)インターネットサービスの会社の採用面接で話したので、当時面接官の方にポカーンとされたのを今でも覚えています。
「テレビ」というのは喩えでして、
自分の作った「モノ」を自らの子供に見せたい、使ってもらいたい、という思いを、子供もいなければ結婚もしていない当時から抱いていたんです。

ようやく念願のリリース…!

私の「お仕事」に話を移します。
最近、実に一年半もかけた新規開発案件がようやくリリースされました。*1
Webサービスをやっていて一年半もの間じっくりと未稼働のサービスに取り組む、というのは初の経験で、勉強になる部分も多かったのですが、

やはり、「本番で動いている」というのは良いものです。

家に帰ってからも思わず、自分のスマートフォンを取り出しては、
こんな感じの画面を見ながら、
動いてる姿にニヤニヤしておりました。
f:id:taichiw:20181215014255p:plain

すると覗き込んでいた息子が、脇から「ANAだー」「JALだー」と言い出したんです。
この、羽のアイコンだけ見て飛行機を表していると気づいたことにまず驚いたのですが、
f:id:taichiw:20181215014703p:plain
どうやらこの検索画面が大変お気に召されたようで、息子氏は私から携帯電話を奪い取り、グリグリと画面をスクロールして、
「この水色のは何?」「これは?」
と、航空会社を覚えることを数日間お楽しみあそばされたのです。

いちいち携帯電話を奪われるので大変面倒くさかったのですが、この日は「 息子が私のサービスを使った記念日」になりました。

残念ながら息子は予約までしてくれたわけではないので、真に「使ってくれた」わけではありません。

また、「お父さんがこれを作った」と話してみたのですが、ピンときていないようでした。

しかしながら、
自分が作ったモノを、誰でも利用できる
というコンシューマ向けサービスのエンジニアならではのやりがいを、息子を通して改めて感じた日でした。

*1:…が、この後、事情により一旦サービ停止中となっております。ご迷惑をおかけしています

ParallelStreamで外部接続しちゃいかんの? → 独自スレッドプールでParallelStreamを使ってみた

ParallelStreamでI/O待ち状態を作るとプール内のスレッドを食いつぶす?

ParallelStreamはアプリ全体で共通で持っているスレッドプールを使う上、CPUのコア数分しかスレッドが無いのでI/O待ちが発生するようなものには使ってはいけない、という話を聞きました。

そうなの?ということで検証してみました。

List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
	list.add(Integer.valueOf(i));
}

list.stream()
  .parallel()
  .forEach(i -> {
    //1秒かかる外部サービス
    restTemplate.getForObject("http://localhost:8081/sleep/", String.class);
    System.out.println(i);
});

f:id:taichiw:20180930225024p:plain
手元のPC上で動かした結果を、Visual VMでみてみたところ。

ForkJoinPool.commonPool-worker- ...
という、アプリ内での共通スレッドプールが、ParallelStreamで使われています。
4コアCPUで実験しているため、プール内のスレッドは3つ。(それに加えて呼び出し元のスレッドがあるので、JVM全体で可動しているスレッドは4つ)。

I/O待ちの間はスレッドは専有状態になるため、
4個通信 → (1秒経ってレスポンス) → また4個通信

というように、外部通信が4件ずつしか行われません。

また、この間に同じアプリ内の他の処理でもParallelStreamを使おうとすると、そちらとの、Common Pool内のスレッド取得合戦が起こってしまうことも確認できました。

オレオレスレッドを使ってみる …でもこれ大丈夫?

Common Poolのスレッド数を設定で増やすことも可能なようですが、「スレッド取得合戦」を避けるため、独自のスレッドを作ってParallelStreamを回すことを検討してみます。

ForkJoinPool pool = new ForkJoinPool(10);
pool.submit(() ->
  list.stream()
  .parallel()
  .forEach(i -> {
    //1秒かかる外部サービス
    restTemplate.getForObject("http://localhost:8081/sleep/", String.class);
    System.out.println(i);
  })
).get();
ForkJoinPool pool = new ForkJoinPool(10);

で、スレッドプールを作成し、このプールを使ってparallel streamを回します。

すると

  1. スレッド数の上限が10になったので、10並列で通信を実行。一気に10回分のサービス呼び出しが終了。
  2. 同時に、アプリ内の他の箇所でParallelStream(デフォルトのCommon Poolを使用)を実行してもスレッド取得が競合しない

という結果が得られました。

ただ… メソッド実行のたびにスレッドが増えていきます。大丈夫なんでしょうか。
f:id:taichiw:20180930222344p:plain

これ、本番可動させてたら無尽蔵にスレッドが増え、一定期間経つとアラートが飛んできて、再起動を余儀なくさせられるんじゃないでしょうか。
過去の悪夢が蘇ってきます。


…ということで軽くロングランテストを実施。
3時間位流し続けてみた限りでは、
GCのタイミングで古いスレッドは消えていっており、特にスレッドが溢れたりメモリリークしたりしている様子はありませんでした。
f:id:taichiw:20180930225134p:plain
49652番目のスレッドプール。これより以前に作成されたものは消えている。

ただ、都度スレッドを作成して破棄するのは、CPUとメモリの無駄遣いっぽい雰囲気がします。

専用の「スレッドプール」を作る

ということで、以下のように(シングルトンの)インスタンス変数としてスレッドプールを定義してみました。

@RestController
public class TestController {

  final ForkJoinPool pool = new ForkJoinPool(100);

  @GetMapping(value = "test")
  public String test() throws ExecutionException, InterruptedException {

    RestTemplate restTemplate = new RestTemplate();

    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
      list.add(Integer.valueOf(i));
    }

    pool.submit(() ->
        list.stream().parallel()
            .forEach(i -> {
              //1秒かかる外部サービス
              restTemplate.getForObject("http://localhost:8081/sleep/", String.class);
              System.out.println(i);
            })
        ).get();

    System.out.println("OK");
    return "OK";
  }
}

f:id:taichiw:20180930224809p:plain

いい感じでスレッドの使い回しができているようです。

SpringBootのアプリで、JMHを使ってみた

メソッドのちょっとした書き方の違いによる速度の計測をしたくて(Micro Benchmarkというらしい。対義語はMacro Benchmark)、JMHで計測をしてみました。

JMHのオフィシャルページを見ると、mvn archetype:generate なるものが出てきて「なんじゃこりゃ?」だったり
適当にググったページでは、そもそも古くて今は動かないサンプルだったりと、どうにもわかりにくかったのですが、こちらのページのとおりに試してみたところ、うまくいきました。
Microbenchmarking with Java | Baeldung

それでもハマったところ
1. SpringBootのmain methodがあるクラスに、Benchmark用のmain methodも書いたところ、以下のようなエラーが出て何故か起動できず。

# JMH version: 1.21
# VM version: JDK 1.8.0_121, Java HotSpot(TM) 64-Bit Server VM, 25.121-b13
# VM invoker: C:\Program Files\Java\jdk1.8.0_121\jre\bin\java.exe
# VM options: -Xmx3000m -Ddebug -XX:TieredStopAtLevel=1 -Xverify:none -Dspring.output.ansi.enabled=always -Dzookeeper.address=b -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=51723 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=localhost -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2018.1\lib\idea_rt.jar=51724:C:\Program Files\JetBrains\IntelliJ IDEA 2018.1\bin -Dfile.encoding=UTF-8
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: rakuten.travel.availability.calculation.api.BenchmarkRunner.benchMarkKick3

# Run progress: 0.00% complete, ETA 00:10:00
# Warmup Fork: 1 of 1
Error: Exception thrown by the agent : java.rmi.server.ExportException: Port already in use: 51723; nested exception is: 
	java.net.BindException: Address already in use: JVM_Bind
<forked VM failed with exit code 1>
<stdout last='20 lines'>
</stdout>
<stderr last='20 lines'>
Error: Exception thrown by the agent : java.rmi.server.ExportException: Port already in use: 51723; nested exception is: 
	java.net.BindException: Address already in use: JVM_Bind
</stderr>

→ 上記ページのように、mainメソッドを書くための専用のクラスを別に作ったところ、問題なく動作。

2. テスト対象のメソッドにパラメータを渡す方法がわからなかった
→ これも上記ページに書いてある


おまけ
とあるメソッドのリファクタ結果

Benchmark                        Mode  Cnt       Score        Error  Units
BenchmarkRunner.benchMarkKick1  thrpt    5   29271.881 ±   1088.831  ops/s
BenchmarkRunner.benchMarkKick2  thrpt    5   74336.732 ±   4597.243  ops/s
BenchmarkRunner.benchMarkKick3  thrpt    5  280990.396 ± 185017.629  ops/s

1はもとのまま。2はちょっとリファクタリング。3はもっとリファクタリング
もとより桁一つ速くなった。うひょー!

Mavenで実行しているTestに割り当てるヒープを増やす方法

Maven Surefire Pluginを使う。
https://maven.apache.org/surefire/maven-surefire-plugin/

  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>-Xmx1024m -XX:MaxPermSize=256m</argLine>
    </configuration>
  </plugin>

ぐぐると、MAVEN_OPTSに書けばいい とか、 Jenkinsの場合はGlobal MAVEN_OPTSに書けばいい とか出てくるんだけど、
これは、MavenJavaプロセスに割り当てるメモリ。
その中で実行されるテスト用のメモリは別に確保される。

実際、テストコード内で以下のように割り当てメモリを書き出してみると

int mb = 1024 * 1024;
Runtime runtime = Runtime.getRuntime();
System.out.println("Max Memory:" + runtime.maxMemory() / mb);

MAVEN_OPTSに何を書いても増えないし減らない。

Chrome Tech Talk Night

events.withgoogle.com
Google さんで行われた、Chrome Tech Talk Night に参加してきました。

弊社の新人研修依頼、10年以上ぶりに森タワーのオフィスに入ったなぁ とか しょうもないことを思いながら。


そもそも私、フロントエンジニアではないので、正直細かいところはよくわからんというか的はずれな理解だった感も否めないのですが…


発表する人、発表する人、

  • これは標準化される(予定である/されている)
  • まだ絶賛仕様決めてるような段階だけどGithubに最新のリポジトリがある
  • オンラインでオープンにディスカッションされている


くっ ここはそういう世界か…!
と。


テクノロジー会社としてのあるべき姿をまざまざと見せつけられた気がしました。

なので忘れないようにブログ。