【深入理解SpringCloud微服务】浅析微服务注册中心Eureka与nacos,手写实现一个微服务注册中心

【深入理解SpringCloud微服务】浅析微服务注册中心Eureka与nacos,手写实现一个微服务注册中心

注册中心

注册中心是微服务体系里面非常重要的一个核心组件,它最重要的作用就是实现服务注册与发现

在过去还没有微服务和注册中心的时候,一个服务存在对另一个服务的调用关系时,需要在自己服务的配置文件里面配置对方的ip端口,当发生调用时,需要读取配置文件里面对方的ip端口,组装请求url,发送请求。这种方式非常的不灵活,当被调用的服务集群用机器上下线时,调用方不能动态感知,需要手动修改配置然后重启服务,并且服务一旦多起来,维护这些配置也是一项繁琐的工作,很容易出错。

为了解决这个问题,于是就有了微服务注册中心。

注册中心是部署在分布式或者微服务环境下的一个中间件服务,提供服务的注册与发现功能。当使用了注册中心之后,调用方被称为服务消费者,被调用方被称为服务提供者。服务提供者启动时往注册中心注册,注册信息包括自己的服务名称、服务实例id、ip地址和端口等,注册中心把这些信息维护到内存注册表。服务消费者启动时(饿汉式加载)或者在发生调用时(懒加载)想注册中心发起服务发现请求,从注册中心拉取服务提供者注册上来的注册信息,缓存到本地的服务列表中。这样,服务消费者通过查询服务列表就能得知要调用的服务提供者的ip端口,无需在配置文件中进行配置,并且当服务提供者对应的服务集群有服务实例上下线时,服务消费者可以通过定时轮询注册中心或者注册中心主动通知的方式动态感知。

注册中心会在内存中维护一个服务注册表,用于存储服务提供者注册上来的信息。比如用一个双层Map,外层key是服务名,内层key是服务实例id(同一服务的不同实例组成集群,因此需要一个类似于id的唯一标识),value是ip端口。

内存中的注册信息有可能还会持久化或者存到外部的存储服务中,比如Mysql、redis、MongoDB、文件都可以。

注册中心为了避免单点故障,往往也是集群部署。因此,注册中心实例之间会有服务注册信息的同步。

当注册中心是集群式部署时,服务提供者启动时就通过某种方式选取到一台注册中心实例注册即可,注册中心会通过集群内同步把注册信息同步到其他注册中心实例。

由于服务有可能因为某些原因而出问题或者下线,服务注册中心需要通过某种方式对服务提供者进行健康检查,把不健康的服务实例从注册表中剔除。但是有的注册中心不会对服务提供者进行健康检查,而是给服务提供者注册上来的信息设置一个过期时间,服务提供者需要定期的进行服务续约,如果超过指定时间不续约,服务提供者的注册信息将会被注册中心从注册表中剔除。注册中心的注册表发生表动,会通知服务消费者,或者由服务消费者自己轮询感知注册表的变化。

手写实现一个注册中心

我们对注册中心已经有了一个认识,总结下来就是有一个服务注册服务端,维护了一个内存注册表,客户端请求服务端进行服务注册与发现,实际上就是读写内存注册表。然后注册中心服务端还要实现注册信息在集群内的同步、服务变更通知客户端、服务健康检查等功能。

那么,我们也可以实现一个自己的注册中心了。

服务端设计

我们还是参考Eureka和1.x版本的nacos,采用http服务端的实现方式。我们定义一个自己的Controller,名字就叫RegistryCenterController,是一个SpringMVC的Controller,接收客户端发来的http请求。

然后我们定义一个Service,名字叫RegistryCenterService,由它来处理内存注册表的读写,内存注册表就直接放在RegistryCenterService中。RegistryCenterController接收到请求之后,会调用registryCenterService进行请求处理。

在定义内存注册表的结构前,我们要定义一个用于存放注册信息的对象,我们定义一个MicroService对象用于封装服务提供者的注册信息,比如ip地址端口号等。

然后内存注册表的结构还是使用双层ConcurrentHashMap,外层key就是服务名serviceName(我们这里不考虑什么namespace和cluster之类的东西),内存key就是服务实例id,value就是MicroService,这样就是一个非常简单的双层ConcurrentHashMap结构的内存注册表,我们给它命名为registryTable。

客户端通过发送http请求来进行服务发现和服务注册,服务端通过RegistryCenterController接收http请求并调用RegistryCenterService读写内存注册表registryTable。

RegistryCenterController:

java 复制代码
/**
 * @author huangjunyi
 * @date 2023/11/30 10:23
 * @desc
 */
@RestController
@RequestMapping("/registry/center")
public class RegistryCenterController {

    @Autowired
    private RegistryCenterService registryCenterService;

    ...

}

RegistryCenterService :

java 复制代码
/**
 * @author huangjunyi
 * @date 2023/11/30 10:40
 * @desc
 */
@Service
public class RegistryCenterService {

	// 内存注册表,双层ConcurrentHashMap:[serviceName, [id, 服务实例信息]]
    private Map<String, Map<String, MicroService>> registryTable = new ConcurrentHashMap<>();

    ...
}

MicroService :

java 复制代码
/**
 * @author huangjunyi
 * @date 2023/11/30 10:41
 * @desc
 */
public class MicroService implements Serializable {

    private String serviceName;

    private String id;

    private String ip;

    private int port;

    private long lastTime;

	...
}    

至于服务同步,我们也是做异步处理。在RegistryCenterService 内部定义一个LinkedBlockingQueue类型的变量作为队列,把注册上来的信息放到这个队列里面,就把MicroService对象放进去。然后使用一个后台线程去轮询这个队列,把MicroService同步到集群中的其他注册中心实例。

RegistryCenterService :

java 复制代码
@Service
public class RegistryCenterService {

	// 内存注册表,双层ConcurrentHashMap:[serviceName, [id, 服务实例信息]]
    private Map<String, Map<String, MicroService>> registryTable = new ConcurrentHashMap<>();

	// 集群同步队列
    private LinkedBlockingQueue<MicroService> syncQueue = new LinkedBlockingQueue<>();

	...

    public boolean registry(MicroService microService) throws InterruptedException {
        ...
        // 注册上来的服务实例放入集群同步队列
        syncQueue.put(microService);
        return true;
    }

	...

    public void syncService() throws InterruptedException {
        ...
        // 循环取出队列中的服务实例
        while (syncQueue.peek() != null) {
            MicroService service = syncQueue.take();
            // 轮询每个集群节点
            for (String node : nodes) {
                ...
                // 通过http请求把实例信息同步过去
                Result result = restTemplate.postForObject(String.format("http://%s:%d/registry/center/sync", ip, port), service, Result.class);
                ...
            }
        }
    }
}

RegistryCenterController通过"/sync"接口接收到注册信息同步后,会调用registryCenterService.addService(microService)方法:

java 复制代码
    @PostMapping("/sync")
    public Result<?> sync(@RequestBody MicroService microService) {
        if (registryCenterService.addService(microService)) {
            return Result.ok(null);
        }
        return Result.error("sync failed");
    }

registryCenterService.addService(microService)方法把注册信息写入内存注册表:

java 复制代码
    public boolean addService(MicroService microService) {
        registryTable.putIfAbsent(microService.getServiceName(), new ConcurrentHashMap<>());
        registryTable.get(microService.getServiceName()).put(microService.getId(), microService);
        LOGGER.info("this service[{}] added", microService.getServiceName() + ":" + microService.getId());
        return true;
    }

SyncServiceTask :

java 复制代码
/**
 * @author huangjunyi
 * @date 2023/11/30 17:33
 * @desc
 */
@Component
public class SyncServiceTask {

    @Autowired
    private RegistryCenterService registryCenterService;

    @Scheduled(cron = "0/10 * * * * ?")
    public void syncService() throws InterruptedException {
    	// 定时任务调用registryCenterService的syncService方法进行集群同步
        registryCenterService.syncService();
    }

}

然后是健康检查,我们也是开一个后台线程,定时检查内存注册表中的服务实例信息里面的最近一次续约时间,超过30s没有续约,就把他踢掉,然后同步到集群中的其他注册中心。

java 复制代码
/**
 * @author huangjunyi
 * @date 2023/11/30 11:43
 * @desc
 */
@Component
public class HealthCheckTask {

    @Autowired
    private RegistryCenterService registryCenterService;

    @Scheduled(cron = "0/30 * * * * ?")
    public void healthCheck() {
    	// 定时任务调用registryCenterService的healthCheck方法进行监控检查
        registryCenterService.healthCheck();
    }

}

最后,我们接入SpringBoot提供的自动装配机制,完成我们注册中心的自动配置,spring.factories文件配置指定我的配置类RegistryCenterServerConfig,然后我们的配置类RegistryCenterServerConfig通过@ComponentScan注解扫描RegistryCenterController、RegistryCenterService、定时任务类等一些核心类,定时任务使用Spring的@EnableScheduling和@Scheduled注解。

spring.factories

java 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.huangjunyi1993.simple.microservice.registry.center.server.config.RegistryCenterServerConfig

RegistryCenterServerConfig

java 复制代码
@Configuration
@EnableScheduling
@ComponentScan(basePackages = {
        "com.huangjunyi1993.simple.microservice.registry.center.server.controller",
        "com.huangjunyi1993.simple.microservice.registry.center.server.service",
        "com.huangjunyi1993.simple.microservice.registry.center.server.task"})
@EnableConfigurationProperties({NodesProperties.class})
public class RegistryCenterServerConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

客户端设计

