最近在为公司做一个消息中心的项目,项目中使用了Apache的MINA 1.7的版本,为消息中心做通讯接口。
[size=13px; line-height: 20px; ][size=x-small;] Apache MINA(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架。当前发行的 MINA 版本支持基于 Java NIO 技术的 TCP/UDP 应用程序开发、串口通讯程序,MINA 所支持的功能也在进一步的扩展中。[/size][/size]
使用MINA组件确实可以提高通讯的并发性能以及可靠性,在1.7版本中,MINA封装了JDK中的ByteBuffer,虽然调用上方便了不少,但是如果不正当的使用它,容易引起性能上的问题,甚至会出现Out of Memory的问题。这次我就遇到了Out of Memory的问题,后来经过我的仔细分析和调试,终于发现了这条臭虫。
首先来看一下如何搭建MINA的TCP的服务端:
// output log to console
org.apache.log4j.BasicConfigurator.configure();
acceptor = new SocketAcceptor();
// Create a service configuration
SocketAcceptorConfig cfg = new SocketAcceptorConfig();
cfg.setReuseAddress(true);
// add a protocol codec, the protocol is wrapper from 0x00 to 0xff, and between 0x00 and 0xff is a message.
cfg.getFilterChain().addLast(
"protocolFilter",
new ProtocolCodecFilter(
new WrapperTagProtocolCodecFactory()));
// add logger filter
cfg.getFilterChain().addLast("logger", new LoggingFilter());
acceptor
.bind(new InetSocketAddress(PORT), new IoHandlerAdapter(){
@Override
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
// do something...
}
@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
session.write("response something").join();
}
}, cfg);
这段代码只是作为测试用例而写的,从代码中我们可以看到,代码中创建了SocketAcceptor对象,并且使用了自定义的ProtocolCodecFilter,这个通讯协议比较简单,协议格式为"<0x00><Message><0xff>",传输的消息被0x00和0xff两个字节包裹着。那么读取这条时,当读到0x00时开始,读取到0xff结束。当未读取到0x00时而接收到了一些数据,这些读取到的数据将被忽略。WrapperTagProtocolCodecFactory是一个工厂类,他将负责创建解码对象和编码对象,解码对象负责解释读取中的协议数据,并将协议数据转换为消息。而编码对象是将消息转换为协议数据。
Socket通讯是一个不可靠的消息传输通道,网络上传输的数据,并不是一次性就从客户端送到服务端,或者是服务端送到客户端,而是通过数据链路层,将数据送过来,这样会产生数据将有可能通过多次送到目的地。而MINA中的组件使用了ByteBuffer对象作为数据接收缓存,所以需要制定合理的通讯协议,来确保接收端收到的数据是完整的,可靠的。
当然如上的协议并不能完全的避免数据传输所产生的问题,因为还有可能产生数据丢包等其他的问题,但这个协议适合数据流小的消息。
当Socket通道的消息经过了协议的过滤器之后,还将经过LoggingFilter的过滤,这里实际上就是将收到的消息记录到Logger中。之后,将送给IoHandler处理,上面的代码通过匿名类继承了IoHandlerAdapter,为了方便测试,这里只重载了messageReceived方法。当MINA收到消息之后,立即发送回应的数据到客户端。
接下来,我们看看WrapperTagProtocolCodecFactory创建的两个对象:
public class WrapperTagProtocolCodecFactory extends
DemuxingProtocolCodecFactory {
public WrapperTagProtocolCodecFactory(){
super.register(WrapperTagDecoder.class);
super.register(WrapperTagEncoder.class);
}
public WrapperTagProtocolCodecFactory(String charset){
super.register(new WrapperTagDecoder(charset));
super.register(new WrapperTagEncoder(charset));
}
}
此工厂创建了WrapperTagDecoder(解码类)和WrapperTagEncoder(编码类)两个对象。
WrapperTagDecoder
public class WrapperTagDecoder extends MessageDecoderAdapter {
private String charset = "gb2312";
private Logger logger = Logger.getLogger(WrapperTagDecoder.class);
public WrapperTagDecoder(String charset){
this.charset = charset;
}
public WrapperTagDecoder(){
}
public MessageDecoderResult decodable(IoSession session, ByteBuffer in) {
return MessageDecoderResult.OK;
}
public MessageDecoderResult decode(IoSession session, ByteBuffer in,
ProtocolDecoderOutput out) throws Exception {
InputStream stream = in.asInputStream();
int head = stream.read();
if(head == -1){
throw new IOException("read -1 byte");
}
logger.info("Read head 0x00.");
int b = -1;
ByteArrayOutputStream os = new ByteArrayOutputStream();
while((b = stream.read()) != 0xff){
os.write(b);
}
logger.info("Read rear 0xff.");
String message = new String(os.toByteArray(), charset);
out.write(message);
logger.debug("Read message:" + message + ", charset:" + charset);
return MessageDecoderResult.OK;
}
}
此类继承了MessageDecoderAdapter类,待会我们来看这个类的问题。
WrapperTagEncoder
public class WrapperTagEncoder implements MessageEncoder {
private static final Set<Class<?>> TYPES;
private String charset = "gb2312";
static {
Set<Class<?>> types = new HashSet<Class<?>>();
types.add(String.class);
TYPES = Collections.unmodifiableSet(types);
}
public WrapperTagEncoder(String charset){
this.charset = charset;
}
public WrapperTagEncoder(){
}
public Set<Class<?>> getMessageTypes() {
return TYPES;
}
public void encode(IoSession session, Object message,
ProtocolEncoderOutput out) throws Exception {
ByteBuffer buf = ByteBuffer.allocate(256);
// Enable auto-expand for easier encoding
buf.setAutoExpand(true);
buf.put((byte)0x00);
buf.put(message.toString().getBytes(charset));
buf.put((byte)0xff);
buf.flip();
out.write(buf);
}
从实现上来看,WrapperTagEncoder比WrapperTagDecoder要来简单一些。WrapperTagEncoder只需要负责写就行了,而不需要控制多段数据的问题。那么上述的WrapperTagDecoder会出现什么问题呢?
MINA的Socket是基于Java的NIO而实现的非阻塞式的通讯,从WrapperTagDecoder代码我们看到ByteBuffer中提供了一个方法asInputStream,也就是将ByteBuffer转换为输入流,这个倒没什么问题,问题是在于读取的时候,使用了阻塞式的调用方法,stream.read()。如果接收的数据大于了ByteBuffer预分配的容量时,会报出:
org.apache.camel.CamelException: org.apache.mina.filter.codec.ProtocolDecoderException: java.lang.OutOfMemoryError: Java heap space (Hexdump: .....一堆ByteBuffer已读取到的数据,16进制显示)
据我分析,ByteBuffer会预先分配一定容量的堆空间,MINA的ByteBuffer应该是直接初始化了JVM底层中的堆空间,ByteBuffer默认的堆空间应该是1024byte,从ByteBuffer的asInputStream的InputStream的实现代码中可以看到,read方法实际上调用的是get()方法,如下:
@Override
public int read() {
if (ByteBuffer.this.hasRemaining()) {
return ByteBuffer.this.get() & 0xff;
} else {
return -1;
}
}
@Override
public int read(byte[] b, int off, int len) {
int remaining = ByteBuffer.this.remaining();
if (remaining > 0) {
int readBytes = Math.min(remaining, len);
ByteBuffer.this.get(b, off, readBytes);
return readBytes;
} else {
return -1;
}
}
实际上MINA的底层实现中,他会先将已读取到缓存中的ByteBuffer先扔出来给Filter,如果此时ByteBuffer已满了,那么就需要MINA会扩展ByteBuffer的容量,在扩展的过程中,就不应该再去调用任何有关get或者read的方法,由于实现中,不断的在使用read循环读取ByteBuffer中的数据,这样效率降低了,而且容易导致阻塞,本身Filter的生命周期的时间是有限的,这样会扰乱MINA本身的处理机制。如果继续想使用read方法获取数据,应该使用read(byte[] b),以块的形式读取,如果返回了-1,那么直接就返回出去,并将已读到消息,缓存起来,直到读取到0xff时,才清除掉缓存,并送到ProtocolDecoderOutput对象中,接着他就会交给IoHandler进行处理。下面是改良后的WrapperTagDecoder。
public class WrapperTagDecoder extends MessageDecoderAdapter {
private String charset = "gb2312";
private Logger logger = Logger.getLogger(WrapperTagDecoder.class);
private boolean readRear = false;
private boolean readHead = false;
private int receivedBufferSize = 1024;
private ByteArrayOutputStream os = new ByteArrayOutputStream();
public WrapperTagDecoder(String charset){
this.charset = charset;
}
public int getReceivedBufferSize() {
return receivedBufferSize;
}
public void setReceivedBufferSize(int receivedBufferSize) {
this.receivedBufferSize = receivedBufferSize;
}
public WrapperTagDecoder(){
}
public MessageDecoderResult decodable(IoSession session, ByteBuffer in) {
return MessageDecoderResult.OK;
}
public MessageDecoderResult decode(IoSession session, ByteBuffer in,
ProtocolDecoderOutput out) throws Exception {
InputStream stream = in.asInputStream();
if(!readHead){
int head = stream.read();
if(head == -1){
throw new IOException("read -1 byte");
}
readHead = true;
logger.info("Read head 0x00.");
}
byte[] buffer = new byte[receivedBufferSize];
int length = stream.read(buffer);
if(length == -1)
return MessageDecoderResult.OK;
if(buffer[length - 1] == -1){
readRear = true;
logger.info("Read rear 0xff.");
os.write(buffer, 0, length - 1);
}else{
os.write(buffer, 0, length);
}
if(readRear){
String message = new String(os.toByteArray(), charset);
out.write(message);
logger.debug("Read message:" + message + ", charset:" + charset);
readRear = false;
readHead = false;
os.reset();
}
return MessageDecoderResult.OK;
}
}
从使用上来看,看上去似乎还在使用阻塞式的方式去取,但是当你看到InputStream的实现时,你会发现,MINA将他封装了一层,内部还是调用这get的方法。InputStream其实是给方便习惯使用了阻塞式编程的程序员而设计的,我也是从这边慢慢的习惯过来,如果要深入了解非阻塞的编程尤其是MINA的组件,可以多看看网上的一些例子,最直接的方式就是去查看MINA其中的一个解析类,org.apache.mina.filter.codec.textline.TextLineCodecFactory。这里面将ByteBuffer运用的炉火纯青,尤其是对position和limit方法运用的是那么的到位,以及内部缓存,看的我是一直叫好。
不过这里还是提醒大家一句,如果是高性能的并发应用,MINA的ByteBuffer从性能上来看并不好到哪里去,还不如Java中的ByteBuffer对象,尤其是对postion的调用,性能上比Java本身的慢了不少。但是MINA 2.x已抛弃了ByteBuffer的设计,使用了更为有效的缓存方式,我建议大家还是使用2.x的版本,由于公司的项目使用了Apache Camel的组件,目前camel-mina的版本封装的是1.7的版本,期待Apache大佬们能够更新到2.x的版本。也希望遇到此问题的朋友们能够得到一点帮助。