目录
-
单体架构
-
垂直应用架构
-
分布式应用架构
-
SOA(Service Oriented Architecture)面向服务架构
-
微服务
-
Service Mesh
-
服务发现
-
网关路由
-
客户端负载均衡
-
客户端缓存
-
强制缓存
-
协商缓存
-
域名解析
-
内容分发网络
-
路由解析
-
内容分发
-
CDN应用
-
负载均衡
-
数据链路层负载均衡
-
网络层负载均衡
-
应用层负载均衡
-
负载均衡策略
-
服务端缓存
-
缓存风险
-
流量治理
-
服务容错
-
流量控制
-
认证
-
HTTP认证
-
Web认证
-
凭证
-
授权
-
OAuth 2.0
-
RBAC(Role-Based Access Control)模型
-
保密
-
传输安全性保证
-
校验验证
说明
阅读《凤凰架构》个人总结和扩展
服务架构演进历史
单体架构
早期的互联网产品用户量少、并发量低、数据量小,单机应用部署是主流架构,方便开发、部署和测试。应用与数据库分开部署,在一定程度上通过负载均衡后挂多个单机来提高系统的性能。但单机应用由于运行在一个进程中,如果一个模块出现了问题,会导致整个进程卡死,从而导致系统不可用。
图示:

单体架构下资源有限,为了降低数据库压力,可以采用读写分离、缓存等方式优化。
图示:

可以使用动静分离,提高用户访问静态资源的速度,降低对后端的访问压力。针对单机应用也可以通过负载均衡分流多台服务器,更大程度提高系统处理能力。
图示:

垂直应用架构
在单体应用的基础上,将原本一个大应用拆分成几个互不相干的应用模块,可以针对不同的应用模块特点进行部署,比如访问量大的应用可以多部署几台。
以电商业务场景图示:

分布式应用架构
随着业务场景的复杂化,将复杂应用拆分分而治之成为更好的选择,大型业务细化并拆分成子业务应用,每个子业务应用可以进行独立开发、部署和运维,如果子业务很多,且拥有自己的数据存储层,数据层的压力会越来越大,通过把子业务应用的通用部分单独抽离出来作为通用服务统一访问数据层减缓数据层压力,子业务应用直接通过RPC或者消息系统进行通信。
图示:

SOA(Service Oriented Architecture)面向服务架构
SOA架构是通过引入ESB(企业服务总线)连接服务节点,让不同的服务间可以互相通讯。
图示:

微服务
随着业务的发展,大型应用的出现,微服务在SOA的基础上去除ESB,以更细粒度去拆分应用,变成一个个的服务,服务可以单独开发、部署和维护,系统分布式架构,服务去中心化实现。
微服务架构有九大特性
-
服务组件化;
-
按业务组织团队;
-
做“产品”的态度;
-
智能端点与哑管道;
-
去中心化治理;
-
去中心化管理数据;
-
基础设施自动化;
-
容错设计;
-
演进式设计;
图示:

Service Mesh
Service Mesh 使开发更加聚焦于业务,而不需要关注基础设施层,通过网络代理进行服务发现、服务通信、配置等基础设施层功能。

