2012年底,末日之后,看到大家都在写年末总结,我也忍不住想一试。工作已经3年半了,头一次写总结。虽然到现在仍是无名小码农一名,但工作这些年,技术着实有不少积累。成长最大的,当然就是这篇文章标题提到的——高性能分布式计算与存储系统的设计和研发过程,这也是我自2010年供职于国内最大的某著名网站之后,和这个系统一起成长,亲眼见证和伴随着它的发展,从一个婴儿一样的"Demo"程序,成长为现在可以处理千万级日PV的强大系统,直到2012年我离开。我也顺势积累了Unix/Linux服务器、多线程、I/O、海量数据处理、注重高性能与效率的C/C++编程等宝贵的码农财富,当然,遗憾和不足,仍然是有许多的。
【番外篇】2012年,其实是自工作以来,技术积淀最多的一年。因为,在2012年,我终于学会了独立思考,我不再像以前一样,许许多的技术只是需要用到的时候,匆忙的google(有时候还要先匆忙的先翻墙),我发现,“好记性不如烂笔头”,古训确实毋庸置疑,有大量的、琐碎的技术经验、编程细节、技巧,需要积淀下来,可能单条的细节与技巧,并不会对一个人的职业生涯产生什么影响,但把它们都积聚起来,就会强大许多,很实际的,带来的技术提升,能带来更高的Offer。所以,2012年,我开始到博客园写技术博客,和众多园友分享我对技术的一知半解,共同进步;也终于耐下心,为自己做了一个简单的个人主页,虽然10年前,我就可以做出这样的东西……;我成为了更忠实的苹果粉,所以我尝试去做iOS创业,虽然这和我的主要工作研究方向并不一致,当我看到自己做的demo在自己iPhone 4s上跑起来,我突然又有一了一种久违的兴奋——那是每一个程序员,都体会过的,小小的成就感;2012年,我开始接触和了解许多以前从来不懂的技术:Hadoop、GoogleFS、JVM、XCode、ARC……小到如何将vim打造成一个IDE……;2012年,我暂时离开了生活工作了6年的北京,来到了陌生的上海,虽然明年我可能就会回到北京,在上海这个繁华的城市,我又体会到了一种和京城码农感觉一样的,技术氛围和文化;最后必须一提的,2012年,我结婚了,并喜得一龙子,在这篇总结里,衷心的对我老婆说一声:“老婆,谢谢你,我爱你。”
【正题】接下来,该进入这篇文章的正题了, 就是简单地谈谈,我这两年,主要做的东西——高性能分布式计算与存储系统。
这个系统看名字十分厉害,所涉足的目前互联网最领先的技术领域。具体有什么用途? 在我之前供职的公司,它主要是作为中间层,给网站页面提供缓存服务的,并且,它对付的难题,是大数据、海量数据,相信,每一个日PV超过千万级的网站,都必须会有类似的系统存在,如果,你曾经看过,博客园里的《淘宝技术发展》等类似文章,就一定不会对我接来将要提到的许多概念和术语感到陌生。对于这样大流量,需要处理大数据的网站而言,由Web的逻辑直接调用管理数据存储,是非常不科学的,实际上也是不可能的,大数据、高并发的对数据库进行读写,通常数据库都会挂掉,从而使网站也挂掉,必须要在Web和数据库之间,通过技术手段实现一种“转换”或“控制”,或“均衡”或“过渡”,我不知道这样用词是否正确,你只要明白其中的意思就好了。这样的技术手段有许多,所实现的东东也有许多,我们用到的,就是被称为“中间层”的一个逻辑层,在这个层,将数据库的海量数据抓出来,做成缓存,运行在服务器的内存中,同理,当有新的数据到来,也先做成缓存,再想办法,持久化到数据库中,就是这样简单的思路,但实现起来,从零到有,可以说难如登天,但是,任何事物,都是在曲折中,不断发展前进的,这是中学我们就学过的哲学理论。这个系统,就被我们称简为“缓存系统”,它最大的好处,就是砍掉了每天上千万次的数据库读写操作,取代而之的,是读取服务器中提供缓存服务的进程所控制的内存,所以你知道,这里面节省了多少的资源申请、竞争、I/O……当然,后面你也会发现,它会带来许多新的问题,最显著的问题,就是数据的同步和一致性,后面我会讲到。
现在,让我们先看看, 这个系统,发展到我离开它的时候,长什么样子?(由于涉及到商业机密,具体的技术不能提供)
就是这样的一张架构图,代表着可以处理每日上千万PV的系统,涉及到许多的技术,让我们一个部分一个部分解读它。
首先,从当我有一个web请求到达时,将会发生怎样的事情说起。比如,我是一个用户,我在这个网站登陆,我的“个人”页面上,将会加载许许多多的东西,有许许多多的图片、文字、消息等,我们举其中一个例子,我将要得到我的好友列表——friend list。通过常识可以知道,这个friend list,不是随机的、临时的,而肯定是一个(一组)持久化存储于数据库里的数据,我们就是一个用户请求得到他的friend list说起,来解读这张架构图。如果我的网站流量很小,每天不超过10万PV,峰值可能就几百个上千个用户,同时请求他们的friend list,那么,现今任何一种语言配上任何一种数据库的搭配,只要稍做处理,都可以很好的完成这个工作——从数据库中,读出该用户的friend list,然后访回给web,如果用户对好友列表作了任何修改,web马上将修改内容写入数据库,形成新的friend list。然而,当访问流量持续提升,达到千万级、甚至亿级PV的时候,刚才说的方法就不可行了。因为,同时可能有几十万甚至上百万用户,通过web请求从数据库中读(如果写将会更糟糕)上百条万数据,数据库将不堪重负,形成巨大的延迟甚至挂掉。通过上面的系统,来解决这样的问题。
现在,我们要设计和研发的上述系统,当一个web页面提交一个获取friend list的请求后,它首先将根据一定的规则,通过负载均衡,然后到达相应的master节点。上面我们提到的是DNS负载均衡,这得众多负载均衡技术中的一种方法。也就是说,我有许许多多的master节点(上图的scalabe表明,我是可扩展的,只要有条件,可随意横向扩展节点,以提高速度、容灾、容量等指标),每个master节点的IP地址(域名)当然不一样,通过DNS负载均衡,合理地把该请求,送到相对“空闲”的master节点服务器。现在解释一下master节点服务器和slave节点服务器的功能:slave节点,主要用于"Running services",即,实际处理请求的缓存服务进程,通常运行在slave节点上;master节点,主要用于分发通过负载均衡的请求(当然,master节点上也可以运行一些“缓存服务进程”,即并发流量不高、较辅助的一些服务),找到用于处理实际请求的合适的slave节点,将该请求交给它处理,再次实现了一道“负载均衡”,同时,需要分布式计算的内容,将可能同时分发到几个slave节点,之后再对结果进行合并返回(Map-Reduce原理)。
好了,现在我们已经知道,一个friend list请求已经通过DNS负载均衡、通过master节点进行分配,到达了相应的slave节点上。我们还知道,所说的“缓存”,正是slave节点中所运行的services进程中所管理的内存,提供同样功能的service可能会有很多份,同时运行在不同slave节点上,以提供高并发和分布式计算的功能。例如,获得friend list就是这样的service,因为这个功能太常用了,所以,在我们的系统中,这样的服务可能同时提供5份、10份甚至更多,那么我这个获取friend list的请求,究竟被分配到哪个slave节点上的service处理呢?这正是刚才提到的master节点来完成这一工作。再比如,我现在需要获取“二度关系”的列表(关于六度人脉理论,可google),所谓“二度关系”,就是好友的好友,那么我要取这样的列表,即friend's every friend list,这样的请求,将会把取每个friend list分配(Map)到不同slave节点上去做(根据一定的规则),然后再进行合并(Reduce)(当然,熟悉算法的同学可能已经发现,这样去获取请求,非常的笨拙,有没有更好的方法呢?当然有!因为好友的好友,其实就是好友的friend list与我和好友的共同好友common friend list的“差集”,对吗?所以我不用去取好友的每个好友的friend list,而只用取2次就可以通过计算完成请求,这又节省了多少资源呢?假如我有100个好友,1000个,10000万个?会节省多少次计算呢?这也证明,一个良好的算法,对改善程序性能,有多么大的帮助!)
好,我们继续。现在,我的获取friend list的请求,已经在被某个slave节点中的负责这一功能的service进程处理,它将根据一定规则,给出两种可能的处理方式:
1、 我这个用户非常活跃,经常登陆网站(一定的规则,认为缓存未到过期时间),且我这个slave节点自上次“重建缓存”(即重新从数据库中读取数据,建立缓存,后面会谈)后,没有发生过down机重启行为(又一定的规则),我也没有收到过master节点发送过来要求更新缓存(即从数据库中比较数据并更新)的Notification(通知),或是在一定条件下我这个slave节点对它掌握的缓存数据版本(版本管理系统原理,思考一下svn的工作原理)和数据库进行了一次比较(注意,比较数据版本可认为只是一个int值,且是原子操作,这和比较整条数据是否一致在性能上有天壤之别)发现是最新的数据版本,那么,我这个slave节点将直接返回缓存数据,而没有任何数据库读操作,也就是说,我这一次获取friend list的请求,得到的是缓存数据,当然,这个缓存数据肯定是最新的、正确的、和数据库中的持久化数据是一致的,后面会提到怎样来尽量保证这一点;
2、第1点中的“一定规则”不满足时,即我这个slave节点的缓存和数据库中的数据可能存在不一致的没有其它办法,我必须从数据库中读取数据,更新缓存,然后再返回。但同时注意,slave节点中的service服务进程,将认为此用户现在活跃,可能还会请求一些相关、类似的数据(如马上可能进行添加好友、删除好友等操作),所以去数据库读取数据的时候,将不会只读friend list,可能与用户有关的其它一部分数据,会被同时读取并更新缓存,如果负责这一部分数据的缓存服务并不是当前的service进程,或在其它slave节点,或同时还有几份service进程在工作,那么slave节点将提交“更新缓存”请求给master节点,通过master节点发出Notification给相关slave节点的相关service进程,从而,尽可能使每一次读取数据库的作用最大化,而如果稍后用户果然进行了我们猜测的行为(可认为cache命中),结果将同第1点,直接通过缓存返回数据而且保证了数据的正确和一致性。
好了,刚刚提到的都是“读操作”,相比“写操作”, 其数据一致性更容易保证,之后我们将讲述“写操作”的工作原理。现在,让我们先跳过这一部分,继续看架构图。slave节点之后,就是实际的数据存储了,使用了MySQL、Redis,MySQL主从之间的协同是DBA的工作,不在此篇讨论,Redis主要存储K-V键值对数据,比如用户id和用户昵称,是最常用的K-V对之一,通过Redis进行存储,再结合上述的工作过程,可保证这个系统的高性能。而架构图最右下角的Hadoop与MongoDB,是可选的MySQL替代方案,其实,正是未来的主要发展方向。如果slave节点中的service服务进程与Hadoop良好结合,系统的性能将更上一层楼。顺便说一句,master、slave节点都是由C++开发的。Why C++?可参考酷壳上的一篇文章《C++ Performance per $》。
好了,上篇就讲到这里,余下的问题,我们在下篇进行讨论,谢谢大家。