从头开始使用 Go 构建 Orchestrator(第 八部分:Manager API)

本章内容涵盖

  1. 理解管理器 API 的目的

  2. 实现处理 API 请求的方法

  3. 创建一个服务器来监听 API 请求

  4. 通过 API 启动、停止和列出任务

在第 7 章中,我们实现了管理器组件的核心功能:从其队列中取出任务,选择一个工作节点来运行这些任务,将任务发送给选定的工作节点,并定期更新任务的状态。该功能仅仅是一个基础,并没有为用户提供与管理器进行交互的简便方式。

因此,就像我们在第 5 章中对工作节点所做的那样,我们将为管理器构建一个 API。这个 API 封装了管理器的核心功能,并将其暴露给用户。对于管理器来说,用户指的是终端用户,也就是那些希望在我们的编排系统中运行其应用程序的开发人员。

管理器的 API 和工作节点的 API 一样,将会很简单。它将为用户提供执行以下基本操作的方法:

  1. 向管理器发送一个任务

  2. 获取任务列表

  3. 停止一个任务

这个 API 将使用与工作节点 API 相同的组件来构建。它将由处理程序、路由和多路复用器组成。

8.1 管理器 API 概述

在深入研究代码之前,让我们先退一步,从更全面的角度来看一下我们的目标。在过去的几章中,我们一直非常专注于细节,所以回顾一下这些技术细节是如何组合在一起的会很有帮助。我们构建管理器和工作节点并不是无缘无故的。构建它们的目的是为了满足一种需求:开发人员需要一种可靠且具有弹性的方式来运行他们的应用程序。管理器和工作节点是一种抽象,使开发人员无需深入思考其应用程序运行的底层基础设施(无论是物理的还是虚拟的)。图 8.1 让我们想起了这种抽象的样子。

图 8.1 显示,管理器由一个 API 服务器和管理器组件构成,同样地,工作节点由一个 API 服务器和工作节点组件构成。用户与管理器进行通信,而管理器则与一个或多个工作节点进行通信。

有了这样的提示,让我们再回到构建管理器 API 的细节上来。由于它与工作节点的 API 类似,我们不会花太多时间深入探讨处理程序、路由和多路复用器的细节。如果你需要复习相关内容,请参考 5.1 节。

8.2 路由

我们先来确定管理器 API 应该处理的路由。管理器 API 的路由与工作节点 API 的路由相同,这并不奇怪。在某种程度上,我们的管理器就像是一个反向代理:它不是像反向代理那样在多个 Web 服务器之间平衡对网页的请求,而是在多个工作节点之间平衡运行任务的请求。

因此,和工作节点的 API 一样,管理器的 API 将处理对 /tasks 的 GET 请求,该请求将返回系统中所有任务的列表(见表 8.1)。这使用户能够查看系统中当前存在哪些任务。它将处理对 /tasks 的 POST 请求,该请求将在一个工作节点上启动一个任务。这使用户能够在系统中运行他们的任务。最后,它将处理对 /tasks/{taskID} 的 DELETE 请求,该请求将停止由路由中 taskID 指定的任务。这使用户能够停止他们的任务。

表 8.1 我们的管理器 API 使用的路由

8.3 数据格式、请求与响应

如果我们为管理器 API 所使用的路由与工作节点 API 的路由类似,那么管理器将接收和返回的数据格式、请求以及响应又是怎样的呢?同样地,管理器 API 将使用与工作节点 API 相同的数据格式(JSON)也就不足为奇了。如果工作节点 API 使用 JSON 格式进行数据交互,那么管理器 API 也使用相同的格式,这样可以最大程度地减少数据格式之间不必要的转换。因此,发送给管理器 API 的任何数据都必须进行 JSON 编码,并且 API 返回的任何数据也将被编码为 JSON 格式。表 8.2 展示了一张更新后的路由表。

表 8.2 经过更新的路由表,显示了路由是否发送请求正文、是否返回响应正文以及请求成功的状态代码

我们可以从图 8.2 中看到这些路由是如何使用的。图中展示了一个用于创建新任务的 POST 请求以及一个用于获取任务列表的 GET 请求。开发人员向管理器发出请求,管理器则返回响应。在第一个示例中,开发人员发出一个 POST 请求,请求体中指定了要运行的任务。管理器以状态码 201 进行响应。在第二个示例中,开发人员发出一个 GET 请求,管理器以状态码 200 并附带任务列表进行响应。

图 8.2 开发人员使用管理器 API 的两个示例

8.4 API 结构体

进一步深入探究,我们注意到管理器 API 与工作节点 API 之间的另一个相似之处。管理器的 API 同样使用了一个 Api 结构体,该结构体将封装其 API 所需的行为。唯一的区别在于替换一个字段:Worker 字段被 Manager 字段所取代,Manager 字段包含一个指向我们管理器实例的指针。除此之外,Address(地址)、Port(端口)和 Router(路由器)字段的类型与它们在工作节点 API 中是相同的,并且发挥着相同的作用。

code 8.1 The manager's Api struct

go 复制代码
type Api struct {
    Address string
    Port int
    Manager *Manager
    Router *chi.Mux
}

8.5 处理请求

继续深入探讨,我们来谈谈管理器 API 的处理程序。这些处理程序应该看起来很熟悉,因为它们与我们为工作节点实现的三个处理程序是相同的:

  1. StartTaskHandler(w http.ResponseWriter, r *http.Request)

  2. GetTasksHandler(w http.ResponseWriter, r *http.Request)

  3. StopTaskHandler(w http.ResponseWriter, r *http.Request)

我们将在一个名为 handlers.go 的文件中实现这些处理程序,你应该在 manager 目录下创建这个文件,与现有的 manager.go 文件放在一起。为了减少不必要的输入量,你可以自由地从工作节点的 API 中复制这些处理程序,并将它们粘贴到管理器的 handlers.go 文件中。我们只需要做的修改是更新对 a.Workera.Manager 的任何引用。让我们从 StartTaskHandler 开始,它的工作方式与其在工作节点中的对应处理程序相同。它期望请求体是经过 JSON 编码的。它将请求体解码为 task.TaskEvent,并检查是否存在任何解码错误。然后,它使用我们在第 7 章中实现的管理器的 AddTask 方法,将任务事件添加到管理器的队列中。实现方式可以在下面的清单中看到。

code 8.2 The manager's StartTaskHandler

接下来是 GetTasksHandler。和 StartTaskHandler 一样,GetTasksHandler 的工作方式与工作节点 API 中对应的处理程序类似。

不过,我们确实需要实现一个辅助方法,以便能轻松地从管理器获取任务列表。我们在 Manager 结构体上创建了 GetTasks() 辅助方法。GetTasks() 方法很直接明了:

  1. 实例化一个名为 tasks 的变量,它是一个指向 task.Task 类型的指针切片。
  2. 遍历管理器的 TaskDb 字段,并将每个任务添加到 tasks 切片中。
  3. 返回任务切片。

code 8.3 The GetTasks helper function returning a slice of pointers to task.Task type

go 复制代码
func (m *Manager) GetTasks() []*task.Task {
    tasks := []*task.Task{}
    for _, t := range m.TaskDb {
        tasks = append(tasks, t)
    }
    return tasks
}

写完 GetTasks() 函数后,我们就可以在 GetTasksHandler 方法中使用它了。与 Worker 的实现相比,我们需要做的唯一改动就是将管理器的 GetTasks() 函数传递给编码器。

code 8.4 The manager's GetTaskHandler

go 复制代码
func (a *Api) GetTasksHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(200)
	json.NewEncoder(w).Encode(a.Manager.GetTasks())
}

最后,让我们来实现 StopTaskHandler。同样,这个方法的工作方式与工作节点中对应的方法相同,所以在这方面没有太多新的内容需要讨论。

code 8.5 The manager's StopTaskHandler

