從Kratos設計看Go微服務工程實踐

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"導讀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"github.com\/go-kratos\/kratos(以下簡稱Kratos)是一套輕量級 Go 微服務框架,致力於提供完整的微服務研發體驗,整合相關框架及周邊工具後,微服務治理相關部分可對整體業務開發週期無感,從而更加聚焦於業務交付。Kratos在設計之初就考慮到了高可擴展性,組件化,工程化,規範化等。對每位開發者而言,整套 Kratos 框架也是不錯的學習倉庫,可以瞭解和參考微服務的技術積累和經驗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們從 "},{"type":"text","marks":[{"type":"strong"}],"text":"Protobuf"},{"type":"text","text":" 、 "},{"type":"text","marks":[{"type":"strong"}],"text":"開放性"},{"type":"text","text":" 、 "},{"type":"text","marks":[{"type":"strong"}],"text":"規範"},{"type":"text","text":" 、 "},{"type":"text","marks":[{"type":"strong"}],"text":"依賴注入"},{"type":"text","text":" 這4個點了解一下Kratos 在Go微服務工程領域的實踐。"}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"基於Protocol Buffers(Protobuf)的生態"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Kratos中,API定義、gRPC Service、HTTP Service、請求參數校驗、錯誤定義、Swagger API json、應用服務模版等都是基於Protobuf IDL來構建的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/9d\/86\/9d63d394e6d47f8f44322d1af42ce986.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"舉一個簡單的helloworld.proto例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"syntax = \"proto3\";\n\npackage helloworld;\n\nimport \"google\/api\/annotations.proto\";\nimport \"protoc-gen-openapiv2\/options\/annotations.proto\";\nimport \"validate\/validate.proto\";\nimport \"errors\/errors.proto\";\n\noption go_package = \"github.com\/go-kratos\/kratos\/examples\/helloworld\/helloworld\";\n\n\/\/ The greeting service definition.\nservice Greeter {\n\/\/ Sends a greeting\n rpc SayHello (HelloRequest) returns (HelloReply) {\n option (google.api.http) = {\n\/\/ 定義一個HTTP GET 接口,並且把 name 映射到 HelloRequest\nget: \"\/helloworld\/{name}\",\n };\n\/\/ 添加API接口描述(swagger api)\noption (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {\ndescription: \"這是SayHello接口\";\n };\n }\n}\n\n\/\/ The request message containing the user's name.\nmessage HelloRequest {\n\/\/ 增加name字段參數校驗,字符數需在1到16之間\n string name = 1 [(validate.rules).string = {min_len: 1, max_len: 16}];\n}\n\n\/\/ The response message containing the greetings\nmessage HelloReply {\n string message = 1;\n}\n\nenum ErrorReason {\n\/\/ 設置缺省錯誤碼\n option (errors.default_code) = 500;\n\/\/ 爲某個錯誤枚舉單獨設置錯誤碼\n USER_NOT_FOUND = 0 [(errors.code) = 404];\n CONTENT_MISSING = 1 [(errors.code) = 400];;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上是一個簡單的helloworld服務定義的例子,這裏我們定義了一個Service叫Greeter,給Greeter添加了一個SayHello的接口,並根據googleapis規範給這個接口添加了Restful風格的HTTP接口定義,然後還利用openapiv2添加了接口的Swagger API描述,同時還給請求消息結構體HelloRequest中的name字段加上了參數校驗,最後我們在文件的末尾還定義了這個服務可能返回的錯誤碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這時我們在終端中執行:kratos proto client api\/helloworld\/ helloworld.proto 便可以生成以下文件:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/a3\/4d\/a3ayyfd9a54d5386d7691e3b068c3a4d.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上,我們看到Kraots腳手架工具幫我們一鍵生成了上面提到的能力。從這個例子中,我們可以直觀感受到使用使用Protobuf帶來的開發效率的提升,除此之外Kratos還有以下優點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"清晰"},{"type":"text","text":":做到了定義即文檔,定義即代碼"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"收斂,統一"},{"type":"text","text":":將邏輯都收斂統一到一起,通過代碼生成工具來保證HTTP Service、grpc Service等功能具有一致的行爲"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"跨語言"},{"type":"text","text":":衆所周知Protobuf是跨語言的,java、go、python、php、js、c等等主流語言都支持"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"擁抱開源生態"},{"type":"text","text":":比如Kratos複用了google.http.api、protoc-gen-openapiv2、protoc-gen-validate 等等一些犀利的Protobuf周邊生態工具或規範,這比起自己造一個IDL的輪子要容易維護得多,同時老的使用這些輪子的gRPC項目遷移成本也更低"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"開放性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個基礎框架在設計的時候就要考慮未來的可擴展性,那Kratos是怎麼做的呢?"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1. Server Transport"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先看下服務協議層的代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/9e\/87\/9e1db6d72e07a001f4bb5318d13f9887.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面是Kratos RPC服務協議層的接口定義,這裏我們可以看到如果想要給Kratos新增一個新的服務協議,只要實現Start()、Stop()、Endpoint()這幾個方法即可。這樣的設計解耦了應用和服務協議層的實現,使得擴展服務協議更加方便。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/2e\/f4\/2ed441b60c8c33bc325f730986ed8ef4.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上圖中我們可以看到App層無需關心底層服務協議的實現,只是一個容器管理好應用配置、服務生命週期、加載順序即可。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2. Log"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們再看一個Kratos日誌模塊的設計:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/5c\/e9\/5cb6a9a6aecb9f6cfbcb646d00e7f6e9.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏Kratos定義了一個日誌輸出接口Logger,它的設計的非常簡單 - 只用了一個方法、兩個輸入、一個輸出。我們知道一個包暴露的接口越少,越容易維護,同時對使用和實現方的心智負擔更小,擴展日誌實現會變得更容易。但問題來了,這個接口從功能上來講似乎只能輸出日誌level和固定的kv paris,如何能支持更高級的功能?比如輸出 caller stack、實時timestamp、 context traceID ?這裏我們定義了一個回調接口Valuer:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/f4\/7a\/f4f03588fb24c2c7a05c25e5f0fc2b7a.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個Valuer可以被當作key\/value pairs中的value被Append到日誌裏,並被實時調用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們看一下如何給日誌加時間戳的Valuer實現:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/ca\/yy\/ca26644737503a508c30247d7769d9yy.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用時只要在原始的logger上再append一個固定的key和一個動態的valuer即可:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/74\/dd\/74bfe991b4033de582d056d29707fbdd.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的With是一個Helper function,裏面new了一個新的logger(也實現了Logger接口),並將key\\value pairs暫存在新的logger裏,等到Log方法被調用時再通過斷言.(Valuer)的方式獲取值並輸出給底層原始的logger。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以我們可以看到僅僅通過兩個簡單的接口+一個Helper function的組合我們就實現了日誌的大多數功能,這樣大大提高了可擴展性。實際上還有日誌過濾、多日誌源輸出等功能也是通過組合使用這兩接口來實現,這裏待下次分享再展開細講。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3. Tracing"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後我們來看下Kratos的Tracing組件,這裏Kratos採用的是CNCF項目OpenTelemetry。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OpenTelemetry在設計之初就考慮到了組件化和高可擴展性,其實現了OpenTracing和W3C Trace Context的規範,可以無縫對接zipkin、jaeger等主流開源tracing系統,並且可以自定義Propagator 和 TraceProvider。通過otel.SetTracerProvider()我們可以輕易得替換Span的落地協議和格式,從而兼容老系統中的trace採集agent;通過otel.SetTextMapPropagtor()我們可以替換Span在RPC中的Encoding協議,從而可以和老系統中的服務互相調用時也能兼容。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"工程流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們知道在工程實踐的時候,強規範和約束往往比自由和更多的選擇更有優勢,那麼在Go工程規範這塊我這裏主要介紹三塊:"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1. 面向包的設計規範"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Go 是一個面向包名設計的語言,Package 在 Go 程序中主要起到功能隔離的作用,標準庫就是很好的設計範例。Kratos也是可以按包進行組織代碼結構,這裏我們抽取Kratos根目錄下主要幾個Package包來看下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"\/cmd"},{"type":"text","text":":可以通過 go install 一鍵安裝生成工具,使用戶更加方便地使用框架。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"\/api"},{"type":"text","text":":Kratos框架本身的暴露的接口定義"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"\/errors"},{"type":"text","text":":統一的業務錯誤封裝,方便返回錯誤碼和業務原因。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"\/config"},{"type":"text","text":":支持多數據源方式,進行配置合併鋪平,通過 Atomic 方式支熱更配置。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"\/internal"},{"type":"text","text":" :存放對外不可見或者不穩定的接口。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"\/transport"},{"type":"text","text":":服務協議層(HTTP\/gRPC)的抽象封裝,可以方便獲取對應的接口信息。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"\/middleware"},{"type":"text","text":":中間件抽象接口,主要跟transport 和 service 之間的橋樑適配器。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"\/third_party"},{"type":"text","text":":第三方外部的依賴"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到Kratos的包命名清晰簡短,按功能進行劃分,每個包具有唯一的職責。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在設計包時我們還需要考慮到以下幾點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"包的設計必須以使用者爲中心,直觀且易於使用,包的命名必須旨在描述它提供的內容,如果包的名稱不能立即暗示這一點,則它可能包含一組零散的功能。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"包的目的是爲特定問題域而提供的,爲了有目的,包必須提供,而不是包含。包不能成爲不同問題域的聚合地,隨着時間的推移,它將影響項目的簡潔和重構、適應、擴展和分離的能力。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"高便攜性,儘量減少依賴其他代碼庫,一個包與其它包依賴越少,一個包的可重用性就越高。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不能成爲單點依賴,當包被單一的依賴點時,就像一個公共包(common),會給項目帶來很高的耦合性。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2. 配置"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,我們來看下常見的基礎框架是怎麼初始化配置的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/59\/1c\/598da73d16860f4efa621dfb329e291c.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是Go標準庫HTTP Server配置初始化的例子,但是這樣做會有如下幾個問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"&http.Server{}由於是一個取址引用,裏面的參數可能會被外部運行時修改,這種運行時修改帶來的危害是不可把控的。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無法區分nil和0值,當裏面的參數值爲0的時候,不知道是用戶未設置還是就是被設置成了0。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"難以分辨必傳和選傳參數,只能通過文檔說明來隱式約定,沒有強約束力。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼Kraots是怎麼解決這些問題的呢?答案就是Functional Options 。我們看下transport\/http\/client.go的代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/9d\/cf\/9d5c4bed31d89e1e211f9734b74947cf.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Client.go中定義了一個回調函數ClientOption,該函數接受一個定義了一個存放實際配置的未導出結構體clientOptions的指針,然後我們在NewClient的時候,使用可變參數進行傳遞,然後再初始化函數內部通過 for 循環調用修改相關的配置。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這麼做有這麼幾個好處:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於clientOptions結構體是未導出的,那麼就不存在被外部修改的可能。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以區分0值和未設置,首先我們在new clientOptions時會設置默認參數,那麼如果外部沒有傳遞相應的Option就不會修改這個默認參數。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"必選參數顯示定義,可選值則通過Go可變參數進行傳遞,很好的區分必傳和選傳。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3. Error規範"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Kratos爲微服務提供了統一的Error模型:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/5c\/4f\/5cae8a9144188ccb20edf32544256f4f.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Code用作外部展示和初步判斷,服務端無需定義大量全局唯一的XXX_NOT_FOUND,而是使用一個標準Code.NOT_FOUND錯誤代碼並告訴客戶端找不到某個資源。錯誤空間變小降低了文檔的複雜性,在客戶端庫中提供了更好的慣用映射,並降低了客戶端的邏輯複雜性。同時這種標準的大類Code的存在也對外部的觀測系統更友好,比如可以通過分析Nginx Access Log中的HTTP StatusCode來做服務端監控和告警。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Reason是具體的錯誤原因,可以用來更詳細的錯誤判定。每個微服務都會定義自己Reason,那麼要保持全局唯一就需要加上領域前綴,比如User_XXX。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Message錯誤信息可以幫助用戶輕鬆快捷地理解和解決API 錯誤"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Metadata中則可以存放一些標準的錯誤詳情,比如retryInfo、error stack等"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種強制規範,避免了開發人員直接透傳Go的error 從而導致一些敏感信息泄露。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們看下Error結構體還實現了哪些接口:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/36\/ff\/36909c8d300b8c0901fdbdd6ffd8b5ff.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現了GRPCStatus () *status.Status 接口,這樣就實現了從http status code到grpc status code的轉換,這樣Kratos Error可以被gRPC直接轉成google.rpc.Status傳遞出去。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現了標準庫errors包的Is (error) bool接口,這樣使用者可以直接調用errors.Is()來比較兩個erorr中的reason是否相等,避免了使用==來直接判斷error是否相等這種錯誤姿勢。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"依賴注入"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"依賴注入 (Dependency Injection)可以理解爲一種代碼的構造模式,按照這樣的方式來寫,能夠讓你的代碼更加容易維護,一般在Java的項目中見到的比較多。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"依賴注入初看起來比較違反直覺,那麼爲什麼Go也需要依賴注入?假設我們要實現一個用戶訪問計數的功能。我們先看看不使用依賴注入的項目代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"type Service struct {\n redisCli *redis.Client\n}\n\nfunc (s *Service) AddUserCount(ctx context.Context) {\n \/\/do some business logic\n s.redisCli.Incr(ctx, \"user_count\")\n}\n\nfunc NewService(cfg *redis.Options) *Service {\n return &Service{redisCli: redis.NewClient(cfg)}\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種方式比較常見,在項目剛開始或者規模小的時候沒什麼問題,但我們如果考慮下面這些因素:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Redis是基礎組件,往往會在項目的很多地方被依賴,那麼如果哪天我們想整體修改redis sdk的甚至想把redis 整體替換成mysql時,需要在每個被用到的地方都進行修改,耗時耗力還容易出錯。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很難對App這個類寫單元測試,因爲我們需要創建一個真實的redis.Client。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用依賴注入改造後的Service:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"type DataSource interface{\n Incr(context.Context, string)\n}\n\ntype Service struct {\n dataSource DataSource\n}\n\nfunc (s *Service) AddUserCount(ctx context.Context) {\n \/\/do some business logic\n s.dataSource.Incr(ctx, \"user_count\")\n}\n\nfunc NewService(ds DataSource) *Service {\n return &Service{dataSource: ds}\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面代碼中我們把*redis.Client實體替換成了一個DataSource接口,同時不控制dataSource的創建和銷燬,把dataSource生命週期控制權交給了上層來處理,以上操作有三個主要原因:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲Service層已不再關心dataSource的創建和銷燬,這樣當我們需要修改dataSource實現的時候,只要在上層統一修改即可,無需在各個被依賴的地方一一修改。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲依賴的是一個接口,我們寫單元測試的時候只要傳遞一個mock後的Datasource實現即可 。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏dataSource這個基礎組件不再被會到處創建,可以做到複用一個單例節省資源開銷。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Go 的依賴注入框架有兩類,一類是通過反射在運行時進行依賴注入,典型代表是 uber 開源的 dig,另外一類是通過 generate 進行代碼生成,典型代表是 Google 開源的 wire。使用 dig 功能會強大一些,但是缺點就是錯誤只能在運行時才能發現,這樣如果不小心的話可能會導致一些隱藏的 bug 出現。使用 wire 的缺點就是功能限制多一些,但是好處就是編譯的時候就可以發現問題,並且生成的代碼其實和我們自己手寫相關代碼差不太多,更符合直覺,心智負擔更小。所以Kratos更加推薦 wire,Kratos的默認項目模板中 kratos-layout 也正是使用了 google\/wire 進行依賴注入。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們來看下wire使用方式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們首先要定義一個ProviderSet,這個Set會返回構建依賴關係所需的組件Provider。如下所示,Provider往往是一些簡單的工廠函數,這些函數不會太複雜:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"type RedisSource struct {\n redisCli *redis.Client\n}\n\n\/\/ RedisSource實現了Datasource的Incr接口\nfunc (ds *RedisSource) Incr(ctx context.Context, key string) {\n ds.redisCli.Incr(ctx, key)\n}\n\n\/\/ 構建實現了DataSource接口的Provider\nfunc NewRedisSource(db *redis.Client) *RedisSource {\n return &RedisSource{redisCli: db}\n}\n\n\/\/ 構建*redis.Client的Provider\nfunc NewRedis(cfg *redis.Options) *redis.Client {\n return redis.NewClient(cfg)\n}\n\/\/ 這是一個Provider的集合,告訴wire這個包提供了哪些Provider\nvar ProviderSet = wire.NewSet(NewRedis, NewRedisSource)\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接着我們要在應用啓動處新建一個wire.go文件並定義Injector,Injctor會分析依賴關係並將Provider串聯起來構建出最終的Service:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\/\/ +build wireinject\n\nfunc initService(cfg *redis.Options) *service.Service {\n panic(wire.Build(\n redisSource.ProviderSet,\n\/\/使用 wire.Bind 將 Struct 和接口進行綁定了,表示這個結構體實現了這個接口,\nwire.Bind(new(data.DataSource), new(*redisSource.RedisSource)),\n service.NewService),\n )\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後執行wire .後自動生成的代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\/\/go:generate go run github.com\/google\/wire\/cmd\/wire\n\/\/+build !wireinject\n\nfunc initService(cfg *redis.Options) *service.Service {\n client := redis2.NewRedis(cfg)\n redisSource := redis2.NewRedisSource(client)\n serviceService := service.NewService(redisSource)\n return serviceService\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由此我們可以看到只要定義好組件初始化的Provider函數,還有把這些Provider組裝在一起的Injector就可以直接生成初始化鏈路代碼了,上手還是相對簡單的,生成的代碼所見即所得,容易Debug。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"綜上可見,Kratos是一款凝結了開源社區力量以及Go同學們大量微服務工程實踐後誕生的一款微服務框架。現在騰訊雲微服務治理治理平臺(微服務平臺TSF)也已支持Kratos框架,給Kratos賦予了更多企業級服務治理能力、提供多維度服務,如:應用生命週期託管、一鍵上雲、私有化部署、多語言發佈。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者介紹"},{"type":"text","text":":"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"曹國樑"},{"type":"text","text":":6年Go微服務研發經歷,騰訊雲高級研發工程師,Kratos Maintainer,gRPC-go contributor"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"本文轉載自公衆號騰訊雲中間件(ID:gh_6ea1bc2dd5fd)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"原文鏈接"},{"type":"text","text":":"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/ZCoc3qA_r1fKzPOURA2V2g","title":"","type":null},"content":[{"type":"text","text":"從Kratos設計看Go微服務工程實踐"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章