3

字节微服务HTTP框架Hertz使用与源码分析|拥抱开源 - 白泽来了

 1 year ago
source link: https://www.cnblogs.com/YLTFY1998/p/16648453.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

字节微服务HTTP框架Hertz使用与源码分析|拥抱开源

Hertz[həːts] 是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttpginecho 的优势, 并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点,目前在字节跳动内部已广泛使用。 如今越来越多的微服务选择使用 Golang,如果对微服务性能有要求,又希望框架能够充分满足内部的可定制化需求,Hertz 会是一个不错的选择。

对于源码该如何阅读,本身就值得思考。这篇文章我将以第一次阅读Hertz源码的视角,分享自己的思考过程,也借此梳理一下自己阅读源码的方法论。

接下来需要你对应打开Hertz的官方文档,以及在本地克隆Hertz的代码仓库,我们开始吧。

Hertz仓库地址:https://github.com/cloudwego/hertz

Hertz文档地址:https://www.cloudwego.io/zh/docs/hertz/getting-started/

二、架构设计

9f48ab86714b42f0816037d35d4c7484~tplv-k3u1fbpfcp-zoom-1.image

这是一张Hertz官方文档的架构设计图,图中的一个个组件对应hertz源码包内的一个个package文件夹,实现了对应的功能,如下:

8bff4b79043e463ebb650f6c372417e8~tplv-k3u1fbpfcp-zoom-1.image

三、快速开始

接下来按照文档的指示,通过hertz的命令行工具初始化一个最简单的hertz项目,先观其形,再会其意。

对应文档地址:https://www.cloudwego.io/zh/docs/hertz/getting-started/

# 安装hertz的命令行工具,用于生成hertz初始代码
go install github.com/cloudwego/hertz/cmd/hz@latest
# 通过hz工具生成代码,如果创建的项目不在GOPATH/src路径下,则需要额外声明-module参数
hz new -module hertz-study
3ca3f1439d1845aebf03688dea9abb29~tplv-k3u1fbpfcp-zoom-1.image

此时按照文档指示,对项目进行编译运行可以访问这个HTTP服务了,它默认实现了一个/ping接口。

curl http://127.0.0.1:8888/ping
# 响应
{"message":"pong"}% 

四、源码解析

server概览

首先看一下main.go函数,这是hertz服务的启动入口,大概可以猜测内容是:1. 初始化了一个默认的hz服务;2. 完成了一些注册工作;3. 启动hz服务(HTTP服务)。

func main() {
   h := server.Default()
​
   register(h)
   h.Spin()
}

回想刚刚这个 http://127.0.0.1:8888/ping 的接口服务,它所声明的IP和Port并未由你手动指定,并且/ping接口也不是你编写的,或许是这个server.Default()的作用。

反之我如果需要指定HTTP服务启动的各种定制化的配置,是否是给这个server.Default()传参数?又或者是换一个创建h的方法?

Default()

// Default creates a hertz instance with default middlewares.
func Default(opts ...config.Option) *Hertz {
   h := New(opts...)
   h.Use(recovery.Recovery())
​
   return h
}

查看Default()方法,发现确实可以传入参数(猜测就是可以自定义配置的内容),然后我们进一步分析New方法的内容,它接受了一个不定长度的Option数组为参。

// Option is the only struct that can be used to set Options.
type Option struct {
  F func(o *Options)
}
​
// New creates a hertz instance without any default config.
func New(opts ...config.Option) *Hertz {
  options := config.NewOptions(opts)
  h := &Hertz{
    Engine: route.NewEngine(options),
  }
  return h
}

接着我们再进入config.NewOptions方法观察这个Option切片将如何把我们自定义的内容应用到Hertz服务的初始化上去。

func NewOptions(opts []Option) *Options {
   options := &Options{
      KeepAliveTimeout: defaultKeepAliveTimeout,
      ReadTimeout: defaultReadTimeout,
      IdleTimeout: defaultReadTimeout,
      RedirectTrailingSlash: true,
      RedirectFixedPath: false,
      HandleMethodNotAllowed: false,
      UseRawPath: false,
      RemoveExtraSlash: false,
      UnescapePathValues: true,
      DisablePreParseMultipartForm: false,
      Network: defaultNetwork,
      Addr: defaultAddr,
      MaxRequestBodySize: defaultMaxRequestBodySize,
      MaxKeepBodySize: defaultMaxRequestBodySize,
      GetOnly: false,
      DisableKeepalive: false,
      StreamRequestBody: false,
      NoDefaultServerHeader: false,
      ExitWaitTimeout: defaultWaitExitTimeout,
      TLS: nil,
      ReadBufferSize: defaultReadBufferSize,
      ALPN: false,
      H2C: false,
      Tracers: []interface{}{},
      TraceLevel: new(interface{}),
      Registry: registry.NoopRegistry,
   }
   // 将自定义配置应用上去的方法
   options.Apply(opts)
   return options
}
​
func (o *Options) Apply(opts []Option) {
  for _, op := range opts {
    op.F(o)
  }
}

通过观察config.NewOptions源码,它首先初始化了一个Options结构,这个结构存放了Hertz服务的各种初始化信息,此时的Options的各个属性都是默认固定的,直到调用了options.Apply(opts)方法,将自定义的配置应用上去。

