背景
自2014年6月份 Kubernetes 发布第一个版本以来,作为 Google 内部历经多年的分布式容器集群管理生产实践的开源版本,备受业界的广泛关注,并迅速成为容器和云原生技术领域最为热门、使用率最高的开源项目之一。
火山引擎上就有诸多基于 Kubernetes 标准原生方案建设的产品,最典型的也就是我们今天要讲到的容器服务。跟大多数B端业务一样,相较于C端业务最大的区别就是需要有比较垂直且深度的领域业务能力,以及针对多核多能的业务场景的复杂交付能力,这些能力在云产品里尤其突出。
那么如何解决复杂交付且领域深度较高的业务呢,这篇文章就从容器服务实际遇到的一些业务问题及思考和沉淀来给大家做个分享,希望能对大家有所收获,有问题欢迎评论交流。
什么是云原生容器?
首先可以简单认识下容器服务:
容器服务(Volcengine Kubernetes Engine,VKE)通过深度融合新一代云原生技术,提供以容器为核心的高性能 Kubernetes 容器集群管理服务,例如集群生命周期管理、容器网络、应用、存储、安全等管理能力,助力用户快速构建容器化应用。
我们遇到的问题
可以看到容器服务最核心的基础能力就是 K8s,而 K8s 又是一套开源的标准规范和协议,面向标准协议其实前端可以做很多事。如果按传统的开发模式来说后端一般会承载了很多交互级别API研发工作,而且需要反复跟前端对齐,对后端来说其实不需要太关注这一层,可以更多时间去优化研发底层能力、性能等,上层交互级别API更适合前端承接,同时也方便前端同学通过深度参与K8s相关建设从而深入业务,不仅是双赢而且更高效。
怎么解决?
基于上面的业务场景和组织形态,我们跟后端同学协商了一套新的研发模式:前端面向交互开发,后端面向原子能力开发。
听起来还像那么回事,研发模式是升级了,但是接下来的一系列难题就来了,比如:如何快速熟悉并上手 K8s?面对大量微服务如何聚合?如何解决 N+1 问题?选 Node 还是 Go?业务属性重的情况下如何保障性能稳定性?前端如何运维部署 Server 端?通用能力怎么处理?等等。虽然问题很多,但是只要你认准是条正确的路就一定能走下去,持续做正确的事而不是容易的事(有点鸡汤嫌疑)。
下面就会将我们的思考过程和方案一一道来。
先看看整体设计
在介绍细节方案前,可以看下整体设计,有个基础认知。前端在里面主要通过 GraphQL 能力聚合各种微服务及自身提供的 Service,同时提供了一系列鉴权、request库、日志、metrics、限流等通用中间件,而 Service 在模块设计里则是主要负责复杂且领域相关的业务逻辑对接中,其中会直接对接用户集群及 RDS 等,Service 本身的设计充分遵循的 DDD 的领域设计,能够快速移植到 Anywhere,不跟其他模块有所耦合。
可能看了上面的图会觉得有点难嚼,不用担心,下面将会为大家详细介绍我们核心的几个模块设计的思路及所要解决的问题。
承前启后的 GraphQL
我们为什么选择了 GraphQL,而不是 REST
VKE Node Server 的根本定位是 BFF,目的是帮助前端页面开发提效,作为浏览器和其他各种后端服务之间的中间层。在 BFF 架构选型中,我们并没有使用 REST 向浏览器暴露 API,而是选择了 GraphQL,这是为什么呢?下面是一个简要的对比:
总结起来的原因是:团队成员有一定 GraphQL 实践经验,且 VKE 容器服务得前端业务场景长期来看,会比较复杂,选择 GraphQL 可以更好的平衡短期和长期的投入产出比。用短期更高的上手和掌握成本,换来以长期更低的维护和迭代成本,从而整体上低成本呈现 BFF 的核心价值:赋能前端页面开发,同时解放后端开发,让他们有更多时间研究底层问题。
在整个 Node Server 架构中,GraphQL 扮演传统意义上 Controller 的角色,其功能说白了就是聚合和转发其他原子化的接口。但相比传统 Controller,GraphQL 出厂自带的强类型 Schema 和字段级组合能力,可以让浏览器和 BFF 层之间实现更明确、更精细化的合作与分工。在浏览器视角上,BFF 确实是一个真正为自己而存在的 Server;在 BFF 视角上,Server 端确实也不必再老是添加一些看上去与 API 本身无关的琐碎逻辑,只是为了让前端某个交互能更好的完成。换句话讲,BFF 本身并不会解决浏览器需求和传统 REST API 之间的 GAP,只是将这个 GAP 由后端开发转移到前端开发身上而已。如果想真正解决这个 GAP,要么是基于最典型的 REST 逐步引入更多实践规范,要么是一步到位,直接使用 GraphQL 这样一个新的 API 交互规范。前者虽然门栏略低,但实际执行中也更容易变形。后者虽然门槛更高,也更难驾驭,但如果定位在服务于前端开发,而非提供通用的 Open API 上,实际成本会低很多。
随着 VKE 业务近两年的迭代,站在现在来看,选择 GraphQL 确实帮我们更好的应对了分工上和架构上的调整,让我们无须在繁忙的业务迭代中途,投入过多精力改造基础架构,也能始终保持一个较高的开发效率和开发体验。
GraphQL 的职责
GraphQL 本身作为一个中间层,职责非常清晰,就是给前端页面开发提供开箱即用的接口,即聚合多方业务数据到同一个接口,方便前端 Web 开发即取即用。同时相比传统 Restful 风格的 API,前端 Web 侧同时也能轻松做到字段粒度上的控制,可以自己控制一次请求所需要的数据多少,同时也不需要担心一次请求太多信息,导致接口整个挂掉,因为 GraphQL 内部的报错是细化到字段级别,如果一个接口聚合了很多信息,主要信息并不会因为次要信息所在的接口发生故障导致主要信息(往往来自 Open API)也无法显示。
被聚合的接口能力,除了后端和其他第三方 Open API 之外,还包括 BFF 自身 Service 模块所提供的能力,具体内容可参见下面的 Service 章节。
引入 GraphQL 解决的问题
低成本弥合前端复杂页面开发和后端原子化 API 开发之间的 GAP
后端 API 以 open API 作为设计目标,致力于打造「核心、稳定、高可用」的原子化 API,但前端开发页面还是希望一个 API 比较贴合页面逻辑,最好拿来即用。同时,在前端页面上做大量接口调度和聚合相关的逻辑,看似可行,但属于典型的费力不讨好,只管现在不顾将来。GraphQL 可以恰到好处的解决此问题。
前端同学再也不需要无条件担任 bug 中转站了
GraphQL 开箱自带强类型校验,配合 TS 技术栈和相关 codegen 工具,可大大提升前端代码仓库的可维护性,以及联调测试过程中前端开发的实际体验。在联调过程中,再也不会出现问题,首先找前端同学排查了,后端 API 的各种错误,会直接在接口请求层就被暴露,一般不会再进入前端组件内部,造成前端页面崩溃啥的。
经典的 n + 1 问题
在列表页面,经常会遇到根据 ID 获取其他信息的需求,此时往往会出现 n + 1 问题,在 GraphQL 体系下,解决此问题可以通过引入开源 dataloader 能力高效解决。具体可参见下面中间层中关于 dataloader 的相关介绍。
低成本解决大业务接口好用但可能不够稳定的问题
虽然有一个明确的 schema,但我们并没有像 rest 那样自动生成一个开箱即用的 client,而是提供半自动生成能力的辅助,主动权和责任依然在前端开发者身上,这样能保证接口的使用保持在一个最佳性能上,而不是像 rest 那样,随着功能堆积,如果不做特殊处理,接口性能会越来越差。虽然 GraphQL 能做的事情,理论上传统 Controller 也都能做,但一个属于出厂默认自带电池,一个属于业务遇到性能和迭代瓶颈了才往上堆补丁。成本上明显是 GraphQL 更低。
高度复用的中间件
在NodeJS实践中,少不了中间件模块,这里介绍几个核心的中间件以及它解决的问题。
核心模块
#### 权限模块【auth】 | #### 请求模块【axios】 |
---|---|
- 获取用户临时秘钥 |
-
设置用户信息
-
用于判断用户信息是否存在,是否过期 | - 生成 requestLogId
-
小流量 traefik 分流 【forwardEnv】
-
top 参数签名加密
-
配合GraphQL 自定义指令@fetch 使用 | | #### 日志模块【logger】 | #### 聚合性能模块【dataloaders】 | | - 提供符合 业务 日志规范的通用日志库,并且集成 HTTP request logger 日志
-
封装了GraphQl的基础报错记录
-
全局错误封装处理 | - 解决数据请求 n+1 问题 |
解决的一些场景问题
如何解决权限问题
火山涉及的内容板块繁多,为了控制不同的账户只能访问和操作自身业务相关操作,比如容器服务的的密钥只能用来访问容器服务的接口,不能访问 IaaS 的服务。主账号和子账号的访问权限也不一样。如主账号可以查看所有资源,但是子账号却不行。为此基于访问者身份,分别授予不同的权限。
名词释义
名词概念 | 释义 |
---|---|
服务AK&SK | 即访问密钥,包含访问密钥ID(Access Key ID)和秘密访问密钥(Secret Access Key)两部分,通过AK识别用户的身份,通过SK对请求数据进行签名验证 |
STS | Security Token Service的缩写,通过STS服务,可以获取一个自定义时效和访问权限的临时AKSK |
跨服务角色 | 用户只能访问当前服务下接口,如果需要使用其他服务的接口,可通过跨服务角色换取一个自定义访问权限的临时AKSK |
服务请求流程
-
使用VKE服务的 AK&SK 和 LoginToken 进行加密,获取STS
-
使用 STS 和请求参数进行加签,对 Top 服务发起请求
-
Top 服务转发到 VKE 服务
-
返回 VKE 服务响应数据
跨服务角色请求流程
在无登录态下常用的请求流程
-
使用跨服务角色进行请求,需要提前审批设置好用户角色。
-
一个账号可以有多个用户角色,一个用户角色里可以关联多个角色策略。
-
在跨服务请求里,如果需要新增第三方服务请求,我们就需要改ServiceRolePolicyForVKE里的策略。
如何解决请求库问题
GraphQL 帮我们做了各种强类型的定义,但是并没有发起请求的能力,那么这一块如何去解决呢?
基于 axios,我们封装了 axiosRPC 实例,在请求头和请求逻辑上添加了一些通用能力。
通用能力
-
requestLogId:headers 填充 requestId
-
requestBamMock:模拟数据请求
- 通过配置参数 bamGroupIds,如果配置了 svc name 对应的 bam id,那么对应请求会到 bam 平台
-
requestTraefik:小流量 traefik 分流
- 当前主服务会根据请求头 forwardEnv 参数到不同的环境的后端服务上,非主服务根据 serviceForwardEnv.forwardEnv 参数是否跟着主服务环境
配合 GraphQL 使用
我们会将 封装好的 axios 实例挂载在 GraphQL 的上下文上,如下:
css
const { apolloServer } = new GraphQLServerMiddleware({
typesDefPath: path.join(__dirname, './schema/**/*.gql'),
resolversPath: path.join(__dirname, resolversPath),
context({ req, res }: { req: RequestWithRPC; res: Response }) {
return {
// axiosRPC 要事先挂载到 req 上, 因为有其他 rest 请求也要用 req.axiosRPC, 故不在此处挂载, 而在外部挂载
axiosRPC: req.axiosRPC,
httpReq: req,
httpRes: res,
vkeAccountInfo: res.locals.loginInfo,
};
},
});
// resolver 文件
{
Query: {
workloads: async (
_: {},
{ body }: { body: WorkloadsWithoutVciBodyInput },
{ axiosRPC }: { axiosRPC: AxiosInstance }
) => {
const data = await axiosRPC.request<{
Items: { Name: string }[];
Total: number;
}>(
...params
);
return data;
},
},
};
同时,我们也对 axiosRPC 做了指令操作,将其传递给 GraphQL schema,方便我们的使用
less
getSometion(body: getSometionBody!): JSON!
@fetch(svc: "vke", path: "/?Action=XXXXX&Version=2021-03-03")
如何解决聚合查询性能问题
聚合陷阱又名 n+1 问题,可存在于 graphql, 普通的 bff 或 web 侧,对于一个请求,往往内部就包含了特别多额外的其他表的请求,那么我们就需要将这些情况进行优化,整合在一次动作中实现,从而减少数据库的访问。
经典的聚合陷阱
由上图可以知道,我们节点列表里面包含了特别多其他服务【k8s、IaaS、Prom】的数据,那么导致我们对每一个节点都要额外请求接口,去获取角色和规格信息 【数据不在一个表中】。
那么导致我们一行数据,就需要额外请求 3 个接口,如果是 100 行数据 那么就要请求 3*100+1 条数据。
这就是 GraphQL 的 n+1 问题。
那么如何去解决呢?
那就是把每次请求的 id 收集起来,一并去查找信息,就只需要对数据库进行一次请求就可以拿到数据了。这也就是 dataloader 的核心功能。
使用方式,如下图:
创建 Dataloader 实例 loader,传入回调方法 batchListZoneAndRoleOfNode。
csharp
/** 批量请求 */
async function batchListZoneAndRoleOfNode(keys: string[]) {
// get all data with keys in a single request
return [
{
id: '1',
name: 'zone1',
role: 'zone',
},
];
}
/** 单个数据 resolve 时需要的 loader */
const ZoneAndRoleOfNodeLoader = new DataLoader((keys: string[]) =>
batchListZoneAndRoleOfNode(keys)
);
/** 获取单个数据 */
const oneRowDataFromK8s = ZoneAndRoleOfNodeLoader.load('1');
DataLoader 工具
进退自如的 Services
一般将业务逻辑抽象到 service
层管理,保持 controller
层逻辑简洁,增强业务逻辑复用性,也方便对业务逻辑单独编写测试用例。
服务技术演进背景
容器服务(VKE)前期业务快速迭代,OpenAPI 设计比较随意,可扩展性和可维护性不够好,且随着业务规模上量,所有数据都从 OpenAPI 获取,接口压力比较大,稳定性不够好。考虑到这些问题,OpenAPI v2 的整体重新设计,不再面向控制台,接口呈现原子化,非必要数据不再返回,因此前端控制台很多数据无法直接从 OpenAPI 获取,只能通过其他渠道获取,这些渠道包括但不限于 Inner API、RDS、IaaS、K8s、Promethus 等服务获取。
基于上述技术架构的演进,我们抽象了多个Service,包括 kubeconfig 获取服务、K8s API 的查询服务以及基于 Prometheus 的数据查询服务等。
为什么需要关系型数据库
容器服务中集群部分主要信息是从 OpenAPI 获取,对于后端不想暴露的接口,但前端或者其他后端服务又需要的数据,其实后端是可以提供 InnerAPI 的。这里有两个问题:前端为什么还有需要直接对接数据库,以及为什么不全部走数据库,这里其实是综合考虑稳定性、可维护性、系统性能以及前后端分工来来决定的。
当前只有频繁使用集群访问凭证(kubeconfig)等信息直接从数据库中获取的。我们在对接Postgres时采用的是ORM框架,这样对接数据库可以事半功倍,同时后续如果要迁移其他数据库,可以无缝迁移。。
数据库 ORM 框架 Sequelize 简介
Sequelize 是一个基于 TypeScript 的 Node.js ORM 框架,目前支持 Oracle、Postgres、MySQL、MariaDB、SQLite 以及 Microsoft SQL Server。它具有强大的事务支持、关联关系、预读和延迟加载、读取复制等功能。
模型(Model)是 sequelize 的本质。模型是表示数据库中表的抽象。在 sequelize 中,它是一个扩展 Model 的类。
Sequelize 提供了 2 中定义 Model 的方法:
-
调用
sequelize.define(modelName, attributes, options)
方法; -
继承 Model ,然后调用
init(attributes, options)
方法;
如何从数据库获取集群访问凭证(kubeconfig)
未来前端很多信息频繁从集群 apiserver 获取,因此首先需要获取集群访问凭证(kubeconfig)使用 Sequelize 从关系型数据库(RDS) 查询 kubeconfig 信息(以 pg 为例)示例如下:先定义 kubeconfig 的 Model,然后使用结构化查询,具体如下:
csharp
// 使用文档: https://sequelize.org/docs/v6/core-concepts/model-basics/
import { Sequelize, DataTypes, Model, Op } from 'sequelize';
// app 初始化时初始化实例
const sequelize = new Sequelize(...);
// 查询 kubeconfig 文件
async function getKubeconfig(params?: { AccountId: string; ClusterId: string; }) {
const { AccountId, ClusterId } = params || {};
const KubeconfigAdmin = sequelize.define('xxx表名', {
// Model 属性定义
kubeconfig: {
type: DataTypes.TEXT,
},
}, {
// 其他配置参数
});
// kubeconfig 查询
const data = await KubeconfigAdmin.findOne({
where: {
cluster_id: ClusterId,
account_id: AccountId,
deleted_at: {
[Op.eq]: null
}
}
});
return data?.get()
}
实际场景中 kubeconfig 的获取会更复杂,需要考虑多云差异、接口兜底等逻辑,确保能稳定获取到 kubeconfig ,因为 kubeconfig 是访问集群的凭证,没有 kubeconfig 无法进行后续 k8s 信息的获取和相关的操作。
为什么需要对接 Kubernetes API
OpenAPI v2 设计上除了原子化外,还有另外一个特点,那就是只提供了集群部分(集群、节点、节点池、组件)的 OpenAPI 。那为什么不提供集群内应用的 OpenAPI 呢?原因就是容器服务核心是 K8s,K8s 本身提供了一套自己的 OpenAPI,在获取集群访问凭证后,完全可以管理集群内所有的资源;其次后端架构上引入了 RDS,K8s 信息和 RDS 之间会频繁同步信息,确保信息一致性,为了保证整体系统性能,RDS 中仅存储了必要的信息,比如节点的标签、污点等信息无法直接通过接口获取。
Kubernetes API 是什么
Kubernetes 控制面 的核心是 API 服务器。API 服务器负责提供 HTTP API,以供用户、集群中的不同部分和集群外部组件相互通信。Kubernetes API 使你可以查询和操纵 Kubernetes API 中对象(例如:Pod、Namespace、ConfigMap 和 Event)。
我们常通过 kubectl 命令行接口来发起 Kubernetes API 的请求。程序中我们可以通过直接调用 HTTP 接口或使用官方提供 SDK 发起接口请求。
如何使用 Kubernetes API
K8s 官方提供了多种语言的 SDK,前端使用 Node SDK,通过加载获取的 kubeconfig 文件,然后调用封装的 kubernetes API 函数,即可获取资源信息,更新资源等操作。
🌰 举个例子,获取 k8s 核心 api 组实例:
typescript
import * as k8s from '@kubernetes/client-node';
async function getK8sClient<T extends k8s.ApiType>(
ClusterId: string
): Promise<{ k8sApi: T; kc: k8s.KubeConfig }> {
const kubeconfig = await getKubeConfig(ClusterId);
const kc = new k8s.KubeConfig();
kc.loadFromString(kubeconfig);
const k8sApi = kc.makeApiClient((k8s.CoreV1Api as unknown) as ApiConstructor<T>);
return { k8sApi, kc };
}
节点信息查询和操作
获取 kubeconfig 并了解 kubernetes API 的使用后,我们就可以对 k8s 的资源进行访问和操作。
前面说到 OpenAPI 提供节点基本信息获取,但是节点上的标签(labels)、污点(taints)以及系统信息、容器运行时版本、kubelet 版本、IP 地址等信息直接从 k8s 的 node 上获取。
🌰 举个例子,获取节点信息 如下:
typescript
import * as k8s from '@kubernetes/client-node';
async function listNode({ Ids, ClusterId }: { Ids: string[]; ClusterId: string }) {
// 待拆解model
if (typeof Ids === 'string') Ids = JSON.parse(Ids);
const { k8sApi } = await getClient({ ClusterId });
const nodes = await k8sApi.listNode(
undefined,
undefined,
undefined,
undefined,
`cluster.vke.volcengine.com/machine-name in (${Ids.join(',')})`
);
return nodes?.body?.items.map(({ spec, status, metadata }) => {
return {
Id: metadata?.labels?.['cluster.vke.volcengine.com/machine-name'],
Name: metadata?.name,
Labels: metadata?.labels,
Taints: spec?.taints,
}
});
}
对接 OpenAPI v2 后,节点封锁、取消封锁、下线等操作也是前端 BFF 直接调用 K8s API 完成,这里可以思考一个问题这些操作具体做了哪些事情呢?
其实节点封锁、取消封锁主要是修改节点的 spec.unschedulable
状态,而节点下线除了修改节点的 spec.unschedulable
状态,同时驱逐所有的非 DaemonSet
的 pod。下面是驱逐节点上某个 pod 的示例 🌰:
csharp
import * as k8s from '@kubernetes/client-node';
async function createNamespacedPodEviction(params?: { name: string; namespace: string; clusterId: string}) {
const { name, namespace, clusterId } = params || {};
const { k8sApi } = await getClient({ ClusterId: clusterId });
const body: k8s.V1Eviction = {
apiVersion: 'policy/v1',
kind: 'Eviction',
metadata: { name, namespace },
};
const ret = await k8sApi.createNamespacedPodEviction(name, namespace, body);
}
如何控制集群资源的访问权限
通过 RDS 或者 API 获取集群访问凭证(kubeconfig)后,原则上可以通过 kubeconfig 调用 Kubernetes API 查看或者操作集群内所有资源,比如节点、命名空间、工作负载、服务、存储、配置等,那不同用户对资源访问权限又是如何控制的呢?下面我们来看看。
K8s 的 ABAC、RBAC
ABAC ****基于属性的权限控制,通过一个静态文件来声明权限授予,需要直接写死给哪个用户或者组分配什么权限,不太灵活,已经不推荐使用了,一个例子如下:
json
{
"apiVersion": "abac.authorization.kubernetes.io/v1beta1",
"kind": "Policy",
"spec": {
"user": "test-admin",
"namespace": "test",
"resource": "*",
// APIgroup就是apiversion中的一部分,apiversion = Apigroup/version
"apiGroup": "*"
}
}
RBAC 是一种基于组织中用户的角色来调节控制对计算机或网络资源的访问的方法。
RBAC 在 ABAC 的基础上,将要被授权的属性抽象出了一层角色,用户不再直接与资源建立联系,而是先将一系列资源绑定到角色上,用户再与角色建立联系。
Kubernetes 通过 RBAC 对用户请求进行鉴权,请参阅使用 RBAC 鉴权。容器服务(VKE)使用预置角色或自定义角色对目标用户进行授权。
RBAC 鉴权机制使用 rbac.authorization.k8s.io
API 组来驱动鉴权决定, 允许你通过 Kubernetes API 动态配置策略。
RBAC API 声明了四种 Kubernetes 对象:Role 、ClusterRole 、RoleBinding 和 ClusterRoleBinding。
Role 或 ClusterRole 声明的对象表示角色,而 RoleBinding ****或 ****ClusterRoleBinding ****声明的对象表示角色绑定 (Role Binding),即把某个权限赋予给某个用户或用户组。Role 和 RoleBinding ****是命名空间级别的,比如指定命名空间;ClusterRole 和 ClusterRoleBinding ****是集群作用域的资源。具体关系如图:
用户的权限如何与请求关联
前面了解了如何获取 kubeconfig,如何使用 kubeconfig 访问集群,以及如何设置用户 RBAC 权限。如果直接使用 kubeconfig 访问是没有关联 k8s RBAC 权限的,那具体用户的 RBAC 权限如何与某个请求关联呢?子用户如何获取权限外的数据,比如命名空间、授权信息呢?
这些问题的核心其实是 用户扮演(User impersonation) 。用户扮演是通过添加 HTTP 请求头扮演另外一个用户。用户扮演首先验证为请求用户,然后切换到扮演的用户信息。比如当前登录的子账号,此时只要扮演成这个子用户,那么请求的权限就按照扮演的子用户权限验证。
可用于用户扮演的 HTTP 请求头包括:
Impersonate-User
:扮演的用户的用户名Impersonate-Group
:扮演的用户组,多个值(出现多次)表示多个组,需要同时指定Impersonate-User
Impersonate-Extra-<extra name>
:动态指定的 key,用于指定用户的其他信息,需要同时指定Impersonate-User
Impersonate-Uid
:被扮演的用户的唯一标志符,需同时指定Impersonate-User
。
更多用户扮演详细使用,请参考 kubernetes 用户扮演 API。
🌰 举个例子:
kotlin
function addK8sUserHeaders(k8sApi: k8s.ApiType, ForceAdmin?: boolean) {
const userId = this.ctx.loginInfo.User?.Id;
const {
Account: { Id: accountId },
} = this.ctx.loginInfo;
if (userId && userId > 0 && !ForceAdmin) {
k8sApi.defaultHeaders['Impersonate-User'] = userId;
} else {
k8sApi.defaultHeaders['Impersonate-User'] = accountId;
k8sApi.defaultHeaders['Impersonate-Group'] = 'system:masters';
}
return k8sApi;
}
为什么需要 Promethus 服务
OpenAPI v1 中资源数据由 OpenAPI 接口返回,而接口从 apiserver 获取,一方面频繁从 apiserver 取资源数据增加了 apiserver 的负担,不利于系统稳定性,另外一方面资源数据只是前端控制台页面展示需要,对于 OpenAPI 用户大部分情况不需要,因此 OpenAPI v2 改造后,OpenAPI 不在返回资源相关的数据。
为了减小 apiserver 负担,容器服务单独部署了一套 VictoriaMetrics 的集群(VM 集群,与 Promethus 兼容,未来会切换为火山托管 Promethus 服务),每个 PaaS 元集群部署一个 vm-operator
和 vm-agent
异步采集数据到 VM 集群,前端 BFF 层使用 PromQL 语句从 VM 集群查询指标数据。
Prometheus 是什么
Prometheus(译: 普罗米修斯)是一个开源的系统监控告警解决方案。Prometheus 于 2016 年加入CNCF,成为继 K8s 之后的第 2个托管项目。
PromQL(Prometheus Query Language)是 Prometheus 提供的一种功能性查询语言,可以让用户实时选择和聚合时间序列数据。表达式的结果既可以显示为图形,也可以在 Prometheus 的表达式浏览器中以表格数据的形式显示,或者由外部系统通过 HTTP API 使用。
更多 Prometheus 介绍可以查看官网。
如何使用 Prometheus 查询数据
Prometheus 服务本质就是提供了 HTTP 的查询和写入地址,后端采集指标数据,前端通过 HTTP 请求查询指标数据,具体指标数据还同时包含不同的标签来唯一确认。
VKE 中使用 prometheus-query
库(其实也可以不需要),对应 Prometheus 的 instant queries(时刻数据查询) 和 range queries(时间段数据查询) 提供了 instantQuery
和 rangeQuery
方法。
🌰 举个例子,比如获取 TotalCpu
:
csharp
import { PrometheusDriver, QueryResult } from 'prometheus-query';
function getTotalCpu() {
const endpoint = process.env.PROM_ENDPOINT || '';
const baseURL = process.env.PROM_BASE_URL || '';
const prom = new PrometheusDriver({ endpoint, baseURL, timeout: 3000 });
// TotalCpu 的 promQL 语句
const TotalCpuQuery = `sum(kube_node_status_capacity{cluster="ccdqq56fqtofg4u0b5oh0", resource="cpu"}*on(cluster, node) max by (cluster, node) (kube_node_labels{cluster="ccdqq56fqtofg4u0b5oh0",label_node_kubernetes_io_instance_type!="virtual-node"}) ) by (cluster)`;
return await prom.instantQuery(query).then((res) => {
// 解析具体的值
const { metric, value } = item || {};
const { labels } = metric || {};
const { cluster, node } = labels || {};
return value.value || 0;
});
}
以上是容器服务前端团队用NodeJS在业务的一些实践分享,欢迎有问题有想法的同学来一起交流。