它最初只是一个简单的应用程序编程接口(API)—— 仅仅是一个轻量级的Go服务,用于处理用户身份验证和支付处理。起初,它每天处理几千次请求时运行得完美无缺。但随着流量的增长,我曾经反应敏捷的后端变得迟缓起来。延迟大幅增加,数据库查询成了瓶颈,服务器在高负载下举步维艰。
有一天,我们的服务被一家知名新闻网站报道了。几分钟内,流量激增了10倍。我的Go后端几乎快跟不上了,基础设施团队大喊道:“我们需要更多的服务器!”
就在那时,我挺身而出,对我们的系统进行了优化,使其能够每秒处理100万次请求(RPS)。以下是我具体的做法。
1. 优化Goroutine以实现真正的并发
起初,我认为Go语言的Goroutine非常神奇。“它们很轻量级!它们可以无限扩展!” —— 我曾经就是这么认为的。然而,我很快就意识到,启动过多的Goroutine会导致过多的CPU上下文切换和内存耗尽。
用工作池解决Goroutine过载问题
我没有无限制地启动Goroutine,而是构建了一个工作池来高效地处理请求。
package main
import (
"fmt"
"sync"
"time"
)
const maxWorkers = 100 // 控制并发级别
const numJobs = 1000000
type Job struct {
ID int
}
func worker(id int, jobs <-chan Job, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
// 模拟CPU密集型处理
time.Sleep(1 * time.Millisecond)
fmt.Printf("Worker %d processed job %d\n", id, job.ID)
}
}
func main() {
jobs := make(chan Job, numJobs)
var wg sync.WaitGroup
for w := 1; w <= maxWorkers; w++ {
wg.Add(1)
go worker(w, jobs, &wg)
}
for j := 1; j <= numJobs; j++ {
jobs <- Job{
ID: j}
}
close(jobs)
wg.Wait()
}
它生效的原理:我没有启动数百万个Goroutine,而是将并发量限制为 maxWorkers
,从而减少了上下文切换,提高了CPU效率。
2. 从REST切换到gRPC以实现极快的API
最初,我们使用基于HTTP的REST来构建我们的API,但在高流量情况下,JSON序列化的开销以及多次往返通信开始严重影响性能。
解决方案是什么呢?迁移到使用二进制序列化和多路复用流的gRPC。
gRPC的实际应用:实现一个高速的Go API
syntax = "proto3";
package pb;
service PaymentService {
rpc ProcessPayment (PaymentRequest) returns (PaymentResponse);
}
message PaymentRequest {
string user_id = 1;
double amount = 2;
}
message PaymentResponse {
bool success = 1;
string transaction_id = 2;
}
package main
import (
"context"
"fmt"
"log"
"net"
"google.golang.org/grpc"
pb "path/to/protobuf"
)
type paymentServer struct {
pb.UnimplementedPaymentServiceServer
}
func (s *paymentServer) ProcessPayment(ctx context.Context, req *pb.PaymentRequest) (*pb.PaymentResponse, error) {
return &pb.PaymentResponse{
Success: true,
TransactionId: "txn_123456",
}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterPaymentServiceServer(s, &paymentServer{
})
fmt.Println("Payment Service running on port 50051...")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
它生效的原理:gRPC使用Protocol Buffers(Protobuf),在数据序列化方面,它比JSON快10倍,并且减少了网络开销。
3. 使用Redis进行负载均衡和缓存
在我们的API优化之后,我们使用NGINX结合轮询负载均衡的方式,将负载分散到多个实例上。但我很快意识到,重复的数据库查询让一切都慢了下来。
解决方案:使用Redis实现缓存
package main
import (
"fmt"
"github.com/go-redis/redis/v8"
"context"
)
var ctx = context.Background()
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
err := rdb.Set(ctx, "lastTransaction", "txn_123456", 0).Err()
if err != nil {
panic(err)
}
val, err := rdb.Get(ctx, "lastTransaction").Result()
if err != nil {
panic(err)
}
fmt.Println("Cached Transaction:", val)
}
它生效的原理:将结果存储在Redis中,减少了80%的数据库查询,将响应时间从50毫秒缩短到了5毫秒以内。
4. 使用Kubernetes和自动伸缩功能扩展到每秒100万次请求
最后,我使用Docker将所有内容进行了容器化处理,并将其部署到了一个启用了自动伸缩功能的Kubernetes集群中。
Kubernetes部署配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
replicas: 10 # 基于负载自动伸缩
selector:
matchLabels:
app: payment-service
template:
metadata:
labels:
app: payment-service
spec:
containers:
- name: payment-service
image: myregistry/payment-service:v1
ports:
- containerPort: 50051
它生效的原理:当流量激增时,Kubernetes会自动扩展实例数量,从而避免服务中断。
最终成果:每秒处理100万次请求,零停机时间
经过这些优化之后,我们的后端能够在保持低延迟和零停机时间的情况下,每秒处理100万次请求。优化后的Goroutine降低了CPU负载,gRPC取代了REST,提高了API性能,Redis缓存减少了数据库查询,Kubernetes在负载高峰时自动扩展了后端。
学到了什么呢?扩展系统并非只是简单地增加更多服务器来解决问题 —— 而是要对系统的每一层进行优化。
你在使用Golang时遇到过扩展方面的挑战吗?我们来讨论一下吧!