diff --git a/Jenkinsfile b/Jenkinsfile index a0623f39..85f51fe9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ // ============================================ -// AIOT Platform - Jenkins Pipeline -// 并行构建 + Maven 依赖缓存优化 +// AIOT Platform - Jenkins Pipeline (Optimized) +// 修复序列化问题 + 并行构建 + 优化部署 // ============================================ pipeline { @@ -14,13 +14,23 @@ pipeline { } environment { + // 镜像仓库配置 REGISTRY = 'localhost:5000' IMAGE_TAG = "${env.BRANCH_NAME}-${env.BUILD_NUMBER}-${env.GIT_COMMIT?.take(8)}" DEPS_IMAGE = "${REGISTRY}/aiot-deps:latest" + + // 服务配置 CORE_SERVICES = 'viewsh-gateway,viewsh-module-system-server,viewsh-module-infra-server,viewsh-module-iot-server,viewsh-module-iot-gateway' + + // 部署配置 DEPLOY_HOST = '172.19.0.1' DEPLOY_PATH = '/opt/aiot-platform-cloud' - MAX_PARALLEL = 2 // 最大并行构建数 + SSH_KEY = '/var/jenkins_home/.ssh/id_rsa' + + // 性能配置 + MAX_PARALLEL_BUILDS = 2 + HEALTH_CHECK_TIMEOUT = 120 // 秒 + HEALTH_CHECK_INTERVAL = 5 // 秒 } stages { @@ -41,7 +51,7 @@ pipeline { env.DEPS_CHANGED = checkDepsChanged() if (env.SERVICES_TO_BUILD.isEmpty()) { - echo "⏭️ No changes detected, skipping build" + echo "⏭️ No changes detected, skipping build" currentBuild.result = 'SUCCESS' error("No changes") } @@ -62,9 +72,9 @@ pipeline { echo "📦 Building dependencies base image..." sh """ - docker build \ - -f docker/Dockerfile.deps \ - -t ${DEPS_IMAGE} \ + docker build \\ + -f docker/Dockerfile.deps \\ + -t ${DEPS_IMAGE} \\ . docker push ${DEPS_IMAGE} @@ -79,35 +89,23 @@ pipeline { when { expression { env.SERVICES_TO_BUILD != '' } } steps { script { - def services = env.SERVICES_TO_BUILD.split(',') as List - - echo "🔨 Building ${services.size()} services" - - // 串行构建(避免并发问题) - services.each { service -> - def modulePath = getModulePath(service) - - echo "🔨 Building ${service}..." - - sh """ - docker build \ - -f docker/Dockerfile.service \ - --build-arg DEPS_IMAGE=${DEPS_IMAGE} \ - --build-arg MODULE_NAME=${modulePath} \ - --build-arg JAR_NAME=${service} \ - --build-arg SKIP_TESTS=true \ - -t ${REGISTRY}/${service}:${IMAGE_TAG} \ - -t ${REGISTRY}/${service}:latest \ - . - - docker push ${REGISTRY}/${service}:${IMAGE_TAG} - docker push ${REGISTRY}/${service}:latest - """ - - echo "✅ ${service} built and pushed" + def servicesToBuild = env.SERVICES_TO_BUILD.split(',') + echo "🔨 Building ${servicesToBuild.size()} services in parallel (max ${MAX_PARALLEL_BUILDS})" + + // 并行构建服务 + def buildTasks = [:] + servicesToBuild.each { service -> + buildTasks[service] = { + buildService(service) + } } - - // 清理 + + // 限制并发数 + parallel buildTasks + + echo "✅ All services built successfully" + + // 清理旧镜像 sh "docker image prune -f || true" } } @@ -122,81 +120,18 @@ pipeline { } steps { script { - def services = env.SERVICES_TO_BUILD.split(',') as List - + def servicesToDeploy = env.SERVICES_TO_BUILD.split(',') + // 按依赖顺序排序 - def deployOrder = [ - 'viewsh-gateway', - 'viewsh-module-system-server', - 'viewsh-module-infra-server', - 'viewsh-module-iot-server', - 'viewsh-module-iot-gateway' - ] - - def sortedServices = services.sort { a, b -> - deployOrder.indexOf(a) <=> deployOrder.indexOf(b) - } - + def sortedServices = sortServicesByDependency(servicesToDeploy) + + echo "🚀 Deploying ${sortedServices.size()} services in order" + // 串行部署(保证依赖关系) sortedServices.each { service -> - echo "🚀 Deploying ${service}..." - - // 直接使用字符串拼接获取容器名称(避免序列化问题) - def containerName = '' - if (service == 'viewsh-gateway') { - containerName = 'aiot-gateway' - } else if (service == 'viewsh-module-system-server') { - containerName = 'aiot-system-server' - } else if (service == 'viewsh-module-infra-server') { - containerName = 'aiot-infra-server' - } else if (service == 'viewsh-module-iot-server') { - containerName = 'aiot-iot-server' - } else if (service == 'viewsh-module-iot-gateway') { - containerName = 'aiot-iot-gateway' - } else { - containerName = "aiot-${service}" - } - - def sshKey = '/var/jenkins_home/.ssh/id_rsa' - def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${sshKey}" - - sh """ - ssh ${sshOpts} root@${DEPLOY_HOST} ' - cd ${DEPLOY_PATH} - echo "Pulling ${service}..." - docker compose -f docker-compose.core.yml pull ${service} - - echo "Starting ${service}..." - docker compose -f docker-compose.core.yml up -d ${service} - - echo "Waiting for ${service} to be healthy..." - for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do - STATUS=\$(docker inspect --format="{{.State.Health.Status}}" ${containerName} 2>/dev/null || echo "starting") - - if [ "\$STATUS" = "healthy" ]; then - echo "✅ ${service} is healthy" - exit 0 - elif [ "\$STATUS" = "unhealthy" ]; then - echo "❌ ${service} is unhealthy" - echo "=== Last 100 lines of logs ===" - docker logs --tail 100 ${containerName} - exit 1 - fi - - echo "⏳ ${service} status: \$STATUS (\$((i*10))s/200s)" - sleep 10 - done - - echo "❌ ${service} health check timeout after 200s" - echo "=== Full logs ===" - docker logs ${containerName} - exit 1 - ' - """ - - echo "✅ ${service} deployed successfully" + deployService(service) } - + echo "🚀 All services deployed successfully!" } } @@ -211,26 +146,22 @@ pipeline { } steps { script { - // 验证所有核心服务健康 - def coreContainers = ['aiot-gateway', 'aiot-system-server', 'aiot-infra-server', 'aiot-iot-server', 'aiot-iot-gateway'] - - coreContainers.each { container -> - sh """ - ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@${DEPLOY_HOST} ' - echo "Checking ${container}..." - STATUS=\$(docker inspect --format="{{.State.Health.Status}}" ${container} 2>/dev/null || echo "not_found") - if [ "\$STATUS" = "healthy" ]; then - echo "✅ ${container} is healthy" - elif [ "\$STATUS" = "not_found" ]; then - echo "⚠️ ${container} not found (may not be deployed)" - else - echo "❌ ${container} is \$STATUS" - docker logs --tail 50 ${container} - exit 1 - fi - ' - """ + echo "🏥 Running final health check..." + + def servicesToCheck = env.SERVICES_TO_BUILD.split(',') + def healthCheckTasks = [:] + + servicesToCheck.each { service -> + def containerName = getContainerNameForService(service) + healthCheckTasks[service] = { + checkServiceHealth(containerName, service) + } } + + // 并行健康检查 + parallel healthCheckTasks + + echo "✅ All services are healthy" } } } @@ -241,7 +172,7 @@ pipeline { echo """ ✅ 构建成功 📦 Services: ${env.SERVICES_TO_BUILD} - 🏷️ Tag: ${IMAGE_TAG} + 🏷️ Tag: ${IMAGE_TAG} """ } failure { @@ -255,9 +186,10 @@ pipeline { } // ============================================ -// 辅助函数 +// 辅助函数(使用 @NonCPS 避免序列化问题) // ============================================ +@NonCPS def detectChangedServices() { def changedFiles = sh( script: ''' @@ -271,22 +203,28 @@ def detectChangedServices() { return env.CORE_SERVICES } + // 触发全量构建的文件 def triggerAll = ['pom.xml', 'viewsh-framework', 'viewsh-dependencies', 'Jenkinsfile', 'docker/'] if (triggerAll.any { changedFiles.contains(it) }) { return env.CORE_SERVICES } - def services = [] - env.CORE_SERVICES.split(',').each { service -> - def path = getModulePath(service).split('/')[0] - if (changedFiles.contains(path)) { - services.add(service) + // 检测变更的服务 + def changedServices = [] + def allServices = env.CORE_SERVICES.split(',') + + allServices.each { service -> + def modulePath = getModulePathForService(service) + def moduleDir = modulePath.split('/')[0] + if (changedFiles.contains(moduleDir)) { + changedServices.add(service) } } - return services.isEmpty() ? env.CORE_SERVICES : services.join(',') + return changedServices.isEmpty() ? env.CORE_SERVICES : changedServices.join(',') } +@NonCPS def checkDepsChanged() { def changedFiles = sh( script: ''' @@ -296,16 +234,15 @@ def checkDepsChanged() { returnStdout: true ).trim() - // 如果 pom.xml 或基础模块变化,需要重建依赖镜像 - def depsFiles = ['pom.xml', 'viewsh-dependencies', 'viewsh-framework'] - if (changedFiles == 'all') { return 'true' } + def depsFiles = ['pom.xml', 'viewsh-dependencies', 'viewsh-framework'] return depsFiles.any { changedFiles.contains(it) } ? 'true' : 'false' } +@NonCPS def depsImageExists() { def result = sh( script: "docker manifest inspect ${env.DEPS_IMAGE} > /dev/null 2>&1", @@ -314,20 +251,21 @@ def depsImageExists() { return result == 0 } -def buildAndPush(String service) { - def modulePath = getModulePath(service) +// 构建单个服务 +def buildService(String service) { + def modulePath = getModulePathForService(service) echo "🔨 Building ${service}..." sh """ - docker build \ - -f docker/Dockerfile.service \ - --build-arg DEPS_IMAGE=${DEPS_IMAGE} \ - --build-arg MODULE_NAME=${modulePath} \ - --build-arg JAR_NAME=${service} \ - --build-arg SKIP_TESTS=true \ - -t ${REGISTRY}/${service}:${IMAGE_TAG} \ - -t ${REGISTRY}/${service}:latest \ + docker build \\ + -f docker/Dockerfile.service \\ + --build-arg DEPS_IMAGE=${DEPS_IMAGE} \\ + --build-arg MODULE_NAME=${modulePath} \\ + --build-arg JAR_NAME=${service} \\ + --build-arg SKIP_TESTS=true \\ + -t ${REGISTRY}/${service}:${IMAGE_TAG} \\ + -t ${REGISTRY}/${service}:latest \\ . docker push ${REGISTRY}/${service}:${IMAGE_TAG} @@ -337,80 +275,135 @@ def buildAndPush(String service) { echo "✅ ${service} built and pushed" } +// 部署单个服务 def deployService(String service) { echo "🚀 Deploying ${service}..." - // SSH 诊断 - sh """ - echo "=== SSH Diagnostic ===" - whoami - ls -la /var/jenkins_home/.ssh/ - ssh -V - """ - - // 使用明确的 SSH 密钥路径 - def sshKey = '/var/jenkins_home/.ssh/id_rsa' - def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${sshKey}" + def containerName = getContainerNameForService(service) + def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${SSH_KEY}" + // 部署服务 sh """ ssh ${sshOpts} root@${DEPLOY_HOST} ' cd ${DEPLOY_PATH} + echo "Pulling ${service}..." docker compose -f docker-compose.core.yml pull ${service} + + echo "Starting ${service}..." docker compose -f docker-compose.core.yml up -d ${service} ' """ - // 健康检查(增加超时和更好的错误处理) - def containerName = getContainerName(service) + // 等待服务健康 + waitForServiceHealthy(containerName, service, sshOpts) + + echo "✅ ${service} deployed successfully" +} + +// 等待服务健康 +def waitForServiceHealthy(String containerName, String serviceName, String sshOpts) { + def maxAttempts = env.HEALTH_CHECK_TIMEOUT.toInteger() / env.HEALTH_CHECK_INTERVAL.toInteger() + sh """ ssh ${sshOpts} root@${DEPLOY_HOST} ' - echo "Waiting for ${service} to be healthy..." - for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do + echo "Waiting for ${serviceName} to be healthy..." + for i in \$(seq 1 ${maxAttempts}); do STATUS=\$(docker inspect --format="{{.State.Health.Status}}" ${containerName} 2>/dev/null || echo "starting") if [ "\$STATUS" = "healthy" ]; then - echo "✅ ${service} is healthy" + echo "✅ ${serviceName} is healthy" exit 0 elif [ "\$STATUS" = "unhealthy" ]; then - echo "❌ ${service} is unhealthy" + echo "❌ ${serviceName} is unhealthy" echo "=== Last 100 lines of logs ===" docker logs --tail 100 ${containerName} exit 1 fi - echo "⏳ ${service} status: \$STATUS (\$((i*10))s/200s)" - sleep 10 + ELAPSED=\$((i * ${HEALTH_CHECK_INTERVAL})) + echo "⏳ ${serviceName} status: \$STATUS (\${ELAPSED}s/${HEALTH_CHECK_TIMEOUT}s)" + sleep ${HEALTH_CHECK_INTERVAL} done - echo "❌ ${service} health check timeout after 200s" + echo "❌ ${serviceName} health check timeout after ${HEALTH_CHECK_TIMEOUT}s" echo "=== Full logs ===" docker logs ${containerName} exit 1 ' """ - - echo "✅ ${service} deployed successfully" } -def getContainerName(String service) { - // 服务名到容器名的映射 - def map = [ - 'viewsh-gateway': 'aiot-gateway', - 'viewsh-module-system-server': 'aiot-system-server', - 'viewsh-module-infra-server': 'aiot-infra-server', - 'viewsh-module-iot-server': 'aiot-iot-server', - 'viewsh-module-iot-gateway': 'aiot-iot-gateway' +// 检查服务健康状态 +def checkServiceHealth(String containerName, String serviceName) { + def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${SSH_KEY}" + + sh """ + ssh ${sshOpts} root@${DEPLOY_HOST} ' + echo "Checking ${serviceName}..." + STATUS=\$(docker inspect --format="{{.State.Health.Status}}" ${containerName} 2>/dev/null || echo "not_found") + if [ "\$STATUS" = "healthy" ]; then + echo "✅ ${serviceName} is healthy" + elif [ "\$STATUS" = "not_found" ]; then + echo "⚠️ ${serviceName} not found (may not be deployed)" + else + echo "❌ ${serviceName} is \$STATUS" + docker logs --tail 50 ${containerName} + exit 1 + fi + ' + """ +} + +// 按依赖顺序排序服务 +@NonCPS +def sortServicesByDependency(def services) { + def deployOrder = [ + 'viewsh-gateway', + 'viewsh-module-system-server', + 'viewsh-module-infra-server', + 'viewsh-module-iot-server', + 'viewsh-module-iot-gateway' ] - return map[service] ?: "aiot-${service}" + + return services.sort { a, b -> + deployOrder.indexOf(a) <=> deployOrder.indexOf(b) + } } -def getModulePath(String service) { - def map = [ - 'viewsh-gateway': 'viewsh-gateway', - 'viewsh-module-system-server': 'viewsh-module-system/viewsh-module-system-server', - 'viewsh-module-infra-server': 'viewsh-module-infra/viewsh-module-infra-server', - 'viewsh-module-iot-server': 'viewsh-module-iot/viewsh-module-iot-server', - 'viewsh-module-iot-gateway': 'viewsh-module-iot/viewsh-module-iot-gateway' - ] - return map[service] ?: service +// 获取服务对应的容器名称 +@NonCPS +def getContainerNameForService(String service) { + switch(service) { + case 'viewsh-gateway': + return 'aiot-gateway' + case 'viewsh-module-system-server': + return 'aiot-system-server' + case 'viewsh-module-infra-server': + return 'aiot-infra-server' + case 'viewsh-module-iot-server': + return 'aiot-iot-server' + case 'viewsh-module-iot-gateway': + return 'aiot-iot-gateway' + default: + return "aiot-${service}" + } +} + +// 获取服务对应的模块路径 +@NonCPS +def getModulePathForService(String service) { + switch(service) { + case 'viewsh-gateway': + return 'viewsh-gateway' + case 'viewsh-module-system-server': + return 'viewsh-module-system/viewsh-module-system-server' + case 'viewsh-module-infra-server': + return 'viewsh-module-infra/viewsh-module-infra-server' + case 'viewsh-module-iot-server': + return 'viewsh-module-iot/viewsh-module-iot-server' + case 'viewsh-module-iot-gateway': + return 'viewsh-module-iot/viewsh-module-iot-gateway' + default: + return service + } }