高并发,我把握不住啊!

核心提示慎入,作者高并发搞得少(没搞过),这里面水太深,什么高并发,大流量的东西都是虚拟的,作者还太年轻,没有那个经历,把握不住。系统只有几QPS,开心快乐就行,不PK,文明PK。我承认我有赌的成分,点进去一看,果然是广告。说真的,内容看起来还是很
慎入,作者高并发搞得少(没搞过),这里面水太深,什么高并发,大流量的东西都是虚拟的,作者还太年轻,没有那个经历,把握不住。系统只有几QPS,开心快乐就行,不PK,文明PK。

我承认我有赌的成分,点进去一看,果然是广告。说真的,内容看起来还是很有吸引力的,但是贫穷阻止了我消费的冲动。

作为一个高并发的门外汉,尝试结合学过的课程和一些网上的资料来整理一下对于高并发的认识。——实战是不可能实战的,只能动动嘴皮这样子。

什么是高并发

高并发指的是系统同时处理很多请求。

高并发是一个结果导向的东西,例如,常见的高并发场景有:淘宝的双11、春运时的抢票、微博大V的热点新闻等,这些典型场景并不是陡然出世,而是随着业务发展的发展而逐渐出现。像2020年淘宝双11全球狂欢季,订单创建峰值达到了惊人的58.3万笔/秒,4年前的2016年,这个数字大概是四分之一,再往前四年,这个数据不可考,但是肯定就没这么夸张了。

高并发的业务场景出现了,随之而来的就是要支持这个高并发业务场景的架构——技术要为业务服务,业务倒逼技术发展。高并发的架构也不是某个天才冥思苦想或者灵机一动,这个过程是随着业务的发展而演进。用一个比喻,先有了秋名山,才到了老司机。

这3个目标是需要通盘考虑的,因为它们互相关联、甚至也会相互影响。

比如说:考虑系统的扩展能力,你需要将服务设计成无状态的,这种集群设计保证了高扩展性,其实也间接提升了系统的性能和可用性。

再比如说:为了保证可用性,通常会对服务接口进行超时设置,以防大量线程阻塞在慢请求上造成系统雪崩,那超时时间设置成多少合理呢?一般,我们会参考依赖服务的性能表现进行设置。

具体目标性能指标

性能指标通过性能指标可以度量目前存在的性能问题,也是高并发主要关注的指标,性能和流量方面常用的一些指标有

  1. QPS/TPS/HPS:QPS是每秒查询数,TPS是每秒事务数,HPS是每秒HTTP请求数。最常用的指标是QPS。

需要注意的是,并发数和QPS是不同的概念,并发数是指系统同时能处理的请求数量,反应了系统的负载能力。

并发数 = QPS✖平均响应时间

  1. 响应时间:从请求发出到收到响应花费的时间,例如一个系统处理一个HTTP请求需要100ms,这个100ms就是系统的响应时间。
  2. 平均响应时间:最常用,但是缺陷很明显,对于慢请求不敏感。比如 1 万次请求,其中 9900 次是 1ms,100 次是 100ms,则平均响应时间为 1.99ms,虽然平均耗时仅增加了 0.99ms,但是 1%请求的响应时间已经增加了 100 倍。
  3. TP90、TP99 等分位值:将响应时间按照从小到大排序,TP90 表示排在第 90 分位的响应时间, 分位值越大,对慢请求越敏感。

对于大多数系统。2个9是基本可用(如果达不到开发和运维可能就要被祭天了),3个9是较高可用,4个9是具有自动恢复能力的高可用。要想达到3个9和4个9很困难,可用性影响因素非常多,很难控制,需要过硬的技术、大量的设备资金投入,工程师要具备责任心,甚至还要点运气。

可扩展性指标

面对突发流量,不可能临时改造架构,最快的方式就是增加机器来线性提高系统的处理能力。

对于业务集群或者基础组件来说,扩展性 = 性能提升比例 / 机器增加比例,理想的扩展能力是:资源增加几倍,性能提升几倍。通常来说,扩展能力要维持在 70%以上。

但是从高并发系统的整体架构角度来看,扩展的目标不仅仅是把服务设计成无状态就行了,因为当流量增加 10 倍,业务服务可以快速扩容 10 倍,但是数据库可能就成为了新的瓶颈。

