谈谈Linux epoll惊群问题的原因和解决方案

2023-11-04

近期排查了一个问题,epoll惊群的问题,起初我并不认为这是惊群导致,因为从现象上看,只是体现了CPU不均衡。一共fork了20个Server进程,在请求负载中等的时候,有三四个Server进程呈现出比较高的CPU利用率,其余的Server进程的CPU利用率都是非常低。

中断,软中断都是均衡的,网卡RSS和CPU之间进行了bind之后依然如故,既然系统层面查不出个所以然,只能从服务的角度来查了。

自上而下的排查首先就想到了strace,没想到一下子就暴露了原形:

accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)

如果仅仅strace accept,即加上“-e trace=accept”参数的话,偶尔会有accept成功的现象:

accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, {sa_family=AF_INET, sin_port=htons(39306), sin_addr=inet_addr("172.16.1.202")}, [16]) = 19
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)
accept(4, 0x9ecd930, [16])              = -1 EAGAIN (Resource temporarily unavailable)

大量的CPU空转,进一步加大请求负载,CPU空转明显降低,这说明在预期的空转期间,新来的请求降低了空转率…现象明显偏向于这就是惊群导致的之判断!

简单介绍惊群和事件模型

关于什么是惊群,这里不再做概念上的解释。

惊群问题一般出现在那些web服务器上,曾经Linux系统有个经典的accept惊群问题困扰了大家非常久的时间,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理,这是通过以下的代码来实现的:

    list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
        unsigned flags = curr->flags;
        if (curr->func(curr, mode, wake_flags, key) &&
                (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
            break;
    }

是的,添加了一个WQ_FLAG_EXCLUSIVE标记,告诉内核进行排他性的唤醒,即唤醒一个进程后即退出唤醒的过程,问题得以解决。

然而,没有哪个web服务器会傻到多个进程直接阻塞在accept上准备接收请求,在更高层次上,多路复用的需求让select,poll,epoll等事件模型更为受到欢迎,所谓的事件模型即阻塞在事件上而不是阻塞在事务上。内核仅仅通知发生了某件事,具体发生了什么事,则有处理进程或者线程自己来poll。如此一来,这个事件模型(无论其实现是select,poll,还是epoll)便可以一次搜集多个事件,从而满足多路复用的需求。

好了,基本原理就介绍到这里,下面我将来详细谈一下Linux epoll中的惊群问题,我们知道epoll在实际中要比直接accept实用性强很多,据我所知,除非编程学习或者验证性小demo,几乎没有直接accept的代码,所有的线上代码几乎都使用了事件模型。然而由于select,poll没有可扩展性,存在O(n)O(n)问题,因此在带宽越来越高,服务器性能越来越强的趋势下,越来越多的代码将收敛到使用epoll的情形,所以有必要对其进行深入的讨论。

相关视频推荐

从“惊群”问题来看 高并发锁方案

6种epoll的设计方法(单线程epoll、多线程epoll、多进程epoll)及每种epoll的应用场景

网络原理tcp/udp,网络编程epoll/reactor,面试中正经“八股文”

学习地址:c/c++ linux服务器开发/后台架构师

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

 

Linux epoll惊群问题

某网站上有一个问题:

3.x内核源码eventpoll.c中已经有如下代码,那为什么还是会发生惊群?
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
           int maxevents, long timeout)
{
....
        init_waitqueue_entry(&wait, current);
        __add_wait_queue_exclusive(&ep->wq, &wait); // **NOTICE**
....
}

下面我来就这个问题给一个答案,这也是我自己思考的答案:

在ep_poll的睡眠中加入WQ_FLAG_EXCLUSIVE标记,确实实实在在解决了epoll的惊群问题

epoll_wait返回后确实也还有多个进程被唤醒只有一个进程能正确处理其他进程无事可做的情况发生,但这不是因为惊群,而是你的使用方法不对。

What?使用方法不对?

是的,使用方法不对。若想了解Why,则必须对epoll的实现细节以及其对外提供的API的语义有充分的理解,接下来我们就循着这个思路来撸个所以然。请继续阅读。

Linux epoll的实现机制

说起实现原理,很多人喜欢撸源码分析,我并不喜欢,我认为源码是自己看看就行了,搞这个行业的能看懂代码是一个最最基本的能力,我比较在意的是对某种机制内在逻辑的深入理解,而这个通过代码是体现不出来的,我一般会做下面几件事:

  • 运行起来并测得预期的数据
  • 看懂代码并画出原理图
  • 自己重新实现一版(时间精力允许的情况下)
  • 写个demo验证一些具体逻辑细节

下面是我总结的一张关于Linux epoll的原理图:

 要说代码实现上,其实也比较简单,大致有以下的几个逻辑:

  1. 创建epoll句柄,初始化相关数据结构
  2. 为epoll句柄添加文件句柄,注册睡眠entry的回调
  3. 事件发生,唤醒相关文件句柄睡眠队列的entry,调用其回调
  4. 唤醒epoll睡眠队列的task,搜集并上报数据

来,一个一个说

1.创建epoll句柄,初始化相关数据结构

这里主要就是创建一个epoll文件描述符,注意,后面操作epoll的时候,就是用这个epoll的文件描述符来操作的,所以这就是epoll的句柄,精简过后的epoll结构如下:

struct eventpoll {
// 阻塞在epoll_wait的task的睡眠队列
wait_queue_head_t wq;
// 存在就绪文件句柄的list,该list上的文件句柄事件将会全部上报给应用
struct list_head rdllist;
// 存放加入到此epoll句柄的文件句柄的红黑树容器
struct rb_root rbr;
// 该epoll结构对应的文件句柄,应用通过它来操作该epoll结构
struct file *file;
};

2.为epoll句柄添加文件句柄,注册睡眠entry的回调

这个步骤中其实有两个子步骤:

1). 添加文件句柄

将一个文件句柄,比如socket添加到epoll的rbr红黑树容器中,注意,这里的文件句柄最终也是一个包装结构,和epoll的结构体类似:

struct epitem {
// 该字段链接入epoll句柄的红黑树容器
struct rb_node rbn;
// 当该文件句柄有事件发生时,该字段链接入“就绪链表”,准备上报给用户态
struct list_head rdllink;
// 该字段封装实际的文件,我已经将其展开
struct epoll_filefd {
struct file *file;
int fd;
} ffd;
// 反向指向其所属的epoll句柄
struct eventpoll *ep;
};

以上结构实例就是epi,将被添加到epoll的rbr容器中的逻辑如下:

struct eventpoll *ep = 待加入文件句柄所属的epoll句柄;
struct file *tfile = 待加入的文件句柄file结构体;
int fd = 待加入的文件描述符ID;
struct epitem *epi = kmem_cache_alloc(epi_cache, GFP_KERNEL);
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;
ep_set_ffd(&epi->ffd, tfile, fd);
...
ep_rbtree_insert(ep, epi);

2). 注册睡眠entry回调并poll文件句柄

在第一个子步骤的代码逻辑中,我有一段“…”省略掉了,这部分比较关键,所以我单独抽取了出来作为第二个子步骤。

我们知道,Linux内核的sleep/wakeup机制非常重要,几乎贯穿了所有的内核子系统,值得注意的是,这里的sleep/wakeup依然采用了OO的思想,并没有限制睡眠的entry一定要是一个task,而是将睡眠的entry做了一层抽象,即:

struct __wait_queue {
unsigned int flags;
// 至于这个private到底是什么,内核并不限制,显然,它可以是task,也可以是别的。
void *private;
wait_queue_func_t func;
struct list_head task_list;
};

以上的这个entry,最终要睡眠在下面的数据结构实例化的一个链表上:

struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};

显然,在这里,一个文件句柄均有自己睡眠队列用于等待自己发生事件的entry在没有发生事件时来歇息,对于TCP socket而言,该睡眠队列就是其sk_wq,通过以下方式取到:

static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
return &rcu_dereference_raw(sk->sk_wq)->wait;
}

我们需要一个entry将来在发生事件的时候从上述wait_queue_head_t中被唤醒,执行特定的操作,即将自己放入到epoll句柄的“就绪链表”中。下面的函数可以完成该逻辑的框架:

// 此处的whead就是上面例子中的sk_sleep返回的wait_queue_head_t实例。
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
poll_table *pt)
{
struct epitem *epi = ep_item_from_epqueue(pt);
struct eppoll_entry *pwq;
if (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL)) {
// 发生事件即调用ep_poll_callback回调函数,该回调函数会将自己这个epitem加入到epoll的“就绪链表”中去。
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
// 是否排他唤醒取决于用户的配置,有些IO是希望唤醒所有entry来处理,有些则不必。注意,这里是针对文件句柄IO而言的,并不是针对epoll句柄的。
if (epi->event.events & EPOLLEXCLUSIVE)
add_wait_queue_exclusive(whead, &pwq->wait);
else
add_wait_queue(whead, &pwq->wait);
}
}

至于说什么时候调用上面的函数,Linux的poll机制仍然是采用了分层抽象的思想,即上述函数会作为另一个回调在相关文件句柄的poll函数中被调用。即:

static inline unsigned int ep_item_poll(struct epitem *epi, poll_table *pt)
{
pt->_key = epi->event.events;
return epi->ffd.file->f_op->poll(epi->ffd.file, pt) & epi->event.events;
}

对于TCP socket而言,其file_operations的poll回调即:

unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
unsigned int mask;
struct sock *sk = sock->sk;
const struct tcp_sock *tp = tcp_sk(sk);
// 此函数会调用poll_wait->wait._qproc
// 而wait._qproc就是ep_ptable_queue_proc
sock_poll_wait(file, sk_sleep(sk), wait);
...
}

现在,我们可以把子步骤1中的逻辑补全了:

struct eventpoll *ep = 待加入文件句柄所属的epoll句柄;
struct file *tfile = 待加入的文件句柄file结构体;
int fd = 待加入的文件描述符ID;
struct epitem *epi = kmem_cache_alloc(epi_cache, GFP_KERNEL);
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;
ep_set_ffd(&epi->ffd, tfile, fd);
// 这里会将wait._qproc初始化成ep_ptable_queue_proc
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
// 这里会调用wait._qproc即ep_ptable_queue_proc,安排entry的回调函数ep_poll_callback,并将entry“睡眠”在socket的sk_wq这个睡眠队列上。
revents = ep_item_poll(epi, &epq.pt);
ep_rbtree_insert(ep, epi);
// 如果刚才的ep_item_poll取出了事件,随即将该item挂入“就绪队列”中,并且wakeup阻塞在epoll_wait系统调用中的task!
if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
}

3.事件发生,唤醒相关文件句柄睡眠队列的entry,调用其回调

上面已经很详细地描述了epoll的基础设施了,现在我们假设一个TCP Listen socket上来了一个连接请求,已经完成了三次握手,内核希望通知epoll_wait返回,然后去取accept。

内核在wakeup这个socket的sk_wq时,最终会调用到ep_poll_callback回调,这个函数我们说了好几次了,现在看看它的真面目:

static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
unsigned long flags;
struct epitem *epi = ep_item_from_wait(wait);
struct eventpoll *ep = epi->ep;
// 这个lock比较关键,操作“就绪链表”相关的,均需要这个lock,以防丢失事件。
spin_lock_irqsave(&ep->lock, flags);
// 如果发生的事件我们并不关注,则不处理直接返回即可。
if (key && !((unsigned long) key & epi->event.events))
goto out_unlock;
// 实际将发生事件的epitem加入到“就绪链表”中。
if (!ep_is_linked(&epi->rdllink)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
}
// 既然“就绪链表”中有了新成员,则唤醒阻塞在epoll_wait系统调用的task去处理。注意,如果本来epi已经在“就绪队列”了,这里依然会唤醒并处理的。
if (waitqueue_active(&ep->wq)) {
wake_up_locked(&ep->wq);
}
out_unlock:
spin_unlock_irqrestore(&ep->lock, flags);
...
}

没什么好多说的。现在“就绪链表”已经有epi了,接下来就要唤醒epoll_wait进程去处理了。

4.唤醒epoll睡眠队列的task,搜集并上报数据

这个逻辑主要集中在ep_poll函数,精简版如下:

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents, long timeout)
{
unsigned long flags;
wait_queue_t wait;
// 当前没有事件才睡眠
if (!ep_events_available(ep)) {
init_waitqueue_entry(&wait, current);
__add_wait_queue_exclusive(&ep->wq, &wait);
for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
...// 例行的schedule timeout
}
__remove_wait_queue(&ep->wq, &wait);
set_current_state(TASK_RUNNING);
}
// 往用户态上报事件,即那些epoll_wait返回后能获取的事件。
ep_send_events(ep, events, maxevents);
}

其中关键在ep_send_events,这个函数实现了非常重要的逻辑,包括LT和ET的逻辑,我不打算深入去解析这个函数,只是大致说下流程:

ep_scan_ready_list()
{
// 遍历“就绪链表”
ready_list_for_each() {
// 将epi从“就绪链表”删除
list_del_init(&epi->rdllink);
// 实际获取具体的事件。
// 注意,睡眠entry的回调函数只是通知有“事件”,具体需要每一个文件句柄的特定poll回调来获取。
revents = ep_item_poll(epi, &pt);
if (revents) {
if (__put_user(revents, &uevent->events) ||
__put_user(epi->event.data, &uevent->data)) {
// 如果没有完成,则将epi重新加回“就绪链表”等待下次。
list_add(&epi->rdllink, head);
return eventcnt ? eventcnt : -EFAULT;
}
// 如果是LT模式,则无论如何都会将epi重新加回到“就绪链表”,等待下次重新再poll以确认是否仍然有未处理的事件。这也符合“水平触发”的逻辑,即“只要你不处理,我就会一直通知你”。
if (!(epi->event.events & EPOLLET)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
}
}
}
// 如果“就绪链表”上仍有未处理的epi,且有进程阻塞在epoll句柄的睡眠队列,则唤醒它!(这将是LT惊群的根源)
if (!list_empty(&ep->rdllist)) {
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
}
}

这里的代码逻辑的分析过程就到此为止了。以对这个代码逻辑的充分理解为基础,接下来我们就可以看具体的问题细节了。

下面一小节先从LT(水平触发模式)以及ET(即边沿触发模式)开始。

epoll的LT和ET以及相关细节问题

简单点解释:

  • LT水平触发

如果事件来了,不管来了几个,只要仍然有未处理的事件,epoll都会通知你。

  • ET边沿触发

如果事件来了,不管来了几个,你若不处理或者没有处理完,除非下一个事件到来,否则epoll将不会再通知你。

理解了上面说的两个模式,便可以很明确地展示可能会遇到的问题以及解决方案了,这将非常简单。

LT水平触发模式的问题以及解决

下面是epoll使用中非常常见的代码框架,我将问题注释于其中:

// 否则会阻塞在IO系统调用,导致没有机会再epoll
set_socket_nonblocking(sd);
epfd = epoll_create(64);
event.data.fd = sd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sd, &event);
while (1) {
epoll_wait(epfd, events, 64, xx);
... // 危险区域!如果有共享同一个epfd的进程/线程调用epoll_wait,它们也将会被唤醒!
// 这个accept将会有多个进程/线程调用,如果并发请求数很少,那么将仅有几个进程会成功:
// 1. 假设accept队列中有n个请求,则仅有n个进程能成功,其它将全部返回EAGAIN (Resource temporarily unavailable)
// 2. 如果n很大(即增加请求负载),虽然返回EAGAIN的比率会降低,但这些进程也并不一定取到了epoll_wait返回当下的那个预期的请求。
csd = accept(sd, &in_addr, &in_len);
...
}

这一切为什么会发生?

我们结合理论和代码一起来分析。

再看一遍LT的描述“如果事件来了,不管来了几个,只要仍然有未处理的事件,epoll都会通知你。”,显然,epoll_wait刚刚取到事件的时候的时候,不可能马上就调用accept去处理,事实上,逻辑在epoll_wait函数调用的ep_poll中还没返回的,这个时候,显然符合“仍然有未处理的事件”这个条件,显然这个时候为了实现这个语义,需要做的就是通知别的同样阻塞在同一个epoll句柄睡眠队列上的进程!在实现上,这个语义由两点来保证:

保证1:在LT模式下,“就绪链表”上取出的epi上报完事件后会重新加回“就绪链表”;

保证2:如果“就绪链表”不为空,且此时有进程阻塞在同一个epoll句柄的睡眠队列上,则唤醒它。

ep_scan_ready_list()
{
// 遍历“就绪链表”
ready_list_for_each() {
list_del_init(&epi->rdllink);
revents = ep_item_poll(epi, &pt);
// 保证1
if (revents) {
__put_user(revents, &uevent->events);
if (!(epi->event.events & EPOLLET)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
}
}
}
// 保证2
if (!list_empty(&ep->rdllist)) {
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
}
}

我们来看一个情景分析。

假设LT模式下有10个进程共享同一个epoll句柄,此时来了一个请求client进入到accept队列,我们发现上述的1和2是一个循环唤醒的过程:

1).假设进程a的epoll_wait首先被ep_poll_callback唤醒,那么满足1和2,则唤醒了进程B;

2).进程B在处理ep_scan_ready_list的时候,发现依然满足1和2,于是唤醒了进程C….

3).上面1)和2)的过程一直到之前某个进程将client取出,此时下一个被唤醒的进程在ep_scan_ready_list中的ep_item_poll调用中将得不到任何事件,此时便不会再将该epi加回“就绪链表”了,LT水平触发结束,结束了这场悲伤的梦!

问题非常明确了,但是怎么解决呢?也非常简单,让不同进程的epoll_waitI调用互斥即可。

但是且慢!

上面的情景分析所展示的是一个“惊群效应”吗?其实并不是!对于Listen socket,当然要避免这种情景,但是对于很多其它的I/O文件句柄,说不定还指望着大家一起来read数据呢…所以说,要说互斥也仅仅要针对Listen socket的epoll_wait调用而言。

换句话说,这里epoll LT模式下有进程被不必要唤醒,这一点并不是内核无意而为之的,内核肯定是知道这件事的,这个并不像之前accept惊群那样算是内核的一个缺陷。epoll LT模式只是提供了一种模式,误用这种模式将会造成类似惊群那样的效应。但是不管怎么说,为了讨论上的方便,后面我们姑且将这种效应称作epoll LT惊群吧。

除了epoll_wait互斥之外,还有一种解决问题的方案,即使用ET边沿触发模式,但是会遇到新的问题,我们接下来来描述。

ET边沿触发模式的问题以及解决

ET模式不满足上述的“保证1”,所以不会将已经上报事件的epi重新链接回“就绪链表”,也就是说,只要一个“就绪队列”上的epi上的事件被上报了,它就会被删除出“就绪队列”。

由于epi entry的callback即ep_poll_callback所做的事情仅仅是将该epi自身加入到epoll句柄的“就绪链表”,同时唤醒在epoll句柄睡眠队列上的task,所以这里并不对事件的细节进行计数,比如说,如果ep_poll_callback在将一个epi加入“就绪链表”之前发现它已经在“就绪链表”了,那么就不会再次添加,因此可以说,一个epi可能pending了多个事件,注意到这点非常重要!

一个epi上pending多个事件,这个在LT模式下没有任何问题,因为获取事件的epi总是会被重新添加回“就绪链表”,那么如果还有事件,在下次check的时候总会取到。然而对于ET模式,仅仅将epi从“就绪链表”删除并将事件本身上报后就返回了,因此如果该epi里还有事件,则只能等待再次发生事件,进而调用ep_poll_callback时将该epi加入“就绪队列”。这意味着什么?

这意味着,应用程序,即epoll_wait的调用进程必须自己在获取事件后将其处理干净后方可再次调用epoll_wait,否则epoll_wait不会返回,而是必须等到下次产生事件的时候方可返回。即,依然以accept为例,必须这样做:

// 否则会阻塞在IO系统调用,导致没有机会再epoll
set_socket_nonblocking(sd);
epfd = epoll_create(64);
event.data.fd = sd;
// 添加ET标记
event.events |= EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, sd, &event);
while (1) {
epoll_wait(epfd, events, 64, xx);
while ((csd = accept(sd, &in_addr, &in_len)) > 0) {
do_something(...);
}
...
}

好了,解释完了。

以上就是epoll的LT,ET相关的两个问题和解决方案。接下来的一节,我将用一个小小的简单Demo来重现上面描述的理论和代码。

测试demo

是时候给出一个实际能run的代码了:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netdb.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <time.h>
#include <signal.h>
#define COUNT 1
int mode = 0;
int slp = 0;
int pid[COUNT] = {0};
int count = 0;
void server(int epfd)
{
struct epoll_event *events;
int num, i;
struct timespec ts;
events = calloc(64, sizeof(struct epoll_event));
while (1) {
int sd, csd;
struct sockaddr in_addr;
num = epoll_wait(epfd, events, 64, -1);
if (num <= 0) {
continue;
}
/*
ts.tv_sec = 0;
ts.tv_nsec = 1;
if(nanosleep(&ts, NULL) != 0) {
perror("nanosleep");
exit(1);
}
*/
// 用于测试ET模式下丢事件的情况
if (slp) {
sleep(slp);
}
sd = events[0].data.fd;
socklen_t in_len = sizeof(in_addr);
csd = accept(sd, &in_addr, &in_len);
if (csd == -1) {
// 打印这个说明中了epoll LT惊群的招了。
printf("shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:%d\n", getpid());
continue;
}
// 本进程一共成功处理了多少个请求。
count ++;
printf("get client:%d\n", getpid());
close(csd);
}
}
static void siguser_handler(int sig)
{
// 在主进程被Ctrl-C退出的时候,每一个子进程均要打印自己处理了多少个请求。
printf("pid:%d count:%d\n", getpid(), count);
exit(0);
}
static void sigint_handler(int sig)
{
int i = 0;
// 给每一个子进程发信号,要求其打印自己处理了多少个请求。
for (i = 0; i < COUNT; i++) {
kill(pid[i], SIGUSR1);
}
}
int main (int argc, char *argv[])
{
int ret = 0;
int listener;
int c = 0;
struct sockaddr_in saddr;
int port;
int status;
int flags;
int epfd;
struct epoll_event event;
if (argc < 4) {
exit(1);
}
// 0为LT模式,1为ET模式
mode = atoi(argv[1]);
port = atoi(argv[2]);
// 是否在处理accept之前耽搁一会儿,这个参数更容易重现问题
slp = atoi(argv[3]);
signal(SIGINT, sigint_handler);
listener = socket(PF_INET, SOCK_STREAM, 0);
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = INADDR_ANY;
bind(listener, (struct sockaddr*)&saddr, sizeof(saddr));
listen(listener, SOMAXCONN);
flags = fcntl (listener, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl (listener, F_SETFL, flags);
epfd = epoll_create(64);
if (epfd == -1) {
perror("epoll_create");
abort();
}
event.data.fd = listener;
event.events = EPOLLIN;
if (mode == 1) {
event.events |= EPOLLET;
} else if (mode == 2) {
event.events |= EPOLLONESHOT;
}
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listener, &event);
if (ret == -1) {
perror("epoll_ctl");
abort();
}
for(c = 0; c < COUNT; c++) {
int child;
child = fork();
if(child == 0) {
// 安装打印count值的信号处理函数
signal(SIGUSR1, siguser_handler);
server(epfd);
}
pid[c] = child;
printf("server:%d pid:%d\n", c+1, child);
}
wait(&status);
sleep(1000000);
close (listener);
}

