从前端角度聊聊IT架构的演变

732 阅读7分钟

一、背景

前端能力边界:

未命名文件 (4).png
在工作的时候,我们往往只需要在已经搭好的架构下做事情,很难跳出当前的圈,但是对于圈外事情的认知往往能够决定我们能力边界范围,以及能cover多大的事情。举例来说,我们在开发过程中,往往只需要关注业务本身,上线的时候时候别人已经搭好基建一顿点,就ok了。但是如果要我们自己构建上线部署流程,又或者构建公司的IT架构,我们又应该怎么做呢?又或者当前的基建到底帮我们解决了什么问题,它到底在干什么?本文想从前端开发部署的角度,来谈谈项目的IT架构变化。

二、前端独立开发,和后端一同部署

在很久以前,前端只需要编写HTML,最后将HTML丢给后端,后端同学会直接对HTML进行修改,改成JSP,PHP等文件,这时候还没有前端,这个工作往往是由UI完成的。再后来随着相应技术的完善,以及前后端分离概念的盛行,前端慢慢从后端中分离出来。使用devServer在开发时单独起服务,并通过charles等代理工具或者直接屏蔽浏览器跨域的方式,做到不用起后端服务,直接本地开发前端,但是这时候前端在部署的时候往往还是直接丢给后端一个dist文件,让起帮忙发布上线。
这时候的项目架构:
未命名文件 (6).png

三、nginx反向代理,前端单独发布上线

随着业务越来越复杂,前端在发布的时候也需要从后端脱离。前端在开发的时候往往会起一个devServer,如果前端单独上线的话,是否也是起了一个devServer呢?答案是否定的,而是使用nginx反向代理,直接打到了本地文件上。
这时候的项目架构:

未命名文件 (7).png

这时候只需要将dist文件放到一个服务器上,并启动nginx服务,便可以完成。如果注意spa和mpa,如果spa的history模式,则需要重定向到index文件,否则会出现404。

nginx配置:


...

