文章可能比价浅显,但还是希望大家看过能有共鸣之处
前两天和同事聊天,他问了我一些问题,关于系统设计的。我说,谨记三点,基本上就不大有问题了:
- 做好读写分离
- 支持横向扩容
- 规划好每个服务/模块/类/方法的职责
第三个我就不说了,因为想说也不大能在小篇幅里说好。
1,2两点很重要。很多初创公司或者小公司,一般都是先把功能实现了再说,但我认为一个好的系统设计师,或者程序员,都应该做到:
- 让系统支持读写分离,横向扩容
- 或者至少设计时留下扩展点,方便后面对这个做支持
为什么这么说呢,我看到了不少创业公司,甚至包括我们公司部分产品,一开始不考虑这些,接着开始遇到性能问题,这个时候才开始想到要做读写分离之类的功能,但是改起来比较困难,要动大手术,产品已经积累了大量用户和内容,如果是新功能增加还好,像这种大改动,研发还是很忌惮的。所以就会想办法拖着,譬如添加各种缓存,来缓解性能和扩展性问题,这会消耗团队大量精力。而随着功能越来越多,你的各种修补也会带来障碍,并且性能会进一步下降。
横向扩容在某些程度和读写分离有比较紧密的关系,一般如果做了读写分离,那么很多情况横向扩容也就自然是水到渠成的,所以我这里重点讲的是如何做好读写分离
我们现在和产品的对接过程中,所有数据写入的是一套流程,所有查询的是另一套流程,而且是由不同的服务群集组成,所以天然实现了读写分离,并且能够很好的支持了横向扩容。
我对数据库读写分离没有做过太多工作,但很多东西我觉得是相通的,我这里只是提提自己的看法。
读写分离说起来容易,其实做起来也有些麻烦,往往一个读的逻辑环节(外部看来),其实也会有大量的写。最简单的例子是,我阅读一篇文章,文章阅读数会变更,会记录最近访问用户等,这些都是写操作。也就是大部分情况,读和写往往是混合的。
这里就会有三套很直观的方案:
- 在数据层面区分读写,应用发现是update,insert等操作就走主库,如果是select 就走从库
- 做一个proxy,该proxy 拦截所有sql,具体功能类似1所说的。
- 对写单独做一个服务,提供写API。
三种方案都需要基于数据库的主从方案。
数据库
- master
- slave-1~slave-n
以博客为例,我们看看第三种方案是如何实现的。
设计时,将所有写逻辑抽象出来,并且单独成一个服务,该服务只和数据库master打交道。原来的服务, 涉及到任何写数据库的部分,则转换为调用应用master提供的API服务,其实就是把写库操作服务API化了,读操作则直接读从库。
大概结构如下:
对我而言,肯定是偏向3的方案。为什么?因为我们要看到最后架构的演化方向。看看各个公司的实际案例就知道,一个系统最终都会被拆分成N个服务,由这些服务互相协同实现整个原来单一进程实现的功能。
所以说,读写分离是系统服务模块化的第一步。这个时候我们的系统其实由原来的单一系统演化出来了一个单独的写服务。随着压力上升即使是写服务无我们可能也会划分成多个服务,读的部分就更不用说了。
总结下,读写分离的实施应该像下面这个样子:
- 所有写操作由master实现API化
- 每个slave是一个正常的应用,接受读和写,只是内部写的时候,不写数据库,而是调用master API
对于现有系统的改造做读写分离也比较简单:
- 新建一个master项目,实现写操作API化
- 原有的程序,将所有写数据库操作都转化成对master API的调用
读的横向扩容比较好做,某些情况可能需要注意将session会话粘滞在一台服务器上。
对于写的横向扩容,我们现在的做法是,通过消息队列 ,可参看我这篇关于消息队列应用场景的介绍文章 大数据技术栈-Web框架&消息队列,所有写的动作都会发布到消息队列,然后通过后端起服务消费消息队列执行真正写的动作。所以后端是可以任意起多个服务的。在数据产品中,这个后端服务其实叫数据链路处理服务。主要执行类似ETL一类的工作,进行规整化后存入索引,HBase,Redis等存储器中。
-
读写分离是能横向扩容的基础
-
其实读写分离本质上是模块化,系统解耦
-
系统扩展性好,可以充分利用云时代扩充服务器的便利。
-
上线(更新)可以实现不中断服务。我们现在就是rolling update(有点类似灰度发布),所以能感觉到它的好。
切掉A服务器流量 -> 更新服务 -> 测试 -> 切回A服务器流量 -> 监控查看 -> 依次升级所有服务器
题外话一下,别小看上线更新发布,如果你做的不是项目,而是一个一直用的产品,系统设计能给上线带来方便的话,真是一件功德无量的事情。
前面也提到了,为什么采用方案3,读写分离是第一步,其实最后我们的目标可能还是会把一个服务拆成N个服务,协同运作。理论都是通的,这就好比这些年规模PC服务器取代中小型机是一样的。