写点什么

横看 Dubbo- 微服务治理之流量防护

作者:Karl
  • 2023-06-17
    浙江
  • 本文字数:7872 字

    阅读完需:约 26 分钟

横看Dubbo-微服务治理之流量防护

我们以开源项目 Sentinel 为例说明流量防护功能在 Dubbo 微服务治理[1]中的具体应用。

并以此为切入点,考察 Dubbo 的扩展性机制如何实现及 Sentinel 如何通过 Dubbo 扩展点来实现流量防护。

流量防护综述

Sentinel 升级为 2.0 版本,作为 OpenSergo[2]数据面的实现标准,对接 OpenSergo 控制面的流量防护治理能力,其标准化模型可用下图表示。


可以看到一个容错治理规则 (FaultToleranceRule) 由以下三部分组成:

  • Target: 针对什么样的请求

  • Strategy: 容错或控制策略,如流控、熔断、并发控制、自适应过载保护、离群实例摘除等

  • FallbackAction: 触发后的 fallback 行为,如返回某个错误或状态码


接下来,我们看容错治理规则在 Sentinel 中具体是怎样实现的。

注:图片及流量防护与容错的规则说明来自于流量防护与容错[3]


流量防护机制

在 Dubbo 经典的架构图基础上,本图标注出 Sentinel 在进行流量防护时基于 Dubbo 扩展点机制在 consumer 及 provider 侧做的增强逻辑,即 SentinelDubboConsumerFilter 和 SentinelDubboProviderFilter。

SentinelDubboConsumerFilter 在 consumer 侧起作用,实践上可进行集群限流、熔断等;

SentinelDubboProviderFilter 在 provider 侧起作用,实践上可进行限流、降级等。

Filter 本身所做的动作包括获取接口、执行流量防护规则及规则触发后的行为设置,对应于容错治理规则。

注:上图 Filter 扩展点参考 sentinel-apache-dubbo-adapter 模块[4]

Dubbo 使用 sentinel 进行流量防护的实践可参考[5]


如何进行流量防护

在了解 Dubbo 进行流量防护治理机制后,我们通过源码进一步观察 Sentinel 如何实现上述容错治理规则。

以 provider 侧的扩展点 SentinelDubboProviderFilter 为例。

可以看到主要经历以下三步,完成完整的流量防护机制:

Step1: 获取接口(Target,针对怎样的流量)

Step2: 执行流量治理策略(Strategy,容错或控制策略)

Step3: 触发防护规则后返回 fallback 行为(FallbackAction,触发后的 fallback 行为)

具体位置可参考代码中的注释。


@Activate(group = PROVIDER)public class SentinelDubboProviderFilter extends BaseSentinelDubboFilter {    ...
@Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { ... // Step1: 获取接口(Target,针对怎样的流量) String interfaceResourceName = getInterfaceName(invoker, prefix); String methodResourceName = getMethodName(invoker, invocation, prefix); try { ... // Step2: 执行流量治理策略(Strategy,容错或控制策略) interfaceEntry = SphU.entry(interfaceResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.IN); methodEntry = SphU.entry(methodResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.IN, invocation.getArguments()); Result result = invoker.invoke(invocation); return result; } catch (BlockException e) { // Step3: 触发防护规则后返回fallback行为(FallbackAction,触发后的 fallback 行为) return DubboAdapterGlobalConfig.getProviderFallback().handle(invoker, invocation, e); } catch (RpcException e) { Tracer.traceEntry(e, interfaceEntry); Tracer.traceEntry(e, methodEntry); throw e; } finally { if (methodEntry != null) { methodEntry.exit(1, invocation.getArguments()); } if (interfaceEntry != null) { interfaceEntry.exit(); } ContextUtil.exit(); } }
复制代码

以上内容介绍了功能方面 Sentinel 时如何进行流量防护治理的说明,接下来我们看 Dubbo 在原理上是如何支持上述扩展点的。

扩展点机制原理

在 Dubbo 扩展点实现的原理上,重点关注:

1)Dubbo 为何重新实现了一套 SPI 机制

2)Dubbo 如何实现一套新的 SPI 机制

