apache kafka 速度快的原理

kafka采用页缓存技术、顺序写入磁盘等技术来提升性能。在顺序读写的情况下,磁盘的顺序读写速度和内存相差无几,PageCache是系统级别的缓存,它把尽可能多的空闲内存当作磁盘缓存使用来进一步提高IO效率;所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手

apache kafka 采用页缓存技术、顺序写入磁盘等技术来提升性能。在顺序读写的情况下,磁盘的顺序读写速度和内存相差无几,PageCache是系统级别的缓存,它把尽可能多的空闲内存当作磁盘缓存使用来进一步提高IO效率;所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手

预备知识一: 页缓存(Page Cache)

缓存能提高I/O性能是基于以下2个重要的原理:

  1. CPU访问内存的速度远远大于访问磁盘的速度(访问速度差距不是一般的大,差好几个数量级)
  2. 数据一旦被访问,就有可能在短期内再次被访问(临时局部原理)
  • 文件一般存放在硬盘(机械硬盘或固态硬盘)中,CPU 并不能直接访问硬盘中的数据,而是需要先将硬盘中的数据读入到内存中,然后才能被 CPU 访问
  • 读写硬盘的速度比读写内存要慢很多,为了避免每次读写文件时,都需要对硬盘进行读写操作,Linux 内核使用 页缓存(Page Cache) 机制来对文件中的数据进行缓存。
  • 为了提升对文件的读写效率,Linux 内核会以页大小(4KB)为单位,将文件划分为多数据块。当用户对文件中的某个数据块进行读写操作时,内核首先会申请一个内存页(称为 页缓存 )与文件中的数据块进行绑定。
  • 页缓存,也称为磁盘缓存,是计算机随机存取存储器(RAM)的一个区域,用于保存并可能修改存储在硬盘或其他永久存储设备上的数据。
  • 页缓存,也称为磁盘缓存,是计算机随机存取存储器(RAM)的一个区域,用于保存并可能修改存储在硬盘或其他永久存储设备上的数据。
    1. 当从文件中读取数据时,如果要读取的数据所在的页缓存已经存在,那么就直接把页缓存的数据拷贝给用户即可。否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把页缓存的数据拷贝给用户。
    2. 当向文件中写入数据时,如果要写入的数据所在的页缓存已经存在,那么直接把新数据写入到页缓存即可。否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把新数据写入到页缓存中。对于被修改的页缓存,内核会定时把这些页缓存刷新到文件中。

预备知识二:「写缓存」常见的有3种策略

  1. 不缓存(nowrite) :: 也就是不缓存写操作,当对缓存中的数据进行写操作时,直接写入磁盘,同时使此数据的缓存失效
  2. 写透缓存(write-through) :: 写数据时同时更新磁盘和缓存
  3. 回写(copy-write or write-behind) :: 写数据时直接写到缓存,由另外的进程(回写进程)在合适的时候将数据同步到磁盘

预备知识三:通用页缓存流程

# DMA direct memory access:直接存储器访问,也就是直接访问RAM,不需要依赖CPU的负载
# CPU :中央核心处理器,主要用于计算,如果用于拷贝就太浪费资源
磁盘文件 ==DMAcopy=> 页缓存 ==CPUcopy=> 用户空间缓存 ==CPUcopy=> Socket缓存 ==DMAcopy=>> 网卡

页缓存减少了连续读写磁盘文件的次数,操作系统自动控制文件块的缓存与回收生命周期,用访问RAM的缓存代替访问磁盘区域的机制,增强查询效率。

Linux操作系统中的vm.dirty_background_ratio参数用来指定当脏页数量达到系统内存的百分之多少之后就会触发pdflush/flush/kdmflush等后台回写进程的运行来处理脏页,一般设置为小于10%的值即可,但不建议设置为0.与这个参数对应的还一个vm.dirty_ratio参数,它用来指定当脏页数量达到系统内存的百分之多少之后就不得不开始对脏页进行处理,在此过程中,新的I/O请求会被阻挡直至所有脏页被冲刷到磁盘中。

kafka页缓存技术如何实现

Kafka中大量使用了页缓存,

  • 消息都是先被写入页缓存,然后由操作系统负责具体的刷盘任务
  • 在Kafka中同样提供了同步刷盘及间断性强制刷盘(fsync)的功能,可以通过log.flush.interval.message、log.flush.interval.ms等参数来控制。
  • 同步刷盘可以提高 消息的可行性,防止由于机器掉电等异常造成处于页缓存而没有及时写入磁盘的消息丢失。
  • 一般不建议做同步刷盘,刷盘任务就应交由操作系统去调配,消息的可靠性应该由多副本机制来保障,而不是由同步刷盘这种严重影响性能的行为来保障

MMFile (Memory Mapped File):

  • (简称 mmap)也被翻译成内存映射文件 ,在 64 位操作系统中一般可以表示 20G 的数据文件,它的工作原理是直接利用操作系统的 Page 来实现文件到物理内存的直接映射。
  • 完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。
  • 通过 mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存),也不必关心内存的大小,有虚拟内存为我们兜底。

kafka的零拷贝是什么意思?

场景描述:把磁盘中的某个文件内容发送到远程服务器上,那么他必须经过几个拷贝过程

  1. 从磁盘中去读取目标文件的内容拷贝到内核缓冲区中
  2. 把内核缓冲区的数据拷贝到用户空间的缓冲区中
  3. 在应用程序中调用write()方法把用户空间缓冲区的数据拷贝到内核空间的socket Buffer中
  4. 把在内核模式下的socket Buffer中的数据赋值到网卡缓冲区,
  5. 最后网卡缓冲区再把数据传输到目标服务器上。

在这个过程中我们发现数据从磁盘到最终发送出去要经历4次拷贝,而在这4次拷贝过程中,有两次拷贝是浪费的,

1. 从内核空间拷贝到用户空间
2. 从用户空间再次拷贝到内核空间。

所谓的零拷贝就是把这两次多余的拷贝忽略掉。应用程序可以直接把磁盘中的数据从内核中直接传输到socket.

零拷贝是一种为了解决数据从内核缓存到用户缓存的CPU拷贝产生的性能消耗的技术。

原理:当数据从磁盘经过DMA copy到页缓存(内核缓存)后,为了减少CPU拷贝的性能损耗,操作系统会将该内核缓存与用户层进行共享,减少一次CPU copy过程,同时用户层的读写也会直接访问该共享存储,本身由用户层到Socket缓存的数据拷贝过程也变成了从 内核到内核的CPU拷贝过程,更加的快速。

磁盘文件 ==DMAcopy=> 【页缓存并共享作为用户空间缓存】 ==CPUcopy=> Socket缓存 ==DMAcopy=>> 网卡

kafka的顺序读写

  • 硬盘是机械结构,每次读写都会“寻址”,其中寻址是一个“机械动作”,是最耗时的;顺序I/O比随机I/O快,为了提高读写硬盘的速度,Kafka 就是使用顺序 I/O
  • 在顺序读写的情况下,磁盘的顺序读写速度和内存相差无几;

顺序写磁盘就是在一个磁道连续的写入,数据都排在一起,分布在连续的磁盘扇区,主需要一次寻址就能找到对应的数据,而kafka本身的数据是不需要删除数据的,是已追加的方式写到磁盘,所以这样就能保证磁盘数据连续紧凑,同时kafka是以segment log flie进行分段存储的,每次访问磁盘文件的时候只需要寻址最后一个segment file的磁盘空间,能够保证写入和读取的效率。

python init new metaclass

在python这个开发语言中同样也有很多特殊的方法,其中__new__是在实例创建之前被调用的,用于创建实例,然后返回该实例对象,是个静态方法。__init__是当实例对象创建完成后被调用的,用于初始化一个类实例,是个实例方法。

python init new 方法都是其构造方法。

__init__方法: init方法通常用在初始化一个类实例的时候.init其实不是实例化一个类的时候第一个被调用的方法。通常来实例化一个类时,最先被调用的方法,其实是new方法。

__new__: new方法接受的参数也是和init一样,但init是在类实例创建之后调用,而new方法也是创建这个类实例的方法

  • init通常用于初始化一个新实例,控制这个初始化的过程,比如添加一些属性,做一些额外的操作,发生在类实例被创建完成之后。它是实例级别的方法
  • new通常用于控制生成一个新实例的过程,他是类级别的方法。
class Animal(object):
    def __init__(self, name, color):
        print('__init__ called')
        self.name = name
        self.color = color

    def __new__(cls, *args, **kwargs):
        print('__new__ called')
        print(args)
        print(kwargs)
        return super(Animal, cls).__new__(cls)

    def __str__(self):
        return '<Animal %s %s>' % (self.name, self.color)

if __name__ == '__main__':
    dog = Animal('Dog', 'red')
    print(dog)

输出结果如下:

__new__ called
('Dog', 'red')
{}
__init__ called
<Animal Dog red>

new方法主要是当你继承一些不可变的class时,如int,str,tuple,提供给你一个自定义这些类的实例化过程的途径,还有就是实现自定义的metaclass。

还可以用new实现设计模式中的单例模式

class SingleModle(object):
    def __new__(cls):
        if not hasattr(cls, 'instance'):
            cls.instance = super(SingleModle, cls).__new__(cls)
        return cls.instance

if __name__ == '__main__':
    obj1 = SingleModle()
    obj2 = SingleModle()
    obj1.params = 'hello world'
    print(obj1.params)
    print(obj2.params)
    print(obj1 is obj2)

输出结果是

hello world
hello world
True

Metaclass 元类

  • 旧版本的class是来源一个built-in type叫做instance. 如果实例化一个class后得到obj, 那么obj.class会显示它来源于哪个class, 但是type(obj)是instance. 下面的例子env是Python2.7
    class Zoom:
    pass
    z = Zoom()
    print z.__class__
    print type(z)
    #__main__.Zoom
    #<type 'instance'>
  • 新版本的class联合了class和type的概念. 如果obj是一个新版本class的实例, 那么type(obj)和obj.class的返回结果是一样的. 在Python 3中, 所有class都是新版本的class. 有一句话, 那就是在Python中, everything is an object. Class本身也是object
    # 熟悉的built-in class也是type
    for t in int, float, dict, list, tuple:
    print(type(t))
    #<class 'type'>
    #<class 'type'>
    #<class 'type'>
    #<class 'type'>
    #<class 'type'>
    # type本身也是type
    print(type(type))
    #<class 'type'>
  • Zoom()创建了一个class Zoom的实例.
  • Zoom的父class中的call()会被启动, 因为Zoom是新版本的class, 所以它的父class就是type, 所以type的class()方法会被调用.
  • call()方法会调用new()以及init()方法.
  • 如果Zoom没有定义这两个方法, 会使用Zoom的祖先中的这两个方法.
  • 如果Zoom定义了这两个方法, 会覆盖祖先中的这两个方法
class Zoom:
    pass
z = Zoom()
print(z.__class__)
print(type(z))
print(type(Zoom))

#<class '__main__.Zoom'>
#<class '__main__.Zoom'>
#<class 'type'>
1. z是Zoom的实例
2. Zoom是type的实例
3. type是type的实例
  • type是一个metaclass, class是实例. 在Python 3中, 任何class都是type这个metaclass的实例

使用type定义class

  • 使用type创建class. 英文上说叫dynamically创建class
  • 用type创建class时有三个参数(, , )
    1. name指的就是class的名字, 它会变成class.name
    2. bases指的是这个class的祖先
    3. dct指的是class定义的一些method, attribute啥的
def f(self):
    print('in f and attr =', self.attr)
    # return self.attr

Zoom = type(
    'Zoom',
    (),
    {
        'attr': 100,
        'attr_val': f
    }
)

z=Zoom()
print(z.attr)
print(z.attr_val())

常规的定义方式

def fn(self, name = 'world'): #先定义一个函数
    print('Hello, %s' % name)

Hello = type('Hello', (object, ), dict(hello=fn)) #创建Hello class,传入class的名称,继承的父类集合class的方法名与函数绑定,这里我们把fn绑定到hello上

h = Hello()
h.hello()
print(type(Hello))
print(type(h))

#Hello, world
#<class 'type'>
#<class '__main__.Hello'>

type作为元类(Metaclass)被继承

Myclass要继承自type, MyClass这个class本身的创建也需要type, type中的一些方法也会在创建class时用到, 不想把type的所有方法都实现, 只是想做一些基于它的自定义. 这里定义了new这个方法, 我们会打印所有的attrs, 这个方法所需要的三个参数其实跟刚刚type中的三个参数是一样的.

class MyClass(type):
    def __new__(self, class_name, bases, attrs):
        modified = {}
        for name, value in attrs.items():
            if name.startswith("__"):
                modified[name] = value
            else:
                modified[name.upper()] = value
        return type(class_name, bases, modified)

class SonClass(metaclass=MyClass):
    color = "Red"
print(dir(SonClass))

vscode 指定anaconda方式

vscode是一种简化且高效的代码编辑器,同时支持诸如调试,任务执行和版本管理之类的开发操作。它的目标是提供一种快速的编码编译调试工具。然后将其余部分留给IDE。vscode集成了所有一款现代编辑器所应该具备的特性,包括语法高亮、可定制的热键绑定、括号匹配、以及代码片段收集等。

vscode是一种简化且高效的代码编辑器,同时支持诸如调试,任务执行和版本管理之类的开发操作。它的目标是提供一种快速的编码编译调试工具。然后将其余部分留给IDE。vscode集成了所有一款现代编辑器所应该具备的特性,包括语法高亮、可定制的热键绑定、括号匹配、以及代码片段收集等。

Anaconda是专注于数据分析、能够对包和环境进行管理的Python发行版本,包含了conda、Python等多个科学包及其依赖项。conda 是开源包(packages)和虚拟环境(environment)的管理系统

  • 先在本地环境中安装anaconda和vscode
  • vscode安装python插件,新版本的管理设置在左下方齿轮状的图片
  • win系统使用快捷键 CTRL+P的按钮打开搜索,然后输入:> select interpreter
  • Mac系统使用Command+p打开搜索

> select interpreter

⚠️注意: 向左箭头也是要输入的 关键字

  • 弹出如下页面后,请自行选择自己想要的anaconda环境,双击F5运行。