微服务框架-不变的基建| 青训营笔记

187 阅读9分钟

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

微服务架构介绍

微服务架构概览

image.png

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

是彻底的服务化,服务至上。

微服务构架三大要素

服务治理

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

可观测性

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

安全性

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

微服务架构原理及特征

基本概念

服务(Service)

一组具有相同逻辑(一个服务的代码是一样的)的运行实体(实例)。

实例(instance)

一个服务中,每个运行实体即为一个实例。

实例与进程的关系

实例与进程之间没有必然对应关系,可以一个实例对应一个或多个进程(反之不常见)。

集群(cluster)

通常指服务内部的逻辑划分,包含多个实例。

常见的实例承载形式

进程、VM、k8s pod。

有状态/无状态服务

服务的实例是否存储了可持久化的数据(例如磁盘文件),存储的服务时有状态,代理形式的服务大概率是无状态。

image.png

服务间的通信

  • 对于单体服务,不同模块通信只是简单的函数调用
  • 对于微服务,服务间通信意味着网络传输,常见的通信协议包括 HTTP、RPC

image.png

服务注册及服务发现

这些概念我们由一个问题引出:

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

或许可以直接指定端口,像这样:

client := grpc.NewClient("10.23.45.67:8080")

但方式指定的问题在于:不太可能指定一个固定的IP地址,在正式微服务环境中IP会动态变化,要借用某种中间件。

image.png 如上图所示,如果直接指定端口,服务B中只有一个实例能被调用到

那么使用DNS呢?

即指定域名与端口,让接受方注册一个域名,这样IP地址变化,改DNS的记录就好了。

image.png

这样好像就能实现了,但还是有一些问题:

  • 本地DNS存在缓存,导致延时,需要实时刷一下
  • 负载均衡问题,DNS会偏向于选择其中第一个IP
  • 不支持服务实例的探火检查
  • 域名无法配置端口,但实际上可以运行上万个服务

基于这些问题,提出了服务注册及发现的概念。

image.png

新增一个统一的服务注册中心,用于存储服务名到服务实例的映射,用一个map去存储。且可以加上个服务的权重。

服务实例上线及下线过程

image.png

image.png 在实现这个需求时,需要考虑一个核心问题:

如果直接把一个实例拿掉,那么流量还是会流向已经下线的实例,由此导致线上问题。

解决方法

  1. 在注册中心把下线的实例的记录删掉,由此调用的服务就不会再调用要下线的实例
  2. 删除要拿掉的实例,因为此时已经没有流量
  3. 要添加实例来缓解其他实例的压力,首先先加入该实例,一开始加记录的话同样会导致流量的失败
  4. 进行health check,会发生一个请求试一下,通过后再注册到服务注册中心,流量恢复

image.png

image.png

流量特征

  • 统一网关入口,处理了负载均衡等
  • 内网通信多数采用RPC(二进制协议),比如Thrift, gRPC,而HTTP是文本协议,运行性能比较差
  • 网状调用链路

基于流量的维度的架构图

image.png

核心服务治理功能

服务发布

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

服务发布的难点

  • 服务不可用
    • 整个服务都要升级
  • 服务抖动
    • 服务中的某个实例要升级,流向该实例的流量就会消失,造成服务功能的缺失
  • 服务回滚
    • 是否回归到之前没问题的代码中

image.png

解决方案

蓝绿部署

方案是让发布分别进行,当一个集群要升级时,将其的流量切给其他集群,此时该集群开始升级。

特点: 简单、稳定,但需要两倍资源,由于要让一半的资源承受所有的流量,所以更适合在流量低时(比如凌晨)进行。

image.png

灰度发布(金丝雀发布)

背景

由于金丝雀对瓦斯极其敏感,在17世纪时,英国矿工会在下矿前放一只金丝雀来探测是否有瓦斯。

具体方法

慢慢加入新代码的实例,如果没问题就继续加,直到替代全部的旧代码。解决了需要额外资源的问题。

  • 先发布少部分实例,接着逐步增加发布比例
  • 不需要增加资源
  • 回滚难度大,基础设施要求高

image.png

问题:这需要一直更新注册表,而且回滚比较困难,因此对基础设施要求高,比如k8s就可以实现精细化回滚的操作。

流量治理

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

image.png

  • 基于地区的控制:基于机器数量的流量控制
  • 基于集群的控制:少部分流量流向测试集群
  • 基于实例的控制:新机器承担高一些的流量
  • 基于请求的控制:正常请求发向正常稳定的集群,内部用户的流量发向测试集群

负载均衡

负责分配组件在上游分配请求在每个下游实例上的分布,希望每个实例的负载即所处理的请求数是均衡的。

image.png

常见的LB策略

  • Round Robin
    • 绝对公平的策略
  • Random
  • Ring Hash(一致性哈希)
    • 跟请求有特点绑定关系,如某一个用户绑定到某一个实例上
  • Least Request

稳定性治理

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

代码控制不了的问题

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

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

  • 限流
    • 拒绝一部分的qps
  • 熔断
    • 类似跳闸,不会再去调用需要的服务,而是拒绝发来的请求,在此期间会不间断的尝试能否连上服务
  • 过载保护
    • 服务的CPU已经过大(>80%),则直接拒绝掉流量或拒绝一部分
  • 降级
    • 保证重要服务能正常工作,拒绝优先级不太高的服务,会识别服务的等级,比如一些内部的用户的服务就可以拒绝掉

image.png

关于重试的实践探索

注:以下内容的成果是由字节跳动产出研发的

请求重试的意义

本地函数调用

可能的异常:

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

除特殊场景外,没有重试的必要,因为函数都是本地调用,如果错了大概率是因为代码的逻辑错误或其他错误。

远程函数调用

可能的异常:

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

是具有重试的意义的。

重试的意义

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

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

请求重试的难点

虽然重试的工程意义是重要的,但在实际中,重试是默认不用的哦,原因是:

幂等性

链路中的每个服务在调用下游的服务时都会重试相同的次数,故上游的重试次数在下游是幂指数增加的。

重试风暴

重试多次,微服务中调用该服务的链路如果很长,那么下游的重试次数会很多,就可能会把下游的服务打垮,发生雪崩。

image.png

超时设置

假设调用时间一共1s,经过多少时间开始重试?

重试策略

限制重试比例

设定一个重试比例阈值(例如1%),重试次数占所有请求比例不超过该阈值,比如一个服务重试99次都是成功,只有一次失败,则有重试的意义,若大部分都是失败的,则无意义。

重试比例是,假如成功了1000次,就限制重试的次数不能超过其百分之一。

image.png

防止链路重试

链路层面的防重试风暴的核心是限制每层都发生重试,理想情况下只有最下一层发生重试。

实现的方法是:可以返回特殊的status表明"请求失败,但别重试"。

image.png

Hedged requests(对冲请求)

该策略是针对延迟很高的场景的。

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

image.png

以上内容若有不正之处,恳请您不吝指正!