pod的每个容器都有自己独立的文件系统, 因为文件系统来自容器镜像. 如果想要共享磁盘, container需要自己进行挂载.

Kubernetes定义了存储卷, 它们被定义为pod的一部分, 并和pod共享相同的声明周期. 这意味着在pod启动时创建卷, 并在删除pod时销毁卷. 因此, 在容器重新启动期间, 卷的内容将保持不变, 在重新启动容器之后, 新容器可以识别前一个容器写入卷的所有文件.

通过卷在容器之间共享数据

使用emptyDir

卷从一个空目录开始, 运行在pod内的应用程序可以写入它需要的任何文件. 因为卷的生存周期与pod的生存周期相关联, 所以当删除pod时, 卷的内容就会丢失.

指定用于EMPTYDIR的介质

作为卷来使用的emptyDir, 是在承载pod的工作节点的实际磁盘上创建的, 因此其性能取决于节点的磁盘类型. 但我们可以通知Kubernetestmfs文件系统(存在内存而非硬盘)上创建emptyDir. 因此, 将emptyDirmedium设置为Memory:

1
2
3
4
volumes:
- name: html
emptyDir:
medium: Memory

使用Git仓库作为存储卷

gitRepo卷基本上也是一个emptyDir卷, 它通过克隆Git仓库并在pod启动时(但在创建容器之前)检出特定版本类填充数据.

在创建gitRepo卷之后, 它并不能和对应repo保持同步. 当向Git仓库推送新增的提交时, 卷中的文件将不会被更新.

1
2
3
4
5
volumes:
- name: html
gitRepo:
repository: https://github.com/luksa/kubia-website-example.git
directory: .

介绍sidercar容器

sidecar container, 它是一种容器, 增加了对pod主容器的操作. 可以将一个sidecar添加到pod中, 这样就可以使用现有的容器镜像, 而不是将附加逻辑填入主应用程序的代码中, 这会使它过于复杂和不可用.

访问工作节点文件系统上的文件

大多数pod应该忽略它们的主机节点, 因此它们不应该访问节点文件系统上的任何文件. 但是某系系统级别的pod(切记, 这些通常由DaemonSet管理)确实需要读取节点的文件或使用节点文件系统来访问节点设备. Kubernetes通过hostPath卷实现了这一点.

介绍hostPath

hostPath卷是持久性存储, 因为gitRepoemptyDir卷的内容都会在pod被删除时被删除, 而hostPath卷的内容则不会被删除. 如果删除了一个pod, 并且下一个pod使用了指向主机上相同路径的hostPath卷, 则新pod将会发现上一个pod留下的数据, 但前提是必须将其调度到与第一个pod相同的节点上.

使用持久化存储

当运行在一个pod中的应用程序需要将数据保存到磁盘上, 并且即使该pod重新调度到另一个节点时也要求具有相同的数据可用. 这就不能使用emptyDir, gitRepohostPath, 由于这些数据需要从任何集群节点访问, 因此必须将其存储在某种类型的网络储存中.

Kubernetes编排原理

为什么我们需要Pod

PodKubernetes项目中最小的API对象. 更专业的表述是: PodKubernetes项目的原子调度单位.

容器的”单进程模型”并不是指容器里只能运行”一个”进程, 而是指容器无法管理多个进程. 这是因为容器里PID=1的进程就是应用本身, 其他进程都是这个PID=1进程的子进程. 可是, 用户编写的应用并不像正常操作系统里的init进程或者systemd那样拥有进程管理的功能.

Kubernetes项目里, Pod的实现需要使用一个中间容器, 这个容器叫作Infra容器. 在这个Pod中, Infra容器永远是第一个被创建的容器, 用户定义的其他容器则通过Join Network Namespace的方式与Infra容器关联在一起.

Pod对象使用进阶

Kubernetes中有几种特殊的Volume, 它们存在的意义不是为了存放容器里的数据, 也不是用于容器和宿主机之间的数据交换, 而是为容器提供预先定义好的数据.

Secret

Secret的作用是帮你把Pod想要访问的加密数据存放在etcd中, 你就可以通过在Pod的容器里挂载Volume的方式访问这些Secret里保存的信息了.

像这样通过挂载方式进入容器里的Secret, 一旦其对应的etcd里的数据更新, 这些Volume里的文件内容也会更新. 其实, 这是kubelet组件在定时维护这些Volume.

需要注意的是, 这个更新可能会有一定的延时. 所以在编写应用程序时, 在发起数据库连接的代码处写好重试和超时的逻辑, 绝对是个好习惯.

ConfigMap

ConfigMapSecret类似, 区别在于ConfigMap保存的是无需加密的, 应用所需要的配置信息.

DownwardAPI

DownwardAPI的作用是让Pod里的容器能够直接获取这个PodAPI对象本身的信息.

不过, 需要注意的是, DownwardAPI能够获取的信息一定是Pod里的容器进程启动之前就能确定下来的信息. 如果你想要获得Pod容器运行后才会出现的信息, 比如容器进程的PID, 就肯定不能使用DownwardAPI了, 而应该考虑在Pod里定义一个sidecar容器.

ServiceAccountToken

Service Account对象的作用就是Kubernetes系统内置的一种”服务账户”, 它是Kubernetes进行权限分配的对象. 比如, Service Account A可以只被允许对Kubernetes进行GET操作, 而Service Account B可以有Kubernetes API的所有操作的权限.

像这样的Service Account的授权信息和文件, 实际上保存在它所绑定的一个特殊的Secret对象里. 这个特殊的Secret对象叫作ServiceAccountToken. 任何在Kubernetes集群上运行的应用, 都必须使用ServiceAccountToken里保存的授权信息(也就是token), 才可以合法访问API Server.

容器健康检查和恢复机制

Kubernetes中的重启, 实际上却是重新创建了容器. 这个功能就是Kubernetes里的Pod恢复机制, 也叫restartPolicy.

一定要强调的是, Pod的恢复过程永远发生在当前节点上, 而不会跑到别的节点上. 事实上, 一旦一个Pod与一个节点绑定, 除非这个绑定关系发生了变化pod.spec.node字段被修改, 否则它永远不会离开这个节点.

如果你想让Pod出现在其他的可用节点上, 就必须使用Deployment这样的”控制器”来管理Pod, 哪怕你只需要一个Pod副本.

如果你关心这个容器退出后的上下文环境, 比如容器退出后的日志, 文件和目录, 就需要将restartPolicy设置为Never. 这是因为一旦容器被自动重新创建, 这些内容就有可能丢失(被垃圾回收了).

只要PodrestartPolicy指定的策略允许重启异常的容器, 那么这个Pod就会保持Running状态并重启容器, 否则Pod会进入Failed状态.

对于包含多个容器的Pod, 只有其中所有的容器都进入异常状态后, Pod才会进入Failed状态.

PodPreset

PodPreset里定义的内容只会在Pod API对象被创建之前追加在这个对象上, 而不会影响任何Pod的控制器的定义. 比如, 现在提交的是一个nginx-deployment, 那么这个Deployment对象永远不会被PodPreset改变, 被修改的只是这个Deployment创建出来的所有Pod.

如果你定义了同时作用于一个Pod对象的多个PodPreset, 会发生什么呢?

实际上, Kubernetes项目会帮你合并这两个PodPreset要做的修改, 而如果它们要做的有冲突的话, 这些冲突字段就不会被修改.

谈谈”控制器”思想

Deployment定义的template字段, 在Kubernetes项目中有一个专属的名字, 叫作PodTemplate(Pod模版).

作业副本与水平扩展

如果你更新了DeploymentPod模版, 那么Deployment就需要遵循一种叫作滚动更新的方式, 来升级现有容器.

