传统应用实现快速部署的一种方案

背景介绍

我们有一个web项目,采用的部署方式是单体应用(dotnet没有core)+负载均衡器(nginx)来实现的分布式部署。由于近期用户量激增,负载机器由原来的2台增加到了8台。这就造成了另外一个问题,那就每次加机器、软件更新、版本回滚,都会变得非常麻烦、痛不欲生,再也不能好好的摸鱼了,人生浪费在那些没技术含量的重复性工作上。

那有老铁可能就说了,为啥不该造成微服务的架构体系,通过kubernetes来一展身手,让你轻松管理成百上千台机器集群,谈笑间,樯橹灰飞烟灭,岂不美哉!

哥们只能说,铁子,我也想享受一手掌控所有的快感,可实际情况还不允许,但已经积极往那个方向发展了。

远水难解近渴,现在的情况亟需一个半临时方案,来解决这个重复劳动的问题。

既然没有现成的解决方案,那就自己手撸一个吧!

整活

手动操作的流程

琢磨一下负载少的时候,人力来更新整个的站点更新流程通常包括以下几个环节。

  • 为了避免更新出错,在更新之前,要将原来稳定运行站点的版本先备份。当然了,也不是所有的文件都需要备份,会造成存储压力,只是根据实际情况备份关键的文件,但至于是哪些关键文件,就需要每次根据情况而定了
  • 打包新发布好的文件
  • 上传服务器,替换原来的文件
  • 如果每次更新的包括服务端文件,则需要重启iis站点(目前我们的web端业务都是跑到iis上的,但实际应该可以扩展到apache,tomcat,nginx等站点服务器的)

自动操作的流程

在整理好更新部署的整个流程之后,我这里把整个的更新流程整理成了以下几个阶段

  • 监听:该阶段,通过程序定时监听某个文件的变化,这个文件我这里设置的是一个json格式文件,里面记录了版本号,新文件的下载地址这些信息,每次检测到版本号变化,就开始自动执行后续流程;
  • 下载文件:这部分就是根据监听阶段获取到的下载地址,来下载文件到指定的位置;
  • 暂停站点(可选):由于并不是每次更新都需要重启站点的,这一步就是可选的;
  • 备份:根据传入的备份参数,进行文件备份,并生成备份日志;
  • 解压:备份完成后,就可以解压第二部下载的文件了,完成新旧文件更替;
  • 重启站点(可选):如果暂停了站点,这里在完成解压换届后要再次启动站点;
  • 通知:整个更新流程结束后,要发送邮件通知给相关开发,运维等人员,告知此次自动更新的关键信息。

明确了以上几个大的流程,就可以开始上代码了!

我这里是用的是控制台结构,用到了几个重要的工具包,包括Coravel,Serilog,SharpCompress,Hosting等。

文末奉上完整代码地址

监听

因为是控制台程序,所以要实现类似web程序那样可以监听访问url的动作,需要引入Microsoft.Extensions.Hosting包。

引入之后,就可以结合Coravel做定时监听了。

public static void UseCoravelService(string[] args)
{
    // Renders each item with own style
    bool intervelFlag = true;
    var process = new Rule().RuleStyle("#FDE047");
    var host = new HostBuilder()
         .ConfigureAppConfiguration((hostContext, configApp) =>
         {
             configApp.AddEnvironmentVariables(prefix: "PREFIX_");
             configApp.AddCommandLine(args);
         })
         .ConfigureServices((hostContext, services) =>
         {
             // Add Coravel's Scheduling...
             services.AddScheduler();
         })
         .Build();

    host.Services.UseScheduler(scheduler =>
        scheduler
        .Schedule(async () =>
        {
            //核心业务
            //...
        })
        .EveryFifteenSeconds()
    );
    host.Run();
}

引入网络代理和Coravel之后,后续的操作就是业务代码了

