This repo contains sample implementations of data validation for Spanner databases using Apache Beam. Since Spanner is a horizontally scalable database that can scale pretty much without limit, we need a framework for data validation that can handle this scale. Apache Beam provides just such a scalable framework. It supports multiple languages - Java, Python, Go, etc..., but we use Java in this repo. While you can run Apache Beam applications locally on your machine or individual instances in the cloud, in order to realize the full power of the framework and scale across multiple instances, you have to run your application/job on a service like Dataflow. I describe how to do this in the section titled Running validation using Dataflow below.
Having said all of the above, I should mention that you don't have to know much about Apache Beam or Dataflow or be a Java developer to use this tool. Simply follow the guidance below to get your own validation against Spanner up and running! You can jump to the Installation section and pick up from there if you'd prefer to skip the following section on the design of the tool.
First, here's a high level architecture diagram of how our data validator will work:
We use Apache Beam running on Dataflow to perform our data validation at scale and then send the reports to BigQuery. The reporting itself is pretty straightforward. Here's a sample report:
The above sample report provides a window into the design of this data validator. Let's start with the notion of partitioning.
Partitioning is key to performing data validation at scale. The approach is simple: we partition data into chunks and for each chunk, issue the same query at the source and target. Here’s a sample query (for both source and target)
select * from customers where customerNumber > ? and customerNumber <= ?
And thanks to partitioning, this query will be run in parallel across Dataflow workers (units of parallelism to be more accurate). The results of the query will be used to perform comparisons and results of the comparisons in turn will be sent to BigQuery (see reporting section below).
- The column name used in the partition query should be the same in source and target and should be an indexed column with very high cardinality.
SELECT Xrelies on column ordering and both source and target have the same column. It is fairly easy to get a SELECT clause as input to the tool where the user ensures the same order and also casting to ensure the same outcome between source and target even if the column type is different.
- Access to a GCP account
- Access to Spanner (Since you'll be performing validation against Spanner :))
- Access to BigQuery
- Access to Dataflow
- Ensure that you have JRE 11 installed on your machine.
- Ensure that you have a MySQL or PostgreSQL source database (or databases in case of sharding) available to run the tool against.
- Create or identify a pre-existing dataset in BQ that you can use for reporting purposes. The validation tool writes the results of the validation to this BigQuery dataset. The validation tool will create a table called ‘SpannerDVTResults’ in this dataset into which it will write the results.
- Request the permissions listed in the section below
- User should have permissions to kick off Dataflow jobs
- Default Dataflow service agent should permissions to read/write from buckets: See link
- Dataflow worker account (default is compute engine service account) should have
- Ability to create tables in specified dataset:
roles/bigquery.dataEditor - Ability to write to the specified dataset:
roles/bigquery.dataEditor - Read from Spanner:
roles/spanner.databaseReader - Read from JDBC (MySQL or Postgresql)
- If using Secret Manager to supply password, the dataflow worker service account should have
roles/secretManager.secretAccessorandroles/secretManager.secretViewer
- Ability to create tables in specified dataset:
spanner-data-validator uses git-lfs to store a JAR
that is required to run custom transformations. Users will need to set up and
use git-lfs to download the JAR file while cloning this repository. Follow
the instructions below -
- Clone the repository.
- Install
git-lfsusing instructions here. - From the
spanner-data-validatorgit repository, rungit lfs pull.
NOTE: Ensure that you're in the spanner-data-validator-java/ folder before running the following command
mvn validateNOTE: mvn validate installs required dependencies to your maven cache for the
build process to work, so don't forget to run this command!. It only needs
to be run once to setup the maven cache successfully.
mvn clean package -Pdirect-and-dataflow -DskipTests
NOTE: In the examples below, we’re using a pre-built jar so you don’t have to run mvn (Maven) commands.
java -jar target/spanner-data-validator-java-bundled-0.1.jar \
--protocol=postgresql \
--server=localhost \
--port=3306 \
--username=<your db username> \
--DBPassFromSecret=<secretId> \
--sourceDB=member_events_db \
--tableSpecJson=src/main/resources/json/member-events-only-with-coverage-spec.json \
--tempLocation=gs://bigdata-stuff/df1 \
--projectId=kt-shared-project \
--instanceId=dvt-test1 \
--spannerDatabaseId=dvt-test1-db \
--BQDatasetName=SpannerDVTDataset \
--streaming=false \
--runner=DirectRunner
Parameters
protocol:postgresqlormysqlserver: DB hostport: DB portusername: DB usernameDBPassFromSecret: GCP secret manager entry containing DB passwordsourceDB: Source DBtableSpecJson: Table spec json file. See sdfkj for more infotempLocation: GCS location for holding staging infoprojectId: This is project id used for Spanner, BQ and Secret ManagerinstanceId: Spanner instance idspannerDatabaseId: Spanner database idBQDatasetName: BQ dataset where report will be writtenstreaming: False here because this will be a batch jobrunner: In this case, Direct Runner, to reflect a local run
java -jar target/spanner-data-validator-java-bundled-0.1.jar \
--project=kt-shared-project \
--network=default \
--subnetwork=https://www.googleapis.com/compute/v1/projects/kt-shared-project/regions/us-central1/subnetworks/default \
--numWorkers=10 \
--region=us-central1 \
--protocol=postgresql \
--server=10.128.15.212 \
--port=3306 \
--username=<your mysql db user> \
--DBPassFromSecret=<secretId> \
--sourceDB=member_events_db \
--tableSpecJson=src/main/resources/json/member-events-only-with-coverage-spec.json \
--tempLocation=gs://bigdata-stuff/df1 \
--projectId=kt-shared-project \
--instanceId=dvt-test1 \
--spannerDatabaseId=dvt-test1-db \
--BQDatasetName=SpannerDVTDataset \
--conflictingRecordsBQTableName=<Name of table to write unmatched records>\
--streaming=false \
--autoscalingAlgorithm=NONE \
--runner=DataflowRunner
Parameters
protocol:postgresqlormysqlserver: DB hostport: DB portusername: DB usernameDBPassFromSecret: GCP secret manager entry containing DB passwordsourceDB: Source DBtableSpecJson: Table spec json file. See sdfkj for more infotempLocation: GCS location for holding staging infoprojectId: This is project id used for Spanner, BQ and Secret ManagerinstanceId: Spanner instance idspannerDatabaseId: Spanner database idBQDatasetName: BQ dataset where report will be writtenstreaming: False here because this will be a batch jobrunner: In this case, Direct Runner, to reflect a local run
You specify tables/queries for validation using the TableSpec json files.
[
{
"tableName": "DataProductRecords",
"sourceQuery": "select * from \"data-products\".data_product_records where id >= uuid(?) and id <= uuid(?)",
"destQuery": "select * FROM data_product_records WHERE id >= $1 AND id <= $2",
"rangeFieldIndex": "0",
"rangeFieldType": "UUID",
"rangeStart": "00000000-0000-0000-0000-000000000000",
"rangeEnd": "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF",
"partitionCount": "100",
"rangeCoverage": "0.00000000002"
},
{
"tableName": "DataProductMetadata",
"sourceQuery": "select * from \"data-products\".data_product_metadata where data_product_id >= uuid(?) and data_product_id <= uuid(?)",
"destQuery": "select key, value, data_product_id FROM data_product_metadata WHERE data_product_id >= $1 AND data_product_id <= $2",
"rangeFieldIndex": "2",
"rangeFieldType": "UUID",
"rangeStart": "00000000-0000-0000-0000-000000000000",
"rangeEnd": "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF",
"partitionCount": "100",
"rangeCoverage": "0.00000000001"
}
]
Custom transformations allow the user to inject a JAR which implements the spanner-migrations-sdk interface to augment the validation logic. This functionality is useful to pass the same custom transformation JAR what might have been used in other data migration tools, such as source-to-spanner dataflow template or datastream-to-spanner dataflow template.
When custom transformations are enabled (that is, the relevant flags are configured), all non-common columns on the spanner that are part of the session file will be assumed under custom transformation.
- Build the JAR for spanner-data-validator using instructions here.
- Build the JAR for your custom transformation as per the instructions here.
- Using a session file is mandatory for using custom transformations.
- The column under custom transformation should be part of the supplied session file.
Custom transformation support requires configuring identical parameters to bulk,
live and reverse replication dataflow jobs. This includes
transformationJarPath,
transformationClassName and transformationCustomParameters. A sample command
is
given below -
java -jar target/spanner-data-validator-java-bundled-0.1.jar \
--project=sample-project \
--network=sample-vpc \
--subnetwork=https://www.googleapis.com/compute/v1/projects/span-cloud-testing/regions/us-central1/subnetworks/aks-test-vpc \
--numWorkers=10 \
--region=us-central1 \
--protocol=mysql \
--server=10.36.112.26 \
--port=3306 \
--username=username \
--password=pwd \
--sourceDB=validation_test \
--sessionFileJson=src/main/resources/json/custom_transform.json \
--tempLocation=gs://sample-bucket/validation-testing \
--projectId=sample-project \
--instanceId=sample-instance \
--spannerDatabaseId=sample-database \
--BQDatasetName=SpannerDVTDataset \
--transformationJarPath=gs://sample-bucket/test/smt_util-0.0.2-SNAPSHOT.jar \
--transformationClassName=com.example.test.smtutil.spanner.Transformer \
--transformationCustomParameters=table.default.abc=colName1,table.list=colName2 \
--runner=DataflowRunner