GlusterFS在Kubernetes中的应用

GlusterFS服务简介

GlusterFS是一个可扩展,分布式文件系统,集成来自多台服务器上的磁盘存储资源到单一全局命名空间,以提供共享文件存储。

特点:

  • 可以扩展到几PB容量
  • 支持处理数千个客户端
  • 兼容POSIX接口
  • 使用通用硬件,普通服务器即可构建
  • 能够使用支持扩展属性的文件系统,例如ext4,XFS
  • 支持工业标准的协议,例如NFS,SMB
  • 提供很多高级功能,例如副本,配额,跨地域复制,快照以及bitrot检测
  • 支持根据不同工作负载进行调优

GFS存储的一些术语:

  • Brick:GlusterFS中的存储单元,通常是一个受信存储池中的服务器的一个目录。可以通过主机名和目录名来标识,如'SERVER:EXPORT'。
  • Node:一个拥有若干brick的设备。
  • Volume:一组bricks的逻辑集合。
  • Client:挂载了GlusterFS卷的设备。
  • GFID:GlusterFS卷中的每个文件或目录都有一个唯一的128位的数据相关联,其用于模拟inode

    Namespace:每个Gluster卷都导出单个ns作为POSIX的挂载点。
  • RDMA:远程直接内存访问,支持不通过双方的OS进行直接内存访问。
  • RRDNS:round robin DNS是一种通过DNS轮转返回不同的设备以进行负载均衡的方法
  • Self-heal:用于后台运行检测复本卷中文件和目录的不一致性并解决这些不一致。
  • Split-brain:脑裂
  • Volfile:Glusterfs进程的配置文件,通常位于/var/lib/glusterd/vols/volname

GFS卷(volume)的模式:

Volume是一组Brick的组合,一个gfs集群中可以有多个volume,可以供客户端挂载、存储数据等,它有很多种模式供选择:

  1. 分布式卷(默认模式):即DHT,文件通过hash算法分布存在不同的brick里,单个brick失效会带来数据丢失,无需额外元数据服务器。
  2. 条带卷:即Striped,文件切分成一个个的chunk,存放于不同的brick上,只建议在非常大的文件时使用。
  3. 复制卷:即AFR,同一份数据会同步在多个Brick中,单一节点故障时保持数据高可用,事务性操作,保持一致性。
  4. 分布式复制卷:即AFR和DHT的组合,最少需要4台服务器才能创建,读操作可以做到负载均衡。
  5. 条带复制卷:Striped与AFR 的组合,最少需要4台服务器才能创建。
  6. 分布式条带复制卷:至少需要8台服务器才能创建,每四台服务器一组。

GlusterFS服务搭建及进程解析

主机名 ip地址 角色
master 192.168.72.100 k8s-master、glusterfs、heketi
node1 192.168.72.101 k8s-work节点
node2 192.168.72.102 k8s-work节点
  1. 获取gluster rpm包,我这里是7.5版本的,需要下载以下几个rpm包:

    # ls
    glusterfs-7.5-1.el7.x86_64.rpm glusterfs-libs-7.5-1.el7.x86_64.rpm
    glusterfs-api-7.5-1.el7.x86_64.rpm glusterfs-server-7.5-1.el7.x86_64.rpm
    glusterfs-api-devel-7.5-1.el7.x86_64.rpm psmisc-22.20-15.el7.x86_64.rpm
    glusterfs-cli-7.5-1.el7.x86_64.rpm userspace-rcu-0.10.0-3.el7.x86_64.rpm
    glusterfs-client-xlators-7.5-1.el7.x86_64.rpm userspace-rcu-0.7.16-1.el7.x86_64.rpm
    glusterfs-fuse-7.5-1.el7.x86_64.rpm

    地址:https://buildlogs.centos.org/centos/7/storage/x86_64/gluster-9/Packages/;如果有外网可以直接配置yum源安装需要的版本。

  2. 安装rpm包

    ## 先安装依赖的环境包
    yum -y install attr psmisc rpcbind ## 安装glusterfs
    rpm -ivh glusterfs-libs-7.5-1.el7.x86_64.rpm
    rpm -ivh glusterfs-7.5-1.el7.x86_64.rpm
    rpm -ivh glusterfs-api-7.5-1.el7.x86_64.rpm glusterfs-cli-7.5-1.el7.x86_64.rpm glusterfs-client-xlators-7.5-1.el7.x86_64.rpm
    rpm -ivh glusterfs-fuse-7.5-1.el7.x86_64.rpm
    rpm -ivh userspace-rcu-0.10.0-3.el7.x86_64.rpm
    rpm -ivh glusterfs-server-7.5-1.el7.x86_64.rpm
    rpm -ivh glusterfs-api-devel-7.5-1.el7.x86_64.rpm

    因为包之间会有依赖关系,按照以上顺序可以顺利安装,如果不想这么麻烦,可强制安装--force --nodeps。

  3. 启动服务

    ## 需要启动两个服务glusterd和glusterfsd
    systemctl start glusterd glusterfsd
  4. glusterd和glusterfsd两个服务的区别,以及gluster和glusterfs命令

    glusterd是其服务的守护进程,是一个管理模块,处理gluster发过来的命令,处理集群管理、存储池管理、brick管理、负载均衡、快照管理等,默认端口24007:

    # ss -tnlp | grep 24007
    LISTEN 0 128 *:24007 *:* users:(("glusterd",pid=7873,fd=10)) ## 进程
    # ps -ef | grep gluster
    root 7873 1 0 Mar28 ? 00:00:41 /usr/sbin/glusterd -p /var/run/glusterd.pid --log-level INFO

    glusterfsd是服务端模块,存储池中的每个brick(卷组)都会启动一个glusterfsd进程。此模块主要是处理客户端的读写请求,默认端口是从49152开始,后续端口都+1。我这里启动了两个卷组:

    # ss -tnlp | grep gluster
    LISTEN 0 128 *:49152 *:* users:(("glusterfsd",pid=7898,fd=11))
    LISTEN 0 128 *:49153 *:* users:(("glusterfsd",pid=44275,fd=11)) ## 进程
    # ps -ef | grep gluster
    root 7898 1 0 Mar28 ? 00:04:46 /usr/sbin/glusterfsd -s 192.168.72.100 --volfile-id xtest-fs.192.168.72.100.data-text-mind -p /var/run/gluster/vols/xtest-fs/192.168.72.100-data-text-mind.pid -S /var/run/gluster/3b5c03678c749dfd.socket --brick-name /data/text-mind -l /var/log/glusterfs/bricks/data-text-mind.log --xlator-option *-posix.glusterd-uuid=46bfd5ab-07be-4a97-993f-1ddab13e0ee9 --process-name brick --brick-port 49152 --xlator-option xtest-fs-server.listen-port=49152

    glusterfs命令是一个客户端模块,负责通过mount挂载集群中某台服务器的存储池,以目录的形式呈现给用户。用户mount几个卷组,就是出现几个glusterfs的进程,如下:

    # ps -ef | grep -w glusterfs | grep -v glusterfsd
    root 31496 1 0 Mar25 ? 00:02:46 /usr/sbin/glusterfs --process-name fuse --volfile-server=yq01-aip-aikefu10.yq01.baidu.com --volfile-id=xtest-fs /data/mnt

    gluster命令就是客户端请求命令,负责发起请求,比如创建、查看当前卷组等:

    # gluster volume create xtest-fs 192.168.72.100:/data/xtest-fs/brick force
    # gluster volume list
  5. 如果是多台gfs构成集群,在服务安装完毕之后,要进行一个添加的动作

    ## 添加
    # gluster peer probe node1
    # gluster peer probe node2 ## 查看当前集群
    # gluster pool list

heketi服务搭建以及管理GFS

heketi提供一个RESTful管理节点界面,可以用来管理GlusterFS卷的生命周期,通过heketi,就可以像使用Opentack Manila,kubernete和openShift一样申请可以动态配置GlusterFS卷,Heketi会动态在集群内选择bricks构建所需的volumes,这样以确保数据的副本会分散到集群不同的故障域内。

  1. 安装heketi服务,有源可以直接yum安装,没有可以单独下载rpm包安装,或者通过docker、k8s安装

    # yum -y install heketi heketi-client
    
    # heketi --version
    Heketi 9.0.0 # systemctl start heketi.service
  2. 配置ssh秘钥,使heketi节点可以免密访问glusterfs节点

    # ssh-keygen -f /etc/heketi/heketi_key -t rsa -N ''
    # ssh-copy-id -i /etc/heketi/heketi_key.pub master
    # chown heketi:heketi /etc/heketi/
  3. 编辑heketi的配置文件/etc/heketi/heketi.json

    heketi有三种执行方式,分别为mock,ssh,kubernetes,官方建议在测试和开发环境使用mock,生产使用ssh,如果glusterfs是部署在k8s上,则使用kubernetes方式。

    下边对配置部分做出解释:

    {
    ## heketi服务的默认端口,可以更改。
    "_port_comment": "Heketi Server Port Number",
    "port": "8080", ## 是否开启认证,一般选是。
    "_use_auth": "Enable JWT authorization. Please enable for deployment",
    "use_auth": true, ## 若开启认证,则需要编辑认证的用户及密码。
    "_jwt": "Private keys for access",
    "jwt": {
    "_admin": "Admin has access to all APIs",
    "admin": {
    "key": "My Secret"
    },
    "_user": "User only has access to /volumes endpoint",
    "user": {
    "key": "My Secret"
    }
    }, "_glusterfs_comment": "GlusterFS Configuration",
    "glusterfs": {
    "_executor_comment": [
    "Execute plugin. Possible choices: mock, ssh",
    "mock: This setting is used for testing and development.",
    " It will not send commands to any node.",
    "ssh: This setting will notify Heketi to ssh to the nodes.",
    " It will need the values in sshexec to be configured.",
    "kubernetes: Communicate with GlusterFS containers over",
    " Kubernetes exec api."
    ],
    ## 选择操作gfs的方式,这里选择ssh。
    "executor": "ssh", ## 配置ssh秘钥
    "_sshexec_comment": "SSH username and private key file information",
    "sshexec": {
    "keyfile": "/etc/heketi/heketi_key",
    "user": "root",
    "port": "22",
    "fstab": "/etc/fstab"
    }, ## 配置k8s证书
    "_kubeexec_comment": "Kubernetes configuration",
    "kubeexec": {
    "host" :"https://kubernetes.host:8443",
    "cert" : "/path/to/crt.file",
    "insecure": false,
    "user": "kubernetes username",
    "password": "password for kubernetes user",
    "namespace": "OpenShift project or Kubernetes namespace",
    "fstab": "Optional: Specify fstab file on node. Default is /etc/fstab"
    }, "_db_comment": "Database file name",
    ## heketi的数据存储位置
    "db": "/var/lib/heketi/heketi.db", "_loglevel_comment": [
    "Set log level. Choices are:",
    " none, critical, error, warning, info, debug",
    "Default is warning"
    ],
    ## heketi的日志级别
    "loglevel" : "debug"
    }
    }
  4. 启动heketi服务,设置开启自启,测试访问

    # systemctl start heketi
    # systemctl enable heketi
    # curl http://master:8080/hello
    Hello from Heketi
  5. 配置heketi连接gfs服务

    (1) 在集群的主节点设置环境变量,注意端口根据实际情况修改

    # export HEKETI_CLI_SERVER=http://master:8080

    (2) 在集群的主节点修改/usr/share/heketi/topology-sample.json配置文件,执行添加节点和添加device的操作:

    {
    "clusters": [
    {
    "nodes": [
    {
    "node": {
    "hostnames": {
    "manage": [
    "master"
    ],
    "storage": [
    "192.168.72.100"
    ]
    },
    "zone": 1
    },
    "devices": [
    {
    "name": "/dev/sdb",
    "destroydata": false
    }
    ]
    }
    ]
    }
    ]
    }

    manage指定gfs主机域名,storage指定gfs主机ip,devices下指定要存储gfs数据的数据盘,此盘应该是一块未分区的干净盘,分区应该也可以,我这里没有试。

    可以添加多个主机和多块盘,具体格式如下:

    {
    "clusters": [
    {
    "nodes": [
    {
    "node": {
    "hostnames": {
    "manage": [
    "192.168.10.100"
    ],
    "storage": [
    "192.168.10.100"
    ]
    },
    "zone": 1
    },
    "devices": [
    {
    "name": "/dev/sdb",
    "destroydata": false
    },
    {
    "name": "/dev/sdc",
    "destroydata": false
    },
    {
    "name": "/dev/sdd",
    "destroydata": false
    },
    {
    "name": "/dev/sde",
    "destroydata": false
    },
    {
    "name": "/dev/sdf",
    "destroydata": false
    },
    {
    "name": "/dev/sdg",
    "destroydata": false
    },
    {
    "name": "/dev/sdh",
    "destroydata": false
    },
    {
    "name": "/dev/sdi",
    "destroydata": false
    }
    ]
    },
    {
    "node": {
    "hostnames": {
    "manage": [
    "192.168.10.101"
    ],
    "storage": [
    "192.168.10.101"
    ]
    },
    "zone": 2
    },
    "devices": [
    {
    "name": "/dev/sdb",
    "destroydata": false
    },
    {
    "name": "/dev/sdc",
    "destroydata": false
    },
    {
    "name": "/dev/sdd",
    "destroydata": false
    },
    {
    "name": "/dev/sde",
    "destroydata": false
    },
    {
    "name": "/dev/sdf",
    "destroydata": false
    },
    {
    "name": "/dev/sdg",
    "destroydata": false
    },
    {
    "name": "/dev/sdh",
    "destroydata": false
    },
    {
    "name": "/dev/sdi",
    "destroydata": false
    }
    ]
    },
    {
    "node": {
    "hostnames": {
    "manage": [
    "192.168.10.102"
    ],
    "storage": [
    "192.168.10.102"
    ]
    },
    "zone": 1
    },
    "devices": [
    {
    "name": "/dev/sdb",
    "destroydata": false
    },
    {
    "name": "/dev/sdc",
    "destroydata": false
    },
    {
    "name": "/dev/sdd",
    "destroydata": false
    },
    {
    "name": "/dev/sde",
    "destroydata": false
    },
    {
    "name": "/dev/sdf",
    "destroydata": false
    },
    {
    "name": "/dev/sdg",
    "destroydata": false
    },
    {
    "name": "/dev/sdh",
    "destroydata": false
    },
    {
    "name": "/dev/sdi",
    "destroydata": false
    }
    ]
    },
    {
    "node": {
    "hostnames": {
    "manage": [
    "192.168.10.103"
    ],
    "storage": [
    "192.168.10.103"
    ]
    },
    "zone": 2
    },
    "devices": [
    {
    "name": "/dev/sdb",
    "destroydata": false
    },
    {
    "name": "/dev/sdc",
    "destroydata": false
    },
    {
    "name": "/dev/sdd",
    "destroydata": false
    },
    {
    "name": "/dev/sde",
    "destroydata": false
    },
    {
    "name": "/dev/sdf",
    "destroydata": false
    },
    {
    "name": "/dev/sdg",
    "destroydata": false
    },
    {
    "name": "/dev/sdh",
    "destroydata": false
    },
    {
    "name": "/dev/sdi",
    "destroydata": false
    }
    ]
    }
    ]
    }
    ]
    }

    (3) 导入配置文件到heketi,使heketi管理gfs

    # heketi-cli -s $HEKETI_CLI_SERVER --user admin --secret 'My Secret' topology load --json=/usr/share/heketi/topology-sample.json
    Creating cluster ... ID: 6b7b8affdb10add55ea367e7e2f8e091
    Allowing file volumes on cluster.
    Allowing block volumes on cluster.
    Creating node master ... ID: 96bbfbf66bc8ad551fe5a8c8ef289218
    Adding device /dev/sdb ... OK

    如果报错:Error: Unable to get topology information: Invalid JWT token: Token missing iss claim。是因为没加用户名和密码 --user admin --secret 'My Secret'。

    (4) 测试创建一个volume,看是否成功

    # heketi-cli volume create --size=10 --durability=none --user "admin" --secret "My Secret"
    Name: vol_38412746b79cf0d7ffa69a3969c1cfed
    Size: 10
    Volume Id: 38412746b79cf0d7ffa69a3969c1cfed
    Cluster Id: 6b7b8affdb10add55ea367e7e2f8e091
    Mount: 192.168.72.100:vol_38412746b79cf0d7ffa69a3969c1cfed
    Mount Options: backup-volfile-servers=
    Block: false
    Free Size: 0
    Reserved Size: 0
    Block Hosting Restriction: (none)
    Block Volumes: []
    Durability Type: none
    Distribute Count: 1

    创建成功,heketi已经可以管理gfs服务。

k8s集群创建StorageClass实现动态管理pv pvc

  1. 创建StorageClass

    vi storageclass-gfs-heketi-distributed.yaml

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
    name: gluster-distributed
    provisioner: kubernetes.io/glusterfs
    reclaimPolicy: Retain
    parameters:
    resturl: "http://192.168.72.100:8080"
    restauthenabled: "true"
    restuser: "admin"
    restuserkey: "My Secret"
    gidMin: "40000"
    gidMax: "50000"
    volumetype: "none"
    allowVolumeExpansion: true
    volumeBindingMode: Immediate

    provisioner:表示存储分配器,需要根据后端存储的不同而变更;

    reclaimPolicy: 默认即“Delete”,删除pvc后,相应的pv及后端的volume,brick(lvm)等一起删除;设置为”Retain”时则保留数据,需要手工处理

    resturl:heketi API服务提供的url;

    restauthenabled:可选参数,默认值为"false",heketi服务开启认证时必须设置为"true";

    restuser:可选参数,开启认证时设置相应用户名;

    restuserkey:可选参数。开启认证时输入对应用户的密码;

    secretNamespace:可选参数,开启认证时可以设置为使用持久化存储的namespace;

    secretName:可选参数,开启认证时,需要将heketi服务的认证密码保存在secret资源中;

    clusterid:可选参数,指定集群id,也可以是1个clusterid列表,格式为"id1,id2";

    volumetype:可选参数,设置卷类型及其参数,如果未分配卷类型,则有分配器决定卷类型;如"volumetype: replicate:3"表示3副本的replicate卷,"volumetype: disperse:4:2"表示disperse卷,其中‘4’是数据,’2’是冗余校验,"volumetype: none"表示distribute卷;

    allowVolumeExpansion:表示是否支持动态扩容,默认为true;

    volumeBindingMode:表示是否立即bound pv,可选值WaitForFirstConsumer和Immediate;

    创建sc:

    # kubectl create -f storageclass-gfs-heketi-distributed.yaml
    
    # kubectl get sc
    NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION
    gluster-distributed kubernetes.io/glusterfs Retain Immediate true
  2. 创建pvc

    vi glusterfs-pvc.yaml

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
    name: glusterfs-test
    spec:
    accessModes:
    - ReadWriteMany
    resources:
    requests:
    storage: 5Gi
    storageClassName: gluster-distributed

    创建pvc

    # kubectl create -f glusterfs-pvc.yaml
    # kubectl get pvc
    NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
    glusterfs-test Bound pvc-0a3cffea-36b0-4609-a983-88df995b99e8 5Gi RWX gluster-distributed 5s

    这个时候 df -h 就可以看到有新的分区被挂载

    # df -h | tail -n 1
    /dev/mapper/vg_5b4ef948902b85845afd387ca71c6858-brick_0fd0afa344ca798d7a9fccab5bb60a9e 5.0G 33M 5.0G 1% /var/lib/heketi/mounts/vg_5b4ef948902b85845afd387ca71c6858/brick_0fd0afa344ca798d7a9fccab5bb60a9e
  3. 创建一个deployment,测试读写

    vi glusterfs-deploy.yaml

    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: glusterfs-deploy
    labels:
    app: nginx
    spec:
    replicas: 2
    selector:
    matchLabels:
    app: nginx
    template:
    metadata:
    labels:
    app: nginx
    spec:
    containers:
    - name: nginx
    image: registry:5000/nginx:1.13
    imagePullPolicy: IfNotPresent
    ports:
    - name: web
    containerPort: 80
    volumeMounts:
    - name: pvc-glusterfs
    mountPath: /data
    volumes:
    - name: pvc-glusterfs
    persistentVolumeClaim:
    claimName: glusterfs-test

    创建deployment

    # kubectl create -f glusterfs-deploy.yaml
    deployment.apps/glusterfs-deploy created # kubectl get pod | grep glus
    glusterfs-deploy-cfd8c8578-45r9z 1/1 Running 0 72s
    glusterfs-deploy-cfd8c8578-vjrzn 1/1 Running 0 104s

    测试读写

    # kubectl exec -it glusterfs-deploy-cfd8c8578-45r9z -- bash
    # cd /data/
    # for i in `seq 1 10`; do echo $i > $i ; done
    # ls
    1 10 2 3 4 5 6 7 8 9
    # cat 1
    1

至此,gfs已经可以作为k8s的storageclass来动态管理pv pvc了。

k8s中应用GlusterFS类型StorageClass的更多相关文章

  1. 【Kubernetes】在K8s中创建StatefulSet

    在K8s中创建StatefulSet 遇到的问题: 使用Deployment创建的Pod是无状态的,当挂在Volume之后,如果该Pod挂了,Replication Controller会再run一个 ...

  2. 在K8s中创建StatefulSet

    在K8s中创建StatefulSet 遇到的问题: 使用Deployment创建的Pod是无状态的,当挂在Volume之后,如果该Pod挂了,Replication Controller会再run一个 ...

  3. k8s 中的 Pod 细节了解

    k8s中Pod的理解 基本概念 k8s 为什么使用 Pod 作为最小的管理单元 如何使用 Pod 1.自主式 Pod 2.控制器管理的 Pod 静态 Pod Pod的生命周期 Pod 如何直接暴露服务 ...

  4. k8s中yaml文常见语法

    在k8s中,所有的配置都是 json格式的.但为了读写方便,通常将这些配置写成yaml 格式,其运行的时候,还是会靠yaml引擎将其转化为json,apiserver 也仅接受json的数据类型. y ...

  5. 在k8s中的基本概念

    在k8s中的基本概念 一.Pod1. podk8s下最重要也最基本的概念,由一个根容器Pause和许多用户业务容器组成,是容器的载体. 2. pod的yaml定义格式及字段 apiVersion: v ...

  6. [转帖]k8s 中的服务如何沟通

    k8s 中的服务如何沟通 https://www.jianshu.com/p/9fae09876eb7 本文将介绍 k8s 中的服务如何相互访问,例如后端服务访问数据库,不同类型的服务间的相互访问.并 ...

  7. K8s中的网络

    Kubernetes的网络通信问题: 1. 容器间通信: 即同一个Pod内多个容器间通信,通常使用loopback来实现. 2. Pod间通信: K8s要求,Pod和Pod之间通信必须使用Pod-IP ...

  8. Kubernetes之在k8s中部署Java应用

    部署好了k8s以后 部署参考https://www.cnblogs.com/minseo/p/12055731.html 怎么在k8s部署应用 项目迁移到k8s平台是怎样的流程 1,制作镜像 2,控制 ...

  9. K8s中Pod健康检查源代码分析

    了解k8s中的Liveness和Readiness Liveness: 表明是否容器正在运行.如果liveness探测为fail,则kubelet会kill掉容器,并且会触发restart设置的策略. ...

随机推荐

  1. Redis 回收进程如何工作的?

    一个客户端运行了新的命令,添加了新的数据.Redi 检查内存使用情况,如 果大于 maxmemory 的限制, 则根据设定好的策略进行回收.一个新的命令被执 行,等等.所以我们不断地穿越内存限制的边界 ...

  2. Java 中,Serializable 与 Externalizable 的区别?

    Serializable 接口是一个序列化 Java 类的接口,以便于它们可以在网络上传输 或者可以将它们的状态保存在磁盘上,是 JVM 内嵌的默认序列化方式,成本高. 脆弱而且不安全.Externa ...

  3. 关于 DispatcherServlet.properties 文件

    1.文件位置 2.文件内容 3.文件作用 前端控制器会从 DispatcherServlet.properties 文件中加载 HandlerMapping(处理器映射器).HandlerAdapte ...

  4. servlet中的HttpServletResponse对象

    当有多个客户端浏览器去请求Tomcat时,Tomcat会为每一个客户端浏览器创建一对独立的HttpServletRequest与HttpServletResponse对象 HttpServletRes ...

  5. 自动驾驶运动规划-Reeds Shepp曲线

    自动驾驶运动规划-Reeds Shepp曲线 相比于Dubins Car只允许车辆向前运动,Reeds Shepp Car既允许车辆向前运动,也允许车辆向后运动. Reeds Shepp Car运动规 ...

  6. 微信小程序开发:python+sanic 实现小程序登录注册

    开发微信小程序时,接入小程序的授权登录可以快速实现用户注册登录的步骤,是快速建立用户体系的重要一步.这篇文章将介绍 python + sanic + 微信小程序实现用户快速注册登录全栈方案. 微信小程 ...

  7. 从八道面试题看JavaScript DOM事件机制

    As we all know,事件机制其实很简单,无非冒泡和捕获这两点,笔者不再赘述,网上相关文章一大堆,下面让我们直接看面试题 题目一到七,统一设置css .test2 { height: 50px ...

  8. JAVA处理Excel表格数据并写入数据库

    package com.hncj.test; import java.io.FileInputStream; import java.sql.Connection; import java.sql.D ...

  9. 微信小程序列表拖动排序Demo

    wxml页面编写 <view class="container"> <view bindtap="box" class="box&q ...

  10. Python入门-变量与命名

    在上一篇中,我们定义了很多变量,变量格式是啥?变量名字可以随意么?有啥命名规范么?下面细讲 变量格式 变量名称 = 常量 把常量赋值给变量的过程,就是定义变量. 定义变量 Python中的变量不需要声 ...