分类目录归档:Java

Netty 长连接服务

Netty 是什么

Netty: http://netty.io/

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

官方的解释最精准了,期中最吸引人的就是高性能了。但是很多人会有这样的疑问:直接用 NIO 实现的话,一定会更快吧?就像我直接手写 JDBC 虽然代码量大了点,但是一定比 iBatis 快!

但是,如果了解 Netty 后你才会发现,这个还真不一定!

利用 Netty 而不用 NIO 直接写的优势有这些:

  • 高性能高扩展的架构设计,大部分情况下你只需要关注业务而不需要关注架构
  • Zero-Copy 技术尽量减少内存拷贝
  • 为 Linux 实现 Native 版 Socket
  • 写同一份代码,兼容 java 1.7 的 NIO2 和 1.7 之前版本的 NIO
  • Pooled Buffers 大大减轻声请 Buffer 和释放 Buffer 的压力
  • ……

特性太多,大家可以去看一下《Netty in Action》这本书了解更多。

另外,Netty 源码是一本很好的教科书!大家在使用的过程中可以多看看它的源码,非常棒!

 

瓶颈是什么

想要做一个长链服务的话,最终的目标是什么?而它的瓶颈又是什么?

其实目标主要就两个:

  1. 更多的连接
  2. 更高的 QPS

所以,下面就针对这连个目标来说说他们的难点和注意点吧。

 

更多的连接

非阻塞 IO

其实无论是用 Java NIO 还是用 Netty,达到百万连接都没有任何难度。因为它们都是非阻塞的 IO,不需要为每个连接创建一个线程了。

欲知详情,可以搜索一下BIO,NIO,AIO的相关知识点。

 

Java NIO 实现百万连接

ServerSocketChannel ssc = ServerSocketChannel.open();
Selector sel = Selector.open();

ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(8080));
SelectionKey key = ssc.register(sel, SelectionKey.OP_ACCEPT);

while(true) {
    sel.select();
    Iterator it = sel.selectedKeys().iterator();
    while(it.hasNext()) {
        SelectionKey skey = (SelectionKey)it.next();
        it.remove();
        if(skey.isAcceptable()) {
            ch = ssc.accept();
        }
    }
}

这段代码只会接受连过来的连接,不做任何操作,仅仅用来测试待机连接数极限。

大家可以看到这段代码是 NIO 的基本写法,没什么特别的。

 

Netty 实现百万连接

NioEventLoopGroup bossGroup =  new NioEventLoopGroup();
NioEventLoopGroup workerGroup= new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup);

bootstrap.channel( NioServerSocketChannel.class);

bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //todo: add handler
    }});
bootstrap.bind(8080).sync();

这段其实也是非常简单的 Netty 初始化代码。同样,为了实现百万连接根本没有什么特殊的地方。

 

瓶颈到底在哪

上面两种不同的实现都非常简单,没有任何难度,那有人肯定会问了:实现百万连接的瓶颈到底是什么?

其实只要 Java 中用的是非阻塞 IO(NIO 和 AIO 都算),那么它们都可以用单线程来实现大量的 Socket 连接。 不会像 BIO 那样为每个连接创建一个线程,因为代码层面不会成为瓶颈。

其实真正的瓶颈是在 Linux 内核配置上,默认的配置会限制全局最大打开文件数(Max Open Files)还会限制进程数。 所以需要对 linux 内核配置进行一定的修改才可以。

这个东西现在看似很简单,按照网上的配置改一下就行了,但是大家一定不知道第一个研究这个人有多难。

这里直接贴几篇文章,介绍了相关配置的修改方式:

构建C1000K的服务器

淘宝技术分享 HTTP长连接200万尝试及调优

 

如何验证

让服务器支持百万连接一点也不难,我们当时很快就搞定了一个测试服务端,但是最大的问题是,我怎么去验证这个服务器可以支撑百万连接呢?

我们用 Netty 写了一个测试客户端,它同样用了非阻塞 IO ,所以不用开大量的线程。 但是一台机器上的端口数是有限制的,用root权限的话,最多也就 6W 多个连接了。 所以我们这里用 Netty 写一个客户端,用尽单机所有的连接吧。

NioEventLoopGroup workerGroup =  new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel( NioSocketChannel.class);

b.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //todo:add handler
    }
    });

for (int k = 0; k < 60000; k++) {
    //请自行修改成服务端的IP
    b.connect(127.0.0.1, 8080);
}

代码同样很简单,只要连上就行了,不需要做任何其他的操作。

这样只要找到一台电脑启动这个程序即可。这里需要注意一点,客户端最好和服务端一样,修改一下 Linux 内核参数配置。

 

怎么去找那么多机器

按照上面的做法,单机最多可以有 6W 的连接,百万连接起码需要17台机器!

如何才能突破这个限制呢?其实这个限制来自于网卡。 我们后来通过使用虚拟机,并且把虚拟机的虚拟网卡配置成了桥接模式解决了问题。

根据物理机内存大小,单个物理机起码可以跑4-5个虚拟机,所以最终百万连接只要4台物理机就够了。

 

讨巧的做法

除了用虚拟机充分压榨机器资源外,还有一个非常讨巧的做法,这个做法也是我在验证过程中偶然发现的。

根据 TCP/IP 协议,任何一方发送FIN后就会启动正常的断开流程。而如果遇到网络瞬断的情况,连接并不会自动断开。

那我们是不是可以这样做?

  1. 启动服务端,千万别设置 Socket 的keep-alive属性,默认是不设置的
  2. 用虚拟机连接服务器
  3. 强制关闭虚拟机
  4. 修改虚拟机网卡的 MAC 地址,重新启动并连接服务器
  5. 服务端接受新的连接,并保持之前的连接不断

我们要验证的是服务端的极限,所以只要一直让服务端认为有那么多连接就行了,不是吗?

经过我们的试验后,这种方法和用真实的机器连接服务端的表现是一样的,因为服务端只是认为对方网络不好罢了,不会将你断开。

另外,禁用keep-alive是因为如果不禁用,Socket 连接会自动探测连接是否可用,如果不可用会强制断开。

 

更高的 QPS

由于 NIO 和 Netty 都是非阻塞 IO,所以无论有多少连接,都只需要少量的线程即可。而且 QPS 不会因为连接数的增长而降低(在内存足够的前提下)。

而且 Netty 本身设计得足够好了,Netty 不是高 QPS 的瓶颈。那高 QPS 的瓶颈是什么?

数据结构的设计!

 

如何优化数据结构

首先要熟悉各种数据结构的特点是必需的,但是在复杂的项目中,不是用了一个集合就可以搞定的,有时候往往是各种集合的组合使用。

既要做到高性能,还要做到一致性,还不能有死锁,这里难度真的不小…

我在这里总结的经验是,不要过早优化。优先考虑一致性,保证数据的准确,然后再去想办法优化性能。

因为一致性比性能重要得多,而且很多性能问题在量小和量大的时候,瓶颈完全会在不同的地方。 所以,我觉得最佳的做法是,编写过程中以一致性为主,性能为辅;代码完成后再去找那个 TOP1,然后去解决它!

 

解决 CPU 瓶颈

在做这个优化前,先在测试环境中去狠狠地压你的服务器,量小量大,天壤之别。

有了压力测试后,就需要用工具来发现性能瓶颈了!

我喜欢用的是 VisualVM,打开工具后看抽样器(Sample),根据自用时间(Self Time (CPU))倒序,排名第一的就是你需要去优化的点了!

备注:Sample 和 Profiler 有什么区别?前者是抽样,数据不是最准但是不影响性能;后者是统计准确,但是非常影响性能。 如果你的程序非常耗 CPU,那么尽量用 Sample,否则开启 Profiler 后降低性能,反而会影响准确性。

还记得我们项目第一次发现的瓶颈竟然是ConcurrentLinkedQueue这个类中的size()方法。 量小的时候没有影响,但是Queue很大的时候,它每次都是从头统计总数的,而这个size()方法我们又是非常频繁地调用的,所以对性能产生了影响。

size()的实现如下:

public int size() {
    int count = 0;
    for (Node<E> p = first(); p != null; p = succ(p))
    if (p.item != null)
    // Collection.size() spec says to max out
    if (++count == Integer.MAX_VALUE)
    break;
    return count;
}

后来我们通过额外使用一个AtomicInteger来计数,解决了问题。但是分离后岂不是做不到高一致性呢? 没关系,我们的这部分代码关心最终一致性,所以只要保证最终一致就可以了。

总之,具体案例要具体分析,不同的业务要用不同的实现。

 

解决 GC 瓶颈

GC 瓶颈也是 CPU 瓶颈的一部分,因为不合理的 GC 会大大影响 CPU 性能。

这里还是在用 VisualVM,但是你需要装一个插件:VisualGC

有了这个插件后,你就可以直观的看到 GC 活动情况了。

按照我们的理解,在压测的时候,有大量的 New GC 是很正常的,因为有大量的对象在创建和销毁。

但是一开始有很多 Old GC 就有点说不过去了!

后来发现,在我们压测环境中,因为 Netty 的 QPS 和连接数关联不大,所以我们只连接了少量的连接。内存分配得也不是很多。

而 JVM 中,默认的新生代和老生代的比例是1:2,所以大量的老生代被浪费了,新生代不够用。

通过调整 -XX:NewRatio 后,Old GC 有了显著的降低。

