RPC性能优化

优化 1:元数据共享

hessian 序列化会将两种信息写到输出流:

  1. 元数据:即类全名,字段名
  2. 值数据:即各个字段对应值(如果字段是复杂类型,则会递归传递该复杂类型
    的元数据和内部字段的值数据)
    在 hessian1 协议里,每次写出 Class  A 的实例时,都会写出 Class  A 的元
    数据和值数据,就是会重复传输相同的元数据。针对这点,hessian2 协议做了一个
    优化就是:在“同一次序列化上下文”里,如果存在 Class  A 的多个实例,只会对
    Class  A 的元数据传输一次。该元数据会在对端被缓存起来重复使用,下次再序列化
    Class  A 的对象时,只需要先写出对元数据的一个引用句柄(缓存中的 index,用一
    个 int 表示),然后直接写出值数据即可。接受方通过元数据句柄即可知道后面的值数
    据对应的类型。
    这是一个极大的提升。因为编码字段名字(就是字符串)所需的字节数很可能比
    它对应的值(可能只是一个 byte)更多。
    不过在官方的 hessian 里,这个优化有两个限制:
  3. 序列化过程中类型对应的 Class 结构不能改变
  4. 元数据引用只能在“同一个序列化上下文”,这里的“上下文”就是指同一
    个 HessianOutput 和 HessianInput。因为元数据的 id 分配和缓存分别是在
    HessianOutput 和 HessianInput 里进行的
    限制 1 我们可以接受,一般 DO 不会再运行时改变。但是限制 2 不太友好,因
    为针对每次请求的序列化和反序列化,HSF 都需要使用全新构造的 HessianOutput
    和 HessianInput。这就导致每次请求都需要重新发送上次请求已经发送过的元数据。
    针对限制 2,HSF 实现了跨请求元数据共享,这样只要发送过一次元数据,以
    后就再也不用发送了,进一步减少传输的数据量。实现机制如下:
  5. 修改 hessian 代码,将元数据 id 分配和缓存的数据结构从 HessianOutput
    和 HessianInput 剥离出来。
  6. 修改 HSF 代码,将上述剥离出来的数据结构作为连接级别的上下文保存起来。
  7. 每次构造 HessianOutput 和 HessianInput 时将其作为参数传入。这样就达
    了跨请求复用元数据的目的。
    该优化的效果取决于业务对象中,元数据所占的比例,如果“精心”构造对象,
    使得元数据所占比例很大,那么测试表现会很好,不过这没有意义。我们还是选取线
    上核心应用的真实业务对象来测试。从线上 tcp dump 了一个真实业务对象,测试同
    学以此编写测试用例得到测试数据如下:
  8. 新版本比老版本 CPU 利用率下降 10% 左右
  9. 新版本的网络流量相比老版本减少约 17%
    线上核心应用压测结果显示数据流量下降一般在 15%~20% 之间。

优化 2:UTF8 解码优化

hessian 传输的字符串都是 utf8 编码的,反序列化时需要进行解码。hessian
现行的解码方式是逐个字符进行。代码如下:

    private int parseUTF8Char() throws IOException {
        int ch = _offset < _length
                ? (_buffer[_offset++] & 0xff) : read();
        if (ch < 0x80)
            return ch;
        else if ((ch & 0xe0) == 0xc0) {
            int ch1 = read();
            int v = ((ch & 0x1f) << 6) + (ch1 & 0x3f);
            return v;
        } else if ((ch & 0xf0) == 0xe0) {
            int ch1 = read();
            int ch2 = read();
            int v = ((ch & 0x0f) << 12) + ((ch1 & 0x3f) << 6) + (ch2 & 0x3f);
            return v;
        } else
            throw error("bad utf-8 encoding at " + codeName(ch));
    }

