<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Data Capture Archives - OVHcloud Blog</title>
	<atom:link href="https://blog.ovhcloud.com/tag/data-capture/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.ovhcloud.com/tag/data-capture/</link>
	<description>Innovation for Freedom</description>
	<lastBuildDate>Wed, 31 Jul 2019 10:09:49 +0000</lastBuildDate>
	<language>en-GB</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://blog.ovhcloud.com/wp-content/uploads/2019/07/cropped-cropped-nouveau-logo-ovh-rebranding-32x32.gif</url>
	<title>Data Capture Archives - OVHcloud Blog</title>
	<link>https://blog.ovhcloud.com/tag/data-capture/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>The Unexpected Quest for Business Intelligence</title>
		<link>https://blog.ovhcloud.com/the-unexpected-quest-for-business-intelligence/</link>
		
		<dc:creator><![CDATA[Steven Le Roux]]></dc:creator>
		<pubDate>Thu, 28 Feb 2019 08:20:06 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Business Intelligence]]></category>
		<category><![CDATA[Data Capture]]></category>
		<guid isPermaLink="false">https://blog.ovh.com/fr/blog/?p=14522</guid>

					<description><![CDATA[Business Intelligence (BI) is the ability to collect substantial data from an information system to feed a Data Warehouse (DWH) or data lake. They usually provide a copy of the data that will be used for BI applications. Different strategies can be applied to feed a DWH. One such strategy is Change Data Capture (CDC), which is the ability to capture changing states from a database, and convert them to events that can be used for other purposes. Most databases are intended for OLTP purposes, and are well designed for this. Nonetheless, different use cases would require the same data with different access patterns. These use cases (big data, ETL, and stream processing, to name a few) mostly fall under the OLAP banner.

OVH, as a cloud provider, manages numerous databases, both for its customers and its own needs. Managing a database lifecycle always involves both keeping the infrastructure up to date, and remaining in synch with the development release cycle, to align the software with its database dependency. For example, an app might require MySQL 5.0, which could then be announced as EOL (End Of Life). In this case the app needs to be modified to support (let’s say) MySQL 5.5. We’re not reinventing the wheel here – this process has been managed by operations and dev teams for decades now.

This becomes trickier if you don’t have control over the application. For example, imagine a third party provides you with an application to ensure encrypted transactions. You have absolutely no control over this application, nor the associated database. Nonetheless, you still need the data from the database.

This blog post relates a similar example we encountered while building the OVH data lake, with the help of an in-house CDC development. This story takes place in early 2015, although I still think it’s worth sharing. &#x1f642;<img src="//blog.ovhcloud.com/wp-content/plugins/matomo/app/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fblog.ovhcloud.com%2Fthe-unexpected-quest-for-business-intelligence%2F&amp;action_name=The%20Unexpected%20Quest%20for%20Business%20Intelligence&amp;urlref=https%3A%2F%2Fblog.ovhcloud.com%2Ffeed%2F" style="border:0;width:0;height:0" width="0" height="0" alt="" />]]></description>
										<content:encoded><![CDATA[
<p class="graf graf--p"><a href="https://en.wikipedia.org/wiki/Business_intelligence" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Business Intelligence</a> (BI) is the ability to collect substantial data from an information system to feed a Data Warehouse (DWH) or data lake. They usually provide a copy of the data that will be used for BI applications. Different strategies can be applied to feed a DWH. One such strategy is <strong>Change Data Capture</strong> (CDC), which is the ability to capture <strong class="markup--strong markup--p-strong">changing states</strong> from a database, and convert them to&nbsp;<strong class="markup--strong markup--p-strong">events</strong>&nbsp;that can be used for other purposes. Most databases are intended for&nbsp;<strong class="markup--strong markup--p-strong">OLTP</strong> purposes, and are well designed for this. Nonetheless, different use cases would require the same data with different access patterns. These use cases&nbsp;(big data, ETL, and stream processing, to name a few) mostly fall under the&nbsp;<strong>OLAP </strong>banner. Mixing them would make the OLTP and production environment at risk, thus we need to enable OLAP in a non-intrusive way.</p>



<p class="graf graf--p">OVH, as a cloud provider, manages numerous databases, both for its customers and its own needs. Managing a database lifecycle always involves both keeping the infrastructure up to date, and remaining in synch with the development release cycle, to align the software with its database dependency. For example, an app might require MySQL 5.0, which could then be announced as EOL (End Of Life). In this case the app needs to be modified to support (let’s say) MySQL 5.5. We’re not reinventing the wheel here – this process has been managed by operations and dev teams for decades now.</p>



<p class="graf graf--p">This becomes trickier if you don’t have control over the application. For example, imagine a third party provides you with an application to ensure encrypted transactions. You have absolutely no control over this application, nor the associated database. Nonetheless, you still need the data from the database.</p>



<p class="graf graf--p">This blog post relates a similar example we encountered while building the OVH data lake, with the help of an in-house CDC development. This story takes place in early 2015, although I still think it&#8217;s worth sharing. 🙂</p>



<h3 class="graf graf--h3 wp-block-heading">Designing a non-intrusive Change Data Capture process</h3>



<p class="graf graf--p">It’s usually good practice to establish the state of the technology before jumping into dev, as it will save time and strengthen communities. Back in early 2015, when the CDC landscape was first emerging (<a href="https://debezium.io/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Debezium</a>, a similar open-source solution, only appeared at the end of the same year), the only existing solution – Databus&nbsp; – came from LinkedIn. The Databus architecture was rather complex, and the project wasn’t very active. Also, it didn’t solve our security requirements, and we come from a strong Ops culture, so running a JVM on the database server was clearly a no-go for our Operations teams.</p>



<p class="graf graf--p">Although there was no CDC software matching our requirements, we eventually found a binlog replication library that we could integrate with the few of them in Go. Binlog is the MySQL name for the WAL database.</p>



<p class="graf graf--p">Our requirements were rather simple:</p>



<ul class="postList wp-block-list"><li class="graf graf--li"><strong class="markup--strong markup--li-strong">Avoid JVM-based</strong> <strong>solutions</strong> (JVM and containers weren&#8217;t working well at the time, and it’d have been hard to get support from Ops)</li><li class="graf graf--li">The CDC agent needed to connect to the CDC gateway for <strong class="markup--strong markup--li-strong">highly-secured environments</strong> (and not a gateway to agents)</li><li class="graf graf--li">The CDC gateway could&nbsp;<strong class="markup--strong markup--li-strong">control its agents&#8217; fleet</strong></li><li class="graf graf--li">The CDC agent could&nbsp;<strong class="markup--strong markup--li-strong">filter</strong>&nbsp;and <strong class="markup--strong markup--li-strong">serialise</strong>&nbsp;events to push them with back pressure control</li><li class="graf graf--li">The CDC agent could&nbsp;<strong class="markup--strong markup--li-strong">dump the DB</strong> to get a first snapshot, since binlogs aren’t always available from the beginning</li></ul>



<p class="graf graf--p">Here is the global design of the &#8216;Menura&#8217; project:</p>



<div class="wp-block-image"><figure class="aligncenter"><a href="/blog/wp-content/uploads/2019/02/IMG_0079-2.png" data-wpel-link="internal"><img fetchpriority="high" decoding="async" width="1024" height="627" src="https://www.ovh.com/blog/wp-content/uploads/2019/02/IMG_0079-2-1024x627.png" alt="Menura agent" class="wp-image-14812" srcset="https://blog.ovhcloud.com/wp-content/uploads/2019/02/IMG_0079-2-1024x627.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/IMG_0079-2-300x184.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/IMG_0079-2-768x470.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/IMG_0079-2-1200x734.png 1200w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption>Menura agent</figcaption></figure></div>



<p class="graf graf--p">Menura is the genus of the <strong class="markup--strong markup--p-strong">lyrebird</strong>: a bird that can replicate any sound. Most BI-related components are &#8216;birds&#8217;, since they’re part of the <strong class="markup--strong markup--p-strong">B</strong>usiness <strong class="markup--strong markup--p-strong">I</strong>ntelligence <strong class="markup--strong markup--p-strong">R</strong>&amp;<strong class="markup--strong markup--p-strong">D</strong> project!</p>



<h3 class="graf graf--h4 wp-block-heading">Automate the BI Control&nbsp;Plane</h3>



<p class="graf graf--p">As Menura was deployed on database servers, it could reflect available databases and tables in the <strong class="markup--strong markup--p-strong">BI Control Plane</strong>, so that a user could ask to sync with a given table. The controlling protocol had a few simple tasks:</p>



<ul class="postList wp-block-list"><li class="graf graf--li">Add and configure a database source</li><li class="graf graf--li">Manage remote configuration</li><li class="graf graf--li">Map agent/tables cartography</li><li class="graf graf--li">Dump database tables</li><li class="graf graf--li">Manage CDC (start/stop sync, commit binlog offsets…)</li></ul>



<p class="graf graf--p"><strong>gRPC</strong> was only just emerging at the time, but we saw in this project, with its strong foundations, an opportunity to reconcile Protobuf, Thrift, and language diversity. Furthermore, the ability to set up a bidirectional streaming RPC was interesting from the point of view of implementing client-to-server connections with server-to-client RPCs, so we made it the foundation of the controlling protocol.</p>



<p class="graf graf--p">gRPC uses <strong>Protocol Buffer</strong> as IDL to serialise structured data. Each StreamRequest is composed of a Header to manage multi-tenancy. This means that if our customers decided to name their sources with the same name, we could isolate control messages by tenant, not just by source. We therefore find a RequestType, as defined in Protobuf v3:</p>



<pre class="wp-block-code"><code lang="json" class="language-json">enum RequestType {
 Control_Config       = 0;
 Control_Hearbeat     = 1;
 Control_MySQLClient  = 10;
 Control_MySQLBinlog  = 11;
 Control_Syslog       = 12;
 Control_PgSQLClient  = 13;
 Control_PgSQLWal     = 14;
 Control_PgSQLLogDec  = 15;
 Control_Kafka        = 16; 
 Control_MSSQLClient  = 17;
}</code></pre>



<p class="graf graf--p">This RequestType allowed us to reach source plugins with the specialised structures they expect. Note that we decoupled DB Clients from DB Replication (binlog, WAL…). The main reason is that they don’t share the same scope, so the libraries aren’t the same. It therefore made sense to keep them separate.</p>



<p class="graf graf--p">Another reason is that replication acts as a slave for the database, meaning there is no significant footprint over a replication process, while a client dumping the DB could imply locking, rows or tables, given the database and its undergoing engine. This could have led to us having two different slaves, or the replication plugged into a master and the client plugged into a slave.</p>



<p class="graf graf--p">These concerns drove us towards a modular design for the Menura agent:</p>



<div class="wp-block-image"><figure class="aligncenter"><a href="/blog/wp-content/uploads/2019/02/IMG_0088.png" data-wpel-link="internal"><img decoding="async" width="1024" height="652" src="https://www.ovh.com/blog/wp-content/uploads/2019/02/IMG_0088-1024x652.png" alt="Menura agent" class="wp-image-14811" srcset="https://blog.ovhcloud.com/wp-content/uploads/2019/02/IMG_0088-1024x652.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/IMG_0088-300x191.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/IMG_0088-768x489.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/IMG_0088-1200x764.png 1200w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption>Menura agent</figcaption></figure></div>



<h4 class="graf graf--h4 wp-block-heading">Filtering data</h4>



<p class="graf graf--p">An important feature was the ability to filter events or columns. There were two reasons for this:</p>



<ul class="postList wp-block-list"><li class="graf graf--li">We encountered databases with so many tables or columns that we needed to cut some noise</li><li class="graf graf--li">We didn’t necessarily need to get certain data out of the database</li></ul>



<p class="graf graf--p">Filtering closest to the DB was certainly the best choice, particularly for our customers, as they could then add or verify filters by themselves. To do this, we defined a <strong class="markup--strong markup--p-strong">Filter Policy</strong> to mimic IPTables policies with <em class="markup--em markup--p-em">accept</em> or <em class="markup--em markup--p-em">drop</em> defaults. The source filter then described how tables and columns would behave, depending on the policy:</p>



<pre class="wp-block-code"><code lang="yaml" class="language-yaml">filter_policy: accept/drop
filter:
 table_a:
 table_b:
   ignored_columns:
     — sensibleColumn</code></pre>



<p class="graf graf--p">The<em class="markup--em markup--p-em"> drop</em> policy will drop anything by default, except tables explicitly listed in the filter, while the&nbsp;<em class="markup--em markup--p-em">accept</em> policy will drop tables listed as empty in the filter, with the exception of tables that have an&nbsp;<em class="markup--em markup--p-em">ignored_columns</em> key, to filter only columns with their names.</p>



<h4 class="graf graf--h4 wp-block-heading">Validations in heterogenous systems</h4>



<p class="graf graf--p">For certain needs, you may want to confirm that you’re processing an analytics job on the same data that the true database consists of. For example, processing revenue calculation over a given period requires the true data from date to date. Validating a replication state between a database and the data lake was challenging. In fact, integrity checks aren’t implemented with the same logic in databases or stores, so we needed a way to abstract them from the native implementation. We thought about using a <a href="https://en.wikipedia.org/wiki/Merkle_tree" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer"><strong class="markup--strong markup--p-strong">Merkle Tree</strong></a> data structure, so that we could maintain a tree of integrity with blocks or rows. If a key/value differed from the database, then the global or intermediate integrity hash would reflect it, and we would only have to scan the leaf block that had an inconsistent hash between both systems.</p>



<div class="wp-block-image wp-image-14825"><figure class="aligncenter is-resized"><img decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2019/02/175C3FC7-61C2-4A39-89B3-EB869DA38D6F-823x1024.png" alt="Merkel tree" class="wp-image-14825" width="617" height="768" srcset="https://blog.ovhcloud.com/wp-content/uploads/2019/02/175C3FC7-61C2-4A39-89B3-EB869DA38D6F-823x1024.png 823w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/175C3FC7-61C2-4A39-89B3-EB869DA38D6F-241x300.png 241w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/175C3FC7-61C2-4A39-89B3-EB869DA38D6F-768x955.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/175C3FC7-61C2-4A39-89B3-EB869DA38D6F-1200x1493.png 1200w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/175C3FC7-61C2-4A39-89B3-EB869DA38D6F.png 1534w" sizes="(max-width: 617px) 100vw, 617px" /><figcaption>Merkel tree</figcaption></figure></div>



<h3 class="graf graf--h3 wp-block-heading">Let’s put things&nbsp;together</h3>



<p class="graf graf--p">As we stated in our introduction, CDC is set up to convert database changes into processable events. The goal here is to fulfil any business needs that require data in an efficient and effective way. Here are two examples of what we did with the data that we now had available as events&#8230;</p>



<h4 class="graf graf--h4 wp-block-heading">Real-time joins between databases</h4>



<p class="graf graf--p">While we were building the data lake from replicated tables, and since this project was mainly for BI purposes, we considered adding some real-time insights, based on the same logic we we&#8217;re using with batch and <a href="http://pig.apache.org/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Apache Pig</a> jobs. Since 2015, the most advanced stream processing framework is <a href="https://flink.apache.org/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer"><strong>Apache Flink</strong></a>, which we used to process real-time joins between different databases and tables.</p>



<p class="graf graf--p"><a href="https://twitter.com/bru_gere" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Alexis</a> did an amazing job describing the join process that we injected into Apache Flink, so that in addition to replicating databases, we were also creating a new, aggregated table. Indeed, we could write an entire blog post just on this topic&#8230;</p>



<div class="wp-block-image wp-image-14827"><figure class="aligncenter is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2019/02/F13EE737-B0E4-4103-9EA1-B3E095BB9749-1024x1019.png" alt="Real-time joins using Apache Flink" class="wp-image-14827" width="512" height="510" srcset="https://blog.ovhcloud.com/wp-content/uploads/2019/02/F13EE737-B0E4-4103-9EA1-B3E095BB9749-1024x1019.png 1024w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/F13EE737-B0E4-4103-9EA1-B3E095BB9749-150x150.png 150w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/F13EE737-B0E4-4103-9EA1-B3E095BB9749-300x300.png 300w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/F13EE737-B0E4-4103-9EA1-B3E095BB9749-768x764.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/F13EE737-B0E4-4103-9EA1-B3E095BB9749-1200x1194.png 1200w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/F13EE737-B0E4-4103-9EA1-B3E095BB9749.png 1470w" sizes="auto, (max-width: 512px) 100vw, 512px" /><figcaption>Real-time joins using Apache Flink</figcaption></figure></div>



<p>We chose Apache Flink for multiple reasons:</p>



<ul class="wp-block-list"><li>Its <strong>documentation</strong> was delightful</li><li>Its <strong>core engine</strong> was brilliant, and very far beyond Apache Spark (the Tungsten project wasn&#8217;t even there)</li><li>It was a <strong>European project</strong>, so we were close to the editor and its community</li></ul>



<h4 class="graf graf--h4 wp-block-heading">Real-time&nbsp;indexing</h4>



<p class="graf graf--p">Now we had a real-time table fed into <a href="http://hbase.apache.org/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Apache HBase</a>, we needed to add a query capability on top of it. While HBase was fine from a storage point of view, it didn’t provide any search capability, and its access pattern wasn’t ideal for scanning over the search criterion.</p>



<p class="graf graf--p">This is where <a href="https://twitter.com/guillaume_salou" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Guillaume</a> worked some magic! By reusing <a href="https://ngdata.github.io/hbase-indexer/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Lily</a>, an HBase Indexer that provided the concept of SEP (Side Effect Processor), he succeeded in reinjecting the aggregated table schema into Lily to build the data type mapping needed to read HBase Byte Arrays values, before indexing them into Solr. We now had a <strong class="markup--strong markup--p-strong">real-time dashboard</strong> of an aggregated real-time joined table, processed from real-time change data capture. Boom!</p>



<div class="wp-block-image wp-image-14828"><figure class="aligncenter is-resized"><img loading="lazy" decoding="async" src="https://www.ovh.com/blog/wp-content/uploads/2019/02/BD2900D5-FF4A-479A-BCB3-2FD8F3A1B3CC-885x1024.png" alt="Real-time indexing" class="wp-image-14828" width="443" height="512" srcset="https://blog.ovhcloud.com/wp-content/uploads/2019/02/BD2900D5-FF4A-479A-BCB3-2FD8F3A1B3CC-885x1024.png 885w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/BD2900D5-FF4A-479A-BCB3-2FD8F3A1B3CC-259x300.png 259w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/BD2900D5-FF4A-479A-BCB3-2FD8F3A1B3CC-768x888.png 768w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/BD2900D5-FF4A-479A-BCB3-2FD8F3A1B3CC-1200x1388.png 1200w, https://blog.ovhcloud.com/wp-content/uploads/2019/02/BD2900D5-FF4A-479A-BCB3-2FD8F3A1B3CC.png 1358w" sizes="auto, (max-width: 443px) 100vw, 443px" /><figcaption>Real-time indexing</figcaption></figure></div>



<p class="graf graf--p">That was when we started getting real customers for our new solution.</p>



<h3 class="graf graf--h3 wp-block-heading">Going live!</h3>



<p class="graf graf--p">If there is still a need to demonstrate that testing in a simulation environment is not the same as testing in a production environment, this next part will likely settle the debate&#8230;</p>



<p class="graf graf--p">After setting up the data pipeline, we discovered a few bugs in the production environments. Here are two of them:</p>



<h3 class="graf graf--h4 wp-block-heading">Interleaved events</h3>



<p class="graf graf--p">As defined by MySQL, an event structure is composed of both a header and a data field.</p>



<p>In <strong class="markup--strong markup--p-strong">Row-based Replication</strong> (RBR), as opposed to <strong>Statement-based Replication </strong>(SBR), each row&#8217;s event is replicated with its corresponding data. DML statements are binlogged into two parts:</p>



<ul class="postList wp-block-list"><li>A <code>TABLE_MAP_EVENT</code></li><li>A <code>ROWS_EVENT</code> (can be <code>WRITE</code>, <code>UPDATE</code> or <code>DELETE</code>)<em class="markup--em markup--li-em"><br></em></li></ul>



<p class="graf graf--p">The first event, <code>TABLE_MAP_EVENT</code>, describes the metadata of the second event&#8217;s content. This metadata contains:</p>



<ul class="postList wp-block-list"><li class="graf graf--li">Included fields</li><li class="graf graf--li">Null values bitmaps</li><li class="graf graf--li">The schema of the upcoming data</li><li>The metadata for the provided schema</li></ul>



<p class="graf graf--p">The second event, <code>WRITE_ROWS_EVENT</code> (for inserts) contains the data. To decode it, you need the previous <code>TABLE_MAP_EVENT</code> event to known how to consume this event, matching the corresponding <code>MYSQL_TYPE_*</code>&nbsp;, and reading the number of bytes expected for each types.</p>



<p>Occasionally, some events were not consumed properly, as a dis-alignment between metadata and data led to <code>VARCHAR</code> values being decoded as <code>DATETIME</code> values, etc.</p>



<p>After some debugging, it turned out that triggers had been added on some MySQL tables by the DBA team. When the replicas had been rebuilt some days later, they had inherited these features, and started to log the events produced by these triggers.</p>



<p>The thing is, triggers are internal with MySQL. In binlog, every&nbsp;event coming from the master is sent like this:</p>



<pre class="wp-block-code"><code class="">TableMapEvent_a
WriteEvent_a
TableMapEvent_b
WriteEvent_b
TableMapEvent_c
WriteEvent_c</code></pre>



<p><code>a</code>, <code>b</code> and <code>c</code> represents events for different schema.tables.</p>



<p>Since triggers don’t come from the master, when the slave receives a&nbsp;<code>TableMapEvent</code> for a specific table, it triggers another <code>TableMapEvent</code>&nbsp;for a specialised table (<code>&lt;table&gt;_event</code>). The same applies to the <code>WriteEvent</code>.</p>



<p>When MySQL triggers an event, it sends it in the binlog, so&nbsp;you will end with a multiplexing of two <code>TableMapEvents</code>, then&nbsp;two <code>RowsEvents</code>, as shown below:</p>



<pre class="wp-block-code"><code class="">TableMapEvent_a
TableMapEvent_a_event
WriteEvent_a
WriteEvent_a_event</code></pre>



<p>Got it! When we tried to decode WriteEvent_a, the previous&nbsp;TableMapEvent was for TableMapEvent_a_event, not for TableMapEvent_a, so it’d&nbsp;try to decode the event with the wrong schema.</p>



<p>We had to find a way to match the WriteEvent to the corresponding&nbsp;TableMapEvent. Ideally, there would have been a TableID in the structure that we could have used for this. In the end though, we just had to buffer all TableMapEvents, making them available to all RowsEvents, start reading the RowsEvent, pick the TableID, and then get the Columns metadata from the matching TableMapEvent. Fixed!</p>



<h4 class="graf graf--h4 wp-block-heading">The elusive decimal&#8230;</h4>



<p class="graf graf--p">We also encountered an arbitrary bug in the library, which caused Menura to explode. Again, we dug into the binlog library to debug the decoding process, step by step. We identified table/column tuples to limit the logging output to a more reasonable rate. A <code>RowEvent</code> looked like this:</p>



<pre class="wp-block-code"><code class="">DEBUG MR: TableMapEvent.read() : event.TableName = myTable
DEBUG MR: TableMapEvent.read() : columnCount= 16
DEBUG MR: TableMapEvent.read() : Read Next columnTypeDef= [3 3 15 246 0 0 12 0 15 12 12 15 3 3 15 15]
DEBUG MR: readStringLength() : r.Next(int(12))
DEBUG MR: TableMapEvent.read() : ReadStringLength columnMetaDef= [255 0 10 2 40 0 10 0 255 0 255 3]
DEBUG MR: TableMapEvent.read() : columnNullBitMap= [10 198]
DEBUG MR: TableMapEvent.read() : switch columnTypeDef[3]=f6
DEBUG MR: TableMapEvent.read() : switch : case metaOffset+=2
DEBUG MR: TableMapEvent.read() : column.MetaInfo
DEBUG MR: TableMapEvent.read() : column.MetaInfo = [10 2]</code></pre>



<p>In this log, there are parts of the decoding process that are quite interesting and worth taking a closer look at. A first column presents the following schema:</p>



<pre class="wp-block-code"><code class="">TableMapEvent.read() : Read Next columnTypeDef= [3 3 15 246 0 0 12 0 15 12 12 15 3 3 15 15]</code></pre>



<p>Some of these data types require metadata to be read. For example, here is the corresponding log with column metadata:</p>



<pre class="wp-block-code"><code class="">TableMapEvent.read() : ReadStringLength columnMetaDef= [255 0 10 2 40 0 10 0 255 0 255 3]</code></pre>



<p>Also, the <code>NullBitMap</code> column is important, since we have to know which null values should be ignored while decoding the buffer.</p>



<p class="graf graf--p">This crash didn’t happen on a daily basis, and the stacktrace didn’t point me to a fixed part of the code. It seemed like a shift in the decoding that would cause arbitrary crashes when casting data types wasn’t possible. To debug at a deeper level, we needed to log more. And so we logged the buffer&#8217;s current offset, the size read for each data type, the metadata for each data type, and the value. Here is an example for a <code>MYSQL_TYPE_NEWDECIMAL</code><em class="markup--em markup--p-em"> : </em></p>



<pre class="wp-block-code"><code class="">DEBUG MR: rowsEvent.read(): pack.Len() BEFORE : 59
DEBUG MR: rowsEvent.read(): Column.MetaInfo: &amp;{246 [10 2] true}
DEBUG MR: rowsEvent.read(): switch column.Type= 246
DEBUG MR: case MYSQL_TYPE_NEWDECIMAL
DEBUG MR: readNewDecimal() precision= 10 scale= 2
DEBUG MR: readNewDecimal() size= 5
DEBUG MR: readNewDecimal() buff=8000000000
DEBUG MR: readNewDecimal() decimalpack=0000000000
DEBUG MR: rowsEvent.read(): pack.Len() AFTER : 54
DEBUG MR: rowsEvent.read(): value : 0
DEBUG MR: rowsEvent.read(): pack.Len() BEFORE : 54
DEBUG MR: rowsEvent.read(): Column.MetaInfo: &amp;{0 [] false}
DEBUG MR: rowsEvent.read(): switch column.Type= 0</code></pre>



<p class="graf graf--p">Regarding the previous schema, we have 16 columns, and according to <a href="https://dev.mysql.com/doc/internals/en/table-map-event.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">MySQL documentation</a> our data types provide metadata as in the following table:</p>



<table class="confluenceTable tablesorter tablesorter-default stickyTableHeaders" role="grid">
<thead class="tableFloatingHeaderOriginal">
<tr class="tablesorter-headerRow" role="row">
<th class="confluenceTh tablesorter-header sortableHeader tablesorter-headerUnSorted" tabindex="0" role="columnheader" scope="col" data-column="0" aria-disabled="false" aria-sort="none" aria-label="type code: No sort applied, activate to apply an ascending sort">
<div class="tablesorter-header-inner">type code</div>
</th>
<th class="confluenceTh tablesorter-header sortableHeader tablesorter-headerUnSorted" tabindex="0" role="columnheader" scope="col" data-column="1" aria-disabled="false" aria-sort="none" aria-label="MYSQL_TYPE: No sort applied, activate to apply an ascending sort">
<div class="tablesorter-header-inner">MYSQL_TYPE</div>
</th>
<th class="confluenceTh tablesorter-header sortableHeader tablesorter-headerUnSorted" tabindex="0" role="columnheader" scope="col" data-column="2" aria-disabled="false" aria-sort="none" aria-label="Metadata: No sort applied, activate to apply an ascending sort">
<div class="tablesorter-header-inner">Metadata</div>
</th>
</tr>
</thead>
<tbody aria-live="polite" aria-relevant="all">
<tr role="row">
<td class="confluenceTd">3</td>
<td class="confluenceTd">MYSQL_TYPE_LONG</td>
<td class="confluenceTd">0</td>
</tr>
<tr role="row">
<td class="confluenceTd">15</td>
<td class="confluenceTd">MYSQL_TYPE_VARCHAR</td>
<td class="confluenceTd">2</td>
</tr>
<tr role="row">
<td class="confluenceTd">0</td>
<td class="confluenceTd">MYSQL_TYPE_DECIMAL</td>
<td class="confluenceTd">2</td>
</tr>
<tr role="row">
<td class="confluenceTd">12</td>
<td class="confluenceTd">MYSQL_TYPE_DATETIME</td>
<td class="confluenceTd">0</td>
</tr>
<tr role="row">
<td class="confluenceTd">0</td>
<td class="confluenceTd">MYSQL_TYPE_NEWDECIMAL</td>
<td class="confluenceTd">2</td>
</tr>
</tbody>
</table>



<p>This gives us&nbsp;<strong>18 bytes</strong>&nbsp;of metadata for this example schema, as opposed to the&nbsp;<strong>10 bytes</strong>&nbsp;in the packet.</p>



<p>We also found that MySQL apparently didn&#8217;t send the metadata needed to read <code>DECIMAL</code> values in the packet. Was this a normal behaviour?</p>



<p><a href="https://dev.mysql.com/doc/refman/5.7/en/precision-math-decimal-characteristics.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">The MySQL documentation is clear</a>: to read a <code>DECIMAL</code> value, you need the metadata&nbsp;(precision, scale etc.). Period.</p>



<p class="graf graf--p">However, we found that <code>MYSQL_TYPE_DECIMAL</code> was treated like <code>MYSQL_TYPE_NEWDECIMAL</code><em class="markup--em markup--p-em">.</em></p>



<pre class="wp-block-code"><code class="">case MYSQL_TYPE_DECIMAL, MYSQL_TYPE_NEWDECIMAL:
value.value = pack.readNewDecimal(int(column.MetaInfo[0]), int(column.MetaInfo[1]))</code></pre>



<p>We stepped back and searched for how this <code>MYSQL_TYPE_DECIMAL</code> was implemented in other binlog libraries. I was not DBA, but it felt strange that schema using <code>DECIMAL</code> values were actually using two different MySQL data types.</p>



<p class="graf graf--p">Okay&#8230; &#8220;Houston, we have a problem.&#8221;</p>



<p>First, nobody was implementing <code>MYSQL_TYPE_DECIMAL</code>, and for a very good reason: we shouldn’t be receiving it, since it had been deprecated from MySQL since version 5.0. This meant the database behind was running a table created from (at best) MySQL 4.9, while the database had been upgraded without having received a proper ALTER to automatically convert data types to&nbsp;<code>MYSQL_TYPE_NEWDECIMAL</code><em class="markup--em markup--p-em">.<br></em></p>



<p class="graf graf--p">Second, since we don’t have any control over the database, how do we decode a <code>MYSQL_TYPE_DECIMAL</code><em class="markup--em markup--p-em">…&nbsp;</em></p>



<h4 class="wp-block-heading">First attempt: Ignore it</h4>



<p class="graf graf--p">We first circumvented this issue by actually not reading two bytes of metadata when we parsed a <code>MYSQL_TYPE_DECIMAL</code> column. This stopped corrupting the metadata offset, and other data types were now aligned with their metadata.</p>



<p class="graf graf--p">We missed the decimal value, but we could continue to read other data types. Well, sort of&#8230; It was better, but to read values after a <code>MYSQL_TYPE_DECIMAL</code> in the data buffer, we needed to know&nbsp;<strong>how many bytes to read</strong>.</p>



<h4 class="wp-block-heading">Second attempt: The naïve approach (i.e. guessing!)</h4>



<p class="graf graf--p">A decimal value is a fractional number, usually encoded as a floating-point number. For example, a <code class="markup--code markup--p-code">DECIMAL(10,2)</code> column has eight integer digits and two fractional digits. The integer digits determine the number of bytes needed to be read. For example, we read four bytes for the integer part, and one byte for the fractional part. This would have been so simple… if we had the metadata.</p>



<p class="graf graf--p">In practice, MySQL didn’t provide any metadata for <code>DECIMAL</code> values, hence why we ignored it in the first iteration, to preserve other data. Have you ever tried to decode old binlogs with the official <em class="markup--em markup--p-em">mysqlbinlog</em> tool? If you had a <code>MYSQL_TYPE_DECIMAL</code> in your data, then it would stop decoding there. Yes&#8230; MySQL doesn’t know how to decode its own data format!</p>



<p>One could argue that if MySQL doesn&#8217;t provide any metadata, it&#8217;s because it stores it internally, at a fixed size. Well&#8230; no!</p>



<table class="confluenceTable tablesorter tablesorter-default stickyTableHeaders" role="grid">
<thead class="tableFloatingHeaderOriginal">
<tr class="tablesorter-headerRow" role="row">
<th class="confluenceTh tablesorter-header sortableHeader tablesorter-headerUnSorted" tabindex="0" role="columnheader" scope="col" data-column="0" aria-disabled="false" aria-sort="none" aria-label="sql value: No sort applied, activate to apply an ascending sort">
<div class="tablesorter-header-inner">sql value</div>
</th>
<th class="confluenceTh tablesorter-header sortableHeader tablesorter-headerUnSorted" tabindex="0" role="columnheader" scope="col" data-column="1" aria-disabled="false" aria-sort="none" aria-label="byte array: No sort applied, activate to apply an ascending sort">
<div class="tablesorter-header-inner">byte array</div>
</th>
<th class="confluenceTh tablesorter-header sortableHeader tablesorter-headerUnSorted" tabindex="0" role="columnheader" scope="col" data-column="2" aria-disabled="false" aria-sort="none" aria-label="type: No sort applied, activate to apply an ascending sort">
<div class="tablesorter-header-inner">type</div>
</th>
</tr>
</thead>
<tbody aria-live="polite" aria-relevant="all">
<tr role="row">
<td class="confluenceTd">0.00</td>
<td class="confluenceTd">32 32 32 32 32 32 32 48 46 48 48</td>
<td class="confluenceTd">decimal(10,2)</td>
</tr>
<tr role="row">
<td class="confluenceTd">0.000</td>
<td class="confluenceTd">32 32 48 46 48 48 48</td>
<td class="confluenceTd">decimal(5,3)</td>
</tr>
</tbody>
</table>



<p class="graf graf--p">Here&#8217;s how it actually works&#8230; Decimals are encoded as VARCHAR in the protocol. I tried to read the value, assuming the space padding, flagged the dot encountered, and tried to read fractional data that seemed coherent for a decimal. If it wasn’t, I eventually unread the last byte in the buffer and continued to the next data type. And it worked. For a time&#8230;</p>



<pre class="wp-block-preformatted graf graf--p">DEBUG MR: case MYSQL_TYPE_DECIMAL
DEBUG MR: readOldDecimalV2: byte = 32
DEBUG MR: readOldDecimalV2: continue
DEBUG MR: readOldDecimalV2: byte = 32
DEBUG MR: readOldDecimalV2: continue
DEBUG MR: readOldDecimalV2: byte = 32
DEBUG MR: readOldDecimalV2: continue
DEBUG MR: readOldDecimalV2: byte = 32
DEBUG MR: readOldDecimalV2: continue
DEBUG MR: readOldDecimalV2: byte = 32
DEBUG MR: readOldDecimalV2: continue
DEBUG MR: readOldDecimalV2: byte = 32
DEBUG MR: readOldDecimalV2: continue
DEBUG MR: readOldDecimalV2: byte = 32
DEBUG MR: readOldDecimalV2: continue
DEBUG MR: readOldDecimalV2: byte = 48
DEBUG MR: readOldDecimalV2: start writing
DEBUG MR: readOldDecimalV2: byte = 46
DEBUG MR: readOldDecimalV2: dot found
DEBUG MR: readOldDecimalV2: writing
DEBUG MR: readOldDecimalV2: byte = 48
DEBUG MR: readOldDecimalV2: writing
DEBUG MR: readOldDecimalV2: byte = 48
DEBUG MR: readOldDecimalV2: writing
DEBUG MR: readOldDecimalV2: byte = 32
DEBUG MR: readOldDecimalV2: unread, break
DEBUG MR: readOldDecimalV2: converting to float&nbsp;: 0.00
DEBUG MR: rowsEvent.read(): pack.Len() AFTER&nbsp;: 43
DEBUG MR: rowsEvent.read(): value&nbsp;: 0</pre>



<p>We hope we don&#8217;t encounter a following VARCHAR type with a length that could be parsed as a DIGIT value, but the dynamic size of the DECIMAL value means that there should be metadata available to properly read this. <strong>There is no other way.</strong></p>



<h4 class="wp-block-heading">Third attempt: There is no compromise when it comes to being a good slave!</h4>



<p class="graf graf--p">We asked ourselves what makes mysqlbinlog different to a MySQL Slave when it comes to reading binlogs. We found that the only difference was that a true slave knew the <code>DECIMAL</code> schema and associated metadata when receiving these data. So it wouldn’t have to guess anything –&nbsp;just read the right number of bytes, according to the known schema.</p>



<p class="graf graf--p">We ended up implementing a MySQL client into our mysqlbinlog source, which initially dumped the schemas of tables to pass in the <em>NumericScale</em> value into the decoding library. The pitfall here is that rows aren’t identified in schemas by their <code>ColumnName</code>. MySQL maintains an <code>OrdinalPosition</code> for the columns in a table, but it’s not the ID that is provided in the binlog protocol (that would be too easy!). You have to maintain your own column index from the schema, to make it match the one you will receive in the binlog protocol. Once you have it, just look up the decimal scale value to know how many bytes you still have to read after the dot.</p>



<p class="graf graf--p">This way, the decoding library was now capable of decoding <code>MYSQL_TYPE_DECIMAL</code> from the binlog stream of events. Hooray!!</p>



<h3 class="graf graf--h3 wp-block-heading">TL;DR</h3>



<p class="graf graf--p">In the end, building a BI stack from scratch took approximately six months. The team was composed of&nbsp; 2.5 people: <a href="https://twitter.com/bru_gere" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Alexis Gendronneau</a>, <a href="https://twitter.com/guillaume_salou" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Guillaume Salou</a> (who joined us after three months) and me. It demonstrated the principle of Change Data Capture applied to real-life use cases, enabling real-time insights into sales, stocks, and more, without any impact on production environments. The team grew as the project extended its scope, with new, more challenging customers, like financial services and management control teams. Weeks later, we succeeded in launching it on Apache Flink, based on the same data pipeline that has since become the trusted source for revenue calculation and other business KPIs.</p>



<p>We learned a lot from this project. A key lesson is how important <strong>keeping your technical debt in control</strong> can be, and what impact it can have on other teams and projects. Also, working with&nbsp;<a href="https://flink.apache.org/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer"><strong>Apache Flink</strong></a>&nbsp;for a range of projects proved to be a wonderful experience for our teams.</p>



<p class="graf graf--p">The whole team delivered great work, and <a href="https://twitter.com/pirionfr" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">Dimitri Capitaine</a> is about to open-source the data collector agent that powered the preview labs: <a href="https://labs.ovh.com/ovh-data-collector" data-wpel-link="exclude">OVH Data Collector</a>. If you&#8217;re interested in discussing Change Data Capture at OVH in greater depth, feel free to join us on the <a href="https://gitter.im/ovh/big-data" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">team&#8217;s Gitter</a>, or <a href="https://twitter.com/stevenleroux" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer">DM me on Twitter</a>.</p>
<img loading="lazy" decoding="async" src="//blog.ovhcloud.com/wp-content/plugins/matomo/app/matomo.php?idsite=1&amp;rec=1&amp;url=https%3A%2F%2Fblog.ovhcloud.com%2Fthe-unexpected-quest-for-business-intelligence%2F&amp;action_name=The%20Unexpected%20Quest%20for%20Business%20Intelligence&amp;urlref=https%3A%2F%2Fblog.ovhcloud.com%2Ffeed%2F" style="border:0;width:0;height:0" width="0" height="0" alt="" />]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
