购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.2 系统设计要点:在设计阶段提前规避问题

在系统设计时,如果能预先看到一些问题,并在设计层面提前解决,就会给后期的开发带来很大的便捷。相反,有缺陷的架构设计可能会导致后期的开发工作十分艰难,甚至会造成“推倒重来”的情形。因此,在系统设计阶段,应该尽可能地规避项目开发中可能会遇到的各种问题。本节选取了几个经典的问题进行介绍。

2.2.1 Session共享问题

在Web项目中,Session是服务端用于保存客户端信息的重要对象。单系统中的Session对象可以直接保存在内存中,但在分布式或集群环境下,多个不同的节点就需要采取措施来共享Session对象,具体可以使用以下3种方式。

1.Session RepIication

Session Replication是指在客户端第一次发出请求后,处理该请求的服务端就会创建一个与之对应的Session对象,用于保存客户端的状态信息,之后为了让其他服务端也能保存一份此Session对象,就需要将此Session对象在各个服务端节点之间进行同步,如图2—4所示。

图2—4 Session Replication

Session Replication的这种Session同步机制虽然能够使所有的服务节点都拥有一份Session对象,但缺点也是很明显的,具体介绍如下。

首先容易引起广播风暴。试想,如果有50个服务节点,当一个节点产生了Session对象后,就需要将该Session对象同步到其他49个节点中,因此会增大网络的开销。

其次会造成严重的冗余。如果有多个用户同时在访问,那么每个服务节点中都会保存多个用户的Session对象。服务节点的个数越多,Session冗余的问题就越严重。因此,Session Replication方式只适用于服务节点较少的场景。

2.Session Sticky

Session Sticky是通过Nginx等负载均衡工具对各个用户进行标记(如对Cookie标记),使每个用户在经过负载均衡工具后都请求固定的服务节点,如图2—5所示。

图2—5 Session Sticky

客户端A的所有请求都被Nginx转发到了应用服务X上,客户端B的所有请求都被转发到了应用服务Y上,因此,各个服务中的Session就无须同步。但此种做法也有严重的弊端,如果某个服务节点宕机,那么该节点上的所有Session对象都会丢失。

3.独立Session服务器

除了Session Replication和Session Sticky两种方式外,还可以将系统中所有的Session对象都存放到一个独立的Session服务中,之后各个应用服务再分别从这个Session服务中获取需要的Session对象,如图2—6所示。在大规模分布式系统中,推荐使用这种独立Session服务方式。这种方式在存储Session对象时,既可以用数据库,又可以使用各种分布式或集群存储系统。

图2—6 独立Session服务器

2.2.2 优先考虑无状态服务

在使用了“独立Session服务器”后,应用服务就是一种“无状态服务”,换句话说,此时的应用服务与用户的状态是无关的。例如,无论是哪个用户在什么时间发出的请求,所有的应用服务都会进行完全相同的处理:先从Session服务中获取Session对象,再进行相同的业务处理。

读者也可以根据“有状态服务”来对比理解“无状态服务”。“有状态服务”是指不同的应用服务与用户的状态有着密切的关系。例如,假设应用服务A中保存着用户Session,应用服务B中没有保存,之后,如果用户发出一个请求,经过Nginx转发到了应用服务A中,那么就可以直接进行下单、结算等业务;而如果用户的请求被Nginx转发到了应用服务B中,就会提示用户“请先登录……”。类似这种不同应用服务因为对用户状态的持有情况不同,从而导致的执行方式不同,就可以理解为“有状态服务”。

总的来讲,“无状态服务”有很多优势,具体介绍如下。

1.数据同步

“有状态服务”为了在不同的服务节点之间共享数据,必然会进行数据同步,而不同节点之间的数据同步又会带来CPU/内存损耗、网络延迟、数据冗余等问题;而“无状态服务”不需要数据同步。

2.快速部署

