1

我需要阅读大约 600 个 pcap 文件,每个文件大约 100MB。我使用 gopacket 加载 pcap 文件,并检查它。

Case1:使用 1 个例程检查。

案例 2:使用 40 个例程进行检查。

而且我发现case1和case2消耗的时间差不多。不同的是case1的cpu使用率只有200%,case2可以达到3000%。我的问题是为什么多个例程不能提高性能? 代码中有一些注释,希望对您有所帮助。

package main

import (
    "flag"
    "fmt"
    "io/ioutil"
    "log"
    "os"
    "strings"
    "sync"

    "github.com/google/gopacket"
    "github.com/google/gopacket/layers"
    "github.com/google/gopacket/pcap"
)

func main() {
    var wg sync.WaitGroup

    var dir = flag.String("dir", "../pcap", "input dir")
    var threadNum = flag.Int("threads", 40, "input thread number")
    flag.Parse()
    fmt.Printf("dir=%s, threadNum=%d\n", *dir, *threadNum)

    pcapFileList, err := ioutil.ReadDir(*dir)
    if err != nil {
        panic(err)
    }

    log.Printf("start. file number=%d.", len(pcapFileList))

    fileNumPerRoutine := len(pcapFileList) / *threadNum
    lastFileNum := len(pcapFileList) % *threadNum

    // split files to different routine
    // each routine only process files which belong to itself
    if fileNumPerRoutine > 0 {
        for i := 0; i < *threadNum; i++ {
            start := fileNumPerRoutine * i
            end := fileNumPerRoutine * (i + 1)
            if lastFileNum > 0 && i == (*threadNum-1) {
                end = len(pcapFileList)
            }
            // fmt.Printf("start=%d, end=%d\n", start, end)
            wg.Add(1)
            go checkPcapRoutine(i, &wg, dir, pcapFileList[start:end])
        }
    }

    wg.Wait()
    log.Printf("end.")
}

func checkPcapRoutine(id int, wg *sync.WaitGroup, dir *string, pcapFileList []os.FileInfo) {
    defer wg.Done()

    for _, p := range pcapFileList {
        if !strings.HasSuffix(p.Name(), "pcap") {
            continue
        }
        pcapFile := *dir + "/" + p.Name()
        log.Printf("checkPcapRoutine(%d): process %s.", id, pcapFile)

        handle, err := pcap.OpenOffline(pcapFile)
        if err != nil {
            log.Printf("error=%s.", err)
            return
        }
        defer handle.Close()

        packetSource := gopacket.NewPacketSource(handle, handle.LinkType())

        // Per my test, if I don't parse packets, it is very fast, even use only 1 routine, so IO should not be the bottleneck.
        // What puzzles me is that every routine has their own packets, each routine is independent, but it still seems to be processed serially.
        // This is the first time I use gopacket, maybe used wrong parameter?
        for packet := range packetSource.Packets() {
            gtpLayer := packet.Layer(layers.LayerTypeGTPv1U)
            lays := packet.Layers()
            outerIPLayer := lays[1]
            outerIP := outerIPLayer.(*layers.IPv4)

            if gtpLayer == nil && (outerIP.Flags&layers.IPv4MoreFragments != 0) && outerIP.Length < 56 {
                log.Panicf("file:%s, idx=%d may leakage.", pcapFile, j+1)
                break
            }
        }
    }
}
4

1 回答 1

4

要并行运行两个或多个任务,执行这些任务所需的操作必须具有不相互依赖或不依赖于某些外部资源的属性,这些外部资源然后被这些任务共享。

在现实世界中,真正完全独立的任务很少见(如此罕见以至于此类任务甚至有一个专门的名称:它们被称为令人尴尬的并行)但是当任务的依赖量相互依赖时进度和竞争访问共享资源低于某个阈值,添加更多“工人”(goroutines)可能会提高完成一组任务所需的总时间。

请注意此处的“可能”:例如,您的存储设备和其上的文件系统以及内核数据结构和与文件系统一起使用的代码,并且存储设备是您的所有 goroutine 必须访问的共享介质。这种介质对吞吐量和延迟都有一定的限制;基本上,您每秒只能从该介质读取 M 字节——无论您是有一个阅读器充分利用此带宽,还是有 N 个阅读器(每个阅读器都使用大约 M/N 的数量)都无关紧要:您在物理上读取速度不能超过 M BPS 的限制。

此外,现实世界中最常见的资源在竞争时往往会降低其性能:例如,如果必须锁定资源才能访问,那么主动想要获取锁定的访问者越多,花费的 CPU 时间就越多在锁管理代码中(当资源更复杂时——例如“存储设备上的 FS——全部由内核管理”的复杂东西的集合体——分析它在同时访问时如何降级成为一种方式更复杂)。

TL;博士

我可以有根据地猜测,您的任务只是 I/O 绑定的,因为 goroutine 必须读取文件。

您可以通过修改代码来验证这一点,首先将所有文件提取到内存中,然后将缓冲区交给解析 goroutine。

在您的案例中,您所观察到的大量 CPU 花费是一条红鲱鱼:当代系统喜欢将 100% 的 CPU 利用率表示为“充分利用单个硬件处理线程” ——所以如果你有,比如 4 个 CPU 内核启用 HyperThreading™(或 AMD 为此提供的任何功能)后,系统的全部容量为 4×2=8,即 800%。
您看到的可能超出理论容量(我们不知道)的事实可能是您的系统以这种方式显示所谓的“饥饿”来解释的:您有许多软件线程想要执行但等待它们的CPU时间,系统显示为疯狂的CPU利用率。

于 2020-08-20T10:42:27.860 回答