【GoLang】golang 微服務框架 介紹

原文以下:html

rpcx是一個相似阿里巴巴 Dubbo 和微博 Motan 的分佈式的RPC服務框架,基於Golang net/rpc實現。git

談起分佈式的RPC框架,比較出名的是阿里巴巴的dubbo,包括由噹噹網維護的dubbox。 
不知道dubbo在阿里的內部競爭中敗給了HSF,仍是阿里有意將其閉源了,官方的代碼使用的spring還停留在2.5.6.SEC03的版本,dubbox的spring也只升級到3.2.9.RELEASE。 
無論怎樣,dubbo仍是在電商企業獲得普遍的應用,京東也有部分在使用dubbo開發。程序員

DUBBO是一個分佈式服務框架,致力於提供高性能和透明化的RPC遠程服務調用方案,是阿里巴巴SOA服務化治理方案的核心框架,天天爲2,000+個服務提供3,000,000,000+次訪問量支持,並被普遍應用於阿里巴巴集團的各成員站點。微博的RPC框架 Motan 也正式開源了,如張雷所說:github

2013 年微博 RPC 框架 Motan 在前輩大師們(福林、fishermen、小麥、王喆等)的精心設計和辛勤工做中誕生,向各位大師們致敬,也獲得了微博各個技術團隊的鼎力支持及不斷完善,現在 Motan 在微博平臺中已經普遍應用,天天爲數百個服務完成近千億次的調用。golang

這兩個個優秀的框架都是使用Java開發的,國外的互聯網企業也有很是出名的的RPC框架如 thrift 、 finagle 。算法

本項目 rpcx 的目標就是實現一個Go生態圈的Dubbo,爲Go生態圈提供一個分佈式的、多插件的、帶有服務治理功能的產品級的RPC框架。spring

Go生態圈已經有一些RPC庫,如官方的 net/rpc 、 grpc-go 、 gorilla-rpc 等,爲何還要開發 rpcx 呢?apache

緣由在於儘管這些框架都是爲Go實現的RPC庫,可是它們的功能比較單一,只是實現了點對點(End-to-End)的通信框架。缺少服務治理的功能,好比服務註冊和發現、負載均衡、容災、服務監控等功能。所以我基於Go net/rpc框架實現了一個相似Dubbo的分佈式框架。編程

和rpcx比較相似的Go RPC框架是 go-micro ,可是rpcx提供了更豐富的功能,基於TCP的通信協議性能更好。json

RPC是什麼

遠程過程調用(英語:Remote Procedure Call,縮寫爲 RPC)是一個計算機通訊協議。該協議容許運行於一臺計算機的程序調用另外一臺計算機的子程序,而程序員無需額外地爲這個交互做用編程。若是涉及的軟件採用面向對象編程,那麼遠程過程調用亦可稱做遠程調用或遠程方法調用,例:Java RMI。簡單地說就是能使應用像調用本地方法同樣的調用遠程的過程或服務。很顯然,這是一種client-server的交互形式,調用者(caller)是client,執行者(executor)是server。典型的實現方式就是request–response通信機制。

RPC 是進程之間的通信方式(inter-process communication, IPC), 不一樣的進程有不一樣的地址空間。 
若是client和server在同一臺機器上,儘管物理地址空間是相同的,可是虛擬地址空間不一樣。 
若是它們在不一樣的主機上,物理地址空間也不一樣。

RPC的實現的技術各不相同,也不必定兼容。

一個正常的RPC過程能夠分紅下面幾步:

  1. client調用client stub,這是一次本地過程調用
  2. client stub將參數打包成一個消息,而後發送這個消息。打包過程也叫作 marshalling
  3. client所在的系統將消息發送給server
  4. server的的系統將收到的包傳給server stub
  5. server stub解包獲得參數。 解包也被稱做 unmarshalling
  6. 最後server stub調用服務過程. 返回結果按照相反的步驟傳給client

RPC只是描繪了 Client 與 Server 之間的點對點調用流程,包括 stub、通訊、RPC 消息解析等部分,在實際應用中,還須要考慮服務的高可用、負載均衡等問題,因此產品級的 RPC 框架除了點對點的 RPC 協議的具體實現外,還應包括服務的發現與註銷、提供服務的多臺 Server 的負載均衡、服務的高可用等更多的功能。目前的 RPC 框架大體有兩種不一樣的側重方向,一種偏重於服務治理,另外一種偏重於跨語言調用。

服務治理型的 RPC 框架有 Dubbo、DubboX、Motan 等,這類的 RPC 框架的特色是功能豐富,提供高性能的遠程調用以及服務發現及治理功能,適用於大型服務的微服務化拆分以及管理,對於特定語言(Java)的項目能夠十分友好的透明化接入。但缺點是語言耦合度較高,跨語言支持難度較大。

跨語言調用型的 RPC 框架有 Thrift、gRPC、Hessian、Hprose 等,這一類的 RPC 框架重點關注於服務的跨語言調用,可以支持大部分的語言進行語言無關的調用,很是適合於爲不一樣語言提供通用遠程服務的場景。但這類框架沒有服務發現相關機制,實際使用時通常須要代理層進行請求轉發和負載均衡策略控制。

本項目 rpcx 屬於服務治理類型,是一個基於 Go 開發的高性能的輕量級 RPC 框架,Motan 提供了實用的服務治理功能和基於插件的擴展能力。

RPCX的特色

rpcx使用Go實現,適合使用Go語言實現RPC的功能。

  • 基於net/rpc,能夠將net/rpc實現的RPC項目輕鬆的轉換爲分佈式的RPC
  • 插件式設計,能夠配置所需的插件,好比服務發現、日誌、統計分析等
  • 基於TCP長鏈接,只需很小的額外的消息頭
  • 支持多種編解碼協議,如Gob、Json、MessagePack、gencode、ProtoBuf等
  • 服務發現:服務發佈、訂閱、通知等,支持多種發現方式如ZooKeeper、Etcd等
  • 高可用策略:失敗重試(Failover)、快速失敗(Failfast)
  • 負載均衡:支持隨機請求、輪詢、低併發優先、一致性 Hash等
  • 規模可擴展,能夠根據性能的需求增減服務器
  • 其餘:調用統計、訪問日誌等

rpcx目標是輕量級的,小而簡單,可是指望全部的功能均可以經過插件的方式搭積木的方式完成。

RPCX架構

rpcx中有服務提供者 RPC Server,服務調用者 RPC Client 和服務註冊中心 Registry 三個角色。

  • Server 向 Registry 註冊服務,並向註冊中心發送心跳彙報狀態(基於不一樣的registry有不一樣的實現)。
  • Client 須要向註冊中心查詢 RPC 服務者列表,Client 根據 Registry 返回的服務者列表,選取其中一個 Sever 進行 RPC 調用。
  • 當 Server 發生宕機時,Registry 會監測到服務者不可用(zookeeper session機制或者手工心跳),Client 感知後會對本地的服務列表做相應調整。client可能被動感知(zookeeper)或者主動定時拉取。
  • 可選地,Server能夠按期向Registry彙報調用統計信息,Client能夠根據調用次數選擇壓力最小的Server

當前rpcx支持zookeeper, etcd等註冊中心,Consul註冊中心正在開發中。

rpcx基於Go net/rpc的底層實現, Client和Server之間通信是經過TCP進行通信的,它們之間經過Client發送Request,Server返回Response實現。 
Request和Response消息的格式都是 Header+Body 的格式。Header和Body具體的格式根據編碼方式的不一樣而不一樣,能夠是二進制,也能夠是結構化數據如JSON。

RPCX的特性

rpcx擁有衆多特性。

服務器特性

編碼 (序列化)

rpcx當前支持多種序列化/反序列化的方式,能夠根據需求選擇合適的編碼庫。

特性 功能描述
gob 官方提供的序列化方式,基於一個包含元數據的流
jsonrpc 也是官方提供的編碼庫,以JSON格式傳輸
msgp 相似json格式的編碼,可是更小更快,能夠直接編碼struct
gencode 一個超級快的序列化庫,須要定義schema,可是定義方式和struct相似
protobuf Google推出的廣受關注的序列化庫,推薦使用 gogo-protobuf ,能夠得到更高的性能

在數據結構簡單的狀況下,這幾種庫均可以知足需求,參照本文中的benchmark測試。可是若是追求性能,建議採用後面三種序列化庫。

序列化庫的選擇對於RPC服務的影響是巨大的,我建立了另一個項目專門比較各序列化庫的性能: gosercomp 。

新的序列化庫的實現也很是簡單,只需實現下面兩個方法便可:

funcNewXXXXXServerCodec(conn io.ReadWriteCloser) rpc.ServerCodec {
 ……
}
funcNewXXXXXClientCodec(conn io.ReadWriteCloser) rpc.ClientCodec {
 ……
}

編碼庫負責marshal/unmarshal Reuqest/Response, 包括消息中的Header和Body。若是你想,你也能夠對Header和Body實現不一樣的編碼。

註冊中心

目前提供了兩種註冊中心:

  • ZooKeeperRegisterPlugin 
    經過ZooKeeper實現服務發現。 
    服務在註冊的時候會自動在ZooKeeper上建立一個Ephemeral節點,所以當服務宕機的時候此節點就被刪除,Client也會感知到。 
    同時,Server也會把調用次數定時更新到ZooKeeper,這樣Client能夠根據一段時間的調用次數選擇壓力較小的服務器節點進行鏈接。

註冊中心的配置只需在服務器初始化的時候增長如下代碼,服務的實現無需作任何的改動,也不須要額外的配置。

plugin := &ZooKeeperRegisterPlugin{
 ServiceAddress: "tcp@127.0.0.1:1234",
 ZooKeeperServers: []string{"127.0.0.1:2181"},
 BasePath: "/betterrpc",
 metrics: metrics.NewRegistry(),
 Services: make([]string,1),
 updateInterval: time.Minute,
}
 server.PluginContainer.Add(plugin)

其中ServiceAddress爲本機(Server)要暴露給Client地址。由於ZooKeeper的節點名不支持"/",因此此處用"@"代替"://"。

ZooKeeperServers爲ZK集羣的地址。

BasePath爲一個服務組,此組下的服務對於Client都是可見的。

  • EtcdRegisterPlugin 
    經過etcd也能夠實現服務發現。

etcd能夠經過TTL判斷服務器的存活,另外此插件也會定時把調用次數定時更新到etcd。

此插件可使用下面的代碼配置:

plugin := &EtcdRegisterPlugin{
 ServiceAddress: "tcp@127.0.0.1:1234",
 EtcdServers: []string{"http://127.0.0.1:2379"},
 BasePath: "/betterrpc",
 metrics: metrics.NewRegistry(),
 Services: make([]string,1),
 updateInterval: time.Minute,
}
 server.PluginContainer.Add(plugin)

注意註冊中心插件必須在配置服務以前設置,不然註冊中心沒法獲取要註冊的服務信息。

擴展點

當前rpcx爲server提供瞭如下擴展點:

  • 服務註冊時
  • Client鏈接時
  • 讀取Request Header的先後
  • 讀取Request Body的先後
  • 返回Response的先後

你能夠根據這些擴展點編寫本身的插件,只需實現相應的接口便可。定義的接口你能夠看godoc的IXXXXXXPlugin的定義。

上面介紹的註冊中心就是經過插件的方式實現。同時rpcx還實現了其它的插件,以下面的介紹。

  • LogRegisterPlugin: 記錄服務註冊日誌
  • MetricsPlugin: 統計服務調用次數和處理時間
  • RateLimitingPlugin: 限流操做,限定服務器的TPS

客戶端特性

負載均衡

負載均衡是經過不一樣的ClientSelector來實現的。

負載均衡器 功能描述
DirectClientSelector 點對點的直連,客戶端直接鏈接一個服務器
MultiClientSelector 多對多的直連,一個客戶端能夠從一組固定的服務器中選擇一個直連,無需註冊中心
ZooKeeperClientSelector 從ZK註冊中心選擇一個服務器鏈接
EtcdClientSelector 從Etcd註冊中心選擇一個服務器鏈接

一個Selector須要實現ClientSelector接口:

typeClientSelectorinterface{
 Select(clientCodecFunc ClientCodecFunc) (*rpc.Client, error)
}

Client的序列化方式必須和服務器的序列化方式保持一致。

容錯

Client提供了兩種容錯方式: Failfast 、 Failover 、 Failtry :

  • Failfast: 若是Client調用失敗,當即返回,不會重試
  • Failover: 若是Client調用失敗,會嘗試從服務列表中選擇另一個服務器調用,直到成功或者到達重試次數
  • Failtry: 若是Client調用失敗,會繼續這個服務器重試,直到成功或者到達重試次數

重選算法

對於多個服務器,重選發送支持:

  • 隨機選擇: 隨機選擇一個服務器並返回,可能和上一次的重複
  • RoundRobin: 按順序選擇一個服務器
  • 一致性哈希 [TODO]:使用 Jump Consistent Hash algorithm
  • CallLeast [TODO]: 根據調用次數選擇壓力最小的服務器

擴展點

Client的擴展點以下:

  • 讀取Response Header的先後
  • 讀取Response Body的先後
  • 寫Request的先後

RPCX例子

點對點

點對點的實現和Go net/rpc的使用基本一致。