客户端方面,我们定义一个RegistryCenterClient用于通过OkHttp请求服务端进行服务注册和发现,RegistryCenterClient实现ApplicationListener<ContextRefreshedEvent>接口,监听ContextRefreshedEvent事件触发服务注册和服务发现。然后通过Spring的@EnableScheduling和@Scheduled注解开启定时任务使用OkHttp定时发送心跳。

java 复制代码
/**
 * @author huangjunyi
 * @date 2023/11/30 20:21
 * @desc
 */
public class SimpleRegistryCenterClient implements RegistryCenterClient, EnvironmentAware, ApplicationListener<ContextRefreshedEvent> {

	...

	@Override
    public boolean sendHeartbeatToServer() {
        // 发送心跳,就是使用OkHttp请求注册中心服务端...
    }

	// 服务注册和服务发现
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        // 解析注册中心集群各实例的ip地址和端口
        String registryCenterServers = registryCenterConfig.getServers();
        String[] registryCenterServerArr = registryCenterServers.split(",");
        registryCenterServerList = new ArrayList<Server>();
        for (String registryCenterServer : registryCenterServerArr) {
            String[] ipPort = registryCenterServer.split(":");
            Server server = new Server();
            server.setIp(ipPort[0]);
            server.setPort(Integer.valueOf(ipPort[1]));
            registryCenterServerList.add(server);
        }
		// 服务注册
        if (microserviceConfig.isRegistry()) {
        	registryToServer(); 
        }
        
        // 服务发现
        if (!CollectionUtils.isEmpty(microserviceConfig.getSubscribeServiceNames())) {
        	for (String subscribeServiceName : microserviceConfig.getSubscribeServiceNames()) {
        		updateServiceList(subscribeServiceName);
        	}
        }
    }

    @Override
    public boolean registryToServer() {
		// 服务注册,就是使用OkHttp请求注册中心服务端...
	}

	// 服务发现
	public boolean updateServiceList(String serviceName) {
        serviceListMap.putIfAbsent(serviceName, new CopyOnWriteArrayList<>());
        // 轮询每个注册中心服务端实例
        for (Server server : registryCenterServerList) {
            // OKHttp代码....
            try {
                response = client.newCall(request).execute();
                if (response.isSuccessful()) {
                	// 将拉取到的注册信息缓存到serviceListMap中
                    Result result = JSONObject.parseObject(Objects.requireNonNull(response.body()).string(), Result.class);
                    List<MicroService> microServiceList = JSONObject.parseArray(JSONObject.toJSONString(result.getData()), MicroService.class);
                    if (CollectionUtils.isEmpty(microServiceList)) {
                    	LOGGER.warn("serviceList is Empty, serviceName={}", serviceName);
                    	continue;
                    }
                    serviceListMap.get(serviceName).clear();
                    serviceListMap.get(serviceName).addAll(microServiceList);
                    return true;
                }
                LOGGER.warn("updateServiceList {} failed: {}", server, response.body() != null ? response.body().toString() : "");
            } catch (IOException e) {
                LOGGER.error("updateServiceList {} error ", server, e);
            }

        }

        return false;
    }
	...
}

客户端的三个定时任务,代码就不看了:

RegistryCenterClient也是通过SpringBoot的自动装配机制自动创建。

spring.factories

java 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.huangjunyi1993.simple.microservice.registry.center.client.config.RegistryCenterClientConfig

RegistryCenterClientConfig

java 复制代码
@Configuration
@EnableScheduling
@ComponentScan(basePackages = {"com.huangjunyi1993.simple.microservice.registry.center.client.task"})
@EnableConfigurationProperties({MicroserviceConfig.class, RegistryCenterConfig.class})
public class RegistryCenterClientConfig {

    @Bean
    @ConditionalOnMissingBean(RegistryCenterClient.class)
    public RegistryCenterClient registryCenterClient() {
        return new SimpleRegistryCenterClient();
    }

}

详细代码可以从git上下载:https://gitee.com/huang_junyi/simple-microservice

相关推荐
也无晴也无风雨27 分钟前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
涔溪2 小时前
Docker简介
spring cloud·docker·eureka
2401_857610034 小时前
多维视角下的知识管理:Spring Boot应用
java·spring boot·后端
代码小鑫4 小时前
A027-基于Spring Boot的农事管理系统
java·开发语言·数据库·spring boot·后端·毕业设计
海波东5 小时前
某m大厂面经1
java·spring
颜淡慕潇5 小时前
【K8S问题系列 | 9】如何监控集群CPU使用率并设置告警?
后端·云原生·容器·kubernetes·问题解决
荆州克莱5 小时前
Mysql学习笔记(一):Mysql的架构
spring boot·spring·spring cloud·css3·技术
独泪了无痕6 小时前
WebStorm 如何调试 Vue 项目
后端·webstorm
林戈的IT生涯6 小时前
一个基于Zookeeper+Dubbo3+SpringBoot3的完整微服务调用程序示例代码
微服务·rpc·dubbo
怒放吧德德7 小时前
JUC从实战到源码:JMM总得认识一下吧
java·jvm·后端