.NET 微服务实践(5)-搭建容器化SQL Server

229 阅读11分钟

.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之后整个微服务的架构 5-2sql server considered arch.png 可以看到,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容器。
  • PortTargetPort一致是为了方便,但是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的超集(NodePortClusterIP的超集)。这就意味着,当创建负载均衡服务时,也就同时创建了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)进行登录查看 5-7sql server login.png 可以在其中创建一个数据库、并向从中添加一些数据。如果此时删除容器,再打开数据库,数据仍然存在。这是因为数据持久化在主机中、不会被容器或者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命令:

>>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的连接

查看初始化数据 5-9 初始化数据.png 然后再通过创建接口,插入一条新数据 5-10 create platform.png 查看全部数据: 5-11 get all platforms.png