但是,生产环境又不一样了,生产环境不会有那么大的 QPS,但是连接会很多,连接相关的对象存活时间非常长,所以生产环境更应该分配更多的老生代。

总之,GC 优化和 CPU 优化一样,也需要不断调整,不断优化,不是一蹴而就的。

 

其他优化

如果你已经完成了自己的程序,那么一定要看看《Netty in Action》作者的这个网站:Netty Best Practices a.k.a Faster == Better

相信你会受益匪浅,经过里面提到的一些小小的优化后,我们的整体 QPS 提升了很多。

最后一点就是,java 1.7 比 java 1.6 性能高很多!因为 Netty 的编写风格是事件机制的,看似是 AIO。 可 java 1.6 是没有 AIO 的,java 1.7 是支持 AIO 的,所以如果用 java 1.7 的话,性能也会有显著提升。

 

最后成果

经过几周的不断压测和不断优化了,我们在一台16核、120G内存(JVM只分配8G)的机器上,用 java 1.6 达到了60万的连接和20万的QPS。

其实这还不是极限,JVM 只分配了8G内存,内存配置再大一点连接数还可以上去;

QPS 看似很高,System Load Average 很低,也就是说明瓶颈不在 CPU 也不在内存,那么应该是在 IO 了! 上面的 Linux 配置是为了达到百万连接而配置的,并没有针对我们自己的业务场景去做优化。

因为目前性能完全够用,线上单机 QPS 最多才 1W,所以我们先把精力放在了其他地方。 相信后面我们还会去继续优化这块的性能,期待 QPS 能有更大的突破!

IntelliJ IDEA 源值1.5已过时,将在未来所有版本中删除

原因:

IDEA默认把项目的源代码版本设置为jdk1.5,目标代码设置为jdk1.5

解决方案:

1修改Maven的Settings.xml文件添加如下内容

<profile>
  <id>jdk-1.8</id>
  <activation>
    <activeByDefault>true</activeByDefault>
    <jdk>1.8</jdk>
  </activation>
  <properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
  </properties>
</profile>

2在项目的pom.xml文件中添加:

<properties>
 <maven.compiler.source>1.8</maven.compiler.source>
  <maven.compiler.target>1.8</maven.compiler.target>
</properties>

3打开项目配置,设置Modules的Language Level为”8”

222222.png

最后按”Ctrl+Alt+S”打开设置,搜索”Java Compiler”,将默认jdk和当前modual的jdk版本切换为1.8即可

mvn 报错 The parameters ‘mainClass’

[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:1.6.0:java (default-cli) on project test_artifact: The parameters ‘mainClass’ for goal org.codehaus.mojo:exec-maven-plugin:1.6.0:java are
missing or invalid -> [Help 1]

因为插件还没配置,注意配置引导路径
解决方法:在pom文件添加如下代码  EchoServer 为包含main的类

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <version>1.6.0</version>
            <executions>
                <execution>
                    <goals>
                        <goal>java</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <mainClass>EchoServer</mainClass>
            </configuration>
        </plugin>
    </plugins>
</build>

Jfinal 里直接用dao或Db

Jfinal 里直接用dao或Db

需要启动DruidPlugin和ActiveRecordPlugin插件

然后start();

代码如下:

public static void startPlugin() {
PropKit.use(“config_jdbc.txt”);
DruidPlugin druidPlugin = new DruidPlugin(PropKit.get(“jdbcUrl”), PropKit.get(“user”), PropKit.get(“password”).trim());
ActiveRecordPlugin arp = new ActiveRecordPlugin(“mysql”,druidPlugin);
_MappingKit.mapping(arp);

druidPlugin.start();
arp.start();
}
然后再main方法里:

public static void main( String[] args ) {
startPlugin();
System.out.println(Db.queryStr(“select ‘hello’ from dual”));
}

Tomcat 内存配置

设置Tomcat启动的初始内存其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4。

可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置

三、实例,以下给出1G内存环境下Java jvm 的参数设置参考:

JAVA_OPTS=”-server -Xms800m -Xmx800m -XX:PermSize=64M -XX:MaxNewSize=256m -XX:MaxPermSize=128m -Djava.awt.headless=true ”

JAVA_OPTS=”-server -Xms768m -Xmx768m -XX:PermSize=128m -XX:MaxPermSize=256m -XX: NewSize=192m -XX:MaxNewSize=384m”

CATALINA_OPTS=”-server -Xms768m -Xmx768m -XX:PermSize=128m -XX:MaxPermSize=256m -XX:NewSize=192m -XX:MaxNewSize=384m”

Linux

在/usr/local/apache-tomcat-5.5.23/bin 目录下的catalina.sh添加:

JAVA_OPTS=’-Xms512m -Xmx1024m’要加“m”说明是MB,否则就是KB了,在启动tomcat时会 报内存不足。

-Xms:初始值-Xmx:最大值-Xmn:最小值

Windows

在catalina.bat最前面加入set JAVA_OPTS=-Xms128m -Xmx350m

如果用startup.bat启动tomcat,OK设置生效.够成功的分配200M内存.

但是如果不是执行startup.bat启动tomcat而是利用windows的系统服务启动tomcat服务,上面的设置就不生效了,就是说set JAVA_OPTS=-Xms128m -Xmx350m 没起作用.

JFinal 部署在tomcat下

环境配置就不说了,JFinal项目和java web项目本质上是一样的,所以部署方式也是相同的

1 进入/var/www目录,解WAR包,目录下会有WEB-INF,META-INF 等文件夹

2 修改  TOMCAT_HOME/conf/ server.xml 文件找到 Host 标记,在其中添加如下子标记:

<Context path=”” docBase=”/var/www” reloadable=”false” />

3 注意 Host 标记中的 appBase 属性不要去修改,让其为默认值 “webapps”

4 启动 tomcat

注意点:

1 不要把项目部署在TOMCAT_HOME/webapps 目录下,避免 tomcat 加载项目两次的坑,这个坑会引发一些莫名奇妙的问题,例如:老版本的tomcat中,多人同时登录后session产生混乱

2 以上配置中的 Context 标记的 path 属性一定要设置为 “” 而不是 “/”,否则也会跳入一个坑。 docBase 属性后面放项目名称的绝对地址

mysql与JFinal的数据关系

MySQL与jfinal在使用过程中,一定要弄清楚数据关系,否则容易发生类型不匹配的错误。

一、关系整理

为此,我根据jfinal的源码和mysql的数据字段类型,整理列表如下:

mysql                                                                                                jfinal                  例子                      编号
varchar, char, enum, set, text, tinytext, mediumtext, longtext                          String          getStr(“xxx”)          0001
int, integer, tinyint(n) n > 1, smallint, mediumint                                                     int             getInt(“xxx”)    0002
bigint, unsign int                                                                                                             long getLong(“xxx”) 0003
unsigned bigint                                                                                                            BigInteger getBigInteger(“xxx”) 0004
date, year                                                                                                                       Date getDate(“xxx”) 0005
time                                                                                                                                 Time getTime(“xxx”) 0006
timestamp, datetime                                                                                                   Timestamp getTimestamp(“xxx”) 0006
real, double                                                                                                                   Double getDouble(“xxx”) 0007
float                                                                                                                                 Float getFloat(“xxx”) 0008
bit, tinyint(1)                                                                                                                 Boolean getBoolean(“xxx”) 0009
decimal, numeric                                                                                                         BigDecimal getBigDecimal(“xxx”) 0010
binary, varbinary, tinyblob, blob, mediumblob, longblob                                   byte[] getBytes(“xxx”) 0011
extends from Number                                                                                                 Number getNumber(“xxx”) 0012
二、tinyint

在mysql中,tinyint(n)中的n对jfinal的数据转换有影响:

n>1时,jfinal要使用getInt。
n=1时,jfinal要使用getBoolean。
三、unsigned

在mysql中unsigned表示有无符号,举例来说:

unsigned int,表示数据>0,不包含负数,那么jfinal就要使用getLong。
unsigned bigint,表示数据>0,不包含负数,那么jfinal就要使用getBigInteger。
四、int(m)

关于mysql中int(m)中的m,这当然包含bigint(m)的m。

int[(m)]
有符号值:-2147683648 到2147683647(- 231 到231- 1)
无符号值:0到4294967295(0 到232 – 1) 4个字节
这意味着,如果你的数据字段是这样子的话

`uid` int(11) NOT NULL DEFAULT ‘0’ COMMENT ‘用户id’
1
1
那么m=11是正确的,因为考虑到“-”(负数),长度最大为11位。

而如果你的数据字段是这样子的话

`uid` int(11) unsigned NOT NULL COMMENT ‘用户id’
1
1
那么m=11是没有意义的,因为最大长度为10位,如果你比较专业的话,此时,你的数据字段就应该是这样子的

`uid` int(10) unsigned NOT NULL COMMENT ‘用户id’
1
1
五、jfinal的api

只看如下代码:

/**
* Get attribute of mysql type: int, integer, tinyint(n) n > 1, smallint, mediumint
*/
public Integer getInt(String attr) {
return (Integer)attrs.get(attr);
}

/**
* Get attribute of mysql type: bigint, unsign int
*/
public Long getLong(String attr) {
return (Long)attrs.get(attr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

1
2
3
4
5
6
7
8
9
10
11
12
13
方法很详细的介绍了应该匹配哪一种mysql数据类型。