分布式架构思维
从思维上出发,可以把架构分为以下几个部分:
-
远程访问,服务间怎么调用? -
分流,流量怎么控制?如何应对大流量?如何应对恶意流量? -
安全,如何认证?如何控制权限? -
容错兜底,如果服务出现故障、网络波动如何应对? -
事务 -
监控追踪,如何宏观观察系统和基础设施的健康程度?复杂服务调用如果出现问题怎么排查?怎么快速响应? -
基础设施,应对大促如何快速弹性扩容?如何更加简单得部署?如何聚焦于业务开发?
最合适、合理的架构选择才是最好的系统,能用简单的架构解决就不要用复杂的架构装逼。
远程服务调用
远程服务调用(Remote Procedure Call)即RPC,像调用本地一样调用远程方法,可以做到跨语言、跨平台,要比本地调用方法复杂太多。RPC的发展阶段都是为了解决以下三个问题:
-
如何表示数据,客户端如何表示参数,服务端如何表示结果,在不同的机器、网络环境同样的数据类型可能会有不同的表现类型(比如占用长度、大小端等),需要RPC序列化协议通过序列化和反序列化表示数据 -
如何传递数据,客户端的参数、服务端的结果需要通过网络进行流转(一般通过网络协议传输,HTTP、TCP、UDP等),除此之外还需要异常、超时、安全认证、授权等 -
如何表示方法,如何跨机器、跨语言去表示一个方法?
常见的RPC框架有Dubbo、gRpc、Thrift等,RPC框架相对于HTTP容器来说,更多得封装了比如服务发现、负载均衡、熔断降级等面向服务得高级特性,使得开发服务时使用RPC更加可靠、方便。
经常跟RPC做比较的还有Restful风格设计,但两者其实不是同一个东西,Restful是一种设计风格,借助HTTP原语(比如GET、POST等)的一种面向资源的接口设计方式,具体可参考RESTful API 设计指南。
在微服务架构中,集群中的节点扮演着服务的生产者、消费者多重角色,形成一个网状调用关系,此时,要考虑以下三个问题:
-
服务发现,对消费者来说,外部的服务是谁提供的?在什么网络位置? -
服务网关路由,对生产者来说,内部需要暴露哪些服务?哪些应该隐藏?以什么形式暴露服务?以什么规则在集群中分配请求? -
服务负载均衡,在调用过程中,如何确保每个远程服务流量均衡,尽可能确保服务的可靠性
服务发现
所有的远程服务都是使用全限定名+端口号+服务标识所构成的三元组来确定一个远程服务的坐标的:
-
全限定名,网络中某台主机的精确位置 -
端口号,主机上某个提供TCP/UDP网络服务的程序 -
服务标识,跟具体协议有关,比如REST,标识是URL地址(例如 http://media-center-service/queryMediaAccount
)J
早起的服务发现依赖于DNS将全限定名翻译为IP地址,借助负载均衡器,把外部IP地址映射到内部IP地址,但随着微服务的流行,服务的上下线变得更加频繁,这种方式进入疲软化。最初通过使用ZK完成服务注册与发现,到之后的Eureka出现,以及Consul和Nacos更加成熟。此时的服务发现框架比较成熟,不仅支持通过DNS或者HTTP请求进行地址转换,还支持了各种的服务监控检查、配置等。随着云原生时代来临,基础设施的灵活性大幅度增强,最初使用基础设施透明化服务发现又被重视起来。
服务发现主要包含三个流程:
-
服务的注册,服务启动时,以某种形式(调用API、发消息、ZK写入节点数据、存入数据库等等)将自己的坐标信息通知到注册中心,这个过程有两种实现方式: -
自注册,应用程序自己完成,比如Spring Cloud的 @EnableEurekaClient
注解 -
第三方注册,容器编排框架或者第三方注册工具,比如K8s -
服务的维护,服务的下线、监控(心跳、探针)等监控服务的健康,把不健康的服务从服务注册表中剔除 -
服务的发现,消费者如何把注册中心中的符号(比如Nacos中的服务名)转换为实际网络坐标的过程
在实际应用场景中,注册中心的地位是很特殊的,是系统中最基础的服务,要尽最大努力保证注册中心的高可用(AP)或者高可靠性(CP),注册中心通常使用集群部署。
不同的注册中心做法不同:
-
Eureka,选择保证高可用性,当有新服务注册时,并不需要等待其他节点复制完成,变动信息不会实时同步给所有节点和客户端,这样的设计使得客户端可以保存服务列表缓存,并以TTL机制来更新,哪怕注册中心宕机,客户端依然可以通过缓存访问到服务 -
Consul,选择保证高可靠性,Consul采用Raft算法,要求多数节点写入成功后服务的变动才生效
注册中心主要有三类:
-
分布式K/V存储框架上自己开发的服务发现,比如ZK -
基础设施实现服务发现,比如SkyDNS、CoreDNS等 -
专门用于服务发现的框架,比如Eureka、Consul和Nacos
网关路由
微服务中的网关,也被称为服务网关或者API网关,一般支持两个功能:
-
路由器,根据流量的某种特征进行路由 -
过滤器,用于实现安全、认证、授权、限流、监控、缓存等
因为网关是所有服务对外的总出口,流量必经之地,所以需要关注网关的性能和可用性。网关默认都必须支持七层路由,通常默认无法直接流量转发,只能采用代理模式,网关的性能主要取决于如何代理网络请求,即网络IO模型。
每一次网络访问,有两个阶段:
-
等待数据从远程逐级到达缓冲区 -
从缓冲区复制数据到应用程序地址空间
根据实现这两个阶段的不同,IO模型分为两类、五种模型:
-
异步IO -
同步IO -
阻塞IO -
非阻塞IO -
多路复用IO -
信号驱动IO
网关产品比如Nginx、Zuul、Ingress等
客户端负载均衡
客户端负载均衡器是和服务实例一一对应的,与服务实例并存于同一个进程内,客户端负载均衡的好处:
-
客户端负载均衡器与服务的信息交换是在进程内的方法调用,不存在额外的网络开销 -
避免了集中式单点问题 -
更加灵活,可以根据服务来单独配置
缺点:
-
与服务运行于一个进程,则受语言限制 -
由于公用一个进程,会影响到整个服务的稳定性,占用资源 -
客户端负载均衡器需要实现上线服务、下线旧服务、剔除、自动恢复等功能,需要访问注册中心来完成,通常通过轮询(gRpc、长连接)等,也会带来不小的负担
Java中典型代表是Netflix的Ribbon和Spring Cloud的LoadBalancer。
服务网格中的代理客户端负载均衡器,是作为一个进程之外,一个Pod内的特殊服务(Side car),从服务进程中分离带来的好处非常明显:
-
均衡器将不在受语言限制 -
服务拓扑感知更有优势,不需要客户端负载均衡器主动轮询 -
安全性、可观测性更高
多级分流
在复杂系统中,请求从浏览器出发,可能需要经过DNS、CDN、Gateway、LB、Cache、Server等一系列基础设施,不同的设施有不同的价值。
客户端缓存
由于HTTP是无状态协议,不可避免会出现携带重复数据的网络开销,HTTP从1.0、1.1再到2.0版本中,实现了强制缓存、协商缓存的HTTP机制。
浏览器向服务器请求A资源,如果A资源在一定时间内不会发生变更,那无需重新请求,只需要缓存在本地即可,在找个环节中引发的问题时浏览器客户端如何知道资源是否变更?是取缓存还是重新请求?
强制缓存
HTTP/1.0提供了Expires
Header,服务端告知客户端所请求的资源直到某个时间点不会发生变更,可以放心食用,示例:
Expires: Wed, 21 Oct 2015 07:28:00 GMT
这种机制有以下缺点:
-
依赖客户端的本地时间 -
无法处理私有资源,比如登录用户缓存特有的资源,如果被代理或者CDN缓存则可能被非法获取到 -
不能不缓存,没有声明某个资源强制不缓存的机制
于是HTTP/1.1定义了新的Header,Cache-Control
。
-
max-age
和s-maxage
(shared,允许CDN、代理缓存有效时间),定义资源多少秒后过期,需要重新请求 -
public
和private
表示是否是用户私有资源,public
可以被CDN、代理等缓存,private
则只可以被客户端缓存 -
no-cahche
表明该资源强制不允许缓存 -
no-store
不强制同一会话中对同一URL资源重复获取,但禁止浏览器、CDN等缓存该资源 -
no-transform
,禁止修改资源,比如CDN进行压缩传输等 -
min-fresh
客户端的Header,表明建议服务端返回包含max-age
且不少于min-fresh
缓存时间的资源 -
only-if-cached
客户端的Header,客户端要求服务端不发送资源,客户端只用缓存资源,如果没有命中缓存,直接503 -
must-revalidate
和proxy-revalidate
,表示客户端在资源过期后,一定要从服务器获取,proxy-revalidate
针对CDN、代理等
协商缓存
强制缓存在保证一致性上略有欠缺,因为无论客户端、服务端不一定完全确定资源的有效期,通过一次变化检测请求,则为协商缓存。
根据最后修改时间的协商机制:
Last-Modified
,服务端响应Header,告知客户端资源的最后修改时间,客户端需要二次请求,通过If-Modified-Since
把之前收到资源的最后修改时间返回给服务端,由服务端根据最后修改时间确认资源是否被修改。如果没有被修改返回304(Not Modified)
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
Last-Modified
有两个缺陷:
-
精度只有秒级,如果在同一秒中发生变更,则无法识别 -
无法处理如果定时生成同一文件,由于创建时间会导致最后修改时间不一致,但实际上是同一份资源的情况
根据签名的协商机制:
ETag
是服务端响应Header,是资源的唯一标识,比如资源的哈希签名。客户端返回If-None-Match
之前收到的资源唯一标识,服务端判断是否一直即可确认资源是否变更。
ETag
是一致性最高的,但性能最差的缓存机制。
域名解析
网络层是通过IP地址访问的,当通过域名访问时,需要经过DNS的翻译工作,即把域名翻译成IP地址。
DNS解析是一种递归(或者迭代)的查找形式,从本地DNS到权威域名DNS到根域名DNS逐级查找直到找到域名对应的IP记录。
DNS是通过TTL来衡量缓存有效期的,TTL超期后重新获取来保证一致性。
图示,以我的个人博客域名为例(阿里云控制台):

DNS中的A记录类型是记录域名–>IP地址
CNAME记录时记录域名–>域名
内容分发网络
一个互联网系统的速度取决于以下四个因素:
-
网站–>网络运营商的出口带宽 -
网络运营商提供给用户的入口带宽 -
从网站到用户经过的不同运营商之间的互联网节点的带宽 -
从网站到用户的物理链路传输延迟(Ping)
除了第二个取决于用户使用设备的带宽,其他三个都能通过内容分发网络显著改善。
内容分发网络的工作流程主要涉及路由解析、内容分发、负载均衡和CDN应用四个方面。
路由解析
在没有CDN的情况下,浏览器请求某个域名(比如blog.hnzhrh.com)资源会有以下流程:
-
本地DNS查找是否有blog.hnzhrh.com的映射记录 -
如果没有逐级查找权威DNS -
直到找到域名-IP映射记录返回 -
客户端根据IP请求服务端
购买了CDN服务后,客户端请求DNS得到的是加速域名,加速域名DNS调度系统返回CDN节点,客户端通过CDN节点访问CDN缓存内容。
阿里云文档说明:

当终端用户向 www.aliyundoc.com 下的指定资源发起请求时,首先向Local DNS(本地DNS)发起请求域名 www.aliyundoc.com 对应的IP。 Local DNS检查缓存中是否有 www.aliyundoc.com 的IP地址记录。如果有,则直接返回给终端用户;如果没有,则向网站授权DNS请求域名www.aliyundoc.com 的解析记录。 当网站授权DNS解析 www.aliyundoc.com 后,返回域名的CNAME www.aliyundoc.com.example.com。 Local DNS向阿里云CDN的DNS调度系统请求域名www.aliyundoc.com.example.com的解析记录,阿里云CDN的DNS调度系统将为其分配最佳节点IP地址。 Local DNS获取阿里云CDN的DNS调度系统返回的最佳节点IP地址。 Local DNS将最佳节点IP地址返回给用户,用户获取到最佳节点IP地址。 用户向最佳节点IP地址发起对该资源的访问请求。
如果该最佳节点已缓存该资源,则会将请求的资源直接返回给用户(图中步骤8),此时请求结束。 如果该最佳节点未缓存该资源或者缓存的资源已经失效,则节点将会向源站发起对该资源的请求。获取源站资源后结合用户自定义配置的缓存策略,将资源缓存到CDN节点并返回给用户(图中步骤8),此时请求结束。
内容分发
CDN缓存节点接管源站资源会有两个问题需要解决:
-
如何获取源站资源? -
如何更新源站资源?
CDN获取源站资源的过程被称为内容分发,有以下两种主流方式:
-
主动分发(Push):源站点主动推送资源到CDN,对用户透明,但对源站不透明,主动分发一般用于网站要预载大量资源的场景(比如双十一大促,淘宝、京东等会把活动资源预先推送到CDN中进行缓存) -
被动回源(Pull):由用户访问触发,全自动、双向透明,用户首次访问时CDN从源站加载资源并缓存,在缓存有效期间接管源站资源,当缓存失效时,再次请求源站获取资源
CDN应用
-
加速静态资源分发 -
安全防御,由于接管了源站,可以防御恶意攻击 -
协议升级,可以实现源站HTTP但通过CDN对外开发的协议为HTTPS -
状态缓存 -
修改资源,比如压缩传输等 -
访问控制,CDN可以实现IP黑白名单等 -
注入功能,CDN可以在不修改源站的情况下注入CDN提供的应用
负载均衡
当系统变得庞大而又复杂是,往往会通过横向扩展来增强服务能力,对于用户通过域名访问资源是透明的,但背后可能是很多太机器,承担调度、统一对外的组件即为负载均衡器。
从形式上可以分为:
-
四层负载均衡(传输层),数据转发,维持着同一条TCP通道 -
七层负载均衡(应用层),代理,客户端于LB和LB与服务端维持着不同TCP通道
从实现上可以分为:
-
软件LB -
操作系统内核LB,比如LVS -
应用程序LB,比如Nginx、KeepAlived -
硬件LB,专用硬件
数据链路层负载均衡
数据链路层负载均衡器主要工作在以太网中,数据链路层负载均衡器会修改数据帧中的MAC地址,转发到真实的服务器上,响应请求不需要通过负载均衡器即可以原路返回,因此数据链路层负载均衡是性能最高的。但除了无法感知应用数据定制负载均衡的缺点外,还不能跨VLAN,必须在一个子网下。

