2024年1月

神经网络的权重初始化

这是一个神经单元初始化地例子,然后再演变到整个深度网络。

来看看只有一个神经元的情况,然后才是深度网络。

单个神经元可能有4个输入特征,从
\(x_{1}\)

\(x_{4}\)
,经过
\(a=g(z)\)
处理,最终得到
\(\hat{y}\)
,稍后讲深度网络时,这些输入表示为
\(a^{[l]}\)
,暂时用
\(x\)
表示。

\(z = w_{1}x_{1} + w_{2}x_{2} + \ldots +w_{n}x_{n}\)

\(b=0\)
,暂时忽略
\(b\)
,为了预防
\(z\)
值过大或过小,可以看到
\(n\)
越大,希望
\(w_{i}\)
越小,因为
\(z\)

\(w_{i}x_{i}\)
的和,如果把很多此类项相加,希望每项值更小,最合理的方法就是设置
\(w_{i}=\frac{1}{n}\)

\(n\)
表示神经元的输入特征数量,实际上,要做的就是设置某层权重矩阵
\(w^{[l]} = np.random.randn( \text{shape})*\text{np.}\text{sqrt}(\frac{1}{n^{[l-1]}})\)

\(n^{[l - 1]}\)
就是喂给第
\(l\)
层神经单元的数量(即第
\(l-1\)
层神经元数量)。

结果,如果是用的是
Relu
激活函数,而不是
\(\frac{1}{n}\)
,方差设置为
\(\frac{2}{n}\)
,效果会更好。常常发现,初始化时,尤其是使用
Relu
激活函数时,
\(g^{[l]}(z) =Relu(z)\)
,它取决于对随机变量的熟悉程度,这是高斯随机变量,然后乘以它的平方根,也就是引用这个方差
\(\frac{2}{n}\)
。这里,用的是
\(n^{[l - 1]}\)
,因为本例中,逻辑回归的特征是不变的。但一般情况下
\(l\)
层上的每个神经元都有
\(n^{[l - 1]}\)
个输入。如果激活函数的输入特征被零均值和标准方差化,方差是1,
\(z\)
也会调整到相似范围,这就没解决问题(梯度消失和爆炸问题)。但它确实降低了梯度消失和爆炸问题,因为它给权重矩阵
\(w\)
设置了合理值,也知道,它不能比1大很多,也不能比1小很多,所以梯度没有爆炸或消失过快。

提到了其它变体函数,刚刚提到的函数是
Relu
激活函数,一篇由
Herd
等人撰写的论文曾介绍过。对于几个其它变体函数,如
tanh
激活函数,有篇论文提到,常量1比常量2的效率更高,对于
tanh
函数来说,它是
\(\sqrt{\frac{1}{n^{[l-1]}}}\)
,这里平方根的作用与这个公式作用相同(
\(\text{np.}\text{sqrt}(\frac{1}{n^{[l-1]}})\)
),它适用于
tanh
激活函数,被称为
Xavier
初始化。
Yoshua Bengio
和他的同事还提出另一种方法,可能在一些论文中看到过,它们使用的是公式
\(\sqrt{\frac{2}{n^{[l-1]} + n^{\left[l\right]}}}\)
。其它理论已对此证明,但如果想用
Relu
激活函数,也就是最常用的激活函数,会用这个公式
\(\text{np.}\text{sqrt}(\frac{2}{n^{[l-1]}})\)
,如果使用
tanh
函数,可以用公式
\(\sqrt{\frac{1}{n^{[l-1]}}}\)
,有些作者也会使用这个函数。

实际上,认为所有这些公式只是给一个起点,它们给出初始化权重矩阵的方差的默认值,如果想添加方差,方差参数则是另一个需要调整的超级参数,可以给公式
\(\text{np.}\text{sqrt}(\frac{2}{n^{[l-1]}})\)
添加一个乘数参数,调优作为超级参数激增一份子的乘子参数。有时调优该超级参数效果一般,这并不是想调优的首要超级参数,但发现调优过程中产生的问题,虽然调优该参数能起到一定作用,但考虑到相比调优,其它超级参数的重要性,通常把它的优先级放得比较低。

希望现在对梯度消失或爆炸问题以及如何为权重初始化合理值已经有了一个直观认识,希望设置的权重矩阵既不会增长过快,也不会太快下降到0,从而训练出一个权重或梯度不会增长或消失过快的深度网络。在训练深度网络时,这也是一个加快训练速度的技巧。

前言

前面分享了 k8s 的部署安装,本篇来点实操,将会把一个 .net core + vue 的项目(zhontai),打包构建成 docker 镜像,推送到 nexus 镜像仓库,并部署到 k8s 中

准备

要实现项目的部署,除了准备要部署的环境(k8s),还需要准备项目所用到的各中间件,本文旨在分享部署的一个整体流程,对项目中所使用到的各中间件(mysql,redis 等)的安装使用可自行在本
DevOps 系列文章
中找到

  • 一个 .net core+vue 的项目


  • Nexus 的安装部署,
    文章介绍


    • 做为镜像仓库使用,将项目打包镜像及项目镜像推送到仓库,k8s 也从此仓库拉取镜像
    • 版本为 v3.61 ,安装地址为 192.168.0.214:8081,并使用局域网域名解析,
    • 在目标机器先登录能够拉取推送镜像,
      参考
    • 拉取镜像地址:
      https://nexus.devops.test.com
    • 推送镜像地址:
      https://push.nexus.devops.test.com
  • Docker 的安装部署,
    文章介绍


    • 使用 doker 拉取 sdk、nodejs 镜像进行打包,构建 k8s 所需要的项目镜像
    • 版本:v24.0.6
  • K8S 的安装与部署,
    文章介绍


    • 部署项目服务
    • 使用 ingress 解析域名到服务
  • 部署前后端项目到 K8S,本文介绍

