网站首页 > 基础教程 正文
本章涵盖
- 用于表示磁盘和状态的 Kubernetes 构造
- 为 Pods 添加持久存储
- 使用 StatefulSet 部署具有领导角色的多 Pod 有状态应用程序
- 通过将 Kubernetes 对象重新链接到磁盘资源来迁移和恢复数据
- 为 Pods 提供大容量的临时存储卷
有状态应用程序(即具有附加存储的工作负载)终于在 Kubernetes 中找到了归宿。虽然无状态应用程序因其易于部署和高可扩展性而受到赞誉,这在很大程度上得益于避免了附加和管理存储的需求,但这并不意味着有状态应用程序没有其存在的价值。无论您是要部署一个复杂的数据库,还是将旧的有状态应用程序从虚拟机(VM)迁移过来,Kubernetes 都能满足您的需求。
使用持久卷,您可以将有状态存储附加到任何 Kubernetes Pod。当涉及到具有状态的多副本工作负载时,正如 Kubernetes 为管理无状态应用程序提供了 Deployment 这一高级构造,StatefulSet 则存在于为有状态应用程序提供高级管理。
9.1 卷、持久卷、声明和存储类
要开始在 Kubernetes 中存储状态,需要在转向更高级的 StatefulSet 构造之前,先了解一些关于卷(磁盘)管理的概念。就像节点是 Kubernetes 对虚拟机的表示一样,Kubernetes 也有自己对磁盘的表示。
9.1.1 卷数
Kubernetes 为 Pods 提供了挂载卷的功能。什么是卷?文档是这样描述的:
在其核心,卷只是一个目录,可能包含一些数据,可以被 Pod 中的容器访问。该目录的形成、支持它的介质以及其内容由所使用的特定卷类型决定。
Kubernetes 附带了一些内置的卷类型,其他类型可以通过存储驱动程序由您的平台管理员添加。您可能经常遇到的一种类型是 emptyDir ,它是与节点生命周期相关的临时卷,ConfigMap,允许您在 Kubernetes 清单中指定文件,并将其作为磁盘上的文件呈现给您的应用程序,以及用于持久存储的云提供商磁盘。
EmptyDir 卷
内置的卷类型 emptyDir 是一个临时卷,它从节点的启动磁盘中分配空间。如果 Pod 被删除或移动到另一个节点,或者节点本身变得不健康,则所有数据都会丢失。那么,这有什么好处呢?
Pod 可以有多个容器,并且 emptyDir 挂载可以在它们之间共享。因此,当您需要在容器之间共享数据时,您可以定义一个 emptyDir 卷并在 Pod 中的每个容器中挂载它(列表 9.1)。数据在容器重启之间也会被保留,只是不会在我之前提到的所有其他事件中保留。这对于短暂数据是有用的,例如磁盘缓存的数据,在 Pod 重启之间保留数据是有益的,但不需要长期存储。
清单 9.1 Chapter09/9.1.1_Volume/emptydir_pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: emptydir-pod
labels:
pod: timeserver-pod
spec:
containers:
- name: timeserver-container
image: docker.io/wdenniss/timeserver:5
volumeMounts:
- name: cache-volume ?
mountPath: /app/cache/ ?
volumes:
- name: cache-volume ?
emptyDir: {} ?
? 挂载路径
? 体积定义
这为什么被称为 emptyDir ?因为数据存储在节点上一个最初为空的目录中。在我看来,这个名称不太准确,但你能怎么办呢?
提示 如果您正在寻找工作负载的临时存储空间,请参阅第 9.4 节关于通用临时卷的内容,这是一种更现代的获取临时存储的方法,无需依赖主机卷。
对于一个实际的例子,请参见第 9.2.2 节,其中 emptyDir 用于在同一 Pod 中的两个容器之间共享数据,其中一个是首先运行的初始化容器,可以为主容器执行设置步骤。
ConfigMap 卷
ConfigMap 是一个有用的 Kubernetes 对象。您可以在一个地方定义键值对,并从多个其他对象中引用它们。您还可以用它们来存储整个文件!通常,这些文件会是配置文件,例如 MariaDB 的 my.cnf 、Apache 的 httpd.conf 、Redis 的 redis.conf 等等。您可以将 ConfigMap 挂载为卷,这样它定义的文件就可以从容器中读取。ConfigMap 卷是只读的。
这种技术特别适用于为公共容器镜像定义配置文件,因为它允许您提供配置而无需扩展镜像本身。例如,要运行 Redis,您可以引用官方的 Redis 镜像,并只需在 Redis 期望的位置使用 ConfigMap 挂载您的配置文件——无需构建自己的镜像仅仅为了提供这个文件。有关通过 ConfigMap 卷指定自定义配置文件配置 Redis 的示例,请参见第 9.2.1 节和 9.2.2 节。
云服务提供商卷
更适合构建有状态应用程序,在这种情况下,您通常不希望使用临时或只读卷,而是将云提供商的磁盘挂载为卷。无论您在哪里运行 Kubernetes,您的提供商都应该在集群中提供驱动程序,以便您挂载持久存储,无论是 NFS 还是基于块的(通常两者都有)。
作为示例,以下列表提供了在 Google Kubernetes Engine (GKE) 中运行的 MariaDB Pod 的规格,该 Pod 在 {{0 }} 处挂载 GCE 持久磁盘以实现持久存储,如图 9.1 所示。
清单 9.2 Chapter09/9.1.1_Volume/mariadb_pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: mariadb-demo
labels:
app: mariadb
spec:
nodeSelector:
topology.kubernetes.io/zone: us-west1-a ?
containers:
- name: mariadb-container
image: mariadb:latest
volumeMounts: ?
- mountPath: /var/lib/mysql ?
name: mariadb-volume ?
env:
- name: MARIADB_ROOT_PASSWORD
value: "your database password"
volumes:
- name: mariadb-volume
gcePersistentDisk:
pdName: mariadb-disk ?
fsType: ext4
? 节点选择器针对磁盘所在的区域,以便 Pod 将在同一区域创建
? 磁盘将被挂载的目录
持久磁盘 Google Cloud 资源的名称
与我们接下来要讨论的更自动化和云无关的方法不同,这种方法与您的云提供商相关,并且需要手动创建磁盘。您需要确保指定名称的磁盘存在,这需要您使用云提供商的工具进行创建,并且磁盘和 Pod 必须在同一区域。在这个例子中,我使用 nodeSelector 来定位磁盘的区域,这对于存在于多个区域的任何 Kubernetes 集群都很重要;否则,您的 Pod 可能会被调度到与磁盘不同的区域。
通过以下命令可以创建此示例所使用的离线磁盘:
gcloud compute disks create --size=10GB --zone=us-west1-a mariadb-disk
注意:此示例及随附的云提供商特定说明是为了完整性而提供的,并说明了卷的开发方式,但这并不是使用卷的推荐方法。请继续阅读,以了解使用 PersistentVolumes 和 StatefulSet 创建磁盘的更好、平台无关的方法!
由于我们是手动创建这个磁盘,请密切注意资源创建的位置。前一个命令中的区域和通过 nodeSelector 配置设置的区域需要匹配。如果您看到您的 Pod 卡在 Container Creating ,请检查您的事件日志以获取答案。这是一个我没有在正确项目中创建磁盘的案例:
$ kubectl get events -w
0s Warning FailedAttachVolume pod/mariadb-demo
AttachVolume.Attach failed for volume "mariadb-volume" : GCE persistent disk
not found: diskName="mariadb-disk" zone="us-west1-a"
直接挂载卷的缺点是磁盘需要在 Kubernetes 之外创建,这意味着以下几点:
- 创建 Pod 的用户必须具有创建磁盘的权限,但这并不总是如此。
- 在 Kubernetes 配置之外存在需要记住并手动运行的步骤。
- 卷描述符是平台依赖的,因此这个 Kubernetes YAML 不可移植,无法在其他提供商上工作。
当然,Kubernetes 对这种缺乏可移植性的问题有解决方案。通过使用 Kubernetes 提供的持久卷抽象,您可以简单地请求所需的磁盘资源,并让它们为您配置,无需执行任何带外步骤。继续阅读。
9.1.2 持久卷和声明
为了以更具平台无关性的方式管理存储卷,Kubernetes 提供了更高级的原语:持久卷(PersistentVolume,PV)和持久卷声明(PersistentVolumeClaim,PVC)。Pod 不直接链接到存储卷,而是引用一个持久卷声明对象,该对象以平台无关的术语定义 Pod 所需的磁盘资源(例如,“1GB 存储”)。磁盘资源本身在 Kubernetes 中使用持久卷对象表示,就像 Kubernetes 中的节点表示虚拟机资源一样。当持久卷声明被创建时,Kubernetes 将寻求通过创建或与持久卷匹配来提供声明中请求的资源,并将这两个对象绑定在一起(图 9.2)。一旦绑定,PV 和 PVC 现在相互引用,通常会保持链接,直到底层磁盘被删除。
这种请求资源的声明和表示资源可用性的对象的行为类似于 Pod 如何请求计算资源,如 CPU 和内存,而集群找到具有这些资源的节点来调度 Pod。这也意味着存储请求以平台无关的方式定义。与直接使用云提供商的磁盘不同,当使用 PersistentVolumeClaim 时,您的 Pods 可以在任何地方部署,只要该平台支持持久存储。
让我们重写上一节中的 Pod,以使用 PersistentVolumeClaim 请求一个新的 PersistentVolume。这个 Pod 将附加一个挂载到 /var/lib/mysql 的外部磁盘,MariaDB 在那里存储其数据。
清单 9.3 Chapter09/9.1.2_PersistentVolume/pvc-mariadb.yaml
apiVersion: v1
kind: Pod
metadata:
name: mariadb-demo
labels:
app: mariadb
spec:
containers:
- name: mariadb-container
image: mariadb:latest
volumeMounts: ?
- mountPath: /var/lib/mysql ?
name: mariadb-volume ?
resources: ?
requests: ?
cpu: 1 ?
memory: 4Gi ?
env:
- name: MARIADB_ROOT_PASSWORD
value: "your database password"
volumes:
- name: mariadb-volume
persistentVolumeClaim: ?
claimName: mariadb-pv-claim ?
---
apiVersion: v1
kind: PersistentVolumeClaim ?
metadata:
name: mariadb-pv-claim
spec:
accessModes:
- ReadWriteOnce
resources: ?
requests: ?
storage: 2Gi ?
? MariaDB 数据目录,PVC 支持的卷将在此挂载
? Pod 请求的计算资源
? 引用 persistentVolumeClaim 对象而不是磁盘资源
? 持久卷声明对象
? Pod 请求的存储资源
在 PersistentVolumeClaim 定义中,我们请求 2 GiB 的存储并指定所需的 accessMode 。 ReadWriteOnce 访问模式适用于像传统硬盘一样的卷,其中您的存储被挂载到单个 Pod 以进行读/写访问,这是最常见的。 accessMode 的其他选择是 ReadOnlyMany ,可以用于挂载跨多个 Pod 共享的现有数据卷,以及 ReadWriteMany 用于挂载文件存储(如 NFS),多个 Pod 可以同时读/写(这是一种相当特殊的模式,仅由少数存储驱动程序支持)。在本章中,目标是由传统块存储卷支持的有状态应用程序,因此在整个过程中使用 ReadWriteOnce 。
如果您的提供商支持动态供给,将会创建一个由磁盘资源支持的 PersistentVolume,以满足 PersistentVolumeClaim 请求的存储,之后 PersistentVolumeClaim 和 PersistentVolume 将会绑定在一起。PersistentVolume 的动态供给行为通过 StorageClass 定义,我们将在下一节中讨论。GKE 和几乎所有提供商都支持动态供给,并且会有一个默认的存储类,因此列表 9.3 中的前一个 Pod 定义几乎可以在任何地方部署。
在极少数情况下,如果您的提供者没有动态配置,您(或集群操作员/管理员)需要手动创建 PersistentVolume,确保有足够的资源来满足 PersistentVolumeClaim 请求(图 9.3)。Kubernetes 仍然负责将声明与手动创建的 PersistentVolumes 进行匹配。
PersistentVolumeClaim,如前面的例子所定义,可以被视为对资源的请求。对资源的声明发生在与 PersistentVolume 资源匹配并绑定时,这两个资源相互关联。基本上,PersistentVolumeClaim 的生命周期从请求开始,当绑定时变为声明。
我们可以就此打住,但由于您的宝贵数据将存储在这些磁盘上,让我们深入了解一下这个绑定是如何工作的。如果我们在清单 9.3 中创建资源,然后在绑定后查询 PersistentVolumeClaim 的 YAML,您会看到它已被更新为 volumeName 。这个 volumeName 是它所链接并现在声明的 PersistentVolume 的名称。以下是它的样子(为了可读性省略了一些多余的信息):
$ kubectl create -f Chapter09/9.1.2_PersistentVolume/pvc-mariadb.yaml
pod/mariadb-demo created
persistentvolumeclaim/mariadb-pv-claim created
$ kubectl get -o yaml pvc/mariadb-pv-claim
apiVersion: v1
PersistentVolumeClaim
metadata:
name: mariadb-pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageClassName: standard-rwo
volumeMode: Filesystem
volumeName: pvc-ecb0c9ed-9aee-44b2-a1e5-ff70d9d3823a ?
status:
accessModes:
- ReadWriteOnce
capacity:
storage: 2Gi
phase: Bound
? 该对象当前绑定的持久卷
我们可以使用 kubectl get -o yaml pv $NAME 查询此配置中命名的 PersistentVolume,我们会看到它直接链接回 PVC。我的配置如下所示:
$ kubectl get -o yaml pv pvc-ecb0c9ed-9aee-44b2-a1e5-ff70d9d3823a
apiVersion: v1
kind: PersistentVolume
metadata:
name: pvc-ecb0c9ed-9aee-44b2-a1e5-ff70d9d3823a
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 2Gi
claimRef:
apiVersion: v1
kind: PersistentVolumeClaim
name: mariadb-pv-claim ?
namespace: default ?
csi:
driver: pd.csi.storage.gke.io
fsType: ext4
volumeAttributes:
storage.kubernetes.io/csiProvisionerIdentity: 1615534731524-8081-pd.
? csi.storage.gke.io
volumeHandle: projects/gke-autopilot-test/zones/us-west1-b/disks/pvc-ecb
? 0c9ed-9aee-44b2-a1e5-ff70d9d3823a ?
persistentVolumeReclaimPolicy: Delete
storageClassName: standard-rwo
volumeMode: Filesystem
status:
phase: Bound ?
? 该持久卷(PersistentVolume)绑定的持久卷声明(PersistentVolumeClaim)
? 指向底层磁盘资源的指针
状态现在已绑定。
这有助于并排可视化,因此请查看图 9.4。
PersistentVolumeClaim 在这里确实经历了一次蜕变,从资源请求变成了对特定磁盘资源的声明,该资源将包含您的数据。这与我能想到的其他 Kubernetes 对象并不相同。虽然 Kubernetes 通常会在对象上添加字段并执行操作,但像这样的变化很少见,它起初是对存储的通用请求和表示,最终变成了一个绑定的有状态对象。
在持久卷声明的典型生命周期中,有一个例外情况,即当您有现有数据希望挂载到 Pod 中时。在这种情况下,您创建持久卷声明和持久卷对象,并使它们相互指向,因此在创建时立即绑定。此场景在第 9.3 节中讨论,涉及迁移和恢复磁盘,包括一个完整的数据恢复场景。
在本地测试 MariaDB Pod
想要连接到 MariaDB 并检查一切是否设置正确吗?这很简单。只需将 mariadb 容器的端口转发到您的机器:
kubectl port-forward pod/mariadb-demo 3306:3306
然后,从本地 MySQL 客户端连接到它。没有合适的客户端?您可以通过 Docker 运行一个!
docker run --net=host -it --rm mariadb mariadb -h localhost -P 3306 \
-u root -p
数据库密码可以在 Pod 的环境变量中找到(列表 9.3)。一旦连接成功,您可以运行 SQL 查询进行测试,例如:
MariaDB [(none)]> SELECT user, host FROM mysql.user;
+-------------+-----------+
| User | Host |
+-------------+-----------+
| root | % |
| healthcheck | 127.0.0.1 |
| healthcheck | ::1 |
| healthcheck | localhost |
| mariadb.sys | localhost |
| root | localhost |
+-------------+-----------+
6 rows in set (0.005 sec)
MariaDB [(none)]> CREATE DATABASE foo;
Query OK, 1 row affected (0.006 sec)
MariaDB [(none)]> exit
Bye
9.1.3 存储类
到目前为止,我们一直依赖于平台提供商的默认动态供应行为。但是,如果我们想在绑定过程中更改获取的磁盘类型怎么办?或者,如果删除 PersistentVolumeClaim,数据会发生什么?这就是存储类的作用。
存储类是一种描述可从 PersistentVolumeClaims 请求的不同类型动态存储的方法,以及以这种方式请求的卷应如何配置。您的 Kubernetes 集群可能已经定义了一些。让我们通过 kubectl get storageclass 查看它们(输出中的某些列已被删除以提高可读性):
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY
premium-rwo pd.csi.storage.gke.io Delete
standard kubernetes.io/gce-pd Delete
standard-rwo (default) pd.csi.storage.gke.io Delete
当我们在上一节中使用 PersistentVolumeClaim 创建 Pod 时,使用了默认存储类( standard-rwo ,在这种情况下)。如果你回去查看绑定的 PersistentVolumeClaim 对象,你会在 storageClassName 下的配置中看到这个存储类。
这是一个相当不错的开始,您可能不需要做太多更改,但有一个方面可能值得审查。如果您查看之前从 kubectl get storageclass 输出的 RECLAIMPOLICY 列,您可能会注意到它指出 Delete 。这意味着如果 PVC 被删除,绑定的 PV 及其支持的磁盘资源也将被删除。如果您的有状态工作负载主要只是存储非关键数据的缓存服务,这可能没问题。然而,如果您的工作负载存储独特且珍贵的数据,这种默认行为就不理想。
Kubernetes 还提供了 Retain 回收策略,这意味着在删除 PVC 时,底层磁盘资源不会被删除。这允许您保留磁盘,并将其绑定到新的 PV 和 PVC,甚至可能是您在完全不同的集群中创建的一个(当迁移工作负载时,您可能会这样做)。 Retain 的缺点,以及它通常不是默认设置的原因,是您需要手动删除不希望保留的磁盘,这对于测试和开发,或具有短暂数据(如缓存)的工作负载并不理想。
要构建我们自己的 StorageClass,最简单的方法是从现有的一个开始,作为模板,例如当前的默认值。我们可以按如下方式导出之前列出的默认 StorageClass。如果您的 StorageClass 名称与我的不同,请将 standard-rwo 替换为您想要修改的存储类:
kubectl get -o yaml storageclass standard-rwo > storageclass.yaml
现在我们可以自定义并设置至关重要的 Retain 回收策略。由于我们想要创建一个新策略,因此为其命名并去除 uid 及其他不需要的元数据字段也很重要。在执行这些步骤后,我得到了以下列表。
清单 9.4 Chapter09/9.1.3_StorageClass/storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
annotations:
storageclass.kubernetes.io/is-default-class: "true" ?
name: example-default-rwo
parameters:
type: pd-balanced ?
provisioner: pd.csi.storage.gke.io
reclaimPolicy: Retain ?
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
? 可选注释以设置为默认值
? 平台特定值以设置存储类型
? 配置为在 PV 被删除时保留磁盘
您可以在任何 PersistentVolumeClaim 对象或模板中直接引用您的新 StorageClass,使用 storageClassName 字段。这是一个很好的选择,比如,如果您只想对少数工作负载使用保留回收策略。
可选地,您可以通过添加列表 9.4 中显示的 is-default-class 注释来设置新的默认存储类。如果您想更改默认值,您需要将当前默认值标记为非默认。您可以使用 kubectl edit storageclass standard-rwo 进行编辑,或使用以下单行命令进行修补。再次,将 standard-rwo 替换为您的默认类的名称:
kubectl patch storageclass standard-rwo -p '{"metadata": {"annotations":
? {"storageclass.kubernetes.io/is-default-class":"false"}}}'
准备好后,使用 kubectl create -f storageclass.yaml 创建新的存储类。如果您更改了默认值,任何新创建的 PersistentVolume 将使用您的新 StorageClass。
通常会为不同类型的数据定义多个具有不同性能和保留特性的存储类。例如,您可能有需要快速存储和保留的数据库关键生产数据,有利于高性能但可以删除的缓存数据,以及可以使用平均性能磁盘且不需要保留的批处理临时存储。根据您的偏好选择一个好的默认值,并通过在 PersistentVolumeClaim 中指定 storageClassName 手动引用其他值。
9.1.4 单个 Pod 有状态工作负载部署
利用 PersistentVolumeClaims,我们可以通过简单地将 Pod 封装到 Deployment 中来配置一个单副本的有状态工作负载。即使对于单副本 Pod,使用 Deployment 的好处在于,如果 Pod 被终止,它将被重新创建。
清单 9.5 Chapter09/9.1.4_部署_MariaDB/mariadb-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mariadb-demo
spec:
replicas: 1
selector:
matchLabels:
app: mariadb
strategy: ?
type: Recreate ?
template: ?
metadata:
labels:
app: mariadb
spec:
containers:
- name: mariadb-container
image: mariadb:latest
volumeMounts:
- mountPath: /var/lib/mysql
name: mariadb-volume
resources:
requests:
cpu: 1
memory: 4Gi
env:
- name: MARIADB_ROOT_PASSWORD
value: "your database password"
volumes:
- name: mariadb-volume
persistentVolumeClaim:
claimName: mariadb-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mariadb-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
重新创建用于防止多个副本在发布期间尝试挂载同一卷的策略。
? Pod 模板规格与第 9.1.2 节中显示的完全相同。
所以,我们有了这个。这是一个 MariaDB 数据库的单个 Pod 部署,附加的磁盘即使在整个 Kubernetes 集群被删除的情况下也不会被删除,这要归功于我们在前一部分创建的默认存储类中的 Retain 策略。
如果您想试用这个数据库,请为其创建一个服务(Chapter09/9.1.4_ Deployment_MariaDB/service.yaml)。一旦服务创建完成,您可以从本地客户端连接到数据库(请参见侧边栏“在本地测试 MariaDB Pod”),或者您可以尝试使用容器化的 phpMyAdmin(请参见与本书附带的代码库中的 Bonus/phpMyAdmin 文件夹)。
在 Kubernetes 中运行数据库
在您决定在 Kubernetes 中管理自己的 MariaDB 数据库之前,您可能想要寻找云服务提供商的托管解决方案。我知道直接在 Kubernetes 中部署是很有诱惑的,因为创建这样的数据库相对简单,正如我所演示的。然而,运营成本会在您需要进行安全、更新和管理时显现出来。一般来说,我建议将 Kubernetes 的有状态工作负载功能保留用于定制或专门服务,或者是您的云服务提供商未提供的托管服务。
如本节所示,我们可以通过使用 PersistentVolumeClaims 附加卷来使我们的工作负载具有状态。然而,使用 Pod 和 Deployment 对象会限制我们只能使用单副本的有状态工作负载。这对于某些情况可能足够,但如果您有像 Elasticsearch 或 Redis 这样的复杂有状态工作负载,并且需要多个副本怎么办?您可以尝试将多个 Deployments 拼接在一起,但幸运的是,Kubernetes 有一个高级构造,专门用于表示这种类型的工作负载,称为 StatefulSet。
- 上一篇: php 发送微信订阅消息
- 下一篇: JWT: 使用JWT+PHP实现登录认证
猜你喜欢
- 2025-01-18 JWT: 使用JWT+PHP实现登录认证
- 2025-01-18 php 发送微信订阅消息
- 2025-01-18 weiphp图灵机器人存在的bug
- 2025-01-18 X语言解析器C++实现(模糊探索篇)
- 2025-01-18 100 个常见的 PHP 面试题和答案分享
- 2025-01-18 PHP 运算符和表达式
- 2025-01-18 php提示undefined index的几种解决方法
- 2025-01-18 通过冒泡排序测试Java和PHP性能
- 2025-01-18 PHP使用Phar打包控制台程序
- 2025-01-18 PHP8内置函数中的变量函数-PHP8知识详解
- 05-162025前端最新面试题之HTML和CSS篇
- 05-16大数据开发基础之HTML基础知识
- 05-16微软专家告诉你Win10 Edge浏览器和EdgeHTML的区别
- 05-16快速免费将网站部署到公网方法(仅支持HTML,CSS,JS)
- 05-16《从零开始学前端:HTML+CSS+JavaScript的黄金三角》
- 05-16一个简单的标准 HTML 设计参考
- 05-16css入门
- 05-16前端-干货分享:更牛逼的CSS管理方法-层(CSS Layers)
- 最近发表
- 标签列表
-
- jsp (69)
- pythonlist (60)
- gitpush (78)
- gitreset (66)
- python字典 (67)
- dockercp (63)
- gitclone命令 (63)
- dockersave (62)
- linux命令大全 (65)
- pythonif (86)
- location.href (69)
- dockerexec (65)
- deletesql (62)
- c++模板 (62)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- console.table (62)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)