微服务的一大优点是,它们可以独立扩展。本文展示了扩展一个微服务及其数据库的好处和挑战。
您将创建一个示例应用程序并手动实现应用程序层分片。它展示了如何根据用例和数据模型选择分片Key。这有助于将相同的原理应用到具有集成扩展(如MongoDB等)的DBMS上。
1.用例和数据模型
示例应用程序由一个User和Post微服务组成。它们通过消息交流:
User微服务处理添加和修改用户。Post微服务处理查看和添加帖子。因为与Post微服务的交互要多得多,所以,当应用程序的负载增加时,Post微服务将成为第一个需要扩展的微服务。
作者的名字是PostService绑定上下文的一部分,因此也是Post微服务的一部分。在User微服务中添加和修改作者。User微服务在添加新用户或更改用户名时发送事件。
PostService的逻辑数据模型
用户可以分类写文章。他们还可以按类别阅读帖子,包括作者姓名。最新的帖子在上面。分类是固定的,很少改变。
基于这些用例,我决定按类别划分数据库分片:
2.实现微服务
创建解决方案并添加名为“PostService”的ASP.NET Core 5 Web API项目。禁用HTTPS并激活OpenAPI支持。
安装以下NuGet软件包:
- Microsoft.EntityFrameworkCore.Tools
- MySql.EntityFrameworkCore
- Newtonsoft.Json
创建实体
Post实体的索引可以加快检索某个类别中最新的帖子:
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
namespace PostService.Entities
{
[Index(nameof(PostId), nameof(CategoryId))]
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int UserId { get; set; }
public User User { get; set; }
[Required]
public string CategoryId { get; set; }
public Category Category { get; set; }
}
}
User实体中的版本稍后将帮助处理无序消息:
namespace PostService.Entities
{
public class User
{
public int ID { get; set; }
public string Name { get; set; }
public int Version { get; set; }
}
}
namespace PostService.Entities
{
public class Category
{
public string CategoryId { get; set; }
}
}
创建PostServiceContext
using Microsoft.EntityFrameworkCore;
namespace PostService.Data
{
public class PostServiceContext : DbContext
{
private readonly string _connectionString;
public PostServiceContext(string connectionString)
{
_connectionString = connectionString;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseMySQL(_connectionString);
}
public DbSet<PostService.Entities.Post> Post { get; set; }
public DbSet<PostService.Entities.User> User { get; set; }
public DbSet<PostService.Entities.Category> Category { get; set; }
}
}
在appsettings.Development.json中添加连接字符串(在调试期间将使用两个分片)
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"PostDbConnectionStrings": {
"Shard0": "server=localhost; port=3310; database=post; user=root; password=pw; Persist Security Info=False; Connect Timeout=300",
"Shard1": "server=localhost; port=3311; database=post; user=root; password=pw; Persist Security Info=False; Connect Timeout=300"
}
}
添加DataAccess代码
GetConnectionString(string category)
计算CategoryId的哈希值。哈希的第一部分将配置的分片数(连接字符串)取模,从而确定给定类别的分片。
InitDatabase
删除并重新创建所有分片中的所有表,并插入虚拟用户和类别。
其他方法用于创建和加载帖子。
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using PostService.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace PostService.Data
{
public class DataAccess
{
private readonly List<string> _connectionStrings = new List<string>();
public DataAccess(IConfiguration configuration)
{
var connectionStrings = configuration.GetSection("PostDbConnectionStrings");
foreach(var connectionString in connectionStrings.GetChildren())
{
Console.WriteLine("ConnectionString: " + connectionString.Value);
_connectionStrings.Add(connectionString.Value);
}
}
public async Task<ActionResult<IEnumerable<Post>>> ReadLatestPosts(string category, int count)
{
using var dbContext = new PostServiceContext(GetConnectionString(category));
return await dbContext.Post.OrderByDescending(p => p.PostId).Take(count).Include(x => x.User).Where(p => p.CategoryId == category).ToListAsync();
}
public async Task<int> CreatePost(Post post)
{
using var dbContext = new PostServiceContext(GetConnectionString(post.CategoryId));
dbContext.Post.Add(post);
return await dbContext.SaveChangesAsync();
}
public void InitDatabase(int countUsers, int countCategories)
{
foreach (var connectionString in _connectionStrings)
{
using var dbContext = new PostServiceContext(connectionString);
dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
for (int i = 1; i <= countUsers; i++)
{
dbContext.User.Add(new User { Name = "User" + i, Version = 1 });
dbContext.SaveChanges();
}
for (int i = 1; i <= countCategories; i++)
{
dbContext.Category.Add(new Category { CategoryId = "Category" + i });
dbContext.SaveChanges();
}
}
}
private string GetConnectionString(string category)
{
using var md5 = MD5.Create();
var hash = md5.ComputeHash(Encoding.ASCII.GetBytes(category));
var x = BitConverter.ToUInt16(hash, 0) % _connectionStrings.Count;
return _connectionStrings[x];
}
}
}
在Startup.cs中将DataAccess注册为单例
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "PostService", Version = "v1" });
});
services.AddSingleton<DataAccess>();
}
---
创建PostController
它使用DataAccess类
using Microsoft.AspNetCore.Mvc;
using PostService.Data;
using PostService.Entities;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace PostService.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PostsController : ControllerBase
{
private readonly DataAccess _dataAccess;
public PostsController(DataAccess dataAccess)
{
_dataAccess = dataAccess;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Post>>> GetLatestPosts(string category, int count)
{
return await _dataAccess.ReadLatestPosts(category, count);
}
[HttpPost]
public async Task<ActionResult<Post>> PostPost(Post post)
{
await _dataAccess.CreatePost(post);
return NoContent();
}
[HttpGet("InitDatabase")]
public void InitDatabase([FromQuery] int countUsers, [FromQuery] int countCategories)
{
_dataAccess.InitDatabase(countUsers, countCategories);
}
}
}
3. 用PostService访问数据库
安装Docker Desktop。
创建两个MySql容器
C:\dev>docker run -p 3310:3306 --name=mysql1 -e MYSQL_ROOT_PASSWORD=pw -d mysql:5.6
C:\dev>docker run -p 3311:3306 --name=mysql2 -e MYSQL_ROOT_PASSWORD=pw -d mysql:5.6
在Visual Studio中启动Post服务。浏览器在打开http://localhost:5001/swagger/index.html
使用swagger UI与服务交互:
初始化包含100个用户和10个类别的数据库:
在“Category1”下增加一个帖子:
{
"title": "MyTitle",
"content": "MyContent",
"userId": 1,
"categoryId": "Category1"
}
阅读“Category1”中排名前10位的帖子:
连接到数据库容器并验证哪个数据库包含新的帖子。
C:\dev>docker container exec -it mysql1 /bin/sh
使用密码“pw”登录MySql并读取帖子:
第二个实例不包含任何帖子:
C:\dev>docker container exec -it mysql2 /bin/sh
4.最后的想法和展望
您创建了一个可工作的应用程序,实现了应用程序层分片,并使用了分片Key的概念。
这只是一个示例应用程序。您必须调整代码才能在生产环境中使用它。
在第二部分中,您将缩放并运行微服务和数据库的多个容器实例。您将使用docker compose和负载平衡器。然后,您将运行JMeter负载测试,以查看应用程序在使用不同数量的实例时是如何伸缩的。最后,您将通过RabbitMQ模拟来自用户微服务的用户事件。