后端架构演进
单体架构
所谓单体架构,就是只有一台服务器,所有的系统、程序、服务、应用都安装在这一台服务器上。比如一个 bbs 系统,它用到的数据库,它需要存储的图片和文件等,统统都部署在同一台服务器上。
单体架构的好处就是简单,相对便宜。一般在互联网早期、或创业型团队,都有经历过单体架构。通过配置升级来应对并发量过高的情况,比如把 CPU/内存升配成 64 核、128G内存。
应用和数据库分离-垂直架构
当服务器负载过高时,拆分和单独部署应用服务器和数据库服务器。
当网站积累的内容多了,另外用户和访问量也越来越大了。每天的 UV 过万,PV 过十万时,服务器的负载会在单体架构下变得越来越高。
当网站的访问量越来越大,顶配服务器能支撑的并发量也是有极限的。这时候,就要对架构进行升级了,最简单的方式,就是从单体架构升级为垂直架构:把应用和数据库分开来部署。
数据库主从架构
网站有源源不断的访问,数据库的压力越来越大,经常出现拥堵和慢查询。此时可以考虑增加数据库的服务器数量。
比较常见的是使用数据库主从模式,可以是一主一从或者一主多从。一主一从就是一个主库加上一个从库,使用两台服务器。
主库负责全部的数据写入请求,从库只能用来查询,分散主库的查询压力。类似的一主多从,就是增加多个从库,这样通过增加从库的服务器资源,来提高查询的性能。
注意:
- 数据库主从模式调整后,要实现数据的读写分离,还需要程序做一些修改。
- 把以前单一的数据库实例改成读写两个数据库实例,数据库的配置信息也要增加主从的数据配置。
- 程序中把大部分的读请求改成从库数据库实例。
存在的问题:数据库主从同步会有延时:一条数据成功写入了主库,但是马上读取从库,因为同步延时,这条数据还没有从主库同步到从库,于是查询无结果。
这时便需要对从库延时进行监控,以 MySQL 为例,在从库上执行 show slave status;
语句查看从库的状态,Slave_IO_Running
和Slave_SQL_Running
都需要是 Yes。Seconds_Behind_Master
的延迟程度,数值越大说明延时越长。除了时刻关注从库的延时情况,在程序方面,也需要有一些策略性的调整。比如不要把全部查询都改为读从库,优先把对时间更新不敏感的数据改为读从库。
分布式应用架构
系统的并发量也同时增加,单台应用服务器的负载太高,考虑对服务器进行扩容,从一台服务器水平扩容成多个应用服务器。
把应用系统在每台服务器上都部署一套,然后再部署一个负载均衡服务器,比如 Nginx 来作为统一的接入层。
在分布式系统中,依赖于本地的文件存在一个问题:上传的图片和文件只保存在本地的文件系统上,其他的服务器上并没有这些文件,所以就会出现 404。比较简单的方法,就是在每一台应用服务器上都挂载同一个 nfs,并且配置一样的目录。这种方式,文件还是集中式的读写,只是通过网络来进行读写,不再是本地的IO了。另外也可搭建额外的 OSS 集群,同时引入 CDN 优化等。
在分布式架构下,正常网站的支撑基本达到 20-30万的 PV。
分布式缓存架构
数据库的压力依然很大,慢查询越来越多,到达瓶颈了,此时便引入分布式缓存服务,解放数据库。
在这个阶段,数据库面对庞大并发压力,再对数据库服务器通过水平、垂直扩容的收益已经不大了。可引入分布式缓存服务。比如:使用Redis集群。
一般认为,MySQL 服务单机可以支持 1000 的并发,Redis 服务单机可以支持10万的并发。利用缓存,可以把一些耗时几百毫秒的慢查询的结果缓存起来,缓存查询耗时只有1-2ms,性能提升了上百倍。
这便有新存在一些问题,比如:数据库与缓存的数据一致性问题(内容较多,此处省略2万字)。终于,在这种系统架构下,已经完全可以应对百万用户,每天几百万的 PV 的情况了。
数据库分表分库
当数据规模越来越大,数据量飞速增长,超过千万规模,只要涉及到数据库的查询、批量更新等操作时,又会变得很慢,再次成为整个系统的瓶颈。针对数据规模的不断增长,需要再一次升级数据库架构,考虑对数据库及数据表做水平拆分和垂直拆分。
单表的数据量太大,考虑对数据表进行分表,比如让单张表的数据规模控制在百万的规模。
- 水平拆分。子表的结构都一模一样,但是每个子表里面都只保存总数据的一部分。
- 垂直拆分。子表的字段都是大表的一部分。
数据库的分库方法跟分表的方法类似,同样是考虑用水平拆分或者垂直拆分。
微服务架构
当应用越来越多,越来越复杂时,考虑将应用拆分为多个子服务,需要注意垂直拆分的服务边界。
对一个大的应用系统进行垂直拆分,而服务拆分的依据就是各个服务的数据尽量独立。比如:在博客系统中,可以考虑把文章系统、评论系统、图片文件系统、等进行拆分。
早期的 SOA架构,即面向服务架构,也是把一个大的系统进行很多的独立的服务化拆分。微服务架构其实和 SOA 架构类似,是在 SOA 上做的升华。微服务架构重点强调的一个是"业务需要彻底的组件化和服务化",原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这样的小应用和其他各个应用之间,相互去协作通信,来完成一个交互和集成。
K8S 容器化、云原生架构
微服务太多,管理难度太大。通过 K8S 等技术,可以轻松管理成千上万的微服务,管理上百万核的服务器资源。同时可以让开发、测试、发布的周期变得更短。
云原生将进一步帮助我们提高开发、运维效率和灵活性,提高系统和产品的可用性。不再受到资源和地域的限制,可以快速在全球几十个地区把服务部署、运行起来。
系统中用到的 MySQL 集群、Redis集群、分布式存储、消息队列、Elasticsearch、Prometheus、分布式日志系统、实时计算服务等。
要管理和维护这么多的大型分布式系统,既要优化提高服务的性能,又要保证系统的高可靠可用性,这里需要投入的人力资源和服务器资源,都会是非常巨大的。如果对于云原生的资源管控不好,成本方面控制不好,可以选择混合云的方式。
微服务技术
- 微服务架构是一个分布式系统,是一个分散式系统。
- 服务部署可能会跨主机、网段、机房甚至是地区,各个服务之间通过 http 接口或者 RPC 进行调用。
服务发现
服务发现是微服务架构的灵魂,就是把服务的信息注册到一个配置中心,而且从配置中心,可以找到服务的更多信息。这里的配置中心,也叫做注册中心,管理着所有的服务信息。注册中心是所有微服务创建、启动、运行、销毁、互相调用都绕不过去的中心服务。
- 管理所有服务信息的注册中心;
- 功能:注册、查询、健康检查;
- 数据量大,包含多种服务,多个实例。
注册中心的功能
服务注册
。服务启动的时候,把服务名称和服务的访问方法等信息注册到注册中心。- 服务名称是一个关键字。
- 服务的访问方法需要有调用协议,是 http 还是 gRPC 等。同时还会有一个访问地址,可能是一个域名,也可能是一个IP:端口的集合。
服务查询
。通过服务名称查找到调用服务的访问方式。健康检查
。让注册中心随时掌握服务的运行状况。当服务的实例出现异常时,进行告警、服务自动重启等。
注册中心本身还是一个分布式的存储系统,作为一个配置中心,除了要保存服务信息,可能还会有各个微服务的自定义配置信息。对于注册中心来说,最大的挑战是系统的可靠性和数据的一致性。所以,所有的注册中心都是部署在多台服务器上。而且,每台机器上都会保存一份完整的配置信息。并且,多台服务器之间的数据同步,为了保证数据的强一致性,需要实现一些复杂的一致性协议。
服务发现过程:
- 服务提供者向注册中心注册服务信息。
- 服务消费者向服务提供者发起请求之前,从注册中心获取服务信息。这里,因为获取服务信息的时机不一样,存在两种服务发现的模式:
- 客户端发现模式,在服务消费者这一侧来获取服务信息。拿到服务信息之后,自己来实现负载均衡策略,直接请求到服务提供者。
- 服务端发现模式,引入一个服务端网关,服务消费者只需要请求服务端网关就可以了。然后在服务端网关这里,统一的去获取服务信息,再实现负载均衡策略。最终请求到服务提供者。
一些服务发现的实现方案:Apache Zookeeper、Etcd、Consul。
服务调用的限频、限流、降级和熔断
为应对线上突发情况,如某个目标服务出现 Bug,性能和并发能力急速下降,需要采取各种措施来保证系统的可用性。
超时
调用方主动中断请求的连接。在调用 API 的时候,一般会根据目标服务预估的接口延时来配置这个 TCP 超时时间。如 API 大部分情况都是100ms 以内就可以返回,那么,就可以把 TCP 请求超时设置为 500ms 或者1s。如果不设置超时,那么当目标服务出现异常,无法正常的快速响应时,那源服务与目标服务的所有请求就会无限的等待,TCP 连接数也就会越来越多,对于系统的资源开销也就越来
越大。
限流
限制请求的最大并发数。如目标服务最大可以支持 100QPS,那就不能让源服务超过这个并发请求进入。
常用算法:
- 计数器固定窗口算法。全局唯一的一个计数器,请求进来计数器 +1,请求结束计数器 -1,当计数器的数量超过限制的最大值,计数器不再 +1,也会拒绝超出的全部请求。计数器的增减通过原子操作进行。缺点就是不能处理同时涌入的大量请求。
- 计数器滑动窗口算法。假设单位时间是1秒,限流阀值为 3。在单位时间1秒内,每来一个请求,计数器就加1,如果计数器累加的次数超过限流阀值 3,后续的请求全部拒绝。等到1s结束后,计数器清0,重新开始计数。缺点:
- 一段时间内(不超过时间窗口)系统服务不可用。比如窗口大小为1s,限流大小为100,然后恰好在某个窗口的第1ms来了100个请求,然后第2ms-999ms的请求就都会被拒绝,这段时间用户会感觉系统服务不可用。
- 窗口切换时可能会产生两倍于阈值流量的请求。假设限流阀值为5个请求,单位时间窗口是1s,如果我们在单位时间内的前0.8-1s和1-1.2s,分别并发5个请求。虽然都没有超过阀值,但是如果算0.8-1.2s,则并发数高达10,已经超过单位时间1s不超过5阀值的定义啦,通过的请求达到了阈值的两倍。
- 滑动窗口限流。滑动窗口限流解决固定窗口临界值的问题,可以保证在任意时间窗口内都不会超过阈值。滑动窗口除了需要引入计数器之外还需要记录时间窗口内每个请求到达的时间点,因此对内存的占用会比较多。
- 漏桶算法。类似消息队列思想,往漏桶中以任意速率流入水,以固定的速率流出水。当水超过桶的容量时,会被溢出,也就是被丢弃。达到削峰填谷,平滑请求。
- 令牌桶算法。
- 令牌管理员根据限流大小,定速往令牌桶里放令牌。
- 如果令牌数量满了,超过令牌桶容量的限制,那就丢弃。
- 系统在接受到一个用户请求时,都会先去令牌桶要一个令牌。如果拿到令牌,那么就处理这个请求的业务逻辑;如果拿不到令牌,就直接拒绝这个请求。
熔断
暂时把调用目标服务的请求快速返回,不允许再重复调用目标服务。因为当目标服务在某段时间内出现大量的异常情况,这时候如果继续请求目标服务,大概率也还是返回异常结果。
熔断只是暂时停止调用,过一段时间后又会放一部分请求进来,看看后端服务是否恢复正常了。如一分钟内,服务的超时请求、500、503 等响应超过 100 次时,触发降级或者熔断。降级或者熔断 3 分钟后,尝试连续 10 次请求到后端服务,如果都正常返回了,则恢复。如果没有全部正常返回,则继续保持降级或者熔断状态,下一个 3 分钟后再来尝试恢复。
降级
类似熔断的场景。目标服务出现大量异常,这时候的响应不要像熔断那么简单粗暴,而是要有更加友好的响应。主要时为了当服务异常时,又想让服务看上去是正常的,有正常的返回。
降级常用的方法:
- 读旧数据,如果有备份数据、缓存数据,这时候也就可以利用起来了。
- 如果服务实现了多个解决方案,那么在方案1异常时,就可以切换到其他的方案来执行。
- 默认值,设置降级时返回默认值。
- 放弃部分请求,可以减少对后端服务的并发量。
- 降低质量,把耗时长、计算量大的逻辑直接跳过,不要求实时计算出所有结果。
- 反向过滤,如果需要过滤一个结果集太费劲,可以考虑不过滤,直接返回全部内容。
- 补偿,服务异常时记录下日志,事后再进行补偿性操作。
隔离
按种类隔离/用户隔离等方式隔离不同的依赖调用,避免服务之间相互影响。如源服务 A/B 都直接调用目标服务 C。源服务 A 出现突发的高并发来请求目标服务 C。导致目标服务 C 出现拥堵,那么,这时候也就会影响源服务 B 的请求。
服务治理
对服务进行管理、监控和控制,以确保其满足业务需求和合规要求。
服务治理包括以下方面:
- 服务设计:确保服务的设计符合业务需求,并且遵循最佳实践和标准。
- 服务开发:确保服务的开发质量和可靠性,遵循编码标准和规范。
- 服务测试:确保服务在各种场景下都能正常工作,包括功能测试、性能测试和安全测试。
- 服务部署:确保服务能够成功部署到目标环境中,并且可以与其他服务集成。
- 服务监控:监控服务的运行状况,及时发现和解决问题,保证服务的稳定性和可靠性。
- 服务升级:在必要的时候,对服务进行升级和更新,以满足业务需求和技术要求。
- 服务管理:管理服务的整个生命周期,包括版本控制、文档管理、安全管理等。
- 服务治理对于现代企业架构中的服务化架构非常重要,可以帮助企业实现服务的复用、提高服务质量、降低服务开发成本、提高服务可靠性和安全性。
负载均衡
简单来讲,负载均衡的作用就是把所有请求合理的分配到每一个服务实例上,避免某个服务实例一直处于负载过高的情况。
负载均衡常见算法
- 轮询法 & 加权轮询法
- 轮询法:现在有三个服务实例,那么第1个请求就会调用实例1,第2个请求调用实例2,第3个请求调用实例3,第4个请求再调用实例1,第5个请求在调用实例2,这样循环下去。首先是设计一个递增的计数器,每个请求都会给计数器递增,然后用这个计数器对实例数量3进行取模操作,得到了余数即为要调用的实例。
- 加权轮询法:实例1的权重是3,那么可以给它分配1、2、3这3个数字作为调用它的余数。实例2的权重是2,那么可以分配4、5这2个数字作为调用它的余数。实例1的权重是1,那么可以分配6这1个数字作为调用它的余数。请求进来,计数器还是一样的递增,只是不对实例数量3进行取模,而是对权重总和 3+2+1=6 进行取模,得到的余数也就知道要调用哪个实例了。
- 随机法 & 加权随机法:把计数器换成了随机数。每个请求进来,不再是给计数器递增,而是给请求分配一个随机数字,然后用这个随机数字进行取模操作,得到的余数就可以确定要调用的实例。
- 随机法:对实例数量取模。
- 加权随机法:对权重总和进行取模。
- 最小连接算法:优先调用连接数最少的服务实例。
- 源地址哈希法:通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。当想要一个用户的所有请求都调用到同一个实例,可以选择IP,或者用户 ID 等来进行哈希计算。
另外还有应用一致性哈希算法的负载均衡策略等。
负载均衡的实现方案,常见的一般在客户端、DNS、网关,另外也有在各种中间件组件处,如数据库代理,缓存代理等。