前言

在 4 月 27 日举办的 Gopher China 2019 中,国内 Go 语言专家,Bilibili 架构师毛剑进行了题为《Go业务基础库之Error & Context》的演讲,主要探讨两个问题:

1. 在业务的基础库中,经常需要针对异常进行处理;

2. 在Go引入context以后,我们如何改造自己的基础库。

本文是他演讲的第二部分——Context 篇,以下为演讲实录。

第二个我们讲上下文,上下文很多地方都在用了,上下文争议非常多。

我们先看背景,我们用上下文到底想解决什么问题?像其他语言,比如说像 Java 可以通过ThreadLocal 很方便取一些东西。Go 经常看到有人搜怎么获取 GoroutineID,或者往里面塞一个什么东西,跨函数调用的时候可以传递等等,这种黑科技尽量少玩,尽量使用 Context 包处理。Context做法其实就是显式传递,我个人比较倾向显式传递比隐示传递好,显式传递我明确传了Context 进去,我就知道通过 Context 暴露方法应该能获取一些什么东西,这是自己的理解(而不是一个神奇的ThreadLocal,还要考虑线程传递,当然使用Context也要注意goroutine传递)。坏处是一污染全污染,所有函数都是首参为Context。

很早以前标准库那个时候没有Context,我们业务基础库改造从无到有(是基于的sub package),觉得两个东西非常重要,第一解决超时传递的问题,第二解决级联取消的问题,还有一些元数据传递的作用。

还有跨进程,我要传给另外一个服务,要识别一些数据,这个我们自己怎么解决呢?

No.1

Context with API

超时&取消

覆盖业务库

第一步覆盖一些业务的基础库。这个是redis的一个Interface,之前是这个样子,标红是新加一个方法,

大家看到有点像net/http,我们是参考它的做法,通过这样一个方法把上下文携带,这样既不会大面积破坏API定义的内容,所以参考的是HTTP的方式做的。覆盖的东西就多了,比如说日志库,上下文要取日志当前的环境(prd还是testing),当前的一些调度路由,APM信息等。Sync不是,这个Sync包和标准Sync有差异,我后面会讲,为什么在这里传递Context,我们Cache,Database,这个不用说了,集成一些中间件需要Tracing就需要传递获取。还有比如说通用的框架,RPC、HTTP一些基础架构的组件,很多是需要传递的。

显示传递大于隐性传递

第二点就是显示传递比隐性传递好,因为明确知道你仓里有Context,我就可以取东西。我非常赞同你们框架比较统一的时候,用一些类似像go generate的方式生成代码。

另外还有一个点特别强调一下,我们有一些框架的代码,有很多同学使用Gin,都会有定义自己一个Context,在自己的Context包了标准库的Context,这种方式我们不建议把这个框架里面的Context传到Service,因为有可能你的service不业务既包括HTTP结果,也会包含gRPC的结构,如果你强依赖是Gin的Context,我后面做一些改装就做不了,所以我们建议框架的Context不下沉,统一使用标准库的Context传递。在框架里面那个Context有一些特殊包装和模板可以内部使用,但是出了这个生命周期不建议再使用他。

Context覆盖这么多东西,我们要干什么呢?

第一就是全基础库覆盖超时,我见到很多语言比如说C++,他们Coroutine实现了异步网络编程的框架,但是一个非常重要的点没有考虑,类似Coroutine这种可以去很方便网络开发,但是他依赖的DB库存、readis库、Memercache库可能并没有覆盖到,这点一定要注意所有使用到的包要覆盖超时传递。

第二就是用了Context之后在root节点可以取消,这个是非常好实现的。

另外刚刚说了传递一些数据。我接下来讲一下我们传递什么东西。我们刚刚说了要控制超时,控制超时一般在哪里做呢?是在流量入口做的,比如说我们HTTP,或者gRPC设置入口超时。在基础库内部会统统记录这一个超时,因为Context有一个deadline还剩多久?通过这种方式你在每一层进入你的库之后当前已经耗时没有多少了,理念就是立马失败。还有一个注意我们利用Context这个传递超时,很多基础库的做法是这样的,开了另外一个Goroutine,那个Goroutine用了Context,你取消的时候把Pending的请求立马返回掉。我们知道有一些系统调用是不方便cancel的。所以要覆盖SetDeadline,在syscall请求前,判断超时传递剩下的quota,重新设置超时,再调用。

