base業務框架開發手冊

cdh0805010118發表於2018-12-27

base 業務框架使用手冊

開發手冊

本專案是對當前使用 base 業務框架的開發人員,提供一份完整的開發手冊,方便開發人員使用。

> 希望本文件能夠給大家帶來方便。同時業務框架在不斷更新中..., 如果文件沒有及時更新,也可以 email 或者提相關的 issue

編寫微服務

採用demo專案,作為初始專案,然後修改為自己的專案。

總體推薦一個新建立的產品,專案結構目錄推薦以下呈現方式:

chen:demo chenchao$ ls
README.md       common          glide.lock      token           vendor
address         gateway         glide.yaml      user
chen:demo chenchao$ ls gateway/
gateway         main.go         r_address.go    r_errors.go     r_me.go         r_token.go      schema.go
chen:demo chenchao$ ls address/
controllers     main.go         models          schema.go
chen:demo chenchao$ ls common/
consts  rpc     types   utils
  1. 對於專案目錄來說,比如:demo 是由四個微服務加上一個common目錄構成,其他是相關依賴包。一個專案必然有一個閘道器服務,對外暴露 API 的;
  2. 對於 gateway 閘道器服務,它是直接轉發 web 的請求訪問,同時可能會做一些身份登入校驗,校驗 token 的有效性;以及可以做鑑權和流控;不過後者可以在上 istio 後,流控就可以業務不關心了
  3. 其他微服務程式碼結構,則是由main.go, schema.go, types, controllers, models五個檔案或者目錄構成;
  4. common目錄,主要用於一些 schema 的資源定義,公共元件,錯誤碼註冊,微服務埠和名稱定義、rpc 微服務呼叫和常量定義。

> 對於上面的第 3 點需要說明的是,types 目錄可能也不需要,它主要定義 schema 的資源物件,如果 gateway 需要與微服務共用,就需要寫入到 common 目錄中

> 這裡說明一點, 理論上微服務容器化後,所有微服務埠都可以是相同的,但是我們為了相容非容器化,所以微服務埠是列舉型的,從 8081 開始進行 iota。

環境變數

名稱 描述
APP_ZK 服務地址 配置中心地址,預設值:127.0.0.1:2181
APP_NAME 專案名稱 產品名稱
APP_LOG log 絕對路徑 預設:/var/log/app
APP_ENV 部署環境 本地環境、開發、測試和生產, 預設:developer
APP_VERSION 產品版本 預設: v1.0
APP_TRACER_AGENT jaeger agent 地址 預設值: 0.0.0.0:6831

graphql 常用方法

graphql 是 facebook 發明的,對外提供 API 的兩種方式:RESTful 與 graphql,各有優劣;graphql 最大的優勢所見即所得,協議即介面。

下面給出的常用方法通常是 rpc 呼叫後,需要進行 graphql 變數值與 golang 變數值的相互轉化。

// FixTypeFromGoToGraphql方法
// @params0: rpc返回的資料
// @params1: rpc返回的資料graphql schema資源物件定義
// 含義:把graphql資源物件資料轉化為golang變數值
// 比如:graphql中的列舉Enum定義,需要轉化為golang中的列舉定義
// 比如:graphql中的資源列表定義,需要轉化為golang中的列表定義,因為在graphql中的列表都是interface型別的,需要轉化為golang中的資源定義的已知型別
// ...
**func FixTypeFromGoToGraphql(v interface{}, argType graphql.Input) (result interface{})**

// FixTypeFromGraphqlToGo方法, 與上面的方法作用相反
// @param0: 
func FixTypeFromGraphqlToGo(data interface{}, t graphql.Output) interface{}

這裡提供了幾個有關這兩個方法的單元測試:

 cd $GOPATH/src/github.com/microsvs/base

 go test -cover=true -run TestFixTypeFromGraphqlToGoSimple

 # 上面單元測試返回結果:
10,20,30

go test -cover=true -run TestFixTypeFromGraphqlToGoComplex 

# 上面單元測返回結果:
map[created_at:2019-11-24 03:29:08 +0800 CST name:kim age:16 vehicle:10 workers:[map[company:alibaba position:30] map[company:tencent position:40]] updated_at:2019-11-24 03:29:08 +0800 CST]

