从SO_REUSEPORT服务器的一个弊端看多队列服务模型_多服务队列模型

在进入关于REUSEPORT的讨论之前,先看一张图,下图描述了单队列服务和多队列服务的区别:
在这里插入图片描述

  • 单队列典型实例:银行业务叫号,机场火车站出排队坐租车等。
  • 多队列典型实例:超市排队结账,医院挂号收费等。

到底哪一种好呢?很多人估计会想当然选择多队列,但我个人更倾向于单队列。

在计算机领域,人们似乎总是倾向于并行化,这背后似乎有着对同步锁的笃恨,比方说,只要你把一个数据结构设计成单链表,那么肯定会有一大堆人跳出来说你这个在多线程环境会很大的锁开销。

当多线程操作同一数据结构时,饱受诟病的就是它的同步开销。

事实确实如此,但不能人云亦云。

同步和锁固然可恨,但这并不是全部,由于近20年来并行多处理器编程的概念被炒作的非常火,以至于很多人本末倒置,忘记了多处理模型中的大头其实并非同步和锁,而是排队模型本身。跳出计算机的圈子试试看。

当然,在操作系统领域,同步和锁争用的话题是非常重要的一啪,同步和锁的问题是单处理器进化到多处理器过程中必须要解决的一个问题。

我们回忆一下早期传统的最简单的服务器编程模型:

sd = socket();
bind(sd);
listen(sd);
while (i++ < NUM) {
    if (fork() == 0) {
        while (true) {
            cd = accept(sd);
            do_work();
            close(cd);
        }
    }
}
wait();

fork出多个进程来处理同一个socket。这是一个典型的 “单队列多服务台排队模型”。 基于它的种种不足,select,poll,epoll被设计了出来。但事情直到REUSEPORT被引入时才发生了本质的变化。

REUSEPORT允许让多个独立的socket绑定完全相同的IP+Port,这在事实上将socket处理本身变成了 “多队列多服务台排队模型”

有了REUSEPORT之后,人们欢呼着纷纷引用。人们认为, *REUSEPORT终于把操作系统协议栈中socket队列的那一把锁解了!

然而,REUSEPORT真的百利无一弊吗?并不是!看看上图中单队列和多队列的对比,REUSEPORT可能更大概率造成客户端延时分布的发散:

  • 对于单独客户端而言,它更容易遭遇到REUSEPORT单独socket的忙碌,被动排队。

是不是这样呢?

在继续之前,我不得不事先声明:

看完本文后,很多人第一反应就是这low爆了,都2908年了,竟然不用epoll!其实我是故意不用epoll的,我的目标不是演示一个高性能服务器应该如何设计(其实我也不会),而是为了揭露REUSEPORT的一个问题,当然是要用最最最简单的方式了。

下面的实验比对了单队列和多队列对客户端延迟的影响。先看简单的单队列服务器,这是一个简单的echo服务器:

#!/usr/bin/python
# single.py

import os
import socket
import time
import random

sd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sd.bind(('127.0.0.1', 1234))
sd.listen(100)
for i in range(4):
    if os.fork () == 0:
        while True:
            cd, addr = sd.accept()
            print('receive data at', str(i))
            data = cd.recv(1024);
            # 制造概率性忙碌
            if random.randint(1,3) == 1:
                time.sleep(2) 
            cd.sendall(data + ' ' + str(i))
            cd.close()
os.wait()

下面代码做同样的事情,只不过它是REUSEPORT多队列版本:

#!/usr/bin/python
# multi.py

import os
import socket
import time
import random

for i in range(4):
    if os.fork () == 0:
        sd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
        sd.bind(('127.0.0.1', 1234))
        sd.listen(100)
        while True:
            cd, addr = sd.accept()
            data = cd.recv(1024);
            print('receive data at', str(i))
            if random.randint(1,3) == 1:
                time.sleep(2)
            cd.sendall(data + ' ' + str(i))
            cd.close()
os.wait()

下面是一个客户端,它同时产生100个线程连接服务器,在获得回应后打印消耗时长,并且在最后打印总消耗时长:

#!/usr/bin/python

import socket
import time
import threading

HOST = '127.0.0.1'
PORT = 1234

def do_echo():
    sd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    l_start = time.time()
    sd.connect((HOST, PORT))
    sd.sendall(b'Hello, world')
    data = sd.recv(1024)
    l_end = time.time()

    print str(l_end - l_start) + ' Sec'

threads = []
for i in range(0, 100):
    t = threading.Thread(target = do_echo)
    threads.append(t)

t_start = time.time()
for t in threads:
    t.start()

for t in threads:
    t.join()
t_end = time.time()
print 'cost ' + str(t_end - t_start) + ' Sec'

OK,现在让我们测试一下,首先看单队列版本的表现:

0.000965118408203 Sec
0.00181603431702 Sec
0.00233006477356 Sec
0.00104022026062 Sec
0.00102591514587 Sec
0.00202798843384 Sec
0.000540971755981 Sec
0.00217485427856 Sec
0.000724077224731 Sec
0.00132298469543 Sec
0.00124096870422 Sec
0.00100612640381 Sec
0.00180292129517 Sec
0.00173401832581 Sec
...
10.0033230782 Sec
12.0018620491 Sec
12.0039548874 Sec
12.0068860054 Sec
12.0079398155 Sec
cost 12.0472490788 Sec

再看REUSEPORT版本服务器的表现:

0.000521898269653 Sec
0.00278997421265 Sec
0.00123596191406 Sec
0.00226593017578 Sec
0.00240898132324 Sec
0.00142693519592 Sec
0.00168108940125 Sec
0.00109219551086 Sec
2.00345492363 Sec
2.00189590454 Sec
2.00391507149 Sec
...
17.9960510731 Sec
18.001486063 Sec
18.01060009 Sec
18.0108709335 Sec
18.0199809074 Sec
18.0170469284 Sec
18.0191478729 Sec
18.0191700459 Sec
18.0200259686 Sec
18.0203499794 Sec
18.0164711475 Sec
cost 18.0505480766 Sec

多测试几次,取平均,我们发现REUSEPORT版本作为服务器时,客户端的延时分布在一个更广的范围内,从上面的多队列的描述可以看出,这应该是客户端由于 “正好撞到” 被延迟处理的队列中,造成了积累排队延时导致。

为了更加形象地说明问题,我们把问题放大。

把两类服务器中的下面代码进行修改:

if random.randint(1,3) == 1:

修改为:

if i == 1:

这样我们固定第一个处理进程是延迟的,然后观察客户端的表现,先看单队列版本的:

...
0.000363111495972 Sec
0.000207901000977 Sec
2.00301790237 Sec
cost 2.0051279068 Sec

合乎逻辑,毕竟只有一个进程忙碌时,其它所有的进程均可接管处理任何队列中的请求,总耗时几乎就是 那一个 碰巧被延迟的请求处理时间。下面我们看REUSEPORT版本的表现:

...
0.000730991363525 Sec
0.00215291976929 Sec
0.000869989395142 Sec
0.000274896621704 Sec
0.000379800796509 Sec
# 从这里往下,我们可以一眼看出所谓的积累延迟效应,以下所有的请求都很不幸被dispatch到了1号进程。
2.00326395035 Sec
4.00580787659 Sec
6.00063920021 Sec
7.99950909615 Sec
9.99837803841 Sec
12.0005970001 Sec
14.0024950504 Sec
15.996434927 Sec
17.9980938435 Sec
19.9969918728 Sec
21.997202158 Sec
23.9789199829 Sec
25.9731481075 Sec
27.9732391834 Sec
29.9746050835 Sec
31.9765660763 Sec
33.978415966 Sec
35.9799618721 Sec
37.9760642052 Sec
cost 38.0491991043 Sec

事情到了这里,你可能已经误会了我,以为我要开历史的倒车,摒弃REUSEPORT了,但其实这并非我要表达的意思。

任何成功的东西都是妥协的结果,为REUSEPORT的socket准备一个standby进程就好了:

#!/usr/bin/python

import os
import socket
import time

for i in range(4):
    if os.fork () == 0:
        sd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
        sd.bind(('127.0.0.1', 1234))
        sd.listen(10)
        if os.fork () == 0:
            while True:
                cd, addr = sd.accept()
                data = cd.recv(1024);
                print('receive data at standby', str(i))
                cd.sendall(data + ' ' + str(i))
                cd.close()

        while True:
            cd, addr = sd.accept()
            data = cd.recv(1024);
            print('receive data at', str(i))
            if i == 1:
                time.sleep(2)
            cd.sendall(data + ' ' + str(i))
            cd.close()
os.wait()

这样一来,每一个REUSEPORT的socket就有两个处理进程了,一个进程不幸忙碌阻塞队列的时候,另一个就可以替补,当然了,也不一定是两个,多个也可以,要看服务器的资源以及忙碌进程阻塞队列的概率了。

分享一篇关于单队列和多队列的文章:
https://www.irisys.net/queue-management-blog/single-vs.-multiple-queues-which-one-is-best-for-you-

经理湿了皮鞋,连绵暴雨,经理或为鱼鳖。


浙江温州皮鞋湿,下雨进水不会胖。

原文链接: https://blog.csdn.net/dog250/article/details/107227145

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    从SO_REUSEPORT服务器的一个弊端看多队列服务模型_多服务队列模型

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/405904

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午9:30
下一篇 2023年4月26日 上午9:30

相关推荐