0%

分布式系统

随着时代的发展,传统的单台机器可能无法完成我们期待的任务,所以发展出了分布式系统这么一个概念,目的是为了提高系统的性能、可伸缩性、可用性、容错性。其本质就是堆机子,通过增加机器数量,完成之前单台机器难以完成的任务。
image.png
分布式系统:多台独立的计算机组成的系统,我们将每一台计算机视为一个结点,这些节点通过网络互相通信和协作。
但是引入了分布式系统之后,随之而来的是单体架构不会出现的问题。
比如

  • 通信和网络问题:在单体架构中,只有一台机器,无需和其他机器进行通信。而在分布式系统中,节点之间通过网络进行通信,这就导致可能会出现消息延迟、丢包、网络不稳定等问题造成的系统稳定性下降
  • 一致性和可用性的权衡:在单体架构中,一台机器接受请求之后进行处理,处理完成即可响应。但是在分布式系统中,在出现网络分区时,你可以选择一致性(先同步数据、再进行响应),也可以选择可用性(先进行响应、再进行数据同步)。
  • 数据一致性问题:多节点环境下,确保数据的一致性变得更为复杂,而且无法做到数据的实时强一致性,只能保证系统在一定时间的稳定运行之后,各节点的数据趋于一致。

为了解决这些问题,业界提出了一些分布式基石级别的理论以及落地方案。
本文会向大家介绍分布式的一些基础理论,以及流行的分布式算法。

分布式基础理论

CAP理论

CAP是分布式系统方向中的一个非常重要的理论,可以粗略的将它看成是分布式系统的起点,CAP分别代表的是分布式系统中的三种性质,分别是Consistency(可用性)、Availability(一致性)、Partition tolerance(网络分区容忍性),它们的第一个字母分别是C A P,于是这个理论被称为CAP理论。

  • 一致性(Consistency):所有节点在同一时间看到的数据是一致的。在分布式系统中,一致性要求所有节点对于某个操作的执行都具有相同的视图。
  • 可用性(Availability):系统在有限时间内能够为用户提供满足要求的响应,即系统对于请求的响应不能无期限的延迟或失败。
  • 分区容忍性(Partition Tolerance):成熟的分布式系统必须满足的性质,系统能够在节点之间发生网络分区的请款修改继续工作。

理论上来说,CAP三者同时最多满足两者,但是并不是必须满足两个,许多系统最多只能同时满足0、1个

为什么CAP最多只能满足两个呢?

我们可以以电商系统来当做例子,这个电商系统有两台服务器,彼此之间使用网络进行通信。

网络正常的时候,可以同步数据到另一台机器之后,再进行返回,或者返回之后再进行数据的同步。但是一旦出现了网络隔离,那么就可以有两个选择,即先同步数据还是先返回响应

image.png
如下图所示,假设当一个请求打到了Server2这里
image.png
C: 追求的是数据一致性 当有一个请求来了之后 它会等待网络隔离的情况结束之后 向另一个机器进行数据的同步

首先,他会在本地处理好请求,这个请求常常会伴随着某些数据的变化,比如缓存内容的变化,程序内部某些共享变量的变化等等。
image.png
之后,它会等待网络恢复,即能够和Server1进行通信。
image.png
网络恢复后,Server1和Server2处于同一个网络中,彼此可以通信,这时进行数据的同步。
image.png
完成数据同步之后,返回响应。
image.png

A: 追求的是可用性 也就是尽可能提供有效服务 当一个请求来了之后 它会立即返回 哪怕数据是陈旧的 也得优先提供服务,其他分区的节点返回的结果(数据)可能是不一样,图也是反过来的。

注意:这里的AC不可同时满足指的是当整个分布式系统中出现网络隔离的时候,我们不能既想着保证数据的实时强一致性,又去追求服务的可用性

但是当没有网络隔离的时候,其实这两个性质是可以同时满足的,因为『同步数据』和『返回结果』这两个操作都是在同一个网络中,只有先后关系,不会因为某个操作导致另一个操作的『死等』。

在分布式系统中,P是会必然发生的,造成P的原因可能是网络隔离,也可能是节点宕机。

我们无法保证分布式系统每一时刻都不出现网络隔离,如果不满足P的特性,一旦发生分区错误,那么分布式系统就无法工作,这显然违背了分布式的理念,连最基本的分布式系统条件都没有满足

典型的CP和AP的产品

CP:Zookeeper 当系统在发生分区故障之后 客户端的所有请求都会被卡死或者超时 但是系统总会返回一致的数据

AP:Eureka 分区发生故障之后 客户端依然可以访问系统 但是获取的数据有的是新数据 有的是老数据

当然 ,CAP这几个特性不是BOOL类型的,而是一个范围类型,完全是看系统具体需要什么样的要求。

比如分区容错,有的系统一台机器出错,系统会认为不影响业务的话,认为分区不存在。只有多台机器都出问题了,系统受到严重影响才认为出现分区

PACELC理论

PACELC理论是对CAP理论的扩展,在维基百科上的定义是

It states that in case of network partitioning (P) in a distributed computer system, one has to choose between availability (A) and consistency (C) (as per the CAP theorem), but else (E), even when the system is running normally in the absence of partitions, one has to choose between latency (L) and consistency (C).

翻译:如果有分区(P),那么系统就必须在可用性(A)和一致性(C)之间取得平衡,否则(E),当系统运行在无分区的情况下,系统需要在延迟(L)和一致性(C)之间取得平衡。

它相比于CAP,多引入了一个延迟Latency的概念,在出现分区错误的时候,取前半部分PAC,理论和CAP的内容一致。没有出现分区错误的时候取LC,也就是Latency与Consistency。

当前分布式系统指导理论更替代CAP理论,理由如下

  • PACELC更能满足实际操作中分布式系统的工作场景,是更好的工程实现策略
  • 当P存在的场景下,需要在A C之间做取舍,但是实际上分布式系统大部分时间里P是不存在的,那么在L和C之间做取舍是一个更好的选择
  • PACELC可以在latency与consistency之间获得平衡

要保证系统的高可用,那么就得采用冗余的思想,我的其他博文有提到4个9的异地多活策略,也是采用的数据冗余思想,而一旦涉及到了复制数据,在分布式中就一定会在Consistency和Latency之间做一个取舍

举个例子

在强一致性的场景下,需要三个从节点都落盘数据,才能给客户端返回OK,这个时候当master向slave同步数据的时候,超过20ms触发超时了,整个系统还是会不断的重试这个过程,这显然造成了系统的可用性比较低
所以我们一般都会在数据一致性和请求时延之间做一个balance

例如:当同步超过五次之后,认为这个节点故障,选择直接返回,可以消除写时的长尾抖动,同时给节点打上故障标签,进行后续的处理

BASE模型

base模型是Basically Avaliable(基本可用)、Soft State(软状态)、Eventually Consistent(最终一致性)三个短语的缩写,核心思想如下

即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。对应到CAP中的概念,就是牺牲C,来保证AP的满足,这是对传统ACID模型的取舍,适用于大规模的分布式系统,尤其是与存储相关的分布式组件,例如分布式缓存、分布式存储等等。

BA:基本可用指的是当系统出现了不可预知的故障,系统依旧可用,不过可用度也许会降低,比如响应时间上出现损失,功能上只能满足基本功能等等

S:基于原子性而言的话,当要求多个节点数据一致时,我们认为这是一种『硬』状态,而允许系统中的数据存在中间状态,并认为其不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据时延

E:最终一致性,系统不可能一直都处于一个软状态中,必须有个时间期限。在期限过后,应该保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间期限取决于时延、负载、方案等等

在工程实践中,有这么几种最终一致性的实现策略,通常都是多种策略混合实现

  • 因果一致性:如果节点A在更新完某个数据后通知了节点B,那么节点B之后对该数据的访问和修改都是基于A更新后的值。与此同时,与节点A无因果关系的节点C的数据访问没有这样的限制
  • 读已知所写:节点A更新一个数据之后,自身总是能访问到更新过的最新值,而不会访问旧值
  • 会话一致性:将对系统数据的访问过程框定在了一个会话当中,系统能保证同一个有效的会话中实现客户端在一个会话中读取到该数据项永远是最新值
  • 单调读一致性:如果一个节点从系统中读取出一个数据项的某个值之后,那么系统对于该节点后续的任何数据访问都不该返回更旧的值
  • 单调写一致性:一个系统要能够保证来自同一个节点的写操作被顺序的执行。

NWR多数派理论

NWR多数派理论是分布式系统共识和分布式一致性算法的基础,只有理解了NWR才能理解RaftPaxosGossip这些分布式一致性算法。这个理论是分布式系统中一种常见的一致性模型,被广泛应用于保证数据的一致性和可靠性,以及系统的可用性。

它指的是:在多数副本的一致性模型中,只有大多数副本确认了某个操作,才认为这个操作已经完成

NWR中N代表的是副本数量,W代表写入的副本数量,R则为读取的副本数量。在多数的一致性模型中,一般要求W+R>N,以保证读写操作的一致性。同样的,NWR也有着三个阶段。

  • Negotiation(协商):所有节点通过相互通信来达成一致性决策,在协商阶段,节点之间需要同步信息,最后达成一致。
  • Write(写入):决策被转化为实际的操作,节点进行写入操作。
  • Read(读取):节点读取其他节点的值,并使用协商阶段的决策和写入阶段的结果,也就是说读一定是读的最新值。

在写入操作的时候,只有W个副本被成功写入才返回成功,而在读取操作时,只有R个副本成功返回相同的数据才返回成功。这样,只要大多数副本成功确认了操作,就可以认为这个操作已经完成。

NWR在现有组件的应用还是很广泛的,比如Raft选主判断逻辑为投票数量>=n/2则成功选主,比如Redis的哨兵机制,有哨兵标记下线则为主观下线,>=n/2标记下线则为客观下线。

分布式一致性算法

分布式一致性算法用于确保在分布式系统中不同节点之间达成一致决策的算法。这些算法致力于解决由于节点故障、网络分区或并发操作等原因导致的数据不一致的问题。
常见的分布式一致性算法有Raft、Paxos、Gossip等等,在分布式一致性算法中,每台计算机都视为等同的节点,这和分布式事务完全相反。
分布式事务和分布式一致性算法的目的是一样的,本质都是将一个任务放在分布式的环境下处理和解决,但是实现则不一样。

  • 在分布式事务中,不同机器的作用是不一样的,一个任务会被拆解成多个步骤,分别交给不同的角色进行处理,全部成功则提交,一旦有失败则回滚,也就是不同机器各司其职。
  • 在分布式算法中,每台机器的作用都是一样的,每一个节点都能处理请求,它的作用是进行各个节点之间的同步,并解决可能出现的问题,比如数据竞态问题(data racing)等等。

具体算法的实现细节我们另开一篇文章来阐述。

总结

本文向大家介绍了常见的分布式基础理论,后面还会向大家介绍Raft、Paxos、Gossip等分布式算法。

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

微服务通信的难点

在微服务架构中,不同的服务是托管在不同的代码库,运行在不同的进程甚至是机器上的,这会比单体架构中服务之间进行通信付出更大的成本

目前有很多进程间通信的技术可供开发者选择,服务可以基于同步请求/响应的通信机制,比如HTTP RESTFUL 或者 gRPC。另外也可以使用具有异步的消息队列,比如AMQP 或者 STOMP。消息的格式也不尽相同,可以选择具有可读性的JSON,也可以使用更加高效的、基于二进制的Protocol Buffers

在通信中最重要的东西其实是协议Protocol,只要双方进行通信,那就都是基于某种Protocol实现的。比如说使用MQ进行通信,以Pulsar进行举例,那么一条消息的ID通常是以下格式

分区ID:分块ID:偏移量

如果使用HTTP进行通信,那么其实URL Header Body如何编码及解析 也是一种协议 比如HTTP HEADER必带的某些属性,比如版本 以及空行等等

语义化版本控制规范(Semvers)

Semvers要求API版本号由三个部分组成

  • MAJOR 当你对API进行不兼容的更改时
  • MINOR 当你对API进行向后兼容的增强时
  • PATCH 当你进行向后兼容的错误修复时

有几个地方可以在API中使用版本号,比如实现REST API,则可以使用主要版本作为URL路径的第一个元素;或者如果要实现使用消息机制的服务,则可以在发布的消息中包含版本号。这些做法都是为了正确地为API设置版本,并以受控的方式更改它们

例如 REST 的API路径为 /v1/...为前缀 就是进行主要并且不向后兼容的改变

两类消息

消息的格式大体可以分为文本和二进制两大类

文本:JSONXML,好处是它们的可读性很高,同事也是自描述的。XMLJSON都可以允许消息的接收方只挑选它们感兴趣的值,而忽略掉其他。因此,对消息的修改可以做到很强的后向兼容性

弊端主要是消息往往会冗余过长,特别是XML。消息的每一次传递都必须反复包含除了值意外的属性名称,这样会造成额外的开销。另一个弊端是解析文本引入的额外开销,尤其是消息比较大的时候,因此在对性能和效率比较敏感的场景下,更得倾向于基于二进制格式的消息
一个典型的XML数据如下

1
2
3
4
5
6
7
8
9
10
11
12
<root>
<person>
<name>John</name>
<age>30</age>
<address>
<street>123 Street</street>
<city>New York</city>
<country>USA</country>
</address>
</person>
</root>

一个典型的JSON数据如下

1
2
3
4
5
6
7
8
9
10
11
{
"person": {
"name": "John",
"age": 30,
"address": {
"street": "123 Street",
"city": "New York",
"country": "USA"
}
}
}

二进制消息:有几种不同的二进制格式,比如Protocol buffersThrift,这两种格式都提供了一个强类型定义的IDL(接口描述文件),用于定义消息的格式。编译器会自动根据这些格式生成序列化和反序列化的代码。因此你不得不采用API优先的方法来进行服务设计

Protocol Buffers使用tagged filed(带标记的字段)来标记数据和格式

REST成熟度模型

REST成熟度模型

  • LEVEL 0 客户端只是向服务端发起POST 请求 进行服务调用 所有操作都通过POST进行实现
  • LEVEL 1 引入了资源的概念。要执行对资源的操作,客户端需要发出执行要执行的操作,和包含任何参数的POST请求 也是所有操作都通过POST实现
  • LEVEL 2 使用HTTP动词来执行操作,譬如GET 获取 POST创建 PUT更新
  • LEVEL 3 在由GET请求返回的资源中包含链接,这些链接能够执行该资源允许的操作。例如客户端通过订单资源的链接取消某一个订单,或者发送GET请求去获取该订单。这样也可以避免在客户端代码中写入硬链接的URL

在一个请求中获取多个资源的挑战–GraphQL

REST资源通常以业务为导向,因此在设计REST API时的一个常见问题就是如何使得客户端可以在单个请求中检索多个相关的对象。假设客户端想要检索Order和这个Order的Consumer,纯REST API要求客户端至少发出两个请求,一个用于Order,另一个用于Consumer。更复杂的情况需要往返并且遭受更多的延迟

一个解决方案是API允许客户端在获取资源时检索相关资源,例如客户可以受用GET /orders/order-id-1345?expand=consumer 检索Order及其consumer 。这种方法在许多场景中都很有效,但对于更加复杂的场景来说,它通常是不够的,实现它也可能很耗时。

这就导致了GraphQL的出现,它们旨在支持高效的数据获取,以下是一个典型的GraphQL请求数据 来进行参数的请求

1
2
3
4
5
6
7
{
"hero": {
"name": "Luke Skywalker",
"height": 1.72,
"mass": 77
}
}

REST优缺点

优点:
1.简单 开发者熟悉
2.可以使用浏览器拓展或者命令行进行测试 比如postman和curl
3.直接支持请求/响应方式的通信
4.HTTP对防火墙友好
5.不需要中间代理 简化结构

缺点:
1.只支持请求/响应方式的通信
2.可能导致可用性降低 由于客户端和服务直接通信而没有使用代理来缓冲 因此它们必须在REST API调用期间都保持在线
3.客户端必须知道服务实例的位置 (URL) 这是现代应用程序中的一个重要问题
4.在单个请求中获取多个资源具有挑战性
5.有时很难将多个更新操作映射到http动词

grpc:使用REST的一个挑战是由于HTTP仅提供优先数量的动词,因此设计支持多个更新操作的REST API并不容易,避免这个问题的进程间通信方式就是GRPC 这是一个用于编写跨语言客户端和服务端的框架

grpc是一种基于二进制消息的协议,可以通过Protocol Buffer的IDL来定义grpc 的api 可以使用protocol buffer编译器来生成客户端的桩 和服务端的骨架 ,分别称为stubskeleton,编译器可以为各种语言生成代码,包括java c ``nodejsgolang

客户端和服务器底层使用的是http 2协议,grpc api由一个或者多个请求/响应消息定义组成,服务定义类似于接口,是强类型方法的集合,除了支持简单的请求/响应RPC之外,grpc还支持流式rpc,分为客户端流式、服务端流式、双向流式等等,并以protocol buffer格式交换二进制的消息数据,protocol buffer是一种高效且紧凑的二进制标记格式。protocol buffers消息的每个字段都有编号,并且有一个类型代码,消息接收方可以提取所需的字段,并跳过其无法识别的字段,因此grpc使api能够在保持向后兼容的同时进行变更

grpc的好处

  • 设计具有复杂更新操作的API很简单
  • 具有高效、紧凑的进程间通信消息,效率很高,尤其是在交换大量消息的时候
  • 支持在远程过程调用和消息传递过程中使用双向流式消息方式
  • 实现了客户端和用各种语言编写的服务端之间的互操作性

弊端

  • 基于REST JSON的API机制相比,请求的客户端需要做更多的工作,比如定义对应的protocol buffer文件
  • 老的防火墙可能不支持http 2

使用服务发现

只要我们进行某些远程调用,不管是rpc还是restful api,为了发出请求,我们都需要知道服务实例的网络位置,我们把这个过程叫做服务发现。

通常RPC调用的服务发现通过服务注册与发现中心来实现,而restful api的服务发现是通过DNS + IP和端口来实现的

在传统的应用程序中,服务实例的网络位置通常是静态的。例如,代码可以从偶尔更新的配置文件中读取网络位置。但在现代的基于云微服务的应用程序中,通常不那么简单,IP地址和服务实例并不是强关联的了,某些时候服务实例会动态的销毁、创建,空出IP地址和占用IP地址

因此服务实例需要具有动态分配的网络地址,并且服务实例会因为自动扩展、故障和升级的原因进行动态的更改

服务发现在概念上非常简单:关键是一个服务注册表,这是包含服务实例网络位置信息的一个数据库,当服务实例启动和停止时,服务发现机制会更新服务注册表。当客户端调用服务时,服务发现机制会查询服务注册表以获取可用服务实例的列表,并将请求路由到其中一个服务实例

实现服务发现有一下两种主要方式

  • 服务及其客户直接与服务注册表交互,比如CONSULETCD等等 也就是自注册+客户端服务发现 服务实例掉哟个服务注册表的注册API来注册起网络位置,并且会定期进行健康检查,通过心跳机制防止过期;当客户端想要调用服务的时候,会查询服务注册表一获取服务实例的列表。为了提高性能,客户端可以选择缓存服务实例,然后通过负载均衡算法来选择服务实例之后发出请求。并且可以处理多平台部署的问题。比如,有一些服务在K8S上部署,其余服务在遗留环境中运行。在这种情况下,使用consul就能同时适用于两种环境,而基于k8s的服务发现仅能用于部署在K8S平台上的部分服务
  • 基础设施来处理服务发现 Docker\K8s,部署平台为每个服务提供DNS 虚拟IP 和解析的DNS名称等等 服务注册、发现、路由完全都通过部署平台进行处理

消息队列

消息队列和上述提到的不同的最大的点就是 不管是HTTP RPC 还是服务注册与发现中心

它们的调用都是同步的 即发出请求 立马就能得到结果

而消息队列可以是异步的 生产者生产完成消息之后 消费者并不一定需要立马去进行消息的消费和后续处理

异步 是消息队列最重要的功能之一

具体的消息队列学习内容可以看笔者之前的关于消息队列系统学习的一篇博客

简单来说 消息队列就是

通过生产者、消息中间件、消费者这几个组件实现点对点和发布订阅、PULL/PUSH几种模式

消息队列又根据有没有消息中间件这个组件分为无代理消息和有代理消息

无代理消息:

  • 允许更轻的网络流量和更低的延迟 少了两次转发
  • 避免了消息中间件可能成为性能瓶颈的可能性
  • 不需要维护消息 降低操作复杂度

弊端:

  • 服务之间需要知道彼此的位置 必须采用服务发现机制
  • 导致可用性降低 因为在交换消息时 消息的发送方和接收方都必须在线

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

K8S对象

什么是K8S对象

在k8s中,对象是持久化的实体,k8s使用这些实体去表示整个集群的状态,它们描述了以下信息

  • 哪些容器化应用正在运行
  • 可以被应用使用的资源
  • 关于应用运行时行为的策略,比如重启策略、升级策略以及容错策略

k8s对象是一种”意向表达(Record of Intent)”,一旦创建该对象,K8S系统将不断工作以确保该对象存在。最终系统应该达到K8S所谓的期望状态
我们操作K8S对象需要用到K8S的API,可以直接使用kubectl命令行,也可以在程序中使用客户端库,直接调用K8S API

对象规约(spec)与状态(status)

几乎每个Kubernetes对象都包含两个嵌套的字段,它们负责管理对象的配置,分别是specstatus
对于具有spec的对象,你必须在创建对象时设置其内容,描述希望对象所具有的特征:期望状态(desired state)
status描述了对象的当前状态(current state),它是由k8s系统和组件设置并更新的。
在任何时刻,控制平面都在管理对象的实际状态,以使其达成期望状态

描述K8S对象

创建K8S对象的时候必须提供对象的spec用来描述对象的期望状态,以及对象的 一些基本信息(name kind),当时用API创建独享的时候,API在请求主体中应该包含JSON格式的数据,大部分情况下我们提供的是.yaml文件来为kubectl提供这些信息,当kubectl发起API请求的时候,这些信息会被转换成JSON格式

下面是一个.yaml的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
apiVersion: apps/v1

kind: Deployment

metadata:

name: nginx-deployment

spec:

selector:

matchLabels:

app: nginx

replicas: 2 # 告知 Deployment 运行 2 个与该模板匹配的 Pod

template:

metadata:

labels:

app: nginx

spec:

containers:

- name: nginx

image: nginx:1.14.2

ports:

- containerPort: 80

除了通过.yaml这种声明式API的方式创建Deployment, 也可以通过kubectl命令行的方式,通过命令将.yaml文件作为参数进行创建,下面是一个栗子

1
2
3
4
kubectl apply -f https://k8s.io/examples/application/deployment.yaml //应用某yaml文件  

deployment.apps/nginx-deployment created // output

必需字段
在创建K8S对象中使用的.yaml文件里,需要配置的字段如下

  • apiVersion 创建对象使用的K8S API的版本
  • kind 想要创建的对象类型
  • metadata 帮助唯一标识对象的一些数据 比如name uid和可选的namespace
  • spec 所期望的该对象的状态

K8S对象管理

kubectl命令行工具支持多种不同的方式来创建和管理k8s对象,需要注意的是应该只使用一种技术来管理k8s对象,混合和匹配技术作用在同一对象上将会导致未被定义的行为

  • 指令式命令 作用于活跃对象 支持多个写者 难度低
  • 指令式对象配置 作用于单个文件 支持一个写者 难度中等
  • 声明式对象配置 作用于文件目录 支持多个写者 难度高

指令式命令

使用指令式命令时,用户可以在集群中的活动对象上进行操作,用户将操作传递给kubectl命令作为参数或者标志
推荐在开始的时候或者在集群中运行一次性任务使用这种方式,因为它直接作用在活动对象上,所以它不提供以前配置的历史记录
下面是一个创建deployment对象来运行nginx的实例

1
kubectl create deployment nginx --image nginx

指令式对象配置

kubectl命令制定操作,可选标志和至少一个文件名,文件内需要包含YAML或者JSON格式的对象的完整定义
下面是一些栗子

1
2
3
kubectl create -f nginx.yaml //创建配置文件中定义的对象
kubectl delete -f nginx.yaml -f redis.yaml //删除两个配置文件中定义的对象
kubectl replace -f nginx.yaml //覆盖配置文件中定义的对象

声明式对象配置

使用声明式对象配置时,用户对本地存储的对象配置文件进行操作,但是用户未定义要对文件执行的操作,kubectl会自动检测每个文件的创建、更新和删除操作,这使得配置可以在目录上工作,根据目录中配置文件对不同的对象执行不同的操作

1
2
kubectl diff -f configs/ //查看configs目录下所有对象配置文件要进行的更改
kubectl apply -f configs/ //将configs目录下要进行的更改应用

对象名称与ID

集群中的每一个对象都有一个名称来标识其在同类资源中的唯一性
每个K8S对象也有一个UID来标识在整个集群中的唯一性
比如同一个namespace下只能有一个名为abc的pod,但是可以命名一个pod和一个deployment同为abc

名称

是客户端提供的字符串,引用资源URL中的对象,如/api/v1/pods/xxx
某一时刻,只能有一个给定类型的对象具有给定的名称。但是如果删除该对象就可以创建同名的新对象
名称在同一资源的所有API版本中必须是唯一的,这些API资源通过各自的API组、资源类型、命名空间和名称来区分,也就是说API版本在上下文中不相关的
资源命名约束有DNS子域名RFC 1123标签名RFC 1035标签名路径分段名称这四种命名约束

UID

uid是系统生成的字符串,唯一标识对象,在K8S集群中每个生命周期中创建的每个对象都有一个不同的UID

标签与选择算法

标签(Labels)是附加到K8S对象(比如Pod)上的键值对,旨在指定对用户有意义并且相关的对象的标识属性,但不直接对核心系统有语义含义。标签可以用于组织和选择对象的子集,可以在创建时附加到对象,随后可以随便添加和修改,每个对象都可以定义一组键值标签,每个键对于给定的对象必须是唯一的

1
2
3
4
5
6
"metadata": {
"labels": {
"key1" : "value1",
"key2" : "value2"
}
}

标签能够允许用户以松耦合的方式将他们自己的组织结构映射到系统对象,而无需客户端存储这些映射
常见标签

  • “release” : “stable”, “release” : “canary”
  • “environment” : “dev”, “environment” : “qa”, “environment” : “production”
  • “tier” : “frontend”, “tier” : “backend”, “tier” : “cache”
  • “partition” : “customerA”, “partition” : “customerB”
  • “track” : “daily”, “track” : “weekly”

下面是一个lables带有enviroment和app两个标签的pod

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Pod
metadata:
name: label-demo
labels:
environment: production
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80

标签选择算符
与名称和UID不同的是,标签不支持唯一性,通常我们希望许多对象携带相同的标签
通过标签选择算符,客户端/用户可以识别一组对象
API目前支持两种类型的选择算符:等值和集合

等值支持三个运算符 分别是 = == != 前两者表示相等,含义相同,后者表示不相等
例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
environment = production  //environment label等于xx 
tier != fronted // tier label != xx

//pod选择accelerator = nvidia
apiVersion: v1
kind: Pod
metadata:
name: cuda-test
spec:
containers:
- name: cuda-test
image: "registry.k8s.io/cuda-vector-add:v0.1"
resources:
limits:
nvidia.com/gpu: 1
nodeSelector:
accelerator: nvidia-tesla-p100

基于集合的标签允许通过一组值来过滤间 支持三种操作符 in notin 和 exists

1
2
3
4
environment in (a,b) //env = a 或 b 
tirer notin (a,b) // tire != a & tirer != b
partition //包含了partition标签
!partition //没包含partition标签

API

1
2
kubectl get pods -l environment=production,tier=frontend 
kubectl get pods -l 'environment in (production),tier in (frontend)'

命名空间

Namespace提供了一种机制,将同一集群中的资源划分为相互隔离的组,同一Namespace内的资源名称要唯一,每个K8s资源只能在一个Namespace中,这其实是在多个用户之间划分集群资源的一种方法
初始命名空间
k8s启动的时候会创建四个初始化命名空间

  • default k8s包含这个命名空间,以便于你无需创建新的命名空间就可以使用新集群
  • kube-node-lease 包含用于各个节点关联的Lease租约对象,节点租约允许kubelet发送心跳,由此控制面板能够检测到结点故障
  • kube-public 所有的客户端(包括未经身份验证的客户端)都可以读取该命名空间,该命名空间主要预留为集群使用,以便某些资源可以在整个集群中可见可读
  • kube-system 用于k8s系统创建的对象
1
2
kubectl get namespace //列出集群中现存的namespace 
kubectl run nginx --image=nginx --namespace=<namespacename>

当创建一个服务的时候,k8s会创建一个相应的DNS条目
该条目的形式是<服务名称>.<名字空间名称>.svc.cluster.local,这意味着如果容器只使用服务名称,就会被解析到本地命名空间的服务
大多数对象都存在namespace中,比如pod、service 等,但是namespace的资源本身并不在namespace中,而且底层资源,比如node和持久化卷,不属于任何命名空间

注解

可以通过注解给对象添加任意的非标识的元数据,客户端可以获取这些元数据信息,注解只是添加一些元数据信息,不用来表示和选择对象,类似于编程中的注释,不过这是对于K8S对象的注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"metadata": {
"annotations": {
"key1" : "value1",
"key2" : "value2"
}
}

apiVersion: v1
kind: Pod
metadata:
name: annotations-demo
annotations:
imageregistry: "https://hub.docker.com/"
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80

通常我们可以用注解来记录以下这些信息

  • 由声明性配置所管理的字段。 将这些字段附加为注解,能够将它们与客户端或服务端设置的默认值、 自动生成的字段以及通过自动调整大小或自动伸缩系统设置的字段区分开来。
  • 构建、发布或镜像信息(如时间戳、发布 ID、Git 分支、PR 数量、镜像哈希、仓库地址)。
  • 指向日志记录、监控、分析或审计仓库的指针。
  • 可用于调试目的的客户端库或工具信息:例如名称、版本和构建信息。
  • 用户或者工具/系统的来源信息,例如来自其他生态系统组件的相关对象的 URL。
  • 轻量级上线工具的元数据信息:例如配置或检查点。
  • 负责人员的电话或呼机号码,或指定在何处可以找到该信息的目录条目,如团队网站。
  • 从用户到最终运行的指令,以修改行为或使用非标准功能。

字段选择器

字段选择器Field Selectors允许你根据一个或者多个资源字段的值筛选K8S对象

1
kubectl get pods --field-selector status.phase=Running //筛出来status.phase字段值为runnning的所有Pod

不同的k8s资源类型支持不同的字段选择器,所有资源类型都支持metadata.namemetadata.namespace字段,使用不被支持的字段选择器会产生错误

终结器

终结器(Finalizer)是带有命名空间的键,告诉k8s等到特定的条件被满足后,再完全删除被标记为删除的资源,Finalizer提醒控制器清理被删除的对象拥有的资源

当你告诉K8S删除一个指定了Finalizer的对象时,K8S API通过填充.metadata.deletionTimestamp来标记要删除的对象,并返回202状态码使其进入只读状态,此时控制平面或者其他组件会采取Finalizer所定义的行动,而目标对象仍然处于终止中(Terminating)的状态,这些行为完成后,控制器会删除目标对象相关的Finalizer。当metadata.finalizers字段为空时,kubernetes认为删除已完成并删除对象

工作原理
当使用清单文件创建资源的时候,可以在metadata.finalizers中指定Finalizers。当视图删除该资源的时候,处理删除请求的API服务器会注意到finalizers字段中的值,并进行以下操作

  • 修改对象 将开始执行删除的时间添加到metadata.deletionTimestamp字段
  • 禁止对象被删除,直到其metadata.finalizers字段为空
  • 返回202状态码

属主与附属

在K8S中,一些对象是其他对象的Owner。例如ReplicaSet是一组Pod的属主,具有属主的对象是属主的附属(Dependent)
附属对象中有一个metadata.ownerReferences字段,用于引用其属主对象,一个有效的属主引用,包含与属主对象同在一个命名空间下的对象名称和一个UID,K8S自动为一些对象的附属资源设置属主引用的值,这些对象包含了ReplicaSet DaemonSet Deployment Job CronJob ReplicationController

推荐使用的标签

除了kubectldashboard之外,还可以使用其他工具来可视化和管理K8S对象,一组通用的标签可以让多个工具之间相互操作,用所有工具都能理解的通用方式描述对象
元数据围绕应用的概念进行组织,K8S不是平台即服服务,应用程序是非正式的,并且使用元数据进行描述,应用程序包含的定义应该是送伞的
共享标签和注解都是用同一个前缀:app.kubernetes.io,没有前缀的标签是用户私有的,共享前缀可以确保共享标签不会干扰用户自定义的标签

描述 示例 类型
app.kubernetes.io/name 应用程序的名称 mysql 字符串
app.kubernetes.io/instance 用于唯一确定应用实例的名称 mysql-abcxzy 字符串
app.kubernetes.io/version 应用程序的当前版本(例如语义版本 1.0
、修订版哈希等) 5.7.21 字符串
app.kubernetes.io/component 架构中的组件 database 字符串
app.kubernetes.io/part-of 此级别的更高级别应用程序的名称 wordpress 字符串
app.kubernetes.io/managed-by 用于管理应用程序的工具 helm 字符串

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

K8S与Docker

K8S是从14年发布的,到现在已经成为了容器编排领域的龙头,大部分的个人开发或者团队都会选择使用Kubernetes进行容器的管理

我们可以把集群简单的理解为:一组能够在一起协同工作的计算机

K8S虽然是现在容器编排领域的龙头,但是他也有他的缺点
1.虽然Kubernetes对外宣传的是单个集群最多支持5000结点,Pod总数不超过150000,容器总数不超过30000,但是在具体生产环境中,集群可能就2000左右
2.多集群管理还不够成熟,是K8S社区正在探索的方向

集群接口:
Cluster API也是Kubernetes社区中和多集群管理相关的项目,目标是通过声明式的API简化多集群的准备、 更新和运维工作,也就是通过声明式API定义机器和集群的状态

K8S的一些应用场景
1.应用分发 K8S提供了几种部署应用的最基本方式,分别是Deployment StatefulSet 和 DaemonSet 这些资源分别适用于无状态服务、有状态服务和节点上的 守护进程,这些资源能够提供最基本的策略但是无法应对更复杂的应用
2.批处理调度
3.硬多租户

K8S是容器编排领域的事实标准,而Docker从诞生之日到今天都在容器中扮演着举足轻重的地位,也一直是K8S的默认容器引擎,然而在2020年12月,K8S社区决定着手移除仓库中Dockershim的相关代码

Dockershim是什么?

它是Docker的垫片,K8S中的结点代理Kubelet为了访问Docker提供的服务,会先访问Dockershim,Dockershim会将请求转发给管理容器的Docker服务

移除的原因

  • K8S引入容器运行时接口(CRI) 隔离不同容器运行时的实现机制,容器编排系统不应该依赖于某个具体的运行时隔离
  • Docker没有支持也不打算支持K8S中的CRI接口,需要K8S社区在仓库中维护Dockershim

从可扩展性的角度看问题

K8S通过引入新的容器运行时接口将容器管理与具体的运行时解耦,不再依赖某个具体的运行时实现,K8S通过下面的一系列接口为不同模块提供了扩展性

K8S在较早期的版本中引入了CRD CNI CRI CSI等接口,而CRI是1.5版本引入的新接口,Kubelet可以通过这个接口使用各种各样的容器运行时,其实CRI的发布就意味着K8S一定会将Dockershim的代码从仓库中移除。

CRI是一系列用于管理容器运行时和镜像的GRPC接口,我们能在它的定义中找到RuntimeService和ImageService两个服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
service RuntimeService {  //Runtime的grpc接口 
rpc Version(VersionRequest) returns (VersionResponse) {}
rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}
rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}
rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {}
rpc ReopenContainerLog(ReopenContainerLogRequest) returns (ReopenContainerLogResponse) {}
...
}
1
2
3
4
5
6
7
service ImageService { //镜像的grpc接口 
rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {}
rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {}
rpc PullImage(PullImageRequest) returns (PullImageResponse) {}
rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {}
rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse) {}
}

而这些接口都是容器运行时需要暴露给Kubelet的接口

Kubernetes作为松散的开源社区,每个成员都只会在开源社区上花费有限时间,所以既然Docker社区没有打算支持K8s的CRI接口,维护Dockershim又需要很多精力,所以K8S会移除对Dockershim的支持

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

在开发中常常会碰到很多JSON类型的数据进行交互,而其中有很多JSON数据你是不能确定它的字段和结构的,而Go语言是一门静态强类型的语言,在进行JSON解析的时候必须要确定字段的类型,定义出对应的结构体,然后再进行Unmarshal,那这二者之间的冲突我们该如何解决呢?

什么是JSON

  • json是JavaScript Object Notation(JavaScript对象表示法)
  • json是轻量级的文本数据交换格式
  • json独立于语言
  • json具有自我描述性,更容易理解
  • json使用js语法来描述数据对象,但是json仍然独立于语言和平台,json解析器和json库支持许多不同的编程语言

json是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成,之所以json这么流行,是因为json的结构和多级结构体(对象)刚好能对应上,而前后端交互的时候后端通常会返回给前端一个多级的结构体,于是json慢慢开始流行了,且json是跨语言和跨平台的,自身也足够轻量级。
json的标准格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
一个标准的json数据
//每个key对应的是一个value
{
“k1": 1,
"k2": 2 //注意结尾的这个不能有逗号
}



json字符串
{
"k1": "1",
"k2": "2"
}


json数组
{
“k1”: [1,2],
“k2”: [3,4]
}


json对象
{
“k1”: {“1”: “haihai”},
“k2”: {“2”:”haihahai”}
}


json对象数组
{
“k1”: [
{“k11”: “hellohello”},
{“k12”: “badbad”}
]
}



json数组对象
{
“k2”: {
“hello”: [1,2,3]
}
}

所有的JSON数据都是由上述几种JSON数据组合而成

如何在Go中解析不确定的JSON数据

通过测试、看文档的方式去确定对应的JSON数据,然后构造对应的结构体

1
2
3
4
5
6
7
8
// 请求其他服务   
jsonStr := xxx

var data interface{}

err := json.Unmarshal([]byte(jsonStr),&data)

fmt.Println(data)

比如可以先拿一个interface{}类型来接住JSON数据,然后看这个interface{}的值,来确定这个JSON数据哪些字段是string 哪些是object 哪些是int float等等
当然这也不是完全适用的,比如下面这种情况,有一个字段如下
type : []
能看出来type是一个切片类型的值,但是具体的类型你并不知道,可能是[]int 也有可能是[]string []float等等

map[string] interface{}

这个类型是map键值对,值可以是任意类型,因为在go中任意类型都实现了空接口interface{},而json数据也是key value的键值对,所以map[string] interface{}天然支持解析json类型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
jsonStr := xxx
var data map[string]interface{}
err := json.Unmarshal([]byte(jsonStr),&data)

// 你想取的字段
fieldValue := data["field"]

// 类型断言
if value,ok := data["field"].(float64);ok {

} else if vluae,ok := data["field"].(int64); ok {

}

理论上所有的合法的JSON数据都可以被反序列化到map[string]interface{}中
但是实际应用中 可能会出现一些无法被map[string]interface{}解析的JSON数据
  • JSON 数据中包含了多层嵌套的数据结构。在这种情况下,如果没有使用递归或者其他方式对嵌套数据进行处理,可能会导致反序列化失败。
  • JSON 数据中包含了数组类型,但是数组元素类型不一致或者无法转换成相应的类型。在这种情况下,可能需要手动处理数组元素或者使用其他数据类型来保存数组数据。
  • JSON 数据中包含了自定义数据类型或者复杂的数据结构,无法使用 map[string]interface{} 类型来反序列化。在这种情况下,需要定义相应的结构体或者使用其他适合的数据类型来反序列化。

第三方库

除了encoding/json之外,还有很多第三方库可以用来解析不确定的JSON数据,例如gjson和jsonparser,这些库通常提供了更加灵活和高效的JSON解析方式,可以根据具体的需求选择合适的库来使用

json.RawMessage与json.Number

  • json.RawMessage 是一个非常高效的数据类型,因为她不需要进行任何解析和类型转换,直接保存了未经处理的原始JSON数据,在反序列化的时候只需要将json.RawMessage转化为对应的数据类型即可,无需重新解析JSON数据
  • json.Number 表示JSON中的数字类型,可以用来保存任意精度的数字。这个数字可以特别大,可能会无法用Go中的整数或者浮点数来表示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package main

import (
"encoding/json"
"fmt"
)

func main() {
jsonData := []byte(`{
"id": 12345,
"name": "John Doe",
"age": 30,
"score": 95.5,
"is_student": true,
"tags": ["tag1", "tag2", "tag3"],
"extra": {
"field1": "value1",
"field2": 123
}
}`)

var m map[string]json.RawMessage
err := json.Unmarshal(jsonData, &m)
if err != nil {
panic(err)
}

var id int
err = json.Unmarshal(m["id"], &id)
if err != nil {
panic(err)
}
fmt.Printf("id: %d\n", id)

var name string
err = json.Unmarshal(m["name"], &name)
if err != nil {
panic(err)
}
fmt.Printf("name: %s\n", name)

var age int
err = json.Unmarshal(m["age"], &age)
if err != nil {
panic(err)
}
fmt.Printf("age: %d\n", age)

var score float64
err = json.Unmarshal(m["score"], &score)
if err != nil {
panic(err)
}
fmt.Printf("score: %f\n", score)

var isStudent bool
err = json.Unmarshal(m["is_student"], &isStudent)
if err != nil {
panic(err)
}
fmt.Printf("is_student: %v\n", isStudent)

var tags []string
err = json.Unmarshal(m["tags"], &tags)
if err != nil {
panic(err)
}
fmt.Printf("tags: %v\n", tags)

var extra map[string]json.RawMessage
err = json.Unmarshal(m["extra"], &extra)
if err != nil {
panic(err)
}
var field1 string
err = json.Unmarshal(extra["field1"], &field1)
if err != nil {
panic(err)
}
fmt.Printf("extra.field1: %s\n", field1)

var field2 int
err = json.Unmarshal(extra["field2"], &field2)
if err != nil {
panic(err)
}
fmt.Printf("extra.field2: %d\n", field2)
}

// 不确定的类型
data := make(map[string]interface{})
if err := json.Unmarshal(rawData, &data); err != nil {
log.Fatal(err)
}

if value, ok := data["age"].(float64); ok {
// 处理年龄为浮点数的情况
} else if value, ok := data["age"].(int); ok {
// 处理年龄为整数的情况
} else {
// 处理年龄为其他类型或不存在的情况
}

需要注意的是:类型断言的底层为反射,因为在运行时需要判断一个接口值的具体类型,而这个类型是在编译时无法确定的,需要在运行时动态地获取。效率比正常的代码低一到两个数量级,而且需要消耗额外的时间和内存

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

在Go 1.18开始提供了Fuzzing能力的支持,testing包在我们常见的T B类型之外新增了F的类型,用于支持模糊测试

Fuzzing Test

日常测试代码的时候我们经常使用table driven test的方式来构造一组输入和预期的结果,之后调用我们的待测函数,检查结果是否和我们的预期匹配,也就是我们平常说的Mock数据

这就引出了一个问题,这个table要多大呢?
通常大家都只是写一个【正常】的case 一个【异常】的case
但是这些样例其实是不够的,比如一些异常值、corner case,可能无法处理,或者可能有程序挂掉、安全问题等等

而fuzzing test的作用就是帮我们自动生成输入数据,以下是维基百科对于fuzzing test的定义

Fuzzing is a technique where you automagically generate input values for your functions to find bugs

模糊测试能够【持续】、【自动】地生成一系列【半随机】的数据作为待测函数的输入,来找到程序里隐藏的bug,对于边界case能够很好的验证。模糊测试中的输入不是由人工指定的,而是自动生成的随机数据,所以可以规避掉人工主观判断造出来的数据。

模糊测试通常可以不依赖于开发测试人员定义好的数据集,取而代之的则是一组通过数据构造引擎自行构造的一系列随机数据。模糊测试会讲这些数据作为输入提供给待测程序,并且监测程序是否出现panic、断言失败、无限循环,或者其他的异常情况

这些通过数据构造引擎生成的数据被称为语料(corpus),另外模糊测试其实也是一种持续测试的手段,因为如果不限制执行的次数或者执行的最大时间,它就会一直不停的执行下去

Go模糊测试

让我们来看看一个Golang实现的模糊测试长什么样

签名部分:从常见的func TestXxx(t *testing.T) 变成了func FuzzXxx(f *testing.F)

seed corpus:一组用户提供的语料,fuzzing引擎会使用这个语料来生成随机数据。其实就是一个样本,之后引擎就知道要生成什么类型的随机数据了

Fuzzing arguments: 接受*testing.t和想要随机生成的数据类型

模糊测试的要求

  • 模糊测试必须是一个名称类似于FuzzXxx的函数,仅接受一个*testing.F参数,无返回值
  • 模糊测试必须在*_test.go中运行
  • Fuzz target(模糊目标)必须是对(*testing.F).Fuzz的方法调用,参数是一个函数,并且这个函数的第一个参数是*tesing.T,然后是模糊参数(fuzzing argument),没有返回值
  • 一个模糊测试中必须只有一个模糊目标
  • 所有种子语料库(seed corpus)必须具有与模糊参数相同的类型,顺序相同
  • 模糊参数只能是以下的类型
1
2
3
4
5
string, []byte
int, int8, int16, int32/rune, int64
uint, uint8/byte, uint16, uint32, uint64
float32, float64
bool

需要注意的一点是,在Go执行的过程中,多个fuzzing target是并行来处理的,底层会有多个worker,调度的顺序也不一定,所以不能做持久化,也不能依赖一些全局状态,不要尝试改变入参

运行模糊测试

我们依然可以使用go test命令来跑模糊测试,只是需要加上一个-fuzz=FuzzTestName的选项。同时这个包下所有其他类型的test都会优先于模糊测试执行,毕竟比较耗费资源,随机数据生成是有样本的。

执行结果如下

1
2
3
4
5
6
7
8
9
~ go test -fuzz FuzzFoo
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)
fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)
fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210)
fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)
PASS
ok foo 12.692s

Fuzzing test的局限性,在单元测试中因为测试输入是固定的,所以可以和把得到的结果和预期结果进行比较来判断执行结果是否与预期相符合。

但是在使用fuzzing的时候,我们无法预测输出结果是什么,因为测试的输入除了我们代码只能给指定的用例之外,还有fuzzing随机生成的输入,所以我们无法提前知道预期结果是什么

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

手搓快排

需要注意的点有两个
1.循环里两数交换的条件为 i < j 意味着左指针还在右指针的左侧 即两者还没相遇
2.递归的时候一个是j  一个是j+1 把数组一分为二进行递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
nums := []int{3, 1, 2, 4, 6, 5}
quickSort(nums, 0, len(nums)-1)
fmt.Println(nums)
}

func quickSort(nums []int, l, r int) {
if l >= r {
return
}
i, j, mid := l-1, r+1, nums[(l+r)/2]
for i < j {
i++
for nums[i] < mid {
i++
}
j--
for nums[j] > mid {
j--
}
if i < j {
nums[i], nums[j] = nums[j], nums[i]
}
}
quickSort(nums, l, j)
quickSort(nums, j+1, r)
}

手搓堆排

需要注意的点:
1.heapify用来递归做调整 其中heapSize表示堆大小 i表示要操作的节点下标 left 和 right 的值分别是 2_i+1 与 2_i+2
2.buildHeap和heapify都传参都是i,n,而堆排的时候是维护一个当前大小的堆,所以是0,i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func main() {
nums := []int{3, 1, 2, 4, 6, 5}
heapSort(nums)
fmt.Println(nums)
}

func buildHeap(nums []int, heapSize int) {
for i := heapSize / 2; i >= 0; i-- {
heapify(nums, i, heapSize)
}
}

func heapSort(nums []int) {
heapSize := len(nums)
buildHeap(nums, heapSize)
for i := heapSize - 1; i >= 0; i-- {
nums[i], nums[0] = nums[0], nums[i]
heapify(nums, 0, i)
}
}

func heapify(nums []int, i, heapSize int) {
largest := i
left, right := 2*i+1, 2*i+2
if left < heapSize && nums[left] > nums[largest] {
largest = left
}
if right < heapSize && nums[right] > nums[largest] {
largest = right
}
if largest != i {
nums[largest], nums[i] = nums[i], nums[largest]
heapify(nums, largest, heapSize)
}
}

实现一个生产者-消费者模型

要注意的就是
通过select来实现一有消息就读,如果读完就退出
那怎么判断是否读完呢?这里就需要生产者主动关闭channel了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func main() {
ch := make(chan int,10)
go Processor(ch)
go Consumer(ch)
//睡一会
time.Sleep(15 * time.Second)
}

func Processor(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
// 模拟发时间的时延 这里可有可无
//time.Sleep(1 * time.Second)
fmt.Println("process:", i)
}
close(ch)
}

func Consumer(ch chan int) {
for {
select {
// 有数据就读出来 关闭了就退出
case i, ok := <-ch:
if ok {
fmt.Println("consumer:", i)
} else {
fmt.Println("over!")
return
}
}
}
}

实现两个goroutine打印奇偶数

需要注意的点是
关闭通道的时候需要异步进行,也就是异步Wait(),然后立马关掉ch
同步的话会有一个问题就是Even和Odd都往这个ch中写东西了,但是没有人去消费它,所以我们只能选择手动关闭。但是等待for range完毕,然后同步关闭的话会导致ch阻塞(因为for range不是消费)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func Even(wg *sync.WaitGroup, ch chan int) {
defer wg.Done()
for i := 0; i <= 100; i += 2 {
ch <- i
}
}

func Odd(wg *sync.WaitGroup, ch chan int) {
defer wg.Done()
for i := 1; i <= 100; i += 2 {
ch <- i
}
}

func main() {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(2)
go Even(&wg, ch)
go Odd(&wg, ch)
go func() {
wg.Wait()
close(ch)
}()
for v := range ch {
fmt.Println("number:", v)
}
}

实现两个goroutine交替打印奇偶数

需要注意的点:
通道必须得是有缓冲的,不然在main goroutine中往里面<-1就会阻塞住,下面的go协程就不会执行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func Even(ch chan int) {
for i := 0; i <= 100; i += 2 {
<-ch
fmt.Println("number:", i)
ch <- 1
}
}

func Odd(ch chan int) {
for i := 1; i <= 100; i += 2 {
<-ch
fmt.Println("number:", i)
ch <- 1
}
}

func main() {
ch := make(chan int, 1)
ch <- 1
go Even(ch)
go Odd(ch)
time.Sleep(time.Second)
}

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

go.sum是Go语言管理包管理 go mod而使用的一种锁文件,用于记录Go项目中所有依赖包的路径和哈希值。每一行记录了一个依赖项的信息,包括依赖项的模块路径、版本、哈希值等等。例如:

1
2
3
4
github.com/pkg/errors v0.8.1 h1:OIzxFfzpYLMvPCkkN+UD9dJ9yRuoxzZbDvI8Du5OJ+E=
github.com/pkg/errors v0.8.1/go.mod h1:9JFJoAoLZpNq3W4x/+xGw15jqJ7VHvq3u/L7V9XbTcg=
golang.org/x/crypto v0.0.0-20210322153248-947e6a75a262 h1:2Q1cGh1Zpq/NWDTzDxKk/gZN+Yyo0F8wgJOu09Kml9Q=
golang.org/x/crypto v0.0.0-20210322153248-947e6a75a262/go.mod h1:XTbTeuV6yl+B2H/UsM6UStw6z/5PhRlQIyH9/pXli/Y=

go.sum文件的作用在于记录各个依赖项的版本和哈希值,用于验证项目的依赖关系是否发生变化。
当使用go mod安装依赖包的时候,会根据go.mod文件中指定的版本号下载相应的依赖包,并计算依赖包的哈希值,将这些信息记录到go.sum中,当再次构建项目的时候,go.mod会检查go.sum文件,确保依赖项的哈希值和之前记录的值一样,以此来保证项目的构建过程是可重现的
go.sum文件的重要性在于保证Go项目的依赖关系的可靠性和安全性,避免因为依赖包版本不一样导致的不可预期的问题

K8S概述

什么是K8S?

Kubernetes是一个可移植、可扩展的开源平台,用于管理容器化的工作负载和服务,可促进声明式配置和自动化。名字源于希腊语,意味”舵手”

k8s出现的必要性
image.png
按照部署的方式来进行划分,我们可以简单的划分成四个时代

传统部署时代
如图一一样,APP部署在OS上,OS跑在硬件上。这会导致一个问题,如果在同一台机器上运行多个应用程序,如果某一个APP占用了机器的大部分资源,那么就会影响到其他APP,造成性能抖动甚至饿死的严重后果

虚拟化部署时代
为了解决上述问题,虚拟化技术被引进了,虚拟化技术允许你在单个物理服务器的CPU上运行多台虚拟机(VM)。虚拟化能使应用程序在不通过的VM之间被彼此隔离,且能提供一定程度的安全性,因为一个应用程序的信息不能被另一应用程序随意访问。
虚拟化技术能够更好地利用物理服务器的资源,并且可以更加轻松地添加、更新应用程序,因此具有更强的可扩缩性,以及降低硬件成本等等的好处

容器部署时代
容器类似于VM,但是相比于VMWare生成的那种VM,容器更加轻量级,并且与VM类似,每个容器都具有自己的文件系统、CPU、内存、进程空间等等,容器之间可以共享OS。容器和基础架构是分离的,因此可以跨云和OS发行版进行移植

容器的优势

  • 敏捷应用程序的创建和部署:相比于VM镜像更加简便,提高了容器镜像创建的简便性和效率
  • 持续开发、集成和部署:基于镜像不可变性,通过快速简单的回滚,提供可靠且频繁的容器镜像构建和部署
  • 关注开发和运维的分离:在构建、发布时创建镜像,而不是部署的时候,从而将应用与基础架构进行分离
  • 可观察性:不仅可以显示OS级别的信息和指标,还能够显示应用程序的运行状态、其他指标信号
  • 跨开发、测试和生产的环境一致性:在开发物理机上也可以和在云中运行一样的应用程序
  • 跨云和操作系统发行版本的可移植性:可在任意环境运行
  • 松耦合、分布式、弹性:一个大的单体被分解成较小的服务,并且可以动态的管理和部署,而不是在一台大型单机上整体运行
  • 资源隔离:可预测的应用程序性能
  • 资源利用:高效率和高密度

K8S能做什么
上文我们说到,容器是打包和运行应用程序的很好的一种方式,而在生产环境中,我们需要对很多容器(容器的集群)进行管理,保证服务不会出故障、确保服务的可靠性,稳定性。
例如一个容器发生故障,则需要把这个容器下掉,新增一个运行该服务的容器,再把它上线。而K8S就是帮助我们实现这个过程,简化操作流程的工具

K8S的功能

  • 服务发现与负载均衡:K8S可以使用DNS、或者自己的IP来暴露容器,如果进入容器的流量很大,K8S通过Kube-Proxy来实现的负载均衡,可支持的负载均衡算法有(轮询、最少链接、IP哈希),默认为轮询,通过负载均衡来分配网络流量,从而使部署更加稳定
  • 存储编排:K8S允许自动挂载你选择的存储系统,例如本地存储、公共云提供商等
  • 自动部署和回滚:可以使用K8S描述已部署容器的所需状态,并控制状态更改的速率
  • 自动完成装箱计算:当K8S拥有许多节点组成一个集群之后,在这个集群上运行容器化的任务时,可以告知K8S每个容器需要多少资源(CPU 内存等),K8S可以将这些容器按实际情况调度到节点上,以最佳方式利用分配的资源
  • 自我修复:K8S将重新启动失败的容器、替换容器、杀死不响应用户定义的健康检查的容器,并且在准备好服务之前不将其通告给客户端
  • 密钥与配置管理:K8S允许你存储和管理敏感信息,例如密码、OAuth令牌和SSH密钥,可以在不重建容器镜像的情况下部署和更新密钥以及应用程序配置,也无需在堆栈配置中暴露密钥

K8S组件


上述是K8S运行的架构流程图,我们可以看得出来,一个K8S集群主要由两部分组成,分别是Control Plane控制平面,老版本也叫做Master;Node工作节点,老版本也叫做Worker Node

我们将一组工作机器称为节点,节点会运行容器化应用程序,每个集群至少有一个工作节点。工作节点会托管Pod,控制平面管理集群中的工作节点与Pod

整体的一个典型的工作流程如下:

  1. 用户使用K8S API与API服务器交互,发布应用程序的描述(如Pod Service等)
  2. 调度器根据应用程序的要求选择合适的节点,并将任务指派给节点上的Kubelet
  3. Kubelet根据指令在节点上创建和管理容器,确保它们的状态与期望的状态

控制平面组件

控制平面组件会为集群做出全体决策,比如资源的调度,以及检测和响应集群事件。可以理解成K8S集群的大脑,负责管理和控制整个集群的行为。
控制平面组件可以在集群中的任何节点上运行,通常来说为了简单起见,只会在同一个计算机上启动所有的控制平面组件,并且不会在这台机器上运行任何容器

**kube-apiserver **
API服务器是K8S控制平面的组件之一,提供了与K8S集群通信的接口,允许用户和其他组件通过HTTP RESTful API与K8S进行交互,这个组件负责公开K8S API,负责处理接受请求的工作,验证并配置API对象的数据,这些对象包括pods services replicationcontrollers等,为REST操作提供服务,可以将它简单理解为K8S控制平面的前端,其他所有组件都通过该前端进行交互。
同时,API SERVER还负责验证请求的身份和权限,通过Token UserName/Password TLS证书等进行确认和交互,验证用户或者组件的身份,一旦用户验证成功,API Server会使用访问控制策略进行角色授权
并且它负责资源管理,维护一组持久化存储(etcd)来存储资源的配置、状态和元数据
它还负责记录集群中的事件和日志信息,当资源对象发生变化或者出现错误的时候,它会生成事件并将其发送给订阅者

etcd
它是一个分布式的一致且高可用的键值存储,用作k8s所有集群数据的后台数据库,存储集群的配置数据、元数据和状态信息的可靠持久化存储。etcd提供了高可用性、一致性和分布式的特性,为K8S的控制平面组件提供了一个共享的数据存储,API Server、kube scheduler 和 CM 等组件通过使用etcd来存储和检索集群的配置信息、资源对象的状态以及各种元数据,这些信息包括Pod Service Namespace PersistentVolume等的定义和状态