通過上面兩個單元測試,我們可以明白 FixTypeFromGraphqlToGo 是用來把 graphql 資料轉化為 golang 識別的資料型別;

注意,在 rpc 呼叫過程中,我們使用的還是 graphql 資料模型的通訊,所以需要在請求資料和響應資料前後進行相關型別的資料轉換,比如:列舉型別、時間型別和其他不相同形式的資料轉換

 cd $GOPATH/src/github.com/microsvs/base

go test -cover=true -run TestFixTypeFromGoToGraphqlSimple

# 上面單元測試返回結果
CEO

go test -cover=true -run TestFixTypeFromGoToGraphqlComplex

# 上面單元測試返回結果:
map[created_at:2018-12-27T14:12:49.523371245+08:00 updated_at:2018-12-27T14:12:49.523371228+08:00 name:"kim" age:16 vehicle:Bike workers:[map[company:"alibaba" position:CEO] map[company:"tencent" position:CTO]]]

通過上面兩個單元測試,我們可以明白 FixTypeFromGoToGraphql 方法是用來把 go 型別資料轉化為 graphql 資料;

注意 graphql 型別資料與 golang 型別資料的相互轉化後的資料對比,看看哪裡資料有變化


// 方法用於隱藏不想展示給呼叫方的欄位資料,比如,有些密碼、token 等隱私資料。 > func HideGLFields(obj *graphql.Object, v ...string) *graphql.Object

// 方法用於 gateway 直接呼叫後端,複用請求,指定目標服務 > func RedirectRequest(p graphql.ResolveParams, target rpc.FGService) (interface{}, error)

// 修改 gateway 接收的請求,然後轉發給指定目標服務 > func RedirectRequestEx( p graphql.ResolveParams, exArgs map[string] interface{}, excommon map[string] graphql.Input, targetService rpc.FGService, targetObj interface{}) (interface{}, error)

該方法使用場景在於,當我們需要組織後端服務收集到的資料,然後再轉發到其他微服務時,就需要構建一個請求

> base.RedirectRequestEx( "QUERY/MUATION", p, map[string]interface{}{"user_id": user.ID}, map[string]graphql.Input{"user_id": graphql.String}, base.FGSUser, "beat_map_type", nil)

// GLObjectFields 獲取 graphql.Object 的所有欄位, 作為 graphql Query/Mutation 的返回值 > func GLObjectFields(obj *graphql.Object) string

配置中心

配置中心,主要是對專案需要用到的初始化或者環境配置,都可以放到配置中心;包括但不限於:專案名、版本號、服務部署環境、cache 配置、db 配置、mq 配置、dns 配置和其他引數配置

每個微服務都單獨配置各項所用到的配置,該業務框架認為各個微服務配置都是不一樣的。

對於儲存配置,包括 cache 和 db,都是主從配置 master 和 slave,一主多從的配置方式;可以不填寫 slave 配置, 則 slave 自動共用 master 配置

slave 可以指定多個,配置由,逗號分隔 slave。

目前環境氛圍四個環境:developer 本地開發環境,dev-開發環境、test-QA 環境、prod-生產環境

快取配置

// 快取,比如 cache,可以指定密碼和沒有密碼兩種方式, 當不指定 redis 密碼時,則@非必空 > /demo/v1.0/dev/cache/user=master=redis://auth-pass@127.0.0.1:6379

> /demo/v1.0/dev/cache/user=master=redis://auth-pass@192.168.1.100:6379,redis://auth-pass@192.168.1.101:6379,192.168.1.102:6379

db

// db 儲存,比如 mysql > /demo/v1.0/dev/db/user=master=username:password@tcp(192.168.1.101:3306)/demo?charset=utf8&parseTime=True&loc=Local

// mongodb > /demo/v1.0/dev/mongo/user=master=username:password@192.168.1.101:3000/demo

訊息佇列

> /demo/v1.0/dev/mq/user=maste=amqp://username:password@192.168.1.101:3001/queue?heartbeat=15

dns

> /demo/v1.0/dev/dns=gateway.api.xhj.com=192.168.1.101:8081

注意 dns 的 key 為 xxx.api.xhj.com, xhj是表示 base 業務框架作者的江湖稱呼:小黃雞;value 可以為 service 的高可用配置, 比如: VIP

引數配置

專案除了通用配置外,可能還會遇到其他引數配置,比如設定 feature 開關等, oss 路徑,sms 配置等

配置儲存

  1. 當微服務第一次從配置中心獲取,需要一次冷載入,可以設定在微服務啟動時,也就是程式的 init 方法中,初始化將要使用的儲存或者其他配置;
  2. 該業務框架使用了本地快取,儲存配置,當微服務再次獲取配置時,則直接在本地快取中獲取的。
  3. 同時第一次從配置中心獲取配置後,就開始了監聽配置中心的 key,並更新到本地快取中。如果是儲存相關包括 db、cache、mq、config 等會監聽到配置變化後,會自動重連各個儲存 server。則對業務是無感知的

日誌模組

base 業務框架的日誌模組,是採用的本地落日誌。這裡講下我做過的日誌優化相關工作。

現象: 因為我在使用該base業務框架進行業務上線後,我們使用的k8s叢集,微服務經常由於記憶體佔用過大(100Mi),導致被殺,然後又漲又殺。我發現微服務本身只佔用了20M的記憶體。

原因:k8s監控pod,不僅僅是微服務程式本身的記憶體佔用;它是對container記憶體資源佔用的總和,我監控container,top發現cache/buffer不斷升高,後來查了文件,發現k8s是統計的cgroup記憶體資源佔用,所以導致的container被殺後,k8s又重新排程。

解決思路:在本地落日誌時,作業系統認為落日誌的檔案會立即讀取,所以作業系統會在日誌落到磁碟之前,會儲存到page cache中,這樣page/cache數值越來越大。

解決方法:在落日誌時,我們業務框架使用bytes.buffer進行4kb的快取,每次寫入4kb,也就是一頁的資料量,這種寫入的方式是直接IO方式,這樣就不會在page cache進行日誌快取了。os.DIRECT_IO,又因為各個平臺底層架構不同,對於macos用的flag與linux用的flag不同等原因,最後就是用的一個簡單封裝庫directio。

效果:優化後,效果非常理想,現線上上業務一直在跑,但是記憶體佔用基本上穩定不變。

後話:cache/buffer是可以重複利用的記憶體,只是對於k8s叢集平臺,它是監控的cgroup資源,所以遇到了這個問題。

日誌使用方法

日誌的使用,不需要初始化,呼叫即初始化。

提供的方法有: log.ErrorRaw, log.Debug, log.Info等方法可以使用。

日誌寫入到的檔案路徑:環境變數 ${APP_LOG},預設/var/log/app 目錄下,再根據微服務名稱進行日誌分資料夾。日誌目錄層次可以自己調整,目前業務預設支援的日誌路徑:

/var/log/app/${service name}/YYYYMM/DD/${service name}_YYYYMMDD.log

> 備註:當日志落地到本地後,你可以通過啟動一個 agent 日誌收集服務,比如 fluentd,ELK。

db 模組

db 模組,我們使用了一個 DAL,資料訪問層,遮蔽了底層 db 儲存的細節,底層支援 PostgreSQL, MySQL, SQLite, MSSQL, QL and MongoDB;

大家可以檢視這個 DAL 資料訪問層的 github 庫,upper/db, 自認為比其他的 orm 好用很多。

對於資料庫的初始化,我們可以微服務啟動時,在 init 方法中進行儲存 client 的初始化連線 > db.InitDB(rpc.FGSUser)

然後在業務需要使用的時候,比如:更新、新增和刪除操作

// 從 slave 獲取一個 db 連線 > db.SlaveDB(rpc.FGSUser) // 從 master 獲取一個 db 連線 > db.MasterDB(rpc.FGSUser)

快取模組

對於 redis 快取,不需要業務端做相關的連線釋放動作。並提供了對快取的 interface。

