对于大部分应用,尽可能快地响应请求是首要任务。例如,应用程序可能正在服务用户的HTTP请求,或者检索复制的数据块。 在这些情况下,你需要做出权衡:是将请求复制到多个处理程序(无论是goutoutine,进程还是服务器),并且其中一个将比其他处理程序返回更快呢,还是立即返回结果——缺点是必须考虑如何高效利用资源来保持处理程序的多个副本同时运行。

如果这种复制是在内存中完成的,它耗费的资源可能并不是那么昂贵,但如果处理程序需要复制进程,服务器甚至数据中心,这就完全不一样了。你必须考虑这样做成本是否值得。

我们来看看如何在单个进程中复制请求。我们将使用多个goroutines作为请求处理程序,并且goroutine将在1到6纳秒之间随机休眠一段时间以模拟负载。 这将使处理程序在不同时间返回结果,并让我们看到这样做能否更高效。

下面这个例子通过10个处理程序复制模拟请求:

doWork := func(done <-chan interface{}, id int, wg *sync.WaitGroup, result chan<- int) {

    started := time.Now()
    defer wg.Done()

    // 模拟随机加载
    simulatedLoadTime := time.Duration(1+rand.Intn(5)) * time.Second
    select {
    case <-done:
    case <-time.After(simulatedLoadTime):
    }

    select {
    case <-done:
    case result <- id:
    }

    took := time.Since(started)
    // 显示处理程序将花费多长时间
    if took < simulatedLoadTime {
        took = simulatedLoadTime
    }
    fmt.Printf("%v took %v\n", id, took)
}

done := make(chan interface{})
result := make(chan int)

var wg sync.WaitGroup
wg.Add(10)

for i := 0; i < 10; i++ { //1
    go doWork(done, i, &wg, result)
}

firstReturned := <-result //2
close(done)               //3
wg.Wait()

fmt.Printf("Received an answer from #%v\n", firstReturned)
  1. 我们开启10个处理程序以处理请求。
  2. 抓取处理程序第一个返回的值。
  3. 取消所有剩余的处理程序。这确保他们不会继续做不必要的工作。

这会输出:

4 took 1s
3 took 1s
6 took 2s
8 took 1s
2 took 2s
0 took 2s
7 took 4s
5 took 5s
1 took 3s
9 took 3s
Received an answer from #3

在输出中,我们显示每个处理程序花费了多久,以便你了解此技术可以节省多少时间。

唯一需要注意的是,所有的处理程序都需要有相同且平等的请求。换句话说,不会出现从无法处理请求的处理程序接收响应时间。正如我所提到的那样,所有处理者用来完成工作的资源都需要复制。

如果你的处理程序太相似了,那么任何一个出现异常的几率都会更小。你只应该将这样的请求复制到具有不同运行时条件的处理程序:不同的进程,计算机,数据存储路径或完全访问不同的数据存储区。

这样做的代价可能是昂贵且难以维护。如果速度是你的目标,那这个技术就很有价值了。当然你还需要考虑容错和可扩展性。

最后编辑: kuteng  文档更新时间: 2021-01-02 17:30   作者:kuteng