你利用Context传递超时,说白了就是一层层包下来,一层层取消,一层层传递,非常方便,这里面核心理念是什么?就是Goroutine的管控,它管控的是生命周期。

另外因为我在很多次分享讲过了,一定要做跨RPC超时传递,所以我们不仅仅是要考虑进程内全链的覆盖,也应该考虑跨服务级的覆盖。这样做比较简单,因为gRPC是天然就传递了这个Metadata,所以我建议大家一定认真看《SRE》这本书。

除了业务框架层面,一定要考虑我们的SaaS基础设施的平台,比如说我们公司用动态CDN加速,从节点回源核心机房,核心机房通过ELB/SLB,最后到达业务API网关,其实上面还有很多层,我曾经见到过很多业务开发同学只关注自己的层面,设置超时传递,上游还有很多基础设施,我希望大家可以考虑一下从边缘CDN节点到我们ELB机房核心的负载均衡的时候,从最上游传递。因为很简单。如果你的边缘已经认为超时了,你的下游还在倒腾处理,其实用户早就收到504,你就浪费资源做无用功,所以要全链路传递,包括我们边缘节点和CDN都要考虑。

讲完了超时,最后讲一下元数据的传递。

元数据传递

框架的拦截器要实现业务逻辑,比如统一拦截鉴权。通过一个token获取到用户的身份,或者用户ID,这个信息放哪里呢?这个东西在内部其实尝试很多方法争论过很多次,第一我觉得不适合直接放在PB里面,对于PB来说把用户ID放在里面,感觉是客户端传一个用户ID要怎么处理,实际上客户端传的是Token,这个放哪里传递呢?我们通过gRPC生成函数原形的时候,以中间件产生的数据不方便放到函数的仓库里面,只能放到context。你怎么知道获取他呢?这个问题我们争论很久了,现在解决方案是这样的。

比如说你提供鉴权服务的人,由鉴权服务的人暴露你的Middleware,使用鉴权的人要引用你的Middleware到HTTP框架里面。到我的业务逻辑层我怎么到上下文取得这个用户ID呢?你要到提供的Middleware的Metadata里去找(即FromContext是库OWNER提供)。这个依赖相对来说比较清晰,我的基础里面永远没有产生这种业务的框架依赖,而是把业务的框架他自己提供,但是由调用者引入进来,同时我知道这个Metadata一定是提供者去取,这个一定要强调一下,否则你代码依赖性和结构性不够清晰。

Caller我们也会通过Context传递。谁调的我的服务,我要分析,我知道整个流量大盘来自谁,但是Caller直接裸传,有一个坏处万一那个同学做恶,伪装成另外一个Caller怎么办?我们自己在逐渐升级做内网Zero Trust。之后在内网启用类似像RootCA这种证书的方式,像gRPC做签名识别对方身份以后,我们内网接口短期不做加通讯加解密,非常核心的接口要做加解密,有一定性能开销,毕竟走RSA还有AES。

另外我们查全链路追踪,这个需要传递。还有路由的信息,比如说Color还有Mirror,Color是染色,像环境里面就是区分环境路由调度(用于多测试环境路由使用)。Mirror就是影子的意思,通过这个标识容易实现全链路压测,很重要就是影子库,压测不能污染线上的数据,通过这个Mirror标识各个中间件传递,我就知道把这个数据应该写到另外一个地方,我们会传递这个标识。

还有我分享过我们gRPC的负载均衡的实现,我们传递gRPC Server一些信息辅助客户端做负载均衡调度的,比如gRPC Server 的CPU 或者 Load。

