找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1514

积分

0

好友

193

主题
发表于 3 天前 | 查看: 10| 回复: 0

写在前面

XXL-Job 是国内任务调度领域的标杆项目,其设计兼顾了易用性与功能完整性。但在全面采用 Nacos 和 Spring Cloud Alibaba 技术栈的架构中,我们观察到了一些架构层面的摩擦点:XXL-Job 拥有独立的注册中心和配置存储,这与 Nacos 体系存在功能重叠。这并非设计缺陷,而是不同架构演进阶段的自然差异。

由此,我们开始思考一个问题:在云原生时代,中间件应该是一个独立的“平台”,还是可以内嵌的“能力模块”? JobFlow 正是基于这一思考而诞生的方案。

在 Nacos 体系下遇到的挑战

当技术栈确定以 Nacos 作为服务发现与配置中心后,使用 XXL-Job 会面临一些架构上的摩擦。这并非 XXL-Job 本身的问题,而是两套系统设计假设不同所致。

挑战一:两套注册中心导致状态不一致

当前典型的架构如下图所示:

XXL-Job与Nacos双注册中心架构对比图

同一个执行器实例需要向两个独立的注册中心汇报状态。这就带来了一个核心问题:两个注册中心的状态可能不一致

举一个实际运维场景:

假设你的某个服务实例内存占用异常升高,怀疑存在内存泄漏,需要执行 JVM Heap Dump 进行分析。于是,你在 Nacos 控制台手动将该实例下线,以避免新的业务流量打入。

运维人员:在 Nacos 控制台点击“下线”按钮。
Nacos:实例状态已更新为“下线” ✓
运维人员:放心开始执行 Dump 操作。
XXL-Job Admin:触发一个定时任务,调度到了这个“已下线”的实例。
运维人员:???

为什么会这样?因为 XXL-Job 自有的注册中心仍然认为这个实例是在线的。你在 Nacos 上的操作,XXL-Job 无从感知。

类似的问题还有:

  • 实例因网络短暂抖动,在 Nacos 健康检查中被标记为不健康,但在 XXL-Job 注册中心里仍是健康状态。
  • 实例重启后,在 Nacos 迅速重新注册成功,但 XXL-Job 可能还未及时更新,仍认为其处于离线状态。
  • 进行灰度发布时,在 Nacos 上通过权重控制流量,但 XXL-Job 的调度依旧按照原有的实例列表平均分配。

两套系统各自为政,状态不同步,给运维带来了额外的心智负担和风险。

挑战二:可观测性存在断层

在现有的调度体系中,调度与执行是割裂的:

调度器触发任务 --> 执行器执行业务 --> 出现问题
      |                 |              |
   Admin日志        执行器日志      问题到底出在哪一环?

要排查一次失败的任务执行,你需要:

  1. 登录 XXL-Job Admin 后台,查看调度日志和触发记录。
  2. 找到对应的执行器服务,去查询其应用日志。
  3. 依靠时间戳人工对齐两边日志,并祈祷两个服务器的时钟是同步的。

由于缺乏一个贯穿始终的 TraceId,排查问题如同大海捞针,效率低下。

挑战三:分片任务缺乏强一致性保障

XXL-Job 的分片机制是“建议式”的。执行器在代码中获取分片参数,然后自行计算需要处理的数据范围:

// 执行器端获取分片参数
int shardIndex = XxlJobHelper.getShardIndex();  // 例如:0
int shardTotal = XxlJobHelper.getShardTotal();  // 例如:10

// 开发者自行根据参数计算处理范围
List<Order> orders = orderDao.findByIdMod(shardIndex, shardTotal);

这里存在一个问题:整个过程没有分布式锁的保护,在某些边界情况下,两个执行器实例可能同时处理同一批数据。

在生产环境中曾遇到过这样的场景:

某个执行器实例因故障重启,XXL-Job Admin 认为它已下线,并将其负责的分片重新分配给了其他实例。然而,该实例快速恢复后,可能仍在处理自己内存中“旧”的分片任务,导致数据被重复处理。

核心设计理念:中间件即业务

在阐述具体方案前,有必要先厘清背后的设计理念。这关乎于我们如何看待现代架构中的中间件。

