一、依然简介

Kubernetes使用Persistent Volume和Persistent Volume Claim两种API资源来管理存储。

PersistentVolume(简称PV):由管理员设置的存储,它是集群的一部分。就像节点(Node)是集群中的资源一样,PV也是集群中的资源。它包含存储类型,存储大小和访问模式。它的生命周期独立于Pod,例如当使用它的Pod销毁时对PV没有影响。

PersistentVolumeClaim(简称PVC): 是用户存储的请求。它和Pod类似。Pod消耗Node资源,PVC消耗PV资源。Pod可以请求特定级别的资源(CPU和MEM)。PVC可以请求特定大小和访问模式的PV。

可以通过两种方式配置PV:静态或动态。

静态PV:集群管理员创建许多PV,它们包含可供集群用户使用的实际存储的详细信息。

动态PV:当管理员创建的静态PV都不匹配用户创建的PersistentVolumeClaim时,集群会为PVC动态的配置卷。此配置基于StorageClasses:PVC必须请求存储类(storageclasses),并且管理员必须已创建并配置该类,以便进行动态创建。

二、关于PersistentVolume的访问方式

ReadWriteOnce- 卷以读写方式挂载到单个节点

ReadOnlyMany- 卷以只读方式挂载到多个节点

ReadWriteMany- 卷以读写方式挂载到多个节点

在CLI中,访问模式缩写为:RWO - ReadWriteOnce

ROX - ReadOnlyMany

RWX - ReadWriteMany

重要:卷只能一次使用一种访问模式安装,即使它支持很多。

三、关于回收策略Retain-  手动回收

