在前面一篇中,我们使用了模拟的数据DummyRepository,数据是存储在内存中的,然而在实际应用中,一般数据是存放在数据库中,所以数据库应该也有单独的容器来承载,那数据库文件呢?这里就用到了Volume, 在这一篇我们会接触到Docker里的两个重要概念:Volume 卷和Software-define Network自定义网络。

    Volume是卷的意思,我们能够将数据库或者应用程序产生的重要数据,存放在单独的Volume,而不是容器内,这样容器删除或者变更不会对这些数据产生影响。

    Software-define Network,软件自定义网络是一种Docker内的网内协议,允许容器内的应用程序能够相互通讯。

了解Volume


    应用程序可能会用到两只类型的文件:运行应用程序所需的文件,包括常见的dll,资源文件等等;应用程序所产生的文件,比如由于用户操作,所产生的文件,比如订单数据,日志文件等等。在Docker中,这两类数据的处理方式不同。

    应用程序运行所需的文件是Docker容器的一部分,比如dll文件,配置文件等等,我们可以在容器下面使用ls命令查看。Docker处理docker文件生成镜像,然后然后根据镜像模板生成容器。比如对于ASP.NET Core MVC程序来说,docker容器包括.NET Core运行时,ASP.NET Core依赖包,自定义的dll,CSS样式表,配置文件等等。缺失这些文件,ASP.NET Core MVC应用程序就不能运行。

▲ Docker容器内的文件

    应用程序产生或使用的数据文件不应该存放到应用程序自身所在容器中。使用容器的最大好处是方面创建和销毁,当应用程序容器销毁时,容器内的所有文件也就跟着删除了,这对于一些需要持久化的应用程序来说,就非常不好了。所以Docker提供了Volume卷的特性,来帮助我们管理应用程序的数据。

   为了方便演示,切换到DockerStudyInVsCode目录,然后新建一个名为VolumeAndNetwork的ASP.NET Core MVC的应用程序:

PS D:\Study\DockStudyInVSCode> dotnet new mvc --no-https --output VolumeAndNetwork --framework net5.0

    然后新建一个Dockerfile.volumes的文件,内容如下:

FROM alpine
WORKDIR /data
ENTRYPOINT (test -e message.txt && echo "File exists"||(echo "Creating File ..." && echo hello, Docker $(date '+%X') > message.txt))&& cat message.txt

    这个Dockerfile的内容首先拉取alpine(一个小型linux发行版),然后设置当前工作目录。最后设置程序启动入口,看message.txt这个文件是否存在,如果不存在则创建并写入创建时间,否则,直接显示文件内容。

   然后创建镜像:

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker build . -t volumeandnetwork/vtest -f Dockerfile.volumes

   然后运行容器:

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker run --name vtest volumeandnetwork/vtest

    可以看到,输出结果如下:

Creating File ...
hello, Docker 07:11:31

   可以看到,该文件是新建的。还有个点,这个时间默认是UTC时间的,要改为北京时间,需要在docker文件里添加:

    由于上述采用的是docker run命令,所以容器在运行完成之后就关闭了,现在重新运行容器:

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker start -a vtest
File exists
hello, Docker 07:11:31

    可以看到,文件是存在的,内容也跟之前一样。

    现在,删除容器,重新运行容器:

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker rm -f vtest
vtest

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker run --name vtest volumeandnetwork/vtest
Creating File ...
hello, Docker 07:35:28

    可以看到,容器删除之后,之前的文件并没有保存,再次运行容器时,重新创建了新的文件。

    现在,使用Volume来保存数据。首先,在Dockerfile.volumes文件中添加 VOLUME /data

FROM alpine
VOLUME /data
WORKDIR /data
ENTRYPOINT (test -e message.txt && echo "File exists"||(echo "Creating File ..." && echo hello, Docker $(date '+%X') > message.txt))&& cat message.txt

    VOLUME命令告诉Docker系统,任何存储到/data下面的数据,都会存储到常规容器之外的文件系统中。需要注意的是,容器里的应用程序并不知道/data文件夹里的文件系统有什么特别,他们就像在容器内的文件系统那样读取。

    然后,根据修改后的dockerfile新建镜像。

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker build . -t volumeandnetwork/vtest -f Dockerfile.volumes

    然后新建一个Volume用来存放数据文件,可以使用下面的命令来创建volume

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker volume create --name testdata
testdata

    紧接着,根据上述创建的镜像和volume,来创建容器。

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker run --name vtest2 -v testdata:/data volumeandnetwork/vtest
Creating File ...
hello, Docker 08:08:34

    -v参数告诉Docker,容器里任何在/data下新建的数据,都存储在名为testdata的卷里。卷非常像一个常规的文件目录。运行结果可以看到,新文件被创建。现在重新删除容器,再次创建并运行容器。

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker rm -f vtest2
vtest2

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker run --name vtest2 -v testdata:/data volumeandnetwork/vtest
File exists
hello, Docker 08:08:34

    可以看到,即使将容器删除,在卷里保存的数据仍然存在。

