ラベル Java の投稿を表示しています。 すべての投稿を表示
ラベル Java の投稿を表示しています。 すべての投稿を表示

2024/10/01

Batch実行時のエラー「java.lang.NullPointerException: Cannot invoke "String.lastIndexOf(int)" because "endpoint" is null」の対処

事象

GAE/JでBatchジョブを起動しようとBatchServiceClient.create()をコールすると、以下のエラーが発生しました。

java.lang.NullPointerException: Cannot invoke "String.lastIndexOf(int)" because "endpoint" is null

ソースコードはGoogleのスニペットどおりで、ヌルポなど発生するわけがないはずです。しかし何度実行しても、ビルドしなおしても状況に変化はありません。


調査

BatchServiceClient.create()ではどのようにしてBatchServiceClientが生成されているのかわかりませんが、BatchServiceClient.create(BatchServiceSettings settings)メソッドを使用して自分でBatchServiceSettingsを生成して渡せば、生成されるBatchServiceClientの内容をある程度推測できそうです。
とりあえず以下のようなスニペットでBatchServiceSettings.newBuilder()でデフォルト設定のBatchServiceSettingsを生成し、別のBatchジョブの起動に成功しているプロジェクトとBatchServiceSettingsの内容を比較してみました。

	BatchServiceSettings settings = BatchServiceSettings.newBuilder().create();

生成されたBatchServiceSettingsの内容で、有意な差異は以下のとおりでした。

成功しているプロジェクト :
	endpoint=batch.googleapis.com:443,
失敗するプロジェクト :
	 endpoint=null,

エラーメッセージどおりですが、endpointがnullです。


endpointは文字列ですし、BatchServiceSettings.Builder.setEntpoint(String endpoint)で設定できます。そこで強制的にendpointに成功しているプロジェクトと同じ値をセットしてBatchServiceSettingsを生成し、それを渡してBatchServiceClientを生成してみると、発生するエラーが以下に変わりました。

com.google.api.gax.rpc.PermissionDeniedException: io.grpc.StatusRuntimeException: PERMISSION_DENIED: Batch API has not been used in project プロジェクト名 before or it is disabled.

Batchが有効になっていない...


解決方法

その1

BatchのAPIを有効にします。GCPコンソールからなら以下の手順になります。

  1. GCPコンソールの「APIとサービス」-「ライブラリ」を表示。
  2. 「APIとサービス」にBatchと入力してBatchを探す。
  3. 検索結果のリストから「Batch API」をクリック。
  4. Batch APIのページの「有効にする」をクリック。



Batchは手動で有効化する必要があったのを忘れていただけでした。そして最初に目にするエラーメッセージはそれを連想しにくいものなので、気づけないというワナもあったということでした。


その2

Batchを有効化し、BatchServiceClient.create()のコールをGoogleのスニペットどおりに戻してみたのですが、相変わらずこの記事の最初と同じヌルポが発生したままです。
とりあえず上記調査と同様にendpointを強制的に設定することで、Batchジョブの実行はできるようになりました。

しかしなぜプロジェクトによって挙動が違うのかは不明のままです。

2024/02/09

GAEのインスタンス起動時間 #2

動機

Java17 + レガシーバンドルサービスを実装したところ、GAEインスタンスのスピンアップが若干遅いと感じました。いつの間にかJava21もプレビューになっていましたので、あわせてインスタンス起動時間を再測定してみました。

また前回(GAEのインスタンス起動時間)はログで確認していたのですが、その方法の精度にも疑問を感じたので、測定方法も対象も変更しています。


今回はJava8がすでにサポート終了しているので、対象外としました。


測定条件・方法

  • リージョン : 大阪
  • ネットワーク : NTT Flets光 隼、実効300Mbps程度
  • インスタンスクラスはF1。
  • ServletのアプリはGoogleのサンプルに入っていたHelloWorld。
  • SpringBootのアプリはGoogleのサンプルに入っていたHelloWorldそのままなので、バージョンは2.7.18。
  • レガシーバンドルサービスは、Googleのサンプルは余計なコードが入っていたのでpom.xmlのみ参考にし、Hello World相当を自作。
  • GoもHello World相当を自作、今回はフレームワークなしのパターンのみ。
  • ブラウザからAjaxでリクエストし、JavaScriptでレスポンスが返るまでの時間を測定。
  • スピンアップあり/なしの2パターンで、それぞれ10回測定し、平均値を算出。
  • 前記2パターンの差から、スピンアップ+VM起動+フレームワーク起動の時間を計算。

測定結果

単位はすべてmsです。測定結果をtableにすると横長になってレイアウトが崩れたので、画像です。
まずはスピンアップあり。

続いてスピンアップなし。

そして、スピンアップ+VM起動+フレームワーク起動の時間。
Servletspringレガシー
Java11625.24728.62098.6
Java17599.84769.21655.0
Java21630.24622.92451.5
Go1.20176.9--


所感

起動時間は 17<11≒21。わずかとはいえ、最新の21で悪化しているという意外な結果に。まだプレビューなのも影響しているのでしょうか。

SpringBoot

前回と比べてバージョンが2.6.6→2.7.18と変化していますが、結果はほぼ同じ。他はJava21で遅いのに、なぜかspringだけはJava21が最速と、一致しません。なぜ...

レガシーバンドルサービス

遅い気がしたのは気のせいではありませんでした。Java8のスピンアップが遅かったのはもしかして、Googleのカスタマイズのせいだったのではという疑惑が生じます。そしてJava21での遅延が目立って大きくなっています。残念過ぎる...

2024/01/14

ビルドエラー "ERROR: failed to initialize analyzer: getting previous image" の対処 [GAE]

事象

久しぶりにGAEのプロジェクトをビルドすると、mavenで以下のエラーに遭遇しました。
このプロジェクトはappengine-maven-pluginを利用してビルドしています。

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:27 min
[INFO] Finished at: 2024-01-xxTxx:xx:xx+09:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal com.google.cloud.tools:appengine-maven-plugin:2.5.0:deploy (default-cli) on project プロジェクトID.サービスID: App Engine application deployment failed: com.google.cloud.tools.appengine.operations.cloudsdk.process.ProcessHandlerException: com.google.cloud.tools.appengine.AppEngineException: Non zero exit: 1 -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException


Cloud Buildを確認すると以下のエラーが出てしました。

ERROR: failed to initialize analyzer: getting previous image: getting config file for image "asia.gcr.io/プロジェクトID/app-engine-tmp/app/サービスID/xxxxxx:latest": GET https://storage.googleapis.com/asia.artifacts.プロジェクトID.appspot.com/containers/images/sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx?access_token=REDACTED: unexpected status code 404 Not Found: <error><code>NoSuchKey</code><message>The specified key does not exist.</message><details>No such object: asia.artifacts.プロジェクトID.appspot.com/containers/images/sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</details></error>


調査

「差分ビルドしているけど前回のビルドイメージが見つからない」と言っているようです。だいぶ前ですが、Cloud Storageのビルドイメージを保存しているバケットにライフサイクルを適用して、生成から14日を過ぎたものは削除するように設定しています。その当時は14日以上空けてビルドしても問題なく再ビルド可能でした。また手作業でCloud Storageのビルドイメージを消してからのビルドも可能でした。それに差分ビルドによるビルド時間短縮の効果もわずかでした。

その後ビルド方法が変更されて差分ビルドしかしてくれなくなったようです。

ネットで探してみると「GAEのデプロイが失敗する: ERROR: (gcloud.app.deploy) Error Response: [9] Cloud build xxxxx status: FAILURE」が見つかりました。まったく同じ状況です。


解決方法

gcloud app deploy のケース

Goで作成しているサービスは上記の記事と同じくgcloud app deploy --no-cacheで回避できました。


maven appengine:deploy のケース

Javaで作成しているサービスはデプロイまでmvn appengine:deployで行っています。appengine-maven-pluginに--no-cacheと同様のパラメータがないか探してみたのですが、見当たりません。serviceAccountKeyFileという怪しいタグがありますがweb上に全く説明が見つからず、またタグ名から推測される動作も異なります。

なぜかJavaで書いたサービスはgcloud app deployだとビルド・デプロイは成功するものの、生成されるwarに依存ライブラリが含まれないので使っていませんでした。

しかし一旦gcloud app deploy --no-cacheでデプロイすればビルドイメージができるので、その後はmvn appengine:deployでデプロイできるようになりました。


2023/12/02

