.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的安全性和功能要比他们差很多。