kratos源码解析

765 阅读3分钟

简单的helloworld

// 首先看最基本的new app run
	return kratos.New(
		kratos.ID(id),
		kratos.Name(Name),
		kratos.Version(Version),
		kratos.Metadata(map[string]string{}),
		kratos.Logger(logger),
		kratos.Server(
			gs,
			hs,
		),
	)
}

那么生成这个kratos的应用时候,大概工作有哪些呢?

// /home/soda/go/pkg/mod/github.com/go-kratos/kratos/v2@v2.4.1/app.go
   
func New(opts ...Option) *App {
    ......
    // 省去上面一堆配置项,关键在于下面,kratos生成了一个专属自己的cancel contex,这个context在后面管理     // http和grpc等服务时候起了关键的作用
	ctx, cancel := context.WithCancel(o.ctx)
	return &App{
		ctx:    ctx,
		cancel: cancel,
		opts:   o,
	}
}

然后到后面关键的启动时候,程序做了哪些动作呢?

func (a *App) Run() error {
    ....
    // 省去配置中心等代码
    // 整个程序的的不同server管理,主要通过go内置的errgroup管理,而errgroup的ctx衍生于前面app初始化时候的ctx.简单说,app的ctx被cancel的时候,整个errgroup的ctx也会被cancel
	eg, ctx := errgroup.WithContext(NewContext(a.ctx, a))
	wg := sync.WaitGroup{}
	for _, srv := range a.opts.servers {
        // 下面是个注意点,在go的for循环中,闭包中有用到循环变量的话,用下面的用法可以防止错误
		srv := srv
		eg.Go(func() error {
			<-ctx.Done() // 当app被cancel或者errgroup启动的协程函数返回错误时候,堵塞在此处的代码开始启动.运行各个server的Stop函数.注意,这里衍生的并不是app的ctx,而是初始化的ctx.
			stopCtx, cancel := context.WithTimeout(NewContext(a.opts.ctx, a), a.opts.stopTimeout)
			defer cancel()
			return srv.Stop(stopCtx)
		})
        // 值得注意的是,下面需要利用WaigGroup来保证各个server启动后才继续进行下一步
		wg.Add(1)
		eg.Go(func() error {
			wg.Done()
            // 任意一个server启动失败的时候,都会返回err.导致errgroup的ctx被取消
			return srv.Start(NewContext(a.opts.ctx, a))
		})
	}
	wg.Wait()
......
	c := make(chan os.Signal, 1)
	signal.Notify(c, a.opts.sigs...)
	eg.Go(func() error {
        // 关键的地方,下面有两种情况会返回.一个是errgroup的ctx被结束时候.这个时候已经不需要再返回啥err
		select {
		case <-ctx.Done():
			return nil
        // 一个是收到系统信号时候,app的Stop函数会被调用.而Stop函数的主要内容有两个
        // 一是在注册中心Deregister,二是调用app的Cancel函数,cancel掉app的ctx
        // 当app的ctx被cancel后,整个errgroup的ctx也会被取消
        // 接下里,前面的<-ctx.Done()将会停止阻塞.然后直接调用server的Stop函数.
        
        // 值得注意的是,srv.Start传入的不是app的ctx或者衍生的errgroup的ctx,所以Start方法并不会第一时间被cancel.而是调用了Stop后才会被取消.这样Stop就可以传入timeout这个超时参数,来等待srv的返回
		case <-c:
			return a.Stop()
		}
	})
	if err := eg.Wait(); err != nil && !errors.Is(err, context.Canceled) {
		return err
	}
	return nil
}

transport结构解析

kratos.New中,需要传入对应的grpc或者http服务.而在app的Run()方法中,就是遍历app的servers []transport.Server来实现的.

所以不管是提供http服务还是grpc服务,都是属于kratos的transport模块下.而krtaos的trans模块,主要抽象了下面两个接口.

// Server is transport server.
// http或者grpc的server,都必须实现下面接口,让app执行Run方法后服务启动或者后续的停止服务.
type Server interface {
	Start(context.Context) error
	Stop(context.Context) error
}
// Transporter is transport context value interface.
// Transporter的主要作用,就是起码当前请求的各种信息,比如是grpc还是http,路径和操作,请求带的header等等.把grpc和http两种类型的服务的请求的各种信息,统一成一种接口返回.
// 通常情况下,Transporter会放在请求的context里面,通过FromServerContext/FromClientContext来返回.
type Transporter interface {
	Kind() Kind
	Endpoint() string
	Operation() string
	RequestHeader() Header
	ReplyHeader() Header
}

transport对应服务实现-grpc-server

krtaos提供的grpc-server关键代码如下:

type Server struct {
    //内嵌了一个grpc的自身的server
	*grpc.Server
    //下面一系列补充属性,后面讲到会补充说明
    ...
}

// 下面的kratos grpc包里实现Transport接口的Transport结构体
type Transport struct {
	endpoint    string
	operation   string
	reqHeader   headerCarrier
	replyHeader headerCarrier
	filters     []selector.Filter
}
// 对grpc的MD类型的扩展,使得实现transport.Header接口
type headerCarrier metadata.MD

