代码改变世界

.NET Core+Nginx+Docker+Redis+Memcached+MySQL搭建跨平台分布式应用

2017-11-04 13:46  天新8206  阅读(1140)  评论(0编辑  收藏  举报

随着网站访问量的增加,单台服务器逐渐无法承担用户的访问,这时就需要搭建集群来分担压力。网上有很多文章介绍分布式集群的文章,但基本都只介绍用Nginx实现反向代理,这篇文章算是一个补充吧,也是对以前学习的知识一个总结。本人第一次写博客,写的不好请谅解,有错误的地方请指出。

一、各组件的安装

需要安装的东西较多,一个个来。由于本人没那么多机子,服务器就用虚拟机来代替。实现基础环境需要六个虚拟机,Nginx一个,Web两个,Redis一个,Memcached一个,MySQL一个,操作系统均使用Ubuntu 16.04。Web的虚拟机其中一个使用Docker创建容器部署程序,另一个直接将程序以服务的形式运行。

 

当然,Nginx、Redis、Memcached、MySQL都可以实现集群,本人电脑配置没那么高,所以就不弄那么复杂了,感兴趣的并且电脑配置够高的或者有足够多的机子,可以自己尝试一下。

1.SSH服务的安装

由于虚拟机较多,一个个虚拟机切换来切换去非常麻烦,而且实际环境也不可能跑到服务器旁边连接上显示器写命令吧,所以我们使用SSH客户端来连接虚拟机。

由于Ubuntu默认不安装SSH服务,所以首先需要对每个虚拟机安装SSH服务,之后通过SSH客户端连接到虚拟机。如果对SSH不太熟悉,可以去百科了解一下:https://baike.baidu.com/item/ssh/10407?fr=aladdin

安装命令如下:

sudo apt-get install openssh-server

安装期间会询问是否继续安装,直接按Y键回车就可以。

安装完成后,启动SSH服务,执行以下命令:

sudo /etc/init.d/ssh start

如果出现,证明SSH已启动。好了,接下来可以SSH客户端工具来连接到虚拟机了。SSH连接工具非常多,我平常使用SSH Cryptonaut,用起来很简单,除了能写命令还可以对服务器的文件进行管理,非常方便。SSH客户端工具使用方法不多说,网上一搜一大堆。

2.Nginx的安装

Nginx 是一个高性能的HTTP和反向代理服务器,可用于实现负载均衡。关于介绍的话语不多说,接下来直接进入正题,开始安装。连接到属于Nginx使用的虚拟机,在命令界面输入以下命令:

sudo apt-get install nginx

安装完成后启动Nginx服务:

sudo service nginx start

查看Nginx是否启动成功:

ps -A | grep nginx

如出现,则说明启动成功。

3.Docker的安装

这里需要重点说一下Docker。

为什么要使用Docker?

相信众位都遇见过这么一个问题:程序本地跑的好好的,一放到服务器上就报错;一台服务器部署了多个程序,程序之间居然会莫名其妙的相互影响!我曾经遇到过不同程序之间的Session相互响应的问题,很奇怪,但就是存在。所以很多公司为了防止程序相互影响,一台服务器只部署一个程序,如果平常不使用这个程序那么这台服务器就处于空闲状态,会对资源造成极大的浪费。虽然可以使用虚拟机,但虚拟机却存在性能问题,不是一个很好的解决方法。但Docker可以很好的解决以上两个问题。

Docker有点类似于虚拟机,但对性能损耗却比虚拟机要低很多。Docker可以在系统中创建多个独立的区域,每个区域之间几乎是完全隔离的,互相影响的几率几乎为零,配置好的容器内容可以在只要支持Docker的任意计算机之间使用,这样不仅可以最大化的利用服务器,而且不需要每次都重新搭建程序的运行环境,实施效率会高很多。

Docker有主要有三个概念:

① 镜像(image):类似于系统的安装包。我们可以基于别人的镜像进行修改并使用,别人也可以基于我们的镜像修改和使用;

② 容器(container):基于镜像所运行的实例;一个镜像可以创建多个容器,同一个镜像可以在不同的服务器上使用;

③ 镜像 layer:一个镜像修改并push就会有一个layer,一个镜像通常会有多个layer。

接下来我们开始安装Docker。连接到属于Docker的虚拟机,输入安装命令:

sudo apt-get install docker.io

安装完成后执行下sudo docker version命令查看是否安装成功。安装成功后我们就可以下载镜像了,镜像安装命令如下:

sudo docker pull 镜像名称

输入相应的镜像名称就可以下载到对应的镜像。.NET Core、Redis、Memcached、MySQL都有对应的Docker镜像,我们只需要下载.NET Core的镜像就行。由于镜像服务器在国外,所以访问速度可想而知,在下载镜像之前,需要使用加速服务,服务很多,我使用的是阿里云的服务,如果没有阿里云的账号去注册一个或者用别的加速服务。命令如下:

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["你的加速地址"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

执行成功后就可以下载.NET Core镜像了。微软提供了.NET Core 2.0的镜像,直接下载即可,命令如下:

sudo docker pull microsoft/dotnet

好了,Docker暂时告一段落,接下来安装Memcached。

4.Memcached的安装

Memcached是一个NoSQL数据库,以Key-Value的形式存储数据,专门用来做缓存的,所有的数据均存储在内存中,拥有非常高效的性能。

为什么要使用Memcached?

应用中使用缓存会极大的提升系统的性能,但不同服务器之间缓存的数据无法共享,所以使用缓存服务器是必然的,而且可以根据职责的不同来搭建不同规格的服务器,例如缓存服务器的配置就可以内存容量大一些,硬盘小一些。

接下来连接到属于Memcached的虚拟机,输入安装命令:

sudo apt-get install memcached

由于Memcached默认不允许外部访问,所以需要修改配置文件,配置文件在/etc/memcached.conf。由于用Linux的vi编辑器太痛苦了,我就进图形界面用文本编辑器来编辑了,如果你的虚拟机装的是服务版的Ubuntu,没图形界面,那建议你使用nano,比vi强多了。我用的是notepadqq编辑器,感觉挺好用的,如果想下载用用看执行以下命令:

sudo add-apt-repository ppa:notepadqq-team/notepadqq
sudo apt-get update
sudo apt-get install notepadqq

安装完成后执行以下命令:

sudo notepadqq /etc/memcached.conf

如果你用的不是notepadqq,那就把他替换为你的编辑器名称。找到-l 127.0.0.1这一列,在前面加一个#,允许所有ip访问。这里注意一下,实际环境绝对不可以允许所有ip访问,这样有非常大的安全隐患。

保存好后启动Memcached服务,输入以下命令:

sudo service memcached start

接下来写代码来测试下。.NET下有很多Memcached的客户端,我经常使用EnyimMemcached,用起来感觉挺方便的,而且也支持.NET Core。Nuget安装命令:Install-Package EnyimMemcached,测试代码如下:

MemcachedClientConfiguration mcConfig = new MemcachedClientConfiguration();
mcConfig.AddServer("192.168.75.133:11211");//memcached默认端口为11211
using (MemcachedClient client = new MemcachedClient(mcConfig))
{
    client.Store(StoreMode.Set, "testkey", "testvalue");
    var value = client.Get("testkey");
    Console.WriteLine(value);
}
Console.ReadKey();

显示结果如下:

一切正常。如果有Memcached集群,只需要多次调用AddServer方法就可以了。接下来安装Redis。

5.Redis的安装

Redis和Memcached一样,都是NoSQL数据库,也是以Key-Value的形式存储数据的,和Memcached不同的是,Redis会将内存中的数据持久化到硬盘中,所以性能不如Memcached,但拥有的功能甩Memcached好几条街。这里不详细介绍Redis具有哪些功能和数据类型,想了解的可以去看看:http://www.runoob.com/redis/redis-tutorial.html

由于直接用apt-get命令安装的Redis版本较低(不知道应该切换到哪个源),所以得去官网下载压缩包,如果写安装过程的话篇幅太长,我这里就不罗嗦了,这里推荐一篇文章:http://www.cnblogs.com/runningsmallguo/p/5871412.html。Redis集群搭建文章推荐:http://www.cnblogs.com/runningsmallguo/p/7588529.html

 

6.MySQL的安装

还是和前面一样,安装SSH服务,然后执行下面的命令安装MySQL服务:

sudo apt-get install mysql-server

安装过程中会提示输入root用户的密码,按照提示输入即可。

执行下面的命令安装MySQL连接客户端:

sudo apt-get install mysql-client

安装完成后执行下面的语句,如果不报错就表明MySQL服务已正常运行。

mysql -uroot -p你的密码

执行quit;退出MySQL连接客户端。

MySQL和Redis、Memcached一样,默认都是不允许远程连接的,所以需要修改配置文件。还是用notepadqq编辑器来修改,执行下面的命令:

sudo notepadqq /etc/mysql/mysql.conf.d/mysqld.cnf

将bind-address = 127.0.0.1注释。

接下来执行下面的命令重启MySQL服务:

sudo /etc/init.d/mysql restart

重启后,进入MySQL连接客户端,执行下面的语句:

grant all privileges on *.* to root@"允许连接的ip" identified by "你的密码" with grant option;
flush privileges;

也可以不限制访问的ip,直接将语句的ip地址换成“%”就行,注意实际环境千万别这样做。

回到Windows界面,用连接工具测试一下,我用的是Navicat。

7..NET Core SDK的安装

进入Web2虚拟机,依次执行下面的命令:

sudo apt install curl
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-xenial-prod xenial main" > /etc/apt/sources.list.d/dotnetdev.list'
sudo apt-get update
sudo apt-get install dotnet-sdk-2.0.2

如果不出差错应该会一次性安装成功。如果有错误,建议去微软的官网去看看,地址:https://www.microsoft.com/net/learn/get-started/linuxubuntu

尝试创建一个控制台程序看看,输入下面的命令:

dotnet new console -o firstcon
cd firstcon
dotnet run

.NET Core 2.0运行程序不再需要restore,直接run就可以。如果出现“Hello World!”说明一个控制台程序创建成功。

环境已经搭建完毕,接下来开始写程序。

二、创建MVC应用程序

1.创建应用程序

用Visual Studio 2017创建一个ASP.NET Core程序,为了方便就不分层了。.NET Core 2.0提供了新的Razor页面,本次模拟程序就采用这个了,创建程序时选择对应的模板即可,如下图:

如果对.NET Core不熟悉的话建议去看官方文档学习一下。创建好后修改Program类中的内容,内容如下:

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace WebApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseKestrel()
                .UseUrls("http://*:5000")
                .Build();
    }
}

