Golang助力优化系统设计

当太多用户同时访问你的应用程序时,它们会不会变慢?我经常看到这种情况,这确实很麻烦。你尝试扩大规模,但旧的方法不管用了。它们给我们带来了复杂的线程和一团糟的设置。当负载增加时,一切都开始崩溃。但如果有办法让它变得更简单呢?

让我们看看 Go 语言是怎么帮我们解决这个问题的。

旧方法不够用

想象一下,一家挤满了人的商店里有很多工人。每个工人只服务一位顾客。随着顾客越来越多,你又增加了工人。很快,商店就挤满了人,没人能走动了。这就是 Java 或 Python 等语言中线程的工作方式。每个线程都占用空间,可能是 1 或 2 MB 的内存,而 CPU 很难在它们之间切换。如果你想处理数千个任务,这样是不可能的。这对系统来说太过繁重,我们最终会陷入困境。

Go 带来新方式

现在想象一下那些工人是不需要太多空间的小帮手。他们来了,做他们的工作,然后迅速离开。这就是 Go 为我们提供的。它是为今天的需求而构建的,它彻底改变了我们处理并发的方式。这并不难,而且非常强大。想看看这对我们有什么作用吗?首先,我们将研究问题,然后 Go 如何逐步解决这些问题。

为什么线程和资源会引起麻烦

并发已经存在很长时间了,但通常的方法存在很大的问题。无论是制作网站还是处理数据,线程都会让事情变得困难。让我们看看为什么会发生这种情况,如果你也遇到过这种情况,你可以思考一下。

线程消耗太多资源

在 Java 或 C++ 等语言中,我们使用线程来实现并发。假设有 10,000 名用户进入。你启动了 10,000 个线程,对吗?这听起来不错,但事实并非如此。每个线程都需要内存来存储其堆栈,而且内存会迅速增加。如果你的服务器有 16 GB,那么你甚至在接近内存之前就会用完。然后我们添加锁来控制它们,这就会变得很麻烦。你花在修复错误上的时间比编写代码的时间还多。你以前遇到过这种情况吗?

扩展变得困难

你的系统在停止正常工作之前可以运行多少个线程?大多数情况下,只有几千个。内存满了,CPU 就跟不上。以一个处理请求的网站为例。如果每个请求都有自己的线程,突然有很多用户进来,它就会变慢或崩溃。我也在大型系统中看到过这种情况。当你有许多服务器相互通信时,线程会增加延迟和混乱。今天的工作,比如实时数据或小型服务,需要的不仅仅是这些。我们不能继续用老办法做。

负荷越大,痛苦越大

这不仅仅是线程的问题。添加的用户或任务越多,情况就越糟。想象一下处理付款的服务器。有一天它还很好,但后来发生了销售,流量猛增。使用线程,你就有麻烦了。系统无法快速增长,用户会感到不安。我们需要一些在事情繁忙时不会中断的东西。

Go 的简单而强大的修复

Go 之所以与众不同,是因为其设计中就包含并发性。它为我们提供了 goroutine 和 channel 来解决这些问题。让我们来看看它们是如何帮助我们的,你就会明白它为什么如此优秀。

Goroutines 保持轻量

goroutine 就像一个微型线程,但要好得多。它是一个小型工作线程,仅使用 2 KB 内存。Go 自己的系统运行它们,而不是操作系统,因此我们可以毫无顾虑地启动数千甚至数百万个 goroutine。它看起来很简单:


func handleRequest(req Request) {
    go process(req)
}


这个 go 关键字启动了一个 goroutine。它非常简单,而且不占用太多空间。如果你的网站收到大量请求,则每个请求都会在自己的 goroutine 中运行。需要更多吗?Go 会自动处理它并保持运行速度。它不像线程那样一切都变得沉重。你可以尝试一下,看看感觉有多轻松!

通道让事情变得清晰

运行任务是其中一部分,但它们需要相互通信。这就是通道的作用所在。它们就像管道,goroutine 可以通过管道安全地发送消息。无需锁定或修复错误。我们可以这样做:


