Nix(OS)挖坑笔记(2) - home-manager

再会

前一篇中说到我在大致了解了Nix(OS)的特性之后,由于没有找到合适的使用场景,并没有实际安装和使用它,但作为一项有潜力的技术,我并没有停止对它的关注。直到有一天,看到一篇帖子分享了使用Nix来管理用户环境和dotfiles的经验时,情况发生了变化。

目标

使用申明式语言来管理本地开发环境,并在多个Linux环境中同步。

问题

管理本地开发环境的需求一直存在,在8年前我曾尝试过dotfiles管理模式但很快就放弃了。现在回想起来,大致有以下原因:

  • dotfiles只管理了Home目录下的配置文件,而软件本身还是需要用apt或者yum进行安装。所以在全新环境中初始化以及在各个环境中保持一致需要引入其他的方案
  • 有不少辅助工具可选,但是试用过几款之后均不是非常满意,特别是在管理变更这方面
  • Ansible等配置工具可以某种程度上实现申明式的开发环境管理,但是对于个人使用来说还是太重了
  • 在服务器上配置个人环境会需要root权限安装全局软件包,一来使得服务器环境变脏,而且多人使用的时候还会出现包冲突的问题
  • 如果想要统一Mac和Linux环境以及多个不同Linux发行版的环境并不容易,需要付出很多额外的努力

我们需要一款能同时管理软件包和它的配置文件的工具,它能够在用户的个人目录中完成全部的工作使得各个用户的环境完全独立互不干扰,并且它应当是申明式的,用户只需要告诉工具他期望的状态,具体的配置和变更则由工具自己决定,用户无需关注。此外,它最好使用起来简单易懂,方便用户上手。

问题是,这样的工具存在吗?

home-manager

Nix的一些特性非常适合用在这个场景下,比如:

  • Nix可以独立于原发行版运行。不论是在Redhat系还是Debian系的发行版中,它都可以建立单独的环境,安装自己的包,而这些包并不会和原发行版冲突。
  • Nix天生是申明式的,无需再引入额外的工具将申明式的定义转换为命令式的指令
  • Nix的强大变更管理功能,使得用户在进行修改时更加有信心。如果变更失败可以随时一键回退到上一个版本

home-manager是Nix(OS)生态中用于管理用户开发环境的工具。它可以结合NixOS使用,在NixOS中对整个系统进行定义,在home-manager中对用户自己的环境进行更加个性化的定义。它也可以独立于NixOS单独使用,在其他非NixOS发行版环境中,使用Nix构建用户自己独立的开发环境。

独立使用home-manager非常适合我这种暂时不打算折腾NixOS的用户,一方面可以在实战中提升对Nix(OS)的理解,另一方面它仅仅管理我的个人环境,不影响底层系统的稳定。

接下来实际上手把它用起来。

安装Nix

home-manager是构建在Nix之上的工具,所以需要先安装Nix。