主要增加了两个方法的调用,一个是使用Kestrel作为SelfHost,另一个就是为程序指定端口号。

2.使用EntityFramework Core建库建表

接下来通过nuget引用EF Core。用过EF的都知道,这玩意只支持SQL Server,要想连接别的数据库得另外引用provider。目前官方的扩展在10月25号发布了稳定版,也没有说明是否支持2.0,我还没尝试使用不知道是否稳定,所以使用一个第三方的扩展:Pomelo.EntityFrameworkCore.MySql,微软的官方文档也有说明这个扩展组件,目前也支持EF Core 2.0了。

Nuget命令:

Install-Package Pomelo.EntityFrameworkCore.MySql
Install-Package Microsoft.EntityFrameworkCore.Tools
Install-Package Microsoft.VisualStudio.Web.CodeGeneration.Design

创建一个类,继承自DbContext,内容如下:

using Microsoft.EntityFrameworkCore;
using System;

namespace WebApplication
{
    public class MyContext : DbContext
    {
        public MyContext(DbContextOptions<MyContext> options) : base(options)
        {

        }

        public DbSet<User> Users { get; set; }
        public DbSet<LoginRecord> LoginRecords { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<User>().ToTable("T_Users");
            modelBuilder.Entity<User>().Property(e => e.UserName).IsRequired().HasMaxLength(20);
            modelBuilder.Entity<User>().Property(e => e.Password).IsRequired().HasMaxLength(36);
            modelBuilder.Entity<User>().Property(e => e.RegisterTime).IsRequired();

            modelBuilder.Entity<LoginRecord>().ToTable("T_LoginRecords");
            modelBuilder.Entity<LoginRecord>().Property(e => e.LoginTime).IsRequired();
            modelBuilder.Entity<LoginRecord>().Property(e => e.RequestIp).IsRequired().HasMaxLength(20);
        }
    }

