Monthly Archives: November 2014

poi – 生成excel时使表头比较醒目、好看


		CellStyle style = sheet.getWorkbook().createCellStyle();
		//置成黄色
		style.setFillForegroundColor(IndexedColors.YELLOW.getIndex());
		style.setFillPattern(CellStyle.SOLID_FOREGROUND);

		//表头的边界线清晰可见
		style.setBorderBottom(HSSFCellStyle.BORDER_THIN);
		style.setBorderTop(HSSFCellStyle.BORDER_THIN);
		style.setBorderRight(HSSFCellStyle.BORDER_THIN);
		style.setBorderLeft(HSSFCellStyle.BORDER_THIN);

		Row header = sheet.createRow(0);
		int columnIndex = 0;
		for (...) {
			Cell cell = createCell(header, columnIndex);
			cell.setCellValue(headerText);
			cell.setCellStyle(style);
			//表头cell的宽度适应文字
			sheet.autoSizeColumn(columnIndex);
			columnIndex++;
		}




其实用JMeter就可以对mysql做性能测试了

我用Super Smack做过几次MySQL性能测试。虽然可以用,但这种用c写的程序存在安装困难的问题:在阿里云的centos上可以装的上,在aws上未必就装的上;配置起来也很难受,那些个smack文件,太古怪。

后来我发现,其实用JMeter的jdbc request就可以对mysql性能测试。我用的模式是:

引用
1. 在开发机器上(mac/ubuntu/windows)以gui方式配置好Test Plan, 并试运行。

2. 将test plan文件上传到无干扰无界面的linux测试机上,以non-gui方式在测试机上执行测试,再收集结果。

JMeter支持server模式:用本地的jmeter控制测试机上的jmeter, 直接在本机上启动、停止、查看结果,用起来非常顺手。但它有个问题:在测试过程中,测试机上的jmeter要不停地从本地机器获取测试数据,并返回采样数据到本地机器。这些数据流动会增加测试机上JMeter的负担,使qps偏低。

所以我还是选择了在测试机上使用non-gui模式这种方案。下面给个完整的例子,例示jmeter jdbc request的使用。

例示:用JMeter对MySQL进行压力测试

测试目标
使用普通varchar作为主键,比起使用自增数字类型作为主键,到底会慢多少?这个性能损失是不是可以忽略不计?

测试数据准备

建两张差不多的表,一个使用数字user_id作主键,一个直接使用user_name

drop table if exists user_id_pk;
drop table if exists user_name_pk;


create table user_id_pk (
  user_id bigint unsigned not null auto_increment ,
  user_name varchar(50) not null 
  description varchar(200) default null
  created_when datetime not null
  primary key(user_id),
  unique key idx_user_name (user_name) 
) ;
create table user_name_pk (
  user_name varchar(50) not null ,
  description varchar(200) default null,
  created_when datetime not null,
  primary key(user_name)
) ;

生成两个数据文件,一个用于测试插入性能,一个用于在插入完后测试查询性能。 两个文件都是一百万行。

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.math.RandomUtils;

/**
 * 请设置-Xmx3072m
 */
public class GenCsvDataForUserTable {

	public static void main(String[] args) throws IOException {
		File rootDir = new File("/home/ec2-user/kentbench/usertable");
		File forInsertFile = new File(rootDir, "for-insert.csv");
		File forQueryFile = new File(rootDir, "for-query.csv");

		rootDir.mkdirs();
		forInsertFile.delete();
		forQueryFile.delete();

		int TOTAL_RECORDS = 1000000;
		int saveTheshhold = TOTAL_RECORDS / 10;

		List<String> forInsertLines = new ArrayList<String>();
		List<String> forQueryLines = new ArrayList<String>();

		for (int i = 1; i <= TOTAL_RECORDS; i++) {
			String userId = String.valueOf(i);
			String userName = UUID.randomUUID().toString();
			String description = RandomStringUtils
					.randomAlphanumeric(RandomUtils.nextInt(200) + 1);
			String line = userId + "," + userName + "," + description;
			forInsertLines.add(line);

			// 同时也用于查询
			forQueryLines.add(line);

			// 累积到一定量就存一下
			if (i % saveTheshhold == 0) {
				System.out.println("done with No." + i);

				FileUtils.writeLines(forInsertFile, forInsertLines, true);
				// 用于查询的数据先弄散,再保存
				Collections.shuffle(forQueryLines);
				FileUtils.writeLines(forQueryFile, forQueryLines, true);
				forInsertLines = new ArrayList<String>();
				forQueryLines = new ArrayList<String>();
			}

		}

		System.out.println("done with for-insert.csv. please check "
				+ forInsertFile);
		System.out.println("done with for-query.csv. please check "
				+ forQueryFile);
	}
}

Test Plan配置
一个plan用来往两个表里插入数据,另一个用来从两个表里查询出数据。

以“插入”为例:

线程池配置

数据库JDBC配置

数据文件配置
这里会用到上面生成的数据文件。另外还为每列指定一个变量名,这些变量名在下面中会用到

