RPC原來就是Socket——RPC框架到dubbo的服務動態註冊,服務路由,負載均衡演化

  序:RPC就是使用socket告訴服務端我要調你的哪個類的哪個方法而後得到處理的結果。服務註冊和路由就是藉助第三方存儲介質存儲服務信息讓服務消費者調用。然咱們本身動手從0開始寫一個rpc功能以及實現服務註冊,動態上下線,服務路由,負載均衡。html

一句話明白RPC原理

  RPC即遠程過程調用,它的實現方式有不少,好比webservice等。框架調多了,煩了,沒激情了,咱們就該問本身,這些框架的做用究竟是什麼,來找回當初的激情。
  通常來講,咱們寫的系統就是一個單機系統,一個web服務器一個數據庫服務,可是當這單臺服務器的處理能力受硬件成本的限制,是不能無限的提高處理性能的。這個時候咱們使用RPC將原來的本地調用轉變爲調用遠端的服務器上的方法,給系統的處理能力和吞吐量帶來了提高。
  RPC的實現包括客戶端和服務端,即服務的調用方和服務的提供方。服務調用方發送rpc請求到服務提供方,服務提供方根據調用方提供的參數執行請求方法,將執行的結果返回給調用方,一次rpc調用完成。
java

原文和做者一塊兒討論:http://www.cnblogs.com/intsmaze/p/6058765.html
mysql

 

從零實現一個RPC功能

先讓咱們利用socket簡單的實現RPC,來看看他是什麼鬼樣子。nginx

服務端代碼以下 web

服務端的提供服務的方法redis

package cn.intsmaze.tcp.two.service; public class SayHelloServiceImpl { public String sayHello(String helloArg) { if(helloArg.equals("intsmaze")) { return "intsmaze"; } else { return "bye bye"; } } }

  服務端啓動接收外部方法請求的端口類,它接收到來自客戶端的請求數據後,利用反射知識,建立指定類的對象,並調用對應方法,而後把執行的結果返回給客戶端便可。算法