从“重型中间件”到“轻量级能力”

在传统架构思维中,中间件往往作为一个“外挂”系统存在:

业务层:订单服务、用户服务...(一系列微服务)
   ↓ 通过RPC/HTTP调用
中间件层:XXL-Job Admin(需独立部署、独立运维、独立监控)

这种架构带来了明显的“屏障”:

  • 中间件平台需要单独部署集群和维护一套配置。
  • 监控告警体系需要单独对接和配置。
  • 日志系统需要额外打通,才能汇聚调度与执行的日志。
  • 配置管理(如线程池大小、超时时间)需要另一套维护流程。
  • 业务研发团队与中间件平台团队可能分离,协同成本高。

但在云原生时代,这些屏障在很大程度上是可以被消除的。

JobFlow 倡导的理念是:中间件即业务

业务层:订单服务、用户服务、JobFlow调度器...
         ↓
   它们都是对等的微服务,共享同一套技术体系。

调度能力不再是一个高高在上的独立“平台”,而是转化为业务体系内的一种“能力模块”:

  • 相同的部署模式:全部容器化,通过 K8s 统一编排和管理。
  • 统一的监控告警:直接复用已有的 Prometheus、Grafana 监控栈。
  • 一致的配置管理:调度器自身的配置(如线程池参数)也存放在 Nacos Config,支持动态刷新。
  • 集成的日志收集:日志通过统一的 ELK 或 Loki 链路收集,利用 TraceId 轻松串联。
  • 统一的维护团队:由业务研发团队自主维护,减少跨团队协作成本。

中间件不再是外挂的系统,而是内嵌在业务架构中的能力组件。

这种理念带来诸多好处:

  • 架构统一:技术栈和运维模式一致,降低学习和认知成本。
  • 基础设施复用:无需为中间件搭建独立的基础设施,显著降低运维成本。
  • 状态一致:服务实例的生命周期由统一的注册中心(Nacos)管理,彻底避免“Nacos 显示下线但调度依然生效”的割裂状态。
  • 团队自主可控:业务团队对调度能力拥有完全的控制权,迭代更敏捷。

因此,JobFlow 的战略定位很明确:并非要打造一个取代所有场景的通用调度平台,而是探索如何让调度能力更优雅地融入已有的微服务技术体系。

实现路径:做减法与做加法

做减法:剔除冗余组件

既然已拥有强大的 Nacos,就无需再维护一套独立的注册中心:

  • 移除自建注册中心:执行器的服务发现完全依托于 Nacos Service Discovery。
  • 精简数据库职责MySQL 仅存储任务定义、执行记录和审计日志,不再承担服务注册信息的存储功能。

做加法:增强核心能力

在去除冗余的同时,针对现有痛点进行能力增强:

  • 内置全链路追踪:调度器生成全局 TraceId,并贯穿整个任务执行链路。
  • 实现真正的分片:提供由分布式锁保护的、有状态、可恢复的强一致性分片机制。
  • 引入智能重试策略:支持指数退避等高级重试机制,并集成死信队列处理彻底失败的任务。
  • 调度器配置云原生化:利用 Nacos Config 管理调度器运行时配置(线程池、超时时间等),支持动态调整、多实例共享和版本回滚。
  • 共享基础设施:作为标准微服务部署,天然复用 Actuator 健康检查、Prometheus 指标暴露、日志收集等现有设施。
  • 提供开箱即用的监控指标:暴露标准的 Prometheus 指标,便于监控调度性能与健康度。
  • 提供完备的 RESTful API:支持手动触发、任务状态查询、失败重试等运维操作。

JobFlow 架构设计

整体架构

JobFlow轻量调度系统架构图

架构变得非常清晰,核心只有三个部分:

  • Nacos:统一的服务发现与配置中心,作为整个体系的基石。
  • JobFlow Scheduler:一个轻量级的无状态调度器服务。
  • MySQL:用于存储任务定义、执行记录和审计日志。

关键在于:JobFlow Scheduler 本身就是一个普通的微服务。 它通过标准方式部署,自动复用现成的 Prometheus 监控、Actuator 管理端点、统一告警和日志收集链路,实现了运维成本的趋近于零。

任务调用流程

JobFlow任务调度全链路流程图

