IT教程 ·

.net 微服务实践

Hibernate入门之命名策略(naming strategy)详解

l  媒介

 本文纪录了我的一次.net core 微效劳架构实践经验,以及所用到的手艺

l  长处

  1. 每一个效劳聚焦于一块营业,不管在开发阶段或是布置阶段都是自力的,更适合被各个小团队开发保护,团队对效劳的全部生命周期担任,事情在自力的上下文当中。
  2. 假如某一项效劳的机能抵达瓶颈,我们只须要增添该效劳负载节点,能够针对体系的瓶颈效劳更有用的应用资本。
  3. 效劳A能够应用.net完成 ,效劳B能够应用java完成,手艺选型天真,体系不会历久限定在某个手艺栈上。
  4. 松耦合、高内聚,代码轻易明白,开发效力高,更好保护。
  5. 高可用,每一个效劳能够启动多个实例负载,单个实例挂了有充足的相应时刻来修复

l  瑕玷

  1. 体系范围巨大,运维要求高,须要devops技能(Jenkins,Kubernetes等等)
  2. 跨效劳需求须要团队之间的合作
  3. 跨效劳的挪用(http/rpc)增添了体系的耽误

l  Docker

  docker是现在广泛应用的容器化手艺,在此架构中我们的应用程序将布置在docker容器内里,经由过程docker宣布应用 须要先编写一个dockerfile,以下

#引入镜像 .net core 3.1
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
#设定事情目次
WORKDIR /app
#在容器中程序应用的端口,一定要和程序启动应用的端口对应上
EXPOSE 80
#复制文件到事情目次
COPY . .
#环境变量  此变量会掩盖appsetting.json 内的同名变量
ENV Ip ""
ENV Port ""
#启动程序
ENTRYPOINT ["dotnet", "Union.UserCenter.dll"]

 

  docker build 敕令 将我们的宣布目次打包一个docker镜像,比方    docker build -t test .    ,test是镜像称号

  docker run 敕令启动我们打包的镜像,比方 docker run -d -p 5002:80 --name="test1" -e Ip="192.168.0.164" -e Port="5002"  test ,-e 示意通报环境变量

  更多docker敕令 请查阅:

  docker官网:

  .net 微服务实践 IT教程 第1张

  • 布置轻易:只须要一个简朴的 docker run敕令,就能够启动一个应用实例了
  • 布置平安:打包镜像的时刻已打包了应用所需环境,运转环境不会涌现任何问题
  • 断绝性好:统一台机械我能够布置java的应用和.net的应用,互不影响
  • 快速回滚:只需镜像存在能够快速回滚到任一版本
  • 成本低:一台机械能够运转许多实例,很轻易就能够完成高可用和横向扩大

 

 

   经测试docker for windows不适合布置生产环境,照样得在liunx体系上跑, .net framework 没法在docker上布置

  Docker compose :Docker官方供应的治理工具,能够简朴的设置一组容器启动参数、启动次序、依靠关联

  Kubernetes :容器数目许多以后会变得难以治理,能够引入Kubernetes对容器举行自动治理,闇练应用有一定难度,中文社区:

l  RPC 远程过程挪用

为何要有RPC

根据微效劳设想头脑,效劳A只专注于效劳A的营业,然则需求上一定会有用劳A须要挪用效劳B来完成一个营业处置惩罚的状况,应用http挪用其他效劳效力相对较低,所以引入了RPC。

  gRPC vs thrift  评测:

这里应用thrift,thrift 官网:

 Thrift 采纳IDL(Interface Definition Language)来定义通用的效劳接口,然后经由过程Thrift供应的编译器,能够将效劳接口编译成差别言语编写的代码,经由过程这个体式格局来完成跨言语的功用,语法请自行百度

  .net 微服务实践 IT教程 第2张

下载thrift 代码生成器   ,thrift-0.13.0.exe 这个文件

  执行敕令 thrift.exe --gen netcore xxxxxxx.thrift ,生成C# 效劳接口代码

.net 微服务实践 IT教程 第3张

  援用官方供应的.net 库,能够去官网下载,找不到的能够直接 nuget援用 Examda.Thrift,这是我为了轻易应用上传的

增加生成的代码到我们的效劳端里,然后本身完成 thrift文件定义的接口

using System.Threading;
using System.Threading.Tasks;
using Union.UnionInfo.Service.Interface;
using static Examda.Contract.UnionInfo.UnionInfoService;