编译之,为a.out。

测试客户端选用了简单webbench,首先我们看一下LT水平触发模式下的问题:

[zhaoya@shit ~/test]$ sudo ./a.out 0 112 0
server:1 pid:9688
server:2 pid:9689
server:3 pid:9690
server:4 pid:9691
server:5 pid:9692
server:6 pid:9693
server:7 pid:9694
server:8 pid:9695
server:9 pid:9696
server:10 pid:9697

另起一个终端运行webbench,并发10,测试5秒:

[zhaoya@shit ~/test]$ webbench -c 10 -t 5 http://127.0.0.1:112/
Webbench - Simple Web Benchmark 1.5
Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
Benchmarking: GET http://127.0.0.1:112/
10 clients, running 5 sec.

而a.out的终端有以下输出:

...
get client:9690
get client:9688
get client:9691
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9693
get client:9692
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9689
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9697
get client:9691
get client:9696
get client:9690
get client:9690
get client:9695
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9697
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9689
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9692
get client:9696
get client:9688
get client:9695
get client:9693
get client:9689
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9691
get client:9695
get client:9691
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:9692
get client:9690
get client:9694
get client:9693
...

所有的“shit 
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:”的行均是被epoll LT惊群不必要唤醒的进程打印的。

接下来用ET模式运行:

[zhaoya@shit ~/test]$ sudo ./a.out 1 112 0

对应的输出如下:

...
get client:14462
get client:14462
get client:14464
get client:14464
get client:14462
get client:14462
get client:14467
get client:14469
get client:14468
get client:14468
get client:14464
get client:14467
get client:14467
get client:14469
get client:14469
get client:14469
get client:14464
get client:14464
get client:14466
get client:14466
get client:14469
get client:14469
...

没有任何一行是shit,即没有被不必要唤醒的惊群现象发生。

以上两个case确认了epoll LT模式的惊群效应是可以通过改用ET模式来解决的,接下来我们确认ET模式非循环处理会丢失事件。

用ET模式运行a.out,这时将slp参数设置为1,即在epoll_wait返回和实际accept之间耽搁1秒,这样可以让一个epi在被加入到“就绪链表”中之后,在其被实际accept处理之前,积累更多的未决事件,即未处理的请求,而我们实验的目的则是,epoll ET会丢失这些事件。

webbench的参数依然如故,a.out的输出如下:

[zhaoya@shit ~/test]$ sudo ./a.out 1 114 1
server:1 pid:31161
server:2 pid:31162
server:3 pid:31163
server:4 pid:31164
server:5 pid:31165
server:6 pid:31166
server:7 pid:31167
server:8 pid:31168
server:9 pid:31169
server:10 pid:31170
get client:31170
get client:31170
get client:31167
get client:31169
get client:31166
get client:31165
get client:31170
get client:31167
get client:31169
get client:31165
get client:31168
get client:31170
get client:31167
get client:31165
get client:31169
get client:31170
get client:31167
get client:31169
get client:31170
get client:31167
get client:31169
^Cpid:31170 count:6
pid:31169 count:5
pid:31163 count:0
pid:31168 count:1
pid:31167 count:5
pid:31165 count:3
pid:31166 count:1
pid:31161 count:0
pid:31162 count:0
pid:31164 count:0
User defined signal 1

同样的webbench参数,仅仅处理了十几个请求,可见大多数都丢掉了。如果我们用LT模式,同样在sleep 1秒导致事件挤压的情况下,是不是会多处理一些呢?我们的预期应该是肯定的,因为LT模式在事件被处理完之前,会一直促使epoll_wait返回继续处理,那么让我们试一下:

[zhaoya@shit ~/test]$ sudo ./a.out 0 115 1
server:1 pid:363
server:2 pid:364
server:3 pid:365
server:4 pid:366
server:5 pid:367
server:6 pid:368
server:7 pid:369
server:8 pid:370
server:9 pid:371
server:10 pid:372
get client:372
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:371
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:365
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:366
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:363
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:367
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:369
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:364
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:368
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:370
get client:370
get client:364
get client:367
get client:368
get client:369
get client:365
get client:371
get client:372
get client:363
get client:366
get client:370
get client:367
get client:364
get client:369
get client:371
get client:368
get client:366
get client:363
get client:365
get client:372
get client:370
get client:367
get client:364
get client:371
get client:369
get client:366
get client:368
get client:363
get client:365
get client:372
get client:370
get client:367
get client:371
get client:364
get client:369
get client:366
get client:365
get client:368
get client:363
get client:372
get client:370
get client:364
get client:371
get client:367
get client:366
get client:369
get client:365
get client:363
get client:368
get client:372
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:371
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:370
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:364
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:367
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:366
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:369
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:365
shit xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:363
^Cpid:363 count:5
pid:368 count:5
pid:372 count:6
pid:369 count:5
pid:366 count:5
pid:370 count:5
pid:367 count:5
pid:371 count:5
pid:365 count:5
pid:364 count:5
User defined signal 1