func worker(ch chan string) {
    res = doTask()
    ch <- res
}

func main() {
    ch = make(chan string)
    go worker(ch)
    out = <-ch
    print(out)
}


通道让一切井然有序。想要在任务之间发送数据?使用 make(chan string, 10) 来保存一些消息,它运行顺畅。这使我们的设计变得简单易懂。不再像线程那样令人困惑。

为什么这很重要

有了 goroutine 和通道,并发性就成了我们的强项。它们轻量、易用,而且可以随着我们的需求而增长。有了这些之后,线程就变得过时了。你不必再与系统作斗争了。Go 为我们完成了艰苦的工作,我们可以构建我们想要的东西。

使用 Go 构建系统

现在我们将看看如何在实际项目中使用它。让我们尝试两个示例:一个小型服务设置和一个自我平衡的数据流。你可以跟着一起思考它如何适合你的工作。

使用 Goroutines 的小型服务

想象一下一个登录服务。用户发送请求,我们检查数据库并生成令牌。使用线程时,当负载增加时它会失败。但使用 Go,我们可以这样做:

func handleLogin(w http.ResponseWriter, r *http.Request) {
    go func() {
        user = checkRequest(r)
        token = createToken(user)
        w.Write([]byte(token))
    }()
}

func main() {
    http.HandleFunc("/login", handleLogin)
    http.ListenAndServe(
":8080", nil)
}


每个请求都会有一个 goroutine。如果有 50,000 个用户,它仍然可以正常运行。Go 保持低内存,并自行清理。大公司将它用于小型服务,因为它非常快。如果数据库很慢怎么办?我们可以添加一组工作程序:


func workerGroup(reqs chan Request) {
    for r = range reqs {
        go processRequest(r)
    }
}


将请求发送到通道,然后 goroutine 会处理这些请求。即使有大量用户,它也能保持快速运行。你可以在自己的服务器上尝试此操作,看看它的表现如何。

使用通道Channel平衡数据

现在考虑处理日志。我们获取日志、读取日志并保存结果。旧方法使用线程或外部工具,但 Go 使用通道使其变得简单:


func fetchLogs(ch chan Log) {
    for log = range getLogs() {
        ch <- log
    }
}

func processLogs(chIn chan Log, chOut chan Result) {
    for log = range chIn {
        res = readLog(log)
        chOut <- res
    }
}

func saveResults(ch chan Result) {
    for r = range ch {
        storeResult(r)
    }
}

func main() {
    logs = make(chan Log, 100)
    results = make(chan Result, 100)
    go fetchLogs(logs)
    for i = 0; i < 5; i++ {
        go processLogs(logs, results)
    }
    go saveResults(results)
    select {}
}


日志通过通道移动,许多 goroutine 对其进行处理。100 的缓冲区在繁忙时会有所帮助,但如果你想要更快的速度,可以添加更多 processLogs()。无需额外的工具。它会自行平衡,Go 会使其保持平稳运行。

增强功能

如果你需要更多控制怎么办?你可以添加超时或限制。例如,如果 goroutine 耗时过长,则停止它:


func handleWithTimeout(req Request) {
    ch = make(chan string)
    go func() {
        res = process(req)
        ch <- res
    }()
    select {
    case out = <-ch:
        print(out)
    case <-time.After(2 * time.Second):
        print("花费时间太长")
    }
}


这样可以保持速度。你可以根据需要进行更改。以后调整非常简单。

用 Go 结束

Go 的并发性确实很棒,因为它改变了我们的思维方式。你已经了解了 goroutine 如何节省空间以及通道如何保持空间畅通。它们帮助我们创建可以轻松扩展的系统。

突出之处

你无需再为缓慢的设置而烦恼。无论是服务器还是大数据流,Go 都能让事情变得快速而简单。它消除了困难的部分,因此我们可以专注于我们想要解决的问题。时间很重要,而 Go 可以为我们节省时间。