像 MySQL 这种有状态的存储服务通常是扩展的技术难点,如果架构上没提前做好规划(垂直和水平拆分),就会涉及到大量数据的迁移。

我们需要站在整体架构的角度,而不仅仅是业务服务器的角度来考虑系统的扩展性 。所以说,数据库、缓存、依赖的第三方、负载均衡、交换机带宽等等都是系统扩展时需要考虑的因素。我们要知 道系统并发到了某一个量级之后,哪一个因素会成为我们的瓶颈点,从而针对性地进行扩展。

高并发架构演进

谁不是生下来就是老司机,架构也不是架起来就支持高并发。我们来看一个经典的架构演进的例子——淘宝,真实诠释了“好的架构是进化来的,不是设计来的”。

以下是来自《淘宝技术这十年》描述的淘宝2003—2012年的架构演进。

个人网站

初代淘宝的团队人员只有十来个,而且面临千载难逢的商业机会,所以要求上线的时间越快越好(实际用了不到一个月),那么淘宝的这些牛人是怎么做到的呢?

——买一个。

初代淘宝买了这样一个架构的网站: LAMP(Linux+Apache+MySQL+PHP)。整个系统的架构如下:

由于商品搜索比较占用数据库资源,后来还引入了阿里巴巴的搜索引擎iSearch。

Oracle/支付宝/旺旺

淘宝飞速发展,流量和交易量迅速提升,给技术带来了新的问题——MySQL抗不住了。怎么办?要搞点事情吗?没有,淘宝买了Oracle数据库,当然这个也考虑到团队里有Oracle大牛的原因。

替换了数据库之后的架构:

Java 时代 2.0

在之前,淘宝的架构的架构主要思路还是“买”,随着业务的发展,到了2005 年,“买”已经很难解决问题了,需要对整个架构进行调整和优化,需要综合考虑容量、性能、成本的问题。

在Java时代2.0,主要做了对数据分库、放弃EJB、引入Spring、加入缓存、加入CDN等。

分布式时代1.0

到了2008年的时候,淘宝的业务进一步发展。

整个主站系统的容量已经到了瓶颈,商品数在1亿个以上,PV在2.5亿个以上,会员数超过了 5000万个。这时Oracle的连接池数量都不够用了,数据库的容量到了极限,即使上层系统加机器也无法继续扩容,我们只有把底层的基础服务继续拆分,从底层开始扩容,上层才能扩展,这才能容纳以后三五年的增长。

淘宝开始对业务模块逐步拆分和服务化改造。例如拆分出了商品中心、商品中心等等。同时引入了一些自研的中间件,如分布式数据库中间件,分布式消息中间件等等。

云平台时代

在服务化的时候,淘宝已经演进到了云平台架构。

所谓的云平台,就是把海量机器资源,通过统一的资源管理,抽象为一个资源整体,在之上可按需动态申请硬件资源(如CPU、内存、网络等),并且之上提供通用的操作系统,提供常用的技术组件(如Hadoop技术栈,MPP数据库等)供用户使用,甚至提供开发好的应用,用户不需要关系应用内部使用了什么技术,就能够解决需求(如音视频转码服务、邮件服务、个人博客等)。

我们看一下一个大概的支持三高的典型架构:

DNS是请求分发的第一个关口,实现的是地理级别的均衡。dns-server对一个域名配置了多个解析ip,每次DNS解析请求来访问dns-server。通常会返回离用户距离比较近的ip,用户再去访问ip。例如,北京的用户访问北京的机房,南京的用户访问南京的资源。

一般不会使用DNS来做机器级别的负载均衡,因为造不起,IP资源实在太宝贵了,例如百度搜索可能需要数万台机器,不可能给每个机器都配置公网IP。一般只会有有限的公网IP的节点,然后再在这些节点上做机器级别的负载均衡,这样各个机房的机器只需要配置局域网IP就行了。

DNS负载均衡的优点是通用(全球通用)、成本低(申请域名,注册DNS即可)。

