1.背景
ユーザーレベルの急速な成長に伴い、vivoオフィシャルモールv1.0のモノリシックアーキテクチャは、モジュールの肥大化、開発効率の低下、パフォーマンスのボトルネックの発生、システムメンテナンスの困難などの欠点を徐々に明らかにしています。
v2.0アーキテクチャのアップグレードは2017年に開始され、ビジネスモジュールに基づく垂直システムの物理分割、分割されたビジネスラインはそれぞれの役割を果たし、サービス指向の機能を提供し、メインサイトビジネスを共同でサポートします。
注文モジュールは、eコマースシステムのトランザクションコアです。蓄積されたデータは、1メートルのストレージのボトルネックに到達しようとしています。システムは、新製品のリリースや主要なプロモーションのフローをサポートできません。サービスの変革が不可欠です。
この記事では、生体内モール注文システムの構築中に発生した問題と解決策を紹介し、アーキテクチャ設計の経験を共有します。
2、システムアーキテクチャ
注文モジュールをモールから分離し、独立したデータベースを使用して注文システムになり、モール内の関連システムの注文、支払い、ロジスティクス、アフターサービスなどの標準化されたサービスを提供します。
システムアーキテクチャを次の図に示します。
3.技術的な課題
3.1データ量と高い同時実行性の問題
最初の課題はストレージシステムです。
-
データ量の問題
過去の注文の蓄積により、MySQLの注文テーブルのデータ量は数千万に達しました。
InnoDBストレージエンジンのストレージ構造はB +ツリーであり、検索時間の複雑さはO(log n)であることがわかっているため、データの総量nが大きくなると、検索速度が必然的に遅くなります。インデックスの追加や最適化の方法を問わず、解決できません。 1つのテーブルのデータ量を減らす方法しか考えられません。
大量のデータのソリューションには、データアーカイブ、サブテーブルが含まれます
-
高い同時実行性の問題
モール事業は急速な発展の時期にあり、注文数は繰り返し新たな高みに達し、事業の複雑さも増しており、アプリケーションによるMySQLへのアクセス数も増加しています。
スタンドアロンのMySQLの処理能力は限られています。プレッシャーが大きすぎると、すべてのリクエストのアクセス速度が低下し、データベースがダウンすることさえあります。
同時実行性の高いソリューションは次のとおりです。キャッシュの使用、読み取りと書き込みの分離、およびサブデータベース
以下は、これらのソリューションの簡単な説明です。
-
データのアーカイブ
注文データには時間属性があり、ホットテール効果があります。ほとんどの場合、最新の注文が取得されますが、使用頻度の低い大量の古いデータが注文テーブルに保存されます。
次に、新しいデータと古いデータを別々に保存し、履歴の順序を別のテーブルに移動し、コード内のクエリモジュールに対応する変更を加えることができます。これにより、大量のデータの問題を効果的に解決できます。
-
キャッシュを使用する
MySQLのプリキャッシュとしてRedisを使用すると、ほとんどのクエリリクエストをブロックし、応答の遅延を減らすことができます。
キャッシュは、商品システムなど、ユーザーとはほとんど関係のないシステムで特に効果的ですが、注文システムでは、ユーザーごとの注文データが異なり、キャッシュヒット率が高くなく、効果があまり良くありません。
-
読み取りと書き込みの分離
マスターデータベースは、データ更新要求を実行し、データ変更をすべてのスレーブライブラリにリアルタイムで同期し、複数のスレーブライブラリを使用してクエリ要求を共有します。
ただし、注文データの更新操作は多数あり、ピーク注文時のメインライブラリのプレッシャーはまだ解決されていません。また、マスタースレーブの同期遅延があります。通常、遅延は1ミリ秒以下と非常に小さいですが、特定の瞬間にマスタースレーブデータの一貫性が失われる可能性もあります。
次に、影響を受けるすべてのビジネスシナリオの互換性を処理する必要があり、いくつかの妥協が行われる可能性があります。たとえば、注文が正常に行われた後、最初に正常な注文ページにジャンプし、ユーザーは手動でクリックして注文を表示し、注文を表示できます。
-
サブライブラリ
サブライブラリには、垂直サブライブラリと水平サブライブラリが含まれます。
①水平サブデータベース:同じテーブルのデータを特定のルールに従って異なるデータベースに分割し、各データベースを異なるサーバーに配置できます。
②垂直サブデータベース:テーブルはビジネスごとに分類され、異なるデータベースに分散されます。各データベースは異なるサーバーに配置できます。そのコアコンセプトは特別なデータベース専用です。
-
サブテーブル
サブテーブルには、垂直サブテーブルと水平サブテーブルが含まれます。
**①水平 サブテーブル:**同じデータベース内で、1つのテーブルのデータが特定のルールに従って複数のテーブルに分割されます。
**②垂直 サブテーブル:**フィールドに応じてテーブルを複数のテーブルに分割し、各テーブルにいくつかのフィールドを格納します。
変革が既存のビジネスに与えるコスト、影響、影響を包括的に検討し、最後のトリックであるサブデータベースとサブテーブルを直接使用することにしました。
3.2サブライブラリとサブメーターテクノロジーの選択
サブライブラリとサブテーブルの技術的な選択は、主に次の方向から検討されます。
-
クライアントSDKオープンソースソリューション
-
ミドルウェアプロキシオープンソースソリューション
-
同社のミドルウェアチームが提供する自己研究フレームワーク
- 独自のホイールを作成する
以前のプロジェクトの経験を参照し、会社のミドルウェアチームと連絡を取り合った後、オープンソースのSharding-JDBCソリューションが採用されました。現在、Sharding-Sphereに名前が変更されています。
-
ドキュメント:公式ドキュメントは大まかなものですが、オンライン資料、ソースコード分析、およびデモは比較的豊富です。
-
コミュニティ:アクティブ
- 機能:jarパッケージで提供され、クライアント側のフラグメンテーションに属し、xaトランザクションをサポートします
3.2.1サブデータベースサブテーブル戦略
ビジネス特性を組み合わせて、シャーディングキーとしてユーザーIDを選択し、ユーザーIDのハッシュ値を計算して係数を取得することにより、ユーザー注文データのデータベーステーブル番号を取得します。
合計n個のライブラリがあり、各ライブラリにm個のテーブルがあるとします。
ライブラリテーブル番号の計算方法は次のとおりです。
-ライブラリのシリアル番号:Hash(userId)/ m%n
-テーブルのシリアル番号:Hash(userId)%m
ルーティングプロセスを次の図に示します。
3.2.2サブデータベースとサブテーブルおよびソリューションの制限
サブデータベースサブテーブルは、データ量と同時実行性の問題を解決しますが、データベースのクエリ機能を大幅に制限します。以前は単純だった関連クエリの中には、サブデータベースとテーブルサブテーブルの後で実現されないものがあるため、個別に確認する必要があります。 Sharding-JDBCでサポートされていないこれらのSQLは書き直されます。
さらに、次の課題が発生しました。
(1)グローバルにユニークなIDデザイン
データベースとテーブルをシャーディングした後、データベースの自動インクリメントプライマリキーはグローバルに一意ではなくなり、注文番号として使用できなくなります。ただし、内部システム間の多くの対話インターフェイスには注文番号しかありません。このシャードキーのユーザーIDはありません。対応する注文番号を見つける方法ライブラリテーブルはどうですか?
注文番号を生成するときに、ライブラリテーブル番号を暗黙的に含めたことがわかりました。このようにして、ライブラリテーブル番号は、ユーザーIDなしで注文番号から取得できます。
(2)履歴注文番号には、暗黙のライブラリテーブル情報がありません
過去の注文番号とユーザーIDの間のマッピング関係を格納するために、単一のテーブルが使用されます。時間が経つにつれて、これらの注文はシステム間の相互作用で使用されなくなります。
(3)管理のバックグラウンドは、さまざまなフィルタリング条件に従って、ページごとに条件を満たすすべての注文を照会する必要があります。
注文データは、バックグラウンドクエリにのみ使用される検索エンジンElasticsearchに冗長的に保存されます。
3.3MySQLからESにデータを同期する方法
前述のように、バックエンドクエリの管理を容易にするために、Elasticsearchに注文データを冗長的に保存します。次に、変更後にMySQLの注文データをESに同期するにはどうすればよいですか。
ここで考慮すべきことは、データ同期の適時性と一貫性、ビジネスコードへの小さな侵入、およびサービス自体のパフォーマンスへの影響がないことです。
-
MQスキーム
コンシューマーとしてのES更新サービス、注文変更MQメッセージを受信した後にESを更新
-
Binlogソリューション
ES更新サービスは、運河などのオープンソースプロジェクトを使用してMySQLのスレーブノードになりすまし、Binlogを受信して解析し、リアルタイムのデータ変更情報を取得してから、この変更情報に基づいてESを更新します。
その中で、BinLogソリューションはより一般的ですが、実装もより複雑です。最終的にMQソリューションを選択しました。
ESデータは管理のバックグラウンドでのみ使用されるため、データの信頼性とリアルタイムの同期の要件は特に高くありません。
ダウンタイムやメッセージ損失などの極端な状況を考慮して、特定の条件下でESデータを手動で同期する機能がバックグラウンドで追加されて補正されます。
3.4データベースを安全に交換する方法
元の単一インスタンスデータベースから新しいデータベースクラスターにデータを移行する方法も、主要な技術的課題です。
データの正確性を確保するだけでなく、各ステップの後で問題が発生した場合は、前のステップにすばやくロールバックできるようにします。
ダウンタイムを伴う移行とダウンタイムを伴わない移行の2つのオプションを検討しました。
(1)ノンストップ移行計画:
-
古いライブラリのデータを新しいライブラリにコピーし、同期プログラムを起動し、Binlogおよびその他のソリューションを使用して、古いライブラリデータを新しいライブラリにリアルタイムで同期します。
-
二重書き込み注文用の新旧のライブラリサービスが開始され、古いライブラリのみが読み書きされます。
-
二重書き込みをオンにし、同時に同期プログラムを停止し、比較補正プログラムを開始して、新しいライブラリデータが古いライブラリと一致していることを確認します。
-
読み取り要求を新しいライブラリに徐々に切り替えます。
-
読み取りと書き込みの両方が新しいライブラリに切り替えられ、補正プログラムが比較されて、古いライブラリデータが新しいライブラリと一致していることが確認されます。
- オフラインの古いライブラリ、オフライン注文の二重書き込み機能、オフライン同期手順、および比較補償手順。
(2)移行計画のシャットダウン:
-
新しい注文システムが開始され、2か月前に注文を新しいデータベースに同期するための移行プログラムが実行され、データが監査されました。
-
モールV1アプリケーションをシャットダウンして、古いデータベースデータが変更されないようにします。
-
移行プロセスを実行し、最初のステップで移行されなかった注文を新しいライブラリに同期して、監査を実施します。
- モールV2アプリケーションを起動し、テストと検証を開始します。失敗した場合は、モールV1アプリケーションに戻ります(新しい注文システムには、古いライブラリを二重に書き込むスイッチがあります)。
ノンストップソリューションのコストが高く、夜間シャットダウンソリューションのビジネス損失が大きくないことを考慮すると、最終的な選択はシャットダウン移行ソリューションです。
3.5分散トランザクションの問題
eコマースのトランザクションプロセスでは、分散トランザクションは次のような典型的な問題です。
-
ユーザーが支払いに成功した後、ユーザーに商品を配達するように配達システムに通知する必要があります。
- ユーザーが受領を確認した後、ユーザーにショッピング特典のポイントを発行するようにポイントシステムに通知する必要があります。
マイクロサービスアーキテクチャでデータの一貫性を確保するにはどうすればよいですか?
ビジネスシナリオが異なれば、データの一貫性に対する要件も異なります。業界の主流のソリューションには、強力な一貫性を解決するための2フェーズ送信(2PC)と3フェーズ送信(3PC)、およびTCC、ローカルメッセージ、トランザクションメッセージやベストエフォート通知など。
上記のスキームの詳細な説明ではありませんが、使用しているローカルメッセージテーブルスキームを紹介します。ローカルトランザクションで実行される非同期操作はメッセージテーブルに記録されます。実行が失敗した場合は、タイミングタスクで補正できます。
次の図は、注文が完了した後にポイントを付与するようにポイントシステムに通知する例を示しています。
3.6システムのセキュリティと安定性
-
ネットワークの分離
外部ネットワークを介してアクセスできるサードパーティインターフェイスはごく少数であり、それらはすべて署名を検証します。内部システムは、内部ネットワークドメイン名およびRPCインターフェイスと対話します。
-
コンカレントロック
注文更新操作の前に、同時更新を防ぐためにデータベースの行レベルのロックによって制限されます。
-
同一性
すべてのインターフェースは独立しているため、相手のネットワークタイムアウトの再試行の影響を心配する必要はありません。
-
ヒューズ
Hystrixコンポーネントを使用して、外部システムへのリアルタイム呼び出しにヒューズ保護を追加し、システム障害の影響が分散システム全体に広がるのを防ぎます。
-
監視と警告
ログプラットフォームのエラーログアラーム、コールチェーンのサービス分析アラーム、および会社のミドルウェアと基本コンポーネントの監視およびアラーム機能を構成することにより、システムの異常を初めて見つけることができます。
3.7段付きピット
MQ消費方法を使用してデータベースの注文関連データをESに同期すると、検出された書き込みデータは注文の最新データではありません
下の図の左側は元の計画です。
注文データ同期のMQを使用する場合、スレッドAが先に実行してデータを見つけると、この時点で注文データが更新され、スレッドBが同期操作を開始します。注文データを見つけた後、スレッドAの前にESに書き込まれます。スレッドAが書き込みを実行すると、スレッドBによって書き込まれたデータが上書きされ、ESの注文データが最新ではなくなります。
解決策は、注文データを照会するときに行ロックを追加することです。ビジネス全体がトランザクションで実行され、実行が完了した後に次のスレッドが実行されます。
sharding-jdbcグループ化、並べ替え、ページングの後、すべてのデータの問題を照会します
表示例:一時グループからa、bをdesc制限で選択します1,10。
実装では、Sharding-jdbcのgroupbyフィールドとorderbyフィールドが順序と一致せず、10がInteger.MAX_VALUEに設定されているため、ページングクエリが失敗します。
io.shardingsphere.core.routing.router.sharding.ParsingSQLRouter#processLimit
private void processLimit(final List<Object> parameters, final SelectStatement selectStatement, final boolean isSingleRouting) {
boolean isNeedFetchAll = (!selectStatement.getGroupByItems().isEmpty() || !selectStatement.getAggregationSelectItems().isEmpty()) && !selectStatement.isSameGroupByAndOrderByItems();
selectStatement.getLimit().processParameters(parameters, isNeedFetchAll, databaseType, isSingleRouting);
}
io.shardingsphere.core.parsing.parser.context.limit.Limit#processParameters
/**
* Fill parameters for rewrite limit.
*
* @param parameters parameters
* @param isFetchAll is fetch all data or not
* @param databaseType database type
* @param isSingleRouting is single routing or not
*/
public void processParameters(final List<Object> parameters, final boolean isFetchAll, final DatabaseType databaseType, final boolean isSingleRouting) {
fill(parameters);
rewrite(parameters, isFetchAll, databaseType, isSingleRouting);
}
private void rewrite(final List<Object> parameters, final boolean isFetchAll, final DatabaseType databaseType, final boolean isSingleRouting) {
int rewriteOffset = 0;
int rewriteRowCount;
if (isFetchAll) {
rewriteRowCount = Integer.MAX_VALUE;
} else if (isNeedRewriteRowCount(databaseType) && !isSingleRouting) {
rewriteRowCount = null == rowCount ? -1 : getOffsetValue() + rowCount.getValue();
} else {
rewriteRowCount = rowCount.getValue();
}
if (null != offset && offset.getIndex() > -1 && !isSingleRouting) {
parameters.set(offset.getIndex(), rewriteOffset);
}
if (null != rowCount && rowCount.getIndex() > -1) {
parameters.set(rowCount.getIndex(), rewriteRowCount);
}
}
正しい表現は、desc、b limit 1,10によって一時グループから選択する必要があります。使用されるバージョンは、sharing-jdbcの3.1.1です。
ESページングクエリの並べ替えフィールドに重複する値がある場合、ページングクエリ中にデータの欠落や重複データの検出を回避するために、2番目の並べ替え条件として一意のフィールドを追加することをお勧めします。たとえば、注文の作成時間が唯一の並べ替え条件として使用されます。時間の経過とともに大量のデータがあると、クエリされた注文の欠落や重複につながるため、2番目の並べ替え条件として一意の値を追加するか、並べ替え条件として直接一意の値を使用する必要があります。
4.結果
-
1回限りのオンラインでの成功、1年以上の安定した運用
-
コアサービスのパフォーマンスが10倍以上向上
-
システムのデカップリング、反復効率が大幅に向上
- モールの急速な発展を少なくとも5年間サポートできます
V.結論
システムを設計する際に最先端の技術やアイデアを盲目的に追求したり、問題に直面したときに主流のeコマースソリューションを直接採用したりするのではなく、実際のビジネス状況に基づいて最適な方法を選択しました。
個人的には、ダニエルが最初に良いシステムを設計するのではなく、事業の発展と進化を徐々に繰り返し、事業の発展の方向性を予測し、建築の進化計画を事前に策定する必要があると感じています。つまり、ビジネスの最前線に行きましょう!
著者:vivo公式ウェブサイトモール開発チーム