我的YouTube Download有一个worker是麻烦盆友帮我跑的,每次更新代码都要让人家重新pull image然后再up,有没有什么能够让我没有ssh也能自助更新代码吗?
最简单的办法是把docker的socket暴露给容器,这样容器就可以为所欲为了。但是这也太危险而且太麻烦
既然大部分情况下也都是更新代码,不会有太大的变更,那么似乎只要想办法把代码更新一下,然后重启下容器内的进程就可以了。
容器内使用git
首先,要确保在Dockerfile中安装了git哦。
我的image都是使用GitHub Actions构建的,并且不知道GitHub Actions给我加入了什么神奇的配置项,导致无法无缝pull。毕竟我的仓库是公开的啊🤔
反正image也是公开的,我就不打码了
所以首先要魔改一下,先给unset掉
git config --unset http.https://github.com/.extraheader
然后unshallow pull
git pull origin --unshallow
下次就可以正常的git pull了,大概的代码如下,能用就行
git_path = "your project root" # or maybe pathlib.Path().cwd().parent.as_posix() logging.info("Hot patching on path %s...", git_path) unset = "git config --unset http.https://github.com/.extraheader" pull_unshallow = "git pull origin --unshallow" pull = "git pull" subprocess.call(unset, shell=True, cwd=git_path) if subprocess.call(pull_unshallow, shell=True, cwd=git_path) != 0: logging.info("Already unshallow, pulling now...") subprocess.call(pull, shell=True, cwd=git_path)
重启应用程序
自动重启应用方法很多,只要确保容器不被删除直接restart就可以。然后我们需要用一个守护进程来帮忙拉起来,比如docker的restart policy可以设置成on-failure
,或者容器内上supervisor也不是不行。
问题是如何退出进程呢?
并对于运行在主线程中的处理函数来说,类似 sys.exit(1)
就可以。注意exit code要和restart policy相配合。比如restart policy是on-failure
,那么exit(0)
就不行了
对于运行在子线程中的处理函数,exit只会退出当前线程而不会把主线程退出。那么一个简单有效的办法就是获取到整个进程的PID,然后kill掉,Python的psutil提供了这样的功能:
psutil.Process().kill()
使用kill
而不是terminate
也是为了避免exit code是0的问题
如果想要通过apscheduler的BackgroundScheduler
来让自己完全自杀,那么这样就是比较好的了。
celery worker broadcast
我的YouTube Download的整体架构是master+N个worker的模式。因此在热更新的时候不仅要更新master,也要把worker都更新了。
直接发布消息是不行的,除非只有一个worker,所以我们需要发布broadcast来确保所有的worker都会收到并且执行命令。
app.control.broadcast("ping")
ping就是表示让worker执行的命令,默认大概有几十个,包括ping、revoke、heartbeat什么的。
我们可以自定义一个command,使用@Panel.register
装饰器
from celery.worker.control import Panel @Panel.register def hello (*args): print("patch...")
然后
app.control.broadcast("hello")
所有worker就都能收到这条消息了
更多……
更有甚者,可以自定义好在热更新的时候要做什么,比如更新代码,更新 Python Packages,更新系统相关工具。反正无非就是在容器内各种subprocess就好了。
当然了别忘了做权限控制,要不然就RCE啦😏😏😏😏😏
完整代码
# tasks.py import psutil from celery.worker.control import Panel @Panel.register def hot_patch(*args): git_path = "your project root" # or maybe pathlib.Path().cwd().parent.as_posix() logging.info("Hot patching on path %s...", git_path) unset = "git config --unset http.https://github.com/.extraheader" pull_unshallow = "git pull origin --unshallow" pull = "git pull" subprocess.call(unset, shell=True, cwd=git_path) if subprocess.call(pull_unshallow, shell=True, cwd=git_path) != 0: logging.info("Already unshallow, pulling now...") subprocess.call(pull, shell=True, cwd=git_path) logging.info("Code is updated, applying hot patch now...") psutil.Process().kill() # control app.control.broadcast("hot_patch")
参考资料
https://github.com/tgbot-collection/ytdlbot/blob/master/ytdlbot/tasks.py#L246
https://github.com/tgbot-collection/ytdlbot/blob/master/ytdlbot/ytdl_bot.py#L100
https://stackoverflow.com/q/65204671/10264400