为何不用 Java SPI

根据 Dubbo 官方文档:

Dubbo 中的扩展能力是从 JDK 标准的 SPI 扩展点发现机制加强而来,它改进了 JDK 标准的 SPI 以下问题:

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。

  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。

用户能够基于 Dubbo 提供的扩展能力,很方便基于自身需求扩展其他协议、过滤器、路由等。下面介绍下 Dubbo 扩展能力的特性。

  • 按需加载。Dubbo 的扩展能力不会一次性实例化所有实现,而是用哪个扩展类则实例化哪个扩展类,减少资源浪费。

  • 增加扩展类的 IOC 能力。Dubbo 的扩展能力并不仅仅只是发现扩展服务实现类,而是在此基础上更进一步,如果该扩展类的属性依赖其他对象,则 Dubbo 会自动的完成该依赖对象的注入功能。

  • 增加扩展类的 AOP 能力。Dubbo 扩展能力会自动的发现扩展类的包装类,完成包装类的构造,增强扩展类的功能。

  • 具备动态选择扩展实现的能力。Dubbo 扩展会基于参数,在运行时动态选择对应的扩展类,提高了 Dubbo 的扩展能力。

  • 可以对扩展实现进行排序。能够基于用户需求,指定扩展实现的执行顺序。

  • 提供扩展点的 Adaptive 能力。该能力可以使的一些扩展类在 consumer 端生效,一些扩展类在 provider 端生效。

注:以上内容来自于 Dubbo 自定义扩展[6]

而对于 Dubbo SPI 是如何实现性能提升的问题,我们先了解 Dubbo SPI 是如何实现的,再与 Java SPI 进行对比,看性能得到优化的原理是什么。


扩展点如何生效

在 sentinel-apache-dubbo-adapter 模块中,Filter 扩展点放置在

META-INF/dubbo/org.apache.dubbo.rpc.Filter 文件中,内容如下:

sentinel.dubbo.provider.filter=com.alibaba.csp.sentinel.adapter.dubbo.SentinelDubboProviderFiltersentinel.dubbo.consumer.filter=com.alibaba.csp.sentinel.adapter.dubbo.SentinelDubboConsumerFilterdubbo.application.context.name.filter=com.alibaba.csp.sentinel.adapter.dubbo.DubboAppContextFilter
复制代码

Dubbo 实现了一套 SPI 机制,并且可以通过配置文件方式生效,类似于 Java SPI,我们从 org.apache.dubbo.rpc.Filter 接口入手,观察调用的地方,发现 ProtocolFilterWrapper 类对象,可以看到 Filter 实现类的加载是通过 ExtensionLoader。


import org.apache.dubbo.rpc.Filter;...public class ProtocolFilterWrapper implements Protocol {	...    private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {        ...        List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);    	...        return last;    }
复制代码

注:Filter 被转换成 FilterNode 及 buildInvokerChain 函数被调用时机可追溯到 org.apache.dubbo.config.bootstrap.DubboBootstrap#start 的逻辑不再展开。

接下来通过 ExtensionLoader 类看 Dubbo 是如何实现 SPI 机制的,ExtensionLoader 在对外提供类对象时最终会调用到 getExtension 函数,我们从这里入手。

public class ExtensionLoader<T> {	...	public T getExtension(String name) {        return getExtension(name, true);    }
public T getExtension(String name, boolean wrap) { if (StringUtils.isEmpty(name)) { throw new IllegalArgumentException("Extension name == null"); } if ("true".equals(name)) { return getDefaultExtension(); } final Holder<Object> holder = getOrCreateHolder(name); Object instance = holder.get(); if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { instance = createExtension(name, wrap); holder.set(instance); } } } return (T) instance; }
复制代码


可以看到,获取实例化对象时,会首先尝试从缓存中获取,如果不存在,才会创建实例。紧接着,创建实例的第一步是解析出需要加载的类。


