接触kubernetes两年有余,从18年初加入kubernetes社区来算,已经一年半,或许是时候写点什么。简单文章如何写好,难点文章如何写透,或许是一种学问,打算推一个系列:《Kubernetes GO》算是对这两年的一个总结。
kubelet组件涉及到eventfd相关概念,在此先作学习铺垫。
1、介绍
先介绍eventfd:
1 |
|
使用这个函数来创建一个事件对象,linux线程间通信为了提高效率,大多使用异步通信,采用事件监听和回调函数的方式来实现高效的任务处理方式(虽然会将逻辑变得复杂)。
linux内核会为这个事件对象维护一个64位的计数器(uint64_t).并在初始化时用传进去的initval来初始化这个计数器,然后返回一个文件描述符来代表这个事件对象。
第二个参数是描述这个事件对象的属性,可以设置为EFD_NONBLOCK , EFD_CLOEXEC;前面的是设置对象为非阻塞状态,如果没有设置为非阻塞状态,read系统调用来读这个计数器,且计数器的值为0时,就会一直阻塞在read系统调用上,反之如果设置了该标志位,就会返回EAGAIN错误。后面的EFD_CLOEXEC功能是在程序调用exec()函数族加载其他程序时自动关闭当前已有的文件描述符(具体为什么暂不解释)。
通过此函数得到的对象既然是一个计数器,我们就可以对它进行读和写:
使用write将缓冲区写入的8字节整形值加到内核计数器上。
使用read将内核计数的8字节值读取到缓冲区中,并把计数器重设为0,如果buffer的长度小于8字节则read会失败,错误码设为EINVAl。
再介绍epoll,忍不住的可以直接向下翻
epoll是对select,poll这种IO多路转接方式的改进
接口:
1 | int epoll_create(int intsize); |
工作模式:
- 水平触发:缺省的工作方式,并且同时支持block和no-blocksocket,在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表
- 边缘触发:高速工作方式,只支持no-blocksocket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了
用途:使用epoll_wait对某个文件描述符进行事件监听,监听到事件后会返回相关的结构体,得到其中有事件到来的fd,使用对应的回调函数(手动实现fd到回调函数的映射)来处理该fd上的事件:读数据或者写数据之类的。
下面进入使用场景:
原始做法:(会有bug,下面分析)
初始化:先生成一个eventfd,初始化计数器为0,此eventfd可以通过一些方法在下面两个线程间共享
线程A:处理一些来自外部的请求,每处理完一个请求后会向eventfd的计数器中写入处理的结果,是一个整型值,然后接着处理下一个请求。
线程B:对eventfd进行Epoll监听,回调函数的功能是对eventfd的计数器读数据出来并将结果进行分发。
用例1:外部单个客户端每隔1秒向线程A发送一个请求。
用例1结果:线程A正确处理请求,并将结果写入eventfd中,线程B及时从eventfd中读取出请求处理结果,并正确分发给其他线程。
用例2:外部单个客户端连续向线程A发送多个请求。
用例2结果:线程A正确处理请求,并正确地将结果写入eventfd中,但在一定概率的情况下,线程B从eventfd中读到的结果不是线程A一次写入的结果,而是多次写入的结果。因此不能正确的分发请求。线程B中epoll捕捉到的事件次数小于线程A写入产生的事件数量。
用例3:外部多个客户端同时向线程A发送一个请求
用例3结果:线程A正确处理请求,并正确的将结果写入eventfd中,在很大的概率情况下,线程B中eventfd中读到的结果不是线程A一次写入的结果,而是多次写入的结果。因此,也不能正确的分发请求。线程B中epoll捕捉到的事件次数小于线程A写入产生的事件数量。
BUG分析:在这个场景中,线程A和线程B分别相当于生产者和消费者,只从原始生产者消费者模型上看并没有问题,满足数据为空时读不到数据,数据满时写不进数据(read,write的功能),但是在当前场景中,加了一个特别的要求:每次写入的数据应该可以被独立识别而不是累加,每次写入的事件也应该被epoll独立的捕捉到。因此,需要对事件和数据各自进行序列化上的拆分。
改进做法:
初始化:先生成一个eventfd,初始化计数器为1,再生成一个空队列Q和互斥锁,此eventfd,队列Q和互斥锁可以通过一些方法在下面两个线程间共享,
线程A:处理一些来自外部的请求,每处理完一个请求后会从eventfd的计数器read数据,加1之后再write,将处理结果写入到队列末尾,然后接着处理下一个请求。
线程B:对eventfd进行Epoll监听,回调函数的功能是对eventfd的计数器read数据出来然后判断,如果大于1就自减1然后从队列头部取出数据,并将结果进行分发
,最后再写入新的计数器数据。如果等于1那么就直接返回,代表没有新的数据到来。
用例1,2,3在此环境下均可正常跑通。
回过头来分析原始做法的fatal error在哪:
作为生产者的线程A没有向线程B解释自己向eventfd中写入了多少个数据,产生了多少次事件。
作为消费者的线程B一次read就把eventfd中所有的数据当做一个数据读了出来,却没有相关依据来对读出来的数据做拆分。
作为通信工具的eventfd只能将数据进行累加,起到计数器的作用而不能存储实际数据。
作为消息监听的epoll在水平触发模式下只能通知是否有事件而不能通知有多少事件,在边缘触发下不能保留每次事件的产生都能及时被消费者捕获到。
因此,改进做法是将事件的多少通过计数器来表达,将实际传输的数据通过FIFO队列来传达。
后续
该文为kubelet相关内容的铺垫,涉及到kubelet对于oom killer的使用。