『複雑なドメインに泥臭く立ち向かう』JJUG CCC 2018 Fall #jjug_ccc
JJUG CCC 2018 Fallに参加し、@su_kun_1899 さんの 『複雑なドメインに泥臭く立ち向かう』というセッションに参加してまいりました。
法律という完全に外部要因で決まって変えることもできないものが要件に深く絡む世界で、
どのように立ち向かっているのか、というお話でした。
今一番読みたかった本が現れたかのように感じたセッション
どうやって複雑なドメインを理解し適切にコードに落とし込むか
というのは今現在の私の最大の関心事の一つでありながら、
もう一段自分のレベルを上げたいのだけれど何を勉強したら良いかわからないのが課題でした。
そんな中、今日のセッションは
- 自分が聞きたかった話を
- 理解できる言葉で
- 体系立てて
話してもらえたセッションでした。
更にいうと、一部の内容は、普段から自分がぼんやりと「思っていた*1」ことを、明確に言語化してもらった内容でした。
以下、私が感じたことが中心です。
順序も元の発表と異なります。ご了承ください。
共感したポイント
一本道を見つける
「変化は受け入れ、一本道に対して複雑さを足していくアプローチをとる」
そうなんですよね。いかに幹を見極め、枝葉と分離できるか。
いまから作ろうとしているプロダクトの背景にある真の目的はなんなのか。
それを見つけ出せることが重要なんだと思います。
モデルを抽出するするために ひたすら write & talk
とにかくなんでも良いから書き始めてみること。
脳内でウンウン考えているだけだと、自分自身が認識するのもなかなか難しい。
サービスクラスがユースケースを表す
すごくわかります。
個人的に、サービスクラスが数行でかけたら価値だと思っています。(というよりはサービスクラスがごちゃごちゃしたら負け)
taichiw.hatenablog.com
コードに書いて、動かして、初めて理解したと言える
今年の私の実体験。
単なるAPIだろうがんなんだろうが、実際に動くものを見て、触れて、初めて理解できることや気づける疑問が人間どうしてもあります。
更にこの時、クソコードだろうがなんだろうが、
とりあえずでも「動いて」、そして一応でも「読める」プログラムがあることによって、それまでに比べて格段に理解がはかどるようになります。
皆さんエンジニアですから。
(クソコードでもよいのですが、「適切な枠で切られている」ことはとっても大事。その中がクソな分には最悪差し替えれば良いのです)
気づかせてもらったこと
最低限のドメインナレッジの勉強は必要
実際の業務のロールプレイをしてみる
手書きで帳票を書く、という言うようなことをしているそうです
3歳の息子が私のサービスを使った日
このエントリは、子育てエンジニア Advent Calendar 2018の14日目のエントリです。
このブログではプライベートなことをあまり書いたことがないのですが、乗っからせていただきます。
About 私 & 息子 (と奥さん)
私には、現在3歳半の息子がおります。一日の生活リズムはこんな感じです。
私の「夢」
私には、ずっと以前、社会人になるよりももっと前から思い描いていた「夢」が一つありました。
「家のリビングにテレビが置いてあって、そのテレビを指差して、『このテレビ、お父さんが作ったんだよ』って自分の子供に向かって話すんです」
この話を唐突に、(今も働いている)インターネットサービスの会社の採用面接で話したので、当時面接官の方にポカーンとされたのを今でも覚えています。
「テレビ」というのは喩えでして、
自分の作った「モノ」を自らの子供に見せたい、使ってもらいたい、という思いを、子供もいなければ結婚もしていない当時から抱いていたんです。
ようやく念願のリリース…!
私の「お仕事」に話を移します。
最近、実に一年半もかけた新規開発案件がようやくリリースされました。*1
Webサービスをやっていて一年半もの間じっくりと未稼働のサービスに取り組む、というのは初の経験で、勉強になる部分も多かったのですが、
やはり、「本番で動いている」というのは良いものです。
家に帰ってからも思わず、自分のスマートフォンを取り出しては、
こんな感じの画面を見ながら、
動いてる姿にニヤニヤしておりました。
すると覗き込んでいた息子が、脇から「ANAだー」「JALだー」と言い出したんです。
この、羽のアイコンだけ見て飛行機を表していると気づいたことにまず驚いたのですが、
どうやらこの検索画面が大変お気に召されたようで、息子氏は私から携帯電話を奪い取り、グリグリと画面をスクロールして、
「この水色のは何?」「これは?」
と、航空会社を覚えることを数日間お楽しみあそばされたのです。
いちいち携帯電話を奪われるので大変面倒くさかったのですが、この日は「 息子が私のサービスを使った記念日」になりました。
残念ながら息子は予約までしてくれたわけではないので、真に「使ってくれた」わけではありません。
また、「お父さんがこれを作った」と話してみたのですが、ピンときていないようでした。
しかしながら、
自分が作ったモノを、誰でも利用できる
というコンシューマ向けサービスのエンジニアならではのやりがいを、息子を通して改めて感じた日でした。
*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); });
手元の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を回します。
すると
- スレッド数の上限が10になったので、10並列で通信を実行。一気に10回分のサービス呼び出しが終了。
- 同時に、アプリ内の他の箇所でParallelStream(デフォルトのCommon Poolを使用)を実行してもスレッド取得が競合しない
という結果が得られました。
ただ… メソッド実行のたびにスレッドが増えていきます。大丈夫なんでしょうか。
これ、本番可動させてたら無尽蔵にスレッドが増え、一定期間経つとアラートが飛んできて、再起動を余儀なくさせられるんじゃないでしょうか。
過去の悪夢が蘇ってきます。
…ということで軽くロングランテストを実施。
3時間位流し続けてみた限りでは、
GCのタイミングで古いスレッドは消えていっており、特にスレッドが溢れたりメモリリークしたりしている様子はありませんでした。
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"; } }
いい感じでスレッドの使い回しができているようです。
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
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に書けばいい とか出てくるんだけど、
これは、MavenのJavaプロセスに割り当てるメモリ。
その中で実行されるテスト用のメモリは別に確保される。
実際、テストコード内で以下のように割り当てメモリを書き出してみると
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年以上ぶりに森タワーのオフィスに入ったなぁ とか しょうもないことを思いながら。
そもそも私、フロントエンジニアではないので、正直細かいところはよくわからんというか的はずれな理解だった感も否めないのですが…
発表する人、発表する人、
くっ ここはそういう世界か…!
と。
テクノロジー会社としてのあるべき姿をまざまざと見せつけられた気がしました。
なので忘れないようにブログ。
"Fiber" で 100万スレッドが実行可能に!? - Project Valhalla とProject Loom : JavaDayTokyo 2018 参加レポート2 #JavaDayTokyo
Project XXX系の話について、Project ValhallaとProject Loomについて聞きました。
どちらも講演はDavid Buckさん*1。
残念ながら、どちらもJava11には入らないので、LTSのリリースサイクルを考えるとまともにサービスで使えるのは早くて2021年…
とはいえ、どちらもとても夢のある話でした。
Project Valhalla
Value Types (値型)
class Point { int x; int y; }
のような、プリミティブ型をフィールドとして持つクラスがある時に
Point[] points;
配列を作ると、各Pointインスタンス毎にヘッダを持ってしまい、大きなメモリのオーバーヘッドが発生します。
メモリの最適化だけ考えると
int[] xList; int[] yList;
とプリミティブ変数の配列を宣言するのが最も良いのですが、これではせっかくのオブジェクト指向が台無し。
そこで、「例えば*2」
value class Point { int x; int y; }
このようにクラス宣言した際に、さも intの配列が2つあるかのようなメモリ確保がされる… というのが概要です。
これによって、省メモリ化に加えて、参照の高速化も図ることができるようになります。
What is Project #Valhalla and what it will bring to your #Java development experience?https://t.co/k8KMxOj0ya pic.twitter.com/V5G1fpvn84
— Java (@java) 2017年11月12日
ここまでは昨年のJavaDayTokyoでも既に紹介されていました。
Generic Specialization (ジェネリックの特殊化)
上記を実現する過程で、このようなことができる様になる… と紹介されました。
ArrayList<int> xList;
要はプリミティブ型がほとんど普通のオブジェクトのように使えると。
さらに、intStreamのような(普通のstreamと別にメソッドを用意しなくてはならないような)「ダサい」対応も不要になるとのことです。
とは言えいろいろ悩んでるらしい
とは言え、この「ジェネリックの特殊化」をいざ実装しようとすると悩ましい課題が多いらしく… まだまだ実用化には時間がかかりそうでした。
実際の所、どのくらい使えるんだろう
現実的な利用シーンを考えると、フィールドにプリミティブしか持たないクラスというのはあんまりない気も。
せめて、StringやLocalDate系が混ざっていてもうまいこと扱っていただけるようになると良さそうなのですが。
Project Loom
こちらがタイトルの、”100万スレッド”です。
現在のJVMでは、JVM上のスレッドは、OSが管理するスレッドとマッピングされています。それぞれのスレッドは、用途が異なるかもしれません。あるスレッドは複雑な暗号化に使われるかもしれないし、またあるスレッドは単に変数を一つ書き換えるだけの簡単な処理に使われるかもしれません。
しかしながらOSは、アプリケーションのコードを知らないため、各スレッドの用途もわかりません。そのため、どのスレッドも一律、「何にでも使えるスレッド」を用意することになります。
ここで、「Fiber」という考え方が登場します。Fiberは、OSではなく、Javaのruntimeやユーザコードによって支配されるスレッド。アプリケーションはスレッドの用途を知っているため、無駄のないスレッドを効率的に作ることが可能です。その結果、OS管理のスレッドに比べてなんと1000倍ほど数のスレッドが作れるようになるとのこと。
それだけOSが作るスレッドには「無駄」があるそうです。
ブロッキングと非同期
ちょうど一年前のJavaDayTokyoで聞いた話ですが、
taichiw.hatenablog.com
ブロッキングな処理はスケールしにくい、という問題点があります。
その対策として、非同期(ノンブロッキング)に処理を制御する、という方法があるものの、プログラミングやデバッギングが難しい、という問題があります。
この双方の問題を解決するのがFiber.
スレッドモデル(=ブロッキングな処理)であるため取扱が簡単であるにもかかわらず、スケーラビリティに優れるという夢のようなお話です。
言ってることが未来
Fiberはスレッドなので、「どんな処理が」「どこまで進んだか」が管理されています。
他方、JVM上で管理されているオブジェクトなので、Serializeが可能とのこと。
その結果何ができるようになるかと言うと…
と言ったことができるようになるとのこと。
… よくわかりません。
まだ検討段階
実際の所、まだまだ開発が始まったばかりでCommitもないとか。
早くて今年中にアーリーアクセスが出る…… かも。
とのことでした。
しかしとっても夢のある話。実現が楽しみです。