namespace Union.UnionInfo.Service
{
    public class UnionInfoServiceImpl : IAsync
    {
        private readonly ILmMembersInfoService _lmMembersInfoService;
        public UnionInfoServiceImpl(ILmMembersInfoService lmMembersInfoService)
        {
            _lmMembersInfoService = lmMembersInfoService;
        }
        //完成接口
        public async Task<string> GetUnionIdAsync(string DozDomain, CancellationToken cancellationToken)
        {
            return (await _lmMembersInfoService.GetMembersInfoByDozDomain(DozDomain)).UnionId;
        }
    }
}

  增加一个类继续 IHostedService 

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using Thrift;
using Thrift.Protocols;
using Thrift.Server;
using Thrift.Transports;
using Thrift.Transports.Server;

namespace Examda.Core.Rpc
{
    public class RpcServiceHost : IHostedService
    {
        public IConfiguration Configuration { get; }

        public ITAsyncProcessor Processor { get; }

        public ILoggerFactory LoggerFactory { get;  }

        public RpcServiceHost(IConfiguration configuration, ITAsyncProcessor processor,ILoggerFactory loggerFactory)
        {
            Configuration = configuration;
            Processor = processor;
            LoggerFactory = loggerFactory;
        }
        //
        public virtual Task StartAsync(CancellationToken cancellationToken)
        {

            TServerTransport serverTransport = new TServerSocketTransport(Configuration.GetValue<int>("RpcPort"));

            TBinaryProtocol.Factory factory1 = new TBinaryProtocol.Factory();
            TBinaryProtocol.Factory factory2 = new TBinaryProtocol.Factory();

            //UnionInfoService.AsyncProcessor processor = new AsyncProcessor(new UnionInfoServiceImpl());完成的效劳这里采纳.net core 自带 DI注入,也能够直接实例化
            TBaseServer server = new AsyncBaseServer(Processor, serverTransport, factory1, factory2, LoggerFactory);

            return server.ServeAsync(cancellationToken);
        }
        public virtual Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}

修正ConfigureServices增加以下代码

            //注入rpc效劳完成实例
            services.AddSingleton<ITAsyncProcessor>(provider =>
            {
                var lmMembersInfoService = provider.GetService<ILmMembersInfoService>();
                return new AsyncProcessor(new UnionInfoServiceImpl(lmMembersInfoService));
            });
            //监听rpc端口
            services.AddHostedService<RpcServiceHost>();

  效劳端就完成了,接下来编写客户端挪用,修正客户端ConfigureServices增加以下代码

            //test rpc效劳
            services.AddScoped(provider =>
            {
                var examdaConsul = provider.GetService<ExamdaConsul>();
                Address address = examdaConsul.GetAddress("UnionInfo");//猎取效劳地点,这里我封装了,测试能够先直接写死
                var tClientTransport = new TSocketClientTransport(IPAddress.Parse(address.Ip), address.Port);
                var tProtocol = new TBinaryProtocol(tClientTransport);
                return new UnionInfoService.Client(tProtocol);
            });

控制器内挪用示例

using System.Threading;
using System.Threading.Tasks;
using Examda.Contract.UnionInfo;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace RPCCLIENT.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly UnionInfoService.Client _rpcClient;
        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger, UnionInfoService.Client rpcClient)
        {
            _logger = logger;
            _rpcClient = rpcClient;
        }

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            await _rpcClient.OpenTransportAsync(CancellationToken.None);
            var order = await _rpcClient.GetUnionIdAsync("wx.hdgk.cn", CancellationToken.None);//rpc挪用
            return Ok(order);
        }
    }
}

l  效劳注册与发明

为何要有用劳注册与发明
  比方:效劳A一开始只要一个实例,此时又启动了一个效劳A的实例,然则挪用效劳A的效劳B并不晓得 效劳A多了一个实例(或许少了),此时引入效劳注册与发明能够让效劳B得知效劳A的变动状况,效劳B就晓得本身要挪用的效劳IP:端口 是多少,不须要人工干预

  罕见的注册中间

.net 微服务实践 IT教程 第4张

 

 

 

这里应用consul