创建Deployment时建议指定--record参数, 因此创建回滚版本的时候执行的kubectl命令都会被记录下来, 否则通过kubectl rollout history命令查看历史版本时, CHANGE-CAUSE这一栏会是空.

深入理解StatefulSet(一): 拓扑状态

这种实例之间有不对等关系, 以及实例对外部是数据有依赖关系的应用, 就称为有状态应用.

StatefulSet的设计其实非常容易理解, 它把现实世界里的应用状态抽象为了两种情况.

拓扑状态

应用的多个实例之间不是完全对等的. 这些应用实例必须按照某种顺序启动, 比如应用的主节点A要先于从节点B启动. 而如果删除A和B两个Pod, 它们再次被创建出来时也必须严格按照这个顺序运行. 并且, 新创建出来的Pod必须和原来Pod的网络标识一样, 这丫原先的访问者才能使用同样的方法访问到这个新的Pod.

存储状态

应用的多个实际分别绑定了不同的存储数据. 对于这些应用实例来说, Pod A第一次读取到的数据和隔了10分钟之后再此读取到数据应该是同一份, 哪怕在此期间Pod A被创建过.

StatefulSet的核心功能, 就是通过某种方式记录这些状态, 然后在Pod被重新创建时, 能够为新的Pod恢复这些状态.

Service如何被访问的

第一种是以ServiceVIP(virtual IP, 虚拟IP)方式.

第二种是以ServiceDNS方式.

在第二种Service DNS的方式下, 具体又可以分为两种处理方法.

第一种处理方法是Normal Service.

第二种处理方法是Headless Service. Headless Servicecluster IP字段是的值是None, 即这个Service没有一个VIP作为”头”. 这个Service被创建后并不会被分配一个VIP, 而是会以DNS记录的方式暴露出它所代理的Pod.

通过这种方法, Kubernetes就成功地将Pod的拓扑状态(比如哪个节点先启动, 哪个节点后启动), 按照Pod的”名字+编号”的方式固定下来. 此外, Kubernetes还为每个Pod提供了一个固定且唯一的访问入口, 即这个Pod对应的DNS记录. 这些状态在StatefulSet的整个生命周期里都保持不变, 绝不会因为对应Pod的删除或者重新创建而失败.

DNS记录本身不会变, 但它解析到的PodIP地址并不是固定的. 这就意味着, 对于有状态应用实例的访问, 必须使用DNS记录或者hostname的方式, 而绝不应该直接访问这些PodIP地址.

深入理解StatefulSet(二): 存储状态

StatefulSet的控制器直接管理的是Pod. Kubernetes通过Headless Service为这些有编号的Pod, 在DNS服务器中生成带有相同编号的DNS编号. StatefulSet还为每一个Pod分配并创建了一个相同编号的PVC.

容器化守护进程: DaemonSet

跟其他编排对象不同, DaemonSet开始运行的时候很多时候要比整个Kubernetes集群出现的时机要早.

Kubernetes项目中, 当一个节点的网络插件尚未安装时, 该节点就会被自动加上名为node.kubernetes.io/network-unavailable的”污点”. 而通过这样一个Toleration, 调度器在调度这个Pod时就会忽略当前节点上的”污点”, 从而成功地将网络插件的Agent组件调度到这台机器上启动起来.

撬动离线业务: JobCronJob

事实上, restartPolicyJob对象里只允许设置为NeverOnFailure; 而在Deployment对象里, restartPolicy只允许设置为Always.

如果restartPolicy=Never, 那么离线业务失败后Job Controller就会不断尝试创建一个新Pod. 如果restartPolicy=OnFailure, 那么离线业务失败后, Job Controller就不会尝试创建新的Pod, 而是不断尝试重启Pod里的容器.

CronJob是一个专门用来管理Job对象的控制器.

基于角色的权限控制: RBAC

我们知道, Kubernetes中所有API对象都保存在etcd里. 可是, 对这些API对象的操作一定都是通过访问kube-apiserver实现的. 其中一个非常重要的原因是, 需要API Server来帮忙做授权工作. 而在Kubernetes项目中, 负责完成授权工作的机制是RBAC.

服务

pod需要一种寻找其他pod的方法来使用其他pod提供的服务, 不想在没有Kubernetes的世界, 系统管理员要在用户端配置文件中明确指出服务的精确IP地址或主机名来配置每个服务短应用.

介绍服务

Kubernetes服务是一种为一组功能相同的pod提供单一不变的接入点的资源. 当服务存在是, 它的IP地址和端口不会改变. 客户端通过IP地址和端口号建立连接, 这些连接会被路由到提供该服务的任意一个pod上. 通过这种方式, 客户端不需要知道每个单独的提供服务的pod的地址, 这样这些pod就可以在集群中随时被创建或移除.

创建服务

服务的后端可以有不止一个pod. 服务的连接对所有的后端pod是负载均衡的.

CLUSTER-IP只能在集群内部可以被访问, 是为了让集群内部的其他pod可以访问当前这组pod.

在运行的容器中远程执行命令

嗯可以使用kubectl exec命令远程地在一个已经存在的pod容器上执行任何命令. 这样就可以很方便地了解pod的内容, 状态及环境. --代表着命令项的结束.

配置服务的回话亲和性

如果希望特定客户端产生的所有请求每次都指向同一个pod, 可以设置服务的sessionAffinity属性为ClientIP(而不是None, None是默认值).

同一个服务暴露多个端口

在创建一个有多个端口的服务的时候, 必须给每个端口指定名字.

使用命名的端口

服务中可以通过数字来指定端口, 也可以通过名称来指定.

服务发现

客户端pod如何知道服务的IP和端口. Kubernetes为客户端提供了发现服务的IP和端口的方式.

通过环境变量发现服务

pod开始运行的时候, Kubernetes会初始化一系列的环境变量指向现在存在的服务. 如果你创建的服务中早于客户端pod的创建, pod上的进程可以根据环境变量获得服务的IP地址和端口号.

通过DNS发现服务

kube-dns运行DNS服务, 在集群中的其他pod都被配置成使用其作为dns(Kubernetes通过修改每个容器的/etc/resolv.conf文件实现). 运行在pod上的进程DNS查询都会被Kubernetes自身的DNS服务器响应, 该服务器知道系统中运行的所有服务.

pod是否使用内部的DNS服务器是根据pod中的specdnsPolicy属性来决定的.

每个服务从内部DNS服务器中获得一个DNS条目, 客户端的pod在知道服务名称的情况下可以通过全限定域名来访问, 而不是诉诸于环境变量.

通过全限定域名连接服务

1
<服务名>.<服务的命名空间>.svc.cluster.local

客户端仍然必须知道服务的端口后. 如果不是标准端口, 客户端可以从环境变量中获取端口号.

连接集群外部的服务

介绍服务endpoint

服务并不是和pod直接相连的. 相反, 有一种资源介于两者之间--它就是Endpoint资源.

1
kubectl get endpoints <服务名>

手动配置服务的endpoint

服务的endpoint与服务解耦后, 可以分别配置和更新它们.

Endpoint对象需要与服务具有相同的名称.

将服务暴露给外部客户端

使用NodePort类型的服务

通过创建NodePort服务, 可以让Kubernetes在其所有节点上保留一个端口(所有节点都使用相同的端口), 并将传入的连接转发给作为服务部分的pod.

通过负载均衡器将服务暴露出来

回话亲和性和Web浏览器

浏览器使用keep-alive连接, 并通过单个连接发送所有请求, 而curl每次都会打开一个新连接. 服务在连接级别工作, 所以当首次打开服务的连接时, 会选择一个随机集群, 然后将属于该连接的所有网络数据包全部发送到单个集群. 即使会话亲和性设置为None, 用户也会始终使用相同的pod(直到连接关闭).