流程中的几个关键点:

  1. 调度器在触发任务时,会生成一个全局唯一的 traceId
  2. traceId 通过 HTTP Header 传递给执行器。
  3. 执行器将 traceId 写入 MDC(Mapped Diagnostic Context)日志上下文。
  4. 此后,执行器业务逻辑中的所有日志都会自动携带此 traceId
  5. 在 ELK 等日志平台中,只需搜索这个 traceId,即可一次性获取从调度触发到业务执行完毕的完整日志链路,极大提升了排查效率。

分片调度机制

JobFlow强一致性分片调度示意图

这不再是“建议你处理哪一段数据”,而是明确指派并锁定一个具体的数据范围。调度器会计算好每个分片对应的精确数据区间(如 ID 0-333333),并将该区间与一个唯一的分布式锁绑定,一同下发给执行器。执行器必须成功获取锁后才能处理该区间数据,从而保证了同一份数据绝不会被多个实例同时处理。

关键特性详解

特性一:全链路 TraceId

这是提升可观测性的核心特性。调度器生成 TraceId 并通过 HTTP 头传递给执行器。

// JobFlow Scheduler 调度器端
String traceId = UUID.randomUUID().toString();
HttpHeaders headers = new HttpHeaders();
headers.set("X-Trace-Id", traceId);
headers.set("X-Shard-Index", "0");
headers.set("X-Shard-Total", "10");
// 调用执行器
restTemplate.postForEntity(url, new HttpEntity<>(params, headers), JobResult.class);

执行器接收后,将其注入日志上下文:

// 执行器端
@PostMapping("/internal/job/{jobName}")
public JobResult execute(@RequestHeader("X-Trace-Id") String traceId, ...) {
    MDC.put("traceId", traceId); // 关键:注入MDC
    try {
        log.info("开始执行任务"); // 此条日志会自动附带 traceId
        // ... 执行业务逻辑
        return JobResult.success();
    } finally {
        MDC.clear();
    }
}

如此一来,在日志系统中搜索 traceId,你可以立刻看到:

  • 调度器在何时触发。
  • 调用了哪个执行器实例(IP:Port)。
  • 执行器处理了哪些业务数据。
  • 执行过程中是否有错误,错误堆栈是什么。
    一个 traceId 贯穿始终,将排查问题的效率提升一个数量级。

特性二:强一致性分片

调度器明确分配数据范围,并辅以分布式锁确保唯一性。

// 调度器计算并分配分片范围
int totalRecords = 1000000;
int shardTotal = 10;
int rangeSize = totalRecords / shardTotal;

for (int i = 0; i < shardTotal; i++) {
    long startId = i * rangeSize;
    long endId = (i + 1) * rangeSize - 1;

    // 为每个分片范围生成唯一的锁键
    String lockKey = String.format("lock:job:order-sync:range:%d-%d", startId, endId);

    JobRequest request = new JobRequest();
    request.setTraceId(traceId);
    request.setStartId(startId); // 明确起始ID
    request.setEndId(endId);     // 明确结束ID
    request.setLockKey(lockKey); // 附上锁标识

    executeAsync(instance, request); // 异步调用执行器
}

执行器接收到明确的范围和锁标识后,首先尝试获取锁:

@PostMapping("/internal/job/order-sync")
public JobResult sync(
    @RequestHeader("X-Start-Id") Long startId,
    @RequestHeader("X-End-Id") Long endId,
    @RequestHeader("X-Lock-Key") String lockKey
) {
    // 1. 先尝试获取分布式锁
    boolean locked = redisLock.tryLock(lockKey, 60, TimeUnit.SECONDS);
    if (!locked) {
        log.warn("分片范围 {}-{} 已被其他实例锁定", startId, endId);
        return JobResult.skip("已有其他实例处理");
    }

    try {
        // 2. 安全地处理指定范围的数据
        List<Order> orders = orderDao.findByIdBetween(startId, endId);
        // ... 业务处理逻辑
        return JobResult.success();
    } finally {
        // 3. 处理完毕释放锁
        redisLock.unlock(lockKey);
    }
}

这种机制确保了:

  • 每个分片有精确、无歧义的数据边界。
  • 同一时刻,一个数据分片只被一个执行器实例处理。
  • 即使执行器实例意外重启,由于锁的保护,分片任务也不会被混乱执行。

