看完这书我的第一想法就是把书名改为“网站技术架构导论”,没什么别的意思,只是觉得这名字更贴切点,当然作者肯定是不会用这种名字,否则没那个出版社敢去出版了。虽然把一个金光闪闪的名字给换成了low卡,其实并没有任何贬低这本书的意思。导论我一直认为是一个重要的词,俗话说“师傅引入门,修行在个人”,真正别人能帮你的也就是为你推开一扇门,对于那些为我们推开新大门的人我们都应该心存感激,这里我也非常感谢李智慧老师,他为我推开了“网站技术架构”的大门。
毕业第一家公司是做企业ERP(企业资源计划系统)的,从名字基本就能看出做的系统是给企业用的,一个小的企业几百人,大点的一两千人,这也就是我当时做的系统的总用户数了。见识决定格局,扯了两年外贸的业务,基本都忘了计算机还需要性能这个东西,想当然的认为架构就是组合下框架(比如:SSH),然后拿着现成的框架,再在上面写点增删改查的就万事大吉,甚至觉得框架已经有现成的,学习SSH都是多余的,只要会用就OK了。
还记得第一次去一号店面试,面试官问我Java集合、集合底层原理、多线程、亿级数据存储时,我很惊讶。惊讶并不是这些问题我不会回答,而是他居然会问我这种问题,主观的认为这些都不是一个程序员应该关心的事,程序员只要理解业务,然后用简单的平台提供好的方法来实现这些业务不就可以了,就像我会开车就行了,干嘛要知道车是怎么做的呢。后面的沟通中他说了一句话让我难以忘怀,“业务做一段时间大家都可以学会的,技术才是我们专业人员的核心”。当时对技术是核心我并不理解,只是觉得他说的很有道理,于是对技术有了一点尊重,知道软件除了业务外,还有技术这么一个东西。
后来进了一家互联网半外包公司(部分产品+部分定制开发),而我做的就是哪部分定制开发的事。突然发现做APP比做ERP简单多了,简单的页面、简单的业务、简单的用户,虽说人们开口都是海量数据、亿级用户,但那时我开发出来的APP都没有日活过千的。再则互联网用户的使用频次是远远低于ERP用户的使用频次,觉得给这种系统配个独立的服务器都是浪费资源,自然是没有性能、架构什么事了。有了这些亲身经历,在加上一些客观原因(设计数据库,都不知道什么是范式,什么是数据冗余,而且表里的记录没有状态,删除信息都是直接用delete,居然夸夸其谈我们要在项目里用redis来解决亿级用户的存储和查询性能问题......)。我很不喜欢说海量数据、亿级用户的人;如果是技术,我只想对他说,你先做出一个稳定的、能给几百人正常使用的系统再说;如果是客户,我只想说,你老有空还是多想想运营的事,怎么让日活过千吧。
也就那样一如既往的做着虎头蛇尾的项目(做之前要求很高,后来基本没用,销售和产品经理在这里功不可没),后来一次偶然的机会,带来了一点点改变。大约是16年5月,来了一个我们分部总监亲戚的项目,我们就管这项目叫A吧。A是一个充值、购买、支付、返还的项目,当然也是需要支持百万用户,三个月完成等等;不同的是这是客户之前已经做个这个产品,只是之前做的有问题,这次是重做的,希望稳定安全。也不知啥原因,总监就让我主要负责来开发这个项目,百万用户我是不怎么相信;不过系统里记录、计算的数据都是钱,这可一点都不假,再加上这是第二版,这给了我很大鼓舞,相信努力去做的东西是有价值的,而不是去忽悠客户的开发经费。
我猜可能是客户吸取了做第一版的经验,这次提的基本都是合理的要求,于是我们也很顺利的用三个月左右的时间完成了项目。客户也是非常积极的去运营这个产品,一年多的时间差不多有大几万用户,交易额也破亿了,系统这一年多的运行基本达到了客户的要求,虽说中间也出过一点小问题,但都被及时处理和修复。这算是我做的第一个有实际意义的互联网产品,也是在这个产品中遇到的问题,让我认识到架构是实实在在有用的,不同数量级用户设计、开发也是大有不同,在后面具体讲到的不同数量级网站架构的地方,会详细去讲解。
说了这么一长串我的认知历程,肯定不是为了证明架构是最重要的,我们要努力深入学习大型网站架构。恰恰相反,我不并认为我以前认为技术架构没用是错的,实践是检验真理的标准,我负责做的项目一般都能达到目标、正常运行,怎么会有错呢,总不至于非要100w做个淘宝出来才算对吧。其实我没错,技术架构也是有用的,因为有一个前提条件,就是应用场景,抛开应用场景谈一切技术、需求都是扯淡。这一点李智慧老师在书中有很经典的阐述:“大型网站是演化而来的,不是设计出来的”,写我的认知历程既是为了诠释李老师书中的精华思想,没有绝对最好的技术方案,只有适合的方案,随着业务发展方案也是随之变化的。
互联网的大用户经验我是没有的,最大用户量的系统就是上面的A了。好的一面呢,是这个系统是我从零建立起来的,而且一直伴随着它的成长,中间也遇到一些问题,自己也学习了一些知识,在这里希望能把这些经验、知识完善成一个完整的体系。一方面作为一个技术沉淀,让以后再遇到类似的问题时,有一个参考;另一方面也做为自己技术的提升,通过系统客观的分析,能更近一步优化自己的架构和设计。
一万用户(用户上限不超过一万)网站架构应该是最简单的网站架构了,应用程序、文件、数据库都在同一个服务器上,下面直接贴出网站架构图:
这算是最简单的架构了,开发设计上也没啥要求(由于本人是做Java开发的,后面的设计开发相关的也都以Java的知识来举例了),基本上来就是一套SSH框架,对增删改查封装下,基本上用着这些方法就能搞定一个系统啦。个别的复杂些的统计,可能需要写少量的sql查询语句,这个一般的开发人员都会,并且在这种极小的数据量情况下,能写出性能问题那也是需要点技术。
这里单独拿出来提的一点就是,当用户量超过5000的时候,注意把数据库连接配置修改下;比如proxool的默认线程活动时间是5分钟(maximum-active-time:如果housekeeper 检测到某个线程的活动时间大于这个数值,它将会杀掉这个线程,默认是5分钟.),而使用hibernate(保存一条数据35ms左右),一分钟大约写/改两千条数据,如果出现什么对全局用户的批量修改,可能会因为数据过多超出了线程线程活动时间,造成部分修改成功,部分修改失败的情况。
十万用户的网站架构就略微复杂点,应用程序、文件、数据库一般都要放在独立的服务器上,按规矩首先还是贴出网站的架构图:
这个和万级用户的架构主要改变就是,把之前放在一个服务器上的应用程序、文件、数据库分别放到三个服务器上,实现了应用与数据的分离,这也是系统优化最基本的思想之一。由于在逻辑上应用程序、文件、数据库本身就是分离的,这次只是部署到不同的服务器上,所以在软件上需要修改的很少(主要就修改下图片、数据库的访问路径前缀),总的来说还是很容易的。
为了更好的利用资源,我们可以分别对这三个服务器的配置做一点调整。应用服务器,部署的是应用程序,主要是用来处理业务逻辑,对CPU要求高,需要配个好点CPU;文件服务器,主要用来存储大量用户上传的文件(主要是图片,视频占带宽一般放到第三方服务器),需要大的硬盘;数据库服务器,数据库做了大量的数据缓存和磁盘检索,需要读写速度快的硬盘和大内存。一套整下来,¥5w能选个不错的配置了,如果使用第三方云服务器成本就更低、选择也更灵活了。关于云服务器插说下,有些人对它有误解,认为服务和数据在别人那里不安全,其实不然,像阿里云服务器的安全性、稳定性就很高,而且监控措施也很齐全,反而是远远优于大多数自建的机房。真正限制使用云服务器的应该是业务需求,比如金融行业,有监管要求,你就不能使用云服务器了;比如做高频交易等,对实时性要求很高,那也是不能使用云服务器的。如果没有特别的需求,这个级别的用户量,个人建议优先选择云服务器,毕竟在成本、稳定性、安全性、扩展性都有压倒性优势。
在程序设计上,主要还是数据的批量操作和数据统计的问题。下面就简单说下我实际中遇到的问题以及解决方案,不是说当前我用的解决方案就是最合理的,只是通过了实践的检验,写出来提供给大家作为一种参考。
比如要群发消息,hibernate写的速度也就每分钟两千条不到,数据量小的时候调整下线程的活跃时间还能凑合(其实用spring事物管理机制,一次修改大量记录而没有实时提交,是可能造成其它线程阻塞的,上面的方法只是临时使用下,正常情况,一次事物时间不要超过1分钟),如果是10w条,那肯定是没得凑合了;这个时候就需要改变我们程序的策略,使用java的批处理,直接提交拼批处理的sql语句,一般情况下能达到每秒5w条的速度,10w条数据也就2s的事,加上查询用户信息1s,加上解析1s,总共也就4s自然是能接受的。像发消息这种比较简单的业务可以用批处理sql语句来解决问题,但在实际生产中还有复杂的需要批量操作的业务,就无法通过简单的批处理sql来解决。
比如我做的那个定时返还的业务,每天晚上会按照业务规则对每个用户返还积分,首先需要查询用户信息,判断返还条件,计算对应返还值,记录返还明细,记录账单信息,同步积分的相关变动,这么复杂业务如果选择拼sql语句来完成,显然不大明智。对于这个问题,我当时的解决办法是在用户表添加一个状态字段,用来标识返还状态,然后用定时程序来扫描这个状态分配执行任务,如果状态为false,那我就对这个用户执行一次返还操作,并将状态修改为true;每天用另外一个定时程序,将全部用户的返还标识状态修改为false。用一个状态成功的将批量操作拆开来做,而且oracle每秒能修改5w条记录的状态,在批量修改用户状态时也完全没有性能问题。
至此实践中遇到的数据批量操作的问题都解决了,不过定时返还业务的解决方案显然是不合理的。不合理的地方有很多,比如扩展性差(如果100w用户,那最重要的用户记录要被全部锁上20s,显然不能接受),比如逻辑不严谨(因为是每天定时去更新状态,万一返还的定时任务出了问题,在你更新的时候有的用户上一次返还没来得及执行等),我觉得最严重的还是耦合的太高、通用性差,因为要给用户表加一个没用的字段,总不能来一个业务就加一个字段吧,这显然只是投机取巧的一种手段,而不是对一类问题的解决方案。能用这种不是办法的办法,只能说那时对数据的处理知识了解太少,现在看来这都是常见问题,有现成的方案--消息机制,无语的是这种方案我自己还用过,居然换个场景就傻了,还是技术没学到位啊。
消息机制是解决批量数据、大数据的通用方案,所有涉及到的地方,最先考虑的就应该是消息机制。还是以定时返需求还为例,用消息我们是怎么解决问题的。首先,我们建立一张“内部系统消息表”,用来负责记录要执行的任务,将所有要执行的任务都拆分成消息记录的形式,写到消息表中,然后再分发执行完成这些消息即可;这里我们就可以对每一个有返还积分需求的用户生都成一条返还消息,然后再用程序一条一条的处理掉这些返还消息就可以了,结合群发消息的知识,我们知道2s就能生成10w消息记录,所以此方法是可行的。该方案核心是实现了批量数据处理与业务的分离,解耦始终都是程序优化重要的方向之一,耦合度越低我们处理的方法就越多,处理起来也更简单,随着数据量的增加和业务的变化这种优势越明显。
这是一块很泛的业务,而且不同的处理方式速度差异特别大,这里还是以返还的积分统计为例,简单的描述下相关的处理。因为每天都要返还积分,一天几万条,一年的返还记录也得过千万。oracle数据库,对一千万条数据进行分组统计大约10s左右,这显然是不能接受的。于是我用定时任务统计每天的返还总额,再后面做返还积分的报表时,只需对每天返还的总记录进行sum()就可以了,几百条数据的按月份分组统计对数据库来说就很容易了。但是这样合并后适用范围就很小了,比如我想按区域分组统计就不行了。一百万以下的统计基本是不需要做任何特殊处理,超过了我们就需要根据报表和数据的实际情况来制定方案了,总的来说全局统计不是高频功能,只要在接受的时间范围内计算出来都是可以的;对应非全局的统计,可以通过索引的设置,间接的降低查询数据量。
处理器:Inter E5506 2.13GHz;
安装内存:4GB;
操作系统:Windows Server 2008(64位);
测试工具:Oracle SQL Developer;
测试语法:
insert:insert all into 表名(字段名)values(字段名)select 字段名 from 表名 where 条件;
delete:delete from 表名 where 条件;
update:update 表名 set 字段 where 条件;
有主键,固定ID(总测试总记录50w条):
insert: 50w, 3s, 4.3s,2.6s,4.2s,2.5s(平均3.3s,每秒15w条);
delete: 50w, 22s,22s, 35s, 43s, 26s (平均30s, 每秒1.7w条);
update: 50w, 19s,14s, 14s, 19s, 18s (平均17s, 每秒2.9w条);
有主键,自增长序列ID(总测试总记录500w条):
insert: 50w, 61s, 61s, 72s, 59s, 61s (平均63s,每秒0.8w条);
delete: 50w, 16s, 13s, 8s, 15s, 12s (平均13s,每秒3.8w条);
update: 50w, 21s, 19s, 24s, 25s, 20s (平均22s,每秒2.3w条);
无主键,固定ID(总测试总记录1000w条,表名z_wog):
insert: 50w, 3s, 2.2s,2.3s,3.5s,5.3s(平均3.3s,每秒15w条);
delete: 50w, 29s,27s, 29s, 21s, 18s (平均30s, 每秒2.5w条);
update: 50w, 31s,28s, 34s, 36s, 36s (平均33s, 每秒1.5w条);
select count(*) from z_wog where id>=3600000 and id<=3600100 (平均9.3s);
select sum(id) from z_wog where id>=3600000 and id<=3600100 (平均9.3s);
select sum(id) from z_wog group by user_op (平均9.5s);
select * from z_wog where id=3600100(平均9.5s);
select * from (select A.*, rownum rn from (select * from z_wog order by id desc) A where rownum < 100000) where rn > 0; (前10w条平均22s,随着行数的限制查询时间变化较大)
测试工具:java 批处理;
有主键,有索引,固定ID(总测试总记录1000w条):
insert: 100w, 33s, 16s,43s,38s,41s(平均34s,每秒2.9w条,分次提交,每次提交0.5w条);
insert: 100w, 11s, 22s,18s,17s,14s(平均16s,每秒6.3w条,分次提交,每次提交1w条);
insert: 100w, 14s, 16s,26s,20s,22s(平均20s,每秒5w条, 分次提交,每次提交2w条);
有主键,有索引,自增长序列ID(总测试总记录2000w条,表名z_yog):
insert: 10w, 14s, 14s,15s,14s,15s(平均14s,每秒0.7w条,分次提交,每次提交0.2w条);
insert: 10w, 13s, 14s,14s,13s,14s(平均14s,每秒0.7w条,分次提交,每次提交0.5w条);
insert: 10w, 13s, 15s,15s,15s,14s(平均14s,每秒0.7w条,分次提交,每次提交1w条);
select count(*) from z_yog where id>=3600000 and id<=3600100(平均0.01s);
select sum(id) from z_yog where id>=3600000 and id<=3600100 (平均0.01s);
select sum(id) from z_yog group by user_op(平均16.7s);
select * from z_wog where id=3600100(平均0.01s);
select * from (select A.*, rownum rn from (select * from z_yog order by id desc) A where rownum < 100000) where rn > 0; (前10w条平均1s,随着行数的限制查询时间变化较大)
到这里关于十万用户的系统架构及软件设计讲的差不多了,主要是在数据库的操作上做了大量的改进,改进最根本的原因就是hibernate保存数据的速度满足不了需求,于是我们需要寻找更快的方式。通过查阅资料和测试,我们发现hibernate每秒保存30条记录,而sql的insert能每秒5w条记录、update每秒2w条记录,通过业务分析,很明显sql的速度能满足我们当前的需求,于是在需要批量操作数据的地方,我们用批处理sql来实现了。由于批处理sql存在本身缺陷,不能很好的执行复杂的业务,于是我们引入了消息机制,实现了业务与数据批量操作的解耦,比较妥善的解决了问题。
在应用服务器和文件服务器我没有去做什么改进,主要是没改进的需求,至少从目前的运行情况来看是这样的。可能会有人觉得10w用户一台Tomcat不够,毕竟Tomcat支持的最优线程就200-300,怎么支持这么多用户同时请求呢?这里我们简单的算下就明白了,假如平均每天每个用户请求50次服务器接口(这已经是非常高频使用了),100000*50/(60*60*10)=139(次),按照每天10小时算,每秒139次,即使按照峰值翻倍也就每秒278次,完全在Tomcat的支持范围之内(这里的应用服务器只分析了Tomcat,实际中Tomcat一般与Apache/Nginx联合使用,一个用处理动态资源,一个用来处理静态资源,因为这种用户的并发量还没到Apache/Nginx服务器的瓶颈,也就没有对Apache/Nginx进行分析,当然分析Tomcat时也只考虑tomcat需要处理的动态资源数据)。关于文件服务器那主要就是磁盘和带宽了;2T的磁盘,平均每个用户20M,如果对上传的图片压缩下,差不多能存100张,这足够用了;至于带宽,那可以根据实际情况随时向代理商购买。
在我看来,这是最实用的,也是我最想花时间写好的。从实际业务场景来看,99%系统的真实用户过不了百万,熟练精通百万用户架构,基本可以解决你遇到的问题;从技术的角度来看,百万级用户是传统技术架构支持的峰值,再往上就涉及到业务拆分,那系统的复杂度和成本就与当前不在一个量级了,使用需慎重。首先还是贴出网站的架构图:
从架构图我们可以清楚的看到,系统由负载均衡服务器提供统一的入口和出口。这样做好处就是系统架构简单,便于管理,只要在入口处做好管理,整个系统与外界的交互也就搞定了;不好的是所有流量都要从这一个节点流入流出,会造成单个节点的并发压力很大,尤其是当用户快速增长时,问题会愈加突出。上面的架构不仅仅在负载均衡服务器地方存在瓶颈问题,在数据库服务器那里也存在同样的瓶颈问题;在系统架构里只要存在不能进行集群或者是分布式的地方,随着业务量的增长最后都会成为系统的瓶颈,而这些瓶颈就是限制系统提高并发量的主要问题。
当前的这种架构的瓶颈就限制了系统的最大支持量只有百万级,如果要获得更高的并发量,就必须解决掉上面的瓶颈。比如负载均衡服务的问题,可以用DNS域名解析,将请求分发到不同的负载均衡服务器;比如数据库服务器的问题,我们可以对数据库进行分表、分区、分库、分片等。但采用了这些方案后,意味着我们的系统将会变的更复杂,更难维护,成本自然就更高了;所以能用简单的方案解决问题时,我们就尽量不要用复杂的方案。关于百万级以上的问题,我们到后面再具体讨论,这里先解决怎么用这套系统架构去支持百万级用户的问题。
这个级别的用户量,负载均衡服务器首推Nignx。我选Nignx的原因很简单,开源免费、简单稳定、社区资料齐全、并且满足我当前的需求。前面三条是主流服务器的通用优点,下面就详细分析下是否满足我们的业务场景。
Nginx官方给出的数据是,最高支持5w的并发数;查了一些第三方给出的资料,一个8核+16GB的服务器正常情况下能支持2-3w的并发数。我们的业务场景是最高支持百万真实用户,但我们需要知道的是某一刻的最大用户数,这就需要我们用一定的模型进行推演,用总的用户数预测出某一刻的最大峰值。预测模型:100w真实用户--》30w月活--》10w日活--》3w在线峰值(其实大多数系统是达不到这样比例的日活和峰值)。3w用户同时在线,也并不是每个用户都同时不停的发请求,所以一台Nginx支持百万用户系统的负载均衡是ok的。
上面给出预测模型,只是一种分析的指导方向,并不是说每个系统就真的存在这种比例情况。比如QQ,注册用户8.3亿,同时在线的有2.3亿;比如百度地图注册用户5亿,日活则只有0.3亿,同时在线的人数估计还要降一个数量级;不同的性质的系统,差不多的注册用户量,最高并发数差距可能达到100倍。所以要对系统的业务、场景、用户性质有全面深入的了解,然后在做对应的预测模型,推导出系统的最高并发量。
Apache:作为目前使用最广泛的web服务器,大多时候市场占有率超过50%,稳居第一。社区高度成熟,支持模块多,功能齐全,性能稳定(比nginx更稳定,bug更少);但是对高并发的支持不是很好,因为底层使用的是同步多进程模式,对高并发的支持数远低于nginx,一个8核+16GB的服务器正常情况下能支持2-3k的并发数。所以在一些对并发数要求不高的系统中,比如企业的ERP、OA等,这些用户数很稳定,并发数也不会太高,这个时候Apache服务器就非常合适了。
Microsoft:作为目前使用量第二的web服务器,由于平台的问题,一般是微软开发体系使用居多,本人是做java的,就不去瞎评论了。
Nginx:作为目前使用量第三的web服务器,最主要的优势就是并发数高、简单、免费。一个8核+16GB的服务器正常情况下能支持2-3w的并发数(对请求采用的是异步处理方式,减少了不要的等待时间),比较适合互联网企业使用,能以较低的成本,为更多的用户提供服务。
LVS:这个是免费产品中性能最好的负载均衡服务器,但配置复杂,对使用人员要求很高,另外LVS的支持范围也有限。LVS一般是在Nginx,甚至是F5负载均衡满足不了需求时,才会使用。LVS是数据链路层负载均衡,使用三角传输模式,与反向代理负载均衡的传输模式不同,很好的提高了并发量。简单的说LVS只是做请求的转发,具体返回值由对应服务器直接返回给客户端;而Nginx等服务器,不仅仅做请求的转发,还负责将应用服务器返回的结果,写回给客户端。
Tomcat:这个是应用服务器,一般用在Apache/Nginx/LVS(这些服务器都是只支持静态文件、数据)后面,提供动态数据服务。一些小系统,为了方便,直接把tomcat拿来当web服务器来用,也是能正常访问的。但是会给一些初学者带来混乱,搞不清楚它们之间的关系,所以这里把tomcat单独列出来。
这里简单的介绍了几种常见服务器的市场情况、产品性能、核心原理、以及适合的应用场景,希望让我们对服务器有一个整体上的认识。这里特别强调一点,负载均衡和反向代理是两个完全不同的东西。负载均衡:是指将请求合理分配给集群里的应用服务器,充分发挥出应用服务器的性能。反向代理:是指代理客户端去请求数据,比如Nginx接受到客户端的请求后,将请求代理给Tomcat应用服务器,得到返回值后,在把结果返还给客户端,在中间起到了代理客户请求的作用。只不过Nginx/Apache同时具备负载均衡和反向代理的功能,而LVS就只有负载均衡的功能。
架构图上我们清楚的看到,应用服务器主要包含三块内容,应用程序、本地缓存、数据访问模块。那我们下面就具体探讨这三块内容。
应用程序就是Java开发好的程序,直接打包部署到应用服务器(tomcat、jboss、weblogic)下面就可以了。唯一值得一提的就是应用服务器的选择,在当前架构下,建议使用tomcat服务器。由于采用了可伸缩的集群架构,应用服务器的性能就不那么重要了,而且应用服务器与其它模块耦合度很高,与当前体系架构系统协调反而会更重要些。
目前Java的主流开发体系有两种,一种是官方标准的EJB3,另一种是来自开源社区的spring。EJB3作为企业级开发官方标准实现,支持组件的分布式部署,一般与jboss联合使用,能充分发挥其性能优势,理论上无用户量限制;spring最流行的java开发体系标准,十分自由灵活,可以自主集成大量第三方组件,一般与tomcat联合使用,简单方便。关于EJB3和spring个人认为是“既生瑜何生亮”,两者都很优秀,只不过在互联网上,我更倾向于spring;毕竟spring的起点低,在互联网系统前期EJB3里的大部分功能并用不到,这时候spring简单好用的优势就很大了;至于发展壮大了,那是以后的事,毕竟互联网公司首先要解决的是生存问题,如果能活下来并发展壮大,那一切问题都不是问题(比如淘宝就是从PHP开始的,后面改用Java,再后面直接请Sun公司的人来指导使用EJB,换了这么多次又有什么关系呢)。
关于EJB3与spring开发体系,tomcat与jboss、weblogic等服务器的选择问题,涉及面太广就不深入讨论了。纯从技术来说,EJB3适合做企业级开发,对安全性、稳定性要求很高,用户量也是比较确定的系统;spring适合做互联网开发,系统变化很大,但对系统稳定性安全性要求不是特别高的系统。从目前市场情况来看,spring占据大部分市场份额,开发人员众多、资料齐全好找,更有利于项目的开发。
关于缓存,是一把双刃剑;一方面你需要使用缓存,来提高访问速度,减少程序与物理数据源的直接交互;另一方面,也会带来数据不一致问题,给服务器的集群带来很大的不便。关于缓存最典型的就算session(用户相关信息数据)了,下面就以session为例分析下我们的本地缓存。
最理想的肯定是把session直接缓存到应用服务器上,这样程序取数据就直接从内存里取,100ns就能拿到数据,快的飞起,实际上小系统也都是这么做的。问题是当用户量大了,一台服务器满足不了需求,需要多台服务器集群就麻烦了。比如,用户登录请求的是A服务器,但是查订单信息请求的是B服务器,那么session是缓存在A上,B上面没有,这自然是不行的。
这个时候你可能觉得这个问题好解决啊,方法一,在登录时我把A的session复制到B上不就ok了。确实应用服务器也提供了这种功能,其中jboss、weblogic对缓存的同步做的比较好,tomcat则差很多(官方建议:做缓存同步服务器不超过4台)。当用户量不大时这种方案确实可行,当量多时就麻烦了;比如一个用户的session为100KB,10w个用户,那么就缓存session就10G了,占用大量昂贵的内存肯定是不划算的,更麻烦的是多台服务器同步session时,会占用大量网络带宽,甚至拖垮网络系统。
方法二,你也别每台服务器都去同步session这么麻烦了,直接在负载均衡时就给你分好,同一个IP的请求都转给同一个服务器,这样既不浪费内存,也不占用网络带宽,貌似完美解决方案。正常情况这样确实没毛病,有问题的不正常才是软件的常态啊,比如用户切换网络了,IP就变了,现在很多银行的APP,你切换网络后就提示你安全有问题;再比如,要添加一台服务器,或者那台服务器坏了,IP的分配策略就变了,这时候缓存就全乱了;还有后期如果涉及到模块拆分,更GG了。
关于session的缓存问题,无论是方法一还是二,都不是良好的解决方案,所以在此种架构下session并不建议存在本地,而应该使用远程分布式缓存集中存储,这样就将session和应用服务器分离,彻底解决了缓存问题。应用服务器如果没有了公用缓存的限制,只缓存服务器自身的数据,那么部署集群上就方便多了,并且所有的服务都可以做成面向无连接的,负载均衡服务器也可以根据运行情况调整请求分配,并且动态增减服务器都不受影响;另一方面,使用分布式缓存,缓存的数据量可以更大,安全性也更高,集中起来更便于管理。
使用中心缓存是为了解决各个服务器缓存不一致的问题,如果该部分缓存只和当前服务器有关,比如一些静态文件等,还是要缓存到本地提高访问速度;只有是公共、并且还会出现修改的部分,都应该放到远程分布式缓存服务器上去。
关于数据库的读写分离,spring和中间件都能做。用spring来做,只需要在配置文件中配置多个数据源,然后用aop对读写进行自动分离,也可以在代码里手动指定数据源。用中间件(比如:proxy,mycat等)来做,引入中间件配置好后,程序直接连中间件就可以了。如果仅仅只做读写分离,就主从两个数据库,是用spring还简单些,性能也更好;如果是1主n从,还要考虑负载均衡,那自然是要用中间件了,这样不用自己去做负载均衡;如果考虑到后期扩展,需要分库、分片,那就只能用中间件。
100w用户量应该还在数据库正常的接受范围,最多对某些可能大的表(比如:账单、消息、日志),进行简单分表、历史数据分离处理下就好了,这里的分表是指将不同类型的账单、消息、日志分表,仅仅从业务层面的水平拆分,而不是分布式数据库中的分片拆分。仅仅考虑当前的用户量,还是建议使用spring,毕竟简单、好用、不加班才是王道。
从本质上来将,Memcached只是一个单一的key-value内存Cache;而Redis则是一个数据结构内存数据库。因此Redis除了单纯的缓存作用外,还可以处理一些简单的逻辑运算,做消息队列代理等。缓存服务器这一块推荐使用Redis,在性能都能满足的情况下,我们选一个功能强大的,能给我们开发、运维省不少事呢。最常用的,我们要向每个用户都产生一条消息,这个时候用Redis的list就能完美解决了,而Memcached则没有这样的便利。缓存是系统很重要、也很麻烦的一环,涉及的内容太多,其实还有像MongoDB、HBase等非关系型数据库也是可以用来做缓存的,这里就不展开讨论,后面会专门写一篇关于分布式缓存的分析。
文件存储这一块目前还比较混乱,主流的文件系统就不下10种,可以说是各有千秋,当然这和文件的复杂性也有很大的关系(比如:视频文件,和图片文件两者差别就很大),只能说根据实际情况再做选择吧。目前通用性较强的的就属HDFS和FastDFS了,两者资料文档齐全,配置相对简单,推荐优先使用,就目前这种用户量,优先考虑方便好用,因为不管用哪种文件系统,性能都是绰绰有余的。
关于文件系统好的一面就是,不管哪种文件系统,或者是第三方服务商提供的文件云存储,使用方式都是固定的。1、客户端上传文件到应用服务器,2、应用服务器将处理后的文件上传到文件服务器,3、文件服务器将文件路径返回给应用服务器,4、应用服务器保存文件路径,5、应用服务器将文件路径返回给客户端,6、客户端通过文件路径直接访问文件。这样文件系统就比较独立了,和总系统耦合度较低,总的来说还是比较好控制的一块;如果对文件仅仅只做存储,而不需要进行数据分析,那处理起来就更容易了。
在当前的这种架构下,数据库的访问压力并不比十万级大多少,可能感觉很奇怪,不过这是真的。主要原因就是采用了缓存技术,这样可以降低很多程序对物理数据源的直接访问。比如用户接口请求日志,就可以先写到缓存中,然后批量同步到数据库;比如用户对首页、常用列表的访问,可以把这些数据都缓存起来,就不用每次都访问数据库;比如用户的信息,可以直接缓存起来,每次查询时读缓存就可以了等等。
通过一系列缓存后,除了一些实时更新的数据,需要立即写到数据库,其它大多可以用缓存解决,这样就很大程度上减轻了数据库的访问压力。即使是一些写操作,如果是非实时立即写入的,通过缓存过渡后,采用批处理一次写入,也能极大的提高写入性能。比如用户访问日志,如果每次都向数据库写记录,则用户请求1w次接口,就需要向数据库写1w条记录,以hibernate 35ms每条的速度,需要350S;如果使用了缓存,批处理写入1w条数据,1s左右就搞定了,这就是缓存带来性能的飞跃。
关于数据库的读写分离,初学者可能以为是增加了一台数据库服务器,自然是性能提高一倍。其实不是这样的,读写分离是为了将读和写分开,我们知道当前主流的关系型数据库(oracle、mysql)都是行级锁,这样当同时对同一张表进行写入、查询操作时,是会有线程阻塞的,尤其当阻塞严重时,会使系统性能急剧下滑、甚至崩溃;如果进行了读写分离后,就不存在这样的问题,主库主要用来写,查询少,这样阻塞很低,从库只读,是不存在阻塞的;这样不但能将单个的数据库服务器性能发挥到最佳,而且稳定性也大有提升,并且做了主从备份后,数据的安全性也得到了提高。总的来说读写分离虽说复杂了点,但带来的收益还是可观的,在条件允许的情况下还是建议做下读写分离,比如在百万级用户情况下,就强烈建议做读写分离了。
关于消息任务的批量处理,引入了缓存机制后,这个问题我们相对就好处理了。比如要向系统每个用户都发一条消息,之前我们是用批处理一次向数据库写入100w条记录,这样会耗时比较长,容易造成程序阻塞;有了缓存后,我们可以先把100w条记录先写到缓存中(如果用redis,每秒能set 5w次,100w条数据如果合并处理下,1s内就能搞定),然后再分批同步到数据库,基本上是不会有什么性能问题;这里麻烦的是,在数据同步时要手动处理事务问题,还要处理缓存中数据丢失问题。
总的来说,数据库除了做下读写分离,其它需要调整的不大,而且读写分离的控制在应用程序那块已经讲了,这里就不再重复。至于主从数据库的数据同步,不管是oracle,还是mysql都提供了同步的方案,参照配置下就好了。