你還在手撕微服務?快來用go-zero自動生成

0. 爲何說作好微服務很難?

要想作好微服務,咱們須要理解和掌握的知識點很是多,從幾個維度上來講:mysql

  • 基本功能層面git

    1. 併發控制&限流,避免服務被突發流量擊垮
    2. 服務註冊與服務發現,確保可以動態偵測增減的節點
    3. 負載均衡,須要根據節點承受能力分發流量
    4. 超時控制,避免對已超時請求作無用功
    5. 熔斷設計,快速失敗,保障故障節點的恢復能力
  • 高階功能層面github

    1. 請求認證,確保每一個用戶只能訪問本身的數據
    2. 鏈路追蹤,用於理解整個系統和快速定位特定請求的問題
    3. 日誌,用於數據收集和問題定位
    4. 可觀測性,沒有度量就沒有優化

對於其中每一點,咱們都須要用很長的篇幅來說述其原理和實現,那麼對咱們後端開發者來講,要想把這些知識點都掌握並落實到業務系統裏,難度是很是大的,不過咱們能夠依賴已經被大流量驗證過的框架體系。go-zero微服務框架就是爲此而生。redis

另外,咱們始終秉承工具大於約定和文檔的理念。咱們但願儘量減小開發人員的心智負擔,把精力都投入到產生業務價值的代碼上,減小重複代碼的編寫,因此咱們開發了goctl工具。算法

下面我經過短鏈微服務來演示經過go-zero快速的建立微服務的流程,走完一遍,你就會發現:原來編寫微服務如此簡單!sql

1. 什麼是短鏈服務?

短鏈服務就是將長的URL網址,經過程序計算等方式,轉換爲簡短的網址字符串。shell

寫此短鏈服務是爲了從總體上演示go-zero構建完整微服務的過程,算法和實現細節儘量簡化了,因此這不是一個高階的短鏈服務。數據庫

2. 短鏈微服務架構圖

架構圖

  • 這裏把shorten和expand分開爲兩個微服務,並非說一個遠程調用就須要拆分爲一個微服務,只是爲了最簡演示多個微服務而已
  • 後面的redis和mysql也是共用的,可是在真正項目裏要儘量每一個微服務使用本身的數據庫,數據邊界要清晰

3. 準備工做

  • 安裝etcd, mysql, redis
  • 準備goctl工具
  • 直接從https://github.com/tal-tech/go-zero/releases下載最新版,後續會加上自動更新
    • 也能夠從源碼編譯,在任意目錄下進行,目的是爲了編譯goctl工具json

      1. git clone https://github.com/tal-tech/go-zero
      2. tools/goctl目錄下編譯goctl工具go build goctl.go
      3. 將生成的goctl放到$PATH下,確保goctl命令可運行
  • 建立工做目錄shorturl
  • shorturl目錄下執行go mod init shorturl初始化go.mod

