Jenkins架构与核心原理:深入理解CI/CD基础设施

1. Jenkins整体架构设计

1.1 架构概览

Jenkins采用经典的Master-Agent分布式架构设计,支持大规模构建任务的并行执行。整体架构遵循"控制器-执行器"模式,实现了任务调度与实际执行的解耦。

外部系统

Agent集群

Jenkins Master (控制器)

调度层

安全层

认证管理器

授权策略

安全域

存储层

JENKINS_HOME

config.xml

jobs/

plugins/

workspace/

核心引擎

Web服务器
Winstone/Jetty

Servlet容器

Jenkins核心

插件管理器

任务队列
Queue

调度器
Scheduler

负载均衡器

Agent 1
SSH/JNLP

Agent 2
SSH/JNLP

Agent N
SSH/JNLP

SCM
Git/SVN

Artifact存储
Nexus/Artifactory

通知系统
邮件/Slack

1.2 Master-Agent通信协议

Jenkins Master与Agent之间支持多种通信协议,每种协议适用于不同的网络环境:

SSH/JNLP Agent Master SSH/JNLP Agent Master alt [SSH协议连接] [JNLP协议连接] [WebSocket连接] SSH连接请求 启动Agent进程 建立TCP连接 发送Slave.jar 启动Agent引擎 注册成功 HTTP下载jnlp文件 返回agent.jnlp Java Web Start启动 建立TCP连接 认证握手 注册成功 WebSocket握手请求 升级为WebSocket 持久化双向通道 心跳保活

协议对比分析:

协议 连接方向 适用场景 安全性 穿透性
SSH Master→Agent Agent在公网、有SSH服务 高(密钥认证) 需开放SSH端口
JNLP Agent→Master Agent在内网、防火墙后 中(密钥认证) 仅需出站HTTP
WebSocket Agent→Master 网络受限环境 高(TLS加密) 穿透NAT/防火墙

1.3 核心设计原则

Jenkins架构设计遵循以下核心原则:

/**
 * Jenkins核心设计原则体现
 * 
 * 1. 可扩展性:所有核心功能通过ExtensionPoint实现
 * 2. 持久化:基于文件系统的轻量级存储
 * 3. 分布式:Master-Agent分离,支持横向扩展
 * 4. 安全性:多层安全架构,细粒度权限控制
 */

// 扩展点示例:自定义构建触发器
public class CustomTrigger extends Trigger<Job<?,?>> {
    
    /**
     * 通过ExtensionPoint实现功能扩展
     * Jenkins启动时自动扫描并注册
     */
    @Override
    public void start(Job<?,?> job, boolean newInstance) {
        super.start(job, newInstance);
        // 触发器初始化逻辑
    }
    
    @Override
    public void run() {
        // 触发构建执行
        if (shouldTrigger()) {
            job.scheduleBuild(new Cause.UserIdCause());
        }
    }
    
    private boolean shouldTrigger() {
        // 自定义触发条件判断
        return true;
    }
}

2. 核心组件深度解析

2.1 Jenkins核心引擎

Jenkins核心引擎基于Java Servlet规范构建,采用Standalone模式运行,内置Winstone(后迁移至Jetty)作为Web容器。

管理

持有

实现

继承

1

1

many

1

Jenkins

+List<ExtensionComponent> extensions

+List<Job> items

+Queue queue

+Computer[] computers

+getDescriptor(Class)

+getExtensionList(Class)

+getItems()

+getComputer(String)

ExtensionComponent

+Object instance

+ExtensionPoint getExtensionPoint()

+double ordinal()

«interface»

ExtensionPoint

Descriptor

+Class clazz

+String displayName

+boolean isApplicable(Class)

+newInstance(StaplerRequest, JSONObject)

Queue

+Item[] items

+BlockedItem[] blockedItems

+BuildableItem[] buildables

+schedule(Task, int, Cause)

+maintain()

Jenkins核心单例类\n管理所有扩展和任务

构建队列管理器\n负责任务调度

核心类职责说明:

类名 职责 生命周期
Jenkins 应用程序入口,管理全局状态 单例,随JVM生命周期
ExtensionComponent 扩展点包装器,支持排序 随扩展注册创建
Descriptor 描述符模式,管理配置元数据 单例,随Jenkins初始化
Queue 构建队列,管理任务调度 单例,随Jenkins初始化

2.2 插件系统架构

Jenkins插件系统是其核心优势之一,通过ExtensionPoint机制实现了高度可扩展性。

扩展注册中心

插件生命周期

类加载层次

Bootstrap ClassLoader
Java核心类

Extension ClassLoader
Jenkins核心

Plugin ClassLoader
插件隔离

插件发现
扫描META-INF/extensions

依赖解析
PluginClassLoader

类加载
隔离加载机制

实例化
反射创建实例

注册
ExtensionList添加

初始化
调用start方法

ExtensionList<Builder>

ExtensionList<SCM>

ExtensionList<Trigger>

ExtensionList<Publisher>

插件描述文件结构:

<!-- 
  文件路径: META-INF/maven/org.jenkins-ci.plugins/plugin-name/plugin.xml
  描述: 插件元数据定义,Jenkins启动时解析
-->
<plugin>
    <!-- 插件基本信息 -->
    <name>Git Plugin</name>
    <version>5.0.0</version>
    <description>Git SCM integration for Jenkins</description>
    
    <!-- 依赖声明 -->
    <dependencies>
        <dependency>
            <groupId>org.jenkins-ci.plugins</groupId>
            <artifactId>scm-api</artifactId>
            <version>3.0.0</version>
            <optional>false</optional>
        </dependency>
        <dependency>
            <groupId>org.jenkins-ci.plugins</groupId>
            <artifactId>credentials</artifactId>
            <version>2.6.0</version>
        </dependency>
    </dependencies>
    
    <!-- 扩展点声明 -->
    <extensions>
        <extension>
            <impl>org.jenkinsci.plugins.git.GitSCM</impl>
            <ordinal>100</ordinal>
        </extension>
    </extensions>
