国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

Nacos2.0配置灰度發(fā)布原理源碼解析

共 101928字,需瀏覽 204分鐘

 ·

2021-09-02 08:18

本文作者:寧與(包冬慶),目前在【阿里云云原生中間件】團(tuán)隊(duì)實(shí)習(xí)

今天分享的是我們組的一個(gè)實(shí)習(xí)生寫(xiě)的一篇源碼解析文章,小伙子實(shí)習(xí)期間在社區(qū)Nacos2.0的基礎(chǔ)上對(duì)灰度發(fā)布的能力進(jìn)行了增強(qiáng),并完成了MSE Nacos2.0上從管控到內(nèi)核的灰度發(fā)布能力的研發(fā)。以下是他對(duì)配置發(fā)布流程的代碼解析,相信看完之后你會(huì)感嘆:現(xiàn)在的實(shí)習(xí)生都有這個(gè)水平了嗎?

說(shuō)到灰度發(fā)布,就不得不提到阿里的安全生產(chǎn)三板斧:可監(jiān)控、可灰度、可回滾。在阿里內(nèi)部,對(duì)于安全生產(chǎn)是高度重視的,灰度可以說(shuō)是發(fā)布之前的必備流程。因此,作為阿里的配置中心,Nacos同樣支持了配置灰度的功能,可以通過(guò)控制臺(tái)進(jìn)行配置的灰度推送、回滾,從而實(shí)現(xiàn)安全的配置發(fā)布。一般來(lái)說(shuō),我們按照下圖所示流程進(jìn)行配置的安全修改。只有在小規(guī)模機(jī)器上驗(yàn)證配置按預(yù)期生效之后才會(huì)正式發(fā)布配置,否則就回滾灰度配置。

發(fā)布流程

配置灰度發(fā)布流程

社區(qū)Nacos的灰度是基于IP的方式進(jìn)行的,用戶需要在控制臺(tái),選擇需要灰度的配置,然后新建灰度配置,選擇灰度機(jī)器的IP進(jìn)行配置推送。整個(gè)交互流程如下圖所示。

IP灰度機(jī)制

具體的使用方法,如果使用的是自建的社區(qū)Nacos,可以訪問(wèn)http://ip:port/nacos進(jìn)入控制臺(tái),在配置管理的編輯頁(yè)面進(jìn)行配置灰度發(fā)布,如下圖。

社區(qū)Nacos控制臺(tái)

如果使用的是阿里云的MSE微服務(wù)引擎,可以查看MSE配置灰度發(fā)布幫助文檔了解使用方法,目前在Nacos2.0專業(yè)版上已經(jīng)支持灰度功能,在MSE控制臺(tái)打開(kāi)Beta按鈕即可,如下圖所示。

MSE Beta發(fā)布

Nacos灰度原理

Nacos的灰度發(fā)布原理其實(shí)并不復(fù)雜,本質(zhì)就如同下面這張流程圖。

灰度原理

乍一看,這個(gè)流程好復(fù)雜,實(shí)際上定睛一看,好像也沒(méi)啥。整個(gè)過(guò)程就是Client、Server和Console之間的交互。Client端監(jiān)聽(tīng)Server上的配置,建立長(zhǎng)連接并上報(bào)自己的客戶端信息,例如IP地址。Console負(fù)責(zé)進(jìn)行配置灰度的調(diào)用,將用戶所需要的灰度配置請(qǐng)求發(fā)送到Server端。然后Server端根據(jù)用戶的灰度配置請(qǐng)求中的IP地址,過(guò)濾與客戶端的長(zhǎng)連接,然后將灰度配置定向推送到對(duì)應(yīng)IP的客戶端中即可。下面筆者從長(zhǎng)連接的建立到配置灰度,進(jìn)行詳細(xì)的源碼分析。

長(zhǎng)連接建立

在Nacos2.0版本之前,Nacos主要采用長(zhǎng)輪詢的方式在客戶端拉取服務(wù)端的配置信息。而在Nacos2.0版本中,引入了基于gRPC的長(zhǎng)連接模型來(lái)提升配置監(jiān)聽(tīng)的性能,客戶端和服務(wù)端會(huì)建立長(zhǎng)連接來(lái)監(jiān)聽(tīng)配置的變更,一旦服務(wù)端有配置變更,就會(huì)將配置信息推送到客戶端中。在Nacos源碼中,這一過(guò)程主要涉及到兩個(gè)組件之間的交互,即com.alibaba.nacos.common.remote.client.grpc包下的GrpcSdkClient類和com.alibaba.nacos.core.remote.grpc包下的GrpcBiStreamRequestAcceptor類。然而,GrpcSdkClient中沒(méi)有定義具體的連接邏輯,其主要邏輯在其父類GrpcClient中。下面這段代碼就是客戶端連接服務(wù)端的核心代碼,位于GrpcClient的connectToServer方法。

    @Override
    public Connection connectToServer(ServerInfo serverInfo) {
        try {
            // ......
            int port = serverInfo.getServerPort() + rpcPortOffset();

            // 創(chuàng)建一個(gè)Grpc的Stub
            RequestGrpc.RequestFutureStub newChannelStubTemp = createNewChannelStub(serverInfo.getServerIp(), port);

            if (newChannelStubTemp != null) {

                // 檢查服務(wù)端是否可用
                Response response = serverCheck(serverInfo.getServerIp(), port, newChannelStubTemp);
                if (response == null || !(response instanceof ServerCheckResponse)) {
                    shuntDownChannel((ManagedChannel) newChannelStubTemp.getChannel());
                    return null;
                }

                // 創(chuàng)建一個(gè)Grpc的Stream
                BiRequestStreamGrpc.BiRequestStreamStub biRequestStreamStub = BiRequestStreamGrpc
                    .newStub(newChannelStubTemp.getChannel());

                // 創(chuàng)建連接信息,保存Grpc的連接信息,也就是長(zhǎng)連接的一個(gè)holder
                GrpcConnection grpcConn = new GrpcConnection(serverInfo, grpcExecutor);
                grpcConn.setConnectionId(((ServerCheckResponse) response).getConnectionId());

                // 創(chuàng)建stream請(qǐng)求同時(shí)綁定到當(dāng)前連接中
                StreamObserver<Payload> payloadStreamObserver = bindRequestStream(biRequestStreamStub, grpcConn);

                // 綁定Grpc相關(guān)連接信息
                grpcConn.setPayloadStreamObserver(payloadStreamObserver);
                grpcConn.setGrpcFutureServiceStub(newChannelStubTemp);
                grpcConn.setChannel((ManagedChannel) newChannelStubTemp.getChannel());

                // 發(fā)送一個(gè)初始化連接請(qǐng)求,用于上報(bào)客戶端的一些信息,例如標(biāo)簽、客戶端版本等
                ConnectionSetupRequest conSetupRequest = new ConnectionSetupRequest();
                conSetupRequest.setClientVersion(VersionUtils.getFullClientVersion());
                conSetupRequest.setLabels(super.getLabels());
                conSetupRequest.setAbilities(super.clientAbilities);
                conSetupRequest.setTenant(super.getTenant());
                grpcConn.sendRequest(conSetupRequest);

                // 等待連接建立成功
                Thread.sleep(100L);
                return grpcConn;
            }
            return null;
        } catch (Exception e) {
            LOGGER.error("[{}]Fail to connect to server!,error={}", GrpcClient.this.getName(), e);
        }
        return null;
    }

上面這段代碼主要功能有兩個(gè),一個(gè)是與服務(wù)端建立gRPC的長(zhǎng)連接,另一個(gè)功能主要是初始化連接,后者是實(shí)現(xiàn)配置灰度發(fā)布的前提。在上文中有提到,配置灰度發(fā)布的過(guò)程中,需要根據(jù)控制臺(tái)的灰度配置請(qǐng)求中的IP信息過(guò)濾長(zhǎng)連接,在服務(wù)端就是根據(jù)連接建立初始化時(shí)上報(bào)的信息實(shí)現(xiàn)的過(guò)濾。從上面的代碼中可以看到,ConnectionSetupRequest作為一個(gè)初始化請(qǐng)求,攜帶著客戶端版本、標(biāo)簽等信息,但是好像并沒(méi)有攜帶IP地址的信息。實(shí)際上,ConnectionSetupRequest也確實(shí)沒(méi)有攜帶IP地址信息。因?yàn)樵贜acos設(shè)計(jì)中,采用Request來(lái)表明客戶端的請(qǐng)求信息,而IP地址更像是屬于連接層的信息,應(yīng)該屬于連接的元信息,因此并沒(méi)有放在Request中進(jìn)行顯式的設(shè)置,而是在發(fā)送請(qǐng)求時(shí)自動(dòng)的作為Metadata信息發(fā)送到服務(wù)端中。可以看一下com.alibaba.nacos.common.remote.client.grpc包下的GrpcConnection的sendRequest方法,該方法接收一個(gè)Request請(qǐng)求作為參數(shù),將請(qǐng)求發(fā)送給服務(wù)端。

    public void sendRequest(Request request) {
        // 將request轉(zhuǎn)換為Grpc的Payload
        Payload convert = GrpcUtils.convert(request);
        // 通過(guò)Grpc的流發(fā)送請(qǐng)求
        payloadStreamObserver.onNext(convert);
    }

IP地址的設(shè)置,就在com.alibaba.nacos.common.remote.client.grpc包下的GrpcUtils的convert方法中,該方法主要將一個(gè)Request轉(zhuǎn)換為gRPC的Payload。

    /**
     * convert request to payload.
     *
     * @param request request.
     * @return payload.
     */

    public static Payload convert(Request request) {
        // 設(shè)置元信息
        Metadata newMeta = Metadata.newBuilder().setType(request.getClass().getSimpleName())
                .setClientIp(NetUtils.localIP()).putAllHeaders(request.getHeaders()).build();
        request.clearHeaders();
        
        // 轉(zhuǎn)換為json
        String jsonString = toJson(request);
        
        Payload.Builder builder = Payload.newBuilder();
     // 創(chuàng)建Payload
        return builder
                .setBody(Any.newBuilder().setValue(ByteString.copyFrom(jsonString, Charset.forName(Constants.ENCODE))))
                .setMetadata(newMeta).build();
        
    }

可以看到,這里通過(guò)NetUtils.localIP()方法獲取客戶端的IP信息,并存入到Metadata中,跟隨Payload一起上報(bào)給服務(wù)端。到這里,客戶端這里的連接過(guò)程就暫時(shí)完成了,下面介紹一下服務(wù)端接收到連接請(qǐng)求的響應(yīng)過(guò)程。

在服務(wù)端,主要通過(guò)GrpcBiStreamRequestAcceptor的requestBiStream方法接收客戶端請(qǐng)求,如下所示。

    @Override
    public StreamObserver<Payload> requestBiStream(StreamObserver<Payload> responseObserver) {
        
        StreamObserver<Payload> streamObserver = new StreamObserver<Payload>() {
            
            final String connectionId = CONTEXT_KEY_CONN_ID.get();
            
            final Integer localPort = CONTEXT_KEY_CONN_LOCAL_PORT.get();
            
            final int remotePort = CONTEXT_KEY_CONN_REMOTE_PORT.get();
            
            String remoteIp = CONTEXT_KEY_CONN_REMOTE_IP.get();
            
            String clientIp = "";
            
            @Override
            public void onNext(Payload payload) {
                // 獲取客戶端IP
                clientIp = payload.getMetadata().getClientIp();
                traceDetailIfNecessary(payload);
                
                Object parseObj;
                try {
                    parseObj = GrpcUtils.parse(payload);
                } catch (Throwable throwable) {
                    Loggers.REMOTE_DIGEST
                            .warn("[{}]Grpc request bi stream,payload parse error={}", connectionId, throwable);
                    return;
                }
                
                if (parseObj == null) {
                    Loggers.REMOTE_DIGEST
                            .warn("[{}]Grpc request bi stream,payload parse null ,body={},meta={}", connectionId,
                                    payload.getBody().getValue().toStringUtf8(), payload.getMetadata());
                    return;
                }
                
                // 處理初始化請(qǐng)求
                if (parseObj instanceof ConnectionSetupRequest) {
                    ConnectionSetupRequest setUpRequest = (ConnectionSetupRequest) parseObj;
                    Map<String, String> labels = setUpRequest.getLabels();
                    String appName = "-";
                    if (labels != null && labels.containsKey(Constants.APPNAME)) {
                        appName = labels.get(Constants.APPNAME);
                    }
                    
                    ConnectionMeta metaInfo = new ConnectionMeta(connectionId, payload.getMetadata().getClientIp(),
                            remoteIp, remotePort, localPort, ConnectionType.GRPC.getType(),
                            setUpRequest.getClientVersion(), appName, setUpRequest.getLabels());
                    metaInfo.setTenant(setUpRequest.getTenant());
                    
                    // 服務(wù)端的長(zhǎng)連接信息holder
                    Connection connection = new GrpcConnection(metaInfo, responseObserver, CONTEXT_KEY_CHANNEL.get());
                    connection.setAbilities(setUpRequest.getAbilities());
                    boolean rejectSdkOnStarting = metaInfo.isSdkSource() && !ApplicationUtils.isStarted();
                    
                    // 注冊(cè)connection到connectionManager中
                    if (rejectSdkOnStarting || !connectionManager.register(connectionId, connection)) {
                        //Not register to the connection manager if current server is over limit or server is starting.
                        try {
                            Loggers.REMOTE_DIGEST.warn("[{}]Connection register fail,reason:{}", connectionId,
                                    rejectSdkOnStarting ? " server is not started" : " server is over limited.");
                            connection.request(new ConnectResetRequest(), 3000L);
                            connection.close();
                        } catch (Exception e) {
                            //Do nothing.
                            if (connectionManager.traced(clientIp)) {
                                Loggers.REMOTE_DIGEST
                                        .warn("[{}]Send connect reset request error,error={}", connectionId, e);
                            }
                        }
                    }
                    
                } else if (parseObj instanceof Response) {
                    Response response = (Response) parseObj;
                    if (connectionManager.traced(clientIp)) {
                        Loggers.REMOTE_DIGEST
                                .warn("[{}]Receive response of server request  ,response={}", connectionId, response);
                    }
                    RpcAckCallbackSynchronizer.ackNotify(connectionId, response);
                    connectionManager.refreshActiveTime(connectionId);
                } else {
                    Loggers.REMOTE_DIGEST
                            .warn("[{}]Grpc request bi stream,unknown payload receive ,parseObj={}", connectionId,
                                    parseObj);
                }
                
            }
            
            // ......
        };
        
        return streamObserver;
    }

這里我們主要看onNext方法,其負(fù)責(zé)處理客戶端的請(qǐng)求信息,即Payload信息。如果是初始化連接的請(qǐng)求ConnectionSetupRequest,就會(huì)記錄與客戶端之間的長(zhǎng)連接信息,并注冊(cè)到ConnectionManager中。ConnectionManager是服務(wù)端維護(hù)所有客戶端連接信息的類,持有所有的長(zhǎng)連接信息,后續(xù)的配置推送等都需要通過(guò)ConnectionManager獲取長(zhǎng)連接信息??梢院?jiǎn)單看一下ConnectionManager的源碼,在com.alibaba.nacos.core.remote包下,如下所示。

/**
 * connect manager.
 *
 * @author liuzunfei
 * @version $Id: ConnectionManager.java, v 0.1 2020年07月13日 7:07 PM liuzunfei Exp $
 */

@Service
public class ConnectionManager extends Subscriber<ConnectionLimitRuleChangeEvent{
    
    // ......
    
    Map<String, Connection> connections = new ConcurrentHashMap<String, Connection>();
    
    // ......
    
    /**
     * register a new connect.
     *
     * @param connectionId connectionId
     * @param connection   connection
     */

    public synchronized boolean register(String connectionId, Connection connection) {
        
        if (connection.isConnected()) {
            if (connections.containsKey(connectionId)) {
                return true;
            }
            if (!checkLimit(connection)) {
                return false;
            }
            if (traced(connection.getMetaInfo().clientIp)) {
                connection.setTraced(true);
            }
            // 注冊(cè)connection
            connections.put(connectionId, connection);
            connectionForClientIp.get(connection.getMetaInfo().clientIp).getAndIncrement();
            
            clientConnectionEventListenerRegistry.notifyClientConnected(connection);
            Loggers.REMOTE_DIGEST
                    .info("new connection registered successfully, connectionId = {},connection={} ", connectionId,
                            connection);
            return true;
            
        }
        return false;
        
    }
    
    // ......
    
}

可以看到,在ConnectionManager中,維護(hù)了一個(gè)Map。在調(diào)用register方法時(shí),將Connection注冊(cè)到Map中,以供后續(xù)的邏輯使用。這里有一個(gè)細(xì)節(jié),注冊(cè)到ConnectionManager中的GrpcConnection與客戶端持有的GrpcConnection不是一個(gè)類。這里的GrpcConnection位于com.alibaba.nacos.core.remote.grpc包,而客戶端的GrpcConnection位于com.alibaba.nacos.common.remote.client.grpc包。事實(shí)上與客戶端有關(guān)的gRPC相關(guān)的類都在com.alibaba.nacos.common.remote.client.grpc。com.alibaba.nacos.core.remote.grpc則是服務(wù)端的相關(guān)實(shí)現(xiàn)。

到這里,長(zhǎng)連接建立的核心流程已經(jīng)介紹完了,接下來(lái)筆者將詳細(xì)介紹一下配置灰度的推送過(guò)程,由于Nacos在這里使用了發(fā)布訂閱模式以及異步的方法調(diào)用,理解起來(lái)可能稍微要麻煩一點(diǎn)。

灰度推送

在Nacos中,提供了一組OpenAPI進(jìn)行配置的管理,配置灰度發(fā)布也是其中一個(gè)功能,可以在com.alibaba.nacos.config.server.controller包下的ConfigController中查看,包括了BetaConfig的發(fā)布、停止和查詢,接下來(lái)筆者將會(huì)一一介紹他們的原理。

創(chuàng)建BetaConfig

