cover_image

作业帮GO应用框架实践

吕亚霖 蒋帅 作业帮技术团队
2022年08月18日 11:26
大家好,这里是作业帮技术团队。欢迎接收我的第29次推送,以下是今日的内容。

图片

出品| 作业帮基础架构团队


图片

作业帮服务端GO语言化背景


作业帮初期因业务快速发展,服务端采用PHP语言作为主要开发语言,很好支撑了业务快速的迭代发展。但随着业务发展,以ODP为代表的PHP服务端技术栈遇到了一些问题,主要是:

·微服务架构支持欠缺:ODP通过PHPLIB耦合服务,类单体架构,服务间边界模糊,框架全局部署且缺乏现代包管理工具。

·性能/成本瓶颈:PHP缺乏线程/协程支持,资源使用率高,业务成本大,在高并发、高性能的部分场景与GO有一定差距。

·云原生适配不足:云原生带来的技术红利,比如容器化、服务治理、devops、服务观测,PHP的适配度低,比如FPM fastcgi在原生mesh的支持上及CD上过多耦合。

所以作业帮选择了GO作为主推的服务端开发语言来替代PHP。作业帮GO语言框架zgin是基于gin衍生而来,是面向web服务的开发框架,提供了开箱即用的常用组件和功能,侧重通用性和稳定性,兼顾性能和时延,构建了符合公司业务场景的生态体系。

图片

框架在技术体系中的定位


图片
图片

设计思想


应用框架是规范了WEB请求处理周期内的主体流程,并且把服务的流程进行抽象和分离。我们希望框架有一个性能优良、稳定可靠、功能简单明确的核心,通过常用组件管理来解决业务通用性扩展的问题。同时应用框架是连接业务研发和基础架构(架构、安全、运维、DBA)的纽带。所以他承担了向下落地研发规范、提升研发效率、保证研发质量;向上承接云原生架构(服务观测、服务治理、容器化、devops)、保证架构的落地和持续迭代。

框架能力/职责如下:

server模块:基于gin的二次开发及相关优化。

支持云原生的服务治理体系:友好支持基础架构的服务发现注册、服务观测、服务间同步异步通信。

封装可靠易用的通用组件:开箱即用、稳定可靠、性能优化、修复开源问题。

提升开发效率:完善的框架周边效率工具,提升研发效率。包含开发、测试、部署到线上维护等方面,比如代码生成工具、构建本地开发环境、本地和kubernetes测试环境互通等。

标准化、规范化:提升代码质量&可维护性

  • 开发规范、目录规范、go语言规范


  • 规范落地:部署、运行、观测


安全:业务无感知的RASP安全防护


架构示意图


图片


httpserver模块


性能优化


GIN是一个通用性应用框架,我们在GIN的基础上做了一些性能优化,用来适配底层基础架构技术方案,同时对业务场景下带来性能提升。

1.netpool or mesh uds。我们知道httpServer中一直以性能著称的fastHttp,与gin相比有一个很大的优势点就是复用了连接池,但是我们并未在gin的二次开发中实现连接池,这里考虑能力下移,借助底层架构的能力,多语言栈保持同等能力。我们所有服务模块都已经接入MESH化治理,上游请求实际上是跟mesh-proxy建立连接,而mesh-proxy和服务模块之间是通过UDS通信且保持长链接,同时优化UDS的零拷贝。

2.GC优化。GC优化。实现自适应GOGC,通过 GC 期间对 finalizer 函数的回调来实现动态设置 GCPercent,根据容器内存大小,自适应调整GC频率。同时尽可能使用sync.Pool复用对象,优化string与[]byte的转换等方式,尽量减少GC压力。

3.GMP优化。作业帮现有服务模块基本以容器K8S POD模式,运行在大规格(256核)裸金属服务器上。我们做了以下优化:1.自动适配容器场景下POD的limit,作为maxProcs。2.在对大规模裸金属服务器的numa拓扑特性做GMP调度优化。

任务功能扩展


除了核心的httpServer外,我们还对处理逻辑进行了扩展,请求的入口除了上游发起的http请求外,还有服务自动发起的任务,包括一次性任务、周期任务、定时任务。应用框架中,我们模拟请求链路实现了任务的封装,实现了requestId串联整个请求、支持加载中间件等功能。


暴露通用接口


httpServer默认添加了一些通用的工具型接口,旨在减少业务对接观测工具平台,主要接口如下:

1.readiness 服务就绪检查接口。一般来说,服务什么时候准备好接收流量意味着:应用运行状况是良好的、任何潜在的初始化步骤均已完成了,发送到应用的任何有效请求都不会导致错误。通常,业务如果没有特殊需求不需要做特殊处理,在框架中我们给出了默认的实现。如果业务有特殊需求,可以重写ready接口来实现。

2.pprof 信息采集接口。定位瓶颈要借助各种性能分析工具,对外提供统一的接口和输出结构体,对接公司工具平台。同时完善pprof能力,对当前go的CPU profiler不能分析off-cpu time的问题,做二次开发。