使用 Docker 打包应用镜像

不管什么语言,基本都可以使用这个打包流程,将官方镜像打包推送到私有镜像仓库个人认为是必要的,不然如果一旦远端的镜像失效,又需要重新拉取镜像时就会很尬尴。

  1. 准备打包所需镜像


    1. 获取基础打包镜像(dotnet 获取 sdk 镜像,vue 获取 node 镜像)
    2. 基于基础镜像,安装所需软件,设置默认配置,复制默认文件,封装项目的打包镜像
    3. 挂载项目到 sdk 镜像进行打包,打包后获取构建完成的产物
  2. 准备运行所需的基础镜像


    1. 获取运行时镜像(.net core 获取 runtime 镜像,vue 获取 nginx 镜像)
    2. 基于运行时镜像,将打包构建完从的产物添加到镜像,构建项目镜像
    3. 推送项目镜像到仓库

.Net Core 7.0 项目镜像

构建所需一个 sdk 镜像用于打包编译项目,一个 runtime 镜像运行 .net core 项目,版本选择对应的 7.0 即可

构建 dotnet sdk 7.0 打包镜像

  1. 拉取 dotnet sdk 镜像:
    docker pull mcr.microsoft.com/dotnet/sdk:7.0


    1. 目前可以直接拉取,若无法拉取则配置国内镜像源
    2. 临时运行容器进行测试:
      docker run -it --rm mcr.microsoft.com/dotnet/sdk:7.0
      ,可以将需要的东西进行安装测试再编写 dockerfile
  2. 使用 Dockerfile 构建打包镜像 dotnet-sdk-7.0


    1. 为了便于后期维护,使用 Dockerfile 来构建
    2. 目录文件:dotnet-sdk-7.0/Dockerfile
    3. 基于 sdk 安装 dotnet-monitor v7.3.2,
      文档
      这里只做演示,暂时没用上,后续使用多阶段构建的时候可以将其复制到运行时镜像中
    4. # 基础sdk镜像 v7.0    
      FROM mcr.microsoft.com/dotnet/sdk:7.0
      # 将tools目录加入环境变量
      ENV PATH="$PATH:/root/.dotnet/tools"
      # 安装 dotnet-monitor 
      RUN dotnet tool install -g dotnet-monitor --version 7.3.2
      
    5. 执行构建:
      docker build -t dotnet-sdk-7.0 -f ./Dockerfile .
  3. 推送镜像到 Nexus 仓库


    1. 镜像登录认证:
      docker login push.nexus.devops.test.com -u pusher -p devops666
    2. 打标签:
      docker tag dotnet-sdk-7.0 push.nexus.devops.test.com/projectbuild/dotnet-sdk-7.0
    3. 推送镜像:
      docker push push.nexus.devops.test.com/projectbuild/dotnet-sdk-7.0
    4. 记得清理本地缓存镜像:
      docker rmi dotnet-sdk-7.0 && docker rmi push.nexus.devops.test.com/projectbuild/dotnet-sdk-7.0
  4. 使用镜像


    1. 后续使用 dotnet sdk 7.0 就可以直接使用
      nexus.devops.test.com/projectbuild/dotnet-sdk-7.0
      即可
    2. 直接拉取:
      docker pull nexus.devops.test.com/projectbuild/dotnet-sdk-7.0

构建 dotnet runtime 7.0 运行时镜像

  1. 拉取 dotnet runtime 镜像:
    docker pull mcr.microsoft.com/dotnet/runtime:7.0


    1. 临时运行容器进行测试:
      docker run -it --rm mcr.microsoft.com/dotnet/runtime:7.0
  2. 使用 Dockerfile 构建运行时镜像


    1. 为了便于后期维护,使用 Dockerfile 来构建
    2. 目录文件:dotnet-runtime-7.0/Dockerfile
    3. 若非需要,可以不安装软件,安装软件后镜像会多个 100M
    4. # 基础runtime镜像 v7.0   
      FROM mcr.microsoft.com/dotnet/aspnet:7.0
      
      # 写入阿里云镜像源
      RUN echo " \
      deb http://mirrors.aliyun.com/debian/ bullseye main contrib non-free\n \
      deb-src http://mirrors.aliyun.com/debian/ bullseye main contrib non-free\n \
      \n \
      deb http://mirrors.aliyun.com/debian-security bullseye-security main contrib non-free\n \
      deb-src http://mirrors.aliyun.com/debian-security bullseye-security main contrib non-free\n \
      \n \
      deb http://mirrors.aliyun.com/debian/ bullseye-updates main contrib non-free\n \
      deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main contrib non-free\n \
      \n \
      deb http://mirrors.aliyun.com/debian/ bullseye-backports main contrib non-free\n \
      deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main contrib non-free\n \
      " | tee /etc/apt/sources.list
      
      # 安装 curl&&vim
      RUN apt-get update -y && apt-get install -y curl && apt-get install -y vim
      
    5. 执行构建:
      docker build -t dotnet-runtime-7.0 -f ./Dockerfile .
  3. 推送镜像到 Nexus 仓库


    1. 镜像登录认证:
      docker login push.nexus.devops.test.com -u pusher -p devops666
    2. 打标签:
      docker tag dotnet-runtime -7.0 push.nexus.devops.test.com/projectbuild/dotnet-runtime-7.0
    3. 推送镜像:
      docker push push.nexus.devops.test.com/projectbuild/dotnet-runtime-7.0
    4. 记得清理本地缓存镜像:
      docker rmi dotnet-runtime-7.0 && docker rmi push.nexus.devops.test.com/projectbuild/dotnet-runtime-7.0
  4. 使用镜像


    1. 后续使用 dotnet runtime 7.0 就可以直接使用
      nexus.devops.test.com/projectbuild/dotnet-runtime-7.0
      即可
    2. 直接拉取:
      docker pull nexus.devops.test.com/projectbuild/dotnet-runtime-7.0

