大型互联网应用系统使用技术方案和手段
一、单体架构性能极致:
1.数据库 :池化技术
大量查询,主从分离
写入数据量大, 分库分表(发号器设计)
高并发下,引入nosql
2.数据库达到瓶颈后,引入缓存
缓存: CDN加速静态资源
热点数据,做本地应用缓存
本地应用与数据之间加分布式缓存
3.短暂高并发写场景应对(秒杀),引入消息队列
削峰填谷
异步
解耦
确保消息被消费一次
降低消息队列系统中消息的延迟
4.负载均衡(代理类负载均衡模式)
1.LVS( 四层负载) 适合在入口处承担大流量的请求分发,而 Nginx ( 七层负载)要部署在业务服务器之前做更细维度的请求分发
2. QPS 在十万以内,可以考虑不引入 LVS 而直接使用 Nginx 作为唯一的负载均衡服务器
二、分布式演化(依赖资源可扩展性出现瓶颈;一体化架构带来研发成本、部署成本难以接受后,进行演化)
1.服务拆分和架构改造
1.1 服务拆分原则:做到单一服务内部功能的高内聚和低耦合;
关注服务拆分的粒度,先粗略拆分再逐渐细化;
拆分的过程,要尽量避免影响产品的日常功能迭代
先剥离比较独立的边界服务(非核心的比如短信、位置服务)
梳理服务依赖关系,优先剥离被依赖的服务
服务接口的定义具备可扩展性(由本地调用变成了跨网络通信,一定要考虑扩展性)
1.2 服务化后带来什么问题:
服务调用: 同一进程的方法调用变成跨进程的网络调用,性能降低。 解决手段:高效RPC框架
需要清楚被调用服务信息。 解决手段:服务注册中心
服务治理: 被依赖服务出现性能问题蔓延,导致系统故障。 解决方案:熔断、降级、限流、超时控制等
调用链路上涉及多个服务,如何确定问题源头。 解决方案:分布式追踪工具
2.设计RPC框架
2.1 选择合适的网络模型
常见IO模型: 同步阻塞I/O、同步非阻塞I/O、同步多路I/O复用、信号驱动I/O、异步I/O
选择IO模型: 同步多路复用I/O (linux中的select、epoll, java中的netty)
2.2有针对性地调整网络参数优化网络传输性能
调优参数:开启tcp_nodelay. (关闭nagle's算法)
发送缓冲区和接收缓冲区大小
客户端连接请求缓冲队列的大小(back log)
2.3选择合适的序列化方式,以提升封包、解包的性能
性能: 时间上(序列化和反序列化速度)、空间上(序列化后二进制串大小,影响传输效率)
是否可以跨语言、跨平台
可扩展性
JSON、Thrift、Protobuf
3.设计注册中心(常见的注册中心:zookeeper、 etcd、Nacos、eureka)
3.1服务优雅关闭设计
先从注册中心服务列表剔除服务,再看是否有流量进入,如果没有,再关闭服务。
3.2 心跳机制设计( 服务状态管理)
服务故障感知
3.3 增加服务自动摘除的保护机制
防止网络抖动,导致摘除所有服务实例
3.4 防止通知风暴
通知消息量达到一定阈值,停止变更通知;
只变更一个节点的时候,只通知这个节点的变更信息;
变更节点的时候,只通知订阅这个节点的服务
4.分布式trace设计
1.对代码要无侵入,你可以使用切面编程的方式来解决;
2.采用静态代理和日志采样的方式,来尽量减少追踪日志对于系统性能的影响
3. 提供一个开关,方便在线上随时将日志打印关闭
4. 采用 traceId + spanId 这两个数据维度来记录服务之间的调用关系, traceId 串起单次请求,spanId 记录每一次 RPC 调用
5.搭建服务端监控体系
1.监控指标:、耗时、请求量和错误数是三种最通用的监控指标,不同的组件还有一些特殊的监控指标;
2.指标数据采集:Agent、埋点和日志是三种最常见的数据采集方式;
agent代理
客户端埋点
日志数据采集
3.数据处理
4.报表展示
访问趋势报表用来展示服务的整体运行情况, 这类报表接入的是 Web 服务器,和应用服务器的访问日志,展示了服务整体的访问量、响应时间情况、错误数量、带宽 等信息
性能报表用来分析资源或者依赖的服务是否出现问题, 这类报表对接的是资源和依赖服务的埋点数据,展示了被埋点资源的访问量和响应时间情况
资源报表用来追查资源问题的根本原因, 这类报表主要对接的是,使用 Agent 采集的资源的运行情况数据。
三、微服务架构
1.API网关设计和使用( 在 API 网关中植入服务熔断、降级和流控等服务治理的策略。)
1.1API网关设计考虑点:
性能考虑:选择多路复用IO模型
扩展性:责任链模式(热插拔)
并行处理能力:线程池隔离
针对不同的服务,采用不同的线程池
在线程池内部针对不同的接口设置配额
1.2API网关使用:
流量网关: 先将 API 网关从 Web 层中独立出来,将协议转换、限流、黑白名单等事情挪到 API 网关中来处理,形成独立的入口网关层
业务网关: 再独立出一组网关专门做服务聚合、超时控制方面的事情(这一层可以用服务层代替, 把原来的服务层分成服务聚合层和服务原子层)
出口网关: 出口网关主要是为调用第三方服务提供统一的出口,在其中可以对调用外部的 API 做统一的认证、授权、审计以及访问控制;
1.3常用网关:
Kong是在 Nginx 中运行的 Lua 程序。得益于 Nginx 的性能优势,Kong 相比于其它的开源 API 网关来说,性能方面是最好的。由于大中型公司对于 Nginx 运维能力 都比较强,所以选择 Kong 作为 API 网关,无论是在性能还是在运维的把控力上,都是比较好的选择;
Zuul是 Spring Cloud 全家桶中的成员,如果你已经使用了 Spring Cloud 中的其他组件,那么也可以考虑使用 Zuul 和它们无缝集成。不过,Zuul1 因为采用同步阻塞 模型,所以在性能上并不是很高效,而 Zuul2 推出时间不长,难免会有坑。但是 Zuul 的代码简单易懂,可以很好地把控,并且你的系统的量级很可能达不到 Netfix 这样的级别,所以对于 Java 技术栈的团队,使用 Zuul 也是一个不错的选择;
Tyk是一种 Go 语言实现的轻量级 API 网关,有着丰富的插件资源,对于 Go 语言栈的团队来说,也是一种不错的选择。
2.配置中心设计
开源配置中心:携程开源的 Apollo、百度开源的 Disconf、360 开源的 QConf、Spring Cloud 的组件 Spring Cloud Config
2.1 配置中心存储设计
Disconf、Apollo 使用的是 MySQL;QConf 使用的是 ZooKeeper, 微博的配置中心使用 Redis 来存储信息,而美图用的是 Etcd
配置存储是分级的,有公共配置,有个性的配置,一般个性配置会覆盖公共配置,这样可以减少存储配置项的数量
支持存储全局配置、机房配置和节点配置。其中,节点配置优先级高于机房配置,机房配置优先级高于全局配置。也就是说,我们会优先读取节点的配置,如果 节点配置不存在,再读取机房配置,最后读取全局配置
/confs/global/{env}/{project}/{service}/{version}/{module}/{key} //全局配置
/confs/regions/{env}/{project}/{service}/{version}/{region}/{module}/{key} //机房配置
/confs/nodes/{env}/{project}/{service}/{version}/{region}/{node}/{module}/{key} //节点配置
2.2 变更推送如何实现
1. 应用程序向配置中心客户端注册一个监听器,配置中心的客户端,定期地(比如 1 分钟)查询所需要的配置是否有变化,如果有变化则通知触发监听器,让应 用程序得到变更通知
2. 给配置中心的每一个配置项多存储一个根据配置项计算出来的 MD5 值, 配置项一旦变化,这个 MD5 值也会随之改变。配置中心客户端在获取到配置的同时, 也会获取到配置的 MD5 值,并且存储起来
3. 轮询查询的时候,需要先确认存储的 MD5 值和配置中心的 MD5 是不是一致的。如果不一致,这就说明配置中心里存储的配置项有变化,然后才会从配置中心 拉取最新的配置
2.3 如何保证配置中心高可用
让配置中心“旁路化”: 在配置中心的客户端上,增加两级缓存:第一级缓存是内存的缓存;另外一级缓存是文件的缓存
配置中心客户端在获取到配置信息后,会同时把配置信息同步地写入到内存缓存,并且异步地写入到文件缓存中
内存缓存的作用是降低客户端和配置中心的交互频率,提升配置获取的性能;
文件的缓存的作用就是灾备,当应用程序重启时,一旦配置中心发生故障,那么应用程序就会优先使用文件中的配置
3.服务治理方案
3.1 由于依赖的资源或者服务不可用,最终导致整体服务宕机( 熔断)
雪崩”: 局部故障最终导致全局故障
断路器模式,在这种模式下,服务调用方为每一个调用的服务维护一个有限状态机,在这个状态机中会有三种状态:关闭(调用远程服务)、半打开(尝试调用远程服 务)和打开(返回错误)
当调用失败的次数累积到一定的阈值时,熔断状态从关闭态切换到打开态。一般在实现时,如果调用成功一次,就会重置调用失败次数。
当熔断处于打开状态时,我们会启动一个超时计时器,当计时器超时后,状态切换到半打开态。你也可以通过设置一个定时器,定期地探测服务是否恢复
在熔断处于半打开状态时,请求可以达到后端服务,如果累计一定的成功次数后,状态切换到关闭态;如果出现调用失败的情况,则切换到打开态
redis熔断器:
在系统初始化的时候,我们定义了一个定时器,当熔断器处于 Open 状态时,定期地检测 Redis 组件是否可用
new Timer("RedisPort-Recover", true).scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
if (breaker.isOpen()) {
Jedis jedis = null;
try {
jedis = connPool.getResource();
jedis.ping(); //验证redis是否可用
successCount.set(0); //重置连续成功的计数
breaker.setHalfOpen(); //设置为半打开态
} catch (Exception ignored) {
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
}, 0, recoverInterval); //初始化定时器定期检测redis是否可用
Redis 客户端操作 Redis 中的数据时,我们会在其中加入熔断器的逻辑:
if (breaker.isOpen()) {
return null; // 断路器打开则直接返回空值
}
K value = null;
Jedis jedis = null;
try {
jedis = connPool.getResource();
value = callback.call(jedis);
if(breaker.isHalfOpen()) { //如果是半打开状态
if(successCount.incrementAndGet() >= SUCCESS_THRESHOLD) {//成功次数超过阈值
failCount.set(0); //清空失败数
breaker.setClose(); //设置为关闭态
}
}
return value;
} catch (JedisException je) {
if(breaker.isClose()){ //如果是关闭态
if(failCount.incrementAndGet() >= FAILS_THRESHOLD){ //失败次数超过阈值
breaker.setOpen(); //设置为打开态
}
} else if(breaker.isHalfOpen()) { //如果是半打开态
breaker.setOpen(); //直接设置为打开态
}
throw je;
} finally {
if (jedis != null) {
jedis.close();
}
}
3.2 当有超过系统承载能力的流量到来时,系统不堪重负,从而出现拒绝服务的情况(降级)
开关降级:代码中预先埋设一些“开关”,用来控制服务调用的返回值。比方说,开关关闭的时候正常调用远程服务,开关打开时则执行降级的策略。这些开关的值 可以存 储在配置中心中,当系统出现问题需要降级时,只需要通过配置中心动态更改开关的值,就可以实现不重启服务快速地降级远程服务了
实现策略主要有返回降级数据、降频和异步三种方案
首先要区分哪些是核心服务,哪些是非核心服务, 只能针对非核心服务来做降级处理,针对具体的业务,制定不同的降级策略:
降级数据:针对读取数据的场景,我们一般采用的策略是直接返回降级数据。比如,如果数据库的压力比较大,我们在降级的时候,可以考虑只读取缓存的数 据,而不再读 取数据库中的数据;如果非核心接口出现问题,可以直接返回服务繁忙或者返回固定的降级数据
降频: 对于一些轮询查询数据的场景,比如每隔 30 秒轮询获取未读数,可以降低获取数据的频率(将获取频率下降到 10 分钟一次)
异步 : 而对于写数据的场景,一般会考虑把同步写转换成异步写,这样可以牺牲一些数据一致性和实效性来保证系统的可用性
3.3 系统的峰值流量会超过了预估的峰值,对于核心服务也产生了比较大的影响(限流)
限流是一种常见的服务保护策略,你可以在整体服务、单个服务、单个接口、单个 IP 或者单个用户等多个维度进行流量的控制;
基于时间窗口维度的算法有固定窗口算法和滑动窗口算法,两者虽然能一定程度上实现限流的目的,但是都无法让流量变得更平滑;
令牌桶算法和漏桶算法则能够塑形流量,让流量更加平滑,但是令牌桶算法能够应对一定的突发流量,所以在实际项目中应用更多。
限流指的是通过限制到达系统的并发请求数量,保证系统能够正常响应部分用户请求,而对于超过限制的流量,则只能通过拒绝服务的方式保证整体系统的可用性
对系统每分钟处理多少请求做限制
针对单个接口设置每分钟请求流量的限制
限制单个 IP、用户 ID 或者设备 ID 在一段时间内发送请求的数量
对于服务于多个第三方应用的开放平台来说,每一个第三方应用对于平台方来说都有一个唯一的 appkey 来标识,可以限制单个 appkey 的访问接口的速率
固定窗口与算法: 首先要启动一个定时器定期重置计数,比如限制每秒钟访问次数。
private AtomicInteger counter;
ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
timer.scheduleAtFixedRate(new Runnable(){
@Override
public void run() {
counter.set(0);
}
}, 0, 1, TimeUnit.SECONDS);
public boolena isRateLimit() {
return counter.incrementAndGet() >= allowedLimit;
}
缺陷: 无法限制短时间之内的集中流量
滑动窗口的算法: 将时间的窗口划分为多个小窗口,每个小窗口中都有单独的请求计数;
将 1s 的时间窗口划分为 5 份,每一份就是 200ms;那么当在 1s 和 1.2s 之间来了一次新的请求时,我们就需要统计之前的一秒钟内的请求量,也就是 0.2s~ 1.2s 这个区间的总请求量,如果请求量超过了限流阈值那么就执行限流策略
漏桶算法与令牌筒算法漏桶算法:
漏桶算法的原理很简单,它就像在流量产生端和接收端之间增加一个漏桶,流量会进入和暂存到漏桶里面,而漏桶的出口处会按照一个固定的速率将流量漏出到接 收端(也就是服务接口)。 如果流入的流量在某一段时间内大增,超过了漏桶的承受极限,那么多余的流量就会触发限流策略,被拒绝服务
令牌桶算法的基本算法:
如果我们需要在一秒内限制访问次数为 N 次,那么就每隔 1/N 的时间,往桶内放入一个令牌;
在处理请求之前先要从桶中获得一个令牌,如果桶中已经没有了令牌,那么就需要等待新的令牌或者直接拒绝服务;
桶中的令牌总数也要有一个限制,如果超过了限制就不能向桶中再增加新的令牌了。这样可以限制令牌的总数,一定程度上可以避免瞬时流量高峰的问题
漏桶算法在面对突发流量的时候,采用的解决方式是缓存在漏桶中, 这样流量的响应时间就会增长,这就与互联网业务低延迟的要求不符;
而令牌桶算法可以在令牌中暂存一定量的令牌,能够应对一定的突发流量, Guava 中的限流方案就是使用令牌桶算法来实现的(优先)
4.ServiceMesh ( 让服务治理的策略在多语言之间复用)
1.什么是ServiceMesh
Service Mesh 主要处理服务之间的通信,它的主要实现形式就是在应用程序同主机上部署一个代理程序
把业务代码和服务治理的策略隔离开,将服务治理策略下沉,让它成为独立的基础模块
sidecar 的植入方式目前主要有两种实现方式,一种是使用 iptables 实现流量的劫持;另一种是通过轻量级客户端来实现流量转发
服务之间的通信也从之前的客户端和服务端直连,变成了下面这种形式
2. Istio
它将组件分为数据平面和控制平面,数据平面就是我提到的 Sidecar(Istio 使用Envoy作为 Sidecar 的实现)。控制平面主要负责服务治理策略的执行,在 Istio 中,主要分为 Mixer、Pilot 和 Istio-auth 三部分
四.多机房部署规划
1. 跨机房的数据传输
1.1 同地双机房之间的专线延迟一般在 1ms~3ms。
1. 2 国内异地双机房之间的专线延迟会在 50ms 之内
1. 3 如果业务是国际化的服务,需要部署跨国的双机房。 依据各大云厂商的数据来看,从国内想要访问部署在美国西海岸的服务,这个延迟会在 100ms~200ms 左右
2. 同城双活
核心思想是尽量避免跨机房的调用。同城多机房方案可以允许有跨机房数据写入的发生,但是数据的读取和服务的调用应该尽量保证在同一个机房中
首先,数据库的主库可以部署在一个机房中,比如部署在 A 机房中,那么 A 和 B 机房数据都会被写入到 A 机房中。然后,在 A、B 两个机房中各部署一个从库,通过 主从复制的方式,从主库中同步数据,这样双机房的查询请求可以查询本机房的从库。一旦 A 机房发生故障,可以通过主从切换的方式将 B 机房的从库提升为主 库,达到容灾的目的
其次,不同机房的 RPC 服务会向注册中心注册不同的服务组,而不同机房的 RPC 客户端,也就是 Web 服务,只订阅同机房的 RPC 服务组,这样就可以实现 RPC 调 用尽量发生在本机房内,避免跨机房的 RPC 调用
3. 异地多活
避免跨机房同步的数据写入和读取,而是采取异步的方式,将数据从一个机房同步到另一个机房
在数据写入时,要保证只写本机房的数据存储服务再采取数据同步的方案,将数据同步到异地机房中
数据同步的方案有两种:
基于存储系统的主从复制,比如 MySQL 和 Redis。也就是在一个机房部署主库,在异地机房部署从库,两者同步主从复制实现数据的同步。
基于消息队列的方式。一个机房产生写入请求后,会写一条消息到消息队列,另一个机房的应用消费这条消息后再执行业务处理逻辑,写入到存储服务中
一般两种结合用:基于消息的方式,同步缓存的数据、HBase 数据等。然后基于存储,主从复制同步 MySQL、Redis 等数据
评论