服務發現方案梳理及NetflixEureka簡介

問題

  • CAP知足哪幾部分html

  • failover方式是怎樣java

  • 語言機制node

服務發現梳理

Open-Source Service Discovery

clipboard.png

zk方案-對後端系統規模上升的一些思考

  • DNS
    最原始的配置文件和 DNS 來作服務發現,Host、端口都是寫在配置文件裏的,發生變動的時候只能修改配置文件並重啓服務。因此當某臺機器掛掉的時候,依賴它上面服務的其餘系統也都所有會出問題。而應急的步驟都是先在別的機器上運行新的實例,修改配置文件並重啓關聯的其餘系統。這樣作費時、費力、且會有一個時間窗口內系統沒法提供服務。git

  • 經過 Nginx 來作了負載均衡/主備的
    這樣作仍是有兩個問題:(1)Nginx 自己成爲一個故障點(2)鏈接數量翻倍,其中第二個問題曾致使咱們的環境出現了 nf_conntrack table full 的問題。咱們的關鍵服務都是多實例負載均衡的,當系統併發上升到必定程度的時候,某些服務器,尤爲是跑着 Nginx 的機器很容易出現這個錯誤。github

  • zk
    服務實例註冊的 Node 類型是 ephemeral node,這種類型的節點只有在客戶端保持着鏈接的時候纔有效。因此當某個服務實例被中止或者出現網絡異常的時候,對應的節點也會被刪掉。所以,任什麼時候候從 ZooKeeper 裏查詢到的都是當前活躍的實例。藉助 ZooKeeper 的推送功能,服務的消費者能夠得知實例的變化,從而能夠從容應對服務實例的宕機和新實例的添加,無需重啓。spring

  • SmartStack: Airbnb的自動服務發現和註冊框架後端

    • DNS 變動延遲問題緩存

    • 中心化負載均衡,單點問題安全

    • zk,多語言問題服務器

    • SmartStack,在zookeeper和haproxy上封裝一層

服務發現:etcd vs Consul vs Zookeeper

  • etcd(coreos開發,系統級別的)
    etcd是一個採用HTTP協議的健/值對存儲系統,它是一個分佈式和功能層次配置系統,可用於構建服務發現系統。其很容易部署、安裝和使用,提供了可靠的數據持久化特性。它是安全的而且文檔也十分齊全。etcd比Zookeeper是比更好的選擇,由於它很簡單,然而,它須要搭配一些第三方工具才能夠提供服務發現功能

  • consul(go語言寫的)
    Consul是強一致性的數據存儲,使用gossip造成動態集羣。它提供分級鍵/值存儲方式,不只能夠存儲數據,並且能夠用於註冊器件事各類任務,從發送數據改變通知到運行健康檢查和自定義命令,具體如何取決於它們的輸出。與Zookeeper和etcd不同,Consul內嵌實現了服務發現系統,因此這樣就不須要構建本身的系統或使用第三方系統。這一發現系統除了上述提到的特性以外,還包括節點健康檢查和運行在其上的服務。Zookeeper和etcd只提供原始的鍵/值隊存儲,要求應用程序開發人員構建他們本身的系統提供服務發現功能。而Consul提供了一個內置的服務發現的框架。客戶只須要註冊服務並經過DNS或HTTP接口執行服務發現。其餘兩個工具須要一個親手製做的解決方案或藉助於第三方工具。Consul爲多種數據中心提供了開箱即用的原生支持,其中的gossip系統不只能夠工做在同一集羣內部的各個節點,並且還能夠跨數據中心工做。

Netflix的Eureka方案

clipboard.png

Eureka 由兩個組件組成: Eureka 服務器 和 Eureka 客戶端 。Eureka 服務器用做服務註冊服務器。Eureka 客戶端是一個 java 客戶端,用來簡化與服務器的交互、做爲輪詢負載均衡器,並提供服務的故障切換支持。Netflix 在其生產環境中使用的是另外的客戶端,它提供基於流量、資源利用率以及出錯狀態的加權負載均衡。
當一箇中間層服務首次啓動時,他會將本身註冊到 Eureka 中,以便讓客戶端找到它,同時每 30 秒發送一次心跳。若是一個服務在幾分鐘內沒有發送心跳,它將從全部 Eureka 節點上註銷。一個 Amazon 域中能夠有一個 Eureka 節點集羣,每一個可用區(Availability Zone)至少有一個 Eureka 節點。AWS 的域相互之間是隔離的。

