以下内容均来自个人笔记并重新梳理,如有错误欢迎指正!如果对您有帮助,烦请点赞、关注、转发!欢迎扫码关注个人公众号!
公众号原文链接:MySQL 源码构建 Docker 镜像(基于 ARM 64 架构)
背景介绍
近期,笔者正推进公司 MySQL 适配 ARM 64 架构工作,由于一直使用 Docker Hub 上的官方镜像,所以第一时间在 Hub 上检索,却发现官方只为 MySQL 8.0 以上版本提供 ARM 64 镜像。
为避免 MySQL 版本变动带来的研发改造成本,笔者最终决定针对 MySQL 5.7.40 版本自行构建 ARM 64 镜像,以下为完整过程。
过程回顾
1、获取 Dockerfile
使用 dfimage 工具获取 MySQL 5.7.40 官方镜像原始的 Dockerfile。
alias dfimage="docker run -v /var/run/docker.sock:/var/run/docker.sock --rm alpine/dfimage"
dfimage -sV=1.36 mysql:5.7.40 > /root/mysql_rebuild/Dockerfile
这里需要说明的是,该 Dockerfile 中是通过 yum 安装 mysql 服务,经过实测其指定的 yum 源也未提供 5.7.40 版本的 ARM 64 rpm文件,因此还需要使用源码自行编译生成 rpm,并修改 Dockerfile 中的安装逻辑。
附 yum 源地址:https://repo.mysql.com/yum
2、编译源码生成 rpm
2.1、在 ARM 环境安装依赖
yum install -y cmake time libaio-devel ncurses-devel numactl-devel openssl-devel zlib-devel \
cyrus-sasl-devel openldap-devel perl-Env gcc gcc-c++ make rpm-build autoconf perl-JSON
2.2、下载源码 rpm 并安装
wget https://repo.mysql.com/yum/mysql-5.7-community/docker/el/7/SRPMS/mysql-community-minimal-5.7.40-1.el7.src.rpm
rpm -ivh mysql-community-minimal-5.7.40-1.el7.src.rpm
2.3、修改源码配置
cd /root/rpmbuild/SOURCES/
tar -xzvf mysql-5.7.40.tar.gz && rm -rf mysql-5.7.40.tar.gz
vim mysq1-5.7.40/sql/mysqld.cc,添加 #include
sed -i "s#-O3#-O1#g" mysql-5.7.40/cmake/build_configurations/compiler_options.cmake
tar -czvf mysql-5.7.40.tar.gz mysql-5.7.40 --remove-files
2.4、编译源码生成 rpm
rpmbuild -bb /root/rpmbuild/SPECS/mysql.spec
生成的 rpm 文件位于 /root/rpmbuild/RPMS/aarch64 目录下
3、构建镜像
cd /root/mysql_rebuild && tree
目录结构如下,部分文件的详细内容见附录:
├── Dockerfile
├── aarch64
│ ├── mysql-community-minimal-debuginfo-5.7.40-1.el7.aarch64.rpm
│ └── mysql-community-server-minimal-5.7.40-1.el7.aarch64.rpm
├── docker-entrypoint.sh
├── gosu
├── gosu.asc
├── my.cnf
└── mysql-shell-8.0.12-1.el7.aarch64.rpm
docker build --pull --platform=linux/arm64 -t mysql:5.7.40-armtest -f Dockerfile . --no-cache
附录
1、Dockerfile
FROM oraclelinux:7-slim
CMD ["/bin/bash"]
RUN set -eux; groupadd --system --gid 999 mysql; useradd --system --uid 999 --gid 999 --home-dir /var/lib/mysql --no-create-home mysql
ENV GOSU_VERSION=1.14
# 此处提前将 gosu 原文件下载好,缩短构建时间
# wget -O gosu.asc https://github.com/tianon/gosu/releases/download/1.14/gosu-arm64.asc
# wget -O gosu https://github.com/tianon/gosu/releases/download/1.14/gosu-arm64
COPY gosu.asc gosu /usr/local/bin
RUN set -eux; export GNUPGHOME="$(mktemp -d)"; gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; chmod +x /usr/local/bin/gosu; gosu --version; gosu nobody true; yum install -y --setopt=skip_missing_names_on_install=False oracle-epel-release-el7; yum install -y --setopt=skip_missing_names_on_install=False bzip2 gzip openssl xz zstd; yum clean all
ENV MYSQL_MAJOR=5.7
ENV MYSQL_VERSION=5.7.40-1.el7
# 此处提前将 rpm 原文件下载好,缩短构建时间
COPY aarch64 mysql-shell-8.0.12-1.el7.aarch64.rpm /tmp
COPY my.cnf /etc/my.cnf
RUN set -eux; yum install -y libaio numactl; cd /tmp && rpm -ivh --force mysql-community-minimal-debuginfo-5.7.40-1.el7.aarch64.rpm mysql-community-server-minimal-5.7.40-1.el7.aarch64.rpm; yum clean all; mkdir -p /etc/mysql/conf.d; mkdir -p /etc/mysql/mysql.conf.d; find /etc/my.cnf /etc/mysql/ -name '*.cnf' -print0 | xargs -0 grep -lZE '^(bind-address|log)' | xargs -rt -0 sed -Ei 's/^(bind-address|log)/#&/'; mkdir -p /var/lib/mysql /var/run/mysqld; chown mysql:mysql /var/lib/mysql /var/run/mysqld; chmod 1777 /var/lib/mysql /var/run/mysqld; mkdir /docker-entrypoint-initdb.d; mysqld --version; mysql --version
ENV MYSQL_SHELL_VERSION=8.0.31-1.el7
RUN set -eux; rpm -ivh /tmp/mysql-shell-8.0.12-1.el7.aarch64.rpm; rm -rf /tmp;mkdir /tmp; chmod -R 777 /tmp; yum clean all; mysqlsh --version
# /var/lib/mysql 需要添加引号,否则运行时报错 mount destination [/var/lib/mysql] not absolute: unknown
VOLUME ["/var/lib/mysql"]
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN ln -s /usr/local/bin/docker-entrypoint.sh /entrypoint.sh # backwards compat
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 3306 33060
CMD ["mysqld"]
2、docker-entrypoint.sh
说明1:EOF 块代码的缩进做了调整,避免报错 warning: here-document at line 161 delimited by end-of-file (wanted `EOF')。
说明2:在原始内容基础上单独添加了 391 行,以解决Kylin V10兼容性问题,有空单独出一篇文讲一下其中的坑。
#!/bin/bash
set -eo pipefail
shopt -s nullglob
# logging functions
mysql_log() {
local type="$1"; shift
# accept argument string or stdin
local text="$*"; if [ "$#" -eq 0 ]; then text="$(cat)"; fi
local dt; dt="$(date --rfc-3339=seconds)"
printf '%s [%s] [Entrypoint]: %s\n' "$dt" "$type" "$text"
}
mysql_note() {
mysql_log Note "$@"
}
mysql_warn() {
mysql_log Warn "$@" >&2
}
mysql_error() {
mysql_log ERROR "$@" >&2
exit 1
}
# usage: file_env VAR [DEFAULT]
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
mysql_error "Both $var and $fileVar are set (but are exclusive)"
fi
local val="$def"
if [ "${!var:-}" ]; then
val="${!var}"
elif [ "${!fileVar:-}" ]; then
val="$(< "${!fileVar}")"
fi
export "$var"="$val"
unset "$fileVar"
}
# check to see if this file is being run or sourced from another script
_is_sourced() {
# https://unix.stackexchange.com/a/215279
[ "${#FUNCNAME[@]}" -ge 2 ] \
&& [ "${FUNCNAME[0]}" = '_is_sourced' ] \
&& [ "${FUNCNAME[1]}" = 'source' ]
}
# usage: docker_process_init_files [file [file [...]]]
# ie: docker_process_init_files /always-initdb.d/*
# process initializer files, based on file extensions
docker_process_init_files() {
# mysql here for backwards compatibility "${mysql[@]}"
mysql=( docker_process_sql )
echo
local f
for f; do
case "$f" in
*.sh)
# https://github.com/docker-library/postgres/issues/450#issuecomment-393167936
# https://github.com/docker-library/postgres/pull/452
if [ -x "$f" ]; then
mysql_note "$0: running $f"
"$f"
else
mysql_note "$0: sourcing $f"
. "$f"
fi
;;
*.sql) mysql_note "$0: running $f"; docker_process_sql < "$f"; echo ;;
*.sql.bz2) mysql_note "$0: running $f"; bunzip2 -c "$f" | docker_process_sql; echo ;;
*.sql.gz) mysql_note "$0: running $f"; gunzip -c "$f" | docker_process_sql; echo ;;
*.sql.xz) mysql_note "$0: running $f"; xzcat "$f" | docker_process_sql; echo ;;
*.sql.zst) mysql_note "$0: running $f"; zstd -dc "$f" | docker_process_sql; echo ;;
*) mysql_warn "$0: ignoring $f" ;;
esac
echo
done
}
# arguments necessary to run "mysqld --verbose --help" successfully (used for testing configuration validity and for extracting default/configured values)
_verboseHelpArgs=(
--verbose --help
--log-bin-index="$(mktemp -u)" # https://github.com/docker-library/mysql/issues/136
)
mysql_check_config() {
local toRun=( "$@" "${_verboseHelpArgs[@]}" ) errors
if ! errors="$("${toRun[@]}" 2>&1 >/dev/null)"; then
mysql_error $'mysqld failed while attempting to check config\n\tcommand was: '"${toRun[*]}"$'\n\t'"$errors"
fi
}
# Fetch value from server config
# We use mysqld --verbose --help instead of my_print_defaults because the
# latter only show values present in config files, and not server defaults
mysql_get_config() {
local conf="$1"; shift
"$@" "${_verboseHelpArgs[@]}" 2>/dev/null \
| awk -v conf="$conf" '$1 == conf && /^[^ \t]/ { sub(/^[^ \t]+[ \t]+/, ""); print; exit }'
# match "datadir /some/path with/spaces in/it here" but not "--xyz=abc\n datadir (xyz)"
}
# Ensure that the package default socket can also be used
# since rpm packages are compiled with a different socket location
# and "mysqlsh --mysql" doesn't read the [client] config
# related to https://github.com/docker-library/mysql/issues/829
mysql_socket_fix() {
local defaultSocket
defaultSocket="$(mysql_get_config 'socket' mysqld --no-defaults)"
if [ "$defaultSocket" != "$SOCKET" ]; then
ln -sfTv "$SOCKET" "$defaultSocket" || :
fi
}
# Do a temporary startup of the MySQL server, for init purposes
docker_temp_server_start() {
if [ "${MYSQL_MAJOR}" = '5.7' ]; then
"$@" --skip-networking --default-time-zone=SYSTEM --socket="${SOCKET}" &
mysql_note "Waiting for server startup"
local i
for i in {30..0}; do
# only use the root password if the database has already been initialized
# so that it won't try to fill in a password file when it hasn't been set yet
extraArgs=()
if [ -z "$DATABASE_ALREADY_EXISTS" ]; then
extraArgs+=( '--dont-use-mysql-root-password' )
fi
if docker_process_sql "${extraArgs[@]}" --database=mysql <<<'SELECT 1' &> /dev/null; then
break
fi
sleep 1
done
if [ "$i" = 0 ]; then
mysql_error "Unable to start server."
fi
else
# For 5.7+ the server is ready for use as soon as startup command unblocks
if ! "$@" --daemonize --skip-networking --default-time-zone=SYSTEM --socket="${SOCKET}"; then
mysql_error "Unable to start server."
fi
fi
}
# Stop the server. When using a local socket file mysqladmin will block until
# the shutdown is complete.
docker_temp_server_stop() {
if ! mysqladmin --defaults-extra-file=<( _mysql_passfile ) shutdown -uroot --socket="${SOCKET}"; then
mysql_error "Unable to shut down server."
fi
}
# Verify that the minimally required password settings are set for new databases.
docker_verify_minimum_env() {
if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
mysql_error < Database is uninitialized and password option is not specified You need to specify one of the following as an environment variable: - MYSQL_ROOT_PASSWORD - MYSQL_ALLOW_EMPTY_PASSWORD - MYSQL_RANDOM_ROOT_PASSWORD EOF fi # This will prevent the CREATE USER from failing (and thus exiting with a half-initialized database) if [ "$MYSQL_USER" = 'root' ]; then mysql_error < MYSQL_USER="root", MYSQL_USER and MYSQL_PASSWORD are for configuring a regular user and cannot be used for the root user Remove MYSQL_USER="root" and use one of the following to control the root user password: - MYSQL_ROOT_PASSWORD - MYSQL_ALLOW_EMPTY_PASSWORD - MYSQL_RANDOM_ROOT_PASSWORD EOF fi # warn when missing one of MYSQL_USER or MYSQL_PASSWORD if [ -n "$MYSQL_USER" ] && [ -z "$MYSQL_PASSWORD" ]; then mysql_warn 'MYSQL_USER specified, but missing MYSQL_PASSWORD; MYSQL_USER will not be created' elif [ -z "$MYSQL_USER" ] && [ -n "$MYSQL_PASSWORD" ]; then mysql_warn 'MYSQL_PASSWORD specified, but missing MYSQL_USER; MYSQL_PASSWORD will be ignored' fi } # creates folders for the database # also ensures permission for user mysql of run as root docker_create_db_directories() { local user; user="$(id -u)" local -A dirs=( ["$DATADIR"]=1 ) local dir dir="$(dirname "$SOCKET")" dirs["$dir"]=1 # "datadir" and "socket" are already handled above (since they were already queried previously) local conf for conf in \ general-log-file \ keyring_file_data \ pid-file \ secure-file-priv \ slow-query-log-file \ ; do dir="$(mysql_get_config "$conf" "$@")" # skip empty values if [ -z "$dir" ] || [ "$dir" = 'NULL' ]; then continue fi case "$conf" in secure-file-priv) # already points at a directory ;; *) # other config options point at a file, but we need the directory dir="$(dirname "$dir")" ;; esac dirs["$dir"]=1 done mkdir -p "${!dirs[@]}" if [ "$user" = "0" ]; then # this will cause less disk access than `chown -R` find "${!dirs[@]}" \! -user mysql -exec chown --no-dereference mysql '{}' \; fi } # initializes the database directory docker_init_database_dir() { mysql_note "Initializing database files" "$@" --initialize-insecure --default-time-zone=SYSTEM mysql_note "Database files initialized" } # Loads various settings that are used elsewhere in the script # This should be called after mysql_check_config, but before any other functions docker_setup_env() { # Get config declare -g DATADIR SOCKET DATADIR="$(mysql_get_config 'datadir' "$@")" SOCKET="$(mysql_get_config 'socket' "$@")" # Initialize values that might be stored in a file file_env 'MYSQL_ROOT_HOST' '%' file_env 'MYSQL_DATABASE' file_env 'MYSQL_USER' file_env 'MYSQL_PASSWORD' file_env 'MYSQL_ROOT_PASSWORD' declare -g DATABASE_ALREADY_EXISTS if [ -d "$DATADIR/mysql" ]; then DATABASE_ALREADY_EXISTS='true' fi } # Execute sql script, passed via stdin # usage: docker_process_sql [--dont-use-mysql-root-password] [mysql-cli-args] # ie: docker_process_sql --database=mydb <<<'INSERT ...' # ie: docker_process_sql --dont-use-mysql-root-password --database=mydb docker_process_sql() { passfileArgs=() if [ '--dont-use-mysql-root-password' = "$1" ]; then passfileArgs+=( "$1" ) shift fi # args sent in can override this db, since they will be later in the command if [ -n "$MYSQL_DATABASE" ]; then set -- --database="$MYSQL_DATABASE" "$@" fi mysql --defaults-extra-file=<( _mysql_passfile "${passfileArgs[@]}") --protocol=socket -uroot -hlocalhost --socket="${SOCKET}" --comments "$@" } # Initializes database with timezone info and root password, plus optional extra db/user docker_setup_db() { # Load timezone info into database if [ -z "$MYSQL_INITDB_SKIP_TZINFO" ]; then # sed is for https://bugs.mysql.com/bug.php?id=20545 mysql_tzinfo_to_sql /usr/share/zoneinfo \ | sed 's/Local time zone must be set--see zic manual page/FCTY/' \ | docker_process_sql --dont-use-mysql-root-password --database=mysql # tell docker_process_sql to not use MYSQL_ROOT_PASSWORD since it is not set yet fi # Generate random root password if [ -n "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then MYSQL_ROOT_PASSWORD="$(openssl rand -base64 24)"; export MYSQL_ROOT_PASSWORD mysql_note "GENERATED ROOT PASSWORD: $MYSQL_ROOT_PASSWORD" fi # Sets root password and creates root users for non-localhost hosts local rootCreate= # default root to listen for connections from anywhere if [ -n "$MYSQL_ROOT_HOST" ] && [ "$MYSQL_ROOT_HOST" != 'localhost' ]; then # no, we don't care if read finds a terminating character in this heredoc # https://unix.stackexchange.com/questions/265149/why-is-set-o-errexit-breaking-this-read-heredoc-expression/265151#265151 read -r -d '' rootCreate < CREATE USER 'root'@'${MYSQL_ROOT_HOST}' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; GRANT ALL ON *.* TO 'root'@'${MYSQL_ROOT_HOST}' WITH GRANT OPTION ; EOSQL fi local passwordSet= # no, we don't care if read finds a terminating character in this heredoc (see above) read -r -d '' passwordSet < ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; EOSQL # tell docker_process_sql to not use MYSQL_ROOT_PASSWORD since it is just now being set docker_process_sql --dont-use-mysql-root-password --database=mysql < -- What's done in this file shouldn't be replicated -- or products like mysql-fabric won't work SET @@SESSION.SQL_LOG_BIN=0; ${passwordSet} GRANT ALL ON *.* TO 'root'@'localhost' WITH GRANT OPTION ; FLUSH PRIVILEGES ; ${rootCreate} DROP DATABASE IF EXISTS test ; EOSQL # Creates a custom database and user if specified if [ -n "$MYSQL_DATABASE" ]; then mysql_note "Creating database ${MYSQL_DATABASE}" docker_process_sql --database=mysql <<<"CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" fi if [ -n "$MYSQL_USER" ] && [ -n "$MYSQL_PASSWORD" ]; then mysql_note "Creating user ${MYSQL_USER}" docker_process_sql --database=mysql <<<"CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" if [ -n "$MYSQL_DATABASE" ]; then mysql_note "Giving user ${MYSQL_USER} access to schema ${MYSQL_DATABASE}" docker_process_sql --database=mysql <<<"GRANT ALL ON \`${MYSQL_DATABASE//_/\\_}\`.* TO '$MYSQL_USER'@'%' ;" fi fi } _mysql_passfile() { # echo the password to the "file" the client uses # the client command will use process substitution to create a file on the fly # ie: --defaults-extra-file=<( _mysql_passfile ) if [ '--dont-use-mysql-root-password' != "$1" ] && [ -n "$MYSQL_ROOT_PASSWORD" ]; then cat < [client] password="${MYSQL_ROOT_PASSWORD}" EOF fi } # Mark root user as expired so the password must be changed before anything # else can be done (only supported for 5.6+) mysql_expire_root_user() { if [ -n "$MYSQL_ONETIME_PASSWORD" ]; then docker_process_sql --database=mysql < ALTER USER 'root'@'%' PASSWORD EXPIRE; EOSQL fi } # check arguments for an option that would cause mysqld to stop # return true if there is one _mysql_want_help() { local arg for arg; do case "$arg" in -'?'|--help|--print-defaults|-V|--version) return 0 ;; esac done return 1 } _main() { # if command starts with an option, prepend mysqld if [ "${1:0:1}" = '-' ]; then set -- mysqld "$@" fi # skip setup if they aren't running mysqld or want an option that stops mysqld if [ "$1" = 'mysqld' ] && ! _mysql_want_help "$@"; then mysql_note "Entrypoint script for MySQL Server ${MYSQL_VERSION} started." # 解决Kylin V10兼容性问题 ulimit -n 1048576 && ulimit -a >/dev/null && ulimit -n mysql_check_config "$@" # Load various environment variables docker_setup_env "$@" docker_create_db_directories "$@" # If container is started as root user, restart as dedicated mysql user if [ "$(id -u)" = "0" ]; then mysql_note "Switching to dedicated user 'mysql'" exec gosu mysql "$BASH_SOURCE" "$@" fi # there's no database, so it needs to be initialized if [ -z "$DATABASE_ALREADY_EXISTS" ]; then docker_verify_minimum_env # check dir permissions to reduce likelihood of half-initialized database ls /docker-entrypoint-initdb.d/ > /dev/null docker_init_database_dir "$@" mysql_note "Starting temporary server" docker_temp_server_start "$@" mysql_note "Temporary server started." mysql_socket_fix docker_setup_db docker_process_init_files /docker-entrypoint-initdb.d/* mysql_expire_root_user mysql_note "Stopping temporary server" docker_temp_server_stop mysql_note "Temporary server stopped" echo mysql_note "MySQL init process done. Ready for start up." echo else mysql_socket_fix fi fi exec "$@" } # If we are sourced from elsewhere, don't perform any further actions if ! _is_sourced; then _main "$@" fi 3、my.cnf # For advice on how to change settings please see # http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html [mysqld] # # Remove leading # and set to the amount of RAM for the most important data # cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. # innodb_buffer_pool_size = 128M # # Remove leading # to turn on a very important data integrity option: logging # changes to the binary log between backups. # log_bin # # Remove leading # to set options mainly useful for reporting servers. # The server defaults are faster for transactions and fast SELECTs. # Adjust sizes as needed, experiment to find the optimal values. # join_buffer_size = 128M # sort_buffer_size = 2M # read_rnd_buffer_size = 2M skip-host-cache skip-name-resolve datadir=/var/lib/mysql socket=/var/run/mysqld/mysqld.sock secure-file-priv=/var/lib/mysql-files user=mysql # Disabling symbolic-links is recommended to prevent assorted security risks symbolic-links=0 #log-error=/var/log/mysqld.log pid-file=/var/run/mysqld/mysqld.pid [client] socket=/var/run/mysqld/mysqld.sock !includedir /etc/mysql/conf.d/ !includedir /etc/mysql/mysql.conf.d/ 推荐链接
发表评论