K8s必知必会

前言

Kubernetes是用于自动部署,扩展和容器花应用程序的开源系统,简单的来讲就是容器集群调度平台,可以自动化应用容器的部署、扩展和操作,提供以容器为中心的基础架构。

使用 Kubernetes, 您可以快速高效地响应客户需求:

  • 快速、可预测地部署您的应用程序
  • 拥有即时扩展应用程序的能力
  • 不影响现有业务的情况下,无缝地发布新功能
  • 优化硬件资源,降低成本

应用场景

我们先看下k8s的官方对应用场景的定义。

Kubernetes是Google基于Borg开源的容器编排调度引擎,作为CNCF(Cloud Native Computing Foundation)最重要的组件之一,它的目标不仅仅是一个编排系统,而是提供一个规范,可以让你来描述集群的架构,定义服务的最终状态,Kubernetes可以帮你将系统自动地达到和维持在这个状态。Kubernetes作为云原生应用的基石,相当于一个云操作系统,其重要性不言而喻。

这些定义是什么鬼?为什么每个字我都认识,但就不明白是啥意思?

来,我们先理解下什么是云?这里的云,肯定不是天上飘的那种,我们说的云,其实就是云服务。我还是先去翻翻维基百科吧。云的定义是这样子的:

云计算(英语:cloud computing),是一种基于互联网的计算方式,通过这种方式,共享的软硬件资源和信息可以按需求提供给计算机各种终端和其他设备。

所谓的云就是,以互联网的方式提供服务器端的计算服务。在云上,你可以运行很多自定义的服务,而不需要关心基础设施的细节,简单的来讲,你不需要自己购买维护服务器硬件了,因为你可以买云提供商已经部署的服务器,直接用就可以了。

那么问题来了,虽然不需要管理维护服务器硬件了,但我们运行部署的自定义服务,还是得自己维护吧。这时候,K8s就站出来说,它可以帮你管理运行的应用,不管你的应用啥时候挂了,还是需要扩容,它都可以帮你搞定。只要你描述了整个集群的架构是怎么样的,定义你的服务最终状态,K8s都可以很好管理和维护服务的状态。那K8s到底是怎么做到的呢?

组成部分

K8s的主要组成部分有3个,分别为:Master、Node、Pods。

Master

负责集群内所有pods的状态,负载均衡等,主要组件有Ectd、Controller Manager、API Server、Scheduler等。

  1. 采用Etcd键值对服务,保存了整个集群的状态。
  2. Controller Manager负责维护集群的状态,比如故障检测、自动扩展、滚动更新等
  3. API Server提供了资源操作的唯一入口,并提供认证、授权、访问控制、API注册和发现等机制
  4. Scheduler负责资源的调度,按照预定的调度策略将Pod调度到相应的机器上

Node

这些Node和我们经常遇到的节点,其实就是一回事。这些都是真实的物理机器,连接了网络,有CPU计算能力。主要组成有:

  1. kubelet负责维护容器的生命周期,同时也负责Volume(CSI)和网络(CNI)的管理

  2. Container runtime负责镜像管理以及Pod和容器的真正运行(CRI)

  3. kube-proxy负责为Service提供cluster内部的服务发现和负载均衡

再往下走,就涉及到主机的操作系统OS,底层硬件了。

Pods

这是整个架构中的精华部分。

对节点抽象出来的一层,节点的操作系统,硬件部分可能会有不同的差异,通过在Docker+Kubelet+kuebe-proxy的基础上抽象出一个集装箱层,类似与编程中对业务逻辑的抽象,也就是Pod.

这一层关注的是标准化,提供集装箱Pod的格式给到业务,至于里面的应用,只要符合Pod的标准,都可以正常运行起来,并且还是支持自动部署,自动恢复,负载均衡等。

关键点

Kubernetes设计理念和功能其实就是一个类似Linux的分层架构。

分类 实现 备注
核心层 Kubernetes最核心的功能,对外提供API构建高层的应用,对内提供插件式应用执行环境
服务层 部署服务(无状态应用、有状态应用、批处理任务、集群应用等)
路由服务(服务发现、DNS解析等)
Service Mesh服务(部分位于应用层)
管理服务:系统度量(如基础设施、容器和网络的度量),自动化(如自动扩展、动态Provision等)以及策略管理(RBAC、Quota、PSP、NetworkPolicy等)、Service Mesh(部分位于管理层)
接口服务:kubectl命令行工具、客户端SDK以及集群联邦
应用层 也就是可以运行的生态系统服务
Kubernetes外部:日志、监控、配置管理、CI/CD、Workflow、FaaS、OTS应用、ChatOps、GitOps、SecOps等
Kubernetes内部:CRICNICSI、镜像仓库、Cloud Provider、集群自身的配置和管理等

