go-kit微服務:服務熔斷

rayson發表於2019-03-05

在微服務架構中,單體服務被拆分為若干微服務,一個服務通常需要呼叫(網路方式)多個服務才能完成預期功能,服務的穩定性受其他服務整體穩定性的制約。若一個服務出現故障,將會影響服務消費方無法正常工作,並將影響逐步放大,甚至導致整個服務叢集崩潰,也就是服務雪崩效應。

為防止服務雪崩,研發人員採用了流量控制、改進快取、服務自動擴容、服務降級與熔斷等方式。本文將介紹服務熔斷,並使用go-kit+Hystrix實現微服務的熔斷方案。

服務熔斷

服務熔斷是指呼叫方發現服務提供方響應緩慢或者不可用時,呼叫方為了自保直接失敗,不再呼叫目標服務。考慮到服務提供方可能會恢復,在一段時間後會進行嘗試訪問。本質上這是一個“斷路器模式”的應用,Martin Fowler有專門的文章對該模式進行講解。通過下面的斷路器開關狀態圖進行說明:

狀態圖

  • 初始狀態為Closed,若請求一直成功,Closed狀態將保持;若失敗次數未超時設定閾值時,也將保持Closed狀態;若失敗次數達到設定閾值,將切換為Open狀態。
  • Open狀態下,呼叫方不再呼叫目標服務,直接失敗返回。若達到設定的重試時間,狀態切換為Half Open,允許部分請求進行嘗試。若嘗試成功,則切換為Closed狀態,若失敗則切換為Open狀態。

針對服務熔斷,業內使用的最多的當屬Netflix退出的Hystrix,它為Spring Cloud構建的微服務提供了便利。以下引自官方對Hystrix的介紹:

Hystrix is a latency and fault tolerance library designed to isolate points of access to remote systems, services and 3rd party libraries, stop cascading failure and enable resilience in complex distributed systems where failure is inevitable.

Hystrix是一個延遲和容錯庫,旨在隔離對遠端系統、服務和第三方庫的訪問點,停止級聯故障,並在故障不可避免的複雜分散式系統中實現恢復能力。

本示例將使用Hystrix的go語言版本afex/hystrix-go實現服務熔斷治理。

實戰演練

本示例基於arithmetic_trace_demo更改,在gateway中增加服務熔斷治理策略,register中除了臨時增加一些故障模擬程式碼不做其他改動。

Step-0:程式碼準備

複製arithmetic_trace_demo目錄,重新命名為arithmetic_circuitbreaker_demo。

下載該示例所需的go依賴:

go get github.com/afex/hystrix-go
複製程式碼

修改docker/docker-compose.yml,增加hystrix-dashboard例項,內容如下:

version: '2'

  consul:
    image: progrium/consul:latest
    ports:
      - 8400:8400
      - 8500:8500
      - 8600:53/udp
    hostname: consulserver
    command: -server -bootstrap -ui-dir /ui

  zipkin:
    image: openzipkin/zipkin
    ports:
      - 9411:9411

  hystrix:
    image: mlabouardy/hystrix-dashboard:latest
    ports:
      - 8181:9002
複製程式碼

Step-2:新增gateway/router.go

開始之前先簡單說下hystrix-go的命令模式,它提供了Do方法通過非同步模式執行使用者的業務邏輯,在執行成功或發生錯誤返回之前將被阻塞,定義如下:

func Do(name string, run runFunc, fallback fallbackFunc) error 
複製程式碼
  • name:為執行的命令名稱,一般設定為請求的名稱或者服務的名稱。
  • run:業務邏輯方法,把對服務提供方的呼叫邏輯封裝在該方法裡面。
  • fallback:run執行過程中發生錯誤時的回撥方法,一般做錯誤資訊封裝。

為了完成hystrix-go的呼叫,我把原來反向代理的邏輯封裝到Do方法中,並通過HystrixRouter型別實現了ServeHTTP,在其中封裝了鏈路追蹤和服務發現邏輯(這麼做僅僅為了演示)。