特性三:智能重试策略

任务执行失败后,并非简单地进行固定间隔重试,而是采用指数退避等更智能的策略。

# 重试配置 (可配置在Nacos中)
retry:
  max: 5                  # 最大重试次数
  backoff: EXPONENTIAL    # 退避策略:指数型
  initialDelay: 1s        # 初始延迟
  maxDelay: 5m            # 最大延迟上限

调度器中的重试逻辑:

public void scheduleRetry(JobExecution execution) {
    int retryCount = execution.getRetryCount();
    if (retryCount >= maxRetry) {
        // 超过最大重试次数,转入死信队列等待人工干预
        deadLetterQueue.send(execution);
        return;
    }

    // 计算延迟时间:1s, 2s, 4s, 8s, 16s... 但不超过maxDelay
    long delay = Math.min(
        initialDelay * (1 << retryCount), // 指数计算
        maxDelay
    );

    // 调度延迟重试
    scheduler.schedule(() -> {
        retry(execution);
    }, delay, TimeUnit.SECONDS);
}

特性四:调度器配置云原生化

JobFlow Scheduler 自身的运行配置完全交由 Nacos Config 管理,享受云原生配置管理的所有优势。

# Nacos Config 配置项: jobflow-scheduler.yaml
jobflow:
  scheduler:
    thread-pool-size: 20   # 调度线程池大小,支持动态调优
    timeout: 300           # 任务默认超时时间(秒)
    max-retry: 3           # 默认重试次数

  executor:
    connect-timeout: 5000  # HTTP调用连接超时
    read-timeout: 30000    # HTTP调用读取超时

  redis:
    lock-timeout: 60       # 分片锁默认持有时间(秒)

  compensation:
    enabled: true          # 是否启用补偿任务
    interval: 60000        # 补偿任务扫描间隔(毫秒)
    stuck-threshold: 600000 # 任务卡住判定阈值(10分钟)

这样做的好处显而易见:

  1. 动态调整,无需重启:业务高峰时段,发现调度器压力大,可直接在 Nacos 控制台将 thread-pool-size 从 20 调整为 50,配置实时推送生效,调度能力即刻提升。
  2. 多实例配置共享:当你部署了多个调度器实例以实现高可用时,只需在 Nacos 修改一次配置,所有实例会同时收到更新,保持配置一致。
  3. 完善的版本管理与回滚:所有配置变更在 Nacos 中都有历史版本记录。如果某次参数调整导致问题,可以一键快速回滚到上一个稳定版本。

注意:这里管理的是调度器自身运行时的配置(如资源参数、超时规则)。任务的定义(任务名、CRON表达式、所属服务等)仍然通过 API 或管理界面维护,并持久化在数据库中。

特性五:精简清晰的数据库设计

MySQL 中只存储最核心、最必要的状态信息,职责清晰。

-- 1. 任务定义表
CREATE TABLE job_definition (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    job_name VARCHAR(100) UNIQUE,      -- 任务名称
    service_name VARCHAR(100),         -- 所属微服务名 (用于Nacos发现)
    handler VARCHAR(100),              -- 执行器中的处理器名
    cron VARCHAR(100),                 -- CRON表达式
    enabled BOOLEAN DEFAULT TRUE,      -- 是否启用
    created_at TIMESTAMP,
    updated_at TIMESTAMP
);

-- 2. 任务执行记录表
CREATE TABLE job_execution (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    job_name VARCHAR(100) NOT NULL,    -- 关联的任务名
    trace_id VARCHAR(64) NOT NULL UNIQUE, -- 全链路唯一标识
    trigger_time TIMESTAMP NOT NULL,   -- 触发时间
    finish_time TIMESTAMP,             -- 完成时间
    status VARCHAR(20) NOT NULL,       -- 状态: PENDING/RUNNING/SUCCESS/FAILED
    retry_count INT DEFAULT 0,         -- 已重试次数
    result_message TEXT,               -- 执行结果或错误信息
    INDEX idx_trace (trace_id),        -- 便于按traceId查询
    INDEX idx_job_time (job_name, trigger_time) -- 便于按任务查历史
);

数据库的职责被严格限定为:

  • 存储任务定义(通过管理API操作)。
  • 记录执行状态与元数据(用于状态查询和报表)。
  • 保存审计日志(满足合规要求)。

而以下数据则存储在其他更合适的组件中:

  • 服务实例信息 -> Nacos Service Discovery
  • 调度器运行时配置 -> Nacos Config
  • 详细的执行过程日志 -> ELK (通过 TraceId 关联查询)

这样的设计使得数据库负载轻量化,查询高效,且各组件各司其职。

常见疑问解答

问题一:如果 Nacos 注册中心挂了,调度是否完全瘫痪?

:如果 Nacos 集群完全不可用,那么基于其进行服务发现的所有微服务(包括业务服务和调度器)之间的调用都会受到影响。此时,任务调度并非需要最高优先保障的环节。

不过,我们可以在客户端(调度器)实现简单的降级策略,例如使用本地缓存:

@Service
public class ExecutorDiscovery {
    // 使用Guava Cache等构建本地缓存
    private LoadingCache<String, List<String>> cache = CacheBuilder.newBuilder()
            .expireAfterWrite(5, TimeUnit.MINUTES) // 缓存5分钟
            .build(key -> namingService.getAllInstances(key));

    public List<String> getInstances(String serviceName) {
        try {
            return namingService.getAllInstances(serviceName);
        } catch (NacosException e) {
            log.warn("Nacos 服务发现暂时不可用,使用本地缓存实例列表");
            return cache.getIfPresent(serviceName); // 降级:返回缓存的实例列表
        }
    }
}

这样,在 Nacos 短暂网络波动或重启期间,调度器仍能基于最近已知的健康实例列表进行任务调度,保障一定的可用性。

问题二:数据库写入失败,导致调度状态与实际不一致怎么办?

:采用最终一致性模型来处理此类问题。核心思想是异步更新状态,并通过补偿任务修复异常。

// 1. 调度开始时,先快速写入一个初始状态(PENDING)
jobExecutionDao.insert(new JobExecution()
        .setTraceId(traceId)
        .setStatus("PENDING")
        .setTriggerTime(now));

// 2. 异步执行任务调用,避免数据库事务阻塞网络IO
CompletableFuture.runAsync(() -> {
    try {
        JobResult result = executeJob(executor, request);
        // 3. 调用成功或失败后,异步更新最终状态
        jobExecutionDao.updateStatus(traceId, result.getStatus());
    } catch (Exception e) {
        // 4. 即使调用过程异常,也标记为失败
        jobExecutionDao.updateStatus(traceId, "FAILED");
    }
});

// 5. 后台定时补偿任务,修复“卡住”的记录
@Scheduled(fixedDelay = 60000)
public void fixStuckExecutions() {
    // 查找状态为 PENDING 但超过10分钟未更新的记录
    List<JobExecution> stuck = jobExecutionDao.findStuckExecutions();
    for (JobExecution exec : stuck) {
        // 可以根据 traceId 去ELK日志系统查询该任务的实际执行结果
        // 如果日志显示已成功,则更新状态为 SUCCESS;若失败则更新为 FAILED;否则标记为 TIMEOUT
        reconcileExecution(exec);
    }
}

即使因为数据库压力导致状态更新延迟或失败,运维人员也可以通过 traceId 在日志系统中定位到该次任务的实际执行情况,系统本身也有补偿机制自动修复状态。

问题三:没有可视化管理界面,如何运维?

:在初期,我们可以提供一套完整的 RESTful API 来满足核心的运维需求,后期再逐步开发功能丰富的管理界面。

@RestController
@RequestMapping("/api/jobs")
public class JobController {

    // 手动立即触发一次任务
    @PostMapping("/{name}/trigger")
    public JobResult trigger(@PathVariable String name) {
        return jobService.triggerNow(name);
    }

    // 分页查询某个任务的执行历史
    @GetMapping("/{name}/executions")
    public Page<JobExecution> history(
            @PathVariable String name,
            @RequestParam int page,
            @RequestParam int size) {
        return jobExecutionDao.findByJobName(name, PageRequest.of(page, size));
    }

