當前位置:首頁 » 硬碟大全 » golang輕量緩存
擴展閱讀
webinf下怎麼引入js 2023-08-31 21:54:13
堡壘機怎麼打開web 2023-08-31 21:54:11

golang輕量緩存

發布時間: 2023-07-16 03:38:05

A. GoLang -- gRPC框架四大服務

在 gRPC 里客戶端應用可以像調用本地對象一樣直接調用另一台不同的機器上服務端 應用的方法,使得您能夠更容易地創建分布式應用和服務。與許多 RPC 系統類似,gRPC 也是基於以下理念:定義一個服務,指定其能夠被遠程調用的方法(包含參數和返回類型)。在服務端實現這個介面,並運行一個 gRPC 伺服器來處理客戶端調用。在客戶端擁有一個存根能夠像服務端一樣的方法。

gRPC 客戶端和服務端可以在多種環境中運行和交互 - 從 google 內部的伺服器到你自己的筆記本,並且可以用任何 gRPC 支持的語言來編寫。所以,你可以很容易地用 Java 創建一個 gRPC 服務端,用 Go、Python、Ruby 來創建客戶端。此外,Google 最新 API 將有 gRPC 版本的介面,使你很容易地將 Google 的功能集成到你的應用里。

gRPC 默認使用 protocol buffers,這是 Google 開源的一套成熟的結構數據序列化機制(當然也可以使用其他數據格式如 JSON)。名叫 proto3 的新風格的 protocol buffers,它擁有輕量簡化的語法、一些有用的新功能,並且支持更多新語言。當前針對 Java 和 C++ 發布了 beta 版本,針對 JavaNano(即 Android Java)發布 alpha 版本,在protocol buffers Github 源碼庫里有 Ruby 支持, 在golang/protobuf Github 源碼庫里還有針對 Go 語言的生成器, 對更多語言的支持正在開發中。

有了 gRPC, 我們可以一次性的在一個 .proto 文件中定義服務並使用任何支持它的語言去實現客戶端和伺服器,反過來,它們可以在各種環境中,從Google的伺服器到你自己的平板電腦—— gRPC 幫你解決了不同語言及環境間通信的復雜性.使用 protocol buffers 還能獲得其他好處,包括高效的序列號,簡單的 IDL 以及容易進行介面更新。

現在讓我們來仔細了解一下當 gRPC 客戶端調用 gRPC 服務端的方法時到底發生了什麼。我們不究其實現細節,關於實現細節的部分,你可以在我們的特定語言頁面里找到更為詳盡的內容。

首先我們來了解一下最簡單的 RPC 形式:客戶端發出單個請求,獲得單個響應。

服務端流式 RPC 除了在得到客戶端請求信息後發送回一個應答流之外,與我們的簡單例子一樣。在發送完所有應答後,服務端的狀態詳情(狀態碼和可選的狀態信息)和可選的跟蹤元數據被發送回客戶端,以此來完成服務端的工作。客戶端在接收到所有服務端的應答後也完成了工作。

客戶端流式 RPC 也基本與我們的簡單例子一樣,區別在於客戶端通過發送一個請求流給服務端,取代了原先發送的單個請求。服務端通常(但並不必須)會在接收到客戶端所有的請求後發送回一個應答,其中附帶有它的狀態詳情和可選的跟蹤數據。

雙向流式 RPC ,調用由客戶端調用方法來初始化,而服務端則接收到客戶端的元數據,方法名和截止時間。服務端可以選擇發送回它的初始元數據或等待客戶端發送請求。 下一步怎樣發展取決於應用,因為客戶端和服務端能在任意順序上讀寫 - 這些流的操作是完全獨立的。例如服務端可以一直等直到它接收到所有客戶端的消息才寫應答,或者服務端和客戶端可以像"乒乓球"一樣:服務端後得到一個請求就回送一個應答,接著客戶端根據應答來發送另一個請求,以此類推。

