2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > 《亿级流量网站架构核心技术》总结

《亿级流量网站架构核心技术》总结

时间:2019-03-09 07:58:32

相关推荐

《亿级流量网站架构核心技术》总结

nginx后端节点健康检查

主要有三种实现方式:

1. 本身自带的ngx_http_proxy_module模块和ngx_http_upstream_module模块,属于惰性检测。

ngx_http_proxy_module:proxy_connect_timeout(与后端服务器建立连接的超时时间)、proxy_read_timeout(从后端服务器读取响应的超时)和proxy_next_upstream(何种情况下一个失败的请求应该被发送到下一台后端服务器)

ngx_http_upstream_module:max_fails(尝试失败次数)、fail_timeout(多长时间内失败,并且在这个时间内不再请求)。

2. nginx_upstream_check_module模块(淘宝开发的模块),属于主动检测。可以间隔检测后台服务器的健康状态,并对服务器的状态进行标记。还提供了路径可以实时查看各服务器的健康状态。

3. 直接使用淘宝开源的Tengine,自带了主动检测健康状态的模块,主动检测。

tomcat处理请求的流程

Connector端口监听到请求,根据http协议解析该次请求。解析的http报文,经装饰模式转化为servlet api对应的HttpServletRequest与HttpServletReponse。经层层容器engine、host、context最终到过我们所写的业务servlet的service方法。业务方法service,处理相关的业务逻辑,写入相应的响应的至response,并返回tomat的容器组件。tomcat该处理线程关闭响应流Response并将响应内容返回客户端。tomcat该处理线程被释放,然后用于下次请求的处理。

servlet3.0的异步特性

优势:同步的servlet线程会阻塞到处理完成返回响应才释放;异步的servlet线程会将实际的业务处理转交给工作线程,然后马上释放servlet线程或放回池中,所以相对于同步servlet能够提升吞吐量。但性能是没有改善的。步骤:

ServletRequest.startAsync开启异步化,获取AsyncContext的实例。AsyncContext提供方法让ServletRequest和ServletResponse对象引用,随后释放servlet线程,但没有关闭相应response流,所以我们可以在业务代码中继续进行处理。随后将请求转交给业务线程池中的一个线程进行处理,处理完之后,调用AsyncContext .complete(),将响应写回引用的AsyncContext的响应流中,并关闭响应流,完成此次请求处理。注意:AsyncContext并不是真正的异步输出,而是同步输出,但是解放服务器端的线程使用。使用AsyncContext的时候,对于浏览器来说,他们是同步在等待输出的,但是对于服务器端来说,处理此请求的线程并没有卡在那里等待,则是把当前的处理转为线程池处理了。在SpringMVC中,controller通过返回一个Callable或者DefferredResult,此时servlet容器线程已经释放,SpringMVC是通过借助TaskExecutor线程池调用池中一个线程来对实际业务进行处理。

Guava框架

相对于Apache Commons Collection提供的集合工具类,Goolgle 的Guava提供了更强大的集合工具类,除了集合的操作,还包含文件、字符串等其他的操作工具类。

1. 对集合创建视图,设置只读操作。

2. 对集合中的数据进行转换、过滤等。

3. 对参数进行验证(非空、长度等)。

4. 对不同集合进行交集、差集、并集。

5. 文件的读取操作。

6. 对字符串的分割、合并等操作。

7. Multimap对map集合的高级操作等。

……………..

分布式系统设计幂等性

幂等性:是指一次和多次请求某一个资源对系统造成的影响是一致的。http请求中,get、put、delete都是幂等的,但post不是幂等。保证幂等性的方法:

去重表,每次接口调用之前都根据请求地址+参数生成一个唯一键,作为去重表的ID,然后保存。不报错则说明成功,报错则说明重复请求了。状态机幂等,比如订单状态的处理,一般情况下存在有限状态机,如果状态机已经处于下一个状态,这时候来了一个上一个状态变更的请求,理论上是不能够变更。保证了有限状态机的幂等。token机制,防止页面重复提交。一般是表单数据提交前先向服务器申请token,token放入缓存并有过期时间,然后提交数据到后台并校验携带过去的token是否一致,同时删除token。

protobuf二进制传输格式

定义:protobuf是Google开源的二进制传输格式。使用:按照一定的语法定义结构化的消息格式,然后送给命令行工具,工具会自动生成相关的类,并且可以很轻松的调用相关方法来序列化和反序列化。性能对比:protobuf序列化后的大小是json的十分之一、xml的二十分之一、是java自带二进制序列化的十分之一,所以性能很高。还有facebook开源的thrift,性能要更高。适合场景:

需要和其他系统进行交互,对消息大小很敏感。小数据的场合。大数据并不适合。

java堆外内存

java本地存储对象的几种方式:堆内存、堆外内存和磁盘。堆外内存可以通过-XX:MaxDirectMemorySize设置,不设置的话默认跟堆内存一样。来自于java.nio。相对于堆内存的优点:

避免了垃圾回收GC的工作。减少IO时的内存复制,不需要堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。创建(以Netty为例):Netty使用的堆外内存是Java NIO的DirectByteBuffer类。首先向Bits类申请额度,根据一个全局变量totalCapacity(记录堆外内存的总大小),判断是否有足够限额分配,批准的话调用sun.misc.Unsafe分配内存,返回内存基地址。额度不够则先进行GC后再分配,如果垃圾回收了100ms内存还不够,则抛OOM异常。回收:只有在进行full GC的时候才会回收对外内存,可以主动调用System.gc来触发,但有不确定性。所以一般主动从DirectByteBuffer中取出sun.misc.Cleaner,然后调用其clean()方法即可。适用场景:不适合存储很复杂的对象,一般简单的对象或者扁平化的结构比较适合。

缓存雪崩,缓存穿透,缓存并发,缓存预热,缓存算法

缓存雪崩:可能是因为数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。解决思路:

加锁计数(即限制并发的数量,可以用semphore)或者起一定数量的队列来避免缓存失效时大量请求并发到数据库。但这种方式会降低吞吐量。分析用户行为,然后失效时间均匀分布。或者在失效时间的基础上再加1~5分钟的随机数。如果是某台缓存服务器宕机,则考虑做主备。缓存穿透:指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库中查询。解决思路:

如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。设置一个过期时间或者当有值的时候将缓存中的值替换掉即可。可以给key设置一些格式规则,然后查询之前先过滤掉不符合规则的Key。缓存并发:如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,同时设置缓存的情况,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题。解决思路:

对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。缓存预热:目的就是在系统上线前,将数据加载到缓存中。解决思路:

数据量不大的话,在系统启动的时候直接加载。自己写个简单的缓存预热程序。缓存算法:

FIFO算法:First in First out,先进先出。原则:一个数据最先进入缓存中,则应该最早淘汰掉。也就是说,当缓存满的时候,应当把最先进入缓存的数据给淘汰掉。LFU算法:Least Frequently Used,最不经常使用算法。LRU算法:Least Recently Used,近期最少使用算法。LRU和LFU的区别。LFU算法是根据在一段时间里数据项被使用的次数选择出最少使用的数据项,即根据使用次数的差异来决定。而LRU是根据使用时间的差异来决定的。

redis的持久化机制

redis主要提供了两种持久化机制:RDB和AOF。RDB:默认开启,会按照配置的指定时间将内存中的数据快照到磁盘中,创建一个dump.rdb文件,redis启动时再回复到内存中。redis会单独创建fork()一个子进程,将数据写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出。需要注意的是,每次快照持久化都是将内存数据完整写入磁盘一次,并不是增量的只同步变更数据,所以如果数据量大的话,而且写操作频繁,必然会引起大量的磁盘I/O操作,会严重影响性能,并且最后一次持久化后的数据可能会丢失。AOF:以日志的形式记录每个写操作(读操作不记录),只需追加文件但不可以改写文件,redis启动时会根据日志从头到尾全部执行一遍以完成数据的恢复工作。包括flushDB也会执行。主要有两种方式触发:有写操作就写、每秒定时写(也会丢数据)。因为AOF采用追加的方式,所以文件会越来越大,针对这个问题,新增了重写机制,就是当日志文件大到一定程度的时候,会fork出一条新进程来遍历进程内存中的数据,每条记录对应一条set语句,写到临时文件中,然后再替换到旧的日志文件(类似rdb的操作方式)。默认触发是当aof文件大小是上次重写后大小的一倍且文件大于64M时触发。当两种方式同时开启时,数据恢复redis会优先选择AOF恢复。一般情况下,只要使用默认开启的RDB即可,因为相对于AOF,RDB便于进行数据库备份,并且恢复数据集的速度也要快很多。开启持久化缓存机制,对性能会有一定的影响,特别是当设置的内存满了的时候,更是下降到几百reqs/s。所以如果只是用来做缓存的话,可以关掉持久化。

线程池ExecutorService的关闭

当jvm关闭或重启的时候,要注意主动关闭线程池,要不然有可能会导致java进程一直残留在OS中。关闭流程:

shutdown,起到通知的作用,不能继续向线程池追加新的任务。awaitTermination,在指定的时间内所有的任务结束的时候,返回true,否则返回false,返回false意味着还有线程未完成。shutdownNow,向所有执行中的线程发出interrupted以终止线程的运行。

HttpClient4.3连接池

使用基本步骤:

创建连接管理器PoolingHttpClientConnectionManager。设置总并发连接数maxTotal和单路由连接数maxPerRoute。设置socket的配置。设置http connection的配置。设置request请求的配置。设置重试策略,可以重写。创建httpClient,设置上面步骤创建的管理器、请求相关配置、重试策略等。