网络层负载均衡
工作在网络层的负载均衡,IP数据包由Header和Payload两部分组成,Header中包含了源IP地址和目标IP地址决定IP数据包的流转起点和终点,网络层负载均衡有两种做法:
-
把源IP数据包的Header和Payload作为新的数据包的Payload,Header中写入新的真实的源IP地址和目标IP地址,真实数据服务器收到数据包后,拆包,把负载均衡器自动添加的Header扔掉,还原出源数据包,这种传输称为IP隧道传输(TUN模式) -
直接修改数据包,修改Header的目标地址为真实目标服务器地址,好处是不需要拆包,坏处是必须在返回给负载均衡器,如果直接返回给客户端则客户端不认识真实目标服务器地址,这种模式称为NAT模式
IP隧道传输模式图示:

NAT模式图示:

应用层负载均衡
代理分为:
-
正向代理,客户端感知,服务端透明 -
反向代理,客户端透明,服务端感知 -
透明代理,两者都透明
应用层负载均衡属于反向代理,工作在应用层,可以做到感知应用层内容,针对内容定制化负载均衡策略。
负载均衡策略
-
轮询 -
权重轮询,根据不同服务器的负载能力,赋予不同的权重 -
随机 -
权重随机 -
一致性哈希 -
响应速度均衡,LB对内部服务器发出探测请求比如Ping,根据响应时间决定策略 -
最少连接均衡,LB进行连接数据记录,根据记录选择连接最少的服务器
服务端缓存
引入缓存会增加系统复杂度,非必要不需要引入缓存,引入缓存的目的:
-
缓解CPU压力,离线数据计算结果的缓存 -
环境IO压力,把原本对网络、磁盘的访问变为内存访问,顺带提升响应
选择缓存需要考虑以下四个方面:
-
吞吐量,缓存的吞吐量用OPS(Operation per second)衡量,反应了缓存的读写命令的效率 -
命中率,成功从缓存中获取的结果次数与总请求次数的比值,命中率越高,说明缓存的价值越大 -
扩展功能,额外的管理功能,比如失效时间控制、命中率统计等 -
分布式缓存
缓存并不是将所有数据都读取到内存中经久不衰,而是有一定的淘汰策略,比如:
-
FIFO,优先淘汰最早缓存的数据 -
LRU(Least Recent Used),优先淘汰最久未被访问的数据,Map加List实现,每次命中缓存把节点调整到List开头,淘汰时从List尾部开始淘汰,热点数据如果一段时间未被访问会有被淘汰风险 -
LFU(Least Frequently Used),优先淘汰最不经常使用的数据,通过计数器记录缓存数据的使用情况,每次访问缓存都需要更新计数器的额外开销,不能处理随时间变化的热度变化,比如短期访问频繁的数据不需要了很难被淘汰掉
分布式缓存和进程内缓存可以搭配使用,构成多级缓存:

缓存风险
-
缓存穿透 -
缓存击穿 -
缓存雪崩 -
缓存污染
缓存穿透,指查询数据库中不存在的数据,数据库中不存在,则缓存中一定不存在,流量会直接打到数据库,起不到缓解压力的作用。缓存穿透有两种解决方案:
-
业务逻辑本身无法避免缓存穿透,可以通过在一定时间内,把结果返回为空的Key插入缓存,使得一段时间中,该Key只会被穿透一次,后续如果数据库插入了Key值记录,则需要清理掉缓存的Key -
恶意流量攻击,在访问缓存之前使用布隆过滤器来解决
缓存击穿,指单个热点Key失效,导致请求全部达到数据库。缓存击穿有两种解决方案:
-
加锁同步,通过Key值加锁,使得只有一个线程可以进入数据库操作,重建缓存,其余线程重试或阻塞,当重建完成后,可以再次从缓存中获取 -
设置永不过期
缓存雪崩,指在某一个时间段内,缓存集中失效,造成缓存雪崩的原因可能是:
-
缓存系统宕机 -
因为预热机制可能有相同的过期时间
解决方案:
-
提示缓存系统的可用性,比如建设集群 -
缓存随机过期时间 -
服务降级、熔断、限流,保护数据库
缓存污染,指缓存中的数据与数据库中的数据不一致现象。解决方案(非强一致性,强一致性需要锁和事务保证,降低并发):
-
Cache Aside,旁路缓存模式 -
读数据时,先读缓存,如果没有,再读数据源,把数据存入缓存 -
写数据时,先写数据源,再失效缓存 -
Read/Write Through,读写穿透 -
Write Behind Caching,异步缓存写入
流量治理
容错性设计是微服务的一个核心原则,当服务越来越复杂,拆分出来的服务越来越多时,会面临两个问题:
-
由于某一个服务的不可用,导致依赖这个服务的其他服务无法工作,造成服务雪崩 -
服务虽然没有崩溃,但由于处理能力有限,当遇到突发大流量请求,大部分请求直到超时都无法完成处理
因此需要服务容错和流量控制机制来保证微服务的正常使用。
服务容错
容错性设计不能妥协的原因是分布式系统的不可靠性。
常见的容错策略:
-
故障转移(Failover),前提是服务具有幂等性,高可用的集群中,多数服务会部署多个副本,故障转移是指如果调用的服务器出现故障,系统不会立即返回失败,而是自动切换到其他副本。故障转移的容错策略一半会有调用次数限制 -
快速失败(Failfast),有一些业务场景是不允许故障转移的,对于非幂等的服务,重复调用可能会产生脏数据,此时应该快速失败。 -
安全失败(Failsafe),在一个调用链路的服务一般会有主路、旁路之分,有部分服务失败不影响核心业务的正确性,这些旁路逻辑实际调用失败了,也当作正确的来返回,这种策略称为安全失败策略。 -
沉默失败(Failsilent),当请求失败后,就默认服务提供者一定时间内无法再对外提供服务,不再向它分配请求流量,将错误隔离开 -
故障恢复(Failback),这种策略一般不会单独存在,作为其他容错策略的补充。故障恢复是指当服务调用出错后,调用失败的信息会存入消息队列,由系统自动开始异步重试(也需要服务幂等),适用于实时性不高的主路逻辑 -
并行调用(Forking),一开始同时向多个服务副本发起调用,只要有一个成功变成功,是一种再关键场景中用更高的执行成本换取成功概率的策略 -
广播调用(Broadcast),同时发起调用,所有请求都成功了才算成功
容错策略 | 优点 | 缺点 | 应用场景 |
---|---|---|---|
故障转移 | 系统自动处理,调用者对失败的信息不可见 | 增加调用时间,额外的资源开销 | 调用幂等服务 对调用时间不敏感的场景 |
快速失败 | 调用者有对失败的处理完全控制权 不依赖服务的幂等性 |
调用者必须正确处理失败逻辑,如果一味只是对外抛异常,容易引起雪崩 | 调用非幂等的服务 超时阈值较低的场景 |
安全失败 | 不影响主路逻辑 | 只适用于旁路调用 | 调用链中的旁路服务 |
沉默失败 | 控制错误不影响全局 | 出错的地方将在一段时间内不可用 | 频繁超时的服务 |
故障恢复 | 调用失败后自动重试,也不影响主路逻辑 | 重试任务可能产生堆积,重试仍然可能失败 | 调用链中的旁路服务 对实时性要求不高的主路逻辑也可以使用 |
并行调用 | 尽可能在最短时间内获得最高的成功率 | 额外消耗机器资源,大部分调用可能都是无用功 | 资源充足且对失败容忍度低的场景 |
广播调用 | 支持同时对批量的服务提供者发起调用 | 资源消耗大,失败概率高 | 只适用于批量操作的场景 |
容错服务设计模式:
断路器模式
例如Hystrix,断路器的基本思想是通过代理(断路器对象)一对一(一个远程服务对应一个断路器对象)接管服务调用者的请求,断路器持续监控并统计服务返回的成功、失败、超时、拒绝等结果,当出现故障(失败、超时、拒绝)的次数达到断路器的阈值时,断路器的状态变为OPEN,后续断路器的远程访问都将直接返回调用失败,而不会真正请求服务,是一种快速失败的策略。
断路器实际上是一种有限状态机,根据自身状态变化调整代理请求策略,一般有三种状态:
-
CLOSED,断路器关闭,请求会真正发送给服务提供者,断路器刚建立时默认处于这种状态,通过监视远程请求,决定是否进入OPEN状态 -
OPEN,断路器开启,此时不会进行远程请求,直接向服务调用者返回调用失败的信息,已实现快速失败策略 -
HALF OPEN,中间状态,断路器的自动故障恢复能力,当进入OPEN状态一段时间后,讲(又下一次请求而不是计数器触发)切换到HALF OPEN状态,执行一次真正的远程调用,根绝这次调用结果,转换为CLOSED或者OPEN状态,以实现断路器的弹性恢复
图示:

舱壁隔离模式
舱壁隔离模式是实现服务隔离的设计模式。
在调用外部服务时的故障主要分为三类:
-
失败,如400 Bad Request,500 Internal Server Error等 -
拒绝,如401 Unauthorized,403 Forbidden等 -
超时,如408 Request Timeout,504 Gateway Timeout等
其中超时故障更容易给调用者带来全局性的风险,因为只要请求不结束,就会一直站着线程不释放,为了不让某一个远程服务的局部失败演变成全局失败,这就是服务隔离的意义。
假设所有请求打在同一个线程池中,当服务调用超时,会阻塞一个线程,如果并发高,很快会导致无线程可用,一种可行的方法时为不同的服务设立单独的线程池,同时控制最大线程数,此时哪怕超时,也是当前服务超时,不会影响到全局。
Hystrix官网图示:

使用局部线程池由很多好处,当服务出现问题时可以隔离影响,当服务恢复后可以通过清理局部线程池,恢复服务调用,但有一个显著缺点,增加CPU的开销。
为应对CPU开销问题,还有一种更轻量级的控制服务最大连接数的办法,通过使用信号量机制,为服务设置一个私有的计数器,当开始调用时加1,返回结果后减1,当超过阈值便开始限流。
更上层一些,服务隔离可以按照功能、系统等条件来隔离,即使某个实例完全崩溃,也只是影响某一部分的用户,尽可能控制波及范围。一般来说,会在服务调用端或者Side Car上实现服务层面的隔离,在DNS或者网关实现系统层面的隔离。
重试模式
重试模式适合解决系统中的瞬时故障,简单说就是有可能自己恢复的临时故障,比如网络抖动、服务的临时过载(比如503 Bad Gateway),能否对与一个服务重试,应满足以下几个前提条件:
-
仅在主路逻辑的关键服务进行同步的重试 -
仅对瞬时故障的失败进行重试(虽然很难精确判断,但可以在HTTP状态码做初步判断,比如返回码是401 Unauthorized,则重试没有意义) -
仅重试幂等服务 -
重试要有明确的终止条件 -
超时终止 -
次数终止
重试次数应注意不能滥用,因为重试模式可以在网络链路多个环节去实现,比如客户端重试、网关重试、负载均衡器自动重试,比如在Zuul、Feign和Ribbon都重试,则总重试次数可能会是他们的乘机。
流量控制
一个系统的运算、存储、网络带宽资源都不是无限的,当系统资源不足以支撑外部突发流量时,应有所取舍,即限流。
健壮的系统需要做到流量控制,需要解决以下问题:
-
依据什么限流?无法静态决定,需要根据系统此前一段时间的运行状况或者未来一段时间的预测情况动态决定 -
具体如何限流?用何种限流算法和设计模式 -
超额流量如何处理? -
否定式限流,直接返回失败,或者强制进入降级逻辑 -
阻塞市限流,请求排队,阻塞一段时间后继续处理
流量控制依赖于哪些指标能反映系统的流量压力:
-
每秒事务数(TPS,Transaction per Second),衡量系统吞吐量的最终指标 -
每秒请求数(HPS,Hit per Second),每秒从客户端发向服务端的请求数 -
每秒查询数(QPS,Query per Second),一台服务器能够响应的查询次数
目前直接针对TPS限流实际上是很难操作的,一般更倾向于使用HPS作为首选限流指标。
常见的限流设计模式有以下几种:
-
流量计数器 -
滑动时间窗 -
漏桶 -
令牌桶
流量计数器是根据当前时刻的流量计数结果是否超过阈值来决定是否限流。
流量计数器的缺陷根源在于针对时间点的离散统计。
滑动窗口模式可以解决流量计数器的缺陷,可以保证在一个统计周期内,就能控制请求次数不超过阈值,这种模式适用于否定式限流,超过阈值的流量必须强制失败或者降级,很难阻塞等待,起不到削峰填谷的作用。
在计算机网络中,流量整形是用来描述如何限制网络设备的流量突变,使得网络报文以比较均匀的速度向外发送,通常通过缓冲区实现,报文发送速度过快时先存储在缓冲区,在控制算法的调节下均匀发送被缓冲的报文。常用的控制算法有漏桶算法和令牌桶算法。
漏桶算法,一个请求对象作为元素的先入先出队列,当队列满了,则认为桶满了,多余的请求会被丢弃。漏桶的两个参数比较重要:
-
桶的大小,如果桶设置过大,则服务可能遭遇大流量而失去保护作用,太小会误杀请求 -
流出速率,一般是个固定值,无法动态伸缩
由于流出速率是固定值,无法处理徒增流量。

令牌桶算法解决了这一缺陷,假设要限制系统在X秒内最大请求次数不超过Y,则每间隔X/Y时间往同种放一个令牌,请求需要先获取令牌,才能进入系统处理,如果拿不到令牌,则失败。可以获取多个令牌机制保证可以处理一定程度的突发流量。

比如Guava限流器RateLimiter
业务上一般通过Sentinel进行限流,但开源的Sentine是阉割版的,需要进行一定程度的改造。
官网介绍:
Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。

架构安全
为了保证系统安全,合格的系统应包含且不限于以下问题的具体解决方案:
-
认证(Authentication):系统如何识别操作用户的身份?(你是谁?) -
授权(Authorization):系统如何控制一个用户的权限?(你能干什么?) -
凭证(Credential):授权后用户的凭证如何维护?(你如何证明?) -
保密(Confidentiality):如何保护敏感数据不被第三方窃听、篡改? -
传输(Transport Security):如何保证通过网络传输的信息不会被拦截、篡改? -
验证(Verification):如何验证数据?验证用户?验证权限是否合规?
认证
认证并不是简单的用户名密码认证,帐权在复杂系统中是非常复杂的一块,主流的认证方式有以下三种:
-
通信信道上的认证,在建立通信信道前,需要先认证,比如SSL传输层的认证 -
通信协议上的认证,在请求资源前,需要先认证,比如基于HTTP协议的认证 -
通信内容上的认证,在使用服务前,需要先认证,比如基于Web内容的认证
HTTP认证
RFC 7235定义了HTTP协议的通用认证框架,要求所有HTTP协议的服务器,在未授权的用户意图访问服务端保护区域的资源时,应返回401 Unauthorized状态码,并且在响应报文中附带以下两个Header之一:
-
WWW-Authenticate: <认证方案> realm=<保护区域的描述信息>
-
Proxy-Authenticate: <认证方案> realm=<保护区域的描述信息>
接收到这个响应后,客户端必须按照响应中的认证方案,在请求资源的报文Header中加入以下一种:
-
Authorization: <认证方案> <凭证内容>
-
Proxy-Authenticate: <认证方案> <凭证内容>
例如使用Nginx基本认证:
location /status {
auth_basic "Access to the staging site";
auth_basic_user_file /etc/apache2/.htpasswd;
}
访问该资源需要输入.htpasswd中的用户名和密码才可以访问。
常见HTTP认证方案:
-
Basic,使用Base64编码,风险很大 -
Digest,HTTP摘要认证 -
Bearer,基于OAuth 2认证 -
HOBA,基于自签名的认证方案
Web认证
依靠内容进行认证,而不是传输协议来实现的认证方式,比如通过表单等。
W3C在2019年批准了Web内容认证的标准WebAuthn(涵盖了注册和认证两个流程,抛弃表单认证,使用生物识别或者实体密钥进行认证)
在Java生态中,有Apache Shiro和Spring Security两个安全框架,功能包括:
-
认证 -
安全上下文,比如获取权限、角色等 -
授权 -
密码的存储和校验
凭证
每一种认证、授权的最终目标是拿到访问令牌,令牌该怎么存储?怎么表示?
凭证的实现一般有两种方式:
-
Cookie-Session -
JWT
Cookie-Session不在分布式场景会比较麻烦,因为要共享session信息,需要权衡CAP:
-
牺牲一致性,根据用户IP、ID为Session分配节点,某个用户的请求只分配到某个节点,每个节点保存着不重复的某一部分session状态 -
牺牲可用性,每个节点组播session,同步代价成本大 -
牺牲分区容错性,单点存储
JWT实际上是明文传输的(Base64URL编码后的),JWT可以防止篡改,但不能防止泄露。
JWT主要有三个部分(用.
隔开):
-
令牌头,描述了类型和令牌签名的算法 -
负载,自定义信息,官方建议: -
iss,签发人 -
exp,令牌过期时间 -
sub,主题 -
aud,令牌受众 -
nbf,令牌生效时间 -
iat,令牌签发时间 -
jti,令牌编号 -
签名,计算方式为 JWT头签名算法函数(base64UrlEncode(header) + "." +base64UrlEncode(payload),服务器保存的密钥)
可以访问JWT官网,以下为一个示例:

JWT的缺点:
-
令牌难以主动失效 -
更容易遭受重放攻击,JWT层面解决重复攻击的手段代价大, -
全局序列号 -
Nonce字符串,比如时间戳 -
建议信道层面解决,比如HTTPS -
只能携带有限数据(不能超过HTTP的Header大小限制)
授权
授权涉及以下两个问题:
-
确保授权的过程可靠,如何让第三方系统能访问资源且保证不泄露用户敏感数据,常用的授权协议比如OAuth 2.0和SAML 2.0 -
确保授权的结果可控,授权的结果用于对功能或者资源的访问控制,常见的权限控制模型有: -
自主访问控制(DAC) -
强制访问控制(MAC) -
基于属性的访问控制(ABAC) -
基于角色的访问控制(RBAC)
OAuth 2.0
OAuth 2.0是面向第三方应用的认证授权模式,比如在某个网站的开发者中心,注册一个应用,请求授权接口登录认证获取授权码,拿授权码请求接口获取Token和Refresh Token的流程,这个Token就是令牌,哪怕令牌泄露,密码也不会泄露,而且可以通过令牌限定资源权限和时效性。
OAuth 2.0有以下几个关键角色:
-
第三方应用,需要得到授权且访问资源的应用 -
授权服务器,提供授权的服务器 -
资源服务器,提供第三方应用所需要资源的服务器 -
资源所有者,拥有授权权限的角色
OAuth 2.0 一共有四种不同的授权模式:
-
授权码模式 -
隐式授权码模式,略去授权码流程 -
密码模式,仅限于用户对第三方应用高度可信的情况下,用户通过用户名和密码注册第三方应用,第三方应用通过用户名和秘密换取令牌 -
客户端模式,应用申请授权,授权服务器发放令牌,这种模式通常用于服务间合法调用
授权码模式的流程如下:
-
第三方应用现在授权服务器上注册(比如XXX开发者中心注册一个应用),填入回调地址,注册应用后会获得应用ID和一个密钥Secret用于接下来的授权 -
用户通过访问授权页面(附带应用ID)并登录认证自身,授权服务器根据应用ID确认授权给哪个应用 -
如果用户同意授权,将重定向到回调地址,并附带认证码 -
用户通过授权服务器提供的服务地址,用认证码和密钥secret换取令牌和刷新令牌 -
携带令牌访问资源服务器提供的资源
OAuth 2.0 有几个点需要理解:
-
避免冒充第三方应用骗取授权,用户注册应用时,会有一个密钥secret,只要secret不泄露,则不会被冒充 -
为什么要用授权码换取令牌?因为HTTP重定向对用户是可见的,如果直接返回令牌可能会被其他应用获取拦截,如果是返回授权码,授权码配合密钥secret换取令牌,由于secret是用户保管的,不存在被拦截风险,且再次通过接口获取的令牌是通过HTTPS传输的,也大大减少了被拦截的风险
RBAC(Role-Based Access Control)模型
访问控制模型实质上是为了解决一个问题:谁拥有什么权限去操作哪些资源?
RBAC模型是基于角色的访问控制模型,权限挂角色,角色挂用户。
访问控制模型的基本目的是为了管理垂直权限和水平权限,垂直权限即功能权限,水平权限即数据权限。
复杂系统中,权限是很复杂的,需要根据业务场景具体定制(尤其是数据权限)。
保密
密码学算法的三个重要用途:
-
摘要,不可逆,主要是用在源信息不被泄露的前提下鉴别真伪 -
加密,加密是可逆的 -
签名,用于鉴定信息是否篡改,是否是可信的
这三种密码学算法的对比:
类型 | 特点 | 算法实现 | 用途 | 局限 |
---|---|---|---|---|
哈希摘要 | 不可逆、输出长度固定 | MD2/4/5、SHA0/1/256/512 | 摘要 | 无法解密 |
对称加密 | 加密和解密是同一个密钥、加密明文长度不受限制 | DES、AES、RC4、IDEA | 加密 | 如何安全传输密钥 |
非对称加密 | 加密和解密使用的是不同的密钥、明文长度不能超过公钥长度 | RSA、BCDSA、EGamal | 签名、传递密钥 | 性能与加密明文长度受限 |
加密分为两种:
-
端的加解密(客户端、服务端存储敏感信息时的加解密) -
链路的加解密(通过网络链路传输时的加解密)
以用户登录为例,列举几种不同强度的保密手段:
-
摘要代替明文,比如MD5,即使被泄露,也不会被逆推出原始信息,但弱密码有被彩虹表攻破的风险 -
加盐哈希,一定程度上能防止彩虹表攻击,但无法阻止加密结果被拦截,被攻击者直接发送加密结果给服务端进行冒认 -
动态加盐,每次密码向服务端传输时都加入动态盐值,每次加密结果都不相同,可以有效防止被拦截二次调用,也难以阻止对其他服务的重放攻击 -
动态令牌,在网关增加校验逻辑,可以做到防止重放攻击,但不能解决传输过程中被嗅探的问题 -
启用HTTPS防御链路上的恶意嗅探,有客户端被攻破伪造根证书风险、服务端被攻破证书泄露等问题 -
物理设备校验,比如银行使用独立于客户端的存储证书的物理设备(U盾)避免客户端证书被伪造
客户端的加密在保证信息不被窃取实际上是没有多大意义,启用HTTPS是最好的方案,但现实中大多依然会在客户端加密敏感信息,主要是因为防止明文密码被滥用,比如日志、数据库明文展示密码等。
一个流程Demo:
密码创建的过程:
注册输入密码 123456
客户端哈希摘要
简单哈希,比如MD2/4/5、SHA1/256/512、BCrypt、PBKDF1/2等 加盐哈希,固定盐值或者伪动态盐值(用户名、日期等,不需要网络通信就可以得到的盐值) 慢哈希(比如BCrypt算法),接受盐值和执行成本两个参数,让哈希计算变慢,可以大幅增加被暴力破解的成本 服务端存储,服务端主要防御被拖库后针对固定盐值的批量彩虹表攻击,可以为客户端传输过来的密码摘要再做一次随机盐值(密码学安全伪随机数生成器),把这个盐值和客户端传输的密码摘要再做一次哈希,把结果和盐值存入数据库中 校验的过程:
客户端加密传输 服务端接收到客户端的密码摘要,从数据库中根据用户查询对应的密文和盐值,采用同样的哈希算法,计算客户端密码摘要与服务端存储的密文对比,相同则密码正确,不同则密码错误
传输安全性保证
HTTPS,见HTTPS基本原理
校验验证
后端需要对前端传递的数据进行校验,且一定要校验,为了增强程序的健壮性,一般对传输过来的参数都是持不可信任的态度去做处理。
参考资料
-
从单体架构到微服务架构演进 -
《凤凰架构》 -
Service Mesh-互联网架构演进 -
MDN -
DNS解析的过程是什么? -
网络基础知识——A记录和CNAME记录的区别 -
LVS居然是这样的 -
LVS 和 Keepalived 的原理介绍和配置实践 -
最牛一篇布隆过滤器详解 -
图解缓存击穿、缓存穿透、缓存雪崩的区别 -
美团二面:Redis与MySQL双写一致性如何保证? -
一文读懂HTTP Basic身份认证 -
RBAC权限管理模型:基本模型及角色模型解析及举例 -
基于 RBAC 权限模型的架构设计 -
JSON Web 令牌(JWT)是如何保护 API 的? -
面试官:啥时请求重放? -
对抗明文口令泄露——Web前端慢Hash -
限流算法之令牌桶与漏桶算法
原文始发于微信公众号(erpang coding):微服务基础架构
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/37343.html