通過運行下面的命令克隆並安裝grpc-go代碼庫:

下載protobuf源碼包

安裝golang-protobuf

第一步使用 protocol buffers去定義 gRPC service 和方法 request 以及 response 的類型。

要定義一個服務,必須在.proto 文件中指定 service:

然後在服務中定義 rpc 方法,指定請求的和響應類型,gRPC 允許定義4種類型的 service 方法。

服務.proto文件如下所示:

B. golang內存擴容

一般來說當內存空間span不足時,需要進行擴容。而在擴容前需要將當前沒有剩餘空間的內存塊相關狀態解除,以便後續的垃圾回收期能夠進行掃描和回收,接著在從中間部件(central)提取新的內存塊放回數組中。

需要注意由於中間部件有scan和noscan兩種類型,則申請的內存空間最終獲取的可能是其兩倍,並由heap堆進行統一管理。中間部件central是通過兩個鏈表來管理其分配的所有內存塊:
1、empty代表「無法使用」狀態,沒有剩餘的空間或被移交給緩存的內存塊
2、noempty代表剩餘的空間,並這些內存塊能夠提供服務

由於golang垃圾回收器使用的累增計數器(heap.sweepgen)來表達代齡的:

從上面內容可以看到每次進行清理操作時 該計數器 +2
再來看下mcentral的構成

當通過mcentral進行空間span獲取時,第一步需要到noempty列表檢查剩餘空間的內存塊,這裡面有一點需要說明主要是垃圾回收器的掃描過程和清理過程是同時進行的,那麼為了獲取更多的可用空間,則會在將分配的內存塊移交給cache部件前,先完成清理的操作。第二步當noempty沒有返回時,則需要檢查下empty列表(由於empty里的內存塊有可能已被標記為垃圾,這樣可以直接清理,對應的空間則可直接使用了)。第三步若是noempty和empty都沒有申請到,這時需要堆進行申請內存的

通過上面的源碼也可以看到中間部件central自身擴容操作與大對象內存分配差不多類似。

在golang中將長度小於16bytes的對象稱為微小對象(tiny),最常見的就是小字元串,一般會將這些微小對象組合起來,並用單塊內存存儲,這樣能夠有效的減少內存浪費。
當微小對象需要分配空間span,首先緩存部件會按指定的規格(tiny size class)取出一塊內存,若容量不足,則重新提取一塊;前面也提到會將微小對象進行組合,而這些組合的微小對象是不能包含指針的,因為垃圾回收的原因,一般都是當前存儲單元里所有的微小對象都不可達時,才會將該塊內存進行回收。
而當從緩沖部件cache中獲取空間span時, 是通過偏移位置(tinyoffset)先來判斷剩餘空間是否滿足需求。若是可以的話則以此計算並返回內存地址;若是空間不足,則提取新的內存塊,直接返回起始地址便可; 最後在對比新舊兩塊內存,空間大的那塊則會被保留。

C. (十一)golang 內存分析

編寫過C語言程序的肯定知道通過malloc()方法動態申請內存,其中內存分配器使用的是glibc提供的ptmalloc2。 除了glibc,業界比較出名的內存分配器有Google的tcmalloc和Facebook的jemalloc。二者在避免內存碎片和性能上均比glic有比較大的優勢,在多線程環境中效果更明顯。
Golang中也實現了內存分配器,原理與tcmalloc類似,簡單的說就是維護一塊大的全局內存,每個線程(Golang中為P)維護一塊小的私有內存,私有內存不足再從全局申請。另外,內存分配與GC(垃圾回收)關系密切,所以了解GC前有必要了解內存分配的原理。

為了方便自主管理內存,做法便是先向系統申請一塊內存,然後將內存切割成小塊,通過一定的內存分配演算法管理內存。 以64位系統為例,Golang程序啟動時會向系統申請的內存如下圖所示:

