饿了么分布式服务治理及优化经验
导读:高可用架构 7 月 30 日在上海举办了『互联网架构的基石』专题沙龙,进行了闭门私董会研讨及对外开放的四个专题的演讲,期望能促进业界对互联网基础架构的建设及发展,本文是兰建刚分享饿了么服务治理经验。
兰建刚,饿了么框架部门技术总监,前爱立信首席软件工程师,10 年以上高可用性,高并发系统架构设计经验。现饿了么框架工具部负责人,负责饿了么中间件的设计及实施,通过中间件以及研发工具的辅助提升研发人员的工作效率,提升网站的稳定性及性能。
今天我想站在一个大的角度上,看一下饿了么最近一年多的时间,经历的技术上一些痛苦的问题与改进的过程。
为什么讲比较痛苦的事情?昨天和一位专家聊天受益很大,他说人在什么时候能够自我驱动?就是痛苦的时候。 只有感到痛苦,才会有改变。 当然改变有两种结果,一种是彻底放弃沉沦,另外就是一想办法自动化、智能化,把自己变成一个高手。
MVP 原则
我现在也很痛苦,但是还没有放弃。先讲一下 MVP 原则,MVP(Minimum Viable Product) 现在比较火, 一个产品是做大而全,还是可用就行? 我从去年 3 月份加入饿了么,开始组建框架和工具的团队。中间件里面很多东西都可以去做,但是我真的需要把所有的东西都做全吗还是 MVP 原则就好?这是我们思考的一个问题。
MVP 的意思就是做一个最小可用的就可以,大家以前很流行说,“世界那么大,我想去看看”,其实框架很多东西看看就好,做全做好是需要长时间积累的,我们缺的恰恰是时间。我们要做的就是立足现状,解决痛点问题。现在饿了么的现状说白了比百废待兴好一点。 当有太多事情可以去做的情况下,更需要抓住重点,不死人的尽量不要去踏。
服务治理的现状
服务治理是一个很大的话题,它涵盖了很多内容,比如前面晓波老师介绍的 Redis 治理、姚捷老师讲的链路监控系统(参看文末文章),都可以涵盖在里面。
编程语言
先介绍语言,刚才会场一些人说他们是异构的语言,但可能还是没有饿了么这么复杂。饿了么语言主要有两种,Python 及 Java,原来整个公司语言都是以 Python 为主,可以说是上海最大的 Python 大厂。为什么不坚持用 Python?不是说 Python 语言不好,而是招不到人。在业务急速发展的时候怎么办?换 Java 语言就成了自然的选择。
在我进公司的时候,其实不仅仅是这两种语言,还有 PHP,C 语言等。基于这些现状,框架的选择点就比较少。因此做了一些妥协,SOA 的框架有两套,主要是为 Python 和 Java 做的,Python 的叫 Vespense,Java 版本的叫 Pylon,Vespense 和 Pylon 都是星际争霸里面的两种最基本的东西,没有这两种东西游戏根本打不下去。
SOA 框架
SOA 框架里面需要包含什么? 首先必须包含 RPC ,我们的 RPC 有两种:Thrift 和 JSON。Python 使用 Thrift,Java 使用 JSON。为什么 Java 框架重新选择一套 RPC 协议? 主要是觉得 Thrift 对 Java 不太友好。举个例子,用 Thrift 生成的 Java 代码在接口比较多的时候,它的一个文件就超过 20M ,连 IDE 都拒绝分析这个文件。另外 JSON 是纯文本的,因为当初也没有日志系统,也没有链路跟踪系统,排查问题的时候,一种好的办法就是抓包,如果是一个二进制的协议的话那就痛苦。所以最终 Java 选择了 JSON。当然 RPC 都是对业务透明的,SOA 框架会屏蔽 RPC 细节,业务就像使用本地调用一样使用远程服务。
路由我们是基于集群做的,没有进一步细化到机器级别,因为觉得这个就足够了。此外也做了客户端的 SLB,还有熔断、降级、限流,这是保护服务的几大法宝,充分证明了这些东西拯救了我们很多次。经常看到监控群里面说,我们把什么什么服务限流了吧,把它降级了吧,那是因为这个服务可能写的不太好,把它降掉了,保住我们的主要业务。还有一些埋点,全链路跟踪等等,全部都内嵌在框架里面。这样的好处就是, 增加特性只要升级一个框架就可以了 。
服务发现与配置中心
我们的服务发现和配置中心叫 Huskar,这是 DOTA 里面的一个英雄人物的名字,它是基于 ZooKeeper。
负载均衡
负载均衡有好几种, 首先有嵌在 SDK 里的软负载 ,拿到一个服务全部的列表,做一个轮循就可以了。这种策略有一些不足,比如一个 IDC 里面机器型号性能可能会稍微不一样,如果单纯用轮循会产生负载不均的问题。但这个问题当前还不是最紧迫的,我们可以绕过去,只要保证同一个集群里机器型号都是相同的就好。
此外也用中间层的方式,以前我们的 haproxy 用起来比较麻烦,配置复杂,而且它不能进行热加载,一个配置上去了之后需要重启一遍,因此工程师就用 Go 语言写了一个 GoProxy,基于四层,它会从服务中心把你需要的列表全部抓下来,做四层的负载均衡。他可以代理一些没有 SOA 框架支持的语言写的服务,也可以代理其他的基础组件,比如说像 Redis,数据库,它都会代理。
CI / CD 灰度发布
我们有 CI、CD 灰度发布的系统,叫 eless,虽然我们做了 CD 但是百废待兴,目前只有一些基本的单元测试,因为这个太耗工夫了。灰度发布也是基于发布系统做的,我们会在发布系统里面定义集群,每个集群里面的机器又分成不同的组,发布的时候按这些组来发布的,你可以先发一个组,观察没有问题后,然后再发其他的组。
监控与报警
我们有自己的监控和报警系统。 监控现在做的比较简单,是 statsd + graphite + grafana 的组合 。现在最大的问题是监控系统不支持 tags,所有的指标汇聚到一起,一个服务的指标是汇聚在一起的,一个机器或者集群慢了,它会把这些指标分摊到其他机器或者集群上去的,所以查的时候比较困难,所以我们现在准备切成我们自己的系统。
报警系统也是自己写的。 报警系统的需要是快、全、准 。我们现在做的是全,逼着大家去把报警系统用起来,只看监控系统是看不过来。如果线上发生了一个故障,比如交换机发生故障,影响到某个业务,但是业务报警没有报出来,那业务要承担连带责任,因为你没有报警出来。
报警最常见的基于阈值,阈值这件事情比较痛苦,我们有很多指标,但这个阈值怎么去配,需要很有经验的人才能配好,阈值配小了,你会经常收到报警,配太大有可能出问题收不到报警,这个非常痛苦。所以一个同事提出 基于趋势 来配置来判断,我们在一段时间发现趋势在偏离了,就做报警。
我们也在做 trace,前两天终于把拓扑图给画出来,把一个业务所有的调用展示成一个调用树,这样就可以很好的分析业务。我们现在是一个近千人公司,业务系统极度复杂,很少有工程师能清晰说出一个业务到底调用了哪些服务,通过这种 trace 方式来做辅助分析就很有用了。
光展示,我们觉得它的价值还没有利用到极致,可以把所有的调用关系和报警结合在一起。大家分析问题的时候,会发现如果某个点上发生的错误一直往上报,从而导致整条链路失败,那这个点就是 root cause,把它修复就可以问题解决了。我们做 trace 的思路是一样的, 在调用链上进行着色,辅助找到问题的 root cause ,也就是最初发生问题的那个点。
服务治理的经验
我们最重要的经验是做好保护与自我保护。
“不能被烂用的框架不是好框架”,这个是我们 CTO 经常说的一句话。原因是什么?比如我们那个监控的 SDK 曾经被业务错误的使用,每发一次报警就启一个新线程,最后整个进程因为开了太多的线程挂掉了。峰哥(CTO)说你无法预测每个开发者会怎么使用你的框架,即使框架被滥用了,最坏的情况,也需要保证能够活的下去。
所以后面写的东西都严格要求自我状态的检查,比如秒杀的时候,所有的监控系统,链路跟踪系统都是可以降级的,不能因为这些东西导致整个系统崩溃。
框架由于在底层,出了问题最容易被怀疑。比如一个 SDK,使用方说为什么占用了整个集群上 8% 的 CPU?跑过去一看,整个机器的 CPU 才 12%。某种程度做框架其实有无助的时候,容易被质疑及谴责,所以做好自我状态检查是很必要的。
定期线上扫描
为了避免滥用的问题,我们会定期线上扫描。比如一些日志本来就是可以降级可以丢的,但如果开发用了写文件的同步方式,那性能就会变慢,通过扫描发现这些问题,改成异步日志服务性能就会更好。
限流、熔断、降级
这个强调多少遍都不过分,因为确实很重要。服务不行的时候一定要熔断。限流是一个保护自己最大的利器,原来我们前端是用 PHP 做的,没有自我保护机制,不管有多少连接都会接收,如果后端处理不过来,前端流量又很大的时候肯定就挂了。所以我们做任何框架都会有限流措施。
有个小插曲,我们上 DAL (数据库中间件)第一版的时候,有次一个业务怎么指标突然降了 50%,然后大家去查,原来 DAL 做了限流,你不能做限流,你把它给我打开,听你们的我打开了,打开了然后数据库的 QPS 瞬间飙到两万,业务部门就慌了,赶紧邀请我们再给他限制住。这是我觉得 DAL 做的最好的一个功能。
连接复用
还有连接复用,有些工程师并不能特别理解,如果不用连接池,来一个请求就发一个连接怎么样?这样就会导致后端资源连接过多。对一些基础服务来说,比如 Redis,数据库,连接是个昂贵的消耗。所以我们一些中间件的服务都实现了连接复用的功能。
代码发布
上线发布是很危险的一件事情,绝大部分的事故都是由发布引起的,所以发布需要跟很多系统结合起来,所以我们做了整套流程。在每次发布的时候,一个发布事件开始,到我们这个监控系统以及调用链上,调用链就开始分析了, 发布后把它的所有指标比一比,到底哪些指标发生了改变,这些指标如果有异常,就发报警 ,所有发布都会打到监控的主屏上面去,如果出了什么问题,这些事情优先回滚,如果可以回滚,我们肯定第一时间就把问题解决掉了。
服务治理的痛点
配置复杂
超时配置:超时配多少是合适的?100ms?300ms?极端情况有些业务配到 3 秒的。阈值怎么配和超时怎么配其实是同一个概念,并不是所有的程序员都知道超时设成多少合适。那怎么办?峰哥(CTO)想了一个办法,你的监控系统,你的调用链分析系统,你的日志系统,基础监控系统每天产生多少数据?这些数据到底有没有用?是否可以从这些数据里面挖掘出一些东西,比如这种超时的配置,是可以基于它历史的超时配置、所有请求的响应时间去做的。
这件事情正在进行中,但落地有点麻烦,比如说我们请求大概每天有三千万的调用量,你只有很小的一个比例它会超时,但是绝对量是很大的,你设置一个超时值,它可能有三万个请求都失败了。现在一直在优化这个东西,希望下次大家来我们这里的时候,能给大家详细介绍一下这个超时到底怎么做。
线程池配置:刚才说最重要的是限流,你不可能无限制的接受请求,不可能一百个并发你就接收一百个并发,并发到底怎么配?又是很复杂的事情。我经常看我们线程池的配置,这个东西要经过严格的性能测试,做很多调整才能调出来。在做性能测试的时候,其实有条曲线的,有个最高点的,我们在想在实时的过程中计算出这个最高点,但发现这个东西其实挺难的。
我们便用了另外一种方法, 每个线程池用一个排列队列 ,当我发现它在涨的时候我就适当把那个线程池扩大一点,同时我们监测其他指标。如果发现在我扩大并发量的时候这些指标产生了报警,那我就把这个线程调整的操作直接拒绝掉,就保持原来那个大小。如果这些指标证明是没有问题的,那我就把它再扩大一点。
Cache,DB,Queue 的手工配置问题。还有一个是服务治理,Redis、数据库等配置还都是手工的,我们也不知道我们线上有 Redis,怎么办?我们正在做基础服务的服务化,业务其实不需要关心到底连到哪个 Redis,你上线的时候你告诉我你需要多大的容量,填个工单过来,运维帮你配好了,然后通过一些自动化的方式你把这些拿到初始化 SDK 就可以了。
故障定位
还有一个比较痛的问题就是排查问题很难。首先故障定位困难,每次我们出了事情之后,大家各自查各自的,比较低效。问题排查其实是有方法可以做,需要把它自动化,我们现在还缺这个东西,调用链分析是需要考虑去做。
性能退化
我们现在的业务增长量非常恐怖,去年我们是以 5 倍的速度增长了,但其实这个 5 – 10 倍要看你的基数,当基数很大,扩一倍量也是非常多,评估线上到底要布多少台机器是一件很复杂的事情。
我们成立一支性能测试团队,做全链路的压测。基于全链路压测的结果来评估整个系统的容量。这个全链路只能在线上做,也不能在白天压,只能在晚上低峰期的时候做。所以性能测试也是一个比较挑战的工作,不仅仅是智力上,也是身体上的一种考验。
全链路压测试一些服务有时候出现性能下降,比如 QPS 从 500 下降到了 400,但大家并不知道最直接的原因。上次毕洪宇老师也帮我们出了主意,比如把全链路指标拉出来做一下对比,看看哪些指标有变化,可能就是罪魁祸首。
容量评估的方法
容量评估方面容易出现温水煮青蛙的事情,今天流量增长一点没问题,明天再增长一点也没有问题,连续几天然后服务就挂了。这个时候怎么办?只能用最苦逼的方法,找个性能测试团队进行压测。有没有更智能化的方法?我们也正在探寻这条道路。
资源利用率低
资源利用率低的问题。很多团队一碰到性能下降,就希望扩容,这会导致很多时候机器利用率只有 10%(更新一下数据,其实很多服务器的利用率不足1%)。我们正在积极准备上容器化方案来解决这个问题。
服务依赖不清晰
大的系统中,服务依赖的调用链相当复杂,一个业务下去到底调用了哪些服务比较难说清。我们已经在做一个泳道规划。泳道这件事情有多种说法,有的人喜欢所有的服务都做一个大池子,只要保证它的足够容量就可以了,但是我更倾向小集群的思路,因为隔离起来就会更安全。
我们现在还没有做到按用户来区分泳道,目前只是按流量来切,50% 随机,部署的东西都一样。我们想通过泳道把这些流量隔离,VIP 客户可以把他放最重要的泳道里面,一些不那么重要的城市,可以放到另一个集群,如果不得已降级,只能牺牲这些次重要的用户。
服务治理的方向
有痛点就要努力,要么放弃,要么努力,这是我们努力的方向,前面讲了一下智能流控系统,超时推荐我们也做,大数据和智能化才是将来。有些监控数据只是落在磁盘上不用那就是浪费,是不是能把它利用起来?
然后我们也在做 Cache、数据库、Queue 等服务化。
Trace 系统我们也在做,拓扑图画出来,帮助大家了解是怎么回事,我们可以做链路染色,帮你了解问题的根源在哪里,我们也可以做依赖度的分析。我们说依赖分两种,强依赖和弱依赖。弱依赖要处理它,有一个异常出来的时候要把它干掉,不能把这个异常跑到最上面去,那整个服务就都挂掉了,但是大家并不知道到底它是弱依赖还是强依赖,这需要分析,我们去统计一下,它是一个强依赖还是弱依赖。然后弱依赖就可以做一些改进,比如做一些异步调用,节省整个服务的调用时间,优化用户体验。
容量预警我们通过做一些大数据的分析,所有的指标跟订单量这些关联,做一个相似度的分析,当这些指标偏离的时候,我们是不是可以认为它的容量有问题,当然这是努力的方向。
容器方面我们也在做,系统叫 APPOS,有的服务 CPU 只用了10%,但是我们规定了一台服务器只能装一个服务怎么办,那就上容器吧。
Q&A
提问:想问一下报警阈值设定,上面提到的方法是根据趋势来设计报警,这个趋势其实您能做到多少准确?能控制在什么情况下?
兰建刚:准确度的确是个问题,我们用了一个算法叫 3-sigma,准确度还不是特别确定,因为这个东西真的是服务治理里面最大的难题,报警分级怎么分?这是很大的学问,我们现在整个报警系统里面报警通道每天上千个报警,很多都不看的,因为觉得这个报警没什么意义。这是一个实际当中要去调整的问题。
提问:报警存在误报的情况?
兰建刚:对,你要知道我们的业务有两个明显的尖峰,十二点和下午四五点的时候都是订餐的高峰,之后则所有的指标都会有下降趋势的时候,如果你曲线偏离的很厉害就会引发报警。
多指标聚合是我们正在做的,发生一个指标报警的时候可能是一个小问题,但是这个问题会触发一个 CEP 的流程,比如“是不是 CPU 飙高的同时响应时间会抖动?”,我们可以定义这样整个一套规则,去做报警来提高准确度。
提问:刚才您提到用 Go 写的一个东西,为什么选择使用 Go 因为 Go 是自己的垃圾回收机制?而且我想了解一下您认为 Go 它是比较适合什么样的系统?
兰建刚:当然是底层的基础服务了,我们不建议用 Go 写业务。为什么我们选 Go 做工具,是因为我前面提到,公司原来一些工具是搭在 Python 上,有一帮 Python 工程师,让他去写 Java 他是绝对不干的,但是让他去写 Go 语言是没有问题的,Python 其实不适合写底层框架,因为它是个动态语言,工程化方面也会差一点。
提问:刚刚看到您提到超时配置推荐,这块是和你们的应用场景有关系,还是说和你们遇到的故障?
兰建刚:就是因为遇到故障了,因为很多超时配得很乱,有的同学直接配 3 秒超时(这是配置模版里的一个例子,很多同学就拿去直接用了),那还不如不配,有些情况很多服务就是 10ms 就正常返回了。只要保证这件事情对绝大部分的服务来说,是有利可图的,那我们就去做这件事情。