是的,多处理了很多,但是出现了LT惊群,这也是意料之中的事。

最后,让我们把这个Demo代码小改一下,改成循环处理,依然采用ET模式,sleep 1秒,看看情况会怎样。修改后的代码如下:

void server(int epfd)
{
struct epoll_event *events;
int num, i;
struct timespec ts;
events = calloc(64, sizeof(struct epoll_event));
while (1) {
int sd, csd;
struct sockaddr in_addr;
num = epoll_wait(epfd, events, 64, -1);
if (num <= 0) {
continue;
}
if (slp)
sleep(slp);
sd = events[0].data.fd;
socklen_t in_len = sizeof(in_addr);
// 这里循环处理,一直到空。
while ((csd = accept(sd, &in_addr, &in_len)) > 0) {
count ++;
printf("get client:%d\n", getpid());
close(csd);
}
}
}

改完代码后,再做同样参数的测试,结果大大不同:

[zhaoya@shit ~/test]$ sudo ./a.out 0 116 1
...
get client:3640
get client:3645
get client:3640
get client:3641
get client:3641
get client:3641
^Cpid:3642 count:14
pid:3647 count:33531
pid:3646 count:21824
pid:3648 count:22
pid:3644 count:32219
pid:3645 count:94449
pid:3641 count:8
pid:3640 count:85385
pid:3643 count:13
pid:3639 count:10
User defined signal 1

可以看到,大多数的请求都得到了处理,同样的逻辑,epoll_wait返回后的循环读和一次读结果显然不同。

问题和解决方案都很明确了,可以结单了吗?我想是的,但是在终结这个话题之前,我还想说一些结论性的东西以供备忘和参考。

结论

曾经,为了实现并发服务器,出现了很多的所谓范式,比如下面的两个很常见:

  1. 范式1:设置多个IP地址,多个IP地址同时侦听相同的端口,前端用4层负载均衡或者反向代理来对这些IP地址进行请求分发;
  2. 范式2:Master进程创建一个Listen socket,然后fork出来N个worker进程,这N个worker进程同时侦听这个socket。

第一个范式与本文讲的epoll无关,更多的体现一种IP层的技术,这里不谈,这里仅仅说一下第二个范式。

为了保证元组的唯一性以及处理的一致性,很长时间以来对于服务器而言,是不允许bind同一个IP地址和端口对的。然而为了可以并发处理多个连接请求,则必须采用某种多处理的方式,为了多个进程可以同时侦听同一个IP地址端口对,便出现了create listener+fork这种模型,具体来讲就是:

sd = create_listen_socket();
for (i = 0; i < N; i++) {
if (fork() == 0) {
// 继承了父进程的文件描述符
server(sd);
}
}

然而这种模式仅仅是做到了进程级的可扩展性,即一个进程在忙时,其它进程可以介入帮忙处理,底层的socket句柄其实是同一个!简单点说,这是一个沙漏模型:

这种模型在处理同一个socket的时候,必须互斥,同时内核必须防止潜在的惊群效应,因为互斥的要求,有且仅有一个进程可以处理特定的请求。这就对编程造成了极大的干扰。

以本文所描述的case为例,如果不清楚epoll LT模式和ET模式潜在的问题,那么就很容易误用epoll导致比较令人头疼的后果。

非常幸运,reuseport出现后,模型彻底变成了桶状:

 于是乎,使用了reuseport,一切都变得明朗了:

不再依赖mem模型

不再担心惊群

为什么reuseport没有惊群?首先我们要知道惊群发生的原因,就是同时唤醒了多个进程处理一个事件,导致了不必要的CPU空转。为什么会唤醒多个进程,因为发生事件的文件描述符在多个进程之间是共享的。而reuseport呢,侦听同一个IP地址端口对的多个socket本身在socket层就是相互隔离的,在它们之间的事件分发是TCP/IP协议栈完成的,所以不会再有惊群发生。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

