关于rpc:极光笔记-聊一聊推送系统中事件驱动架构的应用

微服务间通信形式次要有2种:RPC和消息传递。 通常来说在申请/响应的场景下应用RPC更加适合,具体实现通常是REST API或者基于长链接的协定(例如gRPC/Thrift/Zero ICE等)。两个服务有比拟强的依赖关系, 调用者依赖被调用者的处理结果,调用者解决该申请被梗塞以期待响应后果,同时还要进行负载平衡、限流、熔断、错误处理等。通常是同步的、一对一的交互,异步RPC实质也是要期待响应后果能力持续解决该申请。消息传递以异步音讯作为载体,通过事件代理(消息中间件)连贯音讯解决的上游和上游,上游和上游涣散耦合,通常上游的解决逻辑不依赖上游的处理结果,具体实现通常有发布者/订阅者和事件流等。而事件驱动架构(EDA)就是基于消息传递,通过解耦生成、传输和处理事件,协调事件生产者、消息中间件、消费者工作的一种架构模式,具备涣散耦合、流量削峰、非阻塞、易于扩大、更高的弹性和错误处理能力等长处。不过也带来性能损耗、复杂性等方面的问题。 推送零碎的业务状态在推送零碎中,一个推送申请的解决流程大抵如下: 接管申请,进行用户身份认证、参数校验、申请权限校验申请解析,构建推送工作依据推送工作,筛选推送指标用户,以及获取推送指标用户的根本信息依据推送策略以及各推送指标用户的信息抉择推送通道执行推送工作各推送通道负责具体的推送操作。从业务场景看,极光推送均匀每秒接管2万~3万的推送申请,包含regid、tag、alias、播送等推送形式。各推送指标的推送指标用户数量也不同,少则几个,多则几千万甚至上亿的指标数量。极光反对多推送通道,客户能够依据须要抉择应用哪个通道进行推送,各个通道因为服务质量、地区间隔等方面的因素,申请耗时、稳定性各有不同,此外还有各自的限速逻辑。业务面临着很大的挑战: 接管内部推送申请数大,峰值尖刺甚至可能翻倍一个推送申请可能产生大量的音讯,例如一个播送或者一个大tag推送,有千万级别的指标用户,意味着一个推送申请扩充为千万级别的零碎外部申请。整个零碎中同时解决的音讯体量十分大,常态化的业务峰值超过千万级别,并且一天工夫有多个峰值,不可准确预测。解决流程中波及多个解决环节,各个环节的处理速度并不相同,甚至有可能有微小的差别,例如某些推送通道解决快,某些通道推送解决绝对慢。 推送零碎如何应用事件驱动晋升零碎的整体性能从客户的需要和产品需要登程,心愿每个申请可能疾速、正确地响应并解决,可能及时地把音讯推送给每个指标用户。因而咱们须要解决大量音讯以最快的速度推送的问题。 从业务的角度看,能够从租户分级、租户隔离、推送形式、推送指标规模等维度进行部署隔离,减小各租户、各申请间的相互影响,更加疾速更加稳固地解决整个推送申请,提供更好的服务质量。 从纯技术角度看,采纳了多种技术手段和策略,包含缓存优化、异步解决、并行处理等,晋升整体性能,实现更高效的推送操作和更好的零碎稳定性。 其中采纳事件驱动架构来组织整个解决流程,并行、异步解决,实现零碎整体性能的晋升,并且在突增流量、异样解决方面都可能很不便地应答。 推送流程的EDA实现首先,将上述流程简化为多个外围解决环节,并构建为服务,通过消息中间件进行交互。 pushAPI 接管申请,构建推送工作segmentGateway 筛选推送指标用户,抉择推送通道,执行推送工作pushChannel 负责各个推送通道的推送操作,其中蕴含多个具体的推送通道。各服务基于事件传递状态转移 (Event Carried State Transfer)或者事件告诉(Event Notification)模式生成事件音讯,而后投递到消息中间件中。同时各服务通过音讯队列或者订阅的形式从消息中间件生产音讯,依据理论须要由消息中间件推送事件音讯给消费者或者由消费者被动拉取事件音讯。对于反复音讯的解决,通常有Exactly once和At least once +业务幂等性解决,倡议以第二种形式解决。注:消息中间件的选型、音讯投递服务质量的原理不在本文的形容范畴,未开展阐明。 pushAPI接管推送申请,生成推送工作事件音讯,而后投递到消息中间件。消息中间件依据相干信息发送到指定队列中。 segmentGateway生产指定队列,生产其中的事件音讯。通过解决后(查问指标用户和相干根本信息),批量填充各推送通道的指标用户到推送工作,并生成新的推送工作事件音讯投递到消息中间件中。消息中间件依据相干信息发送到各推送通道相干的队列中。 pushChannel的各推送通道服务生产各自的队列,执行推送操作。 以上流程中,事件音讯的生产者不须要消费者的处理结果,消费者也不依赖生产者,齐全解耦。 EDA解决推送零碎的痛点并行处理: 各服务多节点部署,并行处理申请/音讯,当服务呈现性能不足以解决业务时,K8S环境下减少节点正本数横向扩容即可;此外同一个推送申请通过不同推送通道推送时,多个推送通道并行推送音讯。 异步解决: 各服务专一本人的业务逻辑,不依赖业务上游,通常也不受上游的影响,无需期待处理结果,整个流程异步解决,缩小闲暇等待时间,能够最大化利用资源。 异样解决: 当有突增流量时,申请流量压力过大,超过了某个解决环节的所有服务节点的解决能力;或者某个推送通道因为网络抖动、网络中断等解决慢。这些服务节点作为消息中间件的消费者,因为解决能力有余或者解决变慢,未来得及解决的申请沉积在消息中间件,期待扩容或者采取其余解决措施。缓存申请到消息中间件中,从某种程度上也是背压(Back Pressure)模式的一种解决形式,当然还能够进一步的向上游反馈负载压力信息,由上游采取解决措施。例如大量音讯须要推送到苹果的推送服务,因为网络稳定或者苹果服务器限流,可能呈现推送变慢,这个时候推送iOS音讯可能会沉积在消息中间件中;其余推送通道并不受此影响,仍然可能失常地疾速推送音讯给其余通道。其余: 涣散耦合使各服务更加独立,在进行业务变更时(包含代码逻辑变更和公布变更)通常影响面很小,某些状况下甚至可能不影响上下游逻辑,例如某些推送通道没有进行推送速率的限度,当减少限速逻辑时只影响该通道的服务,对于其余通道和上游服务都不影响。借助EDA,极光推送可能轻松解决高并发推送申请,实现数千万级别的音讯的疾速推送,有更大的弹性应答不可预测的、更大流量的音讯推送,以及有更好的异样解决能力。 将来的瞻望:ServiceMesh/EvenMesh混合架构,对立平台化实际上,在极光推送零碎中,同时存在2种通信形式,例如在下面的推送流程中,须要依据推送形式获取推送指标用户以及相干根本信息,须要从其余子系统服务通过RPC进行查问获取后果。整个推送零碎的其余性能也是如此,依据业务场景、数据/业务量级做衡量取舍,抉择适合的架构模式来构建零碎,保证系统整体性能以及零碎的可用性、可维护性。为了让开发者更专一地解决外围的业务代码逻辑,缩小各种通信交互的解决细节(例如超时解决、限流等),目前申请/响应的模式比拟支流的做法是Service Mesh,并且经验了Sidecar/Proxyless/Sidecarless几种模式的倒退,也做了各种衡量取舍。 EDA也有绝对应的模式,Event Mesh就是其中之一,通过创立可能高效牢靠地解决工作的网状代理网络,解决大规模事件驱动架构的挑战,包含事件路由、发现和交付,实现跨简单分布式系统的事件驱动通信。 咱们的推送零碎实际上是混合2个通信形式的架构,更现实、更适宜咱们的架构应该是二者并存。咱们也继续关注相干技术,在充沛验证的状况下引入相干技术,放弃架构继续更新优化,防止架构腐化,保障推送零碎的高性能、高可用性、高可维护性。 对于极光 极光(Aurora Mobile,纳斯达克股票代码:JG)成立于2011年,是中国当先的客户互动和营销科技服务商。成立之初,极光专一于为企业提供稳固高效的音讯推送服务,凭借先发劣势,曾经成长为市场份额遥遥领先的挪动音讯推送服务商。随着企业对客户触达和营销增长需要的不断加强,极光前瞻性地推出了音讯云和营销云等解决方案,帮忙企业实现多渠道的客户触达和互动需要,以及人工智能和大数据驱动的营销科技利用,助力企业数字化转型。

September 26, 2023 · 1 min · jiezi

关于rpc:RPC-调用

什么是RPC调用?和Http申请有什么不同?

August 26, 2023 · 1 min · jiezi

关于rpc:优雅-gRPC-接口调试指南让你事半功倍

目前市面上可能兼容 gRPC 接口的接口调试与管理工具非常无限,而 gRPC 现已广泛应用于微服务架构中,并且能够预感的是,它会变得越来越风行。 作为业界当先的接口管理工具,Apifox 现已上线 gRPC 接口调试能力,全面兼容以下四种调用类型: Unary:一元调用Server Streaming:服务端流Client Streming:客户端流Bidirectional Streaming:双向流兴许你对 gRPC 接口还不太熟悉,那么咱们无妨先来简略理解一下! 什么是 gRPC?gRPC 是一个由谷歌开发的古代开源高性能 RPC 近程过程调用 ( Remote Procedure Calls) 框架,具备良好的兼容性,可在多个开发环境下运行。 gRPC 的利用场景相较于目前支流的 HTTP API 接口,gRPC 接口采纳了当先的 HTTP/2 底层架构设计作为底层传输协定,可能在大规模数据传输场景(例如视频流传输)和大量服务互相调用的微服务架构场景下大展身手。 数据交换采纳轻量化的 Protobuf 序列化协定,使得它可能在资源受限场景(常见于手机等挪动端设施)提供更快的数据处理速度的同时,缩小网络传输的数据量并节俭网络带宽,从而降低功耗并晋升电池寿命。 在正式开始介绍 gRPC 之前,咱们无妨先弄清楚到底什么是 RPC 以及它的作用,这对于后续的了解非常有帮忙。 什么是 RPC?RPC 协定是一种近程过程调用的实现形式。假如当初有两台服务器 A 和 B。部署在 A 服务器上的服务,想调用正在 B 服务器上运行的另一个过程。但因为单方服务并不在一个内存空间而导致无奈间接调用,那么就必须通过网络通讯来达到调用成果。 要建设网络通讯无非是在传输层发动 TCP 连贯。TCP 的握手机制确保了数据包能牢靠地传输给对方,并且它具备以下三个特点:面向连贯、牢靠、基于字节流。后面两种个性都能够胜任这个场景,但唯独在基于字节流这一点恐怕值得商讨。为什么? 因为它没有边界。字节流实质上是在传输层双向通道中流淌的数据,也就是计算机可能了解的二进制 0 1 数据。所以当发送端应用 TCP 发送“南京市”+“长江大桥”字符时,接收端有可能收到的就是“南京市长”+“江大桥”,也有可能是“南京市长江大桥”等。 过于简略的 TCP 连贯过程无奈保障信息的唯一性和确定性,因而才须要在数据中定义音讯头、音讯体,并且发送方与接管方独特认可这套沟通形式,由此衍生出了HTTP 协定和 RPC 调用等计划,它们实质上都是对数据的传递和调用形式作出了规范化定义,避免出现信息失真。 例如当初有一个购物网站,存在订单服务与用户服务(例如账号治理) 两项微服务。订单服务须要查问到用户服务下的一些数据,然而两者相隔离。此时订单服务就必须通过近程调用的形式获取数据。 ...

July 7, 2023 · 1 min · jiezi

关于rpc:GO语言开源API网关

简介开源Apinto API网关具备优异的性能体现、良好的扩展性以及极低的应用和保护老本,Apinto Dashboard 作为配套可视化控制台我的项目,相比于Apinto Dashboard v1.x版本,它提供了优良的用户体验,更加敌对的交互体验,更加简洁的配置流程,操作简略,上手难度极低,更好地帮忙用户和企业简略、疾速、低成本、低危险地实现:零碎微服务化、系统集成、向合作伙伴、开发者凋谢性能和数据。 本次公布亮点性能是插件治理、插件模板两大模块性能。用户可依据集体需要增加插件,并反对集群治理网关插件的生命周期。此外,还提供插件模板治理性能,用户能够将一个或多个API绑定到具备雷同性能的插件模板上。 性能介绍插件治理插件治理帮忙用户治理着网关应用到的插件,基于零碎架构,零碎内置了利用、拜访、熔断、限流、灰度等插件。零碎提供几十款罕用网关插件,用户可依据需要增加插件,删除插件。 插件治理列表插件可通过高低拖动调整插件的程序 ,如果A插件依赖B插件,那么B插件不可拖动到A插件的上方,插件间的程序扭转后,须要在网关集群的插件治理公布插件才会失效。 当该插件在集群是禁用状态且未被插件模板援用时,是反对删除,插件可公布到不同的网关集群失效。 增加插件扩大ID:网关提供的插件ID,同一个插件可被增加屡次成为用户自定义的插件。 依赖插件:要增加的插件是否依赖其余插件才会失效,如服务治理中拜访策略、灰度策略等用到的拜访插件、灰度插件依赖利用插件。 保留:点击保留后,新增加的插件会同步到网关集群的插件治理里,公布状态为未公布,状态为禁用;欲使其在网关集群失效,须要更改状态为启用或全局启用,而后公布到网关集群才会真正失效。 网关集群-插件治理反对批改插件配置和公布,同时反对查看更改历史和公布历史。更多应用教程可参考:https://help.apinto.com/docs/dashboard-v2/basic/cluster.html 插件模板用户能够将一个或多个API绑定到具备雷同性能的插件模板上,公布插件模板到集群失效。更多应用教程可参考:https://help.apinto.com/docs/dashboard-v2/api/plugin-template... API绑定插件模板 总结Apinto网关开箱即用,喜爱或感兴趣的小伙伴们连忙去下载安装体验吧!为了反对Apinto团队提供更好的开源体验,记得fork一下噢。开源地址:https://github.com/eolinker/apinto

April 4, 2023 · 1 min · jiezi

关于rpc:深入浅出RPC服务-不同层的网络协议

导读:本系列文章从RPC产生的历史背景开始解说,波及RPC外围原理、RPC实现、JSF的实现等,通过图文类比的形式分析它的外部世界,让大家对RPC的设计思维有一个宏观的意识。 作者:王禹展   京东衰弱 网络协议为什么须要网络协议?网络协议是为计算机网络中进行数据交换而建设的规定、规范或约定的汇合。 网络中一个微机用户和一个大型主机的操作员进行通信,因为这两个数据终端所用字符集不同,因而操作员所输出的命令彼此不意识。为了能进行通信,规定每个终端都要将各自字符集中的字符先变换为规范字符集的字符后,才进入网络传送,达到目标终端之后,再变换为该终端字符集的字符。就像咱们谈话用某种语言一样,在网络上的各台计算机之间也有一种语言,这就是网络协议,不同的计算机之间必须应用雷同的网络协议能力进行通信。 一次申请都须要用到那些协定?1.要传输数据,首先如何晓得对应的机器的地址?通过IP能够确认具体的机器(网络层的IP层协定)。 2.找到指标机器后,须要晓得该机器上那个程序承受本次申请?通过端口就能确定具体的程序(传输层的TCP层协定)。 3.确定完程序后,怎么辨别不同的申请,每一个申请如何关联对应的响应呢?(应用层的RPC协定)通过音讯id辨别。 4.以上这些最初是由物理层的光缆、电缆、无线信道等反对的,如何管制信号在物理层之上的传递,还须要PPP协定、ARP协定等。 不同层的协定简介 应用层的协定HTTP协定超文本传输协定(Hyper Text Transfer Protocol,HTTP)是一个简略的申请-响应协定,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的音讯以及失去什么样的响应。 HTTPS协定全称:Hyper Text Transfer Protocol over SecureSocket Layer,是以平安为指标的 HTTP 通道,在HTTP的根底上通过传输加密和身份认证保障了传输过程的安全性 。HTTPS 在HTTP 的根底下退出SSL,HTTPS 的平安根底是 SSL,因而加密的具体内容就须要 SSL。 HTTPS 存在不同于 HTTP 的默认端口及一个加密/身份验证层(在 HTTP与 TCP 之间)。这个零碎提供了身份验证与加密通信办法。它被宽泛用于万维网上平安敏感的通信,例如交易领取等方面。 RPC协定一种通过网络从近程计算机程序上申请服务,而不须要理解底层网络技术的协定。 RTMP协定全称:Real Time Messaging Protocol(实时音讯传输协定)。该协定基于TCP,是一个协定族,包含RTMP根本协定及RTMPT/RTMPS/RTMPE等多种变种。RTMP是一种设计用来进行实时数据通信的网络协议,次要用来在Flash/AIR平台和反对RTMP协定的流媒体/交互服务器之间进行音视频和数据通信。反对该协定的软件包含Adobe Media Server/Ultrant Media Server/red5等。RTMP与HTTP一样,都属于TCP/IP四层模型的应用层。 P2P协定点对点技术又称对等互联网络技术,是一种网络新技术,依赖网络中参与者的计算能力和带宽,而不是把依赖都汇集在较少的几台服务器上。P2P网络通常用于通过Ad Hoc连贯来连贯节点。这类网络能够用于多种用处,各种档案分享软件曾经失去了宽泛的应用。P2P技术也被应用在相似VoIP等实时媒体业务的数据通信中。 DNS协定DNS是一种能够将域名和IP地址互相映射的以层次结构散布的数据库系统。DNS零碎采纳递归查问申请的形式来响应用户的查问,为互联网的运行提供关键性的根底服务。目前绝大多数的防火墙和网络都会凋谢DNS服务,DNS数据包不会被拦挡,因而能够基于DNS协定建设荫蔽信道,从而顺利穿过防火墙,在客户端和服务器之间传输数据。 GTP协定全称:GPRS隧道协定(GPRSTunnelingProtocol),能够分解成三种独立的协定,GTP-C(管制面)、GTP-U(用户面)及GTP'(计费传输)。GTP-C用于GPRS外围网络中,用于不同网络节点之间的信令数据。GTP-U 用于承载用户数据。 GTP能够用在UDP或TCP上,GTP v1仅用于UDP上。用于 GPRS(2.5代通信技术)、UMTS(3G挪动通信技术)、LTE(3G与4G技术之间的过渡) 和 5G 网络。 DHCP协定全称:Dynamic Host Configuration Protocol(动静主机配置协定),通常被利用在大型的局域网络环境中,次要作用是集中地治理、调配IP地址,使网络环境中的主机动静的取得IP地址、Gateway地址、DNS服务器地址等信息,并可能晋升地址的使用率。 其它协定FTP·、Gopher IMAP4 、 IRC 、 NNTP 、XMPP 、POP3 、SIP 、SMTP 、SNMP 、SSH 、TELNET 、RTCP 、RTP 、RTSP 、 SDP 、 SOAP 、STUN 、 NTP 、SSDP 、BGP等。 ...

March 24, 2023 · 1 min · jiezi

关于rpc:开源API网关APINTO应用管理

问题:公司的业务零碎比拟多,各种业务零碎彼此调用,还有调用了第三方厂商的OpenAPI,当初公司面临着无奈监控这些零碎的调用关系以及调用量统计。 更为要害的是,这些零碎的鉴权不对立,每次开拓一条业务线,新上线零碎必须与其余零碎联调,新加盟的经销商同样面临着这些问题,对研发和运维来说,效率极其低下。 Apinto网关提出利用治理概念,对立治理利用及其生命周期。利用作为业务通信的发起者角色,始终贯通着整个调用链,Apinto网关对利用申请的流量进行鉴权认证,并对其所申请的流量进行服务治理,同时还对其监控告警,统计利用调用状况。 Apinto网关中的利用治理完满解决公司治理各业务零碎所面临的问题,这也是领导赞美Apinto网关的其中一个方面,那接下来就把最近钻研的Apinto的利用治理模块具体介绍一下。 配置:2.1 利用列表网关通过申请流量中的鉴权信息,来辨认是哪个利用,利用必须上线到指定的网关集群且是未禁用状态时才真正失效。   网关对匿名利用和其余利用解决逻辑:当没有利用上线(包含匿名利用),此时不须要鉴权,所有申请被放行。 当有利用上线,此时匿名利用开启上线,此时可鉴权可不鉴权,若鉴权失败,则走匿名应用逻辑。 当有利用上线,此时匿名利用开启上线,且给匿名利用配置了拜访策略,仅容许匿名利用拜访失效范畴内的API,其余API申请都须要利用鉴权。当有利用上线,此时匿名利用禁用下线,此时必须要做鉴权 2.2 配置带有鉴权信息的test利用并上线2.3 下线匿名利用,测试test利用调用状况下线匿名利用并且禁用掉,那么不带test这个利用的鉴权,调用testAPI这个API是调不通的报403,看后果:在申请头加上test利用的鉴权信息,应该能够胜利调用,看后果:测试后果如预期个别,必须带有鉴权信息且是正确的鉴权才能够调用API。下回钻研利用的监控调用统计,到时候分享给大家。 总结:对于公司来说,必须监控谁平安合规调用了业务API,什么时段的调用量。好货色必须关注,好了,省得大家去搜,间接提供github地址。开源地址:https://github.com/eolinker/apinto

March 23, 2023 · 1 min · jiezi

关于rpc:最简最速搭建grpc分布式服务的Mac系统开发环境

[TOC] 环境详情golang 1.18macOS Big Surprotobuf 3基本原理整个RPC过程就是: 客户端 发送 数据(以字节流的形式)服务端接管,并解析。 依据约定晓得要晓得执行什么。而后把后果返回客户端RPC就是把—— 上述过程封装下,使其操作更加优化应用一些大家都认可的协定 使其规范化那么——GRPC框架中采纳了protobuf作为这个过程中消息传递的数据结构。 什么是 ProtobufProtobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,用于形容一种轻便高效的结构化数据存储格局,并于2008年对外开源。Protobuf能够用于结构化数据串行化,或者说序列化。它的设计十分实用于在网络通讯中的数据载体,很适宜做数据存储或 RPC 数据交换格局,它序列化进去的数据量少再加上以 K-V 的形式来存储数据,对音讯的版本兼容性十分强,可用于通信协定、数据存储等畛域的语言无关、平台无关、可扩大的序列化构造数据格式。开发者能够通过Protobuf附带的工具生成代码并实现将结构化数据序列化的性能。 本教程将在接下来的内容中形容 .proto 文件的语法以及如何通过 .proto 文件生成数据拜访类和 grpc 服务类,并以此为根底搭建繁难的 grpc 服务端开发环境。 工具装置新建工程,抉择 go modules,配置 GOPROXY 等操作不再赘述; 下载grpc依赖go get -u google.golang.org/grpc@v1.51.0brew 装置 protocolbrew install protobuf装置后能够执行命令protoc,作为protocol编译器应用,可能通过两头文件 .proto 转译成各种语言的grpc文件 brew install protoc-gen-go转译两头文件成go文件的插件 brew install protoc-gen-go-grpc转译两头文件成go-grpc文件的插件。 留神:如果装置失败那可能是依赖装置失败,间接brew intall 装置依赖即可。 至此,工具和依赖装置结束,接下来将构建工程。 环境搭建工程 go-rpc-server 构造: . ├── client.go ——用于编写客户端测试 ├── go.mod ├── go.sum ├── main.go ——服务端的启动 ├── pbfiles ——该目录用于搁置原始proto文件 ...

January 13, 2023 · 2 min · jiezi

关于rpc:这次我设计了一款TPS百万级别的分布式高性能可扩展的RPC框架

作者:冰河 博客地址:https://binghe001.github.io 大家好,我是冰河~~ 没错,这次冰河又要搞事件了,这次筹备下手的是RPC框架我的项目。为什么要对RPC框架我的项目下手呢,因为在现在分布式、微服务乃至云原生一直倒退的过程中,RPC作为底层必不可少的通信组件,被广泛应用在分布式、微服务和云原生我的项目中。 为啥要开发RPC框架事件是这样的,在开发这个RPC框架之前,我破费了不少工夫算是对Dubbo框架彻底钻研透彻了。 冰河在撸透了Dubbo2.x和Dubbo3.x的源码之后,原本想给大家写一个Dubbo源码解析的专栏。为此,我其实私下筹备了一个多月:画流程图、剖析源码、写测试Demo,本人在看Dubbo源码时,也为Dubbo源码增加了十分具体的正文。这里,就蕴含Dubbo2.x和Dubbo3.x的源码。 当我就这么熬夜肝文一个多月后,忽然发现一个问题:Dubbo通过多年一直的迭代开发,它的源码曾经十分多了,如果以文章的模式将Dubbo的源码八面玲珑的剖析到位,那还不晓得要写到何年何月去了。当我写文章剖析Dubbo的最新版本3.x时,可能写到专栏的中后期Dubbo曾经更新到4.x、5.x,设置有可能是6.x、7.x了。 与其这么吃力吧咧的剖析源码,还不如从零开始带着大家一起手撸一个可能在理论生产环境应用的、分布式、高性能、可扩大的RPC框架。这样,大家也可能直观的感触到一个可能在理论场景应用的RPC框架是如何一步步开发进去的。 置信大家在学完《RPC手撸专栏》后,本人再去看Dubbo源码的话,就相对来说简略多了。你说是不是这样的呢? 你能学到什么?既然是整个专栏的开篇嘛,必定是要通知你在这个专栏中可能学习到哪些实用的技术的。这里,我就画一张图来直观的通知你在《RPC手撸专栏》可能学到哪些技术吧。 《RPC手撸专栏》整体框架技术全貌如图所示,退出星球后与冰河一起从零实现它,搞定它,当你紧跟冰河节奏搞定这个RPC框架后,你会发现:什么Dubbo、什么gRPC、什么BRPC、什么Hessian、什么Tars、什么Thrift、什么motan、什么hprose等等等等,市面上支流的RPC框架,对你来说就都不叫事儿了,跟紧冰河的节奏,你能够的。 置信小伙伴们看到《RPC手撸专栏》波及到的知识点,应该可能理解到咱们这个从零开始的《RPC手撸专栏》还是比拟硬核的吧? 另外,咱这RPC我的项目反对同步调用、异步调用、回调和单向调用。 同步调用异步调用 回调 单向调用 对,没错,咱们《RPC手撸专栏》最终实现的RPC框架的定位就是尽量能够在理论环境应用。通过这个专栏的学习,让大家深刻理解到可能在理论场景应用的RPC框架是如何一步步开发进去的。 代码构造我将这个bhrpc我的项目的定位为可在理论场景应用的、分布式、高性能、可扩大的RPC框架,目前总体上曾经开发并欠缺的性能达到60+个子我的项目,大家看图吧。 我的项目大量应用了对标Dubbo的自定义SPI技术实现高度可扩展性,各位小伙伴能够依据本人的须要,依照SPI的设计要求增加本人实现的自定义插件。 演示成果说了那么多,咱们一起来看看这个RPC框架的应用成果吧,因为咱们这个RPC框架反对的调用形式有:原生RPC调用、整合Spring(XML/注解)、整合SpringBoot、整合SpringCloud、整合SpringCloud Alibaba,整合Docker和整合K8S七种应用形式。 这里,咱们就以 整合Spring注解的形式 来给大家演示下这个RPC框架。 RPC外围注解阐明为了让大家更好的理解这个RPC框架,我先给大家看下RPC框架的两个外围注解,一个是RPC的服务提供者注解@RpcService,一个是RPC的服务调用者注解@RpcReference。 (1)服务提供者注解@RpcService的外围源码如下所示。 /** * @author binghe * @version 1.0.0 * @description bhrpc服务提供者注解 */@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Componentpublic @interface RpcService { /** * 接口的Class */ Class<?> interfaceClass() default void.class; /** * 接口的ClassName */ String interfaceClassName() default ""; /** * 版本号 */ String version() default "1.0.0"; /** * 服务分组,默认为空 */ String group() default ""; /** * 提早公布,预留 */ int delay() default 0; /** * 是否导出rpc服务,预留 */ boolean export() default true;}(2)服务调用者注解@RpcReference的外围源码如下所示。 ...

August 24, 2022 · 3 min · jiezi

关于rpc:基于RPC接口的业务侧流量回放

背 景\在产品需要迭代过程中,功能测试与回归测试是必不可少的两个环节。对于改变较大的我的项目,首先,确保性能的实现合乎产品逻辑并做到100%没有问题离不开无效的功能测试;其次,我的项目中很多逻辑的改变都是在原有性能的根底上进行的,这时候就须要肯定的回归测试。通常,在功能测试时,人工case不能模仿线上用户的所有行为,且具备肯定的主观性;回归测试时,采纳全面回归的形式往往也随同着测试老本的减少。一个好的形式就是利用线上流量来验证。 一方面,通过记录线上流量,在沙箱或者测试环境回放,来发现新分支代码是否可能让零碎性能失常运行,从而升高代码变动给整体零碎带来的危险;另一方面,通过线上流量进行线下回归测试能够在保障全面回归的状况下无效的节约测试老本。 简 介流量回放应用实在的线上流量进行线下回放测试,节约回归测试老本,保障代码品质,进而缩小线上事变。 流量平台搭建老本全链路流量平台具备的能力 能录制线上实在流量;能实现海量数据的并发申请;能反对常见协定的申请;对线上尽量利用通明,也就是说无侵入性;工具应用简略,可能满足各场景流量回放。 能够看出依照以上的能力需要搭建一个流量平台须要投入肯定的老本,而咱们目前思考的是针对某一个我的项目的疾速流量回放,可能并不需要设计这么简单并且重量级的流量平台。那么,如何疾速的实现满足业务需要的流量回放呢?基于RPC接口的流量回放,简略、快捷、容易上手。接下来我将通过理论的业务场景来介绍下流量回放在采货侠卖场侧的业务利用。流量回放的利用1、我的项目背景采货侠卖场侧新增后盾配置性能且还波及到对成拍逻辑的改变,须要验证新性能的正确性并对多种成单场景进行肯定的回归测试。 回归工作量大须要无效的伎俩进步测试效率;成单逻辑不容出错须要100%的保障。 2、 计划流量回放 回归测试:应用实在的线上流量进行线下回放测试,比照不反对议价性能的商品线上、线下成单后果的一致性;功能测试:应用实在的线上流量进行线下新性能的验证。线上放弃原有Apollo配置逻辑,线下利用新开发经营后盾的配置性能。放弃雷同配置利用线上流量数据通过新开发分支代码运行获取相应的成单后果,验证补贴后盾和议价后盾性能的正确性。 长处 能够高度联合业务逻辑,实现细粒度定制化流量复制;解放局部回归测试的人力老本,晋升测试效率;对业务的用例场景更加主观,保障代码品质,进而缩小线上事变。 3、外围性能流量采集 通过云窗获取相应要求的线上商品信息以及对应的最高出价和成单后果将云窗所得线上数据的相干信息写入到数据库对应的字段中 流量回放 代码设计逻辑 首先,通过读取线上商品的保底价映射成雷同价格的线下商品参拍暗拍卖场,接着模仿用户在采货侠卖场中的实在行为进行流量回放,并将线下的商品数据和成单后果写入数据库表;整个过程中采纳了多线程以及数据库表批量减少、查找来进步运行效率。 数据存储后果 表中线上、线下数据一一对应存储,清晰、明了有利于后果剖析。 后果剖析后果剖析思路:调用开发分支在测试环境进行流量回放,失去相应的冀望后果后和线上同一商品的相应后果进行比拟,判断后果是否统一。校验的字段可依据理论需要场景进行相应的变更,在本次案例中验证的字段是成单后果和补贴金额。 回归测试验证逻辑 读取不议价商品的流量回放成单后果和线上相应数据的成单后果进行比照,判断后果是否统一。如下图所示为成单验证后果:线上和线下的成单后果全副是统一的,这阐明新需要代码的开发未影响到原有不议价商品成单逻辑的正确性。此外,采纳该办法进行测试使原打算进行回归测试的工夫缩短了3倍多,这无效的节约了回归测试的老本。 功能测试验证逻辑 为了做进一步的性能保障。通过获取线上实在的用户流量,在保障线上原有配置和线下经营后盾统一的状况下,调用新分支代码实现线下流量回放失去相应的预期后果,而后和线上后果进行比对。首先,比照线上、线下成单后果是否统一来验证成单逻辑的正确性;其次通过比拟线上、线下补贴金额是否统一来验证经营后盾补贴配置性能的正确性。本次验证不关怀成单后商品的状态流转,而线上局部商品状态曾经流转,所以验证成单后果时,能够把生成订单后流转的一些状态重置为成单状态和线下成单后果进行比对。逻辑如下: 验证后果如下: 首次验证后果中呈现了局部商品线上、线下成单后果不统一,需进一步剖析解决问题1: 线下局部商品状态始终在售卖中起因: 有局部商品调用完结竞拍没有胜利解决: 商品调用完结竞拍失败后持续从新调用问题2: 线上商品成单,线下商品却是流拍起因: 线下经营后盾局部价格段的设置没有和线上的保持一致解决: 保障线上、线下各价格段补贴、议价金额统一确保线上、线下配置统一的状况下,应用优化后的代码从新进行流量回放,能够失去雷同成单后果和补贴后果。阐明经营后盾配置性能的代码实现以及成单逻辑都是正确的。

August 19, 2022 · 1 min · jiezi

关于rpc:面试官让我手写一个RPC框架

现在,分布式系统大行其道,RPC 有着无足轻重的位置。Dubbo、Thrift、gRpc 等框架各领风骚,学习RPC是老手也是老鸟的必修课。本文带你手撸一个rpc-spring-starter,深刻学习和了解rpc相干技术,包含但不限于 RPC 原理、动静代理、Javassist 字节码加强、服务注册与发现、Netty 网络通讯、传输协定、序列化、包压缩、TCP 粘包、拆包、长连贯复用、心跳检测、SpringBoot 主动装载、服务分组、接口版本、客户端连接池、负载平衡、异步调用等常识,值得珍藏。RPC定义近程服务调用(Remote procedure call)的概念历史已久,1981年就曾经被提出,最后的目标就是为了调用近程办法像调用本地办法一样简略,经验了四十多年的更新与迭代,RPC 的大体思路曾经趋于稳定,现在百家争鸣的 RPC 协定和框架,诸如 Dubbo (阿里)、Thrift(FaceBook)、gRpc(Google)、brpc (百度)等都在不同侧重点去解决最后的目标,有的想极致完满,有的谋求极致性能,有的偏差极致简略。 RPC基本原理让咱们回到 RPC 最后的目标,要想实现调用近程办法像调用本地办法一样简略,至多要解决如下问题: 如何获取可用的近程服务器如何示意数据如何传递数据服务端如何确定并调用指标办法上述四点问题,都能与当初分布式系统炽热的术语一一对应,如何获取可用的近程服务器(服务注册与发现)、如何示意数据(序列化与反序列化)、如何传递数据(网络通讯)、服务端如何确定并调用指标办法(调用办法映射)。笔者将通过一个简略 RPC 我的项目来解决这些问题。 首先来看 RPC 的整体零碎架构图: 图中服务端启动时将本人的服务节点信息注册到注册核心,客户端调用近程办法时会订阅注册核心中的可用服务节点信息,拿到可用服务节点之后近程调用办法,当注册核心中的可用服务节点发生变化时会告诉客户端,防止客户端持续调用曾经生效的节点。那客户端是如何调用近程办法的呢,来看一下近程调用示意图: 客户端模块代理所有近程办法的调用将指标服务、指标办法、调用指标办法的参数等必要信息序列化序列化之后的数据包进一步压缩,压缩后的数据包通过网络通信传输到指标服务节点服务节点将承受到的数据包进行解压解压后的数据包反序列化成指标服务、指标办法、指标办法的调用参数通过服务端代理调用指标办法获取后果,后果同样须要序列化、压缩而后回传给客户端通过以上形容,置信读者应该大体上理解了 RPC 是如何工作的,接下来看如何应用代码具体实现上述的流程。鉴于篇幅笔者会抉择重要或者网络上介绍绝对较少的模块来讲述。 RPC实现细节1. 服务注册与发现作为一个入门我的项目,咱们的零碎选用 Zookeeper 作为注册核心, ZooKeeper 将数据保留在内存中,性能很高。在读多写少的场景中尤其实用,因为写操作会导致所有的服务器间同步状态。服务注册与发现是典型的读多写少的协调服务场景。Zookeeper 是一个典型的CP零碎,在服务选举或者集群半数机器宕机时是不可用状态,绝对于服务发现中支流的AP零碎来说,可用性稍低,然而用于了解RPC的实现,也是入不敷出。 ZooKeeper节点介绍长久节点( PERSISENT ):一旦创立,除非被动调用删除操作,否则始终长久化存储。长期节点( EPHEMERAL ):与客户端会话绑定,客户端会话生效,这个客户端所创立的所有长期节点都会被删除除。节点程序( SEQUENTIAL ):创立子节点时,如果设置SEQUENTIAL属性,则会主动在节点名后追加一个整形数字,下限是整形的最大值;同一目录下共享程序,例如(/a0000000001,/b0000000002,/c0000000003,/test0000000004)。ZooKeeper服务注册在 ZooKeeper 根节点下依据服务名创立长久节点 /rpc/{serviceName}/service ,将该服务的所有服务节点应用长期节点创立在 /rpc/{serviceName}/service 目录下,代码如下(为不便展现,后续展现代码都做了删减): public void exportService(Service serviceResource) { String name = serviceResource.getName(); String uri = GSON.toJson(serviceResource); String servicePath = "rpc/" + name + "/service"; zkClient.createPersistent(servicePath, true); String uriPath = servicePath + "/" + uri; //创立一个新的长期节点,当该节点宕机会话生效时,该长期节点会被清理 zkClient.createEphemeral(uriPath);}注册成果如图,本地启动两个服务则 service 下有两个服务节点信息: ...

July 21, 2022 · 6 min · jiezi

关于rpc:哈希趣投游戏系统开发逻辑源码搭建哈希游戏开发系统方案程序代码

 Hash,个别翻译做“散列”,也有间接音译为“哈希”的(刘森:I8O薇2857電8624掂)就是把任意长度的输出(又叫做预映射,pre-image),通过散列算法,变换成固定长度的输入,该输入就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输出的空间,不同的输出可能会散列成雷同的输入,而不可能从散列值来惟一的确定输出值。简略的说就是一种将任意长度的消息压缩到某一固定长度的音讯摘要的函数。 散列函数能使对一个数据序列的拜访过程更加迅速无效,通过散列函数,数据元素将被更快地定位。罕用Hash函数有: 1.间接寻址法。取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key)=a·key+b,其中a和b为常数(这种散列函数叫做本身函数) 2.数字分析法。剖析一组数据,比方一组员工的出生年月日,这时咱们发现出生年月日的前几位数字大体雷同,这样的话,呈现抵触的几率就会很大,然而咱们发现年月日的后几位示意月份和具体日期的数字差异很大,如果用前面的数字来形成散列地址,则抵触的几率会明显降低。因而数字分析法就是找出数字的法则,尽可能利用这些数据来结构抵触几率较低的散列地址。 3.平方取中法。取关键字平方后的两头几位作为散列地址。 4.折叠法。将关键字宰割成位数雷同的几局部,最初一部分位数能够不同,而后取这几局部的叠加和(去除进位)作为散列地址。 5.随机数法。抉择一随机函数,取关键字作为随机函数的种子生成随机值作为散列地址,通常用于关键字长度不同的场合。 6.除留余数法。取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即H(key)=key MOD p,p<=m。不仅能够对关键字间接取模,也可在折叠、平方取中等运算之后取模。对p的抉择很重要,个别取素数或m,若p选的不好,容易产生碰撞。 public class ConsistentHashingWithoutVirtualNode{ //待增加入Hash环的服务器列表 private static String[]servers={"192.168.0.1:8888","192.168.0.2:8888", "192.168.0.3:8888"}; //key示意服务器的hash值,value示意服务器 private static SortedMap<Integer,String>sortedMap=new TreeMap<Integer,String>(); //程序初始化,将所有的服务器放入sortedMap中 static{ for(int i=0;i<servers.length;i++){ int hash=getHash(servers); System.out.println("["+servers+"]退出汇合中,其Hash值为"+hash); sortedMap.put(hash,servers); } } //失去该当路由到的结点 private static String getServer(String key){ //失去该key的hash值 int hash=getHash(key); //失去大于该Hash值的所有Map SortedMap<Integer,String>subMap=sortedMap.tailMap(hash); ...

June 7, 2022 · 1 min · jiezi

关于rpc:大厂学苑-RPC框架核心源码深度解析

大厂学苑 -RPC框架外围源码深度解析超清原画 残缺无密 包含所有视频课件以及源码 MP4格局 获取ZY:网盘链接hashCode() 和 equals()的区别equals()equals() 方法用于比较两个对象是否相等,它与 == 相等比较符有着本质的不同。 在万物皆对象的 Java 体系中,零碎把判断对象是否相等的权力交给程序员。具体的措施是把 equals() 方法写到 Object 类中,并让所有类继承 Object 类。 这样程序员就能在自定义的类中重写 equals() 方法, 从而实现自己的比较逻辑。 hashCode()hashCode() 的意义是哈希值, 哈希值是经哈希函数运算后失去的后果,哈希函数能够保障雷同的输出能够失去雷同的输入(哈希值),然而不能够保障不同的输出总是能得出不同的输入。 当输出的样本量足够大时,是会产生哈希冲突的,也就是说不同的输出产生了雷同的输入。 暂且不谈冲突,就雷同的输出能够产生雷同的输入这点而言,是及其宝贵的。它使得零碎只需要通过简略的运算,在工夫复杂度O(1)的情况下就能得出数据的映射关系,根据这种个性,散列表应运而生。 一种支流的散列表实现是:用数组作为哈希函数的输入域,输出值通过哈希函数计算后失去哈希值。而后根据哈希值,在数组种找到对应的存储单元。当发生冲突时,对应的存储单元以链表的形式保存冲突的数据。 两者关系在大多数编程实践中,归根结底会落实到数据的存取问题上。 在汇编语言期间,你需要老诚恳实地对每个数据操作编写存取语句。 而随着期间发展到明天,咱们都用更便利灵活的高级语言编写代码,比如 Java。 Java 以面向对象为中心思想,封装了一系列操作数据的 api,升高了数据操作的复杂度。 但在咱们对数据进行操作之前,首先要把数据按照肯定的数据结构保存到存储单元中,否则操作数据将无从谈起。 然而不同的数据结构有各自的个性,咱们在存储数据的时候需要抉择合适的数据结构进行存储。 Java 根据不同的数据结构提供了丰富的容器类,便利程序员抉择适合业务的容器类进行开发。 通过继承关系图咱们看到 Java 的容器类被分为 Collection 和 Map 两大类,Collection 又可能进一步分为 List 和 Set。 其中 Map 和 Set 都是不容许元素重复的,严格来说Map存储的是键值对,它不容许重复的键值。 值得注意的是:Map 和 Set 的绝大多数实现类的底层都会用到散列表结构。 讲到这里咱们提取两个关键字不容许重复和散列表结构, 回顾 hashCode() 和 equals() 的个性,你是否想到了些什么货色呢?equals()力不从心下面提到 Set 和 Map 不存放重复的元素(key),这些容器在存储元素的时必须对元素做出判断:在以后的容器中有没有和新元素雷同的元素? ...

May 1, 2022 · 1 min · jiezi

关于rpc:RPCX源码学习server端

意识RPCRPC是什么货色? RPC: Remote Procedure Call(近程过程调用),是一个计算机通信协议。协定的次要内容是什么? 该协定容许运行于一台计算机中的程序调用另一个地址空间(通常为一个凋谢网络中的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额定的为这个交互作用编程(无需关注细节)。次要用来解决什么问题? 解决分布式系统中服务之间的调用问题。 使近程调用像本地办法调用一样不便,暗藏底层网络通信的复杂性,使咱们更专一于业务。一次rpc通信会波及到哪些角色?流程是什么样的? client: 客户端程序,调用的发起者 client-stub: 将调用参数和办法依照约定的协定进行编码,而后传输到server server: 服务端程序,解决client的调用 server-stub: 收到client的音讯后,依照约定的协定进行解码,而后调用本地办法进行解决 RPCX框架简介rpcx是一个分布式的RPC框架,由go语言开发,反对Zookepper、etcd、consul多种服务发现形式,多种服务路由形式,是目前性能最好的RPC框架之一。 详情见官网介绍: https://books.studygolang.com...server端源码分析从入口开始,咱们启动一个rpc服务时,须要通过NewServer办法去创立一个Server对象,源码如下: // NewServer returns a server.func NewServer(options ...OptionFn) *Server { s := &Server{ // 创立一个Server对象,给一些字段赋默认值 Plugins: &pluginContainer{}, options: make(map[string]interface{}), activeConn: make(map[net.Conn]struct{}), doneChan: make(chan struct{}), serviceMap: make(map[string]*service), router: make(map[string]Handler), AsyncWrite: false, // 除非你想benchmark或者极致优化,否则倡议你设置为false } for _, op := range options { op(s) } // 设置tcp连贯的keepAlive参数 if s.options["TCPKeepAlivePeriod"] == nil { s.options["TCPKeepAlivePeriod"] = 3 * time.Minute } return s}Server构造如下: ...

April 21, 2022 · 7 min · jiezi

关于rpc:NextRPC-RPC多段返回的创新和探索

作者:吴欣蔚(蓄昂) NextRPC 是对 RPC 申请模式的翻新和摸索,它能够像多级火箭一样,返回 Payload 多段数据,从不同的网络通道申请/应答多段返回,最终实现业务场景交付。外面的“Next”有双关的意思: 外围的区别于传统 RPC,申请的特点是让申请响应以流式多段返回给客户端,在流式的语义外面数据不停地通过 Next -> Next -> Next 的模式来响应;Next 有下一代的意思,NextRPC 是传统单响应的下一代 RPC 模式。目前,NextRPC 曾经在手淘的局部交易场景上线,如:下单换购业务在2021年4月初上线,经验9.9、11.11两次大促,业务模式曾经稳固。在2021年的双十一大促中,通过 NextRPC 链路给业务整体带来超过5%的 uv 转化晋升,在多商品中抉择一个最优商品透出的场景下,uv 转化晋升达25%以上。 业务背景在一些交易外围链路如 购物车、订单 等场景,冀望引入导购链路的举荐算法,基于店铺维度针对单品、多品SKU、跨店场景进行个性化举荐,推出如“棘手买一件”的个性化举荐产品 或 其余优惠信息的透出,晋升uv转化率。 问题与挑战当 外围交易场景 遇上 个性化举荐 ,交易 和 导购 的场景碰撞,会产生以下新的问题: 用户体验的抵触:个性化举荐算法服务RT高,不满足外围交易链路对RT的要求;服务质量的抵触:引入个性化举荐,会使得交易链路的上游零碎变得更加简单,对系统的稳定性带来新的挑战;同时,导购链路的「个性化举荐业务」容忍局部不确定性(如申请超时/失败),而「外围交易链路」则对稳定性和确定性要求极高,每一次的失败可能都会错失交易;机器资源的抵触:交易、导购 的多地部署构造不一样,机器容量和散布存在差异化。上述的这些问题,引出了新的技术挑战:如何在一次申请中同时反对对「用户体验」、「服务质量」、「资源」要求不一样的多个业务撑持零碎。 技术选型1、RPC模式分析以下针对五种常见的RPC模型开展比照剖析: 2、申请解决模型剖析申请解决模型次要剖析 串行解决、并行处理两种模型。 串行解决:数据的依赖深度决定了整体可优化的最小rt;并行处理:通过并行化(并发度n),来将对应档次上的rt升高为原来的 max(RTn)。 3、NextRPC的多段返回模式NextRPC联合了 单申请异步推送数据流 的RPC模式和 并行处理 的申请解决模型,解决了新的业务场景带来的挑战: 单申请异步推送数据流:-能够将 外围业务逻辑 和 非核心业务逻辑 解耦,保障外围数据同步响应,非核心数据异步响应,解决服务质量问题; -交易链路、导购链路逻辑解耦,外部跨利用调用,解决机器资源问题; 并行处理:-业务逻辑可并行化解决,节俭串行等待时间,解决交易链路用户体验问题。 NextRPC架构1、客户端架构技术架构网络层,抹平Mtop/Accs通道,对业务通明;数据整流层,管制 单个申请多个返回的时序(其中异步副返回蕴含了异步的业务数据流);数据编排层,管制 多申请间 异步数据的合并。 数据流和多实例一个页面反对多个 NextRPC 实例,多页面可创立多个 NextRPC 实例;一个 NextRPC 实例可进行多个不同申请,接管不同的副响应推送。 ...

March 14, 2022 · 1 min · jiezi

关于rpc:RPC基础概念问答

RPC 原理图 上面的两张原理图是一样的。从第一张原理图中能够看到 rpc 传输过程中应用的传输协定,从第二张原理图中 sockets 是在 Kernel 中,也就是在操作系统的内核中。 什么是RPC?? 近程过程调用。一个服务器应用另一台服务器上的提供的服务或办法。 RPC 解决的什么问题?? 让分布式或微服务零碎中不同服务之间的调用像本地调用一样简略。 为什么要应用 RPC?? 因为两个服务器不是同一个操作系统,也不在一个内存空间,所以不能间接调用,须要通过网络来传递数据。 RPC 怎么解决通信?? 在客户端和服务器之间建设 TCP 连贯,rpc 所有传输的数据都在 TCP 连贯中传输。连贯能够是按需连贯,调用完结后就断开,也能够是和长连贯,多个rpc 共享同一个连贯。 因为客户端和服务器是通过网络进行传输的,所以须要一个传输层,把函数id和序列化后的参数传递给服务器,并把序列化后的调用后果传回客户端。所以 TCP(大部分框架应用)和UDP都能够,也能够应用 http2(grpc应用) RPC 怎么解决寻址的问题?? 客户端上的利用通知 rpc 框架,服务器的主机/IP地址、端口号、办法名称,这样就能够实现调用。 网络协议是基于二进制的,内存中的参数的值须要序列化成二进制的模式。 服务器收到申请后,要对参数进行反序列化,把二进制变成内存中的参数,把参数复原成内存中的表达方式,而后找到对应的办法(寻址的一部分),进行本地调用,失去返回值。 服务器将返回值(序列化后)发给客户端,客户端收到返回值后,进行反序列化,而后传递给它下面的利用。 java的序列化形式:json。 RPC 和 socket 的区别?? socket是两个主机不同过程之间进行通信的形式。 rpc 是建设在 socket 之上的。rpc 通过 socket 实现通信, 也能够不必 socket,而应用其余的通信形式,比方命名管道(windows零碎中)。 RPC 和 HTTP 的分割和区别?? http 协定是应用层的协定。http 是实现 rpc 的一种形式。能够应用 http 协定,也能够应用 TCP / UDP 协定。 ...

January 20, 2022 · 1 min · jiezi

关于rpc:java-从零开始手写-RPC-02netty4-实现客户端和服务端

阐明上一篇代码基于 socket 的实现非常简单,然而对于理论生产,个别应用 netty。 至于 netty 的长处能够参考: 为什么抉择 netty?http://houbb.github.io/2019/05/10/netty-definitive-gudie-04-why-netty 代码实现maven 引入<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>${netty.version}</version></dependency>引入 netty 对应的 maven 包,此处为 4.1.17.Final。 服务端代码实现netty 的服务端启动代码是比拟固定的。 package com.github.houbb.rpc.server.core;import com.github.houbb.log.integration.core.Log;import com.github.houbb.log.integration.core.LogFactory;import com.github.houbb.rpc.server.constant.RpcServerConst;import com.github.houbb.rpc.server.handler.RpcServerHandler;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.nio.NioServerSocketChannel;/** * rpc 服务端 * @author binbin.hou * @since 0.0.1 */public class RpcServer extends Thread { private static final Log log = LogFactory.getLog(RpcServer.class); /** * 端口号 */ private final int port; public RpcServer() { this.port = RpcServerConst.DEFAULT_PORT; } public RpcServer(int port) { this.port = port; } @Override public void run() { // 启动服务端 log.info("RPC 服务开始启动服务端"); EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(workerGroup, bossGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast(new RpcServerHandler()); } }) // 这个参数影响的是还没有被accept 取出的连贯 .option(ChannelOption.SO_BACKLOG, 128) // 这个参数只是过一段时间内客户端没有响应,服务端会发送一个 ack 包,以判断客户端是否还活着。 .childOption(ChannelOption.SO_KEEPALIVE, true); // 绑定端口,开始接管进来的链接 ChannelFuture channelFuture = serverBootstrap.bind(port).syncUninterruptibly(); log.info("RPC 服务端启动实现,监听【" + port + "】端口"); channelFuture.channel().closeFuture().syncUninterruptibly(); log.info("RPC 服务端敞开实现"); } catch (Exception e) { log.error("RPC 服务异样", e); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } }}为了简略,服务端启动端口号固定,RpcServerConst 常量类内容如下: ...

October 9, 2021 · 3 min · jiezi

关于rpc:java-从零开始手写-RPC-01-基于-websocket-实现

RPC解决的问题RPC 次要是为了解决的两个问题: 解决分布式系统中,服务之间的调用问题。近程调用时,要可能像本地调用一样不便,让调用者感知不到近程调用的逻辑。这一节咱们来学习下如何基于 websocket 实现最简略的 rpc 调用,后续会实现基于 netty4 的版本。 开源地址: https://github.com/houbb/rpc残缺流程 其中右边的Client,对应的就是后面的Service A,而左边的Server,对应的则是Service B。 上面一步一步具体解释一下。 Service A的应用层代码中,调用了Calculator的一个实现类的add办法,心愿执行一个加法运算;这个Calculator实现类,外部并不是间接实现计算器的加减乘除逻辑,而是通过近程调用Service B的RPC接口,来获取运算后果,因而称之为Stub;Stub怎么和Service B建设近程通信呢?这时候就要用到近程通信工具了,也就是图中的Run-time Library,这个工具将帮你实现近程通信的性能,比方Java的Socket,就是这样一个库,当然,你也能够用基于Http协定的HttpClient,或者其余通信工具类,都能够,RPC并没有规定说你要用何种协定进行通信;Stub通过调用通信工具提供的办法,和Service B建设起了通信,而后将申请数据发给Service B。须要留神的是,因为底层的网络通讯是基于二进制格局的,因而这里Stub传给通信工具类的数据也必须是二进制,比方calculator.add(1,2),你必须把参数值1和2放到一个Request对象外头(这个Request对象当然不只这些信息,还包含要调用哪个服务的哪个RPC接口等其余信息),而后序列化为二进制,再传给通信工具类,这一点也将在上面的代码实现中体现;二进制的数据传到Service B这一边了,Service B当然也有本人的通信工具,通过这个通信工具接管二进制的申请;既然数据是二进制的,那么天然要进行反序列化了,将二进制的数据反序列化为申请对象,而后将这个申请对象交给Service B的Stub解决;和之前的Service A的Stub一样,这里的Stub也同样是个“假玩意”,它所负责的,只是去解析申请对象,晓得调用方要调的是哪个RPC接口,传进来的参数又是什么,而后再把这些参数传给对应的RPC接口,也就是Calculator的理论实现类去执行。很显著,如果是Java,那这里必定用到了反射。RPC接口执行结束,返回执行后果,当初轮到Service B要把数据发给Service A了,怎么发?一样的情理,一样的流程,只是当初Service B变成了Client,Service A变成了Server而已:Service B反序列化执行后果->传输给Service A->Service A反序列化执行后果 -> 将后果返回给Application,结束。简略实现假如服务 A,想调用服务 B 的一个办法。 因为不在同一个内存中,无奈间接应用。如何能够实现相似 Dubbo 的性能呢? 这里不须要应用 HTTP 级别的通信,应用 TCP 协定即可。 common专用模块,定义通用对象。 Rpc 常量public interface RpcConstant { /** * 地址 */ String ADDRESS = "127.0.0.1"; /** * 端口号 */ int PORT = 12345;}申请入参public class RpcCalculateRequest implements Serializable { private static final long serialVersionUID = 6420751004355300996L; /** * 参数一 */ private int one; /** * 参数二 */ private int two; //getter & setter & toString()}服务接口public interface Calculator { /** * 计算加法 * @param one 参数一 * @param two 参数二 * @return 返回后果 */ int add(int one, int two);}server服务接口的实现public class CalculatorImpl implements Calculator { @Override public int add(int one, int two) { return one + two; }}启动服务public static void main(String[] args) throws IOException { Calculator calculator = new CalculatorImpl(); try (ServerSocket listener = new ServerSocket(RpcConstant.PORT)) { System.out.println("Server 端启动:" + RpcConstant.ADDRESS + ":" + RpcConstant.PORT); while (true) { try (Socket socket = listener.accept()) { // 将申请反序列化 ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); Object object = objectInputStream.readObject(); System.out.println("Request is: " + object); // 调用服务 int result = 0; if (object instanceof RpcCalculateRequest) { RpcCalculateRequest calculateRpcRequest = (RpcCalculateRequest) object; result = calculator.add(calculateRpcRequest.getOne(), calculateRpcRequest.getTwo()); } // 返回后果 ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); objectOutputStream.writeObject(result); } catch (Exception e) { e.printStackTrace(); } } }}启动日志: ...

October 8, 2021 · 2 min · jiezi

关于rpc:B端产品业务

B端的日常场景B端设计的技术:编程语言;数据,数据库,数据仓库,关系型非关系型;客户端技术:PC,安卓,服务端B端正真要思考的流程是什么,怎么更好的服务企业:工作中波及的所有的都要思考.客户不明确的也无需要,商业需要,对于含糊两可的需要,如何召开业务调研:1.专家势力推给客户;2.密集确认(口头复数,文字描述,demo);3.多方推动(找干系人各个击破);4.分步施行,拆分对于B端的定制产品和根底标品的两条铲平线的产品经理侧重点有什么不同:规范是共产主义,定制是米饭咸菜;标准化能力是本身技能的晋升,价值的发明.做得是内容风控的审核台,方向过于窄了,想拓展:举一反三,多看多学axure原型对于B端产品经理更重要:都重要,传递信息工具,能传递分明都行1.客户定制化需要把规范产品改的面目全非;2.没有成就感:1.需要解决,满足客户需要的同时,零碎架构更加正当;2.不能仅仅听业务的1.我的项目进入开发阶段后,因为业务流程比较复杂,不分明流程图该精密到什么水平,是否每个判断都须要变现进去;2.想进步B端产品的外围竞争力,在哪方面花工夫:1.风控;如果必要风控独自画一个2.行业教训,业务知识,产品技能B端是否真的没有想C端那么看重用户体验:与c段相比要弱一些我的项目开发如何做到精确评估需要,并实现客户称心: 开始不会准,逐步精确.;客户称心是另一回事,干系人冀望治理的方法论1.对业务了解不粗浅;2.对数据类的技术常识短少;3.优化零碎,不晓得从那个角度去优化:优化比0-1更难,听听用户声音

August 13, 2021 · 1 min · jiezi

关于rpc:基于RPC的抖音直播爬虫

本案例是基于RPC的抖音web直播数据采集。 文章内容仅供参考学习,如有侵权请分割作者进行删除\可采集内容和页面出现内容雷同,包含用户评论、关注、谁来了、送礼物等数据。RPC(Remote Procedure Call)是近程调用的意思。 在Js逆向时,咱们本地能够和浏览器以服务端和客户端的模式通过websocket协定进行RPC通信,这样能够间接调用浏览器中的一些函数办法,不用去在意函数具体的执行逻辑,能够省去大量的逆向调试工夫。 像抖音直播间的数据传输采纳的是protobuf,如果齐全解析的话切实是浪费时间,不适宜做案例教程。 还有重要的一点是,通过RPC的办法能够不必搞加密参数signature,开一个页面就能够了。 接口分析首先通过控制台进行抓包,一般的get申请。有加密参数signature,不过咱们不须要搞。 然而返回的是通过 protobuf 序列化数据。 更多内容请订阅专栏,查看原文。 专栏链接:https://blog.csdn.net/weixin_... 原文链接:https://blog.csdn.net/weixin_...

August 11, 2021 · 1 min · jiezi

关于rpc:RPC框架的IDL与IDLless

IDL,Interface description language,即接口描述语言。 IDL是一种很有用的工具,它提供了对接口的形容,约定了接口协议。使得通信单方通信时,无需再发送 scheme,无效进步了通信数据的荷载比。 但对于RPC框架而言,IDL又不仅仅是一个接口描述语言。对于市面上绝大多数的RPC框架而言,IDL还是一个工具和一种应用过程,专指依据 IDL 形容文件,用指定的开发语言,生成对应的服务端接口模块,和客户端程序。这样的益处是,便于开发者疾速开发。 初衷是好的,然而,问题随同而来。 首先,根据 IDL 生成对应的客户端和接口模块,这个实质是编译。 但对于编译和语言的惯例了解和意识,却导致了问题的复杂化:IDL成了一门“编译型”的语言,倒退出了一套简单的规定和语法。而且不同 RPC 框架的 IDL 语言不齐全一样。每用一个新的框架,就有一套新的IDL语法和规定,就得重新学习一遍。 其次,市面上绝大多数的IDL语法,对于可选参数和可变类型参数,以及不定长参数存在着不同水平的反对问题。 晚期的 IDL,所有的接口字段必须存在。即便是无用的,也须要赋予一个诸如 'nil' 一类的值。否则就是违反 IDL 的标准。随着技术的倒退,之后的 IDL,呈现了 optional 关键字,但仅仅是不得已的状况下,才被举荐应用。而此时,被强烈推荐应用的却是关键字 require 以及 required。比方 Thrift,ProtoBuf 1.0 都是典型的代表。直到技术一直倒退,RPC 畛域的教训积攒越来越多,关键字 require 以及 required 才不再被持续举荐应用,而关键字 optional 成为 IDL 的新宠。可选参数,到目前为止还没有遇到什么问题。直到 IDL 开始编译,生成对应的客户端和接口代码时,新的问题呈现。 接口代码的生成,确实不便了网络服务和接口调用的开发。但适度简单的接口代码,间接导致了接口的强耦合:所有的业务都依赖于 IDL 生成的客户端和服务端的接口,如果一个变动,其余的须要全副追随变动。如果一个接口被改变,与之关联的所有服务和客户端,必须全副从新编译,否则极有可能在 IDL 生成的代码中,呈现不兼容的问题。 最典型的就是,加了一个新的参数,但不在协定数据的开端,那绝大多数 RPC 框架原有的接口在解决新版接口数据时,便会呈现兼容性谬误。 当然,这在很长一段时间内被视为天经地义。 但视之“天经地义”,却不障碍开发者们对其导致的问题熟视无睹,于是不少IDL为了绕过这个问题,创造了“序号/字段编号”这种本不应该存在的语言标识。 此外,传统的 IDL 一旦遇到参数的缩小,或者参数类型的更改,IDL 所生成的 RPC 框架的接口代码,则往往须要两个独立的接口处理函数,能力同时解决新旧两个版本的数据。所以,为了更加“优雅”的解决参数的删除和类型的扭转,局部 IDL 加强了对“序号/字段编号”这种本不应该存在的语言标识的依赖。字段编号不可反复,一旦确定了,便不能批改。于是字段编号的保护,便成了开发者历史包袱的一部分。 而后对于不定类型的参数,反对该个性的 IDL 会选用 Oneof 或 Union 等来实现,而剩下的 IDL 则间接弃疗。 ...

August 6, 2021 · 2 min · jiezi

关于rpc:分布式RPC服务调用框架选型使用Dubbo实现分布式服务调用

Dubbo概念Dubbo是一个高性能,轻量级的RPC分布式服务框架提供了三外围能力: 面向接口的近程办法调用(@Reference)智能容错负载平衡Dubbo特点: 依照分层的形式来架构,能够使各个层之间解耦合Dubbo的角色: 提供方:Provider生产方:ConsumerDubbo的提供非常简单的服务模型,要么是提供方提供服务,要么是生产方生产服务 Dubbo的服务治理通明近程调用: 调用近程办法就像调用本地办法一样,只需简略配置,没有任何API侵入负载平衡机制: Client端LB,在内网代替F5等硬件负载均衡器容错重试机制: 服务Mock数据,重试次数,超时机制主动注册发现: 注册核心基于接口名查问服务提供者的IP地址,能够增加和删除服务提供者性能日志监控: Monitor,统计服务的调用次数和调用工夫的监控核心服务治理核心: 路由规定,动静配置,服务降级,访问控制,权重调整,负载平衡 Dubbo的外围性能Remoting: 近程通信,提供对多种NIO框架形象封装,包含"同步转异步"和"申请-响应"模式的信息替换形式Cluster: 服务框架,提供基于接口办法的通明近程过程调用,包含:多协定反对,软负载平衡,容错重试,路由规定,动静配置等集群反对Registry: 服务注册核心,服务主动发现.基于注册核心目录服务,使服务生产方能动静地查找服务提供方,使地址通明,使服务提供方能够平滑地减少和缩小机器 通信模型:BIO : 同步并阻塞NIO : 异步并阻塞AIO : 异步非阻塞通信框架 : nettyDubbo组件角色组件角色阐明Provider裸露服务的服务提供方Consumer调用近程服务的服务生产方Registry服务注册与发现的注册核心Monitor统计服务调用次数和调用工夫的监控核心Container服务运行容器组件调用关系阐明服务容器Container负责启动,加载,运行服务提供者服务提供者Provider在启动时,向注册核心注册本人提供的服务服务消费者Consumer在启动时,向注册核心订阅本人所需的服务注册核心Registry返回服务提供者地址列表给消费者,如果有变更,注册核心将基于长连贯推送变更数据给消费者服务消费者Consumer从提供者地址列表中,基于负载平衡算法,抉择一台提供者进行调用,如果调用失败,再选另一台进行调用服务消费者Consumer和服务提供者Provider,在内存中累计调用次数和调用工夫,定时每分钟发送一次统计数据到监控核心 Dubbo Admin治理控制台治理控制台的次要性能: 路由规定动静配置服务降级访问控制权限调整负载平衡

May 18, 2021 · 1 min · jiezi

关于rpc:工商银行分布式服务C10K场景的解决方案

简介:将来,中国工商银行将继续致力于 Dubbo 的金融级规模化利用。 作者:颜高飞,微服务畛域架构师,次要从事服务发现、高性能网络通信等研发工作,善于 ZooKeeper、Dubbo、RPC 协定等技术方向。 Dubbo是一款轻量级的开源Java服务框架,是泛滥企业在建设分布式服务架构时的首选。中国工商银行自2014年开始摸索分布式架构转型工作,基于开源Dubbo自主研发建设了分布式服务平台。Dubbo框架在提供方生产方数量较小的服务规模下,运行稳固、性能良好。 随着银行业务线上化、多样化、智能化的需要越来越旺盛,在可预感的将来,会呈现一个提供方为数千个、甚至上万个生产方提供服务的场景。在如此高负载量下,若服务端程序设计不够良好,网络服务在解决数以万计的客户端连贯时、可能会呈现效率低下甚至齐全瘫痪的状况,即为C10K问题。那么,基于dubbo的分布式服务平台是否应答简单的C10K场景?为此,咱们搭建了大规模连贯环境、模仿服务调用进行了一系列摸索和验证。 C10K场景下Dubbo服务调用呈现大量交易失败筹备环境: 应用dubbo2.5.9(默认netty版本为3.2.5.Final)版本编写服务提供方和对应的服务生产方。提供方服务办法中无理论业务逻辑、仅sleep 100ms;生产方侧配置服务超时工夫为5s,每个生产方启动后每分钟调用1次服务。筹备1台8C16G服务器以容器化形式部署一个服务提供方,筹备数百台8C16G服务器以容器化形式部署7000个服务生产方。启动dubbo监控核心,以监控服务调用状况。 定制验证场景,察看验证后果: 验证状况不尽如人意,C10K场景下dubbo服务调用存在超时失败的状况。如果分布式服务调用耗时长,从服务生产方到服务提供方全链路节点都会长工夫占用线程池资源,减少了额定的性能损耗。而当服务调用并发突增时,很容易造成全链路节点梗塞,从而影响其余服务的调用,并进一步造成整个服务集群性能降落甚至整体不可用,导致产生雪崩。服务调用超时问题不可漠视。因而,针对该C10K场景下dubbo服务调用超时失败状况咱们进行了详细分析。 C10K场景问题剖析依据服务调用交易链路,咱们首先狐疑交易超时是因为提供方或生产方本身过程卡顿或网络存在提早导致的。 因而,咱们在存在交易失败的提供方、生产方服务器上开启过程gc日志,屡次打印过程jstack,并在宿主机进行网络抓包。 察看gc日志、jstack 提供方、生产方过程gc时长、gc距离、内存应用状况、线程堆栈等无显著异样,临时排除gc触发stop the world导致超时、或线程设计不当导致阻塞而超时等猜测。 针对以上两种场景下的失败交易,别离察看网络抓包,对应有以下两种不同的景象:针对场景1:提供方稳固运行过程中交易超时。 跟踪网络抓包及提供方、生产方交易日志。生产方发动服务调用申请发动后,在提供方端迅速抓到生产方申请报文,但提供方从收到申请报文到开始解决交易耗时2s+。 同时,察看交易申请响应的数据流。提供方业务办法处理完毕后到向生产方发送回包之间也耗时2s+,尔后生产方端迅速收到交易返回报文。但此时交易总耗时已超过5s、超过服务调用超时工夫,导致抛出超时异样。 由此,判断导致交易超时的起因不在生产方侧,而在提供方侧。 针对场景2:提供方重启后大量交易超时。 服务调用申请发动后,提供方迅速收到生产方的申请报文,但提供方未失常将交易报文递交给应用层,而是回复了RST报文,该笔交易超时失败。 察看在提供方重启后1-2分钟内呈现大量的RST报文。通过部署脚本,在提供方重启后每隔10ms打印established状态的连接数,发现提供方重启后连接数未能迅速复原到7000,而是通过1-2分钟后连接数才复原至失常数值。而在此过程中,逐台生产方上查问与提供方的连贯状态,均为established,狐疑提供方存在单边连贯状况。 咱们持续别离剖析这两种异样场景。 场景1:提供方理论交易前后均耗时长、导致交易超时细化收集提供方的运行状态及性能指标: 在提供方服务器上每隔3s收集服务提供方jstack,察看到netty worker线程每60s左右频繁解决心跳。同时打印top -H,察看到占用cpu工夫片较多的线程排名前10中蕴含9个netty worker线程。因提供方服务器为8C,dubbo默认netty worker线程数为9个,即所有9个netty worker线程均较繁忙。部署服务器零碎性能采集工具nmon,察看到cpu每隔60秒左右产生毛刺;雷同工夫网络报文数也有毛刺。部署ss -ntp间断打印网络接管队列、发送队列中的数据积压状况。察看到在耗时长的交易工夫点左近队列沉积较多。Dubbo服务框架中提供方和生产方发送心跳报文(报文长度为17)的周期为60s,与以上距离靠近。联合网络抓包,耗时长的交易工夫点左近心跳包较多。依据Dubbo框架的心跳机制,当生产方数量较大时,提供方发送心跳报文、需应答的生产方心跳报文将会很密集。因而,狐疑是心跳密集导致netty线程繁忙,从而影响交易申请的解决,继而导致交易耗时减少。 进一步剖析netty worker线程的运行机制,记录每个netty worker线程在解决连贯申请、解决写队列、解决selectKeys这三个关键环节的解决耗时。察看到每距离60s左右(与心跳距离统一)解决读取数据包较多、耗时较大,期间存在交易耗时减少的状况。同一时间察看网络抓包,提供方收到较多的心跳报文。 因而,确认以上狐疑。心跳密集导致netty worker线程繁忙,从而导致交易耗时增长。 场景2:单边连贯导致交易超时剖析单边连贯产生的起因 TCP建设连贯三次握手的过程中,若全连贯队列满,将导致单边连贯。 全连贯队列大小由零碎参数net.core.somaxconn及listen(somaxconn,backlog)的backlog取最小值决定。somaxconn是Linux内核的参数,默认值是128;backlog在创立Socket时设置,dubbo2.5.9中默认backlog值是50。因而,生产环境全连贯队列是50。通过ss命令(Socket Statistics)也查得全连贯队列大小为50。 察看TCP连贯队列状况,证实存在全连贯队列溢出的景象。 即:全连贯队列容量有余导致大量单边连贯产生。因在本验证场景下,订阅提供方的生产方数量过多,当提供方重启后,注册核心向生产方推送提供方上线告诉,所有生产方简直同时与提供方重建连贯,导致全连贯队列溢出。 剖析单边连贯影响范畴 单边连贯影响范畴多为生产方首笔交易,偶发为首笔开始间断失败2-3笔。建设为单边的连贯下,交易非必然失败。三次握手全连贯队列满后,若半连贯队列闲暇,提供方创立定时器向生产方重传syn+ack,重传默认5次,重传距离以倍数增长,1s..2s..4s..共31s。在重传次数内,若全连贯队列复原闲暇,生产方应答ack、连贯建设胜利。此时交易胜利。 在重传次数内,若全连贯队列依然繁忙,新交易达到超时工夫后失败。达到重传次数后,连贯被抛弃。尔后生产方发送申请,提供方应答RST。后交易达到超时工夫失败。 依据Dubbo的服务调用模型,提供方发送RST后,生产方抛出异样Connection reset by peer,后断开与提供方的连贯。而生产方无奈收到以后交易的响应报文、导致超时异样。同时,生产方定时器每2s检测与提供方连贯,若连贯异样,发动重连,连贯复原。尔后交易失常。 C10K场景问题剖析总结总结以上造成交易超时的起因有两个: 心跳机制导致netty worker线程繁忙。在每个心跳工作中,提供方向所有1个心跳周期内未收发过报文的生产方发送心跳;生产方向所有1个心跳周期内未收发过报文的提供方发送心跳。提供方上所连贯的生产方较多,导致心跳报文沉积;同时,解决心跳过程耗费较多CPU,影响了业务报文的解决时效。全连贯队列容量有余。在提供方重启后该队列溢出,导致大量单边连贯产生。单边连贯下首笔交易大概率超时失败。 下一步思考针对以上场景1:如何能升高单个netty worker线程解决心跳的工夫,减速IO线程的运行效率?初步构想了如下几种计划:• 升高单个心跳的解决耗时• 减少netty worker线程数,升高单个IO线程的负载• 打散心跳,防止密集解决针对以上场景2:如何躲避首笔大量半连贯导致的交易失败?构想了如下计划:• 减少TCP全连贯队列的长度,波及操作系统、容器、Netty• 进步服务端accept连贯的速度 交易报文解决效率晋升逐层优化基于以上构想,咱们从零碎层面、dubbo框架层面进行了大量的优化,以晋升C10K场景下交易解决效率,晋升服务调用的性能容量。优化内容包含以下方面: 具体波及优化的框架层如下: 经对各优化内容逐项验证,各措施均有不同水平的晋升,成果别离如下: 综合优化验证成果综合使用以上优化成果最佳。在此1个提供方连贯7000个生产方的验证场景下,重启提供方后、长时间运行无交易超时场景。比照优化前后,提供方CPU峰值降落30%,生产方与提供方之间解决时差管制在1ms以内,P99交易耗时从191ms降落至125ms。在晋升交易成功率的同时,无效缩小了生产方等待时间、升高了服务运行资源占用、晋升了零碎稳定性。 ...

April 29, 2021 · 1 min · jiezi

关于rpc:Thrift-框架的学习

因为一些历史起因须要在不同服务器之间建设rpc通道,来满足不定时、不定量且数据量可能会大的函数须要。在简略比照了之后投向了thrift,尽管网上大佬们说thrift的文档做的很烂,但具体有多烂这个我也不晓得,可能根底需要来讲也不重要。 Q: 为什么抉择rpc而不是RESTful API的形式?A:资源粒度。RPC 就像本地办法调用,RESTful API 每一次增加接口都可能须要额定地组织凋谢接口的数据,这相当于在利用视图中再写了一次办法调用,而且它还须要保护开发接口的资源粒度、权限等;流量耗费。RESTful API 在应用层应用 HTTP 协定,哪怕应用轻型、高效、传输效率高的 JSON 也会耗费较大的流量,而 RPC 传输既能够应用 TCP 也能够应用 UDP,而且协定个别应用二制度编码,大大降低了数据的大小,缩小流量耗费。Thrift的网络栈构造: 上面两层的数据传输: TTransport层代表thrift的数据传输方式,thrift定义了如下几种罕用数据传输方式 TSocket: 阻塞式socket;TFramedTransport: 以frame为单位进行传输,非阻塞式服务中应用;TFileTransport: 以文件模式进行传输;TProtocol层代表thrift客户端和服务端之间传输数据的协定,艰深来讲就是客户端和服务端之间传输数据的格局(例如json等),thrift定义了如下几种常见的格局 TBinaryProtocol: 二进制格局;TCompactProtocol: 压缩格局;TJSONProtocol: JSON格局;TSimpleJSONProtocol: 提供只写的JSON协定;thrift反对的Server模型thrift次要反对以下几种服务模型 TSimpleServer: 简略的单线程服务模型,罕用于测试;TThreadPoolServer: 多线程服务模型,应用规范的阻塞式IO;TNonBlockingServer: 多线程服务模型,应用非阻塞式IO(须要应用TFramedTransport数据传输方式);THsHaServer: THsHa引入了线程池去解决,其模型读写工作放到线程池去解决,Half-sync/Half-async解决模式,Half-async是在解决IO事件上(accept/read/write io),Half-sync用于handler对rpc的同步解决;thrift IDL文件thrift IDL不反对无符号的数据类型,因为很多编程语言中不存在无符号类型,thrift反对以下几种根本的数据类型 byte: 有符号字节i16: 16位有符号整数i32: 32位有符号整数i64: 63位有符号整数double: 64位浮点数string: 字符串此外thrift还反对以下容器类型: list: 一系列由T类型的数据组成的有序列表,元素能够反复;set: 一系列由T类型的数据组成的无序汇合,元素不可反复;map: 一个字典构造,Key为K类型,Value为V类型,相当于java中的HashMap;应用python编写thrift客户端代码执行thrift --gen py src/thrift/data.thrift生成对应的python代码,并引入到python工程当中,代码构造如下图所示python客户端代码 # -*- coding:utf-8 -*-__author__ = 'kpzhang'from py.thrift.generated import PersonServicefrom py.thrift.generated import ttypesfrom thrift import Thriftfrom thrift.transport import TSocketfrom thrift.transport import TTransportfrom thrift.protocol import TCompactProtocolimport sysreload(sys)sys.setdefaultencoding('utf8')try: tSocket = TSocket.TSocket("localhost", 8899) tSocket.setTimeout(900) transport = TTransport.TFramedTransport(tSocket) protocol = TCompactProtocol.TCompactProtocol(transport) client = PersonService.Client(protocol) transport.open() person = client.getPersonByUsername("张三") print person.username print person.age print person.married print '---------------------' newPerson = ttypes.Person(); newPerson.username = "李四" newPerson.age = 30 newPerson.married = True client.savePerson(newPerson) transport.close()except Thrift.TException, tx: print '%s' % tx.message参考:thrift基本原理 ...

February 25, 2021 · 1 min · jiezi

关于rpc:RPC设计概要

前言RPC全程近程办法调用,曾经在各大小公司被宽泛应用,品种也是很多比方:Dubbo,Spring cloud那一套,GRPC,Thrift,可能还有很多公司自研的等等;每个公司都可能依据本人的业务需要,场景抉择本人适合的RPC框架;但大体的考查维度无非就这么几个:性能,可扩展性,跨平台,功能性,可监控,应用性;所以咱们如果要设计一个RPC框架,能够从这几个角度去思考。 性能作为微服务中的外围组件,在一个零碎中RPC的调用量往往是很高的,所以性能是一个很重要的思考点;既然是近程调用,必然牵扯到网络连接,而I/O模型的抉择间接影响到性能,网络的长连贯短连贯,序列化形式也都影响性能; 1.I/O模型常见的Unix5种I/O模型别离是:阻塞I/O,非阻塞I/O,I/O复用(select,poll,epoll等反对I/O多路复用),信号驱动I/O,异步I/O;从晚期的阻塞I/O形式只能创立大量的线程来保障每个用户互不影响,到当初宽泛应用的I/O多路复用模型,再到异步I/O;从select模型到当初支流的epoll模型,性能有了质的降级;当然咱们没必要本人去实现,能够间接应用网络通讯框架Netty,Mina等; 2.长连贯短连贯短连贯示意每次通信完就敞开连贯,而长连贯通信完持续放弃连贯,这样下次再通信就不须要从新建设连贯了,如果通信频繁,很显著长连贯性能更高;然而长连贯须要做一些额定的工作,比方保活解决;另外就是如果客户端太多的话,服务器端是无奈撑持的。 3.序列化形式网络传输中的数据都须要通过序列化和反序列化解决,所以这一块的性能也很重要;常见的序列化包含:json和二进制形式,json常见的如fastjson,jackson等,二进制如protobuf,thrift ,kryo等;这个能够别离从序列化的性能,大小,以及应用方便性思考;当然稳定性和安全性也须要思考,比方fastjson频繁爆出安全漏洞; 4.协定这里次要讲的是应用层协定,RPC个别都会自定义协定,当然也有间接应用现有协定的比方http协定;自定义协定能够本人掌控,协定能够做的很小很精简,当然解码和编码须要本人去实现;如果应用现有的http协定,相对来说整个协定包是比拟大的,然而曾经是一种标准了,很多货色能够间接拿来用,更加通用; 可扩展性可扩展性能够从两个角度来看,一个是服务提供方和生产方的负载平衡策略;另一个就是用户能够对框架进行自定义扩大; 1.负载平衡RPC的两个外围组件服务提供方和生产方,须要提供横向扩大的机制,用以达到更高的负责,比方Spring Cloud、Dubbo提供的注册核心,而后再联合容错机制达到负载平衡的成果,很容易达到横向扩大; 2.SPI机制一个好的框架是反对用户自定义的,用户依据本人的需要实现自定义扩大;JDK提供了SPI机制,很常见的一个场景就是数据库驱动;另外Dubbo在此基础上提供了更加弱小的SPI机制;这种扩大对RPC来说是多方面的,能够是底层的通信框架扩大,序列化扩大,注册核心扩大,容错机制扩大,协定扩大等等; 跨平台当初开发语言多种多样,如果能做成跨平台,当然是一大劣势,然而可能往往为了跨平台,会在一些中央做衡量退让,当然难度也更大;所以咱们在实现一个RPC时须要明确本人的定位,就是针对某一种语言的还是跨平台反对;常见的反对跨平台的RPC框架如GRPC,Thrift;实现机制能够参考一下,都有本人的一套接口定义语言IDL而后通过不同的代码生成器,生成各种编程语言生产端和服务器端代码,来保障不同语言间接的相干通信。 功能性作为一个RPC框架,除了最外围的通信模块,序列化模块之外,功能性模块也很重要,往往为开发者节俭了大量的工夫,开发者可能因为RPC的某个性能而抉择用此框架;常见的性能包含:容错机制,负载平衡机制,同步异步调用,后果缓存,路由规定,服务降级,多版本,线程模型等; 1.容错机制在盘根错节的网络环境中,近程调用失败再失常不过了,容错机制就显得十分有必要了,常见的容错机制比方:失败重试,疾速失败,失败间接疏忽,并行调用多个服务器只有一个胜利即返回等;用户能够依据需要抉择本人适合的容错计划; 2.负责平衡下面提到服务提供方和生产方的可扩展性,生产方面对多个提供方的时候须要有肯定的负载平衡策略,来保证系统的稳定性;常见的策略如:随机,轮询,起码沉闷调用数,一致性hash等; 3.同步异步调用常见的同步调用有些场景无奈满足需要,比方同时须要调用多个近程办法,而这其中可能有些执行比较慢的;这时候异步调用就显得重要了,能够同时异步发送多个申请,等待时间就是响应最慢的申请;具体能够通过Future,CompletableFuture来实现; 4.后果缓存缓存始终是性能的不二法宝,某些场景下可能对服务提供方响应的数据实时要求性并不高,这时候如果能够在服务提供端提供后果的缓存机制,那么在性能上是一个很大的晋升;能够本人设计一个本地缓存,当然也能够间接整合第三方缓存框架比方ehcache,jcache等; 5.路由规定服务提供方往往是很多的,用户可能有一些非凡的需要,能够依照本人定义个规定来做路由,比方咱们常常做的灰度公布,联合RPC提供的路由规定来实现会很简略;此规定可能是条件表达式,脚本,标签等,咱们设计的RPC框架须要有一个给用户定义规定的中央,比方通过注册核心来实时推送,另外还须要相干的引擎来解决规定,比方脚本引擎; 6.服务降级零碎的高可用性准则中,很重要的一条就是降级解决,在一些非核心的性能中,能够在呈现超时/故障时或者间接设置为降级服务,给一个对立的响应,这样能够把贵重的资源留给那些外围的性能;能够参考Dubbo的实现,向注册核心写入动静配置笼罩规定,从而实现实时降级解决; 7.多版本接口降级时常有的事件,咱们可能会为了兼容之前的版本搜索枯肠,但有时候还是无能为力,这时候多版本就显得很重要了,能够让两个版本同时存在,等到适合的机会在缓缓降级;像dubbo这种服务的维度就是服务名+版本号,所以很好实现多版本;而Spring Cloud维度没有到版本号,能够通过路由规定去实现; 8.线程模型在零碎的高可用准则中,线程隔离是一条重要的准则,为什么要做隔离,能够拿Dubbo及其底层通信框架netty为例,netty作为通信框架自身是有本人的线程模型,如果业务解决线程间接应用底层的通信线程模型,这样就会呈现因为业务阻塞而导致通信线程模型阻塞;这时Dubbo提供本人的线程模型就尤为重要,能够做到线程隔离,业务线程不影响通信线程; 当然作为一个RPC框架实现的性能能够很多,这里次要讲一些咱们平时用的比拟多的性能; 可监控一个运行稳固的零碎,没有一个专门的监控平台是不行的,当然RPC也不例外,常见的比方dubbo的monitor模块,Spring Cloud Admin等;对于一个RPC咱们次要监控:服务提供者有哪些,服务消费者有哪些,以及它们的状态,最好还有一些统计性能比方一段时间内的调用量,成功率,失败率,均匀响应工夫,最大响应工夫,最大并发量等等; 应用性最初说一下,咱们设计进去的框架最终还是要给用户应用的,所以用户是否能够不便的应用也是一个很重要的点,某些框架可能就是因为应用繁琐导致最终被弃用;比方注解的形式相比拟xml的形式就简略不少;还有比方服务维度来说:dubbo维度是接口,而Spring cloud维度是利用,整体来看Spring cloud应用起来更加不便;当然简略的API,文档以及Demo对开发者来说也是必不可少的; 总结RPC实质上其实就是一次网络调用,很多设计其实都是在围绕,如何把它变成一个高可用,高并发的框架;其实这些设计实践实用于大部分的零碎,都在为达到此指标而致力。 感激关注能够关注微信公众号「回滚吧代码」,第一工夫浏览,文章继续更新;专一Java源码、架构、算法和面试。

December 15, 2020 · 1 min · jiezi

关于rpc:grpc-opentrace-jaeger实现

1) RPCRemote Procedure Call 1) 概览 2) 劣势简略、通用、平安、效率2) GRPC1) 简介gRPC 是一个高性能、开源和通用的 RPC 框架,面向挪动和 HTTP/2 设计ps: 多语言反对 C++C#DartGoJavaNode.jsObjective-CPHPPythonRuby2) 概览 3) Protocol BuffersProtocol Buffers是一种与语言无关,平台无关的可扩大机制,用于序列化结构化数据。应用Protocol Buffers能够一次定义结构化的数据,而后能够应用非凡生成的源代码轻松地在各种数据流中应用各种语言编写和读取结构化数据 示例 example.proto syntax = "proto3"; // 版本申明,应用Protocol Buffers v3版本package pb; // 包名// 定义服务service Trainee { // 办法 rpc Sing (HelloRequest) returns (HelloReply) {} // 办法 rpc Dance (HelloRequest) returns (HelloReply) {} // 办法 rpc Rap (HelloRequest) returns (HelloReply) {}}// 申请音讯message HelloRequest { string name = 1;}// 响应音讯message HelloReply { string message = 1;}3) gongjiayun-notification工家云音讯零碎 ...

