本文讲了如何用

1
zero copy
技术来提高I/O性能.

静态文件服务器需要把磁盘上的数据发送给客户端.这里cpu消耗比较少,但是效率不高:内核从磁盘读数据,内核/用户空间交换数据,最后写到socket.数据在内核/用户空间转换时,需要拷贝数据,消耗cpu和内存带宽.对于java应用来说,还需要合理的使用缓冲区来减少gc的压力.

java提供了

1
transferTo
方法来使用
1
zero copy
技术.他可以让数据直接从一个
1
channel
到另外一个
1
channel
.避免上面说到的一些问题.

1.传统的解决办法

过程类似于下面的代码:

1
2
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

1.1数据拷贝

这种方式会有四次内存拷贝

1.2 上下文切换

这种方式会有四次上下文切换.

1.3 过程说明

  1. 1
    read
    
    方法导致一次从
    1
    user mode
    
    1
    kernel mode
    
    的上下文切换.系统调用
    1
    sys_read
    
    从文件读取数据,通过DMA,把磁盘上的数据读到
    1
    kernel address space buffer
    
    .
  2. 数据从
    1
    kernel address space buffer
    
    拷贝到
    1
    user buffer
    
    .
    1
    read
    
    返回,导致从
    1
    kernel mode
    
    1
    user mode
    
    的上下文切换.现在数据被读到了
    1
    user address space buffer
    
    .
  3. 1
    send
    
    方法导致一次
    1
    user mode
    
    1
    kernel mode
    
    的上下文切换.第三次拷贝把拷贝到
    1
    kernel address space buffer
    
    .此buffer关联着
    1
    destination socket
    
  4. 系统
    1
    send
    
    调用返回时导致第四次上下文切换,DMA把
    1
    kernel address space buffer
    
    中的数据发送到协议引擎导致第四次数据拷贝.

1.4 intermediate kernel buffer

使用

1
intermediate kernel buffer
主要为了提高性能,读的时候扮演缓存的角色,写的时候可以让应用程序实现异步(应用程序写到kernel buffer就返回).不幸的是,当我们处理的数据大于内核缓冲大小时,这样的拷贝是完全没有任何意义的.

2.零拷贝的方式

使用如下的代码来完成零拷贝

java方法:

1
public void transferTo(long position, long count, WritableByteChannel target);

系统调用:

1
2
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

2.1 数据拷贝

涉及到3次数据拷贝.

2.2 上下文切换

涉及到2次上下文切换

2.3 过程说明

  1. 1
    transferTo
    
    方法让DMA把磁盘文件读到
    1
    kernel read buffer
    
    .然后内核把
    1
    kernel read buffer
    
    中的数据拷贝到
    1
    socket buffer
    
    .
  2. 1
    DMA
    
    1
    socket buffer
    
    中的数据拷贝到协议引擎.

3 更好的方式

通过上面使用这种方式,上下文切换从4次变为了2次.数据拷贝减少了一次.如果网卡支持

1
gather operations
,linux 2.4内核就开始提供更好的解决方案.

  1. 1
    transferTo
    
    方法让
    1
    DMA engine
    
    把磁盘文件内容拷贝到内核缓冲区.
  2. 数据不需要拷贝到
    1
    socket buffer
    
    .
    1
    socket buffer
    
    里只需写入数据的地址和长度.
    1
    DMA engine
    
    从内核缓冲区把数据读到协议引擎.

通过内核带来的特性,数据拷贝变为了2次(这两次拷贝都是DMA在做).cpu copy变为了0.

4 写在最后

文章地址

1
http://www.ibm.com/developerworks/library/j-zerocopy/
,里面有性能测试结果.后面附带有性能测试程序.不过这个测试程序不太恰当,应该都用nio的api来测试
1
tansferTo
和非
1
tansferTo
.

静态文件服务器一般都有静态资源缓存(apache可以配置,其他的服务器不了解).如果使用内存缓存,减少了读的过程.内存拷贝变为cpu copy

1
application buffer
->
1
socket buffer
,DMA copy
1
socket buffer
->
1
NIC buffer
,磁盘io大大降低了.

1
NIO
不是很熟悉,不知道通过
1
ByteBuffer.allocateDirect()
+
1
transferTo
+
1
gather operations
能不能让copy变为一次.

昨天和澎湃聊了这个事情,当时的想法是数据库中有表记录版本,项目代码中存储变更脚本.无意中看到数据库版本控制工具liquibase.最开始还是有点担心,怕这东西把数据库玩坏了.看了看官方文档,再粗略看了下主流程的源代码,发掘下我想要的功能,这个工具已经足够强大了,我们用好就行.

下面以cs为例,讲讲整个过程.

1. 在cs-dal中添加maven依赖.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    <build>
        <plugin>
            <groupId>org.liquibase</groupId>
            <artifactId>liquibase-maven-plugin</artifactId>
            <version>3.2.2</version>
            <configuration>
                <!--数据库变更主文件-->
                <changeLogFile>src/main/resources/db/changelog/db.master.xml</changeLogFile>
                <!--数据库相关配置文件-->
                <propertyFile>src/main/resources/db/config/dal-${spring.profiles.active}.properties</propertyFile>
            </configuration>
            <executions>
                <execution>
                    <phase>process-resources</phase>
                    <goals>
                        <goal>update</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

2. 编写数据库变更脚本

2.1 数据库变更主文件

1
db.master.xml
:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
     http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.2.xsd">
	<!--数据库变更文件.注意:保证顺序,脚本执行顺序和include的先后有关系.includeAll可以加载所有脚本,加载顺序和文件命名有关系,容易犯错误,建议不使用.-->
	<include relativeToChangelogFile="true" file="db.1.0.sql"/>
   		<include relativeToChangelogFile="true" file="db.2.0.sql"/>
</databaseChangeLog>

2.2 变更脚本

1
db.1.0.sql
:

1
2
3
4
5
6
7
8
--liquibase formatted sql

--changeset qiubo:1
create table test1 (
	id int primary key,
	name varchar(255)
);
--rollback drop table test1;

1
db.2.0.sql
:

1
2
3
4
5
--liquibase formatted sql

--changeset qiubo:2
insert into test1 (id, name) values (1, 'name 1');
insert into test1 (id, name) values (2, 'name 2');

语法参考:http://www.liquibase.org/documentation/sql_format.html

2.3 数据库配置文件

dal-local.properties

1
2
3
4
5
6
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/yjf_cs?useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull
username=root
password=root
#数据库schema名
changelogSchemaName=yjf_cs

建议大家把原来的数据库配置文件中的key改为和这个一致,没必要搞多份配置.

3.路径结构

确保xxx-dal如下的路径结构,请大家统一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|---pom.xml
|---src
|    |---main
|    |    |---java
|    |    |---resources
|    |    |        |---db
|    |    |        |    |---changelog
|    |    |        |    |        |---db.1.0.sql
|    |    |        |    |        |---db.2.0.sql
|    |    |        |    |        |---db.master.xml
|    |    |        |    |---config
|    |    |        |    |     |---dal-dev.properties
|    |    |        |    |     |---dal-local.properties
|    |    |        |    |     |---dal-net.properties
|    |    |        |    |     |---dal-online.properties
|    |    |        |    |     |---dal-pnet.properties
|    |    |        |    |     |---dal-sdev.properties
|    |    |        |    |     |---dal-snet.properties
|    |    |        |    |     |---dal-stest.properties
|    |    |        |    |     |---dal-test.properties

4.执行

在cs-dal目录执行:

1
mvn liquibase:update -Dspring.profiles.active=local

上面的脚本会以local环境执行,读取

1
dal-local.properties
中的数据库配置,执行数据库变更脚本.执行成功后,会在数据库中新建两个表.
1
DATABASECHANGELOG
会记录数据库变更信息,
1
DATABASECHANGELOGLOCK
用于防止多台服务器同时部署时的并发问题.