如果系统的压力太大,就需要增加新的服务节点对系统扩容。如果采用的是“有状态服务”,就需要在扩容后先将其他服务节点上的Session等信息同步到新节点上;而如果采用的是“无状态服务”,就不必同步。由此可见,快速部署的优势本质上也归功于“数据同步”。

实际上,这里介绍的“状态”不仅仅是Session,也可以是任意类型的数据(结构化数据、非结构化数据)、文件等。因此,要想真正地实现应用服务的“无状态”,就需要先将各种类型的“状态”各自集中存储,具体存储方式如下。

(1)Session等保存在内存中的数据:构建独立的服务,如Session服务。

(2)结构化数据 :MySQL等关系型数据库。

(3)非结构化数据。

● 适合存储非结构化数据的数据库,如HBase等;

● 如果是海量的“状态”,也可以存储在ElasticSearch等搜索引擎中。

(4)文件。

● 独立文件服务器,如HDFS、FastDFS等;

● CDN。

以上存储方式的对应关系,如图2—7所示。

图2—7 “状态”的存储方式

明确了各个“状态”的存储方式后,我们就可以搭建出一套可快速扩容的系统。将系统中的应用服务设置为“无状态”,并注册到Eureka或Zookeeper中。因为是“无状态”的服务,因此单个服务的宕机、重启等都不会影响到集群中的其他服务,并且很容易对应用服务进行横向扩展。另一方面,将带有数据的服务设置为“有状态”,并进行集群的“集中部署”(如MySQL集群),从而降低集群内部数据同步带来的延迟。

说明 :“集中部署”是指尽可能地将相同或相关的数据、业务部署在同一机房中,利用内网提高数据的传输速度,尽量避免跨机房调用,如图2—8所示。

图2—8 多机房部署

在实际部署时,可通过IP分组等方法尽量避免跨机房的数据传输、接口调用。并且使用DNS、Nginx等工具在某个机房整体故障时,将流量快速转接到其他机房。

最后要提醒大家的是,“无状态服务”虽然有很多的优势,但也不能盲目地将其作为唯一的选择。任何技术或架构的选择,都得看具体的业务场景,如在小型项目或仅有一个服务的项目中,就可以采用“有状态服务”来降低开发难度,缩短开发周期。

2.2.3 技术选型原则与数据库设计

在做技术选型时,既要注意待选技术的性能,又要考虑技术的安全性,并预估这些技术是否有足够长的生命力,项目组新成员能否快速掌握,而不能一味追求技术的先进性。

这里以设计数据库为例,介绍一种数据库选型的思路。

以MySQL数据库为例,各种版本的MySQL默认的并发连接数为一二百,单机可配置的最大连接数为16384(一般情况下,由于计算机自身硬件的限制,单机实际能够负载的并发数最多为一千左右)。因此,高并发系统面临的最大性能瓶颈就是数据库。我们之前设计各种缓存的目的,就是尽可能地减少对数据库的访问。

除了在页面、应用程序中增加缓存外,还可以在应用程序和数据库之间加一层Redis高速缓存,从而提高数据访问的速度并且减少对数据库的访问次数,具体设计步骤如下。

(1)搭建高可用Redis集群,并通过主从同步进行数据备份、通过读写分离降低并发写操作的冲突、通过哨兵模式在Master挂掉后选举新的Master。

(2)搭建双Master的MySQL集群,并通过主从同步做数据备份。

(3)通过MyCat对大容量的数据进行分库/分表,并控制MySQL的读写分离。

(4)通过Haproxy搭建MyCat集群。

(5)通过Keepalived搭建Haproxy集群,通过心跳检测机制防止单节点故障;并且Keepalived可以生成一个VIP,并用此VIP与Redis建立连接。

以上步骤如图2—9所示。

图2—9 高性能高可用数据库架构

在实际进行数据库开发时,还需要合理使用索引技术及适当设置数据库的各项性能参数,从而最大限度地优化数据访问操作。

2.2.4 缓存穿透与缓存雪崩问题

