Welcome everyone

Spring Cloud 入坑宝典

Spring Cloud 汪明鑫 168浏览 0评论

前言

本文主要是介绍微服务和Spring Cloud相关内容,如果同学们已经在生产环境上熟练使用 Spring Cloud 可以跳过本文。

主要会涉及到注册中心、配置中心、服务容错、服务网关等内容。

 

微服务简介

 

如何理解微服务?

把服务拆的相对小,独立部署的功能单元,微服务是更细粒度的SOA。

 

微服务可能引入的问题?

  • 服务划分过细,服务间关系复杂
  • 服务数量太多,团队效率急剧下降
  • 调用链太长,性能下降
  • 调用链太长,问题定位困难
  • 没有自动化支撑,无法快速交付
  • 没有服务治理,微服务数量多了后管理混乱

 

微服务拆分示例

 

初步认识 Spring Cloud

首先聊到Spring Cloud 不得不提及Spring 和 Spring Boot

Spring 程序员的春天,现在大多叫Java工程师不如说叫Spring 工程师。。。

首先Spring的强大之处在于核心的IOC、AOP能力,以Bean为中心的一套生命周期管理机制,大大的解放了程序员;

那么Spring Boot又是什么鬼,是帮助我们快速使用Spring搭建应用,提供了自动装配的能力;

Spring Cloud就是以Spring Boot为基础搭建微服务的工具集合。

 

Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g. configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, one-time tokens, global locks, leadership election, distributed sessions, cluster state). Coordination of distributed systems leads to boiler plate patterns, and using Spring Cloud developers can quickly stand up services and applications that implement those patterns. They will work well in any distributed environment, including the developer’s own laptop, bare metal data centres, and managed platforms such as Cloud Foundry.

 

功能的话就是微服务治理那一套了

比如服务注册发现,服务路由,负载均衡,断路器等等。

 

Spring Cloud 华山论剑

Spring Cloud Netflix

Spring Cloud Alibaba

Spring Cloud Amazon

Spring Cloud Azure

 

Netflix套件用的还是相对比较多的,因此我们本文还是主要聚焦Spring Cloud Netflix。

 

Spring Cloud  VS Dubbo

 

实际上Dubbo也逐渐从纯粹的rpc框架演进成一套Dubbo生态

Spring Cloud主要在体系完整、灵活,组件透明 通用抽象)

Spring Cloud也支持DubboGrpc

 

技术选型

  • 生产级线上运维管理稳定
  • 一级互联网公司落地产品
  • 开源社区活跃
  • 结合公司业务和技术现状

 

一些思考

业内技术更新迭代很快,因此要抓住一些技术的本质和理念

业务驱动技术,而不是技术驱动业务

互联网没有银弹,最适合的才是最好的

 

 

即将进入正文。。。

 

 

Spring Cloud Netflix组件介绍

 

Eureka注册中心地位就是我们的kess,用于服务注册发现

Spring Cloud Config地位就是我们的配置中心Kconf,Spring Cloud Config功能不够齐全,在生产上还需要额外的配置和开发。配置中心需要提供配置的可视化和版本控制、更改线上及时生效等功能。业界目前比较主流的是携程开源的Apollo,配置的下发是推拉结合的形式。

Zuul是服务网关,是微服务的大门,集成了认证鉴权、服务路由、负载均衡、限流等功能。可以简单把服务网关理解成服务路由 + 过滤器。还有一个功能强大的业内产品Spring Cloud GateWay。

Hystrix是一个服务容错组件,提供了功能降级、断路器、资源隔离、请求缓存和请求合并等功能。

Feign是一个声明式http客户端,Ribbon是一个客户端负载均衡,下文会和Eureka结合简单介绍一下。

Spring Cloud Sleuth 可以简单理解成对Zipkin的封装,是Spring Cloud提供的链路追逐功能。

Spring Cloud Stream 是对消息队列的一种抽象,底层是用RabbitMQ还是Kafka啊都是对业务透明的。

Spring Cloud Bus 是对发布分布式事件的一个封装,实际就是往消息队列发布一个Remote Event。

 

 

注册中心

 

注册中心维护服务提供方的ip:port列表

服务消费者根据被调用标识拿到一组服务ip:port

本质也是增删改查,针对服务的增删改查

 

 

Netflix 套件核心维护者逐一跑路,很多组件停止维护

 

开始搭建第一个Spring Cloud 应用 – Eureka 注册中心

 

依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

 

配置文件

server:
  port: 8882

spring:
  application:
    name: service-consumer

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka/

 

package pers.wmx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer   // 声明Eureka Server
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

真的是开箱即用,一分钟就阔以搭建一个单机版的注册中心,也不要第三方存储,Eureka是以内存存储服务实例信息的

启动Eureka Server

再搭建下服务提供者和服务消费者

 

引入依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

 

【服务提供者】

server:
  port: 8881

spring:
  application:
    name: service-provider

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true  # 拉取服务注册表
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka/  # 注册中心地址
package pers.wmx.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient   // 开启服务注册发现功能
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

 

package pers.wmx.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author wangmingxin03
 * Created on 2020-12-19
 */
@RestController
public class DemoController {
    @GetMapping("/helloSpringCloud")
    public String hello() {
        return "hello spring cloud !!!";
    }
}

 

【服务消费者】

server:
  port: 8882

spring:
  application:
    name: service-consumer

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka/
@SpringBootApplication
@EnableCircuitBreaker   // 开启断路器
@EnableFeignClients     // 开启Feign
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

 

package pers.wmx.demo;

import java.util.List;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;

/**
 * @author wangmingxin03
 * Created on 2020-12-19
 */
@RestController
public class DemoController {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private FeignDemoClient feignDemoClient;

    @GetMapping("/helloFeign")
    public String helloFeign() {
        return feignDemoClient.hello();
    }

    @GetMapping("/hello")
    @HystrixCommand(fallbackMethod = "fallBack")
    public String hello() {
        // 获取服务实例列表
        List<ServiceInstance> instances = discoveryClient
                .getInstances("service-provider");
        // 选择第一个
        ServiceInstance instance = instances.size() > 0 ? instances.get(0) : null;
        if (instance == null) {
            throw new IllegalStateException("not found service");
        }

        String serviceUrl = instance.getUri() + "/helloSpringCloud";
        return restTemplate.getForObject(serviceUrl, String.class);
    }

    public String fallBack(Throwable throwable) {
        logger.info("call error:{}", ExceptionUtils.getRootCauseMessage(throwable));
        return "mock return";
    }

}

 

访问Eureka注册中心,服务提供者和消费者都已经注册上去了

请求接口发起正常调用

 

Spring Cloud 注册中心替换底层实现 ZK,Etcd,Consul

 

Eureka缓存机制

 

Eureka不持久化存储!!! 不依赖第三方存储

完全依赖内存

服务注册表就是一个Map

 

注册表原始数据 com.netflix.eureka.registry.AbstractInstanceRegistry#registry

客户端初始化时会拉取全量注册表信息

 

注册表里存储的实例信息:

com.netflix.appinfo.InstanceInfo

 

一些重要的字段:

// ...

 // The (fixed) instanceId for this instanceInfo. This should be unique within the scope of the appName.
    private volatile String instanceId;

    private volatile String appName;
    @Auto
    private volatile String appGroupName;

    private volatile String ipAddr;

private volatile int port = DEFAULT_PORT;

// ...

 

【注册流程】

客户端http调用

DiscoveryClient -> register

boolean register() throws Throwable {
        logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
        EurekaHttpResponse<Void> httpResponse;
        try {
            httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
        } catch (Exception e) {
            logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
            throw e;
        }
        if (logger.isInfoEnabled()) {
            logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
        }
        return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
    }

 

客户端发起http调用

public EurekaHttpResponse<Void> register(InstanceInfo info) {
        String urlPath = this.serviceUrl + "apps/" + info.getAppName();
        HttpHeaders headers = new HttpHeaders();
        headers.add("Accept-Encoding", "gzip");
        headers.add("Content-Type", "application/json");
        ResponseEntity<Void> response = this.restTemplate.exchange(urlPath, HttpMethod.POST, new HttpEntity(info, headers), Void.class, new Object[0]);
        return EurekaHttpResponse.anEurekaHttpResponse(response.getStatusCodeValue()).headers(headersOf(response)).build();
    }

 

Eureka Server 处理注册请求

@POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info,
                                @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
        logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
        
        // validate that the instanceinfo contains all the necessary required fields
        // ... 参数校验 ...

        // handle cases where clients may be registering with bad DataCenterInfo with missing data
        // ...

        registry.register(info, "true".equals(isReplication));
        return Response.status(204).build();  // 204 to be backwards compatible
    }

    @Override
    public void register(final InstanceInfo info, final boolean isReplication) {
        int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
        if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
            leaseDuration = info.getLeaseInfo().getDurationInSecs();
        }

        // 发起注册
        super.register(info, leaseDuration, isReplication);

        // 同步到其他节点
        replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
    }

 

