vitess.io/vitess@v0.16.2/go/vt/vttablet/tabletserver/vstreamer/snapshot_conn.go (about)

     1  /*
     2  Copyright 2020 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package vstreamer
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"sync/atomic"
    23  
    24  	"github.com/spf13/pflag"
    25  
    26  	"vitess.io/vitess/go/mysql"
    27  	"vitess.io/vitess/go/vt/dbconfigs"
    28  	"vitess.io/vitess/go/vt/log"
    29  	"vitess.io/vitess/go/vt/servenv"
    30  	"vitess.io/vitess/go/vt/sqlparser"
    31  )
    32  
    33  // If the current binary log is greater than this byte size, we
    34  // will attempt to rotate it before starting a GTID snapshot
    35  // based stream.
    36  // Default is 64MiB.
    37  var binlogRotationThreshold = int64(64 * 1024 * 1024) // 64MiB
    38  
    39  // snapshotConn is wrapper on mysql.Conn capable of
    40  // reading a table along with a GTID snapshot.
    41  type snapshotConn struct {
    42  	*mysql.Conn
    43  	cp dbconfigs.Connector
    44  }
    45  
    46  func init() {
    47  	servenv.OnParseFor("vtcombo", registerSnapshotConnFlags)
    48  	servenv.OnParseFor("vttablet", registerSnapshotConnFlags)
    49  }
    50  
    51  func registerSnapshotConnFlags(fs *pflag.FlagSet) {
    52  	fs.Int64Var(&binlogRotationThreshold, "vstream-binlog-rotation-threshold", binlogRotationThreshold, "Byte size at which a VStreamer will attempt to rotate the source's open binary log before starting a GTID snapshot based stream (e.g. a ResultStreamer or RowStreamer)")
    53  }
    54  
    55  func snapshotConnect(ctx context.Context, cp dbconfigs.Connector) (*snapshotConn, error) {
    56  	mconn, err := mysqlConnect(ctx, cp)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  	return &snapshotConn{
    61  		Conn: mconn,
    62  		cp:   cp,
    63  	}, nil
    64  }
    65  
    66  // startSnapshot starts a streaming query with a snapshot view of the specified table.
    67  // It returns the GTID set from the time when the snapshot was taken.
    68  func (conn *snapshotConn) streamWithSnapshot(ctx context.Context, table, query string) (gtid string, rotatedLog bool, err error) {
    69  	// Rotate the binary log if needed to limit the GTID auto positioning overhead.
    70  	// This may be needed as the currently open binary log (which can be up to 1G in
    71  	// size by default) will need to be scanned and empty events will be streamed for
    72  	// those GTIDs in the log that we are skipping. In total, this can add a lot of
    73  	// overhead on both the mysqld instance and the tablet.
    74  	// Rotating the log when it's above a certain size ensures that we are processing
    75  	// a relatively small binary log that will be minimal in size and GTID events.
    76  	// We only attempt to rotate it if the current log is of any significant size to
    77  	// avoid too many unecessary rotations.
    78  	if rotatedLog, err = conn.limitOpenBinlogSize(); err != nil {
    79  		// This is a best effort operation meant to lower overhead and improve performance.
    80  		// Thus it should not be required, nor cause the operation to fail.
    81  		log.Warningf("Failed in attempt to potentially flush binary logs in order to lessen overhead and improve performance of a VStream using query %q: %v",
    82  			query, err)
    83  	}
    84  
    85  	_, err = conn.ExecuteFetch("set session session_track_gtids = START_GTID", 1, false)
    86  	if err != nil {
    87  		// session_track_gtids = START_GTID unsupported or cannot execute. Resort to LOCK-based snapshot
    88  		gtid, err = conn.startSnapshot(ctx, table)
    89  	} else {
    90  		// session_track_gtids = START_GTID supported. Get a transaction with consistent GTID without LOCKing tables.
    91  		gtid, err = conn.startSnapshotWithConsistentGTID(ctx)
    92  	}
    93  	if err != nil {
    94  		return "", rotatedLog, err
    95  	}
    96  	if err := conn.ExecuteStreamFetch(query); err != nil {
    97  		return "", rotatedLog, err
    98  	}
    99  	return gtid, rotatedLog, nil
   100  }
   101  
   102  // snapshot performs the snapshotting.
   103  func (conn *snapshotConn) startSnapshot(ctx context.Context, table string) (gtid string, err error) {
   104  	lockConn, err := mysqlConnect(ctx, conn.cp)
   105  	if err != nil {
   106  		return "", err
   107  	}
   108  	// To be safe, always unlock tables, even if lock tables might fail.
   109  	defer func() {
   110  		_, err := lockConn.ExecuteFetch("unlock tables", 0, false)
   111  		if err != nil {
   112  			log.Warning("Unlock tables failed: %v", err)
   113  		} else {
   114  			log.Infof("Tables unlocked: %v", table)
   115  		}
   116  		lockConn.Close()
   117  	}()
   118  
   119  	tableName := sqlparser.String(sqlparser.NewIdentifierCS(table))
   120  
   121  	log.Infof("Locking table %s for copying", table)
   122  	if _, err := lockConn.ExecuteFetch(fmt.Sprintf("lock tables %s read", tableName), 1, false); err != nil {
   123  		log.Infof("Error locking table %s to read", tableName)
   124  		return "", err
   125  	}
   126  	mpos, err := lockConn.PrimaryPosition()
   127  	if err != nil {
   128  		return "", err
   129  	}
   130  
   131  	// Starting a transaction now will allow us to start the read later,
   132  	// which will happen after we release the lock on the table.
   133  	if _, err := conn.ExecuteFetch("set transaction isolation level repeatable read", 1, false); err != nil {
   134  		return "", err
   135  	}
   136  	if _, err := conn.ExecuteFetch("start transaction with consistent snapshot", 1, false); err != nil {
   137  		return "", err
   138  	}
   139  	if _, err := conn.ExecuteFetch("set @@session.time_zone = '+00:00'", 1, false); err != nil {
   140  		return "", err
   141  	}
   142  	return mysql.EncodePosition(mpos), nil
   143  }
   144  
   145  // startSnapshotWithConsistentGTID performs the snapshotting without locking tables. This assumes
   146  // session_track_gtids = START_GTID, which is a contribution to MySQL and is not in vanilla MySQL at the
   147  // time of this writing.
   148  func (conn *snapshotConn) startSnapshotWithConsistentGTID(ctx context.Context) (gtid string, err error) {
   149  	if _, err := conn.ExecuteFetch("set transaction isolation level repeatable read", 1, false); err != nil {
   150  		return "", err
   151  	}
   152  	result, err := conn.ExecuteFetch("start transaction with consistent snapshot", 1, false)
   153  	if err != nil {
   154  		return "", err
   155  	}
   156  	// The "session_track_gtids = START_GTID" patch is only applicable to MySQL56 GTID, which is
   157  	// why we hardcode the position as mysql.Mysql56FlavorID
   158  	mpos, err := mysql.ParsePosition(mysql.Mysql56FlavorID, result.SessionStateChanges)
   159  	if err != nil {
   160  		return "", err
   161  	}
   162  	if _, err := conn.ExecuteFetch("set @@session.time_zone = '+00:00'", 1, false); err != nil {
   163  		return "", err
   164  	}
   165  	return mysql.EncodePosition(mpos), nil
   166  }
   167  
   168  // Close rollsback any open transactions and closes the connection.
   169  func (conn *snapshotConn) Close() {
   170  	_, _ = conn.ExecuteFetch("rollback", 1, false)
   171  	conn.Conn.Close()
   172  }
   173  
   174  func mysqlConnect(ctx context.Context, cp dbconfigs.Connector) (*mysql.Conn, error) {
   175  	return cp.Connect(ctx)
   176  }
   177  
   178  // limitOpenBinlogSize will rotate the binary log if the current binary
   179  // log is greater than binlogRotationThreshold.
   180  func (conn *snapshotConn) limitOpenBinlogSize() (bool, error) {
   181  	rotatedLog := false
   182  	// Output: https://dev.mysql.com/doc/refman/en/show-binary-logs.html
   183  	res, err := conn.ExecuteFetch("SHOW BINARY LOGS", -1, false)
   184  	if err != nil {
   185  		return rotatedLog, err
   186  	}
   187  	if res == nil || len(res.Rows) == 0 {
   188  		return rotatedLog, fmt.Errorf("SHOW BINARY LOGS returned no rows")
   189  	}
   190  	// the current log will be the last one in the results
   191  	curLogIdx := len(res.Rows) - 1
   192  	curLogSize, err := res.Rows[curLogIdx][1].ToInt64()
   193  	if err != nil {
   194  		return rotatedLog, err
   195  	}
   196  	if curLogSize > atomic.LoadInt64(&binlogRotationThreshold) {
   197  		if _, err = conn.ExecuteFetch("FLUSH BINARY LOGS", 0, false); err != nil {
   198  			return rotatedLog, err
   199  		}
   200  		rotatedLog = true
   201  	}
   202  	return rotatedLog, nil
   203  }
   204  
   205  // GetBinlogRotationThreshold returns the current byte size at which a VStreamer
   206  // will attempt to rotate the binary log before starting a GTID snapshot based
   207  // stream (e.g. a ResultStreamer or RowStreamer).
   208  func GetBinlogRotationThreshold() int64 {
   209  	return atomic.LoadInt64(&binlogRotationThreshold)
   210  }
   211  
   212  // SetBinlogRotationThreshold sets the byte size at which a VStreamer will
   213  // attempt to rotate the binary log before starting a GTID snapshot based
   214  // stream (e.g. a ResultStreamer or RowStreamer).
   215  func SetBinlogRotationThreshold(threshold int64) {
   216  	atomic.StoreInt64(&binlogRotationThreshold, threshold)
   217  }