缺点也比较明显,主要体现在:

  • DNS 缓存的时间比较长,即使将某台业务机器从 DNS 服务器上删除,由于缓存的原因,还是有很多用户会继续访问已经被删除的机器。
  • DNS 不够灵活。DNS 不能感知后端服务器的状态,只能根据配置策略进行负载均衡,无法做到更加灵活的负载均衡策略。比如说某台机器的配置比其他机器要好很多,理论上来说应该多分配一些请求给它,但 DNS 无法做到这一点。

所以对于时延和故障敏感的业务,有实力的公司可能会尝试实现HTTP-DNS的功能,即使用HTTP 协议实现一个私有的 DNS 系统。HTTP-DNS 主要应用在通过 App 提供服务的业务上,因为在 App 端可以实现灵活的服务器访问策略,如果是 Web 业务,实现起来就比较麻烦一些,因为 URL 的解析是由浏览器来完成的,只有 Javascript 的访问可以像 App 那样实现比较灵活的控制。

CDN

CDN是为了解决用户网络访问时的“最后一公里”效应,本质是一种“以空间换时间”的加速策略,即将内容缓存在离用户最近的地方,用户访问的是缓存的内容,而不是站点实时访问的内容。

由于CDN部署在网络运营商的机房,这些运营商又是终端用户的网络提供商,因此用户请求路由的第一跳就到达了CDN服务器,当CDN中存在浏览器请求的资源时,从CDN直接返回给浏览器,最短路径返回响应,加快用户访问速度。

下面是简单的CDN请求流程示意图:

负载均衡典型架构

像上面提到的负载均衡机制,在使用中,可以组合使用。

DNS负载均衡用于实现地理级别的负载均衡,硬件件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡。

对于负载均衡我们主要关心的几个方面如下:

  • 上游服务器配置:使用 upstream server配置上游服务器
  • 负载均衡算法:配置多个上游服务器时的负载均衡机制。
  • 失败重试机制:配置当超时或上游服务器不存活时,是否需要重试其他上游服务器。
  • 服务器心跳检查:上游服务器的健康检查/心跳检查。
upstream server中文直接翻译是上游服务器,意思就是负载均衡服务器设置,就是被nginx代理最后真实访问的服务器。
负载均衡算法

负载均衡算法数量较多,Nginx主要支持以下几种负载均衡算法:

1、轮询(默认)

每个请求按时间顺序逐一分配到不同的后端服务,如果后端某台服务器死机,自动剔除故障系统,使用户访问不受影响。

2、weight(轮询权值)

weight的值越大分配到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。或者仅仅为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。

3、ip_hash

每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,并且可以有效解决动态网页存在的session共享问题。

4、fair

比 weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能地进行负载均衡,也就是根据后端服务器的响应时间 来分配请求,响应时间短的优先分配。Nginx本身不支持fair,如果需要这种调度算法,则必须安装upstream_fair模块。

5、url_hash

按访问的URL的哈希结果来分配请求,使每个URL定向到一台后端服务器,可以进一步提高后端缓存服务器的效率。Nginx本身不支持url_hash,如果需要这种调度算法,则必须安装Nginx的hash软件包。

失败重试

Nginx关于失败重试主要有两部分配置,upstream server 和 proxy_pass。

通过配置上游服务器的 max_fails和 fail_timeout,来指定每个上游服务器,当fail_timeout时间内失败了max_fail次请求,则认为该上游服务器不可用/不存活,然后将会摘掉该上游服务器,fail_timeout时间后会再次将该服务器加入到存活上游服务器列表进行重试。

健康检查

Nginx 对上游服务器的健康检查默认采用的是惰性策略,Nginx 商业版提供了healthcheck 进 行 主 动 健 康 检 查 。当 然 也 可 以 集 成 nginx_upstream_check_module 模块来进行主动健康检查。

nginx_upstream_check_module 支持 TCP 心跳和 HTTP 心跳来实现健康检查。

流量控制流量分发

流量分发就不多说了,上面已经讲了,是接入层的基本功能。

流量切换

我听朋友说过一个有意思的事情,他们公司将流量从一个机房切到另一个机房,结果翻车,所有工程师运维平台一片飘红,全公司集体围观,运维团队就很丢面子。

可以在很多层面做限流,例如服务层网关限流、消息队列限流、Redis限流,这些主要是业务上的限流。

这里我们主要讨论的是接入层的限流,直接在流量入口上限流。

