系统的高可用性是一场战役
什么样的系统是具有高可用性的系统呢?简单的说就是易于扩大系统规模。
简单的增加硬件规模、提高硬件配置,在系统初期能起到立杆见影的效果,也是这一阶段常用的扩展方式。但是随着系统规模的扩大、团队规模的增长,通过硬件的方式扩展系统带来的效果越来越不那么明显了,这时便是考验系统横向扩展能力的时候。因此说系统的高可用性是一场战役而非一次战斗。
我们应该怎么理解横向扩展能力呢?用一个词来概括这种能力就是——分治,将大的问题分解成一个个小的问题然后逐一解决,模块化就是符合这种方法的一种具体方式。
战前分析——关于设计尺度的选择
面对构建高可用性系统的需求,我们首先要进行的就是设计。有很多的设计方式、方法可供我们选择,但是我们要讨论的不是如何进行设计,而是如果避免“过渡设计”。过渡设计是“技术理想主义者”常犯的错误,技术理想主义者往往在进入设计阶段后,容易忽略原始的需求而陷入到对技术的使用中无法自拔。面对一个系统的设计方案,技术诉求与业务诉求同等重要,我们不能厚此薄彼。
过渡设计主要有两种表现:设计与实现超出原有产品的需求和使用过于复杂的实现方式(自维基百科)。面对第一种情况,我们只需要按照产品的设计实现即可避免,对于技术人员来说犯这种错误的概率不大并且犯这种错误的技术驱动源并不存在。第二种情况是“技术理想主义者”常踩的坑,而且几乎是必踩的,表现在设计复杂、代码复杂等等方面。
作为设计者,我们必须还原被设计事务的原貌,一个问题它该是简单的就是简单的,不能复杂化;反之,复杂问题也不能简单化。我们要在设计的过程中,跳出技术或者业务细节,善于从整体审视设计、把控设计,及时纠偏,这是保证不过渡设计的一种重要的方式。当然也可以在最开始制定计划的时候引入设计(实现)的检查点,设计团队(或其他干系人)定期检查、纠偏。对于过渡设计(实现)的代码,要让同组的人走读,不能有让人费解、读不懂的地方,如果为了提高性能使代码复杂一点还可以理解,但是为了体现个人能力或者个人英雄主义而使代码复杂是绝对不能允许的。
比如我们要实现一个公司内部使用的系统,那么高并发、大量用户同时在线等等类似的情况就不是设计的重点(除非这个公司有几十万人并且都要使用),我们应该把重点放到解决业务问题上。另一个例子就是很多网站都会有的站内信或者类似功能,其实它只是起通知作用,那么我们就要让业务代码远离它,还它一个纯粹的业务无关性,做到对发送者、接收者、发送内容开放,对业务逻辑、业务判断封闭。
做好后勤保障——留足可扩展空间
怎么留足可扩展空间呢?要求我们在设计之初就要考虑到未来的扩展方式和可扩展的规模(需要对技术、业务有着整体的了解和一定的前瞻性),在设计时考虑到了,我们要不要按照设计的规模来实现呢?不一定,如果预计短时间内不会有爆发性增长,那么我们可以选择实现设计规模的一部分(比如1/3、1/5等),未来可以根据实际需要再进行扩展。
举个例子来说明这点,当我们设计一个系统时,假设单表日增数据量为10万(那么一年3650万、十年3亿6500万),按照这种不变的方式,假设经过评估后知道这些数据平均分布在5张表可以满足我们的需求,我们可以得到一个按用户ID取余为5的方案写在代码中。这种做法确实解决了当前的问题,但是实际情况很可能是由于市场推广、品牌影响力提升等因素,1年后的日订单量为50万,这是原来的设计就会面临很大的挑战。那么我们应该怎么避免这种尴尬的局面呢?
要想避免这种局面,我们就需要将分表的方式进行一下优化,ID取余的数字做成可配置(可在前期设置为3,以后根据情况增大),这时候你可能会有疑问——这个数字在改变后,怎么能保证原来数据还能够在新规则下正确的查询到、新老表(由3张到5张)数据怎么能趋于平均分配呢?可能需要将原来数据进行重分配,使5张表数据再次平均,然后对外提供服务。这种方式是不是最好的呢?当然不是,当数据量很大时,进行数据的重分配是很耗时的,可能超出了业务能承受的范围,所以这种方式是有使用前提的。关于怎么能够不影响历史数据同时能够对新数据提供正常服务,可以采用老表不在新增只查询,新表接受新增、查询(当需要再次扩展时,仍然使用这种规则),具体的方式我们就不在这里讨论了,举这个例子主要是为了说明在设计之初要考虑到未来的扩展性,同时考虑到因为扩展可能带来的问题以及这些问题是不是我们能承受的。
战役前期——集中优势兵力打歼灭战
80-20原则,相信大家都听说过,在模块化的系统中,有无数多个80-20存在,我们需要做的就是将20%的功能做好、做完善、非常易于扩展,对于其余80%的功能不是放弃了,而是有选择的放弃一些完善性、扩展性、易用性其中的一部分。
仍然用一个例子来说明这个问题,对于一个电商网站(或者app),至少要面对两类用户——消费者、管理者,消费者就是广大用户,管理者就是后台功能使用者(即公司内部使用者,可能有些绝对),那么对于消费者所要访问的页面,我们要考虑到各种浏览器(尤其是那让人疯狂的IE6)、网络环境、操作习惯、用户群特征(年龄、区域等);但是对于管理者,我们如果也考虑这么多,那么可能会给整个开发带来一些困难和花费更多的时间,其实我们可以简化一下,比如只支持chrome浏览器(或者firefox等)、内网环境、统一操作方式等手段,可能会节省更多的时间和精力,换来的也许就是市场竞争中意想不到的优势,在快鱼吃慢鱼的现在,早投入市场一天的价值是不可估量的。
当然,我们也可以选择只完成最核心、最重要的那一些闭环功能投入市场,根据反馈进行调整,也是一种常用的手段。
分割包围各个击破——保持模块(子系统)的独立性
一个系统可能会包含很多个子功能,用以实现业务的不同阶段(或者不同类型)。面对这样的系统时,我们要从设计开始就有意识的将其设计成互相联系但是互不影响的模块,在各模块间采用统一的协议进行通信。这样既能方便多团队、多组同时工作,又能确保一个模块出问题不会影响其它模块。
比如一个点上系统,商品、用户、订单一定是不同的模块(或子系统),当订单系统故障时,用户仍然可以登录、浏览商品,这就是模块间互不影响的好处(关于订单系统的故障问题及处理方式,在后续的文章中会有详细介绍,这里只是为了说明保持模块独立性的好处)。
在保持模块独立性的同时,也要让每个模块具有良好的容错性。容错性能确保在被依赖系统出错时依赖系统不会出现类似404、500这样的错误,而是以一种友好的方式提示用户。比如上面的订单系统出问题时,在用户中心查看“我的订单”时,提示“暂时不能查询”、“由于故障暂时不能提供订单查询”等内容要比直接出404页面、500页面或者在页面显示一堆用户不关心的异常信息要友好得多。这就提示我们,当问题不能避免时,那就让我能提优雅的展现出来,以显示我们设计的专业性以及对用户的友好性。