服务架构演进史
原始分布式时代
UNIX 的分布式设计哲学 保持接口与实现的简单性,比系统的任何其他属性,包括准确性、一致性和完整性,都来得更加重要
在 20 世纪 70 年代末期到 80 年代初,计算机科学刚经历了从以大型机为主向以微型机为主的蜕变,当时计算机硬件局促的运算处理能力,已直接妨碍到了在单台计算机上信息系统软件能够达到的最大规模
某个功能能够进行分布式,并不意味着它就应该进行分布式,强行追求透明的分布式操作,只会自寻苦果
20 世纪 80 年代正是摩尔定律开始稳定发挥作用的黄金时期,信息系统进入了以单台或少量几台计算机即可作为服务器来支撑大型信息系统运作的单体时代
单体系统时代
单体架构(Monolithic) “单体”只是表明系统中主要的过程调用都是进程内调用,不会发生进程间通信,仅此而已
对于小型系统——即由单台机器就足以支撑其良好运行的系统,单体不仅易于开发、易于测试、易于部署,且由于系统中各个功能、模块、方法的调用过程都是进程内调用,不会发生进程间通信(Inter-Process Communication,IPC),因此也是运行效率最高的一种架构风格
优点
易于开发、易于测试、易于部署
由于所有代码都运行在同一个进程空间之内,所有模块、方法的调用都无须考虑网络分区、对象复制这些麻烦的事和性能损失
缺点
如果任何一部分代码出现了缺陷,过度消耗了进程空间内的资源,所造成的影响也是全局性的、难以隔离的。譬如内存泄漏、线程爆炸、阻塞、死循环等问题,都将会影响整个程序,而不仅仅是影响某一个功能、模块本身的正常运作。如果消耗的是某些更高层次的公共资源,譬如端口号或者数据库连接池泄漏,影响还将会波及整台机器,甚至是集群中其他单体副本的正常工作
由于所有代码都共享着同一个进程空间,不能隔离,也就无法(其实还是有办法的,譬如使用 OSGi 这种运行时模块化框架,但是很别扭、很复杂)做到单独停止、更新、升级某一部分代码,所以从可维护性来说,单体系统也是不占优势的
难以技术异构,每个模块的代码都通常需要使用一样的程序语言,乃至一样的编程框架去开发。单体系统的技术栈异构并非一定做不到,譬如 JNI 就可以让 Java 混用 C 或 C++,但这通常是迫不得已的,并不是优雅的选择
单体系统很难兼容“Phoenix”的特性。这种架构风格潜在的观念是希望系统的每一个部件,每一处代码都尽量可靠,靠不出或少出缺陷来构建可靠系统。然而战术层面再优秀,也很难弥补战略层面的不足,单体靠高质量来保证高可靠性的思路,在小规模软件上还能运作良好,但系统规模越大,交付一个可靠的单体系统就变得越来越具有挑战性
SOA时代
SOA 架构(Service-Oriented Architecture) 面向服务的架构是一次具体地、系统性地成功解决分布式服务主要问题的架构模式
- 烟囱式架构, 完全不与其他相关信息系统进行互操作或者协调工作的设计模式
- 微内核架构(Microkernel Architecture):也被称为插件式架构(Plug-in Architecture)将主数据,连同其他可能被各子系统使用到的公共服务、数据、资源集中到一块,成为一个被所有业务系统共同依赖的核心(Kernel),具体的业务系统以插件模块(Plug-in Modules)的形式存在,这样可提供可扩展的、灵活的、天然隔离的功能特性
- 事件驱动架构(Event-Driven Architecture):在子系统之间建立一套事件队列管道(Event Queues),来自系统外部的消息将以事件的形式发送至管道中,各个子系统从管道里获取自己感兴趣、能够处理的事件消息,也可以为事件新增或者修改其中的附加信息,甚至可以自己发布一些新的事件到管道队列中去,如此,每一个消息的处理者都是独立的,高度解耦的,但又能与其他处理者(如果存在该消息处理者的话)通过事件管道进行互动
- 领导制定技术标准的组织 Open CSA
- 明确了采用 SOAP 作为远程调用的协议,依靠 SOAP 协议族(WSDL、UDDI 和一大票 WS-*协议)来完成服务的发布、发现和治理
- 利用一个被称为企业服务总线(的消息管道来实现各个子系统之间的通信交互
- 使用服务数据对象来访问和表示数据
- 使用服务组件架构来定义服务封装的形式和服务运行的容器
缺点
过于严格的规范定义带来过度的复杂性。而构建在 SOAP 基础之上的 ESB、BPM、SCA、SDO 等诸多上层建筑,进一步加剧了这种复杂性
微服务时代
微服务架构(Microservices)
微服务是一种通过多个小型服务组合来构建单个应用的架构风格,这些服务围绕业务能力而非特定的技术标准来构建。各个服务可以采用不同的编程语言,不同的数据存储技术,运行在不同的进程之中。服务采取轻量级的通信机制和自动化的部署机制实现通信与运维。
微服务追求的是更加自由的架构风格,摒弃了几乎所有 SOA 里可以抛弃的约束和规定,提倡以“实践标准”代替“规范标准”
后微服务时代
后微服务时代(Cloud Native)
从软件层面独力应对微服务架构问题,发展到软、硬一体,合力应对架构问题的时代,此即为“后微服务时代”
以 Docker Swarm、Apache Mesos 与 Kubernetes 为主要竞争者的“容器编排战争”终于有了明确的结果,Kubernetes 登基加冕是容器发展中一个时代的终章,也将是软件架构发展下一个纪元的开端
传统 Spring Cloud 与 Kubernetes 提供的解决方案对比
Kubernetes | Spring Cloud |
---|---|
弹性伸缩 | Autoscaling N/A |
服务发现 | KubeDNS, CoreDNS Spring Cloud Eureka |
配置中心 | ConfigMap, Secret Spring Cloud Config |
服务网关 | Ingress Controller Spring Cloud Zuul |
负载均衡 | Load Balancer Spring Cloud Ribbon |
服务安全 | RBAC API Spring Cloud Security |
跟踪监控 | Metrics API, Dashboard Spring Cloud Turbine |
降级熔断 | N/A Spring Cloud Hystrix |
仅从功能上看,单纯的 Kubernetes 反而不如之前的 Spring Cloud 方案。这是因为有一些问题处于应用系统与基础设施的边缘,使得完全在基础设施层面中确实很难精细化地处理。通过 Spring Cloud 这类应用代码实现的微服务中并不难处理,既然是使用程序代码来解决问题,只要合乎逻辑,想要实现什么功能,只受限于开发人员的想象力与技术能力,但基础设施是针对整个容器来管理的,粒度相对粗旷,只能到容器层面,对单个远程服务就很难有效管控
为了解决这一类问题,虚拟化的基础设施很快完成了第二次进化,引入了今天被称为“服务网格”(Service Mesh)的“边车代理模式”(Sidecar Proxy)
由系统自动在服务容器(通常是指 Kubernetes 的 Pod)中注入一个通信代理服务器,以类似网络安全里中间人攻击的方式进行流量劫持,在应用毫无感知的情况下,悄然接管应用所有对外通信。这个代理除了实现正常的服务间通信外(称为数据平面通信),还接收来自控制器的指令(称为控制平面通信),根据控制平面中的配置,对数据平面通信的内容进行分析处理
无服务时代
无服务架构(Serverless)如果说微服务架构是分布式系统这条路的极致,那无服务架构,也许就是“不分布式”的云端系统这条路的起点
只涉及两块内容:后端设施(Backend)和函数(Function)
- 后端设施是指数据库、消息队列、日志、存储,等等这一类用于支撑业务逻辑运行,但本身无业务含义的技术组件,这些后端设施都运行在云中,无服务中称其为“后端即服务”(Backend as a Service,BaaS)
- 函数是指业务逻辑代码,这里函数的概念与粒度,都已经很接近于程序编码角度的函数了,其区别是无服务中的函数运行在云端,不必考虑算力问题,不必考虑容量规划(从技术角度可以不考虑,从计费的角度你的钱包够不够用还是要掂量一下的),无服务中称其为“函数即服务”(Function as a Service,FaaS)
无服务架构对一些适合的应用确实能够降低开发和运维环节的成本,譬如不需要交互的离线大规模计算,又譬如多数 Web 资讯类网站、小程序、公共 API 服务、移动应用服务端等都契合于无服务架构所擅长的短链接、无状态、适合事件驱动的交互形式;但另一方面,对于那些信息管理系统、网络游戏等应用,又或者说所有具有业务逻辑复杂,依赖服务端状态,响应速度要求较高,需要长链接等这些特征的应用,至少目前是相对并不适合的
顺序上笔者将“无服务”安排到了“微服务”和“云原生”时代之后,但它们两者并没有继承替代关系,笔者相信软件开发的未来不会只存在某一种“最先进的”架构风格,多种具针对性的架构风格同时并存,是软件产业更有生命力的形态
架构师的视角
访问远程服务
远程服务调用
调用本地方法
- 传递方法参数
- 确定方法版本
- 执行被调方法
- 返回执行结果
1/4依赖栈内存, 2依赖程序语言定义
进程间通信
- 管道, 在进程间传递少量的字符流或字节流
- 信号, 通知目标进程有某种事件发生, kill即shell向进程发送信号
- 信号量, wait() notify()
- 共享内存
- Socket, 支持远程
分布式计算的八宗罪
- The network is reliable —— 网络是可靠的
- Latency is zero —— 延迟是不存在的
- Bandwidth is infinite —— 带宽是无限的
- The network is secure —— 网络是安全的
- Topology doesn’t change —— 拓扑结构是一成不变的
- There is one administrator —— 总会有一个管理员
- Transport cost is zero —— 不必考虑传输成本
- The network is homogeneous —— 网络是同质化的
RPC的三个基本问题
- 如何表示数据, 各种协议
- 如何传递数据, 除了传输层的UDP/TCP, 还包括应用层的wire protocal, 异常、超时、安全、认证、授权、事务等
- 如何确定方法
RPC的发展方向
- 面向对象, 不满足于 RPC 将面向过程的编码方式带到分布式,希望在分布式系统中也能够进行跨进程的面向对象编程,代表为 RMI、.NET Remoting
- 性能, 代表为 gRPC 和 Thrift, 决定 RPC 性能的主要就两个因素:序列化效率和信息密度。序列化效率很好理解,序列化输出结果的容量越小,速度越快,效率自然越高;信息密度则取决于协议中有效荷载(Payload)所占总传输数据的比例大小,使用传输协议的层次越高,信息密度就越低,SOAP 使用 XML 拙劣的性能表现就是前车之鉴。gRPC 和 Thrift 都有自己优秀的专有序列化器,而传输协议方面,gRPC 是基于 HTTP/2 的,支持多路复用和 Header 压缩,Thrift 则直接基于传输层的 TCP 协议来实现,省去了额外应用层协议的开销
- 简化, 代表为 JSON-RPC
REST 设计风格
表征状态转移
- 面向过程编程时,为什么要以算法和处理过程为中心,输入数据,输出结果?是为了符合计算机世界中主流的交互方式
- 面向对象编程时,为什么要将数据和行为统一起来、封装成对象?是为了符合现实世界的主流的交互方式
- 面向资源编程时,为什么要将数据(资源)作为抽象的主体,把行为看作是统一的接口?是为了符合网络世界的主流的交互方式
REST六大原则
- 服务端与客户端分离
- 无状态
- 可缓存
- 分层系统
- 统一接口
- 按需代码
REST好处
- 降低的服务接口的学习成本
- 资源天然具有集合与层次结构
- REST 绑定于 HTTP 协议
REST缺点
- REST 与 HTTP 完全绑定,不适合应用于要求高性能传输的场景中
- REST 没有传输可靠性支持
- REST 缺乏对资源进行“部分”和“批量”的处理能力
一种理论上较优秀的可以解决以上这几类问题的方案是GraphQL,这是由 Facebook 提出并开源的一种面向资源 API 的数据查询语言。比起依赖 HTTP 无协议的 REST,GraphQL 可以说是另一种“有协议”的、更彻底地面向资源的服务方式。然而凡事都有两面,离开了 HTTP,它又面临着几乎所有 RPC 框架所遇到的那个如何推广交互接口的问题。
事务处理
- 一致性(Consistency)系统中所有的数据都是符合期望的,且相互关联的数据之间不会产生矛盾
- 原子性(Atomic)在同一项业务处理过程中,事务保证了对多个数据的修改,要么同时成功,要么同时被撤销
- 隔离性(Isolation)在不同的业务处理过程中,事务保证了各自业务正在读、写的数据互相独立,不会彼此影响
- 持久性(Durability)事务应当保证所有成功被提交的数据修改都能够正确地被持久化,不丢失数据
A、I、D 是手段,C 是目的
当一个服务只使用一个数据源时,通过 A、I、D 来获得一致性是最经典的做法,也是相对容易的。此时,多个并发事务所读写的数据能够被数据源感知是否存在冲突,并发事务的读写在时间线上的最终顺序是由数据源来确定的,这种事务间一致性被称为__内部一致性__
当一个服务使用到多个不同的数据源,甚至多个不同服务同时涉及多个不同的数据源时,问题就变得相对困难了许多。此时,并发执行甚至是先后执行的多个事务,在时间线上的顺序并不由任何一个数据源来决定,这种涉及多个数据源的事务间一致性被称为__外部一致性__
本地事务
直接依赖于数据源本身提供的事务能力来工作的,在程序代码层面,最多只能对事务接口做一层标准化的包装(如 JDBC 接口),与后续介绍的 XA、TCC、SAGA 等主要靠应用程序代码来实现的事务有着十分明显的区别
实现原子性和隔离性
写入中间状态与崩溃都不可能消除,所以如果不做额外保障措施的话,将内存中的数据写入磁盘,并不能保证原子性与持久性
使用 Commit Logging 实现
Commit Logging基于ARIES理论
将修改数据这个操作所需的全部信息,包括修改什么数据、数据物理上位于哪个内存页和磁盘块中、从什么值改成什么值,等等,以日志的形式——即仅进行顺序追加的文件写入的形式(这是最高效的写入方式)先记录到磁盘中。只有在日志记录全部都安全落盘,数据库在日志中看到代表事务成功提交的“提交记录”(Commit Record)后,才会根据日志上的信息对真正的数据进行修改,修改完成后,再在日志中加入一条“结束记录”(End Record)表示事务已完成持久化
Write-Ahead Logging, 当变动数据写入磁盘前,必须先记录 Undo Log,注明修改了哪个位置的数据、从什么值改成什么值,等等。以便在事务回滚或者崩溃恢复时根据 Undo Log 对提前写入的数据变动进行擦除
实现隔离性
现代数据库均提供了以下三种锁
- 写锁(Write Lock,也叫作排他锁,eXclusive Lock,简写为 X-Lock)
- 读锁(Read Lock,也叫作共享锁,Shared Lock,简写为 S-Lock)
- 范围锁(Range Lock), 在MySQL中实现为gap lock
四个隔离级别
隔离级别 | 使用的锁 | 存在问题 |
---|---|---|
读未提交(Read Uncommitted) | 写锁持续至事务结束,无读锁 | 脏读:在事务执行过程中,一个事务读取到了另一个事务未提交的数据(读数据不需要加读锁,写锁就限制不了读操作了) |
读已提交(Read Committed) | 写锁持续至事务结束,读锁在操作完成后马上释放 | 不可重复读:在事务执行过程中,对同一行数据的两次查询得到了不同的结果,即读到了另一个事务已提交的数据(写锁释放太早,限制不到其他事务加读锁) |
可重复读(Repeatable Read) | 写锁、读锁持续至事务结束 | 幻读:在事务执行过程中,两个完全相同的范围查询得到了不同的结果集,没有范围锁来禁止在该范围内插入新的数据 |
可串行化(Serializable) | 写锁、读锁、范围所持续至事务结束 | - |
MVVC
幻读、不可重复读、脏读等问题都是由于一个事务在读数据过程中,受另外一个写数据的事务影响而破坏了隔离性,针对这种“一个事务读+另一个事务写”的隔离问题,近年来有一种名为“多版本并发控制”(Multi-Version Concurrency Control,MVCC)的无锁优化方案被主流的商业数据库广泛采用。MVCC 是一种读取优化策略,它的“无锁”是特指读取时不需要加锁。MVCC 的基本思路是对数据库的任何修改都不会直接覆盖之前的数据,而是产生一个新版副本与老版本共存,以此达到读取时可以完全不加锁的目的
全局事务
单个服务使用多个数据源场景
X/Open组织(后来并入了The Open Group)提出了一套名为X/Open XA(XA 是 eXtended Architecture 的缩写)的处理事务架构,其核心内容是定义了全局的事务管理器(Transaction Manager,用于协调全局事务)和局部的资源管理器(Resource Manager,用于驱动本地事务)之间的通信接口
Java 中专门定义了JSR 907 Java Transaction API,基于 XA 模式在 Java 语言中的实现了全局事务处理的标准,这也就是我们现在所熟知的 JTA。JTA 最主要的两个接口是:
- 事务管理器的接口:javax.transaction.TransactionManager。这套接口是给 Java EE 服务器提供容器事务(由容器自动负责事务管理)使用的,还提供了另外一套javax.transaction.UserTransaction接口,用于通过程序代码手动开启、提交和回滚事务。
- 满足 XA 规范的资源定义接口:javax.transaction.xa.XAResource,任何资源(JDBC、JMS 等等)如果想要支持 JTA,只要实现 XAResource 接口中的方法即可
2PC
XA 将事务提交拆分成为两阶段过程,“两段式提交”(2 Phase Commit,2PC)协议,协调者、参与者都是可以由数据库自己来扮演的,不需要应用程序介入。协调者一般是在参与者之间选举产生的,而应用程序相对于数据库来说只扮演客户端的角色
准备阶段:又叫作投票阶段,在这一阶段,协调者询问事务的所有参与者是否准备好提交,参与者如果已经准备好提交则回复 Prepared,否则回复 Non-Prepared。对于数据库来说,准备操作是在重做日志中记录全部事务提交操作所要做的内容,它与本地事务中真正提交的区别只是暂不写入最后一条 Commit Record 而已,这意味着在做完数据持久化后并不立即释放隔离性,即仍继续持有锁,维持数据对其他非事务内观察者的隔离状态
提交阶段:又叫作执行阶段,协调者如果在上一阶段收到所有事务参与者回复的 Prepared 消息,则先自己在本地持久化事务状态为 Commit,在此操作完成后向所有参与者发送 Commit 指令,所有参与者立即执行提交操作;否则,任意一个参与者回复了 Non-Prepared 消息,或任意一个参与者超时未回复,协调者将自己的事务状态持久化为 Abort 之后,向所有参与者发送 Abort 指令,参与者立即执行回滚操作。对于数据库来说,这个阶段的提交操作应是很轻量的,仅仅是持久化一条 Commit Record 而已,通常能够快速完成,只有收到 Abort 指令时,才需要根据回滚日志清理已提交的数据,这可能是相对重负载的操作
2PC的前提条件:
必须假设网络在提交阶段的短时间内是可靠的,即提交阶段不会丢失消息
必须假设因为网络分区、机器崩溃或者其他原因而导致失联的节点最终能够恢复,不会永久性地处于失联状态,当失联机器一旦恢复,就能够从日志中找出已准备妥当但并未提交的事务数据,并向协调者查询该事务的状态,确定下一步应该进行提交还是回滚操作
2PC的缺点:
单点问题:协调者
性能问题:要经过两次远程服务调用,三次数据持久化
一致性风险:需要前提条件
3PC
- canCommit,协调者让每个参与的数据库根据自身状态,评估该事务是否有可能顺利完成
- preCommit
- doCommit,未收到协调者返回,参与者默认的操作策略将是提交事务
将2PC的prepare阶段分成两部分,增加了评估阶段,在事务需要回滚的场景中,三段式的性能通常是要比两段式好很多的,但在事务能够正常提交的场景中,两者的性能都依然很差,甚至三段式因为多了一次询问,还要稍微更差一些
缺点:单点问题、性能问题优于2PC,一致性风险大于2PC
分布式事务
多个服务同时访问多个数据源
CAP
一个分布式的系统中,涉及共享数据问题时,以下三个特性最多只能同时满足其中两个:
一致性(Consistency):代表数据在任何时刻、任何分布式节点中所看到的都是符合预期的
可用性(Availability):代表系统不间断地提供服务的能力,可靠性使用平均无故障时间(Mean Time Between Failure,MTBF)来度量;可维护性使用平均可修复时间(Mean Time To Repair,MTTR)来度量。可用性衡量系统可以正常使用的时间与总时间之比,其表征为:A=MTBF/(MTBF+MTTR),即可用性是由可靠性和可维护性计算得出的比例值,譬如 99.9999%可用,即代表平均年故障修复时间为 32 秒
分区容忍性(Partition Tolerance):代表分布式环境中部分节点因网络原因而彼此失联后,即与其他节点形成“网络分区”时,系统仍能正确地提供服务的能力
CAP舍弃其中一个:
CA,不追求分区容忍性即认为节点之间通信永远可靠,不是分布式系统
CP,网络分区后服务不可用,退化为全局事务,使用2PC/3PC,用于对数据要求很高的系统,HBase
AP,网络分区后服务可用但不同实例返回的数据可能不同,大部分nosql系统和分布式缓存
可靠事件队列 BASE
TCC 事务
SAGA 事务
透明多级分流系统
系统中不同的组件:
- 有一些部件位于客户端或网络的边缘,能够迅速响应用户的请求,避免给后方的 I/O 与 CPU 带来压力,典型如本地缓存、内容分发网络、反向代理等。
- 有一些部件的处理能力能够线性拓展,易于伸缩,可以使用较小的代价堆叠机器来获得与用户数量相匹配的并发性能,应尽量作为业务逻辑的主要载体,典型如集群中能够自动扩缩的服务节点。
- 有一些部件稳定服务对系统运行有全局性的影响,要时刻保持着容错备份,维护着高可用性,典型如服务注册中心、配置中心。
- 有一些设施是天生的单点部件,只能依靠升级机器本身的网络、存储和运算性能来提升处理能力,如位于系统入口的路由、网关或者负载均衡器(它们都可以做集群,但一次网络请求中无可避免至少有一个是单点的部件)、位于请求调用链末端的传统关系数据库等
两条原则:
尽可能减少单点部件
奥卡姆剃刀原则
几种优化:
- 客户端缓存
- 域名解析
- 传输链路
- 内容分发网络
- 负载均衡
- 服务端缓存, 介绍了传统意义上缓存的各个方面
架构安全性
认证
授权
- RBAC, user/role/authority/permission/resource
- OAuth2, 解决第三方应用的认证授权协议
凭证
- Cookie-Session, 受制于CAP理论
- JWT, header+payload+sign
保密
传输
验证
分布式的基石
分布式共识算法
Paxos
Multi Paxos
Multi Paxos 对 Basic Paxos 的核心改进是增加了“选主”的过程
Raft 算法: 分布式系统中如何对某个值达成一致
- 如何选主(Leader Election)
- 如何把数据复制到各个节点上(Entity Replication)
- 如何保证过程是安全的(Safety)
Raft 是 Etcd、LogCabin、Consul 等重要分布式程序的实现基础, ZooKeeper 的 ZAB 算法与 Raft 的思路也非常类似,这些算法都被认为是 Multi Paxos 的等价派生实现。
Gossip 协议
- 强一致性, 尽管系统内部节点可以存在不一致的状态,但从系统外部看来不一致的情况并不会被观察到,所以整体上看系统是强一致性的, Paxos、Raft、ZAB
- 最终一致性, 系统中不一致的状态有可能会在一定时间内被外部直接观察到, DNS, Gossip
从类库到服务
服务发现
实现
- 全限定名(定位到主机)
- 端口号(tcp/udp服务)
- 服务标识(具体接口)
功能
- 服务注册
- 服务维护
- 服务发现
服务注册中心一旦崩溃,整个系统都不再可用,因此必须尽最大努力保证服务发现的可用性
CAP矛盾
- Eureka,AP,客户端拿到了已经发生变动的错误地址依赖故障转移(Failover)或者快速失败(Failfast)
- Consul,CP
选择AP/CP
假设系统形成了 A、B 两个网络分区后,A 区的服务只能从区域内的服务发现节点获取到 A 区的服务坐标,B 区的服务只能取到在 B 区的服务坐标,这对你的系统会有什么影响?
- 没有影响,AP
- 影响非常之大,甚至可能带来比整个系统宕机更坏的结果,CP
实现方式
- 在分布式 K/V 存储框架上自己开发, ZooKeeper(CP), Etcd(CP), Redis(AP)
- 以基础设施(主要是指 DNS 服务器)来实现服务发现, SkyDNS、CoreDNS, AP/CP取决于怎样实现
- 专门用于服务发现的框架和工具, Eureka(AP)、Consul(CP) 和 Nacos(AP/CP)
网关路由
网关 = 路由器(基础职能) + 过滤器(可选职能)
网关是网络访问中的单点, 地址具有唯一性不能像服务中心一样做集群
网络IO模型
[以买饭为例]
- 同步IO(Synchronous I/O)
- 阻塞IO(Blocking I/O), 节省 CPU 资源(Java传统IO模型)
- 非阻塞IO(Non-Blocking I/O), 浪费 CPU 资源(Java的NIO)
- 多路复用IO(Multiplexing I/O), 主流(通过NIO实现的Reactor模式)
- 信号驱动IO(Signal-Driven I/O), 需要自己从缓冲区获取数据
- 异步IO(Asynchronous I/O)(通过AIO实现的Proactor模式)
客户端负载均衡
请求的完整路径
- 服务发现
- 网关路由
- 负载均衡
- 服务容错
客户端指集群内部发起服务的进程
- Java, Netflix Ribbon, Spring Cloud Load Balancer
- 其他, 代理均衡器
流量治理
服务降级
- 出错后弥补
- 主动降级
服务容错
容错策略
失败如何弥补
- 故障转移, 重试, 服务具备幂等性, 如果调用的服务器出现故障, 自动切换到其他副本
- 快速失败, 非幂等的服务, 拒绝重试, 抛出异常
- 安全失败, 不影响核心业务的旁路逻辑失败的话返回默认值
- 沉默失败, 当请求失败后,就默认服务提供者一定时间内无法再对外提供服务,不再向它分配请求流量,将错误隔离开来,避免对系统其他部分产生影响
- 故障恢复, 异步的故障转移
调用之前尽量获得最大的成功概率 - 并行调用, 同时调用多个副本, 返回一个成功即可
- 广播调用, 同时调用多个副本, 必须全部返回成功, 刷新分布式缓存
容错策略 | 优点 | 缺点 | 应用场景 |
---|---|---|---|
故障转移 | 系统自动处理,调用者对失败的信息不可见 | 增加调用时间,额外的资源开销 | 调用幂等服务, 对调用时间不敏感的场景 |
快速失败 | 调用者有对失败的处理完全控制权, 不依赖服务的幂等性 | 调用者必须正确处理失败逻辑,如果一味只是对外抛异常,容易引起雪崩 | 调用非幂等的服务, 超时阈值较低的场景 |
安全失败 | 不影响主路逻辑 | 只适用于旁路调用 | 调用链中的旁路服务 |
静默失败 | 控制错误不影响全局 | 出错的地方将在一段时间内不可用 | 频繁超时的服务 |
故障恢复 | 调用失败后自动重试,也不影响主路逻辑 | 重试任务可能产生堆积,重试仍然可能失败 | 调用链中的旁路服务, 对实时性要求不高的主路逻辑也可以使用 |
并行调用 | 尽可能在最短时间内获得最高的成功率 | 额外消耗机器资源,大部分调用可能都是无用功 | 资源充足且对失败容忍度低的场景 |
广播调用 | 支持同时对批量的服务提供者发起调用 | 资源消耗大,失败概率高 | 只适用于批量操作的场景 |
容错设计模式
- 断路器模式, 快速失败
- 舱壁隔离模式, 静默失败
- 局部的线程池来控制服务, 缺点增加了 CPU 的开销, 增加请求延时
- 信号量机制, 为每个远程服务维护一个线程安全的计数器
- 重试模式, 故障转移, 故障恢复
- 仅在主路逻辑的关键服务上进行同步的重试, 尤其不该进行同步重试
- 仅对由瞬时故障导致的失败进行重试, 例如用http状态码来判断
- 仅对具备幂等性的服务进行重试
- 重试必须有明确的终止条件
- 超时
- 次数限制
流量控制
流量统计指标
- TPS
- HPS
- QPS
- 流量, 登录用户数等
限流设计模式
- 流量计数器, 针对时间点进行离散的统计
- 滑动时间窗, 只适用于否决式限流,超过阈值的流量就必须强制失败或降级,很难进行阻塞等待处理
- 漏桶, 首先在缓冲区中暂存,然后再在控制算法的调节下均匀地发送这些被缓冲的报文, 不支持支持变动请求处理速率
- 令牌桶, 请求获取令牌
分布式限流
- 单机限流模式+各主机共享信息, 网络开销大
- 单机限流模式+本地缓存部分信息, 网络开销小, 不准确
可靠通讯
基于边界的安全模型: 把网络划分为不同的区域,不同的区域对应于不同风险级别和允许访问的网络资源权限,将安全防护措施集中部署在各个区域的边界之上,重点关注跨区域的网络流量
零信任安全模型: 除非明确得到了能代表请求来源的身份凭证,否则一律不会有默认的信任关系
可观测性
- 日志, Elasticsearch, Logstash(Fluentd), Kibana
- 追踪
- 度量, Java的JMX(单机), Kubernetes的Prometheus, Zabbix
不可变基础设施
云原生: 微服务, 容器网格, 不可变基础设施, 声明式API
软件兼容性
- ISA兼容: 机器指令集, x86/ARM
- ABI兼容: 操作系统或二进制库, Windows/Linux, DirectX9/DirectX12
- 环境兼容: 配置文件等
虚拟化技术
- 指令集虚拟化, 软件模拟指令集
- 硬件抽象层虚拟化, 以软件或者直接通过硬件来模拟处理器、芯片组、内存、磁盘控制器、显卡等设备, 虚拟机, VMware ESXi和Hyper-V
- 操作系统层虚拟化, 容器化, 只能提供操作系统内核以上的部分 ABI 兼容性与完整的环境兼容性
- 运行库虚拟化, 以一个独立进程来代替操作系统内核来提供目标软件运行所需的全部能力, WINE, WSL1
- 语言层虚拟化, 由虚拟机将高级语言生成的中间代码转换为目标机器可以直接执行的指令,Java 的 JVM 和.NET 的 CLR
虚拟化容器
容器的最初目的: 隔离资源
- 隔离文件: chroot, 当某个进程经过chroot操作之后,它或者它的子进程将不能再访问和操作该目录之外的其他文件
- 隔离访问:namespaces, 由内核直接提供的全局资源封装,是内核针对进程设计的访问隔离机制, 不仅文件系统是独立的,还有着独立的 PID 编号、UID/GID 编号、网络
- 隔离资源:cgroups, 由内核提供的功能,用于隔离或者说分配并限制某个进程组能够使用的资源配额,包括处理器时间、内存大小、磁盘 I/O 速度等
- 封装系统:LXC, LXC封装系统, Docker封装应用
- 封装应用:Docker
- 封装集群:Kubernetes, Docker Engine 经历从不可或缺、默认依赖、可选择、直到淘汰是大概率事件
Kubernetes
Docker 提倡单个容器封装单进程应用, 因为 Docker 只能通过监视 PID 为 1 的进程(即由 ENTRYPOINT 启动的进程)的运行状态来判断容器的工作状态是否正常
Docker Compose 可以设置不同的容器共享volume, 共享 IPC 名称空间
容器的本质是对 cgroups 和 namespaces 所提供的隔离能力的一种封装,然而 Linux 的 cgroups 和 namespaces 原本都是针对进程组而不仅仅是单个进程来设计的,同一个进程组中的多个进程天然就可以共享着相同的访问权限与资源配额, Kubernetes 里的 Pod
容器协作
- 普通非亲密的容器, 以网络交互方式(其他譬如共享分布式存储来交换信息也算跨网络)
- 亲密协作的容器,被调度到同一个集群节点上,可以通过共享本地磁盘等方式协作
- 超亲密的协作, 特指多个容器位于同一个 Pod, 共享: UTS 名称空间, 网络名称空间, IPC 名称空间, 时间名称空间
Kubernetes 将一切皆视为资源,不同资源之间依靠层级关系相互组合协作
- container, 延续了自 Docker 以来一个容器封装一个应用进程的理念,是镜像管理的最小单位
- Pod, 补充了容器化后缺失的与进程组对应的“容器组”的概念,是资源调度的最小单位
- Node, 对应于集群中的单台机器,这里的机器即可以是生产环境中的物理机,也可以是云计算环境中的虚拟节点,节点是处理器和内存等资源的资源池,是硬件单元的最小单位
- Cluster, 当你要部署应用的时候,只需要通过声明式 API 将你的意图写成一份元数据, 是处理元数据的最小单位
资源附加上了期望状态与实际状态两项属性,用户要想使用这些资源来实现某种需求,并不提倡像平常编程那样去调用某个或某一组方法来达成目的,而是通过描述清楚这些资源的期望状态,由 Kubernetes 中对应监视这些资源的控制器来驱动资源的实际状态逐渐向期望状态靠拢,这种交互风格被称为是 Kubernetes 的声明式 API
以应用(集群)为中心的封装
- Kustomize, 根据环境来生成不同的部署配置
- Helm, Chart, 应用商店与包管理工具, 无法很好地管理这种有状态的依赖关系
- Operator, CRD, 要求开发者自己实现一个专门针对该自定义资源的控制器,在控制器中维护自定义资源的期望状态
- 开放应用模型
容器间网络
Linux网络虚拟化
网络通信模型
干预网络通信
虚拟化网络设备
容器间通信
容器网络与生态
CNM 与 CNI
网络插件生态
持久化存储
Kubernetes 存储设计
Mount 和 Volume
静态存储分配
动态存储分配
容器存储与生态
Kubernetes 存储架构
FlexVolume 与 CSI
从 In-Tree 到 Out-of-Tree
容器插件生态
资源与调度
资源模型
服务质量与优先级
驱逐机制
默认调度器
服务网格
透明通信的涅槃
通信的成本
数据平面
控制平面
服务网格与生态
服务网格接口
通用数据平面 API
服务网格生态
技术方法论
微服务
微服务的目的是有效的拆分应用,实现敏捷开发和部署
前提条件
- 决策者与执行者都能意识到康威定律在软件设计中的关键作用(系统的架构趋同于组织的沟通结构)
- 组织中具备一些对微服务有充分理解、有一定实践经验的技术专家
- 系统应具有以自治为目标的自动化与监控度量能力
- 复杂性已经成为制约生产力的主要矛盾
微服务的粒度: 领域驱动设计, DDD
- 能够独立发布、独立部署、独立运行与独立测试
- 强相关的功能与数据在同一个服务中处理
- 一个服务包含至少一项业务实体与对应的完整操作
- 一个 2 Pizza Team (6-12) 能够在一个研发周期内完成的全部需求范围
系统复杂性来源
- 认知负担, 微服务>单体服务
- 合作成本, 随人数的上升而上升, 单体服务上升比例远大于微服务
架构腐化
项目在开始的时候,团队会花很多时间去决策该选择用什么技术体系、哪种架构、怎样的平台框架、甚至具体到开发、测试和持续集成工具。此时就像小孩子们在选择自己所钟爱的玩具,笔者相信无论决策的结果如何,团队都会欣然选择他们所选择的,并且坚信他们的选择是正确的。事实也确实如此,团队选择的解决方案通常能够解决技术选型时就能预料到的那部分困难。但真正困难的地方在于,随着时间的流逝,团队对该项目质量的持续保持能力会逐渐下降,一方面是高级技术专家不可能持续参与软件稳定之后的迭代过程,反过来,如果持续绑定在同一个达到稳定之后的项目上,也很难培养出技术专家。老人的退出新人的加入使得团队总是需要理解旧代码的同时完成新功能,技术专家偶尔来评审一下或救一救火,充其量只能算临时抱佛脚;另一方面是代码会逐渐失控,时间长了一定会有某些并不适合放进最初设计中的需求出现,工期紧任务重业务复杂代码不熟悉都会成为欠下一笔技术债的妥协理由,原则底线每一次被细微地突破,都可能被破窗效应撕裂放大成触目惊心的血痕,最终累积到每个新人到来就马上能嗅出老朽腐臭味道的程度。
架构腐化是软件动态发展中出现的问题,任何静态的治理方案都只能延缓,不能根治,唯一有效的办法是演进式的设计