跳到主要内容

eureka-statefulset.yaml

Spring Cloud开发、交付实践

https://spring.io/projects/spring-cloud#overview

1、Netflix是一家做视频的网站,可以这么说该网站上的美剧应该是最火的。

2、Netflix是一家没有CTO的公司,正是这样的组织架构能使产品与技术无缝的沟通,从而能快速迭代出更优秀的产品。在当时软件敏捷开发中,Netflix的更新速度不亚于当年的微信后台变更,虽然微信比Netflix迟发展,但是当年微信的灰度发布和敏捷开发应该算是业界最猛的。

3、Netflix由于做视频的原因,访问量非常的大,从而促使其技术快速的发展在背后支撑着,也正是如此,Netflix开始把整体的系统往微服务上迁移。

4、Netflix的微服务做的不是最早的,但是确是最大规模的在生产级别微服务的尝试。也正是这种大规模的生产级别尝试,在服务器运维上依托AWS云。当然AWS云同样受益于Netflix的大规模业务不断的壮大。

5、Netflix的微服务大规模的应用,在技术上毫无保留的把一整套微服务架构核心技术栈开源了出来,叫做Netflix OSS,也正是如此,在技术上依靠开源社区的力量不断的壮大。

6、Spring Cloud是构建微服务的核心,而Spring Cloud是基于Spring Boot来开发的。

7、Pivotal在Netflix开源的一整套核心技术产品线的同时,做了一系列的封装,就变成了Spring Cloud;虽然Spring Cloud到现在为止不只有Netflix提供的方案可以集成,还有很多方案,但Netflix是最成熟的。

> 本课程基于SpringBoot 2.3.6.RELEASE 和Spring Cloud Hoxton.SR9 版本

微服务场景

开发APP,提供个人的花呗账单管理。

  • 注册、登录、账单查询
  • 用户服务,账单管理服务

Eureka服务注册中心

SpringCloud体系中,我们知道服务之间的调用是通过http协议进行调用的。而注册中心的主要目的就是维护这些服务的服务列表。

https://docs.spring.io/spring-cloud-netflix/docs/3.0.3/reference/html/

新建项目

pom中引入spring-cloud的依赖:

https://spring.io/projects/spring-cloud#overview

<properties>
<spring.cloud-version>Hoxton.SR9</spring.cloud-version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

引入eureka-server的依赖:

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

https://docs.spring.io/spring-cloud-netflix/docs/2.2.5.RELEASE/reference/html/#spring-cloud-eureka-server-standalone-mode

application.yml

server:
port: 8761

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

启动类:

package com.luffy.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}

启动访问localhost:8761测试

创建spring cloud项目三部曲:

  • 引入依赖包
  • 修改application.yml配置文件
  • 启动类添加注解
eureka认证

没有认证,不安全,添加认证:

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

application.yml

server:
port: 8761
eureka:
client:
service-url:
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@${eureka.instance.hostname}:${server.port}/eureka/
register-with-eureka: false
fetch-registry: false
instance:
hostname: localhost
spring:
security:
user:
name: ${EUREKA_USER:admin}
password: ${EUREKA_PASS:admin}
application:
name: eureka
注册服务到eureka

新建项目,user-service(选择Spring Cloud依赖和SpringBoot Web依赖),用来提供用户查询功能。

三部曲:

  • pom.xml,并添加依赖
  • 创建application.yml配置文件
  • 创建Springboot启动类,并配置注解

在默认的pom.xml中添加:

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

pom.xml添加:

\<?xml version="1.0" encoding="UTF-8"?\>
<project xmlns="http://maven.apache.org/POM/_4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"\>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version>
<relativePath/> \<!-- lookup parent from repository --\>
</parent>
<groupId>com.luffy</groupId>
<artifactId>user-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>user-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring.cloud-version>Hoxton.SR9</spring.cloud-version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

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

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

application.yml

server:
port: 7000
eureka:
client:
serviceUrl:
defaultZone: http://${EUREKA_USER:admin}:${EUREKA_PASS:admin}@localhost:8761/eureka/
instance:
instance-id: ${eureka.instance.hostname}:${server.port}
prefer-ip-address: true
hostname: user-service
spring:
application:
name: user-service

启动类:

package com.luffy.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

//注意这里也可使用@EnableEurekaClient
//但由于springcloud是灵活的,注册中心支持eureka、consul、zookeeper等
//若写了具体的注册中心注解,则当替换成其他注册中心时,又需要替换成对应的注解了。
//所以 直接使用@EnableDiscoveryClient 启动发现。
//这样在替换注册中心时,只需要替换相关依赖即可。
@EnableDiscoveryClient
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}

报错:

c.n.d.s.t.d.RetryableEurekaHttpClient    : Request execution failed with message: com.fasterxml.jackson.databind.exc.MismatchedInputException: Root name 'timestamp' does not match expected ('instance') for type [simple type, class com.netflix.appinfo.InstanceInfo]

新版本的security默认开启csrf了,关掉,在注册中心新建一个类,继承WebSecurityConfigurerAdapter来关闭 ,> 注意,是在eureka server端关闭。

package com.luffy.eureka;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
@Configuration
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(); //关闭csrf
http.authorizeRequests().anyRequest().authenticated().and().httpBasic(); //开启认证
}
}

当注册中心后面维护的服务实例出现故障后,注册中心会存在时间差来感知到服务故障,这个时间差主要通过如下方面来调节:

  • eureka server检测实例是否过期的周期时间

    • eureka server端

      server:
      port: 8761
      eureka:
      client:
      service-url:
      defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@${eureka.instance.hostname}:${server.port}/eureka/
      register-with-eureka: false
      fetch-registry: false
      instance:
      hostname: localhost
      server:
      # 间隔5秒执行一次检测任务
      eviction-interval-timer-in-ms: 5000
      spring:
      security:
      user:
      name: ${EUREKA_USER:admin}
      password: ${EUREKA_PASS:admin}
      application:
      name: eureka
  • eureka client注册端设置的心跳超时后剔除该实例的时间

    • 针对user-service

      server:
      port: 7000
      eureka:
      client:
      serviceUrl:
      defaultZone: http://${EUREKA_USER:admin}:${EUREKA_PASS:admin}@localhost:8761/eureka/
      instance:
      instance-id: ${eureka.instance.hostname}:${server.port}
      prefer-ip-address: true
      hostname: user-service
      #eureka客户端需要多长时间发送心跳给eureka服务器,表明他仍然或者,默认30秒
      lease-renewal-interval-in-seconds: 2
      #eureka服务器在接受到实例的最后一次发出的心跳后,需要等待多久才可以将此实例删除
      lease-expiration-duration-in-seconds: 2
      spring:
      application:
      name: user-service

Eurake有一个配置参数eureka.server.renewalPercentThreshold,定义了renews 和renews threshold的比值,默认值为0.85。当server在15分钟内,比值低于percent,即少了15%的微服务心跳,server会进入自我保护状态

默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,这就可能变得非常危险了,因为微服务本身是健康的,此时本不应该注销这个微服务。

Eureka Server通过“自我保护模式”来解决这个问题,当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,Eureka Server就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。

自我保护模式是一种对网络异常的安全保护措施。使用自我保护模式,而让Eureka集群更加的健壮、稳定。

开发阶段可以通过配置:eureka.server.enable-self-preservation=false关闭自我保护模式。

生产阶段,理应以默认值进行配置。

至于具体具体的配置参数,可至官网查看:http://cloud.spring.io/spring-cloud-static/Finchley.RELEASE/single/spring-cloud.html#_appendix_compendium_of_configuration_properties

高可用

高可用:

  • 优先保证可用性
  • 各个节点都是平等的,1个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务
  • 在向某个Eureka注册时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性)

注意点:

  • 多实例的话eureka.instance.instance-id需要保持不一样,否则会当成同一个
  • eureka.instance.hostname要与defaultZone里的地址保持一致
  • 各个eureka的spring.application.name相同

服务提供者若想连接高可用的eureka,需要修改:

      defaultZone: http://${EUREKA_USER:admin}:${EUREKA_PASS:admin}@peer1:8762/eureka/,http://${EUREKA_USER:admin}:${EUREKA_PASS:admin}@peer2:8763/eureka/

部署k8s集群时,将eureka的集群地址通过参数的形式传递到pod内部,因此本地开发时,直接按照单点模式进行:

server:
port: ${EUREKA_PORT:8761}
eureka:
client:
service-url:
defaultZone: ${EUREKA_SERVER:http://${spring.security.user.name}:${spring.security.user.password}@localhost:8761/eureka/}
fetch-registry: true
register-with-eureka: true
instance:
instance-id: ${eureka.instance.hostname}:${server.port}
hostname: ${EUREKA_INSTANCE_HOSTNAME:localhost}
prefer-ip-address: true
server:
# 间隔5秒执行一次检测任务
eviction-interval-timer-in-ms: 5000
spring:
security:
user:
name: ${EUREKA_USER:admin}
password: ${EUREKA_PASS:admin}
application:
name: eureka-cluster
k8s交付

分析:

高可用互相注册,但是需要知道对方节点的地址。k8s中pod ip是不固定的,如何将高可用的eureka服务使用k8s交付?

  • 使用statefulset管理

添加Dockerfile

在pom.xml中重写jar包名称:

<finalName>${project.artifactId}</finalName>

Dockerfile

FROM openjdk:8-jdk-alpine
ADD target/eureka.jar app.jar
ENV JAVA_OPTS=""
CMD [ "sh", "-c", "java $JAVA_OPTS -jar /app.jar" ]

eureka-statefulset.yaml

# eureka-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: eureka-cluster
namespace: luffy
spec:
serviceName: "eureka"
replicas: 3
selector:
matchLabels:
app: eureka-cluster
template:
metadata:
labels:
app: eureka-cluster
spec:
imagePullSecrets:
- name: registry-172-21-65-226
containers:
- name: eureka
image: 172.21.65.226:5000/spring-cloud/eureka-cluster:v1
ports:
- containerPort: 8761
resources:
requests:
memory: 400Mi
cpu: 50m
limits:
memory: 2Gi
cpu: 2000m
env:
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: EUREKA_SERVER
value: "http://admin:admin@eureka-cluster-0.eureka:8761/eureka/,http://admin:admin@eureka-cluster-1.eureka:8761/eureka/,http://admin:admin@eureka-cluster-2.eureka:8761/eureka/"
- name: EUREKA_INSTANCE_HOSTNAME
value: ${MY_POD_NAME}.eureka
- name: EUREKA_PORT
value: "8761"

eureka-headless-service.yaml

apiVersion: v1
kind: Service
metadata:
name: eureka
namespace: luffy
labels:
app: eureka
spec:
ports:
- port: 8761
name: eureka
clusterIP: None
selector:
app: eureka-cluster

ingress提供访问入口:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: eureka-cluster
namespace: dev
spec:
ingressClassName: nginx
rules:
- host: eureka-cluster.luffy.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: eureka
port:
number: 8761