HystrixRouter定義和新建方法如下所示,主要對鏈路追蹤、服務發現進行封裝。

// HystrixRouter hystrix路由
type HystrixRouter struct {
	svcMap       *sync.Map      //服務例項,儲存已經通過hystrix監控服務列表
	logger       log.Logger     //日誌工具
	fallbackMsg  string         //回撥訊息
	consulClient *api.Client    //consul客戶端物件
	tracer       *zipkin.Tracer //服務追蹤物件
}

func Routes(client *api.Client, zikkinTracer *zipkin.Tracer, fbMsg string, logger log.Logger) http.Handler {
	return HystrixRouter{
		svcMap:       &sync.Map{},
		logger:       logger,
		fallbackMsg:  fbMsg,
		consulClient: client,
		tracer:       zikkinTracer,
	}
}
複製程式碼

接下來的主要邏輯將在ServeHTTP中實現,主要思路為:

  • 解析請求路徑中的服務名稱,檢查是否已經加入Hystrix監控:若否,則配置資訊,並快取至服務列表,若是則跳過。
  • 在Do方法中封裝服務發現、反向搭理、鏈路追蹤邏輯。服務發現失敗,返回錯誤資訊;反向代理失敗返回錯誤資訊(這裡新增了反向代理錯誤時的回撥方法);

詳細程式碼如下所示,可通過註釋進行理解:

func (router HystrixRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	//查詢原始請求路徑,如:/arithmetic/calculate/10/5
	reqPath := r.URL.Path
	if reqPath == "" {
		return
	}
	//按照分隔符'/'對路徑進行分解,獲取服務名稱serviceName
	pathArray := strings.Split(reqPath, "/")
	serviceName := pathArray[1]

	//檢查是否已經加入監控
	if _, ok := router.svcMap.Load(serviceName); !ok {
		//把serviceName作為命令物件,設定引數
		hystrix.ConfigureCommand(serviceName, hystrix.CommandConfig{Timeout: 1000})
		router.svcMap.Store(serviceName, serviceName)
	}

	//執行命令
	err := hystrix.Do(serviceName, func() (err error) {

		//呼叫consul api查詢serviceNam
		result, _, err := router.consulClient.Catalog().Service(serviceName, "", nil)
		if err != nil {
			router.logger.Log("ReverseProxy failed", "query service instace error", err.Error())
			return
		}

		if len(result) == 0 {
			router.logger.Log("ReverseProxy failed", "no such service instance", serviceName)
			return errors.New("no such service instance")
		}

		director := func(req *http.Request) {
			//重新組織請求路徑,去掉服務名稱部分
			destPath := strings.Join(pathArray[2:], "/")

			//隨機選擇一個服務例項
			tgt := result[rand.Int()%len(result)]
			router.logger.Log("service id", tgt.ServiceID)

			//設定代理服務地址資訊
			req.URL.Scheme = "http"
			req.URL.Host = fmt.Sprintf("%s:%d", tgt.ServiceAddress, tgt.ServicePort)
			req.URL.Path = "/" + destPath
		}

		var proxyError error = nil
		// 為反向代理增加追蹤邏輯,使用如下RoundTrip代替預設Transport
		roundTrip, _ := zipkinhttpsvr.NewTransport(router.tracer, zipkinhttpsvr.TransportTrace(true))

		//反向代理失敗時錯誤處理
		errorHandler := func(ew http.ResponseWriter, er *http.Request, err error) {
			proxyError = err
		}

		proxy := &httputil.ReverseProxy{
			Director:     director,
			Transport:    roundTrip,
			ErrorHandler: errorHandler,
		}
		proxy.ServeHTTP(w, r)

		return proxyError

	}, func(err error) error {
		//run執行失敗,返回fallback資訊
		router.logger.Log("fallback error description", err.Error())

		return errors.New(router.fallbackMsg)
	})

	// Do方法執行失敗,響應錯誤資訊
	if err != nil {
		w.WriteHeader(500)
		w.Write([]byte(err.Error()))
	}
}
複製程式碼

