Spring Cloud入门

儿童节快乐!儿童节搞点简单的新活。

参考《Spring Cloud微服务实战》

这书太老了,案例代码部分很多弃用了,修改了一下。

需要注意的问题

写在最前面,因为遇到了真的浪费时间。

注意Spring Cloud版本和Spring Boot版本兼容

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.11.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR9</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

注意Log4J的依赖引入和文件配置

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
log4j.properties

log4j.rootLogger=WARN, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n

最简化Samples

服务提供者

HelloController.java

@RestController
public class HelloController {
    private final Logger logger = Logger.getLogger(getClass());

    @Autowired
    private DiscoveryClient client;

    @Qualifier("eurekaRegistration")
    @Autowired
    private Registration registration;

    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    public String index(){
        ServiceInstance instance = serviceInstance();
        String result =" provider service, host = " + instance.getHost()
                + ", service_id = " + instance.getServiceId();
        logger.info(result);
        return "Hello,Provider!"+result;
    }

    public ServiceInstance serviceInstance(){
        List<ServiceInstance> list = client.getInstances(registration.getServiceId());
        if (list != null && list.size() > 0) {
            return list.get(0);
        }
        return null;
    }
}

原书中getLocalServiceInstance() 方法已废除。

application.properties

spring.application.name=hello-service
eureka.instance.prefer-ip-address=true
eureka.client.service-url.defaultZone=http://127.0.0.1:1111/eureka/,http://127.0.0.1:1112/eureka/

多实例开启例:java -jar xxx.jar –server.port=8081

服务注册中心

添加一下依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
application.properties

server.port=1111
eureka.instance.hostname = localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
application-peer1.properties

spring.application.name=eureka-server
server.port=1111
eureka.instance.prefer-ip-address=true
eureka.instance.hostname=peer1
eureka.client.service-url.defaultZone=http://127.0.0.1:1112/eureka/
application-peer2.properties

spring.application.name=eureka-server
server.port=1112
eureka.instance.prefer-ip-address=true
eureka.instance.hostname=peer2
eureka.client.service-url.defaultZone=http://127.0.0.1:1111/eureka/

peer1对应配置文件启用方式例:java -jar xxx.jar –spring.profiles.active=peer1

peer2同理。

服务消费者

Application部分有变化。

@EnableDiscoveryClient
@SpringBootApplication
public class RibbonConsumerApplication {
    @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
        return new RestTemplate();
    }
    public static void main(String[] args) {
        SpringApplication.run(RibbonConsumerApplication.class, args);
    }

}

ConsumerController.java

@RestController
public class ConsumerController {
    @Autowired
    RestTemplate restTemplate;

    @RequestMapping(value = "/ribbon-consumer",method = RequestMethod.GET)
    public String helloConsumer(){
        return restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();
    }
}
application.properties

spring.application.name=ribbon-consumer
server.port=9000
eureka.client.service-url.defaultZone=http://127.0.0.1:1111/eureka/

概念解释分析

有点像基于pub-sub模式(说起来发布订阅模型确实在各个方向都能找到对应的案例)。

提供者向服务中心注册信息。消费者从服务中心获取服务列表,可以知道调用何种任务。

服务中心可以多实例互相注册,防止中心宕机后系统无法运转。

服务提供者

服务注册

提供者在启动时通过发送REST请求,注册至Eureka Server上,同时带上自身服务的一些元数据信息。Eureka Server接收到这个REST请求,将元数据信息储存在一个双层结构Map中,如图

EurekaServer双层Map

服务注册时,需要确认eureka.client.register-with-eureka=true ,为false不启动注册操作。

服务同步

服务中心之间互相注册为服务,当服务提供者发送注册请求到一个服务注册中心时,它会将请求转发给集群中相连的其他注册中心,从而实现服务的同步。

服务续约

通过维护心跳来保持存活(常见做法)。

# 服务续约任务的调用间隔
eureka.instance.lease-renewal-interval-in-seconds=30

# 服务失效时间
eureka.instance.lease-expiration-duration-in-seconds=90

服务消费者

获取服务

发送REST到注册中心,获取服务清单,缓存清单30秒更新一次。

eureka.client.fetch-registry=true

# 修改缓存清单的更新时间
eureka.client.registry-fetch-intercal-seconds=30

服务调用

获取清单后通过服务名获得具体提供服务的实例名和该实例的元数据信息。Ribbon中默认采用轮询方式调用,实现客户端的负载均衡。

Eureka中有Region和Zone的概念,即大小区块。Region中有多个Zone,每个服务客户端需要被注册到一个Zone中,每个客户端对应一个Region一个Zone。

服务调用时优先访问同一个Zone中的服务提供方,访问不到才访问其他Zone。

服务下线

服务实例正常关闭时,会触发下线的REST请求给服务中心。服务中心接收会将服务状态置为DOWN,并传播出去。

服务注册中心

失效剔除

为了避免服务宕机或其他情况突然无法工作,服务中心设立定时任务,定时清除超时没有续约的服务。

自我保护

Eureka Server运行期间,如果心跳失败的比率在15分钟之内低于85%,Server将会将当前的实例注册信息保护起来,让这些实例不过期。但如果这段期间实例出现问题,客户端很有可能拿到实际已经无法工作(不存在)的服务实例,就会出现调用失败。客户端需要有容错机制(请求重试、断路器)。