DevOps 面试题(山月)
当新入职一家公司时,如何快速搭建开发环境并让应用跑起来
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 9(opens new window)
Author
回答者: shfshanyue(opens new window)
新人入职新上手项目,如何把它跑起来,这是所有人都会碰到的问题:所有人都是从新手开始的。
有可能你会脱口而出:npm run dev/npm start
,但实际工作中,处处藏坑,往往没这么简单。
- 查看是否有
CI/CD
,如果有跟着CI/CD
部署的脚本跑命令 - 查看是否有
dockerfile
,如果有跟着dockerfile
跑命令 - 查看 npm scripts 中是否有 dev/start,尝试
npm run dev/npm start
- 查看是否有文档,如果有跟着文档走。为啥要把文档放到最后一个?原因你懂的
但即便是十分谨慎,也有可能遇到以下几个叫苦不迭、浪费了一下午时间的坑:
- 前端有可能在本地环境启动时需要依赖前端构建时所产生的文件,所以有时需要先正常部署一遍,再试着按照本地环境启动 (即需要先
npm run build
一下,再npm run dev/npm start
)。(比如,一次我们的项目 npm run dev 时需要 webpack DllPlugin 构建后的东西) - 别忘了设置环境变量或者配置文件 (.env/consul/k8s-configmap)
因此,设置一个少的 script,可以很好地避免后人踩坑,更重要的是,可以避免后人骂你,
此时可设置 script hooks,如 prepare
、postinstall
自动执行脚本,来完善该项目的基础设施
{
"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"
}
在一个面向服务端的项目中,如 next
、nuxt
与 nest
。dev 与 start 的区别趋于明显,一个为生产环境,一个为开发环境
- dev: 在开发环境启动项目,一般带有 watch 选项,监听文件变化而重启服务,此时会耗费大量的 CPU 性能,不宜放在生产环境
- start: 在生产环境启动项目
在 nest
项目中进行配置
{
"start": "nest start",
"dev": "nest start --watch"
}
Author
我的意见和楼上相反,应该先大概看一遍文档…… 文档中会描述本地环境的配置方法
查看是否有 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 的区别?
- 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 的区别?
- npm start 是 npm run start 的别名,支持 prestart 和 poststart 钩子
你是真没吃过文档的亏。。。管他什么公司,文档都有坑。
你们的前端项目是如何在线上部署的
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 16(opens new window)
Author
回答者: shfshanyue(opens new window)
今天正好写了一篇长文来回答这个问题
前端一说起刀耕火种,那肯定紧随着前端工程化这一话题。随着 react
/vue
/angular
,es6+
,webpack
,babel
,typescript
以及 node
的发展,前端已经在逐渐替代过去 script 引 cdn 开发的方式了,掀起了工程化这一大浪潮。得益于工程化的发展与开源社区的良好生态,前端应用的可用性与效率得到了很大提高。
前端以前是刀耕火种,那前端应用部署在以前也是刀耕火种。那前端应用部署的发展得益于什么,随前端工程化带来的副产品?
这只是一部分,而更重要的原因是 devops
的崛起。
为了更清晰地理解前端部署的发展史,了解部署时运维和前端(或者更广泛地说,业务开发人员)的职责划分,当每次前端部署发生改变时,可以思考两个问题
- 缓存,前端应用中 http 的
response header
由谁来配?得益于工程化发展,可以对打包后得到带有 hash 值的文件可以做永久缓存 - 跨域,
/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
启动前端应用。前端第一次写 dockerfile
与 docker-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
这时候再思考文章最前面两个问题
- 缓存,由于从静态文件转换为服务,缓存开始交由前端控制 (但是镜像中的
http-server
不太适合做这件事情) - 跨域,跨域仍由运维在
nginx
中配置
前端可以做他应该做的事情中的一部分了,这是一件令人开心的事情
当然,前端对于 dockerfile
的改进也是一个慢慢演进的过程,那这个时候镜像有什么问题呢?
- 构建镜像体积过大
- 构建镜像时间过长
使用多阶段构建优化镜像
这中间其实经历了不少坎坷,其中过程如何,详见我的另一篇文章: 如何使用 docker 部署前端应用(opens new window)。
其中主要的优化也是在上述所提到的两个方面
- 构建镜像体积由 1G+ 变为 10M+
- 构建镜像时间由 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
那它怎么做的
- 先
ADD package.json /code
, 再npm install --production
之后Add
所有文件。充分利用镜像缓存,减少构建时间 - 多阶段构建,大大减小镜像体积
另外还可以有一些小优化,如
npm cache
的基础镜像或者npm
私有仓库,减少npm install
时间,减小构建时间npm install --production
只装必要的包
前端看着自己优化的 dockerfile
,想着前几天还被运维吵,说什么磁盘一半的空间都被前端的镜像给占了,想着自己节省了前端镜像几个数量级的体积,为公司好像省了不少服务器的开销,想着自己的基础盘进一步扩大,不禁开心的笑了
这时候再思考文章最前面两个问题
- 缓存,缓存由前端控制,缓存在 oss 上设置,将会使用 cdn 对 oss 加速。此时缓存由前端写脚本控制
- 跨域,跨域仍由运维在
nginx
中配置
CI/CD 与 gitlab
此时前端成就感爆棚,运维呢?运维还在一遍一遍地上线,重复着一遍又一遍的部署三个动作
- 拉代码
docker-compose up -d
- 重启 nginx
运维觉得再也不能这么下去了,于是他引进了 CI
: 与现有代码仓库 gitlab
配套的 gitlab ci
CI
,Continuous Integration
,持续集成CD
,Continuous Delivery
,持续交付
重要的不是 CI/CD
是什么,重要的是现在运维不用跟着业务上线走了,不需要一直盯着前端部署了。这些都是 CI/CD
的事情了,它被用来做自动化部署。上述提到的三件事交给了 CI/CD
.gitlab-ci.yml
是 gitlab
的 CI 配置文件,它大概长这个样子
deploy:
stage: deploy
only:
- master
script:
- docker-compose up --build -d
tags:
- shell
CI/CD
不仅仅更解放了业务项目的部署,也在交付之前大大加强了业务代码的质量,它可以用来 lint
,test
,package
安全检查,甚至多特性多环境部署,我将会在我以后的文章将这部分事情
我的一个服务器渲染项目 shfshanyue/shici(opens new window) 以前在我的服务器中就是以 docker
/docker-compose/gitlab-ci
的方式部署,有兴趣的可以看看它的配置文件
- shfshanyue/shici:Dockerfile(opens new window)
- shfshanyue/shici:docker-compose.yml(opens new window)
- shfshanyue/shici:gitlab-ci.yml(opens new window)
如果你有个人服务器的话,也建议你做一个自己感兴趣的前端应用和配套的后端接口服务,并且配套 CI/CD
把它部署在自己的自己服务器上
而你如果希望结合 github
做 CI/CD
,那可以试一试 github
+ github action
另外,也可以试试 drone.ci
,如何部署可以参考我以前的文章: github 上持续集成方案 drone 的简介及部署(opens new window)
使用 kubernetes 部署
随着业务越来越大,镜像越来越多,docker-compose
已经不太能应付,kubernetes
应时而出。这时服务器也从 1 台变成了多台,多台服务器就会有分布式问题
一门新技术的出现,在解决以前问题的同时也会引进复杂性。
k8s 部署的好处很明显: 健康检查,滚动升级,弹性扩容,快速回滚,资源限制,完善的监控等等
那现在遇到的新问题是什么?
构建镜像的服务器,提供容器服务的服务器,做持续集成的服务器是一台!
需要一个私有的镜像仓库,这是运维的事情,harbor
很快就被运维搭建好了,但是对于前端部署来说,复杂性又提高了
先来看看以前的流程:
- 前端配置
dockerfile
与docker-compose
- 生产环境服务器的
CI runner
拉代码(可以看做以前的运维),docker-compose up -d
启动服务。然后再重启nginx
,做反向代理,对外提供服务
以前的流程有一个问题: 构建镜像的服务器,提供容器服务的服务器,做持续集成的服务器是一台!,所以需要一个私有的镜像仓库,一个能够访问 k8s
集群的持续集成服务器
流程改进之后结合 k8s
的流程如下
- 前端配置
dockerfile
,构建镜像,推到镜像仓库 - 运维为前端应用配置
k8s
的资源配置文件,kubectl apply -f
时会重新拉取镜像,部署资源
运维问前端,需不需要再扩大下你的基础盘,写一写前端的 k8s
资源配置文件,并且列了几篇文章
- 使用 k8s 部署你的第一个应用: Pod,Deployment 与 Service(opens new window)
- 使用 k8s 为你的应用配置域名: Ingress(opens new window)
- 使用 k8s 为你的域名加上 https(opens new window)
前端看了看后端十几个 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
这时候再思考文章最前面两个问题
- 缓存,缓存由前端控制
- 跨域,跨域仍由运维控制,在后端
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
这时候再思考文章最前面两个问题
- 缓存,缓存由前端控制
- 跨域,跨域由后端控制,配置在后端 Chart 的配置文件
values.yaml
中
到了这时前端和运维的职责所在呢?
前端需要做的事情有:
- 写前端构建的
dockerfile
,这只是一次性的工作,而且有了参考 - 使用
helm
部署时指定参数
那运维要做的事情呢
- 提供一个供所有前端项目使用的
helm chart
,甚至不用提供,如果运维比较懒那就就使用 bitnami/nginx(opens new window) 吧。也是一次性工作 - 提供一个基于
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)
服务端渲染与后端部署
大部分前端应用本质上是静态资源,剩下的少部分就是服务端渲染了,服务端渲染的本质上是一个后端服务,它的部署可以视为后端部署
后端部署的情况更为复杂,比如
- 配置服务,后端需要访问敏感数据,但又不能把敏感数据放在代码仓库。你可以在
environment variables
,consul
或者k8s configmap
中维护 - 上下链路服务,你需要依赖数据库,上游服务
- 访问控制,限制 IP,黑白名单
- RateLimit
- 等等
我将在以后的文章分享如何在 k8s 中部署一个后端
小结
随着 devops
的发展,前端部署越来越简单,可控性也越来越高,建议所有人都稍微学习一下 devops
的东西。
道阻且长,行则将至。
相关文章
- 个人服务器运维指南(opens new window)
- 如果你想搭建一个博客(opens new window)
- 当我有一台服务器时我做了什么(opens new window)
- 使用 k8s 部署你的第一个应用: Pod,Deployment 与 Service(opens new window)
- 使用 k8s 为你的应用配置域名: Ingress(opens new window)
- 使用 k8s 为你的域名加上 https(opens new window)
当你使用 docker 部署应用时,如何查看应用日志
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 19(opens new window)
Author
回答者: shfshanyue(opens new window)
在 docker 中使用 docker logs CONTAINER
如果在 k8s 中使用 kubectl logs POD
你们的前端代码上线部署一次需要多长时间,需要人为干预吗
更多描述
更短的部署时间,更少的人为干预,更有利于敏捷开发
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 一个小时!!!?这也太久了吧
你们后端代码上线部署一次需要多长时间
更多描述
关键在于考虑开发人员对项目部署流程的了解
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 容器
什么是公有云,私有云,混合云以及多重云
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 166(opens new window)
Author
回答者: timtike(opens new window)
公有云就是阿里云 腾讯云 aws 等 私有云 就是公司自己买物理机,在机房自己搭建网络,自己做虚拟机 混合云 就是 公有云 + 私有云 多重云 就是多个公有云
刚刚启动了一个服务,如何知道这个服务对应的端口号是多少
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 252(opens new window)
Author
回答者: edisonwd(opens new window)
在 linux 系统中,我通常通过 ps -aux |grep 服务名
查看服务端口
如何评估一台服务器的 CPU 性能
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 缓存,如何查看缓存命中率
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
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 应用
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 420(opens new window)