核心register方法臭长臭长的,我们挑一部分瞅瞅

服务实例状态

public enum InstanceStatus {
        UP, // Ready to receive traffic
        DOWN, // Do not send traffic- healthcheck callback failed
        STARTING, // Just about starting- initializations to be done - do not
        // send traffic
        OUT_OF_SERVICE, // Intentionally shutdown for traffic
        UNKNOWN;

服务操作类型

public enum ActionType {
        ADDED, // Added in the discovery server
        MODIFIED, // Changed in the discovery server
        DELETED
        // Deleted from the discovery server
    }

 

    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        try {
            read.lock();

            // 从注册表拿服务注册信息
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
            
            // ...     

            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());

            // Retain the last dirty timestamp without overwriting it, if there is already a lease
            // 处理一些时间和租期相关的问题...

            Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
            if (existingLease != null) {
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            gMap.put(registrant.getId(), lease);


            // 这个 recentRegisteredQueue 主要是用于调试使用,可以不用关心
            synchronized (recentRegisteredQueue) {
                recentRegisteredQueue.add(new Pair<Long, String>(
                        System.currentTimeMillis(),
                        registrant.getAppName() + "(" + registrant.getId() + ")"));
            }

            // 处理服务实例状态   
            // This is where the initial state transfer of overridden status happens
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);

            // If the lease is registered with UP status, set lease service up timestamp
            if (InstanceStatus.UP.equals(registrant.getStatus())) {
                lease.serviceUp();
            }
            registrant.setActionType(ActionType.ADDED);

            // 把变更信息扔到一个linkedQueue里
            recentlyChangedQueue.add(new RecentlyChangedItem(lease));

            registrant.setLastUpdatedTimestamp();

            // 把对应服务实例注册信息的本地缓存置为失效,然后就会触发更新本地混存
            invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
            logger.info("Registered instance {}/{} with status {} (replication={})",
                    registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
        } finally {
            read.unlock();
        }
    }

 

注册信息会扔到recentlyChangedQueue

我们再来看下recentlyChangedQueue的读取

 

读取代码就在这里啦

com.netflix.eureka.registry.AbstractInstanceRegistry#getApplicationDeltasFromMultipleRegions

write.lock();
            Iterator<RecentlyChangedItem> iter = this.recentlyChangedQueue.iterator();
            logger.debug("The number of elements in the delta queue is :{}", this.recentlyChangedQueue.size());
            while (iter.hasNext()) {
                Lease<InstanceInfo> lease = iter.next().getLeaseInfo();
                InstanceInfo instanceInfo = lease.getHolder();
                logger.debug("The instance id {} is found with status {} and actiontype {}",
                        instanceInfo.getId(), instanceInfo.getStatus().name(), instanceInfo.getActionType().name());
                Application app = applicationInstancesMap.get(instanceInfo.getAppName());
                if (app == null) {
                    app = new Application(instanceInfo.getAppName());
                    applicationInstancesMap.put(instanceInfo.getAppName(), app);
                    apps.addApplication(app);
                }
                app.addInstance(new InstanceInfo(decorateInstanceInfo(lease)));
            }

什么时候会触发增量读取呢 getApplicationDeltasFromMultipleRegions

前面代码里不是有invalidateCache嘛,就是这里触发的

本地缓存的构建

在初始化Eureka Server时就会构建

com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#init

@Override
    public void init(PeerEurekaNodes peerEurekaNodes) throws Exception {
        this.numberOfReplicationsLastMin.start();
        this.peerEurekaNodes = peerEurekaNodes;

        // 构建缓存
        initializedResponseCache();
        scheduleRenewalThresholdUpdateTask();
        initRemoteRegionRegistry();

        try {
            Monitors.registerObject(this);
        } catch (Throwable e) {
            logger.warn("Cannot register the JMX monitor for the InstanceRegistry :", e);
        }
    }

 

ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
        this.serverConfig = serverConfig;
        this.serverCodecs = serverCodecs;
        this.shouldUseReadOnlyResponseCache = serverConfig.shouldUseReadOnlyResponseCache();
        this.registry = registry;

        long responseCacheUpdateIntervalMs = serverConfig.getResponseCacheUpdateIntervalMs();

        // loadingCache 构建
        this.readWriteCacheMap =
                CacheBuilder.newBuilder().initialCapacity(serverConfig.getInitialCapacityOfResponseCache())
                        .expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS)
                        .removalListener(new RemovalListener<Key, Value>() {
                            @Override
                            public void onRemoval(RemovalNotification<Key, Value> notification) {
                                Key removedKey = notification.getKey();
                                if (removedKey.hasRegions()) {
                                    Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
                                    regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
                                }
                            }
                        })
                        .build(new CacheLoader<Key, Value>() {
                            @Override
                            public Value load(Key key) throws Exception {
                                if (key.hasRegions()) {
                                    Key cloneWithNoRegions = key.cloneWithoutRegions();
                                    regionSpecificKeys.put(cloneWithNoRegions, key);
                                }

                                // 核心方法
                                Value value = generatePayload(key);
                                return value;
                            }
                        });

        // 是否开启只读缓存 -> 开启同步task
        if (shouldUseReadOnlyResponseCache) {
            timer.schedule(getCacheUpdateTask(),
                    new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
                            + responseCacheUpdateIntervalMs),
                    responseCacheUpdateIntervalMs);
        }

        try {
            Monitors.registerObject(this);
        } catch (Throwable e) {
            logger.warn("Cannot register the JMX monitor for the InstanceRegistry", e);
        }
    }

 

我们来看下 generatePayload

 

真正对外暴露的读服务

public String get(final Key key) {
        return get(key, shouldUseReadOnlyResponseCache);
    }


@VisibleForTesting
    String get(final Key key, boolean useReadOnlyCache) {
        Value payload = getValue(key, useReadOnlyCache);
        if (payload == null || payload.getPayload().equals(EMPTY_PAYLOAD)) {
            return null;
        } else {
            return payload.getPayload();
        }
    }

 

/**
     * Get the payload in both compressed and uncompressed form.
     */
    @VisibleForTesting
    Value getValue(final Key key, boolean useReadOnlyCache) {
        Value payload = null;
        try {

            // 是否开启只读缓存
            if (useReadOnlyCache) {
                final Value currentPayload = readOnlyCacheMap.get(key);
                if (currentPayload != null) {
                    payload = currentPayload;
                } else {

                    // 读缓存里没有的话就去写缓存(loadingCache)读,
                    // 读不到回源原始注册表,并会写到只读缓存里
                    payload = readWriteCacheMap.get(key);
                    readOnlyCacheMap.put(key, payload);
                }
            } else {
                payload = readWriteCacheMap.get(key);
            }
        } catch (Throwable t) {
            logger.error("Cannot get value for key : {}", key, t);
        }
        return payload;
    }

 

  // 只读缓存
    private final ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<Key, Value>();
    
    // 读写缓存
    private final LoadingCache<Key, Value> readWriteCacheMap;

 

上面在构建缓存的时候我们看到还起了一个task,其实就是开启了一个同步数据的任务,把写缓存的数据同步到读缓存

timer.schedule(getCacheUpdateTask()

private TimerTask getCacheUpdateTask() {
        return new TimerTask() {
            @Override
            public void run() {
                logger.debug("Updating the client cache from response cache");
                for (Key key : readOnlyCacheMap.keySet()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Updating the client cache from response cache for key : {} {} {} {}",
                                key.getEntityType(), key.getName(), key.getVersion(), key.getType());
                    }
                    try {
                        CurrentRequestVersion.set(key.getVersion());
                        Value cacheValue = readWriteCacheMap.get(key);
                        Value currentCacheValue = readOnlyCacheMap.get(key);
                        if (cacheValue != currentCacheValue) {
                            readOnlyCacheMap.put(key, cacheValue);
                        }
                    } catch (Throwable th) {
                        logger.error("Error while updating the client cache from response cache for key {}", key.toStringCompact(), th);
                    }
                }
            }
        };
    }

 

真正给客户端调用的也是一个接口

@GET
    public Response getApplication(@PathParam("version") String version,
                                   @HeaderParam("Accept") final String acceptHeader,
                                   @HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept) {
        if (!registry.shouldAllowAccess(false)) {
            return Response.status(Status.FORBIDDEN).build();
        }

        EurekaMonitors.GET_APPLICATION.increment();

        CurrentRequestVersion.set(Version.toEnum(version));
        KeyType keyType = Key.KeyType.JSON;
        if (acceptHeader == null || !acceptHeader.contains("json")) {
            keyType = Key.KeyType.XML;
        }

        Key cacheKey = new Key(
                Key.EntityType.Application,
                appName,
                keyType,
                CurrentRequestVersion.get(),
                EurekaAccept.fromString(eurekaAccept)
        );

        String payLoad = responseCache.get(cacheKey);

        if (payLoad != null) {
            logger.debug("Found: {}", appName);
            return Response.ok(payLoad).build();
        } else {
            logger.debug("Not Found: {}", appName);
            return Response.status(Status.NOT_FOUND).build();
        }
    }

无非就是构建key,查缓存

 

 

梳理下源码的流程,大概是这样子

 

Eureka自我保护机制

单机模式下如何触发:

频繁关闭和启动注册到Eureka的服务实例。

 

触发条件:

Eureka Server 节点在短时间内(可配置)丢失了过多实例(一定比例)连接时(比如网络故障或频繁的启动关闭客户端),

那么这个节点就会进入自我保护模式,一旦进入到该模式,Eureka server 就会保护服务注册表中的信息,

不再删除服务注册表中的数据。

 

意义:

不会从注册列表中剔除因长时间没收到心跳导致租期过期的服务,而是等待修复。

例如,两个服务A 和 B 的连通性是良好的,但是由于网络故障,B 未能及时向 Eureka 发送心跳续约,

这时候 Eureka 不会简单的将 B 从注册表中剔除。因为如果剔除了,A 就无法从 Eureka 服务器中获取 B 注册的服务,但是这时候 B 服务是可用的。

 

 

同样是注册中心,Eureka为什么优于Zk

Eureka是一个AP系统,Zk是一个CP系统

rpc场景下,AP一定是优于CP

Zk如果处于崩溃恢复模式,集群整体不可用

会有网络风暴甚至阻塞网络请求的风险

注册中心首先应该保证的是高可用性

Eureka server 可以构建多实例集群,避免单点问题,

同时又是去中心化的架构,没有master/slave的区别

保证高可用和最终一致性

 

 

 

Zk、Etcd和Consul都是CP系统,由于保证集群的数据一致性,会牺牲掉一部分可用性;

Nacos是阿里开源的注册中心/配置中心,功能比较强大,在业内很多中小型公司使用,可根据自主选择AP还是CP,根据是否是临时节点决定,是临时节点就走AP然后保证数据最终一致性,类似Eureka注册信息直接存在内存,持久化节点就走CP来保证数据一致性,底层也是raft协议;

Eureka是没有主从的概念,也不依赖第三方存储,基于多级缓存和客户端本地缓存保证的可用性;

我们的Kess底层存储也是Zk,但是基于我们上文介绍的一些设计,本身也是一个高可用的。

服务实例变更一般都是支持订阅推送,比如zk的watcher机制,这是注册中心一个基本的要求,还有一些是基于长轮询(Long Polling)形式,如Consul、Etcd。

健康检查的话一般都是客户端主动上报心跳,或者服务端主动去做节点状态探测。

Spring Cloud抽象了DiscoveryClient, 可以选择注册中心不同的底层实现。

 

 

Eureka结合FeignRibbon

然后简单介绍下Feign和Ribbon

@FeignClient(name = "service-provider")
public interface FeignDemoClient {
    @GetMapping("/helloSpringCloud")
    String hello();
}

 

 @Autowired
    private FeignDemoClient feignDemoClient;

    @GetMapping("/helloFeign")
    public String helloFeign() {
        return feignDemoClient.hello();
    }

 

发起服务调用,定一个接口,调用时一行搞定,非常简介好用

Ribbon是什么呢?

一种内嵌的客户端负载均衡,dubbo也是客户端负载均衡

还有代理负载均衡,如nginx

 

 

rpc调用场景下一般使用客户端负载均衡

客户端负载均衡实现有DubboRibbon

少了一层代理服务,性能会更好一些

一般会拉取注册中心服务列表,根据默认

或者配置的负载均衡策略来选取一个实例调用。

 

最后再简单介绍下美团的注册中心

美团 OCTO

  • 高性能
  • 多语言支持  (JavaPHP C/C++
  • 自由协议 thrift扩展协议)
  • 易用性
  • 稳定性

 

OCTO大图

 

 

节点状态探测  延迟double check

Spring Cloud 入坑宝典

 

 

COTO MESH化

 

对多语言支持不够好。

