5分鍾學習基於Go,go-microservice-template,Minke的微服務
本文講的是5分鍾學習基於Go,go-microservice-template,Minke的微服務,【編者的話】本篇文章介紹了Go語言下構建微服務的例子,作者利用一個helloword講解了如何使用他的微服務框架,該框架不僅包含了構建服務,還包括路由、請求驗證、日誌記錄、測試、動態配置變更,最後將提供了將服務整合到Docker容器並持續集成。本文幹貨滿滿,雖然需要一些對Go語言的基礎,但是這構建微服務的思路是通用的。
介紹
幾周前我去參加一個零售環境下的技術會議,直到午飯時間都沒人提及'Docker'或者'微服務',我就找了個借口離開了。每個人都在討論微服務,甚至是某天公交司機都會抱怨公交遲到的原因是他們的調度微服務出了問題。從他的描述中得知,錯誤的配置被推送到了服務中,卻沒人知道。於是,最後一部分內容我假定你經曆過這種痛苦,如果沒有的話,沒關係,當它發生的時候你就能體會了。除非你的工作是核導彈發射係統,並且你可能會消滅掉半個星球上的生命,那還是不要體會的好。盡管很多人都在討論微服務,但是框架和開發實踐卻還不夠成熟,有很多知名企業,如Netflix何USwitch,它們使用了這個技術很多年了,但是知識和模式依然進展緩慢。羅馬不是一天造成的,這話同樣適用於微服務框架。雖然有很多驚豔的東西出現,關鍵還是要有大規模的應用和貢獻。
這篇文章將展示如何在5分鍾內從零開始了解微服務,包括架設一個新服務、本地測試、編譯和持續集成測試。假定你已經有Docker工具箱、Ruby和Go語言,否則這些東西可能會超過5分鍾,那就與題目不符了。
框架
當我二次開發一個由C#寫的項目時,我開始構建我的框架,我希望它可以足夠小,並且覆蓋90%的用例。與我思路一致的是Google的Go語言,Cucumber和Ruby作為單元測試,Docker作為平台。當我開始用Go構建微服務時,我發現我需要一遍遍的重複同樣的構建步驟。我需要為功能測試複製部分末班代碼,同樣還有構建腳本。這變的不可管理直到我玩成了第二個服務,我學會了很多東西。當我第一次構建
go-microservice-template
,盡管一段時間內的確很有效,但是它隻方便與添加新的知識模板,但不便於修改現有服務。go-microservice-template
依然用於構建基礎代碼,但是構建和測試工作由單獨的Rubygem完成,Minke。目前它們運行良好,當我從Jenkis服務器切換到CircleCI時,將構建和測試代碼本地化到一個gem中被證實很有用,同時發現並不能使用Docker的exec命令。能夠更新一個中央Gem可以節省很多複製和粘貼的時間。Go 微服務框架
安裝和構建一個微服務
假定你的GOPATH設置正確,它將用於下載一份模板到你的本地機器,你可以簡單的執行下麵語句:$ go get -u -v github.com/nicholasjackson/go-microservice-template
這會從github下載源碼
$ $GOPATH/src/github.com/nicholasjackson/go-microservice-template
構建一個微服務,你隻需要執行下麵的命令
$ go run generate.go
然後,你的屏幕上將出現下圖並有一堆問題:
- 這個微服務的命名空間是什麼?這裏是你的github或者bitbucket路徑,我的是github.com/nicholasjackson.
- 微服務的名字是什麼?這裏填你的服務名。
- 包括StatusD?如果你希望引入StatusD,我會後續來介紹它。要是日誌被詳細的記錄了,公車司機就不會被遲到了。
- 正確麼?
y
用來確認上述配置。
這個應用將會構建微服務,並放置內容在:
$GOPATH/src/[your_namespace]/[service_name]
這個服務已經為編譯和部署準備好了,配有兩個端點,單元測試和功能測試。
Sprint 0
構建這個服務並部署到生產環境。如果我不是每次都因為軟件或人員的服造型導致項目被延遲,我也不會寫這篇博文。我設計這個模板來執行和測試,使得下一次任務可以在持續交付管道中繼續運行。我推薦不僅僅在開發和測試環境中采用構建管道,同樣也要在生產環境中。你可以不需要暴露服務,但是我推薦每次當你提交到主分支時都部署到生產環境。
深入構建服務
對於構建,我之前也提到過我是Bauhaus風格的粉絲,幹淨的線條和極簡主義。然而個別的組件使我不得不在每個服務中都有。- 依賴注入。我當前的是用Facebook的注入包:github.com/facebookgo/inject
- 路由。嚐試並測試過,都用Gorilla。地址:github.com/Gorilla
- 訪問驗證。如我朋友Yan Ettles說的“輸入驗證,輸入驗證,輸入驗證”重要的事情說三遍。安全很重要,這裏采用github.com/asaskevich/govalidator.
- 日誌。度量很重要,個人喜好使用StatusD,同時如果你選擇了這項,那麼Graphite會收集這些度量。如果你想切換到Datadog而不是Graphite,那麼也有個Datadog的客戶端支持StatusD,地址:github.com/alexcesaro/statsd.
依賴注入
Facebook的框架有以下優點:- 共享對象的實例
- 實例命名,當框架不能通過類型來推斷時很有用,如多個類實現了同一個接口。
- 私有對象,對於注入的類是唯一的
這看起來並不像是Go專屬,並且也有一堆疑問關於為什麼要在Go裏用依賴注入,畢竟可以通過共享引用包來解決。對我來說,很大程度上是因為使用共享包時代碼的醜陋和脆弱,同時Facebook的注入包還有一些不錯的特性,例如當創建一個對象時,它會自動遍曆樹結構來創建和注入子依賴。以
handlers/health.go
和handlers/health_test.go
為例。type HealthResponseBuilder struct { statusMessage string } func (b *HealthResponseBuilder) SetStatusMessage(message string) *HealthResponseBuilder { b.statusMessage = message return b } func (b *HealthResponseBuilder) Build() HealthResponse { var hr HealthResponse hr.StatusMessage = b.statusMessage return hr } type HealthDependenciesContainer struct { // if not specified will create singleton SingletonBuilder *HealthResponseBuilder `inject:""` // statsD interface must use a name type as injection cannot infer ducktypes Stats logging.StatsD `inject:"statsd"` // if not specified in the graph will automatically create private instance PrivateBuilder *HealthResponseBuilder `inject:"private"`}
需要警告的是,在health裏還有一些奇怪的代碼。我通常不會這麼寫代碼,但這是一個簡單的方法作為Facebook注入的例子。
路由
談到路由,自從我的第一個go項目就在使用Gorilla,它速度快且滿足我的需要。handler包處理路由,並且用Gorilla創建個新的路由也很簡單。r.Get("/v1/health", HealthHandler) r.Add("POST", "/v1/echo", requestValidationHandler( ECHO_HANDLER+POST, reflect.TypeOf(Echo{}), RouterDependencies.StatsD, http.HandlerFunc(EchoHandler), ))
我們采用兩種方法來注冊,Get請求的處理器(一個路徑和一個函數),而Add則更複雜。這些複雜的選項可以方便在處理器調用前添加中間件,也就是我們增加輸入驗證的地方,這個在下一節會介紹。
請求驗證
go的請求驗證包非常給力,地址: github.com/asaskevich/govalidator。它可以使用結構標注來定義屬性所對應的驗證,然後當發生錯誤時,僅通過調用驗證器上的一個方法就可以返回一組錯誤信息和一個錯誤對象。type Echo struct { Echo string `json:"echo" valid:"stringlength(1|255),required"` } request = Echo{} request.Echo = "Valid String" errors, err = govalidator.ValidateStruct(request)
當把這個代碼放入中間件中,如requestValidationHandler,確認已經讓路由在到達處理器前先經過它,這也就是說我們可以為每個處理器寫一個請求驗證。govalidator對每一種可能的場景都有考慮,包括驗證字符串是否是url或者網絡地址,這些在文檔中都有。
日誌
日誌是必要的,我推薦使用StatusD或者類似提供一堆一堆度量的工具。有很多可選的項目,如Gauges,Counters, Timing Summary Statistics和Sets,不論你選擇選擇哪個,正確的日誌數據不僅可以幫助你調試,而且可以幫助建立預警係統,例如:如果運行失敗,Datadog可以半夜把你叫醒。在模板中的簡單實現采用了Increment(Counter)工具,但是添加其他度量也是很容易的。當你運行這個應用時,Graphite也會運行,可以通過https://192.168.99.100:8080/來訪問,其中192.168.99.100是指你的docker機器ip。我已經在handler包的
const.go
中設定了可選來預定義各種標簽。盡管你不想在你的微服務中隻定義一種通用度量標簽格式,但是可以認為這是一種編碼標準。在所有的微服務中都堅持用同一種格式,它可以讓控製台簡便1000倍,而且你也不需要花一周時間在20個微服務中去重構所有的標簽,因為它們實在太難懂了。package handlers const GET = ".get" const POST = ".post" const CALLED = ".called" const SUCCESS = ".success" const BAD_REQUEST = ".bad_request" const INVALID_REQUEST = ".invalid_request" const VALID_REQUEST = ".valid_request" const HEALTH_HANDLER = "helloworld.health_handler" const ECHO_HANDLER = "helloworld.echo_handler"
測試
用go-microservice-template
測試可以分兩步:- 單元測試(Go)
- 功能測試(Cucumber)
在寫代碼時,我采用了外部的方法學,這是我所偏好的,於是我的框架也是這麼做的。我確實說過這是一個主觀的框架,對吧?
單元測試
對於Go來說,可以通過HTTP服務器直接獨立測試handler,這使得測試handler的邏輯很快速。與依賴注入框架合在一起可以排除依賴或者注入的問題,這樣會有一個高效的測試驅動開發。下麵的代碼是其中一個測試,模擬請求並檢查返回結果,由於這裏並沒有HTTP服務器引入,使得這些測試可以快速的執行。這使得我們可以像其他邏輯測試一樣寫全麵覆蓋handler的測試。唯一不能測試到的就是路由部分和服務啟動,但是這些可以通過功能測試覆蓋。
func TestEchoHandlerCorrectlyEchosResponse(t *testing.T) { echoTestSetup(t) var responseRecorder *httptest.ResponseRecorder var request http.Request responseRecorder = httptest.NewRecorder() echo := Echo{Echo: "Hello World"} context.Set(&request, "request", &echo) EchoHandler(responseRecorder, &request) body := responseRecorder.Body.Bytes() response := Echo{} json.Unmarshal(body, &response) assert.Equal(t, 200, responseRecorder.Code) assert.Equal(t, response.Echo, "Hello World")}
功能測試
所有的功能測試都在_build/features文件夾,並且用Gherkin代碼寫的。Cucumber是一個選擇,Ruby和Rspec正好可以實現這個功能,同時確實隻有少量的Cucumber插件,於是隻能自己實現一些。照著下麵的方法,可以學會創建一些重要的集成測試,同時學會Cucumber中服務的表達方法,它們定義了你和客戶端的交互。Scenario: Echo returns same data as posted Given I send a POST request to "/v1/echo" with the following: | echo | Hello World | Then the response status should be "200" And the JSON response should have "$..echo" with the text "Hello World"
通過使用一個類似
cucumber-rest-api
的gem,上述Cucumber特性將會被執行而不需要額外文件。這並不難掌握,所有反對Cucumber的人都在抱怨它的緩慢和脆弱的測試套件。我嚐試去做一個聲明,你不會喜歡它並且它可能成為評論中的大部分,但是我就是要用它。並不是Cucumber弱
測試你交互的行為,小的整合和確信你的單元測試來覆蓋剩餘部分,往往在功能測試的測試覆蓋率實在是太高了。微服務的另一個好處是你的測試覆蓋率將分布在多個庫中。Minke
Minke對Docker來說,是一個基於像go-microservice-template
一樣的微服務主觀性編譯係統,它封裝了所有的樣板構建腳本並封裝起來,這樣你隻需要像下麵的提取那樣來處理配置文件。對Minke我不會介紹太多,可以去閱讀https://github.com/nicholasjackson/minke,接下來將看一下用於編譯和測試微服務的命令。--- go: namespace: 'github.com/nicholasjackson' application_name: 'helloworld' docker_registry: url: <%= ENV['DOCKER_REGISTRY_URL'] %> user: <%= ENV['DOCKER_REGISTRY_USER'] %> password: <%= ENV['DOCKER_REGISTRY_PASS'] %> email: <%= ENV['DOCKER_REGISTRY_EMAIL'] %> namespace: <%= ENV['DOCKER_NAMESPACE'] %>
在
_build
文件夾中是一個Gemfile,它裏麵有所有的Ruby依賴,如Minke,在編譯服務之前需要安裝這些包。這裏我推薦使用RVM來做Ruby解釋器,並管理不同版本的Ruby和它的依賴。- 確認Docker服務器正在運行,並配置相應的docker環境變量。
- 用
bundle install
安裝相應gems - 用
rake app:build_server
來編譯服務,它會下載go依賴包、執行單元測試、編譯linux下的應用,並打包放入Docker鏡像中 - 用
rake app:cucumber
執行功能測試,並采用Docker Compose來調試環境並執行Cucumber測試。運行正常的話,將在終端下看到下麵的界麵;如果不能正常運行,很有可能是你沒有下載好相關依賴,如Docker是否正確安裝並運行。
Consul
傳遞配置到服務中,我們將采用consul
和consul template
,這個consul template
能夠在_build/dockerfile/[servicename]/config.ctmpl
中找到。Consul模板在容器間作為服務運行,同時與Consul服務器檢測變更。它會根據相應的key來寫入value,同時在檢測到變更時重啟服務器。配置文件存儲在
consul_keys.yaml
文件中,當運行應用或者啟動測試時,Minke會自動導入consul。下麵是一個簡單的Consul模板例子,Consul用Go語言編寫,同樣模板也是Go模板。{ "stats_d_server": "{ {key "app/stats_d_server"} }"}
Docker容器
Docker是我的最愛,事實上我已經不想用任何其他工具來打包和運行應用了。至於其他服務,如Docker Cloud、Amazon Container Service、Google Container Service全都一去不複返了。Docker就是這麼完美,它帶來的痛苦將遠低於不用它帶來的痛苦。Minke編譯你的代碼並打包,可從
_build/dockerfile/[servicename]/
來查看更多內容,但是go-microservice-template
創建的dockerfile是基於Alpine係統,它可以創建更小的鏡像。如果想了解更多內容,可以閱讀另一篇博文,地址是: https://nicholasjackson.github. ... ces/.Docker Compose
當你運行或者測試應用時,Minke采用Docker Compose。如果你查看compose文件,你會看到consul和statsD服務器組件。當我的服務在複雜度上增長的像文件中描述的這樣,我通常用Mimic來控製各種依賴。我試圖將數據庫、隊列等實際依賴降到最低,堅持微服務就應該盡可能小的原則,畢竟如果你需要連接多個數據庫,你的微服務可能有點大了。所有的其他連接都應該被其他服務所管理。helloworld_test: image: helloworld ports: - "8001:8001" environment: - "CONSUL=consul:8500" links: - consul:consul - statsd:statsd consul: image: progrium/consul ports: - "9400:8400" - "9500:8500" - "9600:53/udp" hostname: node1 command: "-server -bootstrap -ui-dir /ui" statsd: image: hopsoft/graphite-statsd ports: - "8080:80" - "2003:2003" - "8125:8125/udp" - "8126:8126"
CI/CD
我先前提到過Sprint 0
使得helloword
在CI下運行並部署到環境中,但是我並沒有強調它可以多大程度上保持CI管道的健康。我最近切換到CircleCI下,因為它們可以像編譯微服務一樣編譯iOS和Android。為了使得基於Go的微服務在CircleCI下運行,首先花了點時間去調研它的自動探測和編譯過程是靠譜的,並不需要用Minke去做。如下麵的配置所示,它在配置方麵足夠簡單了。為了讓服務編譯順利,你所需要做的就是修改依賴部分的路徑,指向你的命名空間和項目名稱。我超愛CircleCI,現在它隻要4分鍾就可以編譯服務並且這些時間是來搭建環境,而且UI也很簡單易用。當我要找到這個配置時,它SSH到編譯宿主的能力也是非常棒的。
machine: pre: - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0 Ruby: version: 2.2.4 services: - docker environment: GOPATH: /home/ubuntu/go dependencies: override: - cd _build && bundle - mkdir -p /home/ubuntu/go/src/github.com/nicholasjackson - cp -R /home/ubuntu/helloworld /home/ubuntu/go/src/github.com/nicholasjackson/ test: override: - cd _build && rake app:build_server - cd _build && rake app:cucumber
總結
本篇文章到此結束,5分鍾從零開始學習微服務主要有以下步驟:- 構建個新服務(go run generate.go)
- 執行編譯腳本(rake app:build_server)
- 執行測試(rake app:cucumber)
- 創建circle.yml配置
- 通知CircleCI監控你的項目
- 推送代碼到庫
- 倒杯水,然後等著編譯變綠
對我來說,隨著我學的更多這永遠是工作進步,我會繼續更新Minke和
go-microservice-template
。如果你有想法,歡迎給我留言、提bug,也歡迎來貢獻代碼。源碼
地址:https://github.com/nicholasjackson/helloworld.依賴
相關項目的地址:- Ruby https://rvm.io/
- Go https://golang.org/
- Docker Toolbox including Docker Compose https://www.docker.com/products/docker-toolbox
- CircleCI https://circleci.com/
原文鏈接:0 to Microservice in 5 minutes with Go, go-microservice-template and Minke(翻譯:陳傑)
===================================================
譯者介紹
陳傑,北京理工大學計算機學院在讀博士,研究方向是自然語言處理在企業網絡信譽評價方麵的應用,平時也樂於去實現一些突發的想法。在疲於配置係統環境時發現了Docker,跟大家一起學習、使用和研究Docker。
原文發布時間為:2016-03-14
本文作者:Sonyfe25cp
本文來自雲棲社區合作夥伴DockerOne,了解相關信息可以關注DockerOne。
原文標題:5分鍾學習基於Go,go-microservice-template,Minke的微服務
最後更新:2017-10-19 11:03:43