[转]老衣的微服务实践简要指引2017版

本文转自:http://yimingzhi.net/2018/03/lao-yi-de-wei-fu-wu-shi-jian-jian-yao-zhi-yin-2017-ban

这是老衣在2017年5月份总结的,适用于中小团队跨平台微服务开发的实践指引(简化版)。若有有不当之处,欢迎指点更正

因本文涉及到大量第三方库或工具,详细学习和了解需要参考相关官方文档。若您在使用Mac电脑,建议安装使用Dash软件下载查阅;其他操作系统上则考虑使用Chrome浏览器在http://devdocs.io/offline 上查阅文档,值得一提的是该网页的文档支持离线模式。

环境准备

全局必要项

  • node.js 可根据实际情况选择安装当前版或是长期支持版
  • docker最新版 根据实际需要决定使用免费的Docker CE版或是收费的Docker EE版。注意 Windows 10 64位专业版和64位企业版可以直接安装(依赖hyper-v),其他旧版Windows,需要使用docker toolbox
  • 根据自己的喜好选择安装下列Git客户端:

全局可选项

开发环境

代码托管环境

为了最大限度提功开发部署灵活性,建议使用Git方式托管。所以可根据实际情况选择下列之一:

  • 支持Git库的TFS
  • GitHub 私有库或企业版
  • Coding.net 私有库或企业版
  • 开源免费,私有部署的Gogs,具有丰富的认证方式选择,并支持Slack平台的WebHook
  • GitLab 可托管部署或私有部署

建议在以上任何一种git服务中,建立或使用一个支持证书登录的用户,该证书用于构建环境自动拉取代码时使用,不建议使用账号密码方式,因为安全性较差。该证书对应的用户应该有所有需要自动构建的代码库读权限。不建议给该用户开放写权限,避免一些潜在安全问题和代码冲突问题

构建环境

构建环境是专门用来自动化、编译、集成、测试、打包、发布的环境,建议使用独立的计算机和服务器做为构建环境。因为该环境对代码和测试、生产环境都有较高的权限,也会涉及到大量的安全证书或密钥等极敏感信息,所以强烈建议该环境所在设备仅限极少数高度可信赖的人访问管理,并有严格的安全规定,不允许随便安装软件或向外复制数据

建议使用linux作为构建环境的操作系统,因为windows的命令行工具能力有限。并确保安装如下工具:

  • Node.js 我们需要依赖很多nodejs工具链,所以这是必须安装的。通过下列命令确认是否安装,以及什么版本

    node --version
    

    还需要确认npm是否安装:

    npm --version
    

    npm安装新版,可以通过自更新实现

    npm install -g npm
    
  • ShellJS 是在Node.js API之上的便携式(Windows/Linux/OS X)Unix Shell命令实现,可以消除或减少您的Shell脚本对特定操作系统的依赖。通过下面的命令全局安装

    npm install -g shelljs
    

    老衣的实践是利用shelljs的脚本能力以及js语言的丰富特性和灵活度,通过编写脚本的方式结合其他工具或平台实现编译、测试、打包、发布、通知等的自动化处理

  • Docker 几乎是微服务架构的必需品,我们通过Docker隔离和构建相关的服务,通过下列命令确认是否安装,以及什么版本

    docker --version
    
  • git 确保安装使用了最新的git命令行客户端,可通过下列命令确认是否安装,以及什么版本

    git --version
    
  • Python 很多辅助工具是用python开发的,所以有备无患,通过下列命令确认是否安装,以及什么版本

    python --version
    
  • Mono 是跨平台的.NET框架实现,目前5.0以上版本已经完整匹配.NET 4.6.2的API集,几乎除了Windows平台特有的API外,Mono和.NET框架几乎是完全兼容的。所以如果你的微服务是用的.NET开发的,这是必须安装的——当然如果你只打算在Windows上用,可以不安装这个。通过下列命令确认是否安装,以及什么版本

    mono --version
    
  • .NET Core 如果你的微服务有用的.NET Core开发的,这是必须安装的。通过下列命令确认是否安装,以及什么版本

    dotnet --version
    
  • Cake 是C# Make的缩写,是一个基于C# DSL的跨平台自动化构建系统。它可以用来编译代码,复制文件以及文件夹,运行单元测试,压缩文件以及构建Nuget包等等。

  • TeamCityJenkins 这两个是独立的自动构建服务器软件,如果不愿意使用shelljs之类的脚本自行编写构建任务,可以通过他们在管理界面上设置,不过学习成本和复杂度蛮高的,如果团队内没有人熟悉这两个工具,早期不建议使用。

  • 其他语言的基础框架根据实际的每个微服务所使用的语言环境决定安装哪些基础支持工具、框架、模块等

自动构建服务,在拉取代码、获取依赖包、编译、测试、打包、发布等各个环节都可能会发生错误或异常,而编译不通过或测试不通过等情况也应该第一时间跟团队或项目管理者报告,我们在实践中更推荐使用Slack来实现,实现方式请参考官方文档 https://api.slack.com/incoming-webhooks,绝大部分情况下1-3行代码即可实现,非常方便,当然了这里有个小问题Slack的所有客户端(web、手机App、桌面应用等)都没有中文版。当然了你愿意使用电子邮件或企业微信或微信服务号来实现也可以,只是实现成本和效果跟Slack比完全不在一个级别上。注意:Slack的webhook地址通常类似于https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX 这属于保密的地址,不应该在公开的文档或代码中体现,否则容易在Slack的相关频道中产生虚假的信息,切记!!!

当然了上述构建所需到基础依赖,也可以在准备好到Docker容器中做,可减少不同服务对基础依赖(比如php、java、nodejs等)版本不同而带来的构建冲突。早期团队可通过技术选型很大程度上避免基础依赖冲突,所以可先不考虑使用大量Docker容器隔离不同到构建环境,待团队逐渐成熟壮大后,可根据需要慢慢向这个方式靠拢。

我们在实践中更倾向于使用Docker构建相应的镜像来部署最终的微服务,所以需要一些必要的基础镜像提前做准备。推荐安装如下镜像:

docker pull alpine
docker pull busybox
docker pull centos
docker pull ubuntu
docker pull postgres
docker pull microsoft/dotnet
docker pull microsoft/aspnetcore
docker pull mono
docker pull node
docker pull golang
docker pull mongo
docker pull mysql
docker pull redis
docker pull rabbitmq
docker pull memcached
docker pull nginx
docker pull openresty/openresty

以上镜像,如果需要安装特定版本,可通过加tag参数来获取对应版本的镜像。特别一提的是其中alpinebusybox都是使用广泛的超小型基础镜像,大小仅有几M而已,对最终镜像大小很敏感的可考虑使用这些镜像作为基础。

通过下列命令可在部署环境中确认上述镜像是否已经pull到:

docker images

私有Docker Registry服务器

Docker Store上已经有大量官方镜像和公开镜像,可供参考使用,但通常我们自己到私有服务的镜像是不希望公开让别人下载使用的。所以需要搭建自己的镜像库,所以我们需要使用Docker Registry来搭建自己的私有镜像库。

注意:新版到registry要求必须使用https协议,所以搭建时需要考虑购买或使用免费的SSL证书。目前国内的腾讯云和阿里云貌似都有免费一年到二级域名证书,可考虑使用。

使用私有的Docker Registry服务后,每次构建好的服务镜像都是先push到私有服务上,然后再在生产环境中pull最新版,并部署使用。

测试&&生产环境

测试或生产环境,基本上仅需要安装Docker,通过相关的命令来拉取相关镜像,并部署或维护所需服务的容器即可。单服务器上多个微服务如果有依赖关系或启动先后顺序等要求时,建议使用Docker Compose对微服务的相关容器进行编排。是否使用Docker Swarm来做集群化编排更新则由实际业务要求和团队对Docker的熟练度来决定。

需要特别指出的是关于数据库的处理,微服务架构通常强调服务的无状态化,其中原因主要是为了服务的可复制性和可迁移性,所以目前的主流实践是,数据库在正式生产环境时,使用公有云的独立数据库服务或自己机房的独立数据库服务器的数据库,不建议使用容器作为数据库的宿主——但测试环境可以根据情况考虑使用,以方便模拟和测试各种情况。

生产环境的服务发现,考虑使用分布式服务发现和配置工具Consul。但如果微服务整体全部都是基于Docker的,而且集群使用等Docker Swarm实现的,那么就不必使用其他工具来做服务发现了,因为docker swarm内置了服务发现机制。

开发过程

  1. 根据业务和系统架构,拆分微服务
  2. 为每个微服务设计 RESTful Web API的文档,建议使用API Designer工具,借助于RAML语言设计API
  3. 根据API设计文档,使用json-server来编写API的Mockup
  4. Web前端或App通过调用mock出来的api开发相关功能页面,同时后端人员根据API设计文档开发后端API,测试人员则根据设计文档和mockup版的API表现编写自动化测试脚本
  5. 构建服务定期从git服务器上拉取最新代码,根据项目的定义自动编译、测试相关微服务,并通过Slack通知团队或项目负责人;
  6. 构建服务在编译和测试都通过后,通过脚本自动构建Docker镜像,然后push到私有的docker镜像库中,并通过Slack通知团队或项目负责人;
  7. 测试&&生产环境根据提前编写好的脚本,自动从私有镜像库中拉取最新版的服务镜像,部署运行对应的新服务

注意事项:

  • 期间测试人员通过编写的自动化API测试脚本不断验证服务的最终表现是否正确,不正确的不应该进入生产环境
  • 单元测试是开发人员自行编写的,但是需要明确测试的命令行调用方式,以便构建服务器能够不停的测试验证代码
  • 每个用到数据库的微服务都应该具备数据库的自动化迁移能力,比如新增的表在服务启动时应该会自动创建,而如果服务回滚到旧版本时必须能够将数据库对应回滚到原来的结构,比如删除增加的表或字段等。
  • 前后端应该完全隔离在不同的镜像中,在服务的总出口处(通常是nginx之类带有反向代理的服务器)将api和页面集成到同一域名下
  • 生产环境尽可能利用nginx服务器的热部署和热更新能力

API的自动化测试,可通过如下nodejs工具开发测试脚本:

实际演练

微服务有几个要点:

  • 功能独立
  • 独立部署
  • 独立进程
  • 可替换性

功能独立容易理解,而难度是整体服务如何合理拆分,其基本原则就是看该服务的数据是否需要关联另外一个服务的数据表才能查出,如果是则应该合并到一个服务中,否则可独立拆分;独立部署独立进程 告诉我们,每个服务运行在独立的进程或服务器中,每个服务都应该可以独立部署,且不应该影响其他服务的运行;可替换性,则是说每个服务都应该可以随时用任何编程语言重新实现并替换掉旧有的服务而不会影响其他服务和整体系统的正常运行。

鉴于每个微服务都可能随时更新,为了保证整体系统运行的稳定性,我们需要至少做到如下几点:

  • 每个微服务的更新过程都要足够的快,要尽可能在1秒内完成,以便减少服务更新时出现的各种异常
  • 每个微服务调用其他服务时最后都具备retry机制,以便在调用到的另外一个微服务出现短期故障(比如更新服务)时,能够具备过一点时间后再访问的能力,这样最前端的用户或者服务调用者会认为当前的服务调用速度只是比原来慢了而已
  • 集中的日志收集服务,可以通过该服务的界面上查看到各个微服务的日志,以便团队或项目领导者能够及时了解和分析系统的运行状态,及时发现和修复各种问题异常
  • 微服务架构的复杂性决定了微服务总体出现异常的几率比单体系统要多,所以单元测试和集成测试变得极其重要,甚至可以说是必须的
  • 每个微服务对外的服务接口(如WebAPI),应该都是具备普适性、自释性、可预见性、明确性的。也就是:
    • 每个微服务的对外接口实现可以不依赖于任何特定的语言
    • 每个微服务调用另外一个服务时,都应该是尽可能使用通用的通信协议,而不是私有的,避免跨语言实现的难度,即通畅是使用HTTP协议
    • 每个服务的最终路由地址是不可随意变更的
    • 每个服务的路径自身就是其功能的最简说明
    • 通过一个api地址就能较为准确的猜出另外一个服务的地址和访问方式

基于以上观点,并假定我们大部分的微服务都使用.NETMono.NET Core编写,那么如下相关的库和组件则应该是必须的或优先考量的:

  • NancyFx 这是一个非常优秀的Web框架,独立于ASP.NET以外,也就是说他自身不依赖System.Web命名空间下的任何东西。具有极好的跨平台支持能力和极佳的可测试性、组件可替换性、路由可视性。用它编写微服务,会让开发过程变得很简单。当然如果你实在不喜欢这套框架的风格,依然可以选用ASP.NETASP.NET MVCASP.NET Core等相对传统的技术,毕竟这并不影响微服务实现的本身。
  • Flurl 这是一个新出现的优雅的的Http客户端库,它依然基于HttpClient,但是提供了Fluent风格的编程模型,所以写代码时会比传统方式要爽快和高效的多。虽然RestSharp也是一个很优秀的WebAPI访问库,但是它目前不支持.NET Core等框架,所以跨平台能力欠佳。
  • xUnit.net 这是NUnit.net原作者几年前重新打造的一个.NET平台的单元测试框架,具有丰富的工具链。微软官方的ASP.NET MVC等框架也都使用的该框架编写的所有单元测试,而没有采用微软自家的单元测试框架。
  • KestrelHttpServer 这是微软官方实现的ASP.NET Core的Host服务器框架,可以实现极高性能的SelfHost服务,而不必依赖IIS。
  • TinyMapper 比老牌的AutoMapper具有更佳的性能(大概快60倍),而且老衣也曾贡献了部分代码增加了一些特性。不过目前对.NET Core支持方面还有点瑕疵,但可以在.NET项目中大规模使用。
  • Mapster 它跟TinyMapper一样是新型的Mapper库,号称快速、有趣且激动人心的Mapper,老衣目前在.NET Core项目中主要采用这个作为主要的Mapper库。
  • Json.NET .NET平台下最流行的高性能(但不是最快的)JSON库。
  • Polly.NET平台下,让开发者可以轻松实现线程安全的重试熔断超时代码,大幅提升应用稳定性的绝对利器
  • C-Sharp-Promise Promise在js开发领域大行其道,深得开发者们的喜爱。C#程序员们可以用C-Sharp-Promise,使用Promise的方式编程。一些时候你会发现它比async的方式更好
  • Topshelf 当你需要将一个.NET的Console或者桌面应用,作为Windows服务运行时,它会很好的帮到你。它还支持Mono,也就是说可以在Linux上玩
  • Dapper 轻量级的通用ORM,支持市面上大多数关系型数据库。兼容.NETMono.NET Core