package cn.intsmaze.tcp.two.service; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Method; import java.net.ServerSocket; import java.net.Socket; public class Provider { public static void main(String[] args) throws Exception { ServerSocket server=new ServerSocket(1234); while(true) { Socket socket=server.accept(); ObjectInputStream input=new ObjectInputStream(socket.getInputStream()); String classname=input.readUTF();//得到服務端要調用的類名 String methodName=input.readUTF();//得到服務端要調用的方法名稱 Class<?>[] parameterTypes=(Class<?>[]) input.readObject();//得到服務端要調用方法的參數類型 Object[] arguments=(Object[]) input.readObject();//得到服務端要調用方法的每個參數的值 Class serviceclass=Class.forName(classname);//建立類 Object object = serviceclass.newInstance();//建立對象 Method method=serviceclass.getMethod(methodName, parameterTypes);//得到該類的對應的方法 Object result=method.invoke(object, arguments);//該對象調用指定方法 ObjectOutputStream output=new ObjectOutputStream(socket.getOutputStream()); output.writeObject(result); socket.close(); } } }  

服務調用者代碼sql

  調用服務的方法,主要就是客戶端啓動一個socket,而後向提供服務的服務端發送數據,其中的數據就是告訴服務端去調用哪個類的哪個方法,已經調用該方法的參數是多少,而後結束服務端返回的數據便可。數據庫

package cn.intsmaze.tcp.two.client; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Socket; public class consumer { @SuppressWarnings({ "unused", "rawtypes" }) public static void main(String[] arg) throws Exception { //咱們要想調用遠程提供的服務,必須告訴遠程咱們要調用你的哪個類,這裏咱們能夠在本地建立一個interface來獲取類的名稱,可是這樣咱們必須 //保證該interface和遠程的interface的所在包名一致。這種方式很差。因此咱們仍是經過硬編碼的方式吧。
     //雖然webservice就是這樣的,我我的以爲不是多好。      // String interfacename=SayHelloService.class.getName(); String classname="cn.intsmaze.tcp.two.service.SayHelloServiceImpl"; String method="sayHello"; Class[] argumentsType={String.class}; Object[] arguments={"intsmaze"}; Socket socket=new Socket("127.0.0.1",1234); ObjectOutputStream output=new ObjectOutputStream(socket.getOutputStream()); output.writeUTF(classname); output.writeUTF(method); output.writeObject(argumentsType); output.writeObject(arguments); ObjectInputStream input=new ObjectInputStream(socket.getInputStream()); Object result=input.readObject(); System.out.println(result); socket.close(); } }

目前的弊端 

   固然實際中出於性能考慮,每每採用非阻塞式I/O,避免無限的等待,帶來系統性能的消耗。json

  上面的只是一個簡單的過程,當系統之間的調用變的複雜以後,該方式有以下不足:服務調用者代碼以硬編碼的方式指明所調用服務的信息(類名,方法名),當服務提供方改動所提供的服務的代碼後,服務調用者必須修改代碼進行調整,否則會致使服務調用者沒法成功進行遠程方法調用致使系統異常,而且當服務提供者宕機下線了,服務調用者並不知道服務端是否存活,仍然會進行訪問,致使異常。

RPC中引入服務註冊

  一個系統中,服務提供者每每不是一個,而是多個,那麼服務消費者如何從衆多的服務者找到對應的服務進行RPC就是一個問題了,由於這個時候咱們不能在在服務調用者代碼中硬編碼指出調用哪個服務的地址等信息,由於咱們能夠想象,沒有一個統一的地方管理全部服務,那麼咱們在錯綜複雜的系統之間沒法理清有哪些服務,已經服務的調用關係,這簡直就是災難。

    這個時候就要進行服務的註冊,經過一個第三方的存儲介質,當服務的提供者上線時,經過代碼將所提供的服務的相關信息寫入到存儲介質中,寫入的主要信息以key-value方式:服務的名稱:(類名,方法名,參數類型,參數,IP地址,端口)。服務的調用者向遠程調用服務時,會先到第三方存儲介質中根據所要調用的服務名獲得(類名,方法名,參數類型,參數,IP地址,端口)等參數,而後再向服務端發出調用請求。經過這種方式,代碼就變得靈活多變,不會再由於一個局部的變得引起全局架構的變更。由於通常的改動是不會變得服務的名稱的。這種方式其實就是soa架構,服務消費者經過服務名稱,從衆多服務中找到要調用的服務的相關信息,稱爲服務的路由。

  下面經過一個靜態MAP對象來模擬第三方存儲的介質。

package cn.intsmaze.tcp.three; import net.sf.json.JSONObject; public class ClassWays { String classname;//類名 String method;//方法 Class[] argumentsType;//參數類型 String ip;//服務的ip地址 int port;//服務的端口 get,set...... }

  第三方存儲介質,這裏固定了服務提供者的相關信息,理想的模擬是,當服務啓動後,自動向該類的map集合添加信息。可是由於服務端和客戶端啓動時,是兩個不一樣的jvm進程,客戶端時沒法訪問到服務端寫到靜態map集合的數據的。

package cn.intsmaze.tcp.three; import java.util.HashMap; import java.util.Map; import net.sf.json.JSONObject; public class ServiceRoute { public static Map<String,String> NAME=new HashMap<String, String>(); public ServiceRoute() { ClassWays classWays=new ClassWays(); Class[] argumentsType={String.class}; classWays.setArgumentsType(argumentsType); classWays.setClassname("cn.intsmaze.tcp.three.service.SayHelloServiceImpl"); classWays.setMethod("sayHello"); classWays.setIp("127.0.0.1"); classWays.setPort(1234); JSONObject js=JSONObject.fromObject(classWays); NAME.put("SayHello", js.toString()); } }

  接下來看服務端代碼的美麗面孔吧。

package cn.intsmaze.tcp.three.service;public class Provider { //服務啓動的時候,組裝相關信息,而後寫入第三方存儲機制,供服務的調用者去獲取 public void reallyUse() { ClassWays classWays = new ClassWays(); Class[] argumentsType = { String.class }; classWays.setArgumentsType(argumentsType); classWays.setClassname("cn.intsmaze.tcp.three.service.SayHelloServiceImpl"); classWays.setMethod("sayHello"); classWays.setIp("127.0.0.1"); classWays.setPort(1234); JSONObject js=JSONObject.fromObject(classWays); //模擬第三方存儲介質,實際中應該是redis,mysql,zookeeper等。 ServiceRoute.NAME.put("SayHello", js.toString()); } public static void main(String[] args) throws Exception { ServerSocket server = new ServerSocket(1234); //實際中,這個地方應該調用以下方法,可是由於簡單的模擬服務的註冊,將註冊的信息硬編碼在ServiceRoute類中,這個類的構造方法裏面會自動註冊服務的相關信息。 //server.reallyUse(); while (true) { Socket socket = server.accept(); ObjectInputStream input = new ObjectInputStream(socket.getInputStream()); String classname = input.readUTF(); String methodName = input.readUTF(); Class<?>[] parameterTypes = (Class<?>[]) input.readObject(); Object[] arguments = (Object[]) input.readObject(); Class serviceclass = Class.forName(classname); Object object = serviceclass.newInstance(); Method method = serviceclass.getMethod(methodName, parameterTypes); Object result = method.invoke(object, arguments); ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream()); output.writeObject(result); socket.close(); } } }

  服務的調用者代碼:

package cn.intsmaze.tcp.three.client;public class Consumer { public Object reallyUse(String provideName,Object[] arguments) throws Exception { //模擬從第三方存儲介質拿去數據 ServiceRoute serviceRoute=new ServiceRoute(); String js=serviceRoute.NAME.get(provideName); JSONObject obj = new JSONObject().fromObject(js); ClassWays classWays = (ClassWays)JSONObject.toBean(obj,ClassWays.class); String classname=classWays.getClassname(); String method=classWays.getMethod(); Class[] argumentsType=classWays.getArgumentsType(); Socket socket=new Socket(classWays.getIp(),classWays.getPort()); ObjectOutputStream output=new ObjectOutputStream(socket.getOutputStream()); output.writeUTF(classname); output.writeUTF(method); output.writeObject(argumentsType); output.writeObject(arguments); ObjectInputStream input=new ObjectInputStream(socket.getInputStream()); Object result=input.readObject(); socket.close(); return result; } @SuppressWarnings({ "unused", "rawtypes" }) public static void main(String[] arg) throws Exception { Consumer consumer=new Consumer(); Object[] arguments={"intsmaze"}; Object result=consumer.reallyUse("SayHello",arguments); System.out.println(result); } }

  回到開始的問題如今咱們保證了服務調用者對服務的調用的相關參數以動態的方式進行控制,經過封裝,服務調用者只須要指定每一次調用時的參數的值便可。可是當服務提供者宕機下線了,服務調用者並不知道服務端是否存活,仍然會進行訪問,致使異常。這個時候咱們該如何考慮解決了?

  剩下的我就不寫代碼示例了,代碼只是思想的表現形式,就像開發語言一直變化,可是思想是不變的。

服務動態上下線

  服務下線咱們應該把該服務從第三方存儲刪除,在服務提供方寫代碼進行刪除控制,也就是服務下線前訪問第三方刪除本身提供的服務。這樣固然行不通的,由於服務宕機時,纔不會說,我要宕機了,服務提供者你快去第三方存儲介質刪掉該服務信息。因此這個時候咱們就要在第三方存儲介質上作手腳,好比服務提供方並非直接把服務信息寫入第三方存儲介質,而是與一個第三方系統進行交互,第三方系統把接收到來自服務提供者的服務信息寫入第三方存儲介質中,而後在服務提供者和第三方系統間創建一個心跳檢測,當第三方系統檢測到服務提供者宕機後,就會自動到第三方介質中刪除對應服務信息。

  這個時候咱們就能夠選擇zookeeper做爲第三方存儲介質,服務啓動會到zookeeper上面建立一個臨時目錄,該目錄存儲該服務的相關信息,當服務端宕機了,zookeeper會自動刪除該文件夾,這個時候就實現了服務的動態上下線了。

  這個地方其實就是dubbo的一大特點功能:服務配置中心——動態註冊和獲取服務信息,來統一管理服務名稱和其對於的服務器的信息。服務提供者在啓動時,將其提供的服務名稱,服務器地址註冊到服務配置中心,服務消費者經過配置中心來得到須要調用服務的機器。當服務器宕機或下線,相應的機器須要動態地從服務配置中心移除,並通知相應的服務消費者。這個過程當中,服務消費者只在第一次調用服務時須要查詢服務配置中心,而後將查詢到的信息緩存到本地,後面的調用直接使用本地緩存的服務地址信息,而不須要從新發起請求到服務配置中心去獲取相應的服務地址,直到服務的地址列表有變動(機器上線或者下線)。

  zookeeper如何知道的?zookeeper其實就是會和客戶端直接有一個心跳檢測來判斷的,zookeeper功能很簡單的,能夠本身去看對應的書籍便可。

服務負載均衡

  隨着業務的發展,服務調用者的規模發展到必定的階段,對服務提供方也帶來了巨大的壓力,這個時候服務提供方就不在是一臺機器了,而是一個服務集羣了。

  服務調用者面對服務提供者集羣如何高效選擇服務提供者集羣中某一臺機器?

  一說到集羣,咱們都會想到反向代理nginx,因此咱們就會採用nginx的配置文件中存儲集羣中的全部IP和端口信息。而後把第三方存儲介質中存儲的服務信息——key-value:服務的名稱:(類名,方法名,參數類型,參數,IP地址,端口)IP地址改成集羣的代理地址,而後服務消費者根據服務名稱得到服務信息後組裝請求把數據發送到nginx,再由nginx負責轉發請求到對應的服務提供者集羣中的一臺。

  這確實是能夠知足的,可是若是吹毛求疵就會發現他所暴露的問題!

  一:使用nginx進行負載均衡,一旦nginx宕機,那麼依賴他的服務均將失效,這個時候服務的提供者並無宕機。

  二:這是一個內部系統的調用,服務調用者集羣數量遠遠小於外部系統的請求數量,那麼咱們將全部的服務消費者到服務提供者的請求都通過nginx,帶來沒必要要的效率開銷。

  改進方案:將服務提供者集羣的全部信息都存儲到第三方系統(如zookeeper)中對應服務名稱下,表現形式爲——服務名:[{機器IP:(類名,方法名,參數類型,參數,IP地址,端口)}...]。這樣服務消費者向第三方存儲系統(如zookeeper)得到服務的全部信息(服務集羣的地址列表),而後服務調用者就從這個列表中根據負載均衡算法選擇一個進行訪問。

  這個時候咱們可能會思考,負載均衡算法咱們是參考nginx把IP地址的分配選擇在第三方系統(如zookeeper)上進行實現仍是在服務調用者端進行實現?負載均衡算法部署在第三方系統(如zookeeper),服務消費者把服務名稱發給第三方系統,第三方系統根據服務名而後根據負載均衡算法從該服務的地址信息列表中選擇一個返回給服務消費者,服務消費者得到所調用服務的具體信息後,直接向服務的提供者發送請求。可是正如我所說,這只是一個內部系統,請求的數量每每沒有多大的變化,並且實現起來要在服務消費者直接調用zookeeper系統前面編寫一箇中間件做爲一箇中間,難免過於麻煩。咱們徹底能夠在服務的消費者處嵌入負載均衡算法,服務消費者獲取服務的地址信息列表後,運算負載均衡算法從所得的地址信息列表中選擇一個地址信息發送請求的數據。更進一步,服務消費者第一次執行負載均衡算法後就把選擇的地址信息存儲到本地緩存,之後再次訪問就直接從本地拿去,再也不到第三方系統中獲取了。

  基於第三方系統實現服務的負載均衡的方案已經實現,那麼咱們來解決下一個問題,服務的上線和下線如何告知服務的消費者,避免服務消費者訪問異常?

  前面咱們說了,服務提供者利用zookeeper系統的特性,能夠實現服務的註冊和刪除,那麼一樣,咱們也可讓服務的消費者監聽zookeeper上對應的服務目錄,當服務目錄變更後,服務消費者則從新到zookeeper上獲取新的服務地址信息,而後運算負載均衡算法選擇一個新的服務進行請求。

  若是有沒有講明白的能夠留言,我進行更正。基本上一個RPC就是這樣,剩下的一些基於RPC的框架無非就是實現了多些協議,以及一些多種語言環境的考慮和效率的提高。

   以爲不錯點個推薦吧,看在我花了一天時間把本身的知識整理分析,謝謝嘍。固然這仍是沒有寫好,等我下週有時間再添加圖片進行完善,關於這個架構的設計歡迎你們討論,共同成長。

相關文章
相關標籤/搜索