这是我参与「第五届青训营 」笔记创作活动的第11天
一、本堂课重点内容:
课程背景:微服务架构是当前大多数互联网公司的标准架构,本节课将重点讲解微服务架构背景由来及全貌,分析其基本原理及特征。
微服务框架介绍
系统架构演变历史
具体详见:架构初探
微服务架构概览
从组件的维度去看看微服务架构的整体视角
- 网关
- 服务配置和治理
- 链路追踪和监控
微服务架构三大要素
微服务拆分后带来的挑战:
-
服务治理(本课程内容)
- 服务注册
- 服务发现
- 负载均衡
- 扩缩容
- 流量治理
- 稳定性治理
-
可观测性
- 日志采集
- 日志分析
- 监控打点
- 监控大盘
- 异常报警
- 链路追踪
-
安全
- 身份验证
- 认证授权
- 访问令牌
- 审计
- 传输加密
- 黑产攻击
微服务架构原理及特征
基本概念
-
服务(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?)
存在以下问题:
- 本地 DNS 存在缓存,导致延迟(需要频繁刷新缓存)
- DNS 没有负载均衡(按顺序访问service中实例)
- 不支持服务探活检查(调用之前不能探查目标service是否可用)
- DNS 不能指定端口(不够灵活)
- 方案三(服务注册与发现)
解决思路: 新增一个统一的服务注册中心,用于存储服务名到服务实例的映射。
基于服务发现来实现平滑无损的服务实例上下线流程 :
- 由于服务都在线上运行,所以在下线某个实例之前,先从注册中心删去其映射,切断流量后即可终止实例运行
- 相反的,当上线一个实例时,需要运行实例 health check,然后在服务注册中心注册该实例后,即可上线流量
流量特征
基于流量的角度来观察微服务架构全貌。
- 统一网关入口
- 内网通信多数采用RPC (因为HTTP文本协议的效率,没有RPC二进制协议效率高)
- 网状调用链路
即同一个客户端长连接发出的请求,理论上可以到达服务中所有实例
API gateway 可以用作身份认证,进而将 token 附在请求上
核心服务治理功能
服务发布
服务发布 (deployment),指让一个服务升级运行新的代码的过程。
因为所有已经上线的服务都是在线服务,所以在服务发布时,需要解决的就是如何在不影响用户使用的的情况下,进行服务发布。
- 产生的问题
- 服务不可用:service B下线进行升级
- 服务抖动:service B中某些实例不可用
- 服务回滚:service B上线后产生了bugs,需要立即回滚到上一个版本,以降低损失
- 蓝绿部署
将服务分成两个部分,分别先后发布(使用两个集群),保证有一个集群可用。 优点是:实现简单,服务稳定。缺点是:需要两倍资源,即升级时一半的资源提供给上游服务(所以服务发布时避开高峰期)
- 灰度发布(金丝雀发布)
金丝雀(canary) 对瓦斯及其敏感,17世纪时,英国旷工在下井前会先放入一只金丝雀以确保矿井中没有瓦斯。
先发布少部分实例,接着逐步增加发布比例,虽然相对蓝绿部署可以节省资源,但是其实现难度较大,当出现错误时,回滚难度大,基础设施要求高。
流量治理(流量控制)
在微服务架构中,可以从各个维度对端到端的流量在链路上进行精确控制。
控制维度:
- 地区维度:根据服务能力分配地区间的流量(beijing 60% shanghai 40%)
- 集群维度:在serviceA中新开一个test cluster,打入少量流量进行测试(用户请求)
- 实例维度:由于每个实例中的物理环境差异,需要控制每个实例的流量比例,保证负载均衡
- 请求维度:在service中新开的 feature test cluster,只接收来自内部用户的测试请求。
负载均衡
负载均衡(Load Balance)负责分配请求在每个下游实例上的分布。
常见的 LB 策略:
- Round Robin (绝对公平)
- Random
- Ring Hash (指定服务)
- Least Request
稳定性治理
线上服务总是会出问题的,这与程序的正确性无关。比如:
- 网络攻击、流量突增、机房断电、光纤被挖、机器故障、网络故障、机房空调故障等等
为了应对这些可能出现的问题,微服务架构中提供了一些典型的稳定性治理功能(嵌入各种组件,提供保护机制):
-
限流:限制服务处理的最大 QPS,拒绝过多请求
-
熔断:中断请求的路径,增加冷却时间从而让故障实例尝试恢复
-
过载保护:在负载高的实例中(CPU 99%),主动拒绝一部分请求,防止实例被打挂
-
降级:服务处理能力不足时,拒绝低级别的请求,只响应线上高优请求
字节跳动服务治理实践
重试的意义
一般我们调用下游函数时,当返回err信息时,可以选择重试调用该函数,以避免一些偶发的错误。
- 本地函数调用
本地函数调用的重试通常是没有意义的,因为其返回结果通常固定,因为异常通常为 : 参数非法、OOM (Out Of Memory)、NPE(Null Pointer Exception)、边界 case、系统崩溃、死循环、程序异常退出。
- 远程函数调用
以grpc调用为例,可能的异常有:网络抖动、下游负载高导致超时、下游机器宕机、 本地机器负载高,调度超时、下游熔断、限流等等。
此时重试可以避免掉偶发的错误,提高 SLA (Service-Level Agreement)。
一般可以选择重试三次。
-
重试的意义
-
降低错误率 : 假设单次请求的错误概率为 0.01,那么连续两次错误概率则为 0.0001.
-
降低长尾延时 : 对于偶尔耗时较长的请求,重试请求有机会提前返回。
-
容忍暂时性错误 : 某些时候系统会有暂时性异常(例如网络抖动),重试可以尽量规避
-
避开下游故障实例 : 一个服务中可能会有少量实例故障 (例如机器故障) ,重试其他实例可以成功。
-
重试的难点
-
幂等性:多次请求可能会造成数据不一致 (POST 请求可以重试吗?)
-
重试风暴:随着调用深度的增加,重试次数会指数级上涨(3^3)
-
超时设置:假设一个调用正常是 1s 的超时时间,如果允许一次重试,那么第一次请求经过多少时间时,才开始重试呢?
重试策略
- 为了避免重试风暴,有如下两个解决方案:
-
限制重试比例:设定一个重试比例阈值 (例如 1%),重试次数占所有请求比例不超过该阈值。例如
,重试次数不超过
1000 * 0.1 = 10。 -
防止链路重试:链路层面的防重试风暴的核心是,限制每层都发生重试,理想情况下只有最下一层发生重试。所以当某一层重试三次都失败时,可以返回给上一层特殊的 status 表明“请求失败,但别重试”。
- Hedged requests
对于可能超时 (或延时高) 的请求,重新向另一个下游实例发送一个相同的请求,并等待先到达的响应。
重试效果验证
实际验证经过上述重试策略后,在链路上发生的重试放大效应: