使用 Go 優化我們的介面

haifeiWu發表於2019-12-30

標題起的是有點大,不過還好本片文章主要也是使用 Go 來優化 HTTP 服務的,也算打個擦邊球吧~

背景

特徵資料暴增,導致獲取一個城市下所有的特徵的介面延時高,下面是監控上看到的介面響應耗時,最慢的時候介面響應時間能達到 5s 多。

cost-time

快取優化方案

程式碼優化思路:

1,使用快取。

1.1為什麼使用記憶體,而不是 Redis?

分析業務需求,當前需要儲存起來的資料是ObjectId,ObjectId 是一個長度為14左右的字串,我們假設平均下來ObjectId是長度為16的字串,這樣算下來就是每個 ObjectId 佔用的記憶體大小是2個位元組,當前業務需要儲存的ObjectId大概是30萬條,這樣算下來當前業務需要儲存的 ObjectId 要佔用的記憶體在 0.5M 完全可以在記憶體中進行操作。相比於使用 Redis 來說沒有網路開銷,效率更高。

1.2 快取初始化:當服務啟動時,本地快取初始化為空。

1.3 關於快取版本的概念。

快取版本是離線特徵生產任務更新後將資料版本更新到 DB 中。

下面三種方案都是基於記憶體儲存 ObjectId 資料,在記憶體更新的時候策略有所不同。

方案一

2.1 快取更新

使用主動更新快取的方式,建立定時任務,每間隔1分鐘查一次 DB 的資料版本,若更新則更新快取中的資料。

2.2 缺點

單獨啟動一個快取更新執行緒,程式碼不好維護,也會有定時任務執行緒掛掉的情況,不易發現。還有就是需要提前把相關引數配置到程式碼中或者引入配置中心,維護成本較高。

方案二

3.1 快取更新

採用被動觸發的快取更新策略,由介面呼叫觸發。請求進來後檢測當前快取中的資料的版本與 DB 中的資料版本是否一致,若版本更新,則重新讀取當前請求對應城市的所有資料到快取中,並將更新後的資料返回給呼叫方。

3.2 缺點

由於是被動觸發的是同步更新快取的,容易造成介面呼叫時如果正好遇上版本更新,需要更新資料到記憶體中,會出現偶現的毛刺。

3.3 業務執行時序圖

方案二時序圖

方案三(最終採用的方案)

4.1,快取更新

採用被動更新快取的策略,由介面呼叫方觸發。若當前快取中有資料則直接返回快取中的資料,然後檢測當前快取中的資料的版本與 DB 中的資料版本是否一致,若版本更新,則重新讀取當前請求對應城市的所有feature資料到快取中,反之結束快取更新邏輯。

4.2 業務執行時序圖

方案三時序圖

併發優化方案

使用 Goroutine 來優化我們的序列邏輯

Go語言最大的特色就是從語言層面支援併發(Goroutine),Goroutine是Go中最基本的執行單元。事實上每一個Go程式至少有一個Goroutine:主Goroutine。當程式啟動時,它會自動建立。

為了更好理解Goroutine,現講一下執行緒和協程的概念:

執行緒(Thread):有時被稱為輕量級程式(Lightweight Process,LWP),是程式執行流的最小單元。一個標準的執行緒由執行緒ID,當前指令指標(PC),暫存器集合和堆疊組成。另外,執行緒是程式中的一個實體,是被系統獨立排程和分派的基本單位,執行緒自己不擁有系統資源,只擁有一點兒在執行中必不可少的資源,但它可與同屬一個程式的其它執行緒共享程式所擁有的全部資源。

執行緒擁有自己獨立的棧和共享的堆,共享堆,不共享棧,執行緒的切換一般也由作業系統排程。

協程(coroutine):又稱微執行緒與子例程(或者稱為函式)一樣,協程(coroutine)也是一種程式元件。相對子例程而言,協程更為一般和靈活,但在實踐中使用沒有子例程那樣廣泛。

和執行緒類似,共享堆,不共享棧,協程的切換一般由程式設計師在程式碼中顯式控制。它避免了上下文切換的額外耗費,兼顧了多執行緒的優點,簡化了高併發程式的複雜。

golang 中的 map 是執行緒不安全的

很顯然,我們可以用鎖機制解決 Map 的併發讀寫問題。我們將上面的map結構改成如下:

// M
type M struct {
    Map    map[string]string
    lock sync.RWMutex // 加鎖
}

// Set ...
func (m *M) Set(key, value string) {
    m.lock.Lock()
    defer m.lock.Unlock()
    m.Map[key] = value
}

// Get ...
func (m *M) Get(key string) string {
    return m.Map[key]
}
複製程式碼

在上面的程式碼中,我們引入了鎖機制操作,從而保證了map在多個goroutine中的安全。

使用策略模式優化我們的邏輯

這塊主要是因為程式碼中存在太多的 if/else ,故採用策略模式來優化我們的程式碼結構。這裡先放上一篇網上找到的文章,之後有時間再單獨出一篇相關文章吧。優化後的程式碼相較於之前程式碼量少了 50% ,更加清晰與便於維護。下面是優化的程式碼上線後的效果,請求耗時都在100ms以下:

監控介面耗時

小結

上面整體介紹了下當我們的介面耗時較長的時候的一般處理方案,當然具體問題還得具體分析,所以當出現介面反應慢的情況的時候,我們應該具體分析介面反應慢的具體原因,方可對症下藥!

關注我們

關注我們

相關文章