Recycle-  基本擦洗(rm -rf /thevolume/*)

Delete -  关联的存储资产(如AWS EBS,GCE PD,Azure磁盘或OpenStack Cinder卷)将被删除。

目前,只有NFS和HostPath支持回收。AWS EBS,GCE PD,Azure磁盘和Cinder卷支持删除。

四、关于PersistentVolume(PV)状态Available(可用状态)-   一块空闲资源还没有被任何声明绑定

Bound(绑定状态)       -   声明分配到PVC进行绑定,PV进入绑定状态

Released(释放状态)    -   PVC被删除,PV进入释放状态,等待回收处理

Failed(失败状态)      -   PV执行自动清理回收策略失败

五、关于PersistentVolumeClaims(PVC)状态Pending(等待状态)-   等待绑定PV

Bound(绑定状态)-   PV已绑定PVC

六、在所有k8s节点上安装ceph-common

七、配置静态PV

1.在默认的RBD pool中创建一个1G的image(ceph集群)# ceph osd pool create rbd 128

pool 'rbd' created

# ceph osd lspools

# rbd create ceph-image -s 1G --image-feature layering       ##创建1G的镜像并指定layering特性

# rbd ls

ceph-image

# ceph osd pool application enable rbd ceph-image            ##进行关联

enabled application 'ceph-image' on pool 'rbd'

# rbd info ceph-image

rbd image 'ceph-image':

size 1 GiB in 256 objects

order 22 (4 MiB objects)

id: 13032ae8944a

block_name_prefix: rbd_data.13032ae8944a

format: 2

features: layering

op_features:

flags:

create_timestamp: Sun Jul 29 13:00:36 2018

2.配置ceph secret(ceph+kubernetes)# ceph auth get-key client.admin | base64        ##获取client.admin的keyring值,并用base64编码(ceph集群)

QVFDOUgxdGJIalc4SWhBQTlCOXRNUCs5RUV3N3hiTlE4NTdLVlE9PQ==

# vim ceph-secret.yaml

apiVersion: v1

kind: Secret

metadata:

name: ceph-secret

type: kubernetes.io/rbd

data:

key: QVFDOUgxdGJIalc4SWhBQTlCOXRNUCs5RUV3N3hiTlE4NTdLVlE9PQ==

# kubectl create -f ceph-secret.yaml

secret/ceph-secret created

# kubectl get secret ceph-secret

NAME             TYPE                       DATA   AGE

ceph-secret      kubernetes.io/rbd          1      3s

3.创建PV(kubernetes)# vim ceph-pv.yaml

apiVersion: v1

kind: PersistentVolume

metadata:

name: ceph-pv

spec:

capacity:

storage: 1Gi

accessModes:

- ReadWriteOnce

storageClassName: "rbd"

rbd:

monitors:

- 192.168.100.116:6789

- 192.168.100.117:6789

- 192.168.100.118:6789

pool: rbd

image: ceph-image

user: admin

secretRef:

name: ceph-secret

fsType: xfs

readOnly: false

persistentVolumeReclaimPolicy: Recycle

# kubectl create -f ceph-pv.yaml

persistentvolume/ceph-pv created

# kubectl get pv

NAME    CAPACITY   ACCESS MODES   RECLAIM POLICY

ceph-pv   1Gi         RWO            Recycle

STATUS   CLAIM     STORAGECLASS   REASON    AGE

Available             rbd                   1m

4.创建PVC(kubernetes)# vim ceph-claim.yaml

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

name: ceph-claim

spec:

storageClassName: "rbd"

accessModes:

- ReadWriteOnce

resources:

requests:

storage: 1Gi

# kubectl create -f ceph-claim.yaml

persistentvolumeclaim/ceph-claim created

# kubectl get pvc ceph-claim

NAME        STATUS  VOLUME   CAPACITY  ACCESS MODES  STORAGECLASS  AGE

ceph-claim  Bound   ceph-pv    1Gi     RWO           rbd           20s

# kubectl get pv

NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY

ceph-pv   1Gi        RWO            Recycle

STATUS    CLAIM             STORAGECLASS   REASON    AGE

Bound     default/ceph-claim   rbd                   1m

5.创建pod(kubernetes)# vim ceph-pod1.yaml

apiVersion: v1

kind: Pod

metadata:

name: ceph-pod1

spec:

containers:

- name: ceph-busybox

image: busybox

command: ["sleep", "60000"]

volumeMounts:

- name: ceph-vol1

mountPath: /usr/share/busybox

readOnly: false

volumes:

- name: ceph-vol1

persistentVolumeClaim:

claimName: ceph-claim

# kubectl create -f ceph-pod1.yaml

pod/ceph-pod1 created

# kubectl get pod ceph-pod1

NAME        READY     STATUS    RESTARTS   AGE

ceph-pod1   1/1       Running   0          2m

# kubectl get pod ceph-pod1 -o wide

NAME        READY     STATUS    RESTARTS   AGE   IP            NODE

ceph-pod1   1/1       Running   0          2m    10.244.88.2   node3

6.测试

进入到该Pod中,向/usr/share/busybox目录写入一些数据,之后删除该Pod,再创建一个新的Pod,看之前的数据是否还存在。# kubectl exec -it ceph-pod1 -- /bin/sh

/ # ls

bin   dev   etc   home  proc  root  sys   tmp   usr   var

/ # cd /usr/share/busybox/

/usr/share/busybox # ls

/usr/share/busybox # echo 'Hello from Kubernetes storage' > k8s.txt

/usr/share/busybox # cat k8s.txt

Hello from Kubernetes storage

/usr/share/busybox # exit

# kubectl delete pod ceph-pod1

pod "ceph-pod1" deleted

# kubectl apply -f ceph-pod1.yaml

pod "ceph-pod1" created

# kubectl get pod -o wide

NAME        READY     STATUS    RESTARTS   AGE    IP          NODE

ceph-pod1   1/1       Running   0          15s   10.244.91.3  node02

# kubectl exec ceph-pod1 -- cat /usr/share/busybox/k8s.txt

Hello from Kubernetes storage

八、配置动态PV

1.创建RBD pool(ceph)# ceph osd pool create kube 128

pool 'kube' created

2.授权 kube 用户(ceph)# ceph auth get-or-create client.kube mon 'allow r' osd 'allow class-read, allow rwx pool=kube' -o ceph.client.kube.keyring

# ceph auth get client.kube

exported keyring for client.kube

[client.kube]

key = AQB2cFxbYZtRBhAAi6xcvhEW7SYx3PlBY/0O0Q==

caps mon = "allow r"

caps osd = "allow class-read, allow rwx pool=kube"

注:

Ceph使用术语“capabilities”(caps)来描述授权经过身份验证的用户使用监视器、OSD和元数据服务器的功能。功能还可以根据应用程序标记限制对池中的数据,池中的命名空间或一组池的访问。Ceph管理用户在创建或更新用户时设置用户的功能。

Mon 权限: 包括 r 、 w 、 x 。

OSD 权限: 包括 r 、 w 、 x 、 class-read 、 class-write 。另外,还支持存储池和命名空间的配置。

3.创建 ceph secret(ceph+kubernetes)# ceph auth get-key client.admin | base64              ##获取client.admin的keyring值,并用base64编码

QVFDOUgxdGJIalc4SWhBQTlCOXRNUCs5RUV3N3hiTlE4NTdLVlE9PQ==

# ceph auth get-key client.kube | base64               ##获取client.kube的keyring值,并用base64编码

QVFCMmNGeGJZWnRSQmhBQWk2eGN2aEVXN1NZeDNQbEJZLzBPMFE9PQ==

# vim ceph-kube-secret.yaml

apiVersion: v1

kind: Namespace

metadata:

name: ceph

---

apiVersion: v1

kind: Secret

metadata:

name: ceph-admin-secret

namespace: ceph

type: kubernetes.io/rbd

data:

key: QVFDOUgxdGJIalc4SWhBQTlCOXRNUCs5RUV3N3hiTlE4NTdLVlE9PQ==

---

apiVersion: v1

kind: Secret

metadata:

name: ceph-kube-secret

namespace: ceph

type: kubernetes.io/rbd

data:

key: QVFCMmNGeGJZWnRSQmhBQWk2eGN2aEVXN1NZeDNQbEJZLzBPMFE9PQ==

# kubectl create -f ceph-kube-secret.yaml

namespace/ceph created

secret/ceph-admin-secret created

secret/ceph-kube-secret created

# kubectl get secret -n ceph

NAME                  TYPE                                  DATA   AGE

ceph-admin-secret     kubernetes.io/rbd                     1      13s

ceph-kube-secret      kubernetes.io/rbd                     1      13s

default-token-tq2rp   kubernetes.io/service-account-token   3      13s

4.创建动态RBD StorageClass(kubernetes)# vim ceph-storageclass.yaml

apiVersion: storage.k8s.io/v1

kind: StorageClass

metadata:

name: ceph-rbd

annotations:

storageclass.kubernetes.io/is-default-class: "true"

provisioner: kubernetes.io/rbd

parameters:

monitors: 192.168.100.116:6789,192.168.100.117:6789,192.168.100.118:6789

adminId: admin

adminSecretName: ceph-admin-secret

adminSecretNamespace: ceph

pool: kube

userId: kube

userSecretName: ceph-kube-secret

fsType: xfs

imageFormat: "2"

imageFeatures: "layering"

# kubectl create -f ceph-storageclass.yaml

storageclass.storage.k8s.io/ceph-rbd created

# kubectl get sc

NAME                 PROVISIONER         AGE

ceph-rbd (default)   kubernetes.io/rbd   10s

注:

storageclass.kubernetes.io/is-default-class:注释为true,标记为默认的StorageClass,注释的任何其他值或缺失都被解释为false。

monitors:Ceph监视器,逗号分隔。此参数必需。

adminId:Ceph客户端ID,能够在pool中创建images。默认为“admin”。

adminSecretNamespace:adminSecret的namespace。默认为“default”。

adminSecret:adminId的secret。此参数必需。提供的secret必须具有“kubernetes.io/rbd”类型。

pool:Ceph RBD池。默认为“rbd”。

userId:Ceph客户端ID,用于映射RBD image。默认值与adminId相同。

userSecretName:用于userId映射RBD image的Ceph Secret的名称。它必须与PVC存在于同一namespace中。此参数必需。提供的secret必须具有“kubernetes.io/rbd”类型,例如以这种方式创建:

kubectl create secret generic ceph-secret --type="kubernetes.io/rbd" \ --from-literal=key='QVFEQ1pMdFhPUnQrSmhBQUFYaERWNHJsZ3BsMmNjcDR6RFZST0E9PQ==' \ --namespace=kube-system

fsType:kubernetes支持的fsType。默认值:"ext4"。

imageFormat:Ceph RBD image格式,“1”或“2”。默认值为“1”。

imageFeatures:此参数是可选的,只有在设置imageFormat为“2”时才能使用。目前仅支持的功能为layering。默认为“”,并且未开启任何功能。

默认的StorageClass标记为(default)

5.创建Persistent Volume Claim(kubernetes)

动态卷配置的实现基于StorageClass API组中的API对象storage.k8s.io。

集群管理员可以 StorageClass根据需要定义任意数量的对象,每个对象都指定一个卷插件(也称为 配置器),用于配置卷以及在配置时传递给该配置器的参数集。集群管理员可以在集群中定义和公开多种存储(来自相同或不同的存储系统),每种存储都具有一组自定义参数。此设计还确保最终用户不必担心如何配置存储的复杂性和细微差别,但仍可以从多个存储选项中进行选择。

用户通过在其中包含存储类来请求动态调配存储PersistentVolumeClaim。# vim ceph-pvc.yaml

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

name: ceph-pvc

namespace: ceph

spec:

storageClassName: ceph-rbd

accessModes:

- ReadOnlyMany

resources:

requests:

storage: 1Gi

# kubectl create -f ceph-pvc.yaml

persistentvolumeclaim/ceph-pvc created

# kubectl get pvc -n ceph

NAME       STATUS    VOLUME

ceph-pvc   Bound     pvc-e55fdebe-9487-11e8-b987-000c29e75f2a

CAPACITY   ACCESS MODES   STORAGECLASS   AGE

1Gi        ROX            ceph-rbd       5s

6.创建Pod并测试# vim ceph-pod2.yaml

apiVersion: v1

kind: Pod

metadata:

name: ceph-pod2

namespace: ceph

spec:

containers:

- name: ceph-busybox

image: busybox

command: ["sleep", "60000"]

volumeMounts:

- name: ceph-vol1

mountPath: /usr/share/busybox

readOnly: false

volumes:

- name: ceph-vol1

persistentVolumeClaim:

claimName: ceph-pvc

# kubectl create -f ceph-pod2.yaml

pod/ceph-pod2 created

# kubectl -n ceph get pod ceph-pod2 -o wide

NAME        READY   STATUS   RESTARTS   AGE   IP           NODE

ceph-pod2   1/1     Running  0          3m    10.244.88.2  node03

# kubectl -n ceph exec -it ceph-pod2 -- /bin/sh

/ # echo 'Ceph from Kubernetes storage' > /usr/share/busybox/ceph.txt

/ # exit

# kubectl -n ceph delete pod ceph-pod2

pod "ceph-pod2" deleted

# kubectl apply -f ceph-pod2.yaml

pod/ceph-pod2 created

# kubectl -n ceph get pod ceph-pod2 -o wide

NAME        READY     STATUS    RESTARTS   AGE  IP            NODE

ceph-pod2   1/1       Running   0          2m   10.244.88.2   node03

# kubectl -n ceph exec ceph-pod2 -- cat /usr/share/busybox/ceph.txt

Ceph from Kubernetes storage

九、使用持久卷部署WordPress和MariaDB

1.为MariaDB密码创建一个Secret# kubectl -n ceph create secret generic mariadb-pass --from-literal=password=zhijian

secret/mariadb-pass created

# kubectl -n ceph get secret mariadb-pass

NAME           TYPE      DATA      AGE

mariadb-pass   Opaque    1         37s

2.部署MariaDB

MariaDB容器在PersistentVolume上挂载 /var/lib/mysql。

设置MYSQL_ROOT_PASSWORD环境变量从Secret读取数据库密码。# vim mariadb.yaml

apiVersion: v1

kind: Service

metadata:

name: wordpress-mariadb

namespace: ceph

labels:

app: wordpress

spec:

ports:

- port: 3306

selector:

app: wordpress

tier: mariadb

clusterIP: None

---

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

name: mariadb-pv-claim

namespace: ceph

labels:

app: wordpress

spec:

storageClassName: ceph-rbd

accessModes:

- ReadWriteOnce

resources:

requests:

storage: 2Gi

---

apiVersion: apps/v1

kind: Deployment

metadata:

name: wordpress-mariadb

namespace: ceph

labels:

app: wordpress

spec:

selector:

matchLabels:

app: wordpress

tier: mariadb

strategy:

type: Recreate

template:

metadata:

labels:

app: wordpress

tier: mariadb

spec:

containers:

- image: 192.168.100.100/library/mariadb:5.5

name: mariadb

env:

- name: MYSQL_ROOT_PASSWORD

valueFrom:

secretKeyRef:

name: mariadb-pass

key: password

ports:

- containerPort: 3306

name: mariadb

volumeMounts:

- name: mariadb-persistent-storage

mountPath: /var/lib/mysql

volumes:

- name: mariadb-persistent-storage

persistentVolumeClaim:

claimName: mariadb-pv-claim

# kubectl create -f mariadb.yaml

service/wordpress-mariadb created

persistentvolumeclaim/mariadb-pv-claim created

deployment.apps/wordpress-mariadb created

# kubectl -n ceph get pvc                            ##查看PVC

NAME               STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE

mariadb-pv-claim   Bound     pvc-33dd4ae7-9bb0-11e8-b987-000c29e75f2a   2Gi        RWO            ceph-rbd       2m

# kubectl -n ceph get pod                            ##查看Pod

NAME                                READY     STATUS    RESTARTS   AGE

wordpress-mariadb-f4d44db9c-fqchx   1/1       Running   0          1m

3.部署WordPress# vim wordpress.yaml

apiVersion: v1

kind: Service

metadata:

name: wordpress

namespace: ceph

labels:

app: wordpress

spec:

ports:

- port: 80

selector:

app: wordpress

tier: frontend

type: LoadBalancer

---

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

name: wp-pv-claim

namespace: ceph

labels:

app: wordpress

spec:

accessModes:

- ReadWriteOnce

resources:

requests:

storage: 2Gi

---

apiVersion: apps/v1

kind: Deployment

metadata:

name: wordpress

namespace: ceph

labels:

app: wordpress

spec:

selector:

matchLabels:

app: wordpress

tier: frontend

strategy:

type: Recreate

template:

metadata:

labels:

app: wordpress

tier: frontend

spec:

containers:

- image: 192.168.100.100/library/wordpress:4.9.8-apache

name: wordpress

env:

- name: WORDPRESS_DB_HOST

value: wordpress-mariadb

- name: WORDPRESS_DB_PASSWORD

valueFrom:

secretKeyRef:

name: mariadb-pass

key: password

ports:

- containerPort: 80

name: wordpress

volumeMounts:

- name: wordpress-persistent-storage

mountPath: /var/www/html

volumes:

- name: wordpress-persistent-storage

persistentVolumeClaim:

claimName: wp-pv-claim

# kubectl create -f wordpress.yaml

service/wordpress created

persistentvolumeclaim/wp-pv-claim created

deployment.apps/wordpress created

# kubectl -n ceph get pvc wp-pv-claim                 ##查看PVC

NAME          STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE

wp-pv-claim   Bound     pvc-334b3bd9-9bde-11e8-b987-000c29e75f2a   2Gi        RWO            ceph-rbd       46s

# kubectl -n ceph get pod                             ##查看Pod

NAME                                READY     STATUS    RESTARTS   AGE

wordpress-5c4ffdcb85-6ftfx          1/1       Running   0          1m

wordpress-mariadb-f4d44db9c-fqchx   1/1       Running   0          5m

# kubectl -n ceph get services wordpress              ##查看Service

NAME       TYPE          CLUSTER-IP      EXTERNAL-IP  PORT(S)     AGE

wordpress  LoadBalancer  10.244.235.175     80:32473/TCP 4m

注:本篇文章参考了该篇文章,感谢:

ceph存放mysql备份_Kubernetes持久化Ceph存储相关推荐

  1. 《MySQL技术内幕:InnoDB存储引擎》第2版笔记

    第1章 MySQL体系结构和存储引擎 1.1 定义数据库和实例 在MySQL数据库中,数据库文件可以是fm.MYD.MYI.ibd结尾的文件. MySQL数据库由后台线程以及一个共享内存区组成. My ...

  2. mysql 自动备份_如何将mysql备份自动存储到minio

    概述 minio 是开源企业级对象存储系统,有着高性能.使用简单.易扩展.兼容性强等特性.下面分享一下如何把mysql备份自动存储在minio中. 一.前提条件 安装minio客户端mc 已经安装好的 ...

  3. Ceph学习笔记2-在Kolla-Ansible中使用Ceph后端存储

    环境说明 使用 Kolla-Ansible 请参考<使用 Kolla-Ansible 在 CentOS 7 单节点上部署 OpenStack Pike >: 部署 Ceph 服务请参考&l ...

  4. docker mysql data volume_Docker 持久化存储, Data Volume/Bind Mounting

    docker容器, 再启动之后 我们可以对其进行 修改删除等等. 如果是一个数据库的容器, 里面的数据 不想随着这个容器的消失, 而消失.  就需要持久化数据存储. Data Volume 这是 do ...

  5. 【CEPH-初识篇】ceph详细介绍+“ 一 ” 篇解决ceph集群搭建, “ 三 ” 大(对象、块、文件)存储使用

    文章目录 前言 简介(理论篇) 逻辑结构 数据存储原理 三大存储 RADOSGW(对象网关) BRD(块存储) CEPHFS(文件存储) 所有组件结合起来 POOL.PG简介 组件结合 搭建ceph( ...

  6. 使用docker 搭建 ceph 开发环境,使用aws sdk 存储数据

    本文的原文连接是: http://blog.csdn.net/freewebsys/article/details/79553386 1,关于ceph Ceph是加州大学Santa Cruz分校的Sa ...

  7. ceph 面试_终于有人把Ceph分布式存储讲清楚了!

    Ceph项目最早起源于Sage就读博士期间的工作(最早的成果于2004年发表),并随后贡献给开源社区.在经过了数年的发展之后,目前已得到众多云计算厂商的支持并被广泛应用.RedHat及OpenStac ...

  8. ceph 代码分析 读_分布式存储 Ceph 的演进经验 SOSP 2019

    『看看论文』是一系列分析计算机和软件工程领域论文的文章,我们在这个系列的每一篇文章中都会阅读一篇来自 OSDI.SOSP 等顶会中的论文,这里不会事无巨细地介绍所有的细节,而是会筛选论文中的关键内容, ...

  9. 关系型数据库之Mysql备份(五)

    二进制日志简要: 二进制日志通常作为备份的重要资源,所以再说备份之前我们来回顾下前面专题讲过的二进制日志内容. 1.二进制日志内容 引起mysql服务器改变的任何操作. 复制功能依赖于此日志. 从服务 ...

最新文章

  1. Git的使用和提交规范
  2. IO流基础,创建File对象与方法是用
  3. Nginx三部曲之一【配置文件详解】
  4. unity 批量导入模型工具_如何将VMD舞蹈导入桌面萌娘MMD
  5. 从运维的角度理解Iaas、Paas、Saas云计算
  6. 今年数据分析到底有多火?全网跪求优质资源!
  7. MySQL Study之--Percona Server版本
  8. java for 迭代器_Java基础-迭代器Iterator与语法糖for-each
  9. Oracle入门(十四.22)之创建DDL和数据库事件触发器
  10. 网站漏洞扫描工具_如何实现免费网站漏洞扫描?推荐一款神器给你
  11. 数据结构-链表的删除和添加
  12. Apollo使用ConfigBean装载配置
  13. Oracle 数据类型 选择自 tjandy 的 Blog
  14. 关于int main(int argc,char* argv[])详解
  15. 10 个 Python 项目简单又超有趣
  16. 高德地图JS-API开发—Marker添加及infoWindow处理
  17. maven安装以及本地创库设置
  18. 量子计算发展史上的27个里程碑事件
  19. 智能可穿戴设备如何跨越监测数据不准的鸿沟?
  20. 康拓普:数据可视化如何让大数据更加人性化?

热门文章

  1. Perceptron Approach
  2. Linux查看端口号相关命令
  3. github页面绑定域名/Domain‘s DNS record could not be retrieved
  4. 【python爬虫】Python爬取+PR绘制你出生那日的星图
  5. WIN10系统右下角时间区显示秒数 批处理
  6. python 高级教程
  7. 用Python绘制螺旋文字
  8. 012用于癫痫发作预测的半扩张卷积神经网络-2021
  9. DICOM--如何判断是否压缩?
  10. 《程序员》与我的程序员之路