判断镜像是否使用了Volume

    有两只方法可以判断某个镜像是否依赖volume,第一种方法是,看创建镜像的Dockerfile文件,可以看Dockerfile文件中是否包含有VOLUME关键字,在查看Dockerfile的时候不要忘记查看对应的base镜像是否也用了volume。

    另外一种方法就是直接检查镜像,当没办法直接访问Dockerfile文件时,使用该方法比较方便。检查镜像是否使用了volume可以使用docker inspect命令:

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker inspect volumeandnetwork/vtest
[
    {
        "Id": "sha256:dc4ad9d14faa667e215fec367000979e168eae69ab825cc815dc5f612a08a8f8",
        "RepoTags": [
            "volumeandnetwork/vtest:latest"
        ],
        "RepoDigests": [],
        "Parent": "",
        "Comment": "buildkit.dockerfile.v0",
        "Created": "2021-04-19T07:01:26.3553777Z",
        "Container": "",
        "ContainerConfig": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": null,
            "Cmd": null,
            "Image": "",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": null
        },
        "DockerVersion": "",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": null,
            "Image": "",
            "Volumes": {
                "/data": {}
            },
            "WorkingDir": "/data",
            "Entrypoint": [
                "/bin/sh",
                "-c",
                "(test -e message.txt && echo \"File exists\"||(echo \"Creating File ...\" && echo hello, Docker $(date '+%X') > message.txt))&& cat message.txt"
            ],
            "OnBuild": null,
            "Labels": null
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 5613158,
        "VirtualSize": 5613158,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/cc789fecd056d12d88e0147c86da115188b61a394c778e2f96ef144668755b92/diff",
                "MergedDir": "/var/lib/docker/overlay2/wvxwhmtwr075vcbdfgv4miqsn/merged",
                "UpperDir": "/var/lib/docker/overlay2/wvxwhmtwr075vcbdfgv4miqsn/diff",
                "WorkDir": "/var/lib/docker/overlay2/wvxwhmtwr075vcbdfgv4miqsn/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:b2d5eeeaba3a22b9b8aa97261957974a6bd65274ebd43e1d81d0a7b8b752b116",
                "sha256:ae57b504dd70112d1c2c027e16a12abfb3b4ff049303cc5dcbe04ecaffa49f88"
            ]
        },
        "Metadata": {
            "LastTagTime": "2021-04-19T08:05:32.1442318Z"
        }
    }
]

      在输出的详情里,我们可以看到如下相关信息,即表示该镜像依赖volume:

...
"Volumes": {
                "/data": {}
            },
...

    除了使用命令,还可以使用Docker Desktop来查看,具体方法如下:

ASP.NET Core MVC使用volume存储MySQL数据


    大多数ASP.NET Core MVC应用程序都依赖数据库,这些数据库数据都存储在volume中,这样当数据库所在的容器删除后,数据库文件仍然存在。有很多数据库产品,单对于容器化的ASP.NET Core程序来说,最常用的数据库就是MySQL,一方面是因为它能很好运行在Linux容器上,另外也能很好的支持Entity Framework Core。

    在开始之前,首先要拉去MySQL镜像到本机,命令很简单 docker pull mysql,就会自动下载最新版本的mysql镜像:

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker pull mysql
Using default tag: latest
latest: Pulling from library/mysql
f7ec5a41d630: Already exists
9444bb562699: Pull complete
6a4207b96940: Pull complete
181cefd361ce: Pull complete
8a2090759d8a: Pull complete
15f235e0d7ee: Pull complete
d870539cd9db: Pull complete
5726073179b6: Pull complete
eadfac8b2520: Pull complete
f5936a8c3f2b: Pull complete
cca8ee89e625: Pull complete
6c79df02586a: Pull complete
Digest: sha256:6e0014cdd88092545557dee5e9eb7e1a3c84c9a14ad2418d5f2231e930967a38
Status: Downloaded newer image for mysql:latest
docker.io/library/mysql:latest

    下载完成之后,使用docker inspect mysql命令查看该镜像是否使用了volume。

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker inspect mysql
[
    {
        "Id": "sha256:cbe8815cbea8fb86ce7d3169a82d05301e7dfe1a8d4228941f23f4f115a887f2",
        "RepoTags": [
            "mysql:latest"
        ],
        "RepoDigests": [
            "mysql@sha256:6e0014cdd88092545557dee5e9eb7e1a3c84c9a14ad2418d5f2231e930967a38"
        ],
        "Parent": "",
        "Comment": "",
        "Created": "2021-04-10T07:22:41.285951659Z",
        "Container": "1f81d0801dce5d57979f73bcebcb363307a6a345cf8910ad61b06a556a1b974e",
        "ContainerConfig": {
            "Hostname": "1f81d0801dce",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "3306/tcp": {},
                "33060/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "GOSU_VERSION=1.12",
                "MYSQL_MAJOR=8.0",
                "MYSQL_VERSION=8.0.23-1debian10"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"mysqld\"]"
            ],
            "Image": "sha256:102429f1f102c2ff7d300a8cf548aa41dfaf6b11b952cea12aabca8d515d95d8",
            "Volumes": {
                "/var/lib/mysql": {}
            },
            "WorkingDir": "",
            "Entrypoint": [
                "docker-entrypoint.sh"
            ],
            "OnBuild": null,
            "Labels": {}
        },
        "DockerVersion": "19.03.12",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "3306/tcp": {},
                "33060/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "GOSU_VERSION=1.12",
                "MYSQL_MAJOR=8.0",
                "MYSQL_VERSION=8.0.23-1debian10"
            ],
            "Cmd": [
                "mysqld"
            ],
            "Image": "sha256:102429f1f102c2ff7d300a8cf548aa41dfaf6b11b952cea12aabca8d515d95d8",
            "Volumes": {
                "/var/lib/mysql": {}
            },
            "WorkingDir": "",
            "Entrypoint": [
                "docker-entrypoint.sh"
            ],
            "OnBuild": null,
            "Labels": null
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 546129528,
        "VirtualSize": 546129528,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/c77ac41207953ab29445647f76cb789a9961418711dc6ee2000b472c15cd6f0d/diff:/var/lib/docker/overlay2/d9c07610cedf37e0827bf22d565879b9d637884de7f6402802e2352fb58cbc5c/diff:/var/lib/docker/overlay2/3c1bb90b081d79b194bd9cff714e1bb6c6c19ed3d1ba74a19055aaf9fce7473d/diff:/var/lib/docker/overlay2/9bd45fa0d3a8d15f4161193a9c8f9e17d4cf3b2cb3199fa9c9f081d1c9dda5c4/diff:/var/lib/docker/overlay2/af5cbf45ac9b7a4712f657a65457048058415c1ec1d79f41a4f299d8b0be112f/diff:/var/lib/docker/overlay2/a370ee63f081eabf435ca1bb04e084f768b25e37e67798d452ce839f259a27c7/diff:/var/lib/docker/overlay2/74e8ab4d2690350a032793c75d4a69810fef15824934fca4e2f72a5e86b5d9db/diff:/var/lib/docker/overlay2/b935574d529f642bbb32c44767f7ed6cd7bb2b04660f21d1c5e977f1bf79178f/diff:/var/lib/docker/overlay2/bcf6cf6a68dbb6ac2a75b8aceb79db0bf4c088f3ae1f05cae13e980f841f17e4/diff:/var/lib/docker/overlay2/92313c33900d50b795451844623e4af783f16bc5ef3b74994dc7188f80a2b5d4/diff:/var/lib/docker/overlay2/2f6a4b1a7154abc9479f1b6097a35320793aff4b21effc7b6783aeef53392534/diff",
                "MergedDir": "/var/lib/docker/overlay2/109a72a5d75e07bee2dd1e97c798dec637d434d08d325fd0f9450034add45905/merged",
                "UpperDir": "/var/lib/docker/overlay2/109a72a5d75e07bee2dd1e97c798dec637d434d08d325fd0f9450034add45905/diff",
                "WorkDir": "/var/lib/docker/overlay2/109a72a5d75e07bee2dd1e97c798dec637d434d08d325fd0f9450034add45905/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:7e718b9c0c8c2e6420fe9c4d1d551088e314fe923dce4b2caf75891d82fb227d",
                "sha256:76233144372b8c3080388cb9a51e4ac54251c878b171c85deb3206ec7ae852b5",
                "sha256:f68961560ec1a3ebe0734207cc012542eaede1f6ed2eaa33fa4622ed71a8670f",
                "sha256:7afbf38fd1da13be912bc228dc4f4a1fcf73c6f6b9173d8c1066706d3c6b8f3a",
                "sha256:2b174283057145361120d65da7c747cc83442c6524284a25b740e45ee6e7b3d8",
                "sha256:2cb169012988b8d656cf0e296476b6c2e554c9cf2bb2eeca86a25a45a3324471",
                "sha256:1ce52ff7c16fb714f74c89a7841e6f92b2fc17e49e0183fccb2028d675bd9b63",
                "sha256:5c8959c97bcaa1bd9a227c47809d42a0c54dbb400182b04c3b72423ae9c6ec75",
                "sha256:dc78957019e43a03e6e1893d274b1e102378d15426456dfa2cedb57491c21a8c",
                "sha256:f6d7f72f6993e795a0f88e3af1913a7e95c64b14e274b7b4ce9ae22360fd05e9",
                "sha256:7d14a1d779d24d5a3b10e2d14b2bebceaaf25cebc7bc592691cb2f8148c156ec",
                "sha256:d2ef8a2711ff0cb0295b151decd23b38288dfbf0546572e1b9fdb9eedd31d5b8"
            ]
        },
        "Metadata": {
            "LastTagTime": "0001-01-01T00:00:00Z"
        }
    }
]

    可以看到,MySQL使用了Volume

 "Volumes": {
                "/var/lib/mysql": {}
            },

   所以要使用MySQL之前,需要创建一个volume,使用命令 docker volume来创建:

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker volume create --name productdata

    接下来万事俱备,我们就可以根据MySQL镜像和自定义volume来创建MySQL的容器了,命令如下:

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker run -d --name mysql -v productdata:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mysecret -e bind-address=0.0.0.0 mysql
  • -d参数,表示在容器后台运行
  • -v参数用来指定volume, 后面的/var/lib/mysql是根据mysql镜像里设置的路径配置的,要查看这个路径可以使用如下命令 docker insepect mysql
  • -e是设置环境变量,这里设置了两个变量,一个是密码,一个是绑定地址
  • -p还可以指定端口映射,宿主机的端口:容器的3306端口,这里不列出了,就是默认的3306端口。

    因为是-d在容器后台运行,使用docker logs 命令,可以查看mysql容器的运行日志:

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker logs -f mysql
2021-04-19 09:11:22+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.23-1debian10 started.
2021-04-19 09:11:22+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
2021-04-19 09:11:22+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.23-1debian10 started.
2021-04-19 09:11:22+00:00 [Note] [Entrypoint]: Initializing database files
2021-04-19T09:11:22.816036Z 0 [System] [MY-013169] [Server] /usr/sbin/mysqld (mysqld 8.0.23) initializing of server in progress as process 42
2021-04-19T09:11:22.823151Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
2021-04-19T09:11:33.486220Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
2021-04-19T09:12:07.267475Z 6 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
2021-04-19 09:13:03+00:00 [Note] [Entrypoint]: Database files initialized
2021-04-19 09:13:03+00:00 [Note] [Entrypoint]: Starting temporary server
2021-04-19T09:13:03.825523Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.23) starting as process 93
2021-04-19T09:13:03.882742Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
2021-04-19T09:13:04.777618Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
2021-04-19T09:13:05.324312Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: /var/run/mysqld/mysqlx.sock
2021-04-19T09:13:06.009203Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
2021-04-19T09:13:06.009542Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
2021-04-19T09:13:06.065575Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a 
different directory.
2021-04-19T09:13:06.132850Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.23'  socket: '/var/run/mysqld/mysqld.sock'  port: 0  MySQL Community Server - GPL.
2021-04-19 09:13:06+00:00 [Note] [Entrypoint]: Temporary server started.
Warning: Unable to load '/usr/share/zoneinfo/iso3166.tab' as time zone. Skipping it.
Warning: Unable to load '/usr/share/zoneinfo/leap-seconds.list' as time zone. Skipping it.
Warning: Unable to load '/usr/share/zoneinfo/zone.tab' as time zone. Skipping it.
Warning: Unable to load '/usr/share/zoneinfo/zone1970.tab' as time zone. Skipping it.

2021-04-19 09:13:14+00:00 [Note] [Entrypoint]: Stopping temporary server
2021-04-19T09:13:15.035015Z 10 [System] [MY-013172] [Server] Received SHUTDOWN from user root. Shutting down mysqld (Version: 8.0.23).
2021-04-19T09:13:31.969834Z 0 [System] [MY-010910] [Server] /usr/sbin/mysqld: Shutdown complete (mysqld 8.0.23)  MySQL Community Server - GPL.
2021-04-19 09:13:32+00:00 [Note] [Entrypoint]: Temporary server stopped

2021-04-19 09:13:32+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up.

2021-04-19T09:13:32.296801Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.23) starting as process 1
2021-04-19T09:13:32.318404Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
2021-04-19T09:13:33.367818Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
2021-04-19T09:13:33.671589Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock
2021-04-19T09:13:34.148361Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
2021-04-19T09:13:34.148556Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
2021-04-19T09:13:34.206543Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a 
different directory.
2021-04-19T09:13:34.230035Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.23'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.

    可以看到“mysqld: ready for connections”,就表示mysql服务已经成功运行了。

准备应用程序


    因为使用的是Entity Framework Core框架来访问MySQL,所以需要添加对EFCore的引用。

    在Visual Studio Code里添加引用可以直接编辑.csproj文件,也可以通过添加“NuGet Package Manager”插件来向导式添加,后者不容易出错。安装完常见之后,按"Ctrl+Shit+P",在弹出的列表中选择“NuGet Package Manager: Add Package”,然后输入“Microsoft.EntityFrameworkCore”,不出意外,会出现错误:

Versioning information could not be retrieved from the NuGet package repository. Please try again later.

   这需要我们编辑位于“C:\Users\yourname\.vscode\extensions\jmrog.vscode-nuget-package-manager-1.1.6\out\src\actions\add-methods>fetchPackageVersions.js”

     在selectedPackageName后面加上toLowerCase()方法,然后保存,然后重启Visual Studio Code,再重试。

    按照以上方法,依次添加如下包,添加完成之后,项目工程文件如下:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.5"/>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.5"/>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3"/>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.5"/>
    <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="5.0.0-alpha.2"/>
  </ItemGroup>
</Project>

    添加好后,执行dotnet restore,将包还原进来。

添加真实的Repository类

    前面用的DummyRepository,现在需要创建真正的EFCore模型,来访问数据库。在“VolumeAndNetwork/Model”文件夹下,新建ProductDbContext.cs文件,内容如下:

using Microsoft.EntityFrameworkCore;

namespace VolumeAndNetwork.Models
{
    public class ProductDbContext : DbContext
    {
        public ProductDbContext(DbContextOptions<ProductDbContext> options) : base(options) { }
        public DbSet<Product> Products { get; set; }
    }
}

    然后新建ProductRepository.cs文件:

using System.Linq;
namespace VolumeAndNetwork.Models{
    public class ProductRepository:IRepository
    {
           private ProductDbContext context;
           public ProductRepository(ProductDbContext ctx)
           {
               context=ctx;
           }
           public IQueryable<Product> Products=>context.Products;
    }
}

    现在我们要提供种子测试数据,用来在初始化时,填充必要的数据到数据库里来。

using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
namespace VolumeAndNetwork.Models
{
    public class SeedData
    {
        public static void EnsurePopulated(IApplicationBuilder app)
        {
            EnsurePopulated(app.ApplicationServices.GetRequiredService<ProductDbContext>());
        }

        public static void EnsurePopulated(ProductDbContext context)
        {
            System.Console.WriteLine("Apply Migration...");
            context.Database.Migrate();
            if (!context.Products.Any())
            {
                System.Console.WriteLine("Create Seed Data...");
                context.Products.AddRange(
                    new Product("花生", "食品", 5.2m),
                    new Product("瓜子", "食品", 7.22m),
                    new Product("榨菜", "食品", 12.0m),
                    new Product("肥皂", "洗护", 32.12m),
                    new Product("洗发水", "洗护", 25.32m),
                    new Product("沐浴露", "洗护", 22.12m),
                    new Product("可乐", "饮料", 3.0m),
                    new Product("白酒", "饮料", 12.0m),
                    new Product("啤酒", "饮料", 8.0m)
                );
                context.SaveChanges();
            }
            else
            {
                System.Console.WriteLine("Seed Data Not Required...");
            }
        }
    }
}

    SeedData里面的EnsurePopulated方法,用来创建数据库,并且填充数据。 context.Database.Migrate(); 这一句话,用来创建数据库迁移。

配置应用程序

    现在要配置EFCore相关的服务,如下:

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            var host=Configuration["DBHOST"]??"localhost";
            var port=Configuration["DBPORT"]??"3306";
            var password=Configuration["DBPASSWORD"]??"mysecret";
            services.AddDbContext<ProductDbContext>(options=>options.UseMySql($"server={host};userid=root;pwd={password};port={port};database=products",ServerVersion.FromString("8.0.23")));
            services.AddSingleton<IConfiguration>(Configuration);
            services.AddTransient<IRepository,ProductRepository>();
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });

            SeedData.EnsurePopulated(app);
        }
    }

    还需要在Programe中修改如下地方:

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseDefaultServiceProvider(i=>i.ValidateScopes=false);
                    webBuilder.UseStartup<Startup>();
                });
    }

    紧接着,在Visual Studio Code的Powershell命令行中,执行migrations命令:

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> dotnet ef migrations add Initial
Build started...
Build succeeded.
The Entity Framework tools version '3.1.3' is older than that of the runtime '5.0.5'. Update the tools for the latest features and bug fixes.
Done. To undo this action, use 'ef migrations remove'

创建ASP.NET Core MVC镜像

    现在,需要将应用程序编译然后发布。首先,添加Dockerfile,内容如下:

#使用asp.net 5.0作为基础镜像,起一个别名为base
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
#设置容器的工作目录为/app
WORKDIR /app
#暴露容器的tcp 80端口
EXPOSE 80/tcp

ENV ASPNETCORE_URLS=http://+:80

# Creates a non-root user with an explicit UID and adds permission to access the /app folder
# For more info, please refer to https://aka.ms/vscode-docker-dotnet-configure-containers
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

#使用.net sdk 5.0作为基础镜像,起一个别名为build
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
#设置容器的工作目录为/src
WORKDIR /src
#将当前本机VolumeAndNetwork/VolumeAndNetwork.csproj项目文件复制到容器中的/src/VolumeAndNetwork/目录下
COPY ["VolumeAndNetwork/VolumeAndNetwork.csproj", "VolumeAndNetwork/"]
#执行dotnet restore,还原本地文件
RUN dotnet restore "VolumeAndNetwork/VolumeAndNetwork.csproj"
#复制当前目录文件,到容器的/src目录下
COPY . .
#设置容器的工作目录为/src/VolumeAndNetwork
WORKDIR "/src/VolumeAndNetwork"
#执行编译生成,以Release模式生成到容器的/app/build目录下
RUN dotnet build "VolumeAndNetwork.csproj" -c Release -o /app/build

#以上面的build作为基础镜像,重命名为publish
FROM build AS publish
#执行dotnet publish命令,发布到容器的/app/publish目录下
RUN dotnet publish "VolumeAndNetwork.csproj" -c Release -o /app/publish

#将上面的base作为基础镜像,重命名为final
FROM base AS final
#设置容器的工作目录为/app
WORKDIR /app
#拷贝/app/publish到当前工作目录
COPY --from=publish /app/publish .
#指定容器入口命令,容器启动时,会运行dotnet VolumeAndNetwork.dll
ENTRYPOINT ["dotnet", "VolumeAndNetwork.dll"]

    然后,切换到上一级目录,执行一下命名,创建镜像:

PS D:\Study\DockStudyInVSCode> docker build . -t volumeandnetwork/testapp -f VolumeAndNetwork/Dockerfile

测试应用程序

    现在镜像建立起来了,要测试应用程序,需要根据镜像创建容器。现在还不能立即创建容器,因为我们的容器依赖MySQL数据库,应用程序镜像是通过docker内部的网络来访问MySQL数据库容器的,这个网络,不是我们配置的localhost,具体的访问地址是有docker设置好的。

    可以通过docker network inspect bridge,来查看docker默认的虚拟network的IP地址信息:

PS D:\Study\DockStudyInVSCode> docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "113e72fc5130d71d2c77c25b22cbcc6d313a1a645c37f9dd2565d4b53bff4b31",
        "Created": "2021-04-19T05:30:46.873625Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "9d3e58a47dd1b721dca406a6498ccd469a1fb6ae51b9395a56ba006b6c6e9698": {
                "Name": "mysql",
                "EndpointID": "19210cee1907ff1ddc3d533a33e259f151a13400513b548f8cedb679930ab291",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

    可以看到container节点里面,Name为mysql, IPv4Address,就是我们访问数据库的IP地址。

    接下来,根据这些信息,我们可以创建测试应用程序了。命令如下:

PS D:\Study\DockStudyInVSCode> docker run -d --name productapp3001 -p 3001:80 -e DBHOST=172.17.0.2 volumeandnetwork/testapp
d0f7ab691ed20766d51b5d0513f8b652f2c02e9376466e31f4824a6ccdab383c

     将本机的3001端口映射到了容器的80端口,并且设定了DBHOST即MySQL的服务器的IP地址参数。

    查看日志,也能看到相关信息:

PS D:\Study\DockStudyInVSCode> docker logs -f productapp3001
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
      Storing keys in a directory '/home/appuser/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {ef67880b-d0ec-4962-9d2d-bc60b62d8794} may be persisted to storage in unencrypted form.
Apply Migration...
Create Seed Data...
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app

    在浏览器中输入 http://localhost:3001/ 即可看到一下信息:

    在命令行输入:

PS D:\Study\DockStudyInVSCode> docker exec -it mysql /bin/bash
root@9d3e58a47dd1:/# mysql -uroot -p
Enter password: 
mysql> use products
mysql> select * from Products;
+-----------+------+----------+-----------------------------------+
| ProductID | Name | Category | Price                             |
+-----------+------+----------+-----------------------------------+
|         1 | ??   | ??       |  5.200000000000000000000000000000 |
|         2 | ??   | ??       |  7.220000000000000000000000000000 |
|         3 | ??   | ??       | 12.000000000000000000000000000000 |
|         4 | ??   | ??       | 32.120000000000000000000000000000 |
|         5 | ???  | ??       | 25.320000000000000000000000000000 |
|         6 | ???  | ??       | 22.120000000000000000000000000000 |
|         7 | ??   | ??       |  3.000000000000000000000000000000 |
|         8 | ??   | ??       | 12.000000000000000000000000000000 |
|         9 | ??   | ??       |  8.000000000000000000000000000000 |
+-----------+------+----------+-----------------------------------+
9 rows in set (0.00 sec)

    就可以看到MySQL里面保存的数据,由于之前保存的是中文,所以这里没法显示。

    退出MySQL和Linux命令行回到Visual Studio Code的PowerShell可以输入exit即可。

自定义网络


    通过软件定义(software-defined network, SDN)的虚拟网络,能够是的容器内的应用程序能够相互通讯。简单的SDN网络被限制在单个服务器上。前面使用的默认网络是 default bridge network。这个网络是Docker在运行时创建的。可以使用以下命令查看Docker上的所有网络。

PS D:\Study\DockStudyInVSCode> docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
113e72fc5130   bridge    bridge    local
49ce19152f68   host      host      local
819bf5184145   none      null      local

    host网络是本机网络,none网络没有任何连接,他能够被用来将容器完全隔离。bridge网络是Docker在每个容器运行起来都会添加的网络。可以通过一下命令查看网络详情:

PS D:\Study\DockStudyInVSCode> docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "113e72fc5130d71d2c77c25b22cbcc6d313a1a645c37f9dd2565d4b53bff4b31",
        "Created": "2021-04-19T05:30:46.873625Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "9d3e58a47dd1b721dca406a6498ccd469a1fb6ae51b9395a56ba006b6c6e9698": {
                "Name": "mysql",
                "EndpointID": "19210cee1907ff1ddc3d533a33e259f151a13400513b548f8cedb679930ab291",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            },
            "d0f7ab691ed20766d51b5d0513f8b652f2c02e9376466e31f4824a6ccdab383c": {
                "Name": "productapp3001",
                "EndpointID": "d13767e92293ad1ea27cdceb53b6a62000c35c56aa89a1598402d08006167387",
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

    可以看到,在bridge网络中,存在我们之前创建的productapp3001和MySQL容器。在前面例子中,我们使用默认的bridge网络将ASP.NET Core MVC应用程序和MySQL连起来了。

    

扩展ASP.NET Core MVC应用程序


    自定义网络跟物理网络一样,容器就跟物理服务器一样能够访问自定义网络,这意味着,扩展ASP.NET Core MVC程序到多个应用服务器上,就跟直接创建和开启额外的容器一样方便,我们可以使用下面命令,创建和运行另外一个服务器:

PS D:\Study\DockStudyInVSCode> docker run -d --name productapp3002 -p 3002:80 -e DBHOST=172.17.0.2 -e MESSAGE="2nd Server" volumeandnetwork/testapp
b85c3a2637778c25131e289735d223b2df83c1d0a099fe7567a9374e9f254f29

   现在我们运行着两个容器productapp3001和productapp3002。需要注意的是,不同的容器,需要指定不同的名称和不同的本机映射端口。在这里,-e添加了额外的MESSAGE参数。-d告诉docker在后台运行。

   在本地的3002和3001端口有两个ASP.NET Core MVC应用程序,他们都能接收请求。

   现在结构图如下:

创建自定义网络

    上面使用的是Docker提供的默认的bridge网络,这个网络有两个限制。首先一个比较尴尬的地方是,我们需要去查看MySQL所使用的的网络的具体的IP地址,来配置ASP.NET Core MVC的MySQL参数。比如上面的-e DBHOST=172.17.0.2。另外一个比较尴尬的地方是,目前所有的容器都连接在了同一个网络上,一般大型的应用程序通常会设计不同的网络,以用来划分不同的功能区域,使得不同的部分相互独立并且能方便的监控和管理。以上两个问题,我们都可以通过创建自定义网络来代替默认的bridge网络来解决。为了演示,首先删除所有的镜像。

PS D:\Study\DockStudyInVSCode> docker rm -f $(docker ps -aq)

    接下来,使用docker network create 来创建两个网络,我们命名为frontend和backend,分别表示前端和后端网络。

PS D:\Study\DockStudyInVSCode> docker network create frontend
68d13bd3b9df10307908b74f95e969daaf98741525293f5de871b4d9317c4a94
PS D:\Study\DockStudyInVSCode> docker network create backend
cf6bfc1cf89dbce4dbfb12c1fcd2335f9ee1732944b575a6c415215a088eff1d

    现在,通过docker network ps查看所有网络:

PS D:\Study\DockStudyInVSCode> docker network ls
NETWORK ID     NAME       DRIVER    SCOPE
cf6bfc1cf89d   backend    bridge    local
113e72fc5130   bridge     bridge    local
68d13bd3b9df   frontend   bridge    local
49ce19152f68   host       host      local
819bf5184145   none       null      local

    可以看到,两个backend和frontend网络创建成功。

将容器连接到自定义网络

    创建完自定义网络之后,我们就可以通过关键字 --network 在创建或运行容器时,把容器连接到自定义网络。如下命令,我们创建一个新的MySQL容器,并将其连接到backend网络上。

PS D:\Study\DockStudyInVSCode> docker run -d --name mysql -v productdata:/var/lib/mysql --network=backend -e MYSQL_ROOT_PASSWORD=mysecret -e bind-address=0.0.0.0 mysql
c56af29915894edad7af17e166663612476218e97a78995a207040dea6fce992

    现在,无论是通过docker inspect mysql,还是通过docker network inspect backend,都能看到MySQL容器已经跟backend自定义网络连接上了。

    需要注意的是,上述创建mysql容器时,没有制定本机到容器的映射,所以本机无法访问MySQL,要访问必须通过backend自定义网络访问。

    使用了自定义网络的容器内的DNS服务,可以用容器名代替主机名,从而不需要使用容器所在的IP地址,这是默认的bridge网络所不具备的特性。比如可以执行下面的命令:

PS D:\Study\DockStudyInVSCode> docker pull alpine
PS D:\Study\DockStudyInVSCode> docker run -it --rm --network backend alpine ping -c 3 mysql
PING mysql (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=8.013 ms
64 bytes from 172.19.0.2: seq=1 ttl=64 time=0.062 ms
64 bytes from 172.19.0.2: seq=2 ttl=64 time=0.073 ms

--- mysql ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.062/2.716/8.013 ms

    这条命令,首先根据Alpine Linux发行版创键并运行一个容器,然后执行 ping命令,来ping一个名为mysql的服务器。在这一过程中Docker会自动解析mysql容器所使用的名为backed自定义网络所在的ip地址。执行完成之后,会删除容器。

    利用使用了自定义网络的容器可以使用容器名称代替IP地址的特性,我们重新创建三个ASP.NET Core MVC应用程序容器,在配置MySQL的DBHOST地址的时候,直接使用容器名称mysql

PS D:\Study\DockStudyInVSCode> docker create --name productapp1 -e DBHOST=mysql -e MESSAGE="1st Server" --network backend volumeandnetwork/testapp
7135a738a7f348533b4237a5c767bfa5e548913d7176d7ba2a755b2aafbb3a45
PS D:\Study\DockStudyInVSCode> docker create --name productapp2 -e DBHOST=mysql -e MESSAGE="2nd Server" --network backend volumeandnetwork/testapp
759a323e36859b2c1964804d7a772fd417863f23d7ba2cdfbe068dfe8b5685db
PS D:\Study\DockStudyInVSCode> docker create --name productapp3 -e DBHOST=mysql -e MESSAGE="3rd Server" --network backend volumeandnetwork/testapp
25181974f1908634043fe174bb9af23b4420f50ea7f63e3dd148c4fba6f290e8

    使用单个docker create和docker run只能将一个自定义网络连到容器上。使用docker network connect可以将特定容器跟特定的自定义网络连接。现在将上述三个容器,跟frontend这个自定义网络连接起来。   

PS D:\Study\DockStudyInVSCode> docker network connect frontend productapp1
PS D:\Study\DockStudyInVSCode> docker network connect frontend productapp2
PS D:\Study\DockStudyInVSCode> docker network connect frontend productapp3

   然后运行起来:

PS D:\Study\DockStudyInVSCode> docker start productapp1 productapp2 productapp3
productapp1
productapp2
productapp3

添加负载均衡


    上面三个容器,我们将他们跟frontend和backend自定义网络关联起来了,因为没有定义端口映射,所以只能通过自定义网络访问,而无法通过在本机上访问。这里我们需要加一个负载均衡器,它接收本机上的一个端口来的HTTP请求,然后将请求通过frontend网络分发到三个ASP.NET Core MVC容器上来。

    有很多种负载均衡产品可以使用,我们这里使用HAProxy,他非常适合在Linux的Docker容器内安装和使用。要使用HAProxy,我们需要在项目下,新建一个名为haproxy.cfg的配置文件,内容如下:

defaults
    timeout connect 5000
    timeout client 50000
    timeout server 50000

frontend localnodes
    bind *:80
    mode http
    default_backend mvc

backend mvc
    mode http
    balance roundrobin
    service mvc1 productapp1:80
    service mvc2 productapp2:80
    service mvc3 productapp3:80

    改配置高速HAProxy,在80端口上接收HTTP请求,然后将该请求转发到三个MVC容器里,这里使用的是容器的名称来表示对应的IP信息,这也是自定义网络的特性。如果使用的是Visual Studio,这个文件保存的时候,需要注意选择正确的编码,否则HAProxy可能识别不了。我们这里用的是Visual Studio Code,所以不存在这个问题。

   要使用haproxy,首先要拉去haproxy,注意,上述配置文件格式,只可以在haproxy:1.7.0上使用,更高版本的可能会报错。使用命令如下:

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker pull haproxy:1.7.0

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker run -d --name loadbalancer --network frontend -v "$(pwd)/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg" -p 3000:80 haproxy:1.7.0
2cd98fbd814dfd4199a75b9c27e1e7f1a306ae2db2c684be93e4b3015f2dba1d

    这里一定要切换到VolumeAndNetwork文件夹下,-v参数表示映射(mount),pwd表示当前目录的意思,-v后面的一串参数,会将本机当前目录下的haproxy.cfg文件映射到haproxy容器里的“/usr/local/etc/haproxy/haproxy.cfg”上,-p表示将本机3000端口指向负载均衡容器的80端口。

    这里使用的是映射,跟创建volume是相似的方式,术语叫Bind Mounts,当然也可以自己先定义Dockerfile.haproxy,将本地的haproxy配置文件通过cp指令拷贝进去。

FROM haproxy:1.7.0
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

   然后,在执行一下命令:

PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker build . -t myhaproxy -f Dockerfile.haproxy
PS D:\Study\DockStudyInVSCode\VolumeAndNetwork> docker run -d --name loadbalancerv2 --network frontend  -p 4000:80 myhaproxy

    所有的容器如下:

   效果是一样的,现在在本地输入 http://localhost:3000/  或者 http://localhost:4000/ 然后点击刷新,可以看到标题会在“1st Server”、“2nd Server”和“3rd Server之间切换”。这就表示负载均衡起作用了,效果如下:

    现在,整个网络结构图如下:

   

    MySQL数据库,只跟backend网络连接。MVC应用程序既跟frontend连接,也跟backend连接。haproxy只和frontend连接,它接收从主机端口映射获取的HTTP请求,然后通过frontend网络发送到ASP.NET Core MVC容器里,然后容器通过backend连接访问MySQL数据库查询数据。

总结


    这篇文章介绍了docker里面的volume,它用来将应用程序中产生的数据或者用到的数据持久化存储下来,这样当应用程序所在的容器销毁时,存储在volume上的数据仍然能够保存。紧接着,了解了docker默认的网络,默认的bridge网络,会连接到各个容器里。我们也可以通过自定义网络来讲系统的不同部分使用不同的自定义网络连接起来。最后介绍了使用haproxy来实现ASP.NET Core MVC应用程序的负载均衡实现。

    下面列出这篇文章中所有的命令:

//判断是否使用了volume
docker inspect mysql
//创建volume
docker volume create --name productdata
//删除所有volume
docker volume rm $(docker volume ls -q)
//通过-v使用volume
docker run -d --name mysql -v productdata:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mysecret -e bind-address=0.0.0.0 mysql

//查看日志
docker logs -f mysql

//配置migration
dotnet ef migrations add Initial

//查看特定网络
docker network inspect bridge
//获取网络里的IP地址后,指定IP地址
 docker run -d --name productapp3001 -p 3001:80 -e DBHOST=172.17.0.2 volumeandnetwork/testapp
 
 //打开mysql交互
docker exec -it mysql /bin/bash
//mysql root账户登录
root@9d3e58a47dd1:/# mysql -uroot -p

//列出所有网络
docker network ls
//创建自定义网络
docker network create frontend
//删除所有network
docker network rm $(docker network ls -q)
//将容器连接到网络,这里可以直接通过--network使用自定义网络代替IP地址。
docker run -d --name mysql -v productdata:/var/lib/mysql --network=backend -e MYSQL_ROOT_PASSWORD=mysecret -e bind-address=0.0.0.0 mysql
//将已存在容器关联到自定义网络
docker network connect frontend productapp1
//docker运行多个容器
docker start productapp1 productapp2 productapp3

//使用负载均衡
docker pull haproxy:1.7.0
//运行负载均衡,并关联到网络,通过-v参数将当前文件夹下的haproxy.cfg配置文件映射到容器内
docker run -d --name loadbalancer --network frontend -v "$(pwd)/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg" -p 3000:80 haproxy:1.7.0

 

参考资料