    // 根据 traceId 查询某次执行的详细记录
    @GetMapping("/executions/{traceId}")
    public JobExecution detail(@PathVariable String traceId) {
        return jobExecutionDao.findByTraceId(traceId);
    }

    // 对失败的任务执行记录进行手动重试
    @PostMapping("/executions/{traceId}/retry")
    public JobResult retry(@PathVariable String traceId) {
        return jobService.retry(traceId);
    }
}

配合 Swagger/OpenAPI 自动生成的文档界面,这些 API 足以支持任务的手动触发、状态查询、历史回溯和失败重试等日常运维操作。待方案稳定后,可以基于这些 API 快速构建一个 Vue/React 管理后台。

问题四:如何保证调度器自身的高可用?

:JobFlow Scheduler 被设计为无状态服务,这是实现高可用的基础。你可以像部署普通微服务一样,部署多个调度器实例。

为了避免多个实例同时触发同一个定时任务,需要引入分布式协调机制。一种简单可靠的方式是使用分布式锁:

@Service
public class DistributedJobScheduler {
    @Scheduled(cron = "0 */1 * * * ?") // 每分钟执行一次
    public void scheduledTrigger() {
        List<JobConfig> jobs = getEnabledJobs();
        for (JobConfig job : jobs) {
            // 为每个任务尝试获取一把分布式锁,例如使用Redis
            String lockKey = "lock:schedule:" + job.getName();
            boolean locked = redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS);
            if (locked) {
                try {
                    trigger(job); // 抢到锁的实例执行调度
                } finally {
                    redisLock.unlock(lockKey);
                }
            }
            // 没抢到锁的实例自动跳过,等待下次调度周期
        }
    }
}

或者,可以采用更优雅的一致性哈希分配策略,让每个调度器实例固定负责一部分任务,减少锁竞争:

public boolean isMyResponsibility(String jobName) {
    int hash = jobName.hashCode();
    List<String> schedulerInstances = getSchedulerInstances(); // 从Nacos获取所有调度器实例
    String responsibleInstance = consistentHash.get(schedulerInstances, hash);
    return responsibleInstance.equals(myInstanceId); // 判断当前实例是否该任务的负责者
}

// 在调度循环中
if (isMyResponsibility(job.getName())) {
    trigger(job); // 只有负责该任务的实例才会触发
}

总结

JobFlow 更多是一种思路的探讨和原型设计,其核心价值在于传递一个理念:中间件即业务

在云原生架构深入人心的今天,像任务调度这样的基础能力,或许不必再被塑造成一个需要独立部署、独立运维的“重型平台”。它可以转化为一个内嵌在微服务体系中的轻量级能力模块,与业务服务共享同一套技术栈、部署模式、监控体系和运维流程。

本文并非旨在替代 XXL-Job。XXL-Job 在通用性、功能完整性和社区生态上有着不可替代的优势,适用于绝大多数场景。JobFlow 的思路则更侧重于服务那些已经深度拥抱 Nacos 和 Spring Cloud Alibaba 技术栈,并对架构一致性、可观测性有更高要求的团队。

它的优势体现在:

  • 更契合云原生生态:彻底复用 Nacos 体系,避免维护两套注册/配置中心,架构统一,运维成本低。
  • 更强的可观测性:通过贯穿始终的 TraceId,将调度、执行、日志无缝串联,问题排查效率倍增。
  • 更严谨的数据一致性:提供由分布式锁保护的强一致性分片机制,避免数据重复处理。

技术方案的价值常常在于思辨与碰撞。我们分享 JobFlow 的设计,是希望抛砖引玉,激发更多关于如何在云原生环境下构建更优雅、更内聚的基础设施的思考。或许这个思路存在未曾考虑的缺陷,或许你有更精妙的解决方案,欢迎在 云栈社区 等相关技术论坛进行深入的交流和讨论。最后,我们由衷感谢许雪里老师及 XXL-Job 开源社区,正是这些优秀的项目为我们提供了坚实的基石和前进的参照。




上一篇:Milvus向量索引分段处理与查询效率优化对比VexDB/Vastbase 100
下一篇:开源工具NocoDB连接MySQL/PostgreSQL数据库,零代码生成多维表格与API
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-9 18:04 , Processed in 0.202945 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表