2.2. 复制器数据库

版本 2.1.0 中的变更: 引入了调度复制器。默认情况下,复制状态不再写回文档。有新的复制作业状态和新的 API 端点 _scheduler/jobs_scheduler/docs

版本 3.2.0 中的变更: 引入了公平共享调度。多个 _replicator 数据库获得运行其作业的平等机会(可配置)。以前,复制作业的调度不考虑其源数据库。

版本 3.3.0 中的变更: winning_revs_only: true 复制器选项用于复制获胜的文档修订版。

The _replicator 数据库的工作方式与 CouchDB 中的任何其他数据库相同,但添加到其中的文档将触发复制。创建(PUTPOST)文档以启动复制。 DELETE 复制文档以取消正在进行的复制。

这些文档的内容与我们用于 POST_replicate 的 JSON 对象完全相同(字段 sourcetargetcreate_targetcreate_target_paramscontinuousdoc_idsfilterquery_paramsuse_checkpointscheckpoint_interval)。

复制文档可以有用户定义的 _id(便于以后查找特定的复制请求)。添加到复制器数据库的设计文档(和 _local 文档)将被忽略。

默认的复制器数据库是 _replicator。可以创建额外的复制器数据库。为了被系统识别为复制器数据库,它们的数据库名称应该以 /_replicator 结尾。

2.2.1. 基础

假设你将以下文档 POST 到 _replicator

{
    "_id": "my_rep",
    "source": "http://myserver.com/foo",
    "target": {
        "url": "http://localhost:5984/bar",
        "auth": {
            "basic": {
                "username": "user",
                "password": "pass"
            }
        }
    },
    "create_target":  true,
    "continuous": true
}

在 Couch 日志中,你会看到 2 个类似这样的条目

[notice] 2017-04-05T17:16:19.646716Z [email protected] <0.29432.0> -------- Replication `"a81a78e822837e66df423d54279c15fe+continuous+create_target"` is using:
    4 worker processes
    a worker batch size of 500
    20 HTTP connections
    a connection timeout of 30000 milliseconds
    10 retries per request
    socket options are: [{keepalive,true},{nodelay,false}]
[notice] 2017-04-05T17:16:19.646759Z [email protected] <0.29432.0> -------- Document `my_rep` triggered replication `a81a78e822837e66df423d54279c15fe+continuous+create_target`

然后可以从 http://adm:pass@localhost:5984/_scheduler/docs/_replicator/my_rep 查询此文档的复制状态

{
    "database": "_replicator",
    "doc_id": "my_rep",
    "error_count": 0,
    "id": "a81a78e822837e66df423d54279c15fe+continuous+create_target",
    "info": {
        "revisions_checked": 113,
        "missing_revisions_found": 113,
        "docs_read": 113,
        "docs_written": 113,
        "changes_pending": 0,
        "doc_write_failures": 0,
        "checkpointed_source_seq": "113-g1AAAACTeJzLYWBgYMpgTmHgz8tPSTV0MDQy1zMAQsMckEQiQ1L9____szKYE01ygQLsZsYGqcamiZjKcRqRxwIkGRqA1H-oSbZgk1KMLCzTDE0wdWUBAF6HJIQ",
        "source_seq": "113-g1AAAACTeJzLYWBgYMpgTmHgz8tPSTV0MDQy1zMAQsMckEQiQ1L9____szKYE01ygQLsZsYGqcamiZjKcRqRxwIkGRqA1H-oSbZgk1KMLCzTDE0wdWUBAF6HJIQ",
        "through_seq": "113-g1AAAACTeJzLYWBgYMpgTmHgz8tPSTV0MDQy1zMAQsMckEQiQ1L9____szKYE01ygQLsZsYGqcamiZjKcRqRxwIkGRqA1H-oSbZgk1KMLCzTDE0wdWUBAF6HJIQ"
    },
    "last_updated": "2017-04-05T19:18:15Z",
    "node": "[email protected]",
    "source_proxy": null,
    "target_proxy": null,
    "source": "http://myserver.com/foo/",
    "start_time": "2017-04-05T19:18:15Z",
    "state": "running",
    "target": "http://localhost:5984/bar/"
}

状态是 running。这意味着复制器已安排此复制作业运行。复制文档内容保持不变。以前,在 2.1 版之前,它会使用 triggered 状态进行更新。

复制作业也会出现在

http://adm:pass@localhost:5984/_scheduler/jobs

{
    "jobs": [
        {
            "database": "_replicator",
            "doc_id": "my_rep",
            "history": [
                {
                    "timestamp": "2017-04-05T19:18:15Z",
                    "type": "started"
                },
                {
                    "timestamp": "2017-04-05T19:18:15Z",
                    "type": "added"
                }
            ],
            "id": "a81a78e822837e66df423d54279c15fe+continuous+create_target",
            "info": {
                "changes_pending": 0,
                "checkpointed_source_seq": "113-g1AAAACTeJzLYWBgYMpgTmHgz8tPSTV0MDQy1zMAQsMckEQiQ1L9____szKYE01ygQLsZsYGqcamiZjKcRqRxwIkGRqA1H-oSbZgk1KMLCzTDE0wdWUBAF6HJIQ",
                "doc_write_failures": 0,
                "docs_read": 113,
                "docs_written": 113,
                "missing_revisions_found": 113,
                "revisions_checked": 113,
                "source_seq": "113-g1AAAACTeJzLYWBgYMpgTmHgz8tPSTV0MDQy1zMAQsMckEQiQ1L9____szKYE01ygQLsZsYGqcamiZjKcRqRxwIkGRqA1H-oSbZgk1KMLCzTDE0wdWUBAF6HJIQ",
                "through_seq": "113-g1AAAACTeJzLYWBgYMpgTmHgz8tPSTV0MDQy1zMAQsMckEQiQ1L9____szKYE01ygQLsZsYGqcamiZjKcRqRxwIkGRqA1H-oSbZgk1KMLCzTDE0wdWUBAF6HJIQ"
            },
            "node": "[email protected]",
            "pid": "<0.1174.0>",
            "source": "http://myserver.com/foo/",
            "start_time": "2017-04-05T19:18:15Z",
            "target": "http://localhost:5984/bar/",
            "user": null
        }
    ],
    "offset": 0,
    "total_rows": 1
}

_scheduler/jobs 显示更多信息,例如状态更改的详细历史记录。如果持久复制尚未开始、已失败或已完成,则只能在 _scheduler/docs 中找到有关其状态的信息。请记住,一些复制文档可能无效,无法成为复制作业。其他文档可能会延迟,因为它们正在从缓慢的源数据库中获取数据。

如果出现错误,例如源数据库丢失,复制作业将崩溃并在等待一段时间后重试。每次连续崩溃都会导致更长的等待时间。

例如,POST 此文档

{
    "_id": "my_rep_crashing",
    "source": "http://myserver.com/missing",
    "target": {
        "url": "http://localhost:5984/bar",
        "auth": {
            "basic": {
                "username": "user",
                "password": "pass"
            }
        }
    },
    "create_target":  true,
    "continuous": true
}

当源数据库丢失时,将导致周期性的启动和崩溃,间隔越来越长。此复制的 _scheduler/jobs 中的 history 列表将类似于以下内容

[
      {
          "reason": "db_not_found: could not open http://adm:*****@localhost:5984/missing/",
          "timestamp": "2017-04-05T20:55:10Z",
          "type": "crashed"
      },
      {
          "timestamp": "2017-04-05T20:55:10Z",
          "type": "started"
      },
      {
          "reason": "db_not_found: could not open http://adm:*****@localhost:5984/missing/",
          "timestamp": "2017-04-05T20:47:10Z",
          "type": "crashed"
      },
      {
          "timestamp": "2017-04-05T20:47:10Z",
          "type": "started"
      }
]

_scheduler/docs 显示更简短的摘要

{
      "database": "_replicator",
      "doc_id": "my_rep_crashing",
      "error_count": 6,
      "id": "cb78391640ed34e9578e638d9bb00e44+create_target",
      "info": {
           "error": "db_not_found: could not open http://myserver.com/missing/"
      },
      "last_updated": "2017-04-05T20:55:10Z",
      "node": "[email protected]",
      "source_proxy": null,
      "target_proxy": null,
      "source": "http://myserver.com/missing/",
      "start_time": "2017-04-05T20:38:34Z",
      "state": "crashing",
      "target": "http://localhost:5984/bar/"
}

