CRaCを用いたJavaアプリケーションの起動高速化
検索エンジンプロダクトを一緒に開発してた同窓会 Advent Calendar 2023 の9日目です。
Javaアプリケーションの起動を高速化する技術として、Application CDSやGraalVM Native ImageによるAOTコンパイルなどがあります。
CRaC(Coordinated Restore at Checkpoint) もまた、Javaアプリケーションの起動高速化を目的としたOpenJDKのプロジェクトです。CRaCはLinuxのCRIUを利用し、実行中のJavaアプリケーションの状態をスナップショットとして保存します。スナップショットからはアプリケーションを高速に復元することができます。
今回はSpring Bootアプリケーションのスナップショットを作成し、スナップショットからアプリケーションを起動してみます。
CRaCをサポートするJDKのインストール
今回は Liberica JDK with CRaC を利用します。
私は SDKMAN でインストールしました。
$ sdk install java 17.0.9.crac-librca $ sdk use java 17.0.9.crac-librca
確認します。
$ sdk current java Using java version 17.0.9.crac-librca
CRaCののドキュメント に従い、実行ユーザがCRIUを利用できるように lib/criu
のパーミッションを変更します。
$ sudo chown root:root jdk/lib/criu $ sudo chmod u+s jdk/lib/criu
helpが表示できれば準備OKです。
$ $JAVA_HOME/lib/criu --help
環境によってはSELinuxのインストールが必要かもしれません。
$ $JAVA_HOME/lib/criu --help /home/todokr/.sdkman/candidates/java/17.0.9.crac-librca/lib/criu: error while loading shared libraries: libselinux.so.1: cannot open shared object file: No such file or directory
$ ldd $JAVA_HOME/lib/criu ... libselinux.so.1 => not found ...
Spring Boot アプリケーションの用意
spring-boot-starter-web をベースにプロジェクトを作成します。
依存ライブラリに org.crac:crac を追加し、SpringがCRaCを利用できるようにします。
dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.crac:crac") ... }
アプリケーションの初回起動と復元の差を際立たせるため、mainメソッド内にsleepを入れておきました。
@SpringBootApplication class SpringBootWithCracApplication fun main(args: Array<String>) { println("Starting app...") Thread.sleep(3000) runApplication<SpringBootWithCracApplication>(*args) }
コードの全体はこちらのリポジトリ にあります。
初回起動とスナップショット作成
アプリケーションをビルドして起動します。CRaCCheckpointTo
にはスナップショットを保存するディレクトリを指定します。
$ ./gradlew build $ java -XX:CRaCCheckpointTo=checkpoint -jar ./build/libs/spring-boot-with-crac-0.0.1-SNAPSHOT.jar Starting app... ... [main] c.e.s.SpringBootWithCracApplicationKt : Started SpringBootWithCracApplicationKt in 0.997 seconds (process running for 4.2)
初回起動はsleepの3秒込みで 4.2
秒かかりました。
jcmd
でスナップショットを作成します。スナップショット作成後、アプリケーションは停止します。
$ jcmd spring-boot-with-crac JDK.checkpoint 1710359: CR: Checkpoint ...
CRaCCheckpointTo
に指定したディレクトリ内にファイルがモリモリと作成されています。
これらは先ほどのアプリケーションのJVMメモリがダンプされたものです。
$ ls -l ./checkpoint core-1712983.img core-1712998.img core-1713046.img dump4.log core-1712984.img core-1712999.img core-1713047.img fdinfo-2.img ... core-1712994.img core-1713042.img core-1713068.img stats-dump
スナップショットからのアプリケーション起動
スナップショットから起動してみます。
$ java -XX:CRaCRestoreFrom=checkpoint [Attach Listener] o.s.c.support.DefaultLifecycleProcessor : Restarting Spring-managed lifecycle beans after JVM restore [Attach Listener] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '' [Attach Listener] o.s.c.support.DefaultLifecycleProcessor : Spring-managed lifecycle restart completed (restored JVM running for 60 ms)
アプリケーションが正常に起動したことがわかります。
ログにあるように、アプリケーションの起動にかかった時間はわずか 60ms
でした。
プロダクションでの利用にはCI/CDのや各種リソースのクローズ/再作成、スナップショットにシリアライズされた機密情報の管理など、考慮が必要な事項が多々ありますが、今後の発展が非常に楽しみな技術に思えました。
ScalaMatsuri 2020 に登壇しました +α
昨年に引き続き、ScalaMatsuriに登壇させていただきました。今年は「Scalaチームのオンボーディング」という題で、チームの同僚である Robert Devlin と2人で40分お話ししました。
「Scalaは表現力の高い言語だが、その表現力が新メンバーのキャッチアップを難しくさせてしまう」ということは、これまでにも各所でしばしば語られてきたことだと思います。このセッションでは新メンバーをお迎えするにあたっての課題、「力を入れる学習トピックをどう選ぶか」や「それらをどのように解説するか」などについて、我々の取り組みをご紹介しました。
ご紹介した取り組みの大半は私ではなく、同僚らの継続的なプロセス改善から生まれてきたものです。オンボーディングプログラムの礎は @scova0731 さんだったり 、最近のプログラム整備は @enket さんがリードしていたりなどなど。プログラムのチューターをしてくれている何人もの同僚もいます。
私は転職ではなく異動という形でこのオンボーディングプログラムを受けたのですが(クロスボーディングというやつですね)、体系だったオンボーディングプログラムは初めてだったので、「こんなに手取り足取り教えてくれていいのだろうか?罠であろうか?」と思ったり思わなかったりしました。
セッションでご紹介した refined + newtypeについては、弊社のエンジニアリングブログでも記事を書かせていただきました。
従業員オンボーディングなどのHRトピックに情熱を持つ開発者の方がいらっしゃれば、ぜひいっしょに働きたいと思っています。もしご興味をお持ちなら、 @todokr にお気軽にDMをいただけるとうれしいです。
また弊社のバーチャルブースでは、 弊社と同じく BtoB SaaS領域でScalaを活用されている Alp株式会社 から @showmant さんと @pictiny さんをお招きし、「Alp x BizReach ゆるゆる会 〜SaaS 事業の2社がお互いに気になることをゆるゆる聞いてみる〜」と称してパネルディスカッションをさせていただきました。
ゆるゆるとは言いつつも、プロダクトアーキテクチャや開発プロセスなどを真面目に話したりと、堅さとゆるさがちょうどよいバランスで、終始和やかな良い雰囲気の会になったのではないかなと思います。
モデレーターは元BizReach、現Alpの @omiend さんに引き受けていただきました。Alp社のみなさまには、急なお願いにも関わらず参加をご快諾いただきまして本当にありがとうございました。
2日目のアンカンファレンスも楽しませていただきました。
「みんなScala教育どうやってますか?」のセッションでお話をさせていただきました。「うちではこんなことやってます」を複数の参加者が持ち寄る形を想定していたので、私が少々準備不足な感じになってしまったのが申し訳なかったです。それを察してか(?)、もう一人の登壇者であった @kmizu さんに「新卒研修どうでした?」など質問をいただいたり、オプトさんのエンジニア新卒研修テキストの大枠を見せていただけたりとリードしていただけたのがありがたかったです。
また「第一回 ひどいScalaコード選手権」にも参加させていただき、まさかの優勝をしてしまいました。
優勝作品はこちらです。
当初は FizzBuzz Enterprise Edition のようなひどいコードを用意できたら...と思っていたのですが、技術力の観点で早々に断念しました。「自分がScalaを学び始めたころに生み出してしまったコードを再現する」に方針転換してできたのがこれです。審査員の方には「ナチュラルボーンなひどさ」という点をご評価いただけたのではないかと思います。他の方の作品も大変コクのあるひどさで最高でした。
初代ひどいScalaコード王者としての誇りを胸に、強く生きていこうと思います。本当にありがとうございました。
今年は初のオンライン開催ということもあり、運営委員会のみなさまには沢山のご苦労があったと思うのですが、初開催とは思えないほど快適で充実した会で、登壇者としても参加者としても例年以上に楽しむことができました。委員会のみなさま、お疲れ様でした & ありがとうございました!
PostgreSQLの識別子のtruncateをFlywayのCallbackとParser Combinatorで防ぐ
PostgreSQLにおける識別子、たとえばテーブル名や外部キー制約名などは最長で63バイトに制限されています。*1
各種識別子の名前が63バイトを超えることは現実的にあまりないように思えますが、複合ユニーク制約や外部キー制約は命名ルールによってはしばしば超えてしまいそうです。*2
では63バイト以上の識別子を指定すると何が起こるでしょうか。
test=# CREATE TABLE two_all_beef_patties_special_sauce_lettuce_cheese_pickles_onions_on_a_sesameseed_bun ( test(# two_all_beef_patties_special_sauce_lettuce_cheese_pickles_onions_on_a_sesameseed_bun_id INT NOT NULL, test(# price INT NOT NULL test(# ); NOTICE: identifier "two_all_beef_patties_special_sauce_lettuce_cheese_pickles_onions_on_a_sesameseed_bun" will be truncated to "two_all_beef_patties_special_sauce_lettuce_cheese_pickles_onion" NOTICE: identifier "two_all_beef_patties_special_sauce_lettuce_cheese_pickles_onions_on_a_sesameseed_bun_id" will be truncated to "two_all_beef_patties_special_sauce_lettuce_cheese_pickles_onion" test=# CREATE UNIQUE INDEX unq_two_all_beef_patties_special_sauce_lettuce_cheese_pickles_onions_on_a_sesameseed_bun ON two_all_beef_patties_special_sauce_lettuce_cheese_pickles_onion USING btree (price); NOTICE: identifier "uniq_two_all_beef_patties_special_sauce_lettuce_cheese_pickles_onions_on_a_sesameseed_bun" will be truncated to "uniq_two_all_beef_patties_special_sauce_lettuce_cheese_pickles_"
NOTICE
として出力されていることから分かるように、これらの識別子はエラーにはなりません。識別子は63バイトにtruncateされて成功してしまいます。
これらを成功とさせず、識別子名のバイト数の超過によるtruncateを事前に防ぎたい、が今回のお題です。
FlywayのCallbackでEventをフックする
識別子名の超過は、各開発者が自身のローカル環境にDB migrationを行うタイミングで気づけるのが良さそうです。
私はmigrationに Flyway を利用しているため、このCallbackを利用して各migrationをフックし、識別子のバリデーションを行おうと思います。
手始めに、migrationをフックし、適用しようとしているSQLのパスを出力させてみます。
import java.nio.file.{Files, Path, Paths} import org.flywaydb.core.api.callback.{Callback, Context, Event} class IdentifierValidator extends Callback { override def handle(event: Event, context: Context): Unit = if (event == Event.BEFORE_EACH_MIGRATE) { val filePath = Paths.get(context.getMigrationInfo.getPhysicalLocation) println(s"about to migrate: $filePath") } override def supports(event: Event, context: Context): Boolean = event == Event.BEFORE_EACH_MIGRATE override def canHandleInTransaction(event: Event, context: Context): Boolean = event == Event.BEFORE_EACH_MIGRATE }
flywayのCalbackを利用するには Callback
インタフェースを実装します。
今回は各migrationを事前にフックしたいので、 BEFORE_EACH_MIGRATION
イベントにのみ処理を適用するようにしています。各イベントの詳細は FlywayのJavaDoc を参照してください。
このCallbackの実装をflywayに認識させるために、flywayのオプションに実装したCallbackのFQDNを指定します。
環境変数として渡すなら下記のようなイメージです。
FLYWAY_OPTIONS="-Dflyway.callbacks=IdentifierValidator -Dflyway.url=...
migrationを実行すると、各migrationファイルのパスがコンソールに出力されることが分かると思います。
識別子をパースする
各migrationをフックすることができたので、続いて識別子名のvalidationを実装します。
今回、validateの対象にしたい構文は下記の3つです。
CREATE INDEX target ...; CREATE UNIQUE INDEX target ...; ALTER TABLE ONLY xxx ADD CONSTRAINT target ...;
識別子を厳密に抽出するならちゃんとしたSQLパーサーを利用するのが良さそうですが、今回は雑に1行ごとに Parser Combinator でパースを行うようにしてみました。*3
入門には Scalaスケーラブルプログラミング(コップ本) の33章がおすすめです。
このような形でパーサーを実装します。
import scala.util.parsing.combinator.RegexParsers trait IdentifierParser extends RegexParsers { def parseIdentifier(line: String): Option[String] = parse(parser, line.toLowerCase).map(Some.apply).getOrElse(None) // create [unique] index private def index: Parser[String] = ("create" ~ opt("unique") ~ "index") ^^ (_.toString) // add constraint private def constraint: Parser[String] = "add" ~ "constraint" ^^ (_.toString) // ↑の後に来る字句をパース private def parser: Parser[String] = (index | constraint) ~> """\w+""".r ^^ (_.toString) }
parseIdentifier(line)
に1行を渡すと、ターゲットとなる識別子がSomeで返されます。ポイントは下記あたりでしょうか。
("create" ~ opt("unique") ~ "index")
は EBNFでいうcreate [ unique ] index
に相当するa ~> b
: aの結果を捨ててbを抽出。(index | constraint) ~> """\w+""".r
で indexあるいはconstraintに続く字句を抽出
識別子をvalidateする
材料が揃ったので、組み合わせてvalidationをしてみます。先に作ったIdentifierValidator#handleを下のように改修します。
class IdentifierValidator extends Callback with IdentifierParser { override def handle(event: Event, context: Context): Unit = if (event == Event.BEFORE_EACH_MIGRATE) { val filePath = Paths.get(context.getMigrationInfo.getPhysicalLocation) if (Files.isRegularFile(filePath)) { val sqlLines = Files.readAllLines(filePath).asScala val tooLongIdentifiers = sqlLines .map(parseIdentifier) .collect { case Some(ientifier) if ientifier.length > 63 => ientifier } if (tooLongIdentifiers.nonEmpty) { val msg = s"too long identifiers:\n${tooLongIdentifiers.mkString("\n")}" throw new FlywayException(msg) } } } }
63バイト以上の識別子が含まれると下記のようにFlywayExceptionが発生し、migrationを失敗させられるようになりました。
[error] (flywayMigrate) org.flywaydb.core.api.FlywayException: too long identifiers: [error] two_all_beef_patties_special_sauce_lettuce_cheese_pickles_onions_on_a_sesameseed_bun [error] two_all_beef_patties_special_sauce_lettuce_cheese_pickles_onions_on_a_sesameseed_bun_id [error] uniq_two_all_beef_patties_special_sauce_lettuce_cheese_pickles_onions_on_a_sesameseed_bun_id
今回の環境は下記のとおりです。
- PostgreSQL 9.6
- Scala 2.12.8
- flyway-core 6.0.7
- scala-parser-combinators 1.1.2
2.6.0-RC1でstableになったAkka Typedを試してみる
「A Senior Engineer's CheckList」日本語訳
期のOKRを考えるにあたって見つけた記事が面白そうだったので、勉強を兼ねて日本語(超)訳してみました。
「翻訳してよい?」と著者にメールで尋ねたところ、「Go ahead!」と爆速で返信をいただきました。感謝🙏
元記事は各項目のカテゴリやインパクトなどでのフィルタリングができるリッチなフォーマットです。
誤訳やより良い表現がありましたら、@todokrまで連絡いただけるとうれしいです:D
シニアエンジニアのチェックリスト
これはシンプルなチェックリストだが、どんなソフトウェアエンジニアにも役に立つ。特にシニアエンジニアに。
- 自分の仕事のビジネス的側面、そして何がお金を生んでいるかを理解すること。結局はこれが唯一の問題。
- 自身のチームや企業の採用活動に巻き込まれること。優れた候補者を採用するための高い基準を整備すること。
- 規模と拡張性、問題の範囲にふさわしいシステムをデザイン・開発すること。オーバーエンジニアリングを避ける。
- 問題と事態の核心にたどり着くまですべてに質問し、「なぜ」について繰り返し尋ねること。
- 責任とオーナーシップを他者から引き受けること。
- 会社が求めることを理解し、明確な像とよいデリバリ計画を持った、インパクトのあるプロジェクトを最低一つは率いること。
- ふわっとした課題をクリアにするよう努力すること。
- 他のチームとの関係を築き、信用を育てること。
- 自身の見解に固執しないこと。他者に耳を傾け、問題の見方や有効な解決策が複数あることを受け入れる。
- いくつものプロジェクトに助言者、レビュワー、メンターとして巻き込まれること。
- 「エクストリーム・オーナーシップ」の原則に従うこと。 (訳注: おそらくこのトークで話されていることか Extreme Ownership | Jocko Willink | TEDxUniversityofNevada - YouTube)
- 強力なメンターを持ち、自身の舵取りと会社での成長を支援してもらうこと。
- 高いリスク、高い見返りのあるプロジェクトに取り組むこと。
- 自分のチームが使っている技術についての高い専門性を持つ努力をすること。
- マネージャーに、ストレッチが要るプロジェクトを要求すること。またはそのような自分向けのプロジェクトを探す手伝いをすること。
- マネージャーのゴール、そして自身の仕事をそれにどのように適応させるかをマネージャーと議論すること。
- 先輩、同僚、後輩との関係性を育むために、時間を効果的に投資すること。
- 何人かのジュニアなエンジニアのメンターになること。
- 自身のチーム/企業のドメインについての知識の幅を広げること。
- 1on1を率先すること。次回の1on1にむけてトピックリストを管理すること。
- 予め解決案を持ったうえで、マネージャーと問題について議論すること。
- 技術的な知識の幅を広げること。
- 小さなプロトタイプを作り、新しい技術を調査すること。
- 毎年何冊かの技術書を読むこと。
- キラキラした大きめの新技術の本番投入を提案する前に、その長所と短所を徹底的に理解すること。
- マネージャーとの定期的な1on1を設定すること。
- 2階層上のマネージャーとの定期的な1on1を設定すること。
- [リマインド] 1on1は状況報告ではない。
- マネージャーを私生活に巻き込むこと(ちょっとだけ)。
- マネージャーからのフィードバックを積極的に求めること。
- 自身が関わっていることについて、マネージャーに都度伝えること。だが不必要な細部にこだわってはいけない。
- 自身の仕事をブロックするものについて、マネージャーに都度伝えること。
- 一緒に働くことが難しい人について、マネージャーに都度伝えること。
- マネージャーに建設的なフィードバックをすること。
- オーバーワークをしているなら、それをマネージャーに知らせること。
- 自身の能力を活用できていないなら、活用できる領域を見つけてもらうようマネージャーに頼むこと。
- もし無能だったり怠慢なマネージャーを持ったなら、マネージャーに自身の期待を伝えること。
- もしマイクロマネジメントをするマネージャーを持ったなら、マネージャーに自身の期待を伝えること。
- もし暴言を吐くマネージャーを持ったなら、証拠を持って2階層上のマネージャーや人事部と話すこと。
- もしマネージャーも2階層上のマネージャーも無能なら、チームや会社を変えること。
- もしマネージャーとの間に心のこもった関係性がないなら、チームや会社を変えること。
- [リマインド] レバレッジ = 生んだインパクト/使った時間。 レバレッジを効率の物差しとして使う。
- 改善したい対象を測定すること。測定可能になるよう努力すること。
- 高いリスクを持ったプロジェクトの「見える化」レベルを高く保つこと。
- 困った人たちを扱うために、マネージャーやメンターと話すこと。
- 困った人たちを扱うために、最初の原則に戻ること。
- 他のエンジニアが自分に接触できるようにしておくこと。
- デリバリに大きく傾いたとしても、品質について妥協しないこと。必要があれば突き返すこと。
- コードやシステム、アーキテクチャを絶え間なくシンプルにすること。
- 高い品質の仕事を他者に求めること。ただし現実的でいること。
- 開発のコストが増加を続ける場合、コードやシステム、アーキテクチャの技術的負債の返済を優先すること。
- 広範囲をドキュメント化し、それを他者にも求めること。"how"より"why"を記述すること。
- 政治を避けること。ただ自分の仕事を保証する適切な人を持つこと。
- 政治的な問題を扱う時には、最初の原則に戻ること。
- もしチームや会社の文化のせいで政治がはびこるようなら、チームや会社を変えること。
- オフィスのゴシップに巻き込まれないようにすること。
- 余力がなさすぎる状態を避け、成果を出すこと。
- 前からあるコードやシステムに敬意を払うこと。プロダクションの全てのコードや安全装置には理由がある。
- 大規模なリファクタを提案する前に、自身がシステムを深く理解しているかを確かめること。
- 主要なシステムをシンプルにリファクタしたくなる衝動に抵抗すること。結局はしばらくしてから同じように複雑なシステムになってしまうリスクがあるため。
ScalaをGraalVMでネイティブイメージにコンパイルしてAWS Lambdaでサクサク動かす
このエントリは ただの集団 Advent Calendar 2018 の18日目です。昨日は ugdark さんの 職種としてのスクラムマスターを調べてみた でした。
普段はScalaアプリケーションをECSやEC2で動かすことが多いのですが、ものによってはPythonやnode.jsでAWS Lambdaの関数ハンドラを書くこともあります。脳内コンテキストスイッチの抑制を考えるとLambda関数ハンドラもScalaで書きたいところですが、コールドスタート時におけるJVM起動のオーバーヘッドなどもあることから、今までにScalaでLambdaという選択は採ることがありませんでした。
そんな中、先日にGraalVMにネイティブイメージをコンパイルする機能があることを知りました。これを使ってScalaコードを実行可能なバイナリにコンパイルし、Lambdaでサクサクと動かしてみようと思います。
GraalVM
GraalVMはJSやPython、Ruby、R、JavaやScala、Kotlin、ClojureといったJVM言語、LLVM言語など様々な言語を実行可能で、ある言語から別の言語をオーバーヘッドなしに呼び出せるpolyglot
なVMです。
これはこれで大変面白いのですが、今回注目したいのはネイティブイメージのコンパイルを行う機能です。これによってJVM言語のアプリケーションを実行可能なバイナリにコンパイルすることで、起動時間の短縮やメモリフットプリントの削減などが期待できるようです。
ネイティブイメージの生成時にはAOT(ahead-of-time)コンパイルを行うため、動的にクラスロードを行うアプリケーションはコンパイルできないなどの制約もあるようですが、Lambda関数ハンドラで行うような処理では問題になることは少ないかも知れません。
AWS Lambda カスタムランタイム
先日のre:Invent 2018で発表されたのがAWS Lambdaのカスタムランタイムです。これによって、あらゆる言語、あらゆる処理系でLambda関数ハンドラが書けるようになりました。
上記のドキュメントから雑に要約すると、カスタムランタイムを動かすために必要なものは以下のとおりです。
- エントリーポイントとなるbootstrapの用意
- 初期化処理の実装
- イベントループの実装
bootstrap
エントリーポイントとなるbootstrap
という名前の実行可能なファイルを用意します。このファイルはLambdaインスタンス作成時に実行されます。
bootstrapは別のファイルを呼び出して実際の処理を委譲してもよいですし、このファイル単体でイベントループを実行しても大丈夫です。
初期化処理
Lambdaインスタンスの実行時に1回だけ実行される処理です。環境変数経由でセッティングを取得したり、DBとのコネクション作成などの初期化処理を必要に応じて行います。
イベントループの実装
Lambdaインスタンスが受け取るイベントを処理するループを実装します。ひとまず動かすためには、
next invocation API
にGETし、リクエストIDやリクエストボディなどが含まれたイベントデータを取得する処理の実装- イベントを処理するLambda関数ハンドラの実装
- 処理結果を
invocation response API
にPOSTする処理を実装 - エラー時に
invocation error API
にPOSTする処理を実装
あたりが必要です。
カスタムランタイムをScalaで実装する
では上で見たようなカスタムランタイムをScalaで実装します。今回はbootstrapファイルの中でイベントループも実装するようにします。
bootstrapの実装
package bootstrap import play.api.libs.json._ import scalaj.http._ object Main { def main(args: Array[String]): Unit = { // 初期化処理 val runtime = System.getenv("AWS_LAMBDA_RUNTIME_API") try { println(s"runtime: $runtime") } catch { case e: Exception => val message = Json.obj("errorMessage" -> e.getMessage, "errorType" -> e.getClass.getName).toString Http(s"http://$runtime/2018-06-01/runtime/init/error").postData(message).asString } // イベントループ while (true) { // イベントデータの取得 val HttpResponse(body, _, headers) = Http(s"http://$runtime/2018-06-01/runtime/invocation/next").asString val requestId = headers("lambda-runtime-aws-request-id").head try { val name = (Json.parse(body) \ "name").get val responseJson = Json.obj("message" -> s"Hello, $name!").toString // レスポンスデータの返却 Http(s"http://$runtime/2018-06-01/runtime/invocation/$requestId/response").postData(responseJson).asString } catch { case e: Exception => println(e.getClass.getName) println(e.getMessage) val message = Json.obj("errorMessage" -> e.getMessage, "errorType" -> e.getClass.getName).toString Http(s"http://$runtime/2018-06-01/runtime/invocation/$requestId/error").postData(message).asString } } } }
next invoke APIなどを呼び出すためにScalaJ-HTTPを、JSONのシリアライズ、デシリアライズにはplay-jsonを使用しています。
やっていることは簡単で、リクエストされたJSON {"name": "todokr"}
から{"message": "Hello, todokr!"}
というレスポンスのJSONを返しているだけです。
sbt-assemblyプラグインの追加
GraalVMでネイティブイメージを生成するnative-image
コマンドは入力としてFat Jarを受け取ります。
今回、Jarの生成にはsbt-assemblyを使います。project
以下にassembly.sbt
というファイルを用意し、sbt-assembly
をsbtプラグインの依存関係に追加します。
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
GraalVMのDockerfileを用意する
MacではLambda実行環境であるLinux向けのネイティブイメージを生成できないので、GraalVMをDockerで動かして利用します。
(このあたりは@k2nakamuraさんのこの記事に助けられました。ありがとうございます。)
このようなDockerfileを用意します。
FROM oracle/graalvm-ce:1.0.0-rc10 RUN yum install -y java-1.8.0-openjdk-headless \ && update-alternatives --set java $JAVA_HOME/bin/java \ && mv $JAVA_HOME/jre/lib/security/cacerts $JAVA_HOME/jre/lib/security/cacerts.bak \ && ln -s /usr/lib/jvm/jre-1.8.0/lib/security/cacerts $JAVA_HOME/jre/lib/security/cacerts CMD tail -f /dev/null
build.sbtを編集する
ネイティブイメージの生成のため、build.sbtを編集します。
assemblyタスクのセッティング
jarのエントリーポイントの指定や生成されるjarの名前をsettings内で指定します。
mainClass in assembly := Some("bootstrap.Main"), assemblyJarName in assembly := s"scala-graalvm-lambda_${version.value}.jar",
GraalVMのdocker imageをビルド/起動するsbtカスタムタスクを用意
シェルスクリプトでやってもよいのですが、せっかくなのでsbtのカスタムタスクにしてみます。ビルド/起動済みかの判定はgrepのexit codeで判定するなどちょっと泥臭い感じです。scala.sys.process.__
が大活躍してます。
runBuildServer
タスクはbuildContainer
タスクへの依存を宣言しています。
lazy val buildContainer = taskKey[Unit]("Build docker image of GraalVM") lazy val runBuildServer = taskKey[Unit]("Run GraalVM server for build native image.") ... buildContainer := { val exitCode = ("docker images" #| "grep graal-build-img").!(ProcessLogger(_ => ())) if (exitCode == 1) { println("Build GraalVM container...") "docker build -f Dockerfile -t graal-build-img .".! } else println("Container is already built.") }, runBuildServer := { buildContainer.value val exitCode = ("docker ps" #| "grep graal-builder$").!(ProcessLogger(_ => ())) if (exitCode == 1) { println("Start build server...") "docker run --name graal-builder -dt graal-build-img:latest".! } else println("Build server is already running.") },
GraalVMでネイティブイメージをコンパイルし、zipに固めるカスタムタスクの用意
せっかくなのでこちらもシェルスクリプトではなくsbtのカスタムタスクにします。clean
とassembly
、runBuildServer
に依存を宣言しています。
nativeCompile := { clean.value assembly.value runBuildServer.value val jarName = (assemblyJarName in assembly).value (s"docker cp target/scala-2.12/$jarName graal-builder:server.jar" #&& "time docker exec graal-builder native-image -H:+ReportUnsupportedElementsAtRuntime -H:EnableURLProtocols=http,https -J-Xmx3G -J-Xms3G --no-server -jar server.jar" #&& "docker cp graal-builder:server target/bootstrap" #&& "docker cp graal-builder:/opt/graalvm-ce-1.0.0-rc10/jre/lib/amd64/libsunec.so target/libsunec.so" #&& "zip -j target/bundle.zip target/bootstrap target/libsunec.so").! }
最終的にbuild.sbtはこのようになりました。
このsbt nativeCompile
を実行すると、target/
にbundle.zip
というzipファイルが生成されるはずです。
AWS Lambdaへのデプロイ
AWSコンソールからLambda関数を作成し、生成したzipをアップロードします。あとはAPI Gatewayと接続するなどいい感じにやっておきます。
実行速度を見てみる
Cloudwatch LogsのREPORT
の行だけを抜き出すとこのような感じです。128MBの環境でもInit込みで150msほど、暖まっていると1.2 ~ 1.5msほど(!)で処理できていることが分かります。
REPORT RequestId: aa66a4c8-020b-11e9-8118-1f1b36c60d1d Init Duration: 122.14 ms Duration: 153.49 ms Billed Duration: 300 ms Memory Size: 128 MB Max Memory Used: 52 MB REPORT RequestId: ab5b8c12-020b-11e9-8b92-a1f863edf3ed Duration: 1.26 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 52 MB REPORT RequestId: af655404-020b-11e9-abba-97121f20de35 Duration: 1.56 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 52 MB REPORT RequestId: c70d15c1-020b-11e9-950c-4b268a6b6d47 Duration: 1.22 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 53 MB
このくらいサクサク動くのであれば、プロダクションでもScala x Lambdaが選択肢に挙がってきそうですね。
サンプルコードの全体はこちらにあります。
sbtのカスタムタスクに入門して、テスト失敗時に爆発音を鳴らしてみる
sbtではアプリケーションの起動やコンパイルといった予め定義済みのタスクの他に、利用者が独自のタスク「カスタムタスク」を定義することができます。
SassやJSなどのトランスパイルやAPIドキュメントの生成など、開発に付随する定型的作業はsbtのカスタムタスクにしてあげると嬉しいかもしれません。
この記事ではsbtの初学者向けに、最小構成のカスタムタスクを作ってみるところから、別のタスクの結果に応じた処理を行うカスタムタスクの作り方までをさらっとメモしていきます。
初めてのカスタムタスク
カスタムタスクを使い始めるまでに必要なことは2つです。
- カスタムタスクのキーを宣言し、タスクとして使えるキーを定義
- カスタムタスクを実装し、キーと実装を紐付ける
$ sbt new sbt/scala-seed.g8
で生成したプロジェクトをベースに、build.sbt
に手を入れてカスタムタスクを実装してみましょう。
1.カスタムタスクのキーを宣言する
まずは「現在の時刻をsbtコンソールに出力する」だけの簡単なタスクを作ってみます。
sbtのキーには、
- プロジェクトのロード時に一度だけ値が計算され、保存された結果が使用されるキーである
SettingKey
- 実行のたびに再計算される「タスク」を呼び出すキーである
TaskKey
- コマンドラインの引数を入力として受け取るタスクキーである
InputKey
の3種類があります。今回はタスクを扱いたいので、下記のようにタスクキーの宣言を行います。
import Dependencies._ // キーの宣言 lazy val sampleDateTimeTask = taskKey[Unit]("A sample task shows current date time.") lazy val root = (project in file(".")). settings( ...
.sbt
ファイル内にはScalaのコードを記載することが可能なので、val
やdef
を用いてプロジェクトの設定に必要な変数などを宣言することができます。初期化順序にまつわる問題を避けるためにlazy
にしておくのが望ましいでしょう。
なお今回は触れませんが、object
やclass
はbuild.sbtのトップレベルには定義できないので、必要な場合はproject/
の下に.scalaのソースを配置します。
タスクキーを宣言するためにはtaskKey[T]
を用います。taskKey[T]の型パラメータTはタスクの結果の型を表します。今回は文字列をコンソールに出力したいだけなので、返り値の方はUnitです。リソースの生成などを行うタスクを実装する場合はFileオブジェクトなどを返すことになるかもしれません。
引数には、そのタスクの解説文を文字列として渡します。ここで渡した文字列はタスク一覧を表示するタスクであるtasks -v
タスクなどの結果に含められます。
sbt:sbt-sandbox> tasks -v This is a list of tasks defined for the current project. It does not list the scopes the tasks are defined in; use the 'inspect' command for that. Tasks produce values. Use the 'show' command to run the task and print the resulting value. bgRun Start an application's default main class as a background job bgRunMain Start a provided main class as a background job ... sampleDateTimeTask A sample task shows current date time. ... update Resolves and optionally retrieves dependencies, producing a report.
キーの定義はbuild.sbt内のどこで書かれてもプロジェクトの設定より前に評価されますが、混乱を避けるために冒頭の方で宣言しておくと良いかもしれません。
2. カスタムタスクを実装する
キーを宣言したら次は実装を与えます。
import Dependencies._ import java.time.LocalDateTime // importを追加 lazy val sampleDateTimeTask = taskKey[Unit]("A sample task shows current date time.") lazy val root = (project in file(".")). settings( ... sampleDateTimeTask := println(LocalDateTime.now) // sampleDateTimeTaskの実装 )
.settings()
内に渡しているのはsbtのDSLで表現された「セッティング式」で、key := body
の左辺がキー、右辺がセッティング本文です。
今回は先に宣言したタスクキーに対して、右辺で実装を与えています。
ここまでできたらsbtシェルからカスタムタスクを呼び出すことができます。既にsbtが立ち上がっているならreload
でbuild.sbt
を再度ローディングし、下記を実行してみましょう。
sbt:sbt-sandbox> reload sbt:sbt-sandbox> sampleDateTimeTask 2018-12-10T11:13:19.659 sbt:sbt-sandbox> sampleDateTimeTask 2018-12-10T11:15:21.048
sbtコンソールに時刻が出力されるはずです。また実行のたびに時刻が変わることから、呼び出しのたびに再計算を行う「タスク」になっていることが分かります。 これでひとまず最小構成の簡単なタスクを定義することができました。
テストの失敗時に音を鳴らすカスタムタスクを作ってみる
次はもう少し意味のあるタスクを作ってみましょう。より失敗感が出るよう、テスト失敗時に爆発音を鳴らすboomTest
というタスクを作ってみたいと思います。
.value
でタスク間の依存を表現する
先に作った簡単なタスクとboomTest
が異なるのは、testの実行結果を何らかの手段で受け取り、その結果を元に挙動を分岐させる必要がある点です。つまりboomTest
のタスクはtest
のタスクの結果に依存する訳です。
このように「別のタスクに依存するタスク」表現する場合には.value
という特殊なメソッドを利用します。具体的な例で見てみましょう。
lazy val taskA = taskKey[Int]("Execute A and return magic number") lazy val taskB = taskKey[Unit]("Execute B") ... settings( ... taskA := { println("taskA is Done") 42 }, taskB := { taskA.value println("taskB is Done") } ) }
上記の例ではtaskA
とtaskB
という2つのカスタムタスクを定義しています。taskBはtaskAに依存するため、セッティング本文の中でtaskA.value
のようにtaskBへの依存性を表現しています。そのためそれぞれの実行結果は下記のようになります。
sbt:sbt-sandbox> taskA taskA is Done sbt:sbt-sandbox> taskB taskA is Done taskB is Done
注意が必要なのは、.value
は普通のメソッド呼び出しではなく、マクロを使ってタスク本体から「持ち上げ」るための構文である点です。上記の例だと、taskA.value
はtaskBのセッティング本文のどこに書かれていようとも、taskBの本文より先に実行されます。例えば上記のtaskBを少し改修し、
taskB := {
println("taskB is Done")
taskA.value
}
と呼び出しの順序を入れ替えたとしても、taskAの「taskA is Done」が必ず先に表示されます。
タスクの実行結果を取得する
「別のタスクに依存するタスク」が表現できるようになったので、次は別のタスクの実行結果にアクセスするようにしてみましょう。
あるタスクの実行結果には、taskX.result.value
とアクセスが可能です。この場合、.value
はsbt.Result
型のインスタンスを返します。
Resultはタスクの正常終了を表すValue
と異常終了を表すInc
からなる代数的データ型なので、パターンマッチで簡単に正常/異常時の処理を分岐させることができます。
lazy val taskA = taskKey[Int]("Execute A and return magic number") lazy val taskB = taskKey[Unit]("Execute B") ... settings( ... taskA := { println("taskA is Done") 42 }, taskB := { taskA.result.value match { case Inc(cause) => // タスクが異常終了した場合 println("oops, taskA is incomplete") throw cause case Value(value) => // タスクが正常終了した場合 println("taskA's answer is " + value) } println("taskB is Done") } )
taskAは異常終了しないので、これを実行すると下記のようになるでしょう。
sbt:sbt-sandbox> taskB taskA is Done taskA's answer is 42 taskB is Done
test
タスクの実行時には、テストに失敗すると.result.value
がInc
を返すので、これを使うとテストの失敗をフックすることができそうです。
組み合わせてテスト失敗時に音を鳴らす
実装に必要な知識が揃ったので、いよいよboomTest
を作り始めてみましょう。こんな感じのものです。
まずは失敗時に鳴らしたい音をsrc/test/resources/
以下に用意します。javax.sound
パッケージで扱いやすいよう、フォーマットはwaveかaiffが良いかと思います。私はfreesoundのこの音源を利用しました。
次にboomTest
タスクの雛形を作ります。例によってタスクキーの宣言とタスクの本体を用意します。
val boomTest = taskKey[Unit]("Execute test & play boom sound on failure") ... settings( ... boomTest := { (test in Test).result.value match { case Inc(cause) => println("boom! ☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆") // 音を鳴らす処理は後ほど実装 throw cause case Value(value) => value } } )
失敗するテストを用意してboomTest
を実行すると、ちゃんと失敗がフックできていることが確認できるはずです。
sbt:sbt-sandbox> boomTest [info] HelloSpec: [info] The Hello object [info] - should say hello *** FAILED *** [info] "hello[!!!]" did not equal "hello[]" (HelloSpec.scala:7) [info] Run completed in 160 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 0, failed 1, canceled 0, ignored 0, pending 0 [info] *** 1 TEST FAILED *** boom! ☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆ [error] Failed tests: [error] example.HelloSpec [error] (Test / test) sbt.TestsFailedException: Tests unsuccessful [error] Total time: 0 s, completed Dec 10, 2018 4:32:29 PM
続いて音を鳴らす処理を追加していきます。sbtには各リソースにアクセスするための便利な関数などが用意されているので、なるべくそれらを使って実装したいと思います。
... boomTest := { (test in Test).result.value match { case Inc(cause) => val audioFile = (resourceDirectory in Test).value / "boom.wav" Using.fileInputStream(audioFile) { stream => val ais = AudioSystem.getAudioInputStream(stream) val format = ais.getFormat val dataLine = new DataLine.Info(classOf[Clip], format) val clip = AudioSystem.getLine(dataLine).asInstanceOf[Clip] clip.open(ais) clip.loop(0) clip.start() while (clip.isRunning) { Thread.sleep(100) } } throw cause case Value(value) => value } }, ...
val audioFile = (resourceDirectory in Test).value / "boom.wav"
ではsrc/test/resources/
以下のwaveファイルをjava.io.Fileオブジェクトとして取得しています。resourceDirectory
以外にもsourceDirectory
やclassDirectory
など各種ディレクトリにアクセスするためのセッティングキーが用意されています。
Using.fileInputStream
はsbt.ioパッケージに用意されている関数で、Loanパターンを実現するためのものです。
ちなみにsbt.ioはライブラリとして切り出されているので、この他の各種便利な機能をアプリケーションで利用することもできます。
stream =>
移行ではAudioInputStreamからClipを生成し、ループなしで一度だけ再生をしています。
これで、テストに失敗すると爆発音が流れるはずです。
testタスク実行時に爆発音を鳴らす
ここまで来るとboomTest
の失敗時だけでなく、test
の失敗時にも爆発音を鳴らしたいですよね?なので、(test in Test)
に:=
で実装を定義し直し、testタスクをoverrideします。
...
(test in Test) := {
(test in Test).result.value match {
case Inc(cause) =>
...
一見testタスクが無限ループしてしまうように見えますが、.value
はタスク依存性を表記しているだけなので、sbtのタスクエンジンはtestタスクを一度だけ実行してくれます。
(このようなoverrideの方法はドキュメントに見つけられなかったのですが、用例はここ やここなどにあります。ドキュメントに記載があれば教えていただけるとありがたいです...)
これでtest
タスクでもテスト失敗時に音を鳴らすことができました。
Scrum Inc.のスクラムマスター研修を受けてLicensed Scrum Master認定を取得しました
先日、Scrum Inc.による2日間のスクラムマスター研修を受け、Licensed Scrum Masterの認定を取得しました。Scrum Inc.はスクラムの共同創案者であるジェフ・サザーランド博士の会社らしいです。
研修の講師はScrum Inc.のPresident of HardwareであるJoe Justiceさんでした。休み時間にもたびたび質問をさせていただいたのですが、快く回答いただいてとてもありがたかったです。
本日オンライン試験に無事合格し、晴れて認定をいただくことができました:D
研修では座学以上に体験学習を楽しみにしていたのですが、座学もとても濃い内容で、スクラムのルールについても「なぜそうするのか?」、「このイベントにはどういう意図があるのか?」などを改めて体系的に学ぶことができたのがありがたかったです。
他にもまた、有名な「紙飛行機量産」などスクラムの効果を実感する各種ワークショップや、スクラムマスターがおかれる様々な状況について「自分だったらこうする」をチームで議論しあうケースメソッドといった体験学習も多くあり、いろいろな発見がありました。
これからのチームのベロシティの成長が楽しみです!(とはいっても今月からスクラムができない規模の小さな新チームになってしまった...)
ちなみにJoeさんがやっている自動車ベンチャーの話も超面白かったです。 http://wikispeed.org/
JJUG CCC 2017 Fallに登壇しました
「新しいプログラミング言語の学び方 HTTPサーバーを作って学ぶ Java, Scala, Clojure」という話を弊社ビズリーチのスポンサー枠で40分間させていただきました。
どのくらい需要がある話なのか正直不安でしたが、ありがたいことに定員が200人強の部屋がほぼ埋まるくらいの方に来ていただきました。来ていただいたみなさんありがとうございました。
公開した資料もはてブ440くらいついてました。ありがたいです。
Clojureでの実装は@_ayato_pさんにレビューいただきました。 コードについてはもちろん、clean-nsやmagic requiresといったclj-refactor.elの便利機能など、日々業務でClojureを書いている方からの実践的なアドバイスを頂けて大変ありがたかったです。改めてありがとうございます。
HTTPサーバーを作るというレッスンは、新卒一年目の時に同期とやってみたのが最初です。
同期の師匠に「ウェブエンジニアなんだからウェブサーバーは作れて当然でしょ!」と言われたのですが、当時は素直に「ふむふむなるほどそういうものなんだな」と思っていました笑
アプリケーションより低いレイヤーの技術に興味を持ったり、何かあったときにフレームワークetcの中身を見てみるようになったのはこのレッスンの影響が大きかったかもしれません。
「クローリングハック」という本を共著しました
クローリングハック あらゆるWebサイトをクロールするための実践テクニック
- 作者: 竹添直樹,島本多可子,田所駿佑,萩野貴拓,川上桃子
- 出版社/メーカー: 翔泳社
- 発売日: 2017/09/14
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
前チームの上司である@takezoenさんや@chibochibo03さんや同僚といっしょに書いた「クローリングハック あらゆるWebサイトをクロールするための実践テクニック」という本が、明日14日に翔泳社さんから発売されます。
僕は3章の「文字化けと戦う」と他の章のコラムをちょこちょこ書いたりしました。
ありがたいことにamazonの「Webプログラミング」カテゴリの1位にもなったみたいです🙏
この本では、クローラーの仕組みやHTTPについてなどの基礎から、文字化けを防ぐテクニックやスクレイピングの肝、認証が必要なサイトやSPAのクローリング、大規模なサイトを効率的にクロールするテクニック、謎挙動サイトにどう対応するかなど、そこそこ以上の規模の実用的なクローラーを作る上でおさえておきたいポイントについて解説しています。
クローラーの開発に携わる方はもちろん、HTTPやHTMLなどWebの仕組みを深く知りたいWeb制作者の方にとっても役立つ内容になったのではないかなと思います。
また書籍をご購入いただいた方は翔泳社さんのサイトから応募していただくと、curlのオプションをまとめた「curlの極意」という特典PDF(12p)が受け取れますのでぜひどうぞ(^q^)