web server性能優化淺談

来源:https://www.cnblogs.com/Jack47/archive/2018/07/31/performance-optimizing-for-gateway.html
-Advertisement-
Play Games

作者: "Jack47, ZhiYan" 轉載請保留作者和 "原文出處" 性能優化,優化的東西一定得在主路徑上,結合測量的結果去優化。不然即使性能再好,邏輯相對而言執行不了幾次,其實對提示性能的影響微乎其微。記得抖哥以前說多隆在幫忙查廣告搜索引擎的問題,看到了一處代碼,激動的說這裡用他的辦法,性能可 ...


作者:Jack47, ZhiYan

轉載請保留作者和原文出處

性能優化,優化的東西一定得在主路徑上,結合測量的結果去優化。不然即使性能再好,邏輯相對而言執行不了幾次,其實對提示性能的影響微乎其微。記得抖哥以前說多隆在幫忙查廣告搜索引擎的問題,看到了一處代碼,激動的說這裡用他的辦法,性能可以提升至少10倍。但實際上,這裡的邏輯基本走不到 face_palm。

性能優化的幾個跟語言無關的大方向:

減少演算法的時間複雜度

例子1

我們實現了一個CallBack的機制,一段執行流程里,會有多個plugin,每個plugin可以添加callback,每個callback有唯一的名字;添加callback時,需要註意覆蓋的問題,如果覆蓋了,需要返回老的callback。一開始我們的實現機制是使用數組,這樣添加時,需要挨個遍歷,查看是否時覆蓋的情況。Update操作的時間複雜度為O(n);後來我們添加了一個輔助的Map,用來存儲 <name, callbackIdx>的映射關係。Update的平均時間複雜度降低為O(1)

例子2

在我們的pipeline場景里,類似net/http里的context,我們有個task的概念的。每個階段(plugin)都可以向裡面塞數據,一開始為了支持cancel某個階段,重新執行這個階段的功能,我們是使用嵌套,類似遞歸的方式。這樣就可以很方便的撤銷某個階段放入的數據。但是這種設計,如果要從裡面取數據,需要層層遍歷,類似遞歸一樣,時間複雜度為O(n);因為每個plugin都會與task打交道,所以這裡 task里數據的存取是高頻操作,而且我們後來經過權衡,覺得支持取消掉某個階段對task的操作,不是必須的,不支持也沒關係,所以後來簡化了task的設計,直接用一個map來做,這樣時間複雜度又降下來了。

根據業務邏輯,設計優化的數據結構

我們有個場景,是要對URL執行類似歸一化的操作,把裡面重覆的\字元刪掉,比如 \\ -> \。這個邏輯對於網關,是高頻邏輯,因為每個請求來了,都需要判斷,但是真正要刪掉重覆的\的操作,其實比較少,大部分場景是檢查完,發現正常,不需要做修改。

一開始我們的實現是把url字元挨個檢查,沒問題的放入 bytes.Buffer 中,最終返回 buffer.String();後來我們優化了一下,採用了標準庫中 path/path.go 中的 Lazybuf 的方式,LazyBuf中發現要寫入的字元和基準的字元不一樣時,才分配記憶體來存儲修改後的字元串,不然最終還是基準的字元,直接返回就行,避免了無謂的記憶體拷貝操作。

這裡其實體現了一個小技巧,儘量想想自己需要的操作,是否標準庫里有,同時也要多看看標準庫的實現,吸取經驗。

儘量減少磁碟IO次數

IO操作儘量批量進行。比如我們的網關會記錄訪問日誌,類似Nginx的access.log。在生產環境/壓測環境下,會生成大量的日誌,雖然操作系統寫入文件是有緩衝的,但是這個緩衝機制我們應用程式沒法直接控制,而且寫入文件時調用系統API,也比較耗時。我們可以在應用層面,給日誌留緩衝區(buffer),定時或達到一定量(4k,跟虛擬文件系統的塊大小保持一致)時調用操作系統IO操作來寫入日誌。

總結一下,就是寫入日誌是非同步的,同時是攢夠一批之後,再調用操作系統的寫入

具體實現:進來的數據,先放到一個2048位元組大小的channel里,由一個固定的go routine負責不斷的從channel里讀取數據,寫入到buffered io里。這裡2048位元組的channel,類似隊列一樣,是有削峰作用的。當有大批日誌寫入時,channel可以暫時緩衝一下,降低 buffer.io 真正flush的頻率。;寫入文件時,套上一個 bufio.Writer(size=512),即內部是有512位元組大小的緩衝區,滿了才使用整塊數據調用Write();

