如何从零开发58的nodejs-rpc客户端

参考知识:

1、负载均衡

2、IO模型

3、Netty工作原理

4、TcpClient

5、如何解码java.jar生成js类文件

6、jvm字节码指令分析

背景:

2013年时,就听到过雪球网在11年的时候,通过node与java的通信完善了前后端分离的实践;到15年初的时候又看到github上分享出阿里巴巴这方面的实践,依托强大的阿里云与阿里大牛的技术支持下,15年阿里整站node化分层,并且当年双11没有出现一起node引起的重大事故,同时阿里也做了性能与工效对比,从阿里分享的资料来看,优势非常明显!

自从来到58的时候我就想着有一天能把这块技术推广!不为别的,只是因为我痛恨没有意义的浪费时间在一些重复性的劳动上,尤其还不能带来社会进步与个人进步的劳动上,对于公司来说也是浪费了很多的人效成本!应该都有体会:假设pm发出一个一般的需求来,前端开发、后端开发各自可能仅用2~4h开发完毕;但是当联调的时候却有可能花费两天以上的时间!

因为不是一个大脑,所以这段距离的交流上就有很长的时间要消耗!

国富论上有句话:“分工的出现能大大提升生产效率,不会虚耗时间在来回奔波上!”

那做这种rpc客户端,跟劳动分工有什么关系呢?同样分离的问题,我完全可以使用其他方式实现!比如http、react、thrift等等,确实有很多方式,而且像58集团也有很多公司在实践,比如英才采用了React、金融使用了http、而转转采用了直接调用node-java模块,都能达到前后端分离的效果,但是也同样有局限性!如果不考虑兼容ie、seo、效率、维护成本、现有状况、可移植性等等问题,确实也有很多的解决方式。比如企业级应用就完全可以使用React等!

详细说来很多可以讲的东西,说服他人确实是一件很难的事,也是推动好久才开始实施的!后来在与其他同事探讨的时候也很难说服!不再扯远了,可以这么理解,打通java与node的通信,能很好的保证业务的再造性,也能很好的分离前后端,包括单页面渲染等。

有一个很好的时间节点,虽然推动了好久才开始进行,但是当工作完成了75%左右的时候,刚好赶上了蚂蚁sofa开源,后期又开源了sofa-node,得以很好的跟蚂蚁的朋友们交流也是不小的收货!

其实如果去看蚂蚁分享出来的文章,写的要好很多,推荐大家可以去看一看《如何实现一个Nodejs RPC》,市面上的rpc框架原理大致相同,都涉及到多路复用的利用,像sofa、hsf、dubbo还有58的scf都是基于netty实现的,相似的还有nginx、nodejs、http2等。

基本原理:

Rpc的原理其实就是基于协议层的通信,最基本的就是tcp协议相关知识!主流的方式无非就是公有协议与私有协议,比如http协议与自定义协议字段的私有协议!http协议很方便且被大多数人所熟悉适合大部分企业使用,但是缺点也很明显,首先他们header头信息就比较庞大,其次如果不是2.0版本效率也不会很高。

私有栈是个不错的选择!当确认好协议方式之后,实际上私有栈仅header头信息就节省出了大量码流;java原生的序列化方式,尽可以抓下tcp包看看他们的组成,会发现一些描述性信息特别多,严重影响效率,所以在编解码方面开源社区也有很多解决方案,比如Google Protocol Buffer。很多公司的私有栈也差不多使用相似的原理实现的,包括sofa与58的rpc。

关于消息在client端与server端的组织形式

基本原理上也基本相同,大致流程为如下。

定义好私有栈:header、body,说明一下,header的内容包括一些通用字段,比如传输大小,sessionId等等,body里的内容就是要被序列化的对象的内容了;

client端封装参数对象成一个list集合到一个Request里,同时Request对象包含要调用的类与方法名的字段,按照顺序将Request对象属性进行序列化成01表示,然后再已经建立好的nio通道内将数据发送到server端;

server端接收到数据并拆包后,反序列化该对象,根据类名、方法名,找到对应要执行的方法,然后根据业务逻辑将需要返回的数据对象封装到一个Response对象中,进行序列化操作,发送数据到client端;

Client端根据之前定义好的sessionId从得到的码流中拆包得到对应二进制流,然后解码成Response对象给client用户使用。

很抱歉没有开源,Nodejs-scf详尽调用方式与原理我会在内部系统中阐述!


内容

协议字段定义

定义协议头其实挺容易的,就是定义好指定位置的指定内容就好了。

大小端兼容

涉及到网络传输就必须考虑到大小端,其实可以理解为波传播的先后顺序。数据的高字节保存在内存的低地址中;数据的高字节保存在内存的高地址中。

心跳与重连

当我们正常的通信调用的时候,必须考虑到server端的可用态,与重启之后的重新建立连接的方式。定时15~30s的心跳ping-pong查看可用状态,如果对应配置的连接可用则重新建立连接。

负载均衡

好的负载方式能帮我们更小的减少损失,给用户更可靠的响应。所以选择了最快响应的方式,通过检测调用的超时情况,逐步降低调用方的对某台server的权重,来做到动态调整负载情况的目的。

编解码

详细原理可以看protobuf的源码,市面上的编解码方案很多也大同小异,自定义编解码方案,详细描述会体现在内部系统中,这里不做阐述。

服务管理

并没有选择SM,而是选择当前在用的管理平台的ETCD一致性组件,保证服务节点的实时更新。

质量上报

采用udp定时上报服务请求状况,达到监控的情况。

问题与挑战

类型匹配与精度问题

这是个挺大的问题的!背景:scf(58rpc)对java支持性很好,也没有做到像thrift那样定义通用Schema。