預申請的內存劃分為spans、bitmap、arena三部分。其中arena即為所謂的堆區,應用中需要的內存從這里分配。其中spans和bitmap是為了管理arena區而存在的。
arena的大小為512G,為了方便管理把arena區域劃分成一個個的page,每個page為8KB,一共有512GB/8KB個頁;
spans區域存放span的指針,每個指針對應一個page,所以span區域的大小為(512GB/8KB)乘以指針大小8byte = 512M
bitmap區域大小也是通過arena計算出來,不過主要用於GC。

span是用於管理arena頁的關鍵數據結構,每個span中包含1個或多個連續頁,為了滿足小對象分配,span中的一頁會劃分更小的粒度,而對於大對象比如超過頁大小,則通過多頁實現。

根據對象大小,劃分了一系列class,每個class都代表一個固定大小的對象,以及每個span的大小。如下表所示:

上表中每列含義如下:
class: class ID,每個span結構中都有一個class ID, 表示該span可處理的對象類型
bytes/obj:該class代表對象的位元組數
bytes/span:每個span佔用堆的位元組數,也即頁數乘以頁大小
objects: 每個span可分配的對象個數,也即(bytes/spans)/(bytes/obj)waste
bytes: 每個span產生的內存碎片,也即(bytes/spans)%(bytes/obj)上表可見最大的對象是32K大小,超過32K大小的由特殊的class表示,該class ID為0,每個class只包含一個對象。

span是內存管理的基本單位,每個span用於管理特定的class對象, 跟據對象大小,span將一個或多個頁拆分成多個塊進行管理。src/runtime/mheap.go:mspan定義了其數據結構:

以class 10為例,span和管理的內存如下圖所示:

spanclass為10,參照class表可得出npages=1,nelems=56,elemsize為144。其中startAddr是在span初始化時就指定了某個頁的地址。allocBits指向一個點陣圖,每位代表一個塊是否被分配,本例中有兩個塊已經被分配,其allocCount也為2。next和prev用於將多個span鏈接起來,這有利於管理多個span,接下來會進行說明。

有了管理內存的基本單位span,還要有個數據結構來管理span,這個數據結構叫mcentral,各線程需要內存時從mcentral管理的span中申請內存,為了避免多線程申請內存時不斷的加鎖,Golang為每個線程分配了span的緩存,這個緩存即是cache。src/runtime/mcache.go:mcache定義了cache的數據結構

alloc為mspan的指針數組,數組大小為class總數的2倍。數組中每個元素代表了一種class類型的span列表,每種class類型都有兩組span列表,第一組列表中所表示的對象中包含了指針,第二組列表中所表示的對象不含有指針,這么做是為了提高GC掃描性能,對於不包含指針的span列表,沒必要去掃描。根據對象是否包含指針,將對象分為noscan和scan兩類,其中noscan代表沒有指針,而scan則代表有指針,需要GC進行掃描。mcache和span的對應關系如下圖所示:

mchache在初始化時是沒有任何span的,在使用過程中會動態的從central中獲取並緩存下來,跟據使用情況,每種class的span個數也不相同。上圖所示,class 0的span數比class1的要多,說明本線程中分配的小對象要多一些。

cache作為線程的私有資源為單個線程服務,而central則是全局資源,為多個線程服務,當某個線程內存不足時會向central申請,當某個線程釋放內存時又會回收進central。src/runtime/mcentral.go:mcentral定義了central數據結構:

lock: 線程間互斥鎖,防止多線程讀寫沖突
spanclass : 每個mcentral管理著一組有相同class的span列表
nonempty: 指還有內存可用的span列表
empty: 指沒有內存可用的span列表
nmalloc: 指累計分配的對象個數線程從central獲取span步驟如下:

將span歸還步驟如下:

從mcentral數據結構可見,每個mcentral對象只管理特定的class規格的span。事實上每種class都會對應一個mcentral,這個mcentral的集合存放於mheap數據結構中。src/runtime/mheap.go:mheap定義了heap的數據結構:

