Spring Cloud :1 . Netflix Eureka

Eureka的作用

随着人们使用网络比重的日益增加,高并发已经的问题已经不可避免,在微服务体系中,往往需要把项目 根据 需求或者业务拆分成多个服务,项目中每个模块都可能是通过调用多个服务组合而成的。那么在微服务 分而治之 的思想中,如何实现解决高并发的呢?

  • 分布式:不同的服务 分 布在不同的主机,根据请求的业务不同去请求对应的主机,从而分担访问压力(每台机器运行不同业务的代码)。
  • 集群:在分布式中,如果某个热点服务依旧访问压力大,可以对这个服务搭建镜像副本,通过负载均衡器对 这些 镜像副本 做负载均衡(多太机器运行相同的业务的代码)。

Eureka为Spring Cloud中的一个组件,这个组件分为Client端和Server端:
server端用于分布式集群中 服务的注册于发现,所有的微服务(微服务中包含Eureka的Client端组件)都可以注册到Eureka Server中并且进行管理,可以根据不同的服务进行分组,同时也可以把相同的镜像服务建立集群。Client端通过心跳的方式在Server端对已注册的服务进行续租,Server通过Client端发送的心跳监控每个注册到 Eureka的服务,如果不符合设定的续租机制,会动态的把死掉的服务剔除。
Client会根据Server中获得的服务列表进行服务的调用,并且根据自己的需求设定 负载均衡机制(轮询,随机,权重)。


Eureka Server集群的搭建

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

我这里使用的是Intellij IDEA创建Spring Initializr项目快速搭建Spring Boot。

Eureka搭建集群有两种方式,无论哪种方式,都是引入Eureka包并且配置application.properties文件直接启动即可:

第一种方式

在这里插入图片描述

Eureka服务器之间互相独立,相互之间不可见,没有依赖关系。但是在Client端注册的时候,需要对每台Eureka进行单独注册,由Client对所有Eureka发送心跳和拉取。

优点:搭建简单。
缺点:无论Client注册 还是 拉取服务列表,都要访问多台Eureka,对Eureka服务器的访问压力比较大.

application.properties文件:

#是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息
eureka.client.register-with-eureka=false
#是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false
eureka.client.fetch-registry=false
#设置服务注册中心的URL,用于client和server端交流,这里需要和启动的tomcat的端口号一致
eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/
spring.application.name=eureka-server
#这里需要先修改hosts文件,对127.0.0.1进行映射,区分不同的主机
eureka.instance.hostname=euk1.com
server.port=7001

第二台Eureka只需要修改端口号即可。

修改hosts文件:“C:\Windows\System32\drivers\etc\hosts”,在末尾处添加

127.0.0.1 euk1.com
127.0.0.1 euk2.com

第二种方式

在这里插入图片描述

多个Eureka互为客户端,互相注册,互相拉取,把对方的数据拉倒自己的本地上。Client只需要访问唯一一台Eureka即可。

application.properties文件:

#需要注册和拉取服务的Eureka地址,为另外一台Eureka暴露出来交互的url。
eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/
#注册到Eureka服务器上的集群分组名称,集群所有Eureka这个属性必须统一
spring.application.name=eureka-server
#当前主机的IP映射的域名
eureka.instance.hostname=euk2.com
#当前Eureka启动的端口(tomcat启动端口)
server.port=7002

#下面这两项可以不配置,因为默认为ture
#是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息
eureka.client.register-with-eureka=ture
#是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false
eureka.client.fetch-registry=ture

第二台的配置文件只需要修改拉取和注册的Eureka地址和当前注意IP的映射地址以及端口号即可。

启动项目,访问http://euk2.com:7002/

在这里插入图片描述


Eureka Client发起注册

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

创建Spring Initializr,天界Spring Web 和 Eureka Client 包。

@RestController
public class MainController {

    @GetMapping("/gethello")
    public String getHello(){
        return "hello";
    }
}

创建一个简单的服务,并且修改application.properties文件:

#当前注册到Eureka服务的名称
spring.application.name=eureka-provider
#当前服务启动端口
server.port=8080
# 往三个Eureka上面注册服务,进行高可用
eureka.client.service-url.defaultZone=http://euk2.com:7001/eureka/,http://euk2.com:7002/eureka/,http://euk2.com:7003/eureka/