$ sh <(curl -L https://nixos.org/nix/install)

经典的一行代码搞定安装。虽然不是特别优雅,但所有的组件都会安装到新创建的/nix目录下,并不会污染我原生的系统。

在安装完成后,多了几个nix开头的可执行文件,比如nix-env, nix-build可以用,之后有时间再来研究它们的功能,现在先把home-manager用起来再说。

安装home-manager

详细的流程在home-manager的README中有介绍,这里简单记录一下

$ nix-channel --add https://github.com/rycee/home-manager/archive/master.tar.gz home-manager
$ nix-channel --update
$ nix-shell '<home-manager>' -A install

现在的我其实并不太理解每条指令的作用,大概是新增加了一个软件仓库,并且安装了这个软件仓库中的home-manager这款软件的意思。

继续跳过nix-channelnix-shell的学习,跟着README接着往下走。

申明式安装软件包

home-manager已经装好,现在我们开始试着来描述我们的开发环境。在~/.config/nixpkgs/home.nix创建以下文件

{ pkgs, ... }:

{
  home.packages = [
    pkgs.go
  ];
}

再运行home-manager switch让我们的描述生效。可以看到home-manager开始干活,结束之后我们来验证一下

$ go version
go version go1.14.4 linux/amd64
$ /usr/local/go/bin/go version
go version go1.12.5 linux/amd64

最新版的Go已经安装成功,而我系统中的Go并没有受影响。which go可以看到go被安装到了~/.nix-profile/bin/go,而它其实是指向/nix/store/ipfzw3gm3vsdn4j0qz42n9438vcikzmb-home-manager-path/bin/go的 symlink

所以本质上所有的软件包还是安装在/nix/store下,只是在我的Home目录下通过symlink将可执行文件暴露了出来。由于Nix在安装时会将~/.nix-profile/bin加入PATH,我可以直接使用已经安装后的可执行文件。

接下来再继续多安装几个包,将配置文件改为

{ pkgs, ... }:

{
  home.packages = [
    pkgs.go
    pkgs.bazel
    pkgs.hugo
  ];
}

再次运行home-manager switch,可以看到bazelhugo已经安装完毕,非常方便。

配置文件使用的是Nix语言,一款函数式编程语言。就目前而言,我只知道往home.packages中增加更多的软件包的名称,就可以让home-manager安装更多的软件包。而删除某一软件包名称后再运行home-manager switch则会让home-manager从我个人环境中移除指向这一软件包的symlink,如果其他用户有安装这一软件包则他的环境不受影响。

至此,我可以在配置文件中声明我的开发环境所需要安装的软件包,利用home-manager实现软件包管理。

申明式管理配置文件

接下来看看如何实现配置文件的管理,修改配置文件为

{ pkgs, ... }:

{
  home.packages = [
    pkgs.go
    pkgs.bazel
    pkgs.hugo
    pkgs.git
  ];
  home.file.".gitconfig".text = ''
    [user]
            email = adieu@adieu.me
            name = Adieu
  '';
}

再次home-manager switch时会提示.gitconfig已经存在,这是一个防止误操作的保护。重命名后再次尝试配置成功

除了以纯文本方式进行配置以外,为了方便用户使用,home-manager还提供了多个模块,方便用户以结构化方式配置常用的软件包。将配置文件改为以下内容:

{ pkgs, ... }:

{
  home.packages = [
    pkgs.go
    pkgs.bazel
    pkgs.hugo
  ];
  programs.git = {
    enable = true;
    userEmail = "adieu@adieu.me";
    userName = "Adieu";
  };
}

这里,programs.git这个配置项会安装git软件包并同时将配置文件写入~/.config/git/config中,比直接用home.file进行配置又更加简化了一些。完整的配置项清单可以参考home-manager的用户手册

利用封装好的模块或者直接配置文件的形式,我已经具备了将现有的配置文件逐步迁移到home-manager中进行管理的能力,剩下的就是体力活了。

一个更加复杂的配置案例

接下来试一下用home-manager来安装VS Code并管理它的配置以及插件。将配置文件改为以下内容

{ pkgs, ... }:

{
  home.packages = [
    pkgs.go
    pkgs.bazel
    pkgs.hugo
  ];
  programs.git = {
    enable = true;
    userEmail = "adieu@adieu.me";
    userName = "Adieu";
  };
  nixpkgs.config.allowUnfree = true;
  nixpkgs.overlays = [ (self: super: {
    vscode-extensions = with self.vscode-utils; super.vscode-extensions // {
        golang.Go = extensionFromVscodeMarketplace {
              name = "Go";
              publisher = "golang";
              version = "0.14.4";
              sha256 = "1rid3vxm4j64kixlm65jibwgm4gimi9mry04lrgv0pa96q5ya4pi";
          };
    };
  ) ];
  programs.vscode = {
    enable = true;
    package = pkgs.vscode;
    userSettings = {
      "telemetry.enableTelemetry" = false;
      "go.useLanguageServer" = true;
      "window.zoomLevel" = 0;
      "workbench.statusBar.feedback.visible" = false;
    };
    extensions = with pkgs.vscode-extensions; [
      golang.Go
    ];
  };
}

这里我遇到了一个问题,VS Code最近将Go插件的维护移交给了Go团队,所以原来已有的ms-vscode.Go插件变得不可用。我在这里用了overlay的办法,动态patch了软件仓库,增加了golang.Go插件,并启用了它。

overlay是Nix非常重要的特性,它可以让你方便的定制整个软件仓库,今后会专门进行讲解。

另外此时我运行VS Code会发现之前工作正常的硬件加速并没有正常工作,导致卡顿明显。解决过程也是一波三折,也留到之后再做分享了。

至于在多个环境中同步,仅需要将配置文件加入版本管理,在其他环境中拉取最新的配置文件,并执行home-manager switch即可。

答案

到目前为止,我对home-manager的表现非常满意。统一的软件包和配置文件管理体验,申明式的配置方法,相对简洁的配置语言都符合我的要求。

虽然Nix的语法现在看上去还有点奇怪,但并不影响我所需要的一些基本操作。

结论

使用home-manager,我圆满完成了最初设定的目标。对于Nix的安装和Nix语言也有了最初步的体验。在前一篇中所列举的种种Nix的优点,在使用home-manager的过程中有了最直接的感受。

但现在只是把Nix当作一个黑盒来使用,还谈不上已经入门Nix,只能勉强算是会使用home-manager这款工具。接下来还需要在实战中学习Nix语法以及理解底层的运作机制。

如果您也对现有的本地开发环境和dotfiles管理方案不满意,或者是打算折腾一下Nix,欢迎也实际上手体验一下home-manager。大家共同交流和分享经验。

Nix(OS)挖坑笔记(1) - 初识

最近得空体验了一下Nix(OS),折腾过程中解决了一些遇到的问题,同时挖了更多的坑。做下笔记记录下整个过程。希望将来有机会把坑给填上。

使用Nix(OS)这一写法的原因是Nix和NixOS是相关联的不同的项目,为了方便起见我们先把它们作为一个整体来理解,将来再逐步厘清它们各自的功能和定位。

由于是从零开始慢慢摸索,难免出错,欢迎大家指正。本文定位于个人主观角度的经验分享,仅供大家参考。

初识

最开始接触Nix(OS)是在SHLUGYu老师安利之下去翻了下它的官网和文档。第一感觉是不明觉厉,为了使用Nix这个包管理工具,竟然要学习一门新的函数式编程语言,而NixOS这个Linux发行版看上去跟以往接触的任何一个发行版都不同,整体来看像是一个面向Geek和Power User的项目。

问题

每当我接触一个全新的项目,在投入时间去弄清楚各种技术细节之前,有两个最基本的问题是我想先弄明白的:

  1. 这个项目有哪些独特的功能和价值点
  2. 在哪些场景下会需要利用这些功能和价值点来解决遇到的问题

特性

要回答这两个问题,我打算从特性入手。官网上列出了多条Nix(OS)的特性,其中有几项吸引了我的注意。虽然暂时还没法完全读懂Nix(OS)的文档和代码,但是在文档的介绍下,结合在其他项目上积累的经验,我试着来理解Nix(OS)的设计思路。

申明式 (Declarative)

理解Declarative特性可以借鉴Kubernetes的设计思路。在Kubernetes中,用户用yaml文件来描述PodService等集群的状态,将这些描述文件提交给Kubernetes后,控制器程序会负责具体的集群变更。对应的,在Nix中用户的主要职责是使用声明式的方法来定义自己使用的软件包以及各软件包的参数。声明所使用的语言是名为Nix的语言。将源文件交给名为Nix的包管理工具后,程序会按照用户的要求安装相应的软件包,并更新用户环境。当用户修改描述文件后再次运行Nix,程序会根据当前的描述安装和激活新的软件包,或者取消激活老的软件包,使得当前用户环境满足用户声明。而NixOS则更进一步,用户可以直接用Nix语言描述整个操作系统。需要指出的是,就像Kubernetes中Deployment版本升级导致的Pod滚动升级一样,Nix并不会在老的软件包上进行修改得到新的软件包,而是完全新安装新的软件包。这里用到了Immutable这一特性,接下来我们来具体分析一下。

不可变 (Immutable)

Immutable并不是指系统一旦安装完成就变为只读不允许修改,而是指一个软件包以及它全部的依赖在其存续期间不变。让我们用Git作为参照物来理解Nix(OS)的Immutable特性,当我们修改一个被Git管理的文件时,修改后的版本会在新创建的commit中被引用,而老版本并不会被清除,用户可以通过老的commit哈希找到老版本。Nix(OS)中每一个软件包可以声明自己所依赖的软件包,这些间接被引入的上游软件包也会一同被安装。所有的软件包在一起构成由软件包为节点构成的依赖树。由于每个软件包在自己独立的目录中,不同版本,不同参数的同一个软件包可以并存,分别被依赖它的不同下游软件包所使用。当安装新的软件包,或者更新已有软件包时,会在原依赖树的基础上派生出新的依赖树,而原依赖树的软件包并没有被清除,只是暂时没有被使用罢了。用户可以通过切换当前版本的方式切换到不同的依赖树,就像git checkout一样。基于这一特性,Nix(OS)实现了一键回退,同一软件包多版本并存等黑科技

高度可定制 (Highly Customizable)

将Nix的包管理系统与其他包管理系统横向比较可以发现其灵活与高度可定制的特性。与使用广泛的yumapt这类基于编译后的二进制文件的包管理系统不同,Nix的包管理设计逻辑更接近于ArchGentoo这类基于源代码的包管理系统,使得用户可以对软件包的参数甚至软件包本身进行定制。这种可定制性足够底层,使得用户可以按照自己的需要打造独一无二的运行环境。此外Nix在设计中还巧妙的引入了缓存,当用户安装软件包时使用的参数与官方一致时,可以直接从缓存中下载编译后的结果,加快软件包安装速度。

除去以上三条,Nix(OS)还有更多有意思的特性,由于篇幅原因就不一一列举了。着重强调这三条的原因是将NixOS与DebianRed Hat等传统Linux发行版相比较,这三条特性正对应了使用传统发行版会遇到的几大痛点。

  • 传统发行版均为命令式的设计,系统管理员通过一系列的命令来配置系统。虽然有通过引入多一层的抽象,使用Ansible/Chef等工具提供描述式的系统配置手段,但是额外的复杂度和学习成本以及各种底层发行版带来的局限性,并没有为用户提供完美的解决方案
  • 传统发行版是基于软件包分发来设计的。软件包之间的依赖关系使得升级软件包变成了高风险的操作,运气不好就会出现软件包之间不兼容,或是不同软件包依赖同一个软件包的不同版本的情况。虽然容器的普及从一定程度上缓解了这一问题,但是无风险的升级和回退始终是系统管理员渴求的功能
  • 传统发行版的软件包普遍采用二进制文件分发的方式来提升软件包的安装体验,但是这意味着用户仅能够通过配置文件来设定软件运行模式,而无法对程序本身进行修改。虽然许多软件包也提供打包源文件,用户可以构建自己定制版的软件包,但操作的复杂程度以及维护的成本让许多用户望而却步

答案

由于自身能力的局限以及Nix(OS)的实际使用经验为零,对其特性的理解还停留在粗浅的阶段,但这并不妨碍我从感性角度去体会Nix(OS)的价值点。就我主观看来,通过创新性的使用Declarative和Immutable等特性,Nix(OS)优雅的解决了在传统发行版中困扰系统管理员的变更管理这项挑战是其最大的突破和价值点。

对于想使用声明式的方法来管理Linux操作系统,以及需要频繁维护和变更系统的用户来讲,Nix(OS)都是不错的选择。个人桌面和大规模服务器集群都能够找到适合它的使用场景。Nix(OS)并不适合推荐给Linux新人,过高的门槛和缺乏横向对比导致的价值点模糊导致劝退属性拉满。它适合有一定Linux管理和运维经验,不满足于传统发行版的局限性,试图想要改进现状并且有一定折腾能力的Power User。

结局

尽管有种种优点,我在现实中却暂时没找到它的用武之地。在服务器端线上生产环境已经有了一整套的自动化服务器管理流程,在没有吃透NixOS之前贸然进行改造出现问题多半搞不定。在桌面端我本地的Chrome OS + Crostini环境也很难把它用起来。

所以在初略看过一眼之后,我就放弃了进一步的学习,把Nix(OS)定位为也许某一天会派上用场的技术,希望将来有空了再来深入了解。

关于Kubernetes Master高可用的一些策略

Kubernetes高可用也许是完成了初步的技术评估,打算将生产环境迁移进Kubernetes集群之前普遍面临的问题。 为了减少因为服务器当机引起的业务中断,生产环境中的业务系统往往已经做好了高可用,而当引入Kubernetes这一套新的集群管理系统之后, 服务器不再是单一的个体,位于中央位置的Kubernetes Master一旦中断服务,将导致所有Node节点均不可控,有可能造成严重的事故。

总体来讲这是一个被多次讨论,但暂时没有形成统一解决方案的话题。今天主要介绍一些Kubernetes Master高可用的策略,供大家参考。

一个小目标

高可用是复杂的系统工程。出于篇幅的考虑以及能力的限制,今天我们先关注一个小目标:所有的Kubernetes Master服务器没有单点故障,任何一台服务器当机均不影响Kubernetes的正常工作。

实现这一目标带来的直接收益是我们可以在不影响业务正常运行的前提下实现所有服务器的滚动升级,有助于完成系统组件升级以及安全补丁的下发。

为了实现没有单点故障的目标,需要为以下几个组件建立高可用方案:

这些组件的关系可参考下面这张集群架构示意图。

下面为大家逐个详细介绍各个组件的高可用策略。

etcd高可用

etcd是Kubernetes当中唯一带状态的服务,也是高可用的难点。Kubernetes选用etcd作为它的后端数据存储仓库正是看重了其使用分布式架构,没有单点故障的特性。

虽然单节点的etcd也可以正常运行。但是推荐的部署方案均是采用3个或者5个节点组成etcd集群,供Kubernetes使用。

大家常使用的kubeadm工具默认是在一个单节点上启动etcd以及所有的Master组件。虽然使用起来非常方便,但是要用到生产环境还是要注意这个节点当机的风险。

etcd的高可用基本有三种思路:

一是使用独立的etcd集群,使用3台或者5台服务器只运行etcd,独立维护和升级。甚至可以使用CoreOS的update-enginelocksmith,让服务器完全自主的完成升级。这个etcd集群将作为基石用于构建整个集群。 采用这项策略的主要动机是etcd集群的节点增减都需要显式的通知集群,保证etcd集群节点稳定可以更方便的用程序完成集群滚动升级,减轻维护负担。

二是在Kubernetes Master上用static pod的形式来运行etcd,并将多台Kubernetes Master上的etcd组成集群。 在这一模式下,各个服务器的etcd实例被注册进了Kubernetes当中,虽然无法直接使用kubectl来管理这部分实例,但是监控以及日志搜集组件均可正常工作。在这一模式运行下的etcd可管理性更强。

三是使用CoreOS提出的self-hosted etcd方案,将本应在底层为Kubernetes提供服务的etcd运行在Kubernetes之上。 实现Kubernetes对自身依赖组件的管理。在这一模式下的etcd集群可以直接使用etcd-operator来自动化运维,最符合Kubernetes的使用习惯。

这三种思路均可以实现etcd高可用的目标,但是在选择过程中却要根据实际情况做出一些判断。简单来讲预算充足但保守的项目选方案一, 想一步到位并愿意承担一定风险的项目选方案三。折中一点选方案二。各个方案的优劣以及做选择过程中的取舍在这里就不详细展开了,对这块有疑问的朋友可以私下联系交流。

kube-apiserver高可用

apiserver本身是一个无状态服务,要实现其高可用相对要容易一些,难点在于如何将运行在多台服务器上的apiserver用一个统一的外部入口暴露给所有Node节点。

说是难点,其实对于这种无状态服务的高可用,我们在设计业务系统的高可用方案时已经有了相当多的经验积累。需要注意的是apiserver所使用的SSL证书要包含外部入口的地址,不然Node节点无法正常访问apiserver。

apiserver的高可用也有三种基本思路:

一是使用外部负载均衡器,不管是使用公有云提供的负载均衡器服务或是在私有云中使用LVS或者HaProxy自建负载均衡器都可以归到这一类。 负载均衡器是非常成熟的方案,在这里略过不做过多介绍。如何保证负载均衡器的高可用,则是选择这一方案需要考虑的新问题。

二是在网络层做负载均衡。比如在Master节点上用BGPECMP,或者在Node节点上用iptables做NAT都可以实现。采用这一方案不需要额外的外部服务,但是对网络配置有一定的要求。

三是在Node节点上使用反向代理对多个Master做负载均衡。这一方案同样不需要依赖外部的组件,但是当Master节点有增减时,如何动态配置Node节点上的负载均衡器成为了另外一个需要解决的问题。

从目前各个集群管理工具的选择来看,这三种模式都有被使用,目前还没有明确的推荐方案产生。建议在公有云上的集群多考虑第一种模式,在私有云环境中由于维护额外的负载均衡器也是一项负担,建议考虑第二种或是第三种方案。

kube-controller-manager与kube-scheduler高可用

这两项服务是Master节点的一部分,他们的高可用相对容易,仅需要运行多份实例即可。这些实例会通过向apiserver中的Endpoint加锁的方式来进行leader election, 当目前拿到leader的实例无法正常工作时,别的实例会拿到锁,变为新的leader。

目前在多个Master节点上采用static pod模式部署这两项服务的方案比较常见,激进一点也可以采用self-hosted的模式,在Kubernetes之上用DaemonSet或者Deployment来部署。

Kube-dns高可用

严格来说kube-dns并不算是Master组件的一部分,因为它是可以跑在Node节点上,并用Service向集群内部提供服务的。但在实际环境中, 由于默认配置只运行了一份kube-dns实例,在其升级或是所在节点当机时,会出现集群内部dns服务不可用的情况,严重时会影响到线上服务的正常运行。

为了避免故障,请将kube-dns的replicas值设为2或者更多,并用anti-affinity将他们部署在不同的Node节点上。这项操作比较容易被疏忽,直到出现故障时才发现原来是kube-dns只运行了一份实例导致的故障。

总结

上面介绍了Kubernetes Master各个组件高可用可以采用的策略。其中etcd和kube-apiserver的高可用是整个方案的重点。由于存在多种高可用方案,集群管理员应当根据集群所处环境以及其他限制条件选择适合的方案。

这种没有绝对的通用方案,需要集群建设者根据不同的现状在多个方案中做选择的情况在Kubernetes集群建设过程中频频出现, 也是整个建设过程中最有挑战的一部分。容器网络方案的选型作为Kubernetes建设过程中需要面对的另外一个大问题也属于这种情况,今后有机会再来分享这个话题。

在实际建设过程中,在完成了上述四个组件的高可用之后,最好采取实际关机检验的方式来验证高可用方案的可靠性,并根据检验的结果不断调整和优化整个方案。

此外将高可用方案与系统自动化升级方案结合在一起考虑,实现高可用下的系统自动升级,将大大减轻集群的日常运维负担,值得投入精力去研究。

虽然本篇主要在讲Kubernetes Master高可用的方案,但需要指出的是,高可用也并不是必须的,为了实现高可用所付出的代价并不低, 需要有相应的收益来平衡。对于大量的小规模集群来说,业务系统并没有实现高可用,贸然去做集群的高可用收益有限。这时采用单Master节点的方案,做好etcd的数据备份,不失为理性的选择。

再见ELK,您好fluent-bit-aliyun

ELKElasticsearchLogstashKibana的缩写,是我们在处理日志时最常用到的方案。其中Logstash负责日志采集, Elasticsearch负责日志存储,Kibana负责日志展示。三款开源项目分工合作,提供了完整的解决方案。 此外也有使用Fluentd替换Logstash组成的EFK方案,同样也非常受欢迎。

针对不同的环境,已经有大量的文档详细介绍了安装和配置的方法。在Kubernetes环境中,管理员甚至可以使用一键部署脚本完成安装。 这些总结下来的经验极大的降低了ELK的上手门槛,运维人员可以很方便的开始将所有服务器产出日志统一的搜集起来。

但在使用了一段时间之后,随着数据量的增加以及集群规模的扩大,维护一套高效运转的ELK系统所需要付出的运维成本在逐渐增大。 管理员将面临以下几个挑战:

  • 多种不同应用的日志格式不同,需要为不同的应用配置专门的日志解析器
  • 在所有服务器上更新组件版本以及配置带来的运维工作量的增加
  • 单机版本的Elasticsearch的性能跟不上日志产出的速度,需要集群化部署ES
  • ES集群的搭建和管理过程中的复杂度对运维人员的能力要求较高。过度依赖脚本和教程的工程师可能无法顺利完成
  • ES消耗的IO,CPU,内存资源均较高。为了能够提供足够的日志处理能力,ELK所需要的计算资源投入对于小型团队来说是不小的负担
  • ELK方案中缺少日志归档,持久保存的功能。而ES的存储能力受集群规模的限制无法无限扩张。管理员需要面临删除老数据或是研发数据导出存档功能的选择

在Kubernetes环境中,使用k8s所提供的调度功能和ConfigMap所提倡的配置管理最佳实践,再配合上elasticsearch-operator这样的工具, 可以大大降低日常的运维负担,但在算力消耗以及成本增加的问题上,能够带来的改善有限。

对于小型项目,我们需要更加轻量更加经济的解决方案,将日志管理SaaS化,交给合适的供应商来提供,用户按需付费可能是更适合的解决方案。

阿里云日志服务

阿里云提供的日志服务是一套完整的日志管理解决方案。它提供的搜集、消费、存储、查询、归档等功能基本覆盖了日志管理绝大部分的需求。 具体的功能清单如下图所示,在阿里云的网站上有更加详细的介绍,这里就不进一步展开了。

阿里云日志服务对运行在阿里云上的服务器有原生的支持,但是对于Kubernetes下的容器环境的支持有限,此外对于非阿里云服务器, 用户需要自己完成配置和对接。为了解决容器环境的日志搜集以及方便大量的非阿里云用户使用阿里云日志服务, 我们为fluent-bit开发了插件来支持向阿里云日志服务输出日志。

fluent-bit-aliyun

fluent-bitfluentd来自同一家公司。fluent-bit使用C语言开发,比使用Ruby开发的fluentd性能更好,资源占用更低。 作为一个新项目,虽然目前支持的插件还没有fluentd丰富,但已经有不少团队开始在生产环境中使用它。

fluent-bit-aliyun是使用Go语言开发的fluent-bit插件,通过API调用将日志输出到阿里云日志服务。 项目地址在https://github.com/kubeup/fluent-bit-aliyun

为了方便使用,我们提供了打包好的Docker镜像,在https://hub.docker.com/r/kubeup/fluent-bit-aliyun/

在Docker环境中安装

Docker原生支持fluentd格式日志输出。我们可以在容器中运行fluent-bit-aliyun,然后在启动新容器时进行配置将日志发送给它即可。

$ docker run -d --network host -e ALIYUN_ACCESS_KEY=YOUR_ACCESS_KEY -e ALIYUN_ACCESS_KEY_SECRET=YOUR_ACCESS_KEY_SECRET -e ALIYUN_SLS_PROJECT=YOUR_PROJECT -e ALIYUN_SLS_LOGSTORE=YOUR_LOGSTORE -e ALIYUN_SLS_ENDPOINT=cn-hangzhou.log.aliyuncs.com kubeup/fluent-bit-aliyun:master /fluent-bit/bin/fluent-bit -c /fluent-bit/etc/fluent-bit-forwarder.conf -e /fluent-bit/out_sls.so
$ docker run --log-driver=fluentd -d nginx

如果在启动Docker Daemon时进行配置,还可以默认将所有日志发送到阿里云日志服务。

在Kubernetes环境中安装

在Kubernetes环境中,我们使用DaemonSet在集群中的所有Node上部署fluent-bit-aliyun,它将搜集每台服务器上所有Pod所输出的日志。 fluent-bit内置的kubernetes过滤器会将Pod的元数据附加到日志上。

首先,我们创建一个新的Secret来保存所有的配置信息:

$ kubectl create secret generic fluent-bit-config --namespace=kube-system --from-literal=ALIYUN_ACCESS_KEY=YOUR_ACCESS_KEY --from-literal=ALIYUN_ACCESS_KEY_SECRET=YOUR_ACCESS_KEY_SECRET --from-literal=ALIYUN_SLS_PROJECT=YOUR_PROJECT --from-literal=ALIYUN_SLS_LOGSTORE=YOUR_LOGSTORE --from-literal=ALIYUN_SLS_ENDPOINT=cn-hangzhou.log.aliyuncs.com

接下来部署DaemonSet:

$ kubectl create -f https://raw.githubusercontent.com/kubeup/fluent-bit-aliyun/master/fluent-bit-daemonset.yaml

我们可以使用kubectl来检查部署情况:

$ kubectl get pods --namespace=kube-system

在阿里云中查看日志

当日志发送到阿里云之后,可以通过管理界面的日志预览功能确认日志搜集和发送的正确性。

在日志查询中开启索引后,可以进行复杂的过滤和查询。

配置日志归档

阿里云还提供了在ELK方案中缺失的归档功能,只需要简单配置即可开通。

具体的设置方案以及其他相关功能,在阿里云有详细的文档说明,这里就不过多展开了。

总结

本文介绍了使用ELK管理日志可能遇到的挑战,同时提出了新的基于阿里云日志服务以及fluent-bit-aliyun管理日志的办法。 新的办法有如下特点:

  • 不再依赖Elasticsearch,减少大量计算资源消耗。fluent-bit的资源占用也远远低于fluentd,可以随着任务Pod部署
  • 当负载增加时,仅需要在阿里云添加更多的Shard即可,伸缩性更好
  • SaaS模式的计费方式,根据使用量计费,大部分情况下可以降低运行成本
  • 依赖阿里云日志服务的扩展功能,可以实现基于日志的消息处理总线,架构上更加灵活
  • 基于开源系统搭建,仅替换了fluent-bit的输出插件,可以复用input和filter插件。当需要切换后端时,前端无需修改
  • 内置归档功能。只需要简单配置,即可将日志输出到OSS长期保存
  • 同时支持阿里云ECS以及阿里云以外的服务器,对于阿里云以外的服务器,可以将阿里云日志服务作为SaaS来使用

fluent-bit-aliyun的项目地址在https://github.com/kubeup/fluent-bit-aliyun,欢迎大家试用和反馈。

基于Kubernetes的分布式压力测试方案

压力测试是用来检测系统承载能力的有效手段。在系统规模较小的时候,在一台空闲的服务器上使用abwrksiege 等工具发起一定量的并发请求即可得到一个初步的测试结果。但在系统复杂度逐步提高,特别是引入了负载均衡, 微服务等架构后,单机的压力测试方案不再可用,企业需要搭建分布式测试集群或者付费使用外部供应商提供的压力测试服务。

不管是采取自主搭建或是采用外购的手段,都会面临系统使用率不高以及成本的问题。基于Kubernetes的动态资源调度功能, 以及Kubernetes集群的动态伸缩特性,我们可以充分利用集群内的闲置计算资源,在需要进行压力测试时启动测试节点, 在测试结束后释放资源给其他业务,甚至通过集群扩容和缩容临时为压力测试提供更多的计算资源。

支持分布式部署的压力测试工具有多款,今天我们将介绍在Kubernetes集群中使用Tsung进行压力测试的方法。

Tsung

Tsung是一款使用Erlang开发的分布式压力测试系统,它支持HTTP,Jabber,MySQL等多种协议,可以用于不同场景的压力测试。 与传统的针对单一测试目标重复请求的压测系统不同,Tsung更侧重于模拟真实使用场景。测试人员指定新用户到访频率, 并设定一系列的模拟操作请求。所有的Slave节点将在Master节点的统一调度下,按照到访频率创建虚拟用户,并发送操作请求。 所有请求的耗时以及错误信息将传回Master节点用于统计和报表。

选择Tsung主要有三方面的考虑:

  • 性能优越。Erlang语言天生就是为高并发网络系统设计的。合理配置的Tsung集群可以实现100W以上的并发流量。
  • 描述式的配置方法。不论简单还是复杂,Tsung均统一使用XML文件描述整个测试步骤以及各种参数。这样可以在集群架构保持不变时完成各种测试。
  • 模拟真实用户的测试理念。在真实场景中,用户会访问系统的各项功能。只有支持模拟真实用户的压力测试系统才能比较准确的反应系统各个部分在压力下的状态,找到瓶颈环节。

由于Tsung采取的工作模式是在配置中注明Slave地址,然后由Master连上Slave完成测试,传统的部署方法是启动多台物理机或者虚拟机, 分别配置它们。在这种工作模式下,会产生大量的运维工作,同时这些计算资源在不进行测试时处于闲置状态,降低了硬件使用率。

在Kubernetes中使用容器运行Tsung

利用Kubernetes强大的调度能力,我们可以将Tsung运行在容器当中,动态的启动和删除。当需要提高测试规模时, 我们仅需要使用Archon等已有的工具对集群进行扩容,就可以很方便的一键扩容Slave的数量,几乎没有带来任何的运维负担。

以下是具体的操作流程:

创建Namespace

$ kubectl create namespace tsung

使用StatefulSet部署Tsung Slave

这里不能使用Deployment,只有使用StatefulSet才能在为每一个Pod分配独立的内部域名,供Master连接。

将以下文件保存为tsung-slave-svc.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    run: tsung-slave
  name: tsung-slave
spec:
  clusterIP: None
  selector:
    run: tsung-slave
  ports:
  - port: 22
  type: ClusterIP

将以下文件保存为tsung-slave.yaml

apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: tsung-slave
spec:
  serviceName: "tsung-slave"
  replicas: 1
  template:
    metadata:
      labels:
        run: tsung-slave
    spec:
      containers:
      - name: tsung
        image: ddragosd/tsung-docker:1.6.0
        env:
        - name: SLAVE
          value: "true"

在Kubernetes中创建相应的资源

$ kubectl create -f tsung-slave-svc.yaml --namespace tsung
$ kubectl create -f tsung-slave.yaml --namespace tsung

这里我们设置了StatefulSetserviceName字段,这样启动的Pod在集群内部就可以通过tsung-slave-0.tsung-slave.tsung.svc.cluster.local 这个域名访问到。

使用StatefulSet部署Tsung Master

与Slave类似,Master节点也要求可以在集群内部通过域名访问。所以我们依然需要使用StatefulSet来运行。

将以下文件保存为tsung-config.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: tsung-config
data:
  config.xml: |
    <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE tsung SYSTEM "/usr/share/tsung/tsung-1.0.dtd" []>
    <tsung loglevel="warning">
      <clients>
        <client host="tsung-slave-0.tsung-slave.tsung.svc.cluster.local" />
      </clients>
      <servers>
        <server host="target" port="8000" type="tcp"/>
      </servers>
      <load>
        <arrivalphase phase="1" duration="1" unit="minute">
          <users arrivalrate="100" unit="second"/>
        </arrivalphase>
      </load>
    <sessions>
      <session name="es_load" weight="1" type="ts_http">
        <for from="1" to="10" incr="1" var="counter">
          <request> <http url="/" method="GET" version="1.1"></http> </request>
        </for>
      </session>
    </sessions>
    </tsung>

将以下文件保存为tsung-master-svc.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    run: tsung-master
  name: tsung-master
spec:
  clusterIP: None
  selector:
    run: tsung-master
  ports:
  - port: 8091
  sessionAffinity: None
  type: ClusterIP

将以下文件保存为tsung-master.yaml

apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: tsung-master
spec:
  serviceName: "tsung-master"
  replicas: 1
  template:
    metadata:
      labels:
        run: tsung-master
    spec:
      containers:
      - name: tsung
        image: ddragosd/tsung-docker:1.6.0
        env:
        - name: ERL_SSH_PORT
          value: "22"
        args:
        - -k
        - -f
        - /tsung/config.xml
        - -F
        - start
        volumeMounts:
        - mountPath: /tsung
          name: config-volume
      volumes:
      - configMap:
          name: tsung-config
        name: config-volume

在Kubernetes中创建相应的资源

$ kubectl create -f tsung-config.yaml --namespace tsung
$ kubectl create -f tsung-master-svc.yaml --namespace tsung
$ kubectl create -f tsung-master.yaml --namespace tsung

当Tsung Master的容器被启动后,它会自动开始运行压力测试。在上面的列子中,Tsung将向http://target:8000发起为期1分钟的压力测试, 在测试期间,每秒钟产生100个模拟用户,每个用户访问10次目标地址。

我们将Tsung的配置文件用ConfigMap注入到了Master容器当中,这样用户仅需要修改tsung-config.yaml的内容, 就可以方便的定义符合自己要求的测试。在实际使用过程中,用户可以自主调整测试持续时间,虚拟用户产生速度, 目标地址等参数。用户还可以通过修改tsung-slave.yamlreplicas的数值,并将更多的Slave地址加入到tsung-config.yaml当中, 来获得更多的测试资源,进一步增加负载量。

在Master的运行参数中,我们使用的-k参数将使得Master在测试完成后仍处于运行状态,这样用户可以通过8091端口访问到测试结果。

$ kubectl port-forward tsung-master-0 -n tsung 8091:8091

之后在本地通过浏览器访问http://localhost:8091即可打开Tsung内置的报表界面。如下图所示:

另外-F参数让Master使用FQDN地址访问Slave节点,这项参数非常关键,缺少它将导致Master无法正常连接上Slave。

资源回收

测试结束后,用户可以使用报表界面查看和保存结果。当所有结果被保存下来之后,可以直接删除Namespace完成资源回收。

$ kubectl delete namespace tsung

这样所有的Tsung相关配置和容器均会被删除。当下次需要测试时,可以从一个全新的状态开始新一次测试。

总结

本文主要介绍了在Kubernetes中部署Tsung这款分布式压力测试系统的方法。其中使用StatefulSet配合-F参数的方法, 使得Master和Slave可以顺利的使用域名找到对方,成功的解决了在容器中运行Tsung会遇到的访问问题。

原本需要专业的运维工程师投入不少时间才能搭建起来的Tsung测试集群,在Kubernetes中几乎可以毫不费力的启动起来, 完成测试。这种使用调度器充分利用集群空闲资源,使用后及时释放供其他系统使用的方法,也充分体现了Kubernetes的优越性。

在下一篇分享中,我们将使用本文所描述的测试系统,对主流的Python WSGI服务器进行压力测试,用以对比各个服务器的性能指标。 希望通过这种实战演示的方式,帮助大家深入了解Tsung以及Kubernetes。敬请期待。