构建 zhontai 后端项目的应用镜像

制作完镜像,下面将使用 sdk 镜像打包项目生成部署文件,再使用 runtime 镜像部署运行。

  1. 下载/克隆项目 admin.core 到服务器,进入项目目录开始执行


    1. # 克隆项目
      git clone https://github.com/zhontai/Admin.Core.git -b v3.7.0
      # 进入项目 cd Admin.Core
      cd Admin.Core
      
    2. src 为.net core 项目代码
  2. 使用 sdk 镜像进行打包,生成部署文件到 publish_output


    1. docker run -i --rm
      创建一个临时容器,容器退出后自动删除容器
    2. -v ./build:/build
      挂载 MSBuild 属性文件目录(./src/Directory.Build.props 中使用)
    3. -v ./src:/src
      挂载源码到容器中
    4. -v ./publish_output:/publish_output
      挂载构建物输出目录
    5. --name build_zhontai_api
      指定运行的容器名称
    6. nexus.devops.test.com/projectbuild/dotnet-sdk-7.0
      sdk 镜像
    7. /bin/bash -c "xxx"
      以交互模式运行容器,运行时执行命令
    8. 若有自定义 nuget 仓库的包还需挂载
      /root/.nuget
      目录,或直接制作在镜像中
    9. 记得挂载 build 目录,否则会提示:Invalid framework identifier
    10. docker run -i --rm \
      -v ./build:/build \
      -v ./src:/src \
      -v ./publish_output:/publish_output \
      --name build_zhontai_api \
      nexus.devops.test.com/projectbuild/dotnet-sdk-7.0 \
      /bin/bash -c 'dotnet publish /src/hosts/ZhonTai.Host -c Release -o /publish_output --runtime linux-x64 --framework net7.0'
      
    11. 执行成功后程序包就生成到 publish_output 中了
  3. 使用 runtime 镜像制作应用镜像


    1. 将上一步的构建物 Admin.Core/publish_output 添加到运行时镜像中
    2. 使用 echo 创建一个 Dockerfile
    3. #创建Dockerfile
      echo 'FROM nexus.devops.test.com/projectbuild/dotnet-runtime-7.0 AS runtime 
      WORKDIR /app 
      COPY ./publish_output /app 
      ENV ASPNETCORE_URLS=http://+:8000 
      ENTRYPOINT ["dotnet", "ZhonTai.Host.dll"]' > Dockerfile
      
    4. 执行构建:
      docker build -t zhontai_api .
    5. 运行测试,成功
  4. 推送镜像到仓库


    1. #打标签
      docker tag zhontai_api push.nexus.devops.test.com/projectapp/zhontai_api
      #推送
      docker push push.nexus.devops.test.com/projectapp/zhontai_api
      
    2. 推送成功,这里手动只构建的 latest 版本,若使用自动化构建,还需构建对应版本的镜像,以支持快速回滚

Vue 3 项目打包

构建所需一个 node 镜像用于 vue 项目打包,nginx 用于部署前台项目

  1. node 镜像地址:
    https://hub.docker.com/r/library/node
    ,选择版本:node:18.17.1
  2. nginx 镜像地址:
    https://hub.docker.com/_/nginx
    ,选择版本:nginx:1.24.0

构建 nodejs 18.17.1 打包镜像

  1. 拉取 nodejs 镜像:
    docker pull node:18.17.1

  2. 将 node 镜像 vue-node-18 打上标签推送到仓库


    1. #拉取仓库
      docker pull node:18.17.1
      # 打标签
      docker tag node:18.17.1 push.nexus.devops.test.com/projectbuild/vue-node-18.17
      #推送
      docker push push.nexus.devops.test.com/projectbuild/vue-node-18.17
      
    2. 测试使用:
      docker run -it --rm nexus.devops.test.com/projectbuild/vue-node-18.17 /bin/bash -c "node -v"

