在 TiDB 上跑 Atlassian Jira

TiDB 是一个兼容 MySQL 协议的分布式数据库,基于其分布式数据库的特征,我们可以部署出很多有有意思拓扑架构,比如在我和一些同学的家中(上海浦东,上海浦西,杭州,重庆,成都)就有一个跑在树莓派上的 TiDB 集群供内部 Metabase 使用,再比如前一阵子我们 v5.0 发布会的时候我也参与设计+搭建了一个跨 PingCAP 办公室网络的跑在树莓派上的”大集群”,供大家签到使用。

以上 Overlay 网络均由 Wireguard 实现

Jira 的母公司 Atlassian 可能对于普通用户来说没啥概念,在他们收购了 Trello (那就不得不有概念了) 了之后,似乎成为了 kanban 类节目的行业老大,Jira 就是一个企业常用的 Issue 管理平台,凭借着超高的内存占用,昂贵的售价和 Java 的风格独领风骚。(但是好用是真的,只需要创建 Issue ,Assign 给对应人,就可以持续追踪整个事情的进度了),只要他别像下图这样操作就好(啥都不记录,然后 6s 内就从 Development 到 Resolved):


TiDB 既然是一个兼容 MySQL 协议的数据库,自然我们希望把 Jira 也直接跑在上面,防止单机数据库宕机或磁盘损坏导致的潜在的不可用的问题。参考「在 Docker 中部署 Jira 和 Confluence 並連接到 Amazon RDS」一文,很自然的我们就会想到启动一个 Jira 的实例,并开始连接 TiDB,其中 Jira 的 docker-compose.yml 文件内容如下:

version: '3.3'
services:
    jira-software:
        image: atlassian/jira-software:8.16.0
        volumes:
            - ./jira_data:/var/atlassian/application-data/jira
            - ./jira_lib/mysql-connector-java-5.1.49-bin.jar:/opt/atlassian/jira/lib/mysql-connector-java-5.1.49-bin.jar
            - ./jira_lib/mysql-connector-java-5.1.49.jar:/opt/atlassian/jira/lib/mysql-connector-java-5.1.49.jar
            - ./jira_lib/mysql-connector-java-8.0.23.jar:/opt/atlassian/jira/lib/mysql-connector-java-8.0.23.jar
        ports:
            - '8080:8080'

这里 TiDB 出于简单考虑,我们直接使用 tiup playground:

tiup playground v5.0.1 --host "0.0.0.0" --tiflash 0

创建数据库:

CREATE DATABASE jiradb CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;

然后配置数据库,就可以…

换个 MySQL 8.0 试试?

咦?

通过阅读 Jira 的文档我们知道,如果使用 MySQL 数据库,需要设置一下 my.ini ,根据文档 Connecting Jira to MySQL 8.0 可以了解到,需要设置以下内容:

default-storage-engine=INNODB
character_set_server=utf8mb4
innodb_default_row_format=DYNAMIC
innodb_log_file_size=2G

所以如果你用我整理好的 Jira 可用的 MySQL 配置(在 https://github.com/n0vad3v/dockerfiles/tree/master/simple-mysql/jira 中)启动 MySQL 的话,可以通过:

SHOW VARIABLES LIKE 'character_set_server';
SELECT @@innodb_default_row_format;
SELECT @@innodb_file_format;
SELECT @@innodb_large_prefix;

发现在 MySQL 下这些参数的表现如下:

mysql> SHOW VARIABLES LIKE 'character_set_server';
+----------------------+---------+
| Variable_name        | Value   |
+----------------------+---------+
| character_set_server | utf8mb4 |
+----------------------+---------+
1 row in set (0.01 sec)mysql> SELECT @@innodb_default_row_format;
+-----------------------------+
| @@innodb_default_row_format |
+-----------------------------+
| dynamic                     |
+-----------------------------+
1 row in set (0.00 sec)mysql> SELECT @@innodb_file_format;
+----------------------+
| @@innodb_file_format |
+----------------------+
| Barracuda            |
+----------------------+
1 row in set (0.00 sec)mysql> SELECT @@innodb_large_prefix;
+-----------------------+
| @@innodb_large_prefix |
+-----------------------+
|                     1 |
+-----------------------+
1 row in set (0.00 sec)

而如果在 TiDB 下执行的话,是如下结果:

mysql> SHOW VARIABLES LIKE 'character_set_server';
+----------------------+---------+
| Variable_name        | Value   |
+----------------------+---------+
| character_set_server | utf8mb4 |
+----------------------+---------+
1 row in set (0.51 sec)mysql> SELECT @@innodb_default_row_format;
ERROR 1193 (HY000): Unknown system variable 'innodb_default_row_format'
mysql> SELECT @@innodb_file_format;
+----------------------+
| @@innodb_file_format |
+----------------------+
| Antelope             |
+----------------------+
1 row in set (0.00 sec)mysql> SELECT @@innodb_large_prefix;
+-----------------------+
| @@innodb_large_prefix |
+-----------------------+
|                     0 |
+-----------------------+
1 row in set (0.00 sec)

可以看到有些系统变量不太一致,这个时候第一反应就是:

mysql> set @@global.innodb_default_row_format = "dynamic";
ERROR 1193 (HY000): Unknown system variable 'innodb_default_row_format'
mysql> set @@global.innodb_file_format = "barracuda";
Query OK, 0 rows affected (0.03 sec)

mysql> set @@global.innodb_large_prefix = 1;
Query OK, 0 rows affected (0.00 sec)

然后重新连接确认:

mysql> SHOW VARIABLES LIKE 'character_set_server';
+----------------------+---------+
| Variable_name        | Value   |
+----------------------+---------+
| character_set_server | utf8mb4 |
+----------------------+---------+
1 row in set (0.62 sec)

mysql> SELECT @@innodb_default_row_format;
ERROR 1193 (HY000): Unknown system variable 'innodb_default_row_format'
mysql> SELECT @@innodb_file_format;
+----------------------+
| @@innodb_file_format |
+----------------------+
| barracuda            |
+----------------------+
1 row in set (0.01 sec)

mysql> SELECT @@innodb_large_prefix;
+-----------------------+
| @@innodb_large_prefix |
+-----------------------+
|                     1 |
+-----------------------+
1 row in set (0.00 sec)

看上去除了 innodb_default_row_format 以外其他的都和 MySQL 的一致了,我们再来试试看:

所以猜测可能就是 innodb_default_row_format 的不一致导致了上述问题。

Jira DB Check

由于报错信息只显示一个「This MySQL instance is not properly configured.」,而且容器日志中没有任何相关的 ERROR 日志,反正只要有任何不对 Jira 就说「not properly configured」…

既然上述猜测没法石锤,作为 Jira 的订阅用户,可以通过阅读源码的方式来判断到底 Jira 在启动的时候检查了什么。

通过 rg 可以快速找到,在 jira-project/jira-components/jira-core/src/main/resources/com/atlassian/jira/web/action/JiraWebActionSupport.properties 文件的中,这个报错信息是被定义在一个变量中了(而且一个等号两边有空格一个没有,非常迷惑)

13299:setupdb.error.mysqlVersion57.wrong.default.configuration=This MySQL instance is not properly configured. Please follow <a target="_blank" href="{0}">the documentation for MySQL 5.7 setup</a>.
13301:setupdb.error.mysqlVersion8.wrong.default.configuration = This MySQL instance is not properly configured. Please follow <a target="_blank" href="{0}">the documentation for MySQL 8 setup</a>.

继续 rg "setupdb.error.mysqlVersion57.wrong.default.configuration" ,可以发现,在 jira-project/jira-components/jira-core/src/main/java/com/atlassian/jira/web/action/setup/SetupDatabase.java 中有使用,查看相关代码可以发现:

try (Connection conn = databaseConfiguration.getDatasource().getConnection(bootstrapManager)){
    isDatabaseEmpty = databaseConfiguration.isDatabaseEmpty(bootstrapManager);
    if ("mysql57".equals(databaseConfiguration.getDatabaseType())) {
        isMySQL57VersionCorrect = new MySQL57OrLaterVersionPredicate().test(conn);
        isMySQL57ConfigurationCorrect = new MySQL57DefaultRowFormatChecker().test(conn);
    } else if ("mysql8".equals(databaseConfiguration.getDatabaseType())) {
        isMySQL8VersionCorrect = new MySQL8VersionPredicate().test(conn);
        isMySQL8ConfigurationCorrect = new MySQL8ConfigurationChecker().test(conn);
    }
} catch (BootstrapException | SQLException e) {
    ...
}

所以对于 MySQL 5.7 来说,就是:

  • MySQL57OrLaterVersionPredicate
  • MySQL57DefaultRowFormatChecker

而对于 MySQL 8.0 来说,是:

  • MySQL8VersionPredicate
  • MySQL8ConfigurationChecker

分别找到对应函数的定义,可以发现,在 jira-project/jira-components/jira-core/src/main/java/com/atlassian/jira/config/database/MySQL57OrLaterVersionPredicate.java 中,会检查 MySQL 5.7 模式下是否是真的 MySQL 5.7:

if ( major < 5 || (major == 5 && minor < 7) || major > 5) {
    return false;
} else {
    return true;
}
} catch (SQLException ex) {
    return false;
}

且在 jira-project/jira-components/jira-core/src/main/java/com/atlassian/jira/config/database/MySQL57DefaultRowFormatChecker.java 中可以看到:

try {
    return "DYNAMIC".equalsIgnoreCase(res.loadGlobalVariableFromServer("innodb_default_row_format")) &&
            "utf8mb4".equalsIgnoreCase(res.loadGlobalVariableFromServer("character_set_server")) &&
            "Barracuda".equalsIgnoreCase(res.loadGlobalVariableFromServer("innodb_file_format")) &&
            "on".equalsIgnoreCase(res.loadGlobalVariableFromServer("innodb_large_prefix"));
} catch (SQLException ex) {
    return false;
}

