<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>mysql | 伪架构师</title>
    <link>/tags/mysql/</link>
      <atom:link href="/tags/mysql/index.xml" rel="self" type="application/rss+xml" />
    <description>mysql</description>
    <generator>Source Themes Academic (https://sourcethemes.com/academic/)</generator><language>zh</language><lastBuildDate>Wed, 31 Oct 2018 21:00:53 +0800</lastBuildDate>
    <image>
      <url>/img/logo-wide.png</url>
      <title>mysql</title>
      <link>/tags/mysql/</link>
    </image>
    
    <item>
      <title>使用 go-mysql-elasticsearch 把 MySQL 中的业务日志导入 Elasticsearch</title>
      <link>/post/from-mysql-to-es/</link>
      <pubDate>Wed, 31 Oct 2018 21:00:53 +0800</pubDate>
      <guid>/post/from-mysql-to-es/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;太长不看版：&lt;a href=&#34;https://github.com/siddontang/go-mysql-elasticsearch&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;go-mysql-elasticsearch&lt;/code&gt;&lt;/a&gt; 能把 MySQL 数据库中的数据导入到 Elasticsearch 之中。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;相当一部分应用的日志是保存在数据库之中的，这些陈旧又稳定的应用在支撑着业务的运行。它们产生的日志通常来说也是有其价值的，但是如果能够融入到目前较为通用的 Elasticsearch 当中的话，可能有助于降低运维工作量，防止信息孤岛，并且进一步挖掘既有应用和业务的商业价值。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/siddontang/go-mysql-elasticsearch&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;go-mysql-elasticsearch&lt;/code&gt;&lt;/a&gt; 就是这样一个项目，它可以从 MySQL 的数据表中读取指定数据表的数据，发送到 ElasticSearch 之中。它会使用 &lt;code&gt;mysqldump&lt;/code&gt; 命令处理现有存量数据，并借助 binlog 的方式跟踪增量数据，从而保证 Elasticsearch 的数据和 MySQL 数据库中的数据保持同步。下面会简单讲一下这一项目的配置，并试验一个简单例子，最后根据实际情况进行一些改进。&lt;/p&gt;

&lt;h2 id=&#34;条件和假设&#34;&gt;条件和假设&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;目前该工具支持 MySQL 和 ES 的版本都是 5.x。&lt;/li&gt;
&lt;li&gt;MySQL 服务器需要开启 row 模式的 binlog。&lt;/li&gt;
&lt;li&gt;因为要使用 &lt;code&gt;mysqldump&lt;/code&gt; 命令，因此该进程的所在的服务器需要部署这一工具。&lt;/li&gt;
&lt;li&gt;这一工具使用 GoLang 开发，需要 Go 1.9+ 的环境进行构建。&lt;/li&gt;
&lt;li&gt;可用的 MySQL、Elasticsearch 以及 Kibana 实例。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外为了进行演示，这里做一点假设：&lt;/p&gt;

&lt;h3 id=&#34;业务日志表&#34;&gt;业务日志表&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-sql&#34;&gt;CREATE TABLE `biz_log` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `receive_content` text,
  `send_content` text,
  `fd_receive_content` text,
  `fd_send_content` text,
  `log_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;四个 &lt;code&gt;text&lt;/code&gt; 字段&lt;strong&gt;使用 JSON 格式&lt;/strong&gt;存储了几个不同的日志种类。&lt;/p&gt;

&lt;h2 id=&#34;工具构建&#34;&gt;工具构建&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;go get github.com/siddontang/go-mysql-elasticsearch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd $GOPATH/src/github.com/siddontang/go-mysql-elasticsearch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;make&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;转换配置&#34;&gt;转换配置&lt;/h2&gt;

&lt;p&gt;执行 &lt;code&gt;go-mysql-elasticsearch --help&lt;/code&gt;，会看到一系列的参数，最主要的参数就是 &lt;code&gt;-config&lt;/code&gt;，这个参数用于设置转换过程所需的参数配置文件，在源码的 &lt;a href=&#34;https://github.com/siddontang/go-mysql-elasticsearch/blob/master/etc/river.toml&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;/etc/river.toml&lt;/code&gt;&lt;/a&gt; 中包含了一份配置样本。作者在这一配置样本中提供了非常详尽的注释，可以对转换过程做出很多定制。但是由于工具本身具备很好的适应能力，加上 ES 的强大功能，只需要一点简单的设置，就能够顺利完成常见任务了。&lt;/p&gt;

&lt;p&gt;一个简单的配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;# MySQL 的相关配置
# 指定用户必须具备复制权限
my_addr = &amp;quot;127.0.0.1:3306&amp;quot;
my_user = &amp;quot;root&amp;quot;
my_pass = &amp;quot;Flzx3000c&amp;quot;
my_charset = &amp;quot;utf8mb4&amp;quot;

# ES 相关配置
es_addr = &amp;quot;127.0.0.1:9200&amp;quot;
es_user = &amp;quot;&amp;quot;
es_pass = &amp;quot;&amp;quot;

# 数据源配置
# 以 Slave 模式工作
server_id = 10001
# mysql/mariadb
flavor = &amp;quot;mysql&amp;quot;

# mysqldump 路径，如果为空或者未设置，会跳过这一环节。
mysqldump = &amp;quot;mysqldump&amp;quot;
bulk_size = 128
flush_bulk_time = &amp;quot;200ms&amp;quot;
skip_no_pk_table = false


[[source]]
# 数据库名称
schema = &amp;quot;biz&amp;quot;
# 数据表同步范围，支持通配符
tables = [&amp;quot;biz_log&amp;quot;]

# 规则定义
[[rule]]
# 数据库名称
schema = &amp;quot;biz&amp;quot;
# 规则对应的数据表，支持通配符
table = &amp;quot;biz_log&amp;quot;
# 目标 ES 索引
index = &amp;quot;biz&amp;quot;
# 该规则在 ES 中生成的文档类型
type = &amp;quot;log_db&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;同步&#34;&gt;同步&lt;/h2&gt;

&lt;p&gt;配置文件完成之后，就可以执行 &lt;code&gt;./go-mysql-elasticsearch -config=./river.toml&lt;/code&gt;，日志中会显示首先执行 &lt;code&gt;mysqldump&lt;/code&gt; 导出存量数据，然后开始守护进程阶段，跟踪 binlog 并进行同步。&lt;/p&gt;

&lt;p&gt;此时打开 Kibana，执行 &lt;code&gt;GET _search&lt;/code&gt;，会看到数据库记录已经进入了 ES 中，并且按照我们定义的规则进行了索引。在守护进行运行期间，如果有新的数据插入，也会同步到 ES 之中。&lt;/p&gt;

&lt;p&gt;但是这里我们会发现一个小问题，前面提到的 JSON 字段被作为单一的字符串存入了 ES 索引。这样就根据 JSON 中的特定字段进行搜索的需要就比较费劲了，而我们也知道，如果直接向 ES 提交文档，其中的 JSON 是会被映射为 Object 类型的。如果对 ES 索引进行数据类型的定义，会发现直接将 JSON 字段映射到 Object 类型后，同步过程会失败，返回错误认为将无效内容映射到了这一类型。因此可以推测是字符串并没有使用原有格式提交给 ES。&lt;/p&gt;

&lt;p&gt;经过对代码的阅读跟踪，发现在 &lt;a href=&#34;https://github.com/siddontang/go-mysql-elasticsearch/blob/d23841980e75da3d6f0c8a13b3876b9a70cb7b80/elastic/client.go#L139&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;elastic/client.go&lt;/code&gt;&lt;/a&gt; 中对数据进行了一次 Json 编码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;    default:
        //for create and index
        data, err = json.Marshal(r.Data)
        if err != nil {
            return errors.Trace(err)
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下面就尝试进行一点改动，使之支持嵌套在字段内容中的 JSON 内容。&lt;/p&gt;

&lt;h2 id=&#34;json&#34;&gt;JSON&lt;/h2&gt;

&lt;p&gt;这里我想到了一个简单粗暴的办法就是，对数据报文进行一次检查，如果该字段内容是有效 JSON 的话，就使用 &lt;code&gt;github.com/buger/jsonparser&lt;/code&gt; 的 &lt;code&gt;set&lt;/code&gt; 方法，将压缩后的 JSON 字符串重新赋值给编码后的 &lt;code&gt;byte[]&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;首先给 &lt;code&gt;BulkRequest&lt;/code&gt; 定义一个新方法，用于数据编码&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;func (b *BulkRequest) encodeData() []byte {
     jsonResults,_ := json.Marshal(b.Data)
     // 判断是否有效的 JSON 数据
     isJson := func(s string) bool {
         var js map[string]interface{}
         return json.Unmarshal([]byte(s), &amp;amp;js) == nil

     }
     for key, value := range b.Data {
         stringValue, ok := value.(string)
         // 如果字段内容是字符串并且是 JSON 格式
         if ok &amp;amp;&amp;amp; isJson(stringValue) {
             // 设置编码后内容该字段的值为原文
             jsonResults,_ = jsonparser.Set(jsonResults, []byte(stringValue), key)
            }
     }
     return jsonResults
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后将原有的 &lt;code&gt;data, err = json.Marshal(r.Data)&lt;/code&gt; 替换为 &lt;code&gt;data = r.encodeData()&lt;/code&gt;，再次构建运行。会看到 ES 成功的将 JSON 字段进行了解析，生成了 Object 类型的映射关系。&lt;/p&gt;

&lt;h2 id=&#34;补充说明&#34;&gt;补充说明&lt;/h2&gt;

&lt;p&gt;这里引用&lt;a href=&#34;https://my.oschina.net/u/2282993/blog/1821930&#34; target=&#34;_blank&#34;&gt;go-mysql-elasticsearch功能及性能验证&lt;/a&gt; 一文的性能测试结果：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;1.全量同步
支持：需要安装mysqldump（mysql自带），同步11.5w数据，耗时3分13秒。
全量基于mysqldump，需要将工具和mysql安装在同一个节点，其它方式尚未找到。
2.增量同步
支持。
增量插入20W数据，耗时8分钟。
删除20w条数据，耗时6分。
更新20w条数据，12分钟。
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一工具还有一些其它亮点，例如多表聚合、字段过滤、自定义字段映射等。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;项目地址：&lt;code&gt;https://github.com/siddontang/go-mysql-elasticsearch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;配置文件样本：&lt;code&gt;https://github.com/siddontang/go-mysql-elasticsearch/blob/master/etc/river.toml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Fork 地址：&lt;code&gt;https://github.com/fleeto/go-mysql-elasticsearch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;作者原版教程：&lt;code&gt;https://www.jianshu.com/p/96c7858b580f&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;go-mysql-elasticsearch 功能及性能验证 ：&lt;code&gt;https://my.oschina.net/u/2282993/blog/1821930&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;JsonParser：&lt;code&gt;https://github.com/buger/jsonparser&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Drupal数据库异常臃肿的检测和排除</title>
      <link>/post/drupal-database-cleanup/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drupal-database-cleanup/</guid>
      <description>&lt;p&gt;Drupal网站利用数据库来保存配置和内容。&lt;/p&gt;

&lt;p&gt;在普遍情况下（小规模部署），这个数据库是很紧凑的。例如我管理的这个网站一般只在5-20M之间。数据库尺寸主要受到内容数量和模块的影响。&lt;/p&gt;

&lt;p&gt;然而，我曾经被一条主机商发来的通知吓了一跳，说我的网站使用了超过2G的存储，我想一定是出了什么问题。&lt;/p&gt;

&lt;p&gt;这里我会分享我解决这个问题的一些心得，我想如果你遇到这些问题，也会有相似的情况。&lt;/p&gt;

&lt;p&gt;（提示一下，可以使用&lt;a href=&#34;http://drush.ws/#sql-cli&#34; target=&#34;_blank&#34;&gt;drush sqlc&lt;/a&gt;来在你的网站数据库上运行SQL命令）&lt;/p&gt;

&lt;p&gt;##数据库层面&lt;/p&gt;

&lt;p&gt;找到受影响的数据库。一般来说，每个网站对应一个数据库——除非你在使用一个多站点配置。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT
    table_schema &amp;quot;DB Name&amp;quot;,
    ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) &amp;quot;DB Size in MB&amp;quot; 
FROM information_schema.tables
GROUP BY table_schema;

+--------------------+---------------+
| DB Name            | DB Size in MB |
+--------------------+---------------+
| information_schema |           0.2 |
| drupal_database    |        1110.2 |
+--------------------+---------------+
2 rows in set (0.01 sec)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里你会发现，我的数据库用户只能看到两个数据库，*information_schema*是一个内部数据库，主数据库叫做drupal_database&lt;/p&gt;

&lt;p&gt;##数据表层面&lt;/p&gt;

&lt;p&gt;下一步是来查明让数据库膨胀至此的原因。对数据库中的表进行排查。&lt;/p&gt;

&lt;p&gt;我发现*&amp;ldquo;order by size&amp;rdquo;*很有用，他只会显示五个最大的表，这五个表很可能就是问题所在。如果不是这样，那可能是个好消息——你的网站已经成长到了如此规模。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT
    table_name,
    table_rows,
    data_length,
    index_length,
    ROUND(((data_length + index_length) / 1024 / 1024),2) &#39;Table Size in MB&#39;
FROM information_schema.tables
WHERE table_type = &#39;BASE TABLE&#39;
ORDER BY data_length DESC
LIMIT 5;

+---------------------+------------+-------------+--------------+------------------+
| table_name          | table_rows | data_length | index_length | Table Size in MB |
+---------------------+------------+-------------+--------------+------------------+
| queue               |     137362 |  1137704424 |      4017152 |          1088.83 |
| field_revision_body |       1731 |     5259276 |       154624 |             5.16 |
| field_data_body     |       1731 |     5259276 |       145408 |             5.15 |
| feeds_log           |      27455 |     2712868 |      1131520 |             3.67 |
| menu_router         |        401 |      406128 |        76800 |             0.46 |
+---------------------+------------+-------------+--------------+------------------+
5 rows in set (0.01 sec)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在我的例子中，*queue*表占用了98%的空间，这就是问题了。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://api.drupal.org/api/drupal/modules!system!system.queue.inc/group/queue/7&#34; target=&#34;_blank&#34;&gt;Drupal使用queue表来存储将被cron运行的任务&lt;/a&gt;，如果cron成功运行，那么这里应该是个空表，或者在上次cron运行之后产生的很少几条记录。&lt;/p&gt;

&lt;p&gt;##任务层面&lt;/p&gt;

&lt;p&gt;为了查明事实，我查看了一下这个表中的task类型。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT
    name,
    COUNT(1)
FROM queue
GROUP BY name;

+---------------------+----------+
| name                | COUNT(1) |
+---------------------+----------+
| feeds_source_import |   137498 |
+---------------------+----------+
1 row in set (0.07 sec)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里只剩下一条结果，是&lt;a href=&#34;https://drupal.org/project/feeds&#34; target=&#34;_blank&#34;&gt;feeds模块&lt;/a&gt;用于从rss导入内容创建的任务。检查了一下这个导入设置，我发现，这个任务被配置为15分钟一次，然而cron被配置为每小时运行一次。这意味着，每次cron运行，是无法完成所有的任务的，所以任务数据就会持续增长下去了。&lt;/p&gt;

&lt;p&gt;解决问题也很简单，提高cron的运行频率，降低导入触发频率，来确保cron能够顺利完成所有任务。&lt;/p&gt;

&lt;p&gt;这里对膨胀的表的处理还有一点遗留问题，如果cron能够顺利完成，数据表应该恢复正常大小。如果你不放心，可以直接truncate这张表，当然是在你知道这一行为的后果是否会影响必要任务的情况下。&lt;/p&gt;

&lt;p&gt;##基准测试&lt;/p&gt;

&lt;p&gt;下面是同一个查询在一个&amp;rdquo;正常&amp;rdquo;的网站下的运行结果。&lt;/p&gt;

&lt;p&gt;这里你会看到，没有什么数据在尺寸上鹤立鸡群，这个数据库只有10M。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;+--------------------+---------------+
| DB Name            | DB Size in MB |
+--------------------+---------------+
| information_schema |           0.2 |
| drupal_database    |           6.1 |
+--------------------+---------------+
2 rows in set (0.12 sec)

+---------------------+------------+-------------+--------------+------------+
| TABLE_NAME          | table_rows | data_length | index_length | Size in MB |
+---------------------+------------+-------------+--------------+------------+
| field_revision_body |        423 |     1556716 |        47104 |       1.53 |
| menu_router         |        450 |      425076 |        80896 |       0.48 |
| system              |        390 |      313544 |        73728 |       0.37 |
| field_data_body     |         37 |      135968 |        18432 |       0.15 |
| registry            |        965 |       94016 |        53248 |       0.14 |
+---------------------+------------+-------------+--------------+------------+
5 rows in set (0.02 sec)

+----------+
| count(1) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
  </channel>
</rss>
