跳转至

DevOps 面试题(山月)

当新入职一家公司时,如何快速搭建开发环境并让应用跑起来

原文:https://q.shanyue.tech/devops/devops/9.html

Issue

欢迎在 Gtihub Issue 中回答此问题: Issue 9(opens new window)

Author

回答者: shfshanyue(opens new window)

新人入职新上手项目,如何把它跑起来,这是所有人都会碰到的问题:所有人都是从新手开始的。

有可能你会脱口而出:npm run dev/npm start,但实际工作中,处处藏坑,往往没这么简单。

  1. 查看是否有 CI/CD,如果有跟着 CI/CD 部署的脚本跑命令
  2. 查看是否有 dockerfile,如果有跟着 dockerfile 跑命令
  3. 查看 npm scripts 中是否有 dev/start,尝试 npm run dev/npm start
  4. 查看是否有文档,如果有跟着文档走。为啥要把文档放到最后一个?原因你懂的

但即便是十分谨慎,也有可能遇到以下几个叫苦不迭、浪费了一下午时间的坑:

  1. 前端有可能在本地环境启动时需要依赖前端构建时所产生的文件,所以有时需要先正常部署一遍,再试着按照本地环境启动 (即需要先 npm run build 一下,再 npm run dev/npm start)。(比如,一次我们的项目 npm run dev 时需要 webpack DllPlugin 构建后的东西)
  2. 别忘了设置环境变量或者配置文件 (.env/consul/k8s-configmap)

因此,设置一个少的 script,可以很好地避免后人踩坑,更重要的是,可以避免后人骂你,

此时可设置 script hooks,如 preparepostinstall 自动执行脚本,来完善该项目的基础设施

{
  "scripts": {
    "start": "npm run dev",
    "config": "node assets && node config",
    "build": "webpack",
    // 设置一个钩子,在 npm install 后自动执行,此处有可能不是必须的
    "prepare": "npm run build",
    "dev": "webpack-dev-server --inline --progress"
  }
} 

npm run dev 与 npm start 的区别

对于一个纯生成静态页面打包的前端项目而言,它们是没有多少区别的:生产环境的部署只依赖于构建生成的资源,更不依赖 npm scripts。可见 如何部署前端项目(opens new window)

使用 create-react-app 生成的项目,它的 npm script 中只有 npm start

{
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
} 

使用 vuepress 生成的项目,它的 npm script 中只有 npm run dev

{
  "dev": "vuepress dev",
  "build": "vuepress build"
} 

在一个面向服务端的项目中,如 nextnuxtnest。dev 与 start 的区别趋于明显,一个为生产环境,一个为开发环境

  • dev: 在开发环境启动项目,一般带有 watch 选项,监听文件变化而重启服务,此时会耗费大量的 CPU 性能,不宜放在生产环境
  • start: 在生产环境启动项目

nest 项目中进行配置

{
  "start": "nest start",
  "dev": "nest start --watch"
} 

Author

回答者: xwxgjs(opens new window)

我的意见和楼上相反,应该先大概看一遍文档…… 文档中会描述本地环境的配置方法

查看是否有 CI/CD,如果有跟着 CI/CD 部署的脚本跑命令
查看是否有 dockerfile,如果有跟着 dockerfile 跑命令
查看 npm scripts 中是否有 dev/start,尝试 npm run dev/npm start 

大部分公司的开发环境都是本地环境,所以什么 CI/CD、Docker 可以先放到一边

npm run dev/npm start 这个是一般的约定,但不是所有的项目都是这样。所以需要先看 package.json 中的 script 来确定

npm run dev 和 npm start 的区别?

  1. npm start 是 npm run start 的别名,支持 prestart 和 poststart 钩子

Author

回答者: linlai163(opens new window)

我的意见和楼上相反,应该先大概看一遍文档…… 文档中会描述本地环境的配置方法

查看是否有 CI/CD,如果有跟着 CI/CD 部署的脚本跑命令 查看是否有 dockerfile,如果有跟着 dockerfile 跑命令 查看 npm scripts 中是否有 dev/start,尝试 npm run dev/npm start

大部分公司的开发环境都是本地环境,所以什么 CI/CD、Docker 可以先放到一边

npm run dev/npm start 这个是一般的约定,但不是所有的项目都是这样。所以需要先看 package.json 中的 script 来确定

npm run dev 和 npm start 的区别?

  1. npm start 是 npm run start 的别名,支持 prestart 和 poststart 钩子

你是真没吃过文档的亏。。。管他什么公司,文档都有坑。

你们的前端项目是如何在线上部署的

原文:https://q.shanyue.tech/devops/devops/16.html

Issue

欢迎在 Gtihub Issue 中回答此问题: Issue 16(opens new window)

Author

回答者: shfshanyue(opens new window)

今天正好写了一篇长文来回答这个问题

前端一说起刀耕火种,那肯定紧随着前端工程化这一话题。随着 react/vue/angulares6+webpackbabeltypescript 以及 node 的发展,前端已经在逐渐替代过去 script 引 cdn 开发的方式了,掀起了工程化这一大浪潮。得益于工程化的发展与开源社区的良好生态,前端应用的可用性与效率得到了很大提高。

前端以前是刀耕火种,那前端应用部署在以前也是刀耕火种。那前端应用部署的发展得益于什么,随前端工程化带来的副产品?

这只是一部分,而更重要的原因是 devops 的崛起。

为了更清晰地理解前端部署的发展史,了解部署时运维和前端(或者更广泛地说,业务开发人员)的职责划分,当每次前端部署发生改变时,可以思考两个问题

  1. 缓存,前端应用中 http 的 response header 由谁来配?得益于工程化发展,可以对打包后得到带有 hash 值的文件可以做永久缓存
  2. 跨域,/api 的代理配置由谁来配?在开发环境前端可以开个小服务,启用 webpack-dev-server 配置跨域,那生产环境呢

这两个问题都是前端面试时的高频问题,但话语权是否掌握在前端手里

时间来到 React 刚刚发展起来的这一年,这时已经使用 React 开发应用,使用 webpack 来打包。但是前端部署,仍是刀耕火种

刀耕火种

一台跳板机

一台生产环境服务器

一份部署脚本

前端调着他的 webpack,开心地给运维发了部署邮件并附了一份部署脚本,想着第一次不用套后端的模板,第一次前端可以独立部署。想着自己基础盘进一步扩大,前端不禁开心地笑了

运维照着着前端发过来的部署邮件,一遍又一遍地拉着代码,改着配置,写着 try_files, 配着 proxy_pass

这时候,前端静态文件由 nginx 托管,nginx 配置文件大致长这个样子

server {
  listen 80;
  server_name shanyue.tech;

  location / {
    # 避免非root路径404
    try_files $uri $uri/ /index.html;
  }

  # 解决跨域
  location /api {
    proxy_pass http://api.shanyue.tech;
  }

  # 为带 hash 值的文件配置永久缓存
  location ~* \.(?:css|js)$ {
      try_files $uri =404;
      expires 1y;
      add_header Cache-Control "public";
  }

  location ~ ^.+\..+$ {
      try_files $uri =404;
  }
} 

不过...经常有时候跑不起来

运维抱怨着前端的部署脚本没有标好 node 版本,前端嚷嚷着测试环境没问题

这个时候运维需要费很多心力放在部署上,甚至测试环境的部署上,前端也要操心放在运维如何部署上。这个时候由于怕影响线上环境,上线往往选择在深夜,前端和运维身心俱疲

不过向来如此

鲁迅说,向来如此,那便对么。

这个时候,无论跨域的配置还是缓存的配置,都是运维来管理,运维不懂前端。但配置方式却是前端在提供,而前端并不熟悉 nginx

使用 docker 构建镜像

