规则引擎的设计

规则引擎

我要做一个需求是要实现一个规则引擎,目的呢就是要进行一系列规则的判断,比如是否有重复,是否超过频率,是否达到最早开始时间和最晚结束时间,不符合条件的都被过滤掉,过滤的规模在2000w,尽量在2小时内完成。

初步想法

最开始的想法就是单次过滤要非常的快。由于一个要进行这么多的判断,是否重复,是否超过频率。这种类型的判断,实际上是要进行历史的对比,所以这些信息只能存在内存里,才能保证单个判断是比较快的。如果单个判断是2ms,那么多个的判断就会在10ms内就有可能满足条件。对于历史的消息的存储想好了就用redis来存了。

引擎的设计

由于判断时,如是否重复,需要加锁,因为有多线程竞态条件,所以必须加锁,如果是直接锁判断的的方法的话,并发发送时估计性能不够。当时就想起来HashMap内部的实现,使用KEY的HASH把竞态的条件分布到不同的区去,在各自的区上加锁。把这个模块做成一个服务,供外部的来调用。但实现难度有点大,这个引擎得自己写啊。

灵机一动

后面突然灵机一动,可以用Redis的乐观锁来实现刚才说的竞态条件啊。使用它的乐观锁。流程大概如下:

watch key
    exists?
    multi
    if(exist)
        //这个时间有可能出现竞态,键被删除,他又不算重复了。
        return 重复
     else
         添加条目
         return 不重复
     exec

这样就既用Redis为存了数据,又利用它的乐观锁的特性实现了业务。突然发现这里应该深补下Redis乐观锁的性能哈,这样按每个key加一个锁,粒度很小了。

进一步加强

刚才用Redis虽然实现了这个,但日志数据2000W需要存来对比量还是蛮大,未来规则复杂了还有扩大的可能,且单个Redis还有单点故障,存在一个Redis里面还不是很安全,后面就想做个Redis集群吧。但是常用的集群插件如Codis,twenty什么的不支持高级命令如事务这些。但我这里的规则在业务上只会针对一个key的操作,所以这样的限制对我其实无意义。后面就自己用jedis支持了通过Key的hash实现的集群

关于hash算法

关于hash算法经管Redis服务器的同学的提点,说可以用一个一致性hash算法,又简单看了下jedis的分片实现源码,本来想自己改一个,后面在网上找到了一个类似的实现,将hash算法改为了jedis的算法。

实际上线情况

上线前压测单次判断是2ms,后面在预生产环境压测时是10ms,当时奇了怪了,找了半天结果发现原来是redis在生产环境配置过大,全在初始化(使用houseMD跟踪,全在init),所以很慢。当时大概的测试逻辑是200个线程同时对10000数进行判断重复。相当于每个数会有200个线程进行竞争的重复判断。测试环境大概7ms左右,生产环境大概2ms单次。后面竞态条件去掉,在生产环境单个判断也差不多要2ms,难道已是极限了?

小插曲

由于一般进行批量推送,单次判断约有200个消息,每个消息进行约5次规则判断,按2ms算,用时要2秒。当时要每次用户存的时候就判断,那么每次用户访问要2s.完全不能接受,后面就只有把这个流程放到其它地方了。可以用多线程批量调用的地方。

HotSpot算法及垃圾收集器简介

垃圾回收算法基本思想:
1.枚举根节点(GC Roots)
在垃圾回收时,我们要想办法找出哪些对象是存活的,一般会选取一些被称为GC Root的对象,从这些对象开始枚举。枚举时要求所有对象停下来,也就是大家所称的“Stop the world”。所有的算法实现都会将虚拟机停下来的,否则分析结果的准确性将无法保证。当执行系统停顿下来之后,虚拟机不需要遍历所有的根节点和上下文去确定GC Roots,而是存在着一个OopMap的数据结构来达到这个目的。在类加载完成的时候,虚拟机就会把什么类的什么偏移上是什么类型的数据计算出来。在JIT编译的时候也会在特定位置记下在寄存器和栈中哪些位置是引用,GC在扫描时就可直接得到信息。

2.安全点
因为程序在运行时不是所有的时候停下来都是安全的(比如运算进行到一半,数据值是一个脏数据),安全点是所有线程在”Stop the world”时到达的一个安全的点。由于堆中的对象庞大,若为每个对象都生成OopMap数据结构将占用大量空间,所以HotSpot只在”安全点“上生成这些数据结构。同时程序并非在所有位置都可以停止,而是只能在安全点才会停止。所以”安全点“还会影响到GC的及时性。”安全点“选取时不能太多,以造成空间上的支出,也不能太少,以让GC等待较长时间。
对于”安全点“还有另外一个需要考虑的问题是,当GC发生时如何让所有线程都跑到最近的”安全点“,一般都两种方案。抢先式中断,抢先式中断是虚拟机将所有线程停下来,然后一一检查是否已达安全点,若没有到达安全点则恢复线程让其到达最近的”安全点”,此种方式几乎没有虚拟机使用。主动式中断的思路是中断发生时,虚拟机在所有的线程上设置一个标志,线程自行检查该标志,然后进入“安全点”。检查标志的地方和安全点是重合的。