中间件和业务绑定在一起,制约着彼此迭代。

 

 

SideCar 边车模式

基础架构完全和业务解藕

稳定性和高可用!!!

SideCar也称DataPlane 数据面板

ControlPlane 控制面板,用于集中

管理和配置数据面板

 

 

 

边车模式会引入什么问题呢?

每一个业务服务实例,都得部署一个对应的基础组件

基础组件部署成本 > 业务服务部署成本

 

引入broker的概念,也是类似数据面板的作用,集成了基础组件

但是由一对一到多对一

 

配置中心

【传统项目配置管理的问题】

本地文件配置

静态变量配置

配置杂乱,不易维护

易由于配置问题引起线上问题

由于配置变更不能及时生效频繁上下线

没有版本控制功能

 

 

比如有下面这些场景

产品张三:这个配置的阈值线上快改一下吧

客户端李四:这个展示样式支持服务端动态下发吧

前端王五:把我们这些input标签和相关配置也都下发下吧

产品赵六:这个背景图片帮忙替换一下吧

测试刘七:帮忙加波白名单xxx,xxx,xxx

。。。

如果没有配置中心!!!

 

 

【配置中心基本要求】

支持可视化管理

支持变更实时生效

支持配置的回滚

支持不同环境的配置

 

Spring Cloud Config

功能不够齐全,且本身不支持配置及时生效,需要额外配置和开发

 

 

Apollo

携程开源的一个目前业内最流行的配置中心

 

配置中心相对简单,就不太多篇幅介绍

后续可以专门开博客一起对Apollo学习和介绍

 

服务容错

 

服务容错手段

  • 限流
  • 熔断
  • 降级
  • 隔离
  • 超时
  • 重试

 

 

 

Hystrix

Hystrix翻译过来中文意思是豪猪

重要功能:

1、服务降级

2、短路器机制

3、资源隔离

 

【服务降级】

 

【断路器状态】

 

【熔断流程】

 

舱壁模式

 

资源隔离

线程池隔离

 

信号量隔离

 

【请求缓存 & 请求合并】

 

 

最后给一个熔断的例子

 

@GetMapping("/hello")
    @HystrixCommand(fallbackMethod = "fallBack")
    public String hello() {
        // 获取服务实例列表
        List<ServiceInstance> instances = discoveryClient
                .getInstances("service-provider");
        // 选择第一个
        ServiceInstance instance = instances.size() > 0 ? instances.get(0) : null;
        if (instance == null) {
            throw new IllegalStateException("not found service");
        }

        String serviceUrl = instance.getUri() + "/helloSpringCloud";
        return restTemplate.getForObject(serviceUrl, String.class);
    }

    public String fallBack(Throwable throwable) {
        logger.info("call error:{}", ExceptionUtils.getRootCauseMessage(throwable));
        return "mock return";
    }

 

 

停止服务提供者时后,访问还显示连接失败,说明真正的发起了服务调用,

多次调用失败,触发断路器熔断,不会发起服务调用,直接走熔断降级

 

服务网关

服务网关的引入

 

微服务网关是一个处于微服务之前的系统,作为微服务环境面向外部服务访问者的唯一入口,用来管理授权、访问控制和流量路由等,这样服务就被网关保护起来,对所有的调用者透明。因此,隐藏在网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施。

 

网关部分主要介绍Zuul

Zuul功能列表

 

简单的看一小部分源码

就是对请求进行过滤、拦截、路由

Spring Cloud 入坑宝典

 

zuul过滤器接口

 

 

zuul过滤器类型

 

大致流程:

 

最后再简单介绍下饿了么网关

前端通过HTTP接口调用Stargate clusterStargate cluster进行一系列的鉴权校验,权限控制等,

然后把流量打到具体的后端业务系统。

通过配置就可以将RPC接口⾃动映射成HTTP API服务,自动生成http文档,HTTP服务自动化部署。

 

 

 

灾难切换可能会导致数据不一致

数据不会丢,但可能会延时复制

业务状态判断补偿。

能够监控哪些数据没有同步过来,禁止update操作

比如说不会发生切流前后支付2次的情况

 

切换条件:

单机房故障影响50%业务

可能导致比较大的 PR 事件

 

 

 

整体概述

 

 

 

 

 

转载请注明:汪明鑫的个人博客 » Spring Cloud 入坑宝典

喜欢 (3)

说点什么

您将是第一位评论人!

提醒
avatar
wpDiscuz