首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >结构化学习思维

结构化学习思维

原创
作者头像
黄豆酱
发布2024-12-17 12:06:12
发布2024-12-17 12:06:12
4121
举报
文章被收录于专栏:方法论方法论

终身学习

终身学习是指社会每个成员为适应社会发展和实现个体发展的需要,贯穿于人的一生的,持续的学习过程。 即我们所常说的“活到老学到老”或者“学无止境”。(百度百科)

世界上没有永远不变的事物,唯一不变的只有不断地变化。持续学习、终身学习是我们应对变化最有效且成本最低的方法。

结构化思维

我认为学习能力是:获取信息的能力+对信息的整合能力+对信息的内化能力+学以致用的能力。单独知识点,它的存在价值是微乎其微的。因为我们在实际的场景中某一个单一维度的知识点被运用起来是非常难的,可以说完全没有。所以我们需要将自己大脑中的各个知识点,尽量的建立起连接,这样当我们遇到需要解决的问题,我们想到的解决思路也会是倾向于结构化的。(前提是掌握全面的信息)

“点动成线,线动成面,面动成体”,这句话很生动形象的描述出个人知识体系的形成过程。这里我把学习分成五个阶段:知识点、知识点的性质、多个知识点之间建立连接形成知识网、形成知识体系、预测事态发展(知识+敏锐度+认知逻辑)。

下面的大致方向是:先纵后横。

第一阶段:特点

这里用消息队列kafka来举例。kafka作为热门的消息中间件,具有高吞吐量、低延迟(kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒)、可扩展性(kafka集群支持热扩展)、持久性(数据落磁盘)、可靠性(消息被持久化到本地磁盘,并且支持数据备份防止数据丢失)、容错性(允许集群中节点故障,一个数据多个副本,少数机器宕机,不会丢失数据)、高并发(支持数千个客户端同时读写)等优势。也有缺点,比如rebalance机制。下面是kafka简单的架构图:

图中有两个producer,分别对应两个topic。其中topicA有三个分区,topicB有一个分区,分区是kafka存储消息的物理概念,新消息会以追加的方式写入分区。每个分区都有多个副本分区,多个分区会均匀的散落在不同broker机器中。主分区实现消息写入和输出,副本分区只需要定时的同步主分区的数据即可。kafka的数据是存储在文件中,数据是分段存储的。一个数据段对应两个文件,一种是数据文件.log一种是索引文件.index,这里使用了稀疏索引。

kafka在日常的开发中使用的频率非常高,上面描述的与kafka相关信息可能大家已经很熟悉了。

那么问题来了:

kafka为什么具有高吞吐、低延迟、可扩展、高可靠、容错性、高并发这些特点?

第二阶段:性质

在上个阶段我们了解到kafka的一些特点。接下来我们挑几个特点来进行深入研究:

kafka高吞吐的原因

1、分区并发

kafka通过分区将topic的消息打散到不通的broker上,每个线程对应一个分区数据,分区是kafka数据存储的最小单位,也是kafka调优并行度的最小单元。kafka的producer和consumer都支持多线程并发处理,分区并发实现了producer和consumer的消息处理的高吞吐。

进一步研究:按照上面的描述topic的分区数越多,集群的吞吐量就越大,那是不是分区越多越好?实际上并不是

producer角度

在客户端producer有个参数batch.size,,默认是16KB,它会为每个分区缓存消息,缓存打满后会将消息批量发出。如果分区越多,这部分缓存所占用的内存就越多。我们假设有10000个分区,那么按照默认的设置这部分缓存需要占用157M内存。

consumer角度

对于consumer端,10000个分区对应10000个线程,也需对应10000个socket获取分区数据,在这种情况下线程上下文切换的开销会很大,甚至影响服务的性能。

文件句柄角度

在kafka底层,一个分区实际对应文件系统中的两种文件,一种是.log,另一种是.index。如果分区越多,那么需要保持打开状态的文件句柄也会增多,可能会突破系统的限制。