創(chuàng)建BetaConfig的API代碼如下,一個(gè)簡(jiǎn)單的Web的API。

    /**
     * Adds or updates non-aggregated data.
     *
     * @throws NacosException NacosException.
     */

    @PostMapping
    @Secured(action = ActionTypes.WRITE, parser = ConfigResourceParser.class)
    public Boolean publishConfig(HttpServletRequest requestHttpServletResponse response,
            @RequestParam(value 
"dataId") String dataId, @RequestParam(value = "group") String group,
            @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
            @RequestParam(value = "content") String content, @RequestParam(value = "tag", required = false) String tag,
            @RequestParam(value = "appName", required = false) String appName,
            @RequestParam(value = "src_user", required = false) String srcUser,
            @RequestParam(value = "config_tags", required = false) String configTags,
            @RequestParam(value = "desc", required = false) String desc,
            @RequestParam(value = "use", required = false) String use,
            @RequestParam(value = "effect", required = false) String effect,
            @RequestParam(value = "type", required = false) String type,
            @RequestParam(value = "schema", required = false) String schema) throws NacosException {
        
        final String srcIp = RequestUtil.getRemoteIp(request);
        final String requestIpApp = RequestUtil.getAppName(request);
        srcUser = RequestUtil.getSrcUserName(request);
        //check type
        if (!ConfigType.isValidType(type)) {
            type = ConfigType.getDefaultType().getType();
        }
        // check tenant
        ParamUtils.checkTenant(tenant);
        ParamUtils.checkParam(dataId, group, "datumId", content);
        ParamUtils.checkParam(tag);
        Map<String, Object> configAdvanceInfo = new HashMap<String, Object>(10);
        MapUtil.putIfValNoNull(configAdvanceInfo, "config_tags", configTags);
        MapUtil.putIfValNoNull(configAdvanceInfo, "desc", desc);
        MapUtil.putIfValNoNull(configAdvanceInfo, "use", use);
        MapUtil.putIfValNoNull(configAdvanceInfo, "effect", effect);
        MapUtil.putIfValNoNull(configAdvanceInfo, "type", type);
        MapUtil.putIfValNoNull(configAdvanceInfo, "schema", schema);
        ParamUtils.checkParam(configAdvanceInfo);
        
        if (AggrWhitelist.isAggrDataId(dataId)) {
            LOGGER.warn("[aggr-conflict] {} attempt to publish single data, {}, {}", RequestUtil.getRemoteIp(request),
                    dataId, group);
            throw new NacosException(NacosException.NO_RIGHT, "dataId:" + dataId + " is aggr");
        }
        
        final Timestamp time = TimeUtils.getCurrentTime();
        
        // 目標(biāo)灰度機(jī)器的IP地址。
        String betaIps = request.getHeader("betaIps");
        
        ConfigInfo configInfo = new ConfigInfo(dataId, group, tenant, appName, content);
        configInfo.setType(type);
        if (StringUtils.isBlank(betaIps)) {
            if (StringUtils.isBlank(tag)) {
                persistService.insertOrUpdate(srcIp, srcUser, configInfo, time, configAdvanceInfo, false);
                ConfigChangePublisher
                        .notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));
            } else {
                persistService.insertOrUpdateTag(configInfo, tag, srcIp, srcUser, time, false);
                ConfigChangePublisher.notifyConfigChange(
                        new ConfigDataChangeEvent(false, dataId, group, tenant, tag, time.getTime()));
            }
        } else {
            // 發(fā)布Beta 配置
            persistService.insertOrUpdateBeta(configInfo, betaIps, srcIp, srcUser, time, false);
            
            // 通知配置變更
            ConfigChangePublisher
                    .notifyConfigChange(new ConfigDataChangeEvent(true, dataId, group, tenant, time.getTime()));
        }
        ConfigTraceService
                .logPersistenceEvent(dataId, group, tenant, requestIpApp, time.getTime(), InetUtils.getSelfIP(),
                        ConfigTraceService.PERSISTENCE_EVENT_PUB, content);
        return true;
    }

該方法接收一個(gè)創(chuàng)建配置的請(qǐng)求,包括配置的data-id、content等信息。從代碼中可以看出,該方法是通過(guò)判斷請(qǐng)求的Header中有無(wú)betaIps的值來(lái)確定是發(fā)布正式配置還是Beta配置的。如果betaIps的值不為空,則表明待發(fā)布的配置是一個(gè)Beta配置。而配置發(fā)布的過(guò)程,實(shí)際上就是把配置插入或者更新到數(shù)據(jù)庫(kù)中。在Nacos中,正式配置和灰度配置是分別存儲(chǔ)在不同的表中的,一旦發(fā)布就會(huì)通過(guò)ConfigChangePublisher發(fā)布一個(gè)ConfigDataChangeEvent事件,然后由訂閱了該事件的監(jiān)聽(tīng)者推送配置信息到客戶端。ConfigDataChangeEvent的監(jiān)聽(tīng)者是AsyncNotifyService類,位于com.alibaba.nacos.config.server.service.notify包下,該類主要用作執(zhí)行集群之間的數(shù)據(jù)Dump操作。該類在初始化的時(shí)候,會(huì)向事件中心NotifyCenter注冊(cè)一個(gè)監(jiān)聽(tīng)者,用以監(jiān)聽(tīng)數(shù)據(jù)變更事件并異步執(zhí)行數(shù)據(jù)的Dump操作,如下所示。

/**
 * Async notify service.
 *
 * @author Nacos
 */

@Service
public class AsyncNotifyService {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(AsyncNotifyService.class);
    
    private final NacosAsyncRestTemplate nacosAsyncRestTemplate = HttpClientManager.getNacosAsyncRestTemplate();
    
    private static final int MIN_RETRY_INTERVAL = 500;
    
    private static final int INCREASE_STEPS = 1000;
    
    private static final int MAX_COUNT = 6;
    
    @Autowired
    private DumpService dumpService;
    
    @Autowired
    private ConfigClusterRpcClientProxy configClusterRpcClientProxy;
    
    private ServerMemberManager memberManager;
    
    @Autowired
    public AsyncNotifyService(ServerMemberManager memberManager) {
        this.memberManager = memberManager;
        
        // Register ConfigDataChangeEvent to NotifyCenter.
        NotifyCenter.registerToPublisher(ConfigDataChangeEvent.classNotifyCenter.ringBufferSize);
        
        // Register A Subscriber to subscribe ConfigDataChangeEvent.
        NotifyCenter.registerSubscriber(new Subscriber() {
            
            @Override
            public void onEvent(Event event) {
                // Generate ConfigDataChangeEvent concurrently
                if (event instanceof ConfigDataChangeEvent) {
                    ConfigDataChangeEvent evt = (ConfigDataChangeEvent) event;
                    long dumpTs = evt.lastModifiedTs;
                    String dataId = evt.dataId;
                    String group = evt.group;
                    String tenant = evt.tenant;
                    String tag = evt.tag;
                    Collection<Member> ipList = memberManager.allMembers();
                    
                    // In fact, any type of queue here can be
                    Queue<NotifySingleTask> httpQueue = new LinkedList<NotifySingleTask>();
                    Queue<NotifySingleRpcTask> rpcQueue = new LinkedList<NotifySingleRpcTask>();
                    
                    for (Member member : ipList) {
                        // 判斷是否是長(zhǎng)輪詢
                        if (!MemberUtil.isSupportedLongCon(member)) {
                            // 添加一個(gè)長(zhǎng)輪詢的異步dump任務(wù)
                            httpQueue.add(new NotifySingleTask(dataId, group, tenant, tag, dumpTs, member.getAddress(),
                                    evt.isBeta));
                        } else {
                            // 添加一個(gè)長(zhǎng)連接的異步dump任務(wù)
                            rpcQueue.add(
                                    new NotifySingleRpcTask(dataId, group, tenant, tag, dumpTs, evt.isBeta, member));
                        }
                    }
                    // 判斷并執(zhí)行長(zhǎng)輪詢的異步dump任務(wù)
                    if (!httpQueue.isEmpty()) {
                        ConfigExecutor.executeAsyncNotify(new AsyncTask(nacosAsyncRestTemplate, httpQueue));
                    }
                    // 判斷并執(zhí)行長(zhǎng)連接的異步dump任務(wù)
                    if (!rpcQueue.isEmpty()) {
                        ConfigExecutor.executeAsyncNotify(new AsyncRpcTask(rpcQueue));
                    }
                    
                }
            }
            
            @Override
            public Class<? extends Event> subscribeType() {
                return ConfigDataChangeEvent.class;
            }
        });
    }
}

在接收到ConfigDataChangeEvent之后,如果Nacos2.0以上的版本,會(huì)創(chuàng)建一個(gè)RpcTask用以執(zhí)行配置變更的通知,由內(nèi)部類AsyncRpcTask執(zhí)行,AsyncRpcTask具體邏輯如下所示。

class AsyncRpcTask implements Runnable {
        
        private Queue<NotifySingleRpcTask> queue;
        
        public AsyncRpcTask(Queue<NotifySingleRpcTask> queue) {
            this.queue = queue;
        }
        
        @Override
        public void run() {
            while (!queue.isEmpty()) {
                NotifySingleRpcTask task = queue.poll();
                // 創(chuàng)建配置變更請(qǐng)求
                ConfigChangeClusterSyncRequest syncRequest = new ConfigChangeClusterSyncRequest();
                syncRequest.setDataId(task.getDataId());
                syncRequest.setGroup(task.getGroup());
                syncRequest.setBeta(task.isBeta);
                syncRequest.setLastModified(task.getLastModified());
                syncRequest.setTag(task.tag);
                syncRequest.setTenant(task.getTenant());
                
                Member member = task.member;
                // 如果是自身的數(shù)據(jù)變更,直接執(zhí)行dump操作
                if (memberManager.getSelf().equals(member)) {
                    if (syncRequest.isBeta()) {
                        // 同步Beta配置
                        dumpService.dump(syncRequest.getDataId(), syncRequest.getGroup(), syncRequest.getTenant(),
                                syncRequest.getLastModified(), NetUtils.localIP(), true);
                    } else {
                        // 同步正式配置
                        dumpService.dump(syncRequest.getDataId(), syncRequest.getGroup(), syncRequest.getTenant(),
                                syncRequest.getTag(), syncRequest.getLastModified(), NetUtils.localIP());
                    }
                    continue;
                }
                
                // 通知其他服務(wù)端進(jìn)行dump
                if (memberManager.hasMember(member.getAddress())) {
                    // start the health check and there are ips that are not monitored, put them directly in the notification queue, otherwise notify
                    boolean unHealthNeedDelay = memberManager.isUnHealth(member.getAddress());
                    if (unHealthNeedDelay) {
                        // target ip is unhealthy, then put it in the notification list
                        ConfigTraceService.logNotifyEvent(task.getDataId(), task.getGroup(), task.getTenant(), null,
                                task.getLastModified(), InetUtils.getSelfIP(), ConfigTraceService.NOTIFY_EVENT_UNHEALTH,
                                0, member.getAddress());
                        // get delay time and set fail count to the task
                        asyncTaskExecute(task);
                    } else {
    
                        if (!MemberUtil.isSupportedLongCon(member)) {
                            asyncTaskExecute(
                                    new NotifySingleTask(task.getDataId(), task.getGroup(), task.getTenant(), task.tag,
                                            task.getLastModified(), member.getAddress(), task.isBeta));
                        } else {
                            try {
                                configClusterRpcClientProxy
                                        .syncConfigChange(member, syncRequest, new AsyncRpcNotifyCallBack(task));
                            } catch (Exception e) {
                                MetricsMonitor.getConfigNotifyException().increment();
                                asyncTaskExecute(task);
                            }
                        }
                      
                    }
                } else {
                    //No nothig if  member has offline.
                }
                
            }
        }
    }

