微服务架构原理及特征 | 青训营

57 阅读7分钟

系统架构演变

为什么系统架构需要演进?

  1. 互联网的爆炸性发展
  2. 硬件设施的快速发展
  3. 需求复杂性的多样化
  4. 开发人员的急剧增加
  5. 计算机理论及技术的发展

单体架构

垂直应用架构

分布式架构

SOA架构

微服务架构

核心要素:服务治理(服务注册、负载均衡等)、可观测性(日志分析、异常报警等)、安全(身份认证、访问令牌、传输加密等)。


微服务架构的原理及特征

基本概念

  1. 服务: 一组具有相同逻辑的运行实体
  2. 实例: 一个服务中,每个实体即为一个实例
  3. 单体服务,一般是简单的函数调用,发送http协议
  4. 微服务间的通信是网络传输, 多数采用RPC(Thrift, gRPC)

Eureka注册中心

  • EurekaServer:服务端,注册中心
    • 记录服务信息
    • 心跳监控
  • EurekaClient:客户端
    • Provider服务提供者。例如案例中的 user-service
      • 注册自己的信息到 EurekaServer
      • 每隔 30 秒向 EurekaServer 发送心跳
    • consumer服务消费者。例如案例中的 order-service
      • 根据服务名称从 EurekaServer 拉取服务列表
      • 基于服务列表做负载均衡,选中一个微服务后发起远程调用

问题 1:order-service 如何得知 user-service 实例地址?

user-service 服务实例启动后,将自己的信息注册到 eureka-server(Eureka 服务端)。即服务注册

eureka-server 保存服务名称到服务实例地址列表的映射关系

order-service 根据服务名称,拉取实例地址列表。这个叫服务发现或服务拉取。

问题 2:order-service 如何从多个 user-service 实例中选择具体的实例?

order-service 从实例列表中利用负载均衡算法选中一个实例地址

向该实例地址发起远程调用

问题 3:order-service 如何得知某个 user-service 实例是否依然健康,是不是已经宕机?

user-service 会每隔一段时间(默认 30 秒)向 eureka-server 发起请求,报告自己状态,称为心跳

当超过一定时间没有发送心跳时,eureka-server 会认为微服务实例故障,将该实例从服务列表中剔除

order-service 拉取服务时,就能将故障实例排除了

Nacos 注册中心

startup.cmd -m standalone

  • Nacos 与 eureka 的共同点
    • 都支持服务注册和服务拉取
    • 都支持服务提供者心跳方式做健康检测

Nacos 与 Eureka 的区别

  • Nacos 支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
  • 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
  • Nacos 支持服务列表变更的消息推送模式,服务列表更新更及时
  • Nacos 集群默认采用 AP 方式;当集群中存在非临时实例时,采用 CP 模式。而 Eureka 集群采用 AP 方式

核心服务治理功能

服务发布

  1. 概念: 让一个服务升级运行新的代码的过程
  2. 服务发布难点
  • 服务不可用
  • 服务抖动
  • 服务回滚
  1. 蓝绿部署
  • 将服务分成两个部分,分别先后发布
  • 简单、稳定
  • 但需要两倍资源(通常在服务器人数少的时候使用此方式)
  1. 灰度发布(金丝雀发布)
  • 先发布少部分实例,接着逐步增加发布比例
  • 不需要增加资源
  • 回滚难度大,基础设施要求高

流量治理

  1. 流量控制
  • 在微服务架构中,可以从各个维度对端到端的流量在链路上进行精确控制
  1. 控制维度
  • 地区维度
  • 集群维度
  • 实例维度
  • 请求维度

负载均衡

常见的LB策略

  • Round Robin
  • Ring Hash
  • Random

Ribbon 负载均衡

负载均衡的实现(以Java项目举例)

基本流程如下:

    1. 拦截 RestTemplate 请求 http://user-service/user/1
    2. RibbonLoadBalancerClient 会从请求 url 中获取服务名称,也就是 user-service
    3. DynamicServerListLoadBalancer 根据 user-service 到 eureka 拉取服务列表
    4. eureka 返回列表,localhost:8081、localhost:8082
    5. IRule 利用内置负载均衡规则,从列表中选择一个,例如 localhost:8081
    6. RibbonLoadBalancerClient 修改请求地址,用 localhost:8081 替代 userservice,得到 http://localhost:8081/user/1,发起真实请求

负载均衡策略

1.默认的实现就是 ZoneAvoidanceRule,是一种轮询方案

2.RandomRule 随机选择一个可用的服务器

自定义负载均衡策略

通过定义 IRule 实现可以修改负载均衡规则,有两种方式:代码的方式和配置文件的方式。

/**
 * 自定义负载均衡规则:此处设置为随机
 *
 * @return
 */
@Bean
public IRule randomRule() {
    return new RandomRule();
}
// 构建一个随机负载均衡算法
type RandomRule struct {
    
}