并且应用上去的方式很特别,它将这个默认创建的Options结构的指针作为参数传递给每一个你声明的Option的F方法,通过F方法的调用去为Options结构赋值,因为是指针,自然能将所有的赋值应用到同一个Options上去。

而具体的Option的F方法如何定义,则可以灵活实现,这也是Hertz拥有良好扩展性的原因之一。

// Default creates a hertz instance with default middlewares.
func Default(opts ...config.Option) *Hertz {
  // h是*Hertz类型,是框架的核心结构
   h := New(opts...)
   h.Use(recovery.Recovery())
​
   return h
}

此时注意到还有一个h.Use(recovery.Recovery())方法,写法很像是gin框架的中间件使用方式。

// Recovery returns a middleware that recovers from any panic and writes a 500 if there was one.
func Recovery() app.HandlerFunc {
   return func(c context.Context, ctx *app.RequestContext) {
      defer func() {
         if err := recover(); err != nil {
            stack := stack(3)
​
            hlog.CtxErrorf(c, "[Recovery] %s panic recovered:\n%s\n%s\n",
               timeFormat(time.Now()), err, stack)
            ctx.AbortWithStatus(consts.StatusInternalServerError)
         }
      }()
      ctx.Next(c)
   }
}

通过阅读注释确实发现这是个中间件,用于从panic中recover。

register()

func main() {
   h := server.Default()
​
   register(h)
   h.Spin()
}

回到最初的main方法中,经过分析我们知道了Default方法大致完成了默认(自定义)Hertz结构的声明,下面看一下register函数的内容

// register registers all routers.
func register(r *server.Hertz) {
​
   router.GeneratedRegister(r)
​
   customizedRegister(r)
}
​
// GeneratedRegister registers routers generated by IDL.
func GeneratedRegister(r *server.Hertz) {
  //INSERT_POINT: DO NOT DELETE THIS LINE!
}
​
// customizeRegister registers customize routers.
func customizedRegister(r *server.Hertz) {
  r.GET("/ping", handler.Ping)
​
  // your code ...
}

register(h)的工作是路由注册(也就是接口的声明),内部完成了两种类型的注册,GeneratedRegister()的注释指出这部分路由是由IDL生成的,关于IDL先卖个关子,你只要知道IDL描述了接口交互的结构。

customizedRegister()则是用于注册自定义的路由接口,并且初始化了一个你熟悉的/ping,当然也你可以在这里注册自己需要的路由,使用的方式也与gin很相似。

Spin()

最后分析一下main方法中的的第三部分,Spin方法。

// Spin runs the server until catching os.Signal or error returned by h.Run().
func (h *Hertz) Spin() {
   errCh := make(chan error)
   h.initOnRunHooks(errCh)
   go func() {
      // 核心方法
      errCh <- h.Run()
   }()
​
   signalWaiter := waitSignal
   if h.signalWaiter != nil {
      signalWaiter = h.signalWaiter
   }
​
   if err := signalWaiter(errCh); err != nil {
      hlog.Errorf("HERTZ: Receive close signal: error=%v", err)
      if err := h.Engine.Close(); err != nil {
         hlog.Errorf("HERTZ: Close error=%v", err)
      }
      return
   }
​
   hlog.Infof("HERTZ: Begin graceful shutdown, wait at most num=%d seconds...", h.GetOptions().ExitWaitTimeout/time.Second)
​
   ctx, cancel := context.WithTimeout(context.Background(), h.GetOptions().ExitWaitTimeout)
   defer cancel()
​
   if err := h.Shutdown(ctx); err != nil {
      hlog.Errorf("HERTZ: Shutdown error=%v", err)
   }
}

完成了一系列的初始化和声明操作之后,Spin()负责触发Hertz的运行,并且处理运行过程中的各种异常。其核心是errCh <- h.Run()

func (engine *Engine) Run() (err error) {
   if err = engine.Init(); err != nil {
      return err
   }
​
   if !atomic.CompareAndSwapUint32(&engine.status, statusInitialized, statusRunning) {
      return errAlreadyRunning
   }
   defer atomic.StoreUint32(&engine.status, statusClosed)
​
   // trigger hooks if any
   ctx := context.Background()
   for i := range engine.OnRun {
      if err = engine.OnRun[i](ctx); err != nil {
         return err
      }
   }
​
   return engine.listenAndServe()
}

再看到末尾的engine.listenAndServe()方法,这是一个接口,查看其实现类,发现可以追溯到standard和netpoll两个包。

7877d7776146493e94bc0ff4f530bcb5~tplv-k3u1fbpfcp-zoom-1.image

作为一个HTTP服务,最重要的就是提供网络通信交互能力,Hertz使用了可插拔的自研网络库netpoll负责网络通信,进一步优化了性能,这部分也将在后续的文章着重分析。

至此Hertz服务开始运行,你可以通过控制台请求:

curl http://127.0.0.1:8888/ping
{"message":"pong"}% 

使用hz工具生成最简易的Hertz代码后,本文粗浅地分析了main方法的内容,将其分为三个部分,服务配置声明Default()、路由注册register()、HTTP服务启动Spin()

虽然没有提及Hertz框架架构图当中的各种类型的package,但是其实处处有它们的身影,后续文章将以此文为基础,深入分析框架的各个功能组件,揭开Hertz的神秘面纱。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK