如何寫一個RPC框架(三):服務註冊與服務發現

在後續一段時間裏, 我會寫一系列文章來講述如何實現一個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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章