Step-3:修改gateway/main.go

首先建立hystrixRouter物件,按照方法引數列表傳遞引數;然後將返回值使用zipkin服務中介軟體進行包裝。

hystrixRouter := Routes(consulClient, zipkinTracer, "Circuit Breaker:Service unavailable", logger)

handler := zipkinhttpsvr.NewServerMiddleware(
	zipkinTracer,
	zipkinhttpsvr.SpanName("gateway"),
	zipkinhttpsvr.TagResponseSize(true),
	zipkinhttpsvr.ServerTags(tags),
)(hystrixRouter)
複製程式碼

為了通過hystrix-dashboard對服務進行監控,需要啟用hystrix的實時監控服務,程式碼如下:

//啟用hystrix實時監控,監聽埠為9010
hystrixStreamHandler := hystrix.NewStreamHandler()
hystrixStreamHandler.Start()
go func() {
	errc <- http.ListenAndServe(net.JoinHostPort("", "9010"), hystrixStreamHandler)
}()
複製程式碼

好了,gateway的程式碼就修改完成了。

Step-4:執行&測試

執行

依次啟動docker、register、gateway,然後使用postman的Runner工具進行測試。

#啟動consul、zipkin、hystrix-dashboard
sudo docker-compose -f docker/docker-compose.yml up

./register/register -consul.host localhost -consul.port 8500 -service.host 192.168.192.146 -service.port 9000

./gateway/gateway -consul.host localhost -consul.port 8500
複製程式碼

Postman中新增一個collection,命名為circuitbreaker,新建post請求。開啟Runner(左上角),選擇新建的集合,設定時間間隔為100毫秒,迭代次數為1000(暫時不執行)。如下圖:

Postman配置

開啟瀏覽器,輸入http://localhost:8181/hystrix,在輸入框輸入hystrix的監控地址http://192.168.192.146:9010(這裡需要配置你的主機地址),然後啟動監控,如下圖:

Hystrix配置

Hystrix監控

準備工作完成了,下面開始測試。

測試

在Postman的Runner介面點選“Run circuitbreak”按鈕,然後檢視Hysytrix監控皮膚,會看到如下介面,斷路器的狀態為Closed

Hystrix正常

然後,關閉register服務(直接在終端停止,方便下面快速啟動)。會發現斷路器狀態很快變為Open

Hystrix熔斷

再開啟register服務,斷路器狀態恢復為Closed

Hystrix恢復

分析

Hystrix預設設定的引數為:

  • DefaultErrorPercentThreshold = 50:請求失敗比例達到50%時,斷路器切換為Open狀態。
  • DefaultTimeout = 1000:請求超過該時間即視為服務異常。我在程式碼中設定的也是1秒。
  • DefaultSleepWindow = 5000:在Open狀態下,間隔5秒進行重試。

初始時register服務正常,所有請求順利執行,所以為Closed狀態;當關閉register(模擬故障)後,請求失敗次數到達設定閾值時,切換為Open狀態;服務恢復後,待Hystrix到達重試時機,服務恢復。

總結

本文在go-kit中使用hystrix-go為閘道器服務gateway增加了服務熔斷治理方案,通過模擬register服務從“正常-故障-恢復”,在hystrix-dashboard中觀察到斷路器狀態的變化。

在實際開發過程中,由於服務之間的依賴關係複雜,非常有必要為我們的服務增加服務熔斷治理措施,確保及時止損,防止因單個依賴服務的故障影響所有業務線。本文僅作為服務熔斷的入門,下來還需要深入研究Hystrix的熔斷與降級機制。

本文參考

本文首發於本人微信公眾號【兮一昂吧】,歡迎掃碼關注!

go-kit微服務:服務熔斷

相關文章