移动互联网、云计算和大数据的成熟和发展,让更多的好想法得以在很短的时间内实现为产品。此时,如果用户需求抓得准,用户数量将很可能获得爆发式增长,而不需要像以往一样需要精心运营几年的时间。然而用户数量的快速增长(尤其是短时间内的爆发式增长),通常会让应用开发者有些吃不消,不得不面临一些严峻的技术挑战:如何避免突发的当机事件,如何避免服务的不可用状态,如何在服务容量不足时,避免用户体验下降,等等。在系统构建之初就采用高可用和可伸缩架构,将能有效避免这些问题。

如何构建高可用和可伸缩架构呢?七牛云存储首席架构师李道兵在3月22的「开发者最佳实践日」第十期沙龙活动上给出了自己的方案。他结合自己多年的实践经验,针对一些不太复杂的业务场景,从入口层、业务层、缓存层和数据库层四个层面细致讲述了如何构建高可用和可伸缩系统。

如何实现高可用

入口层

入口层,通常指Nginx和Apache等层面的东西,负责应用(不管是Web应用还是移动应用)的服务入口。我们通常会将服务定位在一个IP,如果这个IP对应的服务器当机了,那么用户的访问肯定会中断。此时,可以用Keepalived来实现入口层的高可用。例如,机器A 的IP是 1.2.3.4,机器 B 的 IP 是 1.2.3.5, 那么再申请一个 IP 1.2.3.6(称为跳IP), 平时绑定在机器A上,如果A当机,IP会自动绑定在机器B上;如果B当机,IP会自动绑定在机器A上。对于这种形式,我们将DNS绑定到心跳IP上,即可实现入口层的高可用。

但这个方案有一点小问题。第一,它的切换可能会有一到两秒的中断,也就是说,如果不是要求到非常严格的毫秒级就不会有问题。第二,对入口的机器会有些浪费,因为买了两台机器的入口,可能就只有一台机器用上。对一些长连接的应用可能会有一些中断,这时候就需要客户端做配合。简单来说,对于比较普通的业务来说,这个方案就能解决一部分问题。

这里要注意,Keepalived在使用上会有一些限制。

  • 两台机器必须在同一个网段,不是在同一个网段,没有办法实现互相抢IP。

  • 服务监听,以往基本上都是在监听内容,而对于心跳机来讲,必须监听在所有的端口上,我们就会配合IPtables,避免内网的服务不小心暴露在外。主要由于内网其实也有心跳需求,不能为了要心跳,不小心将内网服务暴露在公开的网络上,这样会带来安全的问题。

  • 服务器利用率下降,这时可以考虑做混合部署来改善这一点。
    那么,两台机器,两个公网IP,DNS上把域名同时定位到两个IP,算高可用吗?这完全不是高可用,因为如果一台机器当机,那么就有一半左右的用户无法访问。

业务层

业务层通常是由PHP、JavaScript和Python等写的逻辑代码构成的,需要依赖于后台数据库及一些缓存层面的东西。如何实现业务层的高可用呢?最核心的就是,业务层不要有状态,将状态分散到缓存层和数据库。目前大家通常喜欢将以下几种数据放入业务层。

第一个是Session,即用户登录请求的数据,但好的做法是将Session放在数据库里,或者一个比较稳定的缓存中。

第二个是缓存,在访问数据库的一个查询表时,如果这个查询表慢,就希望将这些结果暂时放到进程里,下次再做查询时就不用再访问数据库了。带来的问题是,数据很难做到一致,因为在做高可用时,就不是一台机器了,也就是说业务成长不是只有一份,各个业务层之间很难保持数据同步。

简简单单的原则就是没有状态。在业务层没有状态时,一台业务层当掉了之后,Nginx/Apache会自动将所有的流量打到另外一台业务层的服务器上。并且由于没有状态,两者没有任何差异,所以用户完全感受不到。如果把Session放在业务层里面的话,那么面临的问题是,这个用户以前是登录的,这个进程死掉后,用户就会登出了。

友情提醒:有一段时间比较流行Cookie Session,就是将Session中的数据加密之后放在客户的Cookie里,然后保存到客户端。这样也能做到与服务端完全无状态,但这里面有很多坑,如果能绕过这些坑就可以这样使用。第一个坑是怎么保证加密的密钥不泄露。 第二个坑是存放攻击,如何避免别人获取到Session中的信息去不停地尝试的密码、验证码或者其他一些攻击手段。如果没有好办法解决这两方面的问题,那么Cookie Session尽量慎用,最好是将Session放在一个性能比较好的数据库中。如果数据库性能不行,那么将Session放在缓存中也比放在Cookie里要好一点。

缓存层

非常简单的架构里是没有缓存这个概念的。但在访问量上来之后,MySQL扛不住了,比如在Sata盘里跑MySQL,QPS到达200、300甚至500时,MySQL的性能会大幅下降,这时就可以考虑用缓存层来挡住绝大部分服务请求,就可以提升系统整体的容量。

缓存层做高可用一个简单的方法就是,将缓存层分得细一点儿。比如说,缓存层就一份的话,那么缓存层当了以后,所有应用层的压力就会往数据库里压,数据库扛不住的话,整个网站(或应用)就会随之当掉。而如果缓存层分在四台机器上的话,每台只有四分之一,这台机器当掉了以后,也只有总访问量的四分之一会压在数据库上面,也许此时数据库能扛住,网站就很稳定地等到缓存层重新起来。在实践中,四分之一显然是不够的,我们会将它分得更细,以保证单台缓存当机后数据库还能撑得住即可。在中小规模下,缓存层和业务层可以混合部署,这样可以节省机器。

数据库层

在数据库层面实现高可用,通常是在软件层面来做。例如,MySQL有主从模式,还有主主模式都能满足需求。MongoDB也有ReplicaSet的概念,基本都能满足大家的需求。

总之,要想实现高可用,需要做到这几点:入口层做心跳,业务层服务器无状态,缓存层减小粒度,数据库做一个主从模式。对于这种模式来讲,我们做的高可用不需要太多服务器,这些东西都可以同时部署在两台服务器上。这时,两台服务器早期的高可用的一个需求就完全满足了。任何一台服务器当机用户完全无感知。

如何实现可伸缩

入口层

在入口层实现伸缩性,可以通过直接铺机器,然后DNS加IP来实现。但需要注意,尽管一个域名解析到十个IP没有问题,但是很多浏览器客户端只会使用前几个IP,部分域名供应商对此有优化(如每次返回的IP顺序随机),只是这个优化效果不稳定。

推荐的做法是使用少量的Nginx机器作为入口,业务服务器隐藏在内网(HTTP类型的业务这种方式居多)。另外,也可以在客户端做一些调度(特别是非HTTP型的业务,如直播)。

业务层

业务层的伸缩性如何实现?与做高可用时的解决方案一样,要实现业务层的伸缩性,保证无状态是很好的手段。此外,加机器继续水平部署即可。

缓存层

比较麻烦的是缓存层的伸缩性,最简单粗暴的方式是什么呢?趁着量比较低的时候,把整个缓存层全部当掉,比如以前四台机器,那么可以加到六台机器。带来的一个问题,以前的缓存会有失效,会有数据不一致的问题。这时,可以将缓存全部停掉,同时再慢慢地把这些缓存启动起来,启动起来之后,再等这些缓存慢慢预热。当然这里一个要求,你真的可以这么干。如果扛不住呢?取决于缓存类型,下面我们先可以将缓存的类型区分一下。

  • 强一致性缓存:无法接受从缓存拿到错误的数据 (比如用户余额,或者会被下游继续缓存这种情形)

  • 弱一致性缓存:能接受在一段时间内从缓存拿到错误的数据 (比如微博的转发数)。

  • 不变型缓存:缓存key对应的value不会变更 (比如从SHA1推出来的密码, 或者其他复杂公式的计算结果)。

那什么缓存类型伸缩性比较好呢?弱一致性和不变型缓存的扩容很方便,用一致性Hash即可;强一致性情况稍微复杂一些。这时要使用一致性Hash,而不能用简单Hash。如果缓存从9台扩容到10台,简单Hash 情况下90%的缓存会马上失效,而一致性Hash情况下,只有10%的缓存会失效。

那么,强一致性缓存会有什么问题?第一个问题是,缓存客户端的配置更新时间会有微小的差异,在这个时间窗内有可能会拿到过期的数据。第二个问题是,如果扩容之后裁撤节点,会拿到脏数据。比如 a 这个key之前在机器1,扩容后在机器2,数据更新了,但裁撤节点后key回到机器1,这时候就会有脏数据的问题。

要解决问题2比较简单,要么保持永不减少节点,要么节点调整间隔大于数据的有效时间。问题1可以用这些方法来解决:两套hash配置都更新到客户端,但仍然使用旧配置;逐个客户端改为只有两套hash结果一致的情况下会使用缓存,其余情况从数据库读,但写入缓存;逐个客户端通知使用新配置。

数据库

在数据库层面实现伸缩,方法很多,文档也很多,此处不做过多赘述。大致方法为:水平拆分、垂直拆分和定期滚动。

总之,我们可以在入口层、业务层面、缓存层和数据库层四个层面,使用刚才介绍的方法和技术实现系统高可用和可伸缩性。具体为:在入口层用心跳,用一些平衡部署的手段;在业务层做到服务无状态;在缓存层,可以减小一些粒度,以方便实现高可用,使用一致性Hash将有助于实现缓存层的伸缩性;数据库层的主从模式能解决高可用问题,拆分和滚动能解决可伸缩问题。

bVlbH1

本文中分享的这些技巧和方法,主要想帮助不太复杂的业务场景或者中小型应用快速搭建起高可用可伸缩的系统。关于如何构建高可用和可伸缩系统还有很多更为细节的点和实践经验值得探讨,望以后能与大家做更充分的交流。