通过Ingress暴露服务

为什么需要Ingress

一个重要的原因是每个LoadBalancer服务都需要自己的负载均衡器, 以及独有的公有IP地址, 而Ingress只需要一个公网IP就能为许多服务提供访问. 当客户端向Ingress发送HTTP请求时, Ingress会根据请求的主机名和路径决定请求转发到的服务.

了解Ingress的工作原理

Ingress控制器不会将请求转发给该服务, 只用它来选择一个pod.

pod就绪后发出信号

介绍就绪探针

就绪探针器会定期调用, 并确定特定的pod是否接收客户端请求. 当容器的准备就绪探测返回成功时, 表示容器已经准备好接收请求.

了解就绪探针的操作

启动容器时, 可以为Kubernetes配置一个等待时间, 经过等待时间后才可以执行第一次就绪检查. 之后, 它会周期性地调用探针, 并根据就绪探针的结果采取行动. 如果某个pod报告它尚未准备就绪, 则会从该服务中删除该pod. 如果pod再次准备就绪, 则重新添加pod.

副本机制和其他控制器: 部署托管的pod

保持pod健康

只要pod调度到某个节点, 该节点上的Kubelet就会运行pod的容器, 从此只要该pod存在, 就会保持运行. 如果容器的主进程崩溃, Kubelet将重启容器.

程序可以能因为无限循环或死锁而停止响应, 为确保应用程序在这种情况下可以重新启动, 必须从外部检查应用程序的运行状态, 而不是依赖于应用的内部检测.

介绍存活探针

Kubernetes可以通过存活探针(liveness probe)检查容器是否还在运行. 可以为pod中每个容器单独指定存活探针, 如果检测失败, Kubernetes将定期执行探针并重启容器.

Kubernetes有以下三种检测容器的机制:

HTTP GET探针对容器的IP地址(你指定的端口和路径)执行HTTP GET请求. 如果探测器收到响应, 并且响应码不代表错误, 则认为探测成功. 如果服务器返回错误响应码或者根本没有响应, 那么特测被认为是失败的, 容器将被重新启动.

TCP套接字探针尝试与容器指定端口建立TCP连接. 如果连接成功建立, 则探测成功. 否则, 容器重新启动.

Exec探针在容器内执行任意命令, 并检查命令的退出状态码. 如果状态码是0, 则探测成功. 所有其他状态码都认为失败.

创建基于HTTP的存活探针

1
2
3
4
liveness:
httpGet:
path: /
port: 8080

pod的描述文件定义了一个httpGet探针, 该探针告诉Kubernetes定期在端口8080路径上执行HTTP GET请求, 以确定容器是否健康.

使用存活探针

获取崩溃容器的应用日志

如果容器重启, Kubectl logs命令将显示当前容器的日志. 当你想知道前一个容器为什么终止时. 可以通过添加--previous选项.

容器被强行终止时, 会创建一个全新的容器--而不是重启原来的容器.

配置存活探针的附加属性

除了明确指定的存活探针选项, 还可以看到其他属性, 例如delay, timeout, period等.

请务必记得设置一个初始延时来说明应用程序的启动时间.

创建有效的存活探针

对于在生产中运行的pod, 一定要定义一个存活探针. 没有探针的话, Kubernetes无法知道你的应用是否还活着. 只要进程还存在, Kubernetes会认为容器是健康的.

了解ReplicationController

ReplicationController是一种Kubernetes资源, 可确保它的pod始终保持运行状态.

ReplicationController的操作

RelicationController会持续监控正在运行的pod列表, 并保证相应”类型”的pod的数目与期望的相符.

介绍控制器的协调流程

ReplicationController的工作是确保pod的数量始终与其标签选择器匹配. 如果不匹配, 则ReplicationController将根据所需, 采取适当地操作来协调pod的数量.

使用DaemonSet在每个节点上运行一个pod

运行执行单个任务的pod

介绍Job资源

在发生节点故障时, 该节点上由Job管理的pod将按照ReplicaSetpod的方式, 重新安排到其他节点. 如果进程本身异常退出(进程返回错误退出代码时), 可以将Job配置为重新启动容器.

安排Job定期运行或在将来运行一次

Job资源在创建时会立即运行pod. 但是许多批处理任务需要在特定时间运行, 或者在指定的时间间隔內重复运行. 在Linux和类UNIX操作系统中, 这些任务通常被称为cron任务.

了解计划任务的运行方式

在正常情况下, CronJob总是计划为配置中的每个执行创建一个Job, 但是可能会同时创建两个Job, 或者根本没有创建. 为了解决第一个问题, 你的任务应该是幂等的. 对于第二个问题, 请确保下一个任务完成本应该由上一次的(错过的)运行完成的工作.

运行于Kubernetes中的容器

介绍pod

在实际应用中我们不会单独部署容器, 更多的是针对一组pod的容器进行部署和操作.

为何需要pod

为何多个容器比单个容器包含多个进程要好

容积被设计为每个容器只运行一个进程(除非进程本身产生进程). 如果在单个容器中运行多个不相关的进程, 那么保持所有进程运行, 管理它们的日志等将会是我们的责任.

了解pod

同一pod中容器之间的部分隔离

容器之间彼此是完全隔离的, 但是此时我们期望的是隔离容器组, 而不是单个容器, 并让每个容器组内的容器共享一些资源, 而不是全部. Kubernetes通过配置Docker来让一个pod内的所有容器共享相同的Linux命名空间, 而不是每个容器都有自己的一组命名空间.

容器如何共享相同的IP和端口空间

由于一个pod中的容器运行于相同的Network命名空间中, 因此它们共享相同的IP地址和端口空间. 有可能会产生端口冲突, 但这只涉及同一个pod中的容器.

介绍平坦pod间网络

Kubernetes集群中的所有pod都在同一个共享网络地址空间中, 这意味着每个pod都可以通过其他podIP地址来实现相互访问.

通过pod合理管理容器

将多层应用分散到多个pod中, 同时基于扩缩容考虑而分割到多个pod中. 当应用可能由一个主进程和一个或多个辅助进程时, 才有可能将多个容器添加到单个pod中.

决定何时在pod中使用多个容器

当决定是将两个容器放入到一个pod还是两个独立的pod时, 我们可以问自己以下几个问题:

  1. 它们需要一起运行还是可以在不同的主机上运行?
  2. 它们代表的是一个整体还是相互独立的组件?
  3. 它们必须一起进行扩缩容还是可以分别进行?

YAMLJSON描述文件创建pod

pod和其他Kubernetes资源通常是通过向Kubernetes REST API提供JSONYAML描述文件来创建的. 此外还有其他更简单的创建资源的方法, 比如kubectl run命令, 但这些方法通常只允许你配置一组有限的属性.

检查现有podYAML描述文件

获得podyaml信息

1
kubectl get pods <pod名称> -o yaml

介绍pod定义的主要部分

pod定义由这么几个部分组成: 首先是YAML中使用的Kubernetes API版本和YAML描述的资源类型; 其次是几乎在所有Kubernetes资源中都可以找到的三大重要部分:

  1. metedata包括名称, 命名空间, 标签和关于该容器的其他信息
  2. spec包含pod内容的实际说明, 例如pod的容器, 卷和其他数据.
  3. status包含运行中的pod的当前信息, 例如pod所处的条件, 每个容器的描述和状态, 以及内部IP和其他基本信息.

pod创建一个简单的YAML描述文件

使用kubectl explain来发现可能的API对象字段

1
2
3
kubectl explain pods

kubectl explain pod.spec

使用kubectl create来创建pod

kubectl create -f命令用于从yamljson文件中创建任何资源(不只是pod).

查看应用程序日志

容器化的应用程序通常会将日志记录到标准输出和标准错误流, 而不是将其写入文件.

容器运行时将这些流重定向到文件, 并允许我们运行以下命令来获取容器的日志:

1
docker logs <container id>

使用ssh命令登录到pod正在运行的节点, 并使用docker logs命令查看其日志, 但Kubernetes提供了一种更为简单的方法.

使用kubectl logs命令获取pod日志

为了查看pod的日志(更准确地说是容器的日志), 只需要在本地机器上运行以下命令(不需要ssh到任何地方):

1
kubectl logs <pod名称>

获取多容器pod的日志时指定容器名称

如果我们的pod包含多个容器, 在运行kubectl logs命令时则必须通过包含-c <容器名称>选项来显示指定容器名称.

1
kubectl logs <pod名称> -c <容器名称>

我们只能获取仍然存在的pod的日志. 当一个pod被删除时, 它的日志也会被删除. 如果希望在pod删除之后仍然可以获取其日志, 我们需要设置中心化的, 集群范围的日志系统, 将所有日志存储到中心存储中.

pod发送请求

将本地网络端口转发到pod中的端口

如果想要在不通过service的情况下与某个特定的pod进行通信(出于调试或其他原因), Kubernetes将允许我们配置端口转发到该pod. 可以通过kubectl port-forward命令完成上述操作. 例如以下命名会将机器的本地端口8888转发到目标pod的端口8080:

1
kubectl port-forward <pod名称> 8888:8080

使用标签组织pod

我们需要一种能够基于任意标准将上述pod组织成更小群体的方式. 此外, 我们希望通过一次操作对属于某个组的所有pod进行操作, 而不必单独为每个pod执行操作.

通过标签来组织pod和所有其他Kubernetes对象.

介绍标签

标签是一种简单却功能强大的Kubernetes特性, 不仅可以组织pod, 也可以组织所有其他的Kubernetes资源. 详细来说, 标签是可以附加到任意资源的任意键值对, 用以选择具有该确切标签的资源(这是通过标签选择器完成的). 只要标签的key在资源内是唯一的, 一个资源便可以拥有多个标签. 通常我们创建资源时就会将标签附加到资源上, 但之后我们也可以在添加其他标签, 或者修改现有标签的值, 而无须重新创建资源.

金丝雀发布是指在部署新版本时, 先只让小部分用户体验新版本以观察新版本的表现, 然后再向所有用户进行推广, 这样可以防止暴露有问题的版本给过多的用户.

创建pod时指定标签

显示所有标签

1
kubectl get pods --show-labels

显示特定标签

1
kubectl get pods -L <label1的key>,<label2的key>

修改现有pod的标签

创建标签

1
kubectl label po <pod的名称> <新label的key>=<新label的value> 

修改标签

1
kubectl label po <pod的名称> <已存在label的key>=<label的值>

通过标签选择器列出pod子集

标签选择器允许我们选择标记有特定标签的pod子集, 并对这些pod执行操作. 可以说标签选择器是一种能够根据是否包含具有特定值的特定标签来过滤资源的准则.

标签选择器根据资源的以下条件来选择资源:

包含(或不包含)使用特定键的标签

包含具有特定键和值的标签

包含就有特定值的标签, 但其值与我们指定的不同

使用标签选择器列出pod

1
2
3
4
5
6
kubectl get po -l '<label的key>'
kubectl get po -l '!<label的key>'
kubectl get po -l <label的key>=<label的value>
kubectl get po -l <label的key>!=<label的value>
kubectl get po -l <label的key>in (<label的value>, <label的value>)
kubectl get po -l <label的key>notin (<label的value>, <label的value>)

在标签选择器中使用多个条件

在包含多个逗号分隔的情况下, 可以在标签选择器中同时使用多个条件, 此时资源需要全部匹配才算成功匹配了选择器.

使用标签和选择器来约束pod调度

使用标签分类工作节点

标签可以附加到任何kubernetes对象上, 包括节点.

pod调度到特定节点

为了让调度器只在合适节点上进行选择, 我们需要在podYAML文件中添加一个节点选择器. 我们只是在spec部分添加了一个nodeSelector字段.

调度到一个特定节点

同样地, 我们也可以将pod调度到某个确定的节点, 由于每个节点都有一个唯一标签, 其中键为kubernetes.io/hostname, 值为该节点的实际主机名, 因此我们也可以将pod调度到某个确定的节点. 我们绝不应该考虑单个节点, 而是应该通过标签选择器考虑符合特定标准的逻辑节点组.

注解pod

除标签外, pod和其他对象还可以包含注解. 注解也是键值对, 所以它们本质上与标签非常相似. 但与标签不同, 注解并不是为了保存标识信息而存在的, 它们不能像标签一样用于对对象进行分组.

大量使用注解可以为每个pod或其他API对象添加说明, 以便每个使用该集群的人都可以快速查找有关每个单独对象的信息.

查找对象的注解

为了查看注解, 我们需要获取pod的完整YAML文件或使用kubectl describe命令.

1
2
3
kubectl get po <pod的名称> -o yaml

kubectl describe

添加和修改注解

和标签一样, 注解可以在创建是就添加到pod中, 也可以在之后再对现有的pod进行添加或修改. 其中将注解添加到现有对象的最简单的方式是通过kubectl annotate

1
kubectl annotate pod <pod的名称> <注解的键>=<注解的值>

使用命名空间对资源进行分组

Kubernetes命名空间简单地为对象名称提供了一个作用域. 此时我们并不会将所有资源都放在同一个命名空间中, 而是将它们组织到多个命名空间中, 这样可以允许我们多次使用相同的资源名称.

发现其他命名空间及其pod

列出命名空间:

1
kubectl get ns

当使用kubectl get命令列出资源时, 我们从未明确指定命名空间, 因此kubectl总是默认为default命名空间, 只显示该命名空间下的对象.

得到指定命名空间下的对象:

1
kubectl get po -n <命名空间>

namespace使我们能够将不属于一组的资源分到不重叠的组中. 如果有多个用户或用户组正在使用同一个Kubernetes集群, 并且它们都各自管理自己独特的资源集合, 那么它们就应该使用各自的命名空间. 这样一来, 它们就不用特别担心无意中修改或删除其他用户的资源, 也无须关心名称冲突. 如前所述, 命名空间为资源名称提供了一个作用域.

除了隔离资源, 命名空间还可用于仅允许某些用户访问特定资源, 甚至限制单个用户可用的计算资源数量.

创建一个命名空间

命名空间是一种和其他资源一样的Kubernetes资源, 因此可以通过将YAML文件提交到Kubernetes API服务器来创建该资源.

还可以使用kubectl create namespace命令创建命名空间

1
kubectl create namespace <命名空间>

命名空间只能包含字母, 数字, 横杠

管理其他命名空间中的对象

如果想在指定的命名空间中创建资源, 可以选择在metedata字段中添加一个namespace: custom-namespace属性, 也可以使用kubectl create命令创建资源时指定命名空间:

1
kubectl create -f 模版 -n <命名空间>

在列出, 描述, 修改或删除其他命名空间中的对象时, 需要给kubectl命令传递--namespace(或-n)选项. 如果不指定命名空间, kubectl将在当前上下文中配置的默认命名空间中执行操作.

命名空间提供的隔离

尽管命名空间将对象分隔到不同组, 只允许你对属于特定命名空间的对象进行操作, 但实际上命名空间之间并不提供对正在运行的对象的任何隔离.

停止和移除pod

按名称删除pod

1
kubectl delete po <po的名称> [<pod名称>...]