构建 nginx 1.24 运行时镜像

  1. 拉取 nginx 镜像:
    docker pull nginx:1.24

  2. 将 nginx 镜像 vue-nginx-1.24 打上标签推送到仓库


    1. #拉取仓库
      docker pull nginx:1.24
      # 打标签
      docker tag nginx:1.24 push.nexus.devops.test.com/projectbuild/vue-nginx-1.24
      #推送
      docker push push.nexus.devops.test.com/projectbuild/vue-nginx-1.24
      
    2. 测试使用:
      docker run -it --rm nexus.devops.test.com/projectbuild/vue-nginx-1.24 /bin/bash
      进入容器后启用 nginx,并使用 curl
      http://localhost
      测试 nginx 可用

构建 zhontai 前端项目的应用镜像

  1. 下载/克隆项目 admin.ui.plus 到文件夹


    1. # 克隆项目
      git clone https://github.com/zhontai/admin.ui.plus.git -b v2.2.0
      # 进入项目cd admin.ui.plus
      cd admin.ui.plus
      
      # 修改接口地址 
      # 编辑.env.production 中的 VITE_API_URL 配置为接口地址
      
  2. 使用 node 镜像进行打包,生成文件到 dist


    1. docker run -i --rm \
      -v ./:/app \
      --name build_zhontai_webui \
      nexus.devops.test.com/projectbuild/vue-node-18.17 \
      /bin/bash -c 'cd /app 
      npm config set registry https://registry.npmmirror.com
      npm install
      npm run build'
      
    2. 执行成功,构建输出到 dist 中
  3. 使用 nginx 镜像制作应用镜像


    1. # 创建nginx.conf
      
      echo '
      server {
          listen       80;
          server_name  localhost;
          charset utf-8;
          location / {
              root   /usr/share/nginx/html;
              try_files $uri $uri/ /index.html;
              index  index.html index.htm;
          }
      
          #error_page  404              /404.html;
          error_page   500 502 503 504  /50x.html;
          location = /50x.html {
              root   html;
          }
      }
      '> vue-nginx.conf
      #创建Dockerfile
      echo '
      FROM nexus.devops.test.com/projectbuild/vue-nginx-1.24
      EXPOSE 80
      COPY ./dist /usr/share/nginx/html
      COPY ./vue-nginx.conf /etc/nginx/conf.d/default.conf' > Dockerfile
      
    2. 执行构建:
      docker build -t zhontai_webui .
    3. 测试访问成功
  4. 推送镜像到仓库

#打标签
docker tag zhontai_webui push.nexus.devops.test.com/projectapp/zhontai_webui
#推送
docker push push.nexus.devops.test.com/projectapp/zhontai_webui

将 Docker 应用镜像部署到 K8S

应用镜像打包成功,现在需要将两个应用精选镜像部署到 k8s 中

应用镜像的拉取凭证设置

因为 nexus 部署在局域网,并且配置的域名是局域网域名,所以面临着如何在 k8s 中访问
https://nexus.devops.test.com
获取镜像的问题,目前我的解决方法时每个节点机器都配置好对应 dns

要想访问到 nexus 仓库,需要满足两个条件,一个是访问到仓库,一个是仓库的认证

  1. 给 k8s 所有节点添加 dns 设置
    nameserver 192.168.123.214


    1. 使用
      docker login nexus.devops.test.com -u puller -p devops666
      在宿主机中登录仓库确保可以在节点拉取镜像
  2. 创建 nexus 登录凭证


    1. kubectl create secret \
      docker-registry \
      nexus-login-registry \
      --docker-server=nexus.devops.test.com \
      --docker-username=puller \
      --docker-password=devops666 \
      -n default
      

使用 Deployment 部署应用

