来自滴滴平台的技术部架构师给大家做一些 golang 相关的分享和交流 - Go语言中文社区

来自滴滴平台的技术部架构师给大家做一些 golang 相关的分享和交流


前言

  

  我是来自滴滴平台的陶文,今天很高兴能够在这里给大家做一些 golang 相关的分享和交流,我的演讲主要分为三个模块:

  

  一、为什么不手写测试?

  

  二、流量录制和回放方案的取舍。

  

  三、golang 实现的相关问题。

  

  为何不手写测试?

  

  两种手写测试的风格

  

  大家都知道一般技术测试是写的而不是录的,那么我们为什么不选择手写呢?这个问题我们也思考了很久。在十多年前我工作那会,大部分的测试都是手写的,一般来说有两种:一种是先把你的服务器建立起来,之后往数据库灌数据跑测试,看数据库的状态是不是和预想的一样。其中最好写的是基于状态的测试。如果需要在测试里面访问邮件服务器、发个邮件,这时候就可以把一个接口做个 mock,以确保在测试的过程中发送了邮件。虽然大部分的测试都可以由 mock 完成的,但是这种方式风险很大。我们曾经花了将近一年的时间用 mock 写测试,后来发现这种方式下只要稍微改一点代码,基于 mock 的测试就挂了。所以一般来说,写测试的话如果绝大多数验证条件都是基于 mock 来测的话是有问题的。


640?wx_fmt=png

  

  跨组织边界的集成测试

  

  为什么不用手写,而是用最传统的基于状态去测试?其中有两个原因,第一个是在微服务的结构下,我们的模块越分越细。需要测一个完整功能的时候,就会调用多个团队的服务。比如说我们会把订单的服务变成所谓的订单系统,除了跑代码还要跑系统,稍微复杂一点的代码甚至还要跑策略,种种理由,你的代码少则几个依赖,多则二三十个。这时候面临的将会是一个跨团队的集成问题,需要融合很多团队的代码一起合作才能形成最简单的对外 rpc 接口。另外,当这个问题和前面的问题耦合之后,当我们的代码本身就是有前后依赖关系的时候,比如你清楚我的代码依赖什么前置状态,但是未必清楚你所依赖的服务就像某某团队的服务可能依赖某个词典。但在加载到某个服务器时你是不知道的,这时候测试就很难按照你真正的想法跑起来。


640?wx_fmt=png

  

  更加困难的是,一旦测试失败了,最关键的问题是谁来定位,如果集成七个模块的代码,那么找哪个模块的负责人去问?其中会有各种各样的问题:语言不熟悉、框架看不懂、模块不清晰,这时候你就会想这到底是不是我的事情,我是不是应该负责这些事情。最后还是倒在了这句话:不是我的问题。

  

  单模块流量录制和回放

  

  而流量录制回放要解决的问题就是在我们没有办法改变团队分工状态的情况下,把我们负责的部分隔离出来,然后把它们周边的模块交互,全部录制下来之后再回放,这种处理方式的核心就在于它没有办法推卸责任,如果挂了就肯定是你的问题,而且它对环境问题的依赖非常小,如果出了问题一定不是由于环境问题导致的,大概率是代码有逻辑上的偏差,或者是实现新功能、做代码重构的情况,这样一来,测试失败的问题就可以变得更加容易定位。


640?wx_fmt=png

  

  流量录制和回放方案的取舍

  

  拦截层次的选择

  

  明确这个目标之后,我们的下一个任务是选择一个实现方式。流量录制和回放有很多实现的可能,从最上层来说,我们的业务代码里面,每一个代码访问,没有函数在入口和出口都可以 load,这是最容易实现的方式,在网卡驱动上,可以实现一个虚拟网卡,像 vpn 一样把所有经过网卡的流量录下来,中间有很多层次可以拦截,比如可以在业务框架可以放在 rpc 框架里,可以在语言标准库,可以在语言标准库所基于编程环境的语言上搞,也可以在进入内核之前搞,我们可以在各个层次上做拦截,拦截的东西有深有浅。在网卡驱动层面只能拦截网卡的流量,甚至不能区分出来到底是 tcp 还是还是别的类别,但是如果在业务代码里面搞的,你可以把相关信息录下来并访问某个内存里面的配置服务,但是如果在网络层面的话就无法录制。


640?wx_fmt=png

  

  根据这个选择,我们要做一个标准,首先要解决这个问题:就是在我们生产环境的服务器当中,流量很嘈杂,需要从生产环境上把这些流量录下来,再分门别类,比如说一个服务器每秒钟至少有两百个并行请求,如果全都串了的话,这将对我们接下来的回放有很高的要求,不能做到精确匹配的话,回放的时候就会弄错流量,一百万线程上面跑了一万个,这种情况下怎么区分请求,这就是我们要解决的问题。

  

