TiDB中的TSO

TiDB中的TSO

前言:分布式数据库要实现全局一致性快照,需要解决不同节点之间时钟一致的问题。工业界目前有3种解决方案:

  1. 全局集中式授时服务,对网络要求比较高,不能跨地域,理论上可以做到外部一致性;
  2. 混合逻辑时钟(HLC),可以保证同一个进程内部事件的时钟顺序,但是解决不了系统外事件发生的逻辑前后顺序与物理时间前后顺序的一致性,因此做不到Linearizability,也就做不到外部一致性;
  3. Google的物理时钟Turetime API,可以做到外部一致性,同时能做到全球化部署。

TiDB作为国内开源分布式数据库的优秀代表,采用集中式的服务TSO来获取全局一致的版本号,并保证事务版本号的单调递增。TSO模块位于TiDB 全局中心总控节点PD中,PD通过集成了 etcd ,保证了持久化数据的强一致性并且可以做到 auto failover,解决了集中式服务带来的单点故障问题。

来看下TSO模块的实现

定义两个时间间隔,具体作用后面会提到。

const (
	// update timestamp every updateTimestampStep.
	updateTimestampStep  = 50 * time.Millisecond
	updateTimestampGuard = time.Millisecond
	maxLogical           = int64(1 << 18)
)

TSO的全局版本号由 physical time + logical time 两个部分组成。

type atomicObject struct {
	physical time.Time
	logical  int64
}

loadTimestamp()从 etcd中获取上一次保存的时间戳,如果获取失败或者获取的值为空,则返回当前时间戳。

func (s *Server) getTimestampPath() string {
	return path.Join(s.rootPath, "timestamp")
}
func (s *Server) loadTimestamp() (time.Time, error) {
	data, err := getValue(s.client, s.getTimestampPath())
	if err != nil {
		return zeroTime, err
	}
	if len(data) == 0 {
		return zeroTime, nil
	}
	return parseTimestamp(data)
}

saveTimestamp()将时间戳持久化到etcd中保存,并设置lastSavedTime的值。

func (s *Server) saveTimestamp(ts time.Time) error {
	data := uint64ToBytes(uint64(ts.UnixNano()))
	key := s.getTimestampPath()

	resp, err := s.leaderTxn().Then(clientv3.OpPut(key, string(data))).Commit()
	if err != nil {
		return errors.WithStack(err)
	}
	if !resp.Succeeded {
		return errors.New("save timestamp failed, maybe we lost leader")
	}

	s.lastSavedTime = ts

	return nil
}

PD第一次启动或者重新选主后通过调用syncTimestamp()来做两件事:

1、需要设置当前全局版本号的physical time部分;

2、将当前physical time加上一个时间间隔(updateTimestampGuard)默认1ms做为预分配窗口时间戳,通过调用saveTimestamp()持久化到etcd。

注意:持久化的并不是当前Physical time。

func (s *Server) syncTimestamp() error {
	tsoCounter.WithLabelValues("sync").Inc()
	last, err := s.loadTimestamp()
	if err != nil {
		return err
	}

	next := time.Now()
//如果当前系统时间戳减去保存的etcd时间戳小于' updateTimestampGuard ',说明当前系统时间可能有延时,时间戳分配需要从etcd中上一次保存的时间戳+updateTimestampGuard开始,从而保证TSO的单调递增。
	if subTimeByWallClock(next, last) < updateTimestampGuard {
		log.Error("system time may be incorrect", zap.Time("last", last), zap.Time("next", next))
		next = last.Add(updateTimestampGuard)
	}
//将physical time+TsoSaveInterval持久化到etcd。
	save := next.Add(s.cfg.TsoSaveInterval.Duration)
	if err = s.saveTimestamp(save); err != nil {
		return err
	}

	tsoCounter.WithLabelValues("sync_ok").Inc()
	log.Info("sync and save timestamp", zap.Time("last", last), zap.Time("save", save), zap.Time("next", next))
//设置当前Physical time
	current := &atomicObject{
		physical: next,
	}
	s.ts.Store(current)

	return nil
}


PD在正常工作期间每隔updateTimestampStep (默认50ms)会定期调用updateTimestamp()更新全局版本号。updateTimestamp()主要做两件事:

1、当logical time即将用完时,需要增加当前的physical time;

2、如果内存中的时间戳窗口快用完了,则需要更新physical time, 并更新etcd中持久化的时间戳。内存窗口中的时间戳宕机后可能会丢失,为了保证时间戳是严格单调递增的只要宕机恢复后跳过这个窗口进去下个窗口就可以。

func (s *Server) updateTimestamp() error {
	prev := s.ts.Load().(*atomicObject)
	now := time.Now()


	tsoCounter.WithLabelValues("save").Inc()

	jetLag := subTimeByWallClock(now, prev.physical)
	if jetLag > 3*updateTimestampStep {
		log.Warn("clock offset", zap.Duration("jet-lag", jetLag), zap.Time("prev-physical", prev.physical), zap.Time("now", now))
		tsoCounter.WithLabelValues("slow_save").Inc()
	}
//如果当前系统时间小于etcd中保存的时间戳,并且不在上个窗口期中,说明系统时间跑的慢了。
	if jetLag < 0 {
		tsoCounter.WithLabelValues("system_time_slow").Inc()
	}

	var next time.Time
	prevLogical := atomic.LoadInt64(&prev.logical)
	// 如果当前系统时间大于etcd中保存的时间戳,并且内存窗口过期了,那么下次分配的physical time取自当前系统时间
	if jetLag > updateTimestampGuard {
		next = now
	} else if prevLogical > maxLogical/2 {
		//如果内存时间窗口未过期,并且logical time已经用了超过1半,将增加physical time
		log.Warn("the logical time may be not enough", zap.Int64("prev-logical", prevLogical))
		next = prev.physical.Add(time.Millisecond)
	} else {
		tsoCounter.WithLabelValues("skip_save").Inc()
		return nil
	}

	// 将 physical time+TsoSaveInterval 作为窗口时间戳持久化到etcd
	if subTimeByWallClock(s.lastSavedTime, next) <= updateTimestampGuard {
		save := next.Add(s.cfg.TsoSaveInterval.Duration)
		if err := s.saveTimestamp(save); err != nil {
			return err
		}
	}
//上次时间窗口过期了或者logical time 快用完了,需要重置logical time
	current := &atomicObject{
		physical: next,
		logical:  0,
	}

	s.ts.Store(current)
	metadataGauge.WithLabelValues("tso").Set(float64(next.Unix()))

	return nil
}


事务请求全局版本号的时候通过gRPC调用getRespTS(),PD将通过在内存中保留的可分配的时间窗口,直接在内存里面计算出TSO 并返回,避免每次分配TSO都需要持久化到etcd带来的开销。如果NTP时钟出现比较大的误差导致updateTimestamp()执行不及时,可能会出现logical time溢出,这个时候需要阻塞请求待updateTimestamp()执行后恢复。

const maxRetryCount = 100
func (s *Server) getRespTS(count uint32) (pdpb.Timestamp, error) {
	var resp pdpb.Timestamp

	if count == 0 {
		return resp, errors.New("tso count should be positive")
	}

	for i := 0; i < maxRetryCount; i++ {
		current, ok := s.ts.Load().(*atomicObject)
		if !ok || current.physical == zeroTime {
			log.Error("we haven't synced timestamp ok, wait and retry", zap.Int("retry-count", i))
			time.Sleep(200 * time.Millisecond)
			continue
		}
                //Physical time 是当前 unix time 的毫秒时间
		resp.Physical = current.physical.UnixNano() / int64(time.Millisecond)
                //logical time 则是一个最大 int64(1 << 18) 的计数器
		resp.Logical = atomic.AddInt64(&current.logical, int64(count))
                //如果logical time 溢出了,需要休眠updateTimestampStep时间,
                //等待调用updateTimestamp()重置logical time
		if resp.Logical >= maxLogical {
			log.Error("logical part outside of max logical interval, please check ntp time",
				zap.Reflect("response", resp),
				zap.Int("retry-count", i))
			tsoCounter.WithLabelValues("logical_overflow").Inc()
			time.Sleep(updateTimestampStep)
			continue
		}
		return resp, nil
	}
	return resp, errors.New("can not get timestamp")
}

总结:PD集群工作期间主节点宕机重启或者切主,这时候如果节点间的物理时钟不一致,可能会造成全局版本号重复的问题。物理节点的时钟同步可以通过NTP来解决,不过众所周知NTP在网络延迟等情况下也会存在误差。为了保证全局版本号单调递增,tidb采用了定期持久化时间戳到etcd的方式(这里和innodb事务id分配有区别,innodb采取的是定量分配才持久化一次,下次启动直接跳过定量的部分),不过这种方式也带来了额外的性能开销,TSO的性能会受限于时间戳持久化到etcd的频率,etcd日志又需要写到多副本中,额外的网络开销不可避免。TSO中通过内存时间戳窗口的优化使性能问题得到很大缓解。

注意:本文中个人观点不代表官方

参考:

pingcap/pdgithub.com图标

编辑于 2019-03-21