本人微信公眾號(jwfy)歡迎關注
上一期完成了手寫一個RPC框架,看看100個線程同時調用效果如何,但還是遺留了很多問題以及可以優化的點,這次就完全重寫之前的代碼,演進到v2版本,使得代碼邏輯更加規范的同時,引入ZooKeeper輔助完成服務治理。
在代碼展示之前還是先介紹一些基本的概念以及設計思路,ZooKeeper是什么,服務治理又是什么等,最后貼了部分關鍵代碼以說明和v1版本的區別,有哪些點的改進措施。
最后還提了個問題:線程池打滿了怎么辦?,你有什么好的解決方案呢?
ZooKeeper
ZooKeeper(直譯為動物管理員,簡稱zk)是一個分布式、開源的應用協調服務,利用和Paxos類似的ZAB選舉算法實現分布式一致性服務。有類似于Unix文件目錄的節點信息,同時可以針對節點的變更添加watcher監聽以能夠即使感知到節點信息變更。可提供的功能例如域名服務、配置維護、同步以及組服務等(此功能介紹來自官網描述:It exposes common services - such as naming, configuration management, synchronization, and group services - in a simple interface)。如下圖就是DUBBO存儲在ZooKeeper的節點數據情況。
在本地啟動服務后通過zk客戶端連接后也可通過命令查看節點信息,如下圖所示。
ZooKeeper包含了4種不同含義的功能節點,在每次創建節點之前都需要明確聲明節點類型。
類型 | 定義 | 描述 |
---|---|---|
PERSISTENT | 持久化目錄節點 | 客戶端與zookeeper斷開連接后,該節點依舊存在 |
PERSISTENT_SEQUENTIAL | 持久化順序編號目錄節點 | 客戶端與zookeeper斷開連接后,該節點依舊存在,只是Zookeeper給該節點名稱進行順序編號 |
EPHEMERAL | 臨時目錄節點 | 客戶端與zookeeper斷開連接后,該節點被刪除 |
EPHEMERAL_SEQUENTIAL | 臨時順序編號目錄節點 | 客戶端與zookeeper斷開連接后,該節點被刪除,只是Zookeeper給該節點名稱進行順序編號 |
ZooKeeper使用之前需要先進行安裝,后開啟服務端的服務,我們的服務作為客戶端
連接ZooKeeper以便于后續的操作。具體可參考官網文檔Zookeeper3.5.5 官方文檔,在實際的java項目開發中也是可以通過maven引入ZkClient或者Curator開源的客戶端,在本文學習筆記中是使用的Curator,因為其已經封裝了原始的節點注冊、數據獲取、添加watcher等功能。具體maven引入的版本如下,
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
服務治理
服務治理也就是針對服務進行管理的措施,例如服務發現
、服務暴露
、負載均衡
、快速上下線
等都是服務治理的具體體現。
- 服務發現:從服務管理中心獲取到需要的服務相關信息,例如我們可以從zk中獲取相關服務的機器信息,然后我們就可以和具體機器直連完成相關功能
- 服務暴露:服務提供方可以提供什么樣子的功能,經過服務暴露暴露出去,其他使用方就可以通過服務發現發現具體的服務提供方信息
- 負載均衡:一般針對的是服務提供方,避免大量請求同時打到一臺機器上,采用隨機、輪詢等措施讓請求均分到各個機器上,提供服務效率,
限流
,灰度
等也都是類似的操作,通過動態路由、軟負載的形式處理分發請求。 - 快速上線下:以往需要上下線可能需要殺掉機器上的進程,現在只需要讓該服務停止暴露即可,實現服務的靈活上下線。
數據處理流程
服務端:服務的提供方,接受網絡傳輸的請求數據、通過網絡把應答數據發送給客戶端
客戶端:服務的調用方,使用本地代理,通過網絡把請求數據發送出去,接受服務端返回的應答數據
所有的數據傳輸都是按照上面圖片說的流程來的,如果需要添加自定義的序列化工具,則需要在把數據提交到socket的輸出流緩沖區之前按照序列化工具完成序列化操作,反序列化則進行反向操作即可。
RPC 實踐 V2版本
文件夾目錄如下圖所示,其中:
- balance文件夾:負載均衡有關
- config文件夾:網絡套接字傳輸的數據模型以及服務暴露、服務發現的數據模型
- core文件夾:核心文件夾,包含了服務端和客戶端的請求處理、代理生成等
- demo文件夾:測試試用
- io.protocol文件夾:目前是只有具體的請求對象和網絡io的封裝
- register:服務注冊使用,實現了使用zk進行服務注冊和服務發現的操作
由于代碼太長,只貼部分重要的代碼操作。
服務暴露 & 服務發現
public interface ServiceRegister {
/**
* 服務注冊
* @param config
*/
void register(BasicConfig config);
/**
* 服務發現,從注冊中心獲取可用的服務提供方信息
* @param request
* @return
*/
InetSocketAddress discovery(RpcRequest request, ServiceType nodeType);
}
默認使用了CuratorFramework客戶端完成zk數據的操作
public class ZkServiceRegister implements ServiceRegister {
private CuratorFramework client;
private static final String ROOT_PATH = "jwfy/simple-rpc";
private LoadBalance loadBalance = new DefaultLoadBalance();
public ZkServiceRegister() {
RetryPolicy policy = new ExponentialBackoffRetry(1000, 3);
this.client = CuratorFrameworkFactory
.builder()
.connectString("127.0.0.1:2182")
.sessionTimeoutMs(50000)
.retryPolicy(policy)
.namespace(ROOT_PATH)
.build();
// 業務的根路徑是 /jwfy/simple-rpc ,其他的都會默認掛載在這里
this.client.start();
System.out.println("zk啟動正常");
}
@Override
public void register(BasicConfig config) {
String interfacePath = "/" + config.getInterfaceName();
try {
if (this.client.checkExists().forPath(interfacePath) == null) {
// 創建 服務的永久節點
this.client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
.forPath(interfacePath);
}
config.getMethods().forEach(method -> {
try {
String methodPath = null;
ServiceType serviceType = config.getType();
if (serviceType == ServiceType.PROVIDER) {
// 服務提供方,需要暴露自身的ip、port信息,而消費端則不需要
String address = getServiceAddress(config);
methodPath = String.format("%s/%s/%s/%s", interfacePath, serviceType.getType(), method.getMethodName(), address);
} else {
methodPath = String.format("%s/%s/%s", interfacePath, serviceType.getType(), method.getMethodName());
}
System.out.println("zk path: [" + methodPath + "]");
this.client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(methodPath, "0".getBytes());
// 創建臨時節點,節點包含了服務提供段的信息
} catch (Exception e) {
e.getMessage();
}
});
} catch (Exception e) {
e.getMessage();
}
}
@Override
public InetSocketAddress discovery(RpcRequest request, ServiceType nodeType) {
String path = String.format("/%s/%s/%s", request.getClassName(), nodeType.getType(), request.getMethodName());
try {
List<String> addressList = this.client.getChildren().forPath(path);
// 采用負載均衡的方式獲取服務提供方信息,不過并沒有添加watcher監聽模式
String address = loadBalance.balance(addressList);
if (address == null) {
return null;
}
return parseAddress(address);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private String getServiceAddress(BasicConfig config) {
String hostInfo = new StringBuilder()
.append(config.getHost())
.append(":")
.append(config.getPort())
.toString();
return hostInfo;
}
private InetSocketAddress parseAddress(String address) {
String[] result = address.split(":");
return new InetSocketAddress(result[0], Integer.valueOf(result[1]));
}
public void setLoadBalance(LoadBalance loadBalance) {
// 可以重新設置負載均衡的策略
this.loadBalance = loadBalance;
}
}
服務啟動后利用zkclient查詢到在zk中包含的節點信息,其中默認的命名空間是jwfy/simple-rpc
負載均衡
public interface LoadBalance {
String balance(List<String> addressList);
}
public abstract class AbstractLoadBalance implements LoadBalance {
@Override
public String balance(List<String> addressList) {
if (addressList == null || addressList.isEmpty()) {
return null;
}
if (addressList.size() == 1) {
return addressList.get(0);
}
return doLoad(addressList);
}
abstract String doLoad(List<String> addressList);
}
public class DefaultLoadBalance extends AbstractLoadBalance {
@Override
String doLoad(List<String> addressList) {
Random random = new Random();
// 利用隨機函數選擇一個,其中random.nextIn生成的數據是在[0, size) 之間
return addressList.get(random.nextInt(addressList.size()));
}
}
上面的負載均衡代碼其實很簡單,就是從一個機器列表addressList中選擇一個,如果列表為空或者不存在則直接返回null,如果機器只有1臺則直接獲取返回即可,當列表記錄超過1條后利用隨機函數生成一個列表偏移量以獲取對應數據。也可以按照類似完成更多負載均衡的策略,然后調用setLoadBalance方法就可以了。
IO 處理
public interface MessageProtocol {
/**
* 服務端解析從網絡傳輸的數據,轉變成request
* @param inputStream
* @return
*/
void serviceToRequest(RpcRequest request, InputStream inputStream);
/**
* 服務端把計算機的結果包裝好,通過outputStream 返回給客戶端
* @param response
* @param outputStream
* @param <T>
*/
<T> void serviceGetResponse(RpcResponse<T> response, OutputStream outputStream);
/**
* 客戶端把請求拼接好,通過outputStream發送到服務端
* @param request
* @param outputStream
*/
void clientToRequest(RpcRequest request, OutputStream outputStream);
/**
* 客戶端接收到服務端響應的結果,轉變成response
* @param response
* @param inputStream
*/
void clientGetResponse(RpcResponse response, InputStream inputStream);
}
實現類DefaultMessageProtocol
public class DefaultMessageProtocol implements MessageProtocol {
@Override
public void serviceToRequest(RpcRequest request, InputStream inputStream) {
try {
ObjectInputStream input = new ObjectInputStream(inputStream);
request.setClassName(input.readUTF());
request.setMethodName(input.readUTF());
request.setParameterTypes((Class<?>[])input.readObject());
request.setArguments((Object[])input.readObject());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public <T> void serviceGetResponse(RpcResponse<T> response, OutputStream outputStream) {
try {
ObjectOutputStream output = new ObjectOutputStream(outputStream);
output.writeBoolean(response.getError());
output.writeObject(response.getResult());
output.writeObject(response.getErrorMessage());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void clientToRequest(RpcRequest request, OutputStream outputStream) {
try {
ObjectOutputStream ouput = new ObjectOutputStream(outputStream);
ouput.writeUTF(request.getClassName());
ouput.writeUTF(request.getMethodName());
ouput.writeObject(request.getParameterTypes());
ouput.writeObject(request.getArguments());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void clientGetResponse(RpcResponse response, InputStream inputStream) {
try {
ObjectInputStream input = new ObjectInputStream(inputStream);
response.setError(input.readBoolean());
response.setResult(input.readObject());
response.setErrorMessage((String) input.readObject());
} catch (Exception e) {
e.printStackTrace();
}
}
}
服務端請求處理
public class ServiceHandler {
private ThreadPoolExecutor executor = null;
private RpcService rpcService;
private MessageProtocol messageProtocol;
public ServiceHandler(RpcService rpcService) {
this.rpcService = rpcService;
ThreadFactory commonThreadName = new ThreadFactoryBuilder()
.setNameFormat("Parse-Task-%d")
.build();
this.executor = new ThreadPoolExecutor(
10,
10,
2,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
commonThreadName, new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
SocketTask socketTask = (SocketTask) r;
Socket socket = socketTask.getSocket();
if (socket != null) {
try {
socket.close();
System.out.println("reject socket:" + socketTask + ", and closed");
// 無法及時處理和響應的就快速拒絕掉
} catch (IOException e) {
}
}
}
});
}
public RpcService getRpcService() {
return rpcService;
}
public void setRpcService(RpcService rpcService) {
this.rpcService = rpcService;
}
public MessageProtocol getMessageProtocol() {
return messageProtocol;
}
public void setMessageProtocol(MessageProtocol messageProtocol) {
this.messageProtocol = messageProtocol;
}
public void handler(Socket socket) {
// 接收到新的套接字,包裝成為一個runnable提交給線程去執行
this.executor.execute(new SocketTask(socket));
}
class SocketTask implements Runnable {
private Socket socket;
public SocketTask(Socket socket) {
this.socket = socket;
}
public Socket getSocket() {
return socket;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
RpcRequest request = new RpcRequest();
messageProtocol.serviceToRequest(request, inputStream);
// 獲取客戶端請求數據,統一包裝成RpcRequest
RpcResponse response = rpcService.invoke(request);
// 反射調用,得到具體的返回值
System.out.println("request:[" + request + "], response:[" + response + "]");
messageProtocol.serviceGetResponse(response, outputStream);
// 再返回給客戶端
} catch (Exception e) {
// error
} finally {
if (socket != null) {
// SOCKET 關閉一定要加上,要不然會出各種事情
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
客戶端 代理對象
public class ProxyInstance implements InvocationHandler {
private RpcClient rpcClient;
private Class clazz;
public ProxyInstance(RpcClient client, Class clazz) {
this.rpcClient = client;
this.clazz = clazz;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
RpcRequest request = new RpcRequest();
request.setClassName(clazz.getName());
request.setMethodName(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setArguments(args);
// 獲取服務提供方信息,這里既是服務發現的入口,找到一個合適的可用的服務提供方信息
InetSocketAddress address = rpcClient.discovery(request);
System.out.println("[" + Thread.currentThread().getName() + "]discover service:" + address);
// 發起網絡請求,得到請求數據
RpcResponse response = rpcClient.invoke(request, address);
return response.getResult();
}
}
上面的InetSocketAddress address = rpcClient.discovery(request)
是相比v1多了一個最重要的東西,每次獲取請求后都實時從zk中獲取對應的服務提供方信息,這就是服務發現。
實踐
public class Client {
public static void main(String[] args) {
RpcClient rpcClient = new RpcClient();
rpcClient.subscribe(Calculate.class);
rpcClient.start();
Calculate<Integer> calculateProxy = rpcClient.getInstance(Calculate.class);
for(int i=0; i< 200; i++) {
new Thread(() -> {
long start = System.currentTimeMillis();
int s1 = new Random().nextInt(100);
int s2 = new Random().nextInt(100);
int s3 = calculateProxy.add(s1, s2);
System.out.println("[" + Thread.currentThread().getName() + "]a: " + s1 + ", b:" + s2 + ", c=" + s3 + ", 耗時:" + (System.currentTimeMillis() - start));
}).start();
}
}
}
客戶端開啟200個線程后,執行結果是順利執行的,在服務端開啟的接受請求被添加到線程池中,而代碼中線程池的任務隊列長度是200,可以完全的存儲200個線程,但是如果我們把客戶端請求量從200個改成500個呢,又會出現什么情況?
服務端
客戶端
如上述的圖片顯示,當請求量打滿線程池之后,線程池的拒絕策略就開始生效了,在本代碼中是直接調用了close操作,而客戶端感知到關閉后也會出現io錯誤,而正常的請求則順利執行。其中還有輸出discover服務發現了服務提供方的機器信息,這也是符合起初的想法的。
這里一定要加上一些策略以及時關閉無法處理的socket,否則就會出現服務提供方無任何可執行,但是服務調用方卻還在等待中,因為socket并沒有關閉,從而出現資源被占用了,還不執行相關任務。
提個問題:線程池打滿了怎么辦?
在本demo中采取了非常粗暴的策略,直接丟棄了無法處理的任務,在實際的線上業務中,可以先加機器以能再最短的時間內恢復線上情況,后期結合業務特點提出針對性的解決方案。如果業務接受一定的延遲,可以考慮接入kafka類似的消息隊列(削峰是mq的一大特點);如果對時間要求很高,要么加機器,要么壓榨機器性能,可能之前設置的線程池的數量太小,那就需要調節線程池的各個核心數據,修改線程池的任務隊列類型也是可以考慮的;此外也有可能是業務耗時太多,無法及時處理完全造成請求堆積導致的,那么就需要考慮業務的同步改異步化。最后線上告警也需要及時完善。
沒有絕對的解決方案,只有最合適當下場景的方案,沒有銀彈,任何不具體結合業務的方案都是扯淡。
總結思考
v2版本相比v1版本修改了整個代碼結構,使得結構能夠更加明確,引入zookeeper作為服務治理功能,大致介紹了zookeeper的特點以及功能,給服務注冊、服務發現、序列化協議等均留下了口子,以便實現自定義的協議,v1的io模型是BIO,v2并沒有變化,只是由單線程改造成多線程。
整體而言符合一個簡單的rpc框架,依舊還是有很多點可以完善、優化的點,如:
- io模型還是沒有替換,后面考慮直接整體接入netty;
- 也不應該每次實時從zk獲取節點信息,應該先設置一個本地緩存,再利用zookeeper的watcher功能,開啟一個異步線程去監聽更新本地緩存,降低和zk交互帶來的性能損耗;
- 也沒有快速失敗、重試的功能,客觀情況下存在網絡抖動的問題,重試就可以了
- 整體的各種協議約定并沒有明確規范,比較混亂
本人微信公眾號(搜索jwfy)歡迎關注