康健搜检:consul自带康健搜检,搜检效劳是不是可用,不可用的效劳将从注册中间剔除,自带的就是隔一段时刻检测一下端口通不通,而且支撑自行扩大康健搜检,可用本身在效劳内完成是不是康健的逻辑,比方虽然接口是通的,然则我发明本身宿主机cpu过80%了,就返回不康健的状况

  效劳注册:nuget装置consul,写一个扩大要领

        /// <summary>
        /// 假如效劳同时包括http,rpc挪用此要领
        /// </summary>
        /// <param name="services"></param>
        /// <param name="Configuration"></param>
        /// <param name="ServiceName"></param>
        /// <param name="Remark"></param>
        public static void AddExamdaServiceRpc(this IServiceCollection services, IConfiguration Configuration, string ServiceName, string Remark)
        {
            var Ip = Configuration.GetValue<string>("Ip");
            var RpcPort = Configuration.GetValue<int>("RpcPort");
            var RpcAddress = $"{Ip}:{RpcPort}";
            var consulClient = new ConsulClient(x => x.Address = new Uri(Configuration.GetValue<string>("ConsulUrl")));//要求注册的 Consul 地点
            var httpCheck = new AgentServiceCheck()
            {
                DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//效劳启动多久后注册
                Interval = TimeSpan.FromSeconds(20),//康健搜检时刻距离,或许称为心跳距离
                Timeout = TimeSpan.FromSeconds(5),
                TCP = RpcAddress
            };
            var registration = new AgentServiceRegistration()
            {
                Checks = new[] { httpCheck },
                ID = RpcAddress,
                Name = ServiceName,
                Address = Ip,
                Port = RpcPort,
                Tags = new[] { Remark }
            };
            consulClient.Agent.ServiceRegister(registration).Wait();
            //应用程序退出时
            AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
            {
                consulClient.Agent.ServiceDeregister(registration.ID).Wait();//consul作废注册效劳
            };
        }

修正ConfigureServices增加以下代码,启动

            services.AddExamdaServiceRpc(Configuration, "UnionInfo", "同盟机构信息效劳");

.net 微服务实践 IT教程 第5张

 

 

装置consul请自行百度

   效劳发明与变动:挪用方设置好本身须要挪用的效劳称号鸠合,然后去consul猎取地点列表,然后根据须要挪用的效劳数目启动N个线程来轮询效劳最新的地点信息,不必忧郁轮询形成的斲丧过大,由于consul供应了Blocking Queries 壅塞查询的体式格局,要求发送到consul以后会在consul壅塞(30)秒,时期有变动或许抵达30秒了以后才会返回地点列表,然后每一次变动以后的地点列表都邑有一个新的版本号。

using Consul;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Examda.Core.Consul
{

    public class Address
    {
        public string Ip { get; set; }

        public int Port { get; set; }
    }
    /// <summary>
    /// 未完成效劳负载平衡,这里随机选一个
    /// </summary>
    public class ExamdaConsul
    {
        private object locker = new object();
        private readonly ConsulClient _consulClient;
        private IDictionary<string, List<Address>> RpcServices { get; set; }
        public ExamdaConsul(IConfiguration configuration)
        {
            RpcServices = new Dictionary<string, List<Address>>();
            _consulClient = new ConsulClient(c =>
            {
                c.Address = new Uri(configuration.GetValue<string>("ConsulUrl"));
            });
            foreach (var item in configuration.GetSection("RpcServiceClient").GetChildren().Select(x => x.Value).ToList())//遍历所须要挪用的效劳称号鸠合
            {
                RpcServices.Add(item, null);
                var res = _consulClient.Catalog.Service(item).Result;
                RpcServices[item] = res.Response.Select(x => new Address() { Ip = x.ServiceAddress, Port = x.ServicePort }).ToList();
                Task.Factory.StartNew(() =>
                {
                    var queryOptions = new QueryOptions { WaitTime = TimeSpan.FromSeconds(30) };//壅塞时刻
                    queryOptions.WaitIndex = res.LastIndex;
                    while (true)
                    {
                        GetAgentServices(queryOptions, item);
                    }
                });
            }
        }
        private void GetAgentServices(QueryOptions queryOptions, string serviceName)
        {
            var res = _consulClient.Catalog.Service(serviceName, null, queryOptions).Result;
            if (queryOptions.WaitIndex != res.LastIndex)
            {
                lock (locker)
                {
                    queryOptions.WaitIndex = res.LastIndex;
                    var currentServices = RpcServices[serviceName];
                    RpcServices[serviceName] = res.Response.Select(x => new Address() { Ip = x.ServiceAddress, Port = x.ServicePort }).ToList();
                }
            }
        }
        /// <summary>
        /// 猎取效劳可用地点
        /// </summary>
        /// <param name="serviceName"></param>
        /// <returns></returns>
        public Address GetAddress(string serviceName)
        {
            for (int i = 0; i < 3; i++)
            {
                Random r = new Random();
                int index = r.Next(RpcServices.Count);
                try
                {
                    return RpcServices[serviceName][index];
                }
                catch
                {

                    Thread.Sleep(10);
                    continue;
                }
            }
            return null;
        }
    }
}

 然后注入一个ExamdaConsul类的单例,讲写死的效劳地点改成从consul猎取

            //注入consul客户端 单例
            services.AddSingleton<ExamdaConsul>();
            //注入UnionInfo rpc客户端 线程单例
            services.AddScoped(provider =>
            {
                var examdaConsul = provider.GetService<ExamdaConsul>();
                Address address = examdaConsul.GetAddress("UnionInfo");//从consul猎取效劳地点
                var tClientTransport = new TSocketClientTransport(IPAddress.Parse(address.Ip), address.Port);
                var tProtocol = new TBinaryProtocol(tClientTransport);
                return new UnionInfoService.Client(tProtocol);
            });

  consul 官网:

