百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

go语言项目优化(经验之谈)

mhr18 2024-11-25 10:40 21 浏览 0 评论

我的课题主要分为以下三章,斗鱼在GO的应用场景,GO在业务中如何优化,我们在GO中踩过了哪些坑。

1 Go应用场景

在斗鱼我们将GO的应用场景分为以下三类,缓存类型数据,实时类型数据,CPU密集型任务。这三类应用场景都有着各自的特点。

  • 缓存类型数据在斗鱼的案例就是我们的首页,列表页,这些页面和接口的特点是不同用户在同一段时间得到的数据都是一样的,通常这些缓存类型数据的包都比较大,并且这些数据没有用户态,具有一定价值,很容易被爬虫爬取。
  • 实时类型数据在斗鱼的案例就是视频流,关注数据,这些数据的特点是每次请求获取的数据都不一样。并且容易因为某些业务场景导流,例如主播开播提醒,或者某个大型赛事开赛,会在短时间内同时涌入大量用户,导致服务器流量陡增。
  • CPU密集型任务在斗鱼的案例就是我们的列表排序引擎。斗鱼的列表排序数据源较多,算法模型复杂。如何在短时间算完这些数据,提高列表的导流能力对于我们也是一个比较大的挑战。

针对这三种业务场景如何做优化,我们也是走了不少弯路。而且跟一些程序员一样,容易陷入到特定的技术和思维当中去。举个简单的例子。早期我们在优化GO的排序引擎的时候,上来就想着各种算法优化,引入了跳跃表,归并排序,看似优化了不少性能,benchmark数据也比较好看。但实际上排序的算法时间和排序数据源获取的时间数量级差别很大。优化如果找不对方向,业务中的优化只能是事倍功半。所以在往后的工作中,我们基本上是按照如下图所示的时间区域,找到业务优化的主要耗时区域。

从图中,我们主要列举了几个时间分布,让大家对这几个数值有所了解。从客户端到CDN回源到机房的时间大概是50ms到300ms。机房内部服务端之间通信大概是5ms到50ms。我们访问的内存数据库redis返回数据大概是500us到1ms。GO内部获取内存数据耗时ns级别。了解业务的主要耗时区域,我们就可以知道应该着重优化哪个部分。

2 Go的业务优化

2.1 缓存数据优化

对于用户访问一个url,我们假定这个url为/hello。这个url每个用户返回的数据结构都是一样的。我们通常有可能会向下面示例这样做。对于开发而言,代码是最直观最可控的。但这种方式通常只是实现功能,但并不能够提升用户体验。因为对于缓存数据我们没有必要每次让CDN回源到源站机房,增加用户访问的链路时间。

    // Echo instance
    e := echo.New()
    e.Use(mw.Cache)    // Routers
    e.GET("/hello", handler(HomeHandler))

2.1.1 添加CDN缓存

所以接下来,对于缓存数据,我们不会用go进行缓存,而是在前端cdn进行缓存优化。CDN链路如下所示

为了让大家更好的了解CDN,我先问大家一个问题。从北京到深圳用光速行驶,大概要多久(7ms)。所以如图所示,当一个用户访问一个缓存数据,我们要尽量的让数据缓存在离用户近的CDN节点,这种优化方式称为CDN缓存优化。通过该技术,CDN节点会把附件用户的请求,聚合到一起统一回源到源站机房。这样可以不仅节省机房流量带宽,并且从物理层面上减少了一次链路。使得用户可以更快的获取到缓存数据。
为了更好的模拟CDN的缓存,我们拿nginx+go来描述这个流程。nginx就相当于图中的基站,go服务就相当于北京的源站机房。
nginx 配置如下所示:

server {    
  listen 8088;    
  location ~ /hello {        
  		access_log /home/www/logs/hello_access.log;        
  		proxy_pass http://127.0.0.1:9090;        
  		proxy_cache vipcache;        
  		proxy_cache_valid 200 302 20s;        
  		proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504 http_403 http_404;        
  		add_header Cache-Status "$upstream_cache_status";
    }
}

go 代码如下所示

package main

import (    "fmt"
    "io"
    "net/http")

func main() {
    http.Handle("/hello", &ServeMux{})
    err := http.ListenAndServe(":9090", nil)    if err != nil {
        fmt.Println("err", err.Error())
    }
}

type ServeMux struct {
}

func (p *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Println("get one request")
    fmt.Println(r.RequestURI)
    io.WriteString(w, "hello world")
}

启动代码后,我们可以发现。

  • 第一次访问\hello,nginx和go都会收到请求,nginx的响应头里cache-status中会有个miss内容,说明了nginx请求穿透到go


  • 第二次再访问\hello,nginx会收到请求,go这个时候就不会收到请求。nginx里响应头里cache-status会与个hit内容,说明了nginx请求没有回源到go
  • 顺带提下nginx这个配置,还有额外的好处,如果后端go服务挂掉,这个缓存url\hello任然是可以返回数据的。nginx返回如下所

2.1.2 CDN去问号缓存

正常用户在访问\hellourl的时候,是通过界面引导,然后获取\hello数据。但是对于爬虫用户而言,他们为了获取更加及时的爬虫数据,会在url后面加各种随机数\hello?123456,这种行为会导致cdn缓存失效,让很多请求回源到源站机房。造成更大的压力。所以一般这种情况下,我们可以在CDN做去问号缓存。通过nginx可以模拟这种行为。nginx配置如下:

server {    
  listen 8088;    
  if ( $request_uri ~* "^/hello") {        
    rewrite /hello? /hello? break;
  }    
  location ~ /hello {        
    access_log /home/www/logs/hello_access.log;        
    proxy_pass http://127.0.0.1:9090;        
    proxy_cache vipcache;        
    proxy_cache_valid 200 302 20s;        
    proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504 http_403 http_404;        
    add_header Cache-Status "$upstream_cache_status";
  }
}

2.1.3 大流量上锁

之前我们有讲过如果突然之间有大型赛事开播,会出现大量用户来访问。这个时候可能会出现一个场景,缓存数据还没有建立,大量用户请求仍然可能回源到源站机房。导致服务负载过高。这个时候我们可以加入proxy_cache_lock和proxy_cache_lock_timeout参数

server {    
  listen 8088;    
  if ( $request_uri ~* "^/hello") {        
    rewrite /hello? /hello? break;
  }    
  location ~ /hello {        
    access_log /home/www/logs/hello_access.log;        
    proxy_pass http://127.0.0.1:9090;        
    proxy_cache vipcache;        
    proxy_cache_valid 200 302 20s;        
    proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504 http_403 http_404;        
    proxy_cache_lock on;        
    procy_cache_lock_timeout 1;        
    add_header Cache-Status "$upstream_cache_status";
  }
}

2.1.4 数据优化

在上面我们还提到斗鱼缓存类型的首页,列表页。这些页面接口数据通常会返回大量数据。在这里我们拿Go模拟了一次请求中获取120个数据的情况。将slice分为三种情况,未预设slice的长度,预设了slice长度,预设了slice长度并且使用了sync.map。代码如下所示。这里面每个goroutine相当于一次http请求。我们拿benchmark跑一次数据

package slice_testimport (
    "strconv"
    "sync"
    "testing")// go test -bench="."type Something struct {
    roomId   int
    roomName string}func BenchmarkDefaultSlice(b *testing.B) {
    b.ReportAllocs()    var wg sync.WaitGroup    for i := 0; i < b.N; i++ {
        wg.Add(1)        go func(wg *sync.WaitGroup) {            for i := 0; i < 120; i++ {
                output := make([]Something, 0)
                output = append(output, Something{
                    roomId:   i,
                    roomName: strconv.Itoa(i),
                })
            }
            wg.Done()
        }(&wg)
    }
    wg.Wait()
}func BenchmarkPreAllocSlice(b *testing.B) {
    b.ReportAllocs()    var wg sync.WaitGroup    for i := 0; i < b.N; i++ {
        wg.Add(1)        go func(wg *sync.WaitGroup) {            for i := 0; i < 120; i++ {
                output := make([]Something, 0, 120)
                output = append(output, Something{
                    roomId:   i,
                    roomName: strconv.Itoa(i),
                })
            }
            wg.Done()
        }(&wg)
    }
    wg.Wait()

}func BenchmarkSyncPoolSlice(b *testing.B) {
    b.ReportAllocs()    var wg sync.WaitGroup    var SomethingPool = sync.Pool{
        New: func() interface{} {
            b := make([]Something, 120)            return &b
        },
    }    for i := 0; i < b.N; i++ {
        wg.Add(1)        go func(wg *sync.WaitGroup) {
            obj := SomethingPool.Get().(*[]Something)            for i := 0; i < 120; i++ {
                some := *obj
                some[i].roomId = i
                some[i].roomName = strconv.Itoa(i)
            }
            SomethingPool.Put(obj)
            wg.Done()
        }(&wg)
    }
    wg.Wait()
}

得到以下结果。可以从最慢的12us降低到1us。

2.2 实时数据优化

2.2.1 减少io操作

