摘要

Kubernetes 是谷歌开源的分布式容器编排和资源管理系统。从 2014 年发布至今,已经成为 GitHub 社区中最炙手可热的开源项目。因为以 K8s 为核心的云原生技术已经成为业界企业数字化转型的事实标准技术栈。

一、K8s应用问题

选择多: K8s 系统是一套专注容器应用管理的集群系统,它的组件一般按功能分别部署在主控节点(master node)和计算节点(agent node)。

  • 对于主控节点,主要包含有 etcd 集群,controller manager 组件,scheduler 组件,api-server 组件。
  • 对于计算节点,主要包含 kubelet 组件和 kubelet-proxy 组件。

K8s系统组件安装到物理机、虚拟机中,并不能保证就是最优的部署配置。因此需要做很多选择题才能调优到最优解。并不止依赖于 K8s,它还需要包括网络、存储等。我们知道容器模型是基于单机设计的,当初设计的时候并没有考虑大规模的容器在跨主机的情况下通信问题。Pod 和 Pod 之间的网络只定义了接口标准,具体实现还要依赖第三方的网络解决方案。因此需要面对选择,选择适合的网络方案和网络存储。目前容器网络并没有完美方案出现,它需要结合你的现有环境和基础硬件的情况来做选择。

排错难: 当前 K8s 社区提供了各种各样的 K8s 运维工具,有 ansible 的,dind 容器化的,有 mac-desktop 桌面版本的,还有其他云原生的部署工具。因为各种底层系统的多样性,你会遇到各种各样的问题,比如容器引擎 Docker 版本低,时间同步组件 ntp 没有安装,容器网络不兼容底层网络等。任何一个点出了问题,你都需要排错。加上企业的系统环境本来就很复杂,很多场景下都是没有互联网可以查资料的,对排错来说即使所有的日志都收集起来做分析也很难轻易的排错。

不管是商业方案还是开源方案都只是片面的考虑到 K8s 核心组件的排错,而真正企业关心的应用容器,集群,主机,网络,存储,监控日志,持续集成发布等方面的排错实践就只能靠自己摸索,你很难系统的学习到。K8s 集群的版本是每个季度有一个大版本的更新。对于企业用户来说怎么才能在保证业务没有影响的情况下平滑更新 K8s 组件呢? 头疼的问题就是这么出来的。一旦发生不可知问题,如何排错和高效的解决问题呢。

场景多: 在早期的应用编排场景,主要是为了削峰填谷,高效利用浪费的主机资源。一个不公开的企业运维秘密就是生产中主机资源平均利用率不超过 20%。这不是因为运维傻,这是因为如果遇到峰值,主机系统需要能平滑应对,需要给业务容量留有余地。因为容器的引入让原来主机仅可以部署 3-4 个进程的系统,现在可以充分利用容器进程隔离的技术在主机上部署 20-40 个进程系统,并且各自还不受影响。这就是容器应用的最大好处。随着应用场景的多样性,在应对突发流量的时候,K8s编排系统就是作为一种弹性伸缩的工具,快速提高进程的数量来承载流量。在解决微服务应用编排上,除了传统的微服务部署需求之外,还有混合部署需要的 Service Mesh 技术也对 K8s提出了流量编排的新要求。另外还有 Serverless 场景下的 FaaS 框架 Openfaas 也对 K8s 带来了新的机会和应用难点。还有很多有状态中间件服务,如数据库集群等也都在大量的迁入到K8s集群中,并获得了很好的实践反馈。

现在K8s已经成为企业数字化转型中应用发布管理的标准基础设施方案,面对这么多场景需求,我们用一套容器编排集群能把所有需求都解决吗?显然是不可以的。实际情况下很多应用集群为了管理和优化上的考虑,都是多套集群系统。多套集群在多个区域部署,给运维带来了不少麻烦。这也是多场景下多集群治理带来的烦恼。

1.1 容器网络的选择问题

容器网络的选择难题一直是 DevOps 团队的痛点。Kubernetes 集群设计了 2 层网络,第一层是服务层网络,第二层是 Pod 网络。Pod 网络可以简单地理解为东西向容器网络,和常见的 Docker 容器网络是一致的设计。服务层网络是 Kubernetes 对外暴露服务的网络,简单地可以理解为南北向容器网络。Kubernetes 官方部署的常见网络是 Flannel,它是最简化的 Overlay 网络,因为性能不高只能在开发测试环境中使用。为了解决网络问题,社区提供了如 Calico、Contiv、Cilium、Kube-OVN 等诸多优秀的网络插件,让用户在选择时产生困惑。

首先企业在引入 Kubernetes 网络时,仅仅把它作为一套系统网络加入企业网络。企业网络一般设计为大二层网络,对于每一套系统的网络规划都是固定的。这样的规划显然无法满足 Kubernetes 网络的发展。为了很好地理解和解决这样的难题,我们可以先把大部分用户的诉求整理如下:

  • 第一 ,由于容器实例的绝对数量剧增,如果按照实例规划 IP 数量,显然不合理。
  • 第二、我们需要像虚拟机实例一样,给每一个容器实例配置固定的 IP 地址。
  • 第三、容器网络性能不应该有损耗,最少应该和物理网络持平。

在这样的需求下,网络性能是比较关键的指标。查阅网上推荐的实践,可以看到一些结论:Calico 的虚拟网络性能是接近物理网络的,它配置简化并且还支持 NetworkPolicy,它是最通用的方案。在物理网络中,可以采用 MacVlan 来获得原生网络的性能,并且能打通和系统外部网络的通信问题。

这样的信息虽然是网上分享的最佳实践,我们仍然会不太放心,还是需要在本地网络环境中通过测试来验证的。 这种网络验证是必须的,只是在选择网络选型时,我们大可不必把每一项网络方案都去测试一遍。我们可以遵循一些合理的分类来明确方向。

其实,我们应该回到 Kubernetes 的设计初衷,它是一套数据中心级别的容器集群系统,我们可以给它独立的网络分层。按照这个方向选择,它的网络应该采用虚拟网络,这样我们就会看到可以选择的方案是 Flannel、Calico、Cilium。

还有另外一个方向,就是希望 Kubernetes 网络是你整个企业网络的延展,这样的设计目标之下,等于你给 Kubernetes 网络落地原有的网络。Kubernetes 上的对象系统全部压缩到只使用 Pod 网络这一层。笔者人为在很多传统遗留的系统中,如果想落地 Kubernetes 方案就会遇到这样的问题。在这样的场景下,应该采用 Contiv、Kube-Ovn 直接对接原生网络。这样的设计会平滑的让很多遗留系统平滑的迁入到云原生的网络之中。这是非常好的实践。