    public class User
    {
        public long Id { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
        public DateTime RegisterTime { get; set; } = DateTime.Now;
    }

    public class LoginRecord
    {
        public long Id { get; set; }
        public DateTime LoginTime { get; set; }
        public string RequestIp { get; set; }
    }
}

为了直观些,我的模型类和数据库的配置信息都写到一个类中了,如果是真实项目的话,数据模型要建一个单独的类,配置信息也建议建一个单独的类,这样不会过于混乱。

接下来将DbContext注册为服务。打开Startup类文件,引用Microsoft.EntityFrameworkCore命名空间,在Configure方法中添加下面的代码:

services.AddDbContext<MyContext>(options => options.UseMySql("你的数据库连接字符串"));

重新生成程序。接下来在程序包管理控制台输入下面的命令:

Add-Migration InitialCreate
Update-Database

这样的话库和表就自动帮我们建好了。执行Add-Migration InitialCreate命令后,vs会自动生成一个继承自Migrations的类,感兴趣的可以去看看官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/data/ef-mvc/migrations#introduction-to-migrations

 

3.注册Memcached服务

接下来开始连接Memcached,执行nuget命令:

Install-Package EnyimMemcachedCore

在appsettings.json中加入下面的节点:

"enyimMemcached": {
  "Servers": [
    {
      "Address": "memcached",
      "Port": 11211
    }
  ]
}

如果有多个Memcached服务器,在Servers节点加多个地址就可以。在Startup类的ConfigureServices方法中加入下面的代码:

services.AddEnyimMemcached(options => Configuration.GetSection("enyimMemcached").Bind(options));

在Configure方法中加入下面的代码:

app.UseEnyimMemcached();

配置完成,现在添加一个Razor页面来测试一下。注意添加的是Razor页面,不要选错。

创建视图之前看下Pages下是否存在_ViewStart.cshtml文件,如果存在注意不要勾选“使用布局页”。

接下来贴代码,视图页面代码如下:

@page
@model WebApplication.Pages.RegisterModel
@{
    ViewData["Title"] = "Register";
}
<form method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" id="UserName" name="UserName" /></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" id="Password" name="Password" /></td>
        </tr>
        <tr>
            <td><input type="submit" value="提交" /></td>
        </tr>
    </table>
</form>

<span>@Model.RegisterResult</span>

后台代码如下:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Enyim.Caching;

namespace WebApplication.Pages
{
    public class RegisterModel : PageModel
    {
        private readonly MyContext _dbContext;
        private readonly IMemcachedClient _memcachedClient;