etcd的一些关键功能

  • 分布式存储:etcd使用Raft一致性算法来实现数据的分布式存储,它将数据分片并复制到集群中的多个节点上,确保数据的可用性和容错性,这意味着即使一些节点失效,集群应该可以继续正常工作
  • 一致性:etcd的Raft算法保证了数据的一致性,所有的写操作都需要经过多数节点的确认,确保数据的正确复制和同步,这样可以避免数据损坏和不一致的情况发生
  • 高可用性:etcd具有高可用性,通过在集群中的多个节点上复制数据,提供了容错能力
  • 快速读写:etcd通过在内存中保持数据的副本,实现了快速的读写操作,使用B+树作为底层数据结构,提供高效的索引和检索功能
  • 监控和故障恢复:etcd提供了一些监控和故障恢复机制,可以监测节点的状态和健康状况。当节点发生故障或变得不可用时,集群可以自动进行重新选举,选择新的领导者节点来接管工作

**kube-scheduler **
负责监视新创建的、未指定运行节点(node)的Pods,并选择节点来让Pod运行在该节点上,以实现负载均衡、资源利用率最大化和高可用性
调度决策考虑的因素包括:单个Pod及Pods集合的资源需求、软硬件及策略约束、亲和性及反亲和性规范、数据位置、工作负载间的干扰及最后时限
scheduler的主要功能如下

  1. 资源调度:scheduler根据容器的资源需求(如CPU 内存)和节点的资源利用率,决定将工作负载调度到哪个节点上运行
  2. 节点选择:scheduler根据工作负载的要求,选择适合的节点进行调度,通过筛选和评分机制来选择节点,同时scheduler还考虑亲和性规则,以便将相关的工作负载调度到同一节点上,提高应用程序的性能和效率
  3. 拓展性和灵活性:scheduler具有可插拔的架构,允许用户根据自己的需求自定义和拓展调度算法,用户可以实现自定义的调度策略,通过调整评分函数和优先级规则来满足特定的业务需求
  4. 调度器扩展:k8s提供了灵活的调度器扩展机制,允许用户添加额外的带哦赌气,这些调度器可以根据特定的需求和场景来实现自定义的调度逻辑

工作流程如下:

  1. 用户创建或者更新一个工作负载的描述,例如Deployment StatefulSet等
  2. 当新的工作负载被提交时,Scheduler接收到这个事件,并根据工作负载的需求和集群状态进行调度决策
  3. Scheduler遍历集群中的可用节点,评估每个节点的适合程度,并为每个节点打分
  4. Schedulergenuine打分结果选择最合适的节点,并将工作负载的调度决策通知给相应的节点的Kubelet
  5. Kubelet在选择的节点上启动和管理容器,并把容器的状态报告给控制平面

kube-controller-manager(cm)
负责运行控制器进程,理论上来说每个控制器都是一个独立的进程,但是为了降低复杂性,它们都被编译到一个可执行文件中,并且在同一个进程中运行。
这些控制器包括

  • 节点控制器(Node Controller):负责在节点出现故障时进行通知和响应
  • 任务控制器(Job Controller):监测代表一次性任务的JOB对象,然后创建Pods来运行这些任务直至完成
  • 端点分片控制器(EndpointSlice controller):填充端点分片对象,以提供给Service和Pod之间的链接
  • 服务账号控制器(ServiceAccount controller):为新的命名空间创建默认的服务账号

工作流程如下:

  1. CM启动时,它的各个控制器开始监视集群中的特定资源对象
  2. 控制器通过API Server获取资源对象的当前状态,并将其与所需要的期望状态进行对比
  3. 如果二者不一致,那么控制器会触发相应的操作来使它们保持一致,这可能包括创建、更新或者删除资源对象
  4. 控制器通过API Server发出相应的操作请求,将更改应用于集群中的资源对象
  5. 控制器不断循环执行,以确保资源对象的状态和行为与期望状态保持一致

**cloud-controller-manager(ccm) **
嵌入了特定的云平台控制逻辑,云控制器管理器允许将你的集群连接到云提供商的API之上,并将与该云平台交互的组件同你的集群交互的组件分离开来。
与cm类似,ccm将若干逻辑上独立的控制回路组合到同一个可执行文件中,供你以同一进程的方式运行。你可以对其执行水平扩容以提升性能或者增强容错能力。

  • 节点控制器(Node Controller): 用于在节点终止响应后检察云提供商以确定节点是否已被删除
  • 路由控制器(Route Controller):用于在底层云基础架构中设置路由
  • 服务控制器(Service Controller):用于创建、更新和删除云提供商负载均衡器

工作流程如下:

  1. CCM组件在启动的时候与云平台的API进行认证和链接,并监视云资源对象的状态
  2. CCM的控制器通过与云平台的API进行交互,获取云平台资源对象的状态,并将其与K8S中的对象进行比较
  3. 如果二者状态不一致,CCM的控制器会触发相应的操作,通过与云平台的API发出请求,将更改应用于云资源

工作节点组件

节点组件会在每个节点上运行,负责维护运行的pod并提供K8S运行环境

**kubelet **
kubelet会在集群中每个节点(node)上运行,它保证容器(containers)都运行在Pod中。
kubelet接收一组通过各类机制提供给它的PodSpecs,确保这些PodSpecs中描述的容器处于运行状态且健康,kubelet不会管理不是由k8s创建的容器,kubelet是k8s集群中的每个node上的主要组件之一,负责管理节点上的容器化工作负载,与控制平面交互,确保集群中的容器正确运行,并且维护节点的健康状态

主要功能:

  • 容器管理:kubelet负责在节点上创建、启动、停止和销毁容器,它通过与容器运行时(Docker Containerd)进行交互,执行容器的生命周期管理操作
  • 资源管理:Kubelet监控节点的资源使用情况,并根据预定义的资源配额和调度策略来分配资源给容器,它会定期向K8S控制平面报告节点上的资源状态
  • 健康检查:Kubelet定期对节点上的容器进行健康检查,包括容器的存活状态、资源利用率等,如果容器故障或者资源不足,Kubelet会尝试重启、恢复或迁移容器
  • 节点注册:Kubelet在节点启动时将自身注册到K8s控制平面,使控制平面能够管理和监控该节点上的容器化工作负载
  • 网络管理:Kubelet配置节点上的网络参数,包括容器网络和节点网络。它为容器分配IP地址,并配置容器之间和容器与外部的网络通信
  • 卷管理:Kubelet负责挂在和卸载容器中使用的持久卷,它与卷插件交互,使容器能访问和使用持久化存储
  • 日志和监控:Kubelet收集和管理节点上容器的日志和监控数据,它可以将日志发送到集中式日志系统,并提供容器的运行指标和事件信息

工作流程如下:

  1. Kubelet监听来自K8S控制平面的指令和命令
  2. K8S获取需要在节点上运行的pod列表,并根据指定的pod规范创建和管理容器
  3. 对于每个容器,Kubelet通过容器运行时(如Docker)来启动和停止容器,并监控其状态
  4. Kubelet定期向K8s控制平面报告节点的资源使用情况和容器状态
  5. Kubelet定期凑够控制平面获取Pod的更新和变更,并相应地执行容器的生命周期管理操作

kube-proxy
kube-proxy是集群中每个节点(node)上所运行的网络代理,实现k8s概念的一部分,它维护节点上的一些网络规则,这些网络规则会允许从集群内部或外部的网络会话与Pod进行网络通信。运行在每个节点上,并与K8S控制平面和节点上的网络组件进行交互,以实现服务的可访问性和网络流量的转发
如果操作系统提供了可用的数据包过滤层,则kube-proxy会通过它来实现网络规则,否则,kube-proxy只做流量转发

主要功能:

  • 服务代理:Kube-proxy监听K8S控制平面中的服务和短空定义,并为它们创建对应的网络代理规则,这些规则通常基于IP Tables或者IPVS,根据服务的选择器和端口信息,将流量转发到相应的后端pod
  • 负载均衡:Kube-proxy实现了负载均衡功能,将来自集群内部和外部的网络请求均匀地分发到后端的Pod。它可以基于轮询、随机选择或最少连接等算法来进行负载均衡
  • 服务发现:Kube-proxy监听K8S控制平面中国的服务和端口定义的变化,当服务的Pod副本数量发生变化、服务的标签或者端口信息发生变更时,Kube-proxy会相应地更新代理规则,以确保服务的访问正常
  • 节点故障处理:Kube-proxy监测节点的健康状态,并在节点故障或网络中断的情况下自动更新代理规则,它会将流量重新路由到其他健康节点上的Pod,以保证服务的高可用性
  • 透明代理:Kube-proxy支持透明代理模式,可以在不修改应用程序代码的情况下,将应用程序流量转发到后端Pod。这种方式对应用程序是透明的,它们无需感知代理的存在

Kube-proxy的工作流程如下:

  1. Kube-proxy从K8S控制平面获取服务和端口定义,并为每个服务创建代理规则
  2. 当有新的服务或端口定义添加到集群中,或者现有的定义发生变化时,Kube-proxy监测到变化并相应地更新代理规则
  3. Kube-proxy监听来自服务暴露的端口上的网络流量
  4. 根据代理规则,Kube-proxy将流量转发到后端Pod上的容器,实现负载均衡和服务发现的功能

Container Runtime 容器运行时
容器运行环境是负责运行容器的软件,K8S支持许多容器运行环境,例如containerd CRI-O 以及K8S CRI的其他任何实现
Container Runtime(容器运行时)是 Kubernetes 中负责管理和运行容器的核心组件。它提供了创建、启动、停止和销毁容器的功能,以及管理容器的资源和隔离性。

Kubernetes 支持多种容器运行时,其中最常用的是 Docker 和 Containerd。下面将详细介绍容器运行时的工作原理和功能:

  1. 容器生命周期管理:容器运行时负责与容器生命周期相关的操作。它可以根据容器镜像创建并启动容器,监控容器的运行状态,并在需要时停止或销毁容器。
  2. 容器隔离性:容器运行时使用 Linux 内核的命名空间和控制组(cgroup)等特性,为容器提供隔离的运行环境。每个容器都具有独立的文件系统、网络栈、进程空间和资源限制,从而实现容器之间的隔离和安全性。
  3. 容器网络:容器运行时负责设置和管理容器的网络。它为每个容器分配唯一的 IP 地址,并处理容器之间的网络通信。容器运行时还可以与网络插件协同工作,以实现更高级的网络功能,如跨主机的容器通信和负载均衡。
  4. 容器存储:容器运行时管理容器的存储。它可以为容器提供本地存储卷或挂载外部存储卷,使容器能够持久化存储和访问数据。
  5. 容器镜像管理:容器运行时负责下载、管理和缓存容器镜像。它可以从容器镜像仓库中拉取镜像,并将其存储在本地节点上,以便在需要时快速创建容器。
  6. 容器资源管理:容器运行时与 Kubernetes 的调度器和资源管理器交互,以确保容器在节点上得到适当的资源分配。它可以根据容器的资源需求和节点的可用资源进行调度和限制,以实现资源的公平分配和利用。

容器运行时在 Kubernetes 中的工作流程如下:

  1. Kubernetes 控制平面下发容器启动的指令,包括容器镜像、资源要求等信息。
  2. 容器运行时根据指令从容器镜像仓库拉取镜像,并创建容器的运行时环境。
  3. 容器运行时使用 Linux 命名空间和控制组等功能,为容器提供隔离的运行环境。
  4. 容器运行时启动容器中的应用程序,并监控容器的运行状态。
  5. 容器运行时与容器网络 插件协同工作,为容器分配 IP 地址,并处理容器之间的网络通信。
  6. 容器运行时根据 Kubernetes 控制平面的指令,停止或销毁容器。

总之,容器运行时是 Kubernetes 中关键的组件之一,它负责管理和运行容器,提供容器的隔离性、生命周期管理、网络和存储功能,与其他 Kubernetes 组件协同工作,实现容器化应用程序的高效运行和管理。

node、kubelet、pod和container之间的联系

