Try   HackMD

簡介

什麼是 go-zero

解放雙手,加速開發

  • 微服務架構
    • go-zero 基於微服務架構設計,其生成的程式碼也有固定的架構
  • 一鍵生成 code 的工具
    • 自動產生的code完整涵蓋入口點到業務邏輯整段,必須要自己寫的部分只有業務邏輯和功能邏輯
  • 程式碼實踐的集合
    • go-zero 中包含大量的工具封裝外 (log、http、mysql、redis 等),也提供很多設定可以去按需啟用或設定一些內建功能(例如限流、監控日誌)
  • 大神們的結晶
    • go-zero 是個開源專案,各路大神的實踐經驗囊括其中

優點

  • 縮短從需求到上線的距離
  • 適用於高流量的大規模系統
  • 分層架構設計
  • 豐富的內建功能支援,ex: log、tracing、限流設定

缺點

  • 缺乏自由度
  • 支援生成程式碼,但拔除需自行刪除相關 func
  • 不支援單元測試相關生成
  • 不支援 redismock
  • 不適用複雜的 model 存取模式 ex. db join

架構

go-zero 可以只拿來開發 http server 或是 gRpc server / client 的 單體服務(mono)

但一個完整的服務通常是我全都要!
建造一個同時提供對外的 Http 接口加上對內的 gRpc 接口的 微服務(micro)

  • 一對多(參考)移除其中一條 rpc 服務就變成一對一

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

微服務間盡可能只使用自己相關的資料庫,資料邊界要劃清

What is rpc

  • rpc = 服務間的傳輸協定
  • gRPC = 基於 rpc 加入 Protobuf 解決溝通介面混亂與傳輸緩慢的問題
  • zRPC = 基於 gRPC 加入 服務註冊、負載均衡、服務攔截器 等功能
    是 go-zero 封裝的 rpc 模組

開發流程

基本上只要理解 .api.proto 要怎麼撰寫,以及如何使用 goctl 指令去幫助程式自動產生
就只需要專注在 依賴注入業務邏輯功能邏輯 的開發就好

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

常用指令

goctl 安裝

$ go install github.com/zeromicro/go-zero/tools/goctl@latest 

要是套件安裝失敗要確認環境$PATH

$ export GOPATH=$HOME/go
$ export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

template 生成

api

goctl api -o xxx.api

protoc

goctl rpc template -o xxx.proto

程式生成

api

goctl api go -api xxx.api -dir .

protoc

goctl rpc protoc xxx.proto --go_out=. --go-grpc_out=. --zrpc_out=.

model
-c 代表該 model 啟用 redis cache 每次查詢後會把相關結果存入 redis 中,於下次查詢時優先查找

goctl model mysql ddl -src= xxx.sql  -dir ./xxx -c 

goctl 程式生成功能概覽

有些是預設有的,有些是下指令時要特別指定的
他們幾乎都可以用 config 再去做一些細部設定(例如:超時控制的秒數、log config)

圖內已翻譯成常用的名詞,但要在官方簡體文件查找相關用法建議使用原文

  • 鏈路追蹤:tracing,追蹤某個請求在微服務間呼叫的路徑
  • 監控報警:監控服務數值,如請求數、資料庫連線數
  • 數據統計:收集數據提供給(熔斷)斷路器使用
  • 自動熔斷:察覺依賴之其他微服務過載時,停止呼叫,並維持自身不噴掉 → 持續等待直到回傳成錯誤
  • 自動降載:微服務本身過載時,拒絕溢出的請求進入
  • 緩存控制:把DB查詢結果緩存在redis裡,查詢時先找緩存,找不到再去DB;如果DB裡找到了,就把該值放入緩存,找不到就不放。
  • 緩存穿透:查詢一個資料庫內一定不存在的值,但因為仍白走了緩存控制的流程,導致服務的壓力
  • 緩存擊穿:緩存key被建立前拿不到資料,導致大量請求流入DB
  • 緩存雪崩:同一時間段內有大量緩存集中失效過期,對服務造成週期性壓力
  • 緩存索引:通過索引查找緩存

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

練習

專案 Github

API server(http only)

超基礎版,含 DB、Redis 的連線與簡單操作

quick start

$ goctl api new demo

可以快速得到一個名叫 demo 的 http server

連線DB

add config

internal/config/config.go

type Config struct {
	rest.RestConf

        // 加上DB結構體
	DB struct {
		DsnString string
	}
}

etc/demo-api.yaml

# DB
DB:
  DsnString: root:yourPwd@tcp(127.0.0.1:3306)/Demo?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
  

導入model

先撿到一份建表使用的.sql,把它放到 model/tableName 底下

CREATE TABLE user (
    id bigint AUTO_INCREMENT,
    name varchar(255) NULL COMMENT 'The username',
    password varchar(255) NOT NULL DEFAULT '' COMMENT 'The user password',
    mobile varchar(255) NOT NULL DEFAULT '' COMMENT 'The mobile phone number',
    gender char(10) NOT NULL DEFAULT 'male' COMMENT 'gender,male|female|unknown',
    nickname varchar(255) NULL DEFAULT '' COMMENT 'The nickname',
    type tinyint(1) NULL DEFAULT 0 COMMENT 'The user type, 0:normal,1:vip, for test golang keyword',
    create_at timestamp NULL,
    update_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE mobile_index (mobile),
    UNIQUE name_index (name),
    PRIMARY KEY (id)
) ENGINE = InnoDB COLLATE utf8mb4_general_ci COMMENT 'user table';

然後下指令

$ goctl model mysql ddl --src user.sql --dir ./model/tableName

它會自動產生這些檔案,usermodel_gen.go 裡面就已經包含了基礎的CRUD,有 unique 的 key 也會產生好 byKey 去查詢的 func,需要更多再自行增加即可

以這個 case 來說,只要把 usermodel.go 中的 UserModel new 出來後,就可以透過 usermodel_gen.go 裡的 func 與 DB 互動了

model/user/usermodel.go (go-zero幫你產生的)

package mysql

import "github.com/zeromicro/go-zero/core/stores/sqlx"

var _ UserModel = (*customUserModel)(nil)

type (
	// UserModel is an interface to be customized, add more methods here,
	// and implement the added methods in customUserModel.
	UserModel interface {
		userModel
	}

	customUserModel struct {
		*defaultUserModel
	}
)

// NewUserModel returns a model for the database table.
func NewUserModel(conn sqlx.SqlConn) UserModel {
	return &customUserModel{
		defaultUserModel: newUserModel(conn),
	}
}

add svc

import usermodel "go_zero/demo/model/user"

type ServiceContext struct {
	Config config.Config

	// 定義 UserModel 結構體
	UserModel usermodel.UserModel
}

func NewServiceContext(c config.Config) *ServiceContext {
	// 資料庫連線
	sqlConn := sqlx.NewMysql(c.DB.DsnString)

	return &ServiceContext{
		Config: c,
		// 把 UserModel物件 new 出來
		UserModel: usermodel.NewUserModel(sqlConn),
	}
}

create api and logic

註冊API

於專案的.api中添加要新增的router、定義傳入及回傳格式

demo.api

type SignUpRequest {
	Name     string `json:"name"`
	Password string `json:"password"`
	Mobile   string `json:"mobile,optional"`
	Gender   string `json:"gender,options=M|F"`
	Nickname string `json:"nickname"`
}

type SignUpResponse {
	Message string `json:"message"`
}

service demo-api {
	@handler UserSignUpHandler
	post /user/sign-up(SignUpRequest) returns (SignUpResponse)
}

執行指令

$ goctl api go --api demo.api --dir .

從 router、handler、logic 的空 func 它自動產生,此時這個路徑已經能正常請求
接下來只要撰寫核心邏輯就好

但如果是移除已經有的api,它不會幫你把存在的檔案刪掉,但會刪除 router

撰寫邏輯

internal/logic/usersignuplogic.go

// ...
// 上面的code是go-zero自動產生,不用改

func (l *UserSignUpLogic) UserSignUp(req *types.SignUpRequest) (resp *types.SignUpResponse, err error) {
    // 呼叫前面在 svc 中新定義的 UserModel 下的功能
    // 將請求內容寫入資料庫
	_, err = l.svcCtx.UserModel.Insert(l.ctx, &usermodel.User{
		Name: sql.NullString{
			String: req.Name,
			Valid:  true,
		},
		Password: req.Password,
		Mobile:   req.Mobile,
		Nickname: req.Nickname,
		Gender:   req.Gender,
	})

    // 設定回傳
	resp = &types.SignUpResponse{
		Message: fmt.Sprintf("Name: %s Added", req.Name),
	}

	return
}

連線Redis

go-zero 不建議(也不支援)用切db的方式來區分不同的用途

載入設定檔

internal/config/config.go

package config

import (
	"github.com/zeromicro/go-zero/core/stores/redis" // go-zero包好的redis庫
	"github.com/zeromicro/go-zero/rest"
)

type Config struct {
	rest.RestConf

	DB struct {
		DsnString string
	}

        // 新增一行 redis 設定
	Redis redis.RedisConf
}

RedisConf 於套件中已定義好的結構如下

type RedisConf struct {
    Host     string
    Type     string `json:",default=node,options=node|cluster"`
    Pass     string `json:",optional"`
    Tls      bool   `json:",optional"`
    NonBlock bool   `json:",default=true"`
    // PingTimeout is the timeout for ping redis.
    PingTimeout time.Duration `json:",default=1s"`
}

於 yaml 檔中添加環境變數,根據上面的結構

# Redis
Redis:
  Host: "127.0.0.1:6379"
  Pass: ""

之後,就會透過 main() 裡的 conf.MustLoad(*configFile, &c) 自動載入了

add svc

然後我們一樣要在 ServiceContext 裡註冊他

package svc

import (
	"go_zero/demo/demo/internal/config"
	usermodel "go_zero/demo/demo/model/user"

	"github.com/zeromicro/go-zero/core/stores/redis"
	"github.com/zeromicro/go-zero/core/stores/sqlx"
)

type ServiceContext struct {
	Config config.Config
	// 定義 redis 連線物件
	RedisClient *redis.Redis

	// 定義 UserModel 結構體
	UserModel usermodel.UserModel
}

func NewServiceContext(c config.Config) *ServiceContext {
	// 資料庫連線
	sqlConn := sqlx.NewMysql(c.DB.DsnString)

	// redis 連線
	redisConn := redis.MustNewRedis(c.Redis)

	return &ServiceContext{
		Config: c,
		// redis 連線物件
		RedisClient: redisConn,
		// UserModel db 連線物件
		UserModel: usermodel.NewUserModel(sqlConn),
	}
}

reids 操作

/logic 中透過 svcCtx 取用 RedisClient redis 連線物件,來進行任何想做的操作

package logic

import (
	"context"
	"database/sql"
	"fmt"

	"go_zero/demo/demo/internal/svc"
	"go_zero/demo/demo/internal/types"

	"github.com/zeromicro/go-zero/core/logx"
)

type UserLoginLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewUserLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserLoginLogic {
	return &UserLoginLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}

//-----以上code由go-zero產生-----

// 以下 UserLogin 內的功能要自己寫
func (l *UserLoginLogic) UserLogin(req *types.LoginRequest) (resp *types.LoginResponse, err error) {
	result, err := l.svcCtx.UserModel.FindOneByName(l.ctx, sql.NullString{
		String: req.Name,
		Valid:  true,
	})

	if result.Password != req.Password {
		return
	}

	// 存入redis
	err = l.svcCtx.RedisClient.Set(fmt.Sprintf("%d", result.Id), result.Name.String)
	resp = &types.LoginResponse{
		Token: fmt.Sprintf("%s@%d", result.Name.String, result.Id),
	}

	return
}

middleware

resp errorHandle

微服務

etcd 部署

先起一台 etcd 服務,後續才能繼續開發 grpc 的微服務

docker-compose.yaml

version: '3'

networks:
  web-network:

services:
  docker-etcd:
    hostname: etcd
    image: bitnami/etcd:3.5.5
    volumes:
      - "./etcd/data:/bitnami/etcd/data"
    environment:
      - ALLOW_NONE_AUTHENTICATION=yes
      - ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379
    ports:
      - "2379:2379"
      - "2380:2380"
    networks:
      - web-network

  docker-etcdkeeper:
    hostname: etcdkeeper
    image: evildecay/etcdkeeper:v0.7.6
    ports:
      - "8099:8080"
    networks:
      - web-network

protoc 撰寫

於目錄 ./project/rpc/{microServiceName} 下執行指令

$ goctl rpc template -o {microServiceName}.proto  

調整裡面的內容後,執行指令來生成微服務

$ goctl rpc protoc {microServiceName}.proto --go_out=. --go-grpc_out=. --zrpc_out=.

這是我們撰寫的

這是goctl幫我們產生的

雜談&待討論

小記

它會幫我們包好 mysql redis的連線以及使用

http error handle

https://go-zero.dev/docs/tutorials/http/server/response/ext

要統一每個專案的錯誤處理評估有兩條路

  1. 每個專案有自己的 errorCode & errorMessage(i18n) config + 統一內容的 response 處理邏輯 pkg code,於每加一個 API 程式碼產生後自行去 handler 裡修改 code

  2. 開發一個單位內部的 package,把所有專案錯誤代碼的邏輯都控在裡面。每個人都去修改自己 goctl 的模板語法去 import 那個 package,之後所有自動產生的 handler 都會使用一樣的錯誤處理方式

單體 api 服務與微服務的選擇

適合單體

  • 服務目的較單純
  • 跟別的服務不會有業務重疊,不需提供對其他專案的 rpc 接口

適合微服務

  • logic 具有複用性,自己用多次 或是跟別的服務有重疊
  • 為什麼 api 中的 logic 不應該複用?
    • logic 層相當於 controller 層,進去出來的參數格式為 api req 和 resp
    • 兩隻 api 參數不可能一樣,即使抽出當中交集的參數出來
      • 若另外定義介面或結構體,它名目是什麼?
      • 若定義一複用 func 吃相同的 input,那變成 logic 直接相依該 func
    • 違反依賴注入的架構特性
  • 什麼時候會使用 rpc ?
    • 兩個 api server 會用到相同的邏輯

會議記錄

分享內容

從零開始學

  • 介紹go-zero
    • 功能
  • 介紹架構
  • 實際生出來 rpc + api + model(含常用指令) demo
    • 先準備檔案 上面寫好註解
    • 直接操作
  • 大致介紹內部 call func 方式 看code
  • 介紹已經串起來的專案 串的方式 看code
    • 可以生成前端 code
  • 其他補充內容(middleware api-error-handle

如何變成我們的架構

  • ++ db redis 連線
  • db join
  • config 從哪來 分環境
  • gcp log、request log 紀錄哪些 logx 怎用
    • gcp
    • elk api request
  • error resp
  • error msg 依類型區分
  • testing
  • pub/sub 哪些模式下會用到
  • 服務怎麼切微服務 repo的大小

產出

三包不同程度的專案

  • 初始

    • .api
    • .protoc
    • 指令流程都準備好
  • 基礎都建好

    • 一對一微服務
    • 有連線,可真的存取資料
    • 開發流程,怎麼注入
  • 可能的未來架構 各功能有可以參考的基底

    • 有中間層
    • 有log
    • 有測試
    • 微服務間的溝通?不同repo間?

架構設計

  • 每個 Microservice 或 MonoService 都以 Domain 做切分,只會去連線 Domain 相關的 table
  • 服務們共用的 package 如 實作 log、api 回傳格式化及 checkSession 的核心邏輯
  • rpc 的定位為,同時提供對內及對外接口,對內給自身的 api gateway,對外供其他 Domain 的服務使用(如:bet in user 需要 call event 的賽事資訊
project-structure-design
 ├── event.go
 ├── go.mod
 ├─> pkg (共用套件)
 │   ├─> checkSession
 │   ├─> logger
 │   └─> responseHandler
 ├─> service-micro (event 微服務)
 │   ├─> api
 │   │   ├─> desc
 │   │   │   ├── event.api
 │   │   │   └─> types
 │   │   │       └── event.api
 │   │   ├─> etc
 │   │   │   └── event.yaml
 │   │   ├── event.go
 │   │   └─> internal
 │   │       ├─> config
 │   │       │   ├── config.go
 │   │       │   └─> errorCode
 │   │       ├─> handler
 │   │       │   ├─> event
 │   │       │   │   └── geteventhandler.go
 │   │       │   └── routes.go
 │   │       ├─> logic
 │   │       │   └─> event
 │   │       │       └── geteventlogic.go
 │   │       ├─> middleware (自定義中間件)
 │   │       │   └── middleware.go
 │   │       ├─> svc
 │   │       │   └── servicecontext.go
 │   │       └─> types
 │   │           └── types.go
 │   ├─> model
 │   │   └─> sql
 │   └─> rpc
 │       ├─> etc
 │       │   └── event.yaml
 │       ├── event.go
 │       ├─> eventctl
 │       │   └── eventctl.go
 │       ├─> internal
 │       │   ├─> config
 │       │   │   └── config.go
 │       │   ├─> logic
 │       │   │   ├── loginlogic.go
 │       │   │   └── registerlogic.go
 │       │   ├─> server
 │       │   │   └── eventctlserver.go
 │       │   └─> svc
 │       │       └── servicecontext.go
 │       ├─> pb
 │       │   ├── event.pb.go
 │       │   └── event_grpc.pb.go
 │       └─> proto
 │           └── event.proto
 └─> service-mono (user 單體服務)
     ├─> api
     │   ├─> desc
     │   │   └── user.api
     │   ├─> etc
     │   │   └── user.yaml
     │   ├─> internal
     │   │   ├─> config
     │   │   │   ├── config.go
     │   │   │   └─> errorCode
     │   │   ├─> handler
     │   │   │   ├─> bet
     │   │   │   │   └── bethandler.go
     │   │   │   └── routes.go
     │   │   ├─> logic
     │   │   │   └─> bet
     │   │   │       └── betlogic.go
     │   │   ├─> middleware
     │   │   ├─> svc
     │   │   │   └── servicecontext.go
     │   │   └─> types
     │   │       └── types.go
     │   └── user.go
     └─> model
         └─> sql