.NET Core上使用Nancy创建一个Hello World级的微服务

  1. 先确保node.jsnpm已安装

    node --version
    npm --version
    

    如果上述命令显示未安装node.js,请到Node.js官网下载对应操作系统的版本,并安装好。

  2. 确保yeoman已经安装

    yo --version
    

    如果上述命令未输出版本号,提示yo命令不存在则,应该使用下面到命令安装它

    npm install -g yo
    
  3. 确保generator-aspnet已经安装

    yo aspnet
    

    如果上述命令提示未安装一个名叫aspnetgenerator,则需要使用下面到命令安装generator-aspnet

    npm install -g generator-aspnet
    
  4. 上一步命令正常会输出如下结果

         _-----_     ╭──────────────────────────╮
        |       |    │      Welcome to the      │
        |--(o)--|    │  marvellous ASP.NET Core │
       `---------´   │        generator!        │
        ( _´U`_ )    ╰──────────────────────────╯
        /___A___   /
         |  ~  |
       __'.___.'__
     ´   `  |° ´ Y `
    
    ? What type of application do you want to create? (Use arrow keys)
    ❯ Empty Web Application
      Empty Web Application (F#)
      Console Application
      Console Application (F#)
      Web Application
      Web Application Basic [without Membership and Authorization]
      Web Application Basic [without Membership and Authorization] (F#)
    (Move up and down to reveal more choices)
    
    

    按键盘的⬇键,直到出现在Nancy ASP.NET Application这一行后,按回车键

    ? What's the name of your ASP.NET application? (NancyApplication)后面输入你的服务的名字(比如:hi)后回车。

    提示: 由于这个名字会出现在代码中,所以请使用类似C#变量的命名规范输入,不要使用任何中文字符或-,也不要是纯数字。

  5. 假设上一步输入的名字是hi,使用下列命令进入hi目录,并使用Visual Studio Code打开该目录

    cd hi
    code .
    
  6. Visual Studio Code打开后,查看修改global.json文件中的sdk version为最新安装的.NET Core SDK的版本。如果不知道当前安装的.NET Core SDK是什么版本,可通过执行下列命令查看

    dotnet --version
    

    如果你安装的是1.0.4版,则此时应该会看到结果是1.0.4

    此时把global.json文件中version后面的版本号,改为刚才命令中输出的版本号(例如1.0.4)。

  7. 查看hi.csproj文件中的TargetFramework节点的值,如果你打算使用.NET Core 1.0则应该是netcoreapp1.0,如果打算使用.NET Core 1.1则应该是netcoreapp1.1。你还需要根据自己选择版本修改Nuget引用的相关库的版本。还要修改AssemblyName的值为你期望的程序集名,通常我们跟服务名一致,本例中应该是hi

    这里我们使用.NET Core 1.1版来开发,那么此时应该将hi.csproj文件的内容调整为如下:

    <Project ToolsVersion="15.0"  Sdk="Microsoft.NET.Sdk.Web">
    
      <PropertyGroup>
        <TargetFramework>netcoreapp1.1</TargetFramework>
        <DebugType>portable</DebugType>
        <AssemblyName>hi</AssemblyName>
        <OutputType>Exe</OutputType>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="1.1.2" />
        <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.1.2" />
        <PackageReference Include="Microsoft.AspNetCore.Owin" Version="1.1.2" />
        <PackageReference Include="Nancy" Version="2.0.0-clinteastwood" />
      </ItemGroup>
    
    </Project>
    

    注意: Nancy的版本号应该使用最新支持.NET Core的版本。2.0.0-clinteastwood是编写此文时的NancyFx最新版。实际操练时需要根据新的情况对应修改。

  8. 执行下列命令,加载依赖包、编译并运行这个hi服务

    dotnet restore
    dotnet build
    dotnet run
    

    会看到如下结果

    Hosting environment: Production
    Content root path: /Users/yimingzhi/Projects/wang/hi
    Now listening on: http://localhost:5000
    Application started. Press Ctrl+C to shut down.
    

    在浏览器中访问http://localhost:5000地址,可以看到Hello from Nancy running on CoreCLR

    这样一个最简单的Hellow World级的微服务已经开发完成。其中HomeModule.cs文件就是这个服务的核心逻辑代码:

    namespace hi
    {
        using Nancy;
    
        public class HomeModule : NancyModule
        {
            public HomeModule()
            {
                Get("/", args => "Hello from Nancy running on CoreCLR");
            }
        }
    }
    

    是不是非常简单呢 _

  9. 目前这个服务看起来很美,但是存在如下问题:

    • 需要运行主机上安装.NET Core的sdk或者运行时
    • 如果多个服务用的不同版本的.NET Core则可能会出现版本冲突等问题
    • 部署起来比较麻烦,需要用ftprdpsshrsync等工具部署大量文件
    • 基本不具备规模化横向部署能力,比如一次性部署100个节点
    • 一旦服务崩溃或其他异常退出时,无法自动重启

    因此我们需要借助Docker这个容器工具来解决它,如果你不确定是否安装了docker,请运行下面的命令确认

    docker --version
    

    当然你也可以通过

    docker info
    

    查看docker的一些信息,还可以通过

    docker images
    

    查看已经pull到本地的docker镜像列表,需要确认.NET Core1.1.2运行时(编写此文时的最新版本)镜像是否存在,如果不存在请使用下列命令拉取镜像:

    docker pull microsoft/dotnet:1.1.2-runtime
    

    拉取完毕后我们就可以准备构建hi服务的docker镜像了

  10. hi目录下创建一个名字为Dockerfile的文件——注意:这个文件名是区分大小写并没有任何扩展名的

  11. Visual Studio 2017Visual Studio Code打开刚才创建的Dockerfile文件,并将以下内容写入并保存:

    FROM microsoft/dotnet:1.1.2-runtime
    ENV TZ=Asia/Shanghai
    COPY ./dist /app
    WORKDIR /app
    EXPOSE 5000/tcp
    ENTRYPOINT ["dotnet","./hi.dll"]
    

    提示:

    ENV TZ Asia/Shanghai 表示该镜像的容器默认的运行时时区为上海时区,这对于大陆的服务来说至关重要,不加这行默认情况一般都是UTC时间,切记 EXPOSE 5000/tcp 表示该镜像的容器默认向外暴露5000端口,这里就是hi服务的默认端口 ENTRYPOINT ["dotnet","./hi.dll"] 中的hi.dll,为服务的主运行程序,不同服务这个地方会有所不同

    之所以使用runtime版的基础镜像而不是sdk版,是因为sdk版的镜像太大(大了几百兆),对publish后的.NET Core应用也是没有必要的。

  12. hi目录下运行下面的一组命令构建正式发布版的hi服务镜像

    dotnet restore
    dotnet build -c Release
    dotnet publish -c Release -o dist
    docker build -t hi . 
    

    使用docker images命令查看是否有名为hi的镜像出现在列表中。如果没有,请检查上面的这组命令是否输出了什么错误信息,修改代码并重新执行上面这组命令,直到docker镜像列表中有名为hi的镜像出现为止。

    提示

    上面这组命令中hi服务被发布在dist目录中,因此需要在.gitignore中将dist目录设置为忽略项,不做版本控制。

  13. 通过下面的命令执行hi镜像:

    docker run --name hiServ --rm -p 5000:5000 hi
    

    此时会收到如下结果:

    Hosting environment: Production
    Content root path: /app
    Now listening on: http://localhost:5000
    Application started. Press Ctrl+C to shut down.
    

    第1行告诉你运行环境是产品环境还是开发环境;第3行意思是你可以通过访问本机的http://localhost:5000看到结果。但是这时候我们在浏览器中访问http://localhost:5000,会提示你:无法访问此网站

    在另外一个控制台窗口(Windows下叫命令行窗口)中运行下面的命令进入容器:

    docker exec -it hiServ bash
    

    运行命令:

    curl http://localhost:5000/
    

    此时会看到输出结果为Hello from Nancy running on CoreCLR,这说明hi服务可从内容内访问,是容器外无法访问。

    输入exit命令退出容器

  14. 打开Program.cs文件,在.UseKestrel()下插入一行新代码.UseUrls("http://*:5000/"),即Program.cs的代码变为:

    namespace hi
    {
        using System.IO;
        using Microsoft.AspNetCore.Hosting;
    
        public class Program
        {
            public static void Main(string[] args)
            {
                var host = new WebHostBuilder()
                    .UseContentRoot(Directory.GetCurrentDirectory())
                    .UseKestrel()
                    .UseUrls("http://*:5000/")
                    .UseStartup<Startup>()
                    .Build();
    
                host.Run();
            }
        }
    }
    
  15. 再次运行下面这组命令,重新构建hi服务的镜像:

    dotnet restore
    dotnet build -c Release
    dotnet publish -c Release -o dist
    docker build -t hi .
    

    运行下面的命令临时将hi镜像以名为hiServ的容器运行:

    docker run --name hiServ --rm -p 5000:5000 hi
    

    打开浏览器,访问http://localhost:5000/,看到了Hello from Nancy running on CoreCLR

    这样我们的hi服务容器镜像就算成功了。

    提示

    docker run --name hiServ --rm -p 5000:5000 hi 中的--name hiServ是为容器起一个特定名字hiServ; --rm是让这个容器在退出时立即删除; -p 5000:5000 是为了将容器的5000端口映射到当前主机的5000端口上,以便在真机中可以测试访问这个服务。 最终生产环境中不会以当前用户的前置进程方式运行,否则用户退出时容器就会结束运行。所以我们通常会额外附加-d参数让服务在后台执行,详情请参考Docker CLI的官方文档。

将hi服务增加数据有关的API

前面,我们演示了如何使用docker将一个.NET Core跑起来。下面为hi服务增加几个复杂的API

增加一个服务状态的api

  1. 在hi这个项目中增加一个新文件StatusModule.cs,内容如下

    namespace hi
    {
        using Nancy;
        using System;
    
        public class StatusModule : NancyModule
        {
            public StatusModule()
            {
                Get("/status", _ =>
                {
                    return new
                    {
                        running = true,
                        time = DateTime.Now
                    };
                });
            }
        }
    }
    

    用下面的一组命令运行起hi服务:

    dotnet restore
    dotnet build
    dotnet run
    

    postman或非windows系统的命令行工具curl,访问http://localhost:5000/status,会收到一个json数据,类似于:

    {
      "running": true,
      "time": "2017-05-16T18:03:34.4867970+08:00"
    }
    

    但是,如果你用浏览器访问http://localhost:5000/status你会收到一个错误页,标题是500 - Internal Server Error,不过你看不到错误内容——让你改一下代码才能看到错误。

    在hi目录中新建一个文件Bootstrapper.cs,内容如下:

    namespace hi
    {
        using Nancy;
    
        public class Bootstrapper : DefaultNancyBootstrapper
        {
            public override void Configure(Nancy.Configuration.INancyEnvironment environment)
            {
    #if DEBUG
                environment.Tracing(enabled: false, displayErrorTraces: true);
    #endif
            }
        }
    }
    

    再次编译运行,

    dotnet restore
    dotnet build
    dotnet run
    

    用浏览器访问http://localhost:5000/status,会看到一个很详细的Nancy.ViewEngines.ViewNotFoundException异常报告,意思是找不到名为><f__AnonymousType0'2的任何视图文件。

    这其实是Nancy框架的一个特性,他可以实现一个地址即是webapi又是网页,其具体的原理就是判断当前请求的Content-Type中是否以text/html开头,如果是说明当前请求是一个浏览器,那么Nancy就会使用当前地址返回的model类型名作为视图名查找视图文件,找到了就用类似于mvc的方式显示视图页面,如果找不到就报错。

    如果我们不希望用这个特性,就是期望该地址永远返回一个json数据,则需要使用Response.AsJson方法。StatusModule.cs应该修改为:

    namespace hi
    {
        using Nancy;
        using System;
    
        public class StatusModule : NancyModule
        {
            public StatusModule()
            {
                Get("/status", _ =>
                {
                    return Response.AsJson(new
                    {
                        running = true,
                        time = DateTime.Now
                    });
                });
            }
        }
    }
    

    再次编译运行,

    dotnet restore
    dotnet build
    dotnet run
    

    用浏览器访问http://localhost:5000/status,我们会看到与在postman看到的一样漂亮的json数据。Great!

  2. 准备一个数据库,为了跨平台我们选择PostgreSQL数据库。当然了在计算机上安装一个数据库是很麻烦的事情,这里我们用Docker直接用官方的postgres镜像运行起一个容器即可。执行下列命令:

    docker run --name pgdb -e POSTGRES_PASSWORD=123456 -p 5432:5432 -d postgres
    

    此时我们就准备好了一个postgres数据库,账号是postgres,密码是123456,并可以通过本机的5432端口访问到它。你可以使用pgAdmin链接并管理这个数据库,老衣假定你已经学会使用pgAdmin的基本功能,才继续下面的演练的。

  3. 使用pgAdmin在pgdb这个数据库容器中创建一个新的数据库hiDB

  4. 在hi项目上,从Nuget引用最新版的Npgsql,先在Bootstrapper.cs中通过Nancy自带的容器注册一个数据库连接:

    namespace hi
    {
        using System.Data;
        using Nancy;
    
        public class Bootstrapper : DefaultNancyBootstrapper
        {
            public override void Configure(Nancy.Configuration.INancyEnvironment environment)
            {
    #if DEBUG
                environment.Tracing(enabled: false, displayErrorTraces: true);
    #endif
            }
    
            protected override void ConfigureApplicationContainer(Nancy.TinyIoc.TinyIoCContainer container)
            {
                base.ConfigureApplicationContainer(container);
    
                container.Register<IDbConnection>((c, p) =>
                {
                    return new Npgsql.NpgsqlConnection("Host=localhost;Username=postgres;Password=123456;Database=hiDB;Timeout=3");
                });
            }
        }
    }
    

    然后增加一个DbConnectExtensions.cs文件,代码:

    using System.Data;
    
    namespace hi
    {
        public static class DbConnectExtensions
        {
            public static bool IsConnectable(this IDbConnection conn)
            {
              bool connectable = false;
              try
              {
                  conn.Open();
                  var cmd = conn.CreateCommand();
                  cmd.CommandText = "select 1;";
                  int r = (int)cmd.ExecuteScalar();
                  connectable = r == 1;
              }
              catch
              {
                  connectable = false;
              }
              finally
              {
                  if (conn.State == ConnectionState.Open)
                  {
                      conn.Close();
                  }
              }
              return connectable;
            }
        }
    }
    

    之后在StatusModule.cs中增加数据库可连接性的状态检测,代码如下:

    namespace hi
    {
        using System;
        using System.Data;
        using Nancy;
    
        public class StatusModule : NancyModule
        {
            public StatusModule(IDbConnection conn)
            {
                Get("/status", _ =>
                {
                    return Response.AsJson(new
                    {
                        running = true,
                        dbConnectable = conn.IsConnectable(),
                        time = DateTime.Now
                    });
                });
            }
        }
    }
    

    再次调试运行hi项目,然后访问http://localhost:5000/status会收到类似于下面这样的json数据:

    {
      running: true,
      dbConnectable: true,
      time: "2017-05-17T14:47:47.1287050+08:00"
    }
    

    其中dbConnectable表示数据库可连接性状态。如果你把数据库容器停止:

    docker stop pgdb
    

    再次调试运行,并访问http://localhost:5000/status,会发现dbConnectable的值变为false了。 嗯,脑袋聪明的你一定会想: 这个数据库检测的代码应该封装成一个方法,这样就可以多个服务中都能方便做这个检查了; 而数据库连接字符串,也应该用配置文件。老衣在这里先提示你这未必是好的,至于原因吗,后面会有讨论。

    当然了,你也可以在这个api中增加CPU、内存等实时环境状态数据,用于监控服务通过这个api定时获取到该服务的相关状态数据

增加一组简单的新闻相关API

  1. 先确保数据库容器pgdb处于启动状态:

    docker start pgdb
    
  2. 在数据库hiDB上建一张news表:

    create table news
    (
        id serial primary key,
        title varchar(200) not NULL,
        content text NOT NULL
    )
    
  3. 在hi项目上,从Nuget引用最新版的DapperDapper.Contrib

  4. 在hi项目中,新建一个文件NewModule.cs:

    using System.Data;
    using Dapper.Contrib.Extensions;
    using Nancy;
    using Nancy.ModelBinding;
    
    namespace hi
    {
        public class NewsModule : NancyModule
        {
            public NewsModule(IDbConnection conn)
            {
                Get("/api/news", _ =>
                {
                    return Response.AsJson(conn.GetAll<News>());
                });
    
                Post("/api/news", _ =>
                {
                    var model = this.Bind<News>();
                    var insertNumber = conn.Insert(model);
                    return Response.AsJson(insertNumber > 0);
                });
    
                Get("/api/news/{id:int}", x =>
                {
                    int id = x.id;
                    var model = conn.Get<News>(id);
                    if (model == null)
                    {
                        return Response.AsJson(new { error = "该新闻不存在" }).WithStatusCode(HttpStatusCode.NotFound);
                    }
                    return Response.AsJson(model);
                });
            }
        }
    
        [Table("news")]
        public class News
        {
            public int id
            {
                get;
                set;
            }
    
            public string title
            {
                get;
                set;
            }
    
            public string content
            {
                get;
                set;
            }
        }
    }
    

    为了简单期间,我把News的定义放在了同一个文件中,实际项目中不应该这样写

    即增加了3个API:

    • 所有新闻的列表
    • 添加一个新闻
    • 获取指定id的新闻

    用postman向http://localhost:5000/api/news POST 一条json数据:

    {
    	"title":"test",
    	"content":"this is only one test"
    }
    

    收到true说明添加成功,接着GET请求http://localhost:5000/api/news会获取含有刚添加的新闻的数组json:

    [
      {
        "id": 1,
        "title": "test",
        "content": "this is only one test"
      }
    ]
    

    GET 请求 http://localhost:5000/api/news/1会获取id为1的新闻数据json:

    {
      "id": 1,
      "title": "test",
      "content": "this is only one test"
    }
    

    GET 请求 http://localhost:5000/api/news/2会获取一个状态码为404的json数据:

    {
      "error": "该新闻不存在"
    }
    

    很棒啊, 3个API都表现正常了。是不是感觉用Nancy替代ASP.NET MVC,用Dapper替代EntityFramework 写代码变得更畅快淋漓呢?嗯,不过也不是说这些替代就任何场合都会更好,这完全可以由开发者或团队根据实际情况*控制。 但老衣在这里主要是想说,一些其他选择也许可以让你的开发变得轻松有趣一些了,甚至更多好处。

    总之,以后我们也就可以使用以上类似的方式,很快速的创建其他微服务了。

  5. 嗯,到这里不知道你是否想起了前面的数据库连接字符串中,包含了Host=localhost 也就是说数据库服务器的所在地是本机。而我们的数据库实际上是在另外一个容器中,只是因为我们运行容器时把本机端口和容器做了个映射,所以造成了数据库在本机的假象。我们到底应该如何处理呢?一台生产环境的机器上可能会有多个数据库容器,或者其他服务的容器,我们不能也不应该把所有服务的端口都在真实生产环境中上进行映射暴露。生产环境中数据库容器或类似的涉及信息安全的容器,都应该尽可能不允许外部直接访问,而是使用容器的相关通信手段。这样一来,我们的hi服务访问的数据库应该是pgdb这个容器。所以在部署(非开发阶段)应该将Host的值改为pgdb,并使用link方式将hi服务的容器与pgdb进行连接,直接通过容器通信。改完数据库连接字符串后,重新制作hi服务的容器镜像

    dotnet restore
    dotnet build -c Release
    dotnet publish -c Release -o dist
    docker build -t hi .
    

    构建完成后,以容器方式连接pgdb并运行

    docker run --name hiServ --rm -p 5000:5000 --link pgdb:pgdb hi
    

    访问http://localhost:5000/status,收到

    {
      running: true,
      dbConnectable: true,
      time: "2017-05-18T14:52:30.3009050+08:00"
    }
    

    可以看出数据库连接是成功的,很棒。即使你把pgdb容器的端口映射移除,也会发现hi服务仍可以访问pgdb容器的数据库,但是你的pgadmin就访问不了,挺安全吧 :D 至于如何跨服务器连接容器通信,这属于高级话题,有机会再细聊或者直接查相关文档和书籍研究一下。

  6. 看起来这时候我们用一个数据库容器和一个hi服务容器实现了期望的功能,是不是完美了呢?当然不完美,因为不完美的人类永远造不出完美的事物来,只能尽力无限趋向完美!(来自老衣语录^_^)把数据库容器也作为一个微服务,我们都知道:是个服务就有升级、更新、重启或宕机的时候。我们希望数据库容器中的数据库因某原因出现崩溃退出时能够自动重启数据库服务,以便达到整体系统的高可用度。docker运行容器时指定--restart参数就可以非常简单做到这一点,Great!看起来很美是不是,但请想一下如果用户刚好在数据库崩溃时访问了hi服务,是不是就会收到服务器端异常呢?就前面实现的代码来说,答案是肯定的。那么怎么能够让用户感受不到这中间的短时间数据库失联呢?前文中我们提到过.NET领域有个很好的库Polly,可以再遇到一些异常时实现Retry。现在用它改造一下hi服务的代码,让新闻有关的几个api实现数据库链接异常时自动Retry:

    先在hi项目上用Nuget引用Polly,然后修改NewsModule.cs的代码为:

    using System.Data;
    using Dapper.Contrib.Extensions;
    using Nancy;
    using Nancy.ModelBinding;
    using Polly;
    
    namespace hi
    {
        public class NewsModule : NancyModule
        {
            public NewsModule(IDbConnection conn)
            {
                Get("/api/news", _ =>
                {
                    var list = DbRetry().Execute(() => { return conn.GetAll<News>(); });
    
                    return Response.AsJson(list);
                });
    
                Post("/api/news", _ =>
                {
                    var model = this.Bind<News>();
                    var insertNumber = DbRetry().Execute(() => { return conn.Insert(model); });
                    return Response.AsJson(insertNumber > 0);
                });
    
                Get("/api/news/{id:int}", x =>
                {
                    int id = x.id;
                    var model = DbRetry().Execute(() => { return conn.Get<News>(id); });
                    if (model == null)
                    {
                        return Response.AsJson(new { error = "该新闻不存在" }).WithStatusCode(HttpStatusCode.NotFound);
                    }
                    return Response.AsJson(model);
                });
            }
    
            Policy DbRetry()
            {
                return Policy.Handle<System.Net.Sockets.SocketException>()
                             .Or<Npgsql.NpgsqlException>()
                             .RetryForever();
            }
        }
    
        [Table("news")]
        public class News
        {
            public int id
            {
                get;
                set;
            }
    
            public string title
            {
                get;
                set;
            }
    
            public string content
            {
                get;
                set;
            }
        }
    }
    

    编译运行hi服务后,不要着急访问news相关api,先模拟数据库失联:

    docker stop pgdb
    

    等数据库停止后,用浏览器访问http://localhost:5000/api/news,你会发现浏览器一直在等待服务器响应…… 立即再切换到刚才的命令行窗口,启动数据库容器:

    docker start pgdb
    

    再次切换到刚才访问http://localhost:5000/api/news那个浏览器窗口,你会惊奇的发现有数据了,而不是返回数据库异常!这就是Polly的强大之处。当然了这个库还有很多强大功能,请移步到官方网站查看文档学习一下吧。

    到这里,聪明的你是否已经联想到,其他情况一个微服务请求另外一个微服务的时候也可以用Polly实现高可用呢?

用用OpenResty吧

OpenResty 是一款基于 NGINXLuaJIT 的 动态Web 平台。你可以根据自己的需求设定启用的模块,并编译生成自己定制的OpenResty。官方的Docker镜像默认包含了一些模块:

  • file-aio
  • http_addition_module
  • http_auth_request_module
  • http_dav_module
  • http_flv_module
  • http_geoip_module=dynamic
  • http_gunzip_module
  • http_gzip_static_module
  • http_image_filter_module=dynamic
  • http_mp4_module
  • http_random_index_module
  • http_realip_module
  • http_secure_link_module
  • http_slice_module
  • http_ssl_module
  • http_stub_status_module
  • http_sub_module
  • http_v2_module
  • http_xslt_module=dynamic
  • ipv6
  • mail
  • mail_ssl_module
  • md5-asm
  • pcre-jit
  • sha1-asm
  • stream
  • stream_ssl_module
  • threads

其中http_auth_request_module可以实现简单的登录后访问某资源的功能,并且身份验证和资源是分开的,具体介绍可参考http://ohmycat.me/nginx/2016/06/28/nginx-ldap.htmlhttp_image_filter_module模块则可以轻松实现实时缩略图、图片旋转等,可参考 https://ruby-china.org/topics/31498 了解。其他模块功能请自行Google吧。

OpenResty继承了Nginx的高性能、反向代理等优点外,还支持使用Lua脚本编写动态逻辑代码,甚至是服务器端视图。这里我们说一下如何用openresty简单实现前面的新闻列表api的功能。

  1. 先在一个新建的目录中创建一个Dockerfile用来创建带有postgres访问能力的openresty镜像:

    FROM centos:7
    
    MAINTAINER Evan Wies <evan@neomantra.net>
    
    # Docker Build Arguments
    ARG RESTY_VERSION="1.11.2.3"
    ARG RESTY_LUAROCKS_VERSION="2.3.0"
    ARG RESTY_OPENSSL_VERSION="1.0.2k"
    ARG RESTY_PCRE_VERSION="8.39"
    ARG RESTY_J="1"
    ARG RESTY_CONFIG_OPTIONS="
        --with-file-aio 
        --with-http_addition_module 
        --with-http_auth_request_module 
        --with-http_dav_module 
        --with-http_flv_module 
        --with-http_geoip_module=dynamic 
        --with-http_gunzip_module 
        --with-http_gzip_static_module 
        --with-http_image_filter_module=dynamic 
        --with-http_mp4_module 
        --with-http_random_index_module 
        --with-http_realip_module 
        --with-http_secure_link_module 
        --with-http_slice_module 
        --with-http_ssl_module 
        --with-http_stub_status_module 
        --with-http_sub_module 
        --with-http_v2_module 
        --with-http_xslt_module=dynamic 
        --with-ipv6 
        --with-mail 
        --with-mail_ssl_module 
        --with-md5-asm 
        --with-pcre-jit 
        --with-sha1-asm 
        --with-stream 
        --with-stream_ssl_module 
        --with-threads 
        --with-http_postgres_module 
        "
    
    # These are not intended to be user-specified
    ARG _RESTY_CONFIG_DEPS="--with-openssl=/tmp/openssl-${RESTY_OPENSSL_VERSION} --with-pcre=/tmp/pcre-${RESTY_PCRE_VERSION}"
    
    # 1) Install yum dependencies
    # 2) Download and untar OpenSSL, PCRE, and OpenResty
    # 3) Build OpenResty
    # 4) Cleanup
    
    RUN 
        yum install -y 
            gcc 
            gcc-c++ 
            gd-devel 
            GeoIP-devel 
            libxslt-devel 
            make 
            perl 
            perl-ExtUtils-Embed 
            readline-devel 
            unzip 
            zlib-devel 
            postgresql-devel 
        && cd /tmp 
        && curl -fSL https://www.openssl.org/source/openssl-${RESTY_OPENSSL_VERSION}.tar.gz -o openssl-${RESTY_OPENSSL_VERSION}.tar.gz 
        && tar xzf openssl-${RESTY_OPENSSL_VERSION}.tar.gz 
        && curl -fSL https://ftp.pcre.org/pub/pcre/pcre-${RESTY_PCRE_VERSION}.tar.gz -o pcre-${RESTY_PCRE_VERSION}.tar.gz 
        && tar xzf pcre-${RESTY_PCRE_VERSION}.tar.gz 
        && curl -fSL https://openresty.org/download/openresty-${RESTY_VERSION}.tar.gz -o openresty-${RESTY_VERSION}.tar.gz 
        && tar xzf openresty-${RESTY_VERSION}.tar.gz 
        && cd /tmp/openresty-${RESTY_VERSION} 
        && ./configure -j${RESTY_J} ${_RESTY_CONFIG_DEPS} ${RESTY_CONFIG_OPTIONS} 
        && make -j${RESTY_J} 
        && make -j${RESTY_J} install 
        && cd /tmp 
        && rm -rf 
            openssl-${RESTY_OPENSSL_VERSION} 
            openssl-${RESTY_OPENSSL_VERSION}.tar.gz 
            openresty-${RESTY_VERSION}.tar.gz openresty-${RESTY_VERSION} 
            pcre-${RESTY_PCRE_VERSION}.tar.gz pcre-${RESTY_PCRE_VERSION} 
        && curl -fSL http://luarocks.org/releases/luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz -o luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz 
        && tar xzf luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz 
        && cd luarocks-${RESTY_LUAROCKS_VERSION} 
        && ./configure 
            --prefix=/usr/local/openresty/luajit 
            --with-lua=/usr/local/openresty/luajit 
            --lua-suffix=jit-2.1.0-beta2 
            --with-lua-include=/usr/local/openresty/luajit/include/luajit-2.1 
        && make build 
        && make install 
        && cd /tmp 
        && rm -rf luarocks-${RESTY_LUAROCKS_VERSION} luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz 
        && yum clean all 
        && ln -sf /dev/stdout /usr/local/openresty/nginx/logs/access.log 
        && ln -sf /dev/stderr /usr/local/openresty/nginx/logs/error.log
    
    # Add additional binaries into PATH for convenience
    ENV PATH=$PATH:/usr/local/openresty/luajit/bin/:/usr/local/openresty/nginx/sbin/:/usr/local/openresty/bin/
    
    ENTRYPOINT ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]
    

    用这个文件构建一个镜像(建议*状态构建,你懂的):

    docker build -t openresty:postgres .
    
  2. 在另外一个新建的目录(假设名字叫newsOR)中,新建一个index.html文件:

    <html ng-app="app">
    <head>
        <title>新闻列表</title>
        <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    </head>
    <body>
        <div class="container" ng-controller="newsListCtrl">
            <h2>新闻列表</h2>
            <table class="table table-bordered">
                <thead>
                    <th>ID</th>
                    <th>标题</th>
                    <th>内容</th>
                </thead>
                <tbody>
                    <tr ng-repeat="item in list">
                        <td ng-bind="item.id"></td>
                        <td ng-bind="item.title"></td>
                        <td ng-bind="item.content"></td>
                    </tr>
                </tbody>
                </ul>
        </div>
        <script src="https://cdn.bootcss.com/angular.js/1.6.4/angular.min.js"></script>
        <script>
            angular.module('app', [])
                .controller('newsListCtrl', function ($scope, $http) {
                    $scope.list = [];
                    $http.get('/api/news').then(function (res) {
                        $scope.list = res.data;
                    });
                });
        </script>
    </body>
    </html>
    
  3. newsOR目录中,创建一个nginx.conf文件:

    #user  nobody;
    worker_processes  1;
    
    events {
        worker_connections  1024;
    }
    
    http {
        include       mime.types;
        default_type  application/octet-stream;
        sendfile        on;
        keepalive_timeout  65;
    
        upstream pgsql {
            postgres_server pgdb:5432 dbname=hiDB password=123456 user=postgres;
            postgres_keepalive off;
        }
    
        server {
            listen       80;
            server_name  localhost;
            charset utf-8;
    
            location / {
                root /www;
                index index.html;
            }
    
            location /api/news {
                postgres_pass pgsql;
                rds_json on;
    
                postgres_query 'select * from news';
            }
        }
    }
    
  4. 在上一步创建的目录中,创建一个Dockerfile文件:

    FROM openresty:postgres
    ENV TZ=Asia/Shanghai
    COPY ./nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
    EXPOSE 80
    ENTRYPOINT ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]
    

    构建这个镜像:

    docker build -t news:or .
    
  5. 连接pgdb容器运行上一步构建的镜像:

    docker start pgdb
    docker run --rm -p 8000:80 --link pgdb:pgdb news:or
    
  6. 用浏览器访问http://localhost:8000/api/news你会看到跟之前我们访问hi服务的新闻列表api获得一样的json数据。但这里我们实际上只是在nginx.conf中加了几行配置就实现了。更加简单快速(虽然第一次准备支持postgres的openresty镜像有点慢,但是这个镜像是可以复用的,不必每次都重新构建)!访问http://localhost:8000/可以看到通过这个api获取数据后绑定到前端页面列表

  7. 如果我们先把之前的hi服务容器hiServ运行起来,并把前面步骤中的nginx.conf中到/api/news的配置改为:

    proxy_set_header Content-Type application/json;
    proxy_pass http://hiServ:5000/api/news;
    

    然后再次构建news:or镜像,并重新运行这个镜像的容器,你会发现效果跟第5步一样。只是原来上是通过反向代理访问了hiServ上的新闻api而已。

  8. 至于如何使用openresty设置更复杂的反向代理、运行Lua脚本等细节,请移步到http://agentzh.org/misc/slides/ngx-openresty-ecosystem/#1 了解一些相关特性,也到https://moonbingbing.gitbooks.io/openresty-best-practices/content/查看相关最佳实践,一次学习终生受用。

用shelljs自动拉取代码构建镜像并推送Slack通知

你可能已经发现了,我们前面的内容中几乎所有的编译、发布、打包、容器的运行停止等都是用命令行来做的。原因就是命令行指令可以写成批处理脚本,脚本并不涉及鼠标点击等人工干预,所以就可以让机器自按照一些计划安排自动反复执行。我们在前面提到过的xunit.netmocha等测试框架甚至gruntgulpwebpack等web前端打包工具也都有对应的命令行(CLI)支持,所以也都可以很方便的自动化执行。

我们选择shelljs作为批处理工具的原因是使用熟悉的javascript语言写比其他有很多额外的好处,比如更丰富的基础类库、工具链、依赖包等。

至于Slack的介绍前面已经讲过,我们主要用它把机器和人之间的交互打通,让机器积极主动的告诉人或团队,一些代码发生了什么事情。

提示:

下面的例子代码可能仅兼容MacOS或Linux,不支持Windows

  1. 准备 Slack 集成的WebHook:

    假设你已经在Slack上注册并创建了自己的团队url是https://XXX.slack.com。那么请登录后访问https://XXX.slack.com/apps/manage/custom-integrations,在Incoming WebHooks configuration中添加真对您某个频道(假设是YYYY)的配置,此时会获得一个WebHook的Url,我们假设这个Url是https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXXincoming-webhooks的使用方式可以查看 https://api.slack.com/incoming-webhooks

  2. 切换到hi服务所在目录的父级目录中

  3. 新建一个用于推送Slack通知的slack.sh文件:

    channel=$1
    nickname=$2
    msg=$3
    
    data="payload={"text": "${msg}", "channel": "#${channel}", "username": "${nickname}", "icon_emoji": ":monkey_face:"}"
    curl -X POST 
    --data-urlencode "${data}" 
     https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
    
  4. 新建一个build.js

    var SECONDS_FOR_BUILD = 1 * 60; //两次构建之间间隔的秒数
    
    exec('clear');
    echo('每隔' + SECONDS_FOR_BUILD + '秒检查git代码是否更新');
    
    function build_dotnet(){
        cd('hi');
    
        echo('从git上拉取最新代码...');
        exec('git pull');
    
        echo('清理编译环境...'); //清理的目的是为了强制更新,可根据自己的实际需求决定是否需要这样做
        rm('-rf', './bin');
        rm('-rf', './obj');
        rm('-rf', './dist');
        exec('dotnet clear');
    
        echo('编译并发布...');
        exec('dotnet restore');
        var buildResult = exec('dotnet build -c Release');
        if (buildResult.code != 0) {
            echo('编译失败!');
            exec('sh slack.sh YYYY CSharp编译器 糟糕代码没有编译通过', { silent: true });
            return;
        }
        exec('dotnet publish -c Release -o dist');
    
        echo('构建Docker镜像...');
        exec('docker build -t hi .');
    
        //echo('推送镜像到私有Docker Registry')
        //exec('docker push xxx.xxxxx.xxx/hi'); //将 xxx.xxxxx.xxx 改为自己的私有Docker Registry地址
    
        echo('完成');
        exec('sh slack.sh YYYY 构建器 恭喜主人,新版本的镜像已经准备好了', { silent: true });
    
        cd('../');
    }
    
    build_dotnet(); //立即执行一次构建
    
    setInterval(function () {
        build_dotnet();
    }, SECONDS_FOR_BUILD * 1000);
    
  5. 运行命令:

    shjs build.js
    

    这个构建程序会每个一段时间(上面代码中为60秒)拉取最新代码,然后编译、发布、打包docker镜像、推送docker镜像等一系列动作。

    如果代码没有编译通过,你会在Slack中收到一个来自机器人CSharp编译器的消息推送糟糕代码没有编译通过;如果编译通过、打包好新的Docker镜像,推送至私有Docker Registry上后,你又会在Slack中收到一个来自机器人构建器的消息推送恭喜主人,新版本的镜像已经准备好了。是不是很酷呢?团队或项目主管可以第一时间通过Slack了解构建程序执行的情况。

  6. 生产环境中则可以用build.js类似的方式定期从私有Docker Registry上使用docker pull xxx.xxxxx.xxx/hi的命令拉取最新hi服务的Docker镜像,并用新的镜像替换旧的。甚至可以使用docker-composedocker-swarm等做docker的编排和集群更新。这里因为各自情况不同,老衣就不给例子代码了,你发挥一下自己的想象力和编程能力吧。

附录

  • 有基于OpenResty的国产API网关:Orange, 可以参考学习一下,因为中文文档的产品对国人来说太好了
  • .NET Core领域的很多优秀库和项目可以参考https://github.com/thangchung/awesome-dotnet-core
  • Actor Model(Actor模型)也可以实现一些场景的微服务,建议学习和了解一些成熟的框架(例如 AkkaAkka.NETOrleans等);目前我比较喜欢的是Proto.Actor
  • zeit 是一家微服务领域很神奇的公司,开源了不少不错的库,比如pkg可以把nodejs项目直接打包成可执行程序,而不必依赖Node.js环境;micro 轻松实现基于Node.js的异步HTTP微服务;hyper 构建在Web技术上的终端控制台;next.js 是在服务器端渲染React应用的框架。仔细学习研究一下会有很多收获的
  • Jint 是一个.NET版的javascript解释器,它提供了对ECMA5.1的完整兼容,并且可以运行在任何.NET平台上。由于它不会动态生成.NET代码,也不使用DLR,所以它可以很快的运行相对较小的js脚本。
  • NLua 是绑定.NET世界和Lua世界的项目。它可以让你在Windows, Linux, Mac, iOS , Android, Windows Phone 7 and 8等几乎任何C#应用中实现.NETLua的相互调用。