在分布式服务中,服务注册中心主要承担的是登记服务“我是谁,我从哪里来”的重要职责。Spring Cloud架构中,org.springframe.netflix.eureka是大家最为熟知的一种实现方式。。
org.springframe.netflix.eureka是基于com.netflix.eureka拓展的,可视为Spring Boot方式的实现。Eureka服务构建如下:
- 新建一个Maven项目,命名eureka,步骤略;
- 在项目pom.xml添加如下配置:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
- 在项目主函数类添加@EnableEurekaServer注解
@SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } }
- 一般为保证服务安全,我们会要求访问服务时进行身份校验,可以添加security来鉴权。
在pom.xml添加security依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
在bootstrap.yml添加security和eureka配置。
spring: application: name: eureka security: basic: enabled: true user: name: admin password: 666666
eureka: instance: hostname: localhost lease-renewal-interval-in-seconds: 10 prefer-ip-address: false client: registerWithEureka: false fetchRegistry: false serviceUrl: defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@${eureka.instance.hostname}:${server.port}/eureka/ server: eviction-interval-timer-in-ms: 5000 renewal-percent-threshold: 0.49
单实例Eureka到此就已经完成了。在开发环境或测试环境中,单实例已经满足性能需求,但实际生产环境一般采用主从实例,防止意外宕机造成的影响,主从实例需将registerWithEureka和fetchRegistry置为true,并修改defaultZone配置两个eureka的访问路径,即自己和另一个eureka的地址(这里用端口号作为区分,端口8761和端口8762,也可以通过域名作为区分,形式不限),两个路径之间通过英文逗号进行分隔。
eureka: instance: hostname: localhost lease-renewal-interval-in-seconds: 10 prefer-ip-address: false client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://admin:666666 @localhost:8761/eureka/, http://admin:666666 @localhost:8762/eureka/ server: eviction-interval-timer-in-ms: 5000 renewal-percent-threshold: 0.49
其他服务注册到eureka的实例如下:
- 创建一个Maven项目,步骤略;
- 在pom.xml添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
- 在项目bootstrap.yml添加eureka配置
eureka: instance: lease-renewal-interval-in-seconds: 10 # 表示eureka间隔多久去拉取服务注册信息,默认为30秒,对于gateway,如果要迅速获取服务注册状态,可以缩小该值,比如1秒 lease-expiration-duration-in-seconds: 20 # 表示eureka至上一次收到心跳之后,等待下一次心跳的超时时间,在这个时间内没收到下一次心跳,则将移除该instance client: security: basic: user: admin# eureka服务账号 password: 666666 # eureka服务密码 serviceUrl: defaultZone: http://${eureka.client.security.basic.user}:${eureka.client.security.basic.password}@localhost:8761/eureka/
- 在项目主函数类添加@SpringCloudApplication或SpringBootApplication、@EnableEurekaClient
@SpringCloudApplication public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }
/** 小白解读时间,若有不妥之处,望请不吝赐教 **/
通过IDEA我们可以查看到org.springframe.netflix.eureka的源码,其项目结构如下:
其中,我们看到在eureka服务添加的注解@EnableEurekaServer的类中有@Import(EurekaServerMarkerConfiguration.class):
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(EurekaServerMarkerConfiguration.class) public @interface EnableEurekaServer { }
而在对应的EurekaServerMarkerConfiguration的类中则是实例一个Marker的Bean:
@Configuration public class EurekaServerMarkerConfiguration { @Bean public Marker eurekaServerMarkerBean() { return new Marker(); } class Marker { } }
接着,我在EurekaServerAutoConfiguration类中找到@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)中有Marker.class,查询手册得知@ConditionalOnBean的含义是当给定的Bean存在时实例化当前Bean,其次,在EurekaServerAutoConfiguration类中@Import(EurekaServerInitializerConfiguration.class),其他的则是向Spring IoC容器注入相关的Bean,实例化com.netflix.eureka和可视化监控等。
@Configuration @Import(EurekaServerInitializerConfiguration.class) @ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class) @EnableConfigurationProperties({ EurekaDashboardProperties.class, InstanceRegistryProperties.class }) @PropertySource("classpath:/eureka/server.properties") public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter { /** * List of packages containing Jersey resources required by the Eureka server */ private static final String[] EUREKA_PACKAGES = new String[] { "com.netflix.discovery", "com.netflix.eureka" }; @Autowired private ApplicationInfoManager applicationInfoManager; @Autowired private EurekaServerConfig eurekaServerConfig; @Autowired private EurekaClientConfig eurekaClientConfig; @Autowired private EurekaClient eurekaClient; @Autowired private InstanceRegistryProperties instanceRegistryProperties; public static final CloudJacksonJson JACKSON_JSON = new CloudJacksonJson(); @Bean public HasFeatures eurekaServerFeature() { return HasFeatures.namedFeature("Eureka Server", EurekaServerAutoConfiguration.class); } @Configuration protected static class EurekaServerConfigBeanConfiguration { @Bean @ConditionalOnMissingBean public EurekaServerConfig eurekaServerConfig(EurekaClientConfig clientConfig) { EurekaServerConfigBean server = new EurekaServerConfigBean(); if (clientConfig.shouldRegisterWithEureka()) { // Set a sensible default if we are supposed to replicate server.setRegistrySyncRetries(5); } return server; } } @Bean @ConditionalOnProperty(prefix = "eureka.dashboard", name = "enabled", matchIfMissing = true) public EurekaController eurekaController() { return new EurekaController(this.applicationInfoManager); } static { CodecWrappers.registerWrapper(JACKSON_JSON); EurekaJacksonCodec.setInstance(JACKSON_JSON.getCodec()); } @Bean public ServerCodecs serverCodecs() { return new CloudServerCodecs(this.eurekaServerConfig); } private static CodecWrapper getFullJson(EurekaServerConfig serverConfig) { CodecWrapper codec = CodecWrappers.getCodec(serverConfig.getJsonCodecName()); return codec == null ? CodecWrappers.getCodec(JACKSON_JSON.codecName()) : codec; } private static CodecWrapper getFullXml(EurekaServerConfig serverConfig) { CodecWrapper codec = CodecWrappers.getCodec(serverConfig.getXmlCodecName()); return codec == null ? CodecWrappers.getCodec(CodecWrappers.XStreamXml.class) : codec; } class CloudServerCodecs extends DefaultServerCodecs { public CloudServerCodecs(EurekaServerConfig serverConfig) { super(getFullJson(serverConfig), CodecWrappers.getCodec(CodecWrappers.JacksonJsonMini.class), getFullXml(serverConfig), CodecWrappers.getCodec(CodecWrappers.JacksonXmlMini.class)); } } @Bean public PeerAwareInstanceRegistry peerAwareInstanceRegistry( ServerCodecs serverCodecs) { this.eurekaClient.getApplications(); // force initialization return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig, serverCodecs, this.eurekaClient, this.instanceRegistryProperties.getExpectedNumberOfClientsSendingRenews(), this.instanceRegistryProperties.getDefaultOpenForTrafficCount()); } @Bean @ConditionalOnMissingBean public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry, ServerCodecs serverCodecs) { return new RefreshablePeerEurekaNodes(registry, this.eurekaServerConfig, this.eurekaClientConfig, serverCodecs, this.applicationInfoManager); } /** * {@link PeerEurekaNodes} which updates peers when /refresh is invoked. * Peers are updated only if * <code>eureka.client.use-dns-for-fetching-service-urls</code> is * <code>false</code> and one of following properties have changed. * </p> * <ul> * <li><code>eureka.client.availability-zones</code></li> * <li><code>eureka.client.region</code></li> * <li><code>eureka.client.service-url.<zone></code></li> * </ul> */ static class RefreshablePeerEurekaNodes extends PeerEurekaNodes implements ApplicationListener<EnvironmentChangeEvent> { public RefreshablePeerEurekaNodes( final PeerAwareInstanceRegistry registry, final EurekaServerConfig serverConfig, final EurekaClientConfig clientConfig, final ServerCodecs serverCodecs, final ApplicationInfoManager applicationInfoManager) { super(registry, serverConfig, clientConfig, serverCodecs, applicationInfoManager); } @Override public void onApplicationEvent(final EnvironmentChangeEvent event) { if (shouldUpdate(event.getKeys())) { updatePeerEurekaNodes(resolvePeerUrls()); } } /* * Check whether specific properties have changed. */ protected boolean shouldUpdate(final Set<String> changedKeys) { assert changedKeys != null; // if eureka.client.use-dns-for-fetching-service-urls is true, then // service-url will not be fetched from environment. if (clientConfig.shouldUseDnsForFetchingServiceUrls()) { return false; } if (changedKeys.contains("eureka.client.region")) { return true; } for (final String key : changedKeys) { // property keys are not expected to be null. if (key.startsWith("eureka.client.service-url.") || key.startsWith("eureka.client.availability-zones.")) { return true; } } return false; } } @Bean public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs, PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) { return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs, registry, peerEurekaNodes, this.applicationInfoManager); } @Bean public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry, EurekaServerContext serverContext) { return new EurekaServerBootstrap(this.applicationInfoManager, this.eurekaClientConfig, this.eurekaServerConfig, registry, serverContext); } /** * Register the Jersey filter */ @Bean public FilterRegistrationBean jerseyFilterRegistration( javax.ws.rs.core.Application eurekaJerseyApp) { FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new ServletContainer(eurekaJerseyApp)); bean.setOrder(Ordered.LOWEST_PRECEDENCE); bean.setUrlPatterns( Collections.singletonList(EurekaConstants.DEFAULT_PREFIX + "/*")); return bean; } /** * Construct a Jersey {@link javax.ws.rs.core.Application} with all the resources * required by the Eureka server. */ @Bean public javax.ws.rs.core.Application jerseyApplication(Environment environment, ResourceLoader resourceLoader) { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider( false, environment); // Filter to include only classes that have a particular annotation. // provider.addIncludeFilter(new AnnotationTypeFilter(Path.class)); provider.addIncludeFilter(new AnnotationTypeFilter(Provider.class)); // Find classes in Eureka packages (or subpackages) // Set<Class<?>> classes = new HashSet<>(); for (String basePackage : EUREKA_PACKAGES) { Set<BeanDefinition> beans = provider.findCandidateComponents(basePackage); for (BeanDefinition bd : beans) { Class<?> cls = ClassUtils.resolveClassName(bd.getBeanClassName(), resourceLoader.getClassLoader()); classes.add(cls); } } // Construct the Jersey ResourceConfig // Map<String, Object> propsAndFeatures = new HashMap<>(); propsAndFeatures.put( // Skip static content used by the webapp ServletContainer.PROPERTY_WEB_PAGE_CONTENT_REGEX, EurekaConstants.DEFAULT_PREFIX + "/(fonts|images|css|js)/.*"); DefaultResourceConfig rc = new DefaultResourceConfig(classes); rc.setPropertiesAndFeatures(propsAndFeatures); return rc; } @Bean public FilterRegistrationBean traceFilterRegistration( @Qualifier("httpTraceFilter") Filter filter) { FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(filter); bean.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return bean; } }
EurekaServerInitializerConfiguration类实现了ServletContextAware, SmartLifecycle, Ordered三个接口,在start()方法中调用了eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);其实现在EurekaServerBootstrap类。
@Configuration public class EurekaServerInitializerConfiguration implements ServletContextAware, SmartLifecycle, Ordered { private static final Log log = LogFactory.getLog(EurekaServerInitializerConfiguration.class); @Autowired private EurekaServerConfig eurekaServerConfig; private ServletContext servletContext; @Autowired private ApplicationContext applicationContext; @Autowired private EurekaServerBootstrap eurekaServerBootstrap; private boolean running; private int order = 1; @Override public void setServletContext(ServletContext servletContext) { this.servletContext = servletContext; } @Override public void start() { new Thread(new Runnable() { @Override public void run() { try { //TODO: is this class even needed now? eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext); log.info("Started Eureka Server"); publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig())); EurekaServerInitializerConfiguration.this.running = true; publish(new EurekaServerStartedEvent(getEurekaServerConfig())); } catch (Exception ex) { // Help! log.error("Could not initialize Eureka servlet context", ex); } } }).start(); } private EurekaServerConfig getEurekaServerConfig() { return this.eurekaServerConfig; } private void publish(ApplicationEvent event) { this.applicationContext.publishEvent(event); } @Override public void stop() { this.running = false; eurekaServerBootstrap.contextDestroyed(this.servletContext); } @Override public boolean isRunning() { return this.running; } @Override public int getPhase() { return 0; } @Override public boolean isAutoStartup() { return true; } @Override public void stop(Runnable callback) { callback.run(); } @Override public int getOrder() { return this.order; } }
在EurekaServerBootstrap类中找到contextInitialized方法,主要是初始化环境配置和上下文。
public void contextInitialized(ServletContext context) { try { initEurekaEnvironment(); initEurekaServerContext(); context.setAttribute(EurekaServerContext.class.getName(), this.serverContext); } catch (Throwable e) { log.error("Cannot bootstrap eureka server :", e); throw new RuntimeException("Cannot bootstrap eureka server :", e); } }
其中,初始化上下文的实现主要是向后兼容、拷贝注册邻近节点信息、注册所有监控统计信息。
protected void initEurekaServerContext() throws Exception { // For backward compatibility JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH); XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH); if (isAws(this.applicationInfoManager.getInfo())) { this.awsBinder = new AwsBinderDelegate(this.eurekaServerConfig, this.eurekaClientConfig, this.registry, this.applicationInfoManager); this.awsBinder.start(); } EurekaServerContextHolder.initialize(this.serverContext); log.info("Initialized server context"); // Copy registry from neighboring eureka node int registryCount = this.registry.syncUp(); this.registry.openForTraffic(this.applicationInfoManager, registryCount); // Register all monitoring statistics. EurekaMonitors.registerAllStats(); }
在EurekaServerInitializerConfiguration类start()后还发布了可用事件EurekaRegistryAvailableEvent、服务启动事件EurekaServerStartedEvent,结合InstanceRegistry类(在EurekaServerAutoConfiguration类被实例化
,继承PeerAwareInstanceRegistryImpl,而PeerAwareInstanceRegistryImpl又是继承AbstractInstanceRegistry
)重写的注册事件EurekaInstanceRegisteredEvent、下线事件EurekaInstanceCanceledEvent、续约事件EurekaInstanceRenewedEvent,我们可以通过监听这些事件自定义服务的健康监控;
EurekaController则是使用freemarker模板引擎实现了可视化管理页面。