#サービス移行 最近、すべてのサービスをCloudflareに移行しました。そこで、遭遇した落とし穴とそこから得た教訓(雑感)を記録するために、このスレッドを立ち上げました。画像は移行前と移行後の大まかな比較です。
まず、言語の選択があります。レガシーサービスはJavaで開発されていましたが、スタンドアロンアプリケーションとしては適切な選択ではありませんでした。まず、Javaアプリケーションは多くのリソースを消費し、サーバー要件も高くなるため、サーバーコストが高くなります。現在、私は2コア、4GBのAlibaba Cloud ECSインスタンスを使用しており、月額約280元です。
第二に、開発効率の面では、JavaはPHP、Node.js、Pythonなどの言語に完全に劣っており、独立系製品ではデプロイメント速度が非常に重要です。第三に、サーバーレス環境への対応はJavaScriptに劣ります。例えば、CFワーカーをサポートしていません。第四に、Javaはカーソルを使用できないため、IntelliJ IDEAへの切り替えが不可能です。
第 5 のエコシステムでは、JavaScript を使用するための興味深いパッケージが多数ありますが、Java は主にエンタープライズ アプリケーションに使用されており、独立した製品がこの分野で勝つチャンスは通常ありません。 こんなに多くの欠点があるのに、なぜ最初からJavaを選んだのでしょうか?実は、当初はPHPかNode.jsを使うつもりで、かなりの時間をかけて学習したのですが、途中で諦めてしまったり、完全に熱意が冷めてしまったりしたのです。
約2年間、これらの最適な技術スタックを何度も学び、学んでは忘れ、また学びました。ある日、ふと2年が経ち、自分のエネルギーがすべて表面的なことに浪費されていたことに気づきました。何も製品を作っておらず、当初何を作りたかったのかさえ忘れてしまっていました。そこで、ある決断をしました。知っていることを活かし、もう気にしないことです。バックエンドには古参のJavaスキル、フロントエンドにはjQuery、HTML、CSSを使いました。何よりも重要なのは、とにかく動かすことでした。
事業がほぼ安定したので(収益は不安定ですが🤡)、少し時間を取ってサービスをNode.jsに移行し、JavaScriptエコシステムを取り入れようと考えています。まずは、問題を見つけやすいTypeScriptを必ず使用します。以前Javaを使っていたので、TypeScriptで問題ありません。次にバックエンドフレームワークです。Javaを使っていた頃は、常にSpring Bootを使っていました。TypeScriptに移行した後、最初はJavaによく似たNestJSを使うことを考えていましたが、後にその考えは断念しました。
主な理由は次のとおりです。まず、NestJS は Spring Boot と同様に非常に強力で機能豊富ですが、その分動作が重く、学習曲線も急峻です。次に、CF ワーカーと Node.js が異なるランタイム環境を使用し、多くの Node.js パッケージに互換性がないことを理解するのに長い時間がかかりました。目標は CF への移行であるため、この点は考慮しませんでした。 その後、honojsという新しいフレームワークを発見し、いろいろ調べた結果、最終的にこれを選択しました。理由は2つあります。
まず、非常に軽量でシンプルで、精神的な負担も最小限です。私のビジネスには十分です。次に、ネイティブNode.js、Bundle、CF Worker、AWS、Vercelなど、さまざまなランタイム環境で簡単に実行できるため、プラットフォームへの依存を最小限に抑えることができます。 それからORMフレームワークですが、JavaではMyBatis Plusを使っていました。移行後に似たようなものを見つけたいと思っています。
TypeScript用のORMフレームワークの選択肢はあまり多くないようです。私は主にPrismaとDizzleを調査しました。個人的にはPrismaの方が強力で、Dizzleの方がシンプルだと感じたので、学習曲線が比較的緩やかなDizzleを選択しました。これは、現時点では特に強力な機能を必要としていないためです。 よく使われるツールキットの選択に関しては、以前はJava用のHutoolを使用していました。これは包括的で多機能でした。JavaScriptエコシステムで同様のものを探していましたが、残念ながらまだ見つかっていません。
異なるパッケージは、異なる機能に対してのみインポートできます。基本ツールキットでは、Lodashよりも軽量で最新のRadashを使用しています。時間処理にはDayJSを使用し、リクエストライブラリにはkyを使用しています。kyも非常に軽量であるためです。
軽量設計を重視する理由は2つあります。1つ目は、CFワーカーは公開パッケージのサイズに制限があるため、大きすぎるパッケージはデプロイに失敗する可能性があることです。2つ目は、重いパッケージはNode.jsへの依存度が高く、CFワーカーの実行中に使用できなくなる可能性があることです。ワーカーにパッケージをインポートするのは、まるでブラインドボックスを開けるようなもので、起動するまで動作するかどうか全くわかりません。これは、ワーカーを使用する際に覚えておくべき重要なポイントです。
Cloudflareに移行した主な理由は、その寛大な無料プランでした。1日あたり10万件のリクエスト、10万件のデータベース書き込み、そして500万件の読み取り。私のビジネスには十分すぎるほどで、もしそれを超過できたらどんなに嬉しいかと思いました。しかし、最終的には月額5ドルの有料プランを選択しました。主な理由は、無料版ではリクエストごとに10ミリ秒のCPU時間しか許可されておらず、複雑なAPIは実行が完了する前にクラッシュする可能性がありました。
さらに、有料プランにも制限があります。1回のAPIリクエストには最大15秒かかることがあり、スケジュールされたタスクやメッセージキューには15分の制限があります。ワーカーAPIへの移行にかかる時間は慎重に評価する必要があります。時間がかかりすぎるものは検討すべきではありません。私自身、この落とし穴に遭遇しました。
JavaScriptでは、非同期メソッドは`async`キーワードで修飾されます。メソッド実行前に`await`を追加すると、メソッドの実行が完了するまで待機し、その後のコードを実行します。`await`がない場合、コードは非同期的に実行され、後続のコードの実行には影響しません。ログ記録やプッシュ通知など、後続のロジックに影響を与えないコード呼び出しでは、通常`await`は使用しません。後続のロジックをブロックするのではなく、非同期的に実行したいからです。
しかし、ワーカーにデプロイした後、`await` 修飾子のないこれらの呼び出しは、おそらく実行されていないことがわかりました。これは、メインプロセスをブロックしなかったためです。後続のコードはリクエストとレスポンスが完了するまで実行を継続し、その後プロセスは終了し、未実行かどうかにかかわらず、すべての未処理タスクが破棄されました。さらに問題なのは、これらの非同期操作がローカル開発環境では正常に動作するにもかかわらず、本番環境では破棄されてしまうことです。
これを解決するにはどうすればよいでしょうか?最も簡単な方法は、すべての非同期メソッド呼び出しに `await` ステートメントを追加することです。しかし、インターフェースに `await` ステートメントが多すぎると、インターフェースの応答が非常に遅くなることに気付くでしょう。私のPaddleコールバックでもこの問題が発生しました。Paddleは、コールバックインターフェースへの呼び出しがタイムアウトを繰り返し、再試行を繰り返すことを検出しました。タイムアウトすると、接続が強制的に閉じられ、後続のロジックの実行が停止します。
この場合、代わりにワーカーの waitUntil メソッドを使用できます。これにより、CPU 使用率の制限を超えない限り、リクエストが完了した後も非同期メソッドが実行さdevelopers.cloudflare.com/workers/runtim…は明日書きます。) https://t.co/N2y3KguqWF
移行操作を2回実行しました。最初の移行後、多数のエラーが発生しましたが、すぐには解決できないと思われたため、元の状態に戻してからゆっくりと問題を解決しました。オンラインエラーメッセージは次のとおりです。
エラー: 別のリクエストに代わって I/O を実行できません。あるリクエスト ハンドラーのコンテキストで作成された I/O オブジェクト (ストリーム、リクエスト/レスポンス本体など) は、別のリクエストのハンドラーからはアクセスできません。
これは Cloudflare Workers の制限であり、全体的なパフォーマンスを向上させることができます。(I/O タイプ: ReadableStreamSource) 翻訳は次のとおりです。
エラー: 異なるリクエストの代理としてI/Oを実行できません。リクエストハンドラのコンテキストで作成されたI/Oオブジェクト(ストリーム、リクエスト/レスポンスボディなど)は、異なるリクエストハンドラからアクセスできません。これはCloudflare Workersの制限であり、全体的なパフォーマンスを向上させるために設定されています。(I/Oタイプ: ReadableStreamSource)
この例外に少し戸惑いました。1つのリクエスト内で別のリクエストの内容にアクセスするにはどうすればいいのでしょうか?理解不能です。各リクエストは独立しているのでしょうか?オンラインで調べてみましたが、役立つ情報は見つかりませんでした。そこで、例外情報をカーソルに渡してコードベース全体をスキャンさせ、どのコードがエラーの原因になっているのかを調べました。カーソルは2つの場所を示しており、1つ目はエントリポイントを記録した場所です。
当初は、リクエストオブジェクトへの影響を避けるため、`c.req.raw.clone().json()` を使用してリクエストパラメータを取得し、クローンオブジェクトを使用するつもりでした。しかし、このクローン操作が別のリクエストに影響を与える可能性があったため、後に`c.req.json()` に変更して問題を解決しました。 次に、コンテキスト オブジェクトは、データベース、キャッシュ、レスポンスなど、hono の多くの場所で使用され、デフォルトのコンテキスト オブジェクトはリクエストのエントリ ポイントで取得されます。
つまり、リクエストはエントリポイントから渡す必要があります。その結果、各メソッドにコンテキストパラメータが必要になり、これは面倒です。Javaでは、この問題はThreadLocalを使って解決します。そこで、JavaScriptにも似たようなものがあるのではないかと考え、実際に見つけました。それがglobalThisです。リクエストのエントリポイントで、globalThis.honoContextプロパティにコンテキストを代入します。
その後、コンテキストオブジェクトを使いたくなったときには、`globalThis.honoContext` を呼び出すだけで取得できました。これで目的の効果は得られましたが、残念ながらワーカー共有の制限が発動してしまう可能性がありました。幸い、Hono は最近、コンテキhono.dev/docs/middlewar…たので、そのメソッドに置き換えたら問題は解決しました。 https://t.co/Dh0zidQ1fc
まず、この問題は非常に厄介です。ローカルの開発環境でも本番環境でも再現できなかったからです。ローカルではエラーなく完璧に動作し、すべての機能が正しく動作しました。しかし、本番環境では必ずしも再現できるとは限りませんでした。マルチスレッドの同時実行環境を実行しても、問題は再現しませんでした。疑わしい2箇所を修正し、本番環境にデプロイして再現するかどうかを確認しました。幸いにも再発はなかったので、これが原因だとほぼ確信しています。
見落としがちなもう一つの問題はタイムゾーンです。ワーカーはUTC-0で動作します。以前のサービスでは、中国より8時間進んでいるGMT+8を使用していました。ただし、データベースにタイムスタンプが保存されるため、ユーザーへの影響はほとんどありません。しかし、SQL統計やスケジュールされたタスクには影響する可能性があります。例えば、午前7時(中国時間)に開始されるスケジュールされたタスクがありました。
昨日の新規ユーザー、具体的には昨日の00:00から23:59:59の間に追加された新規データについて、ユーザーテーブルをクエリしています。移行後、新旧のサービス間に不整合があることがわかりました。調査の結果、タイムゾーンの問題が判明しました。スクリプトを午前7時に実行するのは正しくありませんでした。タイムゾーンの差が8時間あったためです。ワーカーはまだ昨日のタイムゾーンにいたので、実質的に昨日のデータをクエリしていたことになります。そこで、タスクの実行時間を午前8:30に変更しました。
さらに、時差は8時間あります。国内時間で分析する場合は、開始時刻と終了時刻も8時間戻す必要があります。また、Javaのタイムスタンプはデフォルトでミリ秒単位で13桁であるのに対し、JavaScriptのタイムスタンプはデフォルトで秒単位で10桁であることにも注意してください。この変換は一貫性を保つ必要があります。13桁すべてを使用するか、10桁すべてを使用するかのいずれかです。そうしないと、1970年や5万年後の日付が表示される可能性があります。
次に、データベースについてお話しましょう。以前のサービスでは、MySQLの実行にDockerを使用していました。移行オプションとして、CloudflareのD1、SupabaseのPostgreSQL、そしてTursoをいくつか検討しました。これらの無料クォータは非常に寛大で、私のプロジェクトには十分でした。D1とTursoはSQLiteベースですが、SupabaseはPostgreSQLベースです。
最終的にD1を選択しましたが、主な理由は2つあります。1つ目は使いやすさです。D1はクラウドコンピューティング(CF)製品なので、ワーカーとの統合が容易です。2つ目はネットワークのオーバーヘッドです。ワーカーとD1はどちらもCFネットワーク上にあり、同じリージョンで構成することもできるため、データベースアクセスのネットワークオーバーヘッドが削減され、間接的に速度が向上します。一方、SubabaseとTursoでは、これらのオーバーヘッドが発生します。
移行後、一部のユーザーからアカウントにログインできないという報告がありました。私自身も自分のアカウントをテストしたところ、正常にログインできました。ログ分析では問題は見つかりませんでした。ユーザーのメールアドレスとパスワードは正しかったのですが、システムには「ユーザー名またはパスワードが間違っています」というメッセージが表示され続けました。全く理解できず、データベースD1のSQLクエリにユーザーのメールアドレスを入力してみましたが、なんと見つかりませんでした。当初は、不可視文字の問題ではないかと考えました。
ようやく問題が見つかった時、私は完全に驚愕しました。それはMySQLからD1(SQLite)への移行に関するものでした。SQLiteはMySQLよりもはるかに少ない型をサポートしており、1対1のマッピングが不可能であるため、その違いについては覚悟していました。また、SQLiteでは型を定義しても、型を指定せずにデータを保存してもエラーは発生せず、他にも構文の違いはありました。しかし、大文字と小文字の区別の問題を見落としていました。
MySQLデータベースを作成する際は、通常、大文字と小文字を区別しないように設定します。これは標準的な方法で、長年使用してきたデータベースのほとんどが大文字と小文字を区別しませんでした。これはもう体得済みで、SQLiteが大文字と小文字を区別するかもしれないとは考えたこともありませんでした。先ほどのユーザーの質問に戻ると、ユーザーは登録時にAbc@gmail.comと入力し、ログイン時にも再びabc@gmail.comと入力しました。
これは古いMySQLサーバーでは大文字と小文字が区別されず、同じユーザーとして扱われるため、問題なく動作します。しかし、D1では明らかに2人の異なるユーザーとして扱われます。 そこで、どうすれば解決できるか考えました。まず、D1 では MySQL のようなグローバルに大文字小文字を区別しない設定が見つかりませんでした。テーブル作成時に `COLLATE NOCASE` キーワードを使用して、特定のフィールドで大文字小文字を区別しないように指定する必要があります。そこで、テーブルを修正してこのキーワードを追加すればいいと考えました。
その後、SQLiteのALTER TABLE文はテーブル名の変更と列の追加しかサポートしておらず、既存の列の定義を直接変更できないことを発見しました(これも落とし穴です)。テーブルの変更は不可能なので、コードロジックを変更する必要がありました。3つのステップで、まず、emailに関連するすべてのSQL操作を共通メソッドに抽出し、`lower(email)`を使用してクエリ中に強制的に小文字に変換しました。
次に、リクエストのエントリポイントで、パラメータに「email」が含まれている場合は、`toLowerCase()` を呼び出します。最後に、大文字と小文字の不一致によって発生した重複アカウントをマージして削除します。このコードを書くのは本当に大変で、まるで山積みの糞のようでした。
大文字と小文字の区別の問題は一時的に解決しました。数日間ログを観察していたところ、「エラー: D1_ERROR: ネットワーク接続が失われました。」というエラーが、1000回に数回程度の確率で時々発生していることに気付きました。これは私のコードの問題なのか、D1自体の問題なのかは分かりません。オンラインで調べたところ、CFフォーラムやDiscordコミュニティで多くの人が同様の問題を報告していることがわかりましたが、ほとんどの人は解決策を見つけられていません。
唯一、少しだけ役に立った解決策は再試行することでしたが、これもまた、これは通常発生するものだと示唆していました😕。最終的にD1を選んだ主な理由は、ネットワークのオーバーヘッドを削減するためでした。それでもネットワーク接続が途切れる問題が続くのであれば、D1の安定性に疑問を感じます。この問題はまだ完全には解決していないので、進展があればお知らせします。
エンタープライズメールの話に戻りますが、以前はAlibaba Cloudのエンタープライズメールを使用していました。概ね問題なく動作していましたが、無料版のエントリポイントが分かりにくいようでした。複数のドメインをバインドできず、送信制限も不明瞭でした。そこで、複数ドメインに対応したサービスに乗り換えたいと考えました。調べてみると、CloudflareでGmailを使用した際にデータが失われたものの、Gmailアドレスは依然として表示されていたことがわかりました。
iCloudは複数ドメインのバインディングをサポートしており選択肢の一つですが、安定性が非常に低いことで知られています。また、ユーザーからのメールに返信する際に、iCloudアカウントの空き容量が不足しているために受信できないことがよくあるため、リスクがあります。Larkは現在無料ですが、将来性は不透明です。もっと信頼性の高いものを探していました。最終的にZohoを選びました。1ユーザーあたり月額1ドルと安価で、通常は1ユーザーだけで済むからです。
ドメイン数も無制限で、機能も充実しています。高度な機能の一部を使用しない場合は、無料版でも十分です。Zohoの料金プランは国によって異なる点にご注意ください。中国版は1人あたり月額5人民元で、一見安価に思えますが、最低5ユーザーからの登録が必要です。国際版のご利用をお勧めします。
Javaアプリケーションでは、SMTPプロトコルを直接使用してメールを送信しています。しかし、ワーカー上でSMTPプロトコルがサポートされていないため、メールを送信できません。HTTP APIを使用する必要があります(無料版では利用できません)。
以前はデータベースの表示にDataGripを使っていましたが、D1をサポートしていませんでした(少なくとも私が使っていたバージョンでは)。D1のWebインターフェースも非常に簡素です。現在は主に2つのプラchromewebstore.google.com/detail/drizzle…rizzle Studioプラグインで、とても便利です:https://t.co/wMKRcC1LaC
もう一つの選択肢はTablePlusです。Dizzle Studioは便利ですが、ブラウザプラグインなので機能が限られており、SQL予測入力や履歴保存などの機能がありません。一時的な利用には適しています。TablePlusはより包括的な機能を備えており、無料で使えるsetapp版も持っていますが、単体で購入すると高額なのでお勧めしません。
ログ収集と分析に関しては、以前はJava向けのELKスイートに似たAlibaba Cloudのログサービスを利用していました。Cloud Computing(CF)ワーカーはデフォルトではログを保持せず、リアルタイムログの表示のみが可能です。ログを他のログサービスに転送したい場合は、テールワーカー機能を使用できまdevelopers.cloudflare.com/workers/observ…eのサービスを無料で利用できます。BaseLimeはCloud Computingに買収されており、ワンクリックで統合できます。 https://t.co/YuNLJQo6tP
カスタマーサポートに関しては、他のクラウドプロバイダーとは異なり、Cloud Computing (CF) ではデフォルトで請求書、アカウント情報、登録済みチケットのみに対応しており、技術的な問題はサポート対象外です。さらに、オンラインサポートを受けるには、月額250ドルのビジネスプランへのアップグレードが必要です。このプランでは、主にCDN関連の製品権限が使用され、ワーカー権限は使用されません。カスタマーサポート専用のアカウントを開設する可能性は低いでしょう。大規模なユーザーベースとコアユーザーへのサービス提供に重点を置いていることを考えると、これは当然のことです。
では、本当に技術的な問題に遭遇したらどうすればいいのでしょうか?選択肢は2つあります。1つはCF開発者フォーラムに投稿して助けを求めることです。しかし、タイムリーな回答が得られるかどうかは保証できません。また、あまり一般的ではない問題であれば、誰も答えられない可能性も高いです。問題投稿を投稿したのに、何ヶ月経っても返信が来ないという経験があります。返信はたいてい「ねえ、私も同じ問題に遭遇したんだけど、解決した?」といった内容ばかりです。
2つ目は、CF Discordコミュニティに参加してフィードバックを得る方法です。こちらは比較的タイムリーなフィードバックが得られますが、時差があるため、参加している時に他のメンバーがまだ寝ている可能性もあります。また、公式声明では、ここのスタッフは「技術サポート担当者ではなく、空き時間に自発的に質問に答えている普通の開発者や技術専門家」であると強調されています。そのため、ここで質問する際は、期待と感情をコントロールするのが最善です。






