从源代码出发,Jenkins 任务排队时间过长问题的解决过程

最近开发了一个部署相关的工具,使用 Jenkins 来构建应用。Jenkins 的任务从模板中创建而来。每次部署时,通过 Jenkins API 来触发构建任务。在线上运行时发现,通过 API 触发的 Jenkins 任务总是会时不时在队列中等待较长的时间。某些情况下的等待时间甚至长达几分钟。直接在 Jenkins 界面上触发的任务却几乎不需要排队,直接马上就可以执行。过长的等待时间影响了构建的效率,这是一个急需解决的问题。这个问题奇怪的地方在于,手动从界面上触发的任务几乎不需要排队,而 API 触发的任务的排队时间则完全随机,毫无规律可言。

从源代码出发,Jenkins 任务排队时间过长问题的解决过程_第1张图片

当任务在队列中时,Jenkins 会在界面上显示该任务在队列中等待的原因。对于 API 创建的任务,它们的等待原因是“Finished waiting”。从这个原因的字面含义确实看不出来什么。当出现这样的问题时,最直接的办法是从源代码中寻找答案。

从 GitHub 上把 Jenkins 的源代码下载到本地。寻找问题的起点是搜索字符串“Finished waiting”。这个字符串定义在资源包(resource bundle)中,对应的键是 Queue.FinishedWaiting。再搜索使用了这个键的代码,定位到了类 hudson.model.Queue。从名字可以看出来,这个类表示的是 Jenkins 内部的工作队列。在这个类中定义了可能出现在队列中的不同类型的条目。类 WaitingItem 表示的是处于等待状态的条目。这个类的 getCauseOfBlockage()方法刚好用到了Queue.FinishedWaiting 这个键,表示当前条目被阻塞的原因。这个 WaitingItem 类是主要的调查目标。

WaitingItem 类中有 enter() 和 leave() 两个方法,分别表示该条目进入队列和离开队列。看起来那些等待时间过长的任务,在进入了队列之后,经过很长一段时间,它们的 leave() 方法才被调用。

从源代码出发,Jenkins 任务排队时间过长问题的解决过程_第2张图片

继续追踪这两个方法的调用,会发现 enter() 方法在 scheduleInternal() 中被调用,用来调度新的任务。而 leave() 方法则在 maintain() 中被调用,用来在合适的时机从队列中移除任务并执行。所以看起来问题是出在 maintain() 方法中。

maintain() 方法负责维护队列,并把任务在不同的状态中移动。当有可能影响到任务调度的情况发生时,Jenkins 会在内部调用这个方法。在 scheduleInternal() 方法中,把新的任务添加到队列之后,它会调用 scheduleMaintenance() 方法提交一个 Runnable 任务来调用 maintain() 方法。

从源代码出发,Jenkins 任务排队时间过长问题的解决过程_第3张图片

经过上面的分析,任务等待时间过长的原因可能是,maintain() 方法被调用时,并没有发现处于等待的任务。所以这个任务需要等到下一次 maintain() 方法调用时才会被执行。这中间可能有与时序相关的问题。


问题找到了之后,下一步考虑的是怎么解决问题。如果要从根本上解决问题,应该从 Jenkins 入手,尝试在 Jenkins 中找到问题发生的根源,并进行修复。这种方案要求对 Jenkins 的代码库有足够程度的了解,在本地构建开发环境,并尝试稳定地复现问题。找到问题并解决之后,还需要添加 Pull Request 到 Jenkins 的代码库,并等待新版本的发布。整个过程耗时漫长。作为 Jenkins 的使用者,我自己并没有太大的意愿去花费精力在 Jenkins 自身的问题上。这种与时序有关的问题,很难复现和调试。我需要的是一个能够有效解决问题的 workaround。

前面提到了,产生问题的原因是 maintain() 方法在执行时可能没有发现等待中的任务。那么解决的办法可以很直接,那就是每次提交任务之后,等待几秒钟,再调用一次 maintain() 方法。这样就可以确保 maintain() 方法能够发现等待中的任务。maintain() 方法是由 scheduleMaintenance() 方法调用的,而 scheduleMaintenance() 是 Queue 类的一个公开方法。我只需要能够调用这个 scheduleMaintenance() 方法就可以了。

Jenkins 有一个叫做脚本控制台的功能,可以在运行的 Jenkins 实例上执行 Groovy 脚本。Groovy 脚本可以直接对运行的 Jenkins 实例进行修改。

从源代码出发,Jenkins 任务排队时间过长问题的解决过程_第4张图片

每个 Jenkins 运行的实例中,Queue 对象只有一个。只需要找到这个 Queue 对象,并调用其中的 scheduleMaintenance() 方法,问题就解决了。脚本控制台已经内置提供了很多 Jenkins 的对象。实际上需要执行的代码很简单。

Jenkins.instance.queue.scheduleMaintenance()

通过在脚本控制台进行测试,发现只要执行了上述的脚本,原本在队列中的任务,马上就可以被调度执行。这也证明了问题确实解决了。

最后一个问题是如何把对 scheduleMaintenance() 的调用自动化,也就是在每次通过 API 触发了任务,等待几秒钟之后,自动调用 scheduleMaintenance() 方法。Jenkins 的脚本控制台并没有提供公开的 API。我采取的做法是用 HTTP 客户端模拟脚本控制台界面上的操作。脚本控制台的界面在运行脚本时,实际上执行了一个表单提交动作。这个表单被提交到了网址 /computer/(built-in)/script,内容类型是 application/x-www-form-urlencoded。请求中只需要包含一个参数 script,表示需要执行的脚本内容。Jenkins 使用的是 BASIC 认证,需要把访问的用户名和密码包含在请求中。使用 HTTP 客户端模拟上述请求并不难。

下面给出了使用 Spring RestTemplate 执行 Groovy 脚本的代码示例。

var headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
var authentication = "Basic " + Base64.getEncoder()
    .encodeToString(String.format("%s:%s", username, password).getBytes(StandardCharsets.UTF_8));
headers.add("Authorization", authentication);
MultiValueMap map = new LinkedMultiValueMap<>();
map.add("script", "Jenkins.instance.queue.scheduleMaintenance()");
var entity = new HttpEntity<>(map, headers);
try {
  restTemplate.exchange(
      String.format("%s/computer/(built-in)/script", jenkinsUrl),
      HttpMethod.POST, entity,
      String.class);
} catch (Exception e) {
  // 处理异常
}

至此,Jenkins 任务排队时间过长的问题得到了解决。虽然并没有从根本上解决问题,但已经是一个在有限的时间内可以完成的不错的解法。

你可能感兴趣的:(jenkins,运维)