{
    "componentChunkName": "component---src-templates-blog-blog-detail-tsx",
    "path": "/blog/tidb-binlog-source-code-reading-7",
    "result": {"pageContext":{"blog":{"id":"Blogs_25","title":"TiDB Binlog 源码阅读系列文章（七）Drainer server 介绍","tags":["TiDB Binlog 源码阅读","社区"],"category":{"name":"产品技术解读"},"summary":"本文介绍了 Drainer server 的实现。","body":"前面文章介绍了 Pump server，接下来我们来介绍 Drainer server 的实现，Drainer server 的主要作用是从各个 Pump server 获取 binlog，按 commit timestamp 归并排序后解析 binlog 同步到不同的目标系统，对应的源码主要集中在 TiDB Binlog 仓库的 [drainer/](https://github.com/pingcap/tidb-binlog/tree/v3.0.7/drainer) 目录下。\n\n## 启动 Drainer Server\n\nDrainer server 的启动逻辑主要实现在两个函数中：[NewServer](https://github.com/pingcap/tidb-binlog/blob/v3.0.7/drainer/server.go#L88) 和 [(*Server).Start()](https://github.com/pingcap/tidb-binlog/blob/v3.0.7/drainer/server.go#L250) 。\n\n`NewServer` 根据传入的配置项创建 Server 实例，初始化 Server 运行所需的字段。其中重要字段的说明如下：\n\n1.  metrics: [MetricClient](https://github.com/pingcap/tidb-binlog/blob/v3.0.7/pkg/util/p8s.go#L36)，用于定时向 Prometheus Pushgateway 推送 drainer 运行中的各项参数指标。\n\n2.  cp: [checkpoint](https://github.com/pingcap/tidb-binlog/blob/v3.0.7/drainer/checkpoint/checkpoint.go#L29)，用于保存 drainer 已经成功输出到目标系统的 binlog 的 commit timestamp。drainer 在重启时会从 checkpoint 记录的 commit timestamp 开始同步 binlog。\n\n3.  collector: [collector](https://github.com/pingcap/tidb-binlog/blob/v3.0.7/drainer/collector.go#L50)，用于收集全部 binlog 数据并按照 commit timestamp 递增的顺序进行排序。同时 collector 也负责实时维护 pump 集群的状态信息。\n\n4.  syncer: [syncer](https://github.com/pingcap/tidb-binlog/blob/v3.0.7/drainer/syncer.go#L39)，用于将排好序的 binlog 输出到目标系统 (MySQL，Kafka...) ，同时更新同步成功的 binlog 的 commit timestamp 到 checkpoint。\n\nServer 初始化以后，就可以用 `(*Server).Start` 启动服务，启动的逻辑包含：\n\n1.  初始化 `heartbeat` 协程定时上报心跳信息到 etcd （内嵌在 PD 中）。\n\n2.  调用 `collector.Start()` 驱动 `Collector` 处理单元。\n\n3.  调用 `syncer.Start()` 驱动 `Syncer` 处理单元。\n\n    ```go\n    errc := s.heartbeat(s.ctx)\n    go func() {\n        for err := range errc {\n            log.Error(\"send heart failed\", zap.Error(err))\n        }\n    }()\n\n    s.tg.GoNoPanic(\"collect\", func() {\n        defer func() { go s.Close() }()\n        s.collector.Start(s.ctx)\n    })\n\n    if s.metrics != nil {\n        s.tg.GoNoPanic(\"metrics\", func() {\n    ```\n\n后续的章节中，我们会详细介绍 Checkpoint、Collector 与 Syncer。\n\n## Checkpoint\n\nCheckpoint 代码在 [/drainer/checkpoint](https://github.com/pingcap/tidb-binlog/tree/v3.0.7/drainer/checkpoint) 下。\n\n首先看下 [接口定义](https://github.com/pingcap/tidb-binlog/blob/v3.0.7/drainer/checkpoint/checkpoint.go#L29)：\n\n```go\n// When syncer restarts, we should reload meta info to guarantee continuous transmission.\ntype CheckPoint interface {\n    // Load loads checkpoint information.\n    Load() error\n\n    // Save saves checkpoint information.\n    Save(int64, int64) error\n\n    // TS get the saved commit ts.\n    TS() int64\n\n    // Close closes the CheckPoint and release resources after closed other methods should not be called again.\n    Close() error\n}\n```\n\ndrainer 支持把 checkpoint 保存到不同类型的存储介质中，目前支持 mysql 和 file 两种类型，例如 mysql 类型的实现代码在 [mysql.go](https://github.com/pingcap/tidb-binlog/blob/v3.0.7/drainer/checkpoint/mysql.go) 。如果用户没有指定 checkpoit 的存储类型，drainer 会根据目标系统的类型自动选择对应的 checkpoint 存储类型。\n\n当目标系统是 mysql/tidb，drainer 默认会保存 checkpoint 到 `tidb_binlog.checkpoint` 表中：\n\n```shell\nmysql> select * from tidb_binlog.checkpoint;\n+---------------------+---------------------------------------------+\n| clusterID           | checkPoint                                  |\n+---------------------+---------------------------------------------+\n| 6766844929645682862 | {\"commitTS\":413015447777050625,\"ts-map\":{}} |\n+---------------------+---------------------------------------------+\n1 row in set (0.00 sec)\n```\n\ncommitTS 表示这个时间戳之前的数据都已经同步到目标系统了。ts-map 是用来做 [TiDB 主从集群的数据校验](https://pingcap.com/docs-cn/stable/reference/tools/sync-diff-inspector/tidb-diff/) 而保存的上下游 snapshot 对应关系的时间戳。\n\n下面看看 MysqlCheckpoint 主要方法的实现。\n\n```go\n// Load implements CheckPoint.Load interface\nfunc (sp *MysqlCheckPoint) Load() error {\n    sp.Lock()\n    defer sp.Unlock()\n\n    if sp.closed {\n        return errors.Trace(ErrCheckPointClosed)\n    }\n\n    defer func() {\n        if sp.CommitTS == 0 {\n            sp.CommitTS = sp.initialCommitTS\n        }\n    }()\n\n    var str string\n    selectSQL := genSelectSQL(sp)\n    err := sp.db.QueryRow(selectSQL).Scan(&str)\n    switch {\n    case err == sql.ErrNoRows:\n        sp.CommitTS = sp.initialCommitTS\n        return nil\n    case err != nil:\n        return errors.Annotatef(err, \"QueryRow failed, sql: %s\", selectSQL)\n    }\n\n    if err := json.Unmarshal([]byte(str), sp); err != nil {\n        return errors.Trace(err)\n    }\n\n    return nil\n}\n```\n\nLoad 方法从数据库中读取 checkpoint 信息。需要注意的是，如果 drainer 读取不到对应的 checkpoint，会使用 drainer 配置的 `initial-commit-ts` 做为启动的开始同步点。\n\n```go\n// Save implements checkpoint.Save interface\nfunc (sp *MysqlCheckPoint) Save(ts, slaveTS int64) error {\n    sp.Lock()\n    defer sp.Unlock()\n\n    if sp.closed {\n        return errors.Trace(ErrCheckPointClosed)\n    }\n\n    sp.CommitTS = ts\n\n    if slaveTS > 0 {\n        sp.TsMap[\"master-ts\"] = ts\n        sp.TsMap[\"slave-ts\"] = slaveTS\n    }\n\n    b, err := json.Marshal(sp)\n    if err != nil {\n        return errors.Annotate(err, \"json marshal failed\")\n    }\n\n    sql := genReplaceSQL(sp, string(b))\n    _, err = sp.db.Exec(sql)\n    if err != nil {\n        return errors.Annotatef(err, \"query sql failed: %s\", sql)\n    }\n\n    return nil\n}\n```\n\nSave 方法构造对应 SQL 将 checkpoint 写入到目标数据库中。\n\n## Collector\n\nCollector 负责获取全部 binlog 信息后，按序传给 Syncer 处理单元。我们先看下 Start 方法：\n\n```go\n// Start run a loop of collecting binlog from pumps online\nfunc (c *Collector) Start(ctx context.Context) {\n    var wg sync.WaitGroup\n    wg.Add(1)\n    go func() {\n        c.publishBinlogs(ctx)\n        wg.Done()\n    }()\n\n    c.keepUpdatingStatus(ctx, c.updateStatus)\n\n    for _, p := range c.pumps {\n        p.Close()\n    }\n    if err := c.reg.Close(); err != nil {\n        log.Error(err.Error())\n    }\n    c.merger.Close()\n\n    wg.Wait()\n}\n```\n\n这里只需要关注 publishBinlogs 和 keepUpdatingStatus 两个方法。\n\n```go\nfunc (c *Collector) publishBinlogs(ctx context.Context) {\n    defer log.Info(\"publishBinlogs quit\")\n\n    for {\n        select {\n        case <-ctx.Done():\n            return\n        case mergeItem, ok := <-c.merger.Output():\n            if !ok {\n                return\n            }\n            item := mergeItem.(*binlogItem)\n            if err := c.syncBinlog(item); err != nil {\n                c.reportErr(ctx, err)\n                return\n            }\n        }\n    }\n}\n```\n\npublishBinlogs 调用 [merger](https://github.com/pingcap/tidb-binlog/blob/v3.0.7/drainer/merge.go) 模块从所有 pump 读取 binlog，并且按照 binlog 的 commit timestamp 进行归并排序，最后通过调用 `syncBinlog` 输出 binlog 到  Syncer 处理单元。\n\n```go\nfunc (c *Collector) keepUpdatingStatus(ctx context.Context, fUpdate func(context.Context) error) {\n    // add all the pump to merger\n    c.merger.Stop()\n    fUpdate(ctx)\n    c.merger.Continue()\n\n    // update status when had pump notify or reach wait time\n    for {\n        select {\n        case <-ctx.Done():\n            return\n        case nr := <-c.notifyChan:\n            nr.err = fUpdate(ctx)\n            nr.wg.Done()\n        case <-time.After(c.interval):\n            if err := fUpdate(ctx); err != nil {\n                log.Error(\"Failed to update collector status\", zap.Error(err))\n            }\n        case err := <-c.errCh:\n            log.Error(\"collector meets error\", zap.Error(err))\n            return\n        }\n    }\n}\n```\n\nkeepUpdatingStatus 通过下面两种方式从 etcd 获取 pump 集群的最新状态：\n\n1.  定时器定时触发。\n\n2.  notifyChan 触发。这是一个必须要提一下的处理逻辑：当一个 pump 需要加入 pump c 集群的时候，该 pump 会在启动时通知所有在线的 drainer，只有全部 drainer 都被通知都成功后，pump 方可对外提供服务。 这个设计的目的是，防止对应的 pump 的 binlog 数据没有及时加入 drainer 的排序过程，从而导致 binlog 数据同步缺失。\n\n## Syncer\n\nSyncer 代码位于 [drainer/syncer.go](https://github.com/pingcap/tidb-binlog/blob/v3.0.7/drainer/syncer.go)，是用来处理数据同步的关键模块。\n\n```go\ntype Syncer struct {\n    schema *Schema\n    cp     checkpoint.CheckPoint\n    cfg    *SyncerConfig\n    input  chan *binlogItem\n    filter *filter.Filter\n    // last time we successfully sync binlog item to downstream\n    lastSyncTime time.Time\n    dsyncer      dsync.Syncer\n    shutdown     chan struct{}\n    closed       chan struct{}\n}\n```\n\n在 Syncer 的结构定义中，我们关注下面三个对象：\n\n*   dsyncer 是真正同步数据到不同目标系统的执行器实现，我们会在后续章节具体介绍，接口定义如下：\n\n    ```go\n    // Syncer sync binlog item to downstream\n    type Syncer interface {\n        // Sync the binlog item to downstream\n        Sync(item *Item) error\n        // will be close if Close normally or meet error, call Error() to check it\n        Successes() <-chan *Item\n        // Return not nil if fail to sync data to downstream or nil if closed normally\n        Error() <-chan error\n        // Close the Syncer, no more item can be added by `Sync`\n        Close() error\n    }\n    ```\n\n*   schema 维护了当前同步位置点的全部 schema 信息，可以根据 ddl binlog 变更对应的 schema 信息。\n\n*   filter 负责对需要同步的 binlog 进行过滤。\n\nSyncer 运行入口在 [run](https://github.com/pingcap/tidb-binlog/blob/v3.0.7/drainer/syncer.go#L260) 方法，主要逻辑包含：\n\n1.  依次处理 Collector 处理单元推送过来的 binlog 数据。\n\n2.  如果是 DDL binlog，则更新维护的 schema 信息。\n\n3.  利用 filter 过滤不需要同步到下游的数据。\n\n4.  调用 drainer/sync/Syncer.Sync()  异步地将数据同步到目标系统。\n\n5.  处理数据同步结果返回。\n\n    a. 通过 Succsses() 感知已经成功同步到下游的 binlog 数据，保存其对应 commit timestamp 信息到 checkpoint。\n  \n    b. 通过 Error() 感知同步过程出现的错误，drainer 清理环境退出进程。\n\n## 小结\n\n本文介绍了 Drainer server 的主体结构，后续文章会具体介绍其如何同步数据到不同下游。\n\n> 点击查看更多 [TiDB Binlog 源码阅读系列文章](https://pingcap.com/zh/blog/?tag=TiDB%20Binlog%20%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB)","date":"2019-12-24","author":"黄佳豪","fillInMethod":"writeDirectly","customUrl":"tidb-binlog-source-code-reading-7","file":null,"relatedBlogs":[]}}},
    "staticQueryHashes": ["1327623483","1820662718","3081853212","3430003955","3649515864","4265596160","63159454"]}