這里首先創(chuàng)建了一個(gè)ConfigChangeClusterSyncRequest,并將配置信息寫(xiě)入。然后獲取集群信息,通知相應(yīng)的Server處理的數(shù)據(jù)同步請(qǐng)求。同步配置變更信息的核心邏輯由DumpService來(lái)執(zhí)行。我們主要查看同步Beta配置的操作,DumpService的dump方法如下所示。

    /**
     * Add DumpTask to TaskManager, it will execute asynchronously.
     */

    public void dump(String dataId, String group, String tenant, long lastModified, String handleIp, boolean isBeta) {
        String groupKey = GroupKey2.getKey(dataId, group, tenant);
        String taskKey = String.join("+", dataId, group, tenant, String.valueOf(isBeta));
        dumpTaskMgr.addTask(taskKey, new DumpTask(groupKey, lastModified, handleIp, isBeta));
        DUMP_LOG.info("[dump-task] add task. groupKey={}, taskKey={}", groupKey, taskKey);
    }

在該方法中,這里會(huì)根據(jù)配置變更信息,提交一個(gè)異步的DumpTask任務(wù),后續(xù)會(huì)由DumpProcessor類的process方法進(jìn)行處理,該方法如下所示。

/**
 * dump processor.
 *
 * @author Nacos
 * @date 2020/7/5 12:19 PM
 */

public class DumpProcessor implements NacosTaskProcessor {
    
    final DumpService dumpService;
    
    public DumpProcessor(DumpService dumpService) {
        this.dumpService = dumpService;
    }
    
    @Override
    public boolean process(NacosTask task) {
        final PersistService persistService = dumpService.getPersistService();
        DumpTask dumpTask = (DumpTask) task;
        String[] pair = GroupKey2.parseKey(dumpTask.getGroupKey());
        String dataId = pair[0];
        String group = pair[1];
        String tenant = pair[2];
        long lastModified = dumpTask.getLastModified();
        String handleIp = dumpTask.getHandleIp();
        boolean isBeta = dumpTask.isBeta();
        String tag = dumpTask.getTag();
        
        ConfigDumpEvent.ConfigDumpEventBuilder build = ConfigDumpEvent.builder().namespaceId(tenant).dataId(dataId)
                .group(group).isBeta(isBeta).tag(tag).lastModifiedTs(lastModified).handleIp(handleIp);
        
        if (isBeta) {
            // 更新Beta配置的緩存
            ConfigInfo4Beta cf = persistService.findConfigInfo4Beta(dataId, group, tenant);
            
            build.remove(Objects.isNull(cf));
            build.betaIps(Objects.isNull(cf) ? null : cf.getBetaIps());
            build.content(Objects.isNull(cf) ? null : cf.getContent());
            
            return DumpConfigHandler.configDump(build.build());
        }
        if (StringUtils.isBlank(tag)) {
            ConfigInfo cf = persistService.findConfigInfo(dataId, group, tenant);

            build.remove(Objects.isNull(cf));
            build.content(Objects.isNull(cf) ? null : cf.getContent());
            build.type(Objects.isNull(cf) ? null : cf.getType());
        } else {
            ConfigInfo4Tag cf = persistService.findConfigInfo4Tag(dataId, group, tenant, tag);

            build.remove(Objects.isNull(cf));
            build.content(Objects.isNull(cf) ? null : cf.getContent());

        }
        return DumpConfigHandler.configDump(build.build());
    }
}

可以看到,如果是Beta配置,則獲取最新的Beta配置信息,然后觸發(fā)DumpConfigHandler的configDump方法。進(jìn)入configDump可以看到,該方法主要用來(lái)更新緩存的配置信息,調(diào)用ConfigCacheService的相關(guān)操作進(jìn)行配置的更新。

/**
 * Dump config subscriber.
 *
 * @author <a href="mailto:[email protected]">liaochuntao</a>
 */

public class DumpConfigHandler extends Subscriber<ConfigDumpEvent{
    
    /**
     * trigger config dump event.
     *
     * @param event {@link ConfigDumpEvent}
     * @return {@code true} if the config dump task success , else {@code false}
     */

    public static boolean configDump(ConfigDumpEvent event) {
        final String dataId = event.getDataId();
        final String group = event.getGroup();
        final String namespaceId = event.getNamespaceId();
        final String content = event.getContent();
        final String type = event.getType();
        final long lastModified = event.getLastModifiedTs();
        if (event.isBeta()) {
            boolean result = false;
            // 刪除操作
            if (event.isRemove()) {
                result = ConfigCacheService.removeBeta(dataId, group, namespaceId);
                if (result) {
                    ConfigTraceService.logDumpEvent(dataId, group, namespaceId, null, lastModified, event.getHandleIp(),
                            ConfigTraceService.DUMP_EVENT_REMOVE_OK, System.currentTimeMillis() - lastModified, 0);
                }
                return result;
            } else {
                // 更新或者發(fā)布
                result = ConfigCacheService
                        .dumpBeta(dataId, group, namespaceId, content, lastModified, event.getBetaIps());
                if (result) {
                    ConfigTraceService.logDumpEvent(dataId, group, namespaceId, null, lastModified, event.getHandleIp(),
                            ConfigTraceService.DUMP_EVENT_OK, System.currentTimeMillis() - lastModified,
                            content.length());
                }
            }
            
            return result;
        }
        
        // ......
        
    }
    
    @Override
    public void onEvent(ConfigDumpEvent event) {
        configDump(event);
    }
    
    @Override
    public Class<? extends Event> subscribeType() {
        return ConfigDumpEvent.class;
    }
}

在ConfigCacheService中,會(huì)對(duì)比配置信息,如果配置有變化,則發(fā)布事件LocalDataChangeEvent,觸發(fā)RpcConfigChangeNotifier的configDataChanged方法來(lái)推送配置,configDataChanged方法代碼如下。

/**
 * ConfigChangeNotifier.
 *
 * @author liuzunfei
 * @version $Id: ConfigChangeNotifier.java, v 0.1 2020年07月20日 3:00 PM liuzunfei Exp $
 */

@Component(value = "rpcConfigChangeNotifier")
public class RpcConfigChangeNotifier extends Subscriber<LocalDataChangeEvent{
    
    // ......
    
    @Autowired
    ConfigChangeListenContext configChangeListenContext;
    
    @Autowired
    private RpcPushService rpcPushService;
    
    @Autowired
    private ConnectionManager connectionManager;
    
    /**
     * adaptor to config module ,when server side config change ,invoke this method.
     *
     * @param groupKey groupKey
     */

    public void configDataChanged(String groupKey, String dataId, String group, String tenant, boolean isBeta,
            List<String> betaIps, String tag)
 
{
        
        // 獲取配置的所有監(jiān)聽(tīng)者
        Set<String> listeners = configChangeListenContext.getListeners(groupKey);
        if (CollectionUtils.isEmpty(listeners)) {
            return;
        }
        int notifyClientCount = 0;
        // 遍歷所有監(jiān)聽(tīng)者
        for (final String client : listeners) {
            // 獲取長(zhǎng)連接信息
            Connection connection = connectionManager.getConnection(client);
            if (connection == null) {
                continue;
            }

            String clientIp = connection.getMetaInfo().getClientIp();
            String clientTag = connection.getMetaInfo().getTag();
            
            // 判斷是否是Beta的Ip
            if (isBeta && betaIps != null && !betaIps.contains(clientIp)) {
                continue;
            }
            // tag check
            if (StringUtils.isNotBlank(tag) && !tag.equals(clientTag)) {
                continue;
            }
   
            // 配置變更推送請(qǐng)求
            ConfigChangeNotifyRequest notifyRequest = ConfigChangeNotifyRequest.build(dataId, group, tenant);
   
            // 執(zhí)行推送任務(wù)
            RpcPushTask rpcPushRetryTask = new RpcPushTask(notifyRequest, 50, client, clientIp,
                    connection.getMetaInfo().getAppName());
            push(rpcPushRetryTask);
            notifyClientCount++;
        }
        Loggers.REMOTE_PUSH.info("push [{}] clients ,groupKey=[{}]", notifyClientCount, groupKey);
    }
    
    @Override
    public void onEvent(LocalDataChangeEvent event) {
        String groupKey = event.groupKey;
        boolean isBeta = event.isBeta;
        List<String> betaIps = event.betaIps;
        String[] strings = GroupKey.parseKey(groupKey);
        String dataId = strings[0];
        String group = strings[1];
        String tenant = strings.length > 2 ? strings[2] : "";
        String tag = event.tag;
        
        configDataChanged(groupKey, dataId, group, tenant, isBeta, betaIps, tag);
        
    }
    
