.NET 微服务实践(5)-搭建容器化SQL Server
Created: December 26, 2022 5:10 PM Last Edited Time: December 26, 2022 7:34 PM
本文系油管上一个系列教程的学习记录第五章,原链接是.NET Microservices – Full Course。本文分成四部分:持久化卷申领、Kubernetes 密钥配置、部署SQL Server到Kubernetes、和更新平台服务。
Kubetnetes卷管理
无状态和有状态的应用程序
Kubernetes默认使用本地主机磁盘以卷的方式存储数据。
一般容器若是以无状态的方式运行,则其关联的数据采用临时卷的方式存储、即应用程序不会最终存储数据。无状态的容器(在微服务架构中,也可以称之为服务)可以是Web应用的前后端应用——这些应用之间没有相互依赖。当服务生命周期结束的时候,临时卷就会销毁、其中保存的数据就会丢失。
若希望服务关联的数据能够持久化在磁盘中,则需要以有状态的方式部署。有状态的服务可以是各种数据库服务,分布式架构中构成相互关联的服务组。而有状态的方式,就需要为容器服务搭配持久卷(Persistent Volumes Claim,简称PV)。
Persistent Volumn
像集群中的节点一样,PV也是集群中的一种资源。作为卷的一种,PV的特点就是其生命周期和使用它的Pod相互独立的——当使用PV的服务被销毁时,PV仍然存在,例如一个Mysql容器服务从Pod中被销毁,为其创建的PV资源仍然保留,其中保留的数据信息仍然可以通过数据库管理工具查看到。
Persistent Volumes Claim
持久卷的供应有两种方式:1)静态供应;2)动态供应。
静态供应是指集群管理员手动创建一系列的PV卷。这些卷对集群内的Pod可见、供它们使用。对于静态供应,由于其配置是固定的,当PV中存有数据后,进行例如扩容等操作需要花费额外的工作。
动态供应是指基于持久卷申领(PVC)的配置动态创建一个PV,该动态创建的PV会和PVC绑定在一起,只需要指定Pod和PVC的联系,就可以将动态创建的PV和Pod关联。通过这种方式,可以方便管理PV诸如扩容的操作。
创建持久卷申领
配置yaml文件
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mssql-claim
spec:
resources:
requests:
storage: 100Mi
accessModes:
- ReadWriteOnce
然后基于yaml文件创建申领卷
>>kubectl apply -f local.pvc.yaml
检查结果
>>kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mssql-claim Bound pvc-0f123828-5e07-4837-a105-c9f714f489b3 100Mi RWO hostpath 26h
默认创建的持久卷申领会得到一个动态供应的持久卷,可以看到它会自动和持久卷申领绑定在一起。
>>kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-0f123828-5e07-4837-a105-c9f714f489b3 100Mi RWO Delete Bound default/mssql-claim hostpath 25h
>>kubectl describe pv pvc-0f123828-5e07-4837-a105-c9f714f489b3
Name: pvc-0f123828-5e07-4837-a105-c9f714f489b3
Labels: <none>
Annotations: docker.io/hostpath: /var/lib/k8s-pvs/mssql-claim/pvc-0f123828-5e07-4837-a105-c9f714f489b3
pv.kubernetes.io/provisioned-by: docker.io/hostpath
Finalizers: [kubernetes.io/pv-protection]
StorageClass: hostpath
Status: Bound
Claim: default/mssql-claim
Reclaim Policy: Delete
Access Modes: RWO
VolumeMode: Filesystem
Capacity: 100Mi
Node Affinity: <none>
Message:
Source:
Type: HostPath (bare host directory volume)
Path: /var/lib/k8s-pvs/mssql-claim/pvc-0f123828-5e07-4837-a105-c9f714f489b3
HostPathType:
Events: <none>
Kubenetes密钥创建
此处将为之后的SQL Server创建账号以及密码。
>>kubectl create secret generic mssql --from-literal=SA_PASSWORD="pa55wOrd!"
这里的要点是
- Secret的名称是mssql
- Key是SA_PASSWORD
- Key的Value是pa55wOrd!
在创建SQL Server的yaml文件时会运用到以上的信息
在Kubenetes中创建SQL Server
首先来看下加入SQL Server之后整个微服务的架构
可以看到,SQL Server主要暴露出它的ClusterIP,供Platform以及Command服务调用。进行数据的读取以及写入。
创建SQL Server的yaml文件
apiVersion: apps/v1
kind: Deployment
metadata:
name: mssql-depl
spec:
replicas: 1
selector:
matchLabels:
app: mssql
template:
metadata:
labels:
app: mssql
spec:
containers:
- name: mssql
image: mcr.microsoft.com/mssql/server:2017-latest
ports:
- containerPort: 1433
env:
- name: MSSQL_PID
value: "Express"
- name: ACCEPT_EULA
value: "Y"
- name: SA_PASSWORD
valueFrom:
secretKeyRef:
name: mssql
key: SA_PASSWORD
volumeMounts:
- mountPath: /var/opt/mssql/data
name: mssqldb
volumes:
- name: mssqldb
persistentVolumeClaim:
claimName: mssql-claim
注意事项:
- deployment的名字时mssql-depl,之后通过kubectl get deployments查看到的就是这个名字
- 整个deployment会去选择名为mssql的模板(selector app),同时创建了名为mssql的模板(template pp)
- 名为mssql的模板将创建名为mssql的容器(container name),该容器的镜像地址为mcr.microsoft.com/mssql/server:2017-latest,并且配有标签(2017-latest)
- 容器对外暴露的端口号是1433
- 容器的环境变量(配置)如下。MSSQL_PID的值为Express,ACCEPT_EULA的值为Y,SA_PASSWORD的值取自名为mssql的secret中的key为SA_PASSWORD的值
- SQL Server的卷挂载地址为容器所在环境内的地址。这里SQL Server将运行在Linux系统中,所以配置了一个Linux地址。
- SQL Server使用的持久卷名为mssqldb、和挂载的名字一致(实际上,应该是先设置好持久卷名,然后配置容器的持久卷挂载地址、并确定挂载的卷名)。持久卷申领的名字为mssql-claim、即为之前申领的名字
为SQL Server配置Cluster IP
在加入SQL Server后,为了让其它服务能够访问到SQL Server,需要配置一个Cluster IP。
apiVersion: v1
kind: Service
metadata:
name: mssql-clusterip-svc
spec:
type: ClusterIP
selector:
app: mssql
ports:
- name: mssql
protocol: TCP
port: 1433
targetPort: 1433
注意事项:
- 这里选择的
app是mssql、即要创建的mssql容器。 Port和TargetPort一致是为了方便,但是TargetPort必须和ContainerPort一致。
为SQL Server配置负载均衡
负载均衡配置的yaml脚本
apiVersion: v1
kind: Service
metadata:
name: mssql-loadbalancer
spec:
type: LoadBalancer
selector:
app: mssql
ports:
- name: mssql
protocol: TCP
port: 1433
targetPort: 1433
LoadBalancer(负载均衡)服务是Kubernetes提供的服务之一,是NodePort的超集(NodePort是ClusterIP的超集)。这就意味着,当创建负载均衡服务时,也就同时创建了NodePort服务——可以在集群外访问。
>>kubectl describe service mssql-loadbalancer
Name: mssql-loadbalancer
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=mssql
Type: LoadBalancer
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.104.109.162
IPs: 10.104.109.162
LoadBalancer Ingress: localhost
Port: mssql 1433/TCP
TargetPort: 1433/TCP
NodePort: mssql 31137/TCP
Endpoints: 10.1.0.33:1433
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>
LoadBalancer的特征只有当在基于云的集群上才能体现。云服务为自动为负载均衡配置包括对外IP和端口等设置,使得可以通过分配的IP地址以及端口号访问集群内的服务;而在本地,负载均衡服务只是会通过回环地址暴露IP以及目标服务的端口号——与之相比,若采用NodePort还可以更加(手动)灵活地设置端口号和IP地址。
部署SQL Server
执行命令
>>kubectl apply -f mssql-depl.yaml
deployment.apps/mssql-depl created
service/mssql-clusterip-svc created
service/mssql-loadbalancer created
检查Pods
>>kubectl get pods
NAME READY STATUS RESTARTS AGE
commands-depl-7fdbfbc67f-k2jl6 1/1 Running 2 (101m ago) 26h
mssql-depl-856b8c48fd-njn24 1/1 Running 2 (101m ago) 26h
platforms-depl-5748bdd985-9f9l4 1/1 Running 2 (101m ago) 25h
检查Services
>>kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
commands-clusterip-svc ClusterIP 10.97.40.210 <none> 80/TCP 26h
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 26h
mssql-clusterip-svc ClusterIP 10.107.148.45 <none> 1433/TCP 26h
mssql-loadbalancer LoadBalancer 10.104.109.162 localhost 1433:31137/TCP 26h
platforms-clusterip-svc ClusterIP 10.96.108.221 <none> 80/TCP 26h
platforms-np-svc NodePort 10.101.93.173 <none> 80:30001/TCP 26h
Docker中也显示SQL Server已经部署完成(app名为mssql,部署名为mssql-depl)
此时,可以通过数据库管理软件(如SSMS)进行登录查看
可以在其中创建一个数据库、并向从中添加一些数据。如果此时删除容器,再打开数据库,数据仍然存在。这是因为数据持久化在主机中、不会被容器或者Pods的生命周期影响。
更新平台服务、连接SQL Server
配置数据库连接字符串
在appsettings.Production.json中,配置数据库的连接字符串
{
"CommandService": "http://commands-clusterip-svc:80",
"ConnectionStrings": {
"PlatformsConn": "Server=mssql-clusterip-svc,1433;Initial Catalog=platformsdb;User ID=sa;Password=pa55w0rd!"
}
}
这里的Server地址就是Cluster IP——使用这个地址是因为一旦平台服务部署好之后,它会同SQL Server一样都在Kubernetes的Node里面。
配置SQL Server
之前平台服务统一使用内存数据库,现在将更新配置,使得在生产环境下使用SQL Server。
首先要引入环境变量,区分开发与生产环境
using Microsoft.Extensions.Hosting;
public Startup(
IConfiguration configuration, IWebHostEnvironment env)
{
Configuration = configuration;
_env = env;
}
private readonly IWebHostEnvironment _env;
然后根据环境注入不同的数据库
public void ConfigureServices(IServiceCollection services)
{
if (_env.IsDevelopment())
{
Console.WriteLine(">>>Using InMem Db");
services.AddDbContext<ApplicationDbContext>(opt =>
opt.UseInMemoryDatabase("InMemory"));
}
else if (_env.IsProduction())
{
Console.WriteLine(">>>Using SQL Server");
services.AddDbContext<ApplicationDbContext>(opt =>
opt.UseSqlServer(Configuration.GetConnectionString("PlatformsConn")));
}
}
这里会从配置文件中读取数据库连接字符串PlatformConn
string platformConn = Configuration.GetConnectionString("PlatformsConn")
初始化数据库
生成初始化的迁移脚本
一般来说,可以直接调用EF Core的脚本命令(Visual Studio)进行初始化
add-migration InitCreation
但是因为在开发环境(默认环境)下使用内存数据库,migration会发生错误。因此,一方面需要在appsettings.Development.json中,配置数据库的连接字符串,另一方面需要在代码中做出临时的修改。
首先是配置文件
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"CommandService": "https://localhost:44345",
"ConnectionStrings": {
"PlatformsConn": "Server=localhost,1433;Initial Catalog=platformsdb;User ID=sa;Password=pa55w0rd!"
}
}
这里不同于生产环境的配置,因为对数据库的访问是主机访问Kubernetes,因此要使用localhost作为IP地址。
再是在代码中的临时修改
*//if (_env.IsDevelopment())
//{
// Console.WriteLine(">>>Using InMem Db");
// services.AddDbContext<ApplicationDbContext>(opt =>
// opt.UseInMemoryDatabase("InMemory"));
//}
//else if (_env.IsProduction())
//{
// Console.WriteLine(">>>Using SQL Server");
// services.AddDbContext<ApplicationDbContext>(opt =>
// opt.UseSqlServer(Configuration.GetConnectionString("PlatformsConn")));
//}*
services.AddDbContext<ApplicationDbContext>(opt =>
opt.UseSqlServer(Configuration.GetConnectionString("PlatformsConn")));
这样就可以进行migration脚本的生成。
但是还遇到了其它的麻烦,报错内容为
Add-Migration : 无法将“Add-Migration”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
该问题的解决思路可以参考这两篇文章:
- 权限赋予:无法将“Add-Migration”项识别为 cmdlet、函数、脚本文件或可运行程序的名称
- Asp.net Core 添加 EF 工具并执行初始迁移错误解决方法(Add-Migration Initial---Build failed.)
完成之后,再次执行add-migration命令:
>>Add-migration InitialCreation
Build start..
Buidl succeeded.
To undo this action, use Remove-Migration.
可以看到对于之前平台服务创建的模型有对应的migration。
namespace PlatformService.Migrations
{
public partial class InitialCreation : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Platforms",
columns: table => new{
PlatformId = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
Publisher = table.Column<string>(type: "nvarchar(max)", nullable: false),
Cost = table.Column<string>(type: "nvarchar(max)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Platforms", x => x.PlatformId);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Platforms");
}
}
}
初始化数据库
在之前的环境中,是对内存数据库进行初始化的。现在同样也要对这部分进行环境的区分,使得在生产环境下,可以初始化SQL Server数据库。
传入环境变量
public static void MockPopulation(IApplicationBuilder app, bool isProduction)
{
using (var serviceScope = app.ApplicationServices.CreateScope())
{
SeedData(serviceScope.ServiceProvider.GetService<ApplicationDbContext>(), isProduction);
}
}
private static void SeedData(ApplicationDbContext context, bool isProduction)
{ }
外部调用代码为
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
MockInMemoryDatabase.MockPopulation(app, _env.IsProduction());
}
然后在SeedData函数中,针对环境进行初始化区分
if (isProduction)
{
Console.WriteLine(">>>Attempting to apply migration...");
try{
context.Database.Migrate();
}
catch (Exception ex)
{
Console.WriteLine($">>>Cannot run migration: {ex.Message}");
}
}
这里无须担心,每次初始化平台服务时都会进行重复进行migration。这是因为migration时会用现有的migrate脚本名字和数据库(连接字符串对应的数据库)中一张名为__EFMigrationsHistory的表中的migrate脚本名字进行比对,如果数据库中不存在现有的migrate脚本,则将进行migration;反之则不会进行。
重新构建平台服务的镜像,并重新部署
该部分可以参考第四章内容
测试平台服务与SQL Server的连接
查看初始化数据
然后再通过创建接口,插入一条新数据
查看全部数据: