-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 319 KB
/
content.json
1
{"meta":{"title":"ydd","subtitle":"ydd","description":"","author":"Ydd","url":"https://g-ydg.github.io","root":"/"},"pages":[{"title":"项目","date":"2024-10-04T01:12:20.688Z","updated":"2024-10-04T01:12:20.688Z","comments":false,"path":"repository/index.html","permalink":"https://g-ydg.github.io/repository/index.html","excerpt":"","text":""}],"posts":[{"title":"Wechaty+ChatGPT 实现个人聊天机器人","slug":"Wechaty+ChatGPT 实现个人聊天机器人","date":"2023-09-03T22:32:07.000Z","updated":"2024-10-04T01:13:18.016Z","comments":true,"path":"2023/09/03/Wechaty+ChatGPT 实现个人聊天机器人/","link":"","permalink":"https://g-ydg.github.io/2023/09/03/Wechaty+ChatGPT%20%E5%AE%9E%E7%8E%B0%E4%B8%AA%E4%BA%BA%E8%81%8A%E5%A4%A9%E6%9C%BA%E5%99%A8%E4%BA%BA/","excerpt":"","text":"介绍 使用 API 调用,无需科学上网 使用 UOS 协议(包括本项目)登录微信有封号风险,请谨慎使用 流程注册 ChatGPT 账号 注册地址: https://chat.openai.com/chat 注册教程: https://juejin.cn/post/7173447848292253704 获取 ChatGPT 接口访问密钥 创建密钥:https://platform.openai.com/account/api-keys 安装环境要求 node >= 18.0.0 npm >= 9.5.0 源码安装12git clone https://github.com/sunshanpeng/wechaty-chatgpt.gitcd wechaty-chatgpt 123export OPENAI_API_KEY=上文所创建的密钥npm inpm run chatgpt 感谢 https://github.com/sunshanpeng/wechaty-chatgpt/ https://github.com/wechaty/wechaty/ https://github.com/transitive-bullshit/chatgpt-api","categories":[],"tags":[]},{"title":"阿里云CLI","slug":"阿里云CLI","date":"2023-05-17T22:49:05.000Z","updated":"2024-10-04T01:13:18.044Z","comments":true,"path":"2023/05/17/阿里云CLI/","link":"","permalink":"https://g-ydg.github.io/2023/05/17/%E9%98%BF%E9%87%8C%E4%BA%91CLI/","excerpt":"","text":"简介阿里云命令行工具(Alibaba Cloud Command Line Interface)是在 Alibaba Cloud SDK for Go 之上构建的开源工具。您可以在命令行 Shell 中,使用aliyun命令与阿里云服务进行交互,管理您的阿里云资源。 安装 在 Windows 上安装阿里云 CLI 在 Linux 上安装阿里云 CLI 在 macOS 上安装阿里云 CLI 使用配置 AccessKey 凭证 –access-key-id:指定您的 AccessKey ID。 –access-key-secret:指定您的 AccessKey Secret。 配置名为 akProfile 的 AccessKey 凭证。 123456aliyun configure set \\ --profile akProfile \\ --mode AK \\ --region cn-shenzhen \\ --access-key-id AccessKeyId \\ --access-key-secret AccessKeySecret 列出所有配置1aliyun configure list 查询阿里云 ECS 地域1aliyun ecs DescribeRegions 查询SSH 密钥对1aliyun ecs DescribeKeyPairs 导入 SSH 密钥对 –region:地域。 –RegionIdt:地域 ID。 --KeyPairName:密钥对名称。 --PublicKeyBody:密钥对的公钥内容。 在深圳地域导入名为 aliyunKey 的密钥对。 1aliyun ecs ImportKeyPair --region cn-shenzhen --RegionId 'cn-shenzhen' --KeyPairName aliyunKey --PublicKeyBody xxx 更多如何使用阿里云命令行 Shell 管理您的阿里云资源_OpenAPI Explorer-阿里云帮助中心 GitHub - aliyun/aliyun-cli: Alibaba Cloud CLI","categories":[],"tags":[]},{"title":"Kubernetes Tutorials | k8s 教程","slug":"Kubernetes Tutorials | k8s 教程","date":"2023-04-03T18:13:01.000Z","updated":"2024-10-04T01:13:25.067Z","comments":true,"path":"2023/04/03/Kubernetes Tutorials | k8s 教程/","link":"","permalink":"https://g-ydg.github.io/2023/04/03/Kubernetes%20Tutorials%20%EF%BD%9C%20k8s%20%E6%95%99%E7%A8%8B/","excerpt":"","text":"【代码仓库】Kubernetes Tutorials | k8s 教程 k8s 作为云原生时代的操作系统,学习它的必要性不言而喻,如果你遇到了任何问题,可以在 Discussions 中评论或者 Issue 中提出,如果你觉得这个仓库对你有价值,欢迎 star 或者提 PR / Issue,让它变得更好! 在学习本教程前,需要注意本教程侧重于实战引导,以渐进式修改代码的方式,将从最基础的 container 容器的定义开始,经过 pod, deployment, service, ingress, configmap, secret 等资源直到用 helm 来打包部署一套完整服务。所以如果你对容器和 k8s 的基础理论知识不甚了解的话,建议先从 官网文档 或者其它教程获取基础理论知识,再通过实战加深对知识的掌握! 这里是文档的索引: 准备工作 container pod deployment service ingress namespace configmap secret job/cronjob helm dashboard Translate English(未完成) 下面是所有文档的集合: kubernetes tutorials 准备工作 安装 docker 推荐安装方法 其它安装方法 安装 minikube 启动 minikube 安装 kubectl 注册 docker hub 账号登录 Container Pod Pod 与 Container 的不同 Pod 其它命令 Deployment 扩容 升级版本 Rolling Update(滚动更新) 存活探针 (livenessProb) 就绪探针 (readiness) Service ClusterIP NodePort LoadBalancer ingress Namespace Configmap Secret Job CronJob Helm 用 helm 安装 hellok8s 创建 helm charts rollback 多环境配置 helm chart 打包和发布 Dashboard kubernetes dashboard K9s Star History kubernetes tutorials准备工作在开始本教程之前,需要配置好本地环境,以下是需要安装的依赖和包。 安装 docker首先我们需要安装 docker 来打包镜像,如果你本地已经安装了 docker,那么你可以选择跳过这一小节。 推荐安装方法目前使用 Docker Desktop 来安装 docker 还是最简单的方案,打开官网下载对应你电脑操作系统的包即可 (https://www.docker.com/products/docker-desktop/), 当安装完成后,可以通过 docker run hello-world 来快速校验是否安装成功! 其它安装方法目前 Docker 公司宣布 Docker Desktop 只对个人开发者或者小型团体免费 (2021 年起对大型公司不再免费),所以如果你不能通过 Docker Desktop 的方式下载安装 docker,可以参考 这篇文章 只安装 Docker CLI。 安装 minikube我们还需要搭建一套 k8s 本地集群 (使用云厂商或者其它 k8s 集群都可) 。本地搭建 k8s 集群的方式推荐使用 minikube。 可以根据 minikube 快速安装 来进行下载安装,这里简单列举 MacOS 的安装方式,Linux & Windows 操作系统可以参考官方文档 快速安装。 1brew install minikube 启动 minikube因为 minikube 支持很多容器和虚拟化技术 (Docker, Hyperkit, Hyper-V, KVM, Parallels, Podman, VirtualBox, or VMware Fusion/Workstation),也是问题出现比较多的地方,所以这里还是稍微说明一下。 如果你使用 docker 的方案是上面推荐的 Docker Desktop ,那么你以下面的命令启动 minikube 即可,需要耐心等待下载依赖。 1minikube start --vm-driver docker --container-runtime=docker 启动完成后,运行 minikube status 查看当前状态确定是否启动成功! 如果你本地只有 Docker CLI,判断标准如果执行 docker ps 等命令,返回错误 Cannot connect to the Docker daemon at unix:///Users/xxxx/.colima/docker.sock. Is the docker daemon running? 那么就需要操作下面的命令。 1234567891011brew install hyperkitminikube start --vm-driver hyperkit --container-runtime=docker# Tell Docker CLI to talk to minikube's VMeval $(minikube docker-env)# Save IP to a hostnameecho "`minikube ip` docker.local" | sudo tee -a /etc/hosts > /dev/null# Testdocker run hello-world minikube 命令速查 minikube stop 不会删除任何数据,只是停止 VM 和 k8s 集群。 minikube delete 删除所有 minikube 启动后的数据。 minikube ip 查看集群和 docker enginer 运行的 IP 地址。 minikube pause 暂停当前的资源和 k8s 集群 minikube status 查看当前集群状态 安装 kubectl这一步是可选的,如果不安装的话,后续所有 kubectl 相关的命令,使用 minikube kubectl 命令替代即可。 如果你不想使用 minikube kubectl 或者配置相关环境变量来进行下面的教学的话,可以考虑直接安装 kubectl。 1brew install kubectl 注册 docker hub 账号登录因为默认 minikube 使用的镜像地址是 DockerHub,所以我们还需要在 DockerHub (https://hub.docker.com/) 中注册账号,并且使用 login 命令登录账号。 1docker login Container我们的旅程从一段代码开始。新建一个 main.go 文件,复制下面的代码到文件中。 123456789101112131415package mainimport ( "io" "net/http")func hello(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "[v1] Hello, Kubernetes!")}func main() { http.HandleFunc("/", hello) http.ListenAndServe(":3000", nil)} 上面是一串用 Go 写的代码,代码逻辑非常的简单,首先启动 HTTP 服务器,监听 3000 端口,当访问路由 /的时候 返回字符串 [v1] Hello, Kubernetes!。 在以前,如果你想将这段代码运行起来并测试一下。你首先需要懂得如何下载 golang 的安装包进行安装,接着需要懂得 golang module 的基本使用,最后还需要了解 golang 的编译和运行命令,才能将该代码运行起来。甚至在过程中,可能会因为环境变量问题、操作系统问题、处理器架构等问题导致编译或运行失败。 但是通过 Container (容器) 技术,只需要上面的代码,附带着对应的容器 Dockerfile 文件,那么你就不需要 golang 的任何知识,也能将代码顺利运行起来。 Container (容器) 是一种沙盒技术。它是基于 Linux 中 Namespace / Cgroups / chroot 等技术组合而成,更多技术细节可以参照这个视频 如何自己实现一个容器。 下面就是 Go 代码对应的 Dockerfile,简单的方案是直接使用 golang 的 alpine 镜像来打包,但是因为我们后续练习需要频繁的推送镜像到 DockerHub 和拉取镜像到 k8s 集群中,为了优化网络速度,我们选择先在 golang:1.16-buster 中将上述 Go 代码编译成二进制文件,再将二进制文件复制到 base-debian10 镜像中运行 (Dockerfile 不理解没有关系,不影响后续学习)。 这样我们可以将 300MB 大小的镜像变成只有 20MB 的镜像,甚至压缩上传到 DockerHub 后大小只有 10MB! 12345678910111213141516# DockerfileFROM golang:1.16-buster AS builderRUN mkdir /srcADD . /srcWORKDIR /srcRUN go env -w GO111MODULE=autoRUN go build -o main .FROM gcr.io/distroless/base-debian10WORKDIR /COPY --from=builder /src/main /mainEXPOSE 3000ENTRYPOINT ["/main"] 需要注意 main.go 文件需要和 Dockerfile 文件在同一个目录下,执行下方 docker build 命令,第一次需要耐心等待拉取基础镜像。并且需要注意将命令中 **guangzhengli** 替换成自己的 **DockerHub** 注册的账号名称。 这样我们后续可以推送镜像到自己注册的 DockerHub 仓库当中。 12345678910docker build . -t guangzhengli/hellok8s:v1# Step 1/11 : FROM golang:1.16-buster AS builder# ...# ...# Step 11/11 : ENTRYPOINT ["/main"]# Successfully tagged guangzhengli/hellok8s:v1docker images# guangzhengli/hellok8s v1 f956e8cf7d18 8 days ago 25.4MB docker build 命令完成后我们可以通过 docker images 命令查看镜像是否 build 成功,最后我们执行 docker run 命令将容器启动, -p 指定 3000 作为端口,-d 指定刚打包成功的镜像名称。 1docker run -p 3000:3000 --name hellok8s -d guangzhengli/hellok8s:v1 运行成功后,可以通过浏览器或者 curl 来访问 http://127.0.0.1:3000 , 查看是否成功返回字符串 [v1] Hello, Kubernetes!。 这里因为我本地只用 Docker CLI,而 docker runtime 是使用 minikube,所以我需要先调用 minikube ip 来返回 minikube IP 地址,例如返回了 192.168.59.100,所以我需要访问 http://192.168.59.100:3000 来判断是否成功返回字符串 [v1] Hello, Kubernetes!。 最后确认没有问题,使用 docker push 将镜像上传到远程的 DockerHub 仓库当中,这样可以供他人下载使用,也方便后续 Minikube 下载镜像使用。 需要注意将 **guangzhengli** 替换成自己的 **DockerHub** 账号名称。 1docker push guangzhengli/hellok8s:v1 经过这一节的练习,有没有对容器的强大有一个初步的认识呢?可以想象当你想部署一个更复杂的服务时,例如 Nginx,MySQL,Redis。你只需要到 DockerHub 搜索 中搜索对应的镜像,通过 docker pull 下载镜像,docker run 启动服务即可!而无需关心依赖和各种配置! Pod如果在生产环境中运行的都是独立的单体服务,那么 Container (容器) 也就够用了,但是在实际的生产环境中,维护着大规模的集群和各种不同的服务,服务之间往往存在着各种各样的关系。而这些关系的处理,才是手动管理最困难的地方。 Pod 是我们将要创建的第一个 k8s 资源,也是可以在 Kubernetes 中创建和管理的、最小的可部署的计算单元。在了解 pod 和 container 的区别之前,我们可以先创建一个简单的 pod 试试, 我们先创建 nginx.yaml 文件,编写一个可以创建 nginx 的 Pod。 123456789# nginx.yamlapiVersion: v1kind: Podmetadata: name: nginx-podspec: containers: - name: nginx-container image: nginx 其中 kind 表示我们要创建的资源是 Pod 类型, metadata.name 表示要创建的 pod 的名字,这个名字需要是唯一的。 spec.containers 表示要运行的容器的名称和镜像名称。镜像默认来源 DockerHub。 我们运行第一条 k8s 命令 kubectl apply -f nginx.yaml 命令来创建 nginx Pod。 接着通过 kubectl get pods 来查看 pod 是否正常启动。 最后通过 kubectl port-forward nginx-pod 4000:80 命令将 nginx 默认的 80 端口映射到本机的 4000 端口,打开浏览器或者 curl 来访问 http://127.0.0.1:4000 , 查看是否成功访问 nginx 默认页面! 123456789kubectl apply -f nginx.yaml# pod/nginx-pod createdkubectl get pods# nginx-pod 1/1 Running 0 6skubectl port-forward nginx-pod 4000:80# Forwarding from 127.0.0.1:4000 -> 80# Forwarding from [::1]:4000 -> 80 kubectl exec -it 可以用来进入 Pod 内容器的 Shell。通过命令下面的命令来配置 nginx 的首页内容。 12345kubectl exec -it nginx-pod /bin/bashecho "hello kubernetes by nginx!" > /usr/share/nginx/html/index.htmlkubectl port-forward nginx-pod 4000:80 最后可以通过浏览器或者 curl 来访问 http://127.0.0.1:4000 , 查看是否成功启动 nginx 和返回字符串 hello kubernetes by nginx!。 Pod 与 Container 的不同回到 pod 和 container 的区别,我们会发现刚刚创建出来的资源如下图所示,在最内层是我们的服务 nginx,运行在 container 容器当中, container (容器) 的本质是进程,而 pod 是管理这一组进程的资源。 所以自然 pod 可以管理多个 container,在某些场景例如服务之间需要文件交换(日志收集),本地网络通信需求(使用 localhost 或者 Socket 文件进行本地通信),在这些场景中使用 pod 管理多个 container 就非常的推荐。而这,也是 k8s 如何处理服务之间复杂关系的第一个例子,如下图所示: Pod 其它命令我们可以通过 logs 或者 logs -f 命令查看 pod 日志,可以通过 exec -it 进入 pod 或者调用容器命令,通过 delete pod 或者 delete -f nginx.yaml 的方式删除 pod 资源。这里可以看到 kubectl 所有命令。 123456789kubectl logs --follow nginx-podkubectl exec nginx-pod -- lskubectl delete pod nginx-pod# pod "nginx-pod" deletedkubectl delete -f nginx.yaml# pod "nginx-pod" deleted 最后,根据我们在 container 的那节构建的 hellok8s:v1 的镜像,同时参考 nginx pod 的资源定义,你能独自编写出 hellok8s:v1 Pod 的资源文件吗。并通过 port-forward 到本地的 3000 端口进行访问,最终得到字符串 [v1] Hello, Kubernetes!。 hellok8s:v1 Pod 资源定义和相应的命令如下所示: 123456789# hellok8s.yamlapiVersion: v1kind: Podmetadata: name: hellok8sspec: containers: - name: hellok8s-container image: guangzhengli/hellok8s:v1 12345kubectl apply -f hellok8s.yamlkubectl get podskubectl port-forward hellok8s 3000:3000 Deployment在生产环境中,我们基本上不会直接管理 pod,我们需要 kubernetes 来帮助我们来完成一些自动化操作,例如自动扩容或者自动升级版本。可以想象在生产环境中,我们手动部署了 10 个 hellok8s:v1 的 pod,这个时候我们需要升级成 hellok8s:v2 版本,我们难道需要一个一个的将 hellok8s:v1 的 pod 手动升级吗? 这个时候就需要我们来看 kubernetes 的另外一个资源 deployment,来帮助我们管理 pod。 扩容首先可以创建一个 deployment.yaml 的文件。来管理 hellok8s pod。 1234567891011121314151617apiVersion: apps/v1kind: Deploymentmetadata: name: hellok8s-deploymentspec: replicas: 1 selector: matchLabels: app: hellok8s template: metadata: labels: app: hellok8s spec: containers: - image: guangzhengli/hellok8s:v1 name: hellok8s-container 其中 kind 表示我们要创建的资源是 deployment 类型, metadata.name 表示要创建的 deployment 的名字,这个名字需要是唯一的。 在 spec 里面表示,首先 replicas 表示的是部署的 pod 副本数量,selector 里面表示的是 deployment 资源和 pod 资源关联的方式,这里表示 deployment 会管理 (selector) 所有 labels=hellok8s 的 pod。 template 的内容是用来定义 pod 资源的,你会发现和 Hellok8s Pod 资源的定义是差不多的,唯一的区别是我们需要加上 metadata.labels 来和上面的 selector.matchLabels 对应起来。来表明 pod 是被 deployment 管理,不用在template 里面加上 metadata.name 是因为 deployment 会自动为我们创建 pod 的唯一name。 接下来输入下面的命令,可以创建 deployment 资源。通过 get 和 delete pod 命令,我们会初步感受 deployment 的魅力。每次创建的 pod 名称都会变化,某些命令记得替换成你的 pod 的名称 12345678910111213141516kubectl apply -f deployment.yamlkubectl get deployments#NAME READY UP-TO-DATE AVAILABLE AGE#hellok8s-deployment 1/1 1 1 39skubectl get pods#NAME READY STATUS RESTARTS AGE#hellok8s-deployment-77bffb88c5-qlxss 1/1 Running 0 119skubectl delete pod hellok8s-deployment-77bffb88c5-qlxss#pod "hellok8s-deployment-77bffb88c5-qlxss" deletedkubectl get pods#NAME READY STATUS RESTARTS AGE#hellok8s-deployment-77bffb88c5-xp8f7 1/1 Running 0 18s 我们会发现一个有趣的现象,当手动删除一个 pod 资源后,deployment 会自动创建一个新的 pod,这和我们之前手动创建 pod 资源有本质的区别!这代表着当生产环境管理着成千上万个 pod 时,我们不需要关心具体的情况,只需要维护好这份 deployment.yaml 文件的资源定义即可。 接下来我们通过自动扩容来加深这个知识点,当我们想要将 hellok8s:v1 的资源扩容到 3 个副本时,只需要将 replicas 的值设置成 3,接着重新输入 kubectl apply -f deployment.yaml 即可。如下所示: 1234567891011121314151617apiVersion: apps/v1kind: Deploymentmetadata: name: hellok8s-deploymentspec: replicas: 3 selector: matchLabels: app: hellok8s template: metadata: labels: app: hellok8s spec: containers: - image: guangzhengli/hellok8s:v1 name: hellok8s-container 可以在 kubectl apply 之前通过新建窗口执行 kubectl get pods --watch 命令来观察 pod 启动和删除的记录,想要减少副本数时也很简单,你可以尝试将副本数随意增大或者缩小,再通过 watch 来观察它的状态。 升级版本我们接下来尝试将所有 v1 版本的 pod 升级到 v2 版本。首先我们需要构建一份 hellok8s:v2 的版本镜像。唯一的区别就是字符串替换成了 [v2] Hello, Kubernetes!。 123456789101112131415package mainimport ( "io" "net/http")func hello(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "[v2] Hello, Kubernetes!")}func main() { http.HandleFunc("/", hello) http.ListenAndServe(":3000", nil)} 将 hellok8s:v2 推到 DockerHub 仓库中。 12docker build . -t guangzhengli/hellok8s:v2docker push guangzhengli/hellok8s:v2 接着编写 v2 版本的 deployment 资源文件。 1234567891011121314151617apiVersion: apps/v1kind: Deploymentmetadata: name: hellok8s-deploymentspec: replicas: 3 selector: matchLabels: app: hellok8s template: metadata: labels: app: hellok8s spec: containers: - image: guangzhengli/hellok8s:v2 name: hellok8s-container 12345678910111213141516kubectl apply -f deployment.yaml# deployment.apps/hellok8s-deployment configuredkubectl get pods# NAME READY STATUS RESTARTS AGE# hellok8s-deployment-66799848c4-kpc6q 1/1 Running 0 3s# hellok8s-deployment-66799848c4-pllj6 1/1 Running 0 3s# hellok8s-deployment-66799848c4-r7qtg 1/1 Running 0 3skubectl port-forward hellok8s-deployment-66799848c4-kpc6q 3000:3000# Forwarding from 127.0.0.1:3000 -> 3000# Forwarding from [::1]:3000 -> 3000# open another terminalcurl http://localhost:3000# [v2] Hello, Kubernetes! 你也可以输入 kubectl describe pod hellok8s-deployment-66799848c4-kpc6q 来看是否是 v2 版本的镜像。 Rolling Update(滚动更新)如果我们在生产环境上,管理着多个副本的 hellok8s:v1 版本的 pod,我们需要更新到 v2 的版本,像上面那样的部署方式是可以的,但是也会带来一个问题,就是所有的副本在同一时间更新,这会导致我们 hellok8s 服务在短时间内是不可用的,因为所有 pod 都在升级到 v2 版本的过程中,需要等待某个 pod 升级完成后才能提供服务。 这个时候我们就需要滚动更新 (rolling update),在保证新版本 v2 的 pod 还没有 ready 之前,先不删除 v1 版本的 pod。 在 deployment 的资源定义中, spec.strategy.type 有两种选择: RollingUpdate: 逐渐增加新版本的 pod,逐渐减少旧版本的 pod。 Recreate: 在新版本的 pod 增加前,先将所有旧版本 pod 删除。 大多数情况下我们会采用滚动更新 (RollingUpdate) 的方式,滚动更新又可以通过 maxSurge 和 maxUnavailable 字段来控制升级 pod 的速率,具体可以详细看官网定义。: maxSurge: 最大峰值,用来指定可以创建的超出期望 Pod 个数的 Pod 数量。 maxUnavailable: 最大不可用,用来指定更新过程中不可用的 Pod 的个数上限。 我们先输入命令回滚我们的 deployment,输入 kubectl describe pod 会发现 deployment 已经把 v2 版本的 pod 回滚到 v1 的版本。 12345678910kubectl rollout undo deployment hellok8s-deploymentkubectl get pods# NAME READY STATUS RESTARTS AGE# hellok8s-deployment-77bffb88c5-cvm5c 1/1 Running 0 39s# hellok8s-deployment-77bffb88c5-lktbl 1/1 Running 0 41s# hellok8s-deployment-77bffb88c5-nh82z 1/1 Running 0 37skubectl describe pod hellok8s-deployment-77bffb88c5-cvm5c# Image: guangzhengli/hellok8s:v1 除了上面的命令,还可以用 history 来查看历史版本,--to-revision=2 来回滚到指定版本。 12kubectl rollout history deployment hellok8s-deploymentkubectl rollout undo deployment/hellok8s-deployment --to-revision=2 接着设置 strategy=rollingUpdate , maxSurge=1 , maxUnavailable=1 和 replicas=3 到 deployment.yaml 文件中。这个参数配置意味着最大可能会创建 4 个 hellok8s pod (replicas + maxSurge),最小会有 2 个 hellok8s pod 存活 (replicas - maxUnavailable)。 123456789101112131415161718192021apiVersion: apps/v1kind: Deploymentmetadata: name: hellok8s-deploymentspec: strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 1 replicas: 3 selector: matchLabels: app: hellok8s template: metadata: labels: app: hellok8s spec: containers: - image: guangzhengli/hellok8s:v2 name: hellok8s-container 使用 kubectl apply -f deployment.yaml 来重新创建 v2 的资源,可以通过 kubectl get pods --watch 来观察 pod 的创建销毁情况,是否如下图所示。 存活探针 (livenessProb) 存活探测器来确定什么时候要重启容器。 例如,存活探测器可以探测到应用死锁(应用程序在运行,但是无法继续执行后面的步骤)情况。 重启这种状态下的容器有助于提高应用的可用性,即使其中存在缺陷。– LivenessProb 在生产中,有时候因为某些 bug 导致应用死锁或者线程耗尽了,最终会导致应用无法继续提供服务,这个时候如果没有手段来自动监控和处理这一问题的话,可能会导致很长一段时间无人发现。kubelet 使用存活探测器 (livenessProb) 来确定什么时候要重启容器。 接下来我们写一个 /healthz 接口来说明 livenessProb 如何使用。 /healthz 接口会在启动成功的 15s 内正常返回 200 状态码,在 15s 后,会一直返回 500 的状态码。 1234567891011121314151617181920212223242526272829package mainimport ( "fmt" "io" "net/http" "time")func hello(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "[v2] Hello, Kubernetes!")}func main() { started := time.Now() http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { duration := time.Since(started) if duration.Seconds() > 15 { w.WriteHeader(500) w.Write([]byte(fmt.Sprintf("error: %v", duration.Seconds()))) } else { w.WriteHeader(200) w.Write([]byte("ok")) } }) http.HandleFunc("/", hello) http.ListenAndServe(":3000", nil)} 12345678910111213141516# DockerfileFROM golang:1.16-buster AS builderRUN mkdir /srcADD . /srcWORKDIR /srcRUN go env -w GO111MODULE=autoRUN go build -o main .FROM gcr.io/distroless/base-debian10WORKDIR /COPY --from=builder /src/main /mainEXPOSE 3000ENTRYPOINT ["/main"] Dockerfile 的编写和原来保持一致,我们把 tag 修改为 liveness 并推送到远程仓库。 12docker build . -t guangzhengli/hellok8s:livenessdocker push guangzhengli/hellok8s:liveness 最后编写 deployment 的定义,这里使用存活探测方式是使用 HTTP GET 请求,请求的是刚才定义的 /healthz 接口,periodSeconds 字段指定了 kubelet 每隔 3 秒执行一次存活探测。 initialDelaySeconds 字段告诉 kubelet 在执行第一次探测前应该等待 3 秒。如果服务器上 /healthz 路径下的处理程序返回成功代码,则 kubelet 认为容器是健康存活的。 如果处理程序返回失败代码,则 kubelet 会杀死这个容器并将其重启。 123456789101112131415161718192021222324252627apiVersion: apps/v1kind: Deploymentmetadata: name: hellok8s-deploymentspec: strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 1 replicas: 3 selector: matchLabels: app: hellok8s template: metadata: labels: app: hellok8s spec: containers: - image: guangzhengli/hellok8s:liveness name: hellok8s-container livenessProbe: httpGet: path: /healthz port: 3000 initialDelaySeconds: 3 periodSeconds: 3 通过 get 或者 describe 命令可以发现 pod 一直处于重启当中。 1234567891011121314151617181920212223kubectl apply -f deployment.yamlkubectl get pods# NAME READY STATUS RESTARTS AGE# hellok8s-deployment-5995ff9447-d5fbz 1/1 Running 4 (6s ago) 102s# hellok8s-deployment-5995ff9447-gz2cx 1/1 Running 4 (5s ago) 101s# hellok8s-deployment-5995ff9447-rh29x 1/1 Running 4 (6s ago) 102skubectl describe pod hellok8s-68f47f657c-zwn6g# ...# ...# ...# Events:# Type Reason Age From Message# ---- ------ ---- ---- -------# Normal Scheduled 12m default-scheduler Successfully assigned default/hellok8s-deployment-5995ff9447-rh29x to minikube# Normal Pulled 11m (x4 over 12m) kubelet Container image "guangzhengli/hellok8s:liveness" already present on machine# Normal Created 11m (x4 over 12m) kubelet Created container hellok8s-container# Normal Started 11m (x4 over 12m) kubelet Started container hellok8s-container# Normal Killing 11m (x3 over 12m) kubelet Container hellok8s-container failed liveness probe, will be restarted# Warning Unhealthy 11m (x10 over 12m) kubelet Liveness probe failed: HTTP probe failed with statuscode: 500# Warning BackOff 2m41s (x36 over 10m) kubelet Back-off restarting failed container 就绪探针 (readiness) 就绪探测器可以知道容器何时准备好接受请求流量,当一个 Pod 内的所有容器都就绪时,才能认为该 Pod 就绪。 这种信号的一个用途就是控制哪个 Pod 作为 Service 的后端。 若 Pod 尚未就绪,会被从 Service 的负载均衡器中剔除。– ReadinessProb 在生产环境中,升级服务的版本是日常的需求,这时我们需要考虑一种场景,即当发布的版本存在问题,就不应该让它升级成功。kubelet 使用就绪探测器可以知道容器何时准备好接受请求流量,当一个 pod 升级后不能就绪,即不应该让流量进入该 pod,在配合 rollingUpate 的功能下,也不能允许升级版本继续下去,否则服务会出现全部升级完成,导致所有服务均不可用的情况。 这里我们把服务回滚到 hellok8s:v2 的版本,可以通过上面学习的方法进行回滚。 1kubectl rollout undo deployment hellok8s-deployment --to-revision=2 这里我们将应用的 /healthz 接口直接设置成返回 500 状态码,代表该版本是一个有问题的版本。 12345678910111213141516171819package mainimport ( "io" "net/http")func hello(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "[v2] Hello, Kubernetes!")}func main() { http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }) http.HandleFunc("/", hello) http.ListenAndServe(":3000", nil)} 在 build 阶段我们将 tag 设置为 bad,打包后 push 到远程仓库。 123docker build . -t guangzhengli/hellok8s:baddocker push guangzhengli/hellok8s:bad 接着编写 deployment 资源文件,Probe 有很多配置字段,可以使用这些字段精确地控制就绪检测的行为: initialDelaySeconds:容器启动后要等待多少秒后才启动存活和就绪探测器, 默认是 0 秒,最小值是 0。 periodSeconds:执行探测的时间间隔(单位是秒)。默认是 10 秒。最小值是 1。 timeoutSeconds:探测的超时后等待多少秒。默认值是 1 秒。最小值是 1。 successThreshold:探测器在失败后,被视为成功的最小连续成功数。默认值是 1。 存活和启动探测的这个值必须是 1。最小值是 1。 failureThreshold:当探测失败时,Kubernetes 的重试次数。 对存活探测而言,放弃就意味着重新启动容器。 对就绪探测而言,放弃意味着 Pod 会被打上未就绪的标签。默认值是 3。最小值是 1。 123456789101112131415161718192021222324252627apiVersion: apps/v1kind: Deploymentmetadata: name: hellok8s-deploymentspec: strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 1 replicas: 3 selector: matchLabels: app: hellok8s template: metadata: labels: app: hellok8s spec: containers: - image: guangzhengli/hellok8s:bad name: hellok8s-container readinessProbe: httpGet: path: /healthz port: 3000 initialDelaySeconds: 1 successThreshold: 5 通过 get 命令可以发现两个 pod 一直处于还没有 Ready 的状态当中,通过 describe 命令可以看到是因为 Readiness probe failed: HTTP probe failed with statuscode: 500 的原因。又因为设置了最小不可用的服务数量为maxUnavailable=1,这样能保证剩下两个 v2 版本的 hellok8s 能继续提供服务! 12345678910111213141516171819kubectl apply -f deployment.yamlkubectl get pods# NAME READY STATUS RESTARTS AGE# hellok8s-deployment-66799848c4-8xzsz 1/1 Running 0 102s# hellok8s-deployment-66799848c4-m9dl5 1/1 Running 0 102s# hellok8s-deployment-9c57c7f56-rww7k 0/1 Running 0 26s# hellok8s-deployment-9c57c7f56-xt9tw 0/1 Running 0 26skubectl describe pod hellok8s-deployment-9c57c7f56-rww7k# Events:# Type Reason Age From Message# ---- ------ ---- ---- -------# Normal Scheduled 74s default-scheduler Successfully assigned default/hellok8s-deployment-9c57c7f56-rww7k to minikube# Normal Pulled 73s kubelet Container image "guangzhengli/hellok8s:bad" already present on machine# Normal Created 73s kubelet Created container hellok8s-container# Normal Started 73s kubelet Started container hellok8s-container# Warning Unhealthy 0s (x10 over 72s) kubelet Readiness probe failed: HTTP probe failed with statuscode: 500 Service经过前面几节的练习,可能你会有一些疑惑: 为什么 pod 不就绪 (Ready) 的话,kubernetes 不会将流量重定向到该 pod,这是怎么做到的? 前面访问服务的方式是通过 port-forword 将 pod 的端口暴露到本地,不仅需要写对 pod 的名字,一旦 deployment 重新创建新的 pod,pod 名字和 IP 地址也会随之变化,如何保证稳定的访问地址呢?。 如果使用 deployment 部署了多个 Pod 副本,如何做负载均衡呢? kubernetes 提供了一种名叫 Service 的资源帮助解决这些问题,它为 pod 提供一个稳定的 Endpoint。Servie 位于 pod 的前面,负责接收请求并将它们传递给它后面的所有 pod。一旦服务中的 Pod 集合发生更改,Endpoints 就会被更新,请求的重定向自然也会导向最新的 pod。 ClusterIP我们先来看看 Service 默认使用的 ClusterIP 类型,首先做一些准备工作,在之前的 hellok8s:v2 版本上加上返回当前服务所在的 hostname 功能,升级到 v3 版本。 123456789101112131415161718package mainimport ( "fmt" "io" "net/http" "os")func hello(w http.ResponseWriter, r *http.Request) { host, _ := os.Hostname() io.WriteString(w, fmt.Sprintf("[v3] Hello, Kubernetes!, From host: %s", host))}func main() { http.HandleFunc("/", hello) http.ListenAndServe(":3000", nil)} Dockerfile 和之前保持一致,打包 tag=v3 并推送到远程仓库。 123docker build . -t guangzhengli/hellok8s:v3docker push guangzhengli/hellok8s:v3 修改 deployment 的 hellok8s 为 v3 版本。执行 kubectl apply -f deployment.yaml 更新 deployment。 1234567891011121314151617apiVersion: apps/v1kind: Deploymentmetadata: name: hellok8s-deploymentspec: replicas: 3 selector: matchLabels: app: hellok8s template: metadata: labels: app: hellok8s spec: containers: - image: guangzhengli/hellok8s:v3 name: hellok8s-container 接下来是 Service 资源的定义,我们使用 ClusterIP 的方式定义 Service,通过 kubernetes 集群的内部 IP 暴露服务,当我们只需要让集群中运行的其他应用程序访问我们的 pod 时,就可以使用这种类型的 Service。首先创建一个 service-hellok8s-clusterip.yaml` 文件。 1234567891011apiVersion: v1kind: Servicemetadata: name: service-hellok8s-clusteripspec: type: ClusterIP selector: app: hellok8s ports: - port: 3000 targetPort: 3000 首先通过 kubectl get endpoints 来看看 Endpoint。被 selector 选中的 Pod,就称为 Service 的 Endpoints。它维护着 Pod 的 IP 地址,只要服务中的 Pod 集合发生更改,Endpoints 就会被更新。通过 kubectl get pod -o wide 命令获取 Pod 更多的信息,可以看到 3 个 Pod 的 IP 地址和 Endpoints 中是保持一致的,你可以试试增大或减少 Deployment 中 Pod 的 replicas,观察 Endpoints 会不会发生变化。 12345678910111213141516kubectl apply -f service-hellok8s-clusterip.yamlkubectl get endpoints# NAME ENDPOINTS AGE# service-hellok8s-clusterip 172.17.0.10:3000,172.17.0.2:3000,172.17.0.3:3000 10skubectl get pod -o wide# NAME READY STATUS RESTARTS AGE IP NODE# hellok8s-deployment-5d5545b69c-24lw5 1/1 Running 0 112s 172.17.0.7 minikube# hellok8s-deployment-5d5545b69c-9g94t 1/1 Running 0 112s 172.17.0.3 minikube# hellok8s-deployment-5d5545b69c-9gm8r 1/1 Running 0 112s 172.17.0.2 minikube# nginx 1/1 Running 0 112s 172.17.0.9 minikubekubectl get service# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE# service-hellok8s-clusterip ClusterIP 10.104.96.153 <none> 3000/TCP 10s 接着我们可以通过在集群其它应用中访问 service-hellok8s-clusterip 的 IP 地址 10.104.96.153 来访问 hellok8s:v3 服务。 这里通过在集群内创建一个 nginx 来访问 hellok8s 服务。创建后进入 nginx 容器来用 curl 命令访问 service-hellok8s-clusterip 。 12345678910apiVersion: v1kind: Podmetadata: name: nginx labels: app: nginxspec: containers: - name: nginx-container image: nginx 12345678910111213141516kubectl get pods# NAME READY STATUS RESTARTS AGE# hellok8s-deployment-5d5545b69c-24lw5 1/1 Running 0 27m# hellok8s-deployment-5d5545b69c-9g94t 1/1 Running 0 27m# hellok8s-deployment-5d5545b69c-9gm8r 1/1 Running 0 27m# nginx 1/1 Running 0 41mkubectl get service# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE# service-hellok8s-clusterip ClusterIP 10.104.96.153 <none> 3000/TCP 10skubectl exec -it nginx-pod /bin/bash# root@nginx-pod:/# curl 10.104.96.153:3000# [v3] Hello, Kubernetes!, From host: hellok8s-deployment-5d5545b69c-9gm8r# root@nginx-pod:/# curl 10.104.96.153:3000#[v3] Hello, Kubernetes!, From host: hellok8s-deployment-5d5545b69c-9g94t 可以看到,我们多次 curl 10.104.96.153:3000 访问 hellok8s Service IP 地址,返回的 hellok8s:v3 hostname 不一样,说明 Service 可以接收请求并将它们传递给它后面的所有 pod,还可以自动负载均衡。你也可以试试增加或者减少 hellok8s:v3 pod 副本数量,观察 Service 的请求是否会动态变更。调用过程如下图所示: 除了上述的 ClusterIp 的方式外,Kubernetes ServiceTypes 允许指定你所需要的 Service 类型,默认是 ClusterIP。Type 的值包括如下: ClusterIP:通过集群的内部 IP 暴露服务,选择该值时服务只能够在集群内部访问。 这也是默认的 ServiceType。 [NodePort](https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport):通过每个节点上的 IP 和静态端口(NodePort)暴露服务。 NodePort 服务会路由到自动创建的 ClusterIP 服务。 通过请求 <节点 IP>:<节点端口>,你可以从集群的外部访问一个 NodePort 服务。 [LoadBalancer](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer):使用云提供商的负载均衡器向外部暴露服务。 外部负载均衡器可以将流量路由到自动创建的 NodePort 服务和 ClusterIP 服务上。 [ExternalName](https://kubernetes.io/docs/concepts/services-networking/service/#externalname):通过返回 CNAME 和对应值,可以将服务映射到 externalName 字段的内容(例如,foo.bar.example.com)。 无需创建任何类型代理。 NodePort我们知道kubernetes 集群并不是单机运行,它管理着多台节点即 Node,可以通过每个节点上的 IP 和静态端口(NodePort)暴露服务。如下图所示,如果集群内有两台 Node 运行着 hellok8s:v3,我们创建一个 NodePort 类型的 Service,将 hellok8s:v3 的 3000 端口映射到 Node 机器的 30000 端口 (在 30000-32767 范围内),就可以通过访问 http://node1-ip:30000 或者 http://node2-ip:30000 访问到服务。 这里以 minikube 为例,我们可以通过 minikube ip 命令拿到 k8s cluster node IP 地址。下面的教程都以我本机的 192.168.59.100 为例,需要替换成你的 IP 地址。 12minikube ip# 192.168.59.100 接着以 NodePort 的 ServiceType 创建一个 Service 来接管 pod 流量。通过minikube 节点上的 IP 192.168.59.100 暴露服务。 NodePort 服务会路由到自动创建的 ClusterIP 服务。 通过请求 <节点 IP>:<节点端口> – 192.168.59.100:30000,你可以从集群的外部访问一个 NodePort 服务,最终重定向到 hellok8s:v3 的 3000 端口。 1234567891011apiVersion: v1kind: Servicemetadata: name: service-hellok8s-nodeportspec: type: NodePort selector: app: hellok8s ports: - port: 3000 nodePort: 30000 创建 service-hellok8s-nodeport Service 后,使用 curl 命令或者浏览器访问 http://192.168.59.100:30000 可以得到结果。 1234567891011121314151617kubectl apply -f service-hellok8s-nodeport.yamlkubectl get service# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE# service-hellok8s-nodeport NodePort 10.109.188.161 <none> 3000:30000/TCP 28skubectl get pods# NAME READY STATUS RESTARTS AGE# hellok8s-deployment-5d5545b69c-24lw5 1/1 Running 0 27m# hellok8s-deployment-5d5545b69c-9g94t 1/1 Running 0 27m# hellok8s-deployment-5d5545b69c-9gm8r 1/1 Running 0 27mcurl http://192.168.59.100:30000# [v3] Hello, Kubernetes!, From host: hellok8s-deployment-5d5545b69c-9g94tcurl http://192.168.59.100:30000# [v3] Hello, Kubernetes!, From host: hellok8s-deployment-5d5545b69c-24lw5 LoadBalancer[LoadBalancer](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer) 是使用云提供商的负载均衡器向外部暴露服务。 外部负载均衡器可以将流量路由到自动创建的 NodePort 服务和 ClusterIP 服务上,假如你在 AWS 的 EKS 集群上创建一个 Type 为 LoadBalancer 的 Service。它会自动创建一个 ELB (Elastic Load Balancer) ,并可以根据配置的 IP 池中自动分配一个独立的 IP 地址,可以供外部访问。 这里因为我们使用的是 minikube,可以使用 minikube tunnel 来辅助创建 LoadBalancer 的 EXTERNAL_IP,具体教程可以查看官网文档,但是和实际云提供商的 LoadBalancer 还是有本质区别,所以 Repository 不做更多阐述,有条件的可以使用 AWS 的 EKS 集群上创建一个 ELB (Elastic Load Balancer) 试试。 下图显示 LoadBalancer 的 Service 架构图。 ingressIngress 公开从集群外部到集群内服务的 HTTP 和 HTTPS 路由。 流量路由由 Ingress 资源上定义的规则控制。Ingress 可为 Service 提供外部可访问的 URL、负载均衡流量、 SSL/TLS,以及基于名称的虚拟托管。你必须拥有一个 Ingress 控制器 才能满足 Ingress 的要求。 仅创建 Ingress 资源本身没有任何效果。 Ingress 控制器 通常负责通过负载均衡器来实现 Ingress,例如 minikube 默认使用的是 nginx-ingress,目前 minikube 也支持 Kong-Ingress。 Ingress 可以“简单理解”为服务的网关 Gateway,它是所有流量的入口,经过配置的路由规则,将流量重定向到后端的服务。 在 minikube 中,可以通过下面命令开启 Ingress-Controller 的功能。默认使用的是 nginx-ingress。 1minikube addons enable ingress 接着删除之前创建的所有 pod, deployment, service 资源。 1kubectl delete deployment,service --all 接着根据之前的教程,创建 hellok8s:v3 和 nginx 的deployment与 service 资源。Service 的 type 为 ClusterIP 即可。 hellok8s:v3 的端口映射为 3000:3000,nginx 的端口映射为 4000:80,这里后续写 Ingress Route 规则时会用到。 123456789101112131415161718192021222324252627282930apiVersion: v1kind: Servicemetadata: name: service-hellok8s-clusteripspec: type: ClusterIP selector: app: hellok8s ports: - port: 3000 targetPort: 3000---apiVersion: apps/v1kind: Deploymentmetadata: name: hellok8s-deploymentspec: replicas: 3 selector: matchLabels: app: hellok8s template: metadata: labels: app: hellok8s spec: containers: - image: guangzhengli/hellok8s:v3 name: hellok8s-container 123456789101112131415161718192021222324252627282930apiVersion: v1kind: Servicemetadata: name: service-nginx-clusteripspec: type: ClusterIP selector: app: nginx ports: - port: 4000 targetPort: 80---apiVersion: apps/v1kind: Deploymentmetadata: name: nginx-deploymentspec: replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - image: nginx name: nginx-container 1234567891011121314151617181920kubectl apply -f hellok8s.yaml# service/service-hellok8s-clusterip created# deployment.apps/hellok8s-deployment createdkubectl apply -f nginx.yaml# service/service-nginx-clusterip created# deployment.apps/nginx-deployment createdkubectl get pods# NAME READY STATUS RESTARTS AGE# hellok8s-deployment-5d5545b69c-4wvmf 1/1 Running 0 55s# hellok8s-deployment-5d5545b69c-qcszp 1/1 Running 0 55s# hellok8s-deployment-5d5545b69c-sn7mn 1/1 Running 0 55s# nginx-deployment-d47fd7f66-d9r7x 1/1 Running 0 34s# nginx-deployment-d47fd7f66-hp5nf 1/1 Running 0 34skubectl get service# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE# service-hellok8s-clusterip ClusterIP 10.97.88.18 <none> 3000/TCP 77s# service-nginx-clusterip ClusterIP 10.103.161.247 <none> 4000/TCP 56s 这样在 k8s 集群中,就有 3 个 hellok8s:v3 的 pod,2 个 nginx 的 pod。并且hellok8s:v3 的端口映射为 3000:3000,nginx 的端口映射为 4000:80。在这个基础上,接下来编写 Ingress 资源的定义,nginx.ingress.kubernetes.io/ssl-redirect: "false" 的意思是这里关闭 https 连接,只使用 http 连接。 匹配前缀为 /hello 的路由规则,重定向到 hellok8s:v3 服务,匹配前缀为 / 的跟路径重定向到 nginx。 1234567891011121314151617181920212223242526apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: hello-ingress annotations: # We are defining this annotation to prevent nginx # from redirecting requests to `https` for now nginx.ingress.kubernetes.io/ssl-redirect: "false"spec: rules: - http: paths: - path: /hello pathType: Prefix backend: service: name: service-hellok8s-clusterip port: number: 3000 - path: / pathType: Prefix backend: service: name: service-nginx-clusterip port: number: 4000 12345678910111213kubectl apply -f ingress.yaml# ingress.extensions/hello-ingress createdkubectl get ingress# NAME CLASS HOSTS ADDRESS PORTS AGE# hello-ingress nginx * 80 16s# replace 192.168.59.100 by your minikube ipcurl http://192.168.59.100/hello# [v3] Hello, Kubernetes!, From host: hellok8s-deployment-5d5545b69c-sn7mncurl http://192.168.59.100/# (....Thank you for using nginx.....) 上面的教程中将所有流量都发送到 Ingress 中,如下图所示: Namespace在实际的开发当中,有时候我们需要不同的环境来做开发和测试,例如 dev 环境给开发使用,test 环境给 QA 使用,那么 k8s 能不能在不同环境 dev test uat prod 中区分资源,让不同环境的资源独立互相不影响呢,答案是肯定的,k8s 提供了名为 Namespace 的资源来帮助隔离资源。 在 Kubernetes 中,名字空间(Namespace) 提供一种机制,将同一集群中的资源划分为相互隔离的组。 同一名字空间内的资源名称要唯一,但跨名字空间时没有这个要求。 名字空间作用域仅针对带有名字空间的对象,例如 Deployment、Service 等。 前面的教程中,默认使用的 namespace 是 default。 下面展示如何创建一个新的 namespace, namespace.yaml 文件定义了两个不同的 namespace,分别是 dev 和 test。 12345678910apiVersion: v1kind: Namespacemetadata: name: dev---apiVersion: v1kind: Namespacemetadata: name: test 可以通过kubectl apply -f namespaces.yaml 创建两个新的 namespace,分别是 dev 和 test。 1234567891011121314kubectl apply -f namespaces.yaml# namespace/dev created# namespace/test createdkubectl get namespaces# NAME STATUS AGE# default Active 215d# dev Active 2m44s# ingress-nginx Active 110d# kube-node-lease Active 215d# kube-public Active 215d# kube-system Active 215d# test Active 2m44s 那么如何在新的 namespace 下创建资源和获取资源呢?只需要在命令后面加上 -n namespace 即可。例如根据上面教程中,在名为 dev 的 namespace 下创建 hellok8s:v3 的 deployment 资源。 123kubectl apply -f deployment.yaml -n devkubectl get pods -n dev Configmap上面的教程提到,我们在不同环境 dev test uat prod 中区分资源,可以让其资源独立互相不受影响,但是随之而来也会带来一些问题,例如不同环境的数据库的地址往往是不一样的,那么如果在代码中写同一个数据库的地址,就会出现问题。 K8S 使用 ConfigMap 来将你的配置数据和应用程序代码分开,将非机密性的数据保存到键值对中。ConfigMap 在设计上不是用来保存大量数据的。在 ConfigMap 中保存的数据不可超过 1 MiB。如果你需要保存超出此尺寸限制的数据,你可能考虑挂载存储卷。 下面我们可以来看一个例子,我们修改之前代码,假设不同环境的数据库地址不同,下面代码从环境变量中获取 DB_URL,并将它返回。 12345678910111213141516171819package mainimport ( "fmt" "io" "net/http" "os")func hello(w http.ResponseWriter, r *http.Request) { host, _ := os.Hostname() dbURL := os.Getenv("DB_URL") io.WriteString(w, fmt.Sprintf("[v4] Hello, Kubernetes! From host: %s, Get Database Connect URL: %s", host, dbURL))}func main() { http.HandleFunc("/", hello) http.ListenAndServe(":3000", nil)} 构建 hellok8s:v4 的镜像,推送到远程仓库。并删除之前创建的所有资源。 1234docker build . -t guangzhengli/hellok8s:v4docker push guangzhengli/hellok8s:v4kubectl delete deployment,service,ingress --all 接下来创建不同 namespace 的 configmap 来存放 DB_URL。 创建 hellok8s-config-dev.yaml 文件 123456apiVersion: v1kind: ConfigMapmetadata: name: hellok8s-configdata: DB_URL: "http://DB_ADDRESS_DEV" 创建 hellok8s-config-test.yaml 文件 123456apiVersion: v1kind: ConfigMapmetadata: name: hellok8s-configdata: DB_URL: "http://DB_ADDRESS_TEST" 分别在 dev test 两个 namespace 下创建相同的 ConfigMap,名字都叫 hellok8s-config,但是存放的 Pair 对中 Value 值不一样。 12345678910kubectl apply -f hellok8s-config-dev.yaml -n dev# configmap/hellok8s-config createdkubectl apply -f hellok8s-config-test.yaml -n test# configmap/hellok8s-config createdkubectl get configmap --all-namespacesNAMESPACE NAME DATA AGEdev hellok8s-config 1 3m12stest hellok8s-config 1 2m1s 接着使用 POD 的方式来部署 hellok8s:v4,其中 env.name 表示的是将 configmap 中的值写进环境变量,这样代码从环境变量中获取 DB_URL,这个 KEY 名称必须保持一致。valueFrom 代表从哪里读取,configMapKeyRef 这里表示从名为 hellok8s-config 的 configMap 中读取 KEY=DB_URL 的 Value。 1234567891011121314apiVersion: v1kind: Podmetadata: name: hellok8s-podspec: containers: - name: hellok8s-container image: guangzhengli/hellok8s:v4 env: - name: DB_URL valueFrom: configMapKeyRef: name: hellok8s-config key: DB_URL 下面分别在 dev test 两个 namespace 下创建 hellok8s:v4,接着通过 port-forward 的方式访问不同 namespace 的服务,可以看到返回的 Get Database Connect URL: http://DB_ADDRESS_TEST 是不一样的! 123456789101112131415kubectl apply -f hellok8s.yaml -n dev# pod/hellok8s-pod createdkubectl apply -f hellok8s.yaml -n test# pod/hellok8s-pod createdkubectl port-forward hellok8s-pod 3000:3000 -n devcurl http://localhost:3000# [v4] Hello, Kubernetes! From host: hellok8s-pod, Get Database Connect URL: http://DB_ADDRESS_DEVkubectl port-forward hellok8s-pod 3000:3000 -n testcurl http://localhost:3000# [v4] Hello, Kubernetes! From host: hellok8s-pod, Get Database Connect URL: http://DB_ADDRESS_TEST Secret上面提到,我们会选择以 configmap 的方式挂载配置信息,但是当我们的配置信息需要加密的时候, configmap 就无法满足这个要求。例如上面要挂载数据库密码的时候,就需要明文挂载。 这个时候就需要 Secret 来存储加密信息,虽然在资源文件的编码上,只是通过 Base64 的方式简单编码,但是在实际生产过程中,可以通过 pipeline 或者专业的 AWS KMS 服务进行密钥管理。这样就大大减少了安全事故。 Secret 是一种包含少量敏感信息例如密码、令牌或密钥的对象。由于创建 Secret 可以独立于使用它们的 Pod, 因此在创建、查看和编辑 Pod 的工作流程中暴露 Secret(及其数据)的风险较小。 Kubernetes 和在集群中运行的应用程序也可以对 Secret 采取额外的预防措施, 例如避免将机密数据写入非易失性存储。 默认情况下,Kubernetes Secret 未加密地存储在 API 服务器的底层数据存储(etcd)中。 任何拥有 API 访问权限的人都可以检索或修改 Secret,任何有权访问 etcd 的人也可以。 此外,任何有权限在命名空间中创建 Pod 的人都可以使用该访问权限读取该命名空间中的任何 Secret; 这包括间接访问,例如创建 Deployment 的能力。 为了安全地使用 Secret,请至少执行以下步骤: 为 Secret 启用静态加密; 启用或配置 RBAC 规则来限制读取和写入 Secret 的数据(包括通过间接方式)。需要注意的是,被准许创建 Pod 的人也隐式地被授权获取 Secret 内容。 在适当的情况下,还可以使用 RBAC 等机制来限制允许哪些主体创建新 Secret 或替换现有 Secret。 Secret 的资源定义和 ConfigMap 结构基本一致,唯一区别在于 kind 是 Secret,还有 Value 需要 Base64 编码,你可以通过下面命令快速 Base64 编解码。当然 Secret 也提供了一种 stringData,可以不需要 Base64 编码。 12345echo "db_password" | base64# ZGJfcGFzc3dvcmQKecho "ZGJfcGFzc3dvcmQK" | base64 -d# db_password 这里将 Base64 编码过后的值,填入对应的 key - value 中。 1234567# hellok8s-secret.yamlapiVersion: v1kind: Secretmetadata: name: hellok8s-secretdata: DB_PASSWORD: "ZGJfcGFzc3dvcmQK" 123456789101112131415# hellok8s.yamlapiVersion: v1kind: Podmetadata: name: hellok8s-podspec: containers: - name: hellok8s-container image: guangzhengli/hellok8s:v5 env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: hellok8s-secret key: DB_PASSWORD 12345678910111213141516171819package mainimport ( "fmt" "io" "net/http" "os")func hello(w http.ResponseWriter, r *http.Request) { host, _ := os.Hostname() dbPassword := os.Getenv("DB_PASSWORD") io.WriteString(w, fmt.Sprintf("[v5] Hello, Kubernetes! From host: %s, Get Database Connect Password: %s", host, dbPassword))}func main() { http.HandleFunc("/", hello) http.ListenAndServe(":3000", nil)} 在代码中读取 DB_PASSWORD 环境变量,直接返回对应字符串。Secret 的使用方法和前面教程中 ConfigMap 基本一致,这里就不再过多赘述。 123456789docker build . -t guangzhengli/hellok8s:v5docker push guangzhengli/hellok8s:v5kubectl apply -f hellok8s-secret.yamlkubectl apply -f hellok8s.yamlkubectl port-forward hellok8s-pod 3000:3000 Job在实际的开发过程中,还有一类任务是之前的资源不能满足的,即一次性任务。例如常见的计算任务,只需要拿到相关数据计算后得出结果即可,无需一直运行。而处理这一类任务的资源就是 Job。 Job 会创建一个或者多个 Pod,并将继续重试 Pod 的执行,直到指定数量的 Pod 成功终止。 随着 Pod 成功结束,Job 跟踪记录成功完成的 Pod 个数。 当数量达到指定的成功个数阈值时,任务(即 Job)结束。 删除 Job 的操作会清除所创建的全部 Pod。 挂起 Job 的操作会删除 Job 的所有活跃 Pod,直到 Job 被再次恢复执行。 一种简单的使用场景下,你会创建一个 Job 对象以便以一种可靠的方式运行某 Pod 直到完成。 当第一个 Pod 失败或者被删除(比如因为节点硬件失效或者重启)时,Job 对象会启动一个新的 Pod。 下面来看一个 Job 的资源定义,其中 Kind 和 metadata.name 是资源类型和名字就不再解释,completions 指的是会创建 Pod 的数量,每个 pod 都会完成下面的任务。parallelism 指的是并发执行最大数量,例如下面就会先创建 3 个 pod 并发执行任务,一旦某个 pod 执行完成,就会再创建新的 pod 来执行,直到 5 个 pod 执行完成,Job 才会被标记为完成。 restartPolicy = "OnFailure 的含义和 Pod 生命周期相关,Pod 中的容器可能因为退出时返回值非零, 或者容器因为超出内存约束而被杀死等等。 如果发生这类事件,并且 .spec.template.spec.restartPolicy = "OnFailure", Pod 则继续留在当前节点,但容器会被重新运行。因此,你的程序需要能够处理在本地被重启的情况,或者要设置 .spec.template.spec.restartPolicy = "Never"。 1234567891011121314apiVersion: batch/v1kind: Jobmetadata: name: hello-jobspec: parallelism: 3 completions: 5 template: spec: restartPolicy: OnFailure containers: - name: echo image: busybox command: [for i in 9 8 7 6 5 4 3 2 1 ; do echo $i ; done] 通过下面的命令创建 job,可以通过 kubectl get pods -w 来观察 job 创建 pod 的过程和结果。最后可以通过 logs 命令查看日志。 1234567891011121314151617kubectl apply -f hello-job.yamlkubectl get jobs# NAME COMPLETIONS DURATION AGE# hello-job 5/5 19s 83skubectl get pods# NAME READY STATUS RESTARTS AGE# hello-job--1-5gjjr 0/1 Completed 0 34s# hello-job--1-8ffmn 0/1 Completed 0 26s# hello-job--1-ltsvm 0/1 Completed 0 34s# hello-job--1-mttwv 0/1 Completed 0 29s# hello-job--1-ww2qp 0/1 Completed 0 34skubectl logs -f hello-job--1-5gjjr# 1# ... Job 完成时不会再创建新的 Pod,不过已有的 Pod 通常也不会被删除。 保留这些 Pod 使得你可以查看已完成的 Pod 的日志输出,以便检查错误、警告或者其它诊断性输出。 可以使用 kubectl 来删除 Job(例如 kubectl delete -f hello-job.yaml)。当使用 kubectl 来删除 Job 时,该 Job 所创建的 Pod 也会被删除。 CronJobCronJob 可以理解为定时任务,创建基于 Cron 时间调度的 Jobs。 CronJob 用于执行周期性的动作,例如备份、报告生成等。 这些任务中的每一个都应该配置为周期性重复的(例如:每天/每周/每月一次); 你可以定义任务开始执行的时间间隔。 Cron 时间表语法 123456789# ┌───────────── 分钟 (0 - 59)# │ ┌───────────── 小时 (0 - 23)# │ │ ┌───────────── 月的某天 (1 - 31)# │ │ │ ┌───────────── 月份 (1 - 12)# │ │ │ │ ┌───────────── 周的某天 (0 - 6)(周日到周一;在某些系统上,7 也是星期日)# │ │ │ │ │ 或者是 sun,mon,tue,web,thu,fri,sat# │ │ │ │ │# │ │ │ │ │# * * * * * 用法除了需要加上 cron 表达式之外,其余基本和 Job 保持一致。 123456789101112131415apiVersion: batch/v1kind: CronJobmetadata: name: hello-cronjobspec: schedule: "* * * * *" # Every minute jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: echo image: busybox command: [for i in 9 8 7 6 5 4 3 2 1 ; do echo $i ; done] 使用命令和 Job 也基本保持一致,这里就不过多赘述。 12345678910kubectl apply -f hello-cronjob.yaml# cronjob.batch/hello-cronjob createdkubectl get cronjob# NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE# hello-cronjob * * * * * False 0 <none> 8skubectl get pods# NAME READY STATUS RESTARTS AGE# hello-cronjob-27694609--1-2nmdx 0/1 Completed 0 15s Helm经过前面的教程,想必你已经对 kubernetes 的使用有了一定的理解。但是不知道你是否想过这样一个问题,就是我们前面教程中提到的所有资源,包括用 pod, deployment, service, ingress, configmap,secret 所有资源来部署一套完整的 hellok8s 服务的话,难道需要一个一个的 kubectl apply -f 来创建吗?如果换一个 namespace,或者说换一套 kubernetes 集群部署的话,又要重复性的操作创建的过程吗? 我们平常使用操作系统时,需要安装一个应用的话,可以直接使用 apt 或者 brew 来直接安装,而不需要关心这个应用需要哪些依赖,哪些配置。在使用 kubernetes 安装应用服务 hellok8s 时,我们自然也希望能够一个命令就安装完成,而提供这个能力的,就是 CNCF 的毕业项目 Helm。 Helm 帮助您管理 Kubernetes 应用—— Helm Chart,Helm 是查找、分享和使用软件构建 Kubernetes 的最优方式。 复杂性管理 ——即使是最复杂的应用,Helm Chart 依然可以描述, 提供使用单点授权的可重复安装应用程序。 易于升级 ——随时随地升级和自定义的钩子消除您升级的痛苦。 分发简单 —— Helm Chart 很容易在公共或私有化服务器上发版,分发和部署站点。 回滚 —— 使用 helm rollback 可以轻松回滚到之前的发布版本。 我们通过 brew 来安装 helm。更多方式可以参考官方文档。 1brew install helm Helm 的使用方式可以解释为:Helm 安装 charts 到 Kubernetes 集群中,每次安装都会创建一个新的 _release_。你可以在 Helm 的 chart repositories 中寻找新的 chart。 用 helm 安装 hellok8s开始本节教程前,我们先把之前手动创建的 hellok8s 相关的资源删除(防止使用 helm 创建同名的 k8s 资源失败)。 在尝试自己创建 hellok8s helm chart 之前,我们可以先来熟悉一下怎么使用 helm chart。在这里我先创建好了一个 hellok8s(包括会创建 deployment, service, ingress, configmaps, secret)的 helm chart。通过 GitHub actions 生成放在了 gh-pages 分支下的 index.yaml 文件中。 接着可以使用下面命令进行快速安装,其中 helm repo add 表示将我创建好的 hellok8s chart 添加到自己本地的仓库当中,helm install 表示从仓库中安装 hellok8s/hello-helm 到 k8s 集群当中。 12345678helm repo add hellok8s https://guangzhengli.github.io/k8s-tutorials/# "hellok8s" has been added to your repositorieshelm install my-hello-helm hellok8s/hello-helm --version 0.1.0# NAME: my-hello-helm# NAMESPACE: default# STATUS: deployed# REVISION: 1 创建完成后,通过 kubectl get 等命令可以看到所有 hellok8s 资源都创建成功,helm 一条命令即可做到之前教程中所有资源的创建!通过 curl k8s 集群的 ingress 地址,也可以看到返回字符串! 12345678910111213141516171819202122232425262728293031323334kubectl get pods# NAME READY STATUS RESTARTS AGE# hellok8s-deployment-f88f984c6-k8hpz 1/1 Running 0 15h# hellok8s-deployment-f88f984c6-nzwg6 1/1 Running 0 15h# hellok8s-deployment-f88f984c6-s89s7 1/1 Running 0 15h# nginx-deployment-d47fd7f66-6w76b 1/1 Running 0 15h# nginx-deployment-d47fd7f66-tsqj5 1/1 Running 0 15hkubectl get deployments# NAME READY UP-TO-DATE AVAILABLE AGE# hellok8s-deployment 3/3 3 3 15h# nginx-deployment 2/2 2 2 15hkubectl get service# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE# kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 13d# service-hellok8s-clusterip ClusterIP 10.107.198.175 <none> 3000/TCP 15h# service-nginx-clusterip ClusterIP 10.100.144.49 <none> 4000/TCP 15hkubectl get ingress# NAME CLASS HOSTS ADDRESS PORTS AGE# hellok8s-ingress nginx * localhost 80 15hkubectl get configmap# NAME DATA AGE# hellok8s-config 1 15hkubectl get secret# NAME TYPE DATA AGE# hellok8s-secret Opaque 1 15h# sh.helm.release.v1.my-hello-helm.v1 helm.sh/release.v1curl http://192.168.59.100/hello# [v6] Hello, Helm! Message from helm values: It works with Helm Values[v2]!, From namespace: default, From host: hellok8s-deployment-598bbd6884-ttk78, Get Database Connect URL: http://DB_ADDRESS_DEFAULT, Database Connect Password: db_password 创建 helm charts在使用已经创建好的 hello-helm charts 来创建整个 hellok8s 资源后,你可能还是有很多的疑惑,包括 Chart 是如何生成和发布的,如何创建一个新的 Chart?在这节教程中,我们会尝试自己来创建 hello-helm Chart 来完成之前的操作。 首先建议使用 helm create 命令来创建一个初始的 Chart,该命令默认会创建一些 k8s 资源定义的初始文件,并且会生成官网推荐的目录结构,如下所示: 12345678910111213141516helm create hello-helm.├── Chart.yaml├── charts├── templates│ ├── NOTES.txt│ ├── _helpers.tpl│ ├── deployment.yaml│ ├── hpa.yaml│ ├── ingress.yaml│ ├── service.yaml│ ├── serviceaccount.yaml│ └── tests│ └── test-connection.yaml└── values.yaml 我们将默认生成在 templates 目录下面的 yaml 文件删除,用之前教程中 yaml 文件替换它,最终的结构长这样: 1234567891011121314151617.├── Chart.yaml├── Dockerfile├── _helpers.tpl├── charts├── hello-helm-0.1.0.tgz├── index.yaml├── main.go├── templates│ ├── hellok8s-configmaps.yaml│ ├── hellok8s-deployment.yaml│ ├── hellok8s-secret.yaml│ ├── hellok8s-service.yaml│ ├── ingress.yaml│ ├── nginx-deployment.yaml│ └── nginx-service.yaml└── values.yaml 其中 main.go 定义的是 hellok8s:v6 版本的代码,主要是从系统中拿到 message,namespace,dbURL,dbPassword 这几个环境变量,拼接成字符串返回。 1234567891011121314151617181920212223package mainimport ( "fmt" "io" "net/http" "os")func hello(w http.ResponseWriter, r *http.Request) { host, _ := os.Hostname() message := os.Getenv("MESSAGE") namespace := os.Getenv("NAMESPACE") dbURL := os.Getenv("DB_URL") dbPassword := os.Getenv("DB_PASSWORD") io.WriteString(w, fmt.Sprintf("[v6] Hello, Helm! Message from helm values: %s, From namespace: %s, From host: %s, Get Database Connect URL: %s, Database Connect Password: %s", message, namespace, host, dbURL, dbPassword))}func main() { http.HandleFunc("/", hello) http.ListenAndServe(":3000", nil)} 为了让大家更加了解 helm charts values 的使用和熟悉 k8s 资源配置,这几个环境变量 MESSAGE, NAMESPACE, DB_URL, DB_PASSWORD 分别有不同的来源。 首先修改根目录下的 values.yaml 文件,定义自定义的配置信息,从之前教程的 k8s 资源文件中,将一些易于变化的参数提取出来,放在 values.yaml 文件中。全部配置信息如下所示: 123456789101112application: name: hellok8s hellok8s: image: guangzhengli/hellok8s:v6 replicas: 3 message: "It works with Helm Values!" database: url: "http://DB_ADDRESS_DEFAULT" password: "db_password" nginx: image: nginx replicas: 2 那自定义好了这些配置后,如何在 k8s 资源定义中使用这些配置信息呢?Helm 默认使用 Go template 的方式 来完成。 例如之前教程中,将环境变量 DB_URL 定义在 k8s configmaps 中,那么该资源可以定义成如文件所示 hellok8s-configmaps.yaml。其中 metadata.name 的值是 {{ .Values.application.name }}-config,意思是从 values.yaml 文件中获取 application.name 的值 hellok8s,拼接 -config 字符串,这样创建出来的 configmaps 资源名称就是 hellok8s-config。 同理 {{ .Values.application.hellok8s.database.url }} 就是获取值为 http://DB_ADDRESS_DEFAULT 放入 configmaps 对应 key 为 DB_URL 的 value 中。 123456apiVersion: v1kind: ConfigMapmetadata: name: {{ .Values.application.name }}-configdata: DB_URL: {{ .Values.application.hellok8s.database.url }} 上面定义的最终效果和之前在 configmaps 教程中定义的资源没有区别,这种做的好处是可以将所有可变的参数定义在 values.yaml 文件中,使用该 helm charts 的人无需了解具体 k8s 的定义,只需改变成自己想要的参数,即可创建自定义的资源! 同样,我们可以根据之前的教程将 DB_PASSWORD 放入 secret 中,并且通过 b64enc 方法将值 Base64 编码。 1234567# hellok8s-secret.yamlapiVersion: v1kind: Secretmetadata: name: {{ .Values.application.name }}-secretdata: DB_PASSWORD: {{ .Values.application.hellok8s.database.password | b64enc }} 最后,修改 hellok8s-deployment 文件,根据前面的教程,将 metadata.name replicas image configMapKeyRef.name secretKeyRef.name 等值修改成从 values.yaml 文件中获取。 再添加代码中需要的 NAMESPACE 环境变量,从 .Release.Namespace 内置对象 中获取。最后添加 MESSAGE 环境变量,直接从 {{ .Values.application.hellok8s.message }} 中获取。 1234567891011121314151617181920212223242526272829303132apiVersion: apps/v1kind: Deploymentmetadata: name: {{ .Values.application.name }}-deploymentspec: replicas: {{ .Values.application.hellok8s.replicas }} selector: matchLabels: app: hellok8s template: metadata: labels: app: hellok8s spec: containers: - image: {{ .Values.application.hellok8s.image }} name: hellok8s-container env: - name: DB_URL valueFrom: configMapKeyRef: name: {{ .Values.application.name }}-config key: DB_URL - name: DB_PASSWORD valueFrom: secretKeyRef: name: {{ .Values.application.name }}-secret key: DB_PASSWORD - name: NAMESPACE value: {{ .Release.Namespace }} - name: MESSAGE value: {{ .Values.application.hellok8s.message }} 修改 ingress.yaml 将 metadata.name 的值,其它没有变化 1234567apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: {{ .Values.application.name }}-ingress......... nginx-deployment.yaml 1234567891011121314151617apiVersion: apps/v1kind: Deploymentmetadata: name: nginx-deploymentspec: replicas: { { .Values.application.nginx.replicas } } selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - image: { { .Values.application.nginx.image } } name: nginx-container nginx-service.yaml 和 hellok8s-service.yaml 没有变化。所有代码可以在 这里 查看。 稍微修改下默认生成的Chart.yaml 123456apiVersion: v2name: hello-helmdescription: A k8s tutorials in https://github.com/guangzhengli/k8s-tutorialstype: applicationversion: 0.1.0appVersion: "1.16.0" 定义完成所有的 helm 资源后,首先将 **hellok8s:v6** 镜像打包推送到 DockerHub。 之后即可在 hello-helm 的目录下执行 helm upgrade 命令进行安装,安装成功后,执行 curl 命令便能直接得到结果!查看 pod 和 service 等资源,便会发现 helm 能一键安装所有资源! 1234567891011121314151617helm upgrade --install hello-helm --values values.yaml .# Release "hello-helm" does not exist. Installing it now.# NAME: hello-helm# NAMESPACE: default# STATUS: deployed# REVISION: 1curl http://192.168.59.100/hello# [v6] Hello, Helm! Message from helm values: It works with Helm Values!, From namespace: default, From host: hellok8s-deployment-57d7df7964-m6gcc, Get Database Connect URL: http://DB_ADDRESS_DEFAULT, Database Connect Password: db_passwordkubectl get pods# NAME READY STATUS RESTARTS AGE# hellok8s-deployment-f88f984c6-k8hpz 1/1 Running 0 32m# hellok8s-deployment-f88f984c6-nzwg6 1/1 Running 0 32m# hellok8s-deployment-f88f984c6-s89s7 1/1 Running 0 32m# nginx-deployment-d47fd7f66-6w76b 1/1 Running 0 32m# nginx-deployment-d47fd7f66-tsqj5 1/1 Running 0 32m rollbackHelm 也提供了 Rollback 的功能,我们先修改一下 message: "It works with Helm Values[v2]!" 加上 [v2]。 123456789101112application: name: hellok8s hellok8s: image: guangzhengli/hellok8s:v6 replicas: 3 message: "It works with Helm Values[v2]!" database: url: "http://DB_ADDRESS_DEFAULT" password: "db_password" nginx: image: nginx replicas: 2 再执行 helm upgrade 命令更新 k8s 资源,通过 curl http://192.168.59.100/hello 可以看到资源已经更新。 123456789➜ hello-helm git:(main) ✗ helm upgrade --install hello-helm --values values.yaml .# Release "hello-helm" has been upgraded. Happy Helming!NAME: hello-helmNAMESPACE: defaultSTATUS: deployedREVISION: 2curl http://192.168.59.100/hello# [v6] Hello, Helm! Message from helm values: It works with Helm Values[v2]!, From namespace: default, From host: hellok8s-deployment-598bbd6884-4b9bw, Get Database Connect URL: http://DB_ADDRESS_DEFAULT, Database Connect Password: db_password 如果这一次更新有问题的话,可以通过 helm rollback 快速回滚。通过下面命令看到,和 deployment 的 rollback 一样,回滚后 REVISION 版本都会增大到 3 而不是回滚到 1,回滚后使用 curl 命令返回的 v1 版本的字符串。 12345678910111213helm ls# NAME NAMESPACE REVISION STATUS CHART APP VERSION# hello-helm default 2 deployed hello-helm-0.1.0 1.16.0helm rollback hello-helm 1# Rollback was a success! Happy Helming!helm ls# NAME NAMESPACE REVISION STATUS CHART APP VERSION# hello-helm default 3 deployed hello-helm-0.1.0 1.16.0curl http://192.168.59.100/hello# [v6] Hello, Helm! Message from helm values: It works with Helm Values!, From namespace: default, From host: hellok8s-deployment-57d7df7964-482xw, Get Database Connect URL: http://DB_ADDRESS_DEFAULT, Database Connect Password: db_password 多环境配置使用 Helm 也很容易多环境部署,新建 values-dev.yaml 文件,里面内容自定义 dev 环境需要的配置信息。 123456application: hellok8s: message: "It works with Helm Values values-dev.yaml!" database: url: "http://DB_ADDRESS_DEV" password: "db_password_dev" 可以多次指定’–values -f’参数,最后(最右边)指定的文件优先级最高,这里最右边的是 values-dev.yaml 文件,所以 values-dev.yaml 文件中的值会覆盖 values.yaml 中相同的值,-n dev 表示在名字为 dev 的 namespace 中创建 k8s 资源,执行完成后,我们可以通过 curl 命令看到返回的字符串中读取的是 values-dev.yaml 文件的配置,并且 From namespace = dev。 123456789101112131415161718helm upgrade --install hello-helm -f values.yaml -f values-dev.yaml -n dev .# Release "hello-helm" does not exist. Installing it now.# NAME: hello-helm# NAMESPACE: dev# STATUS: deployed# REVISION: 1curl http://192.168.59.100/hello# [v6] Hello, Helm! Message from helm values: It works with Helm Values values-dev.yaml!, From namespace: dev, From host: hellok8s-deployment-f5fff9df-89sn6, Get Database Connect URL: http://DB_ADDRESS_DEV, Database Connect Password: db_password_devkubectl get pods -n dev# NAME READY STATUS RESTARTS AGE# hellok8s-deployment-f5fff9df-89sn6 1/1 Running 0 4m23s# hellok8s-deployment-f5fff9df-tkh6g 1/1 Running 0 4m23s# hellok8s-deployment-f5fff9df-wmlpb 1/1 Running 0 4m23s# nginx-deployment-d47fd7f66-cdlmf 1/1 Running 0 4m23s# nginx-deployment-d47fd7f66-cgst2 1/1 Running 0 4m23s 除此之外,还可以使用 ‘–set-file’ 设置独立的值,类似于 helm upgrade --install hello-helm -f values.yaml -f values-dev.yaml --set application.hellok8s.message="It works with set helm values" -n dev . 方式在命令中设置 values 的值,可以随意修改相关配置,此方法在 CICD 中经常用到。 helm chart 打包和发布上面的例子展示了我们可以用一行命令在一个新的环境中安装所有需要的 k8s 资源!那么如何将 helm chart 打包、分发和下载呢?在官网中,提供了两种教程,一种是以 GCS 存储的教程,还有一种是以 GitHub Pages 存储的教程。 这里我们使用第二种,并且使用 chart-releaser-action 来做自动发布,该 action 会默认生成 helm chart 发布到 gh-pages 分支上,本教程的 hellok8s helm chart 就发布在了本仓库的gh-pages 分支上的 index.yaml 文件中。 在使用 action 自动生成 chart 之前,我们可以先熟悉一下如何手动生成,在 hello-helm 目录下,执行 helm package 将 chart 目录打包到 chart 归档中。helm repo index 命令可以基于包含打包 chart 的目录,生成仓库的索引文件 index.yaml。 最后,可以使用 helm upgrade --install *.tgz 命令将该指定包进行安装使用。 123456helm package hello-helm# Successfully packaged chart and saved it to: /Users/guangzheng.li/workspace/k8s-tutorials/hello-helm/hello-helm-0.1.0.tgzhelm repo index .helm upgrade --install hello-helm hello-helm-0.1.0.tgz 基于上面的步骤,你可能已经想到,所谓的 helm 打包和发布,就是 hello-helm-0.1.0.tgz 文件和 index.yaml 生成和上传的一个过程。而 helm 下载和安装,就是如何将 .tgz 和 index.yaml 文件下载和 helm upgrade --install 的过程。 接下来我们发布生成的 hellok8s helm chart,先将手动生成的 hello-helm-0.1.0.tgz 和 index.yaml 文件删除,后续使用 GitHub action 自动生成和发布这两个文件。 GitHub action 的代码可以参考 官网文档 或者本仓库 .github/workflows/release.yml 文件。代表当 push 代码到远程仓库时,将 helm-charts 目录下的所有 charts 自动打包和发布到 gh-pages 分支去(需要保证 gh-pages 分支已经存在,否则会报错)。 123456789101112131415161718192021222324252627282930313233343536name: Release Chartson: push: branches: - mainjobs: release: # depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions # see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token permissions: contents: write runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Configure Git run: | git config user.name "$GITHUB_ACTOR" git config user.email "[email protected]" - name: Install Helm uses: azure/setup-helm@v1 with: version: v3.8.1 - name: Run chart-releaser uses: helm/[email protected] with: charts_dir: helm-charts env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 接着配置仓库的 Settings -> Pages -> Build and deployment -> Branch,选择 gh-pages 分支,GitHub 会自动在 https://username.github.io/project 发布 helm chart。 最后,你可以将自己的 helm charts 发布到社区中去,可以考虑发布到 ArtifactHub 中,像本仓库生成的 helm charts 即发布在 ArtifactHub hellok8s 中。 Dashboardkubernetes dashboard Dashboard 是基于网页的 Kubernetes 用户界面。 你可以使用 Dashboard 将容器应用部署到 Kubernetes 集群中,也可以对容器应用排错,还能管理集群资源。 你可以使用 Dashboard 获取运行在集群中的应用的概览信息,也可以创建或者修改 Kubernetes 资源 (如 Deployment,Job,DaemonSet 等等)。 例如,你可以对 Deployment 实现弹性伸缩、发起滚动升级、重启 Pod 或者使用向导创建新的应用。 在本地 minikube 环境,可以直接通过下面命令开启 Dashboard。更多用法可以参考官网或者自行探索。 1minikube dashboard K9sK9s 是一个基于 Terminal 的轻量级 UI,可以更加轻松的观察和管理已部署的 k8s 资源。使用方式非常简单,安装后输入 k9s 即可开启 Terminal Dashboard,更多用法可以参考官网。 Star History","categories":[],"tags":[]},{"title":"COS + Github Actions 实现语雀自动发布","slug":"COS + Github Actions 实现语雀自动发布","date":"2023-04-02T23:17:38.000Z","updated":"2024-10-04T01:13:34.018Z","comments":true,"path":"2023/04/02/COS + Github Actions 实现语雀自动发布/","link":"","permalink":"https://g-ydg.github.io/2023/04/02/COS%20+%20Github%20Actions%20%E5%AE%9E%E7%8E%B0%E8%AF%AD%E9%9B%80%E8%87%AA%E5%8A%A8%E5%8F%91%E5%B8%83/","excerpt":"","text":"配置 Github ActionsSecrets SECRET_ID 操作员账号 SECRET_KEY 操作员密码 YUQUE_TOKEN 语雀访问令牌 HEXO_DEPLOY_KEY 部署私钥(用于 hexo deploy) Workflows在根目录下,新增 **.github/workflows/main.yaml **文件 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768name: Deployon: # 外部事件触发 repository_dispatch: types: - start# 定时任务触发# schedule:# - cron: '0 0 * * *'jobs: build: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@master - name: install node uses: actions/setup-node@master with: node-version: "16" - name: 安装依赖 run: npm install - name: 语雀同步 env: SECRET_ID: ${{ secrets.SECRET_ID }} SECRET_KEY: ${{ secrets.SECRET_KEY }} YUQUE_TOKEN: ${{ secrets.YUQUE_TOKEN }} run: | npm run yuque:clean npm run yuque:sync - name: 博客构建 run: | npm run clean npm run build - name: GIT配置 run: | git config --global user.name "your name" git config --global user.email "your email" - name: 提交变更 run: | echo `date +"%Y-%m-%d %H:%M:%S"` begin > deploy.txt git add . git commit -m "Refresh content" -a git pull origin master - name: 推送变更 uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} - name: 博客发布 env: HEXO_DEPLOY_KEY: ${{ secrets.HEXO_DEPLOY_KEY }} run: | mkdir -p ~/.ssh/ echo "$HEXO_DEPLOY_KEY" > ~/.ssh/id_rsa chmod 700 ~/.ssh chmod 600 ~/.ssh/id_rsa ssh-keyscan github.com >> ~/.ssh/known_hosts npm run deploy 构建测试命令行 curl 链接,手动触发查看是否可以构建成功。 123456curl --location --request POST 'https://api.github.com/repos/用户名/仓库名/dispatches' \\--header 'Authorization: token GITHUB访问令牌' \\--header 'Accept: application/vnd.github.everest-preview+json' \\--header 'Content-Type: application/json' \\--header 'User-Agent: curl/7.52.1' \\--data-raw '{"event_type": "start"}' 如下图所示,则说明可以构建成功。 配置腾讯云函数由于语雀的 webhook,不支持 POST 请求以及自定义请求头,所以咱们通过腾讯云的云函数进行处理。 访问 Serverless 控制台,选择函数服务,新增云函数。 模板选择“从头开始”,函数类型选择“事件函数”,运行环境选择“python2.7” 。 函数代码选择“在线编辑”,代码如下。 1234567891011121314151617# -*- coding: utf8 -*-import requestsdef main_handler(event, context): url = "https://api.github.com/repos/用户名/仓库名/dispatches" payload="{\\"event_type\\": \\"start\\"}" headers = { 'Authorization': 'token GITHUB访问令牌', 'Accept': 'application/vnd.github.everest-preview+json', 'Content-Type': 'application/json', 'User-Agent': 'curl/7.52.1' } response = requests.request("POST", url, headers=headers, data=payload) if response.status_code == 204: return "This's OK!" else: return response.status_code 注:event_type 的值需要和 Github Actions 中配置的 repository_dispatch 的 types 值保持一致。 注:Authorization 值为 字符串 “token Github 访问 token“。 触发器配置,选择“自定义创建”,触发方式选择“API 网关触发” 完成创建后,点击查看云函数的公网访问路径。 配置语雀 Webhook知识库设置 -> 消息推送 -> 其他渠道 -> 添加推送。","categories":[],"tags":[]},{"title":"Hexo同步语雀文章","slug":"Hexo同步语雀文章","date":"2023-03-30T23:40:30.000Z","updated":"2024-10-04T01:13:34.875Z","comments":true,"path":"2023/03/30/Hexo同步语雀文章/","link":"","permalink":"https://g-ydg.github.io/2023/03/30/Hexo%E5%90%8C%E6%AD%A5%E8%AF%AD%E9%9B%80%E6%96%87%E7%AB%A0/","excerpt":"","text":"初始化 Hexo首先先搭建 Hexo 项目,详情可参考文章 Github+Hexo 搭建个人博客。 安装 yuque-hexo1npm install yuque-hexo 配置语雀打开 Toten 设置页(工作台 -> 个人中心 -> 账号设置 -> Token),新建 Token 并配置读取权限 修改配置修改 package.json 1234567891011121314151617181920{ "name": "your hexo project", "yuqueConfig": { "postPath": "source/_posts/yuque", "baseUrl": "https://www.yuque.com/api/v2", "login": "your yuque login", "repo": "your yuque repo", "onlyPublished": false, "onlyPublic": false, "lastGeneratePath": "lastGeneratePath.log", "imgCdn": { "enabled": false, "concurrency": 1, "imageBed": "github", "host": "", "bucket": "", "prefixKey": "" } }} 参数名 含义 baseUrl 语雀 API 地址 login 语雀 login (group), 也称为个人路径 repo 语雀仓库短名称,也称为语雀知识库路径 onlyPublished 只展示已经发布的文章 onlyPublic 只展示公开文章 imgCdn 语雀图片转 CDN 配置,支持七牛、腾讯云、阿里云、Github 等 imgCdn 参数名 含义 enabled 是否开启 concurrency 上传图片并发数, 0 代表无限制 imageBed 图床类型,cos、oss、qiniu、upyun、github host 只展示已经发布的文章 bucket 图床的 bucket 名称 prefixKey 文件前缀 Github 图床配置示例Github 图床怎么搭建,可参考文章 《如何利用 Github 搭建自己的免费图床?》 12345678910{ "imgCdn": { "enabled": true, "concurrency": 1, "imageBed": "github", "host": "cdn.jsdelivr.net", "bucket": "images", "prefixKey": "blog" }} bucket:项目仓库 prefixkey:项目仓库目录 同步文章1export YUQUE_TOKEN=xxx SECRET_ID=xxx SECRET_KEY=xxx && yuque-hexo sync 参数名 含义 SECRET_ID 操作员账号 SECRET_KEY 操作员密码 YUQUE_TOKEN 语雀访问令牌 参考链接 yuque-hexo yuque-hexo-example 手把手教你打造语雀+Hexo+Github Actions+COS 持续集成 如何利用 Github 搭建自己的免费图床?","categories":[],"tags":[]},{"title":"SwooleCoroutine","slug":"SwooleCoroutine","date":"2023-03-24T20:55:02.000Z","updated":"2024-10-04T01:13:34.934Z","comments":true,"path":"2023/03/24/SwooleCoroutine/","link":"","permalink":"https://g-ydg.github.io/2023/03/24/SwooleCoroutine/","excerpt":"","text":"什么是协程协程可以简单理解为线程,只不过这个线程是用户态的,不需要操作系统参与,创建销毁和切换的成本非常低,和线程不同的是协程没法利用多核 CPU 的,想利用多核 CPU 需要依赖 Swoole 的多进程模型。 什么是 ChannelChannel 可以理解为消息队列,只不过是协程间的消息队列,多个协程通过 push 和 pop 操作队列中的生产消息和消费消息,用来发送或者接收数据进行协程之间的通讯。需要注意的是 Channel 是没法跨进程的,只能一个 Swoole 进程里的协程间通讯,最典型的应用是连接池和并发调用。 什么是协程容器使用 Coroutine::create 或 go() 方法创建协程 (参考别名小节),在创建的协程中才能使用协程 API,而协程必须创建在协程容器里面,参考协程容器。 协程调度这里将尽量通俗的讲述什么是协程调度,首先每个协程可以简单的理解为一个线程,大家知道多线程是为了提高程序的并发,同样的多协程也是为了提高并发。 用户的每个请求都会创建一个协程,请求结束后协程结束,如果同时有成千上万的并发请求,某一时刻某个进程内部会存在成千上万的协程,那么 CPU 资源是有限的,到底执行哪个协程的代码? 决定到底让 CPU 执行哪个协程的代码的决断过程就是协程调度,Swoole 的调度策略又是怎么样的呢? 首先,在执行某个协程代码的过程中发现这行代码遇到了 Co::sleep() 或者产生了网络 IO,例如 MySQL->query(),这肯定是一个耗时的过程,Swoole 就会把这个 MySQL 连接的 Fd 放到 EventLoop 中。 然后让出这个协程的 CPU 给其他协程使用:即** **yield**(挂起)** 等待 MySQL 数据返回后再继续执行这个协程:即** **resume**(恢复)** 其次,如果协程的代码有 CPU 密集型代码,可以开启 enable_preemptive_scheduler,Swoole 会强行让这个协程让出 CPU。 父子协程优先级优先执行子协程 (即 go() 里面的逻辑),直到发生协程 yield(Co::sleep () 处),然后协程调度到外层协程。 12345678910111213141516use Swoole\\Coroutine;use function Swoole\\Coroutine\\run;echo "main start\\n";run(function () { echo "coro " . Coroutine::getcid() . " start\\n"; Coroutine::create(function () { echo "coro " . Coroutine::getcid() . " start\\n"; Coroutine::sleep(.2); echo "coro " . Coroutine::getcid() . " end\\n"; }); echo "coro " . Coroutine::getcid() . " do not wait children coroutine\\n"; Coroutine::sleep(.1); echo "coro " . Coroutine::getcid() . " end\\n";});echo "end\\n"; 123456789/*main startcoro 1 startcoro 2 startcoro 1 do not wait children coroutinecoro 1 endcoro 2 endend*/ 注意事项在使用 Swoole 编程前应该注意的地方: 全局变量协程使得原有的异步逻辑同步化,但是在协程间的切换是隐式发生的,所以在协程切换的前后不能保证全局变量以及 static 变量的一致性。 在 PHP-FPM 下可以通过全局变量获取到的请求参数,服务器的参数等。 在 Swoole 内,无法 通过 $_GET/$_POST/$_REQUEST/$_SESSION/$COOKIE/$_SERVER 等以 $开头的变量获取到任何属性参数。 可以使用 context 用协程 id 做隔离,实现全局变量的隔离。 多协程共享 TCP 连接对于一个 TCP 连接来说 Swoole 底层允许同时只能有一个协程进行读操作、一个协程进行写操作。也就是说不能有多个协程对一个 TCP 进行读 / 写操作,底层会抛出绑定错误: 1Fatal error: Uncaught Swoole\\Error: Socket#6 has already been bound to another coroutine#2, reading or writing of the same socket in coroutine#3 at the same time is not allowed 重现代码: 12345678910111213use Swoole\\Coroutine;use Swoole\\Coroutine\\Http\\Client;use function Swoole\\Coroutine\\run;run(function() { $cli = new Client('www.xinhuanet.com', 80); Coroutine::create(function () use ($cli) { $cli->get('/'); }); Coroutine::create(function () use ($cli) { $cli->get('/'); });}); 解决方案参考:https://wenda.swoole.com/detail/107474 此限制对于所有多协程环境都有效,最常见的就是在 onReceive 等回调函数中去共用一个 TCP 连接,因为此类回调函数会自动创建一个协程, 那有连接池需求怎么办? Swoole 内置了连接池可以直接使用,或手动用 channel 封装连接池。","categories":[],"tags":[]},{"title":"Swoole常用配置","slug":"Swoole常用配置","date":"2023-03-24T19:39:00.000Z","updated":"2024-10-04T01:13:35.074Z","comments":true,"path":"2023/03/24/Swoole常用配置/","link":"","permalink":"https://g-ydg.github.io/2023/03/24/Swoole%E5%B8%B8%E7%94%A8%E9%85%8D%E7%BD%AE/","excerpt":"","text":"reactor_num设置启动的 Reactor 线程数。【默认值:CPU 核数】。 通过此参数来调节主进程内事件处理线程的数量,以充分利用多核。默认会启用 CPU 核数相同的数量。Reactor 线程是可以利用多核,如:机器有 128 核,那么底层会启动 128 线程。每个线程能都会维持一个 EventLoop。线程之间是无锁的,指令可以被 128 核 CPU 并行执行。考虑到操作系统调度存在一定程度的性能损失,可以设置为 CPU 核数 * 2,以便最大化利用 CPU 的每一个核。 提示 reactor_num 建议设置为 CPU 核数的 1-4 倍 reactor_num 最大不得超过 swoole_cpu_num() * 4 注意 reactor_num 必须小于或等于 worker_num ;如果设置的 reactor_num 大于 worker_num,会自动调整使 reactor_num 等于 worker_num ;在超过 8 核的机器上 reactor_num 默认设置为 8。 worker_num设置启动的 **Worker** 进程数。【默认值:CPU 核数】 如 1 个请求耗时 100ms,要提供 1000QPS 的处理能力,那必须配置 100 个进程或更多。但开的进程越多,占用的内存就会大大增加,而且进程间切换的开销就会越来越大。所以这里适当即可。不要配置过大。 提示 如果业务代码是全异步 IO 的,这里设置为 CPU 核数的 1-4 倍最合理 如果业务代码为同步 IO,需要根据请求响应时间和系统负载来调整,例如:100-500 默认设置为 swoole_cpu_num(),最大不得超过 swoole_cpu_num() * 1000 假设每个进程占用 40M 内存,100 个进程就需要占用 4G 内存 max_request设置 **worker** 进程的最大任务数。【默认值:0 即不会退出进程】。 一个 worker 进程在处理完超过此数值的任务后将自动退出,进程退出后会释放所有内存和资源。 这个参数的主要作用是解决由于程序编码不规范导致的 PHP 进程内存泄露问题。PHP 应用程序有缓慢的内存泄漏,但无法定位到具体原因、无法解决,可以通过设置 max_request 临时解决,需要找到内存泄漏的代码并修复,而不是通过此方案。 设置此参数可能会产生的影响: 客户端重连(BASE 模式) 内存峰值大 无法常驻内存 首次请求慢(opcache, ext minits) 提示 达到 max_request 不一定马上关闭进程,参考 max_wait_time SWOOLE_BASE 下,达到 max_request 重启进程会导致客户端连接断开 当 worker 进程内发生致命错误或者人工执行 exit 时,进程会自动退出。master 进程会重新启动一个新的worker 进程来继续处理请求。 max_conn (max_connection)服务器程序,最大允许的连接数。【默认值:ulimit -n】。 如 max_connection => 10000, 此参数用来设置 Server 最大允许维持多少个 TCP 连接。超过此数量后,新进入的连接将被拒绝。 提示 默认设置 应用层未设置 max_connection,底层将使用 ulimit -n 的值作为缺省设置 在 4.2.9 或更高版本,当底层检测到 ulimit -n 超过 100000 时将默认设置为 100000,原因是某些系统设置了 ulimit -n 为 100 万,需要分配大量内存,导致启动失败 最大上限 请勿设置 max_connection 超过 1M 最小设置 此选项设置过小底层会抛出错误,并设置为 ulimit -n 的值。 最小值为 (worker_num + task_worker_num) * 2 + 32 serv->max_connection is too small. - **<font style="color:rgb(44, 62, 80);">内存占用</font>** * <font style="color:rgb(233, 105, 0);background-color:rgb(248, 248, 248);">max_connection</font><font style="color:rgb(52, 73, 94);"> 参数不要调整的过大,根据机器内存的实际情况来设置。</font><font style="color:rgb(233, 105, 0);background-color:rgb(248, 248, 248);">Swoole</font><font style="color:rgb(52, 73, 94);"> 会根据此数值一次性分配一块大内存来保存 </font><font style="color:rgb(233, 105, 0);background-color:rgb(248, 248, 248);">Connection</font><font style="color:rgb(52, 73, 94);"> 信息,一个 </font><font style="color:rgb(233, 105, 0);background-color:rgb(248, 248, 248);">TCP</font><font style="color:rgb(52, 73, 94);"> 连接的 </font><font style="color:rgb(233, 105, 0);background-color:rgb(248, 248, 248);">Connection</font><font style="color:rgb(52, 73, 94);"> 信息,需要占用 </font><font style="color:rgb(233, 105, 0);background-color:rgb(248, 248, 248);">224</font><font style="color:rgb(52, 73, 94);"> 字节。</font> 注意 max_connection 最大不得超过操作系统 ulimit -n 的值,否则会报一条警告信息,并重置为 ulimit -n 的值 WARN swServer_start_check: serv->max_conn is exceed the maximum value[100000]. WARNING set_max_connection: max_connection is exceed the maximum value, it’s reset to 10240 task_worker_num配置** Task 进程的数量。** 配置此参数后将会启用 task 功能。所以 Server 务必要注册 onTask、onFinish 2 个事件回调函数。如果没有注册,服务器程序将无法启动。 提示 Task 进程是同步阻塞的 最大值不得超过 swoole_cpu_num() * 1000 计算方法 单个 task 的处理耗时,如 100ms,那一个进程 1 秒就可以处理 1/0.1=10 个 task task 投递的速度,如每秒产生 2000 个 task 2000/10=200,需要设置 task_worker_num => 200,启用 200 个 Task 进程 注意 Task 进程内不能使用 Swoole\\Server->task 方法 task_max_request设置** task 进程的最大任务数。**【默认值:0】 设置 task 进程的最大任务数。一个 task 进程在处理完超过此数值的任务后将自动退出。这个参数是为了防止 PHP 进程内存溢出。如果不希望进程自动退出可以设置为 0。 heartbeat_check_interval启用心跳检测【默认值:false】 此选项表示每隔多久轮循一次,单位为秒。如 heartbeat_check_interval => 60,表示每 60 秒,遍历所有连接,如果该连接在 120 秒内(heartbeat_idle_time 未设置时默认为 interval 的两倍),没有向服务器发送任何数据,此连接将被强制关闭。若未配置,则不会启用心跳,该配置默认关闭,参考 Swoole 官方视频教程。 提示 Server 并不会主动向客户端发送心跳包,而是被动等待客户端发送心跳。服务器端的 heartbeat_check 仅仅是检测连接上一次发送数据的时间,如果超过限制,将切断连接。 被心跳检测切断的连接依然会触发 onClose 事件回调 注意 heartbeat_check 仅支持 TCP 连接 heartbeat_idle_time连接最大允许空闲的时间 需要与 heartbeat_check_interval 配合使用 123456array( // 表示一个连接如果600秒内未向服务器发送任何数据,此连接将被强制关闭 'heartbeat_idle_time' => 600, // 表示每60秒遍历一次 'heartbeat_check_interval' => 60,); 提示 启用 heartbeat_idle_time 后,服务器并不会主动向客户端发送数据包 如果只设置了 heartbeat_idle_time 未设置 heartbeat_check_interval 底层将不会创建心跳检测线程,PHP 代码中可以调用 heartbeat 方法手动处理超时的连接 reload_async设置异步重启开关。【默认值:true】 设置异步重启开关。设置为 true 时,将启用异步安全重启特性,Worker 进程会等待异步事件完成后再退出。详细信息请参见 如何正确的重启服务 reload_async 开启的主要目的是为了保证服务重载时,协程或异步任务能正常结束。 1$server->set(['reload_async' => true]); 协程模式 在 4.x 版本中开启 enable_coroutine 时,底层会额外增加一个协程数量的检测,当前无任何协程时进程才会退出,开启时即使 reload_async => false 也会强制打开 reload_async。 max_wait_time设置** **Worker** **进程收到停止服务通知后最大等待时间【默认值:3】 经常会碰到由于 worker 阻塞卡顿导致 worker 无法正常 reload, 无法满足一些生产场景,例如发布代码热更新需要 reload 进程。所以,Swoole 加入了进程重启超时时间的选项。详细信息请参见 如何正确的重启服务 提示 管理进程收到重启、关闭信号后或者达到** **max_request** **时,管理进程会重起该** **worker** **进程。分以下几个步骤: 底层会增加一个 (max_wait_time) 秒的定时器,触发定时器后,检查进程是否依然存在,如果是,会强制杀掉,重新拉一个进程。 需要在 onWorkerStop 回调里面做收尾工作,需要在 max_wait_time 秒内做完收尾。 依次向目标进程发送 SIGTERM 信号,杀掉进程。 注意 v4.4.x 以前默认为 30 秒 enable_coroutine是否启用异步风格服务器的协程支持 enable_coroutine 关闭时在事件回调函数中不再自动创建协程,如果不需要用协程关闭这个会提高一些性能。参考什么是 Swoole 协程。 配置方法 在 php.ini 配置 swoole.enable_coroutine = ‘Off’ (可见 ini 配置文档 ) $server->set([‘enable_coroutine’ => false]); 优先级高于 ini enable_coroutine** **选项影响范围 onWorkerStart onConnect onOpen onReceive setHandler onPacket onRequest onMessage onPipeMessage onFinish onClose tick/after 定时器 开启 enable_coroutine 后在上述回调函数会自动创建协程 当 enable_coroutine 设置为 true 时,底层自动在 onRequest 回调中创建协程,开发者无需自行使用 go 函数创建协程 当 enable_coroutine 设置为 false 时,底层不会自动创建协程,开发者如果要使用协程,必须使用 go 自行创建协程,如果不需要使用协程特性,则处理方式与 Swoole1.x 是 100% 一致的 123456789101112131415161718192021$server = new Swoole\\Http\\Server("127.0.0.1", 9501);$server->set([ //关闭内置协程 'enable_coroutine' => false,]);$server->on("request", function ($request, $response) { if ($request->server['request_uri'] == '/coro') { go(function () use ($response) { co::sleep(0.2); $response->header("Content-Type", "text/plain"); $response->end("Hello World\\n"); }); } else { $response->header("Content-Type", "text/plain"); $response->end("Hello World\\n"); }});$server->start(); task_enable_coroutine开启** **Task** **协程支持。【默认值:false】,v4.2.12 起支持 开启后自动在 onTask 回调中创建协程和协程容器,PHP 代码可以直接使用协程 API。 示例 12345678910111213141516$server->on('Task', function ($serv, Swoole\\Server\\Task $task) { //来自哪个 Worker 进程 $task->worker_id; //任务的编号 $task->id; //任务的类型,taskwait, task, taskCo, taskWaitMulti 可能使用不同的 flags $task->flags; //任务的数据 $task->data; //投递时间,v4.6.0版本增加 $task->dispatch_time; //协程 API co::sleep(0.2); //完成任务,结束并返回数据 $task->finish([123, 'hello']);}); 注意 task_enable_coroutine 必须在 enable_coroutine 为 true 时才可以使用开启 task_enable_coroutine,Task 工作进程支持协程未开启 task_enable_coroutine,仅支持同步阻塞 max_coroutine/max_coro_num设置当前工作进程最大协程数量。【默认值:100000,Swoole 版本小于 v4.4.0-beta 时默认值为 3000】 超过 max_coroutine 底层将无法创建新的协程,服务端的 Swoole 会抛出 exceed max number of coroutine 错误,TCP Server 会直接关闭连接,Http Server 会返回 Http 的 503 状态码。 在 Server 程序中实际最大可创建协程数量等于 worker_num * max_coroutine,task 进程和 UserProcess 进程的协程数量单独计算。 123$server->set(array( 'max_coroutine' => 3000,));","categories":[],"tags":[]},{"title":"PHP的内存泄漏","slug":"PHP的内存泄漏","date":"2023-03-24T17:26:50.000Z","updated":"2024-10-04T01:13:35.088Z","comments":true,"path":"2023/03/24/PHP的内存泄漏/","link":"","permalink":"https://g-ydg.github.io/2023/03/24/PHP%E7%9A%84%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/","excerpt":"","text":"所谓的内存泄漏就是忘记释放内存,导致进程所占用的物理内存持续增长。 FPM 的黑魔法 - php_request_shutdown在 传统运行在 FPM 下的 PHP 代码是没有“内存泄漏”一说的。在 PHP 内核有一个关键函数叫做 php_request_shutdown,此函数会在请求结束后,把请求期间申请的所有内存都释放掉。这从根本上杜绝了内存泄漏,极大的提高了 PHPer 的开发效率,同时也会导致性能的下降,例如单例对象,没必要每次请求都重新申请释放这个单例对象的内存。而这也是 Swoole 等 Cli 方案的优势之一,因为 Cli 请求结束不会清理内存。 Cli 下的内存泄漏 Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 12288 bytes) 相信 PHPer 都遇见过这个报错,这是由于向 PHP 申请的内存达到了上限导致的,在 FPM 下一定是因为这次请求有大内存块申请,例如 SQL 查询返回一个超大结果集,但在 Cli 下报这个错大概率是因为你的 PHP 代码出现了内存泄漏。 常见的泄漏姿势有: 向类的静态属性中追加数据,例如: 12345//不停的调用foo() 内存就会一直涨function foo(){ ClassA::$pro[] = "the big string";} 向 $GLOBAL 全局变量中追加数据,例如: 12345//不停的调用foo() 内存就会一直涨function foo(){ $GLOBAL['arr'][] = "the big string";} 向函数的静态变量中追加数据,例如: 123456//不停的调用foo() 内存就会一直涨function foo(){ static $arr = []; $arr[] = "the big string";}","categories":[],"tags":[]},{"title":"Argocd-状态停留在Deleting,但资源已删除","slug":"Argocd-状态停留在!Deleting!,但资源已删除","date":"2023-02-26T23:05:45.000Z","updated":"2024-10-04T01:13:37.264Z","comments":true,"path":"2023/02/26/Argocd-状态停留在!Deleting!,但资源已删除/","link":"","permalink":"https://g-ydg.github.io/2023/02/26/Argocd-%E7%8A%B6%E6%80%81%E5%81%9C%E7%95%99%E5%9C%A8!Deleting!,%E4%BD%86%E8%B5%84%E6%BA%90%E5%B7%B2%E5%88%A0%E9%99%A4/","excerpt":"","text":"如上图所示,该应用所对应的资源均已被删除,但是 **argocd **这边一直卡住“Deleting”状态。 解决方法 添加 spec.syncPolicy.allowEmpty: true 1234567891011121314apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: service-name namespace: argocdspec: ... project: proj-name syncPolicy: automated: prune: true allowEmpty: true selfHeal: true ... 移除 metadata.finalizers 12345678910111213141516apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: service-name namespace: argocd# finalizers:# - resources-finalizer.argocd.argoproj.iospec: ... project: proj-name syncPolicy: automated: prune: true allowEmpty: true selfHeal: true ... 如上所示,移除 metadata.finalizers ,再进行删除即可。 阿里云 K8S 参考文章 argocd - stuck at deleting but resources are already deleted Argocd:App Deletion","categories":[],"tags":[]},{"title":"EasySwoole踩坑笔记","slug":"EasySwoole踩坑笔记","date":"2022-12-04T19:49:44.000Z","updated":"2024-10-04T01:13:37.270Z","comments":true,"path":"2022/12/04/EasySwoole踩坑笔记/","link":"","permalink":"https://g-ydg.github.io/2022/12/04/EasySwoole%E8%B8%A9%E5%9D%91%E7%AC%94%E8%AE%B0/","excerpt":"","text":"使用 Composer 2.2.x 时服务无法启动在使用 EasySwoole 3.5.x 之前的版本和 Composer 2.2.x 环境开发时,你可能会遇到类似以下这样的错误。 1PHP Fatal error: Namespace declaration statement has to be the very first statement or after any declare call in the script in Xxx 解决方法 框架版本升级 1composer require easyswoole/easyswoole=3.5.x 降低 composer 版本 12wget -nv -O /usr/local/bin/composer https://github.com/composer/composer/releases/download/2.1.14/composer.pharchmod u+x /usr/local/bin/composer Task socket listen fail在 windows 下使用 docker 环境开发,可能会出现 <font style="color:rgb(36, 41, 46);">task socket listen fail</font>的问题,主要原因是框架中的 Temp 目录无法被创建。 解决方法 将 dev.php 中的 Temp 目录改为其他路径即可,如:’/tmp’ 1234567891011<?phpreturn [...... 'TEMP_DIR' => '/tmp',......]","categories":[],"tags":[]},{"title":"MySQL数据碎片整理","slug":"MySQL数据碎片整理","date":"2022-11-30T19:27:17.000Z","updated":"2024-10-04T01:13:37.278Z","comments":true,"path":"2022/11/30/MySQL数据碎片整理/","link":"","permalink":"https://g-ydg.github.io/2022/11/30/MySQL%E6%95%B0%E6%8D%AE%E7%A2%8E%E7%89%87%E6%95%B4%E7%90%86/","excerpt":"","text":"查看表的碎片情况1select table_schema, table_name, concat(data_free/1024/1024, 'M') as data_free from information_schema.tables where data_free > 0 and engine = 'innodb' order by data_free desc; 查看指定表的碎片情况1show table status like 'table_name'; 清理碎片 Alter Table 1alter table tb_name engine=innodb; Optimize Table 1optimize table tb_name; 相关文章 MySQL 的几种碎片整理方案 MySQL 删除方法 delete、truncate、drop 的区别是什么 面试官:为什么 MySQL 中 Delete 表数据后,磁盘空间却还是被占用? 什么?还在用 delete 删除数据《死磕 MySQL 系列 九》","categories":[],"tags":[]},{"title":"PHPStrom: PHP CS Fixer(Windows)","slug":"PHPStrom! PHP CS Fixer(Windows)","date":"2022-11-11T19:45:17.000Z","updated":"2024-10-04T01:13:51.921Z","comments":true,"path":"2022/11/11/PHPStrom! PHP CS Fixer(Windows)/","link":"","permalink":"https://g-ydg.github.io/2022/11/11/PHPStrom!%20PHP%20CS%20Fixer%EF%BC%88Windows%EF%BC%89/","excerpt":"","text":"安装1composer global require friendsofphp/php-cs-fixer 查看路径打开 GitBash,通过 which 命令查询。 配置打开设置,File -> Settings。 PHP -> Quality Tools。 本文以 SystemPHP 为例,可自行选择。 PHP CS Fixer Path,填写 php-cs-fixer.bat 的所在路径。 验证是否可执行。 开启 PHP CS Fixer validation。 本文以自定义规则作为示例,可自行选择。 选择应用生效。 运行配置示例 代码示例","categories":[],"tags":[]},{"title":"阿里云-NAS挂载到ECS实例","slug":"阿里云-NAS挂载到ECS实例","date":"2022-11-08T22:12:25.000Z","updated":"2024-10-04T01:14:04.944Z","comments":true,"path":"2022/11/08/阿里云-NAS挂载到ECS实例/","link":"","permalink":"https://g-ydg.github.io/2022/11/08/%E9%98%BF%E9%87%8C%E4%BA%91-NAS%E6%8C%82%E8%BD%BD%E5%88%B0ECS%E5%AE%9E%E4%BE%8B/","excerpt":"","text":"创建文件系统进入阿里云文件存储。 创建 NAS 挂载到 ECS进入文件系统详情。 添加挂载点 注意:网络 VPC 以及虚拟交换机,需要与挂载的 ECS 实例处于同一网络。 默认协议类型为 NFSv3,若需要多台 ECS 同时编辑一个文件,则建议使用 NFSv4.0。 挂载成功。=","categories":[],"tags":[]},{"title":"在PHPStrom中配置Docker开发环境","slug":"在PHPStrom中配置Docker开发环境","date":"2022-10-21T18:30:23.000Z","updated":"2024-10-04T01:14:34.346Z","comments":true,"path":"2022/10/21/在PHPStrom中配置Docker开发环境/","link":"","permalink":"https://g-ydg.github.io/2022/10/21/%E5%9C%A8PHPStrom%E4%B8%AD%E9%85%8D%E7%BD%AEDocker%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83/","excerpt":"","text":"拉取 Docker 镜像本文以 hyperf/hyperf:8.1-alpine-v3.16-swoole 镜像作为示例参考,实际可根据自身情况进行修改。 1docker pull hyperf/hyperf:8.1-alpine-v3.16-swoole PHP CLI InterpreterFile -> Settings PHP -> CLI Interpreter ** + -> Form Docker… ** Docker -> 选择镜像 -> OK Apply -> OK 配置成功后,PHPStrom 会自动生成路径映射,将项目目录映射到容器的/opt/project 目录,点击Apply进行应用。 PHP ComposerComposer Execution 新增 composer.json 文件 123456789{ "name": "ydd/code-demo", "require": { "php": "^8.0" }, "require-dev": { "swoole/ide-helper": "^4.6.0" }} 使用 IDE 的 Compser 工具提供的命令进行安装 查看日志 Command Line Tool SupportIDE 的 Composer 工具只有几个常用命令,如果要执行一些特殊的命令,则无法满足。故此,需要再配置一个命令工具。 /usr/local/bin/composer为镜像中 composer 的工作目录,可根据自身镜像进行调整。 Ctrl + Shift + X,输入命令composer -V并回车执行 接着输入命令composer install,会发现执行失败,提示未找到 composer.json 文件。这是因为咱们现在的工作路径是/usr/local/bin/composer,而在容器中的项目目录是/opt/project。 选择自定义 根据invoke处并配置 –working-dir=/opt/project 运行命令composer install,此时就执行成功了。 PHP CodeSniffer安装squizlabs/php_codesniffer包 1composer require squizlabs/php_codesniffer --dev 填写路径 1/opt/project/vendor/bin/phpcs 1/opt/project/vendor/bin/phpcbf 点击** Validate**,出现 OK 则配置正确,点击 **Apply **进行应用。 查看效果 PHP CS Fixer安装friendsofphp/php-cs-fixer包 1composer require friendsofphp/php-cs-fixer --dev 查看效果 自定义编码风格 12345678910111213141516171819<?phpreturn (new PhpCsFixer\\Config()) ->setRiskyAllowed(true) ->setRules([ '@PSR2' => true, '@PhpCsFixer' => true, 'list_syntax' => [ 'syntax' => 'short', // 将 list() 转换为 [] ], ]) ->setFinder( PhpCsFixer\\Finder::create() ->exclude('public') ->exclude('runtime') ->exclude('vendor') ->in(__DIR__) ) ->setUsingCache(false); 查看效果","categories":[],"tags":[]},{"title":"Alpine下PHP扩展ICONV不工作","slug":"Alpine下PHP扩展ICONV不工作","date":"2022-09-08T22:59:40.000Z","updated":"2024-10-04T01:14:34.364Z","comments":true,"path":"2022/09/08/Alpine下PHP扩展ICONV不工作/","link":"","permalink":"https://g-ydg.github.io/2022/09/08/Alpine%E4%B8%8BPHP%E6%89%A9%E5%B1%95ICONV%E4%B8%8D%E5%B7%A5%E4%BD%9C/","excerpt":"","text":"错误 Notice: iconv(): Wrong charset, conversion from UTF-8' to UTF-8//IGNORE’ is not allowed in Command line code on line 1 原因在最新版本的 gnu-libiconv 中,不再包含 preloadable_libiconv.so。 解决在使用alpine:3.12镜像时,可以执行以下命令解决: 12RUN apk --no-cache --allow-untrusted --repository http://dl-cdn.alpinelinux.org/alpine/edge/community/ add gnu-libiconv=1.15-r2ENV LD_PRELOAD /usr/lib/preloadable_libiconv.so 在使用alpine:3.13镜像时,可以执行以下命令解决: 12RUN apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.13/community/ gnu-libiconv=1.15-r3ENV LD_PRELOAD /usr/lib/preloadable_libiconv.so php 测试执行以下命令,可用来验证 icnov 是否正常工作: 1php -d error_reporting=22527 -d display_errors=1 -r 'var_dump(iconv("UTF-8", "UTF-8//IGNORE", "This is the Yuan symbol '\\''¥'\\''."));' 参考 Missing preloadable_libiconv.so file in gnu-libiconv 1.16-r0 Proper iconv 使用 7.4-alpine-v3.14-swoole 版本,使用 gnu-libiconv1.15r2 仍失败 底包不支持 iconv()","categories":[],"tags":[]},{"title":"Linux性能优化","slug":"Linux性能优化","date":"2022-09-02T01:54:19.000Z","updated":"2024-10-04T01:14:34.446Z","comments":true,"path":"2022/09/02/Linux性能优化/","link":"","permalink":"https://g-ydg.github.io/2022/09/02/Linux%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/","excerpt":"","text":"CPU进程和 CPU 原理 进程与线程 CPU 调度 中断系统 CPU 缓存 NUMA 性能指标 平均负载 CPU 使用率 用户 CPU 系统 CPU IOWAIT 软中断 硬中断 窃取 CPU 客户 CPU 上下文切换 自愿上下文切换 非自愿上下文切换 CPU 缓存命中率 性能剖析 top/ps vmstat mpstat sar pidstat strace perf execsnoop proc 文件系统 调优方法 CPU 绑定 进程 CPU 资源限制 进程优先级调整 中断负载均衡 CPU 缓存 NUMA 优化 内存内存原理 地址空间 虚拟内存 内存分配与回收 缓存与缓冲区 SWAP 性能指标 系统内存使用量 进程内存使用量 缓存与缓冲区命中率 SWAP 使用量 性能剖析 free top sar vmstat cachestat cachetop memleak proc 文件系统 调优方法 利用缓存与缓冲区 减少 SWAP 使用 减少动态内存分配 优化 NUMA 限制进程内存资源 使用 HugePage 网络网络原理 网络配置 TCP/IP 协议 网络收发流程 高级路由 网络 QoS 网络防火墙 C10K 与 C100K 性能指标 吞吐量 BPS QPS PPS 延迟 丢包 TCP 重传 性能剖析 ethtool sar ping netstat/ss ifstat ifconfig tcpdump wireshark iptables traceroute ipcontrack perf 调优方法 网卡调优 MTU 队列长度 链路聚合 协议调优 HTTP TCP Overlay 资源控制 QoS 内核调优 NAT 调优 功能卸载 负载均衡 DPDK 磁盘 IO磁盘原理 磁盘管理 磁盘类型 磁盘接口 磁盘 I/O 栈 性能指标 使用率 IOPS 吞吐量 IOWAIT 性能剖析 dstat sar iostat pidstat iotop iolatency blktrace fio perf 调优方法 系统调用 I/O 资源控制 充分利用缓存 RAID I/O 隔离 文件系统文件系统原理 虚拟文件系统 文件系统 I/O 栈 文件系统缓存 文件系统种类 性能指标 容量 IOPS 缓存命中率 性能剖析 df strace vmstat sar perf proc 文件系统 调优方法 文件系统选型 利用文件系统缓存 I/O 隔离 Linux 内核内核原理 内核态 性能剖析 BPF perf proc 文件系统 内核调优 内核选项 应用程序性能指标 吞吐量 响应时间 资源使用率 性能剖析 USE 方法 使用率 饱和度 错误 进程剖析 进程状态 资源使用率 I/O 剖析 系统调用 热点函数 动态追踪 APM 调优方法 逻辑简化 编程语言 算法调优 非阻塞 I/O 利用缓存与缓冲区 异步处理与并发 垃圾回收 架构设计空间换时间 缓存 缓冲区 冗余数据 时间换空间 压缩编码 页面交换 并行处理 多线程 多进程 分布式 异步处理 异步 I/O 消息队列 事件通知 性能监控时间序列分析 历史趋势分析 性能模型构建 未来趋势预测 服务调用追踪 服务调用流程追踪 服务调用性能分析 服务调用链拓扑展示 数据可视化 趋势图 散点图 热图 饼图 告警通知 阈值选择 报警策略 通知渠道 性能测试明确需求 系统资源需求 应用程序需求 环境假设 合理的假设 生产环境模拟 生产负载模拟 性能测试 基准测试 负载测试 压力测试 结果分析 应用程序瓶颈 数据库瓶颈 系统资源瓶颈","categories":[],"tags":[]},{"title":"Linux 性能工具图谱","slug":"Linux 性能工具图谱","date":"2022-09-02T01:50:39.000Z","updated":"2024-10-04T01:14:36.654Z","comments":true,"path":"2022/09/02/Linux 性能工具图谱/","link":"","permalink":"https://g-ydg.github.io/2022/09/02/Linux%20%E6%80%A7%E8%83%BD%E5%B7%A5%E5%85%B7%E5%9B%BE%E8%B0%B1/","excerpt":"","text":"","categories":[],"tags":[]},{"title":"ACME.SH基于NGINX申请证书","slug":"ACME.SH基于NGINX申请证书","date":"2022-08-21T21:59:31.000Z","updated":"2024-10-04T01:14:36.661Z","comments":true,"path":"2022/08/21/ACME.SH基于NGINX申请证书/","link":"","permalink":"https://g-ydg.github.io/2022/08/21/ACME.SH%E5%9F%BA%E4%BA%8ENGINX%E7%94%B3%E8%AF%B7%E8%AF%81%E4%B9%A6/","excerpt":"","text":"下载安装1curl https://get.acme.sh | sh -s [email protected] or 1wget -O - https://get.acme.sh | sh -s [email protected] 设置别名1alias acme.sh=~/.acme.sh/acme.sh 查看帮助1acme.sh -h 设置软链1ln -s /usr/local/nginx/sbin/nginx /usr/local/sbin/nginx 申请证书1acme.sh --issue -d example.com --nginx /usr/local/nginx/conf/conf.d/example.com 安装证书1234acme.sh --install-cert -d example.com \\--key-file /path/to/keyfile/in/nginx/key.pem \\--fullchain-file /path/to/fullchain/nginx/cert.pem \\--reloadcmd "service nginx force-reload"","categories":[],"tags":[]},{"title":"在EasySwoole中实现Parallel","slug":"在EasySwoole中实现Parallel","date":"2022-07-19T23:46:26.000Z","updated":"2024-10-04T01:14:37.352Z","comments":true,"path":"2022/07/19/在EasySwoole中实现Parallel/","link":"","permalink":"https://g-ydg.github.io/2022/07/19/%E5%9C%A8EasySwoole%E4%B8%AD%E5%AE%9E%E7%8E%B0Parallel/","excerpt":"","text":"代码实现示例123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101<?phpnamespace App\\Utils;use App\\Utils\\Exception\\ParallelExecutionException;use Swoole\\Coroutine;use Swoole\\Coroutine\\Channel;use Swoole\\Coroutine\\WaitGroup;class Parallel{ /** * @var callable[] */ private $callbacks = []; /** * @var null|Channel */ private $concurrentChannel; /** * @param int $concurrent if $concurrent is equal to 0, that means unlimit */ public function __construct(int $concurrent = 0) { if ($concurrent > 0) { $this->concurrentChannel = new Channel($concurrent); } } public function add(callable $callable, $key = null) { if (is_null($key)) { $this->callbacks[] = $callable; } else { $this->callbacks[$key] = $callable; } } public function wait(bool $throw = true): array { $result = $throwables = []; $wg = new WaitGroup(); $wg->add(count($this->callbacks)); foreach ($this->callbacks as $key => $callback) { $this->concurrentChannel && $this->concurrentChannel->push(true); $result[$key] = null; Coroutine::create(function () use ($callback, $key, $wg, &$result, &$throwables) { try { $result[$key] = call($callback); } catch (\\Throwable $throwable) { $throwables[$key] = $throwable; unset($result[$key]); } finally { $this->concurrentChannel && $this->concurrentChannel->pop(); $wg->done(); } }); } $wg->wait(); if ($throw && ($throwableCount = count($throwables)) > 0) { $message = 'Detecting ' . $throwableCount . ' throwable occurred during parallel execution:' . PHP_EOL . $this->formatThrowables($throwables); $executionException = new ParallelExecutionException($message); $executionException->setResults($result); $executionException->setThrowables($throwables); throw $executionException; } return $result; } public function count(): int { return count($this->callbacks); } public function clear(): void { $this->callbacks = []; } /** * Format throwables into a nice list. * * @param \\Throwable[] $throwables */ private function formatThrowables(array $throwables): string { $output = ''; foreach ($throwables as $key => $value) { $output .= \\sprintf( '(%s) %s: %s' . PHP_EOL . '%s' . PHP_EOL, $key, get_class($value), $value->getMessage(), $value->getTraceAsString() ); } return $output; }} 123456789101112131415161718192021222324252627282930313233343536<?phpnamespace App\\Utils\\Exception;class ParallelExecutionException extends \\RuntimeException{ /** * @var array */ private $results; /** * @var array */ private $throwables; public function getResults() { return $this->results; } public function setResults(array $results) { $this->results = $results; } public function getThrowables() { return $this->throwables; } public function setThrowables(array $throwables) { return $this->throwables = $throwables; }} 代码使用示例1234567891011121314151617181920var_dump("start_time:".time());$parallel = new Parallel();$parallel->add(function () {Coroutine::sleep(1);return 1;});$parallel->add(function () {Coroutine::sleep(1);return 2;});$parallel->add(function () {Coroutine::sleep(1);return 3;});$data = $parallel->wait();var_dump("end_time:".time()); 执行结果:","categories":[],"tags":[]},{"title":"JetBrains IDE 激活","slug":"JetBrains IDE 激活","date":"2022-05-08T17:45:48.000Z","updated":"2024-10-04T01:14:38.180Z","comments":true,"path":"2022/05/08/JetBrains IDE 激活/","link":"","permalink":"https://g-ydg.github.io/2022/05/08/JetBrains%20IDE%20%E6%BF%80%E6%B4%BB/","excerpt":"","text":"下载 ja-netfilter下载 ja-netfilter.zip 并进行解压,解压完成之后根据项目中的 readme.txt 进行操作即可。 https://jetbra.in/5d84466e31722979266057664941a71893322460 激活 add -javaagent:/path/to/ja-netfilter.jar=jetbrains to your vmoptions (manual or auto) for Java 17 you have to add at least these JVM Options 12--add-opens=java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED--add-opens=java.base/jdk.internal.org.objectweb.asm.tree=ALL-UNNAMED log out of the jb account in the ‘Licenses’ window use key on page https://jetbra.in/5d84466e31722979266057664941a71893322460 plugin ‘mymap’ has been deprecated since version 2022.1 don’t care about the activation time, it is a fallback license and will not expire 参考链接 介绍一个”牛逼闪闪”开源库:ja-netfilter 关于 ja-netfilter 适配 Java17 的问题","categories":[],"tags":[]},{"title":"使用ACME.SH管理HTTPS证书","slug":"使用ACME.SH管理HTTPS证书","date":"2022-03-23T22:35:17.000Z","updated":"2024-10-04T01:14:38.645Z","comments":true,"path":"2022/03/23/使用ACME.SH管理HTTPS证书/","link":"","permalink":"https://g-ydg.github.io/2022/03/23/%E4%BD%BF%E7%94%A8ACME.SH%E7%AE%A1%E7%90%86HTTPS%E8%AF%81%E4%B9%A6/","excerpt":"","text":"安装1curl https://get.acme.sh | sh -s [email protected] 配置默认安装路径1~/.acme.sh/ 设置指令别名1alias acme.sh=~/.acme.sh/acme.sh 查看版本1acme.sh -v Crontab安装过程中会自动为创建 cronjob,用于每天自动检测证书,若证书快过期,需要更新,则会自动更新证书。 1crontab -l 生成证书acme.sh 实现了 acme 协议支持的所有验证协议。一般有两种方式验证: http 和 dns 验证。 HTTP 方式http 方式需要在你的网站根目录下放置一个文件,来验证你的域名所有权,完成验证。 1acme.sh --issue -d mydomain.com -d www.mydomain.com --webroot /home/wwwroot/mydomain.com/ 只需要指定域名,并指定域名所在的网站根目录。acme.sh 会全自动的生成验证文件, 并放到网站的根目录, 然后自动完成验证。 最后会聪明的删除验证文件,整个过程没有任何副作用。 apache 服务器如果你用的 apache 服务器,acme.sh 还可以智能的从 apache 的配置中自动完成验证, 你不需要指定网站根目录。 1acme.sh --issue -d mydomain.com --apache nginx 服务器如果你用的 nginx 服务器,acme.sh 还可以智能的从 nginx 的配置中自动完成验证,你不需要指定网站根目录。 1acme.sh --issue -d mydomain.com --nginx 注意,无论是 apache 还是 nginx 模式,acme.sh 在完成验证之后,会恢复到之前的状态,都不会私自更改你本身的配置。好处是你不用担心配置被搞坏, 也有一个缺点,你需要自己配置 ssl 的配置,否则只能成功生成证书,你的网站还是无法访问 https。 如果你还没有运行任何 web 服务,80 端口是空闲的,那么 acme.sh 还能假装自己是一个 webserver,临时听在 80 端口,完成验证。 1acme.sh --issue -d mydomain.com --standalone DNS 方式这种方式的好处是, 你不需要任何服务器, 不需要任何公网 ip, 只需要 dns 的解析记录即可完成验证. 坏处是,如果不同时配置 Automatic DNS API,使用这种方式 acme.sh 将无法自动更新证书,每次都需要手动再次重新解析验证域名所有权。 1acme.sh --issue --dns -d mydomain.com \\ --yes-I-know-dns-manual-mode-enough-go-ahead-please 然后,acme.sh 会生成相应的解析记录显示出来,你只需要在你的域名管理面板中添加这条 txt 记录即可。 等待解析完成之后, 重新生成证书: 12acme.sh --renew -d mydomain.com \\ --yes-I-know-dns-manual-mode-enough-go-ahead-please 注意第二次这里用的是 --renew dns 方式的真正强大之处在于可以使用域名解析商提供的 api 自动添加 txt 记录完成验证。 acme.sh 目前支持 cloudflare,dnspod,cloudxns,godaddy 以及 ovh 等数十种解析商的自动集成。 以 dnspod 为例,你需要先登录到 dnspod 账号,生成你的 api id 和 api key,都是免费的。然后: 12345export DP_Id="1234"export DP_Key="sADDsdasdgdsf"acme.sh --issue --dns dns_dp -d aa.com -d www.aa.com 证书就会自动生成了。 这里给出的 api id 和 api key 会被自动记录下来,将来你在使用 dnspod api 的时候,就不需要再次指定了。 直接生成就好了: 1acme.sh --issue -d mydomain2.com --dns dns_dp Copy/安装证书上面步骤生成完证书之后,接下来就需要把证书 copy 都真正需要用它的地方了。 默认生成的证书都放在安装目录下:~/.acme.sh/。注意,请不要直接使用此目录下的文件, 例如: 不要直接让 nginx/apache 的配置文件使用这下面的文件。这里面的文件都是内部使用,而且目录结构可能会变化。 正确的使用方法是使用 –install-cert 命令,指定到目标位置,然后证书文件会被 copy 到相应的位置。例如: Apache example12345acme.sh --install-cert -d example.com \\--cert-file /path/to/certfile/in/apache/cert.pem \\--key-file /path/to/keyfile/in/apache/key.pem \\--fullchain-file /path/to/fullchain/certfile/apache/fullchain.pem \\--reloadcmd "service apache2 force-reload" Nginx example1234acme.sh --install-cert -d example.com \\--key-file /path/to/keyfile/in/nginx/key.pem \\--fullchain-file /path/to/fullchain/nginx/cert.pem \\--reloadcmd "service nginx force-reload" Nginx 的配置 ssl_certificate 使用 /etc/nginx/ssl/fullchain.cer,而非 /etc/nginx/ssl/.cer ,否则 SSL Labs 的测试会报 Chain issues Incomplete 错误。 –install-cert命令可以携带很多参数,来指定目标文件。 并且可以指定 reloadcmd,当证书更新以后,reloadcmd 会被自动调用,让服务器生效。 相关内容 ACME.SH 仓库","categories":[],"tags":[]},{"title":"Linux /etc/profile配置错误如何处理","slug":"Linux !etc!profile配置错误如何处理","date":"2022-03-23T21:56:39.000Z","updated":"2024-10-04T01:14:38.651Z","comments":true,"path":"2022/03/23/Linux !etc!profile配置错误如何处理/","link":"","permalink":"https://g-ydg.github.io/2022/03/23/Linux%20!etc!profile%E9%85%8D%E7%BD%AE%E9%94%99%E8%AF%AF%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86/","excerpt":"","text":"前言修改/etc/profile 配置文件方法后,导致 bash 命令无法用。 1-bash: ls:command is not found 解决通过 vi 命令修改1/bin/vi /etc/profile 若存在备份,直接通过备份还原1/bin/cp /etc/profile.bak /etc/profile 执行生效1export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin 1source /etc/profile","categories":[],"tags":[]},{"title":"Node环境","slug":"Node环境","date":"2022-03-21T18:23:25.000Z","updated":"2024-10-04T01:14:38.663Z","comments":true,"path":"2022/03/21/Node环境/","link":"","permalink":"https://g-ydg.github.io/2022/03/21/Node%E7%8E%AF%E5%A2%83/","excerpt":"","text":"获取软件安装包上Node.js 官网选择你需要的 Node.js 安装包。 安装下载 1wget https://nodejs.org/dist/v10.9.0/node-v10.9.0-linux-x64.tar.xz 解压 1tar xf node-v10.9.0-linux-x64.tar.xz 进入解压目录 1cd node-v10.9.0-linux-x64/ 查看版本 1/bin/node -v 设置软链接12ln -s /root/node-v10.9.0-linux-x64/bin/npm /usr/local/bin/ln -s /root/node-v10.9.0-linux-x64/bin/node /usr/local/bin/","categories":[],"tags":[]},{"title":"JavaSDK安装","slug":"JavaSDK安装","date":"2022-03-21T18:22:21.000Z","updated":"2024-10-04T01:14:38.665Z","comments":true,"path":"2022/03/21/JavaSDK安装/","link":"","permalink":"https://g-ydg.github.io/2022/03/21/JavaSDK%E5%AE%89%E8%A3%85/","excerpt":"","text":"安装1yum install -y java-11-openjdk java-11-openjdk-devel --nogpgcheck 查看1java --version","categories":[],"tags":[]},{"title":"埋点以及事件模型","slug":"埋点以及事件模型","date":"2022-03-15T20:02:32.000Z","updated":"2024-10-04T01:14:40.535Z","comments":true,"path":"2022/03/15/埋点以及事件模型/","link":"","permalink":"https://g-ydg.github.io/2022/03/15/%E5%9F%8B%E7%82%B9%E4%BB%A5%E5%8F%8A%E4%BA%8B%E4%BB%B6%E6%A8%A1%E5%9E%8B/","excerpt":"","text":"什么是埋点埋点,是互联网获取数据的基础;数据采集系统,则是提升埋点效率、保障埋点规范与数据质量的利器。 埋点,在互联网里,可以说是再常见不过的技术了。大到 BAT,小到创业公司,如果没有埋点,那么基本丧失数据来源的大壁江山。这篇文章,简单介绍一下埋点的概述及数据采集系统。 埋点,指的是针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程。比如用户点击了某个按钮、浏览了某个页面等。 刚入行的小朋友可能会问:为啥要埋点呢?答:是为了获取数据,即获取某个用户在什么时间、什么位置、进行了什么操作。你仔细想想,如果不埋点的话,用户在前端页面点击了某个按钮,你怎么会知道用户点击了呢? 稍微有点技术背景的小朋友又会问:我点击某个按钮,网站不就会收到一次请求,我从后台不就知道了吗,那我干嘛要埋点呢?答:因为不是所有的操作后台都能收到请求的,很多网站页面为了用户使用的便捷性,都是一次请求加载了很多内容,其中的 tab 切换等根本就没有请求服务器,因此会漏掉数据。更别提 APP 端了,很多都是原生页面,页面切来切去的,根本就没有请求网络。 所以,是不是如果和服务器有请求的数据,就不用埋点了?哈哈,这里就引出了埋点的分类:前端埋点和后端埋点。 所谓前端埋点,就是上文提到的,在网站前端或者 APP 上埋入一段 JS 代码或者 SDK,每次用户触发特定的行为,就会收集这么一条日志,定期发送给服务器,这就完成了前端用户行为日志的采集。为啥叫“埋点”?就是因为是把一段段的采集代码埋入了各个目标位置,因此形象化地叫埋点。前端埋点工作量大,比如页面上有 20 个按钮,正常情况下,每个按钮都需要埋一下代码,有些网站有几千个页面,埋码能累死。 所谓后端埋点,其实就是天然地和服务器发生了请求、交互的数据类型,这种就不需要通过前端埋点,只要在服务端把用户每次的请求记录下来,就行了。例如用户在电商网站上发生的搜索行为,每次输入关键词并且搜索,一定是会请求后端的(不然没法有搜索结果),那这时只要从服务端把每次请求的内容、时间、人物等信息记录下来即可。工作量比前端埋码小很多。 当然,用朋友会问,那比如我在搜索页面输入了关键词但是没搜索,如果是后端埋点岂不是记录不下来了?你说的对,不过这种数据一般较少,没必要为了这点数据去做前端埋点,毕竟后端埋点的实施比前端还是容易的多。当然,具体情况具体分析,如果是真的精细化运营,用户哪怕一丁点的行为也要统计,但需要衡量性价比。 什么是事件事件模型,是数据埋点采集的基础。其本质是将用户的互联网行为标准化。 首先,什么是事件呢? 举个例子,用户在微信上添加了一个好友、给好友发了一条信息、打开朋友圈等等,都可以分别称为一次事件。 再说的直白点,事件就是用户在 APP 或网站上发生的某一类型的行为。至于事件具体是什么内容,则可以基于实际的分析用途来自由定义,这也是埋点设计的重要范畴。 为什么有了事件这个概念呢? 本质其实是出于分析的诉求。思考一下,互联网用户在网站或者应用上,其实操作的行为是连续的。比如你要在京东上买个手机,你可能会有如下的一系列操作: 打开 APP 搜索“手机”关键词 浏览商品 咨询客服 领优惠券 加购物车 下单 支付等等 以上列举的内容,都可以称为事件:【启动 APP 事件】、【搜索事件】……等等。这个过程其实是把用户在京东 APP 上的所有操作进行了人工切分、标准化,并将其中认为比较重要的环节进行了数据的采集。 事件模型的含义上面讲了事件的含义,那什么是事件模型呢? 事件模型其实就是将事件进行了标准化的过程。 我们在做分析的时候,经常提到 5W1H,其实道理在事件模型这也是完全相似的。近似的讲,我们可以将事件模型看成如何标准化定义事件的模型。 举个例子。上一部分提到的【加购物车】这个事件,只能算是一个事件的类别,但具体到用户行为上,可以这样: Who:哪个用户(userID、设备码……)加购了? When:用户什么时间加购 Where:用户在什么地方(北京?成都)发生了加购行为 What:发生了什么?(这里就是发生了加购) How:用户通过什么设备完成的? 等等。这里只是列举了模型当中比较重要、通用的几部分。不同的事件类型,在做事件模型的设计的时候,完全可以是不一样的,这个根据具体的业务情况来灵活处理。 事件类型都有哪些通常来讲,埋点的事件类型,抽象出来,可以主要分为以下三类,这三类事件是各个互联网站点、应用比较通用的,用来做标准化埋点比较合适。 但是针对不同的业务类型,往往有其他特殊的事件类型(比如视频网站的数据采集,要采集视频播放相关的事件;直播网站需要采集连麦、直播相关的事件),这个就针对具体情况具体分析。 后续有机会会分享一下不同行业的事件类型设计案例。 (1)浏览事件 浏览事件是用户在访问网站页面时,页面在被浏览器加载呈现采集的事件。 通俗的讲,浏览事件就是打开某个网站页面、某个 APP 页面的事件。 (2)点击事件 点击事件是当页面加载和渲染后,用户与网站页面可以进行点击等交互操作时采集的内容。 通俗的讲,点击事件就是用户点击了页面中某个按钮、某个 tab 页面的事件。比如用户点击了【分享按钮】。 (3)曝光事件 曝光事件是在网页加载时一种用户虚拟点击的交互行为,如轮播图,商品、活动推荐等时采集的内容。 通俗的讲,曝光事件就是页面中的某个元素、某个区域发生了曝光(即展现在页面前端)的事件。 有同学经常对曝光事件和页面浏览事件区分不开,有时还称呼页面浏览事件为【页面曝光】。这个叫法说实在的也没啥问题,但个人建议不要这样称呼,容易有误导。通常的曝光事件,就是指的页面中的某个内容的暴露,属于页面的子集。 事件的属性其实第二部分在讲事件模型的时候,有提到一些相关属性相关的内容。比如 5W1H,其实就是属于事件的属性。属性,是用来更好描述完整事件的内容的。 针对不同的事件,事件的属性设计也不尽相同。我们通常将属性分为两部分: (1)预置属性 所谓预置属性,就是无论事件类型是啥,都需要有的事件属性。比如下图: (2)私域属性 私域属性,是针对该事件,进行的针对性的属性内容。如下图示例: 文章转载自:事件模型:如何将行为标准化?(数据埋点的基础)","categories":[],"tags":[]},{"title":"自动化:持续集成、交付、部署","slug":"自动化:持续集成、交付、部署","date":"2022-03-15T19:41:40.000Z","updated":"2024-10-04T01:14:44.056Z","comments":true,"path":"2022/03/15/自动化:持续集成、交付、部署/","link":"","permalink":"https://g-ydg.github.io/2022/03/15/%E8%87%AA%E5%8A%A8%E5%8C%96%EF%BC%9A%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90%E3%80%81%E4%BA%A4%E4%BB%98%E3%80%81%E9%83%A8%E7%BD%B2/","excerpt":"","text":"自动化:持续集成、交付、部署持续集成持续集成(Continuous integration),指的是**频繁地将代码集成到主干**。 持续集成主要目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。 优点提高开发效率持续集成可以把工程师从繁琐的任务中解放出来,提高工作效率。并且能有效减少发布版本中的错误和 Bug 数量。防止分支大幅偏离主干。如果不是经常集成,主干又在不断更新,会导致以后集成的难度变大,甚至难以集成。 快速发现并定位 BUG通过各种例行测试,您的团队可以在问题变严重前就发现并定位到程序的 Bug。减少由程序错误带来的损失。每完成一点更新,就集成到主干,可以快速发现错误,定位错误也比较容易。 更快速的发布更新持续集成可以帮助您的团队更快速、更积极的发布程序更新程序。在发布时可自动完成大量重复工作完成,节省人力。 Martin Fowler 说过,”持续集成并不能消除 Bug,而是让它们非常容易发现和改正“。 持续交付 持续交付(Continuous delivery),指的是频繁地将软件地新版本,交付给质量团队或者用户,以供评审。如果评审通过,代码就进入生产阶段。它强调的是,不管怎么更新,软件是随时随地可以交付的。 持续交付在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境的「Staging 环境」,比如,我们完成单元测试后,可以把代码部署到连接数据库的 Staging 环境中更多的测试。如果代码没有问题,可以继续部署到生产环境中。 持续部署 持续部署(continuous deployment)是持续交付的下一步,指的是代码通过评审以后,自动部署到生产环境。 目的代码在任何时刻都是可部署的,可以进入生产阶段。 与持续交付的区别持续部署的前提是能自动化完成测试、构建、部署等步骤。它与持续交付的区别,可以参考下图。 通过上图,我们可以看到,持续交付是手动部署到生产环境的,而持续部署是自动部署到生产环境。","categories":[],"tags":[]},{"title":"CODING-持续部署","slug":"CODING-持续部署","date":"2022-03-09T03:28:21.000Z","updated":"2024-10-04T01:15:16.028Z","comments":true,"path":"2022/03/09/CODING-持续部署/","link":"","permalink":"https://g-ydg.github.io/2022/03/09/CODING-%E6%8C%81%E7%BB%AD%E9%83%A8%E7%BD%B2/","excerpt":"","text":"持续部署持续部署(continuous deployment)是持续交付的下一步,指的是代码通过评审以后,自动部署到生产环境。 CONDIG 中的持续部署持续部署指在软件开发过程中,以自动化方式,频繁而且持续性的将软件部署到生产环境,使软件产品能够快速的交付使用。作为持续集成的延伸,持续部署以 CODING 上下游产品优势为根基,是实现 DevOps 闭环的核心流程,实现全流程管控。 CODING 持续部署用于把控构建之后的项目发布与部署交付流程。能够无缝对接上游 Git 仓库、下游制品仓库以实现全自动化部署。同时还支持 Webhook 等外部对接能力,高效集成各种开发、运维工具。在稳定的技术架构、运维工具等基础上,具备蓝绿发布,灰度发布(金丝雀发布),滚动发布,快速回滚等能力。 CODING 提供三种持续部署方式:基于 Kubernetes 的持续部署、基于腾讯云弹性伸缩的持续部署、基于主机的持续部署。 主机部署主机部署提供了基于物理机/虚拟机 的通用部署能力,支持将应用部署到公有云、混合云、私有云/私有环境中。主机部署是早期软件开发里常见、常用的部署方式,但随着 Docker、Kubernetes 的兴起,针对此类部署方式的支持也愈加稀少。因此一套整合了代码、制品、部署的整套流程更是屈指可数。如今 CODING CD 推出的主机组部署功能,正好能够弥补上这个缺口,让这种看似“原始”的方式也能加入进 DevOps 环中。 相关概念 堡垒机:堡垒机是 CODING 持续部署服务与主机之间的代理,CODING CD 通过堡垒机上运行的 Agent 服务管控应用发布过程。 主机组:主机组是主机实例的集合,通常一个主机组对应应用的一个发布集群(测试集群、预发集群、生产集群) 服务:CODING 持续部署抽象的概念。在部署(主机组)阶段需要定义本阶段部署的服务名称,CODING CD 基于此服务名实现版本管理和回滚。 架构图 快速上手点击立即配置 选择添加堡垒机,复制部署命令。 进入堡垒机并执行命令。 执行完成后,返回 coding 查看是否成功。 添加主机组。 点击主机组实例,查看主机状态是否正常。 配置应用。 创建应用。 创建部署流程。 基础配置,在这可进行自动触发器、启动参数等配置。 示例:master 分支变更触发。 配置部署阶段,阶段之间可以配置依赖关系、脚本运行、预置条件检查等。 以主机部署为例。添加部署阶段,主机组部署 -> 部署(主机组)。 基础设置。 配置脚本。 配置健康检查。 配置执行选项。 保存部署流程。 将应用关联到具体项目中。 发布部署。 查看日志。 相关文档 自动化:持续集成、交付、部署 CODING 持续部署 CONDIG 主机部署 CODING CD 主机组部署实践","categories":[],"tags":[]},{"title":"CODING 持续集成","slug":"CODING 持续集成","date":"2022-03-04T00:16:44.000Z","updated":"2024-10-04T01:15:32.565Z","comments":true,"path":"2022/03/04/CODING 持续集成/","link":"","permalink":"https://g-ydg.github.io/2022/03/04/CODING%20%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/","excerpt":"","text":"持续集成持续集成(Continuous integration),指的是**频繁地将代码集成到主干**。 持续集成主要目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。 JenkinsJenkins 是一款开源 CI&CD 软件,用于自动化各种任务,包括构建、测试和部署软件。Jenkins 支持各种运行方式,可通过系统包、Docker 或者通过一个独立的 Java 程序。 流水线流水线,指的是按顺序连接在一起的事件或作业组,可以通过脚本,把项目的构建、测试、部署等阶段组合起来执行。 Jenkins PipelineJenkins Pipeline(或简称为 “Pipeline”)是一套插件,将持续交付的实现和实施集成到 Jenkins 中。它提供了一套可扩展的工具,用于将“简单到复杂”的交付流程实现为“持续交付即代码”。而 Jenkins Pipeline 的定义通常被写入到一个文本文件(称为 Jenkinsfile )中,该文件可以被放入项目的源代码控制库中。 Jenkinsfile JenkinsFile 可以通过两种语法来声明流水线结构,一种是声明式语法,另一种是脚本式语法。 声明式流水线声明式流水线是由一个包含了一些指令和部分的外套代码块组成的。每个部分又可以包含其他的部分、指令和步骤,在某些情况下也会包含条件。 Pipeline流水线(Pipeline)表示一个构建任务的总过程,包含所有阶段,如构建、测试、部署等。所有的 stage/阶段和 step/步骤都在这个块中定义。它是声明性流水线语法的关键块。 语法格式如下: 1234pipeline { ...} Agent指定自定义工作空间。通常定义在pipeline 的顶部位置或者在每个 stage 中。 参数 any:可以运行在任意节点上。 1234pipeline { agent any ...} none :不指定全局代理节点,如有必要,需要为单个阶段指定代理节点。 1234pipeline { agent none ...} label:在指定标签的节点上运行。 1234pipeline { agent {label 'my-defined-label'} ...} docker:使用指定的容器执行流水线或阶段 12345678910pipeline { agent { docker { image 'hyperf/hyperf:7.4-alpine-v3.11-swoole' reuseNode 'true' registryCredentialsId '7ca3f680-1791-4f91-a9bc-d106fa661480' } } ...} dockerfile:执行流水线或阶段, 使用从源代码库包含的 Dockerfile 构建的容器 123456789pipeline { agent { dockerfile { filename 'Dockerfile' dir 'docker' } } ...} Stage流水线中可以包含多个 stage/阶段,一个 stage/阶段执行一个特定任务,例如测试、部署等,每个 stage/阶段可以包含多个步骤。 1234567891011121314151617pipeline { ... stages { stage ('Build') { ... } stage ('Test') { ... } stage ('Deploy') { ... } } ...} Step一个 Step/步骤是指某个阶段中的单个任务。可以在一个阶段块中定义一系列步骤,这些步骤依次执行。 12345678910111213141516171819202122pipeline { ... stages { stage('Parallel Stage') { parallel { stage('Stage A') { steps { ... } } stage('Stage B') { steps { ... } } } } } ...} Parallel并行执行流水线的多个阶段。 注意,一个阶段必须只有一个 steps 或 parallel 的阶段。 嵌套阶段本身不能包含进一步的 parallel 阶段, 但是其他的阶段的行为与任何其他 stage 相同。任何包含 parallel 的阶段不能包含 agent 或 tools 阶段, 因为他们没有相关 steps。 1234567891011pipeline { ... stages { stage ('Build') { steps { echo 'Running build phase...' } } }} Environment定义环境变量,在整个流水线/阶段范围内可见。该指令支持一个特殊的助手方法 credentials(),该方法可用于在 Jenkins 环境中通过标识符访问预定义的凭证。 123456789101112131415161718pipeline { ... environment { USER_NAME = 'admin' } stages { stage('Example') { environment { ACCESS_KEY = credentials('凭证ID或标识字符串') } steps { sh 'printenv' } } }} Script处理非声明式的代码,如赋值语句 def name=’fq’。 12345678910111213pipeline { ... stage('Example') { steps { script { def config = [:] config.name = "config-name" echo "The Config is ${config.name}" } } } } 脚本式流水线与声明式一样的是,都是建立在底层流水线的子系统上的。不同的是,脚本化流水线实际上是由 Groovy 构建的通用 DSL。 Groovy 语言提供的大部分功能都可以用于脚本化流水线的用户。这意味着它是一个非常有表现力和灵活的工具,可以通过它编写持续交付流水线。 Node节点是执行整个工作流的机器。它是脚本化管道语法的关键部分。 语法格式如下: 12345678910// Jenkinsfile (Scripted Pipeline)node { stage('Example') { if (env.BRANCH_NAME == 'master') { echo 'I only execute on the master branch' } else { echo 'I execute elsewhere' } }} 两者区别比较 脚本化流水线为 Jenkins 用户提供了大量的灵活性和可扩展性。但 Groovy 学习曲线通常不适合给定团队的所有成员,因此创造了声明式流水线来为编写 Jenkins 流水线提供一种更简单、更有主见的语法。 两者本质上是相同的流水线子系统。都是 “持续交付即代码“ 的持久实现。它们都能够使用构建到流水线中或插件提供的步骤。 主要区别在于语法和灵活性。 声明式限制了用户使用更严格和预定义的结构, 使其成为更简单的持续交付流水线的理想选择。 脚本化提供了很少的限制, 以至于对脚本和语法的唯一限制往往是由 Groovy 子集本身定义的,而不是任何特定于流水线的系统,这使他成为权利用户和那些有更复杂需求的人的理想选择。 流水线步骤(Pipeline Step) Basic Steps:为流水线提供常用步骤指令。常用指令有 echo、writeFile 等。 Nodes and Processes:为流水线提供节点以及进程管理的步骤指令。常用指令有 bat、powershell、sh 等。 SSH Pipeline Steps:为流水线提供 SSH 工具管理指令,用于命令执行或文件传输。 sshCommand:在远端机器执行指定命令 sshGet:从远端机器获取文件/目录到当前工作空间 sshPut:将当前工作空间的文件/目录放置到远端机器 sshRemove:将远端机器的某个文件/目录移除 sshScript:读取本地 shell 脚本,在远端机器执行 SCM Step:为流水线提供检查源代码指令。 checkout:从版本控制工具中检出代码。 CODING 中的持续集成官方介绍,当提交了一部分修改完成的代码后,我们总是希望可以快速得到直观且有效的反馈,及早暴露问题。在开发过程中总有一部分工作是相对机械化,易出错的(例如打包、部署)。CODING 持续集成便是专门为此工作流而设计的得力功能。通过对每次提交的代码进行自动化的代码检查、单元测试、编译构建、甚至自动部署与发布,能够大大降低开发人员的工作负担,减少许多不必要的重复劳动,持续提升代码质量与开发效率。 凭据管理打开在项目管理下方的项目设置。 选择开发者选项下的凭据管理,可自行进行管理。 快速上手创建代码仓库 选择导入外部仓库,仓库地址:https://github.com/G-YDG/hyperf-demo.git。 点击完成创建,等待项目导入。 导入完成后,进行创建构建计划。 选择自定义构建过程 填写构建计划名称以及选择代码仓库。 创建之后,选择流程配置。可以看到有两种编辑器,图形化编辑器以及文本编辑器。选择文本编辑器,并将以下代码粘贴进去。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697pipeline { agent any stages { stage('检出') { steps { checkout([ $class: 'GitSCM', branches: [[name: GIT_BUILD_REF]], userRemoteConfigs: [[ url: GIT_REPO_URL, credentialsId: CREDENTIALS_ID ]]]) } } stage('代码检查') { parallel { stage('自动化测试') { agent { docker { image 'hyperf/hyperf:7.4-alpine-v3.11-swoole' reuseNode 'true' registryCredentialsId '7ca3f680-1791-4f91-a9bc-d106fa661480' } } steps { echo '自动化测试中...' sh 'composer install && composer test' echo '自动化测试结束...' } } stage('代码风格检查') { agent { docker { image 'hyperf/hyperf:7.4-alpine-v3.11-swoole' reuseNode 'true' } } steps { echo '代码风格检查中...' sh 'composer install && composer cs-fix' echo '代码风格检查结束...' } } } } stage('远程部署') { steps { echo '远程部署开始...' script { def remoteConfig = [:] remoteConfig.name = "remote-server" remoteConfig.host = "${REMOTE_HOST}" remoteConfig.port = "${REMOTE_SSH_PORT}".toInteger() remoteConfig.allowAnyHosts = true node { // 使用当前项目下的凭据管理中的 SSH 私钥 凭据 withCredentials([sshUserPrivateKey( credentialsId: "${REMOTE_CRED}", keyFileVariable: "privateKeyFilePath" )]) { // SSH 登陆用户名 remoteConfig.user = "${REMOTE_USER_NAME}" // SSH 私钥文件地址 remoteConfig.identityFile = privateKeyFilePath // 更新代码 sshCommand( remote: remoteConfig, command: """ cd ${DATA_PATH} git pull """, sudo: false, ) // 启动服务 sshCommand( remote: remoteConfig, command: """ cd /data/hyperf-demo docker-compose up --build --always-recreate-deps -d """, sudo: false, ) } } } echo '远程部署结束...' } } } } 配置流程环境变量。 1234REMOTE_HOST:远程服务器地址REMOTE_USER_NAME:远程连接用户名REMOTE_CRED:Coding 凭据DATA_PATH:项目工作路径 配置触发规则,保存修改。 配置完成后,点击保存修改并立即构建。 查看构建过程记录。 相关文档 自动化:持续集成、交付、部署 Jenkins 流水线语法 Jenkins 流水线步骤 CODING 官网 CODING-持续集成 CODING-流程配置详情 [](https://help.coding.net/docs/test-management/start.html)","categories":[],"tags":[]},{"title":"阿里云CentOS-yum源问题","slug":"阿里云CentOS-yum源问题","date":"2022-03-02T00:36:16.000Z","updated":"2024-10-04T01:15:32.573Z","comments":true,"path":"2022/03/02/阿里云CentOS-yum源问题/","link":"","permalink":"https://g-ydg.github.io/2022/03/02/%E9%98%BF%E9%87%8C%E4%BA%91CentOS-yum%E6%BA%90%E9%97%AE%E9%A2%98/","excerpt":"","text":"备份1cd /etc/yum.repos.d/ 1mkdir /home/yum.repos.d 1cp * /home/yum.repos.d/ 1ll /home/yum.repos.d/ 清空yum 源配置文件1rm -rf /etc/yum.repos.d/* 下载新的阿里云镜像源1wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.rep 生成缓存1yum makecache","categories":[],"tags":[]},{"title":"nginx大量400请求","slug":"nginx大量400请求","date":"2022-02-23T19:27:01.000Z","updated":"2024-10-04T01:15:39.341Z","comments":true,"path":"2022/02/23/nginx大量400请求/","link":"","permalink":"https://g-ydg.github.io/2022/02/23/nginx%E5%A4%A7%E9%87%8F400%E8%AF%B7%E6%B1%82/","excerpt":"","text":"前言在检查服务器日志时,发现 nginx 的 access_log 出现大量 400 错误的请求。 排查查看 access_log 日志,发现出现访问路径都为/hi,推断可能是负载相关的问题。 查看阿里云负载配置,发现健康状态异常,检查路径为/hi。 通过 postman 进行请求,发现响应正常。 查阅 阿里云-配置健康检查文档,关于 健康检查路径和健康检查域名 的说明,“如果没有配置域名,SLB 则不会在请求中附带 host 字段,因此健康检查请求就会被服务器拒绝,可能导致健康检查失败”。 在 postman 中去除 headers 中的 host 字段进行请求,发现响应 400,问题复现。 查看RFC 文档 得知,http1.1 的标准规定请求头部必须包含 host 信息,如果为空就直接返回 400。 解决根据以上问题 以及 阿里云-配置健康检查文档,对健康检查域名进行配置即可解决问题。","categories":[],"tags":[]},{"title":"Supervisor","slug":"Supervisor","date":"2022-02-17T22:12:15.000Z","updated":"2024-10-04T01:15:39.352Z","comments":true,"path":"2022/02/17/Supervisor/","link":"","permalink":"https://g-ydg.github.io/2022/02/17/Supervisor/","excerpt":"","text":"安装查询软件包1yum info supervisor 安装扩展源 EPEL(若无软件包,有则跳过)1yum -y install epel-release 安装1yum install -y supervisor 启动服务1systemctl start supervisord 开机自启1systemctl enable supervisord 查看启动状态1systemctl status supervisord 查看进程1ps -aux | grep supervisord 命令supervisor 是一个 C/S 模型的程序,supervisord 是 server 端,supervisorctl 是 client 端。 supervisord 123456-c, --configuration 指定配置文件路径 (默认为/etc/supervisord.conf)-i, --interactive 执行命令后启动交互式shell-s, --serverurl URL upervisord服务器监听的URL(默认为“ http:// localhost:9001 ”)-u, --username 用于与服务器进行身份验证的用户名-p, --password 用于与服务器进行身份验证的密码-r, --history-file 保留readline历史记录(如果readline可用) supervisorctl 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384add <name> [...]激活进程/组的配置中的任何更新删除<name> [...]remove <name> [...]从活动配置中删除进程/组update重新加载配置,然后根据需要添加和删除(重新启动程序)clear <name>清除进程的日志文件。clear <name> <name>清除多个进程的日志文件clear all清除所有进程的日志文件fg <process>进入supervisor前台模式, 按Ctrl + C退出PID获得supervisord的PID。pid <name>按名称获取单个子进程的PID。pid all获取每个子进程的PID,每行一个。reread重新加载守护程序的配置文件,无需添加/删除(无重启)注意:restart不会重新读取配置文件。可以用reread和updaterestart <name>重新启动进程restart <gname>:*重新启动组中的所有进程restart <name> <name>重新启动多个进程或组restart all重新启动所有进程start <name>开启一个进程start <gname>:*启动组中的所有进程start <name> <name>启动多个进程或组start all开始所有进程status获取所有进程状态信息。status <name>按名称获取单个进程的状态。status <name> <name>获取多个命名进程的状态。stop <name>停止一个进程stop <gname>:*停止组中的所有进程stop <gname> <gname>停止多个进程或组stop all停止所有进程tail [-f] <name> [stdout | stderr](默认stdout)输出进程日志, Ctrl-C的退出。tail -100 <name> 是输出stdout的最后100 个字节 <name> stderr 是输出stderr的最后1600 个字节","categories":[],"tags":[]},{"title":"LNMP环境搭建","slug":"LNMP环境搭建","date":"2022-02-13T19:32:33.000Z","updated":"2024-10-04T01:15:44.264Z","comments":true,"path":"2022/02/13/LNMP环境搭建/","link":"","permalink":"https://g-ydg.github.io/2022/02/13/LNMP%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/","excerpt":"","text":"安装 MySQL 数据库下载并安装 MySQL 官方的 Yum Repository123wget http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm &&yum -y install mysql57-community-release-el7-10.noarch.rpm &&yum -y install mysql-community-server 若出现 Error: Unable to find a match: mysql-community-server,执行以下 1yum module disable mysql -y 若 install 时出现,Error: GPG check FAILED,在命令后加上 **–nogpgcheck 选项** 1yum -y install mysql-community-server --nogpgcheck 启动 MySQL 数据库1systemctl start mysqld.service 查看 MySQL 运行状态1systemctl status mysqld.service 查看 MySQL 初始密码1grep "password" /var/log/mysqld.log 登录数据库1mysql -uroot -p 修改密码安全策略(可跳过)validate_password_policy = 0,代表密码安全策略为低,只校验密码长度,至少 8 位。 1set global validate_password_policy=0; 修改 MySQL 默认密码1ALTER USER 'root'@'localhost' IDENTIFIED BY '12345678'; 授予 root 用户远程管理权限1GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '12345678'; 输入 exit 退出数据库安装 Nginx 服务安装 Nginx 运行所需要的插件1yum install vim gcc pcre pcre-devel zlib-devel -y gcc 是 Linux 下的编译器,它可以编译 C、C++、Ada、Object C 和 Java 等语言。 pcre 是一个 perl 库,Nginx 的 HTTP 模块使用 pcre 来解析正则表达式。 zlib 是一个文件压缩和解压缩的库,Nginx 使用 zlib 对 HTTP 数据包进行 gzip 压缩和解压。 下载 Nginx 安装包1wget http://nginx.org/download/nginx-1.17.10.tar.gz 解压 Nginx 安装包1tar -zxvf nginx-1.17.10.tar.gz 编译安装 Nginx1cd nginx-1.17.10 && ./configure && make && make install 启动 Nginx1cd /usr/local/nginx/ && sbin/nginx 测试 Nginx 启动在浏览器地址栏输入 IP,例如 123.123.123.123,出现如下界面表示安装启动成功。 安装 PHP 环境安装 PHP1yum -y install php php-fpm php-mysqlnd 在 nginx.conf 文件中增加对 PHP 的支持进入 Vim 编辑器后,按下 i 键进入编辑模式。 1vim /usr/local/nginx/conf/nginx.conf 在 server 的根路由配置中新增 index.php。 1234location / { root html; index index.html index.htm index.php;} 在根路由下面新增以下配置。 123456789if (!-e $request_filename) { rewrite ^/(.*)$ /index.php/$1 last;}location ~ .*\\.php(\\/.*)*$ { fastcgi_pass 127.0.0.1:9000; include fastcgi.conf; fastcgi_index index.php;} 修改后的 nginx.conf 文件如下图所示。 按下 ESC 键,输入:wq 保存并退出 Vim 编辑器。 重启 php-fpm 服务1systemctl restart php-fpm 重启 Nginx 服务1/usr/local/nginx/sbin/nginx -s reload 检查 PHP 安装 在 Nginx 的网站根目录下创建 PHP 探针文件 phpinfo.php。 1echo "<?php phpinfo(); ?>" > /usr/local/nginx/html/phpinfo.php 访问 PHP 探针页面。在浏览器地址栏输入 xx.xx.xx.xx/phpinfo.php(请将 xx.xx.xx.xx 替换为对应 IP 地址),出现如下页面表示 PHP 环境配置成功。","categories":[],"tags":[]},{"title":"Serverless 极速部署线上商城","slug":"Serverless 极速部署线上商城","date":"2022-01-06T21:33:15.000Z","updated":"2024-10-04T01:16:33.940Z","comments":true,"path":"2022/01/06/Serverless 极速部署线上商城/","link":"","permalink":"https://g-ydg.github.io/2022/01/06/Serverless%20%E6%9E%81%E9%80%9F%E9%83%A8%E7%BD%B2%E7%BA%BF%E4%B8%8A%E5%95%86%E5%9F%8E/","excerpt":"","text":"https://developer.aliyun.com/adc/scenario/exp/4bc3fb53a8f9474ca481cdc548978ea4 1. 创建资源通过体验中心界面点击创建资源,等待资源创建完成。 2. 准备测试程序的 Jar 包资源1、在远程桌面,双击打开 Firefox ESR 浏览器。 2、复制如下链接,下载测试程序 Jar 包资源: https://labfileapp.oss-cn-hangzhou.aliyuncs.com/cartservice-provider-1.0.0-SNAPSHOT.jar https://labfileapp.oss-cn-hangzhou.aliyuncs.com/frontend-1.0.0-SNAPSHOT.jar https://labfileapp.oss-cn-hangzhou.aliyuncs.com/productservice-provider-1.0.0-SNAPSHOT.jar 3、然后将浏览器下载好的三个 Jar 包拖动到桌面,方便部署时查找。 3. 进入 SAE 控制台1、打开虚拟桌面的 FireFox ESR 浏览器,在 RAM 用户登录页面,输入云产品资源列表中的子用户名称,然后单击下一步。 2、在用户密码页面,输入云产品资源列表中的子用户密码,然后单击登录。 3、在浏览器中打开新页签,访问如下地址,进入 SAE 控制台。 https://sae.console.aliyun.com/ 说明: 由于地域的问题,可能出现授权的问题,您可以直接关闭错误提示框,按如下操作切换所在地域。 4、点击控制台中的应用列表,并确保自己的所在区域为“上海”,可以发现此时已经有了三个测试应用分别是:cartservice、frontend、productservice。 5、需要注意的是,此时这三个 SAE 应用只是 DEMO 应用(为了让大家可以手动体验 SAE 的部署功能),因此,我们接下来需要将真正的微服务应用部署到这三个应用当中去。 4. 部署微服务应用1、配置创建完毕后,回到应用列表,点击与云资源面板上一致的命名空间名称——进入 cartservice 应用,点击部署应用。 2、点击删除原 Jar 包,然后上传之前所准备的 Jar 包(cartservice-provider-1.0.0-SNAPSHOT.jar)完成程序包的替换。同时,点击一下“使用时间戳为版本号”更新一下版本信息。上述操作完成后,滑到最后,点击确认。 3、此时应用进入变更状态,等待片刻后,应用完成部署。 同理,完成剩下两个应用的部署(frontend -> frontend-1.0.0-SNAPSHOT.jar; productservice-> productservice-provider-1.0.0-SNAPSHOT.jar) 5. 配置 SLB,提供外网访问1、回到应用列表页面,点击与云资源面板上一致的命名空间名称——打开 frontend 应用。 2、在 frontend 应用的基本信息页面,在应用访问设置区域的公网访问地址中点击编辑公网 SLB 访问。 3、在编辑公网 SLB 访问的对话框的 TCP 协议中,单击操作列下的删除图标。 4、在编辑公网 SLB 访问对话框中,单击 HTTP 协议,在 HTTP 端口输入 80,在容器端口输入 9999,单击确定。 5、等待片刻,SLB 端口变更完成后,即可在浏览器输入相应的 IP 进行访问。如果访问时出现以下界面,则表示三个微服务应用部署成功了。 6. 配置弹性规则1、接下来,我们将测试 SAE 的弹性功能。回到 SAE 控制台的应用列表,点击与云资源面板上一致的命名空间名称——打开 pruductservice 应用,再点击自动扩缩。 2、在弹性配置界面,选择监控指标弹性,然后完成参数的填入,最后单击确认,完成弹性的设置。 此时,我们得到一条弹性策略,我们需要再点击启用。 7. 触发压测,观察弹性功能回到我们之前的那个 IP 访问的界面,输入压测触发路径,填入总请求数与最大线程并发数。/product/buy/{requestTotal}/{threadNum}; 可参考如下示例: 1/product/buy/400/5 在浏览器地址栏填入上述参数后,键盘敲下回车确认访问。 此时我们便可以在 frontend 这个应用的实时日志中,看到压测程序已经触发。 并且,可以在 productservice 应用中,看到新实例被弹出。同时,我们打开应用监控也可以看到应用处理请求的情况。","categories":[],"tags":[]},{"title":"Windows如何使用make命令","slug":"Windows如何使用make命令","date":"2021-12-11T17:33:45.000Z","updated":"2024-10-04T01:16:37.580Z","comments":true,"path":"2021/12/11/Windows如何使用make命令/","link":"","permalink":"https://g-ydg.github.io/2021/12/11/Windows%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8make%E5%91%BD%E4%BB%A4/","excerpt":"","text":"安装 MinGW下载 mingw,下载完成后,运行文件; 选择安装 gcc,其余不需要的可自行勾掉; Git 集成下载 make-4.3-without-guile-w32-bin.zip ,解压压缩包,将解压出来文件夹复制到自身 Git 安装目录下的 mingw64 文件夹进行合并。 示例 版本查看","categories":[],"tags":[]},{"title":"Docker Desktop 开启 Kubernetes","slug":"Docker Desktop 开启 Kubernetes","date":"2021-11-28T22:56:54.000Z","updated":"2024-10-04T01:16:39.616Z","comments":true,"path":"2021/11/28/Docker Desktop 开启 Kubernetes/","link":"","permalink":"https://g-ydg.github.io/2021/11/28/Docker%20Desktop%20%E5%BC%80%E5%90%AF%20Kubernetes/","excerpt":"","text":"查看版本 Docker -> About Docker Desktop 项目拉取1git clone https://github.com/AliyunContainerService/k8s-for-docker-desktop.git 切换到显示对应版本1git checkout v1.21.5 拉取镜像以管理员模式运行PowerShell 并进入项目路径下。 1.\\load_images.ps1 镜像拉取完成后,重启 DockerDesktop。 配置 Kubernetes切换 Kubernetes 运行上下文至 docker-desktop1kubectl config use-context docker-desktop 查看集群状态12kubectl cluster-infokubectl get nodes 配置 Kubernetes 控制台部署 Kubernetes dashboard1kubectl create -f kubernetes-dashboard.yaml 检查 kubernetes-dashboard 应用状态1kubectl get pod -n kubernetes-dashboard 开启 API Server 访问代理1kubectl proxy 通过如下 URL 访问 Kubernetes dashboard1http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ 配置控制台访问令牌123$TOKEN=((kubectl -n kube-system describe secret default | Select-String "token:") -split " +")[1]kubectl config set-credentials docker-desktop --token="${TOKEN}"echo $TOKEN 登录 dashboard 相关文章 k8s-for-docker-desktop","categories":[],"tags":[]},{"title":"你所做的A/B实验,可能是错的【转载】","slug":"你所做的A!B实验,可能是错的【转载】","date":"2021-11-24T23:54:21.000Z","updated":"2024-10-04T01:16:46.703Z","comments":true,"path":"2021/11/24/你所做的A!B实验,可能是错的【转载】/","link":"","permalink":"https://g-ydg.github.io/2021/11/24/%E4%BD%A0%E6%89%80%E5%81%9A%E7%9A%84A!B%E5%AE%9E%E9%AA%8C%EF%BC%8C%E5%8F%AF%E8%83%BD%E6%98%AF%E9%94%99%E7%9A%84%E3%80%90%E8%BD%AC%E8%BD%BD%E3%80%91/","excerpt":"","text":"在 A/B 实验不断走红的今天,越来越多的企业开始意识到 A/B 实验的重要意义,并试图通过 A/B 实验,前置性地量化决策收益,从而实现增长。然而,当你和其他业务伙伴谈及 A/B 实验时,你总能听到这样的论调: “这事儿很简单,做个实验就行了。准备两个版本,在不同渠道里发版,然后看看数据。” “把用户按照 did(device_id)尾号奇偶分流进实验组和对照组,然后看看数据表现。” 不可否认,这部分企业的确走在前沿,初步拥有了 A/B 实验的思维。然而令人遗憾的是,他们操作的所谓“A/B 实验”,其实并不具备 A/B 实验应有的功效。 更令人遗憾的是,他们似乎对此并不知晓。 对于 A/B 实验原理认知的缺失,致使许多企业在业务增长的道路上始终在操作一批“错误的 A/B 实验”。这些实验并不能指导产品的优化和迭代,甚至有可能与我们的初衷背道而驰,导致“负增长”。 因此,为了能够更好地明白什么是 A/B 实验,我们不妨先来了解几种错误的 A/B 实验。 No1:用户抽样不科学丨 典型表现“用户抽样不科学”是错误 A/B 实验的第一宗罪。操作这种错误 A/B 实验的企业常采取以下做法: 实验中,在不同的渠道/应用市场中,发布不同版本的 APP/页面,并把用户数据进行对比; 简单地从总体流量中抽取 n%用于实验,不考虑流量分布,不做分流处理(例如:简单地从总体流量中任意取出 n%,按照 ID 尾号单双号把用户分成两组)。 丨 错在哪儿不同应用市场/渠道的用户常常带有自己的典型特征,用户分布具有明显区别。对总流量进行“简单粗暴”地抽样也有着同样的问题——分流到实验组和对照组的流量可能存在很大的分布差异。 实际上,A/B 实验要求我们,尽可能地保持实验组和对照组流量分布一致(与总体流量也需保持分布一致),否则得出的实验数据并不具有可信性。 为什么要保持分布一致呢?我们不妨来看一个问题。 某大学由两个学院组成。1 号学院的男生录取率是 75%,女生录取率 49%,男生录取率高于女生;2 号学院男生录取率 10%,女生录取率 5%,男生录取率同样高于女生。问:综合两个学院来看,这所大学的总体录取率是否男生高于女生? 直觉上来说,许多人会觉得,男生录取率总体上会高于女生。然而事实并不是这样,让我们来看看实际数字: 从上表可以看出,尽管两个学院男生录取率都高于女生,但综合考虑两个学院的情况时,男生的总体录取率却要低于女生。这种现象在统计学中被称为辛普森悖论。 辛普森悖论由英国统计学家 E.H 辛普森于 1951 年提出。其主要内容是:几组不同的数据中均存在一种趋势,但当这些数据组合在一起后,这种趋势消失或反转。其产生的原因主要是数据中存在多个变量。这些变量通常难以识别,被称为“潜伏变量”。潜伏变量可能是由于采样错误造成的。 在 A/B 实验中,如果实验组和对照组的样本流量分布不一致,就可能产生辛普森悖论,得到不可靠的实验结果。 分流是 A/B 实验成功与否的关键点,在早期企业还不具备过硬研发能力情况下,想要真正做对 A/B 实验,最佳方法是借助第三方实验工具中成熟的分流服务。 在前一篇《火山引擎 A/B 测试》中,我们曾提到火山引擎 A/B 测试长期服务于抖音、今日头条等头部互联网产品,分流服务科学可靠,并且能够支撑亿级 DAU 产品进行 Push 实验,在高并发场景下保持稳定,帮助我们从总体流量中更加均匀地分流样本,使实验更科学。 No2:互斥层选择错误丨 典型表现接入了实验工具,A/B 实验就能做对了吗?也不尽然。许多实验者在进行实验操作时,将有关联性的实验放置在不同的实验互斥层上,导致实验结果不可信。 何谓“互斥层”?在火山引擎 A/B 测试中,“互斥层”技术是为了让多个实验能够并行,不相互干扰,且都获得足够的流量而研发的流量分层技术。 假设我现在有 4 个实验要进行,每一个实验要取用 30%的流量才能够得出可信的实验结果。此时为了同时运行这 4 个实验就需要 4*30%=120%的流量,这意味着 100%的流量不够同时分配给这 4 个实验。那么此时我只能选择给实验排序,让几个实验先后完成。但这会造成实验效率低下。试想一下,抖音每天有上千个实验要进行,如果只能排队挨号,抖音的实验 schedule 恐怕要排个 10 年。 那么有没有办法可以解决这个问题呢? 有,就是使用互斥层技术,把总体流量“复制”无数遍,形成无数个互斥层,让总体流量可以被无数次复用,从而提高实验效率。 各互斥层之间的流量是正交的,你可以简单理解为:在互斥层选择正确的前提下,流量经过科学的分配,可保证各实验的结果不会受到其他互斥层的干扰。 在选择互斥层的时候,实验者应当要遵循的规则是:假如实验之间有相关性,那么实验必须置于同一互斥层;假如实验之间没有相关性,那么实验可以置于不同互斥层。如果不遵循这一原则,那么 A/B 实验就会出问题。 丨 错在哪儿那么,问题究竟是出在了哪儿呢? 对于实验需求旺盛的企业来说,互斥层技术完美解决了多个实验并行时流量不够用的问题。然而,乱选互斥层会导致实验结果不可信。为什么?举个例子,现在我们想对购买页面的购买按钮进行实验。我们作出两个假设: 假设 1:将购买按钮的颜色从蓝色改为红色,用户购买率可以提高 3%; 假设 2:将购买按钮的形状从方形改为圆形,用户购买率可以提高 1.5%。 针对上述两个假设,我们需要开设两个实验:一个针对按钮颜色,一个针对按钮形状。两个实验均与购买按钮有关系,具有明显的关联性。这两组实验是否可以放在不同互斥层上呢? 丨 情况 1:相关实验置于不同层如下图,我们把两个实验分别放置在两层上,同时开启两个实验。 此时用户 A 打开了我们的购买页面,进入到总体流量之中。在互斥层 1 里,用户被测试按钮颜色的实验命中,进入实验组 Red;在互斥层 2 里,用户被测试按钮形状的实验命中,进入实验组 Round。 由图可知,用户 A 将受到“按钮颜色 Red”以及“按钮形状 Round”两个策略影响,我们无法判断究竟是哪个策略影响了该用户的行为。换句话说,由于两个实验存在关联,用户重复被实验命中,实验结果实际受到了多个策略的影响。这种情况下,两个实验的结果便不再可信了。 丨 情况 2:相关实验置于同一层换个思路,如果将上面的两个实验放置在同一层上,那么用户在进入实验后便只会被一个实验命中。两个实验组均只受到一个策略影响,实验结果可信。 企业在进行 A/B 实验时,工具是基础设施,在实际业务,我们还需要结合具体的实验场景,进行正确的实验设计。 No3:不考虑是否显著丨 典型表现实验结束后,只简单地观测实验数据的涨跌,不考虑实验结果是否显著。 丨 错在哪儿“显著”是一个统计学用词,为什么我们需要在评估实验结果时引入统计学呢? 我们已经知道,A/B 实验是一种小流量实验,我们需要从总体流量中抽取一定量的样本来验证新策略是否有效。然而抽样过程中,样本并不能完全代表整体,虽然我们竭尽全力地进行随机抽样,但最终仍无法避免样本和总体之间的差异。 了解了这一前提我们就能明白,在 A/B 实验中,如果只对数据进行简单的计算,我们对于实验结果的判断很可能会“出错”(毕竟我们通过实验观测得到的是样本数据,而不是整体数据)。 那么,有什么办法去量化样本与总体之间的差异对数据指标造成的影响呢?这就需要结合统计学的方法,在评估实验结果时加入相应的统计学指标,如置信度、置信区间、统计功效等。 原则上,如果实验结果不显著(或说不置信),我们便不能判断数据的涨/跌,是否是由实验中采取的策略造成的(可能由抽样误差造成),我们也不能盲目地全量发布新策略/否定新策略。 A/B 实验中的统计学原理是一个较为庞大复杂的课题,介于篇幅,我们在此暂不做展开解释。对这部分内容感兴趣的读者也可关注本公众号,我们在后期会推出相应内容来为大家进行讲解。需要明确的一点是:评估 A/B 实验,绝不仅仅是比较下实验组和对照组的数据高低这么简单。 在实验结果评估方面,好的实验平台需要具备两个特点:第一是可靠的统计策略,第二是清晰、完善的实验报告。相较于市面上其他实验工具,这两个特点正是火山引擎 A/B 测试的优势所在。 在统计策略方面,火山引擎 A/B 测试的统计策略长期服务于抖音、今日头条等产品,历经打磨,科学可靠;在实验报告方面,从概览至指标详情,火山引擎 A/B 测试依托于经典统计学的假设检验方法,结合置信度、置信区间,帮助实验者全方位的判断实验策略收益。 作为互联网公司的新宠,A/B 实验确有其独到之处,但浅显的实验认知、错误的实验方法,可能会致使企业在增长的道路上“反向前行”。此处让我们借用一句经典的影视台词吧:“发生这种事,大家都不想的。” 事实上,本文中所提及的“错误的 A/B 实验”,只是最浅显的 3 种,在产品增长的道路上,潜伏在一旁埋伏着实验者的“大坑”还有很多,我们也会在本公众号中陆续教给大家如何“避坑”。 想要真正了解 A/B 实验,了解抖音、今日头条等产品的实验 case 和实验方法,欢迎扫描下方二维码,关注火山引擎公众号。 当然,你也直接点击下方阅读原文即可访问官网,试试抖音、今日头条等产品都在用的实验引擎——火山引擎 A/B 测试。 ———————————————————————————————————————- 文章转载:你所做的 A/B 实验,可能是错的","categories":[],"tags":[]},{"title":"Jenkins 构建自动化任务","slug":"Jenkins 构建自动化任务","date":"2021-11-19T22:41:09.000Z","updated":"2024-10-04T01:16:56.791Z","comments":true,"path":"2021/11/19/Jenkins 构建自动化任务/","link":"","permalink":"https://g-ydg.github.io/2021/11/19/Jenkins%20%E6%9E%84%E5%BB%BA%E8%87%AA%E5%8A%A8%E5%8C%96%E4%BB%BB%E5%8A%A1/","excerpt":"","text":"新建任务 项目配置丢弃旧的构建【可选】 源码管理配置仓库地址 配置 Git 账号信息 若认证失败,请检查插件 GitHub Authentication plugin 是否安装 构建触发器 **定期构建: **周期性构建。日程表类似 Linux crontab 书写格式。如上图设置,表示每隔 15 分钟进行一次构建; GitHub hook trigger for GITScm polling:配合 GitWebhooks 使用,当有更改 push 到代码仓库,则触发构建; 轮询 SCM:配置这个选项,jenkins 会周期性的去检查代码仓库是否发生改动,若存在改动则触发构建; 构建 Publish over SSH 配置","categories":[],"tags":[]},{"title":"Github+Hexo搭建个人博客","slug":"Github+Hexo搭建个人博客","date":"2021-11-04T00:13:14.000Z","updated":"2024-10-04T01:17:14.407Z","comments":true,"path":"2021/11/04/Github+Hexo搭建个人博客/","link":"","permalink":"https://g-ydg.github.io/2021/11/04/Github+Hexo%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2/","excerpt":"","text":"前提 github 账号 node.js git 创建 Git 仓库打开 github,新建仓库。仓库名称命名格式:用户名.github.io。例如,我的用户名为 g-ydg,则对应为g-ydg.github.io。创建完成后,点击设置。点击 Pages 选项卡,如下图所示即可。打开浏览器访问g-ydg.github.io,若正常访问,则设置正常。 Hexo 项目搭建新建文件夹,点击文件夹右键打开 Git Bush Here。输入命令安装 hexo。 1$ npm install -g hexo-cli 安装完成后,进行 hexo 初始化。 1$ hexo init 输入 hexo g,进行静态部署。 1$ hexo g 部署完成后,输入 hexo s 启动服务。打开浏览器,访问启动地址。 部署到 github打开项目目录下的_config.yml 文,滑动到文件底部,修改 deoloy 配置。 1234deploy: type: git repository: [email protected]:G-YDG/g-ydg.github.io.git # 你的Git仓库地址 branch: master # 分支默认都为master,若有指定,自行修改 仓库地址与分支名称查看,如下图所示位置。安装 Git 部署插件 1$ npm install hexo-deployer-git --save 完成后,依次执行以下命令。 123hexo clean #清除缓存文件hexo g #生成静态文件 (hexo generate)hexo d #部署到设定仓库 (hexo deploy) 执行完成后,打开浏览器输入 https://你的用户名.github.io,即可访问。 主题优化选择主题关于主题,咱们可以上hexo 官网进行选择。 安装主题本文以 pure 主题为例。进入项目目录,右键 Git Bash,下载主题。 1git clone https://github.com/cofess/hexo-theme-pure themes/pure 下载完成后,查看项目目录。 启用主题打开_config.yml 文件,修改 theme 为 pure。启动本地服务,进行主题效果预览。 1hexo -s 站点基本配置12345678# Sitetitle: ydd # 标题subtitle: 'ydd' # 副标题description: '' # 简介keywords: #关键词author: Ydd # 作者language: zh-CN # 主题语言timezone: Asia/Shanghai # 时区","categories":[],"tags":[]},{"title":"Jenkins快速部署","slug":"Jenkins快速部署","date":"2021-10-26T22:44:31.000Z","updated":"2024-10-04T01:17:24.081Z","comments":true,"path":"2021/10/26/Jenkins快速部署/","link":"","permalink":"https://g-ydg.github.io/2021/10/26/Jenkins%E5%BF%AB%E9%80%9F%E9%83%A8%E7%BD%B2/","excerpt":"","text":"1 项目拉取1git clone https://github.com/G-YDG/docker 2 环境变量配置1cd docker && cp .env.example .env 12345678910# .env···### JENKINS ###############################################JENKINS_HOST_HTTP_PORT=8090JENKINS_HOST_SLAVE_AGENT_PORT=50000JENKINS_HOME=./jenkins/jenkins_home···· 3 启动服务1docker-composer up -d jenkins 4 访问 Jenkins1http://localhost:8090/ 5 解锁 Jenkins进入容器1docker-compose exec jenkins bash 容器内获取密码1cat /var/jenkins_home/secrets/initialAdminPassword 6 插件安装选择 选择插件来安装,勾选 dashboard-view**、**publish-over-ssh 。 勾选完成后,点击 安装。 安装完成后,设置管理员用户。 配置访问链接 安装完成","categories":[],"tags":[]},{"title":"Gitlab开发composer包","slug":"Gitlab开发composer包","date":"2021-10-21T00:33:46.000Z","updated":"2024-10-04T01:17:29.984Z","comments":true,"path":"2021/10/21/Gitlab开发composer包/","link":"","permalink":"https://g-ydg.github.io/2021/10/21/Gitlab%E5%BC%80%E5%8F%91composer%E5%8C%85/","excerpt":"","text":"新建项目 拉取项目初始化项目1composer init 输入包名后,一直回车即可。 123456789101112{ "name": "hdk/common", "description": "", "authors": [ { "name": "ydg", "email": "[email protected]" } ], "require": {}} 代码开发composer.json 修改1234567891011121314151617181920212223242526272829303132{ "name": "hdk/common", "description": "hdk code", "authors": [ { "name": "ydg", "email": "[email protected]" } ], "require": { "php": ">=7.1", "guzzlehttp/guzzle": "^6.2 || ^7.0", "pimple/pimple": "^3.0", "ext-json": "*", "ext-openssl": "*", "ext-mbstring": "*", "ext-ctype": "*" }, "autoload": { "psr-4": { "Hdk\\\\Common\\\\": "Common/src/", "Hdk\\\\Sdk\\\\": "Sdk/src/" } }, "minimum-stability": "dev", "prefer-stable": true, "config": { "optimize-autoloader": true, "sort-packages": true }} 提交代码并推送分支新建标签 在其他项目中引入该 composer 包修改项目中的 composer.json1{ “repositories”: {“haodanku-common”: {“type”: “git”,“url”: “http://gitlab.xxx.com/yandonggan/haodanku_common.git"}},“config”: {“secure-http”: false}} 123```bashcomposer require 包名","categories":[],"tags":[]},{"title":"使用PHPCodeSniffer规范项目代码","slug":"使用PHPCodeSniffer规范项目代码","date":"2021-10-15T19:00:38.000Z","updated":"2024-10-04T01:17:41.854Z","comments":true,"path":"2021/10/15/使用PHPCodeSniffer规范项目代码/","link":"","permalink":"https://g-ydg.github.io/2021/10/15/%E4%BD%BF%E7%94%A8PHPCodeSniffer%E8%A7%84%E8%8C%83%E9%A1%B9%E7%9B%AE%E4%BB%A3%E7%A0%81/","excerpt":"","text":"PHP_CodeSniffer 是什么PHP_CodeSniffer 是一个代码风格检测工具,是确保代码简洁一致的必不可少的开发工具,甚至还可以帮助程序员减少一些语义错误。它包含有两类脚本 phpcs 和 phpcbf。 PHPCSphpcs 脚本对 PHP、JavaScript、CSS 文件定义了一系列的代码规范(通常使用官方的代码规范标准,比如 PHP 的 PSR2),能够检测出不符合代码规范的代码并发出警告或报错(可设置报错等级)。 PHPCBFphpcbf 脚本能自动修正代码格式上不符合规范的部分,比如 PSR2 规范中对每一个 PHP 文件的结尾都需要有一行空行,那么运行这个脚本后就能自动在结尾处加上一行空行。 PHPCodeSniffer 安装全局安装1composer global require "squizlabs/php_codesniffer=*" 安装完毕后,在全局的 Vendor 目录下的 bin 目录中,会生成两个软连接。 查看全局 Vendor 目录位置1composer global config bin-dir --absolute PHPStorm 配置让编辑器使用 PSR 标准(以下以 PSR12 为例) CodeStyle PHP_CodeSniffer 集成到 PHPStormTools 点击运行 PHPCS,进行代码规范检查 点击运行 PHPCBF,进行代码修复。PHPCBF 只能修复代码风格产生的问题,并不能完全修复所有问题,部分问题需自身进行解决。 快捷键配置每次都需要从 Tools -> External Tools 中点击,太过于繁琐,我们可以通过设置快捷键来简化操作。 相关文章 自动检查代码规范 - PHP_CodeSniffer","categories":[],"tags":[]},{"title":"Go快速入门","slug":"Go快速入门","date":"2021-10-13T22:44:41.000Z","updated":"2024-10-04T01:17:44.737Z","comments":true,"path":"2021/10/13/Go快速入门/","link":"","permalink":"https://g-ydg.github.io/2021/10/13/Go%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8/","excerpt":"","text":"文档 Go 语言圣经 Go 语言教程 Go 指南 Go 语言中文网 Go 语言标准库文档 Go镜像代理服务 语言特点 自动立即回收 更丰富的内置类型 函数多返回值 错误处理 匿名函数和闭包 类型和接口 并发编程 反射 语言交互性 基础语法数据类型语言声明与作用域语言作用域 声明在函数内部,是函数的本地值,类似** private** 声明在函数外部,是对当前包可见(包内所有 .go 文件都可见)的全局值,类似 protect 声明在函数外部且首字母大写是所有包可见的全局值,类似 public 语言定义var(声明变量)语法 1234567# 单个var 变量名# 批量变量类型 var ( a int b int) 示例 123456789101112131415// 正常定义方式var a int = 0fmt.Println("常定义方式 :", a)// 根据值得知类型var b = 0fmt.Println("根据值得知类型 :", b)// := 隐式定义方式c := 0 fmt.Println(" := 定义方式 :", c)// 批量定义var ( v_a int = 201 v_b int = 202 v_c int )vc = 203fmt.Printf("批量声明变量 v_a:%a | v_b:%b | v_c:%c \\n", v_a, v_b, v_c) const(声明常量)语法 语法 12345678const identifier [type] = valueconst ( n1 = iota //0 n2 //1 n3 //2 n4 //3)const c_name1, c_name2 = value1, value2 示例 12345678910111213const GNAME = "GNAME"const ( a = 1 b = 2)const ( a1 = 10 b1 c1)fmt.Println("GNAME", GNAME)fmt.Println("a, b: ", a, b)fmt.Println("a1, b1, c1: ", a1, b1, c1) iota(特殊常量)iota 可以认为是一个可以被编译器修改的常量,是 go 语言的常量计数器,只能在常量的表达式中使用。iota 在 const 关键字出现时将被重置为 0。const 中每新增一行常量声明将使 iota 计数一次( iota 可理解为 const 语句块中的行索引)。 使用 iota 能简化定义,在定义枚举时很有用。 12345678910111213141516171819202122232425262728293031323334353637const ( a1 = iota b1 c1)const ( a1 = iota b1 = iota c1 = iota)fmt.Println("a1, b1, c1: ", a1, b1, c1)// 输出都是 : a1, b1, c1: 0 1 2const ( a = iota //0 b //1 c //2 d = "ha" //独立值,iota += 1 e //"ha" iota += 1 f = 100 //iota +=1 g //100 iota +=1 h = iota //7,恢复计数 i //8)fmt.Println(a, b, c, d, e, f, g, h, i)// 输出是 :0 1 2 ha ha 100 100 7 8const ( a, d = iota + 1, iota + 10 b, e c, f)fmt.Println(a, b, c, d, e, f)// 输出是 :1 2 3 10 11 12 type(声明类型)语法 12345type 类型名 struct { 字段名 字段类型 字段名 字段类型 …} 其中: 1. 类型名:标识自定义结构体的名称,在同一个包内不能重复。 2. 字段名:表示结构体字段名。结构体中的字段名必须唯一。 3. 字段类型:表示结构体字段的具体类型。 对于学习过其他语言的同志来说可以理解为是对象属性,只是不存在方法 示例 12345678910111213type class struct { name string stage int}func main() { fmt.Println(class{"class", 1}) var c class c.name = "demo class" c.stage = 1 fmt.Println("名称: ", g.name) fmt.Println("阶段: ", g.stage)} func(声明函数)特点 无需声明原型 支持不定变参 支持多返回值 支持命名返回参数 支持匿名函数和闭包 函数也是一种类型,一个函数可以赋值给变量 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数 不支持 重载 (overload) 不支持 默认参数 (default parameter) 语法 1func function_name( [parameter list] ) [return_types] { 函数体 } 示例 123456789101112131415func main() { a, b := test(1, 2, "sum") fmt.Println("a,b : ", a, b) dome()}func test(x, y int, s string) (int, string) { // 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。 n := x + y return n, s}func dome() { fmt.Println("dome")} init 和 main 函数main 函数Go 语言程序的默认入口函数(主函数):func main() ,函数体用{}一对括号包裹。 main 函数只能用于 main 包中,且只能定义一个。如同 PHP 框架中的 index.php 。 init 函数go 语言中 init 函数用于包(package)的初始化,该函数是 go 语言的一个重要特性。可以理解为如同其他语言中的面向对象。func init() { }。 特点 init 函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等 每个包可以拥有多个 init 函数 包的每个源文件也可以拥有多个 init 函数 同一个包中多个 init 函数的执行顺序 go 语言没有明确的定义(说明) 不同包的 init 函数按照包导入的依赖关系决定该初始化函数的执行顺序 init 函数不能被其他函数调用,而是在 main 函数执行之前,自动被调用 示例123456var name string func init() { name = "demo"}func main() { fmt.Println(name)} 执行流程 对同一个 go 文件的 init()调用顺序是从上到下的。 对同一个 package 中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的 init()函数。 对于不同的 package,如果不相互依赖的话,按照 main 包中”先 import 的后调用”的顺序调用其包中的 init(),如果 package 存在依赖,则先调用最早被依赖的 package 中的 init(),最后调用 main 函数。 如果 init 函数中使用了 println()或者 print()会导致不会顺序执行过程 ,这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用。 import 和 package 的使用 golang 使用 package 来管理定义模块,使用 import 关键字来导入使用。 如果导入的是 go 自带的包,则会去安装目录 $GOROOT/src,按照包路径加载。 如果是我们 go get 安装的或自定义的包,则会去 $GOPATH/src 加载。 package 模块目录文件夹名作为包名,前面的路径只是用来导入而和包名无关 package 的存放位置是以$GOPATH/src 作为根目录,然后灵活的按照目录去组织,且包名需与最后一级目录名一致。 示例 import常用1234import ( "database/sql" "github.com/go-sql-driver/mysql") 别名1234import ( "database/sql" mysql "github.com/go-sql-driver/mysql") 下划线忽略导入的包,只执行包内的 init 方法当导入一个包时,该包下的所有文件的 init() 函数都会被执行,然而,有些时候我们并不需要把整个包都导入进来使用,仅仅是希望它执行 init() 函数而已。 1import _ 包路径 // 只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数。 示例 12import "database/sql"import _ "github.com/go-sql-driver/mysql" 忽略返回值123456789101112package mainimport "fmt"func main() { a, _ := compare(1) fmt.Println(a)}func compare(a int) (int, error) { return a, nil} 解释 1 下划线意思是忽略这个变量. 比如 compare,返回值为 int, error 普通写法是 a,err := compare(1) 如果此时不需要知道返回的错误值 就可以用 a,_ := compare(1) 如此则忽略了 error 变量 解释 2 占位符,意思是那个位置本应赋给某个值,但是咱们不需要这个值 所以就把该值赋给下划线,意思是丢掉不要 这样编译器可以更好的优化,任何类型的单个值都可以丢给下划线 这种情况是占位用的,方法返回两个结果,而你只想要一个结果 那另一个就用”_“占位,而如果用变量的话,不使用,编译器是会报错的 接口断言1234567type Person interface { Say()}type Student struct {}var _ Person = Student{} 判断 Student是否实现了 Person, 用作类型断言,如果 Student 没有实现 Person ,则会报编译错误。 常用命令123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051Go is a tool for managing Go source code.Usage: go <command> [arguments]The commands are: bug start a bug report build compile packages and dependencies clean remove object files and cached files doc show documentation for package or symbol env print Go environment information fix update packages to use new APIs fmt gofmt (reformat) package sources generate generate Go files by processing source get add dependencies to current module and install them install compile and install packages and dependencies list list packages or modules mod module maintenance run compile and run Go program test test packages tool run specified go tool version print Go version vet report likely mistakes in packagesUse "go help <command>" for more information about a command.Additional help topics: buildconstraint build constraints buildmode build modes c calling between Go and C cache build and test caching environment environment variables filetype file types go.mod the go.mod file gopath GOPATH environment variable gopath-get legacy GOPATH go get goproxy module proxy protocol importpath import path syntax modules modules, module versions, and more module-get module-aware go get module-auth module authentication using go.sum packages package lists and patterns private configuration for downloading non-public code testflag testing flags testfunc testing functions vcs controlling version control with GOVCSUse "go help <topic>" for more information about that topic. go env 用于打印 Go 语言的环境信息。 go run 命令可以编译并运行命令源码文件。 go get 可以根据需求从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。 go build 命令用于编译我们指定的源码文件或代码包以及它们的依赖包。 go install 用于编译并安装指定的代码包及它们的依赖包。 go clean 命令会删除掉执行其它命令时产生的一些文件和目录。 go doc 命令可以打印附于 Go 语言程序实体上的文档。我们可以通过把程序实体的标识符作为该命令的参数来达到查看其文档的目的。 go test 命令用于对 Go 语言编写的程序进行测试。 go list 命令的作用是列出指定的代码包的信息。 go fix 会把指定代码包的所有 Go 语言源码文件中的旧版本代码修正为新版本的代码。 go vet 是一个用于检查 Go 语言源码中静态错误的简单工具。","categories":[],"tags":[]},{"title":"Go环境搭建","slug":"Go环境搭建","date":"2021-10-13T21:56:45.000Z","updated":"2024-10-04T01:17:49.134Z","comments":true,"path":"2021/10/13/Go环境搭建/","link":"","permalink":"https://g-ydg.github.io/2021/10/13/Go%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/","excerpt":"","text":"文档 Go 语言中文网:https://studygolang.com/ Go 语言文档:http://docscn.studygolang.com/doc/ Go镜像代理服务:https://goproxy.io/zh/ 环境搭建Linux下载软件包1# wget https://dl.google.com/go/go1.17.2.linux-amd64.tar.gz 解压到/usr/local1# tar -C /usr/local -xzf go1.17.2.linux-amd64.tar.gz 环境变量配置12# vim /etc/profile export PATH=$PATH:/usr/local/go/binexport GOPATH=xxx 12# source /etc/profile 查看环境变量 GOARCH 表示目标处理器架构 GOOS 表示目标操作系统 GOROOT 表示 Go 开发包的安装目录 GOBIN 表示 Go 可执行文件目录 GOPATH 表示 Go 的工作目录 Windows软件安装下载软件包https://dl.google.com/go/go1.17.2.windows-amd64.msi,下载完成后运行安装即可。 查看环境变量 简单示例1234567package main // 声明 main 包,表明当前是一个可执行程序import "fmt" // 导入内置fmtfunc main() { // main函数,程序执行入口 fmt.Printf("hello world")} 运行服务1go run main.go","categories":[],"tags":[]},{"title":"logrotate实现nginx日志自动切割","slug":"logrotate实现nginx日志自动切割","date":"2021-10-12T23:56:58.000Z","updated":"2024-10-04T01:17:49.141Z","comments":true,"path":"2021/10/12/logrotate实现nginx日志自动切割/","link":"","permalink":"https://g-ydg.github.io/2021/10/12/logrotate%E5%AE%9E%E7%8E%B0nginx%E6%97%A5%E5%BF%97%E8%87%AA%E5%8A%A8%E5%88%87%E5%89%B2/","excerpt":"","text":"使用 Logrorate 切割 nginx 日志1vim /etc/logrotate.d/nginx 123456789101112131415/var/log/nginx/*.log #此处为nginx存储日志的地方; { daily #指定转储周期为每天 rotate 30 #转储次数,超过将会删除最老的那一个 missingok #如果日志文件丢失,不要显示错误 compress #通过gzip 压缩转储以后的日志 delaycompress #当前转储的日志文件到下一次转储时才压缩 notifempty #当日志文件为空时,不进行轮转 create 755 nginx adm #创建 775权限、用户组为nginx的日志文件 postrotate #表示当切割之后要执行的命令 if [ -f /var/run/nginx/nginx.pid ]; then kill -USR1 `cat /var/run/nginx/nginx.pid` #PID路径根据实际路径填写; fi endscript} 测试程序执行的情况1logrotate -vf /etc/logrotate.d/nginx 查看 log 文件的具体执行情况1cat /var/lib/logrotate/logrotate.status 加入定时任务1crontab -e 10 0 * * * /usr/sbin/logrotate -vf /etc/logrotate.d/nginx #每天凌晨00:00自动执行日志切割任务; 常见问题1、分割日志时报错:error: skipping “/var/log/nginx/test.access.log” because parent directory has insecure permissions (It’s world writable or writable by group which is not “root”) Set “su” directive in config file to tell logrotate which user/group should be used for rotation. 在配置文件中添加 “su root nginx 12345678910111213141516/var/log/nginx/*.log { su root list daily rotate 30 missingok compress delaycompress notifempty create 755 nginx adm postrotate if [ -f /var/run/nginx/nginx.pid ]; then kill -USR1 `cat /var/run/nginx/nginx.pid` fi endscript}","categories":[],"tags":[]},{"title":"Linux 下 /var/spool/postfix/maildrop 占用空间很大问题","slug":"Linux 下 !var!spool!postfix!maildrop 占用空间很大问题","date":"2021-10-11T19:57:10.000Z","updated":"2024-10-04T01:17:51.597Z","comments":true,"path":"2021/10/11/Linux 下 !var!spool!postfix!maildrop 占用空间很大问题/","link":"","permalink":"https://g-ydg.github.io/2021/10/11/Linux%20%E4%B8%8B%20!var!spool!postfix!maildrop%20%E5%8D%A0%E7%94%A8%E7%A9%BA%E9%97%B4%E5%BE%88%E5%A4%A7%E9%97%AE%E9%A2%98/","excerpt":"","text":"磁盘空间排查 查看磁盘占用 1df -h 查看根目录下占用情况 1du -sh * 发现 var 目录占用达到 21G,进行/var 目录进行排查 经过多次排查,最终锁定在 /var/spool/postfix/maildrop 目录 原因分析在网上查询相关文章之后得知,由于 Linux 在执行 cron 时,会把 cron 执行脚本中的 output 和 warning 信息,以邮件形式发送给 cron 所有者,但由于环境中的 sendmail 和 postfix 没有正常运行,导致邮件发送不成功,发送不成功时,就会将这些信息文件存入 maildrop 目录,而且没有自动清理转换的机制,时间一长就形成堆积。 解决 清空 /var/spool/postfix/maildrop 12cd /var/spool/postfix/maildropls | xargs rm -rf 若不需 crontab 进行邮件通知,可修改配置进行停止 crontab -e 在 cron 的第一行加入 MAILTO=”” tips:Linux 如何删除大量碎小文件","categories":[],"tags":[]},{"title":"Linux如何删除大量碎小文件?","slug":"Linux如何删除大量碎小文件?","date":"2021-10-11T19:45:18.000Z","updated":"2024-10-04T01:17:51.604Z","comments":true,"path":"2021/10/11/Linux如何删除大量碎小文件?/","link":"","permalink":"https://g-ydg.github.io/2021/10/11/Linux%E5%A6%82%E4%BD%95%E5%88%A0%E9%99%A4%E5%A4%A7%E9%87%8F%E7%A2%8E%E5%B0%8F%E6%96%87%E4%BB%B6%EF%BC%9F/","excerpt":"","text":"ls cd <需要清理删除小文件的目录>ls | xargs rm -rf 示例12[root@haodanku-crontab ~]$ cd /var/spool/postfix/maildrop/[root@haodanku-crontab ~]$ ls | xargs rm -rf rsync mkdir <空文件夹>rsync –delete-before -d <空文件夹> <需要清理删除小文件的目录> 示例1234[root@haodanku-crontab ~]$ mkdir /data/null[root@haodanku-crontab ~]$ ls -l /data/nulltotal 0[root@haodanku-crontab ~]$ rsync --delete-before -d /data/null/ /var/spool/postfix/maildrop/ find find <需要清理删除小文件的目录> -type f -delete 示例1[root@haodanku-crontab ~]$ find /var/spool/postfix/maildrop/ -type f -delete 查看释放情况1while true; do df -i /; sleep 10; done","categories":[],"tags":[]},{"title":"使用filebeat替代logstash收集日志","slug":"使用filebeat替代logstash收集日志","date":"2021-09-28T18:15:01.000Z","updated":"2024-10-04T01:17:51.615Z","comments":true,"path":"2021/09/28/使用filebeat替代logstash收集日志/","link":"","permalink":"https://g-ydg.github.io/2021/09/28/%E4%BD%BF%E7%94%A8filebeat%E6%9B%BF%E4%BB%A3logstash%E6%94%B6%E9%9B%86%E6%97%A5%E5%BF%97/","excerpt":"","text":"本文以采集 nginx 日志为例,贴出实现过程中各服务的具体配置。若初次接触 ELK,可查看 ELK 基本部署以及使用 进行初步了解。 Nginxnginx.conf1 log_format access_json escape=json ‘{“@timestamp”:”$time_iso8601”,’ ‘“host”:”$server_addr”,’‘“real-host”:”$server_addr”,’ ‘“clientip”:”$remote_addr”,’‘“real-ip”:”$Real”,’ ‘“size”:$body_bytes_sent,’‘“responsetime”:$request_time,’ ‘“upstreamtime”:”$upstream_response_time”,’‘“upstreamhost”:”$upstream_addr”,’ ‘“http_host”:”$host”,’‘“url”:”$request_uri”,’ ‘“domain”:”$host”,’‘“xff”:”$http_x_forwarded_for”,’ ‘“referer”:”$http_referer”,’‘“status”:”$status”}’; 1 FilebeatDockerfile12ARG ELK_VERSION=7.6.1FROM elastic/filebeat:${ELK_VERSION} filebeat.yml1234567891011121314151617181920212223242526272829303132333435363738filebeat.inputs: ##### nginx-access-log ########### - type: log enabled: true paths: - "/log/access_log.log" fields: type: nginx-access-log fields_under_root: true ##### nginx-error-log ########### - type: log enabled: true paths: - "/log/log.log" fields: type: nginx-error-log fields_under_root: truefilebeat.config: modules: path: ${path.config}/modules.d/*.yml reload.enabled: falseprocessors: - add_cloud_metadata: ~ - add_docker_metadata: ~#----------------------------- Logstash output --------------------------------output.logstash: # The Logstash hosts hosts: ["logstash:5044"]#============================== X-Pack Monitoring ===============================xpack.monitoring.enabled: truexpack.monitoring.elasticsearch.hosts: [ "elasticsearch:9200" ]xpack.monitoring.elasticsearch.username: "elastic"xpack.monitoring.elasticsearch.password: "xxxxxx" logstashfilebeat.conf1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253input { beats { port => "5044" #codec => plain{charset => "ISO-8859-1"} }}filter { if [type] == "nginx-access-log" { json { source => "message" } mutate { split => [ "upstreamtime", "," ] convert => [ "upstreamtime", "float" ] rename => { "[real-host]" => "host" } rename => {"[upstreamtime][0]" => "upstreamtime"} remove_field => ["message"] } } if [type] == "nginx-error-log" { mutate { split => [ "upstreamtime", "," ] convert => [ "upstreamtime", "float" ] rename => {"[upstreamtime][0]" => "upstreamtime"} rename => { "[host][name]" => "host" } } } mutate { remove_field => ["log", "ecs", "agent", "tags", "input"] }}output { if [type] == "nginx-access-log" { elasticsearch { hosts => ["elasticsearch:9200"] index => "nginx-access-log-%{+YYYY.MM.dd}" user => "elastic" password => "xxxxxx" }# stdout { codec => rubydebug } } if [type] == "nginx-error-log" { elasticsearch { hosts => ["elasticsearch:9200"] index => "nginx-error-log-%{+YYYY.MM.dd}" user => "elastic" password => "xxxxxx" }# stdout { codec => rubydebug } }}","categories":[],"tags":[]},{"title":"linux磁盘清理","slug":"linux磁盘清理","date":"2021-09-26T23:02:45.000Z","updated":"2024-10-04T01:17:51.625Z","comments":true,"path":"2021/09/26/linux磁盘清理/","link":"","permalink":"https://g-ydg.github.io/2021/09/26/linux%E7%A3%81%E7%9B%98%E6%B8%85%E7%90%86/","excerpt":"","text":"磁盘空间占用排查常用命令 查看服务器空间 1df -h 查看当前目录下各文件及文件夹占用大小 1du -sh * 查看当前目录下空间占用最大的 10 个目录或文件 1du -sh * | sort -nr | head 查找当前目录下大于 100M 的文件 1find . -type f -size +100M tips:查看文件以及文件夹大小相关命令","categories":[],"tags":[]},{"title":"Supervisor管理swoole进程的坑","slug":"Supervisor管理swoole进程的坑","date":"2021-09-24T22:38:57.000Z","updated":"2024-10-04T01:17:57.376Z","comments":true,"path":"2021/09/24/Supervisor管理swoole进程的坑/","link":"","permalink":"https://g-ydg.github.io/2021/09/24/Supervisor%E7%AE%A1%E7%90%86swoole%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%9D%91/","excerpt":"","text":"环境 系统: CentOS 8.2 64 位 机器:Intel(R) Xeon(R) Platinum 8369HC CPU @ 3.30GHz 4 核 8G PHP :7.4 框架:swoft2.x 发现系统监控发现机器负载异常的高,如下图所示,从 1 点开始,由不到 1 的负载到后面超 100,负载、内存不断的飙高,但 CPU 使用率却几乎没怎么变化过。 排查 通过 top 命令进行排查 通过 top 命令,看到有大量的 D 状态 swoft 进程。 由于我们采用 supervisor 来管理 swoft 进程,所以先排查 supervisor 是否存在问题。 查看配置 12345678910111213141516[program:prod_cms]command=/usr/bin/php /mnt/alidata1/haodanku_CMS/bin/swoft http:start;process_name=%(process_num)02dautostart=trueautorestart=truestartsecs=1startretries=10user=rootstdout_logfile=/data/supervisor/production_cms_swoft_api.logstdout_logfile_maxbytes=1MBstdout_logfile_backups=50stdout_capture_maxbytes=1MBstderr_logfile=/data/supervisor/production_cms_swoft_api_err.logstderr_logfile_maxbytes=1MBstderr_logfile_backups=50stderr_capture_maxbytes=1MB 由配置知道,进程启动失败时有输出日志信息。 查看错误日志信息,发现存在内存溢出问题。但由于该日志信息无输出时间信息,无法判断是否由此问题引起。 查看启动日志信息,发现启动时存在端口被占用。 查看端口占用情况 1netstat -anp | grep 18406 发现存在 tcp 连接,但无对应进程。 分析supervisord 在杀进程时,swoole 子进程事件未处理完毕,主进程就强制杀掉,导致子进程无法收到关闭信号,所以一直没被杀掉,tcp 连接也没有正常关闭。 解决 编辑supervisord配置参数。增加停止等待时间以及杀掉进程时以进程组的形式进行杀掉增加停止等待时间以及杀掉进程时以进程组的形式进行杀掉 1 stopwaitsecs=60stopasgroup=truestopwaitsecs=true 1 tips supervisord","categories":[],"tags":[]},{"title":"开机自启执行脚本","slug":"开机自启执行脚本","date":"2021-09-22T18:33:00.000Z","updated":"2024-10-04T01:17:57.380Z","comments":true,"path":"2021/09/22/开机自启执行脚本/","link":"","permalink":"https://g-ydg.github.io/2021/09/22/%E5%BC%80%E6%9C%BA%E8%87%AA%E5%90%AF%E6%89%A7%E8%A1%8C%E8%84%9A%E6%9C%AC/","excerpt":"","text":"执行脚本12#!/bin/bash/bin/echo $(/bin/date +%F_%T) >> /tmp/start.log 方法一:修改/etc/rc.local编辑 rc.local1 /bin/bash /scripts/start.sh > /dev/null 2 > /dev/null 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556``````bash#查看最后一行tail -n 1 /etc/rc.local```#### 重启系统查看日志```bashcat /tmp/start.log```###### 方法二:<font style="color:rgb(61, 68, 80);">chkconfig管理</font>可参考文章 [Linux配置开机自启动执行脚本的两种方法](https://www.linuxprobe.com/linux-open-sh.html)### 检验IP脚本```bash#!/bin/bash#需要校验的IPIP_ADDR=172.18.1.110#获取本机IP地址列表machine_ips=$(ip addr | grep 'inet' | grep -v 'inet6\\|127.0.0.1' | grep -v grep | awk -F '/' '{print $1}' | awk '{print $2}')#输入的IP与本机IP进行校验ip_check=falsefor machine_ip in ${machine_ips}; do if [[ "X${machine_ip}" == "X${IP_ADDR}" ]]; then ip_check=true fidoneif [[ $ip_check == true ]] then echo "true" exit 1 else echo "false" exit 1fi```**tips:**+ [shell](https://www.runoob.com/linux/linux-shell-process-control.html)+ [nohup](https://www.runoob.com/linux/linux-comm-nohup.html)+ [stat](https://www.runoob.com/linux/linux-comm-stat.html)","categories":[],"tags":[]},{"title":"记一次服务器系统负载过高问题","slug":"记一次服务器系统负载过高问题","date":"2021-09-12T18:45:12.000Z","updated":"2024-10-04T01:18:06.559Z","comments":true,"path":"2021/09/12/记一次服务器系统负载过高问题/","link":"","permalink":"https://g-ydg.github.io/2021/09/12/%E8%AE%B0%E4%B8%80%E6%AC%A1%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%B3%BB%E7%BB%9F%E8%B4%9F%E8%BD%BD%E8%BF%87%E9%AB%98%E9%97%AE%E9%A2%98/","excerpt":"","text":"查看系统负载情况1top -c /usr/bin/php /mnt/alidata1/haodanku_CMS/bin/swoft http:start 进程为 D 状态(不间断睡眠状态) 持续观察发现该进程 PID 存在变化,猜测可能存在进程守护在维护此命令进程。 tips:Linux 进程状态 1supervisorctl status 发现存在两项服务,且 prod_cms 在 6 秒前触发运行过。 12345# 通过locate命令,查看supervisord运行相关文件夹locate supervisord# 若执行locate,出现 locate: can not stat () `/var/lib/mlocate/mlocate.db': No such file or directoryupdatedb 配置目录为 /etc/supervisord.d/ 12# 进入配置目录cd /etc/supervisord.d 查看 prod_cms 对应 配置文件 production_cms_swoft_api.ini 发现配置有输出运行错误日志,查看错误日志 12# 通过tail命令查看最后50行信息tail -n50 /data/supervisor/production_cms_swoft_api_err.log 发现运行命令时,代码抛出异常信息。查看报错信息,发现代码当中存在三元运算符错误。 解决问题解决定位到问题代码 修复代码问题 停止守护进程服务 1supervisorctl stop prod_cms 运行服务,查看是否存在问题 1php /mnt/alidata1/haodanku_CMS/bin/swoft http:start 环境异常运行服务,发现端口 18406 存在占用 12# 查看 18406端口netstat -anp | grep 18406 发现大量处于 CLOSE_WAIT 状态的 TCP 连接 12# 查看18406端口的tcp连接情况netstat -n| grep -i "18406"|awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' 存在 63 个 CLOSE_WAIT 状态的连接 tips:netstat 命令使用 解决 CLOSE_WAIT 问题为什么会出现 CLOSE_WAIT当客户端因为某种原因先于服务端发出了 FIN 信号,就会导致服务端被动关闭,若服务端不主动关闭 socket 发 FIN 给 Client,此时服务端 Socket 会处于 CLOSE_WAIT 状态(而不是 LAST_ACK 状态)。通常来说,一个 CLOSE_WAIT 会维持至少 2 个小时的时间(系统默认超时时间的是 7200 秒,也就是 2 小时)。如果服务端程序因某个原因导致系统造成一堆 CLOSE_WAIT 消耗资源,那么通常是等不到释放那一刻,系统就已崩溃。 方法一:通过修改 TCP/IP 的参数123vi /etc/sysctl.conf# sysctl.conf - start net.ipv4.tcp_keepalive_time = 1800net.ipv4.tcp_keepalive_probes = 3net.ipv4.tcp_keepalive_intvl = 15 123456789101112131415# sysctl.conf - end# 执行生效sysctl -p```**tips:**+ [**TCP CLOSE_WAIT 过多解决方案**](https://blog.csdn.net/wwd0501/article/details/78674170)+ [**Linux之TCPIP内核参数优化**](https://www.cnblogs.com/fczjuever/archive/2013/04/17/3026694.html)******方法二:万能重启大法**","categories":[],"tags":[]},{"title":"Docker及Docker Compose安装","slug":"Docker及Docker Compose安装","date":"2021-09-09T22:29:24.000Z","updated":"2024-10-04T01:18:06.568Z","comments":true,"path":"2021/09/09/Docker及Docker Compose安装/","link":"","permalink":"https://g-ydg.github.io/2021/09/09/Docker%E5%8F%8ADocker%20Compose%E5%AE%89%E8%A3%85/","excerpt":"","text":"Docker 安装1curl -sSL https://get.daocloud.io/docker | sh Docker Compose 安装1234567# 安装sudo curl -L https://get.daocloud.io/docker/compose/releases/download/1.25.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose# 修改权限sudo chmod +x /usr/local/bin/docker-composedocker-compose --version tips [**Docker**](● https://www.runoob.com/docker/ubuntu-docker-install.html) [**Docker Compose**](● https://www.runoob.com/docker/docker-compose.html) [**快速安装 docker-compose**](● https://www.csdcb.cn/article/226.html)","categories":[],"tags":[]},{"title":"WSL发行版 Docker迁移","slug":"WSL发行版 Docker迁移","date":"2021-09-07T01:36:30.000Z","updated":"2024-10-04T01:18:06.573Z","comments":true,"path":"2021/09/07/WSL发行版 Docker迁移/","link":"","permalink":"https://g-ydg.github.io/2021/09/07/WSL%E5%8F%91%E8%A1%8C%E7%89%88%20Docker%E8%BF%81%E7%A7%BB/","excerpt":"","text":"关闭 docker关闭所有发行版1wsl --shutdown 导出1wsl --export docker-desktop-data D:\\wsl\\docker-desktop-data.tar 注销发行版1wsl --unregister docker-desktop-data 导入1wsl --import docker-desktop-data D:\\wsl\\docker-desktop-data\\ D:\\wsl\\docker-desktop-data.tar --version 2 其他相关命令12345# 设置wsl默认版本wsl --set-default-version 2# 设置具体发行版wsl版本wsl --set-version <发行版> 2","categories":[],"tags":[]},{"title":"ELK基本部署以及使用","slug":"ELK基本部署以及使用","date":"2021-09-01T22:05:36.000Z","updated":"2024-10-04T01:18:10.151Z","comments":true,"path":"2021/09/01/ELK基本部署以及使用/","link":"","permalink":"https://g-ydg.github.io/2021/09/01/ELK%E5%9F%BA%E6%9C%AC%E9%83%A8%E7%BD%B2%E4%BB%A5%E5%8F%8A%E4%BD%BF%E7%94%A8/","excerpt":"","text":"ELK 入门学习文章 ELK 快速入门一-基本部署 ELK 快速入门二-通过 logstash 收集日志 ELK 快速入门三-logstash 收集日志写入 redis ELK 快速入门四-filebeat 替代 logstash 收集日志 ELK 快速入门五-配置 nginx 代理 kibana Dockerfile 以及配置文件ENV.env1234567891011121314151617### Drivers ################################################# All volumes driverVOLUMES_DRIVER=local# All Networks driverNETWORKS_DRIVER=bridge### ELK Stack ##################################################ELK_VERSION=7.8.1### ELASTICSEARCH #########################################ELASTICSEARCH_HOST_HTTP_PORT=9200ELASTICSEARCH_HOST_TRANSPORT_PORT=9300### KIBANA ################################################KIBANA_HTTP_PORT=5601 elasticsearchDockerfile1234ARG ELK_VERSION=7.6.1FROM docker.elastic.co/elasticsearch/elasticsearch:${ELK_VERSION}EXPOSE 9200 9300 logstashDockerfile12ARG ELK_VERSION=7.6.1FROM logstash:${ELK_VERSION} logstash.yml1234567http.host: "0.0.0.0"config.reload.automatic: truepath.config: "/usr/share/logstash/pipeline/"xpack.monitoring.enabled: truexpack.monitoring.elasticsearch.hosts: ["elasticsearch:9200"] kibanaDockerfile1234ARG ELK_VERSION=7.6.1FROM docker.elastic.co/kibana/kibana:${ELK_VERSION}EXPOSE 5601 kibana.yml12345server.name: kibanaserver.host: "0"elasticsearch.hosts: [ "http://elasticsearch:9200" ]xpack.monitoring.ui.container.elasticsearch.enabled: truei18n.locale: "zh-CN" docker-composedocker-compose.yml12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879version: '3.4'networks: service: driver: ${NETWORKS_DRIVER}volumes: elasticsearch: driver: ${VOLUMES_DRIVER} logstash: driver: ${VOLUMES_DRIVER} kibana: driver: ${VOLUMES_DRIVER}services: ### ElasticSearch ######################################## elasticsearch: build: context: ./elasticsearch args: - ELK_VERSION=${ELK_VERSION} volumes: - ./elasticsearch/data:/usr/share/elasticsearch/data environment: - cluster.name=cluster - node.name=node - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - cluster.initial_master_nodes=node ulimits: memlock: soft: -1 hard: -1 ports: - "${ELASTICSEARCH_HOST_HTTP_PORT}:9200" - "${ELASTICSEARCH_HOST_TRANSPORT_PORT}:9300" restart: always networks: - service ### Logstash ############################################## logstash: build: context: ./logstash args: - ELK_VERSION=${ELK_VERSION} volumes: - './logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml' - './logstash/pipeline:/usr/share/logstash/pipeline' - './logstash/GeoLite2-City:/usr/share/logstash/GeoLite2-City' ports: - '5001:5001' - '5044:5044' environment: LS_JAVA_OPTS: '-Xmx1g -Xms1g' env_file: - .env networks: - service restart: always depends_on: - elasticsearch ### Kibana ############################################## kibana: build: context: ./kibana args: - ELK_VERSION=${ELK_VERSION} volumes: - ./kibana/config:/usr/share/kibana/config ports: - "${KIBANA_HTTP_PORT}:5601" depends_on: - elasticsearch restart: always networks: - service 部署12345## 启动服务docker-compose up -d## 查看服务是否正常docker-compose ps 测试服务是否可用elasticsearch1localhost:9200 kibana1localhost:5601 Logstash测试标准输入输出12345678910111213bash-4.2$ /usr/share/logstash/bin/logstash -e 'input { stdin {} } output { stdout { codec => rubydebug} }'# 光标闪烁,输入并回车hello world# 控制台输出{ "@version" => "1", #事件版本号,一个事件就是一个ruby对象 "@timestamp" => 2021-09-02T07:57:12.277Z, #事件发生时间 "host" => "e8ff6e2a9658", #事件来源 "message" => "hello world" #消息内容} 测试输出到文件123456789101112bash-4.2$ /usr/share/logstash/bin/logstash -e 'input { stdin{} } output { file { path => "/tmp/log-%{+YYYY.MM.dd}messages.log"}}'# 光标闪烁,输入并回车hello world# 控制台输出Opening file {:path=>"/tmp/log-2021.09.02messages.log"}bash-4.2$ tail /tmp/log-2021.09.02messages.log{"@timestamp":"2021-09-02T08:04:06.500Z","host":"e8ff6e2a9658","message":"hello world","@version":"1"} 测试输出到 elasticsearch12345678bash-4.2$ /usr/share/logstash/bin/logstash -e 'input { stdin{} } output { elasticsearch {hosts => ["elasticsearch:9200"] index => "mytest-%{+YYYY.MM.dd}" }}'# 验证ES是否收到数据bash-4.2$ curl http://elasticsearch:9200/mytest-2021.09.02{"mytest-2021.09.02":{"aliases":{},"mappings":{"properties":{"@timestamp":{"type":"date"},"@version":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},"host":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},"message":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}}}},"settings":{"index":{"creation_date":"1630570225131","number_of_shards":"1","number_of_replicas":"1","uuid":"0yt4C-0RRt2DdG_5aI16UQ","version":{"created":"7070199"},"provided_name":"mytest-2021.09.02"}}}} 常见错误解决Logstash could not be started because there is already another instance using the configured data directory 查看logstash.yml 中 path.data 路径,若无配置,默认在/usr/share/logstash/data 1234567cd /usr/share/logstash/data# 查看是否存在 .lock 文件,ls -alh# 如果存在把它删除rm .lock vm.max_map_count [65530] is too low1234567vim /etc/sysctl.conf# sysctl.confvm.max_map_count=262144# sysctl.confsysctl -p","categories":[],"tags":[]},{"title":"MySQL: frm文件与idb文件恢复数据表","slug":"MySQL! frm文件与idb文件恢复数据表","date":"2021-08-30T20:01:25.000Z","updated":"2024-10-04T01:18:10.904Z","comments":true,"path":"2021/08/30/MySQL! frm文件与idb文件恢复数据表/","link":"","permalink":"https://g-ydg.github.io/2021/08/30/MySQL!%20frm%E6%96%87%E4%BB%B6%E4%B8%8Eidb%E6%96%87%E4%BB%B6%E6%81%A2%E5%A4%8D%E6%95%B0%E6%8D%AE%E8%A1%A8/","excerpt":"","text":"安装 Mysqlfrm123456789101112131415#下载 mysql-utilities 软件包wget https://cdn.mysql.com/archives/mysql-utilities/mysql-utilities-1.6.5.tar.gztar zxf mysql-utilities-1.6.5.tar.gzcd mysql-utilities-1.6.5apt-get install python -ypython ./setup.py buildpython ./setup.py installmysqlfrm --version 生成 SQL1mysqlfrm --diagnostic ./frm文件目录/ >> sql.sql 数据库以及表结构创建生成的 SQL 语句会带有数据库,创建数据库时需进行对应,创建完数据库后,运行 SQL 语句进行表结构创建。 表空间卸载12## 单表ALTER TABLE 表名 DISCARD TABLESPACE; 12## 数据库下的所有表SELECT concat('alter table ', table_name,' discard tablespace;') FROM information_schema.tables WHERE table_schema ='数据库名'; 覆盖 idb 文件将需要恢复的 idb 文件复制到对应数据库文件夹下,覆盖掉默认创建的 idb 文件 修改文件权限12# 此步很重要chown -R mysql:mysql 数据库文件夹/* 导入表空间12## 单表ALTER TABLE 表名 IMPORT TABLESPACE; 12## 数据库下的所有表SELECT concat('alter table ', table_name,' import tablespace;') FROM information_schema.tables WHERE table_schema = '数据库名'; 参考https://pdf.us/2019/01/10/2620.html","categories":[],"tags":[]},{"title":"Mysql Too many connections","slug":"Mysql Too many connections","date":"2021-08-29T18:46:35.000Z","updated":"2024-10-04T01:18:10.908Z","comments":true,"path":"2021/08/29/Mysql Too many connections/","link":"","permalink":"https://g-ydg.github.io/2021/08/29/Mysql%20Too%20many%20connections/","excerpt":"","text":"命令行修改1234567891011121314151617181920212223## 登入mysqlmysql -u root -p## 查看连接数,可以发现有很多连接处于sleep状态,这些其实是暂时没有用的,所以可以kill掉show processlist;## 查看最大连接数,应该是与上面查询到的连接数相同,才会出现too many connections的情况show variables like "max_connections";## 修改最大连接数,但是这不是一劳永逸的方法,应该要让它自动杀死那些sleep的进程set GLOBAL max_connections=1000;## 这个数值指的是mysql在关闭一个非交互的连接之前要等待的秒数,默认是28800sshow global variables like 'wait_timeout';## 修改这个数值,这里可以随意,最好控制在几分钟内set global wait_timeout=300;## 这个数值指的mysql在关闭一个连接之前要等待的秒数,默认是28800sshow global variables like 'interactive_timeout';## 修改这个数值,可以让mysql自动关闭那些没用的连接,需注意正在使用的连接到了时间也会被关闭,因此这个时间值要合适set global interactive_timeout=500; 修改配置 my.cnf 文件1234[mysqld]max_connections=512wait_timeout=300interactive_timeout=500","categories":[],"tags":[]},{"title":"常见设计模式","slug":"常见设计模式","date":"2021-08-03T18:19:39.000Z","updated":"2024-10-04T01:18:12.830Z","comments":true,"path":"2021/08/03/常见设计模式/","link":"","permalink":"https://g-ydg.github.io/2021/08/03/%E5%B8%B8%E8%A7%81%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/","excerpt":"","text":"设计模式设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。而设计原则则是设计模式所遵循的规则,设计模式就是实现了这些原则,从而达到了代码复用、增加可维护性的目的。 常用设计模式策略模式策略模式是对象的行为模式,用意是对一组算法的封装。动态的选择需要的算法并使用。 策略模式是程序中涉及决策控制的一种模式。策略模式功能非常强大,因为这个设计模式本身的核心思想就是面向对象编程的多形性思想。 策略模式的三个角色1.抽象策略角色 2.具体策略角色 3.环境角色(对抽象策略角色的引用) 实现步骤1.定义抽象角色类(定义好各个实现的共同抽象方法) 2.定义具体策略类(具体实现父类的共同方法) 3.定义环境角色类(私有化申明抽象角色变量,重载构造方法,执行抽象方法) 就在编程领域之外,有许多例子是关于策略模式的。例如: 如果我需要在早晨从家里出发去上班,我可以有几个策略考虑:我可以乘坐地铁,乘坐公交车,走路或其它的途径。 每个策略可以得到相同的结果,但是使用了不同的资源。 策略模式的代码实例123456789101112131415161718192021222324252627282930313233343536373839<?phpabstract class baseAgent{ //抽象策略类 abstract function PrintPage();}//用于客户端是IE时调用的类(环境角色)class ieAgent extends baseAgent{ function PrintPage() { return 'IE'; }}//用于客户端不是IE时调用的类(环境角色)class otherAgent extends baseAgent{ function PrintPage() { return 'not IE'; }}class Browser{ //具体策略角色 public function call($object) { return $object->PrintPage(); }}$bro = new Browser ();echo $bro->call(new ieAgent ());?> 工厂模式工厂模式是我们最常用的实例化对象模式,是用工厂方法代替new操作的一种模式。 使用工厂模式的好处是,如果你想要更改所实例化的类名等,则只需更改该工厂方法内容即可,不需逐一寻找代码中,具体实例化的地方(new 处)修改了。为系统结构提供灵活的动态扩展机制,减少了耦合。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253<?phpheader('Content-Type:text/html;charset=utf-8');/***简单工厂模式(静态工厂方法模式) *//*** Interface people 人类 */interface people{ public function say();}/*** Class man 继承people的男人类 */class man implements people{ // 具体实现people的say方法 public function say() { echo '我是男人<br>'; }}/*** Class women 继承people的女人类 */class women implements people{ // 具体实现people的say方法 public function say() { echo '我是女人<br>'; }}/*** Class SimpleFactoty 工厂类 */class SimpleFactoty{ // 简单工厂里的静态方法-用于创建男人对象 static function createMan() { return new man(); } // 简单工厂里的静态方法-用于创建女人对象 static function createWomen() { return new women(); }}/*** 具体调用 */$man = SimpleFactoty::createMan();$man->say();$woman = SimpleFactoty::createWomen();$woman->say(); 单例模式单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。 单例模式是一种常见的设计模式,在计算机系统中,线程池、缓存、日志对象、对话框、打印机、数据库操作、显卡的驱动程序常被设计成单例。 单例模式分3种:懒汉式单例、饿汉式单例、登记式单例。 单例模式有以下3个特点: 1.只能有一个实例。 2.必须自行创建这个实例。 3.必须给其他对象提供这一实例。 那么为什么要使用PHP单例模式? PHP 一个主要应用场合就是应用程序与数据库打交道的场景,在一个应用中会存在大量的数据库操作,针对数据库句柄连接数据库的行为,使用单例模式可以避免大量的 new 操作。因为每一次 new 操作都会消耗系统和内存的资源。 123456789101112131415161718192021222324252627282930313233343536373839<?phpclass Single{ private $name; //声明一个私有的实例变量 private function __construct() { //声明私有构造方法为了防止外部代码使用new来创建对象。 } static public $instance;//声明一个静态变量(保存在类中唯一的一个实例) static public function getInstance() { //声明一个getInstance()静态方法,用于检测是否有实例对象 if (!self::$instance) self::$instance = new self(); return self::$instance; } public function setName($n) { $this->name = $n; } public function getName() { return $this->name; }}$oa = Single::getInstance();$ob = Single::getInstance();$oa->setName('hello world');$ob->setName('good morning');echo $oa->getName(); //good morningecho $ob->getName(); //good morning 注册模式注册模式,解决全局共享和交换对象。已经创建好的对象,挂在到某个全局可以使用的数组上,在需要使用的时候, 直接从该数组上获取即可。将对象注册到全局的树上。任何地方直接去访问。 123456789101112131415161718192021<?phpclass Register{ protected static $objects; function set($alias, $object) //将对象注册到全局的树上 { self::$objects[$alias] = $object; //将对象放到树上 } static function get($name) { return self::$objects[$name]; //获取某个注册到树上的对象 } function _unset($alias) { unset(self::$objects[$alias]); //移除某个注册到树上的对象。 }} 适配器模式将各种截然不同的函数接口封装成统一的 API。 PHP 中的数据库操作有 MySQL,MySQLi,PDO 三种,可以用适配器模式统一成一致,使不同的数据库操作,统一成一样的 API。类似的场景还有 cache 适配器,可以将 memcache,redis,fifile,apc 等不同的缓存函数,统一成一致。 首先定义一个接口(有几个方法,以及相应的参数)。然后,有几种不同的情况,就写几个类实现该接口。将完成相似功能的函数,统一成一致的方法。 12345678910111213#接口 IDatabase<?phpnamespace App;interface IDatabase{ function connect($host, $user, $passwd, $dbname); function query($sql); function close();} MySQL 12345678910111213141516171819202122232425262728<?phpnamespace App\\Database;use App\\IDatabase;class MySQL implements IDatabase{ protected $conn; function connect($host, $user, $passwd, $dbname) { $conn = mysql_connect($host, $user, $passwd); mysql_select_db($dbname, $conn); $this->conn = $conn; } function query($sql) { $res = mysql_query($sql, $this->conn); return $res; } function close() { mysql_close($this->conn); }} MySQLi 1234567891011121314151617181920212223242526<?phpnamespace App\\Database;use App\\IDatabase;class MySQLi implements IDatabase{ protected $conn; function connect($host, $user, $passwd, $dbname) { $conn = mysqli_connect($host, $user, $passwd, $dbname); $this->conn = $conn; } function query($sql) { return mysqli_query($this->conn, $sql); } function close() { mysqli_close($this->conn); }} 观察者模式 观察者模式(Observer),当一个对象状态发生变化时,依赖它的对象全部会收到通知,并自动更新。 事件发生后,要执行一连串更新操作。传统的编程方式,就是在事件的代码之后直接加入处理的逻辑。当更新的逻辑增多之后,代码会变得难以维护。这种方式是耦合的,侵入式的,增加新的逻辑需要修改事件的主体代码。 观察者模式实现了低耦合,非侵入式的通知与更新机制。 定义一个事件触发抽象类 123456789101112131415161718192021#EventGenerator.php<?phprequire_once 'Loader.php';abstract class EventGenerator{ private $observers = array(); function addObserver(Observer $observer) { $this->observers[] = $observer; } function notify() { foreach ($this->observers as $observer) { $observer->update(); } }} 定义一个观察者接口 12345678910#Observer.php<?phprequire_once 'Loader.php';interface Observer{ function update(); //这里就是在事件发生后要执行的逻辑}//一个实现了EventGenerator抽象类的类,用于具体定义某个发生的事件 实现: 12345678910111213141516171819202122232425262728293031323334#Event.php<?phprequire 'Loader.php';class Event extends EventGenerator{ function trigger() { echo "Event<br>"; }}class Observer1 implements Observer{ function update() { echo "逻辑1<br>"; }}class Observer2 implements Observer{ function update() { echo "逻辑2<br>"; }}$event = new Event();$event->addObserver(new Observer1());$event->addObserver(new Observer2());$event->trigger();$event->notify(); 六大设计原则单一职责原则(Single Responsibility Principle - SRP)一个类,只有一个引起它变化的原因。应该只有一个职责。每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。例如:要实现逻辑和界面的分离。 开放封闭原则(Open Closed Principle - OCP)软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。 封装变化,是实现开放封闭原则的重要手段,对于经常发生变化的状态一般将其封装为一个抽象。 拒绝滥用抽象,只将经常变化的部分进行抽象,这种经验可以从设计模式的学习与应用中获得。 里氏替换原则(Liskov Substitution Principle - LSP)里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下 4 层含义 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。 子类中可以增加自己特有的方法。 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格 最少知识原则(Least Knowledge Principle - LKP)最少知识原则又叫迪米特法则。核心思想是:低耦合、高内聚 一个实体应当尽量少的与其他实体之间发生相互作 用,使得系统功能模块相对独立。也就是说一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易,这是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度。 接口隔离原则(Interface Segregation Principle - ISP)接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。 采用接口隔离原则对接口进行约束时,要注意以下几点: 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。 依赖倒置原则(Dependence Inversion Principle - DIP)依赖倒置原则的核心思想是面向接口编程,不应该面向实现类编程。 在实际编程中,要做到下面 3 点: 低层模块尽量都要有抽象类或接口,或者两者都有。 变量的声明类型尽量是抽象类或接口。 使用继承时遵循里氏替换原则。","categories":[],"tags":[]},{"title":"iGG谷歌访问助手","slug":"iGG谷歌访问助手","date":"2021-07-25T23:17:20.000Z","updated":"2024-10-04T01:18:12.836Z","comments":true,"path":"2021/07/25/iGG谷歌访问助手/","link":"","permalink":"https://g-ydg.github.io/2021/07/25/iGG%E8%B0%B7%E6%AD%8C%E8%AE%BF%E9%97%AE%E5%8A%A9%E6%89%8B/","excerpt":"","text":"iGG 谷歌访问助手 使用 Google 搜索、Gmail、Chrome 商店等谷歌服务 插件安装教程","categories":[],"tags":[]},{"title":"Github国内访问慢?","slug":"Github国内访问慢?","date":"2021-07-20T17:14:45.000Z","updated":"2024-10-04T01:18:14.419Z","comments":true,"path":"2021/07/20/Github国内访问慢?/","link":"","permalink":"https://g-ydg.github.io/2021/07/20/Github%E5%9B%BD%E5%86%85%E8%AE%BF%E9%97%AE%E6%85%A2%EF%BC%9F/","excerpt":"","text":"修改 HOST由于网络代理商的原因,github 的 CDN 被某墙屏了,所以访问下载很慢。咱们可在本地直接绑定 host,绕过 dns 解析,该方法也可加速其他因为 CDN 被屏蔽导致访问慢的网站。 查询网址 IP 访问https://www.ipaddress.com,分别输入 github.com 和 github.global.ssl.fastly.net,查询 IP 地址。 修改 hosts 文件 windows 系统的 hosts 文件的位置如下:C:\\Windows\\System32\\drivers\\etc\\hosts mac/linux 系统的 hosts 文件的位置如下:/etc/hosts 123# github199.232.5.194 github.global.ssl.fastly.net140.82.114.3 github.com 更新本地 DNS 缓存 windows 1ipconfig /flushdns linux(unbantu 为例) 12345# 通过nscd刷新缓存sudo /etc/init.d/nscd restart# 若报命令不存在,进行安装sudo apt-get install nscd mac 1sudo killall -HUP mDNSResponder 使用镜像网站常用镜像网站https://hub.fastgit.org使用例如要访问项目https://github.com/jaywcjlove/linux-command 只需将路径中的 https://github.com 修改为 https://hub.fastgit.org 即可 Github 加速器Fast-GitHub 插件,具体使用点击查看。","categories":[],"tags":[]},{"title":"Hyperf Http协程体验记录","slug":"Hyperf Http协程体验记录","date":"2021-06-14T22:10:23.000Z","updated":"2024-10-04T01:18:14.424Z","comments":true,"path":"2021/06/14/Hyperf Http协程体验记录/","link":"","permalink":"https://g-ydg.github.io/2021/06/14/Hyperf%20Http%E5%8D%8F%E7%A8%8B%E4%BD%93%E9%AA%8C%E8%AE%B0%E5%BD%95/","excerpt":"","text":"1ab -n 1000 -c 1000 http://127.0.0.1:9501/order/list 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354This is ApacheBench, Version 2.3 <$Revision: 1843412 $>Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/Licensed to The Apache Software Foundation, http://www.apache.org/Benchmarking 127.0.0.1 (be patient)Completed 100 requestsCompleted 200 requestsCompleted 300 requestsCompleted 400 requestsCompleted 500 requestsCompleted 600 requestsCompleted 700 requestsCompleted 800 requestsCompleted 900 requestsCompleted 1000 requestsFinished 1000 requestsServer Software: HyperfServer Hostname: 127.0.0.1Server Port: 9501Document Path: /order/listDocument Length: 19202 bytesConcurrency Level: 1000Time taken for tests: 8.118 secondsComplete requests: 1000Failed requests: 768 (Connect: 0, Receive: 0, Length: 768, Exceptions: 0)Total transferred: 4648120 bytesHTML transferred: 4489424 bytesRequests per second: 160.19 [#/sec] (mean)Time per request: 8117.578 [ms] (mean)Time per request: 8.118 [ms] (mean, across all concurrent requests)Transfer rate: 559.18 [Kbytes/sec] receivedConnection Times (ms) min mean[+/-sd] median maxConnect: 0 1 16.1 0 511Processing: 687 4345 1399.1 5053 5336Waiting: 679 4342 1401.3 5052 5336Total: 687 4345 1399.5 5053 5520Percentage of the requests served within a certain time (ms) 50% 5053 66% 5065 75% 5077 80% 5139 90% 5274 95% 5300 98% 5318 99% 5324 100% 5520 (longest request) 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253This is ApacheBench, Version 2.3 <$Revision: 1843412 $>Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/Licensed to The Apache Software Foundation, http://www.apache.org/Benchmarking 127.0.0.1 (be patient)Completed 100 requestsCompleted 200 requestsCompleted 300 requestsCompleted 400 requestsCompleted 500 requestsCompleted 600 requestsCompleted 700 requestsCompleted 800 requestsCompleted 900 requestsCompleted 1000 requestsFinished 1000 requestsServer Software: HyperfServer Hostname: 127.0.0.1Server Port: 9501Document Path: /order/listDocument Length: 19518 bytesConcurrency Level: 1000Time taken for tests: 1.681 secondsComplete requests: 1000Failed requests: 0Total transferred: 19679000 bytesHTML transferred: 19518000 bytesRequests per second: 594.93 [#/sec] (mean)Time per request: 1680.876 [ms] (mean)Time per request: 1.681 [ms] (mean, across all concurrent requests)Transfer rate: 11433.19 [Kbytes/sec] receivedConnection Times (ms) min mean[+/-sd] median maxConnect: 0 0 0.4 0 5Processing: 415 1195 156.3 1236 1489Waiting: 152 1191 157.9 1234 1489Total: 416 1195 156.3 1236 1489Percentage of the requests served within a certain time (ms) 50% 1236 66% 1278 75% 1298 80% 1308 90% 1365 95% 1409 98% 1431 99% 1456 100% 1489 (longest request)","categories":[],"tags":[]},{"title":"常见的PHP漏洞","slug":"常见的PHP漏洞","date":"2021-06-10T18:23:37.000Z","updated":"2024-10-04T01:18:14.479Z","comments":true,"path":"2021/06/10/常见的PHP漏洞/","link":"","permalink":"https://g-ydg.github.io/2021/06/10/%E5%B8%B8%E8%A7%81%E7%9A%84PHP%E6%BC%8F%E6%B4%9E/","excerpt":"","text":"一、md5 加密漏洞 比较哈希字符串的时候,php 程序把每一个以“0x”开头的哈希值都解释为科学计数法 0 的多少次方,恒为 0 所以如果两个不同的密码经过哈希以后,其哈希值都是以“0e”开头的,那么 php 将会认为他们相同。 另外 md5 加密是有几率两个字符串不同,但是加密后的值是相同的情况,这种情况称为哈希碰撞 12345678<?php$str1 = 's878926199a';$str2 = 's214587387a';echo json_encode(['md5_str1' => md5($str1),'md5_str2' => md5($str2),'bool' => md5($str1) == md5($str2)]); 结果如下,两个值加密后竟然相等 缺点你懂的,如果一个网站的某个用户密码加密后刚好是 0e 开头的,这个时候黑客过来破解,很容易就攻入了 12345{ "md5_str1": "0e545993274517709034328855841020", "md5_str2": "0e848240448830537924465865611904", "bool": true} 二、is_numeric 漏洞会忽视 0x 这种十六进制的数 容易引发 sql 注入操作,暴漏敏感信息 1234567echo json_encode([ is_numeric(233333), is_numeric('233333'), is_numeric(0x233333), is_numeric('0x233333'), is_numeric('233333abc'),]); 结果如下 16 进制数 0x61646D696EASII 码对应的值是 admin 如果我们执行了后面这条命令的话:SELECT * FROM tp_user where username=0x61646D696E,结果不言而喻 12345678[ true, true, true, false, false] 三、in_array 漏洞in_array 中是先将类型转为整形,再进行判断 PHP 作为弱类型语言,类型转换的时候,会有很大漏洞 转换的时候,如果将字符串转换为整形,从字符串非整形的地方截止转换,如果无法转换,将会返回 0 123<?phpvar_dump(in_array("2%20and%20%", [0,2,3])); 结果如下 1bool(true) 四、switch 漏洞switch 中是先将类型转为整形,再进行判断 PHP 作为弱类型语言,类型转换的时候,会有很大漏洞 转换的时候,如果将字符串转换为整形,从字符串非整形的地方截止转换,如果无法转换,将会返回 0 123456789101112<?php$i ="abc";switch ($i) { case 0: case 1: case 2: echo "i是比3小的数"; break; case 3: echo "i等于3";} 结果如下 i 是比 3 小的数 五、intval 强转漏洞PHP 作为弱类型语言,类型转换的时候,会有很大漏洞 转换的时候,如果将字符串转换为整形,从字符串非整形的地方截止转换,如果无法转换,将会返回 0 12345<?phpvar_dump(intval('2')); //2var_dump(intval('3abcd')); //3var_dump(intval('abcd')); //0","categories":[],"tags":[]},{"title":"提高PHP脚本性能的小技巧","slug":"提高PHP脚本性能的小技巧","date":"2021-06-10T18:01:41.000Z","updated":"2024-10-04T01:18:14.493Z","comments":true,"path":"2021/06/10/提高PHP脚本性能的小技巧/","link":"","permalink":"https://g-ydg.github.io/2021/06/10/%E6%8F%90%E9%AB%98PHP%E8%84%9A%E6%9C%AC%E6%80%A7%E8%83%BD%E7%9A%84%E5%B0%8F%E6%8A%80%E5%B7%A7/","excerpt":"","text":"去除重复项场景一个存有重复项的大型数组,需要去除重复项,仅留取唯一值的数组 常用1array_unique($array); 替代1array_keys(array_flip($array)); 获取随机数组记录场景存有大型数组,需随机取出一个值 常用1array_rand($array); 替代1$array[mt_rand(0, count($array) - 1)]; 字母数字字符验证场景存有一个字符串,需验证它是否仅包含字母数字字符串。 常用1preg_match('/^[a-zA-Z0-9]+$/', $string); 替代1ctype_alnum($string); tip:ctype_alpha()(检查字母字符)和 ctype_digit()(检查数字字符) 替换子字符串场景存有一个字符串,需进行匹配替换 常用1str_replace('a', 'b', $string); 替代1strtr($string, 'a', 'b');","categories":[],"tags":[]},{"title":"常见代码重构技巧","slug":"常见代码重构技巧","date":"2021-05-18T00:11:47.000Z","updated":"2024-10-04T01:18:24.502Z","comments":true,"path":"2021/05/18/常见代码重构技巧/","link":"","permalink":"https://g-ydg.github.io/2021/05/18/%E5%B8%B8%E8%A7%81%E4%BB%A3%E7%A0%81%E9%87%8D%E6%9E%84%E6%8A%80%E5%B7%A7/","excerpt":"","text":"关于重构为什么要重构 项目在不断演进过程中,代码不停地在堆砌。如果没有人为代码的质量负责,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。 造成这样的原因往往有以下几点: 编码之前缺乏有效的设计 成本上的考虑,在原功能堆砌式编程 缺乏有效代码质量监督机制 对于此类问题,业界已有有很好的解决思路:通过持续不断的重构将代码中的“坏味道”清除掉。 什么是重构重构一书的作者 Martin Fowler 对重构的定义: 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。 根据重构的规模可以大致分为大型重构和小型重构: 大型重构:对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。这类重构的工具就是我们学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入 bug 的风险也会相对比较大。 小型重构:对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名和注释、消除超大类或函数、提取重复代码等等。小型重构更多的是使用统一的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入 bug 的风险相对来说也会比较小。 对于小型重构,我们在新功能开发、修 bug 或者代码 review 中出现“代码坏味道”,我们就应该及时进行重构。持续在日常开发中进行小重构,能够降低重构和测试的成本。 代码的坏味道 代码重复 实现逻辑相同、执行流程相同 方法过长 方法中的语句不在同一个抽象层级 逻辑难以理解,需要大量的注释 面向过程编程而非面向对象 过大的类 类做了太多的事情 包含过多的实例变量和方法 类的命名不足以描述所做的事情 逻辑分散 发散式变化:某个类经常因为不同的原因在不同的方向上发生变化 散弹式修改:发生某种变化时,需要在多个类中做修改 严重的情结依恋 某个类的方法过多的使用其他类的成员 数据泥团/基本类型偏执 两个类、方法签名中包含相同的字段或参数 应该使用类但使用基本类型,比如表示数值与币种的 Money 类、起始值与结束值的 Range 类 不合理的继承体系 继承打破了封装性,子类依赖其父类中特定功能的实现细节 子类必须跟着其父类的更新而演变,除非父类是专门为了扩展而设计,并且有很好的文档说明 过多的条件判断 过长的参数列 临时变量过多 令人迷惑的暂时字段 某个实例变量仅为某种特定情况而设置 将实例变量与相应的方法提取到新的类中 纯数据类 仅包含字段和访问(读写)这些字段的方法 此类被称为数据容器,应保持最小可变性 不恰当的命名 命名无法准确描述做的事情 命名不符合约定俗称的惯例 过多的注释 坏代码的问题 难以复用 系统关联性过多,导致很难分离可重用部分 难于变化 一处变化导致其他很多部分的修改,不利于系统稳定 难于理解 命名杂乱,结构混乱,难于阅读和理解 难以测试 分支、依赖较多,难以覆盖全面 什么是好代码 代码质量的评价有很强的主观性,描述代码质量的词汇也有很多,比如可读性、可维护性、灵活、优雅、简洁。这些词汇是从不同的维度去评价代码质量的。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。 要写出高质量代码,我们就需要掌握一些更加细化、更加能落地的编程方法论,这就包含面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。 如何重构SOLID 原则 单一职责原则一个类只负责完成一个职责或者功能,不要存在多于一种导致类变更的原因。 单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、松耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。 开放-关闭原则添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。 开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。 很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。 里氏替换原则子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。 子类可以扩展父类的功能,但不能改变父类原有的功能 父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。 接口隔离原则调用方不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。 接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。 依赖反转原则高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。 迪米特法则一个对象应该对其他对象保持最少的了解 合成复用原则尽量使用合成/聚合的方式,而不是使用继承。 单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。 设计模式设计模式:软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案。 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。 创建型:主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码 结构型:主要通过类或对象的不同组合,解耦不同功能的耦合 行为型:主要解决的是类或对象之间的交互行为的耦合 类型 模式 说明 适用场景 创建型 单例 一个类只允许创建一个实例或对象,并为其提供一个全局的访问点 无状态/全局唯一/控制资源访问 工厂 创建一个或者多个相关的对象,而使用者不用关心具体的实现类 分离对象的创建和使用 建造者 用于创建一种类型的复杂对象,通过设置不同的可选参数进行“定制化” 对象的构造参数较多且多数可选 原型 通过复制已有对象来创建新的对象 对象的创建成本较大且同一类的不同对象之前差别不大 结构型 代理 不改变原始类和不使用继承的情况下,通过引入代理类来给原始类附加功能 增加代理访问,比如监控、缓存、限流、事务、RPC 装饰者 不改变原始类和不使用继承的情况下,通过组合的方式动态扩展原始类的功能 动态扩展类的功能 适配器 不改变原始类的情况下,通过组合的方式使其适配新的接口 复用现有类,但与期望接口不适配 桥接 当类存在多个独立变化的维度时,通过组合的方式使得其可以独立进行扩展 存在多个维度的继承体系时 门面 为子系统中一组接口定义一个更高层的接口,使得子系统更加容易使用 解决接口复用性(细粒度)与接口易用性(粗粒度)的矛盾 组合 将对象组合成树形结构以表示部分-整体的层次结构,统一单个对和组合对象的处理逻辑 满足部分与整体这种树形结构 享元 运用共享技术有效地支持大量细粒度的对象 当系统存在大量的对象,这些对象的很多字段取值范围固定 行为型 观察者 多个观察者监听同一主题对象,当主题对象状态发生变化时通知所有观察者,使它们能够自动更新自己 解耦事件创建者与接收者 模板 定义一个操作中算法的骨架,将某些步骤实现延迟到子类中 解决复用与扩展问题 策略 定义一组算法类,将每个算法分别封装起来,使得它们可以互相替换 消除各种 if-else 分支判断解耦策略的定义、创建、使用 状态 允许一个对象在其内部状态改变的时候改变其行为 分离对象的状态与行为 职责链 将一组对象连成一条链,请求沿着该链传递,直到某个对象能够处理它为止 解耦请求的发送者与接收者 迭代器 提供一种方法顺序访问一个集合对象的各个元素,但不暴露该对象的内部表示 解耦集合对象的内部表示与遍历访问 访问者 封装一些作用于某种数据结构中各元素的操作,在不改变数据结构的前提下,定义作用于这些元素的新操作。 分离对象的数据结构与行为 备忘录 在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态 用于对象的备份与恢复 命令 将不同的请求封装成对应的命令对象,对命令的执行进行控制且对使用方透明 用于控制命令的执行,比如异步、延迟、排队、撤销、存储与撤销 解释器 为某个语言定义它的语法表示,并定义一个解释器来处理这个语法 用于编译器、规则引擎、正则表达式等特定场景 中介 定义一个单独的中介对象,来封装一组对象之间的交互,避免对象之间的直接交互 使各个对象不需要显式地相互引用,从而使其耦合松散 代码分层 模块结构说明 server_main:配置层,负责整个项目的 module 管理,maven 配置管理、资源管理等; server_application:应用接入层,承接外部流量入口,例如:RPC 接口实现、消息处理、定时任务等;不要在此包含业务逻辑; server_biz:核心业务层,用例服务、领域实体、领域事件等 server_irepository:资源接口层,负责资源接口的暴露 server_repository:资源层,负责资源的 proxy 访问,统一外部资源访问,隔离变化。注意:这里强调的是弱业务性,强数据性; server_common:公共层,vo、工具等 代码开发要遵守各层的规范,并注意层级之间的依赖关系。 命名规范一个好的命名应该要满足以下两个约束: 准确描述所做得事情 格式符合通用的惯例 如果你觉得一个类或方法难以命名的时候,可能是其承载的功能太多了,需要进一步拆分。 约定俗称的惯例 场景 强约束 示例 项目名 全部小写,多个单词用中划线分隔‘-’ spring-cloud 包名 全部小写 com.alibaba.fastjson 类名/接口名 单词首字母大写 ParserConfig,DefaultFieldDeserializer 变量名 首字母小写,多个单词组成时,除首个单词,其他单词首字母都要大写 password, userName 常量名 全部大写,多个单词,用’_‘分隔 CACHE_EXPIRED_TIME 方法 同变量 read(), readObject(), getById() 类命名类名使用大驼峰命名形式,类命通常使用名词或名词短语。接口名除了用名词和名词短语以外,还可以使用形容词或形容词短语,如 Cloneable,Callable 等,表示实现该接口的类有某种功能或能力。 场景 约束 示例 抽象类 Abstract 或者 Base 开头 BaseUserService 枚举类 Enum 作为后缀 GenderEnum 工具类 Utils 作为后缀 StringUtils 异常类 Exception 结尾 RuntimeException 接口实现类 接口名+ Impl UserServiceImpl 设计模式相关类 Builder,Factory 等 当使用到设计模式时,需要使用对应的设计模式作为后缀,如 ThreadFactory 处理特定功能的类 Handler,Predicate, Validator 表示处理器,校验器,断言,这些类工厂还有配套的方法名如 handle,predicate,validate 特定层级的类 Controller,Service,ServiceImpl,Dao 后缀 UserController, UserServiceImpl,UserDao 特定层级的值对象 Ao, Param, Vo,Config, Message Param 调用入参;Ao 为 thrift 返回结果;Vo 通用值对象;Config 配置类;Message 为 MQ 消息 测试类 Test 结尾 UserServiceTest, 表示用来测试 UserService 类的 方法命名方法命名采用小驼峰的形式,首字小写,往后的每个单词首字母都要大写。和类名不同的是,方法命名一般为动词或动词短语,与参数或参数名共同组成动宾短语,即动词 + 名词。一个好的函数名一般能通过名字直接获知该函数实现什么样的功能。 场景 约束 示例 返回真伪值 is/can/has/needs/should isValid/canRemove 用于检查 ensure/validate ensureCapacity/validateInputs 按需执行 IfNeeded/try/OrDefault/OrElse drawIfNeeded/tryCreate/getOrDefault 数据相关 get/search/save/update/batchSave/batchUpdate/saveOrUpdateselect/insert/update/delete getUserById/searchUsersByCreateTime 生命周期 initialize/pause/stop/destroy initialize/pause/onPause/stop/onStop 常用动词对 split/join、inject/extract、bind/seperate、increase/decrease、lanch/run、observe/listen、build/publish、encode/decode、submit/commit、push/pull、enter/exit、expand/collapse、encode/decode 重构技巧提炼方法多个方法代码重复、方法中代码过长或者方法中的语句不在一个抽象层级。方法是代码复用的最小粒度,方法过长不利于复用,可读性低,提炼方法往往是重构工作的第一步。 意图导向编程:把处理某件事的流程和具体做事的实现方式分开。 把一个问题分解为一系列功能性步骤,并假定这些功能步骤已经实现 我们只需把把各个函数组织在一起即可解决这一问题 在组织好整个功能后,我们在分别实现各个方法函数 /** _ 1、交易信息开始于一串标准 ASCII 字符串。 _ 2、这个信息字符串必须转换成一个字符串的数组,数组存放的此次交易的领域语言中所包含的词汇元素(token)。 _ 3、每一个词汇必须标准化。 _ 4、包含超过 150 个词汇元素的交易,应该采用不同于小型交易的方式(不同的算法)来提交,以提高效率。 _ 5、如果提交成功,API 返回”true”;失败,则返回”false”。 _/public class Transaction { public Boolean commit(String command) { Boolean result = true; String[] tokens = tokenize(command); normalizeTokens(tokens); if (isALargeTransaction(tokens)) { result = processLargeTransaction(tokens); } else { result = processSmallTransaction(tokens); } return result; }}复制代码 以函数对象取代函数将函数放进一个单独对象中,如此一来局部变量就变成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解为多个小型函数。 引入参数对象方法参数比较多时,将参数封装为参数对象 移除对参数的赋值public int discount(int inputVal, int quantity, int yearToDate) { if (inputVal > 50) inputVal -= 2; if (quantity > 100) inputVal -= 1; if (yearToDate > 10000) inputVal -= 4; return inputVal;} public int discount(int inputVal, int quantity, int yearToDate) { int result = inputVal; if (inputVal > 50) result -= 2; if (quantity > 100) result -= 1; if (yearToDate > 10000) result -= 4; return result;}复制代码 将查询与修改分离任何有返回值的方法,都不应该有副作用 不要在 convert 中调用写操作,避免副作用 常见的例外:将查询结果缓存到本地 移除不必要临时变量临时变量仅使用一次或者取值逻辑成本很低的情况下 引入解释性变量将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途 if ((platform.toUpperCase().indexOf(“MAC”) > -1) && (browser.toUpperCase().indexOf(“IE”) > -1) && wasInitialized() && resize > 0) { // do something} final boolean isMacOs = platform.toUpperCase().indexOf(“MAC”) > -1;final boolean isIEBrowser = browser.toUpperCase().indexOf(“IE”) > -1;final boolean wasResized = resize > 0;if (isMacOs && isIEBrowser && wasInitialized() && wasResized) { // do something}复制代码 使用卫语句替代嵌套条件判断把复杂的条件表达式拆分成多个条件表达式,减少嵌套。嵌套了好几层的 if - then-else 语句,转换为多个 if 语句 //未使用卫语句public void getHello(int type) { if (type == 1) { return; } else { if (type == 2) { return; } else { if (type == 3) { return; } else { setHello(); } } }} //使用卫语句public void getHello(int type) { if (type == 1) { return; } if (type == 2) { return; } if (type == 3) { return; } setHello();}复制代码 使用多态替代条件判断断当存在这样一类条件表达式,它根据对象类型的不同选择不同的行为。可以将这种表达式的每个分支放进一个子类内的复写函数中,然后将原始函数声明为抽象函数。 public int calculate(int a, int b, String operator) { int result = Integer.MIN_VALUE; if (“add”.equals(operator)) { result = a + b; } else if (“multiply”.equals(operator)) { result = a * b; } else if (“divide”.equals(operator)) { result = a / b; } else if (“subtract”.equals(operator)) { result = a - b; } return result;}复制代码 当出现大量类型检查和判断时,if else(或 switch)语句的体积会比较臃肿,这无疑降低了代码的可读性。 另外,if else(或 switch)本身就是一个“变化点”,当需要扩展新的类型时,我们不得不追加 if else(或 switch)语句块,以及相应的逻辑,这无疑降低了程序的可扩展性,也违反了面向对象的开闭原则。 基于这种场景,我们可以考虑使用“多态”来代替冗长的条件判断,将 if else(或 switch)中的“变化点”封装到子类中。这样,就不需要使用 if else(或 switch)语句了,取而代之的是子类多态的实例,从而使得提高代码的可读性和可扩展性。很多设计模式使用都是这种套路,比如策略模式、状态模式。 public interface Operation { int apply(int a, int b);} public class Addition implements Operation { @Override public int apply(int a, int b) { return a + b; }} public class OperatorFactory { private final static Map<String, Operation> operationMap = new HashMap<>(); static { operationMap.put(“add”, new Addition()); operationMap.put(“divide”, new Division()); // more operators } public static Operation getOperation(String operator) { return operationMap.get(operator); }} public int calculate(int a, int b, String operator) { if (OperatorFactory .getOperation == null) { throw new IllegalArgumentException(“Invalid Operator”); } return OperatorFactory .getOperation(operator).apply(a, b);}复制代码 使用异常替代返回错误码非正常业务状态的处理,使用抛出异常的方式代替返回错误码 不要使用异常处理用于正常的业务流程控制 异常处理的性能成本非常高 尽量使用标准异常 避免在 finally 语句块中抛出异常 如果同时抛出两个异常,则第一个异常的调用栈会丢失 finally 块中应只做关闭资源这类的事情 //使用错误码public boolean withdraw(int amount) { if (balance < amount) { return false; } else { balance -= amount; return true; }} //使用异常public void withdraw(int amount) { if (amount > balance) { throw new IllegalArgumentException(“amount too large”); } balance -= amount;}复制代码 引入断言某一段代码需要对程序状态做出某种假设,以断言明确表现这种假设。 不要滥用断言,不要使用它来检查“应该为真”的条件,只使用它来检查“一定必须为真”的条件 如果断言所指示的约束条件不能满足,代码是否仍能正常运行?如果可以就去掉断言 引入 Null 对象或特殊对象当使用一个方法返回的对象时,而这个对象可能为空,这个时候需要对这个对象进行操作前,需要进行判空,否则就会报空指针。当这种判断频繁的出现在各处代码之中,就会影响代码的美观程度和可读性,甚至增加 Bug 的几率。 空引用的问题在 Java 中无法避免,但可以通过代码编程技巧(引入空对象)来改善这一问题。 //空对象的例子public class OperatorFactory { static Map<String, Operation> operationMap = new HashMap<>(); static { operationMap.put(“add”, new Addition()); operationMap.put(“divide”, new Division()); // more operators } public static Optional getOperation(String operator) { return Optional.ofNullable(operationMap.get(operator)); }}public int calculate(int a, int b, String operator) { Operation targetOperation = OperatorFactory.getOperation(operator) .orElseThrow(() -> new IllegalArgumentException(“Invalid Operator”)); return targetOperation.apply(a, b);} //特殊对象的例子public class InvalidOp implements Operation { @Override public int apply(int a, int b) { throw new IllegalArgumentException(“Invalid Operator”); }}复制代码 提炼类根据单一职责原则,一个类应该有明确的责任边界。但在实际工作中,类会不断的扩展。当给某个类添加一项新责任时,你会觉得不值得分离出一个单独的类。于是,随着责任不断增加,这个类包含了大量的数据和函数,逻辑复杂不易理解。 此时你需要考虑将哪些部分分离到一个单独的类中,可以依据高内聚低耦合的原则。如果某些数据和方法总是一起出现,或者某些数据经常同时变化,这就表明它们应该放到一个类中。另一种信号是类的子类化方式:如果你发现子类化只影响类的部分特性,或者类的特性需要以不同方式来子类化,这就意味着你需要分解原来的类。 //原始类public class Person { private String name; private String officeAreaCode; private String officeNumber; public String getName() { return name; } public String getTelephoneNumber() { return (“(“ + officeAreaCode + “)” + officeNumber); } public String getOfficeAreaCode() { return officeAreaCode; } public void setOfficeAreaCode(String arg) { officeAreaCode = arg; } public String getOfficeNumber() { return officeNumber; } public void setOfficeNumber(String arg) { officeNumber = arg; }} //新提炼的类(以对象替换数据值)public class TelephoneNumber { private String areaCode; private String number; public String getTelephnoeNumber() { return (“(“ + getAreaCode() + “)” + number); } String getAreaCode() { return areaCode; } void setAreaCode(String arg) { areaCode = arg; } String getNumber() { return number; } void setNumber(String arg) { number = arg; }}复制代码 组合优先于继承继承使实现代码重用的有力手段,但这并非总是完成这项工作的最佳工具,使用不当会导致软件变得很脆弱。与方法调用不同的是,继承打破了封装性。子类依赖于其父类中特定功能的实现细节,如果父类的实现随着发行版本的不同而变化,子类可能会遭到破坏,即使他的代码完全没有改变。 举例说明,假设有一个程序使用 HashSet,为了调优该程序的性能,需要统计 HashSet 自从它创建以来添加了多少个元素。为了提供该功能,我们编写一个 HashSet 的变体。 // Inappropriate use of inheritance!public class InstrumentedHashSet extends HashSet { // The number of attempted element insertions private int addCount = 0; public InstrumentedHashSet() { } public InstrumentedHashSet(int initCap, float loadFactor) { super(initCap, loadFactor); } @Override public boolean add(E e) { addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; }}复制代码 通过在新的类中增加一个私有域,它引用现有类的一个实例,这种设计被称为组合,因为现有的类变成了新类的一个组件。这样得到的类将会非常稳固,它不依赖现有类的实现细节。即使现有的类添加了新的方法,也不会影响新的类。许多设计模式使用就是这种套路,比如代理模式、装饰者模式 // Reusable forwarding classpublic class ForwardingSet implements Set { private final Set s; public ForwardingSet(Set s) { this.s = s; } @Override public int size() { return s.size(); } @Override public boolean isEmpty() { return s.isEmpty(); } @Override public boolean contains(Object o) { return s.contains(o); } @Override public Iterator iterator() { return s.iterator(); } @Override public Object[] toArray() { return s.toArray(); } @Override public T[] toArray(T[] a) { return s.toArray(a); } @Override public boolean add(E e) { return s.add(e); } @Override public boolean remove(Object o) { return s.remove(o); } @Override public boolean containsAll(Collection c) { return s.containsAll(c); } @Override public boolean addAll(Collection c) { return s.addAll(c); } @Override public boolean retainAll(Collection c) { return s.retainAll(c); } @Override public boolean removeAll(Collection<?> c) { return s.removeAll(c); } @Override public void clear() { s.clear(); }} // Wrappter class - uses composition in place of inheritancepublic class InstrumentedHashSet extends ForwardingSet { private int addCount = 0; public InstrumentedHashSet1(Set s) { super(s); } @Override public boolean add(E e) { addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; }}复制代码 继承与组合如何取舍 只有当子类真正是父类的子类型时,才适合继承。对于两个类 A 和 B,只有两者之间确实存在“is-a”关系的时候,类 B 才应该继承 A; 在包的内部使用继承是非常安全的,子类和父类的实现都处在同一个程序员的控制之下; 对于专门为了继承而设计并且具有很好的文档说明的类来说,使用继承也是非常安全的; 其他情况就应该优先考虑组合的方式来实现 接口优于抽象类Java 提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。自从 Java8 为接口增加缺省方法(default method),这两种机制都允许为实例方法提供实现。主要区别在于,为了实现由抽象类定义的类型,类必须称为抽象类的一个子类。因为 Java 只允许单继承,所以用抽象类作为类型定义受到了限制。 接口相比于抽象类的优势: 现有的类可以很容易被更新,以实现新的接口。 接口是定义混合类型(比如 Comparable)的理想选择。 接口允许构造非层次结构的类型框架。 接口虽然提供了缺省方法,但接口仍有有以下局限性: 接口的变量修饰符只能是 public static final 的 接口的方法修饰符只能是 public 的 接口不存在构造函数,也不存在 this 可以给现有接口增加缺省方法,但不能确保这些方法在之前存在的实现中都能良好运行。 因为这些默认方法是被注入到现有实现中的,它们的实现者并不知道,也没有许可 接口缺省方法的设计目的和优势在于: 为了接口的演化 Java 8 之前我们知道,一个接口的所有方法其子类必须实现(当然,这个子类不是一个抽象类),但是 java 8 之后接口的默认方法可以选择不实现,如上的操作是可以通过编译期编译的。这样就避免了由 Java 7 升级到 Java 8 时项目编译报错了。Java8 在核心集合接口中增加了许多新的缺省方法,主要是为了便于使用 lambda。 可以减少第三方工具类的创建 例如在 List 等集合接口中都有一些默认方法,List 接口中默认提供 replaceAll(UnaryOperator)、sort(Comparator)、、spliterator()等默认方法,这些方法在接口内部创建,避免了为了这些方法而专门去创建相应的工具类。 可以避免创建基类 在 Java 8 之前我们可能需要创建一个基类来实现代码复用,而默认方法的出现,可以不必要去创建基类。 由于接口的局限性和设计目的的不同,接口并不能完全替换抽象类。但是通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。 接口负责定义类型,或许还提供一些缺省方法,而骨架实现类则负责实现除基本类型接口方法之外,剩下的非基本类型接口方法。扩展骨架实现占了实现接口之外的大部分工作。这就是模板方法(Template Method)设计模式。 接口 Protocol:定义了 RPC 协议层两个主要的方法,export 暴露服务和 refer 引用服务 抽象类 AbstractProtocol:封装了暴露服务之后的 Exporter 和引用服务之后的 Invoker 实例,并实现了服务销毁的逻辑 具体实现类 XxxProtocol:实现 export 暴露服务和 refer 引用服务具体逻辑 优先考虑泛型声明中具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型(generic)类或者接口。泛型类和接口统称为泛型(generic type)。泛型从 Java 5 引入,提供了编译时类型安全检测机制。泛型的本质是参数化类型,通过一个参数来表示所操作的数据类型,并且可以限制这个参数的类型范围。泛型的好处就是编译期类型检测,避免类型转换。 // 比较三个值并返回最大值public static <T extends Comparable> T maximum(T x, T y, T z) { T max = x; // 假设 x 是初始最大值 if ( y.compareTo( max ) > 0 ) { max = y; //y 更大 } if ( z.compareTo( max ) > 0 ) { max = z; // 现在 z 更大 } return max; // 返回最大对象} public static void main( String args[] ) { System.out.printf( “%d, %d 和 %d 中最大的数为 %d\\n\\n”, 3, 4, 5, maximum( 3, 4, 5 )); System.out.printf( “%.1f, %.1f 和 %.1f 中最大的数为 %.1f\\n\\n”, 6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 )); System.out.printf( “%s, %s 和 %s 中最大的数为 %s\\n”,”pear”, “apple”, “orange”, maximum( “pear”, “apple”, “orange” ) );}复制代码 不要使用原生态类型由于为了保持 Java 代码的兼容性,支持和原生态类型转换,并使用擦除机制实现的泛型。但是使用原生态类型就会失去泛型的优势,会受到编译器警告。 要尽可能地消除每一个非受检警告每一条警告都表示可能在运行时抛出 ClassCastException 异常。要尽最大的努力去消除这些警告。如果无法消除但是可以证明引起警告的代码是安全的,就可以在尽可能小的范围中,使用@SuppressWarnings(“unchecked”)注解来禁止警告,但是要把禁止的原因记录下来。 利用有限制通配符来提升 API 的灵活性参数化类型不支持协变的,即对于任何两个不同的类型 Type1 和 Type2 而言,List 既不是 List 的子类型,也不是它的超类。为了解决这个问题,提高灵活性,Java 提供了一种特殊的参数化类型,称作有限制的通配符类型,即 List<? extends E>和 List<? super E>。使用原则是 producer-extends,consumer-super(PECS)。如果即是生产者,又是消费者,就没有必要使用通配符了。 还有一种特殊的无限制通配符 List<?>,表示某种类型但不确定。常用作泛型的引用,不可向其添加除 Null 以外的任何对象。 //List<? extends E>// Number 可以认为 是 Number 的 “子类”List<? extends Number> numberArray = new ArrayList();// Integer 是 Number 的子类List<? extends Number> numberArray = new ArrayList();// Double 是 Number 的子类List<? extends Number> numberArray = new ArrayList(); //List<? super E>// Integer 可以认为是 Integer 的 “父类”List<? super Integer> array = new ArrayList();、// Number 是 Integer 的 父类List<? super Integer> array = new ArrayList();// Object 是 Integer 的 父类List<? super Integer> array = new ArrayList(); public static void copy(List<? super T> dest, List<? extends T> src) { int srcSize = src.size(); if (srcSize > dest.size()) throw new IndexOutOfBoundsException(“Source does not fit in dest”); if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) { for (int i=0; i<srcSize; i++) dest.set(i, src.get(i)); } else { ListIterator<? super T> di=dest.listIterator(); ListIterator<? extends T> si=src.listIterator(); for (int i=0; i<srcSize; i++) { di.next(); di.set(si.next()); } }}复制代码 静态成员类优于非静态成员类嵌套类(nested class)是指定义在另一个类的内部的类。嵌套类存在的目的只是为了它的外部类提供服务,如果其他的环境也会用到的话,应该成为一个顶层类(top-level class)。 嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和 局部类(local class)。除了第一种之外,其他三种都称为内部类(inner class)。 匿名类(anonymous class)没有名字,声明的同时进行实例化,只能使用一次。当出现在非静态的环境中,会持有外部类实例的引用。通常用于创建函数对象和过程对象,不过现在会优先考虑 lambda。 局部类(local class)任何可以声明局部变量的地方都可以声明局部类,同时遵循同样的作用域规则。跟匿名类不同的是,有名字可以重复使用。不过实际很少使用局部类。 静态成员类(static member class)最简单的一种嵌套类,声明在另一个类的内部,是这个类的静态成员,遵循同样的可访问性规则。常见的用法是作为公有的辅助类,只有与它的外部类一起使用才有意义。 非静态成员类(nonstatic member class)尽管语法上,跟静态成员类的唯一区别就是类的声明不包含 static,但两者有很大的不同。非静态成员类的每个实例都隐含地与外部类的实例相关联,可以访问外部类的成员属性和方法。另外必须先创建外部类的实例之后才能创建非静态成员类的实例。 总而言之,这四种嵌套类都有自己的用途。假设这个嵌套类属于一个方法的内部,如果只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类。如果一个嵌套类需要在单个方法之外仍然可见,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的,否则就做成静态的。 优先使用模板/工具类通过对常见场景的代码逻辑进行抽象封装,形成相应的模板工具类,可以大大减少重复代码,专注于业务逻辑,提高代码质量。 分离对象的创建与使用面向对象编程相对于面向过程,多了实例化这一步,而对象的创建必须要指定具体类型。我们常见的做法是“哪里用到,就在哪里创建”,使用实例和创建实例的是同一段代码。这似乎使代码更具有可读性,但是某些情况下造成了不必要的耦合。 public class BusinessObject { public void actionMethond { //Other things Service myServiceObj = new Service(); myServiceObj.doService(); //Other things }} public class BusinessObject { public void actionMethond { //Other things Service myServiceObj = new ServiceImpl(); myServiceObj.doService(); //Other things }} public class BusinessObject { private Service myServiceObj; public BusinessObject(Service aService) { myServiceObj = aService; } public void actionMethond { //Other things myServiceObj.doService(); //Other things }} public class BusinessObject { private Service myServiceObj; public BusinessObject() { myServiceObj = ServiceFactory; } public void actionMethond { //Other things myServiceObj.doService(); //Other things }}复制代码 对象的创建者耦合的是对象的具体类型,而对象的使用者耦合的是对象的接口。也就是说,创建者关心的是这个对象是什么,而使用者关心的是它能干什么。这两者应该视为独立的考量,它们往往会因为不同的原因而改变。 当对象的类型涉及多态、对象创建复杂(依赖较多)可以考虑将对象的创建过程分离出来,使得使用者不用关注对象的创建细节。设计模式中创建型模式的出发点就是如此,实际项目中可以使用工厂模式、构建器、依赖注入的方式。 可访问性最小化区分一个组件设计得好不好,一个很重要的因素在于,它对于外部组件而言,是否隐藏了其内部数据和实现细节。Java 提供了访问控制机制来决定类、接口和成员的可访问性。实体的可访问性由该实体声明所在的位置,以及该实体声明中所出现的访问修饰符(private、protected、public)共同决定的。 对于顶层的(非嵌套的)类和接口,只有两种的访问级别:包级私有的(没有 public 修饰)和公有的(public 修饰)。 对于成员(实例/域、方法、嵌套类和嵌套接口)由四种的访问级别,可访问性如下递增: 私有的(private 修饰)–只有在声明该成员的顶层类内部才可以访问这个成员; 包级私有的(默认)–声明该成员的包内部的任何类都可以访问这个成员; 受保护的(protected 修饰)–声明该成员的类的子类可以访问这个成员,并且声明该成员的包内部的任何类也可以访问这个成员; 公有的(public 修饰)–在任何地方都可以访问该成员; 正确地使用这些修饰符对于实现信息隐藏是非常关键的,原则就是:尽可能地使每个类和成员不被外界访问(私有或包级私有)。这样好处就是在以后的发行版本中,可以对它进行修改、替换或者删除,而无须担心会影响现有的客户端程序。 如果类或接口能够做成包级私有的,它就应该被做成包级私有的; 如果一个包级私有的顶层类或接口只是在某一个类的内部被用到,就应该考虑使它成为那个类的私有嵌套类; 公有类不应直接暴露实例域,应该提供相应的方法以保留将来改变该类的内部表示法的灵活性; 当确定了类的公有 API 之后,应该把其他的成员都变成私有的; 如果同一个包下的类之间存在比较多的访问时,就要考虑重新设计以减少这种耦合; 可变性最小化不可变类是指其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例时提供,并在对象的整个生命周期内固定不变。不可变类好处就是简单易用、线程安全、可自由共享而不容易出错。Java 平台类库中包含许多不可变的类,比如 String、基本类型包装类、BigDecimal 等。 为了使类成为不可变,要遵循下面五条规则: 声明所有的域都是私有的 声明所有的域都是 final 的 如果一个指向新创建实例的引用在缺乏同步机制的情况下,从一个线程被传递到另一个线程,就必须确保正确的行为 不提供任何会修改对象状态的方法 保证类不会被扩展(防止子类化,类声明为 final) 防止粗心或者恶意的子类假装对象的状态已经改变,从而破坏该类的不可变行为 确保对任何可变组件的互斥访问 如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用。在构造器、访问方法和 readObject 方法中使用保护性拷贝技术 可变性最小化的一些建议: 除非有很好的理由要让类成为可变的类,否则它就应该是不可变的; 如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性; 除非有令人信服的理由要使域变成非 final 的,否则要使每个域都是 private final 的; 构造器应该创建完全初始化的对象,并建立起所有的约束关系; 质量如何保证测试驱动开发测试驱动开发(TDD)要求以测试作为开发过程的中心,要求在编写任何代码之前,首先编写用于产码行为的测试,而编写的代码又要以使测试通过为目标。TDD 要求测试可以完全自动化地运行,并在对代码重构前后必须运行测试。 TDD 的最终目标是整洁可用的代码(clean code that works)。大多数的开发者大部分时间无法得到整洁可用的代码。办法是分而治之。首先解决目标中的“可用”问题,然后再解决“代码的整洁”问题。这与体系结构驱动(architecture-driven)的开发相反。 采用 TDD 另一个好处就是让我们拥有一套伴随代码产生的详尽的自动化测试集。将来无论出于任何原因(需求、重构、性能改进)需要对代码进行维护时,在这套测试集的驱动下工作,我们代码将会一直是健壮的。 TDD 的开发周期 添加一个测试 -> 运行所有测试并检查测试结果 -> 编写代码以通过测试 -> 运行所有测试且全部通过 -> 重构代码,以消除重复设计,优化设计结构 两个基本的原则 仅在测试失败时才编写代码并且只编写刚好使测试通过的代码 编写下一个测试之前消除现有的重复设计,优化设计结构 关注点分离是这两条规则隐含的另一个非常重要的原则。其表达的含义指在编码阶段先达到代码“可用”的目标,在重构阶段再追求“整洁”目标,每次只关注一件事! 分层测试点 测试类型 目标 测试和结果判定 Dao 测试 验证 mybatis-config、mapper、handler 的正确性 基于内存数据库 可以使用 assert 验证 Adapter 测试 验证外部依赖交互正确验证 converter 正确 依赖外部环境正确性依赖人工判读 Repository 测试 验证内部计算、转换逻辑 可 mock 外部依赖可以使用 assert 验证 biz 层测试 验证内部业务逻辑 尽可能隔离所有外部依赖需要多个测试,每个测试验证一个场景或分支使用 assert 验证,不依赖人工判断 Application 层测试 验证入口参数处理正确验证系统内链路无阻塞 可以隔离外部依赖场景覆盖通过参数控制 可使用单步调试观察代码执行走向 不验证详细逻辑 参考资料 重构-改善既有代码的设计 设计模式 Effective Java 敏捷软件开发与设计的最佳实践 实现模式 测试驱动开发","categories":[],"tags":[]},{"title":"PHP的生成器(generator)","slug":"PHP的生成器(generator)","date":"2021-04-29T23:46:07.000Z","updated":"2024-10-04T01:18:24.547Z","comments":true,"path":"2021/04/29/PHP的生成器(generator)/","link":"","permalink":"https://g-ydg.github.io/2021/04/29/PHP%E7%9A%84%E7%94%9F%E6%88%90%E5%99%A8%EF%BC%88generator%EF%BC%89/","excerpt":"","text":"什么是生成器 生成器其实就是一个用于迭代的迭代器。它提供了一种更容易的方式来实现对象迭代,相比定义类实现 Iterator 接口的方式大大降低性能开销和复杂度。 看以下代码 123456789101112131415161718function test1(){ for ($i = 0; $i < 3; $i++) { yield $i + 1; } yield 1000; yield 1001;}foreach (test1() as $t) { echo $t, PHP_EOL;}// 1// 2// 3// 1000// 1001 这段代码很简单,首先生成器必须在方法中并使用 yield 关键字;其次每一个 yield 都可以看作是一次 return ;最后外部循环时每次循环取一个 yield 的返回值。 在上面例子中,循环了 3 次并返回了 1、2、3 这三个数字。然后在循环外部又写了 2 行 yield 分别输出了 1000 和 1001。因此外部的 foreach 循环一共输出了 5 次。 明明是一个方法,为什么它能够循环,而且返回一种循环体的格式。我们直接打印这个 test() 方法看看打印的是什么: 12345// 是一个生成器对象var_dump(test1());// Generator Object// (// ) 上面的例子告诉你:当使用了 yield 进行内容返回后,返回的是一个 Generator 对象。这个对象就叫作生成器对象,这个对象是不能直接被 new 实例化,只能通过生成器函数这种方式返回实例。 Generator 这个类包含 current() 、 key() 等方法,最主要的这个类实现了 Iterator 接口,也就是说它就是一个特殊的迭代器类。 1234567891011Generator implements Iterator { /* 方法 */ public current ( void ) : mixed public key ( void ) : mixed public next ( void ) : void public rewind ( void ) : void public send ( mixed $value ) : mixed public throw ( Exception $exception ) : void public valid ( void ) : bool public __wakeup ( void ) : void} 生成器有什么用 生成器最强大的部分就在于它不需要一个数组或者任何的数据结构来保存这一系列数据。每次迭代执行到 yield 时都是动态返回的。所以生成器能在很大程度上节约内存。 1234567891011121314151617181920212223242526272829303132333435363738// 内存占用测试$start_time = microtime(true);function test2($clear = false){ $arr = []; if($clear){ $arr = null; return; } for ($i = 0; $i < 1000000; $i++) { $arr[] = $i + 1; } return $arr;}$array = test2();foreach ($array as $val) {}$end_time = microtime(true);echo "time: ", bcsub($end_time, $start_time, 4), PHP_EOL;echo "memory (byte): ", memory_get_usage(true), PHP_EOL;// time: 0.0513// memory (byte): 35655680$start_time = microtime(true);function test3(){ for ($i = 0; $i < 1000000; $i++) { yield $i + 1; }}$array = test3();foreach ($array as $val) {}$end_time = microtime(true);echo "time: ", bcsub($end_time, $start_time, 4), PHP_EOL;echo "memory (byte): ", memory_get_usage(true), PHP_EOL;// time: 0.0517// memory (byte): 2097152 以上代码进行 1000000 个循环后获取结果的简单操作,你可以可以直观地看出使用生成器的版本仅仅消耗了大约 2M 的内存,而未使用生成器的却消耗了 35M 的内存,有 10 多倍的差距了,而且越大的数据量差距会更加明显。所以又有人说生成器是 PHP 中最不可被低估了的一个特性。 生成器的应用生成器主要应用方式有哪些呢。下面我们来看看:1 返回空值以及中断生成器可以返回空值,也就是说不带任何值就可以返回一个空值了,你直接 yield 就好了;用 return 来中断生成器的继续执行。 下面的代码我们在 $i = 4; 的时候返回了空值,也就是不会输出 5 (因为我们返回的是 $i + 1 )。然后在 $i == 7 的时候使用 return;来中断生成器的继续执行,也就是说循环最多只会输出到 7 ,就结束了。 1234567891011121314151617181920212223// 返回空值以及中断function test4(){ for ($i = 0; $i < 10; $i++) { if ($i == 4) { yield; // 返回null值 } if ($i == 7) { return; // 中断生成器执行 } yield $i + 1; }}foreach (test4() as $t) { echo $t, PHP_EOL;}// 1// 2// 3// 4// 5// 6// 7 2 返回键值对形式生成器也可以返回键值对形式的可遍历对象,可以供 foreach 使用的,语法:yield key => value; 和数组项的定义形式一样,非常直观。代码如下: 12345678910111213141516171819function test5(){ for ($i = 0; $i < 10; $i++) { yield 'key.' . $i => $i + 1; }}foreach (test5() as $k=>$t) { echo $k . ':' . $t, PHP_EOL;}// key.0:1// key.1:2// key.2:3// key.3:4// key.4:5// key.5:6// key.6:7// key.7:8// key.8:9// key.9:10 外部传递数据 我们也可以通过 Generator::send 方法来向生成器中传入一个值。这个值将会被当做生成器当前 yield 的返回值。 1234567891011121314151617181920function test6(){ for ($i = 0; $i < 10; $i++) { // 正常获取循环值,当外部send过来值后,yield获取到的就是外部传来的值了 $data = (yield $i + 1); if($data == 'stop'){ return; } }}$t6 = test6();foreach($t6 as $t){ if($t == 3){ $t6->send('stop'); } echo $t, PHP_EOL;}// 1// 2// 3 正常获取循环值,当外部 send 过来值后,yield 获取到的就是外部传来的值了。记住:变量获取 yield 的值时必须要用括号括起来。 yield from 语法这种语法其实就是指从另一个可迭代对象中一个一个的获取数据并形成生成器返回。看代码比较好理解: 12345678910111213141516171819function test7(){ yield from [1, 2, 3, 4]; yield from new ArrayIterator([5, 6]); yield from test1();}foreach (test7() as $t) { echo 'test7:', $t, PHP_EOL;}// test7:1// test7:2// test7:3// test7:4// test7:5// test7:6// test7:1// test7:2// test7:3// test7:1000 在 以上方法中使用了 yield from 分别从普通数组、迭代器对象和另一个生成器中获取了数据,并做为当前生成器的内容进行返回。 生成器可以用 count 获取数量吗? 测试一下就知道了,生成器是不能用 count 来获取它的数量的。使用 count 获取生成器的数量将会直接 Warning 警告。直接输出将会一直显示是 1 ,因为 count 的特性(强制转换成数组都会显示 1 )。 12$c = count(test1()); // Warning: count(): Parameter must be an array or an object that implements Countable// echo $c, PHP_EOL; 总结yield 生成器不仅大大的节约了内存的开销,而且语法其实也非常的简洁明了。我们不需要在方法内部再多定义一个数组去存储返回值,直接 yield 就可以了。在实际的项目中你可以多多的使用这种生成器哦!","categories":[],"tags":[]},{"title":"提升查询技能,这7条SQL查询错误必须解决","slug":"提升查询技能,这7条SQL查询错误必须解决","date":"2021-04-29T23:43:58.000Z","updated":"2024-10-04T01:18:32.595Z","comments":true,"path":"2021/04/29/提升查询技能,这7条SQL查询错误必须解决/","link":"","permalink":"https://g-ydg.github.io/2021/04/29/%E6%8F%90%E5%8D%87%E6%9F%A5%E8%AF%A2%E6%8A%80%E8%83%BD%EF%BC%8C%E8%BF%997%E6%9D%A1SQL%E6%9F%A5%E8%AF%A2%E9%94%99%E8%AF%AF%E5%BF%85%E9%A1%BB%E8%A7%A3%E5%86%B3/","excerpt":"","text":"本文将指出一些常见但却总是被忽略的错误,请静下心来,准备好提升查询技能吧! 让我们以一个虚构的业务为例。假设你是亚马逊电子商务分析团队的一员,需要运行几个简单的查询。你手头有两个表,分别为“product(产品)”和“discount (折扣)”。 1.计算 NULL 字段的数目 为了计算 null 字段的数目,要掌握 COUNT 函数的工作原理。假设计算产品数量时,要求计入表格“product”的“product_id”主键列中遗漏的字段。 12SELECT COUNT(product_id)FROM product;Result: 3 由于要求计入“product_id”列中的 null 值,查询结果应该为 4,但 COUNT()函数不会将 null 值计数。 解决方法:使用 COUNT(*)函数。该函数可以将 null 值计数。 12Select Count(*)From product;Result: 4 这个操作很简单,但是在编写复杂的查询时总会被忽略。 2.使用保留字作为列名 123SELECT product_id,RANK() OVER (ORDER BY price desc) As RankFROM product; 由于列名“Rank”是 Rank 函数的保留字,该查询结果出错。 解决方法: 123SELECT product_id,RANK() OVER (ORDER BY price desc) As ‘Rank’FROM product; 加上单引号,即可得到想要的结果。 3.NULL 的比较运算 123SELECT product_nameFROM productWHERE product_id=NULL; 由于使用了比较运算符“=”,此处运算会出现异常,使用另一比较运算符“!=”运算也会出现异常。这里的逻辑问题在于,你编写的查询得出的是“product_id”列的值是否未知,而无法得出这一列的值是否是未知的产品。 解决方法: 123SELECT product_nameFROM productWHERE product_id ISNULL; 4.ON 子句过滤和 WHERE 子句过滤的区别 这是一个非常有趣的概念,会提高你对于 ON 子句过滤和 WHERE 子句过滤之间区别的基本理解。这并不完全是一个错误,只是演示了两者的用法,你可以根据业务需求选择最佳方案。 123456SELECT d.product_id,price,discountFROM product p RIGHT JOIN discount d ONp.product_id=d.product_idWHERE p.product_id>1; 结果: 在这种情况下,过滤条件在两个表格连接之后生效。因此,所得结果不包含 d.product_id≤1 的行(同理,显然也不包含 p.product≤1 的行)。 解决方法:使用 AND,注意结果上的不同。 123456SELECT d.product_id,price,discountFROM product p RIGHT JOIN discount d ONp.product_id=d.product_idAND p.product_id>1; 结果: 在这里,条件语句 AND 在两个表格连接发生之前计算。可以把此查询看作只适用于一个表(“product”表)的 WHERE 子句。现在,由于右连接,结果中出现了 d.product_id≤1 的行(显然还有 p.product_id>1 的行)。 请注意,ON 子句过滤和 WHERE 子句过滤只在左/右/外连接时不同,而在内连接时相同。 5.在同一查询的 WHERE 子句中使用 Windows 函数生成的列&使用 CASE WHEN 子句 注意,不要在同一查询的 WHERE 子句中使用通过 Windows 函数生成的列名以及 CASE WHEN 子句。 1234SELECT product_id,RANK() OVER (ORDER BY price desc) AS rkFROM productWHERE rk=2; 因为列 rk 由 Windows 函数生成,并且在同一查询的 WHERE 子句中使用了该列,这个查询结果会出现异常。 解决方法:这一问题可以通过使用临时表或者子查询解决。 12345678910WITH CTE AS(SELECT product_id,RANK() OVER (ORDER BY price desc) AS rkFROM product)SELECT product_idFROMCTEWHERE rk=2; 或 12345678SELECT product_idFROM(SELECT product_id,RANK() OVER (ORDER BY price desc) AS rkFROM product;)WHERE rk=2; 同样的方法也适用于使用 CASE WHEN 子句创建的列。请记住,Windows 函数只能出现在 SELECT 或 ORDER BY 子句中。 6.BETWEEN 的使用不正确 如果不清楚 BETWEEN 的有效范围,也许会得不到想要的查询结果。BETWEEN x AND y 语句的有效范围包含 x 和 y。 1234SELECT *FROM discountWHERE offer_valid_till BETWEEN ‘2019/01/01’ AND ‘2020/01/01’ORDER BY offer_valid_till; 结果: 在查询中,也许我们只想得到 2019 年的所有日期,但是结果中还包含了 2020 年 1 月 1 日。这是因为 BETWEEN 语句的有效范围包含 2019/01/01 和 2020/01/01。 解决方法:相应地调整范围可以解决这个问题。 1234SELECT *FROM discountWHERE offer_valid_till BETWEEN ‘2019/01/01’ AND ‘2019/12/31’ORDER BY offer_valid_till; 结果: 现在,所有查询结果均为 2019 年的日期。 7.在 GROUP BY 语句后使用 WHERE 子句 在编写 GROUP BY 语句时,请注意 WHERE 子句的位置。 123456SELECT category,AVG (price)FROM product p INNER JOIN discount d ONp.product_id=d.product_idGROUP BY categoryWHERE discount_amount>10; 由于将 WHERE 子句放在 GROUP BY 语句后,此查询是错误的。这是为什么呢? WHERE 子句用于过滤查询结果,这一步要在将查询结果分组之前实现,而不是先分组再过滤。正确的做法是先应用 WHERE 条件过滤减少数据,再使用 GROUP BY 子句通过聚合函数将数据分组(此处使用聚合函数 AVG)。 解决方法: 123456SELECT category,AVG (price)FROM product p INNER JOIN discount d ONp.product_id=d.product_idWHERE discount_amount>10GROUP BY category; 请注意主要 SQL 语句的执行顺序: · FROM 子句 · WHERE 子句 · GROUP BY 子句 · HAVING 子句 · SELECT 子句 · ORDER BY 子句 以上包含了大部分让人不解的错误,尤其是对初学者而言。正如亨利·福特所说:“唯一的错误是我们从中学不到任何东西”,希望这篇文章能帮助你精进查询技能。","categories":[],"tags":[]},{"title":"SQL语句的执行过程","slug":"SQL语句的执行过程","date":"2021-04-29T23:43:53.000Z","updated":"2024-10-04T01:18:34.481Z","comments":true,"path":"2021/04/29/SQL语句的执行过程/","link":"","permalink":"https://g-ydg.github.io/2021/04/29/SQL%E8%AF%AD%E5%8F%A5%E7%9A%84%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B/","excerpt":"","text":"Select 语句的执行流程第一步:连接器连接器负责跟客户端建立连接、获取权限、维持和管理连接。如果用户名密码验证通过后,连接器会到权限表里面查出你拥有的权限。之后该连接的权限验证都依赖于刚查出来的权限。 第二步:查询缓存当获取连接后,一条 SELECT 语句会先去查询缓存,看之前是否执行过。如果获取到缓存后就执行返回,不然继续后面的步骤。 大多数时候不建议使用缓存,因为只要一个表更新,这个表上的所有缓存数据就会被清空了。对于那些经常更新的表来说,缓存命中率很低。MYSQL8 版本直接将查询缓存的整块功能删掉了。 第三步:分析器分析器首先会做“词法分析”,MYSQL 会识别出 SQL 语句里面的字符串是什么以及代表什么。接下来就是“语法分析器”,分析 SQL 的语法问题。 第四步:优化器优化器会对 SQL 的执行顺序,使用哪个索引进行优化。确定 SQL 的执行方案。 第五步:执行器执行器执行 SQL 语句会对权限进行校验,如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。 Update 语句的执行流程update 语句除了会执行上面的五步,还会涉及两个重要的日志模块。 两个重要的日志模块redo log (重做日志)redo log 是 innodb 所特有的,当有一条更新语句时,innoDB 引擎会先把记录写到 redo log 中,然后更新内存,这时候更新就算完成了。innoDB 会在合适的时候将这个记录更新到磁盘中去。 特点:redo log 是固定大小的,属于循环写。redo log 是物理日志,记录的是“在某个数据页上做了什么修改”。有了 redo log ,InnoDB 可以保证数据库发生异常重启的时候,之前提交的记录不会丢失,这个能力为 crash-safe。 binlog(归档日志)binlog 属于 server 层的日志,是逻辑日志,记录的是这个语句的原始逻辑,比如给“id =1 的一行的某个字段+2”。binlog 是追加写入的,binlog 写到一定的大小后切换到下一个,不会覆盖之前的。 更新语句的内部流程update t set n = n+2 where id =1 执行器先找引擎找到 id=1 的那一行,如果这一行的数据页已经在内存中则直接返回给执行器。否则先从磁盘读入内存中,然后在返回。 执行器拿到了引擎返回的数据行,把这个 n 值+1,得到新的行数据,然后调引擎的接口写入这行新数据。 引擎将这行数据更新到内存中,同时将这个更新操作记录到 redo log 里,此时 rodo log 属于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务了。 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。 执行器调引擎的提交事务接口,引擎把刚刚写入的 redo log 的状态改为 commit 状态,更新完成。 两段式提交redo log 的写入分为两部分,是为了保证这两份日志的逻辑一致。 相关配置redo log 用于保证 crash-safe 能力。 innodb_flush_log_at_trx_commit 这个参数设置成 1,表示每次事务的 redo log 都直接持久化到磁盘。 sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。","categories":[],"tags":[]},{"title":"PHP底层原理以及代码执行过程","slug":"PHP底层原理以及代码执行过程","date":"2021-04-28T20:15:44.000Z","updated":"2024-10-04T01:18:37.839Z","comments":true,"path":"2021/04/28/PHP底层原理以及代码执行过程/","link":"","permalink":"https://g-ydg.github.io/2021/04/28/PHP%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E4%BB%A5%E5%8F%8A%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B/","excerpt":"","text":"PHP 的底层原理 从图上可以看出,php 从下到上是一个 4 层体系 Zend 引擎Zend 整体用纯 c 实现,是 php 的内核部分,它将 php 代码翻译(词法、语法解析等一系列编译过程)为可执行opcode 的处理并实现相应的处理方法、实现了基本的数据结构(如 hashtable、oo)、内存分配及管理、提供了相应的 api 方法供外部调用,是一切的核心,所有的外围功能均围绕 zend 实现。 **Extensions**围绕着 zend 引擎,extensions 通过组件式的方式提供各种基础服务,我们常见的各种内置函数(如 array 系列)、标准库等都是通过 extension 来实现,用户也可以根据需要实现自己的 extension 以达到功能扩展、性能优化等目的(如贴吧正在使用的 php 中间层、富文本解析就是 extension 的典型应用)。 **Sapi**Sapi 全称是 Server Application Programming Interface,也就是服务端应用编程接口,sapi 通过一系列钩子函数,使得 php 可以和外围交互数据,这是 php 非常优雅和成功的一个设计,通过 sapi 成功的将 php 本身和上层应用解耦隔离,php 可以不再考虑如何针对不同应用进行兼容,而应用本身也可以针对自己的特点实现不同的处理方式。 **上层应用**这就是我们平时编写的 php 程序,通过不同的 sapi 方式得到各种各样的应用模式,如通过 webserver 实现 web 应用、在命令行下以脚本方式运行等等。 构架思想引擎(Zend)+组件(ext)的模式降低内部耦合中间层(sapi)隔绝 web server 和 php 如果 php 是一辆车,那么 车的框架就是 php 本身Zend 是车的引擎(发动机)Ext 下面的各种组件就是车的轮子Sapi 可以看做是公路,车可以跑在不同类型的公路上而一次 php 程序的执行就是汽车跑在公路上。因此,我们需要:性能优异的引擎+合适的车轮+正确的跑道 PHP 代码如何执行 当 PHP 拿到一段代码后,经过词法解析、语法解析等阶段后,源程序会被翻译成一个个指令(opcodes),然后 ZEND 虚拟机顺次执行这些指令完成操作。 开启 opcache Opcode cache 的目的是避免重复编译,减少 CPU 和内存开销。 正常执行流程: 开启 opcache 后:","categories":[],"tags":[]},{"title":"Windows10搭建Docker环境","slug":"Windows10搭建Docker环境","date":"2021-04-28T17:55:49.000Z","updated":"2024-10-04T01:18:47.872Z","comments":true,"path":"2021/04/28/Windows10搭建Docker环境/","link":"","permalink":"https://g-ydg.github.io/2021/04/28/Windows10%E6%90%AD%E5%BB%BADocker%E7%8E%AF%E5%A2%83/","excerpt":"","text":"运行 WSL 2 的要求检查系统版本 Win + R,键入“winver”进行查看 系统版本要求 x64 系统:版本 1903 或更高版本,采用内部版本 18362 或更高版本。 ARM64 系统:版本** 2004 或更高版本,采用内部版本 19041 或更高**版本。 若系统版本不符合,需要先进行系统更新升级,可以使用官方的更新助手进行系统升级。 开启“适用于 Linux 的 Windows 子系统”以管理员身份打开 PowerShell 并运行 1dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart 启用“虚拟机平台”以管理员身份打开 PowerShell 并运行 1dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart 重新启动计算机,以完成 WSL 安装并更新到 WSL 2。 下载 Linux 内核更新包 适用于 x64 计算机的 WSL2 Linux 内核更新包 适用于 ARM64 计算机的 WSL2 Linux 内核更新包 根据自身系统类型下载内核更新包,下载完成后,双击运行进行安装。 将 WSL 2 设置为默认版本打开 PowerShell,将 WSL 2 设置为默认版本 12345#设置wsl默认版本wsl --set-default-version 2#查看版本信息wsl -l -v 安装 Linux 分发 打开Microsoft Store,选择自己偏好的 Linux 分发版。 在分发版的页面中,选择“获取”,进行下载安装。 首次启动时,系统会进行初始化,大约 1~2 分钟。初始化完成后,根据系统引导为 Linux 分发版创建用户账户以及密码。 下载安装Docker Desktop 开启 基于 WLS2 引擎(Use the WSL 2 based engine) 将 Docker 环境集成到 Linux 分发版中 使用加速器,以阿里云容器镜像服务提供的加速地址","categories":[],"tags":[]}],"categories":[],"tags":[]}