一. 搭建简单的微服务工程

1. 从单体架构到微服务架构的演变

我们最先接触的单体架构,整个系统就只有一个工程,打包往往是打成了 war 包,然后部署 到单一 tomcat 上面,这种就是单体架构,如图:

微服务搭建一般有哪些模块 微服务工程搭建_microservices

单体架构优点

1、结构简单,部署简单

2、所需的硬件资源少

3、节省成本

缺点

1、版本迭代慢,往往改动一个代码会影响全局

2、不能满足一定并发的访问

3、代码维护困难,所有代码在一个工程里面,存在被其他人修改的风险

随着业务的拓展,公司的发展,单体架构慢慢的不能满足我们的需求,我们需要对架构进行 变动,我们能够想到的最简单的办法就是加机器,对应用横向扩展。 如图:

微服务搭建一般有哪些模块 微服务工程搭建_spring_02

这种架构貌似暂时解决了我们的问题,但是用户量慢慢增加后,我们只能通过横向加机器来解决,还是会存在版本迭代慢,代码维护困难的问题。而且用户请求往往是读多写少的情况, 所以可能真正需要扩容的只是业务量大的模块而已,而现在是整个工程都扩容了,这无形中是一种资源的浪费,因为其他模块可能根本不需要扩容就可以满足需求。所以我们有必要对整个工 程按照模块进行拆分,拆分后的架构图如下:

微服务搭建一般有哪些模块 微服务工程搭建_微服务搭建一般有哪些模块_03

模块拆分后,模块和模块之间是需要通过接口调用的方式进行通信,模块和模块之间通过分流软件进行负载均衡。这个架构解决前面的资源浪费问题和代码管理问题,因为我们是对系统拆分了,各个模块都有单独的工程,比如我修改商品模块,就不需要担心会不会影响购物车模块。但是这种架构扩展非常麻烦,一旦需要横向加机器,或者减机器都需要修改 nginx 配置,一旦机器变多了以后,nginx 的配置量就是一个不能完成的工作。OK,这时候 SOA 服 务治理框架就应运而生,架构图如下:

微服务搭建一般有哪些模块 微服务工程搭建_spring_04

基于注册中心的 SOA 框架,扩展是非常方便的,因为不需要维护分流工具,但我们启动应 用的时候就会把服务通过 http 的方式注册到注册中心。

在 SOA 框架中一般会有三种角色:1、注册中心 2、服务提供方 3、服务消费方

1、注册中心

在注册中心维护了服务列表

2、服务提供方

服务提供方启动的时候会把自己注册到注册中心

3、服务消费方

服务消费方启动的时候,把获取注册中心的服务列表,然后调用的时候从这个服务列表中选择某一个去调用。

微服务工程的特点:

1、扩展灵活

2、每个应用都规模不大

3、服务边界清晰,各司其职

4、打包应用变多,往往需要借助 CI 持续集成工具

2. 简单的微服务工程搭建

1、注册中心搭建(eureka服务端: netflix-eureka-server)

Springcloud 中,我们选择 eureka 作为注册中心,springcloud 工程是基于 springboot 工程的。 pom.xml 中 jar 包依赖:

<!--parent-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.2.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<!--Springcloud 的版本-->
<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
</properties>

<!--Eureka 服务端启动器导入-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!--Springcloud 的依赖仓库导入-->
<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>

application.properties 配置文件(不能叫bootstrap.properties)

server.port=8763
eureka.instance.hostname=localhost
#是否注册到eureka
eureka.client.registerWithEureka=false
#是否从eureka中拉取注册信息
eureka.client.fetchRegistry=false
##暴露eureka服务的地址
# http://localhost:8763/eureka/
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

#自我保护模式,当出现出现网络分区、eureka在短时间内丢失过多客户端时,会进入自我保护模式,即一个服务长时间没有发送心跳,eureka也不会将其删除,默认为true
eureka.server.enable-self-preservation=true

#eureka server清理无效节点的时间间隔,默认60000毫秒,即60秒
eureka.server.eviction-interval-timer-in-ms=60000

#服务手动下线 delete请求 http://localhost:8763/eureka/apps/MICRO-ORDER/localhost:xxx:8084

启动类

package len.hgy;

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

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

查看

http://localhost:8763/

2、服务提供方(eureka 客户端: netflix-eureka-client)

Pom 的 jar 包依赖,其他都跟 eureka 服务端是一样的,只是服务提供方要把服务注册到 eureka 服务端,所以服务提供方就是 eureka 的客户端,所以需要导入 eureka 客户端的启动器。

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

bootstrap.properties

spring.application.name=micro-order
server.port=8084

eureka.client.serviceUrl.defaultZone=http://localhost:8763/eureka/
# 一下有默认配置
#服务续约,心跳的时间间隔
eureka.instance.lease-renewal-interval-in-seconds=30
#如果从前一次发送心跳时间起,90秒没接受到新的心跳,讲剔除服务
eureka.instance.lease-expiration-duration-in-seconds=90
#表示eureka client间隔多久去拉取服务注册信息,默认为30秒
eureka.client.registry-fetch-interval-seconds=30

启动类

package len.hgy;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication(scanBasePackages = {"len.hgy"})
// 注册到eureka
@EnableEurekaClient
@MapperScan("len.hgy.dao")
public class MicroOrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(MicroOrderApplication.class, args);
    }

}

3、服务消费方(eureka 客户端: netflix-eureka-client)

pom 和属性配置文件基本上差不多,消费要负责调用服务提供方,所以需要调用客户端

依赖

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

bootstrap.properties

#是否注册到eureka
eureka.client.registerWithEureka=true
#是否从eureka中拉取注册信息
eureka.client.fetchRegistry=true
eureka.client.serviceUrl.defaultZone=http://localhost:8763/eureka/

启动类

package len.hgy;

import len.hgy.service.feign.StudentService;
import len.hgy.service.feign.TeacherServiceFeign;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication(scanBasePackages = {"len.hgy"})
//注册到eureka
@EnableEurekaClient
//开启断路器功能
//@EnableCircuitBreaker
//开启feign支持,clients指定哪个类开启feign
//@EnableFeignClients(clients = {StudentService.class,TeacherServiceFeign.class})
public class MicroWebApplication {

    @Bean
    // 负载均衡注解
    @LoadBalanced
    RestTemplate restTemplate() {
        return new RestTemplate(); // 使用这个bean实例才能负载均衡
    }

    public static void main(String[] args) {
        SpringApplication.run(MicroWebApplication.class,args);
    }
}

服务调用的时候就根据服务提供方的服务名称来调用的

public static String SERVIER_NAME = "micro-order";

@Override
public List<ConsultContent> queryContents() {
    s.incrementAndGet();
    List<ConsultContent> results = restTemplate.getForObject("http://"                                                             + SERVIER_NAME + "/user/queryContent", List.class);
    return results;
}

服务提供方的名称,再加上服务提供方的接口名就可以完成调用了。 服务提供方和服务消费启动的时候都会往服务注册中心注册服务,eureka 服务端也可以通过 界面查看到服务注册情况:

http://localhost:8763/

微服务搭建一般有哪些模块 微服务工程搭建_java_05

调用web接口测试看看

// controller
package len.hgy.controller;

import len.hgy.bean.ConsultContent;
import len.hgy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping("/queryUser")
    public List<ConsultContent> queryUser() {
        return userService.queryContents();
    }

    @RequestMapping("/queryMonitor")
    public String queryMonitor() {
        return userService.queryMonitor();
    }
}

http://localhost:8083/user/queryUser

3. Eureka 用户认证

连接到 eureka 的时候需要带上连接的用户名和密码

Eureka 服务端改造

添加security启动器

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

关闭 csrf 验证

package hgy.security;

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
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭csrf
        http.csrf().disable();
        //开启认证:URL格式登录必须是httpBasic
        http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
    }
}

application.properties 配置

# 开启 basic 校验,设置登录用户名密码
security.basic.enabled=true
=admin
spring.security.user.password=admin

Eureka 客户端改造

跟 eureka 连接的时候要带上用户名密码

#eureka.client.serviceUrl.defaultZone=http://localhost:9876/eureka/
eureka.client.serviceUrl.defaultZone=http://admin:admin@localhost:9876/eureka/

此时登录页需要输入用户密码

http://localhost:9876/

微服务搭建一般有哪些模块 微服务工程搭建_微服务搭建一般有哪些模块_06

4. 服务续约保活

当客户端启动想 eureka 注册了本身服务列表后,需要隔段时间发送一次心跳给 eureka 服务 端来证明自己还活着,当 eureka 收到这个心跳请求后才会知道客户端还活着,才会维护该 客户端的服务列表信息。一旦因为某些原因导致客户端没有按时发送心跳给 eureka 服务端, 这时候 eureka 可能会认为你这个客户端已经挂了,它就有可能把该服务从服务列表中删除 掉。

有关续约保活的配置

客户端配置

