我轻轻地来,也轻轻地走,不带走一片云彩
一个码农需要掌握几种编程语言?
作为一个十年多的老码农,我个人建议至少要掌握三四种不同类的语言,一种系统级语言比如 c,一种脚本语言比如 python,一种面向对象语言比如 java,一种并发语言比如 go(它的宣传口号是为并发而生,学习中)。
不要单纯学语法,要学内存模型、并发机制(进 /多线程,协程还是事件驱动)、集合、数据结构,再就是语言特有的特性比如指针、元类、反射、channel 通道等。最好懂 os(其实不懂,对大大部分码农的开发也不怎么影响),能写出 netty 、kafka 、docker(见有人用不到 500 行的 c 代码就仿了个简化版的 docker 容器核心功能)等的人不是 java 、go 语言学的多好(哪怕你把语法倒背如流也不行),而是懂了 os 系统方面的知识(某些特性),知道它能干什么用,20%的核心代码(其他 tool 、log 、account 、extend 、webadmin 、security 、example 、test 等包里代码不算,算是辅助性)。
看到不少人都在使用框架经验,我也写写吧,从业至今,用了少说得有十种开源库和中间件(虽然有的过时了,不用了,生态建设不行,被遗弃了,不过原理性的东西没大变,新瓶装旧酒、换汤不换药)。
今先就以kafka介绍,说几个点,api调用我就不说了。
Kafka早期是由Linkedin公司开发,是一个分布式、分区的、多副本的、多订阅者,基于zookeeper协调的分布式日志系统,常见可以用于访问日志,消息服务等,Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目,然后大家都知道了,迅速风靡席卷各大中型互联网公司(后来的事了,移动互联网还没起来,也没有什么抖音快手拼多多这样的公司)。
kafka有以下特性:
我们先看看kafka里的几个专业术语:
- Zookeeper:第三方组件,协调服务组件
- broker :Kafka集群包含一个或多个服务器,这种服务器被称为broker代理
- Topic : 每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic(可以理解为队列queue或者目录)。物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处。
- Partition: Parition是物理机器上真实存在的概念,每个Topic包含一个或多个Partition。
- Producer: 生产者,负责发布消息到Kafka broker。
- Consumer:消息消费者,向Kafka broker读取消息的客户端。
- Consumer Group:每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)。
kafka因何那么快
1. 利用Topic Partition分区实现并行处理
Kafka Topic 只是一个逻辑上的概念,每个 Topic 都包含一个或多个 Partition,不同 Partition 可位于不同节点。
由于不同 Partition 可位于不同机器,因此可以充分利用集群优势,实现机器间的并行处理。另一方面,由于 Partition 在物理上对应一个个文件夹,即使多个 Partition 位于同一个节点,也可通过配置让同一节点上的不同 Partition 置于不同的磁盘上,从而实现磁盘间的并行处理,充分发挥多磁盘的优势。
补充性知识:磁盘/IO
硬盘性能的制约因素是什么?如何根据磁盘I/O特性来进行系统设计?
硬盘内部主要部件为磁盘盘片、传动手臂、读写磁头和主轴马达。实际数据都是写在盘片上,读写主要是通过传动手臂上的读写磁头来完成。实际运行时,主轴让磁盘盘片转动,然后传动手臂可伸展让读取头在盘片上进行读写操作,磁盘物理结构如下图所示:
由于单一盘片容量有限,一般硬盘都有两张以上的盘片,每个盘片有两面,都可记录信息,所以一张盘片对应着两个磁头。盘片被分为许多扇形的区域,每个区域叫一个扇区。盘片表面上以盘片中心为圆心,不同半径的同心圆称为磁道,不同盘片相同半径的磁道所组成的圆柱称为柱面。磁道与柱面都是表示不同半径的圆,在许多场合,磁道和柱面可以互换使用。
影响磁盘的关键因素是磁盘服务时间,即磁盘完成一个I/O请求所花费的时间,它由寻道时间、旋转延迟和数据传输时间三部分构成。
机械硬盘的连续读写性能很好,但随机读写性能很差,这主要是因为磁头移动到正确的磁道上需要时间,随机读写时,磁头需要不停的移动,时间都浪费在了磁头寻址上,所以性能不高。衡量磁盘的重要主要指标是IOPS和吞吐量。
在许多的开源框架如 Kafka、HBase 中,都通过追加写的方式来尽可能的将随机 I/O 转换为顺序 I/O,以此来降低寻址时间和旋转延时,从而最大限度的提高 IOPS。
2. 顺序写磁盘
Kafka 中每个分区是一个有序的,不可变的消息序列,新的消息不断追加到 partition 的末尾,这个就是顺序写。
磁盘容量是有限的,不可能保存所有数据,实际上作为消息系统 Kafka 也没必要保存所有数据,需要删除旧的数据。又由于顺序写入的原因,所以 Kafka 采用各种删除策略删除数据的时候,并非通过使用“读 - 写”模式去修改文件,而是将 Partition 分为多个 Segment,每个 Segment 对应一个物理文件,通过删除整个文件的方式去删除 Partition 内的数据。这种方式清除旧数据的方式,也避免了对文件的随机写操作。
3. 利用Linux Page Cache
“引入 Cache 层的目的是为了提高 Linux 操作系统对磁盘访问的性能。Cache 层在内存中缓存了磁盘上的部分数据。当数据的请求到达时,如果在 Cache 中存在该数据且是最新的,则直接将数据传递给用户程序,免除了对底层磁盘的操作,提高了性能。Cache 层也正是磁盘 IOPS 为什么能突破 200 的主要原因之一。
在 Linux 的实现中,文件 Cache 分为两个层面,一是 Page Cache,另一个 Buffer Cache,每一个 Page Cache 包含若干 Buffer Cache。Page Cache 主要用来作为文件系统上的文件数据的缓存来用,尤其是针对当进程对文件有 read/write 操作的时候。Buffer Cache 则主要是设计用来在系统对块设备进行读写的时候,对块进行数据缓存的系统来使用。
使用 Page Cache 的好处:
-
I/O Scheduler 会将连续的小块写组装成大块的物理写从而提高性能
-
I/O Scheduler 会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间
-
充分利用所有空闲内存(非 JVM 内存)。如果使用应用层 Cache(即 JVM 堆内存),会增加 GC 负担
-
读操作可直接在 Page Cache 内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过 Page Cache)交换数据
-
如果进程重启,JVM 内的 Cache 会失效,但 Page Cache 仍然可用
Broker 收到数据后,写磁盘时只是将数据写入 Page Cache,并不保证数据一定完全写入磁盘。从这一点看,可能会造成机器宕机时,Page Cache 内的数据未写入磁盘从而造成数据丢失。但是这种丢失只发生在机器断电等造成操作系统不工作的场景,而这种场景完全可以由 Kafka 层面的 Replication 机制去解决。如果为了保证这种情况下数据不丢失而强制将 Page Cache 中的数据 Flush 到磁盘,反而会降低性能。也正因如此,Kafka 虽然提供了 flush.messages
和 flush.ms
两个参数将 Page Cache 中的数据强制 Flush 到磁盘,但是 Kafka 并不建议使用。
4. 零拷贝技术
前言,我们知道网络和IO(Input Output)是最基本的功能。
可以简单回顾下读写文件的过程:
4.1 读文件
就以java读文件演示
java程序发起 read文件请求之后,内核接收到 read 请求之后,会先检查内核空间中是否已经存在进程所需要的数据,如果已经存在,则直接把数据 copy 给进程的应用缓冲区。
如果没有内核随即向磁盘控制器发出命令,要求从磁盘读取数据,磁盘控制器把数据直接写入内核 read 缓冲区,这一步通过 DMA 完成。
接下来就是内核将数据 copy 到进程的应用缓冲区。 如果进程发起 Write 请求,同样需要把用户缓冲区里面的数据 copy 到内核的 网络socket 缓冲区里面,然后再通过 DMA 把数据 copy 到网卡中,发送出去。
4. 2. 写操作
综上我们可以看出IO读写操作,总共进行了4次copy动作,当然中间也伴随着4次上下文切换。
我们可以看到数据在内核空间和应用空间之间来回拷贝复制,其实他们什么都没有做,就是仅仅来回复制而已,这种机制太浪费时间了,而且是浪费的CPU的时间。
那么我们能不能让数据不要来回复制呢?零拷贝这个技术就是来解决这个问题。
零拷贝主要的任务就是避免CPU将数据从一块存储拷贝到另外一块存储,主要就是利用各种零拷贝技术,避免让CPU做大量的数据拷贝任务,减少不必要的拷贝,或者让别的组件来做这一类简单的数据传输任务,让CPU解脱出来专注于别的任务。这样就可以让系统资源的利用更加有效。我们如何减少数据拷贝的次数呢?一个很明显的着力点就是减少数据在内核空间和用户空间来回拷贝,这也引入了零拷贝的一个类型:让数据传输不需要经过 user space。
Kafka 中存在大量的网络数据持久化到磁盘(Producer 到 Broker)和磁盘文件通过网络发送(Broker 到 Consumer)的过程。这一过程的性能直接影响 Kafka 的整体吞吐量。
“操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。
为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部分,一部分是内核空间(Kernel-space),一部分是用户空间(User-space)。
传统的 Linux 系统中,标准的 I/O 接口(例如read,write)都是基于数据拷贝操作的,即 I/O 操作会导致数据在内核地址空间的缓冲区和用户地址空间的缓冲区之间进行拷贝,所以标准 I/O 也被称作缓存 I/O。这样做的好处是,如果所请求的数据已经存放在内核的高速缓冲存储器中,那么就可以减少实际的 I/O 操作,但坏处就是数据拷贝的过程,会导致 CPU 开销。
我们把 Kafka 的生产和消费简化成如下两个过程来看:
-
网络数据持久化到磁盘 (Producer 到 Broker)
-
磁盘文件通过网络发送(Broker 到 Consumer)
4.3 网络数据持久化到磁盘 (Producer 到 Broker)
传统模式下,数据从网络传输到文件需要 4 次数据拷贝、4 次上下文切换和两次系统调用。
data = socket.read()// 读取网络数据
File file = new File()
file.write(data)// 持久化到磁盘
file.flush()
这一过程实际上发生了四次数据拷贝:
-
首先通过 DMA copy 将网络数据拷贝到内核态 Socket Buffer
-
然后应用程序将内核态 Buffer 数据读入用户态(CPU copy)
-
接着用户程序将用户态 Buffer 再拷贝到内核态(CPU copy)
-
最后通过 DMA copy 将数据拷贝到磁盘文件
“DMA(Direct Memory Access):直接存储器访问。DMA 是一种无需 CPU 的参与,让外设和系统内存之间进行双向数据传输的硬件机制。使用 DMA 可以使系统 CPU 从实际的 I/O 数据传输过程中摆脱出来,从而大大提高系统的吞吐率。
同时,还伴随着四次上下文切换,如下图所示
数据落盘通常都是非实时的,kafka 生产者数据持久化也是如此。Kafka 的数据并不是实时的写入硬盘,它充分利用了现代操作系统分页存储来利用内存提高 I/O 效率,就是上一节提到的 Linux Page Cache。
对于 kafka 来说,Producer 生产的数据存到 broker,这个过程读取到 socket buffer 的网络数据,其实可以直接在内核空间完成落盘。并没有必要将 socket buffer 的网络数据,读取到应用进程缓冲区;在这里应用进程缓冲区其实就是 broker,broker 收到生产者的数据,就是为了持久化。
在此特殊场景
下:接收来自 socket buffer 的网络数据,应用进程不需要中间处理、直接进行持久化时。可以使用 mmap 内存文件映射。
“Memory Mapped Files:简称 mmap,也有叫 MMFile 的,使用 mmap 的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射。从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程。它的工作原理是直接利用操作系统的 Page 来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上。
使用这种方式可以获取很大的 I/O 提升,省去了用户空间到内核空间复制的开销。
mmap 也有一个很明显的缺陷——不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘。Kafka 提供了一个参数——producer.type
来控制是不是主动flush;如果 Kafka 写入到 mmap 之后就立即 flush 然后再返回 Producer 叫同步(sync);写入 mmap 之后立即返回 Producer 不调用 flush 就叫异步(async),默认是 sync。
“零拷贝(Zero-copy)技术指在计算机执行操作时,CPU 不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及 CPU 的拷贝时间。
它的作用是在数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现 CPU 的零参与,彻底消除 CPU 在这方面的负载。
目前零拷贝技术主要有三种类型[3]:
直接I/O:数据直接跨过内核,在用户地址空间与I/O设备之间传递,内核只是进行必要的虚拟存储配置等辅助工作;
避免内核和用户空间之间的数据拷贝:当应用程序不需要对数据进行访问时,则可以避免将数据从内核空间拷贝到用户空间
mmap
sendfile
splice && tee
sockmap
copy on write:写时拷贝技术,数据不需要提前拷贝,而是当需要修改的时候再进行部分拷贝。
4.4 磁盘文件通过网络发送(Broker 到 Consumer)
传统方式实现:先读取磁盘、再用 socket 发送,实际也是进过四次 copy
buffer = File.read
Socket.send(buffer)
这一过程可以类比上边的生产消息:
-
首先通过系统调用将文件数据读入到内核态 Buffer(DMA 拷贝)
-
然后应用程序将内存态 Buffer 数据读入到用户态 Buffer(CPU 拷贝)
-
接着用户程序通过 Socket 发送数据时将用户态 Buffer 数据拷贝到内核态 Buffer(CPU 拷贝)
-
最后通过 DMA 拷贝将数据拷贝到 NIC Buffer
Linux 2.4+ 内核通过 sendfile 系统调用,提供了零拷贝。数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer,无需 CPU 拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件 - 网络发送由一个 sendfile 调用完成,整个过程只有两次上下文切换,因此大大提高了性能。
Kafka 在这里采用的方案是通过 NIO 的 transferTo/transferFrom
调用操作系统的 sendfile 实现零拷贝。总共发生 2 次内核数据拷贝、2 次上下文切换和一次系统调用,消除了 CPU 数据拷贝。
5. Batch批处理
在很多情况下,系统的瓶颈不是 CPU 或磁盘,而是网络IO。
因此,除了操作系统提供的低级批处理之外,Kafka 的客户端和 broker 还会在通过网络发送数据之前,在一个批处理中累积多条记录 (包括读和写)。记录的批处理分摊了网络往返的开销,使用了更大的数据包从而提高了带宽利用率。
6. 数据压缩
Producer 可将数据压缩后发送给 broker,从而减少网络传输代价,目前支持的压缩算法有:Snappy、Gzip、LZ4。数据压缩一般都是和批处理配套使用来作为优化手段的。
总算小结完了,这几大特性可以让kafka快速、近实时(人的角度)。
注意我用的版本有点老,可能跟现在的最新版有出入,就像springboot、dubbo、spring cloud xxx不同厂家实现、redis(都6版本了)一样,有的改动大,此文只做参考。
后记:当我们想扩写和造轮子时,也可以用这些特性:分区、顺序写、page cache、zero copy、压缩兼批处理、副本,当然其他框架或中间件也用到了这几个特性,有的技术点都可以在os、系统/网络编程都可以找得到精华(跟具体的编程语言无关),有的就是逻辑性构造了。
让我告诉小辈码农们学什么吧,其实是os和网络(请不要问我为什么),外加数学算法(非必须)和数据库(看情况),够了。
上传至https://github.com/dongguangming/java/tree/master/bigdata/kafka
参考:
-
Linux零拷贝原理 http://ifeve.com/linux%E9%9B%B6%E6%8B%B7%E8%B4%9D%E5%8E%9F%E7%90%86/
-
Linux I/O 的 Zero-Copy http://codingcms.cn/2019/11/24/Linux_1/
-
可算是有文章,把Linux零拷贝技术讲透彻了 https://www.modb.pro/db/26821
-
Linux 中的零拷贝技术,第 2 部分 https://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy2/index.html
-
零拷贝技术(zero-copy) https://blog.51cto.com/12182612/2424692
-
支撑百万并发的“零拷贝”技术,你了解吗?https://www.javazhiyin.com/46561.html
-
程序员必知必会的零拷贝技术 https://www.6aiq.com/article/1577782022362
-
一文透彻讲解零拷贝、mmap与Java NIO https://mp.weixin.qq.com/s?__biz=MzIxOTYzMzExNA==&mid=2247485444&idx=2&sn=6141dbaa5908ec016cde49c136586c23&chksm=97d90cb1a0ae85a7c78b0ab0022db2830d7be7b3b6e1728c3cefa311581c9f45611f393c6cad&mpshare=1&scene=23&srcid=0117ROgAWghTgkocDtk5G94r&sharer_sharetime=1611715502027&sharer_shareid=b63fade81b9de48d13df385240595463#rd
-
Kafka是如何利用零拷贝提高性能的 https://juejin.im/post/5eb8ca9d5188256d571ee58a
-
kafka速度快与超高吞吐量的原因分析 http://arick.net/content/8
-
Netty、Kafka中的零拷贝技术到底有多牛? https://developer.51cto.com/art/202002/611180.htm
-
面试问:Kafka 为什么速度那么快?https://zhuanlan.zhihu.com/p/147054382
-
磁盘读写优化技术研究-Zookeeper/Kafka/MySQL https://wangjunfei.com/2020/03/15/%E7%A3%81%E7%9B%98%E8%AF%BB%E5%86%99%E4%BC%98%E5%8C%96%E6%8A%80%E6%9C%AF%E7%A0%94%E7%A9%B6-Zookeeper-Kafka-MySQL/
-
sendfile“零拷贝”、mmap内存映射、DMA https://www.pianshen.com/article/2643906799/
- sendfile(2) - Linux man page
https://linux.die.net/man/2/sendfile -
Zero-Copy in Linux with sendfile() and splice() https://blog.superpat.com/2010/06/01/zero-copy-in-linux-with-sendfile-and-splice/
-
So you want to write to a file real fast… https://blog.plenz.com/2014-04/so-you-want-to-write-to-a-file-real-fast.html
-
It's all about buffers: zero-copy, mmap and Java NIO https://medium.com/@xunnan.xu/its-all-about-buffers-zero-copy-mmap-and-java-nio-50f2a1bfc05c, https://xunnanxu.github.io/2016/09/10/It-s-all-about-buffers-zero-copy-mmap-and-Java-NIO/
-
Zero Copy I: User-Mode Perspective https://www.linuxjournal.com/article/6345