Contents

云计算课设

云计算课设

这个课设是一个基于MapReduce的倒排索引系统,核心是Hadoop+docker+k8s

整体架构

前端 vue+nodejs

后端 nodejs+express+redis+sqlite3

部署方法 dockerfile+k8s

数据处理方法 Hadoop

课设灵感

自己blog的搜索功能很弱,采用的是js插件的全局遍历搜索,就在网上查了一下优化方法,顺便知道了倒排索引这种方法,结合云计算课堂的dockerHadoop知识,便有了这个课设

倒排索引介绍

什么是正排索引

可以简单的理解为通过文档找词

1
2
3
文档--> 单词1 ,单词2

单词1 出现的次数  单词出现的位置; 单词2 单词2出现的位置  ...

正排索引的优势在于可以快速的查找某个文档里包含哪些词项。同理,正排不适用于查找包含某个词项的文档有哪些。

什么是倒排索引

倒排索引(Inverted Index)是一种常用的文本索引数据结构,用于加快文本搜索和信息检索的速度。它是一种反转(Inverted)的索引结构,将文档中的每个单词映射到包含该单词的文档列表。

通常,倒排索引由两个主要组成部分构成:词项表(Term Dictionary)和倒排列表(Inverted List)。

词项表(Term Dictionary):词项表是一个词项到倒排列表的映射,它记录了所有不重复的单词(或词项)以及它们对应的倒排列表的位置信息。

倒排列表(Inverted List):倒排列表包含了一个单词在文档集合中的出现位置。对于每个单词,倒排列表记录了包含该单词的文档的标识符(例如文档ID)以及该单词在文档中的位置信息(例如单词出现的位置或出现的频率)。

用一个图直观展示两者的区别 /images/yjs.png

前端设计

文件结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
│  .dockerignore
│  Dockerfile
│  index.html
│  package-lock.json
│  package.json
│  results.html
│  server.js
│  vite.config.js
├─public
│      favicon-16x16.png
│      icon_search.svg
│      leetcode.png
│      title.ttf
└─src
    └─assets
            base.css
            main.css

文件介绍

.html文件就是用户搜索/显示题目的页面
public存的图片
src存的样式(ai完成的,自己真的不会写)
dokcerfile.dockerignore生成docker用
server.js本地启动项目并且用于后续k8s部署时同步环境
package.json用来配置项目依赖,package-lock.json自动生成

交互设计

前后端通过POST互通,经过解析后显示

后端设计

文件结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
├── data/                #数据存放
├── src/
│   ├── config/         # 配置文件
│   ├── controllers/    # 控制器
│   ├── models/        # 数据模型
│   ├── routes/        # 路由
│   ├── services/      # 业务逻辑
│   └── utils/         # 工具函数
├── Dockerfile        # Docker 配置
├── package.json      # 项目配置
└── README.md         # 项目说明

数据获取

拿的力扣的数据,他们的api用的graphql,本来想用牛客的,结果76跟我说牛客没有这种接口,期末时间紧,也懒得爬了xd

拉数据非常简单,比如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
https://leetcode.com/graphql?query=query
{     
      userContestRanking(username:  "YOUR_USERNAME") 
      {
        attendedContestsCount
        rating
        globalRanking
        totalParticipants
        topPercentage    
      }
      userContestRankingHistory(username: "YOUR_USERNAME")
      {
        attended
        trendDirection
        problemsSolved
        totalProblems
        finishTimeInSeconds
        rating
        ranking
        contest 
        {
          title
          startTime
        }
      }
} 

在浏览器里面搜这个,就可以得到一堆数据了

存到数据里面并把url删除,存于data/input.txt里面,类似1 Two Sum Easy array hash-table,准备进行mapreduce的分词

Hadoop处理

  1. 首先把数据ftp传上去,然后在hdfs里面搞个文件夹,并且数据扔进去

hdfs dfs -mkdir /input hdfs dfs -put ~/input.txt /input/

  1. 使用mapreduce进行分词

根据Hadoop streaming 的规则,只要我们使用标准的输入输出,什么语言都行,直接nodejs启动了,mapper.js起到这个作用。

在map阶段,我们按行读取内容,并将处理后的内容分为docId 和单词,并使用标准输入输出进行输出方便后续reduce处理,所以就有了reduce.js

  1. 生成并且拉出数据

hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-3.3.6.jar -D mapreduce.job.maps=4 -D mapreduce.job.reduces=4 -files mapper.js,reducer.js -mapper “node mapper.js” -reducer “node reducer.js” -input /input/input.txt -output /output

hdfs dfs -get /output ~/hadoop_output

  1. 存入redis 具体来说,我们读取处理好的单词与文章的映射,并根据单词在某个文章中出现的次数对其进行排序,如 ZSM (46,1),(589,1),(32,1),(42,2),(22,1) 变成ZSM (42,2),(46,1),(589,1),(32,1),(22,1)

docker部署

前端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 使用基础的 Node.js 镜像作为基础
FROM node:18

# 设置工作目录
WORKDIR /usr/src/app

# 将 package.json 和 package-lock.json 复制到工作目录
COPY package*.json ./

# 安装依赖
RUN  npm  install

# 复制项目文件到工作目录
COPY . .

# 暴露容器的端口(根据你的项目配置)
EXPOSE 5173

# 运行前端应用
CMD ["node", "server.js"]

全部文件拉进去,运行进行了,一定要在里面去npm install,要不然会有环境问题,比如armx86的包不兼容

后端

这里我把sqlite3也包进去了,反正比较轻量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
FROM node:18-alpine

# 安装构建 better-sqlite3 所需的依赖
RUN apk add --no-cache python3 make g++ sqlite-dev

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 9999

CMD ["npm", "start"]

k8s部署

redis部署

首先要理解,每个pod都是和主机环境直接隔离的,如果我想用主机docker上面部署的redis是可以的,但是需要的url就是http://ip:port,是固定的,那你换个网本地调试是不是炸了,所以拿一个port部署redis是很有必要的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:7.2
          ports:
            - containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
  name: redis-service
spec:
  selector:
    app: redis
  ports:
    - protocol: TCP
      port: 6379
      targetPort: 6379

前端部署

这里我采用docker+k8s,可以看k8s的那个文章,讲了一点点原理,所以本地docker搭建的时候是docker build -t yunjisuanfront:latest .

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: apps/v1
kind: Deployment
metadata:
  name: yunjisuan-frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: yunjisuan-frontend
  template:
    metadata:
      labels:
        app: yunjisuan-frontend
    spec:
      containers:
      - name: yunjisuan-frontend
        image: yunjisuanfront:latest
        imagePullPolicy: Never  # 使用本地镜像
        ports:
        - containerPort: 5173  # 根据你的前端实际端口调整
        env:
        - name: BACKEND_URL
          value: "http://yunjisuan-backend-service:9999"
---
apiVersion: v1
kind: Service
metadata:
  name: yunjisuan-frontend-service
spec:
  selector:
    app: yunjisuan-frontend
  ports:
  - port: 5173
    targetPort: 5173
  type: LoadBalancer  # 负载均衡

后端部署

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
apiVersion: apps/v1
kind: Deployment
metadata:
  name: yunjisuan-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: yunjisuan-backend
  template:
    metadata:
      labels:
        app: yunjisuan-backend
    spec:
      containers:
      - name: yunjisuan-backend
        image: yunjisuan-backend:latest
        imagePullPolicy: Never  # 使用本地镜像
        ports:
        - containerPort: 9999
        env:
        - name: DB_PATH
          value: "/app/data/questions.db"
        - name: REDIS_HOST
          value: "redis-service"
        - name: REDIS_PORT
          value: "6379"
        - name: NODE_ENV
          value: "production"
        volumeMounts:
        - name: data-volume
          mountPath: /app/data
      volumes:
      - name: data-volume
        persistentVolumeClaim:
          claimName: yunjisuan-data-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: yunjisuan-backend-service
spec:
  selector:
    app: yunjisuan-backend
  ports:
  - port: 9999
    targetPort: 9999
  type: ClusterIP

注意要去吃到数据库和redis的路径/端口,然后再挂个持久化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: yunjisuan-data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

关于自动化

以上方法虽然用的是last版本,但是有bug的话你就要删掉重建,很麻烦,如何自动推送启用last版本的docker镜像呢???

方法一
利用自动化sh脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/bash
PREVIOUS_DIGEST=""
while true; do
  DIGEST=$(docker image inspect yunjisuanfront:latest --format='{{index .Id}}')
  if [[ "$DIGEST" != "$PREVIOUS_DIGEST" ]]; then
    echo "镜像已更新,重启 Deployment..."
    kubectl rollout restart deployment yunjisuan-frontend
    PREVIOUS_DIGEST="$DIGEST"
  fi
  sleep 10
done

方法二 使用本地 registry + 镜像 tag 唯一化

每次你 build 镜像时,给它一个唯一 tag(比如使用时间戳、Git 提交哈希),然后更新 Deployment 镜像地址。这样 Kubernetes 会认为镜像变了,从而重建 Pod。

  1. 本地搭建 Docker Registry

docker run -d -p 5000:5000 –name registry –restart=always registry:2

  1. 构建并推送带唯一 tag 的镜像

TIMESTAMP=$(date +%s) docker build -t localhost:5000/yunjisuanfront:$TIMESTAMP . docker push localhost:5000/yunjisuanfront:$TIMESTAMP

  1. 更新 Deployment 的镜像地址

image: localhost:5000/yunjisuanfront:{{TIMESTAMP}} imagePullPolicy: Always

  1. 可以使用脚本/CI 工具(如 GitHub Actions)自动完成 build → push → update YAML → apply。
    比如
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
set -e

# 1. 生成唯一 tag
TAG=$(date +%s)

# 2. 构建镜像
docker build -t localhost:5000/yunjisuanfront:$TAG .

# 3. 推送到本地 registry
docker push localhost:5000/yunjisuanfront:$TAG

# 4. 渲染模板
envsubst < deploy/frontend.yaml.template > deploy/frontend.yaml

# 5. 应用到 K8s
kubectl apply -f deploy/frontend.yaml

echo "✅ 镜像 yunjisuanfront:$TAG 已部署到 Kubernetes"

鸽子时间

如果有时间,可能会研究持久化抓取数据->Hadoop自动化处理->推送数据库->重建镜像->pod重启,但是比较复杂,而且吃性能,后面再说吧。
咕咕咕