几年前 k8s 就是 openbayes 部署的基础平台了,它算是一个相当不错的 PaaS 了。这里介绍一个最近发现的做 healthcheck 的 operator(我不知道这东西用中文怎么说)。
当我们的子系统越来越多的时候,我们希望每个系统有一些监控用于检测其核心的功能是否跑起来了,并希望知晓每个服务的宕机事件。uptimerobot 就是在做类似的事情。我们可以给它一个 http 访问地址,当访问成功的之后就标记为「健康」,当访问失败的时候就标记为「不健康」。这个检测在指定的周期执行,日积月累就能知道一个服务的稳定情况了。
不过以上所述的健康检查就像是一个 curl https://targeturl
似乎在假设所有的子系统都需要有一个可以方便访问的 http path。不过有些时候,我们会需要一些稍微复杂的健康检查:
这里就推荐一下 kuberhealthy 这个项目。首先它是一个 kubernetes 的 operator 那么只有你在使用 k8s 的时候这个东西才算是有意义的。它有两个我很看中的地方:
更多的可以在这里找到。
在 文档 里做了介绍,简单的说就是这样一个东西:
{"OK": true}
以 POST 发送到 http://$KH_REPORTING_URL
Errors
自然就是具体的出错原因了。{
"Errors": [
"Error 1 here",
"Error 2 here"
],
"OK": false
}
这里我写了一个用来测试 openbayes 的服务是否可用的端到端的健康检查,它主要做如下的事情:
具体的 crd 定义如下:
apiVersion: comcast.github.io/v1
kind: KuberhealthyCheck
metadata:
name: openbayes-gear-check
spec:
runInterval: 6h # 指定多久时间跑一次
timeout: 5m # 指定执行的最长时间
podSpec:
containers:
- env: # 自定义的环境变量
- name: OPENBAYES_TOKEN
value: <THE TOKEN>
- name: ENTRYPOINT
value: <THE ENTRYPOINT>
- name: RUNTIME
value: pytorch-1.7.0
- name: RESOURCE
value: titan_rtx
image: xxxx # 镜像
name: main
容器的 Dockerfile 如下:
FROM ubuntu
RUN apt update
RUN apt install wget curl git -y
RUN wget https://gitee.com/openbayes/bayes-releases/attach_files/606636/download/bayes_amd64.deb
RUN dpkg -i bayes_amd64.deb
WORKDIR /empty
COPY run.sh .
CMD ["/bin/bash", "run.sh"]
只做了两件事:
run.sh
脚本其中 run.sh
如下:
set -e # 任意一行错了就不再继续执行了
bayes upgrade # 更新命令行工具到最新
bayes switch test -e $ENTRYPOINT # 切换 bayes 命令行的上下文
bayes login $OPENBAYES_TOKEN # 登录
bayes gear init healthcheck -m '自动测试项目' # 创建项目
bayes gear run task --env $RUNTIME --resource $RESOURCE -f -- echo 123 # 提交 Python 脚本
curl --location --request POST $KH_REPORTING_URL \
--header 'Content-Type: application/json' \
--data-raw '{ "OK": true }' # 提交成功就告知 kuberhealthy 这次检查是成功的
下面是执行的样子,可以看到这个任务就像是 crd 设置的那个样子,每 6 个小时跑一次。
首先 kuberhealthy 自己有一个汇总页面,可以通过 kubectl port-forward svc/kuberhealthy 8888:80
类似的方式去访问:
{
"OK": true,
"Errors": [],
"CheckDetails": {
"kuberhealthy/dns-status-internal": {
"OK": true,
"Errors": [],
"RunDuration": "3.359431829s",
"Namespace": "kuberhealthy",
"LastRun": "2021-03-01T06:08:09.35793049Z",
"AuthoritativePod": "kuberhealthy-84bf9fd647-2x72f",
"uuid": "1d309ed7-4b7a-4fd5-9706-d04f53c9ed30"
},
"kuberhealthy/namespace-pod-check": {
"OK": true,
"Errors": [],
"RunDuration": "13.589526741s",
"Namespace": "kuberhealthy",
"LastRun": "2021-03-01T05:50:19.584075218Z",
"AuthoritativePod": "kuberhealthy-84bf9fd647-2x72f",
"uuid": "49f7bef3-f5d4-4e25-bf06-acd260d52e27"
},
"kuberhealthy/openbayes-gear-check": {
"OK": true,
"Errors": null,
"RunDuration": "1m8.486351014s",
"Namespace": "kuberhealthy",
"LastRun": "2021-03-01T00:51:14.477191396Z",
"AuthoritativePod": "kuberhealthy-84bf9fd647-2x72f",
"uuid": "c13aea0d-52ca-4777-a6e0-551d0c59962a"
},
"kuberhealthy/openbayes-random-image-serving-check": {
"OK": true,
"Errors": null,
"RunDuration": "9.008124976s",
"Namespace": "kuberhealthy",
"LastRun": "2021-03-01T06:08:15.000563842Z",
"AuthoritativePod": "kuberhealthy-84bf9fd647-2x72f",
"uuid": "eb1341d3-9bbf-44ab-9348-76715464df6b"
},
"kuberhealthy/pod-restarts": {
"OK": true,
"Errors": null,
"RunDuration": "2.903617967s",
"Namespace": "kuberhealthy",
"LastRun": "2021-03-01T06:05:09.40445213Z",
"AuthoritativePod": "kuberhealthy-84bf9fd647-2x72f",
"uuid": "4cf2c122-95ac-4902-8ecf-ee484a54c475"
},
"kuberhealthy/pod-status": {
"OK": true,
"Errors": [],
"RunDuration": "2.845955825s",
"Namespace": "kuberhealthy",
"LastRun": "2021-03-01T06:05:08.84049994Z",
"AuthoritativePod": "kuberhealthy-84bf9fd647-2x72f",
"uuid": "77af9108-720b-4ad4-b8cc-2fe81e751d8d"
}
},
"JobDetails": {},
"CurrentMaster": "kuberhealthy-84bf9fd647-2x72f"
}
但是谁也没空去看这么个 json 的,还是需要和 prometheus 这样的东西集成才行:
scrape_configs:
...
- job_name: kuberhealthy
scrape_interval: 1m
honor_labels: true
metrics_path: /metrics
static_configs:
- targets:
- kuberhealthy.kuberhealthy
然后就可以在 grafana 那边去设置具体的展示了,这里我有一个比较简单的:
具体的集成文档见K8s-KPIs-with-Kuberhealthy.md。
考虑到目前 REST API 层和前端对接起来不是那么顺滑,于是有去找解决方案了,找来找去感觉也就是切 GraphQL 了,这次又来看 GraphQL 感觉它比上次看要香了。好好总觉了一些东西出来。
之前有注意到 GraphQL,毕竟也都好几年了呢,但当时一方面是没有对它有比较充分的理解,没有真切的感受到它的好处,另一方面是其存在诸多对于 REST 的一些误解。
这是我从一个介绍 GraphQL 的材料里找到的例子,例子中提到如果用户想要获取一个 book 列表,每个 book 中还要包含 author 的信息,就很容易出现 N+1 个请求的问题,效率非常低下。如果这也是为什么要有 GraphQL 的原因之一的话,那其实没必要有 GraphQL...
REST 也没有这么死板吧。实际上在返回的每一个 book 中包含 author 信息是非常自然的事情:
GET /books
{
"data": [
{
"id": "1",
"title": "how to graphql",
"description": "bla",
"author": {
"id": "x",
"name": "abc",
"avatar": "http://avatar.com"
}
}
...
]
}
自我一篇很古老的文章里就提到了,GET
请求获取的是「读模型」,应该按照调用方的需求合理的展示相关的数据,以方便调用方的使用。如果像这里说的方法就是大家使用 REST 的方式的话,我觉得前端早就疯了。实际上,这种灵活的聚合展示早就是家常便饭了呢,当然,从标准化的角度来说,这却是也是一个巨大的问题。
TolerantReader 有提到,如果你对于一个请求只关心其一部分信息,那就只处理那部分就好,至于其他的字段,请自行忽略。对于这些额外的字段所带来的额外的数据其实是完全不需要担心的:
不过 REST 也不是没有问题,在 REST 的一些实践心得 也做了一些吐槽。总结来说就是这么几点:
HATEOAS 在用户是最终用户的时候比较有意义,可以支持用户像是在浏览器里面做探索,每个页面有链接指引用户。即使页面发生了变化,只要链接都在就能够支持用户。
可惜对于 API Client 来说,只需要一次性去调用具体某一个接口的,如果每次都从根源去探索 API 效率是非常低下的。这种情况下类似于 OpenAPI 所生成的 SDK 是更方便的。
从目前的交互模式来看,前端就是这么一个 API Client 而已,HATEOAS 对其并没有什么意义。任何 API schema 的修改都可能导致 API Client 的崩溃。
subscription
的概念,将长链接的情况也考虑在内了整体来说,我觉得 GraphQL 是一个工具更健全,规范更完善,生命力更持久的体系。却是克服了 rest 中的一些问题,后面会开始尝试用 GraphQL 对原有 API 做一层封装看看效果。
这篇文章是强行插播过来的,本来是在做 GraphQL 的调研,但是发现其实 REST 这边的很多进展根本就没有说啊,那后面比较的时候很多东西就有点说不清楚了呢。所以就介绍下目前 REST API 开发过程中引入的一些不错的实践,重点是如何 OpenAPI 以及其工具链完善开发流程,降低沟通成本。
就是标题了呢,绝大部分是围绕 openapi 以及其工具链介绍具体解决了什么问题。
众所周知,REST API 可以按照行为分为「读」和「写」两种:
GET
方法POST
PUT
DELETE
PATCH
自我一篇很古老的文章里就提到了,GET
请求获取的是「读模型」(区分于文中所陈述的 DDD 的「写」模型),应该按照使用方需求合理展示相应数据。不过当时对「读模型」讲述的不够深入,实际上 GET
所返回的数据不是孤立的,而是相互关联的。例如有这么一个场景,主要包含两个资源 user
和 blog
,blog
会有 user
作为其 author
。有如下两个 API:
GET /users
返回用户列表GET /blogs
返回博客列表其中 blog
下会包含 author
的信息:
{ // blog
"id": "xxx",
"title": "xxx",
"summary": "xxx",
"tags": ["spring", "rest"],
"author": { // author
"name": "xxx",
"avatar": "http://images.jpg",
"description": "bla bla bla"
}
}
然后可能会有一个用户的列表 GET /users
,里面包含了用户的信息:
{
"name": "xxx",
"avatar": "http://images.jpg",
"description": "bla bla bla"
}
blog 下的 author 和 users 下的每个 user 的结构如果没什么特殊原因应该是一致的,否则前端同学肯定一脸懵逼。这就是所谓的「结构标准化」了。就是 REST GET
的返回结果应该按照 API 服务所提供的资源之间的关系有统一的结构。 这部分和 GraphQL 的感觉有点异曲同工了。
而「写模型」数据通常是让前端去提交某种结果,其所需要的结构是在 requestBody
中。如果硬要提交的结构和返回的结构完全一致的话会有三个问题:
blog
的 author
字段通常都是自动生成的,怎么可能是提交上去的,这就不符合实际业务逻辑比如在 openbayes 创建 job 的 API 里面,需要用户去提交什么样子的资源绑定到什么路径上,如果是规范的提交方式应该是这个样子:
POST /jobs
{
"data_bindings": [
{
"path": "/input0",
"data": {
"id": "abc",
"name": "mnist",
"type": "DATASET",
"owner": {
"id": "userid",
"name": "username",
...
}
...
}
}
]
}
其中大部分数据毫无意义,因为绝大部分信息都是在提供了 userid
+ data-type
+ data-id
之后后端直接在数据库中就查的出来的,所以合理的提交方式应该是以下的样子:
POST /jobs
{
"data_bindings": [
{
"path": "/input0",
"data": {
"userid": "userid",
"type": "DATASET",
"id": "xxx"
}
}
]
}
再举个容忍度的例子,管理员创建一个新的商品:
POST /plans
{
"name": "xxx",
"description": "xxxx",
"price": {
"amount": 1.99,
"currency": "CNY"
}
}
其中这个 price
部分有点点小冗余了,如果可以同时支持以下方式可能会更友好:
POST /plans
{
"name": "xxx",
"description": "xxxx",
"price": "CNY 1.99"
}
这部分我觉得就有点额外发挥的意思了,甚至会画蛇添足。将 API 调用 SDK 化可以极大程度上减少这种额外工作的必要。
API 越写越多,接口越来越复杂,前端调用的时候总要有个文档说说到底每个 API 都返回了啥吧,那么就要用 OpenAPI 了。
OpenAPI 是一套标准,定义了如何用 json schema (的变种)去定义一套 REST API 的接口的。然后围绕这个标准产生了一系列的工具,这一系列的工具的组合一定程度上解决了我们 REST 开发中的诸多痛点。
openapi 就是个 yaml 而且其官方提供的 swagger editor 在这个 yaml 比较大的时候会很卡。干脆就用 intellij 去编辑了,但是我总是知道自己的语法写没写对吧。这个时候我就直接用 redoc 这个东西去渲染结果了,如果展示的对就是正确的了。
然后其实随着 api 的规模的增大,单个 yaml 已经承受不了了。最后分化出了多个 yaml 文档,并对 redoc 提供的 html 做了些许魔改形成了下面的这个样子:
可以看到按照边界分成了 6 个 yaml 然后 openapi 支持每个之间做了相互的数据引用。
这个文档在一定程度上解决了前后端沟通的问题。
openapi 文档会又撰写 api 的人同步的更新,为了保证 openapi 上线前不要各种挂,就引入了 openapi-validator
在 ci 那边跑测试,如果校验没过就报错了呢。openapi-validator
相对比较灵活,有点像是 lint
(巧了,openapi-validator 的命令行工具就叫 openapi-lint
)可以自动的定制一些规则,要求文档的撰写者遵循。
文档写好了,单毕竟就是个 yaml 而已,如何保证实际的代码和 openapi 一致呢?这里就魔改出来一个还算凑合的方案。
首先 restassured
是我们之前就广泛使用的 api 测试框架,然后它其实又一个 JsonSchemaValidator
组件,支持用 jsonschema 去校验请求的内容。然后我有找到一个名为 openapi2schema
的东西,可以把 openapi yaml 转换为 json schema 格式。那么我就可以实现用 openapi 文档去测试实际的代码是不是符合规范了,这里就直接截取项目中的一小段代码:
public void should_get_a_workspace_with_one_dataset_binding() {
String schema = getJsonSchema("api.json", "'/users/{userId}/jobs/{jobId}'.get.responses.200");
// bla bla bla
given()
.header("Authorization", "Bearer " + token)
.contentType(ContentType.JSON)
.when()
.get("/users/{uid}/jobs/{jid}", jobData.getUserId(), jobId)
.prettyPeek()
.then()
.statusCode(200)
.body(JsonSchemaValidator.matchesJsonSchema(schema));
}
其中 api.json
就是利用 openapi2schema
将 openapi 转换为 jsonschema
的结果了。baeldung 有篇 文章 介绍了如何使用 rest assured 的 json schema validator。
最后,也是最重要的一环,为了让 openapi 有灵魂,一定是给前端把它的调用做封装,而不是让前端同学按照文档自己去写调用接口。这里用了 openapi-generator-cli
可以生成众多语言的 sdk,甚至支持 typescript 的 sdk。
这部分在 在 Spring Boot 中使用 HATEOAS 介绍过了。简单的说就是两部分:
吐槽下目前 REST 生态吧。
REST 没什么标准(甚至还会时不时为 REST 是什么而争论),唯一看起来像样的就是 OpenAPI 但是 OpenAPI 的问题也挺多的。
OpenAPI 不是什么大厂,没有很强的运营推广能力,很多东西的贡献也全凭兴趣,持久性和可靠性令人怀疑。比如目前 openbayes 的 API 已经相对比较多了,统计下 openapi 文档的行数已经相当恐怖了。以下几个文件还是自己强行拆分出来的,OpenAPI 自身对于多文件的支持并不是很好,目前的方案也不是很完美,可能需要结合 How to split a large OpenAPI Specification into multiple files 这篇文章的方案做更多的尝试了。
ls *.yml | xargs wc
8499 15129 216144 api.yml
1543 2752 40366 finance.yml
167 286 4375 notifications.yml
823 1432 20210 orgs.yml
1366 2401 35797 servings.yml
1550 2696 37413 users.yml
13948 24696 354305 total
redoc 并不支持完整的 openapi 语法,很多时候校验全凭效果,比如 allOf
oneOf
这些继承语法。正如上文所说,「如果展示的对就是正确的了」。类似的问题在 openapi-generator-cli
也会出现,具体的问题也是千奇百怪。最终结果导致我们只能使用一个 openapi 语法的子集,并且我觉得如果 openapi 没有能力让下面的工具链与自己的标准协同更新,openapi 也就慢慢成了一纸空文。以及 openapi2schema
就更没谱了,就是个完完全全的个人作品,已经一年多没有更新了,后面还能不能用都不知道。
总的来说,上面的工具链有点像是纸糊的,摇摇欲坠,这也是我寻求 GraphQL 破局的重要原因之一吧。