前言
在 4 月 27 日举办的 Gopher China 2019 中,来自腾讯 Tars 团队的核心成员陈明杰进行了一场题为《Tars Go 性能提升之路》的演讲。陈明杰,负责腾讯容器云平台及机器学习平台的建设和运营,目前专注 Tars 开发框架的 Golang 版本开发。
以下为演讲实录。
No.0
前言
今天给大家带来微服务的解决方案 Tars 以及过程中的性能优化和容器解决方案里面的一些问题和经验。Tars 是将腾讯内部使用的微服务架构 TAF(Total Application Framework)多年的实践成果总结而成的开源项目,是基于名字服务使用 Tars 协议的高性能RPC开发框架,同时配套一体化的服务治理平台,在腾讯内部已经使用了十年有余。
今天的分享主要分为以下七个部分:
Tars 是什么
Tars 协议分析
Tars 架构体系
TarsGo 的由来
TarsGo 解决了那些性能问题
Tars 应用案例
相关问题以及回答
No.1
Tars 是什么
Tars这个名字来自星际穿越电影机器人Tars,电影中Tars有着非常友好的交互方式,任何初次接触它的人都可以轻松的和它进行交流,同时能在外太空、外星等复杂地形上,超预期的高效率的完成托付的所有任务。拥有着类似设计理念的Tars也是一个兼顾易用性、高性能、服务治理的框架,目的是让开发更简单,聚焦业务逻辑,让运营更高效,一切尽在掌握。
在基础设施方面 Tars 是支持物理机,也支持虚拟机以及容器。在协议方面我们除了支持本身的 Tars 协议之外,还支持TUP、SSL、PB、Http1/2以及自定义协议。 Tars 协议是类似于 protocol buffers 的一个协议, Tars 已有将近10年的大规模应用经验,比谷歌的GRPC开发出来的还要早。
在调用方式上,现在是支持几种调用方式一个是同步,一个是异步,一个是单向。同步调用是比较常见的调用方法,第一个是我们同步调服务端返回我们做后续的处理。另外我调完之后写一个回调处理类,让服务端返回结果后会调我的回调处理类去处理后续要做的事情。还有单向调用,比如说监控上报,我不需要服务端给我反馈成功或者失败,因为我不想因为它拖垮我的服务,它会影响我整体的流程。所以说单向调过去不需要服务端给我返回。
在服务治理方面, Tars 除了支持常见的微服务的治理方案就是服务注册发现、负载均衡、熔断、服务配置,我们还支持日志的集合、自定义监控,还有分布式追踪这一系列的微服务的能力。我们还提供了一个OSS平台,OSS是tars的管理平台。在语言方面最早的版本是C++,C++程序用了很多年,从QQ第一个版本到现在内部基本上都是C++。现在慢慢的大家语言越来越多,像java,在之前大概也有四五年的历史是用java在做的,不过,最近这几年大家重心都转向了GO。C++或者java的程序员很多都转向了Go,还有一个DEVOPS平台,devops平台就是包括代码管理、代码编译、自动化测试、持续部署、推动发布的这些能力。
No.2
Tars RPC协议分析
我们看一下 Tars 协议是什么样的协议,首先,我们也有一个struct,后面跟着struct的名字,前面第一列0和1是type,每个字段排一个type,type只能是增长的,比如说0、1下面可以定义9,但是不能反回去0。接下来字段就是require对应这个字段,必须要传过来。optional是可以传也可以不传。
接下来是定义interface,interface会包含函数,比如说这个函数test,它定义两个结构体,第一个结构体返回登录信息,第二个结构体返回用户,参数就代表一个返回。生成的代码其实像右边那样,可以生成一个相应的接口代码,用户不需要关心代码具体是什么。如果作为客户端只需要把参数传过来生成刚才的结构体,把结构体的值传进去调用一下就可以返回结果,然后作为客户端是这样,作为服务端需要实现接口。不管是什么语言,基本上java、C++还有Go都可以用一个工具将tars协议的文件转化成对应的接口文件。
No.3
Tars 体系架构
服务治理--整体思路
我们看一下服务治理整体思路。我们客户端去获取对应的IP端口,然后解析完之后就做调用服务端,客户端在调用的时候用策略,名字你可以用哈希或者权重都支持的,服务端每个端都有一个node,我们叫tars node。node用来监控服务,起停、监控、发布都是通过node而实现,甚至心跳上报因为一台机上课都会有一个节点多个服务,如果每个客户端都是请求node的话,那主控的压力非常大,因为线上的节点都是几万几十万的级别。所以,我们客户端是支持熔断策略的,我们在调用服务端的时候发现这个节点挂掉或者网络终端导致的一些问题,可能再连续一段时间失败之后就可以把它屏蔽掉。
下面是我们的基础服务集群,包括监控、配置中心、日志聚合、分布式跟踪的基础服务,这些服务都是用tars编写的,然后会提供相应的功能,通过整体的协同工作来完成整体的一个服务的透明发现、注册等基本的服务能力。
自动区域感知
另外服务治理方面的自动区域感知,自动区域感知在线上使用是很有必要的,我们在腾讯内部有很多机房,每个机房可能自己的机房和机房之间会跨区,这个机房之间tars比较高,那你想就近调用怎么办?一种是部署两个服务,这两个服务控制它同一个机房,这时候会带来运维的麻烦,我们得选一个机房,万一这个机房没有机器呢。所以我们实现自动区域感知,在后台去配置每个机房每个网段,如果客户端需要在哪边启用自动区域感知的能力就可以通过名字服务获取,获取的时候会优先返回本机房返回的节点,如果没有再考虑返回其他机房,我们减量减少跨机房带来的延迟的增加。
分布式跟踪
服务治理还支持分布式追踪,分布式追踪我们现在是用zipkin去做,每一个请求会分为四个阶段,每个阶段最终把生成的span发给zipkin。
OSS--运营web化
对整个tars平台支持oss,包括open API可以定制自己的oss系统。我们看一下tars监控页面,我们可以监控当时的总流量,比如说每个时间点调用量是多少,对比一下有没有上涨。耗时跟昨天比会不会上涨,这样我们可以清晰地看到服务的状况。这几个需不需要去监控呢?我们是不需要监控的,每次上报的时候都会把监控信息上报给基础服务,这个基础服务就会把这个数据在页面上做展示。
说一下服务配置,服务配置其实分为好几种,一个是应用级的配置,我们有一个应用tarsAPP,还有Set应用级别的配置,还有节点级别的配置,每一层配置都会应用也可以做集成,这样方便用户定义不同级别的配置文件去做应用,这样方便自己在不同的服务之间做配置的共享。
还有就是服务的发布管理,我们可以在页面平台上自己做服务管理,做服务的发布。在页面上发布的节点,可以看一下服务当前的设置状态。设置状态表示这个服务是想让它开起来还是关闭,你设置。这个状态就是比较重要,当前是活的还是死的,如果没有上报,那这个节点就现实挂了,可能心跳上报超时这时候就会出现这种情况。我们会对这些状态进行一个监控,不同的状态之间其实是可以做监控的。
在服务管理的页面,除了常规的一些操作,比如说服务的上下线都会操作,另外服务的重启、停止还有服务的身份管理都可以通过tars管理平台进行操作。我们还可以将这个节点去设置推动流量,比如说一个节点想要无损的下线掉怎么办?我们可以设置这个节点的流量状态,你可以把它设计灰度流量,只有10%的流量,这样方便点。比如说你发布一个新的功能,你想在这个节点上做一些测试,不想在所有的服务上升降,你可以设计变成灰度流量,这样就方便做功能的验证。我们还支持无损的重启有两方面,一方面是进程可以做到无损重启,我们怎么做?我们现在是通过名字服务把节点设置成请求流量,这样就不会过来了。我们要等待一段时间把队列中的请求把它给向后来的某个节点,当队列中的请求给向后来的这个节点就变成无流量,把这个无流量剔掉这是最常规的做法。我们现在支持另外一个做法,针对每个都要重启的进程,就是通过PID,通过一个进程将前一个进程的描述图寄存过来,可以等到把前面的进程处理完,然后再把前面的进程杀掉,通过新的进程PID去做处理。这时候就达到重启的目的。这种做法很常见。
我们还可以看到管理平台,除了刚才说的操作命令之外,所有的操作记录都会在这边,甚至服务可以上报你自己想要上报的东西在这边做展示。
No.4
TarsGo 由来
我们说完tars整体的研发背景,我们来讲一下GO,为什么研发GO?腾讯在C++用的很广也很久,大家都面临一个新的问题就是我要上手,我一进来要学习C++。做C++的培训周期非常长,一个硬件生进来花一个月或者一两个月的时间才可能慢慢上手服务。如果转变成Go就很快了,一个程序员进来基本上培训1到2周就可以写代码,这是一个场景。
另外微服务的迭代节奏非常快,就是我们现在发布频率很高,一天发布几次都有可能。所以Go是很适合快速微服务的迭代交接的。Go的编码风格比较同意,不管是新人还是老人,大家写的编码看起来差不多,不管是时间方式上还是语言风格上都是好处。再一个是高效,我们刚刚说的腾讯云里面很多做容器化都会应用很广,我们转成Go可以一起解决面临的问题,因为现场使用的时候腾讯都会做很多改造不可能直接拿来用的。
还有就是开发了Go另外的一些优点,包括编译速度,还有提供的代码非常多,还有性能工具像PPROF都可以做到Go的版本里面来。
刚才说的Tars2Go工具,Tars2Go就是根据tars文件做解析生成相应的词法,根据语法再进行代码生成。我们现在用GO写,用Lexer协议代码的解析,解析完生成相应的接口文件,这是Tars2Go工具做的,每个语言都有自己的工具。pacakge tars可以分为几部分,一个是客户端,接下来是服务端,我们要管理很多个服务端,就会通过多级的管理节点来去管理服务,每一个对应到服务端的接口,通过发送队列之后通过下面的broken做处理,然后发给服务端。它接送到请求之后,也会塞到一个接收队列里面,然后接收队列里面再分一些成此。不管是在长连接场景还是短请求的场景,这种都比较通用,但是我们发现如果用一个请求,在短请求的场景其实是很浪费资源的,它创建虽然足够轻量,但是你在短请求里面很快的创建销毁,耗的CPU非常多。接下来是库,这是我们常用的库。
No.5
TarsGo 解决了那些性能问题
Tars 编解码优化
我们讲一下编码优化, Tars 在早期的时候比较注重功能性的开发,后面我们慢慢的做性能的调优。最早我们在 Tars 的编码上用了反射,反射确定结构体的类型,我们发现反射的性能相当差,早期我们是用C++去写 Tars 工具,我们把 Tars 的文件转成Go的接口,用了大量的反射后来就改了。和前面的分享同学说的 Lexer 一样的,做纯手工的编译器,把它转化成全Go的工具。虽然是去掉了,后面改成了用指针的方式做传递,减少了参数的拷贝。
Timer 优化
Timer的优化,我们每个请求要设计超时,要创建一个Timer,Timer做解决的时候发现这个Timer耗时怎么怎么高,可以看到这里面的耗时基本上达到30%,因为每一个请求要创建一个Timer,不断地创建销毁,最早去社区,最早都是Go里面的东西,社区里面也有找到一个问题发现他们自己在多CPU的场景下Timer创建销毁的性能比较低。后来他们那边的实现,就是每个CPU给自己设置一个Timer,我们也做相应的升级性能也得到很大的提升,升级到1.3以上的版本。基于时间轮算法,每一个轮像始终一样慢慢偏移,偏移到Timer里面就做到这个时间点下的处理,这样就可以避免销毁Timer的过程。
Net包的SetDeadline调用性能问题
另外是SetDeadline,我要去设置一个超时这时候发现性能比较低,我们不可能不设,不设置的话可能会出现一个问题。最开始我们通过SetDeadline获取文件描述符,通过SYSFD去改,然后通过反射获取,反射性能很差我们就放弃了。另外是通过修改GO的源码获取socked fd修改Go源码。第三种通过SYSFD设置socket的读写超时的方式。这是当时解决之后的情况,基本上SetDeadline操作提升30%甚至60%,当时跟SetDeadline做沟通把这个一起升级了,可以升级到1.11以上的版本去体验一下。
Bytes的Buffer带来的性能问题
另外是Bytes的Buffer带来的性能问题,在编解码的时候我们用Bytes的Buffer做流的缓冲,但是Bytes的Buffer有一个问题是在搭包的时候需要不断地控这个Buffer,当Buffer不断增大时,底层的Bytes slice需要进行扩容,频繁内存分配导致的 gc性能下降,这就导致耗时增加。那我们怎么解决?现在用sync.pool的方式解决,另外不同的sync.pool,你要选择自己合适用的大小来缓存,除了前面讲的那些还有一些其他的优化,像协程池、和TCP的优化,还有纸张传递和字传递,还有很多的chan传播会带来性能的损耗。
看一下压测的结果,我们当时做了一些性能压测在优化之后基本上Go是仅次于TarsC++的版本,性能比没优化之前提升了5倍。
其他优化
我们面临其他的问题,就tars在腾讯内部也有十几年,我们在运维的管理方面其实是有很多经验的,就是微服务的管理有很多经验,我们面临的几个问题。第一个就是微服务很多,每一台机器的配置非常高,现在机器配置不断地越来越高,对开始是千兆服务器,另外是CPU不断地上升,现在都变成了五十几个,现在线上最大内存100多G都有。不同微服务不只在一个机器,一台机器部署很多微服务,部署不同微服务的时候依赖的东西不一样,大家相互干扰,你做了这个版本的NCC再装另外一个版本的NCC就冲突了。
再一个问题是资源浪费很严重,我们在线上做统计发现80%的微服务小于1核,这里面80%里面的80%,连0.1都用不到,这个微服务小到这种程度。这个时候就会造成资源浪费,为了解决这些问题我们引入了容器。底层可以分为几个部分,一个是tarsGo管理tars的发布和部署,后来叫docker node,做容器的创建、销毁,这是tars底层的变化,所有东西都塞到容器里面。为了解决容器的问题,docker node这个参数能控制CPU的多项时间,分为十万微秒,你可以在十万微秒里面分到多少,最小力度是可以扛到十万微秒非常好用,我们用它限制核数,我们可以把它设置0.1核,在10万微秒里面可以用1万微秒,我们这样限制它,通过参数就可以达到刚才说的问题。
网络模式没有用到太多的各种各样SDU网络,我们用pos网络解决问题,但是pos网络所面临的端口冲突,所有的tars服务端口都是由我们去分配的。业务可能也不用关心,因为我们通过民智服务的方式去做打通,用户不用关心自己部署在哪个节点上。
主动调度和被动调度
除了这个之外我们做了很多调度,调度主要分为主动调度和被动调度,每个调度都是通过监控数据上报,我们所有节点会遇到这样那样的问题,比如说机器挂掉、网络中断,我请求上来了容器资源不够用了怎么办?肯定要用自动化解决问题,我们做了自动调度弹性伸缩。还有可能这个节点刚开始活动或者刚开始的时候用户量很大,资源用的很慢,过了一段时间这个活动下来了,会空出很多CPU我们不能浪费。所以我们主动调动来解决这个问题,可以看一下主动调度。
主动调度包括缩容、降配、部署调优、离线调度、巡检清理。缩容就是我们服务刚才说的CPU利用率下来我们主动帮你做负载的降低,降低服务的能力;部署调优,我们面临的最大的问题就是装包问题,这个节点这个服务需要很大的内存,另外一个服务需要很大的CPU。耗CPU和耗CPU的在一起,那CPU就成为瓶颈,耗内存的跟耗内存的部署在一起内存就成为瓶颈,就面临装包的问题,这时候就会进行一些部署调优,考虑一些装包把它做调整,达到CPU和内存均衡的情况。离线调度就是生产离线容器,我们刚才说的tars是线上服务,线上服务很明显存在波峰波谷,是晚上,晚上用户量很多。晚上我们很多离线服务天天在跑,网上这些需求特别多,这时候可以把时间给算出来,然后去生产离线容器。
另外我们还进行一个超卖,这个容器总共8核,我们可能会卖出10个核,因为知道所有的情况大家用户量很多会超额收钱,所以我们会做一定的超卖。超卖怎么解决服务间的影响的问题呢?被动调度要做的事情就是高负载,这个机器如果说CPU内存、磁盘、网络这些没有隔离的东西用的比较高,就可能触发高的被动调度,帮你把服务调走。还有故障,故障是很常见的,还有业务告警,包括我们说的结构上的失败率、超时率都会做相应的一些被动调度来触发服务的迁移。还有服务的空闲,我们机器上会做转硬线,一旦一个节点上会申请支援,我们在空闲的地方多分配支援给它做一个硬线,你正常超过软线是没有问题的。正常的说你短暂的超软线是没有问题的,在硬线触发以后就会被压回去。由于超卖问题导致整机的负载高,这时候就会触发机器级别的高负债做预警做调度。
标签系统
还有就是标签系统,我们现在面临很多问题,我一个服务要不要跟什么服务部署在一起,一个服务需要什么样的机器,一个服务需要什么样特殊的配置,这就需要标签来解决。
大规模实战中的镜像传输
另外,大规模实战中的镜像传输,以前肯定仓库可能会保存层文件,每一层都是一块文件,我们做存储。虽然是微服务但是有上百万个节点的情况,如果做镜像的拉取就会导致仓库压力非常大,耗时高,请求失败。这时候我们用Golang编写了一个http proxy,通过http proxy的方式,所有的请求都会被Ceph做劫持,劫持完之后我们做P2P的加速,对每一层做加速,我们所有的镜像都不是通过每一层存储,都是我们自己去填每一层,然后计算每一层的体积,然后存到source里面去,都可以绕过每一层的仓库去解决问题。
Tars On Kubernetes
接下来我们推出K8s,我们怎么通过K8s把节点,因为当我把管理的权从刚才说的tars交出去之后,由K8s去创建管理权,那我们一个节点如果有TOKEN被迁移了,那我们怎么保证节点的变化、资源信息的变化,我们通过K8S的获取机制进行通知。我们今年上半年会把K8S的方案推出来供大家使用。
No.6
TARS 应用案例
Tars 应用方面,Tars 在2014年就开始开源了,现在到2018年6月份给了LINUX基金会,现在有很多的公司都在使用Tars。
Go语言开源地址: https://github.com/TarsCloud/TarsGo
Q & A
提问:我们容器做的时候,容器网络怎么处理的?
回答:容器网络是通过host模式,host模式唯一问题就是端口冲突,我们通过管理平台的方式注册不同的IP端口,把它上报到主控上去,用户不需要再关心IP端口,所有都是通过noed管理。
提问:启动的时候是自动检测的吗?
回答:是提前分配好的,通过监听什么端口都是在配置文件里面的,不有用户关系,你点扩容的时候就会帮你扩。
有疑问加站长微信联系(非本文作者)