l  API网关

一切的要求都先经由网关,由转发到对应的效劳,对比了 ocelot 和 Bumblebee 两个c#写的网关。挑选应用了Bumblebee。

  Ocelot机能比较低,吞吐比直接接见下降四倍,然则文档很周全,功用集成许多,不须要本身扩大什么。

  Bumblebee 我做测试发明Bumblebee 机能很优异,为难的是这个险些没什么人用,许多功用须要本身扩大,作者官网 Bumblebee 文档:

  这里应用Bumblebee ,应用要领能够看作者的文档

  康健搜检:不康健的节点将不会被转发要求

  限流:比方限定某个节点最多300rps,假如此节点并发了1000个要求,也许会有700个摆布要求网关会直接返回毛病,不会转发到详细的效劳,能够起到挡洪作用,防止节点直接挂了。

  路由:我是这么设置的 比方  ,Course一级是效劳称号 tool 是效劳的控制器称号 getuserinfo是要领称号

  负载平衡:效劳多个节点负载,网关能够设置负载平衡战略

  .net 微服务实践 IT教程 第6张

 

  注册到网关:redis宣布定阅完成,增加一个扩大要领

        public static void AddExamdaService(this IServiceCollection services, IConfiguration Configuration, string ServiceName, string Remark)
        {
            var Ip = Configuration.GetValue<string>("Ip");
            var Port = Configuration.GetValue<int>("Port");
            var Address = $"http://{Ip}:{Port}";
            services.AddSingleton(new Redis(Configuration.GetValue<string>("Redis")));
            ServiceProvider serviceProvider = services.BuildServiceProvider();
            Redis redis = serviceProvider.GetService<Redis>();
            redis.Publish("ApiGetewap", JsonConvert.SerializeObject(new { Address, ServiceName, Remark }));
            AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
            {
                redis.Publish("ApiGetewapExit", JsonConvert.SerializeObject(new { Address, ServiceName, Remark }));
            };
        }