访问:http://euk2.com:7002/

在这里插入图片描述


Eureka对外暴露的访问接口

GitHub文档:https://github.com/Netflix/eureka/wiki/Eureka-REST-operations

Eureka的所有访问接口都是restfull风格的。

OperationHTTP actionDescription
Register new application instancePOST /eureka/v2/apps/appIDInput: JSON/XMLpayload HTTPCode: 204 on success
De-register application instanceDELETE /eureka/v2/apps/appID/instanceIDHTTP Code: 200 on success
Send application instance heartbeatPUT /eureka/v2/apps/appID/instanceIDHTTP Code: * 200 on success * 404 if instanceIDdoesn’t exist
查询所有实例GET /eureka/v2/appsHTTP Code: 200 on success Output: JSON/XML
Query for all appID instancesGET /eureka/v2/apps/appIDHTTP Code: 200 on success Output: JSON/XML
Query for a specific appID/instanceIDGET /eureka/v2/apps/appID/instanceIDHTTP Code: 200 on success Output: JSON/XML
Query for a specific instanceIDGET /eureka/v2/instances/instanceIDHTTP Code: 200 on success Output: JSON/XML
Take instance out of servicePUT /eureka/v2/apps/appID/instanceID/status?value=OUT_OF_SERVICEHTTP Code: * 200 on success * 500 on failure
Move instance back into service (remove override)DELETE /eureka/v2/apps/appID/instanceID/status?value=UP (The value=UP is optional, it is used as a suggestion for the fallback status due to removal of the override)HTTP Code: * 200 on success * 500 on failure
Update metadataPUT /eureka/v2/apps/appID/instanceID/metadata?key=valueHTTP Code: * 200 on success * 500 on failure
Query for all instances under a particular vip addressGET /eureka/v2/vips/vipAddress* HTTP Code: 200 on success Output: JSON/XML * 404 if the vipAddressdoes not exist.
Query for all instances under a particular secure vip addressGET /eureka/v2/svips/svipAddress* HTTP Code: 200 on success Output: JSON/XML * 404 if the svipAddressdoes not exist.

演示 查询所有实例,这里使用的Postman工具:

在这里插入图片描述

查看某个指定的实例:

在这里插入图片描述


Eureka自定义元数据

Eureka的元数据有两种:标准元数据和自定义元数据。
标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。
自定义元数据:可以使用eureka.instance.metadata-map配置,这些元数据可以在远程客户端中访问,但是一般不改变客户端行为,除非客户端知道该元数据的含义。

application.properties文件配置:

#eureka.instance.metadata-map为固定写法,zidingyikey=zhangsan为自定义key和value
eureka.instance.metadata-map.zidingyikey=zhangsan

在这里插入图片描述


Client获取Eureka Server的服务列表

DiscoveryClient接口

现有服务:

在这里插入图片描述

创建Spring Initisalizr项目,项目名称为 eureka-consumer ,为服务的消费方。主要代码如下:

@RestController
public class MainController {

    @Autowired
    private DiscoveryClient client;

    @GetMapping("/client")
    public Object getclient(){
        return client;
    }
}

访问:http://localhost/client

{
    "discoveryClients": [
        {
            "services": [
                "eureka-server",
                "eureka-provider",
                "eureka-consumer"
            ],
            "order": 0
        },
        {
            "services": [],
            "order": 0
        }
    ],
    "services": [
        "eureka-server",
        "eureka-provider",
        "eureka-consumer"
    ],
    "order": 0
}

可以看到返回了所有的服务名。

DiscoveryClient接口方法:

String description();//获取实现类的描述。
List<String> getServices();//获取所有服务实例id。
List<ServiceInstance> getInstances(String serviceId);//通过服务id查询服务实例信息列表。

演示:

    @GetMapping("/getProvider")
    public Object getProvider(){
        //获取服务提供方
        return client.getInstances("eureka-provider");
    }

访问:http://localhost/getProvider

