在华为云使用 Kubeadm 搭建单 Master 集群

在优麒麟的黑客松活动中,使用了华为云的 ECS 作为实验环境载体。在前期测试中,华为云自己提供的 1.9 版本的 Kubernetes 服务,不知道是因为版本过低还是 Api Server 参数配置的缘故,无法使 Pod 内获得完全的 root 权限,导致优麒麟的镜像无法正常启动,因此采取了自建 Kube 集群的方式。

目前自建的是 1.13.2 版本的单 Master 集群,采用的工具是 Kubeadm,流程也可以参考官方文档:https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/

准备工作

创建华为云 ECS 实例若干,确保所有机器网络互通,关闭 SELinux、swap。

在所有的机器中安装依赖:

Docker

curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun

kubeadm 和 kubelet

首先添加国内源(注意这个 apt key 需要科学上网才能拿到)

apt-get update && apt-get install -y apt-transport-https curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
cat <<EOF > /etc/apt/sources.list.d/kubernetes.list
deb http://mirrors.ustc.edu.cn/kubernetes/apt kubernetes-xenial main
EOF
apt-get update

apt-get install kubelet=1.13.2-00 kubeadm=1.13.2-00 -y

查看 Kube 组件依赖的镜像

# kubeadm config images list
k8s.gcr.io/kube-apiserver:v1.13.2
k8s.gcr.io/kube-controller-manager:v1.13.2
k8s.gcr.io/kube-scheduler:v1.13.2
k8s.gcr.io/kube-proxy:v1.13.2
k8s.gcr.io/pause:3.1
k8s.gcr.io/etcd:3.2.24
k8s.gcr.io/coredns:1.2.6

k8s.gcr.io 在国内是无法访问的,可以曲线救国 pull 好并缓存在机器上。

CoreDNS 的镜像可以在 DockerHub 中拉取:

docker pull coredns/coredns:1.2.6
docker tag coredns/coredns:1.2.6 k8s.gcr.io/coredns:1.2.6

剩余的镜像在 DaoCloud 可以拉取到:

# docker images | grep gcr-mirror
daocloud.io/gcr-mirror/kube-controller-manager-amd64   v1.13.2             b9027a78d94c        3 months ago        146MB
daocloud.io/gcr-mirror/kube-proxy-amd64                v1.13.2             01cfa56edcfc        3 months ago        80.3MB
daocloud.io/gcr-mirror/kube-apiserver-amd64            v1.13.2             177db4b8e93a        3 months ago        181MB
daocloud.io/gcr-mirror/kube-scheduler-amd64            v1.13.2             3193be46e0b3        3 months ago        79.6MB
daocloud.io/gcr-mirror/etcd-amd64                      3.2.24              3cab8e1b9802        7 months ago        220MB
daocloud.io/gcr-mirror/kubernetes-dashboard-amd64      v1.8.3              0c60bcf89900        14 months ago       102MB
daocloud.io/gcr-mirror/pause-amd64                     3.1                 da86e6ba6ca1        16 months ago       742kB

修改 kubelet 的配置

首先是确定 docker 的 Cgroup Driver,如果不是 systemd,则需要修改 kubelet 的配置。

# docker info | grep -i cgroup
Cgroup Driver: cgroupfs

第二是因为优麒麟镜像需要特权执行,因此需要添加允许 Pod 使用特权的 flag 参数。综上,修改 kubelet 的配置文件:

# cat /etc/default/kubelet
KUBELET_EXTRA_ARGS="--cgroup-driver=cgroupfs --allow-privileged=true"

启动和配置集群

创建 master 节点(xxx.xxx.xxx.xxx,为该节点外网 ip,以支持外网访问):

kubeadm init  --pod-network-cidr=10.244.0.0/16 --apiserver-advertise-address=0.0.0.0 --apiserver-cert-extra-sans=xxx.xxx.xxx.xxx

init 完成后可以根据指示配置 kubeconfig,然后就可以在当前机器使用 kbuectl 控制集群了。

因为 init 时在证书里包含了外网地址,将 kubeconfig 下载到本地,并修改其中的 api server 地址为外网 ip 可以在本地访问该集群。

配置网络插件,因为目前没有需求使用网络隔离,因此使用了坑少的 flannel:

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

配置 Node 节点,可以用个 kubeadm 来生成 bootstrap token 以接入 node:

# kubeadm token create --print-join-command
kubeadm join 192.168.0.165:6443 --token eu6d1j.zax8lj1sfhy***** --discovery-token-ca-cert-hash sha256:6d29f0a87379711da7012c7b3f1e50af4eb32186e92a4e2fd89c58e30bd*****

直接在 node 执行稍等片刻便可接入。

踩坑

华为云的 DNS 配置有些奇怪,/etc/resolv.conf 中的 DNS 为 127.0.0.1,因此导致集群中的 CoreDNS 异常:CoreDNS 将主机中 /etc/resolv.conf 的 DNS 作为上游服务器,如果是 127.0.0.1,发生了循环。

[FATAL] plugin/loop: Seen "HINFO IN 8205794187887631643.5216586587165434789." more than twice, loop detected

讨论:https://github.com/coredns/coredns/issues/1986

集群中的 Pod 默认使用 CoreDNS 作为 DNS 服务器,因此现象是网络连接不正常(打不开网页,但是直接访问 ip 是正常的)。目前解决方式是在 CoreDNS 的 ConfigMap 中手动指定 114.114.114.114 作为上游服务器(也可以直接修改 /etc/resolv.conf)。

其他基础设施

2019/4/26 posted in  Kubernetes

Kaniko:无需特权在 Kubernetes 中构建镜像

