docker-swarm 教程:服务管理篇

Swarm服务使用声明性模型,这意味着我们定义服务的所需状态,并依靠Docker来保持这种状态。包括以下信息(但不限于):

  • 服务容器应该运行的镜像名称和标签
  • 有多少docker参与服务
  • 是否有任何端口暴露在集群之外的客户端
  • 服务是否应该在Docker启动时自动启动
  • 重新启动服务时发生的具体行为(例如是否使用滚动重新启动)
  • 服务可以运行的节点的特征(例如资源约束和放置首选项)

创建服务

要创建无需额外配置的单副本服务,只需提供镜像名称。此命令以随机生成的名称启动Nginx服务,并且没有已发布的端口。但是这个服务无法与Nginx服务交互。

docker service create nginx

image-20230727141023177

我们看到 nginx 的服务已经被部署到了集群中的一个节点上。

创建的服务并不总是立即运行。如果服务的镜像不可用,如果没有节点满足您为服务配置的要求,或者出于其他原因,服务可以处于挂起状态。

要为您的服务提供名称,请使用--name标志:

 docker service create --name my_web nginx

就像独立容器一样,您可以通过在图像名称后添加来指定服务容器应运行的命令。此示例启动了一项名为helloworld的服务,该服务使用alpine图像并运行命令ping docker.com

 docker service create --name helloworld alpine ping docker.com

还可以指定要使用的服务的镜像标签。此示例修改了前一个,以使用alpine:3.6标签:

 docker service create --name helloworld alpine:3.6 ping docker.com

使用私有化镜像仓库创建服务

如果您的镜像在需要登录的私有化镜像仓库上可用,在登录后使用带有docker service create--with-registry-auth标志。如果您的镜像存储在registry.example.com,这是一个私有化镜像仓库,请使用以下命令:

 docker login registry.example.com
 docker service  create \
  --with-registry-auth \
  --name my_service \
  registry.example.com/acme/my_image:latest

这使用加密的WAL日志将登录令牌从本地客户端传递到部署服务的群节点。有了这些信息,节点可以登录注册表并拉取镜像。

更新服务

我们可以使用docker service update命令更改现有服务的几乎所有内容。当更新服务时,Docker会停止其容器,并使用新配置重新启动它们。

由于Nginx是一项网络服务,如果将端口80发布到群外客户端,它的效果会好得多。我们可以在创建服务时使用-p--publish标志来指定此信息。更新现有服务时,标志是--publish-add。还有一个--publish-rm标志来删除之前发布的端口。

假设上一节的web服务仍然存在,请使用以下命令将其更新为发布端口80。

 docker service update --publish-add 80 web

image-20230727143333211

删除一项服务

要删除服务,请使用docker service remove命令。可以按其ID或名称删除服务,如docker service ls命令的输出所示。以下命令删除了web服务。

 docker service remove web

配置运行时环境

我们可以为容器中的运行时环境配置以下选项:

  • 使用--env标志的环境变量
  • 使用--workdir标志的容器内的工作目录
  • 使用--user标志的用户名或UID

以下服务的容器有一个环境变量$MYVAR设置为myvalue,从/tmp/目录运行,并以my_user用户身份运行。

 docker service create --name helloworld \
  --env MYVAR=myvalue \
  --workdir /tmp \
  --user my_user \
  alpine ping docker.com

更新现有服务运行的命令

要更新现有服务运行的命令,您可以使用--args标志。以下示例更新了名为helloworld的现有服务,以便它运行ping docker.com命令,而不是之前运行的任何命令:

 docker service update --args "ping docker.com" helloworld

指定服务使用的镜像版本

当我们在没有指定要使用的图像版本的任何详细信息的情况下创建服务时,该服务将使用带有latest标签的版本。可以根据您想要的结果,以几种不同的方式强制服务使用特定版本的图像。

镜像版本可以用几种不同的方式表达:

  • 如果指定标签,管理器(或Docker客户端)会将该标签解析为摘要。当工作节点上收到创建容器任务的请求时,工作节点只看到摘要,看不到标签。

