月度存档: 九月 2013

decoder解码优化

decoder中需要使用byte[]接收数据并转为protobuf对象。

本来考虑的是使用netty自带的缓存功能或直接在每个decoder中分配一个byte[]。这时想起来了fastjson的作者提过使用的线程绑定的byte[]功能。

查找到源代码,摘出来用。也感谢fastjson的作者开源代码。

package cn.joylab.game.util;

import java.lang.ref.SoftReference;

/**
 * 提供线程绑定数组缓存功能。
 * 
 * 可以避免频繁创建byte数组,最大长度限制为128k,超过128k的数据不进行缓存。
 * 
 * 用例1:decoder.
 * 
 * @author ZhuKunqian
 * 
 */
public class ThreadLocalCache {
	private final static int BYTE_CACHE_INIT_SIZE = 1024;
	private final static int BYTE_CACHE_MAX_SIZE = 1024 * 128;
	private static final ThreadLocal<SoftReference<byte[]>> byteBufLocal = new ThreadLocal<SoftReference<byte[]>>();

	public static void clearCache() {
		byteBufLocal.set(null);
	}

	public static byte[] getBytes(int length) {
		SoftReference<byte[]> ref = byteBufLocal.get();
		if (ref == null) {
			return allocateBytes(length);
		}

		byte[] bytes = ref.get();
		if (bytes == null) {
			return allocateBytes(length);
		}
		if (bytes.length < length) {
			bytes = allocateBytes(length);
		}
		return bytes;
	}

	private static int getAllocateLength(int init, int max, int length) {
		int value = init;
		while (true) {
			if (value >= length) {
				return value;
			}
			value *= 2;
			if (value > max) {
				break;
			}
		}
		return length;
	}

	private static byte[] allocateBytes(int length) {
		int allocateLength = getAllocateLength(BYTE_CACHE_INIT_SIZE,
				BYTE_CACHE_MAX_SIZE, length);
		if (allocateLength <= BYTE_CACHE_MAX_SIZE) {
			byte[] bytes = new byte[allocateLength];
			byteBufLocal.set(new SoftReference<byte[]>(bytes));
			return bytes;
		}
		return new byte[length];
	}
}

 

db的选择

今天仔细看了下voltdb,全内存数据库。

在新项目选择上,主要考查了3个数据库mysql,mongodb,voltdb。

mysql就不需要多说了,传统的关系数据库,而且用了多年,也比较熟悉。

mongodb,nosql数据库,使用二进制json格式保存,支持sql语句。

voltdb,关系数据库,也可当做nosql数据库来用,全内存,支持sql语句。因为是全内存操作,所以速度是非常快的。

mysql,mangodb需要读写磁盘,所以相对要慢一些。但voltdb官方推荐是4G内存启动。虽然也可以1G内存启动起来,会有会有其它后果还不清楚。而且还有一个很恶心的问题,voltdb不像mysql那样或以多个database启动,一个voltdb实例,只是一个database。这还不算问题,如果要在一台机器上开启20个database,则无法像官方所说的那样,按4G内存分配。估计每个database只能分配1G。

但voltdb可以当作一个jar嵌入到工程中,这样可以避免线程中的数据传输,又是一个好处。

voltdb也提供了jdbc driver,因此新项目中可以尝试使用voltdb,如果发现有解决不了的问题,可以直接切换至mysql.

linux查看缺少的so连接库

ldd $(which /usr/local/nginx/sbin/nginx)

以上命令可以查看缺少的so连接库

webtool允许远程访问

webtool:start().启动后,无法远程访问,查看

start(Path,Data)->{ok,Pid}|{stop,Reason}

Types:
Path = string() | standard_path
Data = [Port,Address,Name] | PortNumber | standard_data
Port = {port,PortNumber}
Address = {bind_address,IpNumber}
Name = {server_name,ServerName}
PortNumber = integer()
IpNumber = tuple(), e.g. {127,0,0,1}
ServerName = string()
Pid = pid()

Use this function to start WebTool if the default port, ip-number,servername or path can not be used.

Path is the directory where the mime.types file and WebTool's own HTML files are located. By default this is webtool-<vsn>/priv, and in most cases there is no need to change this. If Path is set to standard_path the default will be used.

If Data is set to PortNumber, the default data will be used for ip-number (127.0.0.1) and server name (localhost).

需要设置bind_address为 0,0,0,0才能远程访问。

启动方式如下:

webtool:start(standard_path,[{port,8888},{bind_address,{0,0,0,0}},{server_name,”server”}]).

第一个erlang application

1、创建目录结构 doc、ebin、include、priv、src

2、在ebin下创建应用描述文件,这里应用取名为 first_app,则创建的描述文件为first_app.app

{application, first_app,
  [{description,  "hello world,first app"},
   {id,           "first_app"},
   {vsn,          "0.1.0"},
   {modules,      [
                        first_app,
                        first_app_sup
                        ]},
   {registered,   "first_app_sup"},
   {applications, [kernel,stdlib]},
   {mod,          {first_app,[]}},
   {start_phases, []}
  ]
}.

3、下面编写 src/first_app.erl,此文件的唯一任务就是启动根监督者。

-module(first_app).

-behaviour(application).

-export([start/2,stop/1]).

start(_StartType,_StartArgs)->
        case first_app_sup:start_link() of
                {ok,Pid} -> {ok,Pid};
                Other    -> {error,Other}
        end.

stop(_State)->
        ok.

4、实现监督者 src/first_app_sup.erl

-module(first_app_sup).

-behaviour(supervisor).

-export([start_link/0,start_child/2]).

-export([init/1]).

-define(SERVER,?MODULE).
start_link()->
        supervisor:start_link({local,?SERVER},?MODULE,[]).

start_child(Value,LeaseTime)->
        supervisor:start_child(?SERVER,[Value,LeaseTime]).

init([])->
        Element={
                first_app_tcp_listener,
                        {first_app_tcp_listener,start_link,[]},
                        permanent,10000,worker,
                        [first_app_tcp_listener]
                },
        Children=[Element],
        RestartStrategy={simple_one_for_one,0,1},
        {ok,{RestartStrategy,Children}}.

5、尝试运行:

先编译为beam

erlc -o ebin src/*.erl

再运行。

在erl中输入

application:start(first_app).

注意:simple_one_for_one需要手动去调用start_child才会产生子进程。

一篇nosql内存数据库的文章

http://www.csdn.net/article/2013-04-11/2814850-877000-tps-with-erlang-and-voltdb

1、voltdb安装很简单,解压即可。

在官网上提到有几点影响性能的系统设置:

http://voltdb.com/docs/EnterpriseReleaseNotes/

 

1.1. Disable Swapping
Swapping is an operating system feature that optimizes memory usage when running multiple processes. However, memory is a critical component of the VoltDB server process. Any contention for memory, including swapping, will have a very negative impact on performance and functionality.

We recommend using dedicated servers and disabling swapping when running the VoltDB database server process. Use the swapoff command to disable swapping on Linux systems. If swapping cannot be disabled for any reason, you can reduce the likelihood of VoltDB being swapped out by setting the kernel parameter vm.swappiness to zero.

1.2. Turn off TCP segmentation offload and generic receive offload if cluster stability is a problem.
There is an issue where, under certain conditions, the use of TCP segmentation offload (TSO) and generic receive offload (GRO) can cause nodes to randomly drop out of a cluster. The symptoms of this problem are that nodes timeout — that is, the rest of the cluster thinks they have failed — although the node is still running and no other network issues (such as a network partition) are the cause.

Disabling TSO and GRO is recommended for any VoltDB clusters that experience such instability. The commands to disable offloading are the following, where N is replaced by the number of the ethernet card:

ethtool -K eth<span class="emphasis"><em>N</em></span> tso off
ethtool -K eth<span class="emphasis"><em>N</em></span> gro off

Note that these commands disable offloading temporarily. You must issue these commands every time the node reboots.

 

 

1.2. Turn off TCP segmentation offload and generic receive offload if cluster stability is a problem.
There is an issue where, under certain conditions, the use of TCP segmentation offload (TSO) and generic receive offload (GRO) can cause nodes to randomly drop out of a cluster. The symptoms of this problem are that nodes timeout — that is, the rest of the cluster thinks they have failed — although the node is still running and no other network issues (such as a network partition) are the cause.

Disabling TSO and GRO is recommended for any VoltDB clusters that experience such instability. The commands to disable offloading are the following, where N is replaced by the number of the ethernet card:

ethtool -K eth<span class="emphasis"><em>N</em></span> tso off
ethtool -K eth<span class="emphasis"><em>N</em></span> gro off

Note that these commands disable offloading temporarily. You must issue these commands every time the node reboots.

erlang gen_server 模版

今天终于理解了gen_server行为模式。

这里放一个模版,以方便自己使用:

-module(test8).

-behaviour(gen_server).

-export([start_link/0,stop_link/0]).

-export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2,code_change/3]).

-define(SERVER,?MODULE).

%% 不知道这个state的作用是什么
-record(state,{}).
%% 给外面调用的。

%%% ----------------------------------------------
%%% 公开的API
%%% 
%%% ----------------------------------------------

start_link()->
        gen_server:start_link({local,?SERVER},?MODULE,[],[]).
stop_link()->
        gen_server:cast(?SERVER,stop).

%%% ---------------------------------------------
%%% 私有函数
%%% ---------------------------------------------

%%% -------------------------------------------
%%% 以下是回调函数
%%% -------------------------------------------

%% Result = {ok,State} | {ok,State,Timeout} | {ok,State,hibernate}
%% | {stop,Reason} | ignore
%% 这里设置timeout为0,根据<<erlang并发编程>>描述,可以让init/1尽快结束,是一个众所周知的技巧。
init([])->
        {ok,#state{},0}.

%% Result = {reply,Reply,NewState} | {reply,Reply,NewState,Timeout}
%%  | {reply,Reply,NewState,hibernate}
%%  | {noreply,NewState} | {noreply,NewState,Timeout}
%%  | {noreply,NewState,hibernate}
%%  | {stop,Reason,Reply,NewState} | {stop,Reason,NewState}
handle_call(Request,From,State)->
        Reply=ok,
        {reply,Reply,State}.

%% Result = {noreply,NewState} | {noreply,NewState,Timeout}
%%  | {noreply,NewState,hibernate}
%%  | {stop,Reason,NewState}
handle_cast(Request,State)->
        case Request of
                stop    -> {stop,normal,State};
                _       -> {noreplay,State}
        end.

%% Result = {noreply,NewState} | {noreply,NewState,Timeout}
%%  | {noreply,NewState,hibernate}
%%  | {stop,Reason,NewState}
handle_info(Info,State)->
        {noreply,State}.
terminate(Reason,State)->
        io:format("server is stopped"),
        ok.
code_change(OldVsn,State,Extra)->
        {ok,State}.

erlang蜗牛学习中(六)

有几多日未写erlang学习代码了。

前一段时间主要做了项目的优化,有几点效果挺不错,这里略提一下。

io很耗时,日志通过udp远程写到其它服务器上,会大量减少cpu的消耗。

nosql概念在游戏开中很合适,使用json或protobuf直接存储在key-value数据库中非常方便,采用后基本上无需任何持久层的工作量。而且nosql的设计方式对某些特定场景的处理可大幅提高性能,比如:以前查询玩家的信件,直接使用sql查询,需要查询整张表,采用nosql的设计方式,在玩家中存储一个数组,里面是信件的id,可以直接根据id去查询数据。

好了,来看erlang test7

-module(test7).

-export([start/0]).

start()->
        {ok,ListenSocket} = gen_tcp:listen(8889,[binary,{packet,2},{reuseaddr,true},{active,true}]),
        listen_tcp(ListenSocket),
        gen_tcp:close(ListenSocket),
        io:format("start end.").

listen_tcp(ListenSocket)->
        {ok,Socket}=gen_tcp:accept(ListenSocket),
        spawn_link(fun()-> socket_rec(Socket) end),
        listen_tcp(ListenSocket).

socket_rec(Socket)->
        receive
                {tcp,Socket,Data}->
                        io:format("receive data ~n"),
                        socket_rec(Socket);
                {tcp_closed,Socket}->
                        io:format("tcp_closed ~n"),
                        gen_tcp:close(Socket);
                {tcp_error,Socket,Reason}->
                        io:format("tcp_error ~n"),
                        gen_tcp:close(Socket) 

        end.

test7,打开一个socket监听端口,然后开启一个线程去处理接收数据。

好了,继续测试半主动式:接一个包,设置socket为不接收状态,然后处理完包后,再继续读取数据包。

如果开启了新的线程来接收数据。别忘记调用:

controlling_process(Socket, Pid) -> ok | {error, Reason}

Types:

Socket = socket()
Pid = pid()
Reason = closed | not_owner | inet:posix()

 

Assigns a new controlling process Pid to Socket. The controlling process is the process which receives messages from the socket. If called by any other process than the current controlling process, {error, not_owner} is returned.

他的作用是把一个新的线程和这个socket绑定,这样新线程可以接收到socket的数据。

也让我困惑了两天为什么接收不到数据。

服务器性能优化

今天完成了测试机器人,开始对服务器进行性能测试。

测试环境:

客户端:开始10000个连接,每个连接每秒发送一个包(业务逻辑只对内存操作)。

1、服务器内存1G,发现在连接达到1000个左右时,服务器接近假死状态。

查看gc: jstat -gcutil 21340 5000 999999

  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT   
  0.00 100.00 100.00 100.00  38.29    908   12.490   927 1793.564 1806.054
  0.00 100.00 100.00 100.00  38.29    908   12.490   928 1797.939 1810.429
  0.00 100.00 100.00 100.00  38.29    908   12.490   930 1800.529 1813.019
  0.00 100.00 100.00 100.00  38.29    908   12.490   933 1809.093 1821.583
  0.00 100.00 100.00 100.00  38.29    908   12.490   935 1813.729 1826.219
  0.00 100.00 100.00 100.00  38.29    908   12.490   938 1818.452 1830.942
  0.00 100.00 100.00 100.00  38.29    908   12.490   939 1822.462 1834.952
  0.00 100.00 100.00 100.00  38.29    908   12.490   941 1827.227 1839.717
  0.00 100.00 100.00 100.00  38.29    908   12.490   944 1834.183 1846.673
  0.00 100.00 100.00 100.00  38.29    908   12.490   946 1836.594 1849.085
  0.00 100.00 100.00 100.00  38.29    908   12.490   948 1842.616 1855.106
  0.00  99.99 100.00 100.00  38.29    908   12.490   951 1849.040 1861.531
  0.00 100.00 100.00 100.00  38.29    908   12.490   953 1854.135 1866.625
  0.00 100.00 100.00 100.00  38.29    908   12.490   956 1858.858 1871.348
  0.00 100.00 100.00 100.00  38.29    908   12.490   957 1862.671 1875.161
  0.00 100.00 100.00 100.00  38.29    908   12.490   959 1867.481 1879.971

看出full gc 次数频繁。

2、把内存改为2G再次测试,同时把log日志关闭(udp syslog发送,对性能影响应该不是很大,可以在最后再测试未关闭日志的情况)

这次效果明显,达到2000连接,也未产生full gc,连接数继续加大。

连接已经上升到5000,依旧无压力。

再继续往上升时,发现连续全断开了。

查看log,发现是超时断开了,设置的是2分钟超时。

3、继续解决超时的问题。

机器人连接主要操作是两步,一是登陆(相对来说比较消耗时间),二是发送指令(1秒发送1个,异步发送,速度非常快)。

我这里是创建了一个连接池来处理每个机器人。

在开始运行时,即生成10000个线程,然后依次执行登陆、发送指令,然后继续将自己放入连接池。

而问题则出现在了第一步,在开始登陆时,10000个线程全部放入了连接池,所以连接池需要全部分执行完这10000个线程的登陆,才会进入循环,不停发送指令。而这10000个线程的耗时超过了2分钟,导致大量连接被服务器主动断开。

找到问题后,就好解决了。

再创建一个连接池,所有完成登陆的连接都放入第二个连接池。这样,所有的登陆操作在第一个连接池中,完成登陆的连接在第二个连接池中。顺利解决问题。

4、在测试中发现,虽然使用了udp往rsyslog发送日志,但日志量如果过大,依旧会影响到性能。

5、OK,现在的结果比较理想。最大2G内存,通过probe查看实际消耗不到1G内存,连接数还可以继续往上提升。

在后台查看状态时,又出现问题了,每秒处理一条指令,1万个连接,每秒需要处理1万条指令,我开启了20个线程,发现根本处理不过来。指令队列中的指令越堆越多。

尝试把线程最大数量修改为50。

奇怪,修改为50线程后,没有再出现堆积指令队列的现象,再改为20看下。

果真,发现慢的原因了,游戏使用定时存储,在存储时,1万个玩家对象都要保存进去,才会导致指令队列变慢了。找到原因就好解决了,把定时存储放在另一个线程时,不要占用玩家指令的线程。

2G内存测试的1万个连接,最终稳定在7900个,流量5M左右。其中2千多个连接失败,不知道是因为我在是自己本本上开启1万个连接引起的,还是有其它bug,等下再测试。先试下3G内存开5万个连接。

找了两台PC,每台分别开启5000个连接,非常稳定,没有出现掉线现象。另一台连接提升至1万。有10来个连接断掉。看来连接断开不是服务器的原因,应该是PC上的连接数限制之类的有关。只是还没有查到具体限制。

gc也是很稳定。fullgc不是很频繁,对了,在测试中还发现异步存储原来和用户处理线程都放在同一个线程池,导致的在存储时阻塞了用户处理线程,发现后,专门为异步存储指令开始一个连接池,解决掉阻塞用户处理线程的问题。

2013.09.23 mina2改为netty4,继续进行优化。

3G内存,1万个连接,idle78%左右。网络流量发送74k/s,接收4.4M/S(未开启no_delay)

在压力测试中打开tcpnodelay,结果很多连接直接创建失败。统计了下大概有180个连接创建失败。

这次只是在客户端上打开了tcp_nodelay,查看流量没有什么变化,可能是因为1秒发送一个包,所以只是降低了延迟,对于流量并没有变化。还有可能是因为我在系统注册中已经把no_delay打开了,所以这个开关并没有生效。

再测试下服务器端打开nodelay.服务器打开nodelay后性能并无下降,返回感觉有稍微提升。

使用jprofiler进行性能分析(jprofiler官网上直接下,可以免费用10天,10天对于性能分析足够了)。

查看发现以下几种对象在内存中占用过多:

protobuf对象数量在百万以上。占用的内存达到1.5G左右。

现在开始频繁的出现fgc.

强制结束掉客户端,查看对象是否被回收。好吧,现在完全感觉是内存泄露了。

修改服务器处理程序,每次接收包后,设置autoread为false,处理完包后再设置为true.先跑2个小时看看情况再说。因为上次查看时,指令队列长度最多有400多了,这样可以限制指令队列不会太长,也可以避免以后恶意用户不间断发包把服务器卡住。

找到原因了,是因为数据包太多了,处理不过来,要发送的数据没有发送出去,造成数据不断积累,从而耗尽内存。

2013.09.26记:

1、decoder中的byte[]已经改为使用theadlocal缓存,大量减少了字节数组的创建。

2、去除了中间对象,直接在decoder中解析出ActionRunnable对象,同时此对象使用对象缓冲池回收,大量减少了创建对象的数量。

3、动手修改protobuf,能够缓存protobuf对象。

  1. 先从网上找到protoc的源码,动手修改、
  2. 添加reInit方法,这个方法和init不同,他对集合对象只执行clear,不重新创建一个list赋值。
  3. 添加parseFrom2,内部调用reinit方法并执行parseFrom(byte[])的功能。
  4. 修改完成后,gc情况大为改善。

4、动手修改protobuf中的toByteArray().

  1. 查看protobuf代码,发现一个protobuf中共用一个byte[],因此也可以使用threadlocal缓存进行优化。
                  public byte[] toByteArray() {
    			try {
    				final byte[] result = cn.joylab.game.net.cache.ThreadLocalCache.getBytesProtobuf(getSerializedSize());
    				final CodedOutputStream output = CodedOutputStream
    						.newInstance(result);
    				writeTo(output);
    				output.checkNoSpaceLeft();
    				return result;
    			} catch (IOException e) {
    				throw new RuntimeException(
    						"Serializing to a byte array threw an IOException "
    								+ "(should never happen).", e);
    			}
    		}
  2. 发现这几个方法都提供公开调用,这样不必去修改protoc直接从外部生成byte[].
    /**
    	 * 使用了线程缓存,避免了byte[]的大量创建
    	 * @param messageLite
    	 * @return
    	 * @throws IOException
    	 */
    	private byte[] toByteArray(MessageLite messageLite) throws IOException{
    		byte[] result = cn.joylab.game.net.cache.ThreadLocalCache
    				.getBytesProtobuf(messageLite.getSerializedSize());
    		CodedOutputStream output = CodedOutputStream.newInstance(result);
    		messageLite.writeTo(output);
    
    		return result;
    	}

5、动手修改protobuf中的Builder对象。

  1. 这里比较麻烦一些。因为分为builder对象及message对象。message对象由builder进行生成。为了减少对象产生。这里builder和message对象都需要进行缓存。
  2. builder对象比较方便,本身自带clear()方法,从缓存中取出直接clear()后就可以供后续使用
  3. message对象需要手动调用reInit清理
  4. builder在newBuilder中从缓存中取,在builder()后放入缓存,放入时调用clear()
  5. message对象在builder中取,在decoder完成后放入缓存。取出时调用reInit清理。
  6. 在toByteArray因为用的byte[]是缓存的,所以会遇到checkNoSpaceLeft无法通过。这里

2013.10.07 真奇怪,感觉文章丢失了几段内容,记不起来是丢失了哪几部分了。

这里先记录下国庆前了解的内容。

在每秒1万个数据包的压力下,1小时左右,jvm内存耗尽,无限开始fgc。

这里使用了java自带的内存对象分析工具

jmap -dump:format=b,file=dump1.dump 12377

将内存保存为文件。

然后使用:

jhat dump1.dump 可以使用小型http server来查看内存。

这里就是使用了这个工具来发现了netty自带的内存缓存分配造成的问题。jvm分配了3G内存,netty给自己的缓存分配了2G内存导致内存不足。

AbstractByteBufAllocator中的属性:

MAX_CHUNK_SIZE (I) : 1073741824
MIN_PAGE_SIZE (I) : 4096
DEFAULT_MAX_ORDER (I) : 11
DEFAULT_PAGE_SIZE (I) : 8192
DEFAULT_NUM_DIRECT_ARENA (I) : 8
DEFAULT_NUM_HEAP_ARENA (I) : 8

PoolChunk中分配了具体的内存:

16M的byte[].属性为:memoryMap

 

chunkSize (I) : 16777216
freeBytes (I) : 0

freeBytes为0,说明已经被分配了,为什么没有被释放掉。难道没有被使用,这样的话,需要先输出netty的statistics来查看原因。

可能找到内存未释放的原因了。我在decoder中写了两句测试代码,未被注释掉,导致内存不断被消耗而没有被释放掉。

对了,jetty有一个continuation技术,可以实现http短连接的io与业务逻辑处理线程分离,实现时,可以借签(同时和netty的http技术比较一下)。

java性能监控/优化工具

来源自:jworks.idv.tw/java/?p=13

在igame项目中,一个一个测试以上工具,看看效果如何。