type Connection interface {
    Set(key string, value interface{}) error
    Get(key string) (interface{}, error)
    Del(key string) error
    Expire(key string, sec int) error
    Exist(key string) (bool, error)
    TTL(key string) (time.Duration, error)
    Close() error
    ComplexCmd(cmd string, values ...interface{}) (interface{}, error)
}
// 這個interface前6個方法基本上滿足了90%的需求,最後一個方法ComplexCmd則是對複雜需求的快取操作命令。

初始化和獲取快取連線的方法,同 db 操作

訊息佇列模組

提供了兩個初始化和監聽消費佇列訊息的 API。

// 初始化訊息佇列連線 > func InitMQ(service rpc.FGService)

// 使用預設的方法監聽消費佇列產生的訊息 > func ConsumeDefault(service rpc.FGService, queue string) <-chan amqp.Delivery

// 同時提供了一個多配置監聽消費佇列產生的訊息 > func Consume(service rpc.FGService, queue, consumer string, autoAck, exclusive, noLocal, noWait bool, args amqp.Table) <-chan amqp.Delivery

> 訊息佇列 client 具有重連機制

錯誤碼

base 業務框架本身已經提供了一些錯誤碼,我們可以通過 gateway 的 errors 查詢介面,去檢視這些錯誤碼及其含義。

比如:系統錯誤,客戶端請求錯誤。可以在$GOPATH/src/github.com/microsvs/base/pkg/errors/const.go檔案中檢視。

同時如果業務微服務需要其他錯誤碼,則需要進行錯誤碼的註冊, 通過呼叫pkg/errors/register.go檔案中的 Register 方法進行錯誤碼註冊。

// 業務微服務錯誤碼註冊 > func Register(errMap map[FGErrorCode] string) error

服務註冊

base 業務框架本身也提供了一些服務,包括:閘道器、簽名、流控、使用者、token、地址和圖片共 7 個服務。如果還需要其他微服務,則通過 RegisterService 方法註冊。

檔案路徑:$GOPATH/src/github.com/microsvs/base/pkg/rpc/service.go

> func RegisterService(service FGService, serviceName string) error

rpc 呼叫

當微服務之間需要進行 rpc 呼叫時,框架提供了一個 API 進行跨服務呼叫。

// dns 引數值可以通過 base.Service2Url(service) 獲取 > func CallService(ctx context.Context, dns string, data string) (map[string] interface{}, error)

常用方法

$GOPATH/src/github.com/microsvs/demo/common/utils目錄下提供了一些常用方法。比如:

  1. http 呼叫, HttpPostBody 和 HttpPostJson API;
  2. 提供了阿里雲 oss 儲存封裝好的 API;
  3. 對於所有 graphql 請求的引數必填校驗 API,CheckAndAssignParams,該方法的實用性非常高
  4. ArrayToString 方法,slice 型別的資料,按照指定格式進行字串化;
  5. 生成 4 位的簡訊驗證碼,GenerateVerifyCode
  6. GenerateUUID 方法,生成唯一的 uuid
  7. 還提供了一個獲取時間變數 timer.Now,由一個 goroutine 進行託管, 這樣系統不用頻繁去呼叫時間
// 必填引數校驗,並把資料返回
if err = utils.CheckAndAssignParams(p.Args, map[string]interface{}{
    &quot;sale_order_id&quot;: &amp;saleOrderId,
    &quot;vehicle_no&quot;:    &amp;vehicleNo,
}); err != nil {
    return false, err
}

身份攜帶

最後一個需要說明的小點:

> 有身份的使用者在跨微服務呼叫時,都會把自身的身份帶到 context 中,這樣直接從 context 中獲取 token、mobile 和其他相關資訊是非常方便的。

使用方式如下所示:

if _, ok = p.Context.Value(rpc.KeyUser).(*itypes.User); !ok {
    log.ErrorRaw(&quot;[GetAdOnlineLaunchStatis] get user from context failed.&quot;)
    return nil, errors.FGEInvalidToken
}

通過上面的這種方式,我們就可以校驗使用者是否有登入態,context 目前支援的 key 有

KeyRPCID
KeyService
KeyRawRequest
KeyMobile
KeyUser
KeyConsoleInfo
KeyProtocalType
更多原創文章乾貨分享,請關注公眾號
  • base業務框架開發手冊
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章