マイクロサービスのトピック{2} Naocsソースコードデザインの本質はここにあります。インタビュアーを引き裂く機会を与えてください
Nacosは、高い同時読み取りと書き込みをどのように処理しますか?
私は最近ソースコードをよく読んで、ほとんどのフレームワークが同時読み取りと書き込みを解決するときにCOWを使用するという考えを使用していることを発見しました;
nacosも例外ではありません。
解決
並行データを格納するためのマップを作成するとします。まず、並行シナリオでこのマップから読み取りおよび書き込みを行うときに発生する問題を見てみましょう。
非常に大きなマップへの書き込みには時間がかかり、他のスレッドがこのマップでの読み取りおよび書き込み操作を長時間待機することになります。
それで、それはナコスでどのように解決されますか?
実際、nacos処理のアイデアは非常に単純です。簡単に要約してから、ソースコードに従って、コードの記述方法を示します。
- まず、naocsは登録リストマップをmap1としてメモリにコピーします
- 次に、クライアントによって同期された登録キーをmap1に追加します。
- すべてのキーを処理した後、map1をメモリ内の登録リストマップにコピーします
ソースコードの追跡
ソースコードを読んで、nacosが登録リストを更新する方法を見つけました。
com.alibaba.nacos.naming.core.Cluster.updateIPs()
public void updateIPs(List<Instance> ips, boolean ephemeral) {
// 首先判断是需要更新临时注册列表还是持久化的注册列表(这个会在后面讲解ap/cp提到)
Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;
// 创建一个map,来保存内存中的注册列表
HashMap<String, Instance> oldIPMap = new HashMap<>(toUpdateInstances.size());
// 遍历注册列表,依次添加到副本中
for (Instance ip : toUpdateInstances) {
oldIPMap.put(ip.getDatumKey(), ip);
}
// 省略处理key的过程
toUpdateInstances = new HashSet<>(ips);
// 将更新后的注册列表 重新复制到内存注册列表中
if (ephemeral) {
ephemeralInstances = toUpdateInstances;
} else {
persistentInstances = toUpdateInstances;
}
}
登録センターとしてのユーレカは、どのようにして高い同時読み取りと書き込みを実現していますか?
eurekaでは、マルチレベルのキャッシュ構造を使用して、読み取りと書き込みの同時実行率が高いという問題を解決します。
Eurekaは、読み取り専用登録リストと読み取り/書き込み登録リストを作成
します。クライアントが登録を開始または終了すると、eurekaは最初に最新の登録リストのコンテンツを読み取り/書き込み登録リストに更新し、eurekaがTimedを開始すると作成します。タスクでは、読み取り/書き込み登録リストの内容を読み取り専用登録リストに定期的に同期します。クライアントがサービス検出を実行すると、読み取り専用登録リストから使用可能なサービスリストを取得します。
Nacosのapとcpはどうしたのですか
分散関連のフレームワークを学習するとき、CAP理論なしではできないので、ここではCAP理論をあまり紹介しません。
なぜnacosがapとcpの両方をサポートできるのか疑問に思う理由は、インタビューの過程でよくあることです。この記事を読んだら、インタビュアーを手で引き裂くことができるはずだと思います。
序文
nacosでは、apとcpは主に、登録情報をクラスター内の他のクラスターノードに同期する方法の実装に反映されます
。nacosは、エフェメラルフィールド値を使用して、ap同期とcp同期のどちらを使用するかを決定します。同期登録のデフォルトのapメソッド情報。
ソースコードを読むことで、このコードを見つけることができます。このコードを見つける方法は、nacosソースコードの解釈に関する記事で説明されています。
com.alibaba.nacos.naming.core.ServiceManager.addInstance()
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips) throws NacosException {
// 生成服务的key
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
// 获取服务
Service service = getService(namespaceId, serviceName);
// 使用同步锁处理
synchronized (service) {
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
Instances instances = new Instances();
instances.setInstanceList(instanceList);
// 调用consistencyService.put 处理同步过来的服务
consistencyService.put(key, instances);
}
}
私たちはconsistencyService.putメソッドを入力しています
putメソッドをクリックすると、3つの実装クラスが表示されます。コンテキスト(またはデバッグメソッド)に応じて、DelegateConsistencyServiceImpl実装クラスがここで参照されていると推測できます。
@Override
public void put(String key, Record value) throws NacosException {
// 进入到这个put方法后,就可以知道应该使用ap方式同步还是cp方式同步
mapConsistencyService(key).put(key, value);
}
次の方法から、apとcpのどちらを使用して登録情報をキーで同期するかを判断できます。ここで、キーはエフェメラルフィールドで構成されています。
private ConsistencyService mapConsistencyService(String key) {
return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService : persistentConsistencyService;
}
APモード同期プロセス(ephemeralConsistencyService)
ローカルサーバーは登録情報を処理し、登録情報を他のノードに同期します
@Override
public void put(String key, Record value) throws NacosException {
// 处理本地注册列表
onPut(key, value);
// 添加阻塞任务,同步信息到其他集群节点
taskDispatcher.addTask(key);
}
ローカルに登録されたノードを処理する
Nacosはキーをタスクとして使用し、notiferのブロッキングキュータスクに追加し、シングルスレッド実行を使用します。notiferが初期化されると、スレッドとしてスレッドプールに配置されます(スレッドプールはコアスレッドのみを設定します) );
ほとんどの分散フレームワークでは、時間のかかるタスクを処理するためにシングルスレッドのブロッキングキューが使用されます。一方では同時実行の問題を解決でき、他方では書き込みを解決できます。 -並行性によって引き起こされる書き込みの競合。
スレッドの主な処理ロジックは、ブロッキングキューの内容を周期的に読み取り、登録情報を処理してメモリ登録リストに更新することです。
登録情報を他のクラスターノードに同期します
Nacosは、登録キーをタスクとしてTaskDispatcherのtaskSheduleブロッキングキューに格納し、スレッドを開始して、ループでブロッキングキューを読み取ります。
@Override
public void run() {
List<String> keys = new ArrayList<>();
while (true) {
String key = queue.poll(partitionConfig.getTaskDispatchPeriod(),
TimeUnit.MILLISECONDS);
// 省略判断代码
// 添加同步的key
keys.add(key);
// 计数
dataSize++;
// 判断同步的key大小是否等于 批量同步设置的限量 或者 判断据上次同步时间 是否大于 配置的间隔周期,如果满足任意一个,则开始同步
if (dataSize == partitionConfig.getBatchSyncKeyCount() ||
(System.currentTimeMillis() - lastDispatchTime) > partitionConfig.getTaskDispatchPeriod()) {
// 遍历所有集群节点,直接调用http进行同步
for (Server member : dataSyncer.getServers()) {
if (NetUtils.localServer().equals(member.getKey())) {
continue;
}
SyncTask syncTask = new SyncTask();
syncTask.setKeys(keys);
syncTask.setTargetServer(member.getKey());
if (Loggers.DISTRO.isDebugEnabled() && StringUtils.isNotBlank(key)) {
Loggers.DISTRO.debug("add sync task: {}", JSON.toJSONString(syncTask));
}
dataSyncer.submit(syncTask, 0);
}
// 记录本次同步时间
lastDispatchTime = System.currentTimeMillis();
// 计数清零
dataSize = 0;
}
}
}
}
apを使用した同期のプロセスは非常に単純ですが、単一キーの同期の問題を解決するための2つの設計アイデアがあります。
新しいキーが押されると、nacosが同期を開始します。これにより、ネットワークリソースが浪費されます。時間1つまたは複数のキーのみが同期されます。
少数の主要なソリューションを同期します。
- 指定された数のキーが蓄積された場合にのみ、バッチ同期が開始されます
- 最後の同期時間が設定された制限時間を超えているため、キーの数は無視され、同期が直接開始されます
CPモード同期プロセス(RaftConsistencyServiceImpl)
cpモードはデータの整合性を追求します。データの整合性を保つには、リーダーを選択する必要があります。リーダーは最初に同期し、次にリーダーはフォロワーに通知して、最新の登録済みノードを取得します(または積極的にフォロワーにプッシュします)。
Nacosは、raftプロトコルを使用して、cpモードを実装するリーダーを選出します。
RaftConsistencyServiceImplのputメソッドも入力します
@Override
public void put(String key, Record value) throws NacosException {
try {
raftCore.signalPublish(key, value);
} catch (Exception e) {
Loggers.RAFT.error("Raft put failed.", e);
throw new NacosException(NacosException.SERVER_ERROR, "Raft put failed, key:" + key + ", value:" + value, e);
}
}
raftCore.signalPublishメソッドに、いくつかのキーコードを抽出しました
// 首先判断当前nacos节点是否是leader,如果不是leader,则获取leader节点的ip,然后将请求转发到leader处理,否则往下走
if (!isLeader()) {
JSONObject params = new JSONObject();
params.put("key", key);
params.put("value", value);
Map<String, String> parameters = new HashMap<>(1);
parameters.put("key", key);
raftProxy.proxyPostLarge(getLeader().ip, API_PUB, params.toJSONString(), parameters);
return;
}
//同じキューメソッドを使用してローカル登録リストを処理します
onPublish(datum, peers.local());
public void onPublish(Datum datum, RaftPeer source) throws Exception {
// 添加同步key任务到阻塞队列中
notifier.addTask(datum.key, ApplyAction.CHANGE);
Loggers.RAFT.info("data added/updated, key={}, term={}", datum.key, local.term);
}
すべてのクラスターノードをトラバースし、http同期要求を送信します
for (final String server : peers.allServersIncludeMyself()) {
// 如果是leader,则不进行同步
if (isLeader(server)) {
latch.countDown();
continue;
}
// 组装url 发送同步请求到其它集群节点
final String url = buildURL(server, API_ON_PUB);
HttpClient.asyncHttpPostLarge(url, Arrays.asList("key=" + key), content, new AsyncCompletionHandler<Integer>() {
@Override
public Integer onCompleted(Response response) throws Exception {
if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
Loggers.RAFT.warn("[RAFT] failed to publish data to peer, datumId={}, peer={}, http code={}",
datum.key, server, response.getStatusCode());
return 1;
}
latch.countDown();
return 0;
}
@Override
public STATE onContentWriteCompleted() {
return STATE.CONTINUE;
}
});
}
ここでは、各クラスターノードによる同期要求の処理についてはあまり紹介しません。自分で確認できます。
Wechatで検索を検索[LeZaiオープントーク]ハンサムな私をフォローし、[乾物を受け取る]と返信すると、Javaの基本、Javaの同時実行など、選択を待っているインタビュー資料や建築家の必読の本がたくさんあります。マイクロサービス、ミドルウェアなど。より多くの情報があなたを待っています。