Java是强类型语言,所用类型包括byte、char、int、long、double、float、date、set、list、map、object等;Node数据类型包括Number、string、map、array、symbol。所以如何有效的匹配起来并且能相互表示是一个很大的问题。

针对类型的匹配可以放到后边的“Node端调用方式” 来说,这里主要讲精度的问题。

首先要确定的是在java中,long、double都是8字节的。Node中Number表示6字节。所以如何很好的匹配是个问题。我们尝试过long、另一个日本人写的组件还有big-integer,最终发现似乎只有big-integer合适(后来在sofa开源node版本后,发现long组件也是可用的,可能是我们对long的api不熟悉吧),然后就通过该组件对8字节的数据进行操作,这样就解决了长类型字节匹配的问题,同时也节省了很多时间去做这些字节的操作。

然后涉及到单双精度的问题,为了很好的理解它们的码流方式与计算方式,我编译了一遍jvm的源码,找到了他们的实现方式

switch (id) {

  case vmIntrinsics::_floatToRawIntBits:

    push(_gvn.transform( new (C, 2) MoveF2INode(pop())));

    break;


  case vmIntrinsics::_intBitsToFloat:

    push(_gvn.transform( new (C, 2) MoveI2FNode(pop())));

    break;


  case vmIntrinsics::_doubleToRawLongBits:

    push_pair(_gvn.transform( new (C, 2) MoveD2LNode(pop_pair())));

    break;


  case vmIntrinsics::_longBitsToDouble:

    push_pair(_gvn.transform( new (C, 2) MoveL2DNode(pop_pair())));

    break;


  case vmIntrinsics::_doubleToLongBits: {

Node* value = pop_pair();




case vmIntrinsics::_floatToRawIntBits  : {

    FloatConstant* c = x->argument_at(0)->type()->as_FloatConstant();

    if (c != NULL) {

      JavaValue v;

      v.set_jfloat(c->value());

      set_constant(v.get_jint());

    }

    break;

  }


单纯看源码不是很好理解,这样我们就可以同时结合oracle的官方文档


https://docs.oracle.com/javase/specs/jvms/se6/html/ClassFile.doc.html


其次,我们可以在java源码中发现float、double是有方法可以转成int的,也就是码流上其实无所谓,关键在于一个单双精度的IEEE 754的计算方式上。

我是没有发现node源码中具体的ieee754的实现位置,但是如果在源码搜索ieee754,也能搜出一片内容来,所以基本可以确认,也是遵循规范的。

那么结合big-integer,我们就不需要再去考虑底层位移操作了,直接使用node端提供的writeInt32BE类似这样的方法就可以了。



数据压缩

为了节省码流,我们还有一些优化可以去做,比如按位压缩,像数值这样的定长类型的码流!比如:如果一个int类型的数据,占用4字节,实际上它真实有用的数据只有1个字节,那么我们其实没有必要传输高位的其他字节码;但是对于负数来说可能就不适用了。只要判断出他的高位是不是有用数据,我们就可以做到去除高位无效数据,从而达到压缩的效果。

加减乘除运算的精度问题

前边说了由于精度的不匹配,也可能是我对node中Math的使用不熟悉,本身主流开发是java。

所以为了跟java端运算生成的hash值一致,我采用了位运算来实现加减乘除的方式,这样就可以匹配到对应java的计算值了。

node端调用方式

其实上述内容都花费不了太多时间。最主要的是要定义好更合规、合理、对使用者更方便的形式,所以考虑这些方面花费了不少时间,相比其他模块包括编解码,这块反而花费了更长的时间!

那如何设计一个合规方式呢?我认为宁愿自己麻烦一些,也要给使用的人带来使用的方便!所以我认真再次读取了jvm的源代码,很遗憾,生成字节码与解析字节码的c++文件实在太多,而且太复杂,还不如我挨个看文档看二进制来得快,所以最终采用了nodejs解析对应java的jar包(后来通过交流感觉我应该使用java去解析的),并且解析字节码的二进制文件,通过读取常量池内的内容解析方法、参数、类名、全限定类名、属性值等,然后通过nunjucks生成对应的接口类与实体类对应的js类文件。

为什么要生成对应的接口类文件呢?先看一下部分代码片段的格式吧!


可以看出来,我是将一个java类对象的所有相关属性值生成到js类文件中,这样方便编码的时候找到对应的类型,序列化到码流中。为了使用者在node中更符合使用者的使用习惯,故意在constructor的属性值中没有生成类型信息,对应属性类型信息以及注解等方式,是再次生成在了静态属性中,这样是为了更符合node端的使用方式。也就是生成对应java的元数据类文件。

问题:如果自己从头解析字节码,可能会有些问题,而且时间会比较长。所以采用了github上一个巴西哥们的结构化java字节码的buffer组件,但是这个组件有些问题,首先还是那个问题,就是他对于8字节数据解析有些问题,没有解析成功,其次多参数的参数名称没有解析出来。(抱歉,这个插件没有错误,是不熟悉api,重新看了一下后,发现是用了别的方式表示的,插件没有问题)所以我打算目前支持的类型情况下,之后fork出他的这个组件代码,调整好这几个bug。另外由于jvm对于基本数据类型、包装类、泛型、array以及对象类型的方式上都有些不同,尤其是使用node语言解析,由于时间关系没有解析的很好,未来都会处理这些问题的。

总结:如果要从头开发一个rpc的客户端,关键在于如何在效率与定义方式上寻求方案。

由于一些限制,目前就只写一个大概吧!至此特别感谢蚂蚁的朋友黄庭、晓晨还有朴灵先生,以及腾讯丹华提供的帮助与交流,也特别感谢团队人员的协助,才能逐步实现。

节点:2018.8.14发出node-scf beta版!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容