3.安全区域
上面没有解决的问题是当一个线程处于休眠,或未分配CPU时钟,比如sleep或blocked状态时,他就无法走到安全点去挂起自己。对于这种情况,就需要通过安全区域来解决,安全区域是一个程序不会更改自己引用的区间,在这个区域的任何地方开始GC都是安全的,可以被认为是扩展了的安全点。当线程执行到SafeRegion的代码时,他就会标记自己已经进入Safe Region,此时发生GC,将不会管这些线程。快要离开安全区域的时候他就会去检查当前的状态,如果是在GC中,那边他就会挂起等待可以离开Safe Region的信号,否则他就可以继续运行。

二、垃圾收集器算法简介
JDK1.7版本,包含的垃圾收集处理器如下:
a
上图是jdk1.7提供了垃圾回收器图,两者有连线说明可以搭配使用,回收器还因为其适用的区域分为年青代,年老代。另外垃圾回收处理器没有优劣之分,只有适用与不适用之分。
在介绍垃圾回收器之前,先说明两个概念。
并行(Parallel):指多条垃圾回收处理器产并行工作,但此时用户线程是暂停的。
并发(Concurrent):指用户线程和垃圾收集线程同时执行(也有可能是交替执行),可能用户线程运行在这个CPU上,而垃圾回收线程运行在另一个CPU上。

年青代的垃圾回收器

1.Serial收集器(年老代单线程收集器)

单线程的收集器,收集时还会暂停所有工作线程,直到它收集结束。
Serial收集年青时使用复制算法。暂停所有用户线程
备注:是在client模式下默认的新生代收集器,原因是在桌面模式下管理的内存比较小,几十M,单线程相比多线程回收,少了线程间交互,效率更高。所以单线程模式是一个不错的选择。

2.ParNew收集器(年青代多线程收集器)

ParNew是Serial的多线程版本。新生代收集采用复制算法,收集时会暂停所有用户线程。他是server模式下年青代的默认垃圾回收处理器,一个重要的原因是只有他能与年老代的CMS处理器配合使用。
ParNew处理器在单CPU环境下绝对不会比Serial有更好的表现。因为有线程交互的成本,但是随着CPU数量的增加,使用还是很有好处的。

3.Parllel Scavenge收集器

Parllel Scavenge收集器也是一个并行的多线程收集器,看起来似乎同ParNew收集器无本质的区别,但是他的目标是不一样的。他的目标是达到一个可控制的吞吐量。吞吐量=运行代码时间/(运行代码时间+垃圾回收时间)。其实高吞吐量是指较高程度的利用了CPU。但不意味着其用户体验一定是最好的,因为单次暂停可能会非常长。他有两个参数来控制:

MaxGCPauseMillis:期望的最大GC停顿时间,当这个时间就少,他是通过将新生代调小来减少停顿时间(收集300M比收集500M来得快,但这样会导致垃圾收集发生得更频率,原来10秒一次,每次停100ms,现在每5秒停一次,每次停70秒),但会降低总体的吞吐量。
GCTimeRatio:配置的是用户时间同GC时间的比值,比如19,则是指GC时间同用户使用时间的比值是1:19。那么允许的最大GC时间就是1/(1+19),即5%.
-XX:+UseAdaptiveSizePolicy:这个收集器另外可配置的一个参数,虚拟机会自动根据运行时的情况来动态调整年青代,年老代的大小,以提供最合适的停顿时间或者最大的吞吐量。这个被称为GC自适应的调节策略。
也就是说只需要给虚拟机设定吞吐量目标和管理的最大堆的大小,然后就让自适应调节策略去自动优化吧,这是和ParNew收集器的一个重要区别。

年老代的垃圾回收器

1.Serial Old

Serial Old是单线程版本的老年代收集器,收集时使用“标记-整理”算法,收集时暂停所有用户线程。他存在的主要意义是供Client模式下的虚拟机使用,原因同serial收集器是client模式下的默认收集器一样。
如果是Server模式下,他还有两个用户,一个是和Parallel Scavenge收集器搭配使用。另一个是做为CMS收集器的后备预案,用于在CMS在Concurrent Mode Failure时使用。

2.Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程的“标记-整理”算法。他们俩适合配套使用。由于Parallel Scavenge不能同CMS配合使用,一般就只能同Serial Old配合使用,所以jdk1.6才引入的Parallel Old收集器填补了这块空白。

3.CMS(Concurrent Mark Sweep)并发标记清扫收集器

CMS是一种以获得最短停顿时间为目标的收集器。由于大部分的互联网应用都倾向于向用户提供更好的交互体验,所以希望较短的停顿时间,以带来更好的体验。他的实现相较更复杂,分为如下步骤

  • 初始标记(stop the world)
  • 并发标记
  • 重新标记(stop the world)
  • 并发清除

