1. 背景
之前用Java实现了一个Java版Akka-Rpc,主要设计要求如下:
- 1、使用protostuff序列化。
- 2、使用Netty进行通讯。
- 3、路由策略:随机路由、指定Key路由、资源Id路由、强制路由。
- 4、使用ZK进行集群状态管理。
- 5、使用自定义注解进行服务注册及辅助控制(线程数量、方法名称设置等)
- 6、使用Disruptor实现收件箱、发件箱。
- 7、使用ThreadLocalRandom规避jdk8UUId性能有限问题。
其中1、2、6、7与性能相关,但最终的效果能达到一个什么水平,也没有数据支撑,于是基于Jmeter做了下性能测试。
2. 测试方法
2.1 测试方法
建了2个Actor:FooActor、FooServerActor。fooActor发送消息时将当前时间戳写入foo.timestamp字段,fooServer收到foo消息时,将当前时间戳 - foo.timestamp得到该条采样延时,并构造FooResult发送给fooActor,fooActor收到FooResult写采样日志。
FooActor、FooServerActor、Foo及FooResult定义如下:
@Data
@Builder
public class Foo {
private String id;
private String name;
private Integer age;
private Long timestamp;
}
@Data
public class FooResult {
private int code;
private String msg;
private Long time;
}
@ActorRoute(methods = FOO_METHOD_NAME)
public class FooActor extends TypedActor<FooResult> {
@Override
public void onMessageReceived(FooResult msg) throws Exception {
FooServerSampleLogWriter.writeSampleResultLog(System.currentTimeMillis(),
msg.getTime(),
"actorRpc-FooResult",
"200",
"ok",
Thread.currentThread().getName(),
true);
}
}
@ActorRoute(methods = FOO_SERVER_METHOD_NAME)
public class FooServerActor extends TypedActor<Foo> {
@Override
public void onMessageReceived(Foo msg) throws Exception {
long time = System.currentTimeMillis() - msg.getTimestamp();
FooResult result = new FooResult(200, "OK", time);
this.getSender().tell(result, ActorRef.noSender());
}
}
发送采样逻辑如下:
@NubyBearSamplerBuilder.SamplerType(behaviorName = Const.GUI_BEHAVIOR_ACTOR_RPC)
public class RpcSampler extends BaseSampler {
private static AtomicInteger sampleIndex;
private static ActorRef sender;
private static ServiceBootstrap serviceBootstrap;
private static ServiceConfig serviceConfig;
private static String sampleLabel;
public RpcSampler(SamplerConfig config) {
super(config);
}
@Override
public void testStarted() {
try {
sampleIndex = new AtomicInteger(0);
sampleLabel = config.getBehaviorName();
// init serviceBootstrap
ClusterNode clusterNode = new ClusterNode(config.getNodeName(), config.getVirtualNodesNum(), config.getHost(), config.getPort());
serviceConfig = new ServiceConfig(config.getSystemName(), config.getZkAddress(), clusterNode, null);
serviceBootstrap = new ServiceBootstrap() {
@Override
protected void start() throws Exception {
}
@Override
protected void stop() throws Exception {
}
};
serviceBootstrap.startup(serviceConfig);
// init sender
sender = ClusterRouterFactory.getClusterRouter().getActorSystem().actorOf(FOO_METHOD_NAME);
log.info("RpcSampler.testStarted() done.");
} catch (Exception ex) {
this.isTestStartedError = true;
log.error("RpcSampler.testStarted() error!", ex);
}
}
@Override
public SampleResult sample() {
SampleResult result = new SampleResult();
result.setSampleLabel(sampleLabel);
if (isTestStartedError)
return sampleFailedByTestStartedError();
try {
int i = sampleIndex.getAndIncrement();
Foo foo = Foo.builder().id(String.valueOf(i)).name(sampleLabel).age(i).timestamp(System.currentTimeMillis()).build();
result.sampleStart();
RoutableBean bean = RoutableBeanFactory.buildKeyRouteBean(String.valueOf(i), FOO_SERVER_METHOD_NAME, foo);
ClusterRouterFactory.getClusterRouter().routeMessage(bean, sender);
result.sampleEnd();
result.setSuccessful(true);
result.setResponseCode("200");
result.setResponseMessage("OK");
} catch (Exception ex) {
BaseSampler.setSampleResult("500", "Exception:" + ex.getMessage(), false, result);
log.error("RpcSampler.sample() error!", ex);
}
return result;
}
@Override
public void testEnded() {
}
}
2.2 服务器
云上开的一台8C/16G服务器,lscpu信息如下:
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel Xeon Processor (Skylake)
Stepping: 4
CPU MHz: 3192.498
BogoMIPS: 6384.99
Hypervisor vendor: KVM
Virtualization type: full
L1d cache: 32K
L1i cache: 32K
L2 cache: 4096K
L3 cache: 16384K
NUMA node0 CPU(s): 0-7
3. 测试结果
3.1 测试结果
从Jmeter生成的报告可以看出:TPS为:11万多、平均延时为:298毫秒、总采样数为:50万。
3.2 分析
除了开篇说的1、2、6、7与性能相关,由于使用Jmeter进行测试,写采样日志这件事情绕不过去。测试中每次采样都会写2个采样日志:RpcSampler中的Jmeter写的采样日志、FooActor中自己写的采样日志,因此,TPS为10万的情况下,每秒需要些20万采样日志。关于Jmeter自身写采样日志的机制没有研究过(不清楚是否有什么优化),FooActor中写日志的方式是采样BufferedWriter逐行写,然后每次都flush(bw.write(line + “\r\n”); bw.flush();)。从上述的分析中,我们可以感觉到写采样日志对本次的测试的干扰是比较大的,由于本次测试的目的只是想看看大致能达到的数量级,这里就不过多去纠结他们了,还是结合开篇说的几点简单分析下:
-
使用protostuff序列化
每次采样会涉及Foo,FooResult的序列化和反序列化,也就是说每次采样会有:2次序列化,2次反序列化。如果序列化和反序列化性能不高,那么将直接影响到RPC调用的整体性能表现。 -
使用Disruptor实现收件箱、发件箱
关于Disruptor为啥高性能,可以参考:https://blog.csdn.net/camelials/article/details/123492015 -
使用ThreadLocalRandom规避jdk8UUId性能有限问题
每次采样会有2次actor tell foo -> fooServer、 fooServer->foo。每次actor tell时都需要生成一个16字节的sessionId,其实本质就是UUID(long mostSigBits, long leastSigBits)。基于这种方式,目前基本的选择主要为以下4种,由于目前部署上主要还是java8居多,以及hutoolFastSimpleUUID与jdk8ThreadLocalRandomUUId性能相当,因此选择了后者。查了下ThreadLocalRandom的性能,大致的结果说是70多万+。
jdk8UUId
@Benchmark
public String jdk8UUId() {
return UUID.randomUUID().toString();
}
jdk8ThreadLocalRandomUUId
@Benchmark
public String jdk8ThreadLocalRandomUUId() {
ThreadLocalRandom random = ThreadLocalRandom.current();
UUID uuid = new UUID(random.nextInt(), random.nextInt());
return uuid.toString();
}
hutoolFastSimpleUUID
@Benchmark
public String hutoolFastSimpleUUID() {
return IdUtil.fastSimpleUUID();
}
micaUUId
@Benchmark
public String micaUUId() {
return StringUtil.getUUID();
}
3.3 总结
netty+protostuff的组合从选择上来目前也就这样了、Disruptor也是典范之作,目前能想到的优化空间就是高效UUID这一点上,由于目前已经使用ThreadLocalRandom,因此提升的空间也有限。另外,要获得较为精确的测试结果Jmeter显然不太合适了(光是写采样日志所产生的干扰就非常大),需要使用例如:JMH等基准测试框架了。由于测试结果数量级和ThreadLocalRandom相同(TPS10万级),那么大致认为代码整体性能损失还是可以接受的。
源码地址:
https://github.com/bossfriday/actor-rpc 原型项目(目前已不维护)
https://github.com/bossfriday/bossfriday-nubybear/tree/master/cn.bossfriday.common/src/main/java/cn/bossfriday/common/rpc