《亿级流量网站架构核心技术》读书笔记

豆瓣链接

实验

  1. nginx的tcp负载均衡
  2. consul+consul-template
  3. consul实现配置中心
  • 一个系统不是一下子就能设计完美的
  • 在有限的资源下,优先解决最核心问题

一、原则

  1. 高并发
    1. 无状态
    2. 拆分
    3. 服务化
    4. 消息队列(异步,)
      5. 大流量缓存(先入redis,再同步到db)
      6. 数据校对
    5. 数据异构(类似数据冗余)
      8. 数据异构(类似数据冗余,来提升读取效率,例如分表)
      9. 数据闭环,对于需要多次查询数据的接口,可以把所有数据缓存一次,提升读的速度,然后各个数据修改后,都来改这个缓存
    6. 缓存,
      11. 浏览器缓存
      12. app客户端缓存
      13. cdn缓存
      14. 接入层缓存(Nginx,缓存整个接口)
      15. 应用层缓存
      16. java的线程共享
      17. redis缓存
      18. 分布式缓存
      19. redis缓存
      20. 缓存有多个层级,例如
      21. 接入层(Nginx)
      22. Redis
      23. java的缓存(Python没有)
      24. 回源(数据库,或者调API)
    7. 并发化(如果需要多个IO,可以使用并发获取,而不是串行)
  2. 高可用
    10. 降级(提供有损服务)
    11. 集中开关管理(也就是一个统一的配置后台,设置降级后,所有服务都能识别)
    12. 可降级的多级读服务(对于上面的缓存层级,可以设置最终会去到哪一层。例如Redis压力很大,就可以设置接入层获取不到缓存,就直接返回了,而不继续往下找)
    13. 这这里,返回什么也是个问题,可以返回空列表(会不会被吐槽?),如果是状态,可以返回个默认值,例如有货
    13. 业务降级
    14. 屏蔽次要功能,例如双十一,淘宝查历史账单,只会查近1个月的
    15. 次要流程改为异步,例如微信抢红包,抢到红包是重要功能,红包金额的入账就是次要功能
    16. 限流(限制恶意流量,防止流量超过峰值)
    17. 主动拒绝,返回友好点的文案
    17. 切流量(机器挂了,切流量到其他正常的机器)
    18. 可回滚(代码版本可回滚)

业务设计原则

  1. 幂等
  2. 流程可定义(也就是有流水表)
  3. 状态修改(使用CAS)
  4. 管理后台审计
  5. 文档和注释
  6. 备份(代码和人员)

二、负载均衡和反向代理

接入层、反向代理、负载均衡,一般都是指Nginx

  • 负载均衡(Nginx的upstream)
    • 算法
      • 轮询(weight来指定权重)
      • ip hash 同一个ip去同一台机
      • 其他hash
    • 失败重试
      • 在fail_timeout时间内如果失败max_fails次,认为不可用,在fail_timeout后,重新检测
    • 健康检查
      • 默认是惰性的(应该是请求来才去检测的意思)
      • nginx_upstream_check_module(插件,每n秒请求上游服务,返回2xx或者3xx,表示存活,所以上游服务要做好对接)
  • 反向代理
    • 缓存(存放在tmpfs)

动态负载均衡

如果修改upstream,比较麻烦,需要重启nginx。动态负载均衡就是可以自动发现上游服务器,然后通过管理后台,快速注册或者摘取上游服务器

方案一:consul+consul-template

流程

  • 上游服务向consul_server注册节点
  • 管理后台向consul_server注册或者摘除节点
  • consul_template长轮询监听consul_server的配置变化
  • 有变化后,生成nginx配置,修改nginx配置,重启nginx

好处是节点的注册和摘除,可以在管理后台完成,不需要手动操作nginx配置和重启。
缺点是上游服务需要有consul功能,不知道python有没有,java有

方案二:consul+openResty

