如何进行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分钟,我们就不应该通过一个较小的资源池来提升整体的效率,而应该配置一个较大的资源池,让较多用户连接上并进行下载,否则多数的用户都将会因超时被拒绝,从而造成连接上的超快,连不上的就直接被拒绝。

配置大小优化思路:

配置时应该根据你应用的实际情况,是最占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时的问题

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

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);
	}
}

而运行后的结果如下:

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

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

失败原因

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

    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
    }

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

解决办法

  1. 使用逆序删除的办法
    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);
    	}
    }
    
  2. 使用iterator删除
    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.util.ConcurrentModificationException原因及解决办法

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

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");
			}
		}
	}
}
 

控制台报错如下:

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)

出现异常原因分析:

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

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

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

解决方法

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

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);
	}
}

ConcurrentModificationException进阶

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

  1. 将要删除的对象收集到另一个集合中一起删除
    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);
    
    	}
    }
  2. 第一轮遍历时使用复制对象。
    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);
    	}
    }
    

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

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的文件里。