系统架构迭代过程
随着互联网的发展,网站应用的规模不断扩大,需求的激增,带来了系统架构的升级、迭代。经历了
- 单一应用
- 垂直拆分
- 分布式服务
- SOA
- 微服务
集中式架构
- 代码耦合,开发维护困难
- 无法针对不同模块进行针对性优化
- 水平扩展困难
- 单点容错低,并发能力差
垂直拆分
优点:
- 能够在一定程度上解决并发问题
- 可以针对不同模块进行优化
- 方便水平扩展,负载均衡,容错率提高
缺点: - 系统间相互独立,开发工作重复
分布式服务
当垂直应用越来越多时,应用之间的交互是不可避免的,将核心业务抽取出来,作为独立的服务,形成服务中心。
优点:
- 将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率
缺点: - 系统间耦合度变高。调用关系复杂,维护难度增加
服务治理架构SOA
SOA:面向服务的架构
服务越来越多,小服务的资源浪费问题等逐渐显现,此时需要增加调度中心基于访问压力实时管理集群容量
微服务
SOA架构翻译就是面向服务,微服务,也是服务,都是对系统进行拆分
微服务的特点
- 单一职责:微服务中每一个服务都对应唯一的业务能力,单一职责
- 微:微服务的服务拆分粒度很小
- 面向服务:面向服务就是说每个服务都对外暴露Rest风格服务的API不太关心服务的技术实现。
- 自治:
- 团队独立
- 技术独立
- 前后端分离
- 数据库分离
-
部署独立
服务调用方式
RPC和HTTP
- RPC:远程过程调用,自定义的数据格式,基于原生TCP通信,速度快、效率高,比如早期的webservice现在的dubbo,都是RPC的代表
- Http:http其实是一种网络传输协议,基于TCP
选型问题
- 如果采用Java技术栈,Dubbo作为微服务架构架构师不错的选择
- 如果技术栈多样化,使用SpringCloud搭建微服务是很好的选择
Http客户端工具
因为微服务选择了Http协议,所以就需要http客户端工具
- HttpClient
- OKHttp
- URLConnection
Spring的RestTemplate
Spring提供了一个RestTemplate模板工具类,对基于Http的客户端进行了封装,并且实现了对象与json的序列化和反序列化。RestTemplate并没有限定Http的客户端类型,而是进行了抽象,目前常用的3种都有支持:
- HttpClient
- OkHttp
- JDK原生的URLConnection(默认的)
SpringCloud
微服务是一种架构方式,而实现微服务架构的方式最火就是SpringCloud
SpringCloud优势
- 背景:作为Spring家族,整个Spring背景
- 技术:强大的技术团队
- 基础:广泛的用户基础
- 便捷:配置少、开发方便
简介
SpringCloud是Spring旗下的项目
SpringCloud擅长集成,将现在流行的技术整合到一起,SpringCloud有6大组件
问题 | 组件 |
---|---|
ip和端口都写死不好,需要登记 | Eureka(服务注册中心) |
高可用,服务需要集群,如何访问? | Ribbon(负载均衡) |
某个服务故障,调用这个服务的所有服务都超时,故障蔓延 | Hystrix(断路器) |
服务之间调用每次都拼url | Feign(服务调用) |
客户端访问每个服务都进行安全认证,重复 | Zuul(服务网关) |
每个服务都有配置文件,配置文件太多 | Spring Config(配置中心) |
版本
SpringCloud的版本命名比较特殊,因为SpringCloud不是一个组件,是许多的组件的集合,命名是从A到Z的首字母的一些单词
SpringCloud使用
首先,我们使用SpringCloud需要先模拟一个场景
创建父工程cloud-demo
添加依赖
org.springframework.boot
spring-boot-starter-parent
2.1.3.RELEASE
UTF-8
UTF-
8
1.8
Greenwich.SR1
2.1.4
5.1.47
1.2.5
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
tk.mybatis
mapper-spring-boot-starter
${mapper.starter.version}
mysql
mysql-connector-java
${mysql.version}
org.projectlombok
lombok
org.springframework.boot
spring-boot-maven-plugin
创建服务提供者
- 创建module user-service 父工程选择cloud-demo
- 添加依赖
- pom.xml
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
tk.mybatis
mapper-spring-boot-starter
编写配置文件
application.yml
server:
port: 8088
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud
username: root
password: xml123xml
driver-class-name: com.mysql.jdbc.Driver
启动类
- UserApplication.java
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
实体类
- User.java
@Data
@Table(name = "tb_user")
public class User {
@Id
private Long id;
private String userName; // 用户名
private String password; // 密码
private String name;// 姓名
private Integer age;// 年龄
private Integer sex;// 性别,1男性,2女性
private Date birthday;// 出生日期
private Date created;// 创建时间
private Date updated;// 更新时间
private String note;// 备注
}
- mapper
import com.baomidou.mybatisplus.mapper.BaseMapper;
import com.probuing.user.pojo.User;
/**
* @author wangxin
* @date 2020/1/15 12:15
* @description: TODO
* GOOD LUCK!
*/
public interface UserMapper extends BaseMapper {
}
Controller
- UserController.java
package com.probuing.user.controller;
import com.probuing.user.pojo.User;
import com.probuing.user.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author wangxin
* @date 2020/1/15 12:17
* @description: TODO
* GOOD LUCK!
*/
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserServiceImpl service;
@RequestMapping("/{id}")
public User queryByUserId(@PathVariable("id") Long id) {
return service.findById(id);
}
}
服务调用者
创建服务调用者工程
创建一个模块,调用user-service
- 添加依赖
org.springframework.boot
spring-boot-starter
2.2.2.RELEASE
- 创建启动类 注册RestTemplate
package com.probuing.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
/**
* @author wangxin
* @date 2020/1/15 15:33
* @description: TODO
* GOOD LUCK!
*/
@SpringBootApplication
public class ConsumerServiceRunner {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerServiceRunner.class, args);
}
}
然后我们先启动UserService然后再启动Consumer
在浏览器中访问调用者url:
http://localhost:8081/consumer/1
在这段过程中:
我们的调用服务方式存在一些问题
- 在consumer中,我们把url地址硬编码到代码中,不便于维护
- 在consumer中,我们需要写死user-service地址,如果地址出现变更,可能会导致服务失效
- consumer中不清楚user-service的状态
- user-service只有1台服务,不具备高可用
通过上面我们可以扩展出分布式服务必然要面临的问题: - 服务管理
- 如何自动注册和发现
- 如何实现状态监管
- 如何实现动态路由
- 服务实现负载均衡
- 服务如何解决容灾问题
- 服务统一配置
上面的这些问题,都可以在SpringCloud中找到答案
Eureka注册中心
Eureka是Netflix公司的产品
认识Eureka
Eureka可以记录服务提供者的地址,可以维护服务提供者所提供的信息,服务调用者无需自己寻找服务,而是将服务调用者的需求高速Eureka然后Eureka会将符合的服务高速你。
同时服务提供方与Eureka通过心跳机制进行监控,当服务提供方出现问题时,Eureka可以将它从服务列表中删除
入门案例
编写EurekaServer
创建一个模块 eureka-server 创建服务端
- 引入依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
2.1.2.RELEASE
- 编写启动类
EurekaServer.java
package com.probuing.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author wangxin
* @date 2020/1/15 17:16
* @description: TODO
* GOOD LUCK!
*/
@SpringBootApplication
@EnableEurekaServer
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class, args);
}
}
- 启动类中要添加@EnableEurekaServer开启EurekaServer
- 编写配置
- application.yml
server:
port: 10086
spring:
application:
name: eureka-server #应用名称:在Eureka中的作为服务的id标识
eureka:
client:
service-url: #EurekaServer的地址
defaultZone: http://localhost:10086/eureka
register-with-eureka: false #不注册自己
fetch-registry: false # 不拉取服务
- 启动服务,并访问 http://localhost:10086/
注册服务
在服务提供者上添加Eureka的客户端依赖
在user-service中添加依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
- 在启动类上开启Eureka客户端功能
package com.probuing.user;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @author wangxin
* @date 2020/1/14 20:56
* @description: TODO
* GOOD LUCK!
*/
@SpringBootApplication
@MapperScan("com.probuing.user.mapper")
@EnableEurekaClient
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
- 编写配置
server:
port: 8088
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud
username: root
password: xml123xml
driver-class-name: com.mysql.jdbc.Driver
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
启动项目后,可以看到服务已经注册
服务发现
我们修改user-consumer 从EurekaServer获取服务
需要在项目中添加EurekaClient依赖,就可以通过服务名称来获取信息了
- 添加依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
2.1.2.RELEASE
- 修改启动类开启Eureka客户端
package com.probuing.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
/**
* @author wangxin
* @date 2020/1/15 15:33
* @description: TODO
* GOOD LUCK!
*/
@SpringBootApplication
@EnableEurekaClient
public class ConsumerServiceRunner {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerServiceRunner.class, args);
}
}
- 修改配置
server:
port: 8088
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud
username: root
password: xml123xml
driver-class-name: com.mysql.jdbc.Driver
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
- 修改启动类代码 使用DiscoveryClient来获取服务
package com.probuing.consumer.controller;
import com.probuing.consumer.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* @author wangxin
* @date 2020/1/15 16:24
* @description: TODO
* GOOD LUCK!
*/
@RestController
@RequestMapping("consumer")
public class ConsumerController {
//注册restTemplete
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
public User findById(@PathVariable("id") Long id) {
List list = discoveryClient.getInstances("user-service");
ServiceInstance instance = list.get(0);
User user = restTemplate.getForObject(instance.getUri() + "/user/" + id, User.class);
return user;
}
}
- 运行启动项目,运行 http://localhost:8081/consumer/1,可以看到服务被正常调用
Eureka详解
基础架构
Eureka架构中的三个核心角色:
- 服务注册中心
Eureka的服务端应用,提供服务注册和发现功能 - 服务提供者
提供服务的应用,可以使Springboot应用,也可以使其他的任意技术实现,需要对外提供的是Rest分割的服务,上述例子中的user-service - 服务消费者
消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方
高可用的Eureka Server
我们可以创建Eureka集群,多个的Eureka Server之间会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现高可用集群
。这样的架构中,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取完整的服务列表
搭建高可用EurekaServer
所谓高可用注册中心,其实就是把EurekaServer自己也注册为服务,注册到其他EurekaServer上,这样多个EurekaServer之间可以互相发现。
- 修改案例中的EurekaServer配置
server:
port: 10086
spring:
application:
name: eureka-server #应用名称:在Eureka中的作为服务的id标识
eureka:
client:
service-url: #EurekaServer的地址
# 配置其他EurekaServer的地址
defaultZone: http://localhost:10087/eureka
# register-with-eureka: false #不注册自己
fetch-registry: false # 不拉取服务
-
配置另一个EurekaServer的启动项
-
运行新的EurekaServer
客户端注册服务到集群
由于现在的EurekaServer已经不止一个,因此注册服务提供的时候,service-url参数需要添加多个EurekaServer地址,多个地址以','隔开
server:
port: 8088
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud
username: root
password: xml123xml
driver-class-name: com.mysql.jdbc.Driver
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka,http://localhost:10087/eureka
Eureka客户端
服务注册
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作
服务提供者在启动时,会检测配置属性中的 eureka.client.register-with-erueka=true参数是否正确,默认为true,如果为true,则会向EurekaServer发起Rest请求,并携带自己的元数据信息,EurekaServer会把这些信息保存到一个双层Map结构中
- 第一层Map的key就是服务id,一般是spring.application.name属性
- 第二层map的key是服务的实例id,一般host+serviceId+port
- 值是服务实例对象
user-service默认注册时使用的是主机名。如果我们想用ip注册,可以在user-service的application.yml添加配置
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
instance:
ip-address: 127.0.0.1 # ip地址
prefer-ip-address: true # 更倾向于使用ip,而不是host名
instance-id: ${eureka.instance.ip-address}:${server.port} # 自定义实例的id
服务续约
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:我还活着,我们称为服务的续约
eureka:
instance:
lease-renewal-interval-in-seconds: 30
lease-expiration-duration-in-seconds: 90
- lease-renewal-interval-in-seconds: 30 服务续约的间隔,默认30秒
- lease-expiration-duration-in-seconds: 90 服务失效时间 默认90秒
也就表示默认情况下每隔30秒服务会向注册中心发送一次心跳。证明自己还存活着。如果超过90秒没有发送心跳 EurekaServer就会认为服务宕机。会将服务从服务列表中移除
服务下线、失效剔除和自我保护
服务下线
当服务进行正常关闭操作时,会触发一个任务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求后,将该服务设置为下线状态
失效剔除
有时我们的服务可能由于内存溢出或网络故障等原因使得服务不能正常的工作,而服务注册中心并未收到“服务下线”的请求。相对于服务提供者的“服务续约”操作,服务注册中心在启动时会创建一个定时任务,默认每隔一段时间,将当前清单中超时(默认90秒)没有续约的服务剔除,这个操作就是失效剔除
可以通过eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,单位是毫秒
自我保护
我们关停一个服务,就会在Eureka面板看到一条警告:
这是触发了Eureka的自我保护机制,当服务未按时进行心跳续约时,Eureka会统计服务实例最近15分钟心跳续约的比例是否低于了85%,我们可以通过下面的设置来关停自我保护
server:
enable-self-preservation: false