# Introduction

## Eru

[![MIT license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://opensource.org/licenses/MIT)

## Introduction

Eru is a short word for Eru iluvatar, the God in the lord of the ring's world.

It is a hybrid orchestration system. Can orchestra multiple resource types like container, virtual machine, systemd etc.

作为 Eru 调度编排系统白皮书，会对 Eru 的架构/演进历史/安装部署/使用/参与开发等方方面面给出充分的说明。

### 设计目标

Eru 设计目标包括但不限于：

* 降低系统管理复杂度
* 简化服务的部署管理
* 优化基础服务的调配
* 提高资源的使用效率
* 统一开发/测试/生产等多种环境
* 持续交付工作流的良好支持
* 统一在线和离线资源调度

原则上来说 Eru 支持任意满足其接口定义的容器/虚拟化/系统引擎，并不做过强的耦合和依赖。通过架构层面上的设计和优化，使得 Eru 可以支持上千甚至上万台物理机器集群，满足小型到大型公司平台层面的调度编排需求。

### License

Eru is released under the [MIT license](https://github.com/projecteru2/white-paper/blob/master/LICENSE/README.md).


# Brief

最早是在豆瓣时期做 Douban app engine (Aka DAE) 的时候，我们遇到了一系列的问题，其中最重要也是最棘手的就是如何保证 runtime 的隔离。经过两年的开发，DAE 作为一个 Pure Python PaaS 已经很好的驱动了所有的豆瓣业务，在语言层上也做了一定颗粒度的隔离，但是在底层调度和编排上受限于 CPython 自己的 runtime，自始至终也没很好的解决应用间的隔离和资源配额。

即便是强如豆瓣这样的团队，完全把精力放在 lxc 自行研发一套容器引擎也是一件非常不现实的事情，这时候我们把目光放在了新生事物 Docker 上面。但是在13年年末的时候，Docker 毕竟还很年轻。我们也只是看了看，跑了跑，并未基于此开发，接着我就离职了……

等我回国的时候已经是14年年初，Docker 也发布了 1.0 里程碑式的版本。从生产来看，已经有部分公司开始使用 Docker 来解决运维自动化等一系列问题。在加入芒果TV之后，我组建起新的团队开始在这个方面寻求解决隔离问题的终极版「DAE」。

一开始我们实现了一个基于 Python 开发的 PaaS 平台，Aka NBE。小范围测试后发现效果还不错，资源利用和部署效率上均有不错的提升。在那个年代 K8S 还只是刚出茅庐，Mesos 的 Marthon 框架口碑不错，但公认太重，而且也是刚发布不久。至于原生的 Swarm 还不知道在哪里，于是乎我们决定自研调度编排框架，这就是 [Project Eru](https://github.com/projecteru) 的开始。

经过1年多左右的开发，Eru 一代目很好的完成了自己的目标。截止15年底，至少有 1/5 的芒果TV 业务基于此驱动，在效率和资源成本上达到了局部最优。并且我们的平台上托管了所有的 Redis，很长一段时间内芒果TV的所有 Redis 都是以 Cluster 形式在容器中由 Eru 驱动的。

再然后，我们又离职了。

15年的时候，之前豆瓣平台组的 leader 洪教授 [hongqn](https://github.com/hongqn) 去了宜信大数据，他们基于 docker 的 swarm 开发了 [lain](https://github.com/laincloud)。作为嫡系我们也和教授他们聊了很多，互通了各自设计的有无，比如他们的自举，比如我们的资源分配算法，比如他们偏 PaaS，比如我们偏 IaaS。16年我们离职后加入 [ENJOY](https://enjoy.ricebook.com/)，在 Eru1 的基础上开始进行 v2 的开发，在之前的基础上吸收了不少 lain 优秀的设计，并于16年底完成了驱动 ENJOY 所有业务的目标。

18年我们加入位于新加坡的 [SEA Group](https://www.seagroup.com/home) 旗下电商 [Shopee](https://shopee.sg/) 工作，经历了 SEA 从 40亿美金市值到 700亿美金市值的过程。在这期间因为历史的选择，Eru 逐步演进成 Shopee Cloud 最核心的调度和编排平台，同时也拥有了控制多种资源引擎的能力，不仅限于 Virtual Machine，Systemd 等，成为了 Shopee 最底层的混合资源调度平台。

既然博采众长，那 lain 完善的文档也是一大亮点之一，这就是这本白皮书的由来，感谢友军的工作。在这本白皮书里面我们也会跟 lain 的[白皮书](https://laincloud.gitbooks.io/white-paper)一样介绍 Eru2 的设计方面的各种细节，希望读者能从更高的角度了解这个项目。


# Projects

在 Eru2 的架构中有不少的组件，按照重要性的区别，我们分成了几类。用户可以根据自身的条件选择一类或者几类组合使用，或者结合现有的基础设施进行二次开发。

## 基础组件

* [core](https://github.com/projecteru2/core)
* [agent](https://github.com/projecteru2/agent)
* [cli](https://github.com/projecteru2/cli)
* [minions](https://github.com/projecteru2/minions)
* [barrel](https://github.com/projecteru2/barrel)
* yavirt

## 服务发现

* [elb](https://github.com/projecteru2/elb)

## 其他

* [quickstart](https://github.com/projecteru2/quickstart) A one key quickstart
* [demo](https://raw.githubusercontent.com/projecteru2/site/master/content/demo) A standalone demo


# History

## Core

* [Current](https://github.com/projecteru2/core/releases)
* 2.0

2016-2017 ENJOY 容器调度平台，使用 Golang 重写，放弃了复杂的 Websocket 反连结构。通过 gRPC 重新实现了 Core 的接口，架构上 Core 无状态化，和 Agent 平行。

* 1.0

2015-2016 芒果TV 容器调度平台，用 Python 实现，采用集中式架构。每个 Core 会去通过 Websocket 连上每台机器上的 Agent，计算出资源之后将资源邀约发到 Agent 上通过 Agent 来进行 Docker 操作。

* 0.5

2014-2015 芒果TV 容器化 PaaS，类似于 DAE 的 SDK-Runtime 结构，通过容器包装 Runtime 来支持不同的语言。

## Agent

* [Current](https://github.com/projecteru2/agent/releases)
* 2.0

和 Core 解耦，本质上是跑在 Node 上的一个监听代理，不负责具体的容器操作，负责日志和 metrics 等。

* 1.0

本质上是在 Node 上的一个 Websocket server，这样设计的好处是 Core 可以动态感知 Agent 的可用性，但带来了管理规模上的复杂度。这个版本里面尝试了各种日志输出手段，最后选择了 [logspout](https://github.com/gliderlabs/logspout) 作为参考对象，当然做了很大的[修改](https://github.com/gliderlabs/logspout/pull/15), 并做了一点微小的[工作](https://github.com/gliderlabs/logspout/pull/8)。

## Cli

* [Current](https://github.com/projecteru2/cli/releases)

## Minions

* [Current](https://github.com/projecteru2/minions/releases)

## Barrel

* [Current](https://github.com/projecteru2/barrel/releases)


# Benchmark

对于编排和调度平台而言，最重要的就是资源分配和调度器的实现，我们对我们自己的调度编排算法做了一次 Benchmark。

具体的 Benchmark 有:

* [Benchmark\_CPUAlloc](https://github.com/projecteru2/core/blob/master/scheduler/complex/potassium_test.go#L734)
* [Benchmark\_MemAlloc](https://github.com/projecteru2/core/blob/master/scheduler/complex/potassium_test.go#L752)
* [Benchmark\_CommunismDivisionPlan](https://github.com/projecteru2/core/blob/master/scheduler/complex/communism_test.go#L81)

前面2个分别对应于 CPU 优先的分配和 Memory 优先的分配，模拟情况是：

* CPU 优先，10K 节点，每个节点 24 个核，每个核可以提供 10 份运算力，每个容器需要 1.3 份运算力，总共部署 180K 个容器。
* Memory 优先，10K 节点，每个节点 128G 内存，每个容器需要 128M 内存，总共部署 10.24M 个容器。

对于调度算法的模拟情况是

* 10K 节点，每个节点上模拟已经部署随机个容器数 \[0, 1024), 还可以部署 1024 个容器，总共部署 10.24M - 1 个容器求其分配。

最后结果在一台 MacBookAir 2013 上半年款的开发机上 (Intel Core i5 1.3G 低电压)，如下图所示：

![](https://2370565627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LAatJdyoBI7PIItCyxP%2Fuploads%2Fgit-blob-3cdcf95e1573c609a49bdac8c12fe34b01510716%2Fbenchmark.png?alt=media)

对于结果我们可以看到:

* CPU 优先分配大概在这个量级的时候一次需要 400ms 左右，还可以继续在内存分配上继续优化。
* MEM 优先分配性能大幅优于 CPU 分配，毕竟算法复杂度要小了不少，即便是上千万的容器资源分配也在 60ms 内能完成。
* 最终平均分布算法一次在 70ms 左右。


# FAQ

Q: healthcheck 是否等待 after\_start 退出后再进行验证?

A: after\_start 不是用于 healthcheck 这个目的的，主要是有些业务需要数据预热等需求，成功失败与否都会通过日志反馈出来。对于 healthcheck 而言，它是针对于单个容器状态进行检测，如果有使用类似于 ELB 一样的 proxy 则会通过这个状态来判断是否进行动态切流。

Q: after\_start 是否有退出状态验证?

A: after\_start 的成功与否都会在日志中表现出来，可以参考[这里](https://github.com/projecteru2/core/blob/master/cluster/calcium/hook.go#L10)，当然了如果有强制需要 after\_start ，那么这里可以增加容器回收流程。

Q: healthcheck 是否允许脚本方法判断而非固定使用 HTTP. 比如应对 Java 工程中的 dubbo rpc 框架?

A: TCP 的验证需要跟业务逻辑耦合，不如 HTTP 的来得简单，因此在 Agent 实现中我们暂时只实现了 HTTP 逻辑校验和 TCP 联通校验，针对这个可以根据自身的需求进行定制。对于我们来说，不是很希望平台层面的组件去关心业务的实现并去耦合其逻辑。

Q: 没有累计次数吧？比如像 k8s 的 successThreshold, failureThreshold 等，重试多少次成功或失败才会认为其可用或不可用。

A: 我们的健康检查和 k8s 的出发点还是有些不同的。对 k8s 来说，它关注的是一个 Pod 意义上的 healthcheck，因此需要有这类检测冗余来满足最小服务单元 Pod 对外的稳定性。对于我们来说 Pod 是个逻辑上的机器组结构，最小服务单元就是容器本身，因此冗余并没太多存在的意义。

在实际生产中，上层应用看到的是一组容器来提供这一类服务，因此单个容器探测是否健康并不会对这一组容器行为造成太大影响，无外乎就是本来 1000 个容器在支撑这个服务，而有一个临时被切走了流量变成只有 999 个容器在支撑了。当然具体生产过程中还会更加复杂，比如报警机制的反馈一类的。具体到冗余有没有必要，要根据不同生产来做不同的选择，就我们目前所支撑的业务来说，暂时没这个需求也就没动力去做了。

Q: 平滑升级的并发粒度可控么, 看 specs 中没有相关的参数控制，比如初始粒度为1, 成功后可递增。

A: 在新的 replace 接口中，是允许控制 replace 步进的。

Q: 当初是基于什么原因考虑 把容器创建这些容器的操作放在了 core 而非 agent 上呢？

A: 早期我们刚开始做 Eru 前身也就是 NBE 的时候就采用过 Core 下发 Agent 执行的架构，但这样会带来几个问题：

* Agent 必须高可用，并且得有一个地方能准确的反馈其部署情况。同时在高压力情况下 Agent 的处理效率要足够高，不然就会造成 Node 不可限制的资源消耗增多，影响已经有的容器运行。
* 对于 Core 而言，要么你需要对 Agent 的可用程度进行管理和维护，但这样一来整个 Core 就是带状态的了，大集群横向扩展会比较麻烦。一个 workaround 的方法是每个 Core 都有全量 Agent 的信息，但这样会造成近似于惊群效应。当然也可以把任务下发到 mq 一类的基础设施去执行，但这样一来首先这类基础设施也需要高可用，另外同样的关于系统状态一致性的获取和检测都是一件比较麻烦的事。我们希望 Eru 是旁路的，也就是说无论 Core-Agent 是否有问题都不应该影响现有的容器运行，而每多增加一个组件所需要花费在信息一致性上的成本就越高。
* 安全性考虑，Agent 开放什么协议，开放什么端口，再去包装一层 Docker 的 API 是否有必要，这些也是需要权衡的。相比较之下 Core 目前只需要通过 https 直连 daemon，并不需要做太多其他操作还是要复杂了不少。简单可依赖，高可用可扩展是当时写 Eru2 的目的之一。

大概就是这 3 点，Eru 来说希望更加纯粹一些，所以 Core 做成了无状态。同时现在的结构当一个 Core 已经性能不够的时候完全可以通过 LVS-(CORE, CORE, CORE...) 这样的结构来进行扩展。至于 Agent 它只需要负责好自己的一亩三分地尽可能的不要对 Node 性能预估造成影响就可以了。


# Overview

Eru 是类似于 [Kubernetes](https://kubernetes.io) 的分布式容器编排和部署系统。在整个架构中使用了若干种开源项目构建而成，包括不仅限于如 [etcd](https://github.com/coreos/etcd), [calico](https://www.projectcalico.org/) 等。

Eru 不算是一种 [**PaaS**](https://zh.wikipedia.org/wiki/%E5%B9%B3%E5%8F%B0%E5%8D%B3%E6%9C%8D%E5%8A%A1) 实现，更类似于 [Nomad](https://www.nomadproject.io/) 的 multiple type executor orchestration system。因此它并不会有诸如资源异常退出拉起或者 re-deploy 这样的能力，它只专注于编排和部署。Eru 不但提供了资源维度的调度，同时也负责内容编排，本质上来说是一种抢占式资源的全局调度器。

另外在 Eru 的实现中，我们通过高效的资源分配算法避免了传统上部署加锁的问题，使得 Eru 能高效透明的处理部署和编排行为。对于寻求高效运维方案的组织和 devops 人力缺乏的 startup 以及个人开发者而言更加友善。

同时通过上层支撑组件统一开发工作流，降低运维复杂度，提供了开发，集成，部署，运维的一揽子解决方案。


# Resolution

### 1. 应用开发之下的 devops 问题的整体解决方案

* 无论是云上还是物理机均可以构建 Eru 集群，方便的进行在线的扩容缩容等集群底层资源操作
* 整合了业界沉淀下来的良好的大运维整体实践，提供了一揽子解决方案
* 将系统管理和运维管理行为封装为更简单易用的工具包，简化大部分的系统工作，降低日常维护的技术门槛和人力需求
* 将同质化的工作整合在一起，避免重复劳动
* 开箱即用的各种管理组件

### 2. 规范了应用开发的 workflow ，强制 Git 工作流

* 提供本地开发环境的规范范式
* 提供本地开发过程的工具链，使得开发和构建过程是嵌入在解决方案中的
* 提供 SCM 集成，约束了开发者的开发和发布行为
* 自动从描述文件生成 Dockerfile 减少用户不可预知行为

### 3. 提高整体资源利用率，优化冗余资源池

* 支持容器或者虚拟机技术的资源隔离和控制，实现多种技术栈多种应用在集群内安全的不相互影响的混合部署，通过统一的资源池进行冗余，有效提高资源利用率
* 容器或者虚拟机技术的运用使得对下资源的使用形成完全统一的形式，扩容缩容以及迁移的成本很低，操作也更简单

### 4. 更简单的架构提高整体的可靠性和降低部署难度

* 最小部署仅需 core，编排和部署均是无状态行为，部署简单，可任意扩展
* 高效的分配算法，支持超大规模集群
* 旁路控制，Eru 整体异常不会引起系统崩溃
* 对运行节点额外损耗很低，几乎可以当做纯物理机使用，把资源最大化利用起来
* 隐藏底层执行引擎细节，支持混合引擎


# Features

### 基于配置文件定义应用

* 在现有的应用上只需要增加一个配置文件 spec.yaml 即可定义应用在 Eru 集群里的编译和运行
* 配置文件不与代码/可运行镜像绑定
* 对现有项目 0 侵入

### 调度器中立

* Eru core 层面不绑定 workflow 行为，只关心资源和编排调度
* 通过 [cli](https://github.com/projecteru2/cli) 可以使用本地或者远程配置直接通过 Eru 来部署已有的镜像
* 方便接入上层应用 PaaS 亦或是现有的其他基础设施

### SDN 网络安全隔离

* 默认使用开源的 [calico](https://github.com/projectcalico/calico) 项目构建 [SDN 网络](https://zh.wikipedia.org/wiki/%E8%BB%9F%E9%AB%94%E5%AE%9A%E7%BE%A9%E7%B6%B2%E8%B7%AF)
* 高效率的应用内网络互通
* 多租户隔离(依托于 calico 自己的 ACL 由 Ops 层面决定)
* 支持其他不同的 SDN Driver

### 容器技术支持多样化的技术栈

* 支持 [docker](https://github.com/moby/moby) 构建容器云
* 自动生成 Dockerfile, 支持多步构建部分替代 CI 能力
* 提供动态运行代码/命令入口 (类似于 [AWS lambda](https://aws.amazon.com/cn/lambda/))
* 容器技术天然的支持隔离系统和应用的依赖
* 提供无痛升级能力

### 支持多种 executor

* 支持混合编排容器和虚拟机
* 允许自定义 Executor

### 应用在线扩容缩容

* 自行设计调度核心进行高效调度
* 支持用户从数量和资源两个维度进行扩容/缩容
* 支持应用在不下线的前提下实时重分配资源
* 支持 in-place 复用资源配额就地更新

### 服务健康检查

* 提供自定义服务健康检查，实时知道每一个应用状态 （Agent）
* 同时支持 tcp 和简单 http 健康检查行为

### 集群体系化的日志收集

* 支持多种日志 forward 行为，方便接入各类已有基础设施
* 默认收集应用的 stdout/stderr 日志收集
* 附带足够多元信息日志流方便规整和查询

### 集群自动服务发布

* 基于 [OpenResty](https://openresty.org/en/) 实现了 7 层 [Elastic load balancer](https://github.com/projecteru2/elb)
* 配合健康检查和 Eru 应用控制能力实现动态发布
* 在应用发生异常时通过 Eru 反馈机制实时进行节点摘除行为，避免服务崩溃

### 可选的存储配置和备份

* 支持在 spec.yaml 中显式声明 volume
* 支持备份

### 支持配置分离

* 允许创建时动态传入文件
* 允许自定义环境变量
* 允许运行时传入不同配置

### 高可用

* 提供 Go SDK
* 基于 gRPC 的高可用
* 高性能, 负载均衡在客户端实现

### 告警

* 通过配置文件开启 sentry 告警


# Architect

### 控制平面

> 目标是做成可以横向扩展简高可用以及高性能的资源调度核心

![](https://2370565627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LAatJdyoBI7PIItCyxP%2Fuploads%2Fgit-blob-a00728c8432f8cabf8e5b36b4db17d51c48f88d7%2Fcore.png?alt=media)

物理层面来说，用户操作的 UI 和应用抽象都实现在需要执行实现，现阶段只通过 [cli](https://github.com/projecteru2/cli) 提供命令行支持。它使用 gRPC 与 core 交互，从而的把 App 部署到 Eru 集群之中。

往下则是 Eru 核心组件 [core](https://github.com/projecteru2/core)。core 是无状态的编排调度核心，可以依据集群大小进行横向扩展，其职责是将请求进行调度和编排。按照一定的资源维度调度通过一定的算法计算出资源，编排好之后部署到到一个 Pod 中对应的一个或者多个 Nodes 上。core 之间的共享数据存储在 etcd 之中。

这里的 Pod 是一个逻辑概念，用于描述一组机器（Nodes）。每一个 Pod 都是由一组网络互通的 Node 构成。如果在大二层的层面上可以做到多机房互通的话，Pod 是允许旗下的 Node 也是跨机房的，从而在逻辑层面抹平机房这个概念。

在每一个 Node 上，可以选择这些组件:

* [Docker](https://www.docker.com/)
* [yavirt](https://github.com/projecteru2/yavirt)
* [agent](https://github.com/projecteru2/agent)

其中 agent 是 eru 二元结构中负责在 Node 上监控容器，将日志流打上必要的元信息进行转发，以及 metrics 的收集。agent 可以通过 core 来进行编排部署，因此在 agent 是自举的。

而 yavirt 类似于 docker，是用于提供虚拟机支持，它自身包含了若干 Agent 的行为。

在我们的实际用况中，我们使用了本地 [rsyslog](http://www.rsyslog.com/) 作为节点日志收集器，并转发到远端。同时通过 [moosefs](https://moosefs.com/index.html) 来实现容器/虚拟机间的文件共享。

### 业务平面

> 目标是提供统一的离线在线应用开发流程

![](https://2370565627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LAatJdyoBI7PIItCyxP%2Fuploads%2Fgit-blob-ac9e2055d18d77dae2814e41007ae731a9ef4db7%2Flogic.png?alt=media)

逻辑层面上，我们将每个业务逻辑都抽象成一个个 App，由上层平台来管理，并与 SCM 进行了整合，允许进行版本的跟踪。同时，操作 App 的一切工具如日志，如流量管理均在上层平台上进行。总的来说一切 App 生命周期都由上层平台管控着。

流量流动上面，可以看到外部世界与 App 的交互均需要通过 [elb](https://github.com/projecteru2/elb)，elb 负责所有 7 层 App 的流量导入，并可以通过不同的 version/entrypoint/url 的组合将流量分流，打入到同一个 App 的某一组不同入口的容器/虚拟机里面。

### App

> 抽象出一个或者多个具体的业务，通过不同的入口来区分其功能

![](https://2370565627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LAatJdyoBI7PIItCyxP%2Fuploads%2Fgit-blob-5406226d24a6b30197059df86942aede0b5255e8%2Fapp.png?alt=media)

应用不是代码本身。应用允许有一个或者多个入口( Entrypoint )，每一个入口都代表着不同的功能。

在实际部署的时候，应用用户还可以指定运行时 ENV 来满足环境变量的需求，也可以将本地某配置文件传入到应用运行时，从是实现配置分离。

### 相关组件

* [elb](https://github.com/projecteru2/elb)
* [cli](https://github.com/projecteru2/cli)
* [footstone](https://github.com/projecteru2/footstone)

### SDN 选择

* [Calico](https://www.projectcalico.org/)
* [MacVlan](https://docs.oracle.com/cd/E37670_01/E37355/html/ol_mcvnbr_lxc.html)
* [openresty](https://openresty.org/en/)


# Resource


# HealthCheck

### Old Health Check

Eru 是松耦合结构，Core 和 Agent 并非强制绑定的。因此在老的流程里面，Core 只负责创建容器，并且在 ETCD 中存储好元数据的 Key，至于 Value 则由各种 Agent 实现自行调用 Core 的 API 写入。从外部可以通过 `DeployStatus` 接口拿到数据变化和具体内容。

具体流程为：

1. Core 创建 `/deploy/{APPNAME}/{ENTRYPOINT}/{NODE}/{CONTAINER_ID}` 这样的 Key，值为空。
2. Agent 检测到容器启动
   1. 若没有 Health Check 设置，则直接写入一个 Json，包含 `Health: true, Running: true`。
   2. 若有 Health Check，则先写入 `Health: false, Running: false`，进行 health check 后再更改其值。
   3. 等 Agent 完成一轮 Health Check 后更新数据写入，并缓存下来
   4. 如果新的一 Health Check 得到的结果和缓存的不一致，调用 `DeployContainer` 接口进行更新

这个模式有几点好处：

1. 简单，单向反馈链路。只有 Key 存在的时候，Agent 才能写会成功，避免了数据加锁。
2. 数据反馈次数少，由 Agent 缓存住了。
3. 允许自定义 Agent 写入数据结构。

也有缺点：

1. Client 需要依赖 Core 的同时依赖各种 Agent 实现。
2. 假如 Node 断网，从外部 mark 容器为非健康非运行状态后，Agent 无法在 Node 恢复后自动更新运行和健康（因为被缓存了）。
3. 如果没有 Agent，Core 无法反馈任何容器状态。

因此对于这个 Health Check 流程做了一定的修改。

### New Health Check

对于 Core，增加了一个 `Meta` 数据结构，包含最小公约属性如 `Networks` 和 `Running` 等。各种 Agent 需要继承这个结构做自行扩展。这就解决了第一个缺点，使得 Client 即便在不知道各类 Agent 实现的前提下拿到部分运行时数据。

对于 Agent，通过 [go-cache](https://github.com/patrickmn/go-cache) 实现了 auto-clean 的状态缓存，这就解决了第二个缺点。如果外部 mark 容器健康状态后，一段时间后 Agent 也能正确的上报当前状态。从另外一个角度来说，提供了 maintenance 的能力。

另外重新设计了数据上报接口的逻辑，使得在满足有上报数据路径和上报状态变化 2 个条件的同时，Core 会修改容器的元数据，存储其 `Running` 和 `Networks` 2 个属性。这使得 Core 有能力在没有 Agent 或者 Agent 临时下线后依然能反馈一部分运行时装填，解决了第三个缺点。

### 资源生命周期

在实现新的 Health Check 后，资源的生命周期大体如下：

1. 创建容器，Core 会在元数据中存储创建后得到了 `Running` 和 `Networks`。
2. Agent/Yavirtd 接手资源健康检查：
   1. 如果没有健康检查设置，Healthy 永远为 true。
   2. 如果有健康检查，Healthy 为 false，上报。
   3. 开始健康检查，上报检查结果（Healthy 属性）。
   4. 如此反复。
3. 如果 Core 接到的上报状态与之前的状态不一致则会更新元数据。


# Component

### 1. Core

> 调度器核心

* 无状态
* 生成编排方案
* 并发进行部署

### 2. Agent

> Node 上的控制器

* 资源消耗低
* 负责容器检查
* 获取 Metrics 发送到远端
* 转发日志

### 3. ELB (Eru load balancer)

> 7 层动态服务发布

* 基于 [Openresty](https://openresty.org/en/)
* 本身也是 Eru 应用之一
* 通过指定的 Redis 进行发布工作
* 应用上下线过程中保证流量平稳切换

### 5. Cli

> 命令行工具

* 提供类似于 AWS Lambda 子命令
* 通过 cli 操控集群本身
* 通过 cli 可以在初始化集群之后进行集群自举

### 6. Minions

> A calico libnetwork plugin port

* Calico libnetwork plugin 不支持 docker engine
* 采用最新的 libcalico + etcdv3 实现
* 行为和 calico-cni plugin 一致
* 支持 bird 的最新版本和其特性
* 支持原生 fixed IP 特性

### 7. Barrel

> A docker daemon wrapper

* Docker wrapper for fixed IP feature
* 原生区分了 stop/remove 行为

### 8. Yavirt

> Yet another virt daemon

* 基于 QEMU-KVM 的虚拟机 runtime
* 支持多种行为操作, 比如 execute command, remote console
* 支持 Calico 网络的集成
* 支持镜像一键式打包上传到 VM Image Hub


# Conception

Eru 自身包含一系列的定义，这一部分我们主要来讲讲 Eru 中定义的那些元素都有哪些，这些元素都是由那几部分组成的，分别又有哪些作用。


# Pod

### 概念

对于 Eru 而言，Pod 的概念和 [Kubernetes 的 Pod](https://kubernetes.io/docs/concepts/workloads/pods/pod/) 是不太一样的。对于 K8s 来说 Pod 是最小部署单元，并且也是在 Pod 上实现了最小资源单元。在 Eru 中 Pod 是一个虚拟概念，用途就是很直观的告诉上层这一组机器（Node）是用来做什么的，然后依托于其他手段通过不同的网络参数来满足具体服务之间的隔离或者互通需求。App 能部署在某一个 Pod 或者某几个 Pod 上，Pod 下面的 Node 也不会限制在一个机房之内。

### 属性

Pod 只有一类属性，就是 Nodes，用于描述有多少个节点，对于 Eru 来说最小资源单位是 Node 上的资源总和。


# Node

### 概念

Node 是最小资源单元，也是最后执行部署的地方。通过 Core 的调度 App 容器/虚拟机会分布到某个 Pod 下的若干 Nodes 之中，并「消耗」掉其对应的资源。Node 指代的就是一台机器，多个 Node 组成了一个逻辑上的「业务组」也就是 Pod，多个 Pod 组成了一个 Eru 集群，而多个 Eru 集群通过不同的 Zone 来区分。

### 属性

Node 用于描述一个机器，最重要的属性主要有：

1. 数值型资源，如 Memory，以 bytes 记录。
2. 复合型资源，如 CPU Core，把 CPU 抽象成 Index: Piece 的二元组，Index 表示是第几个核，Piece 表示最多能承载多少份算力。
3. Deploy Status, Node 会记录其上面某一类 App 的部署情况，通过这种信息我们可以在再次部署此类 App 的时候通过算法平衡同一 Pod 下各 Nodes 之间的容器/虚拟机数量，从而平均其容量，增强可用性。

还有一切其他的属性，可以参考其[定义](https://github.com/projecteru2/core/blob/master/types/node.go)。


# Application

### 概念

Application 在 Eru 中用于描述一个可部署的项目。对于 Core 而言它并不关心跑的是哪一个 App，它只负责调度和编排，因此这是一个 Workflow 上的概念。对于 Eru 而言，上面的每一个容器/虚拟机都可以对应到一个具体的 Application 描述。当然由于一些历史原因，我们在定义 App 的时候允许一个 App 有多个入口（多种能力），主要是为了方便项目在非拆分的情况下也能最小化迁移代价。当然对于原生 Docker 应用而言，我们还是建议一个容器一个进程一份目的和明确的 App 用途定义的。

### 属性

对于 App 而言，最重要的属性主要是：

1. Name, 每一个 Eru App 的 Name 都应该是唯一的。
2. Entrypoint，用于描述代码的入口。App 支持多种入口，代表资源的不同行为，在容器引擎的实现下每一种入口的日志都会自动的分流到对应远端。
3. 可选 Stages/Builds，用于描述如何讲项目打包成镜像，也可以用于简单的 CI process。

在 Eru 里面一切都是一个描述文件，具体可以参考 [agent 自举的描述文件](https://github.com/projecteru2/agent/blob/master/app.yaml)。


# Workload

### 概念

Workload 在 Eru 里代表一个最终部署实例抽象, 在不同的 [runtime](https://github.com/projecteru2/white-paper/blob/master/conception/todu/README.md) 上可能代表容器/虚拟机/daemon 进程.

### 操作

Workload 拥有全局唯一到 ID, 通过 ID 可以完成一系列通用操作:

1. Create
2. Stop
3. Start
4. Remove
5. Execute: 在容器 namespace 里, 或者 VM 内部, 执行指定的命令
6. Realloc: 动态调整 workload 的资源配额. 详见 [资源](https://book.eru.sh/conception/resource)
7. Replace: 删除旧实例, 原地起新实例, 可继承网络配置
8. Copy: 从实例里拷贝指定文件到客户端
9. Send: 从客户端拷贝指定文件到实例内部

注意 Realloc 是实时重分配, 容器进程不会被停止; 不过 Yavirt 虚拟机暂不支持实时重分配.

### 生命周期

Eru Workload 可以在创建时指定 `after_start` 和 `before_stop` hook, 可以用来完成一系列应用初始化和清理的工作. 详见 [spec](https://book.eru.sh/specs/app).

Eru Workload 还可以在创建时指定 `healthcheck` 来定义应用的健康状态, 可以定义 tcp ports 和 http url 作为检测方式. 详见 [spec](https://book.eru.sh/specs/app).

Workload 本身的状态由节点上的 eru-agent 监控和上报给 eru-core, 上报信息包括:

1. running: 实例是否存活
2. healthy: 在 `healthcheck` 定义下的实例是否健康
3. networks: 实例 IP
4. extension: 其他 tags

eru-core 提供了一个接口 `WorkloadStatusStream` 供第三方监听 Workload 的生命状态, 一旦有状态变化会推送消息给监听方, 从而完成 failover 等机制.

### 健康检查

健康检查是由 [spec](https://book.eru.sh/specs/spp) 中的 `healthcheck` 部分定义的.

详细的信息参见 [健康检查](https://book.eru.sh/conception/healthcheck).


# Runtime

### 概念

Runtime 是 eru-core 对接的 workload 实际引擎, 目前支持 Docker Container, Yavirt KVM, Systemd Daemon 三种引擎.

对用来来说, 不需要特别关注 Runtime 的实现细节, eru-core 会尽量抹平不同 Runtime 在接口上的区别, 让用户能通过指定不同的 [pod](https://book.eru.sh/conception/pod) 在不同的 Runtime 上部署 Workload.

### Docker Container

eru-core 与节点上的 Dockerd 通讯, 完成基于 dockerd-containerd-runc 的容器部署.

### Yavirt KVM

Yavirt 是 Shopee 自研的基于 QEMU-KVM 的虚拟机 Runtime, 并集成了 Calico SDN, 实现了 eru-core 对容器-虚拟机的混合编排系统.

Yavirt KVM Runtime 相比容器提供了更强的隔离性和安全性.

Yavirt 也实现了类似 Docker 的镜像打包和 hub 体系, 可以很方便地上传下载.

### Systemd Daemon

对于一些更基础的 DaemonSet 性质的服务, 或是容易与 dockerd 的启动呈相互依赖, 如 calico-node 和 CNM/CNI 插件, 使用 Systemd Daemon Runtime 更合理.

当前(2021-02) Systemd Daemon Runtime 尚在施工中, 尤其是二进制的部署方案尚未落地. 请参考[文档](https://github.com/projecteru2/core/issues/317).


# Resource

Eru 目前实现了四种资源的调度, 每种资源又有 request 和 limit 的区分.

### 四种资源

首先, 四种资源在使用前都需要注册到 [node](https://book.eru.sh/conception/node):

```
eru-cli node add --nodename zc \
    --endpoint tcp://127.0.0.1:2376 \
    --cpu 8 \
    --memory 4G \
    --volumes /sda0:100G --volumes /sda1:200G \
    --storage 500G \
    zc_pod
```

#### CPU

CPU 分为绑核和不绑核两种请求, 在截止 v21.02.15 的 eru-core 里它们在容器和 Systemd Runtime 上的实现都是通过 Linux Cgroup V1, 具体如下:

1. cpu=1.2, cpu-bind=false: `cpu.cfs_quota_us=120000`, `cpu.cfs_period_us=100000`
2. cpu=1.2, cpu-bind=true: `cpu.cfs_quota_us=120000`, `cpu.cfs_period_us=100000`, `cpuset.cpus=1,2`

可以看到绑核请求最终会反映到 `cpuset.cpus` 上, 而 cpu quota 会反映到 `cpu.cfs_quota_us`.

绑核请求最终会绑定到那几个核上由 eru-core [scheduler](https://book.eru.sh/conception/scheduling) 计算得出.

CPU 调度行为可以参考[Overall/Resource](https://book.eru.sh/overview/resource).

#### Memory

内存最终也是反映到 Linux Cgroup V1:

1. memory=14M: `memory.`

#### Volume

Volume 分为普通调度挂载, 独占式调度挂载, 硬挂载.

1. 普通调度挂载请求: `AUTO:/data:rw:100` a. `AUTO` 标识需要调度, 由 eru [scheduler](https://book.eru.sh/conception/scheduling) 调度后从节点上可用 volume 选出一个 b. `/data` 是 Workload 内挂载目标 c. `rw` 代表可读写, 另外还可指定 `ro` 只读 d. `100` 代表可写入字节限制 100 字节
2. 独占式调度挂载请求: `AUTO:/data:rwm:100` a. `rwm` 里的 `m` 代表 monopoly, 是独占调度的标识. 有这个 flag 的请求会独占一个空的宿主机 volume, 并且视该 volume 被占满, 不会在未来的调度中再次被分配.
3. 硬挂载: `/var/log:/var/log` a. 硬挂载其实就是传统的挂载, 不会被调度, 不占用宿主机 volume 资源

Volume 目前只有 Yavirt Runtime 实现了 quota, 而 Docker 和 Systemd 只实现了挂载. 换句话说 `AUTO:/data:rw:100` 里的最后一个参数 `100` 没有被 Docker 和 Systemd runtime 实现.

#### Storage

Storage 的语义是: workload 的根目录磁盘 quota. Storage 目前主要是用在 Yavirt Runtime 上, 用来分配给虚拟机实例的根目录挂载大小. 本身也是可以用在 docker container 上, 但是只有特定的文件系统才支持.

### Request, Limit

所有四种类型的资源都区分 Request 和 Limit, 它们的区别是:

* Request 只用在 eru-core 计算资源和调度编排, 最终反映到 eru-core 管理的资源元数据扣减
* Limit 只用在 Runtime 限制实例资源 quota

通过设置 request 和 limit 不同值可以达到不同的效果, 比如:

1. cpu.request=1, cpu.limit=0, cpu-bind=false: 资源扣减 1 核, 但是实际对 workload 不限制 cpu (0=unlimit)
2. memory.request=1G, memory.limit=2G: 资源扣减 1G, 但是实际对 workload 限制 2G 内存使用
3. volumes.request=\[AUTO:/data:rw:100M], volumes.limit=\[AUTO:/data:rw:50M]: 资源扣减 100M, 实际限制 50M 磁盘使用

有一些特殊规则在里面, 比如:

1. cpu-bind=true 时, 强制设置 cpu.limit=cpu.request
2. volumes.request 和 volumes.limit 里除了 quota size 不一样, 其他必须一致

在实际使用的时候, 用户需要知道自己想要什么, 从而做出正确的调用.


# CPU

创建实例的时候, 经过 [调度](https://book.eru.sh/conception/scheduling) 之后, 最后 CPU 以两个属性传递给 [Runtime](https://book.eru.sh/conception/runtime), 并且由具体的 runtime 去实现对 CPU 对分配和限制.

传递给 Runtime 的两个 CPU 属性是 `CPUQuota`(int) 和 `CPUMap`(map\[string]int), 前者代表一个实例最多使用几个核, 后者代表核的绑定情况.

比如:

1. Quota=1, Map=nil: 不绑核, 限制一个 CPU
2. Quota=1, Map={0:100}: 绑定0号核, 限制一个 CPU
3. Quota=1.2, Map={0:100, 1:20}: 绑定0号1号核, 分别占用 100% 和 20%, 总共限制 1.2 个 CPU

### 进程级 Runtime 对上述语义对实现

Docker 和 Systemd Runtime 都是进程级的 Runtime, 对于这种 Runtime 我们使用 Cgroups 去实现对 CPU 的限制.

1. 对于不绑核实例, 设置 cpuset.cpus 为 share cpu pool, 同时设置 cpu.cfs\_quota\_us 限制 CPU 时间片.
2. share cpu pool 会动态改变, 每有一个新的实例绑核, pool 就会把这个已经绑定的核摘除.
3. 对于绑核实例, 同时 CPUMap 全是完整核, 只设置 cpuset.cpus 为 CPUMap 绑定的要求.
4. 对于绑核实例, 同时 CPUMap 里有碎片核, 同时设置 cpuset.cpus 和 cpu.shares; cpu.shares 按照碎片核的占比设置.

实际用例:

1. 请求一 cpu=1, bind=false, 最终 cpuset.cpus=0-7, cpu.cfs\_quota\_us=100000
2. 请求二 cpu=1, bind=true, 最终 cpuset.cpus=0, cpu.shares=1024 (1024 是默认 share, 相当于没有修改); 随后 share pool 被调整为 1-7, 请求一创建的进程的 cpuset.cpus 被调整为 1-7.
3. 请求三 cpu=1.2, bind=true, 最终 cpuset.cpus=1,3, cpu.shares=20; 之后请求 1 创建的进程 cpuset.cpus 再次被调整为 2,4-7.
4. 请求四 cpu=1.2, bind=false, 最终 cpuset.cpus=2,4-7, cpu.cfs\_quota\_us=120000

### NUMA 支持

在 CPU Core 分配模式下，如果 Node 注册的时候包含 NUMA 信息，CPU 分配策略会尽可能的使得目标使用同一个 NUMA Node 上的 CPU。如果所需容器数超过 NUMA Node 能分配的数量，则会自动跨 NUMA Node 计算还能部署多少容器，尽可能的满足部署需求。比如一个 NUMA Node 包含 2 核 1G 内存，有 2 个 NUMA Node。这时候需要 1 核 600M 的容器，则会计算出最多能部署 3 个。分别是 Node 0 和 1 各自 1 个，跨 Node 1 个。


# Scheduling

请先参考 [资源](https://book.eru.sh/conception/resource)

eru-core 把 Scheduling 分为“容量计算”和“编排”两个步骤.

### 容量计算

调度的目的是按照资源请求计算每个节点能部署多少个实例, 以及每个部署方案的资源绑定情况.

比如:

1. 请求 memory 10M, 节点可用内存 100M, 那么该节点能部署(Capacity) 就是 10.
2. 请求 cpu 1 核, 绑定, 节点可用 cpu 状态是 `{0:0, 1:0, 2:100, 3:100}`(0/1 核已用光, 2/3 核还未被调度), 那么 Capacity 就是 2, 方案分别是 `{2:100}`和 `{3:100}`
3. 请求 volume `AUTO:/data:rw:100`, 节点可用磁盘状态是 `{/sda0:1000, /sda1: 200}`, 那么 Capacity 是 12, 方案分别是 10 \* `{/sda0:100}` 和 2 \* `{/sda1:100}`.

这一步计算会有两个产出:

* 第一个产出是容量, CapacityMap(`map[Node]int`), 比如 `{node1: 10, node2: 0, node3:1}`, 记录每个节点可部署的实例数目, 可能是无限大(我们允许资源请求为 0, 即无限制)
* 第二个产出是资源绑定方式, PlanMap(`map[Node][]map[string]int64`), 比如 `{node1: [{1:100}, {2:100}]}`, 记录每个节点上可用的资源绑定细节, 可能为空(比如内存资源并不需要绑定).

实际的调度策略会比这更加复杂, 包括碎片核的调度, 独占式调度, 等.

#### 容量计算算法

**CPU 为主**

假设所需 M bytes 内存和 X.Y 个 CPU，其中 X 表示整数位，Y 表示小数位。我们将单一节点上的 CPU 抽象成由 A 个整数核和 B 个碎片核组成。假如管理者设定的运算量为 P ，那么每个碎片核上的每一份能提供的就是 1/P 运算能力。因此对于单个 Node 而言最大可部署数量为 min( Memory/M，min( A/X，B\*P/Y )) 个。

在 A+B = CPU 总数量的前提下，我们要通过不断的尝试组合 A, B 的值来确定多个 min( A/X, B\*P/Y ) 结果中最多的那个，再与内存计算结果进行比较。由于 cpushare 是单个核全局一致的，因此没法拆分 Y 为更小的多个运算量之和，所以我们只允许一次绑定一个碎片核。实际实现中还需要考虑已有碎片核的处理和碎片核运算量不能跨核进行，因此会更复杂一些。

**Memory 为主**

简单的进行 Memory / M 即可，对于 CPU 需求而言采用 CPU period 和 quota 进行全局控制。

以上 2 种算法在得到资源组合之后，Eru 会对 Nodes 上可部署数量排序，由少到多。然后再通过计算判断 Nodes 上的可部署数量是否满足用户需求。这样虽然会带来单一 Node 资源利用率不够高的可能性，但在大规模部署的情况下会在 Nodes 层面更加平均，分布上会更好。但是当能承载最少数的那个 Node 也能满足用户需求的时候，多次部署会产生堆积到一台 Node 的情况，因此 Eru 引入了更高级的分布算法。

### 编排

经过上一步容量计算后, 我们有了 CapacityMap 和 PlanMap, 接下来这一步会根据此 CapacityMap 和用户请求来选出在哪些节点上分别部署多少个实例. 注意这一步不使用 PlanMap.

这一步计算会产出部署方式, DeployMap(`map[Node]int`), 比如 `{node1: 10, node3:4}`, 代表每个节点分别要部署多少实例. 这就是最终的要部署的实例数目了.

eru-core 内置了 4 种编排策略, 分别是 Communism(Auto), Fill, Global, Average.

#### Communism(Auto)

Communism 策略的目的是保证在调度完成后该应用的所有实例(新+旧)在所有节点上数量尽量平均.

`NodesLimit` 对 Auto 对语义是限制节点上实例总数(新+旧)的上限.

比如:

1. 已有实例状态是 `{node1:0, node2:0, node3:0}`, 请求 Count 3, 最终编排结果是三节点各部署一实例, 最终状态是(新+旧) `{node1:1, node2:1, node3:1}`
2. 已有实例状态是 `{node1:5, node2:4, node3:0}`, 请求 Count 3, 最终编排结果是全部部署到 `node3`, 最终状态是(新+旧) `{node1:5, node2:4, node3:3}`

#### Fill

Fill 策略到目的是保证在调度完成后所有实例(新+旧)的总数量为至少 `Count` \* `NodesLimit` 个(至少 `NodeLimit` 个节点上有至少 `Count` 个实例); 若已满足此状态则报错.

`NodeLimit` 语义是“部署多少个节点”.

比如:

1. 已有实例状态是 `{node1:0, node2:0, node3:0}`, 请求 Count 1, NodesLimit 3, 最终编排结果是三节点各部署一实例, 最终状态是(新+旧) `{node1:1, node2:1, node3:1}`
2. 已有实例状态是 `{node1:1, node2:0, node3:0}`, 请求 Count 1, NodesLimit 3, 最终编排结果是 `node2`/`node3` 各部署一实例, 最终状态是(新+旧) `{node1:1, node2:1, node3:1}`
3. 已有实例状态是 `{node1:1, node2:1, node3:1}`, 请求 Count 1, NodesLimit 3, 最终编排结果是报错, 因为已有状态已满足.
4. 已有实例状态是 `{node1:2, node2:2, node3:0}`, 请求 Count 1, NodesLimit 3, 最终编排结果是 `node3` 部署1实例, 最终状态是 `{node1:2, node2:2, node3:1}`.
5. 已有实例状态是 `{node1:1, node2:1, node3:1}`, 请求 Count 2, NodesLimit 2, 最终编排结果是 `node1`/`node2` 各部署1实例, 最终状态是 `{node1:2, node2:2, node3:1}`.

#### Average

Average 策略的目的是在 `NodesLimit` 个节点上部署 `Count` 个实例, 保证本次增量部署 `Count` \* `NodesLimit` 个实例.

`NodeLimit` 的语义和 Fill 一样, 是“部署多少个节点”的意思.

比如:

1. 已有实例状态是 `{node1:1, node2:0, node3:0}`, 请求 Count 1, NodesLimit 3, 最终编排结果是三节点各部署一实例, 最终状态是(新+旧) `{node1:2, node2:1, node3:1}`

#### Global

Global 策略的目的是保证节点的资源使用率尽量平均.

比如:

1. 当前节点的资源利用率是 `{node1:1%, node2:2%, node3:3%}`, 请求 Count 3, 一个实例在三个节点分别会占用的资源率是 `{node1:0.4%, node2:0.6%, node3:1%}`, 最终编排结果是在 `node1`/`node2` 分别部署 2/1 个, 最终资源利用率是 `{node1:1.8%, node2:2.6%, node3:3%}`

`NodesLimit` 对 Global 暂无语义.


# Networking

eru-core 的 SDN 网络主要由 Runtime 层实现, 用户请求里的 `network` 会被透传.

1. Docker Runtime: 只要有对应的 CNM 插件, 创建了对应都 docker network, 那么用户指定的 network 就能成功创建.
2. Yavirt Runtime: 绑定使用 Calico SDN, 同 docker network 类似, 需要先在 yavirt 中注册网络与 CIDR.
3. Systemd Runtime: 通过 CNI 把 daemon 扔进 Calico SDN, 尚在施工中.

这套机制的好处是:

1. BGP-based: 可通过 Router Reflector 与现有基础设施集成
2. Pure L3: 无封包, 高性能
3. CNI/CNM-based: 成熟的 SDN API, 官方背书
4. 统一网络, 便于混合编排容器和虚拟机

除此之外, eru-core 提供了两套 CNM 插件: eru-minions, eru-barrel

### Minions

Minions 是一个 Calico SDN 的 CNM 插件, 用于在 etcd v3 上打通 dockerd 和 calico-node.

Minions 虽然稳定, 但是推荐使用 eru-barrel, 提供了更多的功能.

### Barrel

Barrel 也是一个 Calico SDN 的 CNM 插件, 但是与 Minions 相比, 他额外提供了 fix-ip 的功能.

先创建一个有 `fixed-ip` label 的容器:

```
docker run -td --label fixed-ip --network calico-sdn bash bash
```

然后把它停止, barrel 保证它的 ip 不会被之后的新容器占用, 因此可以安全有保障地 restart 且拥有相同 ip.

wiki: <https://github.com/projecteru2/barrel/wiki/Quick-Start>


# HA

eru-core 提供了 Go SDK, 只要客户端使用 SDK 与 eru-core 通讯, 就能自动发现多实例的 eru-core, 并在部分 eru-core 实例宕的时候自动切换到其他实例.

它的实现分为服务端和客户端两部分.

### 服务端

eru-core 会不断把自己的 outbound IP 注册到 etcd 上, 其中 outbound IP 是通过 `net.Dial("8.8.8.8")` 后检查 src IP 获得的.

注册到 etcd 上到地址绑定了 lease, 如果没有及时被 keepalive 会被超时删除, 这就是 eru-core 的心跳机制, 如果节点宕, 或者进程崩溃, 进程阻塞, 那么在 keepalive interval 之后会被探测到, 并从可用 eru-core 实例池里剔除.

### 客户端

eru-core SDK 会注册 eru resolver, 该 resolver 会有个 goroutine watch eru-core API `WatchServiceStatus`, 从而获得最新到可用 core 实例地址. 一旦有变化(新增或者减少), 都会通过 gRPC Resolver 机制动态生效.

gRPC 提供了 LB per Call 的机制, 也就是说用户先后发送两个请求给 eru-core, 每次都会被 Round Robin 重新计算一次, 选择一个可用的链接发送请求, 这比 HTTP1.1 长链接高明不少.

此外 eru-core SDK 还定制了 Interceptor, 让各种 Stream 失败的时候能够重试, 从而新选择一个可用的连接.


# Components


# Agent

### 简介

Agent 主要的功能是：

* 检查 Workload 的状态（例如健康状态、网络信息等），并且上报至 Core
* 检查此 Node 的状态，并且上报至 Core
* 获取并转发 Workload 的日志
* 获取并转发 Metrics

Agent 并没有 HA 机制，建议在一个 Node 上只部署一个 Agent，用于监控这个 Node 及在其上运行的 Workload 的状态。

### 结构

Agent 的主体结构如下图：

![](https://2370565627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LAatJdyoBI7PIItCyxP%2Fuploads%2Fgit-blob-d0004b028664270be5f04ee2669e7a325773a864%2Fagent.png?alt=media)

和 Core 一样，Agent 并不与 Runtime 类型强耦合。Agent 把所有与运行时相关的逻辑抽象为如下图所示的 `Runtime` 接口，只需要通过简单适配，实现这个接口即可支持任意 Runtime。目前已经支持了 `Docker` 和 `Yavirt`，未来还会支持 `Systemd` 等。

![](https://2370565627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LAatJdyoBI7PIItCyxP%2Fuploads%2Fgit-blob-fccc9bb8dcd2bebf869b3c40d9ee46737fecd977%2Fruntime.png?alt=media)

Agent 还做了其它抽象化的工作，例如将 `Core Service` 抽象为 `Store`，将 `ETCD` 抽象为 `KV`。这些抽象不仅增强了扩展性，还为设计单元测试提供了便利。


# Selfmon

### 简介

曾经每个 Eru-agent 都要不断地向 Core 上报 Workload 的状态，随后 Core 会把这些信息存入 ETCD，这样给 ETCD 造成了很大的写入压力。为了解决这个问题，我们设计了 Selfmon。

在引入 Selfmon 后，每次上报 Workload Status 之前，Agent 会和本地缓存中的 status 作对比，如果发生变化才会上报到 Core。并且上报时不再设置 TTL，即 ETCD 里对应的 Key 不会过期。

不再设置 TTL 产生的问题就是：如果 Agent 意外退出了，就无法上报 Workload Status，导致会有一部分 Workload 在 Eru 看来永远处于 Running & Healthy 状态。

所以 Selfmon 加入了监控 Node Status 的功能。每个 Agent 会定时上报自己所在 Node 的状态（带TTL），而 Selfmon 会持续监听这些 Node Status 的变化，并且做对应的处理。

如果某个 Agent 意外退出了，它最后一次上报的 Node Status 会在一段时间后过期。Selfmon 监听到了这个过期事件，就会调用 Core 的相关接口，把这个 Node 和上面所有的 Workload 标记为不可用。

Agent 启动时，Selfmon 则会调用 Core 的接口把这个 Node 设置为 Available，由 Agent 自己重新检查并上报 Workload Status。

Selfmon 支持 HA，多个 selfmon 同时只有一个会拿到分布式锁并且正常运行。可以在集群里部署多个 selfmon。

### 流程

下文中会提到 `Node Status` 和 `Node Info`，先解释一下它们的区别。

`Node Info` 存储了 Node 的 metadata，Eru 用来判断这个 Node 是否可用时只会参考 `Node Info`。

而 `Node Status` 是由 Agent 上报， 被 Selfmon 监听的。如果没有 Selfmon，`Node Status` 的变化不会造成任何影响。

Selfmon 会根据 Node Status 的变化来设置对应的 `Node Info`。例如某个 `Node Status` 因为过期被 ETCD 删除了，Selfmon 会把这个 Node 的 `Node Info` 设置为 `Available: false`。

Selfmon 主要的运行流程是：

1. 不断请求拿到分布式锁
2. 拿到锁之后进行初始化工作：调用 Core 的接口拿到所有 Node Status，然后设置对应的 Node Info。
3. 持续监听 `Node Status` 的变化，并做相应处理。


# Specs

这里我们会详细介绍 Eru 的一些东西

* App yaml 描述
* 各组件的配置


# App Spec

#### 定义

App Spec 是用来定义如何操作源码或者镜像的 Yaml 描述文件。在 Spec 中分2个部分

* App 描述
* Build 描述

这2个部分可以放在一个文件中，也可以分开放。Cli 读取之后会分别进行部署操作或者打包操作。

#### App

一个标准的 App 描述如下：

```
appname: "{APPNAME}"                        Application ident
entrypoints:
  {ENTRYPOINT_A}:                           Application role A
    cmd: "run something"                    How to run this role, depend on image
    privileged: true/false                  Run in privileged mode
    dir: "path"                             Working dir
    log:
      type: "journald/none"                 Same as docker log driver name
      config:                               Same as docker log driver config
        config1: "value"
    publish:                                Which port(s) bind this Application
      - "12345"
      - "7788"
      ...
    healthcheck:
      tcp_ports:                            Tcp port check
        - "12345"
        - "7788"
        ...
      http_port: "80"                       A http port for advance checking
      url: "/lol"                           Check url
      code: 200                             Which code is correct
    hook:
      after_start:                          Some commands run after start
        - cmd1
        - cmd2
        ...
      before_stop:                          Some commands run before stop
        - cmd1
        - cmd2
        ...
      force: true/false                     Force to stop container if it set true and before_stop failed.
    restart: always/""                      Always or empty. Always means alway restart when failed, otherwise it will try 3 times then stop
    sysctls:
      net.ipv4.ip_forward: "1"
  {ENTRYPOINT_B}:                           Application role B
        ...
  {ENTRYPOINT_C}:                           Application role C
        ...
volumes:                                    Mount local dir insider container/vm
  - path1:path2:ro
  - path3:path4
volumes_request:                            Mount local dir insider container/vm
  - path1:path2:ro
  - path3:path4
labels:                                     Add labels to container/vm
  label1: "something"
  label2: "anything"
  ...
dns:                                        User define dns
  - {DNS1}
  - {DNS2}
  ...
extra_hosts:                                Add hosts item in host file
  - domain1:IP1
  - domain2:IP2
  ...
```

#### Build

一个标准的 Build 描述如下：

```
stages:                                                 Define stages
  - {BUILD_STAGE_A}
  - {BUILD_STAGE_B}
  - {BUILD_STAGE_C}
  ...
builds:
  {BUILD_STAGE_A}:
    base: "golang:1.10.3-alpine3.7"                     Base image
    repo: "git@github.com:projecteru2/agent.git"        Github/gitlab repo, only support ssh protocol
    version: "HEAD"                                     Version sha
    dir: "/go/src/github.com/projecteru2/agent"         Working dir
    submodule: true                                     Prepare submodules
    commands:                                           Building commands
      - cmd1
      - cmd2
      ...
    envs:                                               Building envars
      A: B
      C: D
      ...
    args:                                               Building args same as dockerfile definition
      A: B
      C: D
      ...
    labels:                                             Building labels
      A: B
      C: D
    artifacts:                                          If provide, eru will earse whole working dir and download artifact in it
      ident1: path1
      ident2: path2
      ...
    cache:                                              If provide, next stage will get those things to working dir.
      src_path1: dst_path1
      src_path2: dst_path2
      ...
    stop_signal: ""                                     If provide, will use this signal for stopping container
    security: true/false                                If provide, will remove .git dir after clone
  {BUILD_STAGE_B}:
    ...
  {BUILD_STAGE_C}:
    ...
```


# Core Config

#### 介绍

Core Config 是用来决定 Core 行为的配置，一般会保存在 `/etc/eru/core.yaml` 也可以通过 `core --config` 来指定。一个标准的 core 配置如下：

```
log_level: "DEBUG"                                  决定 core 自身日志等级
bind: ":5001"                                       gRPC 服务绑定地址和端口
lock_timeout: 30s                                   分布式锁超时时间
global_timeout: 300s                                全局操作超时，目前只用于容器删除的时候加锁防止重复删除
statsd: "127.0.0.1:8125"               optional     core 的状态将会发送到这个 statsd 地址
profile: ":12346"                      optional     profile 端口
cert_path: "/tmp"                      optional     临时存储 certs 文件地址

auth:                                  optional     gRPC basic auth
    username: "user"
    password: "password"

grpc:                                  optional     gRPC 相关配置
    max_concurrent_streams: 100
    max_recv_msg_size: 20971520
    service_discovery_interval: 15s
    service_heartbeat_interval: 15s

git:                                   optional     此处主要用于 Build
    scm_type: "github"                              现在支持 github/gitlab
    private_key: "***REMOVED***"                    SSH priv key path
    token: "***REMOVED***"                          对 github/gitlab 而言需要在这里填 access token
    clone_timeout: 300s                             clone 超时时间

etcd:                                               ETCD 配置
    machines:
        - "http://127.0.0.1:2379"                   ETCD 地址，支持多机
    prefix: "/core"                                 数据 prefix
    lock_prefix: "core/_lock"                       全局锁的 prefix
    ca: ""                             optional     ca
    key: ""                            optional     key
    cert: ""                           optional     cert
    auth:                              optional     etcd auth
        username: "user"
        password: "password"

docker:                                             Docker 配置
    version: "1.32"                                 Docker API Version
    network_mode: "bridge"                          默认网络模型
    hub: "hub.docker.com"              optional     Registry 地址
    namespace: "projecteru2"           optional     Registry 的 Namespace
    build_pod: "eru-test"              optional     采用哪个 Pod 用于 build
    local_dns: true                    optional     是否用本地 DNS，如果 create 容器的时候没下发 DNS 将会使用这个
    log:                               optional     默认日志驱动
        type: "journald"
        config:
            config1: "value"
    auths:                                          Push 和 Pull 验证，core 支持配置多个 registry
      hub.docker.com:
        username: "user1"
        password: "password1"
      hub2.docker.com:
        username: "user2"
        password: "password2"
      ...

scheduler:                                          调度器的配置
    maxshare: -1                                    最大有多少碎片核，-1 表示不限制
    sharebase: 10                                   碎片核最多能分多少份，10表示最小单位是10%，100表示为1%以此类推

virt:                                               yavirt api 版本
    version: "v1"

systemd:
    username: "root"                                systemd 引擎配置
```


# Agent Config

#### 介绍

Agent Config 是用来决定 Agent 行为的配置，一般会保存在 `/etc/eru/agent.yaml` 也可以通过 `agent --config` 来指定。一个标准的 agent 配置如下：

```
pid: /tmp/agent.pid                              pid path
store: grpc                             optional store 的类型，默认为 grpc，即 eru-core 的 grpc 服务
runtime: docker                         optional runtime 类型，默认为 docker
kv: etcd                                optional kv 类型，默认为 ETCD

core:                                            Core 的 API 地址
  - 127.0.0.1:5001                               可以配置多个地址

healthcheck:                                     Health Check 配置
  interval: 120                         optional Health Check 间隔
  timeout: 10                           optional Health Check 超时判定
  cache_ttl: 300                        optional 本地缓存的过期时间，仅当 enable_selfmon 为 true 时生效
  enable_selfmon: false                 optional 集群里是否启动了 selfmon

auth:                                   optional Core API Basic auth
  username: username
  password: password

docker:                                          Docker 配置
  endpoint: unix:///var/run/docker.sock          Docker url，支持 unix socket file 地址和 tcp 地址的形式

yavirt:                                          Yavirt 配置
  endpoint: grpc://127.0.0.1:9697                Yavirt url，支持 grpc 地址和 http 地址
  skip_guest_report_regexps:                     ID 符合任意正则表达式的 VM 将不会被检查
    - .+002

metrics:                                         Metrics 配置
  step: 30                                       发送间隔
  transfers:                            optional 默认支持 prometheus，也可以开启 statsd
    - 127.0.0.1:8125                             可以配置多个 statsd 后端

api:                                    optional profile API 配置
  addr: 127.0.0.1:12345                          profile API 地址

log:                                             日志选项
  forwards:
    - tcp://127.0.0.1:5144              optional 日志远端, 支持 tcp、udp、journal 和特殊的 __discard__
  stdout: False                                  是否把容器日志打到 agent 自身日志流里面

ha_keepalive_interval: 16s              optional selfmon 发送心跳的时间间隔

etcd:                                            ETCD 配置，仅当此 agent 以 selfmon 模式运行时生效
  machines:                                      ETCD 服务器地址
    - 127.0.0.1:2379
  prefix: /agent-selfmon                         Key的前缀
```

Agent 在监听到隶属 Eru 的容器启动后，会把 stdout/stderr 的日志 forward 到配置的远端，同时开始监听 metrics。

对于日志，格式如下：

```
{"id":"{CONTAINER_ID}","name":"{APP_NAME}","type":"{stderr || stdout}","entrypoint":"{APP_ENTRYPOINT}","ident":"{APP_IDENT}","data":"{LOG_MSG}","datetime":"2018-06-27 07:19:25.507018","extra":{CONTAINER_LABELS}}
```

在测试的时候甚至可以用 `netcat` 来模拟日志接收，使用

```
nc -l {PORT} -k
```

需要注意的是，log 参数支持 tcp 和 udp 2种形式，udp 对于单条日志大小限制为了1024K，因此对于线上系统而言，建议使用 TCP 模式。

对于 Metrics，Agent 会发一组数据到配置的远端 statsd 中：

```
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.cpu_usage:0.06814310051085702|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.cpu_user_usage:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.cpu_sys_usage:40.00000000003638|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.mem_rss:6184960|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.mem_usage:2088960|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.mem_max_usage:2215936|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.mem_usage_percent:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.lo.drop.in:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.lo.drop.out:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.lo.err.in:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.lo.err.out:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.lo.bytes.recv:10445.6|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.lo.bytes.sent:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.lo.packets.recv:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.lo.packets.sent:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.cali0.drop.in:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.cali0.drop.out:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.cali0.err.in:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.cali0.err.out:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.cali0.bytes.recv:5191.333333333333|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.cali0.bytes.sent:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.cali0.packets.recv:0|g
eru.{APPNAME}.{TAG}.{ENTRYPOINT}.{HOST}.{CONTAINER_ID}.cali0.packets.sent:0|g
```

通过这个数据我们就可以从容器外面监控容器行为。

如果没有开启 statsd，也可以通过 `API_HOST:PORT/metrics` 拿到 prometheus 规格的 metrics。


# Quickstart

这里我们将介绍如何构建起一个 Eru 集群。主要有：

1. Eru 的依赖
2. 构建单机 Eru 集群
3. 构建多机 Eru 集群
4. 脚本工具

具体可以参考 [projecteru2/quickstart](https://github.com/projecteru2/quickstart)，在这个项目中我们提供了一个快速体验 Eru 集群的工具。运行这个工具会在服务器上部署 Eru 的各组件，并通过使用 cli 来验证其运行正常。


# Install

### 安装的依赖和服务

运行完 [quickstart.sh](https://github.com/projecteru2/quickstart/blob/master/quickstart.sh) 之后，机器上自动安装并配置好的依赖和服务包括：

#### ETCD

Quickstart 会默认安装 v3.3.4 版本的 ETCD，并启动；一些重要的默认配置包括：

```ini
name: etcd0
data-dir: /var/lib/etcd
listen-peer-urls: http://0.0.0.0:2380
listen-client-urls: http://0.0.0.0:2379
enable-v2: true
```

#### Docker

Docker 会启动并监听在默认的 :2376 端口上，并使用上述配置的 ETCD 作为 cluster store

```json
{
  "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
  "cluster-store": "etcd://127.0.0.1:2379"
}
```

Docker 启动成功后会自动注册一个名为 "testpool" 的 Calico network 到该 Docker 集群。

#### Calico node

Quickstart 会确保两项 Kernel 配置为期望值，包括激活 net.ipv4.ip\_forward 和设置 net.netfilter.nf\_conntract\_max 值为 1000000；之后会下载并以 Container 形式启动 v3.4 版本的 Calico node；启动成功后会自动创建名为 "testpool" 的 IPPool，该 IPPool 的 CIDR 默认为 10.10.0.0/16

#### ERU Core

ERU Core 将运行在默认的 :5001 端口，并将所有的元数据信息存储在 ETCD 的 /eru-core 目录下；启动成功后，Quickstart 会下载保存 eru-cli 命令行工具，并配置相应的 PATH 信息到 \~root 账号，以供未来使用；eru-cli 准备完毕后会自动注册一个名为 "eru" 的 POD 到 ERU Core，并在该 POD 下自动新建一个名为 "docker0" 的节点。

### How to Quickstart

#### 运行时依赖

* Ubuntu >=16.04
* ansible >=2.9.10
* jmespath >=0.10.0

#### Quickstart

准备好上述依赖后，即可在目标机器上执行

```bash
curl https://raw.githubusercontent.com/projecteru2/quickstart/master/quickstart.sh | bash
```

#### 升级

重复执行 [quickstart.sh](https://github.com/projecteru2/quickstart/blob/master/quickstart.sh) 命令即可。


# Release

首先我们来看看一个典型的 Eru 集群结构是怎样的，如下图：

![](https://2370565627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LAatJdyoBI7PIItCyxP%2Fuploads%2Fgit-blob-d569f89f9ef32aaec2d6ddedaa2bb9cef908dbd3%2Fprocess.png?alt=media)

我们可以看到，一个 Eru 集群是由一组 eru-core 控制多个 pod，每个 pod 中有多个 node，通过这些 node 来部署目标容器/虚拟机。外部流量可以通过 elb 导入到集群中，也可以通过其他的方式。操作人员只需要关注 cli 就好了。那么我们如何部署一个真实的集群呢？

## etcd & docker

对于 Eru 而言必不可少的只有 etcd 和 docker，其中 etcd 承担着数据持久化的角色，类似于 MySQL。因此无论你采用何种 etcd 部署结构，你都需要保证有一个可用于生产环境的 etcd (集群)。这里你可以参考官方的部署[文档](https://github.com/coreos/etcd/blob/master/Documentation/dev-guide/local_cluster.md)。

然后，将需要接入 Eru 的 node 都预先准备好上面的 docker。需要配置的有两项东西，一项是为了保证 core 与 docker daemon 的通讯安全，我们推荐使用 tls 来配置 docker daemon 。另外一项需要注意的是 docker daemon 需要开启 `cluster-store`, 这个选项会通过 etcd 将我们在其上面配置的 SDN 信息共享到其他的 node，如果你有使用 SDN 的话，一定不要忘记配置它。具体可以参考 [quickstart/docker.sh](https://github.com/projecteru2/quickstart/blob/master/docker.sh) 和 [dockerd配置](https://docs.docker.com/engine/reference/commandline/dockerd/)，在 quickstart 的脚本中我们把 docker daemon 的 cluster-store 指向了那个预先安装的 etcd，并启用了 tls，在生产环境中亦是如此。

## (optional) calico

在我们自己用的集群中是使用 Calico 作为 SDN provider 的，如果你也需要用可以参考[calico.sh](https://github.com/projecteru2/quickstart/blob/master/calico.sh) 的初始化步骤。要注意的是，`docker network create` 只需要在任意节点运行一次即可，并不需要运行多次。只要运行了一次之后，以同一个 etcd 作 cluster-store 的 docker daemon 都会知道有了这么一个网络。

另外如果有自己定制化 Calico SDN ACL 需求的，可以参考其[文档](https://docs.projectcalico.org/v2.6/getting-started/docker/)。

## eru-core

现在我们需要找一个地方运行一个或者一组 eru-core。对于高可用的系统而言，我们建议跑多个 eru-core 并在之上通过 LVS/haproxy 的组件来保证起服务高可用。eru-core 在整个集群没有一个 eru-core 的时候是无法完成[自举](https://github.com/projecteru2/core#build-and-deploy-by-eru-itself)的，因此推荐使用 rpm 的方式或者通过原始的 docker run 的方式启动第一个 eru-core。

无论怎样一旦配置好 etcd 并启动 eru-core 之后，eru 集群就可以说准备好了。

## cli

在这一步你需要有一个地方指定 cli 命令，如果是在 CentOS 7 下可以通过 rpm 来安装 eru cli 的 rpm 包（当然你需要先自己打好，可以参考我们的 [make-rpm](https://github.com/projecteru2/cli/blob/master/make-rpm)）。另外一种方式则是使用已有的[镜像](https://hub.docker.com/r/projecteru2/cli)来执行 cli 命令。

假设用镜像的前提下，可以通过以下命令创建 pod:

```
docker run -it --rm \
  --net host \
  projecteru2/cli \
  erucli pod add <PODNAME>
```

如果 `cli` 和 `core` 并非一台机器的话，传入 `ERU` 变量指向正确的地址即可。

在创建完 pod 之后，可以通过以下命令增加 node:

```
docker run -it --rm --privileged \
  --net host \
  -v <TLS_DIR>:/etc/docker/tls \
  projecteru2/cli \
  erucli node add eru
```

在这里我们假定了 `cli` 所运行的机器就是所需要增加的节点，如果是远程操作的话可以通过 `--nodename` 等参数来指定，具体的可以使用 `cli node add --help` 查看。通过 `cli` 工具你能很方便的组建起一个真实的 Pod-Node 结构的集群。我们的 Node 支持 `Label` ，因此在注册的时候可以通过增加这些元信息来进行部署时的节点过滤。

## eru-agent

在准备好 `core` 和 `cli` 后，我们就可以开始部署 `agent` 了。对于 eru 而言，agent 其实不是必须的，当然了如果你需要这些东西的时候，agent 就必须部署上去了。

1. logs forward with meta data
2. metrics collect
3. healthcheck
4. dynamically update container status like publish ip

你可以通过手动部署 agent 也可以通过镜像部署，在 quickstart 中我们是通过 cli 自举部署了 agent，具体可以参考[这里](https://github.com/projecteru2/quickstart/blob/master/agent.sh)。

## (optional) metrics and logs

测试的时候可以使用 nc 来模拟 tcp 服务，在正式生产中如果你有对应的 `logs` 和 `metrics` 服务那是极好不过了。但要记得修改 core 和 agent 对应的配置文件。

最后，ENJOY run container by Eru now!


# Example

和其他开源的调度和编排平台不同的一点在于，Eru 致力于提供在线和离线混编，因此通过 cli lambda 子命令我们可以用来做一些离线的计算任务。

比如下面视频中，我们把一个大文本抽成了 100 个随机文件，每个包含若干个单词，使用 Lambda 对每一个文件统计其单词总数。这里我们起了 100 个容器，每个容器 1% 的 CPU 占用。

[![asciicast](https://asciinema.org/a/142690.png)](https://asciinema.org/a/142690)

我们也可以在装好 core 之后使用 cli 做 agent 的部署，可以参考下面视频：

[![asciicast](https://asciinema.org/a/142614.png)](https://asciinema.org/a/142614)

实在是没机器的话，也可以尝试 `sh <(curl -fSsL https://eru.sh/demo)` 来启动一个 standalone 的 Eru。


# Installation

这里我们会详细介绍如何部署一个 Eru 集群和如何维护它

* 准备工作
* 部署 Core
* 部署 Agent
* 管理 Pod 和 Node

一般来说，一旦 ERU 集群建立完之后，以后每增加一台机器都只需上去做初始化工作加入集群即可, 通过 Cli 我们可以很容易的完成这些事情。

**注意: 对于刚部署好的 Eru 集群而言，你需要先添加 Pod 才能添加 Node，只有添加完 Node 之后才能部署 Agent。**


# Requirements

Eru 集群只需要以下几个东西

* [ETCD](https://github.com/coreos/etcd) 大于 3.3 的版本
* [Docker](https://www.docker.com/) 大于 17.05 的版本

以下是可选依赖

* [statsdaemon](https://github.com/bitly/statsdaemon) 用于 metrics 收集。目前 Eru 支持 statsd 格式的数据发送，任意支持 statsd 格式的接受者都可以
* [prometheus](https://prometheus.io/) 同样用于 metrics 收集。
* 任意日志收集器，如本地 [syslog](http://man7.org/linux/man-pages/man3/syslog.3.html) 等。

#### Core 依赖安装

ETCD 建议参考官方安装[简介](https://github.com/coreos/etcd/blob/master/Documentation/op-guide/clustering.md)在生产环境中组成 3 节点以上的静态集群或者动态集群。不过在这里建议每一台 Node 上通过 [etcd proxy](https://coreos.com/etcd/docs/latest/v2/proxy.html) 来提升使用体验。ETCD 集群只需要一个即可，余下的每台机器上都可以批量部署 Proxy。

#### Node 节点初始化

以 Docker 为例，可以参考以 [QuickStart](https://github.com/projecteru2/quickstart) 的实现。

每一台 Docker 机初始化的时候我们要注意这些地方：

````
* IP 一定是要 Core 能访问到的 IP，多网卡的情况下可以手动指定，单网卡可以参考以下脚本：

```bash
export IP=$(ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print $2}' | cut -f1  -d'/')
echo 'Current ip:' ${IP}
```

* 配置 Docker 的时候直接把配置存在 ```/etc/docker/daemon.json``` 即可，如果是在中国大陆可以加入以下2个选项来提升体验：

```bash
"dns": ["114.114.114.114"]
"registry-mirrors": ["https://registry.docker-cn.com"]
```

* 配置 tls 证书的时候一定要和选定的访问 IP 一致

以 CentOS 为例，最后我们得到脚本如下：

```bash
yum install -y docker-ce
mkdir -p /etc/docker/tls
echo "{
    \"hosts\": [\"unix:///var/run/docker.sock\", \"tcp://${IP}:2376\"],
    \"tlsverify\": true,
    \"tlscacert\": \"/etc/docker/tls/ca.crt\",
    \"tlscert\": \"/etc/docker/tls/server.crt\",
    \"tlskey\": \"/etc/docker/tls/server.key\",
    \"cluster-store\": \"etcd://${ERU_ETCD}\"
}" > /etc/docker/daemon.json
openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.crt -days 3650 -subj /C=CN
openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj /CN=${IP}
openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt -days 3650
openssl req -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj /CN=client
openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in client.csr -out client.crt -days 3650
chmod 600 ca.key client.key server.key
rm -rf server.csr client.csr
mv ca.* client.* server.* /etc/docker/tls
```
````

完成机器的配置后，就可以来部署安装 Eru 了。


# Deploy core

对于 Eru 而言，Core 有 4 种方式：

1. 对于 CentOS 7 以上的机器 Core 提供了 RPM 方式安装，可以通过源码中的 `make-rpm` 来生成 RPM 包来安装。
2. 对于已有 Eru 而言，也可以通过 Eru 来部署 Core，具体可以参考 Core 的 [README](https://github.com/projecteru2/core#build-and-deploy-by-eru-itself)。
3. 使用 binary 裸运行 core，binary 可以从[这里](https://github.com/projecteru2/core/releases)下载
4. 当然也可以通过容器直接跑 Core，执行以下命令即可：

```
docker run -d \
  --name eru_core_$HOSTNAME \
  --net host \
  --restart always \
  -v <HOST_CONFIG_DIR_PATH>:/etc/eru \
  projecteru2/core \
  /usr/bin/eru-core
```

对于生产环境而言，为了避免 Docker 自身奇怪的问题和保证 Eru 整个集群旁路控制，同时 Core 是无状态的因此最好的情况是一组 VM 单独跑 Core。对于这些 VM 而言只需要暴露 ETCD 给 Core 访问就行了，如果有本地 ETCD Proxy 用户体验会更加简单。若干个 Core 其实就已经能负责上万台机器的调度和编排了。


# Deploy agent

对于 Eru 而言，Agent 有 4 种方式：

1. 对于 CentOS 7 以上的机器 Agent 提供了 RPM 方式安装，可以通过源码中的 `make-rpm` 来生成 RPM 包来安装。
2. 对于已有 Eru 而言，也可以通过 Eru 来部署 Agent，具体可以参考 Agent 的 [README](https://github.com/projecteru2/agent#build-and-deploy-by-eru-itself)。
3. 使用 binary 裸运行 agent，binary 可以从[这里](https://github.com/projecteru2/agent/releases)下载
4. 当然也可以通过容器直接跑 Agent，执行以下命令即可：

```
docker run -d --privileged \
  --name eru_agent_$HOSTNAME \
  --net host \
  --restart always \
  -v /sys/fs/cgroup/:/sys/fs/cgroup/ \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /proc/:/hostProc/ \
  -v <HOST_CONFIG_DIR_PATH>:/etc/eru \
  projecteru2/agent \
  /usr/bin/eru-agent
```

对于生产环境而言，为了避免 Docker 自身奇怪的问题和保证 Eru 整个集群旁路控制，最好就是每台机器用 RPM 安装 Agent，只需要对 Agent 暴露 Core 的地址即可。


# Deploy calico(optional)

每台机器上安装配置 Calico 需要多步配置：

### 安装 calicoctl 和启动 calico-node

需要注意的是，最新的 `calicoctl` 和 `calico-node` 只支持 ETCD v3 协议下的存储模型了，因此过去的 `libnetwork-plugin` 是不包含在 calico-node 的镜像中，也不会启动。

```
export ERU_ETCD=ETCD_IP_PORT
export CALICOCTL_VER=v3.4.0
ls /usr/bin | grep calicoctl &> /dev/null || curl -L https://github.com/projectcalico/calicoctl/releases/download/${CALICOCTL_VER}/calicoctl -o /usr/bin/calicoctl
chmod +x /usr/bin/calicoctl

mkdir -p /etc/calico
echo "apiVersion: projectcalico.org/v3
kind: CalicoAPIConfig
metadata:
spec:
  datastoreType: "etcdv3"
  etcdEndpoints: "http://${ERU_ETCD}"
" > /etc/calico/calicoctl.cfg

calicoctl node run --node-image=calico/node
```

执行完毕后，`calico-node` 就应该启动了。默认情况下 calico 是跑在 BGP Mesh 网络中的，节点中互相 mesh。对于超过100台机器的情况下这个网络模型是不太合适的，一来不适合管理二来性能受限。因此在超大集群模式下，推荐阅读 [Calico 网络模型](https://docs.projectcalico.org/v3.1/reference/private-cloud/l3-interconnect-fabric) 来选择适合自己的网络模式。

### 安装对 Docker 的支持

受限于最新的 [calico-libnetwork-plugin](https://github.com/projectcalico/libnetwork-plugin) 已经不再支持 v3 版本的 calico，Eru 做了一个 port 称之为 [eru-minions](https://github.com/projecteru2/minions)。部署方式和之前的的 libnetwork plugin 一样，但做了对最新的 Calico 支持。具体的修改的话可以参考对上游的 [PR](https://github.com/projectcalico/libnetwork-plugin/pull/183)。

因为 Docker 在 Crash 后重启时会先尝试恢复网络去调用 plugin，如果 plugin 同时也需要 docker 中容器的信息就会导致循环依赖从而 block all。因此插件的安装仅推荐使用 RPM 方式裸安装，通过 `systemctl` 运行。

在 Minions 项目中，可以通过 make-rpm 得到 RPM 包，然后执行 `rpm -i` 安装即可。安装完毕后只需要修改 `/etc/eru/minions.conf` 指定 ETCD 即可启动。启动后，在 docker 的 plugin 文件下应该就会出现 `calico.sock` 和 `calico-ipam.sock` 2个文件了。

### 系统配置

Calico 使用 3 layer 网络进行转发，因此每一台跑了 calico-node 的机器需要对 2 个系统参数修改：

```
echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
echo 'net.netfilter.nf_conntrack_max=1000000' >> /etc/sysctl.conf
sysctl -p
```

### 配置 IP Pool

当我们配置子网的时候，随便找一台 calico 的 node 执行配置即可：

```
export NETPOOL=10.213.0.0/16
export NETNAME="etest"

cat << EOF | calicoctl create -f -
- apiVersion: projectcalico.org/v3
  kind: IPPool
  metadata:
    name: ${NAETNAME}
  spec:
    natOutgoing: true
    cidr: ${NETPOOL}
EOF
docker network create --driver calico --ipam-driver calico-ipam --subnet ${NETPOOL} ${NETNAME}
```

注意，不要采用与物理网络同样的子网。

### 测试

当我们完成这一切的时候，可以通过 `docker -it --rm --network ${NETNAME} alpine ifconfig` 看到是否添加了一块名叫 `cali0` 的网卡，这时候从任意 calico node 或者其他同一子网的其他容器 ping 通这个容器了。

具体 IP Pool 的配置可以参考[这篇](https://docs.projectcalico.org/v3.1/reference/calicoctl/resources/ippool)，余下的配置都可以参考 [Calico 官网的手册](https://docs.projectcalico.org/v3.1/reference/)。


# Management

Eru 完全是旁路控制，因此我们只需要添加好 Pod 和 Node 后就可以操作 Eru 集群了。

### 准备工作

Eru 提供了 [cli](https://github.com/projecteru2/cli) 来操作集群。cli 可以通过容器运行：

```
docker run -it --rm \
  --net host \
  --name eru-cli \
  projecteru2/cli \
  /usr/bin/erucli <PARAMS>
```

也可以直接把 binary 放在 `$PATH` 下作为命令使用，通过 `make-rpm` 能生成 rpm 包并安装在机器中。具体的子命令和参数可以使用 `erucli -h` 获得

### 创建 Pod

```
docker run -it --rm \
  --net host \
  projecteru2/cli \
  erucli pod add ${POD_NAME}
```

在安装好 Core 之后，集群实际上还是不能启动的，需要先注册 Pod。Eru 反对实体富容器 Pod，但通过这种形式可以把多种相关容器限定在一组机器中组成机器层面上的**富容器**。

### 添加节点

创建好 Pod 之后就可以开始给 Pod 添加节点了，在被添加的已经配置好 https docker 的机器上，执行以下命令：

```
docker run -it --rm --privileged \
  --net host \
  -v /etc/docker/tls:/etc/docker/tls \
  projecteru2/cli \
  erucli node add ${POD_NAME}
```

这样节点就会加入到目标 Pod 中，然后就可以进行容器编排和部署了。就是在注册的时候时候建议冗余一些 CPU 和内存给 OS 本身。比如 4Core 的机器写 3Core，128G 内存注册为 120G 等。


# Get Started

搭建一个 eru 小集群感受一下.

* [创建一个单节点集群](https://book.eru.sh/getstarted/setup)
* [创建和操作一个 workload](https://book.eru.sh/getstarted/sample_app)
* [绑定资源](https://book.eru.sh/getstarted/bind_resource)
* [更新 workload](https://book.eru.sh/getstarted/update_app)
* [SDN](https://book.eru.sh/getstarted/sdn)
* [workload 状态](https://book.eru.sh/getstarted/workload_status)


# Setup

在 Linux 拉起来一个最小的 eru 单点集群.

### 前置配置

节点需要安装 Docker 并且把 Docker 配置为监听 `0.0.0.0:2376`.

### 部署一个 etcd

单节点就行了, 用容器跑起来超简单.

```
docker run -d --net host --name etcd --restart always quay.io/coreos/etcd:v3.4.9 /usr/local/bin/etcd --enable-v2 --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://etcd:2379
```

### 准备一个配置文件

这是 eru-core 的配置文件, 详细的配置在[这里](https://github.com/projecteru2/core/blob/master/core.yaml.sample)

```
mkdir -p /etc/eru
cat <<! > /etc/eru/core.yaml
log_level: "DEBUG"
bind: ":5001"
service_address: 127.0.0.1:5001
statsd: "127.0.0.1:8125"
profile: ":12346"
global_timeout: 300s
lock_timeout: 30s
cert_path: "/tmp"
max_concurrency: 20

grpc:
    max_concurrent_streams: 100

etcd:
    machines:
        - "http://localhost:2379"
    prefix: "/eru"
    lock_prefix: "core/_lock"

docker:
    log:
      type: "json-file"
      config:
        "max-size": "10m"
    network_mode: "bridge"
    hub: "hub.docker.com"
    namespace: "projecteru2"
    build_pod: "local"
    local_dns: true

scheduler:
    maxshare: -1
    sharebase: 100
!
```

### 起服务

```
docker run -d --name eru-core --restart always -v /etc/eru:/etc/eru -v /root/.ssh:/root/.ssh --net host projecteru2/core eru-core
```

安装命令行 eru-cli

```
alias eru-cli='docker run -it -v $(pwd):/src -w /src --net host projecteru2/cli eru-cli'
echo alias eru-cli=\'docker run -it -v $(pwd):/src -w /src --net host projecteru2/cli eru-cli\' >> ~/.bashrc
```

运行我们的第一个 eru 命令:

```
eru-cli pod list
```

成功的话能看到输出:

```
┌──────┬─────────────┐
│ NAME │ DESCRIPTION │
├──────┼─────────────┤
└──────┴─────────────┘
```

因为还是一个空集群, 不过:

**Welcome to Eru world!**

### 加节点

就把自己所在的[节点](https://book.eru.sh/conception/node)加入 eru-core:

需要先把 dockerd 设置为绑定 2376 端口:

```
sed -i 's!ExecStart=.*!\0 -H tcp://0.0.0.0:2376!' /lib/systemd/system/docker.service
systemctl daemon-reload && systemctl restart docker
```

然后就可以在 eru-core 里注册[节点](https://book.eru.sh/conception/node)了. 不过你需要先添加一个 [pod](https://book.eru.sh/conception/pod)

```
eru-cli pod add testpod
eru-cli node add --nodename node1 --endpoint tcp://127.0.0.1:2376 testpod
```

顺利的话你能看到正确的输出:

```
root@localhost:~# eru-cli pod add testpod

┌─────────┬─────────────┐
│ NAME    │ DESCRIPTION │
├─────────┼─────────────┤
│ testpod │             │
└─────────┴─────────────┘
root@localhost:~# eru-cli node add --nodename node1 --endpoint tcp://127.0.0.1:2376 testpod
┌───────┬──────────────────────┬──────────┬──────────────────────┬─────────────┬─────────────┐
│ NAME  │ ENDPOINT             │ CPU      │ MEMORY               │ VOLUME      │ STORAGE     │
├───────┼──────────────────────┼──────────┼──────────────────────┼─────────────┼─────────────┤
│ node1 │ tcp://127.0.0.1:2376 │ 0.00 / 4 │ 0 / 6261624012 bytes │ 0 / 0 bytes │ 0 / 0 bytes │
└───────┴──────────────────────┴──────────┴──────────────────────┴─────────────┴─────────────┘
```

接下来我们可以开始创建应用了.


# Sample App

### 第一个应用

“应用”在 Eru 中用于描述一个可部署的项目, 详细的文档在[这里](https://book.eru.sh/conception/application)

部署 eru 应用需要写一个 spec.yaml, 详细的 spec 在[这里](https://book.eru.sh/specs/app).

不过最简单的 spec 写成这样就可以了:

```
cat <<! >spec.yaml
appname: zc
entrypoints:
  zc:
    cmd: sleep 1000000
!
```

然后就可以创建出第一个应用:

```
root@localhost:~# eru-cli workload deploy --image bash --pod testpod --entry zc spec.yaml
INFO[2021-03-12 10:03:39] [Deploy] Success 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f zc_zc_cuQhQH node1 1 1 map[] 536870912 536870912 map[] map[]
```

`--image bash` 指定了使用 bash 镜像; `--pod testpod` 指定使用刚才创建的 pod (里面有一个节点); `--entry` 指定 spec 里的 `zc`.

### Workload

Workload 可以先参看[文档](https://book.eru.sh/conception/workload)

1. 查看已部署的 workload

```
root@localhost:~# eru-cli workload get 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f
┌──────────────────────────────────────────────────────────────────┬───────────────────────────┬──────────────────────────┬──────────┐
│ NAME/ID/POD/NODE                                                 │ STATUS                    │ VOLUME                   │ NETWORKS │
├──────────────────────────────────────────────────────────────────┼───────────────────────────┼──────────────────────────┼──────────┤
│ zc_zc_cuQhQH                                                     │ CPUQuotaRequest: 1.000000 │ VolumePlanRequest: map[] │          │
│ 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f │ CPUQuotaLimit: 1.000000   │ VolumePlanLimit: map[]   │          │
│ testpod                                                          │ CPUMap: map[]             │                          │          │
│ node1                                                            │ MemoryRequest: 536870912  │                          │          │
│                                                                  │ MemoryLimit: 536870912    │                          │          │
│                                                                  │ StorageRequest: 0         │                          │          │
│                                                                  │ StorageLimit: 0           │                          │          │
│                                                                  │ Privileged: false         │                          │          │
└──────────────────────────────────────────────────────────────────┴───────────────────────────┴──────────────────────────┴──────────┘
```

1. 停止, 重启

```
root@localhost:~# eru-cli workload stop 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f
INFO[2021-03-12 10:11:26] [ControlWorkload] 38078e6
root@localhost:~# eru-cli workload start 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f
INFO[2021-03-12 10:11:34] [ControlWorkload] 38078e6
root@localhost:~# eru-cli workload restart 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f
INFO[2021-03-12 10:11:55] [ControlWorkload] 38078e6
```

1. exec

执行一个非交互式命令

```
root@localhost:~# eru-cli workload exec 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f ls
bin
dev
etc
home
lib
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
```

执行一个交互式命令

```
root@localhost:~# eru-cli workload exec -i 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f bash
bash-5.1# ls
bin    dev    etc    home   lib    media  mnt    opt    proc   root   run    sbin   srv    sys    tmp    usr    var
```

1. 把文件发送到容器内

```
root@localhost:~# eru-cli workload send --file ./spec.yaml:/a.yaml 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f
INFO[2021-03-12 10:21:59] [Send] Send /a.yaml to 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f success

root@localhost:~# eru-cli workload exec 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f cat /a.yaml
appname: zc
entrypoints:
  zc:
    cmd: sleep 1000000
```

1. 删除容器

```
root@localhost:~# eru-cli workload remove -f 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f
WARN[2021-03-12 11:01:09] [RemoveWorkload] If workload not stopped, force to remove will not trigger hook process if set
INFO[2021-03-12 11:01:09] [RemoveWorkload] 38078e63ac63c4f8d805ed3d0b94ab2ab23333f9e05ac944e9028571fc0a065f Success
```


# Bind Resources

### 资源

eru 目前管理了四大资源: CPU, Memory, Volume, Storage, 详细的文档在[这里](https://book.eru.sh/conception/resource).

不过我们可以先用几个典型使用场景来感受一下.

1. 限制容器的 cpu 和 memory

```
root@localhost:~# eru-cli workload deploy --pod testpod --image bash --entry zc --cpu-request 1 --cpu-limit 2 --memory-request 15M --memory-limit 15M ./spec.yaml
INFO[2021-03-15 02:26:46] [Deploy] Success ae96772e9cc60b4bc36d663cf66d0b04a849d144cad2905513b2d45ac169b8d3 zc_zc_EwUJow node1 1 2 map[] 15728640 15728640 map[] map[]
```

命令行里的 `--cpu-request 1 --cpu-limit 2` 和 `--memory-request 15M --memory-limit 15M` 就是用来指定资源配额的.

request 和 limit 的区别在[这里](https://book.eru.sh/conception/resource), 不过如果你搞不清楚的话直接让两个都配置一样的值.

查看容器的时候会显示资源:

```
root@localhost:~# eru-cli workload get ae96772e9cc60b4bc36d663cf66d0b04a849d144cad2905513b2d45ac169b8d3
┌──────────────────────────────────────────────────────────────────┬───────────────────────────┬──────────────────────────┬──────────┐
│ NAME/ID/POD/NODE                                                 │ STATUS                    │ VOLUME                   │ NETWORKS │
├──────────────────────────────────────────────────────────────────┼───────────────────────────┼──────────────────────────┼──────────┤
│ zc_zc_EwUJow                                                     │ CPUQuotaRequest: 1.000000 │ VolumePlanRequest: map[] │          │
│ ae96772e9cc60b4bc36d663cf66d0b04a849d144cad2905513b2d45ac169b8d3 │ CPUQuotaLimit: 2.000000   │ VolumePlanLimit: map[]   │          │
│ testpod                                                          │ CPUMap: map[]             │                          │          │
│ node1                                                            │ MemoryRequest: 15728640   │                          │          │
│                                                                  │ MemoryLimit: 15728640     │                          │          │
│                                                                  │ StorageRequest: 0         │                          │          │
│                                                                  │ StorageLimit: 0           │                          │          │
│                                                                  │ Privileged: false         │                          │          │
└──────────────────────────────────────────────────────────────────┴───────────────────────────┴──────────────────────────┴──────────┘
```

1. cpu 的绑定

cpu 绑定的行为稍微复杂一点, 可以看[这里](https://book.eru.sh/conception/cpu).

不过可以简单理解为进程会绑定在指定的 cpu core 上.

使用上的话只要多指定一个 `--cpu-bind` 就可以了.

```
root@localhost:~# eru-cli workload deploy --pod testpod --image bash --entry zc --cpu-request 1 --cpu-limit 1 --cpu-bind ./spec.yaml
INFO[2021-03-15 03:09:06] [Deploy] Success fb689b8227fc274538cb7fe9d4ad81562ae3882567acef71f02da768d3c18736 zc_zc_sCvwqB node1 1 1 map[1:100] 536870912 536870912 map[] map[]
root@localhost:~# eru-cli workload get fb689b8227fc274538cb7fe9d4ad81562ae3882567acef71f02da768d3c18736
┌──────────────────────────────────────────────────────────────────┬───────────────────────────┬──────────────────────────┬──────────┐
│ NAME/ID/POD/NODE                                                 │ STATUS                    │ VOLUME                   │ NETWORKS │
├──────────────────────────────────────────────────────────────────┼───────────────────────────┼──────────────────────────┼──────────┤
│ zc_zc_sCvwqB                                                     │ CPUQuotaRequest: 1.000000 │ VolumePlanRequest: map[] │          │
│ fb689b8227fc274538cb7fe9d4ad81562ae3882567acef71f02da768d3c18736 │ CPUQuotaLimit: 1.000000   │ VolumePlanLimit: map[]   │          │
│ testpod                                                          │ CPUMap: map[1:100]        │                          │          │
│ node1                                                            │ MemoryRequest: 536870912  │                          │          │
│                                                                  │ MemoryLimit: 536870912    │                          │          │
│                                                                  │ StorageRequest: 0         │                          │          │
│                                                                  │ StorageLimit: 0           │                          │          │
│                                                                  │ Privileged: false         │                          │          │
└──────────────────────────────────────────────────────────────────┴───────────────────────────┴──────────────────────────┴──────────┘
```

这时候可以看到 `CPUMap` 里记录了容器绑定的 cpu: `CPUMap: map[1:100]`

1. 无限制的容器

在 eru 资源请求里指定 `0` 即代表无限制, 如

```
root@localhost:~# eru-cli workload deploy --pod testpod --image bash --entry zc --cpu-request 0 --cpu-limit 0 --memory-request 0 --memory-limit 0 ./spec.yaml
INFO[2021-03-15 03:13:07] [Deploy] Success efe21af8d01a526787e73d5d870ad37cd9722647b4cceed9c56ce291d099c91f zc_zc_BweUgb node1 0 0 map[] 0 0 map[] map[]
root@localhost:~# eru-cli workload get efe21af8d01a526787e73d5d870ad37cd9722647b4cceed9c56ce291d099c91f
┌──────────────────────────────────────────────────────────────────┬───────────────────────────┬──────────────────────────┬──────────┐
│ NAME/ID/POD/NODE                                                 │ STATUS                    │ VOLUME                   │ NETWORKS │
├──────────────────────────────────────────────────────────────────┼───────────────────────────┼──────────────────────────┼──────────┤
│ zc_zc_BweUgb                                                     │ CPUQuotaRequest: 0.000000 │ VolumePlanRequest: map[] │          │
│ efe21af8d01a526787e73d5d870ad37cd9722647b4cceed9c56ce291d099c91f │ CPUQuotaLimit: 0.000000   │ VolumePlanLimit: map[]   │          │
│ testpod                                                          │ CPUMap: map[]             │                          │          │
│ node1                                                            │ MemoryRequest: 0          │                          │          │
│                                                                  │ MemoryLimit: 0            │                          │          │
│                                                                  │ StorageRequest: 0         │                          │          │
│                                                                  │ StorageLimit: 0           │                          │          │
│                                                                  │ Privileged: false         │                          │          │
└──────────────────────────────────────────────────────────────────┴───────────────────────────┴──────────────────────────┴──────────┘
```

注意到 request 和 limit 是分离的语义, 所以可以指定 request=0 但是 limit>0, 代表着“不消耗 eru 资源池, 但是在操作系统层面依然做限制”; 或者 request>0 但是 limit=0, 代表“消耗 eru 资源池, 但是不做实际的限制”.

1. 挂载 volume 资源

使用 volume 之前要先注册节点上的 volume 资源.

其实 cpu 和 memory 也需要指定注册, 但是由于是系统指标可以自动采集, 所以不指定的时候默认注册节点上的全部 cpu 和 memory.

```
root@localhost:~# eru-cli pod nodes testpod
┌───────┬──────────────────────┬──────────┬───────────────────────────────┬─────────────┬─────────────┐
│ NAME  │ ENDPOINT             │ CPU      │ MEMORY                        │ VOLUME      │ STORAGE     │
├───────┼──────────────────────┼──────────┼───────────────────────────────┼─────────────┼─────────────┤
│ node1 │ tcp://127.0.0.1:2376 │ 2.00 / 4 │ 1089470464 / 6261624012 bytes │ 0 / 0 bytes │ 0 / 0 bytes │
└───────┴──────────────────────┴──────────┴───────────────────────────────┴─────────────┴─────────────┘
```

这是一开始的节点资源.

```
root@localhost:~# eru-cli node set --delta-volume /data:2G node1
INFO[2021-03-15 03:24:26] [SetNode] set node node1 success
root@localhost:~# eru-cli node get node1
┌───────┬──────────────────────┬──────────┬───────────────────────────────┬──────────────────────┬──────────────────────┐
│ NAME  │ ENDPOINT             │ CPU      │ MEMORY                        │ VOLUME               │ STORAGE              │
├───────┼──────────────────────┼──────────┼───────────────────────────────┼──────────────────────┼──────────────────────┤
│ node1 │ tcp://127.0.0.1:2376 │ 2.00 / 4 │ 1089470464 / 6261624012 bytes │ 0 / 2147483648 bytes │ 0 / 2147483648 bytes │
└───────┴──────────────────────┴──────────┴───────────────────────────────┴──────────────────────┴──────────────────────┘
```

可以看到新加了 2G 的盘 `/data`, `VOLUME` 和 `STORAGE` 都有相应的增加.

然后就可以在接下来的请求里指定 volume 了, 要写在 spec.yaml 里:

```
appname: zc
entrypoints:
  zc:
    cmd: sleep 1000000
volumes:
- /tmp:/tmp
- AUTO:/data2:rw:20000000
volumes_request:
- /tmp:/tmp
- AUTO:/data2:rw:20000000
```

命令行照旧:

```
root@localhost:~# eru-cli workload deploy --pod testpod --image bash --entry zc  ./spec.yaml
INFO[2021-03-15 04:04:16] [Deploy] Success e07fdc0ce44f332e502b3bf77b8f343fe6f5e0fa5f35b04d1f71042bf16a0026 zc_zc_ogOlab node1 1 1 map[] 536870912 536870912 map[] map[]
```

可以看到容器从节点的 `/data` volume 上划分出去里 20,000,000 bytes 的空间给容器, 节点上的资源也能反映出来被使用了这么多:

```
root@localhost:~# eru-cli workload get e07fdc0ce44f332e502b3bf77b8f343fe6f5e0fa5f35b04d1f71042bf16a0026
┌──────────────────────────────────────────────────────────────────┬───────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────┬──────────┐
│ NAME/ID/POD/NODE                                                 │ STATUS                    │ VOLUME                                                                              │ NETWORKS │
├──────────────────────────────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┼──────────┤
│ zc_zc_ogOlab                                                     │ CPUQuotaRequest: 1.000000 │ VolumePlanRequest: map[AUTO:/data2:rw:20000000:volume:{key:"/data" value:20000000}] │          │
│ e07fdc0ce44f332e502b3bf77b8f343fe6f5e0fa5f35b04d1f71042bf16a0026 │ CPUQuotaLimit: 1.000000   │ VolumePlanLimit: map[AUTO:/data2:rw:20000000:volume:{key:"/data" value:20000000}]   │          │
│ testpod                                                          │ CPUMap: map[]             │                                                                                     │          │
│ node1                                                            │ MemoryRequest: 536870912  │                                                                                     │          │
│                                                                  │ MemoryLimit: 536870912    │                                                                                     │          │
│                                                                  │ StorageRequest: 20000000  │                                                                                     │          │
│                                                                  │ StorageLimit: 20000000    │                                                                                     │          │
│                                                                  │ Privileged: false         │                                                                                     │          │
└──────────────────────────────────────────────────────────────────┴───────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────┴──────────┘
root@localhost:~# eru-cli node get node1
┌───────┬──────────────────────┬──────────┬───────────────────────────────┬─────────────────────────────┬─────────────────────────────┐
│ NAME  │ ENDPOINT             │ CPU      │ MEMORY                        │ VOLUME                      │ STORAGE                     │
├───────┼──────────────────────┼──────────┼───────────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ node1 │ tcp://127.0.0.1:2376 │ 3.00 / 4 │ 1626341376 / 6261624012 bytes │ 20000000 / 2147483648 bytes │ 20000000 / 2147483648 bytes │
└───────┴──────────────────────┴──────────┴───────────────────────────────┴─────────────────────────────┴─────────────────────────────┘
```

1. 使用 NUMA

和 Volume 一样, 在使用 NUMA 之前要先注册.

首先查看 NUMA 节点:

```
# numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 20 21 22 23 24 25 26 27 28 29
node 0 size: 64024 MB
node 0 free: 50032 MB
node 1 cpus: 10 11 12 13 14 15 16 17 18 19 30 31 32 33 34 35 36 37 38 39
node 1 size: 64507 MB
node 1 free: 61092 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10
```

然后添加节点的时候需要指定:

```
eru-cli node add --numa-cpu 0,1,2,3,4,5,6,7,8,9,20,21,22,23,24,25,26,27,28,29 --numa-cpu 10,11,12,13,14,15,16,17,18,19,30,31,32,33,34,35,36,37,38,39 --numa-memroy 64024M --numa-memory 64507M
```

NUMA 会影响调度行为, 在计算绑核的时候会保证绑定在同一 NUMA 节点上.


# Update Application

应用是需要更新升级的, 这里总结出几大常用更新/升级场景.

### 更新资源

资源可以参考文档: [资源](https://book.eru.sh/conception/resource)

比如拿 cpu 来说, 从单核扩容到三核, 再缩减到两核, 再扩展到无限制, 可以用以下命令:

```
root@localhost:~# eru-cli workload realloc --cpu-request 2 --cpu-limit 2 fb689b8227fc274538cb7fe9d4ad81562ae3882567acef71f02da768d3c18736
INFO[2021-03-15 06:50:15] [Realloc] Success
root@localhost:~# eru-cli workload get fb689b8227fc274538cb7fe9d4ad81562ae3882567acef71f02da768d3c18736
┌──────────────────────────────────────────────────────────────────┬────────────────────────────────┬──────────────────────────┬──────────┐
│ NAME/ID/POD/NODE                                                 │ STATUS                         │ VOLUME                   │ NETWORKS │
├──────────────────────────────────────────────────────────────────┼────────────────────────────────┼──────────────────────────┼──────────┤
│ zc_zc_sCvwqB                                                     │ CPUQuotaRequest: 3.000000      │ VolumePlanRequest: map[] │          │
│ fb689b8227fc274538cb7fe9d4ad81562ae3882567acef71f02da768d3c18736 │ CPUQuotaLimit: 3.000000        │ VolumePlanLimit: map[]   │          │
│ testpod                                                          │ CPUMap: map[1:100 2:100 3:100] │                          │          │
│ node1                                                            │ MemoryRequest: 536870912       │                          │          │
│                                                                  │ MemoryLimit: 536870912         │                          │          │
│                                                                  │ StorageRequest: 0              │                          │          │
│                                                                  │ StorageLimit: 0                │                          │          │
│                                                                  │ Privileged: false              │                          │          │
└──────────────────────────────────────────────────────────────────┴────────────────────────────────┴──────────────────────────┴──────────┘
```

注意 `realloc` 接口接受的是增量, 因此 `--cpu-request 2` 语义是让容器新增 2 个 cpu 配额, 最终用个 3 cpu.

缩减的话就指定负数:

```
root@localhost:~# eru-cli workload realloc --cpu-request -1 --cpu-limit -1 fb689b8227fc274538cb7fe9d4ad81562ae3882567acef71f02da768d3c18736
```

无限制的话只要把当前的核数减到 0:

```
root@localhost:~# eru-cli workload realloc --cpu-request -2 --cpu-limit -3 fb689b8227fc274538cb7fe9d4ad81562ae3882567acef71f02da768d3c18736
```

也可以在绑核与不绑核之间转换状态:

```
eru-cli workload realloc --cpu-bind fb689b8227fc274538cb7fe9d4ad81562ae3882567acef71f02da768d3c18736
eru-cli workload realloc --cpu-unbind fb689b8227fc274538cb7fe9d4ad81562ae3882567acef71f02da768d3c18736
```

当然其他类型的资源可以如法炮制.

### 更新镜像

更新镜像可以采用 replace 的办法:

```
eru-cli workload replace --pod testpod --image python --entry zc ./spec.yaml
```

这句命令让容器从之前的 `bash` 镜像换到了 `python` 镜像上.

在实际应用上, 一般在发版后升级服务.

注意 replace 命令是不更新资源的, 即使你在请求里指定里新的资源也会被无视.

### 扩容

扩容也是很常见的需求, 我们可以通过 `--count` / `--nodes-limit` / `--deploy-strategy` 来指定.

一个简单的例子, 比如在之前已有一个实例的基础上要再加一个:

```
eru-cli workload deploy --pod testpod --image bash --entry zc --count 2 ./spec.yaml
```

就可以部署第二个容器, 不过依然在同一节点上.

在多节点的情况下 `deploy-strategy` 和 `nodes-limit` 有很重要的作用, 详见 [编排](https://book.eru.sh/conception/scheduling).


# SDN

Eru 目前的 SDN 其实是独立于 eru-core, 而是让各个 runtime 实现各自的 SDN.

### Docker

Eru 提供了基于 Calico 的 fixed-ip CNM 插件: <https://github.com/projecteru2/barrel/>

它要解决的问题是, 一个容器被 restart (stop && start) 前后的 IP 可能会发生变化, 而对于某些业务来说这是不可接受的风险.

因此使用 eru-barrel CNM 插件之后, 一个 fixed-ip 容器流程是这样的:

1. `docker -H unix:///var/run/barrel.sock run -td --name zc2 --net clouddev --label 'fixed-ip=1' bash bash`: 正常创建容器, 只是多加一个 label `fixed-ip=1`
2. `docker exec -it zc2 ip a sh cali0` 显示容器 IP
3. `docker stop -t0 zc2` 停止容器
4. `calicoctl ipam show --ip` 看到 IP 并没有归还给 calico ippool
5. `docker start zc2` 重启后可以检查 IP 没有改变

Barrel 的配置和安装请参考[文档](https://github.com/projecteru2/barrel/wiki/Install-As-Daemon).

### Yavirt

Yavirt 当前的集成 Calico 方案是基于 tun/tap, 不过在不久的将来会拆解到 CNI.

Yavirt 网络设置请参考[文档](https://github.com/projecteru2/white-paper/blob/master/getstarted/TODO/README.md).


# Workload Status

和 K8s 不同, Eru 并不负责 PaaS 的职责, 比方说, K8s 里的 \[Deployment] 和 \[ReplicaSet] 等上层概念将不会由 Eru 提供.

那么如果我们想做类似的工作该怎么办呢?

### Eru Agent and all its friends

在之前的流程中, 我们添加节点只是在 eru-core 里注册了一个 endpoint, 然而这并不是完整的节点部署, 这样部署首当其中的问题是, 我们不知道容器的 IP:

```
root@localhost:~# eru-cli workload get-status cd7c894aa0736bdf1f884115b18416bbeeca4b23e812a986ae2d86b77a332c2c
┌────┬────────────────┬──────────┬────────────┐
│ ID │ STATUS         │ NETWORKS │ EXTENSIONS │
├────┼────────────────┼──────────┼────────────┤
│    │ Running: false │          │            │
│    │ Healthy: false │          │            │
└────┴────────────────┴──────────┴────────────┘
```

这是因为在节点上缺少一个 agent 服务用来上报给 eru-core. 来部署一个单个节点上的:

```
cat <<! > eru-agent.yaml
appname: "eru"
entrypoints:
  agent:
    cmd: "/usr/bin/eru-agent --hostname node1 --config /etc/eru/agent.yaml"
    restart: always
    publish:
      - "12345"
    healthcheck:
      tcp_ports:
        - "12345"
    privileged: true
volumes:
  - /sys:/sys:ro
  - /var/run/docker.sock:/var/run/docker.sock
  - /proc/:/hostProc/
  - /etc/eru:/etc/eru
!

cat <<! > /etc/eru/agent.yaml
pid: /tmp/eru-agent.sock
core: 127.0.0.1:5001

healthcheck:
  interval: 15
  timeout: 10
  status_ttl: 0
  cache_ttl: 300

docker:
  endpoint: unix:///var/run/docker.sock
metrics:
  step: 15
api:
  addr: 0.0.0.0:12345
log:
  forwards:
    - __discard__
  stdout: True

etcd:
  machines:
    - 127.0.0.1:2379
  prefix: /agent-selfmon
!
eru-cli workload deploy --pod testpod --image projecteru2/agent --entry agent eru-agent.yaml
```

然后再去看容器状态就有了:

```
root@localhost:~# eru-cli workload get-status cd7c894aa0736bdf1f884115b18416bbeeca4b23e812a986ae2d86b77a332c2c
┌──────────────────────────────────────────────────────────────────┬───────────────┬────────────────────┬───────────────────────────────────────────────┐
│ ID                                                               │ STATUS        │ NETWORKS           │ EXTENSIONS                                    │
├──────────────────────────────────────────────────────────────────┼───────────────┼────────────────────┼───────────────────────────────────────────────┤
│ cd7c894aa0736bdf1f884115b18416bbeeca4b23e812a986ae2d86b77a332c2c │ Running: true │ bridge: 172.17.0.7 │ ERU: 1                                        │
│                                                                  │ Healthy: true │                    │ ERU_META: {"Publish":null,"HealthCheck":null} │
└──────────────────────────────────────────────────────────────────┴───────────────┴────────────────────┴───────────────────────────────────────────────┘
```

eru-agent 是 eru 对 dockerd 容器 runtime 提供的节点 daemon, 对于不同的 runtime 有不同的服务, 比如 yavirtd 本身自己也包含了这部分健康监控和状态上报的机制.

实例的健康监控目前支持 http 和 tcp, 在 [spec 的文档里](https://book.eru.sh/specs/app) 可以看到详细的配置.

### Workload Status

workload status 里最重要的几条信息, 包括 IP, running status, healthy status, 而这个状态数据是可以通过 eru-core grpc api 去 watch 监控的.

```
rpc WorkloadStatusStream(WorkloadStatusStreamOptions) returns (stream WorkloadStatusStreamMessage) {};

message WorkloadStatusStreamOptions {
    string appname = 1;
    string entrypoint = 2;
    string nodename = 3;
    map<string, string> labels = 4;
}

message WorkloadStatusStreamMessage {
    string id = 1;
    Workload workload = 2;
    WorkloadStatus status = 3;
    string error = 4;
    bool delete = 5;
}
```

考虑实现类似 Kubernetes 的 ReplicaSet 的服务: 保证集群中的实例数目满足请求, 如果有实例下线就立刻新拉起.

ReplicaSet 的功能可以通过一个更上层的服务, 部署服务之后, 调用上述 `WorkloadStatusStream` 接口去 watch 状态, 如果发现实例挂掉就去补充上新实例; 如果旧的下线实例后来又恢复了则把多的实例清除掉.

通过这个机制可以实现高层的 PaaS 服务, 包括灾难恢复, 自动化迁移, 等上层业务系统.

可以参考 [Workload](https://book.eru.sh/conception/workload) 的文档.

健康检查的详细信息在[这里](https://book.eru.sh/conception/healthcheck)


# Check Calico

Dangling Calico WorkloadEndpoints present some WorkloadEndpoints are not allocated to ERU workload. We could look for them via a [scripts/check\_calico.py](https://github.com/projecteru2/core/blob/master/scripts/check_calico.py) and reap them manually.

```bash
$ ./check_calico.py -h
usage: check_calico.py [-h] [-e ERU_ETCD_ENDPOINTS] -p ERU_ETCD_PREFIX

optional arguments:
  -h, --help            show this help message and exit
  -e ERU_ETCD_ENDPOINTS, --eru-etcd-endpoints ERU_ETCD_ENDPOINTS
                        the ERU ETCD endpoints
  -p ERU_ETCD_PREFIX, --eru-etcd-prefix ERU_ETCD_PREFIX
                        the ERU ETCD root prefix
```


