前言Hertz是字节跳动服务框架团队开发的超大型企业级微服务HTTP框架。具有高可用性、易扩展、低延迟等特点。经过字节跳动一年多的内部使用和迭代,现已在CloudWeGo上正式开源。目前,Hertz已经成为字节跳动内部最大的HTTP框架,在线服务超过10000项,峰值QPS超过4000万。除了被各业务线的同学使用外,还服务于很多内部基础组件,如:函数计算平台FaaS、压测平台、各类网关、ServiceMesh控制面等,使用反馈良好.在如此大规模的场景下,赫兹具有极强的稳定性和性能。在内部实践中,一些典型的服务,比如框架占比高的服务,网关等服务,迁移Hertz后,与Gin框架相比,资源占用显着降低30%-60%。到流量的大小,并且延迟也显着降低。赫兹坚持对内对外维护一套代码,为开源的使用提供了强有力的保障。通过开源,Hertz也将丰富云原生Golang中间件体系,完善CloudWeGo生态矩阵,为更多开发者和企业构建云原生大规模分布式系统,提供现代化、资源高效的技术解决方案。本文将重点介绍Hertz的架构设计和功能特点。项目之初,字节跳动内部的HTTP框架是对Gin框架的封装,具有易用性好、生态完整的优点。随着内部业务的不断发展,对高性能、多场景的需求越来越强烈。Gin是Golang原生net/http的二次开发,在按需扩展和性能优化上有很大的局限。因此,为了满足业务需求,更好地服务于各大业务线,2020年初,字节跳动服务框架团队在研究了内部使用场景和外部主流开源HTTP框架Fasthttp、Gin后,开始使用自研网络库,和回声。Netpoll开发了内部框架Hertz,使得Hertz在面对企业级需求时有更好的性能和稳定性,也能满足业务发展和应对不断演进的技术需求。架构设计Hertz在设计之初研究了业界大量优秀的HTTP框架,同时参考了近几年在内部实践中积累的经验。为了保证框架整体满足:1.极致性能优化的可能性;2.扩展能力面对未来不可控的需求,Hertz采用4层分层设计,保证各层级功能的衔接,同时通过层间接口达到灵活扩展的目的.整体架构图如图1所示。图1:赫兹架构图赫兹自上而下分为:应用层、路由层、协议层、传输层。分层重用。此外,Hz脚手架作为子模块与主库一起发布,可以帮助用户快速搭建项目的核心骨架,提供实用的搭建工具链。应用层应用层是直接与用户交互的层,提供丰富易用的API,主要包括Server、Client等一些通用的抽象。Server提供注册HandlerFunc、Binding、Rendering等能力;Client提供调用下游和服务发现的能力;并抽象出一个HTTP请求必须涉及的请求(Request)、响应(Response)、上下文(RequestContext)、中间件(Middleware)等。Hertz的Server和Client都可以提供中间件这样的扩展能力。应用层中一个很重要的抽象就是ServerHandlerFunc的抽象。早期赫兹路由的处理函数(HandlerFunc)并没有接收到标准的context.Context。我们在大量实践中发现,业务方通常需要一个标准的上下文在RPCClient或日志、Tracing等组件之间传递,但由于请求上下文(RequestContext)的生命周期仅限于一次HTTP请求,而上述——提到的场景往往会有异步的传递和处理,如果直接传递请求上下文,会出现一些数据不一致的情况。我们为此做了很多尝试,但由于核心原因是请求上下文(RequestContext)的生命周期不能按需优雅地延长,最终在各种设计权衡下,我们在routinghandlerfunctionsignatureReference,通过分离两个不同生命周期的上下文,从根本上解决上下文生命周期不一致导致的各种异常问题,即:typeHandlerFuncfunc(ccontext.Context,ctx*app.RequestContext)Routinglayer路由层负责根据URI匹配相应的处理函数。起初Hertz的路由是基于httprouter开发的,但是随着越来越多的用户使用,httprouter逐渐不能满足需求,主要是httprouter不能同时注册静态路由和参数路由,即/a/b,/:c/d两条路由不能同时注册;甚至还有一些比较特殊的要求,比如/a/b,/:c/b,在匹配/a/b路由时,两条路由都可以匹配。Hertz为满足这些需求重构了路由树,用户在注册路由时具有很高的自由度:支持静态路由、参数路由注册;支持优先级匹配,比如上面的例子会优先匹配静态路由/a/b;支持路由回溯,比如注册/a/b,/:c/d,匹配/a/d时,仍然可以匹配;支持尾部斜杠重定向,比如注册/a/b,匹配/a/b/时可以重定向到/a/b。Hertz提供了丰富的路由功能来满足用户的需求。更多功能请参考Hertz配置文档。协议层协议层负责不同协议的实现和扩展。Hertz支持协议的扩展。用户只需根据自己的需要实现如下接口即可在引擎(Engine)上扩展协议。同时也支持通过ALPN协议协商注册。Hertz首批只开源了HTTP1的实现,后续会陆续开源HTTP2、QUIC等实现。协议层扩展提供的灵活性甚至可以超出HTTP协议的范围。用户可以注册任何满足自己需求的协议层实现,加入赫兹引擎中。同时,他们也可以无缝享受传输层。来极致的表现。typeServerFactoryinterface{New(coreCore)(serverprotocol.Server,errerror)}typeServerinterface{Serve(ccontext.Context,connnetwork.Conn)error}传输层传输层负责抽象和实现底层网络库。Hertz支持对底层网络库的扩展。Hertz原生完美适配Netpoll,在延迟方面有很多深度优化,非常适合延迟敏感的服务接入。Netpoll对TLS能力的支持有待完善,TLS能力是HTTP框架的必备能力。为此,赫兹底层也支持基于Golang标准网络库的实现和适配,支持一键切换网络库。用户可以根据自己的需要选择合适的网络库进行替换。如果用户有更高效的网络图书馆或其他网络图书馆需求,也可以根据需要进行扩展。Hz脚手架和Hertz一起开源了一个简单易用的命令行工具Hz,用户只需要提供一个IDL,根据定义的接口信息,Hz就可以一键生成项目脚手架,让Hertz实现出-开箱状态;Hz还支持基于IDL的更新能力,可以根据IDL变化智能更新项目代码。目前Hz支持Thrift和ProtobufIDL定义。命令行工具内置了丰富的选项,可以根据自己的需要使用。同时底层依赖官方的Protobuf编译器和自研的Thriftgo编译器,均支持自定义生成代码插件。如果默认模板不能满足要求,可以按需定义。未来,我们将继续迭代Hz,不断整合各种常用的中间件,提供更高层次的模块化构建能力。为Hertz用户提供按需调整的能力,通过灵活的自定义配置打造一套符合自身开发需求的脚手架。Common组件Common组件主要存储一些通用的能力,比如错误处理、单元测试能力、可观察性相关的能力(Log、Trace、Metrics等)。对于服务的可观察性能力,Hertz提供了一个默认的实现,用户可以按需组装;如果用户有特殊需求,也可以通过赫兹提供的接口进行注入。例如,对于Trace能力,Hertz提供了一个默认的实现,同时也提供了一个连接Hertz和Kitex的Example。如果你想注入自己的实现,你也可以实现以下接口://TracerisexecutedatthestartandfinishofanHTTP.typeTracerinterface{Start(ctxcontext.Context,c*app.RequestContext)context.ContextFinish(ctxcontext.Context,c*app.RequestContext)}特性中间件赫兹不仅提供了Server中间件能力,还提供了Client中间件能力。用户可以使用中间件能力将通用逻辑(如日志记录、性能统计、异常处理、认证逻辑等)与业务逻辑分离,让用户更专注于业务代码。Server和Client中间件使用方法相同,使用Use方法注册中间件,中间件执行顺序与注册顺序相同,支持前处理和后处理逻辑。Server和Client的中间件实现方式不同。对于Server,我们希望降低栈的深度,也希望中间件可以默认执行下一个,需要用户手动终止中间件的执行。因此,我们将Server的中间件分为两种,即不在同一个函数调用栈中(中间件被调用后返回,上一个中间件调用下一个中间件,如图2中B和C所示)和同一函数调用栈中的中间件(调用中间件后,中间件会继续调用下一个中间件,如图2中的C和BusinessHandler)。图2:中间件链接的核心是需要一个地方存放当前调用位置索引并保持递增。恰好RequestContext是一个适合存储索引的地方。但是对于Client来说,既然没有合适的地方存放索引,只能退而求其次,放弃索引的实现,将所有中间件构建在同一个调用链上,需要用户手动调用下一个中间件。流处理Hertz提供服务器和客户端流处理功能。HTTP文件场景是一个很常见的场景。除了服务端上传场景,Client下载场景也很常见。为此,Hertz支持Server和Client的流式传输。内部网关场景下,从Gin迁移到Hertz后,根据流量大小,cpu占用率可降低30%-60%。服务压力越大,收益越大。赫兹也很容易启用流媒体功能。您只需要在服务端或客户端添加配置即可。可以参考CloudWeGo官网Hertz文档的流媒体部分。由于Netpoll采用LT触发方式,网络库主动从TCP缓冲区中读取数据到用户态并存入缓冲区,否则epoll事件会继续触发。所以,在超大请求的场景下,由于Netpoll不断的往用户态内存中读数据,可能会存在OOM的风险。HTTP文件上传场景是比较典型的场景,但是HTTP上传服务是很常见的场景,所以我们支持标准的网络库gonet,并且针对Hertz做了专门的优化,暴露Read()接口,防止OOM的发生.对于Client,情况就不一样了。在流式场景下,连接会被封装成Reader暴露给用户,而Client有连接池管理,所以连接多了一个状态。什么时候关闭连接,什么时候重用连接就成了问题。由于框架端不知道连接什么时候会用完,在框架端重用连接是不现实的,会导致包串问题。由于GC会关闭连接,我们最初的设想是流式场景中的连接交给用户后,由GC负责关闭,不会造成资源泄漏。但是经过测试发现,由于GC有一定的时间间隔,TCP中主动关闭连接的一方需要等待2RTT,在高并发场景下会导致fd被占满。最后,我们提供了一个用于多路复用连接的接口。对于性能需求,用户可以在使用完连接后,将连接放回连接池中重新使用。Performanceperformance赫兹采用了字节跳动自研的高性能网络库Netpoll,在提高网络库效率方面有很多实践。参考已发表文章字节跳动在Go网络库上的实践。此外,Netpoll还针对HTTP场景进行了优化,通过减少副本和系统调用的数量来提高吞吐量和降低延迟。为了衡量Hertz的性能指标,我们选取??了社区中具有代表性的框架Gin(net/http)和Fasthttp作为对比,如图3所示。可以看出Hertz的最终吞吐量、TP99等指标都在行业领先水平。未来,Hertz将继续与Netpoll深度合作,探索HTTP框架性能的极限。图3:Hertz与其他框架的性能对比ADemo下面简单演示一下Hertz是如何开发服务的。首先,定义IDL。这里使用Thrift作为IDL的定义(也支持Protobuf定义的IDL),写了一个名为Demo的服务。这个服务有一个API:你好,它的请求参数是一个查询,响应是一个包含RespBody字段的Json。//idl/hello.thriftnamespacegohello.examplestructHelloReq{1:stringName(api.query="name");}structHelloResp{1:stringRespBody;}serviceHelloService{HelloRespHello(1:HelloReqrequest)(api.get="/hello");}接下来我们使用hz生成代码,整理拉取依赖$hznew-idlidl/hello.thrift-modDemo$gomodtidy&&gomodverify填写业务逻辑,比如Wereturnhello,${Name},那么我们可以在biz/handler/example/hello_service.go添加如下代码//Hello.//@router/hello[GET]funcHello(ctxcontext.Context,c*app.RequestContext){varerrerrorvarreqexample.HelloReqerr=c.BindAndValidate(&req)iferr!=nil{c.String(400,err.Error())return}resp:=new(example.HelloResp)resp.RespBody="hello,"+req.Namec.JSON(200,resp)}编译运行项目$gobuild$./Demo现在已经生成了一个简单的赫兹项目,我们来测试一下$curlhttp://localhost:8888/hello\?name\=Xiaoming//如果看到下面的return,说明th在服务已经正常启动。${"RespBody":"hello,Xiaoming"}(上面的demo可以在hertz-examples中查看)之后就可以愉快的搭建自己的项目了。后记希望以上的分享能够让大家对赫兹有一个整体的了解。同时,我们也在不断迭代Hertz,完善CloudWeGo的整体生态。欢迎所有感兴趣的同学加入我们,共同打造CloudWeGo。参考资料Hertz:https://github.com/cloudwego/hertzHertzDoc:https://www.cloudwego.io/zh/docs/hertz/字节跳动在Go网络库上的实践:https://www.cloudwego.io/en/blog/2021/10/09/%E5%AD%97%E8%8A%82%E8%B7%B3%E5%8A%A8%E5%9C%A8-go-%E7%BD%91%E7%BB%9C%E5%BA%93%E4%B8%8A%E7%9A%84%E5%AE%9E%E8%B7%B5/
