走进微服务架构 | 青训营笔记

35 阅读6分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 7 天

微服务架构介绍

微服务架构概览

微服务架构核心要素

服务治理:

  • 服务注册
  • 服务发现
  • 负载均衡
  • 扩缩容
  • 流量治理
  • 稳定性治理
  • ...

可观测性:

  • 日志采集
  • 日志分析
  • 监控大点
  • 监控大盘
  • 异常报警
  • 链路追踪
  • ...

安全:

  • 身份验证
  • 认证授权
  • 访问令牌
  • 审计
  • 传输加密
  • 黑产攻击
  • ...

微服务架构原理及特征

基本概念

服务(Service):一组具有相同逻辑的运行实体。
实例(Instance):一个服务中,每个运行实体即为一个实例。
集群(Cluster):通常指服务内部的逻辑划分,包含多个实例。
常见的实例承载形式:进程、VM、k8s pod......。
实例与进程的关系:实例与进程之间没有必然对应关系,可以一个实例对应一个进程或多个进程(反之不常见)。
有状态 / 无状态服务:服务的实例是否存储了可持久化的数据(例如磁盘文件)。
服务间通信:
对于单体架构,不同模块通信只是简单的函数调用。对于微服务架构,服务间通信意味着网络传输。

服务注册及发现

在代码层面,如何指定调用一个目标服务的地址(ip:port)

  • 不能在代码中写死地址。
  • 一个服务有多个实例,每个实例都有自己的ip:port

使用 DNS 域名可以解决吗: 一个域名可以注册多个 IP ,可以解决上面的问题,但又有新的问题:

  • 本地 DNS 存在缓存,导致延迟。
  • 负载均衡问题,一个域名注册了多个 IP ,域名解析返回哪个 IP 做不到负载均衡。
  • 不支持服务实例的探活检查,配置域名时可以配置不存在的 IP ,只是访问时才出错。
  • 域名无法配置端口,所以所有不同 IP 的实例都要使用同一个端口。

以类似 DNS 的思想,新增一个统一的服务注册中心,用于存储服务名到服务实例的映射。

服务实例上线和下线的过程:
每个服务实例都会不停地获取注册中心中的映射表,如果要下线某个实例,需要先从注册中心中把该实例删除,这样才不会有流量到该实例,之后再下线实例。
同样地,上线某个实例时,要先启动好该实例,并进行健康检查通过之后,再向注册中心登记注册。这样流量到新实例就没有问题了。

流量特征

  • 统一网关入口
  • 内网通信多数采用RPC

HTTP 是文本协议,PRC 是二进制协议,效率、性能上 RPC 较优。

  • 网状调用链路

核心服务治理功能

服务发布

服务发布(deployment),即让一个服务升级运行新的代码的过程。
服务发布的难点:

  • 服务不可用:因升级而停止服务导致不可用。
  • 服务抖动:因停止服务导致已经进入的流量消失。
  • 服务回滚:升级到新的代码后有问题,先倒回以前的没问题的代码。

解决方案:

  • 蓝绿部署:将服务划分为两个集群:蓝集群和绿集群,升级时先将所有流量导向蓝集群,然后给绿集群升级,绿集群升级完成后再将流量导向绿集群,再升级蓝集群,蓝集群升级完成后整个服务完成升级。

    • 优点:稳定可靠。
    • 缺点:升级时需要一半的机器资源承受住所有流量。
    • 适合在流量低峰期进行升级。
  • 灰度发布(金丝雀发布):先将服务中的一个实例换成新的代码,再逐渐将其他的实例一个个都换成新的代码。

    • 优点:不需要两倍资源。
    • 缺点:服务回滚等操作不容易。
    • 需要有强大的基础设施。

流量治理

在微服务架构下,可以基于地区、集群、实例、请求等维度,对端到端流量的路由路径进行精确控制:

负载均衡

负载均衡(Load Balance)负责分配请求在每个下游实例上的分布。
常见的 LB 策略:

  • Round Robin
  • Random
  • Ring Hash
  • Least Request
  • ......

稳定性治理

线上服务总是会出问题,这与程序的正确性无关。

  • 网络攻击
  • 流量突增
  • 机房断电
  • 光纤被挖
  • 机器故障
  • 网络故障
  • 机房空调故障
  • ......

微服务架构中典型的稳定性治理功能:

  • 限流:限制同一时间访问的请求数量,多余的直接拒绝。
  • 熔断:请求到达本服务 A ,本服务 A 需要调用服务 B 却调用失败时,拒绝到达本服务的请求,并隔一段时间就尝试连接服务 B 。
  • 过载保护:当前实例压力过大时拒绝部分流量。
  • 降级:流量过多时保证比较重要的服务的流量正常处理,比较不重要的服务的流量进行拒绝。

重试

重试的意义

本地函数调用可能有哪些异常?

  • 参数非法
  • OOM
  • NPE(Null Pointer Exception)
  • 边界 case
  • 系统崩溃
  • 死循环
  • 程序异常退出
  • ......

本地函数调用重试的意义不大。
远程函数调用可能有哪些异常?

  • 网络抖动
  • 下游负载高导致超时
  • 下游机器宕机
  • 本地机器负载高,调度超时
  • 下游熔断、限流
  • ......

在远程调用中,重试可以避免掉偶发的错误,提高 SLA (Service-Level Agreement).

func RemoteFuncWithRetry() {
    for i := 0; i < 3; i++ { // 最多重试 3 次
        if err := RemoteFunc(); err == nil {
            return // 成功即停止
        }
    }
    // ...
}

重试的意义:

  • 降低错误率:假设单次请求的错误概率为 0.01 ,那么连续两次错误概率则为 0.01 * 0.01 。
  • 降低长尾延迟:对于偶尔耗时较长的请求,重试有机会提前返回。
  • 容忍暂时性错误:某些时候系统会有暂时性异常(例如网络抖动),重试可以尽量规避。
  • 避开下游故障实例:一个服务中可能会有少量实例故障(例如机器故障),重试调用了其他实例则可以成功。

重试的难点

既然网络调用中重试有这么多好处,那么默认要不要使用呢?
重试带来的问题:

  • 幂等性
  • 重试风暴
  • 超时设置

最重要的是重试风暴的问题,随着调用链路的加深,重试次数会以n^m(n为重试次数,m为调用链路长度)放大。

重试策略

为了应对重试风暴,可有如下策略:

  • 限制重试比例:设定重试比例阈值(例如 1%),重试次数占请求比例不超过该阈值。
  • 防止链路重试:链路层面的防重试风暴的核心是限制每层都发生重试,理想情况下只有最尾的一层发生重试。可以返回特殊的状态表明“请求失败,但别重试”。
  • Hedged requests:对冲请求,适用于延迟较高,可能超时的请求,本服务向下游服务的一个实例发送请求后,重新向另一个下游服务的实例发送一个相同的请求,并等待先到达的响应。