4. 編寫API Gateway代碼

  • 經過goctl生成shorturl.api並編輯,爲了簡潔,去除了文件開頭的info,代碼以下:後端

    type (
    	shortenReq struct {
    		url string `form:"url"`
    	}
    
    	shortenResp struct {
    		shortUrl string `json:"shortUrl"`
    	}
    )
    
    type (
    	expandReq struct {
    		key string `form:"key"`
    	}
    
    	expandResp struct {
    		url string `json:"url"`
    	}
    )
    
    service shorturl-api {
    	@server(
    		handler: ShortenHandler
    	)
    	get /shorten(shortenReq) returns(shortenResp)
    
    	@server(
    		handler: ExpandHandler
    	)
    	get /expand(expandReq) returns(expandResp)
    }

    type用法和go一致,service用來定義get/post/head/delete等api請求,解釋以下:

    • service shorturl-api {這一行定義了service名字
    • @server部分用來定義server端用到的屬性
    • handler定義了服務端handler名字
    • get /shorten(shortenReq) returns(shortenResp)定義了get方法的路由、請求參數、返回參數等
  • 使用goctl生成API Gateway代碼

    goctl api go -api shorturl.api -dir api

    生成的文件結構以下:

    .
    ├── api
    │   ├── etc
    │   │   └── shorturl-api.yaml         // 配置文件
    │   ├── internal
    │   │   ├── config
    │   │   │   └── config.go             // 定義配置
    │   │   ├── handler
    │   │   │   ├── expandhandler.go      // 實現expandHandler
    │   │   │   ├── routes.go             // 定義路由處理
    │   │   │   └── shortenhandler.go     // 實現shortenHandler
    │   │   ├── logic
    │   │   │   ├── expandlogic.go        // 實現ExpandLogic
    │   │   │   └── shortenlogic.go       // 實現ShortenLogic
    │   │   ├── svc
    │   │   │   └── servicecontext.go     // 定義ServiceContext
    │   │   └── types
    │   │       └── types.go              // 定義請求、返回結構體
    │   └── shorturl.go                   // main入口定義
    ├── go.mod
    ├── go.sum
    └── shorturl.api
  • 啓動API Gateway服務,默認偵聽在8888端口

    go run api/shorturl.go -f api/etc/shorturl-api.yaml
  • 測試API Gateway服務

    curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn"

    返回以下:

    HTTP/1.1 200 OK
    Content-Type: application/json
    Date: Thu, 27 Aug 2020 14:31:39 GMT
    Content-Length: 15
    
    {"shortUrl":""}

    能夠看到咱們API Gateway其實啥也沒幹,就返回了個空值,接下來咱們會在rpc服務裏實現業務邏輯

  • 能夠修改internal/svc/servicecontext.go來傳遞服務依賴(若是須要)

  • 實現邏輯能夠修改internal/logic下的對應文件

  • 能夠經過goctl生成各類客戶端語言的api調用代碼

  • 到這裏,你已經能夠經過goctl生成客戶端代碼給客戶端同窗並行開發了,支持多種語言,詳見文檔

5. 編寫shorten rpc服務

  • rpc/shorten目錄下編寫shorten.proto文件

    能夠經過命令生成proto文件模板

    goctl rpc template -o shorten.proto

    修改後文件內容以下:

    syntax = "proto3";
    
    package shorten;
    
    message shortenReq {
        string url = 1;
    }
    
    message shortenResp {
        string key = 1;
    }
    
    service shortener {
        rpc shorten(shortenReq) returns(shortenResp);
    }
  • goctl生成rpc代碼,在rpc/shorten目錄下執行命令

    goctl rpc proto -src shorten.proto

    文件結構以下:

    rpc/shorten
    ├── etc
    │   └── shorten.yaml               // 配置文件
    ├── internal
    │   ├── config
    │   │   └── config.go              // 配置定義
    │   ├── logic
    │   │   └── shortenlogic.go        // rpc業務邏輯在這裏實現
    │   ├── server
    │   │   └── shortenerserver.go     // 調用入口, 不須要修改
    │   └── svc
    │       └── servicecontext.go      // 定義ServiceContext,傳遞依賴
    ├── pb
    │   └── shorten.pb.go
    ├── shorten.go                     // rpc服務main函數
    ├── shorten.proto
    └── shortener
        ├── shortener.go               // 提供了外部調用方法,無需修改
        ├── shortener_mock.go          // mock方法,測試用
        └── types.go                   // request/response結構體定義

    直接能夠運行,以下:

    $ go run shorten.go -f etc/shorten.yaml
    Starting rpc server at 127.0.0.1:8080...

    etc/shorten.yaml文件裏能夠修改偵聽端口等配置

6. 編寫expand rpc服務

  • rpc/expand目錄下編寫expand.proto文件

    能夠經過命令生成proto文件模板

    goctl rpc template -o expand.proto

    修改後文件內容以下:

    syntax = "proto3";
    
    package expand;
    
    message expandReq {
        string key = 1;
    }
    
    message expandResp {
        string url = 1;
    }
    
    service expander {
        rpc expand(expandReq) returns(expandResp);
    }
  • goctl生成rpc代碼,在rpc/expand目錄下執行命令

    goctl rpc proto -src expand.proto

    文件結構以下:

    rpc/expand
    ├── etc
    │   └── expand.yaml                // 配置文件
    ├── expand.go                      // rpc服務main函數
    ├── expand.proto
    ├── expander
    │   ├── expander.go                // 提供了外部調用方法,無需修改
    │   ├── expander_mock.go           // mock方法,測試用
    │   └── types.go                   // request/response結構體定義
    ├── internal
    │   ├── config
    │   │   └── config.go              // 配置定義
    │   ├── logic
    │   │   └── expandlogic.go         // rpc業務邏輯在這裏實現
    │   ├── server
    │   │   └── expanderserver.go      // 調用入口, 不須要修改
    │   └── svc
    │       └── servicecontext.go      // 定義ServiceContext,傳遞依賴
    └── pb
        └── expand.pb.go

    修改etc/expand.yaml裏面的ListenOn的端口爲8081,由於8080已經被shorten服務佔用了

    修改後運行,以下:

    $ go run expand.go -f etc/expand.yaml
    Starting rpc server at 127.0.0.1:8081...

    etc/expand.yaml文件裏能夠修改偵聽端口等配置

7. 修改API Gateway代碼調用shorten/expand rpc服務

  • 修改配置文件shorter-api.yaml,增長以下內容

    Shortener:
      Etcd:
        Hosts:
          - localhost:2379
        Key: shorten.rpc
    Expander:
      Etcd:
        Hosts:
          - localhost:2379
        Key: expand.rpc

    經過etcd自動去發現可用的shorten/expand服務

  • 修改internal/config/config.go以下,增長shorten/expand服務依賴

    type Config struct {
    	rest.RestConf
    	Shortener rpcx.RpcClientConf     // 手動代碼
    	Expander  rpcx.RpcClientConf     // 手動代碼
    }
  • 修改internal/svc/servicecontext.go,以下:

    type ServiceContext struct {
    	Config    config.Config
    	Shortener rpcx.Client                                 // 手動代碼
    	Expander  rpcx.Client                                 // 手動代碼
    }
    
    func NewServiceContext(config config.Config) *ServiceContext {
    	return &ServiceContext{
    		Config:    config,
    		Shortener: rpcx.MustNewClient(config.Shortener),    // 手動代碼
    		Expander:  rpcx.MustNewClient(config.Expander),     // 手動代碼
    	}
    }

    經過ServiceContext在不一樣業務邏輯之間傳遞依賴

  • 修改internal/logic/expandlogic.go,以下:

    type ExpandLogic struct {
    	ctx context.Context
    	logx.Logger
    	expander rpcx.Client            // 手動代碼
    }
    
    func NewExpandLogic(ctx context.Context, svcCtx *svc.ServiceContext) ExpandLogic {
    	return ExpandLogic{
    		ctx:    ctx,
    		Logger: logx.WithContext(ctx),
    		expander: svcCtx.Expander,    // 手動代碼
    	}
    }
    
    func (l *ExpandLogic) Expand(req types.ExpandReq) (*types.ExpandResp, error) {
      // 手動代碼開始
    	resp, err := expander.NewExpander(l.expander).Expand(l.ctx, &expander.ExpandReq{
    		Key: req.Key,
    	})
    	if err != nil {
    		return nil, err
    	}
    
    	return &types.ExpandResp{
    		Url: resp.Url,
    	}, nil
      // 手動代碼結束
    }

    增長了對expander服務的依賴,並經過調用expanderExpand方法實現短鏈恢復到url

  • 修改internal/logic/shortenlogic.go,以下:

    type ShortenLogic struct {
    	ctx context.Context
    	logx.Logger
    	shortener rpcx.Client             // 手動代碼
    }
    
    func NewShortenLogic(ctx context.Context, svcCtx *svc.ServiceContext) ShortenLogic {
    	return ShortenLogic{
    		ctx:    ctx,
    		Logger: logx.WithContext(ctx),
    		shortener: svcCtx.Shortener,    // 手動代碼
    	}
    }
    
    func (l *ShortenLogic) Shorten(req types.ShortenReq) (*types.ShortenResp, error) {
      // 手動代碼開始
    	resp, err := shortener.NewShortener(l.shortener).Shorten(l.ctx, &shortener.ShortenReq{
    		Url: req.Url,
    	})
    	if err != nil {
    		return nil, err
    	}
    
    	return &types.ShortenResp{
    		ShortUrl: resp.Key,
    	}, nil
      // 手動代碼結束
    }

    增長了對shortener服務的依賴,並經過調用shortenerShorten方法實現url到短鏈的變換

    至此,API Gateway修改完成,雖然貼的代碼多,可是期中修改的是不多的一部分,爲了方便理解上下文,我貼了完整代碼,接下來處理CRUD+cache

8. 定義數據庫表結構,並生成CRUD+cache代碼

  • shorturl下建立rpc/model目錄:mkdir -p rpc/model

  • 在rpc/model目錄下編寫建立shorturl表的sql文件shorturl.sql,以下:

    CREATE TABLE `shorturl`
    (
      `shorten` varchar(255) NOT NULL COMMENT 'shorten key',
      `url` varchar(255) NOT NULL COMMENT 'original url',
      PRIMARY KEY(`shorten`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  • 建立DB和table

    create database gozero;
    source shorturl.sql;
  • rpc/model目錄下執行以下命令生成CRUD+cache代碼,-c表示使用redis cache

    goctl model mysql ddl -c -src shorturl.sql -dir .

    也能夠用datasource命令代替ddl來指定數據庫連接直接從schema生成

    生成後的文件結構以下:

    rpc/model
    ├── shorturl.sql
    ├── shorturlmodel.go              // CRUD+cache代碼
    └── vars.go                       // 定義常量和變量

9. 修改shorten/expand rpc代碼調用crud+cache代碼

  • 修改rpc/expand/etc/expand.yaml,增長以下內容:

    DataSource: root:@tcp(localhost:3306)/gozero
    Table: shorturl
    Cache:
      - Host: localhost:6379

    可使用多個redis做爲cache,支持redis單點或者redis集羣

  • 修改rpc/expand/internal/config.go,以下:

    type Config struct {
    	rpcx.RpcServerConf
    	DataSource string             // 手動代碼
    	Table      string             // 手動代碼
    	Cache      cache.CacheConf    // 手動代碼
    }

    增長了mysql和redis cache配置

  • 修改rpc/expand/internal/svc/servicecontext.go,以下:

    type ServiceContext struct {
    	c     config.Config
    	Model *model.ShorturlModel   // 手動代碼
    }
    
    func NewServiceContext(c config.Config) *ServiceContext {
    	return &ServiceContext{
    		c:     c,
    		Model: model.NewShorturlModel(sqlx.NewMysql(c.DataSource), c.Cache, c.Table), // 手動代碼
    	}
    }
  • 修改rpc/expand/internal/logic/expandlogic.go,以下:

    type ExpandLogic struct {
    	ctx context.Context
    	logx.Logger
    	model *model.ShorturlModel          // 手動代碼
    }
    
    func NewExpandLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ExpandLogic {
    	return &ExpandLogic{
    		ctx:    ctx,
    		Logger: logx.WithContext(ctx),
    		model:  svcCtx.Model,             // 手動代碼
    	}
    }
    
    func (l *ExpandLogic) Expand(in *expand.ExpandReq) (*expand.ExpandResp, error) {
      // 手動代碼開始
    	res, err := l.model.FindOne(in.Key)
    	if err != nil {
    		return nil, err
    	}
    
    	return &expand.ExpandResp{
    		Url: res.Url,
    	}, nil
      // 手動代碼結束
    }
  • 修改rpc/shorten/etc/shorten.yaml,增長以下內容:

    DataSource: root:@tcp(localhost:3306)/gozero
    Table: shorturl
    Cache:
      - Host: localhost:6379

    可使用多個redis做爲cache,支持redis單點或者redis集羣

  • 修改rpc/shorten/internal/config.go,以下:

    type Config struct {
    	rpcx.RpcServerConf
    	DataSource string            // 手動代碼
    	Table      string            // 手動代碼
    	Cache      cache.CacheConf   // 手動代碼
    }

    增長了mysql和redis cache配置

  • 修改rpc/shorten/internal/svc/servicecontext.go,以下:

    type ServiceContext struct {
    	c     config.Config
    	Model *model.ShorturlModel   // 手動代碼
    }
    
    func NewServiceContext(c config.Config) *ServiceContext {
    	return &ServiceContext{
    		c:     c,
    		Model: model.NewShorturlModel(sqlx.NewMysql(c.DataSource), c.Cache, c.Table), // 手動代碼
    	}
    }
  • 修改rpc/shorten/internal/logic/shortenlogic.go,以下:

    const keyLen = 6
    
    type ShortenLogic struct {
    	ctx context.Context
    	logx.Logger
    	model *model.ShorturlModel          // 手動代碼
    }
    
    func NewShortenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ShortenLogic {
    	return &ShortenLogic{
    		ctx:    ctx,
    		Logger: logx.WithContext(ctx),
    		model:  svcCtx.Model,             // 手動代碼
    	}
    }
    
    func (l *ShortenLogic) Shorten(in *shorten.ShortenReq) (*shorten.ShortenResp, error) {
      // 手動代碼開始,生成短連接
    	key := hash.Md5Hex([]byte(in.Url))[:keyLen]
    	_, err := l.model.Insert(model.Shorturl{
    		Shorten: key,
    		Url:     in.Url,
    	})
    	if err != nil {
    		return nil, err
    	}
    
    	return &shorten.ShortenResp{
    		Key: key,
    	}, nil
      // 手動代碼結束
    }

    至此代碼修改完成,凡事手動修改的代碼我加了標註

10. 完整調用演示

  • shorten api調用

    curl -i "http://localhost:8888/shorten?url=http://www.xiaoheiban.cn"

    返回以下:

    HTTP/1.1 200 OK
    Content-Type: application/json
    Date: Sat, 29 Aug 2020 10:49:49 GMT
    Content-Length: 21
    
    {"shortUrl":"f35b2a"}
  • expand api調用

    curl -i "http://localhost:8888/expand?key=f35b2a"

    返回以下:

    HTTP/1.1 200 OK
    Content-Type: application/json
    Date: Sat, 29 Aug 2020 10:51:53 GMT
    Content-Length: 34
    
    {"url":"http://www.xiaoheiban.cn"}

11. Benchmark

由於寫入依賴於mysql的寫入速度,就至關於壓mysql了,因此壓測只測試了expand接口,至關於從mysql裏讀取並利用緩存,shorten.lua裏隨機從db裏獲取了100個熱key來生成壓測請求

benchmark

能夠看出在個人MacBook Pro上能達到3萬+的qps。

12. 總結

咱們一直強調工具大於約定和文檔

go-zero不僅是一個框架,更是一個創建在框架+工具基礎上的,簡化和規範了整個微服務構建的技術體系。

咱們在保持簡單的同時也儘量把微服務治理的複雜度封裝到了框架內部,極大的下降了開發人員的心智負擔,使得業務開發得以快速推動。

經過go-zero+goctl生成的代碼,包含了微服務治理的各類組件,包括:併發控制、自適應熔斷、自適應降載、自動緩存控制等,能夠輕鬆部署以承載巨大訪問量。

13. 項目地址

https://github.com/tal-tech/go-zero

14. 微信交流羣

微信

相關文章
相關標籤/搜索