JDBC Request配置 – 插入到以user_id为主键的表
“parameter values”对应的就是数据文件配置中定义的变量名

JDBC Request配置 – 插入到以user_name为主键的表

测试的执行
点一下JMeter中的“播放”按钮即可执行测试,看下基本的效果 (本地使用了gui模式, 为了查看执行效果,应该添加一个Listener如Summary Report.  此处不表,请自行google)

我们可以将这个test plan存储为一个*.jmx文件,再把它上传到一个无界面无干扰的测试环境中执行。但在执行这一步之前,应该禁掉一个jdbc request, 只保留另一个。否则JMeter会同时执行两个jdbc request, 这会干扰测试结果.
这里解释了为什么不应该同时执行。

  (这个测完后再互换一下,然后再执行一次测试。)

在测试机上执行:

$jmeter -n -t my.jmx 

执行完后查看控制台输出或者jmeter.log, 可以看到类似于这样的数据:

引用
summary = 1000000 in   349s = 2863.4/s Avg:    34 Min:     0 Max:  2339 Err:     0 (0.00%)

其中s = 2863.4/s就是吞吐量(tps), Avg:34是平均RT (ms)

补充说明
用于查询的Test Plan的配置办法与上述的用于插入的Plan的配法差不多,不过你一定要加上一个Assertion配置,以确保某条select语句符合我们的预期,确实查到了数据。


这个意思是每次查询返回一条记录。”_#”代表返回的记录数. firstColumn代表返回记录的第1个字段,这个变量名要在jdbc request中配置。

(对于这种记录数的Assertion, 是否通过第1个字段来判断根本无所谓,第2个字段,第N个字段都可以)

测试结果
场景: 逐渐插入数据,直到累积到100万条

rt qps
用user_id当主键的表 34 2863.4
用user_name当主键的表 41 2346.8
下表比上表 慢20% 低18%

场景: 在100万条数据的表中查询100万次

rt qps
用user_id当主键的表 16 5803.9
用user_name当主键的表 18 5270.7
下表比上表 慢12.5 低9%

(机器:aws ec2 c3.large, cpu 2核, 内存3.75g, ssd硬盘,操作系统Amazon Linux AMI release, mysql 5.5.40 innodb)

结论:使用varchar作为主键,比使用id作为主键性能要差10%-20%,而且插入的性能差别比查询的性能差别更大。插入场景差别更大应该是因为varchar随机插入时会导致page split所致。

这个经历比起用Super Smack还是愉快的多的!

比较一个系统中两个模块的性能差别:不要同时压这两个模块

要比较一个系统中两个模块的性能差别(比如同一个库中的两个类似的表),有两种压测模式:

1. 一个test plan含两个senario, 执行一次,同时出两个模块的结果。

2. 搞两个test plan, 各带一个senario; 各执行一次,各出一个结果。

第一种模式很有吸引力,因为它可省下很多重复的配置工作;只执行一次,比较省时;像jmeter之类的还会直接给出直方图,以示比较。

然而,我的亲身经历表明,第1种模式由于公共资源占用问题会导致“大锅饭”效应: 同时压测两个模块,慢模块会占用过多公共资源(cpu, 内存,连接池等),导致快模块无法获得足够的资源因而也变慢。 这种模式下比较出来的结果并不准确。

所以,还是应该把两个测试互相隔离起来,搞两个 test plan,各带一个senario吧。

p.s. 如果你用jmeter,可以只写一个test plan,带两个sampler, 然后enable一个,disable另一个,这样每次执行时仍是隔离的。

分布式系统中的三种接口粒度

分布式系统中有三种接口粒度:

1.
内部用的domain service接口。这个接口最精致。

  a. 出入参数跟数据库表有比较强的对应,基本上一个对象对应一张表。

  b. 入参数据都假定已清洗,不必再校验。

  c. 业务逻辑主要是CRUD+高度可重用的核心业务逻辑。

  d. 返回数据一般是数据对象或者它们的类集。错误信息一般直接通过exception返回。

2.
供移动或web前端调用、或者供外部系统调用、面向use case的app service接口。这个接口最重。

  a. 出入参数一般对应数据库中的多张表中的数据,还会有一些use case特有的字段,比如一个“帖子”对象中会给出“帖子作者的头像”、“当前用户能否删除本贴”、“帖子被评论次数”等。 这一层的数据对象是DTO(value object), 一般要从多个domain层对象的字段中组合而来。

  b. 入参数据由用户直接输入或非可信系统输入,所以需要校验。

  c.  业务逻辑一般需要通过组合多个domain service实现,比如 N个crud +  1个权限校验 + 1个展示层数据填充等,很难重用。

  d. 返回数据一般是Result对象: 数据对象 + 错误码 + 错误信息, 不再抛exception.

3.
给内部可信系统调用的rpc service接口。这个接口的轻、重介于上面两种之间。

  a. 出入参数包含的字段往往比domain层参数的稍多一点,比app层参数的少一些或少很多。

  b. 入参数据基本已清洗(比如字符串都trim过了),但为了系统健壮性还是要做些校验,尤其是空值判断。

  c. 业务逻辑基本上就是domain service, 顶多按特定业务做一些增补。 

  d. 由于远程调用的特殊性,直接通过exception返回错误信息可能会引起报文过大;所以返回数据对象一般也可以是Result对象: 数据对象 + 错误码 + 错误信息。

