内容简介:在参考文档:
在 Docker
或者 Kubernetes
中使用 Ceph RBD
块设备,相比于在宿主机中,是否会对性能造成额外损失?带着这些疑问对相关技术进行原理分析。
Linux中的Mount绑定传播
参考文档:
- Shared Subtrees ;
- Mount namespaces and shared subtrees ;
- Mount namespaces, mount propagation, and unbindable mounts 。
Linux的Mount绑定关系
Linux
Mount
命名空间通过隔离文件系统挂载点对隔离文件系统提供支持,它是历史上第一个 Linux
Namespace
,所以它的标识位比较特殊,就是 CLONE_NEWNS
。隔离后,不同 Mount
Namespace
中的文件结构发生变化也互不影响。你可以通过 /proc/[pid]/mounts
查看到所有挂载在当前 Namespace 中的文件系统,还可以通过 /proc/[pid]/mountstats
看到 Mount
Namespace
中文件设备的统计信息,包括挂载文件的名字、文件系统类型、挂载位置等等。
进程在创建 Mount
Namespace
时,会把当前的文件结构复制给新的 Namespace
。新 Namespace
中的所有 Mount 操作都只影响自身的文件系统,而对外界不会产生任何影响。这样做非常严格地实现了隔离,但是某些情况可能并不适用。比如父节点 Namespace
中的进程挂载了一张 CD-ROM
,这时子节点 Namespace
拷贝的目录结构就无法自动挂载上这张 CD-ROM
,因为这种操作会影响到父节点的文件系统。
2006 年引入的挂载传播( Mount
Propagation
)解决了这个问题,挂载传播定义了挂载对象( Mount
Object
)之间的关系,系统用这些关系决定任何挂载对象中的挂载事件如何传播到其他挂载对象(参考自: http://www.ibm.com/developerworks/library/l-mount-namespaces/ )。所谓传播事件,是指由一个挂载对象的状态变化导致的其它挂载对象的挂载与解除挂载动作的事件。
- 共享关系(
Share
Relationship
)。如果两个挂载对象具有共享关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,反之亦然。 - 从属关系(
Slave
Relationship
)。如果两个挂载对象形成从属关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,但是反过来不行;在这种关系中,从属对象是事件的接收者。
一个挂载状态可能为如下的其中一种:
- 共享挂载(
Shared
) - 从属挂载(
Slave
) - 共享
/
从属挂载(Shared
And
Slave
) - 私有挂载(
Private
) - 不可绑定挂载(
Unbindable
)
传播事件的挂载对象称为共享挂载( Shared
Mount
);接收传播事件的挂载对象称为从属挂载( Slave
Mount
)。既不传播也不接收传播事件的挂载对象称为私有挂载( Private
Mount
)。另一种特殊的挂载对象称为不可绑定的挂载( Unbindable
Mount
),它们与私有挂载相似,但是不允许执行绑定挂载,即创建 Mount
Namespace
时这块文件对象不可被复制。
共享挂载的应用场景非常明显,就是为了文件数据的共享所必须存在的一种挂载方式;从属挂载更大的意义在于某些“只读”场景;私有挂载其实就是纯粹的隔离,作为一个独立的个体而存在;不可绑定挂载则有助于防止没有必要的文件拷贝,如某个用户数据目录,当根目录被递归式的复制时,用户目录无论从隐私还是实际用途考虑都需要有一个不可被复制的选项。
- 默认情况下,所有挂载都是私有的。设置为共享挂载的命令如下:
$ mount --make-shared <mount-object>
从共享挂载克隆的挂载对象也是共享的挂载;它们相互传播挂载事件。
- 设置为从属挂载的命令如下:
$ mount --make-slave <shared-mount-object>
从从属挂载克隆的挂载对象也是从属的挂载,它也从属于原来的从属挂载的主挂载对象。
- 将一个从属挂载对象设置为共享 / 从属挂载,可以执行如下命令或者将其移动到一个共享挂载对象下:
$ mount --make-shared <slave-mount-object>
- 如果你想把修改过的挂载对象重新标记为私有的,可以执行如下命令:
$ mount --make-private <mount-object>
- 通过执行以下命令,可以将挂载对象标记为不可绑定的:
$ mount --make-unbindable <mount-object>
Linux的Mount绑定测试
- 查看本机块设备:
$ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT sda 8:0 0 222.6G 0 disk ├─sda1 8:1 0 200M 0 part /boot └─sda2 8:2 0 222.4G 0 part ├─centos-root 253:0 0 122.4G 0 lvm / └─centos-home 253:1 0 100G 0 lvm /home
- 创建和绑定目录:
$ mkdir /opt/tmp /mnt/tmp /mnt/tmp1 /mnt/tmp2 $ mount --bind /opt/tmp /mnt/tmp $ mount --bind /mnt/tmp1 /mnt/tmp2
- 查看绑定目录的详细信息:
$ cat /proc/self/mountinfo | grep /mnt/tmp 549 40 253:0 /opt/tmp /mnt/tmp rw,relatime shared:1 - xfs /dev/mapper/centos-root rw,seclabel,attr2,inode64,noquota 583 40 253:0 /mnt/tmp1 /mnt/tmp2 rw,relatime shared:1 - xfs /dev/mapper/centos-root rw,seclabel,attr2,inode64,noquota
可以看到两个绑定目录都是共享的,且共享 ID
为 1
,父目录在 253:0
设备上。
在 Docker 中使用数据卷的主要方式
参考文档:
-
Docker
支持的挂载模式:
- 创建一个数据卷(卷挂载):
$ docker run --rm -it -v /data1 centos:7 bash # Or $ docker run --rm -it -v data1:/data1 centos:7 bash # Or $ docker run --rm -it --mount target=/data1 centos:7 bash # Or $ docker run --rm -it --mount type=volume,target=/data1 centos:7 bash # Or $ docker run --rm -it --mount type=volume,source=data1,target=/data1 centos:7 bash $ docker ps | awk 'NR==2 {print $1}' | xargs -i docker inspect -f '{{.State.Pid}}' {} | xargs -i cat /proc/{}/mountinfo | grep data 1029 1011 253:0 /var/lib/docker/volumes/239be79a64f7fa6ec815b1d9f2a7773a678ee5c8c1150f03ca81b0d5177b36a0/_data /data1 rw,relatime master:1 - xfs /dev/mapper/centos-root rw,seclabel,attr2,inode64,noquota
- 映射一个外部卷(绑定挂载):
$ docker run --rm -it -v /opt:/data2 centos:7 bash # Or $ docker run --rm -it --mount type=bind,source=/opt,target=/data2 centos:7 bash $ docker ps | awk 'NR==2 {print $1}' | xargs -i docker inspect -f '{{.State.Pid}}' {} | xargs -i cat /proc/{}/mountinfo | grep data 1029 1011 253:0 /opt /data2 rw,relatime - xfs /dev/mapper/centos-root rw,seclabel,attr2,inode64,noquota
- 使用数据型容器(卷挂载):
$ docker create --name vc -v /data1 centos:7 $ docker run --rm -it --volumes-from vc centos:7 bash $ docker ps | awk 'NR==2 {print $1}' | xargs -i docker inspect -f '{{.State.Pid}}' {} | xargs -i cat /proc/{}/mountinfo | grep data 1029 1011 253:0 /var/lib/docker/volumes/fe71f2d0ef18beb92cab7b99afcc5f501e47ed18224463e8c1aa1e8733003803/_data /data1 rw,relatime master:1 - xfs /dev/mapper/centos-root rw,seclabel,attr2,inode64,noquota
- 带打包数据的容器(卷挂载):
编辑 Dockerfile
FROM busybox:latest ADD htdocs /usr/local/apache2/htdocs VOLUME /usr/local/apache2/htdocs
创建容器
$ mkdir htdocs $ echo `date` > htdocs/test.txt $ docker build -t volume-test . $ docker create --name vc2 -v /data1 volume-test $ docker run --rm -it --volumes-from vc2 volume-test sh / # cat /proc/self/mountinfo | grep htdocs 1034 1011 253:0 /var/lib/docker/volumes/54f47af60b8fb25602f022dcd8ad5b3e1a93a2d20c1045184a70391d9bed69b6/_data /usr/local/apache2/htdocs rw,relatime master:1 - xfs /dev/mapper/centos-root rw,seclabel,attr2,inode64,noquota $ docker ps | awk 'NR==2 {print $1}' | xargs -i docker inspect -f '{{.State.Pid}}' {} | xargs -i cat /proc/{}/mountinfo | grep htdocs 1034 1011 253:0 /var/lib/docker/volumes/54f47af60b8fb25602f022dcd8ad5b3e1a93a2d20c1045184a70391d9bed69b6/_data /usr/local/apache2/htdocs rw,relatime master:1 - xfs /dev/mapper/centos-root rw,seclabel,attr2,inode64,noquota
- 使用临时外部卷(临时挂载):
$ docker run --rm -it --mount type=tmpfs,target=/data1 centos:7 bash $ docker ps | awk 'NR==2 {print $1}' | xargs -i docker inspect -f '{{.State.Pid}}' {} | xargs -i cat /proc/{}/mountinfo | grep data 1029 1011 0:160 / /data1 rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,seclabel
在Docker中使用块设备的相关测试
- 在容器内通过绑定挂载使用宿主机的块设备,只能使用已经在宿主机格式化和挂载好的目录:
$ docker run --rm -it -v /data1 -v /opt:/data2 centos:7 bash [root@4282b3df2417 /]# mount | grep data /dev/sdb1 on /data2 type xfs (rw,relatime,attr2,inode64,noquota) /dev/sda1 on /data1 type xfs (rw,relatime,attr2,inode64,noquota) $ docker inspect 4282b3df2417 | grep -i pid "Pid": 12797, "PidMode": "", "PidsLimit": 0, $ cat /proc/12797/mounts | grep data /dev/sdb1 /data2 xfs rw,relatime,attr2,inode64,noquota 0 0 /dev/sda1 /data1 xfs rw,relatime,attr2,inode64,noquota 0 0
- 在容器内通过共享设备使用块设备,可以读写,但是不能挂载:
$ docker run --rm -it --device /dev/sdc:/dev/sdc centos:7 bash [root@55423f5eaeea /]# mkfs -t minix /dev/sdc 21856 inodes 65535 blocks Firstdatazone=696 (696) Zonesize=1024 Maxsize=268966912 [root@55423f5eaeea /]# mknod /dev/sdd b 8 48 [root@55423f5eaeea /]# mkfs -t minix /dev/sdd mkfs.minix: cannot open /dev/sdd: Operation not permitted [root@55423f5eaeea /]# rm /dev/sdc rm: remove block special file '/dev/sdc'? y [root@55423f5eaeea /]# mknod /dev/sdc b 8 32 [root@55423f5eaeea /]# mkfs -t minix /dev/sdc 21856 inodes 65535 blocks Firstdatazone=696 (696) Zonesize=1024 Maxsize=268966912 [root@55423f5eaeea /]# mount /dev/sdc mnt/ [root@55423f5eaeea /]# mount: permission denied [root@55423f5eaeea /]# dd if=/dev/sdc of=/dev/null bs=512 count=10 10+0 records in 10+0 records out 5120 bytes (5.1 kB) copied, 0.000664491 s, 7.7 MB/s [root@55423f5eaeea /]# dd if=/dev/zero of=/dev/sdc bs=512 count=10 10+0 records in 10+0 records out 5120 bytes (5.1 kB) copied, 0.00124138 s, 4.1 MB/s
- 在容器内通过特权模式使用块设备,可以读写和挂载:
$ docker run --rm -it --privileged=true centos:7 bash [root@b5c40e199476 /]# mount /dev/sdc mnt [root@b5c40e199476 /]# mkfs -t minix /dev/sdc mount: unknown filesystem type 'minix' [root@b5c40e199476 /]# yum install -y xfsprogs [root@b5c40e199476 /]# mkfs.xfs /dev/sdc -f meta-data=/dev/sdc isize=512 agcount=4, agsize=6553600 blks = sectsz=512 attr=2, projid32bit=1 = crc=1 finobt=0, sparse=0 data = bsize=4096 blocks=26214400, imaxpct=25 = sunit=0 swidth=0 blks naming =version 2 bsize=4096 ascii-ci=0 ftype=1 log =internal log bsize=4096 blocks=12800, version=2 = sectsz=512 sunit=0 blks, lazy-count=1 realtime =none extsz=4096 blocks=0, rtextents=0 [root@b5c40e199476 /]# mount /dev/sdc mnt [root@b5c40e199476 /]# df -h Filesystem Size Used Avail Use% Mounted on overlay 30G 19G 12G 62% / tmpfs 910M 0 910M 0% /dev tmpfs 910M 0 910M 0% /sys/fs/cgroup /dev/sda1 30G 19G 12G 62% /etc/hosts shm 64M 0 64M 0% /dev/shm /dev/sdc 100G 33M 100G 1% /mnt [root@b5c40e199476 /]# echo `date` > /mnt/time.txt [root@b5c40e199476 /]# cat /mnt/time.txt Wed Mar 6 12:23:05 UTC 2019
Kubernetes中的块设备使用和实现
- 查看
kublet
初始化根目录/var/lib/kubelet
时的源码,可以看到kubelet
使用syscall.MS_SHARED|syscall.MS_REC
标志,该目录下下所有的Mount
都默认共享(相当于执行mount --make-rshared /var/lib/kubelet
):
// pkg/kubelet/kubelet.go // setupDataDirs creates: // 1. the root directory // 2. the pods directory // 3. the plugins directory // 4. the pod-resources directory func (kl *Kubelet) setupDataDirs() error { ... if err := kl.mounter.MakeRShared(kl.getRootDir()); err != nil { return fmt.Errorf("error configuring root directory: %v", err) } ... } // pkg/util/mount/nsenter_mount.go func (n *NsenterMounter) MakeRShared(path string) error { return doMakeRShared(path, hostProcMountinfoPath) } // pkg/util/mount/mount_linux.go // doMakeRShared is common implementation of MakeRShared on Linux. It checks if // path is shared and bind-mounts it as rshared if needed. mountCmd and // mountArgs are expected to contain mount-like command, doMakeRShared will add // '--bind <path> <path>' and '--make-rshared <path>' to mountArgs. func doMakeRShared(path string, mountInfoFilename string) error { shared, err := isShared(path, mountInfoFilename) if err != nil { return err } if shared { klog.V(4).Infof("Directory %s is already on a shared mount", path) return nil } klog.V(2).Infof("Bind-mounting %q with shared mount propagation", path) // mount --bind /var/lib/kubelet /var/lib/kubelet if err := syscall.Mount(path, path, "" /*fstype*/, syscall.MS_BIND, "" /*data*/); err != nil { return fmt.Errorf("failed to bind-mount %s: %v", path, err) } // mount --make-rshared /var/lib/kubelet if err := syscall.Mount(path, path, "" /*fstype*/, syscall.MS_SHARED|syscall.MS_REC, "" /*data*/); err != nil { return fmt.Errorf("failed to make %s rshared: %v", path, err) } return nil }
- 创建一个使用
PVC
创建RBD
的Pod
:
$ echo 'apiVersion: v1 kind: Pod metadata: name: nginx-test spec: containers: - name: nginx image: nginx:latest volumeMounts: - name: nginx-test-vol1 mountPath: /data/ readOnly: false volumes: - name: nginx-test-vol1 persistentVolumeClaim: claimName: nginx-test-vol1-claim' | kubectl create -f - pod/nginx-test created
- 查看
PVC
的状态:
$ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE nginx-test-vol1-claim Bound pvc-d6f6b6f8-446c-11e9-bbd8-6c92bf74be54 10Gi RWO ceph-rbd 114s $ kubectl describe pvc nginx-test-vol1-claim Name: nginx-test-vol1-claim Namespace: default StorageClass: ceph-rbd Status: Bound Volume: pvc-d6f6b6f8-446c-11e9-bbd8-6c92bf74be54 Labels: <none> Annotations: pv.kubernetes.io/bind-completed: yes pv.kubernetes.io/bound-by-controller: yes volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/rbd Finalizers: [kubernetes.io/pvc-protection] Capacity: 10Gi Access Modes: RWO VolumeMode: Filesystem Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ProvisioningSucceeded 6m36s persistentvolume-controller Successfully provisioned volume pvc-d6f6b6f8-446c-11e9-bbd8-6c92bf74be54 using kubernetes.io/rbd Mounted By: nginx-test
- 查看
PV
的状态:
$ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-d6f6b6f8-446c-11e9-bbd8-6c92bf74be54 10Gi RWO Delete Bound default/nginx-test-vol1-claim ceph-rbd 105s $ kubectl describe pv pvc-d6f6b6f8-446c-11e9-bbd8-6c92bf74be54 Name: pvc-d6f6b6f8-446c-11e9-bbd8-6c92bf74be54 Labels: <none> Annotations: kubernetes.io/createdby: rbd-dynamic-provisioner pv.kubernetes.io/bound-by-controller: yes pv.kubernetes.io/provisioned-by: kubernetes.io/rbd Finalizers: [kubernetes.io/pv-protection] StorageClass: ceph-rbd Status: Bound Claim: default/nginx-test-vol1-claim Reclaim Policy: Delete Access Modes: RWO VolumeMode: Filesystem Capacity: 10Gi Node Affinity: <none> Message: Source: Type: RBD (a Rados Block Device mount on the host that shares a pod's lifetime) CephMonitors: [172.29.201.125:6789 172.29.201.126:6789 172.29.201.201:6789] RBDImage: kubernetes-dynamic-pvc-db7fcd29-446c-11e9-af81-6c92bf74be54 FSType: RBDPool: k8s RadosUser: k8s Keyring: /etc/ceph/keyring SecretRef: &SecretReference{Name:ceph-k8s-secret,Namespace:,} ReadOnly: false Events: <none>
- 查看创建和映射的
RBD
:
$ rbd ls -p k8s kubernetes-dynamic-pvc-db7fcd29-446c-11e9-af81-6c92bf74be54 $ lsblk | grep rbd0 rbd0 252:0 0 10G 0 disk /var/lib/kubelet/pods/18a8fb7b-446d-11e9-bbd8-6c92bf74be54/volumes/kubernetes.io~rbd/pvc-d6f6b6f8-446c-11e9-bbd8-6c92bf74be54
- 查看
RBD
的挂载信息:
$ cat /proc/self/mountinfo | grep rbd0 313 40 252:0 / /var/lib/kubelet/plugins/kubernetes.io/rbd/mounts/k8s-image-kubernetes-dynamic-pvc-db7fcd29-446c-11e9-af81-6c92bf74be54 rw,relatime shared:262 - ext4 /dev/rbd0 rw,seclabel,stripe=1024,data=ordered 318 40 252:0 / /var/lib/kubelet/pods/18a8fb7b-446d-11e9-bbd8-6c92bf74be54/volumes/kubernetes.io~rbd/pvc-d6f6b6f8-446c-11e9-bbd8-6c92bf74be54 rw,relatime shared:262 - ext4 /dev/rbd0 rw,seclabel,stripe=1024,data=ordered
可以看到 RBD
被挂载在两个位置,一个是 Pod
的 Volume
目录,还有一个是 RBD
插件目录,而且这两个目录都是 shared:262
,说明这两个目录是被绑定的。
- 查看
RBD
挂载目录的位置:
$ cat /proc/self/mountinfo | grep "^40 " 40 0 253:0 / / rw,relatime shared:1 - xfs /dev/mapper/centos-root rw,seclabel,attr2,inode64,noquota
可以看到 RBD
挂载在 253:0
设备上,这是宿主机的根目录所挂载的位置。
- 查看
Pod
挂载的卷目录:
$ cat /proc/self/mountinfo | grep 18a8fb7b-446d-11e9-bbd8-6c92bf74be54 303 40 0:56 / /var/lib/kubelet/pods/18a8fb7b-446d-11e9-bbd8-6c92bf74be54/volumes/kubernetes.io~secret/default-token-zn95h rw,relatime shared:233 - tmpfs tmpfs rw,seclabel 318 40 252:0 / /var/lib/kubelet/pods/18a8fb7b-446d-11e9-bbd8-6c92bf74be54/volumes/kubernetes.io~rbd/pvc-d6f6b6f8-446c-11e9-bbd8-6c92bf74be54 rw,relatime shared:262 - ext4 /dev/rbd0 rw,seclabel,stripe=1024,data=ordered $ cat /proc/self/mountinfo | grep shared:233 303 40 0:56 / /var/lib/kubelet/pods/18a8fb7b-446d-11e9-bbd8-6c92bf74be54/volumes/kubernetes.io~secret/default-token-zn95h rw,relatime shared:233 - tmpfs tmpfs rw,seclabel
可以看到 Pod
挂载了两个卷,除了之前的 RBD
,还有就是一个存放 Secret
的卷。
- 查看
Pod
的Docker
容器中挂载目录:
$ docker inspect $(docker ps | grep nginx_nginx-test | awk '{print $1}') | grep Mounts -A33 "Mounts": [ { "Type": "bind", "Source": "/var/lib/kubelet/pods/18a8fb7b-446d-11e9-bbd8-6c92bf74be54/volumes/kubernetes.io~rbd/pvc-d6f6b6f8-446c-11e9-bbd8-6c92bf74be54", "Destination": "/data", "Mode": "Z", "RW": true, "Propagation": "rprivate" }, { "Type": "bind", "Source": "/var/lib/kubelet/pods/18a8fb7b-446d-11e9-bbd8-6c92bf74be54/volumes/kubernetes.io~secret/default-token-zn95h", "Destination": "/var/run/secrets/kubernetes.io/serviceaccount", "Mode": "ro,Z", "RW": false, "Propagation": "rprivate" }, { "Type": "bind", "Source": "/var/lib/kubelet/pods/18a8fb7b-446d-11e9-bbd8-6c92bf74be54/etc-hosts", "Destination": "/etc/hosts", "Mode": "Z", "RW": true, "Propagation": "rprivate" }, { "Type": "bind", "Source": "/var/lib/kubelet/pods/18a8fb7b-446d-11e9-bbd8-6c92bf74be54/containers/nginx/190cc168", "Destination": "/dev/termination-log", "Mode": "Z", "RW": true, "Propagation": "rprivate" } ],
可以看到 Docker
的这些卷最后都是通过 Bind
挂载的,而且 Mount
广播使用的是 rprivate
属性。
- 看看
Pod
的容器内Mount
情况:
$ docker exec -it $(docker ps | grep nginx_nginx-test | awk '{print $1}') df -h Filesystem Size Used Avail Use% Mounted on overlay 123G 4.7G 118G 4% / tmpfs 64M 0 64M 0% /dev tmpfs 189G 0 189G 0% /sys/fs/cgroup /dev/rbd0 9.8G 37M 9.7G 1% /data /dev/mapper/centos-root 123G 4.7G 118G 4% /etc/hosts shm 64M 0 64M 0% /dev/shm tmpfs 189G 12K 189G 1% /run/secrets/kubernetes.io/serviceaccount tmpfs 189G 0 189G 0% /proc/acpi tmpfs 189G 0 189G 0% /proc/scsi tmpfs 189G 0 189G 0% /sys/firmware $ docker exec -it $(docker ps | grep nginx_nginx-test | awk '{print $1}') cat /proc/self/mountinfo | grep -e rbd -e serviceaccount 617 599 252:0 / /data rw,relatime - ext4 /dev/rbd0 rw,seclabel,stripe=1024,data=ordered 623 599 0:56 / /run/secrets/kubernetes.io/serviceaccount ro,relatime - tmpfs tmpfs rw,seclabel
可以看到 Pod
的容器内的确主要挂载了 RBD
和 Secret
两个目录。
分析总结
- 在
Docker
中,无论使用哪种方式使用数据卷,实际上都是利用的Linux
的的mount --bind
绑定挂载功能实现。 - 在
Kubernetes
中使用RBD
卷时,首先通过rbd map
到宿主机并进行格式化,然后mount
到宿主机目录,最后把这个宿主机目录mount --bind
到容器的指定目录中使用。
根据原理分析可以初步推断:在宿主机中测试 RBD
读写性能和在 Docker
和 Kubernetes
中分别测试的性能没有本质区别, Docker
和 Kubernetes
本身不会对 RBD
性能造成影响(之后我又使用Fio对其进行完整的性能测试,和这个结论也是一致的)。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Hibernate框架原理及使用
- 浅析volatile原理及其使用
- rsync算法原理及使用
- ThinkPhp缓存原理及使用详解
- 六、protobuf的使用和原理
- 分析 ThreadLocal 原理与使用场景
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
游戏运营:高手进阶之路
饭大官人 / 电子工业出版社 / 2018-1-1 / 79.00元
《游戏运营:高手进阶之路》是一本系统的、成体系的、注重运营效能、强化系统思维、提升专业认知的书籍。《游戏运营:高手进阶之路》几乎完整覆盖了一个游戏运营人员日常工作中的方方面面,并从工作中具体的业务场景出发,归纳整理出各种解决问题的方法论。《游戏运营:高手进阶之路》为广大游戏从业者建立了完整的知识技能成长体系,包含两大岗位基本功—内容输出和协作推进,四大职业技能—活动策划、版本管理、用户运营、数据分......一起来看看 《游戏运营:高手进阶之路》 这本书的介绍吧!