配置仅供参考,关于数据库,配置文件,日志,上传文件等未处理

  • 创建后端部署配置:zhontai_api.yaml


    • ---
      ## 配置服务
      kind: Service
      apiVersion: v1
      metadata:
        name: app-zhontai-api
        namespace: default
        labels:
          app: app-zhontai-api
      spec:
        selector:
          app: app-zhontai-api
        type: ClusterIP
        ports:
          - name: p80
            port: 80
            targetPort: 8000
      ---
      
      kind: Deployment # 指定创建资源的角色/类型
      apiVersion: apps/v1 # 指定api版本,此值必须在kubectl api-versions中
      metadata: # 资源的元数据/属性
        name: app-zhontai-api # 资源的名字,在同一个namespace中必须唯一
        namespace: default # 部署在哪个namespace中
        labels: # 设定资源的标签
          app: app-zhontai-api
      spec: # 资源规范字段  
        selector:
          matchLabels:
            app: app-zhontai-api
        replicas: 2 # 声明副本数目
        revisionHistoryLimit: 2 # 保留历史版本
        strategy: # 策略
          rollingUpdate: # 滚动更新
            maxSurge: 1 # 最大额外可以存在的副本数,可以为百分比,也可以为整数
            maxUnavailable: 1 # 示在更新过程中能够进入不可用状态的 Pod 的最大值,可以为百分比,也可以为整数
          type: RollingUpdate # 滚动更新策略
        template: # 模版
          metadata: # 资源的元数据/属性
            labels: # 设定资源的标签
              app: app-zhontai-api
          spec: # 资源规范字段
            containers:
              - image: nexus.devops.test.com/projectapp/zhontai_api:latest # 容器使用的镜像地址
                name: app-zhontai-api # 容器的名字
                # 每次Pod启动拉取镜像策略,三个选择 Always、Never、IfNotPresent
                # Always,每次都检查;Never,每次都不检查(不管本地是否有);IfNotPresent,如果本地有就不检查,如果没有就拉取
                imagePullPolicy: Always 
                resources: # 资源管理
                  # limits: # 最大使用
                  #   cpu: 300m # CPU,1核心 = 1000m
                  #   memory: 500Mi # 内存,1G = 1024Mi
                  # requests: # 容器运行时,最低资源需求,也就是说最少需要多少资源容器才能正常运行
                  #   cpu: 100m
                  #   memory: 100Mi
                livenessProbe: # pod 内部健康检查的设置
                  httpGet: # 通过httpget检查健康,返回200-399之间,则认为容器正常
                    path: /admin/health # URI地址
                    port: 8000 # 端口
                    scheme: HTTP # 协议
                  initialDelaySeconds: 10 # 表明第一次检测在容器启动后多长时间后开始
                  timeoutSeconds: 5 # 检测的超时时间
                  periodSeconds: 30 # 检查间隔时间
                  successThreshold: 1 # 成功门槛
                  failureThreshold: 5 # 失败门槛,连接失败5次,pod杀掉,重启一个新的pod
                ports:
                  - name: http # 名称
                    containerPort: 80 # 容器开发对外的端口
                    protocol: TCP # 协议
                env:
                  # 时区
                  - name: TZ
                    value: Asia/Shanghai
                  # app name
                  - name: APP_NAME
                    value: app.zhontai.api
                # 挂载
                volumeMounts:
                  - name: app-logs
                    mountPath: /logs #容器中的路径
            # 卷轴
            volumes:
              - name: app-logs
                hostPath: 
                  path: /app/logs #将日志存放在宿主机的路径,需要在宿主机创建目录
                  type: Directory
            #重启策略
            restartPolicy: Always    
            imagePullSecrets: # 镜像仓库拉取密钥
              - name: nexus-login-registry
      
  • 执行部署:
    kubectl apply -f zhontai_api.yaml

  • 创建前端部署配置:zhontai_webui.yaml


    • ---
      ## 配置服务
      kind: Service
      apiVersion: v1
      metadata:
        name: app-zhontai-webui
        namespace: default
        labels:
          app: app-zhontai-webui
      spec:
        selector:
          app: app-zhontai-webui
        type: ClusterIP
        ports:
          - name: p80
            port: 80
            targetPort: 80
      ---
      
      kind: Deployment # 指定创建资源的角色/类型
      apiVersion: apps/v1 # 指定api版本,此值必须在kubectl api-versions中
      metadata: # 资源的元数据/属性
        name: app-zhontai-webui # 资源的名字,在同一个namespace中必须唯一
        namespace: default # 部署在哪个namespace中
        labels: # 设定资源的标签
          app: app-zhontai-webui
      spec: # 资源规范字段  
        selector:
          matchLabels:
            app: app-zhontai-webui
        replicas: 2 # 声明副本数目
        revisionHistoryLimit: 2 # 保留历史版本
        strategy: # 策略
          rollingUpdate: # 滚动更新
            maxSurge: 1 # 最大额外可以存在的副本数,可以为百分比,也可以为整数
            maxUnavailable: 1 # 示在更新过程中能够进入不可用状态的 Pod 的最大值,可以为百分比,也可以为整数
          type: RollingUpdate # 滚动更新策略
        template: # 模版
          metadata: # 资源的元数据/属性
            labels: # 设定资源的标签
              app: app-zhontai-webui
          spec: # 资源规范字段
            containers:
              - image: nexus.devops.test.com/projectapp/zhontai_webui:latest # 容器使用的镜像地址
                name: app-zhontai-webui # 容器的名字
                # 每次Pod启动拉取镜像策略,三个选择 Always、Never、IfNotPresent
                # Always,每次都检查;Never,每次都不检查(不管本地是否有);IfNotPresent,如果本地有就不检查,如果没有就拉取
                imagePullPolicy: Always 
                resources: # 资源管理
                  # limits: # 最大使用
                  #   cpu: 300m # CPU,1核心 = 1000m
                  #   memory: 500Mi # 内存,1G = 1024Mi
                  # requests: # 容器运行时,最低资源需求,也就是说最少需要多少资源容器才能正常运行
                  #   cpu: 100m
                  #   memory: 100Mi
                livenessProbe: # pod 内部健康检查的设置
                  httpGet: # 通过httpget检查健康,返回200-399之间,则认为容器正常
                    path: / # URI地址
                    port: 80 # 端口
                    scheme: HTTP # 协议
                  initialDelaySeconds: 10 # 表明第一次检测在容器启动后多长时间后开始
                  timeoutSeconds: 5 # 检测的超时时间
                  periodSeconds: 30 # 检查间隔时间
                  successThreshold: 1 # 成功门槛
                  failureThreshold: 5 # 失败门槛,连接失败5次,pod杀掉,重启一个新的pod
                ports:
                  - name: http # 名称
                    containerPort: 80 # 容器开发对外的端口
                    protocol: TCP # 协议
                env:
                  # 时区
                  - name: TZ
                    value: Asia/Shanghai
                  # app name
                  - name: APP_NAME
                    value: app.zhontai.webui
            #重启策略
            restartPolicy: Always    
            imagePullSecrets: # 镜像仓库拉取密钥
              - name: nexus-login-registry
      
  • 执行部署:
    kubectl apply -f zhontai_webui.yaml

