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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...