November 30, 2020 · 1 min · jiezi

关于rpc:杂记

这几天看一个rpc库(https://github.com/rpclib/rpc...,server端的注册函数返回类型以及参数类型能够任意,client端只有填充对应类型的参数即可实现调用:Server: #include <iostream>#include "rpc/server.h"void foo() { std::cout << "foo was called!" << std::endl;}int main(int argc, char *argv[]) { // Creating a server that listens on port 8080 rpc::server srv(8080); // Binding the name "foo" to free function foo. // note: the signature is automatically captured srv.bind("foo", &foo); // Binding a lambda function to the name "add". srv.bind("add", [](int a, int b) { return a + b; }); // Run the server loop. srv.run(); return 0;}Client: ...

November 15, 2020 · 2 min · jiezi

关于rpc:RPC-笔记

如何发现服务繁难实现:指定服务者的IP和端口。调用时提供办法名和参数。简单实现:通过服务注册和发现核心拉去信息失败解决与调用次数At Least Once 起码一次RPC调用期待指定工夫在指定工夫后如果没有返回后果,重试若干次在重试均失败后,返回谬误提醒仅适宜于申请操作是幂等性的,也就是说屡次调用都会返回雷同的后果,因而通常适宜读取操作。 At Most Once 最多一次调用最多进行一次——要么齐全不执行,要么执行一次。须要测验反复的数据包,比方对于反复的申请会返回之前的响应而不是从新进行解决。

September 30, 2020 · 1 min · jiezi

关于rpc:被喷了聊聊我开源的RPC框架那些事

前段时间利用业余时间写了一个简略的 RPC 框架,破费了不少精力。开源进去之后,少部分不太敌对的技术人站在上帝视角说了风凉话。就很好受,兄弟,谁还没有一个玻璃心。 简略吐槽一波,给大家聊聊对于 guide-rpc-framework 的一些事件。 01 我的自定义 RPC 框架近况关注我的大部分小伙伴应该都晓得,3个月前,我利用业余时间手写一个简略的 RPC 框架(玩具),名字叫做 guide-rpc-framework。 目前的话,这个我的项目曾经有 0.5k 的 star。感激小伙伴们的反对! 写这个 RPC 框架的次要目标是为了集体学习,开源进去的目标次要是想帮忙到更多人。 02 开源的魅力开源进去之后,大部小伙伴都是比较支持的,有很多小伙伴都参加了进来一起欠缺。 这里点名褒扬一下Github用户名为 sakuragi1111 和 smile2coder 这两位老哥。 sakuragi1111 这位老哥通过参考 Dubbo 源码实现了 SPI 机制。 smile2coder 这位老哥为 guide-rpc-framework 增加了通过注解实现服务生产的性能。 目前的话, guide-rpc-framework 曾经反对通过注解进行服务生产和注册。 程序世界,什么样的人都有,有人感激你,也会有人贬斥你。 03 不那么好的声音在我的 guide-rpc-framework 开源之后,也常常会受到像:“你有本事别用现成的框架写一个啊?”、“你这个写的一点亮点都没有,有啥意思?”、“都有了 Dubbo 之后,为啥还要本人写一个?”、“反复造轮子没意义”......之类的不太友善的话语。 说句心里话,个别说进去这种话的人往往技术水平很低。 如果,你指出我哪里写的不好,我很乐意地去批改。然而,你站在上帝视角说着风凉话,那就是人品有问题了。 1.为什么不能利用现成的框架呢?(比方为啥不必 JDK NIO 而用 Netty?) 毫不夸大地说:开源进去的货色,就是整体技术人独特的财产。 Netty 比 NIO 更好用、更欠缺,我为啥还要间接应用 NIO呢?咱们平时常常接触的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty 啊。 ...

September 17, 2020 · 1 min · jiezi

从零开始手写-dubbo-rpc-框架

rpcrpc 是基于 netty 实现的 java rpc 框架,类似于 dubbo。 主要用于个人学习,由渐入深,理解 rpc 的底层实现原理。 前言工作至今,接触 rpc 框架已经有很长时间。 但是对于其原理一直只是知道个大概,从来没有深入学习过。 以前一直想写,但由于各种原因被耽搁。 技术准备Java 并发实战学习 TCP/IP 协议学习笔记 Netty 权威指南学习 这些技术的准备阶段,花费了比较长的时间。 也建议想写 rpc 框架的有相关的知识储备。 其他 rpc 框架使用的经验此处不再赘述。 快速迭代原来一直想写 rpc,却不行动的原因就是想的太多,做的太少。 想一下把全部写完,结果就是啥都没写。 所以本次的开发,每个代码分支做的事情实际很少,只做一个功能点。 陆陆续续经过近一个月的完善,对 rpc 框架有了自己的体会和进一步的认知。 代码实现功能,主要参考 Apache Dubbo 文档文档文档将使用 markdown 文本的形式,补充 code 层面没有的东西。 代码注释代码有详细的注释,便于阅读和后期维护。 测试目前测试代码算不上完善。后续将陆续补全。 rpc 模块rpc-common 公共代码 rpc-server 服务端 rpc-client 客户端 rpc-register 注册中心 rpc-test 测试模块 代码分支release_0.0.1-server 服务端启动 release_0.0.2-client 客戶端启动 release_0.0.3-客户端调用服务端 release_0.0.4-p2p 客户端主动调用服务端 release_0.0.5-serial 序列化 ...

November 2, 2019 · 1 min · jiezi

基于Netty实现简易RPC框架

前言现在网上有很多关于使用Netty来构建RPC框架的例子,为什么我这里还要写一篇文章进行论述呢,我很清楚我可能没有写得他们那么好。之所以还要写,有两点原因: 一是因为学过Netty之后,还需要去不断实践才能更好的把握Netty的用法,显然,基于Netty实现RPC框架是一个很好的做法;二是因为目前市面上有很多RPC框架,比如Dubbo,这些框架通讯底层都是Netty,所以说通过这个例子,也可以更好的去体验RPC的设计。下面我将从以下几点阐述如何基于Netty实现简易的RPC框架: RPC是什么?实现RPC框架需要关注哪些方面 ?使用Netty如何实现?<!--more--> RPC是什么?RPC,远程过程调用,可以做到像本地调用一样调用远程服务,是一种进程间的通信方式,概念想必大家都很清楚,在看到58沈剑写的RPC文章 之后,意识到其实我们可以换一种思考方式去理解RPC,也就是从本地调用出发,进而去推导RPC调用 1. 本地函数调用本地函数是我们经常碰到的,比如下面示例: public String sayHello(String name) { return "hello, " + name;}我们只需要传入一个参数,调用sayHello方法就可以得到一个输出,也就是输入参数——>方法体——>输出,入参、出参以及方法体都在同一个进程空间中,这就是本地函数调用 2. Socket通信那有没有办法实现不同进程之间通信呢?调用方在进程A,需要调用方法A,但是方法A在进程B中 最容易想到的方式就是使用Socket通信,使用Socket可以完成跨进程调用,我们需要约定一个进程通信协议,来进行传参,调用函数,出参。写过Socket应该都知道,Socket是比较原始的方式,我们需要更多的去关注一些细节问题,比如参数和函数需要转换成字节流进行网络传输,也就是序列化操作,然后出参时需要反序列化;使用socket进行底层通讯,代码编程也比较容易出错。 如果一个调用方需要关注这么多问题,那无疑是个灾难,所以有没有什么简单方法,让我们的调用方不需要关注细节问题,让调用方像调用本地函数一样,只要传入参数,调用方法,然后坐等返回结果就可以了呢? 3. RPC框架RPC框架就是用来解决上面的问题的,它能够让调用方像调用本地函数一样调用远程服务,底层通讯细节对调用方是透明的,将各种复杂性都给屏蔽掉,给予调用方极致体验。 RPC调用需要关注哪些方面前面就已经说到RPC框架,让调用方像调用本地函数一样调用远程服务,那么如何做到这一点呢? 在使用的时候,调用方是直接调用本地函数,传入相应参数,其他细节它不用管,至于通讯细节交给RPC框架来实现。实际上RPC框架采用代理类的方式,具体来说是动态代理的方式,在运行时动态创建新的类,也就是代理类,在该类中实现通讯的细节问题,比如参数序列化。 当然不光是序列化,我们还需要约定一个双方通信的协议格式,规定好协议格式,比如请求参数的数据类型,请求的参数,请求的方法名等,这样根据格式进行序列化后进行网络传输,然后服务端收到请求对象后按照指定格式进行解码,这样服务端才知道具体该调用哪个方法,传入什么样的参数。 刚才又提到网络传输,RPC框架重要的一环也就是网络传输,服务是部署在不同主机上的,如何高效的进行网络传输,尽量不丢包,保证数据完整无误的快速传递出去?实际上,就是利用我们今天的主角——Netty,Netty是一个高性能的网络通讯框架,它足以胜任我们的任务。 前面说了这么多,再次总结下一个RPC框架需要重点关注哪几个点: 代理 (动态代理)通讯协议序列化网络传输当然一个优秀的RPC框架需要关注的不止上面几点,只不过本篇文章旨在做一个简易的RPC框架,理解了上面关键的几点就够了 基于Netty实现RPC框架终于到了本文的重头戏了,我们将根据实现RPC需要关注的几个要点(代理、序列化、协议、编解码),使用Netty进行逐一实现 1. Protocol(协议)首先我们需要确定通信双方的协议格式,请求对象和响应对象 请求对象: @Data@ToStringpublic class RpcRequest { /** * 请求对象的ID */ private String requestId; /** * 类名 */ private String className; /** * 方法名 */ private String methodName; /** * 参数类型 */ private Class<?>[] parameterTypes; /** * 入参 */ private Object[] parameters;}请求对象的ID是客户端用来验证服务器请求和响应是否匹配响应对象: ...

October 6, 2019 · 6 min · jiezi

徒手撸一个简单的RPC框架

徒手撸一个简单的RPC框架之前在牛逼哄哄的 RPC 框架,底层到底什么原理得知了RPC(远程过程调用)简单来说就是调用远程的服务就像调用本地方法一样,其中用到的知识有序列化和反序列化、动态代理、网络传输、动态加载、反射这些知识点。发现这些知识都了解一些。所以就想着试试自己实现一个简单的RPC框架,即巩固了基础的知识,也能更加深入的了解RPC原理。当然一个完整的RPC框架包含了许多的功能,例如服务的发现与治理,网关等等。本篇只是简单的实现了一个调用的过程。 传参出参分析一个简单请求可以抽象为两步 那么就根据这两步进行分析,在请求之前我们应该发送给服务端什么信息?而服务端处理完以后应该返回客户端什么信息? 在请求之前我们应该发送给服务端什么信息?由于我们在客户端调用的是服务端提供的接口,所以我们需要将客户端调用的信息传输过去,那么我们可以将要传输的信息分为两类 第一类是服务端可以根据这个信息找到相应的接口实现类和方法第二类是调用此方法传输的参数信息那么我们就根据要传输的两类信息进行分析,什么信息能够找到相应的实现类的相应的方法?要找到方法必须要先找到类,这里我们可以简单的用Spring提供的Bean实例管理ApplicationContext进行类的寻找。所以要找到类的实例只需要知道此类的名字就行,找到了类的实例,那么如何找到方法呢?在反射中通过反射能够根据方法名和参数类型从而找到这个方法。那么此时第一类的信息我们就明了了,那么就建立相应的是实体类存储这些信息。 @Datapublic class Request implements Serializable { private static final long serialVersionUID = 3933918042687238629L; private String className; private String methodName; private Class<?> [] parameTypes; private Object [] parameters;}服务端处理完以后应该返回客户端什么信息?上面我们分析了客户端应该传输什么信息给服务端,那么服务端处理完以后应该传什么样的返回值呢?这里我们只考虑最简单的情况,客户端请求的线程也会一直在等着,不会有异步处理这一说,所以这么分析的话就简单了,直接将得到的处理结果返回就行了。 @Datapublic class Response implements Serializable { private static final long serialVersionUID = -2393333111247658778L; private Object result;}由于都涉及到了网络传输,所以都要实现序列化的接口如何获得传参信息并执行?-客户端上面我们分析了客户端向服务端发送的信息都有哪些?那么我们如何获得这些信息呢?首先我们调用的是接口,所以我们需要写自定义注解然后在程序启动的时候将这些信息加载在Spring容器中。有了这些信息那么我们就需要传输了,调用接口但是实际上执行的确实网络传输的过程,所以我们需要动态代理。那么就可以分为以下两步 初始化信息阶段:将key为接口名,value为动态接口类注册进Spring容器中执行阶段:通过动态代理,实际执行网络传输初始化信息阶段由于我们使用Spring作为Bean的管理,所以要将接口和对应的代理类注册进Spring容器中。而我们如何找到我们想要调用的接口类呢?我们可以自定义注解进行扫描。将想要调用的接口全部注册进容器中。 创建一个注解类,用于标注哪些接口是可以进行Rpc的 @Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface RpcClient {}然后创建对于@RpcClient注解的扫描类RpcInitConfig,将其注册进Spring容器中 public class RpcInitConfig implements ImportBeanDefinitionRegistrar{ @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { ClassPathScanningCandidateComponentProvider provider = getScanner(); //设置扫描器 provider.addIncludeFilter(new AnnotationTypeFilter(RpcClient.class)); //扫描此包下的所有带有@RpcClient的注解的类 Set<BeanDefinition> beanDefinitionSet = provider.findCandidateComponents("com.example.rpcclient.client"); for (BeanDefinition beanDefinition : beanDefinitionSet){ if (beanDefinition instanceof AnnotatedBeanDefinition){ //获得注解上的参数信息 AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition; String beanClassAllName = beanDefinition.getBeanClassName(); Map<String, Object> paraMap = annotatedBeanDefinition.getMetadata() .getAnnotationAttributes(RpcClient.class.getCanonicalName()); //将RpcClient的工厂类注册进去 BeanDefinitionBuilder builder = BeanDefinitionBuilder .genericBeanDefinition(RpcClinetFactoryBean.class); //设置RpcClinetFactoryBean工厂类中的构造函数的值 builder.addConstructorArgValue(beanClassAllName); builder.getBeanDefinition().setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); //将其注册进容器中 registry.registerBeanDefinition( beanClassAllName , builder.getBeanDefinition()); } } } //允许Spring扫描接口上的注解 protected ClassPathScanningCandidateComponentProvider getScanner() { return new ClassPathScanningCandidateComponentProvider(false) { @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent(); } }; }}由于上面注册的是工厂类,所以我们建立一个工厂类RpcClinetFactoryBean继承Spring中的FactoryBean类,由其统一创建@RpcClient注解的代理类 ...

July 12, 2019 · 3 min · jiezi

TiKV-源码解析系列文章九Service-层处理流程解析

作者:周振靖 之前的 TiKV 源码解析系列文章介绍了 TiKV 依赖的周边库,从本篇文章开始,我们将开始介绍 TiKV 自身的代码。本文重点介绍 TiKV 最外面的一层——Service 层。 TiKV 的 Service 层的代码位于 src/server 文件夹下,其职责包括提供 RPC 服务、将 store id 解析成地址、TiKV 之间的相互通信等。这一部分的代码并不是特别复杂。本篇将会简要地介绍 Service 层的整体结构和组成 Service 层的各个组件。 整体结构位于 src/server/server.rs 文件中的 Server 是我们本次介绍的 Service 层的主体。它封装了 TiKV 在网络上提供服务和 Raft group 成员之间相互通信的逻辑。Server 本身的代码比较简短,大部分代码都被分离到 RaftClient,Transport,SnapRunner 和几个 gRPC service 中。上述组件的层次关系如下图所示: 接下来,我们将详细介绍这些组件。 Resolver在一个集群中,每个 TiKV 实例都由一个唯一的 store id 进行标识。Resolver 的功能是将 store id 解析成 TiKV 的地址和端口,用于建立网络通信。 Resolver 是一个很简单的组件,其接口仅包含一个函数: pub trait StoreAddrResolver: Send + Clone { fn resolve(&self, store_id: u64, cb: Callback) -> Result<()>;}其中 Callback 用于异步地返回结果。PdStoreAddrResolver 实现了该 trait,它的 resolve 方法的实现则是简单地将查询任务通过其 sched 成员发送给 Runner。而 Runner 则实现了 Runnable<Task>,其意义是 Runner 可以在自己的一个线程里运行,外界将会向 Runner 发送 Task 类型的消息,Runner 将对收到的 Task 进行处理。 这里使用了由 TiKV 的 util 提供的一个单线程 worker 框架,在 TiKV 的很多处代码中都有应用。Runner 的 store_addrs 字段是个 cache,它在执行任务时首先尝试在这个 cache 中找,找不到则向 PD 发送 RPC 请求来进行查询,并将查询结果添加进 cache 里。 ...

July 8, 2019 · 3 min · jiezi

腾讯-Tars-Web-管理端用户体系对接

背景这段时间一直在基于 Tars 作开发。最近的文章也多是针对 Tars 的一些学习笔记。前面我们搭建了 Tars 基础框架,打开了 Tars web 管理界面进行服务的运维操作。不过读者肯定很快就会发现:这好像不用登录啊,那怎么保证只有有权限的用户才能更改服务呢? 显然 Tars web 是支持用户鉴权的。官方文档在这里。本文记录一下我的用户体系对接实验中的一些笔记,便于其他 Tars 的用户参阅。(特别是像我这样对 Node.js 不熟悉的小白……) 本系列文章: 腾讯 Tars 基础框架手动搭建——填掉官方 Guide 的坑腾讯 Tars-Go 服务 Hello World——从 HTTP 开始腾讯 Tars-Go 服务 Hello World——RPC 通信腾讯 Tars-Go 服务获取自定义模版(配置)值腾讯 Tars Web 管理端用户体系对接(本文)本文地址:https://segmentfault.com/a/1190000019657656 Tars 用户鉴权流程准备如果要启用 Tars web 的用户功能,那么首先开发者需要设计一个自己的用户登录服务。该服务是 http 服务,有独立的用户登录、登出功能。Tars Web 本身实现了一个简单的用户功能,不过本文我们重新设计一个。为便于说明,我们假设这个 Tars web 和用户服务 web 环境如下: Tars Web URL:https://tars.amc.com用户 Web URL:https://user.amc.com基本流程从用户通过浏览器访问 Tars web 管理平台开始,如果启用了用户功能,那么基本流程如下: 一言以蔽之:每当浏览器向 Tars web 发起一个请求时,Tars web 均向用户服务器发起请求,判断用户是否有权限;如果鉴权通过,则正常操作 Tars;如果没有,则重定向至用户登录页面。 ...

July 3, 2019 · 2 min · jiezi

RPC一thrift-框架-go语言开发

1、写 thrift 文件定义好 service :方法、入参出参2、生成 代码并发包3、编写 server 端实现4、本地启动server端,进行测试5、打包启动 rpc 服务 具体实现:1、写 thrift 文件定义好 service 以及 入参出参的 struct namespace 定义生成的文件目录和名称。 namespace py test_thrift.action_cardnamespace go test_thrift.action_cardnamespace java test_thrift.action_cardenum ResouceCode { ALPHA = 1, BETA = 2}序号:类型 名称struct BannerParams { 1: i64 member_id; 2: i32 num;}service MemberService { Banner get_banner(1: BannerParams params)}生成 py 或者 go 代码,并发布镜像gen: thrift -r --gen go:package_prefix=github.com/serenity/gen-go/ ./thrift_files/test/test.thrift 2、编写 server 端实现 package serviceimport ( "github.com/serenity/gen-go/test_thrift/member" "github.com/serenity/golang/pkg/action_card/controller" "github.com/serenity/golang/pkg/common/model")func GetMemberServiceProcessor(msgChan chan model.Message) *member.MemberServiceProcessor { // service - api memberService := controller.NewMemberService() // processor return member.NewMemberServiceProcessor(memberService)}3、客户端调用测试 ...

June 25, 2019 · 1 min · jiezi

php中使用protobuffer

Protobuf 简介protobuf(Protocol buffers)是谷歌出品的跨平台、跨语言、可扩展的数据传输及存储的协议,是高效的数据压缩编码方式之一。 Protocol buffers 在序列化数据方面,它是灵活的,高效的。相比于 XML 来说,Protocol buffers 更加小巧,更加快速,更加简单。一旦定义了要处理的数据的数据结构之后,就可以利用 Protocol buffers 的代码生成工具生成相关的代码。甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。 Protocol buffers 很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。 此外,Protobuf由于其在内网高效的数据交换效率,是被广泛应用于微服务的,在谷歌的开源框架grpc即是基于此构建起来的。 php-protobuf安装由于protobuf原生并不支持php,所以php如果使用pb则需要安装相应扩展。 pecl install protobuf环境中需要有protoc编译器,下载安装方式: $ wget https://github.com/google/protobuf/releases/download/v2.5.0/protobuf-2.5.0.tar.gz$ tar zxvf protobuf-2.5.0.tar.gz$ cd protobuf-2.5.0$ ./configure --prefix=/usr/local/protobuf$ sudo make $ sudo make install验证安装成功: $ /usr/local/protobuf/bin/protoc --versionlibprotoc 2.5.0php-protobuf安装成功 php --ri protobuf安装lumen和google/protobuf依赖lumen new rpclumen new rpc命令相当于composer create-project laravel/lumen rpccomposer require google/protobuf在composer.json下添加classmap: { "classmap": [ "protobuf/" ]}ok,准备工作都已做好了。 自己做一个demo在代码目录下创建一个protobuf文件夹mkdir protobuf 进入该目录,创建一个文件searchRequest.proto syntax = "proto3";message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } Corpus corpus = 4;}????此处很重要????在composer.json下添加classmap,否则将无法侦测到对应class{ "classmap": [ "protobuf/" ]}在命令行下运行:protoc --proto_path=protobuf/ --php_out=protobuf/ protobuf/searchRequest.proto && composer dump-autoload ...

June 18, 2019 · 2 min · jiezi

Swoole-RPC-的实现

概述这是关于 Swoole 学习的第七篇文章:Swoole RPC 的实现。 第六篇:Swoole 整合成一个小框架第五篇:Swoole 多协议 多端口 的应用第四篇:Swoole HTTP 的应用第三篇:Swoole WebSocket 的应用第二篇:Swoole Task 的应用第一篇:Swoole Timer 的应用有位读者说 “上篇文章,下载代码后直接运行成功,代码简洁明了,简直是 Swoole 入门最好的 Demo ”。 “哈哈哈...” 还有读者说 “有一起学习的组织群吗,可以在里面进行疑难答疑?” 这个还真没有,总觉得维护一个微信群不容易,因为自己本身就不爱在群里说话,另外,自己也在很多微信群中,开始氛围挺好的,大家都聊聊技术,后来技术聊的少了改成聊八卦啦,再后来慢慢就安静了,还有在群里起冲突的... 当然我也知道维护一个微信群的好处是非常大的,如果有这方面经验的同学,咱们一起交流交流 ~ 还有出版社找我写书的. 他们也真是放心,我自己肚子里几滴墨水还是知道的,目前肯定是不行,以后嘛,再说。 还有一些大佬加了微信,可能是出于对晚辈的提携吧,偷偷告诉你,从大佬的朋友圈能学到很多东西。 我真诚的建议,做技术的应该自己多总结总结,将自己会的东西写出来分享给大家,先不说给别人带来太多的价值,反正对自己的帮助是非常非常大的,这方面想交流的同学,可以加我,我可以给你无私分享。 可能都会说时间少,时间只要挤,总会有的,每个人都 24 小时,时间对每个人是最公平的。说到这推荐大家读一下《暗时间》这本书,这是我这本书的 读书笔记,大家可以瞅瞅。 开始今天的文章吧,这篇文章实现了一个简单的 RPC 远程调用,在实现之前需要先了解什么是 RPC,不清楚的可以看下之前发的这篇文章 《我眼中的 RPC》。 下面的演示代码主要使用了 Swoole 的 Task 任务池,通过 OnRequest/OnReceive 获得信息交给 Task 去处理。 举个工作中的例子吧,在电商系统中的两个模块,个人中心模块和订单管理模块,这两个模块是独立部署的,可能不在一个机房,可能不是一个域名,现在个人中心需要通过 用户ID 和 订单类型 获取订单数据。 实现效果客户端HTTP 请求//代码片段<?php$demo = [ 'type' => 'SW', 'token' => 'Bb1R3YLipbkTp5p0', 'param' => [ 'class' => 'Order', 'method' => 'get_list', 'param' => [ 'uid' => 1, 'type' => 2, ], ],];$ch = curl_init();$options = [ CURLOPT_URL => 'http://10.211.55.4:9509/', CURLOPT_POST => 1, CURLOPT_POSTFIELDS => json_encode($demo),];curl_setopt_array($ch, $options);curl_exec($ch);curl_close($ch);TCP 请求//代码片段$demo = [ 'type' => 'SW', 'token' => 'Bb1R3YLipbkTp5p0', 'param' => [ 'class' => 'Order', 'method' => 'get_list', 'param' => [ 'uid' => 1, 'type' => 2, ], ],];$this->client->send(json_encode($demo));请求方式SW 单个请求,等待结果发出请求后,分配给 Task ,并等待 Task 执行完成后,再返回。 ...

May 21, 2019 · 3 min · jiezi

聊聊-Apache-Dubbo

本文来自于我的个人主页:Apache Dubbo,转载请保留链接 ;)在2011年10月27日,阿里巴巴开源了自己的SOA服务化治理方案的核心框架Dubbo,服务治理和SOA的设计理念开始逐渐在国内软件行业中落地,并被广泛应用。 Dubbo作为阿里巴巴内部的SOA服务化治理方案的核心框架,在2012年时已经每天为2000+个服务提供3,000,000,000+次访问量支持,并被广泛应用于阿里巴巴集团的各成员站点。Dubbo自2011年开源后,已被许多非阿里系公司使用,其中既有当当网、网易考拉等互联网公司,也有中国人寿、青岛海尔等传统企业。本文是作者根据官方文档以及自己平时的使用情况,对 Dubbo 所做的一个总结。 Dubbo 官网:https://dubbo.apache.org/zh-cn/index.html <!-- MarkdownTOC --> 一 重要的概念 1.1 什么是 Dubbo?1.2 什么是 RPC?RPC原理是什么?1.3 为什么要用 Dubbo?1.4 什么是分布式?1.5 为什么要分布式?二 Dubbo 的架构 2.1 Dubbo 的架构图解2.2 Dubbo 工作原理三 Dubbo 的负载均衡策略 3.1 先来解释一下什么是负载均衡3.2 再来看看 Dubbo 提供的负载均衡策略 3.2.1 Random LoadBalance(默认,基于权重的随机负载均衡机制)3.2.2 RoundRobin LoadBalance(不推荐,基于权重的轮询负载均衡机制)3.2.3 LeastActive LoadBalance3.2.4 ConsistentHash LoadBalance3.3 配置方式四 zookeeper宕机与dubbo直连的情况<!-- /MarkdownTOC --> 一 重要的概念1.1 什么是 Dubbo?Apache Dubbo (incubating) |db| 是一款高性能、轻量级的开源Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。简单来说 Dubbo 是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。 Dubbo 目前已经有接近 25k 的 Star ,Dubbo的Github 地址:https://github.com/apache/incubator-dubbo 。 另外,在开源中国举行的2018年度最受欢迎中国开源软件这个活动的评选中,Dubbo 更是凭借其超高人气仅次于 vue.js 和 ECharts 获得第三名的好成绩。 ...

April 27, 2019 · 2 min · jiezi

RPC-RESTGraphQL三种API设计方式的简介和比较

RPCRPC=remote procedure call,执行远程服务器上的一个function,举例:服务端定义了三个函数: 客户端发起请求 RPC在一些大公司中依然被使用。RPC的优点有: 设计简洁,便于理解轻量的payload很高的性能表现缺点有: 前后端代码高耦合代码可读性不好,相关代码不容易被定位会导致有大量被定义的函数,难以管理RESTREST = Representational state transfer,直接翻译就是『表现层状态转移』优点: 前后端高度解耦便于理解,即使没有看文档,也能大概知道接口是用来做什么的;接口的功能有单一性,便于扩展和复用;利用了HTTP原本的特性缺点: 有时payload会变的特别大同一个页面可能要调用很多个API,来获取不同的东西,在网络差的情况下会降低体验举例: GraphQLGraphQL = Graph query language吸取了RPC和REST的一些共同优点;以查询为基本单元,方便获取到想要的数据,举例:接口定义接口调用优点: 低网络速度下表现优异声明式地数据获取根据UI需求获取合适的数据,避免不必要的数据传输缺点: 定义起来相对复杂缓存问题,需要一个更加健全的机制中来确保字段级别的缓存版本持续更新中,还不太成熟综合对比与总结API设计也不会有银弹。设计API时,决定使用哪种形式,得先考虑所设计的API将会被谁使用: 如果是关注于对象和资源的项目,需要对接各种不同的端和使用者,需要便于使用和阅读文档,那么适合使用REST如果是面向行为动作,或者内部的一些微服务,对响应要求高,那么可以考虑RPC如果是需要给UI提供数据,或者需要对弱网络环境下优化而减少请求,那么可以考虑GraphQL 参考来源https://www.youtube.com/watch...

April 26, 2019 · 1 min · jiezi

RPC框架是啥之Apache-CXF一款WebService-RPC框架入门教程

本博客 猫叔的博客,转载请申明出处学习系列RPC框架是啥?RPC框架是啥之Java自带RPC实现,RMI框架入门Apache CXF一款WebService RPC框架入门教程CXF官网:http://cxf.apache.org/Apache CXF是一个开源的WebService RPC框架,是由Celtix和Codehaus XFire合并而成的。它可以说是一个功能齐全的集合。 功能特性: 支持Web Service标准,包括SOAP(1.1、1.2)规范、WSI Basic Profile...等等我也不了解的,这里就不一一举例了。支持JSR相关规范和标准,包括....同上。支持多种传输协议和协议绑定(SOAP、REST/HTTP、XML)、数据绑定(JAXB2.X、Aegis、Apache XML Beans)。还是先从案例入手吧项目源码地址:RPC_Demo,记得是项目里面的comgithubcxf1、使用IDEA构建一个maven项目,我选择了maven-archetype-webapp构建基本框架。当然你可能还需要创建一些目录 2、我想是时候先配置好主要的pom文件了。<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cxf</groupId> <artifactId>comgithubcxf</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>comgithubcxf Maven Webapp</name> <!-- FIXME change it to the project's website --> <url>http://www.example.com</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> <cxf.version>3.1.7</cxf.version> <spring.version>4.0.9.RELEASE</spring.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-frontend-jaxws</artifactId> <version>${cxf.version}</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-transports-http</artifactId> <version>${cxf.version}</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-transports-http-jetty</artifactId> <version>${cxf.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> </dependencies> <build> <finalName>comgithubcxf</finalName> <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> <plugins> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>3.1.0</version> </plugin> <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging --> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> </plugin> <plugin> <artifactId>maven-war-plugin</artifactId> <version>3.2.2</version> </plugin> <plugin> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> </plugin> <plugin> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> </plugins> </pluginManagement> </build></project>3、构建Server端还有其服务实现,接口使用@WebService注解,标明是一个WebService远程服务接口。package com.github.cxf.server;import javax.jws.WebService;/** * Create by UncleCatMySelf in 21:57 2019\4\23 0023 */@WebServicepublic interface CxfService { String say(String someOne);}在实现类上也同样加上,并通过endpointInterface标明对接的接口实现 ...

April 24, 2019 · 3 min · jiezi

XXL-RPC v1.4.0,分布式服务框架

Release Notes1、LRU路由更新不及时问题修复;2、JettyClient Buffer 默认长度调整为5M;3、Netty Http客户端配置优化;4、升级依赖版本,如netty/mina/spring等简介XXL-RPC 是一个分布式服务框架,提供稳定高性能的RPC远程服务调用功能。拥有"高性能、分布式、注册中心、负载均衡、服务治理"等特性。现已开放源代码,开箱即用。 特性1、快速接入:接入步骤非常简洁,两分钟即可上手;2、服务透明:系统完整的封装了底层通信细节,开发时调用远程服务就像调用本地服务,在提供远程调用能力时不损失本地调用的语义简洁性;3、多调用方案:支持 SYNC、ONEWAY、FUTURE、CALLBACK 等方案;4、多通讯方案:支持 TCP 和 HTTP 两种通讯方式进行服务调用;其中 TCP 提供可选方案 NETTY 或 MINA ,HTTP 提供可选方案 NETTY_HTTP 或 Jetty;5、多序列化方案:支持 HESSIAN、HESSIAN1、PROTOSTUFF、KRYO、JACKSON 等方案;6、负载均衡/软负载:提供丰富的负载均衡策略,包括:轮询、随机、LRU、LFU、一致性HASH等;7、注册中心:可选组件,支持服务注册并动态发现;可选择不启用,直接指定服务提供方机器地址通讯;选择启用时,内置可选方案:“XXL-REGISTRY 轻量级注册中心”(推荐)、“ZK注册中心”、“Local注册中心”等;8、服务治理:提供服务治理中心,可在线管理注册的服务信息,如服务锁定、禁用等;9、服务监控:可在线监控服务调用统计信息以及服务健康状况等(计划中);10、容错:服务提供方集群注册时,某个服务节点不可用时将会自动摘除,同时消费方将会移除失效节点将流量分发到其余节点,提高系统容错能力。11、解决1+1问题:传统分布式通讯一般通过nginx或f5做集群服务的流量负载均衡,每次请求在到达目标服务机器之前都需要经过负载均衡机器,即1+1,这将会把流量放大一倍。而XXL-RPC将会从消费方直达服务提供方,每次请求直达目标机器,从而可以避免上述问题;12、高兼容性:得益于优良的兼容性与模块化设计,不限制外部框架;除 spring/springboot 环境之外,理论上支持运行在任何Java代码中,甚至main方法直接启动运行;13、泛化调用:服务调用方不依赖服务方提供的API;文档地址中文文档技术交流社区交流

April 22, 2019 · 1 min · jiezi

RPC框架是啥之Java自带RPC实现,RMI框架入门

本博客 猫叔的博客,转载请申明出处学习系列RPC框架是啥?Java自带RPC实现,RMI框架入门首先RMI(Remote Method Invocation)是Java特有的一种RPC实现,它能够使部署在不同主机上的Java对象进行通信与方法调用,它是一种基于Java的远程方法调用技术。 让我们优先来实现一个RMI的RPC案例吧。 项目源码地址:RPC_Demo,记得是项目里面的comgithubrmi1、首先我们需要为服务端创建一个接口方法,而且这个接口最好继承Remotepackage com.github.rmi.server;import java.rmi.Remote;import java.rmi.RemoteException;/** * Create by UncleCatMySelf in 21:03 2019\4\20 0020 */public interface MyService extends Remote { String say(String someOne)throws RemoteException;}2、对于接口实现类,RMI接口方法定义必须显式声明抛出RemoteException异常,服务端方法实现必须继承UnicastRemoteObject类,该类定义了服务调用与服务提供方对象实现,并建立一对一的连接。package com.github.rmi.server;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;/** * Create by UncleCatMySelf in 21:05 2019\4\20 0020 */public class MyServiceImpl extends UnicastRemoteObject implements MyService { protected MyServiceImpl() throws RemoteException { } public String say(String someOne) throws RemoteException { return someOne + ",Welcome to Study!"; }}3、这里我们还需要一个针对服务端的配置类,因为RMI的通信端口是随机产生的,因此有可能会被防火墙拦截。为了防止被防火墙拦截,需要强制制定RMI的通信端口,一般通过自定义一个RMISocketFactory类来实现。package com.github.rmi.config;import java.io.IOException;import java.net.ServerSocket;import java.net.Socket;import java.rmi.server.RMISocketFactory;/** * Create by UncleCatMySelf in 21:15 2019\4\20 0020 */public class CustomerSocketFactory extends RMISocketFactory { public Socket createSocket(String host, int port) throws IOException { return new Socket(host, port); } public ServerSocket createServerSocket(int port) throws IOException { if (port == 0){ port = 8855; } System.out.println("RMI 通信端口 : " + port); return new ServerSocket(port); }}4、好了,这时你可以写出服务端的启动代码了。package com.github.rmi.server;import com.github.rmi.config.CustomerSocketFactory;import java.rmi.Naming;import java.rmi.registry.LocateRegistry;import java.rmi.server.RMISocketFactory;/** * Create by UncleCatMySelf in 21:07 2019\4\20 0020 */public class ServerMain { public static void main(String[] args) throws Exception { //注册服务 LocateRegistry.createRegistry(8866); //指定通信端口,防止被防火墙拦截 RMISocketFactory.setSocketFactory(new CustomerSocketFactory()); //创建服务 MyService myService = new MyServiceImpl(); Naming.bind("rmi://localhost:8866/myService",myService); System.out.println("RMI 服务端启动正常"); }}5、客户端的启动就相对比较简单,我们仅需要进入服务,并调用对应的远程方法即可。package com.github.rmi.client;import com.github.rmi.server.MyService;import java.rmi.Naming;/** * Create by UncleCatMySelf in 21:10 2019\4\20 0020 */public class ClientMain { public static void main(String[] args) throws Exception { //服务引入 MyService myService = (MyService) Naming.lookup("rmi://localhost:8866/myService"); //调用远程方法 System.out.println("RMI 服务端调用返回:" + myService.say("MySelf")); }}最后可以看看效果。 ...

April 22, 2019 · 1 min · jiezi

RPC框架是啥?

本博客 猫叔的博客,转载请申明出处在我刚刚了解分布式的时候,经常对RPC和分布式有些混淆,甚至一直以为两者对等,所以我们先看看他们有什么区别?RPC实现了服务消费调用方Client与服务提供实现方Server之间的点对点调用流程,即包括了stub、通信、数据的序列化/反序列化。且Client与Server一般采用直连的调用方式。而分布式服务框架,除了包括RPC的特性,还包括多台Server提供服务的负载均衡、策略及实现,服务的注册、发布与引入,以及服务的高可用策略、服务治理等等。那么RPC是什么呢?百度百科是这样表示的:RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。它甚至给出了工作原理,这一点很惊喜。1.调用客户端句柄;执行传送参数2.调用本地系统内核发送网络消息3.消息传送到远程主机4.服务器句柄得到消息并取得参数5.执行远程过程6.执行的过程将结果返回服务器句柄7.服务器句柄返回结果,调用远程系统内核8.消息传回本地主机9.客户句柄由内核接收消息10.客户接收句柄返回的数据我喜欢搜查更多的信息资料,所以我又找到了知乎上的回答。知乎1.7k的点赞,应该还是可以参考的。恰如回答提到的,RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。至于为什么使用RPC?答主也提到,无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同系统间的通讯,甚至不同组织间的通讯。这里再说一下关于Netty,Netty框架不局限于RPC,更多的是作为一种网络协议的实现框架,比如HTTP,由于RPC需要高效的网络通信,就可以选择Netty作为基础。除了网络通信,RPC还需要有高效的序列化框架,以及一种寻址方式,如果是带会话(状态)的RPC调用,还需要有会话的状态保持的功能。好了,让我们再来整理一下,什么是RPC?RPC(远程过程调用)一般用来实现部署在不同机器上的系统之间的方法调用,使得程序能够像访问本地系统资源一样,通过网络传输去访问远端系统资源。一般来说,RPC框架实现的架构原理都是类似的。可以这样说,客户端调用:负责发起RPC调用,为调用方用户提供使用API。服务端响应:主要是服务端业务逻辑实现。序列化/反序列化:负责对RPC调用通过网络传输的内容进行序列化与反序列化,不同的RPC框架有不同的实现机制。一般分为文本(XML、JSON)与二进制(Java原生的、Hessian、protobuf、Thrift、Avro、Kryo、MessagePack),需要注意的是,不同的序列化方式在可读性、码流大小、支持的数据类型及性能等方面都存在较大差异,我们可以根据需要自行选择。Stub:我们看成代理对象,它会屏蔽RPC调用过程中的复杂的网络处理逻辑,使其透明简单,且能够保持与本地调用一样的代码风格。通信传输:即RPC的底层通信传输模块,一般通过Socket在客户端与服务端之间传递请求与应答消息。公众号:Java猫说学习交流群:728698035现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。

April 20, 2019 · 1 min · jiezi

RPC架构之SOA服务化架构学习(一)

传统垂直应用架构背景:传统垂直MVC项目简单分为展示层.业务逻辑层.数据访问层缺点:如1.复杂应用的开发维护成本变高,部署效率逐渐降低 2.团队协作效率差,部分公共功能重复开发,代码重复率居高不下 3.系统可靠性变差。随着业务的发展,访问量逐渐攀升,网络流量、负载均衡、数据库连接等都面临着巨大的压力.走向:当垂直引用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。同时将公共能力API抽取出来,作为独立的公共服务供其他调用者消费,以实现服务的共享和重用,降低开发和运维成本。应用拆分之后会按照模块独立部署,接口调用由本地API演进成跨进程的远程方法调用,此时RPC框架应运而生具体可参考《分布式服务框架原理与实践》集群管理,负载均衡负载均衡有F5硬件负载均衡和软负载均衡.这里我简单讲下软负载均衡,nginx的反向代理服务很好的实现了集群管理,负载均衡.反向代理就是根据客户端的请求,从其关系的一组或多组后端服务器上获取资源,然后再将这些资源返回给客户端,客户端只会得知反向代理的IP地址,而不知道在代理服务器后面的服务器簇的存在.session失效: nginx默认算法是轮询服务器,这有一个问题session会失效解决办法1:upstream里设置ip_hash即采用哈希算法则可以解决这个问题,某个用户请求了A服务器,接下来该用户只会请求A服务器,除非A挂了,则会请求转入别的服务器,这时候还是会存在session失效的问题.解决办法2:session共享,比如2个tomcat来说,session共享需要发生网络通信也就是会建立连接,如果集群有多个,多个请求同时到每个不同tomcat,比如100个请求到100个不同tomcat,则会把100个的session共享到另外99个tomcat,则此时连接就100了,集群越多性能反而大大降低了.因此nginx自身session共享不建议,轮询算法中可通过别的方法,如redis共享session.初步学习分布式,理解较为浅,后续还会改动~~~

April 10, 2019 · 1 min · jiezi

远程调用

远程调用包括远程过程调用(RPC)和远程方法调用(RMI)1.请求-应答协议请求-应答协议描述了一个基于消息传递的范型,支持消息双向传输。涉及三个通信原语(1)doOperation:向指定的服务器发送请求消息。(2)getRequest:远程服务器获取客户端的请求。(3)sendReply:发送应答消息给客户端。有三种方式保证doOperation的可靠传输:(1)重发请求消息:一直重发消息直到保证收到请求结果或者认定服务器故障。(2)过滤重复请求:在服务器过滤掉重复的请求。(3)保存重传历史:在服务器保存消息结果,相同的请求直接返回结果。RPC调用语义调用语义重发请求消息过滤请求消息重新执行或者重传消息应答或许(0次或一次)否不适用不适用最多一次是是重传消息应答最少一次是否重新执行2.远程过程调用(RPC)RPC(Remote Procedure Call)允许客户端程序可以调用不同于客户端的计算机的服务器程序的进程。RPC调用过程图片来源:https://www.cs.rutgers.edu/~p…解释下调用的过程:(1)客户端(client functions)调用了一个客户端代理(client stub)。(2)客户端将传输的对象参数序列化为二进制,调用客户端socket进行通信。(3)客户端socket将参数通过网络传递给服务端socket。(4)服务端socket将接收到的参数交给服务端代理(server stub)。(5)服务端代理将接收到的参数反序列化为对象,然后调用服务端(server functions)真正的方法。(6)服务端将方法调用的结果返回给服务端代理。(7)服务端代理将调用结果进行序列化,传递给服务端socket。(8)服务端socket接收到二进制的调用结果之后,通过网络传输给客户端socket。(9)客户端socket将调用结果传递给客户端代理。(10)客户端代理将调用结果反序列化传递给客户端。http请求是基于HTTP协议,适用于不用企业间的方法调用,http请求是低效的,因为每次都需要建立一个连接。RPC适用于企业内部系统之间的调用,RPC可以基于TCP协议,也可以基于HTTP2协议。Dubbo等框架实现了RPC。可以使用XML、JSON、Fastjson、Thrift、Avro、Protobuf或是其它进行消息的序列化。多个消息在一个TCP连接上传输时,TCP会将发送的大消息用多个数据包发送,将发送的小消息合并为一个数据包发送。当多个消息用一个数据包发送时,可以使用文本分割法或长度前缀法得到一个完整的消息。文本分割法:每个完整的消息之后,添加一个分隔符,根据之前的分隔符判断之前的文本是不是同一个消息。长度前缀法:在消息开头添加传输的消息长度。参考资料:https://blog.csdn.net/baiye_x…http://www.360doc.com/content...https://github.com/hzy38324/s… https://www.zhihu.com/search?…《分布式系统概念与设计》https://blog.51cto.com/zero01…http://www.cnblogs.com/zhuxia...3.远程方法调用(RMI)RMI允许一个进程对象的方法可以调用另一个进程对象的方法。不管是否需在同一计算机内,只要是不同进程对象的之间的方法调用就是远程方法调用,而同一进程的方法调用就是本地方法调用。(1)通信模块:两个相互协作的通信模块执行请求-应答协议,在客户端和服务器之间传递请求和应答消息。(2)远程引用模块:负责在本地对象引用和远程对象引用之间进行翻译。每个进程的远程引用模块都会维护一个远程对象表,该表记录了该进程的本地对象引用和远程对象引用的对应关系。(3)伺服器:提供远程对象主体的类的实例。很久以前自己在csdn上写过的一篇关于RMI的文章:https://blog.csdn.net/u012734…其他参考资料:https://blog.csdn.net/mawanli…https://www.cnblogs.com/nashi…《分布式系统概念与设计》4.RPC和RMI的联系和区别(1)RPC和RMI都支持接口编程。(2)RPC和RMI都是基于请求-应答协议,并支持最多一次、最少一次等调用语义。(3)RPC和RMI的输入输出参数都可以是值。而RMI支持输入输出参数是对象引用。

April 6, 2019 · 1 min · jiezi

golang使用grpc

golang使用grpc目前正在学习后端有关概念, 了解到了grpc. 本文将简述一个简单demo的搭建.rpc理解我的网络通信概念还停留在比较基础的地方, 直觉来说就是发送一个get/post请求, 设定一定的参数格式, 这样对方再解析你的数据, 基于此完成通讯.而rpc则不使用这种显式的通讯方式, 通过proto与生成grpc文件, 里面提供方法, 供client与server通过方法来进行通讯. 简单来说, 就是如果你以前要告诉server你好, 需要发送一个完整网络请求, 但是现在你只需要调用一个hello方法就可以了环境依赖以及安装golanggrpcgo get -u google.golang.org/grpcprotocgo get -u github.com/golang/protobuf/protoc-gen-go环境变量配置export PATH=$PATH:$GOPATH/bin项目目录结构grpc_test/|– client| -- client.go|-- proto| |-- greeter.pb.go| – greeter.proto-- server – server.goDEMO1. 编写proto文件 syntax = “proto3”; package proto; service Hello { rpc Hello (Request) returns (Response) {} } message Request { string Name = 1; } message Response { string Msg = 1; }非常简单, 提供了request, response对象, 并且提供了hello方法.2. 生成grpc文件执行protoc -I ./ ./greeter.proto –go_out=plugins=grpc:../proto/ 前面的./与./greeter.proto是上面的proto文件的文件夹以及文件, 后面的../proto是输出目录, 运行成功的时候, 会生成greeter.pb.go文件. 有兴趣可以详细去读里面的代码.3. 编写服务端脚本package mainimport ( “fmt” “net” “context” grpc “google.golang.org/grpc” proto “grpc_test/proto” // 引用刚才生成grpc代码)const( Address = “127.0.0.1:8801” // 监听的端口)type Greeter struct {}// 实现proto中定义的hello方法func (greeter *Greeter) Hello(ctx context.Context, request *proto.Request) (*proto.Response, error){ fmt.Println(“get client info, name is: “, request.Name) response_msg := “Hello " + request.Name return &proto.Response{Msg:response_msg}, nil }func main(){ service := Greeter{} conn, err := net.Listen(“tcp”, Address) // tcp监听端口 if err != nil{ fmt.Println(err) return } fmt.Println(“tcp listening…”) defer conn.Close() server := grpc.NewServer() proto.RegisterHelloServer(server, &service) // 将tcp服务于grpc进行绑定 fmt.Println(“server serve…..”) if err := server.Serve(conn); err != nil{ fmt.Println(err) return } }4. 编写客户端脚本package mainimport ( “context” “fmt” grpc “google.golang.org/grpc” proto “grpc_test/proto” //引用grpc代码)const( Address = “127.0.0.1:8801” // 服务监听的端口)func main() { conn, err := grpc.Dial(Address, grpc.WithInsecure()) if err != nil{ fmt.Println(err) } defer conn.Close() client := proto.NewHelloClient(conn) // 自动生成方法, 因为proto文件中service的名字是hello name := “jhonsmith” result, err := client.Hello(context.Background(), &proto.Request{Name:name}) // 调用grpc方法, 对服务端进行通讯 if err != nil{ fmt.Println(err) } fmt.Println(“I see, u see. I say “, name, “u say “, result.Msg)}終わり。 ...

April 2, 2019 · 2 min · jiezi

Dubbo实现原理分析-SPI&自适应扩展实现

Duboo基本概念Dubbo整体架构:evernotecid://D5D92FBE-C671-4B13-BAC8-4D01D3D20F5B/appyinxiangcom/8739769/ENNote/p68?hash=f07431ff8daafff6906882197d395a2aDubbo SPISPI 全称为 Service Provider Interface,一种服务提供发现机制。可以实现通过配置手段加载相应的实现类。JDK里提供了一种SPI实现,Dubbo并没有使用jdk的spi,而是自己实现了一个Dubbo SPI 服务发现机制。首先看一下JDK SPI实现方式定义一个接口类Travelpublic interface Travel { public void travel();}Travel有两个实现类public class CarTravel implements Travel { @Override public void travel() { System.out.println(“travel by car”); }}public class PlaneTravel implements Travel{ @Override public void travel() { System.out.println(“travel by plane”); }}接下来需要在 META-INF/services 文件夹下建立接口名称对应的文件,META-INF/services/com.demo.spi.jdk.Travel文件内容为接口实现类的名称com.demo.spi.jdk.CarTravelcom.demo.spi.jdk.PlaneTravel测试使用public class JdkSpiTest { public static void main(String[] args) { ServiceLoader<Travel> sServiceLoader = ServiceLoader.load(Travel.class); sServiceLoader.forEach(Travel::travel); }}测试结果travel by cartravel by planeJAVA SPI vs Dubbo SPI首先加载路径不同,java spi路径是 META-INF/services ,dubbo 是META-INF/services/,META-INF/services/internal,META-INF/dubbo/加载方式不同,java spi是全量加载,dubbo是按需加载接口实现类dubbo spi除了加载实现类外还增加了 IOC 和 AOP 特性下面我们看一下dubbo spi的实现首先接口需要有@SPI注解@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})public @interface SPI { /** * default extension name / String value() default “”;}@SPIpublic interface Travel { public void travel();}配置文件采用key=value的形式配置car=com.demo.spi.jdk.CarTravelplane=com.demo.spi.jdk.PlaneTravel测试duubo spi加载public class DubboSPITest { public static void main(String[] args) { ExtensionLoader<Travel> extensionLoader = ExtensionLoader.getExtensionLoader(Travel.class); Travel travel = extensionLoader.getExtension(“car”); travel.travel(); }}输出结果travel by carDubbo SPI实现1.扩展类的加载dubbo spi 是通过 ExtensionLoader 类来实现的,通过ExtensionLoader获取接口对应类型的扩展加载器ExtensionLoader.getExtensionLoader,第一次获取会新创建然后会缓存到 ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS 中,之后就在缓存中取就可以了 //获取扩展加载器ExtensionLoader,type必须是@SPI注解接口 //ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS 缓存 public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) { if (type == null) { throw new IllegalArgumentException(“Extension type == null”); } if (!type.isInterface()) { throw new IllegalArgumentException(“Extension type (” + type + “) is not an interface!”); } if (!withExtensionAnnotation(type)) { throw new IllegalArgumentException(“Extension type (” + type + “) is not an extension, because it is NOT annotated with @” + SPI.class.getSimpleName() + “!”); } ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); if (loader == null) { EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type)); loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); } return loader; }构造方法private ExtensionLoader(Class<?> type) { this.type = type; objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); }获取到接口对应的ExtensionLoader后,通过getExtension(String name) 方法获取name对应的扩展对象public T getExtension(String name) { if (StringUtils.isEmpty(name)) { throw new IllegalArgumentException(“Extension name == null”); } if (“true”.equals(name)) { return getDefaultExtension(); } //根据名称 获取holder对象,如果不存在则创建一个holder //holder里缓存了name对应的实现类 Holder<Object> holder = getOrCreateHolder(name); Object instance = holder.get(); //双重检查 if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { //如果为空,创建 instance = createExtension(name); holder.set(instance); } } } return (T) instance; }扩展对象的创建private T createExtension(String name) { //1.getExtensionClasses() 获取所有扩展类 //根据name获取扩展类的Class Class<?> clazz = getExtensionClasses().get(name); if (clazz == null) { throw findException(name); } try { T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { //2.通过反射newInstance()创建扩展类实例,放到EXTENSION_INSTANCES 缓存中 EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } //3.依赖注入 IOC injectExtension(instance); Set<Class<?>> wrapperClasses = cachedWrapperClasses; //4.循环创建 Wrapper 实例 AOP if (CollectionUtils.isNotEmpty(wrapperClasses)) { for (Class<?> wrapperClass : wrapperClasses) { instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; } catch (Throwable t) { throw new IllegalStateException(“Extension instance (name: " + name + “, class: " + type + “) couldn’t be instantiated: " + t.getMessage(), t); } }获取所有Extension getExtensionClassesprivate Map<String, Class<?>> getExtensionClasses() { Map<String, Class<?>> classes = cachedClasses.get(); if (classes == null) { synchronized (cachedClasses) { classes = cachedClasses.get(); if (classes == null) { //通过loadExtensionClasses 获取所有扩展类,并放到 //cachedClasses 缓存中 classes = loadExtensionClasses(); cachedClasses.set(classes); } } } return classes; }loadExtensionClassesprivate Map<String, Class<?>> loadExtensionClasses() { //缓存默认的扩展名 cacheDefaultExtensionName(); Map<String, Class<?>> extensionClasses = new HashMap<>(); //去dubbo spi所在的路径下META-INF/services/,META-INF/services/internal,META-INF/dubbo/ // 加载扩展类,放到extensionClasses 中 loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName()); loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace(“org.apache”, “com.alibaba”)); loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName()); loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace(“org.apache”, “com.alibaba”)); loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName()); loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace(“org.apache”, “com.alibaba”)); return extensionClasses; }loadDirectory->loadResource->loadClass 最终的类加载实现private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException { if (!type.isAssignableFrom(clazz)) { throw new IllegalStateException(“Error occurred when loading extension class (interface: " + type + “, class line: " + clazz.getName() + “), class " + clazz.getName() + " is not subtype of interface.”); } // 如果类有@Adaptive 注解,缓存到cachedAdaptiveClass if (clazz.isAnnotationPresent(Adaptive.class)) { cacheAdaptiveClass(clazz); } else if (isWrapperClass(clazz)) { //如果类的构造方法有加载类的类型,缓存到Set<Class<?>> cachedWrapperClasses; 实现aop的类 cacheWrapperClass(clazz); } else { //如果是普通类 clazz.getConstructor(); //获取扩展类name,为空就取默认name if (StringUtils.isEmpty(name)) { name = findAnnotationName(clazz); if (name.length() == 0) { throw new IllegalStateException(“No such extension name for the class " + clazz.getName() + " in the config " + resourceURL); } } String[] names = NAME_SEPARATOR.split(name); if (ArrayUtils.isNotEmpty(names)) { cacheActivateClass(clazz, names[0]); for (String n : names) { //缓存到cachedNames cacheName(clazz, n); //存储到extensionClasses saveInExtensionClass(extensionClasses, clazz, name); } } } }2. dubbo ioc 注入实现dubbo ioc aop 利用setter注入方式实现private T injectExtension(T instance) { try { if (objectFactory != null) { for (Method method : instance.getClass().getMethods()) { //判断是否是setter方法 if (isSetter(method)) { /* * Check {@link DisableInject} to see if we need auto injection for this property */ if (method.getAnnotation(DisableInject.class) != null) { continue; } Class<?> pt = method.getParameterTypes()[0]; if (ReflectUtils.isPrimitives(pt)) { continue; } try { String property = getSetterProperty(method); //从objectFactory 获取依赖对象 Object object = objectFactory.getExtension(pt, property); if (object != null) { //setter注入 method.invoke(instance, object); } } catch (Exception e) { logger.error(“Failed to inject via method " + method.getName() + " of interface " + type.getName() + “: " + e.getMessage(), e); } } } } } catch (Exception e) { logger.error(e.getMessage(), e); } return instance; }3. dubbo aop 实现//遍历所有Wrapper类 创建 Wrapper 实例 //setter注入injectExtension if (CollectionUtils.isNotEmpty(wrapperClasses)) { for (Class<?> wrapperClass : wrapperClasses) { instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } }Dubbo 扩展点自适应机制1. 什么自适应扩展自适应扩展的标记注解@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE, ElementType.METHOD})public @interface Adaptive { String[] value() default {};}Adaptive 标记在类上,表示扩展点的加载是人工编码完成,目前只有两个类被@Adaptive, AdaptiveCompiler、AdaptiveExtensionFactoryAdaptive 标记在方法上,表示扩展点的加载是代理完成AdaptiveCompiler 示例@Adaptivepublic class AdaptiveCompiler implements Compiler { private static volatile String DEFAULT_COMPILER; public static void setDefaultCompiler(String compiler) { DEFAULT_COMPILER = compiler; } @Override public Class<?> compile(String code, ClassLoader classLoader) { Compiler compiler; ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class); String name = DEFAULT_COMPILER; // copy reference if (name != null && name.length() > 0) { compiler = loader.getExtension(name); } else { compiler = loader.getDefaultExtension(); } return compiler.compile(code, classLoader); }}2. 加载自适应扩展dubbo通过ExtensionLoader.getAdaptiveExtension() 获取自适应扩展实例public T getAdaptiveExtension() { Object instance = cachedAdaptiveInstance.get(); if (instance == null) { if (createAdaptiveInstanceError == null) { synchronized (cachedAdaptiveInstance) { instance = cachedAdaptiveInstance.get(); if (instance == null) { try { //创建自适应扩展,并缓存到cachedAdaptiveInstance instance = createAdaptiveExtension(); cachedAdaptiveInstance.set(instance); } catch (Throwable t) { createAdaptiveInstanceError = t; throw new IllegalStateException(“Failed to create adaptive instance: " + t.toString(), t); } } } } else { throw new IllegalStateException(“Failed to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError); } } return (T) instance; }createAdaptiveExtension()方法代码private T createAdaptiveExtension() { try { //反射创建AdaptiveExtensionClass,并注入依赖 return injectExtension((T) getAdaptiveExtensionClass().newInstance()); } catch (Exception e) { throw new IllegalStateException(“Can’t create adaptive extension " + type + “, cause: " + e.getMessage(), e); } }获取Class 类 getAdaptiveExtensionClass()private Class<?> getAdaptiveExtensionClass() { getExtensionClasses(); if (cachedAdaptiveClass != null) { return cachedAdaptiveClass; } //创建自适应扩展类 return cachedAdaptiveClass = createAdaptiveExtensionClass(); }createAdaptiveExtensionClass() 生成自适应扩展类private Class<?> createAdaptiveExtensionClass() { //1.生成自适应扩展类代码 String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate(); ClassLoader classLoader = findClassLoader(); //2.获取编译器 org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); //3.编译生成class return compiler.compile(code, classLoader); }Protocol 自适应扩展类生成代码 Protocol$Adaptivepackage org.apache.dubbo.rpc;import org.apache.dubbo.common.extension.ExtensionLoader;public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol { public void destroy() { throw new UnsupportedOperationException(“The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!”); } public int getDefaultPort() { throw new UnsupportedOperationException(“The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!”); } public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException { if (arg1 == null) throw new IllegalArgumentException(“url == null”); org.apache.dubbo.common.URL url = arg1; String extName = (url.getProtocol() == null ? “dubbo” : url.getProtocol()); if (extName == null) throw new IllegalStateException(“Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (” + url.toString() + “) use keys([protocol])”); org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); return extension.refer(arg0, arg1); } public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException { if (arg0 == null) throw new IllegalArgumentException(“org.apache.dubbo.rpc.Invoker argument == null”); if (arg0.getUrl() == null) throw new IllegalArgumentException(“org.apache.dubbo.rpc.Invoker argument getUrl() == null”); org.apache.dubbo.common.URL url = arg0.getUrl(); String extName = (url.getProtocol() == null ? “dubbo” : url.getProtocol()); if (extName == null) throw new IllegalStateException(“Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (” + url.toString() + “) use keys([protocol])”); org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); return extension.export(arg0); }} ...

March 15, 2019 · 6 min · jiezi

Go 实现简易 RPC 框架

本文旨在讲述 RPC 框架设计中的几个核心问题及其解决方法,并基于 Golang 反射技术,构建了一个简易的 RPC 框架。项目地址:Tiny-RPCRPCRPC(Remote Procedure Call),即远程过程调用,可以理解成,服务 A 想调用不在同一内存空间的服务 B 的函数,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。服务端RPC 服务端需要解决 2 个问题:由于客户端传送的是 RPC 函数名,服务端如何维护 函数名 与 函数实体 之间的映射服务端如何根据 函数名 实现对应的 函数实体 的调用核心流程维护函数名到函数的映射在接收到来自客户端的函数名、参数列表后,解析参数列表为反射值,并执行对应函数对函数执行结果进行编码,并返回给客户端方法注册服务端需要维护 RPC 函数名到 RPC 函数实体的映射,我们可以使用 map 数据结构来维护映射关系。type Server struct { addr string funcs map[string]reflect.Value}// Register a method via namefunc (s *Server) Register(name string, f interface{}) { if _, ok := s.funcs[name]; ok { return } s.funcs[name] = reflect.ValueOf(f)}执行调用一般来说,客户端在调用 RPC 时,会将 函数名 和 参数列表 作为请求数据,发送给服务端。由于我们使用了 map[string]reflect.Value 来维护函数名与函数实体之间的映射,则我们可以通过 Value.Call() 来调用与函数名相对应的函数。代码地址:https://play.golang.org/p/jaP…package mainimport ( “fmt” “reflect”)func main() { // Register methods funcs := make(map[string]reflect.Value) funcs[“add”] = reflect.ValueOf(add) // When receives client’s request req := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf(2)} vals := funcs[“add”].Call(req) var rsp []interface{} for _, val := range vals { rsp = append(rsp, val.Interface()) } fmt.Println(rsp)}func add(a, b int) (int, error) { return a + b, nil}具体实现由于篇幅的限制,此处没有贴出服务端实现的具体代码,细节请查看项目地址。客户端RPC 客户端需要解决 1 个问题:由于函数的具体实现在服务端,客户端只有函数的原型,客户端如何通过 函数原型 调用其 函数实体核心流程对调用者传入的函数参数进行编码,并传送给服务端对服务端响应数据进行解码,并返回给调用者生成调用我们可以通过 reflect.MakeFunc 为指定的函数原型绑定一个函数实体。代码地址: https://play.golang.org/p/Aae...package mainimport ( “fmt” “reflect”)func main() { add := func(args []reflect.Value) []reflect.Value { result := args[0].Interface().(int) + args[1].Interface().(int) return []reflect.Value{reflect.ValueOf(result)} } var addptr func(int, int) int container := reflect.ValueOf(&addptr).Elem() v := reflect.MakeFunc(container.Type(), add) container.Set(v) fmt.Println(addptr(1, 2))}具体实现由于篇幅的限制,此处没有贴出客户端实现的具体代码,细节请查看项目地址。数据传输格式我们需要定义服务端与客户端交互的数据格式。type Data struct { Name string // service name Args []interface{} // request’s or response’s body except error Err string // remote server error}与交互数据相对应的编码与解码函数。func encode(data Data) ([]byte, error) { var buf bytes.Buffer encoder := gob.NewEncoder(&buf) if err := encoder.Encode(data); err != nil { return nil, err } return buf.Bytes(), nil}func decode(b []byte) (Data, error) { buf := bytes.NewBuffer(b) decoder := gob.NewDecoder(buf) var data Data if err := decoder.Decode(&data); err != nil { return Data{}, err } return data, nil}同时,我们需要定义简单的 TLV 协议(固定长度消息头 + 变长消息体),规范数据的传输。// Transport structtype Transport struct { conn net.Conn}// NewTransport creates a transportfunc NewTransport(conn net.Conn) *Transport { return &Transport{conn}}// Send datafunc (t *Transport) Send(req Data) error { b, err := encode(req) // Encode req into bytes if err != nil { return err } buf := make([]byte, 4+len(b)) binary.BigEndian.PutUint32(buf[:4], uint32(len(b))) // Set Header field copy(buf[4:], b) // Set Data field _, err = t.conn.Write(buf) return err}// Receive datafunc (t *Transport) Receive() (Data, error) { header := make([]byte, 4) _, err := io.ReadFull(t.conn, header) if err != nil { return Data{}, err } dataLen := binary.BigEndian.Uint32(header) // Read Header filed data := make([]byte, dataLen) // Read Data Field _, err = io.ReadFull(t.conn, data) if err != nil { return Data{}, err } rsp, err := decode(data) // Decode rsp from bytes return rsp, err}相关资料项目地址:Tiny-RPCgo rpc 源码分析 ...

March 11, 2019 · 2 min · jiezi

浅谈RPC

从我们学习编程开始,就对『LPC』(local Procedure Call)十分熟悉,而PRC就是类似LPC的一种调用机制。在服务化、微服务化逐渐成为大中型分布式系统架构的主流方式的过程中,RPC作为基本通用服务成为系统标配的一部分。RPC 的全称是 Remote Procedure Call 是一种进程间通信方式。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的,本质上编写的调用代码基本相同。RPC的概念源于Bruce Jay Nelson的论文Implementing Remote Procedure Calls,有兴趣的可以阅读一下。RPC有许多成熟、开源的实现方案,这些方案均由大厂负责设计、开发,各有优点。grpc(google)https://github.com/grpc/grpcthrift(facebook):独特的序列化格式和IDL,支持很多编程语言。thrift的代码看似分层很清楚,client、server选择很多,但没有一个足够通用,每个server实现都只能解决很小一块场景,每个client都线程不安全。实际使用中非常麻烦。thrift的代码质量也比较差,接口和生成的代码都比较乱。https://github.com/apache/thriftdubbo(alibaba)https://github.com/alibaba/dubbosofa-pbrpc(baidu):百度PS基于boost::asio和protobuf实现的RPC框架,这个库代码工整,接口清晰,支持同步和异步。sofa-pbrpc存在产品线自研框架的鲜明特点:不支持内部其他协议,对名字服务、负载均衡、服务认证、连接方式等多样化的需求的抽象不够一般化。https://github.com/baidu/sofa…baidu-rpc(baidu)提供稳定的RPC框架;适用各类业务场景,提供优秀的延时,吞吐,并发度,具备优秀的多核扩展性;接口易懂,用户体验佳。有完备的调试和运维接口(HTTP)。https://github.com/brpc/brpc。RPC原理和流程1)服务消费方(client)调用以本地调用方式调用服务;2)client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;3)client stub找到服务地址,并将消息发送到服务端;4)server stub收到消息后进行解码;5)server stub根据解码结果调用本地的服务;6)本地服务执行并将结果返回给server stub;7)server stub将返回结果打包成消息并发送至消费方;8)client stub接收到消息,并进行解码;9)服务消费方得到最终结果。RPC中需求关注的几个核心问题1)序列化接口定义方面需要关注客户端和服务端各需要哪些数据来确保调用的正常返回、调用次序和调用结果正确表示。调用方:接口名称、方法名、参数类型及参数值、超时时间、requestID,标识唯一请求id。返回方:返回值、状态码、requestID传输协议方面现在最通用的方式是使用protobuf,能够有效对数据进行压缩并提供高效的转换效率。另外常用的协议还包括idl,百度内部使用nshead+mcpack等,facebook自定义了格式化方法。2)通信模型:主流RPC框架都提供BIO+NIO,提供同步、异步通信机制3)使用zookeeper、bns服务提供服务发现。4)提供接口能够让用户自定义负载均衡算法。5)提供策略框架提供业务策略处理能力。参考资料:1、http://birrell.org/andrew/pap…2、http://blog.jobbole.com/92290/3、http://blog.csdn.net/mindfloa…、http://blog.csdn.net/mindfloating/article/details/39474123

February 27, 2019 · 1 min · jiezi

XXL-RPC v1.3.2,分布式服务框架

Release Notes1、泛化调用:服务调用方不依赖服务方提供的API;2、新增通讯方案 “NETTY_HTTP”;3、新增序列化方案 “KRYO”;4、通讯效率优化:TCP连接池取消,改为单一长连接,移除commons-pool2依赖;5、RPC请求路由时空地址处理优化;6、通讯连接池address参数优化,出IP:PORT格式外兼容支持常规URL格式地址;7、线程名称优化,便于适配监控快速进行线程定位;简介XXL-RPC 是一个分布式服务框架,提供稳定高性能的RPC远程服务调用功能。拥有"高性能、分布式、注册中心、负载均衡、服务治理"等特性。现已开放源代码,开箱即用。特性1、快速接入:接入步骤非常简洁,两分钟即可上手;2、服务透明:系统完整的封装了底层通信细节,开发时调用远程服务就像调用本地服务,在提供远程调用能力时不损失本地调用的语义简洁性;3、多调用方案:支持 SYNC、ONEWAY、FUTURE、CALLBACK 等方案;4、多通讯方案:支持 TCP 和 HTTP 两种通讯方式进行服务调用;其中 TCP 提供可选方案 NETTY 或 MINA ,HTTP 提供可选方案 NETTY_HTTP 或 Jetty;5、多序列化方案:支持 HESSIAN、HESSIAN1、PROTOSTUFF、KRYO、JACKSON 等方案;6、负载均衡/软负载:提供丰富的负载均衡策略,包括:轮询、随机、LRU、LFU、一致性HASH等;7、注册中心:可选组件,支持服务注册并动态发现;可选择不启用,直接指定服务提供方机器地址通讯;选择启用时,内置可选方案:“XXL-REGISTRY 轻量级注册中心”(推荐)、“ZK注册中心”、“Local注册中心”等;8、服务治理:提供服务治理中心,可在线管理注册的服务信息,如服务锁定、禁用等;9、服务监控:可在线监控服务调用统计信息以及服务健康状况等(计划中);10、容错:服务提供方集群注册时,某个服务节点不可用时将会自动摘除,同时消费方将会移除失效节点将流量分发到其余节点,提高系统容错能力。11、解决1+1问题:传统分布式通讯一般通过nginx或f5做集群服务的流量负载均衡,每次请求在到达目标服务机器之前都需要经过负载均衡机器,即1+1,这将会把流量放大一倍。而XXL-RPC将会从消费方直达服务提供方,每次请求直达目标机器,从而可以避免上述问题;12、高兼容性:得益于优良的兼容性与模块化设计,不限制外部框架;除 spring/springboot 环境之外,理论上支持运行在任何Java代码中,甚至main方法直接启动运行;13、泛化调用:服务调用方不依赖服务方提供的API;文档地址中文文档技术交流社区交流

February 22, 2019 · 1 min · jiezi

老代码多=过度耦合=if else?阿里巴巴工程师这样捋直老代码

简介在业务开发的过程中,往往存在平台代码和业务代码耦合严重难以分离、业务和业务之间代码交织缺少拆解的现象。平台和业务代码交织导致不易修改,不同业务的代码交织增加了不同负责团队之间的协同成本。因此不论从代码质量,还是从团队协作的角度来看都严重地影响了开发团队之间的协同效率和开发效率,最终影响到了用户体验和业务发展。在闲鱼,商品发布和编辑功能也是如此。本文将以闲鱼商品发布和编辑功能的改造为例,向大家展示闲鱼是如何解决此类问题,从而更有效地协同更多团队更快更稳定地支撑各种业务的。发布编辑功能的升级改造为了实现上述目标,针对发布和编辑功能,进行了两轮升级。第一轮的目标在于“平台和业务分离、业务和业务隔离”;而第二轮将更进一步,目标在于“系统之间的解耦合,提升团队协同效率”。1.平台和业务分离,业务和业务隔离第一轮改造中,闲鱼将原先的商品发布和编辑功能从老应用中抽取到了新应用item。为了实现“平台和业务分离、业务和业务隔离”的目标,闲鱼自研了一套技术框架SWAK,具体请参考文章《业务代码解构利器–SWAK》,该文介绍了其设计思想和实现原理。接入SWAK框架后,平台逻辑和业务逻辑得到了分离,各个业务(如租房业务、免费送业务)之间的逻辑也不再耦合,而是变成package隔离(当然也是可以做成jar包隔离)。看一看改造后的应用情况示意图:我们根据发布和编辑的主干流程,抽象了17个SWAK扩展点。发布和编辑的主干流程主要就是对这些扩展点的编排。主干流程的编写并不需要考虑业务上怎么实现这些扩展点的。我们根据不同的业务(在SWAK里面更准确的表述是tag)对这些扩展点进行了各自的实现。根据这样的开发方式,我们可以把开发同学分成如下的两种角色:各业务开发人员。各业务开发人员主要是负责各个业务相关的代码。在item应用里面,业务同学需要维护其业务中和发布编辑相关的个性化业务逻辑。主干开发人员。主干的人员只需要维护主干的代码,尤其是扩展点的抽象。随着不同业务的不断接入,原先的扩展点也需要随之调整。如在之前的《业务代码解构利器–SWAK》一文中指出的一样,经过SWAK改造后,获得了如下的几个优点:代码逻辑清晰,可变和不可变一目了然。代码复用度变高。可变逻辑按照标签进行隔离,单个标签的实现不会影响到其他标签的实现,降低开发和测试成本。无论是按照“类型”分还是按照类目分,对应的开发和测试同学只需要关注对应的逻辑即可。新接手的开发人员能够快速理解,轻松上手。2.系统之间的解耦合,提升团队协同效率以租房为例——租房业务的同学需要在item应用中维护一套租房发布编辑相关的逻辑(如校验地小区数据、地铁数据真实性等);租房业务的同学还需要在详情应用的逻辑中维护一套和租房详情相关的逻辑(如展示地图,展示内部设施标签);租房业务的同学还需要在交易应用的逻辑中维护一套和租房交易相关的逻辑(如预约看房)等等。租房的同学不仅仅需要着手于自己的代码逻辑,还需要修改发布和编辑应用item、还需要修改详情应用,还需要修改交易应用……这种体验是非常糟糕的,有极大的可能性接手一个简单业务就需要修改和发布四五个应用。另一方面,从主干开发人员的角度来说,其应用不仅仅由自己或自己的小团队来维护,还有很多业务开发人员也在修改和发布此应用,且频率会远远超过主干开发任务的发布和部署频次(否则就是主干扩展点逻辑抽取得不好了)。这不利于整个应用的稳定性。A业务服务挂了,应该只影响A业务,而不应该影响主干。依此逻辑,最好能做到JVM隔离。本质上来说,第一轮改造完成了业务之间的解耦合,而第二轮则是系统之间的解耦合。康威定律告诉我们:Any organization that designs a system (defined more broadly here than just information systems) will inevitably produce a design whose structure is a copy of the organization’s communication structure.简而言之就是人员组织结构和系统结构之间的一致性。而完成系统之间的解耦合又恰恰是符合康威定律的。这一轮的改造,我们称之为“业务服务化”。业务服务化改造方案首先,我们把租房业务给单独抽取出来。原先的帖子和拍卖业务暂时没有独立的团队来予以维护(但也基本上没有什么新需求)因此暂时仍然放在主干应用中,时机合适将会和租房应用一样迁移出去。其次,租房业务通过远程服务的方式给主干应用提供服务。接口即是主干业务的提供的扩展点。由于现在是优先使用远程服务来连接主干应用和垂直应用,考虑到性能问题和安全问题,我们在扩展点的定义上也做了一些特殊的改动,后文会有针对性的详述。最后,SWAK框架做了一些改变以注册和调用远程服务。相对于本地服务,远程服务一般都是有超时、连接异常等问题。然而不同接口对于这些异常情况其处理策略也是截然不同的,后文“SWAK框架的针对性改进”会详述这些改动。通过这种方式,我们将主干应用和各业务应用彻底分离了。仍然以租房业务为例,租房团队负责开发和维护租房业务的独立应用rent。租房个性化的发布和编辑需求只需要开发和部署rent应用,而不必修改主干应用。主干应用只由主干团队的同学负责维护,不会被其他业务团队的同学所开发和部署,稳定性更加能得以保障。各业务系统独立开发、独立部署。这些都大幅地减少了不必要的沟通成本、提升协同效率。主干应用和业务应用是通过薄薄的一层接口所联系起来的,这层薄薄的接口都是“声明”:Interface定义、DO的定义和扩展点的默认Reduce策略定义。SWAK框架的针对性改进在之前的《业务代码解构利器–SWAK》一文中指出了,SWAK框架在应用启动的时候会通过各种注册器(registery)注册框架所需的信息。其中最重要的信息就是——业务tag及其对应的SWAK接口的实现类类名或者类实例instance。大多RPC框架都会在client端提供一个代理,代理掉内部的服务发现、保活、序列化、网络通信、反序列化等一系列操作。实际上,SWAK为了支持远程服务调用,只需要将业务tag,以及这些RPC的client的instance的对应关系注册进去就可以了。在闲鱼,RPC使用的是阿里通用的HSF框架(其类似的一个开源框架是Dubbo),这里的RPC的client就是HSF中的ConsumerBean。上文还提到了RPC调用会引入服务超时、连接异常的概念。为何要限制超时?是因为不能被单个应用的超时占据了主干应用的服务资源而引起其他服务和整个应用系统受到影响(如大多数线程阻塞在超时调用上)。无论是超时异常还是连接异常,在业务上也有对应的处理策略。在这里,我们定义了三种异常处理策略,通过在配置上设置相应的注解,SWAK框架会自动按照策略来处理异常。这三种策略是:IGNORE。 即,直接向上层抛出异常。SKIP。对于一个接口有多个tag执行的时候,本tag下该扩展点将跳过,继续执行其他tag下该扩展点的实现。DEFAULT_VALUE。返回默认值。默认值通过spel表达式进行设置。减少扩展点数量众所周知,RPC调用相对于本地调用会增加一部分的网络传输和序列化开销。对于单次调用来说,增加若干ms并没有什么问题,但对于调用10次、20次或更多,这笔开销就相当可观而应该引起重视了。为此,如何降低RPC开销,是一个必须要考虑的问题。最可靠的方法就是降低RPC的次数。在实践中我们发现,很多扩展点实际上都是获取业务配置。如在闲鱼业务中,“是否支持多库存”就是一种配置,如租房不支持多库存。这些业务配置项是由其业务形态所决定的,基本不会变动。因此可以将一组配置项打包一起调用,并且可以缓存下来,也可以直接由主干应用进行维护。在item应用里,这些配置项关系到主干的通用存储过程,目前由各业务方委托主干开发人员进行维护,目前配置在主干环境。可以通过阿里的动态配置平台(如Switch、Diamond)进行动态修改。另外我们对部分邻接的扩展点进行了合并。这些相邻扩展点之间的逻辑比较简单,且不会中断主流程。通过“配置型接口”和“邻接扩展点合并”这两种操作,我们将扩展点数量降低由17个降低到了6个。要注意的是,扩展点并不是越少越好。扩展点越少,越意味着“过度拟合”,可能会对后续业务变更无法适应导致主干需要大幅改动,因此需要在数量和扩展性之间找到一个平衡。另外值得一提的是,SWAK为配置型扩展点做了相应的小改造,并提供了查看当前配置型扩展点返回值的可视化界面。开发人员可以直观地了解当前各个业务的配置值。接口对象定义和细节设计在闲鱼,各种业务所需要存储的东西大同小异,从闲鱼的发布界面上来看就不难发现这一点,都是在基础对象(如标题、描述、图片)之外添加一些业务相关的数据,如拍卖业务中指定拍卖的开拍时间等信息,免费送业务中设置兑换币值,图书业务上设置条形码。即对一本图书进行拍卖当然也是允许的,这就出现了拍卖业务和图书业务叠加起来的复合型业务。对于主干应用开发人员来说,应该提供单个接口以支持所有业务类型,这样不用每次修改或者新增业务时都需要提供新接口。从稳定性的角度考虑,这样的要求很合理。既然是单个接口,那么DO的定义也应该统一。以商品DO为例,有以下三种方式:第一种是继承型结构,该结构不适用于业务叠加的情况。另外主干需要知晓各个业务的DO,每次业务修改或新增,主干都需要做变动。第二种是组合型结构,适用于业务叠加的情况,但同上一种一样,主干需要知晓各个业务的DO,每次业务修改或新增,主干也需要随之变动。第三种使用了Map类型类承载各个业务(biz)的定义类型。主干完全不知道、也不需要知道各个业务DO是如何组成的。这种方式具有最好的扩展性(有点无边界的扩展),也分离了主干应用和业务应用,最接近主干和业务分离的期望。最终我们选择了这一种。使用第三种的对象模型,以新加一种业务为例,其开发流程是:新业务服务端开发人员和客户端开发人员约定各业务的DO,这些DO会存储到bizMap字段。主干应用开发人员不需要了解这些约定。主干应用新增一份新业务的配置,实际上是新业务的识别信息和路由信息。新业务应用实现主干扩展点。联调、测试和上线。业务应用在扩展点返回值中设置需要更新的数据,由主干应用合并。业务应用不应该也不可以直接修改ItemDO,避免影响其他业务的处理逻辑。对于发布和编辑这种需要持久化存储的逻辑来说,必须要强控各业务对ItemDO的修改,否则理论上来说,各业务都有可能将所有的关键字段修改得面目全非。前面提到的“配置型接口”中,就有这样的配置——该业务是否可以修改属性字段、该业务是否可以修改描述字段等配置。总结闲鱼的商品发布和编辑功能基于SWAK框架经过了两次改造升级,第一次升级完成了平台和业务之间的解耦合以及业务和业务之间的解耦合,第二次升级通过平台和业务间使用RPC调用完成了系统和系统之间的解耦合。改造之后,能更有效地协同更多团队更快更稳定地支撑各种业务。SWAK框架依然在继续演进,如部分扩展点原则上可以通过并行处理或异步化处理来提升性能,但暂时还没有提供支持。在这两次改造中, 我们还在测试用例的采集、回放、监控告警等方面也有很多积累,敬请期待后续的文章分享。本文作者:闲鱼技术-紫思阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 18, 2019 · 1 min · jiezi

腾讯 Tars-Go 服务 Hello World——从 HTTP 开始

引言本人上一篇文章《腾讯 Tars 基础框架手动搭建》简单介绍了 Tars 框架及其搭建方法。在我们的实际应用中,目前基于 Taf / Tars,主要采用 Node.js 和 C++ 进行开发。对于 C++ 程序员来说,目前最热门的后台开发语言莫过于 Google 的 Go。Tars 框架最新的版本已经把内部的 Taf-Go 开源为 Tars-Go。作为与时俱进的程序员,当然要尝鲜啦。本文中的代码均可以在 我的 GitHub repo 中查阅。本系列文章:腾讯 Tars 基础框架手动搭建——填掉官方 Guide 的坑腾讯 Tars-Go 服务 Hello World——从 HTTP 开始腾讯 Tars-Go 服务 Hello World——RPC 通信环境准备Go 环境开发环境显然要安装好 Go 了。请注意的是,TarsGo 要求 Go 版本 1.9 以上。最新稳定版已经是 1.11 了。安装最新版即可。Go 安装好之后,请注意配置好 $GOPATH 和 $GOROOT 环境变量,建议配置为 $HOME/go 目录。尽管在 Go 1.8 之后,go 命令的运行已经不再需要程序员配置上述变量(go 会自动配置,可执行 $ go env 查看),但是 TarsGo 的脚本在执行的时候还是需要依赖。TarsGo 包执行 go 安装命令并编译:$ go get github.com/TarsCloud/TarsGo/tars$ cd $GOPATH/src/github.com/TarsCloud/TarsGo/tars/tools/tars2go && go build .$ sudo cp tars2go $GOPATH/bin上述命令会把 TarsGo 下载下来,并且将比较重要的一个命令 tars2go 安装好。代码设计TarsGo 的官方 Quick Start 文档 的第一个例子,就是使用 tars 协议进行 server-client 的通信。不过我个人觉得,要说后台服务程序的 hello world 的话,第一个应该是 http 服务嘛,毕竟程序一运行就可以看到效果,这才是 hello world 嘛。给服务命名Tars 实例的名称,有三个层级,分别是 App(应用)、Server(服务)、Servant(服务者,有时也称 Object)三级。在前文我们已经初步接触到了:比如 Tars 基础框架中的 tarsstat,其服务的完整名称即为:tars.tarsstat.StatObj。Tars 实例的名称其中一个非常重要的作用就是用于服务间名字服务寻址。而对于 HTTP 这样的直接对外提供服务的实例而言,其实这块相对不是很重要,我们更多的是以描述服务功能的角度去命名。这里我把我的 HTTP 服务命名为 amc.GoWebServer.GoWebObj创建基础框架和 TarsCpp 一样,TarsGo 也提供了一个 create_tars_server.sh 脚本用于生成 tars 服务,但却没有提供 create_http_server.sh 生成 HTTP 服务。所以这里我们就直接用它就行了:$ cd $GOPATH/src/github.com/TarsCloud/TarsGo/tars/tools$ chmod +x create_tars_server.sh$ ./create_tars_server.sh amc GoWebServer GoWeb执行后我们可以查看生成的文件,清除不需要的:$ cd $GOPATH/src/amc/GoWebServer$ rm -rf GoWeb.tars client debugtool$ chmod +x start.sh$ ls -ltotal 44-rw-rw-r– 1 centos centos 303 Jan 5 22:09 GoWebImp.go-rw-rw-r– 1 centos centos 964 Jan 5 22:09 GoWebServer.conf-rw-rw-r– 1 centos centos 422 Jan 5 22:09 GoWebServer.go-rw-rw-r– 1 centos centos 252 Jan 5 22:09 makefile-rw-rw-r– 1 centos centos 59 Jan 5 22:09 start.shdrwxrwxr-x 2 centos centos 4096 Jan 5 22:09 vendor其实留下的,各文件里的内容,实际上我们都要完全替换掉的……首先是修改 makefile,自动生成的 makefile 内容是这样的:$ cat makefile APP := amcTARGET := GoWebServerMFLAGS :=DFLAGS :=CONFIG := clientSTRIP_FLAG:= NJ2GO_FLAG:= libpath=${subst :, ,$(GOPATH)}$(foreach path,$(libpath),$(eval -include $(path)/src/github.com/TarsCloud/TarsGo/tars/makefile.tars))我们把 “CONFIG := client” 行去掉就行了。代码修改GoWebServer.go接着是修改代码了。首先是 GoWebServer.go,这里参照官方 Guide 的写法就好了,TarsGo 的 HTTP 实现用的是 Go 原生的组件。我稍微调整了一下,把回调函数放在 GoWebImp.go 中(“imp” 是 implementation,我以前一直以为是小恶魔的意思……),将 GoWebServer.go 简化为:package mainimport ( “github.com/TarsCloud/TarsGo/tars”)func main() { mux := &tars.TarsHttpMux{} mux.HandleFunc("/", HttpRootHandler) cfg := tars.GetServerConfig() tars.AddHttpServant(mux, cfg.App+"."+cfg.Server+".GoWebObj") //Register http server tars.Run()}代码还是比较简单的,无需多言。GoWebImp.goGoWebServer.go 中的 HTTPRootHandler 回调函数定义在业务的主要实现逻辑 GoWebImp.go 文件中:package mainimport ( “fmt” “time” “net/http”)func HttpRootHandler(w http.ResponseWriter, r *http.Request) { time_fmt := “2006-01-02 15:04:05” local_time := time.Now().Local() time_str = local_time.Format(time_fmt) ret_str = fmt.Sprintf("{"msg":"Hello, Tars-Go!", "time":"%s"}", time_str) w.Header().Set(“Content-Type”, “application/json;charset=utf-8”) w.Write([]byte(ret_str)) return}部署发布编译打包编译打包上面的工程:$ cd $GOPATH/src/amc/GoWebServer$ make && make tar成功后,会在目录下生成目标文件 GoWebServer.tgz,后文部署发布时需要上传这个包。部署发布创建服务在 Tars 管理平台主页中,点击 “运维管理”,界面如下:Tars 管理平台没有专门的 “新增应用” 功能,所有 app、server、object 的新增都在这个界面中配置。输入一个不存在的对象,就相当于新增操作。所以我们新增 “amc.GoWebServer.GoWebObj”,就是在各项中如下填写:应用:amc服务名称:GoWebServer服务类型:tars_go模板:tars.default节点:填写你打算部署的 IP 地址OBJ:GoWebObj端口类型:TCP协议:非TARS端口可以自定义,也可以填好信息后点 “获取端口” 来生成。各项填写完毕后,点 “确定”,然后刷新界面,重新进入 Tars 管理平台主页,可以看到界面左边的列表就多了上面的配置:发布服务点击 “GoWebServer”,显示 “发布管理” 子标签。在 “服务列表” 中选中需要发布的节点,然后点击 “发布选中节点” 按钮:再点击 “上传发布包”,进入如下界面:点击 “发布包” 右边的 “确定” 按钮,在弹出的对话框中选择前面提到的 GoWebServer.tgz 文件。给这个发布包写好描述之后,点击确认,开始上传发布包:发布成功后,回到 “发布管理” 界面,在该界面中,选择刚才发布的包,然后点击发布,一切正常情况下,即可发布成功。服务验证假设前面获取到的 servant 端口为 10008,那么可以在机器上执行 curl 命令(比如我的机器 IP 是 10.0.4.11):$ curl 10.0.4.11:10008{“msg”:“Hello, Tars-Go!”,“unix”:1546747070,“time”:“2019-01-06 11:57:50”,“client”:":-1"}这就验证 OK 啦,同时也说明了 Tars 管理平台的配置值配置正确了。错误示范此外,本人开始的时候用的是 localhost 地址,但是却错误了:$ curl 127.0.0.1:10008curl: (52) Empty reply from server这里让我误以为服务没有发布成功,折腾了好久。究其原因,是因为在 Tars 中对 servant 自动生成的配置是这样的(以我的为例,在 “服务管理” 中点击 ”管理Servant“):留意在 “绑定地址” 中,线程监听的 IP 地址是 10.0.4.11,所以 localhost 自然就访问不到了。这里不建议修改,如果要修改的话,还需要修改 “服务配置”。这歌内容相对比较深入,本文就不详述了。本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。原文发布于:https://cloud.tencent.com/developer/article/1381300。 ...

January 12, 2019 · 2 min · jiezi

dubbo源码解析(十九)远程调用——开篇

远程调用——开篇目标:介绍之后解读远程调用模块的内容如何编排、介绍dubbo-rpc-api中的包结构设计以及最外层的的源码解析。前言最近我面临着一个选择,因为dubbo 2.7.0-release出现在了仓库里,最近一直在进行2.7.0版本的code review,那我之前说这一系列的文章都是讲述2.6.x版本的源代码,我现在要不要选择直接开始讲解2.7.0的版本的源码呢?我最后还是决定继续讲解2.6.x,因为我觉得还是有很多公司在用着2.6.x的版本,并且对于升级2.7.0的计划应该还没那么快,并且在了解2.6.x版本的原理后,再去了解2.7.0新增的特性会更加容易,也能够品位到设计者的意图。当然在结束2.6.x的重要模块讲解后,我也会对2.7.0的新特性以及实现原理做一个全面的分析,2.7.0作为dubbo社区的毕业版,更加强大,敬请期待。前面讲了很多的内容,现在开始将远程调用RPC,好像又回到我第一篇文章 《dubbo源码解析(一)Hello,Dubbo》,在这篇文章开头我讲到了什么叫做RPC,再通俗一点讲,就是我把一个项目的两部分代码分开来,分别放到两台机器上,当我部署在A服务器上的应用想要调用部署在B服务器上的应用等方法,由于不存在同一个内存空间,不能直接调用。而其实整个dubbo都在做远程调用的事情,它涉及到很多内容,比如配置、代理、集群、监控等等,那么这次讲的内容是只关心一对一的调用,dubbo-rpc远程调用模块抽象各种协议,以及动态代理,Proxy层和Protocol层rpc的核心,我将会在本系列中讲到。下面我们来看两张官方文档的图:暴露服务的时序图:你会发现其中有我们以前讲到的Transporter、Server、Registry,而这次的系列将会讲到的就是红色框框内的部分。引用服务时序图在引用服务时序图中,对应的也是红色框框的部分。当阅读完该系列后,希望能对这个调用链有所感悟。接下来看看dubbo-rpc的包结构:可以看到有很多包,很规整,其中dubbo-rpc-api是对协议、暴露、引用、代理等的抽象和实现,是rpc整个设计的核心内容。其他的包则是dubbo支持的9种协议,在官方文档也能查看介绍,并且包括一种本地调用injvm。那么我们再来看看dubbo-rpc-api中包结构:filter包:在进行服务引用时会进行一系列的过滤。其中包括了很多过滤器。listener包:看上面两张服务引用和服务暴露的时序图,发现有两个listener,其中的逻辑实现就在这个包内protocol包:这个包实现了协议的一些公共逻辑proxy包:实现了代理的逻辑。service包:其中包含了一个需要调用的方法等封装抽象。support包:包括了工具类最外层的实现。下面的篇幅设计,本文会讲解最外层的源码和service下的源码,support包下的源码我会穿插在其他用到的地方一并讲解,filter、listener、protocol、proxy以及各类协议的实现各自用一篇来讲。源码分析(一)Invokerpublic interface Invoker<T> extends Node { /** * get service interface. * 获得服务接口 * @return service interface. / Class<T> getInterface(); /* * invoke. * 调用下一个会话域 * @param invocation * @return result * @throws RpcException / Result invoke(Invocation invocation) throws RpcException;}该接口是实体域,它是dubbo的核心模型,其他模型都向它靠拢,或者转化成它,它代表了一个可执行体,可以向它发起invoke调用,这个有可能是一个本地的实现,也可能是一个远程的实现,也可能是一个集群的实现。它代表了一次调用(二)Invocationpublic interface Invocation { /* * get method name. * 获得方法名称 * @return method name. * @serial / String getMethodName(); /* * get parameter types. * 获得参数类型 * @return parameter types. * @serial / Class<?>[] getParameterTypes(); /* * get arguments. * 获得参数 * @return arguments. * @serial / Object[] getArguments(); /* * get attachments. * 获得附加值集合 * @return attachments. * @serial / Map<String, String> getAttachments(); /* * get attachment by key. * 获得附加值 * @return attachment value. * @serial / String getAttachment(String key); /* * get attachment by key with default value. * 获得附加值 * @return attachment value. * @serial / String getAttachment(String key, String defaultValue); /* * get the invoker in current context. * 获得当前上下文的invoker * @return invoker. * @transient / Invoker<?> getInvoker();}Invocation 是会话域,它持有调用过程中的变量,比如方法名,参数等。(三)Exporterpublic interface Exporter<T> { /* * get invoker. * 获得对应的实体域invoker * @return invoker / Invoker<T> getInvoker(); /* * unexport. * 取消暴露 * <p> * <code> * getInvoker().destroy(); * </code> / void unexport();}该接口是暴露服务的接口,定义了两个方法分别是获得invoker和取消暴露服务。(四)ExporterListener@SPIpublic interface ExporterListener { /* * The exporter exported. * 暴露服务 * @param exporter * @throws RpcException * @see com.alibaba.dubbo.rpc.Protocol#export(Invoker) / void exported(Exporter<?> exporter) throws RpcException; /* * The exporter unexported. * 取消暴露 * @param exporter * @throws RpcException * @see com.alibaba.dubbo.rpc.Exporter#unexport() / void unexported(Exporter<?> exporter);}该接口是服务暴露的监听器接口,定义了两个方法是暴露和取消暴露,参数都是Exporter类型的。(五)Protocol@SPI(“dubbo”)public interface Protocol { /* * Get default port when user doesn’t config the port. * 获得默认的端口 * @return default port / int getDefaultPort(); /* * Export service for remote invocation: <br> * 1. Protocol should record request source address after receive a request: * RpcContext.getContext().setRemoteAddress();<br> * 2. export() must be idempotent, that is, there’s no difference between invoking once and invoking twice when * export the same URL<br> * 3. Invoker instance is passed in by the framework, protocol needs not to care <br> * 暴露服务方法, * @param <T> Service type 服务类型 * @param invoker Service invoker 服务的实体域 * @return exporter reference for exported service, useful for unexport the service later * @throws RpcException thrown when error occurs during export the service, for example: port is occupied / @Adaptive <T> Exporter<T> export(Invoker<T> invoker) throws RpcException; /* * Refer a remote service: <br> * 1. When user calls invoke() method of Invoker object which’s returned from refer() call, the protocol * needs to correspondingly execute invoke() method of Invoker object <br> * 2. It’s protocol’s responsibility to implement Invoker which’s returned from refer(). Generally speaking, * protocol sends remote request in the Invoker implementation. <br> * 3. When there’s check=false set in URL, the implementation must not throw exception but try to recover when * connection fails. * 引用服务方法 * @param <T> Service type 服务类型 * @param type Service class 服务类名 * @param url URL address for the remote service * @return invoker service’s local proxy * @throws RpcException when there’s any error while connecting to the service provider / @Adaptive <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; /* * Destroy protocol: <br> * 1. Cancel all services this protocol exports and refers <br> * 2. Release all occupied resources, for example: connection, port, etc. <br> * 3. Protocol can continue to export and refer new service even after it’s destroyed. / void destroy();}该接口是服务域接口,也是协议接口,它是一个可扩展的接口,默认实现的是dubbo协议。定义了四个方法,关键的是服务暴露和引用两个方法。(六)Filter@SPIpublic interface Filter { /* * do invoke filter. * <p> * <code> * // before filter * Result result = invoker.invoke(invocation); * // after filter * return result; * </code> * * @param invoker service * @param invocation invocation. * @return invoke result. * @throws RpcException * @see com.alibaba.dubbo.rpc.Invoker#invoke(Invocation) / Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;}该接口是invoker调用时过滤器接口,其中就只有一个invoke方法。在该方法中对调用进行过滤(七)InvokerListener@SPIpublic interface InvokerListener { /* * The invoker referred * 在服务引用的时候进行监听 * @param invoker * @throws RpcException * @see com.alibaba.dubbo.rpc.Protocol#refer(Class, com.alibaba.dubbo.common.URL) / void referred(Invoker<?> invoker) throws RpcException; /* * The invoker destroyed. * 销毁实体域 * @param invoker * @see com.alibaba.dubbo.rpc.Invoker#destroy() / void destroyed(Invoker<?> invoker);}该接口是实体域的监听器,定义了两个方法,分别是服务引用和销毁的时候执行的方法。(八)Result该接口是实体域执行invoke的结果接口,里面定义了获得结果异常以及附加值等方法。比较好理解我就不贴代码了。(九)ProxyFactory@SPI(“javassist”)public interface ProxyFactory { /* * create proxy. * 创建一个代理 * @param invoker * @return proxy / @Adaptive({Constants.PROXY_KEY}) <T> T getProxy(Invoker<T> invoker) throws RpcException; /* * create proxy. * 创建一个代理 * @param invoker * @return proxy / @Adaptive({Constants.PROXY_KEY}) <T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException; /* * create invoker. * 创建一个实体域 * @param <T> * @param proxy * @param type * @param url * @return invoker / @Adaptive({Constants.PROXY_KEY}) <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;}该接口是代理工厂接口,它也是个可扩展接口,默认实现javassist,dubbo提供两种动态代理方法分别是javassist/jdk,该接口定义了三个方法,前两个方法是通过invoker创建代理,最后一个是通过代理来获得invoker。(十)RpcContext该类就是远程调用的上下文,贯穿着整个调用,例如A调用B,然后B调用C。在服务B上,RpcContext在B之前将调用信息从A保存到B。开始调用C,并在B调用C后将调用信息从B保存到C。RpcContext保存了调用信息。public class RpcContext { /* * use internal thread local to improve performance * 本地上下文 / private static final InternalThreadLocal<RpcContext> LOCAL = new InternalThreadLocal<RpcContext>() { @Override protected RpcContext initialValue() { return new RpcContext(); } }; /* * 服务上下文 / private static final InternalThreadLocal<RpcContext> SERVER_LOCAL = new InternalThreadLocal<RpcContext>() { @Override protected RpcContext initialValue() { return new RpcContext(); } }; /* * 附加值集合 / private final Map<String, String> attachments = new HashMap<String, String>(); /* * 上下文值 / private final Map<String, Object> values = new HashMap<String, Object>(); /* * 线程结果 / private Future<?> future; /* * url集合 / private List<URL> urls; /* * 当前的url / private URL url; /* * 方法名称 / private String methodName; /* * 参数类型集合 / private Class<?>[] parameterTypes; /* * 参数集合 / private Object[] arguments; /* * 本地地址 / private InetSocketAddress localAddress; /* * 远程地址 / private InetSocketAddress remoteAddress; /* * 实体域集合 / @Deprecated private List<Invoker<?>> invokers; /* * 实体域 / @Deprecated private Invoker<?> invoker; /* * 会话域 / @Deprecated private Invocation invocation; // now we don’t use the ‘values’ map to hold these objects // we want these objects to be as generic as possible /* * 请求 / private Object request; /* * 响应 / private Object response;该类中最重要的是它的一些属性,因为该上下文就是用来保存信息的。方法我就不介绍了,因为比较简单。(十一)RpcException/* * 不知道异常 /public static final int UNKNOWN_EXCEPTION = 0;/* * 网络异常 /public static final int NETWORK_EXCEPTION = 1;/* * 超时异常 /public static final int TIMEOUT_EXCEPTION = 2;/* * 基础异常 /public static final int BIZ_EXCEPTION = 3;/* * 禁止访问异常 /public static final int FORBIDDEN_EXCEPTION = 4;/* * 序列化异常 /public static final int SERIALIZATION_EXCEPTION = 5;该类是rpc调用抛出的异常类,其中封装了五种通用的错误码。(十二)RpcInvocation/* * 方法名称 /private String methodName;/* * 参数类型集合 /private Class<?>[] parameterTypes;/* * 参数集合 /private Object[] arguments;/* * 附加值 /private Map<String, String> attachments;/* * 实体域 /private transient Invoker<?> invoker;该类实现了Invocation接口,是rpc的会话域,其中的方法比较简单,主要是封装了上述的属性。(十三)RpcResult/* * 结果 /private Object result;/* * 异常 /private Throwable exception;/* * 附加值 /private Map<String, String> attachments = new HashMap<String, String>();该类实现了Result接口,是rpc的结果实现类,其中关键是封装了以上三个属性。(十四)RpcStatus该类是rpc的一些状态监控,其中封装了许多的计数器,用来记录rpc调用的状态。1.属性/* * uri对应的状态集合,key为uri,value为RpcStatus对象 /private static final ConcurrentMap<String, RpcStatus> SERVICE_STATISTICS = new ConcurrentHashMap<String, RpcStatus>();/* * method对应的状态集合,key是uri,第二个key是方法名methodName /private static final ConcurrentMap<String, ConcurrentMap<String, RpcStatus>> METHOD_STATISTICS = new ConcurrentHashMap<String, ConcurrentMap<String, RpcStatus>>();/* * 已经没用了 /private final ConcurrentMap<String, Object> values = new ConcurrentHashMap<String, Object>();/* * 活跃状态 /private final AtomicInteger active = new AtomicInteger();/* * 总的数量 /private final AtomicLong total = new AtomicLong();/* * 失败的个数 /private final AtomicInteger failed = new AtomicInteger();/* * 总计过期个数 /private final AtomicLong totalElapsed = new AtomicLong();/* * 失败的累计值 /private final AtomicLong failedElapsed = new AtomicLong();/* * 最大火气的累计值 /private final AtomicLong maxElapsed = new AtomicLong();/* * 最大失败累计值 /private final AtomicLong failedMaxElapsed = new AtomicLong();/* * 成功最大累计值 /private final AtomicLong succeededMaxElapsed = new AtomicLong();/* * Semaphore used to control concurrency limit set by executes * 信号量用来控制execution设置的并发限制 /private volatile Semaphore executesLimit;/* * 用来控制execution设置的许可证 /private volatile int executesPermits;以上是该类的属性,可以看到保存了很多的计数器,分别用来记录了失败调用成功调用等累计数。2.beginCount/* * 开始计数 * @param url /public static void beginCount(URL url, String methodName) { // 对该url对应对活跃计数器加一 beginCount(getStatus(url)); // 对该方法对活跃计数器加一 beginCount(getStatus(url, methodName));}/* * 以原子方式加1 * @param status /private static void beginCount(RpcStatus status) { status.active.incrementAndGet();}该方法是增加计数。3.endCountpublic static void endCount(URL url, String methodName, long elapsed, boolean succeeded) { // url对应的状态中计数器减一 endCount(getStatus(url), elapsed, succeeded); // 方法对应的状态中计数器减一 endCount(getStatus(url, methodName), elapsed, succeeded);}private static void endCount(RpcStatus status, long elapsed, boolean succeeded) { // 活跃计数器减一 status.active.decrementAndGet(); // 总计数器加1 status.total.incrementAndGet(); // 过期的计数器加上过期个数 status.totalElapsed.addAndGet(elapsed); // 如果最大的过期数小于elapsed,则设置最大过期数 if (status.maxElapsed.get() < elapsed) { status.maxElapsed.set(elapsed); } // 如果rpc调用成功 if (succeeded) { // 如果成功的最大值小于elapsed,则设置成功最大值 if (status.succeededMaxElapsed.get() < elapsed) { status.succeededMaxElapsed.set(elapsed); } } else { // 失败计数器加一 status.failed.incrementAndGet(); // 失败的过期数加上elapsed status.failedElapsed.addAndGet(elapsed); // 失败最大值小于elapsed,则设置失败最大值 if (status.failedMaxElapsed.get() < elapsed) { status.failedMaxElapsed.set(elapsed); } }}该方法是计数器减少。(十五)StaticContext该类是系统上下文,仅供内部使用。/* * 系统名称 /private static final String SYSTEMNAME = “system”;/* * 系统上下文集合,仅供内部使用 /private static final ConcurrentMap<String, StaticContext> context_map = new ConcurrentHashMap<String, StaticContext>();/* * 系统上下文名称 /private String name;上面是该类的属性,它还记录了所有的系统上下文集合。(十六)EchoServicepublic interface EchoService { /* * echo test. * 回声测试 * @param message message. * @return message. / Object $echo(Object message);}该接口是回声服务接口,定义了一个一个回声测试的方法,回声测试用于检测服务是否可用,回声测试按照正常请求流程执行,能够测试整个调用是否通畅,可用于监控,所有服务自动实现该接口,只需将任意服务强制转化为EchoService,就可以用了。(十七)GenericException该方法是通用的异常类。/* * 异常类名 /private String exceptionClass;/* * 异常信息 /private String exceptionMessage;比较简单,就封装了两个属性。(十八)GenericServicepublic interface GenericService { /* * Generic invocation * 通用的会话域 * @param method Method name, e.g. findPerson. If there are overridden methods, parameter info is * required, e.g. findPerson(java.lang.String) * @param parameterTypes Parameter types * @param args Arguments * @return invocation return value * @throws Throwable potential exception thrown from the invocation */ Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;}该接口是通用的服务接口,同样定义了一个类似invoke的方法后记该部分相关的源码解析地址:https://github.com/CrazyHZM/i…该文章讲解了远程调用的开篇,介绍之后解读远程调用模块的内容如何编排、介绍dubbo-rpc-api中的包结构设计以及最外层的的源码解析,其中的逻辑不负责,要关注的是其中的一些概念和dubbo如何去做暴露服务和引用服务,其中很多的接口定义需要弄清楚。接下来我将开始对rpc模块的过滤器进行讲解。 ...

January 7, 2019 · 7 min · jiezi

如何设计一个 RPC 系统

本文由云+社区发表RPC是一种方便的网络通信编程模型,由于和编程语言的高度结合,大大减少了处理网络数据的复杂度,让代码可读性也有可观的提高。但是RPC本身的构成却比较复杂,由于受到编程语言、网络模型、使用习惯的约束,有大量的妥协和取舍之处。本文就是通过分析几种流行的RPC实现案例,提供大家在设计RPC系统时的参考。由于RPC底层的网络开发一般和具体使用环境有关,而编程实现手段也非常多样化,但不影响使用者,因此本文基本涉及如何实现一个RPC系统。认识 RPC (远程调用)我们在各种操作系统、编程语言生态圈中,多少都会接触过“远程调用”的概念。一般来说,他们指的是用简单的一行代码,通过网络调用另外一个计算机上的某段程序。比如:RMI——Remote Method Invoke:调用远程的方法。“方法”一般是附属于某个对象上的,所以通常RMI指对在远程的计算机上的某个对象,进行其方法函数的调用。RPC——Remote Procedure Call:远程过程调用。指的是对网络上另外一个计算机上的,某段特定的函数代码的调用。远程调用本身是网络通信的一种概念,他的特点是把网络通信封装成一个类似函数的调用。网络通信在远程调用外,一般还有其他的几种概念:数据包处理、消息队列、流过滤、资源拉取等待。下面比较一下他们差异:方案编程方式信息封装传输模型典型应用远程调用调用函数,输入参数,获得返回值。使用编程语言的变量、类型、函数发出请求,获得响应Java RMI数据包处理调用Send()/Recv(),使用字节码数据,编解码,处理内容把通信内容构造成二进制的协议包发送/接收UDP编程消息队列调用Put()/Get(),使用“包”对象,处理其包含的内容消息被封装成语言可用的对象或结构对某队列,存入一个消息;取出一个消息ActiveMQ流过滤读取一个流,或写出一个流,对流中的单元包即刻处理单元长度很小的统一数据结构连接;发送/接收;处理网络视频资源拉取输入一个资源ID,获得资源内容请求或响应都包含:头部+正文请求后等待响应WWW针对远程调用的特点——调用函数。业界在各种语言下都开发过类似的方案,同时也有些方案是试图做到跨语言的。尽管远程调用在编程方式上,看起来似乎是最简单易用的,但是也有明显的缺点。所以了解清楚远程调用的优势和缺点,是决定是否要开发、或者使用远程调用这种模型的关键问题。远程调用的优势有:屏蔽了网络层。因此在传输协议和编码协议上,我们可以选择不同的方案。比如WebService方案就是用的HTTP传输协议+SOAP编码协议;而REST的方案往往使用HTTP+JSON协议。Facebook的Thrift甚至可以定制任何不同的传输协议和编码协议,你可以用TCP+Google Protocol Buffer,也可以用UDP+JSON……。由于屏蔽了网络层,你可以根据实际需要来独立的优化网络部分,而无需涉及业务逻辑的处理代码,这对于需要在各种网络环境下运行的程序来说,非常有价值。函数映射协议。你可以直接用编程语言来书写数据结构和函数定义,取代编写大量的编码协议格式和分包处理逻辑。对于那些业务逻辑非常复杂的系统,比如网络游戏,可以节省大量定义消息格式的时间。而且函数调用模型非常容易学习,不需要学习通信协议和流程,让经验较浅的程序员也能很容易的开始使用网络编程。远程调用的缺点:增加了性能消耗。由于把网络通信包装成“函数”,需要大量额外的处理。比如需要预生产代码,或者使用反射机制。这些都是额外消耗CPU和内存的操作。而且为了表达复杂的数据类型,比如变长的类型string/map/list,这些都要数据包中增加更多的描述性信息,则会占用更多的网络包长度。不必要的复杂化。如果你仅仅是为了某些特定的业务需求,比如传送一个固定的文件,那么你应该用HTTP/FTP协议模型。如果为了做监控或者IM软件,用简单的消息编码收发会更快速高效。如果是为了做代理服务器,用流式的处理会很简单。另外,如果你要做数据广播,那么消息队列会很容易做到,而远程调用这几乎无法完成。因此,远程调用最适合的场景是:业务需求多变,网络环境多变。RPC方案的核心问题由于远程调用的使用接口是“函数”,所以要如何构建这个“函数”,就产生了三个方面需要决策的问题:1 . 如何表示“远程”的信息所谓远程,就是指网络上另外一个位置,那么网络地址就是必须要输入的部分。在TCP/IP网络下,IP地址和端口号代表了运行中程序的一个入口。所以指定IP地址和端口是发起远程调用所必需的。然而,一个程序可能会运行很多个功能,可以接收多个不同含义的远程调用。这样如何去让用户指定这些不同含义的远程调用入口,就成为了另外一个问题。当然最简单的是每个端口一种调用,但是一个IP最多支持65535个端口,而且别的网络功能也可能需要端口,所以这种方案可能会不够用,同时一个数字代表一个功能也不太好理解,必须要查表才能明白。所以我们必须想别的方法。在面向对象的思想下,有些方案提出了:以不同的对象来归纳不同的功能组合,先指定对象,再指定方法。这个想法非常符合程序员的理解方式,EJB就是这种方案的。一旦你确定了用对象这种模型来定义远程调用的地址,那么你就需要有一种指定远程对象的方法,为了指定对象,你必须要能把对象的一些信息,从被调用方(服务器端)传输给调用方(客户端)。最简单的方案就是客户端输入一串字符串作为对象的“名字”,发给服务器端,查找注册了这个“名字”的对象,如果找到了,服务器端就会用某种技术“传输”这个对象给客户端,然后客户端就可以调用他的方法了。当然这种传输不可能是把整个服务器上的对象数据拷贝给客户端,而是用一些符号或者标志的方法,来代表这个服务器上的对象,然后发给客户端。如果你不是使用面向对象的模型,那么远程的一个函数,也是必须要定位和传输的,因为你调用的函数必须先能找到,然后成为客户端侧的一个接口,才能调用。针对“远程对象”(这里说的对象包括面向对象的对象或者仅仅是 函数)如何表达才能在网络上定位;以及定位成功之后以什么形式供客户端调用,都是“远程调用”设计方案中第一个重要的问题。2 . 函数的接口形式应该如何表示远程调用由于受到网络通信的约束,所以往往不能完全的支持编程语言的所有特性。比如C语言函数中的指针类型参数,就无法通过网络传递出去。因此远程调用的函数定义,能用语言中的什么特性,不能用什么特性,是需要在设计方案是规定下来的。这种规定如果太严格,会影响使用者的易用性;如果太宽泛,则可能导致远程调用的性能低下。如何去设计一种方式,把编程语言中的函数,描述成一个远程调用的函数,也是需要考虑的问题。很多方案采用了配置文件这种通用的方式,而另外一些方案可以直接在源代码中里面加特殊的注释。一般来说,编译型语言如C/C++只能采用源代码根据配置文件生成的方案,虚拟机型语言如C#/JAVA可以采用反射机制结合配置文件(设置是在源代码中用特殊注释来代替配置文件)的方案,如果是脚本语言就更简单,有时候连配置文件都不需要,因为脚本自己就可以充当。总之远程调用的接口要满足怎样的约束,也是一个需要仔细考虑的问题。3. 用什么方法来实现网络通信远程调用最重要的实现细节,就是关于网络通信。用何种通信方式来承载远程调用的问题,细化下来就是两个子问题:用什么样的服务程序提供网络功能?用什么样的通信协议?远程调用系统可以自己直接对TCP/IP编程来实现通信,也可以委托一些其他软件,比如Web服务器、消息队列服务器等等……也可以使用不同的网络通信框架,如Netty/Mina这些开源框架。通信协议则一般有两层:一个是传输协议,比如TCP/UDP或者高层一点的HTTP,或者自己定义的传输协议;另外一个是编码协议,就是如何把一个编程语言中的对象,序列化和反序列化成为二进制字节流的方案,流行的方案有JSON、Google Protocol Buffer等等,很多开发语言也有自己的序列化方案,如JAVA/C#都自带。以上这些技术细节,应该选择使用哪些,直接关系到远程调用系统的性能和环境兼容性。以上三个问题,就是远程调用系统必须考虑的核心选型。根据每个方案所面对的约束不同,他们都会在这三个问题上做出取舍,从而适应其约束。但是现在并不存在一个“万能”或者“通用”的方案,其原因就是:在如此复杂的一个系统中,如果要照顾的特性越多,需要付出的成本(易用性代价、性能开销)也会越多。下面,我们可以研究下业界现存的各种远程调用方案,看他们是如何在这三个方面做平衡和选择的。业界方案举例1. CORBACORBA是一个“古老”的,雄心勃勃的方案,他试图在完成远程调用的同时,还完成跨语言的通信的任务,因此其复杂程度是最高的,但是它的设计思想,也被后来更多的其他方案所学习。在通信对象的定位上,它使用URL来定义一个远程对象,这是在互联网时代非常容易接受的。其对象的内容则限定在C语言类型上,并且只能传递值,这也是非常容易理解的。为了能让不同语言的程序通信,所以就必须要在各种编程语言之外独立设计一种仅仅用于描述远程接口的语言,这就是所谓的IDL:Interface Description Language 接口描述语言。用这个方法,你就可以先用一种超然于所有语言之外的语言来定义接口,然后使用工具自动生成各种编程语言的代码。这种方案对于编译型语言几乎是唯一选择。CORBA并没有对通信问题有任何约定,而是留给具体语言的实现者去处理,这也许是他没有广泛流行的原因之一。实际上CORBA有一个非常著名的继承者,他就是Facebook公司的Thrift框架。Thrift也是使用一种IDL编译生成多种语言的远程调用方案,并且用C++/JAVA等多种语言完整的实现了通信承载,所以在开源框架中是特别有号召力的一个。Thrfit的通信承载还有个特点,就是能组合使用各种不同的传输协议和编码协议,比如TCP/UDP/HTTP配合JSON/BIN/PB……这让它几乎可以选择任何的网络环境。Thrift的模型类似下图,这里有的stub表示“桩代码”,就是客户端直接使用的函数形式程序;skeleton表示“骨架代码”,是需要程序员编写具体提供远程服务功能的模板代码,一般对模版做填空或者继承(扩展)即可。这个stub-skeleton模型几乎是所有远程调用方案的标配。2. JAVA RMIJAVA RMI是JAVA虚拟机自带的一个远程调用方案。它也是可以使用URL来定位远程对象,使用JAVA自带的序列化编码协议传递参数值。在接口描述上,由于这是一个仅限于JAVA环境下的方案,所以直接用JAVA语言的Interface类型作为定义语言。用户通过实现这个接口类型来提供远程服务,同时JAVA会根据这个接口文件自动生成客户端的调用代码供调用者使用。他的底层通信实现,还是用TCP协议实现的。在这里,Interface文件就是JAVA语言的IDL,同时也是skeleton模板,供开发者来填写远程服务内容。而stub代码则由于JAVA的反射功能,由虚拟机直接包办了。这个方案由于JAVA虚拟机的支持,使用起来非常简单,完全按照标志的JAVA编程方法就可以轻松解决问题,但是这也仅仅能在JAVA环境下运行,限制了其适用的范围。鱼与熊掌不可兼得,易用性和适用性往往是互相冲突的。这和CORBA/Thrift追求最大范围的适用性有很大的差别,也导致了两者在易用性上的不同。3. Windows RPCWindows中对RPC支持是比较早和比较完善的。首先它通过GUID来查询对象,然后使用C语言类型作为参数值的传递。由于Windows的API主要是C语言的,所以对于RPC功能来说,还是要用一种IDL来描述接口,最后生成.h和.c文件来生产RPC的stub和skeleton代码。而通信机制,由于是操作系统自带的,所以使用内核LPC机制承载,这一点还是对使用者来说比较方便的。但是也限制了只能用于Windows程序之间做调用。4. WebService & REST在互联网时代,程序需要通过互联网来互相调用。而互联网上最流行的协议是HTTP协议和WWW服务,因此使用HTTP协议的Web Service就顺理成章的成为跨系统调用的最流行方案。由于可以使用大多数互联网的基础设施,所以Web Service的开发和实现几乎是毫无难度的。一般来说,它都会使用URL来定位远程对象,而参数则通过一系列预定义的类型(主要是C语言基础类型),以及对象序列化方式来传递。接口生成方面,你可以自己直接对HTTP做解析,也可以使用诸如WSDL或者SOAP这样的规范。在REST的方案中,则限定了只有PUT/GET/DELETE/POST四种操作函数,其他都是参数。总结一下上面的这些RPC方案,我们发现,针对远程调用的三个核心问题,一般业界有以下几个选择:远程对象定位:使用URL;或者使用名字服务来查找远程调用参数传递:使用C的基本类型定义;或者使用某种预订的序列化(反序列化)方案接口定义:使用某种特定格式的技术,直接按预先约定一种接口定义文件;或者使用某种描述协议IDL来生成这些接口文件通信承载:有使用特定TCP/UDP之类的服务器,也有可以让用户自己开发定制的通信模型;还有使用HTTP或者消息队列这一类更加高级的传输协议方案选型在我们确定了远程调用系统方案几个可行选择后,自然就要明确一下各个方案的优缺点,这样才能选择真正合适需求的设计:1. 对于远程对象的描述:使用URL是互联网通行的标准,比较方便用户理解,也容易添加日后需要扩展到内容,因为URL本身是一个由多个部分组合的字符串;而名字服务则老式一些,但是依然有他的好处,就是名字服务可以附带负载均衡、容灾扩容、自定义路由等一系列特性,对于需求复杂的定位比较容易实现。2. 远程调用的接口描述:如果只限制于某个语言、操作系统、平台上,直接利用“隐喻”方式的接口描述,或者以“注解”类型注释手段来标注源代码,实现远程调用接口的定义,是最方便不过的。但是,如果需要兼容编译型语言,如C/C++,就一定要用某种IDL来生成这些编译语言的源代码了。3.通信承载:给用户自己定制通信模块,能提供最好的适用性,但是也让用户增加了使用的复杂程度。而HTTP/消息队列这种承载方式,在系统的部署、运维、编程上都会比较简单,缺点就是对于性能、传输特性的定制空间就比较小。分析完核心问题,我们还需要考虑一些适用性场景:1. 面向对象还是面向过程:如果我们只是考虑做面向过程的远程调用,只需要定位到“函数”即可。而如果是面向对象的,则需要定位到“对象”。由于函数是无状态的,所以其定位过程可以简单到一个名字即可,而对象则需要动态的查找到其ID或句柄。2.跨语言还是单一语言:单一语言的方案中,头文件或接口定义完全用一种语言处理即可,如果是跨语言的,就少不免要IDL3. 混合式通信承载还是使用HTTP服务器承载:混合式承载可能可以用到TCP/UDP/共享内存等底层技术,可以提供最优的性能,但是使用起来必然非常麻烦。使用HTTP服务器的话,则非常简单,因为WWW服务的开源软件、库众多,而且客户端使用浏览器或者一些JS页面即可调试,缺点是其性能较低。假设我们现在要为某种业务逻辑非常多变的领域,如企业业务应用领域,或游戏服务器端领域,去设计一个远程调用系统,我们可能应该如下选择:1. 使用名字服务定位远程对象:由于企业服务是需要高可用性的,使用名字服务能在查询名字时识别和选择可用性服务对象。J2EE方案中的EJB(企业JavaBean)就是用名字服务的。2. 使用IDL来生成接口定义:由于企业服务或游戏服务,其开发语言可能不是统一的,又或者需要高性能的编程语言如C/C++,所以只能使用IDL。3.使用混合式通信承载:虽然企业服务看起来无需在很复杂的网络下运行,但是不同的企业的网络环境又可能是千差万别的,所以要做一个通用的系统,最好还是不怕麻烦提供混合式的通信承载,这样可以在TCP/UDP等各种协议中选择。此文已由作者授权腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

January 3, 2019 · 1 min · jiezi

Uber RPC 框架TChannel源码分析——多路复用的实现

原文:Uber RPC 框架TChannel源码分析——多路复用的实现声明tchannel-go版本为v1.12.0阅读本篇文章需要go语言,HTTP2——多路复用基础前言 UBER的RPC框架TChannel有一个闪亮点————多路复用。对于多路复用是如何实现一直都很好奇,所以抽了点时间看了TChannel多路复用的实现源码,并整理成这篇文章。文章主要从客户端【发起请求】到服务端【响应请求】一条完整请求来看多路复用整个生命周期的实现。客户端发起调用客户端调用我们把这个过程分成4个步骤: 1. 出站握手 2. 复用链接 3. 消息交换 4. 有序写入——发起请求出站握手github.com/uber/tchannel-go/preinit_connection.go #35func (ch *Channel) outboundHandshake(ctx context.Context, c net.Conn, outboundHP string, events connectionEvents) (_ Connection, err error) { …… msg := &initReq{initMessage: ch.getInitMessage(ctx, 1)} if err := ch.writeMessage(c, msg); err != nil { return nil, err } …… res := &initRes{} id, err := ch.readMessage(c, res) if err != nil { return nil, err } …… return ch.newConnection(c, 1 / initialID */, outboundHP, remotePeer, remotePeerAddress, events), nil} 在开始请求前,TChannel有一次握手,这次握手不是TCP/IP的三次握手,是为了确认服务端能够正常响应。 如果服务端能够正常响应,则这条TCP链接将会被复用。func (ch *Channel) newConnection(conn net.Conn, initialID uint32, outboundHP string, remotePeer PeerInfo, remotePeerAddress peerAddressComponents, events connectionEvents) *Connection { …… connID := nextConnID.Inc() …… c := &Connection{ channelConnectionCommon: ch.channelConnectionCommon, connID: connID, conn: conn, opts: opts, state: connectionActive, sendCh: make(chan *Frame, opts.SendBufferSize), …… inbound: newMessageExchangeSet(log, messageExchangeSetInbound), outbound: newMessageExchangeSet(log, messageExchangeSetOutbound), …… } …… // Connections are activated as soon as they are created. c.callOnActive() go c.readFrames(connID) go c.writeFrames(connID) return c} 当握手成功,这条链接随后会被放入Peer,以备其他请求使用。同时会启动2个协程,“readFrames” 用于读取服务端的响应,“writeFrames”把数据写入TCP链接里面,关于这2个协程的作用下面会详细介绍。复用链接github.com/uber/tchannel-go/peer.go #361func (p *Peer) getActiveConnLocked() (*Connection, bool) { allConns := len(p.inboundConnections) + len(p.outboundConnections) if allConns == 0 { return nil, false } // We cycle through the connection list, starting at a random point // to avoid always choosing the same connection. startOffset := peerRng.Intn(allConns) for i := 0; i < allConns; i++ { connIndex := (i + startOffset) % allConns if conn := p.getConn(connIndex); conn.IsActive() { return conn, true } } return nil, false} 复用链接是多路复用很关键的一步,和HTTP的复用不同,HTTP链接需要响应成功后才能被复用,而多路复用链接只要被创建了就能被复用。消息交换 —— 无序响应github.com/uber/tchannel-go/mex.go #306func (mexset *messageExchangeSet) newExchange(ctx context.Context, framePool FramePool, msgType messageType, msgID uint32, bufferSize int) (*messageExchange, error) { …… mex := &messageExchange{ msgType: msgType, msgID: msgID, ctx: ctx, //请求会等待Frame的写入 recvCh: make(chan *Frame, bufferSize), errCh: newErrNotifier(), mexset: mexset, framePool: framePool, } mexset.Lock() //保存messageExchange addErr := mexset.addExchange(mex) mexset.Unlock() …… mexset.onAdded() …… return mex, nil} 在客户端发起多个请求的时候,由于只有一个TCP链接,如何知道哪个响应是对应哪个请求?为了能够正确响应,TChannel使用了MessageExchange,一个请求对应一个MessageExchange。客户端会以stream id 为下标索引,保存所有的MessageExchange。当有一个请求时,它会阻塞在MessageExchange.recvCh, 响应回来会根据响应的stream id获取对应的MessageExchange, 并把帧放到 MessageExchange.recvCh 从而实现无序响应。有序写入——发起请求先写入队列github.com/uber/tchannel-go/reqres.go #139func (w *reqResWriter) flushFragment(fragment *writableFragment) error { …… frame := fragment.frame.(*Frame) …… select { …… case w.conn.sendCh <- frame: return nil }}获取队列数据,写入TCP链接github.com/uber/tchannel-go/connection.go #706func (c *Connection) writeFrames( uint32) { for { select { case f := <-c.sendCh: …… err := f.WriteOut(c.conn) …… } }} 在多路复用中,只有一条TCP链接,为了避免客户端同时写入链接里,TChannel先把帧写入队列“sendCh”,再使用一个消费者获取队列数据,然后有序写入链接里面。帧结构github.com/uber/tchannel-go/frame.go #107// A Frame is a header and payloadtype Frame struct { buffer []byte // full buffer, including payload and header headerBuffer []byte // slice referencing just the header // The header for the frame Header FrameHeader // The payload for the frame Payload []byte}// FrameHeader is the header for a frame, containing the MessageType and sizetype FrameHeader struct { // The size of the frame including the header size uint16 // The type of message represented by the frame messageType messageType // Left empty reserved1 byte // The id of the message represented by the frame ID uint32 //指Stream ID // Left empty reserved [8]byte} 帧被分为2部分,一部分是Header Frame(只有16字节);另一部分是Data Frame。这2部分数据按照一定格式标准转成二进制数据进行传输。服务端响应服务端响应我们把这个过程分成3个步骤: 1. 入站握手 2. 读取请求数据 3. 有序写入——响应结果入站握手github.com/uber/tchannel-go/preinit_connection.go #69func (ch *Channel) inboundHandshake(ctx context.Context, c net.Conn, events connectionEvents) (_ Connection, err error) { id := uint32(math.MaxUint32) …… req := &initReq{} id, err = ch.readMessage(c, req) if err != nil { return nil, err } …… res := &initRes{initMessage: ch.getInitMessage(ctx, id)} if err := ch.writeMessage(c, res); err != nil { return nil, err } return ch.newConnection(c, 0 / initialID /, "" / outboundHP */, remotePeer, remotePeerAddress, events), nil} 入站握手是对客户端出站握手的响应,当握手成功,服务端这边也会调用newConnection,启动“readFrames” 和 “writeFrames”协程,等待客户端请求。读取请求数据github.com/uber/tchannel-go/connection.go #615func (c *Connection) readFrames(_ uint32) { headerBuf := make([]byte, FrameHeaderSize) …… for { …… //先读头部 if _, err := io.ReadFull(c.conn, headerBuf); err != nil { handleErr(err) return } frame := c.opts.FramePool.Get() if err := frame.ReadBody(headerBuf, c.conn); err != nil { handleErr(err) c.opts.FramePool.Release(frame) return } //handle frame …… }} 在服务端会监听握手成功的链接,如果客户端发送了请求,就会读取链接里面的数据。读取分2步:先读取Header Frame(16字节) Header Frame 的长度固定为16字节,这里面有stream Id 和 Data Frame的长度再读取Data Frame 从Header Frame获取到 Data Frame的长度后,根据长度从链接读取指定的字节长度,就获取到正确的Data Frame。有序写入——响应结果 服务端的有序写入和客户端的有序写入是一样的功能,只是所处的角色不一样,这里不再重复。客户端获取响应结果客户端获取响应结果我们把这个过程分成2个步骤: 1. 读取响应结果 2. 找到MessageExchange响应读取响应结果 客户端获取响应结果和服务端的读取请求数据也是相同的功能,这里不再重复。找到MessageExchange响应github.com/uber/tchannel-go/mex.go #429func (mexset *messageExchangeSet) forwardPeerFrame(frame *Frame) error { …… mexset.RLock() mex := mexset.exchanges[frame.Header.ID] mexset.RUnlock() …… //把帧交给MessageExchange.recvCh if err := mex.forwardPeerFrame(frame); err != nil { …… return err } return nil} 在客户端发起调用时介绍过,它会阻塞在MessageExchange.recvCh,当响应回来时会根据stream Id(上面的frame.Header.ID) 找到对应的MessageExchange,并把frame放入recvCh,完成响应。这一步就体现在上面的代码。结语 至此UBER的RPC框架TChannel————多路复用介绍完,感谢UBER团队的贡献,让我收益很多。 ...

December 30, 2018 · 3 min · jiezi

你真的了解RPC吗?

现微服务盛行,服务之间通信大概就两种方式Api和Rpc。下面两个列子来让你了解Api和Rpc的区别。列子一 文章的增删改查。Api 实现:Router::get(’/article/{id}’,‘ArticleController@get’);Router::post(’/article’,‘ArticleController@create’);Router::put(’/article/{id}’,‘ArticleController@edit’);Router::delete(’/article/{id}’,‘ArticleController@delete’);然后在控制器Article调用模型return Article::find($id)->toArray();Rpc 实现RpcServer::add(‘Article’);没错就一行代码列子二 计算器假如机器A上面一个计算器 Counter,以Rpc的方式提供给其他机器使用.计算器Counter代码class Counter{ private $i = 0; public function __construct($i = 0) { $this->i = $i; } // 加法 public function add($v) { $this->i += $v; return $this; } // 减法 public function sub($v) { $this->i -= $v; return $this; } // 乘法 public function mul($v) { $this->i *= $v; return $this; } // 除法 public function div($v) { $this->i /= $v; return $this; } // 获取结果 public function get() { return $this->i; }}Rpc 实现RpcServer::add(‘Counter’);Rpc客户端调用$c = new ClientCounter(10);echo $c->add(3)->mul(2)->sub(10)->div(5)->get();Api 实现:你觉得 Api 应该怎么实现?以上代码是我在设计 one框架的一些思考?如你喜欢请star https://github.com/lizhichao/one如其他观点,欢迎留言讨论。 ...

December 24, 2018 · 1 min · jiezi

Tars基础框架手动搭建——填掉官方 Guide 的坑

背景Tars 简介腾讯 Tars 是腾讯内部使用的 TAF(Tencent Application Framework)的对外开源版,去掉了许多冗杂多余的部分。该框架集开发、运维、微服务、RPC 等为一体。对程序员而言,这就是一个能够快速搭建整个微服务体系的开发框架。这个框架支持基于 C++、Node.js、PHP、Java 等语言开发,最新版本已经支持后台开发语言新贵——Go。安装任务Tars 实际的应用场景是多机器、多节点部署的,不过从实验验证的角度,我做的只是在单一一台机器上,实现 Tars 管理平台部署,以及任务的发布。由于 Tars 版本一直在迭代,为防止文章过时误导后来者,本文以 f3ef251013 节点为准,请读者 follow 最新的 Tars 版本来。官方安装文档在这里。然而,正如很多新的开源项目一样,代码很丰满,文档很骨感。一步一步跟着官方文档走的话,是有可能无法一次走通的……本文跟随文档的安装脉络,进行了重新梳理,为读者呈现一个尽量不挖坑的搭建过程。请注意,“Tars” 这个概念,除了其基础框架之外,还包含开发工具(如 TarsCpp、TarsJava 等等)、协议等等许多内容。不过本文主要讲述 Tars 基础框架,因此下文所提及的 “基础框架”,如无特别说明,都专指 “Tars 基础框架”。本文解决的问题前文提及,直接 follow 官方文档,是无法完成服务部署的。因为过程中遇到很多坑,每一个坑都导致基础框架无法搭建,或者是搭建后无法发布服务。笔者根据文档搭建过程中遇到的坑有:部分操作需要 sudo——文档中有些温馨地提醒了 sudo,但是另外一些未提醒数据库采用 MySQL 5.6 版,但部分说明不适用于 MariaDB说明文字与配图不完全一致不同代码处的数据库用户名和密码不统一启动所需的数据库表信息有缺失环境准备系统准备部署实验需要准备至少一台 Linux 机器。这可以是一台本地的实体机或虚拟机,也可以是一台云主机。由于部署过程中,需要编译 Tars 框架以及 MySQL 代码(如果系统里没装 Oracle 的 MySQL 的话),强烈建议系统至少要求有 4GB 的内存!系统建议采用 CentOS 或 Ubuntu。本文采用 Ubuntu 来安装,但 CentOS 差别不大,读者可以参考执行。此外,笔者不采用 root 账户,只有在需要 root 权限的时候才使用 sudo 操作。像笔者这样只有 1 核 1GB 云主机的,还需要在本地额外准备一台 Linux 虚拟机用于编译,再将编译出来的目标文件转移到云主机上。软件准备使用 Ubuntu,基础框架和 C++ 开发环境需要以下开发组件:gcc g++(CentOS 则是 gcc-c++) flex bison make cmake perl gcc zlibc gzip git libncurses5-devprotobuf-c-compiler protobuf-compiler libprotobuf-dev libprotobuf-c-dev libprotoc-devlibmariadb-client-lgpl-dev mariadb-client mariadb-server此外,还需要手动安装的软件或库有:mysql-server node.js(包括 npm)TarsCloud/TarsFramework TarsCloud/TarsWeb TarsClous/TarsCpp这些手动安装的软件会在后问说明。MySQL 问题官方文档使用的 MySQL 版本是 5.6。但是这有两个问题:Oracle 维护的开源 MySQL 已经发展到 5.7 和 8.0 版了,Tars 是否向后兼容?最新的 CentOS 和 Ubuntu 的软件包中已移除 MySQL,以 MariaDB 取代,Tars 是否支持?本人的答案是:从网上的资料看,基础框架支持 5.7,但需要修改 cmake 的选项,麻烦点;此外,在交流群中也有反馈 5.7 版有问题,但没有深入了解。支持,详见后文操作。笔者的方案是:使用 mariadb 作为数据库使用 libmysqlclient.a 作为 TarsCpp 开发环境创建时链接的库(基础框架编译时强制链接静态库)使用 libmariadb.so 作为实际应用开发时用的库备份安装之前,强烈建议不熟悉 Tars 基础框架的读者先给自己的机器做下备份,比如打个快照或者做个镜像之类的,这样如果后面部署失败了,也便于回滚系统,而不是一个一个抓虫。安装支持软件笔者所使用的用户名是 ubuntu,后文会有一些 shell 命令中采用了这个用户名,请读者留意替换成为自己的用户名。MySQL安装路径准备默认下载的 Tars 基础框架需要链接 MySQL 的静态库 libmysqlclient.a,此外对库所在的位置也写死在了 Makefile 中。因此我们需要为 Tars 基础框架准备环境。首先创建供基础框架链接的路径:$ sudo mkdir -p /usr/local/mysql$ sudo chown ubuntu:ubuntu /usr/local/mysql然后需要寻找一下 mariadb 的动态库位置和头文件位置:$ sudo find / -name ‘mariadb_com.h’ 2>/dev/null$ sudo find / -name ’libmariadbclient*’ 2>/dev/null笔者的环境中,两者分别在 /usr/include/mariadb 和 /usr/lib/x86_64-linux-gnu 下,那么我们就创建两个软链接:$ ln -s /usr/include/mariadb /usr/local/mysql/include$ ln -s /usr/lib/x86_64-linux-gnu /usr/local/mysql/lib再创建一个目录给 MySQL 实际安装用:$ mkdir /home/ubuntu/mysql-5.6 # /home/ubuntu 是笔者系统的家目录,读者请注意替换。以下同理。编译安装直接从 GitHub 上 clone MySQL 的工程代码后,选择合适的版本:$ mkdir -p ~/github.com/mysql/mysql-server$ cd ~/github.com/mysql/mysql-server$ git clone https://github.com/mysql/mysql-server.git ./$ git checkout -b 5.6 origin/5.6 # 切换到 5.6 版配置、编译、安装:$ cmake . -DCMAKE_INSTALL_PREFIX=/home/ubuntu/mysql-5.6 -DWITH_INNOBASE_STORAGE_ENGINE=1 -DMYSQL_USER=mysql -DDEFAULT_CHARSET=utf8 -DDEFAULT_COLLATION=utf8_general_ci -DDOWNLOAD_BOOST=1 -DWITH_BOOST=/home/ubuntu/others/boost$ make && make install通过之后,我们还需要将 mysql 的静态库复制(或者链接)到为 Tars 基础框架准备的目录下:$ ln -s /home/ubuntu/mysql-5.6/libmysqlclient.a /usr/local/mysql/lib/Node.jsTars 管理平台是使用 node.js 开发的,因此需要安装 nvm。对不熟悉 node 的读者,这里也简单列下安装方式(不建议使用 root 账户操作):$ wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash$ source ~/.bashrc$ nvm install v8.11.3$ npm install -g pm2 –registry=https://registry.npm.taobao.orgTars 基础框架首先,要下载 Tars 的基础框架代码:$ mkdir -p /homes/ubuntu/github.com/TarsCloud/TarsFramework$ cd /homes/ubuntu/github.com/TarsCloud/TarsFramework$ git clone https://github.com/TarsCloud/TarsFramework.git ./编译之:$ cd build$ chmod +x build.sh$ ./build.sh prepare$ ./build.sh all如果编译失败(一般是缺某些库或组件),建议在解决问题后,清掉 TarsFramework 下的所有文件,重新 clone 后再编译。安装的过程复杂一点,安装路径是固定的(虽然可以在工程文件中调整,为统一起见,不建议修改):$ sudo mkdir -p /usr/local/tars$ sudo chown ubuntu:ubuntu /usr/local/tars$ cd /homes/ubuntu/github.com/TarsCloud/TarsFramework/build$ ./build.sh install准备 Tars 数据库Tars 使用用户名 tars、密码 tars2015 的组合,操作 MySQL 数据库。高级玩法自然是修改 Tars 中的用户名/密码组合。这里为了简单起见,干脆就直接采用默认的就好了。不过后文会介绍如何使用自定义的用户名密码。笔者做验证时的 MySQL 与 Tars 部署在同一台机器上,IP 地址是 172.19.16.13。实际部署中,读者请注意换成实际地址。添加相关账户的命令如下:$ mysql -u root -p进入 mysql 命令行后:MariaDB [(none)]> grant all privileges on . to tars@localhost identified by ’tars2015’;MariaDB [(none)]> flush privileges;MariaDB [(none)]> exit;官方文档说的配置备份什么的,就不用关心了。使用 Tars 的系统基本是部署在云上的,大都会用云服务商提供的自带主备的数据库服务(推荐腾讯云 TDSQL 哦,金融级 DB,但是价格和普通 DB 一个等级)。接下来,我们通过执行 Tars 基础框架的 sql 脚本来创建相应的数据库结构:$ cd /homes/ubuntu/github.com/TarsCloud/TarsFramework/sql/$ sed -i “s/192.168.2.131/172.19.16.13/g” grep 192.168.2.131 -rl ./*$ sed -i “s/db.tars.com/172.19.16.13/g” grep db.tars.com -rl ./*$ sed -i “s/10.120.129.226/172.19.16.13/g” grep 10.120.129.226 -rl ./*$ chmod +x exec-sql.sh$ ./exec-sql.sh其中,172.19.16.13 是笔者的 DB 的 IP 地址,读者实际部署时请注意修改。这些命令也给我们一个启发:数据库地址允许采用域名。TIPS-1如果读者不使用 tars/tars2015 的用户名/密码组合来操作数据库,那么可以自行修改上述的 sql 脚本:$ cd /homes/ubuntu/github.com/TarsCloud/TarsFramework/sql/$ git status .On branch masterYour branch is up-to-date with ‘origin/master’.Changes not staged for commit: (use “git add <file>…” to update what will be committed) (use “git checkout – <file>…” to discard changes in working directory) modified: db_tars.sql modified: tarsconfig.sql modified: tarslog.sql modified: tarsnotify.sql modified: tarspatch.sql modified: tarsproperty.sql modified: tarsqueryproperty.sql modified: tarsquerystat.sql modified: tarsstat.sql可以看到被 sed 语句修改了的文件。读者可以在这些文件中找到用户名和密码配置项进行修改后,再执行。TIPS-2MariaDB server 安装之后默认监听 127.0.0.1 地址,但官方文档明确说明 DB 的 IP 地址不能使用 127.0.0.1。读者需要修改 MariaDB 的配置文件中监听地址的选项,否则后续 Tars 基础框架启动后,会遇到没有权限访问数据库的错误。部署 Tars 基础框架Tars 框架核心服务Tars 核心基础框架指的是 tarsAdminRegistry, tarsregistry, tarsnode, tarsconfig, tarspatch 五个。前面我们其实已经编译好了,我们可以先把这些服务打包:$ cd /homes/ubuntu/github.com/TarsCloud/TarsFramework/build$ make framework-tar这会在 build 目录下生成 framework.tgz 文件。接下来我们需要做一些操作:$ sudo mkdir -p /data/log/tars$ sudo mkdir -p /home/tarsproto$ sudo mkdir -p /usr/local/app/tars$ sudo chown -R ubuntu:ubuntu /usr/local/app /data/log/tars /home/tarsproto$ mv /homes/ubuntu/github.com/TarsCloud/TarsFramework/build/framework.tgz /usr/local/app/tars$ cd /usr/local/app/tars$ tar zxvf framework.tgz这会在 /usr/local/app/tars/ 下面生成前述几个核心基础框架组件对应的文件夹。此外,还有 tars_install.sh 和 tarsnode_install.sh 两个脚本。不过我们得先配置一下——五个核心基础框架组件的目录下均有 conf 文件夹,可以看到各个组件的配置文件,比如 tars.tarsregistry.config.conf。对操作已经很熟悉了的读者可以手动修改这些配置。不过也可以简单点地进行以下修改:$ cd /usr/local/app/tars$ sed -i “s/192.168.2.131/172.19.16.13/g” grep 192.168.2.131 -rl ./*$ sed -i “s/db.tars.com/172.19.16.13/g” grep db.tars.com -rl ./*$ sed -i “s/registry.tars.com/172.19.16.13/g” grep registry.tars.com -rl ./*$ sed -i “s/web.tars.com/172.19.16.13/g” grep web.tars.com -rl ./*四个 sed 命令修改的地址,对应的是:本机地址,不能写 127.0.0.1;前述数据库的地址;tarsregistry 的 部署地址,可以不是本机;web.tars.com 是 rsync 服务和 web 管理平台的部署地址。修改了 IP 地址之后,还需要检查 tars 访问数据库的用户名和密码。这里我们最好是手动 vim 去改,因为几个文件的书写格式不完全一致:$ cd /usr/local/app/tars$ grep dbpass -rl ./检查搜索出来的 conf 文件中的 dbuser 和 dbpass 字段。最后就是启动核心框架服务和 rsync(好艰难):$ cd /usr/local/app/tars$ chmod +x tars_install.sh$ ./tars_install.sh$ sudo ./tarspatch/util/init.sh$ chmod +x tarsnode_install.sh$ ./tarsnode_install.sh然后我们可以在 croncab 中配置核心基础框架的监控项: * * * * /usr/local/app/tars/tarsnode/util/monitor.sh这样一来,五个框架核心服务就启动起来了。Tars web 管理平台Tars web 管理系统在另一个 Git repo 中:$ mkdir -p /homes/ubuntu/github.com/TarsCloud/TarsWeb$ cd /homes/ubuntu/github.com/TarsCloud/TarsWeb$ git clone https://github.com/TarsCloud/TarsWeb.git ./$ sed -i ’s/db.tars.com/172.19.16.13/g’ config/webConf.js$ sed -i ’s/registry.tars.com/172.19.16.13/g’ config/tars.conf其中 172.19.16.13 是笔者机器的 IP,读者请注意Tars web 是用 node.js 编写的,不需要编译。管理平台默认监听在 3000 端口上,可在 config/webConf.js 中修改 port 参数的值。配置了端口之后,就可以启动 Tars web 管理平台了:$ cd /homes/ubuntu/github.com/TarsCloud/TarsWeb$ npm install –registry=https://registry.npm.taobao.org$ npm run prd可以查看 TarsWeb 目录下的 package.json 文件可以看到更多的信息——毕竟并不是每个人都懂得 node.js 开发。Tars 框架基础服务检查核心服务状态Tars web 管理平台启动后,如果按照默认设置的话,平台会在 3000 端口建立 http 服务。使用浏览器访问,可以看到如下界面:这三个组件,就是前文所述的 “tars 框架核心服务” ,如果按照前述逻辑部署后,在 web 页面就可以看到的。可以依次点开这三个服务,确保服务的状态都如下图所示:如果服务的状态不对,那么可能是前面哪一步操作不恰当。可以查找 log 来定位(log 文件的路径参见后文)。部署其他基础服务剩下的几个基础框架服务就需要进行手动部署。但部署的方法其实还是蛮简单的,这里挑一个出来细讲,其他的类似。首先,我们需要把其他的基础框架打包出来:$ cd /homes/ubuntu/github.com/TarsCloud/TarsFramework/build$ make tarsstat-tar$ make tarsnotify-tar$ make tarsproperty-tar$ make tarslog-tar$ make tarsquerystat-tar$ make tarsqueryproperty-tar这样会在 /homes/ubuntu/github.com/TarsCloud/TarsFramework/build 目录下分别生成上述六个组件的 tgz 文件。接下来我们以 tarsstat 为例说明部署方法:创建服务点击 web 主页的 “运维管理” 选项卡,进入服务界面:各参数按照如下填写:应用:填 “tars”服务名称:填 “tarsstat”服务类型:在下拉菜单中选 “tars_cpp”模板:在下拉菜单中选 “tars.tarsstat”节点:选择本机的出口地址,像笔者的情况,就是 172.19.16.13SET:现阶段不用填,这是 tars 的进阶功能OBJ:填 “StatObj”OBJ 绑定地址 和 端口:可以手动调整,也可以点 “获取端口” 按钮自动分配端口类型:TCP协议:TARS其他默认即可。填好后,点 “确定” 即可部署。注意,此时的 “部署”,只是在 tars 内注册了一个服务(占了个坑),但这个服务还没有上线提供可用的功能。发布服务部署了服务后,需要刷新页面,这样就可以看到左边 tars 的服务列表多了一项:“tarsstat”现在,我们需要真正地发布这个服务了。点击 “tarsstat” 选项卡,可以看到上面有六个功能选项。点击 “发布管理”,视图如下:勾选节点,然后点击 “发布选中节点”,界面如下:这个时候点 “发布版本” 的下拉菜单,是没有内容的。我们需要点 “上传发布包”,在新打开的窗口,选择上一步 “创建服务” 时打包的 tarsstat.tgz 文件上传即可。上传成功后,我们再在 “发布版本” 下拉菜单中选择刚刚上传的包,然后点 “发布”。稍等一会后,我们只要看到 tarsstat 的状态变成如下,就是发布成功了:其他各个服务的特殊参数除了不需要额外配置数据库之外,接下来的五个服务的配置发布流程基本一致。但也有以下一些不同,读者在发布剩余服务的时候请注意修改:各服务的名称不同,请相应地修改;各服务的服务类型均为 “tars_cpp”,但是模板不同——每一个服务均有其对应的专用模板,比如 tarsconfig 对应 tars.tarsconfig 模板;tarsquerystat 和 tarsqueryproperty 的 “协议” 应选 “非TARS”,其他服务为 “TARS”;各服务对应的 Obj 名称如下:tarsnotify: NotifyObjtarsproperty: PropertyObjtarslog: LogObjtarsquerystat: NoTarsObj(非 TARS 协议)tarsqueryproperty: NoTarsObj(非 TARS 协议)此外,以下这一步是 tarsstat 特有的步骤:在发布服务之前,需要进入数据库进行以下操作,防止 tarsstat 启动失败:MariaDB [(none)]> use db_tars;MariaDB [db_tars]> CREATE TABLE t_server_notifys ( id int(11) NOT NULL AUTO_INCREMENT, application varchar(128) DEFAULT ‘’, server_name varchar(128) DEFAULT NULL, container_name varchar(128) DEFAULT ’’ , node_name varchar(128) NOT NULL DEFAULT ‘’, set_name varchar(16) DEFAULT NULL, set_area varchar(16) DEFAULT NULL, set_group varchar(16) DEFAULT NULL, server_id varchar(100) DEFAULT NULL, thread_id varchar(20) DEFAULT NULL, command varchar(50) DEFAULT NULL, result text, notifytime datetime DEFAULT NULL, PRIMARY KEY (id), KEY index_name (server_name), KEY servernoticetime_i_1 (notifytime), KEY indx_1_server_id (server_id), KEY query_index (application,server_name,node_name,set_name,set_area,set_group) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;MariaDB [db_tars]> exit;所有服务均发布完成并状态正确之后,Tars 基础框架就部署完成啦,恭喜你!日志查询如果部署 Tars 框架服务过程中遇到什么错误,可以查阅的 log 在以下路径:/usr/local/app/TarsWeb/log/usr/local/app/tars/app_log/tars关于自动部署Tars 其实还提供了一套比较简易的快速部署脚本。那个脚本我没有尝试过,但据说也是有一些坑……这里我推荐 maq128 同学 的文章:tars小白安装必成手册,分别讲述了快速部署、手工部署、docker 部署的内容。另外,如果有问题,读者也可以加入 Tars 官方交流群,不保证所有问题都能够精准回答,但是群里不少大神还是给了我不少启发。群号参见 Tars 官方文档。下一步研究按照官方的建议,Tars 的所有基础服务都需要至少进行灾备部署,但是部署方式如何实现,并没有明确的说明或者建议。这是笔者后续准备实验的。参考资料pm2 日志加时间戳mysql/mariadb centos7 修改root用户密码及配置参数tars 部署过程-youz1976的专栏腾讯Tars环境搭建中遇到的坑tars各个自带的服务都是做什么的,又是怎么保障他们的可靠性的?请问t_server_notifys建表语句?TARS 用户体系模块+资源模块使用指引tars小白安装必成手册本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。本文地址:https://segmentfault.com/a/1190000017482106。原文发布于:https://cloud.tencent.com/developer/article/1372998,也是本人的博客。 ...

December 22, 2018 · 4 min · jiezi

XXL-RPC v1.3.1,分布式服务框架

Release Notes1、负载均衡/软负载:提供丰富的负载均衡策略,包括:轮询、随机、LRU、LFU、一致性HASH等;2、服务发现注册逻辑优化:支持批量注册、摘除,升级 xxl-registry 至 v1.0.1;3、新增jfinal类型示例项目 “xxl-rpc-sample-jfinal” 支持jfinal项目快速接入分布式RPC服务功能;高兼容性,原则上支持任务框架,甚至main方法直接运行;4、TCP通讯方案Server端Channel线程优化(线程参数=60/300/1000),避免IO线程阻塞于业务;5、TCP通讯方案Client端Channel线程优化(线程参数=10/100/1000),避免IO线程阻塞于callback业务;6、TCP通讯方案Client初始化逻辑优化;7、TCP长连销毁逻辑优化;8、底层Log整理,RPC报错时打印完整Log,包括请求地址,请求参数等;9、Server端销毁逻辑优化;10、static代码块优化,进行组件无状态优化:response factory等;迁移到invoke factory上来;11、升级多项pom依赖至较新稳定版本;简介XXL-RPC 是一个分布式服务框架,提供稳定高性能的RPC远程服务调用功能。拥有"高性能、分布式、注册中心、负载均衡、服务治理"等特性。现已开放源代码,开箱即用。特性1、快速接入:接入步骤非常简洁,两分钟即可上手;2、服务透明:系统完整的封装了底层通信细节,开发时调用远程服务就像调用本地服务,在提供远程调用能力时不损失本地调用的语义简洁性;3、多调用方案:支持 SYNC、ONEWAY、FUTURE、CALLBACK 等方案;4、多通讯方案:支持 TCP 和 HTTP 两种通讯方式进行服务调用;其中 TCP 提供可选方案 NETTY 或 MINA ,HTTP 提供可选方案 Jetty;5、多序列化方案:支持 HESSIAN、HESSIAN1、PROTOSTUFF、JSON 等方案;6、负载均衡/软负载:提供丰富的负载均衡策略,包括:轮询、随机、LRU、LFU、一致性HASH等;7、注册中心:可选组件,支持服务注册并动态发现;可选择不启用,直接指定服务提供方机器地址通讯;选择启用时,内置可选方案:“XXL-REGISTRY 轻量级注册中心”(推荐)、“ZK注册中心”、“Local注册中心”等;8、服务治理:提供服务治理中心,可在线管理注册的服务信息,如服务锁定、禁用等;9、服务监控:可在线监控服务调用统计信息以及服务健康状况等(计划中);10、容错:服务提供方集群注册时,某个服务节点不可用时将会自动摘除,同时消费方将会移除失效节点将流量分发到其余节点,提高系统容错能力。11、解决1+1问题:传统分布式通讯一般通过nginx或f5做集群服务的流量负载均衡,每次请求在到达目标服务机器之前都需要经过负载均衡机器,即1+1,这将会把流量放大一倍。而XXL-RPC将会从消费方直达服务提供方,每次请求直达目标机器,从而可以避免上述问题;12、高兼容性:得益于优良的兼容性与模块化设计,不限制外部框架;除 spring/springboot 环境之外,理论上支持运行在任何Java代码中,甚至main方法直接启动运行;文档地址中文文档技术交流社区交流

December 21, 2018 · 1 min · jiezi

简易RPC框架:序列化机制

概述在上一篇文章《简易RPC框架:基于 netty 的协议编解码》中谈到对于协议的 decode 和 encode,在谈 decode 之前,必须先要知道 encode 的过程是什么,它把什么东西转化成了二进制协议。由于我们还未谈到具体的 RPC 调用机制,因此暂且认为 encode 就是把一个包含了调用信息的 Java 对象,从 client 经过序列化,变成一串二进制流,发送到了 server 端。这里需要明确的是,encode 的职责是拼协议,它不负责序列化,同样,decode 只是把整个二进制报文分割,哪部分是报文头,哪部分是报文体,诚然,报文体就是被序列化成二进制流的一个 Java 对象。对于调用方来说,先将调用信息封装成一个 Java 对象,经过序列化后形成二进制流,再经过 encode 阶段,拼接成一个完整的遵守我们定好的协议的报文。对于被调用方来说,则是收取完整的报文,在 decode 阶段将报文中的报文头,报文体分割出来,在序列化阶段将报文体反序列化为一个 Java 对象,从而获得调用信息。本文探讨序列化机制。基于 netty handler由于这个 RPC 框架基于 netty 实现,因此序列化机制其实体现在了 netty 的 pipeline 上的 handler 上。例如对于调用方,它需要在 pipeline 上加上一个 序列化 encode handler,用来序列化发出去的请求,同时需要加上一个反序列化的 decode handler, 以便反序列化调用结果。如下所示: protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new ProtocolEncoder()) .addLast(new ProtocolDecoder()) .addLast(new SerializationHandler(serialization)) .addLast(new DeserializationHandler(serialization)); }其中的 SerializationHandler 和 DeserializationHandler 就是上文提到的序列化 encode handler 和反序列化 decode handler。同样,对于被调用方来说,它也需要这两个handler,与调用方的 handler 编排顺序一致。其中,serialization 这个参数的对象代表具体的序列化机制策略。序列化机制上文中,SerializationHandler 和 DeserializationHandler 这两个对象都需要一个 serialization 对象作为参数,它是这么定义的:private ISerialization serialization = SerializationFactory.getSerialization(ServerDefaults.DEFAULT_SERIALIZATION_TYPE);采用工厂模式来创建具体的序列化机制:/** * 序列化工厂 * * @author beanlam * @version 1.0 /public class SerializationFactory { private SerializationFactory() { } public static ISerialization getSerialization(SerializationType type) { if (type == SerializationType.JDK) { return new JdkSerialization(); } return new HessianSerialization(); }}这里暂时只支持 JDK 原生序列化 和 基于 Hessian 的序列化机制,日后若有其他效率更高更适合的序列化机制,则可以在工厂类中进行添加。这里的 hessian 序列化是从 dubbo 中剥离出来的一块代码,感兴趣可以从 dubbo 的源码中的 com.caucho.hessian 包中获得。以 HessianSerialization 为例:/* * @author beanlam * @version 1.0 */public class HessianSerialization implements ISerialization { private ISerializer serializer = new HessianSerializer(); private IDeserializer deserializer = new HessianDeserializer(); @Override public ISerializer getSerializer() { return serializer; } @Override public IDeserializer getDeserializer() { return deserializer; } @Override public boolean accept(Class<?> clazz) { return Serializable.class.isAssignableFrom(clazz); }}根据 Hessian 的 API, 分别返回一个 hessian 的序列化器和反序列化器即可。 ...

November 11, 2018 · 1 min · jiezi

简易RPC框架:基于 netty 的协议编解码

概述在《简易RPC框架:需求与设计》这篇文章中已经给出了协议的具体细节,协议类型为二进制协议,如下: ———————————————————————— | magic (2bytes) | version (1byte) | type (1byte) | reserved (7bits) | ———————————————————————— | status (1byte) | id (8bytes) | body length (4bytes) | ———————————————————————— | | | body ($body_length bytes) | | | ————————————————————————协议的解码我们称为 decode,编码我们成为 encode,下文我们将直接使用 decode 和 encode 术语。decode 的本质就是讲接收到的一串二进制报文,转化为具体的消息对象,在 Java 中,就是将这串二进制报文所包含的信息,用某种类型的对象存储起来。encode 则是将存储了信息的对象,转化为具有相同含义的一串二进制报文,然后网络收发模块再将报文发出去。无论是 rpc 客户端还是服务端,都需要有一个 decode 和 encode 的逻辑。消息类型rpc 客户端与服务端之间的通信,需要通过发送不同类型的消息来实现,例如:client 向 server 端发送的消息,可能是请求消息,可能是心跳消息,可能是认证消息,而 server 向 client 发送的消息,一般就是响应消息。利用 Java 中的枚举类型,可以将消息类型进行如下定义:/** * 消息类型 * * @author beanlam * @version 1.0 / public enum MessageType { REQUEST((byte) 0x01), HEARTBEAT((byte) 0x02), CHECKIN((byte) 0x03), RESPONSE( (byte) 0x04), UNKNOWN((byte) 0xFF); private byte code; MessageType(byte code) { this.code = code; } public static MessageType valueOf(byte code) { for (MessageType instance : values()) { if (instance.code == code) { return instance; } } return UNKNOWN; } public byte getCode() { return code; }}在这个类中设计了 valueOf 方法,方便进行具体的 byte 字节与具体的消息枚举类型之间的映射和转换。调用状态设计client 主动发起的一次 rpc 调用,要么成功,要么失败,server 端有责任告知 client 此次调用的结果,client 也有责任去感知调用失败的原因,因为不一定是 server 端造成的失败,可能是因为 client 端在对消息进行预处理的时候,例如序列化,就已经出错了,这种错误也应该作为一次调用的调用结果返回给 client 调用者。因此引入一个调用状态,与消息类型一样,它也借助了 Java 语言里的枚举类型来实现,并实现了方便的 valueOf 方法:/* * 调用状态 * * @author beanlam * @version 1.0 /public enum InvocationStatus { OK((byte) 0x01), CLIENT_TIMEOUT((byte) 0x02), SERVER_TIMEOUT( (byte) 0x03), BAD_REQUEST((byte) 0x04), BAD_RESPONSE( (byte) 0x05), SERVICE_NOT_FOUND((byte) 0x06), SERVER_SERIALIZATION_ERROR( (byte) 0x07), CLIENT_SERIALIZATION_ERROR((byte) 0x08), CLIENT_CANCELED( (byte) 0x09), SERVER_BUSY((byte) 0x0A), CLIENT_BUSY( (byte) 0x0B), SERIALIZATION_ERROR((byte) 0x0C), INTERNAL_ERROR( (byte) 0x0D), SERVER_METHOD_INVOKE_ERROR((byte) 0x0E), UNKNOWN((byte) 0xFF); private byte code; InvocationStatus(byte code) { this.code = code; } public static InvocationStatus valueOf(byte code) { for (InvocationStatus instance : values()) { if (code == instance.code) { return instance; } } return UNKNOWN; } public byte getCode() { return code; }}消息实体设计我们将 client 往 server 端发送的统一称为 rpc 请求消息,一个请求对应着一个响应,因此在 client 和 server 端间流动的信息大体上其实就只有两种,即要么是请求,要么是响应。我们将会定义两个类,分别是 RpcRequest 和 RpcResponse 来代表请求消息和响应消息。另外由于无论是请求消息还是响应消息,它们都有一些共同的属性,例如说“调用上下文ID”,或者消息类型。因此会再定义一个 RpcMessage 类,作为父类。RpcMessage/* * rpc消息 * * @author beanlam * @version 1.0 /public class RpcMessage { private MessageType type; private long contextId; private Object data; public long getContextId() { return this.contextId; } public void setContextId(long id) { this.contextId = id; } public Object getData() { return this.data; } public void setData(Object data) { this.data = data; } public void setType(byte code) { this.type = MessageType.valueOf(code); } public MessageType getType() { return this.type; } public void setType(MessageType type) { this.type = type; } @Override public String toString() { return “[messageType=” + type.name() + “, contextId=” + contextId + “, data=” + data + “]”; }}RpcRequestimport java.util.concurrent.atomic.AtomicLong;/* * rpc请求消息 * * @author beanlam * @version 1.0 /public class RpcRequest extends RpcMessage { private static final AtomicLong ID_GENERATOR = new AtomicLong(0); public RpcRequest() { this(ID_GENERATOR.incrementAndGet()); } public RpcRequest(long contextId) { setContextId(contextId); setType(MessageType.REQUEST); }}RpcResponse/* * * rpc响应消息 * * @author beanlam * @version 1.0 /public class RpcResponse extends RpcMessage { private InvocationStatus status = InvocationStatus.OK; public RpcResponse(long contextId) { setContextId(contextId); setType(MessageType.RESPONSE); } public InvocationStatus getStatus() { return this.status; } public void setStatus(InvocationStatus status) { this.status = status; } @Override public String toString() { return “RpcResponse[contextId=” + getContextId() + “, status=” + status.name() + “]”; }}netty 编解码介绍netty 是一个 NIO 框架,应该这么说,netty 是一个有良好设计思想的 NIO 框架。一个 NIO 框架必备的要素就是 reactor 线程模型,目前有一些比较优秀而且开源的小型 NIO 框架,例如分库分表中间件 mycat 实现的一个简易 NIO 框架,可以在这里看到。netty 的主要特点有:微内核设计、责任链模式的业务逻辑处理、内存和资源泄露的检测等。其中编解码在 netty 中,都被设计成责任链上的一个一个 Handler。decode 对于 netty 来说,它提供了 ByteToMessageDecoder,它也提供了 MessageToByteEncoder。借助 netty 来实现协议编解码,实际上就是去在这两个handler里面实现编解码的逻辑。decode在实现 decode 逻辑时需要注意的一个问题是,由于二进制报文是在网络上发送的,因此一个完整的报文可能经过多个分组来发送的,什么意思呢,就是当有报文进来后,要确认报文是否完整,decode逻辑代码不能假设收到的报文就是一个完整报文,一般称这为“TCP半包问题”。同样,报文是连着报文发送的,意味着decode代码逻辑还要负责在一长串二进制序列中,分割出一个一个独立的报文,这称之为“TCP粘包问题”。netty 本身有提供一些方便的 decoder handler 来处理 TCP 半包和粘包的问题。不过一般情况下我们不会直接去用它,因为我们的协议比较简单,自己在代码里处理一下就可以了。完整的 decode 代码逻辑如下所示:import cn.com.agree.ats.rpc.message.;import cn.com.agree.ats.util.logfacade.AbstractPuppetLoggerFactory;import cn.com.agree.ats.util.logfacade.IPuppetLogger;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.ByteToMessageDecoder;import java.util.List;/** * 协议解码器 * * @author beanlam * @version 1.0 /public class ProtocolDecoder extends ByteToMessageDecoder { private static final IPuppetLogger logger = AbstractPuppetLoggerFactory .getInstance(ProtocolDecoder.class); private boolean magicChecked = false; @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> list) throws Exception { if (!magicChecked) { if (in.readableBytes() < ProtocolMetaData.MAGIC_LENGTH_IN_BYTES) { return; } magicChecked = true; if (!(in.getShort(in.readerIndex()) == ProtocolMetaData.MAGIC)) { logger.warn( “illegal data received without correct magic number, channel will be close”); ctx.close(); magicChecked = false; //this line of code makes no any sense, but it’s good for a warning return; } } if (in.readableBytes() < ProtocolMetaData.HEADER_LENGTH_IN_BYTES) { return; } int bodyLength = in .getInt(in.readerIndex() + ProtocolMetaData.BODY_LENGTH_OFFSET); if (in.readableBytes() < bodyLength + ProtocolMetaData.HEADER_LENGTH_IN_BYTES) { return; } magicChecked = false;// so far the whole packet was received in.readShort(); // skip the magic in.readByte(); // dont care about the protocol version so far byte type = in.readByte(); byte status = in.readByte(); long contextId = in.readLong(); byte[] body = new byte[in.readInt()]; in.readBytes(body); RpcMessage message = null; MessageType messageType = MessageType.valueOf(type); if (messageType == MessageType.RESPONSE) { message = new RpcResponse(contextId); ((RpcResponse) message).setStatus(InvocationStatus.valueOf(status)); } else { message = new RpcRequest(contextId); } message.setType(messageType); message.setData(body); list.add(message); }}可以看到,我们解决半包问题的时候,是判断有没有收到我们期望收到的报文,如果没有,直接在 decode 方法里面 return,等有更多的报文被收到的时候,netty 会自动帮我们调起 decode 方法。而我们解决粘包问题的思路也很清晰,那就是一次只处理一个报文,不去动后面的报文内容。还需要注意的是,在 netty 中,对于 ByteBuf 的 get 是不会消费掉报文的,而 read 是会消费掉报文的。当不确定报文是否收完整的时候,我们都是用 get开头的方法去试探性地验证报文是否接收完全,当确定报文接收完全后,我们才用 read 开头的方法去消费这段报文。encode直接贴代码,参考前文提到的协议格式阅读以下代码:/* * * 协议编码器 * * @author beanlam * @version 1.0 */public class ProtocolEncoder extends MessageToByteEncoder<RpcMessage> { @Override protected void encode(ChannelHandlerContext ctx, RpcMessage rpcMessage, ByteBuf out) throws Exception { byte status; byte[] data = (byte[]) rpcMessage.getData(); if (rpcMessage instanceof RpcRequest) { RpcRequest request = (RpcRequest) rpcMessage; status = InvocationStatus.OK.getCode(); } else { RpcResponse response = (RpcResponse) rpcMessage; status = response.getStatus().getCode(); } out.writeShort(ProtocolMetaData.MAGIC); out.writeByte(ProtocolMetaData.VERSION); out.writeByte(rpcMessage.getType().getCode()); out.writeByte(status); out.writeLong(rpcMessage.getContextId()); out.writeInt(data.length); out.writeBytes(data); }} ...

November 6, 2018 · 4 min · jiezi

Thrift RPC 系列教程(1)——Thrift语言

Thrift不是严格意义上的编程语言,但是却胜过很多编程语言,充满了美感。基础数据类型Thrift 这门编程语言提供了如下几种基础的数据类型:bool: A boolean value (true or false)byte: An 8-bit signed integeri16: A 16-bit signed integeri32: A 32-bit signed integeri64: A 64-bit signed integerdouble: A 64-bit floating point numberstring: A text string encoded using UTF-8 encoding一般来说,我们也就常用那几种,就像在其他日常编程语言中一样。比如我,基本就是这『三板斧』:booli32 ( 现在逐步常用 i64 了,因为性能啥的,我基本不是第一时间关注的)doublestring复杂数据类型(容器)再让我们来看看,Thrift提供了哪些容器类型:list: An ordered list of elements. Translates to an STL vector, Java ArrayList, native arrays in scripting languages, etc.set: An unordered set of unique elements. Translates to an STL set, Java HashSet, set in Python, etc. Note: PHP does not support sets, so it is treated similar to a Listmap: A map of strictly unique keys to values. Translates to an STL map, Java HashMap, PHP associative array, Python/Ruby dictionary, etc. While defaults are provided, the type mappings are not explicitly fixed. Custom code generator directives have been added to allow substitution of custom types in various destination languages简直了C++ STL 一毛一样,命名都差不多。唯独 list 这种数据结构,其实是『动态数组』,单从名字上看,很容易让人联系到链表,这在其他的编程语言中,也有这个现象,比如Python 中的也叫做 list 。class,即struct稍微正常一点的语言,对 OOP 的支持,自然是必不可少的,我觉得,最好直接提供 class 这个关键字,尽量有清晰的语义。但是 Thrift 只有一个 struct,基本上和 C 的struct,一样,也是功能少得可怜,不过考虑到它仅仅是一个中间语言,自然是情有可原的。我们来看一下,一个写得好的 struct,应该如何定义,做到既清晰又完备的:struct Person { 1: required string name; // 必须字段,很明确 2: required i64 age; 3: optional string addr; // 可选字段 4: optional string defaultValue = “DEFAULT”; // 默认字段 5: string otherValue; // 不是很明确!}interface,即service在『面向接口编程』的原则下,『接口』是一个很重要的因素。有的人称之为函数,有的人称为方法,本文我们统称为『方法』。在Thrift中,定义接口是一件很简单的事情( 摘自官网的一个示例 ):// 接口, 还可以继承, 也许我们有时候可以搞个 『BaseService』 之类的,不过我很少用到。service Calculator extends shared.SharedService { // 正常方法,和C++这类传统语言,基本一模一样。 void ping(), i32 add(1:i32 num1, 2:i32 num2), i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch), // 特殊方法,基本很少用到了,在我有限的经历中,只使用过一次,读者没必要关注它 oneway void zip()}异常,即exception关于异常,在Thrift中就像定义 struct 一样,因为exception从概念上讲,也是一种class,所谓『万事万物皆对象』嘛。不过现在我们用『exception』这个关键字,也正好符合我前文所讲的,清晰的语义。让我们看看Thrift中的异常是如何定义的:exception InvalidOperation { 1: i32 whatOp, 2: string why}枚举枚举这个东西,真的是太重要了,和前面的exception类似,它也不过是一种class而已。不过Thrift中只支持枚举 int 值,比较遗憾,其实很多时候,对枚举的要求,我们是很丰富的,比如支持 枚举 string。Thrift中枚举如下:enum Operation { // 功能着实比较孱弱 ADD = 1, SUBTRACT = 2, MULTIPLY = 3, DIVIDE = 4}如果喜欢我的文章,请关注我的公众号:『浮生若梦的编程』。 也可以关注我的简书专栏:『浮生若梦的编程』。 或者加入我的知识星球,『浮生若梦的编程』,获取更多干货。 ...

November 6, 2018 · 2 min · jiezi