nacos の基本的な使い方は上で紹介しましたが、nacos についてはこの記事でさらに紹介します。
nacos クラスターのデータ同期: ディストリビューション プロトコル
本番環境ではサービスの安定性を確保するため、クラスタデプロイ方式を採用するのが一般的ですが、前回の記事でもクラスタデプロイ方式を紹介しましたが、nacosクラスタ間でデータを同期するにはどうすればよいでしょうか?前回の nacos クラスタの展開方法からもわかるように、nacos にはマスターノードとスレーブノードの区別がなく、各ノードは同じ重みを持ち、読み書き可能です。
nacosは、設定センターと登録/検出センターの2つの機能から構成されます。構成センターについては、nacos によって使用される cp は強力な一貫性があるため、あまり紹介しませんが、登録/検出センターについては、nacos によって使用される ap は、Ali が独自に開発したディストリビューション プロトコルによって実装されます。詳細については、導入部で説明します。
ap が最終的な一貫性であることはわかっていますが、ディストリビューション プロトコルはどのようにして最終的な一貫性を保証するのでしょうか? 主に以下の4つの観点から実施されます。
1 ディストリビューション サービス ノードを追加します
このシナリオは、現在のディストリビューション クラスターでサービス ノードが実行されているという事実を指します。このとき、新しいディストリビューション サービス ノードが追加されます。新しく追加されたディストリビューション サービス ノードはデータ同期をどのように実行しますか?
各ディストリビューション サービス ノードは相互に通信します。ディストリビューション サービス ノードが追加されると、現在のディストリビューション サービス クラスター内の他のすべてのサービス ノードをポーリングしてプルし、完全なプルを実行してから、レジストリ情報をローカルにキャッシュし、対応するサービスの登録が行われます。
2 ディストリビューションサービスノードのデータ同期
ディストリビューション サービス ノードは 5 秒ごとにハートビート チェックを送信します。チェック情報はレジストリ情報ではなく、メタデータ情報です。レジストリ情報を使用すると、ハートビート パケット内のリクエスト データが大きすぎるため、ハートビート チェックは行われません。リクエストデータはメタデータ情報です。検証プロセス中に現在のハートビート リクエスト データがローカル キャッシュ データと矛盾していることが判明した場合、サーバーはデータ同期のための完全なプル操作をトリガーします。
3 データ書き込み動作
クライアントが書き込み操作を発行するとき、書き込み操作のディストリビューション サービス ノードに直接リンクしません。最初に 1 つずつプレフィルターに進みます。フィルターは IP+ポートに従って計算し、ディストリビューション サービスを見つけますルートが対応するフィルターに転送され、書き込み操作を実行するために対応するディストリビューション サービス ノードが呼び出されます。書き込み操作では、このノードで書き込みが完了していることを確認するだけで済みます。他のディストリビューション サービス ノードへの同期書き込みを待つ必要はありません。書き込み操作を実行するサービス ノードは、増分方式で変更情報を他のサービス ノードに定期的に同期します。
フィルタのルーティングと転送は、ディストリビューション サービス プロトコルの実装の中核ポイントです。これにより、クライアントが属するディストリビューション サービス ノードが変更されないことが保証されます。クライアントが読み取りおよび書き込み操作を実行するたびに、要求されたディストリビューション サービスは同じ。
4 読み取り動作
読み取り操作は実際には書き込み操作と似ています。これらはすべて、最初にプレフィルターに進み、次に IP+ポートに従って計算し、それが属するディストリビューション サービス ノードにルーティングします。上記の紹介では、nacos が述べています。クラスターは ap なので、直接読み取り、ディストリビューション サービスのローカル キャッシュを取得して戻ります。ap が保証するのは最終的な一貫性であり、これは前述のデータ検証ハートビート メカニズムを通じて保証できます。
クラスターデータ同期のソースコードの一部を見てみましょう. エントリのソースコードといくつかの重要な手順のみが記載されています. 興味のある学生は自分で見てください.
以下は、データ検証を有効にし、データを初期化するためのソース コードの一部です。
// 入口 com.alibaba.nacos.core.distributed.distro.DistroProtocol
// 在入口类的构造方法中,调用了startDistroTask()方法,接下来我们看一下这个方法
/**
*该方法主要是开始两个任务,一个是验证任务,一个是初始化任务
*/
private void startDistroTask() {
if (EnvUtil.getStandaloneMode()) {
isInitialized = true;
return;
}
// 验证任务
startVerifyTask();
// 初始化任务
startLoadTask();
}
/**
* 开启数据验证的定时任务,每5s发起一次
* DEFAULT_DATA_VERIFY_INTERVAL_MILLISECONDS = 5000L;
*/
private void startVerifyTask() {
GlobalExecutor.schedulePartitionDataTimedSync(new DistroVerifyTimedTask(memberManager, distroComponentHolder,
distroTaskEngineHolder.getExecuteWorkersManager()),
DistroConfig.getInstance().getVerifyIntervalMillis());
}
/**
* 初始化任务,通过线程池执行任务,并传入一个回调函数,用来标识是否完成
*/
private void startLoadTask() {
DistroCallback loadCallback = new DistroCallback() {
@Override
public void onSuccess() {
isInitialized = true;
}
@Override
public void onFailed(Throwable throwable) {
isInitialized = false;
}
};
GlobalExecutor.submitLoadDataTask(
new DistroLoadDataTask(memberManager, distroComponentHolder, DistroConfig.getInstance(), loadCallback));
}
// 接下来再看一下DistroLoadDataTask这个类的实现,在它的run方法中主要是执行了一个load方法
// 我们直接看一下这个load()方法
// 这个方法主要是进行了一些条件的判断,需要注意它使用的while循环操作,如果前面的天剑一直满足
// 则会进入死循环中,因此必须打破前面的两个while条件才会进入最终的数据初始化任务
private void load() throws Exception {
// 除了自身节点以外没有其它节点,则休眠1s
while (memberManager.allMembersWithoutSelf().isEmpty()) {
Loggers.DISTRO.info("[DISTRO-INIT] waiting server list init...");
TimeUnit.SECONDS.sleep(1);
}
// 若数据类型为空,说明distroComponent的组件注册还未初始化完毕
while (distroComponentHolder.getDataStorageTypes().isEmpty()) {
Loggers.DISTRO.info("[DISTRO-INIT] waiting distro data storage register...");
TimeUnit.SECONDS.sleep(1);
}
// 加载每个类型的数据
for (String each : distroComponentHolder.getDataStorageTypes()) {
if (!loadCompletedMap.containsKey(each) || !loadCompletedMap.get(each)) {
// 调用加载方法,标记已处理
loadCompletedMap.put(each, loadAllDataSnapshotFromRemote(each));
}
}
}
// 本方法就是具体的去拉取各个distro服务节点,并更新数据的方法
private boolean loadAllDataSnapshotFromRemote(String resourceType) {
// 获取数传输对象
DistroTransportAgent transportAgent = distroComponentHolder.findTransportAgent(resourceType);
// 获取数据处理器
DistroDataProcessor dataProcessor = distroComponentHolder.findDataProcessor(resourceType);
if (null == transportAgent || null == dataProcessor) {
Loggers.DISTRO.warn("[DISTRO-INIT] Can't find component for type {}, transportAgent: {}, dataProcessor: {}",
resourceType, transportAgent, dataProcessor);
return false;
}
// 向每个节点请求数据
for (Member each : memberManager.allMembersWithoutSelf()) {
try {
Loggers.DISTRO.info("[DISTRO-INIT] load snapshot {} from {}", resourceType, each.getAddress());
// 获取数据
DistroData distroData = transportAgent.getDatumSnapshot(each.getAddress());
// 解析数据并且更新数据
boolean result = dataProcessor.processSnapshot(distroData);
Loggers.DISTRO
.info("[DISTRO-INIT] load snapshot {} from {} result: {}", resourceType, each.getAddress(),
result);
// 如果解析成功,标记此类型数据已经加载完毕
if (result) {
distroComponentHolder.findDataStorage(resourceType).finishInitial();
return true;
}
} catch (Exception e) {
Loggers.DISTRO.error("[DISTRO-INIT] load snapshot {} from {} failed.", resourceType, each.getAddress(), e);
}
}
return false;
}
以下は、増分データ同期のソース コードです。
// 入口为com.alibaba.nacos.naming.consistency.ephemeral.distro.v2.DistroClientDataProcessor
// 增量数据同步是采用发布订阅的方式进行的数据同步
// 主要关注下面的客户端变更事件即可
@Override
public List<Class<? extends Event>> subscribeTypes() {
List<Class<? extends Event>> result = new LinkedList<>();
// 客户端变更的时事件
result.add(ClientEvent.ClientChangedEvent.class);
// 客户端断开的事件
result.add(ClientEvent.ClientDisconnectEvent.class);
// 服务验证失败的事件
result.add(ClientEvent.ClientVerifyFailedEvent.class);
return result;
}
// 当事件触发的时候,会调用该类的onEvent()方法
@Override
public void onEvent(Event event) {
if (EnvUtil.getStandaloneMode()) {
return;
}
if (!upgradeJudgement.isUseGrpcFeatures()) {
return;
}
if (event instanceof ClientEvent.ClientVerifyFailedEvent) {
syncToVerifyFailedServer((ClientEvent.ClientVerifyFailedEvent) event);
} else {
// 将该事件同步到其它distro服务节点,
// 延迟1s进行同步,DEFAULT_DATA_SYNC_DELAY_MILLISECONDS = 1000L
syncToAllServer((ClientEvent) event);
}
}
2. 構成センターの高度な使用法と原理
前回の記事では、構成センターとしての nacos の簡単な使い方について基本的な紹介をしましたが、この記事では 2 つの高度な使い方を紹介します。
1 高度な使い方
高度な使用法は、主に次の 2 つのシナリオの構成に使用されます。
最初のシナリオは、一部の構成では、開発環境、テスト環境、本番環境のいずれであっても、その構成は同じであるというものです。この構成の場合、各環境で構成する必要はありません。パブリック構成ファイルを使用して構成できます。設定ファイルに以下の設定を追加することで実現できます。
spring:
cloud:
nacos:
config:
server-addr: 172.30.10.103:8848
file-extension: yaml
namespace: 4b57e563-2039-42f4-86b1-9c4c7cf58bfc
# 公共配置文件,可以配置多个
shared-configs[0]:
# 公共配置文件名称
dataId: file.yaml
# 公共配置文件所属组
group: DEFAULT_GROUP
# 公共配置文件是否刷新
refresh: true
2 番目のシナリオは、プロジェクト内で構成ファイルの責任のある内容を区別するために、注文関連の構成とユーザー関連の構成など、複数の構成ファイルを使用する必要がある場合です。これは、次の構成によって実現できます。
spring:
cloud:
nacos:
config:
server-addr: 172.30.10.103:8848
file-extension: yaml
namespace: 4b57e563-2039-42f4-86b1-9c4c7cf58bfc
# 扩展配置文件,允许配置多个
extension-configs[0]:
# 扩展配置文件名称
dataId: order.yaml
# 扩展配置文件所属组
group: DEFAULT_GROUP
# 扩展配置文件动态刷新
refresh: true
extension-configs[1]:
dataId: user.yaml
group: DEFAULT_GROUP
refresh: true
2 構成中心の原則
クライアントが構成センターから関連する構成を動的に取得できることは誰もが知っていますが、これは、クライアントをアクティブにプルするか、サーバーをアクティブにプッシュする 2 つの方法にすぎません。その後、nscos クライアントは、プルまたはプッシュを使用してサーバーから構成を取得します。 ?
nacos クライアントは、プルによって動的構成を取得します クライアントは、サーバーとのロング ポーリングを確立します ロング ポーリングの確立中に、サーバーの構成が変更されると、構成が変更されたことをクライアントに通知し、クライアントは端末からの応答を開始します構成の特定のコンテンツを取得するリクエスト。変更がない場合は空を返します。以下のフローチャートを見てください。
上の図から、クライアントがサーバーとの長い接続を確立することがわかります。長い接続が応答する状況は 2 つあります: 1 つは待機時間が 29.5 秒を超えた場合、もう 1 つは設定内容が変更された場合です。 。その中のタイムアウト時間、待ち時間、応答時間等については、ソースコード解析に反映させていただきます。上の図を通して、nacos 構成センターの動的構成取得原理をある程度理解しました。次に、ソース コードを簡単に見てみましょう。興味のある学生は、それについてさらに詳しく学ぶことができます。ソース コードの内容は 2 つの部分に分かれており、1 つの部分はクライアントが長いリンクを確立して構成更新構成を取得する部分であり、もう 1 つの部分はサーバーが構成の変更を受信してクライアントに応答する部分です。
2.1 クライアントのソースコード (nacos-config-2.2.5.release、nacos-client-1.4.1)
次のソース コードでは、主にクライアント ソース コードのエントリを紹介し、重要なクラス ClientWorker を紹介します。
// 入口为com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
// 通过jar包进入,这个类创建了三个bean,我们主要关注的是第二个bean:NacosConfigManager
// 通过查看该类的构造方法,发现其调用了createConfigService方法,在这个方法中通过工厂的方式创建
// service,最终是通过反射的方式创建:
// Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService")
// 我们的源码从NacosConfigService的构造方法开始
public NacosConfigService(Properties properties) throws NacosException {
// 参数校验
ValidatorUtils.checkInitParam(properties);
String encodeTmp = properties.getProperty("encode");
if (StringUtils.isBlank(encodeTmp)) {
this.encode = "UTF-8";
} else {
this.encode = encodeTmp.trim();
}
// 根据配置文件获取namespace
this.initNamespace(properties);
this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
this.agent.start();
// 该构造方法的主要目的就是创建ClientWorker对象,所有的相关操作都是在该对象中实现的
this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}
ClientWorker クラスのコンストラクターでは、タイムアウト期間を含むいくつかの主要なパラメーターが初期化され、2 つの時間制限付きスレッド プールが作成され、構成チェック タスクが開始されます。
public ClientWorker(final HttpAgent agent, ConfigFilterChainManager configFilterChainManager, Properties properties) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
// 初始化参数
this.init(properties);
// 创建第一个线程池,用于启动配置检查任务
this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
t.setDaemon(true);
return t;
}
});
// 第二个定时任务线程池,具体功能后续会出现
this.executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
t.setDaemon(true);
return t;
}
});
// 启动第一个定时任务线程池,用于检查配置,通过线程池参数可以发现,该任务每10s执行一次
this.executor.scheduleWithFixedDelay(new Runnable() {
public void run() {
try {
ClientWorker.this.checkConfigInfo();
} catch (Throwable var2) {
ClientWorker.LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", var2);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
}
// 根据配置信息,初始化相关参数
private void init(Properties properties) {
// 超时时间,properties这个参数是根据配置文件生成的,如果没有配置超时时间,我们可以发现,超时时间为30s
this.timeout = (long)Math.max(ConvertUtils.toInt(properties.getProperty("configLongPollTimeout"), 30000), 10000);
this.taskPenaltyTime = ConvertUtils.toInt(properties.getProperty("configRetryTime"), 2000);
this.enableRemoteSyncConfig = Boolean.parseBoolean(properties.getProperty("enableRemoteSyncConfig"));
}
次のステップは checkConfigInfo メソッドです。このメソッドの主なタスクは、ロング ポーリング タスクをグループ化し、上で作成した executorService スレッド プール LongPollingRunnable を通じてロング ポーリング タスクを実行することです。ロング ポーリング タスクを直接確認してみましょう。 、その実行メソッド。このコードでは、主に 3 つのことが行われます: 最初のステップは、ローカル構成を確認し、さまざまな状況に応じて関連する値を割り当てることです。2 番目のステップは、変更された構成を取得するためにサーバーとの長いリンクを確立することです。ステップ , 前のステップで返された変更情報は構成コンテンツではなく、変更された dataId+group+teanant 関連情報です。このステップでは、サーバーを呼び出して特定の構成コンテンツを取得し、これらの情報に基づいてローカル更新を実行します。
public void run() {
List<CacheData> cacheDatas = new ArrayList();
ArrayList inInitializingCacheList = new ArrayList();
try {
// cacheMap即为缓存的配置信息
Iterator var3 = ClientWorker.this.cacheMap.values().iterator();
// 第一个for循环,比较本地配置
while(var3.hasNext()) {
CacheData cacheData = (CacheData)var3.next();
if (cacheData.getTaskId() == this.taskId) {
cacheDatas.add(cacheData);
try {
// 第一步,检查本地配置,并根据配置的不同信息,进行相关赋值
ClientWorker.this.checkLocalConfig(cacheData);
if (cacheData.isUseLocalConfigInfo()) {
cacheData.checkListenerMd5();
}
} catch (Exception var13) {
ClientWorker.LOGGER.error("get local config info error", var13);
}
}
}
// 第二步 与服务端建立长轮询,获取变更的配置信息
List<String> changedGroupKeys = ClientWorker.this.checkUpdateDataIds(cacheDatas, inInitializingCacheList);
if (!CollectionUtils.isEmpty(changedGroupKeys)) {
ClientWorker.LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
}
Iterator var16 = changedGroupKeys.iterator();
// 第二个for循环,根据上面获取到的变更的配置信息集合,更新本地配置
while(var16.hasNext()) {
String groupKey = (String)var16.next();
String[] key = GroupKey.parseKey(groupKey);
String dataId = key[0];
String group = key[1];
String tenant = null;
if (key.length == 3) {
tenant = key[2];
}
try {
// 第三步 调用服务端获取变更的配置信息,根据dataId, group以及tenant,调用地址为/v1/cs/configs
String[] ct = ClientWorker.this.getServerConfig(dataId, group, tenant, 3000L);
CacheData cache = (CacheData)ClientWorker.this.cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
cache.setContent(ct[0]);
if (null != ct[1]) {
cache.setType(ct[1]);
}
ClientWorker.LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}", new Object[]{ClientWorker.this.agent.getName(), dataId, group, tenant, cache.getMd5(), ContentUtils.truncateContent(ct[0]), ct[1]});
} catch (NacosException var12) {
String message = String.format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s", ClientWorker.this.agent.getName(), dataId, group, tenant);
ClientWorker.LOGGER.error(message, var12);
}
}
var16 = cacheDatas.iterator();
while(true) {
CacheData cacheDatax;
do {
if (!var16.hasNext()) {
inInitializingCacheList.clear();
ClientWorker.this.executorService.execute(this);
return;
}
cacheDatax = (CacheData)var16.next();
} while(cacheDatax.isInitializing() && !inInitializingCacheList.contains(GroupKey.getKeyTenant(cacheDatax.dataId, cacheDatax.group, cacheDatax.tenant)));
cacheDatax.checkListenerMd5();
cacheDatax.setInitializing(false);
}
} catch (Throwable var14) {
ClientWorker.LOGGER.error("longPolling error : ", var14);
ClientWorker.this.executorService.schedule(this, (long)ClientWorker.this.taskPenaltyTime, TimeUnit.MILLISECONDS);
}
}
次に、2 番目のステップでサーバーとのロングリンクを確立する内容を見てください。
// 在checkUpdateDataIds里面主要是组装了调用服务端时的请求信息,
// 每个配置文件对应的相关信息为dataId++group++md5+(+teaant)
// 组装完请求参数以后,调用checkUpdateConfigStr方法,接下来我们看一下相关代码
List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {
// 设置请求参数以及请求头
Map<String, String> params = new HashMap(2);
params.put("Listening-Configs", probeUpdateString);
Map<String, String> headers = new HashMap(2);
// 超时时间在前面的源码中说过,默认为30s
headers.put("Long-Pulling-Timeout", "" + this.timeout);
// 如果是第一次请求,不需要进行挂起
if (isInitializingCacheList) {
headers.put("Long-Pulling-Timeout-No-Hangup", "true");
}
if (StringUtils.isBlank(probeUpdateString)) {
return Collections.emptyList();
} else {
try {
long readTimeoutMs = this.timeout + (long)Math.round((float)(this.timeout >> 1));
// 这里的agent就是在最开始的NcaosConfigService的构造方法中创建的agent
// 开始远程调用服务端的服务,记住这个地址,后面查看服务端源码的时候,即从这个地址入手
HttpRestResult<String> result = this.agent.httpPost("/v1/cs/configs/listener", headers, params, this.agent.getEncode(), readTimeoutMs);
if (result.ok()) {
this.setHealthServer(true);
// 格式化响应信息
return this.parseUpdateDataIdResponse((String)result.getData());
}
this.setHealthServer(false);
LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", this.agent.getName(), result.getCode());
} catch (Exception var8) {
this.setHealthServer(false);
LOGGER.error("[" + this.agent.getName() + "] [check-update] get changed dataId exception", var8);
throw var8;
}
return Collections.emptyList();
}
}
ここではクライアントのソースコードを解析しますが、内部の具体的な実装を詳しく知りたい場合は、上記の順番で詳しく確認することができます。
2.2 サーバーコード (2.1.0)
上記のクライアントのソース コードを分析する過程で、クライアントとサーバー間のポーリング用のアドレスが /v1/cs/configs/listener であり、プロトコルが post であることがわかりました。このメソッドでは、主に inner.doPollingConfig メソッドに焦点を当てます。
// com.alibaba.nacos.config.server.controller.ConfigController的listener方法
// 然后再改方法中,我们关注的重点是doPollingConfig方法调用
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {
.
// 判断当前请求是否为长轮询,通过对客户端源码的分析,这里走的是长轮询的机制
if (LongPollingService.isSupportLongPolling(request)) {
longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
return HttpServletResponse.SC_OK + "";
}
...
}
上記のコードの分析により、ロング ポーリングのブランチに移動することがわかります。このブランチでは、主に 3 つの処理が実行されます: 1. タイムアウト時間を取得する; 2. 同期リクエストを非同期リクエストに変換し、時間を短縮しますサーバーの負荷 同期リクエストの数; 3. ロングポーリングの開始
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
int probeRequestSize) {
// 获取超时时间
String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
// 不允许断开的标记
String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
// 应用名称
String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
String tag = req.getHeader("Vipserver-Tag");
// 延时时间
int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
// 提前500s返回一个响应,避免客户端出现超时,超时时间计算为29.5s
long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
if (isFixedPolling()) {
timeout = Math.max(10000, getFixedPollingInterval());
// Do nothing but set fix polling timeout.
} else {
long start = System.currentTimeMillis();
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
if (changedGroups.size() > 0) {
generateResponse(req, rsp, changedGroups);
LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant",
RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
changedGroups.size());
return;
} else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
changedGroups.size());
return;
}
}
// 获取客户端ip
String ip = RequestUtil.getRemoteIp(req);
// 把当前请求转换为一个异步请求(意味着tomcat线程被释放,最后需要asyncContext来手动完成响应)
final AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(0L);
// 开始执行长轮询,通过线程池创建执行任务,线程池类型为SingleScheduledExecutorService
ConfigExecutor.executeLongPolling(
new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}
次に注目する必要があるのは、スレッド プールによって実行されるこのロング ポーリング タスクです。次に、ClientLongPolling の run() メソッドに注目する必要があります。このメソッドは主に 2 つの部分に分かれています。 29.5 秒の遅延後に開始します 長いリンク応答を作成します。その一環として、長いリンクを allsubs に入れます。
public void run() {
// 构建一个异步任务,延后29.5s执行,如果达到29.5秒以后没有做任何配置的修改,则自行触发执行,即进行长链接响应
asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(() -> {
try {
getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
// 获取删除标识,该标识是一个防止重复响应的标识,在前面讲述原理的时候说过,
// 长链接的响应有两种情况,一种是超时响应,一种是变更响应,就是使用该标识进行判断
boolean removeFlag = allSubs.remove(ClientLongPolling.this);
// 如果删除成功,代表的就是超时响应;删除失败,代表是变更响应
if (removeFlag) {
if (isFixedPolling()) {
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix",
RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
"polling", clientMd5Map.size(), probeRequestSize);
List<String> changedGroups = MD5Util
.compareMd5((HttpServletRequest) asyncContext.getRequest(),
(HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
if (changedGroups.size() > 0) {
sendResponse(changedGroups);
} else {
// 没有变更,返回null
sendResponse(null);
}
} else {
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "timeout",
RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
"polling", clientMd5Map.size(), probeRequestSize);
sendResponse(null);
}
} else {
LogUtil.DEFAULT_LOG.warn("client subsciber's relations delete fail.");
}
} catch (Throwable t) {
LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
}
}, timeoutTime, TimeUnit.MILLISECONDS);
// 将当前请求放入长轮询队列
allSubs.add(this);
}
上記は原理説明のタイムアウト応答に関するソースコード情報ですが、操作応答に関するソースコードはどこにあるのでしょうか? 次に、LongPollingService の構築メソッドに進みます。このメソッドでは、データの変更をリッスンするためにサブスクリプション イベントが登録されます。
public LongPollingService() {
allSubs = new ConcurrentLinkedQueue<>();
ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);
NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);
// 注册订阅事件,用来监听配置变化
NotifyCenter.registerSubscriber(new Subscriber() {
@Override
public void onEvent(Event event) {
if (isFixedPolling()) {
// Ignore.
} else {
if (event instanceof LocalDataChangeEvent) {
LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
}
@Override
public Class<? extends Event> subscribeType() {
return LocalDataChangeEvent.class;
}
});
}
次に、DataChangeTask をフォローアップして、操作応答のソース コードである run メソッドを見つけます。
public void run() {
try {
ConfigCacheService.getContentBetaMd5(groupKey);
// 遍历所有客户端建立的获取配置变化信息的长轮询
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
ClientLongPolling clientSub = iter.next();
// 判断当前的ClientLongPolling中,请求的key是否包含当前修改的groupkey
if (clientSub.clientMd5Map.containsKey(groupKey)) {
if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
continue;
}
if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
continue;
}
getRetainIps().put(clientSub.ip, System.currentTimeMillis());
// 将该长轮询从等待队列中移除,当移除以后,上面源码中的removeFlag则为false
iter.remove();
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - changeTime), "in-advance",
RequestUtil
.getRemoteIp((HttpServletRequest) clientSub.asyncContext.getRequest()),
"polling", clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
// 响应客户端
clientSub.sendResponse(Arrays.asList(groupKey));
}
}
} catch (Throwable t) {
LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));
}
}
nacos 登録センターの 3 つの原則
nacos が登録センターとして使用される場合、プロジェクトの可用性を確保するために AP が使用されます。バージョン 2.0 以降、クライアントとサーバーは grpc プロトコルの長いリンクを確立しました。クライアントは 5 秒ごとにハートビート タスクをサーバーに送信し、サーバーは定期的にハートビート タスクをチェックします。インスタンスを異常としてマークするか、インスタンスを削除します。 。
サーバーと各クライアントにはレジストリ情報があり、クライアントがサービス呼び出しを行うと、レジストリ情報はリモートで取得されるのではなく、ローカルで読み取られます。
クライアントはサーバーとの長いリンクを確立します。2.0 より前では、クライアントはサーバーにハートビート タスクを送信します。クライアントは 5 秒ごとにハートビート タスクをサーバーに送信します。サーバーはハートビート タスクを定期的に検出し、ハートビートは送信されません。 15 秒を超えるタスクでは、クライアントを異常なインスタンスとしてマークし、ハートビート タスクが 30 秒以上送信されない場合、インスタンスは削除されますが、2.0 以降では、grpc ロング リンクが使用され、ハートビートは送信されなくなります。使用され、サーバーは 3 秒以内にスケジュールされたタスクをアクティブに作成します。 1 回実行し、20 秒以上通信していないクライアントを確認し、忘れずにプローブ要求を送信します。応答が 1 秒以内であれば検出は成功し、それ以外の場合はリンクが切断されます。が削除されます。
クライアントが自発的にオフラインになるか、サーバーによって削除されると、サーバーはレジストリ変更イベントを正常な各インスタンスに送信し、クライアントはそれを再度プルします。クライアント側にはスケジュールされたタスクがあり、6 秒ごとに登録センターから登録リストを取得し、変更イベントを発行し、サブスクライバーがローカル レジストリのデータを更新します。
3.1 クライアント
nacos ソース コードの NamingTest クラスを使用して、クライアントの登録をシミュレートします。もちろん、プロジェクト内の対応する nacos jar パッケージを通じて分析することもできます。エントリは NacosServiceRegistryAutoConfiguation#nacosServiceRegistry です。興味のある学生は、このエントリを通じてそれを表示できます。
ソース コード内の NamingTest クラスを分析に使用する理由は、ソース コードを読んだ経験のない学生にとって、このクラスの方が親しみやすく、関連する情報とその手順が比較的明確で明白であるためです。入口では主に、1. サーバーの関連情報の設定、2. 登録するインスタンス情報の設定、3. NacosNamingService の取得と登録を行います。
public void testServiceList() throws Exception {
// nacos服务端的地址以及用户名和密码
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848");
properties.put(PropertyKeyConst.USERNAME, "nacos");
properties.put(PropertyKeyConst.PASSWORD, "nacos");
// 客户端实例的相关信息,包含ip,port,原信息等相关数据
Instance instance = new Instance();
instance.setIp("1.1.1.1");
instance.setPort(800);
instance.setWeight(2);
// 元数据信息,对客户端的相关描述
Map<String, String> map = new HashMap<String, String>();
map.put("netType", "external");
map.put("version", "2.0");
instance.setMetadata(map);
// 通过工厂方法创建namingService,最终是通过反射的方式进行创建,com.alibaba.nacos.client.naming.NacosNamingService
NamingService namingService = NacosFactory.createNamingService(properties);
// 进入具体的注册流程
namingService.registerInstance("nacos.test.1", instance);
...
}
次に、特定の登録プロセスに入り、NacosNamingService#registerInstance のオーバーロードされたメソッドにジャンプします。このメソッドでは、主に 1. ハートビート時間の確認、2. プロキシ経由でのインスタンスの登録の 2 つの処理が行われます。エージェントを使用する理由 以前のバージョンとの互換性のためです。2.0 より前は http プロトコルが登録に使用され、2.0 以降は grpc プロトコルが登録に使用されていました。ソース コード分析は、ロング リンク バージョンである 2.x バージョンに基づいています。 。
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
// 检查心跳,心跳间隔时间以及服务删除时间必须大于心跳间隔,否则抛出异常
NamingUtils.checkInstanceIsLegal(instance);
// 通过代理
clientProxy.registerService(serviceName, groupName, instance);
}
// 看一下NamingUtils.checkInstanceIsLegal(instance);的相关代码
public static void checkInstanceIsLegal(Instance instance) throws NacosException {
// 心跳超时时间以及服务删除时间必须大于心跳时间,否则抛出异常
// 当我们点击进入心跳时间,心跳超时时间,实例删除时间就会发现,这些时间与上面的原理图上面的时间是对应的
if (instance.getInstanceHeartBeatTimeOut() < instance.getInstanceHeartBeatInterval()
|| instance.getIpDeleteTimeout() < instance.getInstanceHeartBeatInterval()) {
throw new NacosException(NacosException.INVALID_PARAM,
"Instance 'heart beat interval' must less than 'heart beat timeout' and 'ip delete timeout'.");
}
}
// 查看一下心跳的相关时间
// 心跳间隔时间,默认为5s
public long getInstanceHeartBeatInterval() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL,
Constants.DEFAULT_HEART_BEAT_INTERVAL);
}
// 心跳超时时间,默认为15s
public long getInstanceHeartBeatTimeOut() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_TIMEOUT,
Constants.DEFAULT_HEART_BEAT_TIMEOUT);
}
// 删除时间,默认为30s
public long getIpDeleteTimeout() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.IP_DELETE_TIMEOUT,
Constants.DEFAULT_IP_DELETE_TIMEOUT);
}
clientProxy はプロキシです。構築メソッドの init メソッドを通じて、その実装タイプが NamingClientProxyDelegate であることがわかり、ここで実際に登録を担当する実装クラスがわかります。
// 通过代理获取到真正进行注册的类
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
}
// 会根据当前的实例类型进行区分,获取到真正的实现类,如果是瞬时对象(也就是注册实例),
// 则会采用grpcClientProxy,这里默认为true
private NamingClientProxy getExecuteClientProxy(Instance instance) {
return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
}
上記のコードを通じて、登録を実際に担当するクラスを見つけました。その後、NamingGrpcClientProxy#registerService までフォローアップし、このメソッドで 2 つのことを実行します。1. 現在のインスタンス情報をキャッシュする、2. Grpc リモート電話
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,
instance);
// 缓存当前注册的实例信息,key为服务信息+组信息,value为服务信息,组信息,实例信息
redoService.cacheInstanceForRedo(serviceName, groupName, instance);
// grpc远程调用
doRegisterService(serviceName, groupName, instance);
}
public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException {
InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName,
NamingRemoteConstants.REGISTER_INSTANCE, instance);
// grpc远程调用
requestToServer(request, Response.class);
// 注册完成以后,将缓存中的注册状态变为true
redoService.instanceRegistered(serviceName, groupName);
}
grpc プロトコルの特定のソース コードは、ここでは一時的に分析されません。興味のある学生は、NamingGrpcClientProxy コンストラクターの start() メソッドを通じて rpcClient.start() を見つけ、このエントリを通じてソース コードを表示できます。
3.2 サーバー
nacous 公式 Web サイトのOpen API ガイドにあるサービス登録を通じて、ソース コード内のコントローラー (InstanceController) に対応する呼び出しアドレスを見つけ、 register メソッドを見つけることができます。インスタンスの登録方法分析は次のとおりです。
// getInstanceOperator().registerInstance(namespaceId, serviceName, instance);调用实例注册
// getInstanceOperator()方法会根据使用的协议不同,选择不同的service
private InstanceOperator getInstanceOperator() {
// 现在使用的grpc,因此选择instanceServiceV2,类型为InstanceOperatorClientImpl
return upgradeJudgement.isUseGrpcFeatures() ? instanceServiceV2 : instanceServiceV1;
}
次に、特定のサービス登録プロセスに入り、主に 2 つのことを行います: まず、生成された clientId を通じてクライアントとの接続を確立します。次に、クライアント インスタンスを登録します。
public void registerInstance(String namespaceId, String serviceName, Instance instance) {
// 判断是否为临时实例
boolean ephemeral = instance.isEphemeral();
// 获取客户端id,ip:port+#+true
String clientId = IpPortBasedClient.getClientId(instance.toInetAddr(), ephemeral);
// 创建与客户端的链接
createIpPortClientIfAbsent(clientId);
// 获取服务
Service service = getService(namespaceId, serviceName, ephemeral);
// 注册服务实例
clientOperationService.registerInstance(service, instance, clientId);
}
インスタンスを登録するときは、現在のインスタンスが一時インスタンスであるか永続インスタンスであるかに基づいてルートが選択されます。サービス登録の場合、登録されたインスタンスは一時インスタンスであるため、一時インスタンスのルートが使用されます。インスタンスの登録はこのルートで行われます
public void registerInstance(Service service, Instance instance, String clientId) {
Service singleton = ServiceManager.getInstance().getSingleton(service);
if (!singleton.isEphemeral()) {
throw new NacosRuntimeException(NacosException.INVALID_PARAM,
String.format("Current service %s is persistent service, can't register ephemeral instance.",
singleton.getGroupedServiceName()));
}
Client client = clientManager.getClient(clientId);
if (!clientIsLegal(client, clientId)) {
return;
}
// 获取实例信息
InstancePublishInfo instanceInfo = getPublishInfo(instance);
// 将instance添加到client中
client.addServiceInstance(singleton, instanceInfo);
client.setLastUpdatedTime();
// 建立service与clientId关系
NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
NotifyCenter
.publishEvent(new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), false));
}
さて、ここでは naocs のソースコードを解析することにしますが、機会があれば、クライアントとサーバー間のヘルス検出、サービスディスカバリ、パブリッシングおよびサブスクリプションイベントに関するソースコードを解析する予定です。