爲何不該該使用ZooKeeper作服務發現

  • zk是知足CP犧牲A,這個不對,看ZooKeeper和CAP理論及一致性原則 ,其實zk只是知足最終一致性C,可用性A這個是保證的,而且保證一半的節點是最新的數據,分區性P這個得看節點多少及讀寫狀況,節點多,則寫耗時長,另外節點多了Leader選舉很是耗時, 就會放大網絡的問題,容易分區。

  • 對於Service發現服務而言,寧肯返回某服務5分鐘以前在哪幾個服務器上可用的信息,也不能由於暫時的網絡故障而找不到可用的服務器,而不返回任何結果。因此說,用ZooKeeper來作Service發現服務是確定錯誤的。總結一句就是,service不是強一致的,因此會有部分狀況下沒發現新服務致使請求出錯。當部分或者全部節點跟ZooKeeper斷開的狀況下,每一個節點還能夠從本地緩存中獲取到數據;可是,即使如此,ZooKeeper下全部節點不可能保證任什麼時候候都能緩存全部的服務註冊信息。若是ZooKeeper下全部節點都斷開了,或者集羣中出現了網絡分割的故障(注:因爲交換機故障致使交換機底下的子網間不能互訪);那麼ZooKeeper會將它們都從本身管理範圍中剔除出去,外界就不能訪問到這些節點了,即使這些節點自己是「健康」的,能夠正常提供服務的;因此致使到達這些節點的服務請求被丟失了。

  • Eureka處理網絡問題致使分區。若是Eureka服務節點在短期裏丟失了大量的心跳鏈接(注:可能發生了網絡故障),那麼這個Eureka節點會進入」自我保護模式「,同時保留那些「心跳死亡「的服務註冊信息不過時。此時,這個Eureka節點對於新的服務還能提供註冊服務,對於」死亡「的仍然保留,以防還有客戶端向其發起請求。當網絡故障恢復後,這個Eureka節點會退出」自我保護模式「。因此Eureka的哲學是,同時保留」好數據「與」壞數據「總比丟掉任何」好數據「要更好,因此這種模式在實踐中很是有效。

  • Eureka就是爲發現服務所設計的,它有獨立的客戶端程序庫,同時提供心跳服務、服務健康監測、自動發佈服務與自動刷新緩存的功能。可是,若是使用ZooKeeper你必須本身來實現這些功能。

Eureka一致性分析

/**
     * Replicates all eureka actions to peer eureka nodes except for replication
     * traffic to this node.
     *
     */
    private void replicateToPeers(Action action, String appName, String id,
            InstanceInfo info /* optional */,
            InstanceStatus newStatus /* optional */, boolean isReplication) {
        Stopwatch tracer = action.getTimer().start();
        try {
 
            if (isReplication) {
                numberOfReplicationsLastMin.increment();
            }
            // If it is a replication already, do not replicate again as this
            // will create a poison replication
            if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
                return;
            }
 
            for (final PeerEurekaNode node : peerEurekaNodes.get()) {
                // If the url represents this host, do not replicate
                // to yourself.
                if (isThisMe(node.getServiceUrl())) {
                    continue;
                }
                replicateInstanceActionsToPeers(action, appName, id, info,
                        newStatus, node);
            }
        } finally {
            tracer.stop();
        }
    }

觸發的時機至關於熱備:每增刪改一次,就同步一次。而後默認是走url的第一個來查的,而後

clipboard.png

調用的時候,第一個掛了,自動去找第二,詳見

/**
     * Makes remote calls with the corresponding action(register,renew etc).
     *
     * @param action
     *            the action to be performed on eureka server.
     * @return ClientResponse the HTTP response object.
     * @throws Throwable
     *             on any error.
     */
    private ClientResponse makeRemoteCall(Action action) throws Throwable {
        return makeRemoteCall(action, 0);
    }

具體內部catch異常後,遞歸調用

/**
     * Makes remote calls with the corresponding action(register,renew etc).
     *
     * @param action
     *            the action to be performed on eureka server.
     *
     *            Try the fallback servers in case of problems communicating to
     *            the primary one.
     *
     * @return ClientResponse the HTTP response object.
     * @throws Throwable
     *             on any error.
     */
    private ClientResponse makeRemoteCall(Action action, int serviceUrlIndex)
            throws Throwable {
        String urlPath = null;
        Stopwatch tracer = null;
        String serviceUrl = eurekaServiceUrls.get().get(serviceUrlIndex);
        ClientResponse response = null;
        logger.debug("Discovery Client talking to the server {}", serviceUrl);
        try {
            // If the application is unknown do not register/renew/cancel but
            // refresh
            if ((UNKNOWN.equals(instanceInfo.getAppName())
                    && (!Action.Refresh.equals(action)) && (!Action.Refresh_Delta
                    .equals(action)))) {
                return null;
            }
            WebResource r = discoveryApacheClient.resource(serviceUrl);
            String remoteRegionsToFetchStr;
            switch (action) {
            case Renew:
                tracer = RENEW_TIMER.start();
                urlPath = "apps/" + appPathIdentifier;
                response = r
                        .path(urlPath)
                        .queryParam("status",
                                instanceInfo.getStatus().toString())
                        .queryParam("lastDirtyTimestamp",
                                instanceInfo.getLastDirtyTimestamp().toString())
                        .put(ClientResponse.class);
                break;
            case Refresh:
                tracer = REFRESH_TIMER.start();
                final String vipAddress = clientConfig.getRegistryRefreshSingleVipAddress();
                urlPath = vipAddress == null ? "apps/" : "vips/" + vipAddress;
                remoteRegionsToFetchStr = remoteRegionsToFetch.get();
                if (!Strings.isNullOrEmpty(remoteRegionsToFetchStr)) {
                    urlPath += "?regions=" + remoteRegionsToFetchStr;
                }
                response = getUrl(serviceUrl + urlPath);
                break;
            case Refresh_Delta:
                tracer = REFRESH_DELTA_TIMER.start();
                urlPath = "apps/delta";
                remoteRegionsToFetchStr = remoteRegionsToFetch.get();
                if (!Strings.isNullOrEmpty(remoteRegionsToFetchStr)) {
                    urlPath += "?regions=" + remoteRegionsToFetchStr;
                }
                response = getUrl(serviceUrl + urlPath);
                break;
            case Register:
                tracer = REGISTER_TIMER.start();
                urlPath = "apps/" + instanceInfo.getAppName();
                response = r.path(urlPath)
                        .type(MediaType.APPLICATION_JSON_TYPE)
                        .post(ClientResponse.class, instanceInfo);
                break;
            case Cancel:
                tracer = CANCEL_TIMER.start();
                urlPath = "apps/" + appPathIdentifier;
                response = r.path(urlPath).delete(ClientResponse.class);
                // Return without during de-registration if it is not registered
                // already and if we get a 404
                if ((!isRegisteredWithDiscovery)
                        && (response.getStatus() == Status.NOT_FOUND
                                .getStatusCode())) {
                    return response;
                }
                break;
            }
 
            if (logger.isDebugEnabled()) {
                logger.debug("Finished a call to service url {} and url path {} with status code {}.",
                            new String[] {serviceUrl, urlPath, String.valueOf(response.getStatus())});
            }
            if (isOk(action, response.getStatus())) {
                return response;
            } else {
                logger.warn("Action: " + action + "  => returned status of "
                        + response.getStatus() + " from " + serviceUrl
                        + urlPath);
                throw new RuntimeException("Bad status: "
                        + response.getStatus());
            }
        } catch (Throwable t) {
            closeResponse(response);
            String msg = "Can't get a response from " + serviceUrl + urlPath;
            if (eurekaServiceUrls.get().size() > (++serviceUrlIndex)) {
                logger.warn(msg, t);
                logger.warn("Trying backup: "
                        + eurekaServiceUrls.get().get(serviceUrlIndex));
                SERVER_RETRY_COUNTER.increment();
                return makeRemoteCall(action, serviceUrlIndex);
            } else {
                ALL_SERVER_FAILURE_COUNT.increment();
                logger.error(
                        msg
                                + "\nCan't contact any eureka nodes - possibly a security group issue?",
                        t);
                throw t;
            }
        } finally {
            if (tracer != null) {
                tracer.stop();
            }
        }
    }

參考

相關文章
相關標籤/搜索