在删除pod的过程中, 实际上我们在指示Kubernetes终止该pod中的所有容器. Kubernetes向进程发送一个SIGTERM信号并等待一定的秒数(默认为30), 使其正常关闭. 如果它没有及时关闭, 则通过SIGKILL终止该进程. 因此, 为了确保你的进程总是正常关闭, 进程需要正确处理SIGTERM信号.

使用标签选择器删除pod

1
kubectl delete po -l <label的名称>=<label的值>

通过删除整个命名空间来删除pod

可以通过删除整个命名空间来删除这个命名空间, 以及这个命名空间下的pod.

1
kubectl delete ns <命名空间>

删除命名空间中所有pod, 当保留命名空间

删除当前命名空间中的所有pod:

1
kubectl delete po --all

删除命名空间中的(几乎)所有资源

通过使用单个命令删除当前命名空间中的所有资源:

1
kubectl delete all --all

使用all关键字删除所有内容并不是真的删除所有内容. 一些资源会被保留下来, 并且需要被明确指定删除.

开始使用KubernetesDocker

Kubernetes上运行第一个应用

部署Node.js应用

部署应用程序最简单的方式是使用kubectl run命令, 该命令可以创建所有必要的组件而无需JSONYAML.

介绍pod

一个pod是一组紧密相关的容器, 它们总是运行在同一个工作节点上, 以及同一个Linux命名空间中. 每个pod就像一个独立的逻辑机器, 拥有自己的IP, 主机名, 进程等, 运行一个独立的应用程序.

列出pod

不能列出单个容器, 因为它们不是独立的Kubernetes对象, 但是可以列出pod. 要查看有关pod的更多信息, 还可以使用kubectl describe pod命令.

访问WEB应用

每个pod都有自己的IP地址, 但是这个地址是集群内部的, 不能从集群外部访问. 要让pod能够从外部访问, 需要通过服务对象公开它, 要创建一个特殊的LoadBalancer类型的服务. 因为如果你创景一个常规服务(一个ClusterIP服务), 比如pod, 它也只能从集群内部访问. 通过创建LoaBalancer类型的服务, 将创建一个外部的负载均衡, 通过负载均衡的公共IP访问pod.

创建一个服务对象

1
kubectl expose <需要暴露的pod类型> <需要暴露的pod名称> --type=<暴露的类型> --name=<暴露的名称> 

列出服务

1
kubectl get services

系统的逻辑部分

一个服务被创建时, 它会得到一个静态的IP, 在服务的生命周期中这个IP不会发生改变. 客户端应该通过固定IP地址连接到服务, 而不是直接连接pod.

服务表示一组或多组提供相同服务的pod的静态地址. 到达服务IP和端口的请求将被转发到属于该服务的一个容器的IP和端口

水平伸缩应用

1
kubectl scale <需要暴露的pod类型> <需要暴露的pod名称> --replicas=<数量>

列出pod时显示pod IPpod的节点

1
kubectl get pods -o wide

Kubernete介绍

Kubernetes使开发者可以自主部署应用, 并且控制部署的频率, 完全脱离运维团队的版主. Kubernetes同时能够让运维团队监控整个系统, 并且在硬件故障时重新调度应用. 系统管理员的工作重心, 从监管应用转移到了监管Kubernetes, 以及剩余的系统资源, 因为Kubernetes会帮助监管所有的应用.

Kubernetes系统的需求

从单体应用到微服务

单体应用由很多个组件构成, 这些组件紧密地耦合在一起, 由于它们在同一操作系统进程中运行, 所以在开发, 部署, 管理的时候必须以同一个实体运行. 对单体应用来说, 即使是某个组件中一个小的修改, 都需要重新部署整个应用. 组件间缺乏严格的边界定义, 相互依赖, 日积月累导致系统复杂度提升, 整体质量也急剧恶化.

将应用拆解为多个微服务

这个问题迫使我们将复杂的大体单体应用, 拆分为小的可独立部署的微服务组件. 每个微服务以独立的进程运行, 并通过简单且定义良好的接口(API)与其他的微服务通信.

因为每个微服务都是独立的进程, 提供相对静态的API, 所以独立开发和部署单个微服务成为了可能. 只要API不变或者向前兼容, 改动一个微服务, 并不会要求其他微服务进行改动或重新部署.

微服务的扩容

面向单体系统, 扩容针对的是整个系统, 而面向微服务架构, 库容却只需要针对单个服务, 这意味着你可以选择扩容那些需要更多资源的服务而保持其他的服务仍然维持在原来的规模.

部署微服务

像大多数情况一样, 微服务也有缺点. 部署微服务, 部署者需要正确地配置所有服务来使其作为一个单一系统能正确工作, 随着微服务的数量不断增加, 配置工作变得冗余且易错.

微服务还带来其他问题, 比如因为垮了多个进程和机器, 使得调试代码和定位异常调用变得困难.

为应用程序提供一个一致的环境

不管你同时开发和部署多少个独立组件, 开发和运维团队总是需要解决的一个最大的问题是程序运行环境的差异性. 为了减少会在生产环境才会暴露的问题, 最理想的做法是让应用在开发和生产可以运行在完全一样的环境下.

迈向持续交付: DevOps和无运维

让同一个团队参与应用的开发, 部署, 运维的整个生命周期更好. 这意味着开发, QA和运维团队彼此之间的合作需要贯穿整个流程. 这种实践称为DevOps.

理想情况是, 开发者自己部署程序本身, 不需要知道硬件基础设施的任何情况, 也不需要和运维团队交涉, 这被叫作NoOps. 很明显, 你仍然需要有一些人来关心硬件基础设施, 但这些人不需要再处理应用程序的独特性.

正如你所看到的, Kubernetes能让我们实现所有这些想法. 通过对实际硬件做抽象, 然后将自身暴露成一个平台, 用于部署和运行应用程序. 它允许开发者自己配置和部署应用程序, 而不需要系统管理员的任何帮助, 让系统管理员聚焦于保持底层基础设施运转正常的同时, 不需要关注实际运行在平台上的应用程序.

介绍容器技术

Kubernetes使用Linux容器技术来提供应用隔离.

什么是容器

当一个应用程序仅由较少数量的大组件构成时, 完全可以接受给每个组件分配专用的虚拟机, 以及通过给每个组件提供自己的操作系统实例来隔离它们的环境. 当时当这些组件开始变小且数量开始增长时, 如果你不想浪费硬件资源, 又想持续压低硬件成本, 就不能给每个组件配置一个虚拟机了.

用Linux容器技术隔离组件

开发者不是使用虚拟机来隔离每个微服务环境, 而是正在转向Linux容器技术. 容器允许你在同一台机器上运行多个服务, 不经是提供不同的环境给每个服务, 而且将它们互相隔离. 容器类似虚拟机, 但开销小很多.

一个容器里运行的进程实际上运行在宿主机的操作系统上, 就像所有其他进程一样(不像虚拟机, 进程是运行在不同的操作系统上的). 但在容器里的进程仍然是和其他进程隔离的. 对于容器内进程本身而言, 就好像是在机器和操作系统上运行的唯一一个进程.

比较虚拟机和容器

和虚拟机计较, 容器更加轻量级, 它允许在相同的硬件上运行更多数量的组件. 主要是因为每个虚拟机需要运行自己的一组系统进程, 这就产生了除组件进程消耗以外的额外计算资源损耗. 从另一方面说, 一个容器仅仅是运行在宿主机上被隔离的单个进程, 仅消耗应用容器消耗的资源, 不会有其他进程的开销.

虚拟机需要一个管理程序, 它将物理硬件资源分成较小部分的虚拟硬件资源, 从而被每个虚拟机里的操作系统使用. 运行在那些虚拟机里的应用程序会执行虚拟机操作系统的系统调用, 然后虚拟机内核会通过管理程序在宿主机上的物理CPU执行指令.