对于 Nginx接入层限流可以使用 Nginx自带的两个模块:连接数限流模块 ngx_http_limit_conn_module和漏桶算法实现的请求限流模块 ngx_http_limit_req_moduleo

还可以使用 OpenResty提供的 Lua限流模块 ua-resty-limit-traffic应对更复杂的限流场景。

limmit_conn用来对某个 key 对应的总的网络连接数进行限流,可以按照如 IP、域名维度进行限流。limit_req用来对某个 key对应的请求的平均速率进行限流,有两种用法:平滑模式(delay ) 和允许突发模式(nodelay )。

流量过滤

很多时候,一个网站有很多流量是爬虫流量,或者直接是恶意的流量。

可以在接入层,对请求的参数进行校验,如果参数校验不合法,则直接拒绝请求,或者把请求打到专门用来处理非法请求的服务。

最简单的是使用Nginx,实际场景可能会使用OpenResty,对爬虫 user-agent 过滤和一些恶意IP

看到这个翻译,相信你会立刻联想到 DNS, 即 Domain Name System。没错,两者的性质是基本类似的。

DNS 的作用将域名解析为 IP 地址,主要原因是我们记不住太多的数字 IP, 域名就容易记住。服务名字系统是为了将 Service 名称解析为 "host + port + 接口名称" ,但是和 DNS一样,真正发起请求的还是请求方。

  • Feign封装RestTemplate实现http请求方式的远程调用
  • Feign封装Ribbon实现客户端负载均衡
  • Euraka集群部署实现注册中心高可用
  • 注册中心心跳监测,更新服务可用状态
  • 集成Hystrix实现熔断机制
  • Zuul作为API 网关 ,提供路由转发、请求过滤等功能
  • Config实现分布式配置管理
  • Sluth实现调用链路跟踪
  • 集成ELK,通过Kafka队列将日志异步写入Elasticsearch,通过Kibana可视化查看

SpringCloud是一整套完整微服务解决方案,被称为“SpringCloud 全家桶”。这里只是简单地介绍一下。

Dubbo主要提供了最基础的RPC功能。

不过SpringCloud的RPC采用了HTTP协议,可能性能会差一些。

利好的是,“SpringCloud2.0”——SpringCloud Alibaba流行了起来,Dubbo也可以完美地融入SpringCloud的生态。

消息队列

消息队列在高性能、高扩展、高可用的架构中扮演着很重要的角色。

消息队列是用来解耦一些不需要同步调用的服务或者订阅一些自己系统关心的变化。使用消息队列可以实现服务解耦(一对多消费)、异步处理、流量削峰/缓冲等。

服务解耦

服务解耦可以降低服务间耦合,提高系统系统的扩展性。

例如一个订单服务,有多个下游,如果不用消息队列,那么订单服务就要调用多个下游。如果需求要再加下游,那么订单服务就得添加调用新下流的功能,这就比较烦。

引入消息队列之后,订单服务就可以直接把订单相关消息塞到消息队列中,下游系统只管订阅就行了。

流量削峰/缓冲

流量削峰/缓冲可以提高系统的可用性。

我们前面提到了接入层的限流,在服务层的限流可以通过消息队列来实现。网关的请求先放入消息队列中,后端服务尽可能去消息队列中消费请求。超时的请求可以直接返回错误,也可以在消息队列中等待。

  • 部署:主要负责将系统发布到线上。例如,包管理、灰度发布管理、回滚等。
  • 监控:主要负责收集系统上线运行后的相关数据并进行监控,以便及时发现问题。
  • 应急:主要负责系统出故障后的处理。例如,停止程序、下线故障机器、切换 IP 等。

运维平台的核心设计要素是“四化"——标准化、平台化、自动化、可视化。

  • 标准化:要制定运维标准,规范配置管理、部署流程、监控指标、应急能力等,各系统按照运维标准来
    实现,避免不同的系统不同的处理方式。
  • 平台化:传统的手工运维方式需要投入大量人力,效率低,容易出错,因此需要在运维标准化的基础上,
    将运维的相关操作都集成到运维平台中,通过运维平台来完成运维工作。
  • 自动化:传统手工运维方式效率低下的一个主要原因就是要执行大量重复的操作,运维平台可以将这些重
    复操作固化下来,由系统自动完成。
  • 可视化:运维平台有非常多的数据,如果全部通过人工去查询数据再来判断,则效率很低,可视化的主要目的就是为了提升数据查看效率。
测试平台

测试平台核心的职责当然就是测试了,包括单元测试、集成测试、接口测试、性能测试等,都可以在测试平台来完成。

测试平台的核心目的是提升测试效率,从而提升产品质量,其设计关键就是自动化。

  1. 数据管理

数据管理包含数据采集、数据存储、数据访问和数据安全四个核心职责,是数据平台的基础功能。

  • 数据采集:从业务系统搜集各类数据。例如,日志、用户行为、业务数据等,将这些数据传送到数据平台。
  • 数据存储:将从业务系统采集的数据存储到数据平台,用于后续数据分析。
  • 数据访问:负责对外提供各种协议用于读写数据。例如,SQL、 Hive、 Key-Value 等读写协议。
  • 数据安全:通常情况下数据平台都是多个业务共享的,部分业务敏感数据需要加以保护,防止被其他业务读取甚至修改,因此需要设计数据安全策略来保护数据。
  1. 数据分析

数据分析包括数据统计、数据挖掘、机器学习、深度学习等几个细分领域。

  • 数据挖掘:数据挖掘这个概念本身含义可以很广,为了与机器学习和深度学习区分开,这里的数据挖掘主要是指传统的数据挖掘方式。例如,有经验的数据分析人员基于数据仓库构建一系列规则来对数据进行分析从而发现一些隐含的规律、现象、问题等,经典的数据挖掘案例就是沃尔玛的啤酒与尿布的关联关系的发现。
  • 机器学习、深度学习:机器学习和深度学习属于数据挖掘的一种具体实现方式,由于其实现方式与传统的数据挖掘方式差异较大,因此数据平台在实现机器学习和深度学习时,需要针对机器学习和深度学习独立进行设计。
  1. 数据应用

数据应用很广泛,既包括在线业务,也包括离线业务。例如,推荐、广告等属于在线应用,报表、欺诈检测、异常检测等属于离线应用。数据应用能够发挥价值的前提是需要有 "大数据" ,只有当数据的规模达到一定程度,基于数据的分析、挖掘才能发现有价值的规律、现象、问题等。如果数据没有达到一定规模,通常情况下做好数据统计就足够了,尤其是很多初创企业,无须一开始就参考 BAT 来构建自己的数据平台。

管理平台

管理平台的核心职责就是权限管理,无论是业务系统(例如,淘宝网) 、中间件系统(例如,消息队列 Kafka) , 还是平台系统(例如,运维平台) ,都需要进行管理。如果每个系统都自己来实现权限管理,效率太低,重复工作很多,因此需要统一的管理平台来管理所有的系统的权限。

说到“平台”,不由地想起这几年一会儿被人猛吹,一会儿被人唱衰的“中台”。在平台里的数据平台,其实已经和所谓的“数据中台”类似了。“中台”是个概念性的东西,具体怎么实现,没有统一的标准方案。作者所在的公司,也跟风建了中台,以“数据中台”为例,我们数据中台的建设主要为了数据共享和数据可视化,简单说就是把各个业务模块的一些数据汇聚起来。说起来简单,落地很难,数据汇聚的及时性、数据共享的快速响应……最终的解决方案是采购了阿里的一些商业化组件,花了老鼻子钱,但是效果,不能说一地鸡毛,也差不多吧。
缓存层

虽然我们可以通过各种手段来提升存储系统的性能,但在某些复杂的业务场景下,单纯依靠存储系统的性能提升不够的。

绝大部分在线业务都是读多写少。例如,微博、淘宝、微信这类互联网业务,读业务占了整体业务量的 90%以上。以微博为例:一个明星发一条微博,可能几千万人来浏览。

如果直接从DB中取数据,有两个问题,一个是DB查询的速度有瓶颈,会增加系统的响应时间,一个是数据库本身的并发瓶颈。缓存就是为了弥补读多写少场景下存储系统的不足。

在前面我们提到的CDN可以说是缓存的一种,它缓存的是静态资源。

从整个架构来看,一般采用多级缓存的架构,在不同层级对数据进行缓存,来提升访问效率。