重复崩溃被描述为 crashing 状态。 -ing 后缀意味着这是一个临时状态。用户可以随时创建丢失的数据库,然后复制作业可以恢复正常。

2.2.2. 描述相同复制的文档

假设 2 个文档按以下顺序添加到 _replicator 数据库

{
    "_id": "my_rep",
    "source": "http://myserver.com/foo",
    "target":  "http://user:pass@localhost:5984/bar",
    "create_target":  true,
    "continuous": true
}

{
    "_id": "my_rep_dup",
    "source": "http://myserver.com/foo",
    "target":  "http://user:pass@localhost:5984/bar",
    "create_target":  true,
    "continuous": true
}

两者都描述了完全相同的复制(只有它们的 _ids 不同)。在这种情况下,文档 my_rep 触发复制,而 my_rep_dup` 将失败。检查 _scheduler/docs 说明了它失败的确切原因

{
    "database": "_replicator",
    "doc_id": "my_rep_dup",
    "error_count": 1,
    "id": null,
    "info": {
        "error": "Replication `a81a78e822837e66df423d54279c15fe+continuous+create_target` specified by document `my_rep_dup` already started, triggered by document `my_rep` from db `_replicator`"
    },
    "last_updated": "2017-04-05T21:41:51Z",
    "source": "http://myserver.com/foo/",
    "start_time": "2017-04-05T21:41:51Z",
    "state": "failed",
    "target": "http://user:****@localhost:5984/bar",
}

注意此复制的状态是 failed。与 crashing 不同,failed 状态是终态。只要这两个文档都存在,复制器就不会重试运行 my_rep_dup 复制。另一个原因可能是文档格式错误。例如,如果工作进程计数被指定为字符串("worker_processes": "a few")而不是整数,则会发生故障。

2.2.3. 复制调度器

创建复制作业后,它们由调度器管理。调度器是复制组件,它定期停止一些作业并启动其他作业。这种行为使得可以拥有比集群可以同时运行的作业数量更多的作业。不断失败的复制作业将受到惩罚,并被迫等待。每次连续失败都会导致等待时间呈指数级增长。

在决定停止哪些作业和启动哪些作业时,调度器使用循环算法来确保公平性。运行时间最长的作业将被停止,等待时间最长的作业将被启动。

注意

非持续(正常)复制在开始运行后会得到不同的处理。有关更多信息,请参阅 正常复制与持续复制 部分。

调度器的行为可以通过 max_jobsintervalmax_churn 选项进行配置。有关更多信息,请参阅 复制器配置部分

2.2.4. 复制状态

复制作业在其生命周期中会经历各种状态。这是所有状态及其之间转换的图表

Replication state diagram

复制状态图

蓝色和黄色形状代表复制作业状态。

梯形代表外部 API,即用户与复制器交互的方式。将文档写入 _replicator 是创建复制的首选方法,但也可以发布到 _replicate HTTP 端点。

六边形表示内部 API 边界。它们对于此图是可选的,仅作为附加信息显示,以帮助阐明复制器的工作原理。有两个处理阶段:第一个是解析复制文档并将其转换为复制作业,第二个是调度器本身。调度器运行复制作业,定期停止和启动一些作业。通过 _replicate 端点发布的作业绕过第一个组件,直接进入调度器。

2.2.4.1. 状态描述

在解释每个状态的细节之前,值得注意的是图中每个状态的颜色和形状

蓝色黄色 将状态分别划分为“健康”和“不健康”。不健康状态表示出现问题,可能需要用户关注。

矩形椭圆形 将“终端”状态与“非终端”状态区分开来。终端状态是指不再会过渡到其他状态的状态。非正式地说,处于终端状态的作业不会重试,也不会消耗内存或 CPU 资源。

  • Initializing: 表示复制器已注意到复制文档的变化。作业应快速通过此状态过渡。如果长时间停滞在此处,可能意味着存在内部错误。

  • Failed: 无法处理复制文档并将其转换为调度器的有效复制作业。此状态是终端状态,需要用户干预才能解决问题。最终导致此状态的一个典型原因是文档格式错误。例如,为接受布尔值的参数指定整数。另一个失败的原因可能是指定重复复制。重复复制是指具有相同参数但文档 ID 不同的复制。

  • Error: 无法将复制文档更新转换为复制作业。与 Failed 状态不同,此状态是临时的,复制器将定期重试。如果连续失败,将应用指数级回退。此状态存在的主要原因是处理具有自定义用户函数的过滤复制。为了计算复制 ID,需要过滤函数内容。在检索到函数代码之前,无法创建复制作业。由于检索是通过网络进行的,因此必须处理临时故障。

  • Running: 复制作业正在正常运行。这意味着,可能存在一个更改馈送打开,如果注意到更改,它们将被处理并发布到目标。即使其工作程序当前没有从源流式传输更改到目标,而只是在更改馈送上等待,作业仍被认为是 Running。连续复制很可能最终处于此状态。

  • Pending: 复制作业未运行,正在等待其轮到。当添加到调度器的复制作业数量超过 replicator.max_jobs 时,将达到此状态。在这种情况下,调度器将定期停止和启动作业子集,试图让每个作业都有公平的机会取得进展。

  • Crashing: 复制作业已成功添加到复制调度器。但是,在上次运行期间遇到了错误。错误可能是网络故障、源数据库丢失、权限错误等。连续的重复崩溃会导致指数级回退。此状态被认为是临时的(非终端状态),复制作业将定期重试。

  • Completed: 这是非连续复制的终端成功状态。一旦进入此状态,复制将被调度器“遗忘”,并且不再消耗任何 CPU 或内存资源。连续复制作业永远不会达到此状态。

注意

状态 ErrorCrashing 的最大回退间隔是根据 replicator.max_history 选项计算的。有关更多信息,请参见 复制器配置部分

2.2.4.2. 正常复制与连续复制

正常(非连续)复制一旦启动,将被允许运行到完成。这种行为是为了保留其将源数据库的快照复制到目标的语义。例如,如果在启动复制后向源添加了新文档,这些更新不应显示在目标数据库中。停止和重新启动正常复制将违反该约束。

警告

当存在连续复制和正常复制的混合时,一旦正常复制被安排运行,它们可能会暂时使连续复制作业饿死。

但是,如果操作员减少了最大复制数量的值,正常复制仍将被停止并重新安排。这样,如果操作员决定复制压倒了节点,则它有能力恢复。任何停止的复制将被重新提交到队列以重新安排。

2.2.5. 兼容模式

CouchDB 复制器的先前版本将状态更新写回复制文档。在用户代码以编程方式读取这些状态的情况下,可以通过配置设置启用兼容模式

[replicator]
update_docs = true

在此模式下,复制器将继续将状态更新写入文档。

要有效地禁用定期停止和启动作业的调度行为,请将 max_jobs 配置设置设置为一个很大的数字。例如

[replicator]
max_jobs = 9999999

有关其他复制器配置选项,请参见 复制器配置部分

2.2.6. 取消复制

要取消复制,只需 DELETE 触发复制的文档。要更新复制,例如更改工作程序数量或源,只需使用新数据更新文档即可。如果复制文档中存在额外的应用程序特定数据,复制器将忽略该数据。

2.2.7. 服务器重启

当 CouchDB 重新启动时,它会检查其 _replicator 数据库,如果文档描述的复制未处于 completedfailed 状态,则重新启动这些复制。如果它们处于这些状态,则会被忽略。

2.2.8. 集群

在集群中,复制作业在所有节点之间均匀平衡,以便复制作业一次仅在一个节点上运行。

每次发生集群成员资格更改时,即添加或删除节点时,就像在滚动重启中一样,复制器应用程序会注意到更改,重新扫描所有文档和正在运行的复制,并根据新的活动节点集重新评估其集群放置。此机制还提供了复制故障转移,以防节点发生故障。从复制文档启动的复制作业(但不是从 _replicate HTTP 端点启动的复制作业)将自动迁移到一个活动节点。

2.2.9. 其他复制器数据库

假设复制器数据库 (_replicator) 有这两个文档,它们代表从服务器 A 和 B 进行拉取复制

{
    "_id": "rep_from_A",
    "source":  "http://aserver.com:5984/foo",
    "target": {
        "url": "http://localhost:5984/foo_a",
        "auth": {
            "basic": {
                "username": "user",
                "password": "pass"
            }
        }
    },
    "continuous":  true
}
{
    "_id": "rep_from_B",
    "source":  "http://bserver.com:5984/foo",
    "target": {
        "url": "http://localhost:5984/foo_b",
        "auth": {
            "basic": {
                "username": "user",
                "password": "pass"
            }
        }
    },
    "continuous":  true
}

现在,无需停止和重新启动 CouchDB,即可添加另一个复制器数据库。例如 another/_replicator

$ curl -X PUT http://user:pass@localhost:5984/another%2F_replicator/
{"ok":true}

注意

数据库名称中的“/”字符在 URL 中使用时应进行转义。

然后将复制文档添加到新的复制器数据库

{
    "_id": "rep_from_X",
    "source":  "http://xserver.com:5984/foo",
    "target":  "http://user:pass@localhost:5984/foo_x",
    "continuous":  true
}

从现在开始,系统中有三个复制处于活动状态:两个来自 A 和 B 的复制,以及一个来自 X 的新复制。

然后删除额外的复制器数据库

$ curl -X DELETE http://user:pass@localhost:5984/another%2F_replicator/
{"ok":true}

此操作后,从服务器 X 拉取的复制将停止,而 _replicator 数据库中的复制(从服务器 A 和 B 拉取)将继续。

2.2.10. 公平份额作业调度

当使用多个 _replicator 数据库时,如果任何节点上的作业总数大于 max_jobs,则复制作业将被调度,以便每个 _replicator 数据库默认情况下都有平等的机会运行其作业。

这是通过为每个 _replicator 数据库分配一定数量的“份额”,然后自动调整运行作业的比例以匹配每个数据库的份额比例来实现的。默认情况下,每个 _replicator 数据库分配 100 个份额。可以在 [replicator.shares] 配置部分中更改每个单独的 _replicator 数据库的份额分配。

公平份额行为可能更容易用一组示例来描述。每个示例都假设 max_jobs = 500 的默认值,以及两个复制器数据库:_replicatoranother/_replicator

示例 1:如果 _replicator 有 1000 个作业,而 another/_replicator 有 10 个作业,则调度器将从 _replicator 运行大约 490 个作业,从 another/_replicator 运行 10 个作业。

示例 2:如果 _replicator 有 200 个作业,而 another/_replicator 也有 200 个作业,那么所有 400 个作业都将被执行,因为所有作业的总和少于 max_jobs 限制。

示例 3:如果两个复制器数据库各拥有 1000 个作业,调度程序将平均从每个数据库运行大约 250 个作业。

示例 4:如果两个复制器数据库各拥有 1000 个作业,但 _replicator 被分配了 400 个份额,那么调度程序平均将从 _replicator 运行大约 400 个作业,从 _another/replicator 运行大约 100 个作业。

示例中描述的比例是近似的,可能会略有波动,并且可能需要从几十分钟到一个小时的时间才能收敛。

2.2.11. 复制复制器数据库

假设您在服务器 C 上有一个复制器数据库,其中包含以下两个拉取复制文档

{
     "_id": "rep_from_A",
     "source":  "http://aserver.com:5984/foo",
     "target":  "http://user:pass@localhost:5984/foo_a",
     "continuous":  true
}
{
     "_id": "rep_from_B",
     "source":  "http://bserver.com:5984/foo",
     "target":  "http://user:pass@localhost:5984/foo_b",
     "continuous":  true
}

现在您希望在服务器 D 上进行相同的拉取复制,也就是说,您希望服务器 D 从服务器 A 和 B 进行拉取复制。您有两个选择

  • 显式地将两个文档添加到服务器 D 的复制器数据库中

  • 将服务器 C 的复制器数据库复制到服务器 D 的复制器数据库中

两种方法都能实现完全相同的结果。

2.2.12. 委托

复制文档可以具有自定义的 user_ctx 属性。此属性定义了复制运行的用户上下文。对于旧的触发复制方式(向 /_replicate/ 发送 POST 请求),不需要此属性。这是因为在复制过程中,有关已认证用户的的信息是 readily available 的,而在这种情况下,该信息不是持久化的。现在,使用复制器数据库,问题在于,有关哪个用户启动特定复制的信息仅在写入复制文档时存在。但是,复制文档中的信息和复制本身是持久的。此实现细节意味着,对于非管理员用户,必须在复制文档中定义一个包含用户名称及其角色子集的 user_ctx 属性。这是由复制器数据库默认设计文档中存在的文档更新验证函数强制执行的。验证函数还确保非管理员用户无法将用户上下文 name 属性的值设置为除他们自己的用户名以外的任何其他值。相同的原则适用于角色。

对于管理员,user_ctx 属性是可选的,如果它不存在,则默认为一个名称为 null 且角色列表为空的用户上下文,这意味着设计文档不会写入本地目标。如果需要将设计文档写入本地目标,则用户上下文的角色列表中必须包含 _admin 角色。

此外,对于管理员,user_ctx 属性可用于代表其他用户触发复制。这是将传递给本地目标数据库文档验证函数的用户上下文。

注意

user_ctx 属性仅对本地端点有效。

示例委托复制文档

{
    "_id": "my_rep",
    "source":  "http://bserver.com:5984/foo",
    "target":  "http://user:pass@localhost:5984/bar",
    "continuous":  true,
    "user_ctx": {
        "name": "joe",
        "roles": ["erlanger", "researcher"]
    }
}

如前所述,user_ctx 属性对于管理员是可选的,而对于普通(非管理员)用户是必需的。当 user_ctx 的 roles 属性不存在时,它默认为空列表 []

2.2.13. 选择器对象

在复制文档中包含选择器对象,可以使您使用查询表达式来确定是否应将文档包含在复制中。

选择器指定文档中的字段,并提供一个表达式,使用字段内容或其他数据进行评估。如果表达式解析为 true,则复制该文档。

选择器对象必须

  • 结构为有效的 JSON。

  • 包含有效的查询表达式。

选择器的语法与用于 selectorsyntax_find 相同。

使用选择器比使用 JavaScript 过滤器函数效率高得多,并且是仅对文档属性进行过滤时的推荐选项。

2.2.14. 指定用户名和密码

有多种方法可以为复制端点指定用户名和密码

  • {"auth": {"basic": ...}} 对象中

    3.2.0 版本新增功能。

    {
        "target": {
            "url": "http://someurl.com/mydb",
            "auth": {
                "basic": {
                    "username": "$username",
                    "password": "$password"
                 }
            }
        },
        ...
    }
    

    这是首选格式,因为它允许在用户名和密码字段中包含 @: 等字符。

  • 在端点 URL 的 userinfo 部分。这允许更紧凑的端点表示,但是,它会阻止在用户名或密码中使用 @: 等字符

    {
        "target":  "http://user:pass@localhost:5984/bar"
        ...
    }
    

    根据 RFC3986,在 URL 的 userinfo 部分指定凭据已过时。CouchDB 仍然支持这种指定凭据的方式,并且还没有确定将删除支持的目标版本。

  • "Authorization: Basic $b64encoded_username_and_password" 标头中

    {
        "target": {
            "url": "http://someurl.com/mydb",
                "headers": {
                    "Authorization": "Basic dXNlcjpwYXNz"
                }
            },
        ...
    }
    

    此方法的缺点是需要经过 base64 编码的额外步骤。此外,它可能会让人误以为它加密或隐藏了凭据,因此可能会鼓励无意中共享和泄露凭据。

当凭据以多种形式提供时,它们将按以下顺序选择

  • "auth": {"basic": {...}} 对象

  • URL userinfo

  • "Authorization: Basic ..." 标头。

首先,检查 auth 对象,如果其中定义了凭据,则使用它们。如果它们不存在,则检查 URL userinfo。如果在那里找到凭据,则使用这些凭据,否则使用基本身份验证标头。

2.2.15. 仅复制获胜修订版

使用 winning_revs_only: true 选项仅复制“获胜”文档修订版。这些是默认情况下由 GET db/doc API 端点返回的修订版,或者以默认参数出现在 _changes 提要中。

POST http://couchdb:5984/_replicate HTTP/1.1
Accept: application/json
Content-Type: application/json

{
    "winning_revs_only" : true
    "source" : "http://source:5984/recipes",
    "target" : "http://target:5984/recipes",
}

使用此模式进行复制会丢弃冲突的修订版,因此它可能是通过复制来消除冲突的一种方法。

winning_revs_only: true 复制生成的复制 ID 和检查点 ID 将不同于默认情况下生成的 ID,因此可以先复制获胜的修订版,然后稍后使用常规复制作业“回填”其余的修订版。

winning_revs_only: true 选项可以与过滤器或其他选项(如 continuous: truecreate_target: true)结合使用。