如何解决 Golang WAF 服务的竞争条件?

How can I solve race condition for Golang WAF service?

首先,对于糟糕的英语表示抱歉。

我尝试用Golang开发一个WAF(Web Application Firewall)服务。一切都在map[string]*Struct{}内存中。当请求到来时,我将请求 header 的主机设置为映射到处理函数中。

host,err := GetHost(r.Host)

func GetHost(host string) (*Host,error){
    split, _, err := net.SplitHostPort(host)
    if err == nil {
        host = split
    }
    if data, val := hosts[host]; val { 
        return data, nil
    }
    return nil,errors.New("host not found!)
}
//hosts is a map for all host, key is host and value is host struct.

问题是,当服务收到大量请求时,地图会变得混乱。例如; host 是 example.comhosts["example.com"] 给出了另一个无关的值。

type Server struct {
    mu   sync.RWMutex
    Host *models.Host
}
func (c *Server) handler(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    mutex.Lock()
    defer mutex.Unlock()
    var err error
    c.Host, err = GetHost(r.Host)
    if err != nil {
        w.WriteHeader(http.StatusBadGateway)
        w.Write([]byte(r.Host + " not found!!"))
        return
    }
    //it's going on..Edited part

所以,我尝试使用mutexwg来解决这个问题,但没有用。欢迎提出任何建议。

您认为需要从 HTTP 处理程序序列化对共享状态的访问是正确的(因为服务器在专用 goroutine 中处理每个 HTTP 请求)。否则,您的程序确实会遇到同步错误,这可能会在执行过程中表现为数据竞争,正如您似乎已经经历过的那样; Go 工具链随附的 race detector 可能会使用它。

可以说,序列化对该共享状态的访问的最简单方法是使用一些互斥体。但是,您需要小心。您对 mutex.Unlock 的延迟调用存在问题,原因至少有一个,可能有两个:

  1. 一般来说,您应该努力保持关键部分(代码中被对LockUnlock的调用包围的部分)尽可能“小”和“便宜”。简而言之,关键部分应该只做内存处理,而不是 I/O 东西。在这里,在处理每个请求的整个过程中都需要持有锁,这很可能会导致您的服务器发生大量争用。
  2. 虽然您在处理程序中省略了代码的末尾,但我猜 (?) 您稍后也会获取锁以更新地图(如果之前未遇到当前请求的主机) .但是,由于包 sync 导出的 none 互斥锁类型是可重入的,您可能会遇到死锁:由于对 Unlock 的调用被推迟,互斥锁将只有当你的处理程序终止时才会被释放。

一个解决方案是避开 defer 并将关键部分限制为对 GetHost 函数的调用。

另一项改进是 eliminate global state,以获得更好的可测试性等。您可以通过简单地将 hosts 地图存储在 Server 的字段中来使您的 hosts 地图成为非全球地图结构并将 GetHost 声明为 *Server.

上的方法
type Server struct {
    mu   sync.RWMutex
    Host *models.Host
    hosts map[string]*Host
}

func (srv *Server) GetHost(host string) (*Host, error){
    split, _, err := net.SplitHostPort(host)
    if err == nil {
        host = split
    }
    if data, exists := srv.hosts[host]; exists { 
        return data, nil
    }
    return nil, errors.New("host not found!")
}

func (srv *Server) handler(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        mutex.Lock()
        srv.Host, err := srv.GetHost(r.Host)
        mutex.Unlock()
        if err != nil {
            w.WriteHeader(http.StatusBadGateway)
            w.Write([]byte(r.Host + " not found!!"))
            return
        }
        // possibly acquire and release the lock again
        // for further treatment of the hosts map
    })
}

我可能遗漏了一些东西,但必须承认我没有看到为每个请求更新 Server 结构的 Host 字段的意义...