UTF8 是变长编码,有三种格式:
1  byte  format:  0xxxxxxx
2  byte  format:  110xxxxx  10xxxxxx
3  byte  format:  1110xxxx  10xxxxxx  10xxxxxx
上面的代码是对每个字节,通过位运算判断属于哪一种格式,然后分别解析。
优化方式是:通过 unsafe 将 8 个字节作为一个 long 读取,然后通过一次位运
算判断这 8 个字节是否都是“1  byte  format”,如果是(很大概率是,因为常用的
ASCII 都是“1  byte  format”),则可以将 8 个字节直接解码返回。以前 8 次位运
算,现在只需要一次了。如果判断失败,则按老的方式,逐个字节进行解码。主要代
码如下:

private boolean parseUTF8Char_improved() throws IOException {
        while (_chunkLength > 0) {
            if (_offset >= _length && !readBuffer()) {
                return false;
            }
            int sizeOfBufferedBytes = _length - _offset;
            int toRead =
                    sizeOfBufferedBytes <= _chunkLength ? sizeOfBufferedBytes : _chunkLength;
            // fast path for ASCII
            int numLongs = toRead >> 3;
            for (int i = 0; i < numLongs; i++) {
                long currentOffset = baseOffset + _offset;
                long test =
                        unsafe.getLong(_buffer, currentOffset);
                if ((test & 0x8080808080808080L) == 0L) {
                    _chunkLength -=
                            8;
                    toRead -= 8;
                    for (int j = 0; j < 8; j++) {
                        _sbuf.append((char) (_
                                buffer[_offset++]));
                    }
                } else {
                    break;
                }
                for (int i = 0; i < toRead; i++) {
                    _chunkLength--;
                    int ch = (_buffer[_offset++] & 0xff);
                    if (ch < 0x80) {
                        _sbuf.append((char) ch);
                    } else if ((ch & 0xe0) == 0xc0) {
                        int ch1 = read();
                        int v = ((ch & 0x1f) << 6) + (ch1 & 0x3f);
                        _sbuf.append((char) v);
                    } else if ((ch & 0xf0) == 0xe0) {
                        int ch1 = read();
                        int ch2 = read();
                        int v = ((ch & 0x0f) << 12) + ((ch1 & 0x3f) << 6) + (ch2 & 0x3f);
                        _sbuf.append((char) v);
                    } else
                        throw error("bad utf-8 encoding at " + codeName(ch));
                }
            }
            return true;
        }
    }

同样使用线上 dump 的业务对象进行对比,测试结果显示该优化带来了 17% 的
性能提升:
time: 981
improved utf8 decode time: 810
(981-810)/981 = 0.1743119266055046

优化 4: map 操作数组化

大型系统里多个模块间经常通过 Map 来交互信息,互相只需要耦合 String 类型
的 key。常见代码如下:

public static final String key = "mykey";
Map<String,Object> attributeMap = new HashMap<String,Object>();
Object value = attributeMap.get(key);

大量的 Map 操作也是性能的一大消耗点。HSF 今年尝试将 Map 操作进行了优
化,改进为数组操作,避免了 Map 操作消耗。新的范例代码如下:

public static final AttributeNamespace ns = AttributeNamespace.
createNamespace("mynamespace");
public static final AttributeKey key = new AttributeKey(ns, "mykey");
DefaultAttributeMap attributeMap = new DefaultAttributeMap(ns, 8);
Object value = attributeMap.get(key);

工作机制简单说明如下:

  1. key 类型由 String 改为自定义的 AttributeKey,AttributeKey 会在初始化阶
    段就去 AttributeNamespace 申请一个固定 id
    2.map 类 型 由 HashMap 改 为 自 定 义 的 DefaultAttributeMap,DefaultAttributeMap
    内部使用数组存放数据
  2. 操作 DefaultAttributeMap 直接使用 AttributeKey 里存放的 id 作为 index 访
    问数组即可,避免了 hash 计算等一系列操作。核心就是将之前的字符串 key
    和一个固定的 id 对应起来,作为访问数组的 index
    对比 HashMap 和 DefaultAttributeMap,性能提升约 30%。

HashMap put time(ms) : 262
ArrayMap put time(ms) : 185
HashMap get time(ms) : 184
ArrayMap get time(ms) : 126

摘自阿里双11技术文档

转载于:https://www.cnblogs.com/james0/p/8343928.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值