我们的宣传口号是:与墙斗,其乐无穷。

1、墙内服务器

1.1、需求与掣肘

需要对服务器数据进行异地备份,数据量级:GB 级。收费的阿里云 OSS 等用不起,土豪请自便。免费的 Google Drive 连不上。

1.2、方案选型

一番折腾后最终选定七牛云免费 10G 对象存储。不够用就建多个账号,用不同的 butket 来备份。

1.3、准备工具

  • 七牛云账号一枚
  • 安装 qshell
  • 本地备份脚本,若无可参考本文 2.4 部分

1.4、实施细节

下载并设置脚本:

wget -c http://devtools.qiniu.com/qshell-linux-x64-v2.4.1.zip
unzip qshell-linux-x64-v2.4.1.zip
mv qshell-linux-x64-v2.4.1 qshell
chmod +x qshell

前往七牛云密钥管理,找到 AccessKey 与 SecretKey:

添加七牛云账号鉴权:

/path/to/qshell account <AccessKey> <SecretKey> <账户标识>

确认一下:

/path/to/qshell user ls
Name: xxxx
AccessKey: xxxx
SecretKey: xxxx

七牛官方建议大于 100MB 的文件使用 rput 来代替 fput 来实现上传。由于备份文件普通较大,这里统一使用 rput:

/path/to/qshell rput xxxx qshell-linux-x64-v2.4.1.zip /root/qshell-linux-x64-v2.4.1.zip
Uploading /root/qshell-linux-x64-v2.4.1.zip => xxxx : qshell-linux-x64-v2.4.1.zip ...

Put file /root/qshell-linux-x64-v2.4.1.zip => xxxx : qshell-linux-x64-v2.4.1.zip success!
Hash: lhKbzJ8255w0m4BKgtYpPthvU5Mc
Fsize: 9739680 ( 9.29 MB )
MimeType: application/zip
Last time: 23.54 s, Average Speed: 413.8 KB/s

上传多个备份文件,我的思路是:

  • 用 bash 脚本跑批
  • 事先 tar 到一个文件中,再用 qshell 上传

- 阅读剩余部分 -

不知道有多少同学遇到过类似反人类的需求:在 Web 上对 MongoDB 进行可视化管理。之前在服务器上用 node 跑了一套 adminMongo,甚是不稳定。用户端一些随机操作容易导致这个服务宕掉,查日志是这个样子的:

/adminMongo/node_modules/nedb/lib/datastore.js:77
    if (err) { throw err; }
               ^

Error: "toString()" failed
    at Buffer.toString (buffer.js:495:11)
    at tryToString (fs.js:453:15)
    at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:444:12)

Google 半天资料也较少,遂欲弃之。

期间也折腾了一下 phpmoadmin,说实话距离我美好的想象相去甚远。

后来重新折腾了一遍 admin-mongo,感觉是我以前的打开方式不对。记录之,用于自我马克。

操作系统 CentOS 7,以 yum 方式安装 node:

yum install nodejs -y

理论上会为您安装好 node 与 npm。确认一下:

node --version
npm --version

我这里的情况是:

node --version
v6.17.1

npm --version
3.10.10

继续安装 pm2:

npm install -g pm2

确认一下:

pm2 --version

我这边是:

pm2 --version
3.3.1

然后就可以直接安装 admin-mongo:

npm install -g admin-mongo

安装目录是 /usr/lib/node_modules/admin-mongo,因此:

cd /usr/lib/node_modules/admin-mongo
pm2 start app.js --name admin-mongo

看到类似这样的信息:

[PM2] Starting /usr/lib/node_modules/admin-mongo/app.js in fork_mode (1 instance)
[PM2] Done.
┌─────────────┬────┬─────────┬──────┬───────┬────────┬─────────┬────────┬─────┬───────────┬──────┬──────────┐
│ App name    │ id │ version │ mode │ pid   │ status │ restart │ uptime │ cpu │ mem       │ user │ watching │
├─────────────┼────┼─────────┼──────┼───────┼────────┼─────────┼────────┼─────┼───────────┼──────┼──────────┤
│ admin-mongo │ 0  │ 0.0.23  │ fork │ 18021 │ online │ 0       │ 0s     │ 0%  │ 13.3 MB   │ root │ disabled │
└─────────────┴────┴─────────┴──────┴───────┴────────┴─────────┴────────┴─────┴───────────┴──────┴──────────┘

到这里 admin-mongo 就开始提供服务了,监听端口 TCP 1234。验证一下:

netstat -antlp | grep 1234
tcp        0      0 0.0.0.0:1234            0.0.0.0:*               LISTEN      18021/node /usr/lib

好,完美。赶紧做个反代用起来,不想被直接访问到的话,我用了比较猥琐的办法:

echo 用户名:密码 > /usr/local/nginx/conf/vhost/xxx.passwd
vim /usr/local/nginx/conf/vhost/xxx.conf

在 location / 中增加:

auth_basic "请替换成您的登录提示";
auth_basic_user_file /usr/local/nginx/conf/vhost/xxx.passwd;

最后 server nginx reload 一下完事儿。

可以看作是这篇文章的升级版本。这里感谢 druggo 留言提醒,让我决定抽时间折腾一下 lftp。

整个过程其实非常简单,照例自己马克一下。

安装 lftp 客户端:

yum -y install lftp

安装后使用过程中遇到报错:

Fatal Error: Certificate Verification: Not Trusted

原因是远端 FTP 服务器使用了自签发 SSL 证书,可以这样解决:

vim /etc/lftp.conf

在文件最后增加一行:

set ssl:verify-certificate no

改造后的脚本:

#!/bin/bash
date=`date +%Y%m%d`
deldate=`date -d -{NUMBER}day +%Y%m%d`
lftp ftp://{FTP_USER}:{FTP_PASSWD}@{HOST}:{PORT} <<!
cd /
lcd /data/backup
mrm *$deldate*
mput *$date*
exit
!

当然 lftp 还有一个更好用的指令:mirror。这里不用的原因是本地存储的时间与远端不一致。

依然是 3 点注意事项:

1、脚本中花括号内容替换为对应的变量;
2、cd 是远程的目录,lcd 是本地目录;
3、备份文件名需符合 date +%Y%m%d 这样的规则。

前阵子写了个小脚本,用于异地备份与自动删除过期文件。使用一段时间后发现异常,表现为:

ps aux | grep ftp

通过这个命令显示有大量的 FTP 客户端进程滞留内存。一开始以为传输未结束导致,经过文件比对后发现传输确实已结束。辣么为何 FTP 进程未能自动退出呢?

网上查询后有人说使用 passive 命令,但我的脚本中已经对 FTP 命令设置了 -inp 的参数,其中参数 p 即启用被动模式传输。若再使用 passive 命令,会使 FTP 从被动模式切换回默认的主动模式,导致连接与传输失败。

权宜之计是在 crontab 中,在定时备份的任务后若干小时再执行这个任务:

for i in `ps aux | grep "ftp -inp" | grep -v grep | awk '{ print $2 }'`; do kill -9 $i; done

确实挺 low 的,Google 了半天貌似没发现还有其他不幸遭遇的同志。若有更好的解决方案,跪求留言。

故事背景:

  • 暂未启用 CI/CD 机制;
  • JVM 以 Jar 包方式启动;
  • 存在开发、测试、生产三套环境。

解决的痛点:

  • 解放劳动力,使 Jar 包从开发、测试到生产环境的部署自动化;
  • 友好且优雅地管理 JVM;
  • 我要看到更详细的日志。嗯,配合 ELK 简直真香。

实现前提:

  • Jar 包中配置文件(数据池、配置项等)剥离,每套环境独立,Jar 包重复利用;
  • 各环境中已配置了 FTP 服务,且可访问到 Jar 包所在目录。

好,开始表演:

#!/bin/sh
# author: xuchao_dot_org

DATE_TIME=$(date "+%Y-%m-%d %H:%M:%S");

BASE_DIR=/data/java/jar/myproject
TMP_DIR=/data/java/tmp/myproject
PROJECT_NAME=myproject_name # 同样也是 Jar 包的文件名
CONFIG_FILE=/data/java/conf/myproject/application.yml
LOG_FILE=/data/java/logs/myproject/$(date +%Y_%m_%d_%H_%M_%S).log

DEV_IP=
DEV_PORT=
DEV_USERNAME=
DEV_PASSWD=
DEV_DIR=/jar/myproject
TEST_IP=
TEST_PORT=
TEST_USERNAME=
TEST_PASSWD=
TEST_DIR=/jar/myproject

export JAVA_HOME=/data/java/jdk/jdk1.8.0_191
export PATH=$JAVA_HOME/bin:$PATH
export JAVA=$JAVA_HOME/bin/java
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar

function running() {
    count=`ps aux | grep java | grep ${PROJECT_NAME}.jar | grep -v grep | wc -l`
    procedure=`ps -ef | grep -w "${PROJECT_NAME}.jar" | grep -w "java" | grep -v "grep" | awk '{print $2}'`
    if [ ${count} -eq 1 -o "${procedure}" != "" ]; then
        return 0
    else
        return 1
    fi
}

function start() {
    if running; then
        echo "${PROJECT_NAME} is running."
        echo "${DATE_TIME} failed to start, ${PROJECT_NAME} is running." 2>&1 >> $LOG_FILE
        exit 1
    fi
    echo "starting ${PROJECT_NAME}..."
    echo "${DATE_TIME} starting ${PROJECT_NAME}..." >> $LOG_FILE
    echo ${CLASSPATH} >> $LOG_FILE
    exec nohup $JAVA -server -Xms128m -Xmx256m -Xss256k -Dspring.config.location=${CONFIG_FILE} -Djava.io.tmpdir=${TMP_DIR} -jar ${BASE_DIR}/${PROJECT_NAME}\.jar >> $LOG_FILE 2>&1 & # 需要调整 JVM 启动参数的选手请看这里
    sleep 15
    if running; then
                echo "${PROJECT_NAME} is successfully started."
        echo "${DATE_TIME} ${PROJECT_NAME} is successfully started." 2>&1 >> ${LOG_FILE}
        else
                echo "${PROJECT_NAME} is failed to start."
        echo "${DATE_TIME} ${PROJECT_NAME} is failed to start." 2>&1 >> ${LOG_FILE}
    fi
}

function stop() {
    if ! running; then
        echo "${PROJECT_NAME} is not running." | tee ${LOG_FILE}
        echo "${DATE_TIME} failed to stop, ${PROJECT_NAME} is not running." 2>&1 >> ${LOG_FILE}
        exit 1
    fi
    echo "stopping ${PROJECT_NAME}..."
    echo "${DATE_TIME} stopping ${PROJECT_NAME}..." >> $LOG_FILE
    pid=`ps -ef | grep -w "${PROJECT_NAME}.jar" | grep -w "java" | grep -v "grep" | awk '{print $2}'`
    kill -15 ${pid}
    sleep 5
    if ! running; then
        echo "${PROJECT_NAME} is successfully stoped."
        echo "${DATE_TIME} ${PROJECT_NAME} is successfully stoped." 2>&1 >> ${LOG_FILE}
    else
        echo "${PROJECT_NAME} is failed to stop. trying again..."
        echo "${DATE_TIME} ${PROJECT_NAME} is failed to stop. trying again..." 2>&1 >> ${LOG_FILE}
        kill -15 ${pid}
        sleep 5
        if ! running; then
            echo "${PROJECT_NAME} is successfully stoped. [2nd attempt]"
            echo "${DATE_TIME} ${PROJECT_NAME} is successfully stoped. [2nd attempt]" 2>&1 >> ${LOG_FILE}
        else
            echo "${PROJECT_NAME} is failed to stop. [2nd attempt]"
            echo "${DATE_TIME} ${PROJECT_NAME} is failed to stop. [2nd attempt]" 2>&1 >> ${LOG_FILE}
        fi
    fi
}

function status() {
    if running; then
        echo "${PROJECT_NAME} is running."
    else
        echo "${PROJECT_NAME} is stopped."
    fi
}

function help() {
    echo "script for jvm managerment. author: xuchao_dot_org"
    echo "usage: $0 [start|stop|restart|status|get|help]"
    echo "    start:        start the ${PROJECT_NAME} server"
    echo "    stop:        stop the ${PROJECT_NAME} server"
    echo "    restart:    restart the ${PROJECT_NAME} server"
    echo "    status:        get ${PROJECT_NAME} current status, running or stopped"
    echo "    get:        [dev|test] [version] get file from dev or test"
    echo "    help:        show this message"
}

function get_dev() {
    [ -z $1 ] && echo "params error. please see [ $0 help ] for more information." && exit 1
    echo "geting file [${PROJECT_NAME}-$1.jar] from dev..."
    echo "${DATE_TIME} geting file [${PROJECT_NAME}-$1.jar] from dev..." 2>&1 >> ${LOG_FILE}
    ftp -inp 2>&1 >> ${LOG_FILE} <<EOF
open ${DEV_IP} ${DEV_PORT}
user ${DEV_USERNAME} ${DEV_PASSWD}
cd ${DEV_DIR}
lcd ${BASE_DIR}
get ${PROJECT_NAME}-$1.jar
close
bye
EOF
    if [ -f "${BASE_DIR}/${PROJECT_NAME}-$1.jar" ]; then
        echo "get file [${PROJECT_NAME}-$1.jar] from dev successfully."
        echo "${DATE_TIME} get file [${PROJECT_NAME}-$1.jar] from dev successfully." 2>&1 >> ${LOG_FILE}
        \cp -rf ${BASE_DIR}/${PROJECT_NAME}-$1.jar ${BASE_DIR}/${PROJECT_NAME}.jar
        echo "copy file [${PROJECT_NAME}-$1.jar] into [${PROJECT_NAME}.jar] successfully."
        echo "${DATE_TIME} copy file [${PROJECT_NAME}-$1.jar] into [${PROJECT_NAME}.jar] successfully." 2>&1 >> ${LOG_FILE}
    else
        echo "failed to get file [${PROJECT_NAME}-$1.jar] from dev."
        echo "${DATE_TIME} failed to get file [${PROJECT_NAME}-$1.jar] from dev." 2>&1 >> ${LOG_FILE}
    fi
}

function get_test() {
    [ -z $1 ] && echo "params error. please see [ $0 help ] for more information." && exit 1
    echo "geting file [${PROJECT_NAME}-$1.jar] from test..."
    echo "${DATE_TIME} geting file [${PROJECT_NAME}-$1.jar] from test..." 2>&1 >> ${LOG_FILE}
    ftp -inp 2>&1 >> ${LOG_FILE} <<EOF
open ${TEST_IP} ${TEST_PORT}
user ${TEST_USERNAME} ${TEST_PASSWD}
cd ${TEST_DIR}
lcd ${BASE_DIR}
get ${PROJECT_NAME}-$1.jar
close
bye
EOF
    if [ -f "${BASE_DIR}/${PROJECT_NAME}-$1.jar" ]; then
        echo "get file [${PROJECT_NAME}-$1.jar] from test successfully."
        echo "${DATE_TIME} get file [${PROJECT_NAME}-$1.jar] from test successfully." 2>&1 >> ${LOG_FILE}
        \cp -rf ${BASE_DIR}/${PROJECT_NAME}-$1.jar ${BASE_DIR}/${PROJECT_NAME}.jar
        echo "copy file [${PROJECT_NAME}-$1.jar] into [${PROJECT_NAME}.jar] successfully."
        echo "${DATE_TIME} copy file [${PROJECT_NAME}-$1.jar] into [${PROJECT_NAME}.jar] successfully." 2>&1 >> ${LOG_FILE}
    else
        echo "failed to get file [${PROJECT_NAME}-$1.jar] from test."
        echo "${DATE_TIME} failed to get file [${PROJECT_NAME}-$1.jar] from test." 2>&1 >> ${LOG_FILE}
    fi
}

function get_file() {
    [[ -z $1 || -z $2 ]] && echo "params error. please see [ $0 help ] for more information." && exit 1
    case "$1" in
        dev)
            get_dev $2
            ;;
        test)
            get_test $2
            ;;
        *)
            help
            exit 1
            ;;
    esac
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        $0 stop
        sleep 5
        $0 start
        ;;
    status)
        status
        ;;
    help)
        help
        ;;
    get)
        get_file $2 $3
        ;;
    *)
        help
        exit 1
        ;;
esac

演出结束,下期再见。