目錄
一、go-micro框架
二、服務注冊發現(etcd)
三、服務網關
四、鏈路追蹤(jaeger)
五、protobuf協議
六、部署(docker-compose)
七、其他
八、錯誤總結
github完整代碼示例:https://github.com/catwrench/go-micro
內容比較多,多看代碼示例,有空再來拆分成多篇文章細說
參考資料
- go-micro中文文檔
安裝 |《Go Micro 中文文檔 2.x》| Go 技術論壇
- micro組件
- 官方示例
- go-plugins支持示例
- go-micro實踐
go-micro V2 從零開始_hotcoffie的博客-CSDN博客
一、go-micro框架
micro 是一套微服務構建工具庫。對于微服務架構的應用,micro 提供平臺層面、高度彈性的工具組件,讓服務開發者們可以把復雜的分布式系統以簡單的方式構建起來,并且盡可能讓開發者使用最少的時間完成基礎架構的構建。
go-micro 是獨立的 rpc 框架,它是 micro 工具集的核心
micro的服務核心組件:
Client:發送RPC請求與廣播消息
Server:接收RPC請求與消費消息
Broker:異步通信組件
Codec:數據編碼組件
Registry:服務注冊組件
Selector:客戶端均衡器
Transport:同步通信組件
其中,
Broker
和Transport
都是通訊組件,區別就是一個是異步
,另一個是同步
。所以我們在業務中使用Transport
組件會比較頻繁,因為業務需要關心調用結果。
Codec
是數據編碼組件,它可以自動將請求及其參數,轉換為需要的格式。例如,當我們需要調用其他服務時,Transport
默認的是使用grpc2
,grpc2
使用的通訊格式是Protobuf
,所以Codec
會幫我們將數據轉為Protobuf
格式進行發送。
Registry
是服務注冊組件,它既可以幫我們把我們的服務注冊到服務中心,又可以在服務中心中獲取已經注冊的服務列表,供我們進行調用。
Selector
客戶端均衡器配合服務注冊組件。當從服務中心中獲取已經注冊的服務列表時,由于相同的一個服務可能是高可用的架構,所以需要一個均衡調度器,根據不同的均衡權重算法,來幫我們選擇一個合適的節點進行調用。
二、服務注冊發現(etcd)
go-micro框架為服務注冊發現提供了標準的接口Registry
。只要實現這個接口就可以定制自己的服務注冊和發現。不過官方已經為主流注冊中心提供了官方的接口實現
目前最新版的go-micro默認使用mDNS 提供零配置的發現系統,他內置于大多數系統。所以之前我們的程序完全不用做任何配置,也不用搭建任何環境,就具備服務注冊和發現能力。
而在生產環境,官方則推薦使用etcd組成更具彈性的集群方案,在v2版中,官方已經不推薦使用consul。
- 配置默認使用etcd
go run main --registry=etcd
通過設置 GoLand 的 Go Modules 環境變量 MICRO_REGISTRY=etcd
來統一設置,這樣,就不需要在啟動服務時額外傳入--registry=etcd
這個選項了(打開 GoLand 的 Preferences 界面即可完成設置)
動手實踐
- 注冊(示例是etcd的,替換成consul也是一樣的)
func init() {
//注冊地址為 ip:端口
etcdRegister := etcd.NewRegistry(
registry.Addrs("192.168.110.195:2379"),
)
}
- 發現
//從注冊中心獲取服務節點
func getSrvNode(reg registry.Registry, srvName string) (*registry.Node, error) {
//獲取服務列表
services, err := reg.GetService(srvName)
if err != nil {
log.Info("未獲取到服務 " + srvName + ",請確認服務是否存在")
return nil, err
}
//獲取隨機服務,也可以使用 RoundRobin 之類的算法
next := selector.Random(services)
node, err := next()
if err != nil {
log.Info("隨機獲取服務 " + srvName + " 實例失敗")
return nil, err
}
return node, nil
}
三、服務網關
Micro的api就是api網關,API參考了API網關模式為服務提供了一個單一的公共入口。基于服務發現,使得micro api可以提供具備http及動態路由的服務。
micro 自帶的api網關功能比較單一,而且http和rpc請求需要起不同的服務來處理,服務網關通常會整合很多系統相關邏輯,所以是使用的gin
框架自行實現的網關。
動手實踐
- main.go
核心是初始化一個服務注冊到etcd,然后啟動一個web服務對外提供api訪問,同時將gin.router
作為處理器綁定到go-micro
,讓gin框架來接管路由
func main() {
......
//etcd注冊實例
etcdRegister := etcd.NewRegistry(
registry.Addrs(etcdAddr),
)
//----------------注冊網關-----------------------------------
//注冊網關服務
//這個服務實際不會調用.run方法,實際會啟動的是下面的webService
gwtService := micro.NewService(
micro.Name(gatewayName),
micro.Version("latest"),
// 配置etcd為注冊中心,配置etcd路徑,默認端口是2379
micro.Registry(etcdRegister),
)
//---------------注冊web服務-------------------------------
//會議室預訂服務的restful api映射
webService := web.NewService(
web.Name(gatewayWeb),
web.Address(webServiceAddr),
web.Registry(etcdRegister),
)
//注冊路由處理器
webService.Handle("/", router.NewRouter())
//啟動服務
if err := webService.Run(); err != nil {
fmt.Println("webService.Run error:", err)
}
......
}
- route.go
//返回gin router
func NewRouter() *gin.Engine {
route := gin.Default()
//跨域處理
route.Use(middleware.Cors())
//通配路由,以 meeting 為前綴的都轉發到會議室預訂服務去
uriMeeting := serviceclient.MeetingApiNode.Address
route.Any("/api/meeting/*any", ReverseProxy(uriMeeting, ""))
route.Any("/api/meetingApplet/*any", ReverseProxy(uriMeeting, ""))
return route
}
//反向代理
func ReverseProxy(host string, scheme string) gin.HandlerFunc {
return func(context *gin.Context) {
director := func(req *http.Request) {
if scheme == "" {
scheme = "http"
}
req.URL.Scheme = scheme
req.URL.Host = host
req.Host = host //一個ip對應多個域名的情況需要設置這項
}
proxy := &httputil.ReverseProxy{Director: director}
proxy.ServeHTTP(context.Writer, context.Request)
}
}
四、鏈路追蹤(jaeger)
中文文檔 介紹
工作原理:
動手實踐
- 改造一下網關的main.go
......
//----------------注冊網關-----------------------------------
// 配置jaeger連接
jaegerTracer, closer, err := tracer.NewJaegerTracer(gatewayName, jaegerAddr)
if err != nil {
log.Fatal(err)
}
defer closer.Close()
wrapperTrace.SetGlobalTracer(jaegerTracer)
//注冊網關服務
//這個服務實際不會調用.run方法,實際會啟動的是下面的webService
gwtService := micro.NewService(
......
// 配置鏈路追蹤為 jaeger
micro.WrapHandler(opentracing.NewHandlerWrapper(wrapperTrace.GlobalTracer())),
)
- 改造一下路由router.go,加入鏈路追蹤中間件
func NewRouter() *gin.Engine {
......
route.Use(
lib.JaegerMiddleware(), //jaeger中間件
)
......
}
- JaegerMiddleware鏈路追蹤中間件
//jaeger中間件,記錄token和span信息
func JaegerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
sp := opentracing.GlobalTracer().StartSpan(c.Request.URL.Path)
tracer := opentracing.GlobalTracer()
//元數據metadata
md := make(map[string]string)
//獲取請求投的 spanCtx
spanCtx, sErr := opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(c.Request.Header))
if sErr != nil {
sp = opentracing.GlobalTracer().StartSpan(c.Request.URL.Path, opentracing.ChildOf(spanCtx))
tracer = sp.Tracer()
}
//基于提取的 spanCtx 創建新的子span
sp = opentracing.GlobalTracer().StartSpan(c.Request.URL.Path, opentracing.ChildOf(spanCtx))
sp.SetTag("Authorization", c.GetHeader("Authorization"))
defer sp.Finish()
//注入span到tracer.text
if err := tracer.Inject(
sp.Context(),
opentracing.TextMap,
opentracing.TextMapCarrier(md),
); err != nil {
log.Log(err)
}
//注入span到tracer.text
if err := tracer.Inject(
sp.Context(),
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(c.Request.Header),
); err != nil {
log.Log(err)
}
ctx := context.TODO()
ctx = opentracing.ContextWithSpan(ctx, sp)
ctx = metadata.NewContext(ctx, md)
c.Set(contextTracerKey, ctx)
c.Next()
//通過ext可以為追蹤設置額外的一些信息
statusCode := c.Writer.Status()
ext.HTTPStatusCode.Set(sp, uint16(statusCode))
ext.HTTPMethod.Set(sp, c.Request.Method)
ext.HTTPUrl.Set(sp, c.Request.URL.EscapedPath())
if statusCode >= http.StatusInternalServerError {
ext.Error.Set(sp, true)
} else if rand.Intn(100) > sf {
ext.SamplingPriority.Set(sp, 0)
}
}
}
// ContextWithSpan 返回context
func ContextWithSpan(c *gin.Context) (ctx context.Context, ok bool) {
v, exist := c.Get(contextTracerKey)
if exist == false {
ok = false
ctx = context.TODO()
return
}
ctx, ok = v.(context.Context)
return
}
五、protobuf協議
工作流程:
- Proto 語言文件的規范
proto 文件遵循只增不減的原則
proto 文件中的接口遵循只增不減的原則
proto 文件中的 message 字段遵循只增不減的原則
proto 文件中的 message 字段類型和序號不得修改
- 官方文檔
- 服務間調用依賴的proto文件如何進行管理
微服務架構下RPC IDL及代碼如何統一管理?_韓亞軍的博客-CSDN博客
動手實踐
這里需要創建兩個子服務,meeting-api
和meeting-srv
,meeting-srv實現會議室預訂相關的CRUD,meeting-api通過rpc遠程調用meeting-srv完成業務組裝,并提供對外部的api訪問。(需要兩個項目持有同一份proto文件,才能通過protobuf協議完成 rpc調用)
- 定義響應的公共文件response.proto
syntax = "proto3";
package response;
import "google/protobuf/any.proto";
option go_package = ".;proto";
message Response {
int64 Code = 1;
string Message = 2;
google.protobuf.Any Data = 3;
}
- 定義會議室room.proto文件
syntax = "proto3";
package meeting;
import "proto/response/response.proto";//標紅無所謂,一樣能導入
option go_package = ".;proto";
service RoomService{
//查詢會議室列表
rpc GetRooms(ReqGetRooms) returns(response.Response){}
//查詢會議室詳情
rpc GetRoom(ReqGetRoom) returns(response.Response){}
//新增會議室
rpc CreateRoom(ReqCreateRoom) returns(response.Response){}
//編輯會議室
rpc UpdateRoom(ReqUpdateRoom) returns(response.Response){}
//刪除會議室
rpc DeleteRoom(ReqDeleteRoom) returns(response.Response){}
}
message Room{
int64 id = 1;
int64 space_id = 2;//所屬地點ID
string name = 3;//會議室名稱
oneof one_status{
string status = 4;// 啟用狀態:0禁用、1啟用
};
string image_url = 5;//會議室圖片
int64 capacity_min = 6;//建議使用人數(最小)
int64 capacity_max = 7;//建議使用人數(最大)
string created_at = 8;
}
message ReqGetRooms{
int64 page = 1;
int64 pageSize = 2;
string sortBy = 3;
string order = 4;
int64 space_id = 5;
string name = 6;
oneof one_status{
string status = 7;
};
}
message ReqGetRoom{
int64 id = 1;
}
message ReqCreateRoom{
int64 space_id = 2;
string name = 3;
oneof one_status{
string status = 4;
};
string image_url = 5;
int64 capacity_min = 6;
int64 capacity_max = 7;
string DeviceIds = 8;
}
message ReqUpdateRoom{
int64 id = 1;
int64 space_id = 2;
string name = 3;
oneof one_status{
string status = 4;
};
string image_url = 5;
int64 capacity_min = 6;
int64 capacity_max = 7;
string DeviceIds = 8;
}
message ReqDeleteRoom{
int64 id = 1;
}
- 在common根目錄下生成所有proto文件命令:(注意執行路徑和輸入輸出路徑)
for x in **/*.proto; do protoc --go_out=protob --micro_out=protob $x; done
六、部署(docker-compose)
直接參考github代碼吧:https://github.com/catwrench/go-micro/tree/main/deploy
ps:開始前請確認
1、將common服務復制到每個項目的submodules路徑下(使用git submodules引入公共模塊,配置其實可以使用etcd存儲)
2、是否將`submodules/common/config/config.dev.yml`重命名為`config.yml`,并填寫正確參數
3、確認`deploy/.env`是否配置正確
# 先構建golang基礎鏡像,打上標簽
cd golang
docker build -t golang:base .
docker tag golang:base golang-base:1.14.4
# 通過docker-compose 構建微服務鏡像并啟動電容器
# 在deploy根目錄執行
docker-compose up --build -d
-
啟動后的效果
deploy/.env
# docker-compose 環境配置
# go-micro公共配置
WORKSPACE="../../go-micro"
MICRO_REGISTRY="etcd"
MICRO_SERVER_ADDRESS=":2379"
# MYSQL配置
MYSQL_DATABASE=micro_dev
MYSQL_PORT=3306
MYSQL_ROOT_USER=root
MYSQL_ROOT_PASSWORD=root
MYSQL_TEST_USER=test
MYSQL_TEST_PASSWORD=test
MYSQL_DATA_DIR=./db_data
MYSQL_LOG=./log/mysql
# REDIS配置
REDIS_PORT=6379
REDIS_PASSWORD=null
#-------------------------------------
# 注冊中心
ALLOW_NONE_AUTHENTICATION="yes"
ETCD_ADVERTISE_CLIENT_URLS="http://etcd:2379"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://0.0.0.0:2380"
ETCD_INITIAL_CLUSTER="http://0.0.0.0:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_NAME="node1"
# 網關
GATEWAY_PORT=8091
# 會議室預訂服務【api】
MEETING_API_PORT=56201
# 會議室預訂服務【srv】
MEETING_SRV_PORT=56302
# 用戶服務【srv】
USER_SRV_PORT=56301
# 消息通知服務【srv】
NOTICE_SRV_PORT=56303
- deploy/docker-compose.yml
version: '3.1'
services:
#mysql服務
db:
build: ./mysql
command: --default-authentication-plugin=mysql_native_password
volumes:
- ${MYSQL_DATA_DIR}:/var/lib/mysql
- ${MYSQL_LOG}:/var/log/mysql
ports:
- "${MYSQL_PORT}:3306"
environment:
#mysql的root密碼
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
#容器會創建的數據庫
MYSQL_DATABASE: ${MYSQL_DATABASE}
#test用戶
MYSQL_USER: ${MYSQL_TEST_USER}
#test用戶的密碼
MYSQL_PASS: ${MYSQL_TEST_PASSWORD}
networks:
- gomicro
#redis服務
redis:
build: ./redis
ports:
- "${REDIS_PORT}:6379"
#指定創建redis容器后,設置的密碼
#command:
# - "--requirepass Admin@${REDIS_PASSWORD}"
networks:
- gomicro
# ------------------------------
# 注冊中心 etcd
etcd:
image: bitnami/etcd:3
ports:
- 2379:2379
- 2380:2380
environment:
ALLOW_NONE_AUTHENTICATION: ${ALLOW_NONE_AUTHENTICATION}
ETCD_ADVERTISE_CLIENT_URLS: ${ETCD_ADVERTISE_CLIENT_URLS}
ETCD_LISTEN_CLIENT_URLS: ${ETCD_LISTEN_CLIENT_URLS}
ETCD_LISTEN_PEER_URLS: ${ETCD_LISTEN_PEER_URLS}
ETCD_INITIAL_ADVERTISE_PEER_URLS: ${ETCD_INITIAL_ADVERTISE_PEER_URLS}
ETCD_INITIAL_CLUSTER: "${ETCD_NAME}=${ETCD_INITIAL_CLUSTER}"
ETCD_INITIAL_CLUSTER_TOKEN: ${ETCD_INITIAL_CLUSTER_TOKEN}
ETCD_INITIAL_CLUSTER_STATE: ${ETCD_INITIAL_CLUSTER_STATE}
ETCD_NAME: ${ETCD_NAME}
networks:
- gomicro
# 鏈路追蹤 jaeger
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- 16686:16686
networks:
- gomicro
# 網關
gateway:
build:
# 設置context為上級相對路徑,避免dockerfile構建時add命令不能添加上級目錄
context: ../
dockerfile: gateway/deploy/Dockerfile
args:
- EXPOSE_PORT=${GATEWAY_PORT}
ports:
- "${GATEWAY_PORT}:${GATEWAY_PORT}"
environment:
MICRO_REGISTRY: ${MICRO_REGISTRY}
MICRO_SERVER_ADDRESS: ":${GATEWAY_PORT}"
volumes:
- "${WORKSPACE}:/go/src/go-micro"
depends_on:
- etcd
- jaeger
- meeting-api
networks:
- gomicro
# ------服務列表------
# 會議室預訂服務【api】
meeting-api:
build:
context: ../
dockerfile: meeting-api/deploy/Dockerfile
args:
- EXPOSE_PORT=${MEETING_API_PORT}
ports:
- "${MEETING_API_PORT}:${MEETING_API_PORT}"
environment:
MICRO_REGISTRY: ${MICRO_REGISTRY}
MICRO_SERVER_ADDRESS: ":${MEETING_API_PORT}"
volumes:
- "${WORKSPACE}:/go/src/go-micro"
depends_on:
- etcd
- jaeger
- meeting-srv
networks:
- gomicro
# 會議室預訂服務【srv】
meeting-srv:
build:
context: ../
dockerfile: meeting-srv/deploy/Dockerfile
args:
- EXPOSE_PORT=${MEETING_SRV_PORT}
ports:
- "${MEETING_SRV_PORT}:${MEETING_SRV_PORT}"
environment:
MICRO_REGISTRY: ${MICRO_REGISTRY}
MICRO_SERVER_ADDRESS: ":${MEETING_SRV_PORT}"
volumes:
- "${WORKSPACE}:/go/src/go-micro"
depends_on:
- etcd
- db
networks:
- gomicro
# 用戶服務【srv】
user-srv:
build:
context: ../
dockerfile: user-srv/deploy/Dockerfile
args:
- EXPOSE_PORT=${USER_SRV_PORT}
ports:
- "${USER_SRV_PORT}:${USER_SRV_PORT}"
environment:
MICRO_REGISTRY: ${MICRO_REGISTRY}
MICRO_SERVER_ADDRESS: ":${USER_SRV_PORT}"
volumes:
- "${WORKSPACE}:/go/src/go-micro"
depends_on:
- etcd
networks:
- gomicro
# 消息通知服務【srv】
notice-srv:
build:
context: ../
dockerfile: notice-srv/deploy/Dockerfile
args:
- EXPOSE_PORT=${NOTICE_SRV_PORT}
ports:
- "${NOTICE_SRV_PORT}:${NOTICE_SRV_PORT}"
environment:
MICRO_REGISTRY: ${MICRO_REGISTRY}
MICRO_SERVER_ADDRESS: ":${NOTICE_SRV_PORT}"
depends_on:
- etcd
- db
- redis
volumes:
- "${WORKSPACE}:/go/src/go-micro"
networks:
- gomicro
networks:
gomicro:
driver: bridge
七、其他組件
具體使用參考github倉庫代碼,限于篇幅這里就不寫了
- 參數驗證(validator/v10, gin框架默認組件)
golang常用庫:字段參數驗證庫-validator使用 - 九卷 - 博客園
- gorm
- 如何使用gorm編寫良好可復用代碼
利用go+grpc+gorm+proto、通過設計好的數據表快速生成curd增刪改查代碼_陳福華的博客-CSDN博客
- 讀取配置文件(viper)
- 時間格式轉換(golang-module/carbon,不推薦用,因為錯誤全部拋出panic)
八、錯誤總結
- 總結:
web服務(消費者)和api服務(提供者)一定是分開的兩個服務,一個以web.newService聲明,一個以micro.newService聲明
web服務無法注冊鏈路追蹤,解決辦法為:micro.newService創建一個普通服務并注冊到注冊中心,但是不啟動,實際啟動的是web服務
rpc遠程調用通常為 res := client.call(service.func, &req ,&parms)
在Micro api功能中,支持多種處理請求路由的方式,我們稱之為Handler。包括:API Handler、RPC Handler、反向代理、Event Handler,RPC等五種方式
網關將請求根據路由前綴批量轉發到api服務(如:mcms、uims),然后由api服務(消費者)進行匹配,調用業務服務(提供者)
git代碼倉庫每個服務分開,可以考慮使用git submodule來進行管理公共服務
web.NewService和micro.NewService的區別
- 可能的錯誤
1、protobuf缺省值問題
原因:proto3傳輸時,0、""、null會被忽略
解決:可以使用one_of,并且將字段類型設置為string,避免int類型默認值被設置為0
https://developers.google.com/protocol-buffers/docs/proto3#oneof
2、gorm使用update進行更新struct時忽略了0值
原因:update存儲時會先將struct轉map,轉換過程中0值會被忽略
解決一:使用save方法進行保存
解決二:使用update方法進行保存,但傳入參數手動轉換為map類型
3、引入viper組件,讀取配置文件后,開啟調試模式報錯
原因:ide配置錯誤
解決:debug配置->go build->Run kind(package)->working directory(debug服務的根目錄)
4、讀取ctx.request.body時報錯"unexpected EOF"
原因:將數據重新寫入body,因為readall讀取一次后就不在了,會導致后面的api服務讀取body是報錯"unexpected EOF"
解決一:ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
解決二:在gin 1.4 之前,重復使用ShouldBind綁定會報錯EOF。
gin 1.4 之后官方提供了一個 ShouldBindBodyWith 的方法,可以支持重復綁定,
原理就是將body的數據緩存了下來,但是二次取數據的時候還是得用 ShouldBindBodyWith
才行,直接用 ShouldBind 還是會報錯的。
5、啟動服務報錯:panic: qtls.ConnectionState not compatible with tls.ConnectionState
原因:go-micro的部分組件還未對go1.15做適配
解決:使用低于1.15版本的go
6、報錯:command not found: micro
原因:在gopath/bin目錄下沒有micro二進制文件 或 未配置系統環境變量
解決:https://blog.csdn.net/m0_38025165/article/details/106865383
7、報錯:undefined: balancer.PickOptions
原因:依賴沖突
解決:replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
8、生成proto文件報錯:handler/hello.go:8:2: package hello/proto/hello is not in GOROOT (/usr/local/go/src/hello/proto/hello)
解決:其實不是GOROOT的問題,是對應的proto文件沒有生成,進入到服務根目錄下,執行make proto