        public RegisterModel(MyContext dbContext, IMemcachedClient memcachedClient)
        {
            _dbContext = dbContext;
            _memcachedClient = memcachedClient;
        }

        public void OnGet()
        {

        }
        

        public async Task OnPostAsync(string userName, string password)
        {
            User user = new User();
            user.UserName = userName;
            user.Password = password;
            await _dbContext.Users.AddAsync(user);
            int res = await _dbContext.SaveChangesAsync();

            ulong val = _memcachedClient.Increment("memIncrement", 1, 1);

            RegisterResult = $"数据库内容:受影响的行数为{res};";
            RegisterResult += $"Memcached递增内容:{val}";
        }

        public string RegisterResult { get; set; }
    }
}

.NET Core自带依赖注入功能,所以不需再使用Autofac之类的工具了。DbContext和Memcached都注册成了系统服务,所以直接通过构造函数形式的依赖注入给属性赋值。运行程序,访问Register,界面如下:

输入内容后提交,界面如下:

经过测试,数据库和Memcached全部正常(Increment方法是用来给值递增的),接下来配置Redis。

4.Redis访问类的封装

在.NET Core中可以使用Redis作为分布式缓存使用,对象类型为IDistributedCache。这个接口只提供了基本的增删改查及刷新功能,所以不采用这种方式,如果想体验一下的,在Startup类的ConfigureServices方法中加入下面的代码,注入IDistributedCache就可以使用了。

services.AddDistributedRedisCache(options =>
{
    options.Configuration = "localhost";
    options.InstanceName = "SampleInstance";
});

Memcached也支持IDistributedCache形式的分布式缓存,具体的自己尝试下吧。

我网上找了一个基于StackExchange.Redis的封装类,挺齐全,推荐使用,地址:http://www.cnblogs.com/liqingwen/p/6672452.html。把文章中的代码整体扒过来,将_configurationOptions字段中get方法的Redis的连接地址修改下就OK。

接下来修改下Register页面的OnPost方法的代码,修改结果如下:

public async Task OnPostAsync(string userName, string password)
{
    User user = new User();
    user.UserName = userName;
    user.Password = password;
    await _dbContext.Users.AddAsync(user);
    int res = await _dbContext.SaveChangesAsync();

    ulong val = _memcachedClient.Increment("memIncrement", 1, 1);
    RedisHelper redis = new RedisHelper();
    double redInc = await redis.SortedSetIncrementAsync("redisIncrement", "register");

    RegisterResult = $"数据库内容:受影响的行数为{res};";
    RegisterResult += $"Memcached递增内容:{val}";
    RegisterResult += $"Redis递增内容:{redInc}";
}

再运行下试试看:

OK,所有组件均已顺利连接,接下来再创建一个Login页面,代码如下:

@page
@model WebApplication.Pages.LoginModel
@{
    ViewData["Title"] = "Login";
}

<form method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="userName" value="" /></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password" value="" /></td>
        </tr>
        <tr>
            <td><input type="submit" name="name" value="登陆" /></td>
        </tr>
    </table>
</form>

<span>@Model.LoginResult</span>
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System;
using System.Threading.Tasks;

namespace WebApplication.Pages
{
    public class LoginModel : PageModel
    {
        private readonly MyContext _dbContext;

