インターネットには、nettyとprotostuffを使用してrpcオブジェクトを送信するデモがたくさんあります。それらのほとんどは、型から彫り出されています。最初に1つもコピーしました。ローカルテストは妨げられておらず、異常は発生しませんでした。
プレリリース環境を展開した後、ストレステストの後、多くの問題があり、さまざまなエラーレポートが際限なく現れました。もちろん、私はストレステスト中に大量のデータを使用し、非常に集中的なリクエストを送信しました。1台のマシンが毎秒最初の100ミリ秒で20,000個のオブジェクトを送信し、900ミリ秒の間待機し、エンドレスループで送信します。合計40台のマシンがクライアントとして使用され、2台のネットワークマシンが同時に送信されます。サーバーサーバーはオブジェクトを送信するため、平均して、各サーバーは1秒あたり約400,000のオブジェクトを受信します。ビジネスロジックの背後にあるため、ロジックは1秒あたり350,000の実際の測定値しか処理できません。
インターネット上のコードは何度も変更され、繰り返しテストされています。最終的に、エラーや例外は発生していません。1台のマシンが1秒あたり350,000を超えるオブジェクトを受け取ることができるため、記事を記述して記録します。テキスト内のコードは、オンラインロジックと一致します。
プロトスタッフのシリアライズとデシリアライズ
これは特別なことではありません。オンラインでツールを見つけてください。
pomを導入する
<protostuff.version>1.7.2</protostuff.version>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>${protostuff.version}</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>${protostuff.version}</version>
</dependency>
public class ProtostuffUtils {
/**
* 避免每次序列化都重新申请Buffer空间
* 这句话在实际生产上没有意义,耗时减少的极小,但高并发下,如果还用这个buffer,会报异常说buffer还没清空,就又被使用了
*/
// private static LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
/**
* 缓存Schema
*/
private static Map<Class<?>, Schema<?>> schemaCache = new ConcurrentHashMap<>();
/**
* 序列化方法,把指定对象序列化成字节数组
*
* @param obj
* @param <T>
* @return
*/
@SuppressWarnings("unchecked")
public static <T> byte[] serialize(T obj) {
Class<T> clazz = (Class<T>) obj.getClass();
Schema<T> schema = getSchema(clazz);
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
byte[] data;
try {
data = ProtobufIOUtil.toByteArray(obj, schema, buffer);
// data = ProtostuffIOUtil.toByteArray(obj, schema, buffer);
} finally {
buffer.clear();
}
return data;
}
/**
* 反序列化方法,将字节数组反序列化成指定Class类型
*
* @param data
* @param clazz
* @param <T>
* @return
*/
public static <T> T deserialize(byte[] data, Class<T> clazz) {
Schema<T> schema = getSchema(clazz);
T obj = schema.newMessage();
ProtobufIOUtil.mergeFrom(data, obj, schema);
// ProtostuffIOUtil.mergeFrom(data, obj, schema);
return obj;
}
@SuppressWarnings("unchecked")
private static <T> Schema<T> getSchema(Class<T> clazz) {
Schema<T> schema = (Schema<T>) schemaCache.get(clazz);
if (Objects.isNull(schema)) {
//这个schema通过RuntimeSchema进行懒创建并缓存
//所以可以一直调用RuntimeSchema.getSchema(),这个方法是线程安全的
schema = RuntimeSchema.getSchema(clazz);
if (Objects.nonNull(schema)) {
schemaCache.put(clazz, schema);
}
}
return schema;
}
}
ここには穴があります。つまり、上部にあるほとんどのオンラインコードは静的バッファを使用しています。シングルスレッドの場合は問題ありません。マルチスレッディングの場合、1回の使用でバッファがクリアされずに別のスレッドで再び使用されるのは非常に簡単で、例外がスローされます。いわゆるバッファスペースを毎回適用することの回避、測定されたパフォーマンスへの影響は非常に小さいです。
また、2つのProtostuffIOUtilも異常があったためProtobufIOUtilに変更され、変更後も異常はありませんでした。
カスタムのシリアル化方法
デコーダーデコーダー:
import com.jd.platform.hotkey.common.model.HotKeyMsg;
import com.jd.platform.hotkey.common.tool.ProtostuffUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
/**
* @author wuweifeng
* @version 1.0
* @date 2020-07-29
*/
public class MsgDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> list) {
try {
byte[] body = new byte[in.readableBytes()]; //传输正常
in.readBytes(body);
list.add(ProtostuffUtils.deserialize(body, HotKeyMsg.class));
// if (in.readableBytes() < 4) {
// return;
// }
// in.markReaderIndex();
// int dataLength = in.readInt();
// if (dataLength < 0) {
// channelHandlerContext.close();
// }
// if (in.readableBytes() < dataLength) {
// in.resetReaderIndex();
// return;
// }
//
// byte[] data = new byte[dataLength];
// in.readBytes(data);
//
// Object obj = ProtostuffUtils.deserialize(data, HotKeyMsg.class);
// list.add(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
エンコーダー
import com.jd.platform.hotkey.common.model.HotKeyMsg;
import com.jd.platform.hotkey.common.tool.Constant;
import com.jd.platform.hotkey.common.tool.ProtostuffUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
/**
* @author wuweifeng
* @version 1.0
* @date 2020-07-30
*/
public class MsgEncoder extends MessageToByteEncoder {
@Override
public void encode(ChannelHandlerContext ctx, Object in, ByteBuf out) {
if (in instanceof HotKeyMsg) {
byte[] bytes = ProtostuffUtils.serialize(in);
byte[] delimiter = Constant.DELIMITER.getBytes();
byte[] total = new byte[bytes.length + delimiter.length];
System.arraycopy(bytes, 0, total, 0, bytes.length);
System.arraycopy(delimiter, 0, total, bytes.length, delimiter.length);
out.writeBytes(total);
}
}
}
まずDecoderデコーダーを見てください。これは、nettyがメッセージを受信した後にメッセージをデコードし、バイトをオブジェクト(カスタムHotKeyMsg)に変換するために使用されます。コメントアウトやコメントアウトした投稿がたくさんあり、インターネット上で見つかるはずの投稿はそのように書かれています。この方法自体は、通常のシーンでは問題なく、デコードも正常ですが、数十万の場合、問題が発生しやすくなります。そこで、このデコーダの前にDelimiterBasedFrameDecoderデリミタデコーダを追加しました。
メッセージが受信されると、まずデリミタデコーダが渡され、次にMsgDecoderになると、分離されたオブジェクトバイトストリームであり、protoツールクラスで直接逆シリアル化できます。Constant.DELIMITERは、区切り文字として使用する、私がカスタマイズした特別な文字列です。
エンコーダー、エンコーダーを見て、最初に送信するオブジェクトをProtostuffUtilsでbyte []にシリアル化してから、カスタムセパレーターを尾にぶら下げます。このようにして、オブジェクトが外部に送信されると、エンコーダーが使用され、セパレーターが追加されます。
対応するサーバー側コードは、おおよそ次のようになります。
その後、転送されたオブジェクトをハンドラーで直接使用できます。
クライアント側をもう一度見てください
これはサーバー側と同じですが、これらのコーデックも同じです。nettyとサーバー間の通信のため、同じオブジェクト定義を使用します。
ハンドラーについても同様です。
単一のマシンとクラスター
上記のすべてを書き込んだ後、実際にテストできます。クライアント、サーバーを起動し、エンドレスループを作成してこのオブジェクトをサーバーに送信し、オブジェクトを受信した後、オブジェクトをサーバーに直接送信できます。それを書き戻し、そのままクライアントに送信します。スムーズに実行され、1秒あたりN、000,000を送信しても問題はなく、コーデックは正常であり、クライアント側とサーバー側は比較的正常です。現在の前提では、ProtoBufのツールクラスは私のものと同じです。バッファは共有しないでください。インターネット上で見つかった記事は基本的にこの時点で終わりです。ランダムにいくつかのメッセージを送信することは問題ありません。ただし、実際には、この種のコードがオンラインになった後、ピットインされます。
実際、ローカルテストも非常に簡単です。サーバーと一緒にいくつかのクライアントを起動し、エンドレスループでオブジェクトを送信して、両端に例外があるかどうかを確認します。この場合、最初のクライアントとの違いは実際にはクライアント側では同じですが、サーバー側では同じです。以前は、同時に送信されたクライアントは1つだけでしたが、現在は2つのクライアントに同時に送信されています。注意しないと、このステップが発生します。問題があります。自分で試すことをお勧めします。
その後、さらに何かを追加してみましょう。2つのポートを持つ2つのサーバーをそれぞれ起動しました。実際には、2つの異なるサーバーサーバーがオンラインにあります。クライアントは、以下のコードに示すように、エンドレスループで同時に2つのサーバーにオブジェクトを送信します。
メッセージの送信には、通常、channel.writeAndFlush()を使用します。同期を削除し、コードを実行して確認できます。あなたは異常に投げられたしこりを見つけるでしょう。明らかに2つの異なるチャネルにメッセージを送信しましたが、同時に送信されたため、深刻なスティッキーパケットが発生しました。サーバーが受信するメッセージの多くは不規則であり、多数のエラーが報告されます。2つのチャンネル間の間隔が100msであれば、状況は解決されます。もちろん、最終的には同期を使用して同期的に送信できるため、例外はスローされません。
上記のコードはテスト済みで、40のクライアントと2つのサーバーで、各サーバーは平均で毎秒約400,000のオブジェクトを受信し、継続的かつ安定して実行できます。