1.2 存储方案的引入问题

容器 Pod 是可以挂载盘的,对于外挂盘有本地存储、网络存储,对象存储这样的三类。在早期 Kubernetes 发展阶段,容器存储驱动是百花齐放,很多驱动的 Bug 导致容器运行出现各种各样的问题。现在 Kubernetes 社区终于制定了 CSI 容器存储标准方案,并且已经达到生产可用阶段。所以,我们在选择存储驱动的时候一定需要选择 CSI 的存储驱动来调用存储。

本地存储原来都是直接挂载目录,现在采用 CSI 方式之后,本地的资源也期望采用 PV、PVC 的方式来申请,不要在直接 Mount 目录了。

对于网络存储,一般我们特别指定 NFS。NFS 比较特别的地方是它是共享挂载方式,也就是一个目录可以被多台主机挂载。但是 PV、PVC 的挂载方式仍然期望目标存储是唯一的,这个时候,一定要注意规划好 NFS 的目录结构。还有需要注意的是存储的大小在 NFS 中是没法限制的,完全有底层 NFS 来规划。为了有效管理存储空间大小,DevOps 团队可以手动创建 PV 并限制好空间大小,然后让用户采用 PVC 来挂载这个手动创建的 PV。

对于对象存储,因为抽象能力的增强,可以实现 PV 的动态创建和 PVC 的自动挂载,并且申请的大小可以动态满足。这个是最理想的存储方案,但是因为后端存储系统如 Ceph 类需要专人维护,才能保证系统的问题性。在投入资源时需要多加考虑。

1.3 容器引擎的选择问题

很多人好奇这有什么好选择的,直接安装 Docker 不就完事了。因为容器技术的发展,目前 Kubernetes 官方引擎已经默认安装为 Cri-O 开源引擎。原来,Kubernetes 社区给容器引擎定义了一套标准,所以容器引擎开始出现多元化。为了更清楚地理解容器引擎的位置,我们可以通过一张图来详细理解容器引擎的位置:

显而易见,Cotnainerd 已经取代 Docker 的位置,由于 Containerd 源自 Docker 源码,它的可靠性是经过多年的历练的,目前是最可靠的容器引擎。除此之外,当业务发展需要实现多租户时,对于主机环境不在信任,这个时候的容器引擎需要更进一步的隔离。目前可以选择的方案有 KataContainer、firecracker、gVisor。这种技术一般被称为富容器技术,通过采用裁剪虚拟化组件来彻底隔离容器环境,是真正轻量级虚拟机。

因为服务器的规格越来越高级,CPU 一般都达到 32 核,内存高达 256G 的规格都很常见,在这么大容量的主机上,如果只跑一个用户群体的容器,显然会浪费。原来我们实现资源划分都是采用虚拟机隔离一层之后在分配给业务,业务 DevOps 团队在规划 Kubernetes 集群资源。当我们把虚拟化这一层例如 Openstack 和 Kubernetes 合并之后,势必需要把虚拟化技术真正引入到 Kubernetes 中,所以,这就是富容器的意义所在。当然这块的实践配置仍然还无法做到傻瓜方式,仍然需要专业的开发人员进行调优,所以我们还需要谨慎试用。

1.4 集群规模的规划问题

对于企业来说,一套系统应该只希望部署一套,减少管理运维成本。但是毕竟 Kubernetes 是一套开源系统,在很多场景下它并没有办法解决跨网的管理,我们不得不为了业务划分部署多套集群。多套集群等于就是多套基础设施,让很多 DevOps 团队开始感到一些运维压力。这里我们可以对比一下:

单一集群 多应用为中心集群
爆炸半径
硬多租户(安全、资源强隔离) 复杂 简单
混合调度多种类型资源 复杂 简单
集群管理复杂性 较低 较高(自建)/较低(采用k8s托管)
集群生命周期的灵活性 困难 简单(不同的集群可以采用不同的版本)
规模化的复杂性 复杂 简单

这里我认为应该参考谷歌内部集群运维的经验,按照数据中心的规模,每个数据中心只规划一套集群。那么国内企业比较头疼的现状是内部网络采用防火墙隔离成若干隔离区域,每个网络区域直接通过白名单方式开放有限的端口,甚至生产网络只允许通过跳板机执行运维操作。如果是一套 Kubernetes 集群部署到这样的复杂网络中,势必需要规划梳理很多参数和规则,比分开部署集群需要投入更多的精力。

为了有效地解决这个难题,我们可以通过迭代的办法,先期采用多套集群的模式,然后通过主机标签和 Namespace 空间方式,不断把多个集群的主机归并到单一集群中。当然,谷歌的安全级别只有一种,不管是开发测试还是集成生产都是一个安全策略,这非常适合一套集群的规划。但是很多企业的安全级别是分开管理的,服务质量 SLA 也是不一样的。在这样的情况下,我们可以把集群分为开发测试集群和生产集群,也是很合理的规划。毕竟,安全是企业的生命线,然后才是集群运营成本的规划。

1.5 安全审计的引入问题

Kubernetes 系统是一套复杂的系统,它的安全问题也是企业非常重视的环节。首先,对于集群调用的认证和授权,原生有一套 RBAC(角色的权限访问控制)模型。这种 RBAC 在角色权限不是很多的情况下,它是可以支撑的。但是对于更细力度的控制就无法轻易满足了。比如:允许用户访问用户的 Namespace,但是不允许访问 kube-system 系统级的命名空间这样细粒度的虚拟。社区提供了 Open Policy Agent 工具就是来解决这个问题的。

简单地说,RBAC 是白名单做法,用户规则多的情况下,策略变更需要涉及多个角色的定义更新,维护成本高。采用 OPA 是黑名单的做法,只需要一个规则就可以搞定变更。另外,企业 Kubernetes 的安全情况需要借助一些工具来定期审查。比较出名的工具是 CIS Kubernetes Benchmark,你可以参考应用。

1.6 业务保障团队的建设问题

很多 DevOps 团队在接手 Kubernetes 之后,明显发现这套系统的运维难度是之前其它系统的数倍。对于业务稳定性的要求给 DevOps 团队带来很多不确定的压力。很明显的原因是,Kubernetes 对人员的能力要求提高了。

