使用Netty发送大型Java对象
我是Netty的新手,使用版本4。在我的项目中,服务器返回客户机一个Java对象,该对象可以很大。为此,我首先使用了ObjectEncoder/解码器和NioSocketChannel。虽然它可以工作,但性能明显低于使用旧的阻塞IO时的性能。线程转储表明ObjectEncoder一直在重新分配直接缓冲区。我猜它是在直接缓冲区中序列化整个对象,然后才通过网络发送它。这很慢,如果有多个这样的请求同时运行,可能会导致OutOfMemoryError。对于快速且使用有限大小缓冲区的高效实现,您有什么建议?此外,服务器返回的某些(但不是全部)对象包含一个长字节数组字段。这一事实能否用于进一步提高性能 正如@MattBakaitis所问的,我正在粘贴代码示例,这是对ObjectEchoServer示例的一个轻微修改。它将一个常量大对象发送回客户端,以响应接收到的消息使用Netty发送大型Java对象,netty,Netty,我是Netty的新手,使用版本4。在我的项目中,服务器返回客户机一个Java对象,该对象可以很大。为此,我首先使用了ObjectEncoder/解码器和NioSocketChannel。虽然它可以工作,但性能明显低于使用旧的阻塞IO时的性能。线程转储表明ObjectEncoder一直在重新分配直接缓冲区。我猜它是在直接缓冲区中序列化整个对象,然后才通过网络发送它。这很慢,如果有多个这样的请求同时运行,可能会导致OutOfMemoryError。对于快速且使用有限大小缓冲区的高效实现,您有什么建议
public final class MyObjectEchoServer {
static final int PORT = Integer.parseInt(System.getProperty("port", "11000"));
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(
new ObjectEncoder(),
new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)),
new ObjectEchoServerHandler());
}
});
// Bind and start to accept incoming connections.
b.bind(PORT).sync().channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class ObjectEchoServerHandler extends ChannelInboundHandlerAdapter {
public static class Response implements Serializable {
public byte[] bytes;
}
private static Response response;
static {
int len = 256 * 1024 * 1024;
response = new Response();
response.bytes = new byte[len];
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println("Received: msg=" + msg);
// Echo back the received object to the client.
System.out.println("Sending response. length: " + response.bytes.length);
ctx.write(response);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
System.out.println("Flushing");
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
如果您正在编写一个大对象,正如您在问题中提到的,可能是多个内存拷贝恰好扩展了输出缓冲区。要解决该问题,您可以覆盖
ObjectEncoder
中的allocateBuffer()
方法(messagetobytecoder
正确),并分配具有更高初始容量的缓冲区。例如:
@Override
protected ByteBuf allocateBuffer(
ChannelHandlerContext ctx, Object msg, boolean preferDirect) {
return ctx.alloc().heapBuffer(1048576);
}
为了进一步减少内存拷贝数,我建议使用直接缓冲区(即ctx.alloc().directBuffer(1048576)
)和pooledbytebuf分配器
)
但是,这并不能解决由于许多对等方交换一个大对象而导致在负载下获取OutOfMemoryError
的问题。Java对象序列化从未实现为与非阻塞连接一起工作;它总是假定流有数据。如果不重新实现对象序列化,就不可能不将整个对象保留在缓冲区中。实际上,这适用于仅使用InputStream
和OutputStream
的其他对象序列化实现
或者,您可以实现一个用于交换大数据流的替代协议,并通过使用对数据流的一些引用来减小对象的大小。几年前,我遇到了一个类似的问题,然后我开始检查通过有线发送的对象。我发现大多数实例变量都是原语,只有5%是另一个对象。 我尝试了JAVA序列化,提取了这些对象的字节[],然后将其包装到ByteBuf并发送到wire。示例代码可以是:
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.close();
return bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
然后进行反序列化:
ByteArrayInputStream ins = new ByteArrayInputStream(b);
ObjectInputStream ois = new ObjectInputStream(ins);
return ois.readObject();
对象的平均大小约为800字节,使用了1GBPS的链接。这对于每秒数千个事务都可以正常工作。现在,如果您想将速度提高到每秒一些LAC,您需要付出一些努力,并通过自己的方式序列化这些对象
下面是这样做的示例:
我的对象->
public class MyObject{
private int i;
private String key;
private AnotherObject obj;
private boolean toUpdate;
public MyObject(int i,String key, AnotherObject obj, boolean toUpdate) {
this.i=i;
this.key = key;
this.obj= obj;
this.toUpdate = toUpdate;
}
/**
* Decode in order:
int i
String key
AnotherObject obj
boolean toUpdate
*
*/
public Object decodeAll(ByteBuf b) {
this.i = readInt(b);
this.key = readString(b);
if (b.readableBytes() > 1) {
AnotherObject obj= new AnotherObject ();
this.value = AnotherObject .decodeAll(b);
}
byte[] bool = new byte[1];
b.readBytes(bool);
this.toUpdate = decodeBoolean(bool);
return this;
}
/**
* Encode in order:
int i
String key
AnotherObject obj
boolean toUpdate
*
*/
public ByteBuf encodeAll() {
ByteBuf buffer = allocator.buffer();
//First the int
writeInt(buffer, this.i);
// String key
writeString(buffer, this.key);
// AnotherObject
if (this.value != null) {
ByteBuf rel = this.value.encodeAll();
buffer.writeBytes(rel);
rel.release();
}
// boolean toUpdate
buffer.writeBytes(encodeBoolean(this.toUpdate));
return buffer;
}
}
在这里,我使用sun.misc.Unsafe API来序列化/反序列化:
protected byte[] encodeBoolean(final boolean value) {
byte[] b = new byte[1];
unsafe.putBoolean(b, byteArrayOffset, value);
return b;
}
public static boolean decodeBoolean(final byte[] b) {
boolean value = unsafe.getBoolean(b, byteArrayOffset);
return value;
}
protected byte[] encodeInt(final int value) {
byte[] b = new byte[4];
unsafe.putInt(b, byteArrayOffset, value);
return b;
}
protected void writeString(ByteBuf buffer, String data) {
if (data == null) {
data = new String();
}
// For String , first write the length as integer and then actual data
buffer.writeBytes(encodeInt(data.getBytes().length));
buffer.writeBytes(data.getBytes());
}
public static String readString(ByteBuf data) {
byte[] temp = new byte[4];
data.readBytes(temp);
int len = decodeInt(temp);
if (len == 0) {
return null;
} else {
temp = new byte[len];
data.readBytes(temp);
return new String(temp);
}
}
同样,您也可以对任何java数据结构进行编码/解码。i、 e
protected void writeArrayList(ByteBuf buffer, ArrayList<String> data) {
if (data == null) {
data = new ArrayList<String>();
}
// Write the number of elements and then all elements one-by-one
buffer.writeBytes(encodeInt(data.size()));
for (String str : data) {
writeString(buffer, str);
}
}
protected ArrayList<String> readArrayList(ByteBuf data) {
byte[] temp = new byte[4];
data.readBytes(temp);
int noOfElements = decodeInt(temp, 0);
ArrayList<String> arr = new ArrayList<>(noOfElements);
for (int i = 0; i < noOfElements; i++) {
arr.add(readString(data));
}
return arr;
}
protectedvoid writeArrayList(字节缓冲区、数组列表数据){
如果(数据==null){
数据=新的ArrayList();
}
//写下元素的数量,然后逐个写下所有元素
writeBytes(encodeInt(data.size());
for(字符串str:data){
writeString(缓冲区,str);
}
}
受保护的ArrayList readArrayList(ByteBuf数据){
字节[]临时=新字节[4];
数据读取字节(temp);
int noOfElements=decodeInt(温度,0);
ArrayList arr=新的ArrayList(noOfElements);
for(int i=0;i
现在,您只需将ByteBuf写入通道,即可获得每秒事务的LAC。如果您可以包含您认为导致错误的代码或配置,您将获得更好的结果和答案。这个问题相当广泛,可能很难回答。@MattBakaitis,我用代码示例和线程堆栈更新了这篇文章。谢谢@trustin。allocateBuffer是4.0.22中的一种新方法。我使用的是4.0.20,其中的代码隐藏在write方法中。我现在明白了为什么ObjectOutputStream比AbstractByteBuf对大型对象的序列化工作得更快。ObjectOutputStream每次需要增长时都会将底层数组的大小增加一倍,而AbstractByteBuf则采取更谨慎的内存策略,如果旧数组的大小超过4MB阈值,则会将其大小增加4MB。这会导致在出现大型对象时产生大量内存拷贝。
protected void writeArrayList(ByteBuf buffer, ArrayList<String> data) {
if (data == null) {
data = new ArrayList<String>();
}
// Write the number of elements and then all elements one-by-one
buffer.writeBytes(encodeInt(data.size()));
for (String str : data) {
writeString(buffer, str);
}
}
protected ArrayList<String> readArrayList(ByteBuf data) {
byte[] temp = new byte[4];
data.readBytes(temp);
int noOfElements = decodeInt(temp, 0);
ArrayList<String> arr = new ArrayList<>(noOfElements);
for (int i = 0; i < noOfElements; i++) {
arr.add(readString(data));
}
return arr;
}