外贸网站 开源,优化营商环境个人心得,wordpress中国服务器,小程序开发制作哪家好背景#xff1a;笔者是一名Javaer#xff0c;但是最近因为某些原因迷上了Python和它的Asyncio#xff0c;至于什么原因#xff1f;请往下看。在着迷”犯浑“的过程中#xff0c;也接触到了一些高并发高性能的组件#xff0c;通过简单的学习和了解#xff0c;aiohttp这个… 背景笔者是一名Javaer但是最近因为某些原因迷上了Python和它的Asyncio至于什么原因请往下看。在着迷”犯浑“的过程中也接触到了一些高并发高性能的组件通过简单的学习和了解aiohttp这个组件引起了我极大的兴趣。 协程、异步非阻塞、”吓人“的性能这些关键词让我不得不注意到它。 老样子我们先看成品再讲讲我曲折的过程。
读取实时日志 构建部署 技术痛点揭露
相信大家一定遇到过笔者这次的场景 疫情隔离居家办公这次我们做的是一个小程序前端的小伙伴们要联调接口可是不能用公司的资源因为公司都断电了 于是乎我自己买bai了piao云服务器自己搭建了一套环境用自己的域名给他们架上了。本以为事情解决了前后端可以愉快地调试接口了但是想都别想现实还是无情地用它宽大的手掌啪啪打我的三寸小脸。 你看后端的小伙伴写完代码改完bug提交了之后一次又一次的让你部署导致吃饭都想着部署每次都是噼里啪啦一堆命令脑瓜子嗡嗡的。First Blood 你再看后端猿小帅和前端媛小美正在对接接口小美说接口怎么一直报错小帅眉头一皱手一抖微信窗口多出个小表情一脸无辜我本地可以啊。对就是这句话“我本地可以啊为啥线上不行”成了接口对接中的口头禅。完了肯定又是我的活这不咔咔一顿艾特让我帮忙看日志啊啊啊啊啊啊一天到晚登上服务器看了不下N次日志我的头发在抗议。Double Kill 你再再再看我们对接用的yapi在接口未完成之前前端调用的是mock完成之后得切换到真实接口。为了保证项目开发进度让前后端的联调顺滑如丝那付出的肯定是我了。一天下来在忙上面事情的同时我还在不断地调整Nginx反向代理配置为他们放开一个个接口的代理。我内心只能说mmp。Triple Kill 你再再再再看看正常开发过程中总有些粗心捣蛋的人提交的代码像一个炸弹。这不刚刚这哥们还在小区楼底下蹦迪下一秒回家晕乎乎地写了几个bug潇洒提交又蹦迪去了。这不提交不要紧一提交之后紧接着我习惯性的部署上去一系列的连锁反应导致几个接口不能用了兄弟们叫苦不迭要不是居家我真想上去抽那仁兄几个嘴巴子。这屁股还是得我擦回退到上个版本先凑合调试着。这种操作隔三差五在上演也是麻烦的很。。。Quadra kill 最后脑补一个五杀Penta kill 尝试曲线救国 上面列举了那么多痛点是个人都被折磨的够呛吧拜托疫情即使在家办公也是要高效更何况家人都在身边不能焦躁不能焦躁不能焦躁 这个时候就有大佬说了你搞这么多费力不讨好的事情为啥不直接用CI/CD持续集成呢我花费了5根头发想了想我这1GB内存1核CPU还能再战吗再摸了摸我那比纸都薄的钱包最后点了三炷香“祭奠”了一下我死去的5根头发心里默默说了声算了忍忍你可以的。
方案一 脚本大法 代理 我开始尝试写脚本。我们的项目是微服务正常部署都应该用docker-compose或者直接上到k8s集群里但是非常时期我们没有办法只能人工部署。所以我写了一个又一个的脚本然后写好备注然后写了一个Low到爆的 HTML写了超级烂的几行Java代码来调用这些脚本。最后通过Nginx给他们代理出去把URL分发出去让他们自己点。 我花费了几个小时完成了上述工作就在我以为万事大吉的时候我发现我服务器进不去了。。。WTF登上控制台看到CPU使用率125%我就一个核怎么还超过100捏虚机超频呸呸呸言归正传排查了半天我发现是因为多个人短时间内执行构建脚本和部署脚本直接启动多个进程把机器“干”死了我摇了摇头方案1去你的吧。
方案二 方案一的“进化” 鉴于方案一存在的致命短板我不得不针对这个问题进行优化优化的手段嘛不出大家所料还是脚本用low到爆的一个办法每次运行构建都通过 ps | grep | xargs kill -9 杀死之前的进程再进行构建。运行结果也增加了反馈用户执行结果会根据Sheel执行返回值进行判断给出成功与否的响应。至于触发方法嘛当然是老样子 继续HTML点击Java调用脚本。 再次试验效果我组织了一场视频会议会议上我让小美和阿伟还有阿强分别点击部署哇效果嘎chao嘎la的ji小美先点击的居然部署成功了阿伟和阿强后来的居然被杀了awsl阿伟死了。后来排查了半天发现小美家的wifi只有一格信号请求发到后台慢了。总体来说方案二的可用度提高了但是依然没什么卵用小美提交的代码运行一会后报错了原因是阿伟提交的一段代码引用了jdk中sun包内的东西服务器openjdk没有相关类服务压根没起来。还是很鸡肋。
方案三 另辟蹊径 方案一和方案二都是短时间内拍脑门儿想出来的活到现在为止我已经发现问题不能这么草率的解决了 否则永远都是不断地返工。我深刻地分析了一下作为一个完备的协同部署功能至少需要满足以下几个条件
1. 能够协同工作和实时交互。看了比较大的运维平台基本上都具备实时的反馈接近SSH会话级别的体验能够确认当前的部署状态和部署进度用户可以及时发现并避免和其他人的交叉使用。此外如果可能的话应该实现当前部署状态未完成其他用户不可操作服务器。
2. 能够查看实时日志。系统运行的状况如何应该具备日志查看的入口这些入口开放给开发人员才能够做到每个人都能及时处理自己的问题。此外日志滚动频率过快应该提供“暂停日志”和“恢复日志”的能力。
3. 实现用户身份标识。该功能也是必须的因为通过HTML按钮点击部署出了问题往往无法追溯是谁干的。后面我设计为每个用户提供身份标识确认通过线下发放key的方式提供服务的使用权限每个key可以绑定到具体的用户绑定key后才可以正常使用运维能力。
4. 具有版本控制和一键回滚。一个合格的部署平台必须具有防范风险的能力体现在健壮性上来说就是版本控制。利用shell脚本实现版本控制并不难实现一键回滚也不难难的是库表结构修改后产生的种种恩怨情仇。 经过系统的分析之后我们说干就干。 开始干活 工欲善其事必先利其器。干活前老样子先做技术选型。为了一步到位我直接选择了Vue3去写前端后端压根没想着用Java去写因为我写过很多pipleline的代码java处理起来冗长又效率低下果断选择了Python大法。事实证明我的选择太明智了。 搭建Vue项目
技术栈
Vue 3
Ant Design Vue 3.1.1
Socket.io-client
CodeMirror Editor
Axios
我们使用最新的vue-cli搭建项目。
1. 环境准备
# 安装 Vue CLI
npm install -g vue/cli# 创建项目
vue create web# 安装依赖
cd web
yarn add ant-design-vue ant-design/icons-vue axios socket.io-client codemirror-editor-vue3
2. 项目配置
babel.config.js - 按需加载配置
module.exports {presets: [vue/cli-plugin-babel/preset],plugins: [[import, { libraryName: ant-design-vue, libraryDirectory: es, style: css }]]
}
vue.config.js - 开发服务器配置
module.exports defineConfig({transpileDependencies: true,devServer: {port: 7777,proxy: {/api: {target: http://localhost:8090,ws: false,changeOrigin: true,},}},
})
2. 核心功能实现
1. WebSocket 通信模块
项目使用 Socket.io 实现与后端的实时通信:
const wsConnect (write, done) {const handler (data) {// 处理管道数据const { success, end, content, msg } dataif (success) {content write(content.replaceAll(\0, ))end write(\n任务执行完毕, done)} else {write(\n管道读取失败 msg, done)}}return {io: null,async connect() {this.io io(WS_URL, {transports: [websocket],query: { token }}).on(pipeline, this.handler.bind(this))},// 发送请求request(event, data {}) {return new Promise((resolve, reject) {const rid Date.now()this.io.emit(event, { rid, ...data })this.pending[rid] { resolve, reject }})}}
}
2. 部署控制台实现
templatediv classdeploya-card :body-style{padding: 10px 24px}div classopt-group构建和部署a-button v-if!deploying :disabledrunning classprimary typeprimary clickdeploytemplate #iconbuild-filled //template构建并部署/a-buttona-button v-else :loadingstopping classprimary typedanger clickstoptemplate #iconclose-circle-filled //template停止部署/a-buttona-button :disableddeploying || running classprimary typeprimary clickwebtemplate #iconglobal-outlined //template构建部署前端/a-buttona-dropdown-button :disableddeploying || running typedanger clickrestore visibleChangeloadHistorieshourglass-filled /回滚版本template #overlaya-menu clickeditFiletemplate v-ifhistories.lengtha-menu-item :keyfile v-forfile in histories{{file}}/a-menu-item/templatea-menu-item v-else disabled keymore暂无可回滚版本/a-menu-item/a-menu/template/a-dropdown-button/diva-divider classdivider typevertical /div classopt-group运行监控a-button v-if!running :disableddeploying classprimary typeprimary clicklogtemplate #iconsnippets-filled //template读取运行日志/a-buttona-button v-else :loadingstopping classprimary typedanger clickstoptemplate #iconclose-circle-filled //template停止日志读取/a-buttona-button :disableddeploying || stopping || running :loadingrestarting classprimary typedangerclickrestarttemplate #iconappstore-filled //template重启项目/a-buttona-button v-if!paused :disabled!running clickpausetemplate #iconpause-circle-filled //template暂停日志/a-buttona-button v-else :disabled!running typeprimary clickplaytemplate #iconplay-circle-filled //template恢复日志/a-button/divtemplate v-ifadmina-divider classdivider typevertical /div classopt-group配置维护a-button classprimary typeprimary clickeditFiletemplate #iconsetting-filled //template修改配置文件/a-buttona-dropdown triggerclicktemplate #overlaya-menu clickeditFilea-menu-item :keyfile v-forfile in files{{file}}/a-menu-itema-menu-item keymore创建脚本.../a-menu-item/a-menu/templatea-button clickloadFiles修改项目脚本DownOutlined //a-button/a-dropdown/div/template/a-carda-cardtemplate #extraa href#当前版本v1.5.3/a/templatetemplate #titlecode-filled stylemargin-right: 10px /控制台a-divider typevertical /a v-ifcurrent deploy当前部署日志/aa v-else当前运行日志/a/templatecode-mirror refeditorRef :height350 :optionscmOptions classconsole //a-carda-drawerwidth1000:visible!!editing.keytitle修改文件内容placementrightcode-mirror height100% :optionscmOptions v-model:valueediting.content classconsole /template #footerdiv styletext-align: centera-button stylemargin-right: 8px clickediting {content: }取消/a-buttona-button typeprimary :loadingediting.loading clicksaveFile保存/a-button/div/template/a-drawer/div
/templatescript
import { onMounted, ref } from vue;
import CodeMirror from codemirror-editor-vue3;
import { message, Modal } from ant-design-vue;
import {AppstoreFilled,BuildFilled,CloseCircleFilled,CodeFilled,DownOutlined,GlobalOutlined,HourglassFilled,PauseCircleFilled,PlayCircleFilled,SettingFilled,SnippetsFilled,
} from ant-design/icons-vue;
// import base style
import codemirror/lib/codemirror.css
import codemirror/theme/material-darker.css
// language
import codemirror/mode/javascript/javascript.js;
import createSocket from /api/pipeline;export default {name: DeployPage,components: {CodeMirror,CodeFilled,GlobalOutlined,BuildFilled,SettingFilled,HourglassFilled,SnippetsFilled,PauseCircleFilled,PlayCircleFilled,CloseCircleFilled,AppstoreFilled,DownOutlined},// 在我们的组件中setup() {// socketioconst socket ref(null);// 编辑器实例const editorRef ref(null);const editor ref(null);// 活跃的回调const callback ref(null);// 日志运行状态const running ref(false);// 日志暂停状态const paused ref(false);// 部署运行状态const deploying ref(false);// 重启运行状态const restarting ref(false);// 通用停止按钮状态const stopping ref(false);// 配置文件列表const files ref([]);// 历史版本列表const histories ref([]);// 当前控制台视图支持deploy部署、log日志const current ref(deploy);// 运行管道信息const runningKey ref();// 缓存内容const cache ref();// 当前编辑文件const editing ref({});// 目标对应字段const dicts {set deploy(value) {deploying.value value;},get deploy() {return deploying.value;},set web(value) {deploying.value value;},get web() {return deploying.value;},set log(value) {running.value value;},get log() {return running.value;},set restart(value) {restarting.value value;running.value value;if (!value) {appender(\n已完成重启请读取日志查看)}},get restart() {return restarting.value;},set restore(value) {deploying.value value;},get restore() {return deploying.value;},};// 日志追加器const appender (text, end, clear) {if (clear) {return editor.value?.setValue(text || )}if (text) {// 如果暂停了进缓存if (paused.value) {cache.value text;} else {editor.value?.replaceRange(text, { line: Infinity });editor.value?.scrollTo(0, Infinity);}}// 具有回调代表结束做一些重置if (end) {end();cache.value ;paused.value false;restarting.value false;runningKey.value ;}};// 建立pipeline并读取const connector async (target) {current.value target;callback.value error {if (error) appender(\n连接中断或异常 error);dicts[target] false;};try {dicts[target] true;runningKey.value await socket.value.open(target);appender(\n管道建立成功进程id: runningKey.value \n);} catch (e) {appender(\n无法建立管道连接 e.message, callback);}}// 挂载后获取实例onMounted(async () {editor.value editorRef.value?.cminstance;socket.value await createSocket(appender, callback).connect();const instance editor.value;if (instance) {instance.setValue(暂无运行日志\n\n\n\n\n\n\n\n\n\n\n\n\n);instance.focus();}});// 返回命名空间return {editorRef,running,stopping,current,deploying,restarting,paused,files,histories,editing,get admin() {return socket.value?.admin;},play: () {paused.value false;const cached cache.value;cache.value appender(cached);},pause: () paused.value true,deploy: async () connector(deploy),log: async () connector(log),restart: async () connector(restart),web: async () connector(web),// 停止pipeline并清理stop: async () {try {stopping.value true;await socket.value.kill(runningKey.value);appender(\n成功发送杀死指令)} catch (e) {appender(\n杀死作业失败 e.message)} finally {stopping.value false;}},restore: () {Modal.confirm({title: 请确认操作,content: 该操作会将上次运行的构建结果替换到当前环境运行并且不可撤销请确认操作,okText: 我确定,cancelText: 还是不了,onOk: async () connector(restore),})},loadFiles: async () {files.value await socket.value.listFile();},loadHistories: async visible {try {histories.value visible ? await socket.value.listHistory() : [];} catch (e) {message.error(e.message);}},editFile: async ({ key }) {try {const body { key };if (!key) {Object.assign(body, await socket.value.createFile(config.json))} else if (key more) {Object.assign(body, await socket.value.createFile())} else {body.content await socket.value.getFile(key)}editing.value body;} catch (e) {message.warn(e.message || e);}},saveFile: async () {const close message.loading(正在保存中..., 0);try {editing.value.loading true;const { key, content } editing.value;await socket.value.saveFile(key, content);editing.value { content: };message.success(保存成功)} catch (e) {message.error(e.message);} finally {close();editing.value.loading false;}},cmOptions: {mode: text/javascript, // Language modetheme: material-darker, // ThemelineNumbers: true, // Show line numbersmartIndent: true, // Smart indentviewportMargin: 350,indentUnit: 2, // The smart indent unit is 2 spaces in lengthfoldGutter: true, // Code foldingstyleActiveLine: true, // Display the style of the selected row},}},
}
/script!-- Add scoped attribute to limit CSS to this component only --
style scoped
h3 {margin: 40px 0 0;
}ul {list-style-type: none;padding: 0;
}li {display: inline-block;margin: 0 10px;
}a {color: #42b983;
}.primary {margin-right: 20px
}.console {}.divider {margin: 0 15px;
}.opt-group {display: inline-block;line-height: 50px;
}media screen and (max-width: 954px) {.opt-group {display: block;}.divider {display: none;}
}
/style3. 项目结构
web/
├── src/
│ ├── components/ # 组件
│ │ └── Deploy.vue # 部署控制台组件
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── public/
│ └── banner.jpg # 静态资源
├── babel.config.js # babel配置
└── vue.config.js # Vue CLI配置
至此我们实现了
实时部署状态监控运行日志实时查看配置文件在线编辑版本回滚功能项目重启功能
总结一下这波操作采用 Vue 3 Ant Design Vue 的技术栈实现了一个功能完整的智能部署控制台。通过 WebSocket 实现了与后端的实时通信使用 CodeMirror 提供了良好的代码编辑体验。
Python异步部署服务端
经过技术的吸收我实现了基于Python 3.9的异步部署工具主要特点:
基于WebSocket的全双工实时通信
支持自定义部署脚本
支持配置热加载
支持多用户管理
支持部署历史版本管理
技术栈 Python 3.9 aiohttp - 异步Web框架 python-socketio - WebSocket库 SQLite3 - 轻量级数据库 watchdog - 文件监控
核心实现
1. WebSocket服务器
使用python-socketio实现WebSocket服务器:
# 初始化socketio服务器
sio socketio.AsyncServer(async_modeaiohttp,cors_allowed_origins[http://localhost:7777, http://deploy.flyfish.group])
app web.Application()
sio.attach(app)# 处理连接事件
sio.event
async def connect(sid, environ):user validate_token(environ[aiohttp.request], sid)# 缓存客户端clients[sid] {process: None, killed: False, name: user[name]}await send(sid, {success: True, user: {name: user[name], authority: user[authorities]}})# 处理断开事件
sio.event
async def disconnect(sid):if sid in clients:process clients[sid][process]if process:await kill_pipeline(sid, {pid: process.pid})del clients[sid]
2. 异步管道实现
使用asyncio.create_subprocess_shell创建子进程,实现命令执行:
# 打开管道
sio.event
async def open_pipeline(sid, message):# 取得类型和命令pipe_type message[type]command configs[scripts][pipe_type]# 启动子进程proc await asyncio.create_subprocess_shell(fcd {configs[work_dir]} {command} {client[name]},stdoutasyncio.subprocess.PIPE,preexec_fnos.setsid)# 返回成功await send(sid, {success: True, pid: proc.pid, rid: message[rid]})# 等待提交await submit(sid, proc)# 异步读取输出
async def submit(sid, proc):item clients[sid]item[process] procwhile True:# 异步读取输出line await proc.stdout.read(BLOCK_SIZE)if not line:break# 实时推送到客户端 await send(sid, {success: True, content: str(line, encodingutf-8)})
3. 配置热加载
使用watchdog监控配置文件变化:
# 配置文件监听器
class ConfigFileHandler(FileSystemEventHandler):def on_modified(self, event):path event.src_pathif path.endswith(config.json):print(修改了配置文件尝试加载...)if load_config():print(配置文件已经重载)# 初始化监听
async def init_app():observer Observer()observer.schedule(ConfigFileHandler(), ./)observer.start()load_config()return app
4. 数据库操作封装
使用上下文管理器封装SQLite操作:
class SqlSession:def execute(self, sql, param()):with self.conn:cursor self.conn.cursor()try:return cursor.execute(sql, param)except sqlite3.Error as e:cursor.close()raise eclass SqlOperation:# 插入操作def insert(self, data):if id in data:del data[id]data[create_time] datetime.now().strftime(%Y-%m-%d %H:%M:%S)keys data.keys()values data.values()sql finsert into {self.table} ({,.join(keys)}) values ({,.join([?] * len(keys))})self.session.execute(sql, tuple(values))
5. Webhook实现
实现Gitee的Webhook接收能够自动部署持续集成
app.route(/deploy, methods[POST])
def post_data():# 验证tokentoken request.headers.get(X-Gitee-Token)if token ! gitee_secret:return token认证无效, 401# 获取推送信息data json.loads(request.data)name data[pusher][name]# 执行部署脚本os.system(fsh deploy.sh {name})return jsonify({status: 200})
项目结构
hooks/
├── bin/ # 核心代码
│ ├── app.py # WebSocket服务器
│ ├── db.py # 数据库操作
│ ├── hook.py # Webhook接收器
│ └── post.py # 消息推送
├── logs/ # 日志目录
└── requirements.txt # 依赖配置
总结
到此我们实现了以下所有能力
1. 全异步通信
使用aiohttp和python-socketio实现全双工通信
异步子进程管理,实时输出
支持多用户并发操作
实时配置
配置文件热加载
支持自定义部署脚本
支持工作目录配置
用户管理
基于Token的认证
会话管理
权限控制
4. 部署管理
支持部署历史
支持版本回滚
支持运行日志查看
通过以上努力我采用Python异步编程实现了一个功能完整的部署工具通过WebSocket实现了与前端的实时通信支持多用户并发操作。 结束语 - 让部署不再是996的理由
写在最后
各位看官读到这里相信你已经发现这不是一个普通的部署工具而是一个能让你告别部署恐惧症的神器
从此告别的场景
再也不用半夜被运维电话叫醒服务器挂了
不用每次部署都像在玩俄罗斯轮盘赌
告别在我电脑上能运行系列尴尬
不用为搞错配置而痛哭流涕
你将收获的快乐
一键部署比订外卖还快
实时日志像看抖音一样上瘾
版本回滚时光机般的存在
配置热加载改完配置说走就走
写给犹豫的你
如果你还在为以下问题困扰
部署靠祈祷
改配置要跪求
看日志要冥想
回滚要许愿
那么来试试这个工具吧它不仅能让你的部署工作变得轻松愉快还能让你在同事面前装个小小的技术大佬。
彩蛋时间
知道为什么我们选择 WebSocket 吗
因为 HTTP 太慢了慢得像极了周一的早晨
因为实时通信快得像极了发工资的瞬间
因为全双工通信比你谈恋爱还要双向奔赴
最后的最后
记住这个工具的诞生不是为了让你加班而是为了让你有更多时间
摸鱼
追剧
打游戏
谈恋爱
如果这个项目帮你节省了时间别忘了给我们点个星⭐️
如果没帮你节省时间...那一定是你还没用熟练
愿你的每一次部署都像喝可乐一样爽快
愿你的每一次发版都像春游一样愉快
愿你的每一次回滚都像退货一样简单
结语中的结语
记住我们的口号 部署不再难生活更自然 配置不用愁周末早回家 日志一目了然Bug无处遁藏 最后送大家一句话 工具再好也补不了你的bug 但至少...它能让你改bug的时候心情好一点 好了快去试试吧让我们一起告别996拥抱995.9 注本项目副作用可能包括但不限于让你对其他部署工具产生严重的依赖性鄙视让你的同事对你投来羡慕的眼光让你的老板觉得你太闲需要安排更多任务... 代码下载 是的我们开源啦 为什么要开源
因为我们相信
好的代码应该像老婆的美貌一样值得炫耀
优秀的项目应该像奶茶一样值得分享
牛逼的工具应该像八卦一样让更多人知道
下载地址奉上希望大家支持开发不易请尊重博主的劳动成果
https://download.csdn.net/download/wybaby168/90373568