[
    {
        "scheme": "http",
        "host": "localhost",
        "port": 8080,
        "metadata": {
            "zidingyi": "lisi",				#之前设置的 元数据 这里也有显示
            "management.port": "8080"		#端口号
        },
        "secure": false,
        "uri": "http://localhost:8080",		#访问地址
        "instanceInfo": {
            "instanceId": "localhost:eureka-provider:8080",		#实例ID
            "app": "EUREKA-PROVIDER",							#APP ID
            "appGroupName": null,
            "ipAddr": "192.168.13.1",							#IP
            "sid": "na",
            "homePageUrl": "http://localhost:8080/",
            "statusPageUrl": "http://localhost:8080/actuator/info",
            "healthCheckUrl": "http://localhost:8080/actuator/health",
            "secureHealthCheckUrl": null,
            "vipAddress": "eureka-provider",
            "secureVipAddress": "eureka-provider",
            "countryId": 1,
            "dataCenterInfo": {
                "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
                "name": "MyOwn"
            },
            "hostName": "localhost",
            "status": "UP",					#状态
            "overriddenStatus": "UNKNOWN",
            "leaseInfo": {
                "renewalIntervalInSecs": 30,
                "durationInSecs": 90,
                "registrationTimestamp": 1594019652826,
                "lastRenewalTimestamp": 1594019652826,
                "evictionTimestamp": 0,
                "serviceUpTimestamp": 1594016342749
            },
            "isCoordinatingDiscoveryServer": false,
            "metadata": {
                "zidingyi": "lisi",
                "management.port": "8080"
            },
            "lastUpdatedTimestamp": 1594019652826,
            "lastDirtyTimestamp": 1594019652794,
            "actionType": "ADDED",
            "asgName": null
        },
        "instanceId": "localhost:eureka-provider:8080",
        "serviceId": "EUREKA-PROVIDER"
    }
]

获取URL,并且访问远程服务

下面是Client通过从Eureka Server中获取列表后访问远程服务的最简单方式,工作中不使用,但是有助于更好的理解Eureka。

@GetMapping("/getUrl")
    public Object getUrl(){

        List<ServiceInstance> instances = client.getInstances("eureka-provider");

        for (ServiceInstance instance : instances) {
            //获取所有服务列表
            System.out.println(ToStringBuilder.reflectionToString(instance));
        }

        if (instances.size()>0){
        	//此处可以修改成负载均衡算法
            ServiceInstance instance = instances.get(0);
            String host = instance.getHost();
            int port = instance.getPort();
            //拼接url,得到url之后就可以直接通过RestTemplate访问服务了。也可以使用HttpClient,但是很low
            String url="http://"+host+":"+port+"/gethello";
            System.out.println(url);
            RestTemplate restTemplate = new RestTemplate();
            //远程调用,获得返回结果
            String result = restTemplate.getForObject(url, String.class);
            System.out.println(result);
        }
        return "ooxx";
    }

控制台输出结果:

org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient$EurekaServiceInstance@6e442cd8[instance=InstanceInfo [instanceId = localhost:eureka-provider:8080, appName = EUREKA-PROVIDER, hostName = localhost, status = UP, ipAddr = 192.168.13.1, port = 8080, securePort = 443, dataCenterInfo = com.netflix.appinfo.MyDataCenterInfo@33622f1f]
http://localhost:8080/gethello
hello

使用Ribbon的方式来做负载均衡:

	
	//springcloud 提供队ribbon的封装
    @Autowired
    private LoadBalancerClient lb;

    @GetMapping("/ribbon")
    public Object ribbon(){

        //ribbon自动做负载均衡获取到一个实例对象,可以获得实例的信息
        ServiceInstance choose = lb.choose("eureka-provider");
        String host = choose.getHost();
        int port = choose.getPort();
        String url="http://"+host+":"+port+"/gethello";
        System.out.println(url);
        RestTemplate restTemplate = new RestTemplate();
        //远程调用,获得返回结果
        String result = restTemplate.getForObject(url, String.class);
        System.out.println(result);

        return "ooxx";
    }

Eureka的CAP原则 和 自我保护机制

Eureka集群搭建无论是第一种方案,还是第二种方案,都没有办法保证数据一致性的问题,Eureka的诞生是面向为服务系统的注册中心,Client向Eureka注册服务信息的时候,每个Eureka的处理时间可能不一样,其中一个处理可能需要20毫秒,另外一个可能需要200毫秒,在这个时间窗内,两个Eureka的数据可能就是不一致的。
在CAP原则中,Eureka侧重于 可用性 和 分区容错性(AP),在第2种Eureka集群模型中,第一个Eureka作为第二个Eureka的客户端,Service的Client在Eureka注册之后,需要定期的跟Eureka发送心跳而保证续租,同时Eureka集群之间需要定期的同步,默认为30秒更新一次,在30秒内,Eureka集群中每个节点的数据可能是不一致的。

在这里插入图片描述

但是由于网络抖动的问题,Eureka Server不能及时收到来自Client端的心跳,在Eureka Server内部会维护一个期望心跳值(可以自定义设置),如果最后一分钟实际收到的 心跳数量 < 期望值 * 0.85(默认可设置),就会触发自我保护机制,Eureka Server中维护的服务列表将不会过期。待网络故障恢复后,就会推出自我保护机制。

自我保护机制也是为了提高服务的可用性,其核心思想为:宁可保留错误的注册信息,也不盲目注销健康的信息。

application.properties文件:

server端的配置:

#关闭自我保护模式
eureka.server.enable-self-preservation=false
#失效服务间隔
eureka.server.eviction-interval-timer-in-ms=3000

Client端的配置(可以配置,也可以不配置):

#续约发送间隔默认30秒,心跳间隔
eureka.instance.lease-renewal-interval-in-seconds=5
#表示eureka client间隔多久去拉取服务注册信息,默认为30秒,对于api-gateway,如果要迅速获取服务注册状态,可以缩小该值,比如5秒
eureka.client.registry-fetch-interval-seconds=5
# 续约到期时间(默认90秒)
eureka.instance.lease-expiration-duration-in-seconds=60

使用Actuator监控信息

Eureka Server端自带了Actuator,而Eureka Client则需要收到开启,首先道具jar包:

		<!--上报节点信息-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

启动项目(eureka-provider),访问 http://localhost:8080/actuator,得到json

{
    "_links": {
        "self": {
            "href": "http://localhost:8080/actuator",					#刚刚访问的地址
            "templated": false
        },
        "health-path": {
            "href": "http://localhost:8080/actuator/health/{*path}",	#查看当前服务的某个接口是否健康
            "templated": true
        },
        "health": {
            "href": "http://localhost:8080/actuator/health",			#查看当前服务是否健康
            "templated": false
        },
        "info": {
            "href": "http://localhost:8080/actuator/info",
            "templated": false
        }
    }
}

查看当前服务是否健康,访问http://localhost:8080/actuator/health,得到结果:

{
    "status": "UP"
}

Actuator默认暴露的接口较少,默认只有actuator和info接口,可以在配置文件中添加配置,暴露所有接口:

#默认为management.endpoints.web.exposure.include=actuator,info
management.endpoints.web.exposure.include=*

重启项目后,访问 http://localhost:8080/actuator,得到结果:

{
    "_links": {
        "self": {
            "href": "http://localhost:8080/actuator",
            "templated": false
        },
        "archaius": {
            "href": "http://localhost:8080/actuator/archaius",
            "templated": false
        },
        "beans": {
            "href": "http://localhost:8080/actuator/beans",
            "templated": false
        },
        "caches-cache": {
            "href": "http://localhost:8080/actuator/caches/{cache}",
            "templated": true
        },
        "caches": {
            "href": "http://localhost:8080/actuator/caches",
            "templated": false
        },
        "health": {
            "href": "http://localhost:8080/actuator/health",
            "templated": false
        },
        "health-path": {
            "href": "http://localhost:8080/actuator/health/{*path}",
            "templated": true
        },
        "info": {
            "href": "http://localhost:8080/actuator/info",
            "templated": false
        },
        "conditions": {
            "href": "http://localhost:8080/actuator/conditions",
            "templated": false
        },
        "configprops": {
            "href": "http://localhost:8080/actuator/configprops",
            "templated": false
        },
        "env": {
            "href": "http://localhost:8080/actuator/env",
            "templated": false
        },
        "env-toMatch": {
            "href": "http://localhost:8080/actuator/env/{toMatch}",
            "templated": true
        },
        "loggers": {
            "href": "http://localhost:8080/actuator/loggers",
            "templated": false
        },
        "loggers-name": {
            "href": "http://localhost:8080/actuator/loggers/{name}",
            "templated": true
        },
        "heapdump": {
            "href": "http://localhost:8080/actuator/heapdump",
            "templated": false
        },
        "threaddump": {
            "href": "http://localhost:8080/actuator/threaddump",
            "templated": false
        },
        "metrics-requiredMetricName": {
            "href": "http://localhost:8080/actuator/metrics/{requiredMetricName}",
            "templated": true
        },
        "metrics": {
            "href": "http://localhost:8080/actuator/metrics",
            "templated": false
        },
        "scheduledtasks": {
            "href": "http://localhost:8080/actuator/scheduledtasks",
            "templated": false
        },
        "mappings": {
            "href": "http://localhost:8080/actuator/mappings",
            "templated": false
        },
        "refresh": {
            "href": "http://localhost:8080/actuator/refresh",
            "templated": false
        },
        "features": {
            "href": "http://localhost:8080/actuator/features",
            "templated": false
        },
        "service-registry": {
            "href": "http://localhost:8080/actuator/service-registry",
            "templated": false
        }
    }
}

通过观察发现里面并没有shutdown的接口,shutdown接口可以远程使服务的状态从up更改为down,如果需要开启,需要在配置文件中单独配置:

management.endpoint.shutdown.enabled=true

重启服务,再次访问 http://localhost:8080/actuator可以看到已经有shutdown的接口了:

		shutdown": {
            "href": "http://localhost:8080/actuator/shutdown",
            "templated": false
        }

