利用nginx fastcgi_cache及golang-lru解決介面瓶頸

為滿足特定的業務需求,我們有些介面數據極大,返回給客戶端的數據gzip後大概是80KB左右, 不做gzip的話大概有600KB的樣子。

實現方案

  1. nginx做接入層,將請求轉發到golang/php實現的介面服務
  2. golang/php接收請求,先查redis集群,如果沒有數據則查mysql,對數據做json序列化再存入redis集群
  3. 如果查到數據則對其做json反序列化,然後做相應業務邏輯生成結果並返回給nginx
  4. nginx對數據做gzip再返回給客戶端

存在問題

  1. redis集群流量較高
  2. golang/php服務的cpu消耗比較明顯,但是內存並沒佔用多少
  3. json反序列化效率極差,尤其在大數據量的場景下

解決方案

由於golang與php的實現是分別由兩個團隊完成的,考慮到實現的可行性,我們先考慮通過nginx的fastcgi_cache來實現靜態介面的緩存。這類介面並不要求實時更新,且大多通過php實現,因此完全可以通過nginx fastcgi_cache實現。而且此方案還有一些額外的好處,譬如避免請求蜂擁至後端,若後端出錯可直接返回老數據等等。

然而,對於動態介面而言,這個方案完全沒有辦法滿足需求。幸而動態介面基本都是基於golang實現的,因此我們只需要引入一種本地內存緩存方案,就可以很好的解決前面提及的三個問題。

經過一周左右時間的調研,我們考察了golang-lru、go-cache、groupcache、freecache及bigcache等各種緩存庫,出於簡單穩定高效的角度,我們最終選用了golang-lru,並在此基礎上實現了expire feature,參考鏈接:GitHub - hnlq715/golang-lru: Golang LRU cache with expire feature.

與此同時,我們也參考了groupcache的特性,實現了類似邏輯,避免同時湧入過多請求到redis:

func (l *lruCache) GetWithLoader(ctx context.Context, key string, load GetterFunc) (interface{}, error) { l.stats.Gets.Add(1) data, ok := l.arc.Get(key) if !ok && load != nil { return l.g.Do(key, func() (interface{}, error) { l.stats.Loads.Add(1) data, err := load() if err != nil { return nil, err } l.Set(ctx, key, data) return data, err }) } l.stats.Hits.Add(1) return data, nil}

通過這個方案,我們實現了以下幾個目標:

  1. 極大降低了redis集群流量
  2. cpu資源消耗降低,內存利用率提升
  3. 避免過多json反序列化

從線上監控情況來看,目前的服務無論從響應時間、緩存命中率還是介面可用性來看,都比之前有了大幅提升。


推薦閱讀:

Nginx 和 Apache 各有什麼優缺點?
Nginx 多進程模型是如何實現高並發的?
Nginx反向代理為什麼可以提高網站性能?
淺談前端線上部署與運維
PHP執行時間長導致PHP-FPM不響應其他請求?

TAG:Nginx | Go语言 | 缓存 |