第一章:云原生技术概论
云计算的演进变革
物理机时代
虚拟化技术成熟
从虚拟化技术的发展中,我们看到业务的工作负载由物理机转向虚拟机,资源有了初级的隔离,并且分配/利用更加合理。
云计算技术成熟
对于 XaaS 的一路演进,可以简单归纳为:
- 有了 IaaS(Infrastructure as a Service,基础设施即服务),客户不用关注物理机器,只需关注基础架构及应用程序。
- 有了 PaaS(Platform as a Service,平台即服务),客户不用关注基础架构,只需关注应用程序。
- 有了 FaaS(Function as a Service,功能即服务),客户只需关注功能和数据。
容器技术的兴起
Docker 创新性地提出了“镜像”(image)的概念,实现了一种新型的应用打包、分发和运行机制,开发人员能够在几秒钟内完成应用程序的部署、运行,无需再担心环境不一致的问题。
云原生出现的背景
部分软件已经变成水电煤一样的社会经济中的基础设施。
过去二十年间,云的底层基础设施和平台越来越强大,软件架构的发展也逐渐和云匹配:
- 通过不可变基础设施(镜像)解决本地和远程一致性问题;
- 通过服务网格(ServiceMesh)将非业务逻辑从应用程序中剥离;
- 通过声明式 API 描述应用程序的状态,而不用管中间的处理过程;
- 通过 DevOps 方法论以及一系列工具来提升研发/运维效率…
应用程序中的非业务逻辑不断被剥离,并下沉到云/基础设施层,代码越来越轻量。由此,工程师的开发工作回归本质(软件开发的本质是解决业务需求,各类“高深”、“复杂”的技术难题是业务需求的副产物,并不是软件开发的主题)。
云原生的定义
CNCF 云原生的定义 v1.0 版本
云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式 API。
这些技术能够构建容错性好、易于管理和便于观察的松耦合系统。结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统作出频繁和可预测的重大变更。
云原生的目标
云原生的几个关键目标:
- 可用(Available):通过各种机制来实现应用的高可用,以保证服务提供的连续性。
- 规模(Scale):要求云原生服务能够适应不同的规模(包括但不限于用户规模/部署规模/请求量),并能够在部署时动态分配资源,以便在不同的规模之间快速和平滑的伸缩。
- 敏捷(Agility):快速响应市场需求。
- 成本(Cost):充分有效的利用资源。
云原生代表技术
云原生的代表技术:容器技术、微服务、服务网格、不可变基础设施、声明式设计以及 DevOps。
容器技术
Google Cloud 对容器的定义: 容器是轻量级应用代码包,它还包含依赖项,例如编程语言运行时的特定版本和运行软件服务所需的库。
容器技术发展历程:
chroot 阶段:隔离文件系统
chroot 被认为是最早的容器技术之一,它能将进程的根目录重定向到某个新目录,复现某些特定环境,同时也将进程的文件读写权限限制在该目录内。
LXC 阶段:封装系统
至 2013 年,Linux 虚拟化技术已基本成型,通过 cgroups、namespace 以及安全防护机制,大体上解决了容器核心技术“运行环境隔离”,但此时仍需等待另一项关键技术的出现,才能迎来容器技术的全面繁荣。
Docker 阶段:封装应用
Docker 的核心创新“容器镜像(container image)”:
- 容器镜像打包了整个容器运行依赖的环境,以避免依赖运行容器的服务器的操作系统,从而实现“build once,run anywhere”。
- 容器镜像一但构建完成,就变成只读状态,成为不可变基础设施的一份子。
- 与操作系统发行版无关,核心解决的是容器进程对操作系统包含的库、工具、配置的依赖(注意,容器镜像无法解决容器进程对内核特性的特殊依赖)。
OCI 阶段:容器标准化
OCI 组织着力解决容器的构建、分发和运行标准问题,其宗旨是制定并维护 OCI Specifications(容器镜像格式和容器运行时的标准规范)。
OCI 有了三个主要的标准:
- OCI Runtime Spec(容器运行时标准):定义了运行一个容器,如何管理容器的状态和生命周期,如何使用操作系统的底层特性(namespace、cgroups、pivot_root 等)。
- OCI Image Spec(容器镜像标准):定义了镜像的格式,配置(包括应用程序的参数、依赖的元数据格式、环境信息等),简单来说就是对镜像文件格式的描述。
- OCI Distribution Spec(镜像分发标准):定义了镜像上传和下载的网络交互过程的规范。
容器编排阶段:封装集群
以 Kubernetes 为代表的容器编排框架,就是把大型软件系统运行所依赖的集群环境也进行了虚拟化,让集群得以实现跨数据中心的绿色部署,并能够根据实际情况自动扩缩。
就像用 docker run 可以启动单个程序一样,现在用 kubectl apply -f 就能部署和运行一个分布式集群应用,而无需关心是在私有云还是公有云或者具体哪家云厂商上。
云原生阶段:百花齐放
微服务
Netflix 云架构师 Adrian Cockcroft定义:微服务架构是一种面向服务的架构,由松耦合的具有有限上下文的元素组成。
Adrian Cockcroft 的观点中有两个核心概念:
- 松耦合(Loosely Coupled):意味着每个服务可以独立的更新,更新一个服务无需要求改变其他服务。
- 限界上下文(Bounded Contexts):意味着每个服务要有明确的边界性,你可以只关注自身软件的发布,而无需考虑谁在依赖你的发布版本。微服务和它的消费者严格通过 API 进行交互,不共享数据结构、数据库等。基于契约的微服务规范要求服务接口是稳定的,而且向下兼容。
微服务架构中,有一些必须解决的问题,如负载均衡、伸缩扩容、传输通讯等等,这些问题可以说只要是分布式架构的系统就无法完全避免。
Kubernetes 在基础设施层面,解决分布式系统问题的方案:
- Kubernetes 用 CoreDNS 替代 Spring Cloud 服务发现组件 Eureka。
- Kubernetes 用 Service/Load Balancer 替代 Spring Cloud 中的负载均衡组件 Ribbon。
- Kubernetes 用 ConfigMap 替代 Spring Cloud 的配置中心 Config。
- Kubernetes 用 Ingress 代替 Spring Cloud 的网关组件 Zuul。
当虚拟化的基础设施从单个服务的容器扩展至由多个容器构成的服务集群,并开始解决分布式的问题。
服务网格
服务网格(Service Mesh)的概念最早由 Buoyant 公司的创始人 William Morgan 于 2016 年提出。
服务网格的定义
服务网格(ServiceMesh)是一个基础设施层,用于处理服务间通信。云原生应用有着复杂的服务拓扑,服务网格保证请求在这些拓扑中可靠地穿梭。在实际应用当中,服务网格通常是由一系列轻量级的网络代理组成的,它们与应用程序部署在一起,但对应用程序透明。
—— by William Morgan
业内绝大部分服务网格产品通常由“数据平面”和“控制平面”两部分组成:
- 数据平面(Data plane):通常采用轻量级的网络代理(如 Envoy)作为 Sidecar,网络代理负责协调和控制服务之间的通信和流量处理,解决微服务之间服务熔断、负载均衡、安全通讯等问题。
- 控制平面(Control plane):包含多个控制组件,它们负责配置和管理 Sidecar ,并提供服务发现(Discovery)、配置管理(Configuration)、安全控制(Certificates)等功能。
不可变基础设施
可变的基础设施会导致以下问题:
- 重大故障时,难以快速重新构建服务:持续过多的手动操作并且缺乏记录,会导致很难由标准初始化的服务器来重新构建起等效的服务;
- 不一致风险:类似于程序变量因并发修改而带来的状态不一致风险。服务运行过程中,频繁的修改基础设施配置,同样会引入中间状态,导致出现无法预知的问题。
不可变基础设施思想的核心是,任何基础设施的运行实例一旦创建之后就变成只读状态。如需修改或升级,应该先修改基础设施的配置模版(例如 yaml、Dockerfile 配置),之后再使用新的运行实例替换。
声明式设计
声明式设计是指一种软件设计理念:“我们描述一个事物的目标状态,而非达成目标状态的流程”。至于目标状态如何达成,则由相应的工具在其内部实现。
和声明式设计相对的是命令式设计(又叫过程式设计),两者的区别是:
- 命令式设计:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现;
- 声明式设计:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
DevOps
DevOps 是个很复杂的概念,几句话很难解释清楚。我们延用之前的惯例,如果要理解一个复杂的概念,就先去了解它出现的背景,以及发展的历史。
DevOps 核心本质是解决软件开发生命周期中的管理问题,我们先从一种名为“瀑布模型”的项目管理方法说起。
虽然敏捷开发提升了开发效率,但它的范围仅限于开发和测试环节,并没有覆盖到部署环节。显然,运维部门并没有收益。相反的,甚至可以说“敏捷”加重了运维的负担。运维追求的目标是稳定,频繁变更是破坏稳定的根源。
DevOps 运动始于 2007 年左右,当时技术社区对开发与运维之间分开工作的方式以及由此引发的冲突感到担忧。
DevOps 的定义
DevOps(Development 和 Operations 的合成词)是一种重视“软件开发人员(Dev)”和“IT 运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。
通过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。
—— from 维基百科
DevOps 的成功实践离不开工具上的支持,这其中包括最重要的自动化 CI/CD 流水线,通过自动化的方式打通软件从构建、测试到部署发布的整个流程。还有实时监控、事件管理、配置管理、协作平台等一系列工具/系统的配合。
第二章: 构建“足够快”的网络服务
HTTPS 请求优化分析
一个完整、未复用连接的 HTTPS 请求需要经过以下 5 个阶段:DNS 域名解析、TCP 握手、SSL 握手、服务器处理、内容传输。
RTT(Round-Trip Time)一个网络数据包从起点到目的地然后再回到起点所花费的时长。
TTFB(Time To First Byte,首字节时间)指从浏览器请求页面到接收来自服务器发送的信息的第一个字节的时间。
网络拥塞控制原理与实践
网络吞吐量与 RTT、带宽密切相关:
- RTT 越低,数据传输的延迟越低;
- 带宽越高,网络在单位时间内传输的数据越多。
一些术语:
- RTprop (Round-Trip propagation time,两个节点之间最小时延):该值取决于物理距离,距离越长,时延越大。
- BtlBw(Bottleneck Bandwidth,瓶颈带宽):如果把网络链路想象成水管,RTprop 是水管的长度,BtlBw 则是水管最窄处的直径。
- BDP(Bandwidth-Delay Product,带宽、时延的乘积):它代表了网络上能够同时容纳的数据量(水管中有多少流动的水)。 BDP 的计算公式是:BDP = 带宽 × 时延。其中,带宽以比特每秒(bps)为单位,时延以秒为单位。
- inflight 数据:指已经发送出去但尚未收到确认的数据包。这些数据包仍在网络中传输,等待接收方的处理或确认。
QUIC 设计原理与实践
QUIC(Quick UDP Internet Connection,快速 UDP 网络连接)是一种基于 UDP 封装的安全可靠传输协议,旨在取代 TCP,成为新一代互联网的主流传输协议。
第三章:深入 Linux 内核网络技术
Linux 内核网络框架
从 Linux 内核 2.4 版本开始,内核引入了一套通用的过滤框架 —— Netfilter,允许外界对网络数据包在内核协议栈流转过程中进行代码干预。
Linux 系统中的各类网络功能,如地址转换、封包处理、地址伪装、协议连接跟踪、数据包过滤、透明代理、带宽限速和访问控制等,都是基于 Netfilter 提供的代码拦截机制实现的。可以说,Netfilter 是整个 Linux 网络系统最重要(没有之一)的基石。
内核旁路技术
对于网络密集型应用,内核态与用户态的频繁切换、复杂的网络协议栈处理,常常使 Linux 内核成为性能瓶颈。
在人们想办法提升 Linux 内核性能的同时,另外一批人抱着它不行就绕开它想法,提出了一种“内核旁路“(Kernel bypass)思想的技术方案。其中,DPDK 和 XDP 是主机内“内核旁路”思想的代表技术,RDMA 是主机之间“内核旁路”思想的代表技术。
Linux 网络虚拟化
虚拟网络通信技术
基于物理设备实现的网络拓扑结构是相对固定的,很难跟得上云原生时代下系统频繁变动的频率。例如,容器的动态扩缩容、集群跨数据中心迁移等等,都要求网络拓扑随时做出调整。正因为如此,软件定义网络(Software Defined Networking,SDN)的需求变得前所未有的迫切。
SDN 思想的核心是,在现有的物理网络之上新增一层虚拟网络,将控制平面(操作系统和各类网络控制软件等)和数据平面(底层通信的物理设备,以及各类通信协议等)解耦,将网络服务从底层硬件设备中抽象出来,由代码直接编程控制。
SDN 网络模型如图 3-16 所示:
- 位于下层的网络称 Underlay 网络,它是由路由器、交换机等硬件设备互联而成的物理网络,负责网络之间的数据传输;
- 位于上层的网络称 Overlay 网络,它是采用多种网络虚拟化技术在 Underlay 网络之上创建的虚拟网络。
第四章:负载均衡与代理技术
负载均衡与代理
从处理请求的网络层次角度看,所有的负载均衡器可归纳为两大类:四层负载均衡和七层负载均衡,分别对应 OSI 模型的第四层(传输层)和第七层(应用层)。
“四层负载均衡”并非严格限定于 OSI 模型的第四层(传输层)。实际上,它的工作模式涉及多个网络层次:
- 第二层(数据链路层):通过修改帧头中的 MAC 地址,将请求从一个物理网络节点转发到另一个节点。这种方式通常用于同一广播域内的转发,例如交换机或桥接设备完成的二层转发操作。
- 第三层(网络层):通过修改 IP 地址,实现跨子网的请求路由和转发。这是路由器的核心功能,通过修改数据包的源或目的 IP 地址,实现子网之间的通信和流量转发。
- 第四层(传输层):通过修改 TCP/UDP 端口号或连接的目标地址,利用网络地址转换(NAT)技术隐藏内部网络结构,将请求从一个入口转发至多个后端服务。
七层负载均衡器工作在应用层,这意味着负载均衡器必须与后端服务器建立新的传输层连接,并将客户端的请求代理到后端服务器。
七层负载均衡器工作在应用层,因此能够检测和处理请求内容,包括:
- 安全层 TLS 协议:TLS 的归属层次在网络领域存在争议,本文为便于讨论假设属于应用层。
- 物理 HTTP 协议:涵盖 HTTP/1、HTTP/2、HTTP/3 等版本。
- 逻辑 HTTP 协议:包括请求的头部、主体和尾部数据。
- 消息协议:如 gRPC、RESTful API、SOAP、AMQP、MQTT 等。
负载均衡部署拓扑
四层负载均衡技术
四层负载均衡器的典型代表是 LVS(Linux Virtual Server,Linux 虚拟服务器),由中国程序员章文嵩于 1998 年发起和开发。
第五章:数据一致性与分布式事务
数据一致性
引入事务的目的,是为了保证数据的“一致性”(Consistency)。
这里的一致性指的是,对数据有特定的预期状态,任何数据更改操作必须满足这些状态约束(或者恒等条件)。
想要达成数据的一致性,需要 3 个方面的努力:
- 原子性(Atomic):“原子”通常指不可分解为更小粒度的东西。这里原子性描述的是,客户端发起一个请求(请求包含多个操作)在异常情况下的行为。例如,只完成了一部分写入操作,系统出现故障了(进程崩溃、网络中断、节点宕机)。把多个操作纳入到一个原子事务,万一出现上述故障导致无法完成最终提交时,则中止事务,丢弃或者撤销那些局部修改。
- 隔离性(Isolation): 同时运行的事务不应互相干扰。例如,当一个事务执行多次写入操作时,其他事务应仅能观察到该事务的最终完成结果,而非中间状态。隔离性旨在防止多个事务交叉操作导致的数据不一致问题。
- 持久性(Durability):事务处理完成后,对数据的修改应当是永久性的,即使系统发生故障也不会丢失。在单节点数据库中,持久性意味着数据已写入存储设备(如硬盘或 SSD)。而在分布式数据库中,持久性要求数据成功复制到多个节点。为确保持久性,数据库必须在完成数据复制后,才能确认事务已成功提交。
这也就是常说的事务的“ACID 特性”。值得一提的是,对于一致性而言,更多的是指数据在应用层的外部表现。应用程序借助数据库提供的原子性、隔离性和持久性,来实现一致性目标。也就是说,A、I、D 是手段,C(Consistency)是 3 者协作的目标,弄到一块完全是为了读起来更顺口。
一致性与可用性的权衡
CAP 是一致性与可用性权衡的理论,是理解分布式系统的起点。
1999 年,美国工程院院士 Eric A.Brewer 发表了论文《Harvest, Yield, and Scalable Tolerant Systems》[1] ,首次提出了“CAP 原理”(CAP Principle)。不过,彼时的 CAP 仅是一种猜想,尚未得到理论上的证明。2002 年,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 用严谨的数学推理证明了 CAP 的正确性。此后,CAP 从原理转变成定理,在分布式系统领域产生了深远的影响。
CAP 定理描述的是一个分布式系统中,涉及共享数据问题时,以下三个特性最多只能满足两个。
- 一致性(Consistency):意味着数据在任何时刻、任何节点上看到的都是符合预期的。为了确保定义的严谨性,学术研究中通常将一致性定义为“强一致性”(Strong Consistency),也称为“线性一致性”(Linearizability)。
- 可用性(Availability):可用性代表系统持续提供服务的能力。要理解可用性,首先需要了解两个密切相关的指标:可靠性(Reliability)和可维护性(Serviceability)。可靠性通过平均无故障时间(MTBF, Mean Time Between Failure)度量;可维护性通过平均修复时间(MTTR, Mean Time To Repair)度量。可用性衡量系统在总时间内可以正常使用的时间比例,计算公式为 A = MTBF / (MTBF + MTTR)。因此,可用性是通过可靠性和可维护性计算得出的比例。例如,99.9999% 的可用性意味着平均每年系统故障修复时间为 32 秒。
- 分区容错性(Partition tolerance):当部分节点由于网络故障或通信中断而无法相互联系,形成“网络分区”时,系统仍能够继续正确地提供服务。
由于 CAP 定理已有严格的证明,我们不再探讨为何 CAP 不可兼得,直接分析舍弃 C、A、P 时所带来的不同影响。
- 放弃分区容忍性(CA without P):意味着我们将假设节点之间通信永远可靠。永远可靠的通信在分布式系统中必定不成立,只要依赖网络共享数据,分区现象就不可避免地存在。如果没有 P(分区容错性),也就谈不上是真正的分布式系统。
- 放弃可用性(CP without A):意味着我们将假设一旦网络发生分区,节点之间的信息同步时间可以无限制延长。在现实中,选择放弃可用性系统(又称为 CP 系统)适用于对数据一致性有严格要求的场景,如金融系统、库存管理系统等。这些应用场景中,数据的一致性和准确性通常比系统的可用性更为重要。
- 放弃一致性(AP without C):意味着在网络分区发生时,节点之间的数据可能会出现不一致。这种情况下,系统会优先保证可用性,而不是一致性。选择放弃一致性系统(又称 AP 系统)已经成为设计分布式系统的主流选择,因为分区容错性(P)是分布式网络的固有属性,不可避免;而可用性(A)通常是建设分布式系统的目标。如果系统在节点数量增加时可用性降低,则其分布式设计的价值也会受到质疑。除了像银行和证券这样的金融交易服务,这些场景中数据一致性至关重要,通常需要保证一致性而可能接受部分中断之外,大多数系统更倾向于在节点增多时保持高可用性,而不是牺牲可用性以维持一致性。
对于分布式系统而言,必须实现分区容错性(P)。因此,CAP 定理实际上要求在可用性(A)和一致性(C)之间选择,即在 AP 和 CP 之间权衡取舍。
工程师们又重新给一致性下了定义,将 CAP、ACID 中讨论的一致性(C)称为“强一致性”,而把牺牲了 C 的 AP 系统但又要尽可能获得正确结果的行为称为追求“弱一致性”。不过,若只是单纯地谈论“弱一致性”,通常意味着不保证一致性。在弱一致性中,工程师们进一步总结出了一种较强的特例,称为“最终一致性”(Eventual Consistency),它由 eBay 的系统架构师 Dan Pritchett 在 BASE 理论中提出。
分布式事务模型
可靠事件队列
2008 年,eBay 架构师 Dan Pritchett 在 ACM 发表了论文《Base: An Acid Alternative》[1]。这篇论文中,作者基于实践总结出一种独立于 ACID 之外,通过消息队列和幂等机制来达成数据一致性的技术手段,并提出了“最终一致性”的概念。
BASE 是“Basically Available”、“Soft State”和“Eventually Consistent”的缩写,其核心理念为:
- 基本可用(Basically Available):系统保证在大多数情况下能够提供服务,即使某些节点出现故障时,仍尽可能保持可用性。这意味着系统优先保障可用性,而非一致性。
- 柔性状态(Soft state):系统状态允许在一段时间内处于不一致状态。与 ACID 强一致性的要求不同,BASE 允许系统在更新过程处于“柔性”状态,即数据在某些节点上可以暂时不一致。
- 最终一致性(Eventually consistent):最终一致性强调,即使在网络分区或系统故障的情况下,在经过足够的时间和多次数据同步操作后,所有节点的数据一定会一致。
服务幂等性设计
幂等性是一个数学概念,后来被引入计算机科学中,用来描述某个操作可以安全地重试,即多次执行的结果与单次执行的结果完全一致。
第六章:分布式共识及算法
世界上只有一种共识算法,就是 Paxos,其他所有的共识算法都是 Paxos 的退化版本。
什么是共识
尽管“共识”和“一致”在汉语中含义相近,但在计算机领域,这两个术语具有截然不同的含义:
- 共识(Consensus):指所有节点就某项操作(如选主、原子事务提交、日志复制、分布式锁管理等)达成一致的实现过程。
- 一致性(Consistency):描述多个节点的数据是否保持一致,关注数据最终达到稳定状态的结果。
在分布式系统中,节点故障不可避免,但部分节点故障不应该影响系统整体状态。通过增加节点数量,依据“少数服从多数”原则,只要多数节点(至少 N/2+1 )达成一致,其状态即可代表整个系统。这种依赖多数节点实现容错的机制称为 Quorum 机制。
Quorum 机制
- 3 节点集群:Quorum 为 2,允许 1 个节点故障;
- 4 节点集群:Quorum 为 3,允许 1 个节点故障;
- 5 节点集群:Quorum 为 3,允许 2 个节点故障。
节点个数为 N 的集群,能容忍 (N−1)/2 个节点故障。你注意到了么?3 节点和 4 节点集群的故障容忍性一致。所以,一般情况下,以容错为目的的分布式系统没必要使用 4 个节点。
根据上述的讨论,基于 Quorum 的机制,在不可靠的环境下,通过“少数服从多数”协商机制达成一致的决策,从而对外表现为一致的运行结果。这一过程被称为节点间的“协商共识”。
一旦解决共识问题,便可提供一套屏蔽内部复杂性的抽象机制,为应用层提供一致性保证,满足多种需求,例如:
- 主节点选举
- 原子事务提交
- 分布式锁管理
- 日志复制
Paxos 算法
Paxos 算法由 Leslie Lamport[1] 于 1990 年提出,是一种基于消息传递、具备高度容错特性的共识算法,是当今分布式系统中最重要的理论基础,几乎就是“共识系统”的代名词。
Lamport 在分布式系统理论方面有非常多的成就,比如 Lamport 时钟、拜占庭将军问题、Paxos 算法等等。除了计算机领域之外,其他领域的无数科研工作者也要成天和 Lamport 开发的一套软件打交道,目前科研行业应用最广泛的论文排版系统 —— LaTeX (名字中的 La 就是指 Lamport)
在 Paxos 算法中,节点分为三种角色:
- 提议者(Proposer):提议者是启动共识过程的节点,它提出一个值,请求其他节点对这个值进行投票,提出值的行为称为发起“提案”(Proposal),提案包含提案编号 (Proposal ID) 和提议的值 (Value)。注意的是,Paxos 算法是一个典型的为“操作转移”模型设计的算法,为简化表述,本文把提案类比成“变量赋值”操作,但你应该理解它是“操作日志”相似的概念,后面介绍的 Raft 算法中,直接就把“提案”称做“日志”了。
- 决策者(Acceptor):接受或拒绝提议者发起的提案,如果一个提案被超过半数的决策者接受,意味着提案被“批准”(accepted)。提案一旦被批准,意味着在所有节点中达到共识,便不可改变、永久生效。
- 记录者(Learner):记录者不发起提案,也不参与决策提案,它们学习、记录被批准的提案。
Raft 算法
Paxos 算法中“节点众生平等”,每个节点都可以发起提案。多个提议者并行发起提案,是活锁、以及其他异常问题的源头。那如何不破坏 Paxos 的“节点众生平等”基本原则,又能在提案节点中实现主次之分,限制节点不受控的提案权利?
Raft 算法的设计机制是明确领导者、增加选举机制交接提案权利。Raft 算法中,节点分为以下三种角色:
- 领导者(Leader):负责处理所有客户端请求,将请求转换为“日志”复制到其他节点,不断地向所有节点广播心跳消息:“你们的领导还在,不要发起新的选举”。
- 跟随者(Follower):接收、处理领导者的消息,并向领导者反馈日志的写入情况。当领导者心跳超时时,他会主动站起来,推荐自己成为候选人。
- 候选人(Candidate):候选人属于过渡角色,他向所有的节点广播投票消息,如果他赢得多数选票,那么他将晋升为领导者;
第七章:容器编排技术
为了减轻管理容器的心智负担,实现容器调度、扩展、故障恢复等自动化机制,容器编排系统应运而生。过去十年间,Kubernetes 发展成为容器编排系统的事实标准,成为大数据分析、机器学习以及在线服务等领域广泛认可的最佳技术底座。
Kubernetes 在解决复杂问题的同时,本身也演变成当今最复杂的软件系统之一。目前,包括官方文档在内的大多数 Kubernetes 资料都聚焦于“怎么做”,鲜有解释“为什么这么做”。自 2015 年起,Google 陆续发布了《Borg, Omega, and Kubernetes》及《Large-scale cluster management at Google with Borg》等论文,分享了 Google 内部开发和运维 Borg、Omega 和 Kubernetes 系统的经验与教训。
容器编排系统的演进
Google 先后设计了三套不同的容器管理系统,Borg、Omega 和 Kubernetes,并向外界分享了大量的设计思想、论文和源码,直接促进了容器技术的普及和发展,对整个行业的技术演进产生了深远的影响。
从 Borg 到 Kubernetes,容器技术的价值早已超越了单纯提升资源利用率。更深远的影响在于,系统开发和运维的理念从“以机器为中心”转变为“以应用为中心”:
- 容器封装了应用程序的运行环境,屏蔽了操作系统和硬件的细节,使得业务开发者不再需要关注底层实现;
- 基础设施团队可以更灵活地引入新硬件或升级操作系统,最大限度减少对线上应用和开发者的影响;
- 每个设计良好的容器通常代表一个应用,因此管理容器就等于管理应用,而非管理机器;
- 将收集的性能指标(如 CPU 使用率、内存用量、QPS 等)与应用程序而非物理机器关联,显著提高了应用监控的精确度和可观测性。
容器技术的原理与演进
文件系统隔离
chroot 是“change root”的缩写,它允许管理员将进程的根目录锁定在特定位置,从而限制进程对文件系统的访问范围。chroot 的隔离功能对安全性至关重要。
1 | $ mkdir -p new-root/{bin,lib64,root} |
除了 /bin 之外,如果我们将程序依赖的 /etc、/proc 等目录一同打包进去,实际上就得到了一个 rootfs 文件。因为 rootfs 包含的不仅是应用,还有整个操作系统的文件和目录,这意味着应用及其所有依赖都被封装在一起,这正是容器被广泛宣传为一致性解决方案的由来。
资源全方位隔离
从 Linux 内核 2.6.19 起,逐步引入了 UTS、IPC、PID、Network 和 User 等命名空间功能。到了 3.8 版本,Linux 实现了容器所需的六项最基本的资源隔离机制。Linux 4.6 版本起,新增了 Cgroup 和 Time 命名空间。
资源全方位限制
进程的资源隔离已经完成,如果再对使用资源进行额度限制,就能对进程的运行环境实现“进乎完美”的隔离。这就要用 Linux 内核的第二项技术 —— Linux Control Cgroup(Linux 控制组群,简称 cgroups)。
cgroups 是 Linux 内核用于隔离、分配并限制进程组使用资源配额的机制。例如,它可以控制进程的 CPU 占用时间、内存大小、磁盘 I/O 速度等。
设计容器协作的方式
登录到 Linux 机器后,执行 pstree -g 命令可以查看当前系统中的进程树状结构。
1 | $ pstree -g |
超亲密容器组 Pod
在 Kubernetes 中,与“进程组”对应的设计概念是 Pod。Pod 是一组紧密关联的容器集合,它们共享 IPC、Network 和 UTS 等命名空间,是 Kubernetes 管理的最基本单位。
容器镜像的原理与应用
所谓的“容器镜像”,其实就是一个“特殊的压缩包”,它将应用及其依赖(包括操作系统中的库和配置)打包在一起,形成一个自包含的环境。
1 | $ docker build 镜像名称 |
Docker 镜像利用联合文件系统的分层设计。整个镜像从下往上由 6 个层组成:
- 最底层是基础镜像 Debian Stretch,相当于“base rootfs”,所有容器可以共享这一层;
- 接下来的 3 层是通过 Dockerfile 中的 ADD、ENV、CMD 等指令生成的只读层;
- Init Layer 位于只读层和可写层之间,存放可能会被修改的文件,如 /etc/hosts、/etc/resolv.conf 等。这些文件原本属于 Debian 镜像,但容器启动时,用户往往会写入一些指定的配置,因此 Docker 为其单独创建了这一层;
- 最上层是通过 CoW(写时复制)技术创建的可写层(Read/Write Layer)。
通过镜像分层设计,以 Docker 镜像为核心,不同公司和团队的开发人员可以紧密协作。每个人不仅可以发布基础镜像,还可以基于他人的基础镜像构建和发布自己的软件。镜像的增量操作使得拉取和推送内容也是增量的,这远比操作虚拟机动辄数 GB 的 ISO 镜像要更敏捷。
容器持久化存储设计
Docker 通过将宿主机目录挂载到容器内部的方式,实现数据持久化存储。如图 7-21 所示,目前它支持三种挂载方式:bind mount、volume 和 tmpfs mount。
容器间通信的原理
资源弹性伸缩
第八章:服务网格技术
什么是服务网格
服务网格是一个处理服务通讯的专门的基础设施层。它的职责是在由云原生应用组成服务的复杂拓扑结构下进行可靠的请求传送。在实践中,它是一组和应用服务部署在一起的轻量级的网络代理,对应用服务透明。
—— What’s a service mesh?And why do I need one?,William Morgan
服务网格是一个基础设施层,用于处理服务间通信。云原生应用有着复杂的服务拓扑,服务网格保证请求在这些拓扑中可靠地穿梭。在实际应用当中,服务网格通常是由一系列轻量级的网络代理组成的,它们与应用程序部署在一起,但对应用程序透明。
服务间通信的演化
实施微服务架构时,需要解决问题(服务注册、服务发现、负载均衡、熔断、限流等)的本质是保证服务间请求的可靠传递。站在业务的角度来看,无论上述逻辑设计的多么复杂,都不会影响业务请求本身的业务语义与业务内容发生任何变化,实施微服务架构的技术挑战和业务逻辑没有任何关系。
回顾前面提到的 TCP/IP 协议案例,我们思考是否服务间的通信是否也能像 TCP 协议栈那样:“人们基于 HTTP 协议开发复杂的应用,无需关心底层 TCP 协议如何控制数据包”。如果能把服务间通信剥离、并下沉到微服务基础层,工程师将不再浪费时间编写基础设施层的代码,而是将充沛的精力聚焦在业务逻辑上。
第一代服务网格由一系列独立运行的代理型服务(Sidecar)构成,但并没有思考如何系统化管理这些代理服务。为了提供统一的运维入口,服务网格继续演化出了集中式的控制面板(Control Plane)。
典型的第二代服务网格以 Google、IBM 和 Lyft 联合开发的 Istio 为代表。根据 Istio 的总体架构(见图 8-8),第二代服务网格由两大核心组成部分:一系列与微服务共同部署的边车代理(称为数据平面),以及用于管理这些代理的控制器(称为控制平面)。控制器向代理下发路由、熔断策略、服务发现等策略信息,代理根据这些策略处理服务间的请求。
数据平面的设计
控制平面的设计
第九章:系统可观测性
什么是可观测性
遥测数据(telemetry data)是指采样和汇总有关软件系统性能和行为的数据,这些数据(接口的响应时间、请求错误率、服务资源消耗等)用于监控和了解系统的当前状态。
实际上,软件领域的观测与上述火箭发射系统相似,都是通过全面收集系统运行数据(遥测数据),以了解内部状态。因此,观测本质上是一种数据收集与分析的科学,旨在帮助解决复杂系统中的故障检测、性能优化和风险预警等问题。
可观测性与传统监控
可观测性与监控的关系
监控告诉我们系统哪些部分是正常的,可观测性告诉我们系统为什么不正常了。
——by《高性能 MySQL》作者 Baron Schwartz
遥测数据的分类与处理
业界将系统输出的数据总结为三种独立的类型,它们的含义与区别如下:
指标(metric):量化系统性能和状态的“数据点”,每个数据点包含度量对象(如接口请求数)、度量值(如 100 次/秒)和发生的时间,多个时间上连续的数据点便可以分析系统性能的趋势和变化规律。指标是发现问题的起点,例如你半夜收到一条告警:“12 点 22 分,接口请求成功率下降到 10%”,这表明系统出现了问题。接着,你挣扎起床,分析链路追踪和日志数据,找到问题的根本原因并进行修复。
日志(log):系统运行过程中,记录离散事件的文本数据。每条日志详细描述了事件操作对象、操作结果、操作时间等信息。例如下面的日志示例,包含了时间、日志级别(ERROR)以及事件描述。日志为问题诊断提供了精准的上下文信息,与指标形成互补。当系统故障时,“指标”告诉你应用程序出现了问题,“日志”则解释了问题出现的原因。
链路追踪(trace):记录请求在多个服务之间的“调用链路”(Trace),以“追踪树”(Trace Tree)的形式呈现请求的“调用”(span)、耗时分布等信息。
指标的处理
为便于理解和使用不同类型的指标,Prometheus 定义了四种指标类型:
- 计数器(Counter):一种只增不减的指标类型,用于记录特定事件的发生次数。常用于统计请求次数、任务完成数量、错误发生次数等。在监控 Web 服务器时,可以使用 Counter 来记录 HTTP 请求的总数,通过观察这个指标的增长趋势,能了解系统的负载情况;
- 仪表盘(Gauge):一种可以任意变化的指标,用于表示某个时刻的瞬时值。常用于监控系统的当前状态,如内存使用量、CPU 利用率、当前在线用户数等;
- 直方图(Histogram):用于统计数据在不同区间的分布情况。它会将数据划分到多个预定义的桶(bucket)中,记录每个桶内数据的数量。常用于分析请求延迟、响应时间、数据大小等分布情况。比如监控服务响应时间时,Histogram 可以将响应时间划分到不同的桶中,如 0-100ms、100-200ms 等,通过观察各个桶中的数据分布,能快速定位响应时间的集中区间和异常情况;
- 摘要(Summary):和直方图类似,摘要也是用于统计数据的分布情况,但与直方图不同的是,Summary 不能提供数据在各个具体区间的详细分布情况,更侧重于单一实例(例如单个服务实例)的数据进行计算。
日志的索引与存储
处理日志本来是件稀松平常的事情,但随着数据规模的增长,量变引发质变,高吞吐写入(GB/s)、低成本海量存储(PB 级别)以及亿级数据的实时检索(1 秒内),已成为软件工程领域最具挑战性的难题之一。
分布式链路追踪
2010 年 4 月,Google 工程师发表了论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》[1],论文总结了他们治理分布式系统的经验,并详细介绍了 Google 内部分布式链路追踪系统 Dapper 的架构设计和实现方法。
如今的链路追踪系统大多以 Dapper 为原型设计,因为它们也统一继承了 Dapper 的核心概念:
- 追踪(trace):Trace 表示一次完整的分布式请求生命周期,它是一个全局上下文,包含了整个调用链所有经过的服务节点和调用路径。例如,用户发起一个请求,从前端服务到后端数据库的多次跨服务调用构成一个 Trace。
- 跨度(Span):Span 是 Trace 中的一个基本单元,表示一次具体的操作或调用。一个 Trace 由多个 Span 组成,按时间和因果关系连接在一起。Span 内有描述操作的名称 span name、记录操作的开始时间和持续时间、Trace ID、当前 Span ID、父 Span ID(构建调用层级关系)等信息。
性能剖析
可观测性领域的性能剖析(Profiling)的目标是分析运行中的应用,生成详细的性能数据(Profiles),帮助工程师全面了解应用的运行行为和资源使用情况,从而识别代码中的性能瓶颈。
性能数据通常以火焰图或堆栈图的形式呈现,分析这些数据是从“是什么”到“为什么”过程中的关键环节。
火焰图分析说明
- 纵轴:表示函数调用的堆栈深度(或层级)。纵向越高表示调用链越深,底部通常是程序的入口函数(如 main 函数),上层是被下层函数调用的函数。
- 横轴:表示函数在特定时间段内所占用的 CPU 时间或内存空间,条形的宽度越大,表示该函数消耗的时间或资源越多。
分析火焰图的关键是观察横向条形的宽度,宽度越大,函数占用的时间越多。如果某个函数的条形图出现“平顶”现象,表示该函数的执行时间过长,可能成为性能瓶颈。
核心转储
核心转储(Core dump)中的 “core” 代表程序的关键运行状态,“dump” 的意思是导出。
第十章 应用封装与交付
“以应用为中心”的设计思想
正是因为以应用为中心,整个云原生技术体系无限强调基础设施更好地服务于应用,以更高效的方式为应用提供基础设施能力,而不是反其道行之。而相应的,Kubernetes 也好、Docker 也好、Istio 也好,这些在云原生生态中起到了关键作用的开源项目,就是让这种思想落地的技术手段。
声明式管理的本质
分析 Kubernetes 的工作原理可以发现,无论是 kube-scheduler 调度 Pod,还是 Deployment 管理 Pod 部署,亦或是 HPA 执行弹性伸缩,它们的整体设计都遵循“控制器模式”。
从“封装配置”到“应用模型”
参考链接
- 深入高可用系统原理与设计,by thebyte.