</plugin>

2.3 插件类加载机制

/**
 * Jenkins插件类加载器实现原理
 * 
 * 每个插件拥有独立的ClassLoader,实现类隔离
 * 避免不同插件间的类冲突
 */
public class PluginClassLoader extends URLClassLoader {
    
    private final PluginWrapper plugin;
    private final ClassLoader parent;
    private final Set<String> parentFirstPackages;
    private final Set<String> pluginFirstPackages;
    
    /**
     * 类加载优先级策略
     * 
     * 1. java.* 委托给父加载器(安全要求)
     * 2. parentFirstPackages 中的包委托给父加载器
     * 3. pluginFirstPackages 中的包优先从插件加载
     * 4. 其他类先尝试插件加载,失败后委托父加载器
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) 
            throws ClassNotFoundException {
        
        // 安全检查:禁止加载某些敏感类
        checkPackageAccess(name);
        
        // 已加载类缓存检查
        Class<?> loaded = findLoadedClass(name);
        if (loaded != null) {
            return loaded;
        }
        
        // 核心类委托给父加载器
        if (name.startsWith("java.") || isParentFirst(name)) {
            return parent.loadClass(name);
        }
        
        // 插件优先加载
        if (isPluginFirst(name)) {
            try {
                return findClass(name);
            } catch (ClassNotFoundException e) {
                return parent.loadClass(name);
            }
        }
        
        // 默认:先父加载器,后插件
        try {
            return parent.loadClass(name);
        } catch (ClassNotFoundException e) {
            return findClass(name);
        }
    }
    
    private boolean isParentFirst(String className) {
        return parentFirstPackages.stream()
            .anyMatch(pkg -> className.startsWith(pkg));
    }
    
    private boolean isPluginFirst(String className) {
        return pluginFirstPackages.stream()
            .anyMatch(pkg -> className.startsWith(pkg));
    }
}

3. 任务调度与执行模型

3.1 构建队列机制

Jenkins构建队列是任务调度的核心组件,负责管理待执行任务的排队、分配和执行。

任务创建

依赖未满足

可立即执行

依赖满足

取消/超时

分配Executor

取消

开始执行

Executor不可用

执行成功

执行失败

中止执行

Waiting

Blocked

Buildable

Cancelled

Pending

Building

Completed

Failed

Aborted

新任务初始状态
等待进入队列

可执行状态
等待分配Executor

执行中状态
占用Executor资源

队列数据结构实现:

/**
 * Jenkins Queue核心数据结构
 * 
 * 维护多个队列实现不同优先级和状态管理
 */
public class Queue {
    
    // 待处理任务队列(按优先级排序)
    private final SortedList<WaitingItem> waitingList = new SortedList<>();
    
    // 阻塞任务队列(等待资源/依赖)
    private final List<BlockedItem> blockedList = new ArrayList<>();
    
    // 可执行任务队列(等待Executor)
    private final List<BuildableItem> buildables = new ArrayList<>();
    
    // 待分配任务(已选择Executor)
    private final Map<Executor, BuildableItem> pending = new HashMap<>();
    
    /**
     * 任务调度主循环
     * 
     * 由Queue.Maintainer线程定期执行
     * 处理状态转换和资源分配
     */
    @SuppressWarnings("deprecation")
    public void maintain() {
        // 1. 清理已完成/取消的任务
        cleanupCompletedItems();
        
        // 2. 检查blocked任务是否可解除阻塞
        for (BlockedItem item : new ArrayList<>(blockedList)) {
            if (canRun(item)) {
                blockedList.remove(item);
                buildables.add(item.toBuildable());
            }
        }
        
        // 3. 检查waiting任务是否可进入buildable
        for (WaitingItem item : new ArrayList<>(waitingList)) {
            if (isReady(item)) {
                waitingList.remove(item);
                if (hasBlocker(item)) {
                    blockedList.add(new BlockedItem(item));
                } else {
                    buildables.add(new BuildableItem(item));
                }
            }
        }
        
        // 4. 为buildable任务分配Executor
        for (BuildableItem item : new ArrayList<>(buildables)) {
            Executor executor = findAvailableExecutor(item);
            if (executor != null) {
                buildables.remove(item);
                pending.put(executor, item);
                executor.start(item);
            }
        }
    }
    
    /**
     * 查找可用Executor
     * 
     * 考虑Label约束、负载均衡策略
     */
    private Executor findAvailableExecutor(BuildableItem item) {
        Label label = item.getAssignedLabel();
        
        // 获取满足Label约束的Computer列表
        List<Computer> computers = getComputers(label);
        
        // 负载均衡:选择负载最低的Computer
        Computer selected = computers.stream()
            .filter(c -> c.isOnline() && c.isAcceptingTasks())
            .min(Comparator.comparingInt(Computer::countBusy))
            .orElse(null);
        
        return selected != null ? selected.getIdleExecutor() : null;
    }
}

3.2 Executor工作原理

Agent Run WorkThread Executor Queue Agent Run WorkThread Executor Queue loop [每个BuildStep] alt [Agent连接成功] [Agent连接失败] 分配任务(BuildableItem) 创建WorkThread 设置任务上下文 获取Agent连接 返回Channel 创建Run实例 初始化环境变量 执行BuildStep 远程执行命令 返回执行结果 构建完成 释放Executor 通知任务完成 抛出异常 标记构建失败 释放Executor

Executor状态管理:

/**
 * Executor状态机实现
 * 
 * 管理单个执行线程的生命周期
 */
public class Executor extends Thread {
    