除1,3步骤是暂停的,其它步骤是并发的。初始标记主要是标记的GC Root直接关联到的对象。并发标记则是走的GC Root Tracing的过程。重新标记是修正在并发标记期间由于用户线程的并发运行引起的变动,耗时会较长,但远比并发标记耗时来得短。整个过程中较耗时的并发标记和并发清除阶段都是和用户进程并发的,所以他较小的减少了停顿时间。
总体而言,CMS是并发的,低停顿收集器,但他还远算不上完美,他有如下一些缺点

  • CPU敏感,会占用系统CPU资源,并发回收时一般开启(CPU数量+3)/4个线程,会导致总吞吐量降低。
  • CMS不能处理浮动垃圾,可能出现”Concurrent Mode Failure”失败而导致一次Full GC.浮动垃圾是指在回收期间新产生的垃圾,这部分垃圾只有留待下次清扫。另外由于用户线程并发运行,他还需要内存来支持,所以收集器会在一个较低的使用率时就开始工作,以便有空间供垃圾回收期间程序并发时使用。使用-XX:CMSInitiatingOccupancyFraction来修改触发时的比例,JDK1.6设置的比率是92%。如果CMS在运行期间预留的内存无法满足程序使用,就会出现“Concurrent Mode Failure”, 从而会启动备选的Serial Old来进行垃圾回收,这样会耗更长的时间,所以一个较合适的比值会提高吞吐量。
  • 使用”标记-清扫”算法会产生碎片,此时需暂停所有用户线程以进行整理,会消耗更多的时间。

4.G1收集器

G1是一款面向服务器端的垃圾收集器,被视为是CMS收集器的后继者,他具有以下特点。

  • 并行与并发。充分利用多CPU来缩短垃圾回收的停顿时间,原本需要停顿的,仍然可以通过并发方式让JAVA程序继续运行
  • 分代收集。G1不需要同其它收集器配合使用。其内部会根据对象的存活长短使用不同的回收算法。
  • 空间整合。整体来看是基于“标记-整理”算法,从局部上来看是基于“复制”算法。这种方式会减少碎片的产生。
  • 可预测的停顿。除了完成尽量减少停顿的目标以外,还建立了可预测停顿时间的模型。

后面我们来看看G1是如何做到这一切的。
G1收集器的内部内存而已与之前收集器有很大不同。他将整个堆划分成多个大小相等的独立区域(Region)。虽然还保留有新生代和老年代的概念,但是他们之间不再物理隔离,他们都是一部分Region(不需要连续)的集合。
为什么G1能做到对垃圾停顿时间的预测,是因为他有计划的避免进行全区域的垃圾回收。他会扫描各区域,分析垃圾回收的价值,在后台维护一个优先列表,只有有较高回收价值的区域才启动回收,这也是Garbage-First名字的由来。
整体上来说,G1的思路就是化整为零。但是其实现具有较大的复杂性,因为Region间是互相引用的,回收一个Region需要遍历到其它Region的内容。
在G1收集器中Region中使用Remebered Set来避免全堆扫描的。当虚拟机发现对引用型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查是否引用到不同的Region,如果是则使用CardTable把相关的信息记录被引用对象所属的Region的Rember Set中。当进行垃圾回收时,GC Root的枚举范围增加Rember Set就不会有遗漏。如果不记对Remembered Set的维护操作,收集步骤大概分为如下几步:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

初始标记
此阶段只是标记GC Roots关联到的对象,并且修改TAMS(Next Top at Mark Start)的值 。让下一阶段并发的用户能在正确的Region上创建新对象,此过程需要停线程,但是耗时很短。
并发标记
遍历GC Roots关联对象,判断可达性,找出存活对象,可并发。
最终标记
在并发标记阶段发生的更改会记录到Remembered Set Log中。需将这些Log数据合并到Remembered Set中。
筛选回收
此阶段是评估回收价值的阶段。回收时会停顿用户线程,因为是对单个子Region回收,由于较小时间可控,停止线程会提高收集效率。后续可能可以做到并发。

关于G1算法的深入原理参见此文:http://hllvm.group.iteye.com/group/topic/44381#post-272188

oracle千万数据中较快速查找满足max条件的数据行

场景

假设我们有这样的一个访问记录表accessTable,记录项分别为用户唯一标识(userId)、访问时间(visitTime)、访问时上报的版本信息(ver)。需求为查询每个用户最近一次访问时上报的版本是什么,环境是在oracle下。本人非DBA,有问题一定要指出来啊。

思路一

使用oracle的分析函数。具体函数使用见这里

[java]
SELECT *
FROM (SELECT t.userId,
t.visitTime,
t.ver,
Row_number()
OVER(
partition BY t.userId
ORDER BY t.visitTime DESC) rk
FROM accessTable t) rank_tab
WHERE rank_tab.rk = 1
[/java]

这里的思路是通过row_number分析函数为数据排名,排名按用户标识分组,并按访问时间由大到小。实际运行时此种方式仍速度极慢。

思路二

这个思路是从一个同事处得来的,首先在此表示感谢,他的实现思路是利用ORACLE的rowid实现。其实我们只需要找到每个userId的最大访问时间的数据行。

[java]
SELECT acc_tab.*,acc_tab.rowid
FROM accessTable acc_tab,
(SELECT Substr(tr_tab.time_rowid, 15) rk_rowid
FROM (SELECT t.userId,
Max(To_char(t.visitTime, ‘yyyymmddhh24miss’)
|| t.rowid) AS time_rowid
FROM accessTable t
GROUP BY t.userId) tr_tab)r_tab
WHERE acc_tab.rowid = r_tab.rk_rowid
[/java]

实现时是将日期字符串和该条记录对应的rowid串起来拼出一个长串,对这个长串来求最大,求出最大的字符串的同时我们可以从字符串中拆出对应的rowid,然后通过rowid关联从而快速的得到满足条件的记录,这种方法在实际环境下效率是不错的。拼串时要注意拼的内容,字符串比较会认为22比111大。

由于水平有限,非专业DBA,错漏难免,如有意见和建议请一定及时指出,防止水平有限误导他人。

Nginx做下载代理IO极高问题

由于某些原因我们的项目使用了NGINX代理下载请求,当NGINX的STATUS中的Writing达到500左右时,服务器的CPU就达到100%了。

项目背景

项目主要提供用户的下载,包大则有300M,小则10来M。项目需要记下载日志,会把下载时间,用户请求的大小,下载了多少,下载时的并发数,是否进行了断点续传等信息记录下来,日志记录使用的是Servlet来实现,故而引入了WEB容器Tomcat,前端加入了Nginx代理,来支持多个Tomcat,以解决单个Tomcat性能有限的问题。

 问题分析

首先是确认CPU是谁引起的较高,通过使用iostat命令查看大概是这样的效果。

[java]
[root@nth-server ~]# iostat -x 1
Linux 3.12.9-x86_64-linode37 (nth-server) 05/07/2014 _x86_64_ (8 CPU)

avg-cpu: %user %nice %system %iowait %steal %idle
0.06 0.00 0.01 0.00 0.11 99.81

Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util
xvda 0.00 0.34 0.02 0.14 0.52 3.84 27.82 0.00 18.64 2.45 0.04
xvdb 0.00 0.00 0.00 0.00 0.00 0.00 24.99 0.00 12.02 7.68 0.00
参数简介:
rrqm/s : 每秒进行 merge 的读操作数目。即 delta(rmerge)/s
wrqm/s : 每秒进行 merge 的写操作数目。即 delta(wmerge)/s
r/s : 每秒完成的读 I/O 设备次数。即 delta(rio)/s
w/s : 每秒完成的写 I/O 设备次数。即 delta(wio)/s
rsec/s : 每秒读扇区数。即 delta(rsect)/s
wsec/s : 每秒写扇区数。即 delta(wsect)/s
rkB/s : 每秒读K字节数。是 rsect/s 的一半,因为每扇区大小为512字节。(需要计算)
wkB/s : 每秒写K字节数。是 wsect/s 的一半。(需要计算)
avgrq-sz: 平均每次设备I/O操作的数据大小 (扇区)。delta(rsect+wsect)/delta(rio+wio)
avgqu-sz: 平均I/O队列长度。即 delta(aveq)/s/1000 (因为aveq的单位为毫秒)。
await : 平均每次设备I/O操作的等待时间 (毫秒)。即 delta(ruse+wuse)/delta(rio+wio)
svctm : 平均每次设备I/O操作的服务时间 (毫秒)。即 delta(use)/delta(rio+wio)
%util : 一秒中有百分之多少的时间用于 I/O 操作,或者说一秒中有多少时间 I/O 队列是非空的。即 delta(use)/s/1000 (因为use的单位为毫秒)
[/java]

上述只是展示了一个示例,由于上班无法截图。大概是iowait达到90%,同时avgqu-sz达到300左右,写扇区数达到14000,但是奇怪的是svctm的时间在3ms,而且比较稳定,await的时间在500以上。由于svctm比较小,说明不是磁盘性能的问题,据说在20ms以上才不正常。总体而言应该是写请求太多了,这个结论当时并没有立刻得到。而是通过找了一下Nginx的proxy_buffer的相关资料,关掉proxy_buffer后CPU降下来得到的结论。

Nginx Proxy buffer 工作原理

Proxy buffer是Nginx在使用proxy功能是缓存后台返回数据的一种机制。通过proxy_buffering off/on;来关闭和打开缓存,默认他是打开的。proxy_buffer的更多介绍。这里来说一下关键的要点。

  1. 若后端返回的文件较大,会将这些来不及发到客户端的请求写到临时文件中。
  2. 这样的缓存是一个请求一个缓存。

这样在我们这样一个下载服务器的环境中就将意味着每一次请求可能都意味着写一个临时的下载文件,若有500左右的同时下载则将意味着500个同时的请求写临时文件,这就将引起大量的IO请求,这就是造成CPU飙高的主因了。但是大量的直接下载一般不会出现CPU很高的情况呢,这是因为操作系统缓存了常用的文件,可通过下面的命令看到。

[java]
[root@nth-server ~]# vmstat 1
procs ———–memory———- —swap– —–io—- –system– —–cpu—–
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 5928 78464 94284 346500 0 0 0 0 1 0 0 0 100 0 0
0 0 5928 78464 94284 346500 0 0 0 0 35 28 0 0 100 0 0
0 0 5928 78464 94284 346500 0 0 0 0 13 10 0 0 100 0 0
0 0 5928 78464 94284 346500 0 0 0 0 15 14 0 0 100 0 0
参数简介:
r 表示运行队列(就是说多少个进程真的分配到CPU),我测试的服务器目前CPU比较空闲,没什么程序在跑,当这个值超过了CPU数目,就会出现CPU瓶颈了。这个也和top的负载有关系,一般负载超过了3就比较高,超过了5就高,超过了10就不正常了,服务器的状态很危险。top的负载类似每秒的运行队列。如果运行队列过大,表示你的CPU很繁忙,一般会造成CPU使用率很高。

b 表示阻塞的进程

swpd 虚拟内存已使用的大小,如果大于0,表示你的机器物理内存不足了,但若si、so项长期为0也说明是正常。的

free 空闲的物理内存的大小,我的机器内存总共8G,剩余3415M。

buff Linux/Unix系统是用来存储,目录里面有什么内容,权限等的缓存,我本机大概占用300多M

cache cache直接用来记忆我们打开的文件,给文件做缓冲,我本机大概占用300多M(这里是Linux/Unix的聪明之处,把空闲的物理内存的一部分拿来做文件和目录的缓存,是为了提高 程序执行的性能,当程序使用内存时,buffer/cached会很快地被使用。)

si 每秒从磁盘读入虚拟内存的大小,如果这个值大于0,表示物理内存不够用或者内存泄露了,要查找耗内存进程解决掉。我的机器内存充裕,一切正常。

so 每秒虚拟内存写入磁盘的大小,如果这个值大于0,同上。

bi 块设备每秒接收的块数量,这里的块设备是指系统上所有的磁盘和其他块设备,默认块大小是1024byte,我本机上没什么IO操作,所以一直是0,但是我曾在处理拷贝大量数据(2-3T)的机器上看过可以达到140000/s,磁盘写入速度差不多140M每秒

bo 块设备每秒发送的块数量,例如我们读取文件,bo就要大于0。bi和bo一般都要接近0,不然就是IO过于频繁,需要调整。

in 每秒CPU的中断次数,包括时间中断

cs 每秒上下文切换次数,例如我们调用系统函数,就要进行上下文切换,线程的切换,也要进程上下文切换,这个值要越小越好,太大了,要考虑调低线程或者进程的数目,例如在apache和nginx这种web服务器中,我们一般做性能测试时会进行几千并发甚至几万并发的测试,选择web服务器的进程可以由进程或者线程的峰值一直下调,压测,直到cs到一个比较小的值,这个进程和线程数就是比较合适的值了。系统调用也是,每次调用系统函数,我们的代码就会进入内核空间,导致上下文切换,这个是很耗资源,也要尽量避免频繁调用系统函数。上下文切换次数过多表示你的CPU大部分浪费在上下文切换,导致CPU干正经事的时间少了,CPU没有充分利用,是不可取的。

us 用户CPU时间,我曾经在一个做加密解密很频繁的服务器上,可以看到us接近100,r运行队列达到80(机器在做压力测试,性能表现不佳)。

sy 系统CPU时间,如果太高,表示系统调用时间长,例如是IO操作频繁。

id 空闲CPU时间,一般来说,id + us + sy = 100,一般我认为id是空闲CPU使用率,us是用户CPU使用率,sy是系统CPU使用率。

wt 等待IO CPU时间。
[/java]

上面的值项中的cache就是表示的操作系统缓存的文件。由于此,所以大量下载同时读的时候性能还是不错的。

解决办法

通过以上的分析我们已经知道了造成问题的罪魁祸首,所以我们的解决办法就是使用proxy_buffering off配置关掉NGINX默认的缓存。关掉这个缓存后,NGINX仍会保留一个基本的MAIN BUFFER,每次从后端读取BUFFER返回内容,然后直同步返回给用户。

影响及其它解决办法

关掉BUFFER将意味着后端的WEB服务器将实时返回结果,此时NGINX的好处仅仅是可以方便扩展,性能上肯定不及直接让后端的服务器直接面对用户,因为NGINX在其中多建立了一次请求,同时后端WEB服务器的压力也得不到释放(将一直保持连接,直到请求完成)。由于一般后端单请求返回的内容小于1M,我们通过设置一个较大的buffer完全可以将返回的内容放进内存(此时后端的WEB容器就可以释放连接,由NGINX来慢慢向慢速的用户返回结果),毕竟使用代理下载是一个太特殊的例子。有两个建议

  1. 下载请求中是否能找出一部分没必要使用到动态脚本,取而代之以NGINX的静态下载来支持,NGINX是支持断点续传的,如带有参数,可使用NGINX的rewrite来改写URL到静态地址。
  2. 若必须使用。则后端的Tomcat的一定要使用NIO或者Apr来解决自身的性能问题,增大可支持的连接数。

如何进行nginx或tomcat的性能调优

最近花了一点时间进行了NGINX加TOMCAT7集群压力测试,下面通过对一些常见问题的回答来说明如何调优服务器的性能,是自己的一些经验,且无实际数据,如有纰漏请见谅。

背景: TOMCAT7已加APR或者NIO。已装简单监控JCONSOLE,监控服务器内存,线程等基本情况。

问题1  一个Tomcat他的maxThreads到底配置多少合适?

一个好的maxThreads的配置就是达到资源的合理化应用。

资源池

在讲其它东西之前,我们先引入一个概念,就是资源池。tomcat7中,他对http请求的处理,也有一个池的概念,配置可以参考这里。每一个请求进来后都是使用线程池中的一个来处理,线程池的大小是由maxThreads来限定的。

异步IO:

当前Tomcat通过使用JAVA NIO或者Apache Portable Runtime这样的异步IO来支持性能的优化。异步IO就是当应用需要进行耗时的IO操作时,向内核发出请求,不用真正等IO操作完成,就去处理其它的请求了,当IO真正完成时会有回调或通知机制通知并完成余下工作。而一般的同步IO是当应用需要IO操作时,向操作系统发出IO Read/Write请求。同时阻塞当前应用,并等待IO返回,返回后才进行后续的操作。从这里可以看出异步IO实际是将请求的处理和IO处理并行了,这样自然能较大的提高系统的吞吐量。

maxThreads的大小:

第一点:从上面的异步IO的机制来看,实际上我们可能可以用一个很小的线程池处理较大的连接数。如当前有100个请求要被处理,处理过程中50个进程都处于IO等待的状态,所以我们实际可能只需要50就能够处理那些不处于IO等待状态的请求就能满足需要了。注意在Tomcat中是使用maxConnection这个配置参数来配置Tomcat的同时处理连接数的。

第二点:盲目的加大线程数会带来一些下面的影响。由于Tomcat处理的线程均会在操作系统中产生对应的实际线程,这就意味着对应的资源消耗(内存,SOCKET等)。另一个影响就是同时处理的请求加大可能导致JAVA内存回收的问题,不同的并发对内存的占用是不同,而实际上90%的内存都是临时变量,可以很快回收。较大的并发同时占用较多的临时变量就会导致容易撑满年青代,从而导致部分内存进入老年代,从而引起更多的Stop The World,甚至OOM,影响JVM性能。其它的影响还包括更高的CPU占用和更多的硬盘读写。这些实际都跟硬件有关。

第三点: 我们可以通过配置一个较合理的资源池,由于资源充裕,单个请求处理迅速,这样能达到最优的系统效率。但是有的时候我们并不总是追求这样的一种情况。比如下载时,单个请求的响应时间将受限于网络,下100M的包可能需要20分钟,我们就不应该通过一个较小的资源池来提升整体的效率,而应该配置一个较大的资源池,让较多用户连接上并进行下载,否则多数的用户都将会因超时被拒绝,从而造成连接上的超快,连不上的就直接被拒绝。

第四点:单个JVM的内存分配较大将导致Full Gc(Stop The World)的中断时间变得更长,影响实时性。高的可达10秒以上的停顿,这段时间所有的东西将被挂起。

配置大小优化思路:

配置时应该根据你应用的实际情况,是最占CPU,内存还是IO,最后达到一个平衡就好,下面来说明思路。

1. 自行保证服务器的资源较够用,如IO、CPU、内存。

2. 在硬件较充裕的情况下尝试以maxThreads配置300、600、1200、1800,分析Tomcat的连接时间,请求耗时,吞吐量等参数。在测试的时候需要密切注意硬盘、带宽、CPU、内存是否处于一个瓶颈情况下。

3. 其实所有的东西最后都有一个极限就是硬件。应用分CPU,IO,内存密集型,这些都会成为你最终的限制性因素。一般应用根据自己的特性划分到不同的机群中,如CPU密集型的会分到一群有更好CPU的集群中。这样可以能充分利用资源。我们以常见的内存为最终限制性因素,并假设CPU足够好,且IO很少来说明思路。通过一些压测工具,我们能容易的找到一个在300~8000的并发数的情况下一个性能的拐点,通过对比不同线程数下请求连接时间、单请求的平均响应时间,总体的吞吐量。这个拐点往往意味着此时的内存回收出现异常,JVM花了更多的时间在回收内存,我们一般可以通过打出gc日志,并使用jmeter等工具来分析得知。此时你可以尝试优化内存结构或加大内存 来解决,若不能解决,可能就意味你前一次的配置就是一个好的选择。当然这些限制因素是可能互相转换的,可能你增加了内存之后内存没有问题了,但是却导致CPU达到100%,从而导致性能下降。此时则要以CPU为最终限制性因素了。

优化测试中陷阱:

以一个下载服务器来例子说明。我们以下载10m的包来做测试,其实你会发现整个服务器的吞吐量很差,响应时间慢。但细心的人会发现此时连接服务器的时间却是很快的,也就是说服务器很快accpet了你的请求,虽然你的吞吐量不大,处理耗时也大。原因是什么呢,其实是你的带宽已经被占满了,你会发现并发下载10个文件就能占满你的所有带宽。所以此时呢你的测试时的对比对象变成了对比连接时间会更加合理。

当然你也可以通过减少包的大小,比如降到 1k,以使带宽不成为瓶颈.这样可能测试出来你的服务器并发极限量,但该并发量可能并不能反应出实际下载的情况,实际的情况就是带宽容易被占满,下载服务器会有一个很大量的连接存在的情况。

问题2. NGINX到底能带来怎么样的性能提升,或者说有什么好处?

1. 测试后发现,NGINX并不能加快响应的速度,为什么呢,因为这是由于NGINX会代理你同后端的请求。也就意味着你原来只需要建立同服务器的一次连接即可完成请求,现在变成了先同NGINX建立连接,NGINX再同后端建立连接。所以引入NGINX后带来了更多的时间消耗,两倍的SOCKET连接消耗。

2. 引入后的好处体现如下。

1) 整体的性能会有提升,通过实测后发现能很大程度上降低最大返回耗时的情况。请求返回更稳定。

2) 降低后端的资源消耗。原来由于客户端网络较慢等因素会让后端在返回数据时处于繁忙的情况,占用资源。通过NGINX向后端代理,同时由于NGINX的缓存机制,后端可以快速返回,并将资源更集中用到处理请求上,这样可以发挥后端的能力。NGINX在保持大量连接这块就得很优秀,内存,CPU都占用很少。