多个容器则会完全执行运行在宿主机上的同一个内核的系统调用, 此内核是唯一一个在宿主机操作系统上执行指令的内核. CPU也不需要做任何对虚拟机那样的虚拟化.

虚拟机的主要好处是它们提供完全隔离的环境, 因为每个虚拟机运行在它自己的Linux内核上, 而容器都是调用同一个内核, 这自然会有安全隐患. 如果你的硬件资源有限, 那当你有少量进程需要隔离的时候, 虚拟机就可以成为一个选项. 为了在同一台机器上运行大量被隔离的进程, 容器因它的低消耗而成为一个更好的选择.

容器实现隔离机制介绍

容器如何隔离进程的, 有两个机制可用: 第一个是Linux命名空间, 它使每个进程只能看到它自己的系统视图(文件, 进程, 网络接口, 主机名等); 第二个是Linux控制组(cgroups), 它限制了进程能使用的资源量(CPU, 内存, 网络带宽等).

Linux命名空间隔离进程

默认情况下, 每个Linux系统最初仅有一个命名空间. 所有系统资源(诸如文件系统, 用户ID, 网络接口等)属于这一个命名空间. 但是你能创建额外的命名空间, 以及在它们之间组织资源. 对于进程, 可以在其中一个命名空间中运行它. 进程将只能看到同一个命名空间下的资源. 当然, 会存在多种类型的多个命名空间, 所以一个进程不单单只属于某一个命名空间, 而属于每个类型的一个命名空间.

存在以下类型的命名空间:

  1. Mount(mnt)
  2. Process(pid)
  3. Network(net)
  4. Inter-process communication(ipd)
  5. UTS
  6. User Id(user)

每种命名空间被用来隔离一组特定的资源.

限制进程的可用资源

另外的隔离性就是限制容器能使用的系统资源. 这通过cgroups来实现. cgroups是一个Linux内核功能, 它被用来限制一个进程或者一组进程的资源使用. 一个进程的资源(CPU, 内存, 网络带宽等)使用量不能超出被分配使用的量. 这种方式下, 进程不能过分使用为其他进程保留的资源, 这和进程运行在不同的机器上是类似的.

Docker容器平台介绍

Docker不仅简化了打包应用的流程, 也简化了打包应用的库和依赖, 甚至整个操作系统地文件都能被打包成一个简单的可移植的包, 这个包可以被用来在任何其他运行Docker的机器上使用.

Docker的概念

Docker是一个打包, 分发和运行应用程序的平台. Docker中三个主要的概念.

  1. 镜像: Docker镜像里包含了你打包的应用程序及其所依赖的环境. 它包含应用程序可用的文件系统和其他元数据, 如镜像运行时的可执行文件路径.
  2. 镜像仓库: Docker镜像仓库用于存放Docker镜像, 以及促进不同人和不同电脑之间共享这些镜像.
  3. 容器: Docker容器通常是一个Linux容器, 它基于Dokcer镜像被创建. 一个运行中的容器是一个运行在Docker主机的进程, 但它和主机, 以及所有运行在主机的其他进程都是隔离的. 这个进程也是资源受限的, 意味着它只能访问和使用分配给它的资源(CPU, 内存等).

镜像层

层不仅使分发更高效, 也助于减少镜像的存储空间. 每一层被存一次, 当基于相同基础层的镜像被创建成两个容器时, 它们就能够读取相同的文件. 但是如果其中一个容器写入某些文件, 另外一个是无法看见文件变更的. 因此, 即使它们共享文件, 仍然彼此隔离. 这是因为容器镜像层是只读的. 容器运行时, 一个新的可写在镜像层之上被创建. 容器中进程写入位于底层的一个文件时, 此文件的一个拷贝在顶层被创建, 进程写的是此拷贝.

容器镜像可移植性的限制

理论上, 一个容器镜像能运行在任何一个运行Docker的机器上. 但是如果一个容器化的应用需要一个特定的内核版本, 那它可能不能在每台机器上都工作. 如果一台机器运行了一个不匹配的Linux内核版本, 或者没有相同内核模块可用, 那么此应用就不能在其上运行.

Kubernetes介绍

深入浅出地了解Kubernetes

Kubernets是一个软件系统, 它允许你在其上很容易地部署和管理容器化的应用. Kubernetes使你在数以千计的电脑节点上运行软件时就像所有节点是单个大节点一样.

Kubenetes的核心功能

整个系统由一个主节点和若干个工作节点组成. 开发者把一个应用列表提交到主节点, Kubernetes会将它们部署到集群的工作节点.

开发者能指定一些应用必须一起运行, Kubernets将会在一个工作节点上部署它们. 其他的将被分散部署到集群中, 但是不管部署在哪儿, 它们都能以相同的方式相互通信.

Kubernetes集群架构

在硬件级别, 一个Kubernetes集群由很多节点组成, 这些节点被分成以下两种类型:

  1. 主节点: 它承载着Kubernetes控制和管理整个集群系统地控制面板
  2. 工作节点: 它们运行用户实际部署的应用

控制面板

控制面板用于控制集群并使它工作. 它包含多个组件, 组件可以运行在单个主节点或者通过副本分别部署在多个主节点以确保高可用性. 这些组件是:

  1. Kubernetes API服务器, 你和其他控制面板组件都要和它通信.
  2. Scheduler, 它调度你的应用(为应用的每个部署组件分配一个工作节点).
  3. Controller Mananger, 它执行集群基本的工作, 如复制组件, 持续跟踪工作节点, 处理节点失败等.
  4. etcd, 一个可靠的分布式数据存储, 它能持久化存储集群配置.

控制面板的组件持有并控制集群状态, 但是它们不运行你的应用程序. 这是由工作节点完成的.

工作节点

工作节点是运行容器化应用的机器. 运行, 监控和管理应用服务的任务是由以下组件完成的:

  1. Docker, rtk或其他的容器类型
  2. Kubelet, 它与API服务器通信, 并管理它所在节点的容器
  3. Kubernetes Service Proxy(kube-proxy), 它负责组件之间的负载均衡网络流量

Kubernetes中运行应用

描述信息怎样成为一个运行的容器

API服务器处理应用描述时, 调度器选择可用的工作节点. 选择时基于所需要的计算资源, 以及调度时每个节点未分配的资源. 然后, 那些节点上的Kubelet指示容器运行时拉取所需要的镜像并运行容器.

保持容器运行

一旦应用程序运行起来, Kubernetes就会不断地确认应用程序的部署状态始终与你提供的描述相匹配. 如果实例之一停止了正常工作, 比如进程崩溃或停止响应时, Kubernetes将自动重启它.

同理, 如果整个工作节点死亡或无法访问, Kubernetes将为在故障节点上运行的所有容器选择新节点, 并在新选择地节点上运行它们.

扩展副本数量

当应用程序运行时, 可以决定要增加或减少副本量, 而Kubernetes将分别增加附加的或停止多余的副本. 甚至可以把决定最佳副本数目的工作交给Kubernetes. 它可以根据实时指标(CPU负载, 内存消耗, 每秒查询或应用程序公开的任何其他指标)自动调整副本数.

命中移动目标

Kubernetes可能需要在集群中迁移你的容器. 当它们运行的节点失败时, 或者为了给其他容器腾出地方而从节点移除时, 就会发生这种情况.

为了让客户能够轻松找到提供特定服务的容器, 可以告诉Kubernetes哪些容器提供相同的服务, 而Kubernetes将通过一个静态IP地址暴露所有容器, 并将该地址暴露给集群中运行的所有应用程序. 这是通过环境变量完成的, 但是客户端也可以通过良好的DNS查找服务器IP. 服务的IP地址保持不变, 因此客户端始终可以连接到它的容器, 即使它们在集群中移动.