分区副本角度

kafka通过分区副本来保持服务的高可用,每个副本存在于不同的broker上。分区有leader副本和follower副本的区分。leader副本负责处理producer和consumer的请求,其他副本充当follower的角色仅保证数据和leader副本的同步。

如果leader副本异常了,会通过zookeeper选举出新的leader副本,正常情况下中间会有短暂的不可用时间,大部分情况下是几毫秒的级别。但是如果有10000个分区,分别分散在10个broker,也就是平均每个broker上有1000个分区。这种情况下,zookeeper需要迅速对1000个分区进行leader副本的选举,这往往需要花费更长的时间。

2、页缓存

kafka中大量使用了页缓存,这是kafka高吞吐的原因之一,消息会先被写入到页缓存,然后由操作系统负责将数据刷到硬盘。同时kafka也提供了同步刷盘、间断性强制刷盘的功能,这些功能可以通过配置参数来控制。同步刷盘可以提高系统的可靠性,防止消息丢失,同时也会牺牲性能。一般情况下,不会配置强制刷盘,因为消息的可靠性主要通过数据冗余来保障。

进一步研究:接下来我们继续向下研究什么是页缓存?

文件一般都放在硬盘中,cpu在读取数据的时候并不是直接去请求磁盘中的数据,而是会把磁盘中的数据读取到内存中,cpu再去访问内存。为了提高对文件的读写效率(读文件、写文件、垃圾回收),内核会以4KB为单位把文件划分问多个数据块。如果用户对文件中的某一块进行读写时,操作系统会把一个page内存和文件中的数据块进行绑定(映射),这就是页缓存。

当kafka的broker对数据进行读写时,实际上是对页缓存进行读写。当要读取的文件内容已经在页缓存中,那么直接拷贝页缓存中的数据即可。否则,内核会现申请一个空闲内存页,然后再从文件中读取数据,并把页缓存拷贝给用户。对于被修改过的页缓存,系统会定时的把数据刷新到磁盘上。下面是页缓存的结构。

代码语言:txt
复制
struct file {
    struct address_space *f_mapping;
}

struct address_space {
    struct inode *host; // 记录页缓存的inode信息,inode是文件的唯一标识
    struct radix_tree_root page_tree; // 这里就是页缓存
    relock_t tree_lock;// 锁,防止并发引起的资源竞争
}

在操作系统中用file来表示文件,file文件中有一个f_mapping字段。从结构中可以看出,页缓存是使用radix树进行存储的。radix可以看成是一个key-value的键值对,key是数据的所在的偏移量,value是对应的page cache。

3、零拷贝

我们都知道操作系统中有用户态和内核态的区分,这是为了操作系统的安全和管理设计的。在操作系统中,内核态是运行操作系统程序、操作硬件状态,有最高权限;用户态是运行用户程序的,它的权限会受到限制。用户态和内核态的主要区别就是在权限和资源的访问上。

代码在实际的运行过程中,需要通过中断和系统调用来实现上下文切换。每个进程会有两个栈,一个用户态栈、一个内核态栈。当程序执行系统调用时,会使用软中断指令保存用户态的栈,转向内核态栈。在这个过程中会涉及到栈信息的切换,而且内核代码对用户是不信任的,还需要进行额外的检查。如果需要在不同的用户程序间进行切换,那么信息的复制和映射需要更换多次。

无论是读写本地文件还是发送网络请求,都不可避免的产生上下文切换。

kafka的数据是存储在文件系统中。当有消费者订阅消息时,数据需要从磁盘中读取出并且将数据写入到Socket中,这个动作的效率非常低。首先内核读出全部数据,然后将数据从内核态拷贝到用户态,然后数据会再次从用户态被推送到内核态并写入到Socket中。这这个过程中,应用程序没有起到设么实质性的作用,只是充当了拷贝中转的角色。前面我们讲过,上下文切换是很影响整体性能的,它涉及到cpu的中断、栈信息拷贝等动作。