3) 支持非常方便的扩展,高可用性等。

JAVA的List使用Remove时的问题

近日在项目中遇到了一个诡异的问题,参考代码如下:

[java]
public class ListTest {
public static List listFactory() {
return new ArrayList(Arrays.asList("a", "b", "c", "d"));
}

public static void main(String[] args) {
List testList = null;
String t;

// 尝试移除集合中的间隔元素a、c
testList = listFactory();
for (int i = 0; i < testList.size(); i++) {
t = testList.get(i);
if (t.equals("a") || t.equals("c")) {
testList.remove(t);
}
}
System.out.println("移除间隔元素a、c后结果:" + testList);

// 尝试移除集合中的相邻元素a、b
testList = listFactory();
for (int i = 0; i < testList.size(); i++) {
t = testList.get(i);
if (t.equals("a") || t.equals("b")) {
testList.remove(t);
}
}
System.out.println("移除相邻元素a、b后结果:" + testList);
}
}
[/java]

而运行后的结果如下:

[java]
移除间隔元素a、c后结果:[b, d]
移除相邻元素a、b后结果:[b, c, d]
[/java]

从运行的结果来看,在操作List时使用remove方法在移除间隔元素成功,而移除相邻元素时会导致漏删除。

失败原因

通过查看remove()的源码后发现,List内实现remove是通过如下方法实现的。

[java]
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

private void fastRemove(int index) {
modCount++;
int numMoved = size – index – 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[–size] = null; // Let gc do its work
}
[/java]

fastRemove方法是实现的关键,从实现上来看,他是将要删除的元素后的元素逐一向前挪一位来实现的。我们在循环删除时若外层当前的index为1,将之删除,后续元素往前挪,然后外层的index加1继续循环,这样会导致被删除元素的后面紧邻元素不会被遍历到,从而导致漏删。

解决办法

  1. 使用逆序删除的办法[java]
    public class ListTest {
    public static List listFactory() {
    return new ArrayList(Arrays.asList("a", "b", "c", "d"));
    }

    public static void main(String[] args) {
    List testList = null;
    String t;

    // 逆序移除相邻元素a、b后
    testList = listFactory();
    int size = testList.size();
    for (int i = size – 1; i >= 0; i–) {
    t = testList.get(i);
    if (t.equals("a") || t.equals("b")) {
    testList.remove(t);
    }
    }
    System.out.println("逆序移除相邻元素a、b后结果:" + testList);
    }
    }
    [/java]

  2. 使用iterator删除[java]
    public class ListTest {
    public static List<String> listFactory() {
    return new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
    }

    public static void main(String[] args) {
    List<String> testList = null;
    String t;

    // 使用iterator移除相邻元素a、b后
    testList = listFactory();
    Iterator<String> iter = testList.iterator();
    while (iter.hasNext()) {
    t = iter.next();
    if (t.equals("a") || t.equals("b")) {
    iter.remove();
    }
    }
    System.out.println("使用iterator移除相邻元素a、b后结果:" + testList);
    }
    }
    [/java]

java.util.ConcurrentModificationException原因及解决办法

这个异常一般在我们遍历删除集合元素时出现。写了下面这个代码来展示这个异常。

[java]
import java.util.ArrayList;
import java.util.List;

public class ExeptionTest {
public static void main(String[] args) {
List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");

for (String s : list) {
if ("c".equals(s)) {
list.remove("c");
}
}
}
}
[/java]

控制台报错如下:

[java]
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
at java.util.ArrayList$Itr.next(ArrayList.java:791)
at ExeptionTest.main(ExeptionTest.java:14)
[/java]

出现异常原因分析:

for循环执行时内部实际是调用的List实现了Iterator接口的方法,换句话说所有实现了Iterator接口的都可以使用for。追查JDK源码可以看到异常报错正是来自ArrayList内部实现的迭代器类Itr的checkForComodification()方法。

[java]
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
[/java]

其实这个方法只做了一件事就是检查迭代器当前的大小是否和原始大小一样,如果不一样。则认为原始的集合已经在其它地方被修改,故而出现此异常。

解决方法

既然报错的原因清楚了,那么我们只要不混用两种遍历方法就没有问题了。

[java]
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ExeptionTest {
public static void main(String[] args) {
List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");

// 法一
for (int i = 0; i < list.size(); i++) {
if (i == 2) {
list.remove(i);
}
}
System.out.println(list);

// 法二
Iterator iter = list.iterator();
while (iter.hasNext()) {
if ("d".equals(iter.next())) {
iter.remove();
}
}
System.out.println(list);
}
}

[/java]

ConcurrentModificationException进阶