很多情况下domain service可以直接用作rpc service. 问题在于domain层可能变化比较频繁并且不保证版本向下兼容,为了不让调用方困扰,一般还是要专门搞一套。

集群环境下慎用本地缓存

集群环境下慎用本地缓存。

用户1在机器A上看到100条记录,用户2在机器B上看到的却是90条记录。

你会说你的业务允许两边看到不一样。 是的,两个用户看到的不一样不要紧。

但是同一个用户看到不一样的话,用户体验会非常差,差到要骂人。 例子是:用户1在机器A上提交表单删除100条记录,服务端处理完毕后让浏览器跳转(Redirect after Submission),负载均衡将这个请求跳转到机器B上,机器B上的本地缓存没变,所以仍然是删除前的记录数。

用户1看到这个结果,脑子里只有一个想法:删除没起作用。

所以,集群环境下使用本地缓存,一定要保证同一个用户先后访问的是同一台机器。

MySQL: 被索引的字段是否有空值对性能影响不大

‘High Performance MySQL’ 虽然说了  “Avoid NULL if possible”,但也说了

引用
“The performance improvement from changing NULL to NOT NULL is usually small”.

我针对这个做了下benchmarking. 性能差别确实很小。请看这里的末尾部分。

业务上说,有很多字段确实既要索引,又可能为空。对于这种情况,该NULLABLE就NULLABLE吧,没必要为了一点点性能上的收益而使用各种默认值给代码留坑。

typeahead.js + remote ajax完整例子

typeahead.js的样例文档很不详细,必须查API,而API也写得比较简略。

我这里给一个比较典型的样例,供参考。

典型用况:修改一条person记录。服务端要你上传personId参数, 但用户只记得personName; 这时你要让用户在一个输入框里输入personName, 搜索出person信息,用户选中一个记录,系统再在另一个只读的输入框里记录被选中的personId, 最后提交表单上传到服务端。

表单DOM:

<form>
		<table>		
			<tr>				
				<td>Person Name</td>
				<td><input type="text" class="typeahead" name="personName" id="personNameInput" placeholder="输入Person Name搜索"/>  </td>
			</tr>
			<tr>				
				<td>Person ID</td>
				<td><input type="text" name="personId" id="personIdInput" readonly/>  </td>
			</tr> 
			 
			 ...		
		</table>
</form>

 

其实也可以只提供一个输入框:用户输入Person Name并选择后,系统在输入框中显示Person ID.  但Person Name和Person ID一个是字符串类型,一个是数字类型。 遇到这种情况,typeahead.js会有个bug: 第一次搜索正常,清空输入框再次搜索就会报JS语法错误。

typeahead相关javascript:

		var persons = new Bloodhound({
		  datumTokenizer: Bloodhound.tokenizers.obj.whitespace('personName'), //服务端返回的json中, Person Name对应的字段叫personName 
		  queryTokenizer: Bloodhound.tokenizers.whitespace,		  
		  remote:{ 
			  url: '/personService?personName=%QUERY',  //'%QUERY' 将被用户输入的值代替
			  filter: function(resp){ //服务端未必直接以json array方式返回搜索结果。如果不是的话,指定一下搜索结果在json中的路径。
			  		return resp.personList;
		  		}				  
		  }

		});
		 
		
		persons.initialize();
		 
		$('#personNameInput').typeahead({ //把这个输入框变成auto complete风格
		  hint: true,
		  highlight: true,
		  minLength: 1
		},
		{
		  name: 'thePersons', //其实没什么用,只是用来构建生成的dom元素的css class名
		  displayKey: 'personName', //选择好结果后,输入框里显示的字段
		  source: persons.ttAdapter(), //请照抄

		  templates: { //这个函数决定下拉列表中的每一行怎么渲染。 
			    suggestion: Handlebars.compile('<p>ID:{{personId}} - name:{{personName}}</p>')  //这是个模板,里面的{{xxx}}中的xxx是服务端所返回JSON数据中所对应的字段。下文会说明Handlebars的角色. 
			  }
		}).on('typeahead:selected',function(evt,datum){
			$('#personIdInput').val(datum.personId); //用户从下拉中选择某项后,刷新那个只读的personId值
		});	    

还要引入这些JS:



    <script src="/js/jquery-1.10.2.min.js"></script>

    <!--handlebars是typeahead所使用的模板引擎,似乎还不能换成别的 -->
    <script src="/js/handlebars-v2.0.0.js"></script>    

    <script src="/js/typeahead.bundle.min.js"></script>    

还有一个很挫的地方:typeahead没有直接提供一个好看的css, 如果你不引入任何css, 所生成的下拉列表就会很丑很丑。 我的前端样式框架是twitter bootstrap, 网上搜了下,下面这个还不错:
https://github.com/bassjobsen/typeahead.js-bootstrap-css