流程

  • 上游服务向consul_server注册节点
  • 管理后台向consul_server注册或者摘除节点
  • nginx启动后调用init_by_lua,想consul_server获取配置
  • 然后nginx定期去consul_server拉取配置,然后reload

缺点是只能定期,不能长连接,所以有延迟,解决方法是nginx暴露一个http api,开发一个agent,长轮询监听consul_server的配置变化,然后调用http api实时更新nginx的配置

感觉上面两个方案都有点蛋疼。。。。如果有msalt,可以自己做个管理后台,修改配置后,发送msalt任务,msalt自己修改nginx配置,然后reload

四层负载均衡

上面的都是http的负载均衡,那tcp连接,就要用四层负载均衡(7层网络模型,tcp在第4层)

三、隔离

隔离是发生故障后,将故障服务和正常服务隔离,避免故障服务影响正常服务,造成滚雪球。

  1. 线程隔离
    2. 系统有两个线程池,将核心业务请求导向线程池A,非核心的导向线程池B
    3. 这样非核心业务的故障不会影响核心业务
  2. 进程隔离
    5. 跟线程类似
  3. 集群隔离
    7. 对于一些压力比较大的业务,例如秒杀,用一个单独的集群来实现,避免影响到其他业务
  4. 机房隔离
    9. 当一个机房发生故障,把流量切到另一个机房,实现高可用
  5. 读写隔离
    11. 例如redis或者mysql,读请求走一个集群,写请求走另一个集群
  6. 动静隔离
    13. 动是动态资源,静是静态资源。静态资源尽量放CDN
  7. 爬虫隔离
    15. 识别爬虫请求,导到单独的集群,避免影响正常请求
  8. 热点隔离
    17. 例如秒杀,可以用单独集群来实现
  9. 资源隔离
    19.资源是指硬件,例如CPU,磁盘,内存。 重要进程,单独分配CPU资源,保证重要进程可用

Hystrix和Servlet3

都是java的组件,
隔离的思路是

  • 一台机有多个线程池
  • 业务区分核心业务和非核心业务,分流到不同的线程池
  • 达到非核心业务过载或者一次,不会影响核心业务

