TarsGo
- Tarsgo是基于Golang编程语言使用Tars协议的高性能RPC框架。随着docker,k8s,etcd等容器化技术的兴起,Go语言变得流行起来。Go的goroutine并发机制使Go非常适合用于大规模高并发后端服务程序的开发。 Go语言具有接近C/C++的性能和接近python的生产力。在腾讯,一部分现有的C++开发人员正逐渐向Go转型,Tars作为广泛使用的RPC框架,现已支持C++/Java/Nodejs/Php,其与Go语言的结合已成为大势所趋。因此,在广大用户的呼声中我们推出了Tarsgo,并且已经将它应用于腾讯地图、应用宝、互联网+以及其他项目中。
- 关于tars的整体架构和设计理念,请阅读 Tars介绍
功能特性
- Tars2go工具: tars文件自动生成并转换为go语言,包含用go语言实现的RPC服务端/客户端代码
- go语言版本的tars的序列化和反序列化包
- 服务端支持心跳上报,统计监控上报,自定义命令处理,基础日志
- 客户端支持直接连接和路由访问,自动重新连接,定期刷新节点状态以及支持UDP/TCP协议
- 提供远程日志
- 提供特性监控上报
- 提供set分组
- 提供 protocol buffers 支持, 详见 pb2tarsgo
安装
- 对于安装OSS和其他基本服务, 请安装文档, 快速安装,请查看
- 要求Go 1.9.x 或以上版本,请查看https://golang.org/doc/install
- go get -u github.com/TarsCloud/TarsGo/tars
快速开始
- 快速开始,请查看快速入门。
性能数据
- 查看性能数据
使用
- 下面是一个完整的示例,用于说明如何使用tarsgo去构建服务端。
接口定义
编译接口定义文件
构建 tars2go
编译并安装tars2go工具
编译tars文件并转成go文
tars2go --outdir=./vendor hello.tars
接口实现
package main
import (
"github.com/TarsCloud/TarsGo/tars"
"TestApp"
)
type HelloImp struct {
}
//implete the Test interface
func (imp *HelloImp) Test() (int32, error) {
return 0, nil
}
//implete the testHello interface
func (imp *HelloImp) TestHello(in string, out *string) (int32, error) {
*out = in
return 0, nil
}
func main() { //Init servant
imp := new(HelloImp) //New Imp
app := new(TestApp.Hello) //New init the A Tars
cfg := tars.GetServerConfig() //Get Config File Object
app.AddServant(imp, cfg.App+"."+cfg.Server+".HelloObj") //Register Servant
tars.Run()
}
说明:
- HelloImp是结构体,你在里面实现Hello和Test接口, 注意Test和Hello必须以大写字母开头才能被导出,这是唯一与tars文件定义有所不同的地方。
- TestApp.Hello是由tar2go工具生成的,它可以在./vendor/TestApp/Hello_IF.go中找到,其中包含一个名为TestApp的软件包,它与tars文件的TestApp模块一样。
- tars.GetServerConfig()用于获得服务端配置。
- cfg.App+”.”+cfg.Server+”.HelloObj” 是绑定到Servant的对象名,客户端将使用此名称访问服务端.
tars.GetServerConfig()返回服务端配置,其定义如下:
type serverConfig struct {
Node string
App string
Server string
LogPath string
LogSize string
LogLevel string
Version string
LocalIP string
BasePath string
DataPath string
config string
notify string
log string
netThread int
Adapters map[string]adapterConfig
Container string
Isdocker bool
Enableset bool
Setdivision string
}
- Node: 本地tarsnode地址,只有你使用tars平台部署才会使用这个参数.
- APP: 应用名.
- Server: 服务名.
- LogPath: 保存日志的目录.
- LogSize: 轮换日志的大小.
- LogLevel: 轮换日志的级别.
- Version: Tarsg的版本.
- LocalIP: 本地ip地址.
- BasePath: 二进制文件的基本路径.
- DataPath: 一些缓存文件存储路径.
- config: 获取配置的配置中心,如tars.tarsconfig.ConfigObj
- notify: 上报通知报告的通知中心,如tars.tarsnotify.NotifyObj
- log: 远程日志中心,如tars.tarslog.LogObj
- netThread: 保留用于控制接收和发送包的go线程.
- Adapters: 每个adapter适配器的指定配置.
- Contianer: 保留供以后使用,用于存储容器名称.
- Isdocker: 保留供以后使用,用于指定服务是否在容器内运行.
- Enableset: 如果使用了set,则为True.
- Setdivision: 指定哪个set,如gray.sz.*
如下是一个服务端配置的例子:
<tars>
<application>
enableset=Y
setdivision=gray.sz.*
<server>
node=tars.tarsnode.ServerObj@tcp -h 10.120.129.226 -p 19386 -t 60000
app=TestApp
server=HelloServer
localip=10.120.129.226
local=tcp -h 127.0.0.1 -p 20001 -t 3000
basepath=/usr/local/app/tars/tarsnode/data/TestApp.HelloServer/bin/
datapath=/usr/local/app/tars/tarsnode/data/TestApp.HelloServer/data/
logpath=/usr/local/app/tars/app_log/
logsize=10M
config=tars.tarsconfig.ConfigObj
notify=tars.tarsnotify.NotifyObj
log=tars.tarslog.LogObj
#timeout for deactiving , ms.
deactivating-timeout=2000
logLevel=DEBUG
</server>
</application>
</tars>
适配器
适配器为每个对象绑定ip和端口.在服务端代码实现的例子中, app.AddServant(imp, cfg.App+”.”+cfg.Server+”.HelloObj”)完成HelloObj的适配器配置和实现的绑定。适配器的完整例子如下:
<tars>
<application>
<server>
#each adapter configuration
<TestApp.HelloServer.HelloObjAdapter>
allow
# ip and port to listen on
endpoint=tcp -h 10.120.129.226 -p 20001 -t 60000
#handlegroup
handlegroup=TestApp.HelloServer.HelloObjAdapter
#max connection
maxconns=200000
#portocol, only tars for now.
protocol=tars
#max capbility in handle queue.
queuecap=10000
#timeout in ms for the request in the queue.
queuetimeout=60000
#servant
servant=TestApp.HelloServer.HelloObj
#threads in handle server side implement code. goroutine for golang.
threads=5
</TestApp.HelloServer.HelloObjAdapter>
</server>
</application>
</tars>
服务端启动
如下命令用于启动服务端:
./HelloServer --config=config.conf
请参阅下面的config.conf的完整示例,稍后我们将解释客户端配置。
<tars>
<application>
enableset=n
setdivision=NULL
<server>
node=tars.tarsnode.ServerObj@tcp -h 10.120.129.226 -p 19386 -t 60000
app=TestApp
server=HelloServer
localip=10.120.129.226
local=tcp -h 127.0.0.1 -p 20001 -t 3000
basepath=/usr/local/app/tars/tarsnode/data/TestApp.HelloServer/bin/
datapath=/usr/local/app/tars/tarsnode/data/TestApp.HelloServer/data/
logpath=/usr/local/app/tars/app_log/
logsize=10M
config=tars.tarsconfig.ConfigObj
notify=tars.tarsnotify.NotifyObj
log=tars.tarslog.LogObj
deactivating-timeout=2000
logLevel=DEBUG
allow
endpoint=tcp -h 10.120.129.226 -p 20001 -t 60000
handlegroup=TestApp.HelloServer.HelloObjAdapter
maxconns=200000
protocol=tars
queuecap=10000
queuetimeout=60000
servant=TestApp.HelloServer.HelloObj
threads=5
</TestApp.HelloServer.HelloObjAdapter>
</server>
<client>
locator=tars.tarsregistry.QueryObj@tcp -h 10.120.129.226 -p 17890
sync-invoke-timeout=3000
async-invoke-timeout=5000
refresh-endpoint-interval=60000
report-interval=60000
sample-rate=100000
max-sample-count=50
asyncthread=3
modulename=TestApp.HelloServer
</client>
</application>
</tars>
用户可以轻松编写客户端代码,而无需编写任何指定协议的通信代码.
请参阅下面的一个客户端例子:
package main
import (
"fmt"
"github.com/TarsCloud/TarsGo/tars"
"TestApp"
)
//tars.Communicator should only init once and be global
var comm *tars.Communicator
func main() {
comm = tars.NewCommunicator()
obj := "TestApp.TestServer.HelloObj@tcp -h 127.0.0.1 -p 10015 -t 60000"
app := new(TestApp.Hello)
comm.StringToProxy(obj, app)
var req string="Hello Wold"
var res string
ret, err := app.TestHello(req, &out)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret, out)
说明:
- TestApp包是由tars2go工具使用tars协议文件生成的.
- comm: Communicator用于与服务端进行通信,它应该只初始化一次并且是全局的.
- obj: 对象名称,用于指定服务端的ip和端口。通常在”@”符号之前我们只需要对象名称.
- app: 与tars文件中的接口关联的应用程序。 在本例中它是TestApp.Hello.
- StringToProxy: StringToProxy方法用于绑定对象名称和应用程序,如果不这样做,通信器将不知道谁与应用程序通信 .
- req, res: 在tars文件中定义的输入和输出参数,用于在TestHello方法中.
- app.TestHello用于调用tars文件中定义的方法,并返回ret和err.
通信器
通信器是为客户端发送和接收包的一组资源,其最终管理每个对象的socket通信。在一个程序中你只需要一个通信器。
var comm *tars.Communicato
comm = tars.NewCommunicator()
comm.SetProperty("property", "tars.tarsproperty.PropertyObj")
comm.SetProperty("locator", "tars.tarsregistry.QueryObj@tcp -h ... -p ...")
描述:
通信器属性描述:
通信器配置文件的格式如下:
超时控制
如果你想在客户端使用超时控制,请使用以ms为单位的TarsSetTimeout。
app := new(TestApp.Hello)
comm.StringToProxy(obj, app)
app.TarsSetTimeout(3000)
本节详细介绍了Tars客户端如何远程调用服务端。
首先,简要描述Tars客户端的寻址模式。 其次,它将介绍客户端的调用方法,包括但不限于单向调用,同步调用,异步调用,hash调用等。
寻址模式简介
Tars服务的寻址模式通常可以分为两种方式:服务名称在master上注册了,服务名称未在master上注册。 master是专用于注册服务节点信息的名字服务(路由服务)。
把服务名添加到名字服务中是通过操作管理平台实现。
对于未在master中注册的服务,可以将其分类为直接寻址,即在调用服务之前需要指定服务提供者的IP地址。 客户端需要在调用服务时指定HelloObj对象的特定地址,即Test.HelloServer.HelloObj@tcp -h 127.0.0.1 -p 9985
Test.HelloServer.HelloObj: 对象名
-h:指定主机地址,这里是127.0.0.1
-p:端口,这里是9985
如果HelloServer在两台服务器上运行,则应用程序初始化如下:
obj:= "Test.HelloServer.HelloObj@tcp -h 127.0.0.1 -p 9985:tcp -h 192.168.1.1 -p 9983"
app := new(TestApp.Hello)
comm.StringToProxy(obj, app)
HelloObj的地址设置为两个服务器的地址。 此时,请求将被分发到两个服务器(可以指定分发方法,这里不再介绍)。 如果一台服务器关闭,请求将自动分配给另一台服务器,服务器将定期重新启动。
对于在master中注册的服务,将根据服务名称对服务进行寻址。 当客户端请求服务时,它不需要指定HelloServer的特定地址,但是在生成通信器或初始化通信器时需要指定registry
的地址。
以下通过设置通信器的参数显示主控的地址:
var *tars.Communicator
comm = tars.NewCommunicator()
comm.SetProperty("locator", "tars.tarsregistry.QueryObj@tcp -h ... -p ...")
由于客户端需要依赖主控的地址,因此主控还必须具有容错能力。 主控的容错方法与上面相同,即指定了两个主控的地址。
单向调用
TODO. tarsgo暂未支持.
同步调用
package main
import (
"fmt"
"github.com/TarsCloud/TarsGo/tars"
"TestApp"
)
var *tars.Communicator
func main() {
comm = tars.NewCommunicator()
obj := "TestApp.TestServer.HelloObj@tcp -h 127.0.0.1 -p 10015 -t 60000"
app := new(TestApp.Hello)
comm.StringToProxy(obj, app)
var req string="Hello Wold"
var res string
ret, err := app.TestHello(req, &out)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret, out)
异步调用
tarsgo可以使用goroutine轻松使用异步调用。 与cpp不同,我们不需要实现回调函数。
package main
import (
"fmt"
"github.com/TarsCloud/TarsGo/tars"
"time"
"TestApp"
)
var *tars.Communicator
func main() {
comm = tars.NewCommunicator()
obj := "TestApp.TestServer.HelloObj@tcp -h 127.0.0.1 -p 10015 -t 60000"
app := new(TestApp.Hello)
comm.StringToProxy(obj, app)
go func(){
var res string
ret, err := app.TestHello(req, &out)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(ret, out)
}()
time.Sleep(1)
通过set调用
客户端可以通过set来调用服务端,只需要配置上文提到的配置文件,其中enableset置为y,setdivision比如设置为gray.sz. *。 有关更多详细信息,请参阅https://github.com/TarsCloud/Tars/blob/master/docs-en/tars_idc_set.md。 如果您想手动通过set调用,tarsgo将很快支持此功能。
Hash调用
由于可以部署多个服务端,因此客户端的请求会随机分发到服务端上,但在某些情况下,希望始终将某些请求发送到特定的服务端。 在这种情况下,Tars提供了一种简单的实现方法,称为hash调用。 Tarsgo很快将支持此功能。
tars定义的返回码
//Define the return code given by the TARS service
const int TARSSERVERSUCCESS = 0; //Server-side processing succeeded
const int TARSSERVERDECODEERR = -1; //Server-side decoding exception
const int TARSSERVERENCODEERR = -2; //Server-side encoding exception
const int TARSSERVERNOFUNCERR = -3; //There is no such function on the server side
const int TARSSERVERNOSERVANTERR = -4; //The server does not have the Servant object
const int TARSSERVERRESETGRID = -5; // server grayscale state is inconsistent
const int TARSSERVERQUEUETIMEOUT = -6; //server queue exceeds limit
const int TARSASYNCCALLTIMEOUT = -7; // Asynchronous call timeout
const int TARSINVOKETIMEOUT = -7; //call timeout
const int TARSPROXYCONNECTERR = -8; //proxy link exception
const int TARSSERVEROVERLOAD = -9; //Server overload, exceeding queue length
const int TARSADAPTERNULL = -10; //The client routing is empty, the service does not exist or all services are down.
const int TARSINVOKEBYINVALIDESET = -11; //The client calls the set rule illegally
const int TARSCLIENTDECODEERR = -12; //Client decoding exception
const int TARSSERVERUNKNOWNERR = -99; //The server is in an abnormal position
日志
使用tarsgo轮换日志的快速示例:
TLOG.Debug("Debug logging")
这将创建一个在tars/util/rogger中定义的*Rogger.Logger,并在调用GetLogger之后,会在config.conf中定义的Logpath下创建一个日志文件,其名称为cfg.App + “.” + cfg.Server + “_“ +名称,该日志文件将在100MB(默认)后轮换,最大轮换文件数为10(默认)。
如果你不想按文件大小轮换日志。 例如,你想要按天轮换,使用:
TLOG := tars.GetDayLogger("TLOG",1)
TLOG.Debug("Debug logging")
使用GetHourLogger(“TLOG”,1)按小时轮换日志。如果你想打日志到config.conf中定义的名为tars.tarslog.LogObj的远程服务器上,你不得不先配置一个日志服务器。可以在tars/protocol/res/LogF.tars中找到完整的tars文件定义,可以在Tencent/Tars/cpp/framework/LogServer中查找日志服务器。快速示例如下:
TLOG := GetRemoteLogger("TLOG")
TLOG.Debug("Debug logging")
如果你想设置日志等级,你可以在Tencent/Tars/web下的tars项目提供的OSS平台上设置它。 如果你想自定义你的日志,请在tars/util/logger,tars/logger.go 和tars/remotelogger.go中的查看更多细节。
服务管理
Tars服务框架支持动态接收命令来处理相关的业务逻辑,例如动态更新配置
tarsgo目前有tars.viewversion / tars.setloglevel管理命令。 用户可以从oss发送管理命令来查看版本或设置日志等级。
如果你想定义你自己的管理命令,请看下面的例子:
func helloAdmin(who string ) (string, error) {
return who, nil
}
tars.RegisterAdmin("tars.helloAdmin", helloAdmin)
然后你可以发送自定义的管理命令“tars.helloAdmin tarsgo”,tarsgo将在浏览器中显示。
举例:
统计上报
客户端调用上报接口后,会暂时将信息存储在内存中,当到达某个时间点时,会向tarsstat服务上报(默认为1分钟上报一次)。 我们将两个上报时间点之间的时间间隔称为统计间隔,在统计间隔中会执行诸如聚合和比较相同key的一些操作。 示例代码如下:
//for error
ReportStat(msg, 0, 1, 0)
//for success
ReportStat(msg, 1, 0, 0)
//func ReportStat(msg *Message, succ int32, timeout int32, exec int32)
//see more detail in tars/statf.go
描述:
异常上报
为了更好地监控,TARS框架支持直接向程序中的tarsnotify上报异常情况,并可在WEB管理页面上查看。
该框架提供了三个宏来上报不同类型的异常:
tars.reportNotifyInfo("Get data from mysql error!")
Info是一个字符串,可以直接将字符串上报给tarsnotify。 上报的字符串可以在页面上看到,随后,我们可以根据上报的信息进行报警。
为了便于业务统计,TARS框架还支持在Web管理平台上显示信息。
目前支持的统计类型包括:
示例代码如下:
sum := tars.NewSum()
count := tars.NewCount()
max := tars.NewMax()
min := tars.NewMin()
d := []int{10, 20, 30, 50}
distr := tars.NewDistr(d)
p := tars.CreatePropertyReport("testproperty", sum, count, max, min, distr)
for i := 0; i < 5; i++ {
v := rand.Intn(100)
p.Report(v)
}
描述:
远程配置
用户可以从OSS设置远程配置。详情请查看https://github.com/TarsCloud/TarsFramework/blob/master/docs-en/tars_config.md . 如下示例用于说明如何使用此api从远程获取配置文件。
import "github.com/TarsCloud/TarsGo/tars"
...
cfg := tars.GetServerConfig()
remoteConf := tars.NewRConf(cfg.App, cfg.Server, cfg.BasePath)
config, _ := remoteConf.GetConfig("test.conf")
...
setting.go
tars包中的setting.go用于控制tarsgo性能和特性。有些选项应该从Getserverconfig()中更新。
//number of woker routine to handle client request
//zero means no contorl ,just one goroutine for a client request.
//runtime.NumCpu() usually best performance in the benchmark.
var MaxInvoke int = 0
const (
//for now ,some option shuold update from remote config
//version
TarsVsersion string = "1.0.0"
//server
AcceptTimeout time.Duration = 500 * time.Millisecond
//zero for not set read deadline for Conn (better performance)
ReadTimeout time.Duration = 0 * time.Millisecond
//zero for not set write deadline for Conn (better performance)
WriteTimeout time.Duration = 0 * time.Millisecond
//zero for not set deadline for invoke user interface (better performance)
HandleTimeout time.Duration = 0 * time.Millisecond
IdleTimeout time.Duration = 600000 * time.Millisecond
ZombileTimeout time.Duration = time.Second * 10
QueueCap int = 10000000
//client
ClientQueueLen int = 10000
ClientIdleTimeout time.Duration = time.Second * 600
ClientReadTimeout time.Duration = time.Millisecond * 100
ClientWriteTimeout time.Duration = time.Millisecond * 3000
ReqDefaultTimeout int32 = 3000
ObjQueueMax int32 = 10000
//report
PropertyReportInterval time.Duration = 10 * time.Second
StatReportInterval time.Duration = 10 * time.Second
//mainloop
MainLoopTicker time.Duration = 10 * time.Second
//adapter
AdapterProxyTicker time.Duration = 10 * time.Second
AdapterProxyResetCount int = 5
//communicator default ,update from remote config
refreshEndpointInterval int = 60000
reportInterval int = 10000
AsyncInvokeTimeout int = 3000
//tcp network config
TCPReadBuffer = 128 * 1024 * 1024
TCPWriteBuffer = 128 * 1024 * 1024
TCPNoDelay = false
)
HTTP支持
目前的tar.TarsHttpMux和golang内置http.ServeMux使用方式是一致的,其中pattern参数做为监控数据的接口名,后续会参考github.com/gorilla/mux
实现功能更强大的路由功能。
具体实现可参考下面的例子:
package main
import (
"net/http"
"github.com/TarsCloud/TarsGo/tars"
)
func main() {
mux := &tars.TarsHttpMux{}
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello tafgo"))
})
cfg := tars.GetServerConfig()
tars.AddHttpServant(mux, cfg.App+"."+cfg.Server+".HttpObj") //Register http server
tars.Run()
}
Context 支持
TarsGo 之前在生成的客户端代码,或者用户传入的实现代码里面,都没有使用context。 这使得我们想传递一些框架的信息,比如客户端ip,端口等,或者用户传递一些调用链的信息给框架,都很难于实现。 通过接口的一次重构,支持了context,这些上下文的信息,将都通过context来实现。 这次重构为了兼容老的用户行为,采用了完全兼容的设计。
服务端使用context
type ContextTestImp struct {
}
//只需在接口上添加 ctx context.Context参数
func (imp *ContextTestImp) Add(ctx context.Context, a int32, b int32, c *int32) (int32, error) {
//我们可以通过context 获取框架传递的信息,比如下面的获取ip, 甚至返回一些信息给框架,详见tars/util/current下面的接口
ip, ok := current.GetClientIPFromContext(ctx)
if !ok {
logger.Error("Error getting ip from context")
}
return 0, nil
}
//以前使用AddServant ,现在只需改成AddServantWithContext
客户端使用context
ctx := context.Background()
c := make(map[string]string)
c["a"] = "b"
//以前使用app.Add 进行客户端调用,这里只要变成app.AddWithContext ,就可以传递context给框架,如果要设置给tars请求的context
//可以多传入参数,比如c,参数c是可选的,格式是 ...[string]string
ret, err := app.AddWithContext(ctx, i, i*2, &out, c)
服务端和客户端的完整例子,详见 TarGo/examples
filter机制(插件) 和 zipkin opentracing
为了支持用户编写插件,我们支持了filter机制,分为服务端的过滤器和客户端过滤器
//服务端过滤器, 传入dispatch,和f, 用于调用用户代码, req, 和resp为传入的用户请求和服务端相应包体
type ServerFilter func(ctx context.Context, d Dispatch, f interface{}, req *requestf.RequestPacket, resp *requestf.ResponsePacket, withContext bool) (err error)
//客户端过滤器, 传入msg(包含obj信息,adapter信息,req和resp包体), 还有用户设定的调用超时
type ClientFilter func(ctx context.Context, msg *Message, invoke Invoke, timeout time.Duration) (err error)
//注册服务端过滤器
//func RegisterServerFilter(f ServerFilter)
//注册客户端过滤器
//func RegisterClientFilter(f ClientFilter)
有了过滤器,我们就能对服务端和客户端的请求做一些过滤,比如使用 hook用于分布式追踪的opentracing 的span。 我们来看下客户端filter的例子:
服务端也会注册一个filter,主要功能就是从request包体的status 提取调用链的上下文,以这个作为父span,进行调用信息的记录。
详细代码参见 TarsGo/tars/plugin/zipkintracing 完整的zipkin tracing的客户端和服务端例子,详见 TarsGo/examples下面的ZipkinTraceClient和ZipkinTraceServer