配置 Ingress 使用域名访问

  • 部署成功后添加对应 ingress 配置,即可使用域名访问

前端项目需要修改为对应的接口地址

确保绑定的域名正常解析到 k8s 节点,即可使用域名访问了,我这里使用的 DnsServer 泛解析,故可以直接访问,

至此,一步步将一个单体项目部署到了 k8s 中,仅供参考,实际如果时微服务,还设计到一些通用和环境的配置,后面再慢慢分享。

根据上面的步骤,后面分享将其整理成脚本,以便后续可以直接使用。

相关文档

后语

本文始于2023末,结束于2024始。

2023的最后两个月,是这几年以来,学习,产出最高的的两个月。

始于国庆,不止步于元旦。

新年快乐!

随着 2023 年的结束,我们也迎来了 2024 年的第一个工作日,新的一年就让「GitHub 热点速递」陪你一起进入工作状态吧!

说到上周的 GitHub 热搜项目就不得不提一下,
一周飙升了 8 千 Star
的 PDF 文件处理神器 Stirling-PDF。还有苹果的开源的多模态 LLM,在过去一周
悄然收获了 2 千 Star
引人关注。不过,我最喜欢的还是
专为程序员打造
的随手记事本 heynote,以及一款国人开发的
TikTok 下载神器

新增的
「开源新闻」
部分,将分享过去一周开源领域的热门新闻,
关注最新开源动态、品热搜开源项目


以下内容摘录自微博
@HelloGitHub
的 GitHub Trending 及 Hacker News 热帖(简称 HN 热帖),选项标准:
新发布
|
实用
|
有趣
,根据项目 release 时间分类,发布时间不超过 14 day 的项目会标注
New
,无该标志则说明项目 release 超过半月。由于本文篇幅有限,还有部分项目未能在本文展示,望周知

决策树
算法是一种既可以用于
分类
,也可以用于
回归
的算法。

决策树回归
是通过对输入特征的不断划分来建立一棵决策树,每一步划分都基于当前数据集的最优划分特征。
它的目标是最小化总体误差或最大化预测精度,其构建通常采用自上而下的贪心搜索方式,通过比较不同划分标准来选择最优划分。

决策树回归
广泛应用于各种回归问题,如预测房价、股票价格、客户流失等。

1. 算法概述

决策树相关的诸多算法之中,有一种
CART算法
,全称是
classification and regression tree
(分类与回归树)。
顾名思义,这个算法既可以用来分类,也可以用来回归,本篇主要介绍其在回归问题上的应用。

决策树算法的核心在于生成一棵决策树过程中,如何划分各个特征到树的不同分支上去。
CART算法
是根据基尼系数(Gini)来划分特征的,每次选择基尼系数最小的特征作为最优切分点。

其中基尼系数的计算方法:
\(gini(p) = \sum_{i=1}^n p_i(1-p_i)=1-\sum_{i=1}^n p_i^2\)

2. 创建样本数据

这次的回归样本数据,我们用
scikit-learn
自带的玩具数据集中的
糖尿病数据集

关于玩具数据集的内容,可以参考:
TODO

from sklearn.datasets import load_diabetes

# 糖尿病数据集
diabetes = load_diabetes()
X = diabetes.data
y = diabetes.target

这个数据集中大约有
400多条
数据。

3. 模型训练

训练之前,为了减少算法误差,先对数据进行
标准化
处理。

from sklearn import preprocessing as pp

# 数据标准化
X = pp.scale(X)
y = pp.scale(y)

接下来分割
训练集

测试集

from sklearn.model_selection import train_test_split

# 分割训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1)

然后用
scikit-learn
中的
DecisionTreeRegressor
模型来训练:

from sklearn.tree import DecisionTreeRegressor

# 定义决策树回归模型
reg = DecisionTreeRegressor(max_depth=2)

# 训练模型
reg.fit(X_train, y_train)

# 在测试集上进行预测
y_pred = reg.predict(X_test)

DecisionTreeRegressor
的主要参数包括:

  1. criterion
    :用于衡量节点划分质量的指标。可以选择的值有'mse'(均方误差)或'mae'(平均绝对误差)。默认值为'mse',适用于大多数情况。
  2. splitter
    :用于决定节点如何进行划分的策略。可以选择的值有'best'(选择最佳划分)或'random'(随机划分)。默认值为'best'。
  3. max_depth
    :决策树的最大深度。默认值为None,表示不限制最大深度。增加最大深度有助于更好地拟合训练数据,但可能导致过拟合。
  4. random_state
    :用于设置随机数生成器的种子。默认值为None,表示使用随机数生成器。
  5. ccp_alpha
    :用于控制正则化强度的参数。默认值为None,表示不进行正则化。
  6. max_samples
    :用于控制每个节点最少需要多少样本才能进行分裂。默认值为None,表示使用整个数据集。
  7. min_samples_split
    :用于控制每个节点最少需要多少样本才能进行分裂。默认值为2,表示每个节点至少需要2个样本才能进行分裂。
  8. min_samples_leaf
    :用于控制每个叶子节点最少需要多少样本才能停止分裂。默认值为1,表示每个叶子节点至少需要1个样本才能停止分裂。
  9. min_weight_fraction_leaf
    :用于控制每个叶子节点最少需要多少样本的权重才能停止分裂。默认值为0.0,表示每个叶子节点至少需要0个样本的权重才能停止分裂。
  10. max_features
    :用于控制每个节点最多需要考虑多少个特征进行分裂。默认值为None,表示使用所有特征。
  11. max_leaf_nodes
    :用于控制决策树最多有多少个叶子节点。默认值为None,表示不限制叶子节点的数量。
  12. min_impurity_decrease
    :用于控制每个节点最少需要减少多少不纯度才能进行分裂。默认值为0.0,表示每个节点至少需要减少0个不纯度才能进行分裂。
  13. min_impurity_split
    :用于控制每个叶子节点最少需要减少多少不纯度才能停止分裂。默认值为None,表示使用min_impurity_decrease参数。
  14. class_weight
    :用于设置类别权重的字典或方法。默认值为None,表示使用均匀权重。

