この記事では、実際の戦闘または実装に焦点を当て、CAPは含まず、ACIDについて簡単に説明します。
この記事は、基本的な分散プログラマーに適しています。
-
この記事では、クラスター内のノードのフェイルオーバーとリカバリーの問題について説明します。
-
この記事では、問題と不透明な問題を扱います。
-
この記事では、Weiboとツイーターについて説明し、ビッグデータの問題を提起します。
配布のトピックが大きすぎ、トランザクションのトピックが大きすぎるため、クラスター内の小さなノードから始めます。
ライブノードとクラスター内の同期
分散システムでは、ノードが生きているかどうかを判断する方法は?
カフカは考えています:
-
このノードと飼育係は話すことができます(ハートビートを通して飼育係とのセッションを続けてください。)
-
このノードがスレーブノードである場合、マスターノードのデータ変更を可能な限り忠実に反映できる必要があります。
つまり、マスターノードが新しいデータを書き込んだ後、これらの変更されたデータを時間内にコピーできる必要があります。いわゆるタイムリーであり、あまりプルダウンすることはできません。
次に、上記の2つの条件を満たすノードは、稼働中または同期していると見なすことができます。
最初の点に関しては、誰もがハートビートに精通しているので、特定のノードは次のように動物園の飼育係と話すことができないと考えることができます。
zookeeper-node:
var timer =
new timer()
.setInterval(10sec)
.onTime(slave-nodes,function(slave-nodes){
slave-nodes.forEach( node -> {
boolean isAlive = node.heartbeatACK(15sec);
if(!isAlive) {
node.numNotAlive += 1;
if(node.numNotAlive >= 3) {
node.declareDeadOrFailed();
slave-nodes.remove(node);
//回调也可 leader-node-app.notifyNodeDeadOrFailed(node)
}
}else
node.numNotAlive = 0;
});
});
timer.run();
//你可以回调也可以像下面这样简单的计时判断
leader-node-app:
var timer =
new timer()
.setInterval(10sec)
.onTime(slave-nodes,function(slave-nodes){
slave-nodes.forEach(node -> {
if(node.isDeadOrFailed) {
//node不能和zookeeper喊话了
}
});
});
timer.run();
2点目はもう少し複雑です。
次のように分析してみましょう。
-
データメッセージ。
-
操作op-log。
-
オフセット位置/オフセット。
// 1. 先考虑messages
// 2. 再考虑log的postion或者offset
// 3. 考虑msg和off都记录在同源数据库或者存储设备上.(database or storage-device.)
var timer =
new timer()
.setInterval(10sec)
.onTime(slave-nodes,function(nodes){
var core-of-cpu = 8;
//嫌慢就并发呗 mod hash go!
nodes.groupParallel(core-of-cpu)
.forEach(node -> {
boolean nodeSucked = false;
if(node.ackTimeDiff > 30sec) {
//30秒内没有回复,node卡住了
nodeSucked = true;
}
if(node.logOffsetDiff > 100) {
//node复制跟不上了,差距超过100条数据
nodeSucked = true;
}
if(nodeSucked) {
//总之node“死”掉了,其实到底死没死,谁知道呢?network-error在分布式系统中或者节点失败这个事情是正常现象.
node.declareDeadOrFailed();
//不和你玩啦,集群不要你了
nodes.remove(node);
//该怎么处理呢,抛个事件吧.
fire-event-NodeDeadOrFailed(node);
}
});
});
timer.run();
上記のノードの状態管理は通常、動物園の飼育係によって行われ、リーダーまたはマスターノードもその状態を維持します。
次に、アプリケーションのリーダーまたはマスターノードは、zookeeperからステータスを取得するだけで済みます。同時に、上記の実装は必ずしも最良ですか?いいえ、ほとんどの操作を組み合わせることができますが、ノードが稼働しているかどうかを説明するために、このように記述しても問題はありません。
ノードが停止している、障害が発生している、または同期していない場合はどうすればよいですか?
さて、私はついにフェイルオーバーとリカバリについて話しましたが、データの読み取りに影響を与えない他のスレーブノードがあるため、フェイルオーバーは比較的簡単です。
-
複数のスレーブノードが同時に失敗しましたか?
100%の可用性はありません。データセンターとコンピューター室が麻痺し、ネットワークケーブルが切断され、ハッカーが侵入してルートを削除します。つまり、RPが故障しています。 -
マスターノードに障害が発生した場合、マスターマスターは機能しませんか?
Keep-alivedまたはLVS、または独自のフェイルオーバーを作成します。
高可用性アーキテクチャ(HA)も重要なことであり、この記事は拡張されません。
リカバリの側面に注目しましょう。ここで視点を開きましょう。再起動後にデータを同期するためにログを追跡するためにスレーブノードに焦点を合わせるだけでなく、実際のアプリケーションで、データ要求(読み取り、書き込み、更新を含む)が失敗した場合はどうなるでしょうか。
誰もが言う、再試行、再生、またはそのままにしておくことができます!
大丈夫です大丈夫ですこれらはすべて戦略ですがあなたは本当にそれを行う方法を知っていますか?
ビッグデータの問題
議論の背景を設定しましょう:
問題:WeiboのWeibo(リール)などのメッセージフローが継続的にアプリケーションに流れ込みます。これらのメッセージを処理するには、次のような要件があります。
リーチは、上のURLに公開されている一意のユーザーの数です。Twitter。
次に、このWeibo(url)の3時間以内のリーチの総数を数えます。
それを解決する方法は?
特定のWeibo(url)を特定の期間内に再投稿した人を引き出し、これらの人のファンを引き出し、重複を削除してから、必要なリーチである総数を計算します。
簡単にするために、日付を無視して、この方法が機能するかどうかを確認しましょう。
/** ---------------------------------
* 1. 求出转发微博(url)的大V.
* __________________________________*/
方法 :getUrlToTweetersMap(String url_id)
SQL : /* 数据库A,表url_user存储了转发某url的user */
SELECT url_user.user_id as tweeter_id
FROM url_user
WHERE url_user.url_id = ${url_id}
返回 :[user_1,...,user_m]
/** ---------------------------------
* 2. 求出大V的粉丝
* __________________________________*/
方法 : getFollowers(String tweeter_id);
SQL : /* 数据库B */
SELECT users.id as user_id
FROM users
WHERE users.followee_id = ${tweeter_id}
返回:tweeter的粉丝
/** ---------------------------------
* 3. 求出Reach
* __________________________________*/
var url = queryArgs.getUrl();
var tweeters = getUrlToTweetersMap();
var result = new HashMap<String,Integer>();
tweeters.forEach(t -> {
// 你可以批量in + 并发读来优化下面方法的性能
var followers = getFollowers(t.tweeter_id);
followers.forEach(f -> {
//hash去重
result.put(f.user_id,1);
});
});
//Reach
return result.size();
何があっても、リーチが見つかりました!
実際、これは非常に重要な問題につながります。これは、フレームワーク、設計、およびパターンについて多くの人が話すときに見過ごされがちです。パフォーマンスとデータベースモデリングの関係です。
-
データ量はどのくらいですか?
読者がこの問題のデータベースI / Oについていくつかのアイデアを持っているかどうかわかりませんか、それとも彼らはショックを受けていますか?
コンピューティングリーチは、単一のマシンには強すぎます。数千のデータベース呼び出しと数千万のタプルが必要になる可能性があります。
上記のデータベース設計では、JOINは回避されます。ビッグVを求めるファンのパフォーマンスを向上させるために、ビッグVのバッチを次のように使用できます。バッチ/バルク、そして複数のバッチが同時に読み取られ、データベースを強制終了することを誓います。
ここでは、Weiboをフォワーダーテーブルが配置されているデータベースに分離し、ファンデータベースから分離します。データが大きい場合はどうなりますか?
データベースサブテーブル...
OK、従来のリレーショナルデータベースサブテーブルとデータルーティング(読み取りパスの集約、書き込みパスの分散)に既に精通している、またはシャーディングテクノロジにも精通している、または優れていると仮定します。 HBaseの水平方向のスケーラビリティを組み合わせ、セカンダリインデックスの問題を解決するための一貫した戦略を持っています。
要するに、ストレージと読み取りの問題は、それを解決したことを前提としています。分散コンピューティングについてはどうでしょうか。 -
Weiboのアプリケーションでは、人と人との関係がグラフ(ウェブ)になります。どのようにモデル化して保存しますか?たとえば、この質問に答えるだけでなく、誰かの友人の友人は誰かにどれだけ近いです
か?
ストームを使用して分散コンピューティングを解決し、ストリーミングコンピューティング機能を提供する方法をご覧ください。
// url到大V -> 数据库1
TridentState urlToTweeters =
topology.newStaticState(getUrlToTweetersState());
// 大V到粉丝 -> 数据库2
TridentState tweetersToFollowers =
topology.newStaticState(getTweeterToFollowersState());
topology.newDRPCStream("reach")
.stateQuery(urlToTweeters, new Fields("args"), new MapGet(), new Fields("tweeters"))
.each(new Fields("tweeters"), new ExpandList(), new Fields("tweeter"))
.shuffle() /* 大V的粉丝很多,所以需要分布式处理*/
.stateQuery(tweetersToFollowers, new Fields("tweeter"), new MapGet(), new Fields("followers"))
.parallelismHint(200) /* 粉丝很多,所以需要高并发 */
.each(new Fields("followers"), new ExpandList(), new Fields("follower"))
.groupBy(new Fields("follower"))
.aggregate(new One(), new Fields("one")) /* 去重 */
.parallelismHint(20)
.aggregate(new Count(), new Fields("reach")); /* 计算reach数 */
せいぜい一度
トピックに戻ると、上記の例が紹介されています。1つは分散(ストレージ+コンピューティング)に関する質問を引き出すことであり、もう1つは意味を明らかにすることです。
プログラマーは、Jay Krepsがどのように発明したかなど、設計と実装に注意を払う必要があります。このホイールのカフカ:]
あなたがまだプログラマーであるなら、実用的にしましょう。recover
ノード回復の問題については先に述べましたが、それでは何個回復する必要がありますか?
基本:
-
ノードステータス
-
ノードデータ
この記事では、この問題をデータレベルから説明します。問題を単純化するために、データを書き込むシナリオを検討します。write-ahead-log
データの複製と一貫性を確保する方法を使用する場合、一貫性の問題にどのように対処しますか?
-
新しいデータがマスターノードに書き込まれます。
-
ノードからのログをたどり、この新しいデータのバッチをコピーする準備をします。スレーブノードは2つのことを行います:
(1)データのIDオフセットをログに書き込みます;
(2)データ自体を処理しようとすると、スレーブノードがハングします。
次に、上記のノードの存続条件に従って、イベントがダウンしていることをスレーブノードが検出しました。スレーブノードは、メンテナンススタッフによって、または単独で手動で復元されます。その後、クラスターに参加して友達がプレイを続ける前に、スレーブノードは自身を同期する必要があります。ステータスとデータ。
ここに問題があります:
ログ内のデータオフセットに基づいてデータが同期されている場合、ノードはデータを処理する前にオフセットを書き込んでいるが、失われたデータのバッチは処理されていないため、ログ後のデータが同期されている場合は、データのそのバッチが失われました-データが失われました。
この場合、データは最大で1回処理されると言われているため、データが失われます。
少なくとも一度は
さて、データの損失は許容できないので、別の方法で対処しましょう。
-
新しいデータがマスターノードに書き込まれます。
-
ノードからのログをたどり、この新しいデータのバッチをコピーする準備をします。スレーブノードは2つのことを行います:
(1)最初にデータを処理します;
(2)データのIDオフセットがログに書き込まれようとしており、スレーブノードがダウンしています。
ここに問題があります:
データを同期するためにノードからログが追跡される場合、データのバッチが処理され、データオフセットがログに反映されないため、この方法で追跡されると、このデータのバッチが複製されます。
このシナリオでは、意味的に言えば、データは少なくとも1回処理されます。つまり、データ処理が繰り返されます。
ちょうど一度
トランザクション
さて、データの重複は許容できませんか?要件は非常に高いです。
強力な一貫性は、誰もが追求していることを保証します(これが最終的な一貫性です)、それをどのように行うのですか?
言い換えれば、データを更新するときに、トランザクション機能を保証するにはどうすればよいですか?
データのバッチが次のようになっているとします。
// 新到数据
{
transactionId:4
urlId:99
reach:5
}
このデータのバッチをライブラリまたはログに更新すると、元の状況は次のようになります。
// 老数据
{
transactionId:3
urlId:99
reach:3
}
次の3点を保証できれば:
-
トランザクションIDの生成は強く順序付けられています。(分離、シリアル)
-
同じトランザクションIDに対応するデータのバッチは同じです(理想性、複数の操作の1つの結果)
-
単一のデータは、特定のデータバッチにのみ表示されます(一貫性、省略、重複はありません)
だから、安心して大胆に更新してください:
// 更新后数据
{
transactionId:4
urlId:99
//3 + 5 = 8
reach:8
}
この更新はIDオフセットとデータで更新されるため、この操作を保証するものはアトミック性であることに注意してください。
あなたのデータベースは原子性を提供していますか?少し後で説明します。
これが正常に更新されました。更新中にノードがハングアップした場合、ライブラリまたはログのIDオフセットは書き込まれず、データは処理されません。ノードが復元されたら、安心して同期してから、クラスターに参加して再生できます。
それで、データが一度だけ処理されることを保証することはまだ難しいですよね?
上記の「一度だけ処理する」というセマンティクスの実現の何が問題になっていますか?
パフォーマンスの問題。
ここでは、ライブラリまたはディスクへのラウンドトリップ時間を短縮するためにバッチ戦略が使用されていますが、ここでのパフォーマンスの問題は何ですか?
マスターノードの可用性を確保するためにマスターマスターアーキテクチャが使用されているが、1つのマスターノードに障害が発生し、別のマスターノードで作業をホストするのに時間がかかることを考慮してください。
スレーブノードが同期していると仮定して、ポップ!マスターノードがダウンしています!セマンティクスが1回だけ処理されるようにする必要があるため、アトミック性は機能し、失敗し、ロールバックしてから、失敗したデータをマスターノードからプルします(このデータのバッチが変更されているか、バッチをまったくキャッシュしなかったため、近くで更新できません)データ)、結果はどうなりますか?
古いマスターノードがダウンしていて、新しいマスターノードがまだ開始されていないため、このトランザクションは、データ同期のソース(マスターノードが要求に応答できる)までここでスタックします。
パフォーマンスを考慮しない場合は、手放すだけです。大したことではありません。
あなたは未完成のようですか?さあ、「銀の弾丸」が何であるか見てください?
不透明-トランザクション
さて、そのような効果を追求しましょう:
データのバッチ(このデータのバッチはトランザクションに対応します)内のデータは失敗する可能性がありますが、別のデータのバッチでは成功します。
つまり、データのバッチのトランザクションIDは同じである必要があります。
例を見てみましょうprevReach
。同じ古いデータですが、より多くのフィールドがあります。
// 老数据
{
transactionId:3
urlId:99
//注意这里多了个字段,表示之前的reach的值
prevReach:2
reach:3
}
// 新到数据
{
transactionId:4
urlId:99
reach:5
}
この場合、新しいトランザクションのIDは大きくなり、小さくなり、新しいトランザクションを実行できることを示します。何を待っていますか?直接更新します。更新されたデータは次のとおりです。
// 新到数据
{
transactionId:4
urlId:99
//注意这里更新为之前的值
prevReach:3
//3 + 5 = 8
reach:8
}
次に、別の状況を見てみましょう。
// 老数据
{
transactionId:3
urlId:99
prevReach:2
reach:3
}
// 新到数据
{
//注意事务ID为3,和老数据中的事务ID相同
transactionId:3
urlId:99
reach:5
}
この状況にどのように対処しますか?スキップしますか?新しいデータのトランザクションIDは、ライブラリまたはログのトランザクションIDと同じであるため、今回はトランザクション要件に従ってデータを処理する必要があります。スキップしますか?
いいえ、この種のことは推測できません。私たちが持っているいくつかのプロパティについて考えてください。重要なポイントは次のとおりです。
データのバッチが与えられると、それらは同じトランザクションIDに属します。
上記の文と次の文の違いを注意深く理解してください。
トランザクションIDが与えられると、それに関連付けられたデータのバッチはいつでも同じになります。
新しく到着したデータのトランザクションIDがストレージ内のトランザクションIDと同じであるため、このデータのバッチは個別にまたは非同期で処理される可能性がありますが、このデータのバッチに対応するトランザクションIDは常に同じであるため、これを行う必要があります。このデータバッチのパートAが最初に処理されます。全員がトランザクションIDであるため、パートAの以前の値は信頼できます。
したがって、更新にはReachではなくprevReachの値に依存します。
// 更新后数据
{
transactionId:3
urlId:99
//这个值不变
prevReach:2
//2 + 5 = 7
reach:7
}
あなたは何を見つけましたか?
トランザクションIDが異なると、値も異なります。
-
ストレージ内のトランザクションID3より大きいトランザクションIDが4の場合、リーチは3 + 5 = 8に更新されます。
-
トランザクションIDが3の場合、これはストレージ内のトランザクションID3と同じであり、リーチは2 + 5 = 7に更新されます。
これですOpaque Transaction
。
このトランザクション機能は最も強力であり、トランザクションが非同期で送信されることを保証できます。したがって、クラスターで次のように言っても、行き詰まる心配はありません。
トランザクション:
-
データはバッチで処理され、各トランザクションIDは同一データの特定のバッチに対応します。
-
トランザクションIDの生成が厳密に順序付けられていることを確認してください。
-
データのバッチが重複または省略されていないことを確認してください。
-
トランザクションが失敗してデータソースが失われた場合、後続のトランザクションはデータソースが復元されるまでスタックします。
不透明-トランザクション:
-
データはバッチで処理され、データの各バッチには明確で一意のトランザクションIDがあります。
-
トランザクションIDの生成が厳密に順序付けられていることを確認してください。
-
データのバッチが重複または省略されていないことを確認してください。
-
トランザクションが失敗した場合、データソースは失われ、後続のトランザクションのデータソースも失われない限り、後続のトランザクションは影響を受けません。
実際、このグローバルIDのデザインも芸術です。
-
冗長アソシエーションテーブルのIDは、結合を減らすために使用されるため、IDはO(1)です。
-
順序付けを回避するための冗長日付(ロングタイプ)フィールド。
-
二次インデックスなし(HBase)の恥ずかしさを回避するための冗長フィルターフィールド。
-
データベースとテーブルが分割された後、アプリケーション層でのデータルーティングの書き込みを容易にするためにmod-hash値を格納します。
このコンテンツは多すぎてトピックが大きすぎるので、ここでは拡張しません。
これで、グローバルに一意で順序付けられたIDを生成するためのTwitterのスノーフレークの重要性がわかりました。
2フェーズコミット
現在、zookeeperを使用して2フェーズのコミットを実行することは、すでにエントリーレベルのテクノロジーであるため、開始されません。
データベースがアトミック操作をサポートしていない場合は、2フェーズコミットを検討してください。
Javaエンジニアリング、高性能、分散型を学びたい場合は、深遠なことを簡単な方法で無料で説明してください。マイクロサービスの友達、Spring、MyBatis、Nettyのソースコード分析を私のJava Advanced Groupに追加できます:478030634、グループにはAli Danielsのライブ説明テクノロジー、Javaの大規模インターネットテクノロジーのビデオを無料で共有できます。
結論
つづく。