我們在併發HTTP Server的時候,經常有對接口內容做緩存的需求。例如:某些熱點內容,我們希望在1分鐘內做緩存,避免短時間內不會對相同的內容進行重複性讀取與運算,同時也降低系統的整體負載。
有時我們需要把緩存邏輯放在Server內部,而非網觀測如Nginx等。是因為這樣我們可以根據需求便捷地清除緩存,或者利用Redis等其他儲存空間做為緩存後端。
也因此需要快取的資料有兩種特徵:
那快取的種類也可依不同類型分為三種:
Client Cache指的是伺服器與瀏覽器之間的快取機制,Server Side透過伺服器的快取將不常變動的資訊儲存在瀏覽器快取之中,避免重複下載浪費效能。
Client Cache的設定方式是透過HTTP Request的Header去帶params,當瀏覽器接收到特定params就會採取相對應的快取處理。
如果對Client Cache有興趣的人可以參考下方連結,胡立大寫的很好
https://blog.techbridge.cc/2017/06/17/cache-introduction/
Server Cache主要指的是在Backend 與 Database間資料的Cache。當大量的Query Request進來時,會導致Database的I/O操作過多,因而造成Session堵塞、效能低落等問題,即使進行讀寫分離在不同的叢集當中,也只能解決部份問題,因此我們會將經常被查詢的資料儲存在像是Redis之類的key-value資料庫,並以LRU等Strategy來進行快取資料的變更,以分擔資料庫I/O壓力。而我們這章節主要也是在講Gin在Server Cache的實作!
有興趣者也可以參考Cloudflare的官方資訊,他們解說的挺詳細的
https://www.cloudflare.com/zh-tw/cdn/
最後則是Network Cache,他的理念即是User會從離他最近的Server去取資料,用以節省Response Time,也就是CDN(Content Delivery Network)的概念!
這樣的緩存場景無非是有緩存時從緩存取,無緩存時從下游服務取,並將數據放入緩存中。這其實是非常通用的邏輯,應該可以將其抽象出來。從而緩存邏輯無需侵入進業務邏輯。
這邊我們選用的是yahui大神所重新封裝的gin-cache
package,因其可以依據自身要求定義cache key, 且在性能方面也較官方的gin-contrib/cache
優秀,因此選用它。
go get -u [github.com/chenyahui/gin-cache](http://github.com/chenyahui/gin-cache%E3%80%82)
app/config/router.go
package config
import (
cache "github.com/chenyahui/gin-cache"
"github.com/chenyahui/gin-cache/persist"
"github.com/gin-gonic/gin"
"ironman-2021/app/controller"
"ironman-2021/app/middleware"
_ "ironman-2021/docs"
"time"
)
func RouteUsers(r *gin.Engine, m *persist.RedisStore) {
posts := r.Group("/v1/users")
{
posts.POST("/", controller.NewUsersController().CreateUser)
posts.GET("/:id", middleware.JWTAuthMiddleware(), cache.CacheByRequestURI(m, 2*time.Hour),
controller.QueryUsersController().GetUser)
posts.POST("/login", controller.LoginUserController().AuthHandler)
}
}
GET /v1/users/:id
這個endpoint吃得到快取,然後快取時間設定為2小時main.go
package main
import (
"github.com/chenyahui/gin-cache/persist"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/joho/godotenv"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
"ironman-2021/app/config"
"ironman-2021/app/dao"
"ironman-2021/app/middleware"
"ironman-2021/app/model"
"os"
)
// @title Gin swagger
// @version 1.0
// @description Gin swagger
// @contact.name Flynn Sun
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// schemes http
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
func main() {
envErr := godotenv.Load()
if envErr != nil {
panic(envErr)
}
port := os.Getenv("PORT")
dbConfig := os.Getenv("DB_CONFIG")
db, ormErr := dao.Initialize(dbConfig)
if ormErr != nil {
panic(ormErr)
}
migrateErr := db.AutoMigrate(&model.User{})
if migrateErr != nil {
return
}
server := gin.Default()
server.Use(middleware.CORSMiddleware())
server.GET("/hc", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "health check",
})
})
redisStore := persist.NewRedisStore(redis.NewClient(&redis.Options{
Network: "tcp",
Addr: "redis:6379",
DB: 0,
}))
config.RouteUsers(server, redisStore)
url := ginSwagger.URL("http://localhost:8080/swagger/doc.json")
server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
err := server.Run(":" + port)
if err != nil {
panic(err)
}
}
我們可以看到下圖在第一次GET /v1/users/:id
與之後幾次的Response Time都差距相當的多,因為第二次之後都是從Cache拿取資料
此外我們也可以進去redis的container,並觀察到當第一次GET /v1/users/:id
後,會多一個/v1/users/1
的Key,我們就以Key-Value的資料去讀取快取
docker exec -it redis bash
root@e2c9049d9ac3:/data# redis-cli -n 0
127.0.0.1:6379> KEYS *
(empty array)
127.0.0.1:6379> KEYS *
1) "/v1/users/1"
127.0.0.1:6379>
題外話,能夠如此容易的使用各項服務與監測效能也是我們用docker
與 docker-compose
的好處之一。
cache.CachePage(store, time.Minute, func(c *gin.Context) {
c.String(200, "pong"+fmt.Sprint(time.Now().Unix()))
})
ResponseWriter
的寫入函數。每次在gin中調用寫入函數時,觸發一次性能的獲取和追加操作。比較差的。func CachePageAtomic(store persistence.CacheStore, expire time.Duration, handle gin.HandlerFunc) gin.HandlerFunc {
var m sync.Mutex
p := CachePage(store, expire, handle)
return func(c *gin.Context) {
m.Lock()
defer m.Unlock()
p(c)
}
}
在緩存設計中,會遇到一個常見的問題:緩存擊穿。緩存擊穿指的是:當某個熱點Key在其緩存過期的一瞬間,大量的請求將訪問不到這個Key對應的緩存。這時請求將直接打到下游的儲存或服務當中。一瞬間大量的請求,可能會對下游服務造成極大的壓力。
關於這個問題,golang 官方有一個Single Flight:golang.org/x/sync/singleflight,可以有效的解決緩存擊穿問題。其原理非常簡單,有興趣的可以直接在 Github 搜源碼看就可以了,到此不再展開討論。
使用Linux CPU 8核,16G內存系統配置下,Cyhone大使用wrk對gin-contrib/cache
與gin-cache
做benchmark壓力測試,這邊我則擷取他的實驗結果來解說,有興趣者可以去Reference的連結轉連。
wrk -c 500 -d 1m -t 5 http://127.0.0.1:8080/hello
gin-cache
提升了23%。gin-cache
相比QPS更提升了30倍左右。當然也使用gin-cache
使用的redis客戶端庫的性能更好。gin-cache
的優勢將更加明顯。https://blog.techbridge.cc/2017/06/17/cache-introduction/
https://www.cloudflare.com/zh-tw/cdn/
https://www.cyhone.com/articles/gin-cache/