某些时候我们可能会遇到遍历时还要再遍历删除的情况。这时该怎么解决呢?对于这样的情况我们有二种解决办法

  1. 将要删除的对象收集到另一个集合中一起删除[java]import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.List;

    public class ExeptionTest {
    public static void main(String[] args) {
    List list = new ArrayList();
    list.add("a");
    list.add("b");
    list.add("c");
    list.add("d");
    list.add("e");

    Iterator iter = list.iterator();
    List toBeRemove = new ArrayList();
    String t = null;
    while (iter.hasNext()) {
    t = iter.next();
    if (t.equals("c")) {
    toBeRemove.add(t);
    }
    }
    //使用removeAll一起删除
    list.removeAll(toBeRemove);
    System.out.println(list);

    }
    }[/java]

  2. 第一轮遍历时使用复制对象。[java]import java.util.ArrayList;
    import java.util.List;

    public class ExeptionTest {
    public static void main(String[] args) {
    List list = new ArrayList();
    list.add("a");
    list.add("b");
    list.add("c");
    list.add("d");
    list.add("e");
    // 遍历复制集合,此时实际使用的是iterator.
    for (String s : new ArrayList(list)) {
    if (s.equals("c"))
    //移除时使用的是非iterator的方式
    list.remove(s);
    }
    System.out.println(list);
    }
    }
    [/java]

本次的文章就写到这里,如有疏漏,欢迎指正。

Linode使用LNMP安装WordPress找不到主题

1. 错误情况是这样的,新安装了主题后,主题页提示已安装,但是管理页面找不到该主题,只有默认主题。

Linode vps安装是使用军哥的lnmp一键安装包或者你也可以自己搭建Nginx+PHP+MySQL环境,架设好wodpress博客后发现除了默认主题,其他主题都不见了。这是因为php设置中禁止了scandir函数,只需要开启这个函数就一切正常。具体方法:

找到 /usr/local/php/etc/php.ini 文件,打开,查找 disable_functions 字段,将后边的 scandir 函数去掉,保存文件,重启lnmp:

/root/lnmp restart

这样你就可以编辑主题文件了。

2. 安装主题过程中还遇到另一个问题是安装失败,查看了一下权限发现wordpress是root权限。而默认使用lnmp安装完成后www用户权限,导致无法操作,修改用户的归属到www用户就可以了。

Linode使用LNMP一键安装包安装pureFTP失败

PUREFTP提示安装成功,但php的管理界面无法登录。提示表不存在。

由于MySQL 5.1和5.5下的语句有些不同导致在MySQL 5.5下安装失败,其实Pureftpd是安装成功的,只不过php的图形界面无法登陆。

MySQL 5.5 且使用了pureftpd的用户需要按如下方法修复,执行如下命令:
wget http://soft.vpser.net/lnmp/ext/fix_pureftpd_mysql55.sh && chmod +x fix_pureftpd_mysql55.sh && ./fix_pureftpd_mysql55.sh

按提示分别输入MySQL root密码,ftp用户管理面板密码和MySQL ftp用户密码就可修复。

若此修复后错误提示变为无权限登录,则可能是第二次输入的ftp的Mysql帐户密码错误。请重置下密码或再运行一遍输入正确的密码即可。

Linode主机从注册到搭建WORDPRESS个人博客

经过长时间的探究,今天终于有时间和精力来实践建立自己的博客了。

选择主机Linode

主机选择的是Linode,选择的原因如下:

  • 网上看了好久,Linode的相对来说性价比高,不太贵,比较稳定,我也怕折腾。
  • 最近升级了硬盘从24->48G,512->1G内存,8CPU,还算给力,仍是20$。
  • 关于访问国外卡顿的情况,Linode有东京机房,据说会好一点。
  • 但是Linode居然只支持信用卡,有点担心安全,差点放弃,这个公司03年就开了,应该还靠谱吧。

Linode注册购买

进入Linode官网,填写相应注册信息,如有不解,可以参考下图:

sign1

 

下面则是选择你的主机类型,按自己的需求选择就可以了。

 

sign2

 

对于Referral Code可以不填或选填:52e274d160c41e8efa5ebd6069d4e41e2d4b8436

对于Promotion Code则不用填,因为Linode基本没有搞促销。经常搞促销的质量也不会好到哪里去。

如果你真想要优惠,就只有直接买一年,可以优惠10%.建议还是一月一月买,因为不满意可随时退款。

点击Continue进入下一步,等待邮箱通知激活成功就好。

关于Linode的入门设置

注册成功并付款后,使用帐号登录后台开始创建你的NODE。

可以参考Linode官方文章(非常完整):https://library.linode.com/getting-started#sph_id16

1. 选择你的机房位置,若是国内博客则推荐使用东京的机房。

2. 选择你的LINUX发行版,对于此无更好的建议,就是折腾。其它默认应该问题不大。

3. 待选择完成后,选择BOOT,启动你的NODE就好了。

WordPress的搭建

1. 在这里你有两个选择Lamp和Lnmp。主要是Linux,Apache/Nginx,Mysql,Php。这里我选择的是lnmp,最近nginx好像好一点。lnmp安装时有一个一键安装,超级方便。安装时有点费时,是因为他要编译安装。自动安装的程序主要有Nginx,Mysql,Php.并送一个phpmyadmin的mysql管理工具。另外还提供一些软件如FTP等,按需自行安装。

2. 增加一个虚拟的Host.安装好LNMP后会在/root/下生成一个vhost.sh的文件。利用这个可以快速配置新的Host.

3. 然后就是安装WordPress到对应的Host所在目录。安装WordPress就参考WordPress,很简单的。请查看5分钟安装文档.其实他关键做的就是连接到你的MYSQL,创建一个供WordPress使用的数据库,创建数据库的用户名密码。然后把数据库名、用户名、密码这样的基础配置信息写入到wordPress一个叫wp-config.php的文件里。