周志明,博士,远光软件研究院院长。
先后出版了《深入理解Java虚拟机》《智慧的疆界》《凤凰架构》等8部计算机技术类书籍。
周志明的软件架构课
极客时间 公开课
在技术圈里即使再有本事,也还是需要好好包装一下。
在正确的时候,正确的人手上有一个优秀的点子,确实有机会引爆一个时代。
一个业界标准成功与否,很大程度上取决于它的支持者阵营的规模。
凡事不能只讲收益不谈成本。
人生最重要的两件事:终身学习和锻炼身体。
希望以后无论你的职业目标是永远做一名程序员,还是架构师,或者是成为一名研发管理者,都不要轻易地离开技术领域的一线。
价值 = (技能收益 + 知识收益) × 提升空间 / 投入成本
首先在社会中务实地生存,不涉及是否快乐,先把本分工作做对做好,再追求兴趣选择和机遇发展,这才是对多数人的最大的公平。
我们花费一定的成本去学习这类知识,目的是要把自己的知识点筑成体系,把大量的、不同的、零散的知识点,通过内化、存储、整理、归档、输出等方式组合起来,以点成线、以线成面,最终形成系统的、有序的、清晰的脉络结构,这就是知识体系。即降低认知负荷(Cognitive Load)。程序员是需要终身学习的群体。
人性会在持续的颓废时发出示警,却也容易被无效的努力所欺骗。
先做好减法,才能做好加法。
将思考具象化。
做技术不仅要去看、去读、去想、去用,更要去写、去说。
流水不腐,有老朽、有消亡、有重生、有更迭,才是正常生态的运作合理规律。
任何事物一旦脱离了人民群众,最终都会淹没在群众的海洋之中,就连信息技术也不曾例外过。
墨菲定律:凡事只要有可能出错,那就一定会出错。
Murphy’s Law: Anything that can go wrong will go wrong.
Alan Turing(Computing Machinery and Intelligence, 1950):
- We can only see a short distance ahead, but we can see plenty there that needs to be done.
尽管目光所及之处,只是不远的前方,即使如此,依然可以看到那里有许多值得去完成的工作在等待我们。
Richard P. Gabriel(The Rise of ‘Worse is Better,1991):
- Simplicity of both the interface and the implementation are more important than any other attributes of the system — including correctness, consistency, and completeness.
保持接口与实现的简单性,比系统的任何其他属性,包括准确性、一致性和完整性,都来得更加重要。
架构的演进
软件架构风格发展:
大型机(Mainframe)-> 多层单体架构(Monolithic)-> 分布式(Distributed)-> 微服务(Microservices)-> 服务网格(Service Mesh)-> 无服务(Serverless)……
在技术架构上呈现出“从大到小”的发展趋势。
- 单体架构
单体架构风格,潜在的观念是希望系统的每一个部件,甚至每一处代码都尽量可靠,不出、少出错误,致力于构筑一个 7×24 小时不间断的可靠系统。
优点:易于分层、易于开发、易于部署测试、进程内的高效交互,等等。
技术异构:允许系统的每个模块,自由选择不一样的程序语言、不一样的编程框架等技术栈去实现。
正是随着软件架构的不断演进,我们构建可靠系统的观念,开始从“追求尽量不出错”,转变为了正视“出错是必然”。
- 分布式
Unix 的分布式设计哲学:保持接口与实现的简单性。
远程的服务在哪里(服务发现)、有多少个(负载均衡)、网络出现分区或超时或者服务出错了怎么办(熔断、隔离、降级)、方法的参数与返回结果如何表示(序列化协议)、如何传输(传输协议)、服务权限如何管理(认证、授权)、如何保证通信安全(网络安全层)、如何令调用不同机器的服务能返回相同的结果(分布式数据一致性)等一系列问题。
- SOA
SOA = Service-Oriented Architecture 面向服务架构:把一个大的单体系统拆分为若干个更小的、不运行在同一个进程的独立服务。
三种代表性的服务拆分架构模式:(SOA 演化过程的中间产物)
- 烟囱式架构(Information Silo Architecture)
孤岛式信息系统 / 烟囱式信息系统:使用信息烟囱/信息孤岛(Information Island)这种架构的系统。
这种信息系统,完全不会跟其他相关的信息系统之间进行互操作,或者是进行协调工作。 - 微内核架构(Microkernel Architecture) / 插件式架构(Plug-in Architecture)
主数据,连同其他可能被各个子系统使用到的公共服务、数据、资源,都集中到一块,成为一个被所有业务系统共同依赖的核心系统(Kernel / Core System)。
具体的业务系统就能以插件模块(Plug-in Modules)的形式存在,可以为整个系统提供可扩展的、灵活的、天然隔离的功能特性。 - 事件驱动架构(Event-Driven Architecture)
运作方案是,在子系统之间建立一套事件队列管道(Event Queues),来自系统外部的消息将以事件的形式发送到管道中,各个子系统可以从管道里获取自己感兴趣、可以处理的事件消息,也可以为事件新增或者是修改其中的附加信息,甚至还可以自己发布一些新的事件到管道队列中去。
每一个消息的处理者都是独立的、高度解耦的,但它又能与其他处理者(如果存在该消息处理者的话)通过事件管道来进行互动。
SOA,软件架构的基础平台。
SOA 拥有领导制定技术标准的组织 Open CSA;
SOA 具有清晰的软件设计的指导原则,比如服务的封装性、自治、松耦合、可重用、可组合、无状态,等等;
SOA 架构明确了采用 SOAP 作为远程调用的协议,依靠 SOAP 协议族(WSDL、UDDI 和一大票 WS-* 协议)来完成服务的发布、发现和治理;
SOA 架构会利用一个被称为是企业服务总线(Enterprise Service Bus,ESB)的消息管道,来实现各个子系统之间的通讯交互,这就让各个服务间在 ESB 的调度下,不需要相互依赖就可以实现相互通讯,既带来了服务松耦合的好处,也为以后可以进一步实现业务流程编排(Business Process Management,BPM)提供了基础;
SOA 架构使用了服务数据对象(Service Data Object,SDO)来访问和表示数据,使用服务组件架构(Service Component Architecture,SCA)来定义服务封装的形式和服务运行的容器;
……
- 微服务 Microservice
微服务是一种通过多个小型服务的组合,来构建单个应用的架构风格,这些服务会围绕业务能力而非特定的技术标准来构建。
各个服务可以采用不同的编程语言、不同的数据存储技术、运行在不同的进程之中。
服务会采取轻量级的通讯机制和自动化的部署机制,来实现通讯与运维。
微服务不是 SOA 的衍生品,应该明确地与 SOA 划清界线,不再贴上任何 SOA 的标签。
提倡以“实践标准”代替“规范标准”。
服务的注册发现、跟踪治理、负载均衡、故障隔离、认证授权、伸缩扩展、传输通讯、事务处理等问题。
九个核心的业务与技术特征:
- 第一,围绕业务能力构建(Organized around Business Capabilities)
- 第二,分散治理(Decentralized Governance)
表达的是“谁家孩子谁来管”。 - 第三,通过服务来实现独立自治的组件(Componentization via Services)
- 第四,产品化思维(Products not Projects)
要避免把软件研发看作是要去完成某种功能,而要把它当做是一种持续改进、提升的过程。 - 第五,数据去中心化(Decentralized Data Management)
- 第六,轻量级通讯机制(Smart Endpoints and Dumb Pipes)
- 第七,容错性设计(Design for Failure)
软件架构不再虚幻地追求服务永远稳定,而是接受服务总会出错的现实。 - 第八,演进式设计(Evolutionary Design)
容错性设计承认服务会出错,而演进式设计则是承认服务会被报废淘汰。 - 第九,基础设施自动化(Infrastructure Automation)
技术架构者的第一职责就是做决策权衡。
- 云原生 Cloud Native & 服务网格 Service Mesh
应用代码与基础设施软硬一体,合力应对。
服务网格(Service Mesh)的“边车代理模式”(Sidecar Proxy)。
微服务基础设施会由系统自动地在服务的资源容器(指 Kubernetes 的 Pod)中注入一个通讯代理服务器(相当于那个挎斗),用类似网络安全里中间人攻击的方式进行流量劫持,在应用毫无感知的情况下,悄悄接管掉应用的所有对外通讯。
这个代理除了会实现正常的服务调用以外(数据平面通讯),同时还接受来自控制器的指令(控制平面通讯),根据控制平面中的配置,分析数据平面通讯的内容,以实现熔断、认证、度量、监控、负载均衡等各种附加功能。
分布式架构最好的时代:业务与技术完全分离,远程与本地完全透明。
- 无服务 Serverless
它最大的卖点就是简单,只涉及了后端设施(Backend)和函数(Function)两块内容。
后端设施是指数据库、消息队列、日志、存储等这一类用于支撑业务逻辑运行,但本身无业务含义的技术组件。
BaaS = Backend as a Service 后端即服务:这些后端设施都运行在云中。
函数是指业务逻辑代码。
FaaS = Function as a Service 函数即服务:无服务中的函数运行在云端,不必考虑算力问题和容量规划(从技术角度可以不考虑,但从计费角度来看,要估算费用)。
无服务的愿景是让开发者只需要纯粹地关注业务:
- 一是,不用考虑技术组件,因为后端的技术组件是现成的,可以直接取用,没有采购、版权和选型的烦恼;
- 二是,不需要考虑如何部署,因为部署过程完全是托管到云端的,由云端自动完成;
- 三是,不需要考虑算力,因为有整个数据中心的支撑,算力可以认为是无限的;
- 四是,不需要操心运维,维护系统持续地平稳运行是云服务商的责任,而不再是开发者的责任。
无服务天生“无限算力”的假设,就决定了它必须要按使用量(函数运算的时间和内存)来计费,以控制消耗算力的规模,所以函数不会一直以活动状态常驻服务器,只有请求到了才会开始运行。
软件产业更有生命力的形态:软件开发的未来,不会只存在某一种“最先进的”架构风格,而是会有多种具有针对性的架构风格并存。
软件开发的最大挑战就在于,只能在不完备的信息下决定当前要处理的问题。
把无服务作为技术层面的架构,把微服务视为应用层面的架构。
Kubernetes vs Spring Cloud:
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 |
架构师的视角
架构师是软件系统中技术模型的系统设计者。
- 进程间通讯(Inter-Process Communication,IPC)
// 调用者(Caller) : main()
// 被调用者(Callee) : println()
// 调用点(Call Site) : 发生方法调用的指令流位置
// 调用参数(Parameter) : 由Caller传递给Callee的数据,即“hello world”
// 返回值(Retval) : 由Callee传递给Caller的数据,如果方法正常完成,返回值是void,否则是对应的异常
public static void main(String[] args) {
System.out.println(“hello world”);
}
进程间通讯的方式:
- 第一,管道(Pipe)或具名管道(Named Pipe)
管道其实类似于两个进程间的桥梁,用于进程间传递少量的字符流或字节流。
普通管道可用于有亲缘关系进程间的通信(由一个进程启动的另外一个进程);
具名管道摆脱了普通管道没有名字的限制,除了具有普通管道所具有的功能以外,它还允许无亲缘关系进程间的通信。
管道的典型应用:命令行中的“|”操作符。 - 第二,信号(Signal)
信号是用来通知目标进程有某种事件发生的。
除了用于进程间通信外,信号还可以被进程发送给进程自身。
信号的典型应用:kill 命令,比如:kill -9 pid,由 Shell 进程向指定 PID 的进程发送 SIGKILL 信号。 - 第三,信号量(Semaphore)
信号量是用于两个进程之间同步协作的手段,相当于操作系统提供的一个特殊变量。
可以在信号量上,进行 wait() 和 notify() 操作。 - 第四,消息队列(Message Queue)
进程可以向队列中添加消息,而被赋予读权限的进程则可以从队列中消费消息。
消息队列就克服了信号承载信息量少、管道只能用于无格式字节流,以及缓冲区大小受限等缺点,但实时性相对受限。 - 第五,共享内存(Shared Memory)
效率最高的进程间通讯形式,允许多个进程可以访问同一块内存空间。
进程的内存地址空间是独立隔离的,但操作系统提供了让进程主动创建、映射、分离、控制某一块内存的接口。
由于内存是多进程共享的,所以往往会与其它通信机制,如信号量等结合使用,来达到进程间的同步及互斥。 - 第六,本地套接字接口(IPC Socket)
套接字接口,是更为普适的进程间通信机制,可用于不同机器之间的进程通信。
Unix Domain Socket / IPC Socket:基于效率考虑,当仅限于本机进程间通讯的时候,套接字接口是被优化过的,不会经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等操作,只是简单地将应用层数据从一个进程拷贝到另一个进程。
通过网络进行分布式运算的八宗罪(8 Fallacies of Distributed Computing):
网络是可靠的(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)
- 远程服务调用(Remote Procedure Call,RPC)
RPC 出现的最初目的,就是为了让计算机能够跟调用本地方法一样,去调用远程方法。
RPC 是一种语言级别的通讯协议,它允许运行于一台计算机上的程序以某种管道作为通讯媒介(即某种传输协议的网络),去调用另外一个地址空间(通常为网络上的另外一台计算机)。
所有流行过的 RPC 协议,都不外乎通过各种手段来解决三个基本问题:
如何表示数据?
将交互双方涉及的数据,转换为某种事先约定好的中立数据流格式来传输,将数据流转换回不同语言中对应的数据类型来使用。(序列化与反序列化)
每种 RPC 协议都应该有对应的序列化协议,比如:- ONC RPC 的 External Data Representation (XDR)
- CORBA 的 Common Data Representation(CDR)
- Java RMI 的 Java Object Serialization Stream Protocol
- gRPC 的 Protocol Buffers
- Web Service 的 XML Serialization
- 众多轻量级 RPC 支持的 JSON Serialization
如何传递数据?
两个服务交互不是只扔个序列化数据流来表示参数和结果就行了,诸如异常、超时、安全、认证、授权、事务等信息,都可能存在双方交换信息的需求。
Wire Protocol,用来表示两个 Endpoint 之间交换这类数据的行为。
常见的 Wire Protocol 有以下几种:- Java RMI 的 Java Remote Message Protocol(JRMP,也支持RMI-IIOP)
- CORBA 的 Internet Inter ORB Protocol(IIOP,是 GIOP 协议在 IP 协议上的实现版本)
- DDS 的 Real Time Publish Subscribe Protocol(RTPS)
- Web Service 的 Simple Object Access Protocol(SOAP)
- 如果要求足够简单,双方都是 HTTP Endpoint,直接使用 HTTP 也可以(如 JSON-RPC)
如何表示方法?
只要给程序中的每个方法,都规定一个通用的又绝对不会重复的编号;在调用的时候,直接传这个编号就可以找到对应的方法。
唯一的“绝不重复”的编码方案UUID。
用于表示方法的协议:- Android 的 Android Interface Definition Language(AIDL)
- CORBA 的 OMG Interface Definition Language(OMG IDL)
- Web Service 的 Web Service Description Language(WSDL)
- JSON-RPC 的 JSON Web Service Protocol(JSON-WSP)
任何一款具有生命力的 RPC 框架,都不再去追求大而全的“完美”,而是会找到一个独特的点作为主要的发展方向。
几个典型的发展方向:
- 朝着面向对象发展 / 分布式对象(Distributed Object)
代表有 RMI、.NET Remoting、CORBA、DCOM。
在分布式系统中,开发者们不再满足于 RPC 带来的面向过程的编码方式,而是希望能够进行跨进程的面向对象编程。 - 朝着性能发展。
决定 RPC 性能主要就两个因素:- 序列化效率。序列化输出结果的容量越小,速度越快,效率自然越高。
- 信息密度。取决于协议中,有效荷载(Payload)所占总传输数据的比例大小,使用传输协议的层次越高,信息密度就越低。
代表有 gRPC 和 Thrift。
gRPC 和 Thrift 都有自己优秀的专有序列化器;在传输协议方面,gRPC 是基于 HTTP/2 的,支持多路复用和 Header 压缩,Thrift 则直接基于传输层的 TCP 协议来实现,省去了额外的应用层协议的开销。
- 朝着简化发展。
代表为 JSON-RPC。
它牺牲了功能和效率(功能弱、速度慢),换来的是协议的简单。JSON-RPC 的接口与格式的通用性很好,尤其适合用在 Web 浏览器这类一般不会有额外协议、客户端支持的应用场合。
最近几年,RPC 框架有明显朝着更高层次(不仅仅负责调用远程服务,还管理远程服务)与插件化方向发展的趋势,不再选择自己去解决表示数据、传递数据和表示方法这三个问题,而是将全部或者一部分问题设计为扩展点,实现核心能力的可配置,再辅以外围功能,如负载均衡、服务注册、可观察性等方面的支持。
这一类框架的代表,有 Facebook 的 Thrift 和阿里的 Dubbo(现在两者都是 Apache 的)。
不可能有一个“完美”的框架同时满足简单、普适和高性能这三个要求。
- RESTful服务
REST = Representational State Transfer 表征状态转移
REST实际上是HTT(Hyper Text Transfer,超文本传输)的进一步抽象,就像是接口与实现类之间的关系。
超文本 hypertext / 超媒体 hypermedia,指的是一种能够对操作进行判断和响应的文本(或声音、图像等)。
差异对比:
REST | RPC | |
---|---|---|
思想上,抽象目标 | 面向资源的编程思想 | 面向过程的编程思想 |
概念上 | 不是一种协议 | 一种远程服务调用协议 |
REST的关键概念:
- 资源(Resource)
某种信息、数据。 - 表征(Representation)
指信息与用户交互时的表示形式。 - 状态(State)
在特定语境中才能产生的上下文信息。
有状态(Stateful)还是无状态(Stateless),都是只相对于服务端来说的。 - 转移(Transfer)
服务器通过某种方式,把“用户当前阅读的文章”转变成“下一篇文章”,这就被称为“表征状态转移”。
概念名词:
- 第一个,统一接口(Uniform Interface)。
HTTP 协议中已经提前约定好了一套“统一接口”,它包括:GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS 七种基本操作,任何一个支持 HTTP 协议的服务器都会遵守这套规定,对特定的 URI 采取这些操作,服务器就会触发相应的表征状态转移。
服务的 Endpoint 应该是一个名词而不是动词。 - 第二个,超文本驱动(Hypertext Driven)。
浏览器作为所有网站的通用的客户端,任何网站的导航(状态转移)行为都不可能是预置于浏览器代码之中,而是由服务器发出的请求响应信息(超文本)来驱动的。 - 第三个,自描述消息(Self-Descriptive Messages)。
告知客户端该消息的类型以及该如何处理这条消息。
一种被广泛采用的自描述方法,是在名为“Content-Type”的 HTTP Header 中标识出互联网媒体类型(MIME type),
比如“Content-Type : application/json; charset=utf-8”,就说明了该资源会以 JSON 的格式返回,请使用 UTF-8 字符集进行处理。
RESTful 风格的系统特征:
- 服务端与客户端分离(Client-Server)
前端代码反过来驱动服务端进行渲染的 SSR(Server-Side Rendering)技术,在 Serverless、SEO 等场景中已经占领了一块领地。 - 无状态(Stateless)
REST 希望服务器能不负责维护状态,每一次从客户端发送的请求中,应该包括所有必要的上下文信息,会话信息也由客户端保存维护,服务器端依据客户端传递的状态信息来进行业务处理,并且驱动整个应用的状态变迁。
在服务端的内存、会话、数据库或者缓存等地方,持有一定的状态是一种现实情况,而且会是长期存在、被广泛使用的主流方案。 - 可缓存(Cacheability)
软件系统能够像万维网一样,客户端和中间的通讯传递者(代理)可以将部分服务端的应答缓存起来。
应答中必须明确或者间接地表明本身是否可以进行缓存,以避免客户端在将来进行请求的时候得到过时的数据。 - 分层系统(Layered System)
客户端一般不需要知道是否直接连接到了最终的服务器,或者是连接到路径上的中间服务器。
中间服务器可以通过负载均衡和共享缓存的机制,提高系统的可扩展性,这样也便于缓存、伸缩和安全策略的部署。 - 统一接口(Uniform Interface)
软件系统设计的重点放在抽象系统该有哪些资源上,而不是抽象系统该有哪些行为(服务)上。
面向资源编程的抽象程度通常更高。
抽象程度高有好处但也有坏处。坏处是往往距离人类的思维方式更远,而好处是往往通用程度会更好。 - 按需代码(Code-On-Demand)
按需代码是指任何按照客户端(如浏览器)的请求,将可执行的软件程序从服务器发送到客户端的技术。
它赋予了客户端无需事先知道,所有来自服务端的信息应该如何处理、如何运行的宽容度。
好处:
- 第一,降低了服务接口的学习成本。
- 第二,资源天然具有集合与层次结构。
- 第三,REST 绑定于 HTTP 协议。
Richardson 成熟度模型:衡量“服务有多么 REST”。
由“RESTful Web APIs”和“RESTful Web Services”的作者伦纳德 · 理查德森(Leonard Richardson)提出。
Richardson 将服务接口按照“REST 的程度”,从低到高分为 0 至 3 共 4 级:
- Level 0: The Swamp of Plain Old XML:完全不 REST。
- Level 1: Resources:开始引入资源的概念。
- Level 2: HTTP Verbs:引入统一接口,映射到 HTTP 协议的方法上。
- Level 3: Hypermedia Controls:超文本驱动或 Hypertext as the Engine of Application State(HATEOAS)。
所有基于网络的操作逻辑,都可以通过解决“信息在服务端与客户端之间如何流动”这个问题来理解。
- 面向过程编程时,为什么要以算法和处理过程为中心,输入数据,输出结果?当然是为了符合计算机世界中主流的交互方式。
- 面向对象编程时,为什么要将数据和行为统一起来、封装成对象?当然是为了符合现实世界的主流交互方式。
- 面向资源编程时,为什么要将数据(资源)作为抽象的主体,把行为看作是统一的接口?当然是为了符合网络世界的主流的交互方式。
REST 与 HTTP 完全绑定,不适用于要求高性能传输的场景中。
应对传输可靠性最简单粗暴的做法,就是把消息再重发一遍。
这种简单处理能够成立的前提,是服务具有幂等性 Idempotency(服务被重复执行多次的效果与执行一次是相等的)。
要解决批量操作这类问题,目前一种从理论上看还比较优秀的解决方案是GraphQL(但实际使用人数并不多)。
GraphQL 是由 Facebook 提出并开源的一种面向资源 API 的数据查询语言。它和 SQL 一样,挂了个“查询语言”的名字,但其实 CRUD 都能做。
相对于依赖 HTTP 无协议的 REST 来说,GraphQL 是另一种“有协议”地、更彻底地面向资源的服务方式。
站在网络角度考虑如何对内封装逻辑、对外重用服务的新思想,也就是面向资源的编程思想,又成为了新的受追捧的对象。
面向资源编程这种思想,是把问题空间中的数据对象作为抽象的主体,把解决问题时从输入数据到输出结果的处理过程,看作是一个(组)数据资源的状态不断发生变换而导致的结果。
这符合目前网络主流的交互方式,也因此 REST 常常被看作是为基于网络的分布式系统量身定做的交互方式。
事务处理
- 事务
事务:保证数据状态正确性或一致性(Consistency)。
A、I、D 是手段,C 是目的,
- 原子性(Atomic):在同一项业务处理过程中,事务保证了多个对数据的修改,要么同时成功,要么一起被撤销。
- 隔离性(Isolation):在不同的业务处理过程中,事务保证了各自业务正在读、写的数据互相独立,不会彼此影响。
- 持久性(Durability):事务应当保证所有被成功提交的数据修改都能够正确地被持久化,不丢失数据。
事务场景,包括但不限于数据库、缓存、事务内存、消息、队列、对象文件存储等等。
事务的开启、终止、提交、回滚、嵌套、设置隔离级别、乃至与应用代码贴近的传播方式,全部都要依赖底层数据库的支持。
通过写入日志来保证原子性和持久性是业界的主流做法。
事务实现方法:
研究事务的实现原理,必定会追溯到ARIES理论(Algorithms for Recovery and Isolation Exploiting Semantics,基于语义的恢复与隔离算法)。
这种数据恢复操作被称为崩溃恢复(Crash Recovery / Failure Recovery / Transaction Recovery)。
Commit Logging:
重做日志 Redo Log,记录用于崩溃恢复时重演数据变动的日志。
为了能够顺利地完成崩溃恢复,在磁盘中写数据就不能像程序修改内存中变量值那样,直接改变某表某行某列的某个值,必须将修改数据这个操作所需的全部信息(比如修改什么数据、数据物理上位于哪个内存页和磁盘块中、从什么值改成什么值等等),以日志的形式(日志特指仅进行顺序追加的文件写入方式,这是最高效的写入方式)先记录到磁盘中。
只有在日志记录全部都安全落盘,见到代表事务成功提交的“Commit Record”后,数据库才会根据日志上的信息对真正的数据进行修改,修改完成后,在日志中加入一条“End Record”表示事务已完成持久化。
Commit Logging 存在一个巨大的缺陷:所有对数据的真实修改都必须发生在事务提交、日志写入了 Commit Record 之后。Shadow Paging 影子分页:
使用代表:常用的轻量级数据库 SQLite Version 3。
大体思路是对数据的变动会写到硬盘的数据中,但并不是直接就地修改原先的数据,而是先将数据复制一份副本,保留原数据,修改副本数据。在事务过程中,被修改的数据会同时存在两份,一份修改前的数据,一份是修改后的数据,这也是“影子”(Shadow)这个名字的由来。
当事务成功提交,所有数据的修改都成功持久化之后,最后一步要修改数据的引用指针,将引用从原数据改为新复制出来修改后的副本,最后的“修改指针”这个操作将被认为是原子操作,所以 Shadow Paging 也可以保证原子性和持久性。
Shadow Paging 相对简单,但涉及到隔离性与锁时,Shadow Paging 实现的事务并发能力相对有限,因此在高性能的数据库中应用不多。Write-Ahead Logging
“提前写入”(Write-Ahead),就是允许在事务提交之前,提前写入变动数据。
Write-Ahead Logging 先将何时写入变动数据,按照事务提交时点为界,分为两类:- FORCE:
当事务提交后,
要求变动数据必须同时完成写入 - FORCE。
不强制变动数据必须同时完成写入 - NO-FORCE。
只要有了日志,变动数据随时可以持久化,从优化磁盘 I/O 性能考虑,没有必要强制数据写入立即进行。 - STEAL:
在事务提交前,
允许变动数据提前写入 - STEAL。
不允许变动数据提前写入 - NO-STEAL。
从优化磁盘 I/O 性能考虑,允许数据提前写入,有利于利用空闲 I/O 资源,也有利于节省数据库缓存区的内存。
- FORCE:
Write-Ahead Logging 在崩溃恢复时,会以此经历以下三个阶段:
- 分析阶段(Analysis):该阶段从最后一次检查点(Checkpoint,在这个点之前所有应该持久化的变动都已安全落盘)开始扫描日志,找出所有没有 End Record 的事务,组成待恢复的事务集合(一般包括 Transaction Table 和 Dirty Page Table)。
- 重做阶段(Redo):该阶段依据分析阶段中,产生的待恢复的事务集合来重演历史(Repeat History),找出所有包含 Commit Record 的日志,将它们写入磁盘,写入完成后增加一条 End Record,然后移除出待恢复事务集合。
- 回滚阶段(Undo):该阶段处理经过分析、重做阶段后剩余的恢复事务集合,此时剩下的都是需要回滚的事务(被称为 Loser),根据 Undo Log 中的信息回滚这些事务。
Commit Logging 允许 NO-FORCE,但不允许 STEAL。
因为假如事务提交前就有部分变动数据写入磁盘,那一旦事务要回滚,或者发生了崩溃,这些提前写入的变动数据就都成了错误。
Write-Ahead Logging 允许 NO-FORCE,也允许 STEAL。
增加了回滚日志 Undo Log。当变动数据写入磁盘前,必须先记录 Undo Log,写明修改哪个位置的数据、从什么值改成什么值,以便在事务回滚或者崩溃恢复时,根据 Undo Log 对提前写入的数据变动进行擦除。
数据库按照“是否允许 FORCE 和 STEAL”可以产生四种组合:
NO-STEAL | STEAL | |
---|---|---|
NO-FORCE | 重做日志 | 重做日志,回滚日志。性能最快 |
FORCE | 不需要日志。性能最慢 | 回滚日志 |
从优化磁盘 I/O 的角度看,NO-FORCE 加 STEAL 组合的性能是最高的;
从算法实现与日志的角度看,NO-FORCE 加 STEAL 组合的复杂度是最高的。
- 加锁同步
现代数据库提供以下三种锁:
- 写锁(Write Lock,也叫做排他锁 eXclusive Lock,简写为 X-Lock):
只有持有写锁的事务才能对数据进行写入操作,数据加持着写锁时,其他事务不能写入数据,也不能施加读锁。 - 读锁(Read Lock,也叫做共享锁 Shared Lock,简写为 S-Lock):
多个事务可以对同一个数据添加多个读锁,数据被加上读锁后就不能再被加上写锁,所以其他事务不能对该数据进行写入,但仍然可以读取。
对于持有读锁的事务,如果该数据只有一个事务加了读锁,那可以直接将其升级为写锁,然后写入数据。 - 范围锁(Range Lock):
对于某个范围直接加排他锁,在这个范围内的数据不能被读取,也不能被写入。
多版本并发控制(Multi-Version Concurrency Control,MVCC)的无锁优化方案,解决“一个事务读 + 另一个事务写”的隔离问题。
MVCC 是一种读取优化策略,它的“无锁”是特指读取时不需要加锁。
MVCC 是只针对“读 + 写”场景的优化.
- 事务提交
X/Open XA(XA= eXtended Architecture)的事务处理框架。
核心内容是,定义了全局的事务管理器(Transaction Manager,用于协调全局事务)和局部的资源管理器(Resource Manager,用于驱动本地事务)之间的通讯接口。
两段式提交(2 Phase Commit,2PC)协议:XA 将事务提交拆分成了两阶段过程。
- 准备阶段/投票阶段。
协调者询问事务的所有参与者是否准备好提交,如果已经准备好提交回复 Prepared,否则回复 Non-Prepared。 - 提交阶段/执行阶段。
协调者如果在准备阶段收到所有事务参与者回复的 Prepared 消息,就会首先在本地持久化事务状态为 Commit,然后向所有参与者发送 Commit 指令,所有参与者立即执行提交操作;
否则,任意一个参与者回复了 Non-Prepared 消息,或任意一个参与者超时未回复,协调者都会将自己的事务状态持久化为“Abort”之后,向所有参与者发送 Abort 指令,参与者立即执行回滚操作。
三段式提交(3 Phase Commit,3PC)协议:
把原本的两段式提交的准备阶段再细分为两个阶段,分别称为 CanCommit、PreCommit,把提交阶段改为 DoCommit 阶段。
新增的 CanCommit 是一个询问阶段,协调者让每个参与的数据库根据自身状态,评估该事务是否有可能顺利完成。
在事务需要回滚的场景中,三段式的性能通常要比两段式好很多,但在事务能够正常提交的场景中,两段式和三段式提交的性能都很差,三段式因为多了一次询问,性能还要更差一些。
- 分布式事务
本地事务(Local Transactions):单个服务使用单个数据源。仅操作特定单一事务资源的、不需要“全局事务管理器”进行协调的事务。
全局事务(Global Transactions):单个服务使用多个数据源。
共享事务(Share Transactions):多个服务共用同一个数据源。
分布式事务(Distributed Transactions):多个服务使用多个数据源。
数据源是指提供数据的逻辑设备,不必与物理设备一一对应。
CAP理论:
- 一致性(Consistency):代表在任何时刻、任何分布式节点中,所看到的数据都是没有矛盾的。
- 可用性(Availability):代表系统不间断地提供服务的能力。
- 分区容忍性(Partition Tolerance):代表分布式环境中,当部分节点因网络原因而彼此失联(即与其他节点形成“网络分区”)时,系统仍能正确地提供服务的能力。
CAP可选方案:
- 如果放弃分区容错性(CA without P)
Oracle数据库的 RAC 集群是通过共享磁盘的方式来避免网络分区的出现。 - 如果放弃可用性(CP without A)
比如 HBase,以它的集群为例,假如某个 RegionServer 宕机了,这个 RegionServer 持有的所有键值范围都将离线,直到数据恢复过程完成为止,通常时间会很长。 - 如果放弃一致性(AP without C)
AP 系统目前是分布式系统设计的主流选择,大多数的 NoSQL 库和支持分布式的缓存都是 AP 系统。
最终一致性(Eventual Consistency):如果数据在一段时间内没有被另外的操作所更改,那它最终将会达到与强一致性过程相同的结果。
最大努力交付(Best-Effort Delivery):靠着持续重试来保证可靠性的操作。比如 TCP 协议中的可靠性保障
最大努力一次提交(Best-Effort 1PC):系统会把最有可能出错的业务,以本地事务的方式完成后,通过不断重试的方式(不限于消息系统)来促使同个事务的其他关联业务完成。
TCC 事务的实现过程分为三个阶段:
- Try:尝试执行阶段。
完成所有业务可执行性的检查(保障一致性),并且预留好事务需要用到的所有业务资源(保障隔离性)。 - Confirm:确认执行阶段。
不进行任何业务检查,直接使用 Try 阶段准备的资源来完成业务处理。
Confirm 阶段可能会重复执行,需要满足幂等性。 - Cancel:取消执行阶段。
释放 Try 阶段预留的业务资源。
Cancel 阶段可能会重复执行,需要满足幂等性。
SAGA 事务基于数据补偿代替回滚的解决思路。
透明多级分流系统
ISO = International Organization for Standardization 国际标准化组织
ISO七层网络架构:
层 | 数据单元 | 功能 | |
---|---|---|---|
7 | 应用层 Application Layer | 数据 Data | 提供为应用软件提供服务的接口,用于与其他应用软件之间的通信。典型协议:HTTP、HTTPS、FTP、Telnet、SSH、SMTP、POP3等。 |
6 | 表达层 Presentation Layer | 数据 Data | 把数据转换为能与接收者的系统格式兼容并适合传输的格式。 |
5 | 会话层 Session Layer | 数据 Data | 负责在数据传输中设置和维护计算机网络中两台计算机之间的通信连接。 |
4 | 传输层 Transport Layer | 数据段 Segments | 把传输表头加至数据以形成数据包。传输表头包含了所使用的协议等发送信息。典型协议:TCP、UDP、RDP、SCTP、FCP等。 |
3 | 网络层 Network Layer | 数据包 Packets | 决定数据的传输路径选择和转发,将网络表头附加至数据段后以形成报文(即数据包)。典型协议:IPv4/IPv6、IGMP、ICMP、EGP、RIP等。 |
2 | 数据链路层 Data Link Layer | 数据帧 Frame | 负责点对点的网络寻址、错误侦测和纠错。当表头和表尾被附加至数据包后,就形成数据帧(Frame)。典型协议:WiFi(802.11)、Ethernet(802.3)、PPP等。 |
1 | 物理层 Physical Layer | 比特流 Bit | 在局域网上传送数据帧,它负责管理电脑通信设备和网络媒体之间的互通。包括了针脚、电压、线缆规范、集线器、中继器、网卡、主机接口卡等。 |
设计原则
第一个原则是尽可能减少单点部件,如果某些单点是无可避免的,则应尽最大限度减少到达单点部件的流量。
第二个原则是奥卡姆剃刀原则,它更为关键。
奥卡姆剃刀原则:如无必要,勿增实体。
Occam’s Razor(William of Ockham):Entities should not be multiplied without necessity.
在能满足需求的前提下,最简单的系统就是最好的系统。
客户端缓存
HTTP 缓存机制:
HTTP/1.0,状态缓存:不经过服务器,客户端直接根据缓存信息来判断目标网站的状态。
HTTP/1.1 强制缓存/强缓存
根据约定,在浏览器的地址输入、页面链接跳转、新开窗口、前进和后退中,强制缓存都可以生效,但在用户主动刷新页面时应当自动失效。
强制缓存是基于时效性的,无论是人还是服务器,在大多数情况下,其实都没有什么把握去承诺某项资源多久不会发生变化。
在 HTTP 协议中,设置了两类可以实现强制缓存的 Headers(标头):Expires 和 Cache-Control。
第一类:Expires
Expires 是 HTTP/1.0 协议中开始提供的 Header,后面跟随了一个截止时间参数。
当服务器返回某个资源时,如果带有该 Header 的话,就意味着服务器承诺在截止时间之前,资源不会发生变动,浏览器可直接缓存该数据,不再重新发请求。HTTP/1.1 200 OK Expires: Wed, 8 Apr 2020 07:28:00 GMT
设计缺陷:
- 受限于客户端的本地时间
比如,在收到响应后,客户端修改了本地时间,将时间点前后调整了几分钟,这就可能会造成缓存提前失效或超期持有。 - 无法处理涉及到用户身份的私有资源
比如,合理的做法是,某些资源被登录用户缓存在了自己的浏览器上。但如果被代理服务器或者内容分发网络(CDN)缓存起来,就可能会被其他未认证的用户获取。 - 无法描述“不缓存”的语义
比如,一般浏览器为了提高性能,往往会自动在当次会话中缓存某些 MINE 类型的资源,这会造成设计者不希望缓存的资源无法被及时更新。而在 HTTP/1.0 的设计中,Expires 并没有考虑这方面的需求,导致无法强制浏览器不允许缓存某个资源。
- 受限于客户端的本地时间
第二类:Cache-Control
Cache-Control 是 HTTP/1.1 协议中定义的强制缓存 Header,它的语义比起 Expires 来说就丰富了很多。而如果 Cache-Control 和 Expires 同时存在,并且语义存在冲突(比如 Expires 与 max-age / s-maxage 冲突)的话,IETF 规定必须以 Cache-Control 为准。
同样这里,我们也看看 Cache-Control 的使用示例:HTTP/1.1 200 OK Cache-Control: max-age=600
Cache-Control 标准的参数主要包括 6 种:
- max-age 和 s-maxage
max-age 后面跟随了一个数字,它是以秒为单位的,表明相对于请求时间(在 Date Header 中会注明请求时间)多少秒以内,缓存是有效的,资源不需要重新从服务器中获取。这个相对时间,就避免了 Expires 中,采用的绝对时间可能受客户端时钟影响的问题。
s-maxage,i.e. share-maxage,意味着“共享缓存”的有效时间,即允许被 CDN、代理等持有的缓存有效时间,这个参数主要是用来提示 CDN 这类服务器如何对缓存进行失效。 - public 和 private
这一类参数表明了是否涉及到用户身份的私有资源。
如果是 public,就意味着资源可以被代理、CDN 等缓存;
如果是 private,就意味着只能由用户的客户端进行私有缓存。 - no-cache 和 no-store
no-cache 表明该资源不应该被缓存,哪怕是同一个会话中对同一个 URL 地址的请求,也必须从服务端获取,从而令强制缓存完全失效(但此时的协商缓存机制依然是生效的);
no-store 不强制会话中是否重复获取相同的 URL 资源,但它禁止浏览器、CDN 等以任何形式保存该资源。 - no-transform
no-transform 禁止资源以任何形式被修改。
比如,某些 CDN、透明代理支持自动 GZip 压缩图片或文本,以提升网络性能,而 no-transform 就禁止了这样的行为,它要求 Content-Encoding、Content-Range、Content-Type 均不允许进行任何形式的修改。 - min-fresh 和 only-if-cached
这两个参数是仅用于客户端的请求 Header。
min-fresh 后续跟随了一个以秒为单位的数字,用于建议服务器能返回一个不少于该时间的缓存资源(即包含 max-age 且不少于 min-fresh 的数字);
only-if-cached 表示服务器希望客户端不要发送请求,只使用缓存来进行响应,若缓存不能命中,就直接返回 503/Service Unavailable 错误。 - must-revalidate 和 proxy-revalidate
must-revalidate 表示在资源过期后,一定要从服务器中进行获取,即超过了 max-age 的时间后,就等同于 no-cache 的行为;
proxy-revalidate 用于提示代理、CDN 等设备资源过期后的缓存行为,除对象不同外,语义与 must-revalidate 完全一致。
- max-age 和 s-maxage
- HTTP/2.0 协商缓存
协商缓存的两种变动检查机制:
- 根据资源的修改时间进行检查
它的语义中包含了两种标准参数:Last-Modified 和 If-Modified-Since。
Last-Modified 是服务器的响应 Header,用来告诉客户端这个资源的最后修改时间。
而对于带有这个 Header 的资源,当客户端需要再次请求时,会通过 If-Modified-Since,把之前收到的资源最后修改时间发送回服务端。
如果此时,服务端发现资源在该时间后没有被修改过,就只要返回一个 304/Not Modified 的响应即可,无需附带消息体,从而达到了节省流量的目的: - 根据资源唯一标识是否发生变化来进行检查
它的语义中也包含了两种标准参数:Etag 和 If-None-Match。
Etag 是服务器的响应 Header,用于告诉客户端这个资源的唯一标识。HTTP 服务器可以根据自己的意愿,来选择如何生成这个标识,
比如 Apache 服务器的 Etag 值,就默认是对文件的索引节点(inode)、大小和最后修改时间进行哈希计算后而得到的。
对于带有这个 Header 的资源,当客户端需要再次请求时,就会通过 If-None-Match,把之前收到的资源唯一标识发送回服务端。
如果此时,服务端计算后发现资源的唯一标识与上传回来的一致,就说明资源没有被修改过,同样也只需要返回一个 304/Not Modified 的响应即可,无需附带消息体,达到节省流量的目的。
Etag 是 HTTP 中一致性最强的缓存机制。
Etag 又是 HTTP 中性能最差的缓存机制。
Etag 和 Last-Modified 是允许一起使用的,服务器会优先验证 Etag,在 Etag 一致的情况下,再去对比 Last-Modified,这是为了防止有一些 HTTP 服务器没有把文件修改日期纳入哈希范围内。
HTTP 的内容协商机制:
HTTP 协议设计了以Accept*(Accept、Accept-Language、Accept-Charset、Accept-Encoding)开头的一套请求 Header,以及对应的以Content-*(Content-Language、Content-Type、Content-Encoding)开头的响应 Header。
Vary Header:
对于一个 URL 能够获取多个资源的场景中,缓存同样也需要有明确的标识来获知,它要根据什么内容来对同一个 URL 返回给用户正确的资源。
Vary 后面应该跟随一组其他 Header 的名字。比如:
HTTP/1.1 200 OK
Vary: Accept, User-Agent
优化传输链路
由于 TCP 协议本身是面向长时间、大数据传输来设计的,所以只有在一段较长的时间尺度内,TCP 协议才能展现出稳定性和可靠性的优势,不会因为建立连接的成本太高,成为了使用瓶颈。
HTTP/1.1 通过分块传输解决了即时压缩与持久连接并存的问题;
HTTP/2,由于多路复用和单域名单连接的设计,已经不需要再刻意去强调持久连接机制了,但数据压缩仍然有节约传输带宽的重要价值。
内容分发网络
CDN = Content Distribution/Delivery Network
内容分发:CDN 获取源站资源的过程。
- 第一种:主动分发(Push)
主动分发就是由源站主动发起,将内容从源站或者其他资源库推送到用户边缘的各个 CDN 缓存节点上。 - 第二种:被动回源(Pull)
被动回源就是指由用户访问所触发的全自动、双向透明的资源缓存过程。
当某个资源首次被用户请求的时候,CDN 缓存节点如果发现自己没有该资源,就会实时从源站中获取。
这时资源的响应时间可粗略认为是资源从源站到 CDN 缓存节点的时间,再加上资源从 CDN 发送到用户的时间之和。
最常见的管理(更新)资源的做法是:超时被动失效 + 手工主动失效。
超时失效是指给予缓存资源一定的生存期,超过了生存期就在下次请求时重新被动回源一次。
手工失效是指CDN 服务商一般会给程序调用提供失效缓存的接口,在网站更新时,由持续集成的流水线自动调用该接口来实现缓存更新。
CDN 应用:
- 加速静态资源
- 安全防御
- 协议升级
- 状态缓存
- 修改资源
- 访问控制
- 注入功能
负载均衡
负载均衡器(Load Balancer):承担了调度后方的多台机器,以统一的接口对外提供服务的技术组件。
两大职责是“选择谁来处理用户请求”和“将用户请求转发过去”。
四层负载均衡的优势是性能高,七层负载均衡的优势是功能强。
做多级混合负载均衡,通常应该是低层的负载均衡在前,高层的负载均衡在后。
代理,根据“哪一方能感知到”的原则,可以分为三类:
- 正向代理:在客户端设置的、代表客户端与服务器通讯的代理服务。它是客户端可知,而对服务器是透明的。
- 反向代理:设置在服务器一侧,代表真实服务器来与客户端通讯的代理服务。此时它对客户端来说是透明的。
- 透明代理:对双方都透明的,配置在网络中间设备上的代理服务。比如,架设在路由器上的透明翻墙代理。
均衡策略与实现:
- 轮循均衡(Round Robin)
- 权重轮循均衡(Weighted Round Robin)
- 随机均衡(Random)
- 权重随机均衡(Weighted Random)
- 一致性哈希均衡(Consistency Hash)
- 响应速度均衡(Response Time)
- 最少连接数均衡(Least Connection)
服务端缓存
硬件缓存是一种硬件对软件运行效率的优化手段。
缓存虽然是典型的以空间换时间来提升性能的手段,但它的出发点是缓解 CPU 和 I/O 资源在峰值流量下的压力,“顺带”而非“专门”地提升响应性能。
缓存属性:
- 吞吐量:缓存的吞吐量使用 OPS 值(每秒操作数,Operations per Second,ops/s)来衡量,它反映了对缓存进行并发读、写操作的效率,即缓存本身的工作效率高低。
- 命中率:缓存的命中率即成功从缓存中返回结果次数与总请求次数的比值,它反映了引入缓存的价值高低,命中率越低,引入缓存的收益越小,价值越低。
- 扩展功能:缓存除了基本读写功能外,还提供了一些额外的管理功能,比如最大容量、失效时间、失效事件、命中率统计,等等。
- 分布式支持:缓存可以分为“进程内缓存”和“分布式缓存”两大类,前者只为节点本身提供服务,无网络访问操作,速度快但缓存的数据不能在各个服务节点中共享。后者则相反。
缓存中最主要的数据竞争来源于读取数据的同时,也会伴随着对数据状态的写入操作,而写入数据的同时,也会伴随着数据状态的读取操作。
淘汰策略:
- 第一种:FIFO(First In First Out)
优先淘汰最早进入被缓存的数据。 - 第二种:LRU(Least Recent Used)
优先淘汰最久未被使用访问过的数据。 - 第三种:LFU(Least Frequently Used)
优先淘汰最不经常使用的数据。 - TinyLFU(Tiny Least Frequently Used)
- W-TinyLFU(Windows-TinyLFU)
主流的进程内缓存方案:
ConcurrentHashMap | Ehcache | Guava Cache | Caffeine | |
---|---|---|---|---|
访问性能 | 最高 | 一般 | 良好 | 优秀,接近于ConcurrentHashMap |
淘汰策略 | 无 | 支持多种淘汰策略 FIFO, LRU, LFU等 | LRU | W-TinyLFU |
扩展功能 | 只提供基础的访问接口 | 并发级别控制,失效策略,容量控制,事件通知,统计信息…… | 大致同左 | 大致同左 |
分布式缓存
缓存形式:
- 复制式缓存
可以看作是“能够支持分布式的进程内缓存”。产品如:Infinispan。 - 集中式缓存
集中式缓存的读、写都需要网络访问,好处是不会随着集群节点数量的增加而产生额外的负担,而坏处自然是读、写都不可能再达到进程内缓存那样的高性能。
它与使用缓存的应用分处在独立的进程空间中。
产品如:Redis,Memcached。 - 透明多级缓存(Transparent Multilevel Cache,TMC)
变更以分布式缓存中的数据为准,访问以进程内缓存的数据优先。
Redis 集群就是典型的 AP 式,它具有高性能、高可用等特点,但它却并不保证强一致性。
保证强一致性的 ZooKeeper、Doozerd、Etcd 等分布式协调框架,通常不会把它们当作“缓存框架”来使用。
缓存风险:
- 缓存穿透:查询不存在数据的现象。
- 缓存击穿:针对单个热点数据,未能命中缓存,都到达真实数据源中去,导致其压力剧增。
办法:加锁同步。热点数据由代码来手动管理。 - 缓存雪崩:缓存服务由于某些原因崩溃后重启,此时也会造成大量数据同时失效。
三种办法:- 提升缓存系统可用性,建设分布式缓存的集群。
- 启用透明多级缓存,各个服务节点的一级缓存中的数据,通常会具有不一样的加载时间,分散了它们的过期时间。
- 将缓存的生存期从固定时间改为一个时间段内的随机时间。
- 缓存污染:缓存中的数据与真实数据源中的数据不一致的现象。
办法:更新缓存时可以遵循的设计模式,比如 Cache Aside、Read/Write Through、Write Behind Caching,等等。
Cache Aside 模式,最简单、成本最低。主要内容只有两条:
- 读数据时,先读缓存,缓存没有的话,再读数据源,然后将数据放入缓存,再响应请求。
- 写数据时,先写数据源,然后失效(而不是更新)掉缓存。
安全架构
安全性:
- 认证(Authentication):系统如何正确分辨出操作用户的真实身份?
- 授权(Authorization):系统如何控制一个用户该看到哪些数据、能操作哪些功能?
- 凭证(Credentials):系统如何保证它与用户之间的承诺是双方当时真实意图的体现,是准确、完整且不可抵赖的?
- 保密(Confidentiality):系统如何保证敏感数据无法被包括系统管理员在内的内外部人员所窃取、滥用?
- 传输(Transport Security):系统如何保证通过网络传输的信息无法被第三方窃听、篡改和冒充?
- 验证(Verification):系统如何确保提交到每项服务中的数据是合乎规则的,不会对系统稳定性、数据一致性、正确性产生风险?
认证
对于某些大规模的信息系统,账户和权限的管理往往要由专门的基础设施来负责。
比如微软的活动目录(Active Directory,AD)或者轻量目录访问协议(Lightweight Directory Access Protocol,LDAP),跨系统的共享使用问题甚至还会用到区块链技术来解决。
架构安全性的经验原则:以标准规范为指导、以标准接口去实现。
认证方式:
- 基于通讯协议:HTTP 认证
HTTP Basic 认证是一种以演示为目的的认证方案。
Bearer:RFC 6750,基于 OAuth 2.0 规范来完成认证。OAuth 2.0 是一个同时涉及到认证与授权的协议。 - 基于通讯内容:Web 认证
Java安全框架:Apache Shiro,Spring Security。
授权
AAAA = Authentication 认证、Authorization 授权、Audit 审计、Account 账号
确保授权的过程可靠
确保授权的结果可控
OAuth 2.0 的核心思想是令牌代替密码。
OAuth 2.0 一共提出了四种不同的授权方式:
- 授权码模式(Authorization Code)
- 简化模式(Implicit)
- 密码模式(Resource Owner Password Credentials)
- 客户端模式(Client Credentials)
所有的访问控制模型,实质上都是在解决同一个问题:谁(User)拥有什么权限(Authority)去操作(Operation)哪些资源(Resource)。
建立访问控制模型的基本目的就是为了管理垂直权限和水平权限。
垂直权限即功能权限;
水平权限则是数据权限,它很难抽象与通用。
状态管理,Cookie-Session
凭证
JWT = JSON Web Token
header.payload.signature
缺陷:令牌难以主动失效。相对更容易遭受重放攻击。只能携带相当有限的数据。必须考虑令牌在客户端如何存储。无状态也不总是好的。
权衡才是架构设计中最关键的地方。
保密
加密与解密。
密码加密是为了防止服务器被黑后密码泄露的问题,并不是为了增强传输过程的安全性。
算法类型:
类型 | 特点 | 常见实现 | 主要用途 | 主要局限 |
---|---|---|---|---|
哈希摘要 | 不可逆,即不能解密,所以并不是加密算法,只是一些场景把它当作加密算法使用。 易变性,输入发生1Bit变动,就可能导致输出结果50%的内容发生改变。 无论输入长度多少,输出长度固定(2的N次幂) |
MD2/4/5/6、SHA0/1/256/512 | 摘要 | 无法解密 |
对称加密 | 加密是指加密和解密是一样的密钥设计难度相对较小,执行速度相对较快加密明文长度不受限制 | DES、AES、RC4、IDEA | 加密 | 要解决如何把密钥安全地传递给解密者 |
非对称加密 | 加密和解密使用的是不同的密钥明文长度不能超过公钥长度 | RSA、BCDSA、EIGamal | 签名、传递秘钥 | 加密明文长度受限 |
传输
摘要的意义就是在源信息不泄露的前提下辨别其真伪。
通常做法是,用非对称加密来安全地传递少量数据给通讯的另一方,然后再以这些数据为密钥,采用对称加密来安全高效地大量加密传输数据。
密码学套件:由多种加密算法组合的应用形式。
密钥协商:非对称加密在这个场景中发挥的作用。
达成信任的方式:
- 基于共同私密信息的信任
- 基于权威公证人的信任
公开密钥基础设施(Public Key Infrastructure,PKI):
又称公开密钥基础架构、公钥基础建设、公钥基础设施、公开密码匙基础建设、公钥基础架构,是一组由硬件、软件、参与者、管理政策与流程组成的基础架构,其目的在于创造、管理、分配、使用、存储以及撤销数字证书。
密码学上,公开密钥基础建设借着数字证书认证中心(Certificate Authority,CA)将用户的个人身份跟公开密钥链接在一起。对每个证书中心用户的身份必须是唯一的。
链接关系通过注册和发布过程创建,取决于担保级别,链接关系可能由 CA 的各种软件或在人为监督下完成。
PKI 的确定链接关系的这一角色称为注册管理中心(Registration Authority,RA)。RA 确保公开密钥和个人身份链接,可以防抵赖。
验证
把校验行为从分层中剥离出来,不是在哪一层做,而是在 Bean 上做。
Java Bean Validation
分布式的基石
分布式共识
状态机(State Machine):在计算机科学中,能够使用确定的操作,促使状态间产生确定的转移结果的计算模型。
共识(Consensus)与一致性(Consistency)是有区别的:
一致性(Consistency)是指数据不同副本之间的差异。
共识(Consensus)是指达成一致性的方法与过程。
状态转移(State Transfer):以同步为代表的数据复制方法。改变数据状态时,直接将目标状态赋予它。比较符合人类思维的可靠性保障手段,但通常要以牺牲可用性为代价。
操作转移(Operation Transfer):通过某种操作,把源状态转换为目标状态。分布式系统里主流的数据复制方法以此为基础的。
分布式共识的复杂性,主要来源于两大因素:网络的不可靠、请求的可并发。
熵(Entropy):它代表的是事物的混乱程度。
反熵就是反混乱,它把提升网络各个节点之间的相似度作为目标。
分布式共识算法:
- Paxos 算法的工作流程
在使用 Paxos 算法的分布式系统里,所有的节点都是平等的,它们都可以承担以上某一种或者多种角色。
Paxos 算法将分布式系统中的节点分为三类:
- 提案节点 Proposer:提出对某个值进行设置操作的节点。值一旦设置成功,就是不会丢失也不可变的。
提案(Proposal):设置值的行为。
Paxos 是典型的基于操作转移模型而非状态转移模型来设计的算法,这里的“设置值”不要类比成程序中变量的赋值操作,而应该类比成日志记录操作。 - 决策节点 Acceptor:是应答提案的节点,决定该提案是否可被投票、是否可被接受。
提案一旦得到过半数决策节点的接受,就意味着这个提案被批准(Accept)。
提案被批准,就意味着该值不能再被更改,也不会丢失,且最终所有节点都会接受它。 - 记录节点 Learner:不参与提案,也不参与决策,只是单纯地从提案、决策节点中学习已经达成共识的提案。
比如,少数派节点从网络分区中恢复时,将会进入这种状态。
Paxos 算法包括“准备(Prepare)”和“批准(Accept)”两个阶段。
两个承诺是指:承诺不会再接受提案 ID 小于或等于 n 的 Prepare 请求;承诺不会再接受提案 ID 小于 n 的 Accept 请求。
一个应答是指:在不违背以前作出的承诺的前提下,回复已经批准过的提案中 ID 最大的那个提案所设定的值和提案 ID,如果该值从来没有被任何提案设定过,则返回空值。如果违反此前做出的承诺,也就是说收到的提案 ID 并不是决策节点收到过的最大的,那就可以直接不理会这个 Prepare 请求。
- Raft 算法:把共识问题分解为“Leader Election”、“Entity Replication”和“Safety”三个问题来思考、解决的解题思路。
- 如何选主(Leader Election)
- 如何把数据复制到各个节点上(Entity Replication)
- 如何保证过程是安全的(Safety)
选主问题的本质,仅仅是分布式系统对“谁来当主节点”这件事情的达成的共识而已。
协定性(Safety):所有的坏事都不会发生(Something “bad” will never happen)。
终止性(Liveness):所有的好事都终将发生,但不知道是啥时候(Something “good” will must happen, but we don’t know when)。
从类库到服务
服务发现
分布式系统永恒的话题:数据一致性与服务可用性之间的矛盾。
服务发现”(Service Discovery):解决“如何确定目标方法的确切位置”的过程。这与编译链接有着等同意义。
所有的远程服务调用都是使用“全限定名(Fully Qualified Domain Name,FQDN)、端口号、服务标识”构成的三元组,来确定一个远程服务的精确坐标的。
服务发现要解决注册、维护和发现三大功能问题。
- 服务的注册(Service Registration)
- 服务的维护(Service Maintaining)
- 服务的发现(Service Discovery)
微服务架构中的一个重要设计原则是:通过服务来实现独立自治的组件(Componentization via Services)。
微服务强调通过“服务”(Service)而不是“类库”(Library)来构建组件。
- 类库是在编译期静态链接到程序中的,通过本地调用来提供功能
- 服务是进程外组件,通过远程调用来提供功能。
使用 DNS 来做服务发现是最符合传统的做法,这也是现代虚拟化容器编排系统(如 Kubernetes)所提供的方案。
微服务网关
微服务中的网关,常被称为“服务网关”或者“API 网关”。
微服务中网关的首要职责,就是以统一的地址对外提供服务,将外部访问这个地址的流量,根据适当的规则路由到内部集群中正确的服务节点之上。
网关 = 路由器(基础职能) + 过滤器(可选职能)
从路由目的这个角度来看,负载均衡器与服务网关的区别在于:
- 负载均衡器是为了根据均衡算法对流量进行平均地路由。
- 服务网关是为了根据流量中的某种特征进行正确地路由。
BFF(Backends for Frontends):在网关这种边缘节点上,针对同样的后端集群,裁剪、适配、聚合出适应不一样的前端服务,有助于后端的稳定,也有助于前端的赋能。
负载均衡
服务端负载均衡器是集中式的,同时为多个节点提供服务,而客户端负载均衡器是和服务实例一一对应的,而且与服务实例并存于同一个进程之内。
代理均衡器 / 代理客户端负载均衡器(Proxy Client-Side Load Balancer):对此前的客户端负载均衡器的改进,将原本嵌入在服务进程中的均衡器提取出来,放到边车代理中去实现,
服务与流量治理
容错性设计 Design for Failure
容错策略,指的是“面对故障,我们该做些什么”。
容错设计模式,指的是“要实现某种容错策略,我们该如何去做”。
7 种常见的容错策略:
- 第一种,故障转移(Failover)
如果调用的服务器出现故障,系统不会立即向调用者返回失败结果,而是自动切换到其他服务副本,尝试其他副本能否返回成功调用的结果,从而保证了整体的高可用性。 - 第二种,快速失败(Failfast)。
因为故障转移策略能够实施的前提,是服务具有幂等性。那对于非幂等的服务,重复调用就可能产生脏数据,引起的麻烦远大于单纯的某次服务调用失败。 - 第三种,安全失败(Failsafe)。
一种理想的容错策略是,即使旁路逻辑调用失败了,也当作正确来返回,如果需要返回值的话,系统就自动返回一个符合要求的数据类型的对应零值,然后自动记录一条服务调用出错的日志备查即可。 - 第四种,沉默失败(Failsilent)。
默认服务提供者一定时间内无法再对外提供服务,不再向它分配请求流量,并将错误隔离开来,避免对系统其他部分产生影响。 - 第五种,故障恢复(Failback)。
当服务调用出错了以后,将该次调用失败的信息存入一个消息队列中,然后由系统自动开始异步重试调用。 - 第六种,并行调用(Forking)。
一开始就同时向多个服务副本发起调用,只要有其中任何一个返回成功,那调用便宣告成功。
这种策略是在一些关键场景中,使用更高的执行成本换取执行时间和成功概率的策略。 - 第七种,广播调用(Broadcast)。
任何一个服务提供者出现异常都算调用失败。
通常被用于实现“刷新分布式缓存”这类的操作。
对比:
容错策略 | 优点 | 缺点 | 应用场景 |
---|---|---|---|
故障转移 | 系统自动处理,调用者对失败的信息不可见 | 会增加调用时间,也会导致额外的资源开销 | 调用幂等服务,对调用时间不敏感的场景 |
快速失败 | 调用者有对失败的处理完全控制权,不依赖服务的幂等性 | 调用者必须正确处理失败逻辑,如果只是一味地对外抛异常,容易引起雪崩 | 调用非幂等的服务,超时阈值较低的场景 |
安全失败 | 不影响主路逻辑 | 只适用于旁路调用 | 调用链中的旁路服务 |
沉默失败 | 控制错误不影响全局 | 出错的地方将在一段时间内不可用推荐用于旁路服务调用,或者对实时性要求不高的主路逻辑。重试任务可能产生堆积,重试仍然可能失败 | 频繁超时的服务 |
故障恢复 | 调用失败后自动重试,也不影响主路逻辑 | 额外消耗机器资源,大部分调用可能都是无用功 | 调用链中的旁路服务,对实时性要求不高的主路逻辑也可以使用 |
并行调用 | 尽可能在最短时间内获得最高的成功率 | 资源消耗大,失败的概率高 | 资源充足且对失败容忍度低的场景 |
广播调用 | 支持同时对批量的服务提供者发起调用 | 只适用于批量操作的场景 |
容错设计模式
- 断路器模式
通过代理(断路器对象)来一对一(一个远程服务对应一个断路器对象)地接管服务调用者的远程请求。
断路器会持续监控并统计服务返回的成功、失败、超时、拒绝等各种结果,当出现故障(失败、超时、拒绝)的次数达到断路器的阈值时,它的状态就自动变为“OPEN”。之后这个断路器代理的远程访问都将直接返回调用失败,而不会发出真正的远程服务请求。
通过断路器对远程服务进行熔断,就可以避免因为持续的失败或拒绝而消耗资源,因为持续的超时而堆积请求,最终可以避免雪崩效应的出现。
断路器做的事情是自动进行服务熔断,属于一种快速失败的容错策略的实现方法。上游服务降级。
- 舱壁隔离模式
服务隔离,就是避免某一个远程服务的局部失败影响到全局,而设置的一种止损方案。
对应的是容错策略中的失败静默策略。
一般来说,会选择将服务层面的隔离实现在服务调用端或者边车代理上,将系统层面的隔离实现在 DNS 或者网关处。
- 重试模式
判断是否应该且是否能够对一个服务进行重试时,要看是否同时满足下面 4 个条件:
- 第一,仅在主路逻辑的关键服务上进行同步的重试。
- 第二,仅对由瞬时故障导致的失败进行重试。
- 第三,仅对具备幂等性的服务进行重试。
- 第四,重试必须有明确的终止条件。
常用的终止条件有超时终止和次数终止两种。通常是重试 2~5 次。
容错策略和容错设计模式,最终目的都是为了避免服务集群中,某个节点的故障导致整个系统发生雪崩效应。
限流
限流:任何一个系统的运算、存储、网络资源都不是无限的,当系统资源不足以支撑外部超过预期的突发流量时,就应该要有取舍,建立面对超额流量自我保护的机制。
流量统计指标:
- 每秒事务数(Transactions per Second,TPS)
- 每秒请求数(Hits per Second,HPS)
- 每秒查询数(Queries per Second,QPS)
限流设计模式:
- 单机限流
- 流量计数器模式
- 滑动时间窗模式
- 漏桶模式
- 令牌桶模式
漏桶是从水池里往系统出水,令牌桶则是系统往排队机中放入令牌。
分布式限流:
一种常见的简单分布式限流方法,是将所有服务的统计结果都存入集中式缓存。
在令牌桶限流模式的基础上,进行“货币化改造”改造。
这里我们将用户 A 的额度表示为 QuanityA。
由于任何一个服务在响应请求时,都需要消耗集群中一定量的处理资源,所以在访问每个服务时都要求消耗一定量的“货币”。
假设服务 X 要消耗的额度表示为 CostX,那当用户 A 访问了 N 个服务以后,他剩余的额度 LimitN 就会表示为:
LimitN = QuanityA - ∑NCostX
此时,我们可以把剩余额度 LimitN 作为内部限流的指标,规定在任何时候,只要剩余额度 LimitN 小于等于 0 时,就不再允许访问其他服务了。
另外,这时还必须先发生一次网络请求,重新向令牌桶申请一次额度,成功后才能继续访问,不成功则进入降级逻辑。
除此之外的任何时刻,即 LimitN 不为 0 时,都无需额外的网络访问,因为计算 LimitN 是完全可以在本地完成的。
可靠通讯
零信任网络安全
安全不可能是绝对的,而是有成本的。
微服务的核心技术特征之一是分散治理(Decentralized Governance)。
安全模型:
- 基于边界的安全模型
- 云原生时代下的基于零信任网络的安全模型
中心思想:不应当以某种固有特征来自动信任任何流量。
基本特征:集中、共享的安全策略实施点,自动化、标准化的变更管理。
主要观点:- 零信任网络不等同于放弃在边界上的保护设施
- 身份只来源于服务
- 服务之间也没有固有的信任关系
- 集中、共享的安全策略实施点
- 受信的机器运行来源已知的代码
- 自动化、标准化的变更管理
- 强隔离性的工作负载
2019年,Google发表的《BeyondProd: A New Approach to Cloud-Native Security》。
BeyondCorp 和 BeyondProd 是谷歌最新一代安全框架的名字。
传统、边界安全模型 | 云原生、零信任安全模型 | 具体需求 |
---|---|---|
基于防火墙等设施,认为边界内可信 | 服务到服务通信需认证,环境内的服务之间默认没有信任 | 保护网络边界(仍然有效);服务之间默认没有互信 |
用于特定的IP和硬件(机器) | 资源利用率、重用、共享更好,包括IP和硬件 | 受信任的机器运行来源已知的代码 |
基于IP的身份 | 基于服务的身份 | 同上 |
服务运行在已知的、可预期的服务器上 | 服务可运行在环境中的任何地方,包括私有云/公有云混合部署 | 同上 |
安全相关的需求由应用来实现,每个应用单独实现 | 由基础设施来实现,基础设施中集成了共享的安全性要求。 | 集中策略实施点(ChokePoints),一致地应用到所有服务 |
对服务如何构建、评审、实施的安全需求的约束力较弱 | 安全相关的需求一致地应用到所以服务 | 同上 |
安全组件的可观测性较弱 | 有安全策略及其是否生效的全局视图 | 同上 |
发布不标准,发布频率较低 | 标准化的构建和发布流程,每个微服务变更独立,变更更频繁 | 简单、自动、标准化的变更发布流程 |
工作负载通常作为虚拟机部署或部署到物理主机,并使用物理机或管理程序进行隔离 | 封装的工作负载及其进程在共享的操作系统中运行,并有管理平台提供的某种机制来进行隔离 | 在共享的操作系统的工作负载之间进行隔离 |
零信任网络安全服务访问
PKI = Public Key Infrastructure 公开密钥基础设施
PKI 是构建传输安全层(Transport Layer Security,TLS)的必要基础。
根据认证的目标对象,认证分为两种类型:
- 服务认证/节点认证(Peer Authentication):以机器作为认证对象,即访问服务的流量来源是另外一个服务。
- 请求认证(Request Authentication):以人类作为认证对象,即访问服务的流量来自于最终用户。
JWK(JSON Web Key):常与 JWT 配合使用的就是一种存储密钥的纯文本格式。
在功能上,它和JKS(Java Key Storage)、P12(Predecessor of PKCS#12)、PEM(Privacy Enhanced Mail)等密钥格式没有本质差别。
JWKS(JSON Web Key Set):一组 JWK 的集合。
支持 JWKS 的系统,能通过 JWT 令牌 Header 中的 KID(Key ID)自动匹配出应该使用哪个 JWK 来验证签名。
可观测性
可观测性(Observability):可以由系统的外部输出推断其内部状态的程度。
可观测性的三个具体研究方向:日志收集、链路追踪和聚合度量。
日志(Logging)追踪(Tracing)度量(Metrics)
事件日志的职责是记录离散事件,通过这些记录事后分析出程序的行为;
追踪的主要目的是排查故障,比如分析调用链的哪一部分、哪个方法出现错误或阻塞,输入输出是否符合预期;
度量是指对系统中某一类信息的统计聚合,主要目的是监控和预警,当某些度量指标达到风险阈值时就触发事件,以便自动处理或者提醒管理员介入。
工业界的技术实现:
- 日志, Elastic Stack(ELK)技术栈
- 追踪,本身有较强的侵入性,通常是以插件式的探针来实现,这也决定了在追踪领域很难出现一家独大的情况
- 度量,Kubernetes 与 Prometheus
CNCF = Cloud Native Computing Foundation 云原生计算基金会:是一个开源软件基金会,致力于云原生技术的普及和可持续发展。
Kubernetes 是 CNCF 第一个孵化成功的项目,起源于 Google 的编排系统 Borg。
Prometheus 是 CNCF 第二个孵化成功的项目,起源于 Google 为 Borg 做的度量监控系统 BorgMon。
https://landscape.cncf.io/?license=apache-license-2-0
日志收集 Logging
- 输出
好的日志要能够毫无遗漏地记录信息、格式统一、内容恰当,而“恰当”的真正含义是指日志中不该出现的内容不要有,而该有的不要少。
不该出现的内容不要有:避免打印敏感信息,避免引用慢操作,避免打印追踪诊断信息,避免误导他人。
该出现的内容不要少:处理请求时的 TraceID,系统运行过程中的关键事件,启动时输出配置信息。
- 收集与缓冲
一种最常用的缓解压力的做法,是将日志接收者从 Logstash 和 Elasticsearch 转移至抗压能力更强的队列缓存。
比如在 Logstash 之前,架设一个 Kafka 或者 Redis 作为缓冲层,当面对突发流量,Logstash 或 Elasticsearch 的处理能力出现瓶颈时,就自动削峰填谷,这样甚至当它们短时间停顿,也不会丢失日志数据。
Elastic.co Beats框架:Elastic.co 公司把所有需要在服务节点中处理的工作,整理成以Libbeat为核心。
- Filebeat,使用 Golang 重写的一个功能较少、更轻量高效的日志收集器
- Metricbeat,用于聚合度量
- Heartbeat,用于心跳检测
- Functionbeat,用于无服务计算架构
- Auditbeat,用于收集 Linux 审计数据
- Journalbeat,用于收集 Linux Systemd Journald 日志
- Winlogbeat,用于收集 Windows 事件日志
- Packetbeat,用于网络包嗅探
……
- 加工与聚合
Logstash 的基本职能是把日志行中的非结构化数据,通过 Grok 表达式语法转换为表格那样的结构化数据。
在离散的日志中获得统计信息的两种解决方案:
- 通过 Elasticsearch 本身的处理能力做实时的聚合统计。
这是一种很便捷的方式,不过要消耗 Elasticsearch 服务器的运算资源。
一般用于应对即席查询。 - 在收集日志后自动生成某些常用的、固定的聚合指标,这种聚合就会在 Logstash 中通过聚合插件来完成。
更多是用于应对固定查询。
数据的存储与查询:
- 数据特征
日志是典型的基于时间的数据流。 - 数据价值
日志基本上只会以最近的数据为检索目标。 - 数据使用
分析日志很依赖全文检索和即席查询,这对实时性的要求就是处于实时与离线两者之间的“近实时”。
Kibana 宣传的核心能力是“探索数据并可视化”。
链路追踪 Tracing
一个完整的分布式追踪系统,
- 从广义上讲,应该由数据收集、数据存储和数据展示三个相对独立的子系统构成;
- 从狭义上讲,则就只是特指链路追踪数据的收集部分。
比如,Spring Cloud Sleuth,通常会搭配 Zipkin 作为数据展示,搭配 Elasticsearch 作为数据存储来组合使用。
追踪系统应考虑的点:
- 功能性 - 异构性
- 非功能性 - 低性能损耗,对应用透明,随应用扩展,持续的扩展
追踪系统根据数据收集方式的差异,可以分为三种主流的实现方式:
- 基于日志的追踪(Log-Based Tracing)
代表产品是 Spring Cloud Sleuth - 基于服务的追踪(Service-Based Tracing)
目前最为常见的追踪实现方式,被 Zipkin、SkyWalking、Pinpoint 等主流追踪系统广泛采用。
服务追踪的实现思路是通过某些手段给目标应用注入追踪探针(Probe),比如针对 Java 应用,一般就是通过 Java Agent 注入的。 - 基于边车代理的追踪(Sidecar-Based Tracing)
市场占有率最高的边车代理Envoy提供了相对完善的追踪功能,但没有提供自己的界面端和存储端,属于狭义的追踪系统,需要配合专门的 UI 与存储来使用。
Zipkin、SkyWalking、Jaeger、LightStep Tracing等系统,都可以接受来自于 Envoy 的追踪数据,充当它的界面端。
追踪规范化,CNCF OpenTracing,Google OpenCensus => CNCF OpenTelemetry
聚合度量 Metrics
度量(Metrics)是用经过聚合统计后的高维度信息,以最简单直观的形式来总结复杂的过程,为监控、预警提供决策支持。
度量的目的是揭示系统的总体运行状态。
度量可以分为三个相对独立的过程:客户端的指标收集、服务端的存储查询、终端的监控预警。
每个过程在系统中一般也会设置对应的组件来实现。
- 指标收集
指标的数据类型(Metrics Types)是可数的,所有通用的度量系统都是面向指标的数据类型来设计的。
度量器:
- 计数度量器(Counter):计数器就是对有相同量纲、可加减数值的合计量。
最常用的指标形式。
比如,服务调用次数,网站访问人数。 - 瞬态度量器(Gauge):表示某个指标在某个时点的数值,连加减统计都不需要。
比如,当前 Java 虚拟机堆内存的使用量,网站在线人数。 - 吞吐率度量器(Meter):用于统计单位时间的吞吐量,即单位时间内某个事件的发生次数。
比如,在交易系统中,常以 TPS 衡量事务吞吐率,即每秒发生了多少笔事务交易;
比如,港口的货运吞吐率常以“吨 / 每天”为单位计算,10 万吨 / 天的港口通常要比 1 万吨 / 天的港口的货运规模更大。 - 直方图度量器(Histogram):常见的二维统计图,它的两个坐标分别是统计样本和该样本对应的某个属性的度量,以长条图的形式记录具体数值。
比如,经济报告中,要衡量某个地区历年的 GDP 变化情况,常会以 GDP 为纵坐标、时间为横坐标构成直方图来呈现。 - 采样点分位图度量器(Quantile Summary):分位图是统计学中通过比较各分位数的分布情况的工具,主要用来验证实际值与理论值的差距,评估理论值与实际值之间的拟合度。
比如,“高考成绩一般符合正态分布”,指的是高考成绩高低分的人数都比较少,中等成绩的比较多,按不同分数段来统计人数,得出的统计结果一般能够与正态分布的曲线较好地拟合。 - 其它度量器,如:Timer、Set、Fast Compass、Cluster Histogram。
“如何将这些指标告诉服务端”,通常有两种解决方案:
- 拉取式采集(Pull-Based Metrics Collection):Pull 是指度量系统主动从目标系统中拉取指标
- 推送式采集(Push-Based Metrics Collection):Push 是指由目标系统主动向度量系统推送指标。
度量面向的是广义上的信息系统,它横跨存储(日志、文件、数据库)、通讯(消息、网络)、中间件(HTTP 服务、API 服务),直到系统本身的业务指标,甚至还会包括度量系统本身(部署两个独立的 Prometheus 互相监控是很常见的)。
存储查询
时序数据库(Time Series Database):用于存储跟随时间而变化的数据,并且以时间(时间点或者时间区间)来建立索引的数据库。
同时具有数据结构简单、数据量大的特点。
时间序列数据是历史烙印,它具有不变性、唯一性、有序性。监控预警
度量系统的组成:
- 广义上
- 客户端 Client:与目标系统进程在一起的 Agent,或者代表目标系统的 Exporter 等。
- 服务端 Server:负责调度、存储和提供查询能力。
Prometheus 的服务端是带存储的,但也有很多度量服务端需要配合独立的存储来使用。 - 终端 Backend:面向最终用户的UI 界面、监控预警功能等。
- 狭义上
只包括客户端和服务端,不包含终端。
Prometheus Exporter
Exporter 的作用是以HTTP协议返回符合 Prometheus 格式要求的文本数据给 Prometheus 服务器。
Prometheus 在2.0版本之前支持过 Protocol Buffer,目前已不再支持。
范围 | 常用Exporter |
---|---|
数据库 | MySQL Exporter、Redis Exporter、MongoDB Exporter、MSSQL Exporter等 |
硬件 | Apcupsd Exporter、loT Edison Exporter、IPMI Exporter、Node Exporter等 |
消息队列 | Beanstalkd Exporter、Kafka Exporter、NSQ Exporter、RabbitMQ Exporter等 |
存储 | Ceph Exporter、Gluster Exporter、HDFS Exporter、ScalelO Exporter等 |
HTTP服务 | Apache Exporter、HAProxy Exporter、Nginx Exporter等 |
API服务 | AWS ECS Exporter、Docker Cloud Exporter、Docker HubExporter、GitHub Exporter等 |
日志 | Fluentd Exporter、Grok Exporter等 |
监控系统 | Collectd Exporter、Graphite Exporter、InfluxDBExporter、Nagios Exporter、SNMP Exporter等 |
其它 | Blockbox Exporter、JIRA Exporter、Jenkins Exporter、Confluence Exporter等 |
不可变基础设施
云原生
云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。
云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式 API。
这些技术能够构建容错性好、易于管理和便于观察的松耦合系统。结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统作出频繁和可预测的重大变更软件兼容性
- ISA 兼容:目标机器指令集兼容性。
比如 ARM 架构的计算机无法直接运行面向 x86 架构编译的程序。 - ABI 兼容:目标系统或者依赖库的二进制兼容性。
比如 Windows 系统环境中无法直接运行 Linux 的程序,DirectX 12 的游戏无法运行在 DirectX 9 之上。 - 环境兼容:目标环境的兼容性。
比如没有正确设置的配置文件、环境变量、注册中心、数据库地址、文件系统的权限等等,当任何一个环境因素出现错误,都会让你的程序无法正常运行。
ISA = Instruction Set Architecture 指令集架构:
是计算机体系结构中与程序设计有关的部分,包含了基本数据类型、指令集、寄存器、寻址模式、存储体系、中断、异常处理以及外部 I/O。
指令集架构包含一系列的 Opcode 操作码(机器语言),以及由特定处理器执行的基本命令。
ABI = Application Binary Interface 应用二进制接口:
是应用程序与操作系统之间或其他依赖库之间的低级接口。
ABI 涵盖了各种底层细节,如数据类型的宽度大小、对象的布局、接口调用约定等等。
ABI 允许编译好的目标代码在使用兼容 ABI 的系统中无需改动就能直接运行。
API = Application Programming Interface 应用程序编程接口:
定义源代码和库之间的接口,因此同样的代码可以在支持这个 API 的任何系统中编译。
- 虚拟化技术
虚拟化技术:把使用仿真(Emulation)以及虚拟化(Virtualization)技术来解决以上三项软件兼容性问题的方法。
根据抽象目标与兼容性高低的不同,虚拟化技术分为五类:
- 指令集虚拟化(ISA Level Virtualization)
即通过软件来模拟不同 ISA 架构的处理器工作过程,它会把虚拟机发出的指令转换为符合本机 ISA 的指令。
指令集虚拟化就是仿真,它提供了几乎完全不受局限的兼容性,甚至能做到直接在 Web 浏览器上运行完整操作系统这种令人惊讶的效果。
但是,由于每条指令都要由软件来转换和模拟,它也是性能损失最大的虚拟化技术。
代表为QEMU和Bochs。 - 硬件抽象层虚拟化(Hardware Abstraction Level Virtualization)
即以软件或者直接通过硬件来模拟处理器、芯片组、内存、磁盘控制器、显卡等设备的工作过程。
硬件抽象层虚拟化既可以使用纯软件的二进制翻译来模拟虚拟设备,也可以由硬件的Intel VT-d、AMD-Vi这类虚拟化技术,将某个物理设备直通(Passthrough)到虚拟机中使用。
代表为VMware ESXi和Hyper-V。 - 操作系统层虚拟化(OS Level Virtualization) / 容器化(Containerization)
即不会提供真实的操作系统,而是会采用隔离手段,使得不同进程拥有独立的系统资源和资源配额,
看起来它好像是独享了整个操作系统一般,但其实系统的内核仍然是被不同进程所共享的。
容器化仅仅是虚拟化的一个子集,它只能提供操作系统内核以上的部分 ABI 兼容性与完整的环境兼容性。
容器化牺牲了一定的隔离性与兼容性,换来的是比前两种虚拟化更高的启动速度、运行性能和更低的执行负担。 - 运行库虚拟化(Library Level Virtualization)
即选择使用软件翻译的方法来模拟系统,它是以一个独立进程来代替操作系统内核,来提供目标软件运行所需的全部能力。
代表为WINE(Wine Is Not an Emulator,一款在 Linux 下运行 Windows 程序的软件),WSL(Windows Subsystem for Linux Version 1)。 - 语言层虚拟化(Programming Language Level Virtualization)
即由虚拟机将高级语言生成的中间代码,转换为目标机器可以直接执行的指令。
代表为 Java JVM ,.NET CLR。
虚拟化容器
容器
隔离文件:chroot
隔离访问:namespaces
Linux 的名称空间是一种由内核直接提供的全局资源封装,它是内核针对进程设计的访问隔离机制。隔离资源:cgroups = Control Groups 控制群组
直接由内核提供功能,用于隔离或者说分配并限制某个进程组能够使用的资源配额。
资源配额包括处理器时间、内存大小、磁盘I/O速度,等等。
控制组子系统 | 功能 |
---|---|
blkio | 为块设备(如磁盘,固态硬盘,USB等等)设定I/O限额 |
cpu | 控制cgroups中进程的处理器占用比率 |
cpuacct | 自动生成cgroups中进程所使用的处理器时间的报告 |
cpuset | 为cgroups中的进程分配独立的处理器(包括多路系统的处理器,多核系统的处理器核心) |
devices | 设置cgroups中的进程访问某个设备的权限(读、写、创建三种权限) |
freezer | 挂起或者恢复cgroups中的进程 |
memory | 设定cgroups中进程使用内存的限制,并自动生成内存资源使用报告 |
net_cis | 使用等级识别符标记网络数据包,可允许Linux流量控制程序识别从具体cgroups中生成的数据包 |
net_prio | 用来设置网络流量的优先级 |
hugetlb | 主要针对于HugeTLB系统进行限制 |
perf_event | 允许Perf工具基于cgroups分组做性能监测 |
Linux Kernel 5.6+,Linux名称空间支持的八种资源的隔离:
名称空间 | 隔离内容 | 内核版本 |
---|---|---|
Mount | 隔离文件系统,功能上大致可以类比chroot | 2.4.19 |
UTS | 隔离主机的Hostname、Domain names | 2.6.19 |
IPC | 隔离进程间通信的渠道(可以回顾“远程服务调用”中对IPC的介绍) | 2.6.19 |
PID | 隔离进程编号,无法看到其他名称空间中的PID,意味着无法对其他进程产生影响 | 2.6.24 |
Network | 隔离网络资源,如网卡、网络栈、IP地址、端口,等等 | 2.6.29 |
User | 隔离用户和用户组 | 3.8 |
Cgroup | 隔离cgourps信息,进程有自己的cgroups的根目录视图(在/proc/self/cgroup不会看到整个系统的信息) | 4.6 |
Time | 隔离系统时间,2020年3月最新的5.6内核开始支持进程独立设置系统时间 | 5.6 |
封装系统:LXC
LXC = LinuX Containers Linux 容器封装应用:Docker
为什么要用 Docker 而不是 LXC?(Why would I use Docker over plain LXC?)
Solomon Hykes (Stackoverflow,2013),
Docker 除了包装来自 Linux 内核的特性之外,它的价值还在于:
- 跨机器的绿色部署:Docker 定义了一种将应用及其所有的环境依赖都打包到一起的格式,仿佛它原本就是绿色软件一样。而 LXC 并没有提供这样的能力,使用 LXC 部署的新机器很多细节都要依赖人的介入,虚拟机的环境基本上肯定会跟你原本部署程序的机器有所差别。
- 以应用为中心的封装:Docker 封装应用而非封装机器的理念贯穿了它的设计、API、界面、文档等多个方面。相比之下,LXC 将容器视为对系统的封装,这局限了容器的发展。
- 自动构建:Docker 提供了开发人员从在容器中构建产品的全部支持,开发人员无需关注目标机器的具体配置,就可以使用任意的构建工具链,在容器中自动构建出最终产品。
- 多版本支持:Docker 支持像 Git 一样管理容器的连续版本,进行检查版本间差异、提交或者回滚等操作。从历史记录中,你可以查看到该容器是如何一步一步构建成的,并且只增量上传或下载新版本中变更的部分。
- 组件重用:Docker 允许将任何现有容器作为基础镜像来使用,以此构建出更加专业的镜像。
- 共享:Docker 拥有公共的镜像仓库,成千上万的 Docker 用户在上面上传了自己的镜像,同时也使用他人上传的镜像。
- 工具生态:Docker 开放了一套可自动化和自行扩展的接口,在此之上用户可以实现很多工具来扩展其功能,比如容器编排、管理界面、持续集成,等等。
- 封装集群:Kubernetes
以 Docker 为代表的容器引擎,是把软件的发布流程从分发二进制安装包,转变为了直接分发虚拟化后的整个运行环境,让应用得以实现跨机器的绿色部署;
以 Kubernetes 为代表的容器编排框架,是把大型软件系统运行所依赖的集群环境也进行了虚拟化,让集群得以实现跨数据中心的绿色部署,并能够根据实际情况自动扩缩。
容器构建系统
UTS = UNIX Time-Sharing System
- 隔离与协作
Pod 两大最基本的职责:
- 扮演容器组的角色,满足容器共享名称空间的需求
- 实现原子性调度
Pod 们的协作方式:
- 对于普通非亲密的容器,它们一般以网络交互方式(其他的如共享分布式存储来交换信息,也算跨网络)协作;
- 对于亲密协作的容器,它们被调度到同一个集群节点上,可以通过共享本地磁盘等方式协作;
- 超亲密的协作,是特指多个容器位于同一个 Pod 这种特殊关系,它们将默认共享以下名称空间:
- UTS 名称空间:所有容器都有相同的主机名和域名。
- 网络名称空间:所有容器都共享一样的网卡、网络栈、IP 地址,等等。同一个 Pod 中不同容器占用的端口不能冲突。
- IPC 名称空间:所有容器都可以通过信号量或者 POSIX 共享内存等方式通信。
- 时间名称空间:所有容器都共享相同的系统时间。
同一个 Pod 的容器,只有 PID 名称空间和文件名称空间默认是隔离的。
Kubernetes 中计算资源的设计意图:
- 容器(Container):是镜像管理的最小单位。
延续了自 Docker 以来一个容器封装一个应用进程的理念。 - 生产任务(Pod):是资源调度的最小单位。
补充了容器化后缺失的与进程组对应的“容器组”的概念,Pod 中的容器共享 UTS、IPC、网络等名称空间。 - 节点(Node):是硬件单元的最小单位。
节点是处理器和内存等资源的资源池。
对应于集群中的单台机器,可以是生产环境中的物理机或是云计算环境中的虚拟节点, - 集群(Cluster):是处理元数据的最小单位。
对应于整个集群,Kubernetes 提倡的理念是面向集群来管理应用。 - 集群联邦(Federation):对应于多个集群,通过联邦可以统一管理多个 Kubernetes 集群。
一种常见应用是支持跨可用区域多活、跨地域容灾的需求。
- 韧性与弹性
服务的韧性(Resilience)与弹性(Elasticity):在云原生时代中,故障恢复、滚动更新、自动扩缩等特性。
滚动更新(Rolling Updates),是指先停止少量旧副本,维持大量旧副本继续提供服务,当停止的旧副本更新成功,新副本可以提供服务以后,再重复以上操作,直至所有的副本都更新成功。
贯穿整个 Kubernetes 的两大设计理念:资源与控制器。
Kubernates 的资源对象
Kubernetes 已内置支持相当多的资源对象,并且还可以使用CRD(Custom Resource Definition)来自定义扩充,
可以使用 kubectl api-resources 来查看。
根据用途分类,常见的资源:
- 用于描述如何创建、销毁、更新、扩缩 Pod,包括:Autoscaling(HPA)、CronJob、DaemonSet、Deployment、Job、Pod、ReplicaSet、StatefulSet
- 用于配置信息的设置与更新,包括:ConfigMap、Secret
- 用于持久性地存储文件或者 Pod 之间的文件共享,包括:Volume、LocalVolume、PersistentVolume、PersistentVolumeClaim、StorageClass
- 用于维护网络通信和服务访问的安全,包括:SecurityContext、ServiceAccount、Endpoint、NetworkPolicy
- 用于定义服务与访问,包括:Ingress、Service、EndpointSlice
- 用于划分虚拟集群、节点和资源配额,包括:Namespace、Node、ResourceQuota
Kubernates 的控制器
与资源相对应的,只要是实际状态有可能发生变化的资源对象,就通常都会由对应的控制器进行追踪,每个控制器至少会追踪一种类型的资源。
一些常见的控制器,按照启动情况分类:
- 必须启用的控制器:
EndpointController、ReplicationController、PodGCController、ResourceQuotaController、NamespaceController、ServiceAccountController、GarbageCollectorController、DaemonSetController、JobController、DeploymentController、ReplicaSetController、HPAController、DisruptionController、StatefulSetController、CronJobController、CSRSigningController、CSRApprovingController、TTLController - 默认启用的可选控制器,可通过选项禁止:T
okenController、NodeController、ServiceController、RouteController、PVBinderController、AttachDetachController - 默认禁止的可选控制器,可通过选项启用:
BootstrapSignerController、TokenCleanerController
为了管理众多资源控制器,Kubernetes 设计了统一的控制器管理框架(kube-controller-manager)来维护这些控制器的正常运作,并设计了统一的指标监视器(kube-apiserver)。
通过副本集(ReplicaSet)来创建 Pod。
应用为中心的封装
无状态应用与有状态应用的区别是应用程序是否要自己持有其运行所需的数据。
无状态应用(Stateless Application):程序每次运行都跟首次运行一样,不依赖之前任何操作所遗留下来的痕迹。
有状态应用(Stateful Application):如果程序推倒重来之后,用户能察觉到该应用已经发生变化
- 无状态
Kustomize
用配置文件来配置文件。Helm
Kubernetes 比作云原生操作系统,Helm 就是成为这个操作系统上面的应用商店与包管理工具。
Helm 提出了与 Linux 包管理直接对应的 Chart 格式和 Repository 应用仓库。
Helm 社区维护了公开的 Stable 和 Incubator 的中央仓库。
Helm 为了支持对同一个 Chart 包进行多次部署,每次安装应用都会产生一个 Release,Release 就相当于该 Chart 的安装实例。
- 有状态
Operator
Operator 是使用自定义资源(Custom Resource,是 CRD 的实例)管理应用及其组件的自定义 Kubernetes 控制器。
高级配置和设置由用户在自定义资源中提供。
Kubernetes Operator 基于嵌入在 Operator 逻辑中的最佳实践,将高级指令转换为低级操作。
Kubernetes Operator 监视自定义资源类型并采取特定于应用的操作,确保当前状态与该资源的理想状态相符。站在 Kubernetes 的角度看,是否有状态的本质差异在于,有状态应用会对某些外部资源有绑定性的直接依赖。
为了管理好那些与应用实例密切相关的状态信息,Kubernetes 从 1.9 版本开始正式发布了 StatefulSet 及对应的 StatefulSetController。
与普通 ReplicaSet 中的 Pod 相比,由 StatefulSet 管理的 Pod 具备几项额外特性:- Pod 会按顺序创建和按顺序销毁:
StatefulSet 中的各个 Pod 会按顺序地创建出来,而且,再创建后面的 Pod 之前,必须要保证前面的 Pod 已经转入就绪状态。
如果要销毁 StatefulSet 中的 Pod,就会按照与创建顺序的逆序来执行。 - Pod 具有稳定的网络名称:
Kubernetes 中的 Pod 都具有唯一的名称,在普通的副本集中,这是靠随机字符产生的;
而在 StatefulSet 中管理的 Pod,会以带有顺序的编号作为名称,而且能够在重启后依然保持不变。 - Pod 具有稳定的持久存储:
StatefulSet 中的每个 Pod 都可以拥有自己独立的 PersistentVolumeClaim 资源。
即使 Pod 被重新调度到其他节点上,它所拥有的持久磁盘也依然会被挂载到该 Pod。
Operator 将简洁的高级指令转化为 Kubernetes 中具体操作的方法,跟 Helm 或 Kustomize 的思路并不一样:
Helm 和 Kustomize 最终仍然是依靠 Kubernetes 的内置资源,来跟 Kubernetes 打交道的;
Operator 则是要求开发者自己实现一个专门针对该自定义资源的控制器,在控制器中维护自定义资源的期望状态。- Pod 会按顺序创建和按顺序销毁:
OAM
OAM = Open Application Mode 开放应用模型
OAM 思想的核心是将开发人员、运维人员与平台人员的关注点分离,
开发人员关注业务逻辑的实现,运维人员关注程序的平稳运行,平台人员关注基础设施的能力与稳定性。嘲讽之”YAML Engineer”
OAM 对云原生应用的定义是:由一组相互关联但又离散独立的组件构成,这些组件实例化在合适的运行时上,由配置来控制行为并共同协作提供统一的功能。
OAM 定义的应用:
一个Application由一组Components构成,每个Component的运行状态由Workload描述,每个Component可以施加Traits来获取额外的运维能力,
可以使用Application Scopes将Components划分到一或者多个应用边界中,便于统一做配置、限制、管理。
把Components、Traits和Scopes组合在一起实例化部署,形成具体的Application Configuration,以便解决应用的多实例部署与升级。一些概念的具体含义:
Components(服务组件):
OAM 的 Component 不仅仅是特指构成应用“整体”的一个“部分”,还抽象出那些应该由开发人员去关注的元素。
比如应用的名字、自述、容器镜像、运行所需的参数,等等。Workload(工作负荷):
Workload 决定了应用的运行模式,每个 Component 都要设定自己的 Workload 类型。
OAM 按照“是否可访问、是否可复制、是否长期运行”预定义了六种 Workload 类型。
如果有必要,使用者还可以通过 CRD 与 Operator 去扩展。工作负荷 可访问 可复制 长期运行 Server √ √ √ Singleton Server √ × √ Worker × √ √ Singleton Worker × × √ Task × √ × Singleton Task × × × Traits(运维特征):
Traits 可以用来封装模块化后的运维能力,它可以针对运维中的可重复操作预先设定好一些具体的 Traits,
比如日志收集 Trait、负载均衡 Trait、水平扩缩容 Trait,等等。
这些预定义的 Traits 定义里,会注明它们可以作用于哪种类型的工作负荷,还包括能填哪些参数、哪些必填选填项、参数的作用描述是什么,等等。Application Scopes(应用边界):
多个 Component 共同组成一个 Scope,可以根据 Component 的特性或作用域来划分 Scope。
比如,具有相同网络策略的 Component 放在同一个 Scope 中,具有相同健康度量策略的 Component 放到另一个 Scope 中。
一个 Component 也可能属于多个 Scope,比如,一个 Component 完全可能既需要配置网络策略,也需要配置健康度量策略。Application Configuration(应用配置):
将 Component(必须)、Trait(必须)、Scope(非必须)组合到一起进行实例化,就形成了一个完整的应用配置。
OAM 使用这些自定义资源,对原先 All-in-One 的复杂配置做了一定层次的解耦:
- 开发人员负责管理 Component;
- 运维人员将 Component 组合并绑定 Trait,把它变成 Application Configuration;
- 平台人员或基础设施提供方负责提供 OAM 的解释能力,将这些自定义资源映射到实际的基础设施。
其它的应用封装技术,如CNAB、Armada、Pulumi。
容器间网络
Linux网络虚拟化
- Linux 系统下的网络通信模型
Linux 网络协议栈,简称网络栈 / 协议栈:
Linux 系统的通信过程无论是按理论上的 OSI 七层模型,还是以实际上的 TCP/IP 四层模型来解构,都明显地呈现出“逐层调用,逐层封装”的特点,这种逐层处理的方式与栈结构,比如程序执行时的方法栈很类似。
程序发送数据做的是层层封包,加入协议头,传给下一层;而接受数据则是层层解包,提取协议体,传给上一层。
Netfilter 框架:从 Linux Kernel 2.4 版开始,内核开放了一套通用的、可供代码干预数据在协议栈中流转的过滤器框架。
Netfilter 框架是 Linux 防火墙和网络的主要维护者罗斯迪·鲁塞尔(Rusty Russell)提出并主导设计的,它围绕网络层(IP 协议)的周围,埋下了五个钩子(Hooks),每当有数据包流到网络层,经过这些钩子时,就会自动触发由内核模块注册在这里的回调函数,程序代码就能够通过回调来干预 Linux 的网络通信。
这五个钩子分别是:
- PREROUTING:来自设备的数据包进入协议栈后,就会立即触发这个钩子。
如果 PREROUTING 钩子在进入 IP 路由之前触发了,就意味着只要接收到的数据包,无论是否真的发往本机,也都会触发这个钩子。
它一般是用于目标网络地址转换(Destination NAT,DNAT)。 - INPUT:报文经过 IP 路由后,如果确定是发往本机的,将会触发这个钩子,
它一般用于加工发往本地进程的数据包。 - FORWARD:报文经过 IP 路由后,如果确定不是发往本机的,将会触发这个钩子,
它一般用于处理转发到其他机器的数据包。 - OUTPUT:从本机程序发出的数据包,在经过 IP 路由前,将会触发这个钩子,
它一般用于加工本地进程的输出数据包。 - POSTROUTING:从本机网卡出去的数据包,无论是本机的程序所发出的,还是由本机转发给其他机器的,都会触发这个钩子,
它一般是用于源网络地址转换(Source NAT,SNAT)。
iptables 的设计意图是因为 Netfilter 的钩子回调虽然很强大,但毕竟要通过程序编码才够能使用,并不适合系统管理员用来日常运维,而它的价值就是以配置去实现原本用 Netfilter 编码才能做到的事情。
一般来说,iptables 会先把用户常用的管理意图总结成具体的行为,预先准备好,然后就会在满足条件的时候自动激活行为,
比如以下几种常见的 iptables 预置的行为:
- DROP:直接将数据包丢弃。
- REJECT:给客户端返回 Connection Refused 或 Destination Unreachable 报文。
- QUEUE:将数据包放入用户空间的队列,供用户空间的程序处理。
- RETURN:跳出当前链,该链里后续的规则不再执行。
- ACCEPT:同意数据包通过,继续执行后续的规则。
- JUMP:跳转到其他用户自定义的链继续执行。
- REDIRECT:在本机做端口映射。
- MASQUERADE:地址伪装,自动用修改源或目标的 IP 地址来做 NAT
- LOG:在 /var/log/messages 文件中记录日志信息。
……
iptables 内置了五张不可扩展的规则表(其中的 security 表并不常用,很多资料只计算了前四张表):
- raw 表:用于去除数据包上的连接追踪机制(Connection Tracking)。
- mangle 表:用于修改数据包的报文头信息,比如服务类型(Type Of Service,ToS)、生存周期(Time to Live,TTL),以及为数据包设置 Mark 标记,典型的应用是链路的服务质量管理(Quality Of Service,QoS)。
- nat 表:用于修改数据包的源或者目的地址等信息,典型的应用是网络地址转换(Network Address Translation)。
- filter 表:用于对数据包进行过滤,控制到达某条链上的数据包是继续放行、直接丢弃或拒绝(ACCEPT、DROP、REJECT),典型的应用是防火墙。
- security 表:用于在数据包上应用SELinux,这张表并不常用。
五张规则表的优先级,从高到低:raw -> mangle -> nat -> filter -> security。
每张表与能够使用到的链的对应关系:
PREROUTING | POSTROUTING | FORWARD | INPUT | OUTPUT | |
---|---|---|---|---|---|
raw | √ | × | × | × | √ |
mangle | √ | √ | √ | √ | √ |
nat (Source) | × | √ | × | √ | × |
nat (Destination) | √ | × | × | × | √ |
filter | × | × | √ | √ | √ |
security | × | × | √ | √ | √ |
- 虚拟化网络设备
- 网卡:tun/tap、veth
直连网线是两头采用同一种标准的网线。
交叉网线是指一头是 T568A 标准,另外一头是 T568B 标准的网线。
网卡对网卡这样的同类设备,需要使用交叉线序的网线来连接,
网卡到交换机、路由器就采用直连线序的网线,
不过现在的网卡大多带有线序翻转功能,直连线也可以网卡对网卡地连通了。
交换机:Linux Bridge
网络:VXLAN = Virtual eXtensible LAN
VXLAN 带来了很高的灵活性、扩展性和可管理性
VXLAN也带来了额外的复杂度和性能开销:传输效率的下降,传输性能的下降。软件定义网络(Software Defined Network,SDN):
SDN 的核心思路是在物理的网络之上,再构造一层虚拟化的网络,把控制平面和数据平面分离开来,实现流量的灵活控制,为核心网络及应用的创新提供良好的平台。
SDN 里,位于下层的物理网络被称为 Underlay,它着重解决网络的连通性与可管理性;
位于上层的逻辑网络被称为 Overlay,它着重为应用提供与软件需求相符的传输服务和网络拓扑。
副本网卡:MACVLAN
- 容器间通信
Docker 提供的三种开箱即用的网络方案:
- 桥接模式,使用
--network=bridge
指定,这种也是未指定网络参数时的默认网络。
桥接模式下,Docker 会为新容器分配独立的网络名称空间,创建好 veth pair,一端接入容器,另一端接入到 docker0 网桥上。
Docker 会为每个容器自动分配好 IP 地址,默认配置下的地址范围是 172.17.0.0/24,docker0 的地址默认是 172.17.0.1,并且会设置所有容器的网关均为 docker0,这样所有接入同一个网桥内的容器,可以直接依靠二层网络来通信,在此范围之外的容器、主机就必须通过网关来访问。 - 主机模式,使用
--network=host
指定。
主机模式下,Docker 不会为新容器创建独立的网络名称空间,这样容器一切的网络设施,比如网卡、网络栈等,都会直接使用宿主机上的,容器也就不会拥有自己独立的 IP 地址。
在这个模式下与外界通信,也不需要进行 NAT 转换,没有性能损耗,
但它的缺点也十分明显,因为没有隔离,就无法避免网络资源的冲突,比如端口号就不允许重复。 - 空置模式,使用
--network=none
指定。
空置模式下,Docker 会给新容器创建独立的网络名称空间,但是不会创建任何虚拟的网络设备,此时容器能看到的只有一个回环设备(Loopback Device)而已。
提供这种方式是为了方便用户去做自定义的网络配置,比如自己增加网络设备、自己管理 IP 地址,等等。
Docker 还支持由用户自行创建的网络,比如:
- 容器模式,创建容器后使用
--network=container:容器名称
指定。
容器模式下,新创建的容器将会加入指定的容器的网络名称空间,共享一切的网络资源,但其他资源,比如文件、PID 等默认仍然是隔离的。
两个容器间可以直接使用回环地址(localhost)通信,端口号等网络资源不能有冲突。 - MACVLAN 模式,使用
docker network create -d macvlan
创建。
这种网络模式允许为容器指定一个副本网卡,容器通过副本网卡的 MAC 地址来使用宿主机上的物理设备,所以在追求通信性能的场合,这种网络是最好的选择。
注意,Docker 的 MACVLAN 只支持 Bridge 通信模式,所以在功能表现上跟桥接模式是类似的。 - Overlay 模式,使用
docker network create -d overlay
创建。
Docker 说的 Overlay 网络,实际上就是特指 VXLAN,这种网络模式主要用于 Docker Swarm 服务之间进行通信。
然而由于 Docker Swarm 败给了 Kubernetes,并没有成为主流,所以这种网络模式实际上很少被人使用。
容器网络与生态
CNM = Container Network Model
CNI = Container Networking Interface
Working as Intended 工作符合预期结果
容器网络标准的目的,就是为了把网络功能从容器运行时引擎、或者容器编排系统中剥离出去。
插件,在形式上也就是一个可执行文件,再配上相应的 Manifests 描述。
从程序功能上看,CNM 和 CNI 的网络插件提供的能力,都可以划分为网络的管理与 IP 地址的管理两类,而插件可以选择只实现其中的某一个,也可以全部都实现。
- 管理网络创建与删除
- 管理 IP 地址分配与回收
kubenet是 kubelet 内置的一个非常简单的网络,它是采用网桥来解决 Pod 间通信。
kubenet 会自动创建一个名为 cbr0 的网桥,当有新的 Pod 启动时,会由 kubenet 自动将其接入 cbr0 网桥中,再将控制权交还给 kubelet,完成后续的 Pod 创建流程。
kubenet 采用 Host-Local 的 IP 地址管理方式,具体来说是根据当前服务器对应的 Node 资源上的PodCIDR字段所设的网段,来分配 IP 地址。
当有新的 Pod 启动时,会由本地节点的 IP 段中分配一个空闲的 IP 给 Pod 使用。
跨主机通信的网络实现方式,有三种模式:
Overlay 模式
Overlay 网络,是一种虚拟化的上层逻辑网络。
好处在于它不受底层物理网络结构的约束,有更大的自由度,更好的易用性;
坏处是由于额外的包头封装,导致信息密度降低,额外的隧道封包解包会导致传输性能下降。
常见的 Overlay 网络插件有 Flannel(VXLAN 模式)、Calico(IPIP 模式)、Weave,等等。路由模式
路由模式是属于 Underlay 模式的一种特例。
相比起 Overlay 网络,路由模式的主要区别在于,它的跨主机通信是直接通过路由转发来实现的,因而不需要在不同主机之间进行隧道封包。
好处是性能相比 Overlay 网络有明显提升;
坏处是路由转发要依赖于底层网络环境的支持,并不是你想做就能做到的。
常见的路由网络有 Flannel(HostGateway 模式)、Calico(BGP 模式)等等。Underlay 模式
这里的 Underlay 模式特指让容器和宿主机处于同一网络,两者拥有相同的地位的网络方案。
Underlay 网络要求容器的网络接口能够直接与底层网络进行通信,该模式是直接依赖于虚拟化设备与底层网络能力的。
常见的 Underlay 网络插件,有 MACVLAN、SR-IOV(Single Root I/O Virtualization)等。
实际上,对于真正的大型数据中心、大型系统来说,Underlay 模式才是最有发展潜力的网络模式。
这种方案能够最大限度地利用硬件的能力,往往有着最优秀的性能表现。
但也是由于它直接依赖于硬件与底层网络环境,必须根据软、硬件情况来进行部署,所以很难能做到 Overlay 网络那样的开箱即用的灵活性。
容器持久化存储
Kubernetes 存储设计理念
容器数据持久化方案:
- docker
- bind Mount - Volume
- kubernetes
- pvc –> pv(手工静态分配)
- pvc –> storageClass –> Provision(动态分配)
Docker 内建支持了三种挂载类型,分别是:
- Bind,
--mount type=bind
- Volume,
--mount type=volume
- tmpfs,
--mount type=tmpfs
,主要用于在内存中读写临时数据。
Mount 和 Volume 都是来源于操作系统的常用术语。
Mount 是动词,表示将某个外部存储挂载到系统中;
Volume 是名词,表示物理存储的逻辑抽象,目的是为物理存储提供有弹性的分割方式。
Kubernetes 是一个工业级的、面向生产应用的容器编排系统。
概念:Volume、PersistentVolume、PersistentVolumeClaim、Provisioner、StorageClass、Volume Snapshot、Volume Snapshot Class、Ephemeral Volumes、FlexVolume Driver、Container Storage Interface、CSI Volume Cloning、Volume Limits、Volume Mode、Access Modes、Storage Capacity……
操作:Mount、Bind、Use、Provision、Claim、Reclaim、Reserve、Expand、Clone、Schedule、Reschedule……
- Static Provisioning
Kubernetes 把 Volume 分为了持久化的 PersistentVolume 和非持久化的普通 Volume 两类。
PersistentVolume 是指能够将数据进行持久化存储的一种资源对象。
A PersistentVolume (PV) is a piece of storage in the cluster that has been provisioned by an administrator.
A PersistentVolumeClaim (PVC) is a request for storage by a user.
PersistentVolume 是由管理员负责提供的集群存储。
PersistentVolumeClaim 是由用户负责提供的存储请求。
apiVersion: v1
kind: PersistentVolume
metadata:
name: nginx-html
spec:
capacity:
storage: 5Gi # 存储的最大容量为5GB
accessModes:
- ReadWriteOnce # 存储的访问模式为RWO
persistentVolumeReclaimPolicy: Retain # 存储的回收策略是Retain
nfs: # 存储驱动是NFS
path: /html
server: 172.17.0.2
存储的访问模式:
- ReadWriteOnce:RWO,只能被一个节点读写挂载
- ReadOnlyMany:ROX,可以被多个节点以只读方式挂载
- ReadWriteMany:RWX,可以被多个节点读写挂载
存储的回收策略:
- Retain:在 Pod 被销毁时并不会删除数据。
- Recycle:在 Pod 被销毁时,由 Kubernetes 自动执行 rm -rf /volume/* 命令来自动删除资料
- Delete:让 Kubernetes 自动调用 AWS EBS、GCE PersistentDisk、OpenStack Cinder 云存储的删除指令
存储驱动:
- NFS、AWS EBS、GCE PD、iSCSI、RBD(Ceph Block Device)、GlusterFS、HostPath,等等。
- Dynamic Provisioning
容器是镜像的运行时实例,为了保证镜像能够重复地产生出具备一致性的运行时实例,必须要求镜像本身是持久而稳定的,这就决定了在容器中发生的一切数据变动操作,都不能真正写入到镜像当中,否则必然会破坏镜像稳定不变的性质。
为此,容器中的数据修改操作,大多是基于写入时复制(Copy-on-Write)策略来实现的,容器会利用叠加式文件系统(OverlayFS)的特性,在用户意图对镜像进行修改时,自动将变更的内容写入到独立区域,再与原有数据叠加到一起,使其外观上看起来像是“覆盖”了原有内容。这种改动通常都是临时的,一旦容器终止运行,这些存储于独立区域中的变动信息也将被一并移除,不复存在。所以可见,如果不去进行额外的处理,容器默认是不具备持久化存储能力的。
而另一方面,容器作为信息系统的运行载体,必定会产生出有价值的、应该被持久保存的信息,比如扮演数据库角色的容器,大概没有什么系统能够接受数据库像缓存服务一样,重启之后会丢失全部数据;多个容器之间也经常需要通过共享存储来实现某些交互操作。
Kubernetes 存储扩展架构
把接入或移除外部存储这件事情,分解为了以下三个操作:
- 决定应准备(Provision)何种存储:
Provision 可类比为给操作系统扩容而购买了新的存储设备。
这步确定了接入存储的来源、容量、性能以及其他技术参数,它的逆操作是移除(Delete)存储。 - 将准备好的存储附加(Attach)到系统中:
Attach 可类比为将存储设备接入操作系统,此时尽管设备还不能使用,但你已经可以用操作系统的fdisk -l命令查看到设备。
这步确定了存储的设备名称、驱动方式等面向系统侧的信息,它的逆操作是分离(Detach)存储设备。 - 将附加好的存储挂载(Mount)到系统中:
Mount 可类比为将设备挂载到系统的指定位置,也就是操作系统中mount命令的作用。
这步确定了存储的访问目录、文件系统格式等面向应用侧的信息,它的逆操作是卸载(Unmount)存储设备。
它们会分别被 Kubernetes 通过两个控制器及一个管理器来进行调用,这些控制器、管理器的作用如下:
- PV 控制器(PersistentVolume Controller)
Kubernetes 里所有的控制器都遵循着相同的工作模式,即让实际状态尽可能接近期望状态。
PV 控制器的期望状态有两个,分别是“所有未绑定的 PersistentVolume 都能处于可用状态”以及“所有处于等待状态的 PersistentVolumeClaim 都能配对到与之绑定的 PersistentVolume”。
它内部也有两个相对独立的核心逻辑(ClaimWorker 和 VolumeWorker)来分别跟踪这两种期望状态。
PV 控制器实现了 PersistentVolume 和 PersistentVolumeClaim 的生命周期管理职能。在这个过程中,它会根据需要调用存储驱动插件的 Provision/Delete 操作。 - AD 控制器(Attach/Detach Controller)
AD 控制器的期望状态是“所有被调度到准备新创建 Pod 的节点,都附加好了要使用的存储;
当 Pod 被销毁后,原本运行 Pod 的节点都分离了不再被使用的存储”。
如果实际状态不符合该期望,会根据需要调用存储驱动插件的 Attach/Detach 操作。 - Volume 管理器(Volume Manager)
Volume 管理器实际上是 kubelet 众多管理器的其中一个,它主要作用是支持本节点中 Volume 执行 Attach/Detach/Mount/Unmount 操作。
不仅有 Mount/Unmount 操作,也出现了 Attach/Detach 操作。
Kubernetes 目前同时支持FlexVolume与CSI(Container Storage Interface)两套独立的存储扩展机制。
FlexVolume
FlexVolume 并不是全功能的驱动。
FlexVolume 部署维护都相对繁琐。
FlexVolume 实现复杂交互也相对繁琐。CSI
主要包括以下三个 gRPC 接口:CSI Identity 接口:
用于描述插件的基本信息,比如插件版本号、插件所支持的 CSI 规范版本、插件是否支持存储卷创建、删除功能、是否支持存储卷挂载功能等等。
Identity 接口还用于检查插件的健康状态,开发者可以通过 Probe 接口对外提供存储的健康度量信息。CSI Controller 接口:
用于从存储系统的角度对存储资源进行管理,比如准备和移除存储(Provision、Delete 操作)、附加与分离存储(Attach、Detach 操作)、对存储进行快照等等。
存储插件并不一定要实现这个接口的所有方法,对于存储本身就不支持的功能,可以在 CSI Identity 接口中声明为不提供。CSI Node 接口:
用于从集群节点的角度对存储资源进行操作,比如存储卷的分区和格式化、将存储卷挂载到指定目录上,或者将存储卷从指定目录上卸载,等等。
Kubernetes 存储生态系统
OSD = Object Storage Device
AWS = Amazon Web Services
存储系统和设备,可以划分三种存储类型:
- 块存储。
是数据存储最古老的形式。
它把数据都储存在一个或多个固定长度的块(Block)中,想要读写访问数据,就必须使用与存储相匹配的协议(SCSI、SATA、SAS、FCP、FCoE、iSCSI……)。
块存储由于贴近底层硬件,没有文件、目录、访问权限等的牵绊,所以性能通常都是最优秀的(吞吐量高,延迟低)。 - 文件存储。
是最贴近人类用户的数据存储形式。
真正被广泛运用的解决方案是把形成链表的指针整合起来统一存放,这就是文件分配表(File Allocation Table,FAT)。
文件系统(File System):定义文件分配表应该如何实现、储存哪些信息、提供什么功能的标准。
FAT32、NTFS、exFAT、ext2/3/4、XFS、BTRFS 等都是很常用的文件系统。 - 对象存储。
是相对较新的数据存储形式。
它是一种随着云数据中心的兴起而发展起来的存储,是以非结构化数据为目标的存储方案。
对象存储不仅易于共享、拥有庞大的容量,还能提供非常高的吞吐量。
亚马逊的存储服务:
- 块存储服务是 Amazon Elastic Block Store(AWS EBS)。
购买 EBS 之后,在 EC2(亚马逊的云计算主机)里看见的是一块原始的、未格式化的块设备。
这点就决定了 EBS 并不能做为一个独立存储而存在,它总是和 EC2 同时被创建的,EC2 的操作系统也只能安装在 EBS 之上。 - 文件存储服务是 Amazon Elastic File System(AWS EFS)。
购买 EFS 之后,只要在 EFS 控制台上创建好文件系统,并且管理好网络信息(如 IP 地址、子网)就可以直接使用,无需依附于任何 EC2 云主机。
EFS 的本质是完全托管在云端的网络文件系统(Network File System,NFS)。 - 对象存储服务是 Amazon Simple Storage Service(AWS S3)。
S3 通常是以 REST Endpoint 的形式对外部提供文件访问服务的。
应该直接使用程序代码来访问 S3,而不是靠操作系统或者容器编排系统去挂载它。
资源与调度
Kubernetes 资源模型
调度是容器编排系统最核心的功能之一。
“一切皆资源”的设计是 Kubernetes 能够顺利施行声明式 API 的必要前提。
从广义上来讲,Kubernetes 系统中所有接触到的,都被抽象成了资源,比如:
- 表示工作负荷的资源(Pod、ReplicaSet、Service、……),
- 表示存储的资源(Volume、PersistentVolume、Secret、……),
- 表示策略的资源(SecurityContext、ResourceQuota、LimitRange、……),
- 表示身份的资源(ServiceAccount、Role、ClusterRole、……),等等。
狭义上的物理资源,即特指排除了广义的那些逻辑上的抽象资源,只包括能够与真实物理底层硬件对应起来的资源。
Node 通常能够提供三方面的资源:
- 计算资源(如处理器、图形处理器、内存)
- 存储资源(如磁盘容量、不同类型的介质)
- 网络资源(如带宽、网络地址)
Pod 资源:
- 可压缩资源(Compressible Resources):如处理器。
特点是当可压缩资源不足时,Pod 只会处于“饥饿状态”,运行变慢,但不会被系统杀死,也就是容器会被直接终止,或者是被要求限时退出。
Kubernetes 给处理器资源设定的默认计量单位是“逻辑处理器的个数”。
默认单位是Core。其它单位如:Millcores。1 Core = 1000 Millcores - 不可压缩资源(Incompressible Resources):如内存。
特点是当不可压缩资源不足,或者超过了容器自己声明的最大限度时,Pod 就会因为内存溢出(Out-Of-Memory,OOM)而被系统直接杀掉。
Kubernetes 给内存设定的是广泛使用的计量单位。
默认单位是Bytes。其它单位如:Ei、Pi、Ti、Gi、Mi、Ki,E、P、T、G、M、K。
Mebibytes = 1024×1024 Bytes
Megabytes = 1000×1000 Bytes
Kubernetes 提供的 Pod 的服务质量等级,分为三级:(由高到低)
- Guaranteed:如果 Pod 中所有的容器都设置了limits和requests,且两者的值相等。
- Burstable:如果 Pod 中有部分容器的 requests 值小于limits值,或者只设置了requests而未设置limits。
- BestEffort:如果 Pod 中有部分容器的 requests 值小于limits值,limits和requests两个都没设置。
Pod 的驱逐机制是通过 kubelet 来执行的:
软驱逐是为了减少资源抖动对服务的影响。
硬驱逐是为了保障核心系统的稳定。
它们并不矛盾,一般会同时使用。
Kubernetes 调度器
默认调度器的要求:
- 运行:从集群的所有节点中,找出一批剩余资源可以满足该 Pod 运行的节点。
Kubernetes 调度器设计了一组名为 Predicate 的筛选算法。 - 恰当:从符合运行要求的节点中,找出一个最适合的节点完成调度。
Kubernetes 调度器设计了一组名为 Priority 的评价算法。
Predicate 本质上是一组节点过滤器(Filter),它会根据预设的过滤策略来筛选节点。
Kubernetes 中默认有三种过滤策略:
- 通用过滤策略:最基础的调度过滤策略,用来检查节点是否能满足 Pod 声明中需要的资源。
比如处理器、内存资源是否满足,主机端口与声明的 NodePort 是否存在冲突,Pod 的选择器或者nodeAffinity指定的节点是否与目标相匹配,等等。 - 卷过滤策略:与存储相关的过滤策略,用来检查节点挂载的 Volume 是否存在冲突(比如将一个块设备挂载到两个节点上),或者 Volume 的可用区域是否与目标节点冲突,等等。
- 节点过滤策略:与宿主机相关的过滤策略,最典型的是 Kubernetes 的污点与容忍度机制(Taints and Tolerations)。
比如默认情况下,Kubernetes 会设置 Master 节点不允许被调度,这就是通过在 Master 中施加污点来避免的。
共享状态(Shared State)的双循环调度机制。
调度缓存,就是两个控制循环的共享状态。
服务网格
UDS = Unix Domain Socket
服务网格将“程序”与“网络”解耦的思路。
分布式服务的通信
- 第一阶段:将通信的非功能性需求视作业务需求的一部分,由程序员来保障通信的可靠性。
- 第二阶段:将代码中的通信功能抽离重构成公共组件库,通信的可靠性由专业的平台程序员来保障。
- 第三阶段:将负责通信的公共组件库分离到进程之外,程序间通过网络代理来交互,通信的可靠性由专门的网络代理提供商来保障。
两种改进形态:- 第一种形态,微服务网关。
将网络代理从进程身边拉远,让它与进程分别处于不同的机器上,这样就可以同时给多个进程提供可靠通信的代理服务。 - 第二种形态,边车代理。
将网络代理往进程方向推近,不仅能让它与进程处于同一个共享网络名称空间的容器组之中,还可以让它透明并强制地接管通讯。
- 第一种形态,微服务网关。
- 第四阶段:将网络代理以边车的形式注入到应用容器,自动劫持应用的网络流量,让通信的可靠性由专门的通信基础设施来保障。
- 第五阶段:将边车代理统一管控起来实现安全、可控、可观测的通信,将数据平面与控制平面分离开来,实现通用、透明的通信,由专门的服务网格框架保障。
数据平面与控制平面
- 数据平面
数据平面 / 转发平面(Forwarding Plane):由一系列边车代理构成,核心职责是转发应用的入站(Inbound)和出站(Outbound)数据包。
边车(Sidecar)是一种常见的容器设计模式,用来形容外挂在容器身上的辅助程序。
边车代理是一个与应用共享网络名称空间的辅助容器。
代理注入
- 基座模式(Chassis):这种方式接入的边车代理对程序就是不透明的,它至少会包括一个轻量级的 SDK,让通信由 SDK 中的接口去处理。
好处是在程序代码的帮助下,有可能达到更好的性能,功能也相对更容易实现。
坏处是对代码有侵入性,对编程语言有依赖性。
典型产品是由华为开源后捐献给 Apache 基金会的ServiceComb Mesher。
基座模式的接入方式目前并不属于主流方式。 - 注入模式(Injector):根据注入方式不同,又可以分为:
- 手动注入模式:对使用者不透明,对程序是透明的。
- 自动注入模式:对使用者和程序都是透明的,也是 Istio 推荐的代理注入方式。
在 Kubernetes 中,服务网格一般是依靠“动态准入控制”(Dynamic Admission Control)中的Mutating Webhook控制器来实现自动注入的。
流量劫持
边车代理做流量劫持最典型的方式是基于 iptables 进行的数据转发。
可靠通信
Envoy 将代理的转发的行为规则抽象成 Listener、Router、Cluster 三种资源,又定义了应该如何发现和访问这些资源的一系列 API,统称为“xDS 协议族”。
- Listener
Listener 可以简单理解为 Envoy 的一个监听端口,用于接收来自下游应用程序(Downstream)的数据。 - Cluster
Cluster 是 Envoy 能够连接到的一组逻辑上提供相同服务的上游(Upstream)主机。
Cluster 包含该服务的连接池、超时时间、Endpoints 地址、端口、类型等信息。 - Router
Listener 负责接收来自下游的数据,Cluster 负责将数据转发送给上游的服务,而 Router 则决定 Listener 在接收到下游的数据之后,具体应该将数据交给哪一个 Cluster 处理。
Router 实际上是承担了服务网关的职责。
xDS v3.0协议族包含的具体协议:
简称 | 全称 | 服务描述 |
---|---|---|
LDS | Listener Discovery Service | 监听器发现服务 |
RDS | Route Discovery Service | 路由发现服务 |
CDS | Cluster Discovery Service | 集群发现服务 |
EDS | Endpoint Discovery Service | 集群成员发现服务 |
ADS | Aggregated Discovery Service | 聚合发现服务 |
HDS | Health Discovery Service | 健康度发现服务 |
SDS | Secret Discovery Service | 密钥发现服务 |
MS | Metric Service | 度量指标服务 |
RLS | Rate Limit Service | 速率限制服务 |
ALS | gRPC Access Log Service | gRPC访问日志服务 |
LRS | Load Reporting service | 负载报告服务 |
RTDS | Runtime Discovery Service | 运行时发现服务 |
CSDS | Client Status Discovery Service | 客户端状态发现服务 |
ECDS | Extension Conflg Discovery Service | 扩展配置发现服务 |
- 控制平面
Istio 在 1.5 版本之前,Istio 自身也是采用微服务架构开发的,它把控制平面的职责分解为四个模块去实现:
- Mixer 负责鉴权策略与遥测;
- Pilot 负责对接 Envoy 的数据平面,遵循 xDS 协议进行策略分发;
- Galley 负责配置管理,为服务网格提供外部配置感知能力;
- Citadel 负责安全加密,提供服务和用户层面的认证和鉴权、管理凭据和 RBAC 等安全相关能力。
单体化之后出现的新进程 Istiod 就承担所有的控制平面职责,具体包括以下几种:
- 数据平面交互:是满足服务网格正常工作所需的必要工作。
边车注入,策略分发,配置分发。 - 流量控制:是用户使用服务网格的最主要目的。
请求路由,流量治理,调试能力。 - 通信安全:包括通信中的加密、凭证、认证、授权等功能。
生成 CA 证书,SDS服务代理,认证,授权。 - 可观测性:包括日志、追踪、度量三大块能力。
日志收集,链路追踪,指标度量。
标准规范
服务网格是数据平面产品与控制平面产品的集合。
- SMI = Service Mesh Interface 服务网格接口
SMI 规范提供了外部环境(实际上就是 Kubernetes)与控制平面交互的标准,使得 Kubernetes 及在其之上的应用,能够无缝地切换各种服务网格产品。
包括四方面的 API 构成:
- 流量规范(Traffic Specs)
目标是定义流量的表示方式,比如 TCP 流量、HTTP/1 流量、HTTP/2 流量、gRPC 流量、WebSocket 流量等应该如何在配置中抽象和使用。 - 流量拆分(Traffic Split)
目标是定义不同版本服务之间的流量比例,提供流量治理的能力,比如限流、降级、容错,等等,以满足灰度发布、A/B 测试等场景。 - 流量度量(Traffic Metrics)
目标是为资源提供通用集成点,度量工具可以通过访问这些集成点来抓取指标。这部分完全遵循了 Kubernetes 的Metrics API进行扩充。 - 流量访问控制(Traffic Access Control)
目标是根据客户端的身份配置,对特定的流量访问特定的服务提供简单的访问控制。
- UDPA = Universal Data Plane API 通用数据平面API
UDPA 规范则提供了控制平面与数据平面交互的标准,使得服务网格产品能够灵活地搭配不同的边车代理,针对不同场景的需求,发挥各款边车代理的功能或者性能优势。
UDPA-WG = Universal Data Plane API Working Group 通用数据平面API工作组:每年推出一个大版本、每个版本从发布到淘汰起要经历 Alpha、Stable、Deprecated、Removed 四个阶段、每个阶段持续一年时间。
服务网格生态
数据平面的主流产品:
- Linkerd
2016 年 1 月发布的Linkerd是服务网格的鼻祖,使用 Scala 语言开发的 Linkerd-proxy 也就成为了业界第一款正式的边车代理。
一年后的 2017 年 1 月,Linkerd 成功进入 CNCF,成为云原生基金会的孵化项目,但此时的 Linkerd 其实已经显露出了明显的颓势。
由于 Linkerd-proxy 运行需要 Java 虚拟机的支持,启动时间、预热、内存消耗等方面,相比起晚它半年发布的挑战者 Envoy,均处于全面劣势,
因而 Linkerd 很快就被 Istio 和 Envoy 的组合所击败,结束了它短暂的统治期。 - Envoy
2016 年 9 月开源的Envoy是目前边车代理产品中,市场占有率最高的一款,已经在很多个企业的生产环境里经受过大量检验。
Envoy 最初由 Lyft 公司开发,后来 Lyft 与 Google 和 IBM 三方达成合作协议,Envoy 就成了 Istio 的默认数据平面。
Envoy 使用 C++ 语言实现,比起 Linkerd 在资源消耗方面有了明显的改善。
此外,由于采用了公开的 xDS 协议进行控制,Envoy 并不只为 Istio 所私有,这个特性也让 Envoy 被很多其他的管理平面选用,为它夺得市场占有率桂冠做出了重要贡献。
2017 年 9 月,Envoy 加入 CNCF,成为 CNCF 继 Linkerd 之后的第二个数据平面项目。 - nginMesh
2017 年 9 月,在 NGINX Conf 2017 大会上,Nginx 官方公布了基于著名服务器产品 Nginx 实现的边车代理nginMesh。
nginMesh 使用 C 语言开发(有部分模块用了 Golang 和 Rust),是 Nginx 从网络通信踏入程序通信的一次重要尝试。
Nginx 在网络通信和流量转发方面拥有其他厂商难以匹敌的成熟经验,因此本该成为数据平面的有力竞争者才对。
然而结果却是 Nginix 在这方面投入资源有限,方向摇摆,让 nginMesh 的发展一直都不温不火,
到了 2020 年,nginMesh 终于宣告失败,项目转入“非活跃”(No Longer Under Active)状态。 - Conduit/Linkerd 2
2017 年 12 月,在 KubeCon 大会上,Buoyant 公司发布了 Conduit 的 0.1 版本,
这是 Linkerd-proxy 被 Envoy 击败后,Buoyant 公司使用 Rust 语言重新开发的第二代的服务网格产品,
最初是以 Conduit 命名,在 Conduit 加入 CNCF 后不久,Buoyant 公司宣布它与原有的 Linkerd 项目合并,被重新命名为Linkerd 2(这样就只算一个项目了)。
使用 Rust 重写后,Linkerd2-proxy的性能与资源消耗方面,都已经不输 Envoy 了,
但它的定位通常是作为 Linkerd 2 的专有数据平面,所以成功与否,在很大程度上还是要取决于 Linkerd 2 的发展如何。 - MOSN
2018 年 6 月,来自蚂蚁金服的MOSN宣布开源,
MOSN 是 SOFAStack 中的一部分,使用 Golang 语言实现,在阿里巴巴及蚂蚁金服中经受住了大规模的应用考验。
由于 MOSN 是技术阿里生态的一部分,对于使用了 Dubbo 框架,或者 SOFABolt 这样的 RPC 协议的微服务应用,MOSN 往往能够提供些额外的便捷性。
2019 年 12 月,MOSN 也加入了CNCF Landscape。 - 其他的数据平面产品:HAProxy Connect、Traefik、ServiceComb Mesher,等等。
控制平面产品:
- Linkerd 2
这是 Buoyant 公司的服务网格产品,可以发现无论是数据平面还是控制平面,他们都采用了“Linkerd”和“Linkerd 2”的名字。
现在 Linkerd 2 的身份,已经从领跑者变成了 Istio 的挑战者。
不过虽然代理的性能已经赶上了 Envoy,但功能上 Linkerd 2 还是不能跟 Istio 相媲美,
在 mTLS、多集群支持、支持流量拆分条件的丰富程度等方面,Istio 都比 Linkerd 2 要更有优势,
毕竟两者背后的研发资源并不对等,一方是创业公司 Buoyant,而另一方是 Google、IBM 等巨头。
然而,相比起 Linkerd 2,Istio 的缺点很大程度上也是由于其功能丰富带来的,
每个用户真的都需要支持非 Kubernetes 环境、支持多集群单控制平面、支持切换不同的数据平面等这类特性吗?
其实我认为,在满足需要的前提下,更小的功能集合往往意味着更高的性能与易用性。 - Istio
这是 Google、IBM 和 Lyft 公司联手打造的产品,它是以自己的 Envoy 为默认数据平面。
Istio 是目前功能最强大的服务网格,
如果你苦恼于这方面产品的选型,直接挑选 Istio 的话,不一定是最合适的,但起码能保证应该是不会有明显缺陷的选择;
同时,Istio 也是市场占有率第一的控制平面,不少公司发布的服务网格产品都是在它的基础上派生增强而来的,
比如蚂蚁金服的 SOFAMesh、Google Cloud Service Mesh 等。
不过,服务网格毕竟比容器运行时、容器编排要年轻,
Istio 在服务网格领域尽管占有不小的优势,但统治力还远远不能与容器运行时领域的 Docker 和容器编排领域的 Kubernetes 相媲美。 - Consul Connect
Consul Connect 是来自 HashiCorp 公司的服务网格,
Consul Connect 的目标是把现有由 Consul 管理的集群,平滑升级为服务网格的解决方案。
就像 Connect 这个名字所预示的“链接”含义一样,Consul Connect 十分强调它整合集成的角色定位,
它不跟具体的网络和运行平台绑定,可以切换多种数据平面(默认为 Envoy),支持多种运行平台,
比如 Kubernetest、Nomad 或者标准的虚拟机环境。 - OSM
Open Service Mesh(OSM)是微软公司在 2020 年 8 月开源的服务网格,它同样是以 Envoy 为数据平面。
OSM 项目的其中一个主要目标,是作为 SMI 规范的参考实现。
同时,为了跟强大却复杂的 Istio 进行差异化竞争,OSM 明确以“轻量简单”为卖点,通过减少边缘功能和对外暴露的 API 数量,降低服务网格的学习使用成本。
探索与实践
让后端服务保持无状态,而把状态维持在前端中的设计,对服务的伸缩性和系统的鲁棒性都有着很大的益处,多数情况下都是值得倡导的良好设计。
基于Spring Boot的单体架构
技术组件:
- JSR 370:Java API for RESTful Web Services 2.1(JAX-RS 2.1)
在 RESTFul 服务方面,采用的实现为 Jersey 2,可以替换为 Apache CXF、RESTeasy、WebSphere、WebLogic 等。 - JSR 330:Dependency Injection for Java 1.0
在依赖注入方面,采用的实现为 Spring Boot 2.0 中内置的 Spring Framework 5。 - JSR 338:Java Persistence 2.2
在持久化方面,采用的实现为 Spring Data JPA。可以替换为 Batoo JPA、EclipseLink、OpenJPA 等实现。 - JSR 380:Bean Validation 2.0
在数据验证方面,采用的实现为 Hibernate Validator 6,可以替换为 Apache BVal 等其他验证框架。 - JSR 315:Java Servlet 3.0
在 Web 访问方面,采用的实现为 Spring Boot 2.0 中默认的 Tomcat 9 Embed,可以替换为 Jetty、Undertow 等其他 Web 服务器。 - JSR 375:Java EE Security API specification 1.0
在认证 / 授权方面,采用 Spring Security 5 作为认证服务,Spring Security OAuth 2.3 作为授权服务,Spring Security JWT 作为 JWT 令牌支持。 - JSR 353/367:Java API for JSON Processing/Binding
在 JSON 序列化 / 反序列化方面,采用 Spring Security OAuth 默认的 Jackson。
工程结构 Resource, Application, Domain, Infrastructure
基于Spring Cloud的微服务架构
采用基于 Spring Cloud 微服务架构,微服务部分主要采用 Netflix OSS 组件进行支持。
技术组件:
- 配置中心:默认采用 Spring Cloud Config,也可使用 Spring Cloud Consul、Spring Cloud Alibaba Nacos 代替。
- 服务发现:默认采用 Netflix Eureka,也可使用 Spring Cloud Consul、Spring Cloud ZooKeeper、etcd 等代替。
- 服务网关:默认采用 Netflix Zuul,也可使用 Spring Cloud Gateway 代替。
- 服务治理:默认采用 Netflix Hystrix,也可使用 Sentinel、Resilience4j 代替。
- 进程内负载均衡:默认采用 Netfilix Ribbon,也可使用 Spring Cloud Loadbalancer 代替。
- 声明式 HTTP 客户端:默认采用 Spring Cloud OpenFeign。
可以考虑Retrofit,或者使用 RestTemplete 乃至于更底层的OkHTTP、HTTPClient以命令式编程来访问,多写一些代码而已。
![软件架构-探索与实践-基于SpringCloud的微服务架构](https://cdn.jsdelivr.net/gh/sstian/images/blogimg/软件架构-探索与实践-基于Spring Cloud的微服务架构.jpeg)
基于Kubernetes的微服务架构
“后微服务时代”中的下一次架构演进,这次升级的目标主要有两点:
- 目标一:尽可能缩减非业务功能代码的比例。
- 目标二:尽可能在不影响原有代码的前提下完成迁移。
技术组件:
- 配置中心
采用 Kubernetes 的 ConfigMap 来管理,通过 Spring Cloud Kubernetes Config 自动将 ConfigMap 的内容注入到 Spring 配置文件中,并实现动态更新。 - 服务发现
采用 Kubernetes 的 Service 来管理,通过 Spring Cloud Kubernetes Discovery 自动将 HTTP 访问中的服务转换为FQDN。 - 负载均衡
采用 Kubernetes Service 本身的负载均衡能力实现(DNS 负载均衡),可以不再需要 Ribbon 这样的客户端负载均衡。
Spring Cloud Kubernetes 从 1.1.2 开始,也已经移除了对 Ribbon 的适配支持,也(暂时)没有对其代替品 Spring Cloud LoadBalancer 提供适配。
基于Istio的服务网格架构
技术组件:
- 配置中心:通过 Kubernetes 的 ConfigMap 来管理。
- 服务发现:通过 Kubernetes 的 Service 来管理。
- 负载均衡:未注入边车代理时,依赖 KubeDNS 实现基础的负载均衡,一旦有了 Envoy 的支持,就可以配置丰富的代理规则和策略。
- 服务网关:依靠 Istio Ingress Gateway 来实现。
- 服务容错:依靠 Envoy 来实现。
- 认证授权:依靠 Istio 的安全机制来实现。
Spring Security OAuth 2.0 仍然以第三方 JWT 授权中心的角色存在,为系统提供终端用户认证,为服务网格提供令牌生成、公钥JWKS等支持。
基于云计算的无服务架构
无服务架构(Serverless)跟微服务架构本身没有继承替代的关系,它们并不是同一种层次的架构。
无服务的云函数可以作为微服务的一种实现方式,甚至可能是未来很主流的实现方式。