作者:孫浩 來源:https://xiaomi-info.github.io/2020/03/02/rpc-achieve/ 引言 本文主要論述的是“RPC 實現原理”,那麼首先明確一個問題什麼是 RPC 呢?RPC 是 Remote Procedure Call 的縮寫,即,遠程過程調用。RPC ...
作者:孫浩
來源:https://xiaomi-info.github.io/2020/03/02/rpc-achieve/
引言
本文主要論述的是“RPC 實現原理”,那麼首先明確一個問題什麼是 RPC 呢?RPC 是 Remote Procedure Call 的縮寫,即,遠程過程調用。RPC 是一個電腦通信協議。該協議允許運行於一臺電腦的程式調用另一臺電腦的子程式,而開發人員無需額外地為這個交互編程。
值得註意是,兩個或多個應用程式都分佈在不同的伺服器上,它們之間的調用都像是本地方法調用一樣。接下來我們便來分析一下一次 RPC 調用發生了些什麼?
一次基本的 RPC 調用會涉及到什麼?
現在業界內比較流行的一些 RPC 框架,例如 Dubbo 提供的是基於介面的遠程方法調用
,即客戶端只需要知道介面的定義即可調用遠程服務。在 Java 中介面並不能直接調用實例方法,必須通過其實現類對象來完成此操作,這意味著客戶端必須為這些介面生成代理對象
,對此 Java 提供了 Proxy
、InvocationHandler
生成動態代理的支持;生成了代理對象,那麼每個具體的發方法是怎麼調用的呢?jdk 動態代理生成的代理對象調用指定方法時實際會執行 InvocationHandler
中定義的 #invoke
方法,在該方法中完成遠程方法調用並獲取結果。
拋開客戶端,回過頭來看 RPC 是兩台電腦間的調用,實質上是兩台主機間的網路通信
,涉及到網路通信又必然會有序列化、反序列化
,編解碼
等一些必須要考慮的問題;同時實際上現在大多系統都是集群部署的,多台主機/容器對外提供相同的服務,如果集群的節點數量很大的話,那麼管理服務地址也將是一件十分繁瑣的事情,常見的做法是各個服務節點將自己的地址和提供的服務列表註冊到一個 註冊中心
,由 註冊中心
來統一管理服務列表;這樣的做法解決了一些問題同時為客戶端增加了一項新的工作——那就是服務發現
,通俗來說就是從註冊中心中找到遠程方法對應的服務列表並通過某種策略從中選取一個服務地址來完成網路通信。
聊了客戶端和 註冊中心
,另外一個重要的角色自然是服務端,服務端最重要的任務便是提供服務介面的真正實現併在某個埠上監聽網路請求,監聽到請求後從網路請求中獲取到對應的參數(比如服務介面、方法、請求參數等),再根據這些參數通過反射
的方式調用介面的真正實現獲取結果並將其寫入對應的響應流中。
綜上所述,一次基本的 RPC 調用流程大致如下:
基本實現
服務端(生產者)
- 服務介面
在 RPC 中,生產者和消費者有一個共同的服務介面 API。如下,定義一個 HelloService 介面。
/**
* @author 孫浩
* @Descrption 服務介面
***/
public interface HelloService {
String sayHello(String somebody);
}
- 服務實現
生產者要提供服務介面的實現,創建 HelloServiceImpl 實現類。
/**
* @author 孫浩
* @Descrption 服務實現
***/
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String somebody) {
return "hello " + somebody + "!";
}
}
- 服務註冊
本例使用 Spring 來管理 bean,採用自定義 xml 和解析器的方式來將服務實現類載入容器(當然也可以採用自定義註解的方式,此處不過多論述)並將服務介面信息註冊到註冊中心。
首先自定義xsd
,
<xsd:element name="service">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="beans:identifiedType">
<xsd:attribute name="interface" type="xsd:string" use="required"/>
<xsd:attribute name="timeout" type="xsd:int" use="required"/>
<xsd:attribute name="serverPort" type="xsd:int" use="required"/>
<xsd:attribute name="ref" type="xsd:string" use="required"/>
<xsd:attribute name="weight" type="xsd:int" use="optional"/>
<xsd:attribute name="workerThreads" type="xsd:int" use="optional"/>
<xsd:attribute name="appKey" type="xsd:string" use="required"/>
<xsd:attribute name="groupName" type="xsd:string" use="optional"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
分別指定 schema 和 xmd,schema 和對應 handler 的映射:
schema
http\://www.storm.com/schema/storm-service.xsd=META-INF/storm-service.xsd
http\://www.storm.com/schema/storm-reference.xsd=META-INF/storm-reference.xsd
handler
http\://www.storm.com/schema/storm-service=com.hsunfkqm.storm.framework.spring.StormServiceNamespaceHandler
http\://www.storm.com/schema/storm-reference=com.hsunfkqm.storm.framework.spring.StormRemoteReferenceNamespaceHandler
將編寫好的文件放入 classpath
下的 META-INF
目錄下:
在 Spring 配置文件中配置服務類:
<!-- 發佈遠程服務 -->
<bean id="helloService" class="com.hsunfkqm.storm.framework.test.HelloServiceImpl"/>
<storm:service id="helloServiceRegister"
interface="com.hsunfkqm.storm.framework.test.HelloService"
ref="helloService"
groupName="default"
weight="2"
appKey="ares"
workerThreads="100"
serverPort="8081"
timeout="600"/>
編寫對應的 Handler 和 Parser:StormServiceNamespaceHandler
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
/**
* @author 孫浩
* @Descrption 服務發佈自定義標簽
***/
public class StormServiceNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("service", new ProviderFactoryBeanDefinitionParser());
}
}
ProviderFactoryBeanDefinitionParser
:
protected Class getBeanClass(Element element) {
return ProviderFactoryBean.class;
}
protected void doParse(Element element, BeanDefinitionBuilder bean) {
try {
String serviceItf = element.getAttribute("interface");
String serverPort = element.getAttribute("serverPort");
String ref = element.getAttribute("ref");
// ....
bean.addPropertyValue("serverPort", Integer.parseInt(serverPort));
bean.addPropertyValue("serviceItf", Class.forName(serviceItf));
bean.addPropertyReference("serviceObject", ref);
//...
if (NumberUtils.isNumber(weight)) {
bean.addPropertyValue("weight", Integer.parseInt(weight));
}
//...
} catch (Exception e) {
// ...
}
}
ProviderFactoryBean
:
/**
* @author 孫浩
* @Descrption 服務發佈
***/
public class ProviderFactoryBean implements FactoryBean, InitializingBean {
//服務介面
private Class<?> serviceItf;
//服務實現
private Object serviceObject;
//服務埠
private String serverPort;
//服務超時時間
private long timeout;
//服務代理對象,暫時沒有用到
private Object serviceProxyObject;
//服務提供者唯一標識
private String appKey;
//服務分組組名
private String groupName = "default";
//服務提供者權重,預設為 1 , 範圍為 [1-100]
private int weight = 1;
//服務端線程數,預設 10 個線程
private int workerThreads = 10;
@Override
public Object getObject() throws Exception {
return serviceProxyObject;
}
@Override
public Class<?> getObjectType() {
return serviceItf;
}
@Override
public void afterPropertiesSet() throws Exception {
//啟動 Netty 服務端
NettyServer.singleton().start(Integer.parseInt(serverPort));
//註冊到 zk, 元數據註冊中心
List<ProviderService> providerServiceList = buildProviderServiceInfos();
IRegisterCenter4Provider registerCenter4Provider = RegisterCenter.singleton();
registerCenter4Provider.registerProvider(providerServiceList);
}
}
//================RegisterCenter#registerProvider======================
@Override
public void registerProvider(final List<ProviderService> serviceMetaData) {
if (CollectionUtils.isEmpty(serviceMetaData)) {
return;
}
//連接 zk, 註冊服務
synchronized (RegisterCenter.class) {
for (ProviderService provider : serviceMetaData) {
String serviceItfKey = provider.getServiceItf().getName();
List<ProviderService> providers = providerServiceMap.get(serviceItfKey);
if (providers == null) {
providers = Lists.newArrayList();
}
providers.add(provider);
providerServiceMap.put(serviceItfKey, providers);
}
if (zkClient == null) {
zkClient = new ZkClient(ZK_SERVICE, ZK_SESSION_TIME_OUT, ZK_CONNECTION_TIME_OUT, new SerializableSerializer());
}
//創建 ZK 命名空間/當前部署應用 APP 命名空間/
String APP_KEY = serviceMetaData.get(0).getAppKey();
String ZK_PATH = ROOT_PATH + "/" + APP_KEY;
boolean exist = zkClient.exists(ZK_PATH);
if (!exist) {
zkClient.createPersistent(ZK_PATH, true);
}
for (Map.Entry<String, List<ProviderService>> entry : providerServiceMap.entrySet()) {
//服務分組
String groupName = entry.getValue().get(0).getGroupName();
//創建服務提供者
String serviceNode = entry.getKey();
String servicePath = ZK_PATH + "/" + groupName + "/" + serviceNode + "/" + PROVIDER_TYPE;
exist = zkClient.exists(servicePath);
if (!exist) {
zkClient.createPersistent(servicePath, true);
}
//創建當前伺服器節點
int serverPort = entry.getValue().get(0).getServerPort();//服務埠
int weight = entry.getValue().get(0).getWeight();//服務權重
int workerThreads = entry.getValue().get(0).getWorkerThreads();//服務工作線程
String localIp = IPHelper.localIp();
String currentServiceIpNode = servicePath + "/" + localIp + "|" + serverPort + "|" + weight + "|" + workerThreads + "|" + groupName;
exist = zkClient.exists(currentServiceIpNode);
if (!exist) {
//註意,這裡創建的是臨時節點
zkClient.createEphemeral(currentServiceIpNode);
}
//監聽註冊服務的變化,同時更新數據到本地緩存
zkClient.subscribeChildChanges(servicePath, new IZkChildListener() {
@Override
public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
if (currentChilds == null) {
currentChilds = Lists.newArrayList();
}
//存活的服務 IP 列表
List<String> activityServiceIpList = Lists.newArrayList(Lists.transform(currentChilds, new Function<String, String>() {
@Override
public String apply(String input) {
return StringUtils.split(input, "|")[0];
}
}));
refreshActivityService(activityServiceIpList);
}
});
}
}
}
至此服務實現類已被載入 Spring 容器中,且服務介面信息也註冊到了註冊中心。
- 網路通信
作為生產者對外提供 RPC 服務,必須有一個網路程式來來監聽請求和做出響應。在 Java 領域 Netty 是一款高性能的 NIO 通信框架,很多的框架的通信都是採用 Netty 來實現的,本例中也採用它當做通信伺服器。
構建並啟動 Netty 服務監聽指定埠:
public void start(final int port) {
synchronized (NettyServer.class) {
if (bossGroup != null || workerGroup != null) {
return;
}
bossGroup = new NioEventLoopGroup();
workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//註冊解碼器 NettyDecoderHandler
ch.pipeline().addLast(new NettyDecoderHandler(StormRequest.class, serializeType));
//註冊編碼器 NettyEncoderHandler
ch.pipeline().addLast(new NettyEncoderHandler(serializeType));
//註冊服務端業務邏輯處理器 NettyServerInvokeHandler
ch.pipeline().addLast(new NettyServerInvokeHandler());
}
});
try {
channel = serverBootstrap.bind(port).sync().channel();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
上面的代碼中向 Netty 服務的 pipeline 中添加了編解碼和業務處理器,當接收到請求時,經過編解碼後,真正處理業務的是業務處理器,即NettyServerInvokeHandler
, 該處理器繼承自SimpleChannelInboundHandler
, 當數據讀取完成將觸發一個事件,並調用NettyServerInvokeHandler#channelRead0
方法來處理請求。
@Override
protected void channelRead0(ChannelHandlerContext ctx, StormRequest request) throws Exception {
if (ctx.channel().isWritable()) {
//從服務調用對象里獲取服務提供者信息
ProviderService metaDataModel = request.getProviderService();
long consumeTimeOut = request.getInvokeTimeout();
final String methodName = request.getInvokedMethodName();
//根據方法名稱定位到具體某一個服務提供者
String serviceKey = metaDataModel.getServiceItf().getName();
//獲取限流工具類
int workerThread = metaDataModel.getWorkerThreads();
Semaphore semaphore = serviceKeySemaphoreMap.get(serviceKey);
if (semaphore == null) {
synchronized (serviceKeySemaphoreMap) {
semaphore = serviceKeySemaphoreMap.get(serviceKey);
if (semaphore == null) {
semaphore = new Semaphore(workerThread);
serviceKeySemaphoreMap.put(serviceKey, semaphore);
}
}
}
//獲取註冊中心服務
IRegisterCenter4Provider registerCenter4Provider = RegisterCenter.singleton();
List<ProviderService> localProviderCaches = registerCenter4Provider.getProviderServiceMap().get(serviceKey);
Object result = null;
boolean acquire = false;
try {
ProviderService localProviderCache = Collections2.filter(localProviderCaches, new Predicate<ProviderService>() {
@Override
public boolean apply(ProviderService input) {
return StringUtils.equals(input.getServiceMethod().getName(), methodName);
}
}).iterator().next();
Object serviceObject = localProviderCache.getServiceObject();
//利用反射發起服務調用
Method method = localProviderCache.getServiceMethod();
//利用 semaphore 實現限流
acquire = semaphore.tryAcquire(consumeTimeOut, TimeUnit.MILLISECONDS);
if (acquire) {
result = method.invoke(serviceObject, request.getArgs());
//System.out.println("---------------"+result);
}
} catch (Exception e) {
System.out.println(JSON.toJSONString(localProviderCaches) + " " + methodName+" "+e.getMessage());
result = e;
} finally {
if (acquire) {
semaphore.release();
}
}
//根據服務調用結果組裝調用返回對象
StormResponse response = new StormResponse();
response.setInvokeTimeout(consumeTimeOut);
response.setUniqueKey(request.getUniqueKey());
response.setResult(result);
//將服務調用返回對象回寫到消費端
ctx.writeAndFlush(response);
} else {
logger.error("------------channel closed!---------------");
}
}
此處還有部分細節如自定義的編解碼器等,篇幅所限不在此詳述,繼承 MessageToByteEncoder
和 ByteToMessageDecoder
覆寫對應的 encode
和 decode
方法即可自定義編解碼器,使用到的序列化工具如 Hessian/Proto 等可參考對應的官方文檔。
- 請求和響應包裝
為便於封裝請求和響應,定義兩個 bean 來表示請求和響應。
請求:
/**
* @author 孫浩
* @Descrption
***/
public class StormRequest implements Serializable {
private static final long serialVersionUID = -5196465012408804755L;
//UUID, 唯一標識一次返回值
private String uniqueKey;
//服務提供者信息
private ProviderService providerService;
//調用的方法名稱
private String invokedMethodName;
//傳遞參數
private Object[] args;
//消費端應用名
private String appName;
//消費請求超時時長
private long invokeTimeout;
// getter/setter
}
響應:
/**
* @author 孫浩
* @Descrption
***/
public class StormResponse implements Serializable {
private static final long serialVersionUID = 5785265307118147202L;
//UUID, 唯一標識一次返回值
private String uniqueKey;
//客戶端指定的服務超時時間
private long invokeTimeout;
//介面調用返回的結果對象
private Object result;
//getter/setter
}
客戶端(消費者)
客戶端(消費者)在 RPC 調用中主要是生成服務介面的代理對象,並從註冊中心獲取對應的服務列表發起網路請求。
客戶端和服務端一樣採用 Spring 來管理 bean 解析 xml 配置等不再贅述,重點看下以下幾點:
- 通過 jdk 動態代理來生成引入服務介面的代理對象
public Object getProxy() {
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{targetInterface}, this);
}
- 從註冊中心獲取服務列表並依據某種策略選取其中一個服務節點
//服務介面名稱
String serviceKey = targetInterface.getName();
//獲取某個介面的服務提供者列表
IRegisterCenter4Invoker registerCenter4Consumer = RegisterCenter.singleton();
List<ProviderService> providerServices = registerCenter4Consumer.getServiceMetaDataMap4Consume().get(serviceKey);
//根據軟負載策略,從服務提供者列表選取本次調用的服務提供者
ClusterStrategy clusterStrategyService = ClusterEngine.queryClusterStrategy(clusterStrategy);
ProviderService providerService = clusterStrategyService.select(providerServices);
- 通過 Netty 建立連接,發起網路請求
/**
* @author 孫浩
* @Descrption Netty 消費端 bean 代理工廠
***/
public class RevokerProxyBeanFactory implements InvocationHandler {
private ExecutorService fixedThreadPool = null;
//服務介面
private Class<?> targetInterface;
//超時時間
private int consumeTimeout;
//調用者線程數
private static int threadWorkerNumber = 10;
//負載均衡策略
private String clusterStrategy;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
//複製一份服務提供者信息
ProviderService newProvider = providerService.copy();
//設置本次調用服務的方法以及介面
newProvider.setServiceMethod(method);
newProvider.setServiceItf(targetInterface);
//聲明調用 AresRequest 對象,AresRequest 表示發起一次調用所包含的信息
final StormRequest request = new StormRequest();
//設置本次調用的唯一標識
request.setUniqueKey(UUID.randomUUID().toString() + "-" + Thread.currentThread().getId());
//設置本次調用的服務提供者信息
request.setProviderService(newProvider);
//設置本次調用的方法名稱
request.setInvokedMethodName(method.getName());
//設置本次調用的方法參數信息
request.setArgs(args);
try {
//構建用來發起調用的線程池
if (fixedThreadPool == null) {
synchronized (RevokerProxyBeanFactory.class) {
if (null == fixedThreadPool) {
fixedThreadPool = Executors.newFixedThreadPool(threadWorkerNumber);
}
}
}
//根據服務提供者的 ip,port, 構建 InetSocketAddress 對象,標識服務提供者地址
String serverIp = request.getProviderService().getServerIp();
int serverPort = request.getProviderService().getServerPort();
InetSocketAddress inetSocketAddress = new InetSocketAddress(serverIp, serverPort);
//提交本次調用信息到線程池 fixedThreadPool, 發起調用
Future<StormResponse> responseFuture = fixedThreadPool.submit(RevokerServiceCallable.of(inetSocketAddress, request));
//獲取調用的返回結果
StormResponse response = responseFuture.get(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);
if (response != null) {
return response.getResult();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}
// ...
}
Netty 的響應是非同步的,為了在方法調用返回前獲取到響應結果,需要將非同步的結果同步化。
- Netty 非同步返回的結果存入阻塞隊列
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, StormResponse response) throws Exception {
//將 Netty 非同步返回的結果存入阻塞隊列,以便調用端同步獲取
RevokerResponseHolder.putResultValue(response);
}
- 請求發出後同步獲取結果
//提交本次調用信息到線程池 fixedThreadPool, 發起調用
Future<StormResponse> responseFuture = fixedThreadPool.submit(RevokerServiceCallable.of(inetSocketAddress, request));
//獲取調用的返回結果
StormResponse response = responseFuture.get(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);
if (response != null) {
return response.getResult();
}
//===================================================
//從返回結果容器中獲取返回結果,同時設置等待超時時間為 invokeTimeout
long invokeTimeout = request.getInvokeTimeout();
StormResponse response = RevokerResponseHolder.getValue(request.getUniqueKey(), invokeTimeout);
測試
Server
:
/**
* @author 孫浩
* @Descrption
***/
public class MainServer {
public static void main(String[] args) throws Exception {
//發佈服務
final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("storm-server.xml");
System.out.println(" 服務發佈完成");
}
}
Client
:
public class Client {
private static final Logger logger = LoggerFactory.getLogger(Client.class);
public static void main(String[] args) throws Exception {
final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("storm-client.xml");
final HelloService helloService = (HelloService) context.getBean("helloService");
String result = helloService.sayHello("World");
System.out.println(result);
for (;;) {
}
}
}
結果
生產者:
消費者:
註冊中心
總結
本文簡單介紹了 RPC 的整個流程,並實現了一個簡單的 RPC 調用。希望閱讀完本文之後,能加深你對 RPC 的一些認識。
- 生產者端流程:
- 載入服務介面,並緩存
- 服務註冊,將服務介面以及服務主機信息寫入註冊中心(本例使用的是 zookeeper)
- 啟動網路伺服器並監聽
- 反射,本地調用
- 消費者端流程:
- 代理服務介面生成代理對象
- 服務發現(連接 zookeeper,拿到服務地址列表,通過客戶端負載策略獲取合適的服務地址)
- 遠程方法調用(本例通過 Netty,發送消息,並獲取響應結果)
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!
覺得不錯,別忘了隨手點贊+轉發哦!