网关定阅这个频道

 g = new OverrideApiGetewap();
            g.HttpOptions(o =>
            {
                o.Port = 80;
                o.LogToConsole = true;
                o.LogLevel = BeetleX.EventArgs.LogType.Error;
            });
            g.Open();
            var sub = Program.redis.GetSubscriber();
            //注册效劳
            sub.Subscribe("ApiGetewap",(chanel,message)=> {
                var service = JsonConvert.DeserializeObject<Service>(message);
                var route = g.Routes.NewOrGet(string.Format("^/{0}.*", service.ServiceName), service.Remark);
                route.AddServer(service.Address, 0);
            });
            //效劳退出
            sub.Subscribe("ApiGetewapExit", (chanel, message) => {
                var service = JsonConvert.DeserializeObject<Service>(message);
                var route = g.Routes.NewOrGet(string.Format("^/{0}.*", service.ServiceName), service.Remark);
                route.RemoveServer(service.Address);
            });

  修正ConfigureServices增加以下代码,启动。如许网关也能动态的发明我们的效劳了

            //注册此效劳到网关
            services.AddExamdaService(Configuration, "Course", "同盟我的课程效劳");

  非常流量拉黑:比方某个ip 10s内要求数目凌驾300 将他拉黑 30 分钟,这里应用redis完成计数器

  本身写的大略版本

            //要求完成触发的事宜,不会壅塞要求
            g.RequestIncrement += (sender, e) =>
            {
                Task.Factory.StartNew(() =>
                {
                    var db = Program.redis.GetDatabase();
                    var counter = db.KeyExists(e.Request.RemoteIPAddress);//推断该ip是不是存在计数器
                    if (counter)
                    {
                        var count = db.StringIncrement(e.Request.RemoteIPAddress);//计数器加1
                        if (count > 300)
                        {
                            db.StringSet("BlackList_" + e.Request.RemoteIPAddress, "", new TimeSpan(0, 1, 0), flags: StackExchange.Redis.CommandFlags.FireAndForget);//拉黑半个小时,不守候返回值
                        }
                    }
                    else
                    {
                        db.StringIncrement(e.Request.RemoteIPAddress, flags: StackExchange.Redis.CommandFlags.FireAndForget);//建立计数器
                        db.KeyExpire(e.Request.RemoteIPAddress, new TimeSpan(0, 0, 10), flags: StackExchange.Redis.CommandFlags.FireAndForget);//设置10s逾期
                    }
                });
            };
    class OverrideApiGetewap : Bumblebee.Gateway
    {
        //要求管道的第一个事宜
        protected override void OnHttpRequest(object sender, EventHttpRequestArgs e)
        {
            if (!e.Request.Path.Contains("/__system/bumblebee") && e.Request.Path != "/")//排撤除接见网关ui的
            {
                var db = Program.redis.GetDatabase();
                var isBlack = db.KeyExists("BlackList_" + e.Request.RemoteIPAddress);
                if (isBlack)
                {
                    e.Response.Result(new JsonResult("你被拉黑了"));
                    e.Cancel = true;//作废要求
                }
                else
                {
                    base.OnHttpRequest(sender, e);
                }
                //base.OnHttpRequest(sender, e);
            }
            else
            {
                base.OnHttpRequest(sender, e);
            }
        }
    }

  熔断器:当某个要求转发下流效劳返回毛病次数或许超时次数抵达阀值时自动熔断该节点,暂未完成

  接口验签:客户端要求都带上用 url时刻戳 参数加密的署名,网关举行考证,确保是正当的客户端

  网关自带UI

  .net 微服务实践 IT教程 第7张

l  链路追踪 机能监控

Skywalking 官网: 

  每一个要求的链路,每一个步骤的耗时都能够查到,以下图的一个要求执行了许屡次sql,每一个步骤的sql语句都能够看到,集成很简朴,应用官方供应的.net探针集成到各个效劳就好了,无代码入侵。

.net 微服务实践 IT教程 第8张

 

 

.net 微服务实践 IT教程 第9张

 

 

有一个很壮大的ui界面,也能够供应报警等功用,ui能够查看到相应很慢的接口,均匀相应时刻,以及每一个效劳的关联关联,然则有个问题我没有解决,RPC链路追踪不到。

能够自行去官方查阅应用文档

  

l  分布式日记网络框架

  实例太多了,不可能应用单机日记,须要一个分布式日记网络框架把一切日记网络到一同,能够斟酌应用java的elk 或许 .net core 的Exceptionless

l  分布式事件

 跨效劳之间挪用而且涉及到事件的处置惩罚体式格局,还在想怎么弄

l  设置中间

各个实例逐一设置太麻烦了,特别是假如更改了数据库地点,每一个效劳的一切实例都要改,改死去,而且重启实例也不现实,一定要支撑设置热更新,试了下携程的Apollo有点斲丧资本

l  CI/CD

将源码治理做一个开发分支,一个测试分支,一个宣布分支,开发只动开发分支,开发完成后提交代码,由测试合并到测试分支,并关照Jenkins生成镜像并宣布到测试站点,测试经由过程以后由运维合并到宣布分支,或手动或自动经由过程Jenkins宣布,应当保证 测试分支与宣布分支的版本能对应docker镜像堆栈的每一个版本,个人见解。

l  例:XXXX效劳的项目源码构造

 

Magicodes.IE基础教程之导出Pdf

参与评论