Node:是K8S集群中的工作节点,也被称为主机或者服务器,每个Node提供容器运行的基础设施,并承载运行着的容器
Kubelet:是运行在每个Node上的K8S组件之一,是Node上的代理程序,Kubelet负责管理和运行Node上的容器,并与K8S控制平面交互
Pod:Pod是K8S的最小调度和部署单元,它是一个逻辑上相关的容器组,可以包含一个或者多个容器,Pod提供了一个抽象层,为容器提供共享的网络和存储资源,使容器之间可以进行通信和共享数据,Pod是在Node上进行调度和运行的
Container:容器是在Pod中运行的实际应用程序或服务,一个Pod可以包含一个或者多个容器,这些容器共享同一个网络命名空间和存储卷,容器被kubelet创建、启动、停止和销毁

通过这种方式,kubelet作为Node上的代理程序,负责与Kubernetes控制平面交互,并协调管理Node上的容器。Pod是在Node上调度和运行的最小单元,它可以包含一个或多个容器,这些容器共享同一个网络和存储环境。这种关系使得kubernetes可以以分布式和高可用的方式运行和管理容器化应用程序。

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

什么是消息队列

消息队列,我们一般会称为MQ(Message Queue),也就是说消息队列的本质就是一个队列,而队列是一种先进先出的数据结构,提供消息传递和消息排队模型,可以在分布式环境下提供应用解耦、弹性伸缩、流量削峰、异步通信、数据同步、微服务之间通信等功能,作为分布式系统架构中的一个重要组件,有着举足轻重的地位
我们会将要传输的数据、消息放在消息队列中
其中,往MQ里放东西的叫做生产者
从MQ里面取消息的叫做消费者

为什么要用消息队列

解耦

现在有一个系统A,A可以产生一个UserId
然后有系统B和系统C都需要这个UserId去做相关的操作

这样就会有一个问题,每当出现改动的时候,我都要改动整个系统,比如B不需要A的消息了,那么就要改A、B,新来的一个D服务,需要用到A的消息,那么又需要修改,整个系统的冗余度很高
引入消息队列之后:系统A将id写到消息队列中,BC服务从消息队列中拿数据

系统A只负责写数据,不关心数据的消费、处理,而BC服务只负责拿数据,即使BC服务挂了,也和系统A没关系,只和消息队列有关,这样就能做到多个服务之间的解耦

异步


如果是同步的话,A必须要等待B C D 处理完之后才能返回,整个时间比较久

如果是异步的话,A(比如注册服务)将消息写道消息队列之后就可以返回,之后再发给邮件服务和短信服务消费

限流、削峰

当请求来的时候,先把请求放在消息队列中,然后系统再根据自己能够处理的请求数去消息队列里面拿数据,这样即使每秒请求数很大,也不会把系统打崩

流量控制

消息队列通常有很多种方式来实现流量控制

1.配额控制:通过为每个生产者或者消费者分配配额,限制它们可以发送或者接受的消息数量。这可以保证消息队列中的资源不会被过度使用,并确保系统在高负载情况下的稳定性

2.窗口机制:当生产者将消息发送到消息队列的时候,消息队列会给每个生产和分配一个发送窗口,当消费者确认之后把对应的消息从窗口里面删除

3.缓冲区:消息队列把消息往缓冲区里丢,消费者从缓冲区里去取

4.速率限制:这个一般是在客户端实现的,可以实现生产者在每秒、每分钟生产多少条消息

如何保证消息不被重复消费

常见的消息队列都有确认机制(ACK机制),当消费者消费数据之后会给消息队列中间件发送一个确认消息,消息队列收到之后就会把这条消息从队列中删除。

当出现网络传输等故障,ACK没有传送到消息队列,导致消息队列不知道消费者已经消费过该消息了,再次将消息分发给其他的消费者

解决:

1.看场景,如果场景不需要幂等,那么可以不管,比如这条消息拿去插入数据库,重复插入主键相同的数据是会自动出错的,再比如做redis的set操作,也不需要管,多次set之后仍然是幂等的

2.准备第三方介质做消费记录,比如加个redis,给消息分配一个全局id,只要消费过该消息就将<id,message>写入redis。消费者消费之前先去redis中查有没有消费记录即可

如何保证消息的可靠性传输

消息队列丢数据主要有三个可能

1.生产者丢数据

2.消息队列组件丢数据

3.消费者丢数据

生产者:可以采用transaction机制,开启事务来发送消息,如果发送失败就回滚。但是生产中用的不多,因为会导致吞吐量的下降,一般都是用confirm机制,如果生产者成功把消息发送给队列,队列会回一个ack,否则回一个nack

消息队列:可以开启持久化,而且一般是集群部署的,有master和slave节点,一般都是同步复制,只有主节点和从节点都写入成功才返回ack给生产者

消费者:取消自动确认,自动确认后消息队列收到ACK会立马把消息从队列中删掉,而是手动确认(即处理后才回ACK)。

消息队列需要考虑的问题

高可用

消息队列肯定不是单机的,这样可用性和健壮性都非常差,所以项目中使用的消息队列都得集群或者分布式

数据问题

消息丢失:当消费者拿了数据还没使用的时候,服务就挂掉了,就会导致消息的丢失,一般会使用ACK应答机制,当消费者拿到消息发送确认ACK信号,消息队列才会把对应的消息删掉

消息堆积:消息堆积分为客户端堆积和服务端堆积
一般都会设置告警规则来通知开发者消息堆积的问题

如果是客户端消息堆积,那可以考虑扩大消费线程或者节点来解决, 针对于某些特殊场景,如果消息堆积已经影响到业务,并且堆积的消息可以跳过不消费,那么可以重置消费消息位置为最新位置开始消费,快速恢复业务。

如果是服务端消息堆积,考虑服务端宕机的情况,快速恢复之后重新可用

消费者取数据

两种方法

1.push 消息队列有新消息的时候主动叫消费者去拿,实时性强。如果消费者故障,服务端堆积消息。

2.pull 消费者不断的轮询消息队列,看看有没有新数据,如果有就消费,实时性弱。

消息队列的传输模式

点对点模型

用于消息生产者和消息消费者之间点到点的通信,消息生产者将消息发送到某个特定的消费者
特点:
1.每个消息只有一个消费者
2.发送者和接收者都没有时间依赖
3.接受者确认消息接受和处理成功

发布订阅模型

发布订阅模型支持向一个特定的消息主题产生消息,在这种模型下,发布和订阅者彼此不知道对方的存在,多个消费者可以获得消息,在发布者和订阅者之间存在时间依赖性发布者publish需要建立一个订阅subscription,以便消费者能够订阅。订阅者必须保持持续的活动状态接受消息

主题、订阅、消费者(组)之间的关系为 M:N:O

在这种情况下,订阅者未连接时,发布的消息将在订阅者重新连接的时候重新发布
特点:
1.每个消息可以有多个订阅者
2.客户端只有订阅之后才能收到消息
3.持久订阅和非持久订阅

持久订阅:订阅关系建立之后,消息就不会消失,不管订阅者是否在线
非持久订阅:订阅者为了接受消息,必须一直在线,当只有一个订阅者的时候等于点对点模式

pub-sub vs queue

发布订阅和队列模式是消息队列中的两种不同的消息模式

发布-订阅模式:发布者将消息发送到特定的主题(topic)上,订阅者可以选择订阅感兴趣的主题,从而接受与该主题相关的消息。在该模式中,消息被广播给所有订阅者,每个订阅者可以独立处理消息,订阅者之间不会相互干扰。发布-订阅模式通常用于广播消息或者通知

队列模式:消息发送到队列中,然后一个或者多个消费者从队列中收取并处理消息。在该模式中,每条消息只能被一个消费者接收和处理。如果有多个消费者,消息将被平均分配给它们。队列模式通常用于实现任务分配或者负载均衡等场景

市面上消息队列对比

记住两个最常见的MQ的对比就可以,硬盘MQ代表是Kafka 内存MQ代表是RabbitMQ

Kafka的优点是客户端支持多语言、使用pull模式,支持消息批量操作,支持replica机制,Zookeeper自动选举leader恢复能力,数据可靠,有容错容灾的能力,单机吞吐量为10W级,延迟毫秒,数据基于硬盘层面存储,多Client支持有序,不支持事务,但是可以通过LOW LEVEL API的方式保证消息只支持消费一次

RabbitMQ的优点是客户端支持多语言,多协议支持,不支持消息批量操作,有pull和push两种模式,使用的主从模式master/slave,master提供服务,slave做备份,数据可靠(因为有备份),单机吞吐量为万级别,消息延迟为微秒级,内存级别,可以主动开启持久化,支持集群和负载均衡,不保证多Client消息有序

pulsar kafka rabbitmq nsq的异同

1.pulsar 和 kafka基于发布订阅模式,而rabbitmq 和 nsq基于的是队列模式

2.pulsar和kafka都采用了持久化机制,以支持高吞吐量和高可靠性,而rabbitmq和nsq则将数据存储在内存中,以支持更低的延迟和更高的吞吐量

3.pulsar和kafka都支持 多租户和多数据中心部署,可以轻松地在多个数据中心或者云平台上进行扩展,而rabbitmq和nsq则更加适合单个数据中心的部署

4.pulsar和kafka都具有出色的可伸缩性和高可用性,处理数据单位是百万级别的;而rabbitmq和nsq则更加适合小规模的应用程序,具有更低的延迟和更高的性能

5.pulsar和kafka都提供了消息流的处理程序,使用户可以对消息进行实时分析和处理

总的来说:pulsar和kafka更适合处理大量消息和数据流,rabbitmq和nsq则更适合小规模应用程序,具有更低的延迟和更高的性能

Pulsar底层实现

Pulsar是Apache基金会的顶级项目,是云原生的分布式消息队列,集消息、存储、轻量化函数式计算为一体,采用计算与存储分离架构设计,支持多租户、持久化存储、多机房跨区域数据恢复机制,具有强一致性、高吞吐、低时延及高可扩展性等流数据存储特性

  • 云原生MQ
  • 单个Pulsar实例原生支持多集群,可跨机房完成消息复制
  • 支持超过一百万个topic
  • 支持多语言客户端
  • 主题多种订阅模式(独占、共享和故障转移)
  • 通过Book Keeper来实现持久化存储,保证消息传递
  • 分层式存储,可在数据陈旧时将数据从热存储卸载到冷存储中

消息

Messages有很多的部分组成, 下面是几个值得注意的

  • value/payload 消息的数据
  • properties 可选的属性,是一个key->value的键值对
  • producer name 生产者名称
  • publish time 发布时间戳
  • sequence id 在topic中 每个msg属于一个有序的队列 sequence id是它在序列中的次序

Pulsar和其他的MQ一样,会对消息的大小做出限制

这个限制通过broker.conf中的maxMessageSize 决定

不设置的话,默认为5MB

生产者

生产者是关联到topic的程序,它发布消息到Pulsar的broker上

发送模式

  • 异步发送:生产者发送消息之后会等待broker的确认,如果没有收到确认则认为是发送失败
  • 同步发送:会把消息放在阻塞队列中,然后立马返回,然后这个阻塞队列会往broker中发消息

主题访问模式

  • Shared(共享)  多个生产者可以发布一个主题,这是默认设置
  • Exclusive(独占) 一个主题只能由一个生产者发布,如果已经有生产者链接,其他生产者试图发布该主题将立即得到错误。如果”老”的生产者与broker发生网络分区,”老”生产者将被驱逐,”新”生产者将被选为下一个唯一的生产者
  • WaitForExclusive(独占等待) 如果已经有一个生产者连接,生产者的创建是未决的,直到生产者获得独占访问。成功成为排他性的生产者被视为领导者,因此,如果想实现leader选举方案,可以使用这种模式

压缩、批处理与分块
压缩:我们可以主动压缩生产者在传输期间发布的消息,Pulsar目前支持以下类型的压缩

  • LZ4
  • ZLIB
  • ZSTD
  • SNAPPY

批处理:如果批处理开启,producer将会积累一批消息,然后通过一次请求发送出去。批处理的大小取决于最大的消息数量及最大的延迟发布

分块:分块和批处理不能同时启用,要启用分块,必须提前禁用批处理。Chunking只支持持久化的主题

一个producer与一个订阅consumer的分块消息
当生产者向主题发送一批大的分块消息和普通的非分块消息时。将M1切成分块M1-C1、M1-C2、M1-C3。这个broker在其管理的ledger里面保存所有的三个块消息,然后以相同的顺序分发给消费者(独占/灾备)。消费者将在内存缓存所有的块消息,直到收到所有的消息快。将这些消息合并成为原始的消息M1,发送给处理进程

消费者

消费者通过订阅关联到主题,然后接受消息的程序

接收模式

消息可以通过同步或者异步的方式从broker接受

同步:同步接收将会阻塞,直到消息可用

异步:异步接收立刻返回future值,一旦新消息可用,它将即可完成

监听

客户端类库提供了它们对于consumer的监听实现,在这个接口中,一旦接受到新的消息,received方法将被调用

确认

消费者成功处理消息之后需要发送确认(ack)给broker,以让broker丢掉这条消息(否则将一直存储)。消息的确认可以逐个进行,也可以累积到一起。累计确认的时候,消费者只需要确认最后一条它收到的消息,所有之前的消息都认为被成功消费。累积确认不能用于shared模式,因为shared订阅为同一个订阅引入了多个消费者

主题

和其他的MQ一样,Pulsar中的topic是带有名称的通道,用来从producer到consumer传输消息,topic的名称是符合良好结构的URL

{persistent|non-persistent}://tenant/namespace/topic

  • peisistent|non-persistent 定义了topic的类型,Pulsar支持两种不同的topic:持久化和非持久化,默认是持久化类型,也就是会保存到硬盘上的类型
  • tenant 实例中topic的租户,tenant是Pulsar多租户的基本要素,可以被跨集群的传播
  • namespace topic的管理单元,与topic组的管理机制相关。大多数的topic配置在namespace层面生效,每个tenant可以有多个namespace
  • topic 主题的最后组成部分

Partitioned topics 分区主题

普通的主题只由单个broker提供服务,这限制了主题的最大吞吐量,分区主题是由多个broker处理的一种特殊类型的主题,因此允许更高的吞吐量
分区主题实际上实现为N个主题,N是分区的数量。当消息发布到分区主题的时候,每个消息都被路由到几个Broker中的一个。分区在broker之间的分布由Pulsar自动的进行处理

如上图,Topic有五个分区,划分在3个broker上,因为分区比broker多,前两个broker分别处理两个分区,而第三个broker只处理一个分区(Pulsar自动处理分区的分布)
此主题的消息将广播给两个消费者,路由模式决定将每个消息发布到哪个分区,而订阅模式决定将哪些消息发送到哪个消费者

路由模式

  • RoundRobinPartition message无key则轮询,有key则hash指定分区(默认模式)
  • SinglePartition message 无key 则producer将会随机选择一个分区,将所有的消息都发送给该分区。如果message有key,那么会hash指定分区
  • CustomParition 使用自定义消息路由实现

顺序保证

消息的顺序与路由模式和消息的key有关

  • Per-key-partition (按key分区) 具有相同key的所有消息将被顺序放置在同一个分区中
  • Per-producer (按照Producer) 来自同一生产者的所有消息都是有序的

哈希方案

HashingScheme是一个enum,表示在选择要为特定消息使用的分区时可用的标准哈希函数集

有两种类型的标准哈希函数可用:JavaStringHash和Murmur3_32Hash,生产者的默认哈希函数是Java,但是当生产者的客户端是多语言的时候,Java是没用的

持久/非持久化主题

默认情况下,Pulsar会保存所有没确认的消息到Book Keeper中,持久Topic消息会在broker重启或者consumer出问题的时候保存下来

Pulsar也支持非持久化Topic 这些Topic的消息只存在于内存中,不会存储到磁盘

因为Broker不会对消息进行持久化存储,当Producer将消息发送给Broker时,Broker可以立即将ack返回给Producer,所以非持久化的消息传递会比持久化的更快。相对的,当Broker因为一些原因宕机、重启后,非持久化的Topic消息都会消失,订阅者将无法收到这些消息。

Dead letter topic 死信主题

死信主题允许你在用户无法成功消费某些消息时使用新消息。在这种机制中,无法使用的消息存储在单独的主题中,成为死信主题。

死信主题依赖于消息的重新投递,由于确认超时或者否认确认,消息将被重新发送。如果要对消息使用否定确认,请确保在确认超时之前对齐进行否定确认。

Retry letter topic 重试主题

对于许多在线业务系统,由于业务逻辑处理中出现异常,消息会被重复消费。

若要配置重新消费失败消息的延迟时间,可以配置生产者将消息发送到业务主题和重试主题,并在消费者上启用自动重试。当在消费者上启用自动重试的时候,如果消息没有被消费,那么就会存储到重试主题中,在指定的延迟时间后,消费者会主动接受来自重试主题的失败消息

订阅模式

Pulsar支持exclusive (独占) failover(灾备) shared(共享) 和 key_shared(Key共享)四种消息订阅模式,示意图如下

独占模式
默认的消息订阅模式。只能有一个消费者消费消息

灾备模式
灾备模式下,一个topic也是只有单个consumer消费一个订阅关系的消息,但是在这个模式下,每个消费者会被排序,当前面的消费者无法连接上broker后,消息会由下一个消费者进行消费

共享模式
共享模式下,消息可被多个consumer同时消费,无法保证消费的顺序,消息通过roundrobin的方式投递到每一个消费者

key共享模式
按照key对消息进行投递,相同的key的消息会被投递到同一个consumer上,消费示意图如下

消息保留与过期
默认策略

  • 立即删除所有被消费者确认过的消息
  • 以backlog的形式,持久化保存所有未被确认的消息

两个特性

  • 消息保留可以让你保存consumer确认过的消息
  • 消息过期可以让你给未被确认的消息设置ttl

消息保留和过期是针对namesapce层面进行设置和管理的

消息去重

实现消息去重的一种方式是确保消息只生产一次,即生产者幂等,这种方式的缺点在于把消息去重的工作交给应用来做。
在pulsar中,broker支持配置开启消息去重,用户不需要主动在代码中保证Producer只生产一次,启动之后即使消息被多次发送到topic上,也只会被持久化到磁盘一次

原理:Producer对每一个发送的消息,都会采用递增的方式生成一个唯一的sequence ID,这个消息会放在message的元数据中传递给broker。

同时,broker也会维护一个pendingmessage队列,当broker返回发送成功ack之后,producer会将pendingmessage队列中的sequence id删除,标识producer任务这个消息生产成功。

broker会记录针对每个producer接受到的最大sequence id和已经处理完的最大sequence id

当broker开启消息去重之后,Broker会针对每个消息请求进行是否去重的判断,如果消息重复,则直接返回ack,不走后续存储的流程

延时消息

延时消息功能允许Consumer能够在消息发送到topic之后,过一段时间之后才能消费到这条消息。在这种集中中,消息在发布到broker之后,会被存储在book keeper中,当到消息特定的延迟时间时,消息就会传递给consumer

broker不会在存储的时候做特殊处理,而是会把设置了延迟时间的消息加入到DelayedDeliveryTracker中,当到了指定的发送时间时,Tracker才会把这条消息推送给消费者


原理:

在Pulsar中有两种方式实现延迟消息,分别为deliverAfter和deliverAt

deliverAfter可以指定在多长时间之后进行消费

deliverAt可以指定具体的延迟消费时间戳

DelayedDeliveryTracker会记录所有需要延迟投递的消息的index,index由timestamp、 ledger id 、和entry id三部分组成,其中ledger id 和 entry id用来定位该消息

timestamp除了记录需要投递的时间,还用于延迟优先级队列排序。tracker会根据延迟时间对消息进行排序

多租户模式

Pulsar的云原生架构天然支持多租户,每个租户下还支持多Namespace,非常适合做共享大集群,方便维护。此外Pulsar天然支持租户之间的逻辑隔离,防止互相干扰,还能实现大集群资源的充分利用

  • Tenant(租户)和 Namespace是Pulsar支持多租户的两个核心概念
  • 在租户级别,Pulsar为特定的租户预留合适的存储空间、应用授权和认证机制
  • 在namespace级别,Pulsar有一系列的配置策略(policy),包括存储配额、流控、消息过期策略等等

统一消息模型

Pulsar做了队列模型和流模型的统一,在topic级别只需要保存一份数据,同一份数据可多次消费。以流式、队列等方式计算不同的订阅模型,大大的提升了灵活度

同时Pulsar通过事务采用Exactly-Once刚好一次的语义,在进行消息传输过程中,可以确保消息不重不丢

分片流

Pulsar将无界的数据看作是分片的流,分片分散存储在分层存储(tiered storage)、BookKeeper集群和Broker节点上,而对外提供一个统一的、无界数据的视图

不需要用户显示迁移数据,对用户无感知,减少存储成本并保持近似无限的存储

跨地域复制

Pulsar中的跨地域复制是将Pulsar中持久化的消息在多个集群之间备份
在Pulsar2.4.0中新增了复制订阅模式,在某个集群失效情况下,该功能可以在其他集群恢复消费者的消费状态,从而达到热备模式下的消息服务高可用

架构

单个Pulsar集群由以下三部分组成

  • 一个或者多个broker 用于负责处理和负载均衡producer发出的消息 并将这些消息分派给consumer。Broker 和 Pulsar配置存储交互来处理相应的任务,并将消息存储在BookKeeper实例中(bookies);Broker底层依赖的是Zookeeper集群来处理特定的任务
  • 包含一个或者多个bookie的BookKeeper负责消息的持久化存储
  • 一个ZooKeeper集群用来处理多个Pulsar集群之间的协调任务

Pulsar分离出Broker和Bookie两层架构,Broker为无状态的服务,用于发布和消费消息,而BookKeeper专注于存储,Pulsar存储是分片的,这种架构可以避免扩容时受到限制,实现数据的独立拓展和快速恢复

Brokers
Pulsar的broker是一个无状态的组件,主要负责运行另外的两个组件

  • 一个HTTP服务器(service discovery) 它暴露了REST系统管理接口以及在生产者和消费者之间进行Topic查找的API
  • 一个调度分发器(Dispatcher) 它是一个异步的TCP服务器 通过自定义二进制协议应用与所有相关的数据传输

出于性能考虑,消息通常从Managed Ledger缓存中分派出去,除非积压超过缓存大小。如果积压的消息对于缓存来说太大了,则Broker开始从BookKeeper中读取Entries

为了支持全局Topic异地复制,Broker会控制Replicators追踪本地发布的条目

ZooKeeper元数据存储

Pulsar使用ZooKeeper进行元数据存储、集群配置和协调

  • 配置存储Quorum存储了租户、命名空间、和其他需要全局一致的配置项
  • 每个集群有自己独立的本地ZooKeeper保存集群内部的配置,例如broker负责哪几个主题及所有权归属元数据、broker负载报告ledger元数据等等

BookKeeper持久化存储

Apache Pulsar为应用程序提供有保障的信息传递,如果消息成功到达broker,就认为其预期达到了目的地

为了提供这种保证,未确认送达的消息需要持久化直到它们被确认送达。这种消息传递模式通常成为持久消息传递,在Pulsar内部,没分新消息都被保存并同步N份

BookKeeper是一个分布式的预写日志(WAL)系统,有如下几个特性

  • 使得Pulsar能够利用独立的日志,称为ledgers,随着时间的推移可以为topic创建多个ledgers
  • 保证多系统挂掉时的ledgers的读取一致性
  • 提供不同Boookies之间均匀的IO分布的特性
  • 容量和吞吐量都具有水平伸缩性,能够通过增加bookies立即增加容量到集群中