Kaniko 是 Google 造的轮子之一,用于在 Kubernetes 上无需特权的构建 docker image,在 github(https://github.com/GoogleContainerTools/kaniko) 中,是这样介绍的:

kaniko is a tool to build container images from a Dockerfile, inside a container or Kubernetes cluster.

工作原理

传统的 Docker build 是 Docker daemon 根据 Dockerfile,使用特权用户(root)在宿主机依次执行,并生成镜像的每一层:

而 Kaniko 工作原理和此类似,也是按顺序执行每条命令,每条命令执行完毕后为文件系统做快照(snapshot)。并与上一个快照进行对比,如果发现任何不一致,变回创建一个新的层级,并将任何修改都写入镜像的元数据中。

当 Dockerfile 中每条命令都执行完毕后,Kaniko将新生成的镜像 push 到指定的 registry。

使用

Kaniko 解决了在 Kubernetes 构建的问题,但是构建的项目、目标 registry 的认证、Dockerfile 的分发,还是需要我们自己考虑。为了简单,我直接把项目代码、Dockerfile 放在某个 node 的 /root 下。

# pwd
/root/flask-demo
# ls
Dockerfile  README.md  app.py  requirements.txt  test.py

Dockerfile:

FROM python:3.6
RUN mkdir /catking
WORKDIR /catking
COPY ./requirements.txt /catking

RUN pip install -r requirements.txt -i https://pypi.douban.com/simple/
COPY . /catking

CMD ["python", "app.py"]

EXPOSE 5000

首先是解决目标 registry 的认证问题,官方文档中的样例是通过添加一个kaniko-secret.json 并把内容赋值给 GOOGLE_APPLICATION_CREDENTIALS 这个环境变量,如果是自建 registry 可以直接使用 docker config。

# echo "{\"auths\":{\"172.16.105.1\":{\"username\":\"username\",\"password\":\"password\"}}}" > config.json
# kubectl create configmap docker-config --from-file=config.json
configmap/docker-config created

使用 Pod 构建镜像:

apiVersion: v1
kind: Pod
metadata:
  name: kaniko
spec:
  containers:
  - name: kaniko
    image: daocloud.io/gcr-mirror/kaniko-project-executor:latest
    args: ["--dockerfile=/demo/Dockerfile",
            "--context=/demo",
            "--insecure=true",
            "--skip-tls-verify=true",
            "--destination=172.16.105.1/demo/flask-web:v1"]
    volumeMounts:
      - name: docker-config
        mountPath: /kaniko/.docker/
      - name: project-volume
        mountPath: /demo
  restartPolicy: Never
  volumes:
    - name: docker-config
      configMap:
        name: docker-config
    - name: project-volume
      hostPath:
        path: /root/flask-demo
  nodeSelector:
    k8s.ihypo.net/build: kaniko

构建日志

镜像

补充

GCR 镜像获取不到

可以将

gcr.io/kaniko-project/executor
gcr.io/kaniko-project/warmer

替换为

daocloud.io/gcr-mirror/kaniko-project-executor
daocloud.io/gcr-mirror/kaniko-project-warmer

Debug

可以使用 debug 镜像进入 debug 模式:

daocloud.io/gcr-mirror/kaniko-project-executor:debug

构建缓存

可以使用 --cache=true 开启构建缓存,如果是本地缓存,将会使用 --cache-dir 定义的缓存目录。
也可以使用 --cache-repo 参数来指定用来缓存的远程仓库。

遇到的问题

  1. 构建成功后有 push 失败的情况且原因不明
  2. Harbor 作为目标 registry 的时候,在 Web UI 看不到镜像(https://github.com/GoogleContainerTools/kaniko/issues/539

Build on Kube

更多在 Kube 上构建镜像的讨论,请见:https://github.com/kubernetes/kubernetes/issues/1806

2019/1/29 posted in  Kubernetes

入门向:Istio 与服务网格

近期,服务网格(Service Mesh)越加流行红火,各类社区讨论也层出不穷。面对如此火热的技术,我们不免有些疑问:服务网格究竟是什么,服务网格解决了什么?本文尝试简单讲解服务网格的架构设计,并介绍其流行解决方案 Istio。

从分布式系统聊起

现代的应用已经很少采用单体架构了,当分布式架构成为主流,系统组件间的网络调用变成了自然而然的问题。

当然服务数目比较少的时候,服务可以通过配置文件来记录其他服务的网络位置,进行调用时只需要读取配置即可,但当系统越来越庞大、自动化程度越来越高,配置文件这种方式便会成为一种负担。因此会引入 服务中心 来统一管理所有的服务,类似一个系统级的 DNS,来帮助某个服务来找到所依赖的服务。

上图便是一种常见的服务中心流程,Spring 全家桶中的 Eureka 便是采取这种模式。这种模式的特征比较明显:1. 服务会自注册,2. 服务主动去 Service Name System 中查询其他服务的地址。换句话说,服务是知道有服务中心存在的,并且有部分逻辑会侵入代码。

当然,还有不侵入代码的架构方式,就是把服务的注册、发现下沉到基础设施,在宿主机上运行代理进程,服务通过代理对其他组件发起访问。

如上图,服务本身可能并不知道服务中心或者代理的存在,但是整个系统依然拥有了服务注册、服务发现的能力。

服务网格的网格

说起最能体现服务网格 “样子” 的图片,肯定是这一张:

绿色的部分就是我们自己定义的服务,而蓝色的部分,便是 Sidecar。其工作原理,类似于上面提到的第二种服务发现模式,不过是高配版,因为并非在宿主机部署 Proxy,而是每个服务都拥有自己的 Proxy(Sidecar)。

但也只有 Proxy 是不够的,还需要一个 Service Name System,服务网格仅有 Sidecar 也是不够的,还需要一个控制平面:

只不过控制平面并不只是作为注册中心,还有很多强大的功能,下面,我便以具体的服务网格解决方案:Istio 来介绍。

Istio

Istio 服务网格逻辑上分为 数据平面控制平面

  • 数据平面 由一组以 sidecar 方式部署的智能代理组成。这些代理可以调节和控制微服务及 Mixer 之间所有的网络通信。
  • 控制平面 负责管理和配置代理来路由流量。此外控制平面配置 Mixer 以实施策略和收集遥测数据。

官方推荐使用 Envoy 作为 Sidecar。Envoy 是一个 C++ 写的高性能代理,根据官方描述,Envoy 具有动态服务发现、负载均衡、多协议支持等优点,如果是通过 Kubernetes 部署,只需要将 Envoy 和业务服务放在同一个 Pod,经过简单配置,便可接入网格。

Istio 的控制平面采用方便拓展的设计结构,主要由 Pilot、Mixer、Citadel 组件组成,并可以根据自己的需求插拔或者拓展。

Pilot 起到了前文提到的 Service Name System 的作用,担当服务发现、智能路由、流量管控的大任。

Mixer 主要的作用检查和遥测,比如前置条件检查(如认证、白名单等),配额检查(如判断服务的访问频率是否超标等),监控(如链路追踪、日志等)。

也正是因为 Mixer 在整个网络中起到了无微不至的大管家角色,在被人诟病性能问题时首当其冲。不过好在 Mixer 是一个比较独立的组件,如果系统已经有自己比较完善的监控、认证方案,也可以不启用 Mixer。

Istio 中,所有服务间的通讯全部是经过 Sidecar 的,而 Citadel 便是负责两个服务间通讯的安全问题,其提供了终端用户认证、流量加密的能力。

Istio 的组件比较简单,但也就是其简单的架构,帮我们完成和掩盖了大量复杂的事情。当然,Istio 并不是唯一的选择,老牌的 Linkerd,华为、阿里根据自己的需求改进并开源的 SM 解决方案,都是很不错的选择。

如何看待服务网格

如何看待当下火热的服务网格呢?在此之前,我们先看它为我们解决了什么?

  1. 流量管理
  2. 安全
  3. 可观测性

那除此之外,对使用者来说,又带来了什么隐患?

  1. 架构变复杂,运维难度升级
  2. 需要去了解,有一定的学习成本
  3. 每次请求都加了两跳,排错困难、性能隐患

因此,如果引入服务网格不是为了解决当前面临的问题,就没有引入的必要,还是那句老话:“如无必要勿增实体”,何况这个实体还是个黑盒子。不过,如果引入服务网格是为了解决当前的问题,那需要想清楚自己是否承受的起上面提到的隐患,毕竟没有东西会是银弹。

参考

2018/12/11 posted in  Service Mesh

浅谈 WSGI

WSGI 是 Python Web 开发中经常提到的名词,在维基百科中,定义如下:

Web服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。自从WSGI被开发出来以后,许多其它语言中也出现了类似接口。

正如定义,WSGI 不是服务器,不是 API,不是 Python 模块,而是一种规定服务器和客户端交互的 接口规范

WSGI 目标是在 Web 服务器和 Web 框架层之间提供一个通用的 API 标准,减少之间的互操作性并形成统一的调用方式。根据这个定义,满足 WSGI 的 Web 服务器会将两个固定参数传入 WSGI APP:环境变量字典和一个初始化 Response 的可调用对象。而 WSGI APP 会处理请求并返回一个可迭代对象。

WSGI APP

根据定义,我们可以实现一个非常简单的满足 WSGI 的 App:

def demo_wsgi_app(environ, start_response):
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(status, headers)
    yield "Hello World!"

可以看到,该 App 通过 start_response 初始化请求,并通过 yield 将 body 返回。除了 yield,也可以直接返回一个可迭代对象。

在标准库 wsgiref 中已经包含了一个简单的 WSGI APP,可以在 wsgiref.simple_server 中找到,可以看到,这也是在做相同的事情:

def demo_app(environ,start_response):
    from io import StringIO
    stdout = StringIO()
    print("Hello world!", file=stdout)
    print(file=stdout)
    h = sorted(environ.items())
    for k,v in h:
        print(k,'=',repr(v), file=stdout)
    start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')])
    return [stdout.getvalue().encode("utf-8")]

将这个 App 运行起来如下:

在 Django 中,可以在默认 app 下的 wsgi.py 中找到 get_wsgi_application,Django 通过这个方法创建并返回了一个 WSGIHandle,其本质,依然是一个 WSGI APP,可以看其 __call__ 方法:

class WSGIHandler(base.BaseHandler):
    request_class = WSGIRequest

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.load_middleware()

    def __call__(self, environ, start_response):
        set_script_prefix(get_script_name(environ))
        signals.request_started.send(sender=self.__class__, environ=environ)
        request = self.request_class(environ)
        response = self.get_response(request)

        response._handler_class = self.__class__

        status = '%d %s' % (response.status_code, response.reason_phrase)
        response_headers = list(response.items())
        for c in response.cookies.values():
            response_headers.append(('Set-Cookie', c.output(header='')))
        start_response(status, response_headers)
        if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
            response = environ['wsgi.file_wrapper'](response.file_to_stream)
        return response

WSGI 服务器

从 WSGI APP 的写法上就基本能推测出 WSGI 服务器做了什么,因此可以尝试实现一个简陋的 WSGI 服务器:

def run_wsgi_app(app, environ):
    from io import StringIO
    body = StringIO()

    def start_response(status, headers):
        body.write('Status: {}\r\n'.format(status))
        for header in headers:
            body.write("{}: {}".format(*header))
        return body.write

    iterable = app(environ, start_response)
    try:
        if not body.getvalue():
            raise RuntimeError("No exec start_response")
        body.write("\r\n{}\r\n".format('\r\n'.join(line for line in iterable)))
    finally:
        if hasattr(iterable, "close") and callable(iterable.close):
            iterable.close()
    # 这里瞎扯
    return body.getvalue()

对于真正(可用)的 WSGI 服务器,常用的比如 Gunicorn,在不同的 Worker(gunicorn.worker 模块中)中,都实现了一个叫 handle_request 的类方法,这个方法便是调用 WSGI APP,并完成 Response 拼装的,虽然不同的 Worker 的实现略有差异,但比较共通的代码:

respiter = self.wsgi(environ, resp.start_response)
try:
    if isinstance(respiter, environ['wsgi.file_wrapper']):
        resp.write_file(respiter)
    else:
        for item in respiter:
            resp.write(item)
    resp.close()
    request_time = datetime.now() - request_start
    self.log.access(resp, req, environ, request_time)
finally:
    if hasattr(respiter, "close"):
        respiter.close()

这段代码便是调用 WSGI APP,并通过循环把 Body 写入到 resp 中。

中间件

因为 WSGI 的定义方式,可以写多个 WSGI APP 进行嵌套并处理不同的逻辑,比如:

def first_wsgi_app(environ, start_response):
    import logging
    logging.info("new request")
    rsp = second_wsgi_app(environ, start_response)
    logging.info("finish request")
    return rsp


def second_wsgi_app(environ, start_response):
    if environ.get("HTTP_ROLE") == "ADMIN":
        return third_wsgi_app(environ, start_response)
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(status, headers)
    yield "Hello User!"


def third_wsgi_app(environ, start_response):
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(status, headers)
    yield "Hello Admin!"

这时候我们把第一个 WSGI APP first_wsgi_app 传给 Server。在执行时,first_wsgi_app 可以完成日志的记录,second_wsgi_app 可以完成鉴权,third_wsgi_app 来真正的处理请求。

这种 App 的洋葱结构,被正伦亲切的称为俄罗斯套娃。

2018/9/21 posted in  Python 黑魔法

Linux 系统调用

内核提供了用户进程和内核进行交互的一组接口,这些接口在应用程序和内核之间扮演了使者的角色,应用程序发出各种请求,而内核负责满足这些请求,而这些接口,即是 系统调用

作用

系统调用主要起到保护系统稳定可靠运行的作用,避免应用程序肆意妄为。

除此,系统调用还起到了为用户空间进程与硬件设备之间添加一个中间层的作用,该层的主要作用三个:

  1. 为用户空间提供一种硬件的抽象接口,使用户进程不用区分硬件类型
  2. 使得内核可以基于权限、用户累心和其他一些规则对需要进行的访问进行判断
  3. 为用户空间和系统的其余部分提供公共接口

工作原理

一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。在 Unix 中,最流行的应用编程接口是基于 POSIX 标准的,所以 Liunx的系统调用像绝大多数 Unix 一样,作为 C 库的一部分提供:

关于 Unix 的接口设计中有一句格言:“提供机制而不是策略”。换句话说,Unix 的系统调用抽象出了用于完成某种确定的目的的函数,至于这些函数怎么用完全不需要被内核关心。

要访问系统调用(在 Linux 被称为 syscall),通常通过 C 库中定义的函数调用来进行。通常系统调用可以定义零个到多个参数,也会返回一个 long 来标记执行情况或者遇到的错误,而具体的错误信息会写在 error 全局变量中,通过 perror 库函数便可以打印出可读的错误信息。

在 Linux 中每个系统调用都被赋予一个系统调用号(存储在 sys_call_table 中),这样通过独一无二的编号就可以关联系统调用使用户进程不需要提及系统调用的名称。

Linux 系统的系统调用比其他操作系统执行要快的多,首先是因为 Linux 很短的上下文切换时间,其次就是 Linux 的系统调用本身就被设计的非常简洁。

用户空间进程通过系统调用来 “执行” 内核代码,其通知内核的机制是通过软中断实现的: 通过引发一个异常来促使系统切换到内核态去执行异常处理程序 (用户空间引发异常陷入内核)。

在使用系统调用时,除了调用号以外,一般还需要一些外部的参数输入,所以在陷入内核的时候,需要把这些参数从用户空间传入到内核,而最简单的方法就是像传递系统调用号一样,把这些参数也存放在寄存器中。

2018/7/8 posted in  Kernel