private T createExtension(String name, boolean wrap) {    Class<?> clazz = getExtensionClasses().get(name);    if (clazz == null || unacceptableExceptions.contains(name)) {        throw findException(name);    }    try {        T instance = (T) EXTENSION_INSTANCES.get(clazz);        if (instance == null) {            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());            instance = (T) EXTENSION_INSTANCES.get(clazz);        }        injectExtension(instance);      ...        return instance;    } catch (Throwable t) {        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +                type + ") couldn't be instantiated: " + t.getMessage(), t);    }}
复制代码


这里可分为两个阶段:加载类(getExtensionClasses())阶段与实例化类对象阶段(newInstance())。


实例化对象的逻辑较为清晰,我们重点关注 getExtensionClasses 函数,同样的,可以看到获取类对象时会先尝试从缓存中加载类,如果不存在,才会执行 loadExtensionClasses 真正加载扩展类。


    private Map<String, Class<?>> getExtensionClasses() {        Map<String, Class<?>> classes = cachedClasses.get();        if (classes == null) {            synchronized (cachedClasses) {                classes = cachedClasses.get();                if (classes == null) {                    classes = loadExtensionClasses();                    cachedClasses.set(classes);                }            }        }        return classes;    }
private Map<String, Class<?>> loadExtensionClasses() { cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
for (LoadingStrategy strategy : strategies) { loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages()); loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages()); }
return extensionClasses; }
复制代码


而在实际加载类阶段,用到了 strategies 本地变量,查看 strategies 初始化的地方,可以看到是通过 loadLoadingStrategies 函数初始化,此处正是通过 Java SPI 加载所需的类对象。


private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
private static LoadingStrategy[] loadLoadingStrategies() { return stream(load(LoadingStrategy.class).spliterator(), false) .sorted() .toArray(LoadingStrategy[]::new);}
复制代码


其中的 load 函数是 ServiceLoader.load(),打开 SPI 文件,可以看到 Dubbo 加载自定义 SPI 时所用到的策略类对象。META-INF/services/org.apache.dubbo.common.extension.LoadingStrategy


org.apache.dubbo.common.extension.DubboInternalLoadingStrategyorg.apache.dubbo.common.extension.DubboLoadingStrategyorg.apache.dubbo.common.extension.ServicesLoadingStrategy
复制代码

以 DubboLoadingStrategy 类为例,可以看到加载的目录正是"META-INF/dubbo/",即 Sentinel 流量防护 Filter 扩展点设置的位置。

public class DubboLoadingStrategy implements LoadingStrategy {
@Override public String directory() { return "META-INF/dubbo/"; } ...}
复制代码

至此,我们完成了 Sentinel 流量防护扩展点在 Dubbo 中如何生效的机制解析。

有了以上铺垫后,接下来再来看上文提出的 Dubbo 为何不用 Java SPI,要另实现一套,性能究竟是如何提升的。

Dubbo SPI 如何提升性能

在说明 Dubbo SPI 如何提升性能之前,先简要回顾下 Java SPI 的机制,通过对比突出 Dubbo SPI 的设计优越性。

Java SPI 加载机制

解说样例:

    ServiceLoader<Hello> protocols = ServiceLoader.load(Hello.class);    Iterator<Hello> iterator = protocols.iterator();    while (iterator.hasNext()){        Hello protocol = iterator.next();        protocol.sayHello();    }
复制代码


我们对 Java SPI 的使用已经十分熟悉,直接到源码阅读环节。

可以看到 ServiceLoader 在执行 load 后,实际上除了创建内部对象外,什么都没做。

// 执行loadpublic static <S> ServiceLoader<S> load(Class<S> service) {    ClassLoader cl = Thread.currentThread().getContextClassLoader();    return ServiceLoader.load(service, cl);}
// 初始化对象private ServiceLoader(Class<S> svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload();}// 初始化对象public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader);}
复制代码


所以类的解析、加载和实例化是什么时候做的?

答案是 iterator()方法执行时。