最后验证模型的训练效果:

from sklearn import metrics

# 在测试集上进行预测
y_pred = reg.predict(X_test)

mse, r2, m_error = 0.0, 0.0, 0.0
y_pred = reg.predict(X_test)
mse = metrics.mean_squared_error(y_test, y_pred)
r2 = metrics.r2_score(y_test, y_pred)
m_error = metrics.median_absolute_error(y_test, y_pred)

print("均方误差:{}".format(mse))
print("复相关系数:{}".format(r2))
print("中位数绝对误差:{}".format(m_error))

# 运行结果
均方误差:0.5973573097746598
复相关系数:0.5153160857515913
中位数绝对误差:0.5496418600646286

从预测的
误差
来看,训练的
效果还不错

这里用
DecisionTreeRegressor
训练模型时使用了参数
max_depth=2

我从
max_depth=1
逐个尝试到了
max_depth=10
,发现
max_depth=2
时误差最小。

4. 总结

决策树回归
具有直观、易于理解、易于实现等优点。
生成的决策树可以直观地展示出输入特征与输出结果之间的关系,因此对于非专业人士来说也易于理解。
此外,
决策树回归算法
相对简单,易于实现,且对数据的预处理要求较低。

然而,决策树回归也存在一些缺点。
首先,它容易
过拟合
训练数据,特别是当训练数据量较小时;
其次,决策树的性能受
划分标准选择的影响较大
,不同的划分标准可能会导致生成的决策树性能差异较大;
此外,决策树回归在处理大规模数据时可能会
比较耗时
,因为需要遍历整个数据集进行训练和预测。

Java JVM内存结构的面试常问知识

说说JVM内存整体的结构?线程私有还是共享的?

JVM 整体架构,中间部分就是 Java 虚拟机定义的各种运行时数据区域。

Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。

  • 线程私有
    :程序计数器、虚拟机栈、本地方法区
  • 线程共享
    :堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、代码缓存)

什么是程序计数器(线程私有)?

PC 寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。

  • PC寄存器为什么会被设定为线程私有的?

多线程在一个特定的时间段内只会执行其中某一个线程方法,CPU会不停的做任务切换,这样必然会导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以为每个线程都分配了一个PC寄存器,每个线程都独立计算,不会互相影响。

什么是虚拟机栈(线程私有)?

主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。

  • 特点?
  1. 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
  2. JVM 直接对虚拟机栈的操作只有两个:每个方法执行,伴随着
    入栈
    (进栈/压栈),方法执行结束
    出栈
  3. 栈不存在垃圾回收问题
  4. 可以通过参数
    -Xss
    来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
  • 该区域有哪些异常
  1. 如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个
    StackOverflowError
    异常
  2. 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个
    OutOfMemoryError
    异常
  • 栈帧的内部结构?
  1. 局部变量表(Local Variables)
  2. 操作数栈(Operand Stack)(或称为表达式栈)
  3. 动态链接(Dynamic Linking):指向运行时常量池的方法引用
  4. 方法返回地址(Return Address):方法正常退出或异常退出的地址
  5. 一些附加信息

Java虚拟机栈如何进行方法计算的?

以如下代码为例:

private static int add(int a, int b) {
    int c = 0;
    c = a + b;
    return c;
}

可以通过jsclass 等工具查看bytecode

压栈的步骤如下:

0:   iconst_0 // 0压栈
1:   istore_2 // 弹出int,存放于局部变量2
2:   iload_0  // 把局部变量0压栈
3:   iload_1  // 局部变量1压栈
4:   iadd     //弹出2个变量,求和,结果压栈
5:   istore_2 //弹出结果,放于局部变量2
6:   iload_2  //局部变量2压栈
7:   ireturn  //返回

如果计算100+98的值,那么操作数栈的变化如下图

什么是本地方法栈(线程私有)?

  • 本地方法接口

一个 Native Method 就是一个 Java 调用非 Java 代码的接口。我们知道的 Unsafe 类就有很多本地方法。

  • 本地方法栈(Native Method Stack)

Java 虚拟机栈用于管理 Java 方法的调用,而本地方法栈用于管理本地方法的调用

什么是方法区(线程共享)?

方法区(method area)只是
JVM 规范
中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,并没有规定如何去实现它,不同的厂商有不同的实现。而
永久代(PermGen)

