一种让运行在CentOS下的.NET CORE的Web项目简单方便易部署的自动更新方案
一、项目运行环境
项目采用的是.NET5开发的Web系统,独立部署在省内异地多台CentOS服务器上,它们运行在甲方专网环境中(不接触互联网),甲方进行业务运作时(一段时间内)会要求异地服务器开机上线,同时要求我们在总部进行驻场运维和技术支持。
二、自动更新需求
每年(次)的业务流程甲方会要求做出一些调整,要求在线的服务器可以自动更新。
异地服务器对使用人员处于黑盒模式,同时项目可以运行在root权限下。
三、自动升级方案对比
1、Jenkins+Gitlab+自动代码审查+人工代码审查+人工发布更新;
2、Docker构建私有源,上游更新镜像后下游拉取新镜像启动;
3、国人开发的AntDeploy(https://github.com/yuzd/AntDeploy)
- 支持docker一键部署(支持netcore)
- 支持iis一键部署(支持netcore和framework)
- 支持windows服务一键部署(支持netcore和framework)
- 支持linux服务一键部署(支持netcore)
- (支持增量发布)(支持一键回滚)(支持点火)(支持选择特定文件发布)(支持查看发布记录)
- 支持脱离Visual Studio独立使用(跨平台支持windows系统和mac系统)
- 支持Agent批量更新
4、国人开发的GeneralUpdate(https://gitee.com/Juster-zhu/GeneralUpdate)
GeneralUpdate寓意为通用更新力致于成为全平台更新组件,包含常见个人、企业项目所需特性。并提供GeneralUpdate.PacketTool更新包打包工具。不过目前好像尚不支持.NET CORE的更新。
因为我们只有在甲方业务运行期间才有服务器的使用权,异地部署的服务器的使用人员不掌握服务器密码和不具备Linux操作能力,同时由于种种原因我们不能也不方便在甲方内网中部署Jenkins和Docker服务,加上现有的几种自动更新(持续交付)方案对我们来说比较复杂,所以我们只有另辟蹊径寻找一种对我们来说简单实用易部署的方案。
四、使用的自动升级方案
在我们开发另外一套客户端程序的时候,集成过一套自动更新组件(SimpleUpdater),简单描述一下就是它可以在客户端程序启动后到指定的http地址下载更新摘要文件和本地对比,如果远程版本高于本地版本则提示更新,更新过程就是从远程web服务器下载下来更新包解压后按照规则替换当前程序目录下的文件,从而实现更新的目的。
基于这个流程,通过试验我们实现了这种基于HTTP服务器提供更新服务,可以让Web项目自动更新自己的解决方案。
方案搭建起来相当简单,只需要架设一台提供HTTP服务的服务器(IIS、Nginx等都可以),然后Web服务器上放一个Json文件和更新压缩包(zip格式),Json文件中包含当前Web系统的版本号和下载地址。
当异地服务器启动,使用人员访问系统的时候,后台会开一个进程通过HTTP请求的方式到升级服务器(appsettings.json中可配置地址)访问约定的Json文件,访问成功后解析得到服务器端的版本号,然后和本地版本号做对比,如果服务器版本号较新,就调用一个sh脚本下载Json文件中指定路径的更新包,
sh脚本下载成功后停止当前Web系统,进行解压覆盖,覆盖完成后重新启动Web服务。(我们的Web项目采用的是Kestrel提供代理服务,supervisor进行守护。)
这样就简单方便快捷的实现了基于CentOS的.NET CORE项目的自动更新。
五、升级流程及代码
1、部署一台提供升级的服务器,提供HTTP服务,我们使用了Windows服务器+IIS模式,和甲方约定这台服务器的IP地址为升级专用,不分配给其它服务器使用。
2、.NET CORE项目的appsettings.json中配置服务器IP地址。
3、在项目的登录页后台代码中标识当前版本,同时在访问的时候开进程去访问升级服务器(前台访问后台检测升级接口,同时可以采用遮罩阻止用户登录),进行升级检测流程。
后台代码:
public class LoginController : Controller
{
private readonly ILogger<LoginController> _logger;
private readonly int _webVer = 1001;//当前运行中的系统版本号
public LoginController(ILogger<LoginController> logger)
{
_logger = logger;
} public IActionResult Login()
{
//其它业务代码
return View();
}
#region 检测更新
public async Task<JsonResult> CheckUpdateAsync()
{
await Task.Delay(1000);
//AppSettings为读取appsettings.json中相关配置的实体类,这里是伪代码
if (!AppSettings.ContainsKey("key1") || string.IsNullOrWhiteSpace(AppSettings["key1"])) return new JsonResult(new { code = 300, msg = "未能获取到更新配置" }); try
{
var restClient = new RestClient($"{AppSettings["key1"]}/update.json");
var restRequest = new RestRequest("", Method.GET); var cancelToken = new CancellationTokenSource(TimeSpan.FromSeconds(15)); var response = await restClient.ExecuteGetAsync(restRequest, cancelToken.Token);
if (response.StatusCode != HttpStatusCode.OK)
{
_logger.LogError($"检测升级失败,服务器状态:{response.StatusCode}");
return new JsonResult(new { code = 300, msg = "检测升级失败" });
} var responseContent = response.Content;
if (string.IsNullOrWhiteSpace(responseContent))
{
_logger.LogInformation("更新内容为空");
return new JsonResult(new { code = 300, msg = "升级更新内容为空" });
} var content = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseContent);
if (content == null)
{
_logger.LogInformation("更新内容序列化后为空");
return new JsonResult(new { code = 300, msg = "更新内容序列化后为空" });
}
#region 唤醒更新脚本
var argument = content; if (!argument.ContainsKey("webver") || !argument.ContainsKey("weburl"))
{
_logger.LogError("检测升级失败,升级文件中没有获取到必须的Web项目。");
return new JsonResult(new { code = 300, msg = "升级项中不包含本项目" }); ;
}
await Task.Factory.StartNew(async () =>
{
TryParse(argument["webver"], out var ver);
if (ver > 0 && ver > _webVer)
{
await Task.Delay(1000);
var sh = $@"{Directory.GetCurrentDirectory()}{Path.DirectorySeparatorChar}update.sh";
_logger.LogError("检测到升级条件,开始唤醒升级脚本");
try
{
await Process.Start(sh, $" {argument["weburl"]}")?.WaitForExitAsync()!;
}
catch (Exception e)
{
_logger.LogError($"更新脚本执行失败:{e}");
}
}
}, TaskCreationOptions.LongRunning);
#endregion }
catch (Exception e)
{
_logger.LogError($"加载更新数据失败:{e}");
return new JsonResult(new { code = 300, msg = "加载更新数据失败" });
} return new JsonResult(new { code = 200, msg = "检测成功" });
}
#endregion
}
前台检测更新代码(基于LayUI)

1 <script type="text/javascript">
2 layui.use('layer');
3 $(document).ready(function () {
4 var index = layer.open({
5 type: 1,
6 area: ['400px', '260px'],
7 id: 'layer_update',
8 resize: false,
9 title: '正在检测更新',
10 closeBtn: 0,
11 shadeClose: false,
12 content: '<div class="layui-field-box">正在从服务器获取新版本信息,请勿重复刷新页面。</div>'
13 });
14 $.ajax({
15 type: 'POST',
16 url: '/login/CheckUpdate',
17 data:'',
18 dataType: "json",
19 success: function (result) {
20 if (result != '' && result != 'undefined') {
21 if (result.code != "200") {
22 layer.alert(result.msg, {
23 title: '错误'
24 });
25 return;
26 }
27 }
28 else {
29 layer.alert('返回数据错误。', {
30 title: '错误'
31 });
32 }
33 },
34 complete: function (xhr, ts) {
35 layer.close(index);
36 }
37 });
38 });
39 </script>
4、执行更新操作的sh脚本代码,脚本执行后面的第一个参数即为更新包下载地址,注意脚本不要用记事本编辑,最好使用vscode来编辑。

1 #!/usr/bin/env bash
2
3 source /etc/profile
4 date=$(date)
5
6 if [ -z $1 ];then
7 echo "请添加下载路径"
8 exit
9 fi
10
11 wget -P /tmp $1 -O /tmp/www.zip
12 if [ $? -ne 0 ] ;then
13 echo "----------------下载失败---------------" >> /root/update.log
14 exit
15 else
16 echo "----------------下载成功---------------" >> /root/update.log
17 fi
18
19 mkdir /tmp/Webupdate
20 cd /tmp/Webupdate
21 unzip /tmp/www.zip
22
23 if [ $? -ne 0 ] ;then
24 echo "-----解压失败-----" >> /root/update.log
25 rm -rf /tmp/Webupdate
26 #WebName 是自己定义的用supervisor守护的web服务名称
27 supervisorctl start WebName
28 exit
29 else
30 echo "-----解压成功-----" >> /root/update.log
31 #WebName 是自己定义的用supervisor守护的web服务名称
32 supervisorctl stop WebName
33 #/usr/local/www/ 是web所在目录
34 mv -f /tmp/Webupdate/* /usr/local/www/
35
36 supervisorctl start WebName
37 supervisorctl status WebName
38 rm -rf /tmp/Webupdate
39 rm -rf /tmp/www.zip
40 fi
5、Json文件结构
{
"webver":1009,
"weburl":"http://192.168.12.25/web.zip"
}
6、后续
因为mv命令在使用中没办法移动目录去覆盖程序目录,比如压缩包中有个 wwwroot/abc.js,在使用中发现mv命令好像没有办法把wwwroot/abc.js移动覆盖项目中的同路径文件,所以后续我们在更新脚本中使用了rsync命令,这个命令需要单独安装才可以使用。
可以先去下载安装这个组件后把sh脚本中的mv命令换成rsync即可
一种让运行在CentOS下的.NET CORE的Web项目简单方便易部署的自动更新方案的更多相关文章
- macOS Sierra Version 10.12.6 环境下Tomcat的下载与安装以及InterlliJ IDEA 2017.2 环境下配置Tomcat 与创建Web项目
一.Tomcat的下载与安装 1.官网(http://tomcat.apache.org/)下载Tomcat 9.0 Core:zip包: 2.解压到指定的文件夹即可安装完成: 3.测试是否安装成功 ...
- Web项目打成war包部署Tomcat时运行startup.bat直接闪退部署失败解决方案
即上篇通过将web项目打成war包部署到Tomcat服务器,解决mysql问题后,又出现了新问题,真是一波三折,所以将解决过程分享给大家,希望能帮助到小伙伴们~ 将打好的war包拷贝到Tomcat的w ...
- SpringMVC框架下Web项目的搭建与部署
这篇文章已被废弃. 现在,Deolin使用Maven构建项目,而不是下载Jar文件,使用Jetty插件调试项目,而不是外部启动Tomcat. SpringMVC比起Servlet/JSP方便了太多 W ...
- Mac下Intellij IDea发布Java Web项目详解五 开始测试
测试前准备工作目录 Mac下Intellij IDea发布Web项目详解一 Mac下Intellij IDea发布Java Web项目(适合第一次配置Tomcat的家伙们)详解二 Mac下Intell ...
- CentOS下安装.net core环境并部署WebAPI
1.安装CentOS 7 2.安装.net Core 2环境,参考官方文档:(建议采用SDK (tar.gz)安装) https://www.microsoft.com/net/download/li ...
- 如何在IntelJ下用Maven创建一个Web项目
相信一开始做Web项目的同学都是直接把项目包放在WEB-INF/lib目录下,然后就开始做项目了.但是之后接触了一些使用了Maven进行包管理的项目,我们就不得不学一学如何用Maven做包管理. 在网 ...
- Eclipse 导入外部项目无法识别为web项目并且无法在部署到tomcat下
uss_web如果没有左上角那个球,tomcat就识别不出来的. 1.进入项目目录,找到.project文件,打开. 2.找到...代码段,加入如下标签内容并保存: <nature>org ...
- Maven构建web项目在Eclipse中部署的几种方法
目录: 方法一:运用Maven的plugin:jetty来部署web 方法二:运用Eclipse 的Jetty插件直接部署 方法三:运用Run on Server(tomcat)部署 [方法一].运用 ...
- Mac下Intellij IDea发布Java Web项目详解四 为所有Module配置Tomcat Deployment
准备工作1:新建第一个JavaWeb项目 准备工作2:新建Module step5 为所有项目配置Deployment 5.1 如图 5.2 [+][Artifact] 5.3 将这里列出的所有内容选 ...
随机推荐
- 【Git】一台电脑与多个分布式版本管理平台连接
六. 一台电脑与多个版本控制平台 1. 一台电脑同时通过ssh连接github和码云gitee 打开git bash 进入.ssh cd ~/.ssh 分别生成两个平台的公钥和私钥 $ ssh-key ...
- 【CSAPP】Attack Lab实验笔记
attacklab这节玩的是利用一个字符串进行缓冲区溢出漏洞攻击,就小时候想象中黑客干的事儿. 做题的时候好几次感叹这些人的脑洞,"这都可以攻击?还能这么注入?这还可能借力打力?" ...
- 10个最危险的Linux命令,希望你牢记在心
点击上方"开源Linux",选择"设为星标" 回复"学习"获取独家整理的学习资料! 来源:Linux迷链接:https://www.linu ...
- C++进阶-1-模板基础(函数模板、类模板)
C++进阶 模板 1.1 函数模板 1 #include<iostream> 2 using namespace std; 3 4 // 模板 5 6 // 模板的简单实例 7 // 要求 ...
- 使用 .net + blazor 做一个 kubernetes 开源文件系统
背景 据我所知,目前 kubernetes 本身或者其它第三方社区都没提供 kubernetes 的文件系统.也就是说要从 kubernetes 的容器中下载或上传文件,需要先进入容器查看目录结构,然 ...
- 【python疫情可视化】用pyecharts开发全国疫情动态地图,效果酷炫!
一.效果演示 我用python开发了一个动态疫情地图,首先看下效果: 如图所示,地图根据实时数据通过时间线轮播的方式,动态展示数据的变化.随着时间的推移,疫情确诊数量的增多,地图各个省份颜色逐渐加深, ...
- C# 随机给一个全部信息都未知的类类型,如何获取该类的类名、属性个数、属性名、属性的数据类型、属性值?
一.场景假设 假设现在有一个泛型类T的实例对象t,该T类的全部信息都未知. 要求:打印输出实例对象t的类名.属性个数.属性名.属性的数据类型.属性值. 二.解决问题 1.我们根据输出的内容要求定义一个 ...
- Redis源码漂流记(二)-搭建Redis调试环境
Redis源码漂流记(二)-搭建Redis调试环境 一.目标 搭建Redis调试环境 简要理解Redis命令运转流程 二.前提 1.有一些c知识简单基础(变量命名.常用数据类型.指针等) 可以参考这篇 ...
- 批量上传文件或者上传大文件时 gateWay报错DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144
一.描述 最近在批量上传文件时网关出现了异常,后面发现上传大文件也会出现文件超过256发生异常,异常信息如下: org.springframework.core.io.buffer.DataBuffe ...
- 免费yum源镜像地址
收集的镜像,yum源等网站地址 阿里巴巴开源镜像站 https://opsx.alibaba.com/mirror http://mirrors.aliyun.com/centos/ 网易开源镜像站 ...