儘量復用資源

資源的申請和釋放,跟記憶體(也是一種資源)的申請和釋放其實是一樣的,儘量復用,避免重覆/頻繁申請;
比如下麵的這個time.Tick,適用於使用者不需要關閉它,即非頻繁調用的情況。使用它很方便,但是要註意,它沒法關閉,所以垃圾回收器也沒法回收它。來看一下下麵的這段代碼修改記錄:

+   ticker := time.NewTicker(time.Second)
+   defer ticker.Stop()
+
    for {
        select {
-       case <-time.Tick(time.Second):
+       case <-ticker.C:

修改前,for迴圈里會頻繁創建time.Ticker,但都沒有回收機制。改動後,for迴圈里復用同一個time.Ticker,而且會在當前函數執行結束時釋放time.Ticker。

sync.Map的使用

其實看清楚map.go里的註釋,註意使用場景。

sync.Map適合兩種用途:

  1. 指定的key,value只會被寫入一次,但是會被讀取很多次
  2. 多個goroutine讀取、寫入、覆蓋的數據都是沒有交集的

只有上述情況下,sync.Map才能相比Go map搭配單獨的Mutex或RWMutex而言,顯著降低鎖的競爭,均攤複雜度是常數(amortized constant time)

大部分情況下,應該用 map ,然後用單獨的鎖或者同步機制,這樣類型安全,而且可以有其他的邏輯

鎖相關

Mutexes

鎖在滿足以下條件的情況下,是很快的:

  • 沒有其他人競爭 (想象為擠公交車,此時沒人跟你搶,你直接上車)
  • 鎖覆蓋的代碼,執行時間非常快 (想象為擠公交車,大家速度都很快,嗖嗖就上去了,下一個人等待上一個人擠上去的時間很短)

當競爭越激烈,鎖的性能下降的越厲害。

Reference:

locks aren't slow, lock contention is

鎖的粒度儘量小

比如我們的pipeline生命周期的管理,一開始是通過一把大鎖來控制併發的,後續優化時,發現裡面可以細分成兩塊,各自可以用一把鎖來控制,這樣鎖的粒度變小,併發程度會提高。

這裡比較好的例子是BigCache的實現。它使用分片(sharding)的方式,
跟Java 7里的concurrent hash map的實現類似,對數據進行分片,分片之間是獨立的,可以併發的進行寫操作。對細分後的分片進行併發控制,這樣能有效減小鎖的粒度,讓併發度儘可能高。

Reference: Writing Fast Cache Service in Go

RWMutexes

  1. 是否有多讀少寫的場景,如果是,儘量用讀寫鎖;這樣儘量把寫鎖的粒度縮小,能用讀鎖解決的,就不需要用寫鎖,真正需要修改結果時,才使用寫鎖。

比如:

func (b *DataBucket) QueryDataWithBindDefault(key interface{}, defaultValueFunc DefaultValueFunc) (interface{}, error) {
    先上讀鎖,看key是否存在,如果存在,就返回 // 大部分情況下是這樣,所以這個優化肯定很有意義
    否則,上寫鎖,把預設值加上 // 這種情況只會發生一次
 }

儘量使用無鎖的方式:

是否真的需要加鎖?是否能用CAS的操作來代替Mutex?

例如:

利用 atmoic int stopped = 0/1 來代表是否停止,需要停止時,設置為1。

golang里Atomic操作有:Atomic.CompareAndSet, LoadInt(), StoreInt()

如果利用某個變數代表現在是否在幹活,close時需要等別人幹完活,那麼在close時,需要通過spin的方式等待幹活的人結束:

for atomic.LoadInt(&doing) > 0 {
    sleep(1ms)
}

記憶體相關

減少記憶體分配的次數

生成字元串時,儘量寫入 bytes.Buffer, 而不是用 fmt.Sprintf()

+   var repeatingRune rune
-   result := string(s[0])  
+   result := bytes.NewBuffer(nil)
    for _, r := range s[:1] {
-       result = fmt.Sprintf("%s%s", result, string(r)) 
+       result.WriteRune(r)
+   }

數據結構初始化時,儘量指定合適的容量

比如Java或者Go裡面,如果數組,Map的大小已知,可以在聲明時指定大小,這樣避免後續追加數據時需要擴展內部容量,造成多次記憶體分配

-   eventStream := make(chan cluster.Event)
+   eventStream := make(chan cluster.Event, 1024)

語言(Go)相關

語言相關的其實還有很多,但是隨著語言的發展,基本上都會被解決掉,所以這裡只提一下下麵的這個,對Go語言感興趣的同學,可以看So You Wanna Go Fast

避免記憶體拷貝

如下的代碼,兩者有什麼區別?

-   for _, bucket := range s.buckets {
-       bucket.Update(v)
+   for i := 0; i < len(s.buckets); i++ {
        buckets[i].Update(v)

修改前的這種方式,bucket是通過拷貝生成的臨時變數;而且這種方式下,由於操作的是臨時變數,所以 s.buckets並不會被更新!

Go routine雖好,也有代價

我們的網關,一開始的時候,由於大家也都是剛接觸Go語言,用Go routine用的也順手,所以很喜歡用Go routine;比如我們的主流程里,需要記錄本次請求的一些指標,為了不影響主流程的執行,這些記錄指標的邏輯都是啟動一個新的go routine去執行的。後來發現我們在一臺機器上,一個程式里,某一時刻啟動了十萬計的go routine,而這些go routine生命周期很短,會不斷的銷毀和創建。我也簡單的用Go Benchmark測試模擬了一個場景,測試了之後發現go routine數量上去後,性能下降很大,說明此時的調度開銷也比較大了。後來我們修改了設計,讓大家把需要更新的數據放到channel里,啟動固定的go routine去做更新的事情,這樣可以避免頻繁創建go routine的情況。

使用多個http.Client來發送請求

一開始我們是通過一個http.Client來發送同一個API的請求,後來擔心這裡可能存在併發的瓶頸,嘗試了創建多個http.Client,發送時隨機使用某一個發送的機制,發現性能提升了。其實性能有多少提升,取決於使用場景的,還是得實際測量,用數值說話,我們的方法不一定對你們有用!

Go語言在benchmark方面,提供了很多強有力的工具,可以參加下麵的文章:

High performance go workshop

An Introduction to go tool trace

Writing and Optimizing Go code

Go tooling essentials

好了,以上就是所有內容了,歡迎留下你的性能優化的思路和方法!



如果您看了本篇博客,有所收穫,請點擊右下角的“推薦”,讓更多人看到!

打賞也是對自己的肯定 pay_weixin 微信打賞
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ...
  • 1. 學習計劃 1、使用freemarker實現網頁靜態化 2、ActiveMq同步生成靜態網頁 2. 網頁靜態化 可以使用Freemarker實現網頁靜態化。 2.1. 什麼是freemarker FreeMarker是一個用Java語言編寫的模板引擎,它基於模板來生成文本輸出。FreeMarke ...
  • 1、類的編譯和運行簡易過程: java的源碼文件(也稱為編譯單元,以.java為尾碼的文件) ↓ 文件內最多只能有一個public修飾的類,否則編譯器報錯;某個類被public修飾,該類名必需與文件名稱一致; java的位元組碼文件(以.class為尾碼的文件) ↓ 源碼文件經過編譯器編譯後產生的位元組 ...
  • 命名空間的小弟作用域 在這要明確一個觀點,在Python中萬物皆對象,而變數指向的就是對象。 變數可以是 類名,函數名,儲存數據的變數…… 對象可以是 類 ,被封裝的一段代碼(函數),數據…… 命名空間 命名空間是從名字到對象的映射。在Python大多數命名空間目前以字典的形式實現。變數名是“鍵”, ...
  • 在Java語言的Arrays類下提供了一系列排序(sort)方法,幫助使用者對各種不同數據類型的數組進行排序. 在1.7之後的版本中, Arrays.sort()方法在操作過程中實際調用的是DualPivotQuicksort類下的sort方法,DualPivotQuicksort和Arrays一樣 ...
  • #include #include //提供malloc()原型 typedef struct LNode *List; typedef int ElementType; //定義數據結構的自定義名稱 struct LNode{ ElementType Data; //數據域 List Next; ... ...
  • 1.什麼是列表 列表是一種可變的數據類型 列表由[]來表示,每一項元素使用逗號隔開,列表什麼都能裝,叫做能裝對象的對象 列表可以裝大量的數據 2.列表的索引和切片 列表和字元串一樣,也有索引和切片,只不過切出來的內容是列表 索引的下標從0開始 切片:[起始位置:結束位置:步長] 3.列表的增刪改查 ...
  • 1.0 新建運行環境 命令: pyvip@Vip:~$ mkvirtualenv -p /usr/bin/python3 Django2Running virtualenv with interpreter /usr/bin/python3Using base prefix '/usr'New py ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...