参考谷歌的 SRE 团队的建设历程,我发现这是国内企业比较缺失的一个岗位。SRE 在国内传统企业并不多见,它类似资深运维架构师,但处理问题的视角以业务为中心来保障企业的正常运营。随着阿里系在引入业务保障体系之后的成功,国内领先的大厂已经渐渐接受了这种新的角色,并且还在不断升华这个岗位的能力范围。

对于传统企业来说,现有 DevOps 人员如何有效的升级知识结构,并能转变思路以业务保障为中心全局的来思考问题成为新的课题。从资源上来讲,很多企业的技术能力是由合作伙伴的整合来完成的,并不是一定需要传统企业打破原有企业的岗位规划,完全采用互联网的做法也是有很多风险在里面。因为传统企业的第一要素是安全,然后才是可靠性。因为传统企业的数据可靠性早就比互联网企业要成熟很多,通过大量的冗余系统足够保证数据的完整性。在这样的情况之下,原厂的 DevOps 团队应该充分理解 Kubernetes 的能力缺陷,多借助合作伙伴的技术合作共赢的方式,让 Kubernetes 系统的落地更加稳健。

1.7 集群安装的问题

大家别小看 Kubernetes 的安装工具的问题,目前业界有很多种 Kubernetes 集群安装部署方式,这让企业的 DevOps 团队感到困惑。

首先,Kubernetes 的核心组件发布的都是二进制版本,也就是在主机层面可以使用 systemd 的方式来安装部署。有一些部署版本采用静态 Pod 的方式来部署都是比较特别的部署方式,不建议传统企业采用。还有一些发行版本完全采用容器的部署方式来数据,虽然早期使用过程中感觉很方便,但是在日后运维中,因为容器的隔离导致组件的状态无法第一时间获取,可能会给业务故障的排查带来一定的障碍。所以笔者推荐的方式还是采用二进制组件的方式来部署最佳。当前 Kubernetes 官方主推的是 kubeadm 来安装集群,但是 kubeadm 竟然也是采用镜像来部署核心组件,虽然在便捷性上给用户节省了很多事情,但是也给未来的故障运维带来很多坑,请小心使用。

其次,因为企业对集群有高可用的需求,所以 Master 节点一般配置为 3 台。其中,最重要的组件就是 etcd 键值集群的维护。其中让企业很容易混淆的地方是,Master 节点只要有 3 台以上,我们的系统就是高可用的,其实不然。如果 3 台 Master 节点放在一个网络区域,当这个网络区域出现抖动的时候,服务仍然还是会出问题。解决办法就是把 Master 放在 3 个不同的网络区域才能实现容错和高可用。

另外,还有很大一部分企业的情况是服务器的规模有限,比如 5~6 台左右,也想使用 Kubernetes 集群。如果划出 3 台作为 Master 节点不跑业务会觉得很浪费。这个时候,我认为应该采用更轻量级的 Kubernetes 集群系统来支持。这里我推荐 K3s 集群系统,它巧妙的把所有核心组件都编译到一个二进制程序里,只需要 40M 大小就可以部署。虽然这个集群是单机版本的集群,但是 Kubernetes 集群的 Node 节点上承载的业务并不会因为 Master 节点宕机就不可以访问。我们只需要在业务上做到多节点部署,就可以完美切合这样的单节点集群。企业可以根据需要灵活采用这种方案。

综上所述,我认为企业落地 Kubernetes 是有一定的技术挑战的,DevOps 需要迎面接受挑战并结合落地情况选择一些合理的方案。

1.8 基于 Kubernetes 下的微服务注册中心的部署问题

经典的微服务体系都是以注册中心为核心,通过 CS 模式让客户端注册到注册中心服务端,其它微服务组件才能互相发现和调用。当我们引入 Kubernetes 之后,因为 Kubernetes 提供了基于 DNS 的名字服务发现,并且提供 Pod 级别的网格,直接打破了原有物理网络的单层结构,让传统的微服务应用和 Kubernetes 集群中的微服务应用无法直接互联互通。为了解决这个问题,很多技术团队会采用如下两种方式来打破解决这种困境。

创建大二层网络,让 Pod 和物理网络互联互通

这个思路主要的目的是不要改变现有网络结构,让 Kubernetes 的网络适应经典网络。每一个 Pod 分配一个可控的网络段 IP。常用的方法有 macvlan、Calico BGP、Contiv 等。这样的做法直接打破了 Kubernetes 的应用架构哲学,让 Kubernetes 成为了一个运行 Pod 的资源池,而上面的更多高级特性 Service,Ingress、DNS 都无法配合使用。随着 Kubernetes 版本迭代,这种阉割功能的 Kubernetes 架构就越来越食之无味弃之可惜了。

注册中心部署到 Kubernetes 集群中,外网服务直接使用 IP 注册

这种思路是当前最流行的方式,也是兼顾历史遗留系统的可以走通的网络部署结构。采用 StatefulSet 和 Headless Service,我们可以轻松地搭建 AP 类型的注册中心集群。当 Client 端连接 Server 端时,如果在 Kubernetes 内部可以采用域名的方式。例如:

eureka:client:serviceUrl:defaultZone: http://eureka-0.eureka.default.svc.cluster.local:8761/eureka,http://eureka-1.eureka.default.svc.cluster.local:8761/eureka,http://eureka-2.eureka.default.svc.cluster.local:8761/eureka

对于集群外部的微服务,可以直接采用 IP 直连 Servicer 端的 NodeIP,例如:

eureka:client:serviceUrl:defaultZone: http://<node-ip>:30030/eureka

我们回顾 Kubernetes 设计之初就会发现,它是为数据中心设计的应用基础设施,并没有设计能兼容传统网络的架构,所以才导致我们部署起来感觉怎么操作都不对劲。但是企业内部的业务逻辑复杂,技术团队一般都是小心谨慎地把业务系统慢慢迁移到新的云原生的集群中,所以我们势必又会遇到这样的混合架构的场景。这个时候我们可以借鉴业界实践过的单元化设计,按照网关为边界,划分应用单元,把完整的一套微服务上架到 Kubernetes 中。这样,Kubernetes 集群和外部的服务之间的调用可以采用 RPC/HTTP API 的方式进行异构调用,从而规避打破 Kubernetes 云原生体系。

1.9 微服务核心能力的优化设计问题