那么零拷贝就是解决这个问题的,使用零拷贝的应用程序可要求内核直接将数据从磁盘拷贝到Socket。零拷贝减少了上下文切换,提高了应用程序的性能。

4、数据压缩

kafka支持在producer端和broker端将消息进行压缩,在consumer端进行解压缩。producer端和broker端在满足cpu的资源条件下,使用环境中限制带宽资源,那么建议打开压缩。大部分情况下,broker从producer端接收到消息后是原封不动的保存,不对对消息进行任何修改。只有两种情况下会是broker端重新压缩消息:

第一种情况,broker指定了和producer不同的压缩算法。kafka的broker端也有个压缩算法参数,这里的参数设置默认是和producer参数设置一样,表示broker尊重producer端使用的压缩算法。如果在broker端设置的不同于producer压缩算法,会发生意料之外的压缩/解析压缩的操作,导致broker的cpu飙升。比如broker收到了gzip的压缩消息后,broker被指定了snappy的压缩算法,这样broker需要解压gzip后再压缩。

第二种情况,broker端发送消息的格式发生了变化。在一个生产环境中,kafka集群中同时保存了多种版本的消息格式,为了兼容老的版本格式,broker通常会对新版本消息执行向老版本格式的转换。这个过程会涉及到加压的重新压缩。一般情况下这种消息格式的转化对性能有很大的影响。除了解压、压缩会额外的消耗cpu之外,还会使kafka丧失零拷贝的特性。所以在生产环境中,尽量的保持消息格式的统一。

5、磁盘顺序写

当数据写入时,kafka会先将内容写入内存的缓冲区,当缓冲区满后,会将数据一次性顺序的刷入磁盘中。这种写入方式,省去了磁盘磁头寻址的时间。从而大大提高的写入和读取的效率。磁盘分为两种,一种是机械磁盘、一种是固态磁盘。

机械硬盘的寻址是通过磁头移动来实现的,它需要把磁头移动到特定位置,才能读取或写入数据。寻址就是从这些扇形中找数据的过程。在不知道具体的物理地址之前,需要进行全盘遍历。如果整个圆柱体转动结束都读不到数据的情况下,会移动磁头,也就是进行寻道,再接着继续进行旋转读取磁道信息,直到找到对应的数据。其中寻道时间远大于旋转时间。所以我们在寻址时优先考虑磁盘旋转,而后再考虑磁头移动。

固态硬盘是通过分块来存储数据的,系统会为每个块起一个编号。在每次访问数据时,会提供对应块的编号,硬盘控制器会根据编号找到对应的存储块,然后将数据读取处理。固态硬盘是通过光学寻址,利用一个微型激光器发出微弱的光,将光定位在数据所在的块上。

固态硬盘价格高,固态硬盘在达到一定的擦写次数(全盘)之后会有损坏。

kafka高可靠的原因

kafka的高可靠的核心是保证消息在传递的过程中不丢失。

1、从消息生产者到broker

消息从生产者可靠的发送到broker:在producer发送消息时,能够收到broker发送的ack,确保消息发送成功。producer对发送失败的消息会做捕捉,并做对应的处理。具体做了哪些处理?

ack策略:分成三种等级发送即任务成功,不关心是否写成功;leader分区写入成功后才算是发送成功;所有的分区副本同步成功后才算是写入成功,强可靠性的保证。

消息发送策略

kafka提供两种消息发送方式,同步和异步。kafka发送消息其实包含了两步:发送消息和消息分发partition。

异步消息

在主协程调用异步发布kafka消息时,其本质是将消息体放入了一个input的channel,只要channel写入成功函数就会直接返回不会有任何阻塞。相反,如果channel失败,就会返回错误信息。因此调用async返回的结果信息是写入channel的结果,至于消息是否有写入到broker是无法得知的。