使用Kubernetes的好处

如果在所有服务器上都部署了Kubernetes, 那么运维团队就不需要在部署应用程序.

简化应用程序部署

由于Kubernetes将其所有工作节点作为一个部署平台, 因此应用程序开发人员可以开始自己开始部署应用程序, 不需要了解组成集群的服务器.

健康检查和自修复

当服务器发生故障时, 拥有一个允许在任何时候跨集群迁移应用程序的系统也很有价值.

Kubernetes监控你的应用程序组件和它们运行的节点, 并在节点出现故障时自动将它们重新调度到其他节点. 这使运维团队不必手动迁移应用程序组件, 并允许团队立即专注与修复节点本身, 并将其修好送回到可用的硬件资源池中.

Kubernetes集群搭建和配置

Kubernetes部署利器: kubeadm

1
2
3
4
5
# 创建一个Master节点
kubeadm init

# 将一个Node节点加入当前集群
kubeadm join <Master节点的IP和端口>

kubeadm的工作原理

在部署时, 它的每个组件都是一个需要被执行的, 单独的二进制文件.

Kubernetes集群初始化

Kubernetes中有一种特殊的容器启动方法, 叫做static pod. 它允许你把要部署的podYAML文件放在一个指定的目录中. 这样, 当这台机器上的kubelet启动时, 它会自动检查该目录, 加载所有的Pod YAML文件并在这台机器上启动它们.

GoroutinesChannels

Go语言中的并发程序可以用两种手段来实现. goroutinechannel, 其支持”顺序通信进程”(CSP). CSP是一种现代的并发编程模型, 在这种编程模型中值会在不同的运行实例goroutine中传递, 尽管大多数情况下仍然是被限制在单一实例中.

Goroutines

Go语言中, 每一个并发的执行单元叫作一个goroutine. 当一个程序启动时, 其主函数即在一个单独的goroutine中运行, 我们叫它main goroutine. 新的gorountine会用go语句来创建. 在语法上, go语句是一个普通的函数或方法调用前加上关键字go. go语句会使其语句中的函数在一个新创建的goroutine中运行. 而go语句本身会迅速地完成.

主函数返回时, 所有的goroutine都会被直接打断, 程序退出. 除了从主函数退出或者直接终止程序之外, 没有其它的编程方法能够让一个goroutine来打断另一个执行, 但是之后可以看到一种方式来实现这个目的, 通过goroutine之间的通信来让一个goroutine请求其它的goroutine, 让被请求的goroutine自行结束执行.

示例: 并发的Clock服务

time.Time.Format将时间格式化. time.Parse将字符串转化为时间.

示例: 并发的Echo服务

函数值在循环体中才会出现捕获迭代变量的情况.

Channels

如果说goroutineGo语言程序的并发体的话, 那么channels是它们之间的通信机制. 一个channels是一个通信机制, 它可以让一个goroutine通过它给另一个goroutine发送值信息. 每个channel都有一个特殊的类型, 也就是channels可以发送的数据类型. 一个可以发送int类型数据的channel一般写作chan int.

使用内置的make函数, 我们可以创建一个channel:

1
ch := make(chan int)

map类似, channel也是一个对应make创建的底层数据结构的引用. 当我们复制一个channel或用于函数传递参数时, 我们只是拷贝了一个channel引用, 因此调用者和被调用者讲引用同一个channel对象. 和其他的引用类型一样, channel的零值也是nil.

两个相同类型的channel可以使用==运算符比较. 如果两个channel引用的是相通的对象, 那么比较的结果为真. 一个channel也可以和nil进行比较.

channel支持close操作, 用于关闭channel, 随后对与基于该channel的任何发送操作都将都导致panic异常.

不带缓存的Channels

一个基于无缓存channel的发送操作将导致发送者goroutine阻塞, 直到另一个goroutine在相同的channel上执行接收操作, 当发送的值通过channel成功传输之后, 两个goroutine可以继续执行后面的语句. 反之, 如果接收操作先发生, 那么接收者goroutine也将阻塞, 直到有另一个goroutine在相同的channel上执行发送操作.

串联的channels(pipeline)

channel也可以用于将多个goroutine链接在一起, 一个channel的输出作为下一个channel输入. 这种串联的channel就是所谓的管道pipeline.

当一个channel被关闭后, 再向该channel发送数据将导致panic异常. 当一个被关闭的channel中已经发送的数据都被成功接收后, 后续的接收操作将不再阻塞, 它们会立即返回一个零值.

没有办法直接测试一个channel是否被关闭, 但是接收操作有一个变体形式: 它多接收一个结果, 多接收的第二个结果是一个布尔值, true表示成功从channel接收到值, false表示channel已经被关闭并且里面没有值可被接收.

Go语言的range循环可直接在channel上迭代, 它依次从channel接收数据, 当channel被关闭并且没有值可被接收时跳出循环.

并不需要关闭每一个channel. 不管一个channel是否被关闭, 当它没有被引用时会被Go语言的垃圾自动回收器回收.

试图重复关闭一个channel将导致panic异常, 试图关闭一个nilchannel也将导致panic异常. 关闭channel还会触发一个广播机制.

单方向的channel

当一个channel作为函数参数时, 它一般总是被专门用于只发送或者只接收.

为了表示这种意图并防止被滥用, Go语言的类型系统提供了单方向的channel类型, 分别用于只发送或只接收的channel. 类型chan<- int表示一个只发送intchannel, 只能发送不能接收. 相反, 类型<-chan int表示一个只接收intchannel, 只能接收不能发送. 这种限制将在编译期检测.

因为close操作说明了通道上没有数据再发送, 仅仅在发送方goroutine上才能调用它, 所以试图关闭一个仅能接收的channel在编译时会报错.

任何双向channel向单向channel变量的赋值操作都将导致隐式转换. 这里没有反向转换的语法, 也就是不能将单向channel转换为双向channel.

带缓存的channel

带缓存的channel内部持有一个元素队列. 队列的最大容量是在调用make函数创建channel时通过第二个参数指定的.

向缓存channel的发送操作就是向内部缓存队列的尾部插入元素, 接收操作则是从队列的头部删除元素. 如果内部缓存队列是满的, 那么发送操作将阻塞直到因另一个goroutine执行接收从而释放了新的队列空间. 相反, 如果channel是空的, 接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素.

在某些特殊情况下, 程序可能需要知道channel内部缓存的容量, 可以用内置的cap函数获取.

同样, 对于内置的len函数, 如果传入的是channel, 那么将返回channel内部缓存队列中有效元素的个数.

goroutine可能因为channel无法接收可卡住, 导致goroutine泄露. 泄漏的goroutine并不会被自动回收, 因此确保每个不再需要的goroutine能正常退出是重要的.

并发的循环

每个子问题都是完全彼此独立的问题叫做易并行问题. 易并行问题是最容易被实现成并行的一类问题, 并且是最能够享受并发带来的好处, 能够随着并行的规模线性地扩展.

为了知道最后一个goroutine什么时候结束, 我们需要一个递增的计数器, 在每一个goroutine启动时加一, 在goroutine退出时减一. 这需要一种特殊的计数器, 这个计数器需要多个goroutine操作时做到安全并且提供在其减为零之前一直等待的一种方法. 这种计数类型被称为sync.WaitGroup.

实例: 并发的Web爬虫

注意channel可能造成死锁.

限制goroutine数量的方法. 使用缓存队列.

终止程序的方法.

限制goroutine数量的方法. 保持长活goroutine.

基于select的多路复用

time.Tick函数返回一个channel, 程序会周期性的像一个节拍器一样向这个channel发送事件. 每一个事件的值是一个时间搓.

1
2
3
4
5
6
7
8
9
10
select {
case <- ch1:
// ...
case x := <- ch2:
// ...
case ch3 <- y:
// ...
default:
// ...
}

select会等待case中有能够执行的case时去执行. 当条件满足时, select才会去通信并执行case之后的语句; 这时候其它通信是不会执行的. 一个没有任何caseselect语句写作select{}, 会永远地等待下去.

time.After函数会立即返回一个channel, 并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值.

如果多个case同时就绪, select会随机地选择一个执行, 这样保证每一个channel都有平等的被select的机会.

Tick函数挺方便, 但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适. 否则应该使用, 下面这种模式.

1
2
3
ticker := time.NewTicker(1 * time.Second)
<-ticker.C
ticker.Stop()

有时候我们希望能够从channel中发送或者接收值, 并避免因为发送或者接收导致的阻塞, 尤其是当channel没有准备好写或者读时. select语句就可以实现这样的功能. select会有一个default来设置当其它的操作都不能够被马上处理时程序需要执行哪些逻辑.

因为对一个nilchannel发送和接收操作会被永远阻塞, 在select语句中操作nilchannel永远都不会被select到.

这使得我们可以使用nil来激活或者禁用case, 来达成处理其它输入或输出事件时超时和取消的逻辑.

示例: 并发的字典遍历

nilchannel可以和select配合来达到关闭case的效果.

并发的退出

Go语言并没有提供一个goroutine中终止另一个goroutine的方法, 由于这样会导致goroutine之间的共享变量落在未定义的状态上.

可以创建一个退出channel, 只用退出来, 作为一个广播.

1
2
3
4
5
6
7
8
9
10
var done = make(chan struct{})

func cancelled() bool {
select {
case <- done:
return true
default:
return false
}
}

实例: 聊天服务

学习broadcaster中的select用法.

常用函数

1
2
3
4
5
6
7
8
# 获得文件的属性
os.Stat()

# 获得目录下的所有文件
ioutil.ReadDir()

# 读取命令行参数
flag.Args()

接口

Go语言中接口类型的独特之处在于它是满足隐式实现的. 也就是说, 我们没有必要对于给定的具体类型定义所有满足的接口类型; 简单地拥有一些必需的方法就足够了. 这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义; 当我们使用的类型来自于不受我们控制的包时这种设计尤其有用.

接口约定

接口类型是一种抽象的类型. 它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合; 它们只会展示出它们自己的方法. 也就是说当你看到一个接口类型的值时, 你不知道它时什么, 唯一知道的就是可以通过它的方法来做些什么.

一个类型可以自由的使用另一个满足相同接口的类型来进行替换被称作可替换性(LSP里氏替换).

接口类型

接口类型具体描述了一系列方法的集合, 一个实现了这些方法的具体类型是这个接口类型的实例.

新的接口类型可以通过组合已有的接口来定义.

实现接口的条件

一个类型如果拥有一个接口需要的所有方法, 那么这个类型就实现了这个接口.

interface{}被称为空接口类型. 因为空接口类型对实现它的类型没有要求, 所以我们可以将任意一个值赋给空接口类型.

判断是否实现了接口只需要比较具体类型和接口类型的方法, 所以没有必要在具体类型的定义中声明这种关系.

非空的接口类型比如io.Writer经常被指针类型实现, 尤其当一个或多个接口方法像Write方法那样隐式地给接受者带来变化的时候. 一个结构体的指针是非常常见的承载方法的类型.

flag.Value接口

调用flag.CommandLine.Var方法把标记加入到应用的命令行标记集合中.

接口值

概念上讲一个接口的值, 接口值, 由两个部分组成, 一个具体的类型和那个类型的值. 通常被称为接口的动态类型和动态值. 对于像Go语言这种静态类型的语言, 类型是编译期的概念: 因此一个类型不是一个值. 在我们的概念中, 用类型描述符提供每个类型的具体信息, 比如它的名字和方法. 对于一个接口值, 类型部分就用对应的类型描述符来表述.

Go语言中, 变量总是被一个定义明确的值初始化, 即使接口类型也不例外. 对于一个接口的零值就是它的类型和值的部分都是nil.

一个接口值基于它的动态类型被描述为空或非空. 可以通过==nil!=nil来判断接口值是是否为空. 调用一个空接口值上的任意方法都会产生panic.

一般来讲, 在编译时我们无法知道一个接口值的动态类型会是什么, 所以通过接口来做调用必须需要使用动态分发. 编译器必须生成一段代码来从类型描述符拿到名为write的方法地址, 在间接调用该方法地址. 调用的接收者就是接口的动态值.

接口值可以用==!=操作符来做比较. 如果两个接口值都是nil或者二者的动态类型完全一致且二者动态值相等(使用动态类型的==操作符来比较), 那么两个接口值相等. 应为接口值是可以比较的, 所以它们可以作为map的键, 也可以作为switch语句的操作数.

然而, 如果两个接口值的动态类型相同, 但是这个动态类型时不可比较的(比如切片), 将它们进行比较就是失败且panic.

考虑到这点, 接口类型时非常与众不同的. 其它类型那么是安全地可比较类型(如基本类型和指针)要么是完全不可比较的类型(如切片, 映射类型, 和函数), 但是在比较接口值或者包含了接口值的聚合类型时, 我们必须要意识到潜在的panic. 同样的风险也存在与使用接口作为map的键或者switch的操作数. 只能比较你非常确定的动态值是可比较类型的接口值.

警告: 一个包含nil指针的接口不是nil接口

空的接口值(其中不包含任何信息)与仅仅动态值为nil的接口值是不一样的.

sort.Interface接口

需要实现sort.Interface接口, 然后使用sort.Sort进行排序, 检查是否有序sort.IsSorted.

http.Handler接口

了解ServerMux.

error接口

error类型就是一个interface类型, 这个类型有一个返回错误信息的单一方法:

1
2
3
type error interface {
Error() string
}

类型断言

类型断言是一个使用在接口值上的操作. 语法它看起来像x.(T), 其中x是一个接口类型的表达式, 而T是一个类型(称为断言类型). 一个类型断言检查它操作对象的动态类型是否和断言的类型匹配.

这里有两种情况. 第一种, 如果断言的类型T是一个具体的类型, 然后断言类型检查x的动态类型是否和T相同. 如果检查成功了, 这个断言的结果是x的动态值, 当然的类型是T. 如果检查失败, 这个检查失败, 将会抛出panic.

第二种情况, 如果断言的类型T是一个接口类型, 然后断言检查是否x的动态类型满足T. 如果检查成功, 这个结果是一个有相同类型和值部分的接口值, 但是结果类型为T.

如果断言操作的对象是一个nil接口值, 那么不论被断言的类型是什么类型断言都会失败. 我们几乎不需要对一个更少限制的接口类型做断言, 因为它表现得就像赋值操作一样, 除了对于nil接口值的情况.

如果类型断言出现在一个预期有两个结果的赋值操作中, 这个操作不会在失败的时候发生panic, 但是用第二个布尔值来表示转换成功或失败. 失败时, 第一个值是被断言类型的零值.

基于类型断言区别错误类型

通过检查错误消息是否含有特定的子字符串从而来区分错误的类型是非常不成熟的. 一个更可靠的方式是使用一个专门的类型来描述结构化的错误.

类型开关

1
2
3
4
5
6
7
switch x.(type) {
case nil:
case int, uint:
case bool:
case string:
default:
}

常用的方法

1
2
3
4
5
# 从输入s中解析出一个变量. 
fmt.Sscanf()

# 字符串排序
sort.Strings()
0%