Как избежать повторной регистрации на сервере Eureka, когда клиент Eureka обнаруживает изменения конфигурации с сервера конфигурации? - PullRequest
1 голос
/ 11 октября 2019

Я настроил среду Spring Cloud, включая Eureka Server, Config Server с использованием git в качестве источника данных, клиент Eureka с использованием Config Server и периодический опрос конфигурации. Я видел, как каждый раз, когда я меняю конфигурацию через git, клиент сначала отменяет регистрацию на сервере Eureka, а затем снова регистрируется. Как избежать процесса отмены регистрации-регистрации?

В клиенте я использовал @EnableScheduling для включения Spring Scheduling и создал класс ConfigGitClientWatch для опроса сервера Config, другой класс MyContextRefresher для применения опрошенных изменений конфигурации в Spring Environment.

ConfigGitClientWatch:

@Component
@Slf4j
public class ConfigGitClientWatch implements Closeable, EnvironmentAware {

    private final AtomicBoolean running = new AtomicBoolean(false);
    private final AtomicReference<String> version = new AtomicReference<>();
    private final MyContextRefresher refresher;
    private final ConfigServicePropertySourceLocator locator;

    private Environment environment;

    public ConfigGitClientWatch(
            MyContextRefresher refresher, ConfigServicePropertySourceLocator locator) {
        this.refresher = refresher;
        this.locator = locator;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @PostConstruct
    public void start() {
        running.compareAndSet(false, true);
    }

    @Scheduled(
            initialDelayString = "${spring.cloud.config.watch.git.initialDelay:180000}",
            fixedDelayString = "${spring.cloud.config.watch.git.delay:500}"
    )
    public void watchConfigServer() {
        if (running.get()) {
            String newVersion = fetchNewVersion();
            String oldVersion = version.get();

            if (versionChanged(oldVersion, newVersion)) {
                version.set(newVersion);
                final Set<String> refreshedProperties = refresher.refresh();
                if(!refreshedProperties.isEmpty()) {
                    log.info("Refreshed properties:{}", String.join(",", refreshedProperties));
                }
            }
        }
    }

    private String fetchNewVersion() {
        CompositePropertySource propertySource = (CompositePropertySource) locator.locate(environment);
        return (String) propertySource.getProperty("config.client.version");
    }

    private static boolean versionChanged(String oldVersion, String newVersion) {
        return !hasText(oldVersion) && hasText(newVersion)
                || hasText(oldVersion) && !oldVersion.equals(newVersion);
    }

    @Override
    public void close() {
        running.compareAndSet(true, false);
    }

}

MyContextRefresher:

@Component
@Slf4j
public class MyContextRefresher {

    private static final String REFRESH_ARGS_PROPERTY_SOURCE = "refreshArgs";

    private static final String[] DEFAULT_PROPERTY_SOURCES = new String[] {
            // order matters, if cli args aren't first, things get messy
            CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME,
            "defaultProperties" };

    private Set<String> standardSources = new HashSet<>(
            Arrays.asList(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
                    StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
                    StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME,
                    StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME,
                    StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME,
                    "configurationProperties"));

    private ConfigurableApplicationContext context;

    private RefreshScope scope;

    public MyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) {
        this.context = context;
        this.scope = scope;
    }

    protected ConfigurableApplicationContext getContext() {
        return this.context;
    }

    protected RefreshScope getScope() {
        return this.scope;
    }

    public synchronized Set<String> refresh() {
        Set<String> keys = refreshEnvironment();
        if(!keys.isEmpty()) {
            this.scope.refreshAll();
        }
        return keys;
    }

    private final List<String> skippedKeys = Arrays.asList(
            "config.client.version",
            "spring.cloud.client.hostname",
            "local.server.port"
    );

    public synchronized Set<String> refreshEnvironment() {
        return addConfigFilesToEnvironment();
    }

    private Set<String> addConfigFilesToEnvironment() {
        Map<String, Object> before = extract(
                this.context.getEnvironment().getPropertySources());
        ConfigurableApplicationContext capture = null;
        Set<String> changedKeys = new HashSet<>();
        try {
            StandardEnvironment environment = copyEnvironment(
                    this.context.getEnvironment());

            SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
                    .bannerMode(Mode.OFF).web(WebApplicationType.NONE)
                    .environment(environment);
            // Just the listeners that affect the environment (e.g. excluding logging
            // listener because it has side effects)
            builder.application()
                    .setListeners(Arrays.asList(new BootstrapApplicationListener(),
                            new ConfigFileApplicationListener()));
            capture = builder.run();
            if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
                environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
            }
            MutablePropertySources target = this.context.getEnvironment()
                    .getPropertySources();
            String targetName = null;
            for (PropertySource<?> source : environment.getPropertySources()) {
                String name = source.getName();
                if (target.contains(name)) {
                    targetName = name;
                }
                if (!this.standardSources.contains(name)) {
                    if (target.contains(name)) {
                        target.replace(name, source);
                    }
                    else {
                        if (targetName != null) {
                            target.addAfter(targetName, source);
                        }
                        else {
                            // targetName was null so we are at the start of the list
                            target.addFirst(source);
                            targetName = name;
                        }
                    }
                }
            }
            final Map<String, Object> after = extract(environment.getPropertySources());
            changedKeys = changes(before, after).keySet();
            changedKeys.removeAll(skippedKeys);
        }
        finally {
            if(!changedKeys.isEmpty()) {
                ConfigurableApplicationContext closeable = capture;
                while (closeable != null) {
                    try {
                        closeable.close();
                    } catch (Exception e) {
                        // Ignore;
                    }
                    if (closeable.getParent() instanceof ConfigurableApplicationContext) {
                        closeable = (ConfigurableApplicationContext) closeable.getParent();
                    } else {
                        break;
                    }
                }
                this.context.publishEvent(new EnvironmentChangeEvent(this.context, changedKeys));
            }
        }
        return changedKeys;
    }

    // Don't use ConfigurableEnvironment.merge() in case there are clashes with property
    // source names
    private StandardEnvironment copyEnvironment(ConfigurableEnvironment input) {
        StandardEnvironment environment = new StandardEnvironment();
        MutablePropertySources capturedPropertySources = environment.getPropertySources();
        // Only copy the default property source(s) and the profiles over from the main
        // environment (everything else should be pristine, just like it was on startup).
        for (String name : DEFAULT_PROPERTY_SOURCES) {
            if (input.getPropertySources().contains(name)) {
                if (capturedPropertySources.contains(name)) {
                    capturedPropertySources.replace(name,
                            input.getPropertySources().get(name));
                }
                else {
                    capturedPropertySources.addLast(input.getPropertySources().get(name));
                }
            }
        }
        environment.setActiveProfiles(input.getActiveProfiles());
        environment.setDefaultProfiles(input.getDefaultProfiles());
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("spring.jmx.enabled", false);
        map.put("spring.main.sources", "");
        capturedPropertySources
                .addFirst(new MapPropertySource(REFRESH_ARGS_PROPERTY_SOURCE, map));
        return environment;
    }

    private Map<String, Object> changes(Map<String, Object> before,
                                        Map<String, Object> after) {
        Map<String, Object> result = new HashMap<String, Object>();
        for (String key : before.keySet()) {
            if (!after.containsKey(key)) {
                result.put(key, null);
            }
            else if (!equal(before.get(key), after.get(key))) {
                result.put(key, after.get(key));
            }
        }
        for (String key : after.keySet()) {
            if (!before.containsKey(key)) {
                result.put(key, after.get(key));
            }
        }
        return result;
    }

    private boolean equal(Object one, Object two) {
        if (one == null && two == null) {
            return true;
        }
        if (one == null || two == null) {
            return false;
        }
        return one.equals(two);
    }

    private Map<String, Object> extract(MutablePropertySources propertySources) {
        Map<String, Object> result = new HashMap<String, Object>();
        List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>();
        for (PropertySource<?> source : propertySources) {
            sources.add(0, source);
        }
        for (PropertySource<?> source : sources) {
            if (!this.standardSources.contains(source.getName())) {
                extract(source, result);
            }
        }
        return result;
    }

    private void extract(PropertySource<?> parent, Map<String, Object> result) {
        if (parent instanceof CompositePropertySource) {
            try {
                List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>();
                for (PropertySource<?> source : ((CompositePropertySource) parent)
                        .getPropertySources()) {
                    sources.add(0, source);
                }
                for (PropertySource<?> source : sources) {
                    extract(source, result);
                }
            }
            catch (Exception e) {
                return;
            }
        }
        else if (parent instanceof EnumerablePropertySource) {
            for (String key : ((EnumerablePropertySource<?>) parent).getPropertyNames()) {
                result.put(key, parent.getProperty(key));
            }
        }
    }

    @Configuration
    protected static class Empty {

    }

}

Журнал клиента следующий:

c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://172.39.8.118:14102/
c.c.c.ConfigServicePropertySourceLocator : Located environment: name=user-service, profiles=[peer2], label=null, version=3961593acd49e60c194aebc224adc6a4dfa9f530, state=null
trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$39b14aa7] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
o.s.c.n.eureka.InstanceInfoFactory       : Setting initial instance status as: STARTING
com.netflix.discovery.DiscoveryClient    : Initializing Eureka in region us-east-1
c.n.d.provider.DiscoveryJerseyProvider   : Using JSON encoding codec LegacyJacksonJson
c.n.d.provider.DiscoveryJerseyProvider   : Using JSON decoding codec LegacyJacksonJson
c.n.d.provider.DiscoveryJerseyProvider   : Using XML encoding codec XStreamXml
c.n.d.provider.DiscoveryJerseyProvider   : Using XML decoding codec XStreamXml
c.n.d.s.r.aws.ConfigClusterResolver      : Resolving eureka endpoints via configuration
com.netflix.discovery.DiscoveryClient    : Disable delta property : false
com.netflix.discovery.DiscoveryClient    : Single vip registry refresh property : null
com.netflix.discovery.DiscoveryClient    : Force full registry fetch : false
com.netflix.discovery.DiscoveryClient    : Application is null : false
com.netflix.discovery.DiscoveryClient    : Registered Applications size is zero : true
com.netflix.discovery.DiscoveryClient    : Application version is -1: true
com.netflix.discovery.DiscoveryClient    : Getting all instance registry info from the eureka server
com.netflix.discovery.DiscoveryClient    : The response status is 200
com.netflix.discovery.DiscoveryClient    : Not registering with Eureka server per configuration
com.netflix.discovery.DiscoveryClient    : Discovery Client initialized at timestamp 1570780512926 with initial instances count: 6
o.s.c.n.e.s.EurekaServiceRegistry        : Registering application USER-SERVICE with eureka with status UP
c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://172.39.8.118:14102/
c.c.c.ConfigServicePropertySourceLocator : Located environment: name=user-service, profiles=[peer2], label=null, version=3961593acd49e60c194aebc224adc6a4dfa9f530, state=null
b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name='configService', propertySources=[MapPropertySource {name='configClient'}, MapPropertySource {name='http://localhost:3000/longqinsi/demo-config.git/user-service.yml'}, MapPropertySource {name='http://localhost:3000/longqinsi/demo-config.git/application.yml'}]}
o.s.boot.SpringApplication               : The following profiles are active: peer2
com.example.userservice.HelloController  : Received heartbeat from user service at port 14105.
o.s.boot.SpringApplication               : Started application in 0.299 seconds (JVM running for 10313.932)
o.s.c.n.e.s.EurekaServiceRegistry        : Unregistering application USER-SERVICE with eureka with status DOWN
com.netflix.discovery.DiscoveryClient    : Shutting down DiscoveryClient ...
com.netflix.discovery.DiscoveryClient    : Completed shut down of DiscoveryClient
com.netflix.discovery.DiscoveryClient    : Shutting down DiscoveryClient ...
com.example.userservice.HelloController  : Received heartbeat from user service at port 14105.
com.netflix.discovery.DiscoveryClient    : Unregistering ...
com.netflix.discovery.DiscoveryClient    : DiscoveryClient_USER-SERVICE/eureka1:user-service:14104 - deregister  status: 200
com.netflix.discovery.DiscoveryClient    : Completed shut down of DiscoveryClient
o.s.c.n.eureka.InstanceInfoFactory       : Setting initial instance status as: STARTING
com.netflix.discovery.DiscoveryClient    : Initializing Eureka in region us-east-1
c.n.d.provider.DiscoveryJerseyProvider   : Using JSON encoding codec LegacyJacksonJson
c.n.d.provider.DiscoveryJerseyProvider   : Using JSON decoding codec LegacyJacksonJson
c.n.d.provider.DiscoveryJerseyProvider   : Using XML encoding codec XStreamXml
c.n.d.provider.DiscoveryJerseyProvider   : Using XML decoding codec XStreamXml
c.n.d.s.r.aws.ConfigClusterResolver      : Resolving eureka endpoints via configuration
com.netflix.discovery.DiscoveryClient    : Disable delta property : false
com.netflix.discovery.DiscoveryClient    : Single vip registry refresh property : null
com.netflix.discovery.DiscoveryClient    : Force full registry fetch : false
com.netflix.discovery.DiscoveryClient    : Application is null : false
com.netflix.discovery.DiscoveryClient    : Registered Applications size is zero : true
com.netflix.discovery.DiscoveryClient    : Application version is -1: true
com.netflix.discovery.DiscoveryClient    : Getting all instance registry info from the eureka server
com.netflix.discovery.DiscoveryClient    : The response status is 200
com.netflix.discovery.DiscoveryClient    : Starting heartbeat executor: renew interval is: 30
c.n.discovery.InstanceInfoReplicator     : InstanceInfoReplicator onDemand update allowed rate per min is 4
com.netflix.discovery.DiscoveryClient    : Discovery Client initialized at timestamp 1570780516317 with initial instances count: 6
o.s.c.n.e.s.EurekaServiceRegistry        : Unregistering application USER-SERVICE with eureka with status DOWN
com.netflix.discovery.DiscoveryClient    : Saw local status change event StatusChangeEvent [timestamp=1570780516320, current=DOWN, previous=STARTING]
com.netflix.discovery.DiscoveryClient    : DiscoveryClient_USER-SERVICE/eureka1:user-service:14104: registering service...
o.s.c.n.e.s.EurekaServiceRegistry        : Registering application USER-SERVICE with eureka with status UP
com.netflix.discovery.DiscoveryClient    : Saw local status change event StatusChangeEvent [timestamp=1570780516320, current=UP, previous=DOWN]
o.s.c.n.e.s.EurekaServiceRegistry        : Unregistering application USER-SERVICE with eureka with status DOWN
o.s.c.n.e.s.EurekaServiceRegistry        : Registering application USER-SERVICE with eureka with status UP
com.netflix.discovery.DiscoveryClient    : DiscoveryClient_USER-SERVICE/eureka1:user-service:14104 - registration status: 204
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...