/// <summary>
/// 监听
/// </summary>
internal class StepMonitor
{
    public static async Task<bool> getJsonFile(string address)
    {            
        try
        {
            if (string.IsNullOrEmpty(address))
                address = "<我这里给了一个默认地址,也可以不要>";
            HttpClient hc = new HttpClient();
            var content = await hc.GetStringAsync(address);
            var json = JsonHelper.JsonDeserialize<VersionModel>(content);
            string versionFile = "currVersion.txt";
            string currVersion = "0";

            currVersion = await MainScheduling.ReadFile(versionFile);
            if(currVersion.Trim()==json.Version) 
            {
                return false;//线上版本和本地版本相同,继续等待;
            }
            
            await MainScheduling.WriteFile("currVersion.txt", json.Version);
            await MainScheduling.WriteFile("DownloadList.json", JsonHelper.JsonSerialize(json.Items));
            return true;
        }
        catch (Exception ex)
        {
            AnsiConsole.WriteLine($"[red]下载更新索引文件失败,{ex.Message}[/]");
            throw;
        }

    }
}

定义好监听的相关方法后,就可以回到Schedule里进行调用了

host.Services.UseScheduler(scheduler =>
        scheduler
        .Schedule(async () =>
        {
            string cache = CacheManager.Default.Get<string>("step");
            AnsiConsole.MarkupLine($"[#1E9FFF]当前状态:【{cache}】,{DateTime.Now.ToString()}[/]");
            if (cache == "waiting")
            {
                intervelFlag = false;
                OutputStep(0, "监听中(waiting),正在执行...");
                string file = GetParamValue(args, "file");
                if (await StepMonitor.getJsonFile(file))
                {
                    //CacheManager.Default.Set_SlidingExpire<string>("step", "download", TimeSpan.FromMinutes(10));
                    await SetStep("download");
                }
                intervelFlag = true;
            }
        })

最终监听的效果如下

0a1229313e7688cd09e543bba68dea07.png

下载

这部分没啥可说的,可以根据自己喜欢的方式,把文件下载下来就好,甚至调用wget这些成型工具也不是不可,只是做好和程序的信息交互就好。

我这里就不贴下载部分的代码了,可以到源码里查看,截图如下

ca1d62e533538b8a846594a29b53981d.png

关闭站点(可选)

这里是否关闭站点的业务判断我是根据version文件里是否包含static关键字来判定的。

string currVersion = await ReadFile("currVersion.txt");
if (currVersion.Contains("static"))
{
    OutputStep(2, "仅更新静态文件,无需重启站点");
}
else
{
    OutputStep(2, "站点关闭中(stopweb),正在执行...");                            
    string appSite = GetParamValue(args, "appSite");
    string appPool = GetParamValue(args, "appPool");
    StepIISManager.Stop(appSite, appPool);                                                    
}

两种情况的截图如下

2acbd9f5e29b5110e29b61d657749d0a.png

45ae0898e89be29028930798c789f268.png

备份文件

备份文件,我这里设置了一个专门的传入参数“-subpath”,如果不传入这个参数的话,会对站点进行全量备份,传入的话,则会根据传入的文件或者目录进行备份。

//这里的参数是解压地址
string inputPath = GetParamValue(args, "output");
string outputPath = GetParamValue(args, "backuppath");

string currVersion = await ReadFile("currVersion.txt");
string subPath = GetParamValue(args, "subpath");
OutputStep(3, "站点备份中(backup),正在执行...");
if (!string.IsNullOrEmpty(subPath))
{
    AnsiConsole.MarkupLine("[#FDE047]检测到子目录参数,不再进行全量备份,正在顺序备份[/]");
    if (!string.IsNullOrEmpty(subPath))
    {
        string[] parts = subPath.Split(',');
        var paths = parts.Where(u => !u.Contains(".")).ToList();
        var files = parts.Where(u => u.Contains(".")).ToList();
        if (files.Any()&&paths.Any(u=>!u.Equals("_temp")))
        {
            paths.Add("_temp");
        }
        foreach(var file in files)
        {
            AnsiConsole.MarkupLine($"[#FDE047]文件{file}备份中...[/]");
            string subInputPath = Path.Combine(inputPath, file);
            if (File.Exists(subInputPath))
            {
                string tempPath = Path.Combine(inputPath, "_temp");
                if (!Directory.Exists(tempPath))
                {
                    Directory.CreateDirectory(tempPath);
                }
                File.Copy(subInputPath, Path.Combine(tempPath, file), true);                                                                                
            }
        }
        foreach (var item in paths)
        {
            
            AnsiConsole.MarkupLine($"[#FDE047]{item}备份中...[/]");
            string subInputPath = Path.Combine(inputPath, item);                                    
            await StepDeCompress.ZipCompress(subInputPath, outputPath, item);
        }
    }
}
else
{
    await StepDeCompress.ZipCompress(inputPath, outputPath);
}

6e773434141f2bdf78f3c886a46d1bf7.png

解压

解压就是把第二步下载的文件解压到指定目录,接住了sharpcompress组件的能力

/// <summary>
/// 解压文件
/// </summary>
/// <param name="inputFile">解压文件路径</param>
/// <param name="outputFile">解压文件后路径</param>
public static void Decompression(string inputFile, string outputFile)
{
    try
    {
        if (!inputFile.Contains(":"))
        {
            inputFile = Path.Combine(Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName), inputFile);
        }
        SharpCompress.Readers.ReaderOptions options = new SharpCompress.Readers.ReaderOptions();
        options.ArchiveEncoding.Default = Encoding.GetEncoding("utf-8");
        var archive = ArchiveFactory.Open(inputFile, options);
        AnsiConsole.Status()
            .Start("解压文件...", ctx =>
            {
                foreach (var entry in archive.Entries)
                {
                    if (!entry.IsDirectory)
                    {                        
                        entry.WriteToDirectory(outputFile, new ExtractionOptions { ExtractFullPath = true, Overwrite = true });
                    }
                }
                AnsiConsole.MarkupLine("[#1E9FFF]解压完成[/]");
            });
    }
    catch (Exception ex)
    {
        AnsiConsole.WriteLine($"[red]{ex.Message}[/]");
        throw;
    }
}

效果图

f6a30980c4a55eb22a1be0b2dd39ed03.png

重启站点(可选)

和暂停站点的步骤一样,只是传入的命令变成了start

if (currVersion.Contains("static"))
{
    OutputStep(5, "仅更新静态文件,无需重启站点");
}
else
{
    OutputStep(5, "站点启动中(startweb),正在执行...");                            
    string appSite = GetParamValue(args, "appSite");
    string appPool = GetParamValue(args, "appPool");
    StepIISManager.Start(appSite, appPool);
    Console.WriteLine(DateTime.Now.ToString());
}

效果

6bcad7bf550556a31f6962cb7a61cff7.png

发送通知

这个部分其实也不用多说,就是发送一封邮件给指定的管理人员,邮件内容要包括此次更新的具体情况

我这里也不贴代码了看下效果图

ac36f149aa68988b468ee10a6f725971.png

3927d101fdadac9e5824a49f9e9b9032.png

总结

虽然现在已经是云原生的时代,各种转向微服务的框架层出不穷,但有一个不能忽视的现状就是,单体应用在如今占比仍然十分可观。尤其是在小微企业甚至相当部分的中大型企业的项目,单体应用应该还是占到了主流,因为自身业务本身的局限,有些业务可能从生到死都不适合转到微服务行列。这是现实,也是遗憾。

我个人对微服务的态度基本是信徒级别,而且在积极转型拥抱,团队领导也支持我们将现有的业务结合实际情况按部就班的转成微服务架构,但毕竟是已经投入生产的项目,还是要以稳定为第一要务,因此我们的步子迈的要慢很多。大量的业务还是基于单体应用的结构在运行,所以主要的开发和维护任务还是以实际的业务为准,对转型我们是谨慎乐观的。我觉得这是客观,积极且正确的态度,创新本身就是一件令人着迷,却又前途未卜的事情,但过于投入,甚至做一些极端的操作,会造成无法预估的风险。从实际出发,结合业务情况,按部就班才是正途。我也相信在2023年,我们终将在转型道路上迈出最为关键的一步。

代码地址:为自己_带盐 / AutoDeploy · GitCode

猜你喜欢

转载自blog.csdn.net/juanhuge/article/details/128677380