Kubernetes如何通过StatefulSet支持有状态应用?
Kubernetes如何通过StatefulSet支持有状态应用?
为什么Deployment不能编排所有类型应用?
Deployment认为一个应用中所有的Pod是完全一样的,所以他们之间没有顺序,也无所谓运行在哪台宿主机上。需要的时候,Deployment就可以通过Pod模板创建新的Pod;不需要的时候,Deployment就可以"杀掉"任意一个Pod。
但是,在实际的场景中,并不是所有的应用都可以满足这样的要求。
尤其是分布式应用,它的多个实例之间,往往有依赖关系,比如:主从关系、主备关系 。还有就是数据存储类应用,它的多个实例,往往都会在本地磁盘上保存一份数据。而这些实例一旦被杀掉,即便重建出来,实例与数据之间的对应关系也已经丢失,从而导致应用失败。
所以,这种实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,就被称为“有状态 应用”(Stateful Application)。
得益于“控制器模式”的设计思想,Kubernetes 项目很早就在 Deployment 的基础上,扩展 出了对“有状态应用”的初步支持。这个编排功能,就是:StatefulSet。
StatefulSet 的设计其实非常容易理解。它把真实世界里的应用状态,抽象为了两种情况:
- 拓扑状态。这种情况意味着,应用的多个实例之间不是完全对等的关系。这些应用实例,必 须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果你把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到 这个新 Pod。
- 存储状态。这种情况意味着,应用的多个实例分别绑定了不同的存储数据。对于这些应用实 例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一 份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的 多个存储实例。
所以,StatefulSet 的核心功能,就是通过某种方式记录这些状态,然后在 Pod 被重新创建时, 能够为新 Pod 恢复这些状态。
Headless Service
在开始讲述 StatefulSet 的工作原理之前,我们必须先了解一个 Kubernetes 项目中非常实用的概念:Headless Service。
Service 是 Kubernetes 项目中用来将 一组 Pod 暴露给外界访问的一种机制。比如,一个 Deployment 有 3 个 Pod,那么我就可以定义一个 Service。然后,用户只要能访问到这个 Service,它就能访问到某个具体的 Pod。
Service被访问有两种方式:
- 以Service的VIP(Virtual IP,虚拟IP) 方式。比如:当我访问10.0.23.1这个Service的IP时,10.0.23.1其实就是一个VIP,它会把请求转发到该Service所代理的某一个Pod上。
- Headless Service, 这种情况下,访问“my-svc.my-namespace.svc.cluster.local”解析到的,直接就是 my-svc 代理的某一个 Pod 的 IP 地址。
可以看到,这里的区别在于,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 记录 的方式解析出被代理 Pod 的 IP 地址。
那么,这样的设计又有什么作用呢? 想要回答这个问题,我们需要从 Headless Service 的定义方式看起。 下面是一个标准的 Headless Service 对应的 YAML 文件:
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
可以看到,所谓的 Headless Service,其实仍是一个标准 Service 的 YAML 文件。只不过,它的 clusterIP 字段的值是:None,即:这个 Service,没有一个 VIP 作为“头”。这也就是 Headless 的含义。所以,这个 Service 被创建后并不会被分配一个 VIP,而是会以 DNS 记录 的方式暴露出它所代理的 Pod。
而它所代理的Pod,依然采用的是Label Selector
机制选择出来的,即所有携带了app=nginx
标签的Pod,都会被这个Service代理起来。
当我们按照这样的方式创建了一个 Headless Service 之后,它所代理的所有 Pod 的 IP 地址,都会被绑定一个这样格式的 DNS 记录,如下所示:
<pod-name>.<svc-name>.<namespace>.svc.cluster.local
这个 DNS 记录,正是 Kubernetes 项目为 Pod 分配的唯一的“可解析身份”(Resolvable Identity)。
有了这个“可解析身份”,只要知道了一个 Pod 的名字,以及它对应的 Service 的名字,就可以非常确定地通过这条 DNS 记录访问到 Pod 的 IP 地址。
StatefulSet的拓扑状态
那么,StatefulSet又是如何使用DNS记录来维持Pod的拓扑状态呢?
我们来编写一个StatefulSet的yaml文件,如下所示:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.17.1
ports:
- containerPort: 80
name: web
这个yaml文件,和我们deployment的唯一区别就是多了个serviceName=nginx
字段,这个字段的作用,就是告诉StatefulSet控制器,在执行控制循环的时候,请使用nginx这个Headless Service来保证Pod的"可解析身份"。
所以当我们通过kubectl create
创建上面这个Service和StatefulSet 之后,就会看到如下两个对象:
# kubectl get service nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP None <none> 80/TCP 2m54s
# kubectl get statefulset web
NAME READY AGE
web 2/2 72s
我们通过查看StatefulSet的Events来查看Pod的创建过程:
# kubectl describe statefulset web
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 66s statefulset-controller create Pod web-0 in StatefulSet web successful
Normal SuccessfulCreate 64s statefulset-controller create Pod web-1 in StatefulSet web successful
我们不难看到,StatefulSet 给它所管理的所有 Pod 的名字, 进行了编号,编号规则是:-。 而且这些编号都是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应,绝不重复。 更重要的是,这些 Pod 的创建,也是严格按照编号顺序进行的。
当这两个 Pod 都进入了 Running 状态之后,我们就可以查看到它们各自唯一的“网络身份”了。我们使用 kubectl exec
命令进入到容器中查看它们的 hostname:
# kubectl exec web-0 -- sh -c 'hostname'
web-0
# kubectl exec web-1 -- sh -c 'hostname'
web-1
可以看到,这两个 Pod 的 hostname 与 Pod 名字是一致的,都被分配了对应的编号。
接下来,我们再试着以 DNS 的方式,访问一下这个 Headless Service:
# kubectl run -it --image busybox:1.28.3 test --restart=Never --rm /bin/sh
通过这条命令,我们启动了一个一次性的 Pod,因为–-rm
意味着 Pod 退出后就会被删除掉。然后,在这个 Pod 的容器里面,我们尝试用 nslookup 命令,解析一下 Pod 对应的 Headless Service:
/ # nslookup web-0.nginx
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.nginx
Address 1: 192.168.166.138 web-0.nginx.default.svc.cluster.local
/ # nslookup web-1.nginx
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-1.nginx
Address 1: 192.168.166.140 web-1.nginx.default.svc.cluster.local
从 nslookup 命令的输出结果中,我们可以看到,在访问 web-0.nginx 的时候,最后解析到的,正是 web-0 这个 Pod 的 IP 地址;而当访问 web-1.nginx 的时候,解析到的则是 web-1 的 IP 地址。
这时候,我们在另外一个 Terminal 里把这两个“有状态应用”的 Pod 删掉:
# kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
在当前 Terminal 里 Watch 一下这两个 Pod 的状态变化,就会发现一个有趣的现象:
# kubectl get pod -w -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 0/1 Terminating 0 3m5s
web-1 0/1 Terminating 0 2m55s
web-0 0/1 Terminating 0 3m10s
web-0 0/1 Terminating 0 3m10s
web-0 0/1 Pending 0 0s
web-1 0/1 Terminating 0 3m
web-0 0/1 Pending 0 0s
web-1 0/1 Terminating 0 3m
web-0 0/1 ContainerCreating 0 1s
web-0 0/1 ContainerCreating 0 1s
web-0 1/1 Running 0 2s
web-1 0/1 Pending 0 0s
web-1 0/1 Pending 0 0s
web-1 0/1 ContainerCreating 0 0s
web-1 0/1 ContainerCreating 0 0s
web-1 1/1 Running 0 2s
可以看到,当我们把这两个 Pod 删除之后,Kubernetes 会按照原先编号的顺序,创建出了两个新的 Pod。并且,Kubernetes 依然为它们分配了与原来相同的“网络身份”:web-0.nginx 和 web-1.nginx。
通过这种严格的对应规则,StatefulSet 就保证了 Pod 网络标识的稳定性。 比如,如果 web-0 是一个需要先启动的主节点,web-1 是一个后启动的从节点,那么只要这个 StatefulSet 不被删除,你访问 web-0.nginx 时始终都会落在主节点上,访问 web-1.nginx 时,则始终都会落在从节点上,这个关系绝对不会发生任何变化。
所以,如果我们再用 nslookup 命令,查看一下这个新 Pod 对应的 Headless Service :
/ # nslookup web-0.nginx
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.nginx
Address 1: 192.168.166.147 web-0.nginx.default.svc.cluster.local
/ # nslookup web-1.nginx
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-1.nginx
Address 1: 192.168.166.145 web-1.nginx.default.svc.cluster.local
我们可以看到,在这个 StatefulSet 中,这两个新 Pod 的“网络标识”(比如:web-0.nginx 和 web-1.nginx),再次解析到了正确的 IP 地址(比如:web-0 Pod 的 IP 地址 192.168.166.147)。
通过这种方法,Kubernetes 就成功地将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录。 这些状态,在 StatefulSet 的整个生命周期里都会保持不变,绝不会因为对应 Pod 的删除或者 重新创建而失效。
不过,相信你也已经注意到了,尽管 web-0.nginx 这条记录本身不会变,但它解析到的 Pod 的 IP 地址,并不是固定的。这就意味着,对于“有状态应用”实例的访问,你必须使用 DNS 记录 或者 hostname 的方式,而绝不应该直接访问这些 Pod 的 IP 地址。
StatefulSet 这个控制器的主要作用之一,就是使用 Pod 模板创建 Pod 的时候, 对它们进行编号,并且按照编号顺序逐一完成创建工作。而当 StatefulSet 的“控制循环”发现 Pod 的“实际状态”与“期望状态”不一致,需要新建或者删除 Pod 进行“调谐”的时候,它会严格按照这些 Pod 编号的顺序,逐一完成这些操作。
与此同时,通过 Headless Service 的方式,StatefulSet 为每个 Pod 创建了一个固定并且稳定的 DNS 记录,来作为它的访问入口。
StatefulSet的存储状态
在开始之前,我们先准备两个1G存储卷(PV):
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv001
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: /tmp/pv001
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv002
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: /tmp/pv002
然后直接创建pv即可:
# kubectl create -f pv.yaml
persistentvolume/pv001 created
persistentvolume/pv002 created
# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv001 1Gi RWO Retain Available 13s
pv002 1Gi RWO Retain Available 13s
可以看到成功创建了两个 PV 对象,状态是:Available
。
然后接下来声明一个如下所示的StatefulSet资源清单:(nginx-sts.yaml)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.17.1
ports:
- containerPort: 80
name: web
volumeMounts:
- name: ww
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: ww
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
这次,我们为这个 StatefulSet 额外添加了一个 volumeClaimTemplates
字段。从名字就可以看出来,它跟 Deployment 里 Pod 模板(PodTemplate)的作用类似。也就是说,凡是被这个StatefulSet管理的 Pod,都会声明一个对应的 PVC;而这个 PVC 的定义,就来自于 volumeClaimTemplates
这个模板字段。
更重要的是,这个 PVC 的名字,会被分配一个与这个 Pod 完全一致的编号。 这个自动创建的 PVC,与 PV 绑定成功后,就会进入 Bound 状态,这就意味着这个 Pod 可以挂载并使用这个 PV 了。
PVC 其实就是一种特殊的 Volume。只不过一个 PVC 具体是什么类型的 Volume,要在跟某个 PV 绑定之后才知道。 当然,PVC 与 PV 的绑定得以实现的前提是,已经在系统里创建好了符合条件的 PV(比如,我们在前面用到的 pv.yaml)
所以,我们在使用 kubectl create
创建了 StatefulSet 之后,就会看到 Kubernetes 集群里出 现了两个 PVC:
# kubectl get pvc -l app=nginx
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
ww-web-0 Bound pv001 1Gi RWO 8m45s
ww-web-1 Bound pv002 1Gi RWO 8m41s
可以看到,这些 PVC,都"<PVC名字>-<StatefulSet名字>-< 编号 >”的方式命名,并且处于 Bound 状态。
由于我们这里用volumeClaimTemplates
声明的模板是挂载点的方式,并不是 volume,所有实际上是把 PV 的存储挂载到容器中,所以会覆盖掉容器中的数据,在容器启动完成后我们可以手动在 PV 的存储里面新建 index.html 文件来保证容器的正常访问,当然也可以进入到容器中去创建,这样更加方便:
for i in 0 1; do kubectl exec web-$i -- sh -c 'echo hello $(hostname) > /usr/share/nginx/html/index.html'; done
如上所示,通过 kubectl exec
指令,我们在每个 Pod 的 Volume 目录里,写入了一个 index.html 文件。这个文件的内容,正是 Pod 的 hostname。比如,我们在 web-0 的 index.html 里写入的内容就是 "hello web-0"。
# for i in 0 1; do kubectl exec -it web-$i -- sh -c "cat /usr/share/nginx/html/index.html"; done
hello web-0
hello web-1
如果使用kubectl delete
删除这两个pod,这些volume文件会不会丢失呢?
# kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
我们知道,上面删除的两个pod会被按照编号的循环重新创建,那么我们写入的index.html文件是否还在?
# for i in 0 1; do kubectl exec -it web-$i -- sh -c "cat /usr/share/nginx/html/index.html"; done
hello web-0
hello web-1
这个请求依然会返回:hello web-0,hello web-1。也就是说,原先与名叫 web-0 的 Pod 绑定的 PV,在这个 Pod 被重新创建之后,依然同新的名叫 web-0 的 Pod 绑定在了一起。对于 Pod web-1 来说,也是完全一样的情况。
这是怎么做到的呢?
首先,当把一个 Pod,比如 web-0,删除之后,这个 Pod 对应的 PVC 和 PV,并不会被删除,而这个 Volume 里已经写入的数据,也依然会保存在远程存储服务里。
此时,StatefulSet 控制器发现,一个名叫 web-0 的 Pod 消失了。所以,控制器就会重新创建 一个新的、名字还是叫作 web-0 的 Pod 来,“纠正”这个不一致的情况。 需要注意的是,在这个新的 Pod 对象的定义里,它声明使用的 PVC 的名字,还是叫作:ww-web-0。
这个 PVC 的定义,还是来自于 PVC 模板(volumeClaimTemplates
),这是 StatefulSet 创建 Pod 的标准流程。
所以,在这个新的 web-0 Pod 被创建出来之后,Kubernetes 为它查找名叫 ww-web-0 的 PVC 时,就会直接找到旧 Pod 遗留下来的同名的 PVC,进而找到跟这个 PVC 绑定在一起的 PV。 这样,新的 Pod 就可以挂载到旧 Pod 对应的那个 Volume,并且获取到保存在 Volume 里的数据。
通过这种方式,Kubernetes 的 StatefulSet 就实现了对应用存储状态的管理。
更新策略
在 StatefulSet 中同样也支持两种升级策略:onDelete
和 RollingUpdate
,同样可以通过设置 .spec.updateStrategy.type
进行指定。
OnDelete
: 该策略表示当更新了StatefulSet
的模板后,只有手动删除旧的 Pod 才会创建新的 Pod。RollingUpdate
:该策略表示当更新 StatefulSet 模板后会自动删除旧的 Pod 并创建新的Pod,如果更新发生了错误,这次“滚动更新”就会停止。不过需要注意 StatefulSet 的 Pod 在部署时是顺序从 0~n 的,而在滚动更新时,这些 Pod 则是按逆序的方式即 n~0 依次删除并创建。
另外SatefulSet
的滚动升级还支持 Partitions
特性,通过.spec.updateStrategy.rollingUpdate.partition
进行设置,在设置 partition 后,SatefulSet 的 Pod 中序号大于或等于 partition 的 Pod 会在 StatefulSet 的模板更新后进行滚动升级,而其余的 Pod 保持不变。
......
updateStrategy:
rollingUpdate: # 如果更新的策略是OnDelete,那么rollingUpdate就失效
partition: 2 # 表示从第2个分区开始更新,默认是0
type: RollingUpdate /OnDelete # 滚动更新/删除之后更新
总结
1、 StatefulSet 的控制器直接管理的是 Pod。这是因为,StatefulSet 里的不同 Pod 实例, 不再像 ReplicaSet 中那样都是完全一样的,而是有了细微区别的。
比如,每个 Pod 的 hostname、名字等都是不同的、携带了编号的。而 StatefulSet 区分这些实例的方式,就是通过在 Pod 的名字里加上事先约定好的编号。
2、Kubernetes 通过 Headless Service,为这些有编号的 Pod在 DNS 服务器中生成带有同样编号的 DNS 记录。只要 StatefulSet 能够保证这些 Pod 名字里的编号不变。
那么 Service 里类似于web-0.nginx.default.svc.cluster.local
这样的 DNS 记录也就不会变,而这条记录解 析出来的 Pod 的 IP 地址,则会随着后端 Pod 的删除和再创建而自动更新。
这当然是 Service 机制本身的能力,不需要 StatefulSet 操心。
3、StatefulSet 还为每一个 Pod 分配并创建一个同样编号的 PVC。这样,Kubernetes 就可以通过 Persistent Volume 机制为这个 PVC 绑定上对应的 PV,从而保证了每一个 Pod 都拥有 一个独立的 Volume。
在这种情况下,即使 Pod 被删除,它所对应的 PVC 和 PV 依然会保留下来。所以当这个 Pod 被重新创建出来之后,Kubernetes 会为它找到同样编号的 PVC,挂载这个 PVC 对应的 Volume,从而获取到以前保存在 Volume 里的数据。
Kubernetes如何通过StatefulSet支持有状态应用?的更多相关文章
- Kubernetes系列(四) StatefulSet
作者: LemonNan 原文地址: https://juejin.im/post/6870071267438329869 Kubernetes系列(四) StatefulSet Kubernetes ...
- 我的Android进阶之旅------>Android中StateListDrawable支持的状态
Android中StateListDrawable支持的状态 android:state_active 代表是否处于激活状态 android:state_checked 代表是否处于已勾选状态 an ...
- Springboot整合Spring Cloud Kubernetes读取ConfigMap,支持自动刷新配置
1 前言 欢迎访问南瓜慢说 www.pkslow.com获取更多精彩文章! Docker & Kubernetes相关文章:容器技术 之前介绍了Spring Cloud Config的用法,但 ...
- Kubernetes如何支持有状态服务的部署?
作者:Jack47 转载请保留作者和原文出处 PS:如果喜欢我写的文章,欢迎关注我的微信公众账号程序员杰克,两边的文章会同步,也可以添加我的RSS订阅源. Kubernetes对无状态服务有完善的支持 ...
- [Kubernetes]深入理解StatefulSet
前面我写的一系列博客,如果你能够耐心看到这一篇,那你应该对一个概念就不是太陌生了:Deployment. 为什么提这个概念呢,这就要说到Deployment的一个不足了.Deployment不足以覆盖 ...
- 在kubernetes中运行单节点有状态MySQL应用
Deploy MySQL https://kubernetes.io/docs/tasks/run-application/run-single-instance-stateful-applicati ...
- Statefulset的拓扑状态
Statefulset: 实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,就被称为“有状态应用”(Stateful Application). StatefulSet 的设计其实非常容易理解 ...
- Kubernetes部署通用手册 (支持版本1.19,1.18,1.17,1.16)
Kubernetes平台环境规划 操作环境 rbac 划分(HA高可用双master部署实例) 本文穿插了ha 高可用部署的实例,当前章节设计的是ha部署双master 部署 内网ip 角色 安装软件 ...
- Kubernetes v1.6开始支持RBAC
Kubernetes v1.6的一个亮点就是RBAC认证特性成为了beta版本.RBAC,基于角色的访问控制(Role-Based Access Control),是用于管理Kubernetes资源访 ...
- 【Kubernetes】Kubernetes删除namespace后持续terminating状态
删除isti和foo的配置文件之后,namespace持续terminating状态,此时也无法再创建istio-system的namespace namespace "istio-syst ...
随机推荐
- 解决Dcat Admin laravel框架登录报错问题,(blocked:mixed-content)
前言 在使用 Dcat Admin 后台登录时,发生 error 报错:(blocked:mixed-content) xhr VM484:1,浏览器拦截 其实这是浏览器在 HTTPS 页面中尝试加载 ...
- go 地址对齐保证
unsafe标准库包 func Alignof(variable ArbitraryType) uintptr. 此函数用来取得一个值在内存中的地址对齐保证(address alignment gua ...
- Liunx配置sudo使oracle用户有root权限执行脚本
1. vi /etc/sudoers 将%wheel 两行前的注释# 删除 2. vi /etc/group 将oracle用户 加入 wheel组
- Oracle体系结构和用户管理
本篇博客将对Oracle的体系结构.存储结构.内存结构和进程结构进行初步介绍,从而从宏观上把握它的物理组成.文件组成和各种进程,对于进一步的了解可以起到很好地作用 一.Oralce体系结构 1.概述 ...
- 掌握 K8s Pod 基础应用 (二)
Pod生命周期 我们一般将pod对象从创建至终的这段时间范围称为pod的生命周期,它主要包含下面的过程: pod创建过程 运行初始化容器(init container)过程 运行主容器(main co ...
- [每日算法 - 华为机试] leetcode45 :跳跃游戏 II 「动态规划神器推荐」
leetcode入口 45. 跳跃游戏 IIhttps://leetcode.cn/problems/jump-game-ii/ 题目描述 给定一个长度为 n 的 0 索引整数数组 nums.初始位置 ...
- Vim 操作-替换
Vim 操作-替换 substitute :[range]s[ubstitute]/{pattern}/{string}/{flag} 替换的操作范围以行为基础: %-全局范围,m,n-使用逗号隔开的 ...
- Electron 客户端开机自启动
app.setLoginItemSettings 与 auto-launch 对比分析 一.稳定性对比 1. app.setLoginItemSettings 优点:作为Electron官方API,有 ...
- 学习Kotlin语法(三)
简介 在上一节,我们对Kotlin中面向对象编程(OOP)的相关知识有了大致的了解,本章节我们将去进一步了解函数.lambada表达式.内联函数.操作符重载.作用域函数. 目录 函数 函数的使用 参数 ...
- SEO老了?GEO来了!玩转传统搜索+AI搜索,吸引眼球大作战!
上网冲浪.查资料.找游戏攻略.看爱豆新闻--你们肯定天天都在用搜索引擎,对吧?比如百度.谷歌啥的.但你们有没有想过,为啥有些网站总排在前面,有些却石沉大海?这背后可有"潜规则"! ...