lock: 互斥鎖
spans: 指向spans區域,用於映射span和page的關系
bitmap:bitmap的起始地址
arena_start: arena區域首地址
arena_used: 當前arena已使用區域的最大地址
central: 每種class對應的兩個mcentral
從數據結構可見,mheap管理著全部的內存,事實上Golang就是通過一個mheap類型的全局變數進行內存管理的。mheap內存管理示意圖如下:

系統預分配的內存分為spans、bitmap、arean三個區域,通過mheap管理起來。接下來看內存分配過程。

針對待分配對象的大小不同有不同的分配邏輯:
(0, 16B) 且不包含指針的對象: Tiny分配
(0, 16B) 包含指針的對象:正常分配
[16B, 32KB] : 正常分配
(32KB, -) : 大對象分配其中Tiny分配和大對象分配都屬於內存管理的優化范疇,這里暫時僅關注一般的分配方法。
以申請size為n的內存為例,分配步驟如下:

Golang內存分配是個相當復雜的過程,其中還摻雜了GC的處理,這里僅僅對其關鍵數據結構進行了說明,了解其原理而又不至於深陷實現細節。1、Golang程序啟動時申請一大塊內存並劃分成spans、bitmap、arena區域
2、arena區域按頁劃分成一個個小塊。
3、span管理一個或多個頁。
4、mcentral管理多個span供線程申請使用
5、mcache作為線程私有資源,資源來源於mcentral。

D. golang sync.pool對象復用 並發原理 緩存池

在go http每一次go serve(l)都會構建Request數據結構。在大量數據請求或高並發的場景中,頻繁創建銷毀對象,會導致GC壓力。解決辦法之一就是使用對象復用技術。在http協議層之下,使用對象復用技術創建Request數據結構。在http協議層之上,可以使用對象復用技術創建(w,*r,ctx)數據結構。這樣即可以回快TCP層讀包之後的解析速度,也可也加快請求處理的速度。

先上一個測試:

結論是這樣的:

貌似使用池化,性能弱爆了???這似乎與net/http使用sync.pool池化Request來優化性能的選擇相違背。這同時也說明了一個問題,好的東西,如果濫用反而造成了性能成倍的下降。在看過pool原理之後,結合實例,將給出正確的使用方法,並給出預期的效果。

sync.Pool是一個 協程安全 臨時對象池 。數據結構如下:

local 成員的真實類型是一個 poolLocal 數組,localSize 是數組長度。這涉及到Pool實現,pool為每個P分配了一個對象,P數量設置為runtime.GOMAXPROCS(0)。在並發讀寫時,goroutine綁定的P有對象,先用自己的,沒有去偷其它P的。go語言將數據分散在了各個真正運行的P中,降低了鎖競爭,提高了並發能力。

不要習慣性地誤認為New是一個關鍵字,這里的New是Pool的一個欄位,也是一個閉包名稱。其API:

如果不指定New欄位,對象池為空時會返回nil,而不是一個新構建的對象。Get()到的對象是隨機的。

原生sync.Pool的問題是,Pool中的對象會被GC清理掉,這使得sync.Pool只適合做簡單地對象池,不適合作連接池。

pool創建時不能指定大小,沒有數量限制。pool中對象會被GC清掉,只存在於兩次GC之間。實現是pool的init方法注冊了一個poolCleanup()函數,這個方法在GC之前執行,清空pool中的所有緩存對象。

為使多協程使用同一個POOL。最基本的想法就是每個協程,加鎖去操作共享的POOL,這顯然是低效的。而進一步改進,類似於ConcurrentHashMap(JDK7)的分Segment,提高其並發性可以一定程度性緩解。