// 初始化grpc-server方法
func NewServer(opts ...ServerOption) *Server {
    // 下面两行代码是关键,初始化两个grpc拦截器数组,而这个数组在最开始就分别添加了unaryServerInterceptor和StreamServerInterceptor这两个拦截器.
	unaryInts := []grpc.UnaryServerInterceptor{
		srv.unaryServerInterceptor(),
	}
	streamInts := []grpc.StreamServerInterceptor{
		srv.streamServerInterceptor(),
	}
    // 下面才是把我们自己需要的拦截器append到这两个数组
	if len(srv.unaryInts) > 0 {
		unaryInts = append(unaryInts, srv.unaryInts...)
	}
	if len(srv.streamInts) > 0 {
		streamInts = append(streamInts, srv.streamInts...)
	}
    // 最终传给嵌入grpc server初始化的数组
	grpcOpts := []grpc.ServerOption{
		grpc.ChainUnaryInterceptor(unaryInts...),
		grpc.ChainStreamInterceptor(streamInts...),
	}
    ....
    // 配置项完成后,才开始
	srv.Server = grpc.NewServer(grpcOpts...)
	srv.metadata = apimd.NewServer(srv.Server)
	// internal register
	grpc_health_v1.RegisterHealthServer(srv.Server, srv.health)
	apimd.RegisterMetadataServer(srv.Server, srv.metadata)
	reflection.Register(srv.Server)
	return srv
}


kratos是通过unaryServerInterceptor/ChainStreamInterceptor拦截器,用请求和服务器信息生成对应的transport,传入到请求的context中的,我们来看其中一个unaryServerInterceptor方法



func (s *Server) unaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        // kratos的一个工具方法,把服务器的contex和请求的context合并成一个.
		ctx, cancel := ic.Merge(ctx, s.baseCtx)
		defer cancel()
        // 取出grpc请求中对于的metadata
		md, _ := grpcmd.FromIncomingContext(ctx)
		replyHeader := grpcmd.MD{}
		tr := &Transport{
			operation:   info.FullMethod,
            // 把md转化成实现transport.Header接口的headerCarrier
			reqHeader:   headerCarrier(md),
			replyHeader: headerCarrier(replyHeader),
		}
		if s.endpoint != nil {
			tr.endpoint = s.endpoint.String()
		}
		ctx = transport.NewServerContext(ctx, tr)
		if s.timeout > 0 {
			ctx, cancel = context.WithTimeout(ctx, s.timeout)
			defer cancel()
		}
		h := func(ctx context.Context, req interface{}) (interface{}, error) {
			return handler(ctx, req)
		}
		if next := s.middleware.Match(tr.Operation()); len(next) > 0 {
			h = middleware.Chain(next...)(h)
		}
		reply, err := h(ctx, req)
		if len(replyHeader) > 0 {
			_ = grpc.SetHeader(ctx, replyHeader)
		}
		return reply, err
	}
}

上面可以看到这个拦截器做了两个关键的动作:

  1. 通过一个工具方法ic.Merge,使得kratos-grpc-server的context和每个grpc请求的context合并为一个请求.
  2. 通过拦截器,首先把transport放在贯穿整个service的context里面.而把grpc的metadata和路由方法等信息放在统一transport里面.使得无论是grpc或者是后面要讲的http都能统一通过获取transport获取这些信息.

1.ic.Merge简单了解

// 合并后实现了Context接口的mergeCtx结构体
type mergeCtx struct {
	parent1, parent2 context.Context

	done     chan struct{}
	doneMark uint32
	doneOnce sync.Once
	doneErr  error

	cancelCh   chan struct{}
	cancelOnce sync.Once
}

// mergeCtx结构体把两个成员都作为自己的属性(双亲contex).在新建的时候就做好了其中一个已经被cancel的准备了
func Merge(parent1, parent2 context.Context) (context.Context, context.CancelFunc) {
	mc := &mergeCtx{
		parent1:  parent1,
		parent2:  parent2,
		done:     make(chan struct{}),
		cancelCh: make(chan struct{}),
	}
	select {
	case <-parent1.Done():
		_ = mc.finish(parent1.Err())
	case <-parent2.Done():
		_ = mc.finish(parent2.Err())
	default:
		go mc.wait()
	}
	return mc, mc.cancel
}
// 利用sync.Once只执行一次的特性,结束自身的done channel同时,
// 只保留第一个结束的子ctx的err
func (mc *mergeCtx) finish(err error) error {
	mc.doneOnce.Do(func() {
		mc.doneErr = err
		atomic.StoreUint32(&mc.doneMark, 1)
		close(mc.done)
	})
	return mc.doneErr
}
// 在任一双亲ctx结束时候,调用finish方法结束自身
func (mc *mergeCtx) wait() {
	var err error
	select {
	case <-mc.parent1.Done():
		err = mc.parent1.Err()
	case <-mc.parent2.Done():
		err = mc.parent2.Err()
	case <-mc.cancelCh:
		err = context.Canceled
	}
	_ = mc.finish(err)
}
// 下面两个是关键方法,很好的阐述明白了golang里面ctx的特性:
// 当父ctx(现在是任一双亲ctx)被结束时,自身也会被终结.
func (mc *mergeCtx) Done() <-chan struct{} {
	return mc.done
}
// 注意的是,如果已经被结束了(doneMark已经被标记),就不必在去判断双亲节点结束与否
func (mc *mergeCtx) Err() error {
	if atomic.LoadUint32(&mc.doneMark) != 0 {
		return mc.doneErr
	}
	var err error
	select {
	case <-mc.parent1.Done():
		err = mc.parent1.Err()
	case <-mc.parent2.Done():
		err = mc.parent2.Err()
	case <-mc.cancelCh:
		err = context.Canceled
	default:
		return nil
	}
	return mc.finish(err)
}

// 总结上面就是,自身监控着双亲context是否结束
// 一旦双亲结束或者自身被取消,则调用finish方法close自身done通道,且标记doneMark


2.kratos的中间件设计

kratos提供了一个通用的Middleware类型.使得一个这样类型的中间件可以同时用在grpc和http服务中使用.这部分内容可以后面再详细讲讲

// Middleware is HTTP/gRPC transport middleware.
type Middleware func(Handler) Handler