总结

文章中,我们了解到云的定义,K8s的应用场景,以及K8s的常用组件,下面,我们尝试搭建K8s组件Dashborad,方便管理集群。

参考链接

  1. Kubernetes架构

为什么你需要写单元测试?

前言

维基百科中对单元测试的定义是这样的:

计算机编程中,单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

但在日常开发中,大多数情况下,可能开发需求都排得满满的,哪还有时间去写单元测试?在我工作的前几年,基本上也没有写单元测试。单元测试只是学习了解而已。直到,我遇到了一个由于bug引起的重大事故。事后的反思,让我开始关注到代码质量的问题。

有没有办法提前发现这类问题?如何防止这类问题再次发生?如何提升代码质量?

面对上面的灵魂拷问,我对如何写好代码进行深刻的反思。写完代码,并不代表,你已经做完了,做好了,代码质量才是程序员的核心竞争力。测试通过的代码,才能有信心交付。单元测试才是有效提升代码质量的最简单可靠的方式。

为什么要写单元测试

对自己的代码有信心

修改代码后通过单元测试,说明修改的部分并没有破坏测试用例,这能增加对代码的交付信心。起码你有底气交给测试的同学去测试你的代码。但也要了解,通过单元测试并不意味着没有问题,有一些bug并不是单元测试的问题。单元测试关注的一个小的单元,整体功能的完整性并不能通过单元测试代替集成测试。

代码重构

很多时候,写业务代码,时间有很紧张,想要日后优化重构的话,没有写单元测试用例,根本就是寸步难行。很可能重构后,代码会不小心破坏掉之前填的坑。所以开发一般不会去重构现有的代码。但如果有单元测试的话,就不一样了,对现有的功能重构,建立对应的测试用例,改完代码,跑一边单元测试,如果测试都通过了,那么就代表重构并没有破坏原有的逻辑正确性。只有单元测试质量高,覆盖率高,基本上你可以随时重构优化你的代码。当然,单元测试依然适用于小的单元,重构一个函数,一个类,都是可以的。比如将当前的项目重构程为微服务,这时可能就需要重写单元测试了。

熟悉代码

单元测试通常会包含较多业务异常的测试用例,通过单元测试可以了解到需要注意的点,哪些业务是如何运行的,你不需要深入阅读代码就可以了解代码是做什么的,也是一个熟悉业务和代码的好办法。

怎么写单元测试

通常情况下,我们都是在已有的项目下进行单元测试的编写。单元测试都会有对应开发框架,以PHP为例,我推荐PHPunit,它是面向程序员的单元测试框架。在已有的项目加入phpunit,很简单。你需要以下3个步骤:

  1. 在项目加入配置文件phpunit.xml
  2. 安装好composer,并添加phpunit依赖
  3. 添加自定义引导文件

phpunit.xml

通常phpunit.xml的模板如下:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="cases/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>./cases/</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory suffix=".php">cases/</directory>
        </whitelist>
    </filter>
    <logging>
        <log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
    </logging>
</phpunit>

常见选项参数解释:

  1. bootstrap 定义你的引导文件,通常是你的框架加载类。
  2. colors 为true 可在终端显示颜色
  3. convertErrorsToExceptions 转换Fatal Error为异常
  4. convertErrorsToExceptions 转换注意Notice为异常
  5. convertWarningsToExceptions 转换警告warn为异常
  6. processIsolation 为每个测试运行在单独的进程增加隔离
  7. stopOnFailure 出现失败后是否停止运行测试用例
  8. testsuites->testsuite 通过name参数区分不同的测试用例,需要配置搜索用例的目录
  9. filter->whitelist 配置代码覆盖率的白名单
  10. logging->log->coverage-text 配置代码覆盖率的输出为php 标准输出,不显示白名单文件

composer

通常安装composer很简单

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

通过composer安装phpunit

#php7.0 - 7.2
composer require --dev phpunit/phpunit ^6
#php7.1 - 7.3
composer require --dev phpunit/phpunit ^7
#php7.2 - 7.4
composer require --dev phpunit/phpunit ^8

引导文件

有几种方式添加你的引导文件,第一种直接在命令行添加:

phpunit --bootstrap cases/autoload.php

第二种,直接在phpunit.xml配置参数

bootstrap="cases/autoload.php"

该文件用于加载常用的框架,公用库,函数等。

TDD

