写点什么

【线上问题】记一次公司日志基础组件 SPI 使用不当导致业务中断

作者:Disaster
  • 2024-03-17
    上海
  • 本文字数:4623 字

    阅读完需:约 15 分钟

Informal Essay By English

It is always a pleasure to learn

背景

叮叮叮、叮叮叮....,某年某月某日晚上,上海某出租屋内,刚被放在桌上的手机的铃声在安静的屋内显得很 piercing。来电显示是一个广东电话号码,电话号码非常的熟悉,是系统的告警专用电话。我平静的打开电脑,打开钉钉,看了一下 alert 群内的异常信息。然后开始熟练的打开公司的日志平台,进行异常聚合搜索。嗯~,很好,有很多的异常,看来有的看了。然后 15 分钟后,不出意料的找到了异常的根因,这次告警有好几处异常,本文只分析、描述跟业务无关的异常。

问题描述

当时在日志平台上输出的异常如下:



由于完整的日志输出涉及到公司的代码, 这里只截图部分关键堆栈信息。抛出异常的类是属于基建日志组件包,贴一下异常抛出点的代码:


public class Operators {  static ServiceLoader<OperatorGetter> OperatorGetter = ServiceLoader.load(OperatorGetter.class);      public static Object current() {          //dosomething          for (OperatorGetter i : OperatorGetter) {              Object operator = i.currentOperator();              if (operator != null) {                  return operator;              }          }          return null;      }    }
复制代码

问题分析

问题出现在前端调用一个后端业务接口没有成功。在用户层面的来看,表现为用户触发一次业务请求没有成功。


java.util.NoSuchElementException 是 Java 编程语言中的一个异常类,属于 java.util 包。这个异常通常在试图访问一个枚举(Enumeration)、迭代器(Iterator)或者其他类型的集合中的元素,但已经没有更多的元素时抛出。


当时看到这个异常一开始以为是 META-INF/services/下面没有定义相关接口文件,但是后面通过分析拉到的 jar,发现里面有相应的接口定义文件与实现。到这里已经先排除 SPI 没有找到对应的实现类而抛出异常的场景。到这一步 SPI 的错误的使用方式场景我们已经排除,接下来就只能从 SPI 的实现角度去分析这个问题。SPI 这个知识点博主在之前的文章中已经有了详细的介绍,感兴趣的可以去看SPI详解 ,但是为了使文章能够顺畅的阅读下去,这里还是对 SPI 最核心的一些实现进行简单的描述。

SPI

Java 的 SPI(Service Provider Interface)是一种服务发现机制。它允许服务提供者在运行时被发现和加载,而不是在编译时硬编码。SPI 是一种为某些接口寻找服务实现的方式,是 Java 提供的一种原生的插件功能。它主要用于可以插拔的组件之间的解耦。


在 Java 的 SPI 机制中,服务提供者会在类路径下的 META-INF/services 目录中创建一个名字为服务接口全限定名的文件。该文件内部列出了实现该服务接口的具体实现类的全限定名。在运行时,Java 的 SPI 机制会查找这些配置文件,并加载并实例化这些实现类,从而实现了服务的动态查找与加载。


Java 的 SPI 广泛应用于 JDK 中,例如 java.sql.Driver 接口,JDBC 驱动就是通过 SPI 机制被加载的。应用程序可以通过 ServiceLoader 类来加载服务:


ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);for (MyService service : loader) {    // 使用service}
复制代码


这里,MyService 是服务接口,而具体的实现类可以在运行时通过放置在 META-INF/services 目录下的配置文件来指定。


SPI 的基本介绍完成,我们再来看看 SPI 的核心 api 的实现。


java.util.ServiceLoader#load(java.lang.Class<S>)


public static <S> ServiceLoader<S> load(Class<S> service) {    //获取应用类加载器        ClassLoader cl = Thread.currentThread().getContextClassLoader();        //调用了另一个load方法进行ServiceLoader对象的创建        return ServiceLoader.load(service, cl);    }
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { return new ServiceLoader<>(service, loader); }
复制代码


load 方法完成 ServiceLoader 对象的创建,其中需要我们关注的是在 ServiceLoader 构造器的中会调用一个 reload 方法,此方法会进行迭代器类的创建,此类是 SPI 最核心的实现类。


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);    }
复制代码


java.util.ServiceLoader.LazyIterator#hasNext


public boolean hasNext() {            if (acc == null) {                return hasNextService();            } else {                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {                    public Boolean run() { return hasNextService(); }                };                return AccessController.doPrivileged(action, acc);            }        }private boolean hasNextService() {            if (nextName != null) {                return true;            }            if (configs == null) {                try {                  //这里的PREFIX就是META-INF/services/                    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;        }
复制代码


本文不对 hasNextService()方法里面的各种处理去做详细的分析,但是有一个点需要我们知道的是,这个方法没有进行并发场景下的处理。


java.util.ServiceLoader.LazyIterator#next


public S next() {            if (acc == null) {                return nextService();            } else {                PrivilegedAction<S> action = new PrivilegedAction<S>() {                    public S run() { return nextService(); }                };                return AccessController.doPrivileged(action, acc);            }        }
private S nextService() { //这里的NoSuchElementException~~~~大家自己想象⛄️ if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { 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 { 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 }
复制代码


这个方法就是代码案例获取实例对象最终会调用的方法,这里的 if (!hasNextService())throw new NoSuchElementException();对于后面分析问题很重要~


至此,SPI 的使用与实现我们都有大概的了解。这里再针对 SPI 的并发问题做一个解释,SPI 本身的概念并不直接涉及线程安全问题。线程安全主要取决于 SPI 的具体实现。也就是说,一个服务提供者实现的线程安全性是由提供该服务的类或者库的作者来保证的。


到这里大家其实都已经知道这次的异常是什么原因导致。那我们就直接开始问题处理

问题处理

处理方式一:通过加锁进行处理,加锁又有 synchronized、juc lock 两种方式,下面贴下两种处理方式代码:


public class Operators {    static ServiceLoader<OperatorGetter> OperatorGetter = ServiceLoader.load(OperatorGetter.class);    static ReentrantLock lock = new ReentrantLock();    static Object monitor = new Object();
public static Object current() { CallContext context = CallContexts.get(); if (context != null) { return context.getOperator(); } lock.lock(); try { for (OperatorGetter i : OperatorGetter) { Object operator = i.currentOperator(); if (operator != null) { return operator; } } } finally { lock.unlock(); } synchronized (monitor){ for (OperatorGetter i : OperatorGetter) { Object operator = i.currentOperator(); if (operator != null) { lock.unlock(); return operator; } } } return null; }}
复制代码


处理方式二:static 方法块保证线程安全,代码如下:


public class Operators {    static ServiceLoader<OperatorGetter> OperatorGetter = ServiceLoader.load(OperatorGetter.class);    static Object operator;
static { for (OperatorGetter i : OperatorGetter) { Object object = i.currentOperator(); if (operator != null) { operator = object; } } }
public static Object current() { CallContext context = CallContexts.get(); if (context != null) { return context.getOperator(); } for (OperatorGetter i : OperatorGetter) { Object operator = i.currentOperator(); if (operator != null) { return operator; } } return null; }}
复制代码


最后提出一个问题,如果是你碰到这个问题,你会怎么去处理呢?

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

Disaster

关注

talk is cheap,show me the code 2021-12-29 加入

A coder who likes open source, graduated from Jishou University with a major in computer science and technology, has worked in the field of network security and Android, and is now constantly explorin

评论

发布
暂无评论
【线上问题】记一次公司日志基础组件SPI使用不当导致业务中断_bug_Disaster_InfoQ写作社区