这里有一个问题。我们看了一下上下文传递无非就是两种,Incoming和outgoing,比如你做了HTTPServer或者gRPCServer,你调我接口肯定传元数据给我,哪一些元数据我需要Carry带到自己的上下文里面,这是Incoming。Outgoing是我作为一个调用者,我要调另外一个人的RPC或者HTTP接口,我要把哪一些东西发出去,这个非常关键。以前我们人肉编码传递非常麻烦。后来我们想了想怎么办?无非就是知道哪一些处理Incoming就是挂载当前上下文,哪一些Outgoing挂载框架上下文。我们会定义这样一个MAP会告诉你哪一些是要发出去,哪一些要进来,挂载上下文的。通过这两个MAP暴露两个方法,在我们内部框架库里面可以自动复循环拷进去或者传递出去比较方便了,不需要硬编码。

这个是我们梳理上下文的一些经验。

还有Goroutine上下文的传递,曾经有同学问,我在写框架代码的时候传递没有问题。但是有时候自己Go一个出去,但是发现原数据丢失了,所以我们内部的Metadata一定提供help方法,通过某种方式引导他,让他跨Goroutine的时候记得传递。比如说可以在Metadata这个包里面提供一个方法,把当前的Context里面的取出来做一个copy,Copy出来以后生成一个新Context,这个Context会用于跨Goroutine,假设要使用的话就把之前的原数据拷进去了,就不容易丢失,不然还得自己手动写这一行代码,容易出错。但是这种方式还是有可能会出错,Context经常容易传错,比如说HTTP框架里有一个Context,本来传递的时候开了一个Goroutine,结果把这个HTTP框架传到这个Goroutine,这个时候HTTP 结束,框架会调Context Cancel,结果go出去的Goroutine出现一个错 context cancel,这种方式确实容易出现,所以通过CI里面的一些脚本,尽可能检测是不是有传错,这个很难处理。

第二个方式就是防御编程。

我们提供一些让它启动Goroutine明确告诉他传一个什么参数进去不容易忘,内部发现因为开的Goroutine无非想异步做一个什么事情,所以把这个模型叫Fanout库,我们提供一个这样一个fanout库,告诉你一定要传,我会在内部自动进行一些Metadata的Copy,并且有全链路的Trees信息的一些挂载,生成新的Context再传过去。实际的函数原形不是像Goroutine一样,而是明确传参,尽可能减少Context传错,这样尽可能帮助它减少传错Context的保护。我们内部是这样考虑的。

No.2

Best Pratice

这是网上贴的列表,看一下上下文最佳实践。Background一般是TOP级别,root开启上下文一定是从它开启的,所有项目都是从Background派生的。第二有一些地方传Context你不知道怎么传,你不传,有可能崩溃了,我们很讨厌在传Context还看是不是nil,很烦。如果大家有看过谷歌开源一些代码比如说Cromie,很多C++代码不会看看那个东西是不是传 nil,因为大家编程意识和规范比较好,在最底层判断就行了。为什么提这个要求,不要传nil,不知道传什么的时候就传同步。

另外Context.Value不要什么东西都放进去,参数就是参数不是context value。

另外不要放到结构体里面,放到一个结构体里面,通过结构体访问到他们。你可以包含他,传那个结构体没有关系,因为一定实现接口,但是不要直接传递结构体作为参数。

Error和Context的知识就到这里,其实也偏一些业务开发或者是偏一些技术开发考虑的。

No.3

Conclusion

自己的总结,业务基础库开发没有想象中那么简单,虽然不像高大上的开发,但是当一百个人两百个人用你的基础库的时候,你的任何一个设计不当都会导致别人犯错,所以我一直觉得业务的基础库开发没有想象那么简单。所以我们一定要慎重思考你这个东西会不会导致别人会犯错。这里面有几个点。

第一,不要抽象非常复杂,认为无比可扩展。简单一点相对可靠一点,越简单越不容易出错。

第二,让每一个人正确使用。你设计好的一个函数原型才不容易犯错。另外你设计好了不要假设一定会用得很好,所以要定期看别人怎么用的你的API,比如说你设计的API有人瞎用,跟最初想的完全不一样,这个是要经常回顾看别人咋用的,你作为这个API OWNER要考虑。

