1. 并发过高导致程序崩溃 先看一个简单的例子
1 2 3 4 5 6 7 8 9 10 11 12 func main () { var wg sync.WaitGroup for i := 0 ; i < math.MaxInt32; i++ { wg.Add(1 ) go func (i int ) { defer wg.Done() fmt.Println(i) time.Sleep(time.Second) }(i) } wg.Wait() }
这个例子要创建 math.MaxInt32个协程,每个协程只是输出当前的编号,正常情况下,会乱序输出 0 ~math.MaxInt32 的数字,但实际执行一段时间后 直接panic。
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 1298211 251117 1297752 panic: too many concurrent operations on a single file or socket (max 1048575) goroutine 1300064 [running]: internal/poll.(*fdMutex).rwlock(0xc000026120, 0x0?) /usr/local/go/src/internal/poll/fd_mutex.go:147 +0x11b internal/poll.(*FD).writeLock(...) /usr/local/go/src/internal/poll/fd_mutex.go:239 internal/poll.(*FD).Write(0xc000026120, {0xc11b3ff928, 0x8, 0x8}) /usr/local/go/src/internal/poll/fd_unix.go:370 +0x72 os.(*File).write(...) /usr/local/go/src/os/file_posix.go:48 os.(*File).Write(0xc000012018, {0xc11b3ff928?, 0x8, 0xc1255acf50?}) /usr/local/go/src/os/file.go:175 +0x65 fmt.Fprintln({0x10c6b88, 0xc000012018}, {0xc1255acf90, 0x1, 0x1}) /usr/local/go/src/fmt/print.go:285 +0x75 fmt.Println(...) /usr/local/go/src/fmt/print.go:294 main.main.func1(0x0?) /Users/shiguofu/Documents/hw.go:16 +0x8f created by main.main /Users/shiguofu/Documents/hw.go:14 +0x3c panic: too many concurrent operations on a single file or socket (max 1048575)
报错信息也很明显,too many concurrent operations on a single file or socket
对单个 file/socket 的并发操作个数超过了系统最大值,这个错误是由于fmt.Sprintf引起的,它将格式化数据输出到标准输出。标准输出在linux系统中是文件描述符为1的文件,标准错误输出是2,标准输入是0。
总之就是系统资源耗尽了。
那假如去掉fmt这行输出呢?程序很可能就会因为内部不足而被迫退出,笔者尝试去掉跑出错误,电脑16G的,跑到5G多已经卡的不行, 就强行退出了。其实也好理解,每个协程约占用2K控件,1M个协程就是2G的内存,那Math.MaxInt 读者自己计算下。
2. 如何解决呢 2.1 利用channel的缓冲区大小来控制协程个数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { var wg sync.WaitGroup ch := make (chan struct {}, 3 ) for i := 0 ; i < 10 ; i++ { ch <- struct {}{} wg.Add(1 ) go func (i int ) { defer wg.Done() log.Println(i) time.Sleep(time.Second) <-ch }(i) } wg.Wait() }
make(chan struct{}, 3) 缓冲区大小为3,没有被消费的情况下,最多发送3个就被阻塞了
开启协程前 往通道写入数据,缓冲区满 就阻塞了
协程结束后 消费通道数据,缓冲区就可以继续写入数据,就可以再创建新的协程
如下进行封装,可直接像waitgroup一样使用
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 import ( "context" "sync" ) const defaultSize = 32 type SizeWaitGroup struct { buf chan struct {} wg sync.WaitGroup } func NewSizeWaitGroup (size int ) *SizeWaitGroup { if size <= 0 { size = defaultSize } return &SizeWaitGroup{ buf: make (chan struct {}, size), wg: sync.WaitGroup{}, } } func (c *SizeWaitGroup) Add () { _ = c.AddWithContext(context.Background()) } func (c *SizeWaitGroup) AddWithContext (ctx context.Context) error { select { case <-ctx.Done(): return ctx.Err() case c.buf <- struct {}{}: break } c.wg.Add(1 ) return nil } func (c *SizeWaitGroup) Done () { <-c.buf c.wg.Done() } func (c *SizeWaitGroup) Wait () { c.wg.Wait() }
2.2 调整系统资源上限 2.2.1 ulimit 有些场景下,即使我们有效地限制了协程的并发数量,但是仍旧出现了某一类资源不足的问题,例如:
too many open files
out of memory
…
操作系统通常会限制同时打开文件数量、栈空间大小等,ulimit -a
可以看到系统当前的设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 63068 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 65535 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 63068 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited
我们可以使用 ulimit -n 999999
,将同时打开的文件句柄数量调整为 999999 来解决这个问题,其他的参数也可以按需调整。
2.2.2 虚拟内存(virtual memory) 虚拟内存是一项非常常见的技术了,即在内存不足时,将磁盘映射为内存使用,比如 linux 下的交换分区(swap space)。
在linux创建交换分区
1 2 3 4 5 sudo fallocate -l 20G /mnt/.swapfile # 创建 20G 空文件 sudo mkswap /mnt/.swapfile # 转换为交换分区文件 sudo chmod 600 /mnt/.swapfile # 修改权限为 600 sudo swapon /mnt/.swapfile # 激活交换分区 free -m # 查看当前内存使用情况(包括交换分区)
关闭交换分区
1 2 sudo swapoff /mnt/.swapfile rm -rf /mnt/.swapfile
磁盘的 I/O 读写性能和内存条相差是非常大的,例如 DDR3 的内存条读写速率很容易达到 20GB/s,但是 SSD 固态硬盘的读写性能通常只能达到 0.5GB/s,相差 40倍之多。因此,使用虚拟内存技术将硬盘映射为内存使用,显然会对性能产生一定的影响。如果应用程序只是在较短的时间内需要较大的内存,那么虚拟内存能够有效避免 out of memory
的问题。如果应用程序长期高频度读写大量内存,那么虚拟内存对性能的影响就比较明显了。