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,一经查实,立即删除!

相关文章

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): …

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

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

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

这个教程是用Visual Studio.net 2008建立&#xff0c;也可以使用VS2005&#xff0c;但你需要从这里下载安装Microsofts ASP.NET AJAX Extensions&#xff0c;AJAX和LINQ是微软目前主要焦点&#xff0c;两个看上去不足为奇&#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(): 光标移动是以字节为单位的整数移动. 三种模式:(分别为…

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

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

一次缓存性能问题排查

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

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

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

记录一次webpack3升级到webpack4过程

升级之前也参考了一些网上的教程。借鉴之&#xff0c;进行的自己的升级。一些版本为什么设为那个版本号也是参考别人的结果。 整体是按照先升级npm run dev&#xff1b;在升级npm run build的顺序。 首先升级webpack&#xff0c;在package.json文件中将webpack版本号修改为4.8.…

plsql如何执行存储过程_如何理解Spark应用的执行过程

从Spark应用的提交到执行完成有很多步骤&#xff0c;为了便于理解&#xff0c;我们把应用执行的整个过程划分为三个阶段。而我们知道Spark有多种运行模式&#xff0c;不同模式下这三个阶段的执行流程也不相同。本文介绍这三个阶段的划分&#xff0c;并概要介绍不同模式下各个阶…

vc如何打开plt图像_图像基本操作-open cv

import cv2 import matplotlib.pyplot as plt import numpy as np %matplotlib inline img cv2.imread(revolte.jpg) img # 读取的是array 格式 array([[[240, 243, 255],[239, 242, 255],[238, 241, 255],...,def cv_show(name,image):cv2.imshow(name,image)cv2.waitKey(0)c…

python调用api应用接口_Python接口测试之urllib2库应用

在接口测试中或者说在网络爬虫中&#xff0c;urllib2库是必须要掌握的一个库&#xff0c;当然还有优秀的requests库&#xff0c;今天重点来说urllib2库在接口测试中的应用。urllib2定义了很多的函数和类&#xff0c;这些函数和类能够帮助我们在复杂情况下获取URLS的内容。这些情…

CSS3透明背景表单

在线演示 本地下载

r-studio扫描后各种颜色_iPhone手机备忘录,原来还隐藏着扫描仪,你不会还不知道吧?...

大家好&#xff0c;今天就来给大家讲一讲&#xff0c;iPhone手机备忘录里面的一个隐藏功能&#xff0c;大家对手机备忘录应该都不陌生吧&#xff0c;iPhone手机的备忘录里有一个扫描仪的功能&#xff0c;可以将纸质文档变成电子档&#xff0c;不知道的小伙伴就和我一起来看看吧…

今早新闻的翻译

踏切で人身事故 東上線乱れ&#xff14;万人に影響  &#xff11;&#xff15;日午前&#xff17;時&#xff12;&#xff10;分ごろ、東京都板橋区常盤台&#xff13;丁目の東武東上線ときわ台―上板橋間の踏切で遮断機の下をくぐった女性が成増発池袋行き普通電車にはねられ…

TiDB 在小米的应用实践

作者&#xff1a;张良&#xff0c;小米 DBA 负责人&#xff1b;潘友飞&#xff0c;小米 DBA&#xff1b;王必文&#xff0c;小米开发工程师。一、应用场景介绍 MIUI 是小米公司旗下基于 Android 系统深度优化、定制、开发的第三方手机操作系统&#xff0c;也是小米的第一个产品…

java图片识别查看器模拟_[转载]windows照片查看器无法显示图片内存不足

问题描述最近在使用Windows照片查看器打开一个jpg文件的时候异常Windows照片查看器无法显示此图片&#xff0c;因为计算机上的可用内存可能不足。请关闭一些目前没有使用的程序或者释放部分硬盘空间(如果硬盘几乎已满)&#xff0c;然后重试问题分析这时我们按F11或者图片下方中…

智能云改-docker云迁移实战

本次安装的linux版本是centos7.4&#xff0c;docker安装不依赖任何环境&#xff0c;但是必须要连接网络&#xff0c;满足这一点就可以进行docker安装了。 一、安装&#xff1a; 1.删除就版本的docker输入命令&#xff1a;yum -y remove docker \docker-common \docker-selinux …

python词频统计代码_python统计词频

一、程序分析 &#xff08;1&#xff09;将文件读入缓冲区&#xff08;dst指文本文件存放路径&#xff0c;设置成形参&#xff0c;也可以不设&#xff0c;具体到函数里设置&#xff09; def process_file(dst): # 读文件到缓冲区try: # 打开文件 txtopen(dst,"r") ex…

Oracle的resouce、unlimited tablespace 及如何把数据导入不同的表空间

resouce是角色&#xff0c;unlimited tablespace是权限。 很多人在进行数据迁移时&#xff0c;希望把数据导入不同于原系统的表空间&#xff0c;在导入之后却往往发现&#xff0c;数据被导入了原表空间。本例举例说明解决这个问题:1.如果缺省的用户具有DBA权限那么导入时会按照…