使用嵌套循环检查 shell 中两台服务器的连接

Checking connection to two servers in shell using nested loops

我在 docker-compose 中定义了 docker 环境。 它有 3 个容器。两个使用数据库,第三个使用 cli 脚本解释器。

第三个容器必须等待 2 个数据库容器接受连接。

当我只有一个数据库容器时,这很简单。 第三个容器(基于 Alpine)entrypoint.sh:

#!/bin/sh
while ! nc -z $MYSQL_HOST $MYSQL_PORT; do
    echo "Waiting for MySQL to accept connections ..."
    sleep 0.2
done
echo "MYSQL ready"

exec "$@"

现在有了第二个容器,我想使用嵌套循环来解决这个问题,但是由于 posix shell 通常不支持数组,所以我不知道具体如何。

一般的想法是创建一个字符串 host1:port1 由 space

分隔
#!/bin/sh
SERVERS="DB1_HOST:DB1_PORT DB2_HOST:DB2_PORT"

通过使用 space(我认为 IFS=' ')创建项目来遍历它,而不是使用 DB:PORT 使用 nc 检查连接并从服务器中删除 DB:PORT当连接成为可能时变量。

我主要担心的是我不知道如何从 SERVERS 变量中删除 DB:PORT。

您可以将值存储为函数参数:

#!/bin/sh

sqlup() {
    for server do
        IFS=: read -r host port

        while ! nc -z "$host" "$port"; do
            printf 'Waiting for MySQL to accept connections...\n'
            sleep 0.2
        done
    done

    printf 'MySQL ready\n'
}

sqlup DB1_HOST:DB1_PORT DB2_HOST:DB2_PORT # ...
exec "$@"

工作解决方案。使用 ASH

在 Linux Alpine 3.7 上进行测试

如果有人知道为什么脚本被破坏(检查代码中以#开头的行,请post发表评论~谢谢)

#!/bin/sh
MYSQL_HOST=${MYSQL_HOST:-3}
MYSQL_PORT=${MYSQL_PORT:-3}
REDIS_HOST=${REDIS_HOST:-3}
REDIS_PORT=${REDIS_PORT:-3}

REQ_VARS="MYSQL_HOST\nMYSQL_PORT\nREDIS_HOST\nREDIS_PORT"

printf "$REQ_VARS" | while IFS='' read -r VAR
do
    value=$(eval "echo $$VAR")
    if [[ ${value} == 3 ]]; then
        echo "Error: Variable '$VAR' cannot be undefined"
        exit 1
    fi
done

SERVERS="MYSQL_HOST:MYSQL_PORT\nREDIS_HOST:REDIS_PORT"

while test ! -z "$SERVERS"
do
    for server in $(printf "$SERVERS"); do
        while IFS=: read HOST PORT; do
            DB_HOST=$(eval "echo $$HOST")
            DB_PORT=$(eval "echo $$PORT")

            if nc -z $DB_HOST $DB_PORT; then
                echo "Host '$DB_HOST' started accepting connections"

                # When removing last server from SERVERS script was terminating
                if [ "$(printf "$SERVERS" | wc -l)" -gt 0 ]; then
                    SERVERS=$(printf "$SERVERS" | grep -iv "$HOST:$PORT")
                else
                    SERVERS=""
                fi

                continue
            fi

            echo "Waiting for host '$DB_HOST' to start accepting connections on port '$DB_PORT' ..."

        done <<EOF
$(printf "$server")
EOF

    done

    # Wait 0.4 of the second before checking again
    sleep 0.4
done

这是对您发布的答案的重构,它避免了一些特质和低效率。

#!/bin/sh
: ${MYSQL_HOST?must not be unset}
: ${MYSQL_PORT?must not be unset}
: ${REDIS_HOST?must not be unset}
: ${REDIS_PORT?must not be unset}

# avoid uppercase for private variables
# avoid gratuitous non-portable newline
# pad with surrounding spaces
servers=" MYSQL_HOST:MYSQL_PORT REDIS_HOST:REDIS_PORT "

while true; do
    for server in $servers; do
        db_host=${server%:*}
        db_port=${server#*:}
        if nc -z "$db_host" "$db_port"; then
            # Show diagnostic messages on stderr
            echo "[=10=]: Host '$db_host' started accepting connections" >&2
            servers="${servers#* $db_host:$db_port } ${servers% $db_host:$db_port *}"
        else
            echo "[=10=]: Waiting for host '$db_host' to start accepting connections on port '$db_port' ..." >&2
        fi
    done
    case $servers in
      *[! ]*) ;;
      *) break
    esac
    sleep 0.4
done

使用单个 space 分隔的字符串进行循环有点麻烦,但至少我们避免了外部进程在这里操纵服务器列表。在没有数组变量的情况下,没有真正好的方法来选择性地删除一些服务器并保留其他服务器。此外,由于 space 填充,当我们删除所有服务器时 servers 字符串不一定为空,因此我们在循环内部检查它是否包含非 space 字符。

接受服务器作为命令行参数(也许然后直接以 host:port 形式)可能比要求调用者设置多个变量(更不用说然后对这些变量使用大写)更有意义).然后遍历 "$@" 可能也会使操作服务器列表更简单。

我会向您指出 http://shellcheck.net/ which diagnoses many of these errors without human intervention, and http://www.iki.fi/era/unix/award.html#echo which points out some common antipatterns. The POSIX sh spec on parameter expansion explains many of the constructs in the code above. The Bash manual section on parameter expansion 显然是文档 Bash,但作为辅助资源也可能有用。