而对于可以丢失的缓存数据,可以考虑一致性哈希,即使其中一个实例出问题只是丢一小部分。

热点本地缓存

对于那些访问非常频繁的热点缓存,如果每次都去远程缓存系统中获取,可能会因为访问量太大导致远程缓存系统请求过多、负载过高或者带宽过高等问题,最终可能导致缓存响应慢,使客户端请求超时。

一种解决方案是通过挂更多的从缓存,客户端通过负载均衡机制读取从缓存系统数据。不过也可以在客户端所在的应用/代理层本地存储一份,从而避免访问远程缓存,即使像库存这种数据,在有些应用系统中也可以进行几秒钟的本地缓存,从而降低远程系统的压力。

缓存的引入虽然提高了系统的性能,但同时也增加了系统的复杂度,带来了一些运维的成本。
缓存穿透

缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据,结果存储系统也没有数据。

缓存穿透的示意图:

关键在于某个热点的key失效了,导致大并发集中打在数据库上。所以要从两个方面解决,第一是否可以考虑热点key不设置过期时间,第二是否可以考虑降低打在数据库上的请求数量。

主要有两个解决办法:

  • 利用互斥锁保证同一时刻只有一个客户端可以查询底层数据库的这个数据,一旦查到数据就缓存至Redis内,避免其他大量请求同时穿过Redis访问底层数据库。这种方式会阻塞其他的线程,此时系统的吞吐量会下降
  • 热点数据缓存永远不过期。

永不过期有两种方式:

  • 物理不过期,针对热点key不设置过期时间
  • 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建
缓存雪崩

缓存雪崩,指的是是缓存不可用,或者同一时刻是大量热点key失效。

两种情况导致的同样的后果就是大量的请求直接落在数据库上,对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求,最严重的后果就是直接导致数据库宕机,可能会引起连锁反应,导致系统崩溃。

读写分离的基本实现是:

  • 数据库服务器搭建主从集群,一主一从、一主多从都可以。
  • 数据库主机负责读写操作,从机只负责读操作。
  • 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
  • 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。

解决主从复制延迟的常见方法:

  1. 数据的冗余

我们可以在发送消息队列时不仅仅发送微博 ID,而是发送队列处理机需要的所有微博信息,借此避免从数据库中重新查询数据。

  1. 使用缓存

我们可以在同步写数据库的同时,也把微博的数据写入到缓存里面,队列处理机在获取微博信息的时候会优先查询缓存,这样也可以保证数据的一致性。

  1. 二次读取

我们可以对底层数据库访问的API进行封装,一次读取从库发现不实时之后再读取一次,例如我们通过微博ID没有在从库里读到微博,那么第二次就直接去主库读取。

  1. 查询主库

我们可以把关键业务,或者对实时性有要求的业务读写操作全部指向主机,非关键业务或者实时性要求不高的业务采用读写分离。

分配机制

将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。

  1. 程序代码封装

程序代码封装指在代码中抽象一个数据访问层(所以有的文章也称这种方式为 "中间层封装" ) ,实现读写操作分离和数据库服务器连接的管理。例如,基于 Hibernate 进行简单封装,就可以实现读写分离,基本架构是:

  1. 中间件封装

中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供 SQL 兼容的协议,业务服务器无须自己进行读写分离。对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。

其基本架构是:

虽然业务分库能够分散存储和访问压力,但同时也带来了新的问题,接下来我们详细分析一下。

  1. join 操作问题

业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用 SQL 的 join 查 询。

例如: "查询购买了化妆品的用户中女性用户的列表〃 这个功能,虽然订单数据中有用户的 ID信息,但是用户的性别数据在用户数据库中,如果在同一个库中,简单的 join 查询就能完成;但现在数据分散在两个不同的数据库中,无法做 join 查询,只能采取先从订单数据库中查询购买了化妆品的用户 ID 列表,然后再到用户数据库中查询这批用户 ID 中的女性用户列表,这样实现就比简单的 join 查询要复杂一些。

  1. 事务问题

原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改。虽然数据库厂商提供了一些分布式事务的解决方案(例如,MySQL 的 XA) , 但性能实在太低,与高性能存储的目标是相违背的。

例如,用户下订单的时候需要扣商品库存,如果订单数据和商品数据在同一个数据库中,我们可订单,如果因为订单数据库异常导致生成订单失败,业务程序又需要将商品库存加上;而如果因为业务程序自己异常导致生成订单失败,则商品库存就无法恢复了,需要人工通过曰志等方式来手工修复库存异常。

  1. 成本问题

业务分库同时也带来了成本的代价,本来 1 台服务器搞定的事情,现在要 3 台,如果考虑备份,那就是 2 台变成了 6 台。

基于上述原因,对于小公司初创业务,并不建议一开始就这样拆分,主要有几个原因:初创业务存在很大的不确定性,业务不一定能发展起来,业务开始的时候并没有真正的存储和访问压力,业务分库并不能为业务带来价值。业务分库后,表之间的 join 查询、数据库事务无法简单实现了。

业务分库后,因为不同的数据要读写不同的数据库,代码中需要增加根据数据类型映射到不同数据库的逻辑,增加了工作量。而业务初创期间最重要的是快速实现、快速验证,业务分库会拖慢业务节奏。

单表拆分

将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。

单表数据拆分有两种方式:垂直分表和水平分表。示意图如下:

  1. 垂直分表

垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如,前面示意图中的nickname 和 desc 字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外—张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。垂直分表引入的复杂性主要体现在表操作的数量要增加。例如,原来只要一次查询就可以获取name、age、sex、nickname、description, 现在需要两次查询,—次查询获取 name、age、 sex, 另一次查询获取 nickname、desc。

不过相比接下来要讲的水平分表,这个复杂性就是小巫见大巫了。

  1. 水平分表

水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000 万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。但不管怎样,当看到表的数据量达到干万级别时,这很可能是架构的性能瓶颈或者隐患。

水平分表相比垂直分表,会引入更多的复杂性,主要表现在下面几个方面:

  • 路由

水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性。

常见的路由算法有:

在图中用到了ES搜索集群来处理搜索业务,同样也可以我们前面提到的跨库join的问题。

在设计异构的时候,我们可以充分利用一些流行的NoSQL数据库。NoSQL尽管已经被证明不能取代关系型数据库,但是在很多场景下是关系型数据库的有力补充。

举几个例子,像我们熟悉的Redis这样的KV存储,有极高的读写性能,在读写性能有要求的场景可以使用;

Hbase、Cassandra 这样的列式存储数据库。这种数据库的特点是数据不像传统数据库以行为单位来存储,而是以列来存储,适用于一些离线数据统计的场景;

MongoDB、CouchDB 这样的文档型数据库,具备 Schema Free(模式自由)的特点,数据表中的字段可以任意扩展,可以用于数据字段不固定的场景。

查询维度异构

比如对于订单库,当对其分库分表后,如果想按照商家维度或者按照用户维度进行查询,那么是非常困难的,因此可以通过异构数据库来解决这个问题。可以采用下图的架构。

异构数据主要存储数据之间的关系,然后通过查询源库查询实际数据。不过,有时可以通过数据冗余存储来减少源库查询量或者提升查询性能。

聚合据异构

商品详情页中一般包括商品基本信息、商品属性、商品图片,在前端展示商品详情页时,是按照商品 ID 维度进行查询,并且需要查询 3 个甚至更多的库才能查到所有展示数据。此时,如果其中一个库不稳定,就会导致商品详情页出现问题,因此,我们把数据聚合后异构存储到 KV 存储集群(如存储 JSON ), 这样只需要一次查询就能得到所有的展示数据。这种方式也需要系统有了一定的数据量和访问量时再考虑。

高可用要点

除了从技术的角度来考虑,保证高可用同样需要良好的组织制度,来保证服务出现问题的快速恢复。

高扩展要点

1、合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。

2、存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。

3、业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心请求和非核心请求拆分,还可以按照请求源拆(比如To C和To B,APP和H5 )。

好了,攒的这一篇终于完事了,更深入学习建议阅读书籍参考。祝各位架构师能真的如江似海,把握高并发,多挣达不溜。

作者:三分恶链接:https://www.cnblogs.com/three-fighter/p/14757813.html#navigator来源:博客园

 
友情链接
鄂ICP备19019357号-22