(转发)后台架构
网络层
多机器
堆机器不是万能的,不堆机器是万万不能的。
我们努力地升级改造架构,最后让我们提供的服务能够快速横向扩展。横向扩展的基础同样是要有一定数量的、一定性能的机器。
还是上面哪个比喻,你要打十个大汉,等你努力练成了叶师傅,你突然发现对面的孩子都长大了,人数×2,这时候你还是得叫兄弟。
一般狗大户大厂在全国各地都有机房,可能光北京就有两个,把不同地方的请求分到不同的机房,再分到不同的集群,再分到不同的机器,这么一匀,就在服务能扛的范畴之内了。我们大概来看一下,怎么估算所需机器的数量。
- 通过QPS和PV计算部署服务器的台数
公式1:每天总PV = QPS * 3600 * 6 公式2:每天总PV = QPS * 3600 * 8
服务器计算:
服务器数量 = ceil( 每天总PV / 单台服务器每天总PV )
- 峰值QPS和机器计算公式
公式:( 总PV数 * 80% ) / ( 每天秒数 * 20% ) = 峰值时间每秒请求数(QPS)
机器:峰值时间每秒QPS / 单台机器的QPS = 需要的机器。
一般有大流量业务的公司都实现了多机房,包括同城多机房、跨城多机房、跨国多机房等。为了保证可用性,财大气粗的公司会预备大量的冗余,一般会保证机器数是计算峰值所需机器数的两倍。需要节约成本的,也可以考虑当前流行的云平台,之前热点事件的时候,微博就从阿里云租了不少云服务器。
DNS
文章图片
image DNS是请求分发的第一个关口,实现的是地理级别的均衡。dns-server对一个域名配置了多个解析ip,每次DNS解析请求来访问dns-server。通常会返回离用户距离比较近的ip,用户再去访问ip。例如,北京的用户访问北京的机房,南京的用户访问南京的资源。
一般不会使用DNS来做机器级别的负载均衡,因为造不起,IP资源实在太宝贵了,例如百度搜索可能需要数万台机器,不可能给每个机器都配置公网IP。一般只会有有限的公网IP的节点,然后再在这些节点上做机器级别的负载均衡,这样各个机房的机器只需要配置局域网IP就行了。
DNS负载均衡的优点是通用(全球通用)、成本低(申请域名,注册DNS即可)。
缺点也比较明显,主要体现在:
- DNS 缓存的时间比较长,即使将某台业务机器从 DNS 服务器上删除,由于缓存的原因,还是有很多用户会继续访问已经被删除的机器。
- DNS 不够灵活。DNS 不能感知后端服务器的状态,只能根据配置策略进行负载均衡,无法做到更加灵活的负载均衡策略。比如说某台机器的配置比其他机器要好很多,理论上来说应该多分配一些请求给它,但 DNS 无法做到这一点。
CDN CDN是为了解决用户网络访问时的“最后一公里”效应,本质是一种“以空间换时间”的加速策略,即将内容缓存在离用户最近的地方,用户访问的是缓存的内容,而不是站点实时访问的内容。
由于CDN部署在网络运营商的机房,这些运营商又是终端用户的网络提供商,因此用户请求路由的第一跳就到达了CDN服务器,当CDN中存在浏览器请求的资源时,从CDN直接返回给浏览器,最短路径返回响应,加快用户访问速度。
下面是简单的CDN请求流程示意图:
文章图片
image CDN能够缓存的一般是静态资源,如图片、文件、CSS、Script脚本、静态网页等,但是这些文件访问频度很高,将其缓存在CDN可极大改善网页的打开速度。
反向代理层 我们把这一层叫反向代理层,也可以叫接入层、或者负载层。这一层是流量的入口,是系统抗并发很关键的一层。
还是那个比喻,还是你打十个大汉,这次你叫了十九个兄弟,理想的情况是你们两个打对面一个,但是你由于太激动,冲在了最前面,结果瞬间被十个大汉暴打……
反向代理会对流量进行分发,保证最终落到每个服务上的流量是服务能扛的范围之内。
Nginx、LVS、F5 DNS 用于实现地理级别的负载均衡,而 Nginx、 LVS、 F5 用于同一地点内机器级别的负载均衡。其中 Nginx 是软件的 7 层负载均衡,LVS 是内核的 4 层负载均衡,F5 是硬件的 4 层负载均衡。
软件和硬件的区别就在于性能,硬件远远高于软件,Ngxin 的性能是万级,一般的 Linux 服务器上装个 Nginx 大概能到 5 万 / 秒;LVS 的性能是十万级,据说可达到 80万 / 秒;F5 性能是百万级,从 200 万 / 秒到 800 万 / 秒都有。
硬件虽然性能高,但是单台硬件的成本也很高,一台最便宜的 F5 都是几十万,但是如果按照同等请求量级来计算成本的话,实际上硬件负载均衡设备可能会更便宜,例如假设每秒处理 100 万请求,用一台 F5 就够了,但用 Nginx, 可能要 20 台,这样折算下来用 F5 的成本反而低。因此通常情况下,如果性能要求不高,可以用软件负载均衡;如果性能要求很髙,推荐用硬件负载均衡。
4 层和 7 层的区别就在于协议和灵活性。Nginx 支持 HTTP、 E-mail 协议,而 LVS 和 F5 是 4层负载均衡,和协议无关,几乎所有应用都可以做,例如聊天、数据库等。目前很多云服务商都已经提供了负载均衡的产品,例如阿里云的 SLB、UCIoud 的 ULB 等,中小公司直接购买即可。
对于开发而言,一般只需要关注到Nginx这一层面就行了。
文章图片
image 负载均衡典型架构 像上面提到的负载均衡机制,在使用中,可以组合使用。
DNS负载均衡用于实现地理级别的负载均衡,硬件件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡。
文章图片
image 整个系统的负载均衡分为三层。
- 地理级别负载均衡:www.xxx.com 部署在北京、广州、上海三个机房,当用户访问时,DNS 会根据用户的地理位置来决定返回哪个机房的 IP,图中返回了广州机房的 IP 地址,这样用户就访问到广州机房了。
- 集群级别负载均衡:广州机房的负载均衡用的是 F5 设备,F5 收到用户请求后,进行集群级别的负载均衡,将用户请求发给 3 个本地集群中的一个,我们假设 F5 将用户请求发给了 “广州集群 2” 。
- 机器级别的负载均衡:广州集群 2 的负载均衡用的是 Nginx, Nginx 收到用户请求后,将用户请求发送给集群里面的某台服务器,服务器处理用户的业务请求并返回业务响应。
文章图片
image 对于负载均衡我们主要关心的几个方面如下:
- 上游服务器配置:使用 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( github.com/yaoweibin/n… module ) 模块来进行主动健康检查。
nginx_upstream_check_module 支持 TCP 心跳和 HTTP 心跳来实现健康检查。
流量控制 流量分发 流量分发就不多说了,上面已经讲了,是接入层的基本功能。
流量切换 我听朋友说过一个有意思的事情,他们公司将流量从一个机房切到另一个机房,结果翻车,所有工程师运维平台一片飘红,全公司集体围观,运维团队就很丢面子。
文章图片
image 流量切换就是在某些情况下,比如机房故障、光纤被挖断、服务器故障故障情况,或者灰度发布、A/B等运维测试场景,需要将流量切到不同的机房、服务器等等。
就像我们上面提到的负载均衡典型架构,不同层级的负载负责切换不同层级的流量。
- DNS:切换机房入口。
- HttpDNS:主要 APP 场景下,在客户端分配好流量入口,绕过运营商 LocalDNS并实现更精准流量调度。
- LVS/HaProxy:切换故障的 Nginx 接入层。
- Nginx:切换故障的应用层。
限流 限流是保证系统可用的一个重要手段,防止超负荷的流量直接打在服务上,限流算法主要有令牌桶、漏桶。
文章图片
image 可以在很多层面做限流,例如服务层网关限流、消息队列限流、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 (通过统计 IP 访问量来配置阈值),将它们分流到固定分组,这种情况会存在一定程度的误杀,因为公司的公网 IP —般情况下是同一个,大家使用同一个公网出口 IP 访问网站,因此,可以考虑 IP+Cookie 的方式,在用户浏览器种植标识用户身份的唯一 Cookie。访问服务前先种植 Cookie, 访问服务时验证该 Cookie, 如果没有或者不正确,则可以考虑分流到固定分组,或者提示输入验证码后访问。
文章图片
image 降级 降级也是保证高可用的一把利剑,降级的思路是“弃车保帅”,在眼看着不能保证全局可用的情况下,抛弃或者限制一些不重要的服务。
降级一般分为多个层级,例如在应用层进行降级,通过配置中心设置降级的阈值,一旦达到阈值,根据不同的降级策略进行降级。
也可以把降级开关前置到接入层,在接入层配置功能降级开发,然后根据情况行自动/人工降级。后端应用服务出问题时,通过接入层降级,可以避免无谓的流量再打到后端服务,从而给应用服务有足够的时间恢复服务。
Web层 经过一系列的负载均衡,用户终于请求到了web层的服务。web服务开发完成,经过部署,运行在web服务器中给用户提供服务。
集群 一般会根据业务模块,来划分不同的服务,一个服务部署多个实例组成集群。
文章图片
image 为了隔离故障,可以再将集群进行分组,这样一个分组出现问题,也不会影响其它分组。像比较常问的秒杀,通常会将秒杀的服务集群和普通的服务集群进行隔离。
能做到集群化部署的三个要点是无状态、拆分、服务化。
- 无状态:设计的应用是无状态的,那么应用比较容易进行水平扩展。
- 拆分:设计初期可以不用拆分,但是后期访问量大的时候,就可以考虑按功能拆分系统。拆分的维度也比较灵活,根据实际情况来选择,例如根据系统维度、功能维度、读写维度、AOP 维度、模块维度等等。
- 服务化:拆分更多的是设计,服务化是落地,服务化一般都得服务治理的问题。除了最基本的远程调用,还得考虑负载均衡、服务发现、服务隔离、服务限流、服务访问黑白名单等。甚至还有细节需要考虑,如超时时间、重试机制、服务路由、故障补偿等。
服务器的选择主要和开发语言相关,例如,Java 的有 Tomcat、JBoss、Resin 等,PHP/Python 的用 Nginx。
Web服务器的性能之类的一般不会成为瓶颈,例如Java最流行的Web服务器Tomcat默认配置的最大请求数是 150,但是没有关系,集群部署就行了。
容器 容器是最近几年才开始火起来的,其中以 Docker 为代表,在 BAT 级别的公司已经有较多的应用。
容器化可以说给运维带来了革命性的变化。Docker 启动快,几乎不占资源,随时启动和停止,基于Docker 打造自动化运维、智能化运维逐渐成为主流方式。
容器化技术也天生适合当前流行的微服务,容器将微服务进程和应用程序隔离到更小的实例里,使用更少的资源,更快捷地部署。结合容器编排技术,可以更方便快速地搭建服务高可用集群。
文章图片
image 服务层 开发框架 一般,互联网公司都会指定一个大的技术方向,然后使用统一的开发框架。例如,Java 相关的开发框架 SSH、SpringBoot, Ruby 的 Ruby on Rails, PHP 的 ThinkPHP, Python 的Django 等。
框架的选择,有一个总的原则:优选成熟的框架,避免盲目追逐新技术!
对于一般的螺丝工而言,所做的主要工作都是在这个开发框架之下。对于开发语言和框架的使用,一定要充分了解和利用语言和框架的特性。
以Java为例,在作者的开发中,涉及到一个加密解密的服务调用,服务提供方利用了JNI的技术——简单说就是C语言编写代码,提供api供Java调用,弥补了Java相对没那么底层的劣势,大大提高了运算的速度。
在服务开发这个日常工作的层面,可以做到这些事情来提高性能:
- 并发处理,通过多线程将串行逻辑并行化。
- 减少IO次数,比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。
- 减少IO时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。
- 程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化,或者采用更高效的算法
- 各种池化技术的使用和池大小的设置,包括HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。
- JVM优化,包括新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。
- 锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。
- 设置合适的超时时间、重试次数及机制,必要时要及时降级,返回兜底数据等,防止把服务提方供打崩
- 防重设计:通过防重key、防重表等方式实现防重
- 幂等设计:在接口层面实现幂等设计
比如说总共有 10 个系统依赖 A 系统的 X 接口,A 系统实现了一个新接口 Y, 能够更好地提供原有 X 接口的功能,如果要让已有的 10 个系统都切换到 Y 接口,则这 10 个系统的几十上百台器的配置都要修改,然后重启,可想而知这个效率是很低的。
服务中心的实现主要采用服务名字系统。
- 服务务名字系统 (Service Name System)
DNS 的作用将域名解析为 IP 地址,主要原因是我们记不住太多的数字 IP, 域名就容易记住。服务名字系统是为了将 Service 名称解析为 "host + port + 接口名称" ,但是和 DNS一样,真正发起请求的还是请求方。
文章图片
image 在微服务的架构下,实现这个功能的称之为注册中心,例如在Java语言体系下,开源的注册中心有Nacos、Ecuraka等。
配置中心 配置中心就是集中管理各个服务的配置。
在服务不多的时候,各个服务各自管理自己的配置,没有问题,但是当服务成百上千,再各行其政,就是一个比较头疼的事。
所以将配置中心抽象成公共的组件,集中配置多个系统,操作效率高。
在微服务架构体系下,配置中心的开源方案有SpringCloud的SpringCloud Config、阿里的Nacos等。
服务框架 服务拆分最直接的影响就是本地调用的服务变成了远程调用,服务消费者A需要通过注册中心去查询服务提供者B的地址,然后发起调用,这个看似简单的过程就可能会遇到下面几种情况,比如:
- 注册中心宕机;
- 服务提供者B有节点宕机;
- 服务消费者A和注册中心之间的网络不通;
- 服务提供者B和注册中心之间的网络不通;
- 服务消费者A和服务提供者B之间的网络不通;
- 服务提供者B有些节点性能变慢;
- 服务提供者B短时间内出现问题。
在Java语言体系下,目前流行的服务治理框架有SpringCloud和Dubbo。
以SpringCloud为例:
文章图片
image
- Feign封装RestTemplate实现http请求方式的远程调用
- Feign封装Ribbon实现客户端负载均衡
- Euraka集群部署实现注册中心高可用
- 注册中心心跳监测,更新服务可用状态
- 集成Hystrix实现熔断机制
- Zuul作为API 网关 ,提供路由转发、请求过滤等功能
- Config实现分布式配置管理
- Sluth实现调用链路跟踪
- 集成ELK,通过Kafka队列将日志异步写入Elasticsearch,通过Kibana可视化查看
Dubbo主要提供了最基础的RPC功能。
不过SpringCloud的RPC采用了HTTP协议,可能性能会差一些。
利好的是,“SpringCloud2.0”——SpringCloud Alibaba流行了起来,Dubbo也可以完美地融入SpringCloud的生态。
消息队列 消息队列在高性能、高扩展、高可用的架构中扮演着很重要的角色。
消息队列是用来解耦一些不需要同步调用的服务或者订阅一些自己系统关心的变化。使用消息队列可以实现服务解耦(一对多消费)、异步处理、流量削峰/缓冲等。
服务解耦 服务解耦可以降低服务间耦合,提高系统系统的扩展性。
例如一个订单服务,有多个下游,如果不用消息队列,那么订单服务就要调用多个下游。如果需求要再加下游,那么订单服务就得添加调用新下流的功能,这就比较烦。
引入消息队列之后,订单服务就可以直接把订单相关消息塞到消息队列中,下游系统只管订阅就行了。
文章图片
image 异步处理 异步处理可以降低响应时间,提高系统性能。
随着业务的发展项目的请求链路越来越长,这样一来导致的后果就是响应时间变长,有些操作其实不用同步处理,这时候就可以考虑采用异步的方式了。
文章图片
image 流量削峰/缓冲 流量削峰/缓冲可以提高系统的可用性。
我们前面提到了接入层的限流,在服务层的限流可以通过消息队列来实现。网关的请求先放入消息队列中,后端服务尽可能去消息队列中消费请求。超时的请求可以直接返回错误,也可以在消息队列中等待。
文章图片
image 消息队列系统基本功能的实现比较简单,但要做到高性能、高可用、消息时序性、消息事务性则比较难。业界已经有很多成熟的开源实现方案,如果要求不高,基本上拿来用即可,例如,RocketMQ、Kafka、ActiveMQ 等。
但如果业务对消息的可靠性、时序、事务性要求较高时,则要深入研究这些开源方案,提前考虑可能会遇到的问题,例如消息重复消费、消息丢失、消息堆积等等。
平台层 当业务规模比较小、系统复杂度不高时,运维、测试、数据分析、管理等支撑功能主要由各系统或者团队独立完成。随着业务规模越来越大,系统复杂度越来越高,子系统数量越来越多,如果继续采取各自为政的方式来实现这些支撑功能,会发现重复工作非常多。所以就会自然地把相关功能抽离出来,作为公共的服务,避免重复造轮子,减少不规范带来的沟通和协作成本。
平台层是服务化思维下的产物。将公共的一些功能拆分出来,让相关的业务服务只专注于自己的业务,这样有利于明确服务的职责,方便服务扩展。
同时一些公共的平台,也有利于各个服务之间的统筹,例如数据平台,可以对数据进行聚合,某个服务以前需要一些整合一些数据可能要调用多个上游服务,但是引入数据平台以后,只需要从数据平台取数据就可以了,可以降低服务的响应时间。
运维平台 运维平台核心的职责分为四大块:配置、部署、监控、应急,每个职责对应系统生命周期的一个阶段,如下图所示:
文章图片
image
- 部署:主要负责将系统发布到线上。例如,包管理、灰度发布管理、回滚等。
- 监控:主要负责收集系统上线运行后的相关数据并进行监控,以便及时发现问题。
- 应急:主要负责系统出故障后的处理。例如,停止程序、下线故障机器、切换 IP 等。
- 标准化:要制定运维标准,规范配置管理、部署流程、监控指标、应急能力等,各系统按照运维标准来实现,避免不同的系统不同的处理方式。
- 平台化:传统的手工运维方式需要投入大量人力,效率低,容易出错,因此需要在运维标准化的基础上,将运维的相关操作都集成到运维平台中,通过运维平台来完成运维工作。
- 自动化:传统手工运维方式效率低下的一个主要原因就是要执行大量重复的操作,运维平台可以将这些重复操作固化下来,由系统自动完成。
- 可视化:运维平台有非常多的数据,如果全部通过人工去查询数据再来判断,则效率很低,可视化的主要目的就是为了提升数据查看效率。
测试平台的核心目的是提升测试效率,从而提升产品质量,其设计关键就是自动化。
文章图片
image 数据平台 数据平台的核心职责主要包括三部分:数据管理、数据分析和数据应用。每一部分又包含更多的细分领域,详细的数据平台架构如下图所示:
文章图片
image
- 数据管理
- 数据采集:从业务系统搜集各类数据。例如,日志、用户行为、业务数据等,将这些数据传送到数据平台。
- 数据存储:将从业务系统采集的数据存储到数据平台,用于后续数据分析。
- 数据访问:负责对外提供各种协议用于读写数据。例如,SQL、 Hive、 Key-Value 等读写协议。
- 数据安全:通常情况下数据平台都是多个业务共享的,部分业务敏感数据需要加以保护,防止被其他业务读取甚至修改,因此需要设计数据安全策略来保护数据。
- 数据分析
- 数据挖掘:数据挖掘这个概念本身含义可以很广,为了与机器学习和深度学习区分开,这里的数据挖掘主要是指传统的数据挖掘方式。例如,有经验的数据分析人员基于数据仓库构建一系列规则来对数据进行分析从而发现一些隐含的规律、现象、问题等,经典的数据挖掘案例就是沃尔玛的啤酒与尿布的关联关系的发现。
- 机器学习、深度学习:机器学习和深度学习属于数据挖掘的一种具体实现方式,由于其实现方式与传统的数据挖掘方式差异较大,因此数据平台在实现机器学习和深度学习时,需要针对机器学习和深度学习独立进行设计。
- 数据应用
管理平台 管理平台的核心职责就是权限管理,无论是业务系统(例如,淘宝网) 、中间件系统(例如,消息队列 Kafka) , 还是平台系统(例如,运维平台) ,都需要进行管理。如果每个系统都自己来实现权限管理,效率太低,重复工作很多,因此需要统一的管理平台来管理所有的系统的权限。
说到“平台”,不由地想起这几年一会儿被人猛吹,一会儿被人唱衰的“中台”。在平台里的数据平台,其实已经和所谓的“数据中台”类似了。“中台”是个概念性的东西,具体怎么实现,没有统一的标准方案。作者所在的公司,也跟风建了中台,以“数据中台”为例,我们数据中台的建设主要为了数据共享和数据可视化,简单说就是把各个业务模块的一些数据汇聚起来。说起来简单,落地很难,数据汇聚的及时性、数据共享的快速响应……最终的解决方案是采购了阿里的一些商业化组件,花了老鼻子钱,但是效果,不能说一地鸡毛,也差不多吧。缓存层 虽然我们可以通过各种手段来提升存储系统的性能,但在某些复杂的业务场景下,单纯依靠存储系统的性能提升不够的。
绝大部分在线业务都是读多写少。例如,微博、淘宝、微信这类互联网业务,读业务占了整体业务量的 90%以上。以微博为例:一个明星发一条微博,可能几千万人来浏览。
如果直接从DB中取数据,有两个问题,一个是DB查询的速度有瓶颈,会增加系统的响应时间,一个是数据库本身的并发瓶颈。缓存就是为了弥补读多写少场景下存储系统的不足。
在前面我们提到的CDN可以说是缓存的一种,它缓存的是静态资源。
从整个架构来看,一般采用多级缓存的架构,在不同层级对数据进行缓存,来提升访问效率。
文章图片
image 简单说一下整体架构和流程,缓存一级一级地去读取,没有命中再去读取下一级,先读取本地缓存,再去读取分布式缓存,分布式缓存也没有命中,最后就得去读取DB。
分布式缓存 为了提高缓存的可用性,一般采用分布式缓存。分布式缓存一般采用分片实现,即将数据分散到多个实例或多台服务器。算法一般釆用取模和一致性哈希。
要采用不过期缓存机制,可以考虑取模机制,扩容时一般是新建一个集群。
文章图片
image 而对于可以丢失的缓存数据,可以考虑一致性哈希,即使其中一个实例出问题只是丢一小部分。
文章图片
image 对于分片实现可以考虑客户端实现,或者使用如Twemproxy 中间件进行代理(分片对客户端是透明的)。
如果使用 Redis, 则 可 以考虑使用 redis-cluster 分布式集群方案。
文章图片
image 热点本地缓存 对于那些访问非常频繁的热点缓存,如果每次都去远程缓存系统中获取,可能会因为访问量太大导致远程缓存系统请求过多、负载过高或者带宽过高等问题,最终可能导致缓存响应慢,使客户端请求超时。
一种解决方案是通过挂更多的从缓存,客户端通过负载均衡机制读取从缓存系统数据。不过也可以在客户端所在的应用/代理层本地存储一份,从而避免访问远程缓存,即使像库存这种数据,在有些应用系统中也可以进行几秒钟的本地缓存,从而降低远程系统的压力。
缓存的引入虽然提高了系统的性能,但同时也增加了系统的复杂度,带来了一些运维的成本。缓存穿透 缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据,结果存储系统也没有数据。
缓存穿透的示意图:
文章图片
image 一般情况下,如果存储系统中没有某个数据,则不会在缓存中存储相应的数据,这样就导致用户查询的时候,在缓存中找不到对应的数据,每次都要去存储系统中再查询一遍,然后返回数据不存在。缓存在这个场景中并没有起到分担存储系统访问压力的作用。
通常情况下,业务上读取不存在的数据的请求量并不会太大,但如果出现一些异常情况,例如被黑客攻击,故意大量访问某些读取不存在数据的业务,有可能会将存储系统拖垮。
这种情况的解决办法有两种:
一种比较简单,如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值) 存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统。
一种需要引入布隆过滤器,它的原理也很简单就是利用高效的数据结构和算法,快速判断出查询的Key是否在数据库中存在,不存在直接返回空,存在就去查了DB,刷新KV再返回值。
缓存击穿 缓存击穿和缓存穿透也有点难以区分,缓存穿透表示的是缓存和数据库中都没有数据,缓存击穿表示缓存中没有数据而数据库中有数据。缓存击穿是某个热点的key失效,大并发集中对其进行请求,就会造成大量请求读缓存没读到数据,从而导致高并发访问数据库,引起数据库压力剧增。这种现象就叫做缓存击穿。
缓存击穿示意图:
文章图片
image 关键在于某个热点的key失效了,导致大并发集中打在数据库上。所以要从两个方面解决,第一是否可以考虑热点key不设置过期时间,第二是否可以考虑降低打在数据库上的请求数量。
主要有两个解决办法:
- 利用互斥锁保证同一时刻只有一个客户端可以查询底层数据库的这个数据,一旦查到数据就缓存至Redis内,避免其他大量请求同时穿过Redis访问底层数据库。这种方式会阻塞其他的线程,此时系统的吞吐量会下降
- 热点数据缓存永远不过期。
- 物理不过期,针对热点key不设置过期时间
- 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建
两种情况导致的同样的后果就是大量的请求直接落在数据库上,对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求,最严重的后果就是直接导致数据库宕机,可能会引起连锁反应,导致系统崩溃。
文章图片
image 缓存雪崩的解决方案可以分为三个维度:
- 事前:
② 分级缓存:第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。
③ 热点数据缓存永远不过期。
④ 保证Redis缓存的高可用,防止Redis宕机导致缓存雪崩的问题。可以使用 Redis集群等方式来避免 Redis 全盘崩溃的情况。
- 事中:
② 使用熔断机制,限流降级。当流量达到一定的阈值,直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上将数据库击垮,至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。
- 事后:
存储层 不管是为了满足业务发展的需要,还是为了提升自己的竞争力,关系数据库厂商(Oracle、DB2、MySQL 等)在优化和提升单个数据库服务器的性能方面也做了非常多的技术优化和改进。但业务发展速度和数据增长速度,远远超出数据库厂商的优化速度,尤其是互联网业务兴起之后,海量用户加上海量数据的特点,单个数据库服务器已经难以满足业务需要,必须考虑数据库集群的方式来提升性能。
读写分离 读写分离的基本原理是将数据库读写操作分散到不同的节点上,下面是其基本架构图:
文章图片
image 读写分离的基本实现是:
- 数据库服务器搭建主从集群,一主一从、一主多从都可以。
- 数据库主机负责读写操作,从机只负责读操作。
- 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
- 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
文章图片
image 读写分离的实现逻辑并不复杂,但有两个细节点将引入设计复杂度:主从复制延迟和分配机制。
复制延迟 以 MySQL 为例,主从复制延迟可能达到 1 秒,如果有大量数据同步,延迟 1 分钟也是有可能的。
主从复制延迟会带来一个问题:如果业务服务器将数据写入到数据库主服务器后立刻 (1 秒 内)进行读取,此时读操作访问的是从机,主机还没有将数据复制过来,到从机读取数据是读不到最新数据的,业务上就可能出现问题。
比如说将微博的信息同步给审核系统,所以我们在更新完主库之后,会将微博的 ID 写入消息队列,再由队列处理机依据 ID 在从库中 获取微博信息再发送给审核系统。此时如果主从数据库存在延迟,会导致在从库中获取不到微博信息,整个流程会出现异常。
文章图片
image 解决主从复制延迟的常见方法:
- 数据的冗余
- 使用缓存
- 二次读取
- 查询主库
分配机制 将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。
- 程序代码封装
文章图片
image 程序代码封装的方式具备几个特点:
- 实现简单,而且可以根据业务做较多定制化的功能。
- 每个编程语言都需要自己实现一次,无法通用,如果一个业务包含多个编程语言写的多个子系统,则重复开发的工作量比较大。
- 故障情况下,如果主从发生切换,则可能需要所有系统都修改配置并重启。
文章图片
image
- 中间件封装
其基本架构是:
文章图片
image 数据库中间件的方式具备的特点是:
- 能够支持多种编程语言,因为数据库中间件对业务服务器提供的是标准 SQL 接口。
- 数据库中间件要支持完整的 SQL 语法和数据库服务器的协议(例如,MySQL 客户端和服务器的连接协议) ,实现比较复杂,细节特别多,很容易出现 bug, 需要较长的时间才能稳定。
- 数据库中间件自己不执行真正的读写操作,但所有的数据库操作请求都要经过中间件,中间件的性能要求也很高。
- 数据库主从切换对业务服务器无感知,数据库中间件可以探测数据库服务器的主从状态。例如,向某个测试表写入一条数据,成功的就是主机,失败的就是从机。
分库分表 读写分离分散了数据库读写操作的压力,但没有分散存储压力,当数据量达到干万甚至上亿条的时候,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在这几个方面:
- 数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。
- 数据文件会变得很大,数据库备份和恢复需要耗费很长时间。
- 数据文件越大,极端情况下丟失数据的风险越高(例如,机房火灾导致数据库主备机都发生故障)。
业务分库 业务分库指的是按照业务模块将数据分散到不同的数据库服务器。例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上。
文章图片
image 虽然业务分库能够分散存储和访问压力,但同时也带来了新的问题,接下来我们详细分析一下。
- join 操作问题
例如: "查询购买了化妆品的用户中女性用户的列表〃 这个功能,虽然订单数据中有用户的 ID信息,但是用户的性别数据在用户数据库中,如果在同一个库中,简单的 join 查询就能完成;但现在数据分散在两个不同的数据库中,无法做 join 查询,只能采取先从订单数据库中查询购买了化妆品的用户 ID 列表,然后再到用户数据库中查询这批用户 ID 中的女性用户列表,这样实现就比简单的 join 查询要复杂一些。
- 事务问题
例如,用户下订单的时候需要扣商品库存,如果订单数据和商品数据在同一个数据库中,我们可订单,如果因为订单数据库异常导致生成订单失败,业务程序又需要将商品库存加上;而如果因为业务程序自己异常导致生成订单失败,则商品库存就无法恢复了,需要人工通过曰志等方式来手工修复库存异常。
- 成本问题
基于上述原因,对于小公司初创业务,并不建议一开始就这样拆分,主要有几个原因:初创业务存在很大的不确定性,业务不一定能发展起来,业务开始的时候并没有真正的存储和访问压力,业务分库并不能为业务带来价值。业务分库后,表之间的 join 查询、数据库事务无法简单实现了。
业务分库后,因为不同的数据要读写不同的数据库,代码中需要增加根据数据类型映射到不同数据库的逻辑,增加了工作量。而业务初创期间最重要的是快速实现、快速验证,业务分库会拖慢业务节奏。
单表拆分 将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。
单表数据拆分有两种方式:垂直分表和水平分表。示意图如下:
文章图片
image 分表能够有效地分散存储压力和带来性能提升,但和分库一样,也会引入各种复杂性。
两种分表方式可以用一个例子比喻,我们很多人可能都看过这么一篇文章,怎么把苹果切出星星来,答案是横着切。
文章图片
image
- 垂直分表
不过相比接下来要讲的水平分表,这个复杂性就是小巫见大巫了。
- 水平分表
水平分表相比垂直分表,会引入更多的复杂性,主要表现在下面几个方面:
- 路由
常见的路由算法有:
文章图片
image 范围路由:选取有序的数据列 (例如,整形、时间戳等) 作为路由的条件,不同分段分散到不同的数据库表中。以订单 Id 为例,路由算法可以按照 1000万 的范围大小进行分段。范围路由设计的复杂点主要体现在分段大小的选取上,分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在 100 万至2000 万之间,具体需要根据业务选取合适的分段大小。
范围路由的优点是可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。范围路由的一个比较隐含的缺点是分布不均匀,假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1000 条,而另外一个分段实际存储的数据量有 900 万条。
Hash 路由:选取某个列 (或者某几个列组合也可以) 的值进行 Hash 运算,然后根据 Hash 结果分散到不同的数据库表中。同样以订单 id 为例,假如我们一开始就规划了 4个数据库表,路由算法可以简单地用 id % 4 的值来表示数据所属的数据库表编号,id 为 12的订单放到编号为 50的子表中,id为 13的订单放到编号为 61的字表中。
Hash 路由设计的复杂点主要体现在初始表数量的选取上,表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。而用了 Hash 路由后,增加字表数量是非常麻烦的,所有数据都要重分布。
Hash 路由的优缺点和范围路由基本相反,Hash 路由的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据都要重分布。
配置路由:配置路由就是路由表,用一张独立的表来记录路由信息。
同样以订单id 为例,我们新增一张 order_router 表,这个表包含 orderjd 和 tablejd 两列 , 根据 orderjd 就可以查询对应的 table_id。
配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。
配置路由的缺点就是必须多查询一次,会影响整体性能;而且路由表本身如果太大(例如,几亿条数据) ,性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则又面临一个死循环式的路由算法选择问题。
- join 操作
- count()操作
count() 相加:具体做法是在业务代码或者数据库中间件中对每个表进行 count操作,然后将结果相加。这种方式实现简单,缺点就是性能比较低。例如,水平分表后切分为 20 张表,则要进行 2 0 次 count()操作,如果串行的话,可能需要几秒钟才能得到结果。
记录数表:具体做法是新建一张表,假如表名为 "记录数表” ,包含 table_name、 row_count两个字段,每次插入或者删除子表数据成功后,都更新 "记录数表“。这种方式获取表记录数的性能要大大优于 count()相加的方式,因为只需要一次简单查询就可以获取数据。缺点是复杂度增加不少,对子表的操作要同步操作 "记录数表" ,如果有一个业务逻辑遗漏了,数据就会不一致;且针对 "记录数表" 的操作和针对子表的操作无法放在同一事务中进行处理,异常的情况下会出现操作子表成功了而操作记录数表失败,同样会导致数据不一致。
此外,记录数表的方式也增加了数据库的写压力,因为每次针对子表的 insert 和 delete 操作都要 update 记录数表,所以对于一些不要求记录数实时保持精确的业务,也可以通过后台定时更新记录数表。定时更新实际上就是 "count()相加" 和 "记录数表" 的结合,即定时通过count()相加计算表的记录数,然后更新记录数表中的数据。
- order by 操作
实现方法
和数据库读写分离类似,分库分表具体的实现方式也是 "程序代码封装" 和 "中间件封装" ,但实现会更复杂。读写分离实现时只要识别 SQL 操作是读操作还是写操作,通过简单的判断SELECT、UPDATE、 INSERT、DELETE 几个关键字就可以做到,而分库分表的实现除了要判断操作类型外,还要判断 SQL 中具体需要操作的表、操作函数(例如 count 函数)、order by、group by 操作等,然后再根据不同的操作进行不同的处理。例如 order by 操作,需要先从多个库查询到各个库的数据,然后再重新 order by 才能得到最终的结果。
数据异构 完成分库分表以后,我们看到存在一些问题,除了"程序代码封装" 和 "中间件封装"之外,我们还有一种办法,就是数据异构。数据异构就是将数据进行异地存储,比如业务上将MySQL的数据,写一份到Redis中,这就是实现了数据在集群中的异地存储,也就是数据异构。
在数据量和访问量双高时使用数据异构是非常有效的,但增加了架构的复杂度。异构时可以通过双写、订阅 MQ 或者 binlog 并解析实现。
- 双写:在写入数据的时候,同时将数据写入MySQL和异构存储系统;
- MQ:写入MySQL成功后,发一个mq消息,缓存读取mq消息并将消息写入异构存储系统;
- binlog:写入MySQL后,缓存系统x消费binlog,将变动写入异构存储系统。
文章图片
image 在图中用到了ES搜索集群来处理搜索业务,同样也可以我们前面提到的跨库join的问题。
在设计异构的时候,我们可以充分利用一些流行的NoSQL数据库。NoSQL尽管已经被证明不能取代关系型数据库,但是在很多场景下是关系型数据库的有力补充。
举几个例子,像我们熟悉的Redis这样的KV存储,有极高的读写性能,在读写性能有要求的场景可以使用;
Hbase、Cassandra 这样的列式存储数据库。这种数据库的特点是数据不像传统数据库以行为单位来存储,而是以列来存储,适用于一些离线数据统计的场景;
MongoDB、CouchDB 这样的文档型数据库,具备 Schema Free(模式自由)的特点,数据表中的字段可以任意扩展,可以用于数据字段不固定的场景。
查询维度异构 比如对于订单库,当对其分库分表后,如果想按照商家维度或者按照用户维度进行查询,那么是非常困难的,因此可以通过异构数据库来解决这个问题。可以采用下图的架构。
文章图片
image 或者采用下图的ES异构:
文章图片
image 异构数据主要存储数据之间的关系,然后通过查询源库查询实际数据。不过,有时可以通过数据冗余存储来减少源库查询量或者提升查询性能。
聚合据异构 商品详情页中一般包括商品基本信息、商品属性、商品图片,在前端展示商品详情页时,是按照商品 ID 维度进行查询,并且需要查询 3 个甚至更多的库才能查到所有展示数据。此时,如果其中一个库不稳定,就会导致商品详情页出现问题,因此,我们把数据聚合后异构存储到 KV 存储集群(如存储 JSON ), 这样只需要一次查询就能得到所有的展示数据。这种方式也需要系统有了一定的数据量和访问量时再考虑。
文章图片
image 高并发架构要点 通过前面的内容,已经差不多了解高并发的架构是一个什么样,接下来做一些总结和补充。
高性能要点
文章图片
image 高可用要点
文章图片
image 除了从技术的角度来考虑,保证高可用同样需要良好的组织制度,来保证服务出现问题的快速恢复,Java并发编程实战笔记。
高扩展要点 1、合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。
2、存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。
3、业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心请求和非核心请求拆分,还可以按照请求源拆(比如To C和To B,APP和H5 )。
推荐阅读
- echart|echart 双轴图开发
- 程序员|【高级Java架构师系统学习】毕业一年萌新的Java大厂面经,最新整理
- locate搜索
- 8、Flask构建弹幕微电影网站-搭建后台页面-密码修改、主页控制面板
- 年薪30万的Java架构师必会的springboot面试题
- EdgeDB 架构简析
- 转发的,
- 大众点评(redux架构)
- 命令行上传小程序版本至微信后台
- 架构的架构基础