作者:洪泽国,2007 年硕士毕业于中科大,先后在 Oracle、腾讯等公司就职,主要关注在线服务的高可用、高性能和易扩展。
大家好,我来自滴滴出行,今天其他老师分享的内容覆盖内容比较大,我分享的话题相对小一些,讲一个具体的应用。我们在 passport 设计时候踩过很多坑,后来在可用性方面做了很多优化实践,今天给大家分享其中的 7 个小优化。
我的题目就指出了 Passport 设计的一切都是为了高可用。Passport 主要有两个功能,第一登录;第二,授权或者鉴权,每一个请求过来,我这边都会做一个校验,校验量是比较大的。再考虑到滴滴的场景,我们在座的大家可能是乘客端,但是我们还有司机端、代驾端等,司机端每一秒都会发请求过来,请求方就会到 Passport 请求一下,所以是一个典型的高并发高可用场景。
先简单介绍一下业务场景,我来自滴滴平台部门,平台是一个业务支撑部门,支付、账号、消息等功能都会在我们平台里。今天主要给大家介绍账号子系统,我们设计 Passport,有很多优化的规则,比如大系统做小,做服务拆分,力度拆得非常小,目的是为了高可用。
Passport 的应用场景,工作之一就是登录。登录成功之后返回 ticket,之后每一个业务请求都会把 ticket 传过来,如果合法,则返回给调用方用户真实的信息。
Passport 简单理解,它是三元组。登录的凭证是手机号码、密码、UID,可以简单理解为 Passport 只维护了三元组。
在我们开始设计一个账户,用户其他资料一开始揉在一起设计,后来我们发现这个问题非常麻烦,可用性会存在一些瓶颈,因此把大系统做小,把 Passport 单独拆出来,只包括这三元组。
我的第二个分享内容是一切为了高可用,我们做了什么?我们会从编程语言上,最早用 PHP 写的现在用 golang。最小闭环,柔性降级,异地多活,访问控制,接口拆分等。
我们编程语言是 PHP,现在账号系统用 golang,提升非常明显。有一个例子,一个乘客的用户服务,在线上布了 45 个实例,司机端或者乘客端都有心跳,每一个端有点像 ddos 攻击一样,不停的轮询,司机要不停上报他的状态或者坐标等等信息,访问量非常大。一开始用 45 台 PHP,后面用 golang 重写了一下,只用 6 台机解决了这个问题。
刚才也说到用户的资料包括 count、UID、密码、名字等信息,我们把它做了一个拆分。拆分有什么作用?和我之前在腾讯的经历有关系。之前在腾讯的时候,老大一直说 QQ 永远不能存在不登录情况下,即时登录进去都是空白都能接受。这个的确有很大的差异,当用户不能登录,他以为他的账户被盗了,这会形成很大的惶恐,但登录之后什么都没有,他知道肯定是系统挂了,不会有恐慌的心理。因此对于帐号系统来说,需要永远要保证它是能登录的。
最小闭环刚才说了,passport 只包括三个最主要的属性。我们乘客端刚刚上了密码登录,司机端都是用密码登录。在 QQ 时登录量是非常大的,校验量非常大需要做很多细致的工作。腾讯包括微信的架构都有一条经典的经验,大系统小做,当你把系统做小之后,高可用性最容易做,每一个功能比如用户存储的信息越多,这个事情就越难一些。
(小编注:在上一篇大促系统全流量压测及稳定性保证——京东交易架构分享文章中,京东商城的杨超将这种设计方法叫异构)
很多公司都需要降级,在柔性降级里面举几个例子跟大家分享。
在移动客户端应用,登录时间通常是很长的。比如大家用微信,不需要经常登录,但是服务端需要有踢出用户的能力。踢出是什么概念?登录后,可以用另外一个手机登录就把前一个踢出,这样应用就会更安全。就因为可踢出,实现就会稍微复杂一点。
我加入滴滴之前已经存在一个 Passport,最早是 PHP 语言写的。在滴滴合并快的,我也了解快的那边的情况,大家在设计 ticket 时比较简单和类似,一登录,生成一个 ticket,业务来请求提供认证,认证服务和 ticket 进行对比,对的就通过,不对就让用户踢出。
我相信很多帐号系统都是这样实现,但这里面隐藏比较严重的问题。ticket 是无语义的,里面没有任何信息。其次如果 ticket 服务不稳定校验就会不通过,所有的业务请求第一步就是来校验,它对系统的要求,第一是低延时,你得足够稳定足够快。第二,不能有故障,一旦你个服务失败,用户端就会请求失败,就叫不到车。
在滴滴,不管基础组件比如 MySQL 都需要考虑失败的情况,和滴滴快速成长有一定的关系,所有业务系统,在实现时就需要充分考虑系统的不可靠性。
于是我们对 ticket 重新进行设计,下图是目前的设计。第一我们 ticket 增加了语义,里面是有内容进行了加密。这里面提一点,加解密尽量不要用 RSA 非对称算法,那会是一个灾难。ticket 里面包含一些信息,包括手机号、UID、密码。
图上面有一个 seq,可能是我们这边比较独特的设计,你要实现可踢除,就像前图更多是对比两个 ticket 是不是对等,ticket 比较大是一个串,放在一个存储里面,空间挺大,并且不停地变,我们想把它改造成 seq,一个四字节的 int,通过 seq 达到 ticket 踢除的目的。
在用户登录的时候发 ticket,ticket 有 seq,跟手机号加密在里面,每登录一次我会 seq + 1,我们有状态的是 seq,由很长的存储变成 int。我们验证 ticket 是否有效?只需要解密,把 seq 拿出来,跟数据库 seq 对比是不是一样?一样,就过了。
这个项目我说了几点。第一,我用 token 的概念,实际上是没用,把它干掉,通过有状态的 seq 做到。另外,ticket 里面是自己包含内容是有语义的,这为我们降级各方面做了很多的探索,我们在降级的情况下会牺牲一点点的安全性。刚才说 Seq,由 ticket 变成 seq,存储下降非常多。
这里实例说的还是柔性降级,假设 seq 存在 cache 里面,cache 这一级挂了,我们还是能够做到验证 seq,能解密,seq 判断符合要求,在降级的情况下也是可以过的。当然这也牺牲了一点安全性。
我们最早大部分登录使用验证码,另外我们系统有很多的入口,我们在腾讯的微信、支付宝里面都有访问入口。在 Web 环境下系统很容易被攻击,后面我会讲攻击的事情。
登录时候,输手机,发验证码,输入验证码,然后到我这边服务端做校验。通常做法也是用户点获取验证码,验证码有效期几分钟,系统存储一个手机号跟 code(验证码)的关系,登录的时候把手机号跟 code 传进来。
验证时候根据手机号找到存储里面 code,两个一比,相同就通过了。但是也有问题,假如 cache 挂了,登录不了就会很被动,如何实现高可用?
Cache 复制永远高可用是另外一个话题,我们尝试了另外一种柔性可用的方法。我们的需求是验证码能够在几分钟内有效,我们也可以计算,手机号加上当前时间戳,实际是 unix seconds 变成 unix minutes,算当前是多少秒分钟。通过手机号加时间,在它发的时候算 code,输入就是手机号加 unit,输出给它一个 code。
第二步,用户输入手机号传过来手机号加 code,假设配置是 5 分钟有效,计算其中的时间,拿手机号加上当前的时间,假设是 5 分钟,递减 5 分钟,当前的分钟数和手机号算一个值与 code 对比,不对的话最多循环五次(当然这个也有优化之处)。
当极端情况 cache 不可用的时候,我们可以手机号以及时间,通过内部的算法算出验证码是否基本可符,降级之后安全级别会一定降低,在可用性和安全性方面取得一个折衷。(编者:假如在系统正常情况下,cache 的验证码可以通过算法再加一个随机因子,严格符合才能通过,这样正常时候安全级别是可以有保证。)
攻击者有两种攻击,一种拿着手机号,换不同的 code,这是一种攻击。另外一种攻击,拿固定的 code 换不同的手机号,我们现在结合的方式,我们现在还是采用第一种,当我们后端服务不可用,还有兜底方,当然牺牲了一点点安全性。当我挂掉那段时间,我还是照样可以登录。
讲一下异地多活,保证系统永远可登录。在滴滴,由于业务发展太快,下图是当前业务分布的情况,它带给我们的一些麻烦。
上面是当前 Passport 简单的图解,我们现在是有 3 IDC,每一个 IDC 里面部署不同的业务,我们滴滴还没有做到业务异地多活。可能 IDC1 有专车快车,IDC2 有顺风车,IDC3 里面有代驾。
我们现在是租用的机房,一个 IDC 如果机器不够用,就从别的机房匀出一些,导致我们的业务非常分散,这也给 Passport 和账号团队的服务提出挑战,我们要提前业务做异地多活。但是现在业务本身并没有做到异地多活。
我们把登录实现了多活,注册还没做,但是目前已经足够满足我们要求,如果一个机房挂掉只是影响新注册的用户,在一定程度是可以接受的,所有其它的服务可保证正常使用。
刚才提到不同的 IDC 存在不同的业务。一个人登录进来,先用快车在 IDC1,点开顺风车在 IDC2。这里面就有一些很细节的东西,也就是刚才说 ticket 问题。用户用户在 IDC1 登录,IDC1 给他一个 ticket,这个时候 IDC2 里的 ticket 并没有更新,因为我们所有的请求都是在同机房完成。这时候切到另外一个 IDC 校验,如果当前的 seq 比它传过来,而且发现比他当前小的情况,可以考虑放行。这是由于有可能同步的延迟,seq 还没同步过来。通过这个柔性可用策略,一定程度解决了多机房数据同步不一致的问题。
我们部门所有的服务都是平台级的业务,比如账号支付,所有业务线都要访问,一般都是通过内网来访问。
为什么要做过载保护?当公司业务部门增多后,会碰到不同的业务拿到线上做压力测试的情况,我们现在所有的公共业务部署,不是按业务线多地部署,我们是大池子大集群,每个业务线都来混合访问。账号访问由于容量比较大,一般压测并没有引起问题。但是在支付的时候,做压力测试在线上支付,就可能会直接把支付拖挂。
我们想必须有一种机制,不能相信任何的业务方,它随时能犯错误,需要通过技术手段去解决。因此需要有过载保护,包括权限控制等一系列机制等。
如上图,防控就是 Argus 系统,承载了过载保护,白名单、安全策略等等职责。它是独立的服务,所有的业务流量打过来,都需要通过它做过滤。
上面提到现在业务并没有多机房的部署,因此如果需要对调用方进行 QPS 的限制?只需要通过在 cache 里设置一个配额,每调一次检验一次。
但这样有个问题,调用量太大。比如说快车有千万级别调用,调用量比较大,我给快车的某一个核心业务一个配额,如果都放在单个实例是支撑不住。因此可以增加一个简单的散列的方式,比如每个调用方调用的时通过 hash 到不同的 Argus 节点上。比如配额是十万 QPS,则可以部署 10 个节点,Argus 每一个就是一万,这样访问就比较可控了。
刚才说的核心登录功能,不经常变,我们希望最稳定的接口独立出来,目的是让稳定不变更的部分故障率降低,所以需要考虑进行拆分。
核心的接口包括登录这一块,其实不经常变,但是像一些小逻辑,策略会经常跟着去上线,但大部分事故都是上线引起的。
分享一个 Pass-TT 的案例。当时跟快的合并时候,快的所有业务在阿里,滴滴所有的服务在深圳腾讯机房,ticket 服务在内网,两个机房跨公网,改造太大了,并且延时不可以接受。
所以我们设计了一个方式,简单说,登录从我们这边登录,访问快的服务的时候再给它 ticket,token。这个 token 专门为代驾用的,但是设计时候犯了一个错误,就是 RSA 方式进行加密。因为有一个远程校验,为了不想 key 泄漏,所以用 RSA 的方式,他们那边部署了一个公钥,我们这边是私钥,token 用我们的私钥加密,然后到它那边进行解密就 OK 了。
这些 token 失效是通过有效期,就是几个小时,失效后就用 ticket 来换。
结果一上线,由于端上有一个 bug,然后所有的等于 ddos 攻击一样,不停闪。当时 Passport 服务器还比较少,10 台左右的服务,正常 CPU 利用率大概在 30% 不到,那个接口一上一下子到 70%,眼看全要挂了,然后连夜我们赶快把那样一个接口单独拆出来,打到不同的单元上,然后重新部署。
所以给大家一个建议,当用 SOA 的时候,如果你的量特别大的时候,你一定要记得把它的 CPU 占用非常高那些功能及接口提前单独拆分出来。
更多得益于 golang 本身,我们去年在线单机 QPS 峰值超过一万,现在我们有更多的机房,滴滴整个的订单去年和今年都是有数倍增长。
我们响应时间,我们的所有接口无论获取用户信息或者是校验,其实是非常小的,目前高并发接口小于 5 毫秒。密码相关的接口是在 200 毫秒,这主要加解密本身的耗时。
滴滴跟前面分享的今日头条非常相似,发展太快,四年的时间,滴滴的技术规范完全没有统一,文嵩加入滴滴之后,有机会再做服务治理包括框架统一,但是这件事可能会比较挑战,目前滴滴的技术体系特别异构,PHP、Java、Golang 都有,因此目前也不能太多从系统层面给大家做分享,以后应该有这样的机会。
后面有一句话,什么是一切为了高可用?针对 Passport 账号这种系统,需要有柔性的降级,可能需要一些巧妙设计,包括多机房。
one more thing
被攻击的问题
再补充一个被攻击的问题。其它的系统被攻击相对好一点,账号给你挡了,登录后再攻击,至少有 ticket 给你挡了。
发短信攻击,这个维护费用是非常巨大的,攻击者目的你并不知道,他就是要你不停发短信,曾经被这个东西搞得焦头烂额。
当然可以做一些蜜罐的机制,当发现有异常,返回正常值,让攻击者觉得正常,其实服务端什么也没做,这是蜜罐机制。要真正解决这类问题,跟安全部一起做了试点,端上做这种是比较简单,认证比较容易。但是在 app 上不好做,通过 JavaScript 算,其实比较麻烦,并且容易别识破。另外还可以做一些人机识别机制。
Q:token 什么时候更新?
洪泽国:token 只有两种失效机制,一个是用户重新登录,第二个踢掉,现在参照微信的做法,端上定期会拿着来换一次。
Q:密码更新,所有的 token 都失效 如何解决?
洪泽国:密钥一更新,登录都踢出?这个问题之前的确有讨论过,有两种方式,第一,让它失效,所有人重新登录,并不是完全不能接受,对用户来讲就是重新登录,密钥泄漏是一次事故。第二,看紧急程度,当前哪些人登录,比如先把没有登录的踢掉,后端算出来,对于已经有的,然后慢慢踢。新的在入口的时候,就要给它打到新的一套服务里面去,你要做新老两套密钥服务之间的切换,并且在上线是知道。
Q:HttpDNS 作用?
洪泽国:HttpDNS 从入口层面上就决定了所有的这一个请求全部落在这个机房,而不会存在两个 IDC 之间内部错峰的一切交替,不会你这个故障某一个服务我就打到那边。HttpDNS 比较简单,维护的信息比较少。无非就是一个服务对应一个IP列表,这是动态,这是很容易做的,信息不会太多。
Q:切换 IDC 怎么做,数据你怎么复制?
洪泽国:我们的确在讨论这样的方式,我不太确认是不是能够最完美回答你的问题,我们现在方案通过 HttpDNS 做中转,通过它我能够完成这件事情,第二,怎么发现故障,这里面包括两个问题,一个是怎么发现 IDC 挂了。第二,发现后怎么做,发现后 HttpDNS 很容易给一个新地址。第一个问题怎么发现,我们内部也在尝试一些方法,可能更多通过手段的方式,或者通过检测链路故障,无非触发时机,所有的链路故障都是从系统,网络层面上触发的。只要 HttpDNS 提供一个接口,一旦故障给它打个标记,A 故障,HttpDNS 就返回 B,这是很容易做到。但是怎么判断它是故障,这是一个问题。这个更多从网络层面去触发。
Q:刚才说如果其它服务链路问题导致 Web 失效,把用户踢下去,对于这种情况你们怎么处理,不是你本身的问题。
洪泽国:这个是调用方有约束的,调网络错误不应该是提的,更多是要从业务层面上,端和业务系统的配合形成。它调一个业务,业务调我,业务调我超时了,现在好多业务处理不是那么完美,出错也踢掉这是不合理的,我们跟他梳理,只有失效踢出是最合理。如果没有的话就踢掉,短信服务提供商那边压力是比较大的,做柔性降级。
本文相关 PPT 链接如下
评论列表点此评论