這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
介紹 註:如果您對這篇文章有不同觀點,歡迎指正 - 我並不是這方面的專家。 我們從大量的 go 使用者中收集了有關於使用 defer 和 panic 的效能統計。不像其它的 APM/error 記錄公司,我們的重點不是告訴你有一個問題,而是實際解決問題的方法。這就是為什麼我們要使用 go。 這使我們看出對大家來說什麼是好的,壞的和醜的。 當你的項目中隱藏了一個大問題時,如果你只是來回查看通常是不能發現它的,直到你對它進行測試。 許多效能問題可能已經存在了數周。 |
一曲 翻譯於 2周前 0人頂 頂 翻譯的不錯哦! |
通常這些問題大部分可以簡單的通過 pprof 等方法分析目標程式來解決掉。 一旦你懷疑一個地方是錯誤的,你就應該先看看這個(pprof)。 然而,有時候,在經過幾天都不能找到問題源頭的時候,程式猿就會開始抱怨一切。 是資料庫的問題! 架構的問題! 是 cat(電腦輔助程式)的問題!(貓?。。。) 是記憶體回收行程的問題! 每當這個點上,團隊裡就會有個傢伙跳出來說,他使用的某某語言更好(-_-!,那樣如樣你不是個軟體工程師,你將被他搞得暈頭轉向。 我這裡不討論關於語言、cats(又是貓?。。)或者架構。 我只是想引導大家討論更有效率的方案。 |
一曲 翻譯於 2周前 0人頂 頂 翻譯的不錯哦! |
我確實想說說記憶體回收行程。它是在大多數語言中常常被提及的效能問題,但是,如果問題隨手就可以解決掉,那它也就不會引起人們的關注了。 反對記憶體回收行程的爭論,在相當長的一段時間內,有快速趕超寫入程式碼彙編的爭論。當然,如果你瞭解需要知道的一切,關於目標編譯器,目標架構,目標作業系統等,那麼你選擇一兩項內容做最佳化是說得通的,但是,這基本上是說,你比一些最好的編譯器作者還要更勝一籌。你可能具備這樣的能力。 然而, 這是相當高的要求。 實際上,有許多人們覺得相當不錯的記憶體回收行程,如 azul 的記憶體回收器. |
gones945 翻譯於 1周前 1人頂 頂 翻譯的不錯哦! |
背景“C程式員認為記憶體管理太重要了,不能由電腦自己處理。 Lisp 的程式員認為記憶體管理太重要了,不能由使用者來處理。“ —— Bjarne Stroustrup 要開始使用 - 大部分現代程式設計語言都使用自動化的垃圾收集機制。 無論是 Java 或是 C# 都有垃圾收集器。並且,一些解釋型語言也有。 約翰·麥卡錫(John McCarthy)1958年在歸途中發明了一種叫“記憶體回收行程”的東西。這是在成功發射 Sputnik 的第二年。之後他還發明了別的東西。 所以,這個概念已經有相當長的時間了,但一些開發人員似乎仍然對這個概念感到不安。為什麼呢? 要回答這個問題,我們應該看看各種方法的記憶體管理。 |
HappyBKs 翻譯於 2周前 0人頂 頂 翻譯的不錯哦! |
記憶體管理的方法 這裡有一些記憶體管理記憶體的方法,你可以: * 在編譯時間分配所有的記憶體 * 手動管理記憶體 * 自動管理記憶體 對於我們的定義,我們認為垃圾是使用完畢的已指派記憶體。同時,也為了這篇部落格,唯一的差別在於,垃圾可以由程式員手動清理(或者不清理),或者由程式設計語言自動清理。 在管理記憶體的過程中,存在許多問題。其中,有些問題是顯而易見的,有些問題則不容易發覺;有些問題不容易解決,而另一些問題甚至無人意識到它的存在。 這通常只是想當然,因為--你通常擁有一個記憶體回收行程!!:) |
gones945 翻譯於 1周前 0人頂 頂 翻譯的不錯哦! |
管理記憶體中的問題: * 引用不再使用的記憶體 * 使用未分配記憶體的指標 * 分配記憶體但從不釋放 * 使用釋放了記憶體的指標 * 沒有分配足夠的記憶體 * 分配記憶體大小錯誤 * 分配或者釋放記憶體太快或者太慢 * 在記憶體中儲存資料之前載入記憶體 * 安全的從記憶體中刪除敏感性資料 * 內部片段 * 外部片段 實際上--安全行業統計過,關於記憶體管理的錯誤使用,涉及到數十億美元。這顯然是一個問題。 |
gones945 翻譯於 1周前 0人頂 頂 翻譯的不錯哦! |
基本的記憶體管理操作 記憶體管理在電腦科學中有它的獨特之處,但是,這裡有兩種最常用和最重要的操作: 分配:這是分配一塊記憶體給指定請求的操作,同時保證分配器不會將此記憶體配置給其它地方,除非你說不。 釋放/重用:這就是當你說--這塊記憶體我已經使用完了,你可以按照你覺得合適的方式,自由的處理它了。記憶體並未“刪除”。記憶體也並未“釋放”到空中,或者其它地方。所做的,只是記憶體做了標記,這樣,下一次請求的時候,它就可以重新使用了。 |
gones945 翻譯於 1周前 0人頂 頂 翻譯的不錯哦! |
關於記憶體管理的錯誤假設預留 vs. 分配許多人困惑的一件事情是,分配記憶體和預留記憶體的差別。Go 語言 一開始就預留了一大塊記憶體(reserves a large chunk of memory right off the bat) 。這樣做有很多原因--其中之一是,連續vs.非連續的記憶體映射。你希望儘可能的減少片段。 對於我們而言,我們定義預留是對 malloc 函數的一次調用,然而,當我們決定使用這片記憶體的時候,記憶體配置才真正發生。只要存在未預留的記憶體空間,我們就可以隨心所欲的調用 malloc 函數。但是,當我們使用記憶體,並消耗完它的時候,我們就會得到記憶體不足(OOM)的錯誤資訊。 |
gones945 翻譯於 6天前 0人頂 頂 翻譯的不錯哦! |
作業系統中斷開發人員認為如果他們可以分配和釋放記憶體,他們的程式就不會因 GC 調用而中斷和變慢。 如果上述假設是真實的,那麼這種邏輯的問題所在是他們忘記了作業系統本身可以阻塞記憶體的分配,發送器切換到其他任務, 而且所有的裝置驅動程式都有中斷處理。你的代碼不是孤立存在的,有太多的因素公導致它產生暫停。 記憶體釋放不一定是免費的使用一個好的 GC 確實可以減少記憶體配置時間。同時釋放記憶體並不意味著立即釋放記憶體塊,通過你用的任何版本的 malloc 都會把它放在一個釋放列表裡 - 這樣可以協助阻止片段化。 當然,釋放記憶體有時間並不是免費的,它可以是隨機的,你需要分頁,導致糟糕的 IO。試想做大量的小記憶體配置,你會導致大量的片段,為了避免崩潰不得不重新分配堆棧大小。 |
wancheng 翻譯於 5天前 0人頂 頂 翻譯的不錯哦! |
片段我已經提到片段好幾次了 - 讓我告訴你一個簡單的例子。 比方說,我們有一個地方可以動態分配記憶體。有4個我們可以訪問的槽。但是,每次我們請求記憶體時,我們只能擷取2個插槽。
我們第一次分配,我們搶到了前兩個槽。然後,我們斷言我們之後不再需要第一個槽,所以我們“刪除”了。好吧,太棒了。現在看起來是這樣的。
現在一個大的資料庫作業來了,我們還需要一個槽。太糟糕了,我們的分配機制只允許我們一次擷取2個槽 - 所以會發生什嗎?我們得到了接下來的兩個槽。現在,我們已經分配了四分之三的槽,即使我們只用了其中的2個。這樣繼續下去,直到剩下的全都是我們不能夾這些已經分配的槽中間使用很多的空閑縫隙。 這是讓我們抓狂的片段。
|
HappyBKs 翻譯於 1周前 0人頂 頂 翻譯的不錯哦! |
配置策略
一般來說經典的分配策略可以有兩種形式:{單個列表或列表的數組}: 我不會介紹陣數組實現,也不會去談現有的那些更加奇特的策略。 我們說一個與上面的那個類似的鏈表。 現在有幾個關於在單空閑單鏈表找到一個合適的地址(槽)的策略方式供選擇: 首次適應(first fit):我們通過掃描整個鏈表,尋找第一個能夠符合我們的分配請求的位置。 迴圈首次適應(next fit):與首次適應類似,但我們追蹤上衣一次訪問的地方,然後從這裡開始尋找下一個符合請求要求的位置,所以你沒必要每次都從鏈表的頭開始掃描。 最佳策略(best fit):尋找記憶體中符合要求的最小的塊。 最壞策略(worst fit):尋找記憶體中符合要求的最大的塊。
|
HappyBKs 翻譯於 1周前 0人頂 頂 翻譯的不錯哦! |
Go的記憶體回收
要立刻實現此種記憶體回收方式,go還是一種很年輕的語言。毫無疑問在未來會有諸多改進,但這將是一個不平凡的任務,並且需要有人來為此努力。我確信並非所有人擁有這樣的才能。 兩種不同的方法在記憶體回收方面佔有很多大優勢:{跟蹤,引用計數} 1.4 經典的STW記憶體回收行程。 1.5 引入了一個並發收集器。 這兩者都是標記和清除,這是跟蹤收集器的一個實現。 他們是非移動的,這意味著記憶體回收行程將不會像可壓縮的收集器那樣移動引用。 您可以通過設定環境變數關閉記憶體回收行程:
GOGC=off ./myproggie 然而,這一點毫無意義,除非你要手動運行收集器 - 並且運行環境自己分配記憶體 - 所以你產生的有垃圾會有人來處理它。你每天都拖垃圾到某處傾倒? 當然不,你有更重要的事情要做。 至於為什麼go有一個垃圾收集器 - 我不認為這點足夠驅使人們不那麼做。其中這個語言的一個主要賣點是並發。傳統的記憶體管理已經很難實現來了,更別說現在要在一個並發的環境實現記憶體回收了。 請看horse’s mouth。 |
HappyBKs 翻譯於 1周前 0人頂 頂 翻譯的不錯哦! |
反模式 總算切入正題了…… 很多開發人員都選擇使用進階語言來管理記憶體事務。的許多開發人員經常碰到記憶體問題,因為所有的記憶體管理是從他們隱藏。這並不意味因為大部分記憶體都被影藏了。 但這不意味著他們是拙劣的開發人員,只是這些記憶體事務不在他們的思考範疇內。 這也不應該是他們在開發自己的代碼錢所必須修完的功課。 我猜想,我們所看到的使用者出現的各種問題大部分來自於一些簡單但卻常常被忽略的模式,如下: 字串串連 在 java 中有一個眾所周知的反模式,我們可以使用在一個環結構中連結字串而不是使用字串產生器。好的,go 也有自己的反模式。 需要知曉的事情是,當你 concat 字串的時候,go 分配到了一個新地址給新的值。因此,事情變得如此簡單: ?
| 1234 |
blah := "stuff" + "more stuff" + "even more stuff" for i:=0; i<10*10*10; i++ { blah += "and more stuff... " + "ad infinitum" } |
多勞者多得。 關於這種方式可以參考 stdlib @strings.Join 。你也可以使用 bytes.Buffer.WriteString。 |
HappyBKs 翻譯於 1周前 1人頂 頂 翻譯的不錯哦! |
手動管理記憶體用例你還是要手動管理?好吧。 一些真實用例可能包含以下: * 真正的大型堆 - 假設大型記憶體資料庫操作; * 管理相同大小同一類型的對象; * 即時系統 - 這不是一個好用例,因為這不是go的特長,你最好選擇一個完全不同的語言。 自己寫或者使用它來做老實講,恕我直言,除非你是這方面的專家(我肯定不是),這兒沒有太多好的用例。如果你是,我可能會建議你繼續使用go工作。 大部分人不會寫底層的裝置指令或者即時軟體。我不是講為即時而生的node.js/websockets 和別的類似軟體,我們講的是特定的作業系統,存在於你手機而不是Linux的第二作業系統。 如果你必須這樣做: * 你的磁碟有大量片段; * 你的磁碟不是安全執行緒; * 你覺得記憶體回收行程比較混亂,所以你可以關閉它,我們已經提到過這會有別的問題。 |
wancheng 翻譯於 5天前 0人頂 頂 翻譯的不錯哦! |
還是要自己管理? 好了,你可以採用 hacky,管理它可能不像在 C 語言裡那麼有效,僅僅使用 CGO 連結到它。 或 你可以使用 STDLIB 的東西: sync.Pool 或 你也可以使用緩衝通道。 此外,隨著有了越來越多參考資料,看看couchbase 和cloudflare如何?它們。 監控 還記得在本文的前面時,我說,大多數人都沒有意識到一個問題,直到它變成一個很大的問題或者你應該權衡的事情? 我們只是碰巧如此做,對於 go 來說是主動地:
我們甚至可以提醒你之前,事情變得無法忍受。 此外,由於我們只支援 go,我們工具是為 go 定製的,除了洗碗槽。(譯者補充:“洗碗槽”典故來自二戰時期的成語"everything but the kitchen sink" ,當時是指敵人炮火猛烈(除了洗碗槽外,各式各樣的炮彈齊發),現在指太多的東西)
|
HappyBKs 翻譯於 2周前 0人頂 頂 翻譯的不錯哦! |
封裝 go 語言有很多方法手動管理記憶體的方式,單著似乎無法解決我們需要解決的問題。 我認為我們應該關注一下像 Blade paper 提出的真正意義上的記憶體回收的問題的解決方案。 然而你真的沒有必要求助於這些工具。如果你發現自己在抱怨記憶體回收行程,那麼你應該立刻拿出 pprof並責難那該死的記憶體回收行程。如果真的是這樣,你確定要考慮改進它嗎? |
HappyBKs 翻譯於 1周前 0人頂 頂 翻譯的不錯哦! |
本文中的所有譯文僅用於學習和交流目的,轉載請務必註明文章譯者、出處、和本文連結
我們的翻譯工作遵照 CC 協議,如果我們的工作有侵犯到您的權益,請及時聯絡我們