源码中显示 knownProviders 做了简单的缓存,核心逻辑则在 LazyIterator lookupIterator 对象中。

   public Iterator<S> iterator() {        return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
public boolean hasNext() { if (knownProviders.hasNext()) return true; return lookupIterator.hasNext(); }
public S next() { if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); }
public void remove() { throw new UnsupportedOperationException(); }
}; }
复制代码


LazyIterator 的核心成员变量如下:


接下来分析 LazyIterator 实例化类对象的过程。

1)类的解析:hasNextService 函数中判断是否存在实现类

private class LazyIterator    implements Iterator<S>{  ...    private boolean hasNextService() {        if (nextName != null) {            return true;        }        if (configs == null) {            try {                String fullName = PREFIX + service.getName();                if (loader == null)                    configs = ClassLoader.getSystemResources(fullName);                else                    configs = loader.getResources(fullName);            } catch (IOException x) {                fail(service, "Error locating configuration files", x);            }        }        while ((pending == null) || !pending.hasNext()) {            if (!configs.hasMoreElements()) {                return false;            }            //             pending = parse(service, configs.nextElement());        }        nextName = pending.next();        return true;    }    ...
复制代码


2)类的加载和实例化:nextService 函数中

    private S nextService() {        if (!hasNextService())            throw new NoSuchElementException();        String cn = nextName;        nextName = null;        Class<?> c = null;        try {            // Step 1: 加载类对象            c = Class.forName(cn, false, loader);        } catch (ClassNotFoundException x) {            fail(service,                 "Provider " + cn + " not found");        }        if (!service.isAssignableFrom(c)) {            fail(service,                 "Provider " + cn  + " not a subtype");        }        try {            // Step 2: 实例化类对象            S p = service.cast(c.newInstance());            providers.put(cn, p);            return p;        } catch (Throwable x) {            fail(service,                 "Provider " + cn + " could not be instantiated",                 x);        }        throw new Error();          // This cannot happen    }
复制代码


到此,完成 ServiceLoader 源码解析。

提升性能的关键点

可以看到 Java SPI 的类实例化过程一气呵成,毫无间断。

而这正是 Dubbo SPI 在性能方面可以有更好表现的基础,具体而言,Dubbo SPI 只是加载配置文件中的类,并分成不同的种类缓存在内存中,而不会立即全部初始化。

结合‘扩展点如何生效’小节,缓存的使用分为如下两个方面:

1)Class 缓存:Dubbo SPI 获取扩展类是,会先从缓存中读取,如果不存在,则加载配置文件,根据配置把 Class 缓存到内存中,而不会直接全部初始化。

2)实例缓存:每次获取实例的时候,会先从缓存中读取,如果不存在,才会实例化并缓存起来。

可以看到,Dubbo SPI 缓存的 Class 并不会全部实例化,而是按需实例化并缓存,因此性能更好。


以上。


如果本文对您有帮助,欢迎关注[微服务骑士]公众号并转发~

声明:本文言论仅代表个人观点。


参考资料

[1] Dubbo 服务治理:https://cn.dubbo.apache.org/zh-cn/overview/what/overview/

[2] OpenSergo 概念:https://opensergo.io/zh-cn/docs/what-is-opensergo/concepts/

[3] 流量防护与容错:https://github.com/opensergo/opensergo-specification/blob/main/specification/zh-Hans/fault-tolerance.md

[4] Sentinel dubbo adapter:https://github.com/alibaba/Sentinel/tree/master/sentinel-adapter/sentinel-apache-dubbo-adapter

[5] Sentinel 限流:https://cn.dubbo.apache.org/zh-cn/overview/tasks/rate-limit/sentinel/

[6] 自定义扩展:https://cn.dubbo.apache.org/zh-cn/overview/tasks/extensibility/#%E5%9F%BA%E4%BA%8E-java-spi-%E7%9A%84%E6%89%A9%E5%B1%95%E8%83%BD%E5%8A%9B%E8%AE%BE%E8%AE%A1


发布于: 刚刚阅读数: 5
用户头像

Karl

关注

还未添加个人签名 2020-02-13 加入

还未添加个人简介

评论

发布
暂无评论
横看Dubbo-微服务治理之流量防护_原创_Karl_InfoQ写作社区