0%

《操作系统》关于如何处理大量连接

accept 每次都是从已经完成三次握手的 tcp 队列中取出一个连接

多个线程 accept,然后处理连接

这种做法的缺点是:

  • 为确保只有一个线程收到连接和防止惊群问题,内核中多线程争抢锁,竞争严重
  • 高负载下,线程之间处理不均衡,出现少数几个大量得到连接而且处理不过来,而大量线程得不到连接非常空闲

一个线程 accept,然后新启动线程处理这个连接

这种做法的缺点是:

  • 极高负载下,accept 的那个线程会出现瓶颈,即便只是接收连接

Golang 的 http 库是采用这种方式。配合宏观上的负载均衡(多机器多实例)可以覆盖绝大部分场景

宏观上的负载均衡(多机器多实例)其实就类似于下面第三种方式,只不过下面的方法是在一台机器上

跟 Nodejs 中的 Libuv 模块以及 Redis 处理连接的方式一样

使用 SO_REUSEADDR、SO_REUSEPORT 机制,并结合前面两种方式

正常情况下,多个进程或者多个线程是无法同时监听同一个端口的

但是如果启用了 SO_REUSEADDR、SO_REUSEPORT 机制(就是使用 Setsockopt 为 socket 设置这两个标志),这个 socket 就可以被多个线程同时监听

一旦有事件到达,内核会负责负载均衡

多个线程监听同一个地址和端口并 accept,然后启动新线程处理

可以解决上述所有问题

下面看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"context"
"fmt"
"golang.org/x/sys/unix"
"log"
"net"
"syscall"
"time"
)

func test(haha string) {
lc := net.ListenConfig{
Control: func(network, address string, c syscall.RawConn) error {
if err := c.Control(func(fd uintptr) {
err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
if err != nil {
return
}
err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1)
if err != nil {
return
}
}); err != nil {
return err
}
return nil
},
}
l, err := lc.Listen(context.Background(), "tcp", "0.0.0.0:8000")
if err != nil {
log.Fatal(err)
}
fmt.Println(haha, "成功")
for {
conn, err := l.Accept()
if err != nil {
log.Fatal(err)
}
fmt.Println(haha)
fmt.Println(conn.RemoteAddr())
conn.Close()
}
}

func main() {
go func() {
test("1")
}()

test("2")
}

实测发现 darwin 下可以运行,但是均衡并没有效果。linux 上可以正常负载均衡




微信关注我,及时接收最新技术文章