访问http://localhost:8080/actuator/shutdown注意:必须是post请求,可以使用postman工具发送)后,会发现服务程序已经终止运行。

{
    "message": "Shutting down, bye..."
}

手动Down掉服务列表中的服务

首先把之前的provider服务再创建一个镜像,测试的话只需要两个provider只是端口号不同,并且启动:

在这里插入图片描述
在配置文件中配置:

#上报当前服务真正的健康信息
management.endpoint.shutdown.enabled=true

那么什么是当前服务真正的健康信息呢?

在这里插入图片描述

Eureka Client的健康状态是根据心跳来决定维护的,但是有的时候,服务虽然在运行,但是业务代码已经出现了异常不可被访问,这个时候Client依然会给Eureka Server发送心跳,而这个时候我们可以通过 try…catch 代码块捕获异常,主动 修改Server端当前服务的状态为Down。

创建类 HealthStatusService:

@Service
public class HealthStatusService implements HealthIndicator {

    private Boolean status = true;

    public void setStatus(Boolean status) {
        this.status  = status;
    }

    @Override
    public Health health() {

        if(status){
        	//如果状态为Ture,则服务上线
            return new Health.Builder().up().build();
        }
        //如果状态为false,则服务下线
        return new Health.Builder().down().build();
    }

    public String getStatus() {

        return this.status.toString();
    }
}

创建Controller:

    @Autowired
    HealthStatusService healthStatusService;

    @GetMapping("/health")
    public String health(@RequestParam("status") Boolean status) {

        healthStatusService.setStatus(status);
        return healthStatusService.getStatus();
    }

访问:http://localhost:8080/health?status=false,查看Eureka服务列表:

在这里插入图片描述

访问http://localhost:8080/health?status=true,服务上线,查看状态:

在这里插入图片描述

Eureka安全认证

Spring Cloud Eureka和spring security有良好的集成,首先在Server端引入security的start启动包:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

在配置文件中添加配置:

#设置账号和密码
spring.security.user.name=admin
spring.security.user.password=123456

启动 Eureka 访问 http://euk1.com:7001/login,出现登录页面,输入username和password,登录:

Eureka登录

在设置了登录账号和密码后,在后期Client端进行注册服务的时候,同样需要进行username和password的认证。

只需要修改previder端的配置文件:

#之前的配置是eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/
eureka.client.service-url.defaultZone=http://admin:123456@euk1.com:7001/eureka/

需要注意的是Server端要关闭防跨域攻击,不然Client端无法注册成功,导致报错。代码如下:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //关闭跨域攻击
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        super.configure(http);
    }

}

这篇文章是本人的个人理解,不保证准确性,如果有错误的地方希望大家留言指正,一起学习共同进步!
如果转载请标明出处。谢谢

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值