本文来自阿里沐剑老师的分享。
写在前面
大家好,我今天分享的题目是《高可用实践:从淘宝到上云的差异》,取这个标题是因为会涉及到两个方面内容,一方面以淘宝为例子,传统的 IDC 的时候,我们稳定性是怎么做的,另外在云计算背景下,有很多创业公司是基于阿里云这样的公有云基础设施做研发,在公有云的环境下怎么做好我们系统的高可用。
我的花名叫沐剑,2011 年加入淘宝做评价系统,2012-2015 年在店铺平台,负责店铺的前台浏览系统和后台的 RPC 服务,以及一些性能优化、双 11 保障的事情。到了 2015 年开始到了 TAE 团队,开始负责云端架构及整体高可用方案,TAE 的升级版 EWS 现在也在聚石塔上面帮大量 ISV 和创业公司解决运维部署、自动化监控和性能分析等等问题。
去年我是作为阿里商家事业部双 11 作战项目研发的 PM。2017 年我开始接手商家营销团队。在阿里五六年的经验,其实就做了几件事,比如连续五年参加了双十一的核心备战,然后像去 IOE、异地多活,全链路压测、安全混合云、容器服务等项目参与设计和实施。
首先我会从淘宝店铺角度分享,以前在店铺是怎么样做双 11 保障的,后面是一些公有云相关的内容。
淘宝店铺稳定性体系建设
这是一个淘宝的店铺系统,这套系统是一个非常典型的高并发的浏览系统,在前几年的双 11 峰值有 20 万次的 Web 页面请求,平均一个页面对应了 20 次的 RPC 调用,这个时候对于整个系统的集合来说每秒的 QPS 是 400 万次,这中间就会涉及到缓存、数据库以及其它二方的 RPC 调用,对于这样的系统来说,在性能、稳定性和体验间要做一个平衡,既不能纯用太多的机器死扛这个访问量,又要保障用户的体验。
基础链路设计
从请求链路来说,首先 DNS 把 CDN 的 VIP 解析出来,分布在全球不同的区域,CDN 回源到接入层分别经过 4 层和 7 层的负载均衡,近几年会发现 CDN 这个行业早已不仅仅局限做 CSS/JS 等静态资源的缓存,也承担了一些动态加速和收敛的特性,所以我们是通过 CDN 做域名收敛,收敛后会把这个流量发到统一接入层,然后到应用集群,后面再经过应用存储、Cache 这些服务。
当我们在做稳定性的时候,会考虑性能和稳定性之间是什么关系,很多人认为这两者是冲突的,比如我要保障一个东西的性能非常高的时候,要牺牲掉很多别的东西,可能你要引入一个非常新的框架或者基础设施来提升性能,但它的稳定性可能是不那么成熟的,但是从容量规划的角度看,只有这套系统性能足够好,才能承担像双 11 那样的大访问量。
店铺也是一套经历了很多年的系统,在应用层上的优化基本上已经做到极致了,我们就转变思路,在操作系统层能不能做一些优化,这里借助了一个比较好的工具 perf,在操作系统层面告诉你系统调用的开销是集中在哪里,从 perf 上就可以定位到有一个百分比,可以看到是比如数组分配还是 GC 产生了大量的开销。
最初我们发现是异常带来的开销,就会看为什么这个系统的异常会导致 20% 以上的 CPU 开销,最后用 BTrace 跟了一下异常的构造函数,发现是我们依赖的开源的三方包里通过异常做控制流,每一次它处理结束的时候,就抛一个 EOFException 出来,这个就导致了非常大的开销,我们就把开源包替换掉了。
当你依赖一些底层的东西的时候,如果对原理不太了解会给你带来一些意料之外的事情。JVM 里是有一个常量池存储字符串常量的地方,就是一个哈希表,如果说这个表的大小不足够大,就会从哈希查询变成链表查询,性能就会特别低。
再谈一个 warm up 的问题,当我们应用刚刚启动的时候,还没有把字节码编译成 native code,延迟非常高,用户就得到一个有损的服务。我们现在在内部的 JVM 做一个功能,会采集线上系统的调用,把热点方法收集下来做分析,在应用把真实流量挂上去之前,已经预先把所有的热点方法编译成 native code 保证这个性能。开源界也有其他的方案,比如 Azul 的 Zing 有个 ReadyNow,IBM 的 J9 有个 AOT,也是做类似的事情。
缓存设计
谈到缓存,Cache 里有一些小技巧,在做双十一备战时发现一个店铺的基础服务平时每天日常就有 100 亿的调用量,当时是几十台机器估了一下可能要成倍增长,成本是非常高的,怎么解决这个问题,当时写了个富客户端,让业务方先去查我们分布式 Cache,如果命中就直接返回来,如果不命中再走我们的服务端查。这种情况下,只要你能够保证命中率足够高,比如 98% 的命中率,就意味着只有 2% 是需要后端服务器承担剩下的请求,用非常少的服务器去承担非常大的流量,这是成本和性能间的权衡。
在缓存方面,我们很少会关心缓存的高可用是怎么部署的,它是一个偏运维的内容,我把缓存的部署简化成一个双机房的模型,因为它在高可用里是最简单的场景。
对于缓存来说有两种经典部署模式,第一种叫共享集群部署,在 IDC 里我的应用是分机房部署的,Cache 集群也是分机房部署,对于应用服务器来说,两边的 Cache 对他来说逻辑上是一个集群,会往 IDC 1 的 Cache 写一半过去,往 IDC 2 也写一半过去,这种部署的好处在于,机房间网络断掉的时候,有一半的数据是在缓存的,保证一半的数据能够命中,不会直接死掉,另外对成本上相对比较友好,没有浪费任何一个 Cache 的节点,这个 Cache 本身是复用的。
但是也正如刚才说的问题,如果中间断掉了,有一半的命中率是有损的,所以就诞生了另外的一个部署模式,就是独立部署,不管你哪个机房挂掉,命中率是基本不变的,两边同时保持了 98% 的命中率,但是它是成本不友好的,两边要同时部署,同时承担副本的作用,并且失效时,要同时失效另外一个 IDC 2,这样才保证一致性。
在缓存上,我认为一切东西都是可以被缓存的,通常我们认为缓存跟实际数据库里存在的东西可能是不一样的,有几毫秒的延迟或者怎么样,所以我们设计一个系统的时候,对一致性要求非常高的时候,会倾向于不用缓存,用数据库扛这个流量。
但以 MySQL 为例,InnoDB 里有一个很重要的缓存 Buffer Pool,对于一个数据库,在冷库的情况下用一堆 SQL 去查它,和慢慢预热完再查它的时候效果是不一样的,这个是我们当初在做异地多活时面临的一个问题。例如我已经有一个机房,希望建立一个新单元去承担这个机房的流量,当我建完这个单元,把所有的应用都部署好了后,把流量切 50% 过来会怎么样,假设这两个单元的机器数一样,这个单元会挂,因为这个数据库是冷的,缓存是空的,不能承担之前那个单元数据库所能承担的 QPS。
现在业界有很多叫 API 网关或者 CDN,他们在边缘节点也做了一层短暂的 Cache,可能只 Cache 50 或者 100 毫秒,但是当你系统受到攻击的时候可以拯救你后端的应用系统,攻击引发的命中率通常比较高,有这 50 毫秒的缓存,可能后端只有几百个 QPS 过来,那个流量你是可以承受的。
在高可用里两个非常经典的做法是限流和降级,在阿里双 11,有一位老兵说过一句话,他说当双 11 到来的时候,任何一个系统都可能出问题,你要做的是对你的上游限流,对你的下游限流。怎么理解,当上流的流量超过你的能力的时候就要限流,当下游比如 DBA 告诉你数据库压力很大了,那就对下游限流,只要保证住这个限流,你基本不会挂,每个系统都做到这个的时候,整个系统都是可用的。当流量超出你掌控的时候,这个做法可以让你成为这个暴风下的幸存者。
对限流降级的思考,第一限流降级考验的是什么问题,我认为本质上考验的是故障自恢复能力,在平时工作中会遇到机房断网或者停电,每半个月都会做断网演练,不告诉你发生什么,就把这个网切断,看你的应用 O 不 OK,一般是在晚上两三点,接到很多的机房报警,这个时候看你的架构设计的是否足够可用,如果足够可用就没问题,不会造成什么影响,继续睡觉,如果设计不好,就得爬起来立即处理。
而开关降级最大的作用,比如我们发现一些线上的问题,第一反映是赶紧回滚,但是当你的系统很大的时候,特别像 Java 这种,一个系统启动要启动几分钟,你的回滚完成,20 分钟都过去了,这个过程对用户来说都是有损的,而开关可以在一瞬间把所有的逻辑切到老的。这个是避免回滚时间导致的问题。开关有的时候能救命,如果没有这个开关的话,避免问题放大就只能回滚,所以开关是一个很大的价值所在。
容灾设计
另外一点非常重要的是,在设计一个技术方案的时候,就会把容灾的设计融入到方案里。比如在设计技术方案的时候,在最后一章单独有一个容灾设计,这个节点里任何服务挂掉的时候,你要保持什么样的方式保持这个服务是可用的。
在容灾设计时有几点必须考虑,比如我引了一个新 jar 包或者调了一个新的 RPC 的服务、引入了分布式的存储,以前没用过也不知道它稳不稳定,第一想法是它肯定会挂,它挂了我们怎么做,我们当时在做前台系统的异步化的时候,因为 Redis 支持 map 的数据结构,所以我们就是用 Redis 的 hmget 从这个 map 里拿出部分的 key 减少网卡的流量,但即使这个挂掉了,我们还会走老的 Cache,只不过网卡流量会大一些,但是对用户的服务是无损的,所以这里要考虑如果它挂了怎么做降级,有什么样的恢复流程。
另外是发布计划,在新系统上线时就会关注这些问题,比如这次有没有做数据迁移,比如以前我是 8 个库不够用了我拆到 16 个库或者 32 个库,中间一定是有数据迁移的,涉及到数据迁移一定要有一套对账系统保证这个数据是新数据和老数据是对得平的,不然一定有问题,因为我们是做交易相关的,订单、金额绝对不能出问题。
另外是你的发布顺序是不是有依赖,如果出了问题的时候,谁要先回滚,这里是取决于技术设计。另外是否要通过客服公告的方式告诉外部用户说有 5 分钟的不可用,如果真的有用户打电话有疑问客服同学可以向用户解释。
在高可用这个领域做久了会有一种直觉,这个直觉很重要,来源于你的经验转换成这种直觉,但是对于一个成熟的团队来说,需要把这种直觉转化为产品或工具。有很多牛人他们的技能都只能叫手艺,你需要把这种手艺转换成产品和工具。
公有云高可用设计
2015 年我去做云产品,这里给大家分享下我们是怎么样帮客户包括我们的系统在云上是做高可用的。
经典故障案例
首先看两个经典故障案例,第一个是 Gitlab 生产数据库删了,它恢复了很久,Snapshot 等全都没有生效,做了五六层的备份也都没有什么用。这个事情说明第一我们的故障要定期演练,比如中间件在做的线上故障演练,你说你的系统可用性好,我把这个主库断了,虚拟机挂掉几台试试,做这些演练就可以知道你这个容灾体系是不是可靠的,如果没有这个演练的话,当真正的故障发生时你才会发现这个东西是不 OK 的。
另外一个很典型的问题,Gitlab 对备份的原理是不够了解的。比如当时用的 PostgreSQL 的一个版本,当时是有问题的,没有验证,开发人员对这个又不是特别了解的情况下就会出现这个问题,这就是为什么要去了解你的依赖以及你依赖的依赖。
去年我们做压测,有个应用一边压测一边在优化做发布,发现第一批发的起不来了,就只是改了一两行代码加日志,他就去看什么原因,最后发现依赖的某个 jar 包依赖一个配置,而这个配置在压测中被降级了,一个 jar 包就把应用启动卡住了。如果在双十一当天或者在平时业务高峰期的时候发现这个问题是来不及修复的。所以这个时候,我们就要求,依赖的二方 jar 包必须看一下里面是怎么实现的,依赖哪些东西。
反过来说,别人依赖我的客户端就意味着他不仅依赖着我的服务还依赖着我的缓存,这个缓存出了问题对他也有影响,我们每年双十一前有一个强弱依赖梳理,不仅要梳理自己应用里面的,还有依赖的所有东西都梳理出来,中间任何一个节点挂掉了你应该怎么办,需要给一个明确答复。
第二个故障案例是今年发生的,AWS S3 敲错了一个命令把基础核心服务下线了,有一个对象索引服务和位置服务系统被 offline,后来也做了一些改进,每次敲的命令有一个静默期,让你有个反悔的机会,线上有个最小的资源保证服务。
这个给我们带来的启示是什么,云服务本身也是会发生故障的,比如买了云数据库,我们没有办法假设它是 100% 可用的,当它出现问题我们怎么办,是给云厂商提工单说什么时候能恢复,还是我自己能够有一个容灾的方案解决这个问题。从 2015 年开始,我们越来越多地发现,对架构可用性最大的威胁是什么?在市政施工里一定几率就会莫名其妙搞出光缆被挖断等故障,我们不得不考虑,当云服务本身出现问题我们该怎么办。
应对措施
所以我们需要有一套面向云的高可用架构。在很早以前有厂商提出类似 SDN 的一个概念,叫 SDI——软件定义基础设施,过去我们发现只有大厂可以做这个事情,设计一套很复杂的管理系统帮他实现,这里放一个路由器,这边放一台虚拟机,可以通过软件的控制流去控制这个东西。但是在云的时代,资源变得很容易获得。以阿里云为例子,可以用 API 随时创建新的虚拟机,新的负载均衡,或者是新的存储,都可以通过 API 随时创建随时销毁,这对于你的基础设施的灵活度非常有好处。
以前有的人会觉得,性能问题或者容量问题应该通过性能优化的方式解决,通过一些黑科技方式解决,加机器会觉得很 low。但我觉得一个问题如果能简单用加机器来解决是很不容易的,意味着对你的整个架构的水平扩展性要求非常高,而且解决效率很高,加机器就解决了,而对一些中心化的系统来说就比较麻烦,加机器都加不了,可能要把机器关掉升配置再重新拉起来。所以我们说,在公有云上面,在资源如此容易获得的情况下要充分利用这个特性,要做一个能够做水平扩展的架构。
那么第一步要做什么,前两年很火的容器、微服务,本质上都是解决了是无状态的应用怎么做自动化的扩容这个问题。右边这个图上面,上面是一个负载均衡,中间是一个前端的服务,后端是一个无状态的后端服务,底层是 MQ、对象存储、数据库这些东西,如果我们能够把前端和后端的无状态服务第一步先容器化,就可以做到当流量过来的时候,只要后端的存储没有问题,整套架构就是能够水平扩展的。
从去年公开的报道和故障来看,很多人有个误会是说云厂商的机器应该是不会挂的,我买了一台云厂商的虚拟机应该是随时可用的,即使不可用云厂商也要帮我解决热迁移的问题,热迁移在业界是很复杂的问题,不光涉及到磁盘存储的迁移,也涉及到内存是要做迁移的,可能还要用 RDMA。并且对于传统的 IDC 来说,不管物理机还是虚拟机都是有可能挂的,对云也不例外。
当我们在使用公有云服务的时候,都是会挂的,这是个心理准备。不光是机器,包括负载均衡是不是也有可能挂,下面的消息队列或者数据库是不是也有可能会挂,当你基于任何东西都可能会挂的前提设计一个系统的时候,才能真正做到这个系统在任何情况下都不会受底层云服务的故障影响。
而对于不同的云服务来说是有不同的容灾策略。比如一台虚拟机挂了,通常来说负载均衡不管是 4 层还是 7 层都会做健康检查,挂了健康检查不通自动会把流量切断。如果我的负载均衡挂了怎么办,如果 DNS 有健康检查那就方便了,如果没有的话可能就要设计一个旁路系统解决这个问题,发现这个已经不通了,就自动把它从 DNS 上摘掉。
不管是云服务发生故障还是自己应用发生故障有个大原则是如何最快速解决问题,就是一个字,切。为什么要做异地多活,为什么要把流量往各个地方引,切流量是解决问题最快的,把坏流量切到好的地方马上就解决了,如果你要等定位问题解决问题再上线客户就流失掉了。对淘宝来说也是一样,当某一个单元低于我们认为的可用性的时候,我们会把这个单元的流量引到另外一个可用的单元,当然前提是那个单元的容量是足够的。
弹性是不是万能的?所有的云服务都是弹性的,弹性其实不是万能的,容量规划仍然是有必要的,不然就没必要做双十一备战了。这里有一个你需要付出的代价,弹性的过程往往是需要时间的,那么容量规划在这个环节中起到的作用就很重要,当真的逼不得已的时候,我要扩容了,怎么保证我扩完容之前系统不雪崩?就是要结合之前的限流,尽可能保障每个客户得到他应有的服务,但是也要保障系统的稳定性。
Region 和 Availability Zone 这两个,当实践的时候,购买虚拟机、负载均衡或者数据库,一定要选择多可能区的服务,比如阿里云买 SLB 是可选可用区的,有个主可用区和副可用区,如果一边挂了可以切换到另外一边,RDS 也是一样的。
这幅图是一套典型基于公有云的一套架构,不叫异地多活,应该叫跨区域设计。左右两个大框是两个城市,左边是北京,右边是上海,每个里面又有不同的机房可用区去承担这个流量,假如北京的挂掉了,就切,原来是在 A 可用区,就切到 B 可用区,这时候 A 可用区没有流量进来,通过切换的方式能够把这个服务快速恢复。下面画了一个跨区域复制,我们在异地多活项目里,涉及到了跨城市跨数据中心的复制,比如我的北京是提供写服务的,上海要提供读服务,就要通过这种方式同步数据过去。