Sniper Framework 轻量级业务框架开源项目

我要开发同款
匿名用户2019年06月19日
127阅读
开发技术GO语言
所属分类Google Go、Web框架、Web应用开发
授权协议未知

作品详情

Sniper起源于一项新业务。在转岗之前,我一直在L部门写PHP代码,遇到过如下问题:

基于TCP的RPC协议,我们都称之为 Weisai-RPC手工维护RPC文档,难以及时更新手写代码处理RPC入参,难以保证参数类型,如数字 1 和字符串 "1" 的区别无法方便地查询一个请求对应的所有日志服务拆分得很细,难以进行调用链路追踪使用JSON做为配置,难改难认难以监控服务运行状态代码分层标准不统一没有单元测试

大约在2018年的六月底,我得知要去新的C部门做新业务。没有任何历史包袱,我马上着手准备,希望能全方位的解决上面提到的问题。

Go语言

首先要解决语言选择的问题。PHP是最熟悉的,但从过去的经验来看,无论从性能还是从代码可维护性方面考虑,PHP都不是一个好的选择。当时有两种选择,一个是Java,另一个是go。平心而论,Java是要比Go要成熟得多。但Go更加简单轻便,从PHP过渡成本更低。而且当时公司正在推动用Go重写原有的Java项目。自然就选了Go。

RPC协议

有了语言,接下来就要确定通信协议。首先不要使用REST风格接口。REST中看不中用。REST的核心是资源和状态,所有的变更都对应状态的转变。

对于简单的场景,REST看似完美,如:GET/user/123 表示查询。

但如果是发送一条短信呢?一种方案是使用 POST/sms 表示创建一条短信资源,另一种方案则是 POST/sms:send 直接发送。

但不管哪种方式,都不如RPC调用直观,其原因有二:

一是http的方法(GET,POST,PUT,DELETE等)太少,基本都是面向静态资源的,表达能力有限二是将业务过程转成资源状态变化本身就比较烧脑,而且存在无法转化的场景

REST还有一个比较大的问题就是url中有数字id,统计prometheus监控指标的时候必须做归一化处理。

所以,不用REST。

WeisaiRPC

这得从原来在L部门用的Weisai-RPC说起。该RPC基于TCP传输,消息结构如下:

typedefstructswoole_message{uint32_theader_magic;//magic字段默认2233uint32_theader_ts;//unix时间戳uint32_theader_check_sum;//校验和,暂未定义,默认为0uint32_theader_version;//版本号uint32_theader_reserved;//保留字段,默认0,live-api转发时设置为1uint32_theader_seq;//序列号uint32_theader_len;//body长度charcmd[32];//命令字符串//格式{message_type}controller.method,//message_type0request,1response//长度没满右端补充\0,超过自动右端截断.char*body;//可变长度为header_len格式为JSON://{"header":...,"body":....}}rpc_message_t;

典型的面向c语言的设计,方便c语言解析,但不太灵活。

比如,cmd字段只有32字节,也就是说接口名字最多只能是32字节。还有body是字符串,但实际传输的是JSON,需要二次解析。使用结构化二进制消息就是为了提高解析速度,但这种改过跟JSON解码想比又可以忽略。所以,这种混合型的设计除了看上去比较复杂以外,确实没什么优点了。

因为没有采用HTTP协议,后来不得不在body中定义了header字段用来传输HTTP请求的header。像nginx,curl,tcpdump这样的标准也基本上无法正常使用。为此,还专门引入了一个接入层负责RPC和HTTP之间的相互转换。

切实体会到了Weisai-RPC的不便之后,我决定业务RPC协议只用HTTP传输,原则上不使用二进制消息格式。

关于gRPC

说到HTTP就不得不说说gRPC。gRPC是Google开放的一种RPC协议,其主要特性:

只支持protobuf编码强依赖HTTP2协议支持stream接口每个消息都有五字节的二进制前缀其他细节请参考 PROTOCOL-HTTP2。

protobuf本身是支持JSON的,不明白为什么gRPC的实现不支持。而支持stream接口则是gRPC的一大特色,使gRPC能够胜任诸如语音实时识别等场景。但这一类场景是比较少见的。我们绝大多数业务场景都是一问一答的。为了实现这个stream特性,gRPC不得不依赖HTTP2,不得不自行定义了一种有固定五字节头的消息格式。与此同时,gRPC也就放弃了HTTP协议原生的压缩功能,也没法使用HTTP协议的content-length头传递消息长度。这也是gRCP消息五字节头的功能所在,头一个字节表示是否压缩,后四个字节表示消息长度。

有个所谓的 2-8 原则:

一般只用 20% 的代码就可以解决 80% 的问题。但要想解决剩下 20% 的问题的话,则需要额外 80% 的代码。

gRPC的stream接口就是剩下的 20%的问题。

gRPC还有个web支持的问题。浏览器的js无法使用HTTP2的特性,所以不能直接与gRPC服务通信。于是有了 grpc-web,还有 grpc-gateway。

所以,如果没有stream接口需求,则完全没有必要使用gRPC;如果直的有这类需求,也不可能太多,直接使用原生TCP/WebSocket协议开发也不是难事。

最终我们选择了 twirp。twirp可以看作是简化版的gRPC,同样用protobuf描述,不依赖HTTP2,同时支持protobuf和JSON,没有五字节的二进制前缀。但我们对原生的twirp做了修改,形成了自己的版本,主要改动就是添加了对 www-form-urlencoded 编码格式的支持,这是移动端的历史包袱导致的,没办法。

现在的移动端使用www-form-urlencoded编码,更加简单;管理后台使用JSON编码,更加灵活。如果对性能有要求也可以使用protobuf编码,但没目前没有用,估计也不会有人喜欢用。

接口文档

使用proto描述RPC接口有一个问题,就是接口说明分了request,response和service,比较分散,尤其是要用到嵌套message的时候,对移动端开发同学很不友好。目前也一些文档生成工具,比如:protoc-gen-doc。但protoc-gen-doc也是为不同message生成对应文档,使用者需要在文档的不同部分来回跳转,很不直观。所以我们开发了 protoc-gen-markdown。这是生成的文档示例。最终,我们给gitlab加了一个webhook,当有新分支创建或者更新的时候会自动生成markdown文档并进而转化成html文档,彻底解决了文档同步的问题。

protoc-gen-markdown也不完美。它无法正确处理proto中的map消息。但我们在业务中没有用到这种类型,所以没有受到影响。但这始终是个问题。protoc-gen-markdown最早是跟twirp的改造一起进行的。最早的提交记录是从2018年7月3日开始的,主要功能到7月7日就完成了,到现在也没有大的变动。

配置系统

解决了通信问题之后,接下来要设计配置系统。

在L部门的时候都是用JSON做配置。JSON一方面对格式要求比较高,比如列表最后一个元素之后不能加逗号等;另一方面不支持注释,时间长了很难弄清各配置项的含义。还有就是JSON很灵活,导致很多业务配置层层嵌套,不好读、不敢改。

鉴于之前的经验,我们放弃了JSON,最终选择了 toml。而且框加要求所有配置只能是k-v型字符串的。如果业务代码要用复杂的配置,则需要自行处理反序列化逻辑。因为是k-v型的,所以很容易兼容环境变量,所有的配置项都可以通过环境变量覆盖。最后就是框架支持配置的热更新,会实时读取配置文件内容的变更。

我们也没有重复造轮子,配置的解析和加载都是通过 viper 完成的。

日志与监控

日志组件选用 logrus。没别的原因,就是star比较多。logrus支持不同的formatter,开发环境会将日志写到标准输出设备,其他环境会通过lancer写到elk(这一部分不适合开源)。

框架在处理请求的时候会创建一个opentracing的span。这个span是有一个trace-id的。框架会把这个trace-id注入到ctx中。我们希望相关的日志都要带有这个trace-id,所以需要通过 sniper/util/log.Get(ctxcontext.Context) 方法来获取logger实例,使用获取的实例记录日志会自动输出trace-id。框架在输出响应内容的时候也会自动在header中加上这个trace-id。

公司内部有个叫dapper组件,但没有opentracingsdk。框架自己提供了一个,但这一部分不适合开源。

好在是适配了opentracing,大家可以很方便的集成jaeger等组件。

基础组件

主要的基础组件有三个,分别是HTTP客户端、mysql客户端、memcache客户端。redis客户端是后来加入的,现在还没在业务中使用。

Sniper对基础组件提供统一封装,主要解决以下问题:

加载配置处理ctx输出日志支持opentracing统计prometheus指标

现在很少有框架会注意到这些方面,尤其是后三条。大家观注更多的往往是性能,往往是框架代码是否优雅。估计只有在生产环境摸爬滚打过几次才会对这些东西产生共鸣。

关于ORM

很多框架都提供ORM组件,但sniper不然。不推荐使用ORM,原因如下:

ORM固然方便,但会隐藏SQL查询细节,不利于程序员全盘掌握db查询情况。ORM用法并不统一,相对SQL标准有额外的学习负担。ORM无法覆盖所有有SQL查询,在特定业务场景下仍需要写原生SQL。ORM大多基于反射,有一定的性能损失。业务代码一般会有数据访问层(DAO),既便引入ORM,也只局限在DAO层。关于集群

Sniper框架的memcache和redis组件都不支持集群的,而且是有意不支持甚至是将已有的相关代码直接删除。

为什么呢?我们认为这些细节不应该是一个业务框架要关心的内容。这些内容应该交给统一的中间件处理。业务代码连中间件,根本无需感知集群的存在。对于memcache,我们生产环境用的是 twemproxy,对于redis和http服务,我们用的是 envoy。

我们坚信,未来一定是service-mesh的世界,诸如服务发现、负载均衡、限流熔断这一类的功能应该交由mesh服务处理。让我们试目以待。

单元测试

单元测试部分不适合开源,只能分享一些相关的思考。

没有单元测试,就很难有真正的积累。我们的核心业务逻辑基本都有单元测试覆盖。有一次要改支付逻辑,我改完跑通测试后直接移交测试,测试通过,直接上线,一气呵成。我甚至都没自己用curl调一下接口,因为我知道,单元测试已经覆盖的已知的关键流程。

这当然不是什么值得炫耀的事情。但有效的单元测试确实对提高代码的质量有很大的裨益。

但怎么测才好呢?关键在mock。Go对mock并不是很友好。而且如果mock多了,一方面会极大降低写测试用例的体验;另一方面会导致测试用例真就成单元测试了,可能出现各单元都没问题,但整个系统有问题的情况。

所以,写测试一定要简单,测试逻辑一定要有效。为实现这两个目标,我们定了两条规则:

外部http请求一律mock,这个基于 jarcoal/httpmockmysql,memcache,redis直接起服务,各测试用例自行维护自己的测试数据集

为了进一步降低编写测试用例的复杂度,我们还提供了自动同步表结构和导入种数据的功能。如果测试用例不想手工维护测试数据集,则可以将相关数据写种子数据集。测试框架会自动导入。

总结

引入sniper框架快一年了,基本上解决了在L部门遇到的问题,无论在线下开发、联调和测试效率方面,还是线上运行、排错效率方面,都有不俗的表现。

声明:本文仅代表作者观点,不代表本站立场。如果侵犯到您的合法权益,请联系我们删除侵权资源!如果遇到资源链接失效,请您通过评论或工单的方式通知管理员。未经允许,不得转载,本站所有资源文章禁止商业使用运营!
下载安装【程序员客栈】APP
实时对接需求、及时收发消息、丰富的开放项目需求、随时随地查看项目状态

评论