四、限流

  • 限流算法

    • 令牌桶法
      • 进程A往桶里塞令牌,例如速度是1s10个令牌,桶的容量有个上限,假如是100,溢出就丢弃
      • 请求来了,从桶里获取令牌(可以根据请求的不同设置不同的令牌数,例如耗时的业务需要2个令牌,简单的业务只需要1个)
      • 能获取足够的令牌,就处理请求
      • 不能,就丢弃请求,或者等待
    • 漏桶算法(有错误,需要重新整理)
      • 进程A以速率A流入水滴到桶里
      • 如果溢出,就丢弃
      • 按照常量速率流出水滴
    • 感觉两个算法都很类似,都是类似生产消费的模式,来控制消费的速率
    • 令牌法允许突发流量,例如1s内把所有令牌都获取完
    • 漏桶法不允许特发流量,而且配置的流出速率不能小于流入,不然就没什么意义了
  • 应用级限流(也就是在单台机上面限流)

    • 限流总并发/连接/请求数 (例如某个时刻,如果并发数超过阈值,就丢弃请求)
    • 限制总资源数(资源一般指数据库连接等)
    • 限制单个接口的总并发数/请求数(这个粒度就小一点)
    • 限制窗口时间的并发数,例如1s内,并发数不能大于N
    • 实现
      • 组件的限流功能,例如gevent的最大连接数
      • 自己用redis实现
        • 总并发数。key是接口名+机器ip,通过incr方法,如果小于N,执行,大于N丢弃,最后执行incr -1),然后设置超时时间
        • 窗口时间内总并发数。key是接口名+机器ip,通过incr方法,如果小于N,执行,大于N丢弃,然后设置超时时间(例如1s),这里要考虑设置超时时间失败,导致永远释放不了的问题,所以key最好带上时间,
      • 可以写成一个装饰器
  • 分布式限流(相对于单机器,主要的难点是原子性)

    • 上面的redis也可以实现分布式限流
    • redis+lua 利用redis的incr实现原子性
    • Nginx+lua 利用lua的锁来实现原子性(底层应该是信号量)
  • 接入层限流(Nginx的限流)

    • ngx_http_limit_conn_module
      http {
      limit_conn_zone $binary_remote_addr zone=addr:10m; # 定义限流模块addr 以ip地址作为key;10m表示使用10m内存来进行ip传输;除了binary_remote_addr 表示ip外,server_name表示域名
      limit_conn_log_level error; #限流日志,触发限流会打error日志
      limit_conn_status 503; #限流时返回的http code
      server {
      location /limit{
      limit_conn addr 1; #定义addr模块限流1
      }
      }
      }
	* ngx_http_limit_req_module(使用漏桶算法来实现)[官方文档](http://nginx.org/en/docs/http/ngx_http_limit_req_module.html)
		* ```
		limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s
		location /limit{
			limit_req zone=one burst=5 nodelay;
		}
		```
		* 其他配置和limit_conn一样
		* 这里有三个参数:
			* 1r/s  表示每秒处理2个请求
			* burst=5 表示桶的容量=5
			* nodelay 非延迟,不加这个配置就是默认延迟
		* 情况:
			* 情况1 burst=0 
				* 2r/s 表示500毫秒内,只能处理一个请求,多出的请求返回503。时间窗口是500毫秒。(时间窗口从什么时候开始,这个是nginx决定的,而且不是非常准确,但是这个影响不大)。例如时间窗口是05.000秒到05.500秒。如果在05.300处理了一个请求,下一个请求要到05.500秒,300-500内的请求会被拒绝
			* 情况2 burst大于0,例如1,
				* 2r/s 表示500毫秒内,只能处理一个请求,超出的请求会放进桶里,最多1个请求。超出桶容量的请求依然返回503。到下一个时间窗口,nginx会从桶里取一个请求出来处理.所以假设3个请求在同一个时间窗口内到来,第一个会被正常处理,第二个会等待500毫秒再处理,第三个返回503
			* 情况2 burst大于0,例如1,nodelay
				* 和情况2一样,不同的是,放进桶里的请求会被立刻处理,而不是等下一个时间窗口。假设3个请求在同一个时间窗口内到来,第一个会被正常处理,第二个会放进桶里,但是是立刻处理,不会等待,第三个返回503

	* 更高级的限流
		* 如果要更加复杂,更加了灵活的限流策略,就要用OpenResty,也就是用lua语言,写限流策略,然后在Nginx上面执行
		
* 节流
	* 节流是指在特定的时间窗口,对相同的请求,只处理一次。或者限制多个时间的执行间隔
	* 应用:
		* 例如防止用户一直刷新页面
	* 策略
		*  throttleFirst,对于相同的请求,只处理第一个
		*  throttleLast,对于相同的请求,只处理最后一个
		*  throttleWithTimeout,限制两个请求的间隔不能小于某个时间
	*  实现:
		*  RxJava


## 五、降级

###1.降级的分类
* 从服务端链路考虑,可以进行降级的地方有:
	* 页面降级
	* 页面片段降级。例如商品详情页,不重要的信息不请求,例如商家信息
	* 读降级
	* 写降级
	* 爬虫降级
	* 风控降级。识别用户是否机器人,如果是,进行降级
* 从读和写服务分类
	* 读降级(包括上面的读降级,页面降级,页面片段降级等)
		* 降级方案:
			1. 返回缓存 。返回缓存里的数据,或者返回上一次成功读取的数据。这些数据不会很实时,所以适用于对数据一致性要求不高的场景。
			2. 返回默认值。例如读取库存的接口,默认返回没货。例如列表接口,默认返回空列表
			3. 返回兜底数据,例如列表接口返回静态的几个item,这些item一般写在代码里面或者静态文件里面
	* 写降级
		* 降级方案
			1. 异步。
				2. **先写redis,异步写DB**。适用于需要判断状态的操作。例如下单,需要在redis判断是否有缓存,如果有,写入Redis,返回下单成功,如果没有,返回失败。如果成功,异步同步Redis到DB。
				3. **异步执行全部操作**。适用于不需要判断状态的操作。例如写评论,直接返回写入成功。异步执行评论逻辑
				

###2.降级的触发

触发分为**降级**(打开降级开关)和降级后的**恢复**(关闭降级开噶)

* 人工触发。当开发人员通过监控或者告警,意识到系统需要降级时,手工打开降级开关,进行降级。当系统恢复时,手工关闭开关进行恢复
* 自动降级。当系统通过某些指标,判断系统需要降级时,自动打开降级开关。当系统判断系统负载降低后,自动恢复
	* 降级指标有:
		1. 超时。当系统执行某个操作超时时,自动降级
		2. 失败次数。当系统执行某个操作失败次数超过N时,自动降级
		3. 故障。当系统执行某个操作失败时,自动降级
		4. 限流。当某个服务触发限流时,自动降级
	* 恢复
		* 时间窗口重试。当降级后,每个时间窗口执行一次降级前的操作,如果成功,关闭降级开关。例如每1s执行一次操作。

###3.降级的配置
降级开关,其实也就是一个配置,这个配置的实现可以:
* 代码变量。也就是写死在代码里面,修改配置需要修改代码,然后重启服务。
* 配置中心。通过页面就可以修改配置,修改后可以同步到所有机器
	* 开源方案:Zookeeper,Consul等
	* 实现方案,配置中心的难点是修改配置后,怎么同步到多台机器的多个进程里面(一个进程里面的多个线程或者协程,会共享一份配置)
		1. 定时更新。例如进程里面每隔1s或者每100次读取配置,就主动去配置中心更新最新的配置
		2. 监听。当配置修改,通过IO多路复用机制通知进程去更新。类似于消息队列,配置中心是生产者,进程是消费者。
		

###4.降级的实现

书中介绍了使用Hystrix来实现降级

具体做法是
1. 定义run和getFallback两个函数,这个是不同业务不一样的
2. 当请求尽量,先执行run函数,如果成功,就返回
3. 如果失败或者超时,进行降级,返回getFallback函数的内容


自己项目的做法
* 对于每一个接口,定义两个函数,一个是未降级的逻辑run,另一个是已降级的逻辑getFallback。
* 封装一层降级逻辑:
	* 如果降级开关关闭,执行run
	* 如果降级开关打开,执行getFallback
	* 如果run超时或者失败,决定是否自动降级
	* 降级后 每个时间窗口执行一次run,如果成功,决定是否恢复
	* getFallback可以默认不定义


## 六、重试

### 代理层
代理层主要就是Nginx了

Nginx有有很多超时,或者重试的配置
下面的time是时间,例如可以是`5s`
* 客户端超时配置
	* client_header_timeout time; nginx接收客户端请求头的超时时间
	* client_body_timeout time nginx接收客户端请求体的超时时间
	* send_timeout time; 发送响应到客户端的超时时间
	* keepalive_timeout timeout [header_timeout] 长连接的超时时间,header_timeout是返回给客户端的,例如如果设置了,返回的响应头就会有:`Keep-Alive:timeout=10`
		* 默认http1.1是打开长连接的,1.0不会,可以通过wireshark来看看是否真的没有3次握手
		* keepalive_requests 100  表示长连接可以处理100次请求

* 代理超时设置(代理就是上游的服务)
	* 连接超时:
		* proxy_connect_time time;  建立连接超时时间
		* proxy_read_timeout time ;从上游服务读取响应的超时时间。注意不是读取的超时时间,是发送请求后,到可以读取的超时时间
		* proxy_send_time time;建立连接后,发送请求,到上游开始接受请求的超时时间,注意不是开始接收请求到接收完请求的时间。
	* 失败重试机制
		* proxy_next_upstream ;这个配置可以是多个下面的选项
			* timeout 超时,包括建立连接,写请求,读响应头的超时
			* invalid_header 上游服务返回错误响应头
			* http_xxx,例如http_500,表示上游返回指定的httpcode
			* non_idempotent  非幂等请求(idempotent 是幂等的意思)。POST、LOCK、PATCH都是非幂等的请求。默认幂等的请求都是允许重试的。
			* off 关闭重试
		* proxy_next_upstream_tries number;失败重试次数,包含第一次请求,也就是1表示不重试。0表示不限制。
		* proxy_next_upstream_timeout time;在此时间内执行重试,超过后就不重试了。0表示不限制


### 应用层
* 超时
	* 设置好超时时间
		* 例如A调用B调用C,超时时间一定是A>B,不然会导致一直重试
		* 超时时间不能大于用户的容忍时间,不然用户自己会不断重试
		* 
	* 超时后的策略是重试或者降级
* 重试
	* 超时后一般的策略是重试
	* 非幂等的操作不能重试,最好设置所有操作都是幂等。
	* 

### 应用层上游
例如redis,mysql的连接都要设置好超时时间




## 七、回滚

* 事务回滚
	* 如果是单机数据库,执行rollback回滚就可以了
	* 如果是分布式事务
		* 补偿机制
			* 回滚事务。例如扣优惠券成功了,但是下单事务失败了,就把扣优惠券的事务回滚。这里有有个问题就是万一回滚前,进程挂了,就不能回滚了,所以要有个定时扫描机制,把未回滚的事务进行回滚
			* 重试。通过定时扫描机制,把失败的事务进行重试。例如上面的下单事务
		* TCC事务。每个事务分3步,Try-Confirm-Cancel。
			1. 扣优惠券,下单,都执行Try。例如优惠券的Try就是把要减的优惠券冻结
			2. Confirm。等Try都执行成功,执行Confirm,例如优惠券就是把冻结的优惠券扣掉
			3. Cancel。如果其中一个Try失败,就执行Cancel,也就是回滚,例如把冻结的优惠券恢复
* 代码库回滚,这些Git和Svn都很成熟了
* 部署版本回滚。例如上线了新版本,但是有问题,需要回滚到旧版本
	* 部署前备份旧版本,做到可以快速回滚
	* 灰度。
	 
##八、压测
系统雪崩:整个系统全部不可用
雪崩效应:由于一个小问题,导致整个系统不可用。例如整个系统都依赖于一个非核心业务,但是没有做好降级,导致一旦这个业务不可用,导致所有核心业务都不可用。

###压测
* 系统压测,用来评估系统的稳定性和性能。常用指标有:
	* QPS/TPS  T是事务
	* 响应时间,也就是时延
	* 机器负载
* 压测方式:
	* 线下压测。也就是在非线上环境测试,例如测试环境。优点是不用考虑正常用户,缺点的压测结果不够真实
	* 线上压测。在线上环境测试,这时要注意不要影响正常用户,包括请求和数据。
	* 仿真压测。模仿真实环境的访问情况(可以通过access日志来达到)。可以对访问量翻倍的方式增加压力
	* 隔离集群压测,从线上集群中摘除一台机器,用来压测
	* 导流压测,把集群的所有流量导到一台机,风险比较大
	* 单机压测,只在一台机上面压测,得到单机的并发能力
	* 离散压测,也就是不要只访问热点数据,因为热点数据一般有缓存


###系统优化和容灾

压测后,就知道系统的性能,就能根据预期负载来决定是否需要优化性能或者增加机器


###应急预案

当上面两步都做了,项目上线后,还是会有一些突发情况,对于这些突发情况,需要做好预案。
一个预案需要有
* 预案名称
* 问题描述(也就是遇到了什么突发情况)
* 执行操作(遇到突发情况怎么处理)
* 相关人员


例如 

| 预案名称|     问题描述 |   执行操作|   相关人员|
| :-------- | --------:| :------: | :------: |
|机房故障   |   机房网络不可用|  DNS配置中摘掉该机房的IP|  小A|


#高并发

## 九、缓存、HTTP缓存、多级缓存
主要讲java的进程内缓存。但是现在基本都用redis缓存了,感觉需要用进程内缓存的场景不多了。
浏览器中Ctrl+F5可以强制刷新缓存

使用缓存能大幅提升系统的QPS,但是要注意下面几点:
* 缓存命中率,缓存设置了,但是大部分请求还是去DB了
* 缓存一致性,DB改了,缓存还是旧数据
* 缓存雪崩,多个缓存KEY一起过期,导致请求都去DB了
* 缓存穿透,缓存设置了,但是永远用不了
* 缓存更新。
	* 过期更新。缓存设置过期时间,如果过期,就回源。这里有个坑是如果并发较大,而且回源的速度很慢,会导致多个请求同时回源,弄挂回源的服务。所以碎玉回源速度慢的业务,适用下面的定时更新
	*  定时任务更新。设置定时任务,定时回源,更新缓存。

多级缓存有(从用户端到后端),缓存离用户越近越好:
* 浏览器缓存
* CDN
* 接入层缓存(Nginx+Lua+Redis)
* 应用层缓存,基本是Redis

## 十二、连接池,线程池
池化技术用于建设一些消耗,来提升性能。例如避免TCP连接和端口,避免线程创建和消耗

一般有指标:
* 最小数量,当系统空闲时,最小维护的连接数量。数量太大会导致资源占用较多,太小会起不到连接池的作用。一般这个数量乘以进程数,就是总的连接数。
* 最大数量,当系统繁忙时,最大支持的连接数量,超过就等待,用来保护上游服务。

坑
* 上游服务主动关闭连接,例如Mysql一般8小时后会主动断开空闲连接。所以业务端获取连接池里面的连接后,需要进行reconnect操作。可以再获取连接对象就检查连接是否可用,也可以在需要传输数据,也就是执行命令时,检查是否可用。Redis是后者。
* 等待超时时间,不知道是什么意思,但是好像会遇到,也就是有大量TIMED_WAIT连接


##十三、异步
书上说的异步是指处理多个IO请求的并行问题,所以这个叫并行合适点。

解决方法是从串行改为并行,但是就算改为并行,还是需要阻塞一个线程,在java中,是有线程池的, 但是没有协程,所以还是会造成一个线程的浪费。


##十四、扩容

* 垂直扩容,例如换CPU,加内存,加硬盘等
* 水平扩容,加机器
* 应用拆分,把一个大系统拆分为多个小系统,也就是微服务化。带来的问题是分布式事务,join查询等。
* 分库分表
	* 分表
		* 当一个表太大,导致容量和磁盘/带宽IO瓶颈(不是很明白)
	* 分库
		* 当一台机的性能不够的时候,分为多个库。这里的分库是不同表分不分的库。
	* 分库分表
		* 分库分表是原来的一张表,拆分为多张子表,放在不同的库。
	* 带来的问题:
		* 查询问题,由于分表是按特定的一个字段(例如用户ID)分的,如果需用用其他维度来查询,例如商品ID,就需要
			* 用合并表,或者另一组分表
			* ES等搜索引擎
			* 上面两种方法相当于数据冗余,必然存在数据不一致请求,解决方法是1.消息队列,通知对应的业务方更新数据 2. 监听binlog日志
		* 分布式事务问题,这个上面有说
		

##十五、队列术

队列的作用:
* 异步处理,同步任务发送消息都队列,另一个进程从队列获取消息,处理异步任务。好处是:
	* 提升响应速度
	* 流量削峰
* 系统解耦,系统1触发一个事件,通过发消息队列的方式,通知多个其他系统。
* 数据同步。系统1修改了数据,通过发消息队列的方式,通知多个其他系统。

## 十六、案例
主要讲京东几个业务的实现架构
posted @ 2019-10-05 12:46  Xjng  阅读(1104)  评论(0编辑  收藏  举报