【G】开源的分布式部署解决方案(三) - 一期规划定稿与初步剖析
G.系列导航
抱歉
首先我先说声抱歉,因为上一篇结尾预告第三篇本该是“部署项目管理”,那为什么变成本篇呢?
请容我解释一下,在预告篇到现在为止,经常会有人问我这个项目到底是干什么的。或许之前写的比较粗糙。那我相信目前定稿后的功能概览图应该会给大家一个比较清晰的认识。
另外也有不少人问我,更新进度这么慢会不会太监?
这个请放心,不会的,因为我们公司也要用到本项目,只是没列为紧急项目内,我也是抽空写写,毕竟我们已经有个精简中的精简版在运行着,也就是本项目的雏形。
PS:以下绝大多数内容没有明显说明的话仅针对一期有效。
一期功能概览图
(点击图片看大图)
先简单解释下图标的意思
相信已经有人知道了,是的,就是进度。
虽然纯黄色的比较多,但因为本身就定位PC+Mobile双端的情况下,确实对我这个不写前端的人有一点点挑战的。
好在目前进展还算顺利,发现的坑都填上了。
PS:这是终结吗?并不是,后面还会继续维护出新功能,比如项目组内各项目依赖部署、多人部署同一项目时的实时进度跟进、项目健康检查、灰度发布支持、更人性化的UI、更严谨的逻辑,部署本身就是个大工程,先走好当下的第一步。
适用场景
单机项目:
服务器数量:1
项目类型:网站
宿主:IIS
负载均衡:无
持续集成:无
可使用单机部署,或创建环境后只加入一台服务器,后续操作同多机项目。
多机项目
服务器数量:>1
项目类型:网站 (目前仅支持网站,后期会支持服务、Job程序,允许扩展)
宿主:IIS(目前仅支持IIS,后期会支持Windows服务,允许扩展)
负载均衡:有(目前仅支持阿里云,允许扩展)
持续集成:有(目前仅支持Jenkins,允许扩展)
项目部署、版本回滚、部署后预热、按单机部署或项目级部署、环境隔离、控制负载均衡阻断请求、防止正在处理的请求中断等。
至于详细的功能剖析,会在后面的博客中
名词解释
项目组
一个虚拟分组,用于将一些业务耦合度较高的项目捆绑在一起,后续或许会根据需要扩展支持按项目组部署,可根据依赖关系等条件部署。
项目
部署基准元素,可针对项目设置一些属性来决定部署流程的走向。
环境
一个虚拟的分组,与项目组不同的是,这个分组作用在服务器上。这里有一个比较特殊的概念叫环境标签,主要是用来区分环境分类的,比如开发、测试、仿真、生产等,作为一个一级分类来使用。
而环境这个概念本身,会在创建部署任务的时候,用于关联部署应该发生在哪些服务器上的。因为环境本身又包括了服务器。这样部署概念就完成了一个闭环。
当然这个流程就发生在刚才,我写环境介绍的时候突然想到的,我漏掉了关键的一个环节就是如何让部署、项目、服务器之间联系在一起,这的确是不该发生的事情。
服务器
部署单元,作为部署真正作用到的实体,这里有个小坑,就是目前暂时不支持服务器环境的初始化。比如安装个.net之类的就需要你手动来操作了。当然,服务器多的情况下,像阿里云本身就有快照之类的服务提供。
一键部署
一个自动化操作的动作,会根据之前项目配置阶段设置好的属性、流程等自动完成部署。因为部署本身是一个很复杂的过程,从项目构建、测试、打包、上传、覆盖、重启服务等等一系列操作。这当中人为的失误以及繁琐过程实在是让人抓狂至极。通过之前一步步的操作走到这里,以及后续博客篇幅以几个示例展开讲解会慢慢揭开自动化部署的神秘面纱。
UI
拿出几个有代表性的界面给大家先看看吧
项目列表界面-PC版
项目列表界面-手机版
项目组列表-PC版
项目组列表-手机版
抽取公共类的小波折
写这个类的时候其实是因为发现单表操作的功能还是不少的,如果提炼出来一个公共操作类的话就节约了我不少的时间。
在这个单表操作公共类里我目前希望有4个方法,因为这4个的操作概率比较高。Create、Update、GetList、GetByID。
方法不难找,只是过程有点反复,有几个比较关键的信息,比如底层操作数据库用的EF,又有Entity和Model的概念,自然就少不了AutoMapper。
using Framework.Mapping.Base; using G.Client.Data.Entities.Base; using G.Client.Data.Wrapper; using G.Client.Model.DeployManage; using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Linq.Expressions; using System.Text; using System.Threading.Tasks; namespace G.Client.Infrastructure.Data.Pipeline.Database { public class SingleTableEngineer<TEntity, TKey, TListModel, TCreateModel, TEditModel> where TEntity : EntityBase<TKey> where TListModel : class where TCreateModel : class where TEditModel : class, IEditViewModel<TKey> where TKey: IEquatable<TKey> { public virtual async Task<List<TListModel>> GetList() { using (var dbContext = GDbContext.Create()) { var query = from entity in GDbContext.GetDbSet<TEntity, TKey>(dbContext).Where(entity => !entity.IsDelete) select entity; var mapper = new MapperBase<TListModel, TEntity>(); return mapper.GetModelList(await query.ToListAsync()); } } public virtual async Task<TKey> Create(TCreateModel model) { using (var dbContext = GDbContext.Create()) { var mapper = new MapperBase<TCreateModel, TEntity>(); var entity = mapper.GetEntity(model); GDbContext.GetDbSet<TEntity, TKey>(dbContext).Add(entity); await dbContext.SaveChangesAsync(); return entity.ID; } } public async Task<TEditModel> GetByID(TKey id) { using (var dbContext = GDbContext.Create()) { var entity = await GDbContext.GetDbSet<TEntity, TKey>(dbContext).SingleAsync(e => e.ID.Equals(id)); var mapper = new MapperBase<TEditModel, TEntity>(); return mapper.GetModel(entity); } } public virtual async Task<TEditModel> Update(TEditModel model) { using (var dbContext = GDbContext.Create()) { var entity = await GDbContext.GetDbSet<TEntity, TKey>(dbContext).SingleAsync(e => e.ID.Equals(model.ID)); entity = new MapperBase<TEditModel, TEntity>().GetEntity(model, entity); entity.SetUpdateInfo(); GDbContext.GetDbSet<TEntity, TKey>(dbContext).Attach(entity); dbContext.Entry(entity).State = EntityState.Modified; await dbContext.SaveChangesAsync(); return model; } } } }
Not Support Exception
整个类提炼过程并没有想象中那么复杂,就是遇到了个小坑。
刚开始的时候TKey是没有做约束的,这样再调用 SingleAsync的时候就报错了。
首先不做约束 == 就不能使用,只能用Equals,那对于SingleAsync这个方法来说就变成了 object.Equals(object) ,而EF也大大方方的抛出了一个 Not Support Exception。
后来加了一行代码 where TKey: IEquatable<TKey> ,轻松搞定。
丑陋的GetDbSet
因为要根据泛型获取到DbSet,所以DbContext暴露了一个GetDbSet的方法。
然后就是操作公共类中出现的 DbContext.GetDbSet<TEntity, TKey>(dbContext) ,其实只不知道原由的话第一反应是上面的using已经Create了一个DbContext为什么下面还这么复杂的GetDbSet又把这个DbContext作为参数给扔了回去。
这我真的只能说,已经拖了很多进度就没多少心情纠结这个事情了。
小技巧
作为一个做C/S转B/S的人,深深的感觉到要踩的坑还有太多太多。比如一个创建页面,text不赋值的时候是null,而我的数据库设计是不允许为null的,但业务又允许为空,那我希望这个值是空字符串。
所以之前傻傻的先 a??"" 这样写了一下。后来越来越觉得不对劲,本着自己的经验,猜测DataAnnotation应该会处理这样的数据,毕竟从很多人的设计角度上来说是应该有这样的功能的。
于是出现了下面这段代码
namespace G.Client.Model.DeployManage.DeployProjectModels { public class CreateDeployProjectViewModel { [Required, StringLength(50, MinimumLength = 2)] public string Name { get; set; } public int DeployProjectGroupID { get; set; } public DeployProjectType Type { get; set; } public DeployHostType Host { get; set; } [Required(AllowEmptyStrings = true), StringLength(50, MinimumLength = 0)] [DisplayFormat(ConvertEmptyStringToNull = false, NullDisplayText = "")] public string LoadBalanceIdentity { get; set; } } }
[Required(AllowEmptyStrings = true), StringLength(50, MinimumLength = 0)]
[DisplayFormat(ConvertEmptyStringToNull = false, NullDisplayText = "")]
这两行是关键,首先 Required 是必填字段,这里按理说为空字符串也是应该报错的,但 AllowEmptyStrings=True 就保证了空字符串可以通过校验。
其次 ConvertEmptyStringToNull = false,这里就阻止了把空字符串转换为null,同时 NullDisplayText = "" 也保证了如果真的是null也会被纠正为空字符串。
从此我就告别了 ??
国际惯例
源码Git地址:http://git.oschina.net/doddgu/G
G.开源分布式部署 QQ群:601476986 (本群会实时更新进度,相比来说肯定比博客频繁得多)
下一篇预告:经过这次的事情,防止打脸,不敢预告了。
自动签名