我们以开源项目 Sentinel 为例说明流量防护功能在 Dubbo 微服务治理[1]中的具体应用。
并以此为切入点,考察 Dubbo 的扩展性机制如何实现及 Sentinel 如何通过 Dubbo 扩展点来实现流量防护。
流量防护综述
Sentinel 升级为 2.0 版本,作为 OpenSergo[2]数据面的实现标准,对接 OpenSergo 控制面的流量防护治理能力,其标准化模型可用下图表示。
可以看到一个容错治理规则 (FaultToleranceRule) 由以下三部分组成:
接下来,我们看容错治理规则在 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.SentinelDubboProviderFilter
sentinel.dubbo.consumer.filter=com.alibaba.csp.sentinel.adapter.dubbo.SentinelDubboConsumerFilter
dubbo.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.DubboInternalLoadingStrategy
org.apache.dubbo.common.extension.DubboLoadingStrategy
org.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 后,实际上除了创建内部对象外,什么都没做。
// 执行load
public 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
评论