这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天
1.1 系统架构演变历史
1.1.1单体
1.1.2 垂直应用架构
垂直应用是面向业务的,也就是把整个业务分解为很多小业务。
水平则是面向资源的,在业务不变的情况下,扩展业务集群结点数量
1.1.3 分布式架构
此时已经有了面向服务的雏形。
但是此时基本没有管理层,服务之间的调用得不到保障
每个模块存在冗余的依赖。
1.1.4 SOA架构
引入了中心化的服务注册中心(ESB,一种通信规范,SOA架构中的所有服务按照ESB来通信,这个通信是中心化的)
对服务调用进行了一定的管理。
但是是从上而下的设计,即先有架构,再有服务。
业务逻辑A的开发人员,需要考虑业务逻辑B的依赖。
微服务
- 完全面向服务的架构
- 去中心化,节点之间依靠通信手段进行通信
- 自下而上设计(每个服务都可以独立完成,最后由服务集群建立起整体的框架),业务逻辑开发者专注自己的开发
1.2 微服务架构核心要素
服务治理:nacos,流量治理(熔断,降级... hystrix)
可观测性:logging(efk) metrics(Prometheus,指标采集聚合) tracing(opentracing)
安全:jwt ...
1.2.1 微服务架构原理&特征
服务 include 多个集群
多个集群include 多个实例
只要服务内的实例运行逻辑一致,就可以作为一个服务(相当于水平扩展)
实例承载:进程(运行在物理机上的服务),VM(docker容器,k8s pod)
我们可以尝试用微服务的概念理解一些分布式系统。当然可以。
比如hdfs,mysql集群,redis集群,或者分布式存储这些。
- 服务间通信
一般http仅用于对外暴露端点。
而rpc则负责在内部调用,得益于rpc的协议简单性(控制字段少,采取二进制格式[protobuf/thrift]),使得网络io效率高于http调用
1.2.2 服务注册&发现
硬编码ip:port的结果 ==> 换地方了。我们希望通过服务名就可以找到服务实例,因为往往采取更灵活的vm部署,所以服务的ip和port很容易变化。
方案一:DNS
DNS协议的设计上,不是为了服务发现的
因此,DNS在本地的缓存会导致寻找到错误的实例。
DNS不支持负载均衡,一般的DNS系统只会匹配最上面的,像CDN这种只能的DNS系统,导致可以具备负载均衡能力
我们可以把不存在的ip绑定到域名上,也就是DNS不会探活
使用DNS,那么就意味着服务内的所有实例,端口都要写死,如果是VM这样的布置,节省主机个数,就难以完成了
使用注册中心,存储服务名到实例的映射。注册中心本质是一个存储系统,因此可以用分布式的KV来实现。
但是如果只存储数据,那么和DNS有何差异?因此注册中心还要具备的是
- 服务下线时,先删除注册中心的记录,然后下线实例
- 服务上线时,注册中心应该能通过探活机制,健康检查通过后,才能上线实例(在eureka的实现中,服务注册一方会通过心跳包,向注册中心发送自己是健康的,服务中心发现实例正常,才能在自己的存储中加入记录。)
1.2.3 流量特征
2. 核心服务治理
2.1 服务发布
服务发布的难点
由于微服务各服务是去中心化的,服务之间通过rpc通信。如果要对服务B升级,势必会对整条链路有影响。
如果升级后,服务不可用,那么整条链路面临崩溃!
服务抖动:一个服务的负载在同一时刻忽高忽低,很可能是集群内实例升级导致挂了,这样多余的流量就打到自己身上了。
服务回滚:如果线上出了bug,肯定不能像我们之前那样,打个断点,去csdn,StackOverflow一顿乱查,debug完事上线。而是要火速回滚服务。
解决办法
蓝绿发布、滚动发布、灰度发布,有什么区别 ? - 知乎 (zhihu.com)
蓝绿部署:分批次的更新服务。
- 有效缓解了服务更新后失败导致的链路不可用
- 由于是批次更新,因此服务回滚也可以批次回滚。
灰度发布
逐渐更新服务,serviceA调用的serviceB是一个灰度发布的过程
- 相比蓝绿更新,节约了资源。
- 相对的,回滚起来比较麻烦。
下线待升级的服务器,进行灰度升级后,重新上线(这里的上线和下线都是经过服务中心的)。如果成功,则对第二个实例重复之前的,否则回滚。
2.2 流量治理
由于微服务存在了调用链路。这未必是坏事,因为我们可以在调用链路上做手脚。
我们可以基于 地区,集群,CPU(实例),请求进行精准控制。
有些注册中心有基于地区(Zone)的配置,可以在配置文件中将 集群进一步划分为地区的集合,对于不同地区,采取不同的负载策略。
2.3 负载均衡
客户端负载均衡:客户端发起的请求自带负载均衡,比如java的ribbon,可以在服务发现时,获取服务的负载,在客户端进行负载均衡,首先定位到一个服务实例
服务端负载均衡:服务端发现请求来的时候,会被网关拦截,在网关这一层,又会进行一次负载均衡
网关的作用:进行负载均衡 ==> 选择客户端要执行的服务链路,并进行服务发现,通过均衡找到一个服务实例
进行urlRewrite:在有网关的系统中,请求都是网关的地址,网关这时候可以通过URIRewrite来定位到其他的服务
3.4 稳定性治理
限流:通过漏桶算法,可以进行快失败,直接拒绝,也可以在用户容忍的时延内,慢处理一些请求
熔断:当服务A发现服务B请求失败达到一定指标后,采取熔断策略,可以执行默认的失败方法。等到冷却后,重新尝试连接服务B,如果成功则冷却,否则继续熔断
过载保护:一种快速失败策略,当目标服务的CPU负载过高,目标的过载保护器就会拒绝请求
降级:当负载过高,触发降级策略时,目标服务将拒绝响应优先级差的服务
4. 实践:重试
有必要对本地函数进行重试吗?
- 没必要,因为本地函数错了就是错了,大概率是代码写错了。重试几次代码都是错的。或者输入参数有问题,同样没必要重试。
为什么远程函数调用需要重试
- 代码和输入都正确,但是错在了无法预测的网络状况。或者下游服务的情况。
简单的基于for循环重试。
好处
- 可以把错误率提升到4个9,5个9 ....
- 在进行qps的benchmark中,我们可以看到一个指标 p99,这意味着99%的中位数。这个指标的含义就是:尽管平均值是一定的,但是总有一些请求,延迟远大于平均值,这些请求占比很少。这样,假如一个请求进入了长尾延时,需要2s,正常请求就需要100ms,超时时间为500ms,这样,如果不重试,需要两秒,重试则需要500+100=600ms
- 暂时性错误:由于网络IO导致的,或者机器负载,流量倾斜等情况造成的服务暂时性不可用。
- 下有故障:服务中的一个实例刚好挂了
3.1 重试的难点
幂等性:重试相当于调用一个过程三次,这使得原本的调用失去了幂等性。尤其是在消息队列中,一个超时的请求可能被mq收到了,但是这边检测到超时,就进行重试,这会导致mq重复生产/消费。
重试风暴:
重试次数随着调用链路指数增长
解决办法:如果失败次数超过链路中的请求失败阈值,则不重试
解决办法2:只有链路末尾的服务才重试,如果重试都失败了,则返回给上游服务器,让他们别重试了。
解决方法3:
对于极大可能超时的请求,可以采取碰撞请求,在请求超时的一定比例因子时,发出一个碰撞请求。(比如预定超时时间为2s,在1.5s时就发送)。等待先到达的响应,忽略后到达的响应。
但是如果请求造成的双倍的影响,这时候应该主动考虑幂等性,比如引入全局唯一事务id等。