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.pswp.cn/news/276816.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/276816.shtml
英文地址,請注明出處:http://en.pswp.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;又經歷一次殘酷的饑餓歷…