上面我们提到了在业务突然导流的情况下,我们服务有可能在短时间内涌入大量流量,如果不对这些流量进行处理,有可能会将后端数据源击垮。还有一种情况在突发流量下像视频流这种请求如果耗时较长,用户在长时间得不到的数据,有可能进一步刷新页面重新请求接口,造成二次攻击。所以我们针对这种实时接口,进行了合理优化。

我们对于量大的实时数据,做了三层缓存。第一层是白名单,这类数据主要是通过人工干预,预设一些内存数据。第二层是通过算法,将我们的一些比较重要的房间信息放入到服务内存里,第三层是通过请求量动态调整。通过这三层缓存设计。像大型赛事,大主播开播的时候,我们的请求是不会穿透到数据源,直接服务器的内存里已经将数据返回。这样的好处不仅减少了IO操作,而且还对流量起到了镇流的作用,使流量平稳的到达数据源。

其他量级小的非实时数据,我们都是通过etcd进行推送

2.2.2 对redis参数调优

要充分理解redis的参数。只有这样我们才能根据业务合理调整redis的参数。达到最佳性能。maxIdle设置高点,可以保证突发流量情况下,能够有足够的连接去获取redis,不用在高流量情况下建立连接。maxActive,readTimeout,writeTimeout的设置,对redis是一种保护,相当于go服务对redis这块做的一种简单限流,降频操作。

redigo 参数调优
maxIdle = 30
maxActive =  500
dialTimeout = "1s"
readTimeout = "500ms"
writeTimeout = "500ms"
idleTimeout = "60s"

2.2.3 服务和redis调优

因为redis是内存数据库,响应速度比较块。服务里可能会大量使用redis,很多时候我们服务的压测,瓶颈不在代码编写上,而是在redis的吞吐性能上。因为redis是单线程模型,所以为了提高速度,我们通常做的方式是采用pipeline指令,增加redis从库,这样go就可以根据redis数量,并发拉取数据,达到性能最佳。以下我们模拟了这种场景。

package redis_testimport (
    "sync"
    "testing"
    "time"
    "fmt")// go testfunc Test_OneRedisData(t *testing.T) {
    t1 := time.Now()    for i := 0; i < 120; i++ {
        getRemoteOneRedisData(i)
    }
    fmt.Println("Test_OneRedisData cost: ",time.Since(t1))
}func Test_PipelineRedisData(t *testing.T) {
    t1 := time.Now()
    ids := make([]int,0, 120)    for i := 0; i < 120; i++ {
        ids = append(ids, i)
    }
    getRemotePipelineRedisData(ids)
    fmt.Println("Test_PipelineRedisData cost: ",time.Since(t1))

}func Test_GoroutinePipelineRedisData(t *testing.T) {
    t1 := time.Now()
    ids := make([]int,0, 120)    for i := 0; i < 120; i++ {
        ids = append(ids, i)
    }
    getGoroutinePipelineRedisData(ids)
    fmt.Println("Test_GoroutinePipelineRedisData cost: ",time.Since(t1))
}func getRemoteOneRedisData(i int) int {    // 模拟单个redis请求,定义为600us
    time.Sleep(600 * time.Microsecond)    return i
}func getRemotePipelineRedisData(i []int) []int {
    length := len(i)    // 使用pipeline的情况下,单个redis数据,为500us
    time.Sleep(time.Duration(length)*500*time.Microsecond)    return i
}func getGoroutinePipelineRedisData(ids []int) []int {
    idsNew := make(map[int][]int, 0)
    idsNew[0] = ids[0:30]
    idsNew[1] = ids[30:60]
    idsNew[2] = ids[60:90]
    idsNew[3] = ids[90:120]

    resp := make([]int,0,120)    var wg sync.WaitGroup    for j := 0; j < 4; j++ {
        wg.Add(1)        go func(wg *sync.WaitGroup, j int) {
            resp = append(resp,getRemotePipelineRedisData(idsNew[j])...)
            wg.Done()
        }(&wg, j)
    }
    wg.Wait()    return resp
}


从图中,我们可以看出采用并发拉去加pipeline方式,性能可以提高5倍。
redis的优化方式还有很多。例如

1.增加redis从库2.对批量数据,根据redis从库数量,并发goroutine拉取数据3.对批量数据大量使用pipeline指令4.精简key字段5.redis的value解码改为msgpack

3 GO的踩坑经验

踩坑代码地址: https://github.com/askuy/gopherlearn

3.1 指针类型串号

3.2 多重map上锁问题

3.3 channel使用问题

4 相关文献

坑踩得多,说明书看的少。
https://stackoverflow.com/questions/18435498/why-are-receivers-pass-by-value-in-go/18435638
以上问题都可以在相关文献中找到原因,具体原因请阅读文档。

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: