在後續一段時間裏, 我會寫一系列文章來講述如何實現一個RPC框架(我已經實現了一個示例框架, 代碼在我的github上)。 這是系列第三篇文章, 主要講述了服務註冊和服務發現這一塊。
在系列的第一篇文章中提到,我們的RPC框架需要有一個服務註冊中心。 通過這個中心,服務可以把自己的信息註冊進來,也可以獲取到別的服務的信息(例如ip、端口、版本信息等)。這一塊有個統一的名稱,叫服務發現。
對於服務發現,現在有很多可供選擇的工具,例如zookeeper, etcd或者是consul等。 有一篇文章專門對這三個工具做了對比: 服務發現:Zookeeper vs etcd vs Consul。 在我的框架中, 我選擇使用Consul來實現服務發現。對於Consul不瞭解的朋友可以去看我之前寫的關於Consul的博客。
Consul客戶端也有一些Java的實現,我用到了consul-api。
服務註冊
首先,我們定義一個接口:
public interface ServiceRegistry {
void register(String serviceName, ServiceAddress serviceAddress);
}
這個接口很簡單,向服務註冊中心註冊自己的地址。
對應的consul的實現:
public class ConsulServiceRegistry implements ServiceRegistry {
private ConsulClient consulClient;
public ConsulServiceRegistry(String consulAddress) {
String address[] = consulAddress.split(":");
ConsulRawClient rawClient = new ConsulRawClient(address[0], Integer.valueOf(address[1]));
consulClient = new ConsulClient(rawClient);
}
@Override
public void register(String serviceName, ServiceAddress serviceAddress) {
NewService newService = new NewService();
newService.setId(generateNewIdForService(serviceName, serviceAddress));
newService.setName(serviceName);
newService.setTags(new ArrayList<>());
newService.setAddress(serviceAddress.getIp());
newService.setPort(serviceAddress.getPort());
// Set health check
NewService.Check check = new NewService.Check();
check.setTcp(serviceAddress.toString());
check.setInterval("1s");
newService.setCheck(check);
consulClient.agentServiceRegister(newService);
}
private String generateNewIdForService(String serviceName, ServiceAddress serviceAddress){
// serviceName + ip + port
return serviceName + "-" + serviceAddress.getIp() + "-" + serviceAddress.getPort();
}
}
這裏我向consul註冊服務的時候,還設定了健康狀態檢查方式爲TCP連接方式, 即每過一秒,consul都會嘗試與該地址建立TCP連接以驗證服務狀態。 除了TCP連接之外,consul還提供了http、ttl等多種檢查方式。
另外一點值得注意的是,要確保id絕對唯一。 我能想到的比較直觀的解決方案是serviceName + 本機ip + 本機port的組合。
服務發現
對於服務發現而言, 值得注意的是,我們需要去watch consul上值的變化, 並更新保存在應用中的服務的地址。
首先,我們定義一個接口:
public interface ServiceDiscovery {
String discover(String serviceName);
}
這個接口很簡單,傳入serviceName,獲取一個可以訪問的該service的地址。
對應的consul的實現:
public class ConsulServiceDiscovery implements ServiceDiscovery {
private ConsulClient consulClient;
// 這裏我用到了LoadBalancer, 關於LB這塊,後續文章會專門講述
Map<String, LoadBalancer<ServiceAddress>> loadBalancerMap = new ConcurrentHashMap<>();
public ConsulServiceDiscovery(String consulAddress) {
String[] address = consulAddress.split(":");
ConsulRawClient rawClient = new ConsulRawClient(address[0], Integer.valueOf(address[1]));
consulClient = new ConsulClient(rawClient);
}
@Override
public String discover(String serviceName) {
List<HealthService> healthServices;
if (!loadBalancerMap.containsKey(serviceName)) {
healthServices = consulClient.getHealthServices(serviceName, true, QueryParams.DEFAULT)
.getValue();
loadBalancerMap.put(serviceName, buildLoadBalancer(healthServices));
// Watch consul
longPolling(serviceName);
}
return loadBalancerMap.get(serviceName).next().toString();
}
private void longPolling(String serviceName){
new Thread(new Runnable() {
@Override
public void run() {
long consulIndex = -1;
do {
QueryParams param =
QueryParams.Builder.builder()
.setIndex(consulIndex)
.build();
Response<List<HealthService>> healthyServices =
consulClient.getHealthServices(serviceName, true, param);
consulIndex = healthyServices.getConsulIndex();
log.debug("consul index for {} is: {}", serviceName, consulIndex);
List<HealthService> healthServices = healthyServices.getValue();
log.debug("service addresses of {} is: {}", serviceName, healthServices);
loadBalancerMap.put(serviceName, buildLoadBalancer(healthServices));
} while(true);
}
}).start();
}
private LoadBalancer buildLoadBalancer(List<HealthService> healthServices) {
return new RandomLoadBalancer(healthServices.stream()
.map(healthService -> {
HealthService.Service service =healthService.getService();
return new ServiceAddress(service.getAddress() , service.getPort());
})
.collect(Collectors.toList()));
}
}
這裏我用到了LoadBalancer, 關於LB這塊,後續文章會專門講述。 除此之外, 這裏的核心是在獲取完服務地址之後,會watch該服務地址的變化, 並更新對應的LB中的地址列表。
就這樣, 一個簡單的服務註冊與發現功能就實現了。 完整代碼請看我的github。