缓存可以在一定程度上缓解高并发造成的性能问题,但在一些特定场景下缓存自身也会带来一些问题,比较典型的就是缓存穿透与缓存雪崩问题。

注意

为了讲解方便,本小节用MySQL代指所有的关系型数据库,用Redis代指所有数据库的缓存组件。

1.缓存穿透

缓存穿透是指大量查询一些数据库中不存在的数据,从而影响数据库的性能。例如,Redis等KV存储结构的中间件可以作为MySQL等数据库的缓存组件,但如果某些数据没有被Redis缓存却被大量查询,就会给MySQL带来巨大压力,如图2—10所示。

图2—10 缓存穿透

前面介绍过,单机MySQL最大能够承受的并发连接数只有一千左右,因此无论是设计失误(如某个高频访问的缓存对象过期)、恶意攻击(如频繁查询某个不存在的数据),还是偶然事件(如由于社会新闻导致某个热点的搜索量大增)等,都可能让MySQL遭受缓存穿透,从而宕机。

理解了缓存穿透的原因后,解决思路就已经明确了,举例如下。

(1)拦截非法的查询请求,仅将合理的请求发送给MySQL。例如,可以使用验证码、IP限制等手段限制恶意攻击,并用敏感词过滤器等拦截不合理的非法查询。

(2)缓存空对象。例如,假设在iPhone 9上市后,可能会导致大量用户搜索iPhone 9,但此时Redis和MySQL中还没有iPhone 9这个词。一种解决办法就是,将数据库中不存在的iPhone 9也缓存在Redis中,如Key=iPhone 9,value=""。之后,当用户再次搜索iPhone 9时,就可以直接从Redis中拿到结果,从而避免对MySQL的访问,如图2—11所示。

图2—11 缓存空对象

注意

为了减少Redis对大量空对象的缓存,可以适当减少空对象的过期时间。

(3)建立数据标识仓库。将MySQL中的所有数据的name值都映射成Hash值,例如,可以将“商品表”中的商品名“iPhone 8”映射成MD5计算出来的Hash值b2dd48ff3e52d0796675693d08fb192e,然后再将全部name的Hash值放入Redis中,从而构建出一个“数据库中所有可查数据的Hash仓库”。之后,每次在查询MySQL之前都会先查询这个Hash仓库,如果要查询数据的Hash值存在于仓库中,再进入MySQL做真实的查询,如果不存在则直接返回。

需要注意的是,由于不同数据的Hash值在概率上可能相同,因此可能会漏掉对个别数据的拦截,如图2—12中的“B”。

图2—12 不同数据的Hash值相同而造成的问题

2.缓存雪崩

除了缓存穿透外,在使用缓存时还需要考虑缓存雪崩的情况。缓存雪崩是指由于某种原因造成Redis突然失效,造成MySQL瞬间压力骤增,进而严重影响MySQL性能,甚至造成MySQL服务宕机。以下是造成缓存雪崩的两个常见原因。

(1)Redis重启。

(2)Redis中的大量缓存对象都设置了相同的过期时间。

为了避免缓存雪崩的发生,可参考以下解决方案。

(1)搭建Redis集群,保证高可用。

(2)避免大量缓存对象的Key集中失效,尽量让过期时间分配均匀一些。例如,可以将各个缓存的过期时间乘以一个随机数。

(3)通过队列、锁机制等控制并发访问MySQL的线程数。

2.2.5 综合因素

除了前面介绍的一些原则和设计要点外,系统设计者还需要考量项目的各个功能模块是否都符合相关法律法规,项目组员之间是否有足够的沟通,项目的迭代周期是否合适,各种技术中的性能参数如何优化,如何根据项目情况决定测试与实施的方式,如何在不同的技术之间做平滑迁移,客户在使用时是否有方便的反馈渠道,项目的开发成本是否符合预期等问题。 DM2EraNnYpOWAAFlohmTJPS4SwQuhRFGQ2wVuabLn+YIdN9NK1gfhdqL2ZGWDO85

点击中间区域
呼出菜单
上一章
目录
下一章
×