    private volatile State state = State.IDLE;
    private Run<?, ?> currentBuild;
    private long startTime;
    private long estimatedDuration;
    
    /**
     * Executor状态枚举
     */
    public enum State {
        IDLE,           // 空闲,可接受任务
        PENDING,        // 等待任务启动
        RUNNING,        // 正在执行任务
        COMPLETING,     // 任务完成中
        TERMINATING     // 正在终止
    }
    
    /**
     * 执行主循环
     */
    @Override
    public void run() {
        while (!isTerminated()) {
            try {
                // 等待任务分配
                synchronized (this) {
                    while (currentBuild == null && !isTerminated()) {
                        wait();
                    }
                }
                
                if (isTerminated()) break;
                
                // 执行构建
                state = State.RUNNING;
                startTime = System.currentTimeMillis();
                
                try {
                    currentBuild.run();
                } catch (Throwable t) {
                    // 记录构建失败
                    currentBuild.setResult(Result.FAILURE);
                } finally {
                    // 清理资源
                    state = State.COMPLETING;
                    cleanup();
                    
                    Run<?, ?> completed = currentBuild;
                    currentBuild = null;
                    state = State.IDLE;
                    
                    // 通知Queue任务完成
                    owner.getQueue().onCompleted(completed);
                }
                
            } catch (InterruptedException e) {
                // 响应中断
                if (currentBuild != null) {
                    currentBuild.getExecutor().interrupt();
                }
            }
        }
    }
    
    /**
     * 中止当前构建
     */
    public void interrupt(Result result) {
        if (currentBuild != null) {
            currentBuild.setResult(result);
            currentBuild.getExecution().stop();
        }
        super.interrupt();
    }
}

3.3 任务调度策略

优先级计算

调度决策流程

达到上限

未达上限

资源不足

资源充足

无匹配Agent

有匹配Agent

新任务入队

检查并发限制

进入Blocked状态

检查资源约束

进入Blocked状态

检查Label约束

进入Blocked状态

进入Buildable状态

等待条件满足

等待Executor分配

开始执行

基础优先级

+ Job权重

+ 等待时间补偿

+ 用户优先级

最终优先级

优先级计算实现:

/**
 * 任务优先级计算器
 * 
 * 综合多个因素计算任务执行优先级
 */
public class PriorityCalculator {
    
    /**
     * 计算任务优先级
     * 
     * @param item 待计算的任务项
     * @return 优先级值(数值越大优先级越高)
     */
    public int calculatePriority(Queue.Item item) {
        int priority = 0;
        
        // 1. 基础优先级(来自Job配置)
        priority += getJobPriority(item);
        
        // 2. 等待时间补偿(防止饥饿)
        long waitTime = System.currentTimeMillis() - item.getInQueueSince();
        priority += (int) (waitTime / 60000); // 每分钟+1
        
        // 3. 用户优先级(管理员可手动调整)
        priority += getUserPriority(item);
        
        // 4. 任务类型权重
        priority += getTypeWeight(item);
        
        return priority;
    }
    
    private int getJobPriority(Queue.Item item) {
        // 从Job配置中读取优先级
        Job<?, ?> job = item.task instanceof Job ? (Job<?, ?>) item.task : null;
        if (job != null && job.getProperty(PriorityJobProperty.class) != null) {
            return job.getProperty(PriorityJobProperty.class).getPriority();
        }
        return 0;
    }
    
    private int getTypeWeight(Queue.Item item) {
        // 不同类型任务的权重
        if (item.task instanceof FreeStyleProject) return 10;
        if (item.task instanceof WorkflowJob) return 15;
        if (item.task instanceof MatrixProject) return 5;
        return 0;
    }
}

4. 数据持久化机制

4.1 JENKINS_HOME目录结构

JENKINS_HOME目录结构

JENKINS_HOME/

config.xml
全局配置

jobs/
任务定义与历史

plugins/
插件存储

users/
用户配置

secrets/
敏感数据

workspace/
工作空间

builds/
构建历史

job-name/

config.xml
任务配置

builds/

workspace/

nextBuildNumber

plugin-name.jpi
插件文件

plugin-name.jpi.pinned
固定标记

master.key
主密钥

hudson.util.Secret
加密密钥

org.jenkinsci.plugins.plaincredentials
凭证存储

4.2 配置文件格式与解析

<!-- 
  文件: JENKINS_HOME/config.xml
  描述: Jenkins全局配置文件
  
  采用XStream序列化格式,支持版本迁移
-->
<hudson>
    <!-- 版本号,用于配置迁移 -->
    <version>2.400</version>
    
    <!-- 安全配置 -->
    <securityRealm class="hudson.security.HudsonPrivateSecurityRealm">
        <disableSignup>true</disableSignup>
        <enableCaptcha>false</enableCaptcha>
    </securityRealm>
    
    <authorizationStrategy class="hudson.security.RoleBasedAuthorizationStrategy">
        <!-- 角色权限配置 -->
    </authorizationStrategy>
    
    <!-- 全局工具配置 -->
    <jdk>
        <name>JDK 11</name>
        <home>/usr/lib/jvm/java-11-openjdk</home>
    </jdk>
    
    <maven>
        <name>Maven 3.8</name>
        <home>/usr/share/maven</home>
    </maven>
    
    <!-- 全局环境变量 -->
    <globalNodeProperties>
        <hudson.slaves.EnvironmentVariablesNodeProperty>
            <envVars serialization="custom">
                <unserializable-parents/>
                <tree-map>
                    <entry>
                        <string>JAVA_HOME</string>
                        <string>/usr/lib/jvm/java-11</string>
                    </entry>
                </tree-map>
            </envVars>
        </hudson.slaves.EnvironmentVariablesNodeProperty>
    </globalNodeProperties>
    
    <!-- Agent配置 -->
    <slaves>
        <hudson.slaves.DumbSlave>
            <name>agent-01</name>
            <remoteFS>/home/jenkins/agent</remoteFS>
            <numExecutors>2</numExecutors>
            <mode>NORMAL</mode>
            <retentionStrategy class="hudson.slaves.RetentionStrategy$Always"/>
            <launcher class="hudson.slaves.JNLPLauncher">
                <workDirSettings>
                    <disabled>false</disabled>
                    <internalDir>remoting</internalDir>
                </workDirSettings>
            </launcher>
            <label>linux docker</label>
        </hudson.slaves.DumbSlave>
    </slaves>
</hudson>

4.3 构建历史存储

/**
 * 构建历史存储机制
 * 
 * 每次构建生成独立的目录存储相关数据
 */
public class RunMap<S extends Job<S,R>, R extends Run<S,R>> 
        extends AbstractMap<Integer, R> {
    
    // 构建目录命名格式: builds/2024-01-15_10-30-45/
    private static final DateFormat DATE_FORMAT = 
        new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
    
    /**
     * 构建目录结构
     * 
     * builds/
     * ├── 1/                      # 构建编号
     * │   ├── build.xml           # 构建元数据
     * │   ├── log                 # 构建日志
     * │   ├── changelog.xml       # 变更记录
     * │   └── archive/            # 归档产物
     * ├── 2/
     * ├── 3/
     * └── permutations/           # Matrix构建子任务
     */
    
    /**
     * 加载构建历史
     * 
     * 懒加载策略:仅加载元数据,日志按需读取
     */
    public synchronized R loadBuild(File buildDir) throws IOException {
        // 读取build.xml获取构建元数据
        File buildXml = new File(buildDir, "build.xml");
        if (!buildXml.exists()) {
            return null;
        }
        
        // 使用XStream反序列化
        XStream xs = new XStream();
        R run = (R) xs.fromXML(buildXml);
        
        // 设置构建目录引用
        run.setBuildDir(buildDir);
        
        return run;
    }
    
    /**
     * 构建日志存储
     * 
     * 采用流式写入,支持实时查看
     */
    public static class BuildLog {
        
        private final File logFile;
        private RandomAccessFile writer;
        
        /**
         * 写入日志行
         * 
         * 追加模式,支持并发读取
         */
        public synchronized void writeLine(String line) throws IOException {
            if (writer == null) {
                writer = new RandomAccessFile(logFile, "rw");
                writer.seek(writer.length());
            }
            writer.writeBytes(line + "\n");
        }
        
        /**
         * 读取日志范围
         * 
         * 支持分页读取,避免内存溢出
         */
        public List<String> readLines(long offset, int count) throws IOException {
            List<String> lines = new ArrayList<>();
            try (RandomAccessFile reader = new RandomAccessFile(logFile, "r")) {
                reader.seek(offset);
                for (int i = 0; i < count; i++) {
                    String line = reader.readLine();
                    if (line == null) break;
                    lines.add(line);
                }
            }
            return lines;
        }
    }
}

4.4 敏感数据加密存储

密钥管理

解密流程

加密流程

AES-128-ECB

验证通过

验证失败

明文Secret

SecretCipher.encrypt

加密算法

使用ConfidentialKey

Base64编码

密文存储

密文读取

Base64解码

SecretCipher.decrypt

密钥验证

返回明文

抛出异常

master.key

随机生成
每次启动不同

hudson.util.Secret

加密存储
使用master.key加密

凭证加密实现:

/**
 * Jenkins凭证加密机制
 * 
 * 使用多层加密保护敏感数据
 */
public class Secret {
    
    private final byte[] iv;        // 初始化向量
    private final byte[] ciphertext; // 密文
    
    /**
     * 加密明文
     * 
     * 使用AES-128-CBC加密算法
     */
    public static Secret encrypt(String plaintext) {
        try {
            // 获取主密钥
            ConfidentialKey key = ConfidentialStore.get().getKey();
            
            // 生成随机IV
            byte[] iv = new byte[16];
            new SecureRandom().nextBytes(iv);
            
            // AES加密
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, 
                new SecretKeySpec(key.getPayload(), "AES"),
                new IvParameterSpec(iv));
            
            byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
            
            return new Secret(iv, encrypted);
            
        } catch (Exception e) {
            throw new RuntimeException("Encryption failed", e);
        }
    }
    
    /**
     * 解密密文
     */
    public String decrypt() {
        try {
            ConfidentialKey key = ConfidentialStore.get().getKey();
            
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE,
                new SecretKeySpec(key.getPayload(), "AES"),
                new IvParameterSpec(iv));
            
            byte[] decrypted = cipher.doFinal(ciphertext);
            return new String(decrypted, StandardCharsets.UTF_8);
            
        } catch (Exception e) {
            throw new RuntimeException("Decryption failed", e);
        }
    }
    
    /**
     * 序列化格式
     * 
     * 输出格式: {BASE64_IV}{BASE64_CIPHERTEXT}
     */
    @Override
    public String toString() {
        return "{" + Base64.getEncoder().encodeToString(iv) + "}" +
               Base64.getEncoder().encodeToString(ciphertext);
    }
}

5. 扩展点体系

5.1 ExtensionPoint机制

Jenkins扩展点(ExtensionPoint)是实现插件扩展的核心机制,所有可扩展功能都通过定义ExtensionPoint接口实现。

«interface»

ExtensionPoint

«abstract»

Builder

+build(AbstractBuild, Launcher, BuildListener)

+prebuild(AbstractBuild, BuildListener)

+getDescriptor()

«abstract»

SCM

+checkout(AbstractBuild, Launcher, FilePath, BuildListener, File)

+createChangeLogParser()

«abstract»

Trigger

+start(Job, boolean)

+run()

+stop()

«abstract»

Publisher

+perform(AbstractBuild, Launcher, BuildListener)

+needsToRunAfterFinalized()

«abstract»

SecurityRealm

+authenticate(String, String)

+loadUserByUsername(String)

«abstract»

AuthorizationStrategy

+getACL(Job)

+hasPermission(Authentication, Permission)

核心扩展点标记接口\n所有可扩展组件必须实现

构建步骤扩展\n如: Shell, Ant, Maven

版本控制扩展\n如: Git, SVN, Mercurial

触发器扩展\n如: Timer, SCM, Webhook

5.2 常用扩展点列表

扩展点 用途 典型实现
Builder 构建步骤 Shell, Ant, Maven, Gradle
SCM 版本控制 Git, Subversion, Mercurial
Trigger 构建触发 TimerTrigger, GitHubPushTrigger
Publisher 构建后操作 JUnitResultArchiver, Mailer
SecurityRealm 认证方式 LDAP, ActiveDirectory, OAuth
AuthorizationStrategy 授权策略 RoleBased, ProjectMatrix
LabelFinder Agent标签 动态标签分配
QueueSorter 队列排序 优先级调度
ToolInstallation 工具配置 JDK, Maven, Gradle
BuildWrapper 构建包装 环境变量, 超时控制

5.3 自定义扩展点实现

/**
 * 自定义Builder扩展点示例
 * 
 * 实现一个执行Docker命令的构建步骤
 */
public class DockerBuilder extends Builder implements SimpleBuildStep {
    
    private final String imageName;
    private final String command;
    private final boolean forcePull;
    
    /**
     * 构造函数(由Descriptor.newInstance调用)
     * 
     * @param imageName Docker镜像名称
     * @param command 容器内执行的命令
     * @param forcePull 是否强制拉取最新镜像
     */
    @DataBoundConstructor
    public DockerBuilder(String imageName, String command, boolean forcePull) {
        this.imageName = imageName;
        this.command = command;
        this.forcePull = forcePull;
    }
    
    /**
     * 构建执行入口
     * 
     * @param build 构建上下文
     * @param ws 工作空间
     * @param launcher 命令执行器
     * @param listener 日志输出
     */
    @Override
    public void perform(Run<?, ?> build, FilePath ws, Launcher launcher, 
            TaskListener listener) throws InterruptedException, IOException {
        
        PrintWriter logger = listener.getLogger();
        logger.println("=== Docker Build Step ===");
        logger.println("Image: " + imageName);
        logger.println("Command: " + command);
        
        // 构建Docker命令
        ArgumentListBuilder args = new ArgumentListBuilder();
        args.add("docker");
        
        if (forcePull) {
            // 先拉取镜像
            launcher.launch()
                .cmds("docker", "pull", imageName)
                .stdout(logger)
                .join();
        }
        
        // 执行Docker run命令
        args.add("run", "--rm");
        args.add("-v", ws.getRemote() + ":/workspace");
        args.add("-w", "/workspace");
        args.add(imageName);
        args.addTokenized(command);
        
        // 执行并获取结果
        int exitCode = launcher.launch()
            .cmds(args)
            .stdout(logger)
            .pwd(ws)
            .join();
        
        // 设置构建结果
        if (exitCode != 0) {
            build.setResult(Result.FAILURE);
            logger.println("Docker command failed with exit code: " + exitCode);
        } else {
            logger.println("Docker command completed successfully");
        }
    }
    
    // Getter方法(用于Jelly视图渲染)
    public String getImageName() { return imageName; }
    public String getCommand() { return command; }
    public boolean isForcePull() { return forcePull; }
    
    /**
     * 描述符类
     * 
     * 管理配置元数据和表单验证
     */
    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
        
        /**
         * 显示名称(在UI中显示)
         */
        @Override
        public String getDisplayName() {
            return "Execute Docker container";
        }
        
        /**
         * 适用性检查
         * 
         * @param jobType 任务类型
         * @return 是否适用于该任务类型
         */
        @Override
        public boolean isApplicable(Class<? extends AbstractProject> jobType) {
            return true; // 适用于所有项目类型
        }
        
        /**
         * 表单验证:镜像名称
         */
        @FormValidator
        public FormValidation doCheckImageName(@QueryParameter String value) {
            if (value == null || value.isEmpty()) {
                return FormValidation.error("Image name is required");
            }
            
            // 验证镜像名称格式
            if (!value.matches("^[a-zA-Z0-9][a-zA-Z0-9._-]*(/[a-zA-Z0-9][a-zA-Z0-9._-]*)?(:[a-zA-Z0-9._-]+)?$")) {
                return FormValidation.warning("Image name format may be invalid");
            }
            
            return FormValidation.ok();
        }
        
        /**
         * 表单自动完成
         */
        public ListBoxModel doFillImageNameItems() {
            ListBoxModel items = new ListBoxModel();
            items.add("Select an image", "");
            items.add("nginx:latest", "nginx:latest");
            items.add("node:18", "node:18");
            items.add("python:3.11", "python:3.11");
            return items;
        }
    }
}

5.4 扩展点注册与发现

渲染错误: Mermaid 渲染失败: Parse error on line 13: ...List] H --> I[触发@Initializer方法] ----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'

扩展点注册代码:

/**
 * ExtensionList实现原理
 * 
 * 管理特定类型扩展点的所有实现
 */
public class ExtensionList<T> extends CopyOnWriteArrayList<ExtensionComponent<T>> {
    
    private final Class<T> extensionType;
    private final Jenkins jenkins;
    
    /**
     * 获取扩展实例列表
     * 
     * 返回按ordinal排序的扩展实例
     */
    public List<T> getInstances() {
        return this.stream()
            .sorted(Comparator.comparingDouble(ExtensionComponent::ordinal).reversed())
            .map(ExtensionComponent::getInstance)
            .collect(Collectors.toList());
    }
    
    /**
     * 动态注册扩展
     * 
     * 用于运行时添加扩展(如动态加载插件)
     */
    public void add(ExtensionComponent<T> component) {
        // 检查是否已存在
        for (ExtensionComponent<T> existing : this) {
            if (existing.getInstance().getClass() == component.getInstance().getClass()) {
                return; // 已存在,跳过
            }
        }
        
        super.add(component);
        
        // 触发扩展添加事件
        fireOnAdded(component.getInstance());
    }
    
    /**
     * 查找特定扩展
     */
    public <U extends T> U get(Class<U> type) {
        for (ExtensionComponent<T> component : this) {
            if (type.isInstance(component.getInstance())) {
                return type.cast(component.getInstance());
            }
        }
        return null;
    }
}

6. 安全架构设计

6.1 安全框架概览

Jenkins安全框架基于多层防护设计,涵盖认证、授权、加密和审计等维度。

安全架构层次

审计层

访问日志

操作审计

安全事件

防护层

CSRF保护

Content-Security-Policy

Agent协议安全

脚本安全

授权层

ACL权限检查

角色策略

矩阵策略

项目级权限

认证层

HTTP Basic Auth

Form Authentication

API Token

SSO/OAuth

用户请求

业务处理

6.2 认证机制

/**
 * Jenkins认证流程实现
 * 
 * 支持多种认证方式的统一抽象
 */
public abstract class SecurityRealm implements ExtensionPoint {
    
    /**
     * 用户认证入口
     * 
     * @param username 用户名
     * @param password 密码
     * @return 认证成功返回Authentication对象
     * @throws AuthenticationException 认证失败
     */
    public abstract Authentication authenticate(String username, String password) 
            throws AuthenticationException;
    
    /**
     * 加载用户详情
     * 
     * 用于权限检查和用户信息展示
     */
    public abstract UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException;
    
    /**
     * 默认本地认证实现
     */
    public static class HudsonPrivateSecurityRealm extends SecurityRealm {
        
        private final Map<String, User> users = new ConcurrentHashMap<>();
        private final PasswordEncoder encoder = new BCryptPasswordEncoder();
        
        @Override
        public Authentication authenticate(String username, String password) {
            User user = users.get(username);
            if (user == null) {
                throw new BadCredentialsException("User not found: " + username);
            }
            
            if (!encoder.matches(password, user.getPasswordHash())) {
                throw new BadCredentialsException("Invalid password");
            }
            
            // 创建认证令牌
            return new UsernamePasswordAuthenticationToken(
                username, 
                null, 
                user.getAuthorities()
            );
        }
        
        @Override
        public UserDetails loadUserByUsername(String username) {
            User user = users.get(username);
            if (user == null) {
                throw new UsernameNotFoundException(username);
            }
            
            return new org.springframework.security.core.userdetails.User(
                username,
                user.getPasswordHash(),
                user.getAuthorities()
            );
        }
    }
}

6.3 授权策略

授权策略类型

Legacy
所有用户完全控制

Logged-in
登录用户完全控制

Matrix
细粒度权限矩阵

Role-Based
角色权限映射

Project Matrix
项目级权限矩阵

授权检查流程

有权限

无权限

访问资源

获取资源ACL

获取用户身份

检查权限

允许访问

拒绝访问
403 Forbidden

权限矩阵实现:

/**
 * 矩阵授权策略实现
 * 
 * 支持全局和项目级别的细粒度权限控制
 */
public class GlobalMatrixAuthorizationStrategy extends AuthorizationStrategy {
    
    // 权限矩阵: SID -> Permission -> Boolean
    private final Map<Permission, Map<String, Boolean>> grantedPermissions 
        = new ConcurrentHashMap<>();
    
    /**
     * 权限检查
     * 
     * @param a 用户身份
     * @param permission 请求的权限
     * @param resource 资源对象
     */
    @Override
    public boolean hasPermission(Authentication a, Permission permission, 
            Resource resource) {
        
        // 获取用户的所有SID(用户名+所属组)
        Set<String> sids = getSids(a);
        
        // 检查权限继承链
        Permission p = permission;
        while (p != null) {
            if (hasExplicitPermission(sids, p)) {
                return true;
            }
            p = p.impliedBy; // 检查父权限
        }
        
        return false;
    }
    
    /**
     * 检查显式权限
     */
    private boolean hasExplicitPermission(Set<String> sids, Permission permission) {
        Map<String, Boolean> acl = grantedPermissions.get(permission);
        if (acl == null) {
            return false;
        }
        
        for (String sid : sids) {
            Boolean granted = acl.get(sid);
            if (Boolean.TRUE.equals(granted)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 获取用户的所有安全标识符
     */
    private Set<String> getSids(Authentication a) {
        Set<String> sids = new HashSet<>();
        
        // 添加用户名
        sids.add(a.getName());
        
        // 添加所属组
        for (GrantedAuthority auth : a.getAuthorities()) {
            sids.add(auth.getAuthority());
        }
        
        return sids;
    }
    
    /**
     * 授予权限
     */
    public void grant(Permission permission, String sid) {
        grantedPermissions
            .computeIfAbsent(permission, k -> new ConcurrentHashMap<>())
            .put(sid, Boolean.TRUE);
    }
    
    /**
     * 撤销权限
     */
    public void revoke(Permission permission, String sid) {
        Map<String, Boolean> acl = grantedPermissions.get(permission);
        if (acl != null) {
            acl.remove(sid);
        }
    }
}

6.4 CSRF防护

/**
 * CSRF防护机制
 * 
 * 使用Crumb令牌验证请求来源
 */
public class CrumbIssuer implements ExtensionPoint {
    
    private final String crumbSalt = UUID.randomUUID().toString();
    
    /**
     * 生成Crumb令牌
     * 
     * 基于会话ID和盐值生成唯一令牌
     */
    public String issueCrumb(HttpServletRequest request) {
        String sessionId = request.getSession().getId();
        String raw = sessionId + ":" + crumbSalt;
        
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] hash = md.digest(raw.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(hash);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * 验证Crumb令牌
     * 
     * @param request HTTP请求
     * @return 验证是否通过
     */
    public boolean validateCrumb(HttpServletRequest request, String crumb) {
        String expected = issueCrumb(request);
        return MessageDigest.isEqual(
            expected.getBytes(StandardCharsets.UTF_8),
            crumb.getBytes(StandardCharsets.UTF_8)
        );
    }
    
    /**
     * 请求过滤器
     * 
     * 拦截所有POST/PUT/DELETE请求进行验证
     */
    @Extension
    public static class CrumbFilter implements Filter {
        
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, 
                FilterChain chain) throws IOException, ServletException {
            
            HttpServletRequest req = (HttpServletRequest) request;
            
            // 仅对修改操作进行验证
            if (requiresCrumbCheck(req)) {
                CrumbIssuer issuer = Jenkins.get().getCrumbIssuer();
                String crumb = req.getHeader(issuer.getCrumbRequestField());
                
                if (!issuer.validateCrumb(req, crumb)) {
                    ((HttpServletResponse) response).sendError(
                        HttpServletResponse.SC_FORBIDDEN, 
                        "Invalid CSRF Crumb"
                    );
                    return;
                }
            }
            
            chain.doFilter(request, response);
        }
        
        private boolean requiresCrumbCheck(HttpServletRequest req) {
            String method = req.getMethod().toUpperCase();
            return "POST".equals(method) || "PUT".equals(method) || 
                   "DELETE".equals(method);
        }
    }
}

7. 高可用与集群部署

7.1 高可用架构设计

高可用架构

Agent池

共享存储

Master集群

负载均衡层

健康探测

健康探测

健康探测

Agent 1

HAProxy/Nginx

Master 1
Active

Master 2
Standby

Master 3
Standby

NFS/GlusterFS
JENKINS_HOME

数据库
MySQL/PostgreSQL

Agent 2

Agent N

健康检查

7.2 主备切换机制

服务启动

获取分布式锁成功

等待锁释放

正常服务

检测到故障

释放锁

释放失败

主动降级

恢复

Standby

Active

Failing

Error

持有分布式锁
处理所有请求
管理Agent连接

监控锁状态
预加载配置
准备接管

主备切换实现:

/**
 * Jenkins高可用主备切换实现
 * 
 * 基于分布式锁实现Active-Standby模式
 */
public class FailoverManager implements ExtensionPoint {
    
    private final DistributedLock lock;
    private volatile boolean isActive = false;
    private final Thread heartbeatThread;
    
    /**
     * 分布式锁接口
     * 
     * 可基于ZooKeeper、etcd、Redis等实现
     */
    public interface DistributedLock {
        boolean tryLock(long timeout, TimeUnit unit);
        void unlock();
        boolean isLocked();
        String getLockOwner();
    }
    
    /**
     * 初始化故障转移管理器
     */
    public FailoverManager() {
        // 创建分布式锁(示例使用ZooKeeper)
        this.lock = new ZooKeeperDistributedLock(
            System.getProperty("jenkins.ha.zkServers", "localhost:2181"),
            "/jenkins/ha/lock"
        );
        
        // 启动心跳线程
        this.heartbeatThread = new Thread(this::heartbeatLoop, "HA-Heartbeat");
        this.heartbeatThread.setDaemon(true);
        this.heartbeatThread.start();
    }
    
    /**
     * 心跳循环
     * 
     * 定期续约锁,检测主节点状态
     */
    private void heartbeatLoop() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                if (isActive) {
                    // Active节点:续约锁
                    if (!lock.isLocked()) {
                        // 锁丢失,降级为Standby
                        demote();
                    }
                } else {
                    // Standby节点:尝试获取锁
                    if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
                        promote();
                    }
                }
                
                Thread.sleep(5000); // 5秒心跳间隔
                
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } catch (Exception e) {
                // 记录错误并继续
            }
        }
    }
    
    /**
     * 提升为Active节点
     */
    private synchronized void promote() {
        if (isActive) return;
        
        try {
            // 1. 加载JENKINS_HOME
            Jenkins jenkins = Jenkins.getInstance();
            jenkins.reload();
            
            // 2. 恢复构建队列
            jenkins.getQueue().restore();
            
            // 3. 接受Agent连接
            for (Computer c : jenkins.getComputers()) {
                if (c instanceof SlaveComputer) {
                    ((SlaveComputer) c).setAcceptingTasks(true);
                }
            }
            
            // 4. 启动调度器
            jenkins.getQueue().maintain();
            
            isActive = true;
            
        } catch (Exception e) {
            // 提升失败,释放锁
            lock.unlock();
            throw new RuntimeException("Failed to promote to active", e);
        }
    }
    
    /**
     * 降级为Standby节点
     */
    private synchronized void demote() {
        if (!isActive) return;
        
        try {
            // 1. 停止接受新任务
            Jenkins jenkins = Jenkins.getInstance();
            jenkins.getQueue().maintain();
            
            // 2. 等待当前构建完成或超时
            waitForBuildsCompletion(60, TimeUnit.SECONDS);
            
            // 3. 断开Agent连接
            for (Computer c : jenkins.getComputers()) {
                if (c instanceof SlaveComputer) {
                    ((SlaveComputer) c).setAcceptingTasks(false);
                }
            }
            
            // 4. 释放锁
            lock.unlock();
            
            isActive = false;
            
        } catch (Exception e) {
            throw new RuntimeException("Failed to demote to standby", e);
        }
    }
    
    private void waitForBuildsCompletion(long timeout, TimeUnit unit) 
            throws InterruptedException {
        long deadline = System.currentTimeMillis() + unit.toMillis(timeout);
        Jenkins jenkins = Jenkins.getInstance();
        
        while (System.currentTimeMillis() < deadline) {
            boolean hasRunning = false;
            for (Computer c : jenkins.getComputers()) {
                if (c.countBusy() > 0) {
                    hasRunning = true;
                    break;
                }
            }
            
            if (!hasRunning) break;
            Thread.sleep(1000);
        }
    }
}

7.3 数据一致性保障

/**
 * 共享存储一致性保障
 * 
 * 使用文件锁防止多实例同时写入
 */
public class SharedStorageManager {
    
    private final FileLock fileLock;
    private final FileChannel channel;
    
    /**
     * 获取独占锁
     * 
     * 在访问JENKINS_HOME前必须获取锁
     */
    public boolean acquireLock() {
        try {
            File lockFile = new File(Jenkins.get().getRootDir(), ".lock");
            channel = new RandomAccessFile(lockFile, "rw").getChannel();
            fileLock = channel.tryLock();
            return fileLock != null;
        } catch (IOException e) {
            return false;
        }
    }
    
    /**
     * 释放锁
     */
    public void releaseLock() {
        try {
            if (fileLock != null) {
                fileLock.release();
            }
            if (channel != null) {
                channel.close();
            }
        } catch (IOException e) {
            // 忽略关闭错误
        }
    }
}

7.4 集群部署最佳实践

# Kubernetes部署配置示例
# 文件: jenkins-ha.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: jenkins
  labels:
    app: jenkins
spec:
  serviceName: jenkins
  replicas: 1  # Active-Standby模式,同时只运行一个实例
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      serviceAccountName: jenkins
      containers:
      - name: jenkins
        image: jenkins/jenkins:lts
        ports:
        - containerPort: 8080
          name: web
        - containerPort: 50000
          name: agent
        env:
        - name: JENKINS_HOME
          value: /var/jenkins_home
        - name: JAVA_OPTS
          value: >-
            -Djenkins.install.runSetupWizard=false
            -Dhudson.slaves.WorkspaceDir=/var/jenkins_home/workspace
            -Dhudson.model.DirectoryBrowserSupport.CSP=""
        volumeMounts:
        - name: jenkins-home
          mountPath: /var/jenkins_home
        - name: jenkins-config
          mountPath: /var/jenkins_config
        livenessProbe:
          httpGet:
            path: /login
            port: 8080
          initialDelaySeconds: 120
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /login
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 5
        resources:
          requests:
            cpu: "1"
            memory: "2Gi"
          limits:
            cpu: "4"
            memory: "8Gi"
      volumes:
      - name: jenkins-config
        configMap:
          name: jenkins-config
  volumeClaimTemplates:
  - metadata:
      name: jenkins-home
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "standard"
      resources:
        requests:
          storage: 50Gi

---
# Service配置
apiVersion: v1
kind: Service
metadata:
  name: jenkins
spec:
  selector:
    app: jenkins
  ports:
  - port: 80
    targetPort: 8080
    name: web
  - port: 50000
    targetPort: 50000
    name: agent
  type: LoadBalancer

---
# Agent Pod模板
apiVersion: v1
kind: ConfigMap
metadata:
  name: jenkins-agent-pod-template
data:
  template.yaml: |
    apiVersion: v1
    kind: Pod
    spec:
      containers:
      - name: jnlp
        image: jenkins/inbound-agent:latest
        args: ['\$(JENKINS_SECRET)', '\$(JENKINS_NAME)']
      - name: maven
        image: maven:3.8-openjdk-11
        command: ['sleep', '99d']
        volumeMounts:
        - name: maven-repo
          mountPath: /root/.m2
      volumes:
      - name: maven-repo
        emptyDir: {}

总结

本文深入分析了Jenkins的核心架构设计,涵盖以下关键知识点:

模块 核心要点
整体架构 Master-Agent分布式设计、多协议通信、扩展点机制
核心组件 Jenkins单例模式、插件类加载隔离、Descriptor模式
任务调度 Queue状态机、Executor生命周期、优先级算法
数据持久化 JENKINS_HOME结构、XStream序列化、敏感数据加密
扩展点体系 ExtensionPoint接口、@Extension注解、动态注册机制
安全架构 认证/授权分离、CSRF防护、权限矩阵
高可用 Active-Standby模式、分布式锁、共享存储
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