Hotspot
虚拟机特有的概念, Java8 的时候又被
元空间
取代了,永久代和元空间都可以理解为方法区的落地实现。

JDK1.8之前调节方法区大小:

-XX:PermSize=N //方法区(永久代)初始大小
-XX:MaxPermSize=N //方法区(永久代)最大大小,超出这个值将会抛出OutOfMemoryError 

JDK1.8开始方法区(HotSpot的永久代)被彻底删除了,取而代之的是元空间,元空间直接使用的是本机内存。参数设置:

-XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置Metaspace的最大大小

栈、堆、方法区的交互关系

永久代和元空间内存使用上的差异?

Java虚拟机规范中只定义了方法区用于存储已被虚拟机加载的类信息、常量、静态变量和即时编译后的代码等数据

  1. jdk1.7开始符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中,但分离的并不彻底,此时永久代中还保存另一些与类的元数据无关的杂项
  2. jdk8后HotSpot 原永久代中存储的类的
    元数据将存储在metaspace
    中,而
    类的静态变量和字符串常量将放在Java堆中
    ,metaspace是方法区的一种实现,只不过它使用的不是虚拟机内的内存,而是本地内存。在元空间中保存的数据比永久代中纯粹很多,就只是类的元数据,这些信息只对编译期或JVM的运行时有用。
  3. 永久代有一个JVM本身设置固定大小上线,无法进行调整,而
    元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError
  4. 符号引用没有存在元空间中,而是存在native heap中
    ,这是两个方式和位置,不过都可以算作是本地内存,在虚拟机之外进行划分,没有设置限制参数时只受物理内存大小限制,即只有占满了操作系统可用内存后才OOM。

堆区内存是怎么细分的?

对于大多数应用,Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。

为了进行高效的垃圾回收,虚拟机把堆内存
逻辑上
划分成三块区域(分代的唯一理由就是优化 GC 性能):

  1. 新生带(年轻代):新对象和没达到一定年龄的对象都在新生代
  2. 老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大

Java 虚拟机规范规定,Java 堆可以是处于物理上不连续的内存空间中,只要逻辑上是连续的即可,像磁盘空间一样。实现时,既可以是固定大小,也可以是可扩展的,主流虚拟机都是可扩展的(通过
-Xmx

-Xms
控制),如果堆中没有完成实例分配,并且堆无法再扩展时,就会抛出
OutOfMemoryError
异常。

  • 年轻代 (Young Generation)

年轻代是所有新对象创建的地方。当填充年轻代时,执行垃圾收集。这种垃圾收集称为
Minor GC
。年轻一代被分为三个部分——伊甸园(
Eden Memory
)和两个幸存区(
Survivor Memory
,被称为from/to或s0/s1),默认比例是
8:1:1

  1. 大多数新创建的对象都位于 Eden 内存空间中
  2. 当 Eden 空间被对象填充时,执行
    Minor GC
    ,并将所有幸存者对象移动到一个幸存者空间中
  3. Minor GC 检查幸存者对象,并将它们移动到另一个幸存者空间。所以每次,一个幸存者空间总是空的
  4. 经过多次 GC 循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代
  • 老年代(Old Generation)

旧的一代内存包含那些经过许多轮小型 GC 后仍然存活的对象。通常,垃圾收集是在老年代内存满时执行的。老年代垃圾收集称为 主GC(Major GC),通常需要更长的时间。

大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝

JVM中对象在堆中的生命周期?

  1. 在 JVM 内存模型的堆中,堆被划分为新生代和老年代
    • 新生代又被进一步划分为
      Eden区

      Survivor区
      ,Survivor 区由
      From Survivor

      To Survivor
      组成
  2. 当创建一个对象时,对象会被优先分配到新生代的 Eden 区
    • 此时 JVM 会给对象定义一个
      对象年轻计数器

      -XX:MaxTenuringThreshold
  3. 当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC)
    • JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1
    • 对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1
  4. 如果分配的对象超过了
    -XX:PetenureSizeThreshold
    ,对象会
    直接被分配到老年代

JVM中对象的分配过程?

为对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法和内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片。

  1. new 的对象先放在伊甸园区,此区有大小限制
  2. 当伊甸园的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
  3. 然后将伊甸园中的剩余对象移动到幸存者 0 区
  4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 0 区,如果没有回收,就会放到幸存者 1 区
  5. 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区
  6. 什么时候才会去养老区呢?默认是 15 次回收标记
  7. 在养老区,相对悠闲。当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理
  8. 若养老区执行了 Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常

什么是 TLAB (Thread Local Allocation Buffer)?

  • 从内存模型而不是垃圾回收的角度,对 Eden 区域继续进行划分,JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内
  • 多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为
    快速分配策略
  • OpenJDK 衍生出来的 JVM 大都提供了 TLAB 设计

为什么要有 TLAB ?

  • 堆区是线程共享的,任何线程都可以访问到堆区中的共享数据
  • 由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度

尽管不是所有的对象实例都能够在 TLAB 中成功分配内存,但 JVM 确实是将 TLAB 作为内存分配的首选。

在程序中,可以通过
-XX:UseTLAB
设置是否开启 TLAB 空间。

默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden 空间的 1%,我们可以通过
-XX:TLABWasteTargetPercent
设置 TLAB 空间所占用 Eden 空间的百分比大小。

一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。