http {
    include       mime.types;
    default_type  application/octet-stream;


    server {
        listen       80;
        server_name  localhost;

        root /app/dist;

        location / {
           try_files $uri $uri/ /index.html;   //spa页面需要重定向
        }


}

四、发布工具jenkins

这时候我们虽然可以独立部署前端,但是每一次更新我们都需要手动去服务器上更改文件吗?那肯定不是的。jenkins就很好的帮助我们解决这个问题,这里不详细列出jenkins使用细节,给出一个使用jenkins更新服务的流程图。

未命名文件 (10).png

五、业务越来越复杂,BFF的出现

1、架构

随着业务越来越复杂,一个服务再也不能满足当前需求,比如鉴权,审核需要单独开发部署,这时候便出现了RPC的概念,网关层用来返回给前端,而真正的业务逻辑则在相应的RPC服务中。这时候,前端往往会构建一个BFF作为网关层,调用下游服务,将后端推向更后端。
这时候的项目架构:

未命名文件 (11).png

这时候前端就需要构建服务器端的能力,nodejs往往使用pm2守护进程进行构建,RPC的方式很多种(thrift,grpc),下面是一个grpc的例子。 RPC端:
server.addService(mdx_proto.MDX.service, {
    // 实现 render 方法(是不是有点接口 interface 实现的味道)
    render: async (call, cb) => {
        try {
            ...
        } catch (err) {
            console.log('服务器出错', err)
        }
    }
})


// 启动服务,设置端口
server.bind('127.0.0.1:55555', grpc.ServerCredentials.createInsecure())
server.start()
console.log('server start 127.0.0.1:55555...')

BFF端口:

const grpc = require('grpc')

const protoLoader = require('@grpc/proto-loader')

const path = require('path');

console.log(path.join(__dirname, 'hello.proto'));

const packageDefinition = protoLoader.loadSync(path.join(__dirname, 'mdx.proto'), {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true,
})

// grpc 加载包
const mdx_proto = grpc.loadPackageDefinition(packageDefinition).mdx

// ---------
console.log('init client')

// 创建客户端,连接 服务 服务
const mdxService = new mdx_proto.MDX('127.0.0.1:55555', grpc.credentials.createInsecure())
export default mdxService;
router.get('/render', async (ctx: {
  query: any;
  body: string; }) => {
  const { mdx } = ctx.query;

  const data: string = await new Promise((resolve) => {
    mdxService.render({
      // 按照 proto「message HelloReq」中的约定传参
      renderString: mdx,
    }, (err: any, response: { message: any; code: any }) => {
      if (err) return console.log(err)
      // 按照 proto「message HelloResp」中的约定返回
      const { message } = response
      // 服务端的返回信息
      resolve(message);
    })
  })
  ctx.body = data;

})

2、nodejs构建BFF的特点

(1)前端不用学习新的语言,可以更快上手。
(2)不会为每个请求创建线程,而是非阻塞异步IO,处理IO密集型任务更有优势。因为是单线程,如果当一个服务计算需要消耗大量时间,则会阻塞后面的计算,所以处理CPU密集型任务不适合(不过现在nodejs也已经支持多线程)。

image.png

(3)因为不用创建线程,减少了创建和销毁线程的消耗,并可以有更大的连接数。假设一个线程理论消耗2MB内存,所以一个8GB内存的服务器最多可以处理4000个请求。而nodejs则无需这些消耗,可以同时处理更多的请求。网上一些资料认为8GB的nodejs服务器可以同时处理超过4万的请求。

(4)node的http服务,在接受到请求后,会根据当前处理服务的数量,计算一个time的时间,最后将逻辑放到setTimeout(fn,time)中执行。

3、 其他需要关注的点

(1)APM监控,云供应商往往自带一些CPU,内存监控,或者可以构建grafana。
(2)代码错误日志上报,Sentry,ELK。
(3)埋点上报,ELK?暂时没有找到比较好的免费平台。
(4)优雅退出,如果在更新服务的时候有代码正在执行,docker可以根据信号的方式来进行等待更新。

未命名文件 (12).png

六、业务更加复杂,集群多实例登上历史舞台

随着业务的发展,一个服务构建一个实例已经不能满足要求。这时候docker,k8s开始上场。
这时候的项目架构:

未命名文件 (14).png

1、docker优势:

(1)更高效的利用系统资源,内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。
(2)更快速的启动时间,因此可以做到秒级,而docker容器应用,由于直接运行于宿主内核,无需启动完整的操作系统,因此可以做到秒级,甚至毫秒级的启动时间,大大的节约了开发测试,部署的时间。
(3)一致的运行环境,避免因为环境不同造成的bug。
(4)image,container更加利于部署,扩容,迁移。

2、k8s优势:

K8s是一个最初由Google开发的,用于自动化部署、扩展和管理容器化应用的开源容器编排器技术。 K8s使部署和管理微服务架构应用程序变得很简单。它通过在集群之上形成一个抽象层来实现这一点,允许开发团队平滑地部署应用程序,而 K8s主要处理以下任务:
(1)控制和管理应用程序对资源的使用。
(2)自动负载均衡应用程序的多个实例之间请求。
(3)监控资源使用和资源限制,为了可以自动阻止应用消耗过多的资源并且可以再次恢复它们。
(4)如果主机资源耗尽或主机死机,将应用程序实例从一台主机迁移到另一台主机是一个可行的选项。
(5)当有新的主机加入集群时,新增加的额外资源可以被自动使用。

3、k8s无状态deployment部署流程(事先构建好集群)

(1)使用(docker build -t frontend . )来Dockerfile构建镜像名字为frontend的镜像,一个简单的Dockerfile。重复部署需要先删除原有镜像,否则内存空间将会一直被占用。

FROM nginx:1.14.1


ADD ./ /app

WORKDIR /app

EXPOSE 80

CMD ["nginx", "-c", "/app/server/nginx.conf", "-g" ,"daemon off;"]

(2)因为是本地镜像,所以需要将镜像scp到各个节点。
(3)使用deployment.yaml部署pod(kubectl apply -f deployment.yaml)。一个简单的yaml文件

apiVersion: apps/v1
kind: Deployment
metadata:
  # 部署名字
  name: frontend
spec:
  replicas: 2
  # 用来查找关联的 Pod,所有标签都匹配才行
  selector:
    matchLabels:
      app: frontend
  # 定义 Pod 相关数据
  template:
    metadata:
      labels:
        app: frontend
    spec:
      # 定义容器,可以多个
      containers:
        - name: frontend # 容器名字
          image: frontend # 镜像
          imagePullPolicy: Never # 只使用本地镜像
          ports:
            - containerPort: 80

(4)使用kubectl get pods -o wide查看pod信息。
(5)构建service,kubectl apply -f service.yaml,一个简单的service.yaml。

apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  selector:
    app: frontend
  # 默认 ClusterIP 集群内可访问,NodePort 节点可访问,LoadBalancer 负载均衡模式(需要负载均衡器才可用)
  type: NodePort
  ports:
    - port: 8080        # 本 Service 的端口
      targetPort: 80  # 容器端口
      nodePort: 31000   # 节点端口,范围固定 30000 ~ 32767

(6)kubectl get svc查看service。