随着人们使用网络比重的日益增加,高并发已经的问题已经不可避免,在微服务体系中,往往需要把项目 根据 需求或者业务拆分成多个服务,项目中每个模块都可能是通过调用多个服务组合而成的。那么在微服务 分而治之 的思想中,如何实现解决高并发的呢?
Eureka为Spring Cloud中的一个组件,这个组件分为Client端和Server端:
server端用于分布式集群中 服务的注册于发现,所有的微服务(微服务中包含Eureka的Client端组件)都可以注册到Eureka Server中并且进行管理,可以根据不同的服务进行分组,同时也可以把相同的镜像服务建立集群。Client端通过心跳的方式在Server端对已注册的服务进行续租,Server通过Client端发送的心跳监控每个注册到 Eureka的服务,如果不符合设定的续租机制,会动态的把死掉的服务剔除。
Client会根据Server中获得的服务列表进行服务的调用,并且根据自己的需求设定 负载均衡机制(轮询,随机,权重)。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
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/
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
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/
GitHub文档:https://github.com/Netflix/eureka/wiki/Eureka-REST-operations
Eureka的所有访问接口都是restfull风格的。
Operation | HTTP action | Description |
---|---|---|
Register new application instance | POST /eureka/v2/apps/appID | Input: JSON/XMLpayload HTTPCode: 204 on success |
De-register application instance | DELETE /eureka/v2/apps/appID/instanceID | HTTP Code: 200 on success |
Send application instance heartbeat | PUT /eureka/v2/apps/appID/instanceID | HTTP Code: * 200 on success * 404 if instanceIDdoesn’t exist |
查询所有实例 | GET /eureka/v2/apps | HTTP Code: 200 on success Output: JSON/XML |
Query for all appID instances | GET /eureka/v2/apps/appID | HTTP Code: 200 on success Output: JSON/XML |
Query for a specific appID/instanceID | GET /eureka/v2/apps/appID/instanceID | HTTP Code: 200 on success Output: JSON/XML |
Query for a specific instanceID | GET /eureka/v2/instances/instanceID | HTTP Code: 200 on success Output: JSON/XML |
Take instance out of service | PUT /eureka/v2/apps/appID/instanceID/status?value=OUT_OF_SERVICE | HTTP 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 metadata | PUT /eureka/v2/apps/appID/instanceID/metadata?key=value | HTTP Code: * 200 on success * 500 on failure |
Query for all instances under a particular vip address | GET /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 address | GET /eureka/v2/svips/svipAddress | * HTTP Code: 200 on success Output: JSON/XML * 404 if the svipAddressdoes not exist. |
演示 查询所有实例,这里使用的Postman工具:
查看某个指定的实例:
Eureka的元数据有两种:标准元数据和自定义元数据。
标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。
自定义元数据:可以使用eureka.instance.metadata-map配置,这些元数据可以在远程客户端中访问,但是一般不改变客户端行为,除非客户端知道该元数据的含义。
application.properties文件配置:
#eureka.instance.metadata-map为固定写法,zidingyikey=zhangsan为自定义key和value
eureka.instance.metadata-map.zidingyikey=zhangsan
现有服务:
创建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 getServices();//获取所有服务实例id。
List 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"
}
]
下面是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集群搭建无论是第一种方案,还是第二种方案,都没有办法保证数据一致性的问题,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
Eureka Server端自带了Actuator,而Eureka Client则需要收到开启,首先道具jar包:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
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..."
}
首先把之前的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,服务上线,查看状态:
Spring Cloud Eureka和spring security有良好的集成,首先在Server端引入security的start启动包:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
在配置文件中添加配置:
#设置账号和密码
spring.security.user.name=admin
spring.security.user.password=123456
启动 Eureka 访问 http://euk1.com:7001/login,出现登录页面,输入username和password,登录:
在设置了登录账号和密码后,在后期Client端进行注册服务的时候,同样需要进行username和password的认证。
只需要修改previder端的配置文件:
#之前的配置是eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/
eureka.client.service-url.defaultZone=http://admin:[email protected]: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);
}
}
这篇文章是本人的个人理解,不保证准确性,如果有错误的地方希望大家留言指正,一起学习共同进步!
如果转载请标明出处。谢谢