        public LoginModel(MyContext dbContext)
        {
            _dbContext = dbContext;
        }

        public void OnGet()
        {

        }

        public async Task OnPostAsync(string userName, string password)
        {
            bool exists = await _dbContext.Users.AnyAsync(u => u.UserName == userName && u.Password == password);
            if (!exists)
            {
                LoginResult = "用户名或密码不正确!";
            }
            else
            {
                LoginResult = $"登陆成功。请求地址:{Request.Host.Value}{Request.Path.Value},请求时间:{DateTime.Now.ToLongTimeString()}";

                LoginRecord loginRecord = new LoginRecord();
                loginRecord.LoginTime = DateTime.Now;
                loginRecord.RequestIp = Request.Host.Value;
                await _dbContext.AddAsync(loginRecord);
                await _dbContext.SaveChangesAsync();
            }
        }

        public string LoginResult { get; set; }
    }
}

在Startup类中的Configure方法中配置转发中间件,添加如下的代码:

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

app.UseAuthentication();

接下来发布程序,发布的文件如下:

程序部分已完毕,接下来开始部署。

三、实施程序

1.将程序部署为服务

连接到Web的虚拟机(没有安装Docker的),将程序打包成zip,上传到服务器,执行unzip命令解压。在/etc/systemd/system下创建一个disweb.service文件,文件内容如下:

[Unit]
Description=Example .NET Web Distributed Application running on Ubuntu

[Service]
WorkingDirectory=/var/aspnetcore/hellomvc  (这里写你的程序目录)
ExecStart=/usr/bin/dotnet /var/aspnetcore/hellomvc/hellomvc.dll  (你的程序目录/你的主dll文件名)
Restart=always
RestartSec=10  # Restart service after 10 seconds if dotnet service crashes
SyslogIdentifier=dotnet-example
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install] WantedBy
=multi-user.target

保存文件后执行下面的命令启动服务:

systemctl start disweb.service

执行下面的命令查看服务状态:

systemctl status disweb.service

如果启动成功的话,尝试用浏览器访问看看,下面是我本地在Ubuntu上访问站点的图片。

 

如果服务启动失败,可以执行/usr/bin/dotnet /var/aspnetcore/hellomvc/hellomvc.dll这个命令查看启动错误原因,我在启动的时候就发现www-data没有我的程序所在的文件夹的访问权限。错误原因可能很多,自己尝试定位吧。官方文档地址:https://docs.microsoft.com/en-us/aspnet/core/publishing/linuxproduction?tabs=aspnetcore2x

2.创建Docker镜像和容器

在程序的根目录下创建一个Dockerfile文件。注意这个文件没有后缀,字母的大小写不要写错。在文件中输入下面的内容:

FROM microsoft/dotnet
COPY . /publish
WORKDIR /publish
EXPOSE 5000/tcp
CMD [“dotnet”,“你的程序dll名称”]

将Dockerfile放到发布的程序的根目录下,将所有的文件打包成一个zip包。接下来创建一个用来存放程序文件的目录,执行下面的命令:

mkdir zipdir

接下来将zip包传到zipdir目录中,执行unzip命令解压包,然后执行下面的命令创建docker镜像:

sudo docker build -t netcore/disweb .

写命令的时候注意结尾的空格和“.”。执行下面的命令创建容器:

sudo docker run -d -p :81:5000 netcore/disweb

这句命令的意思是,创建一个netcore/disweb镜像的容器并在后台运行,服务器的81端口对应这个容器内的5000端口,注意这个5000端口在程序的Program类中定义。创建完成后输入下面的命令查看容器列表:

sudo docker ps -a

这个命令的意思是查看所有正在运行的容器,如果有一个实例说明创建容器成功。

再次执行创建容器的命令,分别占用服务器的82、83、84、85端口,创建完成后用浏览器访问下测试看是否成功。

OK,Docker部分完毕,接下来配置Nginx。

3.配置Nginx

进入Nginx所在的虚拟机,执行下面的命令打开配置文件:

sudo notepadqq /etc/nginx/sites-available/default

打开后在server节点上面写入下面的内容:

upstream myserver{
    server *:5000;
    server *:5000;
    server *:5000;
    server *:5000;
}

有几个站点就在上面写几个地址。定位到server的location节点,将try_files $uri $uri/ =404;注释掉,然后写入下面的内容:

proxy_pass http://myserver;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;

保存文件,执行下面的命令使Nginx配置生效:

sudo nginx -s reload

用浏览器访问Nginx所在虚拟机的IP地址,如果出现页面则说明配置成功。如果怀疑Nginx是否正确的分发请求,可以在程序中做一些小动作来证明。

Nginx负载均衡策略可以看看这篇文章:http://www.cnblogs.com/andashu/p/6377323.html

 

所有的部署工作已经完成了。这只是我在本地模拟了一个局域网环境,如果是真实环境部署还是写shell脚本来执行各种命令,包括创建容器等等,这样不容易出错。注意在实际环境中,一定不要将Kestrel直接暴露在外,要使用IIS、Nginx这些Web服务器将Http请求转交给Kestrel,因为相对于这些Web服务器,Kestrel的安全性和功能要比他们差很多。