# 客户端配置
#服务续约,心跳的时间间隔 
eureka.instance.lease-renewal-interval-in-seconds=30
#如果从前一次发送心跳时间起,90 秒没接受到新的心跳,将剔除服务 
eureka.instance.lease-expiration-duration-in-seconds=90
#表示 eureka client 间隔多久去拉取服务注册信息,默认为 30 秒 
eureka.client.registry-fetch-interval-seconds=30

服务端配置

# 服务端配置
#自我保护模式,当出现出现网络分区、eureka 在短时间内丢失过多客户端时, 会进入自我保护模式,即一个服务长时间没有发送心跳,eureka 也不会将其删 除,默认为 true
eureka.server.enable-self-preservation=true
#Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低 于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来
eureka.server.renewal-percent-threshold=0.85
#eureka server 清理无效节点的时间间隔,默认 60000 毫秒,即 60 秒 
eureka.server.eviction-interval-timer-in-ms=60000

5. Eureka 健康检测

Eureka 默认的健康检测只是你校验服务连接是否是 UP 还是 DOWN 的,然后客户端只会调用 状态为 UP 状态的服务,但是有的情况下,虽然服务连接是好的,但是有可能这个服务的某 些接口不是正常的,可能由于需要连接 Redis,mongodb 或者 DB 有问题导致接口调用失败, 所以理论上服务虽然能够正常调用,但是它不是一个健康的服务。所以我们就有必要对这种 情况做自定义健康检测。

application.properties 配置

开启健康检测

#健康检测
eureka.client.healthcheck.enabled=true

自定义健康检测代码

@Configuration
public class MicroWebHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        //这个状态就是数据库是否连接OK
        if(UserController.canVisitDb) { // 业务标识, 更具db,redis, mysql等的连接情况得出一个标识
            return new Health.Builder(Status.UP).build();
        } else {
            return new Health.Builder(Status.DOWN).build();
        }
    }
}

jar

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

6. 服务下线

比如有些情况是服务主机意外宕机了,也就意味着服务没办法给 eureka 心跳信息了,但是 eureka 在没有接受到心跳的情况下依赖维护该服务 90s,在这 90s 之内可能会有客户端调用 到该服务,这就可能会导致调用失败。所以我们必须要有一个机制能手动的立马把宕机的服 务从 eureka 服务列表中清除掉,避免被服务调用方调用到。

调用服务下线的接口: 这个接口是调用 eureka 服务端的接口

http://localhost:9876/eureka/apps/micro-order/localhost:micro-order:8765

delete请求

微服务搭建一般有哪些模块 微服务工程搭建_微服务搭建一般有哪些模块_07

注意:

测试时候如果服务没有关闭, 会一直有心跳, 导致手动下线失败

另外手动停止idea的服务会直接移除掉服务, 不会保活90s, 因为是正常停机, 客户端自动服务下线了

可以通过如下命令自己模拟异常宕机

windows

netstat -nao | findstr 8765
taskkill /f /pid  19016 # 此时eureka的服务列表中还是有服务, 到保活时间过去, eureka也会将其删除掉

linux

ps -aux | grep 8765
kill -9 19016

暂停服务接口

PUT http://localhost:9876/eureka/apps/micro-order/localhost:micro-order:8765?value=OUT_OF_SERVICE

上线服务

PUT http://localhost:9876/eureka/apps/micro-order/localhost:micro-order:8765?value=UP

7. Eureka 高可用

Eureka 热备份的架构图如下:

微服务搭建一般有哪些模块 微服务工程搭建_java_08

整个微服务中存在多个 eureka 服务,每个 eureka 服务都是相互复制的,会把客户端注册进 来的服务复制到 eureka 集群中的其他节点里面来。其实简单来说就是 eureka 每个节点相互 复制。

具体配置如下:

端口为 9878 的 eureka 服务端把自己注册到 9877 的 eureka 服务端

# 多个使用逗号分隔
server.port=9877
eureka.client.serviceUrl.defaultZone=http://localhost:9878/eureka/

端口为 9877 的 eureka 服务端把自己注册到 9878 的 eureka 服务端

# 多个使用逗号分隔
server.port=9878
eureka.client.serviceUrl.defaultZone=http://localhost:9877/eureka/

配置文件

application-9877.properties

application-9878.properties

启动的时候按照指定配置文件启动

java -jar netflix-eureka-server-1.0-SNAPSHOT.jar --spring.profiles.active=9877 --server.port=9877

java -jar netflix-eureka-server-1.0-SNAPSHOT.jar --spring.profiles.active=9878 --server.port=9878

配置就是这样的了

eureka.client.serviceUrl.defaultZone=http://admin:admin@localhost:9877/eureka/