    // ......
}

到這里,基本上就是配置變更推送的最后一個(gè)步驟了,如代碼中注釋所示,通過(guò)調(diào)用ConnectionManager的getConnection方法,遍歷所有監(jiān)聽(tīng)者的連接,根據(jù)其中的Meta信息判斷是否是Beta推送的目標(biāo),然后執(zhí)行推送任務(wù),也就是執(zhí)行push方法,如下所示。

 private void push(RpcPushTask retryTask) {
        ConfigChangeNotifyRequest notifyRequest = retryTask.notifyRequest;
        // 判斷是否重試次數(shù)達(dá)到限制
        if (retryTask.isOverTimes()) {
            Loggers.REMOTE_PUSH
                    .warn("push callback retry fail over times .dataId={},group={},tenant={},clientId={},will unregister client.",
                            notifyRequest.getDataId(), notifyRequest.getGroup(), notifyRequest.getTenant(),
                            retryTask.connectionId);
            // 主動(dòng)注銷連接
            connectionManager.unregister(retryTask.connectionId);
        } else if (connectionManager.getConnection(retryTask.connectionId) != null) {
            // first time :delay 0s; sencond time:delay 2s  ;third time :delay 4s
            // 嘗試執(zhí)行配置推送
            ConfigExecutor.getClientConfigNotifierServiceExecutor()
                    .schedule(retryTask, retryTask.tryTimes * 2, TimeUnit.SECONDS);
        } else {
            // client is already offline,ingnore task.
        }
        
    }

這里實(shí)際上也是一個(gè)異步執(zhí)行的過(guò)程,推送任務(wù)RpcPushTask會(huì)被提交到ClientConfigNotifierServiceExecutor來(lái)計(jì)劃執(zhí)行,第一次會(huì)立即推送配置,即調(diào)用RpcPushTask的run方法,如果失敗則延遲重試次數(shù)x2的秒數(shù)再次執(zhí)行,直到超過(guò)重試次數(shù),主動(dòng)注銷當(dāng)前連接。其中,RpcPushTask的定義如下。

    class RpcPushTask implements Runnable {
        
        ConfigChangeNotifyRequest notifyRequest;
        
        int maxRetryTimes = -1;
        
        int tryTimes = 0;
        
        String connectionId;
        
        String clientIp;
        
        String appName;
        
        public RpcPushTask(ConfigChangeNotifyRequest notifyRequest, int maxRetryTimes, String connectionId,
                String clientIp, String appName)
 
{
            this.notifyRequest = notifyRequest;
            this.maxRetryTimes = maxRetryTimes;
            this.connectionId = connectionId;
            this.clientIp = clientIp;
            this.appName = appName;
        }
        
        public boolean isOverTimes() {
            return maxRetryTimes > 0 && this.tryTimes >= maxRetryTimes;
        }
        
        @Override
        public void run() {
            tryTimes++;
            if (!tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH, connectionId, clientIp)) {
                push(this);
            } else {
                // 推送配置
                rpcPushService.pushWithCallback(connectionId, notifyRequest, new AbstractPushCallBack(3000L) {
                    @Override
                    public void onSuccess() {
                        tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH_SUCCESS, connectionId, clientIp);
                    }
                    
                    @Override
                    public void onFail(Throwable e) {
                        tpsMonitorManager.applyTpsForClientIp(POINT_CONFIG_PUSH_FAIL, connectionId, clientIp);
                        Loggers.REMOTE_PUSH.warn("Push fail", e);
                        push(RpcPushTask.this);
                    }
                    
                }, ConfigExecutor.getClientConfigNotifierServiceExecutor());
                
            }
            
        }
    }

可以看到,在RpcPushTask的run方法中,調(diào)用了RpcPushService的pushWithCallback方法,如下所示。

/**
 * push response  to clients.
 *
 * @author liuzunfei
 * @version $Id: PushService.java, v 0.1 2020年07月20日 1:12 PM liuzunfei Exp $
 */

@Service
public class RpcPushService {
    
    @Autowired
    private ConnectionManager connectionManager;
    
    /**
     * push response with no ack.
     *
     * @param connectionId    connectionId.
     * @param request         request.
     * @param requestCallBack requestCallBack.
     */

    public void pushWithCallback(String connectionId, ServerRequest request, PushCallBack requestCallBack,
            Executor executor)
 
{
        Connection connection = connectionManager.getConnection(connectionId);
        if (connection != null) {
            try {
                // 執(zhí)行配置推送
                connection.asyncRequest(request, new AbstractRequestCallBack(requestCallBack.getTimeout()) {
                    
                    @Override
                    public Executor getExecutor() {
                        return executor;
                    }
                    
                    @Override
                    public void onResponse(Response response) {
                        if (response.isSuccess()) {
                            requestCallBack.onSuccess();
                        } else {
                            requestCallBack.onFail(new NacosException(response.getErrorCode(), response.getMessage()));
                        }
                    }
                    
                    @Override
                    public void onException(Throwable e) {
                        requestCallBack.onFail(e);
                    }
                });
            } catch (ConnectionAlreadyClosedException e) {
                connectionManager.unregister(connectionId);
                requestCallBack.onSuccess();
            } catch (Exception e) {
                Loggers.REMOTE_DIGEST
                        .error("error to send push response to connectionId ={},push response={}", connectionId,
                                request, e);
                requestCallBack.onFail(e);
            }
        } else {
            requestCallBack.onSuccess();
        }
    }
    
}

其持有ConnectionManager對(duì)象,當(dāng)需要推送配置到客戶端時(shí),會(huì)獲取相應(yīng)的Connection,然后執(zhí)行asyncRequest將配置推送到客戶端中。如果連接已經(jīng)關(guān)閉,則注銷連接。在asyncRequest底層即是調(diào)用Grpc建立的Stream的onNext方法,將配置推送給客戶端,如下。

/**
 * grpc connection.
 *
 * @author liuzunfei
 * @version $Id: GrpcConnection.java, v 0.1 2020年07月13日 7:26 PM liuzunfei Exp $
 */

public class GrpcConnection extends Connection {
    
    private StreamObserver streamObserver;
    
    private Channel channel;
    
    public GrpcConnection(ConnectionMeta metaInfo, StreamObserver streamObserver, Channel channel) {
        super(metaInfo);
        this.streamObserver = streamObserver;
        this.channel = channel;
    }
    
    @Override
    public void asyncRequest(Request request, RequestCallBack requestCallBack) throws NacosException {
        sendRequestInner(request, requestCallBack);
    }
    
    private DefaultRequestFuture sendRequestInner(Request request, RequestCallBack callBack) throws NacosException {
        final String requestId = String.valueOf(PushAckIdGenerator.getNextId());
        request.setRequestId(requestId);
        
        DefaultRequestFuture defaultPushFuture = new DefaultRequestFuture(getMetaInfo().getConnectionId(), requestId,
                callBack, () -> RpcAckCallbackSynchronizer.clearFuture(getMetaInfo().getConnectionId(), requestId));
        
        RpcAckCallbackSynchronizer.syncCallback(getMetaInfo().getConnectionId(), requestId, defaultPushFuture);
        sendRequestNoAck(request);
        return defaultPushFuture;
    }
    
    private void sendRequestNoAck(Request request) throws NacosException {
        try {
            //StreamObserver#onNext() is not thread-safe,synchronized is required to avoid direct memory leak.
            synchronized (streamObserver) {
                
                Payload payload = GrpcUtils.convert(request);
                traceIfNecessary(payload);
                streamObserver.onNext(payload);
            }
        } catch (Exception e) {
            if (e instanceof StatusRuntimeException) {
                throw new ConnectionAlreadyClosedException(e);
            }
            throw e;
        }
    }
    
}

主要推送邏輯的代碼如上所示,調(diào)用asyncRequest之后,會(huì)將請(qǐng)求交給sendRequestInner處理,sendRequestInner又會(huì)調(diào)用sendRequestNoAck將推送請(qǐng)求推入gRPC的流中,客戶端收到配置更新的請(qǐng)求,就會(huì)更新客戶端的配置了。至此,一個(gè)灰度配置就發(fā)布成功了。

刪除/查詢BetaConfig

刪除和查詢BetaConfig的方法都很簡(jiǎn)單,都是簡(jiǎn)單的操作數(shù)據(jù)庫(kù)即可。如果是刪除配置,則會(huì)觸發(fā)ConfigDataChangeEvent來(lái)告知客戶端更新配置,這里筆者就不多加贅述了。

    /**
     * Execute to remove beta operation.
     *
     * @param dataId dataId string value.
     * @param group  group string value.
     * @param tenant tenant string value.
     * @return Execute to operate result.
     */

    @DeleteMapping(params = "beta=true")
    @Secured(action = ActionTypes.WRITE, parser = ConfigResourceParser.class)
    public RestResult<BooleanstopBeta(@RequestParam(value 
"dataId") String dataId,
            @RequestParam(value = "group") String group,
            @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant) {
        try {
            persistService.removeConfigInfo4Beta(dataId, group, tenant);
        } catch (Throwable e) {
            LOGGER.error("remove beta data error", e);
            return RestResultUtils.failed(500false"remove beta data error");
        }
        ConfigChangePublisher
                .notifyConfigChange(new ConfigDataChangeEvent(true, dataId, group, tenant, System.currentTimeMillis()));
        return RestResultUtils.success("stop beta ok"true);
    }
    
    /**
     * Execute to query beta operation.
     *
     * @param dataId dataId string value.
     * @param group  group string value.
     * @param tenant tenant string value.
     * @return RestResult for ConfigInfo4Beta.
     */

    @GetMapping(params = "beta=true")
    @Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
    public RestResult<ConfigInfo4BetaqueryBeta(@RequestParam(value 
"dataId") String dataId,
            @RequestParam(value = "group") String group,
            @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant) {
        try {
            ConfigInfo4Beta ci = persistService.findConfigInfo4Beta(dataId, group, tenant);
            return RestResultUtils.success("stop beta ok", ci);
        } catch (Throwable e) {
            LOGGER.error("remove beta data error", e);
            return RestResultUtils.failed("remove beta data error");
        }
    }

