应用服务器性能优化
应用服务器是开发最复杂,变化最多的地方,优化手段主要有:缓存、集群、异步
- 分布式缓存
- 异步操作
- 使用集群
- 代码优化
一。分布式缓存
网站性能优化:优先考虑使用缓存优化性能;
缓存包括:浏览器缓存,应用服务器缓存,数据库缓存,文件缓存,页面缓存等等。
好处:
- 访问速度快,减少数据访问时间
- 数据无需重复计算即可直接使用,减少计算时间
1. 缓存的基本原理
知识点 1:缓存本质是一个内存 Hash 表,以 Key/Value 形式储存在内存 Hash 表中,复杂度为 O(1)
【如图】
知识点 2:缓存主要存放那些读写比很高,很少变化的数据;
网站访问二八定律,80% 访问落在 20% 数据上,将 20% 数据缓存起来,可以很好地改善系统性能,提高数据读取速度,降低存储访问压力
知识点 3:应用程序读取数据时,先到缓存中读取,如果读取不到或数据已失效,在访问数据库,并将数据写入缓存
【如图】
2. 合理使用缓存
不合理的使用回事系统的累赘甚至风险!
频繁修改的数据
如果频繁修改的数据,缓存就没有必要,反而浪费性能;
没有热点的访问
大部分数据访问并没有几种在小部分数据上,那么缓存就没有意义
数据不一致与脏读
缓存读取机制,导致数据更改,缓存要延迟更改(过了失效时间再去数据库拿),因为要容忍一定时间的数据不一致;
比如更改了商品介绍页,过段时间才能被买家看到;
缓存可用性
缓存雪崩:当缓存服务器崩溃时候,数据库因为完全不能承受如此大的压力而宕机,进而导致整个网站不可用,这种情况为雪崩;
发生这种情况,不能简单的重启缓存服务器和数据库服务器来恢复网站访问;
可以通过分布式缓存服务器集群,可以一定程度上改善缓存的可用性;
缓存不应该作为一个可靠的数据源来使用!
实际支出就要明确定位:哪些需要实现,哪些不需要提供;不能摇摆不定!
缓存预热
缓存是热点数据,热点数据可能是数据库筛选淘汰出来的,这个过程可能很久;缓存刚开始时候,没有任何数据,重建缓存对系统性能和数据库负载都不太好;
最好的办法就是缓存开启的时候就把热点数据加载好,这个预加载要缓存预热;
缓存穿透
不恰当的业务,或者高并发的恶意攻击请求某个不存在的数据,会穿透缓存服务器,直接落在数据库上;
简单的策略是:将不存在的数据也缓存起来(value 值为 null)
3. 分布式缓存架构
分布式缓存架构:部署再多个服务器组成的集群中;
分为两种方案:
- 需要更新同步的分布式缓存 (JBoss Cache)
- 不相互通信的分布式缓存 (Memcached)
需要更新同步的分布式缓存
通常将应用程序和缓存部署再同一台服务器上,应用程序可以快速从本地获取缓存数据;
缺点:受限于单一服务器的内存空间
4.Memcached
非常受欢迎;特点是:
- 简单的设计
- 优异的性能
- 互不通信的服务器集群
- 海量数据可伸缩
简单的通信协议
需要考虑两方面的要素;
- 通信协议
- TCP(常用)
- UDP
- HTTP
- 通信序列化协议
- json
- XML
丰富的客户端程序
通信协议简单,只要支持该协议的客户端都可以和 Memcached 服务器通信;几乎所有主流编程都有;
高性能的网络通信
服务器通信模块基于 Libevent,一个支持事件触发的网络通信程序库;
高效的内存管理
固定空间分配;slab_class
/slab
/chunk
【如图】
互不通信的服务器集群架构
集群内服务器互不通信,是的集群可以做到几乎无限制的线性扩展,这也是目前流行的许多大数据技术的基本架构特点;
二。异步操作
使用消息队列将调用异步化,可改善网站的扩展性和性能;
【如图】
核心:任何可以晚点做的事情,都应该晚一点做;
消息队列有很好的削峰作用:通过异步处理,将短时间高并发产生的事务消息储存再消息队列中,从而削平高峰期的并发事务;
【如图】
特别注意
由于消息写入消息队列后立即返回给用户,数据再后续的业务校验,写数据库等可能失败;因此需要适当修改业务流程进行配合;
比如订单提交后,不能立即提示用户订单成功,需要队列中真正完成该订单,再通知用户订单成功;
三。使用集群
使用负载均衡技术为一个应用构建一个由多台服务器组成的服务器集群,将请求分发到多台服务器上处理;
避免单一服务器因负载压力过大而响应缓慢,使用户请求具体更好的响应延迟特性;
【如图】
四。代码优化
1. 多线程
网站开发天然就是多线程编程;
由于线程比进程更轻量,更少占有系统资源,切换代价更小,主流方案都采用多线程的方式响应并发用户请求;
资源利用角度看,使用多线程原因
- IO 阻塞
- IO 操作需要较长时间,CPU 可以调度其它线程进行处理;
- 多 CPU
- 要想最大限度的使用多核 CPU,必须启动多线程;
启动线程数量
启动线程数 = [ 任务执行时间 / (任务执行时间-IO等待时间) ] * CPU内核数
线程数: 和CPU内核数成正比
和IO阻塞时间成反比
如果任务都是CPU计算型任务,那么线程数最多不超过CPU内核数;(因为起再多线程,CPU也来不及调度)
多线程注意
多线程并发对某个资源修改,可能导致数据混乱
,
因为所有资源(对象,内存,文件,数据库,乃至另一个线程)都可能被多线程并发访问;
将对象设计为无状态对象
对象本身不储存状态信息,对象无成员变量,或者成员变量也是无状态对象;
(从面向对象的设计看,无状态对象使不良设计)
使用局部对象
方法内部创建对象,这样可以互不影响;(除非有意识的将这些对象传递给其它线程;)
并发访问资源时使用锁
通过锁的方式,使多线程并发操作转化为顺序操作。从而避免并发修改;
2. 资源复用
尽量避免 / 减少开销很大的系统资源创建和销毁;
比如:数据库连接,网络通信连接,线程,复杂对象;
两种模式:
- 单例模式
- 对象池
单例模式
目前 WEB 开发注意使用贫血模式,从 service 到 Dao 都是无状态对象, 无需重复创建;自然使用单例模式;(注意 Java Spring 的单利是 spring 容器管理的单例,而不是用单例模式构造的单例)
对象池
对象池:通过复用对象实例,减少对象创建和资源消耗;;
对数据库:频繁创建创建关闭数据库连接,对数据库性能消耗比较多;
实践中,数据库连接基本使用连接池的方式(begin/commit更新需要保证同一个连接);
对WEB请求:每次请求都需要一个独立线程去处理,也可以使用线程池方式;
3. 数据结构
下面这种可以获取很好的随机散列
- 1.对字符串取信息指纹;
- 2.对指纹求HashCode;
【如图】
4. 垃圾回收
内存分为 栈内存(堆栈/stack) 和 堆内存(堆/heap)
- 栈内存:储存线程的 上下文信息/方法参数/局部变量
- 堆内存:储存对象的 内存空间,对象的创建和释放;(垃圾回收机制发生在这里)
堆内存分为
- 年老代
- 年轻代
- Eden区(满的时候触发垃圾回收机制)
- From区
- To区