找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2019

积分

0

好友

285

主题
发表于 昨天 06:43 | 查看: 5| 回复: 0

前几天有件挺有意思的事。周五晚上快九点,我正准备收工,组里同事在群里问:“东哥,你刚改的那个服务我要发测试,怎么包的版本还是上个月的?”

一问才知道,他每次发版还在手动操作:打包 → 用 scp 传文件 → 登录测试机 → 停止旧服务 → 解压新包 → 启动服务。中途要是来个电话或者被叫去开会,回来就忘了进行到哪一步,难怪经常会发错版本。

我当时就想,这都什么年代了,这种重复劳动还不能让机器自己完成吗?于是决定写个 Python 小脚本,让它盯着构建产物的文件夹,一有变化就自动执行部署流程。这样谁改完代码把包往那一放,服务就自己更新了,多省事。

整个思路很简单,就三步:监控目录变化 → 判定是否为有效部署触发点 → 执行部署脚本

用Python实现文件夹变化监控

首先解决“盯着文件夹”的问题。我们肯定不想用 while Truesleep 轮询目录,那种方式既耗CPU又不实时。

Python 里有个很好用的库叫 watchdog,它跨平台,能通过事件回调的方式精准通知文件变化。

先安装一下(建议在你的虚拟环境里操作):

pip install watchdog

写个最简单的示例,先把监控到的事件打印出来感受一下:

# file: simple_watch.py
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class PrintEventHandler(FileSystemEventHandler):
    def on_any_event(self, event):
        # event.event_type: modified / created / deleted / moved
        # event.src_path: 发生变化的文件路径
        print(f"[EVENT] type={event.event_type}, path={event.src_path}")

if __name__ == "__main__":
    watch_dir = "/opt/app/build"  # 你要监控的目录,根据实际情况修改

    event_handler = PrintEventHandler()
    observer = Observer()
    observer.schedule(event_handler, watch_dir, recursive=True)
    observer.start()

    print(f"开始盯着 {watch_dir} 了... 按 Ctrl+C 退出")
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

运行后,在 watch_dir 目录里新建或修改文件,看看终端是否会实时输出日志。这一步通了,我们就能在事件回调里加入自己的业务逻辑了。

事件过滤:并非所有变化都要触发部署

想想实际场景:前端构建会生成一堆 .js.map.css 文件,后端构建可能产出 .tar.gz 包或新的 Docker 镜像标签。我们通常只关心:某个特定的关键文件(构建产物)是否已准备就绪

例如,我们可以约定:后端构建完成后,将打包好的文件放到 /opt/app/deploy/app.tar.gz。那么监控脚本就只关注这个文件的变化即可。

基于这个思路,我们来改造事件处理器:

# file: deploy_watcher.py
import os
import time
import subprocess
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

TARGET_FILE = "/opt/app/deploy/app.tar.gz"

class DeployEventHandler(FileSystemEventHandler):
    def __init__(self):
        super().__init__()
        self._last_deploy_ts = 0

    def _should_trigger(self, event_path: str) -> bool:
        # 1. 只关心目标文件
        if os.path.abspath(event_path) != os.path.abspath(TARGET_FILE):
            return False

        # 2. 简单防抖,10秒内只触发一次部署
        now = time.time()
        if now - self._last_deploy_ts < 10:
            print(“10 秒内已经部署过一次了,稍等片刻...”)
            return False

        self._last_deploy_ts = now
        return True

    def on_modified(self, event):
        if event.is_directory:
            return
        if not self._should_trigger(event.src_path):
            return
        print(f”[WATCH] 检测到构建产物变化:{event.src_path}“)
        self.deploy()

    def on_created(self, event):
        # 有些构建工具会先写临时文件,再重命名为目标文件,这种情况会触发 created 事件
        if event.is_directory:
            return
        if not self._should_trigger(event.src_path):
            return
        print(f”[WATCH] 检测到构建产物创建:{event.src_path}“)
        self.deploy()

    def deploy(self):
        print(”[DEPLOY] 开始执行部署脚本...“)
        try:
            # 调用你实际的部署脚本,例如 /opt/app/deploy/deploy.sh
            result = subprocess.run(
                [“/bin/bash”, “/opt/app/deploy/deploy.sh”],
                check=True,
                capture_output=True,
                text=True,
            )
            print(”[DEPLOY] 部署成功,输出如下:“)
            print(result.stdout)
        except subprocess.CalledProcessError as e:
            print(”[DEPLOY] 部署失败,错误信息:“)
            print(e.stderr)

主函数部分:

if __name__ == ”__main__“:
    watch_dir = os.path.dirname(TARGET_FILE)

    event_handler = DeployEventHandler()
    observer = Observer()
    observer.schedule(event_handler, watch_dir, recursive=False)
    observer.start()

    print(f”开始监控 {TARGET_FILE},构建产物放过来就会自动部署...“)
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

这样一来,你只需要确保 CI 流程或本地打包脚本的最后一步,是将构建好的 app.tar.gz 放到指定路径,监控脚本就会自动触发部署。

部署脚本里应该做什么?

前面的 Python 脚本只是一个“触发器”,真正的部署动作最好封装在一个独立的 deploy.sh 脚本中,这样更清晰,也便于单独测试。

下面是一个典型的非 Docker 后端服务部署脚本示例:

#!/bin/bash
# file: /opt/app/deploy/deploy.sh
set -e  # 任何一步执行失败就退出脚本

APP_NAME=“demo-service”
DEPLOY_DIR=“/opt/app/runtime”
PACKAGE=“/opt/app/deploy/app.tar.gz”

echo ”[deploy.sh] 开始部署 ${APP_NAME} ...“

# 1. 停止旧服务(生产环境可换成 systemctl 或 supervisorctl)
if pgrep -f “${APP_NAME}” > /dev/null; then
    echo “[deploy.sh] 停止旧进程...”
    pkill -f “${APP_NAME}”
    # 等待进程完全退出
    sleep 2
fi

# 2. 清理旧部署文件
echo “[deploy.sh] 清理旧文件...”
mkdir -p “${DEPLOY_DIR}”
rm -rf “${DEPLOY_DIR:?}/”*

# 3. 解压新版本包
echo “[deploy.sh] 解压新包...”
tar -xzf “${PACKAGE}” -C “${DEPLOY_DIR}”

# 4. 安装依赖(以Python项目为例)
if [ -f “${DEPLOY_DIR}/requirements.txt” ]; then
    echo “[deploy.sh] 安装Python依赖...”
    pip install -r “${DEPLOY_DIR}/requirements.txt” --no-cache-dir
fi

# 5. 启动新服务(示例使用 nohup 后台启动)
echo “[deploy.sh] 启动新进程...”
cd “${DEPLOY_DIR}”
nohup python main.py > app.log 2>&1 &

echo “[deploy.sh] 部署完成。”

如果你使用 Docker 或 Kubernetes,部署脚本的逻辑就变成了构建镜像、执行 docker compose up -dkubectl rollout restart 等命令。核心思路不变,只是调用的命令不同。

如何监控多个服务?

实际项目通常不止一个服务。一个脚本只监控一个文件显得不够用。我们可以引入配置文件,来管理“监控目标”与“部署脚本”的映射关系。

例如,创建一个 config.yaml

# file: /opt/app/deploy/config.yaml
items:
  - name: “user-service”
    target: “/opt/app/deploy/user/app.tar.gz”
    script: “/opt/app/deploy/user/deploy.sh”

  - name: “order-service”
    target: “/opt/app/deploy/order/app.tar.gz”
    script: “/opt/app/deploy/order/deploy.sh”

然后,改造 Python 脚本以支持多规则配置:

# file: multi_deploy_watcher.py
import os
import time
import subprocess
from dataclasses import dataclass
from typing import Dict

import yaml
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

@dataclass
class DeployItem:
    name: str
    target: str
    script: str
    last_ts: float = 0.0

class MultiDeployHandler(FileSystemEventHandler):
    def __init__(self, items: Dict[str, DeployItem]):
        super().__init__()
        # key: 目标文件的绝对路径
        self.items = items

    def _trigger(self, path: str):
        abspath = os.path.abspath(path)
        item = self.items.get(abspath)
        if not item:
            return

        now = time.time()
        # 防抖:10秒内不重复部署同一服务
        if now - item.last_ts < 10:
            print(f”[{item.name}] 10 秒内已部署过,忽略此次事件“)
            return
        item.last_ts = now

        print(f”[{item.name}] 检测到 {abspath} 变化,开始部署...“)
        try:
            result = subprocess.run(
                [“/bin/bash”, item.script],
                check=True,
                capture_output=True,
                text=True,
            )
            print(f”[{item.name}] 部署成功:“)
            print(result.stdout)
        except subprocess.CalledProcessError as e:
            print(f”[{item.name}] 部署失败:“)
            print(e.stderr)

    def on_modified(self, event):
        if event.is_directory:
            return
        self._trigger(event.src_path)

    def on_created(self, event):
        if event.is_directory:
            return
        self._trigger(event.src_path)

def load_config(config_path: str) -> Dict[str, DeployItem]:
    with open(config_path, “r”, encoding=“utf-8”) as f:
        data = yaml.safe_load(f)

    items: Dict[str, DeployItem] = {}
    for raw in data.get(“items”, []):
        target = os.path.abspath(raw[“target”])
        items[target] = DeployItem(
            name=raw[“name”],
            target=target,
            script=os.path.abspath(raw[“script”]),
        )
    return items

if __name__ == ”__main__“:
    CONFIG = “/opt/app/deploy/config.yaml”
    items = load_config(CONFIG)
    if not items:
        print(”配置为空,无法启动监控。“)
        exit(1)

    # 收集所有需要监控的目录(去重)
    watch_dirs = set(os.path.dirname(it.target) for it in items.values())

    handler = MultiDeployHandler(items)
    observer = Observer()
    for d in watch_dirs:
        observer.schedule(handler, d, recursive=False)
        print(f”正在监控目录:{d}“)

    observer.start()
    print(”所有监控已启动,构建完丢包即可自动触发部署。“)
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

现在,只要你的构建流程将 user-service 的包输出到 /opt/app/deploy/user/app.tar.gz,监控脚本就会自动调用对应的 user/deploy.sh 完成部署,实现真正的 运维 自动化。

需要注意的几个关键点

这个方案看似简单,但如果想用在准生产环境,有几个坑需要注意,否则可能会在半夜被报警叫起来“救火”。

  1. 构建和文件拷贝是原子的吗?
    有些打包工具是先写临时文件,再通过重命名(rename)操作移到目标位置,这种方式是原子的,没问题。但有些工具会直接往目标文件流式写入,这可能导致脚本在文件还没写完(“半包”状态)时就触发部署。一个简单的应对策略是:在部署脚本开始时增加文件完整性校验,例如检查压缩包是否能正常解压,或文件大小是否达到预期阈值,校验不通过则直接失败退出。

  2. 部署过程耗时较长怎么办?
    如果一次部署需要1分钟,而在这1分钟内又来了新的构建包,上面的代码仅做了防抖,并没有处理任务排队。一个简单的改进方法是:部署开始时在 /tmp 目录创建一个锁文件(lock file),部署完成后删除。在 _should_trigger_trigger 方法中检查锁文件是否存在,如果存在则跳过本次触发。如果需要更精细的控制,可以实现一个任务队列,由单独的 worker 进程顺序处理。

  3. 权限问题
    本地测试时一切顺利,放到服务器上可能就会出现各种 Permission denied。这个监控脚本本质上是“自动帮你执行 shell 命令”,它的权限完全取决于运行它的用户。建议创建一个专用用户(如 deploy),并提前配置好该用户对监控目录、部署脚本以及目标运行目录的读写执行权限。

  4. 认清定位:这只是轻量级辅助工具
    这个方案更适合作为测试环境、个人项目或内网工具的“轻量级热部署助手”。对于正式的生产环境,还是应该搭建具备审批流程、回滚机制、灰度发布等能力的完整 CI/CD 流水线。不过,在合适的场景下,这个脚本用起来是真的香。

如何以守护进程方式可靠运行?

通常,我会在服务器上这样部署它:

  1. 创建一个专用系统用户,例如 deploy
  2. multi_deploy_watcher.pyconfig.yaml 等文件放到 /opt/app/deploy 目录下。
  3. 编写一个 systemd 服务单元文件,将这个监控脚本作为守护进程长期运行。

一个示例的 systemd 服务文件 (/etc/systemd/system/deploy-watcher.service) 如下:

[Unit]
Description=Simple Python Deploy Watcher
After=network.target

[Service]
User=deploy
WorkingDirectory=/opt/app/deploy
ExecStart=/usr/bin/python /opt/app/deploy/multi_deploy_watcher.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

然后执行以下命令启用并启动服务:

systemctl daemon-reload
systemctl enable deploy-watcher
systemctl start deploy-watcher
systemctl status deploy-watcher  # 查看运行状态

这样一来,监控脚本就在后台稳定运行了。你或你的 CI 系统只需要将构建好的包放到约定目录,它就会默默地把服务更新上线。作为开发者,你只需要查看一下日志确认部署结果即可。

通过这样一个简单的自动化脚本,就能把同事从繁琐的手动操作中解放出来,省下来的时间干点啥不好呢?如果你对这类提升效率的自动化脚本感兴趣,欢迎来 云栈社区 交流更多实战技巧。




上一篇:开源数据库备份工具 Databasus:支持PostgreSQL/MySQL/MongoDB的Docker部署与Web管理
下一篇:Jackson 2.16安全特性解析与Java JSON工具库选型指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 20:16 , Processed in 0.479101 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表