Server

packagemain

import"github.com/smallnest/rpcx"

typeArgsstruct{
 A int`msg:"a"`
 B int`msg:"b"`
}

typeReplystruct{
 C int`msg:"c"`
}

typeArithint

func(t *Arith) Mul(args *Args, reply *Reply) error {
 reply.C = args.A * args.B
returnnil
}

func(t *Arith) Error(args *Args, reply *Reply) error {
panic("ERROR")
}

funcmain() {
 server := rpcx.NewServer()
 server.RegisterName("Arith",new(Arith))
 server.Serve("tcp","127.0.0.1:8972")
}

Client

同步方式:

packagemain

import(
"fmt"
"time"

"github.com/smallnest/rpcx"
)

typeArgsstruct{
 A int`msg:"a"`
 B int`msg:"b"`
}

typeReplystruct{
 C int`msg:"c"`
}

funcmain() {
 s := &rpcx.DirectClientSelector{Network: "tcp", Address:"127.0.0.1:8972", Timeout:10* time.Second}
 client := rpcx.NewClient(s)

 args := &Args{7,8}
varreply Reply
 err := client.Call("Arith.Mul", args, &reply)
iferr !=nil{
 fmt.Printf("error for Arith: %d*%d, %v \n", args.A, args.B, err)
 } else{
 fmt.Printf("Arith: %d*%d=%d \n", args.A, args.B, reply.C)
 }

 client.Close()
}

異步方式(經過Channel得到執行結果):

packagemain

import(
"fmt"
"time"

"github.com/smallnest/rpcx"
)

typeArgsstruct{
 A int`msg:"a"`
 B int`msg:"b"`
}

typeReplystruct{
 C int`msg:"c"`
}

funcmain() {
 s := &rpcx.DirectClientSelector{Network: "tcp", Address:"127.0.0.1:8972", Timeout:10* time.Second}
 client := rpcx.NewClient(s)

 args := &Args{7,8}
varreply Reply
 divCall := client.Go("Arith.Mul", args, &reply,nil)
 replyCall := <-divCall.Done // will be equal to divCall
ifreplyCall.Error !=nil{
 fmt.Printf("error for Arith: %d*%d, %v \n", args.A, args.B, replyCall.Error)
 } else{
 fmt.Printf("Arith: %d*%d=%d \n", args.A, args.B, reply.C)
 }

 client.Close()
}

多服務器

Server

這裏例子啓動了兩個服務器,其中一個服務器故意將 7 * 8 計算成 560 ,以便和另一個服務器進行區分,咱們能夠觀察計算結果。

packagemain

import"github.com/smallnest/rpcx"

typeArgsstruct{
 A int`msg:"a"`
 B int`msg:"b"`
}

typeReplystruct{
 C int`msg:"c"`
}

typeArithint

func(t *Arith) Mul(args *Args, reply *Reply) error {
 reply.C = args.A * args.B
returnnil
}

func(t *Arith) Error(args *Args, reply *Reply) error {
panic("ERROR")
}

typeArith2int

func(t *Arith2) Mul(args *Args, reply *Reply) error {
 reply.C = args.A * args.B *10
returnnil
}

func(t *Arith2) Error(args *Args, reply *Reply) error {
panic("ERROR")
}

funcmain() {
 server1 := rpcx.NewServer()
 server1.RegisterName("Arith",new(Arith))
 server1.Start("tcp","127.0.0.1:8972")

 server2 := rpcx.NewServer()
 server2.RegisterName("Arith",new(Arith2))
 server2.Serve("tcp","127.0.0.1:8973")
}

Client

隨機選取服務器的例子:

packagemain

import(
"fmt"
"time"

"github.com/smallnest/rpcx"
"github.com/smallnest/rpcx/clientselector"
)

typeArgsstruct{
 A int`msg:"a"`
 B int`msg:"b"`
}

typeReplystruct{
 C int`msg:"c"`
}

funcmain() {
 server1 := clientselector.ServerPair{Network: "tcp", Address:"127.0.0.1:8972"}
 server2 := clientselector.ServerPair{Network: "tcp", Address:"127.0.0.1:8973"}

 servers := []clientselector.ServerPair{server1, server2}

 s := clientselector.NewMultiClientSelector(servers, rpcx.RandomSelect,10*time.Second)

fori :=0; i <10; i++ {
 callServer(s)
 }
}

funccallServer(s rpcx.ClientSelector) {
 client := rpcx.NewClient(s)

 args := &Args{7,8}
varreply Reply
 err := client.Call("Arith.Mul", args, &reply)
iferr !=nil{
 fmt.Printf("error for Arith: %d*%d, %v \n", args.A, args.B, err)
 } else{
 fmt.Printf("Arith: %d*%d=%d \n", args.A, args.B, reply.C)
 }

 client.Close()
}

RoundRobin選取服務器的例子

packagemain

import(
"fmt"
"time"

"github.com/smallnest/rpcx"
"github.com/smallnest/rpcx/clientselector"
)

typeArgsstruct{
 A int`msg:"a"`
 B int`msg:"b"`
}

typeReplystruct{
 C int`msg:"c"`
}

funcmain() {
 server1 := clientselector.ServerPair{Network: "tcp", Address:"127.0.0.1:8972"}
 server2 := clientselector.ServerPair{Network: "tcp", Address:"127.0.0.1:8973"}

 servers := []clientselector.ServerPair{server1, server2}

 s := clientselector.NewMultiClientSelector(servers, rpcx.RoundRobin,10*time.Second)

fori :=0; i <10; i++ {
 callServer(s)
 }
}

funccallServer(s rpcx.ClientSelector) {
 client := rpcx.NewClient(s)
 args := &Args{7,8}
varreply Reply
 err := client.Call("Arith.Mul", args, &reply)
iferr !=nil{
 fmt.Printf("error for Arith: %d*%d, %v \n", args.A, args.B, err)
 } else{
 fmt.Printf("Arith: %d*%d=%d \n", args.A, args.B, reply.C)
 }

 client.Close()
}

Failover

packagemain

import(
"fmt"
"time"

"github.com/smallnest/rpcx"
"github.com/smallnest/rpcx/clientselector"
)

typeArgsstruct{
 A int`msg:"a"`
 B int`msg:"b"`
}

typeReplystruct{
 C int`msg:"c"`
}

funcmain() {
 server1 := clientselector.ServerPair{Network: "tcp", Address:"127.0.0.1:8972"}
 server2 := clientselector.ServerPair{Network: "tcp", Address:"127.0.0.1:8973"}
 server3 := clientselector.ServerPair{Network: "tcp", Address:"127.0.0.1:8974"}

 servers := []clientselector.ServerPair{server1, server2, server3}

 s := clientselector.NewMultiClientSelector(servers, rpcx.RoundRobin,10*time.Second)

fori :=0; i <10; i++ {
 callServer(s)
 }
}

funccallServer(s rpcx.ClientSelector) {
 client := rpcx.NewClient(s)
 client.FailMode = rpcx.Failover
 args := &Args{7,8}
varreply Reply
 err := client.Call("Arith.Mul", args, &reply)
iferr !=nil{
 fmt.Printf("error for Arith: %d*%d, %v \n", args.A, args.B, err)
 } else{
 fmt.Printf("Arith: %d*%d=%d \n", args.A, args.B, reply.C)
 }

 client.Close()
}

Benchmark

rpcx基於Go net/rpc框架實現,它的插件機制並不會帶來多少性能的損失,以下面的測試,rpcx性能和官方的Go net/rpc持平。

[root@localhostrpcx]# go test -bench . -test.benchmem
PASS
BenchmarkNetRPC_gob-1610000018742ns/op321B/op9allocs/op
BenchmarkNetRPC_jsonrpc-1610000021360ns/op1170B/op31allocs/op
BenchmarkNetRPC_msgp-1610000018617ns/op776B/op35allocs/op
BenchmarkRPCX_gob-1610000018718ns/op320B/op9allocs/op
BenchmarkRPCX_json-1610000021238ns/op1170B/op31allocs/op
BenchmarkRPCX_msgp-1610000018635ns/op776B/op35allocs/op
BenchmarkRPCX_gencodec-1610000018454ns/op4485B/op17allocs/op
BenchmarkRPCX_protobuf-1610000017234ns/op733B/op13allocs/op

參考文檔

  1. 誰能用通俗的語言解釋一下什麼是RPC框架?
  2. DUBBO
  3. 支撐微博千億調用的輕量級RPC框架:Motan
  4. 你應該知道的 RPC 原理
  5. Twitter的RPC框架Finagle簡介
  6. armeria: Netty的做者正在開發的一個RPC庫
  7. wikipedia RPC

 

 

參考資料:

RPCX:  http://www.tuicool.com/m/articles/vYB3euv

go-micro: https://github.com/micro/go-micro

https://blog.micro.mu/2016/03/28/go-micro.html

相關文章
相關標籤/搜索