Principes Fondamentaux du Registre de Services
Le centre d'enregistrement est la pierre angulaire des architectures microservices. Son rôle principal consiste à gérer le cycle de vie des instances de service : les fournisseurs s'enregistrent au démarrage et se désenregistrent à l'arrêt, tandis que les consommateurs interrogent le registre pour obtenir les adresses des instances disponibles. De plus, le registre doit intégrer un mécanisme de vérification de l'état de santé (health check) pour s'assurer que les instances répertoriées sont effectivement capables de traiter les requêtes.
Cycle de Vie de l'Enregistrement des Services
Déclenchement de l'Enregistrement
Dans l'écosystème Spring Cloud, l'interface ServiceRegistry définit le contrat standard pour l'enregistrement des services. Nacos implémente ce contrat via la classe NacosServiceRegistry. L'intégration s'appuie sur le mécanisme d'auto-configuration de Spring, notamment via la classe AutoServiceRegistrationAutoConfiguration.
@Configuration(proxyBeanMethods = false)
@Import(AutoServiceRegistrationConfiguration.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
public class AutoServiceRegistrationAutoConfiguration {
@Autowired(required = false)
private AutoServiceRegistration registrationHandler;
@Autowired
private AutoServiceRegistrationConfiguration registryProperties;
@PostConstruct
protected void validateRegistration() {
if (this.registrationHandler == null && this.registryProperties.isFailFast()) {
throw new IllegalStateException(
"L'enregistrement automatique est requis, mais aucun bean AutoServiceRegistration n'a été trouvé.");
}
}
}
L'implémentation spécifique à Nacos, NacosAutoServiceRegistration, hérite de AbstractAutoServiceRegistration. Cette classe abstraite implémente l'interface ApplicationListener pour écouter l'événement WebServerInitializedEvent, qui est publié lorsque le serveur web embarqué (comme Tomcat ou Undertow) est entièrement initialisé et prêt à accepter des connexions.
public abstract class AbstractAutoServiceRegistration<R extends Registration>
implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener<WebServerInitializedEvent> {
private final AtomicInteger serverPort = new AtomicInteger(0);
@Override
public void onApplicationEvent(WebServerInitializedEvent event) {
processServerInitialization(event);
}
private void processServerInitialization(WebServerInitializedEvent event) {
ApplicationContext appContext = event.getApplicationContext();
if (appContext instanceof ConfigurableWebServerApplicationContext) {
String namespace = ((ConfigurableWebServerApplicationContext) appContext).getServerNamespace();
if ("management".equals(namespace)) {
return;
}
}
this.serverPort.compareAndSet(0, event.getWebServer().getPort());
this.start();
}
}
Exécution de l'Enregistrement
La méthode start() orchestre le processus d'enregistrement. Elle publie des événements Spring avant et après l'enregistrement, et délègue l'opération réelle à la méthode register().
public void start() {
if (!isEnabled() || this.running.get()) {
return;
}
this.context.publishEvent(new InstancePreRegisteredEvent(this, getRegistration()));
register();
if (shouldRegisterManagement()) {
registerManagement();
}
this.context.publishEvent(new InstanceRegisteredEvent<>(this, getConfiguration()));
this.running.compareAndSet(false, true);
}
protected void register() {
this.serviceRegistry.register(getRegistration());
}
La classe NacosServiceRegistry prend le relais pour convertir les métadonnées Spring en un objet Instance Nacos, puis invoque le client Nacos pour effectuer l'enregistrement réseau.
public void register(Registration registration) {
String serviceIdentifier = registration.getServiceId();
if (StringUtils.isEmpty(serviceIdentifier)) {
log.warn("Aucun identifiant de service fourni pour l'enregistrement Nacos.");
return;
}
NamingService namingClient = namingService();
String targetGroup = nacosDiscoveryProperties.getGroup();
Instance nacosInstance = convertToNacosInstance(registration);
try {
namingClient.registerInstance(serviceIdentifier, targetGroup, nacosInstance);
log.info("Enregistrement Nacos réussi pour {} {} {}:{}", targetGroup, serviceIdentifier,
nacosInstance.getIp(), nacosInstance.getPort());
} catch (Exception e) {
handleRegistrationException(serviceIdentifier, registration, e);
}
}
Mécanisme de Battement de Cœur (Heartbeat)
Pour les instances éphémères, Nacos utilise un mécanisme de battement de cœur pour maintenir l'état de santé. Lors de l'enregistrement, le client construit un objet BeatInfo et le soumet au BeatReactor.
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
NamingUtils.checkInstanceIsLegal(instance);
String fullServiceName = NamingUtils.getGroupedName(serviceName, groupName);
if (instance.isEphemeral()) {
BeatInfo heartbeatData = beatReactor.buildBeatInfo(fullServiceName, instance);
beatReactor.addBeatInfo(fullServiceName, heartbeatData);
}
serverProxy.registerService(fullServiceName, groupName, instance);
}
Planification et Envoi des Signaux de Présence
Le BeatReactor stocke les informations de battement de cœur dans une map concurrente et planifie une tâche asynchrone pour envoyer périodiquement ces données au serveur.
public void addBeatInfo(String serviceName, BeatInfo heartbeatData) {
String uniqueKey = buildKey(serviceName, heartbeatData.getIp(), heartbeatData.getPort());
BeatInfo existingBeat = dom2Beat.remove(uniqueKey);
if (existingBeat != null) {
existingBeat.setStopped(true);
}
dom2Beat.put(uniqueKey, heartbeatData);
taskScheduler.schedule(new HeartbeatExecutionTask(heartbeatData), heartbeatData.getPeriod(), TimeUnit.MILLISECONDS);
}
La tâche HeartbeatExecutionTask gère la communication avec le serveur. Si le serveur répond que l'instance n'est pas trouvée (code RESOURCE_NOT_FOUND), le client déclenche automatiquement une procédure de ré-enregistrement.
class HeartbeatExecutionTask implements Runnable {
private final BeatInfo heartbeatData;
public HeartbeatExecutionTask(BeatInfo heartbeatData) {
this.heartbeatData = heartbeatData;
}
@Override
public void run() {
if (heartbeatData.isStopped()) return;
long nextExecutionDelay = heartbeatData.getPeriod();
try {
JsonNode serverResponse = serverProxy.sendBeat(heartbeatData, BeatReactor.this.lightBeatEnabled);
long serverInterval = serverResponse.get("clientBeatInterval").asLong();
if (serverResponse.has(CommonParams.LIGHT_BEAT_ENABLED)) {
BeatReactor.this.lightBeatEnabled = serverResponse.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
}
if (serverInterval > 0) {
nextExecutionDelay = serverInterval;
}
int responseCode = serverResponse.has(CommonParams.CODE) ? serverResponse.get(CommonParams.CODE).asInt() : NamingResponseCode.OK;
if (responseCode == NamingResponseCode.RESOURCE_NOT_FOUND) {
registerMissingInstance();
}
} catch (Exception ex) {
NAMING_LOGGER.error("[CLIENT-BEAT] Échec de l'envoi du battement de cœur: {}", ex.getMessage());
} finally {
taskScheduler.schedule(new HeartbeatExecutionTask(heartbeatData), nextExecutionDelay, TimeUnit.MILLISECONDS);
}
}
private void registerMissingInstance() {
Instance fallbackInstance = new Instance();
fallbackInstance.setPort(heartbeatData.getPort());
fallbackInstance.setIp(heartbeatData.getIp());
fallbackInstance.setWeight(heartbeatData.getWeight());
fallbackInstance.setMetadata(heartbeatData.getMetadata());
fallbackInstance.setClusterName(heartbeatData.getCluster());
fallbackInstance.setServiceName(heartbeatData.getServiceName());
fallbackInstance.setEphemeral(true);
try {
serverProxy.registerService(heartbeatData.getServiceName(),
NamingUtils.getGroupName(heartbeatData.getServiceName()), fallbackInstance);
} catch (Exception ignored) {}
}
}
Côté serveur, si aucune pulsation n'est reçue pendant une période définie (généralement 15 secondes), l'instance est marquée comme non saine (healthy = false). Si l'absence dépasse 30 secondes, l'instance est définitivement supprimée du registre.
Découverte Dynamique et Mise à Jour des Adresses
Interrogation des Instances
Le serveur Nacos expose des points de terminaison pour récupérer la liste des instances. L'API REST /v1/ns/instance/list permet aux clients d'interroger les fournisseurs disponibles.
@GetMapping("/list")
@Secured(action = ActionTypes.READ)
public Object list(HttpServletRequest request) throws Exception {
String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
String userAgent = WebUtils.getUserAgent(request);
String clusters = WebUtils.optional(request, "clusters", StringUtils.EMPTY);
String clientIP = WebUtils.optional(request, "clientIP", StringUtils.EMPTY);
int udpPort = Integer.parseInt(WebUtils.optional(request, "udpPort", "0"));
boolean healthyOnly = Boolean.parseBoolean(WebUtils.optional(request, "healthyOnly", "false"));
Subscriber subscriber = new Subscriber(clientIP + ":" + udpPort, userAgent, "app", clientIP, namespaceId, serviceName, udpPort, clusters);
return getInstanceOperator().listInstance(namespaceId, serviceName, subscriber, clusters, healthyOnly);
}
Côté client, le SDK Nacos filtre les instances pour ne retourner que celles qui sont saines, activées et ayant un poids supérieur à zéro.
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy, boolean subscribe) throws NacosException {
String clusterString = String.join(",", clusters);
ServiceInfo serviceInfo;
if (subscribe) {
serviceInfo = serviceInfoHolder.getServiceInfo(serviceName, groupName, clusterString);
if (serviceInfo == null) {
serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);
}
} else {
serviceInfo = clientProxy.queryInstancesOfService(serviceName, groupName, clusterString, 0, false);
}
return filterInstances(serviceInfo, healthy);
}
private List<Instance> filterInstances(ServiceInfo serviceInfo, boolean healthy) {
if (serviceInfo == null || CollectionUtils.isEmpty(serviceInfo.getHosts())) {
return Collections.emptyList();
}
return serviceInfo.getHosts().stream()
.filter(instance -> instance.isHealthy() == healthy && instance.isEnabled() && instance.getWeight() > 0)
.collect(Collectors.toList());
}
Abonnement et Rafraîchissement Automatique
Spring Cloud Alibaba active par défaut la surveillance dynamique des services via la classe NacosWatch. Cette classe s'intègre au cycle de vie de Spring et s'abonne aux événements de changement d'instances (NamingEvent).
public class NacosWatch implements ApplicationEventPublisherAware, SmartLifecycle, DisposableBean {
private final Map<String, EventListener> listenerRegistry = new ConcurrentHashMap<>(16);
@Override
public void start() {
if (this.running.compareAndSet(false, true)) {
EventListener eventListener = listenerRegistry.computeIfAbsent(buildKey(), event -> new EventListener() {
@Override
public void onEvent(Event event) {
if (event instanceof NamingEvent) {
List<Instance> instances = ((NamingEvent) event).getInstances();
Optional<Instance> instanceOptional = selectCurrentInstance(instances);
instanceOptional.ifPresent(currentInstance -> resetIfNeeded(currentInstance));
}
}
});
NamingService namingService = nacosServiceManager.getNamingService(properties.getNacosProperties());
try {
namingService.subscribe(properties.getService(), properties.getGroup(),
Arrays.asList(properties.getClusterName()), eventListener);
} catch (Exception e) {
log.error("Échec de l'abonnement au service de nommage", e);
}
}
}
}
Lors de l'abonnement, le HostReactor initialise une tâche périodique (UpdateTask) qui interroge le serveur pour détecter les modifications. Si le serveur pousse les mises à jour via UDP, la tâche ajuste son cmoportement pour éviter d'écraser les données reçues.
public class UpdateTask implements Runnable {
@Override
public void run() {
long delayTime = DEFAULT_DELAY;
try {
ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
if (serviceObj == null || serviceObj.getLastRefTime() <= lastRefTime) {
updateService(serviceName, clusters);
serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
} else {
refreshOnly(serviceName, clusters);
}
lastRefTime = serviceObj.getLastRefTime();
if (CollectionUtils.isEmpty(serviceObj.getHosts())) {
incFailCount();
return;
}
delayTime = serviceObj.getCacheMillis();
resetFailCount();
} catch (Throwable e) {
incFailCount();
} finally {
long nextDelay = Math.min(delayTime << failCount, DEFAULT_DELAY * 60);
executor.schedule(this, nextDelay, TimeUnit.MILLISECONDS);
}
}
}
La méthode updateService effectue l'appel réseau réel et met à jour le cache local, tandis que refreshOnly se contente de notifier le serveur de la présence du client sans modifier le cache local, optimisant ainsi les performances lors des mises à jour poussées par le serveur.
public void updateService(String serviceName, String clusters) throws NacosException {
ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
try {
String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);
if (StringUtils.isNotEmpty(result)) {
processServiceJson(result);
}
} finally {
if (oldService != null) {
synchronized (oldService) {
oldService.notifyAll();
}
}
}
}
public void refreshOnly(String serviceName, String clusters) {
try {
serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);
} catch (Exception e) {
NAMING_LOGGER.error("[NA] Échec du rafraîchissement du service: " + serviceName, e);
}
}