注意到pool中的對象是無差異性的,加鎖或者分段加鎖都不是較好的做法。go的做法是為每一個綁定協程的P都分配一個子池。每個子池又分為私有池和共享列表。共享列表是分別存放在各個P之上的共享區域,而不是各個P共享的一塊內存。協程拿自己P里的子池對象不需要加鎖,拿共享列表中的就需要加鎖了。

Get對象過程:

Put過程:

如何解決Get最壞情況遍歷所有P才獲取得對象呢:

方法1止前sync.pool並沒有這樣的設置。方法2由於goroutine被分配到哪個P由調度器調度不可控,無法確保其平衡。

由於不可控的GC導致生命周期過短,且池大小不可控,因而不適合作連接池。僅適用於增加對象重用機率,減少GC負擔。2

執行結果:

單線程情況下,遍歷其它無元素的P,長時間加鎖性能低下。啟用協程改善。

結果:

測試場景在goroutines遠大於GOMAXPROCS情況下,與非池化性能差異巨大。

測試結果

可以看到同樣使用*sync.pool,較大池大小的命中率較高,性能遠高於空池。

結論:pool在一定的使用條件下提高並發性能,條件1是協程數遠大於GOMAXPROCS,條件2是池中對象遠大於GOMAXPROCS。歸結成一個原因就是使對象在各個P中均勻分布。

池pool和緩存cache的區別。池的意思是,池內對象是可以互換的,不關心具體值,甚至不需要區分是新建的還是從池中拿出的。緩存指的是KV映射,緩存里的值互不相同,清除機制更為復雜。緩存清除演算法如LRU、LIRS緩存演算法。

池空間回收的幾種方式。一些是GC前回收,一些是基於時鍾或弱引用回收。最終確定在GC時回收Pool內對象,即不迴避GC。用java的GC解釋弱引用。GC的四種引用:強引用、弱引用、軟引用、虛引用。虛引用即沒有引用,弱引用GC但有空間則保留,軟引用GC即清除。ThreadLocal的值為弱引用的例子。

regexp 包為了保證並發時使用同一個正則,而維護了一組狀態機。

fmt包做字串拼接,從sync.pool拿[]byte對象。避免頻繁構建再GC效率高很多。

E. 嵌入式golang佔用內存高

嵌入式golang佔用內存高可能問題在於緩存。
清空日誌後比較驚喜地發現,內存瞬間暴降至20M。
嵌入式系統由硬體和軟體組成.是能夠獨立進行運作的器件。其軟體內容只包括軟體運行環境及其操作系統。硬體內容包括信號處理器、存儲器、通信模塊等在內的多方面的內容。相比於一般的計算機處理系統而言,嵌入式系統存在較大的差異性,它不能實現大容量的存儲功能,因為沒有與之相匹配的大容量介質,大部分採用的存儲介質有E-PROM、EEPROM等,軟體部分以API編程介面作為開發平台的核心。嵌入式系統最核心的層次是中央處理單元部分,它包含運算器和控制器模塊,在cpu的基礎上進一步配上存儲器模塊、電源模塊、復位模塊等就構成了通常所說的最小系統。由於技術的進步,集成電路生產商通常會把許多外設做進同一個集成電路中,這樣在使用上更加方便,這樣一個晶元通常稱之為微控制器。在微控制器的基礎上進一步擴展電源感測與檢測、執行器模塊以及配套軟體並構成一個具有特定功能的完整單元,就稱之為一個嵌入式系統或嵌入式應用。

F. golang適合做web開發嗎

適合。框架足夠成熟了 A Survey of 5 Go Web Frameworks
小型項目你甚至不用框架,用net/http http - The Go Programming Language
常用庫也成熟了 Top - Go Search
golang的web後端即使不concurrent也比php,ruby,python快很多很多
golang里用concurrent真的非常方便,非常非常快,超大web項目golang scale成本低
如果你想,golang的部署可以比php更方便,使用go get和http.ServeAndListen()可以不用nginx和apache
對於文件改動重新編譯其實並不是大問題,看pilu/fresh · GitHub,其實你自己寫shell腳本(也可以直接用go寫,因為它本身就是系統語言)監控文件系統改動然後自動重新build,即使是C/C++的項目這也不是大問題,人們不用C/C++寫web是因為它們不是寫web app的最佳選擇
golang寫的代碼編譯通過後,要比scripting language魯棒,因為go compiler強制一些最佳實踐