在日常工作中,使用单元测试的工作方式有3类:

  1. 从一开始就编写对应测试用例,也就是测试驱动开发,这其实就是TDD。
  2. 第二种是,先编写部分的代码,然后再编写对应的测试用例。
  3. 第三种,编写完代码,再编写测试用例。
分类 优点 不足 备注
TDD 不需要了解小单元的业务逻辑 关注点更多是在测试上,无法深入了解业务逻辑
T&C(Test & Code) 实时性强,写完一段代码就运行下测试,通过测试很有成就感 实现的同时就需要考虑,如何更好的测试复用
TAC(Test After Code) 符合日常开发流程 忘记写测试用例,重新梳理思路

实践经验

在实践单元测试的过程中,有一些经验可以供你参考。

什么时候需要写单元测试

如果每个函数方法都去写单元测试的话,时间可能根本来不及。建议以下代码进行单元测试:

  1. 核心业务逻辑。阅读、充值、购买等。
  2. 逻辑比较复杂的。
  3. 容易出错。
  4. 不容易理解的代码。
  5. 公共库文件。

测试用例

最好的情况下,是把测试用例与代码一起管理,这样可以方便阅读测试与被测试的代码。在进行代码部署时,可以不需要发布测试用例。如果想要做持续集成,也可以单独管理测试用例。建议的目录以模块进行组织,避免出现一个文件几千条测试用例的混乱情况。下面是一个参考的目录结构:

➜  tests git:(master) tree
.
├── bin
│   ├── release.sh // 发布脚本
├── cases
│   ├── APITest.php // 单元测试基类
│   ├── AppTest.php // App帮助函数
│   ├── autoload.php // 自动加载框架
│   ├── modulea // 模块a
│   ├── moduleb // 模块b
│   ├── modulec // 模块c
│   ├── moduled // 模块d
│   ├── modulee // 模块e
│   └── modulef // 模块f
├── composer.json // composer加载配置文件
├── composer.lock // composer锁文件,锁定安装时的版本
├── phpunit.xml // phpunit单元测试配置文件
└── README.md //单元测试文档

经验之谈

  1. 单元测试不是越多越好,而是越有效越好!
  2. 单元测试,最好是与实现代码同时进行。
  3. 单元测试需要覆盖核心业务逻辑等。

总结

在这篇文章,总结了从自己不会写单元测试到开始学习单元测试的经验,对比了3类单元测试的开发方式,分享了单元测试的实践经验。如果可能的话,可以将单元测试加入到日常的持续集成中,大大提高构建出来的代码质量。

程序员使用单元测试是提升代码质量最简单可靠的方式。

Git工作流程实践

前言

上篇文章说到,选择Git作为版本控制系统,就是选择一种新的团队协作方式。Git常用的工作流程有几种:Git Flow、GitHub Flow、GitLab Flow。这三种工作流程几乎都是以功能驱动开发。先是有需求,才开始进行开发,建立对应的功能分支,完成开发后,该分支就合并到主分支,然后被删除。阮老师有一篇文章讲述得很清楚,这里我只是讲自己实践下来,觉得比较好的一种模式:GitLab Flow New。

为什么使用GitLab Flow New工作流程?

这里,我先讲讲为什么没有采用Git Flow 与 GitHub Flow的工作流程。

Git Flow的问题在于你需要花费时间去维护一个开发分支。

大部分的人都需要从开发分支新建分支,如果人数超过3个以后,你会发现需要去解决日常工作中的异常多的代码冲突。因为可能很多人,在新建的分支上进行开发,但合并开发分支的更新,却是在完成功能开发,这时候开发分支已经累计了无数的代码更新,再去合并的话,可能冲突会很多。

GitHub Flow的问题在于,你需要发起一个Pull Request,简称PR。你的PR通过了评审和讨论后,就合并到master里。可实际上合并到master的代码,也不一定是线上最新的代码。实际工作里,可能还会有预发布这一环节。

如何优雅使用GitLab Flow New工作流程

实际上,我们是这样使用GitLab Flow的。

首先定义3个主要分支。master为生产环境的分支,与线上代码保持一致。预发布的分支为prelease。开发测试分支从master里新建分支进行功能开发。

分支 作用 备注
Master 主分支,与生产环境的代码保持一致
Prelease 预发布分支,与预发布环境的代码保持一致
fea/xxx 开发分支从master创建分支

当你的功能开发完成并通过测试后,会新建一个MR(Merge Request与Pull Request类似,这是GitLab 里的叫法),请求合并到prelease分支。

发起MR后,就可以通知相关人员进行代码的Review,代码审核和讨论,没有问题后,会尝试合并到prelease。

