Golang协程准入控制机制探讨

Golang协程准入控制机制探讨 你好,

我是Go语言的新手,目前正在编写一个处理网络设备漏洞扫描的Web服务(使用Gin框架)。以下是我计划开发的基本流程:

  • /api/v1/devicescan 发送POST请求,负载为 {"device": "ROUTER1"}
  • 创建Goroutine来启动扫描任务,并在完成后报告扫描结果

我希望实现的目标是防止API使用者对正在扫描的设备重复发送扫描请求。

我考虑了三种可能的解决方案,但希望得到一些关于哪种方案最优的提示:

  1. 将正在扫描的设备分配给通道,在启动扫描任务之前读取通道值。如果从通道中读取到请求扫描的设备,则中止操作。我知道从通道读取是阻塞操作,但使用带有default子句的select可以实现非阻塞通道操作。https://gobyexample.com/non-blocking-channel-operations

  2. 使用Postgres临时表来保存当前正在扫描的设备。在启动扫描任务之前,读取临时表的行以确定请求的设备是否已在扫描中。这涉及在每次扫描任务之前执行数据库查询的开销,可能不是最佳方案。

  3. 使用Gin框架API列出正在进行的请求及其值。我尚未确定Gin是否提供此类API。

提前感谢。


更多关于Golang协程准入控制机制探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

感谢您的建议。我现在认为使用带切片的简单FIFO队列应该就能完成这项工作。

更多关于Golang协程准入控制机制探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


  1. 扫描需要多长时间,是毫秒、秒还是分钟?
  2. 如果第二个请求到达。它是否也应该等待扫描完成并获取结果,或者应该收到一些错误信息?

感谢您的回复。

设备扫描通常需要约10秒,具体取决于网络延迟。如果对同一设备发出第二次扫描请求,则应返回错误信息,提示该设备当前正在扫描中。

可以使用映射来避免多次运行。

func longWork( params... ) {
  delete(jobs, id)
}

//request

if _ ,ok = jobs[id]; ok {
    //already run
}

jobs[id] = true
queue <- id

luca_brasi:

我想要实现的是防止API消费者对正在被扫描的设备重复发送扫描请求。

你可以使用一个结构体数组或映射来存储当前正在扫描的设备信息,并为每个设备设置一个信号量。只有当信号量处于空闲状态、设备不在列表中或满足其他条件时,才启动新的扫描。你也可以通过通道来实现,但我认为使用数据库表有些过于复杂。

// 示例代码
type Device struct {
    ID       string
    Status   string
    Semaphore chan bool
}

var scanningDevices = make(map[string]*Device)

func startScan(deviceID string) {
    if device, exists := scanningDevices[deviceID]; exists {
        select {
        case device.Semaphore <- true:
            // 开始扫描
            fmt.Println("开始扫描设备:", deviceID)
        default:
            // 设备正在扫描中
            fmt.Println("设备正在扫描中:", deviceID)
        }
    } else {
        // 设备不在列表中,创建新设备并开始扫描
        newDevice := &Device{
            ID:       deviceID,
            Semaphore: make(chan bool, 1),
        }
        scanningDevices[deviceID] = newDevice
        newDevice.Semaphore <- true
        fmt.Println("开始扫描设备:", deviceID)
    }
}

在Go语言中实现协程准入控制来处理设备扫描的重复请求是一个常见的需求。基于你的场景,我推荐使用基于内存的同步机制,因为它比数据库方案更高效,且比依赖框架特性更可控。

以下是使用sync.Map实现的方案,它比通道方案更简洁,比数据库方案性能更高:

package main

import (
    "net/http"
    "sync"
    
    "github.com/gin-gonic/gin"
)

var (
    scanningDevices sync.Map // 使用sync.Map存储正在扫描的设备
)

type ScanRequest struct {
    Device string `json:"device"`
}

func deviceScanHandler(c *gin.Context) {
    var req ScanRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
        return
    }
    
    device := req.Device
    
    // 检查设备是否已在扫描中
    if _, loaded := scanningDevices.LoadOrStore(device, true); loaded {
        c.JSON(http.StatusConflict, gin.H{
            "error": "Device is already being scanned",
            "device": device,
        })
        return
    }
    
    // 启动扫描协程
    go func(dev string) {
        defer scanningDevices.Delete(dev) // 扫描完成后移除设备
        
        // 执行扫描逻辑
        result := performVulnerabilityScan(dev)
        
        // 处理扫描结果(可存储到数据库或推送到消息队列)
        processScanResult(result)
    }(device)
    
    c.JSON(http.StatusAccepted, gin.H{
        "message": "Scan started",
        "device": device,
    })
}

func performVulnerabilityScan(device string) string {
    // 模拟漏洞扫描逻辑
    // 这里实现实际的扫描代码
    return "scan completed for " + device
}

func processScanResult(result string) {
    // 处理扫描结果
    // 可存储到数据库或发送通知
}

func main() {
    r := gin.Default()
    r.POST("/api/v1/devicescan", deviceScanHandler)
    r.Run(":8080")
}

这个实现提供了以下优势:

  1. 原子性操作sync.MapLoadOrStore方法提供原子性检查与存储操作
  2. 自动清理:使用defer确保扫描完成后自动从映射中移除设备
  3. 高性能:完全在内存中操作,避免数据库查询开销
  4. 线程安全sync.Map专为并发场景设计

对于你的三个方案分析:

  • 方案1(通道):通道更适合任务分发和协程间通信,不适合做状态跟踪
  • 方案2(数据库):引入不必要的数据库依赖和性能开销
  • 方案3(Gin API):Gin没有内置的请求状态跟踪功能

如果需要持久化跟踪扫描状态或支持多实例部署,可以考虑结合Redis等内存数据库,但在单实例场景下,sync.Map方案是最佳选择。

回到顶部