第七節: 服務發現和負載均衡java
原文地址git
轉載請註明原文及翻譯地址github
這篇文章將關注兩個微服務架構的重要部分:服務發現和負載均衡.和他們是如何幫助咱們2017年常常要求的橫向擴展容量的spring
負載均衡和出名.服務發現須要一些解釋,從一個問題開始:
"服務A如何請求服務B,若是不知道怎麼找到B"
換句話說,若是你有10個服務B在隨機的集羣節點上運行,有人要記錄這些實例,因此當A須要和B聯繫時,至少一個IP地址或者主機名能夠用(用戶負載均衡),或者說,服務A必須能從第三方獲得服務B的邏輯名字(服務器負載均衡).在微服務架構下,這兩種方法都須要服務發現這一功能.簡單來講,服務發現就是一個各類服務的註冊器
若是這聽起來像dns,確實是.不一樣是,這個服務發現用在你集羣的內部,幫助服務找到彼此.然而,dns一般更靜態,是幫助外部來請求你的服務.同時,dns服務器和dns協議不適合控制微服務多變的環境,容器和節點常常增長和減小.
大部分爲服務框架提供一個或多個選擇給服務發現.默認下,spring cloud/netflix OSS用netflix eureka(同時支持consul, etcd, zooKeeper),每一個服務會在eureka實例中註冊,以後發送heartbeats來讓eureka知道他們還在工做.另外一個有名的是consul,他提供不少功能還包括集成的DNS.其餘有名的選擇使用鍵值對存儲註冊服務,例如etcd.
這裏,咱們主要看一下Swarm中的機制.同時,咱們看一下用unit test(gock)模擬http請求,由於咱們要作服務到服務的溝通.docker
爲服務實現中,咱們把負載均衡分爲兩種:json
當咱們用docker swarm的服務,服務器端真正的服務(producer service)註冊是徹底透明給開發者的.也就是說,咱們的服務不知道他們在服務器端負載均衡下運行,docker swarm完成整個註冊/heartbeat/解除註冊.segmentfault
假設你想建立一個定製的監控應用,須要請求全部部署的服務的/health路徑,你的監控應用怎樣知道這些IP和端口.你須要獲得服務請求的細節.對於swarm保存這些信息,你怎樣獲得他們.對於客戶端的方法,例如eureka,你能夠直接用api,然而,對於依賴於部署的服務發現,這不容易,我能夠說有一個方法來作,同時有好多方法針對於不一樣的情形.設計模式
我推薦用docker遠程api,用docker api在你的服務中來向swarm manager請求其餘服務的信息.畢竟,若是你用你的容器部署的內置服務發現機制,這也是你應該請求的地方.若是有問題,別人也能寫一個適配器給你的部署.然而,用部署api也有限制:你牢牢以來容器的api,你也要肯定你的應用能夠和docker manager交流.api
git checkout P7
咱們看一下可否啓動多個accountservice實例實現擴展同時看咱們swarm自動作到負載均衡請求.
爲了知道哪一個實例回覆咱們的請求,咱們加入一個新的Account結構,咱們能夠輸出ip地址.打開account.go緩存
type Account struct { Id string `json:"id"` Name string `json:"name"` //new ServedBy string `json:"servedBy" }
打開handlers.go,加入GetIp()函數,讓他輸出ServedBy的值:
func GetAccount(w http.ResponseWriter, r *http.Request) { // Read the 'accountId' path parameter from the mux map var accountId = mux.Vars(r)["accountId"] // Read the account struct BoltDB account, err := DBClient.QueryAccount(accountId) account.ServedBy = getIP() // NEW, add this line ... } // ADD THIS FUNC func getIP() string { addrs, err := net.InterfaceAddrs() if err != nil { return "error" } for _, address := range addrs { // check the address type and if it is not a loopback the display it if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { if ipnet.IP.To4() != nil { return ipnet.IP.String() } } } panic("Unable to determine local IP address (non loopback). Exiting.") }
getIp()函數應該用一些utils包,由於這些能夠重複用,當咱們須要判斷一個運行服務的non-loopback ip地址.
從新編譯和部署咱們的服務
> ./copyall.sh
等到結束,輸入
> docker service ls ID NAME REPLICAS IMAGE yim6dgzaimpg accountservice 1/1 someprefix/accountservice
用curl
> curl $ManagerIP:6767/accounts/10000 {"id":"10000","name":"Person_0","servedBy":"10.255.0.5"}
如今咱們看到回覆中有容器的ip地址,然咱們擴展這些服務
> Docker service scale accountservice=3 accountservice scaled to 3
等一會運行
> docker service ls ID NAME REPLICAS IMAGE yim6dgzaimpg accountservice 3/3 someprefix/accountservice
如今有三個實例,咱們curl幾回,看一看獲得的ip地址
curl $ManagerIP:6767/accounts/10000 {"id":"10000","name":"Person_0","servedBy":"10.0.0.22"} curl $ManagerIP:6767/accounts/10000 {"id":"10000","name":"Person_0","servedBy":"10.255.0.5"} curl $ManagerIP:6767/accounts/10000 {"id":"10000","name":"Person_0","servedBy":"10.0.0.18"} curl $ManagerIP:6767/accounts/10000 {"id":"10000","name":"Person_0","servedBy":"10.0.0.22"}
咱們看到四次請求用round-robin的方法分給每個實例.這種swarm提供的服務很好,由於它很方便,咱們也不須要像客戶端發現服務那樣從一堆ip地址中選擇一個.並且,swarm不會把請求發送給那些擁有healthcheck方法,卻沒有報告他們健康的節點.當你擴容和縮減很頻繁時,同時你的服務很複雜,須要比accountservice啓動多不少的時間的時候,這將會很重要.
看一看擴容後的延遲和cpu/內存使用吧.會不會增長?
> docker service scale accountservice=4
gatling測試(1k req/s)
CONTAINER CPU % MEM USAGE / LIMIT accountservice.3.y8j1imkor57nficq6a2xf5gkc 12.69% 9.336 MiB / 1.955 GiB accountservice.2.3p8adb2i87918ax3age8ah1qp 11.18% 9.414 MiB / 1.955 GiB accountservice.4.gzglenb06bmb0wew9hdme4z7t 13.32% 9.488 MiB / 1.955 GiB accountservice.1.y3yojmtxcvva3wa1q9nrh9asb 11.17% 31.26 MiB / 1.955 GiB
咱們的四個實例平分這些工做,這三個新的實例用低於10mb的內存,在低於250 req/s狀況下.
一個實例的gatling測試
四個實例的gatling測試
區別不大,本該這樣.由於咱們的四個實例也是在同一個虛擬機硬件上運行的.若是咱們給swarm分配一些主機還沒用的資源,咱們會看到延遲降低的.咱們看到一點小小的提高,在95和99平均延遲上.咱們能夠說,swarm負載均衡沒有對性能有負面影響.
記得咱們的基於java的quotes-service麼?讓咱們擴容他而且從accountservice請求他,用服務名quotes-service.目的是看一看咱們只知道名字的時候,服務發現和負載均衡好很差用.
咱們先修改一下account.go
type Account struct { Id string `json:"id"` Name string `json:"name"` ServedBy string `json:"servedBy"` Quote Quote `json:"quote"` // NEW } // NEW struct type Quote struct { Text string `json:"quote"` ServedBy string `json:"ipAddress"` Language string `json:"language"` }
咱們用json標籤來轉換名稱,從quote到text,ipAddress到ServedBy.
更改handler.go.咱們加一個簡單的getQuote函數來請求http://quotes-service:8080/api/quote,返回值用來輸出新的Quote結構.咱們在GetAccount函數中請求他.
首先,咱們處理鏈接,keep-alive將會有負載均衡的問題,除非咱們更改go的http客戶端.在handler.go中,加入:
var client = &http.Client{} func init() { var transport http.RoundTripper = &http.Transport{ DisableKeepAlives: true, } client.Transport = transport }
init方法確保發送的http請求有合適的頭信息,能使swarm的負載均衡正常工做.在GetAccount函數下,加入getQuote函數
func getQuote() (model.Quote, error) { req, _ := http.NewRequest("GET", "http://quotes-service:8080/api/quote?strength=4", nil) resp, err := client.Do(req) if err == nil && resp.StatusCode == 200 { quote := model.Quote{} bytes, _ := ioutil.ReadAll(resp.Body) json.Unmarshal(bytes, "e) return quote, nil } else { return model.Quote{}, fmt.Errorf("Some error") } }
沒什麼特別的,?strength=4是讓quotes-service api用多少cpu.若是請求錯誤,返回一個錯誤.
咱們從GetAccount函數中請求getQuote函數,把Account實例返回的值附給Quote.
// Read the account struct BoltDB account, err := DBClient.QueryAccount(accountId) account.ServedBy = getIP() // NEW call the quotes-service quote, err := getQuote() if err == nil { account.Quote = quote }
若是咱們跑handlers_test.go的unit test,咱們會失敗.GetAccount函數會試着請求一個quote,可是這個URL上沒有quotes的服務.
咱們有兩個辦法來解決這個問題
1) 提取getQuote函數爲一個interface,提供一個真的和一個假的方法.
2) 用http特定的mcking框架處理髮送的請求同時返回一個寫好的答案.內置的httptest包能夠幫咱們開啓一個內置的http服務器用於unit test.可是我喜歡用第三方gock框架.
在handlers_test.go中,在TestGetAccount(t *testing)加入init函數.這會使咱們的http客戶端實例被gock獲取
func inti() { gock.InterceptClient(client) }
gock DSL提供很好地控制給期待的外部http請求和回覆.在下面的例子中,咱們用New(), Get()和MatchParam()來讓gock期待http://quotes-service:8080/api/quote?strength=4 Get 請求,回覆http 200和json字符串.
func TestGetAccount(t *testing.T) { defer gock.Off() gock.New("http://quotes-service:8080"). Get("/api/quote"). MatchParam("strength", "4"). Reply(200). BodyString(`{"quote":"May the source be with you. Always.","ipAddress":"10.0.0.5:8080","language":"en"}`)
defer gock.Off()確保咱們的test會中止http獲取,由於gock.New()會開啓http獲取,這可能會是後來的測試失敗.
然咱們斷言返回的quote
Convey("Then the response should be a 200", func() { So(resp.Code, ShouldEqual, 200) account := model.Account{} json.Unmarshal(resp.Body.Bytes(), &account) So(account.Id, ShouldEqual, "123") So(account.Name, ShouldEqual, "Person_123") // NEW! So(account.Quote.Text, ShouldEqual, "May the source be with you. Always.") })
是指跑一下accountservice下全部的測試
從新部署用./copyall.sh,試着curl
> go test ./... ? github.com/callistaenterprise/goblog/accountservice [no test files] ? github.com/callistaenterprise/goblog/accountservice/dbclient [no test files] ? github.com/callistaenterprise/goblog/accountservice/model [no test files] ok github.com/callistaenterprise/goblog/accountservice/service 0.011s
> curl $ManagerIP:6767/accounts/10000 {"id":"10000","name":"Person_0","servedBy":"10.255.0.8","quote": {"quote":"You, too, Brutus?","ipAddress":"461caa3cef02/10.0.0.5:8080","language":"en"} }
擴容quotes-service
> docker service scale quotes-service=2
對於spring boot的quotes-service來講,須要15-30s,不像go那樣快.咱們curl幾回
{"id":"10000","name":"Person_0","servedBy":"10.255.0.15","quote":{"quote":"To be or not to be","ipAddress":"768e4b0794f6/10.0.0.8:8080","language":"en"}} {"id":"10000","name":"Person_0","servedBy":"10.255.0.16","quote":{"quote":"Bring out the gimp.","ipAddress":"461caa3cef02/10.0.0.5:8080","language":"en"}} {"id":"10000","name":"Person_0","servedBy":"10.0.0.9","quote":{"quote":"You, too, Brutus?","ipAddress":"768e4b0794f6/10.0.0.8:8080","language":"en"}}
咱們看到咱們的servedBy循環用accountservice實例.咱們也看到quote的ip地址有兩個.若是咱們沒有關閉keep-alive,咱們可能只會看到一個quote-service實例
這篇咱們接觸了服務發現和負載均衡和怎樣用服務名稱來請求其餘服務下一篇,咱們會繼續微服務的知識點,中心化配置.