这时自动构建工具会自动发布一个版本到预发布环境中。与此同时,根据这次改动将全部的代码打包一个全量/增量的zip压缩包,并请求发布系统获取标识唯一的版本号码,比如20190607_130102_9159

相关的开发和测试人员可以进行预发布环境的测试,当通过测试验证后,在发布系统对上述的版本号码进行发布,这样就更新到全量的线上机器了。

这里用一张图,说明该工作流程的步骤。
Git工作流程实践

常用技巧

Merge Request

我们采用GitLab的Merge Request机制进行代码Review,每一个新功能必须通过MR才能合并到prelease以及master。与GitLab Flow的上游优先,持续发布的原则类似,开发分支的代码只有在开发/测试环境通过了测试,才会允许发起MR合并到prelease。唯一例外的是,线上紧急问题的修复,是可以跳过这个流程的。

采用MR后,会利用no fast worword合并,并生成一个对应commit id,这样实现了代码线性的提交,也方便后面代码回滚。确认代码合并时,通常会选择合并后自动删除源分支的选项。

合并prelease的更新

开发人员提交MR后,可能会遇到prelease已更新,也就是落后提交的问题(x commits behind),这时,采用rebase的方式将prelease分支的代码与自己分支进行合并,并重新提交。

举例,当前你发起了一个MR,分支为fea/new,在merge_requests界面发现落后prelease几个提交,那么正确的方式是:

# 切换到prelease分支
git checkout prelease
# 拉取对应的更新
git pull --rebase origin prelease
# 切换到开发分支
git checkout fea/new
# 合并prelease的更新
git rebase prelease
# 推送到远端分支
git push -f origin fea/new

合并提交

为了方便他人阅读你的代码,也会要求开发人员将多次简短的提交,合并一个完整注释的提交,方便代码review。

举例,你会发现有这样的提交记录:

* 调试功能a x7 commitid7 author1 2019-06-15 17:00:07
* 调试功能a x6 commitid6 author1 2019-06-15 17:00:06
* 调试功能a x5 commitid5 author1 2019-06-15 17:00:07
* 调试功能a x4 commitid4 author1 2019-06-15 17:00:06
* 调试功能a x3 commitid3 author1 2019-06-15 17:00:07
* 调试功能a x2 commitid2 author1 2019-06-15 17:00:06
* 添加功能a    commitid1 author1 2019-06-15 17:00:07

你应该把这些提交合并到一起:

git checkout fea/new
git reset commitid1
git add .
git commit -m "+ 添加功能a,完成测试,参考下面的注释模板"
git push -f origin fea/new

Protected Branch

我们是会把prelease以及master进行保护,只有管理员、拥有代码审核权限的人员才会允许进行代码的推送。开发人员只允许推送自己的开发,测试分支。这样会避免prelease以及master主干分支的代码被不小心”污染”,也就是没有通过测试的代码提交到主干分支中。

注释模板

一个较好的开发习惯是提交详细的注释。详细的注释有几个好处:

  1. 提交更多的提交信息
  2. 方便快速浏览查找。
  3. 可以直接生成改动日志。

我常用的几个小技巧:

  1. 提交信息前添加固定字符代表增删改查,比如* 代表更新 + 代表添加 – 代表文件删除。
  2. 代码注释每行限制72个字符,方便过长被自动截断。
  3. 最后空出一个行,让上面的行作为标题,再下一行提供相关的链接和关键字。

一个简单的模板如下:

* 更新 写作 Git工作流程实践

整理自己在日常工作中实践下来,有效的的Git工作流程。

还有他人提供的常用提交模板,你也可以参考https://gist.github.com/jmaxhu/8e7fb69a7dcec1b9b953、

合并最新的代码

将自己的开发分支合并master更新的代码。这是一个很重要的操作。每天上班第一件事,就是拉取最新master分支的代码,采用rebase方式,合并自己开发分支的代码,保证自己的代码提交是线性的。这样在后续代码合并会显著的减少很多不必要的冲突合并时间。

总结

在这篇文章里,我分享自己在Git工作流程的一些尝试。定义工作流程不是目的,我认为最终目的是提升整体团队的工作效率以及避免经常遇到的”代码覆盖”问题,进而引起的”环境问题”。解决在日常工作可能会遇到的问题,才是我们最终目的。

每个团队的使用习惯,业务模式,可能都不相同。一种工作流程可能也无法满足所有人的需求,大家可以根据自己的习惯或者最适合当前团队的方法去尝试,去定义适合自身团队的工作流程。

Git工作流程实践