微服务架构 |青训营笔记

114 阅读8分钟

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

一、本堂课重点内容:

课程背景:微服务架构是当前大多数互联网公司的标准架构,本节课将重点讲解微服务架构背景由来及全貌,分析其基本原理及特征。

微服务框架介绍

系统架构演变历史

具体详见:架构初探

image.png

微服务架构概览

从组件的维度去看看微服务架构的整体视角

  • 网关
  • 服务配置和治理
  • 链路追踪和监控

image.png

微服务架构三大要素

微服务拆分后带来的挑战:

  • 服务治理(本课程内容)

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

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

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

微服务架构原理及特征

基本概念

  • 服务(service) :一组具有相同逻辑的运行实体(一个服务就是运行同一份代码的多个实例)

  • 实例(instance) : 一个服务中的每个运行实体即为一个实例

  • 集群(cluster):通常指服务内部的逻辑划分,包含多个实例

  • 实例与进程的关系 : 没有必然对应关系,一般一对一或者一对多

  • 常见的实例承载形式 : 进程、VM、k8s pod......

eg:如果把HDFS看成一组微服务,可以拆分如下(两个service):

服务间通信

对于单体服务,不同模块通信只是简单的函数调用。对于微服务,微服务之间通过网络进行通信,常见的通信协议包括 HTTP、RPC。

服务注册及发现

  • 基本问题

微服务架构中,服务之间需要频繁调用,在代码层面如何实现调用一个目标服务地址(ip:port)?

  • 方案一 (hardcode?)

使用hardcode(硬编码),也就是在代码中写死调用地址。

// Service A wants to call service B.
client := grpc.NewClient(“10.23.45.67:8080")

但是这样会存在巨大隐患:由于同一个服务的不同实例可以部署在不同的IP之下,所以由于 service A中运行的instance都是同一份代码,从而都会访问service B中同一个instance。另外,服务实例 ip port 本身是动态变化的。

  • 方案二(DNS?)

存在以下问题:

  1. 本地 DNS 存在缓存,导致延迟(需要频繁刷新缓存)
  2. DNS 没有负载均衡(按顺序访问service中实例)
  3. 不支持服务探活检查(调用之前不能探查目标service是否可用)
  4. DNS 不能指定端口(不够灵活)
  • 方案三(服务注册与发现)

解决思路: 新增一个统一的服务注册中心,用于存储服务名到服务实例的映射。

基于服务发现来实现平滑无损的服务实例上下线流程 :

  1. 由于服务都在线上运行,所以在下线某个实例之前,先从注册中心删去其映射,切断流量后即可终止实例运行
  1. 相反的,当上线一个实例时,需要运行实例 health check,然后在服务注册中心注册该实例后,即可上线流量

流量特征

基于流量的角度来观察微服务架构全貌。

  • 统一网关入口
  • 内网通信多数采用RPC (因为HTTP文本协议的效率,没有RPC二进制协议效率高)
  • 网状调用链路

image.png

即同一个客户端长连接发出的请求,理论上可以到达服务中所有实例

API gateway 可以用作身份认证,进而将 token 附在请求上

核心服务治理功能

服务发布

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

因为所有已经上线的服务都是在线服务,所以在服务发布时,需要解决的就是如何在不影响用户使用的的情况下,进行服务发布。

  • 产生的问题
  1. 服务不可用:service B下线进行升级
  2. 服务抖动:service B中某些实例不可用
  3. 服务回滚:service B上线后产生了bugs,需要立即回滚到上一个版本,以降低损失

image.png

  • 蓝绿部署

将服务分成两个部分,分别先后发布(使用两个集群),保证有一个集群可用。 优点是:实现简单,服务稳定。缺点是:需要两倍资源,即升级时一半的资源提供给上游服务(所以服务发布时避开高峰期)

  • 灰度发布(金丝雀发布)

金丝雀(canary) 对瓦斯及其敏感,17世纪时,英国旷工在下井前会先放入一只金丝雀以确保矿井中没有瓦斯。

先发布少部分实例,接着逐步增加发布比例,虽然相对蓝绿部署可以节省资源,但是其实现难度较大,当出现错误时,回滚难度大,基础设施要求高。

image.png

流量治理(流量控制)

在微服务架构中,可以从各个维度对端到端的流量在链路上进行精确控制。

控制维度

  • 地区维度:根据服务能力分配地区间的流量(beijing 60% shanghai 40%)
  • 集群维度:在serviceA中新开一个test cluster,打入少量流量进行测试(用户请求)
  • 实例维度:由于每个实例中的物理环境差异,需要控制每个实例的流量比例,保证负载均衡
  • 请求维度:在service中新开的 feature test cluster,只接收来自内部用户的测试请求。

image.png

负载均衡

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

常见的 LB 策略:

  • Round Robin (绝对公平)
  • Random
  • Ring Hash (指定服务)
  • Least Request

稳定性治理

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

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

为了应对这些可能出现的问题,微服务架构中提供了一些典型的稳定性治理功能(嵌入各种组件,提供保护机制):

  • 限流:限制服务处理的最大 QPS,拒绝过多请求

  • 熔断:中断请求的路径,增加冷却时间从而让故障实例尝试恢复

  • 过载保护:在负载高的实例中(CPU 99%),主动拒绝一部分请求,防止实例被打挂

  • 降级:服务处理能力不足时,拒绝低级别的请求,只响应线上高优请求

image.png

字节跳动服务治理实践

重试的意义

一般我们调用下游函数时,当返回err信息时,可以选择重试调用该函数,以避免一些偶发的错误。

  • 本地函数调用

本地函数调用的重试通常是没有意义的,因为其返回结果通常固定,因为异常通常为 : 参数非法、OOM (Out Of Memory)、NPE(Null Pointer Exception)、边界 case、系统崩溃、死循环、程序异常退出。

  • 远程函数调用

以grpc调用为例,可能的异常有:网络抖动、下游负载高导致超时、下游机器宕机、 本地机器负载高,调度超时、下游熔断、限流等等。

此时重试可以避免掉偶发的错误,提高 SLA (Service-Level Agreement)。

一般可以选择重试三次。

  • 重试的意义

    • 降低错误率 : 假设单次请求的错误概率为 0.01,那么连续两次错误概率则为 0.0001.

    • 降低长尾延时 : 对于偶尔耗时较长的请求,重试请求有机会提前返回。

    • 容忍暂时性错误 : 某些时候系统会有暂时性异常(例如网络抖动),重试可以尽量规避

    • 避开下游故障实例 : 一个服务中可能会有少量实例故障 (例如机器故障) ,重试其他实例可以成功。

重试的难点

  • 幂等性:多次请求可能会造成数据不一致 (POST 请求可以重试吗?)

  • 重试风暴:随着调用深度的增加,重试次数会指数级上涨(3^3)

  • 超时设置:假设一个调用正常是 1s 的超时时间,如果允许一次重试,那么第一次请求经过多少时间时,才开始重试呢?

重试策略

  • 为了避免重试风暴,有如下两个解决方案:
  1. 限制重试比例:设定一个重试比例阈值 (例如 1%),重试次数占所有请求比例不超过该阈值。例如 ,重试次数不超过1000 * 0.1 = 10

  2. 防止链路重试:链路层面的防重试风暴的核心是,限制每层都发生重试,理想情况下只有最下一层发生重试。所以当某一层重试三次都失败时,可以返回给上一层特殊的 status 表明“请求失败,但别重试”。

image.png

  • Hedged requests

对于可能超时 (或延时高) 的请求,重新向另一个下游实例发送一个相同的请求,并等待先到达的响应。

重试效果验证

实际验证经过上述重试策略后,在链路上发生的重试放大效应:

image.png

参考资料

后端专场-学习资料四