docker service create –name=“myservice” redis:6.0.20-alpine


一些标签表示离散版本,例如 `redis:6.0.20-alpine`。随着时间的推移,像这样的标签几乎总是会稳定地分解为摘要。建议您在可能的情况下使用这种标签。

其他类型的标签,如`latest``nightly`,可能会经常解析为新的摘要,这取决于镜像作者更新标签的频率。不建议使用经常更新的标签运行服务,以防止不同的服务副本任务使用不同的图像版本。

- 如果根本没有指定版本,按照惯例,图像`latest`标签将解析为摘要。docker 在创建服务任务时使用此摘要中的图像。

因此,以下两个命令是等价的:

```sh
 docker service create --name="myservice" redis
 docker service create --name="myservice" redis:latest
  • 如果直接指定摘要,则在创建服务任务时始终使用该图像的明确版本号。

     docker service create \
        --name="myservice" \
        redis:latest@sha256:35bc48a1ca97c3971611dc4662d08d131869daa692acb281c7e9e052924e38b1
    

当创建服务时,图像的标签将解析为服务创建时标签指向的特定摘要。该服务的工作节点永远使用该特定摘要,除非服务被明确更新。如果您确实使用经常更改的标签(如latest),此功能尤为重要,因为它确保所有服务任务使用相同版本的图像。

创建服务后,除非您明确使用--image标志执行docker service update其映像永远不会更新,如下所述。其他更新操作,如缩放服务、添加或删除网络或卷、重命名服务或任何其他类型的更新操作,都不会更新服务的图像。

创建后更新服务的图像

每个标签代表一个摘要,类似于Git散列。一些标签,如latest,经常更新以指向新的摘要。其他版本,如redis:6.0.20-alpine,代表已发布的软件版本,预计不会经常更新以指向新的摘要。当创建服务时,它必须使用特定的图像摘要创建任务,直到您使用--image标志使用service update来更新服务。

当使用--image标志运行service update时,群组管理器会查询Docker Hub或您的私有Docker仓库,以获取标签当前指向的摘要,并更新服务任务以使用该摘要。

通常,可以将标签解析为新的摘要和服务更新,重新部署每个任务以使用新镜像。如果管理服务无法解决标签或出现其他问题。

如果Manager解决了标签

如果集群管理器可以将镜像标签解析为摘要,它会指示工作节点重新部署任务并在该摘要中使用镜像。

  • 如果worker在该摘要中缓存了镜像,它将使用它。
  • 如果没有,它会尝试从Docker Hub或私有 docker hub 中提取镜像。
    • 如果成功,则使用新镜像部署任务。
    • 如果worker无法提取镜像,则服务无法在该worker节点上部署。Docker再次尝试部署任务,可能是在另一个工作节点上。

如果Manager无法解决标签

如果集群管理器无法将镜像解析为摘要,则不会丢失所有内容:

  • 管理器指示工作节点使用该标签上的镜像重新部署任务。
  • 如果worker有一个本地缓存的图像解析到该标签,它将使用该镜像。
  • 如果worker没有解析为标签的本地缓存图像,则工人尝试连接到Docker Hub或私人注册表,以在该标签处拉取镜像。
    • 如果成功,工人将使用该镜像。
    • 如果失败,任务部署失败,manager 再次尝试部署任务,可能是在另一个工作节点上。

发布端口

当创建集群服务时,可以通过两种方式将该服务的端口发布到群之外的主机:

  • 路由网格。当发布服务端口时,群会使服务在每个节点上的目标端口上都可以访问,无论服务是否在该节点上运行的任务。这样很简单,是许多类型服务的正确选择。
  • 可以直接在服务运行. 这绕过了路由网格,并提供了最大的灵活性,包括开发自己的路由框架的能力。但是,我们有责任跟踪每个任务的运行位置,并将请求路由到任务,并跨节点进行负载平衡。

使用路由网格发布服务的端口

要在集群外部发布服务的端口,请使用--publish <PUBLISHED-PORT>:<SERVICE-PORT>标志。集群使服务可以在每个群节点上的已发布端口访问。如果外部主机连接到任何群节点上的该端口,路由网格会将其路由到任务。外部主机不需要知道服务任务的IP地址或内部使用的端口即可与服务交互。当用户或进程连接到服务时,运行服务任务的任何工作节点都可以响应。

demo:在10节点群上运行三任务Nginx服务

比如我们你有一个10节点群,部署了一个Nginx服务,在10个节点群上运行三个任务:

 docker service create --name web \
                        --replicas 10 \
                        --publish published=8080,target=80 \
                        nginx

三个任务在最多三个节点上运行。不需要知道哪些节点正在运行任务;连接到10个节点中的任何一个节点上的端口8080将您连接到三个nginx任务之一。可以使用curl测试此。以下示例假设localhost是群节点之一。如果不是这样,或者localhost没有解析为主机上的IP地址,请替换主机的IP地址或可解析的主机名。

直接在集群节点上发布服务的端口

如果您需要根据应用程序状态做出路由决策,或者您需要完全控制将请求路由到服务任务的过程,那么使用路由网格可能不是应用程序的正确选择。要将服务的端口直接发布在运行的节点上,请使用mode=host选项到--publish标志。

注意:如果使用mode=host直接在群节点上发布服务的端口,并设置published=<PORT>,这会创建一个隐式限制,即只能在给定的群节点上为该服务运行一个任务。我们可以通过在没有端口定义的情况下指定published来解决这个问题,这导致Docker为每个任务分配一个随机端口。

此外,如果您使用mode=host,并且在docker service create不使用--mode=global标志,则很难知道哪些节点正在运行该服务来将工作路由到它们。

是一个开源反向代理、负载均衡器、HTTP缓存和Web服务器。如果您使用路由网格将nginx作为服务运行,则连接到任何群节点上的nginx端口都会向您显示(有效地)运行服务的随机群节点的网页。

以下示例在群中的每个节点上运行nginx作为服务,并在每个群节点上本地公开nginx端口。

 docker service create \
  --mode global \
  --publish mode=host,target=80,published=8080 \
  --name=nginx \
  nginx:latest

您可以在每个群节点的端口8080上访问nginx服务器。如果您将一个节点添加到群中,则会在上面启动一个nginx任务。您无法在任何绑定到端口8080的群节点上启动另一个服务或容器。

将服务连接到网络

可以使用overlay网络在集群中连接一个或多个服务。

首先,使用带有--driver overlay标志的docker network create命令在管理器节点上创建覆盖网络。

 docker network create --driver overlay my-network

在集群模式下创建覆盖网络后,所有管理器节点都可以访问网络。

您可以创建新服务并传递--network标志,以将服务附加到覆盖网络:

 docker service create \
  --replicas 3 \
  --network my-network \
  --name my-web \
  nginx

群将my-network扩展到运行服务的每个节点。

您还可以使用--network-add标志将现有服务连接到覆盖网络。

 docker service update --network-add my-network my-web

要断开正在运行的服务与网络的连接,请使用--network-rm标志。

 docker service update --network-rm my-network my-web

控制服务部署

Swarm服务提供了几种不同的方法来控制服务在不同节点上的规模和位置。

  • 可以指定服务是否需要运行特定数量的副本,还是应该在每个工作节点上全局运行。

  • 可以配置服务的CPU或内存要求,并且该服务仅在能够满足这些要求的节点上运行。

  • 部署允许我们将服务配置为仅在具有特定(任意)元数据集的节点上运行,如果不存在适当的节点,则导致部署失败。例如,可以指定服务只应在任意标签pci_compliant设置为true的节点上运行。

  • 部署选项允许将具有一系列值的任意标签应用于每个节点,并使用算法将服务的任务分散到这些节点。目前,唯一支持的算法是spread,它试图将它们均匀放置。例如,如果您用值为1-10的标签rack标记每个节点,然后指定在rack上键入的位置首选项,那么在考虑其他放置约束、放置首选项和其他特定于节点的限制后,尽可能均匀地在具有标签rack的所有节点上放置服务任务。

    与约束不同,放置首选项是最佳选择,如果没有节点可以满足首选项,服务不会无法部署。如果您为服务指定放置首选项,当群管理器决定哪些节点应运行服务任务时,与该首选项匹配的节点排名会更高。其他因素,如服务的高可用性,也考虑了哪些节点被安排运行服务任务。例如,如果您有N个带有机架标签的节点(然后是其他一些节点),并且您的服务被配置为运行N+1副本,则+1被安排在一个节点上,该节点上还没有服务,无论该节点是否有机rack标签。

replicatedglobal服务

Swarm 模式有两种类型的服务:replicated和global。对于replicated服务,指定集群管理器要安排到可用节点上的副本任务数量。对于global 服务,调度程序在每个满足服务可用节点上放置一个任务。

您可以使用--mode标志控制服务类型。如果没有指定模式,则服务默认为replicated。对于复制服务,您指定要使用--replicas标志开始的副本任务数量。例如,要启动具有3个副本任务的复制nginx服务:

 docker service create \
  --name my_web \
  --replicas 3 \
  nginx

要在每个可用节点上启动全局服务,请将--mode globaldocker service create。每当新节点可用时,调度程序都会在新节点上放置global服务的任务。例如,要启动在群中的每个节点上运行alpine的服务:

 docker service create \
  --name myservice \
  --mode global \
  alpine top

服务约束允许调度器将服务部署到节点之前为节点设置满足的标准。可以根据节点属性和元数据或引擎元数据对服务应用约束。

配置服务内存或CPU

要为服务保留给定的内存量或CPU数量,请使用--reserve-memory--reserve-cpu标志。如果没有可用的节点可以满足要求(例如,如果您请求4个CPU,并且群中没有节点有4个CPU),则服务将保持处于挂起状态,直到适当的节点可用于运行其任务。

内存不足异常(OOME)

如果您的服务尝试使用的内存超过群节点可用的内存,您可能会遇到内存不足异常(OOME),并且容器或Docker守护进程可能会被内核OOM杀手杀死。为了防止这种情况发生,请确保您的应用程序在具有足够内存的主机上运行。

集群服务允许使用资源约束、放置首选项和标签,以确保您的服务部署到适当的群节点。

使用constraint将服务部署到对应的节点上

使用放置约束来控制可以分配服务的节点。在以下示例中,服务仅在region设置为east的节点上运行。如果没有适当标记的节点可用,任务将在Pending中等待,直到它们可用。--constraint标志使用平等运算符(==!=)。对于replicated服务,所有服务都可能在同一节点上运行,或者每个节点只运行一个副本,或者一些节点不运行任何副本。对于global服务,该服务在满足放置约束和任何资源要求的每个节点上运行。

 docker service create \
  --name my-nginx \
  --replicas 5 \
  --constraint node.labels.region==east \
  nginx

您还可以在docker-compose.yml文件中使用constraint服务级别键。

如果您指定多个放置约束,则服务仅部署到满足所有约束的节点上。以下示例限制服务在region设置为easttype未设置为devel的所有节点上运行:

 docker service create \
  --name my-nginx \
  --mode global \
  --constraint node.labels.region==east \
  --constraint node.labels.type!=devel \
  nginx

您还可以将放置约束与放置首选项和CPU/内存约束结合使用。小心不要使用无法实现的设置。

使用datacenter部署服务

虽然constraint限制了服务可以运行的节点,但改首选项尝试以算法方式将任务放置在适当的节点上(目前仅均匀分布)。例如,如果为每个节点分配一个rack标签,以按值在具有rack标签的节点之间均匀地部署服务。这样如果部署服务宕机后改服务仍然在其他机器上的节点上运行。

然后datacenter没有严格执行。如果没有节点有在首选项中指定的标签,则部署服务就像没有设置首选项一样。

以下示例设置了根据datacenter标签的值在节点之间分散部署的首选项。如果一些节点有datacenter=us-east,而其他节点有datacenter=us-west,则服务将尽可能均匀地部署在两组节点上。

 docker service create \
  --replicas 9 \
  --name redis_2 \
  --placement-pref 'spread=node.labels.datacenter' \
  redis:3.0.6

可以指定多个放置首选项,并按照顺序处理它们。以下示例设置了具有多个datacenter的服务。任务首先分布在各个数据中心,然后分布在机器上 (如各自的标签所示):

 docker service create \
  --replicas 9 \
  --name redis_2 \
  --placement-pref 'spread=node.labels.datacenter' \
  --placement-pref 'spread=node.labels.rack' \
  redis:3.0.6

您还可以将放置首选项与放置约束或CPU/内存约束结合使用。

这张图表说明了放置偏好是如何工作的:

安置偏好如何运作

使用docker service update更新服务时,--placement-pref-add在所有现有放置首选项后附加一个新的放置首选项。--placement-pref-rm删除与参数匹配的现有放置首选项。

配置服务的更新行为

当创建服务时,可以指定滚动更新行为,以便在运行docker service update时,群应如何对服务应用更改。您还可以将这些标志指定为更新的一部分,作为docker service update的参数。

--update-delay标志配置服务任务或任务集更新之间的时间延迟。您可以将时间T描述为秒Ts、分钟Tm或小时Th的组合。所以10m30s表示延迟10分30秒。

默认情况下,调度程序一次更新1个任务。您可以传递--update-parallelism标志来配置调度器同时更新的最大服务任务数量。

当单个任务的更新返回RUNNING状态时,调度程序通过继续执行另一个任务来继续更新,直到所有任务都更新。如果在更新期间的任何时候任务返回FAILED,调度程序将暂停更新。您可以使用docker service createdocker service update--update-failure-action标志来控制行为。

在下面的示例服务中,调度器一次最多对2个副本应用更新。当更新的任务返回RUNNINGFAILED,调度程序等待10秒,然后停止下一个任务进行更新:

 docker service create \
  --replicas 10 \
  --name my_web \
  --update-delay 10s \
  --update-parallelism 2 \
  --update-failure-action continue \
  alpine

--update-max-failure-ratio标志控制在更新期间,在更新整体被视为失败之前,有多少任务可能会失败。例如,使用--update-max-failure-ratio 0.1 --update-failure-action pause,在10%的更新任务失败后,更新暂停。

如果任务没有启动,或者如果在--update-monitor标志指定的监控期内停止运行,则单个任务更新被视为失败。--update-monitor的默认值为30秒,这意味着任务在启动后前30秒内失败计入服务更新失败阈值,之后的失败不计算在内。

回滚到以前的服务版本

如果服务的更新版本无法按预期运行,可以使用docker service update --rollback标志手动回滚到服务的上一个版本。这将使服务恢复到最新的docker service update命令之前到位的配置。

其他选项可以与--rollback相结合;例如,--update-delay 0s,在任务之间毫不延迟地执行回滚:

 docker service update \
  --rollback \
  --update-delay 0s

可以将服务配置为在服务更新部署失败时自动回滚。

手动回滚在服务器端处理,这允许手动启动的执行回滚参数。--rollback不能与其他标志结合使用,以docker service update

如果更新失败,自动回滚

可以配置服务,如果服务更新导致重新部署失败,该服务可以自动回滚到以前的配置。这有助于保护服务的可用性。可以在服务创建或更新时设置以下一个或多个标志。如您没有设置值,则使用默认值。

旗帜 默认 描述
--rollback-delay 0s 回滚任务后等待的时间,然后再回滚下一个任务。值为0意味着在第一个回滚任务部署后立即回滚第二个任务。
--rollback-failure-action pause 当任务无法回滚时,是pause还是continue尝试回滚其他任务。
--rollback-max-failure-ratio 0 回滚期间容忍的失败率,指定为0和1之间的浮点数。例如,给定5个任务,失败率为.2可以容忍一个任务无法回滚。值为0表示不容忍故障,而值为1表示容忍任何数量的故障。
--rollback-monitor 5s 每个任务回滚后的持续时间,以监控失败。如果任务在此时间段过去之前停止,则回滚被视为失败。
--rollback-parallelism 1 并行回滚的最大任务数。默认情况下,一次回滚一个任务。值为0会导致所有任务并行回滚。

以下示例将redis服务配置为在docker service update无法部署时自动回滚。两个任务可以并行回滚。任务在回滚后被监控20秒,以确保它们不会退出,并且最大失败率为20%。默认值用于--rollback-delay--rollback-failure-action

 docker service create --name=my_redis \
                        --replicas=5 \
                        --rollback-parallelism=2 \
                        --rollback-monitor=20s \
                        --rollback-max-failure-ratio=.2 \
                        redis:latest

授予服务对卷或绑定挂载的访问权限

为了获得最佳性能和可移植性,应该避免将重要数据直接写入容器的可写层。相反,应该使用数据卷或绑定挂载。这一原则也适用于服务。

我们可以为集群中服务创建两种类型的挂载、volume挂载或bind挂载。无论使用哪种类型的挂载,请在创建服务时使用--mount标志进行配置,或在更新现有服务时使用--mount-add--mount-rm标志进行配置。如果您没有指定类型,则默认为数据卷。

数据卷

数据卷是独立于容器存在的存储。集群服务下数据卷的生命周期与容器下的生命周期相似。卷比任务和服务更久,因此必须单独管理其删除。卷可以在部署服务之前创建,或者如果当任务安排在特定主机上不存在,则根据服务的卷规范自动创建卷。

要将现有数据卷与服务一起使用,请使用--mount标志:

 docker service create \
  --mount src=<VOLUME-NAME>,dst=<CONTAINER-PATH> \
  --name myservice \
  <IMAGE>

如果当任务被安排到特定主机时,名为<VOLUME-NAME>的卷不存在,则会创建一个。默认卷驱动程序是local。要使用此按需创建模式使用不同的卷驱动程序,请使用--mount标志指定驱动程序及其选项:

 docker service create \
  --mount type=volume,src=<VOLUME-NAME>,dst=<CONTAINER-PATH>,volume-driver=<DRIVER>,volume-opt=<KEY0>=<VALUE0>,volume-opt=<KEY1>=<VALUE1>

挂载路径

绑定挂载是来自调度器为任务部署容器的主机的文件系统路径。Docker将路径安装到容器中。在集群初始化任务的容器之前,文件系统路径必须存在。

以下示例显示了绑定挂载语法:

  • 挂载读写绑定:

     docker service create \
      --mount type=bind,src=<HOST-PATH>,dst=<CONTAINER-PATH> \
      --name myservice \
      <IMAGE>
    
  • 挂载只读绑定:

     docker service create \
      --mount type=bind,src=<HOST-PATH>,dst=<CONTAINER-PATH>,readonly \
      --name myservice \
      <IMAGE>
    

注意:

  • 如果将主机路径绑定到服务的容器中,则该路径必须存在于每个群节点上。Docker群模式调度器可以在任何满足资源可用性要求并满足您指定的所有约束和放置首选项的机器上调度容器。
  • 如果正在运行的服务容器变得不健康或无法访问,Docker swarm 模式调度器可能会随时重新安排它们。
  • 主机绑定挂载是不便携的。当使用绑定挂载时,不能保证您的应用程序在开发中运行的方式与在生产中相同。

使用模板创建服务

您可以使用Golang template 包提供的语法,将模板用于某些service create标志。

支持以下标志:

  • --hostname
  • --mount
  • --env

Go模板的有效占位符是:

占位符 描述
.Service.ID 服务ID
.Service.Name 服务名称
.Service.Labels 服务标签
.Node.ID 节点ID
.Node.Hostname 节点主机名
.Task.Name 任务名称
.Task.Slot 任务插槽

模板示例

此示例根据服务的名称和容器运行的节点的ID设置创建容器的模板:

 docker service create --name hosttempl \
                        --hostname="{{.Node.ID}}-{{.Service.Name}}"\
                         busybox top

要查看使用模板的结果,请使用docker service psdocker inspect命令。

 docker service ps va8ew30grofhjoychbr6iot8c
 docker inspect --format="{{.Config.Hostname}}" hosttempl.1.wo41w8hg8qanxwjwsg4kxpprj

类似的帖子