而对于 MySQL 8.0 模式下,我们可以在 jira-project/jira-components/jira-core/src/main/java/com/atlassian/jira/config/database/MySQL8ConfigurationChecker.java 文件中找到:

try {
    MySQLGlobalVariableResolver res = new MySQLGlobalVariableResolver(connection);
    return "DYNAMIC".equalsIgnoreCase(res.loadGlobalVariableFromServer("innodb_default_row_format")) &&
            "utf8mb4".equalsIgnoreCase(res.loadGlobalVariableFromServer("character_set_server"));
} catch (SQLException ex) {
    return false;
}

通过以上代码可以判定, Jira 在初始化数据库的时候会对数据库有以下检查:

  • MySQL 5.7 模式

    • 数据库汇报版本需要是 !(major < 5 || (major == 5 && minor < 7) || major > 5)
    • innodb_default_row_formatDYNAMIC
    • character_set_serverutf8mb4
    • innodb_file_formatBarracuda
    • innodb_large_prefixon
  • MySQL 8.0 模式

    • innodb_default_row_formatDYNAMIC
    • character_set_serverutf8mb4

这样看来可以解释上面遇到的以下问题:

  1. 在 MySQL 8.0 模式下报 MySQL 8 was selected but underlying database reports a different version. 的问题,因为 TiDB 对外展示的版本是 Server version: 5.7.25-TiDB-v5.0.1 TiDB Server (Apache License 2.0) Community Edition, MySQL 5.7 compatible ,显示的是 5.7
  2. 在 MySQL 5.7 模式下报 This MySQL instance is not properly configured. 因为 TiDB 没有 innodb_default_row_format 这个变量

Hack TiDB

通过上文源码的分析我们可以发现 Jira 在使用 MySQL 8.0 的时候对数据库的检查更少(此外,这里还有一个未展开的问题,Jira 会使用到名为 LEAD 的字段,使用 MySQL 5.7 模式会报错),所以我们决定使用 MySQL 8.0 模式来初始化 Jira。

Version

第一个问题就是 TiDB 的 Version ,我们希望 Jira 认为他是一个真正的 MySQL 8.0 ,所以需要让 TiDB “伪装”一下,通过查阅我们文档站 https://docs.pingcap.com/tidb/stable/tidb-configuration-file#server-version,可以发现有一个 server-version 的变量可以让 TiDB 的 Version 显示为一个不同的值,所以创建一个叫 8.0.toml 的文件,内容写一行:

server-version = "8.0.0-How-Ever-This-Is-TiDB-v5.0.1"

然后通过以下命令启动:

tiup playground v5.0.1 --host "0.0.0.0" --tiflash 0  --db.config ./8.0.toml

可以发现 TiDB 已经显示出了我们需要的版本:

~ # mysql -u root -h localhost -P4000
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1367
Server version: 8.0.0-How-Ever-This-Is-TiDB-v5.0.1 TiDB Server (Apache License 2.0) Community Edition, MySQL 5.7 compatible

innodb_default_row_format

接下来就是 innodb_default_row_format 这个变量的问题了,通过上文我们也发现,使用 set @@global.innodb_default_row_format = "dynamic"; 是无效的,会返回:

ERROR 1193 (HY000): Unknown system variable 'innodb_default_row_format'

所以可以判断出 TiDB 没有对应的系统变量,既然没有变量,那我们就 Mock 一个上去,在 https://github.com/pingcap/tidb/blob/master/sessionctx/variable/sysvar.go#L503 中我们可以发现 TiDB 的系统变量的定义的位置:

var defaultSysVars = []*SysVar{
	{Scope: ScopeGlobal, Name: MaxConnections, Value: "151", Type: TypeUnsigned, MinValue: 1, MaxValue: 100000, AutoConvertOutOfRange: true},
  ...
}

同时还能发现对应的注释:

// ScopeNone means the system variable can not be changed dynamically.
ScopeNone ScopeFlag = 0
// ScopeGlobal means the system variable can be changed globally.
ScopeGlobal ScopeFlag = 1 << 0
// ScopeSession means the system variable can only be changed in current session.
ScopeSession ScopeFlag = 1 << 1

考虑到之前是使用的 v5.0.1 版本的 TiDB,所以就是:

git clone https://github.com/pingcap/tidb
git checkout v5.0.1

由于我不怎么会写代码,所以只能照葫芦画瓢在 var defaultSysVars = []*SysVar{ 底下糊上一行:

{Scope: ScopeNone, Name: "innodb_default_row_format", Value: "dynamic"},

加完这一行后 make,然后把 bin/tidb-server 替换到 /root/.tiup/components/tidb/v5.0.1/tidb-server 这个文件中,然后重新启动数据库。

然后就可以正常安装使用啦~

以上。

Reference

  1. Connecting Jira to MySQL 8.0
  2. TiDB Configuration File

comments powered by Disqus