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

1 Go的应用场景

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

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

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

baa1a586c037a1d4a5f3f70921eddfbc959d28d6

从图中,我们主要列举了几个时间分布,让大家对这几个数值有所了解。从客户端到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链路如下所示

1097b0dcb16ae543a309c8ec75957163d2defc70

为了让大家更好的了解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

ba226686c3217c67fef022c2df6b6f0da6d9fb6a

 ●  第二次再访问\hello,nginx会收到请求,go这个时候就不会收到请求。nginx里响应头里cache-status会与个hit内容,说明了nginx请求没有回源到go

920f0278a1bf8c9cfd731fc5f8121b9088ebb952

 ●  顺带提下nginx这个配置,还有额外的好处,如果后端go服务挂掉,这个缓存url\hello任然是可以返回数据的。nginx返回如下所

701f2a032c38c3874749f850f61206581b95ca3d

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,
wg.Done()
roomName: strconv.Itoa(i), }) }
}func BenchmarkPreAllocSlice(b *testing.B) {
}(&wg) } wg.Wait()
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,
wg.Done()
roomName: strconv.Itoa(i), }) }
}func BenchmarkSyncPoolSlice(b *testing.B) {
}(&wg) } wg.Wait()
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。

07b3fc1e6185796df4e3d9035784302fa7713167

2.2 实时数据优化2.2.1 减少io操作

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

1ac6c282bf0b00909c0dc49f028d05cbe5cddef5

我们对于量大的实时数据,做了三层缓存。第一层是白名单,这类数据主要是通过人工干预,预设一些内存数据。第二层是通过算法,将我们的一些比较重要的房间信息放入到服务内存里,第三层是通过请求量动态调整。通过这三层缓存设计。像大型赛事,大主播开播的时候,我们的请求是不会穿透到数据源,直接服务器的内存里已经将数据返回。这样的好处不仅减少了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
}

fe29e41bf10da13cf8f56b05bada93aa5c5a9ca9
从图中,我们可以看出采用并发拉去加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
以上问题都可以在相关文献中找到原因,具体原因请阅读文档。

 
When are function parameters passed by value?
As in all languages in the C family, everything in Go is passed by value. That is, a function always gets a copy of the thing being passed, as if there were an assignment statement assigning the value to the parameter. For instance, passing an int value to a function makes a copy of the int, and passing a pointer value makes a copy of the pointer, but not the data it points to. (See a later section for a discussion of how this affects method receivers.)
Map and slice values behave like pointers: they are descriptors that contain pointers to the underlying map or slice data. Copying a map or slice value doesn’t copy the data it points to. Copying

原文发布时间为:2018-11-26
本文作者:askuy
本文来自云栖社区合作伙伴“Golang语言社区”,了解相关信息可以关注“Golang语言社区”。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/276816.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AddTransient、AddSingleton、AddScoped 三者都应该在什么场景下使用

网上随便一搜&#xff0c;能搜出一大堆对三者进行区别分析的文章&#xff0c;但是呢&#xff0c;理论是一回事&#xff0c;实际使用又是另外一回事&#xff0c;到底在何种场景下应该使用何种注入方式呢&#xff1f; 通过这篇文章和我自身的实际经验&#xff0c;来说一说实际应用…

python交互界面用图片当背景_wxPython实现窗口用图片做背景

本文实例为大家分享了wxPython实现窗口用图片做背景的具体代码&#xff0c;供大家参考&#xff0c;具体内容如下 效果图&#xff1a;实现代码&#xff1a; #!/usr/bin/env python # -*- encoding:utf-8 -*- import wx class MyPanel(wx.Panel): def __init__(self,parent,id): …

css超出隐藏显示省略号

width: 300px; overflow: hidden;/*超出部分隐藏*/ text-overflow:ellipsis;/* 超出部分显示省略号 */ white-space: nowrap;/*规定段落中的文本不进行换行 */ 转载于:https://www.cnblogs.com/songmengwen1124/p/11354620.html

信号为E时,如何让语音识别脱“网”而出?

欢迎大家前往腾讯云社区&#xff0c;获取更多腾讯海量技术实践干货哦~ 本文由腾讯教育云发表于云社区专栏 一般没有网络时&#xff0c;语音识别是这样的 ▽ 而同等环境下&#xff0c;嵌入式语音识别&#xff0c;是这样的 ▽ 不仅可以帮您边说边识、出口成章&#xff0c;有个性化…

TwinSocketStream

使用TwinSocketStream 当为一个blocking连接实现一个线程时,你必须确定在连接的另一端的socket是准备写还是读.Blocking连接不会通知socket当它准备好写或读操作的时候.想看看连接是否准备好,使用TWinSocketStream对象.TWinSocketStream提供一个方法去帮助调整读或写操作时间的…

c 字符串转数字_C语言实现十进制转216进制、十六进制转十进制

1、十进制转2&#xff5e;16进制【问题描述】从键盘输入十进制整数num及转换的进制数base&#xff0c;将整数num转换为base进制(base取值范围为 2&#xff5e;16)。方法为&#xff1a;十进制数除base取余法&#xff0c;即十进制数除以base&#xff0c;余数为权位上的数&#xf…

Mysql循环语句,死循环解决办法

delimiter // #定义标识符为双斜杠 drop procedure if exists test; #如果存在test存储过程则删除 create procedure test() #创建无参存储过程,名称为testbegindeclare i int; #申明变量set i 0; …

QML-关于Qt.rgba()颜色无法正常显示问题

GitHub:八至 作者&#xff1a;狐狸家的鱼 本文链接&#xff1a;关于Qt.rgba()颜色的正确写法 当在正常给color属性写颜色的时候&#xff0c;用十六进制能正常显示&#xff0c;但是用Qt.rgba()时&#xff0c;颜色无法正常显示出来。 按照文档的正常写法&#xff1a; color: Qt.r…

一个简单的LINQ TO XML, AJAX 例子[译]

这个教程是用Visual Studio.net 2008建立&#xff0c;也可以使用VS2005&#xff0c;但你需要从这里下载安装Microsofts ASP.NET AJAX Extensions&#xff0c;AJAX和LINQ是微软目前主要焦点&#xff0c;两个看上去不足为奇&#xff0c;但背后都隐藏着巨大的潜力和力量。在这个示…

1.ASP.NET Core介绍

优点&#xff1a; 1.跨平台&#xff0c;高性能&#xff0c;开源&#xff0c;运行在.Net Core 或.Net Framework框架上&#xff08;asp.net core 3.0及以后只支持.Net Core&#xff09;。 2.各平台上开发工具支持&#xff0c;能够开发web应用&#xff0c;webapi&#xff0c;移动…

python3性能还低吗_Python3 vs. Python2 大作战,谁将是性能之王?

渲染 HTML 模板 django_html 测试将使用 Django 模板渲染引擎来构建一个 150x150 的 HTML 表格。 它利用了 Django 引擎的 Content 和 Template 类。如图所示&#xff0c;Python 3.7 比 Python 2.7 快 1.19 倍&#xff0c;但除此之外&#xff0c;其他 Python 3 版本都没有 Pyth…

python day08

一 文件处理补充 控制文件中光标移动 1 f.read(n): l.文件打开方式为文本模式的时,代表读取N个字符 ll.文件打开方式为b模式时,读取N个字节 强调:只有在read(n)模式下 N代表字符个数,除此之外的是以字节为单位 2 f.seek(): 光标移动是以字节为单位的整数移动. 三种模式:(分别为…

百度地图移动端开发和ArcGIS for Android 开发入门

打开链接http://pan.baidu.com/s/1eQpFNWY&#xff0c;可以查看 转载于:https://www.cnblogs.com/David-Young/p/3827058.html

VSCode 小鸡汤 第00期 —— 安装和入门

简介 这将是一个新的系列&#xff0c;将会以 Visual Studio Code&#xff08;后文都简称为 VSCode 啦&#xff09;的操作&#xff0c;环境配置&#xff0c;插件介绍为主&#xff0c;为大家不定期的介绍 VSCode 的一些操作技巧&#xff0c;所以取名 VSCode 小鸡汤&#xff0c;本…

.net生成文字图片

System.Drawing.Bitmap bmpnew Bitmap(Bitmap.FromFile(Server.MapPath("42.jpg")));//载入图片 System.Drawing.Graphics gGraphics.FromImage(bmp); g.DrawString("abcd测试",new Font("黑体",18),new SolidBrush(Co…

python可以调用windows资源吗_如何在Windows上用Python调用WinRar?还有问题吗

使用zipfile模块&#xff0c;我创建了一个脚本来提取我的归档文件&#xff0c;但是这个方法会破坏除txt文件之外的所有内容。在def unzip(zip): filelist [] dumpfold rM:\SVN_EReportingZones\eReportingZones\data\input\26012012 storage rM:\SVN_EReportingZones\eRepor…

SQLServer、Mysql、Oracle 创建、删除用户和授予用户权限

SQLServer 1、创建用户 CREATE LOGIN [用户名称] WITH PASSWORD用户密码, DEFAULT_DATABASE[默认数据库名称], CHECK_EXPIRATIONOFF, CHECK_POLICYOFF GO USE [数据库名称] GO CREATE USER [用户名称] FOR LOGIN [用户名称] WITH DEFAULT_SCHEMA[dbo] GO 2、授予全库只读权限 A…

一次缓存性能问题排查

概述以下分享的都跳过了很多坑&#xff0c;包括redis、tomcat环境配置、机器硬件配置等等问题&#xff08;与线上保持一致&#xff0c;或者硬件性能减配系数&#xff0c;例如线上&#xff1a;8C16G&#xff0c;压测&#xff1a;4C8G&#xff0c;系数简单相差2倍&#xff09;&am…

python 多个列表_Python同时迭代多个列表

没有人会记得死的东西&#xff0c;所以要活下去&#xff0c;咬牙切齿的活下去&#xff01; import sys from itertools import chain from random import randint reload(sys) sys.setdefaultencoding(utf-8) 案例一 要求&#xff1a;一个班级有10个人&#xff0c;考试语数外三…

再读新疆系列(六)——吹拂“卡拉库里湖”的风

一下飞机&#xff0c;导游王雪作了简短的自我介绍&#xff0c;马不停蹄地带着我们经喀什市区直接向帕米尔高原的“卡拉库里”湖走。 问午饭在哪吃&#xff1f; 答&#xff1a;“湖边”。 “几点能到&#xff1f;” “大约下午二点多。”妈呀&#xff0c;又经历一次残酷的饥饿历…