• 一种让运行在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>
    View Code

    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
    View Code

    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即可

  • 相关阅读:
    C语言I博客作业04
    解决@ResponseBody注解返回的json中文乱码问题
    自定义 Helper Method
    webapi 学习1
    分布式事务TransactionOptions及设置msdtc
    了解websocket是什么
    用postman 来实现post方式返回json数据
    异步 Controller
    asp.net mvc Filter
    asp.net mvc Controller Factory
  • 原文地址:https://www.cnblogs.com/wdw984/p/16426163.html
Copyright © 2020-2023  润新知