前几个月阅读了这本京东张开涛出的《亿级流量网站核心技术》,在此记录下一些细节。
几点阅读感悟如下:
优点:
- 作者的知识视野很开阔,各个章节的组织和结构都比较全面,看得出来还是费了心思的;
- 偏向工程实践,show you code~
- 通过二维码拓展阅读的方式挺新颖。
缺点:
- 涉及了过多的代码细节,所以整本书很像是若干篇技术博客的精校版;
- 全书整体来看,技术深度一般,更偏向工程实践(所以多抓鱼上0.1折收购
),定价99还是偏贵了,须知《深入理解计算机系统》这本 CS 神书也才112
整体评分:6.0。
1 高可用
1.1 隔离术
线程隔离:将请求分类,交给不同的线程池处理。一种业务请求发生问题时,不会讲故障扩散到其他线程池。
进程隔离:目前这种方案不太推荐,可以将服务进行拆分为多个子系统实现物理隔离。
集群隔离:如秒杀这种压力较大的服务,建议单独部署在集群里,可以通过服务分组实现。
机房隔离:多机房部署。
读写隔离:将读和写请求分到不同的集群,同时还可以搭配主从模式使用。
动静隔离:动态资源和静态资源分离,静态资源放在 CDN
上。
爬虫隔离:可以在负载均衡层面,将爬虫流量分组到独立的集群里(如何识别爬虫流量?)。
其他隔离:
- 资源隔离
- 环境隔离
- 压测隔离
- …
1.2 限流术
缓存可以提升系统访问速度和增大系统处理能力;降级是当业务出现问题时或者影响到核心流程的性能时需要暂时屏蔽掉。
缓存和降级无法解决的一些场景,如稀缺资源秒杀、频繁的复杂查询、写服务等,需要通过限流保证高并发。
限流算法:
- 令牌桶算法
- 漏桶算法
应用级限流
- 限流总并发/连接/请求数:设置 WEB 容器的总响应数;
- 限流总资源数:池化处理,线程池、数据库连接池;
- 限流某个接口的总并发数:
AtomicLong
或Semaphore
进行限流(Hystrix
的信号量限流用到了Semaphore
); - 限流某个接口的时间窗口请求数:
Guava CacheBuilder
; - 平滑限流某个接口的请求数:
Guava RateLimiter
(基于令牌桶)。
分布式限流
分布式限流的关键是要将限流服务做成原子化的。
解决方案:
Redis
+Lua
Nginx
+Lua
京东的抢购业务的限流方案就是采用的 Redis + Lua 方案实现的。
接入层限流
Nginx 自带有连接数限流模块 ngx_http_limit_conn_odule
和 请求数限流模块 ngx_http_limit_req_odule
。
1.3 降级特技
降级的目的是保证核心服务可用,即使是有损的。
降级分类:
- 是否自动化划分:自动开关降级/人工开关降级;
- 按照功能划分:读服务降级/写服务降级
自动开关降级
自动降级是根据系统负载、资源使用情况、SLA 等指标进行降级。
1.4 超时与重试机制
2 高并发
2.1 缓存
- 缓存算法:FIFO、LRU、LFU
- 缓存命中率:从缓存中读取次数/(总读取次数(从缓存中读取次数 + 从慢速设备上读取的次数))
2.1.1 应用级缓存
- 堆缓存:没有序列化和反序列化的限制,但是受限于堆空间;实现:
Guava Cache
,Ehcache3.x
等; - 堆外缓存:减少GC时间,同时支持更大的缓存空间。可以使用
Ehcache3.x
、MapDB
实现; - 磁盘缓存:优点是
JVM
重启后不会丢失; - 分布式缓存:解决多
JVM
示例时的数据一致性问题。
缓存使用的两种模式:
- 单机时:存储最热的数据到堆缓存,相对热的数据到堆外缓存,不热的数据到磁盘缓存;
- 集群时:存储最热的数据到堆缓存,相对热的数据到堆外缓存,全量数据到分布式缓存(异步写)。
2.1.2 HTTP 缓存
- 浏览器缓存
HttpClient
缓存;Nginx
代理缓存;
2.1.3 多级缓存
分级缓存架构:应用Nginx缓存、分布式缓存、tomcat 缓存。应用级缓存用以解决热点缓存问题,分布式缓存用来减少访问回源率,tomcat 缓存用于防止相关缓存失效/崩溃之后的冲击。
如何缓存?
- 过期不过期?
- 不过期缓存:写数据库,如果成功,则写缓存。这种场景存在事务成功,缓存写失败但无法回滚事务的情况。此外,不要把写缓存放在事务中,尤其是写分布式缓存,因为网络抖动可能导致写缓存响应时间很慢,造成写事务阻塞,可以考虑定期全量同步缓存。
- 过期缓存:读取缓存,如果不命中,则查询数据,然后异步写入缓存并设置过期时间。
- 维度化更新:将数据进行维度化并增量更新(只更新变的部分);
- 大 Value 缓存:警惕缓存中的大 Value,尤其是使用
Redis
的时候,如Memcached
。
分布式缓存的两种算法:
- 轮询:优点是到应用
Nginx
的请求更加均匀,使得每个服务器的负载基本均衡。缺点是随着应用Nginx
的数量增加,缓存的命中率会下降,比如原来10台服务器的缓存命中率是90%,再加10台将可能降到45%。 - 一致性哈希:有点是相同的请求会转发到同一台服务器,命中率不会随着增加服务器而降低。缺点是因为相同的请求会转发到同一台服务器,因此,可能会造成某台服务器负载过重。
实际使用时:
- 负载较低时:使用一致性哈希;
- 热点请求降级一致性哈希为轮询,或者使用带权重的一致性哈希;
- 将热点数据推送到接入层
Nginx
,直接响应给用户。
2.2 连接池
2.2.1 通用
MYSQL
驱动在创建每个连接时会创建一个Timer
(每个Timer
是一个Thread
),然后每个连接中创建的每个Statement
会提交一个TimerTask
(超时则每个Task
在执行时会创建并启动一个新的Task),举例:如果连接池中有500个连接,每个连接执行一个statement
,最坏情况会创建 1000个线程。- 在初始化
dataSource
时,需要配置 Bean 的destroy-method='close'
方法,因为在 JVM 关闭/重启时,如果没有配置destroy-method
,则会造成重启后旧的数据库连接池的连接不释放,如果重启此处太频繁,后续将无法建立连接。
2.2.2 数据库连接池
- 建议1:需要留意网络阻塞/不稳定时的级联效应,连接池内部应根据当前当前网络的状态,对于一定时间内的全部 timeout,即有快速熔断和快速失败机制;
- 建议2:等待超时时间尽可能小一些,即使返回错误,也不阻塞强。
DBCP
比较容易出问题的就是设置的超时时间太长,造成大量的TIMED_WAIT
和线程阻塞,而且像滚雪球,一旦出现问题很难立即恢复。
2.2.3 HTTP 连接池
以 HttpClient
为例:
- 在开启长连接时才是真正的连接池,如果是短连接,则只是作为一个信号量来限制总的请求数,连接并没有实现复用;
- JVM 在停止或重启时,记得关闭连接池释放连接;
HtppClient
是线程安全的,不要每次使用创建一个;- 使用连接池时,要尽快消费响应并释放连接到连接池,不要保持太久(如 S3 Client 消费请求中的文件流时,会出现这种情况)。
2.2.4 线程池
依据个人经验来看,还是自己实现线程池工厂比较靠谱,尽量避免通过 Executors
的工厂方法生成线程池。
此外,Docker 容器中使用 Runtime.getRuntime().availableProcessors()
获取到的是物理机核数,而不是容器实际使用的核数,这将对性能造成很大的影响。
2.5 队列术
队列功能:
- 异步处理:提升主流程相应速度;
- 系统解耦:适用于依赖最终一致性的场景;
- 数据同步:
MySQL
变更到Redis
、机房同步、主从同步等; - 流量削峰:缓存 + 队列 实现削峰;
- 拓展、缓冲…