根据不同的属性不同,此时会创建不同的Executor执行器。HttpClient使用了责任链模式,所有Executor都实现了ClientExecChain接口的execute()方法,每个Executor都持有下一个要执行的Executor的引用,这样就会形成一个Executor的执行链条,请求在这个链条上传递。创建一个get请求,并重新设置请求参数,覆盖默认。执行请求。释放连接回到连接池、关闭连接、关闭连接管理器。基本结构:

连接池的实体PoolEntry<HttpRoute, ManagedHttpConnection>

包含ManagedHttpClientConnection连接,其实就是一个httpClient连接,真正连接后会bind绑定一个socket,用于传输http报文。连接的route路由信息。连接存活时间相隔信息。LinkedList<PoolEntry> avaiable,存放可用的连接。使用完后所有可重用的连接回被放到available链表头部,之后再获取连接时优先从available链表头部迭代可用的连接。之所以使用LinkedList是利用了其队列的特性,即可以在队首和队尾分别插入、删除。入available链表时都是addFirst()放入头部,获取时都是从头部依次迭代可用的连接,这样可以获取到最新放入链表的连接,其离过期时间更远(这种策略可以尽量保证获取到的连接没有过期,而从队尾获取连接是可以做到在连接过期前尽量使用,但获取到过期连接的风险就大了),删除available链表中连接时是从队尾开始,即先删除最可能快要过期的连接。HashSet<PoolEntry> leased,存放被租用的连接。maxTotal限制的是外层httpConnPool中leased集合和available队列的总和的大小,leased和available的大小没有单独限制。LinkedList<PoolEntryFuture> pending,存放等待获取连接的线程的Future。当从池中获取连接时,如果available链表没有现成可用的连接,且当前路由或连接池已经达到了最大数量的限制,也不能创建连接了,此时不会阻塞整个连接池,而是将当前线程用于获取连接的Future放入pending链表的末尾,之后当前线程调用await(),释放持有的锁,并等待被唤醒。当有连接被release()释放回连接池时,会从pending链表头获取future,并唤醒其线程继续获取连接,做到了先进先出。注意事项

引起TCP连接过高的问题:因为连接池用完之后的close,并不会真的释放掉tcp而是放回池中,所以当不小心new了多个httpClient的话,并发量一高,就有可能导致tcp链接高居不下,要等到超时了才会自己释放。因为HttpClient在多线程下是线程安全的,在多线程的环境中应该只是用一个全局单例的HttpClient,并且使用MultiThreadHttpConnectionManager来管理Connection。要注意设置每个路由的最大连接数(路由指的是某个地址),默认值是2。引发经常抛连接超时异常的问题:有可能是某个路由的最大连接数设置得不对,还有就是读取数据的超时时间设置过大。源码解析参考:HttpClient 4.3连接池参数配置及源码解读

协程(纤程)Fiber

协程概念:一种用户态的轻量级线程,其实就是单线程,指定执行整个函数中到一部分然后就先出去执行别的,等条件满足时,协程下次更新帧到了再继续往下执行。优点是无需线程上下文切换的开销,充分开发了单CPU的能力,资源占用低,适合高并发I/O。缺点也很明显,就是没办法利用多CPU的优势。框架:Quasar,调度器使用ForkJoinPool来调度这些fiber。Fiber调度器FiberScheduler是一个高效的、work-stealing、多线程的调度器。场景:服务A平时需要调用其他服务,但其他服务在并发高的时候延迟很严重。

一开始可以用httpClient连接池+线程池来处理,但如果调用服务的时候延迟太高或者超时,则会导致服务A的吞吐量会特别差。原因主要是一般一个链接由一个线程来处理,是阻塞的,所以在线程池数有限的情况下,吞吐量肯定上不去。并且当所有线程都I/O阻塞的时候,会很浪费CPU资源,并且CPU会一直做无用的上下文切换。这时候可以考虑协程来替换。参考文章:/qq910894904/article/details/41699541,

/hj7jay/article/details/51980038,

/zdy0_/article/details/51323583

ForkJoinPool线程池

Fork/Join框架是Java7提供了的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。工作窃取(work-stealing)算法是Fork/Join框架最重要的特性。一般一个线程会对应一个任务队列,当处理较快的线程处理完自己的任务之后,就会窃取另外一个处理比较慢的线程对应的任务,这时候会存在两个线程同时处理一个队列的情况,所以任务队列一般使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。优点是充分利用线程进行并行计算,并减少了线程间的竞争。

NIO原理

由一个专门的线程来处理所有的IO事件,并负责分发。 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。异步事件驱动模型中,把会导致阻塞的操作转化为一个异步操作,主线程负责发起这个异步操作,并处理这个异步操作的结果。nginx就是使用的这种模型。线程通讯:线程之间通过wait、notify等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。