vsReversiアップデート

 Google Cloudのデモとして用意していたvsReversiをアップデートしました。今回はJava8→17の更新だけです。内容は特に変わっていません。Google App EngineのJava8サポート終了が予定されているので、それに備えての更新です。

Java11を飛ばして17にした理由は、単にライフサイクルがJava17のほうが長いからです。App Engineのランタイムサポートスケジュールを参照してください。Java11だと残り1年もないですね。すでにJavaのLTSバージョンは今年秋に21がリリースされていますが、これもそのうちApp Engineで使えるようになることでしょう。


2022/07/06

GAEのインスタンス起動時間

動機

GAEのスタンダード環境では、言語によってインスタンスの起動時間に差があるのは、Java8を含む第1世代VMを利用している人には、良く知られた事実です。既に起動しているインスタンスがあっても、リクエストが増えて新たにインスタンスが起動される場合にも、この起動時間がかかります。そのためインスタンス起動が遅いと、時々レスポンスが悪く感じることになります。

昨年公式に公開されたJava17がいつの間にかGAEでもプレビュー版として使えるようになっていましたので、バージョンアップで起動時間がどう変化したか確かめてみたくなりました。それにSpringの起動時間も気になります。で、言語とそのバージョン + Webフレームワークの違いで、実際のインスタンス起動時間を計ってみました。


最初に断っておきますが、私が使用する(可能性のある)組み合わせのみの測定です。


測定条件・方法

  • 基本的にアプリはGoogleのサンプルに入っていたHelloWorld、それに最低限のログを加えたもの。
  • ただしJava8/Goは手元にあった単純なアプリ。Slim3やObjectifyなどは入っていないので、結果には影響しないはず。
  • SpringBootはGoogleのサンプルに入っていたHelloWorldそのままなので、バージョンは2.6.6。
  • GoのフレームワークはGin。
  • インスタンスクラスはF1。
  • インスタンスが立ち上がっていないことをGCPコンソールで確認してから、ブラウザからリクエスト開始。
  • Cloud Loggingに記録されたログにより、リクエストの到着から、リクエストハンドラの先頭に入れたログが出るまでの時間を計算。
  • それぞれ5回測定し、平均値を算出。
  • 1回測定ごとにGCPコンソールから手動でインスタンスを停止。次の実行まで数分間放置。

測定結果

フレームワーク
なしあり
言語Java82.063-
Java111.0553.737
Java170.9393.551
Go 1.160.1760.205

単位はms。


所感

Java8

過去の経験から3秒くらいと思っていたのですが、なんか早いという結果になりました。意外でしたが、ちょっと嬉しい。

Java11

VMの起動が早いのでしょうか。フレームワークなしならVM起動してても、ほとんど待たされている感覚はありません。PHP/Pythonと同じくらいの起動時間だと思われます。


Java17

まだプレビュー版であることはご理解ください。しかしJava11より少し早くなっているのはいいですね。


SpringBoot

JavaVMに1秒+Springに3秒弱という結果でした。ブラウザを触っていても「あれ、遅いかな」と感じたころに応答が描画される感じです。これだけで十分「使えない」と判断できます。
起動時間の4秒というのは、GAEのJavaというと8しかなかった時代に、起動が遅いといわれていた状況よりも悪くなっています。Springはサーバレスという考えのなかった時代の、「起動しておいて口開けて待っている」ことが前提の時代の産物だけに、時代遅れの遺物になってしまった感があります。
サーバレスでやりたいなら、潔くSpringは捨てましょう。


Go

インタプリタもVMも存在しない、ネイティブ実行されるだけあって、爆速です。ブラウザを触っていても、インスタンス起動が起きていることを全く感じることがありません。


Gin

「フレームワークというよりライブラリ」と言われるだけあって、遅延は30ms程度と、ほとんど影響がありません。SpringBootと対照的な結果ですが、時代が違うのでサーバレスも考慮された結果なのかもしれません。


(2022/07/14追記)

現在、GAEのJava17はプレビューから一般提供に格上げされています。

2022/06/25

残念なレガシーバンドルサービス

GAE/JのJava17対応


(2024/02/26追記)
本記事の内容はすでに古くなっています。また最初の公開時に記載内容に間違いがあった可能性があります。


 久しぶりにApp Engineのドキュメントを見たら、まだプレビュー版ですが、Java17の対応が載っていて驚きました。Java17って去年秋にリリースされたばかりなので、GCPの対応はまだ先かなと想像していたのですが、予想よりずっと早いようです。Java17はJava11以来のLTSなので、注目に値します。


ランタイムの世代

GAEのランタイムには第1世代と第2世代の2種類があります。これらは実行されるランタイムのバージョンが異なるだけでなく、第1世代のみGoogleが実装した拡張機能があります。
これらの拡張機能のうち、よく使われるのは以下ではないでしょうか。
  • Memcache : オンメモリのキャッシュ
  • Search API : Googleのサーチエンジンと同じ全文検索
  • Mail API : メールの送受信
  • Users API : Googleアカウントによるユーザ認証

ランタイムを選択するうえで、これらの機能が必要かどうかも分かれ目になります。

個人的にはまだ第1世代ランタイムであるJava8を使用していて、第2世代ランタイムに移行への移行はまだです。その理由がこれらの機能を使用したいからです。


代替サービス/ライブラリ

以前は第2世代ランタイムでは、前記の第1世代ランタイムでしか提供されない機能の代わりに、他のサービス/ライブラリを使用するように、Googleは説明していました。これらの代替手段は現在でも有効で、本来はこれらの代替手段で対応するものと思います。主なものは以下のとおり。 

Memcache → Memorystore (無料枠なし)
Search API → Elasticsearch
Mail API → SendGrid/Mailgun/Mailgrid など
Users API → Identity Platform/Firebase Authentication/OpenID Connect など


Memcacheは、有料のMemorystoreが嫌なら、ちょっと極端ですが諦めてもいいでしょう。

Search APIは、代わりとして気軽に使えるものが事実上ありません。

Mail APIは、SendGridのみ無料枠が用意されています。SendGridは使いやすくて性能も十分と思います。

Users APIは優秀な代替手段が用意されており、無料枠もあるので、積極的に置き換えるべきと思います。


これらのGAEの機能と同じような状況にあったのがDatastoreですが、Datastoreはすでにサービス終了しています。代わって登場したFirestoreには「Datastore互換モード」があるので、Datastoreとほぼ同じように使えます。DatastoreはJavaライブラリが使いにくいですが、Firestoreはいくらかライブラリが使いやすくなっています。個人的にはFirestoreネイティブモードに乗り換えました。


レガシーバンドルサービスの提供

Java17対応と同時に、第2世代ランタイムに「レガシーバンドルサービス」が提供されるようになっていることに気付いて2度びっくりしました。

レガシーバンドルサービス」は第2世代ランタイムから、第1世代ランタイムのこれらの拡張機能を使用するためのライブラリのようです。これを使用することで、第2世代ランタイムも第1世代ランタイムと同様に使用することが出来るようになるようです。


しかし残念なことに、一番使いたいSearch APIは提供されないようです。

Elasticsearchがあるとはいえ、ちょっと興味本位で調査したい場合や、商用であってもサービス立ち上げ直後で利益が出せるのがいつになるか不安な状態では、安くない月額固定料金のサービスへの乗り換えは厳しいです。


Googleの本音を予想

GAEの第1世代ランタイムに用意されていた拡張機能は、Googleがランタイム(Javaの場合はJavaVM)を独自拡張して実装したのではないかと想像しています (だからランタイムの存在しないGoにはこれらの拡張機能が提供されない)。

App Engineスタンダード環境のランタイム」を読んでいるとGoogleは、第1世代ランタイムを停止して第2世代ランタイムに移行させたいのではないかと思えてきます (実際すぐに第1世代ランタイムが止まることはないことは、「レガシーランタイムの長期サポート」に説明されています)。古くなったランタイムのメンテナンスが行われなくなるのは当然なのでずっと第1世代ランタイムがサポートされることはないのですが、第2世代ランタイムへの移行を促したいのは、ランタイムの独自拡張のメンテナンスも止めたいからではないでしょうか。

なかなか移行が進まない原因を探ったら第1世代ランタイムの拡張機能が原因だったから、第2世代でも利用できるように「レガシーバンドルサービス」という形で対処したものと思われます。


Search APIの代替が用意されない理由はちょっと想像できません。Googleにとっては自社製の検索エンジンの応用らしいので、技術的には用意できなくはないはずです。政治的な理由しか考えられません。Elasticsearchに誘導したい理由は何なのでしょう。