G. Golang 語言深入理解:channel

本文是對 Gopher 2017 中一個非常好的 Talk�: [Understanding Channel](GopherCon 2017: Kavya Joshi - Understanding Channels) 的學習筆記,希望能夠通過對 channel 的關鍵特性的理解,進一步掌握其用法細節以及 Golang 語言設計哲學的管窺蠡測。

channel 是可以讓一個 goroutine 發送特定值到另一個 gouroutine 的通信機制。

原生的 channel 是沒有緩存的(unbuffered channel),可以用於 goroutine 之間實現同步。

關閉後不能再寫入,可以讀取直到 channel 中再沒有數據,並返回元素類型的零值。

gopl/ch3/netcat3

首先從 channel 是怎麼被創建的開始:

在 heap 上分配一個 hchan 類型的對象,並將其初始化,然後返回一個指向這個 hchan 對象的指針。

理解了 channel 的數據結構實現,現在轉到 channel 的兩個最基本方法: sends 和 receivces ,看一下以上的特性是如何體現在 sends 和 receives 中的:

假設發送方先啟動,執行 ch <- task0 :

如此為 channel 帶來了 goroutine-safe 的特性。

在這樣的模型里, sender goroutine -> channel -> receiver goroutine 之間, hchan 是唯一的共享內存,而這個唯一的共享內存又通過 mutex 來確保 goroutine-safe ,所有在隊列中的內容都只是副本。
這便是著名的 golang 並發原則的體現:

發送方 goroutine 會阻塞,暫停,並在收到 receive 後才恢復。

goroutine 是一種 用戶態線程 , 由 Go runtime 創建並管理,而不是操作系統,比起操作系統線程來說,goroutine更加輕量。
Go runtime scheler 負責將 goroutine 調度到操作系統線程上。

runtime scheler 怎麼將 goroutine 調度到操作系統線程上?

當阻塞發生時,一次 goroutine 上下文切換的全過程:

然而,被阻塞的 goroutine 怎麼恢復過來?

阻塞發生時,調用 runtime sheler 執行 gopark 之前,G1 會創建一個 sudog ,並將它存放在 hchan 的 sendq 中。 sudog 中便記錄了即將被阻塞的 goroutine G1 ,以及它要發送的數據元素 task4 等等。
接收方 將通過這個 sudog 來恢復 G1

接收方 G2 接收數據, 並發出一個 receivce ,將 G1 置為 runnable :

同樣的, 接收方 G2 會被阻塞,G2 會創建 sudoq ,存放在 recvq ,基本過程和發送方阻塞一樣。
不同的是,發送方 G1如何恢復接收方 G2,這是一個非常神奇的實現。

理論上可以將 task 入隊,然後恢復 G2, 但恢復 G2後,G2會做什麼呢?
G2會將隊列中的 task 復制出來,放到自己的 memory 中,基於這個思路,G1在這個時候,直接將 task 寫到 G2的 stack memory 中!

這是違反常規的操作,理論上 goroutine 之間的 stack 是相互獨立的,只有在運行時可以執行這樣的操作。
這么做純粹是出於性能優化的考慮,原來的步驟是:

優化後,相當於減少了 G2 獲取鎖並且執行 mem 的性能消耗。

channel 設計背後的思想可以理解為 simplicity 和 performance 之間權衡抉擇,具體如下:

queue with a lock prefered to lock-free implementation:

比起完全 lock-free 的實現,使用鎖的隊列實現更簡單,容易實現