微服务二:服务发现与多租户


上篇文章我们讲到微服务的定义,优缺点,对外暴露等,服务除了对外暴露之外,服务之间还需要相互进行调用,不同的服务之间通过什么样的协议进行交互,服务发现如何实现,如何保证服务的平滑发布与重启,测试环境的问题如何解决等。

一:服务间通信方式: gRPC

为什么采用 gRPC?

  • 多语言:语言中立,支持多种语言。
  • 轻量级、高性能:序列化支持 PB(Protocol Buffer)JSONPB 是一种语言无关的高性能序列化框架。
  • 可插拔
  • IDL:基于文件定义服务,通过proto3工具生成指定语言的数据结构、服务端接口以及客户端 Stub
  • 移动端:基于标准的 HTTP2 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量。
  • 服务而非对象、消息而非引用:促进微服务的系统间粗粒度消息交互设计理念。
  • 负载无关的:不同的服务需要使用不同的消息类型和编码,例如 protocol buffers、JSON、XML 和 Thrift
  • 流:Streaming API
  • 阻塞式和非阻塞式:支持异步和同步处理在客户端和服务端间交互的消息序列。
  • 元数据交换:常见的横切关注点,如认证或跟踪,依赖数据交换。
  • 标准化状态码:客户端通常以有限的方式响应 API 调用返回的错误。

为什么不使用 restful

  • 每个客户端都需要单独写 SDK,复杂麻烦
  • 需要单独写文档,常常会因为代码更新了但是文档没更新陷入坑中
  • 性能不太好,json 传递相对于 pb 更耗流量,性能更低
  • http1.1 是一个单连接的请求,在内部网络环境,使用 http 比较浪费
  • restful是一个松散约束的协议,非常灵活,每个人,每个团队出来的代码都不太一样,比较容易出错

一般前后端交互使用restful,而后端各服务内部使用RPC

二:服务优雅启动(注册)与服务退出(注销)

优雅启动(服务注册思路)

  1. Provider 启动,k8s 中的启动脚本会定时去检查服务的健康检查接口
  2. 健康检查通过之后,服务注册脚本向注册中心注册服务(rpc://ip:port)
  3. 消费者定时从服务注册中心获取服务方地址信息
  4. 获取成功后,会定时的向服务方发起健康检查,健康检查通过后才会向这个地址发起请求,在运行过程中如果健康检查出现问题,会从消费者本地的负载均衡中移除
    在这里插入图片描述

优雅退出(服务注销思路)

  1. 触发下线操作: 首先用户在发布平台点击发版/下线按钮
  2. 发布部署平台向注册中心发起服务注销请求,在注册中心下线服务的这个节点,这里在发布部署平台实现有个好处,不用每个应用都去实现一遍相同的逻辑,在应用受到退出信号之后由应用主动发起注销操作也是可以的
    2.1 注册中心下线应用之后,消费者会获取到服务注销的事件
    2.2 然后将服务方的节点从本地负载均衡当中移除,注意这一步操作会有一段时间,下面的第4步并不是这一步结束了才开始
  3. 发布部署平台向应用发送 SIGTERM 信号,应用捕获到之后执行将健康检查接口设置为不健康,返回错误
    这个时候如果消费者还在调用应用程序,调用健康检查接口发现无法通过,也会将服务节点从本地负载均衡当中移除
    调用 grpc/httpshutdown 接口,并且传递超时时间,等待连接全部关闭后退出,这个超时时间一般为 2 个心跳周期
  4. 发布部署平台如果发现应用程序长时间没有完成退出,发送 SIGKILL 强制退出应用,这个超时时间根据应用进行设置一般为 10 - 60s
    在这里插入图片描述
    优雅启动和关闭简要代码
package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"
)

// 模拟慢请求
func sleep(ctx *gin.Context) {
	t := ctx.Query("t")
	s, err := strconv.Atoi(t)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"msg": "参数错误: " + t})
		return
	}

	time.Sleep(time.Duration(s) * time.Second)
	ctx.JSON(http.StatusOK, gin.H{"msg": fmt.Sprintf("sleep %d s", s)})
}


const (
	stateHealth   = "health"
	stateUnHealth = "unhealth"
)

var state = stateHealth

func health(ctx *gin.Context) {
	status := http.StatusOK
	if state == stateUnHealth {
		status = http.StatusServiceUnavailable
	}
	ctx.JSON(status, gin.H{"data": state})
}


func main() {
	e := gin.Default()
	e.GET("/health", health)
	e.GET("/sleep", sleep)

	server := &http.Server{
		Addr:    ":8080",
		Handler: e,
	}

	go func() {
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("server run err: %+v", err)
		}
	}()

	// 用于捕获退出信号
	quit := make(chan os.Signal)

	// kill (no param) default send syscall.SIGTERM
	// kill -2 is syscall.SIGINT
	// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	log.Println("Shutting down server...")

	// 捕获到退出信号之后将健康检查状态设置为 unhealth
	state = stateUnHealth
	log.Println("Shutting down state: ", state)

	// 设置超时时间,两个心跳周期,假设一次心跳 3s
	ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
	defer cancel()

	// Shutdown 接口,如果没有新的连接了就会释放,传入超时 context
	// 调用这个接口会关闭服务,但是不会中断活动连接
	// 首先会将端口监听移除
	// 然后会关闭所有的空闲连接
	// 然后等待活动的连接变为空闲后关闭
	// 如果等待时间超过了传入的 context 的超时时间,就会强制退出
	// 调用这个接口 server 监听端口会返回 ErrServerClosed 错误
	// 注意,这个接口不会关闭和等待websocket这种被劫持的链接,如果做一些处理。可以使用 RegisterOnShutdown 注册一些清理的方法
	if err := server.Shutdown(ctx); err != nil {
		log.Fatal("Server forced to shutdown:", err)
	}

	log.Println("Server exiting")
}

测试时,可以调用sleep,传一个长一点的时间,然后kill -2结束服务,但是服务还是会等到sleep相应完成才退出。

可以参考另外两篇博客:18. Go实现Gin服务优雅关机 以及 14. Go实现简易分布式注册中心

三:注册中心要点

CP、CA、还是 AP
实际场景是海量服务发现和注册,服务状态可以弱一致, 需要的是 AP 系统,只需最终一致性即可

  • 注册的事件延迟
    高可用的服务在这方面问题不大

  • 注销的事件延迟
    因为有上文提到的健康检查的机制,即使注销延迟,客户端也会主动的将节点移除

  • 服务注册
    注册:服务方启动后向注册中心任意一个节点发送注册请求,然后这个节点会向其他节点进行广播同步
    心跳:注册后定期(30s)向注册中心发送心跳,或注册中心向服务发起心跳检测
    下线:下线时向注册中心发送下线请求
    注意:注册中心节点启动时需要加载缓存进行预热,所以不建议这个时候服务进行重启或者是发版

  • 服务发现
    消费者定期向注册中心长轮询获取节点信息,获取到之后缓存到本地

  • 网络故障
    服务方与注册中心:注册中心会定期(60s)检测已失效(90s 未更新)的实例,失效之后就会移除,但是如果短时间内丢失大量心跳连接,(15min 内心跳低于期望值的 85%)就会开启自我保护模式,保留过期的服务不会进行删除
    注册中心与消费者:消费者本地有缓存,问题不大
    服务方与消费者 :有健康检查,健康检查不通过时,会从消费者本地负载均衡中移除

  • 注册中心故障
    1.不建议这个时候服务进行重启或者是发版,因为这个时候注册不上,会导致服务不可用,不发版短时间没有影响
    2.如果注册中心节点全部挂掉,启动时必须要等两到三个心跳周期,等所有的服务都注册上之后再开始提供服务运行,让消费者拉取数据
    3.如果挂掉一个注册中心节点,需要等其他的节点将信息同步到本机之后再提供服务
    4.数据同步时会对比时间戳,会保证当前节点的数据是最新的

四:多集群

注意这里的多集群都是单个机房内的,多个机房的一般就是异地多活了

多集群需求从何而来?
对于类似账号服务的L0级别的服务,几乎所有的服务都有依赖,需要尽可能的提高服务的可用性

  • 从单一集群考虑,多个节点保证可用性,我们通常使用 N+2 的方式来冗余节点。N 一般通过压测得出
  • 从单一集群故障带来的影响面角度考虑冗余多套集群。例如依赖的 redis 出现问题,整个集群挂掉了,还可以访问其他集群
  • 多机房部署,如果在云上可能是多个可用区

什么是多集群?
给某个服务部署多套,每一套都拥有独立的缓存,物理上相当于有多套资源,逻辑上划分为不同的集群,在服务注册的时候向注册中心注册的时候携带相关的集群标签

五:多租户

简介

随着互联网的发展,越来越多的企业开始向多租户的方向转型,提高竞争力。微服务架构中允许同一个系统多套代码共存,这一般被称为多租户(multi-tenancy)。多租户系统允许多个租户共享同一套应用程序和基础设施,每个租户都拥有自己的数据和隐私保护。

租户可以是测试,金丝雀发布,影子系统(shadow systems),甚至服务层或者产品线,使用租户能够保证代码的隔离性并且能够基于流量租户做路由决策。

染色发布
在这里插入图片描述

可以把待测试的服务 B 在一个隔离的沙盒环境中启动一个B',并且在沙盒环境下可以访问集成环境(UAT)CD

把测试流量路由到服务 B',同时保持生产流量正常流入到集成服务(如在请求中带上泳道环境标识,请求便会优先路由到对应的泳道上去,链路上的任意服务部署了该泳道下的实例,则请求该泳道下的实例,若没有部署该泳道下的实例,则路由到基准环境(集成环境)下的实例

服务 B' 仅仅处理测试流量而不处理生产流量。

生产中的测试提出了两个基本要求,它们也构成了多租户体系结构的基础:

  • 流量路由:能够基于流入栈中的流量类型做路由(如POE(Product Offline Environment)PPE(Product Preview Environment)以及是否带泳道)

  • 隔离性:能够可靠地隔离测试和生产中的资源,这样可以保证对于关键业务微服务没有副作用。

为了实现多租户系统,需要考虑多维度的设计,涉及到数据隔离、安全性等问题。

多租户系统设计需求

在多租户系统设计过程中,需要考虑如下需求:

  • 数据隔离:每个租户的数据需要被隔离,不能相互干扰。
  • 安全性:保证每个租户的数据隐私和安全性。
  • 扩展性:系统需要支持横向和纵向扩展。
  • 高可用性:系统需要保证高可用性,不能因为某个租户的问题导致整个系统崩溃。
  • 管理性:系统需要提供方便的管理和维护功能。

多维度的设计方案

为了满足上述需求,需要从多个维度考虑设计方案。

数据隔离

在设计数据隔离方案时,可以采用以下策略:

  • 独立数据库:对于每个租户,分配一个独立的数据库实例,确保数据不会被混淆。
  • 共享数据库但不同表或不同Schema:对于每个租户,不同的租户使用同一个数据库,可以采用不同的schema或者表前缀,确保不同租户之间不会访问到相同的数据。
  • 共享数据库、共享表、共享Schema:在数据表中新增TenantID字段,通过字段进行数据隔离

安全性

为了保证安全性,可以采用以下措施:

为每个租户分配一个唯一的标识符,确保不同租户之间数据不会被误用。
使用加密技术对租户数据进行保护。
采用权限控制机制,保证每个租户只能访问属于自己的数据。

扩展性

为了支持横向和纵向扩展,可以采用以下策略:

  • 采用负载均衡机制,将请求分配到不同的节点上,以支持多节点的扩展。
  • 设计合理的分表策略,以支持大规模数据量的存储。
  • 对于大数据量或者并发量大的租户,可以采用分片或者分块技术,以支持高效的数据处理。

高可用性

为了保证高可用性,可以采用以下措施:

  • 设计合理的系统架构,支持多节点、多副本、多数据中心等机制,防止单点故障。
  • 采用容错机制,保证即使发生故障,也可以继续提供服务。

管理性

为了提高管理和维护效率,可以采用以下策略:

  • 提供简单易用的管理界面,方便管理员进行维护和监控。
  • 提供合理的数据备份和恢复机制,保证数据的安全性和可靠性。
  • 采用自动化部署和配置管理机制,提高系统的可维护性。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
微服务架构中,每个服务都是独立的,需要自己管理自己的数据源。如果需要实现多租户功能,可以在每个服务中引入mybatisplus,通过配置动态数据源实现租户隔离。 1. 引入mybatisplus 在每个服务的pom.xml文件中添加如下依赖: ```xml <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.x.x</version> </dependency> ``` 2. 配置动态数据源 在每个服务中配置动态数据源,可以使用mybatisplus提供的DynamicDataSource类来实现。DynamicDataSource类继承AbstractRoutingDataSource,可以根据不同的租户来切换数据源。 ```java @Configuration public class DataSourceConfig { @Autowired private DataSourceProperties properties; @Bean @ConfigurationProperties("spring.datasource") public DataSource dataSource() { return properties.initializeDataSourceBuilder().build(); } @Bean public DataSource dynamicDataSource(DataSource dataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); // 配置多个数据源 targetDataSources.put(TenantContextHolder.getTenantId(), dataSource); DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(dataSource); return dynamicDataSource; } @Bean public SqlSessionFactory sqlSessionFactory(DataSource dynamicDataSource) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dynamicDataSource); return sqlSessionFactory.getObject(); } } ``` 3. 切换数据源 在每个服务实现租户隔离,可以通过拦截器来实现。拦截器可以拦截每个请求,并根据请求中的租户id来切换数据源。 ```java @Component public class DynamicDataSourceInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String tenantId = request.getHeader("tenantId"); if (StringUtils.isNotBlank(tenantId)) { TenantContextHolder.setTenantId(tenantId); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { TenantContextHolder.clear(); } } ``` 4. 实现多租户功能 在每个服务实现多租户功能,需要在每个实体类中添加租户id字段,并在mybatisplus中配置租户id的自动填充。 ```java @Data @TableName("user") public class User { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer age; @TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime; @TableLogic @TableField(fill = FieldFill.INSERT) private Integer deleted; @TableField(fill = FieldFill.INSERT) private String tenantId; } ``` 在mybatisplus的配置文件中添加租户id的自动填充: ```java @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); TenantLineInnerInterceptor tenantLineInnerInterceptor = new TenantLineInnerInterceptor(); tenantLineInnerInterceptor.setTenantLineHandler(new TenantLineHandler() { @Override public Expression getTenantId() { return new LongValue(TenantContextHolder.getTenantId()); } @Override public String getTenantIdColumn() { return "tenant_id"; } @Override public boolean ignoreTable(String tableName) { return false; } }); interceptor.addInnerInterceptor(tenantLineInnerInterceptor); return interceptor; } } ``` 以上就是整合mybatisplus多租户的步骤,可以根据实际需求进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值