Jenkins:持续集成和持续交付(CI/CD)工具

目录

一、概念

[1.1 定义与价值](#1.1 定义与价值)

[1.2 核心概念](#1.2 核心概念)

[1.3 核心功能](#1.3 核心功能)

[1.4 工作流程](#1.4 工作流程)

[1.5 常见CI/CD工具对比](#1.5 常见CI/CD工具对比)

二、安装与配置

[2.1 环境准备](#2.1 环境准备)

[2.2 安装Jenkins](#2.2 安装Jenkins)

[2.2.1 方式一:通过包管理器安装(推荐用于生产环境)](#2.2.1 方式一:通过包管理器安装(推荐用于生产环境))

[2.2.2 方式二:通过 WAR 包安装(适合快速测试)](#2.2.2 方式二:通过 WAR 包安装(适合快速测试))

[2.3 防火墙与访问设置](#2.3 防火墙与访问设置)

[2.4 初始配置与插件管理](#2.4 初始配置与插件管理)

三、简单使用(自由风格项目)

[3.1 基本任务创建](#3.1 基本任务创建)

[3.2 源码管理(Git)](#3.2 源码管理(Git))

[3.3 构建触发器](#3.3 构建触发器)

[3.4 构建环境与步骤](#3.4 构建环境与步骤)

[2.5 保存与构建](#2.5 保存与构建)

[2.6 实用技巧](#2.6 实用技巧)

四、使用(流水线项目)

[4.1 创建流水线任务](#4.1 创建流水线任务)

[4.2 配置流水线任务](#4.2 配置流水线任务)

[4.3 保存并运行](#4.3 保存并运行)

五、Jenkinsfile核心指令


一、概念

1.1 定义与价值

Jenkins 是一个开源的、用 Java 编写的持续集成和持续交付(CI/CD)工具 。它本质上是一个自动化服务器,用于自动化软件开发过程中的各种任务,例如编译、测试、打包、部署等。

核心价值:自动化构建、测试和部署过程,尽早发现集成错误,提高软件质量与交付效率。

1.2 核心概念

  • 任务/项目(Job/Project): Jenkins中自动化流程的配置单元,比如一个构建Java应用的任务。

  • 流水线 :这是现代 Jenkins 的核心功能。可以将整个构建、测试、部署流程以代码的形式(称为 Jenkinsfile)定义下来,这使得流程可版本化、可审查、可重复。流水线分为声明式和脚本式两种,前者语法更简洁,后者灵活性更高。

    • 声明式流水线(Declarative Pipeline) :推荐使用,提供了更简单、更结构化的语法,降低了上手难度。

    • 脚本式流水线(Scripted Pipeline):基于 Groovy 的 DSL,提供极大的灵活性,但语法相对复杂。

  • 构建(Build): 执行一次任务的过程,包括拉取代码、编译、测试等步骤。

  • 工作空间(Workspace): Jenkins每次构建时,用来存放源代码和生成产物的目录。

  • 插件(Plugin): Jenkins 拥有一个极其丰富的插件生态系统(超过 1800 个插件),这使其能够与 Git、Docker、Kubernetes、Jira 等几乎所有主流开发工具无缝集成,功能得以无限扩展。

1.3 核心功能

  • 持续集成(CI)

    • 自动化构建:当开发人员向版本控制系统(如 Git、SVN)提交代码后,Jenkins 可以自动侦听这一事件(通过 Webhook),拉取最新代码。

    • 自动化编译/打包:根据项目类型(如 Java/Maven、.NET、Node.js、Python),调用相应的构建工具(如 Maven、Gradle、npm、dotnet)将源代码编译成可执行的软件包(如 JAR、WAR、Docker 镜像)。

  • 持续测试

    • 自动化测试:在构建完成后,自动运行各种测试套件,包括单元测试、集成测试等。

    • 测试报告生成:收集测试结果,生成可视化的报告和趋势图,帮助团队快速了解代码质量和测试覆盖率。

  • 持续交付/持续部署(CD)

    • 自动化部署:将构建好的、通过测试的软件包自动部署到各种环境中,如测试环境、预生产环境,乃至生产环境。

    • 多环境管理:通过插件和配置,轻松管理向不同环境的部署流程,通常与审批流程结合(持续交付),或完全自动化(持续部署)。

  • 强大的流水线(Pipeline)

    • 这是 Jenkins 的灵魂功能。它将整个 CI/CD 流程定义为一个"流水线",由多个"阶段"(Stage)组成,例如:代码检出 -> 编译 -> 单元测试 -> 集成测试 -> 构建Docker镜像 -> 部署到测试环境 -> 验收测试。

    • Jenkinsfile:使用 Groovy 语法将流水线定义为代码,存储在项目源码库中。这使得流程与代码同在,便于管理和协作。

  • 广泛的插件生态系统

    • Jenkins 拥有超过 1800 个插件,这是它最核心的竞争力之一。几乎所有能想到的第三方工具都可以通过插件与 Jenkins 集成,例如:

      • 版本控制:Git、GitHub、GitLab、Bitbucket。

      • 构建工具:Maven、Gradle、Ant、npm、Yarn。

      • 测试框架:JUnit、Selenium、Cucumber。

      • 部署目标:Kubernetes、Docker、AWS、Azure、Tomcat、SSH。

      • 通知:Email、Slack、钉钉、企业微信。

  • 监控与通知

    • Jenkins 会监控整个构建和部署过程的状态(成功、失败、不稳定)。

    • 当流程中断时,它会通过邮件、即时消息等渠道通知相关人员,确保问题能被及时处理。

1.4 工作流程

  1. 监听触发:Jenkins 会持续监听代码仓库(如 Git)的变动,一旦有新的代码提交,或者到达预设的时间点,它就会被触发开始工作。

  2. 拉取代码:从版本控制仓库拉取最新的源代码。

  3. 执行任务:按照预先定义好的"流水线",一步步执行命令,常见步骤包括编译代码、运行测试、打包项目、进行代码质量检查以及部署到服务器等。

  4. 反馈结果:任务执行完成后,Jenkins 会生成详细报告,并通过邮件、钉钉等方式通知相关人员构建结果。

1.5 常见CI/CD工具对比

工具 托管方式 配置方式 核心优势 主要缺点 最佳适用场景
Jenkins 主要自托管 Jenkinsfile (Groovy) / UI 极度灵活,插件生态丰富,免费 维护成本高,学习曲线陡,界面陈旧 需要高度定制化,有专人维护的复杂环境
GitLab CI/CD SaaS/自托管 .gitlab-ci.yml (YAML) 与GitLab无缝集成,一体化的DevOps体验 与GitLab平台强绑定 已使用GitLab,追求端到端工具链的团队
GitHub Actions SaaS YAML 工作流文件 与GitHub深度集成,事件驱动,Action市场丰富 与GitHub平台强绑定 代码在GitHub,希望利用事件驱动自动化的团队
CircleCI 主要SaaS .circleci/config.yml (YAML) 云版本性能快,Docker支持好,配置清晰 免费计划限制较多 追求速度和稳定性,使用Docker的云原生项目
Azure Pipelines SaaS/自托管代理 YAML / UI 平台无关性,免费额度慷慨,微软栈集成好 微软生态外非首选 微软技术栈,Azure用户,开源项目

二、安装与配置

2.1 环境准备

Jenkins 需要运行在具有 Java 环境的服务器上,以下是针对不同操作系统的安装方法:

安装 Java

Ubuntu/Debian 系统:

bash 复制代码
sudo apt update
sudo apt install openjdk-11-jdk
java -version # 验证安装

CentOS/RHEL 系统:

bash 复制代码
sudo yum install java-11-openjdk
  • 版本选择 :Jenkins 2.357 及更高版本需要 Java 11 或更高版本。建议选择 Java 11 或 Java 17 这些长期支持版(LTS)。

环境配置 :设置 JAVA_HOME 环境变量,例如在 /etc/profile 文件中添加:

bash 复制代码
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
export PATH=$JAVA_HOME/bin:$PATH

然后执行 source /etc/profile 使配置生效。

2.2 安装Jenkins

2.2.1 方式一:通过包管理器安装(推荐用于生产环境)

这种方法便于后续的管理和升级。

Ubuntu/Debian 系统:

bash 复制代码
# 添加仓库密钥和源
sudo wget -O /usr/share/keyrings/jenkins-keyring.asc https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt update
sudo apt install jenkins

CentOS/RHEL 系统:

bash 复制代码
# 添加仓库
sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
sudo yum install jenkins

安装完成后,启动 Jenkins 并设置开机自启:

bash 复制代码
sudo systemctl start jenkins
sudo systemctl enable jenkins

2.2.2 方式二:通过 WAR 包安装(适合快速测试)

这种方式简单灵活,只需具备 Java 环境即可。

bash 复制代码
wget https://get.jenkins.io/war-stable/2.426.2/jenkins.war
sudo java -jar jenkins.war --httpPort=8090

2.3 防火墙与访问设置

确保服务器防火墙开放了 Jenkins 所使用的端口(例如默认的 8080 端口)。

  • 查看防火墙状态 :systemctl status firewalld

  • 开放端口(以 8080 为例):

bash 复制代码
firewall-cmd --zone=public --add-port=8080/tcp --permanent
firewall-cmd --reload
  • 访问 Jenkins :在浏览器中输入 http://<你的服务器IP地址>:8080 即可访问 Jenkins 初始页面。

2.4 初始配置与插件管理

首次访问 Jenkins 时:

  1. 获取管理员密码 :在服务器上执行 sudo cat /var/lib/jenkins/secrets/initialAdminPassword 查看初始密码,并将其输入网页。

  2. 选择插件安装方式 :建议先**"安装推荐的插件"**。如果网络环境导致下载缓慢或失败,可以参考后续的"插件加速"部分进行配置后再安装。

由于网络原因,直接从 Jenkins 官方仓库下载插件可能很慢。可以切换至国内镜像源,例如清华大学镜像源。

  1. 登录 Jenkins 管理后台,进入 Manage Jenkins > Manage Plugins > Advanced。

  2. 在 Update Site 栏目中,将 URL 修改为:
    https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

  3. 点击 Submit 保存。

    如果修改配置后插件安装问题依旧,可以尝试重启 Jenkins 服务(sudo systemctl restart jenkins)或手动修改 Jenkins 主机上的更新中心配置文件。

创建管理员用户:在插件安装完成后,系统会提示你创建一个新的管理员用户。建议务必在此步骤创建,以便后续管理。

全局工具配置:为了让 Jenkins 能够执行构建、打包等任务,需要配置所需工具的路径。

进入 Manage Jenkins > Global Tool Configuration:

  • JDK :可以取消"自动安装",手动指定 JAVA_HOME 路径,如 /usr/lib/jvm/java-11-openjdk。

  • Git :通常使用系统自带的 Git,路径可能是 /usr/bin/git。如果系统未安装,需先执行 yum install git 或 apt install git。

  • Maven:如果构建 Java Maven 项目,可以指定 Jenkins 自动安装特定版本,或手动指定已安装在服务器上的 Maven 路径。

  • Node.js :对于前端项目,可以在插件市场安装 NodeJS Plugin 后,在此处配置 Node.js 版本。

三、简单使用(自由风格项目)

3.1 基本任务创建

  1. 点击 Jenkins 首页的 新建任务(New Item)。

  2. 输入任务名称,例如 My-First-Project。

  3. 选择任务类型,对于初学者,常见的类型有:

    • Freestyle project(自由风格项目):提供灵活的图形化配置界面,适合大多数简单场景。

    • Pipeline (流水线项目):使用代码(Jenkinsfile)来定义构建流程,更适合复杂、多阶段的 CI/CD 流程。

      本例中选择 Freestyle project。

3.2 源码管理(Git)

在任务配置页面的 Source Code Management 部分:

  1. 选择 Git。

  2. 在 Repository URL 中输入 Git 仓库地址,例如 https://github.com/username/repo.git。

  3. 如果仓库是私有的,需要配置凭据(Credentials)。点击 Add > Jenkins:

    • Kind 选择 "Username with password"。

    • 输入 Git 用户名和密码(或 Personal Access Token)。

  4. 在 Branches to build 指定分支,例如 */main 或 */master。

3.3 构建触发器

在 Build Triggers 部分,可以设置何时自动触发构建:

  • 手动构建 :不配置任何触发器,每次在 Jenkins 页面上手动点击 Build Now。

  • 定时构建 :例如,H 2 * * * 表示每天凌晨 2 点左右构建。

  • 轮询 SCM :定期检查代码仓库是否有变更,如有变更则触发构建。例如 */5 * * * * 表示每 5 分钟检查一次。

  • GitHub Webhook(更高效):当代码推送到仓库时,GitHub 会主动通知 Jenkins 触发构建。这需要额外的 Webhook 配置。

3.4 构建环境与步骤

在 Build 部分,点击 Add build step,根据项目类型添加相应的步骤。

  • 对于 Maven 项目 :可以选择 Invoke top-level Maven targets ,然后指定 Maven 目标,如 clean package。

  • 对于 Shell 脚本 :可以选择 Execute shell,然后输入命令,例如:

bash 复制代码
# 前端项目示例
npm install
npm run build
# 将构建产物复制到指定目录
cp -r dist /path/to/target/

2.5 保存与构建

配置完成后,点击页面底部的 Save 。在项目详情页,点击左侧的 Build Now 即可立即开始一次构建。点击构建历史记录中的链接(例如 #1),再选择 Console Output,可以查看详细的构建日志,这对于调试非常重要。

2.6 实用技巧

  • 修改 JVM 内存设置 :如果构建时出现 java.lang.OutOfMemoryError,需要增加 Jenkins 的堆内存。可以通过修改启动参数实现,例如在 JAVA_OPTS 中加入 -Xms512m -Xmx1024m。

  • 设置"丢弃旧的构建":在项目配置中勾选此选项,可以设置保留构建的天数和最大保留个数,有效节省服务器磁盘空间。

  • 权限管理 :安装 Role-based Authorization Strategy 插件,可以为不同用户(如开发、测试)分配不同的项目查看和操作权限。

四、使用(流水线项目)

以一个完整的 Java Maven 项目为例,结合 Docker 部署

4.1 创建流水线任务

  1. 登录 Jenkins 控制台,点击首页左侧的 "新建任务" (New Item)

  2. 输入任务名称,例如 my-pipeline-project

  3. 关键选择 :在任务类型中选择 "流水线" (Pipeline)

  4. 点击 "确定"

4.2 配置流水线任务

进入任务配置页面后,找到 "流水线" (Pipeline) 部分,这里有几种定义流水线的方式:

方式一:直接编写脚本 (Pipeline script)

  • 适用场景:快速测试、简单脚本、学习阶段

  • 操作方法 :在 "脚本" (Script) 文本框中直接粘贴或编写 Jenkinsfile 内容

  • 示例:

Groovy 复制代码
pipeline {
    agent any // 指定流水线在任何可用的代理上执行:cite[5]:cite[10]

    // 定义环境变量
    environment {
        REGISTRY_URL = 'harbor.example.com' // 私有镜像仓库地址
        PROJECT_NAME = 'my-java-app'
        DOCKER_CREDENTIALS_ID = 'docker-harbor-login' // Jenkins中配置的Docker凭据ID
    }
    
    stages {
        // 阶段1:拉取代码
        stage('Checkout') {
            steps {
                checkout scm: [
                    $class: 'GitSCM', 
                    branches: [[name: '*/main']], 
                    userRemoteConfigs: [[url: 'http://your-git-repo.com/your-project.git']]
                ]
                echo "代码拉取完成"
            }
        }
        
        // 阶段2:编译和单元测试
        stage('Build and Test') {
            steps {
                sh 'mvn clean compile' // 编译代码
                sh 'mvn test' // 运行单元测试
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml' // 归档JUnit测试报告:cite[2]
                }
            }
        }
        
        // 阶段3:打包JAR文件
        stage('Package') {
            steps {
                sh 'mvn package -Dmaven.test.skip=true' // 跳过测试进行打包
                archiveArtifacts artifacts: 'target/*.jar', fingerprint: true // 归档生成的JAR包
            }
        }
        
        // 阶段4:构建Docker镜像
        stage('Build Docker Image') {
            steps {
                script {
                    // 动态生成镜像标签,例如使用构建号
                    dockerImage = docker.build "${REGISTRY_URL}/${PROJECT_NAME}:v1.0.${BUILD_NUMBER}"
                }
            }
        }
        
        // 阶段5:推送Docker镜像
        stage('Push Docker Image') {
            steps {
                script {
                    docker.withRegistry("https://${REGISTRY_URL}", DOCKER_CREDENTIALS_ID) {
                        dockerImage.push() // 推送镜像到仓库
                    }
                }
            }
        }
        
        // 阶段6:部署到服务器
        stage('Deploy') {
            steps {
                script {
                    // 使用SSH远程执行部署脚本
                    sshagent(['your-ssh-credentials-id']) {
                        sh """
                            ssh -o StrictHostKeyChecking=no user@your-server-ip '
                                docker stop ${PROJECT_NAME} || true
                                docker rm ${PROJECT_NAME} || true
                                docker pull ${REGISTRY_URL}/${PROJECT_NAME}:v1.0.${BUILD_NUMBER}
                                docker run -d -p 8080:8080 --name ${PROJECT_NAME} ${REGISTRY_URL}/${PROJECT_NAME}:v1.0.${BUILD_NUMBER}
                            '
                        """
                    }
                }
            }
        }
    }
    
    // 构建后处理
    post {
        always {
            echo "流水线执行完成,构建号:${BUILD_NUMBER}"
            // 这里可以添加清理工作,例如删除临时文件
        }
        success {
            echo "构建成功!"
            // 可以在这里添加成功通知,例如发送邮件:cite[3]
        }
        failure {
            echo "构建失败!"
            // 可以在这里添加失败通知
        }
    }
}
阶段 核心任务 关键命令/插件 说明
Checkout 从版本库拉取代码 checkout scm 获取最新的源代码。
Build and Test 编译代码并运行单元测试 mvn clean compile, mvn test, junit 运行测试并生成测试报告。
Package 打包应用程序 mvn package, archiveArtifacts 生成可部署的JAR/WAR包并归档。
Build Docker Image 构建Docker镜像 docker.build 使用项目中的Dockerfile构建镜像。
Push Docker Image 推送镜像到仓库 docker.withRegistry, dockerImage.push 将镜像推送到私有仓库以备部署。
Deploy 部署到目标服务器 sshagent, ssh 通过SSH在目标服务器上拉取新镜像并重启容器。

方式二:从代码仓库获取 (Pipeline script from SCM) - 推荐用于生产

  • 适用场景:实际项目开发,实现"Pipeline as Code"

  • 配置细节:

    • SCM:选择版本控制系统(Git、SVN等)

    • Repository URL:输入代码仓库地址

    • 凭据:添加访问仓库的用户名/密码或SSH密钥

    • 分支指定器 :默认 */main 或 */master

    • 脚本路径 :关键设置,指定 Jenkinsfile 在仓库中的路径

脚本路径的详细说明:

在 "脚本路径" (Script Path) 字段中,需要告诉 Jenkins 在哪里找到 Jenkinsfile:

场景 脚本路径示例 说明
根目录下 Jenkinsfile Jenkinsfile 直接在仓库根目录
子目录下 ci/Jenkinsfile Jenkinsfile 在 ci 文件夹下
不同名称 Jenkinsfile.prod 使用非标准名称的流水线文件
多分支 Jenkinsfile 多分支流水线项目中每个分支可有不同的Jenkinsfile

假设项目结构如下:

my-java-app/

├── src/

├── pom.xml

└── Jenkinsfile ← 流水线定义文件

在Jenkins中的配置:

  1. 任务名称 :my-java-app-pipeline

  2. 流水线定义 :选择 "Pipeline script from SCM"

  3. SCM :Git

  4. Repository URL :https://github.com/yourname/my-java-app.git

  5. 凭据:添加GitHub访问令牌

  6. 分支 :*/main

  7. 脚本路径 :Jenkinsfile (保持默认)

对应的 Jenkinsfile 内容举例(无docker):

Groovy 复制代码
pipeline {
    agent any
    tools {
        maven 'Maven-3.8.6'  // 在Jenkins全局工具配置中定义的Maven版本
        jdk 'JDK-11'         // 在Jenkins全局工具配置中定义的JDK版本
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm  // 拉取代码
            }
        }
        
        stage('Build') {
            steps {
                sh 'mvn clean compile'
            }
        }
        
        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'  // 归档测试报告
                }
            }
        }
        
        stage('Package') {
            steps {
                sh 'mvn package -DskipTests'
                archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
            }
        }
    }
    
    post {
        always {
            echo "构建完成: ${currentBuild.fullDisplayName}"
        }
        success {
            emailext (
                subject: "SUCCESS: 构建 ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "构建成功!\n详情: ${env.BUILD_URL}",
                to: "dev-team@company.com"
            )
        }
        failure {
            emailext (
                subject: "FAILED: 构建 ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "构建失败,请检查!\n详情: ${env.BUILD_URL}",
                to: "dev-team@company.com"
            )
        }
    }
}

4.3 保存并运行

  1. 点击页面底部的 "保存" (Save)

  2. 返回项目页面,点击 "立即构建" (Build Now)

  3. 查看构建进度和日志

五、Jenkinsfile核心指令

指令/区块 是否必须 说明与示例
pipeline 是 声明流水线的根区块。
agent 是 指定整个流水线或特定阶段在哪个 Jenkins 节点上执行。常用选项有 any(任意可用节点)、label '节点标签' 或使用 docker 提供容器化环境。
stages 是 包含一个或多个 stage 指令的序列,定义了流水线的所有阶段。
stage 是 定义流水线中的一个具有明确职责的阶段,如构建、测试、部署。stages 内至少包含一个 stage。
steps 是 在每个 stage 中定义具体要执行的命令序列。
environment 否 定义环境变量,可在流水线级别或阶段级别使用。支持使用 credentials() 方法安全地获取 Jenkins 中存储的密码等凭据。
parameters 否 定义流水线的参数,实现参数化构建。支持字符串、选择框、布尔值等类型。
options 否 配置流水线特有的选项,例如设置超时时间 timeout(time: 1, unit: 'HOURS')。
post 否 根据流水线或阶段的最终状态执行附加操作。例如,无论成功失败都发送通知(always),仅在失败时发送警报(failure)。
triggers 否 定义流水线的自动触发条件,如定时构建 cron('0 8 * * 1-5') 或轮询 SCM。
相关推荐
虎头金猫3 天前
4K 视频总卡在公网带宽?用 N1 + OpenList 把网盘播放链路重新理顺
运维·服务器·网络·python·容器·beautifulsoup·pandas
AI职业加油站3 天前
AI智能体应用工程师证书:政策红利下的职业新风口
大数据·运维·人工智能·学习·职场发展
此冬歌咏3 天前
K8s 节点故障实战:优雅驱逐 31 秒,硬故障 331 秒,以及那个永远 Pending 的 Pod
运维·k8s
-梅3 天前
linux(8) 软硬链接
linux·运维·服务器
张洛闻Eren3 天前
k8s云原生【第十课】:水平 Pod 自动扩缩容
运维·数据库·云原生·kubernetes·github
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
布裘3 天前
【银河麒麟】V4桌面图标消失,右击鼠标没反应排查
运维·银河麒麟·桌面环境
懂软件的胡子个哥3 天前
微信机器人为什么需要“回复候选”而不是所有 AI 内容直接发送
运维·微信·自动化·wechatapi·个人微信号二次开发
云贝贝贝3 天前
腾讯云 TDSQL(MySQL 版)性能优化与慢查询排障实战 6 招
运维·腾讯云
Lsetea3 天前
证书没到期却报certificate has expired:OpenSSL定位中间证书与系统时间
运维·https·ssl证书·openssl·证书链