5.注意事项

  1. 先不要搞线上.
  2. 变更脚本命名:我现在做得简单
    1
    db.1.0.sql
    
    ,最好版本号为项目版本号,便于跟踪.
  3. 新项目建议采用此方案,跟踪数据库所有的开发变动.
  4. 老项目可以采用全量的方式使用.全量,先根据数据库的基础数据生成变更脚本generating_changelogs,然后同步版本(changeLogSync)到数据库中.这样做的好处是,以后可以从无到有的创建当前版本的数据库了.参考Adding Liquibase on an Existing project
  5. 老项目也可以采用增量的方式使用,增量的方式不会管以前的数据版本.如果采用这种方式,在新环境搭建数据库,你需要先用数据库工具还原到没有版本之前的状态,然后再执行变更脚本.参考Adding Liquibase on an Existing project
  6. 请不要修改(脚本内容/脚本路径)之前的数据库变更脚本,liquibase会对每个Changesets生成摘要,执行时会去对比,如果你修改了以前的Changesets,会报错(所有的变更在事务中执行,出错了会回滚,不用担心会影响到数据库).
  7. 官方文档很全,想深入的同学请阅读FAQ/BEST PRACTICES/Maven Liquibase Plugin.遇到问题之前先检查配置是否正确,有bug可以找我^_^.

很早之前写的一篇文字,一直没有搬上blog,以后会慢慢把有些东西放到blog上来.

webservice的性能实在是敢恭维。曾经因为webservice吞吐量上不去,对webservice进行了一些性能方面的优化:

1.分析

1.1 FastInfoset

采用了FastInfoset(简称FI),效果很明显,极端条件下的大数据量传输,性能提高60%,他可以减少传输成本,序列化成本和xml解析成本。cxf基于http协商机制(检查请求header中

1
Accept: application/fastinfoset
)来启用FI。

1.2 Gzip

客户端和服务器端是否使用Gzip压缩,也是基于http协议协商的(检查请求header 中是否有

1
Accept-encoding:gzip
)。但是这里需要仔细权衡下。对于小数据量,启用gzip压缩支持是吃力不讨好的行为,数据量很小的时候,gzip压缩结果不明显,还浪费cpu。

1.3 unexpected element异常

见:http://bohr.me/cxf-unexpected-element/

1.4 处理过程分析

cxf 中通过一些列interceptor来完成数据解析处理操作,每个interceptor绑定到特定的阶段,下面是GZIP 和FI interceptor所处的阶段

类型 Direction Phase
Gzip IN Phase.RECEIVE
  Out Phase.PREPARE_SEND
FI IN Phase.POST_STREAM
  Out Phase.PRE_STREAM

数据进来时,先

1
RECEIVE
阶段适配InputStream对象为GZIPInputStream,然后在
1
POST_STREAM
阶段解析数据。完成gzip解压缩,FI解析数据过程。

数据出去时,在

1
PREPARE_SEND
阶段适配OutputStream对象为GZipThresholdOutputStream,在
1
PRE_STREAM
阶段再序列化为二进制数据传输出去。完成FI序列化数据,GZIP压缩数据过程。

测试发送20250byte数据,仅仅启用FI时,发送数据量为20181byte,再启用Gzip压缩后,发送数据量为258byte。

2.操作步骤

2.1添加依赖

cxf版本修改为2.7.0并加入FastInfoset

1
2
3
4
5
<dependency>
	<groupId>com.sun.xml.fastinfoset</groupId>
	<artifactId>FastInfoset</artifactId>
	<version>1.2.9</version>
</dependency>

2.2 修改cxf配置

2.2.1 删除引入的cxf配置

1
2
3
<import resource="classpath:META-INF/cxf/cxf.xml" />
<import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" />
<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

我们项目中很多spring配置文件都加入了上面的东东,这个不是必须的,不删除这东东会导致配置不生效。

2.2.2 配置gzip和FI

Spring配置文件中引入cxf namespace

1
2
xmlns:cxf=http://cxf.apache.org/core和xsi:schemaLocation
http://cxf.apache.org/core  http://cxf.apache.org/schemas/core.xsd

然后加入配置

1
2
3
4
5
6
7
8
9
10
<cxf:bus>
	<cxf:features>
		<cxf:fastinfoset force="false" />
		<bean class="org.apache.cxf.transport.common.gzip.GZIPFeature">
			<property name="threshold">
				<value>2048</value>
			</property>
		</bean>
	</cxf:features>
</cxf:bus>

注意这些特性client和server端都要配置。

3.写在最后

启用

1
gzip
1
FastInfoset
,性能基本上也到达webservice的极致了.通过
1
IgnoreUnexpectedElementValidationEventHandler
再解决易用性问题,基本完美.