小劇場一下
當自己寫好的萬人響應(?)的服務上線啦,每秒都破千人在request,
這時候發現乾,程式有罷格,會寫入錯誤資料,為了不被DBA用手刀打死,~
趕快更版上新程式
這時侯會有一個問題
還在執行中的request怎麼辦?
A:管它去死
B:管它去死
C:通通去死~~YA
以上的情境如果發生在單純request在讀取資料來看,其實無傷大雅,
因為不會影響到資料的正確性,頂多是無回應或是http 500噴掉,
就是這個but!
如果今天微服務是某個大系統中的中間點,就會影響資料的正確性,
因為微服務是無法進行db的transaction,所以沒有辦法進行rollback讓資料恢復到一致性。
graceful shutdown就是先停止收request,然後把已經收下來的request全部執行完,再把服務關掉,
頂多是前一個服務會噴錯,而不是一堆不一致的資料,微服務的架構下,修資料是一件很可怕的事情。
目前實作Graceful Shutdown以http服務為主,Golang在1.8版時提供了http的Shutdown,不需要使用額外的套件或是自行建立worker的機制。
以下範例會使用到channel阻塞的特性,http服務用Goroutine來啟動,主線程使用channel來監聽系統終止的訊號
package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/DoGetByQueryString", func(c *gin.Context) {
p1 := c.DefaultQuery("param1", "Default")
p2 := c.Query("param2")
c.JSON(http.StatusOK, gin.H{"param1": p1, "param2": p2})
})
//原本是用router.Run(),要使用net/http套件的shutdown的話,需要使用原生的ListenAndServe
srv := &http.Server{
Addr: ":8787",
Handler: router,
}
//新增一個channel,type是os.Signal
ch := make(chan os.Signal, 1)
//call goroutine啟動http server
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Println("SERVER GG惹:", err)
}
}()
//Notify:將系統訊號轉發至channel
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
//阻塞channel
<-ch
fmt.Println("Graceful Shutdown start")
}
如果用ctrl+c去強制關閉服務會發生什麼事呢?
Graceful Shutdown start
以上的code可以確認服務被終止時會做一些特定的邏輯,而不是收到終止訊息時就直接關掉服務
func main() {
ctx := context.Background()
router := gin.Default()
router.GET("/DoGetByQueryString", func(c *gin.Context) {
time.Sleep(20 * time.Second)
p1 := c.DefaultQuery("param1", "Default")
p2 := c.Query("param2")
c.JSON(http.StatusOK, gin.H{"param1": p1, "param2": p2})
})
//原本是用router.Run(),要使用net/http套件的shutdown的話,需要使用原生的ListenAndServe
srv := &http.Server{
Addr: ":8787",
Handler: router,
}
//新增一個channel,type是os.Signal
ch := make(chan os.Signal, 1)
//call goroutine啟動http server
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Println("SERVER GG惹:", err)
}
}()
//Notify:將系統訊號轉發至channel
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
//阻塞channel
<-ch
//收到關機訊號時做底下的流程
fmt.Println("Graceful Shutdown start - 1")
//透過context.WithTimeout產生一個新的子context,它的特性是有生命週期,這邊是設定10秒
//只要超過10秒就會自動發出Done()的訊息
c, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
fmt.Println("Graceful Shutdown start - 2")
//使用net/http的shutdown進行關閉http server,參數是上面產生的子context,會有生命週期10秒,
//所以10秒內要把request全都消化掉,如果超時一樣會強制關閉,所以如果http server要處理的是
//需要花n秒才能處理的request就要把timeout時間拉長一點
if err := srv.Shutdown(c); err != nil {
log.Println("srv.Shutdown:", err)
}
//使用select去阻塞主線程,當子context發出Done()的訊號才繼續向下走
select {
case <-c.Done():
fmt.Println("Graceful Shutdown start - 3")
close(ch)
}
fmt.Println("Graceful Shutdown end ")
}
做法類似http server的版本,一樣是監聽關機訊號,然後把grpc server執行GracefulStop()就好了,
如果有興趣可以看這篇文章 grpc.GracefulStop()
使用DAY20範例做魔改
func main() {
ch := make(chan os.Signal, 1)
go httpServer()
apiListener, err := net.Listen("tcp", ":8787")
if err != nil {
panic(err)
}
grpc := grpc.NewServer(
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
grpc_recovery.UnaryServerInterceptor(),
)),
)
gw.RegisterSaySomethingServiceServer(grpc, newSaySomethingServiceServer())
reflection.Register(grpc)
if err = grpc.Serve(apiListener); err != nil {
panic(err)
}
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
<-ch
c, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
//加入這段,只是因為使用GRPC GATEWAY,會多了一段http server的部份就是了,基本上GRPC SERVER關
//掉也無法打過來就是了
grpc.GracefulStop()
select {
case <-c.Done():
close(ch)
}
fmt.Println("Graceful Shutdown end ")
}