分析Mysql 5.6的Dockerfile


Docker官方的Mysql鏡像的Dockerfile托管在Github上,地址如下:

https://github.com/docker-library/mysql/tree/5836bc9af9deb67b68c32bebad09a0f7513da36e/5.6

仔細研究了一下其Dockerfile,發現最有技術含量的倒不是其Dockerfile本身,無非是更新軟件倉庫,下載Mysql Server,稍微修改一下其配置文件。

倒是ENTRYPOINT對應的docker-entrypoint.sh很耐人尋味,這個文件相當詳實,涉及了Mysql如何初始化,如何設置密碼,如何啟動服務等關鍵問題。

今天花費了大半天來分析這個腳本,果然是受益匪淺。

腳本及分析結果如下:

#!/bin/bash
set -e

# if command starts with an option, prepend mysqld
if [ "${1:0:1}" = '-' ]; then
    set -- mysqld "$@"
fi

if [ "$1" = 'mysqld' ]; then
    # Get config
    DATADIR="$("$@" --verbose --help 2>/dev/null | awk '$1 == "datadir" { print $2; exit }')"

    if [ ! -d "$DATADIR/mysql" ]; then
        if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" ]; then
            echo >&2 'error: database is uninitialized and MYSQL_ROOT_PASSWORD not set'
            echo >&2 '  Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?'
            exit 1
        fi

        mkdir -p "$DATADIR"
        chown -R mysql:mysql "$DATADIR"

        echo 'Running mysql_install_db'
        mysql_install_db --user=mysql --datadir="$DATADIR" --rpm --keep-my-cnf
        echo 'Finished mysql_install_db'

        mysqld --user=mysql --datadir="$DATADIR" --skip-networking &
        pid="$!"

        mysql=( mysql --protocol=socket -uroot )

        for i in {30..0}; do
            if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then
                break
            fi
            echo 'MySQL init process in progress...'
            sleep 1
        done
        if [ "$i" = 0 ]; then
            echo >&2 'MySQL init process failed.'
            exit 1
        fi

        # 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/' | "${mysql[@]}" mysql

        "${mysql[@]}" <<-EOSQL
            -- What's done in this file shouldn't be replicated
            --  or products like mysql-fabric won't work
            SET @@SESSION.SQL_LOG_BIN=0;
            DELETE FROM mysql.user ;
            CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;
            GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;
            DROP DATABASE IF EXISTS test ;
            FLUSH PRIVILEGES ;
        EOSQL

        if [ ! -z "$MYSQL_ROOT_PASSWORD" ]; then
            mysql+=( -p"${MYSQL_ROOT_PASSWORD}" )
        fi

        if [ "$MYSQL_DATABASE" ]; then
            echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" | "${mysql[@]}"
            mysql+=( "$MYSQL_DATABASE" )
        fi

        if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then
            echo "CREATE USER '"$MYSQL_USER"'@'%' IDENTIFIED BY '"$MYSQL_PASSWORD"' ;" | "${mysql[@]}"

            if [ "$MYSQL_DATABASE" ]; then
                echo "GRANT ALL ON \`"$MYSQL_DATABASE"\`.* TO '"$MYSQL_USER"'@'%' ;" | "${mysql[@]}"
            fi

            echo 'FLUSH PRIVILEGES ;' | "${mysql[@]}"
        fi

        echo
        for f in /docker-entrypoint-initdb.d/*; do
            case "$f" in
                *.sh)  echo "$0: running $f"; . "$f" ;;
                *.sql) echo "$0: running $f"; "${mysql[@]}" < "$f" && echo ;;
                *)     echo "$0: ignoring $f" ;;
            esac
            echo
        done

        if ! kill -s TERM "$pid" || ! wait "$pid"; then
            echo >&2 'MySQL init process failed.'
            exit 1
        fi

        echo
        echo 'MySQL init process done. Ready for start up.'
        echo
    fi

    chown -R mysql:mysql "$DATADIR"
fi

exec "$@"

說明如下:

1> set -e, Manual文檔的說明如下:

Exit immediately if a pipeline (which may consist of a single simple command), a subshell com‐
mand enclosed in parentheses, or one of the commands executed as part of a command list
enclosed by braces (see SHELL GRAMMAR above) exits with a non-zero status.

這句語句告訴bash如果任何語句的執行結果不是true則應該退出。這樣的好處是防止錯誤像滾雪球般變大導致一個致命的錯誤,而這些錯誤本應該在之前就被處理掉。

關於set -e的利弊,可參考一下兩篇博客

(1)Unix/Linux 腳本中 “set -e” 的作用

(2)linux中的set命令: "set -e" 與 "set -o pipefail"

2> 

if [ "${1:0:1}" = '-' ]; then
    set -- mysqld "$@"
fi

用於判斷該腳本后面的參數是否以“-”開始,它考慮的是啟動mysqld是帶參數的情況,如果有的話,就將mysqld和參數作為變量存到$@中。

關於set --,Manual文檔的說明如下:

If no arguments follow this option, then the positional parameters are unset. Otherwise, the
positional parameters are set to the args, even if some of them begin with a -.

驗證如下:

首先構造腳本

[root@localhost ~]# cat 4.sh 
#!/bin/bash
if [ "${1:0:1}" = '-' ]; then
        set -- mysqld "$@"
fi
echo '$@: '"$@"
echo '$1: '"$1"

加入參數進行驗證

[root@localhost ~]# sh 4.sh 123
$@: 123
$1: 123
[root@localhost ~]# sh 4.sh -123
$@: mysqld -123
$1: mysqld
[root@localhost ~]# sh 4.sh -123 456
$@: mysqld -123 456
$1: mysqld

補充一點:關於$@與$*的區別,$@指每個位置參數參數都是一個獨立的""引用字串,這就意味着參數被完整地傳遞,而$*則指所有位置參數只被一個""引用,相當於一個參數。

3>

if [ "$1" = 'mysqld' ]; then

如果$1為mysqld,則執行下面的代碼,如果不是,則執行該腳本最后一行的exec "$@"。

注意:Dockerfile中的CMD命令為 ["mysqld"],CMD命令其實就是ENTRYPOINT的參數,譬如如果ENTRYPOINT命令為ls,則CMD命令為-l,則實現的效果就是ls -l,

在啟動容器時,自己輸入的命令其實是覆蓋CMD參數的,具體在本例中,就意味着在啟動容器時,自已輸入的命令只有在三種情況下才會啟動mysqld服務:一、mysqld(相當於CMD參數)。二、以“-”開始的參數列表,這樣上述2中的腳本才會判斷為真。三、mysqld + 以“-”開始的參數列表。除此之外,其它所有的命令都不會啟動mysql server服務,而是直接執行自己輸入的命令。

4>

 DATADIR="$("$@" --verbose --help 2>/dev/null | awk '$1 == "datadir" { print $2; exit }')"

獲取mysql server的數據目錄,倘若我們沒有輸入任何以“-”開始的參數列表,則$@為mysqld,上述命令執行的結果如下:

[root@localhost ~]# mysqld --verbose --help 2>/dev/null | awk '$1 == "datadir" { print $2; exit }'
/var/lib/mysql/

5> 

 if [ ! -d "$DATADIR/mysql" ]; then

如果存在/var/lib/mysql/mysql目錄存在文件,則跳過中間的步驟,直接執行chown -R mysql:mysql "$DATADIR",它這里判斷的一個依據是,如果/var/lib/mysql/mysql存在文件,則代表mysql server已經安裝,這時就無需安裝(當然,如果強行安裝的 話,可能會覆蓋),直接將其屬主修改為mysql用戶。很多童鞋可能好奇,不是新建的鏡像么?這個目錄怎么可能存在呢?我當初也存在這樣的疑惑,后來驗證了一番,發現如果將宿主機的目錄直接掛載到鏡像的/var/lib/mysql下,則啟動mysql鏡像時,沒有進行mysql的初始化,設置root密碼等,直接啟動mysql服務,具體如下:

[root@localhost ~]# docker run -v /var/lib/mysql:/var/lib/mysql mysql
2015-09-24 02:08:05 0 [Note] mysqld (mysqld 5.6.26) starting as process 1 ...
2015-09-24 02:08:05 1 [Note] Plugin 'FEDERATED' is disabled.
2015-09-24 02:08:05 1 [Note] InnoDB: Using atomics to ref count buffer pool pages
2015-09-24 02:08:05 1 [Note] InnoDB: The InnoDB memory heap is disabled
2015-09-24 02:08:05 1 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins
2015-09-24 02:08:05 1 [Note] InnoDB: Memory barrier is not used
2015-09-24 02:08:05 1 [Note] InnoDB: Compressed tables use zlib 1.2.7
2015-09-24 02:08:05 1 [Note] InnoDB: Using Linux native AIO
2015-09-24 02:08:05 1 [Note] InnoDB: Using CPU crc32 instructions
2015-09-24 02:08:05 1 [Note] InnoDB: Initializing buffer pool, size = 128.0M
2015-09-24 02:08:05 1 [Note] InnoDB: Completed initialization of buffer pool
2015-09-24 02:08:05 1 [Note] InnoDB: Highest supported file format is Barracuda.
2015-09-24 02:08:06 1 [Note] InnoDB: 128 rollback segment(s) are active.
2015-09-24 02:08:06 1 [Note] InnoDB: Waiting for purge to start
2015-09-24 02:08:06 1 [Note] InnoDB: 5.6.26 started; log sequence number 1697388
2015-09-24 02:08:06 1 [Note] Server hostname (bind-address): '*'; port: 3306
2015-09-24 02:08:06 1 [Note] IPv6 is available.
2015-09-24 02:08:06 1 [Note]   - '::' resolves to '::';
2015-09-24 02:08:06 1 [Note] Server socket created on IP: '::'.
2015-09-24 02:08:06 1 [Warning] 'user' entry 'root@localhost.localdomain' ignored in --skip-name-resolve mode.
2015-09-24 02:08:06 1 [Warning] 'user' entry '@localhost.localdomain' ignored in --skip-name-resolve mode.
2015-09-24 02:08:06 1 [Warning] 'proxies_priv' entry '@ root@localhost.localdomain' ignored in --skip-name-resolve mode.
2015-09-24 02:08:06 1 [Note] Event Scheduler: Loaded 0 events
2015-09-24 02:08:06 1 [Note] mysqld: ready for connections.
Version: '5.6.26'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)

而先前的啟動過程則涉及到初始化,啟動,顯性設置root密碼等。

將宿主機的var/lib/mysql/掛載給容器后,我們再來看看宿主機上該目錄的權限。

[root@localhost ~]# ll /var/lib/mysql/
total 110604
-rw-rw---- 1 polkitd ssh_keys       56 Sep 14 15:46 auto.cnf
drwx------ 2 polkitd ssh_keys      131 Sep 14 16:03 db1
drwx------ 2 polkitd ssh_keys      131 Sep 14 16:03 db2
drwx------ 2 polkitd ssh_keys       55 Sep 14 16:02 db3
-rw-rw---- 1 polkitd ssh_keys 12582912 Sep 24 10:08 ibdata1
-rw-rw---- 1 polkitd ssh_keys 50331648 Sep 24 10:08 ib_logfile0
-rw-rw---- 1 polkitd ssh_keys 50331648 Sep 14 15:45 ib_logfile1
drwx------ 2 polkitd ssh_keys     4096 Sep 23 14:18 mysql
drwx------ 2 polkitd ssh_keys     4096 Sep 22 13:47 performance_schema

屬主為polkitd,屬組為ssh_keys。

此時,需顯性將/var/lib/mysql的屬主和屬組恢復為mysql,即chown -R mysql:mysql /var/lib/mysql/,不然,宿主機的mysql服務將無法啟動。

 

6> 

 if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" ]; then
            echo >&2 'error: database is uninitialized and MYSQL_ROOT_PASSWORD not set'
            echo >&2 '  Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?'
            exit 1
 fi

設置mysql的root賬戶的密碼,其中-z判斷是否為空字符串,-a 兩個條件同時滿足,才為true。從這里也可以看出來,隨意給MYSQL_ALLOW_EMPTY_PASSWORD賦一個值,都可以實現無密碼登錄。

7>

        mkdir -p "$DATADIR"
        chown -R mysql:mysql "$DATADIR"

        echo 'Running mysql_install_db'
        mysql_install_db --user=mysql --datadir="$DATADIR" --rpm --keep-my-cnf
        echo 'Finished mysql_install_db'

        mysqld --user=mysql --datadir="$DATADIR" --skip-networking &
        pid="$!"

創建/var/lib/mysql,同時將其屬主和屬組設置為mysql,然后初始化數據庫,最后用mysqld命令啟動數據庫。$!指的是Shell最后運行的后台Process的PID。

8> 

        mysql=( mysql --protocol=socket -uroot )

        for i in {30..0}; do
            if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then
                break
            fi
            echo 'MySQL init process in progress...'
            sleep 1
        done
        if [ "$i" = 0 ]; then
            echo >&2 'MySQL init process failed.'
            exit 1
        fi

這段代碼主要是利用mysql客戶端測試mysql服務是否啟動。這里面利用括號()構造mysql變量的方式挺有意思的,以前沒有見過。特意驗證了一下:

[root@localhost ~]# mysql=( mysql --protocol=socket -uroot )
[root@localhost ~]# echo ${mysql}
mysql
[root@localhost ~]# echo ${mysql[@]}
mysql --protocol=socket -uroot

這段代碼給了30s的時間來判斷mysql服務是否已啟動,如果啟動了,則退出循環,如果沒有啟動,循環結束后,變量i的值為0,通過后續的if語句,屏幕輸出“MySQL init process failed”。

這里判斷mysql服務是否啟動的方式蠻有意思的,

[root@localhost ~]# mysql=( mysql --protocol=socket -uroot )
[root@localhost ~]# echo 'SELECT 1' | "${mysql[@]}" 
1
1
[root@localhost ~]# mysql
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 11
Server version: 5.6.26 MySQL Community Server (GPL)
mysql> select 1;
+---+
| 1 |
+---+
| 1 |
+---+
1 row in set (0.01 sec)

如果停掉了mysql服務,則輸出如下:

[root@localhost ~]# systemctl stop mysqld
[root@localhost ~]# echo 'SELECT 1' | "${mysql[@]}" 
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2)

在這里,還有一點很讓人疑惑,

 if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then
    break
 fi

if判斷的條件難道不是echo 'SELECT 1' | "${mysql[@]}"命令的返回碼么?如果是這樣的話,成功執行,則$?為0,此時不應該執行break語句的,但事實與推測的相反。關於這點暫且留下。

9> 

mysql_tzinfo_to_sql /usr/share/zoneinfo | sed 's/Local time zone must be set--see zic manual page/FCTY/' | "${mysql[@]}" mysql

修改mysql關於時區的一個bug,不去深究。

10> 

       "${mysql[@]}" <<-EOSQL
            -- What's done in this file shouldn't be replicated
            --  or products like mysql-fabric won't work
            SET @@SESSION.SQL_LOG_BIN=0;
            DELETE FROM mysql.user ;
            CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;
            GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;
            DROP DATABASE IF EXISTS test ;
            FLUSH PRIVILEGES ;
        EOSQL

這段主要是用客戶端登錄數據庫進行相關操作,包括修改root密碼,為其授權,刪除測試數據庫等。SET @@SESSION.SQL_LOG_BIN=0的作用是停止使用日志文件,這點不是很明白。

11> 

        if [ ! -z "$MYSQL_ROOT_PASSWORD" ]; then
            mysql+=( -p"${MYSQL_ROOT_PASSWORD}" )
        fi

        if [ "$MYSQL_DATABASE" ]; then
            echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" | "${mysql[@]}"
            mysql+=( "$MYSQL_DATABASE" )
        fi

        if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then
            echo "CREATE USER '"$MYSQL_USER"'@'%' IDENTIFIED BY '"$MYSQL_PASSWORD"' ;" | "${mysql[@]}"

            if [ "$MYSQL_DATABASE" ]; then
                echo "GRANT ALL ON \`"$MYSQL_DATABASE"\`.* TO '"$MYSQL_USER"'@'%' ;" | "${mysql[@]}"
            fi

            echo 'FLUSH PRIVILEGES ;' | "${mysql[@]}"
        fi

這段代碼主要是創建數據庫,新建mysql用戶,並授權。這其實意味着用戶在啟動容器時可以通過指定MYSQL_DATABASE參數來創建數據庫,通過MYSQL_USER和MYSQL_PASSWORD來創建新的數據庫用戶。

其中,mysql+=( -p"${MYSQL_ROOT_PASSWORD}" )用於拼接變量。譬如:

[root@localhost ~]# mysql=( mysql --protocol=socket -uroot )
[root@localhost ~]# MYSQL_DATABASE=docker
[root@localhost ~]# mysql+=( "$MYSQL_DATABASE" )
[root@localhost ~]# echo ${mysql[@]}
mysql --protocol=socket -uroot docker

12>

       for f in /docker-entrypoint-initdb.d/*; do
            case "$f" in
                *.sh)  echo "$0: running $f"; . "$f" ;;
                *.sql) echo "$0: running $f"; "${mysql[@]}" < "$f" && echo ;;
                *)     echo "$0: ignoring $f" ;;
            esac
            echo
        done      

其它需要執行的shell腳本或者sql腳本,可放到/docker-entrypoint-initdb.d/目錄下。只需啟動容器時,通過-v參數將容器該目錄掛載到宿主機目錄上。

13> 

        if ! kill -s TERM "$pid" || ! wait "$pid"; then
            echo >&2 'MySQL init process failed.'
            exit 1
        fi

關閉已啟動的mysql server,可能很多人會感到好奇,不是提供mysql服務么?為什么還要關閉呢?

答案就在於dockerfile中CMD的命令為 ["mysqld"],如果不關閉的話,這里就無法啟動。

這里比較有意思的還是在於腳本部分,

一、關於kill,kill命令是通過向進程發送指定的信號來結束進程的。

-s的意思是Specify the signal to send.  The signal may be given as a signal name or number.指定需要發送的信號.

如果沒有指定發送信號,那么默認值為TERM信號。

關於-TERM和-9的區別

kill -TERM PID:TERM是請求徹底終止某項執行操作.它期望接收進程清除自給的狀態並退出,它是一種較溫和的方式。

kill -9 PID:

這個強大和危險的命令迫使進程在運行時突然終止,進程在結束后不能自我清理。危害是導致系統資源無法正常釋放,一般不推薦使用,除非其他辦法都無效。
當使用此命令時,一定要通過ps -ef確認沒有剩下任何僵屍進程。只能通過終止父進程來消除僵屍進程。如果僵屍進程被init收養,問題就比較嚴重了。殺死init進程意味着關閉系統。
如果系統中有僵屍進程,並且其父進程是init,而且僵屍進程占用了大量的系統資源,那么就需要在某個時候重啟機器以清除進程表

二、關於||運算符

關於&&和||的區別

command1 && command2 :左邊的命令(命令1)返回真(即返回0,成功被執行)后,&&右邊的命令(命令2)才能夠被執行。

command1 || command2:左邊的命令(命令1)執行失敗了,就執行右邊的命令(命令2)。

三、關於wait

wait命令用來等待指令的完成,直到其執行完畢后返回終端。

所以這段腳本的邏輯是首先用kill -s TERM "$pid"的方式關閉mysqld進程,如果執行成功了,則! kill -s TERM "$pid"的結果為false,這時候就執行wait "$pid",wait是等待mysqld的關閉,mysqld關閉完畢后,wait "$pid"結果為真,此時! wait "$pid"結果為假。echo語句就不執行。

至此,分析完畢~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM