这是我参与「第五届青训营」伴学笔记创作活动的第 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 策略:
稳定性治理
线上服务总是会出问题,这与程序的正确性无关。
- 网络攻击
- 流量突增
- 机房断电
- 光纤被挖
- 机器故障
- 网络故障
- 机房空调故障
- ......
微服务架构中典型的稳定性治理功能:
- 限流:限制同一时间访问的请求数量,多余的直接拒绝。
- 熔断:请求到达本服务 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:对冲请求,适用于延迟较高,可能超时的请求,本服务向下游服务的一个实例发送请求后,重新向另一个下游服务的实例发送一个相同的请求,并等待先到达的响应。