總結(jié)

Nacos2.0使用長(zhǎng)連接代替了短連接的長(zhǎng)輪詢,性能幾乎提升了10倍。在阿里內(nèi)部,也在逐漸推進(jìn)Nacos2作為統(tǒng)一的配置中心。目前在微服務(wù)引擎(Micro Service Engine,簡(jiǎn)稱 MSE),Nacos作為注冊(cè)配置中心,提供了純托管的服務(wù),只需要購(gòu)買(mǎi)Nacos專業(yè)版即可享受到10倍的性能提升。

此外,MSE微服務(wù)引擎顧名思義,是一個(gè)面向業(yè)界主流開(kāi)源微服務(wù)生態(tài)的一站式微服務(wù)平臺(tái), 幫助微服務(wù)用戶更穩(wěn)定、更便捷、更低成本的使用開(kāi)源微服務(wù)技術(shù)構(gòu)建微服務(wù)體系。不但提供注冊(cè)中心、配置中心全托管(兼容 Nacos/ZooKeeper/Eureka),而且提供網(wǎng)關(guān)(兼容 Ingress/Enovy)和無(wú)侵入的開(kāi)源增強(qiáng)服務(wù)治理能力。

在阿里,MSE微服務(wù)引擎已經(jīng)被大規(guī)模的接入使用,經(jīng)歷阿里內(nèi)部生產(chǎn)考驗(yàn)以及反復(fù)淬煉,其中微服務(wù)服務(wù)治理能力支撐了大量的微服務(wù)系統(tǒng),對(duì)包括Spring Cloud、Dubbo等微服務(wù)框架的治理功能增強(qiáng),提供了無(wú)損上下線、金絲雀發(fā)布、離群摘除以及無(wú)損滾動(dòng)升級(jí)的功能。

如果有快速搭建高性能微服務(wù)以及大規(guī)模服務(wù)治理的需求,相比于從零搭建和運(yùn)維,MSE微服務(wù)引擎是一個(gè)不錯(cuò)的選擇。

END -

「技術(shù)分享」某種程度上,是讓作者和讀者,不那么孤獨(dú)的東西。歡迎關(guān)注我的微信公眾號(hào):「Kirito的技術(shù)分享」