第三,就是去大神化编程。因为所有人不是超过大神的很厉害的人物。我希望的基础库随便怎么犯错都不应该很容易挂掉,或者程序出现异常,这个是不好的。

第四,鲁棒性和健壮性是要考虑的,不要老想通过中间件,无比强大都怎么用都不会出问题的中间件解决所有问题。我们基础库也要考虑健壮性。

另外常常思考和进步,CaseStudy里面追溯问题一定要找到代码,为什么会这一行写错,是设计者设计得不好,还是使用者用错了,双向都要考虑,不要怪别人用错,有可能你东西不好用才会用错。

不断质疑自己的设计,不断参考一些优秀好的设计,反思自己基础库哪一些地方做得不好。

Q & A

提问:你好我请问刚刚讲到Context可以在跨服务间传递,应该里面涉及到Context上下文的拷贝,我想请问跨服务间的Context Cancel下游怎么知道是Cancel?

毛剑:这个问题非常有意思,我看谷歌SRE的时候就讲过这个问题,上一个已经失败了,下一个还在跑。第一个通过超时控制的,比如说配一个超时也能快速消耗掉。第二是主动Cancel,Grpc当你客户端调出Cancel是往下游通知把这个方法Cancel的。是一层层通过Grpc传递。曾经因为这个Cancel功能导致C++出现一个BUG堵死了,他是明确告诉你要Cancel掉。

提问:就是上一拨Cancel,也可以有Grpc请求是吗?

毛剑:是的。

提问:您好毛老师,看Ctx封装的时候有一个读传Ctx,FackCtx,是这个意思吗?这个函数在上游调用的时候是不是有问题?比如说实现的时候把两个Ctx变量不一样会用错,如果一样又觉得不太优雅。

毛剑:确实我们之前有同学一团名字,如果你的CTX非常强,一定会报错的。另外其实不建议取一样的名字。

提问:如果起不一样的名字更容易出错,比如说读第一个是C,第二是C,我调用的时候更容易出错?

毛剑:所以说我们内部CI的流程里面有针对这种函数的一些方法去检测,还是防御的手段。这种方式主要是鼓励他不要直接用上面一个人的Context,还是用很低级的手段检测,这个确实没有很好方法,容易写错。

提问:毛老师问一个比较简单的问题,Context时间表一般在Context里面放多少数据?有没有一个参考,几十个,几百个?

毛剑:Context如果直接调V,其实用的是一个新的,像联表关联起来,第一个找不到复循环找第二个第三个,所以你挂越多性能越差。一般会参考看一下Grpc的管理,他通过一个MAP,Context挂载是挂载一个MAP,那个MAP可以放很多个。

关联阅读:

对本期嘉宾分享的 Go 业务基础库之 Context

有何感想,请评论留言

24小时内点赞前3名的同学将获得

由比原链提供的

《Go 语言公链开发实战》一本

重磅活动预告

Gopher Meetup 广州站即将开启。来自小鹏汽车、腾讯、早安科技、PingCAP的大咖讲师带来 Go 开发领域的一线实践经验分享,尽在10月26日,小鹏汽车总部销售展厅!

报名请戳:阅读原文

Go中国

扫码关注

国内最具规模和生命力的 Go 开发者社区

