【IT168专稿】全球系统架构师大会于8月10-12日在深圳万科国际会议中心隆重举行。在今天下午的演讲中,来自人人网的高级架构师刘源给我们分享了人人网网站架构变迁过程。在演讲中他表示,SNS是我们的社交关系、好友关系,但是想要把用户留下来,就要不停地推出喜欢用的、觉得好用的东西。
▲人人网高级架构师 刘源
畅谈服务化
服务化,往往是在非常难以解决、非常痛苦的角度下才会做一个架构迁移,为什么要做服务化呢?
在依赖很复杂、很混乱的情况下,会有什么问题呢?大家在座的都是一线的工程师和架构师,也有高级人士,从一线走过来的。大家都知道,比如说有两个非常有依赖的服务,依赖了另外的模块,但是依赖了不同的版本,这时线上就会出现一些诡异的问题。还有很多依赖所造成的就是非技术层面的问题,沟通非常难、上线很难,为什么?
拿状态服务为例,可能有数十个业务和服务都去依赖它。这种情况下,状态服务的特殊变更、非常细小的变更,都需要去更新相关人的所谓模块。这时候要大量的沟通,会给你造成什么样的影响,会推动上线。上线过程中会有一些很不幸的事情。比如说一个模块的负责人休假去了,就知道上线的过程是一个多么漫长的情况。
服务化能解决依赖混乱的问题吗?能解决沟通太多的问题吗?上线很痛苦的问题吗?
某种程度上能简化一点。服务化把二进制的关系上升到一个层次,上升到我们约定的接口层面,接口之下的变动对接口的使用方是透明的。当然,这是在某种程度上。所以说,服务化比较适合移动的系统和持续变更的系统。
人人网为什么要服务化?
一方面,人人网复杂度是逐渐递增的过程,超出了所能控制的边界,到后来已经不能控制了。其二,我们需要去面对各种各样的变化,这种变化有些是从技术层面过来的,比如说有新的需求,这些需求是在已有的业务中前所未有的东西。第三方面是自己的发展轨迹。在开心001冲很猛的时候,人人网创立了开心.com,之后合并。现在又收了新的网站,比较有名的是56。人人网里面最强的团队是什么呢?是人人网的DBA团队,在人人网发展过程中对于数据库的拆表合表、用户数据集成基本上可以认为处于世界互联网前列的。这就是人人网的特点。
开始人人网的服务化之路
我们实现了REST的框架,使用java的,做得非常精妙,使应用更加便捷。大家都听过ICE的框架,是个非常完整和强大的框架。在这边实现了像缓存等中间层以及新鲜事儿系统,因为量大就用它来实现了。
走到这一步的时候我们发现对服务化的理解不是那么深,在应用一项新技术的时候,会充满各种各样奇妙的预期,能够解决我们所面对的各种各样的困境。但是,任何的技术其实都是一把双刃剑。你接受它给你带来的收益时,不可避免要忍受它给你带来的缺陷,只是这些缺陷是你之前没有注意到而已。
我们在过程中所遗留下来的问题是面对易购的总线,有自己实现的REST框架和开源ICE的框架,REST是Java架构,跨语言服务无法调用,非常有局限性。而ICE我相信都使用过,前几年非常火,应用的人都知道是非常庞大的框架,手册像一本书一样有好几百页,能够去解决你能想到的所有问题。
但是,它过于完整了。如果想去修改它、扩展它,怎么办?我们在重造轮子和使用开源方案两难的情况下怎么抉择?这是服务化过程中第一个需要解决的问题。
我们从具体的线上事故说起
从这个线上事故中反思在服务化过程中是不是走错了什么路?现在看到的是最初的状态,3G业务在人人网基础架构之外的了,依赖了人人网的状态服务。这个时候世界都是很平静的,我们有一个新的功能去添加上,而且这个功能采用服务化的方式来实现,叫做SocialWIKI,加上这个之后服务还是可以的,我们另外一个服务也使用SocialWIKI,就是相册服务,量非常大,超出了我们所有的预估,然后就把SocialWIKI压死了。谁依赖它,谁死。3G最终的倒霉蛋,跟SocialWIKI一点关系都没有,也被弄死了。
值得总结的教训
到这里我们想一个问题,从之前蜘蛛网混乱的专制系统中,走向完全无政府主义的状态,是不是走的太远了,是不是步子迈的太大了,服务化是不是不靠谱,必须响应这个问题。虽然之前这个事故持续时间不长,但是已经给我们非常惨痛的教训。我们对服务化过度的时候,会有一系列新的问题需要去思考。
在这个框架下,不可避免常见的问题,单边失效,要在系统上实现容错。局部的过载让各种因素都会失效,这个问题就是要去尝试深入剖析的问题,尝试对它进行挖掘。
有一些什么样的心得呢?
首先,为什么不适用非阻塞方式?3G压力过高被阻塞在那儿了,我们调用需求有阻塞的也有非阻塞的。而发状态只需要把状态安全发出去就可以了,为什么不换一种?为什么不做一些配额限制呢?相册这种非预期的庞大流量忽然一口气压过来了,为什么不能做一些机制上的约束。
SocialWIKI还处于没有上线的阶段,线下的或者预上线的系统把上线的给弄死了。我们为什么不能自动去识别一些依赖,去应对一些全线隔离的策略,动态增加一些资源。最终,如果让系统反馈更早一些,能够遇到什么样的线索解决各种突发性的问题。
我们要尝试进一步推进和演化我们的架构,已有的东西不足的,要去尝试解决发现的问题。我们把问题做一个稍稍的抽象,变成一个比较独立的子模块,它们之间不会干扰。我们想要结束混乱必须要添加规则,想要去尝试统一的服务方式和全新的控制体系。
对于易犯错的问题,人在这么复杂性的东西面前很容易遗漏,很容易犯很细小的错误,但是在自动化过程中可以解决这些比较琐碎但是很重要的一些检验,从而实现服务的自动调动。
进一步补充已有的Log/Profiling的收集分析,既要离线也要在线。
现在看到的是人人网在应对孵化中所给出的架构图,叫XOA。中间黄色的是搭建的业务和逻辑。
进入具体问题方案拆借的阶段
我们首先考虑的是基础总线,基总线的能力影响着其他所有模块能走多远。还是回到之前的问题,要自建与开源的折中。如果大家看过类似于ICE的框架业务量,大家会发现其实很多的工作是在做一些所谓的非常繁琐、非常细致,但是跟人人网没有联系的工作,没有太大精力做,但是用ICE框架,我们就可能被这个框架绑架,很蓝把我们的需求添加到系统中去。所以,又给出我们对开源系统的态度,使用的时候作为组件而不是框架,于是我们选用了Thrift,因为Thrift层次化做得非常好,可以对每一层进行深入的定制。在不影响已有功能的情况下,快速应用我们的需求。
在人人网内部我们对Thrift做了一个扩展包,叫ThriftEX,我们尝试定制传输层和协议层。这个服务跑起来一大张网,场景怎么复线,是一个非常恐怖的事情,我们尝试把RBC的调用都能够做到路由存储以及加用调试信息,当然还要定义协议层。
基于Thrift已有的服务层,会按照我们的需求做一个扩展和集中。比如说可以做一些进程模型,确保安全性。还有比较重要的是我们去集成facebook303,这个东西有些听过,有些没有听过,为每一个框架上跑的服务添加一个后门.
这些后门是什么呢?
我看一下服务当前的状态,是活了还是死了,看一下服务的内部统计信息,比如说某个函数过去一个时间段的平均调用时间。还有,可以让你的服务做变身,可以加载其他的模块变身成另外一个服务。
民兵服务,是处理预定义的语义,紧急情况下可告知要加载。
接下来我们认为第二重要的,我们的调用语义怎么来接受,状态服务常用的调用语义有两种,双向阻塞、发状态。发状态只需要把状态安全发出就可以了,不需要获取返回值。现在看到的是一个阻塞图,如果把最下面系统返回的去掉之后,把左边加长就是一个非阻塞的单向调用,单向调用的语义是消息交换模式。
就像我刚才提到的消息——交换模式,我们的语义全展开来看非常多,可以阻塞、可以非阻塞,可以同步、可以异步,可以Event、可以回调。交换里面有单向、双向,有最少一次,最多一次等。我们是不是都要实现?这是非常让我们花心思的问题。最终的结果是我们不去实现他们,我们先仅针对我们的应用,去实现双向阻塞和单向非阻塞的安全送达的语义。为什么?
其一,我们观察了模式,之前的这么多年没有一个人在用,业务使用框架是用一条最短、最容易的路使用,除非遇见了不能解决的问题,至少现在没有出现这种情况。
其二,这些语义是基于两点之间的问题抽象出来的,如果我们的调用链很长,这个链达到两到三级的时候,这个链上调用语义不同,有的使用非阻塞的,有的使用异步回调,有的使用阻塞的。这种情况下,如果你是一个架构师,如何一个时刻能够描述清楚系统的状态吗?这其实是可以的,但是如果碰到之前的问题,失败的问题,这个链中间某种情况下断掉了,调用链是什么样?暂时没有想明白,所以我们不用为奢侈品付钱。
我看了小米的架构师PPT和我们问题比较类似,实现非阻塞用了MQ,为什么不用MQ?因为业务只需要非阻塞,不需要MQ,只会增大我们系统的混乱度。
现在看到的是我们为了解决单向非阻塞调用所设计的简单系统原形。单项的语义通过IDL的描述,会做一个路由,会到自定义的MQ中,MQ做数据缓冲作用,之后调用端和服务端做一个完全形式上的接口,面向连接的情况。回到我们之前的态度,我们希望选择一个组件,而不是一个完整的框架。
接下来的问题重要性在于解决沟通的问题,系统的复杂度和混乱度是很大情况下是由于人员交流过程中所出现的偏差和信息的丢失。一个很常见的是,如果要依赖一个服务,请服务方告诉我服务的入口怎么获取,用什么样的方式提供出来。如果依赖十个服务,把工作做十遍,这个东西是非常恼人的事情,而且很容易出错。我们还会尝试在定位规则上去实现我们的全线隔离和一些配额的控制。
在此之前我们先看一下已有的可参考的东西
比如说Linux的文件系统标准,如果是Linux可以尽量遵守文件系统标准,配置文件放在ETC下面。我们会关注PROC的路径结构,所有的进程都会在PROC有一个环境,环境会描述现在执行的状态。
Linux有非常简洁的权限划分,rwx用户组和之外的划分,能够隔绝一些风险,我们尝试把这些设计类的理念融入到Zookeeper上建立服务定位,全线隔离配置。具体的服务定位如PPT上所示。
我们的服务首先要选择一个权限级别,我是一个在线的服务,还是QA测试的沙盘服务,还是单测的test服务,要在根路径下进行划分。对于service和version是定义服务的名称和服务的版本好。每个服务启动的时候都会按照自己的权限级别和自己的名字、版本找到对应的位置完成注册。这就是我们的stat和node后续的语义。
在这个基础之上,我们能够不进行点对点的商定就可以实现服务的注册和去查找。我们只要去看一看你所说的服务名和版本号下面有没有注册的服务信息,就知道你的服务是不是已经OK了。当然,这只是解决服务的查询问题,还需要解决权限的隔离,要在常见的情况做一个深挖,有什么类的角色,产生什么样的行为,对系统造成一个什么样的伤害。最基本的,一个服务出现一个情况,新入职的同学对系统很弱,线上拷贝一个程序,就出现常见的测试流量达到线上的问题。所以,我们就要去约束这种情况。
运维的角色容易被大家遗漏
在一个服务启动之前,需要做一系列的检查,要检查这个服务是不是满足我们的假设条件,是不是应该被启动。因为运维不管是人还是程序也可能犯错,系统混乱的时候会碰到一些问题,运维人员把线上的问题删了,线上注册服务路径删了,也是常见的。所以我们做了一些划分。首先root建立最基本的级别和做一些意向之外的注册。服务路径和版本号是由运维来创建的,从而保障了服务启动时,不需要去做状态检查,同时也不用考虑这个服务是不是我创建的,该不该我删的问题。
最终的目录层级由server和client来创建、访问,每个节点都有不同层次的接触。这是解决权限隔离,防止遭受一些由于误操作、服务异常等问题造成的冲击。
服务的配额更加简单,在我们服务的注册路径中,在stat下添加一个Quota文件,约束了Client角色中的User的访问权限配额,在这里进行一个限制。
我们能不能做一些自动化的调度?
调度在服务化系统中不是必须的,只是系统太混乱了或者有些东西通过人工来干涉比较难以解决,比如说实时的操作,遭受一些攻击或者春节、节日后的高峰,用人来响应可能会来不及。在线响应需要秒级别时间内给出服务调度的响应。
所以,将我们想要的调度划分为两种,一种是离线服务调度,其实直接应对的是运维人员的手工起停、手工检验、手工操作。是刚才角色划分中的运维结束,我们基于Hadoop的框架来实现:
1、确认环境、权限。
2、检查服务的依赖链和配额。
3、管理服务生命期。服务死掉的话,有一个机制起来。
4、环境回收。
如果你把它减到最简的话,由第一个Map和reduce、第二个Map组成。
比较难的事情是我们要做一个异常调度,如何在异常情况下、服务负载过高的情况下,在秒级别内把更多的资源添加进来。刚才有一个所谓的民兵服务的服务状态,因此我们所要做的工作是会启动一个监控的服务,当然这个服务也是服务化里面的一个服务而已。监控服务所做的工作是我们去扫描,需要特殊关注的服务列表去看一下。
如果扫描掉那种异常情况之后,就会去寻找民兵服务,民兵服务是一种特殊的服务,并不是所有的服务都标成民兵服务,只是把非重要的,可以给线上服务让路的标识为民兵服务。找到民兵服务之后,就去发送服务切换指令,把服务给变掉了,变掉之后服务再去做一下自助测。流程也是非常直白、非常简单,复杂的系统我们理解不了。
服务日志的收集
关于服务日志收集的需求大家都会用到,我们都需要对服务的日值或者Profiling信息进行收集工作。当然,对于离线来说,延迟比较大。我们把日志收集起来,写到文件系统上,间隔数十分钟之后,让我们的程序跑起来,把这些日志给分析一下。
我们针对待收集数据的假设,如果真的去收集很重要的数据,量很小的。量很大的数据应该是不太重要的。如果大家在现实情况中,遇见了一个又重要、量又大的需求,可以尝试想一想,是不是努力的方向错掉了。举一个例子,如果要统计服务的响应时间,要求打出来的间隔非常短,收集非常快。间隔很短量很大,收集快不了。合理的方案是我们直接获取程序内部已经有语义的信息,又回到程序后门。所以把收集分为两类:
一类被动收集。在被动收集上实施在线化。像Error、Warn、Info重要性不同。或者已经约定好的语义化的接口放在后门里面。当然,还有一个缺点需求越来越多,后门加越来越大。所以除非是常见、周期性的需求都会放在里面,非常重要的信息走特殊的通道。
现在看到的这张图是Raw和Semantic,有非常多的开源系统参考,刚才提到后门的方式跟之前的在线调度的方式近似。
我们把人人网现在服务化的架构所看到的问题描述完了,尝试对这些东西做一些总结,解决方案应该有一些基本的准则,这些准则影响着我们在不同方案中的一些抉择。
首先,系统的控制力还是人对其控制力,我们要明白业务到底要干什么,不需要一个完美的系统,我们需要一个可用的系统。要控制我们的调用链,因为调用链太长,不需要发生什么事,我们尝试去做充分的预估和充分的灾备。因为所有对系统的理解都有可能失误,都有可能造成线上的问题,这种情况下回复到一个正常的状态。系统的控制力还建立在非置信的关系上,不相信有不会挂的系统。所以整体尝试把每一个组件做得尽量小、简单。
我们不相信有充分沟通的交流。这是跟业务相关的,因为如果大家的业务是很明确的用户和服务,可以很充分的交流。但是在服务化的情况下服务的链会很长,调用A、调用B、调用C,A和C可能会就漏洞的,我们让犯错的几率小。