瀏覽 80
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)
評(píng)論
圖片
表情
推薦
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 国产精品无码永久免费不卡| 一级操逼大片| 亚洲日韩欧美国产| 国产中文字幕在线免费观看| 亚洲码无| 深爱五月婷婷| 日韩一及| 人人操人人操人人操人人操| 五月丁香六月色| 亚洲国产精品午夜福利| 亲子乱一区二区三区视频| 久久五月天婷婷| 男人的天堂在线视频| 另类老妇性BBwBBw| 亚州AV在线| 91精品国产91久久久久久吃药 | 91羞羞网站| 麻豆传媒一区| 欧美老熟妇BBBBB搡BBB| 亚洲黄色电影在线| 国产综合婷婷| 色五月综合网| 欧美日韩a| 亚洲欧美美国产| 欧美aaaaaa| 影音先锋久久久久AV综合网成人| 无码国产+白浆| 好吊顶亚洲AV大香蕉色色| 在线观看亚洲专区| 日韩AV免费在线| 国产成人AV网站| 囯产精品久久久久久久久久久久久久 | 2025av天堂网| 精品91| 欧美日韩亚洲一区二区三区| 少妇搡BBBB搡BBB搡AA| 免费AA片| 亚色网址| 黄色片成人| 国产情侣在线视频| 国产美女裸体网站| 国产嫩草久久久一二三久久免费观看| 在线免费看A| 国产精品无码激情| 中文熟女| 日韩欧美在线播放| 毛片天天干| 欧美极品视频| 后入少妇视频| 东北老女人操逼| 天堂a√中文8| 久久精品国产亚洲AV麻豆痴男| 欧美操逼图| aV无码av天天aV天天爽第一| 老熟女露脸25分钟91秒| 熟女人妻人妻の视频| 一级黄色免费电影| 青青草视频免费在线观看| 国产一级A片久久久免费看快餐| jizz免费视频| 爆操人妻| 双腿张开被9个男人调教| 三级片视频网址| 亚洲色五月| 高清欧美日韩第一摸| 色天堂网| a网站在线观看| 无码囯无精品毛片大码| 99人妻在线| 中文字幕无码网站| 色婷婷激情综合网| 亚洲精品国产精品国自产A片同性 丰满人妻一区二区三区四区不卡 国产1级a毛a毛1级a毛1级 | 色片在线| 亚洲AV无码乱码国产精品蜜芽| 狠狠干伊人| 污视频网站在线观看| 欧美插菊花综合网| 蜜桃人妻无码AV天堂三区| 色噜噜网站| 国产女人18毛片水18精品软件| 亚洲人妻性爱| 国产久久免费视频| 麻豆蜜桃wwww精品无码| 色欲影视插综合一区二区三区| 国产黄色视频在线| 中文字幕有码在线观看| 亚洲一区视频| 人人操人| 国产精品视频免费| 青娱乐偷拍| 青青草97国产精品麻豆| 中文视频在线观看| 成人国产精品| 亚洲精品内射| 无码免费婬AV片在线观看| 加勒比在线| 无码群交| 中文字幕在线观看AV| 亚洲一区二区三区在线| 亚洲高清av| 在线黄色小视频| 亚洲国产免费| 黄片亚洲| 亚洲一区二区三区在线| 又紧又嫩又爽无遮挡免费| 蜜桃BBwBBWBBwBBw| 97精品在线| 激情丁香五月天| 一级a一级a爱片免费视频| 国产精品无码AV| 残忍另类BBWBBWBBW| 国产AV黄色| AV在线免费观看网站| 国产一级a毛一级a毛视频在线网站 | 日韩五月婷婷| 爱五月| 免费看黄色电影| 夜色福利在线看| 女女女女女女BBBBBB手| 免费看片av| 国产精品视频导航| 亚洲国产一| 少妇BBB| 伊人大香蕉视频在线观看| 国产色悠悠| 成人精品一区日本无码网站suv| 黄色网页在线| 91成人小视频| 国产成人午夜精品无码区久久麻豆 | 色综合久久天天综合网| 超碰人人草| 一本色道久久综合亚洲精品久久| 久在线| 欧美三P囗交做爰| 久久国产精品波多野结衣AV| 亚洲色影院| 国产高清AV| 日本乱伦电影中文字幕| 乱子伦一区二区三区视频在线观看| 色丁香六月| 大香蕉伊人导航| 日本免费在线观看视频| 午夜福利123| 嫩BBB槡BBBB槡BBBB百度| 国产黄色视频免费看| 一区二区三区免费播放| 大陆一级片| 亚洲国产激情| 超碰国产在线| 人人操人妻| 亚洲无码网站| 欧洲三级片网站| 天天干在线观看视频| 午夜无码精品| 天天日天天干天天草| 五月天三级片| 一区二区三区免费在线观看| 国产成人va| 亚洲精品456| 国产中文字幕在线播放| 精品一区二区三区三区| 三级无码av| 日本色五月| 国产91无码精品秘入口| 东京热A片| 人人操人人摸人人看| 色吟AV| 国产A片视频| 四虎精品一区二区三区| 91亚洲精品久久久久蜜桃| 99精品丰满人妻无码| 欧美日韩一区在线| 丁香五月av| 一区二区三区久久| 亚洲欧美日韩性爱| 91AV在线观看视频| 国产又黄又| 翔田千里在线播放| 色婷婷日韩精品一区二区三区| 91丨熟女丨首页| 精品免费国产一区二区三区四区的使用方法 | 51毛片| 51成人网站免费| 亚洲天堂一级片| 日韩无码破解| 亚洲AV成人片色在线观看高潮 | 成人小说亚洲一区二区三区| 青青草无码在线| 亚洲AV无码久久精品色无码蜜桃 | 91www| 亚洲免费高清视频| 日韩中文字幕在线| 成人久久AV| 亚洲无码乱码av| 天天日,天天干,天天操| 久久韩国| 国产内射在线观看| 欧美国产成人在线| 青青操天天干| 欧美国产性爱| 69成人免费视频| 欧美一二区| 国产免费AV片在线无码免费看| 一区无码视频| 中文激情网| 天堂一区二区18| 精品久久久久久久久久久| 嫩BBB槡BBBB槡BBB| 美女久草| P站免费版-永久免费的福利视频平台| 无码无码无码| 婷婷五月天黄色| 人人爽人人爽| 亚洲欧美日韩在线| 成人黄A片免费| 桃色五月天| 天天日天天插| 欧美精品成人免费片| 91AV在线观看视频| 综合偷拍| 亚洲无码小电影| 99热这里都是精品| 亚洲最新无码视频| 国产女人高潮毛片| 黄色视频网站在线免费观看| 久久久1| 香蕉成人视频| 国产秘精品区二区三区日本| A一级黄色片| 黄片免费观看视频| 午夜操一操一级| 五月天综合久久| 成年人视频免费看| 91久久综合亚洲鲁鲁五月天| 日韩AAA在线| 国产黄色一级电影| 午夜69成人做爱视频网站| 中文日韩欧美| 丁香婷婷色五月激情综合三级三级片欧美日韩国| 欧美性爱在线网站| 日韩久久中文字幕| 殴美A片| 美妇肥臀一区二区三区-久久99精品国 | 亚洲猛男操逼欧美国产视频| 蜜臀av网站| 婷色五月天| 伊人久久网站| 超碰激情| 三级av在线观看| AV无码在线播放| 少妇大战黑人46厘米| 成人国产AV网站| 亚洲视频五区| A视频在线观看| 欧美成人图片视频在线| 日逼| 欧美亚洲一区二区三区| 婷婷五月丁香五月| 在线不卡无码| 大鸡巴免费视频| 中文字幕1区| 成人无码小电影| 婷婷午夜福利| 在线免费A片| 日韩AAA在线| 日韩三级片在线播放| 午夜激情四射| 高清一区二区| 色播视频在线观看| 人人操人人摸人人爱| 日韩做爱视频| 青青无码视频| 最近中文字幕在线| 高清无码黄片| 午夜成人无码视频| 天堂中文资源库| 亚洲第一视频| 婷婷色色五月| 激情深爱五月| 超碰99在线| 色色色五月婷婷| 特级婬片A片AAA毛片AA做头| 免费国产视频| 日韩操逼AV| 久久草大香蕉| 日韩性爱视频| 日本黄A三级三级三级| 一区二区视频在线| 漂亮人妻吃鸡啪啪哥哥真的好| 无码熟妇人妻无码AV在线天堂| 亚洲黄色小视频| 国产成人精品八戒| 国产乱子伦精品久久| 3344gc在线观看入口| 91人妻人人澡人人爽人人精吕| 日韩一级免费在线观看| 日韩一级黄色| 国精产品一区二区三区在线观看 | 成人免费毛片片v| 成人亚洲av| 天天做天天爱天天爽| 久久久久三级| 国产精品成人免费视频| 91精品国产综合久久久蜜臀酒店 | 色五月婷婷五月| 午夜美女视频| 免费A在线观看| 熟妇导航| 天堂中文资源在线观看| 亚洲欧洲成人在线| 欧美A视频在线观看| 3D动漫精品啪啪一区二区竹笋| 亚洲免费高清视频| 久久精品无码一区二区无码性色| 青青草原网址| 撸一撸成人在线做爱视频。| 国产美女做爱视频| 亚洲免费性爱视频| 一本大道久久久久| 久久免费视屏| 麻豆91免费视频| 亚洲AV黄片| 操比视频| 日日久视频| 99视频在线观看免费| 免费无码一区二区三区四区五区| 国产黄色片视频| 伊人大香蕉视频| 国产字幕| 精品人妻一区二区| 亚洲高清毛片一区二区| 人妻无码专区| 高清无码片| 亚洲无码视频一区二区| 视色视频在线观看| 久久久成人免费视频| 一级片免费| 日逼视频网| 中文字幕成人网| 无码三级午夜久久人妻| 日韩va中文字幕无码免费| 狼人狠干| 91久久精品一区二区三| 青操av| 亚洲无码蜜桃| 欧美无人区码suv| 一本加勒比HEZYO东京热无码| 欧美艹逼| 亚洲男人av| 国产成人精品三级麻豆| 国产夫妻自拍av| 一本一道无码免费看视频| 成年人性生活免费视频| 亚洲精品久久久久久久久久久 | 午夜天堂网| 久久99精品国产.久久久久| 久久嫩草国产成人一区| 国产一区二区做爱| 激情伊人五月天| 成人视频网站在线观看18| 99热日本| 黄色录像一级片| 青青草原AV| 免费成人黄色网址| 久久精品禁一区二区三区四区五区| 国产精品国产精品国产专区不52 | 久久艹视频| 无码色色| 婷婷久久在线| 欧美一卡| 精品孕妇一区二区三区| 亚洲无码系列| 日韩视频在线免费观看| 亚洲无码人妻| 天天射夜夜骑| 三级网址在线| 韩国无码一区二区| 中文字幕福利视频| 亚洲天堂第一页| 91久久国产性奴调教| 一区二区三区不卡视频| 亚洲18禁| 亚洲中文字幕无码在线观看| P站免费版-永久免费的福利视频平台| 日韩精品久久久| 3D动漫精品啪啪一区二区免费| 青青草乱伦视频| 99大香蕉| www.91AV| 视色视频在线观看| 大茄子熟女AV导航| 天天插天天| 性满足BBWBBWBBW| 北条麻妃无码视频在线| 亚洲天堂一区二区三区| 自慰一区二区| 韩日毛片| 九色蝌蚪视频| 色色99| 国产精品成人一区二区| 99亚洲欲妇| 日韩精品91| 午夜成人精品一区二区三区| 俺也去在线视频| 美女网站视频黄| 国产又黄又爽| 不卡a12| 2025四虎在线视频观看| jizz麻豆| 国产亚洲综合无码| 欧美猛交| 亚洲AV无码乱码国产精品黑人| 黄色在线观看免费| 一本无码中文字幕| 开心色婷婷| 色爽AV| 青娱乐成人在线视频| 东北嫖老熟女一区二区视频网站| 黄色视频A| 亚洲无码影院| caopeng97| 日韩成人在线看| 日韩综合色| 久久精品苍井空免费一区| 国产成人久久| 免费无码av| 日韩AV无码专区亚洲AV紧身裤| 麻豆网站| 91久久午夜无码鲁丝片久久人妻 | 特级西西WWW888| 久热免费视频在线观看| 奶头和荫蒂添的好舒服囗交漫画| 亚洲秘无码一区二区三区蜜桃中文 | 日韩在线91| 大香蕉一区| 玖玖资源网站| 成人一区二区在线| 久久久久网站| 国产精品一区二区在线观看| 精品无码久久久久久久久app| 久久久成人免费电影| 免费在线a视频| 三级片视频网站| 精品无码三级在线观看视频| 日韩极品视频在线| 精品人无码一区二区三区下载| 99精品免费观看| 农村新婚夜一级A片| 久久这里只有精品99| 操逼999| 亚洲狠狠| 黄色直播在线观看| 无码国产99精品久久久久网站| 青草福利| 蜜桃精品视频在线观看| 91视频美女| 中文字幕毛片| 99热在线观看免费精品| 操日韩美女| 超碰成人福利| 亚洲日产专区| 亚洲AV资源在线| 亚洲性爱在线观看| 成人电影久久久| 中文字幕天天在线| 欧洲精品在线观看| 亚洲欧美网站| 日韩一区二区在线看在线看 | 91久久婷婷| 国产成人无码Av片在线公司| 久久大伊人| 亚洲日本欧美| 黄色精品网站| 国产精品视频久久久久| 蜜臀色欲AV无码人妻| 欧洲精品视频在线观看| 亚洲欧美91| 成人在线视频免费观看| 色婷婷AV一区二区三区之e本道| 青娱乐一级无码| 久久无码电影| 欧美超碰在线| 国内精品久久久久久久久久| 亚洲精品成人片在线观看精品字幕 | 99都是精品| 男人天堂网站| 99热这里有精品| 婷婷色视频| brazzers疯狂作爱| 久久久久大香蕉| 国产精品不卡一区二区三区| 中文无码毛片| 操逼网123| 欧美八区| 91麻豆精品国产91久久久久久| 中文字幕无码一区二区| 国产高清视频在线观看| 91抽插| 日本在线观看www| 最新va在线观看| 人妻少妇一区二区三区| 中文字幕无码成人| 欧美一级操逼视频| 国产av一区二区三区| 免费无码进口视频| 成人免费在线网站| 欧美18禁| 成人视频网站在线观看18| 免费视频一二三区| 精品动漫3D一区二区三区免费版| 欧美成人乱码一区二区三区| 免费毛片网站| 337p大胆色噜噜噜噜噜| 久久久久久久久久久国产| 最美人妖系列国产Ts涵涵| 色综合天天| 水蜜桃在线观看视频| av在线一区二区三区| 操B视频在线免费观看| 精品人妻一区二区三区鲁大师| 国产综合久久777777麻豆| 亚洲AV无码成人精品区在线欢看 | 日本暖暖视频| 中文原创麻豆传媒md0052| 一本道无码在线观看| 国产性爱在线视频| 色色影音先锋| 蜜臀网在线观看| 肉片无遮挡一区二区三区免费观看视频 | 九九视频免费观看| 黄色操逼片| 夜夜夜久久久| 亚洲精品日韩中文字幕| 黄色大片免费观看| 欧美日韩性爱网站| 久久一级A片| 香蕉av在线播放| 18禁在线播放| 欧美一级婬片AAAA毛片| 久久精品国产亚洲AV成人婷婷 | 亚洲精品国产成人| 欧美日韩久久| 91探花在线观看| 丁香五月天社区| 自拍欧美亚洲| 天天综合网久久| 亚洲精品无码视频| 国产精品一区二区三| 亚洲三级片在线| 麻豆自拍偷拍视频| 91porn国产| 男女网站在线观看| 国产免费无码| 一本道在线无码| 大香蕉欧美在线| 国产91白丝在线播放| 西西444WWW无码视频软件功能介绍 | 精品一区二区三区av| 日日干夜夜撸| 久久国产精品网站| 亚洲小说欧美激情另类A片小说 | 亚洲图片欧美另类| 偷拍视频第一页| 五月激情婷婷网| 久久青草影院| 欧美日本成人网站入口| 加勒比综合在线| 七十路の高齢熟女千代子| 超碰操| 国产精品无码激情| 中文字幕无码Av在线| 91蜜桃传媒| 波多野结衣无码视频| 麻豆mdapp01.tⅴ| 黄色视频网站在线免费观看| 成人无码日韩精品| 中文无码一区二区三区四区| 夜夜撸夜夜操| 久久精彩| 丁香六月婷婷久久综合| 爽好紧别夹喷水网站| 91亚洲一线产区二线产区| 粉嫩av懂色av蜜臀av熟妇| 噜噜噜久久久| 欧美精品一级| 亚洲精品日日夜夜| 影音先锋人妻资源| 91青青草| 夜夜骚av一区二区三区| 自拍偷拍中文字幕| 日韩中文字幕高清| 人人爱人人摸人人操| 嫩BBB槡BBBB槡BBBB百度| 2017天天干天天射| 国产操P| 麻豆MD传媒MD0071| 91视频一区| 大肉大捧一进一出两腿| 一区二区三区在线免费观看| 无码人妻精品一区二区蜜桃91| 免费无码又爽又黄又刺激网站| 欧美国产综合在线| 五月丁香综合在线| 超碰2021| 五月婷婷啪| 日日操天天操夜夜操| 日韩不卡| wwwA片| 国产一区不卡| 国产成人91| 免费无码进口视频| 热99视频| 色色热热| 日韩三级一区| 99热免费精品| 在线观看免费黄网站| 在线h网站| 五月丁香色色网| av天天av无码av天天爽| 亚洲第一香蕉视频| 在线观看免费完整版中文字幕视频| 亚洲成人黄色| 国产三级无码| 国内自拍视频在线观看| 国产毛片一区二区三区| 靠逼免费视频| 北条麻妃无码精品| 91人妻人人澡人人爽人人精| 国产精品秘久久久久久1-~/\v7-/ 囯产精品一区二区三区线一牛影视1 | 日本在线免费观看| 成人精品永久免费视频99久久精品| 久久不雅视频| 婷婷午夜精品久久久久久性色| 国产亚洲久一区二区三区| 丁香五月社区| 在线观看黄色视频网站| 国产6区| 亚洲中文中出| 欧美囗交大荫蒂免费| 亚洲秘无码一区二区三区蜜桃中文 | 91美女网站| 特级444WWW大胆高清| 美女网站黄| 黄片中文| 在线观看免费视频a| 亚洲精品黄色电影| 国产av日韩av| 无码免费高清视频| 九九九在线视频| 亚洲无码一二三区| 黄色日逼视频| 中文字幕一区三区人妻视频| 国产毛片精品一区二区色欲黄A片| 国产熟女一区二区| 国产午夜福利电影| 五月婷婷一区| 黄色福利在线观看| 91无码精品国产| 成人A片免费看| 男女无码视频| 91人妻无码精品一区二区| 内射极品美女| 欧美拍拍视频| 宅男视频| 欧美精品三级| 国产网站在线| 成人区精品一区二区婷婷| 少妇嫩搡BBBB搡BBBB| 特黄aaaaaaaa真人毛片| 大香蕉性爱| 免费看无码一级A片在线播放| 免费看黃色AAAAAA片| 亚洲高清在线观看视频| 日韩精品123| 日本不卡一区二区三区| www.黄色视频| 国产精品久久久精品| 国产黄片免费观看| 日韩一级一级| 日本不卡三区| www.91av| 黄色操逼网站?| 色色激情视频| 亚洲无码中文人妻| 91香蕉在线观看视频在线播放| www超碰在线| 日韩一区二区免费看| 国产精品色呦呦| 天堂成人网站| 中文字幕无码一区二区三区一本久| 欧美成人69| 麻豆91蜜桃传媒在线观看| 亚洲三级无码在线| 亚洲成人777| 91久久精品无码一区| 日韩三级av| 骚五月| 人成视频在线免费观看| 日韩无码三级| 黄色资源在线观看| 亚洲精品一区二区三区| 日韩中文在线观看| 中文字幕++中文字幕明步| 黃色A片一級二級三級免費久久久| 国产丝袜AV| 免费无码一区二区三区四区五区 | 一本色道久久综合无码人妻四虎| 国产在线播放av| 成人久久综合| 欧美伦妇AAAAAA片| 一级黄片免费观看| 99热99精品| 熟女网址| 欧美成人网站在线观看| 国产无套进入免费| 国产毛片久久久久久国产毛片| 欧美色啪| 精品久草| 波多野结衣在线精品| 黄色在线视频观看| 中文区中文字幕免费看| 日本一本不卡| 国产一级免费| 51妺妺嘿嘿午夜成人A片| 三级片无码| 北条麻妃电影九九九| 日屄在线观看| 国产亚洲欧美日韩高清| 免费黄色av网址| 色色激情五月天| 先锋影音一区二区| 国产在线拍偷自揄拍无码一区二区 | 刘玥精品A片在线观看| 黄色电影AV| 亚洲精品操逼| 自拍偷拍网址| 欧美中文日韩| 日本精品国产| 超碰在线观看97| 日本高清视频九区| 久久国产精品在线| 国产美女被爽到高潮免费A片软件 国产无遮挡又黄又爽又色视频软件 | 无码一区二区视频| 成人免费视频18| 高清无码网站| 香蕉av在线播放| 日韩精品人妻| 中文字幕第一页av| 亚洲视频欧美视频| 最新av| 国产亚洲欧美在线| 玖玖视频| 欧美日本一区二区三区| 欧美精品一级| 熟女人妻在线| 亚洲春色一区二区三区| 日本色网站| 中文字幕精品无码亚| 亚洲精品97久久| 91久久偷拍视频| 男女AV网站| 97人妻精品一区二区三区图片| 日韩成人无码电影| 日韩视频一区二区| 亚洲猛男操逼欧美国产视频| 一区二区三区三级片| 91一起草高清资源| 香蕉视频一区| 蜜桃91精品秘成人取精库| www.大鸡巴| 奶头和荫蒂添的好舒服囗交漫画| 操B视频在线| 香蕉午夜视频| 亚洲精品乱码久久久久久| 韩日精品视频| 黄色电影免费在线观看| 免费的黄色A片| 大香蕉久久精品| 国产熟女一区| 国产精品国产三级国产专业不| 夜夜操影院| 字幕一区二区久久人妻网站| 日韩三级视频在线观看| 青青草黄色视频| a级毛片在线观看| 草逼的视频| 欧美成人视频18| 亚洲无码免费视频在线观看| 99在线观看免费| 亚洲A片V一区二区三区| 思思热99| 无码人妻一区二区三区四区老鸭窝| 操逼色| 大香蕉少妇| 成年人视频在线免费观看| 高清无码三级| 无码高清视频| 无码人妻免费视频| 天天色天| 亚洲综合久| 日韩性爱网| 欧美不卡在线视频| 人人妻人人躁人人DVD| 精品孕妇一区二区三区| 亚洲精品午夜精品| 亚洲成人精品少妇| 亚洲AV片一区二区三区| 亚洲综合色网站| 免费欧美性爱视频| 日韩婷婷| 天天撸天天色| 国产欧美日韩视频| 国产日韩一区| 伊人在线| 成人三级片视频| www.97av| 免费看黄片的网站| 久色网| 超碰在线免费播放| 亚洲无码AV一区二区| 成人免费在线| 成人网站在线观看免费| 一级a黄片| 日韩色婷婷| 精品乱子伦一区二区三区免费播成| 国产人人干| 在线免费亚洲| 久久婷婷国产综合| 人妻少妇一区二区三区| 国产精品秘久久久久久网站| 7777精品伊人久久7777| 亚洲成人无码网站| 精品人妻一区二区三区-国产精品 无码人妻av黄色一区二区三区 | 444444免费高清在线观看电视剧的注意 | 高清色视频| 四虎操逼| 九九热免费视频| 性99网站| 97人妻精品一区二区三区软件| 国产女主播在线观看| 久久精品福利视频| 超碰自拍99| 中文字幕无码在线观看视频| 可以免费看的av| 国产日逼视频| 天堂AV无码AV| 日皮视频在线看| 性久久久久| 亚洲精品久久久久毛片A级牛奶| 天天中文字幕| av免费播放| 日韩无码人妻一区二区三区| 黄色免费视频| 欧美三级片在线视频| 91在线精品视频| 在线视频观看一区| 三级午夜在线无码| 超碰c| A免费观看| 黑人巨大精品欧美| 大肉大捧一进一出两腿| 成人久操| 一级黄影| 97人妻精品一区二区三区| 一级无码A片| 丝袜毛片| 青青草操逼视频| 香蕉一级视频| 在线观看成人18| 91蜜桃在线| 国产乱婬AV片免费| 7799综合| 苍井空一区二区| 特猛特黄AAAAAA片| 四虎成人无码A片观看| 五月婷婷在线播放| TokyoKot大交乱无码| 大香蕉网伊人在线| 少妇福利| 亚洲女人天堂AV| 亚洲人妻在线播放| 天天久久综合| 高清免费在线中文Av| 国产毛片在线视频| 男女日皮视频| 日韩人妻系列| 免费无码婬片AAAA片直播| 999reav| 五月天丁香花| 欧美日韩中文字幕在线| 秋霞亚洲| 欧美性爱导航| 成人免费毛片AAAAAA片| 欧美日韩国产三级| 中文有码在线观看| 色交视频| 艹逼视频| 日韩人妻无码中文字幕| 91麻豆精品国产91久久久久久久久| 国产三级免费观看|