3.go runtime信息采集接口。针对go运行时的相关指标(GC/memory/goroutine),对外提供统一的接口和输出格式,对接公司工具平台。


通用组件


针对研发过程中常见的组件进行封装,修复开源问题、保证稳定性、持续优化性能,按照作用组件大致可以分为三类:


通用client组件


通用client组件,主要包括消息队列、对象存储、数据存储、httpClient常用库等。主要工作:

1.对接服务治理体系:所有网络client组件都接入MESH治理体系。出流量观测示例:

图片

图片

图片

2.对开源软件做规范适配、性能优化:规范比如日志规范、容器化运行规范;性能优化比如:json编解码优化、字符串拼接及转化的优化、对频繁分配的小对象使用sync.Pool、避免反射、减少加锁粒度、优化MAP key类型使用不当造成的gc问题等。

3.对应用技术栈平台做适配:适配脚手架工具和平台,方便后期升级推送、统一升级,此外还提供了丰富的demo实例。

通用基础组件


该类组件主要是选型的问题,通过对比、压测选取最优的开源包或者自行开发,定制合理的规范、保证可靠性、优化性能。该类型组件比如:json库、协程池、日志组件等。

以日志组件为例:

gin中日志输出相对较少,所以使用的是原生的日志库,没有考虑日志格式及性能的问题。然而日志对整个研发周期来说非常重要,日志消费方有研发人员、监控报警、大数据分析等。并且日志输出是非常耗性能的(根据我们观察,很多业务喜欢开debug级别的日志,日志量非常大,一个接口中日志输出占到30%的CPU时间都是很常见的),所以我们在框架中封装了zlog提供给业务使用,同时优化了gin框架中日志打印不规范的问题。

关于日志格式

目标:最大限度发挥日志的价值,需要让日志既满足人类可读性的要求,又满足机器结构化的需求。常见的日志格式:

格式一:kv拼接格式,即半结构化格式。原服务端C++项目多采用这种格式。

优点:
·可读性高:方便业务方进行日志排查
·明文匹配:对于需要按照正则匹配特定字符串的系统友好

缺点:
·value中可能包含特殊字符串,无法进行结构化分析。上述示例中,key value使用=分隔,多个key=value使用空格分隔,当value存在分隔符(即:=或者空格)时,程序就无法自动解析。

为了解决无法自动解析的问题,对value进行encode,避免value中出现特殊字符的问题,优化后格式如下:

优化后的日志格式虽然解决了结构化解析的问题,但是又引入了新的问题:可读性差、对需要正则匹配的消费方无法直接消费。

格式二:json格式

Json日志也可称作结构化日志,好处显而易见:简化了日志解析,使得日志的后续处理、分析或查询变得方便高效。

·可读性:适中,可读性没有方式一中kv拼接格式好,但是经过日志检索平台的解析后,可读性与<方式一>基本无差别了。
·明文匹配:由于value是明文的,能够兼容正则匹配。

同时考虑到json格式的受欢迎程度,易接受以及已经有大量服务使用了json做日志序列化方式,我们最终选择了json格式作为日志组件的标准格式。

日志性能优化点

1.合理设置日志级别

2.json序列化:在go中json编解码是非常耗费性能的,我们在框架里json编码实现是手动拼接字符串了。这种方式虽然使用不友好,但是对于基础组件中进行特定的优化,收益非常大。同样大小的json进行编码,使用字符串拼接方式能提升4-5倍的性能。

3.减少反射:牺牲体验以换取性能,在对业务提供的日志输出方法中,日志中的扩展字段需要明确指定类型。

4.使用对象池:一条日志的输出往往会涉及到多次小对象的分配,这种问题的常规做法就是使用 sync.Pool 增加临时对象的重用率,减少GC负担。

5.缓存/异步:开启buffer缓存,缓存满或到了设定的最大间隔才输出。需要注意的是,最大间隔的设置与监控的实时性要求是不可调和的,框架需要权衡间隔时间的设置,框架默认(interval=5s,bufferSize=256k)。我们模拟一个线上真实的服务输出日志的场景,一次请求输出15条日志,压测结果显示,开启buffer后 CPU能下降 40%,T99可降低90%。 


公司特定utils工具库


针对公司私有库中的一些基础函数(比如加解密函数、位运算函数、特定序列化函数)进行封装,避免每个业务实现一遍。另外针对go中常用的方法及耗费性能却不容易发现的方法进行封装,避免业务过度关注基础方法性能优化的问题。


效率工具


代码生成工具:简单易用,可以快速生成服务代码的脚手架,同时根据基础模板加可选的组件来生成业务应用的功能。一方面,可以规范业务对框架的使用,在目录结构、配置加载方式、组件初始化方式上给出了模版范例,避免个性化的写法,以减少后期维护的成本。另一方面,组件中给出了很多可选的示例代码,给用户提供了简单易上手的示例的同时还给用户演示了遵守框架标准规范的写法。