docker 的引进,很大程度地解决了部署脚本跑不了这个大 BUG。dockerfile 即部署脚本,部署脚本即 dockerfile。也很大程度缓解了前端运维的摩擦,毕竟前端越来越靠谱了,至少部署脚本没有问题了 (笑

这时候,前端不再提供静态资源,而是提供服务,一个 http 服务

前端写的 dockerfile 大致长这个样子

FROM node:alpine

# 代表生产环境
ENV PROJECT_ENV production
# 许多 package 会根据此环境变量,做出不同的行为
# 另外,在 webpack 中打包也会根据此环境变量做出优化,但是 create-react-app 在打包时会写死该环境变量
ENV NODE_ENV production
WORKDIR /code
ADD . /code
RUN npm install && npm run build && npm install -g http-server
EXPOSE 80

CMD http-server ./public -p 80 

单单有 dockerfile 也跑不起来,另外前端也开始维护一个 docker-compose.yaml,交给运维执行命令 docker-compose up -d 启动前端应用。前端第一次写 dockerfiledocker-compose.yaml,在部署流程中扮演的角色越来越重要。想着自己基础盘进一步扩大,前端不禁开心地笑了

version: "3"
services:
  shici:
    build: .
    expose:
      - 80 

运维的 nginx 配置文件大致长这个样子

server {
  listen 80;
  server_name shanyue.tech;

  location / {
    proxy_pass http://static.shanyue.tech;
  }

  location /api {
    proxy_pass http://api.shanyue.tech;
  }
} 

运维除了配置 nginx 之外,还要执行一个命令: docker-compose up -d

这时候再思考文章最前面两个问题

  1. 缓存,由于从静态文件转换为服务,缓存开始交由前端控制 (但是镜像中的 http-server 不太适合做这件事情)
  2. 跨域,跨域仍由运维在 nginx 中配置

前端可以做他应该做的事情中的一部分了,这是一件令人开心的事情

当然,前端对于 dockerfile 的改进也是一个慢慢演进的过程,那这个时候镜像有什么问题呢?

  1. 构建镜像体积过大
  2. 构建镜像时间过长

使用多阶段构建优化镜像

这中间其实经历了不少坎坷,其中过程如何,详见我的另一篇文章: 如何使用 docker 部署前端应用(opens new window)

其中主要的优化也是在上述所提到的两个方面

  1. 构建镜像体积由 1G+ 变为 10M+
  2. 构建镜像时间由 5min+ 变为 1min (视项目复杂程度,大部分时间在构建时间与上传静态资源时间)
FROM node:alpine as builder

ENV PROJECT_ENV production
ENV NODE_ENV production

WORKDIR /code

ADD package.json /code
RUN npm install --production

ADD . /code

# npm run uploadCdn 是把静态资源上传至 oss 上的脚本文件,将来会使用 cdn 对 oss 加速
RUN npm run build && npm run uploadCdn

# 选择更小体积的基础镜像
FROM nginx:alpine
COPY --from=builder code/public/index.html code/public/favicon.ico /usr/share/nginx/html/
COPY --from=builder code/public/static /usr/share/nginx/html/static 

那它怎么做的

  1. ADD package.json /code, 再 npm install --production 之后 Add 所有文件。充分利用镜像缓存,减少构建时间
  2. 多阶段构建,大大减小镜像体积

另外还可以有一些小优化,如

  • npm cache 的基础镜像或者 npm 私有仓库,减少 npm install 时间,减小构建时间
  • npm install --production 只装必要的包

前端看着自己优化的 dockerfile,想着前几天还被运维吵,说什么磁盘一半的空间都被前端的镜像给占了,想着自己节省了前端镜像几个数量级的体积,为公司好像省了不少服务器的开销,想着自己的基础盘进一步扩大,不禁开心的笑了

这时候再思考文章最前面两个问题

  1. 缓存,缓存由前端控制,缓存在 oss 上设置,将会使用 cdn 对 oss 加速。此时缓存由前端写脚本控制
  2. 跨域,跨域仍由运维在 nginx 中配置

CI/CD 与 gitlab

此时前端成就感爆棚,运维呢?运维还在一遍一遍地上线,重复着一遍又一遍的部署三个动作

  1. 拉代码
  2. docker-compose up -d
  3. 重启 nginx

运维觉得再也不能这么下去了,于是他引进了 CI: 与现有代码仓库 gitlab 配套的 gitlab ci

  • CIContinuous Integration,持续集成
  • CDContinuous Delivery,持续交付

重要的不是 CI/CD 是什么,重要的是现在运维不用跟着业务上线走了,不需要一直盯着前端部署了。这些都是 CI/CD 的事情了,它被用来做自动化部署。上述提到的三件事交给了 CI/CD

.gitlab-ci.ymlgitlab 的 CI 配置文件,它大概长这个样子

deploy:
  stage: deploy
  only:
    - master
  script:
    - docker-compose up --build -d
  tags:
    - shell 

CI/CD 不仅仅更解放了业务项目的部署,也在交付之前大大加强了业务代码的质量,它可以用来 linttestpackage 安全检查,甚至多特性多环境部署,我将会在我以后的文章将这部分事情

我的一个服务器渲染项目 shfshanyue/shici(opens new window) 以前在我的服务器中就是以 docker/docker-compose/gitlab-ci 的方式部署,有兴趣的可以看看它的配置文件

如果你有个人服务器的话,也建议你做一个自己感兴趣的前端应用和配套的后端接口服务,并且配套 CI/CD 把它部署在自己的自己服务器上

而你如果希望结合 githubCI/CD,那可以试一试 github + github action

另外,也可以试试 drone.ci,如何部署可以参考我以前的文章: github 上持续集成方案 drone 的简介及部署(opens new window)

使用 kubernetes 部署

随着业务越来越大,镜像越来越多,docker-compose 已经不太能应付,kubernetes 应时而出。这时服务器也从 1 台变成了多台,多台服务器就会有分布式问题

一门新技术的出现,在解决以前问题的同时也会引进复杂性。

k8s 部署的好处很明显: 健康检查,滚动升级,弹性扩容,快速回滚,资源限制,完善的监控等等

那现在遇到的新问题是什么?

构建镜像的服务器,提供容器服务的服务器,做持续集成的服务器是一台!

需要一个私有的镜像仓库,这是运维的事情,harbor 很快就被运维搭建好了,但是对于前端部署来说,复杂性又提高了

先来看看以前的流程:

  1. 前端配置 dockerfiledocker-compose
  2. 生产环境服务器的 CI runner 拉代码(可以看做以前的运维),docker-compose up -d 启动服务。然后再重启 nginx,做反向代理,对外提供服务

以前的流程有一个问题: 构建镜像的服务器,提供容器服务的服务器,做持续集成的服务器是一台!,所以需要一个私有的镜像仓库,一个能够访问 k8s 集群的持续集成服务器

流程改进之后结合 k8s 的流程如下

  1. 前端配置 dockerfile,构建镜像,推到镜像仓库
  2. 运维为前端应用配置 k8s 的资源配置文件,kubectl apply -f 时会重新拉取镜像,部署资源

运维问前端,需不需要再扩大下你的基础盘,写一写前端的 k8s 资源配置文件,并且列了几篇文章

前端看了看后端十几个 k8s 配置文件之后,摇摇头说算了算了

这个时候,gitlab-ci.yaml 差不多长这个样子,配置文件的权限由运维一人管理

deploy:
  stage: deploy
  only:
    - master
  script:
    - docker build -t harbor.shanyue.tech/fe/shanyue
    - docker push harbor.shanyue.tech/fe/shanyue
    - kubectl apply -f https://k8s-config.default.svc.cluster.local/shanyue.yaml
  tags:
    - shell 

这时候再思考文章最前面两个问题

  1. 缓存,缓存由前端控制
  2. 跨域,跨域仍由运维控制,在后端 k8s 资源的配置文件中控制 Ingress

使用 helm 部署

这时前端与运维已不太往来,除了偶尔新起项目需要运维帮个忙以外

但好景不长,突然有一天,前端发现自己连个环境变量都没法传!于是经常找运维修改配置文件,运维也不胜其烦

于是有了 helm,如果用一句话解释它,那它就是一个带有模板功能的 k8s 资源配置文件。作为前端,你只需要填参数。更多详细的内容可以参考我以前的文章 使用 helm 部署 k8s 资源(opens new window)

假如我们使用 bitnami/nginx(opens new window) 作为 helm chart,前端可能写的配置文件长这个样子

image:
  registry: harbor.shanyue.tech
  repository: fe/shanyue
  tag: 8a9ac0

ingress:
  enabled: true
  hosts:
    - name: shanyue.tech
      path: /

  tls:
    - hosts:
        - shanyue.tech
      secretName: shanyue-tls

      # livenessProbe:
      #   httpGet:
      #     path: /
      #     port: http
      #   initialDelaySeconds: 30
      #   timeoutSeconds: 5
      #   failureThreshold: 6
      #
      # readinessProbe:
      #   httpGet:
      #     path: /
      #     port: http
      #   initialDelaySeconds: 5
      #   timeoutSeconds: 3
      #   periodSeconds: 5 

这时候再思考文章最前面两个问题

  1. 缓存,缓存由前端控制
  2. 跨域,跨域由后端控制,配置在后端 Chart 的配置文件 values.yaml

到了这时前端和运维的职责所在呢?

前端需要做的事情有:

  1. 写前端构建的 dockerfile,这只是一次性的工作,而且有了参考
  2. 使用 helm 部署时指定参数

那运维要做的事情呢

  1. 提供一个供所有前端项目使用的 helm chart,甚至不用提供,如果运维比较懒那就就使用 bitnami/nginx(opens new window) 吧。也是一次性工作
  2. 提供一个基于 helm 的工具,禁止业务过多的权限,甚至不用提供,如果运维比较懒那就直接使用 helm

这时前端可以关注于自己的业务,运维可以关注于自己的云原生,职责划分从未这般清楚

统一前端部署平台

后来运维觉得前端应用的本质是一堆静态文件,较为单一,容易统一化,来避免各个前端镜像质量的参差不齐。于是运维准备了一个统一的 node 基础镜像

前端再也不需要构建镜像,上传 CDN 了,他只需要写一份配置文件就可以了,大致长这个样子

build:
  command: npm run build
  dist: /dist

hosts:
  - name: shanyue.tech
    path: /

headers:
  - location: /*
    values:
      - cache-control: max-age=7200
  - location: assets/*
    values:
      - cache-control: max-age=31536000

redirects:
  - from: /api
    to: https://api.shanyue.tech
    status: 200 

此时,前端只需要写一份配置文件,就可以配置缓存,配置 proxy,做应该属于前端做的一切,而运维也再也不需要操心前端部署的事情了

前端看着自己刚刚写好的配置文件,怅然若失的样子...

不过一般只有大厂会有这么完善的前端部署平台,如果你对它有兴趣,你可以尝试下 netlify,可以参考我的文章: 使用 netlify 部署你的前端应用(opens new window)

服务端渲染与后端部署

大部分前端应用本质上是静态资源,剩下的少部分就是服务端渲染了,服务端渲染的本质上是一个后端服务,它的部署可以视为后端部署

后端部署的情况更为复杂,比如

  1. 配置服务,后端需要访问敏感数据,但又不能把敏感数据放在代码仓库。你可以在 environment variablesconsul 或者 k8s configmap 中维护
  2. 上下链路服务,你需要依赖数据库,上游服务
  3. 访问控制,限制 IP,黑白名单
  4. RateLimit
  5. 等等

我将在以后的文章分享如何在 k8s 中部署一个后端

小结

随着 devops 的发展,前端部署越来越简单,可控性也越来越高,建议所有人都稍微学习一下 devops 的东西。

道阻且长,行则将至。

相关文章

当你使用 docker 部署应用时,如何查看应用日志

原文:https://q.shanyue.tech/devops/devops/19.html

Issue

欢迎在 Gtihub Issue 中回答此问题: Issue 19(opens new window)

Author

回答者: shfshanyue(opens new window)

在 docker 中使用 docker logs CONTAINER

如果在 k8s 中使用 kubectl logs POD

你们的前端代码上线部署一次需要多长时间,需要人为干预吗

原文:https://q.shanyue.tech/devops/devops/95.html

更多描述

更短的部署时间,更少的人为干预,更有利于敏捷开发

Issue

欢迎在 Gtihub Issue 中回答此问题: Issue 95(opens new window)

Author

回答者: shfshanyue(opens new window)

TODO

Author

回答者: DoubleRayWang(opens new window)

Jenkins+docker

Author

回答者: Carrie999(opens new window)

需要 1 个小时,需要

Author

回答者: shfshanyue(opens new window)

@Carrie999 一个小时!!!?这也太久了吧

你们后端代码上线部署一次需要多长时间

原文:https://q.shanyue.tech/devops/devops/102.html

更多描述

关键在于考虑开发人员对项目部署流程的了解

Issue

欢迎在 Gtihub Issue 中回答此问题: Issue 102(opens new window)

Author

回答者: fmleing(opens new window)

30 分钟左右

Author

回答者: shfshanyue(opens new window)

30 分钟左右

那你们部署的流程是什么呢?我觉得半个小时有点多呀

Author

回答者: fmleing(opens new window)

30 分钟左右

那你们部署的流程是什么呢?我觉得半小时有点多呀

估计和 OS 有关,放在测试环境上的是 Linux 比较快,正式环境是 window 就比较慢,使用的是 jekins+tomcat 容器

什么是公有云,私有云,混合云以及多重云

原文:https://q.shanyue.tech/devops/devops/166.html

Issue

欢迎在 Gtihub Issue 中回答此问题: Issue 166(opens new window)

Author

回答者: timtike(opens new window)

公有云就是阿里云 腾讯云 aws 等 私有云 就是公司自己买物理机,在机房自己搭建网络,自己做虚拟机 混合云 就是 公有云 + 私有云 多重云 就是多个公有云

刚刚启动了一个服务,如何知道这个服务对应的端口号是多少

原文:https://q.shanyue.tech/devops/devops/252.html

Issue

欢迎在 Gtihub Issue 中回答此问题: Issue 252(opens new window)

Author

回答者: edisonwd(opens new window)

在 linux 系统中,我通常通过 ps -aux |grep 服务名 查看服务端口

如何评估一台服务器的 CPU 性能

原文:https://q.shanyue.tech/devops/devops/407.html

Issue

欢迎在 Gtihub Issue 中回答此问题: Issue 407(opens new window)

Author

回答者: shfshanyue(opens new window)

sysbench

$ sysbench --threads=4 --time=30 cpu run

sysbench 1.0.17 (using system LuaJIT 2.1.0-beta3)

Running the test with following options:
Number of threads: 4
Initializing random number generator from current time

Prime numbers limit: 10000

Initializing worker threads...

Threads started!

CPU speed:
    events per second:  3651.16

General statistics:
    total time:                          30.0010s
    total number of events:              109545

Latency (ms):
         min:                                    1.08
         avg:                                    1.10
         max:                                    5.78
         95th percentile:                        1.12
         sum:                               119955.35

Threads fairness:
    events (avg/stddev):           27386.2500/91.56
    execution time (avg/stddev):   29.9888/0.00 

stress

什么是 CPU 缓存,如何查看缓存命中率

原文:https://q.shanyue.tech/devops/devops/414.html

Issue

欢迎在 Gtihub Issue 中回答此问题: Issue 414(opens new window)

Author

回答者: edisonwd(opens new window)

CPU 缓存介于 CPU 和内存之间,缓存的是热点的内存数据。这些缓存按照大小不同分为 L1、L2、L3 等三级缓存,其中 L1 和 L2 在同一个 cpu 核中, 而在同一个 CPU 插槽中的多个核共享一个 L3 缓存。

缓存命中率,即直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比。当可以直接通过缓存获取到需要的数据,则命中缓存;否则需要从磁盘等地方读取获取数据。缓存命中率越高,表示直接从缓存获取数据的次数越多,程序执行效率越高。 使用 cachestat 可以查看整个个操作系统缓存的读写命中情况: cachestat 安装方式: sudo apt install perf-tools-unstable

下面以 1 秒间隔输出三组缓存信息:

$ sudo cachestat 1
Counting cache functions... Output every 1 seconds.
    HITS   MISSES  DIRTIES    RATIO   BUFFERS_MB   CACHE_MB
    1989        0       13   100.0%          501       2600
   12969        0     1412   100.0%          501       2600
   16798        0     2803   100.0%          501       2600 

从结果可以看到,HITS 是缓存命中的次数;MISSES 是缓存未命中的次数;DIRTIES 是表示新增到缓存中的脏页数;BUFFERS_MB 表示 Buffers 的大小,单位为 MB;CACHED_MB 表示 Cache 的大小,单位为 MB。

什么是 BNF 与 ABNF

原文:https://q.shanyue.tech/devops/devops/416.html

Issue

欢迎在 Gtihub Issue 中回答此问题: Issue 416(opens new window)

Author

回答者: shfshanyue(opens new window)

BNF (巴克斯范式) 是一种描述编程语言语法的元语言

ABNF (Augmented BNF),扩展的 BNF,通过 https://www.ietf.org/rfc/rfc5234.txt(opens new window) 规范

如何在生产环境部署一个 Node 应用

原文:https://q.shanyue.tech/devops/devops/420.html

Issue

欢迎在 Gtihub Issue 中回答此问题: Issue 420(opens new window)


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组