谈谈Linux epoll惊群问题的原因和解决方案 的相关文章

  • 当用户按下打印时运行脚本,并且在脚本结束之前不开始假脱机(linux,cups)

    我需要做的是结合用户按下打印来执行 python 程序 脚本 并且在该程序退出之前不要让打印作业假脱机 原因是打印驱动程序不是开源的 我需要更改用户设置 在本例中是部门 ID 和密码 通常是每个用户 但因为这是一个信息亭 具有相同帐户的不同
  • ADB TCPIP 连接问题

    我有两台 Galaxy S3 其中一个已扎根 另一个则未扎根 因此 当我尝试通过本地网络连接它们时 计算机可以看到已root的计算机 但是正常的就卡在tcpip这一步了 所以 我写 adb tcpip 5555 It says restar
  • 如何从 PROC 获取有关子进程的信息

    我正在尝试编写一个以几个进程作为参数的程序 然后父进程执行每个子进程并打印出一些相关的统计信息 示例 generate ls l 将生成一个程序 打印出有关 ls l 的一些统计信息 特别是其系统时间 用户时间和上下文切换次数 我不想使用
  • 将 C++ TCP/IP 应用程序从 IPv4 转换为 IPv6。难的?值得这么麻烦吗?

    多年来 我使用 WinSock 为 Windows 开发了少量 C 服务器 客户端应用程序 路由器 Web 邮件 FTP 服务器等 等等 我开始越来越多地考虑创建这些应用程序的 IPv6 版本 当然 同时也保留原始的 IPv4 版本 问题
  • linux命令中括号的用途是什么[重复]

    这个问题在这里已经有答案了 我在 Linux 终端中运行以下命令 谁能告诉我 Linux 终端中括号和以下命令的用途是什么 echo GET HTTP 1 0 echo 主机 www google com echo 数控 www googl
  • 捕获数据包后会发生什么?

    我一直在阅读关于网卡捕获数据包后会发生什么的内容 我读得越多 我就越困惑 首先 我读过传统上 在网卡捕获数据包后 它会被复制到内核空间中的一个内存块 然后复制到用户空间 供随后处理数据包数据的任何应用程序使用 然后我读到了 DMA 其中 N
  • 码头无故停止

    我需要经验丰富的码头用户的建议 我在负载均衡器 亚马逊云 后面维护着 2 台 Linux 机器 使用 Jetty 9 0 3 有时我的 Jetty 容器会被 Thread 2 无故关闭 同时地 显示以下日志并且容器无故停止 没有错误 没有例
  • 从 php/linux 获取 pdf 的布局模式(横向或纵向)

    给定一个 PDF 如何使用 PHP lib 或 Linux 命令行工具获取 PDF 的布局模式 或相对宽度 高度 Using http www tecnick com public code cp dpage php aiocp dp tc
  • Fortran 中的共享库,最小示例不起作用

    我试图了解如何在 Linux 下的 Fortran 中动态创建和链接共享库 我有两个文件 第一个 liblol f90 看起来像这样 subroutine func print lol end subroutine func 我用它编译gf
  • 如何在gnuplot中将字符串转换为数字

    有没有办法将表示数字 以科学格式 的字符串转换为 gnuplot 中的数字 IE stringnumber 1 0e0 number myconvert stringnumber plot 1 1 number 我可能使用 shell 命令
  • 在 Docker 容器中以主机用户身份运行

    在我的团队中 我们在进行开发时使用 Docker 容器在本地运行我们的网站应用程序 假设我正在开发 Flask 应用程序app py具有依赖关系requirements txt 工作流程大致如下 I am robin and I am in
  • 通过 TCP/.NET SSLStream 发送文件很慢/无法正常工作

    我正在编写一个与 SSL 配合使用的服务器 客户端应用程序 通过SSLStream 它必须做很多事情 不仅仅是文件接收 发送 目前 它的工作原理是 只有一个连接 我总是使用从客户端 服务器发送数据SSLStream WriteLine 并使
  • 运行此处编译的 C 程序会导致在另一台服务器上找不到 GLIBC 库错误 - 是我的错还是他们的错?

    此处编译的 C 程序在我们的 Ubuntu 服务器上运行良好 但是当其他人尝试在他们的特定 Linux 服务器上运行它时 他们会收到以下错误 myprog install lib tls libc so 6 version GLIBC 2
  • ubuntu 的 CSS 更少(并且自动编译)? [关闭]

    很难说出这里问的是什么 这个问题是含糊的 模糊的 不完整的 过于宽泛的或修辞性的 无法以目前的形式得到合理的回答 如需帮助澄清此问题以便重新打开 访问帮助中心 help reopen questions 我尝试过 simples 但现在 l
  • 使用c在linux上分块读写

    我有一个 ASCII 文件 其中每一行都包含一个可变长度的记录 例如 Record 1 15 characters Record 2 200 characters Record 3 500 characters Record n X cha
  • 裸机交叉编译器输入

    裸机交叉编译器的输入限制是什么 比如它不编译带有指针或 malloc 的程序 或者任何需要比底层硬件更多的东西 以及如何才能找到这些限制 我还想问 我为目标 mips 构建了一个交叉编译器 我需要使用这个交叉编译器创建一个 mips 可执行
  • 在 Linux 控制台中返回一行?

    我知道我可以返回该行并用以下内容覆盖其内容 r 现在我怎样才能进入上一行来改变它呢 或者有没有办法打印到控制台窗口中的特定光标位置 我的目标是使用 PHP 创建一些自刷新的多行控制台应用程序 Use ANSI 转义码 http en wik
  • 如何反汇编、修改然后重新组装 Linux 可执行文件?

    无论如何 这可以做到吗 我使用过 objdump 但它不会产生我所知道的任何汇编器都可以接受的汇编输出 我希望能够更改可执行文件中的指令 然后对其进行测试 我认为没有任何可靠的方法可以做到这一点 机器代码格式非常复杂 比汇编文件还要复杂 实
  • 如何在perl中使用O_ASYNC和fcntl?

    我想使用 O ASYNC 选项 当管道可以读取时 SIGIO 的处理程序将运行 但以下代码不起作用 任何人都可以帮助我吗 bin env perl use Fcntl SIG IO sub print catch SIGIO n my fl
  • 如何在文件夹中的 xml 文件中 grep 一个单词

    我知道我可以使用 grep 在这样的文件夹中的所有文件中查找单词 grep rn core 但我当前的目录有很多子目录 我只想搜索当前目录及其所有子目录中存在的所有 xml 文件 我怎样才能做到这一点 我试过这个 grep rn core

随机推荐

  • 1分钟解决IntelliJ IDEA 控制台中文乱码,统一设置 utf-8,再也不会乱码了

    首发地址 https it1314 top article 776 IDEA 控制台中文乱码 4 种解决方案 图文教程 中文再也不会乱码了 IntelliJ IDEA 如果不进行相关设置 可能会导致控制台中文乱码 配置文件中文乱码等问题 非
  • Apache 开源项目分类列表

    分 类 项目名 说明 开发语言 服务器 共20 Apache HTTP Server 全球第一HTTP服务器 C C Tomcat Java的Web服务器 Java James 邮件服务器 Java SpamAssassin 反垃圾邮件 C
  • 音频格式_想入坑HIFI?你得先了解这些——音频格式篇

    闲暇时戴上耳机 任旋律静静流淌 无论是忧郁的琴曲 还是律动的电音 都能带给人独特的享受 喜爱听歌的你对音频格式了解多少呢 这些傻傻分不清楚的格式又该如何区分 为什么需要高音质音源 一套最基本的音频系统涵盖了四个部分 每一个部分都缺一不可 如
  • 网络编程8/15——TCP服务器模型(多进程并发、多线程并发),TCP和UDP的本地通信(域套接字)

    目录 多进程并发服务器 模型 代码 多线程并发服务器 模型 代码 TCP本地通信 服务器 客户端 UDP本地通信 服务器 客户端 多进程并发服务器 模型 void handler int sig 回收僵尸进程 回收成功则再回收一次 直到回收
  • 论述奇偶校验和海明码

    一 奇偶校验 奇偶校验码是奇校验码和偶校验码的统称 是一种检错码 用于检查二进制数据的位错 并且用1个比特位来标记校验结果 所以当我们的数据有n位时 要传输给接收端的数据有n 1位 采用奇校验时 若所要传输的数据 含有奇数个1 则校验位为0
  • LM 系列开关电源芯片

    LM3477 High Efficiency High Side N Channel Controller 312 2006 2 18 3 03 59 LM3477A High Efficiency High Side N Channel
  • 经典网络结构梳理:Mobilenet网络结构

    论文下载地址 https arxiv org abs 1704 04861 Caffe复现地址 https github com shicai MobileNet Caffe Mobilenet发布在2017年的CVPR Mobilenet
  • CURL命令

    生成一个ca证书 openssl pkcs12 in test p12 out test crt 使用证书访问 curl cert test p12 cert type P12 cacert test crt header content
  • unity进阶--xml的使用学习笔记

    文章目录 xml实例 解析方法一 解析方法二 xml path 创建xml文档 xml实例 解析方法一 解析方法二 xml path 创建xml文档
  • 利用 RDMA 技术加速 Ceph 存储解决方案

    利用 RDMA 技术加速 Ceph 存储解决方案 晓兵XB 云原生云 2023 04 29 20 37 发表于四川 首发链接 利用 RDMA 技术加速 Ceph 存储解决方案 在本文中 我们首先回顾了 Ceph 4K I O 工作负载中遇到
  • Linux内核:系统调用大全(持续更新中)

    系统调用 1 sys brk 1 sys brk 系统调用sys brk的函数原型 sys brk 是一个操作系统调用 用于更改进程的堆空间大小 sys brk 函数接收一个无符号长整型参数brk 表示要求的新的程序数据段 堆 结束地址 如
  • Kubernetes 之深入理解 DaemonSet

    文章目录 Daemon Pod 的 过人之处 Daemon Pod 的定义 如何保证每个 Node 只有一个被管理的 Pod 何为 Toleration DaemonSet 是一个非常简单的控制器 DaemonSet 的使用方法 Daemo
  • 博思得标签打印机驱动_博思得V6驱动

    博思得Postek V6标签打印机驱动是博思得Postek品牌旗下V6型号标签打印机使用的驱动程序 这款驱动程序可以为您解决标签打印机连接不上电脑的情况 并且可以为您解决两者之间的故障 使用更便捷 博思得Postek V6标签打印机驱动安装
  • Appium自动化框架从0到1之 Driver配置封装

    不管是调用模拟器 还是调用真机 都需要准备一些driver的参数 以便被调用 思想 我们把driver配置信息 封装到yaml文件 然后通过读取yaml文件的内容 调用其driver信息 为了更直观的看如何封装 我们直接上代码 caps y
  • shell单双引号嵌套+变量

    metadata annotations volume kubernetes io selected node TARGET NODE
  • 云计算中微服务是什么Java之命名、标示符、变量

    微服务架构是一种架构模式 它提倡将单一应用程序划分成一组小的服务 服务之间相互协调 互相配合 为用户提供最终价值 每个服务运行在其独立的进程中 服务和服务之间采用轻量级的通信机制相互沟通 每个服务都围绕着具体的业务进行构建 并且能够被独立的
  • 【笔试强训选择题】Day34.习题(错题)解析

    作者简介 大家好 我是未央 博客首页 未央 303 系列专栏 笔试强训选择题 每日一句 人的一生 可以有所作为的时机只有一次 那就是现在 文章目录 前言 一 Day34习题 错题 解析1 总结 前言 一 Day34习题 错题 解析 1 解析
  • 升级 Linux 系统中的 Python 版本

    升级 Linux 系统中的 Python 版本 Python 是一种非常流行的编程语言 广泛应用于各种领域 包括 Web 开发 数据分析等 而对于 Linux 系统来说 Python 更是一个必须的组件 在系统运行和管理中都扮演了重要的角色
  • 大模型Founation Model

    一 背景 自从chatgpt gpt4以特别好的效果冲入人们的视野中 也使得AI产业发生了巨大变革 从17年以来的bert 将AI的各种领域都引入bert类的fine tune方法 来解决单个领域单个任务的一一个预训练模型 在学术界和工业界
  • 谈谈Linux epoll惊群问题的原因和解决方案

    近期排查了一个问题 epoll惊群的问题 起初我并不认为这是惊群导致 因为从现象上看 只是体现了CPU不均衡 一共fork了20个Server进程 在请求负载中等的时候 有三四个Server进程呈现出比较高的CPU利用率 其余的Server