Java版Akka-Rpc性能测试

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BossFriday

原创不易,请给作者打赏或点赞!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值