围绕grpc,打造工具库和代码生成工具

配置加载

app采用ini格式的配置,也考虑过使用yaml,但yaml的灵活度较高,代码生成比较难搞。将app的配置分为两种:

  • 值配置:app中使用该值,支持string/int/int_array/string_array
  • 对象配置:framework中加载该值,但不直接使用,支持redis/mysql/es,代码生成逻辑回识别这些section并生成对应的对象,对象的具体使用方法根据选择的sdk有所不同,这里不详细描述。

在迁移go之前,我做过几个简单的项目,类似http网关、http服务、对接kafka的消费者,采用的yaml配置,每次新增项目,就需要将旧项目的配置加载部分抄过来,需要根据配置的构建对应的struct,需要copy yaml的依赖包到新项目,很乏味,而且费事。所以搞了配置代码生成功能。

下面是代码生成工具的结果,注意Config中的*.Val*.Obj代表的是上述的两种配置配置。

package app

type autoBase struct {
	Port      int64
	AdminPort int64
}
type autoMysql struct {
	Addr         string
	Dbname       string
	User         string
	Passwd       string
	Timeout      int64
	ReadTimeout  int64
	WriteTimeout int64
}
type Config struct {
	BaseVal  autoBase
	MysqlVal autoMysql
	MysqlObj *DpMysql
}

工具库

工具库主要包含对redis/mysql/es的封装,以上服务是app主要使用的几种存储,github上已经存在比较成熟的sdk,直接拿过来用即可,再次封装的目的有两个:

  • 抽离出通用服务的初始化,提升app的开发效率,新app只需要简单的在ini配置中设置下,就可以在代码中使用对应通用服务的client对象。
  • 允许app依赖同一种通用服务的多个endpoint,配置文件中使用类似redis_xxx这种sectionframework会自动生成和配置对应的client对象,app中可以使用app.RedisXxx这种方式直接使用。

依赖上述配置加载,识别出通用服务的section并调用提前写好的特定通用服务初始化工具方法和对象,生成初始化client对象的语句,并在app启动时进行加载。

package app

import (
	"time"

	"github.com/gomodule/redigo/redis"
)

// 包装redis.Pool不让上层用户直接使用第三方库中的redis,
// 因为会造成用户code中也import上面的github路径,这样就
// 不能控制app的使用方式。
type DpRedisPool struct {
	redis.Pool
}

// redis库内部存在Options,这里根据最佳实践,筛选出需要
// app关注的,二次封装,虽然屏蔽细节,但经过打磨后应该会
// 节省app开发时间。
type redisOptions struct {
	addr        string
	maxIdle     int64
	idleTimeout time.Duration

	// 下面3个选项,在初始化时时放在Dial方法中用的,这里app不需要关注
	dialConnTimeout  time.Duration
	dialReadTimeout  time.Duration
	dialWriteTimeout time.Duration
}

var defaultRedisOptions = redisOptions{
	maxIdle:     50,
	idleTimeout: 240 * time.Second,

	dialConnTimeout:  300 * time.Millisecond,
	dialReadTimeout:  1 * time.Second,
	dialWriteTimeout: 1 * time.Second,
}

type redisOptionsFunc func(*redisOptions)

func redisAddr(s string) redisOptionsFunc {
	return func(o *redisOptions) {
		o.addr = s
	}
}

func redisMaxIdle(s int64) redisOptionsFunc {
	return func(o *redisOptions) {
		o.maxIdle = s
	}
}

func redisIdleTimeout(s int64) redisOptionsFunc {
	return func(o *redisOptions) {
		o.idleTimeout = time.Duration(s) * time.Millisecond
	}
}

func redisDialConnTimeout(s int64) redisOptionsFunc {
	return func(o *redisOptions) {
		o.dialConnTimeout = time.Duration(s) * time.Millisecond
	}
}

func redisDialReadTimeout(s int64) redisOptionsFunc {
	return func(o *redisOptions) {
		o.dialReadTimeout = time.Duration(s) * time.Millisecond
	}
}

func redisDialWriteTimeout(s int64) redisOptionsFunc {
	return func(o *redisOptions) {
		o.dialWriteTimeout = time.Duration(s) * time.Millisecond
	}
}

func initRedis(opt ...redisOptionsFunc) (*DpRedisPool, error) {
	opts := defaultRedisOptions
	for _, o := range opt {
		o(&opts)
	}

	f := func() (redis.Conn, error) {
		return redis.Dial(
			"tcp",
			opts.addr,
			redis.DialConnectTimeout(opts.dialConnTimeout),
			redis.DialReadTimeout(opts.dialReadTimeout),
			redis.DialWriteTimeout(opts.dialWriteTimeout))
	}

	return &DpRedisPool{redis.Pool{
		MaxIdle:     int(opts.maxIdle),
		IdleTimeout: opts.idleTimeout,
		Dial:        f}}, nil
}

逻辑本身比较简单,选择的是redigo这个库,主要是对这个库的一些配置项和初始化方法进行封装。不用app做重复的事情了。

interceptor

go-grpc-middleware

soa服务的核心使用grpc,该工具库也是围绕grpc打造,在请求预处理这块,之前以为需要修改protoc-gen-go,在生成代码时对Service类进行二次封装,幸好grpc支持拦截器,这样就需要那个写一些比较难维护的代码生成逻辑。

初版,我仅增加两个拦截器:

  • 统计接口耗时和qps的统计interceptor
  • 从请求Header中读取Log-Id,没有就生成Log-Id的interceptor
package log

import (
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

func createLogId() string {
	t := time.Now().UnixNano()
	r := rand.Intn(10000)
	return fmt.Sprintf("%d%d", t, r)
}

func UnaryServerInterceptor(authFunc AuthFunc) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		var (
			newCtx context.Context
			logId  string
		)

		md, ok := metadata.FromIncomingContext(ctx)
		if ok {
			v, ok := md["Log-Id"]
			if ok {
				logId = v[0]
			}
		}

		if logId == "" {
			logId = createLogId()
		}

		newCtx = context.WithValue(ctx, "Log-Id", logId)

		return handler(newCtx, req)
	}
}

func StreamServerInterceptor(authFunc AuthFunc) grpc.StreamServerInterceptor {
	return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
		var (
			newCtx context.Context
			logId  string
		)

		md, ok := metadata.FromIncomingContext(ctx)
		if ok {
			v, ok := md["Log-Id"]
			if ok {
				logId = v[0]
			}
		}

		if logId == "" {
			logId = createLogId()
		}

		newCtx = context.WithValue(stream.Context(), "Log-Id", logId)

		wrapped := grpc_middleware.WrapServerStream(stream)
		wrapped.WrappedContext = newCtx
		return handler(srv, wrapped)
	}
}

在main.go中启动服务时,将上述方法注册到Server中即可。

依赖封装

框架依赖的代码库,已经拉下来并放到vendor文件夹中,不需要app自己管理这部分代码。

handler和main.go生成

如果只使用grpc,参考gprc,利用protoc生成x.pb.go后,需要app自己实现proto文件中声明的service,我改造了protoc-gen-go,在生成pb文件的同时,会根据proto中service的定义,生成handler文件,用来给app做参考。

main.go这个文件是逻辑固定,下面给出demo:

package main

import (
	"fmt"
	"net"
	"os"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	"helloworld/framework/app"
	"helloworld/framework/util"
	"helloworld/handler"
	pb "helloworld/helloworld"
)

func main() {
	app.LoadConfig()

	ctx := util.NewContextWithLogID(context.Background())

	port := fmt.Sprintf(":%d", app.ConfigVal.BaseVal.Port)
	lis, err := net.Listen("tcp", port)
	if err != nil {
		util.Errorf(ctx, fmt.Sprintf("failed to listen: %v", err))
		os.Exit(3)
	}
	s := grpc.NewServer()

	pb.RegisterGreeterServer(s, &handler.GreeterHandler{})

	reflection.Register(s)
	if err := s.Serve(lis); err != nil {
		util.Errorf(ctx, fmt.Sprintf("failed to serve: %v", err))
		os.Exit(3)
	}
}

我这边和grpc官方例子的不同是,增加了配置加载以及替换掉之前的log库。

build.sh

该文件是项目初始化的发起点,主要工作就是发起代码生成,为app构建开发环境。设计思路直接看代码

#!/bin/bash

# caution!!!!! 项目名称==proto文件名称,app自己定义
project_name="helloworld"
path=`pwd`
gopath=$GOPATH/src
deadpool_path=$gopath/deadpool

cd $gopath
echo "Updating deadpool..."
# deadpool
if [ ! -d $deadpool_path ]
then
	# deadpool封装工具库和代码生成工具
	git clone http://gitlab.sftcwl.com/logistics/deadpool.git $deadpool_path
else
	cd $deadpool_path
	git pull
fi

echo "Generating config obj and init func..."
# 生成配置对象,以及配置加载代码
cd $deadpool_path
go run main.go

cp -r $deadpool_path/framework $path/
cp -r $deadpool_path/vendor $path/
cp -r $deadpool_path/deploy $path/
cp -r $deadpool_path/conf $path/
cp -r $deadpool_path/.gitignore $path/

cd $path

# proto文件放到和项目同名的子目录中
proto_path=$project_name.proto
if [ ! -d $project_name ]
then
	mkdir $project_name
	mv $proto_path $project_name
else
	proto_path=$project_name/$proto_path
fi

echo "Generating main.go and service handler..."
# 生成handler文件(grpc service)以及main.go
echo `protoc -I $project_name/ $proto_path --go_out=plugins=grpc:$project_name`

# 存放用户的handler
if [ ! -d handler ]
then
	mkdir handler
fi

echo "Project init completed"

echo "You can run 'cp -r template/* handler/' and then try 'go run main.go'"
围绕grpc,打造工具库和代码生成工具
Share this