640?wx_fmt=png


  分布式追踪技术

  

  大多数公司的解决方案是用分布式追踪技术,通过修改代码来取得 trace ID,如果没有生成一个 trace ID 的话,在访问订单系统各种各样其他服务的时候把入口带上,这样就可以有一个完整的请求,再把各种请求关联上。其实大部分公司所谓分布式追踪技术的实现方式,要么是去手工修改代码,要么是取得当前线程关联的上下文,但是如果依赖这种技术,就需要我们对上面的业务有一定的掌控,因为滴滴目前来说是多语言多框架的技术站,我们很难在语言和框架层面做出统一,所以我们最后的选择是利用线程 ID。我们要拿到当前的线程 ID,关联入口和出口的流量,比较现实的方式是在用户程序中间做拦截,这样我们实际上是在业务的进程内部进行拦截,从而拿到线程 ID。


640?wx_fmt=png

  

  还有一个问题,就是多业务绘画可能用同一个线程的情况,一个线程 ID上跑的东西未必是连续的业务请求,这样就相当于说是一个分时租赁的模式,可能在一个时间段内跑一个业务,在另外一个时间段跑另外一个业务,产生一种映射关系,每次发生切换会有标记,可以把一个完整的 rpc 请求链录制下来。

  

640?wx_fmt=png


  golang实现的相关问题

  

  整体架构

  

  从实现角度讲,整体架构的录制本身是 SO,是动态运行库,可以注入 SUT,可以理解为外部的进程,可能是 Java 这样的一个虚拟机,可能是一个 php 虚拟机,也可能是 golang 本身,或者其他语言的执行环境。而在这个环境的外面两侧,我们去做拦截,所有的请求都通过 infound 进来做回放。所有的对外的请求是在 outfound,拦截所有外部服务。所以从 infound、outfound 角度来讲,infound 是接受 replay,可以用编码进行回放,而 outfound 是 TCP 服务器,外面只要是 TCP 的服务都会被 mock 掉,再根据全局的脚本做回放,大概的回放的结构就是这样。


640?wx_fmt=png

  

  劫持网络请求

  

  录制就是把流量录下来,但是走的是同一个代码,比如同一个 SO 既做录制也做回放,关键原理就是:首先我们劫持网络请求,发起 infound,php 监听,然后它会去调用其他方法,会被我们重新定向到 outfound TCP的 mock 上,它本身就是一个循环,把它跟所有录制流量做匹配,匹配到了再把之前线上录制的内容放上去。

  

640?wx_fmt=png


  匹配请求的算法

  

  匹配算法要求要快,因为线上很多请求超时非常短,不可能几十毫秒匹配出来,这样就会超时。那么我们做了一个非常难的 if 匹配的算法,就是把它切开,只要这里面没有出现大规模的重排序或是 key 的颠倒,大部分情况都是能够匹配的,如果中间稍微有一两个不见或者多出来的都是可以被近似掉的。

  

640?wx_fmt=png


  拦截的实现

  

  拦截本身就是通用机制,常用的符号机制,当我们注入 SO 之后,如果实现了同样的符号,就会在绑定函数指正的时候,不会绑定到真正的 libc,也就不会绑定到我们 SO 上面。

  

  所以其实真正关键的技术很简单,就是你怎么用 GO 写一个链接库,然后曝露符号,编辑的时候如果 mock 是这个的话,就可以写出一个 SO,可覆盖。

  

640?wx_fmt=png

640?wx_fmt=png


  暴露符号

  

  用 go 直接去写的话会有一些限制,主要是函数签名很难一一对应 C 的函数签名,完整的模拟 C 的函数签名有一定难度,所以更简单的办法是在代码里面写一个 cgo 实现符号的曝露,拦截之后转交给 go,曝露符号也可以用 cgo 来写,起到的类似作用,但掌控会更高一点,因为我们是拦截下来的,并不是完全的把它 mock 掉,所以还要回到原来的 libc 函数。再通过 som 可以直接达到原来的 libc 函数指正。

  

640?wx_fmt=png


  php-fpm 父子进程

  

  当然,我们也遇到了几个问题,一个是 php 和 fpm 父子进程问题,我们主要的线上服务器是 php 写的,当我们要注入的时候,发现它不好使,因为 php 副进程是通过转交形式完成的,但是 fpm 进程会引起 golang 错乱,出于某种原因 golang 歇菜了,不会有任何 go 的行为的触发,虽然是被拦截下了,但是 go 整个虚拟机就不再往前走了。

  

640?wx_fmt=png


  跳板loader

  

  解决方法是通过跳板,我们在副进程注入的不是 go 写的 so 文件,而副进程写的 so 文件是用纯 C 写的,这样副进程里面的 so 就没有 go wrong time,当副进程的注入发现 tcp,再去调由 go 写的 so,再去做拦截,就可以避免问题。


640?wx_fmt=png

  

  支持mac

  

  另外一个兴趣点就是,由于这种开发方式比较方便,可以支持不用把所有的依赖环境搭建起来,于是我们的很多同事就用 mac 做开发,做出了一个能够在 mac 本地起图形化的 php storm 的界面,能够直接把整个环境的流量做回放,mac 上有一个类似的东西叫做 dldyld,这种机制可以达到类似于 ld 的效果,可以使用同样的用一套代码支持两种平台,我们在 go 上面和 mac 上面都可以实现同样的拦截效果。

 

640?wx_fmt=png


  用 go 写 so 的优势

  

  最后来讲一下用 go 写 so 的优势,其实主要是我们的团队对 C 很熟,可以写很复杂的 C代码,基本的数据结构支持要更加全面,同时又比像 Java 这样高阶语言跟 C 的交互更有优势,又比较简单,同时我们可以在单线程的情况下能够起多个 service,前面的例子如果要起多个 srd,在线上做有一种跑满 cpu 的风险,而单线程在安全性上面更有优势。

  

  Q&A

  

  提问:这个到底好在哪里?

  

  陶文:traceID 主要是修改原代码写 load,我们线上是这样的方式,需要修改代码传递 traceID,以及要打 load,修改代码本身以及打 load 本身是依赖于你去推广,这很难保证代码不遗漏的。

  

  我们两套都有,我们并不需要(全量)流量做录制,只需要采样部分流量。没有什么道理,就是发现这个很好用,我们测试过32和8,没有16好,所以就改成了16,这个纯粹是启发式的算法。

  

  提问:流量回放能百分之百回放吗?

  

  陶文:可以修改一下百分之百的定义,就可以百分百。

  

  提问:是不是有可能只回放了90%,但是正好剩下的10%触发了,就挂了。

  

  陶文:有可能的,线上录的时候 mac 序列化是 ABC,回放是 CBA,就匹配不上。

  

  提问:回放必须再现上操作码?最后说的优点是说线上的时候为了防止 CPO 占满,所以用了一个 go,但是线下回放,那就没有什么关系了。也不能算是优点,如果有人线上误错作,算是优点。

  

  陶文:回放在本地,go 在这个例子里面优点没有那么明显,取决于这种团队熟不熟。

  

  提问:这个开源了吗?

  

  陶文:没有开源,我们有一个更好的版本在内部使用。

  

  提问:用回放有没有什么限制?需要依赖回放系统,如果一套系统支持多个自己本身的子服务的话,这样流量回放是不是一直都需要?golang 否支持?

  

  陶文:流量回放有一定的劣势,就是说如果你把多个模块合在一起当成一个模块进行回放的话,按照这种方式回放,没有那么容易。

  

  提问:这种回放会很花费时间吗

  

  陶文:回放速度非常快,比跑单测还快。

  

  提问:相当于你如果改了一个功能,相当于覆盖了以前的功能的话,你这个回放是不是就一定跑的快?

  

  陶文:当然是有可能的,所以主要的应用场景还是在重构下面。

  

  提问:大范围能 cover,小范围修 bug 不能 cover?

  

  陶文:添加新的功能,会cover。

  

  提问:刚才您提到的开发新业务,肯定要加新的逻辑,新的逻辑肯定流量也不知道,我们回放的时候,是怎么把这些逻辑区分开?在真实应用中,因为可能加了一个新逻辑,以前没有这个流量,这个主要是做回归测试。我们怎么把新的业务和老的回归的老的业务区分开?

  

  陶文:我们主要是用重构的,如果说服他们用这个新的是很有难度的,虽然我们可以构造所有的 mock 请求,如果愿意做维护还是比较困难的。

  

  提问:前面的图里面有一个录制点,是线上的吗?我没有看明白,就是录制点流量很多,通过你的 so 拦截,这个 so 是加载线上的服务器上面是吗?后面 mock 是一个实时的吗?

  

  陶文:是,录制的时候,线上服务,它插入的点就在这里,在 vc 中间。是真实访问服务器,在调用函数的时候,在中间拦截一下,会在一个存储里面,测试的时候再拉出来。

  

  提问:外部数据都会记录下去吗,或者加密的。

  

  陶文:服务器 rpc 之间还没有走内网请求,录制本身来说,对采样的流量是全量的,没有采样的是没有录制的。

  

  提问:有对比两个方案吗?

  

  陶文:我理解现在 tcp 更多是流量压制的方式,我们这种方式,我们最后验证是不是一样的,中间也是不一样的,最多是对正确性的校验而不是流量压制的校验。

  

  提问:你们这个流量录制对线上服务器的性能有多大,采样率怎么考虑的?

  

  陶文:采样率设的非常低,有些接口是万分之一。采样比较低也没有什么太大的影响。

  

  提问:怎么解决回放的数据依赖?

  

  陶文:回放的时候它的依赖并不依赖真实的数据,因为它访问数据库本身,也是被 mock 掉的,也是走的录制流量,所以相当于没有依赖,唯一的依赖是内存中的配置,这个可能是我们还是通过传统的改代码的方式,在代码里面录下来,访问了哪些配置,我们在流量回放的时候把配置加载回去。


版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/RA681t58CJxsgCkJ31/article/details/83112301
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-02 18:45:23
  • 阅读 ( 1000 )
  • 分类:架构

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