问题和性能分析:当业务方需要时可以在性能平台按照条件和需求开启pprof,提供在线分析报告。支持业务开启go runtime通用指标采集并有统一监控大盘展示。

测试环境互通工具:提供简易命令行工具,可以代理测试环境指定服务的流量到本地端口,也可以转发本地的出流量到测试环境上。

图片

框架推广和应用


框架推广痛点


升级推动困难


首先从0到1将大家收敛使用新框架的周期很长,需要业务方升级、部署,观察框架在他们业务上的表现。而升级框架对业务方来说优先级不高、成本大,需要熟悉框架、培训宣讲、也需要QA全面回归测试。

其次对与框架一些小版本出现的BUG,需要强制快速通知升级,避免问题蔓延。而框架对业务方来说本身是一个SDK,需要及时通知业务方主动升级。
最后业务方缺乏主动升级动力。很多业务方对性能优化、稳定性提升不敏感。比如管理后台类服务,往往框架版本落后release版本较多。

如何推动:

首先做好工具及生态建设,比如代码生成脚手架,组件更新脚手架等工具平台建设。同时对使用文档和使用样例、问题排查和注意事项,不断维护补充。

其次我们建立反向包管理工具平台。通过CI分析,知道所有使用方使用的是哪个包版本。对某个版本的使用方都明确建立索引关系。如下图:

图片
图片

当某个版本有明确问题需要升级或者下线时:

1.系统会主动发布通知,通知到所有使用方。
2.同步给服务模块评价体系,评价体系会将服务模块评分调低同时提出优化改进项和方法,在每日巡检里发送邮件给使用方。
3.在CICD pipeline里,会拦截包含该版本的编译上线,提示使用方升级。

最后框架开发上尽量不做破坏性升级,尽量维持平滑升级。针对一些较早版本,我们会组织年度升级计划(21年我们把0.X版本的业务全量推动升级到1.X版本以上)。通过平台工具,协助业务升级,让业务做好回归验证。


问题排查定位


使用框架过程中,基础架构面临最多的问题是协助业务方定位疑难问题。大部分场景里,业务方只要认为业务逻辑没有问题,遇到性能问题、偶现性BUG、疑难问题都会寻求基础架构协助排查。而在规模较大的研发团队内,靠人工协助是很难做到快速响应,高效处理。

如何解决:

一是提供平台工具让业务方可以基于监控、日志、分布式追踪做一个初步分析。应用框架会适配基础架构服务观测体系,观测体系对全量流量做采集分析,不仅有RPC流量包括mysql、redis、对象存储等有网络交互的组件纳管到服务观测体系中,同时打通了监控、日志、分布式追踪的数据互通,可以从分布式追踪跳转到日志检索找到对应所有日志,也可以从监控数据分析上下游流量的QPS、时延、成功率等黄金指标。

二是提供平台工具让业务方自行分析pprof和go runtime各种指标。业务方如果做好以上分析后还是不能解决问题,转发给框架方来进一步定位问题。


资源和性能收益


和原生GIN性能对比


压测场景:模拟业务场景的数据,压测接口中我们模拟了从db查询10条记录压测案例:

·原生gin:直接使用原生gin + gorm 实现一个接口
·zgin:基于框架cg一个新项目,实现同样功能的一个接口

pod规格:2c/4G

压测结果:

CPU 对比:
QPS
600
1000
1200
1500
2000
2500
原生gin
1.4
2
3
4
-
-
zgin
0.8
1.3
1.5
1.8
2.5
3

T99 对比:
QPS
600
1000
1200
1500
2000
2500
原生gin
18ms
23ms
28ms
100ms
-
-
zgin
16ms
21ms
23ms
28ms
32ms
60ms

因为框架为适配底层架构做了很多优化,所以从整体上看CPU可以节省30%左右。

真实业务服务下对比

业务场景:真实业务从原生GIN迁移到ZGIN

迁移前后对比(10:30 发版上线):

图片


图片


应用框架应用情况

历经2年发展,GO从0演化成服务端使用数量最多的开发语言,已有GO项目全部基于ZGIN构建。

增长数量见下图:

图片

服务模块数量:600+

服务POD数量:1万+


图片总结与展望

作业帮zgin框架落地过程是结合业务实际需求、贴合业务场景,对开源框架进行二次优化,构建了初步的go生态,保证了云原生架构的快速落地和持续迭代。对开发、部署、运行、运维等各个阶段都提供了有力支持。同时也看到在框架落地推广的过程不是一蹴而就的,是一个长期治理升级的过程。

------END------

也许你还想看
|直播间的Electron实践
|帮帮技术漫谈 | 智能批改、试卷还原?实现起来没那么难!
|混合云的多活架构指南

继续滑动看下一个
作业帮技术团队
向上滑动看下一个