// 实现IRule接口的Choose方法
func (r *RandomRule) Choose(servers []*Server) *Server {
    if len(servers) == 0 {
        return nil
    }
    // 生成随机数
    rand.Seed(time.Now().UnixNano())
    index := rand.Intn(len(servers))
    return servers[index]
}

// 创建一个随机负载均衡算法的工厂方法
func NewRandomRule() IRule {
    return &RandomRule{}
}

// main函数中使用随机负载均衡算法
func main() {
    // 创建服务列表
    servers := []*Server{
        {Address: "192.168.0.1", Port: 8080},
        {Address: "192.168.0.2", Port: 8080},
        {Address: "192.168.0.3", Port: 8080},
    }
    
    // 创建负载均衡器
    lb := LoadBalancer{Rule: NewRandomRule()}
    
    // 循环调用Choose方法选择服务器
    for i := 0; i < 10; i++ {
        server := lb.Choose(servers)
        fmt.Printf("第%d次选择的服务器为:%s:%d\n", i+1, server.Address, server.Port)
    }
}
user-service: # 给某个微服务配置负载均衡规则
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则:此处设置为随机
package main

import (
	"fmt"
	"github.com/afex/hystrix-go/hystrix"
	"github.com/afex/hystrix-go/plugins"
	"github.com/afex/hystrix-go/hystrix/metric_collector"
	"github.com/afex/hystrix-go/hystrix/rolling"
)

func main() {
	hystrix.ConfigureCommand("user-service", hystrix.CommandConfig{
		Timeout:                1000,   // 超时时间(单位:毫秒)
		MaxConcurrentRequests:  100,    // 最大并发请求数
		SleepWindow:            5000,   // 隔离策略触发后,在该时间窗口内禁止请求
		RequestVolumeThreshold: 10,     // 触发熔断的最小请求数
		ErrorPercentThreshold:  50,     // 触发熔断的错误百分比阈值
	})

	// 注册自定义的熔断指标收集器,如 PrometheusCollector、StatsdCollector 等
	metricCollector.Registry.Register(plugin.PrometheusCollector{})
	metricRegistry := metricCollector.Registry.(*metricCollector.Registry)

	// 自定义熔断指标的滚动窗口大小、时间跨度、时间刷新间隔
	rolling.StatsPoller.TickDuration = 1
	rolling.StatsPoller.FlushDuration = 2000
	rolling.StatInterval = 2000

	// 输出自定义熔断指标的报告
	go func() {
		for {
			fmt.Println(metricRegistry.Get("user-service", "requests"))
			fmt.Println(metricRegistry.Get("user-service", "successes"))
			fmt.Println(metricRegistry.Get("user-service", "failures"))
			fmt.Println(metricRegistry.Get("user-service", "timeouts"))
			fmt.Println(metricRegistry.Get("user-service", "rejections"))
			fmt.Println(metricRegistry.Get("user-service", "semaphore_rejected"))
			fmt.Println(metricRegistry.Get("user-service", "short_circuited"))
			fmt.Println(metricRegistry.Get("user-service", "fallback_successes"))
			fmt.Println(metricRegistry.Get("user-service", "fallback_failures"))
			fmt.Println(metricRegistry.Get("user-service", "total_duration"))
		}
	}()

	// 执行用户服务请求
	for i := 0; i < 100; i++ {
		shystrix.Go("user-service", func() error {
			// 这里可以是用户服务的具体调用代码
			// ...
			return nil
		}, nil)
	}
}

这段代码使用了 afex/hystrix-go 库来实现断路器(Circuit Breaker)的功能。断路器是用于提高系统稳定性和可用性的一种设计模式,当服务出现故障或者响应过慢时,断路器会中断该服务的调用,防止故障的传播,从而保护系统。

稳定性治理

  1. 限流 : 限制服务处理的最大 QPS,拒绝过多请求
  2. 熔断 : 中断请求路径,增加冷却时间从而让故障实例尝试恢复
  3. 过载保护 : 在负载高的实例中,主动拒绝一部分请求,防止实例被打挂
  4. 降级 : 服务处理能力不足时,拒绝低级别的请求,只响应线上高优请求

小结

微服务中还有许多内容需要更详细的了解,比如Redis,MQ, Docker等一系列中间件。微服务架构适用于大型应用程序和复杂业务场景,因为它可以提高开发效率、可维护性强和扩展性强等一系列优点。但是,微服务架构也有一些挑战,比如,微服务架构将应用程序拆分为多个独立的微服务,每个微服务都需要进行单独的部署、监控和管理。这增加了系统的复杂性和管理的难度,可能需要更多的开发人员和运维人员来维护和管理整个系统。微服务通常都有自己的数据库,这也使得在进行数据一致性和事务管理时变得更加困难。因此,在采用微服务架构之前,需要仔细考虑其优缺点。