请给我一篇 Go 工程实践干货 @ Go中国相关推荐

  1. 阿里卖家 Flutter for Web 工程实践

    作者:马坤乐(坤吾) Flutter 自 2015 年初次亮相以来,经过了多年的发展已经相当成熟,在阿里.美团.拼多多等互联网公司都有广泛的应用.在 ICBU 阿里卖家上 90+% 的新业务使用 Fl ...

  2. LDA工程实践之算法篇之(一)算法实现正确性验证(转)

    研究生二年级实习(2010年5月)开始,一直跟着王益(yiwang)和靳志辉(rickjin)学习LDA,包括对算法的理解.并行化和应用等等.毕业后进入了腾讯公司,也一直在从事相关工作,后边还在yiw ...

  3. flutter release 版本 调试_腾讯课堂Flutter工程实践系列——接入篇

    前言 课堂目前的技术栈是React Native + Hybird + Native,随着技术的演进多端融合的趋势越来越明显,而RN的弊端也突显出来,jsBridge性能不是最优,占用前端人力,定位问 ...

  4. C++ 工程实践:避免使用虚函数作为库的接口

    原文: http://blog.csdn.net/Solstice/archive/2011/03/12/6244905.aspx 陈硕 (giantchen_AT_gmail) Blog.csdn. ...

  5. C++ 工程实践(5):避免使用虚函数作为库的接口

    https://blog.csdn.net/solstice/article/details/6244905 摘要:作为 C++ 动态库的作者,应当避免使用虚函数作为库的接口.这么做会给保持二进制兼容 ...

  6. 工程实践:如何给变量取一个好的名字

    工程实践:如何给变量取一个好的名字 在上一篇文章中跟大家分享了关于函数命名的一些实践心得,今天我们继续命名这个话题,来讲一讲如何对变量命名. 以下是本文的目录大纲: 一. 变量命名风格 二. 变量命名 ...

  7. C++ 工程实践(7):iostream 的用途与局限

    陈硕 (giantchen_AT_gmail) http://blog.csdn.net/Solstice  http://weibo.com/giantchen 陈硕关于 C++ 工程实践的系列文章 ...

  8. 算法模型部署上线工程实践

    本文出自:https://blog.csdn.net/u012294181/article/details/54564391 本文由携程技术中心投递,ID:ctriptech.作者:潘鹏举,携程酒店研 ...

  9. 刘昊天:以数据思维助力工程实践 | 提升之路系列(十一)

    导读 为了发挥清华大学多学科优势,搭建跨学科交叉融合平台,创新跨学科交叉培养模式,培养具有大数据思维和应用创新的"π"型人才,由清华大学研究生院.清华大学大数据研究中心及相关院系共 ...

最新文章

  1. javaScript通用数据类型校验
  2. stm32 独立看门狗学习
  3. PHP处理跨域:header(AccessControlAllowOrigin:星)允许所有来源访问;后端Curl请求转发
  4. 自动判断PC端、手机端跳往不同的域名JS实现代码
  5. 21天学MySQL_SQL21天自学通.pdf
  6. java编码技巧_编码小技巧 让java编程更便捷
  7. 【广告技术】隐私集合交集运算结合同态加密,在保障数据安全的同时追踪广告效果
  8. 在appdelegate中 设置跟视图控制器 但是没办法全屏
  9. string的find( )函数✅
  10. Mac如何解压rar,zip等各种格式文件
  11. 用 Truffle 插件自动在Etherscan上验证合约代码
  12. 【肌电信号】基于matlab带通滤波肌电信号处理【含Matlab源码 965期】
  13. 【细胞分割】基于matlab GUI形态学算法红细胞计数【含Matlab源码 638期】
  14. 载硫酸庆大霉素PLGA纳米粒PNPs(GS修饰PLGA纳米粒)/cRGD修饰PLGA纳米粒的制备方法
  15. windows下编译64位x264
  16. centos执行yum命令报错,There are no enable repos
  17. HTML5游戏化互动学习平台,h5游戏平台_触摸型互动slg黄油手游
  18. 西门子1200PLC控制加KPT1200触摸屏,污水处理厂自控项目实例
  19. Jenkins_Docker
  20. 注册的业务、登录业务、个人中心、nginx配置【VUE项目】

热门文章

  1. Python3+pygame实现有趣好玩的飞机大战游戏(附源码及素材)
  2. 高新技术企业认定有什么样的条件?
  3. js当前日期向前推3个月时的日期
  4. 去除法定节假日以及周末,计算请假时间
  5. 技能梳理17@Rasphberry Pi 3B+stm32+dht11+lora+onenet
  6. 小米9,K20PRO基带未知/丢失IEMI恢复关联分区
  7. [盈利]移动APP盈利模式简述
  8. Field Status Variant
  9. 如何用4行 C 代码实现一个跨平台的命令行 mp3 播放器
  10. 去甲肾上腺素是什么?