go 复制代码
func (a *Api) StopTaskHandler(w http.ResponseWriter, r *http.Request) {
    taskID := chi.URLParam(r, "taskID")
    if taskID == "" {
        log.Printf("No taskID passed in request.\n")
        w.WriteHeader(400)
    }
    
    tID, _ := uuid.Parse(taskID)
    taskToStop, ok := a.Manager.TaskDb[tID]
    if !ok {
        log.Printf("No task with id %v found", tID)
        w.WriteHeader(404)
    }
    
    te := task.TaskEvent{
        ID: uuid.New(),
        State: task.Completed,
        Timestamp: time.Now(),
    }
    
    taskCopy := *taskToStop
    taskCopy.State = task.Completed
    te.Task = taskCopy
    a.Manager.AddTask(te)
    
    log.Printf("Added task event %v to stop task %v\n", te.ID, taskToStop.ID)
    w.WriteHeader(204)
}

需要提醒的是,就像我们在工作节点中所做的那样,管理器的处理程序方法并不会直接对任务进行操作。我们已经将响应 API 请求的职责与启动和停止任务的操作分离开来。所以,API 只是通过 AddTask 方法将请求放入管理器的队列中,然后管理器从队列中取出任务并执行必要的操作。

到目前为止,我们通过复制粘贴工作节点 API 中的处理程序并做一些小的调整,成功实现了管理器 API 中的处理程序。在这一步,我们已经实现了 API 的核心部分。

8.6 提供 API 服务

既然我们已经实现了管理器的处理程序,那就让我们完成剩下的工作,以便能够向用户提供 API 服务。我们首先从工作节点中复制粘贴 initRouter 方法。这个方法会设置我们的路由器并创建所需的端点,由于这些端点将与工作节点的端点相同,所以我们不需要对其进行任何修改。

code 8.6 The manager API's initRouter method

go 复制代码
func (a *Api) initRouter() {
	a.Router = chi.NewRouter()
	a.Router.Route("/tasks", func(r chi.Router) {
		r.Post("/", a.StartTaskHandler)
		r.Get("/", a.GetTasksHandler)
		r.Route("/{taskID}", func(r chi.Router) {
			r.Delete("/", a.StopTaskHandler)
		})
	})
}

为了锦上添花,让我们复制 Worker API 中的 Start 方法来启动 API。管理器的 API 将以同样的方式启动,因此与 initRouter 方法一样,我们不需要做任何更改。

code 8.7 The manager API's Start method

go 复制代码
func (a *Api) Start() {
    a.initRouter()
    http.ListenAndServe(fmt.Sprintf("%s:%d", a.Address, a.Port), a.Router)
}

8.7 进行一些重构以使工作更轻松

到目前为止,我们已经实现了管理器功能正常的 API 所需的所有内容。但既然我们已经走到了这一步,即拥有了工作节点和管理器的 API,那就让我们进行一些小的调整,以便更轻松地运行这些 API。

如果你还记得第 5 章的内容,我们在 main.go 文件中创建了 runTasks 函数。我们用它来持续检查工作节点需要运行的新任务。如果在工作节点的队列中发现任何任务,它就会调用工作节点的 RunTask 方法。

与其让这个函数作为 main.go 文件的一部分,不如把它移到工作节点自身的代码中。这样的更改将把所有必要的工作节点行为封装在 Worker 对象中。从 main.go 文件中复制 runTasks 函数,并将其粘贴到 worker.go 文件中。然后,为了整理所有内容以便代码能够运行,我们将进行三处更改:

  1. 将现有的 RunTask 方法重命名为 runTask

  2. main.go 文件中的 runTasks 函数重命名为 RunTasks

  3. 修改 RunTasks 方法,使其调用新重命名的 runTask 方法。

你可以在下面的清单中看到这些更改。

8.8 Moving the runTasks function from main.go to the worker

go 复制代码
func (w *Worker) runTask() task.DockerResult{}

func (w *Worker) RunTasks() {
    for {
        if w.Queue.Len() != 0 {
            result := w.runTask()
            if result.Error != nil {
                log.Printf("Error running task: %v\n", result.Error)
            }
        } else {
            log.Printf("No tasks to process currently.\n")
        }
        log.Println("Sleeping for 10 seconds.")
        time.Sleep(10 * time.Second)
    }
}

以类似的方式,我们将对 Manager 结构体进行一些更改,这将使我们在 main.go 文件中使用管理器变得更加容易。首先,让我们将管理器的 UpdateTasks 方法重命名为 updateTasks 。该方法应该如下所示(方法的主体保持不变):

go 复制代码
func (m *Manager) updateTask() {}

接下来,让我们在 Manager 结构体上创建一个名为 UpdateTasks 的新方法。这个方法的作用与我们添加到工作节点的 RunTasks 方法类似。它运行一个无限循环,在循环内部调用管理器的 updateTasks 方法。这样的改动使我们能够移除在第 7 章 main.go 文件中使用的执行相同功能的匿名函数。

code 8.9 Adding the UpdateTasks method to the manager

go 复制代码
func (m *Manager) ProcessTasks() {
    for {
        log.Println("Processing any tasks in the queue")
        m.SendWork()
        log.Println("Sleeping for 10 second")
        time.Sleep(10 * time.Second)
    }
}

最后,让我们在管理器中添加下一个列表中的 ProcessTasks 方法。该方法的工作原理与 Worker 的 RunTasks 方法类似:它运行一个无休止的循环,反复调用管理器的 SendWork 方法。

code 复制代码
```go
func (m *Manager) ProcessTasks()

8.8 整合所有内容

好了,又到了这个时候 ------ 是时候运行我们在本章中构建的内容了。不过,在这之前,让我们快速总结一下到目前为止我们所构建的内容:

  1. 我们为管理器组件封装了一个 API,使用户能够与管理器进行通信。

  2. 我们使用与工作节点 API 相同类型的组件(处理程序、路由和路由器)来构建管理器的 API。

  3. 路由器监听对路由的请求,并将这些请求分派给相应的处理程序。

让我们先从第 7 章的 main.go 文件中复制粘贴 main 函数。这将是我们的起点。本章结束时的情况与上一章有一个主要区别:我们现在同时拥有工作节点和管理器的 API。所以,虽然我们会创建它们各自的实例,但不会像过去那样直接与它们进行交互。相反,我们会将这些实例传递给它们各自的 API,然后启动这些 API,使其监听 HTTP 请求。

在之前启动工作节点 API 实例的章节中,我们设置了两个环境变量:CUBE_HOSTCUBE_PORT。它们用于将工作节点 API 设置为监听 http://localhost:5555 的请求。然而,现在我们需要启动两个 API。为了应对这种新情况,让我们设置 main 函数,以便为每个 API 提取一组主机:端口的环境变量。正如你在下面的代码清单中看到的,这些环境变量分别是 CUBE_WORKER_HOSTCUBE_WORKER_PORTCUBE_MANAGER_HOSTCUBE_MANAGER_PORT

code 8.11 Extracting the host and port for each API from environment variables

go 复制代码
func main() {
    whost := os.Getenv("CUBE_WORKER_HOST") 
    wport, _ := strconv.Atoi(os.Getenv("CUBE_WORKER_PORT"))
    
    mhost := os.Getenv("CUBE_MANAGER_HOST") 
    mport, _ := strconv.Atoi(os.Getenv("CUBE_MANAGER_PORT"))
}

接下来,在从环境变量中提取主机和端口值并将它们存储在命名恰当的变量中之后,让我们启动工作节点 API。这个过程应该和第 7 章的内容很相似。不过,这里唯一的区别在于,我们是在工作节点对象上调用 RunTasks 方法,而不是调用之前在 main.go 里定义的单独的 runTasks 函数。和第 7 章一样,我们使用 go 关键字来调用这些方法,这样每个方法都会在单独的协程中运行。

code 8.12 Starting up the worker API

go 复制代码
    fmt.Println("Starting Cube Worker")
    w := worker.Worker{
        Queue: *queue.New(),
        Db: make(map[uuid.UUID]*task.Task),
    }
    
    wapi := worker.Api{Address: whost, Port: wport, Worker: &w}
    
    go w.RunTasks()
    go w.CollectStats()
    go wapi.Start()

最后,我们将启动管理器 API。这个过程的起始步骤与上一章相同。我们创建一个工作节点列表,其中包含之前创建的单个工作节点,该工作节点以 <主机>:<端口> 的字符串形式表示。然后,我们创建一个管理器实例,并传入这个工作节点列表。接下来,我们创建一个管理器 API 的实例,并将其存储在一个名为 mapi 的变量中。

在我们的 main 函数中,接下来的两行代码设置了两个将与运行 API 的主协程并行运行的协程。第一个协程将运行管理器的 ProcessTasks 方法。这确保了管理器会处理来自用户的任何传入任务。第二个协程将运行管理器的 UpdateTasks 方法。它将通过查询工作节点的 API 来获取每个任务的最新状态,从而确保管理器更新任务的状态。

然后就到了我们一直期待的部分。我们通过调用 Start 方法来启动管理器的 API。

code 8.13 Starting up the manager API

go 复制代码
    fmt.Println("Starting Cube manager")
    workers := []string{fmt.Sprintf("%s:%d", whost, wpost)}
    m := manager.New(workers)
    mapi := manager.Api{Address: mhost, Port: mport, Manager: m}
    
    go m.ProcessTasks()
    go m.UpdateTasks()
    
    mapi.Start()

至此,剩下要做的就是运行我们的主程序,这样管理器 API 和工作节点 API 就都能运行起来了。如你所见,工作节点和管理器都会启动:

go 复制代码
$ CUBE_WORKER_HOST=localhost CUBE_WORKER_PORT=5555 CUBE_MANAGER_HOST=localhost  CUBE_MANAGER_PORT=5556 go run main.go

Starting Cube Worker
Starting Cube Manager
2025/03/13 14:05:15 No tasks in the queue
2025/03/13 14:05:15 Sleeping for 10 second
2025/03/13 14:05:15 Processing any tasks in the queue
2025/03/13 14:05:15 No work in the queue
2025/03/13 14:05:15 Sleeping for 10 seconds
2025/03/13 14:05:15 Collecting stats
2025/03/13 14:05:15 Checking for task updates from workers
2025/03/13 14:05:15 Checking worker localhost:5555 for task updates
2025/03/13 14:05:15 No tasks in the database
2025/03/13 14:05:15 Task update completed
2025/03/13 14:05:15 Sleeping for 15 seconds

让我们做一个快速的合理性检查,以验证我们的管理器确实会响应请求。让我们发出一个 GET 请求,获取它所知道的任务列表。它应该返回一个空列表:

markdown 复制代码
$ curl -v 127.0.0.1:5556/tasks
*   Trying 127.0.0.1:5556...
* Connected to 127.0.0.1 (127.0.0.1) port 5556
> GET /tasks HTTP/1.1
> Host: 127.0.0.1:5556
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Thu, 13 Mar 2025 03:22:22 GMT
< Content-Length: 3
< 
[]
* Connection #0 to host 127.0.0.1 left intact

太酷了!我们的管理器正如预期的那样在监听请求。不过,正如我们所见,它目前没有任何任务,因为我们还没有让它运行任何任务。让我们迈出下一步,向管理器发送一个请求,指示它为我们启动一个任务。

为此,我们在 main.go 文件所在的同一个目录中创建一个名为 task.json 的文件。在这个文件中,我们创建一个任务的 JSON 表示形式,如下一个代码清单所示。这种表示形式与我们在第 7 章的 main.go 中使用的类似,只是我们把它移到了一个单独的文件中。

json 复制代码
{
    "ID": "6be4cb6b-61d1-40cb-bc7b-9cacefefa60c", 
    "State": 2, 
    "Task": { 
        "State": 1, 
        "ID": "21b23589-5d2d-4731-b5c9-a97e9832d021", 
        "Name": "test-chapter-5", 
        "Image": "docker.io/strm/helloworld-http"
    }
}

既然我们已经创建了包含要通过 API 发送给管理器的任务的 task.json 文件,那就用 curl 向管理器 API 的 tasks 端点发送 POST 请求吧。不出所料,管理器的 API 会以 201 响应代码做出响应:

ruby 复制代码
$ curl -v --request POST --header 'Content-Type: application/json' --data @task.json http://localhost:5556/tasks     
......
> 
* upload completely sent off: 244 bytes
< HTTP/1.1 201 Created
< Date: Thu, 13 Mar 2025 06:05:21 GMT
< Content-Length: 347
....
< 
{
    "ID":"21b23589-5d2d-4731-b5c9-a97e9832d021",
    "ContainerID":"",
    "Name":"test-chapter-5",
    "State":1,
    "CPU":0,
    "Memory":0,
    "Disk":0,
    "Image":"docker.io/strm/helloworld-http",
    "RestartPolicy":"","ExposedPorts":null,"HostPorts":null,"PortBindings":null,"StartTime":"0001-01-01T00:00:00Z","FinishTime":"0001-01-01T00:00:00Z","HealthCheck":"","RestartCount":0
}
* Connection #0 to host localhost left intact

关于管理器 API 返回的 JSON 数据,有一点需要注意。注意到 ContainerID 字段是空的。原因在于,和工作节点的 API 一样,管理器的 API 并不直接操作任务。当任务进入 API 时,它们会被添加到管理器的队列中,管理器会独立于该请求来处理这些任务。在我们发出请求时,管理器还没有将任务发送给工作节点,所以它无法知道 ContainerID 会是什么。如果我们随后向管理器的 API 发送一个 GET /tasks 请求,应该就能看到我们任务的 ContainerID 了:

json 复制代码
$ curl -v 127.0.0.1:5556/tasks|jq
...
[
  {
    "ID": "21b23589-5d2d-4731-b5c9-a97e9832d021",
    "ContainerID": "c35f0fee4871efe7bbf78c18192692173a586b8970b5048dff84d21da4e60fa1",
    "Name": "test-chapter-5",
    "State": 2,
    "CPU": 0,
    "Memory": 0,
    "Disk": 0,
    "Image": "docker.io/strm/helloworld-http",
    "RestartPolicy": "",
    "ExposedPorts": null,
    "HostPorts": null,
    "PortBindings": null,
    "StartTime": "2025-03-13T06:05:35.084065Z",
    "FinishTime": "0001-01-01T00:00:00Z",
    "HealthCheck": "",
    "RestartCount": 0
  }
]

当像我们之前那样查询管理器的 API 时,有一个小细节需要记住。这取决于我们在发送初始的 POST /tasks 请求之后,以多快的速度发出 GET /tasks 请求,我们可能仍然看不到 ContainerID。这是为什么呢?如果你还记得的话,管理器是通过向工作节点的 API 发送 GET /tasks 请求来更新它对任务的视图的。然后,它会使用该请求的响应来更新其自身数据存储中任务的状态。如果你回顾一下清单 8.13,你会看到我们的 main.go 程序在一个单独的协程中运行管理器的 UpdateTasks 方法,并且该方法在每次尝试更新任务之间会休眠 15 秒。

一旦管理器显示任务正在运行,也就是说,我们在 GET /tasks 的响应中得到了一个 ContainerID,我们就可以使用 docker ps 命令进一步验证该任务是否正在运行:

bash 复制代码
$ docker ps 
CONTAINER ID   IMAGE                  COMMAND      CREATED         STATUS         PORTS                   NAMES
c35f0fee4871   strm/helloworld-http   "/main.sh"   3 seconds ago   Up 2 seconds   0.0.0.0:55006->80/tcp   test-chapter-5

既然我们已经知道可以使用管理器的 API 来启动任务和获取任务列表,那么我们就用它来停止正在运行的任务。为此,我们发出一个 DELETE tasks/{taskID} 请求,如下所示:

markdown 复制代码
$ curl -v --request DELETE "http://localhost:5556/tasks/21b23589-5d2d-4731-b5c9-a97e9832d021"                        
* Host localhost:5556 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:5556...
* connect to ::1 port 5556 from ::1 port 58918 failed: Connection refused
*   Trying 127.0.0.1:5556...
* Connected to localhost (127.0.0.1) port 5556
> DELETE /tasks/21b23589-5d2d-4731-b5c9-a97e9832d021 HTTP/1.1
> Host: localhost:5556
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 204 No Content
< Date: Thu, 13 Mar 2025 06:06:02 GMT
< 
* Connection #0 to host localhost left intact

正如我们所看到的,管理器的 API 接受了我们的请求,并如期回复了 204。您还可以在我们的 main.go 程序的以下输出中看到它(为了便于阅读,我截断了部分输出):

yaml 复制代码
2025/03/13 14:06:02 Added task event bfaa54c5-4f00-4384-927b-2f4e2656f333 to stop task 21b23589-5d2d-4731-b5c9-a97e9832d021
2025/03/13 14:06:05 Processing any tasks in the queue
2025/03/13 14:06:05 Added task {21b23589-5d2d-4731-b5c9-a97e9832d021 c35f0fee4871efe7bbf78c18192692173a586b8970b5048dff84d21da4e60fa1 test-chapter-5 3 0 0 0 docker.io/strm/helloworld-http  map[] map[] map[] 2025-03-13 06:05:35.084065 +0000 UTC 0001-01-01 00:00:00 +0000 UTC  0}
2025/03/13 14:06:10 Attempting to stop container c35f0fee4871efe7bbf78c18192692173a586b8970b5048dff84d21da4e60fa1
2025/03/13 14:06:15 Sleeping for 15 seconds
2025/03/13 14:06:15 Stopped and removed container c35f0fee4871efe7bbf78c18192692173a586b8970b5048dff84d21da4e60fa1 for task 21b23589-5d2d-4731-b5c9-a97e9832d021

同样,我们可以使用 docker ps 命令来确认我们的管理器是否完成了我们期望它做的事情,在本例中,就是停止任务:

ruby 复制代码
$ docker ps                                                                                  
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

总结

和工作节点的 API 一样,管理器将其核心功能进行了封装,并以 HTTP 服务器的形式将其公开。与工作节点的 API 不同,工作节点 API 的主要使用者是管理器,而管理器 API 的主要使用者则是终端用户,换句话说,就是开发人员。因此,用户通过与管理器的 API 进行交互,从而在编排系统上运行他们的任务。管理器和工作节点的 API 为我们的物理或虚拟基础设施提供了一种抽象,使得开发人员无需关注诸如底层细节之类的问题。开发人员无需考虑他们的应用程序如何在机器上运行,而只需关注他们的应用程序如何在容器中运行。如果应用程序在他们机器上的容器中按预期运行,那么它也可以在任何同样运行着相同容器框架(即 Docker)的机器上运行。

和工作节点的 API 一样,管理器的 API 也是一个基于 REST 的简单 API。它定义了一些路由,使用户能够创建、查询和停止任务。此外,当接收数据时,它要求数据必须以 JSON 格式进行编码,同样地,它发送的任何数据也都将编码为 JSON 格式。

相关推荐
Super_man541881 小时前
k8s之service解释以及定义
java·开发语言·云原生·容器·kubernetes
hwj运维之路1 小时前
k8s监控方案实践(一):部署Prometheus与Node Exporter
容器·kubernetes·prometheus
和计算机搏斗的每一天1 小时前
k8s术语之DaemonSet
云原生·容器·kubernetes
斯普信专业组5 小时前
基于Kubernetes的Apache Pulsar云原生架构解析与集群部署指南(上)
云原生·kubernetes·apache
极小狐8 小时前
如何减少极狐GitLab 容器镜像库存储?
运维·git·rpc·kubernetes·ssh·gitlab·terraform
韩先超9 小时前
2025年3月,韩先超对国网宁夏进行Python线下培训
python·ai·云原生·kubernetes·devops
小马爱打代码12 小时前
K8S - 金丝雀发布实战 - Argo Rollouts 流量控制解析
云原生·容器·kubernetes
C-200217 小时前
使用Deployment部署运行Nginx和Apache服务
运维·kubernetes·apache
斯普信专业组18 小时前
基于Kubernetes的Apache Pulsar云原生架构解析与集群部署指南(下)
云原生·kubernetes·apache
张青贤1 天前
k8s的pod挂载共享内存
云原生·容器·kubernetes