经典微服务架构的内部,服务和服务之间的函数调用,我们通常见过的有 Spring Feign/Dubbo RPC/gRPC ProtoBuf 等。为了能知道服务的所在位置,我们必须有一个注册中心才能获得对方的 IP 调用关系。然后你在结合 Kubernetes 集群的 CoreDNS 的实现,你会自然想到一个问题,如果所有服务组件都在一个 Namespace 之下,它们直接的关系直接可以在配置文件里面写入名字,CoreDNS 是帮助我们实现在集群之下的服务发现的。也就是说,当你把微服务部署到 Kubernetes 之后,像 Eureka 这样的服务基本就是鸡肋了。

很多有经验的架构师会说,道理是这样的,但是例如 Spring Cloud 的微服务体系就是以 Eureka 为中心的,可能不用它不行。这个问题我觉得是历史遗留问题,以 Spring Boot 框架做为基础,我们完全可以基于 Kubernetes 的服务发现能力构建微服务体系。

另外,因为 Kubernetes 的 Pod 设计包含了 SideCar 模型,所以我们可以把通用的微服务关心的限流、熔断、安全 mTLS、灰度发布等特性都放在一个独立的 Sidecar proxy 容器中,代理所有这些通用的容器治理需求。这样就可以极大的解放开发人员的心智模型,专心写业务代码就可以了。大家已经看出来,这不就是服务网格吗?是的,确实融入到服务网格的设计模式中了,但是当前的服务网格参考 Istio 并没有在业界大量落地使用,我们仍需要利用现有的微服务框架自建这样的体系。

另外,微服务体系的业务观测能力,通过 Kubernetes 的生态图,我们可以采用 ELK 收集业务日志,通过 Prometheus 监控加上 Grafana 构建可视化业务模型,更进一步完善微服务的能力设计体系。

1.10 微服务应用的部署策略痛点

很多微服务应用在部署 Kubernetes 集群时,多采用 Deployment 对象。其实当前 Kubernetes 还提供了 StatefulSet 对象。这个 Workload 对象一般开发者望文生义,以为就是有状态的,需要挂盘才用这个。其实 StatefulSet 还提供了强劲的滚动更新的策略,因为 StatefulSet 对每一个 Pod 都提供了唯一有编号的名字,所以更新的时候可以按照业务需要一个一个地更新容器 Pod。这个其实对业务系统来说特别重要,我甚至认为,微服务的容器服务都应该用上 StatefulSet 而不是 Deployment。Deployment 其实更适合 Node.js、Nginx 这样的无状态需求的应用场景。

另外,微服务部署在 Kubernetes,并不是说你的微服务就是高可用了。你仍然需要提供亲和性/反亲和性策略让 Kubernetes 调度应用到不同的主机上,让业务容器能合理地分布,不至于当出现宕机时直接导致“血崩”现象,并直接影响你的业务系统。

affinity:podAntiAffinity:requiredDuringSchedulingIgnoredDuringExecution:- weight: 100labelSelector:matchExpressions:- key: k8s-appoperator: Invalues:- kube-dnstopologyKey: kubernetes.io/hostname

微服务应用在更新过程中,肯定需要更新 Endpoint 和转发规则,这个一直是 Kubernetes 集群的性能瓶颈点,我们可以采用 readinessProbe 和 preStop 让业务更平滑地升级,示例如下:

二、Kubernets的编排对象应用

Kubernetes 系统是一套分布式容器应用编排系统,当我们用它来承载业务负载时主要使用的编排对象有 Deployment、ReplicaSet、StatefulSet、DaemonSet 等。这些对象都是对 Pod 对象的扩展封装。并且这些对象作为核心工作负载 API 固化在 Kubernetes 系统中了。

2.1 常规业务容器部署策略

2.1.1 策略一:强制运行不少于 2 个容器实例副本

在应对常规业务容器的场景之下,Kubernetes 提供了 Deployment 标准编排对象,从命令上我们就可以理解它的作用就是用来部署容器应用的。Deployment 管理的是业务容器 Pod,因为容器技术具备虚拟机的大部分特性,往往让用户误解认为容器就是新一代的虚拟机。从普通用户的印象来看,虚拟机给用户的映象是稳定可靠。如果用户想当然地把业务容器 Pod 也归类为稳定可靠的实例,那就是完全错误的理解了。容器组 Pod 更多的时候是被设计为短生命周期的实例,它无法像虚拟机那样持久地保存进程状态。因为容器组 Pod 实例的脆弱性,每次发布的实例数一定是多副本,默认最少是 2 个。

部署多副本示例:

apiVersion: apps/v1
kind: Deployment
metadata:name: nginx-deploymentlabels:app: nginx
spec:replicas: 2selector:matchLabels:app: nginxtemplate:metadata:labels:app: nginxspec:containers:- name: nginximage: nginx:1.7.9ports:- containerPort: 80

2.1.2 策略二:采用节点亲和,Pod 间亲和/反亲和确保 Pod 实现高可用运行

当运维发布多个副本实例的业务容器的时候,一定需要仔细注意到一个事实。Kubernetes 的调度默认策略是选取最空闲的资源主机来部署容器应用,不考虑业务高可用的实际情况当你的集群中部署的业务越多,你的业务风险会越大。一旦你的业务容器所在的主机出现宕机之后,带来的容器重启动风暴也会即可到来。为了实现业务容错和高可用的场景,我们需要考虑通过 Node 的亲和性和 Pod 的反亲和性来达到合理的部署。这里需要注意的地方是,Kubernetes 的调度系统接口是开放式的,你可以实现自己的业务调度策略来替换默认的调度策略。我们这里的策略是尽量采用 Kubernetes 原生能力来实现。

首先,Kubernetes 并不是谷歌内部使用的 Borg 系统,大部分中小企业使用的 Kubernetes 部署方案都是人工扩展的私有资源池。当你发布容器到集群中,集群不会因为资源不够能自动扩展主机并自动负载部署容器 Pod。即使是在公有云上的 Kubernetes 服务,只有当你选择 Serverlesss Kubernetes 类型时才能实现资源的弹性伸缩。很多传统企业在落地 Kubernetes 技术时比较关心的弹性伸缩能力,目前只能折中满足于在有限静态资源的限制内动态启停容器组 Pod,实现类似的业务容器的弹性。用一个不太恰当的比喻就是房屋中介中,从独立公寓变成了格子间公寓,空间并没有实质性扩大。在实际有限资源的情况下,Kubernetes 提供了打标签的功能,你可以给主机、容器组 Pod 打上各种标签,这些标签的灵活运用,可以帮你快速实现业务的高可用运行。

其次,实践中你会发现,为了高效有效的控制业务容器,你是需要资源主机的。你不能任由 Kubernetes 调度来分配主机启动容器,这个在早期资源充裕的情况下看不到问题。当你的业务复杂之后,你会部署更多的容器到资源池中,这个时间你的业务运行的潜在危机就会出现。因为你没有管理调度资源,导致很多关键业务是运行在同一台服务器上,当主机宕机发生时,让你很难处理这种灾难。所以在实际的业务场景中,业务之间的关系需要梳理清楚,设计单元化主机资源模块,比如 2 台主机为一个单元,部署固定的业务容器组 Pod,并且让容器组 Pod 能足够分散的运行在这两台主机之上,当任何一台主机宕机也不会影响到主体业务,实现真正的高可用。

主机亲和性示例

pods/pod-with-node-affinity.yaml apiVersion: v1
kind: Pod
metadata:name: with-node-affinity
spec:affinity:nodeAffinity:requiredDuringSchedulingIgnoredDuringExecution:nodeSelectorTerms:- matchExpressions:- key: kubernetes.io/e2e-az-nameoperator: Invalues:- e2e-az1- e2e-az2preferredDuringSchedulingIgnoredDuringExecution:- weight: 1preference:matchExpressions:- key: another-node-label-keyoperator: Invalues:- another-node-label-valuecontainers:- name: with-node-affinityimage: gcr.azk8s.cn/google-samples/node-hello:1.0

目前有两种类型的节点亲和,分别为 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution。你可以视它们为“硬”和“软”,意思是,前者指定了将 pod 调度到一个节点上必须满足的规则,后者指定调度器将尝试执行但不能保证的偏好。

Pod 间亲和与反亲和示例

使用反亲和性避免单点故障例子:

affinity:podAntiAffinity:requiredDuringSchedulingIgnoredDuringExecution:- labelSelector:matchExpressions:- key: "app"operator: Invalues:- zktopologyKey: "kubernetes.io/hostname"

意思是以主机的 hostname 命名空间来调度,运行打了标签键 "app" 并含有 "zk" 值的 Pod 在不同节点上部署。

2.1.3 策略三:使用 preStop Hook 和 readinessProbe 保证服务平滑更新不中断

我们部署应用之后,接下来会做的工作就是服务更新的操作。如何保证容器更新的时候,业务不中断是最重要的关心事项。参考示例:

apiVersion: apps/v1
kind: Deployment
metadata:name: nginx
spec:replicas: 1selector:matchLabels:component: nginxtemplate:metadata:labels:component: nginxspec:containers:- name: nginximage: xds2000/nginx-hostnameports:- name: httphostPort: 80containerPort: 80protocol: TCPreadinessProbe:httpGet:path: /port: 80httpHeaders:- name: X-Custom-Headervalue: AwesomeinitialDelaySeconds: 15timeoutSeconds: 1lifecycle:preStop:exec:command: ["/bin/bash", "-c", "sleep 30"]

给 Pod 里的 container 加 readinessProbe(就绪检查),通常是容器完全启动后监听一个 HTTP 端口,kubelet 发送就绪检查探测包,正常响应说明容器已经就绪,然后修改容器状态为 Ready,当 Pod 中所有容器都 Ready 之后这个 Pod 才会被 Endpoint Controller 加进 Service 对应 Endpoint IP:Port 列表,然后 kube-proxy 再更新节点转发规则,更新完了即便立即有请求被转发到新的 Pod 也能保证能够正常处理连接,避免了连接异常。

给 Pod 里的 container 加 preStop hook,让 Pod 真正销毁前先 sleep 等待一段时间,留点时间给 Endpoint controller 和 kube-proxy 更新 Endpoint 和转发规则,这段时间 Pod 处于 Terminating 状态,即便在转发规则更新完全之前有请求被转发到这个 Terminating 的 Pod,依然可以被正常处理,因为它还在 sleep,没有被真正销毁。

2.1.4 策略四:通过泛域名转发南北向流量范式

常规集群对外暴露一个公网 IP 作为流量入口(可以是 Ingress 或 Service),再通过 DNS 解析配置一个泛域名指向该 IP(比如 *.test.foo.io),现希望根据请求中不同 Host 转发到不同的后端 Service。比如 a.test.foo.io 的请求被转发到 my-svc-a,b.test.foo.io 的请求转发到 my-svc-b。当前 Kubernetes 的 Ingress 并不原生支持这种泛域名转发规则,我们需要借助 Nginx 的 Lua 编程能力解决实现泛域名转发。

Nginx proxy 示例(proxy.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:labels:component: nginxname: proxy
spec:replicas: 1selector:matchLabels:component: nginxtemplate:metadata:labels:component: nginxspec:containers:- name: nginximage: "openresty/openresty:centos"ports:- name: httpcontainerPort: 80protocol: TCPvolumeMounts:- mountPath: /usr/local/openresty/nginx/conf/nginx.confname: configsubPath: nginx.conf- name: dnsmasqimage: "janeczku/go-dnsmasq:release-1.0.7"args:- --listen- "127.0.0.1:53"- --default-resolver- --append-search-domains- --hostsfile=/etc/hosts- --verbosevolumes:- name: configconfigMap:name: configmap-nginx---apiVersion: v1
kind: ConfigMap
metadata:labels:component: nginxname: configmap-nginx
data:nginx.conf: |-worker_processes  1;error_log  /error.log;events {accept_mutex on;multi_accept on;use epoll;worker_connections  1024;}http {include       mime.types;default_type  application/octet-stream;log_format  main  '$time_local $remote_user $remote_addr $host $request_uri $request_method $http_cookie ''$status $body_bytes_sent "$http_referer" ''"$http_user_agent" "$http_x_forwarded_for" ''$request_time $upstream_response_time "$upstream_cache_status"';log_format  browser '$time_iso8601 $cookie_km_uid $remote_addr $host $request_uri $request_method ''$status $body_bytes_sent "$http_referer" ''"$http_user_agent" "$http_x_forwarded_for" ''$request_time $upstream_response_time "$upstream_cache_status" $http_x_requested_with $http_x_real_ip $upstream_addr $request_body';log_format client '{"@timestamp":"$time_iso8601",''"time_local":"$time_local",''"remote_user":"$remote_user",''"http_x_forwarded_for":"$http_x_forwarded_for",''"host":"$server_addr",''"remote_addr":"$remote_addr",''"http_x_real_ip":"$http_x_real_ip",''"body_bytes_sent":$body_bytes_sent,''"request_time":$request_time,''"status":$status,''"upstream_response_time":"$upstream_response_time",''"upstream_response_status":"$upstream_status",''"request":"$request",''"http_referer":"$http_referer",''"http_user_agent":"$http_user_agent"}';access_log  /access.log  main;sendfile        on;keepalive_timeout 120s 100s;keepalive_requests 500;send_timeout 60000s;client_header_buffer_size 4k;proxy_ignore_client_abort on;proxy_buffers 16 32k;proxy_buffer_size 64k;proxy_busy_buffers_size 64k;proxy_send_timeout 60000;proxy_read_timeout 60000;proxy_connect_timeout 60000;proxy_cache_valid 200 304 2h;proxy_cache_valid 500 404 2s;proxy_cache_key $host$request_uri$cookie_user;proxy_cache_methods GET HEAD POST;proxy_redirect off;proxy_http_version 1.1;proxy_set_header Host                $http_host;proxy_set_header X-Real-IP           $remote_addr;proxy_set_header X-Forwarded-For     $proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto   $scheme;proxy_set_header X-Frame-Options     SAMEORIGIN;server_tokens off;client_max_body_size 50G;add_header X-Cache $upstream_cache_status;autoindex off;resolver      127.0.0.1:53 ipv6=off;server {listen 80;location / {set $service  '';rewrite_by_lua 'local host = ngx.var.hostlocal m = ngx.re.match(host, "(.+).test.foo.io")if m thenngx.var.service = "my-svc-" .. m[1]end';proxy_pass http://$service;}}}

用 Service 的示例(service.yaml):

apiVersion: v1
kind: Service
metadata:labels:component: nginxname: service-nginx
spec:type: LoadBalancerports:- name: httpport: 80targetPort: httpselector:component: nginx

用 Ingress 的示例(ingress.yaml):

apiVersion: extensions/v1beta1
kind: Ingress
metadata:name: ingress-nginx
spec:rules:- host: "*.test.foo.io"http:paths:- backend:serviceName: service-nginxservicePort: 80path: /

2.2 有状态业务容器部署策略

StatefulSet 旨在与有状态的应用及分布式系统一起使用。为了理解 StatefulSet 的基本特性,我们使用 StatefulSet 部署一个简单的 Web 应用。

创建一个 StatefulSet 示例(web.yaml):

apiVersion: v1
kind: Service
metadata:name: nginxlabels:app: nginx
spec:ports:- port: 80name: webclusterIP: Noneselector:app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:name: web
spec:serviceName: "nginx"replicas: 2selector:matchLabels:app: nginxtemplate:metadata:labels:app: nginxspec:containers:- name: nginximage: gcr.azk8s.cn/google_containers/nginx-slim:0.8ports:- containerPort: 80name: webvolumeMounts:- name: wwwmountPath: /usr/share/nginx/htmlvolumeClaimTemplates:- metadata:name: wwwspec:accessModes: [ "ReadWriteOnce" ]resources:requests:storage: 1Gi

注意 StatefulSet 对象运行的特点之一就是,StatefulSet 中的 Pod 拥有一个唯一的顺序索引和稳定的网络身份标识。这个输出最终将看起来像如下样子:

kubectl get pods -l app=nginxNAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          1m
web-1     1/1       Running   0          1m

很多文档在提及 Statefulset 对象的概念时,用户容易望文生义,常常把挂盘的容器实例当成有状态实例。这是不准确的解释。在 Kubernetes 的世界里,有稳定的网络身份标识的容器组才是有状态的应用。例如:

for i in 0 1; do kubectl exec web-$i -- sh -c 'hostname'; done
web-0
web-1

另外,我们使用 kubectl run 运行一个提供 nslookup 命令的容器。通过对 Pod 的主机名执行 nslookup,你可以检查他们在集群内部的 DNS 地址。示例如下:

kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm
nslookup web-0.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.localName:      web-0.nginx
Address 1: 10.244.1.6nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.localName:      web-1.nginx
Address 1: 10.244.2.6

2.2.1 灵活运用 StatefulSet 的 Pod 管理策略

通常对于常规分布式微服务业务系统来说,StatefulSet 的顺序性保证是不必要的。这些系统仅仅要求唯一性和身份标志。为了加快这个部署策略,我们通过引入 .spec.podManagementPolicy 解决。

Parallel pod 管理策略告诉 StatefulSet 控制器并行的终止所有 Pod,在启动或终止另一个 Pod 前,不必等待这些 Pod 变成 Running 和 Ready 或者完全终止状态。示例如下:

apiVersion: v1
kind: Service
metadata:name: nginxlabels:app: nginx
spec:ports:- port: 80name: webclusterIP: Noneselector:app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:name: web
spec:serviceName: "nginx"podManagementPolicy: "Parallel"replicas: 2selector:matchLabels:app: nginxtemplate:metadata:labels:app: nginxspec:containers:- name: nginximage: gcr.azk8s.cn/google_containers/nginx-slim:0.8ports:- containerPort: 80name: webvolumeMounts:- name: wwwmountPath: /usr/share/nginx/htmlvolumeClaimTemplates:- metadata:name: wwwspec:accessModes: [ "ReadWriteOnce" ]resources:requests:storage: 1Gi

2.3 业务运维类容器部署策略

在我们部署 Kubernetes 扩展 DNS、Ingress、Calico 能力时需要在每个工作节点部署守护进程的程序,这个时候需要采用 DaemonSet 来部署系统业务容器。默认 DaemonSet 采用滚动更新策略来更新容器,可以通过执行如下命令确认:

kubectl get ds/<daemonset-name> -o go-template='{{.spec.updateStrategy.type}}{{"\n"}}'RollingUpdate

在日常工作中,我们对守护进程只需要执行更换镜像的操作:

kubectl set image ds/<daemonset-name> <container-name>=<container-new-image>

查看滚动更新状态确认当前进度:

kubectl rollout status ds/<daemonset-name>

当滚动更新完成时,输出结果如下:

daemonset "<daemonset-name>" successfully rolled out

此外,我们还有一些定期执行脚本任务的需求,这些需求可以通过 Kubernetes 提供的 CronJob 对象来管理,示例如下:

apiVersion: batch/v1beta1
kind: CronJob
metadata:name: hello
spec:schedule: "*/1 * * * *"successfulJobsHistoryLimit: 0failedJobsHistoryLimit: 0jobTemplate:spec:template:spec:containers:- name: helloimage: busyboxargs:- /bin/sh- -c- date; echo Hello from the Kubernetes clusterrestartPolicy: OnFailure

三、K8s 落地方案设计

3.1 构建弹性集群策略

Kubernetes 集群架构是为单数据中心设计的容器管理集群系统。在企业落地的过程中,因为场景、业务、需求的变化,我们已经演化出不同的集群部署方案,大概分类为统一共享集群、独立环境多区集群、应用环境多区集群、专用小型集群:通过以上的对比分析,显然当前最佳的方式是,以环境为中心或以应用为中心部署多集群模式会获得最佳的收益。

成本 管理 弹性 安全
统一共享集群
独立环境多集群 中上 中上 中下 中下
应用环境多集群 中下 中下 中上 中上
专用小集群

3.2 构建弹性 CI/CD 流程的策略

构建 CI/CD 流程的工具很多, 但是我们无论使用何种工具,我们都会困 惑如何引入 Kubernetes 系统。通过实践得知,目前业界主要在采用 GitOps 工作流与 Kubernetes 配合使用可以获得很多的收益。这里我们可以参考业界知名的 CI/CD 工具 JenkinsX 架构图作为参考:

GitOps 配合 Jenkins 的 Pipeline 流水线,可以创建业务场景中需要的流水线,可以让业务应用根据需要在各种环境中切换并持续迭代。这种策略的好处在于充分利用 Git 的版本工作流控制了代码的集成质量,并且依靠流水线的特性又让持续的迭代能力可以得到充分体现。

3.2 构建弹性多租户资源管理策略

Kubernetes 内部的账号系统有 User、Group、ServiceAccount,当我们通过 RBAC 授权获得资源权限之后,其实这 3 个资源的权限能力是一样的。因为使用场景的不同,针对人的权限,我们一般会提供 User、Group 对象。当面对 Pod 之间,或者是外部系统服务对 Kubernetes API 的调用时,一般会采用 ServiceAccount。在原生 Kubernetes 环境下,我们可以通过 Namespace 把账号和资源进行绑定,以实现基于 API 级别的多租户。但是原生的多租户配置过于繁琐,一般我们会采用一些辅助的开源多租户工具来帮助我们,例如 Kiosk 多租户扩展套件:通过 Kiosk 的设计流程图,我们可以清晰地定义每一个用户的权限,并配置合理的资源环境。让原来繁琐的配置过程简化成默认的租户模板,让多租户的配置过程变得更标准。

3.2 构建弹性安全策略

基于 Kubernetes 容器集群的安全考量,它的攻击面很多。所以我们要想做一份完备的安全策略,依然需要借助在系统层面的安全经验作为参考。根据业界知名的 MITRE ATT&CK 全球安全知识库的安全框架设计,我们有如下方面需要考量:

3.2.1 Initial Access(准入攻击面)

我们需要考虑的面主要是认证授权的审计工作。比如在云端的 Kubernetes,当云端的认证凭证泄露就会导致容器集群暴露在外。比如 Kubeconfig 文件,它是集群管理员的管理授权文件,一旦被攻击者获得授权,整个集群就会暴露在攻击者的眼前。另外基础镜像的潜在 Bug 问题、应用程序的漏洞等问题,稍有不慎,也会对集群带来安全隐患。还有内置的开源面板 Kubernetes Dashboard 也不应该暴露在外网,需要保证其面板的端口安全。

3.2.2 Execution(执行攻击面)

本攻击面需要防范的地方是防止攻击者能直接在容器内部执行程序的能力。比如 Kubernetes 的 kubectl exec 命令就可以进入容器内部执行命令。另外,攻击者如果包含有运行容器的权限,就可以使用合法的 Service Account 账号访问 API Server,然后尝试攻击。还有如果容器内置了 SSH 服务,也能通过网络钓鱼的方式让攻击者获取容器的远程访问权限。

3.2.3 Persistence(后门攻击面)

这个攻击面主要利用集群特性来部署后门来获得持续控制集群资源的目的。比如提供含有后门程序的容器就可以在每一台主机上部署一个实例隐藏后门程序。另外,Kubernetes 集群默认支持 hostPath 挂载特性方便攻击者挂载可读写目录并在主机留下后门程序,方便下次通过 Cronjob 技术挂载此目录并执行后门程序。

3.2.4 Privilege escalation(权限提权攻击面)

这里主要是因为容器默认具备系统特权执行的能力,当容器启动 Privileged 参数是可以直接访问主机 Kernel 提供的系统能力的,让攻击者可以执行系统后门攻击。另外,Kubernetes 内置了 cluster-admin 超级管理员权限,当攻击者具有 cluster-binding 的权力,他就可以赋予普通用户 cluster-admin 的角色并直接取得集群管理员的角色权力。

3.2.5 Defense evasion(防御性攻击面)

这个技术主要是攻击者通过清空日志或者事件来隐藏自己的攻击行踪的技术。比如:攻击者通过删除容器系统日志来隐藏后门程序容器的破坏行为。另外攻击者可以通过 kubectl delete 方式重置容器实例,变相清空事件日志来达到隐藏攻击行为。

3.2.6 Credential access(凭证访问攻击面)

这个攻击技术主要是攻击者了解 Kubernetes 的特性,专门扫描获取密钥凭证的技术。比如通过扫描 secrets 获得潜在的攻击密钥。另外容器应用程序一般通过环境变量赋值密钥位置,攻击者也可以通过遍历环境变量获得敏感凭证数据。

3.2.7 Discovery(扫描攻击技术面)

当攻击者熟悉 Kubernetes 集群的特性之后,可以通过扫描 API Server 的接口、Kubelet API 接口、Pod 端口获得必要的攻击漏洞。另外攻击者可以在集群中运行容器,然后渗透进入 Dashboard 开源面板容器,用此面板容器的身份去 API Server 收集集群的信息。

3.2.8 Lateral movement(侧面攻击面)

攻击者通过第三方系统的漏洞获得攻击 Kubernetes 集群的能力。比如当攻击者拥有 Dashboard 的管理权限,就可以通过内部容器的 exec 能力在容器内部执行木马漏洞程序。因为集群内部的 Pod 网络是互联互通的,所以攻击者也可以任意访问任何感兴趣的 Pod 容器。

3.2.9 Impact(破坏攻击面)

攻击者通过破坏、滥用和扰乱正常执行行为来达到破坏环境的目的。例如删除 Deployment 配置、存储和计算资源等破坏容器运行。另外就是在容器内运行挖矿程序等非法滥用计算资源。还有 API Server 的拒绝服务攻击让集群不可用。

为此,我们的安全策略是给用户提供最小的授权来运行容器。很多用户通过建立专用的管理面板来阻隔用户对 Kubernetes 的接触,这是比较常见的做法。但是,目前云端很多 Kubernetes 服务仍然会让用户接触到主机层面的入口,让安全问题暴露在潜在攻击者的面前。一般通过 VPC 的方式限制只有内部人员可以访问集群,但是内部的安全审计仍然是一个长期需要维护的过程,需要专业的安全人员制定完善的防范策略来降低攻击风险。

四、k8s资料总结

Kubernetes 大版本基本上 3 个月就会更新一次,如果让我们天天泡在阅读各种文档资料的话,我相信一定会让很多人头脑大爆炸。为了解决业务应用的发布问题,Kubernetes 的源头信息必要保证最新、最全、最权威。这里,笔者再次强调,官方的社区文档站点(Kubernetes Documentation | Kubernetes)就是最新、最全、最权威的参考资料了。不要在到别处求证,官方的文档都是经过全球开发者的阅读和监督,比其它转载的要及时可靠。

另外,Kubernetes 是云原生计算基金会(CNCF)的技术蓝图中了一个组件,当你遇到技术难题时,请第一时间参考 CNCF 的技术蓝图找点灵感:

CNCF Cloud Native Interactive Landscape

相信你一定可以获得满意的架构建议和方案。

笔者建议:遇到概念问题不清楚,请到 Kubernetes Documentation | Kubernetes 搜索获取最新的资料。遇到技术架构的问题,请到 CNCF Cloud Native Interactive Landscape 参考云原生的技术架构蓝图,相信也可以找到线索。

博文参考

Kubernetes——Kubernetes系统组件与架构相关推荐

  1. Kubernetes的系统架构与设计理念

    Kubernetes与云原生应用简介 随着Docker技术的发展和广泛流行,云原生应用和容器调度管理系统也成为IT领域大热的词汇.事实上,云原生应用的思想,在Docker技术火爆之前,已经由云计算技术 ...

  2. 《Kubernetes与云原生应用》系列之Kubernetes的系统架构与设计理念

    http://www.infoq.com/cn/articles/kubernetes-and-cloud-native-applications-part01 <Kubernetes与云原生应 ...

  3. Kubernetes 下零信任安全架构分析

    作者 杨宁(麟童) 阿里云基础产品事业部高级安全专家 刘梓溪(寞白) 蚂蚁金服大安全基础安全安全专家 李婷婷(鸿杉) 蚂蚁金服大安全基础安全资深安全专家 简介 零信任安全最早由著名研究机构 Forre ...

  4. Kubernetes — 调度系统

    目录 文章目录 目录 调度系统 Kubernetes 调度器的设计 Kubernetes 调度器的工作流 Kubernetes 调度系统的未来 Scheduler Extender(调度器扩展) Mu ...

  5. 阿里云容器服务新增支持Kubernetes编排系统,性能重大提升

    摘要: 作为容器编排系统的两大流派, Kubernetes和Swarm的重要性不言而喻.融合了两大高性能集成的阿里云容器服务,不仅可以降低50%的基础架构成本,提高交付速度将产品迭代加快13倍,还可以 ...

  6. 进击的 Kubernetes 调度系统(二):支持批任务的 Coscheduling/Gang scheduling

    作者 | 王庆璨(阿里云技术专家).张凯(阿里云高级技术专家) **导读:**阿里云容器服务团队结合多年 Kubernetes 产品与客户支持经验,对 Kube-scheduler 进行了大量优化和扩 ...

  7. 进击的 Kubernetes 调度系统(一):Kubernetes scheduling framework

    作者 | 王庆璨(阿里云技术专家).张凯(阿里云高级技术专家) 导读:阿里云容器服务团队结合多年 Kubernetes 产品与客户支持经验,对 Kube-scheduler 进行了大量优化和扩展,逐步 ...

  8. 进击的Kubernetes调度系统(一):SchedulingFramework

    作者:王庆璨 张凯 前言 Kubernetes已经成为目前事实标准上的容器集群管理平台.它为容器化应用提供了自动化部署.运维.资源调度等全生命周期管理功能.经过3年多的快速发展,Kubernetes在 ...

  9. 阿里云容器服务新增支持Kubernetes编排系统,性能重大提升 1

    摘要: 作为容器编排系统的两大流派, Kubernetes和Swarm的重要性不言而喻.融合了两大高性能集成的阿里云容器服务,不仅可以降低50%的基础架构成本,提高交付速度将产品迭代加快13倍,还可以 ...

最新文章

  1. 简单实现ReplaceAll(转)
  2. 解决yum错误Error: requested datatype primary not available
  3. Android 体系结构和应用程序组成
  4. RabbitMQ快速入门--介绍和安装
  5. 频域/s域/z域三大变换的发展史及其联系
  6. [人工智能]隔墙有眼,吓屎了
  7. Redis数据库,Jedis接口分类(使用)说明
  8. 有没有知道如何连接DB2的数据库?
  9. Java常用到的快捷键
  10. 快速(动易)模板制作
  11. java soap_Java使用SOAP协议访问webservice接口
  12. 超级详细-NMOS、PMOS的工作原理及相关内容整理(上)
  13. XJOI 9552矩阵游戏(2级1段)
  14. 坐标方位角计算通用公式
  15. matlab学习—分段函数计算
  16. 【Unity3D】 Unity Chan项目分享
  17. 魂武者服务器维护,《魂武者》8月16日停机更新公告
  18. 在二叉树中找到两个节点的最近公共祖先(C++)
  19. 苹果cmsV10魔改短视多功能主题5.2版本
  20. 电脑技术小技巧,绝对精品~~

热门文章

  1. 3款优秀开源密码保护锁
  2. 《Tensorflow 从基础到实战》01 安装与基础操作、手写数据集、逻辑回归
  3. Docker小白入门教程--docker理解与实战(懵逼三连--Docker是什么,为什么要使用Docker,如何使用Docker?)
  4. canvas生成图片上传服务器,网络图片生成图片文件
  5. 俄罗斯离美国有多近?只有3701米
  6. 以旧焕新的css滤镜
  7. 保姆级idea配置Git管理工具
  8. 二分查找算法-C语言实现
  9. 产品出现品质问题,生产和品管到底谁来负责?
  10. scrapy项目2:爬取智联招聘的金融类高端岗位(spider类)