当消息进入input的channel后,会有调度协程负责遍历input channel,再将消息写入到broker上的partition中。发送的结果通过一个异步的协程监听,循环处理err channel和success channel,如果出现了error就记录一个日志,所以在异步的场景中如果发送消息产生了错误,我们只能通过错误日志来发现,并且在kafka中不支持自建函数来进行兜底。

同步消息

同步消息发送是在producer中开启两个异步协程处理消息成功和失败的回调,并使用waitgroup进行等待,将异步的操作转变成同步的操作。其实kafka发送消息本质上都是异步的,同步的发送是通过waitGroup将异步操作转化为同步操作。同步操作在一定程度上确保了在跨网络向broker发送消息时,消息一定可以可靠的传输到broker。如果发生因为网络抖动、机器宕机等原因导致的发送失败或者结果不明,可以通过重试等方法确保消息至少一次发送到broker上。

2、broker的消息持久化

kafka为了获得更高的吞吐,broker接收到消息后只是将数据写入到pagecache后就会认为消息已经写入成功。pagecache中的数据通过系统异步的将数据顺序写入磁盘。这里的消息是写入到pagecache的,单机情况下在还没刷盘成功broker就宕机了,就会产生数据的丢失。

replica副本机制

kafka的一个分区有多个副本,同一个分区的多个副本会分布在不同的broker上,副本之前是一主多从的关系。follower副本是否与leader副本同步的判断标准取决于broker端的参数replica.lag.time.max.ms(默认10秒),follower默认每隔500ms向leader拉取一次数据,只要一个follower副本落后leader的数据时间不超过连续的10秒,那么kafka就认为follower副本和leader副本是同步的。

当leader所在的副本出现宕机时,kafka会借助zk从follower副本中选举出新的leader副本来对外提供服务,实现故障的自动转移,保证服务可用。选举会优先选择同步度高的副本作为新的leader副本。

3、consumer消费位移offset

consumer在消费的过程中需要向kafka汇报自己的位移数据,只有当consumer向kafka汇报了消息位移,这条消息才会被broker认为已经被消费。所以,consumer端消息的可靠性主要和offset提交方式有关。kafka消费端提供了两种消息提交方式:自动提交和手动提交。

通过上面对kafka高吞吐、高可靠原因的探索,我们得到了这样一张图。

建立连接

建立连接就是以上面这张图为起点,扩张图中的知识点。其实就是把自己能想到的任何可以和上图节点有逻辑共性的知识点进行关联。下面简单举几个例子:

radix树、字典树、B+树、跳表

思路过程:什么是radix树——类似字典树——都是前缀树——还知道mysqlB+树——类似跳表

radix树和字典树都是前缀树,适合公共前缀多的。它和字段树的最大区别是:它每个节点可以以1个或者多个字符叠加作为一个分支,这样对于长字符key来说减少了树的深度。在操作系统中,都是以0、1来表示数据,radix比字典树就更加适合。前缀树对比B+树,B+树更适合作为范围的查询。同为范围索引的还有跳表。

B+树和跳表又有什么关联?整体上,B+树和跳表都是链表+多级索引构成的。他们有什么不同

B+树的节点大小符合操作系统的页大小page (16K);跳表是 node 节点 ,一个node 几十个字节;

B+树是多叉树结构,每个结点都是一个16k的数据页,能存放较多索引信息。把三层B+树塞满,大概需要2kw左右的数据,最多需要查询三次磁盘IO。同样的数跳表最底层要存放2kw数据,大概高度在24层左右。 如果要一个节点要进行一次磁盘IO,大概要进行 24次。树高度的降低能减少磁盘寻道的次数。所以跳表不适合磁盘IO。

上下文切换——go协程调度GMP模型

上面我们说到,操作系统整个体系分为用户态和内核态。当用线程主动休眠、线程等待锁、线程主动让出cpu、高优先级线程抢占、线程时间片用完等时机都会发生上下文切换。进行上下文切换有额外的开销:需要保存/恢复栈信息、线程调度器调度线程等。所以,降低上下文切换的次数就等于提升性能。

我们知道go协程机制使用的调度算法是GMP模型,线程是协程的运行载体。go调度的本质是把大量的协程分配到少量的线程中去执行,利用多核并行,实现轻量级并发。

go的协程是非抢占式的,在发生IO操作时并不是调度器强制切换执行其他的协程,而是当前协程交出了控制权,调度器才去执行其他协程。如果是抢占式,那么就会在当前线程正在做一件事,做到一半就强制停止,这时就必须多保存很多信息,避免再次切换回来时任务出错。

我们看golang的调度算法,就是在原有的用户线程和内核线程中间增加了一层调度,来提高对字段的更细粒度的控制权。这就对应了一句话:

任何软件工程遇到的问题都可以通过增加一个中间层来解决

我们日常工作中见到的通过“加一层”来解决问题的场景有哪些?

page cache就是运用了这种思想,预加载/写入,我们可以把页缓存看成lru 缓存;

mysql保证写入数据不丢失的方法是2层写缓存机制,第一层是binlog第二程是redolog;

mysql在数据写入时,也是用了顺序写;

mysql的mvcc的高低水位和kafka消息的写入和消费;

等等……

通过各种知识点的关联我们可以得到上面的图片,这里的每一条路径都是通过我们思考两个知识点之前的共性、以及对比得到的。虽然看起来比较乱。当你完成了这种知识点之间的链接后,日后你在听到上面的任意一点时,你大脑中浮现的一定是今天链接成的这张网状的图谱。

然后会快乐的发现,万物皆可连。如果想不明白两个知识点之间有啥关联,那一定是获取到的信息不够。

成体系

个人知识体系,可以理解成是无数个关联的知识集合,是个人认知、实践中总结出来的。包括理论知识、实践技能、逻辑思维、哲学、习惯嗜好等,是广泛、复杂的。(都是场面话)

个人知识体系。这个词是去年偶然间听到的,当时查了挺多相关信息,目的是想快速找到一条可以复制的构建个人知识体系的“捷径”,结果不尽如人意。大部分信息都是停留在概念上,要么就是推荐对应的信息处理工具,很少有从0到1的搭建过程。然后我意识到,个人知识体系可能是差异化比较大的,所以如果想构建属于自己的个人知识体系,需要亲自去踩一踩。

不论是否真的有去搭建个人知识体系,这个系统化、结构化的意识是需要存在且时常强调的。

小技巧

集中突破一个点

工作中用的技术、架构、中间件很多,初期可能会不知道从哪方面开始。就找一个点,集中精力研究透一个点。然后可能就“一通百通”了。

反者道之动

基础知识永远是最重要的,基础知识是上层复杂知识的组成部分。上层知识的特性,其实是继承了基础知识的特点。

看新闻

我们看新闻报道的事件,以自己现有的信息量来进行推测,这个事件的发生会对社会产生哪些影响。随着时间的推移验证自己的判断是否准确。其实这个就是锻炼个人对知识的综合处理和推测能力。

写自己的(技术)书

写一本关于自己的(技术)书,直接写书的内容是比较难的,你会不知道如何下手。那就可以从目录开始写。这样做至少有两个好处:一个你会对自己已经学会的知识进行一个大整理;还一个如果你有想学还没来得及学会的知识,那么写在这里会对你有正向的牵引作用。

理性看待方法论

方法论没有好坏之分,唯一会产生差异的是个人行动力和思维力。

知之愈明,则行之愈笃。

行之愈笃,则知之益明。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 终身学习
  • 结构化思维
    • 第一阶段:特点
    • 第二阶段:性质
      • kafka高吞吐的原因
      • kafka高可靠的原因
    • 建立连接
      • radix树、字典树、B+树、跳表
      • 上下文切换——go协程调度GMP模型
      • 任何软件工程遇到的问题都可以通过增加一个中间层来解决
  • 成体系
  • 小技巧
    • 集中突破一个点
    • 反者道之动
    • 看新闻
    • 写自己的(技术)书
    • 理性看待方法论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档